[Scummvm-git-logs] scummvm master -> 007741ef41d3cce2d9ec09352c6e654f0c6cc3ff

sev- noreply at scummvm.org
Tue Sep 2 20:07:02 UTC 2025


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

Summary:
2c9c7a16a7 ALCACHOFA: Add initial engine template
b0fda8ff19 ALCACHOFA: Add first detection entry
b2ab2453ae ALCACHOFA: Read world data
ea67e329f3 ALCACHOFA: Add initial animation and OpenGL support
51836a48cd ALCACHOFA: Add Graphic playback methods
1859ad6807 ALCACHOFA: Add DrawQueue and AnimationDrawRequest
15915b5407 ALCACHOFA: Add camera and 3D draw requests
872e5b3715 ALCACHOFA: Add basic room loop and GraphicObject drawing
c5e969cc8a ALCACHOFA: Add SpecialEffectGraphicObject rendering
afe0f02dba ALCACHOFA: Fix sprite prerendering with semi-transparent images
6bfee722ae ALCACHOFA: Fix shape loading and add debug drawing
f84c679788 ALCACHOFA: Add mouse input and Shape::contains
2e8b508a5f ALCACHOFA: Add shape interpolations
8518c4d275 ALCACHOFA: Add path finding
9fd0d74242 ALCACHOFA: Add drawing characters
bc3380d4ac ALCACHOFA: Add walking characters
b0fc780802 ALCACHOFA: Add parts of main characters
49e716eeb0 ALCACHOFA: Initial scheduler and script
adc4328047 ALCACHOFA: Add script execution
f561316bd8 ALCACHOFA: Move updateCommonVariables to Script
cb1c9873f0 ALCACHOFA: Implement easy kernel tasks
0e38466e35 ALCACHOFA: Fix kernel call animate not accepting string as boolean arg
bb2848b371 ALCACHOFA: Move changing state into Player instead of World
24dbc38528 ALCACHOFA: Initialize items
0cf432015c ALCACHOFA: Add game room interaction
4fff63d978 ALCACHOFA: Add door shortcuts
a13f5d8999 ALCACHOFA: Add camera following characters
7df9ae0993 ALCACHOFA: Add technical room changes
df52d04b70 ALCACHOFA: Add various debug commands
c2abfe5b56 ALCACHOFA: Lock characters during script processes
8a7d02239b ALCACHOFA: Fix door cursors
e8539aac3f ALCACHOFA: Add character interaction and fix crash on exit
e116042bc7 ALCACHOFA: Execute object and character scripts on triggering
21812391c8 ALCACHOFA: Add triggering of doors
ae53e521cd ALCACHOFA: Toggle related objects of interactable objects
e28435d1a8 ALCACHOFA: Let camera catch up on room changes
ae9c2d0d21 ALCACHOFA: Add text drawing
72b4c99bf3 ALCACHOFA: Load and display localized names
6938f1c87b ALCACHOFA: Load dialog lines
b4a467c892 ALCACHOFA: Add voice sounds and sayText kernel task
fff9c7cc09 ALCACHOFA: Refactor stream-helper into common
114d88a87e ALCACHOFA: Add dialog menu kernel task
e430e6d46c ALCACHOFA: Fix text blending mode
75a8d917aa ALCACHOFA: Add camera lerp kernel tasks
742c584885 ALCACHOFA: Add LerpCameraToObject kernel tasks
550f23ab4e ALCACHOFA: Reorder and group kernel tasks
97e9bc81b4 ALCACHOFA: Add fades for doors
206c749922 ALCACHOFA: Add inventory
509ebca2ee ALCACHOFA: Add three more kernel tasks
7f23983242 ALCACHOFA: Fix closing game during character processes
f18ced5d41 ALCACHOFA: Add PlayVideo kernel call
8ad4f2e58b ALCACHOFA: Add permanent fade state
30419251dc ALCACHOFA: Ignore additional missing animation
37aabca132 ALCACHOFA: Fix incompatibilities after rebase
d9450568a0 ALCACHOFA: Add ClosestFloorPoint debug mode
43c17c33d0 ALCACHOFA: Improve Polygon::closestPointTo
c52ad25703 ALCACHOFA: Add FloorIntersections debug handler
fc39a1344c ALCACHOFA: Fix floor polygon connections
013ea9e5d8 ALCACHOFA: Decrease texture size of fonts
af87f81354 ALCACHOFA: Add script debug tracing
84bcbbea4e ALCACHOFA: Disable alpha premultiplication, seems to be wrong
356122bb8e ALCACHOFA: Fix object query for some cutscenes
e157840815 ALCACHOFA: Fix loading OFELIA_QUIETA.AN0
c49f2cbfd6 ALCACHOFA: Add two original hard-coded special cases
5fdd0de637 ALCACHOFA: Fix loading room VIA_TREN_ATADOS_NOCHE
6c551fc9ca ALCACHOFA: Add teleport debug handler
01233d57cf ALCACHOFA: Add AnimateTalking kernel call
75201ef028 ALCACHOFA: Add AnimateCharacter kernel call
2bd64cd558 ALCACHOFA: Add PlaySound kernel call
ae5c98228e ALCACHOFA: Add LerpCharacterLodBias kernel call
313a811a40 ALCACHOFA: Add ChangeCharacter kernel call
14d6797687 ALCACHOFA: Refactor fonts into new GlobalUI component
e3625bf330 ALCACHOFA: Refactor inventory UI triggers into GlobalUI
17c4a38686 ALCACHOFA: Add character change button
c97572c371 ALCACHOFA: Speed up room transitions by buffering images
25e4aebd79 ALCACHOFA: Disable OpenGL debugging by default
381872dfda ALCACHOFA: Fix changing character during dialog
3cb5cebc18 ALCACHOFA: Fix Filemon not being rendered
cabcd49607 ALCACHOFA: Fix Drop kernel calls with nullptr item
9e2d1cfbc1 ALCACHOFA: Fix a couple corner cases
b5c5cfaef9 ALCACHOFA: Simplify and fix Player::changeRoom
ce65cf96c2 ALCACHOFA: Workaround bug with invalid PUT target objecgt
71988c9edf ALCACHOFA: Workaround bugs in CASA_FREDDY_ARRIBA
7b08ff53d7 ALCACHOFA: Workaround bugs in HABITACION_DRACULA and MOTEL_ENTRADA
a2266bd22c ALCACHOFA: Fix InteractableObject being door target
eced79308f ALCACHOFA: Use the helper functions more in the kernel procs
72391731e5 ALCACHOFA: Fix entering ESTOMAGO and DINOSAURIO
8e7069d366 ALCACHOFA: Fix return value of kernel calls
171e0af2af ALCACHOFA: Fix ScriptTimerTask
f8e15ea9f4 ALCACHOFA: Clear screen in order to fix ESTOMAGO
9bf99c3f6a ALCACHOFA: Fix text line array being too small
178faf9b47 ALCACHOFA: More exceptions to make game completable
23f316821e ALCACHOFA: Fix out-of-bounds heap access
e9c1594ea2 ALCACHOFA: Fix case of input.cpp and input.h
e2c661a407 ALCACHOFA: Fix texture wrapping
6644e4abde ALCACHOFA: Fix tiling on effect objects
6a7bd63da8 ALCACHOFA: Fix parameter name "center" to "topLeft"
d598530b45 ALCACHOFA: Fix black frames on opening/closing inventory
7e7163768f ALCACHOFA: Update 3D mouse pos always
d72e14505c ALCACHOFA: Fix exception for BACTERIO/PULSAR
bbfcb8f7f5 ALCACHOFA: Fix lens flares outside the western town
d1c56de460 ALCACHOFA: Add main character evasion
cd0dcde22c ALCACHOFA: Add ShowCenterBottomText kernel call
24453e1d01 ALCACHOFA: Reduce unnecessary string allocations
c107f98062 ALCACHOFA: Code conventions - Fix indentations
ea85359693 ALCACHOFA: Code conventions - Pass Point by value
54e1201f92 ALCACHOFA: Fix taking and combining inventory items
7bcb573207 ALCACHOFA: Fix blending modes
22bace3365 ALCACHOFA: Fix invalid door target in LABERINTO
a905192ce6 ALCACHOFA: Fix draw order of hovered object names
ffedb0376c ALCACHOFA: Fix invisible cursor in DINOSAURIO
26a1af2f93 ALCACHOFA: Fix crash with zero-size sound files
a80d819951 ALCACHOFA: Fix hieroglyphic display in 16
94dc9afec8 ALCACHOFA: Fix TELEFRUSKYMATIC in LAB_BACTERIO
181bede30e ALCACHOFA: Fix holding items after pickup
904577adb3 ALCACHOFA: Fix cursor directions for doors
d131608699 ALCACHOFA: Fix camera transitions on character change/inventory close
8f2345f2d9 ALCACHOFA: Fix some camera moves when scaled
0764c495ca ALCACHOFA: Refactor animation-related methods
1fca043e6b ALCACHOFA: Add letter-boxes
e1a0e6a159 ALCACHOFA: Fix floor shapes with lines
5c578daf71 ALCACHOFA: Fix TGADecoder changes after rebase
d2c05d444a ALCACHOFA: Add camera tasks for inactive player
e014523711 ALCACHOFA: Add kernel task CamShake
df191096d2 ALCACHOFA: Clear heldItem on clearInventory
2259614542 ALCACHOFA: Refactor error handling
4aea1d5bf9 ALCACHOFA: Replace TODO comment in WalkingCharacter::draw
dbfa3a5600 ALCACHOFA: Refactor room reading to assert object sizes
8f17fceba2 ALCACHOFA: Add lip-sync
0f167489ae ALCACHOFA: Fix character direction after triggering doors
0f8d9f70ba ALCACHOFA: Add FloorColorDebugHandler
536eb12185 ALCACHOFA: Fix reading floor brightness
39d0d54da0 ALCACHOFA: Increase read buffer for less latency on playing voice lines
2c48107288 ALCACHOFA: Add character lighting
1322ef52ca ALCACHOFA: Add music
4ad1f1f806 ALCACHOFA: Add keymap and input handling to open menu
446ca8765b ALCACHOFA: Open main menu and add MenuButtton
65a7d31541 ALCACHOFA: Add first main menu actions
ee1808a5d1 ALCACHOFA: Add Config class and GUI options
74512630d6 ALCACHOFA: Add CheckBox and initial options menu
563bc6e7cf ALCACHOFA: Implement options menu actions
5eaa3e56c6 ALCACHOFA: Fix options menu arm
cfa7f70811 ALCACHOFA: Add slide buttons
7165397f70 ALCACHOFA: Replace SoundID with SoundHandle
c9d1bd1725 ALCACHOFA: Fix pausing on game menu and ScummVM menus
7e819190f4 ALCACHOFA: Add most of syncGame subroutines
3fb9469d2a ALCACHOFA: Add syncGame for scheduler and all tasks
700eee9de4 ALCACHOFA: Remove virtual on overridden methods
e31e933cb3 ALCACHOFA: Fix various bugs related to saving/loading
6a49931fed ALCACHOFA: Implement canLoadGameStateCurrently
1650b7610c ALCACHOFA: Add loading from in-game menu
59303318d7 ALCACHOFA: Fix assert when loading while walking through door
3143836091 ALCACHOFA: Use MessageDialog for multiplayer warning
a8d936fcd2 ALCACHOFA: Add saving with in-game menu
c58bd40362 ALCACHOFA: Remove getRandomNumber from template
6458f3e871 ALCACHOFA: Replace std::move with Common::move
a73b70adde ALCACHOFA: Add showGraphics debug flag
3d3dda8d63 ALCACHOFA: Render-to-texture and initial savestate thumbnails
4495dc5d90 ALCACHOFA: Decrease savestate size
bb50a4b394 ALCACHOFA: Fix fadeExit
af7cc2c587 ALCACHOFA: Add high-quality check
2778159830 ALCACHOFA: Add key inputs for opening/closing inventory
f4bbb6551a ALCACHOFA: Mark typeName methods as override
1ec9365bd2 ALCACHOFA: Fix compilation warnings and CI errors
a22af48933 ALCACHOFA: Fix additional CI errors
6cd7965a9d ALCACHOFA: Fix even more CI warnings
344d8c9944 ALCACHOFA: Remove redundant semicolon after DECLARE_TASK
83c49b1a95 ALCACHOFA: Fix two CI warnings
8a2c772bcc ALCACHOFA: Remove remaining virtual keyword on overridden methods
395f1d9111 ALCACHOFA: Fix MSVC Analysis warnings
42fe7869d8 ALCACHOFA: Fix configure.engine
4a1b0a6473 ALCACHOFA: Fix black thumbnail after saving in-game
04240a15b7 ALCACHOFA: Fix filemon being able to leave POBALDO_INDIO
330d81a940 ALCACHOFA: Fix some objects being enabled after load
1adac7a86f ALCACHOFA: Add spanish and english steam versions
7cfe04ad78 ALCACHOFA: Handle more dialog line formats
268a6538bb ALCACHOFA: Fix draw order of benter bottom text
4fa0c5a590 ALCACHOFA: Handle video playback errors more gracefully
7275e191a9 ALCACHOFA: Fix loading empty dialog lines
e5400d1190 ALCACHOFA: Add support for german demo
45514217d9 ALCACHOFA: Rename header guards
d4fbf73b72 ALCACHOFA: Remove redundant semicolons
59960db114 ALCACHOFA: Fix crash in fadeExit with gcc builds
bdc4cd1d38 ALCACHOFA: Handle missing voice/music files better
d2e767e75b ALCACHOFA: Probably fix two compiler warnings
603264e965 ALCACHOFA: Use engine name in include paths
820ba7894b ALCACHOFA: Remove scummsys include
85897a520d ALCACHOFA: Fix bracing style
9f774019ae ALCACHOFA: Break up one-liners in switches
e5e42cc7b1 ALCACHOFA: Remove u8 string prefix
b3368f5742 ALCACHOFA: Rename gameid to aventuradecine
999a5b0167 ALCACHOFA: Add ADGF_REMASTERED in preparation for edicion original
07a39679dd ALCACHOFA: Remove temporary saveFileMgr variable
ac843c9fd0 ALCACHOFA: Remove redundant pragma once
917ceb5115 ALCACHOFA: Replace _DEBUG macro usage
32c4ebc08a ALCACHOFA: Fix end-of-file comment
92d0688429 ALCACHOFA: Use luminance for grayscaled thumbnails
17175d0de9 ALCACHOFA: Fix regression error when loading a DelayTask
e45cb342be ALCACHOFA: Remove feature 3d
eb8499e902 ALCACHOFA: Split up OpenGL renderer
4cd127c9ff ALCACHOFA: Add OpenGL shaders support for GLES2 platforms
007741ef41 ALCACHOFA: Fix invalid ODR-usage


Commit: 2c9c7a16a7813acecdebc63dcf74d9784f5f7cb7
    https://github.com/scummvm/scummvm/commit/2c9c7a16a7813acecdebc63dcf74d9784f5f7cb7
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:44+02:00

Commit Message:
ALCACHOFA: Add initial engine template

Changed paths:
  A engines/alcachofa/POTFILES
  A engines/alcachofa/alcachofa.cpp
  A engines/alcachofa/alcachofa.h
  A engines/alcachofa/configure.engine
  A engines/alcachofa/console.cpp
  A engines/alcachofa/console.h
  A engines/alcachofa/credits.pl
  A engines/alcachofa/detection.cpp
  A engines/alcachofa/detection.h
  A engines/alcachofa/detection_tables.h
  A engines/alcachofa/metaengine.cpp
  A engines/alcachofa/metaengine.h
  A engines/alcachofa/module.mk


diff --git a/engines/alcachofa/POTFILES b/engines/alcachofa/POTFILES
new file mode 100644
index 00000000000..271127f50bf
--- /dev/null
+++ b/engines/alcachofa/POTFILES
@@ -0,0 +1 @@
+engines/alcachofa/metaengine.cpp
diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
new file mode 100644
index 00000000000..f8bc232747e
--- /dev/null
+++ b/engines/alcachofa/alcachofa.cpp
@@ -0,0 +1,109 @@
+/* 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 "graphics/framelimiter.h"
+#include "alcachofa/detection.h"
+#include "alcachofa/console.h"
+#include "common/scummsys.h"
+#include "common/config-manager.h"
+#include "common/debug-channels.h"
+#include "common/events.h"
+#include "common/system.h"
+#include "engines/util.h"
+#include "graphics/paletteman.h"
+
+namespace Alcachofa {
+
+AlcachofaEngine *g_engine;
+
+AlcachofaEngine::AlcachofaEngine(OSystem *syst, const ADGameDescription *gameDesc) : Engine(syst),
+	_gameDescription(gameDesc), _randomSource("Alcachofa") {
+	g_engine = this;
+}
+
+AlcachofaEngine::~AlcachofaEngine() {
+	delete _screen;
+}
+
+uint32 AlcachofaEngine::getFeatures() const {
+	return _gameDescription->flags;
+}
+
+Common::String AlcachofaEngine::getGameId() const {
+	return _gameDescription->gameId;
+}
+
+Common::Error AlcachofaEngine::run() {
+	// Initialize 320x200 paletted graphics mode
+	initGraphics(320, 200);
+	_screen = new Graphics::Screen();
+
+	// Set the engine's debugger console
+	setDebugger(new Console());
+
+	// If a savegame was selected from the launcher, load it
+	int saveSlot = ConfMan.getInt("save_slot");
+	if (saveSlot != -1)
+		(void)loadGameState(saveSlot);
+
+	// Draw a series of boxes on screen as a sample
+	for (int i = 0; i < 100; ++i)
+		_screen->frameRect(Common::Rect(i, i, 320 - i, 200 - i), i);
+	_screen->update();
+
+	// Simple event handling loop
+	byte pal[256 * 3] = { 0 };
+	Common::Event e;
+	int offset = 0;
+
+	Graphics::FrameLimiter limiter(g_system, 60);
+	while (!shouldQuit()) {
+		while (g_system->getEventManager()->pollEvent(e)) {
+		}
+
+		// Cycle through a simple palette
+		++offset;
+		for (int i = 0; i < 256; ++i)
+			pal[i * 3 + 1] = (i + offset) % 256;
+		g_system->getPaletteManager()->setPalette(pal, 0, 256);
+		// Delay for a bit. All events loops should have a delay
+		// to prevent the system being unduly loaded
+		limiter.delayBeforeSwap();
+		_screen->update();
+		limiter.startFrame();
+	}
+
+	return Common::kNoError;
+}
+
+Common::Error AlcachofaEngine::syncGame(Common::Serializer &s) {
+	// The Serializer has methods isLoading() and isSaving()
+	// if you need to specific steps; for example setting
+	// an array size after reading it's length, whereas
+	// for saving it would write the existing array's length
+	int dummy = 0;
+	s.syncAsUint32LE(dummy);
+
+	return Common::kNoError;
+}
+
+} // End of namespace Alcachofa
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
new file mode 100644
index 00000000000..ae6eb72cc92
--- /dev/null
+++ b/engines/alcachofa/alcachofa.h
@@ -0,0 +1,105 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef ALCACHOFA_H
+#define ALCACHOFA_H
+
+#include "common/scummsys.h"
+#include "common/system.h"
+#include "common/error.h"
+#include "common/fs.h"
+#include "common/hash-str.h"
+#include "common/random.h"
+#include "common/serializer.h"
+#include "common/util.h"
+#include "engines/engine.h"
+#include "engines/savestate.h"
+#include "graphics/screen.h"
+
+#include "alcachofa/detection.h"
+
+namespace Alcachofa {
+
+struct AlcachofaGameDescription;
+
+class AlcachofaEngine : public Engine {
+private:
+	const ADGameDescription *_gameDescription;
+	Common::RandomSource _randomSource;
+protected:
+	// Engine APIs
+	Common::Error run() override;
+public:
+	Graphics::Screen *_screen = nullptr;
+public:
+	AlcachofaEngine(OSystem *syst, const ADGameDescription *gameDesc);
+	~AlcachofaEngine() override;
+
+	uint32 getFeatures() const;
+
+	/**
+	 * Returns the game Id
+	 */
+	Common::String getGameId() const;
+
+	/**
+	 * Gets a random number
+	 */
+	uint32 getRandomNumber(uint maxNum) {
+		return _randomSource.getRandomNumber(maxNum);
+	}
+
+	bool hasFeature(EngineFeature f) const override {
+		return
+		    (f == kSupportsLoadingDuringRuntime) ||
+		    (f == kSupportsSavingDuringRuntime) ||
+		    (f == kSupportsReturnToLauncher);
+	};
+
+	bool canLoadGameStateCurrently(Common::U32String *msg = nullptr) override {
+		return true;
+	}
+	bool canSaveGameStateCurrently(Common::U32String *msg = nullptr) override {
+		return true;
+	}
+
+	/**
+	 * Uses a serializer to allow implementing savegame
+	 * loading and saving using a single method
+	 */
+	Common::Error syncGame(Common::Serializer &s);
+
+	Common::Error saveGameStream(Common::WriteStream *stream, bool isAutosave = false) override {
+		Common::Serializer s(nullptr, stream);
+		return syncGame(s);
+	}
+	Common::Error loadGameStream(Common::SeekableReadStream *stream) override {
+		Common::Serializer s(stream, nullptr);
+		return syncGame(s);
+	}
+};
+
+extern AlcachofaEngine *g_engine;
+#define SHOULD_QUIT ::Alcachofa::g_engine->shouldQuit()
+
+} // End of namespace Alcachofa
+
+#endif // ALCACHOFA_H
diff --git a/engines/alcachofa/configure.engine b/engines/alcachofa/configure.engine
new file mode 100644
index 00000000000..a5decb9ba59
--- /dev/null
+++ b/engines/alcachofa/configure.engine
@@ -0,0 +1,3 @@
+# This file is included from the main "configure" script
+# add_engine [name] [desc] [build-by-default] [subengines] [base games] [deps]
+add_engine alcachofa "Alcachofa" no "" "" ""
diff --git a/engines/alcachofa/console.cpp b/engines/alcachofa/console.cpp
new file mode 100644
index 00000000000..a64a7fd216f
--- /dev/null
+++ b/engines/alcachofa/console.cpp
@@ -0,0 +1,38 @@
+/* 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/console.h"
+
+namespace Alcachofa {
+
+Console::Console() : GUI::Debugger() {
+	registerCmd("test",   WRAP_METHOD(Console, Cmd_test));
+}
+
+Console::~Console() {
+}
+
+bool Console::Cmd_test(int argc, const char **argv) {
+	debugPrintf("Test\n");
+	return true;
+}
+
+} // End of namespace Alcachofa
diff --git a/engines/alcachofa/console.h b/engines/alcachofa/console.h
new file mode 100644
index 00000000000..f8b2028fefc
--- /dev/null
+++ b/engines/alcachofa/console.h
@@ -0,0 +1,40 @@
+
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef ALCACHOFA_CONSOLE_H
+#define ALCACHOFA_CONSOLE_H
+
+#include "gui/debugger.h"
+
+namespace Alcachofa {
+
+class Console : public GUI::Debugger {
+private:
+	bool Cmd_test(int argc, const char **argv);
+public:
+	Console();
+	~Console() override;
+};
+
+} // End of namespace Alcachofa
+
+#endif // ALCACHOFA_CONSOLE_H
diff --git a/engines/alcachofa/credits.pl b/engines/alcachofa/credits.pl
new file mode 100644
index 00000000000..53df699a73d
--- /dev/null
+++ b/engines/alcachofa/credits.pl
@@ -0,0 +1,3 @@
+begin_section("Alcachofa");
+	add_person("Hermann Noll", "Helco", "");
+end_section();
diff --git a/engines/alcachofa/detection.cpp b/engines/alcachofa/detection.cpp
new file mode 100644
index 00000000000..54d3b03454d
--- /dev/null
+++ b/engines/alcachofa/detection.cpp
@@ -0,0 +1,45 @@
+/* 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 "base/plugins.h"
+#include "common/config-manager.h"
+#include "common/file.h"
+#include "common/md5.h"
+#include "common/str-array.h"
+#include "common/translation.h"
+#include "common/util.h"
+#include "alcachofa/detection.h"
+#include "alcachofa/detection_tables.h"
+
+const DebugChannelDef AlcachofaMetaEngineDetection::debugFlagList[] = {
+	{ Alcachofa::kDebugGraphics, "Graphics", "Graphics debug level" },
+	{ Alcachofa::kDebugPath, "Path", "Pathfinding debug level" },
+	{ Alcachofa::kDebugFilePath, "FilePath", "File path debug level" },
+	{ Alcachofa::kDebugScan, "Scan", "Scan for unrecognised games" },
+	{ Alcachofa::kDebugScript, "Script", "Enable debug script dump" },
+	DEBUG_CHANNEL_END
+};
+
+AlcachofaMetaEngineDetection::AlcachofaMetaEngineDetection() : AdvancedMetaEngineDetection(Alcachofa::gameDescriptions,
+	sizeof(ADGameDescription), Alcachofa::alcachofaGames) {
+}
+
+REGISTER_PLUGIN_STATIC(ALCACHOFA_DETECTION, PLUGIN_TYPE_ENGINE_DETECTION, AlcachofaMetaEngineDetection);
diff --git a/engines/alcachofa/detection.h b/engines/alcachofa/detection.h
new file mode 100644
index 00000000000..4d1c45fd311
--- /dev/null
+++ b/engines/alcachofa/detection.h
@@ -0,0 +1,69 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef ALCACHOFA_DETECTION_H
+#define ALCACHOFA_DETECTION_H
+
+#include "engines/advancedDetector.h"
+
+namespace Alcachofa {
+
+enum AlcachofaDebugChannels {
+	kDebugGraphics = 1,
+	kDebugPath,
+	kDebugScan,
+	kDebugFilePath,
+	kDebugScript,
+};
+
+extern const PlainGameDescriptor alcachofaGames[];
+
+extern const ADGameDescription gameDescriptions[];
+
+#define GAMEOPTION_ORIGINAL_SAVELOAD GUIO_GAMEOPTIONS1
+
+} // End of namespace Alcachofa
+
+class AlcachofaMetaEngineDetection : public AdvancedMetaEngineDetection {
+	static const DebugChannelDef debugFlagList[];
+
+public:
+	AlcachofaMetaEngineDetection();
+	~AlcachofaMetaEngineDetection() override {}
+
+	const char *getName() const override {
+		return "alcachofa";
+	}
+
+	const char *getEngineName() const override {
+		return "Alcachofa";
+	}
+
+	const char *getOriginalCopyright() const override {
+		return "Alcachofa (C)";
+	}
+
+	const DebugChannelDef *getDebugChannels() const override {
+		return debugFlagList;
+	}
+};
+
+#endif // ALCACHOFA_DETECTION_H
diff --git a/engines/alcachofa/detection_tables.h b/engines/alcachofa/detection_tables.h
new file mode 100644
index 00000000000..7b725a732cb
--- /dev/null
+++ b/engines/alcachofa/detection_tables.h
@@ -0,0 +1,43 @@
+/* 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/>.
+ *
+ */
+
+namespace Alcachofa {
+
+const PlainGameDescriptor alcachofaGames[] = {
+	{ "alcachofa", "Alcachofa" },
+	{ 0, 0 }
+};
+
+const ADGameDescription gameDescriptions[] = {
+	{
+		"alcachofa",
+		nullptr,
+		AD_ENTRY1s("file1.bin", "00000000000000000000000000000000", 11111),
+		Common::EN_ANY,
+		Common::kPlatformDOS,
+		ADGF_UNSTABLE,
+		GUIO1(GUIO_NONE)
+	},
+
+	AD_TABLE_END_MARKER
+};
+
+} // End of namespace Alcachofa
diff --git a/engines/alcachofa/metaengine.cpp b/engines/alcachofa/metaengine.cpp
new file mode 100644
index 00000000000..b00bbba2d43
--- /dev/null
+++ b/engines/alcachofa/metaengine.cpp
@@ -0,0 +1,69 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/translation.h"
+
+#include "alcachofa/metaengine.h"
+#include "alcachofa/detection.h"
+#include "alcachofa/alcachofa.h"
+
+namespace Alcachofa {
+
+static const ADExtraGuiOptionsMap optionsList[] = {
+	{
+		GAMEOPTION_ORIGINAL_SAVELOAD,
+		{
+			_s("Use original save/load screens"),
+			_s("Use the original save/load screens instead of the ScummVM ones"),
+			"original_menus",
+			false,
+			0,
+			0
+		}
+	},
+	AD_EXTRA_GUI_OPTIONS_TERMINATOR
+};
+
+} // End of namespace Alcachofa
+
+const char *AlcachofaMetaEngine::getName() const {
+	return "alcachofa";
+}
+
+const ADExtraGuiOptionsMap *AlcachofaMetaEngine::getAdvancedExtraGuiOptions() const {
+	return Alcachofa::optionsList;
+}
+
+Common::Error AlcachofaMetaEngine::createInstance(OSystem *syst, Engine **engine, const ADGameDescription *desc) const {
+	*engine = new Alcachofa::AlcachofaEngine(syst, desc);
+	return Common::kNoError;
+}
+
+bool AlcachofaMetaEngine::hasFeature(MetaEngineFeature f) const {
+	return checkExtendedSaves(f) ||
+		(f == kSupportsLoadingDuringStartup);
+}
+
+#if PLUGIN_ENABLED_DYNAMIC(ALCACHOFA)
+REGISTER_PLUGIN_DYNAMIC(ALCACHOFA, PLUGIN_TYPE_ENGINE, AlcachofaMetaEngine);
+#else
+REGISTER_PLUGIN_STATIC(ALCACHOFA, PLUGIN_TYPE_ENGINE, AlcachofaMetaEngine);
+#endif
diff --git a/engines/alcachofa/metaengine.h b/engines/alcachofa/metaengine.h
new file mode 100644
index 00000000000..e31ae83271a
--- /dev/null
+++ b/engines/alcachofa/metaengine.h
@@ -0,0 +1,43 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef ALCACHOFA_METAENGINE_H
+#define ALCACHOFA_METAENGINE_H
+
+#include "engines/advancedDetector.h"
+
+class AlcachofaMetaEngine : public AdvancedMetaEngine {
+public:
+	const char *getName() const override;
+
+	Common::Error createInstance(OSystem *syst, Engine **engine, const ADGameDescription *desc) const override;
+
+	/**
+	 * Determine whether the engine supports the specified MetaEngine feature.
+	 *
+	 * Used by e.g. the launcher to determine whether to enable the Load button.
+	 */
+	bool hasFeature(MetaEngineFeature f) const override;
+
+	const ADExtraGuiOptionsMap *getAdvancedExtraGuiOptions() const override;
+};
+
+#endif // ALCACHOFA_METAENGINE_H
diff --git a/engines/alcachofa/module.mk b/engines/alcachofa/module.mk
new file mode 100644
index 00000000000..f32728feb12
--- /dev/null
+++ b/engines/alcachofa/module.mk
@@ -0,0 +1,17 @@
+MODULE := engines/alcachofa
+
+MODULE_OBJS = \
+	alcachofa.o \
+	console.o \
+	metaengine.o
+
+# This module can be built as a plugin
+ifeq ($(ENABLE_ALCACHOFA), DYNAMIC_PLUGIN)
+PLUGIN := 1
+endif
+
+# Include common rules
+include $(srcdir)/rules.mk
+
+# Detection objects
+DETECT_OBJS += $(MODULE)/detection.o


Commit: b0fda8ff19860c526a0cd39f07feb97e604d6323
    https://github.com/scummvm/scummvm/commit/b0fda8ff19860c526a0cd39f07feb97e604d6323
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:44+02:00

Commit Message:
ALCACHOFA: Add first detection entry

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


diff --git a/engines/alcachofa/detection.cpp b/engines/alcachofa/detection.cpp
index 54d3b03454d..f743c0465da 100644
--- a/engines/alcachofa/detection.cpp
+++ b/engines/alcachofa/detection.cpp
@@ -40,6 +40,7 @@ const DebugChannelDef AlcachofaMetaEngineDetection::debugFlagList[] = {
 
 AlcachofaMetaEngineDetection::AlcachofaMetaEngineDetection() : AdvancedMetaEngineDetection(Alcachofa::gameDescriptions,
 	sizeof(ADGameDescription), Alcachofa::alcachofaGames) {
+	_flags |= kADFlagMatchFullPaths;
 }
 
 REGISTER_PLUGIN_STATIC(ALCACHOFA_DETECTION, PLUGIN_TYPE_ENGINE_DETECTION, AlcachofaMetaEngineDetection);
diff --git a/engines/alcachofa/detection.h b/engines/alcachofa/detection.h
index 4d1c45fd311..62298ab9b14 100644
--- a/engines/alcachofa/detection.h
+++ b/engines/alcachofa/detection.h
@@ -58,7 +58,7 @@ public:
 	}
 
 	const char *getOriginalCopyright() const override {
-		return "Alcachofa (C)";
+		return "Alcachofa Soft (C)";
 	}
 
 	const DebugChannelDef *getDebugChannels() const override {
diff --git a/engines/alcachofa/detection_tables.h b/engines/alcachofa/detection_tables.h
index 7b725a732cb..49e2ad33df4 100644
--- a/engines/alcachofa/detection_tables.h
+++ b/engines/alcachofa/detection_tables.h
@@ -22,17 +22,17 @@
 namespace Alcachofa {
 
 const PlainGameDescriptor alcachofaGames[] = {
-	{ "alcachofa", "Alcachofa" },
+	{ "mort_phil_adventura_de_cine", "Mort&Phil: A movie adventure" },
 	{ 0, 0 }
 };
 
 const ADGameDescription gameDescriptions[] = {
 	{
-		"alcachofa",
+		"mort_phil_adventura_de_cine",
 		nullptr,
-		AD_ENTRY1s("file1.bin", "00000000000000000000000000000000", 11111),
-		Common::EN_ANY,
-		Common::kPlatformDOS,
+		AD_ENTRY1s("Textos/Objetos.nkr", "a2b1deff5ca7187f2ebf7f2ab20747e9", 17606),
+		Common::DE_DEU,
+		Common::kPlatformWindows,
 		ADGF_UNSTABLE,
 		GUIO1(GUIO_NONE)
 	},


Commit: b2ab2453aef543390193e2c522c9520350f47828
    https://github.com/scummvm/scummvm/commit/b2ab2453aef543390193e2c522c9520350f47828
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:44+02:00

Commit Message:
ALCACHOFA: Read world data

Changed paths:
  A engines/alcachofa/game-objects.cpp
  A engines/alcachofa/general-objects.cpp
  A engines/alcachofa/graphics.cpp
  A engines/alcachofa/graphics.h
  A engines/alcachofa/objects.h
  A engines/alcachofa/rooms.cpp
  A engines/alcachofa/rooms.h
  A engines/alcachofa/shape.cpp
  A engines/alcachofa/shape.h
  A engines/alcachofa/stream-helper.cpp
  A engines/alcachofa/stream-helper.h
  A engines/alcachofa/ui-objects.cpp
    engines/alcachofa/alcachofa.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index f8bc232747e..813f2a4c30a 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -31,6 +31,8 @@
 #include "engines/util.h"
 #include "graphics/paletteman.h"
 
+#include "rooms.h"
+
 namespace Alcachofa {
 
 AlcachofaEngine *g_engine;
@@ -57,6 +59,9 @@ Common::Error AlcachofaEngine::run() {
 	initGraphics(320, 200);
 	_screen = new Graphics::Screen();
 
+	auto world = new World();
+	delete world;
+
 	// Set the engine's debugger console
 	setDebugger(new Console());
 
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
new file mode 100644
index 00000000000..0aacd2510dc
--- /dev/null
+++ b/engines/alcachofa/game-objects.cpp
@@ -0,0 +1,171 @@
+/* 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 "objects.h"
+#include "rooms.h"
+#include "stream-helper.h"
+
+using namespace Common;
+
+namespace Alcachofa {
+
+Item::Item(Room *room, ReadStream &stream)
+	: GraphicObject(room, stream) {
+	stream.readByte(); // unused and ignored byte
+}
+
+InteractableObject::InteractableObject(Room *room, ReadStream &stream)
+	: PhysicalObject(room, stream)
+	, _interactionPoint(Shape(stream).firstPoint())
+	, _cursorType((CursorType)stream.readSint32LE())
+	, _relatedObject(readVarString(stream)) {
+	_relatedObject.toUppercase();
+}
+
+Door::Door(Room *room, ReadStream &stream)
+	: InteractableObject(room, stream)
+	, _targetRoom(readVarString(stream))
+	, _targetObject(readVarString(stream))
+	, _characterDirection((Direction)stream.readSint32LE()) {
+	_targetRoom.replace(' ', '_');
+}
+
+Character::Character(Room *room, ReadStream &stream)
+	: ShapeObject(room, stream)
+	, _interactionPoint(Shape(stream).firstPoint())
+	, _direction((Direction)stream.readSint32LE())
+	, _graphicNormal(stream)
+	, _graphicTalking(stream) {
+	_graphicNormal.start(true);
+	_order = _graphicNormal.order();
+}
+
+void Character::serializeSave(Serializer &serializer) {
+	ShapeObject::serializeSave(serializer);
+	serializer.syncAsByte(_isTalking);
+	serializer.syncAsSint32LE(_curDialogId);
+	_graphicNormal.serializeSave(serializer);
+	_graphicTalking.serializeSave(serializer);
+	syncObjectAsString(serializer, _curAnimateObject);
+	syncObjectAsString(serializer, _curTalkingObject);
+	serializer.syncAsFloatLE(_lodBias);
+}
+
+void Character::syncObjectAsString(Serializer &serializer, ObjectBase *&object) {
+	String name;
+	if (serializer.isSaving() && object != nullptr)
+		name = object->name();
+
+	serializer.syncString(name);
+
+	if (serializer.isLoading()) {
+		if (name.empty())
+			object = nullptr;
+		else {
+			object = room()->getObjectByName(name);
+			if (object == nullptr)
+				object = room()->world()->getObjectByName(name);
+			if (object == nullptr)
+				error("Invalid object name \"%s\" saved for \"%s\" in \"%s\"",
+					name.c_str(), this->name().c_str(), room()->name().c_str());
+		}
+	}
+}
+
+WalkingCharacter::WalkingCharacter(Room *room, ReadStream &stream)
+	: Character(room, stream) {
+	for (int32 i = 0; i < kDirectionCount; i++) {
+		auto fileName = readVarString(stream);
+		_walkingAnimations[i].reset(new Animation(Common::move(fileName)));
+	}
+	for (int32 i = 0; i < kDirectionCount; i++) {
+		auto fileName = readVarString(stream);
+		_standingAnimations[i].reset(new Animation(Common::move(fileName)));
+	}
+}
+
+void WalkingCharacter::serializeSave(Serializer &serializer) {
+	Character::serializeSave(serializer);
+	serializer.syncAsSint32LE(_lastWalkAnimFrame);
+	serializer.syncAsSint32LE(_walkSpeed);
+	syncPoint(serializer, _sourcePos);
+	syncPoint(serializer, _targetPos);
+	serializer.syncAsByte(_isWalking);
+	syncArray(serializer, _pathPoints, syncPoint);
+	syncEnum(serializer, _direction);
+	_graphicWalking.serializeSave(serializer);
+}
+
+MainCharacter::MainCharacter(Room *room, ReadStream &stream)
+	: WalkingCharacter(room, stream) {
+	stream.readByte(); // unused byte
+	_order = 100;
+
+	_kind =
+		name().equalsIgnoreCase("MORTADELO") ? MainCharacterKind::Mortadelo
+		: name().equalsIgnoreCase("FILEMON") ? MainCharacterKind::Filemon
+		: MainCharacterKind::None;
+}
+
+MainCharacter::~MainCharacter() {
+	for (auto *item : _items)
+		delete item;
+}
+
+void syncDialogMenuLine(Serializer &serializer, DialogMenuLine &line) {
+	serializer.syncAsSint32LE(line._dialogId);
+	serializer.syncAsSint32LE(line._yPosition);
+	serializer.syncAsSint32LE(line._returnId);
+}
+
+void MainCharacter::serializeSave(Serializer &serializer) {
+	String roomName = room()->name();
+	serializer.syncString(roomName);
+	if (serializer.isLoading()) {
+		room() = room()->world()->getRoomByName(roomName);
+		if (room() == nullptr)
+			error("Invalid room name \"%s\" saved for \"%s\"", roomName.c_str(), name().c_str());
+	}
+
+	Character::serializeSave(serializer);
+	serializer.syncAsSint32LE(_relatedProcessCounter);
+	syncArray(serializer, _dialogMenuLines, syncDialogMenuLine);
+	syncObjectAsString(serializer, _currentlyUsingObject);
+
+	for (auto *item : _items) {
+		bool isEnabled = item->isEnabled();
+		serializer.syncAsByte(isEnabled);
+		item->toggle(isEnabled);
+	}
+}
+
+Background::Background(Room *room, const String &animationFileName, int16 scale)
+	: GraphicObject(room, "BACKGROUND") {
+	_graphic._animation.reset(new Animation(animationFileName, AnimationFolder::Fondos));
+	_graphic._scale = scale;
+	_graphic._order = 59;
+}
+
+FloorColor::FloorColor(Room *room, ReadStream &stream)
+	: ObjectBase(room, stream)
+	, _shape(stream) {}
+
+}
diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
new file mode 100644
index 00000000000..bc8a965c66c
--- /dev/null
+++ b/engines/alcachofa/general-objects.cpp
@@ -0,0 +1,134 @@
+/* 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 "objects.h"
+#include "rooms.h"
+#include "stream-helper.h"
+
+#include "common/system.h"
+
+using namespace Common;
+
+namespace Alcachofa {
+
+ObjectBase::ObjectBase(Room *room, const char *name)
+	: _room(room)
+	, _name(name)
+	, _isEnabled(false) {
+	assert(room != nullptr);
+}
+
+ObjectBase::ObjectBase(Room *room, ReadStream &stream)
+	: _room(room) {
+	assert(room != nullptr);
+	_name = readVarString(stream);
+	_isEnabled = readBool(stream);
+}
+
+void ObjectBase::toggle(bool isEnabled) {
+	_isEnabled = isEnabled;
+}
+
+void ObjectBase::render() {
+}
+
+void ObjectBase::update() {
+}
+
+void ObjectBase::loadResources() {
+}
+
+void ObjectBase::freeResources() {
+}
+
+void ObjectBase::serializeSave(Serializer &serializer) {
+	serializer.syncAsByte(_isEnabled);
+}
+
+Graphic *ObjectBase::graphic() {
+	return nullptr;
+}
+
+Shape *ObjectBase::shape() {
+	return nullptr;
+}
+
+PointObject::PointObject(Room *room, ReadStream &stream)
+	: ObjectBase(room, stream) {
+	_pos = Shape(stream).firstPoint();
+}
+
+GraphicObject::GraphicObject(Room *room, ReadStream &stream)
+	: ObjectBase(room, stream)
+	, _graphic(stream)
+	, _type((GraphicObjectType)stream.readSint32LE())
+	, _posterizeAlpha(100 - stream.readSint32LE()) {
+	_graphic.start(true);
+}
+
+GraphicObject::GraphicObject(Room *room, const char *name)
+	: ObjectBase(room, name)
+	, _type(GraphicObjectType::Type0)
+	, _posterizeAlpha(0) {
+}
+
+void GraphicObject::serializeSave(Serializer &serializer) {
+	ObjectBase::serializeSave(serializer);
+	_graphic.serializeSave(serializer);
+}
+
+Graphic *GraphicObject::graphic() {
+	return &_graphic;
+}
+
+ShiftingGraphicObject::ShiftingGraphicObject(Room *room, ReadStream &stream)
+	: GraphicObject(room, stream) {
+	_pos = Shape(stream).firstPoint();
+	_size = Shape(stream).firstPoint();
+	_texShift.setX(stream.readSint32LE() / 256.0f);
+	_texShift.setY(stream.readSint32LE() / 256.0f);
+	_startTime = g_system->getMillis();
+}
+
+ShapeObject::ShapeObject(Room *room, ReadStream &stream)
+	: ObjectBase(room, stream)
+	, _shape(stream)
+	, _cursorType((CursorType)stream.readSint32LE()) {
+}
+
+void ShapeObject::serializeSave(Serializer &serializer) {
+	serializer.syncAsSByte(_order);
+}
+
+Shape *ShapeObject::shape() {
+	return &_shape;
+}
+
+CursorType ShapeObject::cursorType() const {
+	return _cursorType;
+}
+
+PhysicalObject::PhysicalObject(Room *room, ReadStream &stream)
+	: ShapeObject(room, stream) {
+	_order = stream.readSByte();
+}
+
+}
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
new file mode 100644
index 00000000000..6603ea99256
--- /dev/null
+++ b/engines/alcachofa/graphics.cpp
@@ -0,0 +1,69 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "graphics.h"
+#include "stream-helper.h"
+
+#include "common/system.h"
+
+using namespace Common;
+
+namespace Alcachofa {
+
+Animation::Animation(String fileName, AnimationFolder folder)
+	: _fileName(move(fileName))
+	, _folder(folder) {
+}
+
+Graphic::Graphic() {
+}
+
+Graphic::Graphic(ReadStream &stream) {
+	_center.x = stream.readSint16LE();
+	_center.y = stream.readSint16LE();
+	_scale = stream.readSint16LE();
+	_order = stream.readSByte();
+	auto animationName = readVarString(stream);
+	_animation.reset(new Animation(std::move(animationName)));
+}
+
+void Graphic::start(bool isLooping) {
+	_isPaused = false;
+	_isLooping = isLooping;
+	_lastTime = g_system->getMillis();
+}
+
+void Graphic::stop() {
+	_isPaused = true;
+	_isLooping = false;
+	_lastTime = g_system->getMillis() - _lastTime;
+}
+
+void Graphic::serializeSave(Serializer &serializer) {
+	syncPoint(serializer, _center);
+	serializer.syncAsSint16LE(_scale);
+	serializer.syncAsUint32LE(_lastTime);
+	serializer.syncAsByte(_isPaused);
+	serializer.syncAsByte(_isLooping);
+	serializer.syncAsFloatLE(_camAcceleration);
+}
+
+}
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
new file mode 100644
index 00000000000..d7e20a458fe
--- /dev/null
+++ b/engines/alcachofa/graphics.h
@@ -0,0 +1,98 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef GRAPHICS_H
+#define GRAPHICS_H
+
+#include "common/ptr.h"
+#include "common/stream.h"
+#include "common/serializer.h"
+#include "common/rect.h"
+
+namespace Alcachofa {
+
+enum class CursorType {
+	Normal,
+	LookAt,
+	Use,
+	GoTo,
+	LeaveUp,
+	LeaveRight,
+	LeaveDown,
+	LeaveLeft
+};
+
+enum class Direction {
+	Up,
+	Down,
+	Left,
+	Right
+};
+
+constexpr const int32 kDirectionCount = 4;
+
+enum class AnimationFolder {
+	Animations,
+	Maskaras,
+	Fondos
+};
+
+class Animation {
+public:
+	Animation(Common::String fileName, AnimationFolder folder = AnimationFolder::Animations);
+
+private:
+	Common::String _fileName;
+	AnimationFolder _folder;
+};
+
+class Graphic {
+public:
+	Graphic();
+	Graphic(Common::ReadStream &stream);
+
+	inline int8 order() const { return _order; }
+
+	void start(bool looping);
+	void stop();
+	void serializeSave(Common::Serializer &serializer);
+
+public:
+	Common::SharedPtr<Animation> _animation;
+	Common::Point _center;
+	int16 _scale = 300;
+	int8 _order = 0;
+
+private:
+	bool _isPaused = true,
+		_isLooping = true;
+	uint32 _lastTime = 0;
+	float _camAcceleration = 1.0f;
+};
+
+class IGraphics {
+public:
+	virtual ~IGraphics() = default;
+};
+
+}
+
+#endif
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
new file mode 100644
index 00000000000..bf5797e71f6
--- /dev/null
+++ b/engines/alcachofa/objects.h
@@ -0,0 +1,388 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef OBJECTS_H
+#define OBJECTS_H
+
+#include "Shape.h"
+#include "Graphics.h"
+
+#include "common/serializer.h"
+
+namespace Alcachofa {
+
+class Room;
+
+class ObjectBase {
+public:
+	static constexpr const char *kClassName = "CObjetoBase";
+	ObjectBase(Room *room, const char *name);
+	ObjectBase(Room *room, Common::ReadStream &stream);
+	virtual ~ObjectBase() = default;
+
+	inline const Common::String &name() const { return _name; }
+	inline Room *&room() { return _room; }
+	inline Room *room() const { return _room; }
+	inline bool isEnabled() const { return _isEnabled; }
+
+	virtual void toggle(bool isEnabled);
+	virtual void render();
+	virtual void update();
+	virtual void loadResources();
+	virtual void freeResources();
+	virtual void serializeSave(Common::Serializer &serializer);
+	virtual Graphic *graphic();
+	virtual Shape *shape();
+
+private:
+	Common::String _name;
+	bool _isEnabled = true;
+	Room *_room = nullptr;
+};
+
+class PointObject : public ObjectBase {
+public:
+	static constexpr const char *kClassName = "CObjetoPunto";
+	PointObject(Room *room, Common::ReadStream &stream);
+
+	inline Common::Point &position() { return _pos; }
+	inline Common::Point position() const { return _pos; }
+
+private:
+	Common::Point _pos;
+};
+
+enum class GraphicObjectType : byte
+{
+	Type0,
+	Type1,
+	Type2
+};
+
+class GraphicObject : public ObjectBase {
+public:
+	static constexpr const char *kClassName = "CObjetoGrafico";
+	GraphicObject(Room *room, Common::ReadStream &stream);
+	virtual ~GraphicObject() override = default;
+
+	virtual void serializeSave(Common::Serializer &serializer) override;
+	virtual Graphic *graphic() override;
+
+protected:
+	GraphicObject(Room *room, const char *name);
+
+	Graphic _graphic;
+	GraphicObjectType _type;
+	int32 _posterizeAlpha;
+};
+
+class ShiftingGraphicObject final : public GraphicObject {
+public:
+	static constexpr const char *kClassName = "CObjetoGraficoMuare";
+	ShiftingGraphicObject(Room *room, Common::ReadStream &stream);
+
+private:
+	Common::Point _pos, _size;
+	Math::Vector2d _texShift;
+	uint32 _startTime = 0;
+};
+
+class ShapeObject : public ObjectBase {
+public:
+	ShapeObject(Room *room, Common::ReadStream &stream);
+	virtual ~ShapeObject() override = default;
+
+	virtual void serializeSave(Common::Serializer &serializer) override;
+	virtual Shape *shape() override;
+	virtual CursorType cursorType() const;
+
+private:
+	Shape _shape;
+	CursorType _cursorType;
+
+protected:
+	// original inconsistency: base class has member that is read by the sub classes
+	int8 _order = 0;
+};
+
+class PhysicalObject : public ShapeObject {
+public:
+	PhysicalObject(Room *room, Common::ReadStream &stream);
+};
+
+class MenuButton : public PhysicalObject {
+public:
+	static constexpr const char *kClassName = "CBotonMenu";
+	MenuButton(Room *room, Common::ReadStream &stream);
+	virtual ~MenuButton() override = default;
+
+	inline int32 actionId() const { return _actionId; }
+
+private:
+	int32 _actionId;
+	Graphic
+		_graphicNormal,
+		_graphicHovered,
+		_graphicClicked,
+		_graphicDisabled;
+};
+
+class InternetMenuButton final : public MenuButton {
+public:
+	static constexpr const char *kClassName = "CBotonMenuInternet";
+	InternetMenuButton(Room *room, Common::ReadStream &stream);
+};
+
+class OptionsMenuButton final : public MenuButton {
+public:
+	static constexpr const char *kClassName = "CBotonMenuOpciones";
+	OptionsMenuButton(Room *room, Common::ReadStream &stream);
+};
+
+class MainMenuButton final : public MenuButton {
+public:
+	static constexpr const char *kClassName = "CBotonMenuPrincipal";
+	MainMenuButton(Room *room, Common::ReadStream &stream);
+};
+
+class PushButton final : public PhysicalObject {
+public:
+	static constexpr const char *kClassName = "CPushButton";
+	PushButton(Room *room, Common::ReadStream &stream);
+
+private:
+	// TODO: Reverse engineer PushButton
+	bool _alwaysVisible;
+	Graphic _graphic1, _graphic2;
+	int32 _actionId;
+};
+
+class EditBox final : public PhysicalObject {
+public:
+	static constexpr const char *kClassName = "CEditBox";
+	EditBox(Room *room, Common::ReadStream &stream);
+
+private:
+	// TODO: Reverse engineer EditBox
+	int32 i1;
+	Common::Point p1;
+	Common::String _labelId;
+	bool b1;
+	int32 i3, i4, i5,
+		_fontId;
+};
+
+class CheckBox : public PhysicalObject {
+public:
+	static constexpr const char *kClassName = "CCheckBox";
+	CheckBox(Room *room, Common::ReadStream &stream);
+	virtual ~CheckBox() override = default;
+
+private:
+	// TODO: Reverse engineer CheckBox
+	bool b1;
+	Graphic
+		_graph1,
+		_graph2,
+		_graph3,
+		_graph4;
+	int32 _valueId;
+};
+
+class CheckBoxAutoAdjustNoise final : public CheckBox {
+public:
+	static constexpr const char *kClassName = "CCheckBoxAutoAjustarRuido";
+	CheckBoxAutoAdjustNoise(Room *room, Common::ReadStream &stream);
+};
+
+class SlideButton final : public ObjectBase {
+public:
+	static constexpr const char *kClassName = "CSlideButton";
+	SlideButton(Room *room, Common::ReadStream &stream);
+	virtual ~SlideButton() override = default;
+
+private:
+	// TODO: Reverse engineer SlideButton
+	int32 i1;
+	Common::Point p1, p2;
+	Graphic
+		_graph1,
+		_graph2,
+		_graph3;
+};
+
+class IRCWindow final : public ObjectBase {
+public:
+	static constexpr const char *kClassName = "CVentanaIRC";
+	IRCWindow(Room *room, Common::ReadStream &stream);
+
+private:
+	Common::Point _p1, _p2;
+};
+
+class MessageBox final : public ObjectBase {
+public:
+	static constexpr const char *kClassName = "CMessageBox";
+	MessageBox(Room *room, Common::ReadStream &stream);
+	virtual ~MessageBox() override = default;
+
+private:
+	// TODO: Reverse engineer MessageBox
+	Graphic
+		_graph1,
+		_graph2,
+		_graph3,
+		_graph4,
+		_graph5;
+};
+
+class VoiceMeter final : public GraphicObject {
+public:
+	static constexpr const char *kClassName = "CVuMeter";
+	VoiceMeter(Room *room, Common::ReadStream &stream);
+};
+
+class Item : public GraphicObject {
+public:
+	static constexpr const char *kClassName = "CObjetoInventario";
+	Item(Room *room, Common::ReadStream &stream);
+};
+
+class InteractableObject : public PhysicalObject {
+public:
+	static constexpr const char *kClassName = "CObjetoTipico";
+	InteractableObject(Room *room, Common::ReadStream &stream);
+	virtual ~InteractableObject() override = default;
+
+private:
+	Common::Point _interactionPoint;
+	CursorType _cursorType;
+	Common::String _relatedObject;
+};
+
+class Door final : public InteractableObject {
+public:
+	static constexpr const char *kClassName = "CPuerta";
+	Door(Room *room, Common::ReadStream &stream);
+
+private:
+	Common::String _targetRoom, _targetObject;
+	Direction _characterDirection;
+};
+
+class Character : public ShapeObject {
+public:
+	static constexpr const char *kClassName = "CPersonaje";
+	Character(Room *room, Common::ReadStream &stream);
+	virtual ~Character() override = default;
+
+	virtual void serializeSave(Common::Serializer &serializer) override;
+
+protected:
+	void syncObjectAsString(Common::Serializer &serializer, ObjectBase *&object);
+
+private:
+	Common::Point _interactionPoint;
+	Direction _direction;
+	Graphic _graphicNormal, _graphicTalking;
+
+	bool _isTalking = false;
+	int _curDialogId = -1;
+	float _lodBias = 0.0f;
+	ObjectBase
+		*_curAnimateObject = nullptr,
+		*_curTalkingObject = nullptr;
+};
+
+class WalkingCharacter : public Character {
+public:
+	static constexpr const char *kClassName = "CPersonajeAnda";
+	WalkingCharacter(Room *room, Common::ReadStream &stream);
+	virtual ~WalkingCharacter() override = default;
+
+	virtual void serializeSave(Common::Serializer &serializer) override;
+
+private:
+	Graphic _graphicWalking;
+	Common::SharedPtr<Animation>
+		_walkingAnimations[kDirectionCount],
+		_standingAnimations[kDirectionCount];
+
+	int32
+		_lastWalkAnimFrame = -1,
+		_walkSpeed = 0,
+		_curPathPointI = -1;
+	Common::Point
+		_sourcePos,
+		_targetPos;
+	bool _isWalking = false;
+	Direction _direction = Direction::Up;
+	Common::Array<Common::Point> _pathPoints;
+};
+
+enum class MainCharacterKind {
+	None,
+	Mortadelo,
+	Filemon
+};
+
+struct DialogMenuLine {
+	int32 _dialogId;
+	int32 _yPosition;
+	int32 _returnId;
+};
+
+class MainCharacter final : public WalkingCharacter {
+public:
+	static constexpr const char *kClassName = "CPersonajePrincipal";
+	MainCharacter(Room *room, Common::ReadStream &stream);
+	virtual ~MainCharacter() override;
+
+	inline MainCharacterKind kind() const { return _kind; }
+
+	virtual void serializeSave(Common::Serializer &serializer) override;
+
+private:
+	Common::Array<Item *> _items;
+	Common::Array<DialogMenuLine> _dialogMenuLines;
+	ObjectBase *_currentlyUsingObject = nullptr;
+	MainCharacterKind _kind;
+	int32_t _relatedProcessCounter = 0;
+};
+
+class Background final : public GraphicObject {
+public:
+	Background(Room *room, const Common::String &animationFileName, int16 scale);
+};
+
+class FloorColor final : public ObjectBase {
+public:
+	static constexpr const char *kClassName = "CSueloColor";
+	FloorColor(Room *room, Common::ReadStream &stream);
+	virtual ~FloorColor() override = default;
+
+private:
+	FloorColorShape _shape;
+};
+
+}
+
+#endif // OBJECTS_H
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
new file mode 100644
index 00000000000..0814d9c9934
--- /dev/null
+++ b/engines/alcachofa/rooms.cpp
@@ -0,0 +1,283 @@
+/* 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 "rooms.h"
+#include "stream-helper.h"
+
+#include "common/file.h"
+
+using namespace Common;
+
+namespace Alcachofa {
+
+Room::Room(World *world, ReadStream &stream) : Room(world, stream, false) {
+}
+
+static ObjectBase *readRoomObject(Room *room, ReadStream &stream) {
+	const auto type = readVarString(stream);
+	if (type == ObjectBase::kClassName)
+		return new ObjectBase(room, stream);
+	else if (type == PointObject::kClassName)
+		return new PointObject(room, stream);
+	else if (type == GraphicObject::kClassName)
+		return new GraphicObject(room, stream);
+	else if (type == ShiftingGraphicObject::kClassName)
+		return new ShiftingGraphicObject(room, stream);
+	else if (type == Item::kClassName)
+		return new Item(room, stream);
+	else if (type == PhysicalObject::kClassName)
+		return new PhysicalObject(room, stream);
+	else if (type == MainMenuButton::kClassName)
+		return new MainMenuButton(room, stream);
+	else if (type == InternetMenuButton::kClassName)
+		return new InternetMenuButton(room, stream);
+	else if (type == OptionsMenuButton::kClassName)
+		return new OptionsMenuButton(room, stream);
+	else if (type == EditBox::kClassName)
+		return new EditBox(room, stream);
+	else if (type == PushButton::kClassName)
+		return new PushButton(room, stream);
+	else if (type == CheckBox::kClassName)
+		return new CheckBox(room, stream);
+	else if (type == CheckBoxAutoAdjustNoise::kClassName)
+		return new CheckBoxAutoAdjustNoise(room, stream);
+	else if (type == SlideButton::kClassName)
+		return new SlideButton(room, stream);
+	else if (type == IRCWindow::kClassName)
+		return new IRCWindow(room, stream);
+	else if (type == MessageBox::kClassName)
+		return new MessageBox(room, stream);
+	else if (type == VoiceMeter::kClassName)
+		return new VoiceMeter(room, stream);
+	else if (type == InteractableObject::kClassName)
+		return new InteractableObject(room, stream);
+	else if (type == Door::kClassName)
+		return new Door(room, stream);
+	else if (type == Character::kClassName)
+		return new Character(room, stream);
+	else if (type == WalkingCharacter::kClassName)
+		return new WalkingCharacter(room, stream);
+	else if (type == MainCharacter::kClassName)
+		return new MainCharacter(room, stream);
+	else if (type == FloorColor::kClassName)
+		return new FloorColor(room, stream);
+	else
+		error("Unknown type for room objects: %s", type.c_str());
+}
+
+Room::Room(World *world, ReadStream &stream, bool hasUselessByte)
+	: _world(world) {
+	_name = readVarString(stream);
+	_musicId = stream.readSByte();
+	_characterAlpha = stream.readByte();
+	auto backgroundScale = stream.readSint16LE();
+	_floors[0] = PathFindingShape(stream);
+	_floors[1] = PathFindingShape(stream);
+	_cameraFollowsUponLeaving = readBool(stream);
+	PathFindingShape _(stream); // unused path finding area
+	_characterAlphaPercent = stream.readByte();
+	if (hasUselessByte)
+		stream.readByte();
+
+	uint32 objectSize = stream.readUint32LE(); // TODO: Maybe switch to seekablereadstream and assert objectSize?
+	while (objectSize > 0)
+	{
+		_objects.push_back(readRoomObject(this, stream));
+		objectSize = stream.readUint32LE();
+	}
+	_objects.push_back(new Background(this, _name, backgroundScale));
+
+	if (!_floors[0].empty())
+		_activeFloorI = 0;
+}
+
+Room::~Room() {
+	for (auto *object : _objects)
+		delete object;
+}
+
+ObjectBase *Room::getObjectByName(const Common::String &name) const {
+	for (auto *object : _objects) {
+		if (object->name().equalsIgnoreCase(name))
+			return object;
+	}
+	return nullptr;
+}
+
+void Room::loadResources() {
+	for (auto *object : _objects)
+		object->loadResources();
+}
+
+void Room::freeResources() {
+	for (auto *object : _objects)
+		object->freeResources();
+}
+
+void Room::serializeSave(Serializer &serializer) {
+	serializer.syncAsSByte(_musicId);
+	serializer.syncAsSByte(_activeFloorI);
+	for (auto *object : _objects)
+		object->serializeSave(serializer);
+}
+
+OptionsMenu::OptionsMenu(World *world, ReadStream &stream)
+	: Room(world, stream, true) {
+}
+
+ConnectMenu::ConnectMenu(World *world, ReadStream &stream)
+	: Room(world, stream, true) {
+}
+
+ListenMenu::ListenMenu(World *world, ReadStream &stream)
+	: Room(world, stream, true) {
+}
+
+Inventory::Inventory(World *world, ReadStream &stream)
+	: Room(world, stream, true) {
+}
+
+Inventory::~Inventory() {
+	for (auto *item : _items)
+		delete item;
+}
+
+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++) {
+		if (loadWorldFile(*itMapFile))
+			_loadedMapCount++;
+	}
+
+	_globalRoom = getRoomByName("GLOBAL");
+	if (_globalRoom == nullptr)
+		error("Could not find GLOBAL room");
+	_inventory = dynamic_cast<Inventory *>(getRoomByName("INVENTARIO"));
+	if (_inventory == nullptr)
+		error("Could not find INVENTARIO");
+	_filemon = dynamic_cast<MainCharacter *>(_globalRoom->getObjectByName("FILEMON"));
+	if (_filemon == nullptr)
+		error("Could not find FILEMON");
+	_mortadelo = dynamic_cast<MainCharacter *>(_globalRoom->getObjectByName("MORTADELO"));
+	if (_mortadelo == nullptr)
+		error("Could not find MORTADELO");
+}
+
+World::~World() {
+	for (auto *room : _rooms)
+		delete room;
+}
+
+MainCharacter &World::getMainCharacterByKind(MainCharacterKind kind) const {
+	switch (kind) {
+	case MainCharacterKind::Mortadelo: return *_mortadelo;
+	case MainCharacterKind::Filemon: return *_filemon;
+	default:
+		error("Invalid character kind given to getMainCharacterByKind");
+	}
+}
+
+Room *World::getRoomByName(const Common::String &name) const {
+	for (auto *room : _rooms) {
+		if (room->name().equalsIgnoreCase(name))
+			return room;
+	}
+	return nullptr;
+}
+
+ObjectBase *World::getObjectByName(const Common::String &name) const {
+	ObjectBase *result = nullptr;
+	if (result == nullptr && _currentRoom != nullptr)
+		result = _currentRoom->getObjectByName(name);
+	if (result == nullptr)
+		result = globalRoom().getObjectByName(name);
+	if (result == nullptr)
+		result = inventory().getObjectByName(name);
+	return result;
+}
+
+const Common::String &World::getGlobalAnimationName(GlobalAnimationKind kind) const {
+	int kindI = (int)kind;
+	assert(kindI >= 0 && kindI < (int)GlobalAnimationKind::Count);
+	return _globalAnimationNames[kindI];
+}
+
+static Room *readRoom(World *world, ReadStream &stream) {
+	const auto type = readVarString(stream);
+	if (type == Room::kClassName)
+		return new Room(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
+		error("Unknown type for room %s", type.c_str());
+}
+
+bool World::loadWorldFile(const char *path) {
+	Common::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
+		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();
+	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);
+
+	_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);
+
+	uint32 roomEnd = file.readUint32LE();
+	while (roomEnd > 0) {
+		_rooms.push_back(readRoom(this, file));
+		assert(file.pos() == roomEnd);
+		roomEnd = file.readUint32LE();
+	}
+
+	return true;
+}
+
+}
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
new file mode 100644
index 00000000000..e311bc9362a
--- /dev/null
+++ b/engines/alcachofa/rooms.h
@@ -0,0 +1,141 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef ROOMS_H
+#define ROOMS_H
+
+#include "Objects.h"
+
+namespace Alcachofa {
+
+class World;
+
+class Room {
+public:
+	static constexpr const char *kClassName = "CHabitacion";
+	Room(World *world, Common::ReadStream &stream);
+	virtual ~Room();
+
+	inline World *world() { return _world; }
+	inline const Common::String &name() const { return _name; }
+
+	ObjectBase *getObjectByName(const Common::String &name) const;
+
+	virtual void loadResources();
+	virtual void freeResources();
+	virtual void serializeSave(Common::Serializer &serializer);
+
+protected:
+	Room(World *world, Common::ReadStream &stream, bool hasUselessByte);
+
+	World *_world;
+	Common::String _name;
+	PathFindingShape _floors[2];
+	bool _cameraFollowsUponLeaving;
+	int8
+		_musicId,
+		_activeFloorI = -1;
+	uint8
+		_characterAlpha,
+		_characterAlphaPercent;
+
+	Common::Array<ObjectBase *> _objects;
+};
+
+class OptionsMenu final : public Room {
+public:
+	static constexpr const char *kClassName = "CHabitacionMenuOpciones";
+	OptionsMenu(World *world, Common::ReadStream &stream);
+};
+
+class ConnectMenu final : public Room {
+public:
+	static constexpr const char *kClassName = "CHabitacionConectar";
+	ConnectMenu(World *world, Common::ReadStream &stream);
+};
+
+class ListenMenu final : public Room {
+public:
+	static constexpr const char *kClassName = "CHabitacionEsperar";
+	ListenMenu(World *world, Common::ReadStream &stream);
+};
+
+class Inventory final : public Room {
+public:
+	static constexpr const char *kClassName = "CInventario";
+	Inventory(World *world, Common::ReadStream &stream);
+	virtual ~Inventory() override;
+
+private:
+	Common::Array<Item *> _items;
+};
+
+enum class GlobalAnimationKind {
+	GeneralFont = 0,
+	TextFont,
+	Cursor,
+	MortadeloIcon,
+	FilemonIcon,
+	InventoryIcon,
+	MortadeloDisabledIcon,
+	FilemonDisabledIcon,
+	InventoryDisabledIcon,
+
+	Count
+};
+
+class World final {
+public:
+	World();
+	~World();
+
+	// reference-returning queries will error if the object does not exist
+
+	inline Room &globalRoom() const { return *_globalRoom; }
+	inline Inventory &inventory() const { return *_inventory; }
+	inline MainCharacter &filemon() const { return *_filemon; }
+	inline MainCharacter &mortadelo() const { return *_mortadelo; }
+	inline const Common::String &initScriptName() const { return _initScriptName; }
+	inline uint8 loadedMapCount() const { return _loadedMapCount; }
+
+	inline Room *&currentRoom() { return _currentRoom; }
+	inline Room *currentRoom() const { return _currentRoom; }
+
+	MainCharacter &getMainCharacterByKind(MainCharacterKind kind) const;
+	Room *getRoomByName(const Common::String &name) const;
+	ObjectBase *getObjectByName(const Common::String &name) const;
+	const Common::String &getGlobalAnimationName(GlobalAnimationKind kind) const;
+
+private:
+	bool loadWorldFile(const char *path);
+
+	Common::Array<Room *> _rooms;
+	Common::String _globalAnimationNames[(int)GlobalAnimationKind::Count];
+	Common::String _initScriptName;
+	Room *_globalRoom, *_currentRoom = nullptr;
+	Inventory *_inventory;
+	MainCharacter *_filemon, *_mortadelo;
+	uint8 _loadedMapCount = 0;
+};
+
+}
+
+#endif // ROOMS_H
diff --git a/engines/alcachofa/shape.cpp b/engines/alcachofa/shape.cpp
new file mode 100644
index 00000000000..74fe921f75d
--- /dev/null
+++ b/engines/alcachofa/shape.cpp
@@ -0,0 +1,140 @@
+/* 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 "shape.h"
+#include "stream-helper.h"
+
+using namespace Common;
+
+namespace Alcachofa {
+
+Shape::Shape() {}
+
+Shape::Shape(ReadStream &stream) {
+	auto complexity = stream.readByte();
+	uint8 pointsPerPolygon;
+	if (complexity < 0 || complexity > 3)
+		error("Invalid shape complexity %d", complexity);
+	else if (complexity == 3)
+		pointsPerPolygon = 0; // read in per polygon
+	else
+		pointsPerPolygon = 1 << complexity;
+
+	int polygonCount = stream.readUint16LE();
+	_polygons.reserve(polygonCount);
+	_points.reserve(MIN(3, (int)pointsPerPolygon) * polygonCount);
+	for (int i = 0; i < polygonCount; i++) {
+		auto pointCount = pointsPerPolygon == 0
+			? stream.readByte()
+			: pointsPerPolygon;
+		for (int j = 0; j < pointCount; j++)
+			_points.push_back(readPoint(stream));
+		addPolygon(pointCount);
+	}
+}
+
+uint Shape::addPolygon(uint maxCount) {
+	// Common functionality of shapes is that polygons are reduced
+	// so that the first point is not duplicated
+	uint firstI = empty() ? 0 : _polygons.back().first + _polygons.back().second;
+	uint newCount = maxCount;
+	if (maxCount > 1) {
+		for (newCount = 1; newCount < maxCount; newCount++) {
+			if (_points[firstI + newCount] == _points[firstI])
+				break;
+		}
+	}
+	_polygons.push_back({ firstI, newCount });
+	return newCount;
+}
+
+Polygon Shape::at(uint index) {
+	auto range = _polygons[index];
+	Polygon p;
+	p._points = Span<Point>(_points.data() + range.first, range.second);
+	return p;
+}
+
+PathFindingShape::PathFindingShape() {}
+
+PathFindingShape::PathFindingShape(ReadStream &stream) {
+	auto polygonCount = stream.readUint16LE();
+	_polygons.reserve(polygonCount);
+	_polygonValues.reserve(polygonCount);
+	_points.reserve(polygonCount * kPointsPerPolygon);
+	_pointValues.reserve(polygonCount * kPointsPerPolygon);
+
+	for (int i = 0; i < polygonCount; i++) {
+		for (int j = 0; j < kPointsPerPolygon; j++)
+			_points.push_back(readPoint(stream));
+		_polygonValues.push_back(stream.readSByte());
+		for (int j = 0; j < kPointsPerPolygon; j++)
+			_pointValues.push_back(stream.readSByte());
+
+		addPolygon(kPointsPerPolygon);
+	}
+
+	// TODO: Implement the path finding
+}
+
+PathFindingPolygon PathFindingShape::at(uint index) {
+	auto range = _polygons[index];
+	PathFindingPolygon p;
+	p._points = Span<Point>(_points.data() + range.first, range.second);
+	p._pointValues = Span<int8>(_pointValues.data() + range.first, range.second);
+	p._polygonValue = _polygonValues[index];
+	return p;
+}
+
+FloorColorShape::FloorColorShape() {}
+
+FloorColorShape::FloorColorShape(ReadStream &stream) {
+	auto polygonCount = stream.readUint16LE();
+	_polygons.reserve(polygonCount);
+	_polygonValues.reserve(polygonCount);
+	_points.reserve(polygonCount * kPointsPerPolygon);
+	_pointColors.reserve(polygonCount * kPointsPerPolygon);
+	_pointWeights.reserve(polygonCount * kPointsPerPolygon);
+
+	for (int i = 0; i < polygonCount; i++) {
+		for (int j = 0; j < kPointsPerPolygon; j++)
+			_points.push_back(readPoint(stream));
+		for (int j = 0; j < kPointsPerPolygon; j++)
+			_pointWeights.push_back(stream.readSByte());
+		for (int j = 0; j < kPointsPerPolygon; j++)
+			_pointColors.push_back(stream.readUint32LE());
+		_polygonValues.push_back(stream.readSByte());
+
+		addPolygon(kPointsPerPolygon);
+	}
+}
+
+FloorColorPolygon FloorColorShape::at(uint index) {
+	auto range = _polygons[index];
+	FloorColorPolygon p;
+	p._points = Span<Point>(_points.data() + range.first, range.second);
+	p._pointWeights = Span<uint8>(_pointWeights.data() + range.first, range.second);
+	p._pointColors = Span<uint32>(_pointColors.data() + range.first, range.second);
+	p._polygonValue = _polygonValues[index];
+	return p;
+}
+
+}
diff --git a/engines/alcachofa/shape.h b/engines/alcachofa/shape.h
new file mode 100644
index 00000000000..4bb3bd8e95d
--- /dev/null
+++ b/engines/alcachofa/shape.h
@@ -0,0 +1,156 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SHAPE_H
+#define SHAPE_H
+
+#include "common/stream.h"
+#include "common/array.h"
+#include "common/rect.h"
+#include "common/span.h"
+#include "common/util.h"
+#include "math/vector2d.h"
+
+namespace Alcachofa {
+
+struct Polygon {
+	Common::Span<Common::Point> _points;
+};
+
+struct PathFindingPolygon : Polygon {
+	Common::Span<int8> _pointValues;
+	int8 _polygonValue;
+};
+
+struct FloorColorPolygon : Polygon {
+	Common::Span<uint32> _pointColors;
+	Common::Span<uint8> _pointWeights;
+	int8 _polygonValue;
+};
+
+template<class TShape, typename TPolygon>
+struct PolygonIterator {
+	using difference_type = uint;
+	using value_type = TPolygon;
+
+	inline value_type operator*() const {
+		return _shape.at(_index);
+	}
+
+	inline PolygonIterator<TShape, TPolygon> &operator++() {
+		assert(_index < _shape.polygonCount());
+		_index++;
+		return *this;
+	}
+
+	inline PolygonIterator<TShape, TPolygon> &operator++(int) {
+		assert(_index < _shape.polygonCount());
+		auto tmp = *this;
+		++*this;
+		return tmp;
+	}
+
+private:
+	friend typename Common::remove_const_t<TShape>;
+	PolygonIterator(TShape &shape, uint index = 0)
+		: _shape(shape)
+		, _index(index) {
+	}
+
+	TShape &_shape;
+	uint _index;
+};
+
+class Shape {
+public:
+	using iterator = PolygonIterator<Shape, Polygon>;
+
+	Shape();
+	Shape(Common::ReadStream &stream);
+
+	inline Common::Point firstPoint() const { return _points.empty() ? Common::Point() : _points[0]; }
+	inline uint polygonCount() const { return _polygons.size(); }
+	inline bool empty() const { return polygonCount() == 0; }
+	inline iterator begin() { return { *this, 0 }; }
+	inline iterator end() { return { *this, polygonCount() }; }
+
+	Polygon at(uint index);
+
+protected:
+	uint addPolygon(uint maxCount);
+
+	using PolygonRange = Common::Pair<uint, uint>;
+	Common::Array<PolygonRange> _polygons;
+	Common::Array<Common::Point> _points;
+};
+
+/**
+ * @brief Path finding is based on the Shape class with the invariant that
+ * every polygon is a convex quad.
+ * Equal points of different quads link them together, for edges we add an
+ * additional link point in the center of the edge.
+ *
+ * The resulting graph is processed using Floyd-Warshall to precalculate for
+ * the actual path finding. Additionally we check whether a character can
+ * walk straight through an edge instead of following the link points.
+ *
+ * None of this is implemented yet by the way ;)
+ */
+class PathFindingShape final : public Shape {
+public:
+	using iterator = PolygonIterator<PathFindingShape, PathFindingPolygon>;
+	static constexpr const uint kPointsPerPolygon = 4;
+
+	PathFindingShape();
+	PathFindingShape(Common::ReadStream &stream);
+
+	inline iterator begin() { return { *this, 0 }; }
+	inline iterator end() { return { *this, polygonCount() }; }
+
+	PathFindingPolygon at(uint index);
+
+private:
+	Common::Array<int8> _pointValues;
+	Common::Array<int8> _polygonValues;
+};
+
+class FloorColorShape final : public Shape {
+public:
+	using iterator = PolygonIterator<FloorColorShape, FloorColorPolygon>;
+	static constexpr const uint kPointsPerPolygon = 4;
+
+	FloorColorShape();
+	FloorColorShape(Common::ReadStream &stream);
+
+	inline iterator begin() { return { *this, 0 }; }
+	inline iterator end() { return { *this, polygonCount() }; }
+
+	FloorColorPolygon at(uint index);
+
+private:
+	Common::Array<uint32> _pointColors;
+	Common::Array<uint8> _pointWeights;
+	Common::Array<int8> _polygonValues;
+};
+
+}
+
+#endif // SHAPE_H
diff --git a/engines/alcachofa/stream-helper.cpp b/engines/alcachofa/stream-helper.cpp
new file mode 100644
index 00000000000..f849549f19a
--- /dev/null
+++ b/engines/alcachofa/stream-helper.cpp
@@ -0,0 +1,74 @@
+/* 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 "stream-helper.h"
+
+#include "common/textconsole.h"
+
+using namespace Common;
+
+namespace Alcachofa {
+
+bool readBool(ReadStream &stream) {
+	return stream.readByte() != 0;
+}
+
+Point readPoint(ReadStream &stream) {
+	return { (int16)stream.readSint32LE(), (int16)stream.readSint32LE() };
+}
+
+static uint32 readVarInt(ReadStream &stream) {
+	uint32 length = stream.readByte();
+	if (length != 0xFF)
+		return length;
+	length = stream.readUint16LE();
+	if (length != 0xFFFF)
+		return length;
+	return stream.readUint32LE();
+}
+
+String readVarString(ReadStream &stream) {
+	uint32 length = readVarInt(stream);
+	if (length == 0)
+		return Common::String();
+
+	// TODO: Being able to resize a string would avoid the double-allocation :/
+	char *buffer = new char[length];
+	if (buffer == nullptr)
+		error("Out of memory in readVarString");
+	if (stream.read(buffer, length) != length)
+		error("Could not read all %u bytes in readVarString", length);
+
+	String result(buffer, buffer + length);
+	delete[] buffer;
+	return result;
+}
+
+void skipVarString(SeekableReadStream &stream) {
+	stream.skip(readVarInt(stream));
+}
+
+void syncPoint(Serializer &serializer, Point &point) {
+	serializer.syncAsSint32LE(point.x);
+	serializer.syncAsSint32LE(point.y);
+}
+
+}
diff --git a/engines/alcachofa/stream-helper.h b/engines/alcachofa/stream-helper.h
new file mode 100644
index 00000000000..07bab1338a4
--- /dev/null
+++ b/engines/alcachofa/stream-helper.h
@@ -0,0 +1,56 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef STREAM_HELPER_H
+#define STREAM_HELPER_H
+
+#include "common/stream.h"
+#include "common/serializer.h"
+#include "common/rect.h"
+
+namespace Alcachofa {
+
+bool readBool(Common::ReadStream &stream);
+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);
+
+template<typename T>
+inline void syncArray(Common::Serializer &serializer, Common::Array<T> &array, void (*serializeFunction)(Common::Serializer &, T &)) {
+	auto size = array.size();
+	serializer.syncAsUint32LE(size);
+	array.resize(size);
+	serializer.syncArray(array.data(), size, serializeFunction);
+}
+
+template<typename T>
+inline void syncEnum(Common::Serializer &serializer, T &enumValue) {
+	// syncAs does not have a cast for saving
+	int32 intValue = static_cast<int32>(enumValue);
+	serializer.syncAsSint32LE(intValue);
+	enumValue = static_cast<T>(intValue);
+}
+
+}
+
+#endif // STREAM_HELPER_H
diff --git a/engines/alcachofa/ui-objects.cpp b/engines/alcachofa/ui-objects.cpp
new file mode 100644
index 00000000000..c845ba2492e
--- /dev/null
+++ b/engines/alcachofa/ui-objects.cpp
@@ -0,0 +1,121 @@
+/* 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 "objects.h"
+#include "rooms.h"
+#include "stream-helper.h"
+
+using namespace Common;
+
+namespace Alcachofa {
+
+MenuButton::MenuButton(Room *room, ReadStream &stream)
+	: PhysicalObject(room, stream)
+	, _actionId(stream.readSint32LE())
+	, _graphicNormal(stream)
+	, _graphicHovered(stream)
+	, _graphicClicked(stream)
+	, _graphicDisabled(stream) {
+}
+
+InternetMenuButton::InternetMenuButton(Room *room, ReadStream &stream)
+	: MenuButton(room, stream) {
+}
+
+OptionsMenuButton::OptionsMenuButton(Room *room, ReadStream &stream)
+	: MenuButton(room, stream) {
+}
+
+MainMenuButton::MainMenuButton(Room *room, ReadStream &stream)
+	: MenuButton(room, stream) {
+}
+
+PushButton::PushButton(Room *room, ReadStream &stream)
+	: PhysicalObject(room, stream)
+	, _alwaysVisible(readBool(stream))
+	, _graphic1(stream)
+	, _graphic2(stream)
+	, _actionId(stream.readSint32LE()) {
+}
+
+EditBox::EditBox(Room *room, ReadStream &stream)
+	: PhysicalObject(room, stream)
+	, i1(stream.readSint32LE())
+	, p1(Shape(stream).firstPoint())
+	, _labelId(readVarString(stream))
+	, b1(readBool(stream))
+	, i3(stream.readSint32LE())
+	, i4(stream.readSint32LE())
+	, i5(stream.readSint32LE())
+	, _fontId(stream.readSint32LE()) {
+}
+
+CheckBox::CheckBox(Room *room, ReadStream &stream)
+	: PhysicalObject(room, stream)
+	, b1(readBool(stream))
+	, _graph1(stream)
+	, _graph2(stream)
+	, _graph3(stream)
+	, _graph4(stream)
+	, _valueId(stream.readSint32LE()) {
+}
+
+CheckBoxAutoAdjustNoise::CheckBoxAutoAdjustNoise(Room *room, ReadStream &stream)
+	: CheckBox(room, stream) {
+	stream.readByte(); // unused and ignored byte
+}
+
+SlideButton::SlideButton(Room *room, ReadStream &stream)
+	: ObjectBase(room, stream)
+	, i1(stream.readSint32LE())
+	, p1(Shape(stream).firstPoint())
+	, p2(Shape(stream).firstPoint())
+	, _graph1(stream)
+	, _graph2(stream)
+	, _graph3(stream) {
+}
+
+IRCWindow::IRCWindow(Room *room, ReadStream &stream)
+	: ObjectBase(room, stream)
+	, _p1(Shape(stream).firstPoint())
+	, _p2(Shape(stream).firstPoint()) {
+}
+
+MessageBox::MessageBox(Room *room, ReadStream &stream)
+	: ObjectBase(room, stream)
+	, _graph1(stream)
+	, _graph2(stream)
+	, _graph3(stream)
+	, _graph4(stream)
+	, _graph5(stream) {
+	_graph1.start(true);
+	_graph2.start(true);
+	_graph3.start(true);
+	_graph4.start(true);
+	_graph5.start(true);
+}
+
+VoiceMeter::VoiceMeter(Room *room, ReadStream &stream)
+	: GraphicObject(room, stream) {
+	stream.readByte(); // unused and ignored byte
+}
+
+}


Commit: ea67e329f380da83cbfe3d821deb69ee3a1a8132
    https://github.com/scummvm/scummvm/commit/ea67e329f380da83cbfe3d821deb69ee3a1a8132
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:44+02:00

Commit Message:
ALCACHOFA: Add initial animation and OpenGL support

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


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 813f2a4c30a..e09fe231768 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -56,11 +56,13 @@ Common::String AlcachofaEngine::getGameId() const {
 
 Common::Error AlcachofaEngine::run() {
 	// Initialize 320x200 paletted graphics mode
-	initGraphics(320, 200);
-	_screen = new Graphics::Screen();
+	_renderer.reset(IRenderer::createOpenGLRenderer(Common::Point(1024, 768)));
 
 	auto world = new World();
 	delete world;
+	auto animation = new Animation("MORTADELO_ACOSTANDOSE");
+	animation->load();
+	animation->draw2D(0, Math::Vector2d(512, 300), 1.0f, Math::Angle(), BlendMode::Alpha, { 255, 255, 255, 255 });
 
 	// Set the engine's debugger console
 	setDebugger(new Console());
@@ -70,21 +72,27 @@ Common::Error AlcachofaEngine::run() {
 	if (saveSlot != -1)
 		(void)loadGameState(saveSlot);
 
-	// Draw a series of boxes on screen as a sample
-	for (int i = 0; i < 100; ++i)
-		_screen->frameRect(Common::Rect(i, i, 320 - i, 200 - i), i);
-	_screen->update();
-
 	// Simple event handling loop
 	byte pal[256 * 3] = { 0 };
 	Common::Event e;
 	int offset = 0;
 
 	Graphics::FrameLimiter limiter(g_system, 60);
+	int32 frame = 0;
+	uint32 nextSecond = g_system->getMillis();
 	while (!shouldQuit()) {
 		while (g_system->getEventManager()->pollEvent(e)) {
 		}
 
+		_renderer->begin();
+
+		if (g_system->getMillis() >= nextSecond) {
+			frame = (frame + 1) % animation->frameCount();
+			nextSecond = g_system->getMillis() + animation->frameDuration(frame);
+		}
+
+		animation->draw2D(frame, Math::Vector2d(100, 100), 1.0f, Math::Angle(), BlendMode::Alpha, { 255, 255, 255, 255 });
+
 		// Cycle through a simple palette
 		++offset;
 		for (int i = 0; i < 256; ++i)
@@ -93,10 +101,12 @@ Common::Error AlcachofaEngine::run() {
 		// Delay for a bit. All events loops should have a delay
 		// to prevent the system being unduly loaded
 		limiter.delayBeforeSwap();
-		_screen->update();
+		_renderer->end();
 		limiter.startFrame();
 	}
 
+	delete animation;
+
 	return Common::kNoError;
 }
 
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index ae6eb72cc92..a703d18341b 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -35,6 +35,7 @@
 #include "graphics/screen.h"
 
 #include "alcachofa/detection.h"
+#include "alcachofa/graphics.h"
 
 namespace Alcachofa {
 
@@ -47,12 +48,12 @@ private:
 protected:
 	// Engine APIs
 	Common::Error run() override;
-public:
-	Graphics::Screen *_screen = nullptr;
 public:
 	AlcachofaEngine(OSystem *syst, const ADGameDescription *gameDesc);
 	~AlcachofaEngine() override;
 
+	inline IRenderer &renderer() const { return *_renderer; }
+
 	uint32 getFeatures() const;
 
 	/**
@@ -95,6 +96,10 @@ public:
 		Common::Serializer s(stream, nullptr);
 		return syncGame(s);
 	}
+
+private:
+	Graphics::Screen *_screen = nullptr;
+	Common::ScopedPtr<IRenderer> _renderer;
 };
 
 extern AlcachofaEngine *g_engine;
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 0aacd2510dc..b77281f9b76 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -159,7 +159,7 @@ void MainCharacter::serializeSave(Serializer &serializer) {
 
 Background::Background(Room *room, const String &animationFileName, int16 scale)
 	: GraphicObject(room, "BACKGROUND") {
-	_graphic._animation.reset(new Animation(animationFileName, AnimationFolder::Fondos));
+	_graphic._animation.reset(new Animation(animationFileName, AnimationFolder::Backgrounds));
 	_graphic._scale = scale;
 	_graphic._order = 59;
 }
diff --git a/engines/alcachofa/graphics-opengl.cpp b/engines/alcachofa/graphics-opengl.cpp
new file mode 100644
index 00000000000..4a376b1a40f
--- /dev/null
+++ b/engines/alcachofa/graphics-opengl.cpp
@@ -0,0 +1,297 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "graphics.h"
+
+#include "common/system.h"
+#include "engines/util.h"
+#include "graphics/managed_surface.h"
+#include "graphics/opengl/system_headers.h"
+#include "graphics/opengl/debug.h"
+
+using namespace Common;
+using namespace Math;
+using namespace Graphics;
+
+namespace Alcachofa {
+
+struct OpenGLFormat {
+	GLenum _format, _type;
+	inline bool isValid() const { return _format != GL_NONE; }
+};
+
+static bool areComponentsInOrder(const PixelFormat &format, int r, int g, int b, int a) {
+	return format == (a < 0
+		? PixelFormat(3, 8, 8, 8, 0, r * 8, g * 8, b * 8, 0)
+		: PixelFormat(4, 8, 8, 8, 8, r * 8, g * 8, b * 8, a * 8));
+}
+
+static OpenGLFormat getOpenGLFormatOf(const PixelFormat &format) {
+	if (areComponentsInOrder(format, 0, 1, 2, 3))
+		return { GL_RGBA, GL_UNSIGNED_BYTE };
+	else if (areComponentsInOrder(format, 3, 2, 1, 0))
+		return { GL_RGBA, GL_UNSIGNED_INT_8_8_8_8 };
+	else if (areComponentsInOrder(format, 0, 1, 2, -1))
+		return { GL_RGB, GL_UNSIGNED_BYTE };
+	else if (areComponentsInOrder(format, 2, 1, 0, 3))
+		return { GL_BGRA, GL_UNSIGNED_BYTE };
+	else if (areComponentsInOrder(format, 2, 1, 0, -1))
+		return { GL_BGR, GL_UNSIGNED_BYTE };
+	// we could look for packed formats here as well in the future
+	else
+		return { GL_NONE, GL_NONE };
+}
+
+
+class OpenGLTexture : public ITexture {
+public:
+	OpenGLTexture(int32 w, int32 h, bool withMipmaps)
+		: ITexture({ (int16)w, (int16)h })
+		, _withMipmaps(withMipmaps) {
+		GL_CALL(glEnable(GL_TEXTURE_2D));
+		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));
+		GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT));
+		GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT));
+	}
+
+	virtual ~OpenGLTexture() override {
+		if (_handle != 0)
+			GL_CALL(glDeleteTextures(1, &_handle));
+	}
+
+	virtual void update(const ManagedSurface &surface) {
+		OpenGLFormat format = getOpenGLFormatOf(surface.format);
+		assert(surface.w == size().x && surface.h == size().y);
+		assert(format.isValid());
+
+		GL_CALL(glEnable(GL_TEXTURE_2D));
+		GL_CALL(glBindTexture(GL_TEXTURE_2D, _handle));
+		GL_CALL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, surface.w, surface.h, 0, format._format, format._type, surface.getPixels()));
+		if (_withMipmaps)
+			GL_CALL(glGenerateMipmap(GL_TEXTURE_2D));
+		else
+			GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0));
+	}
+
+	inline GLuint handle() const { return _handle; }
+
+private:
+	GLuint _handle;
+	bool _withMipmaps;
+};
+
+class OpenGLRenderer : public IRenderer {
+public:
+	OpenGLRenderer(Point resolution)
+		: _resolution(resolution) {
+		initViewportAndMatrices();
+		GL_CALL(glDisable(GL_LIGHTING));
+		GL_CALL(glDisable(GL_DEPTH_TEST));
+		GL_CALL(glDisable(GL_SCISSOR_TEST));
+		GL_CALL(glDisable(GL_STENCIL_TEST));
+		GL_CALL(glEnable(GL_BLEND));
+		GL_CALL(glDepthMask(GL_FALSE));
+	}
+
+	virtual ScopedPtr<ITexture> createTexture(int32 w, int32 h, bool withMipmaps) override {
+		assert(w > 0 && h > 0);
+		return ScopedPtr<ITexture>(new OpenGLTexture(w, h, withMipmaps));
+	}
+
+	virtual void begin() override {
+		GL_CALL(glEnableClientState(GL_VERTEX_ARRAY));
+		GL_CALL(glDisableClientState(GL_INDEX_ARRAY));
+		GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE));
+		GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_TEXTURE));
+		GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_CONSTANT));
+		GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_ALPHA, GL_CONSTANT));
+		_currentLodBias = -1000.0f;
+		_currentTexture = nullptr;
+		_currentBlendMode = (BlendMode)-1;
+
+#ifdef _DEBUG
+		glClearColor(0.5f, 0.0f, 0.5f, 1.0f);
+		glClear(GL_COLOR_BUFFER_BIT);
+#endif
+	}
+
+	virtual void end() override {
+		GL_CALL(glFlush());
+		g_system->updateScreen();
+	}
+
+	virtual void setTexture(const ITexture *texture) override {
+		if (texture == _currentTexture)
+			return;
+		else if (texture == nullptr) {
+			GL_CALL(glDisable(GL_TEXTURE_2D));
+			GL_CALL(glDisableClientState(GL_TEXTURE_COORD_ARRAY));
+		}
+		else {
+			if (_currentTexture == nullptr) {
+				GL_CALL(glEnable(GL_TEXTURE_2D));
+				GL_CALL(glEnableClientState(GL_TEXTURE_COORD_ARRAY));
+			}
+			auto glTexture = dynamic_cast<const OpenGLTexture *>(texture);
+			assert(glTexture != nullptr);
+			GL_CALL(glBindTexture(GL_TEXTURE_2D, glTexture->handle()));
+		}
+		_currentTexture = texture;
+	}
+
+	virtual void setBlendMode(BlendMode blendMode) override {
+		if (blendMode == _currentBlendMode)
+			return;
+		// first the blend func
+		switch (blendMode) {
+		case BlendMode::AdditiveAlpha:
+			GL_CALL(glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA));
+			break;
+		case BlendMode::Additive:
+			GL_CALL(glBlendFunc(GL_ONE, GL_ONE));
+			break;
+		case BlendMode::Multiply:
+			GL_CALL(glBlendFunc(GL_DST_COLOR, GL_ONE));
+			break;
+		case BlendMode::Alpha:
+			GL_CALL(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
+			break;
+		case BlendMode::Tinted:
+			GL_CALL(glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA));
+			break;
+		default: assert(false && "Invalid blend mode"); break;
+		}
+
+		/** now the texture stage, mind that this always applies:
+		 * SRC0_RGB is TEXTURE
+		 * SRC1_RGB/ALPHA is CONSTANT
+		 * COMBINE_ALPHA is REPLACE
+		 */ 
+		switch (blendMode) {
+		case BlendMode::AdditiveAlpha:
+		case BlendMode::Additive:
+		case BlendMode::Multiply:
+			// (1 - TintAlpha) * TexColor, TexAlpha
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_ONE_MINUS_SRC_ALPHA));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_ALPHA, GL_TEXTURE));
+			break;
+		case BlendMode::Alpha:
+			// TexColor, TintAlpha
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_REPLACE));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_ALPHA, GL_CONSTANT));
+			break;
+		case BlendMode::Tinted:
+			// (TintColor * TintAlpha) * TexColor, TexAlpha
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR)); // pre-multiplied with alpha
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_ALPHA, GL_TEXTURE));
+			break;
+		default: assert(false && "Invalid blend mode"); break;
+		}
+		_currentBlendMode = blendMode;
+	}
+
+	virtual void setLodBias(float lodBias) override {
+		if (abs(_currentLodBias - lodBias) < epsilon)
+			return;
+		GL_CALL(glTexEnvf(GL_TEXTURE_FILTER_CONTROL, GL_TEXTURE_LOD_BIAS, lodBias));
+		_currentLodBias = lodBias;
+	}
+
+	virtual void quad(
+		Vector2d center,
+		Vector2d size,
+		Color color,
+		Angle rotation,
+		Vector2d texMin,
+		Vector2d texMax) override {
+		size *= 0.5f;
+		center += size;
+		Vector2d positions[] = {
+			center + Vector2d(-size.getX(), -size.getY()),
+			center + Vector2d(-size.getX(), +size.getY()),
+			center + Vector2d(+size.getX(), +size.getY()),
+			center + Vector2d(+size.getX(), -size.getY()),
+		};
+		if (abs(rotation.getDegrees()) > epsilon) {
+			const Vector2d zero(0, 0);
+			for (int i = 0; i < 4; i++)
+				positions[i].rotateAround(zero, rotation);
+		}
+
+		Vector2d texCoords[] = {
+			{ texMin.getX(), texMin.getY() },
+			{ texMin.getX(), texMax.getY() },
+			{ texMax.getX(), texMax.getY() },
+			{ texMax.getX(), texMin.getY() }
+		};
+
+		float colors[] = { color.r / 255.0f, color.g / 255.0f, color.b / 255.0f, color.a / 255.0f };
+
+		glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
+		GL_CALL(glVertexPointer(2, GL_FLOAT, 0, positions));
+		if (_currentTexture != nullptr)
+			GL_CALL(glTexCoordPointer(2, GL_FLOAT, 0, texCoords));
+		GL_CALL(glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, colors));
+		GL_CALL(glDrawArrays(GL_QUADS, 0, 4));
+
+#if DEBUG
+		// make sure we crash instead of someone using our stack arrays
+		GL_CALL(glVertexPointer(2, GL_FLOAT, sizeof(Vector2d), nullptr));
+		GL_CALL(glTexCoordPointer(2, GL_FLOAT, sizeof(Vector2d), nullptr));
+#endif
+	}
+
+private:
+	void initViewportAndMatrices() {
+		int32 screenWidth = g_system->getWidth();
+		int32 screenHeight = g_system->getHeight();
+		Rect viewport(
+			MIN<int32>(screenWidth, screenHeight * (float)_resolution.x / _resolution.y),
+			MIN<int32>(screenHeight, screenWidth * (float)_resolution.y / _resolution.x));
+		viewport.translate(
+			(screenWidth - viewport.width()) / 2,
+			(screenHeight - viewport.height()) / 2);
+
+		GL_CALL(glViewport(viewport.left, viewport.top, viewport.width(), viewport.height()));
+		GL_CALL(glMatrixMode(GL_PROJECTION));
+		GL_CALL(glLoadIdentity());
+		GL_CALL(glOrtho(0.0f, _resolution.x, _resolution.y, 0.0f, -1.0f, 1.0f));
+		GL_CALL(glMatrixMode(GL_MODELVIEW));
+		GL_CALL(glLoadIdentity());
+	}
+
+	Point _resolution;
+	const ITexture *_currentTexture = nullptr;
+	BlendMode _currentBlendMode = (BlendMode)-1;
+	float _currentLodBias = 0.0f;
+};
+
+IRenderer *IRenderer::createOpenGLRenderer(Point resolution) {
+	initGraphics3d(resolution.x, resolution.y);
+	return new OpenGLRenderer(resolution);
+}
+
+}
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 6603ea99256..9230ba4fa0b 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -21,18 +21,236 @@
 
 #include "graphics.h"
 #include "stream-helper.h"
+#include "alcachofa.h"
 
 #include "common/system.h"
+#include "common/file.h"
+#include "common/substream.h"
+#include "image/tga.h"
 
 using namespace Common;
+using namespace Math;
+using namespace Image;
+using namespace Graphics;
 
 namespace Alcachofa {
 
-Animation::Animation(String fileName, AnimationFolder folder)
+ITexture::ITexture(Point size) : _size(size) {}
+
+AnimationBase::AnimationBase(String fileName, AnimationFolder folder)
 	: _fileName(move(fileName))
 	, _folder(folder) {
 }
 
+AnimationBase::~AnimationBase() {
+	freeImages();
+}
+
+void AnimationBase::load() {
+	if (_isLoaded)
+		return;
+
+	Common::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");
+	}
+	if (_fileName.size() < 4 || scumm_strnicmp(_fileName.end() - 4, ".AN0", 4) != 0)
+		_fileName += ".AN0";
+	fullPath += _fileName;
+	Common::File file;
+	if (!file.open(fullPath.c_str())) {
+		// original fallback
+		fullPath = "Mascaras/" + _fileName;
+		if (!file.open(fullPath.c_str()))
+			error("Could not open animation %s", _fileName.c_str());
+	}
+
+	uint spriteCount = file.readUint32LE();
+	assert(spriteCount < kMaxSpriteIDs);
+	_spriteBases.reserve(spriteCount);
+
+	uint imageCount = file.readUint32LE();
+	_images.reserve(imageCount);
+	_imageOffsets.reserve(imageCount);
+	for (uint i = 0; i < imageCount; i++) {
+		_images.push_back(readImage(file));
+	}
+
+	// an inconsistency, maybe a historical reason:
+	// the sprite bases are also stored as fixed 256 array, but as sprite *indices*
+	// 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(file.readUint32LE());
+		assert(_spriteBases.back() < imageCount);
+	}
+#ifdef _DEBUG
+	for (uint i = spriteCount; i < kMaxSpriteIDs; i++)
+		assert(file.readSint32LE() == 0);
+#else
+	file.skip(sizeof(int32) * (kMaxSpriteIDs - spriteCount));
+#endif
+
+	for (uint i = 0; i < imageCount; i++)
+		_imageOffsets.push_back(readPoint(file));
+	for (uint i = 0; i < kMaxSpriteIDs; i++)
+		_spriteIndexMapping[i] = file.readSint32LE();
+
+	uint frameCount = file.readUint32LE();
+	_frames.reserve(frameCount);
+	_spriteOffsets.reserve(frameCount * spriteCount);
+	for (uint i = 0; i < frameCount; i++) {
+		for (uint j = 0; j < spriteCount; j++)
+			_spriteOffsets.push_back(file.readUint32LE());
+		AnimationFrame frame;
+		frame._center = readPoint(file);
+		frame._offset = readPoint(file);
+		frame._duration = file.readUint32LE();
+		_frames.push_back(frame);
+	}
+
+	_isLoaded = true;
+}
+
+void AnimationBase::freeImages() {
+	if (!_isLoaded)
+		return;
+	for (auto *image : _images) {
+		if (image != nullptr)
+			delete image;
+	}
+	_isLoaded = false;
+}
+
+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());
+
+	// The length of the image is unknown but TGADecoder does not read
+	// the end marker, so let's search for it.
+	static const char *kExpectedMarker = "TRUEVISION-XFILE.";
+	static const uint kMarkerLength = 18;
+	char buffer[kMarkerLength] = { 0 };
+	char *potentialStart = buffer + kMarkerLength;
+	do {
+		uint nextRead = potentialStart - buffer;
+		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());
+		potentialStart = find(buffer + 1, buffer + kMarkerLength, kExpectedMarker[0]);
+	} while (strncmp(buffer, kExpectedMarker, kMarkerLength) != 0);
+
+	// instead of not storing unused frame images the animation contains
+	// transparent 2x1 images. Let's just ignore them.
+	auto source = decoder.getSurface();
+	if (source->w == 2 && source->h == 1)
+		return nullptr;
+
+	auto target = source->convertTo(BlendBlit::getSupportedPixelFormat(), decoder.getPalette(), decoder.getPaletteColorCount());	
+	return new ManagedSurface(target);
+}
+
+Animation::Animation(String fileName, AnimationFolder folder)
+	: AnimationBase(fileName, folder) {
+}
+
+void Animation::load() {
+	if (_isLoaded)
+		return;
+	AnimationBase::load();
+	const auto withMipmaps = _folder != AnimationFolder::Backgrounds;
+	Rect maxBounds = maxFrameBounds();
+	_renderedSurface.create(maxBounds.width(), maxBounds.height(), BlendBlit::getSupportedPixelFormat());
+	_renderedTexture = g_engine->renderer().createTexture(maxBounds.width(), maxBounds.height(), withMipmaps);
+}
+
+int32 Animation::imageIndex(int32 frameI, int32 spriteId) const {
+	assert(frameI >= 0 && (uint)frameI < frameCount());
+	assert(spriteId >= 0 && (uint)spriteId < spriteCount());
+	int32 spriteIndex = _spriteIndexMapping[spriteId];
+	int32 offset = _spriteOffsets[frameI * spriteCount() + spriteIndex];
+	return offset <= 0 ? -1
+		: offset + _spriteBases[spriteIndex] - 1;
+}
+
+Rect Animation::spriteBounds(int32 frameI, int32 spriteId) const {
+	int32 imageI = imageIndex(frameI, spriteId);
+	auto image = imageI < 0 ? nullptr : _images[imageI];
+	return image == nullptr ? Rect()
+		: Rect(_imageOffsets[imageI], image->w, image->h);
+}
+
+Rect Animation::frameBounds(int32 frameI) const {
+	if (spriteCount() == 0)
+		return Rect();
+	Rect bounds = spriteBounds(frameI, 0);
+	for (uint spriteI = 1; spriteI < spriteCount(); spriteI++)
+		bounds.extend(spriteBounds(frameI, spriteI));
+	return bounds;
+}
+
+Rect Animation::maxFrameBounds() const {
+	if (frameCount() == 0)
+		return Rect();
+	Rect bounds = frameBounds(0);
+	for (uint frameI = 1; frameI < frameCount(); frameI++)
+		bounds.extend(frameBounds(frameI));
+	return bounds;
+}
+
+void Animation::prerenderFrame(int32 frameI) {
+	assert(frameI >= 0 && (uint)frameI < frameCount());
+	if (frameI == _renderedFrameI)
+		return;
+	auto bounds = frameBounds(frameI);
+	_renderedSurface.clear();
+	for (uint spriteI = 0; spriteI < spriteCount(); spriteI++) {
+		int32 imageI = imageIndex(frameI, spriteI);
+		auto image = imageI < 0 ? nullptr : _images[imageI];
+		if (image == nullptr)
+			continue;
+		int offsetX = _imageOffsets[imageI].x - bounds.left;
+		int offsetY = _imageOffsets[imageI].y - bounds.top;
+		image->blendBlitTo(_renderedSurface, offsetX, offsetY);
+	}
+
+	if (_premultiplyAlpha != 100) {
+		byte *itPixel = (byte*)_renderedSurface.getPixels();
+		uint componentCount = _renderedSurface.w * _renderedSurface.h * 4;
+		for (uint32 i = 0; i < componentCount; i++, itPixel++)
+			*itPixel = *itPixel * _premultiplyAlpha / 100;
+	}
+
+	_renderedTexture->update(_renderedSurface);
+	_renderedFrameI = frameI;
+}
+
+void Animation::draw2D(int32 frameI, Vector2d center, float scale, Angle rotation, 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);
+
+	Vector2d size(bounds.width(), bounds.height());
+	Vector2d offset(
+		bounds.left - _frames[frameI]._center.x + _frames[frameI]._offset.x,
+		bounds.top - _frames[frameI]._center.y + _frames[frameI]._offset.y);
+	center += offset * scale;
+	size *= scale;
+
+	auto &renderer = g_engine->renderer();
+	renderer.setTexture(_renderedTexture.get());
+	//renderer.setTexture(nullptr);
+	renderer.setBlendMode(blendMode);
+	renderer.quad(center, size, color, rotation, texMin, texMax);
+}
+
 Graphic::Graphic() {
 }
 
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index d7e20a458fe..987416d6533 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -26,9 +26,31 @@
 #include "common/stream.h"
 #include "common/serializer.h"
 #include "common/rect.h"
+#include "math/vector2d.h"
+#include "graphics/managed_surface.h"
 
 namespace Alcachofa {
 
+/**
+ * Because this gets confusing fast, here in tabular form
+ *
+ * | BlendMode     | SrcColor                         | SrcAlpha  | SrcBlend | DstBlend     |
+ * |:-------------:|:---------------------------------|:----------|:---------|:-------------|
+ * | AdditiveAlpha | (1 - TintAlpha) * TexColor       | TexAlpha  | One      | 1 - SrcAlpha |
+ * | Additive      | (1 - TintAlpha) * TexColor       | TexAlpha  | One      | One          |
+ * | Multiply      | (1 - TintAlpha) * TexColor       | TexAlpha  | DstColor | One          |
+ * | Alpha         | TexColor                         | TintAlpha | SrcAlpha | 1 - SrcAlpha |
+ * | Tinted        | TintColor * TintAlpha * TexColor | TexAlpha  | One      | 1 - SrcAlpha |
+ *
+ */
+enum class BlendMode {
+	AdditiveAlpha,
+	Additive,
+	Multiply,
+	Alpha,
+	Tinted
+};
+
 enum class CursorType {
 	Normal,
 	LookAt,
@@ -49,21 +71,135 @@ enum class Direction {
 
 constexpr const int32 kDirectionCount = 4;
 
+struct Color {
+	uint8 b, g, r, a;
+};
+
+class ITexture {
+public:
+	ITexture(Common::Point size);
+	virtual ~ITexture() = default;
+
+	virtual void update(const Graphics::ManagedSurface &surface) = 0;
+
+	inline const Common::Point &size() const { return _size; }
+
+private:
+	Common::Point _size;
+};
+
+class IRenderer {
+public:
+	virtual ~IRenderer() = default;
+
+	virtual Common::ScopedPtr<ITexture> createTexture(int32 w, int32 h, bool withMipmaps = true) = 0;
+
+	virtual void begin() = 0;
+	virtual void setTexture(const ITexture *texture) = 0;
+	virtual void setBlendMode(BlendMode blendMode) = 0;
+	virtual void setLodBias(float lodBias) = 0;
+	virtual void quad(
+		Math::Vector2d center,
+		Math::Vector2d size,
+		Color color = { 255, 255, 255, 255 },
+		Math::Angle rotation = Math::Angle(),
+		Math::Vector2d texMin = Math::Vector2d(0, 0),
+		Math::Vector2d texMax = Math::Vector2d(1, 1)) = 0;
+	virtual void end() = 0;
+
+	static IRenderer *createOpenGLRenderer(Common::Point resolution);
+};
+
 enum class AnimationFolder {
 	Animations,
-	Maskaras,
-	Fondos
+	Masks,
+	Backgrounds
 };
 
-class Animation {
+struct AnimationFrame {
+	Common::Point
+		_center, ///< the center is used for more than just drawing the animation frame
+		_offset; ///< the offset is only used for drawing the animation frame
+	uint32 _duration;
+};
+
+/**
+ * An animation contains one or more sprites which change their position and image during playback.
+ *
+ * Internally there is a single list of images. Every sprite ID is mapped to an index
+ * (via _spriteIndexMapping) which points to:
+ *   1. The fixed image base for that sprite
+ *   2. The image offset for that sprite for the current frame
+ * Image indices are unfortunately one-based
+ *
+ * As fonts are handled very differently they are split into a second class
+ */
+class AnimationBase {
+protected:
+	AnimationBase(Common::String fileName, AnimationFolder folder = AnimationFolder::Animations);
+	~AnimationBase();
+
+	void load();
+	void freeImages();
+	Graphics::ManagedSurface *readImage(Common::SeekableReadStream &stream) const;
+
+	static constexpr const uint kMaxSpriteIDs = 256;
+	Common::String _fileName;
+	AnimationFolder _folder;
+	bool _isLoaded = false;
+
+	int32 _spriteIndexMapping[kMaxSpriteIDs] = { -1 };
+	Common::Array<uint32>
+		_spriteOffsets, ///< index offset per sprite and animation frame
+		_spriteBases; ///< base index per sprite
+	Common::Array<AnimationFrame> _frames;
+	Common::Array<Graphics::ManagedSurface *> _images; ///< will contain nullptr for fake images
+	Common::Array<Common::Point> _imageOffsets;
+};
+
+/**
+ * Animations prerenders its sprites into a single texture for a set frame.
+ * This prerendering can be customized with a alpha to be premultiplied
+ */
+class Animation : private AnimationBase {
 public:
 	Animation(Common::String fileName, AnimationFolder folder = AnimationFolder::Animations);
 
+	void load();
+	using AnimationBase::freeImages;
+
+	inline bool isLoaded() const { return _isLoaded; }
+	inline uint frameCount() const { return _frames.size(); }
+	inline uint spriteCount() const { return _spriteBases.size(); }
+	inline uint32 frameDuration(int32 frameI) const { return _frames[frameI]._duration; }
+
+	void draw2D(
+		int32 frameI,
+		Math::Vector2d center,
+		float scale,
+		Math::Angle rotation,
+		BlendMode blendMode,
+		Color color);
+
 private:
-	Common::String _fileName;
-	AnimationFolder _folder;
+	int32 imageIndex(int32 frameI, int32 spriteI) const;
+	Common::Rect spriteBounds(int32 frameI, int32 spriteI) const;
+	Common::Rect frameBounds(int32 frameI) const;
+	Common::Rect maxFrameBounds() const;
+	void prerenderFrame(int32 frameI);
+
+	int32_t _renderedFrameI = -1;
+	uint8 _premultiplyAlpha = 100; ///< in percent [0-100] not [0-255]
+
+	Graphics::ManagedSurface _renderedSurface;
+	Common::ScopedPtr<ITexture> _renderedTexture;
+};
+
+class Font : private AnimationBase {
+
 };
 
+
 class Graphic {
 public:
 	Graphic();
@@ -88,11 +224,6 @@ private:
 	float _camAcceleration = 1.0f;
 };
 
-class IGraphics {
-public:
-	virtual ~IGraphics() = default;
-};
-
 }
 
 #endif


Commit: 51836a48cd6166c3ee40c19aeedc1ad99825d213
    https://github.com/scummvm/scummvm/commit/51836a48cd6166c3ee40c19aeedc1ad99825d213
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:44+02:00

Commit Message:
ALCACHOFA: Add Graphic playback methods

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


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index e09fe231768..1d8d2fb793e 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -60,9 +60,10 @@ Common::Error AlcachofaEngine::run() {
 
 	auto world = new World();
 	delete world;
-	auto animation = new Animation("MORTADELO_ACOSTANDOSE");
-	animation->load();
-	animation->draw2D(0, Math::Vector2d(512, 300), 1.0f, Math::Angle(), BlendMode::Alpha, { 255, 255, 255, 255 });
+	Graphic graphic;
+	graphic.setAnimation("MORTADELO_ACOSTANDOSE", AnimationFolder::Animations);
+	graphic.loadResources();
+	graphic.start(true);
 
 	// Set the engine's debugger console
 	setDebugger(new Console());
@@ -78,20 +79,15 @@ Common::Error AlcachofaEngine::run() {
 	int offset = 0;
 
 	Graphics::FrameLimiter limiter(g_system, 60);
-	int32 frame = 0;
-	uint32 nextSecond = g_system->getMillis();
 	while (!shouldQuit()) {
 		while (g_system->getEventManager()->pollEvent(e)) {
 		}
 
 		_renderer->begin();
 
-		if (g_system->getMillis() >= nextSecond) {
-			frame = (frame + 1) % animation->frameCount();
-			nextSecond = g_system->getMillis() + animation->frameDuration(frame);
-		}
+		graphic.update();
 
-		animation->draw2D(frame, Math::Vector2d(100, 100), 1.0f, Math::Angle(), BlendMode::Alpha, { 255, 255, 255, 255 });
+		graphic.testDraw();
 
 		// Cycle through a simple palette
 		++offset;
@@ -105,8 +101,6 @@ Common::Error AlcachofaEngine::run() {
 		limiter.startFrame();
 	}
 
-	delete animation;
-
 	return Common::kNoError;
 }
 
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index b77281f9b76..c57ace585f2 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -159,9 +159,9 @@ void MainCharacter::serializeSave(Serializer &serializer) {
 
 Background::Background(Room *room, const String &animationFileName, int16 scale)
 	: GraphicObject(room, "BACKGROUND") {
-	_graphic._animation.reset(new Animation(animationFileName, AnimationFolder::Backgrounds));
-	_graphic._scale = scale;
-	_graphic._order = 59;
+	_graphic.setAnimation(animationFileName, AnimationFolder::Backgrounds);
+	_graphic.scale() = scale;
+	_graphic.order() = 59;
 }
 
 FloorColor::FloorColor(Room *room, ReadStream &stream)
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 9230ba4fa0b..15c415f195c 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -110,6 +110,7 @@ void AnimationBase::load() {
 		frame._offset = readPoint(file);
 		frame._duration = file.readUint32LE();
 		_frames.push_back(frame);
+		_totalDuration += frame._duration;
 	}
 
 	_isLoaded = true;
@@ -251,6 +252,15 @@ void Animation::draw2D(int32 frameI, Vector2d center, float scale, Angle rotatio
 	renderer.quad(center, size, color, rotation, texMin, texMax);
 }
 
+int32 Animation::frameAtTime(uint32 time) const {
+	for (int32 i = 0; (uint)i < _frames.size(); i++) {
+		if (time < _frames[i]._duration)
+			return i;
+		time -= _frames[i]._duration;
+	}
+	return -1;
+}
+
 Graphic::Graphic() {
 }
 
@@ -263,18 +273,57 @@ Graphic::Graphic(ReadStream &stream) {
 	_animation.reset(new Animation(std::move(animationName)));
 }
 
+void Graphic::loadResources() {
+	assert(_animation != nullptr);
+	_animation->load();
+}
+
+void Graphic::freeResources() {
+	_animation.reset();
+}
+
+void Graphic::update() {
+	if (_animation == nullptr || _animation->frameCount() == 0)
+		return;
+
+	const uint32 totalDuration = _animation->totalDuration();
+	uint32 curTime = _lastTime;
+	if (!_isPaused)
+		curTime = g_system->getMillis() - curTime;
+	if (curTime > totalDuration) {
+		if (_isLooping && totalDuration > 0)
+			curTime %= totalDuration;
+		else {
+			pause();
+			_lastTime = totalDuration - 1;
+		}
+	}
+
+	_frameI = _animation->frameAtTime(curTime);
+	assert(_frameI >= 0);
+}
+
 void Graphic::start(bool isLooping) {
 	_isPaused = false;
 	_isLooping = isLooping;
 	_lastTime = g_system->getMillis();
 }
 
-void Graphic::stop() {
+void Graphic::pause() {
 	_isPaused = true;
 	_isLooping = false;
 	_lastTime = g_system->getMillis() - _lastTime;
 }
 
+void Graphic::reset() {
+	_frameI = 0;
+	_lastTime = _isPaused ? 0 : g_system->getMillis();
+}
+
+void Graphic::setAnimation(const Common::String &fileName, AnimationFolder folder) {
+	_animation.reset(new Animation(fileName, folder));
+}
+
 void Graphic::serializeSave(Serializer &serializer) {
 	syncPoint(serializer, _center);
 	serializer.syncAsSint16LE(_scale);
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 987416d6533..6e5d0f20b72 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -31,6 +31,9 @@
 
 namespace Alcachofa {
 
+static constexpr const int16_t kBaseScale = 300; ///< this number pops up everywhere in the engine
+static constexpr const float kInvBaseScale = 1.0f / kBaseScale;
+
 /**
  * Because this gets confusing fast, here in tabular form
  *
@@ -74,6 +77,9 @@ constexpr const int32 kDirectionCount = 4;
 struct Color {
 	uint8 b, g, r, a;
 };
+static constexpr const Color kWhite = { 255, 255, 255, 255 };
+static constexpr const Color kBlack = { 0, 0, 0, 255 };
+static constexpr const Color kClear = { 0, 0, 0, 0 };
 
 class ITexture {
 public:
@@ -101,7 +107,7 @@ public:
 	virtual void quad(
 		Math::Vector2d center,
 		Math::Vector2d size,
-		Color color = { 255, 255, 255, 255 },
+		Color color = kWhite,
 		Math::Angle rotation = Math::Angle(),
 		Math::Vector2d texMin = Math::Vector2d(0, 0),
 		Math::Vector2d texMax = Math::Vector2d(1, 1)) = 0;
@@ -147,6 +153,7 @@ protected:
 	Common::String _fileName;
 	AnimationFolder _folder;
 	bool _isLoaded = false;
+	uint32 _totalDuration = 0;
 
 	int32 _spriteIndexMapping[kMaxSpriteIDs] = { -1 };
 	Common::Array<uint32>
@@ -172,6 +179,8 @@ public:
 	inline uint frameCount() const { return _frames.size(); }
 	inline uint spriteCount() const { return _spriteBases.size(); }
 	inline uint32 frameDuration(int32 frameI) const { return _frames[frameI]._duration; }
+	inline uint32 totalDuration() const { return _totalDuration; }
+	int32 frameAtTime(uint32 time) const;
 
 	void draw2D(
 		int32 frameI,
@@ -205,22 +214,36 @@ public:
 	Graphic();
 	Graphic(Common::ReadStream &stream);
 
-	inline int8 order() const { return _order; }
+	inline int8 &order() { return _order; }
+	inline int16 &scale() { return _scale; }
+	inline Animation &animation() {
+		assert(_animation != nullptr && _animation->isLoaded());
+		return *_animation;
+	}
 
+	void loadResources();
+	void freeResources();
+	void update();
 	void start(bool looping);
-	void stop();
+	void pause();
+	void reset();
+	void setAnimation(const Common::String &fileName, AnimationFolder folder);
 	void serializeSave(Common::Serializer &serializer);
 
-public:
+	inline void testDraw() {
+		animation().draw2D(_frameI, Math::Vector2d(100, 100), 1.0f, Math::Angle(), BlendMode::Alpha, { 255, 255, 255, 255 });
+	}
+
+private:
 	Common::SharedPtr<Animation> _animation;
 	Common::Point _center;
-	int16 _scale = 300;
+	int16 _scale = kBaseScale;
 	int8 _order = 0;
 
-private:
 	bool _isPaused = true,
 		_isLooping = true;
-	uint32 _lastTime = 0;
+	uint32 _lastTime = 0; ///< either start time or played duration at pause
+	int32 _frameI = -1;
 	float _camAcceleration = 1.0f;
 };
 


Commit: 1859ad68072ca2a6f78f2f4d4fb85e9a43bdadb2
    https://github.com/scummvm/scummvm/commit/1859ad68072ca2a6f78f2f4d4fb85e9a43bdadb2
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:44+02:00

Commit Message:
ALCACHOFA: Add DrawQueue and AnimationDrawRequest

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


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 1d8d2fb793e..a6321123d91 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -30,6 +30,7 @@
 #include "common/system.h"
 #include "engines/util.h"
 #include "graphics/paletteman.h"
+#include "graphics/framelimiter.h"
 
 #include "rooms.h"
 
@@ -43,7 +44,6 @@ AlcachofaEngine::AlcachofaEngine(OSystem *syst, const ADGameDescription *gameDes
 }
 
 AlcachofaEngine::~AlcachofaEngine() {
-	delete _screen;
 }
 
 uint32 AlcachofaEngine::getFeatures() const {
@@ -57,6 +57,7 @@ Common::String AlcachofaEngine::getGameId() const {
 Common::Error AlcachofaEngine::run() {
 	// Initialize 320x200 paletted graphics mode
 	_renderer.reset(IRenderer::createOpenGLRenderer(Common::Point(1024, 768)));
+	_drawQueue.reset(new DrawQueue(_renderer.get()));
 
 	auto world = new World();
 	delete world;
@@ -73,31 +74,31 @@ Common::Error AlcachofaEngine::run() {
 	if (saveSlot != -1)
 		(void)loadGameState(saveSlot);
 
-	// Simple event handling loop
-	byte pal[256 * 3] = { 0 };
 	Common::Event e;
-	int offset = 0;
-
 	Graphics::FrameLimiter limiter(g_system, 60);
 	while (!shouldQuit()) {
 		while (g_system->getEventManager()->pollEvent(e)) {
 		}
 
 		_renderer->begin();
+		_drawQueue->clear();
 
 		graphic.update();
+		graphic.center() = { 0, 0 };
+		_drawQueue->add<AnimationDrawRequest>(graphic, false, BlendMode::AdditiveAlpha);
+		graphic.center() = { 100, 0 };
+		_drawQueue->add<AnimationDrawRequest>(graphic, false, BlendMode::AdditiveAlpha);
+		graphic.center() = { 0, 100 };
+		_drawQueue->add<AnimationDrawRequest>(graphic, false, BlendMode::AdditiveAlpha);
+		graphic.center() = { 100, 100 };
+		_drawQueue->add<AnimationDrawRequest>(graphic, false, BlendMode::AdditiveAlpha);
+
+		_drawQueue->draw();
+		_renderer->end();
 
-		graphic.testDraw();
-
-		// Cycle through a simple palette
-		++offset;
-		for (int i = 0; i < 256; ++i)
-			pal[i * 3 + 1] = (i + offset) % 256;
-		g_system->getPaletteManager()->setPalette(pal, 0, 256);
 		// Delay for a bit. All events loops should have a delay
 		// to prevent the system being unduly loaded
 		limiter.delayBeforeSwap();
-		_renderer->end();
 		limiter.startFrame();
 	}
 
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index a703d18341b..ade684e18df 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -98,8 +98,8 @@ public:
 	}
 
 private:
-	Graphics::Screen *_screen = nullptr;
 	Common::ScopedPtr<IRenderer> _renderer;
+	Common::ScopedPtr<DrawQueue> _drawQueue;
 };
 
 extern AlcachofaEngine *g_engine;
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 15c415f195c..15f4ffb61fb 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -254,7 +254,7 @@ void Animation::draw2D(int32 frameI, Vector2d center, float scale, Angle rotatio
 
 int32 Animation::frameAtTime(uint32 time) const {
 	for (int32 i = 0; (uint)i < _frames.size(); i++) {
-		if (time < _frames[i]._duration)
+		if (time <= _frames[i]._duration)
 			return i;
 		time -= _frames[i]._duration;
 	}
@@ -295,7 +295,7 @@ void Graphic::update() {
 			curTime %= totalDuration;
 		else {
 			pause();
-			_lastTime = totalDuration - 1;
+			curTime = _lastTime = totalDuration - 1;
 		}
 	}
 
@@ -330,7 +330,123 @@ void Graphic::serializeSave(Serializer &serializer) {
 	serializer.syncAsUint32LE(_lastTime);
 	serializer.syncAsByte(_isPaused);
 	serializer.syncAsByte(_isLooping);
-	serializer.syncAsFloatLE(_camAcceleration);
+	serializer.syncAsFloatLE(_depthScale);
+}
+
+static int8 shiftAndClampOrder(int8 order) {
+	return MAX<int8>(0, MIN<int8>(kOrderCount - 1, order + kForegroundOrderCount));
+}
+
+IDrawRequest::IDrawRequest(int8 order)
+	: _order(shiftAndClampOrder(order)) {
+}
+
+AnimationDrawRequest::AnimationDrawRequest(Graphic &graphic, bool is3D, BlendMode blendMode, float lodBias)
+	: IDrawRequest(graphic._order)
+	, _is3D(is3D)
+	, _animation(&graphic.animation())
+	, _frameI(graphic._frameI)
+	, _center(graphic._center.x, graphic._center.y)
+	, _scale(graphic._scale * graphic._depthScale)
+	, _color(graphic.color())
+	, _blendMode(blendMode)
+	, _lodBias(lodBias) {
+	assert(_frameI >= 0 && (uint)_frameI < _animation->frameCount());
+}
+
+AnimationDrawRequest::AnimationDrawRequest(Animation *animation, int32 frameI, Vector2d center, int8 order)
+	: IDrawRequest(order)
+	, _is3D(false)
+	, _animation(animation)
+	, _frameI(frameI)
+	, _center(center)
+	, _scale(1.0f)
+	, _color(kWhite)
+	, _blendMode(BlendMode::AdditiveAlpha)
+	, _lodBias(0.0f) {
+	assert(animation != nullptr && animation->isLoaded());
+	assert(_frameI >= 0 && (uint)_frameI < _animation->frameCount());
+}
+
+void AnimationDrawRequest::draw() {
+	_animation->draw2D(_frameI, _center, _scale * kInvBaseScale, Angle(), _blendMode, _color);
+}
+
+DrawQueue::DrawQueue(IRenderer *renderer)
+	: _renderer(renderer)
+	, _allocator(1024) {
+	assert(renderer != nullptr);
+}
+
+void DrawQueue::clear() {
+	memset(_requestsPerOrderCount, 0, sizeof(_requestsPerOrderCount));
+	memset(_lodBiasPerOrder, 0, sizeof(_lodBiasPerOrder));
+}
+
+void DrawQueue::addRequest(IDrawRequest *drawRequest) {
+	assert(drawRequest != nullptr && drawRequest->order() >= 0 && drawRequest->order() < kOrderCount);
+	auto order = drawRequest->order();
+	if (_requestsPerOrderCount[order] < kMaxDrawRequestsPerOrder)
+		_requestsPerOrder[order][_requestsPerOrderCount[order]++] = drawRequest;
+	else
+		error("Too many draw requests in order %d", order);
+}
+
+void DrawQueue::setLodBias(int8 orderFrom, int8 orderTo, float newLodBias) {
+	orderFrom = shiftAndClampOrder(orderFrom);
+	orderTo = shiftAndClampOrder(orderTo);
+	if (orderFrom <= orderTo) {
+		Common::fill(_lodBiasPerOrder + orderFrom, _lodBiasPerOrder + orderTo + 1, newLodBias);
+	}
+}
+
+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();
+		}
+	}
+	_allocator.deallocateAll();
+}
+
+BumpAllocator::BumpAllocator(size_t pageSize) : _pageSize(pageSize) {
+	allocatePage();
+}
+
+BumpAllocator::~BumpAllocator() {
+	for (auto page : _pages)
+		free(page);
+}
+
+void *BumpAllocator::allocateRaw(size_t size, size_t align) {
+	assert(size <= _pageSize);
+	uintptr_t page = (uintptr_t)_pages[_pageI];
+	uintptr_t top = page + _used;
+	top = top + align - 1;
+	top = top - (top % align);
+	if (page + _pageSize - top >= size) {
+		_used = top + size - page;
+		return (void *)top;
+	}
+
+	_pageI++;
+	if (_pageI >= _pages.size())
+		allocatePage();
+	return allocateRaw(size, align);
+}
+
+void BumpAllocator::allocatePage() {
+	auto page = malloc(_pageSize);
+	if (page == nullptr)
+		error("Out of memory in BumpAllocator");
+	_pages.push_back(page);
+}
+
+void BumpAllocator::deallocateAll() {
+	_pageI = 0;
+	_used = 0;
 }
 
 }
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 6e5d0f20b72..618ec410961 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -73,6 +73,8 @@ enum class Direction {
 };
 
 constexpr const int32 kDirectionCount = 4;
+constexpr const int8 kOrderCount = 70;
+constexpr const int8 kForegroundOrderCount = 10;
 
 struct Color {
 	uint8 b, g, r, a;
@@ -214,8 +216,10 @@ public:
 	Graphic();
 	Graphic(Common::ReadStream &stream);
 
+	inline Common::Point &center() { return _center; }
 	inline int8 &order() { return _order; }
 	inline int16 &scale() { return _scale; }
+	inline Color &color() { return _color; }
 	inline Animation &animation() {
 		assert(_animation != nullptr && _animation->isLoaded());
 		return *_animation;
@@ -230,21 +234,113 @@ public:
 	void setAnimation(const Common::String &fileName, AnimationFolder folder);
 	void serializeSave(Common::Serializer &serializer);
 
-	inline void testDraw() {
-		animation().draw2D(_frameI, Math::Vector2d(100, 100), 1.0f, Math::Angle(), BlendMode::Alpha, { 255, 255, 255, 255 });
-	}
-
 private:
+	friend class AnimationDrawRequest;
 	Common::SharedPtr<Animation> _animation;
 	Common::Point _center;
 	int16 _scale = kBaseScale;
 	int8 _order = 0;
+	Color _color = kWhite;
 
 	bool _isPaused = true,
 		_isLooping = true;
 	uint32 _lastTime = 0; ///< either start time or played duration at pause
 	int32 _frameI = -1;
-	float _camAcceleration = 1.0f;
+	float _depthScale = 1.0f;
+};
+
+enum class DrawRequestType {
+	Animation2D,
+	Animation3D,
+	AnimationTiled,
+	Rectangle,
+	FadeToBlack,
+	FadeToWhite,
+	CrossFade,
+	Text
+};
+
+class IDrawRequest {
+public:
+	IDrawRequest(int8 order);
+	virtual ~IDrawRequest() = default;
+
+	inline int8 order() const { return _order; }
+	virtual void draw() = 0;
+
+private:
+	const int8 _order;
+};
+
+class AnimationDrawRequest : public IDrawRequest {
+public:
+	AnimationDrawRequest(
+		Graphic &graphic,
+		bool is3D,
+		BlendMode blendMode,
+		float lodBias = 0.0f);
+	AnimationDrawRequest(
+		Animation *animation,
+		int32 frameI,
+		Math::Vector2d center,
+		int8 order
+	);
+
+	virtual void draw() override;
+
+private:
+	bool _is3D;
+	Animation *_animation;
+	int32 _frameI;
+	Math::Vector2d _center;
+	float _scale;
+	Color _color;
+	BlendMode _blendMode;
+	float _lodBias;
+};
+
+class BumpAllocator {
+public:
+	BumpAllocator(size_t pageSize);
+	~BumpAllocator();
+
+	template<typename T, typename... Args>
+	inline T *allocate(Args&&... args) {
+		return new(allocateRaw(sizeof(T), alignof(T))) T(Common::forward<Args>(args)...);
+	}
+	void *allocateRaw(size_t size, size_t align);
+	void deallocateAll();
+
+private:
+	void allocatePage();
+
+	const size_t _pageSize;
+	size_t _pageI = 0, _used = 0;
+	Common::Array<void *> _pages;
+};
+
+class DrawQueue {
+public:
+	DrawQueue(IRenderer *renderer);
+
+	template<typename T, typename... Args>
+	inline void add(Args&&... args) {
+		addRequest(_allocator.allocate<T>(Common::forward<Args>(args)...));
+	}
+
+	void clear();
+	void setLodBias(int8 orderFrom, int8 orderTo, float newLodBias);
+	void draw();
+
+private:
+	void addRequest(IDrawRequest *drawRequest);
+
+	static constexpr const uint kMaxDrawRequestsPerOrder = 50;
+	IRenderer *const _renderer;
+	BumpAllocator _allocator;
+	IDrawRequest *_requestsPerOrder[kOrderCount][kMaxDrawRequestsPerOrder] = { 0 };
+	uint8 _requestsPerOrderCount[kOrderCount] = { 0 };
+	float _lodBiasPerOrder[kOrderCount] = { 0 };
 };
 
 }


Commit: 15915b540732e8b0a18b7442f1b82eab18a0ace0
    https://github.com/scummvm/scummvm/commit/15915b540732e8b0a18b7442f1b82eab18a0ace0
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:44+02:00

Commit Message:
ALCACHOFA: Add camera and 3D draw requests

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


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index a6321123d91..3096263844e 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -34,6 +34,8 @@
 
 #include "rooms.h"
 
+using namespace Math;
+
 namespace Alcachofa {
 
 AlcachofaEngine *g_engine;
@@ -55,12 +57,10 @@ Common::String AlcachofaEngine::getGameId() const {
 }
 
 Common::Error AlcachofaEngine::run() {
-	// Initialize 320x200 paletted graphics mode
 	_renderer.reset(IRenderer::createOpenGLRenderer(Common::Point(1024, 768)));
 	_drawQueue.reset(new DrawQueue(_renderer.get()));
+	_world.reset(new World());
 
-	auto world = new World();
-	delete world;
 	Graphic graphic;
 	graphic.setAnimation("MORTADELO_ACOSTANDOSE", AnimationFolder::Animations);
 	graphic.loadResources();
@@ -82,17 +82,12 @@ Common::Error AlcachofaEngine::run() {
 
 		_renderer->begin();
 		_drawQueue->clear();
+		_camera.shake() = Vector2d();
 
 		graphic.update();
-		graphic.center() = { 0, 0 };
-		_drawQueue->add<AnimationDrawRequest>(graphic, false, BlendMode::AdditiveAlpha);
-		graphic.center() = { 100, 0 };
-		_drawQueue->add<AnimationDrawRequest>(graphic, false, BlendMode::AdditiveAlpha);
-		graphic.center() = { 0, 100 };
-		_drawQueue->add<AnimationDrawRequest>(graphic, false, BlendMode::AdditiveAlpha);
-		graphic.center() = { 100, 100 };
-		_drawQueue->add<AnimationDrawRequest>(graphic, false, BlendMode::AdditiveAlpha);
+		drawQueue().add<AnimationDrawRequest>(graphic, false, BlendMode::AdditiveAlpha);
 
+		_camera.update();
 		_drawQueue->draw();
 		_renderer->end();
 
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index ade684e18df..54a8cf57837 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -35,10 +35,13 @@
 #include "graphics/screen.h"
 
 #include "alcachofa/detection.h"
-#include "alcachofa/graphics.h"
+#include "alcachofa/camera.h"
 
 namespace Alcachofa {
 
+class IRenderer;
+class DrawQueue;
+class World;
 struct AlcachofaGameDescription;
 
 class AlcachofaEngine : public Engine {
@@ -52,7 +55,10 @@ public:
 	AlcachofaEngine(OSystem *syst, const ADGameDescription *gameDesc);
 	~AlcachofaEngine() override;
 
-	inline IRenderer &renderer() const { return *_renderer; }
+	inline IRenderer &renderer() { return *_renderer; }
+	inline DrawQueue &drawQueue() { return *_drawQueue; }
+	inline Camera &camera() { return _camera; }
+	inline World &world() { return *_world; }
 
 	uint32 getFeatures() const;
 
@@ -100,6 +106,8 @@ public:
 private:
 	Common::ScopedPtr<IRenderer> _renderer;
 	Common::ScopedPtr<DrawQueue> _drawQueue;
+	Common::ScopedPtr<World> _world;
+	Camera _camera;
 };
 
 extern AlcachofaEngine *g_engine;
diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
new file mode 100644
index 00000000000..7798f52a004
--- /dev/null
+++ b/engines/alcachofa/camera.cpp
@@ -0,0 +1,132 @@
+/* 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 "camera.h"
+#include "alcachofa.h"
+
+#include "common/system.h"
+#include "math/vector4d.h"
+
+using namespace Common;
+using namespace Math;
+
+namespace Alcachofa {
+
+void Camera::setRoomBounds(Rect bgBounds, float bgScale) {
+	float scaleFactor = 1 - bgScale * kInvBaseScale;
+	_roomMin = Vector2d(
+		g_system->getWidth() / 2 * scaleFactor,
+		g_system->getHeight() / 2 * scaleFactor);
+	_roomMax = _roomMin + Vector2d(
+		bgBounds.width() * bgScale * kInvBaseScale,
+		bgBounds.height() * bgScale * kInvBaseScale);
+}
+
+static Matrix4 scaleMatrix(float scale) {
+	Matrix4 m;
+	m(0, 0) = scale;
+	m(1, 1) = scale;
+	m(2, 2) = scale;
+	return m;
+}
+
+void Camera::setupMatricesAround(Vector3d center) {
+	Matrix4 matTemp;
+	matTemp.buildAroundZ(_rotation);
+	_mat3Dto2D.setToIdentity();
+	_mat3Dto2D.translate(-center);
+	_mat3Dto2D = matTemp * _mat3Dto2D;
+	_mat3Dto2D = _mat3Dto2D * scaleMatrix(_scale);
+
+	_mat2Dto3D.setToIdentity();
+	_mat2Dto3D.translate(center);
+	matTemp.buildAroundZ(-_rotation);
+	matTemp = scaleMatrix(1 / _scale) * matTemp;
+	_mat2Dto3D = matTemp * _mat2Dto3D;
+}
+
+void minmax(Vector3d &min, Vector3d &max, Vector3d val)
+{
+	min.set(
+		MIN(min.x(), val.x()),
+		MIN(min.y(), val.y()),
+		MIN(min.z(), val.z()));
+	max.set(
+		MAX(max.x(), val.x()),
+		MAX(max.y(), val.y()),
+		MAX(max.z(), val.z()));
+}
+
+Vector3d Camera::setAppliedCenter(Vector3d center) {
+	setupMatricesAround(center);
+	if (true) { // g_engine->script().getVariable("EncuadrarCamara")
+		const float screenW = g_system->getWidth(), screenH = g_system->getHeight();
+		Vector3d min, max;
+		min = max = transform2Dto3D(Vector3d(0, 0, _roomScale));
+		minmax(min, max, transform2Dto3D(Vector3d(screenW, 0, _roomScale)));
+		minmax(min, max, transform2Dto3D(Vector3d(screenW, screenH, _roomScale)));
+		minmax(min, max, transform2Dto3D(Vector3d(0, screenH, _roomScale)));
+		center.x() += MAX(0.0f, _roomMin.getX() - min.x());
+		center.y() += MAX(0.0f, _roomMin.getY() - min.y());
+		center.x() -= MAX(0.0f, max.x() - _roomMax.getX());
+		center.y() -= MAX(0.0f, max.y() - _roomMax.getY());
+		setupMatricesAround(center);
+	}
+	return _appliedCenter = center;
+}
+
+Vector3d Camera::transform2Dto3D(Vector3d v3d) const {
+	// if this looks like normal 3D math to *someone* please contact.
+	Vector4d vh;
+	vh.w() = 1.0f;
+	vh.z() = v3d.z() - _usedCenter.z();
+	vh.y() = (v3d.y() - g_system->getHeight() * 0.5f) * vh.z() * kInvBaseScale;
+	vh.x() = (v3d.x() - g_system->getWidth() * 0.5f) * vh.z() * kInvBaseScale;
+	vh = _mat2Dto3D * vh;
+	return Vector3d(vh.x(), vh.y(), 0.0f);
+}
+
+Vector3d Camera::transform3Dto2D(Vector3d v2d) const {
+	// I swear there is a better way than this. This is stupid. But it is original.
+	float depthScale = v2d.z() * kInvBaseScale;
+	Vector4d vh;
+	vh.x() = v2d.x() * depthScale + (1 - depthScale) * g_system->getWidth() * 0.5f;
+	vh.y() = v2d.y() * depthScale + (1 - depthScale) * g_system->getHeight() * 0.5f;
+	vh.z() = v2d.z();
+	vh.w() = 1.0f;
+	vh = _mat3Dto2D * vh;
+	return Vector3d(
+		g_system->getWidth() * 0.5f + vh.x() * kBaseScale / vh.z(),
+		g_system->getHeight() * 0.5f + vh.y() * kBaseScale / vh.z(),
+		_scale * kBaseScale / vh.z());
+}
+
+void Camera::update() {
+	// original would be some smoothing of delta times, let's not.
+	uint32 now = g_system->getMillis();
+	float deltaTime = now - _lastUpdateTime;
+	deltaTime = MAX(0.001f, MIN(0.5f, deltaTime));
+	_lastUpdateTime = now;
+
+	setAppliedCenter(_usedCenter + Vector3d(_shake.getX(), _shake.getY(), 0.0f));
+}
+
+}
diff --git a/engines/alcachofa/camera.h b/engines/alcachofa/camera.h
new file mode 100644
index 00000000000..811fa68b0cc
--- /dev/null
+++ b/engines/alcachofa/camera.h
@@ -0,0 +1,74 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef CAMERA_H
+#define CAMERA_H
+
+#include "common/serializer.h"
+#include "common/rect.h"
+#include "math/vector2d.h"
+#include "math/vector3d.h"
+#include "math/matrix4.h"
+
+namespace Alcachofa {
+
+class Personaje;
+
+static constexpr const int16_t kBaseScale = 300; ///< this number pops up everywhere in the engine
+static constexpr const float kInvBaseScale = 1.0f / kBaseScale;
+
+class Camera {
+public:
+	inline Math::Angle rotation() const { return _rotation; }
+	inline Math::Vector2d &shake() { return _shake; }
+
+	void update();
+	Math::Vector3d transform2Dto3D(Math::Vector3d v) const;
+	Math::Vector3d transform3Dto2D(Math::Vector3d v) const;
+	void setRoomBounds(Common::Rect bgBounds, float bgScale);
+
+private:
+	static constexpr const float kAccelerationThreshold = 2.89062f;
+	static constexpr const float kAcceleration = 3.94922f;
+
+	Math::Vector3d setAppliedCenter(Math::Vector3d center);
+	void setupMatricesAround(Math::Vector3d center);
+
+	uint32 _lastUpdateTime = 0;
+	float
+		_scale = 1.0f,
+		_roomScale = 1.0f;
+	Math::Angle _rotation;
+	Math::Vector2d
+		_roomMin = Math::Vector2d(-10000, -10000),
+		_roomMax = Math::Vector2d(10000, 10000),
+		_shake;
+	Math::Vector3d
+		_usedCenter,
+		_appliedCenter;
+	Math::Matrix4
+		_mat3Dto2D,
+		_mat2Dto3D;
+};
+
+}
+
+#endif // CAMERA_H
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 15f4ffb61fb..ca735402080 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -205,6 +205,23 @@ Rect Animation::maxFrameBounds() const {
 	return bounds;
 }
 
+Math::Vector2d Animation::totalFrameOffset(int32 frameI) const {
+	const auto &frame = _frames[frameI];
+	const auto bounds = frameBounds(frameI);
+	return Vector2d(
+		bounds.left - frame._center.x + frame._offset.x,
+		bounds.top - frame._center.y + frame._offset.y);
+}
+
+int32 Animation::frameAtTime(uint32 time) const {
+	for (int32 i = 0; (uint)i < _frames.size(); i++) {
+		if (time <= _frames[i]._duration)
+			return i;
+		time -= _frames[i]._duration;
+	}
+	return -1;
+}
+
 void Animation::prerenderFrame(int32 frameI) {
 	assert(frameI >= 0 && (uint)frameI < frameCount());
 	if (frameI == _renderedFrameI)
@@ -232,33 +249,46 @@ void Animation::prerenderFrame(int32 frameI) {
 	_renderedFrameI = frameI;
 }
 
-void Animation::draw2D(int32 frameI, Vector2d center, float scale, Angle rotation, BlendMode blendMode, Color color) {
+void Animation::draw2D(int32 frameI, Vector2d center, 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);
 
 	Vector2d size(bounds.width(), bounds.height());
-	Vector2d offset(
-		bounds.left - _frames[frameI]._center.x + _frames[frameI]._offset.x,
-		bounds.top - _frames[frameI]._center.y + _frames[frameI]._offset.y);
-	center += offset * scale;
+	center += totalFrameOffset(frameI) * scale;
 	size *= scale;
 
 	auto &renderer = g_engine->renderer();
 	renderer.setTexture(_renderedTexture.get());
-	//renderer.setTexture(nullptr);
 	renderer.setBlendMode(blendMode);
-	renderer.quad(center, size, color, rotation, texMin, texMax);
+	renderer.quad(center, size, color, Angle(), texMin, texMax);
 }
 
-int32 Animation::frameAtTime(uint32 time) const {
-	for (int32 i = 0; (uint)i < _frames.size(); i++) {
-		if (time <= _frames[i]._duration)
-			return i;
-		time -= _frames[i]._duration;
-	}
-	return -1;
+static Vector3d as3D(const Vector2d &v) {
+	return Vector3d(v.getX(), v.getY(), 0.0f);
+}
+
+static Vector2d as2D(const Vector3d &v) {
+	return Vector2d(v.x(), v.y());
+}
+
+void Animation::draw3D(int32 frameI, Vector3d center, 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);
+
+	center += as3D(totalFrameOffset(frameI)) * scale;
+	center = g_engine->camera().transform3Dto2D(center);
+	const auto rotation = -g_engine->camera().rotation();
+	Vector2d size(bounds.width(), bounds.height());
+	size *= scale * center.z();
+
+	auto &renderer = g_engine->renderer();
+	renderer.setTexture(_renderedTexture.get());
+	renderer.setBlendMode(blendMode);
+	renderer.quad(as2D(center), size, color, rotation, texMin, texMax);
 }
 
 Graphic::Graphic() {
@@ -346,7 +376,7 @@ AnimationDrawRequest::AnimationDrawRequest(Graphic &graphic, bool is3D, BlendMod
 	, _is3D(is3D)
 	, _animation(&graphic.animation())
 	, _frameI(graphic._frameI)
-	, _center(graphic._center.x, graphic._center.y)
+	, _center(graphic._center.x, graphic._center.y, graphic._scale)
 	, _scale(graphic._scale * graphic._depthScale)
 	, _color(graphic.color())
 	, _blendMode(blendMode)
@@ -359,7 +389,7 @@ AnimationDrawRequest::AnimationDrawRequest(Animation *animation, int32 frameI, V
 	, _is3D(false)
 	, _animation(animation)
 	, _frameI(frameI)
-	, _center(center)
+	, _center(as3D(center))
 	, _scale(1.0f)
 	, _color(kWhite)
 	, _blendMode(BlendMode::AdditiveAlpha)
@@ -369,7 +399,10 @@ AnimationDrawRequest::AnimationDrawRequest(Animation *animation, int32 frameI, V
 }
 
 void AnimationDrawRequest::draw() {
-	_animation->draw2D(_frameI, _center, _scale * kInvBaseScale, Angle(), _blendMode, _color);
+	if (_is3D)
+		_animation->draw3D(_frameI, _center, _scale * kInvBaseScale, _blendMode, _color);
+	else
+		_animation->draw2D(_frameI, as2D(_center), _scale * kInvBaseScale, _blendMode, _color);
 }
 
 DrawQueue::DrawQueue(IRenderer *renderer)
@@ -431,6 +464,7 @@ void *BumpAllocator::allocateRaw(size_t size, size_t align) {
 		return (void *)top;
 	}
 
+	_used = 0;
 	_pageI++;
 	if (_pageI >= _pages.size())
 		allocatePage();
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 618ec410961..a3bfeed7804 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -29,10 +29,9 @@
 #include "math/vector2d.h"
 #include "graphics/managed_surface.h"
 
-namespace Alcachofa {
+#include "camera.h"
 
-static constexpr const int16_t kBaseScale = 300; ///< this number pops up everywhere in the engine
-static constexpr const float kInvBaseScale = 1.0f / kBaseScale;
+namespace Alcachofa {
 
 /**
  * Because this gets confusing fast, here in tabular form
@@ -188,7 +187,12 @@ public:
 		int32 frameI,
 		Math::Vector2d center,
 		float scale,
-		Math::Angle rotation,
+		BlendMode blendMode,
+		Color color);
+	void draw3D(
+		int32 frameI,
+		Math::Vector3d center,
+		float scale,
 		BlendMode blendMode,
 		Color color);
 
@@ -197,6 +201,7 @@ private:
 	Common::Rect spriteBounds(int32 frameI, int32 spriteI) const;
 	Common::Rect frameBounds(int32 frameI) const;
 	Common::Rect maxFrameBounds() const;
+	Math::Vector2d totalFrameOffset(int32 frameI) const;
 	void prerenderFrame(int32 frameI);
 
 	int32_t _renderedFrameI = -1;
@@ -210,7 +215,6 @@ class Font : private AnimationBase {
 
 };
 
-
 class Graphic {
 public:
 	Graphic();
@@ -292,7 +296,7 @@ private:
 	bool _is3D;
 	Animation *_animation;
 	int32 _frameI;
-	Math::Vector2d _center;
+	Math::Vector3d _center;
 	float _scale;
 	Color _color;
 	BlendMode _blendMode;


Commit: 872e5b371524cbc4ce200729ab06370b5f1504c6
    https://github.com/scummvm/scummvm/commit/872e5b371524cbc4ce200729ab06370b5f1504c6
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:44+02:00

Commit Message:
ALCACHOFA: Add basic room loop and GraphicObject drawing

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


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 3096263844e..d96f822ee48 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -61,10 +61,12 @@ Common::Error AlcachofaEngine::run() {
 	_drawQueue.reset(new DrawQueue(_renderer.get()));
 	_world.reset(new World());
 
-	Graphic graphic;
-	graphic.setAnimation("MORTADELO_ACOSTANDOSE", AnimationFolder::Animations);
-	graphic.loadResources();
-	graphic.start(true);
+	world().globalRoom().loadResources();
+
+	auto room = world().getRoomByName("CASA_FREDDY_ARRIBA");
+	assert(room != nullptr);
+	world().currentRoom() = room;
+	room->loadResources();
 
 	// Set the engine's debugger console
 	setDebugger(new Console());
@@ -84,11 +86,8 @@ Common::Error AlcachofaEngine::run() {
 		_drawQueue->clear();
 		_camera.shake() = Vector2d();
 
-		graphic.update();
-		drawQueue().add<AnimationDrawRequest>(graphic, false, BlendMode::AdditiveAlpha);
+		world().currentRoom()->update();
 
-		_camera.update();
-		_drawQueue->draw();
 		_renderer->end();
 
 		// Delay for a bit. All events loops should have a delay
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index c57ace585f2..55217e81faa 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -82,7 +82,7 @@ void Character::syncObjectAsString(Serializer &serializer, ObjectBase *&object)
 		else {
 			object = room()->getObjectByName(name);
 			if (object == nullptr)
-				object = room()->world()->getObjectByName(name);
+				object = room()->world().getObjectByName(name);
 			if (object == nullptr)
 				error("Invalid object name \"%s\" saved for \"%s\" in \"%s\"",
 					name.c_str(), this->name().c_str(), room()->name().c_str());
@@ -140,7 +140,7 @@ void MainCharacter::serializeSave(Serializer &serializer) {
 	String roomName = room()->name();
 	serializer.syncString(roomName);
 	if (serializer.isLoading()) {
-		room() = room()->world()->getRoomByName(roomName);
+		room() = room()->world().getRoomByName(roomName);
 		if (room() == nullptr)
 			error("Invalid room name \"%s\" saved for \"%s\"", roomName.c_str(), name().c_str());
 	}
@@ -159,6 +159,7 @@ void MainCharacter::serializeSave(Serializer &serializer) {
 
 Background::Background(Room *room, const String &animationFileName, int16 scale)
 	: GraphicObject(room, "BACKGROUND") {
+	toggle(true);
 	_graphic.setAnimation(animationFileName, AnimationFolder::Backgrounds);
 	_graphic.scale() = scale;
 	_graphic.order() = 59;
diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index bc8a965c66c..cf89e0bc5df 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -22,6 +22,7 @@
 #include "objects.h"
 #include "rooms.h"
 #include "stream-helper.h"
+#include "alcachofa.h"
 
 #include "common/system.h"
 
@@ -47,7 +48,7 @@ void ObjectBase::toggle(bool isEnabled) {
 	_isEnabled = isEnabled;
 }
 
-void ObjectBase::render() {
+void ObjectBase::draw() {
 }
 
 void ObjectBase::update() {
@@ -86,10 +87,29 @@ GraphicObject::GraphicObject(Room *room, ReadStream &stream)
 
 GraphicObject::GraphicObject(Room *room, const char *name)
 	: ObjectBase(room, name)
-	, _type(GraphicObjectType::Type0)
+	, _type(GraphicObjectType::Normal)
 	, _posterizeAlpha(0) {
 }
 
+void GraphicObject::draw() {
+	if (!isEnabled())
+		return;
+	const BlendMode blendMode = _type == GraphicObjectType::Alpha
+		? BlendMode::Alpha
+		: BlendMode::AdditiveAlpha;
+	const bool is3D = room() == &g_engine->world().inventory();
+	_graphic.update();
+	g_engine->drawQueue().add<AnimationDrawRequest>(_graphic, is3D, blendMode);
+}
+
+void GraphicObject::loadResources() {
+	_graphic.loadResources();
+}
+
+void GraphicObject::freeResources() {
+	_graphic.freeResources();
+}
+
 void GraphicObject::serializeSave(Serializer &serializer) {
 	ObjectBase::serializeSave(serializer);
 	_graphic.serializeSave(serializer);
@@ -108,6 +128,9 @@ ShiftingGraphicObject::ShiftingGraphicObject(Room *room, ReadStream &stream)
 	_startTime = g_system->getMillis();
 }
 
+void ShiftingGraphicObject::draw() {
+}
+
 ShapeObject::ShapeObject(Room *room, ReadStream &stream)
 	: ObjectBase(room, stream)
 	, _shape(stream)
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index ca735402080..e26b08d4cb0 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -320,8 +320,8 @@ void Graphic::update() {
 	uint32 curTime = _lastTime;
 	if (!_isPaused)
 		curTime = g_system->getMillis() - curTime;
-	if (curTime > totalDuration) {
-		if (_isLooping && totalDuration > 0)
+	if (curTime > totalDuration && totalDuration > 0) {
+		if (_isLooping)
 			curTime %= totalDuration;
 		else {
 			pause();
@@ -329,7 +329,7 @@ void Graphic::update() {
 		}
 	}
 
-	_frameI = _animation->frameAtTime(curTime);
+	_frameI = totalDuration == 0 ? 0 : _animation->frameAtTime(curTime);
 	assert(_frameI >= 0);
 }
 
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index bf5797e71f6..dbe27be262d 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -44,7 +44,7 @@ public:
 	inline bool isEnabled() const { return _isEnabled; }
 
 	virtual void toggle(bool isEnabled);
-	virtual void render();
+	virtual void draw();
 	virtual void update();
 	virtual void loadResources();
 	virtual void freeResources();
@@ -72,9 +72,9 @@ private:
 
 enum class GraphicObjectType : byte
 {
-	Type0,
-	Type1,
-	Type2
+	Normal,
+	NormalPosterize, // the posterization is not actually applied in the original engine
+	Alpha
 };
 
 class GraphicObject : public ObjectBase {
@@ -83,6 +83,9 @@ public:
 	GraphicObject(Room *room, Common::ReadStream &stream);
 	virtual ~GraphicObject() override = default;
 
+	virtual void draw() override;
+	virtual void loadResources() override;
+	virtual void freeResources() override;
 	virtual void serializeSave(Common::Serializer &serializer) override;
 	virtual Graphic *graphic() override;
 
@@ -99,6 +102,8 @@ public:
 	static constexpr const char *kClassName = "CObjetoGraficoMuare";
 	ShiftingGraphicObject(Room *room, Common::ReadStream &stream);
 
+	virtual void draw() override;
+
 private:
 	Common::Point _pos, _size;
 	Math::Vector2d _texShift;
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 0814d9c9934..6d133cf530d 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -19,6 +19,7 @@
  *
  */
 
+#include "alcachofa.h"
 #include "rooms.h"
 #include "stream-helper.h"
 
@@ -103,7 +104,8 @@ Room::Room(World *world, ReadStream &stream, bool hasUselessByte)
 		_objects.push_back(readRoomObject(this, stream));
 		objectSize = stream.readUint32LE();
 	}
-	_objects.push_back(new Background(this, _name, backgroundScale));
+	if (!_name.equalsIgnoreCase("Global"))
+		_objects.push_back(new Background(this, _name, backgroundScale));
 
 	if (!_floors[0].empty())
 		_activeFloorI = 0;
@@ -122,6 +124,31 @@ ObjectBase *Room::getObjectByName(const Common::String &name) const {
 	return nullptr;
 }
 
+void Room::update() {
+	if (world().currentRoom() == this)
+		updateObjects();
+	if (world().currentRoom() == this) {
+		g_engine->camera().update();
+		drawObjects();
+		world().globalRoom().drawObjects();
+		// TODO: Draw black borders
+		g_engine->drawQueue().draw();
+	}
+}
+
+void Room::updateObjects() {
+	for (auto *object : _objects) {
+		object->update();
+		if (world().currentRoom() != this)
+			return;
+	}
+}
+
+void Room::drawObjects() {
+	for (auto *object : _objects)
+		object->draw();
+}
+
 void Room::loadResources() {
 	for (auto *object : _objects)
 		object->loadResources();
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index e311bc9362a..0d95142cf7e 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -34,17 +34,19 @@ public:
 	Room(World *world, Common::ReadStream &stream);
 	virtual ~Room();
 
-	inline World *world() { return _world; }
+	inline World &world() { return *_world; }
 	inline const Common::String &name() const { return _name; }
 
-	ObjectBase *getObjectByName(const Common::String &name) const;
-
+	void update();
 	virtual void loadResources();
 	virtual void freeResources();
 	virtual void serializeSave(Common::Serializer &serializer);
+	ObjectBase *getObjectByName(const Common::String &name) const;
 
 protected:
 	Room(World *world, Common::ReadStream &stream, bool hasUselessByte);
+	void updateObjects();
+	void drawObjects();
 
 	World *_world;
 	Common::String _name;


Commit: c5e969cc8a2870f457f3565418e3fadde812ae14
    https://github.com/scummvm/scummvm/commit/c5e969cc8a2870f457f3565418e3fadde812ae14
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:44+02:00

Commit Message:
ALCACHOFA: Add SpecialEffectGraphicObject rendering

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


diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index 7798f52a004..67870f4434c 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -93,24 +93,24 @@ Vector3d Camera::setAppliedCenter(Vector3d center) {
 	return _appliedCenter = center;
 }
 
-Vector3d Camera::transform2Dto3D(Vector3d v3d) const {
+Vector3d Camera::transform2Dto3D(Vector3d v2d) const {
 	// if this looks like normal 3D math to *someone* please contact.
 	Vector4d vh;
 	vh.w() = 1.0f;
-	vh.z() = v3d.z() - _usedCenter.z();
-	vh.y() = (v3d.y() - g_system->getHeight() * 0.5f) * vh.z() * kInvBaseScale;
-	vh.x() = (v3d.x() - g_system->getWidth() * 0.5f) * vh.z() * kInvBaseScale;
+	vh.z() = v2d.z() - _usedCenter.z();
+	vh.y() = (v2d.y() - g_system->getHeight() * 0.5f) * vh.z() * kInvBaseScale;
+	vh.x() = (v2d.x() - g_system->getWidth() * 0.5f) * vh.z() * kInvBaseScale;
 	vh = _mat2Dto3D * vh;
 	return Vector3d(vh.x(), vh.y(), 0.0f);
 }
 
-Vector3d Camera::transform3Dto2D(Vector3d v2d) const {
+Vector3d Camera::transform3Dto2D(Vector3d v3d) const {
 	// I swear there is a better way than this. This is stupid. But it is original.
-	float depthScale = v2d.z() * kInvBaseScale;
+	float depthScale = v3d.z() * kInvBaseScale;
 	Vector4d vh;
-	vh.x() = v2d.x() * depthScale + (1 - depthScale) * g_system->getWidth() * 0.5f;
-	vh.y() = v2d.y() * depthScale + (1 - depthScale) * g_system->getHeight() * 0.5f;
-	vh.z() = v2d.z();
+	vh.x() = v3d.x() * depthScale + (1 - depthScale) * g_system->getWidth() * 0.5f;
+	vh.y() = v3d.y() * depthScale + (1 - depthScale) * g_system->getHeight() * 0.5f;
+	vh.z() = v3d.z();
 	vh.w() = 1.0f;
 	vh = _mat3Dto2D * vh;
 	return Vector3d(
diff --git a/engines/alcachofa/camera.h b/engines/alcachofa/camera.h
index 811fa68b0cc..520655c558f 100644
--- a/engines/alcachofa/camera.h
+++ b/engines/alcachofa/camera.h
@@ -62,7 +62,7 @@ private:
 		_roomMax = Math::Vector2d(10000, 10000),
 		_shake;
 	Math::Vector3d
-		_usedCenter,
+		_usedCenter = Math::Vector3d(512, 384, 0),
 		_appliedCenter;
 	Math::Matrix4
 		_mat3Dto2D,
diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index cf89e0bc5df..5189793ec2c 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -94,10 +94,10 @@ GraphicObject::GraphicObject(Room *room, const char *name)
 void GraphicObject::draw() {
 	if (!isEnabled())
 		return;
-	const BlendMode blendMode = _type == GraphicObjectType::Alpha
+	const BlendMode blendMode = _type == GraphicObjectType::Effect
 		? BlendMode::Alpha
 		: BlendMode::AdditiveAlpha;
-	const bool is3D = room() == &g_engine->world().inventory();
+	const bool is3D = room() != &g_engine->world().inventory();
 	_graphic.update();
 	g_engine->drawQueue().add<AnimationDrawRequest>(_graphic, is3D, blendMode);
 }
@@ -119,16 +119,30 @@ Graphic *GraphicObject::graphic() {
 	return &_graphic;
 }
 
-ShiftingGraphicObject::ShiftingGraphicObject(Room *room, ReadStream &stream)
+SpecialEffectObject::SpecialEffectObject(Room *room, ReadStream &stream)
 	: GraphicObject(room, stream) {
-	_pos = Shape(stream).firstPoint();
-	_size = Shape(stream).firstPoint();
-	_texShift.setX(stream.readSint32LE() / 256.0f);
-	_texShift.setY(stream.readSint32LE() / 256.0f);
-	_startTime = g_system->getMillis();
+	_topLeft = Shape(stream).firstPoint();
+	_bottomRight = Shape(stream).firstPoint();
+	_texShift.setX(stream.readSint32LE());
+	_texShift.setY(stream.readSint32LE());
+	_texShift *= kShiftSpeed;
 }
 
-void ShiftingGraphicObject::draw() {
+void SpecialEffectObject::draw() {
+	if (!isEnabled()) // TODO: Add high quality check
+		return;
+	const auto texOffset = g_system->getMillis() * 0.001f * _texShift;
+	const BlendMode blendMode = _type == GraphicObjectType::Effect
+		? BlendMode::Additive
+		: BlendMode::AdditiveAlpha;
+	Point topLeft = _topLeft, bottomRight = _bottomRight;
+	if (topLeft.x == bottomRight.x || topLeft.y == bottomRight.y) {
+		topLeft = _graphic.center();
+		bottomRight = topLeft + _graphic.animation().imageSize(0);
+	}
+
+	_graphic.update();
+	g_engine->drawQueue().add<SpecialEffectDrawRequest>(_graphic, topLeft, bottomRight, texOffset, blendMode);
 }
 
 ShapeObject::ShapeObject(Room *room, ReadStream &stream)
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index e26b08d4cb0..43b927f6ded 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -222,6 +222,11 @@ int32 Animation::frameAtTime(uint32 time) const {
 	return -1;
 }
 
+Point Animation::imageSize(int32 imageI) const {
+	auto image = _images[imageI];
+	return image == nullptr ? Point() : Point(image->w, image->h);
+}
+
 void Animation::prerenderFrame(int32 frameI) {
 	assert(frameI >= 0 && (uint)frameI < frameCount());
 	if (frameI == _renderedFrameI)
@@ -291,6 +296,27 @@ void Animation::draw3D(int32 frameI, Vector3d center, float scale, BlendMode ble
 	renderer.quad(as2D(center), size, color, rotation, texMin, texMax);
 }
 
+void Animation::drawEffect(int32 frameI, Vector3d topLeft, Vector2d tiling, Vector2d texOffset, BlendMode blendMode) {
+	prerenderFrame(frameI);
+	auto bounds = frameBounds(frameI);
+	Vector2d texMin(0, 0);
+	Vector2d texMax(tiling.getX() / _renderedSurface.w, tiling.getY() / _renderedSurface.h);
+
+	topLeft += as3D(totalFrameOffset(frameI));
+	topLeft = g_engine->camera().transform3Dto2D(topLeft);
+	const auto rotation = -g_engine->camera().rotation();
+	Vector2d size(bounds.width(), bounds.height());
+	size *= topLeft.z();
+
+	if (abs(tiling.getX()) > epsilon)
+		size = size * texMax;
+
+	auto &renderer = g_engine->renderer();
+	renderer.setTexture(_renderedTexture.get());
+	renderer.setBlendMode(blendMode);
+	renderer.quad(as2D(topLeft), size, kWhite, rotation, texMin + texOffset, texMax + texOffset);
+}
+
 Graphic::Graphic() {
 }
 
@@ -405,6 +431,21 @@ void AnimationDrawRequest::draw() {
 		_animation->draw2D(_frameI, as2D(_center), _scale * kInvBaseScale, _blendMode, _color);
 }
 
+SpecialEffectDrawRequest::SpecialEffectDrawRequest(Graphic &graphic, Point topLeft, Point bottomRight, Vector2d texOffset, BlendMode blendMode)
+	: IDrawRequest(graphic._order)
+	, _animation(&graphic.animation())
+	, _frameI(graphic._frameI)
+	, _topLeft(topLeft.x, topLeft.y, graphic._scale)
+	, _tiling(bottomRight.x - topLeft.x, bottomRight.y - topLeft.y)
+	, _texOffset(texOffset)
+	, _blendMode(blendMode) {
+	assert(_frameI >= 0 && (uint)_frameI < _animation->frameCount());
+}
+
+void SpecialEffectDrawRequest::draw() {
+	_animation->drawEffect(_frameI, _topLeft, _tiling, _texOffset, _blendMode);
+}
+
 DrawQueue::DrawQueue(IRenderer *renderer)
 	: _renderer(renderer)
 	, _allocator(1024) {
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index a3bfeed7804..f6de8336212 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -177,11 +177,12 @@ public:
 	using AnimationBase::freeImages;
 
 	inline bool isLoaded() const { return _isLoaded; }
-	inline uint frameCount() const { return _frames.size(); }
 	inline uint spriteCount() const { return _spriteBases.size(); }
+	inline uint frameCount() const { return _frames.size(); }
 	inline uint32 frameDuration(int32 frameI) const { return _frames[frameI]._duration; }
 	inline uint32 totalDuration() const { return _totalDuration; }
 	int32 frameAtTime(uint32 time) const;
+	Common::Point imageSize(int32 imageI) const;
 
 	void draw2D(
 		int32 frameI,
@@ -195,6 +196,12 @@ public:
 		float scale,
 		BlendMode blendMode,
 		Color color);
+	void drawEffect(
+		int32 frameI,
+		Math::Vector3d center,
+		Math::Vector2d tiling,
+		Math::Vector2d texOffset,
+		BlendMode blendMode);
 
 private:
 	int32 imageIndex(int32 frameI, int32 spriteI) const;
@@ -240,6 +247,7 @@ public:
 
 private:
 	friend class AnimationDrawRequest;
+	friend class SpecialEffectDrawRequest;
 	Common::SharedPtr<Animation> _animation;
 	Common::Point _center;
 	int16 _scale = kBaseScale;
@@ -303,6 +311,27 @@ private:
 	float _lodBias;
 };
 
+class SpecialEffectDrawRequest : public IDrawRequest {
+public:
+	SpecialEffectDrawRequest(
+		Graphic &graphic,
+		Common::Point topLeft,
+		Common::Point bottomRight,
+		Math::Vector2d texOffset,
+		BlendMode blendMode);
+
+	virtual void draw() override;
+
+private:
+	Animation *_animation;
+	int32 _frameI;
+	Math::Vector3d _topLeft;
+	Math::Vector2d
+		_tiling,
+		_texOffset;
+	BlendMode _blendMode;
+};
+
 class BumpAllocator {
 public:
 	BumpAllocator(size_t pageSize);
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index dbe27be262d..0fde17ce3f6 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -74,7 +74,7 @@ enum class GraphicObjectType : byte
 {
 	Normal,
 	NormalPosterize, // the posterization is not actually applied in the original engine
-	Alpha
+	Effect
 };
 
 class GraphicObject : public ObjectBase {
@@ -97,17 +97,17 @@ protected:
 	int32 _posterizeAlpha;
 };
 
-class ShiftingGraphicObject final : public GraphicObject {
+class SpecialEffectObject final : public GraphicObject {
 public:
 	static constexpr const char *kClassName = "CObjetoGraficoMuare";
-	ShiftingGraphicObject(Room *room, Common::ReadStream &stream);
+	SpecialEffectObject(Room *room, Common::ReadStream &stream);
 
 	virtual void draw() override;
 
 private:
-	Common::Point _pos, _size;
+	static constexpr const float kShiftSpeed = 1 / 256.0f;
+	Common::Point _topLeft, _bottomRight;
 	Math::Vector2d _texShift;
-	uint32 _startTime = 0;
 };
 
 class ShapeObject : public ObjectBase {
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 6d133cf530d..0cfb0207dbb 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -40,8 +40,8 @@ static ObjectBase *readRoomObject(Room *room, ReadStream &stream) {
 		return new PointObject(room, stream);
 	else if (type == GraphicObject::kClassName)
 		return new GraphicObject(room, stream);
-	else if (type == ShiftingGraphicObject::kClassName)
-		return new ShiftingGraphicObject(room, stream);
+	else if (type == SpecialEffectObject::kClassName)
+		return new SpecialEffectObject(room, stream);
 	else if (type == Item::kClassName)
 		return new Item(room, stream);
 	else if (type == PhysicalObject::kClassName)


Commit: afe0f02dbad2b256b41a28fce8cb491b8a2c5f01
    https://github.com/scummvm/scummvm/commit/afe0f02dbad2b256b41a28fce8cb491b8a2c5f01
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:45+02:00

Commit Message:
ALCACHOFA: Fix sprite prerendering with semi-transparent images

Changed paths:
    engines/alcachofa/graphics.cpp


diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 43b927f6ded..8a6301de810 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -227,6 +227,32 @@ Point Animation::imageSize(int32 imageI) const {
 	return image == nullptr ? Point() : Point(image->w, image->h);
 }
 
+// unfortunately ScummVMs BLEND_NORMAL does not blend alpha
+// but this also bad, let's find/discuss a better solution later
+static void fullBlend(const ManagedSurface &source, ManagedSurface &destination, int offsetX, int offsetY) {
+	assert(source.format == BlendBlit::getSupportedPixelFormat());
+	assert(destination.format == BlendBlit::getSupportedPixelFormat());
+	assert(offsetX >= 0 && offsetX + source.w <= destination.w);
+	assert(offsetY >= 0 && offsetY + source.h <= destination.h);
+
+	const byte *sourceLine = (byte *)source.getPixels();
+	byte *destinationLine = (byte *)destination.getPixels() + offsetY * source.pitch + offsetX * 4;
+	for (int y = 0; y < source.h; y++) {
+		const byte *sourcePixel = sourceLine;
+		byte *destPixel = destinationLine;
+		for (int x = 0; x < source.w; x++) {
+			byte alpha = (*(const uint32 *)sourcePixel) & 0xff;
+			for (int i = 1; i < 4; i++)
+				destPixel[i] = ((byte)(alpha * sourcePixel[i] / 255)) + ((byte)((255 - alpha) * destPixel[i] / 255));
+			destPixel[0] = alpha + ((byte)((255 - alpha) * destPixel[0] / 255));
+			sourcePixel += 4;
+			destPixel += 4;
+		}
+		sourceLine += source.pitch;
+		destinationLine += destination.pitch;
+	}
+}
+
 void Animation::prerenderFrame(int32 frameI) {
 	assert(frameI >= 0 && (uint)frameI < frameCount());
 	if (frameI == _renderedFrameI)
@@ -240,7 +266,7 @@ void Animation::prerenderFrame(int32 frameI) {
 			continue;
 		int offsetX = _imageOffsets[imageI].x - bounds.left;
 		int offsetY = _imageOffsets[imageI].y - bounds.top;
-		image->blendBlitTo(_renderedSurface, offsetX, offsetY);
+		fullBlend(*image, _renderedSurface, offsetX, offsetY);
 	}
 
 	if (_premultiplyAlpha != 100) {


Commit: 6bfee722ae08996d69cf47f29c24153cb43792af
    https://github.com/scummvm/scummvm/commit/6bfee722ae08996d69cf47f29c24153cb43792af
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:45+02:00

Commit Message:
ALCACHOFA: Fix shape loading and add debug drawing

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/alcachofa.h
    engines/alcachofa/console.cpp
    engines/alcachofa/console.h
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/general-objects.cpp
    engines/alcachofa/graphics-opengl.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/graphics.h
    engines/alcachofa/objects.h
    engines/alcachofa/rooms.cpp
    engines/alcachofa/rooms.h
    engines/alcachofa/shape.cpp
    engines/alcachofa/shape.h


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index d96f822ee48..fda27aa2c04 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -57,6 +57,7 @@ Common::String AlcachofaEngine::getGameId() const {
 }
 
 Common::Error AlcachofaEngine::run() {
+	setDebugger(&_console);
 	_renderer.reset(IRenderer::createOpenGLRenderer(Common::Point(1024, 768)));
 	_drawQueue.reset(new DrawQueue(_renderer.get()));
 	_world.reset(new World());
@@ -68,9 +69,6 @@ Common::Error AlcachofaEngine::run() {
 	world().currentRoom() = room;
 	room->loadResources();
 
-	// Set the engine's debugger console
-	setDebugger(new Console());
-
 	// If a savegame was selected from the launcher, load it
 	int saveSlot = ConfMan.getInt("save_slot");
 	if (saveSlot != -1)
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index 54a8cf57837..9f37babf13b 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -36,6 +36,7 @@
 
 #include "alcachofa/detection.h"
 #include "alcachofa/camera.h"
+#include "alcachofa/console.h"
 
 namespace Alcachofa {
 
@@ -59,6 +60,7 @@ public:
 	inline DrawQueue &drawQueue() { return *_drawQueue; }
 	inline Camera &camera() { return _camera; }
 	inline World &world() { return *_world; }
+	inline Console &console() { return _console; }
 
 	uint32 getFeatures() const;
 
@@ -108,6 +110,7 @@ private:
 	Common::ScopedPtr<DrawQueue> _drawQueue;
 	Common::ScopedPtr<World> _world;
 	Camera _camera;
+	Console _console;
 };
 
 extern AlcachofaEngine *g_engine;
diff --git a/engines/alcachofa/console.cpp b/engines/alcachofa/console.cpp
index a64a7fd216f..b810f355886 100644
--- a/engines/alcachofa/console.cpp
+++ b/engines/alcachofa/console.cpp
@@ -24,15 +24,11 @@
 namespace Alcachofa {
 
 Console::Console() : GUI::Debugger() {
-	registerCmd("test",   WRAP_METHOD(Console, Cmd_test));
+	registerVar("showInteractables", &_showInteractables);
+	registerVar("showFloor", &_showFloor);
 }
 
 Console::~Console() {
 }
 
-bool Console::Cmd_test(int argc, const char **argv) {
-	debugPrintf("Test\n");
-	return true;
-}
-
 } // End of namespace Alcachofa
diff --git a/engines/alcachofa/console.h b/engines/alcachofa/console.h
index f8b2028fefc..0bf5bf6ef94 100644
--- a/engines/alcachofa/console.h
+++ b/engines/alcachofa/console.h
@@ -28,11 +28,22 @@
 namespace Alcachofa {
 
 class Console : public GUI::Debugger {
-private:
-	bool Cmd_test(int argc, const char **argv);
 public:
 	Console();
 	~Console() override;
+
+	inline bool showInteractables() const { return _showInteractables; }
+	inline bool showFloor() const { return _showFloor; }
+
+	inline bool isAnyDebugDrawingOn() const {
+		return
+			_showInteractables ||
+			_showFloor;
+	}
+
+private:
+	bool _showInteractables = true;
+	bool _showFloor = true;
 };
 
 } // End of namespace Alcachofa
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 55217e81faa..3e9be3137ca 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -22,8 +22,10 @@
 #include "objects.h"
 #include "rooms.h"
 #include "stream-helper.h"
+#include "alcachofa.h"
 
 using namespace Common;
+using namespace Math;
 
 namespace Alcachofa {
 
@@ -40,6 +42,14 @@ InteractableObject::InteractableObject(Room *room, ReadStream &stream)
 	_relatedObject.toUppercase();
 }
 
+void InteractableObject::drawDebug() {
+	auto renderer = dynamic_cast<IDebugRenderer *>(&g_engine->renderer());
+	if (!g_engine->console().showInteractables() || renderer == nullptr || !isEnabled())
+		return;
+
+	renderer->debugShape(*shape());
+}
+
 Door::Door(Room *room, ReadStream &stream)
 	: InteractableObject(room, stream)
 	, _targetRoom(readVarString(stream))
diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index 5189793ec2c..c0ae21dbb84 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -51,6 +51,9 @@ void ObjectBase::toggle(bool isEnabled) {
 void ObjectBase::draw() {
 }
 
+void ObjectBase::drawDebug() {
+}
+
 void ObjectBase::update() {
 }
 
diff --git a/engines/alcachofa/graphics-opengl.cpp b/engines/alcachofa/graphics-opengl.cpp
index 4a376b1a40f..0899d3f08e9 100644
--- a/engines/alcachofa/graphics-opengl.cpp
+++ b/engines/alcachofa/graphics-opengl.cpp
@@ -101,7 +101,7 @@ private:
 	bool _withMipmaps;
 };
 
-class OpenGLRenderer : public IRenderer {
+class OpenGLRenderer : public IDebugRenderer {
 public:
 	OpenGLRenderer(Point resolution)
 		: _resolution(resolution) {
@@ -110,6 +110,7 @@ public:
 		GL_CALL(glDisable(GL_DEPTH_TEST));
 		GL_CALL(glDisable(GL_SCISSOR_TEST));
 		GL_CALL(glDisable(GL_STENCIL_TEST));
+		GL_CALL(glDisable(GL_CULL_FACE));
 		GL_CALL(glEnable(GL_BLEND));
 		GL_CALL(glDepthMask(GL_FALSE));
 	}
@@ -250,7 +251,7 @@ public:
 
 		float colors[] = { color.r / 255.0f, color.g / 255.0f, color.b / 255.0f, color.a / 255.0f };
 
-		glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
+		GL_CALL(glColor4f(1.0f, 1.0f, 1.0f, 1.0f));
 		GL_CALL(glVertexPointer(2, GL_FLOAT, 0, positions));
 		if (_currentTexture != nullptr)
 			GL_CALL(glTexCoordPointer(2, GL_FLOAT, 0, texCoords));
@@ -264,6 +265,31 @@ public:
 #endif
 	}
 
+	virtual void debugPolygon(
+		Span<Vector2d> points,
+		Color color
+	) override {
+		setTexture(nullptr);
+		setBlendMode(BlendMode::Alpha);
+		GL_CALL(glVertexPointer(2, GL_FLOAT, 0, points.data()));
+		GL_CALL(glLineWidth(4.0f));
+		GL_CALL(glPointSize(8.0f));
+
+		GL_CALL(glColor4ub(color.r, color.g, color.b, color.a));
+		if (points.size() > 2)
+			GL_CALL(glDrawArrays(GL_POLYGON, 0, points.size()));
+
+		color.a = (byte)(MIN(255.0f, color.a * 1.3f));
+		GL_CALL(glColor4ub(color.r, color.g, color.b, color.a));
+		if (points.size() > 1)
+			GL_CALL(glDrawArrays(GL_LINE_LOOP, 0, points.size()));
+
+		color.a = (byte)(MIN(255.0f, color.a * 1.3f));
+		GL_CALL(glColor4ub(color.r, color.g, color.b, color.a));
+		if (points.size() > 0)
+			GL_CALL(glDrawArrays(GL_POINTS, 0, points.size()));
+	}
+
 private:
 	void initViewportAndMatrices() {
 		int32 screenWidth = g_system->getWidth();
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 8a6301de810..ecded3d37c4 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -22,6 +22,7 @@
 #include "graphics.h"
 #include "stream-helper.h"
 #include "alcachofa.h"
+#include "shape.h"
 
 #include "common/system.h"
 #include "common/file.h"
@@ -37,6 +38,22 @@ namespace Alcachofa {
 
 ITexture::ITexture(Point size) : _size(size) {}
 
+void IDebugRenderer::debugShape(const Shape &shape, Color color) {
+	constexpr uint kMaxPoints = 16;
+	Vector2d points2d[kMaxPoints];
+	for (auto polygon : shape) {
+		// I don't think this will happen but let's be sure
+		assert(polygon._points.size() <= kMaxPoints);
+		for (uint i = 0; i < polygon._points.size(); i++) {
+			const auto p3d = polygon._points[i];
+			const auto p2d = g_engine->camera().transform3Dto2D(Vector3d(p3d.x, p3d.y, kBaseScale));
+			points2d[i] = Vector2d(p2d.x(), p2d.y());
+		}
+
+		debugPolygon({ points2d, polygon._points.size() }, color);
+	}
+}
+
 AnimationBase::AnimationBase(String fileName, AnimationFolder folder)
 	: _fileName(move(fileName))
 	, _folder(folder) {
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index f6de8336212..e1028f6bf55 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -26,6 +26,7 @@
 #include "common/stream.h"
 #include "common/serializer.h"
 #include "common/rect.h"
+#include "common/span.h"
 #include "math/vector2d.h"
 #include "graphics/managed_surface.h"
 
@@ -76,11 +77,15 @@ constexpr const int8 kOrderCount = 70;
 constexpr const int8 kForegroundOrderCount = 10;
 
 struct Color {
-	uint8 b, g, r, a;
+	uint8 r, g, b, a;
 };
 static constexpr const Color kWhite = { 255, 255, 255, 255 };
 static constexpr const Color kBlack = { 0, 0, 0, 255 };
 static constexpr const Color kClear = { 0, 0, 0, 0 };
+static constexpr const Color kDebugRed = { 250, 0, 0, 70 };
+static constexpr const Color kDebugBlue = { 0, 0, 255, 110 };
+
+class Shape;
 
 class ITexture {
 public:
@@ -117,6 +122,19 @@ public:
 	static IRenderer *createOpenGLRenderer(Common::Point resolution);
 };
 
+class IDebugRenderer : public IRenderer {
+public:
+	virtual void debugPolygon(
+		Common::Span<Math::Vector2d> points,
+		Color color = kDebugRed
+	) = 0;
+
+	virtual void debugShape(
+		const Shape &shape,
+		Color color = kDebugRed
+	);
+};
+
 enum class AnimationFolder {
 	Animations,
 	Masks,
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 0fde17ce3f6..75a9d747e69 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -45,6 +45,7 @@ public:
 
 	virtual void toggle(bool isEnabled);
 	virtual void draw();
+	virtual void drawDebug();
 	virtual void update();
 	virtual void loadResources();
 	virtual void freeResources();
@@ -277,6 +278,8 @@ public:
 	InteractableObject(Room *room, Common::ReadStream &stream);
 	virtual ~InteractableObject() override = default;
 
+	virtual void drawDebug() override;
+
 private:
 	Common::Point _interactionPoint;
 	CursorType _cursorType;
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 0cfb0207dbb..2ba3e27ea91 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -133,6 +133,8 @@ void Room::update() {
 		world().globalRoom().drawObjects();
 		// TODO: Draw black borders
 		g_engine->drawQueue().draw();
+		drawDebug();
+		world().globalRoom().drawDebug();
 	}
 }
 
@@ -149,6 +151,17 @@ void Room::drawObjects() {
 		object->draw();
 }
 
+void Room::drawDebug() {
+	auto renderer = dynamic_cast<IDebugRenderer *>(&g_engine->renderer());
+	if (renderer == nullptr || !g_engine->console().isAnyDebugDrawingOn())
+		return;
+	for (auto *object : _objects)
+		object->drawDebug();
+	if (_activeFloorI >= 0 && g_engine->console().showFloor())
+		renderer->debugShape(_floors[_activeFloorI], kDebugBlue);
+
+}
+
 void Room::loadResources() {
 	for (auto *object : _objects)
 		object->loadResources();
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index 0d95142cf7e..c76189de741 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -47,6 +47,7 @@ protected:
 	Room(World *world, Common::ReadStream &stream, bool hasUselessByte);
 	void updateObjects();
 	void drawObjects();
+	void drawDebug();
 
 	World *_world;
 	Common::String _name;
diff --git a/engines/alcachofa/shape.cpp b/engines/alcachofa/shape.cpp
index 74fe921f75d..451b6ac6b5f 100644
--- a/engines/alcachofa/shape.cpp
+++ b/engines/alcachofa/shape.cpp
@@ -61,15 +61,16 @@ uint Shape::addPolygon(uint maxCount) {
 			if (_points[firstI + newCount] == _points[firstI])
 				break;
 		}
+		_points.resize(firstI + newCount);
 	}
 	_polygons.push_back({ firstI, newCount });
 	return newCount;
 }
 
-Polygon Shape::at(uint index) {
+Polygon Shape::at(uint index) const {
 	auto range = _polygons[index];
 	Polygon p;
-	p._points = Span<Point>(_points.data() + range.first, range.second);
+	p._points = Span<const Point>(_points.data() + range.first, range.second);
 	return p;
 }
 
@@ -95,11 +96,11 @@ PathFindingShape::PathFindingShape(ReadStream &stream) {
 	// TODO: Implement the path finding
 }
 
-PathFindingPolygon PathFindingShape::at(uint index) {
+PathFindingPolygon PathFindingShape::at(uint index) const {
 	auto range = _polygons[index];
 	PathFindingPolygon p;
-	p._points = Span<Point>(_points.data() + range.first, range.second);
-	p._pointValues = Span<int8>(_pointValues.data() + range.first, range.second);
+	p._points = Span<const Point>(_points.data() + range.first, range.second);
+	p._pointValues = Span<const int8>(_pointValues.data() + range.first, range.second);
 	p._polygonValue = _polygonValues[index];
 	return p;
 }
@@ -127,12 +128,12 @@ FloorColorShape::FloorColorShape(ReadStream &stream) {
 	}
 }
 
-FloorColorPolygon FloorColorShape::at(uint index) {
+FloorColorPolygon FloorColorShape::at(uint index) const {
 	auto range = _polygons[index];
 	FloorColorPolygon p;
-	p._points = Span<Point>(_points.data() + range.first, range.second);
-	p._pointWeights = Span<uint8>(_pointWeights.data() + range.first, range.second);
-	p._pointColors = Span<uint32>(_pointColors.data() + range.first, range.second);
+	p._points = Span<const Point>(_points.data() + range.first, range.second);
+	p._pointWeights = Span<const uint8>(_pointWeights.data() + range.first, range.second);
+	p._pointColors = Span<const uint32>(_pointColors.data() + range.first, range.second);
 	p._polygonValue = _polygonValues[index];
 	return p;
 }
diff --git a/engines/alcachofa/shape.h b/engines/alcachofa/shape.h
index 4bb3bd8e95d..a28d9f90ced 100644
--- a/engines/alcachofa/shape.h
+++ b/engines/alcachofa/shape.h
@@ -32,17 +32,17 @@
 namespace Alcachofa {
 
 struct Polygon {
-	Common::Span<Common::Point> _points;
+	Common::Span<const Common::Point> _points;
 };
 
 struct PathFindingPolygon : Polygon {
-	Common::Span<int8> _pointValues;
+	Common::Span<const int8> _pointValues;
 	int8 _polygonValue;
 };
 
 struct FloorColorPolygon : Polygon {
-	Common::Span<uint32> _pointColors;
-	Common::Span<uint8> _pointWeights;
+	Common::Span<const uint32> _pointColors;
+	Common::Span<const uint8> _pointWeights;
 	int8 _polygonValue;
 };
 
@@ -50,6 +50,7 @@ template<class TShape, typename TPolygon>
 struct PolygonIterator {
 	using difference_type = uint;
 	using value_type = TPolygon;
+	using my_type = PolygonIterator<TShape, TPolygon>;
 
 	inline value_type operator*() const {
 		return _shape.at(_index);
@@ -68,14 +69,22 @@ struct PolygonIterator {
 		return tmp;
 	}
 
+	inline bool operator==(const my_type& it) const {
+		return &this->_shape == &it._shape && this->_index == it._index;
+	}
+
+	inline bool operator!=(const my_type& it) const {
+		return &this->_shape != &it._shape || this->_index != it._index;
+	}
+	
 private:
 	friend typename Common::remove_const_t<TShape>;
-	PolygonIterator(TShape &shape, uint index = 0)
+	PolygonIterator(const TShape &shape, uint index = 0)
 		: _shape(shape)
 		, _index(index) {
 	}
 
-	TShape &_shape;
+	const TShape &_shape;
 	uint _index;
 };
 
@@ -89,10 +98,10 @@ public:
 	inline Common::Point firstPoint() const { return _points.empty() ? Common::Point() : _points[0]; }
 	inline uint polygonCount() const { return _polygons.size(); }
 	inline bool empty() const { return polygonCount() == 0; }
-	inline iterator begin() { return { *this, 0 }; }
-	inline iterator end() { return { *this, polygonCount() }; }
+	inline iterator begin() const { return { *this, 0 }; }
+	inline iterator end() const { return { *this, polygonCount() }; }
 
-	Polygon at(uint index);
+	Polygon at(uint index) const;
 
 protected:
 	uint addPolygon(uint maxCount);
@@ -122,10 +131,10 @@ public:
 	PathFindingShape();
 	PathFindingShape(Common::ReadStream &stream);
 
-	inline iterator begin() { return { *this, 0 }; }
-	inline iterator end() { return { *this, polygonCount() }; }
+	inline iterator begin() const { return { *this, 0 }; }
+	inline iterator end() const { return { *this, polygonCount() }; }
 
-	PathFindingPolygon at(uint index);
+	PathFindingPolygon at(uint index) const;
 
 private:
 	Common::Array<int8> _pointValues;
@@ -140,10 +149,10 @@ public:
 	FloorColorShape();
 	FloorColorShape(Common::ReadStream &stream);
 
-	inline iterator begin() { return { *this, 0 }; }
-	inline iterator end() { return { *this, polygonCount() }; }
+	inline iterator begin() const { return { *this, 0 }; }
+	inline iterator end() const { return { *this, polygonCount() }; }
 
-	FloorColorPolygon at(uint index);
+	FloorColorPolygon at(uint index) const;
 
 private:
 	Common::Array<uint32> _pointColors;


Commit: f84c67978821b5bedbd951774b3fc9c10c02fc5a
    https://github.com/scummvm/scummvm/commit/f84c67978821b5bedbd951774b3fc9c10c02fc5a
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:45+02:00

Commit Message:
ALCACHOFA: Add mouse input and Shape::contains

Changed paths:
  A engines/alcachofa/Input.cpp
  A engines/alcachofa/Input.h
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/alcachofa.h
    engines/alcachofa/camera.cpp
    engines/alcachofa/camera.h
    engines/alcachofa/rooms.cpp
    engines/alcachofa/rooms.h
    engines/alcachofa/shape.cpp
    engines/alcachofa/shape.h


diff --git a/engines/alcachofa/Input.cpp b/engines/alcachofa/Input.cpp
new file mode 100644
index 00000000000..e227fc59492
--- /dev/null
+++ b/engines/alcachofa/Input.cpp
@@ -0,0 +1,61 @@
+/* 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 "input.h"
+#include "alcachofa.h"
+
+using namespace Common;
+
+namespace Alcachofa {
+
+void Input::nextFrame() {
+	_wasMouseLeftPressed = false;
+	_wasMouseRightPressed = false;
+}
+
+bool Input::handleEvent(const Common::Event &event) {
+	switch (event.type) {
+	case EVENT_LBUTTONDOWN:
+		_wasMouseLeftPressed = true;
+		_isMouseLeftDown = true;
+		return true;
+	case EVENT_LBUTTONUP:
+		_isMouseLeftDown = false;
+		return true;
+	case EVENT_RBUTTONDOWN:
+		_wasMouseRightPressed = true;
+		_isMouseRightDown = true;
+		return true;
+	case EVENT_RBUTTONUP:
+		_isMouseRightDown = false;
+		return true;
+	case EVENT_MOUSEMOVE: {
+		_mousePos2D = event.mouse;
+		auto pos3D = g_engine->camera().transform2Dto3D({ (float)_mousePos2D.x, (float)_mousePos2D.y, kBaseScale });
+		_mousePos3D = { (int16)pos3D.x(), (int16)pos3D.y() };
+		return true;
+	}
+	default:
+		return false;
+	}
+}
+
+}
diff --git a/engines/alcachofa/Input.h b/engines/alcachofa/Input.h
new file mode 100644
index 00000000000..a648d72b848
--- /dev/null
+++ b/engines/alcachofa/Input.h
@@ -0,0 +1,54 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef INPUT_H
+#define INPUT_H
+
+#include "common/events.h"
+
+namespace Alcachofa {
+
+class Input {
+public:
+	inline bool wasMouseLeftPressed() const { return _wasMouseLeftPressed; }
+	inline bool wasMouseRightPressed() const { return _wasMouseRightPressed; }
+	inline bool isMouseLeftDown() const { return _isMouseLeftDown; }
+	inline bool isMouseRightDown() const { return _isMouseRightDown; }
+	inline const Common::Point &mousePos2D() const { return _mousePos2D; }
+	inline const Common::Point &mousePos3D() const { return _mousePos3D; }
+
+	void nextFrame();
+	bool handleEvent(const Common::Event &event);
+
+private:
+	bool
+		_wasMouseLeftPressed,
+		_wasMouseRightPressed,
+		_isMouseLeftDown,
+		_isMouseRightDown;
+	Common::Point
+		_mousePos2D,
+		_mousePos3D;
+};
+
+}
+
+#endif // INPUT_H
diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index fda27aa2c04..76d3c998b7d 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -57,7 +57,7 @@ Common::String AlcachofaEngine::getGameId() const {
 }
 
 Common::Error AlcachofaEngine::run() {
-	setDebugger(&_console);
+	setDebugger(_console);
 	_renderer.reset(IRenderer::createOpenGLRenderer(Common::Point(1024, 768)));
 	_drawQueue.reset(new DrawQueue(_renderer.get()));
 	_world.reset(new World());
@@ -74,10 +74,15 @@ Common::Error AlcachofaEngine::run() {
 	if (saveSlot != -1)
 		(void)loadGameState(saveSlot);
 
+	g_system->showMouse(true);
+
 	Common::Event e;
 	Graphics::FrameLimiter limiter(g_system, 60);
 	while (!shouldQuit()) {
+		_input.nextFrame();
 		while (g_system->getEventManager()->pollEvent(e)) {
+			if (_input.handleEvent(e))
+				continue;
 		}
 
 		_renderer->begin();
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index 9f37babf13b..3a183acd868 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -36,6 +36,7 @@
 
 #include "alcachofa/detection.h"
 #include "alcachofa/camera.h"
+#include "alcachofa/input.h"
 #include "alcachofa/console.h"
 
 namespace Alcachofa {
@@ -59,8 +60,9 @@ public:
 	inline IRenderer &renderer() { return *_renderer; }
 	inline DrawQueue &drawQueue() { return *_drawQueue; }
 	inline Camera &camera() { return _camera; }
+	inline Input &input() { return _input; }
 	inline World &world() { return *_world; }
-	inline Console &console() { return _console; }
+	inline Console &console() { return *_console; }
 
 	uint32 getFeatures() const;
 
@@ -106,11 +108,12 @@ public:
 	}
 
 private:
+	Console *_console = new Console();
 	Common::ScopedPtr<IRenderer> _renderer;
 	Common::ScopedPtr<DrawQueue> _drawQueue;
 	Common::ScopedPtr<World> _world;
 	Camera _camera;
-	Console _console;
+	Input _input;
 };
 
 extern AlcachofaEngine *g_engine;
diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index 67870f4434c..fabc394209b 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -30,14 +30,14 @@ using namespace Math;
 
 namespace Alcachofa {
 
-void Camera::setRoomBounds(Rect bgBounds, float bgScale) {
+void Camera::setRoomBounds(Point bgSize, int16 bgScale) {
 	float scaleFactor = 1 - bgScale * kInvBaseScale;
 	_roomMin = Vector2d(
 		g_system->getWidth() / 2 * scaleFactor,
 		g_system->getHeight() / 2 * scaleFactor);
 	_roomMax = _roomMin + Vector2d(
-		bgBounds.width() * bgScale * kInvBaseScale,
-		bgBounds.height() * bgScale * kInvBaseScale);
+		bgSize.x * bgScale * kInvBaseScale,
+		bgSize.y * bgScale * kInvBaseScale);
 }
 
 static Matrix4 scaleMatrix(float scale) {
diff --git a/engines/alcachofa/camera.h b/engines/alcachofa/camera.h
index 520655c558f..68d79891524 100644
--- a/engines/alcachofa/camera.h
+++ b/engines/alcachofa/camera.h
@@ -43,7 +43,7 @@ public:
 	void update();
 	Math::Vector3d transform2Dto3D(Math::Vector3d v) const;
 	Math::Vector3d transform3Dto2D(Math::Vector3d v) const;
-	void setRoomBounds(Common::Rect bgBounds, float bgScale);
+	void setRoomBounds(Common::Point bgSize, int16 bgScale);
 
 private:
 	static constexpr const float kAccelerationThreshold = 2.89062f;
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 2ba3e27ea91..2c94ae81b0c 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -125,6 +125,10 @@ ObjectBase *Room::getObjectByName(const Common::String &name) const {
 }
 
 void Room::update() {
+	if (world().currentRoom() == this) {
+		updateRoomBounds();
+		updateInput();
+	}
 	if (world().currentRoom() == this)
 		updateObjects();
 	if (world().currentRoom() == this) {
@@ -138,6 +142,22 @@ void Room::update() {
 	}
 }
 
+void Room::updateInput() {
+	if (g_engine->input().wasMouseLeftPressed()) {
+		Point p2d = g_engine->input().mousePos2D();
+		Point p3d = g_engine->input().mousePos3D();
+		bool contains = _floors[_activeFloorI].contains(p3d);
+		warning("Mouse 2D: %d, %d \t\t3D: %d, %d\t%s", p2d.x, p2d.y, p3d.x, p3d.y, contains ? "yes" : "no");
+	}
+}
+
+void Room::updateRoomBounds() {
+	auto background = getObjectByName("Background");
+	auto graphic = background == nullptr ? nullptr : background->graphic();
+	if (graphic != nullptr)
+		g_engine->camera().setRoomBounds(graphic->animation().imageSize(0), graphic->scale());
+}
+
 void Room::updateObjects() {
 	for (auto *object : _objects) {
 		object->update();
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index c76189de741..7678552c69d 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -38,6 +38,7 @@ public:
 	inline const Common::String &name() const { return _name; }
 
 	void update();
+	virtual void updateInput();
 	virtual void loadResources();
 	virtual void freeResources();
 	virtual void serializeSave(Common::Serializer &serializer);
@@ -45,6 +46,7 @@ public:
 
 protected:
 	Room(World *world, Common::ReadStream &stream, bool hasUselessByte);
+	void updateRoomBounds();
 	void updateObjects();
 	void drawObjects();
 	void drawDebug();
diff --git a/engines/alcachofa/shape.cpp b/engines/alcachofa/shape.cpp
index 451b6ac6b5f..4ba213ba938 100644
--- a/engines/alcachofa/shape.cpp
+++ b/engines/alcachofa/shape.cpp
@@ -22,10 +22,31 @@
 #include "shape.h"
 #include "stream-helper.h"
 
+#include "math/line2d.h"
+
 using namespace Common;
+using namespace Math;
 
 namespace Alcachofa {
 
+static int sideOfLine(const Point &a, const Point &b, const Point &q) {
+	return (b.x - a.x) * (q.y - a.y) - (b.y - a.y) * (q.x - a.x);
+}
+
+bool Polygon::contains(const Point &query) const {
+	switch (_points.size()) {
+	case 0: return false;
+	case 1: return query == _points[0];
+	default:
+		// we assume that the polygon is convex
+		for (uint i = 1; i < _points.size(); i++) {
+			if (sideOfLine(_points[i - 1], _points[i], query) < 0)
+				return false;
+		}
+		return sideOfLine(_points[_points.size() - 1], _points[0], query) >= 0;
+	}
+}
+
 Shape::Shape() {}
 
 Shape::Shape(ReadStream &stream) {
@@ -74,6 +95,14 @@ Polygon Shape::at(uint index) const {
 	return p;
 }
 
+bool Shape::contains(const Point &query) const {
+	for (auto polygon : *this) {
+		if (polygon.contains(query))
+			return true;
+	}
+	return false;
+}
+
 PathFindingShape::PathFindingShape() {}
 
 PathFindingShape::PathFindingShape(ReadStream &stream) {
diff --git a/engines/alcachofa/shape.h b/engines/alcachofa/shape.h
index a28d9f90ced..2734f37cd93 100644
--- a/engines/alcachofa/shape.h
+++ b/engines/alcachofa/shape.h
@@ -33,6 +33,8 @@ namespace Alcachofa {
 
 struct Polygon {
 	Common::Span<const Common::Point> _points;
+
+	bool contains(const Common::Point &query) const;
 };
 
 struct PathFindingPolygon : Polygon {
@@ -102,6 +104,7 @@ public:
 	inline iterator end() const { return { *this, polygonCount() }; }
 
 	Polygon at(uint index) const;
+	bool contains(const Common::Point &query) const;
 
 protected:
 	uint addPolygon(uint maxCount);


Commit: 2e8b508a5f86a0cb9847b61eb3e901eb26ca1516
    https://github.com/scummvm/scummvm/commit/2e8b508a5f86a0cb9847b61eb3e901eb26ca1516
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:45+02:00

Commit Message:
ALCACHOFA: Add shape interpolations

Changed paths:
  A engines/alcachofa/common.h
    engines/alcachofa/console.cpp
    engines/alcachofa/console.h
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/graphics.h
    engines/alcachofa/objects.h
    engines/alcachofa/shape.cpp
    engines/alcachofa/shape.h


diff --git a/engines/alcachofa/common.h b/engines/alcachofa/common.h
new file mode 100644
index 00000000000..8f9669d5ea2
--- /dev/null
+++ b/engines/alcachofa/common.h
@@ -0,0 +1,61 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef COMMON_H
+#define COMMON_H
+
+namespace Alcachofa {
+
+enum class CursorType {
+	Normal,
+	LookAt,
+	Use,
+	GoTo,
+	LeaveUp,
+	LeaveRight,
+	LeaveDown,
+	LeaveLeft
+};
+
+enum class Direction {
+	Up,
+	Down,
+	Left,
+	Right
+};
+
+constexpr const int32 kDirectionCount = 4;
+constexpr const int8 kOrderCount = 70;
+constexpr const int8 kForegroundOrderCount = 10;
+
+struct Color {
+	uint8 r, g, b, a;
+};
+static constexpr const Color kWhite = { 255, 255, 255, 255 };
+static constexpr const Color kBlack = { 0, 0, 0, 255 };
+static constexpr const Color kClear = { 0, 0, 0, 0 };
+static constexpr const Color kDebugRed = { 250, 0, 0, 70 };
+static constexpr const Color kDebugGreen = { 0, 255, 0, 85 };
+static constexpr const Color kDebugBlue = { 0, 0, 255, 110 };
+
+}
+
+#endif // ALCACHOFA_COMMON_H
diff --git a/engines/alcachofa/console.cpp b/engines/alcachofa/console.cpp
index b810f355886..aed5c63bded 100644
--- a/engines/alcachofa/console.cpp
+++ b/engines/alcachofa/console.cpp
@@ -26,6 +26,7 @@ namespace Alcachofa {
 Console::Console() : GUI::Debugger() {
 	registerVar("showInteractables", &_showInteractables);
 	registerVar("showFloor", &_showFloor);
+	registerVar("showFloorColor", &_showFloorColor);
 }
 
 Console::~Console() {
diff --git a/engines/alcachofa/console.h b/engines/alcachofa/console.h
index 0bf5bf6ef94..898605f2e0e 100644
--- a/engines/alcachofa/console.h
+++ b/engines/alcachofa/console.h
@@ -34,16 +34,19 @@ public:
 
 	inline bool showInteractables() const { return _showInteractables; }
 	inline bool showFloor() const { return _showFloor; }
+	inline bool showFloorColor() const { return _showFloorColor; }
 
 	inline bool isAnyDebugDrawingOn() const {
 		return
 			_showInteractables ||
-			_showFloor;
+			_showFloor ||
+			_showFloorColor;
 	}
 
 private:
 	bool _showInteractables = true;
-	bool _showFloor = true;
+	bool _showFloor = false;
+	bool _showFloorColor = true;
 };
 
 } // End of namespace Alcachofa
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 3e9be3137ca..3b3085d79f3 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -179,4 +179,16 @@ FloorColor::FloorColor(Room *room, ReadStream &stream)
 	: ObjectBase(room, stream)
 	, _shape(stream) {}
 
+void FloorColor::drawDebug() {
+	auto renderer = dynamic_cast<IDebugRenderer *>(&g_engine->renderer());
+	if (!g_engine->console().showFloorColor() || renderer == nullptr || !isEnabled())
+		return;
+
+	renderer->debugShape(*shape(), kDebugGreen);
+}
+
+Shape *FloorColor::shape() {
+	return &_shape;
+}
+
 }
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index e1028f6bf55..3f5954a9ff9 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -31,6 +31,7 @@
 #include "graphics/managed_surface.h"
 
 #include "camera.h"
+#include "common.h"
 
 namespace Alcachofa {
 
@@ -54,37 +55,6 @@ enum class BlendMode {
 	Tinted
 };
 
-enum class CursorType {
-	Normal,
-	LookAt,
-	Use,
-	GoTo,
-	LeaveUp,
-	LeaveRight,
-	LeaveDown,
-	LeaveLeft
-};
-
-enum class Direction {
-	Up,
-	Down,
-	Left,
-	Right
-};
-
-constexpr const int32 kDirectionCount = 4;
-constexpr const int8 kOrderCount = 70;
-constexpr const int8 kForegroundOrderCount = 10;
-
-struct Color {
-	uint8 r, g, b, a;
-};
-static constexpr const Color kWhite = { 255, 255, 255, 255 };
-static constexpr const Color kBlack = { 0, 0, 0, 255 };
-static constexpr const Color kClear = { 0, 0, 0, 0 };
-static constexpr const Color kDebugRed = { 250, 0, 0, 70 };
-static constexpr const Color kDebugBlue = { 0, 0, 255, 110 };
-
 class Shape;
 
 class ITexture {
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 75a9d747e69..37d4de22251 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -387,6 +387,9 @@ public:
 	FloorColor(Room *room, Common::ReadStream &stream);
 	virtual ~FloorColor() override = default;
 
+	virtual void drawDebug() override;
+	virtual Shape *shape() override;
+
 private:
 	FloorColorShape _shape;
 };
diff --git a/engines/alcachofa/shape.cpp b/engines/alcachofa/shape.cpp
index 4ba213ba938..c6e644a9457 100644
--- a/engines/alcachofa/shape.cpp
+++ b/engines/alcachofa/shape.cpp
@@ -29,6 +29,10 @@ using namespace Math;
 
 namespace Alcachofa {
 
+static Vector2d asVec(const Point &p) {
+	return Vector2d((float)p.x, (float)p.y);
+}
+
 static int sideOfLine(const Point &a, const Point &b, const Point &q) {
 	return (b.x - a.x) * (q.y - a.y) - (b.y - a.y) * (q.x - a.x);
 }
@@ -47,6 +51,110 @@ bool Polygon::contains(const Point &query) const {
 	}
 }
 
+EdgeDistances Polygon::edgeDistances(uint startPointI, const Point &query) const {
+	assert(startPointI < _points.size());
+	uint endPointI = startPointI + 1 == _points.size() ? 0 : startPointI + 1;
+	Vector2d
+		a = asVec(_points[startPointI]),
+		b = asVec(_points[endPointI]),
+		q = asVec(query);
+	float edgeLength = a.getDistanceTo(b);
+	Vector2d edgeDir = (b - a) / edgeLength;
+	Vector2d edgeNormal(-edgeDir.getY(), edgeDir.getX());
+	EdgeDistances distances;
+	distances._edgeLength = edgeLength;
+	distances._onEdge = edgeDir.dotProduct(q - a);
+	distances._toEdge = abs(edgeNormal.dotProduct(q) - edgeNormal.dotProduct(a));
+	return distances;
+}
+
+static float depthAtForLine(const Point &a, const Point &b, const Point &q, int8 depthA, int8 depthB) {
+	return (sqrtf(a.sqrDist(q)) / a.sqrDist(b) * depthB + depthA) * 0.01f;
+}
+
+static float depthAtForConvex(const PathFindingPolygon &p, const Point &q) {
+	float sumDepths = 0, sumDistances = 0;
+	for (uint i = 0; i < p._points.size(); i++) {
+		uint j = i + 1 == p._points.size() ? 0 : i + 1;
+		auto distances = p.edgeDistances(i, q);
+		float depthOnEdge = p._pointDepths[i] + distances._onEdge * (p._pointDepths[j] - p._pointDepths[i]) / distances._edgeLength;
+		if (distances._toEdge < epsilon) // q is directly on the edge
+			return depthOnEdge;
+		sumDepths += 1 / distances._toEdge * depthOnEdge;
+		sumDistances += 1 / distances._toEdge;
+	}
+	return sumDepths / sumDistances * 0.01f;
+}
+
+float PathFindingPolygon::depthAt(const Point &query) const {
+	switch (_points.size()) {
+	case 0:
+	case 1: return 1.0f;
+	case 2: return depthAtForLine(_points[0], _points[1], query, _pointDepths[0], _pointDepths[1]);
+	default: return depthAtForConvex(*this, query);
+	}
+}
+
+static Color colorAtForLine(const Point &a, const Point &b, const Point &q, Color colorA, Color colorB) {
+	// I highly suspect RGB calculation being very bugged, so for now I just ignore and only calc alpha
+	float phase = sqrtf(q.sqrDist(a)) / a.sqrDist(b);
+	colorA.a += phase * colorB.a;
+	return colorA;
+}
+
+static Color colorAtForConvex(const FloorColorPolygon &p, const Point &query) {
+	// This is a quite literal translation of the original engine
+	// There may very well be a better way than this...
+	float weights[FloorColorShape::kPointsPerPolygon];
+	memset(weights, 0, sizeof(weights));
+
+	for (uint i = 0; i < p._points.size(); i++) {
+		EdgeDistances distances = p.edgeDistances(i, query);
+		float edgeWeight = distances._toEdge * distances._onEdge / distances._edgeLength;
+		if (distances._edgeLength > 1) {
+			weights[i] += edgeWeight;
+			weights[i + 1 == p._points.size() ? 0 : i + 1] += edgeWeight;
+		}
+	}
+	float weightSum = 0;
+	for (uint i = 0; i < p._points.size(); i++)
+		weightSum += weights[i];
+	for (uint i = 0; i < p._points.size(); i++) {
+		if (weights[i] < epsilon)
+			return p._pointColors[i];
+		weights[i] = weightSum / weights[i];
+	}
+
+	weightSum = 0;
+	for (uint i = 0; i < p._points.size(); i++)
+		weightSum += weights[i];
+	for (uint i = 0; i < p._points.size(); i++)
+		weights[i] /= weightSum;
+
+	float r = 0, g = 0, b = 0, a = 0.5f;
+	for (uint i = 0; i < p._points.size(); i++) {
+		r += p._pointColors[i].r * weights[i];
+		g += p._pointColors[i].g * weights[i];
+		b += p._pointColors[i].b * weights[i];
+		a += p._pointColors[i].a * weights[i];
+	}
+	return {
+		(byte)MIN(255, MAX(0, (int)r)),
+		(byte)MIN(255, MAX(0, (int)g)),
+		(byte)MIN(255, MAX(0, (int)b)),
+		(byte)MIN(255, MAX(0, (int)a)),
+	};
+}
+
+Color FloorColorPolygon::colorAt(const Point &query) const {
+	switch (_points.size()) {
+	case 0: return kWhite;
+	case 1: return { 255, 255, 255, _pointColors[0].a };
+	case 2: return colorAtForLine(_points[0], _points[1], query, _pointColors[0], _pointColors[1]);
+	default: return colorAtForConvex(*this, query);
+	}
+}
+
 Shape::Shape() {}
 
 Shape::Shape(ReadStream &stream) {
@@ -95,12 +203,16 @@ Polygon Shape::at(uint index) const {
 	return p;
 }
 
-bool Shape::contains(const Point &query) const {
-	for (auto polygon : *this) {
-		if (polygon.contains(query))
-			return true;
+int32 Shape::polygonContaining(const Point &query) const {
+	for (uint i = 0; i < _polygons.size(); i++) {
+		if (at(i).contains(query))
+			return (int32)i;
 	}
-	return false;
+	return -1;
+}
+
+bool Shape::contains(const Point &query) const {
+	return polygonContaining(query) >= 0;
 }
 
 PathFindingShape::PathFindingShape() {}
@@ -108,18 +220,20 @@ PathFindingShape::PathFindingShape() {}
 PathFindingShape::PathFindingShape(ReadStream &stream) {
 	auto polygonCount = stream.readUint16LE();
 	_polygons.reserve(polygonCount);
-	_polygonValues.reserve(polygonCount);
+	_polygonOrders.reserve(polygonCount);
 	_points.reserve(polygonCount * kPointsPerPolygon);
-	_pointValues.reserve(polygonCount * kPointsPerPolygon);
+	_pointDepths.reserve(polygonCount * kPointsPerPolygon);
 
 	for (int i = 0; i < polygonCount; i++) {
 		for (int j = 0; j < kPointsPerPolygon; j++)
 			_points.push_back(readPoint(stream));
-		_polygonValues.push_back(stream.readSByte());
+		_polygonOrders.push_back(stream.readSByte());
 		for (int j = 0; j < kPointsPerPolygon; j++)
-			_pointValues.push_back(stream.readSByte());
+			_pointDepths.push_back(stream.readByte());
 
-		addPolygon(kPointsPerPolygon);
+		uint pointCount = addPolygon(kPointsPerPolygon);
+		assert(pointCount <= kPointsPerPolygon);
+		_pointDepths.resize(_points.size());
 	}
 
 	// TODO: Implement the path finding
@@ -129,31 +243,47 @@ PathFindingPolygon PathFindingShape::at(uint index) const {
 	auto range = _polygons[index];
 	PathFindingPolygon p;
 	p._points = Span<const Point>(_points.data() + range.first, range.second);
-	p._pointValues = Span<const int8>(_pointValues.data() + range.first, range.second);
-	p._polygonValue = _polygonValues[index];
+	p._pointDepths = Span<const uint8>(_pointDepths.data() + range.first, range.second);
+	p._order = _polygonOrders[index];
 	return p;
 }
 
+int8 PathFindingShape::orderAt(const Point &query) const {
+	int32 polygon = polygonContaining(query);
+	return polygon < 0 ? 49 : _polygonOrders[polygon];
+}
+
+float PathFindingShape::depthAt(const Point &query) const {
+	int32 polygon = polygonContaining(query);
+	return polygon < 0 ? 1.0f : at(polygon).depthAt(query);
+}
+
 FloorColorShape::FloorColorShape() {}
 
 FloorColorShape::FloorColorShape(ReadStream &stream) {
 	auto polygonCount = stream.readUint16LE();
 	_polygons.reserve(polygonCount);
-	_polygonValues.reserve(polygonCount);
 	_points.reserve(polygonCount * kPointsPerPolygon);
 	_pointColors.reserve(polygonCount * kPointsPerPolygon);
-	_pointWeights.reserve(polygonCount * kPointsPerPolygon);
 
 	for (int i = 0; i < polygonCount; i++) {
 		for (int j = 0; j < kPointsPerPolygon; j++)
 			_points.push_back(readPoint(stream));
+		Color color; // RGB and A components are stored separately
 		for (int j = 0; j < kPointsPerPolygon; j++)
-			_pointWeights.push_back(stream.readSByte());
-		for (int j = 0; j < kPointsPerPolygon; j++)
-			_pointColors.push_back(stream.readUint32LE());
-		_polygonValues.push_back(stream.readSByte());
+			color.a = stream.readByte();
+		for (int j = 0; j < kPointsPerPolygon; j++) {
+			color.r = stream.readByte();
+			color.g = stream.readByte();
+			color.b = stream.readByte();
+			stream.readByte(); // second alpha value is ignored
+		}
+		_pointColors.push_back(color);
+		stream.readByte(); // unused byte per polygon
 
-		addPolygon(kPointsPerPolygon);
+		uint pointCount = addPolygon(kPointsPerPolygon);
+		assert(pointCount <= kPointsPerPolygon);
+		_pointColors.resize(_points.size());
 	}
 }
 
@@ -161,10 +291,15 @@ FloorColorPolygon FloorColorShape::at(uint index) const {
 	auto range = _polygons[index];
 	FloorColorPolygon p;
 	p._points = Span<const Point>(_points.data() + range.first, range.second);
-	p._pointWeights = Span<const uint8>(_pointWeights.data() + range.first, range.second);
-	p._pointColors = Span<const uint32>(_pointColors.data() + range.first, range.second);
-	p._polygonValue = _polygonValues[index];
+	p._pointColors = Span<const Color>(_pointColors.data() + range.first, range.second);
 	return p;
 }
 
+OptionalColor FloorColorShape::colorAt(const Common::Point &query) const {
+	int32 polygon = polygonContaining(query);
+	return polygon < 0
+		? OptionalColor(false, kClear)
+		: OptionalColor(true, at(polygon).colorAt(query));
+}
+
 }
diff --git a/engines/alcachofa/shape.h b/engines/alcachofa/shape.h
index 2734f37cd93..a74335763ca 100644
--- a/engines/alcachofa/shape.h
+++ b/engines/alcachofa/shape.h
@@ -29,23 +29,34 @@
 #include "common/util.h"
 #include "math/vector2d.h"
 
+#include "common.h"
+
 namespace Alcachofa {
 
+struct EdgeDistances {
+	float _edgeLength;
+	float _onEdge;
+	float _toEdge;
+};
+
 struct Polygon {
 	Common::Span<const Common::Point> _points;
 
 	bool contains(const Common::Point &query) const;
+	EdgeDistances edgeDistances(uint startPointI, const Common::Point &query) const;
 };
 
 struct PathFindingPolygon : Polygon {
-	Common::Span<const int8> _pointValues;
-	int8 _polygonValue;
+	Common::Span<const uint8> _pointDepths;
+	int8 _order;
+
+	float depthAt(const Common::Point &query) const;
 };
 
 struct FloorColorPolygon : Polygon {
-	Common::Span<const uint32> _pointColors;
-	Common::Span<const uint8> _pointWeights;
-	int8 _polygonValue;
+	Common::Span<const Color> _pointColors;
+
+	Color colorAt(const Common::Point &query) const;
 };
 
 template<class TShape, typename TPolygon>
@@ -58,13 +69,13 @@ struct PolygonIterator {
 		return _shape.at(_index);
 	}
 
-	inline PolygonIterator<TShape, TPolygon> &operator++() {
+	inline my_type &operator++() {
 		assert(_index < _shape.polygonCount());
 		_index++;
 		return *this;
 	}
 
-	inline PolygonIterator<TShape, TPolygon> &operator++(int) {
+	inline my_type &operator++(int) {
 		assert(_index < _shape.polygonCount());
 		auto tmp = *this;
 		++*this;
@@ -104,6 +115,7 @@ public:
 	inline iterator end() const { return { *this, polygonCount() }; }
 
 	Polygon at(uint index) const;
+	int32 polygonContaining(const Common::Point &query) const;
 	bool contains(const Common::Point &query) const;
 
 protected:
@@ -138,12 +150,15 @@ public:
 	inline iterator end() const { return { *this, polygonCount() }; }
 
 	PathFindingPolygon at(uint index) const;
+	int8 orderAt(const Common::Point &query) const;
+	float depthAt(const Common::Point &query) const;
 
 private:
-	Common::Array<int8> _pointValues;
-	Common::Array<int8> _polygonValues;
+	Common::Array<uint8> _pointDepths;
+	Common::Array<int8> _polygonOrders;
 };
 
+using OptionalColor = Common::Pair<bool, Color>;
 class FloorColorShape final : public Shape {
 public:
 	using iterator = PolygonIterator<FloorColorShape, FloorColorPolygon>;
@@ -156,11 +171,10 @@ public:
 	inline iterator end() const { return { *this, polygonCount() }; }
 
 	FloorColorPolygon at(uint index) const;
+	OptionalColor colorAt(const Common::Point &query) const;
 
 private:
-	Common::Array<uint32> _pointColors;
-	Common::Array<uint8> _pointWeights;
-	Common::Array<int8> _polygonValues;
+	Common::Array<Color> _pointColors;
 };
 
 }


Commit: 8518c4d275e96605990b81ecbfd1a1e84092a326
    https://github.com/scummvm/scummvm/commit/8518c4d275e96605990b81ecbfd1a1e84092a326
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:45+02:00

Commit Message:
ALCACHOFA: Add path finding

Changed paths:
    engines/alcachofa/console.h
    engines/alcachofa/graphics-opengl.cpp
    engines/alcachofa/graphics.h
    engines/alcachofa/rooms.cpp
    engines/alcachofa/shape.cpp
    engines/alcachofa/shape.h


diff --git a/engines/alcachofa/console.h b/engines/alcachofa/console.h
index 898605f2e0e..77de4df1744 100644
--- a/engines/alcachofa/console.h
+++ b/engines/alcachofa/console.h
@@ -45,8 +45,8 @@ public:
 
 private:
 	bool _showInteractables = true;
-	bool _showFloor = false;
-	bool _showFloorColor = true;
+	bool _showFloor = true;
+	bool _showFloorColor = false;
 };
 
 } // End of namespace Alcachofa
diff --git a/engines/alcachofa/graphics-opengl.cpp b/engines/alcachofa/graphics-opengl.cpp
index 0899d3f08e9..b1998aa8f39 100644
--- a/engines/alcachofa/graphics-opengl.cpp
+++ b/engines/alcachofa/graphics-opengl.cpp
@@ -290,6 +290,26 @@ public:
 			GL_CALL(glDrawArrays(GL_POINTS, 0, points.size()));
 	}
 
+	virtual void debugPolyline(
+		Span<Vector2d> points,
+		Color color
+	) override {
+		setTexture(nullptr);
+		setBlendMode(BlendMode::Alpha);
+		GL_CALL(glVertexPointer(2, GL_FLOAT, 0, points.data()));
+		GL_CALL(glLineWidth(4.0f));
+		GL_CALL(glPointSize(8.0f));
+
+		GL_CALL(glColor4ub(color.r, color.g, color.b, color.a));
+		if (points.size() > 1)
+			GL_CALL(glDrawArrays(GL_LINE_STRIP, 0, points.size()));
+
+		color.a = (byte)(MIN(255.0f, color.a * 1.3f));
+		GL_CALL(glColor4ub(color.r, color.g, color.b, color.a));
+		if (points.size() > 0)
+			GL_CALL(glDrawArrays(GL_POINTS, 0, points.size()));
+	}
+
 private:
 	void initViewportAndMatrices() {
 		int32 screenWidth = g_system->getWidth();
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 3f5954a9ff9..56ee902df88 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -98,6 +98,10 @@ public:
 		Common::Span<Math::Vector2d> points,
 		Color color = kDebugRed
 	) = 0;
+	virtual void debugPolyline(
+		Common::Span<Math::Vector2d> points,
+		Color color = kDebugRed
+	) = 0;
 
 	virtual void debugShape(
 		const Shape &shape,
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 2c94ae81b0c..f675738b950 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -141,13 +141,34 @@ void Room::update() {
 		world().globalRoom().drawDebug();
 	}
 }
+using namespace Math;
+static Array<Vector2d> path;
+
+static Vector2d asVec(const Point &p) {
+	return Vector2d((float)p.x, (float)p.y);
+}
+
 
 void Room::updateInput() {
+	static bool hasLastP3D = false;
+	static Point lastP3D;
+
+
 	if (g_engine->input().wasMouseLeftPressed()) {
 		Point p2d = g_engine->input().mousePos2D();
 		Point p3d = g_engine->input().mousePos3D();
-		bool contains = _floors[_activeFloorI].contains(p3d);
-		warning("Mouse 2D: %d, %d \t\t3D: %d, %d\t%s", p2d.x, p2d.y, p3d.x, p3d.y, contains ? "yes" : "no");
+
+		if (hasLastP3D) {
+			Stack<Point> pathi;
+			bool result = _floors[0].findPath(lastP3D, p3d, pathi);
+			path.clear();
+			path.push_back(asVec(lastP3D));
+			while (!pathi.empty())
+				path.push_back(asVec(pathi.pop()));
+			warning("Did %sfind a path in %d steps", result ? "" : "not ", path.size());
+		}
+		hasLastP3D = true;
+		lastP3D = p3d;
 	}
 }
 
@@ -177,9 +198,19 @@ void Room::drawDebug() {
 		return;
 	for (auto *object : _objects)
 		object->drawDebug();
+	if (_activeFloorI < 0)
+		return;
 	if (_activeFloorI >= 0 && g_engine->console().showFloor())
 		renderer->debugShape(_floors[_activeFloorI], kDebugBlue);
 
+	renderer->debugPolyline({ path.begin(), path.size()}, kWhite);
+
+	Common::Array<Vector2d> asd;
+	for (auto p : _floors[0]._linkPoints)
+	{
+		auto v = asVec(p);
+		renderer->debugPolyline({ &v, 1 }, { 255, 0, 255, 255 });
+	}
 }
 
 void Room::loadResources() {
diff --git a/engines/alcachofa/shape.cpp b/engines/alcachofa/shape.cpp
index c6e644a9457..dd12ab65ca8 100644
--- a/engines/alcachofa/shape.cpp
+++ b/engines/alcachofa/shape.cpp
@@ -22,8 +22,6 @@
 #include "shape.h"
 #include "stream-helper.h"
 
-#include "math/line2d.h"
-
 using namespace Common;
 using namespace Math;
 
@@ -37,6 +35,31 @@ static int sideOfLine(const Point &a, const Point &b, const Point &q) {
 	return (b.x - a.x) * (q.y - a.y) - (b.y - a.y) * (q.x - a.x);
 }
 
+static bool segmentsIntersect(const Point &a1, const Point &b1, const Point &a2, const Point &b2) {
+	// as there are a number of special cases to consider, this method is a direct translation
+	// of the original engine
+
+	const auto sideOfLine = [](const Point &a, const Point &b, const Point q) {
+		return Alcachofa::sideOfLine(a, b, q) > 0;
+	};
+	const auto lineIntersects = [&](const Point &a1, const Point &b1, const Point &a2, const Point &b2) {
+		return sideOfLine(a1, b1, a2) != sideOfLine(a1, b1, b2);
+	};
+
+	if (a2.x > b2.x) {
+		if (a1.x > b1.x)
+			return lineIntersects(b1, a1, b2, a2) && lineIntersects(b2, a2, b1, a1);
+		else
+			return lineIntersects(a1, b1, b2, a2) && lineIntersects(b2, a2, a1, b1);
+	}
+	else {
+		if (a1.x > b1.x)
+			return lineIntersects(b1, a1, a2, b2) && lineIntersects(a2, b2, b1, a1);
+		else
+			return lineIntersects(a1, b1, a2, b2) && lineIntersects(a2, b2, a1, b1);
+	}
+}
+
 bool Polygon::contains(const Point &query) const {
 	switch (_points.size()) {
 	case 0: return false;
@@ -95,6 +118,21 @@ float PathFindingPolygon::depthAt(const Point &query) const {
 	}
 }
 
+uint PathFindingPolygon::findSharedPoints(
+	const PathFindingPolygon &other,
+	Common::Span<SharedPoint> sharedPoints) const {
+	uint count = 0;
+	for (uint outerI = 0; outerI < _points.size(); outerI++) {
+		for (uint innerI = 0; innerI < other._points.size(); innerI++) {
+			if (_points[outerI] == other._points[innerI]) {
+				assert(count < sharedPoints.size());
+				sharedPoints[count++] = { outerI, innerI };
+			}
+		}
+	}
+	return count;
+}
+
 static Color colorAtForLine(const Point &a, const Point &b, const Point &q, Color colorA, Color colorB) {
 	// I highly suspect RGB calculation being very bugged, so for now I just ignore and only calc alpha
 	float phase = sqrtf(q.sqrDist(a)) / a.sqrDist(b);
@@ -199,6 +237,7 @@ uint Shape::addPolygon(uint maxCount) {
 Polygon Shape::at(uint index) const {
 	auto range = _polygons[index];
 	Polygon p;
+	p._index = index;
 	p._points = Span<const Point>(_points.data() + range.first, range.second);
 	return p;
 }
@@ -236,12 +275,15 @@ PathFindingShape::PathFindingShape(ReadStream &stream) {
 		_pointDepths.resize(_points.size());
 	}
 
-	// TODO: Implement the path finding
+	setupLinks();
+	initializeFloydWarshall();
+	calculateFloydWarshall();
 }
 
 PathFindingPolygon PathFindingShape::at(uint index) const {
 	auto range = _polygons[index];
 	PathFindingPolygon p;
+	p._index = index;
 	p._points = Span<const Point>(_points.data() + range.first, range.second);
 	p._pointDepths = Span<const uint8>(_pointDepths.data() + range.first, range.second);
 	p._order = _polygonOrders[index];
@@ -258,6 +300,238 @@ float PathFindingShape::depthAt(const Point &query) const {
 	return polygon < 0 ? 1.0f : at(polygon).depthAt(query);
 }
 
+PathFindingShape::LinkPolygonIndices::LinkPolygonIndices() {
+	Common::fill(_points, _points + kPointsPerPolygon, LinkIndex( -1, -1 ));
+}
+
+static Pair<int32, int32> orderPoints(const Polygon &polygon, int32 point1, int32 point2) {
+	if ((point1 > point2 && point1 + 1 != (int32)polygon._points.size()) ||
+		point2 + 1 == (int32)polygon._points.size()) {
+		int32 tmp = point1;
+		point1 = point2;
+		point2 = tmp;
+	}
+	return { point1, point2 };
+}
+
+void PathFindingShape::setupLinks() {
+	// just a heuristic, each polygon will be attached to at least one other
+	_linkPoints.reserve(polygonCount() * 3);
+	_linkIndices.resize(polygonCount());
+	_targetQuads.resize(polygonCount() * kPointsPerPolygon);
+	Common::fill(_targetQuads.begin(), _targetQuads.end(), -1);
+	Pair<uint, uint> sharedPoints[2];
+	for (uint outerI = 0; outerI < polygonCount(); outerI++) {
+		const auto outer = at(outerI);
+		for (uint innerI = outerI + 1; innerI < polygonCount(); innerI++) {
+			const auto inner = at(innerI);
+			uint sharedPointCount = outer.findSharedPoints(inner, { sharedPoints, 2 });
+			if (sharedPointCount > 0)
+				setupLinkPoint(outer, inner, sharedPoints[0]);
+			if (sharedPointCount > 1) {
+				auto outerPoints = orderPoints(outer, sharedPoints[0].first, sharedPoints[1].first);
+				auto innerPoints = orderPoints(inner, sharedPoints[0].second, sharedPoints[1].second);
+				setupLinkEdge(outer, inner, outerPoints.first, outerPoints.second, innerPoints.first);
+				setupLinkPoint(outer, inner, sharedPoints[1]);
+			}
+		}
+	}
+}
+
+void PathFindingShape::setupLinkPoint(
+	const PathFindingPolygon &outer,
+	const PathFindingPolygon &inner,
+	PathFindingPolygon::SharedPoint pointI) {
+	auto &outerLink = _linkIndices[outer._index]._points[pointI.first];
+	auto &innerLink = _linkIndices[inner._index]._points[pointI.second];
+	if (outerLink.first < 0) {
+		outerLink.first = _linkPoints.size();
+		_linkPoints.push_back(outer._points[pointI.first]);
+	}
+	innerLink.first = outerLink.first;
+}
+
+void PathFindingShape::setupLinkEdge(
+	const PathFindingPolygon &outer,
+	const PathFindingPolygon &inner,
+	int32 outerP1, int32 outerP2, int32 innerP) {
+	_targetQuads[outer._index * kPointsPerPolygon + outerP1] = inner._index;
+	_targetQuads[inner._index * kPointsPerPolygon + innerP] = outer._index;
+	auto &outerLink = _linkIndices[outer._index]._points[outerP1];
+	auto &innerLink = _linkIndices[inner._index]._points[innerP];
+	if (outerLink.second < 0) {
+		outerLink.second = _linkPoints.size();
+		_linkPoints.push_back((outer._points[outerP1] + outer._points[outerP2]) / 2);
+	}
+	innerLink.second = outerLink.second;
+}
+
+void PathFindingShape::initializeFloydWarshall() {
+	_distanceMatrix.resize(_linkPoints.size() * _linkPoints.size());
+	_previousTarget.resize(_linkPoints.size() * _linkPoints.size());
+	Common::fill(_distanceMatrix.begin(), _distanceMatrix.end(), UINT_MAX);
+	Common::fill(_previousTarget.begin(), _previousTarget.end(), -1);
+
+	// every linkpoint is the shortest path to itself
+	for (uint i = 0; i < _linkPoints.size(); i++) {
+		_distanceMatrix[i * _linkPoints.size() + i] = 0;
+		_previousTarget[i * _linkPoints.size() + i] = i;
+	}
+
+	// every linkpoint to linkpoint within the same polygon *is* the shortest path
+	// between them. Therefore these are our initial paths for Floyd-Warshall
+	for (const auto &linkPolygon : _linkIndices) {
+		for (uint i = 0; i < 2 * kPointsPerPolygon; i++) {
+			LinkIndex linkFrom = linkPolygon._points[i / 2];
+			int32 linkFromI = i % 2 ? linkFrom.second : linkFrom.first;
+			if (linkFromI < 0)
+				continue;
+			for (uint j = i + 1; j < 2 * kPointsPerPolygon; j++) {
+				LinkIndex linkTo = linkPolygon._points[j / 2];
+				int32 linkToI = j % 2 ? linkTo.second : linkTo.first;
+				if (linkToI >= 0) {
+					const int32 linkFromFullI = linkFromI * _linkPoints.size() + linkToI;
+					const int32 linkToFullI = linkToI * _linkPoints.size() + linkFromI;
+					_distanceMatrix[linkFromFullI] = _distanceMatrix[linkToFullI] =
+						(uint)sqrtf(_linkPoints[linkFromI].sqrDist(_linkPoints[linkToI]) + 0.5f);
+					_previousTarget[linkFromFullI] = linkFromI;
+					_previousTarget[linkToFullI] = linkToI;
+				}
+			}
+		}
+	}
+}
+
+void PathFindingShape::calculateFloydWarshall() {
+	const auto distance = [&](uint a, uint b) -> uint& {
+		return _distanceMatrix[a * _linkPoints.size() + b];
+	};
+	const auto previousTarget = [&](uint a, uint b) -> int32& {
+		return _previousTarget[a * _linkPoints.size() + b];
+	};
+	for (uint over = 0; over < _linkPoints.size(); over++) {
+		for (uint from = 0; from < _linkPoints.size(); from++) {
+			for (uint to = 0; to < _linkPoints.size(); to++) {
+				if (distance(from, over) != UINT_MAX && distance(over, to) != UINT_MAX &&
+					distance(from, over) + distance(over, to) < distance(from, to)) {
+					distance(from, to) = distance(from, over) + distance(over, to);
+					previousTarget(from, to) = previousTarget(over, to);
+				}
+			}
+		}
+	}
+
+	// in the game all floors should be fully connected
+	assert(find(_previousTarget.begin(), _previousTarget.end(), -1) == _previousTarget.end());
+}
+
+bool PathFindingShape::findPath(const Point &from, const Point &to_, Stack<Point> &path) const {
+	Point to = to_; // we might want to correct it
+	path.clear();
+
+	int32 fromContaining = polygonContaining(from);
+	if (fromContaining < 0)
+		return false;
+	int32 toContaining = polygonContaining(to);
+	if (toContaining < 0) {
+		to = getClosestPoint(to);
+		toContaining = polygonContaining(to);
+		assert(toContaining >= 0);
+	}
+	//if (canGoStraightThrough(from, to, fromContaining, toContaining)) {
+	if (canGoStraightThrough(from, to, fromContaining, toContaining)) {
+		path.push(to);
+		return true;
+	}
+	floydWarshallPath(from, to, fromContaining, toContaining, path);
+	return true;
+}
+
+bool PathFindingShape::canGoStraightThrough(
+	const Point &from, const Point &to,
+	int32 fromContainingI, int32 toContainingI) const {
+	int32 lastContainingI = -1;
+	while (fromContainingI != toContainingI) {
+		auto toContaining = at(toContainingI);
+		bool foundPortal = false;
+		for (uint i = 0; i < toContaining._points.size(); i++) {
+			uint fullI = toContainingI * kPointsPerPolygon + i;
+			if (_targetQuads[fullI] < 0 || _targetQuads[fullI] == lastContainingI)
+				continue;
+
+			uint j = i + 1 == toContaining._points.size() ? 0 : i + 1;
+			if (segmentsIntersect(from, to, toContaining._points[i], toContaining._points[j])) {
+				foundPortal = true;
+				lastContainingI = toContainingI;
+				toContainingI = _targetQuads[fullI];
+				break;
+			}
+		}
+		if (!foundPortal)
+			return false;
+	}
+	return true;
+}
+
+void PathFindingShape::floydWarshallPath(
+	const Point &from, const Point &to,
+	int32 fromContaining, int32 toContaining,
+	Stack<Point> &path) const {
+	path.push(to);
+	// first find the tuple of link points to be used
+	uint fromLink = UINT_MAX, toLink = UINT_MAX, bestDistance = UINT_MAX;
+	const auto &fromIndices = _linkIndices[fromContaining];
+	const auto &toIndices = _linkIndices[toContaining];
+	for (uint i = 0; i < 2 * kPointsPerPolygon; i++) {
+		const auto &curFromPoint = fromIndices._points[i / 2];
+		int32 curFromLink = i % 2 ? curFromPoint.second : curFromPoint.first;
+		if (curFromLink < 0)
+			continue;
+		uint curFromDistance = (uint)sqrtf(from.sqrDist(_linkPoints[curFromLink]) + 0.5f);
+
+		for (uint j = 0; j < 2 * kPointsPerPolygon; j++) {
+			const auto &curToPoint = toIndices._points[j / 2];
+			int32 curToLink = j % 2 ? curToPoint.second : curToPoint.first;
+			if (curToLink < 0)
+				continue;
+			uint totalDistance =
+				curFromDistance +
+				_distanceMatrix[curFromLink * _linkPoints.size() + curToLink] +
+				(uint)sqrtf(to.sqrDist(_linkPoints[curToLink]) + 0.5f);
+			if (totalDistance < bestDistance) {
+				bestDistance = totalDistance;
+				fromLink = curFromLink;
+				toLink = curToLink;
+			}
+		}
+	}
+	assert(fromLink != UINT_MAX && toLink != UINT_MAX);
+
+	// then walk the matrix back to reconstruct the path
+	while (fromLink != toLink) {
+		path.push(_linkPoints[toLink]);
+		toLink = _previousTarget[fromLink * _linkPoints.size() + toLink];
+		assert(toLink < _linkPoints.size());
+	}
+	path.push(_linkPoints[fromLink]);
+}
+
+Point PathFindingShape::getClosestPoint(const Point &query) const {
+	assert(!_points.empty());
+	Point bestPoint;
+	uint bestDistance = UINT_MAX;
+	for (auto p : _points) {
+		uint curDistance = query.sqrDist(p);
+		if (curDistance < bestDistance) {
+			bestDistance = curDistance;
+			bestPoint = p;
+		}
+	}
+
+	assert(bestDistance < UINT_MAX);
+	return bestPoint;
+}
+
 FloorColorShape::FloorColorShape() {}
 
 FloorColorShape::FloorColorShape(ReadStream &stream) {
@@ -290,6 +564,7 @@ FloorColorShape::FloorColorShape(ReadStream &stream) {
 FloorColorPolygon FloorColorShape::at(uint index) const {
 	auto range = _polygons[index];
 	FloorColorPolygon p;
+	p._index = index;
 	p._points = Span<const Point>(_points.data() + range.first, range.second);
 	p._pointColors = Span<const Color>(_pointColors.data() + range.first, range.second);
 	return p;
diff --git a/engines/alcachofa/shape.h b/engines/alcachofa/shape.h
index a74335763ca..ca527fc8c17 100644
--- a/engines/alcachofa/shape.h
+++ b/engines/alcachofa/shape.h
@@ -24,6 +24,7 @@
 
 #include "common/stream.h"
 #include "common/array.h"
+#include "common/stack.h"
 #include "common/rect.h"
 #include "common/span.h"
 #include "common/util.h"
@@ -40,6 +41,7 @@ struct EdgeDistances {
 };
 
 struct Polygon {
+	uint _index;
 	Common::Span<const Common::Point> _points;
 
 	bool contains(const Common::Point &query) const;
@@ -50,7 +52,10 @@ struct PathFindingPolygon : Polygon {
 	Common::Span<const uint8> _pointDepths;
 	int8 _order;
 
+	using SharedPoint = Common::Pair<uint, uint>;
+
 	float depthAt(const Common::Point &query) const;
+	uint findSharedPoints(const PathFindingPolygon &other, Common::Span<SharedPoint> sharedPoints) const;
 };
 
 struct FloorColorPolygon : Polygon {
@@ -128,15 +133,13 @@ protected:
 
 /**
  * @brief Path finding is based on the Shape class with the invariant that
- * every polygon is a convex quad.
- * Equal points of different quads link them together, for edges we add an
- * additional link point in the center of the edge.
+ * every polygon is a convex polygon with at most four points.
+ * Equal points of different quads link them together, for shared edges we
+ * add an additional link point in the center of the edge.
  *
  * The resulting graph is processed using Floyd-Warshall to precalculate for
  * the actual path finding. Additionally we check whether a character can
  * walk straight through an edge instead of following the link points.
- *
- * None of this is implemented yet by the way ;)
  */
 class PathFindingShape final : public Shape {
 public:
@@ -152,10 +155,63 @@ public:
 	PathFindingPolygon at(uint index) const;
 	int8 orderAt(const Common::Point &query) const;
 	float depthAt(const Common::Point &query) const;
+	bool findPath(
+		const Common::Point &from,
+		const Common::Point &to,
+		Common::Stack<Common::Point> &path) const;
+	Common::Point getClosestPoint(const Common::Point &query) const;
+
+private: //ASDJALSKDJALKXDJALSKDJALSKDJALKjs
+public:
+	void setupLinks();
+	void setupLinkPoint(
+		const PathFindingPolygon &outer,
+		const PathFindingPolygon &inner,
+		PathFindingPolygon::SharedPoint pointI);
+	void setupLinkEdge(
+		const PathFindingPolygon &outer,
+		const PathFindingPolygon &inner,
+		int32 outerP1, int32 outerP2, int32 innerP);
+	void initializeFloydWarshall();
+	void calculateFloydWarshall();
+	bool canGoStraightThrough(
+		const Common::Point &from,
+		const Common::Point &to,
+		int32 fromContaining, int32 toContaining) const;
+	void floydWarshallPath(
+		const Common::Point &from,
+		const Common::Point &to,
+		int32 fromContaining, int32 toContaining,
+		Common::Stack<Common::Point> &path) const;
 
-private:
 	Common::Array<uint8> _pointDepths;
 	Common::Array<int8> _polygonOrders;
+
+	/**
+	 * These are the edges in the graph, they are either points
+	 * that are shared by two polygons or artificial points in
+	 * the center of a shared edge
+	 */
+	Common::Array<Common::Point> _linkPoints;
+	/**
+	 * For each point of each polygon the index (or -1) to 
+	 * the corresponding link point. The second point is the
+	 * index to the artifical center point
+	 */
+	using LinkIndex = Common::Pair<int32, int32>;
+	struct LinkPolygonIndices {
+		LinkPolygonIndices();
+		LinkIndex _points[kPointsPerPolygon];
+	};
+	Common::Array<LinkPolygonIndices> _linkIndices;
+	/**
+	 * For the going-straight-through-edges check we need 
+	 * to know for each shared edge (defined by the starting point)
+	 * into which quad we will walk.
+	 */
+	Common::Array<int32> _targetQuads;
+	Common::Array<uint> _distanceMatrix; ///< for Floyd-Warshall
+	Common::Array<int32> _previousTarget; ///< for Floyd-Warshall
 };
 
 using OptionalColor = Common::Pair<bool, Color>;


Commit: 9fd0d742428835fabf0869f0c898d9ee38db0777
    https://github.com/scummvm/scummvm/commit/9fd0d742428835fabf0869f0c898d9ee38db0777
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:45+02:00

Commit Message:
ALCACHOFA: Add drawing characters

Changed paths:
  A engines/alcachofa/player.h
    engines/alcachofa/Input.cpp
    engines/alcachofa/Input.h
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/alcachofa.h
    engines/alcachofa/console.cpp
    engines/alcachofa/console.h
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/general-objects.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/objects.h
    engines/alcachofa/rooms.cpp
    engines/alcachofa/rooms.h


diff --git a/engines/alcachofa/Input.cpp b/engines/alcachofa/Input.cpp
index e227fc59492..86c9181b86f 100644
--- a/engines/alcachofa/Input.cpp
+++ b/engines/alcachofa/Input.cpp
@@ -29,6 +29,8 @@ namespace Alcachofa {
 void Input::nextFrame() {
 	_wasMouseLeftPressed = false;
 	_wasMouseRightPressed = false;
+	_wasMouseLeftReleased = false;
+	_wasMouseRightReleased = false;
 }
 
 bool Input::handleEvent(const Common::Event &event) {
@@ -38,6 +40,7 @@ bool Input::handleEvent(const Common::Event &event) {
 		_isMouseLeftDown = true;
 		return true;
 	case EVENT_LBUTTONUP:
+		_wasMouseLeftReleased = true;
 		_isMouseLeftDown = false;
 		return true;
 	case EVENT_RBUTTONDOWN:
@@ -45,6 +48,7 @@ bool Input::handleEvent(const Common::Event &event) {
 		_isMouseRightDown = true;
 		return true;
 	case EVENT_RBUTTONUP:
+		_wasMouseRightReleased = true;
 		_isMouseRightDown = false;
 		return true;
 	case EVENT_MOUSEMOVE: {
diff --git a/engines/alcachofa/Input.h b/engines/alcachofa/Input.h
index a648d72b848..ee6028ad355 100644
--- a/engines/alcachofa/Input.h
+++ b/engines/alcachofa/Input.h
@@ -30,6 +30,10 @@ class Input {
 public:
 	inline bool wasMouseLeftPressed() const { return _wasMouseLeftPressed; }
 	inline bool wasMouseRightPressed() const { return _wasMouseRightPressed; }
+	inline bool wasAnyMousePressed() const { return _wasMouseLeftPressed || _wasMouseRightPressed; }
+	inline bool wasMouseLeftReleased() const { return _wasMouseLeftReleased; }
+	inline bool wasMouseRightReleased() const { return _wasMouseRightReleased; }
+	inline bool wasAnyMouseReleased() const { return _wasMouseLeftReleased || _wasMouseRightReleased; }
 	inline bool isMouseLeftDown() const { return _isMouseLeftDown; }
 	inline bool isMouseRightDown() const { return _isMouseRightDown; }
 	inline const Common::Point &mousePos2D() const { return _mousePos2D; }
@@ -42,6 +46,8 @@ private:
 	bool
 		_wasMouseLeftPressed,
 		_wasMouseRightPressed,
+		_wasMouseLeftReleased,
+		_wasMouseRightReleased,
 		_isMouseLeftDown,
 		_isMouseRightDown;
 	Common::Point
diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 76d3c998b7d..25363ded13f 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -62,9 +62,9 @@ Common::Error AlcachofaEngine::run() {
 	_drawQueue.reset(new DrawQueue(_renderer.get()));
 	_world.reset(new World());
 
-	world().globalRoom().loadResources();
+	//world().globalRoom().loadResources();
 
-	auto room = world().getRoomByName("CASA_FREDDY_ARRIBA");
+	auto room = world().getRoomByName("SALOON");
 	assert(room != nullptr);
 	world().currentRoom() = room;
 	room->loadResources();
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index 3a183acd868..913da07cbea 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -37,6 +37,7 @@
 #include "alcachofa/detection.h"
 #include "alcachofa/camera.h"
 #include "alcachofa/input.h"
+#include "alcachofa/player.h"
 #include "alcachofa/console.h"
 
 namespace Alcachofa {
@@ -61,6 +62,7 @@ public:
 	inline DrawQueue &drawQueue() { return *_drawQueue; }
 	inline Camera &camera() { return _camera; }
 	inline Input &input() { return _input; }
+	inline Player &player() { return _player; }
 	inline World &world() { return *_world; }
 	inline Console &console() { return *_console; }
 
@@ -114,6 +116,7 @@ private:
 	Common::ScopedPtr<World> _world;
 	Camera _camera;
 	Input _input;
+	Player _player;
 };
 
 extern AlcachofaEngine *g_engine;
diff --git a/engines/alcachofa/console.cpp b/engines/alcachofa/console.cpp
index aed5c63bded..2a50d20e85e 100644
--- a/engines/alcachofa/console.cpp
+++ b/engines/alcachofa/console.cpp
@@ -25,6 +25,7 @@ namespace Alcachofa {
 
 Console::Console() : GUI::Debugger() {
 	registerVar("showInteractables", &_showInteractables);
+	registerVar("showCharacters", &_showCharacters);
 	registerVar("showFloor", &_showFloor);
 	registerVar("showFloorColor", &_showFloorColor);
 }
diff --git a/engines/alcachofa/console.h b/engines/alcachofa/console.h
index 77de4df1744..cc250e38524 100644
--- a/engines/alcachofa/console.h
+++ b/engines/alcachofa/console.h
@@ -33,18 +33,21 @@ public:
 	~Console() override;
 
 	inline bool showInteractables() const { return _showInteractables; }
+	inline bool showCharacters() const { return _showCharacters; }
 	inline bool showFloor() const { return _showFloor; }
 	inline bool showFloorColor() const { return _showFloorColor; }
 
 	inline bool isAnyDebugDrawingOn() const {
 		return
 			_showInteractables ||
+			_showCharacters ||
 			_showFloor ||
 			_showFloorColor;
 	}
 
 private:
 	bool _showInteractables = true;
+	bool _showCharacters = true;
 	bool _showFloor = true;
 	bool _showFloorColor = false;
 };
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 3b3085d79f3..0d724001809 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -68,6 +68,69 @@ Character::Character(Room *room, ReadStream &stream)
 	_order = _graphicNormal.order();
 }
 
+static Graphic *graphicOf(ObjectBase *object, Graphic *fallback = nullptr) {
+	auto objectGraphic = object == nullptr ? nullptr : object->graphic();
+	return objectGraphic == nullptr ? fallback : objectGraphic;
+}
+
+void Character::update() {
+	if (!isEnabled())
+		return;
+	updateSelection();
+
+	Graphic *animateGraphic = graphicOf(_curAnimateObject);
+	if (animateGraphic != nullptr) {
+		animateGraphic->center() = Point(0, 0);
+		animateGraphic->update();
+	}
+	else if (_isTalking)
+		updateTalkingAnimation();
+	else if (g_engine->world().somebodyUsing(this)) {
+		Graphic *talkGraphic = graphicOf(_curTalkingObject, &_graphicTalking);
+		talkGraphic->start(true);
+		talkGraphic->pause();
+		talkGraphic->update();
+	}
+	else
+		_graphicNormal.update();
+}
+
+void Character::updateTalkingAnimation() {
+	Graphic *talkGraphic = graphicOf(_curTalkingObject, &_graphicTalking);
+	if (!_isTalking) {
+		talkGraphic->reset();
+		return;
+	}
+	// TODO: Add lip-sync(?) animation behavior
+	talkGraphic->update();
+}
+
+void Character::draw() {
+	if (!isEnabled())
+		return;
+	Graphic *activeGraphic = graphic();
+	assert(activeGraphic != nullptr);
+	g_engine->drawQueue().add<AnimationDrawRequest>(*activeGraphic, true, BlendMode::AdditiveAlpha, _lodBias);
+}
+
+void Character::drawDebug() {
+	auto renderer = dynamic_cast<IDebugRenderer *>(&g_engine->renderer());
+	if (!g_engine->console().showCharacters() || renderer == nullptr || !isEnabled())
+		return;
+
+	renderer->debugShape(*shape());
+}
+
+void Character::loadResources() {
+	_graphicNormal.loadResources();
+	_graphicTalking.loadResources();
+}
+
+void Character::freeResources() {
+	_graphicNormal.freeResources();
+	_graphicTalking.freeResources();
+}
+
 void Character::serializeSave(Serializer &serializer) {
 	ShapeObject::serializeSave(serializer);
 	serializer.syncAsByte(_isTalking);
@@ -79,6 +142,15 @@ void Character::serializeSave(Serializer &serializer) {
 	serializer.syncAsFloatLE(_lodBias);
 }
 
+Graphic *Character::graphic() {
+	Graphic *activeGraphic = graphicOf(_curAnimateObject);
+	if (activeGraphic == nullptr && (_isTalking || g_engine->world().somebodyUsing(this)))
+		activeGraphic = graphicOf(_curTalkingObject, &_graphicTalking);
+	if (activeGraphic == nullptr)
+		activeGraphic = &_graphicNormal;
+	return activeGraphic;
+}
+
 void Character::syncObjectAsString(Serializer &serializer, ObjectBase *&object) {
 	String name;
 	if (serializer.isSaving() && object != nullptr)
diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index c0ae21dbb84..f16fc2c56ed 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -154,6 +154,15 @@ ShapeObject::ShapeObject(Room *room, ReadStream &stream)
 	, _cursorType((CursorType)stream.readSint32LE()) {
 }
 
+void ShapeObject::update() {
+	if (isEnabled())
+		updateSelection();
+	else {
+		_isSelected = false;
+		_wasSelected = false;
+	}
+}
+
 void ShapeObject::serializeSave(Serializer &serializer) {
 	serializer.syncAsSByte(_order);
 }
@@ -166,6 +175,45 @@ CursorType ShapeObject::cursorType() const {
 	return _cursorType;
 }
 
+void ShapeObject::onHoverStart() {
+	onHoverUpdate();
+}
+
+void ShapeObject::onHoverEnd() {
+}
+
+void ShapeObject::onHoverUpdate() {
+	// TODO: Add text request for name
+}
+
+void ShapeObject::onClick() {
+	onHoverUpdate();
+}
+
+void ShapeObject::markSelected() {
+	_isSelected = true;
+}
+
+void ShapeObject::updateSelection() {
+	if (_isSelected) {
+		_isSelected = false;
+		if (_wasSelected) {
+			if (g_engine->input().wasAnyMouseReleased() && g_engine->player().selectedObject() == this)
+				onClick();
+			else
+				onHoverUpdate();
+		}
+		else {
+			_wasSelected = true;
+			onHoverStart();
+		}
+	}
+	else if (_wasSelected) {
+		_wasSelected = false;
+		onHoverEnd();
+	}
+}
+
 PhysicalObject::PhysicalObject(Room *room, ReadStream &stream)
 	: ShapeObject(room, stream) {
 	_order = stream.readSByte();
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index ecded3d37c4..28e08c12776 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -253,7 +253,7 @@ static void fullBlend(const ManagedSurface &source, ManagedSurface &destination,
 	assert(offsetY >= 0 && offsetY + source.h <= destination.h);
 
 	const byte *sourceLine = (byte *)source.getPixels();
-	byte *destinationLine = (byte *)destination.getPixels() + offsetY * source.pitch + offsetX * 4;
+	byte *destinationLine = (byte *)destination.getPixels() + offsetY * destination.pitch + offsetX * 4;
 	for (int y = 0; y < source.h; y++) {
 		const byte *sourcePixel = sourceLine;
 		byte *destPixel = destinationLine;
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 37d4de22251..4ce0412146e 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -116,17 +116,26 @@ public:
 	ShapeObject(Room *room, Common::ReadStream &stream);
 	virtual ~ShapeObject() override = default;
 
+	virtual void update() override;
 	virtual void serializeSave(Common::Serializer &serializer) override;
 	virtual Shape *shape() override;
 	virtual CursorType cursorType() const;
-
-private:
-	Shape _shape;
-	CursorType _cursorType;
+	virtual void onHoverStart();
+	virtual void onHoverEnd();
+	virtual void onHoverUpdate();
+	virtual void onClick();
+	void markSelected();
 
 protected:
+	void updateSelection();
+
 	// original inconsistency: base class has member that is read by the sub classes
 	int8 _order = 0;
+private:
+	Shape _shape;
+	CursorType _cursorType;
+	bool _isSelected = false,
+		_wasSelected = false;
 };
 
 class PhysicalObject : public ShapeObject {
@@ -302,10 +311,17 @@ public:
 	Character(Room *room, Common::ReadStream &stream);
 	virtual ~Character() override = default;
 
+	virtual void update() override;
+	virtual void draw() override;
+	virtual void drawDebug() override;
+	virtual void loadResources() override;
+	virtual void freeResources() override;
 	virtual void serializeSave(Common::Serializer &serializer) override;
+	virtual Graphic *graphic() override;
 
 protected:
 	void syncObjectAsString(Common::Serializer &serializer, ObjectBase *&object);
+	void updateTalkingAnimation();
 
 private:
 	Common::Point _interactionPoint;
@@ -365,6 +381,7 @@ public:
 	virtual ~MainCharacter() override;
 
 	inline MainCharacterKind kind() const { return _kind; }
+	inline ObjectBase *currentlyUsing() const { return _currentlyUsingObject; }
 
 	virtual void serializeSave(Common::Serializer &serializer) override;
 
diff --git a/engines/alcachofa/player.h b/engines/alcachofa/player.h
new file mode 100644
index 00000000000..b0abfa21f7e
--- /dev/null
+++ b/engines/alcachofa/player.h
@@ -0,0 +1,39 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef PLAYER_H
+#define PLAYER_H
+
+namespace Alcachofa {
+
+class ShapeObject;
+
+class Player {
+public:
+    inline ShapeObject *selectedObject() { return _selectedObject; }
+
+private:
+    ShapeObject *_selectedObject = nullptr;
+};
+
+}
+
+#endif // PLAYER_H
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index f675738b950..e35fa824ac7 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -134,7 +134,7 @@ void Room::update() {
 	if (world().currentRoom() == this) {
 		g_engine->camera().update();
 		drawObjects();
-		world().globalRoom().drawObjects();
+		// TODO: world().globalRoom().drawObjects();
 		// TODO: Draw black borders
 		g_engine->drawQueue().draw();
 		drawDebug();
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index 7678552c69d..e50c10ceb8f 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -124,6 +124,11 @@ public:
 	inline Room *&currentRoom() { return _currentRoom; }
 	inline Room *currentRoom() const { return _currentRoom; }
 
+	inline bool somebodyUsing(ObjectBase *object) const {
+		return filemon().currentlyUsing() == object ||
+			mortadelo().currentlyUsing() == object;
+	}
+
 	MainCharacter &getMainCharacterByKind(MainCharacterKind kind) const;
 	Room *getRoomByName(const Common::String &name) const;
 	ObjectBase *getObjectByName(const Common::String &name) const;


Commit: bc3380d4aca21b4cbc22af6574368f5d8e3d4570
    https://github.com/scummvm/scummvm/commit/bc3380d4aca21b4cbc22af6574368f5d8e3d4570
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:45+02:00

Commit Message:
ALCACHOFA: Add walking characters

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/common.h
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/graphics.h
    engines/alcachofa/objects.h
    engines/alcachofa/rooms.cpp
    engines/alcachofa/rooms.h
    engines/alcachofa/shape.cpp
    engines/alcachofa/shape.h
    engines/alcachofa/stream-helper.h


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 25363ded13f..a536098f100 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -62,7 +62,7 @@ Common::Error AlcachofaEngine::run() {
 	_drawQueue.reset(new DrawQueue(_renderer.get()));
 	_world.reset(new World());
 
-	//world().globalRoom().loadResources();
+	world().globalRoom().loadResources();
 
 	auto room = world().getRoomByName("SALOON");
 	assert(room != nullptr);
@@ -74,11 +74,11 @@ Common::Error AlcachofaEngine::run() {
 	if (saveSlot != -1)
 		(void)loadGameState(saveSlot);
 
-	g_system->showMouse(true);
 
 	Common::Event e;
 	Graphics::FrameLimiter limiter(g_system, 60);
 	while (!shouldQuit()) {
+		g_system->showMouse(true);
 		_input.nextFrame();
 		while (g_system->getEventManager()->pollEvent(e)) {
 			if (_input.handleEvent(e))
diff --git a/engines/alcachofa/common.h b/engines/alcachofa/common.h
index 8f9669d5ea2..92c8c636704 100644
--- a/engines/alcachofa/common.h
+++ b/engines/alcachofa/common.h
@@ -37,9 +37,11 @@ enum class CursorType {
 
 enum class Direction {
 	Up,
+	Right,
 	Down,
 	Left,
-	Right
+
+	Invalid = -1
 };
 
 constexpr const int32 kDirectionCount = 4;
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 0d724001809..c44ea36f1f2 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -65,6 +65,7 @@ Character::Character(Room *room, ReadStream &stream)
 	, _graphicNormal(stream)
 	, _graphicTalking(stream) {
 	_graphicNormal.start(true);
+	_graphicNormal.frameI() = _graphicTalking.frameI() = 0;
 	_order = _graphicNormal.order();
 }
 
@@ -180,20 +181,266 @@ WalkingCharacter::WalkingCharacter(Room *room, ReadStream &stream)
 	}
 	for (int32 i = 0; i < kDirectionCount; i++) {
 		auto fileName = readVarString(stream);
-		_standingAnimations[i].reset(new Animation(Common::move(fileName)));
+		_talkingAnimations[i].reset(new Animation(Common::move(fileName)));
+	}
+}
+
+void WalkingCharacter::update() {
+	Character::update();
+	if (!isEnabled())
+		return;
+	updateWalking();
+
+	auto activeFloor = room()->activeFloor();
+	if (activeFloor != nullptr) {
+		if (activeFloor->polygonContaining(_sourcePos) < 0)
+			_sourcePos = _currentPos = activeFloor->getClosestPoint(_sourcePos);
+		if (activeFloor->polygonContaining(_currentPos) < 0)
+			_currentPos = activeFloor->getClosestPoint(_currentPos);
+	}
+
+	if (!_isWalking) {
+		_graphicTalking.setAnimation(talkingAnimation());
+		updateTalkingAnimation();
+		_currentPos = _sourcePos;
+	}
+
+	_graphicNormal.center() = _graphicTalking.center() = _currentPos;
+	auto animateGraphic = graphicOf(_curAnimateObject);
+	auto talkingGraphic = graphicOf(_curTalkingObject);
+	if (animateGraphic != nullptr)
+		animateGraphic->center() = _currentPos;
+	if (talkingGraphic != nullptr)
+		talkingGraphic->center() = _currentPos;
+	if (room() != &g_engine->world().globalRoom()) {
+		float depth = room()->depthAt(_currentPos);
+		int8 order = room()->orderAt(_currentPos);
+		_graphicNormal.order() = _graphicTalking.order() = order;
+		_graphicNormal.depthScale() = _graphicTalking.depthScale() = depth;
+		if (animateGraphic != nullptr) {
+			animateGraphic->order() = order;
+			animateGraphic->depthScale() = depth;
+		}
+		if (talkingGraphic != nullptr) {
+			talkingGraphic->order() = order;
+			talkingGraphic->depthScale() = depth;
+		}
+	}
+
+	_interactionPoint = _currentPos;
+	_interactionDirection1 = Direction::Right;
+	if (this != g_engine->world().activeCharacter()) {
+		int16 interactionOffset = (int16)(150 * _graphicNormal.depthScale());
+		_interactionPoint.x -= interactionOffset;
+		if (activeFloor != nullptr && activeFloor->polygonContaining(_interactionPoint) < 0) {
+			_interactionPoint.x = _currentPos.x + interactionOffset;
+			_interactionDirection1 = Direction::Left;
+		}
+	}
+}
+
+static Direction getDirection(const Point &from, const Point &to) {
+	Point delta = from - to;
+	if (from.x == to.x)
+		return from.y < to.y ? Direction::Up : Direction::Down;
+	else if (from.x < to.x) {
+		int slope = 1000 * delta.y / -delta.x;
+		return slope > 1000 ? Direction::Up
+			: slope < -1000 ? Direction::Down
+			: Direction::Right;
+	}
+	else { // from.x > to.x
+		int slope = 1000 * delta.y / delta.x;
+		return slope > 1000 ? Direction::Up
+			: slope < -1000 ? Direction::Down
+			: Direction::Left;
+	}
+}
+
+void WalkingCharacter::updateWalking() {
+	if (!_isWalking)
+		return;
+	static constexpr float kHigherStepSizeThreshold = 0x4CCC / 65535.0f;
+	static constexpr float kMinStepSizeFactor = 0x3333 / 65535.0f;
+	_stepSizeFactor = _graphicNormal.depthScale();
+	if (_stepSizeFactor < kHigherStepSizeThreshold)
+		_stepSizeFactor = _stepSizeFactor / 3.0f + kMinStepSizeFactor;
+
+	Point targetPos = _pathPoints.top();
+	if (_sourcePos == targetPos) {
+		_currentPos = targetPos;
+		_pathPoints.pop();
+	}
+	else {
+		updateWalkingAnimation();
+		const int32 distanceToTarget = (int32)(sqrtf(_sourcePos.sqrDist(targetPos)));
+		if (_walkedDistance < distanceToTarget) {
+			// separated because having only 16 bits and multiplications seems dangerous
+			_currentPos.x = _sourcePos.x + _walkedDistance * (targetPos.x - _sourcePos.x) / distanceToTarget;
+			_currentPos.y = _sourcePos.y + _walkedDistance * (targetPos.y - _sourcePos.y) / distanceToTarget;
+		}
+		else {
+			_sourcePos = _currentPos = targetPos;
+			_pathPoints.pop();
+			_walkedDistance = 1;
+			_lastWalkAnimFrame = 0;
+		}
+	}
+	
+	if (_pathPoints.empty()) {
+		_isWalking = false;
+		_currentPos = _sourcePos = targetPos;
+		if (_endWalkingDirection != Direction::Invalid)
+			_direction = _endWalkingDirection;
+		onArrived();
+	}
+	_graphicNormal.center() = _currentPos;
+}
+
+void WalkingCharacter::updateWalkingAnimation()
+{
+	_direction = getDirection(_sourcePos, _pathPoints.top());
+	auto animation = walkingAnimation();
+	_graphicNormal.setAnimation(animation);
+
+	// this is very confusing. Let's see what it does
+	const int32 halfFrameCount = (int32)animation->frameCount() / 2;
+	int32 expectedFrame = (int32)(g_system->getMillis() - _graphicNormal.lastTime()) * 12 / 1000;
+	const bool isUnexpectedFrame = expectedFrame != _lastWalkAnimFrame;
+	int32 stepFrameFrom, stepFrameTo;
+	if (expectedFrame < halfFrameCount - 1) {
+		_lastWalkAnimFrame = expectedFrame;
+		stepFrameFrom = 2 * expectedFrame - 2;
+		stepFrameTo = 2 * expectedFrame;
+	}
+	else {
+		const int32 frameThreshold = _lastWalkAnimFrame <= halfFrameCount - 1
+			? _lastWalkAnimFrame
+			: (_lastWalkAnimFrame - halfFrameCount + 1) % (halfFrameCount - 2) + 1;
+		_lastWalkAnimFrame = expectedFrame;
+		expectedFrame = (expectedFrame - halfFrameCount + 1) % (halfFrameCount - 2) + 1;
+		if (expectedFrame >= frameThreshold) {
+			stepFrameFrom = 2 * expectedFrame - 2;
+			stepFrameTo = 2 * expectedFrame;
+		}
+		else {
+			stepFrameFrom = 2 * halfFrameCount - 4;
+			stepFrameTo = 2 * halfFrameCount - 2;
+		}
+	}
+	if (isUnexpectedFrame) {
+		const uint stepSize = (uint)sqrtf(animation->frameCenter(stepFrameFrom).sqrDist(animation->frameCenter(stepFrameTo)));
+		_walkedDistance += (int32)(stepSize * _stepSizeFactor);
+	}
+	_graphicNormal.frameI() = 2 * expectedFrame; // especially this: wtf?
+}
+
+void WalkingCharacter::onArrived() {
+}
+
+void WalkingCharacter::stopWalkingAndTurn(Direction direction) {
+	_isWalking = false;
+	_direction = direction;
+}
+
+void WalkingCharacter::walkTo(
+	const Point &target, Direction endDirection,
+	ShapeObject *activateObject, const char *activateAction,
+	bool useAlternateObjectDirection) {
+	// all the activation parameters are only relevant for MainCharacter
+
+	if (_isWalking)
+		_sourcePos = _currentPos;
+	else {
+		_lastWalkAnimFrame = 0;
+		int32 prevWalkFrame = _graphicNormal.frameI();
+		_graphicNormal.reset();
+		_graphicNormal.frameI() = prevWalkFrame;
+	}
+
+	_pathPoints.clear();
+	auto floor = room()->activeFloor();
+	if (floor != nullptr)
+		floor->findPath(_sourcePos, target, _pathPoints);
+	if (_pathPoints.empty()) {
+		_isWalking = false;
+		onArrived();
+		return;
+	}
+
+	_isWalking = true;
+	_endWalkingDirection = endDirection;
+	_walkedDistance = 0;
+	updateWalking();
+}
+
+void WalkingCharacter::setPosition(const Point &target) {
+	_isWalking = false;
+	_sourcePos = _currentPos = target;
+}
+
+void WalkingCharacter::draw() {
+	if (!isEnabled())
+		return;
+
+	Graphic *currentGraphic = graphicOf(_curAnimateObject);
+	if (currentGraphic == nullptr && _isWalking)
+		currentGraphic = &_graphicNormal;
+	if (currentGraphic == nullptr && g_engine->world().somebodyUsing(this)) {
+		currentGraphic = graphicOf(_curTalkingObject, &_graphicTalking);
+		currentGraphic->start(true);
+		currentGraphic->pause();
+	}
+	if (currentGraphic == nullptr) {
+		// TODO: draw dialog line
+		currentGraphic = graphicOf(_curTalkingObject, &_graphicTalking);
+	}
+
+	assert(currentGraphic != nullptr);
+	g_engine->drawQueue().add<AnimationDrawRequest>(*currentGraphic, true, BlendMode::AdditiveAlpha);
+}
+
+void WalkingCharacter::drawDebug() {
+	Character::drawDebug();
+	auto renderer = dynamic_cast<IDebugRenderer *>(&g_engine->renderer());
+	if (!g_engine->console().showCharacters() || renderer == nullptr || !isEnabled() || _pathPoints.empty())
+		return;
+
+	Array<Vector2d> points2D(_pathPoints.size() + 1);
+	_pathPoints.push(_sourcePos);
+	for (uint i = 0; i < _pathPoints.size(); i++) {
+		auto v = g_engine->camera().transform3Dto2D({ (float)_pathPoints[i].x, (float)_pathPoints[i].y, kBaseScale });
+		points2D[i] = { v.x(), v.y() };
+	}
+	_pathPoints.pop();
+	renderer->debugPolyline({ points2D.data(), points2D.size() }, kWhite);
+}
+
+void WalkingCharacter::loadResources() {
+	Character::loadResources();
+	for (int i = 0; i < kDirectionCount; i++) {
+		_walkingAnimations[i]->load();
+		_talkingAnimations[i]->load();
+	}
+}
+
+void WalkingCharacter::freeResources() {
+	Character::freeResources();
+	for (int i = 0; i < kDirectionCount; i++) {
+		_walkingAnimations[i]->freeImages();
+		_talkingAnimations[i]->freeImages();
 	}
 }
 
 void WalkingCharacter::serializeSave(Serializer &serializer) {
 	Character::serializeSave(serializer);
 	serializer.syncAsSint32LE(_lastWalkAnimFrame);
-	serializer.syncAsSint32LE(_walkSpeed);
+	serializer.syncAsSint32LE(_walkedDistance);
 	syncPoint(serializer, _sourcePos);
-	syncPoint(serializer, _targetPos);
+	syncPoint(serializer, _currentPos);
 	serializer.syncAsByte(_isWalking);
-	syncArray(serializer, _pathPoints, syncPoint);
+	syncStack(serializer, _pathPoints, syncPoint);
 	syncEnum(serializer, _direction);
-	_graphicWalking.serializeSave(serializer);
 }
 
 MainCharacter::MainCharacter(Room *room, ReadStream &stream)
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 28e08c12776..60af943f46a 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -369,16 +369,18 @@ Graphic::Graphic(ReadStream &stream) {
 	_scale = stream.readSint16LE();
 	_order = stream.readSByte();
 	auto animationName = readVarString(stream);
-	_animation.reset(new Animation(std::move(animationName)));
+	if (!animationName.empty())
+		setAnimation(animationName, AnimationFolder::Animations);
 }
 
 void Graphic::loadResources() {
-	assert(_animation != nullptr);
-	_animation->load();
+	if (_animation != nullptr)
+		_animation->load();
 }
 
 void Graphic::freeResources() {
-	_animation.reset();
+	_ownedAnimation.reset();
+	_animation = nullptr;
 }
 
 void Graphic::update() {
@@ -420,7 +422,12 @@ void Graphic::reset() {
 }
 
 void Graphic::setAnimation(const Common::String &fileName, AnimationFolder folder) {
-	_animation.reset(new Animation(fileName, folder));
+	_ownedAnimation.reset(new Animation(fileName, folder));
+	_animation = _ownedAnimation.get();
+}
+
+void Graphic::setAnimation(Animation *animation) {
+	_animation = animation;
 }
 
 void Graphic::serializeSave(Serializer &serializer) {
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 56ee902df88..72c6ce28225 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -172,6 +172,7 @@ public:
 	inline uint spriteCount() const { return _spriteBases.size(); }
 	inline uint frameCount() const { return _frames.size(); }
 	inline uint32 frameDuration(int32 frameI) const { return _frames[frameI]._duration; }
+	inline const Common::Point &frameCenter(int32 frameI) const { return _frames[frameI]._center; }
 	inline uint32 totalDuration() const { return _totalDuration; }
 	int32 frameAtTime(uint32 time) const;
 	Common::Point imageSize(int32 imageI) const;
@@ -222,7 +223,10 @@ public:
 	inline Common::Point &center() { return _center; }
 	inline int8 &order() { return _order; }
 	inline int16 &scale() { return _scale; }
+	inline float &depthScale() { return _depthScale; }
 	inline Color &color() { return _color; }
+	inline int32 &frameI() { return _frameI; }
+	inline uint32 lastTime() const { return _lastTime; }
 	inline Animation &animation() {
 		assert(_animation != nullptr && _animation->isLoaded());
 		return *_animation;
@@ -235,12 +239,14 @@ public:
 	void pause();
 	void reset();
 	void setAnimation(const Common::String &fileName, AnimationFolder folder);
+	void setAnimation(Animation *animation); ///< no memory ownership is given, but for prerendering it has to be mutable
 	void serializeSave(Common::Serializer &serializer);
 
 private:
 	friend class AnimationDrawRequest;
 	friend class SpecialEffectDrawRequest;
-	Common::SharedPtr<Animation> _animation;
+	Common::ScopedPtr<Animation> _ownedAnimation;
+	Animation *_animation = nullptr;
 	Common::Point _center;
 	int16 _scale = kBaseScale;
 	int8 _order = 0;
@@ -253,17 +259,6 @@ private:
 	float _depthScale = 1.0f;
 };
 
-enum class DrawRequestType {
-	Animation2D,
-	Animation3D,
-	AnimationTiled,
-	Rectangle,
-	FadeToBlack,
-	FadeToWhite,
-	CrossFade,
-	Text
-};
-
 class IDrawRequest {
 public:
 	IDrawRequest(int8 order);
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 4ce0412146e..87d16526953 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -22,8 +22,8 @@
 #ifndef OBJECTS_H
 #define OBJECTS_H
 
-#include "Shape.h"
-#include "Graphics.h"
+#include "shape.h"
+#include "graphics.h"
 
 #include "common/serializer.h"
 
@@ -323,7 +323,6 @@ protected:
 	void syncObjectAsString(Common::Serializer &serializer, ObjectBase *&object);
 	void updateTalkingAnimation();
 
-private:
 	Common::Point _interactionPoint;
 	Direction _direction;
 	Graphic _graphicNormal, _graphicTalking;
@@ -342,24 +341,58 @@ public:
 	WalkingCharacter(Room *room, Common::ReadStream &stream);
 	virtual ~WalkingCharacter() override = default;
 
+	virtual void update() override;
+	virtual void draw() override;
+	virtual void drawDebug() override;
+	virtual void loadResources() override;
+	virtual void freeResources() override;
 	virtual void serializeSave(Common::Serializer &serializer) override;
+	virtual void walkTo(
+		const Common::Point &target,
+		Direction endDirection = Direction::Invalid,
+		ShapeObject *activateObject = nullptr,
+		const char *activateAction = nullptr,
+		bool useAlternateObjectDirection = false
+	);
+	void stopWalkingAndTurn(Direction direction);
+	void setPosition(const Common::Point &target);
+
+protected:
+	virtual void onArrived();
 
 private:
-	Graphic _graphicWalking;
-	Common::SharedPtr<Animation>
+	void updateWalking();
+	void updateWalkingAnimation();
+
+	inline Animation *currentAnimationOf(Common::ScopedPtr<Animation> *const animations) {
+		Animation *animation = animations[(int)_direction].get();
+		if (animation == nullptr)
+			animation = animations[0].get();
+		assert(animation != nullptr);
+		return animation;
+	}
+	inline Animation *walkingAnimation() { return currentAnimationOf(_walkingAnimations); }
+	inline Animation *talkingAnimation() { return currentAnimationOf(_talkingAnimations); }
+
+	Common::ScopedPtr<Animation>
 		_walkingAnimations[kDirectionCount],
-		_standingAnimations[kDirectionCount];
+		_talkingAnimations[kDirectionCount];
 
 	int32
 		_lastWalkAnimFrame = -1,
-		_walkSpeed = 0,
+		_walkedDistance = 0,
 		_curPathPointI = -1;
+	float _stepSizeFactor = 0.0f;
 	Common::Point
 		_sourcePos,
-		_targetPos;
+		_currentPos;
 	bool _isWalking = false;
-	Direction _direction = Direction::Up;
-	Common::Array<Common::Point> _pathPoints;
+	Direction
+		_direction = Direction::Right,
+		_interactionDirection1 = Direction::Right,
+		_interactionDirection2 = Direction::Right,
+		_endWalkingDirection = Direction::Invalid;
+	Common::Stack<Common::Point> _pathPoints;
 };
 
 enum class MainCharacterKind {
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index e35fa824ac7..a0fc1de786c 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -129,25 +129,20 @@ void Room::update() {
 		updateRoomBounds();
 		updateInput();
 	}
+	// TODO: Add condition for global room update
+	world().globalRoom().updateObjects();
 	if (world().currentRoom() == this)
 		updateObjects();
 	if (world().currentRoom() == this) {
 		g_engine->camera().update();
 		drawObjects();
-		// TODO: world().globalRoom().drawObjects();
+		world().globalRoom().drawObjects();
 		// TODO: Draw black borders
 		g_engine->drawQueue().draw();
 		drawDebug();
 		world().globalRoom().drawDebug();
 	}
 }
-using namespace Math;
-static Array<Vector2d> path;
-
-static Vector2d asVec(const Point &p) {
-	return Vector2d((float)p.x, (float)p.y);
-}
-
 
 void Room::updateInput() {
 	static bool hasLastP3D = false;
@@ -157,18 +152,16 @@ void Room::updateInput() {
 	if (g_engine->input().wasMouseLeftPressed()) {
 		Point p2d = g_engine->input().mousePos2D();
 		Point p3d = g_engine->input().mousePos3D();
+		auto m = &g_engine->world().filemon();
 
-		if (hasLastP3D) {
-			Stack<Point> pathi;
-			bool result = _floors[0].findPath(lastP3D, p3d, pathi);
-			path.clear();
-			path.push_back(asVec(lastP3D));
-			while (!pathi.empty())
-				path.push_back(asVec(pathi.pop()));
-			warning("Did %sfind a path in %d steps", result ? "" : "not ", path.size());
+		if (!hasLastP3D) {
+			m->setPosition(p3d);
+		}
+		else {
+			m->room() = this;
+			m->walkTo(p3d);
 		}
 		hasLastP3D = true;
-		lastP3D = p3d;
 	}
 }
 
@@ -180,9 +173,10 @@ void Room::updateRoomBounds() {
 }
 
 void Room::updateObjects() {
+	const auto *previousRoom = world().currentRoom();
 	for (auto *object : _objects) {
 		object->update();
-		if (world().currentRoom() != this)
+		if (world().currentRoom() != previousRoom)
 			return;
 	}
 }
@@ -202,15 +196,6 @@ void Room::drawDebug() {
 		return;
 	if (_activeFloorI >= 0 && g_engine->console().showFloor())
 		renderer->debugShape(_floors[_activeFloorI], kDebugBlue);
-
-	renderer->debugPolyline({ path.begin(), path.size()}, kWhite);
-
-	Common::Array<Vector2d> asd;
-	for (auto p : _floors[0]._linkPoints)
-	{
-		auto v = asVec(p);
-		renderer->debugPolyline({ &v, 1 }, { 255, 0, 255, 255 });
-	}
 }
 
 void Room::loadResources() {
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index e50c10ceb8f..d174620618e 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -36,6 +36,15 @@ public:
 
 	inline World &world() { return *_world; }
 	inline const Common::String &name() const { return _name; }
+	inline const PathFindingShape *activeFloor() const {
+		return _activeFloorI < 0 ? nullptr : &_floors[_activeFloorI];
+	}
+	inline int8 orderAt(const Common::Point &query) const {
+		return _activeFloorI < 0 ? 49 : activeFloor()->orderAt(query);
+	}
+	inline float depthAt(const Common::Point &query) const {
+		return _activeFloorI < 0 ? 1 : activeFloor()->depthAt(query);
+	}
 
 	void update();
 	virtual void updateInput();
@@ -118,6 +127,7 @@ public:
 	inline Inventory &inventory() const { return *_inventory; }
 	inline MainCharacter &filemon() const { return *_filemon; }
 	inline MainCharacter &mortadelo() const { return *_mortadelo; }
+	inline MainCharacter *activeCharacter() const { return _activeCharacter; }
 	inline const Common::String &initScriptName() const { return _initScriptName; }
 	inline uint8 loadedMapCount() const { return _loadedMapCount; }
 
@@ -142,7 +152,7 @@ private:
 	Common::String _initScriptName;
 	Room *_globalRoom, *_currentRoom = nullptr;
 	Inventory *_inventory;
-	MainCharacter *_filemon, *_mortadelo;
+	MainCharacter *_filemon, *_mortadelo, *_activeCharacter = nullptr;
 	uint8 _loadedMapCount = 0;
 };
 
diff --git a/engines/alcachofa/shape.cpp b/engines/alcachofa/shape.cpp
index dd12ab65ca8..eb9019997da 100644
--- a/engines/alcachofa/shape.cpp
+++ b/engines/alcachofa/shape.cpp
@@ -38,6 +38,8 @@ static int sideOfLine(const Point &a, const Point &b, const Point &q) {
 static bool segmentsIntersect(const Point &a1, const Point &b1, const Point &a2, const Point &b2) {
 	// as there are a number of special cases to consider, this method is a direct translation
 	// of the original engine
+	// TODO: It is still bad and does sometimes not work correctly. Check this. keep in mind
+	// it *could* also be a case of incorrect floor segments being passed into in the first place.
 
 	const auto sideOfLine = [](const Point &a, const Point &b, const Point q) {
 		return Alcachofa::sideOfLine(a, b, q) > 0;
@@ -102,7 +104,7 @@ static float depthAtForConvex(const PathFindingPolygon &p, const Point &q) {
 		auto distances = p.edgeDistances(i, q);
 		float depthOnEdge = p._pointDepths[i] + distances._onEdge * (p._pointDepths[j] - p._pointDepths[i]) / distances._edgeLength;
 		if (distances._toEdge < epsilon) // q is directly on the edge
-			return depthOnEdge;
+			return depthOnEdge * 0.01f;
 		sumDepths += 1 / distances._toEdge * depthOnEdge;
 		sumDistances += 1 / distances._toEdge;
 	}
@@ -517,6 +519,8 @@ void PathFindingShape::floydWarshallPath(
 }
 
 Point PathFindingShape::getClosestPoint(const Point &query) const {
+	// TODO: Improve this function, it does not seem correct
+
 	assert(!_points.empty());
 	Point bestPoint;
 	uint bestDistance = UINT_MAX;
diff --git a/engines/alcachofa/shape.h b/engines/alcachofa/shape.h
index ca527fc8c17..20168647c6f 100644
--- a/engines/alcachofa/shape.h
+++ b/engines/alcachofa/shape.h
@@ -161,8 +161,7 @@ public:
 		Common::Stack<Common::Point> &path) const;
 	Common::Point getClosestPoint(const Common::Point &query) const;
 
-private: //ASDJALSKDJALKXDJALSKDJALSKDJALKjs
-public:
+private:
 	void setupLinks();
 	void setupLinkPoint(
 		const PathFindingPolygon &outer,
diff --git a/engines/alcachofa/stream-helper.h b/engines/alcachofa/stream-helper.h
index 07bab1338a4..7c41ca41376 100644
--- a/engines/alcachofa/stream-helper.h
+++ b/engines/alcachofa/stream-helper.h
@@ -25,6 +25,7 @@
 #include "common/stream.h"
 #include "common/serializer.h"
 #include "common/rect.h"
+#include "common/stack.h"
 
 namespace Alcachofa {
 
@@ -43,6 +44,23 @@ inline void syncArray(Common::Serializer &serializer, Common::Array<T> &array, v
 	serializer.syncArray(array.data(), size, serializeFunction);
 }
 
+template<typename T>
+inline void syncStack(Common::Serializer &serializer, Common::Stack<T> &stack, void (*serializeFunction)(Common::Serializer &, T &)) {
+	auto size = stack.size();
+	serializer.syncAsUint32LE(size);
+	if (serializer.isLoading()) {
+		for (uint i = 0; i < size; i++) {
+			T value;
+			serializeFunction(serializer, value);
+			stack.push(value);
+		}
+	}
+	else {
+		for (uint i = 0; i < size; i++)
+			serializeFunction(serializer, stack[i]);
+	}
+}
+
 template<typename T>
 inline void syncEnum(Common::Serializer &serializer, T &enumValue) {
 	// syncAs does not have a cast for saving


Commit: b0fc780802b633e97011be5e21ba3afb27d27215
    https://github.com/scummvm/scummvm/commit/b0fc780802b633e97011be5e21ba3afb27d27215
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:45+02:00

Commit Message:
ALCACHOFA: Add parts of main characters

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


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index c44ea36f1f2..b5c894997a8 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -34,10 +34,13 @@ Item::Item(Room *room, ReadStream &stream)
 	stream.readByte(); // unused and ignored byte
 }
 
+ITriggerableObject::ITriggerableObject(ReadStream &stream)
+	: _interactionPoint(Shape(stream).firstPoint())
+	, _interactionDirection((Direction)stream.readSint32LE()) {}
+
 InteractableObject::InteractableObject(Room *room, ReadStream &stream)
 	: PhysicalObject(room, stream)
-	, _interactionPoint(Shape(stream).firstPoint())
-	, _cursorType((CursorType)stream.readSint32LE())
+	, ITriggerableObject(stream)
 	, _relatedObject(readVarString(stream)) {
 	_relatedObject.toUppercase();
 }
@@ -50,6 +53,10 @@ void InteractableObject::drawDebug() {
 	renderer->debugShape(*shape());
 }
 
+void InteractableObject::trigger(const char *action) {
+	warning("stub: Trigger object %s with %s", name().c_str(), action == nullptr ? "<null>" : action);
+}
+
 Door::Door(Room *room, ReadStream &stream)
 	: InteractableObject(room, stream)
 	, _targetRoom(readVarString(stream))
@@ -60,8 +67,7 @@ Door::Door(Room *room, ReadStream &stream)
 
 Character::Character(Room *room, ReadStream &stream)
 	: ShapeObject(room, stream)
-	, _interactionPoint(Shape(stream).firstPoint())
-	, _direction((Direction)stream.readSint32LE())
+	, ITriggerableObject(stream)
 	, _graphicNormal(stream)
 	, _graphicTalking(stream) {
 	_graphicNormal.start(true);
@@ -173,6 +179,10 @@ void Character::syncObjectAsString(Serializer &serializer, ObjectBase *&object)
 	}
 }
 
+void Character::trigger(const char *action) {
+	warning("stub: Trigger character %s with %s", name().c_str(), action == nullptr ? "<null>" : action);
+}
+
 WalkingCharacter::WalkingCharacter(Room *room, ReadStream &stream)
 	: Character(room, stream) {
 	for (int32 i = 0; i < kDirectionCount; i++) {
@@ -228,13 +238,13 @@ void WalkingCharacter::update() {
 	}
 
 	_interactionPoint = _currentPos;
-	_interactionDirection1 = Direction::Right;
+	_interactionDirection = Direction::Right;
 	if (this != g_engine->world().activeCharacter()) {
 		int16 interactionOffset = (int16)(150 * _graphicNormal.depthScale());
 		_interactionPoint.x -= interactionOffset;
 		if (activeFloor != nullptr && activeFloor->polygonContaining(_interactionPoint) < 0) {
 			_interactionPoint.x = _currentPos.x + interactionOffset;
-			_interactionDirection1 = Direction::Left;
+			_interactionDirection = Direction::Left;
 		}
 	}
 }
@@ -345,8 +355,7 @@ void WalkingCharacter::stopWalkingAndTurn(Direction direction) {
 
 void WalkingCharacter::walkTo(
 	const Point &target, Direction endDirection,
-	ShapeObject *activateObject, const char *activateAction,
-	bool useAlternateObjectDirection) {
+	ITriggerableObject *activateObject, const char *activateAction) {
 	// all the activation parameters are only relevant for MainCharacter
 
 	if (_isWalking)
@@ -459,6 +468,80 @@ MainCharacter::~MainCharacter() {
 		delete item;
 }
 
+void MainCharacter::update() {
+	if (_relatedProcessCounter == 0)
+		_currentlyUsingObject = nullptr;
+	WalkingCharacter::update();
+
+	const int16 halfWidth = (int16)(60 * _graphicNormal.depthScale());
+	const int16 height = (int16)(310 * _graphicNormal.depthScale());
+	shape()->setAsRectangle(Rect(
+		_currentPos.x - halfWidth, _currentPos.y - height,
+		_currentPos.x + halfWidth, _currentPos.y));
+
+	// TODO: Update character alpha tint
+}
+
+void MainCharacter::onArrived() {
+	if (_activateObject == nullptr)
+		return;
+
+	ITriggerableObject *activateObject = _activateObject;
+	const char *activateAction = _activateAction;
+	_activateObject = nullptr;
+	_activateAction = nullptr;
+
+	stopWalkingAndTurn(activateObject->interactionDirection());
+	if (g_engine->world().activeCharacter() == this)
+		activateObject->trigger(activateAction);
+}
+
+void MainCharacter::walkTo(
+	const Point &target, Direction endDirection,
+	ITriggerableObject *activateObject, const char *activateAction) {
+	_activateObject = activateObject;
+	_activateAction = activateAction;
+
+	// TODO: Add collision avoidance
+
+	WalkingCharacter::walkTo(target, endDirection, activateObject, activateAction);
+	if (this == g_engine->world().activeCharacter()) {
+		// TODO: Add camera following character
+	}
+}
+
+void MainCharacter::draw() {
+	if (this == &g_engine->world().mortadelo()) {
+		if (_currentPos.y <= g_engine->world().filemon()._currentPos.y) {
+			g_engine->world().mortadelo().drawInner();
+			g_engine->world().filemon().drawInner();
+		}
+		else {
+			g_engine->world().filemon().drawInner();
+			g_engine->world().mortadelo().drawInner();
+		}
+	}
+}
+
+void MainCharacter::drawInner() {
+	if (room() != g_engine->world().currentRoom() || !isEnabled())
+		return;
+	Graphic *activeGraphic = graphicOf(_curAnimateObject);
+	if (activeGraphic == nullptr && _isWalking) {
+		activeGraphic = &_graphicNormal;
+		_graphicNormal.premultiplyAlpha() = room()->characterAlphaPremultiplier();
+	}
+	if (activeGraphic == nullptr) {
+		activeGraphic = graphicOf(_curTalkingObject, &_graphicTalking);
+		_graphicTalking.premultiplyAlpha() = room()->characterAlphaPremultiplier();
+	}
+
+	assert(activeGraphic != nullptr);
+	activeGraphic->color() = kWhite; // TODO: Add and use character color
+	g_engine->drawQueue().add<AnimationDrawRequest>(*activeGraphic, true, BlendMode::AdditiveAlpha, _lodBias);
+
+}
+
 void syncDialogMenuLine(Serializer &serializer, DialogMenuLine &line) {
 	serializer.syncAsSint32LE(line._dialogId);
 	serializer.syncAsSint32LE(line._yPosition);
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 72c6ce28225..191c1ba9896 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -174,6 +174,7 @@ public:
 	inline uint32 frameDuration(int32 frameI) const { return _frames[frameI]._duration; }
 	inline const Common::Point &frameCenter(int32 frameI) const { return _frames[frameI]._center; }
 	inline uint32 totalDuration() const { return _totalDuration; }
+	inline uint8 &premultiplyAlpha() { return _premultiplyAlpha; }
 	int32 frameAtTime(uint32 time) const;
 	Common::Point imageSize(int32 imageI) const;
 
@@ -231,6 +232,10 @@ public:
 		assert(_animation != nullptr && _animation->isLoaded());
 		return *_animation;
 	}
+	inline uint8 &premultiplyAlpha() {
+		assert(_animation != nullptr);
+		return _animation->premultiplyAlpha();
+	}
 
 	void loadResources();
 	void freeResources();
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 87d16526953..0af7ed1c386 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -281,17 +281,30 @@ public:
 	Item(Room *room, Common::ReadStream &stream);
 };
 
-class InteractableObject : public PhysicalObject {
+class ITriggerableObject {
+public:
+	ITriggerableObject(Common::ReadStream &stream);
+
+	inline Direction interactionDirection() const { return _interactionDirection; }
+	inline const Common::Point &interactionPoint() const { return _interactionPoint; }
+
+	virtual void trigger(const char *action) = 0;
+
+protected:
+	Common::Point _interactionPoint;
+	Direction _interactionDirection = Direction::Right;
+};
+
+class InteractableObject : public PhysicalObject, public ITriggerableObject {
 public:
 	static constexpr const char *kClassName = "CObjetoTipico";
 	InteractableObject(Room *room, Common::ReadStream &stream);
 	virtual ~InteractableObject() override = default;
 
 	virtual void drawDebug() override;
+	virtual void trigger(const char *action) override;
 
 private:
-	Common::Point _interactionPoint;
-	CursorType _cursorType;
 	Common::String _relatedObject;
 };
 
@@ -305,7 +318,7 @@ private:
 	Direction _characterDirection;
 };
 
-class Character : public ShapeObject {
+class Character : public ShapeObject, public ITriggerableObject {
 public:
 	static constexpr const char *kClassName = "CPersonaje";
 	Character(Room *room, Common::ReadStream &stream);
@@ -318,12 +331,12 @@ public:
 	virtual void freeResources() override;
 	virtual void serializeSave(Common::Serializer &serializer) override;
 	virtual Graphic *graphic() override;
+	virtual void trigger(const char *action) override;
 
 protected:
 	void syncObjectAsString(Common::Serializer &serializer, ObjectBase *&object);
 	void updateTalkingAnimation();
 
-	Common::Point _interactionPoint;
 	Direction _direction;
 	Graphic _graphicNormal, _graphicTalking;
 
@@ -350,17 +363,13 @@ public:
 	virtual void walkTo(
 		const Common::Point &target,
 		Direction endDirection = Direction::Invalid,
-		ShapeObject *activateObject = nullptr,
-		const char *activateAction = nullptr,
-		bool useAlternateObjectDirection = false
-	);
+		ITriggerableObject *activateObject = nullptr,
+		const char *activateAction = nullptr);
 	void stopWalkingAndTurn(Direction direction);
 	void setPosition(const Common::Point &target);
 
 protected:
 	virtual void onArrived();
-
-private:
 	void updateWalking();
 	void updateWalkingAnimation();
 
@@ -389,8 +398,6 @@ private:
 	bool _isWalking = false;
 	Direction
 		_direction = Direction::Right,
-		_interactionDirection1 = Direction::Right,
-		_interactionDirection2 = Direction::Right,
 		_endWalkingDirection = Direction::Invalid;
 	Common::Stack<Common::Point> _pathPoints;
 };
@@ -416,14 +423,28 @@ public:
 	inline MainCharacterKind kind() const { return _kind; }
 	inline ObjectBase *currentlyUsing() const { return _currentlyUsingObject; }
 
+	virtual void update() override;
+	virtual void draw() override;
 	virtual void serializeSave(Common::Serializer &serializer) override;
+	virtual void walkTo(
+		const Common::Point &target,
+		Direction endDirection = Direction::Invalid,
+		ITriggerableObject *activateObject = nullptr,
+		const char *activateAction = nullptr) override;
+
+protected:
+	virtual void onArrived() override;
 
 private:
+	void drawInner();
+
 	Common::Array<Item *> _items;
 	Common::Array<DialogMenuLine> _dialogMenuLines;
 	ObjectBase *_currentlyUsingObject = nullptr;
 	MainCharacterKind _kind;
 	int32_t _relatedProcessCounter = 0;
+	ITriggerableObject *_activateObject = nullptr;
+	const char *_activateAction = nullptr;
 };
 
 class Background final : public GraphicObject {
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index a0fc1de786c..237849addb5 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -88,13 +88,13 @@ Room::Room(World *world, ReadStream &stream, bool hasUselessByte)
 	: _world(world) {
 	_name = readVarString(stream);
 	_musicId = stream.readSByte();
-	_characterAlpha = stream.readByte();
+	_characterAlphaTint = stream.readByte();
 	auto backgroundScale = stream.readSint16LE();
 	_floors[0] = PathFindingShape(stream);
 	_floors[1] = PathFindingShape(stream);
 	_cameraFollowsUponLeaving = readBool(stream);
 	PathFindingShape _(stream); // unused path finding area
-	_characterAlphaPercent = stream.readByte();
+	_characterAlphaPremultiplier = stream.readByte();
 	if (hasUselessByte)
 		stream.readByte();
 
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index d174620618e..eb28fcc69ea 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -45,6 +45,8 @@ public:
 	inline float depthAt(const Common::Point &query) const {
 		return _activeFloorI < 0 ? 1 : activeFloor()->depthAt(query);
 	}
+	inline uint8 characterAlphaTint() const { return _characterAlphaTint; }
+	inline uint8 characterAlphaPremultiplier() const { return _characterAlphaPremultiplier; }
 
 	void update();
 	virtual void updateInput();
@@ -68,8 +70,8 @@ protected:
 		_musicId,
 		_activeFloorI = -1;
 	uint8
-		_characterAlpha,
-		_characterAlphaPercent;
+		_characterAlphaTint,
+		_characterAlphaPremultiplier; ///< for some reason in percent instead of 0-255
 
 	Common::Array<ObjectBase *> _objects;
 };
diff --git a/engines/alcachofa/shape.cpp b/engines/alcachofa/shape.cpp
index eb9019997da..66e3b137096 100644
--- a/engines/alcachofa/shape.cpp
+++ b/engines/alcachofa/shape.cpp
@@ -256,6 +256,16 @@ bool Shape::contains(const Point &query) const {
 	return polygonContaining(query) >= 0;
 }
 
+void Shape::setAsRectangle(const Rect &rect) {
+	_polygons.resize(1);
+	_polygons[0] = { 0, 4 };
+	_points.resize(4);
+	_points[0] = { rect.left, rect.top };
+	_points[1] = { rect.right, rect.top };
+	_points[2] = { rect.right, rect.bottom };
+	_points[3] = { rect.left, rect.bottom };
+}
+
 PathFindingShape::PathFindingShape() {}
 
 PathFindingShape::PathFindingShape(ReadStream &stream) {
diff --git a/engines/alcachofa/shape.h b/engines/alcachofa/shape.h
index 20168647c6f..0452cd45d8f 100644
--- a/engines/alcachofa/shape.h
+++ b/engines/alcachofa/shape.h
@@ -122,6 +122,7 @@ public:
 	Polygon at(uint index) const;
 	int32 polygonContaining(const Common::Point &query) const;
 	bool contains(const Common::Point &query) const;
+	void setAsRectangle(const Common::Rect &rect);
 
 protected:
 	uint addPolygon(uint maxCount);


Commit: 49e716eeb0cb824f9269ffe219f1df7a8bc7c6c8
    https://github.com/scummvm/scummvm/commit/49e716eeb0cb824f9269ffe219f1df7a8bc7c6c8
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:45+02:00

Commit Message:
ALCACHOFA: Initial scheduler and script

Changed paths:
  A engines/alcachofa/scheduler.cpp
  A engines/alcachofa/scheduler.h
  A engines/alcachofa/script.cpp
  A engines/alcachofa/script.h
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/alcachofa.h
    engines/alcachofa/common.h
    engines/alcachofa/objects.h


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index a536098f100..e5aa732849f 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -33,6 +33,7 @@
 #include "graphics/framelimiter.h"
 
 #include "rooms.h"
+#include "script.h"
 
 using namespace Math;
 
@@ -61,6 +62,7 @@ Common::Error AlcachofaEngine::run() {
 	_renderer.reset(IRenderer::createOpenGLRenderer(Common::Point(1024, 768)));
 	_drawQueue.reset(new DrawQueue(_renderer.get()));
 	_world.reset(new World());
+	_script.reset(new Script());
 
 	world().globalRoom().loadResources();
 
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index 913da07cbea..7205cd1c9c6 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -38,6 +38,7 @@
 #include "alcachofa/camera.h"
 #include "alcachofa/input.h"
 #include "alcachofa/player.h"
+#include "alcachofa/scheduler.h"
 #include "alcachofa/console.h"
 
 namespace Alcachofa {
@@ -45,6 +46,7 @@ namespace Alcachofa {
 class IRenderer;
 class DrawQueue;
 class World;
+class Script;
 struct AlcachofaGameDescription;
 
 class AlcachofaEngine : public Engine {
@@ -64,6 +66,8 @@ public:
 	inline Input &input() { return _input; }
 	inline Player &player() { return _player; }
 	inline World &world() { return *_world; }
+	inline Script &script() { return *_script; }
+	inline Scheduler &scheduler() { return _scheduler; }
 	inline Console &console() { return *_console; }
 
 	uint32 getFeatures() const;
@@ -114,9 +118,11 @@ private:
 	Common::ScopedPtr<IRenderer> _renderer;
 	Common::ScopedPtr<DrawQueue> _drawQueue;
 	Common::ScopedPtr<World> _world;
+	Common::ScopedPtr<Script> _script;
 	Camera _camera;
 	Input _input;
 	Player _player;
+	Scheduler _scheduler;
 };
 
 extern AlcachofaEngine *g_engine;
diff --git a/engines/alcachofa/common.h b/engines/alcachofa/common.h
index 92c8c636704..77d2c348afc 100644
--- a/engines/alcachofa/common.h
+++ b/engines/alcachofa/common.h
@@ -22,6 +22,8 @@
 #ifndef COMMON_H
 #define COMMON_H
 
+#include "common/scummsys.h"
+
 namespace Alcachofa {
 
 enum class CursorType {
@@ -44,6 +46,12 @@ enum class Direction {
 	Invalid = -1
 };
 
+enum class MainCharacterKind {
+	None,
+	Mortadelo,
+	Filemon
+};
+
 constexpr const int32 kDirectionCount = 4;
 constexpr const int8 kOrderCount = 70;
 constexpr const int8 kForegroundOrderCount = 10;
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 0af7ed1c386..4ceb3f8f7a8 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -402,12 +402,6 @@ protected:
 	Common::Stack<Common::Point> _pathPoints;
 };
 
-enum class MainCharacterKind {
-	None,
-	Mortadelo,
-	Filemon
-};
-
 struct DialogMenuLine {
 	int32 _dialogId;
 	int32 _yPosition;
diff --git a/engines/alcachofa/scheduler.cpp b/engines/alcachofa/scheduler.cpp
new file mode 100644
index 00000000000..187d8d932c7
--- /dev/null
+++ b/engines/alcachofa/scheduler.cpp
@@ -0,0 +1,212 @@
+/* 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 "scheduler.h"
+
+#include "common/system.h"
+#include "alcachofa.h"
+
+using namespace Common;
+
+namespace Alcachofa {
+
+struct DelayTask : public Task {
+	DelayTask(Process &process, uint32 millis)
+		: Task(process)
+		, _endTime(millis) {}
+
+	virtual TaskReturn run() override {
+		TASK_BEGIN;
+		_endTime += g_system->getMillis();
+		while (g_system->getMillis() < _endTime)
+			TASK_YIELD;
+		TASK_END;
+	}
+
+	virtual void debugPrint() {
+		uint32 remaining = g_system->getMillis() <= _endTime ? _endTime - g_system->getMillis() : 0;
+		g_engine->getDebugger()->debugPrintf("Delay for further %ums\n", remaining);
+	}
+
+private:
+	uint32 _endTime;
+};
+
+TaskReturn::TaskReturn() {
+	_type = TaskReturnType::Yield;
+	_returnValue = 0;
+	_taskToWaitFor = nullptr;
+}
+
+TaskReturn TaskReturn::finish(int32 returnValue) {
+	TaskReturn r;
+	r._type = TaskReturnType::Finished;
+	r._returnValue = returnValue;
+	return r;
+}
+
+TaskReturn TaskReturn::waitFor(Task *task) {
+	assert(task != nullptr);
+	TaskReturn r;
+	r._type = TaskReturnType::Waiting;
+	r._taskToWaitFor = task;
+	return r;
+}
+
+Task::Task(Process &process) : _process(process) {}
+
+Task *Task::delay(uint32 millis) {
+	return new DelayTask(process(), millis);
+}
+
+Process::Process(ProcessId pid, MainCharacterKind characterKind)
+	: _pid(pid)
+	, _character(characterKind)
+	, _name("Unnamed process") {
+}
+
+Process::~Process() {
+	while (!_tasks.empty())
+		delete _tasks.pop();
+}
+
+TaskReturnType Process::run() {
+	while (!_tasks.empty()) {
+		TaskReturn ret = _tasks.top()->run();
+		switch (ret.type()) {
+		case TaskReturnType::Yield: return TaskReturnType::Yield;
+		case TaskReturnType::Waiting:
+			_tasks.push(ret.taskToWaitFor());
+			break;
+		case TaskReturnType::Finished:
+			_lastReturnValue = ret.returnValue();
+			_tasks.pop();
+			break;
+		default:
+			assert(false && "Invalid task return type");
+			return TaskReturnType::Finished;
+		}
+	}
+	return TaskReturnType::Finished;
+}
+
+void Process::debugPrint() {
+	auto *debugger = g_engine->getDebugger();
+	const char *characterName;
+	switch (_character) {
+	case MainCharacterKind::None: characterName = "    <none>"; break;
+	case MainCharacterKind::Filemon: characterName =   " Filemon"; break;
+	case MainCharacterKind::Mortadelo: characterName = "Mortadelo"; break;
+	default: characterName = "<invalid>"; break;
+	}
+	debugger->debugPrintf("pid: %3u char: %s ret: %2d \"%s\"\n", _pid, characterName, _lastReturnValue, _name.c_str());
+
+	for (uint i = 0; i < _tasks.size(); i++) {
+		debugger->debugPrintf("\t%u: ", i);
+		_tasks[i]->debugPrint();
+	}
+}
+
+static void killProcessesForIn(MainCharacterKind characterKind, Array<Process *> &processes, uint firstIndex) {
+	assert(firstIndex < processes.size());
+	uint count = processes.size() - 1 - firstIndex;
+	for (uint i = 0; i < count; i++) {
+		Process **process = &processes[processes.size() - 1 - i];
+		if ((*process)->character() == characterKind || characterKind == MainCharacterKind::None) {
+			delete *process;
+			processes.erase(process);
+		}
+	}
+}
+
+Scheduler::~Scheduler() {
+	killAllProcesses();
+	killProcessesForIn(MainCharacterKind::None, _backupProcesses, 0);
+}
+
+Process *Scheduler::createProcessInternal(MainCharacterKind character) {
+	Process *process = new Process(_nextPid++, character);
+	processesToRunNext().push_back(process);
+	return process;
+}
+
+void Scheduler::run() {
+	assert(processesToRun().empty()); // otherwise we somehow left normal flow
+	_currentArrayI = (_currentArrayI + 1) % 2;
+	// processesToRun() can be modified during loop so do not replace with iterators
+	for (_currentProcessI = 0; _currentProcessI < processesToRun().size(); _currentProcessI++) {
+		Process *process = processesToRun()[_currentProcessI];
+		auto ret = process->run();
+		if (ret == TaskReturnType::Finished)
+			delete process;
+		else
+			processesToRunNext().push_back(process);
+	}
+	processesToRun().clear();
+	_currentProcessI = UINT_MAX;
+}
+
+void Scheduler::backupContext() {
+	assert(processesToRun().empty());
+	_backupProcesses.push_back(processesToRunNext());
+	processesToRunNext().clear();
+}
+
+void Scheduler::restoreContext() {
+	assert(processesToRun().empty());
+	processesToRunNext().push_back(_backupProcesses);
+	_backupProcesses.clear();
+}
+
+void Scheduler::killAllProcesses() {
+	killProcessesForIn(MainCharacterKind::None, _processArrays[0], 0);
+	killProcessesForIn(MainCharacterKind::None, _processArrays[1], 0);
+}
+
+void Scheduler::killAllProcessesFor(MainCharacterKind characterKind) {
+	// this method can be called during run() so be careful
+	killProcessesForIn(characterKind, processesToRunNext(), 0);
+	killProcessesForIn(characterKind, processesToRun(), _currentProcessI == UINT_MAX ? 0 : _currentProcessI);
+}
+
+static Process **getProcessByName(Array<Process *> &_processes, const String &name) {
+	for (auto &process : _processes) {
+		if (process->name() == name)
+			return &process;
+	}
+	return nullptr;
+}
+
+void Scheduler::killProcessByName(const String &name) {
+	assert(processesToRun().empty());
+	Process **process = getProcessByName(processesToRunNext(), name);
+	if (process != nullptr) {
+		delete *process;
+		processesToRunNext().erase(process);
+	}
+}
+
+bool Scheduler::hasProcessWithName(const String &name) {
+	assert(processesToRun().empty());
+	return getProcessByName(processesToRunNext(), name) != nullptr;
+}
+
+}
diff --git a/engines/alcachofa/scheduler.h b/engines/alcachofa/scheduler.h
new file mode 100644
index 00000000000..8e3a74ab8d2
--- /dev/null
+++ b/engines/alcachofa/scheduler.h
@@ -0,0 +1,178 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SCHEDULER_H
+#define SCHEDULER_H
+
+#include "common.h"
+
+#include "common/stack.h"
+#include "common/str.h"
+
+namespace Alcachofa {
+
+struct Task;
+class Process;
+
+
+enum class TaskReturnType {
+	Yield,
+	Finished,
+	Waiting
+};
+
+struct TaskReturn {
+	static inline TaskReturn yield() { return {}; }
+	static TaskReturn finish(int32 returnValue);
+	static TaskReturn waitFor(Task *task);
+
+	inline TaskReturnType type() const { return _type; }
+	inline int32 returnValue() const {
+		assert(_type == TaskReturnType::Finished);
+		return _returnValue;
+	}
+	inline Task *taskToWaitFor() const {
+		assert(_type == TaskReturnType::Waiting);
+		return _taskToWaitFor;
+	}
+
+private:
+	TaskReturn();
+	TaskReturnType _type;
+	union {
+		int32 _returnValue;
+		Task *_taskToWaitFor;
+	};
+};
+
+struct Task {
+	Task(Process &process);
+	virtual ~Task() = default;
+	virtual TaskReturn run() = 0;
+	virtual void debugPrint() = 0;
+
+	inline Process &process() const { return _process; }
+
+protected:
+	Task *delay(uint32 millis);
+
+	uint32 _line = 0;
+private:
+	Process &_process;
+};
+
+// TODO: This probably should be scummvm common
+#if __cplusplus >= 201703L
+#define TASK_BREAK_FALLTHROUGH [[fallthrough]];
+#else
+#define TASK_BREAK_FALLTHROUGH
+#endif
+
+#define TASK_BEGIN \
+	switch(_line) { \
+	case 0:; \
+
+#define TASK_END \
+	TASK_RETURN(0); \
+	TASK_BREAK_FALLTHROUGH \
+	default: assert(false && "Invalid line in task"); \
+	} return TaskReturn::finish(0)
+
+#define TASK_YIELD \
+	do { \
+		_line = __LINE__; \
+		return TaskReturn::yield(); \
+		TASK_BREAK_FALLTHROUGH \
+		case __LINE__:; \
+	} while(0);
+
+#define TASK_WAIT(task) \
+	do { \
+		_line = __LINE__; \
+		return TaskReturn::waitFor(task); \
+		TASK_BREAK_FALLTHROUGH \
+		case __LINE__:; \
+	} while(0);
+
+#define TASK_RETURN(value) \
+	do { \
+		return TaskReturn::finish(value); \
+		_line = UINT_MAX; \
+	} while(0)
+
+using ProcessId = uint;
+class Process {
+public:
+	Process(ProcessId pid, MainCharacterKind characterKind);
+	~Process();
+
+	inline ProcessId pid() const { return _pid; }
+	inline MainCharacterKind character() const { return _character; }
+	inline int32 returnValue() const { return _lastReturnValue; }
+	inline Common::String &name() { return _name; }
+
+	TaskReturnType run();
+	void debugPrint();
+
+private:
+	friend class Scheduler;
+	ProcessId _pid;
+	MainCharacterKind _character;
+	Common::Stack<Task *> _tasks;
+	Common::String _name;
+	int32 _lastReturnValue = 0;
+};
+
+class Scheduler {
+public:
+	~Scheduler();
+
+	void run();
+	void backupContext();
+	void restoreContext();
+	void killAllProcesses();
+	void killAllProcessesFor(MainCharacterKind characterKind);
+	void killProcessByName(const Common::String &name);
+	bool hasProcessWithName(const Common::String &name);
+
+	template<typename TTask, typename... TaskArgs>
+	Process *createProcess(MainCharacterKind character, TaskArgs&&... args) {
+		Process *process = createProcessInternal(character);
+		process->_tasks.push(new TTask(*process, Common::forward<TaskArgs>(args)...));
+		return process;
+	}
+
+private:
+	Process *createProcessInternal(MainCharacterKind character);
+
+	inline Common::Array<Process *> &processesToRun() { return _processArrays[_currentArrayI]; }
+	inline Common::Array<Process *> &processesToRunNext() { return _processArrays[!_currentArrayI]; }
+	Common::Array<Process *> _processArrays[2];
+	Common::Array<Process *> _backupProcesses;
+	uint8 _currentArrayI = 0;
+	ProcessId _nextPid = 1;
+	uint _currentProcessI = UINT_MAX;
+
+};
+
+}
+
+#endif // SCHEDULER_H
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
new file mode 100644
index 00000000000..be8f197645b
--- /dev/null
+++ b/engines/alcachofa/script.cpp
@@ -0,0 +1,156 @@
+/* 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 "script.h"
+#include "stream-helper.h"
+#include "alcachofa.h"
+
+#include "common/file.h"
+
+using namespace Common;
+
+namespace Alcachofa {
+
+ScriptInstruction::ScriptInstruction(ReadStream &stream)
+	: _op((ScriptOp)stream.readSint32LE())
+	, _arg(stream.readSint32LE()) {}
+
+Script::Script() {
+	File file;
+	if (!file.open("script/SCRIPT.COD"))
+		error("Could not open script");
+
+	uint32 stringBlobSize = file.readUint32LE();
+	uint32 memorySize = file.readUint32LE();
+	_strings = SpanOwner<Span<char>>({ new char[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();
+	for (uint32 i = 0; i < variableCount; i++) {
+		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();
+	for (uint32 i = 0; i < procedureCount; i++) {
+		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();
+	for (uint32 i = 0; i < behaviorCount; i++) {
+		String behaviorName = readVarString(file) + '/';
+		variableCount = file.readUint32LE(); // not used by the original game
+		assert(variableCount == 0);
+		procedureCount = file.readUint32LE();
+		for (uint32 j = 0; j < procedureCount; j++) {
+			String name = behaviorName + readVarString(file);
+			uint32 offset = file.readUint32LE();
+			file.skip(sizeof(uint32));
+			_procedures[name] = offset - 1;
+		}
+	}
+
+	uint32 instructionCount = file.readUint32LE();
+	_instructions.reserve(instructionCount);
+	for (uint32 i = 0; i < instructionCount; i++)
+		_instructions.push_back(ScriptInstruction(file));
+}
+
+int32 Script::variable(const char *name) const {
+	uint32 index;
+	if (!_variableNames.tryGetVal(name, index))
+		error("Unknown variable: %s", name);
+	return _variables[index];
+}
+
+int32 &Script::variable(const char *name) {
+	uint32 index;
+	if (!_variableNames.tryGetVal(name, index))
+		error("Unknown variable: %s", name);
+	return _variables[index];
+}
+
+struct ScriptTask : public Task {
+	ScriptTask(Process &process, const String &name, uint32 pc)
+		: Task(process)
+		, _name(name)
+		, _pc(pc) {}
+
+	virtual TaskReturn run() override {
+		warning("STUB: Script execution at %u", _pc);
+		return TaskReturn::finish(0);
+	}
+
+	virtual void debugPrint() {
+		g_engine->getDebugger()->debugPrintf("\"%s\" at %u\n", _name.c_str(), _pc);
+	}
+
+private:
+	enum class StackEntryType {
+		Numeric,
+		Variable,
+		String
+	};
+
+	struct StackEntry {
+		StackEntryType _type;
+		union {
+			int32 _numeric;
+			int32 *_variable;
+			const char *_string;
+		};
+	};
+
+	Stack<StackEntry> _stack;
+	String _name;
+	uint32 _pc;
+};
+
+Process *Script::createProcess(MainCharacterKind character, const String &behavior, const String &action, bool allowMissing) {
+	return createProcess(character, behavior + '/' + action, allowMissing);
+}
+
+Process *Script::createProcess(MainCharacterKind character, const String &procedure, bool allowMissing) {
+	uint32 offset;
+	if (!_procedures.tryGetVal(procedure, offset)) {
+		if (allowMissing)
+			return nullptr;
+		error("Unknown required procedure: %s", procedure.c_str());
+	}
+	Process *process = g_engine->scheduler().createProcess<ScriptTask>(character, procedure, offset);
+	process->name() = procedure;
+	return process;
+}
+
+}
diff --git a/engines/alcachofa/script.h b/engines/alcachofa/script.h
new file mode 100644
index 00000000000..1ee23e37809
--- /dev/null
+++ b/engines/alcachofa/script.h
@@ -0,0 +1,170 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SCRIPT_H
+#define SCRIPT_H
+
+#include "common.h"
+
+#include "common/hashmap.h"
+#include "common/span.h"
+#include "common/stream.h"
+#include "common/system.h"
+
+namespace Alcachofa {
+
+class Process;
+
+enum class ScriptOp {
+	Nop,
+	Dup,
+	PushAddr,
+	PushValue,
+	Deref,
+	Crash5, ///< would crash original engine by writing to read-only memory
+	PopN,
+	Store,
+	Crash8,
+	Crash9,
+	LoadString,
+	LoadString2, ///< exactly the same as LoadString
+	Crash12,
+	ScriptCall,
+	KernelCall,
+	JumpIfFalse,
+	JumpIfTrue,
+	Jump,
+	Negate,
+	BooleanNot,
+	Mul,
+	Crash21,
+	Crash22,
+	Add,
+	Sub,
+	Less,
+	Greater,
+	LessEquals,
+	GreaterEquals,
+	Equals,
+	NotEquals,
+	BitAnd,
+	BitOr,
+	Crash33,
+	Crash34,
+	Crash35,
+	Crash36,
+	Return
+};
+
+enum class ScriptKernelTask {
+	PlayVideo,
+	PlaySound,
+	PlayMusic,
+	StopMusic,
+	WaitForMusicToEnd,
+	ShowCenterBottomText,
+	StopAndTurn,
+	StopAndTurnMe,
+	ChangeCharacter,
+	SayText,
+	Nop10,
+	Go,
+	Put,
+	ChangeCharacterRoom,
+	KillProcesses,
+	Timer,
+	On,
+	Off,
+	Pickup,
+	CharacterPickup,
+	Drop,
+	CharacterDrop,
+	Delay,
+	HadNoMousePressFor,
+	Nop24,
+	Fork,
+	Animate,
+	AnimateCharacter,
+	AnimateTalking,
+	ChangeRoom,
+	ToggleRoomFloor,
+	SetDialogLineReturn,
+	DialogMenu,
+	ClearInventory,
+	Nop34,
+	FadeType0,
+	FadeType1,
+	SetLodBias,
+	FadeType2,
+	SetActiveTextureSet,
+	SetMaxCamSpeedFactor,
+	WaitCamStopping,
+	CamFollow,
+	CamShake,
+	LerpCamXY,
+	LerpCamZ,
+	LerpCamScale,
+	LerpCamToObjectWithScale,
+	LerpCamToObjectResettingZ,
+	LerpCamRotation,
+	FadeIn,
+	FadeOut,
+	FadeIn2,
+	FadeOut2,
+	LerpCamXYZ,
+	LerpCamToObjectKeepingZ
+};
+
+struct ScriptInstruction {
+	ScriptInstruction(Common::ReadStream &stream);
+
+	ScriptOp _op;
+	int32 _arg;
+};
+
+class Script {
+public:
+	Script();
+
+	int32 variable(const char *name) const;
+	int32 &variable(const char *name);
+	Process *createProcess(
+		MainCharacterKind character,
+		const Common::String &procedure,
+		bool allowMissing = false);
+	Process *createProcess(
+		MainCharacterKind character,
+		const Common::String &behavior,
+		const Common::String &action,
+		bool allowMissing = false);
+
+private:
+	friend struct ScriptTask;
+	Common::HashMap<Common::String, uint32> _variableNames;
+	Common::HashMap<Common::String, uint32> _procedures;
+	Common::Array<ScriptInstruction> _instructions;
+	Common::Array<int32> _variables;
+	Common::SpanOwner<Common::Span<char>> _strings;
+};
+
+}
+
+#endif // SCRIPT_H


Commit: adc43280472bcc3ae27f39bb05ced535e98e1837
    https://github.com/scummvm/scummvm/commit/adc43280472bcc3ae27f39bb05ced535e98e1837
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:45+02:00

Commit Message:
ALCACHOFA: Add script execution

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


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index e5aa732849f..bac8624a363 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -66,7 +66,7 @@ Common::Error AlcachofaEngine::run() {
 
 	world().globalRoom().loadResources();
 
-	auto room = world().getRoomByName("SALOON");
+	auto room = world().getRoomByName("MAPA_TERROR");
 	assert(room != nullptr);
 	world().currentRoom() = room;
 	room->loadResources();
@@ -115,4 +115,20 @@ Common::Error AlcachofaEngine::syncGame(Common::Serializer &s) {
 	return Common::kNoError;
 }
 
+void AlcachofaEngine::updateScriptVariables() {
+	if (_input.wasAnyMousePressed()) // yes, this variable is never reset by the engine
+		_script->variable("SeHaPulsadoRaton") = 1;
+
+	if (_script->variable("CalcularTiempoSinPulsarRaton")) {
+		if (_scriptTimer == 0)
+			_scriptTimer = g_system->getMillis();
+	}
+	else
+		_scriptTimer = 0;
+
+	_script->variable("EstanAmbos") = _world->mortadelo().room() == _world->filemon().room();
+	_script->variable("textoson") = 1; // TODO: Add subtitle option
+	_script->variable("modored") = 1; // this is signalling whether a network connection is established
+}
+
 } // End of namespace Alcachofa
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index 7205cd1c9c6..0246823d4c5 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -69,6 +69,8 @@ public:
 	inline Script &script() { return *_script; }
 	inline Scheduler &scheduler() { return _scheduler; }
 	inline Console &console() { return *_console; }
+	inline uint32 scriptTimer() const { return _scriptTimer; }
+	void updateScriptVariables();
 
 	uint32 getFeatures() const;
 
@@ -123,6 +125,8 @@ private:
 	Input _input;
 	Player _player;
 	Scheduler _scheduler;
+
+	uint32 _scriptTimer = 0;
 };
 
 extern AlcachofaEngine *g_engine;
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 237849addb5..31938e4c062 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -21,6 +21,7 @@
 
 #include "alcachofa.h"
 #include "rooms.h"
+#include "script.h"
 #include "stream-helper.h"
 
 #include "common/file.h"
@@ -125,6 +126,8 @@ ObjectBase *Room::getObjectByName(const Common::String &name) const {
 }
 
 void Room::update() {
+	updateScripts();
+
 	if (world().currentRoom() == this) {
 		updateRoomBounds();
 		updateInput();
@@ -144,6 +147,13 @@ void Room::update() {
 	}
 }
 
+void Room::updateScripts() {
+	g_engine->updateScriptVariables();
+	if (!g_engine->scheduler().hasProcessWithName("ACTUALIZAR_" + _name))
+		g_engine->script().createProcess(MainCharacterKind::None, "ACTUALIZAR_" + _name, true);
+	g_engine->scheduler().run();
+}
+
 void Room::updateInput() {
 	static bool hasLastP3D = false;
 	static Point lastP3D;
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index eb28fcc69ea..b7be6a583b0 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -57,6 +57,7 @@ public:
 
 protected:
 	Room(World *world, Common::ReadStream &stream, bool hasUselessByte);
+	void updateScripts();
 	void updateRoomBounds();
 	void updateObjects();
 	void drawObjects();
diff --git a/engines/alcachofa/scheduler.h b/engines/alcachofa/scheduler.h
index 8e3a74ab8d2..a045eb4ce4c 100644
--- a/engines/alcachofa/scheduler.h
+++ b/engines/alcachofa/scheduler.h
@@ -102,7 +102,7 @@ private:
 		return TaskReturn::yield(); \
 		TASK_BREAK_FALLTHROUGH \
 		case __LINE__:; \
-	} while(0);
+	} while(0)
 
 #define TASK_WAIT(task) \
 	do { \
@@ -110,7 +110,7 @@ private:
 		return TaskReturn::waitFor(task); \
 		TASK_BREAK_FALLTHROUGH \
 		case __LINE__:; \
-	} while(0);
+	} while(0)
 
 #define TASK_RETURN(value) \
 	do { \
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index be8f197645b..397bdb5a6e1 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -101,15 +101,169 @@ int32 &Script::variable(const char *name) {
 	return _variables[index];
 }
 
+enum class StackEntryType {
+	Number,
+	Variable,
+	String,
+	Instruction
+};
+
+struct StackEntry {
+	StackEntry(StackEntryType type, int32 number) : _type(type), _number(number) {}
+	StackEntry(StackEntryType type, uint32 index) : _type(type), _index(index) {}
+
+	StackEntryType _type;
+	union {
+		int32 _number;
+		uint32 _index;
+	};
+};
+
 struct ScriptTask : public Task {
 	ScriptTask(Process &process, const String &name, uint32 pc)
 		: Task(process)
+		, _script(g_engine->script())
 		, _name(name)
-		, _pc(pc) {}
+		, _pc(pc) {
+		pushInstruction(UINT_MAX);
+	}
+
+	ScriptTask(Process &process, const ScriptTask &forkParent)
+		: Task(process)
+		, _script(g_engine->script())
+		, _name(forkParent._name + " FORKED")
+		, _pc(forkParent._pc) {
+		for (uint i = 0; i < forkParent._stack.size(); i++)
+			_stack.push(forkParent._stack[i]);
+		pushNumber(1); // this task is the forked one
+	}
 
 	virtual TaskReturn run() override {
-		warning("STUB: Script execution at %u", _pc);
-		return TaskReturn::finish(0);
+		if (_returnsFromKernelCall)
+			pushNumber(process().returnValue());
+		_returnsFromKernelCall = false;
+
+		while (true) {
+			if (_pc >= _script._instructions.size())
+				error("Script process reached instruction out-of-bounds");
+			const auto &instruction = _script._instructions[_pc++];
+			switch (instruction._op) {
+			case ScriptOp::Nop: break;
+			case ScriptOp::Dup:
+				if (_stack.empty())
+					error("Script tried to duplicate stack top, but stack is empty");
+				_stack.push(_stack.top());
+				break;
+			case ScriptOp::PushAddr:
+				pushVariable(instruction._arg);
+				break;
+			case ScriptOp::PushValue:
+				pushNumber(instruction._arg);
+				break;
+			case ScriptOp::Deref:
+				pushNumber(popVariable());
+				break;
+			case ScriptOp::PopN:
+				if (instruction._arg < 0 || (uint)instruction._arg > _stack.size())
+					error("Script tried to pop more entries than are available on the stack");
+				for (int32 i = 0; i < instruction._arg; i++)
+					_stack.pop();
+				break;
+			case ScriptOp::Store: {
+				int32 value = popNumber();
+				popVariable() = value;
+				pushNumber(value);
+			}break;
+			case ScriptOp::LoadString:
+			case ScriptOp::LoadString2:
+				pushString(popNumber());
+				break;
+			case ScriptOp::ScriptCall:
+				pushInstruction(_pc);
+				_pc = instruction._arg - 1;
+				break;
+			case ScriptOp::KernelCall: {
+				TaskReturn kernelReturn = kernelCall((ScriptKernelTask)instruction._arg);
+				if (kernelReturn.type() == TaskReturnType::Waiting) {
+					_returnsFromKernelCall = true;
+					return kernelReturn;
+				}
+				else
+					pushNumber(kernelReturn.returnValue());
+			}break;
+			case ScriptOp::JumpIfFalse:
+				if (popNumber() == 0)
+					_pc = _pc - 1 + instruction._arg;
+				break;
+			case ScriptOp::JumpIfTrue:
+				if (popNumber() != 0)
+					_pc = _pc - 1 + instruction._arg;
+				break;
+			case ScriptOp::Jump:
+				_pc = _pc - 1 + instruction._arg;
+				break;
+			case ScriptOp::Negate:
+				pushNumber(-popNumber());
+				break;
+			case ScriptOp::BooleanNot:
+				pushNumber(popNumber() == 0 ? 1 : 0);
+				break;
+			case ScriptOp::Mul:
+				pushNumber(popNumber() * popNumber());
+				break;
+			case ScriptOp::Add:
+				pushNumber(popNumber() + popNumber());
+				break;
+			case ScriptOp::Sub:
+				pushNumber(popNumber() - popNumber());
+				break;
+			case ScriptOp::Less:
+				pushNumber(popNumber() < popNumber());
+				break;
+			case ScriptOp::Greater:
+				pushNumber(popNumber() > popNumber());
+				break;
+			case ScriptOp::LessEquals:
+				pushNumber(popNumber() <= popNumber());
+				break;
+			case ScriptOp::GreaterEquals:
+				pushNumber(popNumber() >= popNumber());
+				break;
+			case ScriptOp::Equals:
+				pushNumber(popNumber() == popNumber());
+				break;
+			case ScriptOp::NotEquals:
+				pushNumber(popNumber() != popNumber());
+				break;
+			case ScriptOp::BitAnd:
+				pushNumber(popNumber() & popNumber());
+				break;
+			case ScriptOp::BitOr:
+				pushNumber(popNumber() | popNumber());
+				break;
+			case ScriptOp::Return: {
+				int32 returnValue = popNumber();
+				_pc = popInstruction();
+				if (_pc == UINT_MAX)
+					return TaskReturn::finish(returnValue);
+				else
+					pushNumber(returnValue);
+			}break;
+			case ScriptOp::Crash5:
+			case ScriptOp::Crash8:
+			case ScriptOp::Crash9:
+			case ScriptOp::Crash12:
+			case ScriptOp::Crash21:
+			case ScriptOp::Crash22:
+			case ScriptOp::Crash33:
+			case ScriptOp::Crash34:
+			case ScriptOp::Crash35:
+			case ScriptOp::Crash36:
+				error("Script reached crash instruction");
+			default:
+				error("Script reached invalid instruction");
+			}
+		}
 	}
 
 	virtual void debugPrint() {
@@ -117,24 +271,258 @@ struct ScriptTask : public Task {
 	}
 
 private:
-	enum class StackEntryType {
-		Numeric,
-		Variable,
-		String
-	};
+	void pushNumber(int32 value) {
+		_stack.push({ StackEntryType::Number, value });
+	}
 
-	struct StackEntry {
-		StackEntryType _type;
-		union {
-			int32 _numeric;
-			int32 *_variable;
-			const char *_string;
-		};
-	};
+	void pushVariable(uint32 offset) {
+		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 });
+	}
+
+	void pushString(uint32 offset) {
+		if (offset >= _script._strings->size())
+			error("Script tried to push invalid string offset");
+		_stack.push({ StackEntryType::String, offset });
+	}
+
+	void pushInstruction(uint32 pc) {
+		_stack.push({ StackEntryType::Instruction, pc });
+	}
+
+	StackEntry pop() {
+		if (_stack.empty())
+			error("Script tried to pop empty stack");
+		return _stack.pop();
+	}
+
+	int32 popNumber() {
+		auto entry = pop();
+		if (entry._type != StackEntryType::Number)
+			error("Script tried to pop, but top of stack is not a number");
+		return entry._number;
+	}
+
+	int32 &popVariable() {
+		auto entry = pop();
+		if (entry._type != StackEntryType::Variable)
+			error("Script tried to pop, but top of stack is not a variable");
+		return _script._variables[entry._index];
+	}
+
+	const char *popString() {
+		auto entry = pop();
+		if (entry._type != StackEntryType::String)
+			error("Script tried to pop, but top of stack is not a string");
+		return _script._strings->data() + entry._index;
+	}
+
+	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;
+	}
+
+	StackEntry getArg(uint argI) {
+		if (_stack.size() < argI + 1)
+			error("Script did not supply enough arguments for kernel call");
+		return _stack[_stack.size() - 1 - argI];
+	}
+
+	int32 getNumberArg(uint argI) {
+		auto entry = getArg(argI);
+		if (entry._type != StackEntryType::Number)
+			error("Expected number in argument %u for kernel call", argI);
+		return entry._number;
+	}
+
+	const char *getStringArg(uint argI) {
+		auto entry = getArg(argI);
+		if (entry._type != StackEntryType::String)
+			error("Expected string in argument %u for kernel call", argI);
+		return _script._strings->data() + entry._index;
+	}
+
+	TaskReturn kernelCall(ScriptKernelTask task) {
+		switch (task) {
+		case ScriptKernelTask::PlayVideo:
+			warning("STUB KERNEL CALL: PlayVideo");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::PlaySound:
+			warning("STUB KERNEL CALL: PlaySound");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::PlayMusic:
+			warning("STUB KERNEL CALL: PlayMusic");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::StopMusic:
+			warning("STUB KERNEL CALL: StopMusic");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::WaitForMusicToEnd:
+			warning("STUB KERNEL CALL: WaitForMusicToEnd");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::ShowCenterBottomText:
+			warning("STUB KERNEL CALL: ShowCenterBottomText");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::StopAndTurn:
+			warning("STUB KERNEL CALL: StopAndTurn");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::StopAndTurnMe:
+			warning("STUB KERNEL CALL: StopAndTurnMe");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::ChangeCharacter:
+			warning("STUB KERNEL CALL: ChangeCharacter");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::SayText:
+			warning("STUB KERNEL CALL: SayText");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::Go:
+			warning("STUB KERNEL CALL: Go");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::Put:
+			warning("STUB KERNEL CALL: Put");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::ChangeCharacterRoom:
+			warning("STUB KERNEL CALL: ChangeCharacterRoom");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::KillProcesses:
+			warning("STUB KERNEL CALL: KillProcesses");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::LerpLodBias:
+			warning("STUB KERNEL CALL: LerpLodBias");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::On:
+			warning("STUB KERNEL CALL: On");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::Off:
+			warning("STUB KERNEL CALL: Off");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::Pickup:
+			warning("STUB KERNEL CALL: Pickup");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::CharacterPickup:
+			warning("STUB KERNEL CALL: CharacterPickup");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::Drop:
+			warning("STUB KERNEL CALL: Drop");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::CharacterDrop:
+			warning("STUB KERNEL CALL: CharacterDrop");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::Delay:
+			return getNumberArg(0) <= 0
+				? TaskReturn::finish(0)
+				: TaskReturn::waitFor(delay((uint32)getNumberArg(0)));
+		case ScriptKernelTask::HadNoMousePressFor:
+			warning("STUB KERNEL CALL: HadNoMousePressFor");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::Fork:
+			g_engine->scheduler().createProcess<ScriptTask>(process().character(), *this);
+			return TaskReturn::finish(0); // 0 means this is the forking process
+		case ScriptKernelTask::Animate:
+			warning("STUB KERNEL CALL: Animate");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::AnimateCharacter:
+			warning("STUB KERNEL CALL: AnimateCharacter");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::AnimateTalking:
+			warning("STUB KERNEL CALL: AnimateTalking");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::ChangeRoom:
+			warning("STUB KERNEL CALL: ChangeRoom");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::ToggleRoomFloor:
+			warning("STUB KERNEL CALL: ToggleRoomFloor");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::SetDialogLineReturn:
+			warning("STUB KERNEL CALL: SetDialogLineReturn");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::DialogMenu:
+			warning("STUB KERNEL CALL: DialogMenu");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::ClearInventory:
+			warning("STUB KERNEL CALL: ClearInventory");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::FadeType0:
+			warning("STUB KERNEL CALL: FadeType0");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::FadeType1:
+			warning("STUB KERNEL CALL: FadeType1");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::SetLodBias:
+			warning("STUB KERNEL CALL: SetLodBias");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::FadeType2:
+			warning("STUB KERNEL CALL: FadeType2");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::SetActiveTextureSet:
+			warning("STUB KERNEL CALL: SetActiveTextureSet");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::SetMaxCamSpeedFactor:
+			warning("STUB KERNEL CALL: SetMaxCamSpeedFactor");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::WaitCamStopping:
+			warning("STUB KERNEL CALL: WaitCamStopping");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::CamFollow:
+			warning("STUB KERNEL CALL: CamFollow");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::CamShake:
+			warning("STUB KERNEL CALL: CamShake");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::LerpCamXY:
+			warning("STUB KERNEL CALL: LerpCamXY");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::LerpCamZ:
+			warning("STUB KERNEL CALL: LerpCamZ");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::LerpCamScale:
+			warning("STUB KERNEL CALL: LerpCamScale");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::LerpCamToObjectWithScale:
+			warning("STUB KERNEL CALL: LerpCamToObjectWithScale");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::LerpCamToObjectResettingZ:
+			warning("STUB KERNEL CALL: LerpCamToObjectResettingZ");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::LerpCamRotation:
+			warning("STUB KERNEL CALL: LerpCamRotation");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::FadeIn:
+			warning("STUB KERNEL CALL: FadeIn");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::FadeOut:
+			warning("STUB KERNEL CALL: FadeOut");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::FadeIn2:
+			warning("STUB KERNEL CALL: FadeIn2");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::FadeOut2:
+			warning("STUB KERNEL CALL: FadeOut2");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::LerpCamXYZ:
+			warning("STUB KERNEL CALL: LerpCamXYZ");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::LerpCamToObjectKeepingZ:
+			warning("STUB KERNEL CALL: LerpCamToObjectKeepingZ");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::Nop10:
+		case ScriptKernelTask::Nop24:
+		case ScriptKernelTask::Nop34:
+			return TaskReturn::finish(0);
+		default:
+			error("Invalid kernel call");
+			return TaskReturn::finish(0);
+		}
+	}
 
+	Script &_script;
 	Stack<StackEntry> _stack;
 	String _name;
 	uint32 _pc;
+	bool _returnsFromKernelCall = false;
 };
 
 Process *Script::createProcess(MainCharacterKind character, const String &behavior, const String &action, bool allowMissing) {
diff --git a/engines/alcachofa/script.h b/engines/alcachofa/script.h
index 1ee23e37809..fa69a57dc2d 100644
--- a/engines/alcachofa/script.h
+++ b/engines/alcachofa/script.h
@@ -75,7 +75,7 @@ enum class ScriptOp {
 };
 
 enum class ScriptKernelTask {
-	PlayVideo,
+	PlayVideo = 1,
 	PlaySound,
 	PlayMusic,
 	StopMusic,
@@ -90,7 +90,7 @@ enum class ScriptKernelTask {
 	Put,
 	ChangeCharacterRoom,
 	KillProcesses,
-	Timer,
+	LerpLodBias,
 	On,
 	Off,
 	Pickup,


Commit: f561316bd8713612c2f4adc10d45504cf244eb60
    https://github.com/scummvm/scummvm/commit/f561316bd8713612c2f4adc10d45504cf244eb60
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:45+02:00

Commit Message:
ALCACHOFA: Move updateCommonVariables to Script

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


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index bac8624a363..61fb9703ce3 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -115,20 +115,4 @@ Common::Error AlcachofaEngine::syncGame(Common::Serializer &s) {
 	return Common::kNoError;
 }
 
-void AlcachofaEngine::updateScriptVariables() {
-	if (_input.wasAnyMousePressed()) // yes, this variable is never reset by the engine
-		_script->variable("SeHaPulsadoRaton") = 1;
-
-	if (_script->variable("CalcularTiempoSinPulsarRaton")) {
-		if (_scriptTimer == 0)
-			_scriptTimer = g_system->getMillis();
-	}
-	else
-		_scriptTimer = 0;
-
-	_script->variable("EstanAmbos") = _world->mortadelo().room() == _world->filemon().room();
-	_script->variable("textoson") = 1; // TODO: Add subtitle option
-	_script->variable("modored") = 1; // this is signalling whether a network connection is established
-}
-
 } // End of namespace Alcachofa
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index 0246823d4c5..98e3e48a3d6 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -68,9 +68,7 @@ public:
 	inline World &world() { return *_world; }
 	inline Script &script() { return *_script; }
 	inline Scheduler &scheduler() { return _scheduler; }
-	inline Console &console() { return *_console; }
-	inline uint32 scriptTimer() const { return _scriptTimer; }
-	void updateScriptVariables();
+	inline Console &console() { return *_console; }	
 
 	uint32 getFeatures() const;
 
@@ -125,8 +123,6 @@ private:
 	Input _input;
 	Player _player;
 	Scheduler _scheduler;
-
-	uint32 _scriptTimer = 0;
 };
 
 extern AlcachofaEngine *g_engine;
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 31938e4c062..cc885d66373 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -148,7 +148,7 @@ void Room::update() {
 }
 
 void Room::updateScripts() {
-	g_engine->updateScriptVariables();
+	g_engine->script().updateCommonVariables();
 	if (!g_engine->scheduler().hasProcessWithName("ACTUALIZAR_" + _name))
 		g_engine->script().createProcess(MainCharacterKind::None, "ACTUALIZAR_" + _name, true);
 	g_engine->scheduler().run();
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 397bdb5a6e1..3c1105d1b0b 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -21,6 +21,7 @@
 
 #include "script.h"
 #include "stream-helper.h"
+#include "rooms.h"
 #include "alcachofa.h"
 
 #include "common/file.h"
@@ -541,4 +542,20 @@ Process *Script::createProcess(MainCharacterKind character, const String &proced
 	return process;
 }
 
+void Script::updateCommonVariables() {
+	if (g_engine->input().wasAnyMousePressed()) // yes, this variable is never reset by the engine
+		variable("SeHaPulsadoRaton") = 1;
+
+	if (variable("CalcularTiempoSinPulsarRaton")) {
+		if (_scriptTimer == 0)
+			_scriptTimer = g_system->getMillis();
+	}
+	else
+		_scriptTimer = 0;
+
+	variable("EstanAmbos") = g_engine->world().mortadelo().room() == g_engine->world().filemon().room();
+	variable("textoson") = 1; // TODO: Add subtitle option
+	variable("modored") = 1; // this is signalling whether a network connection is established
+}
+
 }
diff --git a/engines/alcachofa/script.h b/engines/alcachofa/script.h
index fa69a57dc2d..7a08e79fc58 100644
--- a/engines/alcachofa/script.h
+++ b/engines/alcachofa/script.h
@@ -144,6 +144,7 @@ class Script {
 public:
 	Script();
 
+	void updateCommonVariables();
 	int32 variable(const char *name) const;
 	int32 &variable(const char *name);
 	Process *createProcess(
@@ -163,6 +164,7 @@ private:
 	Common::Array<ScriptInstruction> _instructions;
 	Common::Array<int32> _variables;
 	Common::SpanOwner<Common::Span<char>> _strings;
+	uint32 _scriptTimer = 0;
 };
 
 }


Commit: cb1c9873f03667e6a24cd247fc7832f8a40d2fab
    https://github.com/scummvm/scummvm/commit/cb1c9873f03667e6a24cd247fc7832f8a40d2fab
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:46+02:00

Commit Message:
ALCACHOFA: Implement easy kernel tasks

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


diff --git a/engines/alcachofa/camera.h b/engines/alcachofa/camera.h
index 68d79891524..4e06640361a 100644
--- a/engines/alcachofa/camera.h
+++ b/engines/alcachofa/camera.h
@@ -31,6 +31,8 @@
 namespace Alcachofa {
 
 class Personaje;
+class Process;
+struct Task;
 
 static constexpr const int16_t kBaseScale = 300; ///< this number pops up everywhere in the engine
 static constexpr const float kInvBaseScale = 1.0f / kBaseScale;
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index b5c894997a8..a0baef9e6f1 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -452,6 +452,28 @@ void WalkingCharacter::serializeSave(Serializer &serializer) {
 	syncEnum(serializer, _direction);
 }
 
+struct ArriveTask : public Task {
+	ArriveTask(Process &process, const WalkingCharacter &character)
+		: Task(process)
+		, _character(character) {}
+
+	virtual TaskReturn run() override {
+		return _character._isWalking
+			? TaskReturn::yield()
+			: TaskReturn::finish(1);
+	}
+
+	virtual void debugPrint() override {
+		g_engine->getDebugger()->debugPrintf("Wait for %s to arrive", _character.name().c_str());
+	}
+private:
+	const WalkingCharacter &_character;
+};
+
+Task *WalkingCharacter::waitForArrival(Process &process) {
+	return new ArriveTask(process, *this);
+}
+
 MainCharacter::MainCharacter(Room *room, ReadStream &stream)
 	: WalkingCharacter(room, stream) {
 	stream.readByte(); // unused byte
@@ -569,6 +591,48 @@ void MainCharacter::serializeSave(Serializer &serializer) {
 	}
 }
 
+void MainCharacter::clearInventory() {
+	for (auto *item : _items)
+		item->toggle(false);
+	// TODO: Clear held item on clearInventory
+	g_engine->world().inventory().updateItemsByActiveCharacter();
+}
+
+Item *MainCharacter::getItemByName(const String &name) const {
+	for (auto *item : _items) {
+		if (item->name() == name)
+			return item;
+	}
+	return nullptr;
+}
+
+bool MainCharacter::hasItem(const String &name) const {
+	auto item = getItemByName(name);
+	return item == nullptr || item->isEnabled();
+}
+
+void MainCharacter::pickup(const String &name, bool putInHand) {
+	auto item = getItemByName(name);
+	if (item == nullptr)
+		error("Tried to pickup unknown item: %s", name.c_str());
+	item->toggle(true);
+	if (g_engine->world().activeCharacter() == this) {
+		// TODO: Put item in hand for pickup
+		g_engine->world().inventory().updateItemsByActiveCharacter();
+	}
+}
+
+void MainCharacter::drop(const Common::String &name) {
+	auto item = getItemByName(name);
+	if (item == nullptr)
+		error("Tried to drop unknown item: %s", name.c_str());
+	item->toggle(false);
+	if (g_engine->world().activeCharacter() == this) {
+		// TODO: Clear held item for drop
+		g_engine->world().inventory().updateItemsByActiveCharacter();
+	}
+}
+
 Background::Background(Room *room, const String &animationFileName, int16 scale)
 	: GraphicObject(room, "BACKGROUND") {
 	toggle(true);
diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index f16fc2c56ed..0756be39745 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -21,6 +21,7 @@
 
 #include "objects.h"
 #include "rooms.h"
+#include "scheduler.h"
 #include "stream-helper.h"
 #include "alcachofa.h"
 
@@ -122,6 +123,39 @@ Graphic *GraphicObject::graphic() {
 	return &_graphic;
 }
 
+struct AnimateTask : public Task {
+	AnimateTask(Process &process, GraphicObject *object)
+		: Task(process)
+		, _object(object) {
+		assert(_object != nullptr);
+		_graphic = object->graphic();
+		assert(_graphic != nullptr);
+		_duration = _graphic->animation().totalDuration();
+	}
+
+	virtual TaskReturn run() override {
+		TASK_BEGIN;
+		_object->toggle(true);
+		_graphic->start(false);
+		TASK_WAIT(delay(_duration));
+		_object->toggle(false);
+		TASK_END;
+	}
+
+	virtual void debugPrint() override {
+		g_engine->getDebugger()->debugPrintf("Animate \"%s\" for %ums", _object->name().c_str(), _duration);
+	}
+
+private:
+	GraphicObject *_object;
+	Graphic *_graphic;
+	uint32 _duration;
+};
+
+Task *GraphicObject::animate(Process &process) {
+	return new AnimateTask(process, this);
+}
+
 SpecialEffectObject::SpecialEffectObject(Room *room, ReadStream &stream)
 	: GraphicObject(room, stream) {
 	_topLeft = Shape(stream).firstPoint();
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 4ceb3f8f7a8..92f0158bd14 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -30,6 +30,8 @@
 namespace Alcachofa {
 
 class Room;
+class Process;
+struct Task;
 
 class ObjectBase {
 public:
@@ -90,6 +92,8 @@ public:
 	virtual void serializeSave(Common::Serializer &serializer) override;
 	virtual Graphic *graphic() override;
 
+	Task *animate(Process &process);
+
 protected:
 	GraphicObject(Room *room, const char *name);
 
@@ -368,7 +372,10 @@ public:
 	void stopWalkingAndTurn(Direction direction);
 	void setPosition(const Common::Point &target);
 
+	Task *waitForArrival(Process &process);
+
 protected:
+	friend struct ArriveTask;
 	virtual void onArrived();
 	void updateWalking();
 	void updateWalkingAnimation();
@@ -425,11 +432,16 @@ public:
 		Direction endDirection = Direction::Invalid,
 		ITriggerableObject *activateObject = nullptr,
 		const char *activateAction = nullptr) override;
+	void clearInventory();
+	bool hasItem(const Common::String &name) const;
+	void pickup(const Common::String &name, bool putInHand);
+	void drop(const Common::String &name);
 
 protected:
 	virtual void onArrived() override;
 
 private:
+	Item *getItemByName(const Common::String &name) const;
 	void drawInner();
 
 	Common::Array<Item *> _items;
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index cc885d66373..c0f4359de05 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -225,6 +225,10 @@ void Room::serializeSave(Serializer &serializer) {
 		object->serializeSave(serializer);
 }
 
+void Room::toggleActiveFloor() {
+	_activeFloorI ^= 1;
+}
+
 OptionsMenu::OptionsMenu(World *world, ReadStream &stream)
 	: Room(world, stream, true) {
 }
@@ -246,6 +250,13 @@ Inventory::~Inventory() {
 		delete item;
 }
 
+void Inventory::updateItemsByActiveCharacter() {
+	auto *character = world().activeCharacter();
+	assert(character != nullptr);
+	for (auto *item : _items)
+		item->toggle(character->hasItem(item->name()));
+}
+
 static constexpr const char *kMapFiles[] = {
 	"MAPAS/MAPA5.EMC",
 	"MAPAS/MAPA4.EMC",
@@ -309,6 +320,40 @@ ObjectBase *World::getObjectByName(const Common::String &name) const {
 	return result;
 }
 
+ObjectBase *World::getObjectByName(MainCharacterKind character, const Common::String &name) const {
+	if (character == MainCharacterKind::None)
+		return getObjectByName(name);
+	ObjectBase *result = nullptr;
+	if (activeCharacterKind() == character && currentRoom() == activeCharacter()->room())
+		result = currentRoom()->getObjectByName(name);
+	if (result == nullptr)
+		result = activeCharacter()->room()->getObjectByName(name);
+	if (result == nullptr)
+		result = globalRoom().getObjectByName(name);
+	if (result == nullptr)
+		result = inventory().getObjectByName(name);
+	return result;
+}
+
+ObjectBase *World::getObjectByNameFromAnyRoom(const Common::String &name) const {
+	for (auto *room : _rooms) {
+		ObjectBase *result = room->getObjectByName(name);
+		if (result != nullptr)
+			return result;
+	}
+	return nullptr;
+}
+
+void World::toggleObject(MainCharacterKind character, const Common::String &objName, bool isEnabled) {
+	ObjectBase *object = getObjectByName(character, objName);
+	if (object == nullptr)
+		object = getObjectByNameFromAnyRoom(objName);
+	if (object == nullptr)
+		error("Tried to toggle unknown object: %s", objName.c_str());
+	else
+		object->toggle(isEnabled);
+}
+
 const Common::String &World::getGlobalAnimationName(GlobalAnimationKind kind) const {
 	int kindI = (int)kind;
 	assert(kindI >= 0 && kindI < (int)GlobalAnimationKind::Count);
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index b7be6a583b0..605a7df9d6b 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -54,6 +54,7 @@ public:
 	virtual void freeResources();
 	virtual void serializeSave(Common::Serializer &serializer);
 	ObjectBase *getObjectByName(const Common::String &name) const;
+	void toggleActiveFloor();
 
 protected:
 	Room(World *world, Common::ReadStream &stream, bool hasUselessByte);
@@ -101,6 +102,8 @@ public:
 	Inventory(World *world, Common::ReadStream &stream);
 	virtual ~Inventory() override;
 
+	void updateItemsByActiveCharacter();
+
 private:
 	Common::Array<Item *> _items;
 };
@@ -141,12 +144,19 @@ public:
 		return filemon().currentlyUsing() == object ||
 			mortadelo().currentlyUsing() == object;
 	}
+	inline MainCharacterKind activeCharacterKind() const {
+		return _activeCharacter == nullptr ? MainCharacterKind::None : _activeCharacter->kind();
+	}
 
 	MainCharacter &getMainCharacterByKind(MainCharacterKind kind) const;
 	Room *getRoomByName(const Common::String &name) const;
 	ObjectBase *getObjectByName(const Common::String &name) const;
+	ObjectBase *getObjectByName(MainCharacterKind character, const Common::String &name) const;
+	ObjectBase *getObjectByNameFromAnyRoom(const Common::String &name) const;
 	const Common::String &getGlobalAnimationName(GlobalAnimationKind kind) const;
 
+	void toggleObject(MainCharacterKind character, const Common::String &objName, bool isEnabled);
+
 private:
 	bool loadWorldFile(const char *path);
 
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 3c1105d1b0b..69e9659536a 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -102,6 +102,31 @@ int32 &Script::variable(const char *name) {
 	return _variables[index];
 }
 
+struct ScriptTimerTask : public Task {
+	ScriptTimerTask(Process &process, int32 durationSec)
+		: Task(process)
+		, _durationSec(durationSec) {}
+
+	virtual TaskReturn run() override {
+		TASK_BEGIN;
+		if (_durationSec >= (int32)((g_system->getMillis() - g_engine->script()._scriptTimer) / 1000) &&
+			g_engine->script().variable("SeHaPulsadoRaton"))
+			_result = 0;
+		
+		// TODO: Add network behavior for script timer
+		TASK_YIELD;
+		TASK_END;
+	}
+
+	virtual void debugPrint() override {
+		g_engine->getDebugger()->debugPrintf("Check input timer for %dsecs", _durationSec);
+	}
+
+private:
+	int32 _durationSec;
+	int32 _result = 1;
+};
+
 enum class StackEntryType {
 	Number,
 	Variable,
@@ -347,6 +372,17 @@ private:
 		return _script._strings->data() + entry._index;
 	}
 
+	MainCharacter &relatedCharacter() {
+		if (process().character() == MainCharacterKind::None)
+			error("Script tried to use character from non-character-related process");
+		return g_engine->world().getMainCharacterByKind(process().character());
+	}
+
+	bool shouldSkipCutscene() {
+		return process().character() != MainCharacterKind::None &&
+			g_engine->world().activeCharacterKind() != process().character();
+	}
+
 	TaskReturn kernelCall(ScriptKernelTask task) {
 		switch (task) {
 		case ScriptKernelTask::PlayVideo:
@@ -367,64 +403,107 @@ private:
 		case ScriptKernelTask::ShowCenterBottomText:
 			warning("STUB KERNEL CALL: ShowCenterBottomText");
 			return TaskReturn::finish(0);
-		case ScriptKernelTask::StopAndTurn:
-			warning("STUB KERNEL CALL: StopAndTurn");
-			return TaskReturn::finish(0);
-		case ScriptKernelTask::StopAndTurnMe:
-			warning("STUB KERNEL CALL: StopAndTurnMe");
-			return TaskReturn::finish(0);
+		case ScriptKernelTask::StopAndTurn: {
+			auto object = g_engine->world().getObjectByName(process().character(), getStringArg(0));
+			auto character = dynamic_cast<WalkingCharacter *>(object);
+			if (character == nullptr)
+				error("Script tried to stop-and-turn unknown character");
+			else
+				character->stopWalkingAndTurn((Direction)getNumberArg(1));
+			return TaskReturn::finish(1);
+		}
+		case ScriptKernelTask::StopAndTurnMe: {
+			relatedCharacter().stopWalkingAndTurn((Direction)getNumberArg(0));
+			return TaskReturn::finish(1);
+		}
 		case ScriptKernelTask::ChangeCharacter:
 			warning("STUB KERNEL CALL: ChangeCharacter");
 			return TaskReturn::finish(0);
 		case ScriptKernelTask::SayText:
 			warning("STUB KERNEL CALL: SayText");
 			return TaskReturn::finish(0);
-		case ScriptKernelTask::Go:
-			warning("STUB KERNEL CALL: Go");
-			return TaskReturn::finish(0);
-		case ScriptKernelTask::Put:
-			warning("STUB KERNEL CALL: Put");
-			return TaskReturn::finish(0);
+		case ScriptKernelTask::Go: {
+			auto characterObject = g_engine->world().getObjectByName(process().character(), getStringArg(0));
+			auto character = dynamic_cast<WalkingCharacter *>(characterObject);
+			if (character == nullptr)
+				error("Script tried to make invalid character go: %s", getStringArg(0));
+			auto targetObject = g_engine->world().getObjectByName(process().character(), getStringArg(1));
+			auto target = dynamic_cast<PointObject *>(targetObject);
+			if (target == nullptr)
+				error("Script tried to make character go to invalid object %s", getStringArg(1));
+			character->walkTo(target->position());
+
+			// TODO: if (flags & 2) g_engine->camera().setFollow(nullptr);
+
+			return (getNumberArg(2) & 1)
+				? TaskReturn::finish(1)
+				: TaskReturn::waitFor(character->waitForArrival(process()));
+		}
+		case ScriptKernelTask::Put: {
+			auto characterObject = g_engine->world().getObjectByName(process().character(), getStringArg(0));
+			auto character = dynamic_cast<WalkingCharacter *>(characterObject);
+			if (character == nullptr)
+				error("Script tried to make invalid character go: %s", getStringArg(0));
+			auto targetObject = g_engine->world().getObjectByName(process().character(), getStringArg(1));
+			auto target = dynamic_cast<PointObject *>(targetObject);
+			if (target == nullptr)
+				error("Script tried to make character go to invalid object %s", getStringArg(1));
+			character->setPosition(target->position());
+			return TaskReturn::finish(1);
+		}
 		case ScriptKernelTask::ChangeCharacterRoom:
 			warning("STUB KERNEL CALL: ChangeCharacterRoom");
 			return TaskReturn::finish(0);
 		case ScriptKernelTask::KillProcesses:
 			warning("STUB KERNEL CALL: KillProcesses");
 			return TaskReturn::finish(0);
-		case ScriptKernelTask::LerpLodBias:
-			warning("STUB KERNEL CALL: LerpLodBias");
+		case ScriptKernelTask::LerpCharacterLodBias:
+			warning("STUB KERNEL CALL: LerpCharacterLodBias");
 			return TaskReturn::finish(0);
 		case ScriptKernelTask::On:
-			warning("STUB KERNEL CALL: On");
+			g_engine->world().toggleObject(process().character(), getStringArg(0), true);
 			return TaskReturn::finish(0);
 		case ScriptKernelTask::Off:
-			warning("STUB KERNEL CALL: Off");
+			g_engine->world().toggleObject(process().character(), getStringArg(0), false);
 			return TaskReturn::finish(0);
 		case ScriptKernelTask::Pickup:
-			warning("STUB KERNEL CALL: Pickup");
-			return TaskReturn::finish(0);
-		case ScriptKernelTask::CharacterPickup:
-			warning("STUB KERNEL CALL: CharacterPickup");
-			return TaskReturn::finish(0);
+			relatedCharacter().pickup(getStringArg(0), getNumberArg(1));
+			return TaskReturn::finish(1);
+		case ScriptKernelTask::CharacterPickup: {
+			auto &character = g_engine->world().getMainCharacterByKind((MainCharacterKind)getNumberArg(1));
+			character.pickup(getStringArg(0), getNumberArg(2));
+			return TaskReturn::finish(1);
+		}
 		case ScriptKernelTask::Drop:
-			warning("STUB KERNEL CALL: Drop");
-			return TaskReturn::finish(0);
-		case ScriptKernelTask::CharacterDrop:
-			warning("STUB KERNEL CALL: CharacterDrop");
-			return TaskReturn::finish(0);
+			relatedCharacter().drop(getStringArg(0));
+			return TaskReturn::finish(1);
+		case ScriptKernelTask::CharacterDrop: {
+			auto &character = g_engine->world().getMainCharacterByKind((MainCharacterKind)getNumberArg(1));
+			character.drop(getStringArg(0));
+			return TaskReturn::finish(1);
+		}
 		case ScriptKernelTask::Delay:
 			return getNumberArg(0) <= 0
 				? TaskReturn::finish(0)
 				: TaskReturn::waitFor(delay((uint32)getNumberArg(0)));
 		case ScriptKernelTask::HadNoMousePressFor:
-			warning("STUB KERNEL CALL: HadNoMousePressFor");
-			return TaskReturn::finish(0);
+			return TaskReturn::waitFor(new ScriptTimerTask(process(), getNumberArg(0)));
 		case ScriptKernelTask::Fork:
 			g_engine->scheduler().createProcess<ScriptTask>(process().character(), *this);
 			return TaskReturn::finish(0); // 0 means this is the forking process
-		case ScriptKernelTask::Animate:
-			warning("STUB KERNEL CALL: Animate");
-			return TaskReturn::finish(0);
+		case ScriptKernelTask::Animate: {
+			auto object = g_engine->world().getObjectByName(process().character(), getStringArg(0));
+			auto graphicObject = dynamic_cast<GraphicObject *>(object);
+			if (graphicObject == nullptr)
+				error("Script tried to animate invalid graphic object %s", getStringArg(0));
+			if (getNumberArg(1)) {
+				graphicObject->toggle(true);
+				graphicObject->graphic()->start(false);
+				return TaskReturn::finish(1);
+			}
+			else
+				return TaskReturn::waitFor(graphicObject->animate(process()));
+		}
 		case ScriptKernelTask::AnimateCharacter:
 			warning("STUB KERNEL CALL: AnimateCharacter");
 			return TaskReturn::finish(0);
@@ -435,8 +514,13 @@ private:
 			warning("STUB KERNEL CALL: ChangeRoom");
 			return TaskReturn::finish(0);
 		case ScriptKernelTask::ToggleRoomFloor:
-			warning("STUB KERNEL CALL: ToggleRoomFloor");
-			return TaskReturn::finish(0);
+			if (process().character() == MainCharacterKind::None) {
+				if (g_engine->world().currentRoom() != nullptr)
+					g_engine->world().currentRoom()->toggleActiveFloor();
+			}
+			else
+				g_engine->world().getMainCharacterByKind(process().character()).room()->toggleActiveFloor();
+			return TaskReturn::finish(1);
 		case ScriptKernelTask::SetDialogLineReturn:
 			warning("STUB KERNEL CALL: SetDialogLineReturn");
 			return TaskReturn::finish(0);
@@ -444,23 +528,24 @@ private:
 			warning("STUB KERNEL CALL: DialogMenu");
 			return TaskReturn::finish(0);
 		case ScriptKernelTask::ClearInventory:
-			warning("STUB KERNEL CALL: ClearInventory");
-			return TaskReturn::finish(0);
+			switch((MainCharacterKind)getNumberArg(0)) {
+			case MainCharacterKind::Mortadelo: g_engine->world().mortadelo().clearInventory();
+			case MainCharacterKind::Filemon: g_engine->world().filemon().clearInventory();
+			default: error("Script attempted to clear inventory with invalid character kind");
+			}
+			return TaskReturn::finish(1);
 		case ScriptKernelTask::FadeType0:
 			warning("STUB KERNEL CALL: FadeType0");
 			return TaskReturn::finish(0);
 		case ScriptKernelTask::FadeType1:
 			warning("STUB KERNEL CALL: FadeType1");
 			return TaskReturn::finish(0);
-		case ScriptKernelTask::SetLodBias:
-			warning("STUB KERNEL CALL: SetLodBias");
+		case ScriptKernelTask::LerpWorldLodBias:
+			warning("STUB KERNEL CALL: LerpWorldLodBias");
 			return TaskReturn::finish(0);
 		case ScriptKernelTask::FadeType2:
 			warning("STUB KERNEL CALL: FadeType2");
 			return TaskReturn::finish(0);
-		case ScriptKernelTask::SetActiveTextureSet:
-			warning("STUB KERNEL CALL: SetActiveTextureSet");
-			return TaskReturn::finish(0);
 		case ScriptKernelTask::SetMaxCamSpeedFactor:
 			warning("STUB KERNEL CALL: SetMaxCamSpeedFactor");
 			return TaskReturn::finish(0);
@@ -509,6 +594,10 @@ private:
 		case ScriptKernelTask::LerpCamToObjectKeepingZ:
 			warning("STUB KERNEL CALL: LerpCamToObjectKeepingZ");
 			return TaskReturn::finish(0);
+		case ScriptKernelTask::SetActiveTextureSet:
+			// Fortunately this seems to be unused.
+			warning("STUB KERNEL CALL: SetActiveTextureSet");
+			return TaskReturn::finish(0);
 		case ScriptKernelTask::Nop10:
 		case ScriptKernelTask::Nop24:
 		case ScriptKernelTask::Nop34:
diff --git a/engines/alcachofa/script.h b/engines/alcachofa/script.h
index 7a08e79fc58..827726f2018 100644
--- a/engines/alcachofa/script.h
+++ b/engines/alcachofa/script.h
@@ -90,7 +90,7 @@ enum class ScriptKernelTask {
 	Put,
 	ChangeCharacterRoom,
 	KillProcesses,
-	LerpLodBias,
+	LerpCharacterLodBias,
 	On,
 	Off,
 	Pickup,
@@ -112,7 +112,7 @@ enum class ScriptKernelTask {
 	Nop34,
 	FadeType0,
 	FadeType1,
-	SetLodBias,
+	LerpWorldLodBias,
 	FadeType2,
 	SetActiveTextureSet,
 	SetMaxCamSpeedFactor,
@@ -159,6 +159,7 @@ public:
 
 private:
 	friend struct ScriptTask;
+	friend struct ScriptTimerTask;
 	Common::HashMap<Common::String, uint32> _variableNames;
 	Common::HashMap<Common::String, uint32> _procedures;
 	Common::Array<ScriptInstruction> _instructions;


Commit: 0e38466e3573387d32d473ce43902dff09bc8ce4
    https://github.com/scummvm/scummvm/commit/0e38466e3573387d32d473ce43902dff09bc8ce4
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:46+02:00

Commit Message:
ALCACHOFA: Fix kernel call animate not accepting string as boolean arg

Changed paths:
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 69e9659536a..5a7c130c586 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -372,6 +372,15 @@ private:
 		return _script._strings->data() + entry._index;
 	}
 
+	int32 getNumberOrStringArg(uint argI) {
+		// Original inconsistency: sometimes a string is passed instead of a number
+		// as it will be interpreted as a boolean we only care about == 0 / != 0
+		auto entry = getArg(argI);
+		if (entry._type != StackEntryType::Number && entry._type != StackEntryType::String)
+			error("Expected number of string in argument %u for kernel call", argI);
+		return entry._number;
+	}
+
 	MainCharacter &relatedCharacter() {
 		if (process().character() == MainCharacterKind::None)
 			error("Script tried to use character from non-character-related process");
@@ -496,7 +505,7 @@ private:
 			auto graphicObject = dynamic_cast<GraphicObject *>(object);
 			if (graphicObject == nullptr)
 				error("Script tried to animate invalid graphic object %s", getStringArg(0));
-			if (getNumberArg(1)) {
+			if (getNumberOrStringArg(1)) {
 				graphicObject->toggle(true);
 				graphicObject->graphic()->start(false);
 				return TaskReturn::finish(1);


Commit: bb2848b3715f3b5154796a2215bd09517cecb63d
    https://github.com/scummvm/scummvm/commit/bb2848b3715f3b5154796a2215bd09517cecb63d
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:46+02:00

Commit Message:
ALCACHOFA: Move changing state into Player instead of World

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


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 61fb9703ce3..cb7fe5cba19 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -68,7 +68,7 @@ Common::Error AlcachofaEngine::run() {
 
 	auto room = world().getRoomByName("MAPA_TERROR");
 	assert(room != nullptr);
-	world().currentRoom() = room;
+	player().currentRoom() = room;
 	room->loadResources();
 
 	// If a savegame was selected from the launcher, load it
@@ -91,7 +91,7 @@ Common::Error AlcachofaEngine::run() {
 		_drawQueue->clear();
 		_camera.shake() = Vector2d();
 
-		world().currentRoom()->update();
+		player().currentRoom()->update();
 
 		_renderer->end();
 
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index a0baef9e6f1..928f5017170 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -239,7 +239,7 @@ void WalkingCharacter::update() {
 
 	_interactionPoint = _currentPos;
 	_interactionDirection = Direction::Right;
-	if (this != g_engine->world().activeCharacter()) {
+	if (this != g_engine->player().activeCharacter()) {
 		int16 interactionOffset = (int16)(150 * _graphicNormal.depthScale());
 		_interactionPoint.x -= interactionOffset;
 		if (activeFloor != nullptr && activeFloor->polygonContaining(_interactionPoint) < 0) {
@@ -514,7 +514,7 @@ void MainCharacter::onArrived() {
 	_activateAction = nullptr;
 
 	stopWalkingAndTurn(activateObject->interactionDirection());
-	if (g_engine->world().activeCharacter() == this)
+	if (g_engine->player().activeCharacter() == this)
 		activateObject->trigger(activateAction);
 }
 
@@ -527,7 +527,7 @@ void MainCharacter::walkTo(
 	// TODO: Add collision avoidance
 
 	WalkingCharacter::walkTo(target, endDirection, activateObject, activateAction);
-	if (this == g_engine->world().activeCharacter()) {
+	if (this == g_engine->player().activeCharacter()) {
 		// TODO: Add camera following character
 	}
 }
@@ -546,7 +546,7 @@ void MainCharacter::draw() {
 }
 
 void MainCharacter::drawInner() {
-	if (room() != g_engine->world().currentRoom() || !isEnabled())
+	if (room() != g_engine->player().currentRoom() || !isEnabled())
 		return;
 	Graphic *activeGraphic = graphicOf(_curAnimateObject);
 	if (activeGraphic == nullptr && _isWalking) {
@@ -616,7 +616,7 @@ void MainCharacter::pickup(const String &name, bool putInHand) {
 	if (item == nullptr)
 		error("Tried to pickup unknown item: %s", name.c_str());
 	item->toggle(true);
-	if (g_engine->world().activeCharacter() == this) {
+	if (g_engine->player().activeCharacter() == this) {
 		// TODO: Put item in hand for pickup
 		g_engine->world().inventory().updateItemsByActiveCharacter();
 	}
@@ -627,7 +627,7 @@ void MainCharacter::drop(const Common::String &name) {
 	if (item == nullptr)
 		error("Tried to drop unknown item: %s", name.c_str());
 	item->toggle(false);
-	if (g_engine->world().activeCharacter() == this) {
+	if (g_engine->player().activeCharacter() == this) {
 		// TODO: Clear held item for drop
 		g_engine->world().inventory().updateItemsByActiveCharacter();
 	}
diff --git a/engines/alcachofa/player.h b/engines/alcachofa/player.h
index b0abfa21f7e..10b35706717 100644
--- a/engines/alcachofa/player.h
+++ b/engines/alcachofa/player.h
@@ -22,16 +22,27 @@
 #ifndef PLAYER_H
 #define PLAYER_H
 
-namespace Alcachofa {
+#include "rooms.h"
 
-class ShapeObject;
+namespace Alcachofa {
 
 class Player {
 public:
-    inline ShapeObject *selectedObject() { return _selectedObject; }
+	inline Room *currentRoom() const { return _currentRoom; }
+	inline Room *&currentRoom() { return _currentRoom; }
+	inline MainCharacter *activeCharacter() const { return _activeCharacter; }
+    inline ShapeObject *selectedObject() const { return _selectedObject; }
+	inline Item *heldItem() const { return _heldItem; }
+
+	inline MainCharacterKind activeCharacterKind() const {
+		return _activeCharacter == nullptr ? MainCharacterKind::None : _activeCharacter->kind();
+	}
 
 private:
+	Room *_currentRoom = nullptr;
+	MainCharacter *_activeCharacter;
     ShapeObject *_selectedObject = nullptr;
+	Item *_heldItem = nullptr;
 };
 
 }
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index c0f4359de05..80f96791948 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -128,15 +128,15 @@ ObjectBase *Room::getObjectByName(const Common::String &name) const {
 void Room::update() {
 	updateScripts();
 
-	if (world().currentRoom() == this) {
+	if (g_engine->player().currentRoom() == this) {
 		updateRoomBounds();
 		updateInput();
 	}
 	// TODO: Add condition for global room update
 	world().globalRoom().updateObjects();
-	if (world().currentRoom() == this)
+	if (g_engine->player().currentRoom() == this)
 		updateObjects();
-	if (world().currentRoom() == this) {
+	if (g_engine->player().currentRoom() == this) {
 		g_engine->camera().update();
 		drawObjects();
 		world().globalRoom().drawObjects();
@@ -183,10 +183,10 @@ void Room::updateRoomBounds() {
 }
 
 void Room::updateObjects() {
-	const auto *previousRoom = world().currentRoom();
+	const auto *previousRoom = g_engine->player().currentRoom();
 	for (auto *object : _objects) {
 		object->update();
-		if (world().currentRoom() != previousRoom)
+		if (g_engine->player().currentRoom() != previousRoom)
 			return;
 	}
 }
@@ -251,7 +251,7 @@ Inventory::~Inventory() {
 }
 
 void Inventory::updateItemsByActiveCharacter() {
-	auto *character = world().activeCharacter();
+	auto *character = g_engine->player().activeCharacter();
 	assert(character != nullptr);
 	for (auto *item : _items)
 		item->toggle(character->hasItem(item->name()));
@@ -311,8 +311,8 @@ Room *World::getRoomByName(const Common::String &name) const {
 
 ObjectBase *World::getObjectByName(const Common::String &name) const {
 	ObjectBase *result = nullptr;
-	if (result == nullptr && _currentRoom != nullptr)
-		result = _currentRoom->getObjectByName(name);
+	if (result == nullptr && g_engine->player().currentRoom() != nullptr)
+		result = g_engine->player().currentRoom()->getObjectByName(name);
 	if (result == nullptr)
 		result = globalRoom().getObjectByName(name);
 	if (result == nullptr)
@@ -323,11 +323,12 @@ ObjectBase *World::getObjectByName(const Common::String &name) const {
 ObjectBase *World::getObjectByName(MainCharacterKind character, const Common::String &name) const {
 	if (character == MainCharacterKind::None)
 		return getObjectByName(name);
+	const auto &player = g_engine->player();
 	ObjectBase *result = nullptr;
-	if (activeCharacterKind() == character && currentRoom() == activeCharacter()->room())
-		result = currentRoom()->getObjectByName(name);
+	if (player.activeCharacterKind() == character && player.currentRoom() == player.activeCharacter()->room())
+		result = player.currentRoom()->getObjectByName(name);
 	if (result == nullptr)
-		result = activeCharacter()->room()->getObjectByName(name);
+		result = player.activeCharacter()->room()->getObjectByName(name);
 	if (result == nullptr)
 		result = globalRoom().getObjectByName(name);
 	if (result == nullptr)
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index 605a7df9d6b..d06ca35d565 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -133,20 +133,13 @@ public:
 	inline Inventory &inventory() const { return *_inventory; }
 	inline MainCharacter &filemon() const { return *_filemon; }
 	inline MainCharacter &mortadelo() const { return *_mortadelo; }
-	inline MainCharacter *activeCharacter() const { return _activeCharacter; }
 	inline const Common::String &initScriptName() const { return _initScriptName; }
 	inline uint8 loadedMapCount() const { return _loadedMapCount; }
 
-	inline Room *&currentRoom() { return _currentRoom; }
-	inline Room *currentRoom() const { return _currentRoom; }
-
 	inline bool somebodyUsing(ObjectBase *object) const {
 		return filemon().currentlyUsing() == object ||
 			mortadelo().currentlyUsing() == object;
 	}
-	inline MainCharacterKind activeCharacterKind() const {
-		return _activeCharacter == nullptr ? MainCharacterKind::None : _activeCharacter->kind();
-	}
 
 	MainCharacter &getMainCharacterByKind(MainCharacterKind kind) const;
 	Room *getRoomByName(const Common::String &name) const;
@@ -163,9 +156,9 @@ private:
 	Common::Array<Room *> _rooms;
 	Common::String _globalAnimationNames[(int)GlobalAnimationKind::Count];
 	Common::String _initScriptName;
-	Room *_globalRoom, *_currentRoom = nullptr;
+	Room *_globalRoom;
 	Inventory *_inventory;
-	MainCharacter *_filemon, *_mortadelo, *_activeCharacter = nullptr;
+	MainCharacter *_filemon, *_mortadelo;
 	uint8 _loadedMapCount = 0;
 };
 
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 5a7c130c586..d27be2671e2 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -389,7 +389,7 @@ private:
 
 	bool shouldSkipCutscene() {
 		return process().character() != MainCharacterKind::None &&
-			g_engine->world().activeCharacterKind() != process().character();
+			g_engine->player().activeCharacterKind() != process().character();
 	}
 
 	TaskReturn kernelCall(ScriptKernelTask task) {
@@ -524,8 +524,8 @@ private:
 			return TaskReturn::finish(0);
 		case ScriptKernelTask::ToggleRoomFloor:
 			if (process().character() == MainCharacterKind::None) {
-				if (g_engine->world().currentRoom() != nullptr)
-					g_engine->world().currentRoom()->toggleActiveFloor();
+				if (g_engine->player().currentRoom() != nullptr)
+					g_engine->player().currentRoom()->toggleActiveFloor();
 			}
 			else
 				g_engine->world().getMainCharacterByKind(process().character()).room()->toggleActiveFloor();
@@ -538,9 +538,9 @@ private:
 			return TaskReturn::finish(0);
 		case ScriptKernelTask::ClearInventory:
 			switch((MainCharacterKind)getNumberArg(0)) {
-			case MainCharacterKind::Mortadelo: g_engine->world().mortadelo().clearInventory();
-			case MainCharacterKind::Filemon: g_engine->world().filemon().clearInventory();
-			default: error("Script attempted to clear inventory with invalid character kind");
+			case MainCharacterKind::Mortadelo: g_engine->world().mortadelo().clearInventory(); break;
+			case MainCharacterKind::Filemon: g_engine->world().filemon().clearInventory(); break;
+			default: error("Script attempted to clear inventory with invalid character kind"); break;
 			}
 			return TaskReturn::finish(1);
 		case ScriptKernelTask::FadeType0:


Commit: 24dbc3852808912f3c66adb841e515c4c9d0ad3e
    https://github.com/scummvm/scummvm/commit/24dbc3852808912f3c66adb841e515c4c9d0ad3e
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:46+02:00

Commit Message:
ALCACHOFA: Initialize items

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


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 928f5017170..9c3bff96b41 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -34,6 +34,14 @@ Item::Item(Room *room, ReadStream &stream)
 	stream.readByte(); // unused and ignored byte
 }
 
+Item::Item(const Item &other)
+	: GraphicObject(other.room(), other.name().c_str()) {
+	_type = other._type;
+	_posterizeAlpha = other._posterizeAlpha;
+	_graphic.~Graphic();
+	new (&_graphic) Graphic(other._graphic);
+}
+
 ITriggerableObject::ITriggerableObject(ReadStream &stream)
 	: _interactionPoint(Shape(stream).firstPoint())
 	, _interactionDirection((Direction)stream.readSint32LE()) {}
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 60af943f46a..baf21976bbc 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -373,6 +373,18 @@ Graphic::Graphic(ReadStream &stream) {
 		setAnimation(animationName, AnimationFolder::Animations);
 }
 
+Graphic::Graphic(const Graphic &other)
+	: _animation(other._animation)
+	, _center(other._center)
+	, _scale(other._scale)
+	, _order(other._order)
+	, _color(other._color)
+	, _isPaused(other._isPaused)
+	, _isLooping(other._isLooping)
+	, _lastTime(other._lastTime)
+	, _frameI(other._frameI)
+	, _depthScale(other._depthScale) {}
+
 void Graphic::loadResources() {
 	if (_animation != nullptr)
 		_animation->load();
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 191c1ba9896..7eb866456d0 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -220,6 +220,7 @@ class Graphic {
 public:
 	Graphic();
 	Graphic(Common::ReadStream &stream);
+	Graphic(const Graphic &other); // animation reference is taken, so keep other alive
 
 	inline Common::Point &center() { return _center; }
 	inline int8 &order() { return _order; }
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 92f0158bd14..90db2a1ce80 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -283,6 +283,7 @@ class Item : public GraphicObject {
 public:
 	static constexpr const char *kClassName = "CObjetoInventario";
 	Item(Room *room, Common::ReadStream &stream);
+	Item(const Item &other);
 };
 
 class ITriggerableObject {
@@ -341,7 +342,7 @@ protected:
 	void syncObjectAsString(Common::Serializer &serializer, ObjectBase *&object);
 	void updateTalkingAnimation();
 
-	Direction _direction;
+	Direction _direction = Direction::Right;
 	Graphic _graphicNormal, _graphicTalking;
 
 	bool _isTalking = false;
@@ -441,6 +442,7 @@ protected:
 	virtual void onArrived() override;
 
 private:
+	friend class Inventory;
 	Item *getItemByName(const Common::String &name) const;
 	void drawInner();
 
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 80f96791948..3cf924a76d2 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -250,6 +250,19 @@ Inventory::~Inventory() {
 		delete item;
 }
 
+void Inventory::initItems() {
+	auto &mortadelo = world().mortadelo();
+	auto &filemon = world().filemon();
+	for (auto object : _objects) {
+		auto item = dynamic_cast<Item *>(object);
+		if (item == nullptr)
+			continue;
+		_items.push_back(item);
+		mortadelo._items.push_back(new Item(*item));
+		filemon._items.push_back(new Item(*item));
+	}
+}
+
 void Inventory::updateItemsByActiveCharacter() {
 	auto *character = g_engine->player().activeCharacter();
 	assert(character != nullptr);
@@ -285,6 +298,8 @@ World::World() {
 	_mortadelo = dynamic_cast<MainCharacter *>(_globalRoom->getObjectByName("MORTADELO"));
 	if (_mortadelo == nullptr)
 		error("Could not find MORTADELO");
+
+	_inventory->initItems();
 }
 
 World::~World() {
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index d06ca35d565..d7b7f08ab9d 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -102,6 +102,7 @@ public:
 	Inventory(World *world, Common::ReadStream &stream);
 	virtual ~Inventory() override;
 
+	void initItems();
 	void updateItemsByActiveCharacter();
 
 private:


Commit: 0cf432015cbf5ba9de28d04a081c73446f667af3
    https://github.com/scummvm/scummvm/commit/0cf432015cbf5ba9de28d04a081c73446f667af3
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:46+02:00

Commit Message:
ALCACHOFA: Add game room interaction

theoretically at least

Changed paths:
  A engines/alcachofa/player.cpp
    engines/alcachofa/Input.h
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/alcachofa.h
    engines/alcachofa/common.h
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/graphics-opengl.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/graphics.h
    engines/alcachofa/objects.h
    engines/alcachofa/player.h
    engines/alcachofa/rooms.cpp
    engines/alcachofa/rooms.h
    engines/alcachofa/shape.cpp


diff --git a/engines/alcachofa/Input.h b/engines/alcachofa/Input.h
index ee6028ad355..248a40a62c2 100644
--- a/engines/alcachofa/Input.h
+++ b/engines/alcachofa/Input.h
@@ -36,6 +36,7 @@ public:
 	inline bool wasAnyMouseReleased() const { return _wasMouseLeftReleased || _wasMouseRightReleased; }
 	inline bool isMouseLeftDown() const { return _isMouseLeftDown; }
 	inline bool isMouseRightDown() const { return _isMouseRightDown; }
+	inline bool isAnyMouseDown() const { return _isMouseLeftDown || _isMouseRightDown; }
 	inline const Common::Point &mousePos2D() const { return _mousePos2D; }
 	inline const Common::Point &mousePos3D() const { return _mousePos3D; }
 
@@ -44,12 +45,12 @@ public:
 
 private:
 	bool
-		_wasMouseLeftPressed,
-		_wasMouseRightPressed,
-		_wasMouseLeftReleased,
-		_wasMouseRightReleased,
-		_isMouseLeftDown,
-		_isMouseRightDown;
+		_wasMouseLeftPressed = false,
+		_wasMouseRightPressed = false,
+		_wasMouseLeftReleased = false,
+		_wasMouseRightReleased = false,
+		_isMouseLeftDown = false,
+		_isMouseRightDown = false;
 	Common::Point
 		_mousePos2D,
 		_mousePos3D;
diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index cb7fe5cba19..5218047a42b 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -58,11 +58,13 @@ Common::String AlcachofaEngine::getGameId() const {
 }
 
 Common::Error AlcachofaEngine::run() {
+	g_system->showMouse(false);
 	setDebugger(_console);
 	_renderer.reset(IRenderer::createOpenGLRenderer(Common::Point(1024, 768)));
 	_drawQueue.reset(new DrawQueue(_renderer.get()));
 	_world.reset(new World());
 	_script.reset(new Script());
+	_player.reset(new Player());
 
 	world().globalRoom().loadResources();
 
@@ -78,9 +80,8 @@ Common::Error AlcachofaEngine::run() {
 
 
 	Common::Event e;
-	Graphics::FrameLimiter limiter(g_system, 60);
+	Graphics::FrameLimiter limiter(g_system, 120);
 	while (!shouldQuit()) {
-		g_system->showMouse(true);
 		_input.nextFrame();
 		while (g_system->getEventManager()->pollEvent(e)) {
 			if (_input.handleEvent(e))
@@ -90,8 +91,9 @@ Common::Error AlcachofaEngine::run() {
 		_renderer->begin();
 		_drawQueue->clear();
 		_camera.shake() = Vector2d();
-
-		player().currentRoom()->update();
+		_player->preUpdate();
+		_player->currentRoom()->update();
+		_player->postUpdate();
 
 		_renderer->end();
 
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index 98e3e48a3d6..117b6b4ddab 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -64,7 +64,7 @@ public:
 	inline DrawQueue &drawQueue() { return *_drawQueue; }
 	inline Camera &camera() { return _camera; }
 	inline Input &input() { return _input; }
-	inline Player &player() { return _player; }
+	inline Player &player() { return *_player; }
 	inline World &world() { return *_world; }
 	inline Script &script() { return *_script; }
 	inline Scheduler &scheduler() { return _scheduler; }
@@ -119,9 +119,9 @@ private:
 	Common::ScopedPtr<DrawQueue> _drawQueue;
 	Common::ScopedPtr<World> _world;
 	Common::ScopedPtr<Script> _script;
+	Common::ScopedPtr<Player> _player;
 	Camera _camera;
 	Input _input;
-	Player _player;
 	Scheduler _scheduler;
 };
 
diff --git a/engines/alcachofa/common.h b/engines/alcachofa/common.h
index 77d2c348afc..9d41e4f9020 100644
--- a/engines/alcachofa/common.h
+++ b/engines/alcachofa/common.h
@@ -23,18 +23,19 @@
 #define COMMON_H
 
 #include "common/scummsys.h"
+#include "common/rect.h"
+#include "math/vector2d.h"
+#include "math/vector3d.h"
 
 namespace Alcachofa {
 
 enum class CursorType {
-	Normal,
-	LookAt,
-	Use,
-	GoTo,
+	Point,
 	LeaveUp,
 	LeaveRight,
 	LeaveDown,
-	LeaveLeft
+	LeaveLeft,
+	WalkTo
 };
 
 enum class Direction {
@@ -66,6 +67,52 @@ static constexpr const Color kDebugRed = { 250, 0, 0, 70 };
 static constexpr const Color kDebugGreen = { 0, 255, 0, 85 };
 static constexpr const Color kDebugBlue = { 0, 0, 255, 110 };
 
+/**
+ * @brief This *fake* semaphore does not work in multi-threaded scenarios
+ * It is used as a safer option for a simple "isBusy" counter
+ */
+struct FakeSemaphore {
+	FakeSemaphore(uint initialCount = 0) : _counter(initialCount) {}
+	~FakeSemaphore() {
+		assert(_counter == 0);
+	}
+
+	inline bool isReleased() const { return _counter == 0; }
+	inline uint counter() const { return _counter; }
+private:
+	friend struct FakeLock;
+	uint _counter = 0;
+};
+
+struct FakeLock {
+	FakeLock(FakeSemaphore &semaphore) : _semaphore(semaphore) {
+		semaphore._counter++;
+	}
+
+	~FakeLock() {
+		assert(_semaphore._counter > 0);
+		_semaphore._counter--;
+	}
+private:
+	FakeSemaphore &_semaphore;
+};
+
+inline Math::Vector3d as3D(const Math::Vector2d &v) {
+	return Math::Vector3d(v.getX(), v.getY(), 0.0f);
+}
+
+inline Math::Vector3d as3D(const Common::Point &p) {
+	return Math::Vector3d((float)p.x, (float)p.y, 0.0f);
+}
+
+inline Math::Vector2d as2D(const Math::Vector3d &v) {
+	return Math::Vector2d(v.x(), v.y());
+}
+
+inline Math::Vector2d as2D(const Common::Point &p) {
+	return Math::Vector2d((float)p.x, (float)p.y);
+}
+
 }
 
 #endif // ALCACHOFA_COMMON_H
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 9c3bff96b41..4e9be05190e 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -498,8 +498,12 @@ MainCharacter::~MainCharacter() {
 		delete item;
 }
 
+bool MainCharacter::isBusy() const {
+	return !_semaphore.isReleased() || !g_engine->player().semaphore().isReleased();
+}
+
 void MainCharacter::update() {
-	if (_relatedProcessCounter == 0)
+	if (_semaphore.isReleased())
 		_currentlyUsingObject = nullptr;
 	WalkingCharacter::update();
 
@@ -588,7 +592,9 @@ void MainCharacter::serializeSave(Serializer &serializer) {
 	}
 
 	Character::serializeSave(serializer);
-	serializer.syncAsSint32LE(_relatedProcessCounter);
+	uint semaphoreCounter = _semaphore.counter();
+	serializer.syncAsSint32LE(semaphoreCounter);
+	_semaphore = FakeSemaphore(semaphoreCounter);
 	syncArray(serializer, _dialogMenuLines, syncDialogMenuLine);
 	syncObjectAsString(serializer, _currentlyUsingObject);
 
@@ -641,6 +647,20 @@ void MainCharacter::drop(const Common::String &name) {
 	}
 }
 
+void MainCharacter::walkToMouse() {
+	Point targetPos = g_engine->input().mousePos3D();
+	if (room()->activeFloor() != nullptr) {
+		_pathPoints.clear();
+		room()->activeFloor()->findPath(_sourcePos, targetPos, _pathPoints);
+		if (!_pathPoints.empty())
+			targetPos = _pathPoints[0];
+	}
+
+	const uint minDistance = (uint)(50 * _graphicNormal.depthScale());
+	if (_sourcePos.sqrDist(targetPos) > minDistance * minDistance)
+		walkTo(targetPos);
+}
+
 Background::Background(Room *room, const String &animationFileName, int16 scale)
 	: GraphicObject(room, "BACKGROUND") {
 	toggle(true);
diff --git a/engines/alcachofa/graphics-opengl.cpp b/engines/alcachofa/graphics-opengl.cpp
index b1998aa8f39..56062fb74fa 100644
--- a/engines/alcachofa/graphics-opengl.cpp
+++ b/engines/alcachofa/graphics-opengl.cpp
@@ -131,10 +131,7 @@ public:
 		_currentTexture = nullptr;
 		_currentBlendMode = (BlendMode)-1;
 
-#ifdef _DEBUG
-		glClearColor(0.5f, 0.0f, 0.5f, 1.0f);
-		glClear(GL_COLOR_BUFFER_BIT);
-#endif
+		// Do not clear the screen as the engine sometimes relies on the old frame to be reused
 	}
 
 	virtual void end() override {
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index baf21976bbc..8c64748aa16 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -222,10 +222,10 @@ Rect Animation::maxFrameBounds() const {
 	return bounds;
 }
 
-Math::Vector2d Animation::totalFrameOffset(int32 frameI) const {
+Point Animation::totalFrameOffset(int32 frameI) const {
 	const auto &frame = _frames[frameI];
 	const auto bounds = frameBounds(frameI);
-	return Vector2d(
+	return Point(
 		bounds.left - frame._center.x + frame._offset.x,
 		bounds.top - frame._center.y + frame._offset.y);
 }
@@ -297,6 +297,8 @@ void Animation::prerenderFrame(int32 frameI) {
 	_renderedFrameI = frameI;
 }
 
+
+
 void Animation::draw2D(int32 frameI, Vector2d center, float scale, BlendMode blendMode, Color color) {
 	prerenderFrame(frameI);
 	auto bounds = frameBounds(frameI);
@@ -304,7 +306,7 @@ void Animation::draw2D(int32 frameI, Vector2d center, float scale, BlendMode ble
 	Vector2d texMax((float)bounds.width() / _renderedSurface.w, (float)bounds.height() / _renderedSurface.h);
 
 	Vector2d size(bounds.width(), bounds.height());
-	center += totalFrameOffset(frameI) * scale;
+	center += as2D(totalFrameOffset(frameI)) * scale;
 	size *= scale;
 
 	auto &renderer = g_engine->renderer();
@@ -313,14 +315,6 @@ void Animation::draw2D(int32 frameI, Vector2d center, float scale, BlendMode ble
 	renderer.quad(center, size, color, Angle(), texMin, texMax);
 }
 
-static Vector3d as3D(const Vector2d &v) {
-	return Vector3d(v.getX(), v.getY(), 0.0f);
-}
-
-static Vector2d as2D(const Vector3d &v) {
-	return Vector2d(v.x(), v.y());
-}
-
 void Animation::draw3D(int32 frameI, Vector3d center, float scale, BlendMode blendMode, Color color) {
 	prerenderFrame(frameI);
 	auto bounds = frameBounds(frameI);
@@ -478,7 +472,7 @@ AnimationDrawRequest::AnimationDrawRequest(Animation *animation, int32 frameI, V
 	, _animation(animation)
 	, _frameI(frameI)
 	, _center(as3D(center))
-	, _scale(1.0f)
+	, _scale(kBaseScale)
 	, _color(kWhite)
 	, _blendMode(BlendMode::AdditiveAlpha)
 	, _lodBias(0.0f) {
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 7eb866456d0..196e093a08f 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -175,7 +175,9 @@ public:
 	inline const Common::Point &frameCenter(int32 frameI) const { return _frames[frameI]._center; }
 	inline uint32 totalDuration() const { return _totalDuration; }
 	inline uint8 &premultiplyAlpha() { return _premultiplyAlpha; }
+	Common::Point totalFrameOffset(int32 frameI) const;
 	int32 frameAtTime(uint32 time) const;
+	int32 imageIndex(int32 frameI, int32 spriteI) const;
 	Common::Point imageSize(int32 imageI) const;
 
 	void draw2D(
@@ -198,11 +200,9 @@ public:
 		BlendMode blendMode);
 
 private:
-	int32 imageIndex(int32 frameI, int32 spriteI) const;
 	Common::Rect spriteBounds(int32 frameI, int32 spriteI) const;
 	Common::Rect frameBounds(int32 frameI) const;
 	Common::Rect maxFrameBounds() const;
-	Math::Vector2d totalFrameOffset(int32 frameI) const;
 	void prerenderFrame(int32 frameI);
 
 	int32_t _renderedFrameI = -1;
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 90db2a1ce80..241d31eb42a 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -120,6 +120,8 @@ public:
 	ShapeObject(Room *room, Common::ReadStream &stream);
 	virtual ~ShapeObject() override = default;
 
+	inline int8 order() const { return _order; }
+
 	virtual void update() override;
 	virtual void serializeSave(Common::Serializer &serializer) override;
 	virtual Shape *shape() override;
@@ -424,6 +426,8 @@ public:
 
 	inline MainCharacterKind kind() const { return _kind; }
 	inline ObjectBase *currentlyUsing() const { return _currentlyUsingObject; }
+	inline FakeSemaphore &semaphore() { return _semaphore; }
+	bool isBusy() const;
 
 	virtual void update() override;
 	virtual void draw() override;
@@ -433,6 +437,7 @@ public:
 		Direction endDirection = Direction::Invalid,
 		ITriggerableObject *activateObject = nullptr,
 		const char *activateAction = nullptr) override;
+	void walkToMouse();
 	void clearInventory();
 	bool hasItem(const Common::String &name) const;
 	void pickup(const Common::String &name, bool putInHand);
@@ -450,7 +455,7 @@ private:
 	Common::Array<DialogMenuLine> _dialogMenuLines;
 	ObjectBase *_currentlyUsingObject = nullptr;
 	MainCharacterKind _kind;
-	int32_t _relatedProcessCounter = 0;
+	FakeSemaphore _semaphore;
 	ITriggerableObject *_activateObject = nullptr;
 	const char *_activateAction = nullptr;
 };
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
new file mode 100644
index 00000000000..1aef10f1914
--- /dev/null
+++ b/engines/alcachofa/player.cpp
@@ -0,0 +1,87 @@
+/* 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 "player.h"
+#include "alcachofa.h"
+
+using namespace Common;
+
+namespace Alcachofa {
+
+Player::Player()
+	: _activeCharacter(&g_engine->world().mortadelo()) {
+	const auto &cursorPath = g_engine->world().getGlobalAnimationName(GlobalAnimationKind::Cursor);
+	_cursorAnimation.reset(new Animation(cursorPath));
+	_cursorAnimation->load();
+}
+
+void Player::preUpdate() {
+	_selectedObject = nullptr;
+	_cursorFrameI = 0;
+}
+
+void Player::postUpdate() {
+	if (g_engine->input().wasAnyMouseReleased())
+		_pressedObject = nullptr;
+}
+
+void Player::updateCursor() {
+	if (_isOptionsMenuOpen || !_isGameLoaded)
+		_cursorFrameI = 0;
+	else if (_selectedObject == nullptr)
+		_cursorFrameI = !g_engine->input().isMouseLeftDown() || _pressedObject != nullptr ? 6 : 7;
+	else {
+		auto type = _selectedObject->cursorType();
+		switch (type) {
+		case CursorType::Point: _cursorFrameI = 0; break;
+		case CursorType::LeaveUp: _cursorFrameI = 8; break;
+		case CursorType::LeaveRight: _cursorFrameI = 10; break;
+		case CursorType::LeaveDown: _cursorFrameI = 12; break;
+		case CursorType::LeaveLeft: _cursorFrameI = 14; break;
+		case CursorType::WalkTo: _cursorFrameI = 6; break;
+		default: error("Invalid cursor type %u", (uint)type); break;
+		}
+
+		if (_cursorFrameI != 0) {
+			if (g_engine->input().isAnyMouseDown() && _pressedObject == _selectedObject)
+				_cursorFrameI++;
+		}
+		else if (g_engine->input().isMouseLeftDown())
+			_cursorFrameI = 2;
+		else if (g_engine->input().isMouseRightDown())
+			_cursorFrameI = 4;
+	}
+
+	Point cursorPos = g_engine->input().mousePos2D();
+	if (_heldItem == nullptr)
+		g_engine->drawQueue().add<AnimationDrawRequest>(_cursorAnimation.get(), _cursorFrameI, as2D(cursorPos), -10);
+	else {
+		auto itemGraphic = _heldItem->graphic();
+		assert(itemGraphic != nullptr);
+		auto &animation = itemGraphic->animation();
+		auto frameOffset = animation.totalFrameOffset(0);
+		auto imageSize = animation.imageSize(animation.imageIndex(0, 0));
+		cursorPos -= frameOffset + imageSize / 2;
+		g_engine->drawQueue().add<AnimationDrawRequest>(&animation, 0, as2D(cursorPos), -10);
+	}
+}
+
+}
diff --git a/engines/alcachofa/player.h b/engines/alcachofa/player.h
index 10b35706717..bcbb0a64ced 100644
--- a/engines/alcachofa/player.h
+++ b/engines/alcachofa/player.h
@@ -28,21 +28,40 @@ namespace Alcachofa {
 
 class Player {
 public:
+	Player();
+
 	inline Room *currentRoom() const { return _currentRoom; }
 	inline Room *&currentRoom() { return _currentRoom; }
 	inline MainCharacter *activeCharacter() const { return _activeCharacter; }
-    inline ShapeObject *selectedObject() const { return _selectedObject; }
-	inline Item *heldItem() const { return _heldItem; }
+    inline ShapeObject *&selectedObject() { return _selectedObject; }
+	inline ShapeObject *&pressedObject() { return _pressedObject; }
+	inline Item *&heldItem() { return _heldItem; }
+	inline FakeSemaphore &semaphore() { return _semaphore; }
+
+	inline bool &isOptionsMenuOpen() { return _isOptionsMenuOpen; }
+	inline bool &isGameLoaded() { return _isGameLoaded; }
 
 	inline MainCharacterKind activeCharacterKind() const {
 		return _activeCharacter == nullptr ? MainCharacterKind::None : _activeCharacter->kind();
 	}
 
+	void preUpdate();
+	void postUpdate();
+	void updateCursor();
+
+
 private:
+	Common::ScopedPtr<Animation> _cursorAnimation;
+	FakeSemaphore _semaphore;
 	Room *_currentRoom = nullptr;
 	MainCharacter *_activeCharacter;
     ShapeObject *_selectedObject = nullptr;
+    ShapeObject *_pressedObject = nullptr;
 	Item *_heldItem = nullptr;
+	int32 _cursorFrameI = 0;
+	bool
+		_isOptionsMenuOpen = false,
+		_isGameLoaded = true;
 };
 
 }
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 3cf924a76d2..0e9344d524f 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -130,7 +130,8 @@ void Room::update() {
 
 	if (g_engine->player().currentRoom() == this) {
 		updateRoomBounds();
-		updateInput();
+		if (!updateInput())
+			return;
 	}
 	// TODO: Add condition for global room update
 	world().globalRoom().updateObjects();
@@ -154,25 +155,49 @@ void Room::updateScripts() {
 	g_engine->scheduler().run();
 }
 
-void Room::updateInput() {
-	static bool hasLastP3D = false;
-	static Point lastP3D;
+bool Room::updateInput() {
+	auto &player = g_engine->player();
+	auto &input = g_engine->input();
+	if (player.heldItem() != nullptr && !player.activeCharacter()->isBusy() && input.wasMouseRightPressed()) {
+		player.heldItem() = nullptr;
+		return false;
+	}
 
+	bool canInteract = !player.activeCharacter()->isBusy();
+	// A complicated network condition can prevent interaction at this point
+	if (player.isOptionsMenuOpen() || !player.isGameLoaded())
+		canInteract = true;
+	if (canInteract)
+		updateInteraction();
 
-	if (g_engine->input().wasMouseLeftPressed()) {
-		Point p2d = g_engine->input().mousePos2D();
-		Point p3d = g_engine->input().mousePos3D();
-		auto m = &g_engine->world().filemon();
+	// TODO: Add main menu and opening inventory handling
+	return player.currentRoom() == this;
+}
 
-		if (!hasLastP3D) {
-			m->setPosition(p3d);
-		}
-		else {
-			m->room() = this;
-			m->walkTo(p3d);
+void Room::updateInteraction() {
+	auto &player = g_engine->player();
+	auto &input = g_engine->input();
+	// TODO: Add interaction with change character button / opening inventory
+
+	if (player.activeCharacter()->room() != this) {
+		player.activeCharacter()->room() = this;
+	}
+
+	player.selectedObject() = world().globalRoom().getSelectedObject(getSelectedObject());
+	if (player.selectedObject() == nullptr) {
+		if (input.wasMouseLeftPressed() && _activeFloorI >= 0 &&
+			player.activeCharacter()->room() == this &&
+			player.pressedObject() == nullptr) {
+			player.activeCharacter()->walkToMouse();
+			// TODO: Activate camera following character
 		}
-		hasLastP3D = true;
 	}
+	else {
+		player.selectedObject()->markSelected();
+		if (input.wasAnyMousePressed())
+			player.pressedObject() = player.selectedObject();
+	}
+	player.updateCursor();
 }
 
 void Room::updateRoomBounds() {
@@ -229,6 +254,20 @@ void Room::toggleActiveFloor() {
 	_activeFloorI ^= 1;
 }
 
+ShapeObject *Room::getSelectedObject(ShapeObject *best) const {
+	for (auto object : _objects) {
+		auto shape = object->shape();
+		auto shapeObject = dynamic_cast<ShapeObject *>(object);
+		if (!object->isEnabled() || shape == nullptr || shapeObject == nullptr ||
+			object->room() != this || // e.g. a main character that is in another room
+			!shape->contains(g_engine->input().mousePos3D()))
+			continue;
+		if (best == nullptr || shapeObject->order() < best->order())
+			best = shapeObject;
+	}
+	return best;
+}
+
 OptionsMenu::OptionsMenu(World *world, ReadStream &stream)
 	: Room(world, stream, true) {
 }
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index d7b7f08ab9d..ee5ee8a4717 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -49,7 +49,7 @@ public:
 	inline uint8 characterAlphaPremultiplier() const { return _characterAlphaPremultiplier; }
 
 	void update();
-	virtual void updateInput();
+	virtual bool updateInput();
 	virtual void loadResources();
 	virtual void freeResources();
 	virtual void serializeSave(Common::Serializer &serializer);
@@ -60,9 +60,11 @@ protected:
 	Room(World *world, Common::ReadStream &stream, bool hasUselessByte);
 	void updateScripts();
 	void updateRoomBounds();
+	void updateInteraction();
 	void updateObjects();
 	void drawObjects();
 	void drawDebug();
+	ShapeObject *getSelectedObject(ShapeObject *best = nullptr) const;
 
 	World *_world;
 	Common::String _name;
diff --git a/engines/alcachofa/shape.cpp b/engines/alcachofa/shape.cpp
index 66e3b137096..568917385b4 100644
--- a/engines/alcachofa/shape.cpp
+++ b/engines/alcachofa/shape.cpp
@@ -27,10 +27,6 @@ using namespace Math;
 
 namespace Alcachofa {
 
-static Vector2d asVec(const Point &p) {
-	return Vector2d((float)p.x, (float)p.y);
-}
-
 static int sideOfLine(const Point &a, const Point &b, const Point &q) {
 	return (b.x - a.x) * (q.y - a.y) - (b.y - a.y) * (q.x - a.x);
 }
@@ -80,9 +76,9 @@ EdgeDistances Polygon::edgeDistances(uint startPointI, const Point &query) const
 	assert(startPointI < _points.size());
 	uint endPointI = startPointI + 1 == _points.size() ? 0 : startPointI + 1;
 	Vector2d
-		a = asVec(_points[startPointI]),
-		b = asVec(_points[endPointI]),
-		q = asVec(query);
+		a = as2D(_points[startPointI]),
+		b = as2D(_points[endPointI]),
+		q = as2D(query);
 	float edgeLength = a.getDistanceTo(b);
 	Vector2d edgeDir = (b - a) / edgeLength;
 	Vector2d edgeNormal(-edgeDir.getY(), edgeDir.getX());


Commit: 4fff63d97889722bb3aa5e69aa0174c76ad667b5
    https://github.com/scummvm/scummvm/commit/4fff63d97889722bb3aa5e69aa0174c76ad667b5
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:46+02:00

Commit Message:
ALCACHOFA: Add door shortcuts

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


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 4e9be05190e..c9dd61d1281 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -61,6 +61,17 @@ void InteractableObject::drawDebug() {
 	renderer->debugShape(*shape());
 }
 
+void InteractableObject::onClick() {
+	auto heldItem = g_engine->player().heldItem();
+	const char *action;
+	if (heldItem == nullptr)
+		action = g_engine->input().wasMouseLeftReleased() ? "MIRAR" : "PULSAR";
+	else
+		action = heldItem->name().c_str();
+	g_engine->player().activeCharacter()->walkTo(_interactionPoint, Direction::Invalid, this, action);
+	onHoverUpdate();
+}
+
 void InteractableObject::trigger(const char *action) {
 	warning("stub: Trigger object %s with %s", name().c_str(), action == nullptr ? "<null>" : action);
 }
@@ -73,6 +84,19 @@ Door::Door(Room *room, ReadStream &stream)
 	_targetRoom.replace(' ', '_');
 }
 
+void Door::onClick() {
+	if (g_system->getMillis() - _lastClickTime < 500 && g_engine->player().activeCharacter()->clearTargetIf(this))
+		trigger(nullptr);
+	else {
+		InteractableObject::onClick();
+		_lastClickTime = g_system->getMillis();
+	}
+}
+
+void Door::trigger(const char *_) {
+	warning("STUB: Triggering door to %s", _targetRoom.c_str());
+}
+
 Character::Character(Room *room, ReadStream &stream)
 	: ShapeObject(room, stream)
 	, ITriggerableObject(stream)
@@ -661,6 +685,15 @@ void MainCharacter::walkToMouse() {
 		walkTo(targetPos);
 }
 
+bool MainCharacter::clearTargetIf(const ITriggerableObject *target) {
+	if (_activateObject == target) {
+		_activateObject = nullptr;
+		return true;
+	}
+	return false;
+}
+
+
 Background::Background(Room *room, const String &animationFileName, int16 scale)
 	: GraphicObject(room, "BACKGROUND") {
 	toggle(true);
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 241d31eb42a..01fc546cd24 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -309,6 +309,7 @@ public:
 	virtual ~InteractableObject() override = default;
 
 	virtual void drawDebug() override;
+	virtual void onClick() override;
 	virtual void trigger(const char *action) override;
 
 private:
@@ -320,9 +321,13 @@ public:
 	static constexpr const char *kClassName = "CPuerta";
 	Door(Room *room, Common::ReadStream &stream);
 
+	virtual void onClick() override;
+	virtual void trigger(const char *action) override;
+
 private:
 	Common::String _targetRoom, _targetObject;
 	Direction _characterDirection;
+	uint32 _lastClickTime = 0;
 };
 
 class Character : public ShapeObject, public ITriggerableObject {
@@ -438,6 +443,7 @@ public:
 		ITriggerableObject *activateObject = nullptr,
 		const char *activateAction = nullptr) override;
 	void walkToMouse();
+	bool clearTargetIf(const ITriggerableObject *target);
 	void clearInventory();
 	bool hasItem(const Common::String &name) const;
 	void pickup(const Common::String &name, bool putInHand);


Commit: a13f5d8999e979004be5506bac8566847e552204
    https://github.com/scummvm/scummvm/commit/a13f5d8999e979004be5506bac8566847e552204
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:46+02:00

Commit Message:
ALCACHOFA: Add camera following characters

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


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 5218047a42b..25de2090802 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -68,7 +68,7 @@ Common::Error AlcachofaEngine::run() {
 
 	world().globalRoom().loadResources();
 
-	auto room = world().getRoomByName("MAPA_TERROR");
+	auto room = world().getRoomByName("SALOON");
 	assert(room != nullptr);
 	player().currentRoom() = room;
 	room->loadResources();
diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index fabc394209b..47a0ccda58c 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -20,6 +20,7 @@
  */
 
 #include "camera.h"
+#include "script.h"
 #include "alcachofa.h"
 
 #include "common/system.h"
@@ -38,6 +39,14 @@ void Camera::setRoomBounds(Point bgSize, int16 bgScale) {
 	_roomMax = _roomMin + Vector2d(
 		bgSize.x * bgScale * kInvBaseScale,
 		bgSize.y * bgScale * kInvBaseScale);
+	_roomScale = bgScale;
+}
+
+void Camera::setFollow(WalkingCharacter *target) {
+	_followTarget = target;
+	_lastUpdateTime = g_system->getMillis();
+	if (target == nullptr)
+		_isChanging = false;
 }
 
 static Matrix4 scaleMatrix(float scale) {
@@ -77,7 +86,7 @@ void minmax(Vector3d &min, Vector3d &max, Vector3d val)
 
 Vector3d Camera::setAppliedCenter(Vector3d center) {
 	setupMatricesAround(center);
-	if (true) { // g_engine->script().getVariable("EncuadrarCamara")
+	if (g_engine->script().variable("EncuadrarCamara") || true) {
 		const float screenW = g_system->getWidth(), screenH = g_system->getHeight();
 		Vector3d min, max;
 		min = max = transform2Dto3D(Vector3d(0, 0, _roomScale));
@@ -122,11 +131,67 @@ Vector3d Camera::transform3Dto2D(Vector3d v3d) const {
 void Camera::update() {
 	// original would be some smoothing of delta times, let's not.
 	uint32 now = g_system->getMillis();
-	float deltaTime = now - _lastUpdateTime;
+	float deltaTime = (now - _lastUpdateTime) / 1000.0f;
 	deltaTime = MAX(0.001f, MIN(0.5f, deltaTime));
 	_lastUpdateTime = now;
 
+	updateFollowing(deltaTime);
 	setAppliedCenter(_usedCenter + Vector3d(_shake.getX(), _shake.getY(), 0.0f));
 }
 
+void Camera::updateFollowing(float deltaTime) {
+	if (_followTarget == nullptr)
+		return;
+	const float resolutionFactor = g_system->getWidth() * 0.00125f;
+	const float acceleration = 460 * resolutionFactor;
+	const float baseDeadZoneSize = 25 * resolutionFactor;
+	const float minSpeed = 20 * resolutionFactor;
+	const float maxSpeed = this->_maxSpeedFactor * resolutionFactor;
+	const float depthScale = _followTarget->graphic()->depthScale();
+	const auto characterPolygon = _followTarget->shape()->at(0);
+	const float halfHeight = ABS(characterPolygon._points[0].y - characterPolygon._points[2].y) / 2.0f;
+
+	Vector3d targetCenter = setAppliedCenter({
+		_shake.getX() + _followTarget->position().x,
+		_shake.getY() + _followTarget->position().y - depthScale * 85,
+		_usedCenter.z()});
+	targetCenter.y() -= halfHeight;
+	float distanceToTarget = as2D(_usedCenter - targetCenter).getMagnitude();
+	float moveDistance = _followTarget->stepSizeFactor() * _speed * deltaTime;
+
+	float deadZoneSize = baseDeadZoneSize / _scale;
+	if (_followTarget->isWalking() && depthScale > 0.8f)
+		deadZoneSize = (baseDeadZoneSize + (depthScale - 0.8f) * 200) / _scale;
+	bool isFarAway = false;
+	if (ABS(targetCenter.x() - _usedCenter.x()) > deadZoneSize ||
+		ABS(targetCenter.y() - _usedCenter.y()) > deadZoneSize) {
+		isFarAway = true;
+		_isBraking = false;
+		_isChanging = true;
+	}
+
+	if (_isBraking) {
+		_speed -= acceleration * 0.9f * deltaTime;
+		_speed = MAX(_speed, minSpeed);
+	}
+	if (_isChanging && !_isBraking) {
+		_speed += acceleration * deltaTime;
+		_speed = MIN(_speed, maxSpeed);
+		if (!isFarAway)
+			_isBraking = true;
+	}
+	if (_isChanging) {
+		if (distanceToTarget <= moveDistance) {
+			_usedCenter = targetCenter;
+			_isChanging = false;
+			_isBraking = false;
+		}
+		else {
+			Vector3d deltaCenter = targetCenter - _usedCenter;
+			deltaCenter.z() = 0.0f;
+			_usedCenter += deltaCenter * moveDistance / distanceToTarget;
+		}
+	}
+}
+
 }
diff --git a/engines/alcachofa/camera.h b/engines/alcachofa/camera.h
index 4e06640361a..bd58ee6df75 100644
--- a/engines/alcachofa/camera.h
+++ b/engines/alcachofa/camera.h
@@ -30,7 +30,7 @@
 
 namespace Alcachofa {
 
-class Personaje;
+class WalkingCharacter;
 class Process;
 struct Task;
 
@@ -46,18 +46,21 @@ public:
 	Math::Vector3d transform2Dto3D(Math::Vector3d v) const;
 	Math::Vector3d transform3Dto2D(Math::Vector3d v) const;
 	void setRoomBounds(Common::Point bgSize, int16 bgScale);
+	void setFollow(WalkingCharacter *target);
 
 private:
-	static constexpr const float kAccelerationThreshold = 2.89062f;
-	static constexpr const float kAcceleration = 3.94922f;
-
 	Math::Vector3d setAppliedCenter(Math::Vector3d center);
 	void setupMatricesAround(Math::Vector3d center);
+	void updateFollowing(float deltaTime);
 
 	uint32 _lastUpdateTime = 0;
+	bool _isChanging = false,
+		_isBraking = false;
 	float
 		_scale = 1.0f,
-		_roomScale = 1.0f;
+		_roomScale = 1.0f,
+		_maxSpeedFactor = 230.0f,
+		_speed = 0.0f;
 	Math::Angle _rotation;
 	Math::Vector2d
 		_roomMin = Math::Vector2d(-10000, -10000),
@@ -69,6 +72,7 @@ private:
 	Math::Matrix4
 		_mat3Dto2D,
 		_mat2Dto3D;
+	WalkingCharacter *_followTarget = nullptr;
 };
 
 }
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index c9dd61d1281..decb6b6961a 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -284,7 +284,7 @@ void WalkingCharacter::update() {
 static Direction getDirection(const Point &from, const Point &to) {
 	Point delta = from - to;
 	if (from.x == to.x)
-		return from.y < to.y ? Direction::Up : Direction::Down;
+		return from.y < to.y ? Direction::Down : Direction::Up;
 	else if (from.x < to.x) {
 		int slope = 1000 * delta.y / -delta.x;
 		return slope > 1000 ? Direction::Up
@@ -490,7 +490,7 @@ struct ArriveTask : public Task {
 		, _character(character) {}
 
 	virtual TaskReturn run() override {
-		return _character._isWalking
+		return _character.isWalking()
 			? TaskReturn::yield()
 			: TaskReturn::finish(1);
 	}
@@ -564,7 +564,7 @@ void MainCharacter::walkTo(
 
 	WalkingCharacter::walkTo(target, endDirection, activateObject, activateAction);
 	if (this == g_engine->player().activeCharacter()) {
-		// TODO: Add camera following character
+		g_engine->camera().setFollow(this);
 	}
 }
 
@@ -674,10 +674,16 @@ void MainCharacter::drop(const Common::String &name) {
 void MainCharacter::walkToMouse() {
 	Point targetPos = g_engine->input().mousePos3D();
 	if (room()->activeFloor() != nullptr) {
+		/* this would be original, but it can cause the character teleporting to the target
 		_pathPoints.clear();
 		room()->activeFloor()->findPath(_sourcePos, targetPos, _pathPoints);
 		if (!_pathPoints.empty())
-			targetPos = _pathPoints[0];
+			targetPos = _pathPoints[0]; */
+
+		Stack<Point> tmpPath;
+		room()->activeFloor()->findPath(_sourcePos, targetPos, tmpPath);
+		if (!tmpPath.empty())
+			targetPos = tmpPath[0];
 	}
 
 	const uint minDistance = (uint)(50 * _graphicNormal.depthScale());
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 01fc546cd24..697f9ee2d61 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -366,6 +366,10 @@ public:
 	WalkingCharacter(Room *room, Common::ReadStream &stream);
 	virtual ~WalkingCharacter() override = default;
 
+	inline bool isWalking() const { return _isWalking; }
+	inline const Common::Point &position() const { return _currentPos; }
+	inline float stepSizeFactor() const { return _stepSizeFactor; }
+
 	virtual void update() override;
 	virtual void draw() override;
 	virtual void drawDebug() override;
@@ -383,7 +387,6 @@ public:
 	Task *waitForArrival(Process &process);
 
 protected:
-	friend struct ArriveTask;
 	virtual void onArrived();
 	void updateWalking();
 	void updateWalkingAnimation();
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 0e9344d524f..abb704e3072 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -189,7 +189,7 @@ void Room::updateInteraction() {
 			player.activeCharacter()->room() == this &&
 			player.pressedObject() == nullptr) {
 			player.activeCharacter()->walkToMouse();
-			// TODO: Activate camera following character
+			g_engine->camera().setFollow(player.activeCharacter());
 		}
 	}
 	else {
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index d27be2671e2..f12cd0d4881 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -442,7 +442,8 @@ private:
 				error("Script tried to make character go to invalid object %s", getStringArg(1));
 			character->walkTo(target->position());
 
-			// TODO: if (flags & 2) g_engine->camera().setFollow(nullptr);
+			if (getNumberArg(2) & 2)
+				g_engine->camera().setFollow(nullptr);
 
 			return (getNumberArg(2) & 1)
 				? TaskReturn::finish(1)


Commit: 7df9ae09933819f678d181ba384e8155373ce2a2
    https://github.com/scummvm/scummvm/commit/7df9ae09933819f678d181ba384e8155373ce2a2
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:46+02:00

Commit Message:
ALCACHOFA: Add technical room changes

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


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 25de2090802..a43ebf7d16f 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -66,18 +66,7 @@ Common::Error AlcachofaEngine::run() {
 	_script.reset(new Script());
 	_player.reset(new Player());
 
-	world().globalRoom().loadResources();
-
-	auto room = world().getRoomByName("SALOON");
-	assert(room != nullptr);
-	player().currentRoom() = room;
-	room->loadResources();
-
-	// If a savegame was selected from the launcher, load it
-	int saveSlot = ConfMan.getInt("save_slot");
-	if (saveSlot != -1)
-		(void)loadGameState(saveSlot);
-
+	_player->changeRoom("MINA", true);
 
 	Common::Event e;
 	Graphics::FrameLimiter limiter(g_system, 120);
diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index 47a0ccda58c..89f6a3de561 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -31,6 +31,12 @@ using namespace Math;
 
 namespace Alcachofa {
 
+void Camera::resetRotationAndScale() {
+	_scale = 1;
+	_rotation = 0;
+	_usedCenter.z() = 0;
+}
+
 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 bd58ee6df75..091bf4a7312 100644
--- a/engines/alcachofa/camera.h
+++ b/engines/alcachofa/camera.h
@@ -45,6 +45,7 @@ public:
 	void update();
 	Math::Vector3d transform2Dto3D(Math::Vector3d v) const;
 	Math::Vector3d transform3Dto2D(Math::Vector3d v) const;
+	void resetRotationAndScale();
 	void setRoomBounds(Common::Point bgSize, int16 bgScale);
 	void setFollow(WalkingCharacter *target);
 
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index decb6b6961a..f129faedf6c 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -674,11 +674,8 @@ void MainCharacter::drop(const Common::String &name) {
 void MainCharacter::walkToMouse() {
 	Point targetPos = g_engine->input().mousePos3D();
 	if (room()->activeFloor() != nullptr) {
-		/* this would be original, but it can cause the character teleporting to the target
-		_pathPoints.clear();
-		room()->activeFloor()->findPath(_sourcePos, targetPos, _pathPoints);
-		if (!_pathPoints.empty())
-			targetPos = _pathPoints[0]; */
+		// original would be overwriting the current path but this
+		// can cause the character teleporting to the new target
 
 		Stack<Point> tmpPath;
 		room()->activeFloor()->findPath(_sourcePos, targetPos, tmpPath);
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 8c64748aa16..af8574fa1e4 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -81,8 +81,10 @@ void AnimationBase::load() {
 	if (!file.open(fullPath.c_str())) {
 		// original fallback
 		fullPath = "Mascaras/" + _fileName;
-		if (!file.open(fullPath.c_str()))
-			error("Could not open animation %s", _fileName.c_str());
+		if (!file.open(fullPath.c_str())) {
+			loadMissingAnimation();
+			return;
+		}
 	}
 
 	uint spriteCount = file.readUint32LE();
@@ -174,6 +176,22 @@ ManagedSurface *AnimationBase::readImage(SeekableReadStream &stream) const {
 	return new ManagedSurface(target);
 }
 
+void AnimationBase::loadMissingAnimation() {
+	// only allow missing animations we know are faulty in the original game
+	if (!_fileName.equalsIgnoreCase("ANIMACION.AN0"))
+		error("Could not open animation %s", _fileName.c_str());
+
+	// otherwise setup a functioning but empty animation
+	_isLoaded = true;
+	_totalDuration = 1;
+	_spriteIndexMapping[0] = 0;
+	_spriteOffsets.push_back(1);
+	_spriteBases.push_back(0);
+	_images.push_back(nullptr);
+	_imageOffsets.push_back(Point());
+	_frames.push_back({ Point(), Point(), 1 });
+}
+
 Animation::Animation(String fileName, AnimationFolder folder)
 	: AnimationBase(fileName, folder) {
 }
@@ -200,7 +218,8 @@ int32 Animation::imageIndex(int32 frameI, int32 spriteId) const {
 Rect Animation::spriteBounds(int32 frameI, int32 spriteId) const {
 	int32 imageI = imageIndex(frameI, spriteId);
 	auto image = imageI < 0 ? nullptr : _images[imageI];
-	return image == nullptr ? Rect()
+	return image == nullptr
+		? Rect(imageI < 0 ? Point() : _imageOffsets[imageI], 2, 1)
 		: Rect(_imageOffsets[imageI], image->w, image->h);
 }
 
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 196e093a08f..c12adb6077d 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -139,6 +139,7 @@ protected:
 	~AnimationBase();
 
 	void load();
+	void loadMissingAnimation();
 	void freeImages();
 	Graphics::ManagedSurface *readImage(Common::SeekableReadStream &stream) const;
 
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index 1aef10f1914..7ee6d4619b2 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -84,4 +84,37 @@ void Player::updateCursor() {
 	}
 }
 
+void Player::changeRoom(const Common::String &targetRoomName, bool resetCamera) {
+	// original would be to always free all resources from globalRoom, inventory, GlobalUI
+
+	Room &inventory = g_engine->world().inventory();
+	bool keepResources;
+	if (_currentRoom == &inventory)
+		keepResources = _roomBeforeInventory != nullptr && _roomBeforeInventory->name().equalsIgnoreCase(targetRoomName);
+	else {
+		keepResources = targetRoomName.equalsIgnoreCase("inventario") ||
+			(_currentRoom != nullptr && _currentRoom->name().equalsIgnoreCase(targetRoomName));
+	}
+
+	if (!keepResources && _currentRoom != nullptr) {
+		g_engine->scheduler().killProcessByName("ACTUALIZAR_" + _currentRoom->name());
+		_currentRoom->freeResources();
+	}
+	_currentRoom = g_engine->world().getRoomByName(targetRoomName);
+	if (_currentRoom == nullptr)
+		error("Invalid room name: %s", targetRoomName.c_str());
+
+	if (!_didLoadGlobalRooms) {
+		_didLoadGlobalRooms = true;
+		inventory.loadResources();
+		g_engine->world().globalRoom().loadResources();
+	}
+	if (!keepResources)
+		_currentRoom->loadResources();
+
+	if (resetCamera)
+		g_engine->camera().resetRotationAndScale();
+	_pressedObject = _selectedObject = nullptr;
+}
+
 }
diff --git a/engines/alcachofa/player.h b/engines/alcachofa/player.h
index bcbb0a64ced..e58fed5f053 100644
--- a/engines/alcachofa/player.h
+++ b/engines/alcachofa/player.h
@@ -48,12 +48,14 @@ public:
 	void preUpdate();
 	void postUpdate();
 	void updateCursor();
+	void changeRoom(const Common::String &targetRoomName, bool resetCamera);
 
 
 private:
 	Common::ScopedPtr<Animation> _cursorAnimation;
 	FakeSemaphore _semaphore;
-	Room *_currentRoom = nullptr;
+	Room *_currentRoom = nullptr,
+		*_roomBeforeInventory = nullptr;
 	MainCharacter *_activeCharacter;
     ShapeObject *_selectedObject = nullptr;
     ShapeObject *_pressedObject = nullptr;
@@ -61,7 +63,8 @@ private:
 	int32 _cursorFrameI = 0;
 	bool
 		_isOptionsMenuOpen = false,
-		_isGameLoaded = true;
+		_isGameLoaded = true,
+		_didLoadGlobalRooms = false;
 };
 
 }


Commit: df52d04b70f215d2becfc7015d0017b1d7e778c7
    https://github.com/scummvm/scummvm/commit/df52d04b70f215d2becfc7015d0017b1d7e778c7
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:46+02:00

Commit Message:
ALCACHOFA: Add various debug commands

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


diff --git a/engines/alcachofa/console.cpp b/engines/alcachofa/console.cpp
index 2a50d20e85e..4c72539a084 100644
--- a/engines/alcachofa/console.cpp
+++ b/engines/alcachofa/console.cpp
@@ -19,7 +19,11 @@
  *
  */
 
-#include "alcachofa/console.h"
+#include "console.h"
+#include "script.h"
+#include "alcachofa.h"
+
+using namespace Common;
 
 namespace Alcachofa {
 
@@ -28,9 +32,109 @@ Console::Console() : GUI::Debugger() {
 	registerVar("showCharacters", &_showCharacters);
 	registerVar("showFloor", &_showFloor);
 	registerVar("showFloorColor", &_showFloorColor);
+
+	registerCmd("var", WRAP_METHOD(Console, cmdVar));
+	registerCmd("processes", WRAP_METHOD(Console, cmdProcesses));
+	registerCmd("room", WRAP_METHOD(Console, cmdRoom));
+	registerCmd("rooms", WRAP_METHOD(Console, cmdRooms));
+	registerCmd("changeRoom", WRAP_METHOD(Console, cmdChangeRoom));
+	registerCmd("disableDebugDraw", WRAP_METHOD(Console, cmdDisableDebugDraw));
 }
 
 Console::~Console() {
 }
 
+bool Console::cmdVar(int argc, const char **args) {
+	auto &script = g_engine->script();
+	if (argc < 2 || argc > 3)
+		debugPrintf("usage: %s <name> [<value>]\n", args[0]);
+	else if (argc == 3) {
+		char *end = nullptr;
+		int32 value = (int32)strtol(args[2], &end, 10);
+		if (end == nullptr || *end != '\0')
+			debugPrintf("Invalid variable value: %s", args[2]);
+		else if (!script.hasVariable(args[1]))
+			debugPrintf("Invalid variable name: %s", args[1]);
+		else
+			script.variable(args[1]) = value;
+	}
+	else if (argc == 2) {
+		bool hadSomeMatch = false;
+		for (auto it = script.beginVariables(); it != script.endVariables(); it++) {
+			if (matchString(it->_key.c_str(), args[1], true)) {
+				hadSomeMatch = true;
+				debugPrintf("  %32s = %d\n", it->_key.c_str(), script.variable(it->_key.c_str()));
+			}
+		}
+		if (!hadSomeMatch)
+			debugPrintf("Could not find any variable with pattern: %s\n", args[1]);
+	}
+	return true;
+}
+
+bool Console::cmdProcesses(int argc, const char **args) {
+	g_engine->scheduler().debugPrint();
+	return true;
+}
+
+bool Console::cmdRoom(int argc, const char **args) {
+	if (argc > 2) {
+		debugPrintf("usage: %s [<name>]\n", args[0]);
+		return true;
+	}
+	Room *room = nullptr;
+	if (argc == 1) {
+		room = g_engine->player().currentRoom();
+		if (room == nullptr) {
+			debugPrintf("Player is currently in no room, cannot print details\n");
+			return true;
+		}
+	}
+	else {
+		room = g_engine->world().getRoomByName(args[1]);
+		if (room == nullptr) {
+			debugPrintf("Could not find room with exact name: %s\n", args[1]);
+			return cmdRooms(argc, args);
+		}
+	}
+	room->debugPrint(true);
+	return true;
+}
+
+bool Console::cmdRooms(int argc, const char **args) {
+	if (argc != 2) {
+		debugPrintf("usage: %s <pattern>\n", args[0]);
+		return true;
+	}
+	bool hadSomeMatch = false;
+	for (auto it = g_engine->world().beginRooms(); it != g_engine->world().endRooms(); it++) {
+		if ((*it)->name().matchString(args[1], true)) {
+			hadSomeMatch = true;
+			(*it)->debugPrint(false);
+		}
+	}
+	if (!hadSomeMatch)
+		debugPrintf("Could not find any room with pattern: %s\n", args[1]);
+	return true;
+}
+
+bool Console::cmdChangeRoom(int argc, const char **args) {
+	if (argc > 2)
+		debugPrintf("usage: %s <name>\n", args[0]);
+	else if (argc == 1) {
+		Room *current = g_engine->player().currentRoom();
+		debugPrintf("Current room: %s\n", current == nullptr ? "<null>" : current->name().c_str());
+	}
+	else if (g_engine->world().getRoomByName(args[1]) == nullptr)
+		debugPrintf("Invalid room name: %s\n", args[1]);
+	else
+		g_engine->player().changeRoom(args[1], true);
+	return true;
+}
+
+bool Console::cmdDisableDebugDraw(int argc, const char **args) {
+	_showInteractables = _showCharacters = _showFloor = _showFloorColor = false;
+	return true;
+}
+
 } // End of namespace Alcachofa
diff --git a/engines/alcachofa/console.h b/engines/alcachofa/console.h
index cc250e38524..771873d0617 100644
--- a/engines/alcachofa/console.h
+++ b/engines/alcachofa/console.h
@@ -46,6 +46,13 @@ public:
 	}
 
 private:
+	bool cmdVar(int argc, const char **args);
+	bool cmdProcesses(int argc, const char **args);
+	bool cmdRoom(int argc, const char **args);
+	bool cmdRooms(int argc, const char **args);
+	bool cmdChangeRoom(int argc, const char **args);
+	bool cmdDisableDebugDraw(int argc, const char **args);
+
 	bool _showInteractables = true;
 	bool _showCharacters = true;
 	bool _showFloor = true;
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index f129faedf6c..658bd94235c 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -29,6 +29,8 @@ using namespace Math;
 
 namespace Alcachofa {
 
+const char *Item::typeName() const { return "Item"; }
+
 Item::Item(Room *room, ReadStream &stream)
 	: GraphicObject(room, stream) {
 	stream.readByte(); // unused and ignored byte
@@ -46,6 +48,8 @@ ITriggerableObject::ITriggerableObject(ReadStream &stream)
 	: _interactionPoint(Shape(stream).firstPoint())
 	, _interactionDirection((Direction)stream.readSint32LE()) {}
 
+const char *InteractableObject::typeName() const { return "InteractableObject"; }
+
 InteractableObject::InteractableObject(Room *room, ReadStream &stream)
 	: PhysicalObject(room, stream)
 	, ITriggerableObject(stream)
@@ -76,6 +80,8 @@ void InteractableObject::trigger(const char *action) {
 	warning("stub: Trigger object %s with %s", name().c_str(), action == nullptr ? "<null>" : action);
 }
 
+const char *Door::typeName() const { return "Door"; }
+
 Door::Door(Room *room, ReadStream &stream)
 	: InteractableObject(room, stream)
 	, _targetRoom(readVarString(stream))
@@ -97,6 +103,8 @@ void Door::trigger(const char *_) {
 	warning("STUB: Triggering door to %s", _targetRoom.c_str());
 }
 
+const char *Character::typeName() const { return "Character"; }
+
 Character::Character(Room *room, ReadStream &stream)
 	: ShapeObject(room, stream)
 	, ITriggerableObject(stream)
@@ -215,6 +223,8 @@ void Character::trigger(const char *action) {
 	warning("stub: Trigger character %s with %s", name().c_str(), action == nullptr ? "<null>" : action);
 }
 
+const char *WalkingCharacter::typeName() const { return "WalkingCharacter"; }
+
 WalkingCharacter::WalkingCharacter(Room *room, ReadStream &stream)
 	: Character(room, stream) {
 	for (int32 i = 0; i < kDirectionCount; i++) {
@@ -506,6 +516,8 @@ Task *WalkingCharacter::waitForArrival(Process &process) {
 	return new ArriveTask(process, *this);
 }
 
+const char *MainCharacter::typeName() const { return "MainCharacter"; }
+
 MainCharacter::MainCharacter(Room *room, ReadStream &stream)
 	: WalkingCharacter(room, stream) {
 	stream.readByte(); // unused byte
@@ -696,6 +708,7 @@ bool MainCharacter::clearTargetIf(const ITriggerableObject *target) {
 	return false;
 }
 
+const char *Background::typeName() const { return "Background"; }
 
 Background::Background(Room *room, const String &animationFileName, int16 scale)
 	: GraphicObject(room, "BACKGROUND") {
@@ -705,6 +718,8 @@ Background::Background(Room *room, const String &animationFileName, int16 scale)
 	_graphic.order() = 59;
 }
 
+const char *FloorColor::typeName() const { return "FloorColor"; }
+
 FloorColor::FloorColor(Room *room, ReadStream &stream)
 	: ObjectBase(room, stream)
 	, _shape(stream) {}
diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index 0756be39745..3cbd3e0d210 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -31,6 +31,8 @@ using namespace Common;
 
 namespace Alcachofa {
 
+const char *ObjectBase::typeName() const { return "ObjectBase"; }
+
 ObjectBase::ObjectBase(Room *room, const char *name)
 	: _room(room)
 	, _name(name)
@@ -76,11 +78,15 @@ Shape *ObjectBase::shape() {
 	return nullptr;
 }
 
+const char *PointObject::typeName() const { return "PointObject"; }
+
 PointObject::PointObject(Room *room, ReadStream &stream)
 	: ObjectBase(room, stream) {
 	_pos = Shape(stream).firstPoint();
 }
 
+const char *GraphicObject::typeName() const { return "GraphicObject"; }
+
 GraphicObject::GraphicObject(Room *room, ReadStream &stream)
 	: ObjectBase(room, stream)
 	, _graphic(stream)
@@ -156,6 +162,8 @@ Task *GraphicObject::animate(Process &process) {
 	return new AnimateTask(process, this);
 }
 
+const char *SpecialEffectObject::typeName() const { return "SpecialEffectObject"; }
+
 SpecialEffectObject::SpecialEffectObject(Room *room, ReadStream &stream)
 	: GraphicObject(room, stream) {
 	_topLeft = Shape(stream).firstPoint();
@@ -182,6 +190,8 @@ void SpecialEffectObject::draw() {
 	g_engine->drawQueue().add<SpecialEffectDrawRequest>(_graphic, topLeft, bottomRight, texOffset, blendMode);
 }
 
+const char *ShapeObject::typeName() const { return "ShapeObject"; }
+
 ShapeObject::ShapeObject(Room *room, ReadStream &stream)
 	: ObjectBase(room, stream)
 	, _shape(stream)
@@ -248,6 +258,8 @@ void ShapeObject::updateSelection() {
 	}
 }
 
+const char *PhysicalObject::typeName() const { return "PhysicalObject"; }
+
 PhysicalObject::PhysicalObject(Room *room, ReadStream &stream)
 	: ShapeObject(room, stream) {
 	_order = stream.readSByte();
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 697f9ee2d61..5d36e5b3690 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -54,6 +54,7 @@ public:
 	virtual void serializeSave(Common::Serializer &serializer);
 	virtual Graphic *graphic();
 	virtual Shape *shape();
+	virtual const char *typeName() const;
 
 private:
 	Common::String _name;
@@ -68,6 +69,7 @@ public:
 
 	inline Common::Point &position() { return _pos; }
 	inline Common::Point position() const { return _pos; }
+	virtual const char *typeName() const;
 
 private:
 	Common::Point _pos;
@@ -91,6 +93,7 @@ public:
 	virtual void freeResources() override;
 	virtual void serializeSave(Common::Serializer &serializer) override;
 	virtual Graphic *graphic() override;
+	virtual const char *typeName() const;
 
 	Task *animate(Process &process);
 
@@ -108,6 +111,7 @@ public:
 	SpecialEffectObject(Room *room, Common::ReadStream &stream);
 
 	virtual void draw() override;
+	virtual const char *typeName() const;
 
 private:
 	static constexpr const float kShiftSpeed = 1 / 256.0f;
@@ -130,6 +134,7 @@ public:
 	virtual void onHoverEnd();
 	virtual void onHoverUpdate();
 	virtual void onClick();
+	virtual const char *typeName() const;
 	void markSelected();
 
 protected:
@@ -147,6 +152,7 @@ private:
 class PhysicalObject : public ShapeObject {
 public:
 	PhysicalObject(Room *room, Common::ReadStream &stream);
+	virtual const char *typeName() const;
 };
 
 class MenuButton : public PhysicalObject {
@@ -156,6 +162,7 @@ public:
 	virtual ~MenuButton() override = default;
 
 	inline int32 actionId() const { return _actionId; }
+	virtual const char *typeName() const;
 
 private:
 	int32 _actionId;
@@ -170,18 +177,24 @@ class InternetMenuButton final : public MenuButton {
 public:
 	static constexpr const char *kClassName = "CBotonMenuInternet";
 	InternetMenuButton(Room *room, Common::ReadStream &stream);
+
+	virtual const char *typeName() const;
 };
 
 class OptionsMenuButton final : public MenuButton {
 public:
 	static constexpr const char *kClassName = "CBotonMenuOpciones";
 	OptionsMenuButton(Room *room, Common::ReadStream &stream);
+
+	virtual const char *typeName() const;
 };
 
 class MainMenuButton final : public MenuButton {
 public:
 	static constexpr const char *kClassName = "CBotonMenuPrincipal";
 	MainMenuButton(Room *room, Common::ReadStream &stream);
+
+	virtual const char *typeName() const;
 };
 
 class PushButton final : public PhysicalObject {
@@ -189,6 +202,8 @@ public:
 	static constexpr const char *kClassName = "CPushButton";
 	PushButton(Room *room, Common::ReadStream &stream);
 
+	virtual const char *typeName() const;
+
 private:
 	// TODO: Reverse engineer PushButton
 	bool _alwaysVisible;
@@ -201,6 +216,8 @@ public:
 	static constexpr const char *kClassName = "CEditBox";
 	EditBox(Room *room, Common::ReadStream &stream);
 
+	virtual const char *typeName() const;
+
 private:
 	// TODO: Reverse engineer EditBox
 	int32 i1;
@@ -217,6 +234,8 @@ public:
 	CheckBox(Room *room, Common::ReadStream &stream);
 	virtual ~CheckBox() override = default;
 
+	virtual const char *typeName() const;
+
 private:
 	// TODO: Reverse engineer CheckBox
 	bool b1;
@@ -232,6 +251,8 @@ class CheckBoxAutoAdjustNoise final : public CheckBox {
 public:
 	static constexpr const char *kClassName = "CCheckBoxAutoAjustarRuido";
 	CheckBoxAutoAdjustNoise(Room *room, Common::ReadStream &stream);
+
+	virtual const char *typeName() const;
 };
 
 class SlideButton final : public ObjectBase {
@@ -240,6 +261,8 @@ public:
 	SlideButton(Room *room, Common::ReadStream &stream);
 	virtual ~SlideButton() override = default;
 
+	virtual const char *typeName() const;
+
 private:
 	// TODO: Reverse engineer SlideButton
 	int32 i1;
@@ -255,6 +278,8 @@ public:
 	static constexpr const char *kClassName = "CVentanaIRC";
 	IRCWindow(Room *room, Common::ReadStream &stream);
 
+	virtual const char *typeName() const;
+
 private:
 	Common::Point _p1, _p2;
 };
@@ -265,6 +290,8 @@ public:
 	MessageBox(Room *room, Common::ReadStream &stream);
 	virtual ~MessageBox() override = default;
 
+	virtual const char *typeName() const;
+
 private:
 	// TODO: Reverse engineer MessageBox
 	Graphic
@@ -279,6 +306,8 @@ class VoiceMeter final : public GraphicObject {
 public:
 	static constexpr const char *kClassName = "CVuMeter";
 	VoiceMeter(Room *room, Common::ReadStream &stream);
+
+	virtual const char *typeName() const;
 };
 
 class Item : public GraphicObject {
@@ -286,6 +315,8 @@ public:
 	static constexpr const char *kClassName = "CObjetoInventario";
 	Item(Room *room, Common::ReadStream &stream);
 	Item(const Item &other);
+
+	virtual const char *typeName() const;
 };
 
 class ITriggerableObject {
@@ -311,6 +342,7 @@ public:
 	virtual void drawDebug() override;
 	virtual void onClick() override;
 	virtual void trigger(const char *action) override;
+	virtual const char *typeName() const;
 
 private:
 	Common::String _relatedObject;
@@ -323,6 +355,7 @@ public:
 
 	virtual void onClick() override;
 	virtual void trigger(const char *action) override;
+	virtual const char *typeName() const;
 
 private:
 	Common::String _targetRoom, _targetObject;
@@ -344,6 +377,7 @@ public:
 	virtual void serializeSave(Common::Serializer &serializer) override;
 	virtual Graphic *graphic() override;
 	virtual void trigger(const char *action) override;
+	virtual const char *typeName() const;
 
 protected:
 	void syncObjectAsString(Common::Serializer &serializer, ObjectBase *&object);
@@ -383,6 +417,7 @@ public:
 		const char *activateAction = nullptr);
 	void stopWalkingAndTurn(Direction direction);
 	void setPosition(const Common::Point &target);
+	virtual const char *typeName() const;
 
 	Task *waitForArrival(Process &process);
 
@@ -451,6 +486,7 @@ public:
 	bool hasItem(const Common::String &name) const;
 	void pickup(const Common::String &name, bool putInHand);
 	void drop(const Common::String &name);
+	virtual const char *typeName() const;
 
 protected:
 	virtual void onArrived() override;
@@ -472,6 +508,7 @@ private:
 class Background final : public GraphicObject {
 public:
 	Background(Room *room, const Common::String &animationFileName, int16 scale);
+	virtual const char *typeName() const;
 };
 
 class FloorColor final : public ObjectBase {
@@ -482,6 +519,7 @@ public:
 
 	virtual void drawDebug() override;
 	virtual Shape *shape() override;
+	virtual const char *typeName() const;
 
 private:
 	FloorColorShape _shape;
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index abb704e3072..eafa536a93b 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -309,6 +309,20 @@ void Inventory::updateItemsByActiveCharacter() {
 		item->toggle(character->hasItem(item->name()));
 }
 
+void Room::debugPrint(bool withObjects) const {
+	auto &console = g_engine->console();
+	console.debugPrintf("  %s\n", _name.c_str());
+	if (!withObjects)
+		return;
+
+	for (auto *object : _objects) {
+		console.debugPrintf("\t%20s %-32s %s\n",
+			object->typeName(),
+			object->name().c_str(),
+			object->isEnabled() ? "" : "disabled");
+	}
+}
+
 static constexpr const char *kMapFiles[] = {
 	"MAPAS/MAPA5.EMC",
 	"MAPAS/MAPA4.EMC",
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index ee5ee8a4717..313e602066c 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -55,6 +55,7 @@ public:
 	virtual void serializeSave(Common::Serializer &serializer);
 	ObjectBase *getObjectByName(const Common::String &name) const;
 	void toggleActiveFloor();
+	void debugPrint(bool withObjects) const;
 
 protected:
 	Room(World *world, Common::ReadStream &stream, bool hasUselessByte);
@@ -132,6 +133,9 @@ public:
 
 	// reference-returning queries will error if the object does not exist
 
+	using RoomIterator = Common::Array<const Room *>::const_iterator;
+	inline RoomIterator beginRooms() const { return _rooms.begin(); }
+	inline RoomIterator endRooms() const { return _rooms.end(); }
 	inline Room &globalRoom() const { return *_globalRoom; }
 	inline Inventory &inventory() const { return *_inventory; }
 	inline MainCharacter &filemon() const { return *_filemon; }
diff --git a/engines/alcachofa/scheduler.cpp b/engines/alcachofa/scheduler.cpp
index 187d8d932c7..ad39277adc4 100644
--- a/engines/alcachofa/scheduler.cpp
+++ b/engines/alcachofa/scheduler.cpp
@@ -126,8 +126,8 @@ void Process::debugPrint() {
 }
 
 static void killProcessesForIn(MainCharacterKind characterKind, Array<Process *> &processes, uint firstIndex) {
-	assert(firstIndex < processes.size());
-	uint count = processes.size() - 1 - firstIndex;
+	assert(firstIndex <= processes.size());
+	uint count = processes.size() - firstIndex;
 	for (uint i = 0; i < count; i++) {
 		Process **process = &processes[processes.size() - 1 - i];
 		if ((*process)->character() == characterKind || characterKind == MainCharacterKind::None) {
@@ -209,4 +209,48 @@ bool Scheduler::hasProcessWithName(const String &name) {
 	return getProcessByName(processesToRunNext(), name) != nullptr;
 }
 
+void Scheduler::debugPrint() {
+	auto &console = g_engine->console();
+	bool didPrintSomething = false;
+
+	if (!processesToRun().empty()) {
+		console.debugPrintf("Currently running processes:\n");
+		for (uint32 i = 0; i < processesToRun().size(); i++) {
+			if (_currentProcessI == UINT_MAX || i > _currentProcessI)
+				console.debugPrintf("  ");
+			else if (i < _currentProcessI)
+				console.debugPrintf("# ");
+			else
+				console.debugPrintf("> ");
+			processesToRun()[i]->debugPrint();
+		}
+		didPrintSomething = true;
+	}
+
+	if (!processesToRunNext().empty()) {
+		if (didPrintSomething)
+			console.debugPrintf("\n");
+		console.debugPrintf("Scheduled processes:\n");
+		for (auto *process : processesToRunNext()) {
+			console.debugPrintf("  ");
+			process->debugPrint();
+		}
+		didPrintSomething = true;
+	}
+
+	if (!_backupProcesses.empty()) {
+		if (didPrintSomething)
+			console.debugPrintf("\n");
+		console.debugPrintf("Backed up processes:\n");
+		for (auto *process : _backupProcesses) {
+			console.debugPrintf("  ");
+			process->debugPrint();
+		}
+		didPrintSomething = true;
+	}
+
+	if (!didPrintSomething)
+		console.debugPrintf("No processes running or backed up\n");
+}
+
 }
diff --git a/engines/alcachofa/scheduler.h b/engines/alcachofa/scheduler.h
index a045eb4ce4c..a5317482bb9 100644
--- a/engines/alcachofa/scheduler.h
+++ b/engines/alcachofa/scheduler.h
@@ -152,6 +152,7 @@ public:
 	void killAllProcessesFor(MainCharacterKind characterKind);
 	void killProcessByName(const Common::String &name);
 	bool hasProcessWithName(const Common::String &name);
+	void debugPrint();
 
 	template<typename TTask, typename... TaskArgs>
 	Process *createProcess(MainCharacterKind character, TaskArgs&&... args) {
diff --git a/engines/alcachofa/script.h b/engines/alcachofa/script.h
index 827726f2018..6c4d328d6db 100644
--- a/engines/alcachofa/script.h
+++ b/engines/alcachofa/script.h
@@ -157,6 +157,11 @@ public:
 		const Common::String &action,
 		bool allowMissing = false);
 
+	using VariableNameIterator = Common::HashMap<Common::String, uint32>::const_iterator;
+	inline VariableNameIterator beginVariables() const { return _variableNames.begin(); }
+	inline VariableNameIterator endVariables() const { return _variableNames.end(); }
+	inline bool hasVariable(const char *name) const { return _variableNames.contains(name); }
+
 private:
 	friend struct ScriptTask;
 	friend struct ScriptTimerTask;
diff --git a/engines/alcachofa/ui-objects.cpp b/engines/alcachofa/ui-objects.cpp
index c845ba2492e..1b78a192b04 100644
--- a/engines/alcachofa/ui-objects.cpp
+++ b/engines/alcachofa/ui-objects.cpp
@@ -27,6 +27,8 @@ using namespace Common;
 
 namespace Alcachofa {
 
+const char *MenuButton::typeName() const { return "MenuButton"; }
+
 MenuButton::MenuButton(Room *room, ReadStream &stream)
 	: PhysicalObject(room, stream)
 	, _actionId(stream.readSint32LE())
@@ -36,18 +38,26 @@ MenuButton::MenuButton(Room *room, ReadStream &stream)
 	, _graphicDisabled(stream) {
 }
 
+const char *InternetMenuButton::typeName() const { return "InternetMenuButton"; }
+
 InternetMenuButton::InternetMenuButton(Room *room, ReadStream &stream)
 	: MenuButton(room, stream) {
 }
 
+const char *OptionsMenuButton::typeName() const { return "OptionsMenuButton"; }
+
 OptionsMenuButton::OptionsMenuButton(Room *room, ReadStream &stream)
 	: MenuButton(room, stream) {
 }
 
+const char *MainMenuButton::typeName() const { return "MainMenuButton"; }
+
 MainMenuButton::MainMenuButton(Room *room, ReadStream &stream)
 	: MenuButton(room, stream) {
 }
 
+const char *PushButton::typeName() const { return "PushButton"; }
+
 PushButton::PushButton(Room *room, ReadStream &stream)
 	: PhysicalObject(room, stream)
 	, _alwaysVisible(readBool(stream))
@@ -56,6 +66,8 @@ PushButton::PushButton(Room *room, ReadStream &stream)
 	, _actionId(stream.readSint32LE()) {
 }
 
+const char *EditBox::typeName() const { return "EditBox"; }
+
 EditBox::EditBox(Room *room, ReadStream &stream)
 	: PhysicalObject(room, stream)
 	, i1(stream.readSint32LE())
@@ -68,6 +80,8 @@ EditBox::EditBox(Room *room, ReadStream &stream)
 	, _fontId(stream.readSint32LE()) {
 }
 
+const char *CheckBox::typeName() const { return "CheckBox"; }
+
 CheckBox::CheckBox(Room *room, ReadStream &stream)
 	: PhysicalObject(room, stream)
 	, b1(readBool(stream))
@@ -78,11 +92,15 @@ CheckBox::CheckBox(Room *room, ReadStream &stream)
 	, _valueId(stream.readSint32LE()) {
 }
 
+const char *CheckBoxAutoAdjustNoise::typeName() const { return "CheckBoxAutoAdjustNoise"; }
+
 CheckBoxAutoAdjustNoise::CheckBoxAutoAdjustNoise(Room *room, ReadStream &stream)
 	: CheckBox(room, stream) {
 	stream.readByte(); // unused and ignored byte
 }
 
+const char *SlideButton::typeName() const { return "SlideButton"; }
+
 SlideButton::SlideButton(Room *room, ReadStream &stream)
 	: ObjectBase(room, stream)
 	, i1(stream.readSint32LE())
@@ -93,12 +111,16 @@ SlideButton::SlideButton(Room *room, ReadStream &stream)
 	, _graph3(stream) {
 }
 
+const char *IRCWindow::typeName() const { return "IRCWindow"; }
+
 IRCWindow::IRCWindow(Room *room, ReadStream &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)
 	: ObjectBase(room, stream)
 	, _graph1(stream)
@@ -113,6 +135,8 @@ MessageBox::MessageBox(Room *room, ReadStream &stream)
 	_graph5.start(true);
 }
 
+const char *VoiceMeter::typeName() const { return "VoiceMeter"; }
+
 VoiceMeter::VoiceMeter(Room *room, ReadStream &stream)
 	: GraphicObject(room, stream) {
 	stream.readByte(); // unused and ignored byte


Commit: c2abfe5b56e78843966c27054f001a2f44101eba
    https://github.com/scummvm/scummvm/commit/c2abfe5b56e78843966c27054f001a2f44101eba
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:46+02:00

Commit Message:
ALCACHOFA: Lock characters during script processes

Changed paths:
    engines/alcachofa/common.h
    engines/alcachofa/console.cpp
    engines/alcachofa/player.cpp
    engines/alcachofa/player.h
    engines/alcachofa/rooms.cpp
    engines/alcachofa/script.cpp
    engines/alcachofa/script.h


diff --git a/engines/alcachofa/common.h b/engines/alcachofa/common.h
index 9d41e4f9020..e8d83db85b8 100644
--- a/engines/alcachofa/common.h
+++ b/engines/alcachofa/common.h
@@ -85,16 +85,29 @@ private:
 };
 
 struct FakeLock {
-	FakeLock(FakeSemaphore &semaphore) : _semaphore(semaphore) {
-		semaphore._counter++;
+	FakeLock() : _semaphore(nullptr) {}
+
+	FakeLock(FakeSemaphore &semaphore) : _semaphore(&semaphore) {
+		_semaphore->_counter++;
+	}
+
+	FakeLock(const FakeLock &other) : _semaphore(other._semaphore) {
+		assert(_semaphore != nullptr);
+		_semaphore->_counter++;
+	}
+
+	FakeLock(FakeLock &&other) noexcept : _semaphore(other._semaphore) {
+		other._semaphore = nullptr;
 	}
 
 	~FakeLock() {
-		assert(_semaphore._counter > 0);
-		_semaphore._counter--;
+		if (_semaphore == nullptr)
+			return;
+		assert(_semaphore->_counter > 0);
+		_semaphore->_counter--;
 	}
 private:
-	FakeSemaphore &_semaphore;
+	FakeSemaphore *_semaphore;
 };
 
 inline Math::Vector3d as3D(const Math::Vector2d &v) {
diff --git a/engines/alcachofa/console.cpp b/engines/alcachofa/console.cpp
index 4c72539a084..ed7e1629771 100644
--- a/engines/alcachofa/console.cpp
+++ b/engines/alcachofa/console.cpp
@@ -127,8 +127,10 @@ bool Console::cmdChangeRoom(int argc, const char **args) {
 	}
 	else if (g_engine->world().getRoomByName(args[1]) == nullptr)
 		debugPrintf("Invalid room name: %s\n", args[1]);
-	else
+	else {
 		g_engine->player().changeRoom(args[1], true);
+		return false;
+	}
 	return true;
 }
 
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index 7ee6d4619b2..8cb43714a21 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -117,4 +117,14 @@ void Player::changeRoom(const Common::String &targetRoomName, bool resetCamera)
 	_pressedObject = _selectedObject = nullptr;
 }
 
+FakeSemaphore &Player::semaphoreFor(MainCharacterKind kind) {
+	static FakeSemaphore dummySemaphore;
+	switch (kind) {
+	case MainCharacterKind::None: return _semaphore;
+	case MainCharacterKind::Mortadelo: return g_engine->world().mortadelo().semaphore();
+	case MainCharacterKind::Filemon: return g_engine->world().filemon().semaphore();
+	default: assert(false && "Invalid main character kind"); return dummySemaphore;
+	}
+}
+
 }
diff --git a/engines/alcachofa/player.h b/engines/alcachofa/player.h
index e58fed5f053..3760395e989 100644
--- a/engines/alcachofa/player.h
+++ b/engines/alcachofa/player.h
@@ -37,6 +37,7 @@ public:
 	inline ShapeObject *&pressedObject() { return _pressedObject; }
 	inline Item *&heldItem() { return _heldItem; }
 	inline FakeSemaphore &semaphore() { return _semaphore; }
+	FakeSemaphore &semaphoreFor(MainCharacterKind kind);
 
 	inline bool &isOptionsMenuOpen() { return _isOptionsMenuOpen; }
 	inline bool &isGameLoaded() { return _isGameLoaded; }
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index eafa536a93b..790da4c9041 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -151,7 +151,7 @@ void Room::update() {
 void Room::updateScripts() {
 	g_engine->script().updateCommonVariables();
 	if (!g_engine->scheduler().hasProcessWithName("ACTUALIZAR_" + _name))
-		g_engine->script().createProcess(MainCharacterKind::None, "ACTUALIZAR_" + _name, true);
+		g_engine->script().createProcess(MainCharacterKind::None, "ACTUALIZAR_" + _name, true, true);
 	g_engine->scheduler().run();
 }
 
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index f12cd0d4881..8031ea86088 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -146,11 +146,12 @@ struct StackEntry {
 };
 
 struct ScriptTask : public Task {
-	ScriptTask(Process &process, const String &name, uint32 pc)
+	ScriptTask(Process &process, const String &name, uint32 pc, FakeLock &&lock)
 		: Task(process)
 		, _script(g_engine->script())
 		, _name(name)
-		, _pc(pc) {
+		, _pc(pc)
+		, _lock(Common::move(lock)) {
 		pushInstruction(UINT_MAX);
 	}
 
@@ -158,7 +159,8 @@ struct ScriptTask : public Task {
 		: Task(process)
 		, _script(g_engine->script())
 		, _name(forkParent._name + " FORKED")
-		, _pc(forkParent._pc) {
+		, _pc(forkParent._pc)
+		, _lock(forkParent._lock) {
 		for (uint i = 0; i < forkParent._stack.size(); i++)
 			_stack.push(forkParent._stack[i]);
 		pushNumber(1); // this task is the forked one
@@ -623,20 +625,24 @@ private:
 	String _name;
 	uint32 _pc;
 	bool _returnsFromKernelCall = false;
+	FakeLock _lock;
 };
 
-Process *Script::createProcess(MainCharacterKind character, const String &behavior, const String &action, bool allowMissing) {
-	return createProcess(character, behavior + '/' + action, allowMissing);
+Process *Script::createProcess(MainCharacterKind character, const String &behavior, const String &action, bool allowMissing, bool isBackground) {
+	return createProcess(character, behavior + '/' + action, allowMissing, isBackground);
 }
 
-Process *Script::createProcess(MainCharacterKind character, const String &procedure, bool allowMissing) {
+Process *Script::createProcess(MainCharacterKind character, const String &procedure, bool allowMissing, bool isBackground) {
 	uint32 offset;
 	if (!_procedures.tryGetVal(procedure, offset)) {
 		if (allowMissing)
 			return nullptr;
 		error("Unknown required procedure: %s", procedure.c_str());
 	}
-	Process *process = g_engine->scheduler().createProcess<ScriptTask>(character, procedure, offset);
+	FakeLock lock;
+	if (!isBackground)
+		new (&lock) FakeLock(g_engine->player().semaphoreFor(character));
+	Process *process = g_engine->scheduler().createProcess<ScriptTask>(character, procedure, offset, Common::move(lock));
 	process->name() = procedure;
 	return process;
 }
diff --git a/engines/alcachofa/script.h b/engines/alcachofa/script.h
index 6c4d328d6db..fbcc05fda08 100644
--- a/engines/alcachofa/script.h
+++ b/engines/alcachofa/script.h
@@ -150,12 +150,14 @@ public:
 	Process *createProcess(
 		MainCharacterKind character,
 		const Common::String &procedure,
-		bool allowMissing = false);
+		bool allowMissing = false,
+		bool isBackground = false);
 	Process *createProcess(
 		MainCharacterKind character,
 		const Common::String &behavior,
 		const Common::String &action,
-		bool allowMissing = false);
+		bool allowMissing = false,
+		bool isBackground = false);
 
 	using VariableNameIterator = Common::HashMap<Common::String, uint32>::const_iterator;
 	inline VariableNameIterator beginVariables() const { return _variableNames.begin(); }


Commit: 8a7d02239ba421a614b825a6b710c57900a1a98e
    https://github.com/scummvm/scummvm/commit/8a7d02239ba421a614b825a6b710c57900a1a98e
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:46+02:00

Commit Message:
ALCACHOFA: Fix door cursors

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


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 658bd94235c..361c30641cd 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -90,6 +90,19 @@ Door::Door(Room *room, ReadStream &stream)
 	_targetRoom.replace(' ', '_');
 }
 
+CursorType Door::cursorType() const {
+	CursorType fromObject = ShapeObject::cursorType();
+	if (fromObject != CursorType::Point)
+		return fromObject;
+	switch (_characterDirection) {
+	case Direction::Up: return CursorType::LeaveUp;
+	case Direction::Right: return CursorType::LeaveRight;
+	case Direction::Down: return CursorType::LeaveDown;
+	case Direction::Left: return CursorType::LeaveLeft;
+	default: assert(false && "Invalid door character direction"); return fromObject;
+	}
+}
+
 void Door::onClick() {
 	if (g_system->getMillis() - _lastClickTime < 500 && g_engine->player().activeCharacter()->clearTargetIf(this))
 		trigger(nullptr);
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 5d36e5b3690..2221e50ae2d 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -353,6 +353,7 @@ public:
 	static constexpr const char *kClassName = "CPuerta";
 	Door(Room *room, Common::ReadStream &stream);
 
+	virtual CursorType cursorType() const override;
 	virtual void onClick() override;
 	virtual void trigger(const char *action) override;
 	virtual const char *typeName() const;
diff --git a/engines/alcachofa/scheduler.cpp b/engines/alcachofa/scheduler.cpp
index ad39277adc4..e649779b621 100644
--- a/engines/alcachofa/scheduler.cpp
+++ b/engines/alcachofa/scheduler.cpp
@@ -120,7 +120,7 @@ void Process::debugPrint() {
 	debugger->debugPrintf("pid: %3u char: %s ret: %2d \"%s\"\n", _pid, characterName, _lastReturnValue, _name.c_str());
 
 	for (uint i = 0; i < _tasks.size(); i++) {
-		debugger->debugPrintf("\t%u: ", i);
+		debugger->debugPrintf("    %u: ", i);
 		_tasks[i]->debugPrint();
 	}
 }


Commit: e8539aac3fa733834d25d95222e6db37641d174b
    https://github.com/scummvm/scummvm/commit/e8539aac3fa733834d25d95222e6db37641d174b
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:46+02:00

Commit Message:
ALCACHOFA: Add character interaction and fix crash on exit

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


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 361c30641cd..77fc7c18b65 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -48,6 +48,16 @@ ITriggerableObject::ITriggerableObject(ReadStream &stream)
 	: _interactionPoint(Shape(stream).firstPoint())
 	, _interactionDirection((Direction)stream.readSint32LE()) {}
 
+void ITriggerableObject::onClick() {
+	auto heldItem = g_engine->player().heldItem();
+	const char *action;
+	if (heldItem == nullptr)
+		action = g_engine->input().wasMouseLeftReleased() ? "MIRAR" : "PULSAR";
+	else
+		action = heldItem->name().c_str();
+	g_engine->player().activeCharacter()->walkTo(_interactionPoint, Direction::Invalid, this, action);
+}
+
 const char *InteractableObject::typeName() const { return "InteractableObject"; }
 
 InteractableObject::InteractableObject(Room *room, ReadStream &stream)
@@ -66,13 +76,7 @@ void InteractableObject::drawDebug() {
 }
 
 void InteractableObject::onClick() {
-	auto heldItem = g_engine->player().heldItem();
-	const char *action;
-	if (heldItem == nullptr)
-		action = g_engine->input().wasMouseLeftReleased() ? "MIRAR" : "PULSAR";
-	else
-		action = heldItem->name().c_str();
-	g_engine->player().activeCharacter()->walkTo(_interactionPoint, Direction::Invalid, this, action);
+	ITriggerableObject::onClick();
 	onHoverUpdate();
 }
 
@@ -232,6 +236,11 @@ void Character::syncObjectAsString(Serializer &serializer, ObjectBase *&object)
 	}
 }
 
+void Character::onClick() {
+	ITriggerableObject::onClick();
+	onHoverUpdate();
+}
+
 void Character::trigger(const char *action) {
 	warning("stub: Trigger character %s with %s", name().c_str(), action == nullptr ? "<null>" : action);
 }
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 2221e50ae2d..30737c00106 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -329,6 +329,8 @@ public:
 	virtual void trigger(const char *action) = 0;
 
 protected:
+	void onClick();
+
 	Common::Point _interactionPoint;
 	Direction _interactionDirection = Direction::Right;
 };
@@ -377,6 +379,7 @@ public:
 	virtual void freeResources() override;
 	virtual void serializeSave(Common::Serializer &serializer) override;
 	virtual Graphic *graphic() override;
+	virtual void onClick() override;
 	virtual void trigger(const char *action) override;
 	virtual const char *typeName() const;
 
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 790da4c9041..b5fb75ac652 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -285,8 +285,7 @@ Inventory::Inventory(World *world, ReadStream &stream)
 }
 
 Inventory::~Inventory() {
-	for (auto *item : _items)
-		delete item;
+	// No need to delete items, they are room objects and thus deleted in Room::~Room
 }
 
 void Inventory::initItems() {


Commit: e116042bc754e85588f092884f09589f57bd59ea
    https://github.com/scummvm/scummvm/commit/e116042bc754e85588f092884f09589f57bd59ea
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:47+02:00

Commit Message:
ALCACHOFA: Execute object and character scripts on triggering

Changed paths:
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/objects.h
    engines/alcachofa/player.cpp
    engines/alcachofa/player.h
    engines/alcachofa/rooms.cpp
    engines/alcachofa/rooms.h
    engines/alcachofa/scheduler.cpp
    engines/alcachofa/script.cpp
    engines/alcachofa/script.h


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 77fc7c18b65..d7154af2bc5 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -81,7 +81,8 @@ void InteractableObject::onClick() {
 }
 
 void InteractableObject::trigger(const char *action) {
-	warning("stub: Trigger object %s with %s", name().c_str(), action == nullptr ? "<null>" : action);
+	g_engine->player().activeCharacter()->stopWalking();
+	g_engine->player().triggerObject(this, action);
 }
 
 const char *Door::typeName() const { return "Door"; }
@@ -242,7 +243,12 @@ void Character::onClick() {
 }
 
 void Character::trigger(const char *action) {
-	warning("stub: Trigger character %s with %s", name().c_str(), action == nullptr ? "<null>" : action);
+	g_engine->player().activeCharacter()->stopWalking(_interactionDirection);
+	if (scumm_stricmp(action, "iSABANA") == 0 && // Original hack probably to fix some bug :)
+		dynamic_cast<MainCharacter *>(this) != nullptr &&
+		room()->name().equalsIgnoreCase("CASA_FREDDY_ARRIBA"))
+		error("Not sure what *should* happen. How do we get here?");
+	g_engine->player().triggerObject(this, action);
 }
 
 const char *WalkingCharacter::typeName() const { return "WalkingCharacter"; }
@@ -412,9 +418,13 @@ void WalkingCharacter::updateWalkingAnimation()
 void WalkingCharacter::onArrived() {
 }
 
-void WalkingCharacter::stopWalkingAndTurn(Direction direction) {
+void WalkingCharacter::stopWalking(Direction direction) {
+	// be careful, the original engine had two versions of this method
+	// one without resetting _sourcePos
 	_isWalking = false;
-	_direction = direction;
+	_sourcePos = _currentPos;
+	if (direction != Direction::Invalid)
+		_direction = direction;
 }
 
 void WalkingCharacter::walkTo(
@@ -583,7 +593,7 @@ void MainCharacter::onArrived() {
 	_activateObject = nullptr;
 	_activateAction = nullptr;
 
-	stopWalkingAndTurn(activateObject->interactionDirection());
+	stopWalking(activateObject->interactionDirection());
 	if (g_engine->player().activeCharacter() == this)
 		activateObject->trigger(activateAction);
 }
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 30737c00106..a9104789198 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -419,7 +419,7 @@ public:
 		Direction endDirection = Direction::Invalid,
 		ITriggerableObject *activateObject = nullptr,
 		const char *activateAction = nullptr);
-	void stopWalkingAndTurn(Direction direction);
+	void stopWalking(Direction direction = Direction::Invalid);
 	void setPosition(const Common::Point &target);
 	virtual const char *typeName() const;
 
@@ -472,6 +472,7 @@ public:
 	virtual ~MainCharacter() override;
 
 	inline MainCharacterKind kind() const { return _kind; }
+	inline ObjectBase *&currentlyUsing() { return _currentlyUsingObject; }
 	inline ObjectBase *currentlyUsing() const { return _currentlyUsingObject; }
 	inline FakeSemaphore &semaphore() { return _semaphore; }
 	bool isBusy() const;
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index 8cb43714a21..cd29d94f7e9 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -20,6 +20,7 @@
  */
 
 #include "player.h"
+#include "script.h"
 #include "alcachofa.h"
 
 using namespace Common;
@@ -117,6 +118,12 @@ void Player::changeRoom(const Common::String &targetRoomName, bool resetCamera)
 	_pressedObject = _selectedObject = nullptr;
 }
 
+MainCharacter *Player::inactiveCharacter() const {
+	if (_activeCharacter == nullptr)
+		return nullptr;
+	return &g_engine->world().getOtherMainCharacterByKind(activeCharacterKind());
+}
+
 FakeSemaphore &Player::semaphoreFor(MainCharacterKind kind) {
 	static FakeSemaphore dummySemaphore;
 	switch (kind) {
@@ -127,4 +134,29 @@ FakeSemaphore &Player::semaphoreFor(MainCharacterKind kind) {
 	}
 }
 
+void Player::triggerObject(ObjectBase *object, const char *action) {
+	assert(object != nullptr && action != nullptr);
+	if (_activeCharacter->isBusy() || _activeCharacter->currentlyUsing() != nullptr)
+		return;
+	debug("Trigger object %s %s with %s", object->typeName(), object->name().c_str(), action);
+
+	if (inactiveCharacter()->currentlyUsing() == object) {
+		action = "MIRAR";
+		_activeCharacter->currentlyUsing() = nullptr;
+	}
+	else
+		_activeCharacter->currentlyUsing() = object;
+
+	auto &script = g_engine->script();
+	if (script.createProcess(activeCharacterKind(), object->name(), action, ScriptFlags::AllowMissing) != nullptr)
+		return;
+	else if (scumm_stricmp(action, "MIRAR") == 0)
+		script.createProcess(activeCharacterKind(), "DefectoMirar");
+	else if (action[0] == 'i' && object->name()[0] == 'i')
+		// TODO: Check if and how this can happen. I guess it crashes now but might be ignored by the original engine
+		script.createProcess(activeCharacterKind(), "DefectoObjeto");
+	else
+		script.createProcess(activeCharacterKind(), "DefectoUsar");
+}
+
 }
diff --git a/engines/alcachofa/player.h b/engines/alcachofa/player.h
index 3760395e989..b68c3da585a 100644
--- a/engines/alcachofa/player.h
+++ b/engines/alcachofa/player.h
@@ -37,6 +37,7 @@ public:
 	inline ShapeObject *&pressedObject() { return _pressedObject; }
 	inline Item *&heldItem() { return _heldItem; }
 	inline FakeSemaphore &semaphore() { return _semaphore; }
+	MainCharacter *inactiveCharacter() const;
 	FakeSemaphore &semaphoreFor(MainCharacterKind kind);
 
 	inline bool &isOptionsMenuOpen() { return _isOptionsMenuOpen; }
@@ -50,7 +51,7 @@ public:
 	void postUpdate();
 	void updateCursor();
 	void changeRoom(const Common::String &targetRoomName, bool resetCamera);
-
+	void triggerObject(ObjectBase *object, const char *action);
 
 private:
 	Common::ScopedPtr<Animation> _cursorAnimation;
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index b5fb75ac652..e40cd5d083e 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -151,7 +151,7 @@ void Room::update() {
 void Room::updateScripts() {
 	g_engine->script().updateCommonVariables();
 	if (!g_engine->scheduler().hasProcessWithName("ACTUALIZAR_" + _name))
-		g_engine->script().createProcess(MainCharacterKind::None, "ACTUALIZAR_" + _name, true, true);
+		g_engine->script().createProcess(MainCharacterKind::None, "ACTUALIZAR_" + _name, ScriptFlags::AllowMissing | ScriptFlags::IsBackground);
 	g_engine->scheduler().run();
 }
 
@@ -368,6 +368,15 @@ MainCharacter &World::getMainCharacterByKind(MainCharacterKind kind) const {
 	}
 }
 
+MainCharacter &World::getOtherMainCharacterByKind(MainCharacterKind kind) const {
+	switch (kind) {
+	case MainCharacterKind::Mortadelo: return *_filemon;
+	case MainCharacterKind::Filemon: return *_mortadelo;
+	default:
+		error("Invalid character kind given to getOtherMainCharacterByKind");
+	}
+}
+
 Room *World::getRoomByName(const Common::String &name) const {
 	for (auto *room : _rooms) {
 		if (room->name().equalsIgnoreCase(name))
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index 313e602066c..c349ae5933e 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -149,6 +149,7 @@ public:
 	}
 
 	MainCharacter &getMainCharacterByKind(MainCharacterKind kind) const;
+	MainCharacter &getOtherMainCharacterByKind(MainCharacterKind kind) const;
 	Room *getRoomByName(const Common::String &name) const;
 	ObjectBase *getObjectByName(const Common::String &name) const;
 	ObjectBase *getObjectByName(MainCharacterKind character, const Common::String &name) const;
diff --git a/engines/alcachofa/scheduler.cpp b/engines/alcachofa/scheduler.cpp
index e649779b621..ae24ec1d6ae 100644
--- a/engines/alcachofa/scheduler.cpp
+++ b/engines/alcachofa/scheduler.cpp
@@ -98,7 +98,7 @@ TaskReturnType Process::run() {
 			break;
 		case TaskReturnType::Finished:
 			_lastReturnValue = ret.returnValue();
-			_tasks.pop();
+			delete _tasks.pop();
 			break;
 		default:
 			assert(false && "Invalid task return type");
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 8031ea86088..6760473afe2 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -167,9 +167,11 @@ struct ScriptTask : public Task {
 	}
 
 	virtual TaskReturn run() override {
+		if (_isFirstExecution || _returnsFromKernelCall)
+			setCharacterVariables();
 		if (_returnsFromKernelCall)
 			pushNumber(process().returnValue());
-		_returnsFromKernelCall = false;
+		_isFirstExecution = _returnsFromKernelCall = false;
 
 		while (true) {
 			if (_pc >= _script._instructions.size())
@@ -299,6 +301,11 @@ struct ScriptTask : public Task {
 	}
 
 private:
+	void setCharacterVariables() {
+		_script.variable("m_o_f") = (int32)process().character();
+		_script.variable("m_o_f_real") = (int32)g_engine->player().activeCharacterKind();
+	}
+
 	void pushNumber(int32 value) {
 		_stack.push({ StackEntryType::Number, value });
 	}
@@ -420,11 +427,11 @@ private:
 			if (character == nullptr)
 				error("Script tried to stop-and-turn unknown character");
 			else
-				character->stopWalkingAndTurn((Direction)getNumberArg(1));
+				character->stopWalking((Direction)getNumberArg(1));
 			return TaskReturn::finish(1);
 		}
 		case ScriptKernelTask::StopAndTurnMe: {
-			relatedCharacter().stopWalkingAndTurn((Direction)getNumberArg(0));
+			relatedCharacter().stopWalking((Direction)getNumberArg(0));
 			return TaskReturn::finish(1);
 		}
 		case ScriptKernelTask::ChangeCharacter:
@@ -625,22 +632,23 @@ private:
 	String _name;
 	uint32 _pc;
 	bool _returnsFromKernelCall = false;
+	bool _isFirstExecution = false;
 	FakeLock _lock;
 };
 
-Process *Script::createProcess(MainCharacterKind character, const String &behavior, const String &action, bool allowMissing, bool isBackground) {
-	return createProcess(character, behavior + '/' + action, allowMissing, isBackground);
+Process *Script::createProcess(MainCharacterKind character, const String &behavior, const String &action, ScriptFlags flags) {
+	return createProcess(character, behavior + '/' + action, flags);
 }
 
-Process *Script::createProcess(MainCharacterKind character, const String &procedure, bool allowMissing, bool isBackground) {
+Process *Script::createProcess(MainCharacterKind character, const String &procedure, ScriptFlags flags) {
 	uint32 offset;
 	if (!_procedures.tryGetVal(procedure, offset)) {
-		if (allowMissing)
+		if (flags & ScriptFlags::AllowMissing)
 			return nullptr;
 		error("Unknown required procedure: %s", procedure.c_str());
 	}
 	FakeLock lock;
-	if (!isBackground)
+	if (!(flags & ScriptFlags::IsBackground))
 		new (&lock) FakeLock(g_engine->player().semaphoreFor(character));
 	Process *process = g_engine->scheduler().createProcess<ScriptTask>(character, procedure, offset, Common::move(lock));
 	process->name() = procedure;
diff --git a/engines/alcachofa/script.h b/engines/alcachofa/script.h
index fbcc05fda08..46102d58f52 100644
--- a/engines/alcachofa/script.h
+++ b/engines/alcachofa/script.h
@@ -133,6 +133,18 @@ enum class ScriptKernelTask {
 	LerpCamToObjectKeepingZ
 };
 
+enum class ScriptFlags {
+	None = 0,
+	AllowMissing = (1 << 0),
+	IsBackground = (1 << 1)
+};
+inline ScriptFlags operator | (ScriptFlags a, ScriptFlags b) {
+	return (ScriptFlags)(((uint)a) | ((uint)b));
+}
+inline bool operator & (ScriptFlags a, ScriptFlags b) {
+	return ((uint)a) & ((uint)b);
+}
+
 struct ScriptInstruction {
 	ScriptInstruction(Common::ReadStream &stream);
 
@@ -150,14 +162,12 @@ public:
 	Process *createProcess(
 		MainCharacterKind character,
 		const Common::String &procedure,
-		bool allowMissing = false,
-		bool isBackground = false);
+		ScriptFlags flags = ScriptFlags::None);
 	Process *createProcess(
 		MainCharacterKind character,
 		const Common::String &behavior,
 		const Common::String &action,
-		bool allowMissing = false,
-		bool isBackground = false);
+		ScriptFlags flags = ScriptFlags::None);
 
 	using VariableNameIterator = Common::HashMap<Common::String, uint32>::const_iterator;
 	inline VariableNameIterator beginVariables() const { return _variableNames.begin(); }


Commit: 21812391c8b8d4932cadff5fae5ea73c510cfe70
    https://github.com/scummvm/scummvm/commit/21812391c8b8d4932cadff5fae5ea73c510cfe70
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:47+02:00

Commit Message:
ALCACHOFA: Add triggering of doors

Changed paths:
    engines/alcachofa/camera.cpp
    engines/alcachofa/camera.h
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/graphics.h
    engines/alcachofa/objects.h
    engines/alcachofa/player.cpp
    engines/alcachofa/player.h
    engines/alcachofa/rooms.cpp
    engines/alcachofa/rooms.h
    engines/alcachofa/scheduler.cpp


diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index 89f6a3de561..3ec3a456741 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -55,6 +55,12 @@ void Camera::setFollow(WalkingCharacter *target) {
 		_isChanging = false;
 }
 
+void Camera::setPosition(Vector2d v) {
+	_usedCenter.x() = v.getX();
+	_usedCenter.y() = v.getY();
+	setFollow(nullptr);
+}
+
 static Matrix4 scaleMatrix(float scale) {
 	Matrix4 m;
 	m(0, 0) = scale;
diff --git a/engines/alcachofa/camera.h b/engines/alcachofa/camera.h
index 091bf4a7312..fc3dd924ea9 100644
--- a/engines/alcachofa/camera.h
+++ b/engines/alcachofa/camera.h
@@ -48,6 +48,7 @@ public:
 	void resetRotationAndScale();
 	void setRoomBounds(Common::Point bgSize, int16 bgScale);
 	void setFollow(WalkingCharacter *target);
+	void setPosition(Math::Vector2d v);
 
 private:
 	Math::Vector3d setAppliedCenter(Math::Vector3d center);
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index d7154af2bc5..861d2b9925a 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -118,7 +118,7 @@ void Door::onClick() {
 }
 
 void Door::trigger(const char *_) {
-	warning("STUB: Triggering door to %s", _targetRoom.c_str());
+	g_engine->player().triggerDoor(this);
 }
 
 const char *Character::typeName() const { return "Character"; }
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index af8574fa1e4..755c4d7a722 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -121,6 +121,7 @@ void AnimationBase::load() {
 	uint frameCount = file.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(file.readUint32LE());
@@ -142,6 +143,11 @@ void AnimationBase::freeImages() {
 		if (image != nullptr)
 			delete image;
 	}
+	_images.clear();
+	_spriteOffsets.clear();
+	_spriteBases.clear();
+	_frames.clear();
+	_imageOffsets.clear();
 	_isLoaded = false;
 }
 
@@ -206,6 +212,16 @@ void Animation::load() {
 	_renderedTexture = g_engine->renderer().createTexture(maxBounds.width(), maxBounds.height(), withMipmaps);
 }
 
+void Animation::freeImages() {
+	if (!_isLoaded)
+		return;
+	AnimationBase::freeImages();
+	_renderedSurface.free();
+	_renderedTexture.reset(nullptr);
+	_renderedFrameI = -1;
+	_premultiplyAlpha = 100;
+}
+
 int32 Animation::imageIndex(int32 frameI, int32 spriteId) const {
 	assert(frameI >= 0 && (uint)frameI < frameCount());
 	assert(spriteId >= 0 && (uint)spriteId < spriteCount());
@@ -404,8 +420,12 @@ void Graphic::loadResources() {
 }
 
 void Graphic::freeResources() {
-	_ownedAnimation.reset();
-	_animation = nullptr;
+	if (_ownedAnimation == nullptr)
+		_animation = nullptr;
+	else {
+		_ownedAnimation->freeImages();
+		_animation = _ownedAnimation.get();
+	}
 }
 
 void Graphic::update() {
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index c12adb6077d..2f2306ddd30 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -167,7 +167,7 @@ public:
 	Animation(Common::String fileName, AnimationFolder folder = AnimationFolder::Animations);
 
 	void load();
-	using AnimationBase::freeImages;
+	void freeImages();
 
 	inline bool isLoaded() const { return _isLoaded; }
 	inline uint spriteCount() const { return _spriteBases.size(); }
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index a9104789198..7e48f54b391 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -355,6 +355,10 @@ public:
 	static constexpr const char *kClassName = "CPuerta";
 	Door(Room *room, Common::ReadStream &stream);
 
+	inline const Common::String &targetRoom() const { return _targetRoom; }
+	inline const Common::String &targetObject() const { return _targetObject; }
+	inline Direction characterDirection() const { return _characterDirection; }
+
 	virtual CursorType cursorType() const override;
 	virtual void onClick() override;
 	virtual void trigger(const char *action) override;
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index cd29d94f7e9..d57c9120706 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -159,4 +159,65 @@ void Player::triggerObject(ObjectBase *object, const char *action) {
 		script.createProcess(activeCharacterKind(), "DefectoUsar");
 }
 
+struct DoorTask : public Task {
+	DoorTask(Process &process, const Door *door, FakeLock &&lock)
+		: Task(process)
+		, _lock(move(lock))
+		, _sourceDoor(door)
+		, _character(g_engine->player().activeCharacter())
+		, _player(g_engine->player()) {
+		_targetRoom = g_engine->world().getRoomByName(door->targetRoom());
+		if (_targetRoom == nullptr)
+			error("Invalid door target room: %s", door->targetRoom().c_str());
+
+		_targetDoor = dynamic_cast<Door *>(_targetRoom->getObjectByName(door->targetObject()));
+		if (_targetDoor == nullptr)
+			error("Invalid door target door: %s", door->targetObject().c_str());
+
+		process.name() = String::format("Door to %s %s", _targetRoom->name().c_str(), _targetDoor->name().c_str());
+	}
+
+	virtual TaskReturn run() {
+		TASK_BEGIN;
+		// TODO: Fade out music on room change
+		// TODO: Fade out/in on room change instead of delay
+		TASK_WAIT(delay(500));
+		_player.changeRoom(_targetRoom->name(), true);
+
+		if (_targetRoom->fixedCameraOnEntering())
+			g_engine->camera().setPosition(as2D(_targetDoor->interactionPoint()));
+		else {
+			_character->room() = _targetRoom;
+			_character->setPosition(_targetDoor->interactionPoint());
+			_character->stopWalking(_targetDoor->characterDirection());
+			g_engine->camera().setFollow(_character);
+		}
+
+		// TODO: Start music on room change
+		if (g_engine->script().createProcess(_character->kind(), "ENTRAR_" + _targetRoom->name(), ScriptFlags::AllowMissing))
+			TASK_WAIT(delay(0));
+		else
+			TASK_WAIT(delay(500));
+		TASK_END;
+	}
+
+	virtual void debugPrint() {
+		g_engine->console().debugPrintf("%s\n", process().name().c_str());
+	}
+
+private:
+	FakeLock _lock;
+	const Door *_sourceDoor, *_targetDoor;
+	Room *_targetRoom;
+	MainCharacter *_character;
+	Player &_player;
+};
+
+void Player::triggerDoor(const Door *door) {
+	_heldItem = nullptr;
+	
+	FakeLock lock(_activeCharacter->semaphore());
+	g_engine->scheduler().createProcess<DoorTask>(activeCharacterKind(), door, move(lock));
+}
+
 }
diff --git a/engines/alcachofa/player.h b/engines/alcachofa/player.h
index b68c3da585a..9b6d9ea79aa 100644
--- a/engines/alcachofa/player.h
+++ b/engines/alcachofa/player.h
@@ -52,6 +52,7 @@ public:
 	void updateCursor();
 	void changeRoom(const Common::String &targetRoomName, bool resetCamera);
 	void triggerObject(ObjectBase *object, const char *action);
+	void triggerDoor(const Door *door);
 
 private:
 	Common::ScopedPtr<Animation> _cursorAnimation;
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index e40cd5d083e..a69e61ab2e6 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -93,7 +93,7 @@ Room::Room(World *world, ReadStream &stream, bool hasUselessByte)
 	auto backgroundScale = stream.readSint16LE();
 	_floors[0] = PathFindingShape(stream);
 	_floors[1] = PathFindingShape(stream);
-	_cameraFollowsUponLeaving = readBool(stream);
+	_fixedCameraOnEntering = readBool(stream);
 	PathFindingShape _(stream); // unused path finding area
 	_characterAlphaPremultiplier = stream.readByte();
 	if (hasUselessByte)
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index c349ae5933e..7c18b2df093 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -47,6 +47,7 @@ public:
 	}
 	inline uint8 characterAlphaTint() const { return _characterAlphaTint; }
 	inline uint8 characterAlphaPremultiplier() const { return _characterAlphaPremultiplier; }
+	inline bool fixedCameraOnEntering() const { return _fixedCameraOnEntering; }
 
 	void update();
 	virtual bool updateInput();
@@ -70,7 +71,7 @@ protected:
 	World *_world;
 	Common::String _name;
 	PathFindingShape _floors[2];
-	bool _cameraFollowsUponLeaving;
+	bool _fixedCameraOnEntering;
 	int8
 		_musicId,
 		_activeFloorI = -1;
diff --git a/engines/alcachofa/scheduler.cpp b/engines/alcachofa/scheduler.cpp
index ae24ec1d6ae..c8776fa611c 100644
--- a/engines/alcachofa/scheduler.cpp
+++ b/engines/alcachofa/scheduler.cpp
@@ -196,7 +196,6 @@ static Process **getProcessByName(Array<Process *> &_processes, const String &na
 }
 
 void Scheduler::killProcessByName(const String &name) {
-	assert(processesToRun().empty());
 	Process **process = getProcessByName(processesToRunNext(), name);
 	if (process != nullptr) {
 		delete *process;


Commit: ae53e521cdd115688edb92b2b25fb49b93c39f79
    https://github.com/scummvm/scummvm/commit/ae53e521cdd115688edb92b2b25fb49b93c39f79
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:47+02:00

Commit Message:
ALCACHOFA: Toggle related objects of interactable objects

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


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 861d2b9925a..d7051f7cbec 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -85,6 +85,13 @@ void InteractableObject::trigger(const char *action) {
 	g_engine->player().triggerObject(this, action);
 }
 
+void InteractableObject::toggle(bool isEnabled) {
+	ObjectBase::toggle(isEnabled);
+	ObjectBase *related = room()->getObjectByName(_relatedObject);
+	if (related != nullptr)
+		related->toggle(isEnabled);
+}
+
 const char *Door::typeName() const { return "Door"; }
 
 Door::Door(Room *room, ReadStream &stream)
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 7e48f54b391..0d5f7e26636 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -344,6 +344,7 @@ public:
 	virtual void drawDebug() override;
 	virtual void onClick() override;
 	virtual void trigger(const char *action) override;
+	virtual void toggle(bool isEnabled) override;
 	virtual const char *typeName() const;
 
 private:


Commit: e28435d1a8280fa1f45a245e4255c3aca56e0a16
    https://github.com/scummvm/scummvm/commit/e28435d1a8280fa1f45a245e4255c3aca56e0a16
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:47+02:00

Commit Message:
ALCACHOFA: Let camera catch up on room changes

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


diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index 3ec3a456741..13f309bdb81 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -48,9 +48,10 @@ void Camera::setRoomBounds(Point bgSize, int16 bgScale) {
 	_roomScale = bgScale;
 }
 
-void Camera::setFollow(WalkingCharacter *target) {
+void Camera::setFollow(WalkingCharacter *target, bool catchUp) {
 	_followTarget = target;
 	_lastUpdateTime = g_system->getMillis();
+	_catchUp = catchUp;
 	if (target == nullptr)
 		_isChanging = false;
 }
@@ -147,7 +148,12 @@ void Camera::update() {
 	deltaTime = MAX(0.001f, MIN(0.5f, deltaTime));
 	_lastUpdateTime = now;
 
-	updateFollowing(deltaTime);
+	if (_catchUp && _followTarget != nullptr) {
+		for (int i = 0; i < 4; i++)
+			updateFollowing(50.0f);
+	}
+	else
+		updateFollowing(deltaTime);
 	setAppliedCenter(_usedCenter + Vector3d(_shake.getX(), _shake.getY(), 0.0f));
 }
 
diff --git a/engines/alcachofa/camera.h b/engines/alcachofa/camera.h
index fc3dd924ea9..ac04907493f 100644
--- a/engines/alcachofa/camera.h
+++ b/engines/alcachofa/camera.h
@@ -41,13 +41,14 @@ class Camera {
 public:
 	inline Math::Angle rotation() const { return _rotation; }
 	inline Math::Vector2d &shake() { return _shake; }
+	inline WalkingCharacter *followTarget() { return _followTarget; }
 
 	void update();
 	Math::Vector3d transform2Dto3D(Math::Vector3d v) const;
 	Math::Vector3d transform3Dto2D(Math::Vector3d v) const;
 	void resetRotationAndScale();
 	void setRoomBounds(Common::Point bgSize, int16 bgScale);
-	void setFollow(WalkingCharacter *target);
+	void setFollow(WalkingCharacter *target, bool catchUp = false);
 	void setPosition(Math::Vector2d v);
 
 private:
@@ -57,7 +58,8 @@ private:
 
 	uint32 _lastUpdateTime = 0;
 	bool _isChanging = false,
-		_isBraking = false;
+		_isBraking = false,
+		_catchUp = false;
 	float
 		_scale = 1.0f,
 		_roomScale = 1.0f,
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index d57c9120706..6639f8ef32d 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -115,6 +115,9 @@ void Player::changeRoom(const Common::String &targetRoomName, bool resetCamera)
 
 	if (resetCamera)
 		g_engine->camera().resetRotationAndScale();
+	WalkingCharacter *followTarget = g_engine->camera().followTarget();
+	if (followTarget != nullptr)
+		g_engine->camera().setFollow(followTarget, true);
 	_pressedObject = _selectedObject = nullptr;
 }
 
@@ -190,7 +193,7 @@ struct DoorTask : public Task {
 			_character->room() = _targetRoom;
 			_character->setPosition(_targetDoor->interactionPoint());
 			_character->stopWalking(_targetDoor->characterDirection());
-			g_engine->camera().setFollow(_character);
+			g_engine->camera().setFollow(_character, true);
 		}
 
 		// TODO: Start music on room change


Commit: ae9c2d0d2155b3782cb8c1e63524d02aa9085f83
    https://github.com/scummvm/scummvm/commit/ae9c2d0d2155b3782cb8c1e63524d02aa9085f83
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:47+02:00

Commit Message:
ALCACHOFA: Add text drawing

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


diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index 3cbd3e0d210..3f164c4b723 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -227,7 +227,11 @@ void ShapeObject::onHoverEnd() {
 }
 
 void ShapeObject::onHoverUpdate() {
-	// TODO: Add text request for name
+	g_engine->drawQueue().add<TextDrawRequest>(
+		g_engine->world().generalFont(),
+		name().c_str(),
+		g_engine->input().mousePos2D() - Point(0, 35),
+		-1, true, kWhite, 0);
 }
 
 void ShapeObject::onClick() {
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 755c4d7a722..bb7963eaf9d 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -198,6 +198,37 @@ void AnimationBase::loadMissingAnimation() {
 	_frames.push_back({ Point(), Point(), 1 });
 }
 
+// unfortunately ScummVMs BLEND_NORMAL does not blend alpha
+// but this also bad, let's find/discuss a better solution later
+void AnimationBase::fullBlend(const ManagedSurface &source, ManagedSurface &destination, int offsetX, int offsetY) {
+	assert(source.format == BlendBlit::getSupportedPixelFormat());
+	assert(destination.format == BlendBlit::getSupportedPixelFormat());
+	assert(offsetX >= 0 && offsetX + source.w <= destination.w);
+	assert(offsetY >= 0 && offsetY + source.h <= destination.h);
+
+	const byte *sourceLine = (byte *)source.getPixels();
+	byte *destinationLine = (byte *)destination.getPixels() + offsetY * destination.pitch + offsetX * 4;
+	for (int y = 0; y < source.h; y++) {
+		const byte *sourcePixel = sourceLine;
+		byte *destPixel = destinationLine;
+		for (int x = 0; x < source.w; x++) {
+			byte alpha = (*(const uint32 *)sourcePixel) & 0xff;
+			for (int i = 1; i < 4; i++)
+				destPixel[i] = ((byte)(alpha * sourcePixel[i] / 255)) + ((byte)((255 - alpha) * destPixel[i] / 255));
+			destPixel[0] = alpha + ((byte)((255 - alpha) * destPixel[0] / 255));
+			sourcePixel += 4;
+			destPixel += 4;
+		}
+		sourceLine += source.pitch;
+		destinationLine += destination.pitch;
+	}
+}
+
+Point AnimationBase::imageSize(int32 imageI) const {
+	auto image = _images[imageI];
+	return image == nullptr ? Point() : Point(image->w, image->h);
+}
+
 Animation::Animation(String fileName, AnimationFolder folder)
 	: AnimationBase(fileName, folder) {
 }
@@ -274,37 +305,6 @@ int32 Animation::frameAtTime(uint32 time) const {
 	return -1;
 }
 
-Point Animation::imageSize(int32 imageI) const {
-	auto image = _images[imageI];
-	return image == nullptr ? Point() : Point(image->w, image->h);
-}
-
-// unfortunately ScummVMs BLEND_NORMAL does not blend alpha
-// but this also bad, let's find/discuss a better solution later
-static void fullBlend(const ManagedSurface &source, ManagedSurface &destination, int offsetX, int offsetY) {
-	assert(source.format == BlendBlit::getSupportedPixelFormat());
-	assert(destination.format == BlendBlit::getSupportedPixelFormat());
-	assert(offsetX >= 0 && offsetX + source.w <= destination.w);
-	assert(offsetY >= 0 && offsetY + source.h <= destination.h);
-
-	const byte *sourceLine = (byte *)source.getPixels();
-	byte *destinationLine = (byte *)destination.getPixels() + offsetY * destination.pitch + offsetX * 4;
-	for (int y = 0; y < source.h; y++) {
-		const byte *sourcePixel = sourceLine;
-		byte *destPixel = destinationLine;
-		for (int x = 0; x < source.w; x++) {
-			byte alpha = (*(const uint32 *)sourcePixel) & 0xff;
-			for (int i = 1; i < 4; i++)
-				destPixel[i] = ((byte)(alpha * sourcePixel[i] / 255)) + ((byte)((255 - alpha) * destPixel[i] / 255));
-			destPixel[0] = alpha + ((byte)((255 - alpha) * destPixel[0] / 255));
-			sourcePixel += 4;
-			destPixel += 4;
-		}
-		sourceLine += source.pitch;
-		destinationLine += destination.pitch;
-	}
-}
-
 void Animation::prerenderFrame(int32 frameI) {
 	assert(frameI >= 0 && (uint)frameI < frameCount());
 	if (frameI == _renderedFrameI)
@@ -332,8 +332,6 @@ void Animation::prerenderFrame(int32 frameI) {
 	_renderedFrameI = frameI;
 }
 
-
-
 void Animation::draw2D(int32 frameI, Vector2d center, float scale, BlendMode blendMode, Color color) {
 	prerenderFrame(frameI);
 	auto bounds = frameBounds(frameI);
@@ -389,6 +387,73 @@ void Animation::drawEffect(int32 frameI, Vector3d topLeft, Vector2d tiling, Vect
 	renderer.quad(as2D(topLeft), size, kWhite, rotation, texMin + texOffset, texMax + texOffset);
 }
 
+Font::Font(String fileName) : AnimationBase(fileName) {}
+
+static int16 nextPowerOfTwo(int16 v) {
+	// adapted from https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2
+	assert(v > 0);
+	v--;
+	v |= v >> 1;
+	v |= v >> 2;
+	v |= v >> 4;
+	v |= v >> 8;
+	return v + 1;
+}
+
+void Font::load() {
+	if (_isLoaded)
+		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
+	Point cellSize;
+	for (auto image : _images) {
+		assert(image != nullptr); // no fake pictures in fonts please
+		cellSize.x = MAX(cellSize.x, image->w);
+		cellSize.y = MAX(cellSize.y, image->h);
+	}
+
+	_texMins.resize(_images.size());
+	_texMaxs.resize(_images.size());
+	ManagedSurface atlasSurface(nextPowerOfTwo(cellSize.x * 16), nextPowerOfTwo(cellSize.y * 16), BlendBlit::getSupportedPixelFormat());
+	cellSize.x = atlasSurface.w / 16;
+	cellSize.y = atlasSurface.h / 16;
+	const float invWidth = 1.0f / atlasSurface.w;
+	const float invHeight = 1.0f / atlasSurface.h;
+	for (uint i = 0; i < _images.size(); i++) {
+		int offsetX = (i % 16) * cellSize.x + (cellSize.x - _images[i]->w) / 2;
+		int offsetY = (i / 16) * cellSize.y + (cellSize.y - _images[i]->h) / 2;
+		fullBlend(*_images[i], atlasSurface, offsetX, offsetY);
+
+		_texMins[i].setX(offsetX * invWidth);
+		_texMins[i].setY(offsetY * invHeight);
+		_texMaxs[i].setX((offsetX + _images[i]->w) * invWidth);
+		_texMaxs[i].setY((offsetY + _images[i]->h) * invHeight);
+	}
+	_texture = g_engine->renderer().createTexture(atlasSurface.w, atlasSurface.h, false);
+	_texture->update(atlasSurface);
+	debug("Rendered font atlas %s at %dx%d", _fileName.c_str(), atlasSurface.w, atlasSurface.h);
+}
+
+void Font::freeImages() {
+	if (!_isLoaded)
+		return;
+	AnimationBase::freeImages();
+	_texture.reset();
+	_texMins.clear();
+	_texMaxs.clear();
+}
+
+void Font::drawCharacter(int32 imageI, Point centerPoint, Color color) {
+	assert(imageI >= 0 && (uint)imageI < _images.size());
+	Vector2d center = as2D(centerPoint + _imageOffsets[imageI]);
+	Vector2d size(_images[imageI]->w, _images[imageI]->h);
+
+	auto &renderer = g_engine->renderer();
+	renderer.setTexture(_texture.get());
+	renderer.setBlendMode(BlendMode::Tinted);
+	renderer.quad(center, size, color, Angle(), _texMins[imageI], _texMaxs[imageI]);
+}
+
 Graphic::Graphic() {
 }
 
@@ -541,6 +606,124 @@ void SpecialEffectDrawRequest::draw() {
 	_animation->drawEffect(_frameI, _topLeft, _tiling, _texOffset, _blendMode);
 }
 
+static const byte *trimLeading(const byte *text) {
+	while (*text && *text <= ' ')
+		text++;
+	return text;
+}
+
+static const byte *trimTrailing(const byte *text, const byte *begin) {
+	while (text != begin && *text <= ' ')
+		text--;
+	return text;
+}
+
+static Point characterSize(const Font &font, byte ch) {
+	if (ch <= ' ' || 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)
+	, _color(color) {
+	const int screenW = g_system->getWidth();
+	const int screenH = g_system->getHeight();
+	if (maxWidth < 0)
+		maxWidth = screenW;
+
+	// allocate on drawQueue to prevent having destruct it
+	assert(originalText != nullptr);
+	auto textLen = strlen(originalText);
+	char *text = (char*)g_engine->drawQueue().allocator().allocateRaw(textLen + 1, 1);
+	memcpy(text, originalText, textLen + 1);
+
+	// split into trimmed lines
+	uint lineCount = 0;
+	const byte *itChar = (byte*)text, *itLine = (byte*)text;
+	int lineWidth = 0;
+	while (true) {
+		if (lineCount >= kMaxLines)
+			error("Text to be rendered has too many lines, check text validity and max line count");
+
+		if (*itChar != '\r' && *itChar)
+			lineWidth += characterSize(font, *itChar).x;
+		if (lineWidth <= maxWidth && *itChar != '\r' && *itChar) {
+			itChar++;
+			continue;
+		}
+		// now we are in new-line territory
+
+		if (centered) {
+			auto itLineEnd = trimTrailing(itChar, itLine);
+			itLine = trimLeading(itLine);
+			_allLines[lineCount] = TextLine(itLine, itLineEnd - itLine + 1);
+			itChar = trimLeading(itChar);
+		}
+		else
+			_allLines[lineCount] = TextLine(itLine, itChar - itLine);
+		lineCount++;
+		itLine = itChar;
+
+		if (!*itChar)
+			break;
+	}
+	_lines = Span<TextLine>(_allLines, lineCount);
+	_posX = Span<int>(_allPosX, lineCount);
+
+	// calc line widths and max line width
+	_width = 0;
+	for (uint i = 0; i < lineCount; i++) {
+		lineWidth = 0;
+		for (auto ch : _lines[i]) {
+			if (ch != '\r' && ch)
+				lineWidth += characterSize(font, ch).x;
+		}
+		_posX[i] = lineWidth;
+		_width = MAX(_width, lineWidth);
+	}
+
+	// setup line positions
+	if (centered) {
+		if (pos.x - _width / 2 < 0)
+			pos.x = _width / 2 + 1;
+		if (pos.x + _width / 2 >= screenW)
+			pos.x = screenW - _width / 2 - 1;
+		for (auto &linePosX : _posX)
+			linePosX = pos.x - linePosX / 2;
+	}
+	else
+		fill(_posX.begin(), _posX.end(), pos.x);
+
+	// setup height and y position
+	_height = (int)lineCount * (font.imageSize(0).y * 4 / 3);
+	_posY = pos.y;
+	if (centered)
+		_posY -= _height / 2;
+	if (_posY < 0)
+		_posY = 0;
+	if (_posY + _height >= screenH)
+		_posY = screenH - _height;
+}
+
+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 > ' ' && ch < _font.imageCount())
+				_font.drawCharacter(ch - ' ', Point(cursor.x, cursor.y), _color);
+			cursor.x += charSize.x;
+		}
+		cursor.y += spaceSize.y * 4 / 3;
+	}
+}
+
 DrawQueue::DrawQueue(IRenderer *renderer)
 	: _renderer(renderer)
 	, _allocator(1024) {
@@ -593,8 +776,8 @@ void *BumpAllocator::allocateRaw(size_t size, size_t align) {
 	assert(size <= _pageSize);
 	uintptr_t page = (uintptr_t)_pages[_pageI];
 	uintptr_t top = page + _used;
-	top = top + align - 1;
-	top = top - (top % align);
+	top += align - 1;
+	top -= top % align;
 	if (page + _pageSize - top >= size) {
 		_used = top + size - page;
 		return (void *)top;
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 2f2306ddd30..4395d269f40 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -142,6 +142,14 @@ protected:
 	void loadMissingAnimation();
 	void freeImages();
 	Graphics::ManagedSurface *readImage(Common::SeekableReadStream &stream) const;
+	Common::Point imageSize(int32 imageI) const;
+	inline bool isLoaded() const { return _isLoaded; }
+
+	static void fullBlend(
+		const Graphics::ManagedSurface &source,
+		Graphics::ManagedSurface &destination,
+		int offsetX,
+		int offsetY);
 
 	static constexpr const uint kMaxSpriteIDs = 256;
 	Common::String _fileName;
@@ -169,7 +177,7 @@ public:
 	void load();
 	void freeImages();
 
-	inline bool isLoaded() const { return _isLoaded; }
+	using AnimationBase::isLoaded;
 	inline uint spriteCount() const { return _spriteBases.size(); }
 	inline uint frameCount() const { return _frames.size(); }
 	inline uint32 frameDuration(int32 frameI) const { return _frames[frameI]._duration; }
@@ -179,7 +187,7 @@ public:
 	Common::Point totalFrameOffset(int32 frameI) const;
 	int32 frameAtTime(uint32 time) const;
 	int32 imageIndex(int32 frameI, int32 spriteI) const;
-	Common::Point imageSize(int32 imageI) const;
+	using AnimationBase::imageSize;
 
 	void draw2D(
 		int32 frameI,
@@ -214,7 +222,20 @@ private:
 };
 
 class Font : private AnimationBase {
+public:
+	Font(Common::String fileName);
+
+	void load();
+	void freeImages();
+	void drawCharacter(int32 imageI, Common::Point center, Color color);
+
+	using AnimationBase::isLoaded;
+	using AnimationBase::imageSize;
+	inline uint imageCount() const { return _images.size(); }
 
+private:
+	Common::Array<Math::Vector2d> _texMins, _texMaxs;
+	Common::ScopedPtr<ITexture> _texture;
 };
 
 class Graphic {
@@ -326,6 +347,33 @@ private:
 	BlendMode _blendMode;
 };
 
+class TextDrawRequest : public IDrawRequest {
+public:
+	TextDrawRequest(
+		Font &font,
+		const char *text,
+		Common::Point pos,
+		int maxWidth,
+		bool centered,
+		Color color,
+		int8 order);
+
+	inline Common::Point size() const { return { (int16)_width, (int16)_height }; }
+	virtual void draw() override;
+
+private:
+	static constexpr uint kMaxLines = 8;
+	using TextLine = Common::Span<const byte>; ///< byte to convert 128+ characters to image indices
+
+	Font &_font;
+	int _posY, _height, _width;
+	Color _color;
+	Common::Span<TextLine> _lines;
+	Common::Span<int> _posX;
+	TextLine _allLines[kMaxLines];
+	int _allPosX[kMaxLines];
+};
+
 class BumpAllocator {
 public:
 	BumpAllocator(size_t pageSize);
@@ -354,6 +402,7 @@ public:
 	inline void add(Args&&... args) {
 		addRequest(_allocator.allocate<T>(Common::forward<Args>(args)...));
 	}
+	inline BumpAllocator &allocator() { return _allocator; }
 
 	void clear();
 	void setLodBias(int8 orderFrom, int8 orderTo, float newLodBias);
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index a69e61ab2e6..b3580ec65a1 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -217,16 +217,20 @@ void Room::updateObjects() {
 }
 
 void Room::drawObjects() {
-	for (auto *object : _objects)
-		object->draw();
+	for (auto *object : _objects) {
+		if (object->room() == g_engine->player().currentRoom())
+			object->draw();
+	}
 }
 
 void Room::drawDebug() {
 	auto renderer = dynamic_cast<IDebugRenderer *>(&g_engine->renderer());
 	if (renderer == nullptr || !g_engine->console().isAnyDebugDrawingOn())
 		return;
-	for (auto *object : _objects)
-		object->drawDebug();
+	for (auto *object : _objects) {
+		if (object->room() == g_engine->player().currentRoom())
+			object->drawDebug();
+	}
 	if (_activeFloorI < 0)
 		return;
 	if (_activeFloorI >= 0 && g_engine->console().showFloor())
@@ -258,8 +262,9 @@ ShapeObject *Room::getSelectedObject(ShapeObject *best) const {
 	for (auto object : _objects) {
 		auto shape = object->shape();
 		auto shapeObject = dynamic_cast<ShapeObject *>(object);
-		if (!object->isEnabled() || shape == nullptr || shapeObject == nullptr ||
-			object->room() != this || // e.g. a main character that is in another room
+		if (!object->isEnabled() ||
+			shape == nullptr || shapeObject == nullptr ||
+			object->room() != g_engine->player().currentRoom() || // e.g. a main character that is in another room
 			!shape->contains(g_engine->input().mousePos3D()))
 			continue;
 		if (best == nullptr || shapeObject->order() < best->order())
@@ -351,6 +356,11 @@ World::World() {
 	if (_mortadelo == nullptr)
 		error("Could not find MORTADELO");
 
+	_generalFont.reset(new Font(getGlobalAnimationName(GlobalAnimationKind::GeneralFont)));
+	_generalFont->load();
+	_dialogFont.reset(new Font(getGlobalAnimationName(GlobalAnimationKind::DialogFont)));
+	_dialogFont->load();
+
 	_inventory->initItems();
 }
 
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index 7c18b2df093..1f4d83fedaf 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -115,7 +115,7 @@ private:
 
 enum class GlobalAnimationKind {
 	GeneralFont = 0,
-	TextFont,
+	DialogFont,
 	Cursor,
 	MortadeloIcon,
 	FilemonIcon,
@@ -137,10 +137,12 @@ public:
 	using RoomIterator = Common::Array<const Room *>::const_iterator;
 	inline RoomIterator beginRooms() const { return _rooms.begin(); }
 	inline RoomIterator endRooms() const { return _rooms.end(); }
-	inline Room &globalRoom() const { return *_globalRoom; }
-	inline Inventory &inventory() const { return *_inventory; }
-	inline MainCharacter &filemon() const { return *_filemon; }
-	inline MainCharacter &mortadelo() const { return *_mortadelo; }
+	inline Room &globalRoom() const { assert(_globalRoom != nullptr); return *_globalRoom; }
+	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 Font &generalFont() const { assert(_generalFont != nullptr); return *_generalFont; }
+	inline Font &dialogFont() const { assert(_dialogFont != nullptr); return *_dialogFont; }
 	inline const Common::String &initScriptName() const { return _initScriptName; }
 	inline uint8 loadedMapCount() const { return _loadedMapCount; }
 
@@ -168,6 +170,7 @@ private:
 	Room *_globalRoom;
 	Inventory *_inventory;
 	MainCharacter *_filemon, *_mortadelo;
+	Common::ScopedPtr<Font> _generalFont, _dialogFont;
 	uint8 _loadedMapCount = 0;
 };
 


Commit: 72b4c99bf3238ff9c0ed4809885742961317b36c
    https://github.com/scummvm/scummvm/commit/72b4c99bf3238ff9c0ed4809885742961317b36c
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:47+02:00

Commit Message:
ALCACHOFA: Load and display localized names

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


diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index 3f164c4b723..47ed70df3a0 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -229,7 +229,7 @@ void ShapeObject::onHoverEnd() {
 void ShapeObject::onHoverUpdate() {
 	g_engine->drawQueue().add<TextDrawRequest>(
 		g_engine->world().generalFont(),
-		name().c_str(),
+		g_engine->world().getLocalizedName(name()),
 		g_engine->input().mousePos2D() - Point(0, 35),
 		-1, true, kWhite, 0);
 }
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index bb7963eaf9d..9ef040a0d7e 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -619,7 +619,7 @@ static const byte *trimTrailing(const byte *text, const byte *begin) {
 }
 
 static Point characterSize(const Font &font, byte ch) {
-	if (ch <= ' ' || ch >= font.imageCount())
+	if (ch <= ' ' || ch - ' ' >= font.imageCount())
 		ch = 0;
 	else
 		ch -= ' ';
@@ -716,7 +716,7 @@ void TextDrawRequest::draw() {
 		cursor.x = _posX[i];
 		for (auto ch : _lines[i]) {
 			const Point charSize = characterSize(_font, ch);
-			if (ch > ' ' && ch < _font.imageCount())
+			if (ch > ' ' && ch - ' ' < _font.imageCount())
 				_font.drawCharacter(ch - ' ', Point(cursor.x, cursor.y), _color);
 			cursor.x += charSize.x;
 		}
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index b3580ec65a1..267a4de29cf 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -342,6 +342,7 @@ World::World() {
 		if (loadWorldFile(*itMapFile))
 			_loadedMapCount++;
 	}
+	loadLocalizedNames();
 
 	_globalRoom = getRoomByName("GLOBAL");
 	if (_globalRoom == nullptr)
@@ -498,4 +499,61 @@ bool World::loadWorldFile(const char *path) {
 	return true;
 }
 
+/**
+ * @brief Behold the incredible encryption of text files:
+ *   - first 32 bytes are cipher
+ *   - next byte is the XOR key
+ *   - next 4 bytes are garbage
+ *   - rest of the file is cipher
+ */
+static void loadEncryptedFile(const char *path, 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() - 5 + 1);
+	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;
+	output.back() = ' '; // one for good measure and a zero-terminator
+}
+
+static char *trimTrailing(char *start, char *end) {
+	while (start < end && isSpace(end[-1]))
+		end--;
+	return end;
+}
+
+void World::loadLocalizedNames() {
+	loadEncryptedFile("Textos/OBJETOS.nkr", _namesChunk);
+	char *lineStart = _namesChunk.begin(), *fileEnd = _namesChunk.end();
+	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);
+		if (valueEnd == keyEnd + 1)
+			error("Invalid localized name value");
+
+		*keyEnd = 0;
+		*valueEnd = 0;
+		_localizedNames[lineStart] = keyEnd + 1;
+
+		lineStart = lineEnd + 1;
+	}
+}
+
+const char *World::getLocalizedName(const String &name) const {
+	const char *localizedName;
+	return _localizedNames.tryGetVal(name.c_str(), localizedName)
+		? localizedName
+		: name.c_str();
+}
+
 }
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index 1f4d83fedaf..b87e0dbbb1e 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -158,11 +158,20 @@ public:
 	ObjectBase *getObjectByName(MainCharacterKind character, const Common::String &name) const;
 	ObjectBase *getObjectByNameFromAnyRoom(const Common::String &name) const;
 	const Common::String &getGlobalAnimationName(GlobalAnimationKind kind) const;
+	const char *getLocalizedName(const Common::String &name) const;
+	const char *getDialogLine(int32 dialogId) const;
 
 	void toggleObject(MainCharacterKind character, const Common::String &objName, bool isEnabled);
 
 private:
 	bool loadWorldFile(const char *path);
+	void loadLocalizedNames();
+	void loadDialogLines();
+
+	// the default Hash<const char*> works on the characters, but the default EqualTo compares pointers...
+	struct StringEqualTo {
+		bool operator()(const char *a, const char *b) const { return strcmp(a, b) == 0; }
+	};
 
 	Common::Array<Room *> _rooms;
 	Common::String _globalAnimationNames[(int)GlobalAnimationKind::Count];
@@ -172,6 +181,11 @@ private:
 	MainCharacter *_filemon, *_mortadelo;
 	Common::ScopedPtr<Font> _generalFont, _dialogFont;
 	uint8 _loadedMapCount = 0;
+	Common::HashMap<const char *, const char *,
+		Common::Hash<const char*>,
+		StringEqualTo> _localizedNames;
+	Common::Array<const char *> _dialogLines;
+	Common::Array<char> _namesChunk, _dialogChunk; ///< holds the memory for localizedNames / dialogLines
 };
 
 }


Commit: 6938f1c87bfe32b8aaa3c4060f33310c3f0c0625
    https://github.com/scummvm/scummvm/commit/6938f1c87bfe32b8aaa3c4060f33310c3f0c0625
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:47+02:00

Commit Message:
ALCACHOFA: Load dialog lines

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


diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 9ef040a0d7e..a442c982945 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -619,7 +619,7 @@ static const byte *trimTrailing(const byte *text, const byte *begin) {
 }
 
 static Point characterSize(const Font &font, byte ch) {
-	if (ch <= ' ' || ch - ' ' >= font.imageCount())
+	if (ch <= ' ' || (uint)(ch - ' ') >= font.imageCount())
 		ch = 0;
 	else
 		ch -= ' ';
@@ -716,7 +716,7 @@ void TextDrawRequest::draw() {
 		cursor.x = _posX[i];
 		for (auto ch : _lines[i]) {
 			const Point charSize = characterSize(_font, ch);
-			if (ch > ' ' && ch - ' ' < _font.imageCount())
+			if (ch > ' ' && (uint)(ch - ' ') < _font.imageCount())
 				_font.drawCharacter(ch - ' ', Point(cursor.x, cursor.y), _color);
 			cursor.x += charSize.x;
 		}
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 267a4de29cf..c2fedbd6e3f 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -343,6 +343,7 @@ World::World() {
 			_loadedMapCount++;
 	}
 	loadLocalizedNames();
+	loadDialogLines();
 
 	_globalRoom = getRoomByName("GLOBAL");
 	if (_globalRoom == nullptr)
@@ -448,6 +449,19 @@ const Common::String &World::getGlobalAnimationName(GlobalAnimationKind kind) co
 	return _globalAnimationNames[kindI];
 }
 
+const char *World::getLocalizedName(const String &name) const {
+	const char *localizedName;
+	return _localizedNames.tryGetVal(name.c_str(), localizedName)
+		? localizedName
+		: name.c_str();
+}
+
+const char *World::getDialogLine(int32 dialogId) const {
+	if (dialogId < 0 || (uint)dialogId >= _dialogLines.size())
+		error("Invalid dialog line index %d", dialogId);
+	return _dialogLines[dialogId];
+}
+
 static Room *readRoom(World *world, ReadStream &stream) {
 	const auto type = readVarString(stream);
 	if (type == Room::kClassName)
@@ -544,16 +558,31 @@ void World::loadLocalizedNames() {
 		*keyEnd = 0;
 		*valueEnd = 0;
 		_localizedNames[lineStart] = keyEnd + 1;
-
 		lineStart = lineEnd + 1;
 	}
 }
 
-const char *World::getLocalizedName(const String &name) const {
-	const char *localizedName;
-	return _localizedNames.tryGetVal(name.c_str(), localizedName)
-		? localizedName
-		: name.c_str();
+void World::loadDialogLines() {
+	loadEncryptedFile("Textos/DIALOGOS.nkr", _dialogChunk);
+	char *lineStart = _dialogChunk.begin(), *fileEnd = _dialogChunk.end();
+	while (lineStart < fileEnd) {
+		char *lineEnd = find(lineStart, fileEnd, '\n');
+		char *firstQuote = find(lineStart, lineEnd, '\"');
+		if (firstQuote == lineEnd)
+			error("Invalid dialog line - first quote");
+		char *secondQuote = find(firstQuote + 1, lineEnd, '\"');
+		if (secondQuote == lineEnd) {
+			// unfortunately one invalid line in the game
+			if (_dialogLines.size() != 4542)
+				error("Invalid dialog line - second quote");
+			firstQuote = lineStart; // for the invalid one save an empty string
+			secondQuote = firstQuote + 1;
+		}
+
+		*secondQuote = 0;
+		_dialogLines.push_back(firstQuote + 1);
+		lineStart = lineEnd + 1;
+	}
 }
 
 }


Commit: b4a467c892adb22df6338b7d44badad3c3a4b318
    https://github.com/scummvm/scummvm/commit/b4a467c892adb22df6338b7d44badad3c3a4b318
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:47+02:00

Commit Message:
ALCACHOFA: Add voice sounds and sayText kernel task

and fix a lot of bugs in the process

Changed paths:
  A engines/alcachofa/sounds.cpp
  A engines/alcachofa/sounds.h
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/alcachofa.h
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/objects.h
    engines/alcachofa/rooms.h
    engines/alcachofa/scheduler.cpp
    engines/alcachofa/scheduler.h
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index a43ebf7d16f..959f57a1c83 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -66,6 +66,8 @@ Common::Error AlcachofaEngine::run() {
 	_script.reset(new Script());
 	_player.reset(new Player());
 
+	_script->createProcess(MainCharacterKind::None, "Inicializar_Variables");
+
 	_player->changeRoom("MINA", true);
 
 	Common::Event e;
@@ -77,6 +79,7 @@ Common::Error AlcachofaEngine::run() {
 				continue;
 		}
 
+		_sounds.update();
 		_renderer->begin();
 		_drawQueue->clear();
 		_camera.shake() = Vector2d();
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index 117b6b4ddab..2d643c1c8de 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -37,6 +37,7 @@
 #include "alcachofa/detection.h"
 #include "alcachofa/camera.h"
 #include "alcachofa/input.h"
+#include "alcachofa/sounds.h"
 #include "alcachofa/player.h"
 #include "alcachofa/scheduler.h"
 #include "alcachofa/console.h"
@@ -64,6 +65,7 @@ public:
 	inline DrawQueue &drawQueue() { return *_drawQueue; }
 	inline Camera &camera() { return _camera; }
 	inline Input &input() { return _input; }
+	inline Sounds &sounds() { return _sounds; }
 	inline Player &player() { return *_player; }
 	inline World &world() { return *_world; }
 	inline Script &script() { return *_script; }
@@ -122,6 +124,7 @@ private:
 	Common::ScopedPtr<Player> _player;
 	Camera _camera;
 	Input _input;
+	Sounds _sounds;
 	Scheduler _scheduler;
 };
 
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index d7051f7cbec..e8053f9097e 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -258,6 +258,59 @@ void Character::trigger(const char *action) {
 	g_engine->player().triggerObject(this, action);
 }
 
+struct SayTextTask : public Task {
+	SayTextTask(Process &process, Character *character, int32 dialogId)
+		: Task(process)
+		, _character(character)
+		, _dialogId(dialogId) { }
+
+	virtual TaskReturn run() override {
+		TASK_BEGIN;
+		_character->_isTalking = true;
+		graphicOf(_character->_curTalkingObject, &_character->_graphicTalking)->start(true);
+		while (true) {
+			if (_soundId == kInvalidSoundID)
+				_soundId = g_engine->sounds().playVoice(
+					String::format(_character == &g_engine->world().mortadelo() ? "M%04d" : "%04d", _dialogId),
+					0);
+			g_engine->sounds().setAppropriateVolume(_soundId, process().character(), _character);
+			if (!g_engine->sounds().isAlive(_soundId) || g_engine->input().wasAnyMouseReleased())
+				_character->_isTalking = false;
+
+			if (true && // TODO: Add game option for subtitles
+				process().isActiveForPlayer()) {
+				g_engine->drawQueue().add<TextDrawRequest>(
+					g_engine->world().dialogFont(),
+					g_engine->world().getDialogLine(_dialogId),
+					Point(g_system->getWidth() / 2, g_system->getHeight() - 200),
+					-1, true, kWhite, 0);
+			}
+			// TODO: Add lip syng for sayText
+
+			if (!_character->_isTalking) {
+				g_engine->sounds().fadeOut(_soundId, 100);
+				TASK_WAIT(delay(200));
+				TASK_RETURN(0);
+			}
+			TASK_YIELD;
+		}
+		TASK_END;
+	}
+
+	virtual void debugPrint() override {
+		g_engine->console().debugPrintf("SayText %s, %d\n", _character->name().c_str(), _dialogId);
+	}
+
+private:
+	Character *_character;
+	int32 _dialogId;
+	SoundID _soundId = kInvalidSoundID;
+};
+
+Task *Character::sayText(Process &process, int32 dialogId) {
+	return new SayTextTask(process, this, dialogId);
+}
+
 const char *WalkingCharacter::typeName() const { return "WalkingCharacter"; }
 
 WalkingCharacter::WalkingCharacter(Room *room, ReadStream &stream)
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index a442c982945..dfe32d14778 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -606,14 +606,14 @@ void SpecialEffectDrawRequest::draw() {
 	_animation->drawEffect(_frameI, _topLeft, _tiling, _texOffset, _blendMode);
 }
 
-static const byte *trimLeading(const byte *text) {
-	while (*text && *text <= ' ')
+static const byte *trimLeading(const byte *text, const byte *end) {
+	while (*text && text < end && *text <= ' ')
 		text++;
 	return text;
 }
 
-static const byte *trimTrailing(const byte *text, const byte *begin) {
-	while (text != begin && *text <= ' ')
+static const byte *trimTrailing(const byte *text, const byte *begin, bool trimSpaces) {
+	while (text != begin && (*text <= ' ') == trimSpaces)
 		text--;
 	return text;
 }
@@ -643,7 +643,7 @@ TextDrawRequest::TextDrawRequest(Font &font, const char *originalText, Point pos
 
 	// split into trimmed lines
 	uint lineCount = 0;
-	const byte *itChar = (byte*)text, *itLine = (byte*)text;
+	const byte *itChar = (byte *)text, *itLine = (byte *)text, *textEnd = itChar + textLen + 1;
 	int lineWidth = 0;
 	while (true) {
 		if (lineCount >= kMaxLines)
@@ -658,14 +658,17 @@ TextDrawRequest::TextDrawRequest(Font &font, const char *originalText, Point pos
 		// now we are in new-line territory
 
 		if (centered) {
-			auto itLineEnd = trimTrailing(itChar, itLine);
-			itLine = trimLeading(itLine);
-			_allLines[lineCount] = TextLine(itLine, itLineEnd - itLine + 1);
-			itChar = trimLeading(itChar);
+			if (*itChar > ' ')
+				itChar = trimTrailing(itChar, itLine, false); // trim last word
+			itChar = trimTrailing(itChar, itLine, true) + 1;
+			itLine = trimLeading(itLine, itChar);
+			_allLines[lineCount] = TextLine(itLine, itChar - itLine);
+			itChar = trimLeading(itChar, textEnd);
 		}
 		else
 			_allLines[lineCount] = TextLine(itLine, itChar - itLine);
 		lineCount++;
+		lineWidth = 0;
 		itLine = itChar;
 
 		if (!*itChar)
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 0d5f7e26636..24cd9928ecb 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -388,7 +388,10 @@ public:
 	virtual void trigger(const char *action) override;
 	virtual const char *typeName() const;
 
+	Task *sayText(Process &process, int32 dialogId);
+
 protected:
+	friend struct SayTextTask;
 	void syncObjectAsString(Common::Serializer &serializer, ObjectBase *&object);
 	void updateTalkingAnimation();
 
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index b87e0dbbb1e..96ff4da9a02 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -22,7 +22,7 @@
 #ifndef ROOMS_H
 #define ROOMS_H
 
-#include "Objects.h"
+#include "objects.h"
 
 namespace Alcachofa {
 
diff --git a/engines/alcachofa/scheduler.cpp b/engines/alcachofa/scheduler.cpp
index c8776fa611c..e692e2c3a93 100644
--- a/engines/alcachofa/scheduler.cpp
+++ b/engines/alcachofa/scheduler.cpp
@@ -88,6 +88,10 @@ Process::~Process() {
 		delete _tasks.pop();
 }
 
+bool Process::isActiveForPlayer() const {
+	return _character == MainCharacterKind::None || _character == g_engine->player().activeCharacterKind();
+}
+
 TaskReturnType Process::run() {
 	while (!_tasks.empty()) {
 		TaskReturn ret = _tasks.top()->run();
diff --git a/engines/alcachofa/scheduler.h b/engines/alcachofa/scheduler.h
index a5317482bb9..22c335c3602 100644
--- a/engines/alcachofa/scheduler.h
+++ b/engines/alcachofa/scheduler.h
@@ -128,6 +128,7 @@ public:
 	inline MainCharacterKind character() const { return _character; }
 	inline int32 returnValue() const { return _lastReturnValue; }
 	inline Common::String &name() { return _name; }
+	bool isActiveForPlayer() const; ///< and thus should e.g. draw subtitles or effects
 
 	TaskReturnType run();
 	void debugPrint();
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 6760473afe2..a689a179648 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -244,20 +244,21 @@ struct ScriptTask : public Task {
 			case ScriptOp::Add:
 				pushNumber(popNumber() + popNumber());
 				break;
+			// flipped operators to not use a temporary
 			case ScriptOp::Sub:
-				pushNumber(popNumber() - popNumber());
+				pushNumber(-popNumber() + popNumber());
 				break;
 			case ScriptOp::Less:
-				pushNumber(popNumber() < popNumber());
+				pushNumber(popNumber() >= popNumber());
 				break;
 			case ScriptOp::Greater:
-				pushNumber(popNumber() > popNumber());
+				pushNumber(popNumber() <= popNumber());
 				break;
 			case ScriptOp::LessEquals:
-				pushNumber(popNumber() <= popNumber());
+				pushNumber(popNumber() >= popNumber());
 				break;
 			case ScriptOp::GreaterEquals:
-				pushNumber(popNumber() >= popNumber());
+				pushNumber(popNumber() <= popNumber());
 				break;
 			case ScriptOp::Equals:
 				pushNumber(popNumber() == popNumber());
@@ -437,9 +438,20 @@ private:
 		case ScriptKernelTask::ChangeCharacter:
 			warning("STUB KERNEL CALL: ChangeCharacter");
 			return TaskReturn::finish(0);
-		case ScriptKernelTask::SayText:
-			warning("STUB KERNEL CALL: SayText");
-			return TaskReturn::finish(0);
+		case ScriptKernelTask::SayText: {
+			const char *characterName = getStringArg(0);
+			int32 dialogId = getNumberArg(1);
+			if (strncmp(characterName, "MENU_", 5) == 0) {
+				warning("STUB: adding dialog menu line %d", dialogId);
+				return TaskReturn::finish(1);
+			}
+			Character *_character = strcmp(characterName, "AMBOS") == 0
+				? &g_engine->world().getMainCharacterByKind(process().character())
+				: dynamic_cast<Character *>(g_engine->world().getObjectByName(characterName));
+			if (_character == nullptr)
+				error("Invalid character for sayText: %s", characterName);
+			return TaskReturn::waitFor(_character->sayText(process(), dialogId));
+		};
 		case ScriptKernelTask::Go: {
 			auto characterObject = g_engine->world().getObjectByName(process().character(), getStringArg(0));
 			auto character = dynamic_cast<WalkingCharacter *>(characterObject);
@@ -632,7 +644,7 @@ private:
 	String _name;
 	uint32 _pc;
 	bool _returnsFromKernelCall = false;
-	bool _isFirstExecution = false;
+	bool _isFirstExecution = true;
 	FakeLock _lock;
 };
 
diff --git a/engines/alcachofa/sounds.cpp b/engines/alcachofa/sounds.cpp
new file mode 100644
index 00000000000..57952a47b44
--- /dev/null
+++ b/engines/alcachofa/sounds.cpp
@@ -0,0 +1,180 @@
+/* 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 "sounds.h"
+#include "rooms.h"
+#include "alcachofa.h"
+
+#include "common/file.h"
+#include "common/substream.h"
+#include "audio/audiostream.h"
+#include "audio/decoders/wave.h"
+#include "audio/decoders/adpcm.h"
+#include "audio/decoders/raw.h"
+
+using namespace Common;
+using namespace Audio;
+
+namespace Alcachofa {
+
+Sounds::Playback::Playback(uint32 id, SoundHandle handle, Mixer::SoundType type)
+	: _id(id), _handle(handle), _type(type) {}
+
+Sounds::Sounds() : _mixer(g_system->getMixer()) {
+	assert(_mixer != nullptr);
+}
+
+Sounds::~Sounds() {
+	_mixer->stopAll();
+}
+
+Sounds::Playback *Sounds::getPlaybackById(SoundID id) {
+	if (_playbacks.empty())
+		return nullptr;
+	uint first = 0, last = _playbacks.size() - 1;
+	while (first < last) {
+		uint mid = (first + last) / 2;
+		if (_playbacks[mid]._id == id)
+			return _playbacks.data() + mid;
+		else if (_playbacks[mid]._id < id)
+			first = mid + 1;
+		else
+			last = mid == 0 ? 0 : mid - 1;
+	}
+	return first == last && first < _playbacks.size()
+		? _playbacks.data() + first
+		: nullptr;
+}
+
+void Sounds::update() {
+	for (uint i = _playbacks.size(); i > 0; i--) {
+		Playback &playback = _playbacks[i - 1];
+		if (!_mixer->isSoundHandleActive(playback._handle))
+			_playbacks.erase(_playbacks.begin() + i - 1);
+		else if (playback._fadeDuration != 0) {
+			if (g_system->getMillis() >= playback._fadeStart + playback._fadeDuration) {
+				_mixer->stopHandle(playback._handle);
+				_playbacks.erase(_playbacks.begin() + i - 1);
+			}
+			else {
+				byte newVolume = (g_system->getMillis() - playback._fadeStart) * Mixer::kMaxChannelVolume / playback._fadeDuration;
+				_mixer->setChannelVolume(playback._handle, Mixer::kMaxChannelVolume - newVolume);
+			}
+		}
+	}
+}
+
+static AudioStream *loadSND(File *file) {
+	// 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");
+	uint16 format = file->readUint16LE();
+	uint16 channels = file->readUint16LE();
+	uint32 freq = file->readUint32LE();
+	file->skip(sizeof(uint32)); // bytesPerSecond, unnecessary for us
+	uint16 bytesPerBlock = file->readUint16LE();
+	uint16 bitsPerSample = file->readUint16LE();
+	if (endOfFormat >= 2 * sizeof(uint32) + 20) {
+		file->skip(sizeof(uint16)); // size of extra data
+		uint16 extra = file->readUint16LE();
+		bytesPerBlock = 4 * channels * ((extra + 14) / 8);
+	}
+	file->seek(endOfFormat, SEEK_SET);
+	auto subStream = new SeekableSubReadStream(file, (uint32)file->pos(), (uint32)file->size(), DisposeAfterUse::YES);
+	if (format == 1 && channels <= 2 && (bitsPerSample == 8 || bitsPerSample == 16))
+		return makeRawStream(subStream, (int)freq,
+			(channels == 2 ? FLAG_STEREO : 0) | (bitsPerSample == 16 ? FLAG_16BITS | FLAG_LITTLE_ENDIAN : FLAG_UNSIGNED));
+	else if (format == 17 && channels <= 2)
+		return makeADPCMStream(subStream, DisposeAfterUse::YES, 0, kADPCMMSIma, (int)freq, (int)channels, (uint32)bytesPerBlock);
+	error("Invalid SND file, format: %u, channels: %u, freq: %u, bps: %u", format, channels, freq, bitsPerSample);
+}
+
+static AudioStream *openAudio(const String &fileName) {
+	String path = String::format("Sonidos/%s.SND", fileName.c_str());
+	File *file = new File();
+	if (file->open(path.c_str()))
+		return 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;
+	error("Could not open audio file: %s", fileName.c_str());
+}
+
+SoundID Sounds::playVoice(const String &fileName, byte volume) {
+	AudioStream *stream = openAudio(fileName);
+	SoundHandle handle;
+	_mixer->playStream(Mixer::kSpeechSoundType, &handle, stream, -1, volume);
+	SoundID id = _nextID++;
+	_playbacks.push_back({ id, handle, Mixer::kSpeechSoundType });
+	return id;
+}
+
+void Sounds::stopVoice() {
+	for (uint i = _playbacks.size(); i > 0; i--) {
+		if (_playbacks[i - 1]._type == Mixer::kSpeechSoundType) {
+			_mixer->stopHandle(_playbacks[i - 1]._handle);
+			_playbacks.erase(_playbacks.begin() + i - 1);
+		}
+	}
+}
+
+bool Sounds::isAlive(SoundID id) {
+	Playback *playback = getPlaybackById(id);
+	return playback != nullptr && _mixer->isSoundHandleActive(playback->_handle);
+}
+
+void Sounds::setVolume(SoundID id, byte volume) {
+	Playback *playback = getPlaybackById(id);
+	if (playback != nullptr)
+		_mixer->setChannelVolume(playback->_handle, volume);
+}
+
+void Sounds::setAppropriateVolume(SoundID id,
+	MainCharacterKind processCharacter,
+	Character *speakingCharacter) {
+	static constexpr byte kAlmostMaxVolume = Mixer::kMaxChannelVolume * 9 / 10;
+
+	auto &player = g_engine->player();
+	byte newVolume;
+	if (player.activeCharacter() == nullptr || player.activeCharacter() == speakingCharacter)
+		newVolume = Mixer::kMaxChannelVolume;
+	else if (speakingCharacter != nullptr && speakingCharacter->room() == player.currentRoom())
+		newVolume = kAlmostMaxVolume;
+	else if (g_engine->world().getMainCharacterByKind(processCharacter).room() == player.currentRoom())
+		newVolume = kAlmostMaxVolume;
+	else
+		newVolume = 0;
+	setVolume(id, newVolume);
+}
+
+void Sounds::fadeOut(SoundID id, uint32 duration) {
+	Playback *playback = getPlaybackById(id);
+	if (playback != nullptr) {
+		playback->_fadeStart = g_system->getMillis();
+		playback->_fadeDuration = MAX<uint32>(duration, 1);
+	}
+}
+
+}
diff --git a/engines/alcachofa/sounds.h b/engines/alcachofa/sounds.h
new file mode 100644
index 00000000000..2a2629d2da7
--- /dev/null
+++ b/engines/alcachofa/sounds.h
@@ -0,0 +1,68 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SOUNDS_H
+#define SOUNDS_H
+
+#include "common.h"
+#include "audio/mixer.h"
+
+namespace Alcachofa {
+
+class Character;
+
+using SoundID = uint32;
+static constexpr SoundID kInvalidSoundID = 0;
+class Sounds {
+public:
+	Sounds();
+	~Sounds();
+
+	void update();
+	SoundID playVoice(const Common::String &fileName, byte volume = Audio::Mixer::kMaxChannelVolume);
+	void stopVoice();
+	void fadeOut(SoundID id, uint32 duration);
+	bool isAlive(SoundID id);
+	void setVolume(SoundID id, byte volume);
+	void setAppropriateVolume(SoundID id,
+		MainCharacterKind processCharacter,
+		Character *speakingCharacter);
+
+private:
+	struct Playback {
+		Playback(uint32 id, Audio::SoundHandle handle, Audio::Mixer::SoundType type);
+
+		SoundID _id;
+		Audio::SoundHandle _handle;
+		Audio::Mixer::SoundType _type;
+		uint32 _fadeStart = 0,
+			_fadeDuration = 0;
+	};
+	Playback *getPlaybackById(SoundID id);
+
+	Common::Array<Playback> _playbacks;
+	Audio::Mixer *_mixer;
+	SoundID _nextID = 1;
+};
+
+}
+
+#endif // SOUNDS_H


Commit: fff9c7cc09a845dfacd3cb1793a7e86b7f7a2261
    https://github.com/scummvm/scummvm/commit/fff9c7cc09a845dfacd3cb1793a7e86b7f7a2261
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:47+02:00

Commit Message:
ALCACHOFA: Refactor stream-helper into common

Changed paths:
  A engines/alcachofa/common.cpp
  R engines/alcachofa/stream-helper.cpp
  R engines/alcachofa/stream-helper.h
    engines/alcachofa/common.h
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/general-objects.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/rooms.cpp
    engines/alcachofa/script.cpp
    engines/alcachofa/shape.cpp
    engines/alcachofa/ui-objects.cpp


diff --git a/engines/alcachofa/stream-helper.cpp b/engines/alcachofa/common.cpp
similarity index 66%
rename from engines/alcachofa/stream-helper.cpp
rename to engines/alcachofa/common.cpp
index f849549f19a..f8bf519936d 100644
--- a/engines/alcachofa/stream-helper.cpp
+++ b/engines/alcachofa/common.cpp
@@ -19,14 +19,57 @@
  *
  */
 
-#include "stream-helper.h"
-
-#include "common/textconsole.h"
+#include "common.h"
 
 using namespace Common;
+using namespace Math;
 
 namespace Alcachofa {
 
+FakeSemaphore::FakeSemaphore(uint initialCount) : _counter(initialCount) {}
+
+FakeSemaphore::~FakeSemaphore() {
+	assert(_counter == 0);
+}
+
+FakeLock::FakeLock() : _semaphore(nullptr) {}
+
+FakeLock::FakeLock(FakeSemaphore &semaphore) : _semaphore(&semaphore) {
+	_semaphore->_counter++;
+}
+
+FakeLock::FakeLock(const FakeLock &other) : _semaphore(other._semaphore) {
+	assert(_semaphore != nullptr);
+	_semaphore->_counter++;
+}
+
+FakeLock::FakeLock(FakeLock &&other) noexcept : _semaphore(other._semaphore) {
+	other._semaphore = nullptr;
+}
+
+FakeLock::~FakeLock() {
+	if (_semaphore == nullptr)
+		return;
+	assert(_semaphore->_counter > 0);
+	_semaphore->_counter--;
+}
+
+Vector3d as3D(const Vector2d &v) {
+	return Vector3d(v.getX(), v.getY(), 0.0f);
+}
+
+Vector3d as3D(const Common::Point &p) {
+	return Vector3d((float)p.x, (float)p.y, 0.0f);
+}
+
+Vector2d as2D(const Vector3d &v) {
+	return Vector2d(v.x(), v.y());
+}
+
+Vector2d as2D(const Point &p) {
+	return Vector2d((float)p.x, (float)p.y);
+}
+
 bool readBool(ReadStream &stream) {
 	return stream.readByte() != 0;
 }
diff --git a/engines/alcachofa/common.h b/engines/alcachofa/common.h
index e8d83db85b8..cc1efc2f96b 100644
--- a/engines/alcachofa/common.h
+++ b/engines/alcachofa/common.h
@@ -24,6 +24,9 @@
 
 #include "common/scummsys.h"
 #include "common/rect.h"
+#include "common/serializer.h"
+#include "common/stream.h"
+#include "common/stack.h"
 #include "math/vector2d.h"
 #include "math/vector3d.h"
 
@@ -72,10 +75,8 @@ static constexpr const Color kDebugBlue = { 0, 0, 255, 110 };
  * It is used as a safer option for a simple "isBusy" counter
  */
 struct FakeSemaphore {
-	FakeSemaphore(uint initialCount = 0) : _counter(initialCount) {}
-	~FakeSemaphore() {
-		assert(_counter == 0);
-	}
+	FakeSemaphore(uint initialCount = 0);
+	~FakeSemaphore();
 
 	inline bool isReleased() const { return _counter == 0; }
 	inline uint counter() const { return _counter; }
@@ -85,45 +86,57 @@ private:
 };
 
 struct FakeLock {
-	FakeLock() : _semaphore(nullptr) {}
-
-	FakeLock(FakeSemaphore &semaphore) : _semaphore(&semaphore) {
-		_semaphore->_counter++;
-	}
-
-	FakeLock(const FakeLock &other) : _semaphore(other._semaphore) {
-		assert(_semaphore != nullptr);
-		_semaphore->_counter++;
-	}
-
-	FakeLock(FakeLock &&other) noexcept : _semaphore(other._semaphore) {
-		other._semaphore = nullptr;
-	}
-
-	~FakeLock() {
-		if (_semaphore == nullptr)
-			return;
-		assert(_semaphore->_counter > 0);
-		_semaphore->_counter--;
-	}
+	FakeLock();
+	FakeLock(FakeSemaphore &semaphore);
+	FakeLock(const FakeLock &other);
+	FakeLock(FakeLock &&other) noexcept;
+	~FakeLock();
 private:
 	FakeSemaphore *_semaphore;
 };
 
-inline Math::Vector3d as3D(const Math::Vector2d &v) {
-	return Math::Vector3d(v.getX(), v.getY(), 0.0f);
-}
-
-inline Math::Vector3d as3D(const Common::Point &p) {
-	return Math::Vector3d((float)p.x, (float)p.y, 0.0f);
+Math::Vector3d as3D(const Math::Vector2d &v);
+Math::Vector3d as3D(const Common::Point &p);
+Math::Vector2d as2D(const Math::Vector3d &v);
+Math::Vector2d as2D(const Common::Point &p);
+
+bool readBool(Common::ReadStream &stream);
+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);
+
+template<typename T>
+inline void syncArray(Common::Serializer &serializer, Common::Array<T> &array, void (*serializeFunction)(Common::Serializer &, T &)) {
+	auto size = array.size();
+	serializer.syncAsUint32LE(size);
+	array.resize(size);
+	serializer.syncArray(array.data(), size, serializeFunction);
 }
 
-inline Math::Vector2d as2D(const Math::Vector3d &v) {
-	return Math::Vector2d(v.x(), v.y());
+template<typename T>
+inline void syncStack(Common::Serializer &serializer, Common::Stack<T> &stack, void (*serializeFunction)(Common::Serializer &, T &)) {
+	auto size = stack.size();
+	serializer.syncAsUint32LE(size);
+	if (serializer.isLoading()) {
+		for (uint i = 0; i < size; i++) {
+			T value;
+			serializeFunction(serializer, value);
+			stack.push(value);
+		}
+	}
+	else {
+		for (uint i = 0; i < size; i++)
+			serializeFunction(serializer, stack[i]);
+	}
 }
 
-inline Math::Vector2d as2D(const Common::Point &p) {
-	return Math::Vector2d((float)p.x, (float)p.y);
+template<typename T>
+inline void syncEnum(Common::Serializer &serializer, T &enumValue) {
+	// syncAs does not have a cast for saving
+	int32 intValue = static_cast<int32>(enumValue);
+	serializer.syncAsSint32LE(intValue);
+	enumValue = static_cast<T>(intValue);
 }
 
 }
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index e8053f9097e..2be6d72fc6c 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -21,7 +21,6 @@
 
 #include "objects.h"
 #include "rooms.h"
-#include "stream-helper.h"
 #include "alcachofa.h"
 
 using namespace Common;
@@ -285,7 +284,7 @@ struct SayTextTask : public Task {
 					Point(g_system->getWidth() / 2, g_system->getHeight() - 200),
 					-1, true, kWhite, 0);
 			}
-			// TODO: Add lip syng for sayText
+			// TODO: Add lip sync for sayText
 
 			if (!_character->_isTalking) {
 				g_engine->sounds().fadeOut(_soundId, 100);
diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index 47ed70df3a0..cca8b6c27f4 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -22,7 +22,6 @@
 #include "objects.h"
 #include "rooms.h"
 #include "scheduler.h"
-#include "stream-helper.h"
 #include "alcachofa.h"
 
 #include "common/system.h"
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index dfe32d14778..2b07ab58f07 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -20,7 +20,6 @@
  */
 
 #include "graphics.h"
-#include "stream-helper.h"
 #include "alcachofa.h"
 #include "shape.h"
 
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index c2fedbd6e3f..75855459e4d 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -22,7 +22,6 @@
 #include "alcachofa.h"
 #include "rooms.h"
 #include "script.h"
-#include "stream-helper.h"
 
 #include "common/file.h"
 
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index a689a179648..82e9b992acd 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -20,7 +20,6 @@
  */
 
 #include "script.h"
-#include "stream-helper.h"
 #include "rooms.h"
 #include "alcachofa.h"
 
diff --git a/engines/alcachofa/shape.cpp b/engines/alcachofa/shape.cpp
index 568917385b4..a136888af8b 100644
--- a/engines/alcachofa/shape.cpp
+++ b/engines/alcachofa/shape.cpp
@@ -20,7 +20,6 @@
  */
 
 #include "shape.h"
-#include "stream-helper.h"
 
 using namespace Common;
 using namespace Math;
diff --git a/engines/alcachofa/stream-helper.h b/engines/alcachofa/stream-helper.h
deleted file mode 100644
index 7c41ca41376..00000000000
--- a/engines/alcachofa/stream-helper.h
+++ /dev/null
@@ -1,74 +0,0 @@
-/* ScummVM - Graphic Adventure Engine
- *
- * ScummVM is the legal property of its developers, whose names
- * are too numerous to list here. Please refer to the COPYRIGHT
- * file distributed with this source distribution.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- */
-
-#ifndef STREAM_HELPER_H
-#define STREAM_HELPER_H
-
-#include "common/stream.h"
-#include "common/serializer.h"
-#include "common/rect.h"
-#include "common/stack.h"
-
-namespace Alcachofa {
-
-bool readBool(Common::ReadStream &stream);
-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);
-
-template<typename T>
-inline void syncArray(Common::Serializer &serializer, Common::Array<T> &array, void (*serializeFunction)(Common::Serializer &, T &)) {
-	auto size = array.size();
-	serializer.syncAsUint32LE(size);
-	array.resize(size);
-	serializer.syncArray(array.data(), size, serializeFunction);
-}
-
-template<typename T>
-inline void syncStack(Common::Serializer &serializer, Common::Stack<T> &stack, void (*serializeFunction)(Common::Serializer &, T &)) {
-	auto size = stack.size();
-	serializer.syncAsUint32LE(size);
-	if (serializer.isLoading()) {
-		for (uint i = 0; i < size; i++) {
-			T value;
-			serializeFunction(serializer, value);
-			stack.push(value);
-		}
-	}
-	else {
-		for (uint i = 0; i < size; i++)
-			serializeFunction(serializer, stack[i]);
-	}
-}
-
-template<typename T>
-inline void syncEnum(Common::Serializer &serializer, T &enumValue) {
-	// syncAs does not have a cast for saving
-	int32 intValue = static_cast<int32>(enumValue);
-	serializer.syncAsSint32LE(intValue);
-	enumValue = static_cast<T>(intValue);
-}
-
-}
-
-#endif // STREAM_HELPER_H
diff --git a/engines/alcachofa/ui-objects.cpp b/engines/alcachofa/ui-objects.cpp
index 1b78a192b04..926d960d96b 100644
--- a/engines/alcachofa/ui-objects.cpp
+++ b/engines/alcachofa/ui-objects.cpp
@@ -21,7 +21,6 @@
 
 #include "objects.h"
 #include "rooms.h"
-#include "stream-helper.h"
 
 using namespace Common;
 


Commit: 114d88a87e1dc342cf251150ac437e157d57d0e2
    https://github.com/scummvm/scummvm/commit/114d88a87e1dc342cf251150ac437e157d57d0e2
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:47+02:00

Commit Message:
ALCACHOFA: Add dialog menu kernel task

although with broken text blending

Changed paths:
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/objects.h
    engines/alcachofa/player.cpp
    engines/alcachofa/player.h
    engines/alcachofa/scheduler.h
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 2be6d72fc6c..3a85ea4d9e5 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -282,7 +282,7 @@ struct SayTextTask : public Task {
 					g_engine->world().dialogFont(),
 					g_engine->world().getDialogLine(_dialogId),
 					Point(g_system->getWidth() / 2, g_system->getHeight() - 200),
-					-1, true, kWhite, 0);
+					-1, true, kWhite, -kForegroundOrderCount);
 			}
 			// TODO: Add lip sync for sayText
 
@@ -706,7 +706,7 @@ void MainCharacter::drawInner() {
 void syncDialogMenuLine(Serializer &serializer, DialogMenuLine &line) {
 	serializer.syncAsSint32LE(line._dialogId);
 	serializer.syncAsSint32LE(line._yPosition);
-	serializer.syncAsSint32LE(line._returnId);
+	serializer.syncAsSint32LE(line._returnValue);
 }
 
 void MainCharacter::serializeSave(Serializer &serializer) {
@@ -722,7 +722,7 @@ void MainCharacter::serializeSave(Serializer &serializer) {
 	uint semaphoreCounter = _semaphore.counter();
 	serializer.syncAsSint32LE(semaphoreCounter);
 	_semaphore = FakeSemaphore(semaphoreCounter);
-	syncArray(serializer, _dialogMenuLines, syncDialogMenuLine);
+	syncArray(serializer, _dialogLines, syncDialogMenuLine);
 	syncObjectAsString(serializer, _currentlyUsingObject);
 
 	for (auto *item : _items) {
@@ -799,6 +799,103 @@ bool MainCharacter::clearTargetIf(const ITriggerableObject *target) {
 	return false;
 }
 
+struct DialogMenuTask : public Task {
+	DialogMenuTask(Process &process, MainCharacter *character)
+		: Task(process)
+		, _input(g_engine->input())
+		, _character(character) {}
+
+	virtual TaskReturn run() override {
+		TASK_BEGIN;
+		layoutLines();
+		while (true) {
+			TASK_YIELD;
+			if (g_engine->player().activeCharacter() != _character)
+				continue;
+			g_engine->player().heldItem() = nullptr;
+			g_engine->player().drawCursor();
+
+			_clickedLineI = updateLines();
+			if (_clickedLineI != UINT_MAX) {
+				TASK_YIELD;
+				TASK_WAIT(_character->sayText(process(), _character->_dialogLines[_clickedLineI]._dialogId));
+				int32 returnValue = _character->_dialogLines[_clickedLineI]._returnValue;
+				_character->_dialogLines.clear();
+				TASK_RETURN(returnValue);
+			}
+		}
+		TASK_END;
+	}
+
+	virtual void debugPrint() override {
+		g_engine->console().debugPrintf("DialogMenu for %s with %u lines\n",
+			_character->name().c_str(), _character->_dialogLines.size());
+	}
+
+private:
+	static constexpr int kTextXOffset = 5;
+	static constexpr int kTextYOffset = 10;
+	inline int maxTextWidth() const {
+		return g_system->getWidth() - 2 * kTextXOffset;
+	}
+
+	void layoutLines() {
+		auto &lines = _character->_dialogLines;
+		for (auto &itLine : lines) {
+			// we reuse the draw request to measure the actual height without using it to actually draw
+			TextDrawRequest request(
+				g_engine->world().dialogFont(),
+				g_engine->world().getDialogLine(itLine._dialogId),
+				Point(kTextXOffset, 0), maxTextWidth(), false, kWhite, 2);
+			itLine._yPosition = request.size().y; // briefly storing line height
+		}
+
+		lines.back()._yPosition = g_system->getHeight() - kTextYOffset - lines.back()._yPosition;
+		for (uint i = lines.size() - 1; i > 0; i--)
+			lines[i - 1]._yPosition = lines[i]._yPosition - kTextYOffset - lines[i - 1]._yPosition;
+	}
+
+	uint updateLines() {
+		bool isSomethingHovered = false;
+		for (uint i = _character->_dialogLines.size(); i > 0; i--) {
+			auto &itLine = _character->_dialogLines[i - 1];
+			bool isHovered = !isSomethingHovered && _input.mousePos2D().y >= itLine._yPosition - kTextYOffset;
+			g_engine->drawQueue().add<TextDrawRequest>(
+				g_engine->world().dialogFont(),
+				g_engine->world().getDialogLine(itLine._dialogId),
+				Point(kTextXOffset + (isHovered * 20), itLine._yPosition),
+				maxTextWidth(), false, isHovered ? Color{ 255, 0, 0, 255 } : kWhite, -kForegroundOrderCount + 2);
+			isSomethingHovered = isSomethingHovered || isHovered;
+			if (isHovered && _input.wasMouseLeftReleased())
+				return i - 1;
+		}
+		return UINT_MAX;
+	}
+
+	Input &_input;
+	MainCharacter *_character;
+	uint _clickedLineI = UINT_MAX;
+};
+
+void MainCharacter::addDialogLine(int32 dialogId) {
+	assert(dialogId >= 0);
+	DialogMenuLine line;
+	line._dialogId = dialogId;
+	_dialogLines.push_back(line);
+}
+
+void MainCharacter::setLastDialogReturnValue(int32 returnValue) {
+	if (_dialogLines.empty())
+		error("Tried to set return value of non-existent dialog line");
+	_dialogLines.back()._returnValue = returnValue;
+}
+
+Task *MainCharacter::dialogMenu(Process &process) {
+	if (_dialogLines.empty())
+		error("Tried to open dialog menu without any lines set");
+	return new DialogMenuTask(process, this);
+}
+
 const char *Background::typeName() const { return "Background"; }
 
 Background::Background(Room *room, const String &animationFileName, int16 scale)
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 24cd9928ecb..84d281178b1 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -469,8 +469,8 @@ protected:
 
 struct DialogMenuLine {
 	int32 _dialogId;
-	int32 _yPosition;
-	int32 _returnId;
+	int32 _yPosition = 0;
+	int32 _returnValue = 0;
 };
 
 class MainCharacter final : public WalkingCharacter {
@@ -488,6 +488,7 @@ public:
 	virtual void update() override;
 	virtual void draw() override;
 	virtual void serializeSave(Common::Serializer &serializer) override;
+	virtual const char *typeName() const;
 	virtual void walkTo(
 		const Common::Point &target,
 		Direction endDirection = Direction::Invalid,
@@ -499,18 +500,21 @@ public:
 	bool hasItem(const Common::String &name) const;
 	void pickup(const Common::String &name, bool putInHand);
 	void drop(const Common::String &name);
-	virtual const char *typeName() const;
+	void addDialogLine(int32 dialogId);
+	void setLastDialogReturnValue(int32 returnValue);
+	Task *dialogMenu(Process &process);
 
 protected:
 	virtual void onArrived() override;
 
 private:
 	friend class Inventory;
+	friend struct DialogMenuTask;
 	Item *getItemByName(const Common::String &name) const;
 	void drawInner();
 
 	Common::Array<Item *> _items;
-	Common::Array<DialogMenuLine> _dialogMenuLines;
+	Common::Array<DialogMenuLine> _dialogLines;
 	ObjectBase *_currentlyUsingObject = nullptr;
 	MainCharacterKind _kind;
 	FakeSemaphore _semaphore;
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index 6639f8ef32d..2e094dba117 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -71,6 +71,10 @@ void Player::updateCursor() {
 			_cursorFrameI = 4;
 	}
 
+	drawCursor();
+}
+
+void Player::drawCursor() {
 	Point cursorPos = g_engine->input().mousePos2D();
 	if (_heldItem == nullptr)
 		g_engine->drawQueue().add<AnimationDrawRequest>(_cursorAnimation.get(), _cursorFrameI, as2D(cursorPos), -10);
@@ -81,7 +85,7 @@ void Player::updateCursor() {
 		auto frameOffset = animation.totalFrameOffset(0);
 		auto imageSize = animation.imageSize(animation.imageIndex(0, 0));
 		cursorPos -= frameOffset + imageSize / 2;
-		g_engine->drawQueue().add<AnimationDrawRequest>(&animation, 0, as2D(cursorPos), -10);
+		g_engine->drawQueue().add<AnimationDrawRequest>(&animation, 0, as2D(cursorPos), -kForegroundOrderCount);
 	}
 }
 
diff --git a/engines/alcachofa/player.h b/engines/alcachofa/player.h
index 9b6d9ea79aa..4841a448609 100644
--- a/engines/alcachofa/player.h
+++ b/engines/alcachofa/player.h
@@ -50,6 +50,7 @@ public:
 	void preUpdate();
 	void postUpdate();
 	void updateCursor();
+	void drawCursor();
 	void changeRoom(const Common::String &targetRoomName, bool resetCamera);
 	void triggerObject(ObjectBase *object, const char *action);
 	void triggerDoor(const Door *door);
diff --git a/engines/alcachofa/scheduler.h b/engines/alcachofa/scheduler.h
index 22c335c3602..67984040254 100644
--- a/engines/alcachofa/scheduler.h
+++ b/engines/alcachofa/scheduler.h
@@ -87,6 +87,7 @@ private:
 #endif
 
 #define TASK_BEGIN \
+	enum { TASK_COUNTER_BASE = __COUNTER__ }; \
 	switch(_line) { \
 	case 0:; \
 
@@ -96,21 +97,17 @@ private:
 	default: assert(false && "Invalid line in task"); \
 	} return TaskReturn::finish(0)
 
-#define TASK_YIELD \
+#define TASK_INTERNAL_BREAK(ret) \
 	do { \
-		_line = __LINE__; \
-		return TaskReturn::yield(); \
+		enum { TASK_COUNTER = __COUNTER__ - TASK_COUNTER_BASE }; \
+		_line = TASK_COUNTER; \
+		return ret; \
 		TASK_BREAK_FALLTHROUGH \
-		case __LINE__:; \
+		case TASK_COUNTER:; \
 	} while(0)
 
-#define TASK_WAIT(task) \
-	do { \
-		_line = __LINE__; \
-		return TaskReturn::waitFor(task); \
-		TASK_BREAK_FALLTHROUGH \
-		case __LINE__:; \
-	} while(0)
+#define TASK_YIELD TASK_INTERNAL_BREAK(TaskReturn::yield())
+#define TASK_WAIT(task) TASK_INTERNAL_BREAK(TaskReturn::waitFor(task))
 
 #define TASK_RETURN(value) \
 	do { \
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 82e9b992acd..ba74a95d630 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -441,7 +441,7 @@ private:
 			const char *characterName = getStringArg(0);
 			int32 dialogId = getNumberArg(1);
 			if (strncmp(characterName, "MENU_", 5) == 0) {
-				warning("STUB: adding dialog menu line %d", dialogId);
+				g_engine->world().getMainCharacterByKind(process().character()).addDialogLine(dialogId);
 				return TaskReturn::finish(1);
 			}
 			Character *_character = strcmp(characterName, "AMBOS") == 0
@@ -552,11 +552,10 @@ private:
 				g_engine->world().getMainCharacterByKind(process().character()).room()->toggleActiveFloor();
 			return TaskReturn::finish(1);
 		case ScriptKernelTask::SetDialogLineReturn:
-			warning("STUB KERNEL CALL: SetDialogLineReturn");
+			g_engine->world().getMainCharacterByKind(process().character()).setLastDialogReturnValue(getNumberArg(0));
 			return TaskReturn::finish(0);
 		case ScriptKernelTask::DialogMenu:
-			warning("STUB KERNEL CALL: DialogMenu");
-			return TaskReturn::finish(0);
+			return TaskReturn::waitFor(g_engine->world().getMainCharacterByKind(process().character()).dialogMenu(process()));
 		case ScriptKernelTask::ClearInventory:
 			switch((MainCharacterKind)getNumberArg(0)) {
 			case MainCharacterKind::Mortadelo: g_engine->world().mortadelo().clearInventory(); break;


Commit: e430e6d46cfa159762876f7045d7d0bd440695d7
    https://github.com/scummvm/scummvm/commit/e430e6d46cfa159762876f7045d7d0bd440695d7
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:47+02:00

Commit Message:
ALCACHOFA: Fix text blending mode

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


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 3a85ea4d9e5..a8607d2b872 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -863,8 +863,8 @@ private:
 			g_engine->drawQueue().add<TextDrawRequest>(
 				g_engine->world().dialogFont(),
 				g_engine->world().getDialogLine(itLine._dialogId),
-				Point(kTextXOffset + (isHovered * 20), itLine._yPosition),
-				maxTextWidth(), false, isHovered ? Color{ 255, 0, 0, 255 } : kWhite, -kForegroundOrderCount + 2);
+				Point(kTextXOffset, itLine._yPosition),
+				maxTextWidth(), false, isHovered ? Color{ 255, 255, 128, 255 } : kWhite, -kForegroundOrderCount + 2);
 			isSomethingHovered = isSomethingHovered || isHovered;
 			if (isHovered && _input.wasMouseLeftReleased())
 				return i - 1;
diff --git a/engines/alcachofa/graphics-opengl.cpp b/engines/alcachofa/graphics-opengl.cpp
index 56062fb74fa..823f7f5f868 100644
--- a/engines/alcachofa/graphics-opengl.cpp
+++ b/engines/alcachofa/graphics-opengl.cpp
@@ -125,8 +125,8 @@ public:
 		GL_CALL(glDisableClientState(GL_INDEX_ARRAY));
 		GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE));
 		GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_TEXTURE));
-		GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_CONSTANT));
-		GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_ALPHA, GL_CONSTANT));
+		GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_PRIMARY_COLOR));
+		GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_ALPHA, GL_PRIMARY_COLOR));
 		_currentLodBias = -1000.0f;
 		_currentTexture = nullptr;
 		_currentBlendMode = (BlendMode)-1;
@@ -183,7 +183,7 @@ public:
 
 		/** now the texture stage, mind that this always applies:
 		 * SRC0_RGB is TEXTURE
-		 * SRC1_RGB/ALPHA is CONSTANT
+		 * SRC1_RGB/ALPHA is PRIMARY COLOR
 		 * COMBINE_ALPHA is REPLACE
 		 */ 
 		switch (blendMode) {
@@ -248,7 +248,8 @@ public:
 
 		float colors[] = { color.r / 255.0f, color.g / 255.0f, color.b / 255.0f, color.a / 255.0f };
 
-		GL_CALL(glColor4f(1.0f, 1.0f, 1.0f, 1.0f));
+		//GL_CALL(glColor4f(1.0f, 1.0f, 1.0f, 1.0f));
+		GL_CALL(glColor4f(color.r / 255.0f, color.g / 255.0f, color.b / 255.0f, color.a / 255.0f));
 		GL_CALL(glVertexPointer(2, GL_FLOAT, 0, positions));
 		if (_currentTexture != nullptr)
 			GL_CALL(glTexCoordPointer(2, GL_FLOAT, 0, texCoords));


Commit: 75a8d917aa08526ccda714c40ae4d98c03235dc0
    https://github.com/scummvm/scummvm/commit/75a8d917aa08526ccda714c40ae4d98c03235dc0
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:48+02:00

Commit Message:
ALCACHOFA: Add camera lerp kernel tasks

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


diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index 13f309bdb81..c6719fef539 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -57,8 +57,11 @@ void Camera::setFollow(WalkingCharacter *target, bool catchUp) {
 }
 
 void Camera::setPosition(Vector2d v) {
-	_usedCenter.x() = v.getX();
-	_usedCenter.y() = v.getY();
+	setPosition({ v.getX(), v.getY(), _usedCenter.z() });
+}
+
+void Camera::setPosition(Vector3d v) {
+	_usedCenter = v;
 	setFollow(nullptr);
 }
 
@@ -212,4 +215,173 @@ void Camera::updateFollowing(float deltaTime) {
 	}
 }
 
+struct CamLerpTask : public Task {
+	CamLerpTask(Process &process, uint32 duration, EasingType easingType)
+		: Task(process)
+		, _camera(g_engine->camera())
+		, _duration(duration)
+		, _easingType(easingType) {}
+
+	virtual TaskReturn run() override {
+		TASK_BEGIN;
+		_startTime = g_system->getMillis();
+		while (g_system->getMillis() - _startTime < _duration) {
+			update(ease((g_system->getMillis() - _startTime) / (float)_duration, _easingType));
+			_camera._isChanging = true;
+			TASK_YIELD;
+		}
+		update(1.0f);
+		TASK_END;
+	}
+
+	virtual void debugPrint() override {
+		uint32 remaining = g_system->getMillis() - _startTime <= _duration
+			? _duration - (g_system->getMillis() - _startTime)
+			: 0;
+		g_engine->console().debugPrintf("Lerp camera with %ums remaining\n", remaining);
+	}
+
+protected:
+	virtual void update(float t) = 0;
+
+	Camera &_camera;
+	uint32 _startTime = 0, _duration;
+	EasingType _easingType;
+};
+
+struct CamLerpPosTask final : public CamLerpTask {
+	CamLerpPosTask(Process &process, Vector3d targetPos, int32 duration, EasingType easingType)
+		: CamLerpTask(process, duration, easingType)
+		, _fromPos(_camera._appliedCenter)
+		, _deltaPos(targetPos - _camera._appliedCenter) {}
+
+protected:
+	virtual void update(float t) override {
+		_camera.setPosition(_fromPos + _deltaPos * t);
+	}
+
+	Vector3d _fromPos, _deltaPos;
+};
+
+struct CamLerpScaleTask final : public CamLerpTask {
+	CamLerpScaleTask(Process &process, float targetScale, int32 duration, EasingType easingType)
+		: CamLerpTask(process, duration, easingType)
+		, _fromScale(_camera._scale)
+		, _deltaScale(targetScale - _camera._scale) {}
+
+protected:
+	virtual void update(float t) override {
+		_camera._scale = _fromScale + _deltaScale * t;
+	}
+
+	float _fromScale, _deltaScale;
+};
+
+struct CamLerpPosScaleTask final : public CamLerpTask {
+	CamLerpPosScaleTask(Process &process, Vector3d targetPos, float targetScale, int32 duration, EasingType easingType)
+		: CamLerpTask(process, duration, easingType)
+		, _fromPos(_camera._appliedCenter)
+		, _deltaPos(targetPos - _camera._appliedCenter)
+		, _fromScale(_camera._scale)
+		, _deltaScale(targetScale - _camera._scale) {}
+
+protected:
+	virtual void update(float t) override {
+		_camera.setPosition(_fromPos + _deltaPos * t);
+		_camera._scale = _fromScale + _deltaScale * t;
+	}
+
+	Vector3d _fromPos, _deltaPos;
+	float _fromScale, _deltaScale;
+};
+
+struct CamLerpRotationTask final : public CamLerpTask {
+	CamLerpRotationTask(Process &process, float targetRotation, int32 duration, EasingType easingType)
+		: CamLerpTask(process, duration, easingType)
+		, _fromRotation(_camera._rotation.getDegrees())
+		, _deltaRotation(targetRotation - _camera._rotation.getDegrees()) {}
+
+protected:
+	virtual void update(float t) override {
+		_camera._rotation = Angle(_fromRotation + _deltaRotation * t);
+	}
+
+	float _fromRotation, _deltaRotation;
+};
+
+struct CamWaitToStopTask final : public Task {
+	CamWaitToStopTask(Process &process)
+		: Task(process)
+		, _camera(g_engine->camera()) {}
+
+	virtual TaskReturn run() override {
+		return _camera._isChanging
+			? TaskReturn::yield()
+			: TaskReturn::finish(1);
+	}
+
+	virtual void debugPrint() override {
+		g_engine->console().debugPrintf("Wait for camera to stop moving\n");
+	}
+
+private:
+	Camera &_camera;
+};
+
+Task *Camera::lerpPos(Process &process, Vector2d targetPos, int32 duration, EasingType easingType) {
+	if (!process.isActiveForPlayer()) {
+		warning("stub: non-active camera lerp script invoked");
+		return new DelayTask(process, duration);
+	}
+	Vector3d targetPos3d(targetPos.getX(), targetPos.getY(), _appliedCenter.z());
+	return new CamLerpPosTask(process, targetPos3d, duration, easingType);
+}
+
+Task *Camera::lerpPos(Process &process, Vector3d targetPos, int32 duration, EasingType easingType) {
+	if (!process.isActiveForPlayer()) {
+		warning("stub: non-active camera lerp script invoked");
+		return new DelayTask(process, duration);
+	}
+	setFollow(nullptr); // 3D position lerping is the only task that resets following
+	return new CamLerpPosTask(process, targetPos, duration, easingType);
+}
+
+Task *Camera::lerpPosZ(Process &process, float targetPosZ, int32 duration, EasingType easingType) {
+	if (!process.isActiveForPlayer()) {
+		warning("stub: non-active camera lerp script invoked");
+		return new DelayTask(process, duration);
+	}
+	Vector3d targetPos(_appliedCenter.x(), _appliedCenter.y(), targetPosZ);
+	return new CamLerpPosTask(process, targetPos, duration, easingType);
+}
+
+Task *Camera::lerpScale(Process &process, float targetScale, int32 duration, EasingType easingType) {
+	if (!process.isActiveForPlayer()) {
+		warning("stub: non-active camera lerp script invoked");
+		return new DelayTask(process, duration);
+	}
+	return new CamLerpScaleTask(process, targetScale, duration, easingType);
+}
+
+Task *Camera::lerpRotation(Process &process, float targetRotation, int32 duration, EasingType easingType) {
+	if (!process.isActiveForPlayer()) {
+		warning("stub: non-active camera lerp script invoked");
+		return new DelayTask(process, duration);
+	}
+	return new CamLerpRotationTask(process, targetRotation, duration, easingType);
+}
+
+Task *Camera::lerpPosScale(Process &process, Vector2d targetPos, float targetScale, int32 duration, EasingType easingType) {
+	if (!process.isActiveForPlayer()) {
+		warning("stub: non-active camera lerp script invoked");
+		return new DelayTask(process, duration);
+	}
+	Vector3d targetPos3d(targetPos.getX(), targetPos.getY(), _appliedCenter.z());
+	return new CamLerpPosScaleTask(process, targetPos3d, targetScale, duration, easingType);
+}
+
+Task *Camera::waitToStop(Process &process) {
+	return new CamWaitToStopTask(process);
+}
+
 }
diff --git a/engines/alcachofa/camera.h b/engines/alcachofa/camera.h
index ac04907493f..c8573673972 100644
--- a/engines/alcachofa/camera.h
+++ b/engines/alcachofa/camera.h
@@ -22,10 +22,7 @@
 #ifndef CAMERA_H
 #define CAMERA_H
 
-#include "common/serializer.h"
-#include "common/rect.h"
-#include "math/vector2d.h"
-#include "math/vector3d.h"
+#include "common.h"
 #include "math/matrix4.h"
 
 namespace Alcachofa {
@@ -34,7 +31,7 @@ class WalkingCharacter;
 class Process;
 struct Task;
 
-static constexpr const int16_t kBaseScale = 300; ///< this number pops up everywhere in the engine
+static constexpr const int16_t kBaseScale = 300;
 static constexpr const float kInvBaseScale = 1.0f / kBaseScale;
 
 class Camera {
@@ -50,8 +47,25 @@ public:
 	void setRoomBounds(Common::Point bgSize, int16 bgScale);
 	void setFollow(WalkingCharacter *target, bool catchUp = false);
 	void setPosition(Math::Vector2d v);
+	void setPosition(Math::Vector3d v);
+
+	Task *lerpPos(Process &process, Math::Vector2d targetPos, int32 duration, EasingType easingType);
+	Task *lerpPos(Process &process, Math::Vector3d targetPos, int32 duration, EasingType easingType);
+	Task *lerpPosZ(Process &process, float targetPosZ, int32 duration, EasingType easingType);
+	Task *lerpScale(Process &process, float targetScale, int32 duration, EasingType easingType);
+	Task *lerpRotation(Process &process, float targetRotation, int32 duration, EasingType easingType);
+	Task *lerpPosScale(Process &process, Math::Vector2d targetPos, float targetScale, int32 duration, EasingType easingType);
+	//Task *shake(Process &process, Math::Vector2d amplitude, Math::Vector2d frequency, int32 duration);
+	Task *waitToStop(Process &process);
 
 private:
+	friend struct CamLerpTask;
+	friend struct CamLerpPosTask;
+	friend struct CamLerpScaleTask;
+	friend struct CamLerpPosScaleTask;
+	friend struct CamLerpRotationTask;
+	//friend struct CamShakeTask;
+	friend struct CamWaitToStopTask;
 	Math::Vector3d setAppliedCenter(Math::Vector3d center);
 	void setupMatricesAround(Math::Vector3d center);
 	void updateFollowing(float deltaTime);
diff --git a/engines/alcachofa/common.cpp b/engines/alcachofa/common.cpp
index f8bf519936d..3f7afe1cc31 100644
--- a/engines/alcachofa/common.cpp
+++ b/engines/alcachofa/common.cpp
@@ -26,6 +26,16 @@ using namespace Math;
 
 namespace Alcachofa {
 
+float ease(float t, EasingType type) {
+	switch (type) {
+	case EasingType::Linear: return t;
+	case EasingType::InOut: return (1 - cosf(t * M_PI)) * 0.5f;
+	case EasingType::In: return 1 - cosf(t * M_PI * 0.5f);
+	case EasingType::Out: return sinf(t * M_PI * 0.5f);
+	default: return 0.0f;
+	}
+}
+
 FakeSemaphore::FakeSemaphore(uint initialCount) : _counter(initialCount) {}
 
 FakeSemaphore::~FakeSemaphore() {
diff --git a/engines/alcachofa/common.h b/engines/alcachofa/common.h
index cc1efc2f96b..783d6dbde86 100644
--- a/engines/alcachofa/common.h
+++ b/engines/alcachofa/common.h
@@ -56,6 +56,13 @@ enum class MainCharacterKind {
 	Filemon
 };
 
+enum class EasingType {
+	Linear,
+	InOut,
+	In,
+	Out
+};
+
 constexpr const int32 kDirectionCount = 4;
 constexpr const int8 kOrderCount = 70;
 constexpr const int8 kForegroundOrderCount = 10;
@@ -95,6 +102,8 @@ private:
 	FakeSemaphore *_semaphore;
 };
 
+float ease(float t, EasingType type);
+
 Math::Vector3d as3D(const Math::Vector2d &v);
 Math::Vector3d as3D(const Common::Point &p);
 Math::Vector2d as2D(const Math::Vector3d &v);
diff --git a/engines/alcachofa/scheduler.cpp b/engines/alcachofa/scheduler.cpp
index e692e2c3a93..61a7e1b783a 100644
--- a/engines/alcachofa/scheduler.cpp
+++ b/engines/alcachofa/scheduler.cpp
@@ -28,28 +28,6 @@ using namespace Common;
 
 namespace Alcachofa {
 
-struct DelayTask : public Task {
-	DelayTask(Process &process, uint32 millis)
-		: Task(process)
-		, _endTime(millis) {}
-
-	virtual TaskReturn run() override {
-		TASK_BEGIN;
-		_endTime += g_system->getMillis();
-		while (g_system->getMillis() < _endTime)
-			TASK_YIELD;
-		TASK_END;
-	}
-
-	virtual void debugPrint() {
-		uint32 remaining = g_system->getMillis() <= _endTime ? _endTime - g_system->getMillis() : 0;
-		g_engine->getDebugger()->debugPrintf("Delay for further %ums\n", remaining);
-	}
-
-private:
-	uint32 _endTime;
-};
-
 TaskReturn::TaskReturn() {
 	_type = TaskReturnType::Yield;
 	_returnValue = 0;
@@ -77,6 +55,23 @@ Task *Task::delay(uint32 millis) {
 	return new DelayTask(process(), millis);
 }
 
+DelayTask::DelayTask(Process &process, uint32 millis)
+	: Task(process)
+	, _endTime(millis) {}
+
+TaskReturn DelayTask::run(){
+	TASK_BEGIN;
+	_endTime += g_system->getMillis();
+	while (g_system->getMillis() < _endTime)
+		TASK_YIELD;
+	TASK_END;
+}
+
+void DelayTask::debugPrint() {
+	uint32 remaining = g_system->getMillis() <= _endTime ? _endTime - g_system->getMillis() : 0;
+	g_engine->getDebugger()->debugPrintf("Delay for further %ums\n", remaining);
+}
+
 Process::Process(ProcessId pid, MainCharacterKind characterKind)
 	: _pid(pid)
 	, _character(characterKind)
diff --git a/engines/alcachofa/scheduler.h b/engines/alcachofa/scheduler.h
index 67984040254..9ea21f9d415 100644
--- a/engines/alcachofa/scheduler.h
+++ b/engines/alcachofa/scheduler.h
@@ -79,6 +79,15 @@ private:
 	Process &_process;
 };
 
+struct DelayTask : public Task {
+	DelayTask(Process &process, uint32 millis);
+	virtual TaskReturn run() override; 
+	virtual void debugPrint() override;
+
+private:
+	uint32 _endTime;
+};
+
 // TODO: This probably should be scummvm common
 #if __cplusplus >= 201703L
 #define TASK_BREAK_FALLTHROUGH [[fallthrough]];
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index ba74a95d630..01b2c10ea52 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -26,6 +26,7 @@
 #include "common/file.h"
 
 using namespace Common;
+using namespace Math;
 
 namespace Alcachofa {
 
@@ -563,24 +564,16 @@ private:
 			default: error("Script attempted to clear inventory with invalid character kind"); break;
 			}
 			return TaskReturn::finish(1);
-		case ScriptKernelTask::FadeType0:
-			warning("STUB KERNEL CALL: FadeType0");
-			return TaskReturn::finish(0);
-		case ScriptKernelTask::FadeType1:
-			warning("STUB KERNEL CALL: FadeType1");
-			return TaskReturn::finish(0);
 		case ScriptKernelTask::LerpWorldLodBias:
 			warning("STUB KERNEL CALL: LerpWorldLodBias");
 			return TaskReturn::finish(0);
-		case ScriptKernelTask::FadeType2:
-			warning("STUB KERNEL CALL: FadeType2");
-			return TaskReturn::finish(0);
+
+		// Camera tasks
 		case ScriptKernelTask::SetMaxCamSpeedFactor:
 			warning("STUB KERNEL CALL: SetMaxCamSpeedFactor");
 			return TaskReturn::finish(0);
 		case ScriptKernelTask::WaitCamStopping:
-			warning("STUB KERNEL CALL: WaitCamStopping");
-			return TaskReturn::finish(0);
+			return TaskReturn::waitFor(g_engine->camera().waitToStop(process()));
 		case ScriptKernelTask::CamFollow:
 			warning("STUB KERNEL CALL: CamFollow");
 			return TaskReturn::finish(0);
@@ -588,22 +581,44 @@ private:
 			warning("STUB KERNEL CALL: CamShake");
 			return TaskReturn::finish(0);
 		case ScriptKernelTask::LerpCamXY:
-			warning("STUB KERNEL CALL: LerpCamXY");
-			return TaskReturn::finish(0);
+			return TaskReturn::waitFor(g_engine->camera().lerpPos(process(),
+				Vector2d(getNumberArg(0), getNumberArg(1)),
+				getNumberArg(2), (EasingType)getNumberArg(3)));
+		case ScriptKernelTask::LerpCamXYZ:
+			return TaskReturn::waitFor(g_engine->camera().lerpPos(process(),
+				Vector3d(getNumberArg(0), getNumberArg(1), getNumberArg(2)),
+				getNumberArg(3), (EasingType)getNumberArg(4)));
 		case ScriptKernelTask::LerpCamZ:
-			warning("STUB KERNEL CALL: LerpCamZ");
-			return TaskReturn::finish(0);
+			return TaskReturn::waitFor(g_engine->camera().lerpPosZ(process(),
+				getNumberArg(0),
+				getNumberArg(1), (EasingType)getNumberArg(2)));
 		case ScriptKernelTask::LerpCamScale:
-			warning("STUB KERNEL CALL: LerpCamScale");
+			return TaskReturn::waitFor(g_engine->camera().lerpScale(process(),
+				getNumberArg(0) * 0.01f,
+				getNumberArg(1), (EasingType)getNumberArg(2)));
+		case ScriptKernelTask::LerpCamRotation:
+			return TaskReturn::waitFor(g_engine->camera().lerpRotation(process(),
+				getNumberArg(0),
+				getNumberArg(1), (EasingType)getNumberArg(2)));
+		case ScriptKernelTask::LerpCamToObjectKeepingZ:
+			warning("STUB KERNEL CALL: LerpCamToObjectKeepingZ");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::LerpCamToObjectResettingZ:
+			warning("STUB KERNEL CALL: LerpCamToObjectResettingZ");
 			return TaskReturn::finish(0);
 		case ScriptKernelTask::LerpCamToObjectWithScale:
 			warning("STUB KERNEL CALL: LerpCamToObjectWithScale");
 			return TaskReturn::finish(0);
-		case ScriptKernelTask::LerpCamToObjectResettingZ:
-			warning("STUB KERNEL CALL: LerpCamToObjectResettingZ");
+
+		// Fades
+		case ScriptKernelTask::FadeType0:
+			warning("STUB KERNEL CALL: FadeType0");
 			return TaskReturn::finish(0);
-		case ScriptKernelTask::LerpCamRotation:
-			warning("STUB KERNEL CALL: LerpCamRotation");
+		case ScriptKernelTask::FadeType1:
+			warning("STUB KERNEL CALL: FadeType1");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::FadeType2:
+			warning("STUB KERNEL CALL: FadeType2");
 			return TaskReturn::finish(0);
 		case ScriptKernelTask::FadeIn:
 			warning("STUB KERNEL CALL: FadeIn");
@@ -617,12 +632,7 @@ private:
 		case ScriptKernelTask::FadeOut2:
 			warning("STUB KERNEL CALL: FadeOut2");
 			return TaskReturn::finish(0);
-		case ScriptKernelTask::LerpCamXYZ:
-			warning("STUB KERNEL CALL: LerpCamXYZ");
-			return TaskReturn::finish(0);
-		case ScriptKernelTask::LerpCamToObjectKeepingZ:
-			warning("STUB KERNEL CALL: LerpCamToObjectKeepingZ");
-			return TaskReturn::finish(0);
+
 		case ScriptKernelTask::SetActiveTextureSet:
 			// Fortunately this seems to be unused.
 			warning("STUB KERNEL CALL: SetActiveTextureSet");


Commit: 742c5848854daa5235a7377d38b504f4e304cd4c
    https://github.com/scummvm/scummvm/commit/742c5848854daa5235a7377d38b504f4e304cd4c
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:48+02:00

Commit Message:
ALCACHOFA: Add LerpCameraToObject kernel tasks

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


diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index c6719fef539..5dd3f2b2bcb 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -278,21 +278,26 @@ protected:
 };
 
 struct CamLerpPosScaleTask final : public CamLerpTask {
-	CamLerpPosScaleTask(Process &process, Vector3d targetPos, float targetScale, int32 duration, EasingType easingType)
-		: CamLerpTask(process, duration, easingType)
+	CamLerpPosScaleTask(Process &process,
+		Vector3d targetPos, float targetScale,
+		int32 duration, EasingType moveEasingType, EasingType scaleEasingType)
+		: CamLerpTask(process, duration, EasingType::Linear) // linear as we need different ones per component
 		, _fromPos(_camera._appliedCenter)
 		, _deltaPos(targetPos - _camera._appliedCenter)
 		, _fromScale(_camera._scale)
-		, _deltaScale(targetScale - _camera._scale) {}
+		, _deltaScale(targetScale - _camera._scale)
+		, _moveEasingType(moveEasingType)
+		, _scaleEasingType(scaleEasingType) {}
 
 protected:
 	virtual void update(float t) override {
-		_camera.setPosition(_fromPos + _deltaPos * t);
-		_camera._scale = _fromScale + _deltaScale * t;
+		_camera.setPosition(_fromPos + _deltaPos * ease(t, _moveEasingType));
+		_camera._scale = _fromScale + _deltaScale * ease(t, _scaleEasingType);
 	}
 
 	Vector3d _fromPos, _deltaPos;
 	float _fromScale, _deltaScale;
+	EasingType _moveEasingType, _scaleEasingType;
 };
 
 struct CamLerpRotationTask final : public CamLerpTask {
@@ -328,7 +333,9 @@ private:
 	Camera &_camera;
 };
 
-Task *Camera::lerpPos(Process &process, Vector2d targetPos, int32 duration, EasingType easingType) {
+Task *Camera::lerpPos(Process &process,
+	Vector2d targetPos,
+	int32 duration, EasingType easingType) {
 	if (!process.isActiveForPlayer()) {
 		warning("stub: non-active camera lerp script invoked");
 		return new DelayTask(process, duration);
@@ -337,7 +344,9 @@ Task *Camera::lerpPos(Process &process, Vector2d targetPos, int32 duration, Easi
 	return new CamLerpPosTask(process, targetPos3d, duration, easingType);
 }
 
-Task *Camera::lerpPos(Process &process, Vector3d targetPos, int32 duration, EasingType easingType) {
+Task *Camera::lerpPos(Process &process,
+	Vector3d targetPos,
+	int32 duration, EasingType easingType) {
 	if (!process.isActiveForPlayer()) {
 		warning("stub: non-active camera lerp script invoked");
 		return new DelayTask(process, duration);
@@ -346,7 +355,9 @@ Task *Camera::lerpPos(Process &process, Vector3d targetPos, int32 duration, Easi
 	return new CamLerpPosTask(process, targetPos, duration, easingType);
 }
 
-Task *Camera::lerpPosZ(Process &process, float targetPosZ, int32 duration, EasingType easingType) {
+Task *Camera::lerpPosZ(Process &process,
+	float targetPosZ,
+	int32 duration, EasingType easingType) {
 	if (!process.isActiveForPlayer()) {
 		warning("stub: non-active camera lerp script invoked");
 		return new DelayTask(process, duration);
@@ -355,7 +366,9 @@ Task *Camera::lerpPosZ(Process &process, float targetPosZ, int32 duration, Easin
 	return new CamLerpPosTask(process, targetPos, duration, easingType);
 }
 
-Task *Camera::lerpScale(Process &process, float targetScale, int32 duration, EasingType easingType) {
+Task *Camera::lerpScale(Process &process,
+	float targetScale,
+	int32 duration, EasingType easingType) {
 	if (!process.isActiveForPlayer()) {
 		warning("stub: non-active camera lerp script invoked");
 		return new DelayTask(process, duration);
@@ -363,7 +376,9 @@ Task *Camera::lerpScale(Process &process, float targetScale, int32 duration, Eas
 	return new CamLerpScaleTask(process, targetScale, duration, easingType);
 }
 
-Task *Camera::lerpRotation(Process &process, float targetRotation, int32 duration, EasingType easingType) {
+Task *Camera::lerpRotation(Process &process,
+	float targetRotation,
+	int32 duration, EasingType easingType) {
 	if (!process.isActiveForPlayer()) {
 		warning("stub: non-active camera lerp script invoked");
 		return new DelayTask(process, duration);
@@ -371,13 +386,14 @@ Task *Camera::lerpRotation(Process &process, float targetRotation, int32 duratio
 	return new CamLerpRotationTask(process, targetRotation, duration, easingType);
 }
 
-Task *Camera::lerpPosScale(Process &process, Vector2d targetPos, float targetScale, int32 duration, EasingType easingType) {
+Task *Camera::lerpPosScale(Process &process,
+	Vector3d targetPos, float targetScale,
+	int32 duration, EasingType moveEasingType, EasingType scaleEasingType) {
 	if (!process.isActiveForPlayer()) {
 		warning("stub: non-active camera lerp script invoked");
 		return new DelayTask(process, duration);
 	}
-	Vector3d targetPos3d(targetPos.getX(), targetPos.getY(), _appliedCenter.z());
-	return new CamLerpPosScaleTask(process, targetPos3d, targetScale, duration, easingType);
+	return new CamLerpPosScaleTask(process, targetPos, targetScale, duration, moveEasingType, scaleEasingType);
 }
 
 Task *Camera::waitToStop(Process &process) {
diff --git a/engines/alcachofa/camera.h b/engines/alcachofa/camera.h
index c8573673972..995521b7bd8 100644
--- a/engines/alcachofa/camera.h
+++ b/engines/alcachofa/camera.h
@@ -49,12 +49,24 @@ public:
 	void setPosition(Math::Vector2d v);
 	void setPosition(Math::Vector3d v);
 
-	Task *lerpPos(Process &process, Math::Vector2d targetPos, int32 duration, EasingType easingType);
-	Task *lerpPos(Process &process, Math::Vector3d targetPos, int32 duration, EasingType easingType);
-	Task *lerpPosZ(Process &process, float targetPosZ, int32 duration, EasingType easingType);
-	Task *lerpScale(Process &process, float targetScale, int32 duration, EasingType easingType);
-	Task *lerpRotation(Process &process, float targetRotation, int32 duration, EasingType easingType);
-	Task *lerpPosScale(Process &process, Math::Vector2d targetPos, float targetScale, int32 duration, EasingType easingType);
+	Task *lerpPos(Process &process,
+		Math::Vector2d targetPos,
+		int32 duration, EasingType easingType);
+	Task *lerpPos(Process &process, 
+		Math::Vector3d targetPos,
+		int32 duration, EasingType easingType);
+	Task *lerpPosZ(Process &process,
+		float targetPosZ,
+		int32 duration, EasingType easingType);
+	Task *lerpScale(Process &process,
+		float targetScale,
+		int32 duration, EasingType easingType);
+	Task *lerpRotation(Process &process,
+		float targetRotation,
+		int32 duration, EasingType easingType);
+	Task *lerpPosScale(Process &process,
+		Math::Vector3d targetPos, float targetScale,
+		int32 duration, EasingType moveEasingType, EasingType scaleEasingType);
 	//Task *shake(Process &process, Math::Vector2d amplitude, Math::Vector2d frequency, int32 duration);
 	Task *waitToStop(Process &process);
 
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 01b2c10ea52..061dca0e653 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -600,15 +600,39 @@ private:
 			return TaskReturn::waitFor(g_engine->camera().lerpRotation(process(),
 				getNumberArg(0),
 				getNumberArg(1), (EasingType)getNumberArg(2)));
-		case ScriptKernelTask::LerpCamToObjectKeepingZ:
-			warning("STUB KERNEL CALL: LerpCamToObjectKeepingZ");
-			return TaskReturn::finish(0);
-		case ScriptKernelTask::LerpCamToObjectResettingZ:
-			warning("STUB KERNEL CALL: LerpCamToObjectResettingZ");
-			return TaskReturn::finish(0);
-		case ScriptKernelTask::LerpCamToObjectWithScale:
-			warning("STUB KERNEL CALL: LerpCamToObjectWithScale");
-			return TaskReturn::finish(0);
+		case ScriptKernelTask::LerpCamToObjectKeepingZ: {
+			if (!process().isActiveForPlayer())
+				return TaskReturn::finish(0); // contrary to ...ResettingZ this one does not delay if not active
+			auto object = g_engine->world().getObjectByName(process().character(), getStringArg(0));
+			auto pointObject = dynamic_cast<PointObject *>(object);
+			if (pointObject == nullptr)
+				error("Invalid target object for LerpCamToObjectKeepingZ: %s", getStringArg(0));
+			return TaskReturn::waitFor(g_engine->camera().lerpPos(process(),
+				as2D(pointObject->position()),
+				getNumberArg(1), EasingType::Linear));
+		}
+		case ScriptKernelTask::LerpCamToObjectResettingZ: {
+			if (!process().isActiveForPlayer())
+				return TaskReturn::waitFor(delay(getNumberArg(1)));
+			auto object = g_engine->world().getObjectByName(process().character(), getStringArg(0));
+			auto pointObject = dynamic_cast<PointObject *>(object);
+			if (pointObject == nullptr)
+				error("Invalid target object for LerpCamToObjectResettingZ: %s", getStringArg(0));
+			return TaskReturn::waitFor(g_engine->camera().lerpPos(process(),
+				as3D(pointObject->position()),
+				getNumberArg(1), (EasingType)getNumberArg(2)));
+		}
+		case ScriptKernelTask::LerpCamToObjectWithScale: {
+			if (!process().isActiveForPlayer())
+				return TaskReturn::waitFor(delay(getNumberArg(2)));
+			auto object = g_engine->world().getObjectByName(process().character(), getStringArg(0));
+			auto pointObject = dynamic_cast<PointObject *>(object);
+			if (pointObject == nullptr)
+				error("Invalid target object for LerpCamToObjectWithScale: %s", getStringArg(0));
+			return TaskReturn::waitFor(g_engine->camera().lerpPosScale(process(),
+				as3D(pointObject->position()), getNumberArg(1) * 0.01f,
+				getNumberArg(2), (EasingType)getNumberArg(3), (EasingType)getNumberArg(4)));
+		}
 
 		// Fades
 		case ScriptKernelTask::FadeType0:


Commit: 550f23ab4e88fcbff5a57b3faf58fb8d03778cb9
    https://github.com/scummvm/scummvm/commit/550f23ab4e88fcbff5a57b3faf58fb8d03778cb9
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:48+02:00

Commit Message:
ALCACHOFA: Reorder and group kernel tasks

Changed paths:
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 061dca0e653..d6b78cfd2fa 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -404,6 +404,7 @@ private:
 
 	TaskReturn kernelCall(ScriptKernelTask task) {
 		switch (task) {
+		// sound/video
 		case ScriptKernelTask::PlayVideo:
 			warning("STUB KERNEL CALL: PlayVideo");
 			return TaskReturn::finish(0);
@@ -419,9 +420,65 @@ private:
 		case ScriptKernelTask::WaitForMusicToEnd:
 			warning("STUB KERNEL CALL: WaitForMusicToEnd");
 			return TaskReturn::finish(0);
+
+		// Misc / control flow
 		case ScriptKernelTask::ShowCenterBottomText:
 			warning("STUB KERNEL CALL: ShowCenterBottomText");
 			return TaskReturn::finish(0);
+		case ScriptKernelTask::Delay:
+			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::Fork:
+			g_engine->scheduler().createProcess<ScriptTask>(process().character(), *this);
+			return TaskReturn::finish(0); // 0 means this is the forking process
+		case ScriptKernelTask::KillProcesses:
+			warning("STUB KERNEL CALL: KillProcesses");
+			return TaskReturn::finish(0);
+
+		// player/world state changes
+		case ScriptKernelTask::ChangeCharacter:
+			warning("STUB KERNEL CALL: ChangeCharacter");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::ChangeRoom:
+			warning("STUB KERNEL CALL: ChangeRoom");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::ToggleRoomFloor:
+			if (process().character() == MainCharacterKind::None) {
+				if (g_engine->player().currentRoom() != nullptr)
+					g_engine->player().currentRoom()->toggleActiveFloor();
+			}
+			else
+				g_engine->world().getMainCharacterByKind(process().character()).room()->toggleActiveFloor();
+			return TaskReturn::finish(1);
+		case ScriptKernelTask::LerpWorldLodBias:
+			warning("STUB KERNEL CALL: LerpWorldLodBias");
+			return TaskReturn::finish(0);
+		
+		// object control / animation
+		case ScriptKernelTask::On:
+			g_engine->world().toggleObject(process().character(), getStringArg(0), true);
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::Off:
+			g_engine->world().toggleObject(process().character(), getStringArg(0), false);
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::Animate: {
+			auto object = g_engine->world().getObjectByName(process().character(), getStringArg(0));
+			auto graphicObject = dynamic_cast<GraphicObject *>(object);
+			if (graphicObject == nullptr)
+				error("Script tried to animate invalid graphic object %s", getStringArg(0));
+			if (getNumberOrStringArg(1)) {
+				graphicObject->toggle(true);
+				graphicObject->graphic()->start(false);
+				return TaskReturn::finish(1);
+			}
+			else
+				return TaskReturn::waitFor(graphicObject->animate(process()));
+		}
+
+		// character control / animation
 		case ScriptKernelTask::StopAndTurn: {
 			auto object = g_engine->world().getObjectByName(process().character(), getStringArg(0));
 			auto character = dynamic_cast<WalkingCharacter *>(object);
@@ -435,23 +492,6 @@ private:
 			relatedCharacter().stopWalking((Direction)getNumberArg(0));
 			return TaskReturn::finish(1);
 		}
-		case ScriptKernelTask::ChangeCharacter:
-			warning("STUB KERNEL CALL: ChangeCharacter");
-			return TaskReturn::finish(0);
-		case ScriptKernelTask::SayText: {
-			const char *characterName = getStringArg(0);
-			int32 dialogId = getNumberArg(1);
-			if (strncmp(characterName, "MENU_", 5) == 0) {
-				g_engine->world().getMainCharacterByKind(process().character()).addDialogLine(dialogId);
-				return TaskReturn::finish(1);
-			}
-			Character *_character = strcmp(characterName, "AMBOS") == 0
-				? &g_engine->world().getMainCharacterByKind(process().character())
-				: dynamic_cast<Character *>(g_engine->world().getObjectByName(characterName));
-			if (_character == nullptr)
-				error("Invalid character for sayText: %s", characterName);
-			return TaskReturn::waitFor(_character->sayText(process(), dialogId));
-		};
 		case ScriptKernelTask::Go: {
 			auto characterObject = g_engine->world().getObjectByName(process().character(), getStringArg(0));
 			auto character = dynamic_cast<WalkingCharacter *>(characterObject);
@@ -485,18 +525,36 @@ private:
 		case ScriptKernelTask::ChangeCharacterRoom:
 			warning("STUB KERNEL CALL: ChangeCharacterRoom");
 			return TaskReturn::finish(0);
-		case ScriptKernelTask::KillProcesses:
-			warning("STUB KERNEL CALL: KillProcesses");
-			return TaskReturn::finish(0);
 		case ScriptKernelTask::LerpCharacterLodBias:
 			warning("STUB KERNEL CALL: LerpCharacterLodBias");
 			return TaskReturn::finish(0);
-		case ScriptKernelTask::On:
-			g_engine->world().toggleObject(process().character(), getStringArg(0), true);
+		case ScriptKernelTask::AnimateCharacter:
+			warning("STUB KERNEL CALL: AnimateCharacter");
 			return TaskReturn::finish(0);
-		case ScriptKernelTask::Off:
-			g_engine->world().toggleObject(process().character(), getStringArg(0), false);
+		case ScriptKernelTask::AnimateTalking:
+			warning("STUB KERNEL CALL: AnimateTalking");
 			return TaskReturn::finish(0);
+		case ScriptKernelTask::SayText: {
+			const char *characterName = getStringArg(0);
+			int32 dialogId = getNumberArg(1);
+			if (strncmp(characterName, "MENU_", 5) == 0) {
+				g_engine->world().getMainCharacterByKind(process().character()).addDialogLine(dialogId);
+				return TaskReturn::finish(1);
+			}
+			Character *_character = strcmp(characterName, "AMBOS") == 0
+				? &g_engine->world().getMainCharacterByKind(process().character())
+				: dynamic_cast<Character *>(g_engine->world().getObjectByName(characterName));
+			if (_character == nullptr)
+				error("Invalid character for sayText: %s", characterName);
+			return TaskReturn::waitFor(_character->sayText(process(), dialogId));
+		};
+		case ScriptKernelTask::SetDialogLineReturn:
+			g_engine->world().getMainCharacterByKind(process().character()).setLastDialogReturnValue(getNumberArg(0));
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::DialogMenu:
+			return TaskReturn::waitFor(g_engine->world().getMainCharacterByKind(process().character()).dialogMenu(process()));
+
+		// Inventory control
 		case ScriptKernelTask::Pickup:
 			relatedCharacter().pickup(getStringArg(0), getNumberArg(1));
 			return TaskReturn::finish(1);
@@ -513,50 +571,6 @@ private:
 			character.drop(getStringArg(0));
 			return TaskReturn::finish(1);
 		}
-		case ScriptKernelTask::Delay:
-			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::Fork:
-			g_engine->scheduler().createProcess<ScriptTask>(process().character(), *this);
-			return TaskReturn::finish(0); // 0 means this is the forking process
-		case ScriptKernelTask::Animate: {
-			auto object = g_engine->world().getObjectByName(process().character(), getStringArg(0));
-			auto graphicObject = dynamic_cast<GraphicObject *>(object);
-			if (graphicObject == nullptr)
-				error("Script tried to animate invalid graphic object %s", getStringArg(0));
-			if (getNumberOrStringArg(1)) {
-				graphicObject->toggle(true);
-				graphicObject->graphic()->start(false);
-				return TaskReturn::finish(1);
-			}
-			else
-				return TaskReturn::waitFor(graphicObject->animate(process()));
-		}
-		case ScriptKernelTask::AnimateCharacter:
-			warning("STUB KERNEL CALL: AnimateCharacter");
-			return TaskReturn::finish(0);
-		case ScriptKernelTask::AnimateTalking:
-			warning("STUB KERNEL CALL: AnimateTalking");
-			return TaskReturn::finish(0);
-		case ScriptKernelTask::ChangeRoom:
-			warning("STUB KERNEL CALL: ChangeRoom");
-			return TaskReturn::finish(0);
-		case ScriptKernelTask::ToggleRoomFloor:
-			if (process().character() == MainCharacterKind::None) {
-				if (g_engine->player().currentRoom() != nullptr)
-					g_engine->player().currentRoom()->toggleActiveFloor();
-			}
-			else
-				g_engine->world().getMainCharacterByKind(process().character()).room()->toggleActiveFloor();
-			return TaskReturn::finish(1);
-		case ScriptKernelTask::SetDialogLineReturn:
-			g_engine->world().getMainCharacterByKind(process().character()).setLastDialogReturnValue(getNumberArg(0));
-			return TaskReturn::finish(0);
-		case ScriptKernelTask::DialogMenu:
-			return TaskReturn::waitFor(g_engine->world().getMainCharacterByKind(process().character()).dialogMenu(process()));
 		case ScriptKernelTask::ClearInventory:
 			switch((MainCharacterKind)getNumberArg(0)) {
 			case MainCharacterKind::Mortadelo: g_engine->world().mortadelo().clearInventory(); break;
@@ -564,9 +578,6 @@ private:
 			default: error("Script attempted to clear inventory with invalid character kind"); break;
 			}
 			return TaskReturn::finish(1);
-		case ScriptKernelTask::LerpWorldLodBias:
-			warning("STUB KERNEL CALL: LerpWorldLodBias");
-			return TaskReturn::finish(0);
 
 		// Camera tasks
 		case ScriptKernelTask::SetMaxCamSpeedFactor:
@@ -657,6 +668,7 @@ private:
 			warning("STUB KERNEL CALL: FadeOut2");
 			return TaskReturn::finish(0);
 
+		// Unused and useless
 		case ScriptKernelTask::SetActiveTextureSet:
 			// Fortunately this seems to be unused.
 			warning("STUB KERNEL CALL: SetActiveTextureSet");


Commit: 97e9bc81b4bd6a2b0eea72a50e52fad34b82b542
    https://github.com/scummvm/scummvm/commit/97e9bc81b4bd6a2b0eea72a50e52fad34b82b542
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:48+02:00

Commit Message:
ALCACHOFA: Add fades for doors

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


diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 2b07ab58f07..22c56ce5ec6 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -656,16 +656,16 @@ TextDrawRequest::TextDrawRequest(Font &font, const char *originalText, Point pos
 		}
 		// now we are in new-line territory
 
+		if (*itChar > ' ')
+			itChar = trimTrailing(itChar, itLine, false); // trim last word
 		if (centered) {
-			if (*itChar > ' ')
-				itChar = trimTrailing(itChar, itLine, false); // trim last word
 			itChar = trimTrailing(itChar, itLine, true) + 1;
 			itLine = trimLeading(itLine, itChar);
 			_allLines[lineCount] = TextLine(itLine, itChar - itLine);
-			itChar = trimLeading(itChar, textEnd);
 		}
 		else
 			_allLines[lineCount] = TextLine(itLine, itChar - itLine);
+		itChar = trimLeading(itChar, textEnd);
 		lineCount++;
 		lineWidth = 0;
 		itLine = itChar;
@@ -726,6 +726,84 @@ void TextDrawRequest::draw() {
 	}
 }
 
+FadeDrawRequest::FadeDrawRequest(FadeType type, float value, int8 order)
+	: IDrawRequest(order)
+	, _type(type)
+	, _value(value) {}
+
+void FadeDrawRequest::draw() {
+	Color color;
+	const byte valueAsByte = (byte)(_value * 255);
+	switch (_type) {
+		case FadeType::ToBlack:
+			color = { 0, 0, 0, valueAsByte };
+			g_engine->renderer().setBlendMode(BlendMode::AdditiveAlpha);
+			break;
+		case FadeType::ToWhite:
+			color = { valueAsByte, valueAsByte, valueAsByte, valueAsByte };
+			g_engine->renderer().setBlendMode(BlendMode::Additive);
+			break;
+		default:
+			assert(false && "Invalid fade type");
+			return;
+	}
+	g_engine->renderer().setTexture(nullptr);
+	g_engine->renderer().quad(Vector2d(0, 0), as2D(Point(g_system->getWidth(), g_system->getHeight())), color);
+}
+
+struct FadeTask : public Task {
+	FadeTask(Process &process, FadeType fadeType,
+		float from, float to,
+		uint32 duration, EasingType easingType,
+		int8 order)
+		: Task(process)
+		, _fadeType(fadeType)
+		, _from(from)
+		, _to(to)
+		, _duration(duration)
+		, _easingType(easingType)
+		, _order(order) {}
+
+	virtual TaskReturn run() override {
+		TASK_BEGIN;
+		_startTime = g_system->getMillis();
+		while (g_system->getMillis() - _startTime < _duration) {
+			draw((g_system->getMillis() - _startTime) / (float)_duration);
+			TASK_YIELD;
+		}
+		draw(1.0f); // so that during a loading lag the screen is completly black/white
+		TASK_END;
+	}
+
+	virtual void debugPrint() override {
+		uint32 remaining = g_system->getMillis() - _startTime <= _duration
+			? _duration - (g_system->getMillis() - _startTime)
+			: 0;
+	}
+
+private:
+	void draw(float t) {
+		g_engine->drawQueue().add<FadeDrawRequest>(_fadeType, _from + (_to - _from) * ease(t, _easingType), _order);
+	}
+
+	FadeType _fadeType;
+	float _from, _to;
+	uint32 _startTime = 0, _duration;
+	EasingType _easingType;
+	int8 _order;
+};
+
+Task *fade(Process &process, FadeType fadeType,
+	float from, float to,
+	int32 duration, EasingType easingType,
+	int8 order) {
+	if (duration <= 0)
+		return new DelayTask(process, 0);
+	if (!process.isActiveForPlayer())
+		return new DelayTask(process, (uint32)duration);
+	return new FadeTask(process, fadeType, from, to, duration, easingType, order);
+}
+
 DrawQueue::DrawQueue(IRenderer *renderer)
 	: _renderer(renderer)
 	, _allocator(1024) {
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 4395d269f40..6f5b538a9d0 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -81,7 +81,7 @@ public:
 	virtual void setBlendMode(BlendMode blendMode) = 0;
 	virtual void setLodBias(float lodBias) = 0;
 	virtual void quad(
-		Math::Vector2d center,
+		Math::Vector2d center, // TOOD: Use topLeft&size instead of center&size
 		Math::Vector2d size,
 		Color color = kWhite,
 		Math::Angle rotation = Math::Angle(),
@@ -374,6 +374,28 @@ private:
 	int _allPosX[kMaxLines];
 };
 
+enum class FadeType {
+	ToBlack,
+	ToWhite
+	// TODO: Add CrossFade fade type
+};
+
+class FadeDrawRequest : public IDrawRequest {
+public:
+	FadeDrawRequest(FadeType type, float value, int8 order);
+
+	virtual void draw() override;
+
+private:
+	FadeType _type;
+	float _value;
+};
+
+Task *fade(Process &process, FadeType fadeType,
+	float from, float to,
+	int32 duration, EasingType easingType,
+	int8 order);
+
 class BumpAllocator {
 public:
 	BumpAllocator(size_t pageSize);
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index 2e094dba117..ddff15a0e69 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -188,7 +188,7 @@ struct DoorTask : public Task {
 		TASK_BEGIN;
 		// TODO: Fade out music on room change
 		// TODO: Fade out/in on room change instead of delay
-		TASK_WAIT(delay(500));
+		TASK_WAIT(fade(process(), FadeType::ToBlack, 0, 1, 500, EasingType::Out, -5));
 		_player.changeRoom(_targetRoom->name(), true);
 
 		if (_targetRoom->fixedCameraOnEntering())
@@ -202,9 +202,9 @@ struct DoorTask : public Task {
 
 		// TODO: Start music on room change
 		if (g_engine->script().createProcess(_character->kind(), "ENTRAR_" + _targetRoom->name(), ScriptFlags::AllowMissing))
-			TASK_WAIT(delay(0));
+			TASK_YIELD;
 		else
-			TASK_WAIT(delay(500));
+			TASK_WAIT(fade(process(), FadeType::ToBlack, 1, 0, 500, EasingType::Out, -5));
 		TASK_END;
 	}
 
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index d6b78cfd2fa..de05df98fdb 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -647,32 +647,34 @@ private:
 
 		// Fades
 		case ScriptKernelTask::FadeType0:
-			warning("STUB KERNEL CALL: FadeType0");
-			return TaskReturn::finish(0);
+			return TaskReturn::waitFor(fade(process(), FadeType::ToBlack,
+				getNumberArg(0) * 0.01f, getNumberArg(1) * 0.01f,
+				getNumberArg(2), (EasingType)getNumberArg(4), getNumberArg(3)));
 		case ScriptKernelTask::FadeType1:
-			warning("STUB KERNEL CALL: FadeType1");
-			return TaskReturn::finish(0);
-		case ScriptKernelTask::FadeType2:
-			warning("STUB KERNEL CALL: FadeType2");
-			return TaskReturn::finish(0);
+			return TaskReturn::waitFor(fade(process(), FadeType::ToWhite,
+				getNumberArg(0) * 0.01f, getNumberArg(1) * 0.01f,
+				getNumberArg(2), (EasingType)getNumberArg(4), getNumberArg(3)));
 		case ScriptKernelTask::FadeIn:
-			warning("STUB KERNEL CALL: FadeIn");
-			return TaskReturn::finish(0);
+			return TaskReturn::waitFor(fade(process(), FadeType::ToBlack,
+				1.0f, 0.0f, getNumberArg(0), EasingType::Out, -5));
 		case ScriptKernelTask::FadeOut:
-			warning("STUB KERNEL CALL: FadeOut");
-			return TaskReturn::finish(0);
+			return TaskReturn::waitFor(fade(process(), FadeType::ToBlack,
+				0.0f, 1.0f, getNumberArg(0), EasingType::Out, -5));
 		case ScriptKernelTask::FadeIn2:
-			warning("STUB KERNEL CALL: FadeIn2");
-			return TaskReturn::finish(0);
+			return TaskReturn::waitFor(fade(process(), FadeType::ToBlack,
+				0.0f, 1.0f, getNumberArg(0), (EasingType)getNumberArg(1), -5));
 		case ScriptKernelTask::FadeOut2:
-			warning("STUB KERNEL CALL: FadeOut2");
-			return TaskReturn::finish(0);
+			return TaskReturn::waitFor(fade(process(), FadeType::ToBlack,
+				1.0f, 0.0f, getNumberArg(0), (EasingType)getNumberArg(1), -5));
 
 		// Unused and useless
 		case ScriptKernelTask::SetActiveTextureSet:
 			// Fortunately this seems to be unused.
 			warning("STUB KERNEL CALL: SetActiveTextureSet");
 			return TaskReturn::finish(0);
+		case ScriptKernelTask::FadeType2:
+			warning("STUB KERNEL CALL: FadeType2"); // Crossfade, unused from script
+			return TaskReturn::finish(0);
 		case ScriptKernelTask::Nop10:
 		case ScriptKernelTask::Nop24:
 		case ScriptKernelTask::Nop34:


Commit: 206c749922ddbd29a01470ebd6b6488aa9732fb5
    https://github.com/scummvm/scummvm/commit/206c749922ddbd29a01470ebd6b6488aa9732fb5
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:48+02:00

Commit Message:
ALCACHOFA: Add inventory

Changed paths:
    engines/alcachofa/camera.cpp
    engines/alcachofa/camera.h
    engines/alcachofa/console.cpp
    engines/alcachofa/console.h
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/graphics.h
    engines/alcachofa/objects.h
    engines/alcachofa/player.cpp
    engines/alcachofa/player.h
    engines/alcachofa/rooms.cpp
    engines/alcachofa/rooms.h
    engines/alcachofa/script.cpp
    engines/alcachofa/script.h


diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index 5dd3f2b2bcb..282932f65f9 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -32,9 +32,9 @@ using namespace Math;
 namespace Alcachofa {
 
 void Camera::resetRotationAndScale() {
-	_scale = 1;
-	_rotation = 0;
-	_usedCenter.z() = 0;
+	_cur._scale = 1;
+	_cur._rotation = 0;
+	_cur._usedCenter.z() = 0;
 }
 
 void Camera::setRoomBounds(Point bgSize, int16 bgScale) {
@@ -49,7 +49,7 @@ void Camera::setRoomBounds(Point bgSize, int16 bgScale) {
 }
 
 void Camera::setFollow(WalkingCharacter *target, bool catchUp) {
-	_followTarget = target;
+	_cur._followTarget = target;
 	_lastUpdateTime = g_system->getMillis();
 	_catchUp = catchUp;
 	if (target == nullptr)
@@ -57,14 +57,26 @@ void Camera::setFollow(WalkingCharacter *target, bool catchUp) {
 }
 
 void Camera::setPosition(Vector2d v) {
-	setPosition({ v.getX(), v.getY(), _usedCenter.z() });
+	setPosition({ v.getX(), v.getY(), _cur._usedCenter.z() });
 }
 
 void Camera::setPosition(Vector3d v) {
-	_usedCenter = v;
+	_cur._usedCenter = v;
 	setFollow(nullptr);
 }
 
+void Camera::backup(uint slot) {
+	assert(slot < kStateBackupCount);
+	_backups[slot] = _cur;
+}
+
+void Camera::restore(uint slot) {
+	assert(slot < kStateBackupCount);
+	auto backupState = _backups[slot];
+	_backups[slot] = _cur;
+	_cur = backupState;
+}
+
 static Matrix4 scaleMatrix(float scale) {
 	Matrix4 m;
 	m(0, 0) = scale;
@@ -75,16 +87,16 @@ static Matrix4 scaleMatrix(float scale) {
 
 void Camera::setupMatricesAround(Vector3d center) {
 	Matrix4 matTemp;
-	matTemp.buildAroundZ(_rotation);
+	matTemp.buildAroundZ(_cur._rotation);
 	_mat3Dto2D.setToIdentity();
 	_mat3Dto2D.translate(-center);
 	_mat3Dto2D = matTemp * _mat3Dto2D;
-	_mat3Dto2D = _mat3Dto2D * scaleMatrix(_scale);
+	_mat3Dto2D = _mat3Dto2D * scaleMatrix(_cur._scale);
 
 	_mat2Dto3D.setToIdentity();
 	_mat2Dto3D.translate(center);
-	matTemp.buildAroundZ(-_rotation);
-	matTemp = scaleMatrix(1 / _scale) * matTemp;
+	matTemp.buildAroundZ(-_cur._rotation);
+	matTemp = scaleMatrix(1 / _cur._scale) * matTemp;
 	_mat2Dto3D = matTemp * _mat2Dto3D;
 }
 
@@ -122,7 +134,7 @@ Vector3d Camera::transform2Dto3D(Vector3d v2d) const {
 	// if this looks like normal 3D math to *someone* please contact.
 	Vector4d vh;
 	vh.w() = 1.0f;
-	vh.z() = v2d.z() - _usedCenter.z();
+	vh.z() = v2d.z() - _cur._usedCenter.z();
 	vh.y() = (v2d.y() - g_system->getHeight() * 0.5f) * vh.z() * kInvBaseScale;
 	vh.x() = (v2d.x() - g_system->getWidth() * 0.5f) * vh.z() * kInvBaseScale;
 	vh = _mat2Dto3D * vh;
@@ -141,7 +153,7 @@ Vector3d Camera::transform3Dto2D(Vector3d v3d) const {
 	return Vector3d(
 		g_system->getWidth() * 0.5f + vh.x() * kBaseScale / vh.z(),
 		g_system->getHeight() * 0.5f + vh.y() * kBaseScale / vh.z(),
-		_scale * kBaseScale / vh.z());
+		_cur._scale * kBaseScale / vh.z());
 }
 
 void Camera::update() {
@@ -151,66 +163,66 @@ void Camera::update() {
 	deltaTime = MAX(0.001f, MIN(0.5f, deltaTime));
 	_lastUpdateTime = now;
 
-	if (_catchUp && _followTarget != nullptr) {
+	if (_catchUp && _cur._followTarget != nullptr) {
 		for (int i = 0; i < 4; i++)
 			updateFollowing(50.0f);
 	}
 	else
 		updateFollowing(deltaTime);
-	setAppliedCenter(_usedCenter + Vector3d(_shake.getX(), _shake.getY(), 0.0f));
+	setAppliedCenter(_cur._usedCenter + Vector3d(_shake.getX(), _shake.getY(), 0.0f));
 }
 
 void Camera::updateFollowing(float deltaTime) {
-	if (_followTarget == nullptr)
+	if (_cur._followTarget == nullptr)
 		return;
 	const float resolutionFactor = g_system->getWidth() * 0.00125f;
 	const float acceleration = 460 * resolutionFactor;
 	const float baseDeadZoneSize = 25 * resolutionFactor;
 	const float minSpeed = 20 * resolutionFactor;
-	const float maxSpeed = this->_maxSpeedFactor * resolutionFactor;
-	const float depthScale = _followTarget->graphic()->depthScale();
-	const auto characterPolygon = _followTarget->shape()->at(0);
+	const float maxSpeed = this->_cur._maxSpeedFactor * resolutionFactor;
+	const float depthScale = _cur._followTarget->graphic()->depthScale();
+	const auto characterPolygon = _cur._followTarget->shape()->at(0);
 	const float halfHeight = ABS(characterPolygon._points[0].y - characterPolygon._points[2].y) / 2.0f;
 
 	Vector3d targetCenter = setAppliedCenter({
-		_shake.getX() + _followTarget->position().x,
-		_shake.getY() + _followTarget->position().y - depthScale * 85,
-		_usedCenter.z()});
+		_shake.getX() + _cur._followTarget->position().x,
+		_shake.getY() + _cur._followTarget->position().y - depthScale * 85,
+		_cur._usedCenter.z()});
 	targetCenter.y() -= halfHeight;
-	float distanceToTarget = as2D(_usedCenter - targetCenter).getMagnitude();
-	float moveDistance = _followTarget->stepSizeFactor() * _speed * deltaTime;
+	float distanceToTarget = as2D(_cur._usedCenter - targetCenter).getMagnitude();
+	float moveDistance = _cur._followTarget->stepSizeFactor() * _cur._speed * deltaTime;
 
-	float deadZoneSize = baseDeadZoneSize / _scale;
-	if (_followTarget->isWalking() && depthScale > 0.8f)
-		deadZoneSize = (baseDeadZoneSize + (depthScale - 0.8f) * 200) / _scale;
+	float deadZoneSize = baseDeadZoneSize / _cur._scale;
+	if (_cur._followTarget->isWalking() && depthScale > 0.8f)
+		deadZoneSize = (baseDeadZoneSize + (depthScale - 0.8f) * 200) / _cur._scale;
 	bool isFarAway = false;
-	if (ABS(targetCenter.x() - _usedCenter.x()) > deadZoneSize ||
-		ABS(targetCenter.y() - _usedCenter.y()) > deadZoneSize) {
+	if (ABS(targetCenter.x() - _cur._usedCenter.x()) > deadZoneSize ||
+		ABS(targetCenter.y() - _cur._usedCenter.y()) > deadZoneSize) {
 		isFarAway = true;
-		_isBraking = false;
+		_cur._isBraking = false;
 		_isChanging = true;
 	}
 
-	if (_isBraking) {
-		_speed -= acceleration * 0.9f * deltaTime;
-		_speed = MAX(_speed, minSpeed);
+	if (_cur._isBraking) {
+		_cur._speed -= acceleration * 0.9f * deltaTime;
+		_cur._speed = MAX(_cur._speed, minSpeed);
 	}
-	if (_isChanging && !_isBraking) {
-		_speed += acceleration * deltaTime;
-		_speed = MIN(_speed, maxSpeed);
+	if (_isChanging && !_cur._isBraking) {
+		_cur._speed += acceleration * deltaTime;
+		_cur._speed = MIN(_cur._speed, maxSpeed);
 		if (!isFarAway)
-			_isBraking = true;
+			_cur._isBraking = true;
 	}
 	if (_isChanging) {
 		if (distanceToTarget <= moveDistance) {
-			_usedCenter = targetCenter;
+			_cur._usedCenter = targetCenter;
 			_isChanging = false;
-			_isBraking = false;
+			_cur._isBraking = false;
 		}
 		else {
-			Vector3d deltaCenter = targetCenter - _usedCenter;
+			Vector3d deltaCenter = targetCenter - _cur._usedCenter;
 			deltaCenter.z() = 0.0f;
-			_usedCenter += deltaCenter * moveDistance / distanceToTarget;
+			_cur._usedCenter += deltaCenter * moveDistance / distanceToTarget;
 		}
 	}
 }
@@ -266,12 +278,12 @@ protected:
 struct CamLerpScaleTask final : public CamLerpTask {
 	CamLerpScaleTask(Process &process, float targetScale, int32 duration, EasingType easingType)
 		: CamLerpTask(process, duration, easingType)
-		, _fromScale(_camera._scale)
-		, _deltaScale(targetScale - _camera._scale) {}
+		, _fromScale(_camera._cur._scale)
+		, _deltaScale(targetScale - _camera._cur._scale) {}
 
 protected:
 	virtual void update(float t) override {
-		_camera._scale = _fromScale + _deltaScale * t;
+		_camera._cur._scale = _fromScale + _deltaScale * t;
 	}
 
 	float _fromScale, _deltaScale;
@@ -284,15 +296,15 @@ struct CamLerpPosScaleTask final : public CamLerpTask {
 		: CamLerpTask(process, duration, EasingType::Linear) // linear as we need different ones per component
 		, _fromPos(_camera._appliedCenter)
 		, _deltaPos(targetPos - _camera._appliedCenter)
-		, _fromScale(_camera._scale)
-		, _deltaScale(targetScale - _camera._scale)
+		, _fromScale(_camera._cur._scale)
+		, _deltaScale(targetScale - _camera._cur._scale)
 		, _moveEasingType(moveEasingType)
 		, _scaleEasingType(scaleEasingType) {}
 
 protected:
 	virtual void update(float t) override {
 		_camera.setPosition(_fromPos + _deltaPos * ease(t, _moveEasingType));
-		_camera._scale = _fromScale + _deltaScale * ease(t, _scaleEasingType);
+		_camera._cur._scale = _fromScale + _deltaScale * ease(t, _scaleEasingType);
 	}
 
 	Vector3d _fromPos, _deltaPos;
@@ -303,12 +315,12 @@ protected:
 struct CamLerpRotationTask final : public CamLerpTask {
 	CamLerpRotationTask(Process &process, float targetRotation, int32 duration, EasingType easingType)
 		: CamLerpTask(process, duration, easingType)
-		, _fromRotation(_camera._rotation.getDegrees())
-		, _deltaRotation(targetRotation - _camera._rotation.getDegrees()) {}
+		, _fromRotation(_camera._cur._rotation.getDegrees())
+		, _deltaRotation(targetRotation - _camera._cur._rotation.getDegrees()) {}
 
 protected:
 	virtual void update(float t) override {
-		_camera._rotation = Angle(_fromRotation + _deltaRotation * t);
+		_camera._cur._rotation = Angle(_fromRotation + _deltaRotation * t);
 	}
 
 	float _fromRotation, _deltaRotation;
diff --git a/engines/alcachofa/camera.h b/engines/alcachofa/camera.h
index 995521b7bd8..cdca9db6852 100644
--- a/engines/alcachofa/camera.h
+++ b/engines/alcachofa/camera.h
@@ -36,9 +36,9 @@ static constexpr const float kInvBaseScale = 1.0f / kBaseScale;
 
 class Camera {
 public:
-	inline Math::Angle rotation() const { return _rotation; }
+	inline Math::Angle rotation() const { return _cur._rotation; }
 	inline Math::Vector2d &shake() { return _shake; }
-	inline WalkingCharacter *followTarget() { return _followTarget; }
+	inline WalkingCharacter *followTarget() { return _cur._followTarget; }
 
 	void update();
 	Math::Vector3d transform2Dto3D(Math::Vector3d v) const;
@@ -48,6 +48,8 @@ public:
 	void setFollow(WalkingCharacter *target, bool catchUp = false);
 	void setPosition(Math::Vector2d v);
 	void setPosition(Math::Vector3d v);
+	void backup(uint slot);
+	void restore(uint slot);
 
 	Task *lerpPos(Process &process,
 		Math::Vector2d targetPos,
@@ -82,27 +84,31 @@ private:
 	void setupMatricesAround(Math::Vector3d center);
 	void updateFollowing(float deltaTime);
 
+	struct State {
+		Math::Vector3d _usedCenter = Math::Vector3d(512, 384, 0);
+		float
+			_scale = 1.0f,
+			_speed = 0.0f,
+			_maxSpeedFactor = 230.0f;
+		Math::Angle _rotation;
+		bool _isBraking = false;
+		WalkingCharacter *_followTarget = nullptr;
+	};
+
+	static constexpr uint kStateBackupCount = 2;
+	State _cur, _backups[kStateBackupCount];
 	uint32 _lastUpdateTime = 0;
 	bool _isChanging = false,
-		_isBraking = false,
 		_catchUp = false;
-	float
-		_scale = 1.0f,
-		_roomScale = 1.0f,
-		_maxSpeedFactor = 230.0f,
-		_speed = 0.0f;
-	Math::Angle _rotation;
+	float _roomScale = 1.0f;
 	Math::Vector2d
 		_roomMin = Math::Vector2d(-10000, -10000),
 		_roomMax = Math::Vector2d(10000, 10000),
 		_shake;
-	Math::Vector3d
-		_usedCenter = Math::Vector3d(512, 384, 0),
-		_appliedCenter;
+	Math::Vector3d _appliedCenter;
 	Math::Matrix4
 		_mat3Dto2D,
 		_mat2Dto3D;
-	WalkingCharacter *_followTarget = nullptr;
 };
 
 }
diff --git a/engines/alcachofa/console.cpp b/engines/alcachofa/console.cpp
index ed7e1629771..81206ab9779 100644
--- a/engines/alcachofa/console.cpp
+++ b/engines/alcachofa/console.cpp
@@ -39,6 +39,8 @@ Console::Console() : GUI::Debugger() {
 	registerCmd("rooms", WRAP_METHOD(Console, cmdRooms));
 	registerCmd("changeRoom", WRAP_METHOD(Console, cmdChangeRoom));
 	registerCmd("disableDebugDraw", WRAP_METHOD(Console, cmdDisableDebugDraw));
+	registerCmd("pickup", WRAP_METHOD(Console, cmdItem));
+	registerCmd("drop", WRAP_METHOD(Console, cmdItem));
 }
 
 Console::~Console() {
@@ -139,4 +141,56 @@ bool Console::cmdDisableDebugDraw(int argc, const char **args) {
 	return true;
 }
 
+bool Console::cmdItem(int argc, const char **args) {
+	auto &inventory = g_engine->world().inventory();
+	auto &mortadelo = g_engine->world().mortadelo();
+	auto &filemon = g_engine->world().filemon();
+	auto *active = g_engine->player().activeCharacter();
+	if (argc < 2 || argc > 3) {
+		debugPrintf("usage: %s [Mortadelo/Filemon] [<item>]\n\n", args[0]);
+		debugPrintf("%20s%10s%10s\n", "Item", "Mortadelo", "Filemon");
+		for (auto itItem = inventory.beginObjects(); itItem != inventory.endObjects(); ++itItem) {
+			if (dynamic_cast<const Item *>(*itItem) == nullptr)
+				continue;
+			debugPrintf("%20s%10s%10s\n",
+				(*itItem)->name().c_str(),
+				mortadelo.hasItem((*itItem)->name()) ? "YES" : "no",
+				filemon.hasItem((*itItem)->name()) ? "YES" : "no");
+		}
+		return true;
+	}
+	if (argc == 2 && active == nullptr) {
+		debugPrintf("No character is active, name has to be specified\n");
+		return true;
+	}
+
+	const char *itemName = args[1];
+	if (argc == 3) {
+		itemName = args[2];
+		if (strcmpi(args[1], "mortadelo") == 0 || strcmpi(args[1], "m") == 0)
+			active = &mortadelo;
+		else if (strcmpi(args[1], "filemon") == 0 || strcmpi(args[1], "f") == 0)
+			active = &filemon;
+		else {
+			debugPrintf("Invalid character name \"%s\", has to be either \"mortadelo\" or \"filemon\"\n", args[1]);
+			return true;
+		}
+	}
+
+	bool hasMatchedSomething = false;
+	for (auto itItem = inventory.beginObjects(); itItem != inventory.endObjects(); ++itItem) {
+		if (dynamic_cast<const Item *>(*itItem) == nullptr ||
+			!(*itItem)->name().matchString(itemName, true))
+			continue;
+		hasMatchedSomething = true;
+		if (args[0][0] == 'p')
+			active->pickup((*itItem)->name(), false);
+		else
+			active->drop((*itItem)->name());
+	}
+	if (!hasMatchedSomething)
+		debugPrintf("Cannot find any item matching \"%s\"\n", itemName);
+	return true;
+}
+
 } // End of namespace Alcachofa
diff --git a/engines/alcachofa/console.h b/engines/alcachofa/console.h
index 771873d0617..b59755b16f3 100644
--- a/engines/alcachofa/console.h
+++ b/engines/alcachofa/console.h
@@ -52,6 +52,7 @@ private:
 	bool cmdRooms(int argc, const char **args);
 	bool cmdChangeRoom(int argc, const char **args);
 	bool cmdDisableDebugDraw(int argc, const char **args);
+	bool cmdItem(int argc, const char **args);
 
 	bool _showInteractables = true;
 	bool _showCharacters = true;
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index a8607d2b872..2e90023932c 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -21,6 +21,7 @@
 
 #include "objects.h"
 #include "rooms.h"
+#include "script.h"
 #include "alcachofa.h"
 
 using namespace Common;
@@ -43,6 +44,24 @@ Item::Item(const Item &other)
 	new (&_graphic) Graphic(other._graphic);
 }
 
+void Item::trigger() {
+	auto &player = g_engine->player();
+	auto &heldItem = player.heldItem();
+	if (g_engine->input().wasMouseRightReleased()) {
+		if (heldItem == nullptr)
+			player.triggerObject(this, "MIRAR");
+		else
+			heldItem = nullptr;
+	}
+	else if (heldItem == nullptr)
+		heldItem = this;
+	else if (g_engine->script().hasProcedure(name(), heldItem->name()) ||
+		!g_engine->script().hasProcedure(heldItem->name(), name()))
+		player.triggerObject(this, heldItem->name().c_str());
+	else
+		player.triggerObject(heldItem, name().c_str());
+}
+
 ITriggerableObject::ITriggerableObject(ReadStream &stream)
 	: _interactionPoint(Shape(stream).firstPoint())
 	, _interactionDirection((Direction)stream.readSint32LE()) {}
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 22c56ce5ec6..2bde10577bb 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -779,6 +779,7 @@ struct FadeTask : public Task {
 		uint32 remaining = g_system->getMillis() - _startTime <= _duration
 			? _duration - (g_system->getMillis() - _startTime)
 			: 0;
+		g_engine->console().debugPrintf("Fade (%d) from %.2f to %.2f with %ums remaining\n", (int)_fadeType, _from, _to, remaining);
 	}
 
 private:
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 6f5b538a9d0..24119792664 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -184,6 +184,7 @@ public:
 	inline const Common::Point &frameCenter(int32 frameI) const { return _frames[frameI]._center; }
 	inline uint32 totalDuration() const { return _totalDuration; }
 	inline uint8 &premultiplyAlpha() { return _premultiplyAlpha; }
+	Common::Rect frameBounds(int32 frameI) const;
 	Common::Point totalFrameOffset(int32 frameI) const;
 	int32 frameAtTime(uint32 time) const;
 	int32 imageIndex(int32 frameI, int32 spriteI) const;
@@ -210,7 +211,6 @@ public:
 
 private:
 	Common::Rect spriteBounds(int32 frameI, int32 spriteI) const;
-	Common::Rect frameBounds(int32 frameI) const;
 	Common::Rect maxFrameBounds() const;
 	void prerenderFrame(int32 frameI);
 
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 84d281178b1..b3aba89d14c 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -317,6 +317,7 @@ public:
 	Item(const Item &other);
 
 	virtual const char *typeName() const;
+	void trigger();
 };
 
 class ITriggerableObject {
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index ddff15a0e69..a298634ffd6 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -74,10 +74,13 @@ void Player::updateCursor() {
 	drawCursor();
 }
 
-void Player::drawCursor() {
+void Player::drawCursor(bool forceDefaultCursor) {
 	Point cursorPos = g_engine->input().mousePos2D();
-	if (_heldItem == nullptr)
+	if (_heldItem == nullptr || forceDefaultCursor) {
+		if (forceDefaultCursor)
+			_cursorFrameI = 0;
 		g_engine->drawQueue().add<AnimationDrawRequest>(_cursorAnimation.get(), _cursorFrameI, as2D(cursorPos), -10);
+	}
 	else {
 		auto itemGraphic = _heldItem->graphic();
 		assert(itemGraphic != nullptr);
@@ -97,8 +100,12 @@ void Player::changeRoom(const Common::String &targetRoomName, bool resetCamera)
 	if (_currentRoom == &inventory)
 		keepResources = _roomBeforeInventory != nullptr && _roomBeforeInventory->name().equalsIgnoreCase(targetRoomName);
 	else {
-		keepResources = targetRoomName.equalsIgnoreCase("inventario") ||
-			(_currentRoom != nullptr && _currentRoom->name().equalsIgnoreCase(targetRoomName));
+		keepResources = _currentRoom != nullptr && _currentRoom->name().equalsIgnoreCase(targetRoomName);
+	}
+	_roomBeforeInventory = nullptr;
+	if (targetRoomName.equalsIgnoreCase("inventario")) {
+		keepResources = true;
+		_roomBeforeInventory = _currentRoom;
 	}
 
 	if (!keepResources && _currentRoom != nullptr) {
@@ -125,6 +132,11 @@ void Player::changeRoom(const Common::String &targetRoomName, bool resetCamera)
 	_pressedObject = _selectedObject = nullptr;
 }
 
+void Player::changeRoomToBeforeInventory() {
+	assert(_roomBeforeInventory != nullptr);
+	changeRoom(_roomBeforeInventory->name(), true);
+}
+
 MainCharacter *Player::inactiveCharacter() const {
 	if (_activeCharacter == nullptr)
 		return nullptr;
@@ -147,7 +159,7 @@ void Player::triggerObject(ObjectBase *object, const char *action) {
 		return;
 	debug("Trigger object %s %s with %s", object->typeName(), object->name().c_str(), action);
 
-	if (inactiveCharacter()->currentlyUsing() == object) {
+	if (strcmp(action, "MIRAR") == 0 || inactiveCharacter()->currentlyUsing() == object) {
 		action = "MIRAR";
 		_activeCharacter->currentlyUsing() = nullptr;
 	}
@@ -160,8 +172,9 @@ void Player::triggerObject(ObjectBase *object, const char *action) {
 	else if (scumm_stricmp(action, "MIRAR") == 0)
 		script.createProcess(activeCharacterKind(), "DefectoMirar");
 	else if (action[0] == 'i' && object->name()[0] == 'i')
-		// TODO: Check if and how this can happen. I guess it crashes now but might be ignored by the original engine
-		script.createProcess(activeCharacterKind(), "DefectoObjeto");
+		// 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 and ignore
+		;
 	else
 		script.createProcess(activeCharacterKind(), "DefectoUsar");
 }
diff --git a/engines/alcachofa/player.h b/engines/alcachofa/player.h
index 4841a448609..e8170185b78 100644
--- a/engines/alcachofa/player.h
+++ b/engines/alcachofa/player.h
@@ -31,7 +31,6 @@ public:
 	Player();
 
 	inline Room *currentRoom() const { return _currentRoom; }
-	inline Room *&currentRoom() { return _currentRoom; }
 	inline MainCharacter *activeCharacter() const { return _activeCharacter; }
     inline ShapeObject *&selectedObject() { return _selectedObject; }
 	inline ShapeObject *&pressedObject() { return _pressedObject; }
@@ -50,8 +49,9 @@ public:
 	void preUpdate();
 	void postUpdate();
 	void updateCursor();
-	void drawCursor();
+	void drawCursor(bool forceDefaultCursor = false);
 	void changeRoom(const Common::String &targetRoomName, bool resetCamera);
+	void changeRoomToBeforeInventory();
 	void triggerObject(ObjectBase *object, const char *action);
 	void triggerDoor(const Door *door);
 
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 75855459e4d..0b7771844e9 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -29,6 +29,21 @@ 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.
+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());
+}
+
 Room::Room(World *world, ReadStream &stream) : Room(world, stream, false) {
 }
 
@@ -129,11 +144,13 @@ void Room::update() {
 
 	if (g_engine->player().currentRoom() == this) {
 		updateRoomBounds();
+		updateClosingInventory();
 		if (!updateInput())
 			return;
 	}
-	// TODO: Add condition for global room update
-	world().globalRoom().updateObjects();
+	if (!g_engine->player().isOptionsMenuOpen() &&
+		g_engine->player().currentRoom() != &g_engine->world().inventory())
+		world().globalRoom().updateObjects();
 	if (g_engine->player().currentRoom() == this)
 		updateObjects();
 	if (g_engine->player().currentRoom() == this) {
@@ -166,8 +183,10 @@ bool Room::updateInput() {
 	// A complicated network condition can prevent interaction at this point
 	if (player.isOptionsMenuOpen() || !player.isGameLoaded())
 		canInteract = true;
-	if (canInteract)
+	if (canInteract) {
 		updateInteraction();
+		player.updateCursor();
+	}
 
 	// TODO: Add main menu and opening inventory handling
 	return player.currentRoom() == this;
@@ -176,9 +195,12 @@ bool Room::updateInput() {
 void Room::updateInteraction() {
 	auto &player = g_engine->player();
 	auto &input = g_engine->input();
-	// TODO: Add interaction with change character button / opening inventory
+	// TODO: Add interaction with change character button
+
+	if (updateOpeningInventory())
+		return;
 
-	if (player.activeCharacter()->room() != this) {
+	if (player.activeCharacter()->room() != this) { // TODO: Remove active character hack
 		player.activeCharacter()->room() = this;
 	}
 
@@ -196,7 +218,6 @@ void Room::updateInteraction() {
 		if (input.wasAnyMousePressed())
 			player.pressedObject() = player.selectedObject();
 	}
-	player.updateCursor();
 }
 
 void Room::updateRoomBounds() {
@@ -272,6 +293,51 @@ ShapeObject *Room::getSelectedObject(ShapeObject *best) const {
 	return best;
 }
 
+void Room::startClosingInventory() {
+	_isOpeningInventory = false;
+	_isClosingInventory = true;
+	_timeForInventory = g_system->getMillis();
+}
+
+void Room::updateClosingInventory() {
+	static constexpr uint32 kDuration = 300;
+	static constexpr float kSpeed = -10 / 3.0f / 1000.0f;
+
+	uint32 deltaTime = g_system->getMillis() - _timeForInventory;
+	if (!_isClosingInventory || deltaTime >= kDuration)
+		_isClosingInventory = false;
+	else
+		g_engine->world().inventory().drawAsOverlay((int32)(g_system->getHeight() * (deltaTime * kSpeed)));
+}
+
+bool Room::updateOpeningInventory() {
+	static constexpr float kSpeed = 10 / 3.0f / 1000.0f;
+	if (g_engine->player().isOptionsMenuOpen() || !g_engine->player().isGameLoaded())
+		return false;
+
+	if (_isOpeningInventory) {
+		uint32 deltaTime = g_system->getMillis() - _timeForInventory;
+		if (deltaTime >= 1000) {
+			_isOpeningInventory = false;
+			g_engine->world().inventory().open();
+		}
+		else {
+			deltaTime = MIN<uint32>(300, deltaTime);
+			g_engine->world().inventory().drawAsOverlay((int32)(g_system->getHeight() * (deltaTime * kSpeed - 1)));
+		}
+		return true;
+	}
+	else if (openInventoryTriggerBounds().contains(g_engine->input().mousePos2D())) {
+		_isClosingInventory = false;
+		_isOpeningInventory = true;
+		_timeForInventory = g_system->getMillis();
+		g_engine->player().activeCharacter()->stopWalking();
+		g_engine->world().inventory().updateItemsByActiveCharacter();
+		return true;
+	}
+	return false;
+}
+
 OptionsMenu::OptionsMenu(World *world, ReadStream &stream)
 	: Room(world, stream, true) {
 }
@@ -292,6 +358,63 @@ Inventory::~Inventory() {
 	// No need to delete items, they are room objects and thus deleted in Room::~Room
 }
 
+bool Inventory::updateInput() {
+	auto &player = g_engine->player();
+	auto &input = g_engine->input();
+	auto *hoveredItem = getHoveredItem();
+
+	if (!player.activeCharacter()->isBusy())
+		player.drawCursor(0);
+
+	if (hoveredItem != nullptr && !player.activeCharacter()->isBusy()) {
+		if (input.wasMouseLeftPressed() && player.heldItem() == nullptr ||
+			input.wasMouseLeftReleased() && player.heldItem() != nullptr ||
+			input.wasMouseRightReleased()) {
+			hoveredItem->trigger();
+			player.pressedObject() = nullptr;
+		}
+
+		g_engine->drawQueue().add<TextDrawRequest>(
+			g_engine->world().generalFont(),
+			g_engine->world().getLocalizedName(hoveredItem->name()),
+			input.mousePos2D() + Point(0, -50),
+			-1, true, kWhite, -kForegroundOrderCount + 1);
+	}
+
+	if (!player.activeCharacter()->isBusy() &&
+		closeInventoryTriggerBounds().contains(input.mousePos2D()))
+		close();
+
+	if (!player.activeCharacter()->isBusy() &&
+		hoveredItem == nullptr &&
+		input.wasMouseRightReleased()) {
+		player.heldItem() = nullptr;
+		return false;
+	}
+
+	return player.currentRoom() == this;
+}
+
+Item *Inventory::getHoveredItem() {
+	auto &mousePos = g_engine->input().mousePos2D();
+	for (auto item : _items) {
+		if (!item->isEnabled())
+			continue;
+		if (g_engine->player().heldItem() != nullptr &&
+			g_engine->player().heldItem()->name().equalsIgnoreCase(item->name()))
+			continue;
+
+		auto graphic = item->graphic();
+		assert(graphic != nullptr);
+		auto bounds = graphic->animation().frameBounds(0);
+		auto totalOffset = graphic->animation().totalFrameOffset(0);
+		auto delta = mousePos - graphic->center() - totalOffset;
+		if (delta.x >= 0 && delta.y >= 0 && delta.x <= bounds.width() && delta.y <= bounds.height())
+			return item;
+	}
+	return nullptr;
+}
+
 void Inventory::initItems() {
 	auto &mortadelo = world().mortadelo();
 	auto &filemon = world().filemon();
@@ -312,6 +435,36 @@ void Inventory::updateItemsByActiveCharacter() {
 		item->toggle(character->hasItem(item->name()));
 }
 
+void Inventory::drawAsOverlay(int32 scrollY) {
+	for (auto object : _objects) {
+		auto graphic = object->graphic();
+		if (graphic == nullptr)
+			continue;
+
+		int16 oldY = graphic->center().y;
+		int8 oldOrder = graphic->order();
+		graphic->center().y += scrollY;
+		graphic->order() = -kForegroundOrderCount;
+		if (object->name().equalsIgnoreCase("Background"))
+			graphic->order()++;
+		object->draw();
+		graphic->center().y = oldY;
+		graphic->order() = oldOrder;
+	}
+}
+
+void Inventory::open() {
+	g_engine->camera().backup(1);
+	g_engine->player().changeRoom(name(), true);
+	updateItemsByActiveCharacter();
+}
+
+void Inventory::close() {
+	g_engine->player().changeRoomToBeforeInventory();
+	g_engine->camera().restore(1);
+	g_engine->player().currentRoom()->startClosingInventory();
+}
+
 void Room::debugPrint(bool withObjects) const {
 	auto &console = g_engine->console();
 	console.debugPrintf("  %s\n", _name.c_str());
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index 96ff4da9a02..cdff4c62ee9 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -49,6 +49,10 @@ public:
 	inline uint8 characterAlphaPremultiplier() const { return _characterAlphaPremultiplier; }
 	inline bool fixedCameraOnEntering() const { return _fixedCameraOnEntering; }
 
+	using ObjectIterator = Common::Array<const ObjectBase *>::const_iterator;
+	inline ObjectIterator beginObjects() const { return _objects.begin(); }
+	inline ObjectIterator endObjects() const { return _objects.end(); }
+
 	void update();
 	virtual bool updateInput();
 	virtual void loadResources();
@@ -56,12 +60,15 @@ public:
 	virtual void serializeSave(Common::Serializer &serializer);
 	ObjectBase *getObjectByName(const Common::String &name) const;
 	void toggleActiveFloor();
+	void startClosingInventory();
 	void debugPrint(bool withObjects) const;
 
 protected:
 	Room(World *world, Common::ReadStream &stream, bool hasUselessByte);
 	void updateScripts();
 	void updateRoomBounds();
+	bool updateOpeningInventory();
+	void updateClosingInventory();
 	void updateInteraction();
 	void updateObjects();
 	void drawObjects();
@@ -71,13 +78,17 @@ protected:
 	World *_world;
 	Common::String _name;
 	PathFindingShape _floors[2];
-	bool _fixedCameraOnEntering;
+	bool
+		_fixedCameraOnEntering,
+		_isOpeningInventory = false,
+		_isClosingInventory = false;
 	int8
 		_musicId,
 		_activeFloorI = -1;
 	uint8
 		_characterAlphaTint,
 		_characterAlphaPremultiplier; ///< for some reason in percent instead of 0-255
+	uint32 _timeForInventory = 0;
 
 	Common::Array<ObjectBase *> _objects;
 };
@@ -106,10 +117,17 @@ public:
 	Inventory(World *world, Common::ReadStream &stream);
 	virtual ~Inventory() override;
 
+	virtual bool updateInput() override;
+
 	void initItems();
 	void updateItemsByActiveCharacter();
+	void drawAsOverlay(int32 scrollY);
+	void open();
+	void close();
 
 private:
+	Item *getHoveredItem();
+
 	Common::Array<Item *> _items;
 };
 
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index de05df98fdb..3b807b4ec70 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -102,6 +102,14 @@ int32 &Script::variable(const char *name) {
 	return _variables[index];
 }
 
+bool Script::hasProcedure(const Common::String &behavior, const Common::String &action) const {
+	return hasProcedure(behavior + '/' + action);
+}
+
+bool Script::hasProcedure(const Common::String &procedure) const {
+	return _procedures.contains(procedure);
+}
+
 struct ScriptTimerTask : public Task {
 	ScriptTimerTask(Process &process, int32 durationSec)
 		: Task(process)
diff --git a/engines/alcachofa/script.h b/engines/alcachofa/script.h
index 46102d58f52..e1b614a4999 100644
--- a/engines/alcachofa/script.h
+++ b/engines/alcachofa/script.h
@@ -168,6 +168,8 @@ public:
 		const Common::String &behavior,
 		const Common::String &action,
 		ScriptFlags flags = ScriptFlags::None);
+	bool hasProcedure(const Common::String &behavior, const Common::String &action) const;
+	bool hasProcedure(const Common::String &procedure) const;
 
 	using VariableNameIterator = Common::HashMap<Common::String, uint32>::const_iterator;
 	inline VariableNameIterator beginVariables() const { return _variableNames.begin(); }


Commit: 509ebca2ee73f1545ca3a3e198681cc85834b9df
    https://github.com/scummvm/scummvm/commit/509ebca2ee73f1545ca3a3e198681cc85834b9df
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:48+02:00

Commit Message:
ALCACHOFA: Add three more kernel tasks

for changeRoom, changeCharacterRoom and camFollow

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


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 959f57a1c83..928bd49df44 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -66,9 +66,10 @@ Common::Error AlcachofaEngine::run() {
 	_script.reset(new Script());
 	_player.reset(new Player());
 
-	_script->createProcess(MainCharacterKind::None, "Inicializar_Variables");
-
-	_player->changeRoom("MINA", true);
+	//_script->createProcess(MainCharacterKind::None, "Inicializar_Variables");
+	//_player->changeRoom("MINA", true);
+	_script->createProcess(MainCharacterKind::None, "CREDITOS_INICIALES");
+	_scheduler.run();
 
 	Common::Event e;
 	Graphics::FrameLimiter limiter(g_system, 120);
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 2e90023932c..ec6d915801a 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -329,6 +329,12 @@ Task *Character::sayText(Process &process, int32 dialogId) {
 	return new SayTextTask(process, this, dialogId);
 }
 
+void Character::resetTalking() {
+	_isTalking = false;
+	_curDialogId = -1;
+	_curTalkingObject = nullptr;
+}
+
 const char *WalkingCharacter::typeName() const { return "WalkingCharacter"; }
 
 WalkingCharacter::WalkingCharacter(Room *room, ReadStream &stream)
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index b3aba89d14c..90f5c831d3d 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -390,6 +390,7 @@ public:
 	virtual const char *typeName() const;
 
 	Task *sayText(Process &process, int32 dialogId);
+	void resetTalking();
 
 protected:
 	friend struct SayTextTask;
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index a298634ffd6..02beea7a10e 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -94,6 +94,10 @@ void Player::drawCursor(bool forceDefaultCursor) {
 
 void Player::changeRoom(const Common::String &targetRoomName, bool resetCamera) {
 	// original would be to always free all resources from globalRoom, inventory, GlobalUI
+	if (targetRoomName.equalsIgnoreCase("SALIR")) {
+		_currentRoom = nullptr;
+		return;
+	}
 
 	Room &inventory = g_engine->world().inventory();
 	bool keepResources;
@@ -171,10 +175,9 @@ void Player::triggerObject(ObjectBase *object, const char *action) {
 		return;
 	else 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 and ignore
-		;
+	//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
 	else
 		script.createProcess(activeCharacterKind(), "DefectoUsar");
 }
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 0b7771844e9..b15b3a003ac 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -119,7 +119,8 @@ Room::Room(World *world, ReadStream &stream, bool hasUselessByte)
 		_objects.push_back(readRoomObject(this, stream));
 		objectSize = stream.readUint32LE();
 	}
-	if (!_name.equalsIgnoreCase("Global"))
+	if (!_name.equalsIgnoreCase("Global") &&
+		!_name.equalsIgnoreCase("HABITACION_NEGRA"))
 		_objects.push_back(new Background(this, _name, backgroundScale));
 
 	if (!_floors[0].empty())
@@ -200,7 +201,7 @@ void Room::updateInteraction() {
 	if (updateOpeningInventory())
 		return;
 
-	if (player.activeCharacter()->room() != this) { // TODO: Remove active character hack
+	if (false && player.activeCharacter()->room() != this) { // TODO: Remove active character hack
 		player.activeCharacter()->room() = this;
 	}
 
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 3b807b4ec70..a5229ab8588 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -451,8 +451,29 @@ private:
 			warning("STUB KERNEL CALL: ChangeCharacter");
 			return TaskReturn::finish(0);
 		case ScriptKernelTask::ChangeRoom:
-			warning("STUB KERNEL CALL: ChangeRoom");
-			return TaskReturn::finish(0);
+			if (strcmpi(getStringArg(0), "SALIR") == 0) {
+				g_engine->quitGame();
+				g_engine->player().changeRoom("SALIR", true);
+			}
+			else if (strcmpi(getStringArg(0), "MENUPRINCIPALINICIO") == 0)
+				warning("STUB: change room to MenuPrincipalInicio special case");
+			else {
+				auto targetRoom = g_engine->world().getRoomByName(getStringArg(0));
+				if (targetRoom == nullptr)
+					error("Invalid room name: %s\n", getStringArg(0));
+				if (process().isActiveForPlayer()) {
+					g_engine->player().heldItem() = nullptr;
+					if (g_engine->player().currentRoom() == &g_engine->world().inventory())
+						g_engine->world().inventory().close();
+					if (targetRoom == &g_engine->world().inventory())
+						g_engine->world().inventory().open();
+					else
+						g_engine->player().changeRoom(targetRoom->name(), true);
+					// TODO: Change music on kernel change room
+				}
+				g_engine->script().createProcess(process().character(), "ENTRAR_" + targetRoom->name(), ScriptFlags::AllowMissing);
+			}
+			return TaskReturn::finish(1);
 		case ScriptKernelTask::ToggleRoomFloor:
 			if (process().character() == MainCharacterKind::None) {
 				if (g_engine->player().currentRoom() != nullptr)
@@ -530,9 +551,17 @@ private:
 			character->setPosition(target->position());
 			return TaskReturn::finish(1);
 		}
-		case ScriptKernelTask::ChangeCharacterRoom:
-			warning("STUB KERNEL CALL: ChangeCharacterRoom");
-			return TaskReturn::finish(0);
+		case ScriptKernelTask::ChangeCharacterRoom: {
+			auto *character = dynamic_cast<Character *>(g_engine->world().globalRoom().getObjectByName(getStringArg(0)));
+			if (character == nullptr)
+				error("Invalid character name: %s", getStringArg(0));
+			auto *targetRoom = g_engine->world().getRoomByName(getStringArg(1));
+			if (targetRoom == nullptr)
+				error("Invalid room name: %s", getStringArg(1));
+			character->resetTalking();
+			character->room() = targetRoom;
+			return TaskReturn::finish(1);
+		}
 		case ScriptKernelTask::LerpCharacterLodBias:
 			warning("STUB KERNEL CALL: LerpCharacterLodBias");
 			return TaskReturn::finish(0);
@@ -594,8 +623,10 @@ private:
 		case ScriptKernelTask::WaitCamStopping:
 			return TaskReturn::waitFor(g_engine->camera().waitToStop(process()));
 		case ScriptKernelTask::CamFollow:
-			warning("STUB KERNEL CALL: CamFollow");
-			return TaskReturn::finish(0);
+			g_engine->camera().setFollow(
+				&g_engine->world().getMainCharacterByKind((MainCharacterKind)getNumberArg(0)),
+				getNumberArg(1) != 0);
+			return TaskReturn::finish(1);
 		case ScriptKernelTask::CamShake:
 			warning("STUB KERNEL CALL: CamShake");
 			return TaskReturn::finish(0);


Commit: 7f2398324200fad980ad2149b6bf2581df522f34
    https://github.com/scummvm/scummvm/commit/7f2398324200fad980ad2149b6bf2581df522f34
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:48+02:00

Commit Message:
ALCACHOFA: Fix closing game during character processes

Changed paths:
    engines/alcachofa/scheduler.cpp


diff --git a/engines/alcachofa/scheduler.cpp b/engines/alcachofa/scheduler.cpp
index 61a7e1b783a..8456a520649 100644
--- a/engines/alcachofa/scheduler.cpp
+++ b/engines/alcachofa/scheduler.cpp
@@ -126,12 +126,12 @@ void Process::debugPrint() {
 
 static void killProcessesForIn(MainCharacterKind characterKind, Array<Process *> &processes, uint firstIndex) {
 	assert(firstIndex <= processes.size());
-	uint count = processes.size() - firstIndex;
-	for (uint i = 0; i < count; i++) {
+	for (uint i = 0; i < processes.size() - firstIndex; i++) {
 		Process **process = &processes[processes.size() - 1 - i];
 		if ((*process)->character() == characterKind || characterKind == MainCharacterKind::None) {
 			delete *process;
 			processes.erase(process);
+			i--; // underflow is fine here
 		}
 	}
 }


Commit: f18ced5d41ecddd886ac8ce3d5cad5b59bb601da
    https://github.com/scummvm/scummvm/commit/f18ced5d41ecddd886ac8ce3d5cad5b59bb601da
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:48+02:00

Commit Message:
ALCACHOFA: Add PlayVideo kernel call

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/alcachofa.h
    engines/alcachofa/graphics-opengl.cpp
    engines/alcachofa/graphics.h
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 928bd49df44..99730b2602f 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -31,6 +31,7 @@
 #include "engines/util.h"
 #include "graphics/paletteman.h"
 #include "graphics/framelimiter.h"
+#include "video/mpegps_decoder.h"
 
 #include "rooms.h"
 #include "script.h"
@@ -99,6 +100,42 @@ Common::Error AlcachofaEngine::run() {
 	return Common::kNoError;
 }
 
+void AlcachofaEngine::playVideo(int32 videoId) {	
+	Video::MPEGPSDecoder decoder;
+	if (!decoder.loadFile(Common::Path(Common::String::format("Data/DATA%02d.BIN", videoId + 1))))
+		error("Could not find video %d", videoId);
+	auto texture = _renderer->createTexture(decoder.getWidth(), decoder.getHeight(), false);
+	decoder.start();
+
+	Common::Event e;
+	while (!decoder.endOfVideo() && !shouldQuit()) {
+		if (decoder.needsUpdate())
+		{
+			auto surface = decoder.decodeNextFrame();
+			if (surface)
+				texture->update(*surface);
+			_renderer->begin();
+			_renderer->setBlendMode(BlendMode::Alpha);
+			_renderer->setLodBias(0.0f);
+			_renderer->setTexture(texture.get());
+			_renderer->quad({}, { (float)g_system->getWidth(), (float)g_system->getHeight() });
+			_renderer->end();
+		}
+
+		_input.nextFrame();
+		while (g_system->getEventManager()->pollEvent(e)) {
+			if (_input.handleEvent(e))
+				continue;
+		}
+		if (_input.wasAnyMouseReleased())
+			break;
+
+		g_system->updateScreen();
+		g_system->delayMillis(decoder.getTimeToNextFrame() / 2);
+	}
+	decoder.stop();
+}
+
 Common::Error AlcachofaEngine::syncGame(Common::Serializer &s) {
 	// The Serializer has methods isLoading() and isSaving()
 	// if you need to specific steps; for example setting
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index 2d643c1c8de..a345a0b130b 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -72,6 +72,8 @@ public:
 	inline Scheduler &scheduler() { return _scheduler; }
 	inline Console &console() { return *_console; }	
 
+	void playVideo(int32 videoId);
+
 	uint32 getFeatures() const;
 
 	/**
diff --git a/engines/alcachofa/graphics-opengl.cpp b/engines/alcachofa/graphics-opengl.cpp
index 823f7f5f868..f373b1980e7 100644
--- a/engines/alcachofa/graphics-opengl.cpp
+++ b/engines/alcachofa/graphics-opengl.cpp
@@ -80,7 +80,7 @@ public:
 			GL_CALL(glDeleteTextures(1, &_handle));
 	}
 
-	virtual void update(const ManagedSurface &surface) {
+	virtual void update(const Surface &surface) {
 		OpenGLFormat format = getOpenGLFormatOf(surface.format);
 		assert(surface.w == size().x && surface.h == size().y);
 		assert(format.isValid());
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 24119792664..b65dcfdd53c 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -62,7 +62,8 @@ public:
 	ITexture(Common::Point size);
 	virtual ~ITexture() = default;
 
-	virtual void update(const Graphics::ManagedSurface &surface) = 0;
+	virtual void update(const Graphics::Surface &surface) = 0;
+	inline void update(const Graphics::ManagedSurface &surface) { update(surface.rawSurface()); }
 
 	inline const Common::Point &size() const { return _size; }
 
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index a5229ab8588..575fd325b45 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -414,7 +414,7 @@ private:
 		switch (task) {
 		// sound/video
 		case ScriptKernelTask::PlayVideo:
-			warning("STUB KERNEL CALL: PlayVideo");
+			g_engine->playVideo(getNumberArg(0));
 			return TaskReturn::finish(0);
 		case ScriptKernelTask::PlaySound:
 			warning("STUB KERNEL CALL: PlaySound");


Commit: 8ad4f2e58b3e5f54b9c2328e0d043c99e2e85932
    https://github.com/scummvm/scummvm/commit/8ad4f2e58b3e5f54b9c2328e0d043c99e2e85932
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:48+02:00

Commit Message:
ALCACHOFA: Add permanent fade state

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


diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 2bde10577bb..1d1d830b70a 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -755,23 +755,29 @@ struct FadeTask : public Task {
 	FadeTask(Process &process, FadeType fadeType,
 		float from, float to,
 		uint32 duration, EasingType easingType,
-		int8 order)
+		int8 order,
+		PermanentFadeAction permanentFadeAction)
 		: Task(process)
 		, _fadeType(fadeType)
 		, _from(from)
 		, _to(to)
 		, _duration(duration)
 		, _easingType(easingType)
-		, _order(order) {}
+		, _order(order)
+		, _permanentFadeAction(permanentFadeAction){}
 
 	virtual TaskReturn run() override {
 		TASK_BEGIN;
+		if (_permanentFadeAction == PermanentFadeAction::UnsetFaded)
+			g_engine->player().setPermanentFade(false);
 		_startTime = g_system->getMillis();
 		while (g_system->getMillis() - _startTime < _duration) {
 			draw((g_system->getMillis() - _startTime) / (float)_duration);
 			TASK_YIELD;
 		}
 		draw(1.0f); // so that during a loading lag the screen is completly black/white
+		if (_permanentFadeAction == PermanentFadeAction::SetFaded)
+			g_engine->player().setPermanentFade(true);
 		TASK_END;
 	}
 
@@ -792,17 +798,19 @@ private:
 	uint32 _startTime = 0, _duration;
 	EasingType _easingType;
 	int8 _order;
+	PermanentFadeAction _permanentFadeAction;
 };
 
 Task *fade(Process &process, FadeType fadeType,
 	float from, float to,
 	int32 duration, EasingType easingType,
-	int8 order) {
+	int8 order,
+	PermanentFadeAction permanentFadeAction) {
 	if (duration <= 0)
 		return new DelayTask(process, 0);
 	if (!process.isActiveForPlayer())
 		return new DelayTask(process, (uint32)duration);
-	return new FadeTask(process, fadeType, from, to, duration, easingType, order);
+	return new FadeTask(process, fadeType, from, to, duration, easingType, order, permanentFadeAction);
 }
 
 DrawQueue::DrawQueue(IRenderer *renderer)
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index b65dcfdd53c..ff8379cfdfb 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -381,6 +381,12 @@ enum class FadeType {
 	// TODO: Add CrossFade fade type
 };
 
+enum class PermanentFadeAction {
+	Nothing,
+	SetFaded,
+	UnsetFaded
+};
+
 class FadeDrawRequest : public IDrawRequest {
 public:
 	FadeDrawRequest(FadeType type, float value, int8 order);
@@ -395,7 +401,8 @@ private:
 Task *fade(Process &process, FadeType fadeType,
 	float from, float to,
 	int32 duration, EasingType easingType,
-	int8 order);
+	int8 order,
+	PermanentFadeAction permanentFadeAction = PermanentFadeAction::Nothing);
 
 class BumpAllocator {
 public:
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index 02beea7a10e..050e548cc6e 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -44,6 +44,11 @@ void Player::postUpdate() {
 		_pressedObject = nullptr;
 }
 
+void Player::drawScreenStates() {
+	if (_isPermanentFaded && !_isOptionsMenuOpen)
+		g_engine->drawQueue().add<FadeDrawRequest>(FadeType::ToBlack, 1.0f, -9);
+}
+
 void Player::updateCursor() {
 	if (_isOptionsMenuOpen || !_isGameLoaded)
 		_cursorFrameI = 0;
@@ -93,6 +98,8 @@ void Player::drawCursor(bool forceDefaultCursor) {
 }
 
 void Player::changeRoom(const Common::String &targetRoomName, bool resetCamera) {
+	debug("Change room to %s", targetRoomName.c_str());
+
 	// original would be to always free all resources from globalRoom, inventory, GlobalUI
 	if (targetRoomName.equalsIgnoreCase("SALIR")) {
 		_currentRoom = nullptr;
@@ -243,4 +250,8 @@ void Player::triggerDoor(const Door *door) {
 	g_engine->scheduler().createProcess<DoorTask>(activeCharacterKind(), door, move(lock));
 }
 
+void Player::setPermanentFade(bool isFaded) {
+	_isPermanentFaded = isFaded;
+}
+
 }
diff --git a/engines/alcachofa/player.h b/engines/alcachofa/player.h
index e8170185b78..3d65dce9947 100644
--- a/engines/alcachofa/player.h
+++ b/engines/alcachofa/player.h
@@ -48,12 +48,14 @@ public:
 
 	void preUpdate();
 	void postUpdate();
+	void drawScreenStates(); // black borders and/or permanent fade
 	void updateCursor();
 	void drawCursor(bool forceDefaultCursor = false);
 	void changeRoom(const Common::String &targetRoomName, bool resetCamera);
 	void changeRoomToBeforeInventory();
 	void triggerObject(ObjectBase *object, const char *action);
 	void triggerDoor(const Door *door);
+	void setPermanentFade(bool isFaded);
 
 private:
 	Common::ScopedPtr<Animation> _cursorAnimation;
@@ -68,7 +70,8 @@ private:
 	bool
 		_isOptionsMenuOpen = false,
 		_isGameLoaded = true,
-		_didLoadGlobalRooms = false;
+		_didLoadGlobalRooms = false,
+		_isPermanentFaded = false;
 };
 
 }
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index b15b3a003ac..4b64996246a 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -159,6 +159,7 @@ void Room::update() {
 		drawObjects();
 		world().globalRoom().drawObjects();
 		// TODO: Draw black borders
+		g_engine->player().drawScreenStates();
 		g_engine->drawQueue().draw();
 		drawDebug();
 		world().globalRoom().drawDebug();
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 575fd325b45..15701db2aa6 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -695,16 +695,20 @@ private:
 				getNumberArg(2), (EasingType)getNumberArg(4), getNumberArg(3)));
 		case ScriptKernelTask::FadeIn:
 			return TaskReturn::waitFor(fade(process(), FadeType::ToBlack,
-				1.0f, 0.0f, getNumberArg(0), EasingType::Out, -5));
+				1.0f, 0.0f, getNumberArg(0), EasingType::Out, -5,
+				PermanentFadeAction::UnsetFaded));
 		case ScriptKernelTask::FadeOut:
 			return TaskReturn::waitFor(fade(process(), FadeType::ToBlack,
-				0.0f, 1.0f, getNumberArg(0), EasingType::Out, -5));
+				0.0f, 1.0f, getNumberArg(0), EasingType::Out, -5,
+				PermanentFadeAction::SetFaded));
 		case ScriptKernelTask::FadeIn2:
 			return TaskReturn::waitFor(fade(process(), FadeType::ToBlack,
-				0.0f, 1.0f, getNumberArg(0), (EasingType)getNumberArg(1), -5));
+				0.0f, 1.0f, getNumberArg(0), (EasingType)getNumberArg(1), -5,
+				PermanentFadeAction::UnsetFaded));
 		case ScriptKernelTask::FadeOut2:
 			return TaskReturn::waitFor(fade(process(), FadeType::ToBlack,
-				1.0f, 0.0f, getNumberArg(0), (EasingType)getNumberArg(1), -5));
+				1.0f, 0.0f, getNumberArg(0), (EasingType)getNumberArg(1), -5,
+				PermanentFadeAction::SetFaded));
 
 		// Unused and useless
 		case ScriptKernelTask::SetActiveTextureSet:


Commit: 30419251dc9eb98ae33b383fc24b7a387be1b76c
    https://github.com/scummvm/scummvm/commit/30419251dc9eb98ae33b383fc24b7a387be1b76c
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:48+02:00

Commit Message:
ALCACHOFA: Ignore additional missing animation

Changed paths:
    engines/alcachofa/graphics.cpp


diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 1d1d830b70a..0469ed8aefc 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -183,7 +183,8 @@ ManagedSurface *AnimationBase::readImage(SeekableReadStream &stream) const {
 
 void AnimationBase::loadMissingAnimation() {
 	// only allow missing animations we know are faulty in the original game
-	if (!_fileName.equalsIgnoreCase("ANIMACION.AN0"))
+	if (!_fileName.equalsIgnoreCase("ANIMACION.AN0") &&
+		!_fileName.equalsIgnoreCase("DESPACHO_SUPER2_OL_SOMBRAS2.AN0"))
 		error("Could not open animation %s", _fileName.c_str());
 
 	// otherwise setup a functioning but empty animation


Commit: 37aabca132feea4526ee6037e206c89554417290
    https://github.com/scummvm/scummvm/commit/37aabca132feea4526ee6037e206c89554417290
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:48+02:00

Commit Message:
ALCACHOFA: Fix incompatibilities after rebase

Changed paths:
    engines/alcachofa/detection.cpp
    engines/alcachofa/detection.h
    engines/alcachofa/graphics.cpp
    engines/alcachofa/metaengine.cpp
    engines/alcachofa/metaengine.h
    engines/alcachofa/module.mk


diff --git a/engines/alcachofa/detection.cpp b/engines/alcachofa/detection.cpp
index f743c0465da..0af2d47309a 100644
--- a/engines/alcachofa/detection.cpp
+++ b/engines/alcachofa/detection.cpp
@@ -38,8 +38,8 @@ const DebugChannelDef AlcachofaMetaEngineDetection::debugFlagList[] = {
 	DEBUG_CHANNEL_END
 };
 
-AlcachofaMetaEngineDetection::AlcachofaMetaEngineDetection() : AdvancedMetaEngineDetection(Alcachofa::gameDescriptions,
-	sizeof(ADGameDescription), Alcachofa::alcachofaGames) {
+AlcachofaMetaEngineDetection::AlcachofaMetaEngineDetection() : AdvancedMetaEngineDetection<ADGameDescription>(
+	Alcachofa::gameDescriptions, Alcachofa::alcachofaGames) {
 	_flags |= kADFlagMatchFullPaths;
 }
 
diff --git a/engines/alcachofa/detection.h b/engines/alcachofa/detection.h
index 62298ab9b14..5012d77e2a6 100644
--- a/engines/alcachofa/detection.h
+++ b/engines/alcachofa/detection.h
@@ -42,7 +42,7 @@ extern const ADGameDescription gameDescriptions[];
 
 } // End of namespace Alcachofa
 
-class AlcachofaMetaEngineDetection : public AdvancedMetaEngineDetection {
+class AlcachofaMetaEngineDetection : public AdvancedMetaEngineDetection<ADGameDescription> {
 	static const DebugChannelDef debugFlagList[];
 
 public:
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 0469ed8aefc..9bac7851553 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -177,8 +177,10 @@ ManagedSurface *AnimationBase::readImage(SeekableReadStream &stream) const {
 	if (source->w == 2 && source->h == 1)
 		return nullptr;
 
-	auto target = source->convertTo(BlendBlit::getSupportedPixelFormat(), decoder.getPalette(), decoder.getPaletteColorCount());	
-	return new ManagedSurface(target);
+	auto target = new ManagedSurface();
+	target->setPalette(decoder.getPalette(), 0, decoder.getPaletteColorCount());
+	target->convertFrom(*source, BlendBlit::getSupportedPixelFormat());
+	return target;
 }
 
 void AnimationBase::loadMissingAnimation() {
diff --git a/engines/alcachofa/metaengine.cpp b/engines/alcachofa/metaengine.cpp
index b00bbba2d43..8b45c140cf0 100644
--- a/engines/alcachofa/metaengine.cpp
+++ b/engines/alcachofa/metaengine.cpp
@@ -22,7 +22,6 @@
 #include "common/translation.h"
 
 #include "alcachofa/metaengine.h"
-#include "alcachofa/detection.h"
 #include "alcachofa/alcachofa.h"
 
 namespace Alcachofa {
diff --git a/engines/alcachofa/metaengine.h b/engines/alcachofa/metaengine.h
index e31ae83271a..e50800e10ca 100644
--- a/engines/alcachofa/metaengine.h
+++ b/engines/alcachofa/metaengine.h
@@ -24,7 +24,7 @@
 
 #include "engines/advancedDetector.h"
 
-class AlcachofaMetaEngine : public AdvancedMetaEngine {
+class AlcachofaMetaEngine : public AdvancedMetaEngine<ADGameDescription> {
 public:
 	const char *getName() const override;
 
diff --git a/engines/alcachofa/module.mk b/engines/alcachofa/module.mk
index f32728feb12..bca3bc181a8 100644
--- a/engines/alcachofa/module.mk
+++ b/engines/alcachofa/module.mk
@@ -2,8 +2,23 @@ MODULE := engines/alcachofa
 
 MODULE_OBJS = \
 	alcachofa.o \
+	camera.cpp \
+	common.cpp \
 	console.o \
-	metaengine.o
+	game-objects.cpp \
+	general-objects.cpp \
+	graphics.cpp \
+	graphics-opengl.cpp \
+	Input.cpp \
+	metaengine.o \
+	player.cpp \
+	rooms.cpp \
+	scheduler.cpp \
+	script.cpp \
+	shape.cpp \
+	sounds.cpp \
+	ui-objects.cpp \
+
 
 # This module can be built as a plugin
 ifeq ($(ENABLE_ALCACHOFA), DYNAMIC_PLUGIN)


Commit: d9450568a0ab9e1e2852ee7deb61afbd32b25668
    https://github.com/scummvm/scummvm/commit/d9450568a0ab9e1e2852ee7deb61afbd32b25668
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:48+02:00

Commit Message:
ALCACHOFA: Add ClosestFloorPoint debug mode

Changed paths:
  A engines/alcachofa/debug.h
    engines/alcachofa/Input.cpp
    engines/alcachofa/Input.h
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/alcachofa.h
    engines/alcachofa/console.cpp
    engines/alcachofa/console.h
    engines/alcachofa/graphics.h
    engines/alcachofa/rooms.cpp


diff --git a/engines/alcachofa/Input.cpp b/engines/alcachofa/Input.cpp
index 86c9181b86f..fb22ea6f3e4 100644
--- a/engines/alcachofa/Input.cpp
+++ b/engines/alcachofa/Input.cpp
@@ -34,6 +34,9 @@ void Input::nextFrame() {
 }
 
 bool Input::handleEvent(const Common::Event &event) {
+	if (_debugInput != nullptr)
+		return _debugInput->handleEvent(event);
+
 	switch (event.type) {
 	case EVENT_LBUTTONDOWN:
 		_wasMouseLeftPressed = true;
@@ -62,4 +65,15 @@ bool Input::handleEvent(const Common::Event &event) {
 	}
 }
 
+void Input::toggleDebugInput(bool debugMode) {
+	if (!debugMode) {
+		_debugInput.reset();
+		return;
+	}
+	if (_debugInput == nullptr)
+		_debugInput.reset(new Input());
+	nextFrame(); // resets frame-specific flags
+	_isMouseLeftDown = _isMouseRightDown = false;
+}
+
 }
diff --git a/engines/alcachofa/Input.h b/engines/alcachofa/Input.h
index 248a40a62c2..9f1f6a5e31c 100644
--- a/engines/alcachofa/Input.h
+++ b/engines/alcachofa/Input.h
@@ -23,6 +23,7 @@
 #define INPUT_H
 
 #include "common/events.h"
+#include "common/ptr.h"
 
 namespace Alcachofa {
 
@@ -39,9 +40,11 @@ public:
 	inline bool isAnyMouseDown() const { return _isMouseLeftDown || _isMouseRightDown; }
 	inline const Common::Point &mousePos2D() const { return _mousePos2D; }
 	inline const Common::Point &mousePos3D() const { return _mousePos3D; }
+	const Input &debugInput() const { scumm_assert(_debugInput != nullptr); return *_debugInput; }
 
 	void nextFrame();
 	bool handleEvent(const Common::Event &event);
+	void toggleDebugInput(bool debugMode); ///< Toggles input debug mode which blocks any input not retrieved with debugInput
 
 private:
 	bool
@@ -54,6 +57,7 @@ private:
 	Common::Point
 		_mousePos2D,
 		_mousePos3D;
+	Common::ScopedPtr<Input> _debugInput;
 };
 
 }
diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 99730b2602f..880b6448758 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -35,6 +35,7 @@
 
 #include "rooms.h"
 #include "script.h"
+#include "debug.h"
 
 using namespace Math;
 
@@ -67,9 +68,9 @@ Common::Error AlcachofaEngine::run() {
 	_script.reset(new Script());
 	_player.reset(new Player());
 
-	//_script->createProcess(MainCharacterKind::None, "Inicializar_Variables");
-	//_player->changeRoom("MINA", true);
-	_script->createProcess(MainCharacterKind::None, "CREDITOS_INICIALES");
+	_script->createProcess(MainCharacterKind::None, "Inicializar_Variables");
+	_player->changeRoom("MINA", true);
+	//_script->createProcess(MainCharacterKind::None, "CREDITOS_INICIALES");
 	_scheduler.run();
 
 	Common::Event e;
@@ -88,7 +89,8 @@ Common::Error AlcachofaEngine::run() {
 		_player->preUpdate();
 		_player->currentRoom()->update();
 		_player->postUpdate();
-
+		if (_debugHandler != nullptr)
+			_debugHandler->update();
 		_renderer->end();
 
 		// Delay for a bit. All events loops should have a delay
@@ -136,6 +138,16 @@ void AlcachofaEngine::playVideo(int32 videoId) {
 	decoder.stop();
 }
 
+void AlcachofaEngine::setDebugMode(DebugMode mode, int32 param)
+{
+	switch (mode)
+	{
+	case DebugMode::ClosestFloorPoint: _debugHandler.reset(new ClosestFloorPointDebugHandler(param)); break;
+	default: _debugHandler.reset(nullptr);
+	}
+	_input.toggleDebugInput(isDebugModeActive());
+}
+
 Common::Error AlcachofaEngine::syncGame(Common::Serializer &s) {
 	// The Serializer has methods isLoading() and isSaving()
 	// if you need to specific steps; for example setting
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index a345a0b130b..0b48f5c01a4 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -44,6 +44,7 @@
 
 namespace Alcachofa {
 
+class IDebugHandler;
 class IRenderer;
 class DrawQueue;
 class World;
@@ -70,9 +71,11 @@ public:
 	inline World &world() { return *_world; }
 	inline Script &script() { return *_script; }
 	inline Scheduler &scheduler() { return _scheduler; }
-	inline Console &console() { return *_console; }	
+	inline Console &console() { return *_console; }
+	inline bool isDebugModeActive() const { return _debugHandler != nullptr; }
 
 	void playVideo(int32 videoId);
+	void setDebugMode(DebugMode debugMode, int32 param);
 
 	uint32 getFeatures() const;
 
@@ -119,6 +122,7 @@ public:
 
 private:
 	Console *_console = new Console();
+	Common::ScopedPtr<IDebugHandler> _debugHandler;
 	Common::ScopedPtr<IRenderer> _renderer;
 	Common::ScopedPtr<DrawQueue> _drawQueue;
 	Common::ScopedPtr<World> _world;
diff --git a/engines/alcachofa/console.cpp b/engines/alcachofa/console.cpp
index 81206ab9779..571325b7749 100644
--- a/engines/alcachofa/console.cpp
+++ b/engines/alcachofa/console.cpp
@@ -41,11 +41,22 @@ Console::Console() : GUI::Debugger() {
 	registerCmd("disableDebugDraw", WRAP_METHOD(Console, cmdDisableDebugDraw));
 	registerCmd("pickup", WRAP_METHOD(Console, cmdItem));
 	registerCmd("drop", WRAP_METHOD(Console, cmdItem));
+	registerCmd("debugMode", WRAP_METHOD(Console, cmdDebugMode));
 }
 
 Console::~Console() {
 }
 
+bool Console::isAnyDebugDrawingOn() const
+{
+	return
+		g_engine->isDebugModeActive() ||
+		_showInteractables ||
+		_showCharacters ||
+		_showFloor ||
+		_showFloorColor;
+}
+
 bool Console::cmdVar(int argc, const char **args) {
 	auto &script = g_engine->script();
 	if (argc < 2 || argc > 3)
@@ -193,4 +204,31 @@ bool Console::cmdItem(int argc, const char **args) {
 	return true;
 }
 
+bool Console::cmdDebugMode(int argc, const char **args)
+{
+	if (argc < 2 || argc > 3) {
+		debugPrintf("usage: debugMode <mode> [<param>]\n");
+		debugPrintf("modes:\n");
+		debugPrintf("  0 - None, disables debug mode\n");
+		debugPrintf("  1 - Closest floor point, param limits to polygon\n");
+		return true;
+	}
+
+	int32 param = -1;
+	if (argc > 2)
+	{
+		char *end = nullptr;
+		param = (int32)strtol(args[2], &end, 10);
+		if (end == nullptr || *end != '\0')
+		{
+			debugPrintf("Debug mode parameter can only be integers");
+			return true;
+		}
+	}
+
+	auto mode = (DebugMode)strtol(args[1], nullptr, 10);
+	g_engine->setDebugMode(mode, param);
+	return true;
+}
+
 } // End of namespace Alcachofa
diff --git a/engines/alcachofa/console.h b/engines/alcachofa/console.h
index b59755b16f3..ccf045de5fa 100644
--- a/engines/alcachofa/console.h
+++ b/engines/alcachofa/console.h
@@ -27,6 +27,11 @@
 
 namespace Alcachofa {
 
+enum class DebugMode {
+	None,
+	ClosestFloorPoint
+};
+
 class Console : public GUI::Debugger {
 public:
 	Console();
@@ -36,14 +41,7 @@ public:
 	inline bool showCharacters() const { return _showCharacters; }
 	inline bool showFloor() const { return _showFloor; }
 	inline bool showFloorColor() const { return _showFloorColor; }
-
-	inline bool isAnyDebugDrawingOn() const {
-		return
-			_showInteractables ||
-			_showCharacters ||
-			_showFloor ||
-			_showFloorColor;
-	}
+	bool isAnyDebugDrawingOn() const;
 
 private:
 	bool cmdVar(int argc, const char **args);
@@ -53,6 +51,7 @@ private:
 	bool cmdChangeRoom(int argc, const char **args);
 	bool cmdDisableDebugDraw(int argc, const char **args);
 	bool cmdItem(int argc, const char **args);
+	bool cmdDebugMode(int argc, const char **args);
 
 	bool _showInteractables = true;
 	bool _showCharacters = true;
diff --git a/engines/alcachofa/debug.h b/engines/alcachofa/debug.h
new file mode 100644
index 00000000000..a768fcbbd2b
--- /dev/null
+++ b/engines/alcachofa/debug.h
@@ -0,0 +1,64 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef DEBUG_H
+#define DEBUG_H
+
+#include "alcachofa.h"
+
+using namespace Common;
+
+namespace Alcachofa {
+
+class IDebugHandler {
+public:
+	virtual ~IDebugHandler() = default;
+
+	virtual void update() = 0;
+};
+
+class ClosestFloorPointDebugHandler final : public IDebugHandler {
+	int32 _polygonI;
+public:
+	ClosestFloorPointDebugHandler(int32 polygonI) : _polygonI(polygonI) {}
+
+	virtual void update() final
+	{
+		auto mousePos2D = g_engine->input().debugInput().mousePos2D();
+		auto mousePos3D = g_engine->input().debugInput().mousePos3D();
+		auto* floor = g_engine->player().currentRoom()->activeFloor();
+		Point target3D;
+
+		if (_polygonI < 0 || (uint)_polygonI >= floor->polygonCount())
+			target3D = floor->getClosestPoint(mousePos3D);
+		else
+			target3D = floor->at((uint)_polygonI)._points[0];
+		auto target2Dv = g_engine->camera().transform3Dto2D(
+			{ (float)target3D.x, (float)target3D.y, kBaseScale });
+
+		auto renderer = dynamic_cast<IDebugRenderer *>(&g_engine->renderer());
+		renderer->debugPolyline(mousePos2D, { (int16)target2Dv.x(), (int16)target2Dv.y() });
+	}
+};
+
+}
+
+#endif // DEBUG_H
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index ff8379cfdfb..4cb29f0fdcd 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -108,6 +108,12 @@ public:
 		const Shape &shape,
 		Color color = kDebugRed
 	);
+
+	inline void debugPolyline(Common::Point a, Common::Point b, Color color = kDebugRed)
+	{
+		Math::Vector2d points[] = { { (float)a.x, (float)a.y }, { (float)b.x, (float)b.y } };
+		debugPolygon({ points, 2 }, color);
+	}
 };
 
 enum class AnimationFolder {
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 4b64996246a..f6c0e1b429e 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -141,19 +141,23 @@ ObjectBase *Room::getObjectByName(const Common::String &name) const {
 }
 
 void Room::update() {
-	updateScripts();
+	if (!g_engine->isDebugModeActive())
+	{
+		updateScripts();
 
-	if (g_engine->player().currentRoom() == this) {
-		updateRoomBounds();
-		updateClosingInventory();
-		if (!updateInput())
-			return;
+		if (g_engine->player().currentRoom() == this) {
+			updateRoomBounds();
+			updateClosingInventory();
+			if (!updateInput())
+				return;
+		}
+		if (!g_engine->player().isOptionsMenuOpen() &&
+			g_engine->player().currentRoom() != &g_engine->world().inventory())
+			world().globalRoom().updateObjects();
+		if (g_engine->player().currentRoom() == this)
+			updateObjects();
 	}
-	if (!g_engine->player().isOptionsMenuOpen() &&
-		g_engine->player().currentRoom() != &g_engine->world().inventory())
-		world().globalRoom().updateObjects();
-	if (g_engine->player().currentRoom() == this)
-		updateObjects();
+
 	if (g_engine->player().currentRoom() == this) {
 		g_engine->camera().update();
 		drawObjects();


Commit: 43c17c33d02bf243d09c721c97395dd17b5c068e
    https://github.com/scummvm/scummvm/commit/43c17c33d02bf243d09c721c97395dd17b5c068e
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:49+02:00

Commit Message:
ALCACHOFA: Improve Polygon::closestPointTo

Changed paths:
    engines/alcachofa/debug.h
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/shape.cpp
    engines/alcachofa/shape.h


diff --git a/engines/alcachofa/debug.h b/engines/alcachofa/debug.h
index a768fcbbd2b..f01659ad5a1 100644
--- a/engines/alcachofa/debug.h
+++ b/engines/alcachofa/debug.h
@@ -48,9 +48,9 @@ public:
 		Point target3D;
 
 		if (_polygonI < 0 || (uint)_polygonI >= floor->polygonCount())
-			target3D = floor->getClosestPoint(mousePos3D);
+			target3D = floor->closestPointTo(mousePos3D);
 		else
-			target3D = floor->at((uint)_polygonI)._points[0];
+			target3D = floor->at((uint)_polygonI).closestPointTo(mousePos3D);
 		auto target2Dv = g_engine->camera().transform3Dto2D(
 			{ (float)target3D.x, (float)target3D.y, kBaseScale });
 
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index ec6d915801a..ac8cab2d6d6 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -358,9 +358,9 @@ void WalkingCharacter::update() {
 	auto activeFloor = room()->activeFloor();
 	if (activeFloor != nullptr) {
 		if (activeFloor->polygonContaining(_sourcePos) < 0)
-			_sourcePos = _currentPos = activeFloor->getClosestPoint(_sourcePos);
+			_sourcePos = _currentPos = activeFloor->closestPointTo(_sourcePos);
 		if (activeFloor->polygonContaining(_currentPos) < 0)
-			_currentPos = activeFloor->getClosestPoint(_currentPos);
+			_currentPos = activeFloor->closestPointTo(_currentPos);
 	}
 
 	if (!_isWalking) {
diff --git a/engines/alcachofa/shape.cpp b/engines/alcachofa/shape.cpp
index a136888af8b..89e51be1eff 100644
--- a/engines/alcachofa/shape.cpp
+++ b/engines/alcachofa/shape.cpp
@@ -88,6 +88,51 @@ EdgeDistances Polygon::edgeDistances(uint startPointI, const Point &query) const
 	return distances;
 }
 
+static Point wiggleOnToLine(Point a, Point b, Point q)
+{
+	// due to rounding errors contains(bestPoint) might be false for on-edge closest points, let's fix that
+	// maybe there is a more mathematical solution to this, but it suffices for now
+	if (sideOfLine(a, b, q) >= 0) return q;
+	if (sideOfLine(a, b, q + Point(+1, 0)) >= 0) return q + Point(+1, 0);
+	if (sideOfLine(a, b, q + Point(-1, 0)) >= 0) return q + Point(-1, 0);
+	if (sideOfLine(a, b, q + Point(0, +1)) >= 0) return q + Point(0, +1);
+	if (sideOfLine(a, b, q + Point(0, -1)) >= 0) return q + Point(0, -1);
+	assert(false && "More than two pixels means some more serious math error occured");
+}
+
+Point Polygon::closestPointTo(const Common::Point& query, float &distanceSqr) const
+{
+	assert(_points.size() > 0);
+	Common::Point bestPoint = {};
+	distanceSqr = std::numeric_limits<float>::infinity();
+	for (uint i = 0; i < _points.size(); i++)
+	{
+		auto edgeDists = edgeDistances(i, query);
+		if (edgeDists._onEdge < 0.0f)
+		{
+			float pointDistSqr = as2D(query - _points[i]).getSquareMagnitude();
+			if (pointDistSqr < distanceSqr)
+			{
+				bestPoint = _points[i];
+				distanceSqr = pointDistSqr;
+			}
+		}
+		if (edgeDists._onEdge >= 0.0f && edgeDists._onEdge <= edgeDists._edgeLength)
+		{
+			float edgeDistSqr = powf(edgeDists._toEdge , 2.0f);
+			if (edgeDistSqr < distanceSqr)
+			{
+				distanceSqr = edgeDistSqr;
+				uint j = (i + 1) % _points.size();
+				bestPoint = _points[i] + (_points[j] - _points[i]) * (edgeDists._onEdge / edgeDists._edgeLength);
+				bestPoint = wiggleOnToLine(_points[i], _points[j], bestPoint);
+			}
+		}		
+	}
+	assert(contains(bestPoint));
+	return bestPoint;
+}
+
 static float depthAtForLine(const Point &a, const Point &b, const Point &q, int8 depthA, int8 depthB) {
 	return (sqrtf(a.sqrDist(q)) / a.sqrDist(b) * depthB + depthA) * 0.01f;
 }
@@ -251,6 +296,25 @@ bool Shape::contains(const Point &query) const {
 	return polygonContaining(query) >= 0;
 }
 
+Point Shape::closestPointTo(const Point &query, int32 &polygonI) const
+{
+	assert(_polygons.size() > 0);
+	float bestDistanceSqr = std::numeric_limits<float>::infinity();
+	Point bestPoint = {};
+	for (uint i = 0; i < _polygons.size(); i++)
+	{
+		float curDistanceSqr = std::numeric_limits<float>::infinity();
+		Point curPoint = at(i).closestPointTo(query, curDistanceSqr);
+		if (curDistanceSqr < bestDistanceSqr)
+		{
+			bestDistanceSqr = curDistanceSqr;
+			bestPoint = curPoint;
+			polygonI = (int32)i;
+		}
+	}
+	return bestPoint;
+}
+
 void Shape::setAsRectangle(const Rect &rect) {
 	_polygons.resize(1);
 	_polygons[0] = { 0, 4 };
@@ -441,8 +505,7 @@ bool PathFindingShape::findPath(const Point &from, const Point &to_, Stack<Point
 		return false;
 	int32 toContaining = polygonContaining(to);
 	if (toContaining < 0) {
-		to = getClosestPoint(to);
-		toContaining = polygonContaining(to);
+		to = closestPointTo(to, toContaining);
 		assert(toContaining >= 0);
 	}
 	//if (canGoStraightThrough(from, to, fromContaining, toContaining)) {
@@ -523,24 +586,6 @@ void PathFindingShape::floydWarshallPath(
 	path.push(_linkPoints[fromLink]);
 }
 
-Point PathFindingShape::getClosestPoint(const Point &query) const {
-	// TODO: Improve this function, it does not seem correct
-
-	assert(!_points.empty());
-	Point bestPoint;
-	uint bestDistance = UINT_MAX;
-	for (auto p : _points) {
-		uint curDistance = query.sqrDist(p);
-		if (curDistance < bestDistance) {
-			bestDistance = curDistance;
-			bestPoint = p;
-		}
-	}
-
-	assert(bestDistance < UINT_MAX);
-	return bestPoint;
-}
-
 FloorColorShape::FloorColorShape() {}
 
 FloorColorShape::FloorColorShape(ReadStream &stream) {
diff --git a/engines/alcachofa/shape.h b/engines/alcachofa/shape.h
index 0452cd45d8f..17d7aa4b16f 100644
--- a/engines/alcachofa/shape.h
+++ b/engines/alcachofa/shape.h
@@ -46,6 +46,11 @@ struct Polygon {
 
 	bool contains(const Common::Point &query) const;
 	EdgeDistances edgeDistances(uint startPointI, const Common::Point &query) const;
+	Common::Point closestPointTo(const Common::Point &query, float& distanceSqr) const;
+	inline Common::Point closestPointTo(const Common::Point &query) const {
+		float dummy;
+		return closestPointTo(query, dummy);
+	}
 };
 
 struct PathFindingPolygon : Polygon {
@@ -122,6 +127,11 @@ public:
 	Polygon at(uint index) const;
 	int32 polygonContaining(const Common::Point &query) const;
 	bool contains(const Common::Point &query) const;
+	Common::Point closestPointTo(const Common::Point &query, int32 &polygonI) const;
+	inline Common::Point closestPointTo(const Common::Point &query) const {
+		int32 dummy;
+		return closestPointTo(query, dummy);
+	}
 	void setAsRectangle(const Common::Rect &rect);
 
 protected:
@@ -160,7 +170,6 @@ public:
 		const Common::Point &from,
 		const Common::Point &to,
 		Common::Stack<Common::Point> &path) const;
-	Common::Point getClosestPoint(const Common::Point &query) const;
 
 private:
 	void setupLinks();


Commit: c52ad2570337b3899c0f047a800edf937d7b94f2
    https://github.com/scummvm/scummvm/commit/c52ad2570337b3899c0f047a800edf937d7b94f2
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:49+02:00

Commit Message:
ALCACHOFA: Add FloorIntersections debug handler

Changed paths:
    engines/alcachofa/Input.cpp
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/camera.cpp
    engines/alcachofa/camera.h
    engines/alcachofa/console.cpp
    engines/alcachofa/console.h
    engines/alcachofa/debug.h
    engines/alcachofa/shape.cpp
    engines/alcachofa/shape.h


diff --git a/engines/alcachofa/Input.cpp b/engines/alcachofa/Input.cpp
index fb22ea6f3e4..92c97325fbf 100644
--- a/engines/alcachofa/Input.cpp
+++ b/engines/alcachofa/Input.cpp
@@ -27,6 +27,9 @@ using namespace Common;
 namespace Alcachofa {
 
 void Input::nextFrame() {
+	if (_debugInput != nullptr)
+		return _debugInput->nextFrame();
+
 	_wasMouseLeftPressed = false;
 	_wasMouseRightPressed = false;
 	_wasMouseLeftReleased = false;
@@ -70,10 +73,10 @@ void Input::toggleDebugInput(bool debugMode) {
 		_debugInput.reset();
 		return;
 	}
-	if (_debugInput == nullptr)
-		_debugInput.reset(new Input());
 	nextFrame(); // resets frame-specific flags
 	_isMouseLeftDown = _isMouseRightDown = false;
+	if (_debugInput == nullptr)
+		_debugInput.reset(new Input());
 }
 
 }
diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 880b6448758..427a9cf1d8d 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -142,7 +142,12 @@ void AlcachofaEngine::setDebugMode(DebugMode mode, int32 param)
 {
 	switch (mode)
 	{
-	case DebugMode::ClosestFloorPoint: _debugHandler.reset(new ClosestFloorPointDebugHandler(param)); break;
+	case DebugMode::ClosestFloorPoint:
+		_debugHandler.reset(new ClosestFloorPointDebugHandler(param));
+		break;
+	case DebugMode::FloorIntersections:
+		_debugHandler.reset(new FloorIntersectionsDebugHandler(param));
+		break;
 	default: _debugHandler.reset(nullptr);
 	}
 	_input.toggleDebugInput(isDebugModeActive());
diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index 282932f65f9..3be4477e706 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -156,6 +156,11 @@ Vector3d Camera::transform3Dto2D(Vector3d v3d) const {
 		_cur._scale * kBaseScale / vh.z());
 }
 
+Point Camera::transform3Dto2D(Point p3d) const {
+	auto v2d = transform3Dto2D({ (float)p3d.x, (float)p3d.y, kBaseScale });
+	return { (int16)v2d.x(), (int16)v2d.y() };
+}
+
 void Camera::update() {
 	// original would be some smoothing of delta times, let's not.
 	uint32 now = g_system->getMillis();
diff --git a/engines/alcachofa/camera.h b/engines/alcachofa/camera.h
index cdca9db6852..7d22cbef8c6 100644
--- a/engines/alcachofa/camera.h
+++ b/engines/alcachofa/camera.h
@@ -43,6 +43,7 @@ public:
 	void update();
 	Math::Vector3d transform2Dto3D(Math::Vector3d v) const;
 	Math::Vector3d transform3Dto2D(Math::Vector3d v) const;
+	Common::Point transform3Dto2D(Common::Point p) const;
 	void resetRotationAndScale();
 	void setRoomBounds(Common::Point bgSize, int16 bgScale);
 	void setFollow(WalkingCharacter *target, bool catchUp = false);
diff --git a/engines/alcachofa/console.cpp b/engines/alcachofa/console.cpp
index 571325b7749..74afc720cf9 100644
--- a/engines/alcachofa/console.cpp
+++ b/engines/alcachofa/console.cpp
@@ -211,6 +211,7 @@ bool Console::cmdDebugMode(int argc, const char **args)
 		debugPrintf("modes:\n");
 		debugPrintf("  0 - None, disables debug mode\n");
 		debugPrintf("  1 - Closest floor point, param limits to polygon\n");
+		debugPrintf("  2 - Floor edge intersections, param limits to polygon\n");
 		return true;
 	}
 
diff --git a/engines/alcachofa/console.h b/engines/alcachofa/console.h
index ccf045de5fa..5939f41a1c6 100644
--- a/engines/alcachofa/console.h
+++ b/engines/alcachofa/console.h
@@ -29,7 +29,8 @@ namespace Alcachofa {
 
 enum class DebugMode {
 	None,
-	ClosestFloorPoint
+	ClosestFloorPoint,
+	FloorIntersections
 };
 
 class Console : public GUI::Debugger {
diff --git a/engines/alcachofa/debug.h b/engines/alcachofa/debug.h
index f01659ad5a1..de9cba0997a 100644
--- a/engines/alcachofa/debug.h
+++ b/engines/alcachofa/debug.h
@@ -44,18 +44,72 @@ public:
 	{
 		auto mousePos2D = g_engine->input().debugInput().mousePos2D();
 		auto mousePos3D = g_engine->input().debugInput().mousePos3D();
-		auto* floor = g_engine->player().currentRoom()->activeFloor();
-		Point target3D;
+		auto floor = g_engine->player().currentRoom()->activeFloor();
+		auto renderer = dynamic_cast<IDebugRenderer *>(&g_engine->renderer());
+		if (floor == nullptr || renderer == nullptr)
+			return;
 
+		Point target3D;
 		if (_polygonI < 0 || (uint)_polygonI >= floor->polygonCount())
 			target3D = floor->closestPointTo(mousePos3D);
 		else
 			target3D = floor->at((uint)_polygonI).closestPointTo(mousePos3D);
-		auto target2Dv = g_engine->camera().transform3Dto2D(
-			{ (float)target3D.x, (float)target3D.y, kBaseScale });
+		renderer->debugPolyline(mousePos2D, g_engine->camera().transform3Dto2D(target3D));
+	}
+};
 
+class FloorIntersectionsDebugHandler final : public IDebugHandler {
+	int32 _polygonI;
+	Point _fromPos3D;
+public:
+	FloorIntersectionsDebugHandler(int32 polygonI) : _polygonI(polygonI) {}
+
+	virtual void update() final
+	{
+		auto floor = g_engine->player().currentRoom()->activeFloor();
 		auto renderer = dynamic_cast<IDebugRenderer *>(&g_engine->renderer());
-		renderer->debugPolyline(mousePos2D, { (int16)target2Dv.x(), (int16)target2Dv.y() });
+		if (floor == nullptr || renderer == nullptr)
+			return;
+
+		if (g_engine->input().debugInput().wasMouseLeftPressed())
+			_fromPos3D = g_engine->input().debugInput().mousePos3D();
+		renderer->debugPolyline(
+			g_engine->camera().transform3Dto2D(_fromPos3D),
+			g_engine->input().debugInput().mousePos2D(),
+			kDebugRed);
+
+		if (_polygonI >= 0 && (uint)_polygonI < floor->polygonCount())
+			drawIntersectionsFor(floor->at((uint)_polygonI), renderer);
+		else
+		{
+			for (uint i = 0; i < floor->polygonCount(); i++)
+				drawIntersectionsFor(floor->at(i), renderer);
+		}
+	}
+
+private:
+	static constexpr float kMarkerLength = 16;
+
+	void drawIntersectionsFor(const Polygon& polygon, IDebugRenderer* renderer)
+	{
+		auto &camera = g_engine->camera();
+		auto mousePos2D = g_engine->input().debugInput().mousePos2D();
+		auto mousePos3D = g_engine->input().debugInput().mousePos3D();
+		for (uint i = 0; i < polygon._points.size(); i++)
+		{
+			if (!polygon.intersectsEdge(i, _fromPos3D, mousePos3D))
+				continue;
+			auto a = camera.transform3Dto2D(polygon._points[i]);
+			auto b = camera.transform3Dto2D(polygon._points[(i + 1) % polygon._points.size()]);
+			auto mid = (a + b) / 2;
+			auto length = sqrtf(a.sqrDist(b));
+			auto normal = a - b;
+			normal = { normal.y, (int16)-normal.x};
+			auto inner = mid + normal * (kMarkerLength / length);
+
+			renderer->debugPolyline(a, b, kDebugGreen);
+			renderer->debugPolyline(mid, inner, kDebugGreen);
+		}
 	}
 };
 
diff --git a/engines/alcachofa/shape.cpp b/engines/alcachofa/shape.cpp
index 89e51be1eff..b4251aed8e8 100644
--- a/engines/alcachofa/shape.cpp
+++ b/engines/alcachofa/shape.cpp
@@ -71,6 +71,12 @@ bool Polygon::contains(const Point &query) const {
 	}
 }
 
+bool Polygon::intersectsEdge(uint startPointI, Point a, Point b) const {
+	assert(startPointI < _points.size());
+	uint endPointI = (startPointI + 1) % _points.size();
+	return segmentsIntersect(_points[startPointI], _points[endPointI], a, b);
+}
+
 EdgeDistances Polygon::edgeDistances(uint startPointI, const Point &query) const {
 	assert(startPointI < _points.size());
 	uint endPointI = startPointI + 1 == _points.size() ? 0 : startPointI + 1;
@@ -98,6 +104,7 @@ static Point wiggleOnToLine(Point a, Point b, Point q)
 	if (sideOfLine(a, b, q + Point(0, +1)) >= 0) return q + Point(0, +1);
 	if (sideOfLine(a, b, q + Point(0, -1)) >= 0) return q + Point(0, -1);
 	assert(false && "More than two pixels means some more serious math error occured");
+	return q;
 }
 
 Point Polygon::closestPointTo(const Common::Point& query, float &distanceSqr) const
@@ -529,8 +536,7 @@ bool PathFindingShape::canGoStraightThrough(
 			if (_targetQuads[fullI] < 0 || _targetQuads[fullI] == lastContainingI)
 				continue;
 
-			uint j = i + 1 == toContaining._points.size() ? 0 : i + 1;
-			if (segmentsIntersect(from, to, toContaining._points[i], toContaining._points[j])) {
+			if (toContaining.intersectsEdge(i, from, to)) {
 				foundPortal = true;
 				lastContainingI = toContainingI;
 				toContainingI = _targetQuads[fullI];
diff --git a/engines/alcachofa/shape.h b/engines/alcachofa/shape.h
index 17d7aa4b16f..ac5d339ca5f 100644
--- a/engines/alcachofa/shape.h
+++ b/engines/alcachofa/shape.h
@@ -45,6 +45,7 @@ struct Polygon {
 	Common::Span<const Common::Point> _points;
 
 	bool contains(const Common::Point &query) const;
+	bool intersectsEdge(uint startPointI, Common::Point a, Common::Point b) const;
 	EdgeDistances edgeDistances(uint startPointI, const Common::Point &query) const;
 	Common::Point closestPointTo(const Common::Point &query, float& distanceSqr) const;
 	inline Common::Point closestPointTo(const Common::Point &query) const {


Commit: fc39a1344c790085ea973ced1b5a4ccdcf6ea81d
    https://github.com/scummvm/scummvm/commit/fc39a1344c790085ea973ced1b5a4ccdcf6ea81d
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:49+02:00

Commit Message:
ALCACHOFA: Fix floor polygon connections

Changed paths:
    engines/alcachofa/common.h
    engines/alcachofa/console.cpp
    engines/alcachofa/console.h
    engines/alcachofa/graphics-opengl.cpp
    engines/alcachofa/rooms.cpp
    engines/alcachofa/shape.cpp
    engines/alcachofa/shape.h


diff --git a/engines/alcachofa/common.h b/engines/alcachofa/common.h
index 783d6dbde86..559823d471b 100644
--- a/engines/alcachofa/common.h
+++ b/engines/alcachofa/common.h
@@ -76,6 +76,7 @@ static constexpr const Color kClear = { 0, 0, 0, 0 };
 static constexpr const Color kDebugRed = { 250, 0, 0, 70 };
 static constexpr const Color kDebugGreen = { 0, 255, 0, 85 };
 static constexpr const Color kDebugBlue = { 0, 0, 255, 110 };
+static constexpr const Color kDebugLightBlue = { 80, 80, 255, 190 };
 
 /**
  * @brief This *fake* semaphore does not work in multi-threaded scenarios
diff --git a/engines/alcachofa/console.cpp b/engines/alcachofa/console.cpp
index 74afc720cf9..1783cf9ecee 100644
--- a/engines/alcachofa/console.cpp
+++ b/engines/alcachofa/console.cpp
@@ -30,7 +30,8 @@ namespace Alcachofa {
 Console::Console() : GUI::Debugger() {
 	registerVar("showInteractables", &_showInteractables);
 	registerVar("showCharacters", &_showCharacters);
-	registerVar("showFloor", &_showFloor);
+	registerVar("showFloorShape", &_showFloor);
+	registerVar("showFloorEdges", &_showFloorEdges);
 	registerVar("showFloorColor", &_showFloorColor);
 
 	registerCmd("var", WRAP_METHOD(Console, cmdVar));
@@ -54,6 +55,7 @@ bool Console::isAnyDebugDrawingOn() const
 		_showInteractables ||
 		_showCharacters ||
 		_showFloor ||
+		_showFloorEdges ||
 		_showFloorColor;
 }
 
diff --git a/engines/alcachofa/console.h b/engines/alcachofa/console.h
index 5939f41a1c6..b21e06085a5 100644
--- a/engines/alcachofa/console.h
+++ b/engines/alcachofa/console.h
@@ -41,6 +41,7 @@ public:
 	inline bool showInteractables() const { return _showInteractables; }
 	inline bool showCharacters() const { return _showCharacters; }
 	inline bool showFloor() const { return _showFloor; }
+	inline bool showFloorEdges() const { return _showFloorEdges; }
 	inline bool showFloorColor() const { return _showFloorColor; }
 	bool isAnyDebugDrawingOn() const;
 
@@ -57,6 +58,7 @@ private:
 	bool _showInteractables = true;
 	bool _showCharacters = true;
 	bool _showFloor = true;
+	bool _showFloorEdges = false;
 	bool _showFloorColor = false;
 };
 
diff --git a/engines/alcachofa/graphics-opengl.cpp b/engines/alcachofa/graphics-opengl.cpp
index f373b1980e7..dcde6af735e 100644
--- a/engines/alcachofa/graphics-opengl.cpp
+++ b/engines/alcachofa/graphics-opengl.cpp
@@ -248,7 +248,6 @@ public:
 
 		float colors[] = { color.r / 255.0f, color.g / 255.0f, color.b / 255.0f, color.a / 255.0f };
 
-		//GL_CALL(glColor4f(1.0f, 1.0f, 1.0f, 1.0f));
 		GL_CALL(glColor4f(color.r / 255.0f, color.g / 255.0f, color.b / 255.0f, color.a / 255.0f));
 		GL_CALL(glVertexPointer(2, GL_FLOAT, 0, positions));
 		if (_currentTexture != nullptr)
@@ -256,7 +255,7 @@ public:
 		GL_CALL(glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, colors));
 		GL_CALL(glDrawArrays(GL_QUADS, 0, 4));
 
-#if DEBUG
+#if _DEBUG
 		// make sure we crash instead of someone using our stack arrays
 		GL_CALL(glVertexPointer(2, GL_FLOAT, sizeof(Vector2d), nullptr));
 		GL_CALL(glTexCoordPointer(2, GL_FLOAT, sizeof(Vector2d), nullptr));
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index f6c0e1b429e..c5c6c55a20e 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -105,6 +105,8 @@ Room::Room(World *world, ReadStream &stream, bool hasUselessByte)
 	_musicId = stream.readSByte();
 	_characterAlphaTint = stream.readByte();
 	auto backgroundScale = stream.readSint16LE();
+	if (_name == "MINA")
+		backgroundScale += 0;
 	_floors[0] = PathFindingShape(stream);
 	_floors[1] = PathFindingShape(stream);
 	_fixedCameraOnEntering = readBool(stream);
@@ -259,8 +261,28 @@ void Room::drawDebug() {
 	}
 	if (_activeFloorI < 0)
 		return;
-	if (_activeFloorI >= 0 && g_engine->console().showFloor())
-		renderer->debugShape(_floors[_activeFloorI], kDebugBlue);
+	auto &floor = _floors[_activeFloorI];
+	if (g_engine->console().showFloor())
+		renderer->debugShape(floor, kDebugBlue);
+
+	if (g_engine->console().showFloorEdges()) {
+		auto &camera = g_engine->camera();
+		for (uint polygonI = 0; polygonI < floor.polygonCount(); polygonI++)
+		{
+			auto polygon = floor.at(polygonI);
+			for (uint pointI = 0; pointI < polygon._points.size(); pointI++)
+			{
+				int32 targetI = floor.edgeTarget(polygonI, pointI);
+				if (targetI < 0)
+					continue;
+				Point a = camera.transform3Dto2D(polygon._points[pointI]);
+				Point b = camera.transform3Dto2D(polygon._points[(pointI + 1) % polygon._points.size()]);
+				Point source = (a + b) / 2;
+				Point target = camera.transform3Dto2D(floor.at((uint)targetI).midPoint());
+				renderer->debugPolyline(source, target, kDebugLightBlue);
+			}
+		}
+	}
 }
 
 void Room::loadResources() {
diff --git a/engines/alcachofa/shape.cpp b/engines/alcachofa/shape.cpp
index b4251aed8e8..5ff9a856ebd 100644
--- a/engines/alcachofa/shape.cpp
+++ b/engines/alcachofa/shape.cpp
@@ -31,11 +31,8 @@ static int sideOfLine(const Point &a, const Point &b, const Point &q) {
 }
 
 static bool segmentsIntersect(const Point &a1, const Point &b1, const Point &a2, const Point &b2) {
-	// as there are a number of special cases to consider, this method is a direct translation
-	// of the original engine
-	// TODO: It is still bad and does sometimes not work correctly. Check this. keep in mind
-	// it *could* also be a case of incorrect floor segments being passed into in the first place.
-
+	// as there are a number of special cases to consider,
+	// this method is a direct translation of the original engine
 	const auto sideOfLine = [](const Point &a, const Point &b, const Point q) {
 		return Alcachofa::sideOfLine(a, b, q) > 0;
 	};
@@ -140,6 +137,14 @@ Point Polygon::closestPointTo(const Common::Point& query, float &distanceSqr) co
 	return bestPoint;
 }
 
+Point Polygon::midPoint() const {
+	assert(_points.size() > 0);
+	Common::Point sum = {};
+	for (uint i = 0; i < _points.size(); i++)
+		sum += _points[i];
+	return sum / (int16)_points.size();
+}
+
 static float depthAtForLine(const Point &a, const Point &b, const Point &q, int8 depthA, int8 depthB) {
 	return (sqrtf(a.sqrDist(q)) / a.sqrDist(b) * depthB + depthA) * 0.01f;
 }
@@ -383,12 +388,14 @@ PathFindingShape::LinkPolygonIndices::LinkPolygonIndices() {
 }
 
 static Pair<int32, int32> orderPoints(const Polygon &polygon, int32 point1, int32 point2) {
-	if ((point1 > point2 && point1 + 1 != (int32)polygon._points.size()) ||
-		point2 + 1 == (int32)polygon._points.size()) {
+	if (point1 > point2) {
 		int32 tmp = point1;
 		point1 = point2;
 		point2 = tmp;
 	}
+	const int32 maxPointI = polygon._points.size() - 1;
+	if (point1 == 0 && point2 == maxPointI)
+		return { maxPointI, 0 };
 	return { point1, point2 };
 }
 
@@ -409,7 +416,7 @@ void PathFindingShape::setupLinks() {
 			if (sharedPointCount > 1) {
 				auto outerPoints = orderPoints(outer, sharedPoints[0].first, sharedPoints[1].first);
 				auto innerPoints = orderPoints(inner, sharedPoints[0].second, sharedPoints[1].second);
-				setupLinkEdge(outer, inner, outerPoints.first, outerPoints.second, innerPoints.first);
+				setupLinkEdge(outer, inner, outerPoints, innerPoints);
 				setupLinkPoint(outer, inner, sharedPoints[1]);
 			}
 		}
@@ -432,14 +439,15 @@ void PathFindingShape::setupLinkPoint(
 void PathFindingShape::setupLinkEdge(
 	const PathFindingPolygon &outer,
 	const PathFindingPolygon &inner,
-	int32 outerP1, int32 outerP2, int32 innerP) {
-	_targetQuads[outer._index * kPointsPerPolygon + outerP1] = inner._index;
-	_targetQuads[inner._index * kPointsPerPolygon + innerP] = outer._index;
-	auto &outerLink = _linkIndices[outer._index]._points[outerP1];
-	auto &innerLink = _linkIndices[inner._index]._points[innerP];
+	PathFindingShape::LinkIndex outerP,
+	PathFindingShape::LinkIndex innerP) {
+	_targetQuads[outer._index * kPointsPerPolygon + outerP.first] = inner._index;
+	_targetQuads[inner._index * kPointsPerPolygon + innerP.first] = outer._index;
+	auto &outerLink = _linkIndices[outer._index]._points[outerP.first];
+	auto &innerLink = _linkIndices[inner._index]._points[innerP.first];
 	if (outerLink.second < 0) {
 		outerLink.second = _linkPoints.size();
-		_linkPoints.push_back((outer._points[outerP1] + outer._points[outerP2]) / 2);
+		_linkPoints.push_back((outer._points[outerP.first] + outer._points[outerP.second]) / 2);
 	}
 	innerLink.second = outerLink.second;
 }
@@ -524,6 +532,12 @@ bool PathFindingShape::findPath(const Point &from, const Point &to_, Stack<Point
 	return true;
 }
 
+int32 PathFindingShape::edgeTarget(uint polygonI, uint pointI) const {
+	assert(polygonI < polygonCount() && pointI < kPointsPerPolygon);
+	uint fullI = polygonI * kPointsPerPolygon + pointI;
+	return _targetQuads[fullI];
+}
+
 bool PathFindingShape::canGoStraightThrough(
 	const Point &from, const Point &to,
 	int32 fromContainingI, int32 toContainingI) const {
@@ -532,14 +546,14 @@ bool PathFindingShape::canGoStraightThrough(
 		auto toContaining = at(toContainingI);
 		bool foundPortal = false;
 		for (uint i = 0; i < toContaining._points.size(); i++) {
-			uint fullI = toContainingI * kPointsPerPolygon + i;
-			if (_targetQuads[fullI] < 0 || _targetQuads[fullI] == lastContainingI)
+			int32 target = edgeTarget((uint)toContainingI, i);
+			if (target < 0 || target == lastContainingI)
 				continue;
 
 			if (toContaining.intersectsEdge(i, from, to)) {
 				foundPortal = true;
 				lastContainingI = toContainingI;
-				toContainingI = _targetQuads[fullI];
+				toContainingI = target;
 				break;
 			}
 		}
diff --git a/engines/alcachofa/shape.h b/engines/alcachofa/shape.h
index ac5d339ca5f..b42239edce4 100644
--- a/engines/alcachofa/shape.h
+++ b/engines/alcachofa/shape.h
@@ -52,6 +52,7 @@ struct Polygon {
 		float dummy;
 		return closestPointTo(query, dummy);
 	}
+	Common::Point midPoint() const;
 };
 
 struct PathFindingPolygon : Polygon {
@@ -171,8 +172,11 @@ public:
 		const Common::Point &from,
 		const Common::Point &to,
 		Common::Stack<Common::Point> &path) const;
+	int32 edgeTarget(uint polygonI, uint pointI) const;
 
 private:
+	using LinkIndex = Common::Pair<int32, int32>;
+
 	void setupLinks();
 	void setupLinkPoint(
 		const PathFindingPolygon &outer,
@@ -181,7 +185,7 @@ private:
 	void setupLinkEdge(
 		const PathFindingPolygon &outer,
 		const PathFindingPolygon &inner,
-		int32 outerP1, int32 outerP2, int32 innerP);
+		LinkIndex outerP, LinkIndex innerP);
 	void initializeFloydWarshall();
 	void calculateFloydWarshall();
 	bool canGoStraightThrough(
@@ -208,7 +212,6 @@ private:
 	 * the corresponding link point. The second point is the
 	 * index to the artifical center point
 	 */
-	using LinkIndex = Common::Pair<int32, int32>;
 	struct LinkPolygonIndices {
 		LinkPolygonIndices();
 		LinkIndex _points[kPointsPerPolygon];


Commit: 013ea9e5d8e159aa8e3118adc0df6cbdd964d73a
    https://github.com/scummvm/scummvm/commit/013ea9e5d8e159aa8e3118adc0df6cbdd964d73a
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:49+02:00

Commit Message:
ALCACHOFA: Decrease texture size of fonts

Changed paths:
    engines/alcachofa/graphics.cpp


diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 9bac7851553..e64380c1d7b 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -407,9 +407,14 @@ 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
+	// 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])
+			continue;
 		cellSize.x = MAX(cellSize.x, image->w);
 		cellSize.y = MAX(cellSize.y, image->h);
 	}
@@ -422,6 +427,8 @@ 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;
+
 		int offsetX = (i % 16) * cellSize.x + (cellSize.x - _images[i]->w) / 2;
 		int offsetY = (i / 16) * cellSize.y + (cellSize.y - _images[i]->h) / 2;
 		fullBlend(*_images[i], atlasSurface, offsetX, offsetY);


Commit: af87f81354452f6895b62c6e91a28a7b68a12441
    https://github.com/scummvm/scummvm/commit/af87f81354452f6895b62c6e91a28a7b68a12441
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:49+02:00

Commit Message:
ALCACHOFA: Add script debug tracing

Changed paths:
  A engines/alcachofa/script-debug.h
    engines/alcachofa/detection.cpp
    engines/alcachofa/detection.h
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/detection.cpp b/engines/alcachofa/detection.cpp
index 0af2d47309a..1d0c5a94ad9 100644
--- a/engines/alcachofa/detection.cpp
+++ b/engines/alcachofa/detection.cpp
@@ -31,9 +31,6 @@
 
 const DebugChannelDef AlcachofaMetaEngineDetection::debugFlagList[] = {
 	{ Alcachofa::kDebugGraphics, "Graphics", "Graphics debug level" },
-	{ Alcachofa::kDebugPath, "Path", "Pathfinding debug level" },
-	{ Alcachofa::kDebugFilePath, "FilePath", "File path debug level" },
-	{ Alcachofa::kDebugScan, "Scan", "Scan for unrecognised games" },
 	{ Alcachofa::kDebugScript, "Script", "Enable debug script dump" },
 	DEBUG_CHANNEL_END
 };
diff --git a/engines/alcachofa/detection.h b/engines/alcachofa/detection.h
index 5012d77e2a6..96a07feed09 100644
--- a/engines/alcachofa/detection.h
+++ b/engines/alcachofa/detection.h
@@ -28,9 +28,6 @@ namespace Alcachofa {
 
 enum AlcachofaDebugChannels {
 	kDebugGraphics = 1,
-	kDebugPath,
-	kDebugScan,
-	kDebugFilePath,
 	kDebugScript,
 };
 
diff --git a/engines/alcachofa/script-debug.h b/engines/alcachofa/script-debug.h
new file mode 100644
index 00000000000..1e7d9a7a7c1
--- /dev/null
+++ b/engines/alcachofa/script-debug.h
@@ -0,0 +1,130 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SCRIPT_DEBUG_H
+#define SCRIPT_DEBUG_H
+
+namespace Alcachofa {
+
+static const char* const ScriptOpNames[] = {
+	"Nop",
+	"Dup",
+	"PushAddr",
+	"PushValue",
+	"Deref",
+	"Crash5",
+	"PopN",
+	"Store",
+	"Crash8",
+	"Crash9",
+	"LoadString",
+	"LoadString2",
+	"Crash12",
+	"ScriptCall",
+	"KernelCall",
+	"JumpIfFalse",
+	"JumpIfTrue",
+	"Jump",
+	"Negate",
+	"BooleanNot",
+	"Mul",
+	"Crash21",
+	"Crash22",
+	"Add",
+	"Sub",
+	"Less",
+	"Greater",
+	"LessEquals",
+	"GreaterEquals",
+	"Equals",
+	"NotEquals",
+	"BitAnd",
+	"BitOr",
+	"Crash33",
+	"Crash34",
+	"Crash35",
+	"Crash36",
+	"Return"
+};
+
+static const char *const KernelCallNames[] = {
+	"<null>",
+	"PlayVideo",
+	"PlaySound",
+	"PlayMusic",
+	"StopMusic",
+	"WaitForMusicToEnd",
+	"ShowCenterBottomText",
+	"StopAndTurn",
+	"StopAndTurnMe",
+	"ChangeCharacter",
+	"SayText",
+	"Nop10",
+	"Go",
+	"Put",
+	"ChangeCharacterRoom",
+	"KillProcesses",
+	"LerpCharacterLodBias",
+	"On",
+	"Off",
+	"Pickup",
+	"CharacterPickup",
+	"Drop",
+	"CharacterDrop",
+	"Delay",
+	"HadNoMousePressFor",
+	"Nop24",
+	"Fork",
+	"Animate",
+	"AnimateCharacter",
+	"AnimateTalking",
+	"ChangeRoom",
+	"ToggleRoomFloor",
+	"SetDialogLineReturn",
+	"DialogMenu",
+	"ClearInventory",
+	"Nop34",
+	"FadeType0",
+	"FadeType1",
+	"LerpWorldLodBias",
+	"FadeType2",
+	"SetActiveTextureSet",
+	"SetMaxCamSpeedFactor",
+	"WaitCamStopping",
+	"CamFollow",
+	"CamShake",
+	"LerpCamXY",
+	"LerpCamZ",
+	"LerpCamScale",
+	"LerpCamToObjectWithScale",
+	"LerpCamToObjectResettingZ",
+	"LerpCamRotation",
+	"FadeIn",
+	"FadeOut",
+	"FadeIn2",
+	"FadeOut2",
+	"LerpCamXYZ",
+	"LerpCamToObjectKeepingZ"
+};
+
+}
+
+#endif // SCRIPT_DEBUG_H
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 15701db2aa6..f0281433c63 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -22,6 +22,7 @@
 #include "script.h"
 #include "rooms.h"
 #include "alcachofa.h"
+#include "script-debug.h"
 
 #include "common/file.h"
 
@@ -30,6 +31,13 @@ using namespace Math;
 
 namespace Alcachofa {
 
+enum ScriptDebugLevel {
+	SCRIPT_DEBUG_LVL_NONE = 0,
+	SCRIPT_DEBUG_LVL_TASKS = 1,
+	SCRIPT_DEBUG_LVL_KERNELCALLS = 2,
+	SCRIPT_DEBUG_LVL_INSTRUCTIONS = 3
+};
+
 ScriptInstruction::ScriptInstruction(ReadStream &stream)
 	: _op((ScriptOp)stream.readSint32LE())
 	, _arg(stream.readSint32LE()) {}
@@ -161,6 +169,7 @@ struct ScriptTask : public Task {
 		, _pc(pc)
 		, _lock(Common::move(lock)) {
 		pushInstruction(UINT_MAX);
+		debugC(SCRIPT_DEBUG_LVL_TASKS, kDebugScript, "%u: Script start at %u", process.pid(), pc);
 	}
 
 	ScriptTask(Process &process, const ScriptTask &forkParent)
@@ -172,6 +181,7 @@ struct ScriptTask : public Task {
 		for (uint i = 0; i < forkParent._stack.size(); i++)
 			_stack.push(forkParent._stack[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);
 	}
 
 	virtual TaskReturn run() override {
@@ -185,6 +195,9 @@ struct ScriptTask : public Task {
 			if (_pc >= _script._instructions.size())
 				error("Script process reached instruction out-of-bounds");
 			const auto &instruction = _script._instructions[_pc++];
+			debugC(SCRIPT_DEBUG_LVL_INSTRUCTIONS, kDebugScript, "%u: %5u %-12s %8d",
+				process().pid(), _pc - 1, ScriptOpNames[(int)instruction._op], instruction._arg);
+
 			switch (instruction._op) {
 			case ScriptOp::Nop: break;
 			case ScriptOp::Dup:
@@ -411,6 +424,9 @@ private:
 	}
 
 	TaskReturn kernelCall(ScriptKernelTask task) {
+		debugC(SCRIPT_DEBUG_LVL_KERNELCALLS, kDebugScript, "%u: %5u Kernel %-25s",
+			process().pid(), _pc - 1, KernelCallNames[(int)task]);
+
 		switch (task) {
 		// sound/video
 		case ScriptKernelTask::PlayVideo:


Commit: 84bcbbea4e7a2c0e91e8c2145ddead209a84185c
    https://github.com/scummvm/scummvm/commit/84bcbbea4e7a2c0e91e8c2145ddead209a84185c
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:49+02:00

Commit Message:
ALCACHOFA: Disable alpha premultiplication, seems to be wrong

Changed paths:
    engines/alcachofa/graphics.cpp


diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index e64380c1d7b..6d3e2b1366a 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -323,12 +323,13 @@ void Animation::prerenderFrame(int32 frameI) {
 		fullBlend(*image, _renderedSurface, offsetX, offsetY);
 	}
 
+	/* TODO: Find a situation where this is actually used, otherwise this currently just produces bugs
 	if (_premultiplyAlpha != 100) {
 		byte *itPixel = (byte*)_renderedSurface.getPixels();
 		uint componentCount = _renderedSurface.w * _renderedSurface.h * 4;
 		for (uint32 i = 0; i < componentCount; i++, itPixel++)
 			*itPixel = *itPixel * _premultiplyAlpha / 100;
-	}
+	}*/
 
 	_renderedTexture->update(_renderedSurface);
 	_renderedFrameI = frameI;


Commit: 356122bb8e732ba52647dc2998ef9331a799389b
    https://github.com/scummvm/scummvm/commit/356122bb8e732ba52647dc2998ef9331a799389b
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:49+02:00

Commit Message:
ALCACHOFA: Fix object query for some cutscenes

Changed paths:
    engines/alcachofa/rooms.cpp


diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index c5c6c55a20e..c04ed7e7c09 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -593,7 +593,7 @@ ObjectBase *World::getObjectByName(MainCharacterKind character, const Common::St
 		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() != player.activeCharacter()->room())
 		result = player.currentRoom()->getObjectByName(name);
 	if (result == nullptr)
 		result = player.activeCharacter()->room()->getObjectByName(name);


Commit: e157840815c686842d8c831ed90d44c671123997
    https://github.com/scummvm/scummvm/commit/e157840815c686842d8c831ed90d44c671123997
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:49+02:00

Commit Message:
ALCACHOFA: Fix loading OFELIA_QUIETA.AN0

Changed paths:
    engines/alcachofa/graphics-opengl.cpp


diff --git a/engines/alcachofa/graphics-opengl.cpp b/engines/alcachofa/graphics-opengl.cpp
index dcde6af735e..c3e882f222d 100644
--- a/engines/alcachofa/graphics-opengl.cpp
+++ b/engines/alcachofa/graphics-opengl.cpp
@@ -116,7 +116,7 @@ public:
 	}
 
 	virtual ScopedPtr<ITexture> createTexture(int32 w, int32 h, bool withMipmaps) override {
-		assert(w > 0 && h > 0);
+		assert(w >= 0 && h >= 0);
 		return ScopedPtr<ITexture>(new OpenGLTexture(w, h, withMipmaps));
 	}
 


Commit: c49f2cbfd665b3c515f6c4b50a5b2121fd4980a0
    https://github.com/scummvm/scummvm/commit/c49f2cbfd665b3c515f6c4b50a5b2121fd4980a0
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:49+02:00

Commit Message:
ALCACHOFA: Add two original hard-coded special cases

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 ac8cab2d6d6..0f73218cc6c 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -288,9 +288,14 @@ struct SayTextTask : public Task {
 		graphicOf(_character->_curTalkingObject, &_character->_graphicTalking)->start(true);
 		while (true) {
 			if (_soundId == kInvalidSoundID)
+			{
+				bool isMortadeloVoice =
+					_character == &g_engine->world().mortadelo() ||
+					_character->name().equalsIgnoreCase("MORTADELO_TREN"); // an original hard-coded special case
 				_soundId = g_engine->sounds().playVoice(
-					String::format(_character == &g_engine->world().mortadelo() ? "M%04d" : "%04d", _dialogId),
+					String::format(isMortadeloVoice ? "M%04d" : "%04d", _dialogId),
 					0);
+			}
 			g_engine->sounds().setAppropriateVolume(_soundId, process().character(), _character);
 			if (!g_engine->sounds().isAlive(_soundId) || g_engine->input().wasAnyMouseReleased())
 				_character->_isTalking = false;
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 6d3e2b1366a..c7cf2885dec 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -186,7 +186,8 @@ ManagedSurface *AnimationBase::readImage(SeekableReadStream &stream) const {
 void AnimationBase::loadMissingAnimation() {
 	// only allow missing animations we know are faulty in the original game
 	if (!_fileName.equalsIgnoreCase("ANIMACION.AN0") &&
-		!_fileName.equalsIgnoreCase("DESPACHO_SUPER2_OL_SOMBRAS2.AN0"))
+		!_fileName.equalsIgnoreCase("DESPACHO_SUPER2_OL_SOMBRAS2.AN0") &&
+		!_fileName.equalsIgnoreCase("PP_MORTA.AN0"))
 		error("Could not open animation %s", _fileName.c_str());
 
 	// otherwise setup a functioning but empty animation


Commit: 5fdd0de63706dd491a1f5709e229506ecf2faca3
    https://github.com/scummvm/scummvm/commit/5fdd0de63706dd491a1f5709e229506ecf2faca3
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:49+02:00

Commit Message:
ALCACHOFA: Fix loading room VIA_TREN_ATADOS_NOCHE

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


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 0f73218cc6c..ed8968aabf8 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -200,7 +200,8 @@ void Character::draw() {
 		return;
 	Graphic *activeGraphic = graphic();
 	assert(activeGraphic != nullptr);
-	g_engine->drawQueue().add<AnimationDrawRequest>(*activeGraphic, true, BlendMode::AdditiveAlpha, _lodBias);
+	if (activeGraphic->hasAnimation())
+		g_engine->drawQueue().add<AnimationDrawRequest>(*activeGraphic, true, BlendMode::AdditiveAlpha, _lodBias);
 }
 
 void Character::drawDebug() {
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 4cb29f0fdcd..796488700c6 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -258,6 +258,7 @@ public:
 	inline Color &color() { return _color; }
 	inline int32 &frameI() { return _frameI; }
 	inline uint32 lastTime() const { return _lastTime; }
+	inline bool hasAnimation() const { return _animation != nullptr; }
 	inline Animation &animation() {
 		assert(_animation != nullptr && _animation->isLoaded());
 		return *_animation;


Commit: 6c551fc9ca25641fd01f9ad38b4bdf030a64d4d7
    https://github.com/scummvm/scummvm/commit/6c551fc9ca25641fd01f9ad38b4bdf030a64d4d7
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:49+02:00

Commit Message:
ALCACHOFA: Add teleport debug handler

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


diff --git a/engines/alcachofa/Input.cpp b/engines/alcachofa/Input.cpp
index 92c97325fbf..6f7e38aa7d6 100644
--- a/engines/alcachofa/Input.cpp
+++ b/engines/alcachofa/Input.cpp
@@ -38,7 +38,12 @@ void Input::nextFrame() {
 
 bool Input::handleEvent(const Common::Event &event) {
 	if (_debugInput != nullptr)
-		return _debugInput->handleEvent(event);
+	{
+		auto result = _debugInput->handleEvent(event);
+		_mousePos2D = _debugInput->mousePos2D(); // even for debug input we want to e.g. draw a cursor
+		_mousePos3D = _debugInput->mousePos3D();
+		return result;
+	}
 
 	switch (event.type) {
 	case EVENT_LBUTTONDOWN:
diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 427a9cf1d8d..78f66ca0b8b 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -148,6 +148,9 @@ void AlcachofaEngine::setDebugMode(DebugMode mode, int32 param)
 	case DebugMode::FloorIntersections:
 		_debugHandler.reset(new FloorIntersectionsDebugHandler(param));
 		break;
+	case DebugMode::TeleportCharacter:
+		_debugHandler.reset(new TeleportCharacterDebugHandler(param));
+		break;
 	default: _debugHandler.reset(nullptr);
 	}
 	_input.toggleDebugInput(isDebugModeActive());
diff --git a/engines/alcachofa/console.cpp b/engines/alcachofa/console.cpp
index 1783cf9ecee..247035a44a4 100644
--- a/engines/alcachofa/console.cpp
+++ b/engines/alcachofa/console.cpp
@@ -43,6 +43,7 @@ Console::Console() : GUI::Debugger() {
 	registerCmd("pickup", WRAP_METHOD(Console, cmdItem));
 	registerCmd("drop", WRAP_METHOD(Console, cmdItem));
 	registerCmd("debugMode", WRAP_METHOD(Console, cmdDebugMode));
+	registerCmd("tp", WRAP_METHOD(Console, cmdTeleport));
 }
 
 Console::~Console() {
@@ -214,6 +215,7 @@ bool Console::cmdDebugMode(int argc, const char **args)
 		debugPrintf("  0 - None, disables debug mode\n");
 		debugPrintf("  1 - Closest floor point, param limits to polygon\n");
 		debugPrintf("  2 - Floor edge intersections, param limits to polygon\n");
+		debugPrintf("  3 - Teleport character to mouse click, param selects character\n");
 		return true;
 	}
 
@@ -234,4 +236,31 @@ bool Console::cmdDebugMode(int argc, const char **args)
 	return true;
 }
 
+bool Console::cmdTeleport(int argc, const char **args)
+{
+	if (argc < 1 || argc > 2)
+	{
+		debugPrintf("usagge: tp [<character>]\n");
+		debugPrintf("characters:\n");
+		debugPrintf("  0 - Both\n");
+		debugPrintf("  1 - Mortadelo\n");
+		debugPrintf("  2 - Filemon\n");
+	}
+
+	int32 param = 0;
+	if (argc > 1)
+	{
+		char *end = nullptr;
+		param = (int32)strtol(args[2], &end, 10);
+		if (end == nullptr || *end != '\0')
+		{
+			debugPrintf("Character kind can only be integer");
+			return true;
+		}
+	}
+
+	g_engine->setDebugMode(DebugMode::TeleportCharacter, param);
+	return false;
+}
+
 } // End of namespace Alcachofa
diff --git a/engines/alcachofa/console.h b/engines/alcachofa/console.h
index b21e06085a5..236e842fdc9 100644
--- a/engines/alcachofa/console.h
+++ b/engines/alcachofa/console.h
@@ -30,7 +30,8 @@ namespace Alcachofa {
 enum class DebugMode {
 	None,
 	ClosestFloorPoint,
-	FloorIntersections
+	FloorIntersections,
+	TeleportCharacter,
 };
 
 class Console : public GUI::Debugger {
@@ -54,6 +55,7 @@ private:
 	bool cmdDisableDebugDraw(int argc, const char **args);
 	bool cmdItem(int argc, const char **args);
 	bool cmdDebugMode(int argc, const char **args);
+	bool cmdTeleport(int argc, const char **args);
 
 	bool _showInteractables = true;
 	bool _showCharacters = true;
diff --git a/engines/alcachofa/debug.h b/engines/alcachofa/debug.h
index de9cba0997a..be9196c9b2f 100644
--- a/engines/alcachofa/debug.h
+++ b/engines/alcachofa/debug.h
@@ -40,7 +40,7 @@ class ClosestFloorPointDebugHandler final : public IDebugHandler {
 public:
 	ClosestFloorPointDebugHandler(int32 polygonI) : _polygonI(polygonI) {}
 
-	virtual void update() final
+	virtual void update() override
 	{
 		auto mousePos2D = g_engine->input().debugInput().mousePos2D();
 		auto mousePos3D = g_engine->input().debugInput().mousePos3D();
@@ -64,7 +64,7 @@ class FloorIntersectionsDebugHandler final : public IDebugHandler {
 public:
 	FloorIntersectionsDebugHandler(int32 polygonI) : _polygonI(polygonI) {}
 
-	virtual void update() final
+	virtual void update() override
 	{
 		auto floor = g_engine->player().currentRoom()->activeFloor();
 		auto renderer = dynamic_cast<IDebugRenderer *>(&g_engine->renderer());
@@ -113,6 +113,54 @@ private:
 	}
 };
 
+class TeleportCharacterDebugHandler final : public IDebugHandler {
+	MainCharacterKind _kind;
+public:
+	TeleportCharacterDebugHandler(int32 kindI) : _kind((MainCharacterKind)kindI) {}
+
+	virtual void update() override
+	{
+		g_engine->drawQueue().clear();
+		g_engine->player().drawCursor(true);
+		g_engine->drawQueue().draw();
+
+		auto &input = g_engine->input().debugInput();
+		if (input.wasMouseRightPressed())
+		{
+			g_engine->setDebugMode(DebugMode::None, 0);
+			return;
+		}
+
+		if (!input.wasMouseLeftPressed())
+			return;
+		auto floor = g_engine->player().currentRoom()->activeFloor();
+		if (floor == nullptr || !floor->contains(input.mousePos3D()))
+			return;
+
+		if (_kind == MainCharacterKind::Filemon)
+			teleport(g_engine->world().filemon(), input.mousePos3D());
+		else if (_kind == MainCharacterKind::Mortadelo)
+			teleport(g_engine->world().mortadelo(), input.mousePos3D());
+		else {
+			teleport(g_engine->world().filemon(), input.mousePos3D());
+			teleport(g_engine->world().mortadelo(), input.mousePos3D());
+		}
+		g_engine->setDebugMode(DebugMode::None, 0);
+	}
+
+private:
+	void teleport(MainCharacter &character, Point position)
+	{
+		auto currentRoom = g_engine->player().currentRoom();
+		if (character.room() != currentRoom)
+		{
+			character.resetTalking();
+			character.room() = currentRoom;
+		}
+		character.setPosition(position);
+	}
+};
+
 }
 
 #endif // DEBUG_H


Commit: 01233d57cf6df7d51b57cf621c7abb3812fb11bb
    https://github.com/scummvm/scummvm/commit/01233d57cf6df7d51b57cf621c7abb3812fb11bb
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:50+02:00

Commit Message:
ALCACHOFA: Add AnimateTalking kernel call

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


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index ed8968aabf8..b7f87c9dae6 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -341,6 +341,18 @@ void Character::resetTalking() {
 	_curTalkingObject = nullptr;
 }
 
+void Character::talkUsing(ObjectBase *talkObject) {
+	_curTalkingObject = talkObject;
+	if (talkObject == nullptr)
+		return;
+	auto graphic = talkObject->graphic();
+	if (graphic == nullptr)
+		error("Talk object %s does not have a graphic", talkObject->name().c_str());
+	graphic->start(true);
+	if (room() == g_engine->player().currentRoom())
+		graphic->update();
+}
+
 const char *WalkingCharacter::typeName() const { return "WalkingCharacter"; }
 
 WalkingCharacter::WalkingCharacter(Room *room, ReadStream &stream)
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 90f5c831d3d..4751c2be428 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -391,6 +391,7 @@ public:
 
 	Task *sayText(Process &process, int32 dialogId);
 	void resetTalking();
+	void talkUsing(ObjectBase *talkObject);
 
 protected:
 	friend struct SayTextTask;
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index f0281433c63..6b3adf5d77f 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -584,9 +584,21 @@ private:
 		case ScriptKernelTask::AnimateCharacter:
 			warning("STUB KERNEL CALL: AnimateCharacter");
 			return TaskReturn::finish(0);
-		case ScriptKernelTask::AnimateTalking:
-			warning("STUB KERNEL CALL: AnimateTalking");
-			return TaskReturn::finish(0);
+		case ScriptKernelTask::AnimateTalking: {
+			auto *character = dynamic_cast<Character *>(g_engine->world().getObjectByName(getStringArg(0)));
+			if (character == nullptr)
+				error("Invalid character name: %s", getStringArg(0));
+			const char *talkObjectName = getStringArg(1);
+			ObjectBase *talkObject = nullptr;
+			if (talkObjectName != nullptr && *talkObjectName)
+			{
+				talkObject = g_engine->world().getObjectByName(talkObjectName);
+				if (talkObject == nullptr)
+					error("Invalid talk object name: %s", talkObjectName);
+			}
+			character->talkUsing(talkObject);
+			return TaskReturn::finish(1);
+		}
 		case ScriptKernelTask::SayText: {
 			const char *characterName = getStringArg(0);
 			int32 dialogId = getNumberArg(1);


Commit: 75201ef02858acad30c789a37bacf15256c51cdc
    https://github.com/scummvm/scummvm/commit/75201ef02858acad30c789a37bacf15256c51cdc
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:50+02:00

Commit Message:
ALCACHOFA: Add AnimateCharacter kernel call

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


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index b7f87c9dae6..2c1d084200d 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -277,7 +277,7 @@ void Character::trigger(const char *action) {
 	g_engine->player().triggerObject(this, action);
 }
 
-struct SayTextTask : public Task {
+struct SayTextTask final : public Task {
 	SayTextTask(Process &process, Character *character, int32 dialogId)
 		: Task(process)
 		, _character(character)
@@ -353,6 +353,51 @@ void Character::talkUsing(ObjectBase *talkObject) {
 		graphic->update();
 }
 
+struct AnimateCharacterTask final : public Task {
+	AnimateCharacterTask(Process &process, Character *character, ObjectBase *animateObject)
+		: Task(process)
+		, _character(character)
+		, _animateObject(animateObject)
+		, _graphic(animateObject->graphic()) {
+		scumm_assert(_graphic != nullptr);
+	}
+
+	virtual TaskReturn run() override {
+		TASK_BEGIN;
+		while (_character->_curAnimateObject != nullptr)
+			TASK_YIELD;
+
+		_character->_curAnimateObject = _animateObject;
+		_graphic->start(false);
+		if (_character->room() == g_engine->player().currentRoom())
+			_graphic->update();
+		do
+		{
+			TASK_YIELD;
+			if (process().isActiveForPlayer() && g_engine->input().wasAnyMouseReleased())
+				_graphic->pause();
+		} while (!_graphic->isPaused());
+
+		_character->_curAnimateObject = nullptr;
+		_character->_curTalkingObject = nullptr;
+		TASK_END;
+	}
+
+	virtual void debugPrint() override {
+		g_engine->console().debugPrintf("AnimateCharacter %s, %s\n", _character->name().c_str(), _animateObject->name().c_str());
+	}
+
+private:
+	Character *_character;
+	ObjectBase *_animateObject;
+	Graphic *_graphic;
+};
+
+Task *Character::animate(Process &process, ObjectBase *animateObject) {
+	assert(animateObject != nullptr);
+	return new AnimateCharacterTask(process, this, animateObject);
+}
+
 const char *WalkingCharacter::typeName() const { return "WalkingCharacter"; }
 
 WalkingCharacter::WalkingCharacter(Room *room, ReadStream &stream)
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 796488700c6..a394604ca41 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -258,6 +258,7 @@ public:
 	inline Color &color() { return _color; }
 	inline int32 &frameI() { return _frameI; }
 	inline uint32 lastTime() const { return _lastTime; }
+	inline bool isPaused() const { return _isPaused; }
 	inline bool hasAnimation() const { return _animation != nullptr; }
 	inline Animation &animation() {
 		assert(_animation != nullptr && _animation->isLoaded());
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 4751c2be428..6111ed4f450 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -392,9 +392,11 @@ public:
 	Task *sayText(Process &process, int32 dialogId);
 	void resetTalking();
 	void talkUsing(ObjectBase *talkObject);
+	Task *animate(Process &process, ObjectBase *animateObject);
 
 protected:
 	friend struct SayTextTask;
+	friend struct AnimateCharacterTask;
 	void syncObjectAsString(Common::Serializer &serializer, ObjectBase *&object);
 	void updateTalkingAnimation();
 
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 6b3adf5d77f..bdc3ea554c6 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -581,9 +581,15 @@ private:
 		case ScriptKernelTask::LerpCharacterLodBias:
 			warning("STUB KERNEL CALL: LerpCharacterLodBias");
 			return TaskReturn::finish(0);
-		case ScriptKernelTask::AnimateCharacter:
-			warning("STUB KERNEL CALL: AnimateCharacter");
-			return TaskReturn::finish(0);
+		case ScriptKernelTask::AnimateCharacter: {
+			auto *character = dynamic_cast<Character *>(g_engine->world().getObjectByName(getStringArg(0)));
+			if (character == nullptr)
+				error("Invalid character name: %s", getStringArg(0));
+			auto *animObject = g_engine->world().getObjectByName(getStringArg(1));
+			if (animObject == nullptr)
+				error("Invalid animate object name: %s", getStringArg(1));
+			return TaskReturn::waitFor(character->animate(process(), animObject));
+		}
 		case ScriptKernelTask::AnimateTalking: {
 			auto *character = dynamic_cast<Character *>(g_engine->world().getObjectByName(getStringArg(0)));
 			if (character == nullptr)


Commit: 2bd64cd55849271b962f856f1cd2798fe5fb899b
    https://github.com/scummvm/scummvm/commit/2bd64cd55849271b962f856f1cd2798fe5fb899b
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:50+02:00

Commit Message:
ALCACHOFA: Add PlaySound kernel call

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


diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index bdc3ea554c6..a1ccdb4800c 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -432,9 +432,13 @@ private:
 		case ScriptKernelTask::PlayVideo:
 			g_engine->playVideo(getNumberArg(0));
 			return TaskReturn::finish(0);
-		case ScriptKernelTask::PlaySound:
-			warning("STUB KERNEL CALL: PlaySound");
-			return TaskReturn::finish(0);
+		case ScriptKernelTask::PlaySound: {
+			auto soundID = g_engine->sounds().playSFX(getStringArg(0));
+			g_engine->sounds().setAppropriateVolume(soundID, process().character(), nullptr);
+			return getNumberArg(1) == 0
+				? TaskReturn::waitFor(new PlaySoundTask(process(), soundID))
+				: TaskReturn::finish(1);
+		}
 		case ScriptKernelTask::PlayMusic:
 			warning("STUB KERNEL CALL: PlayMusic");
 			return TaskReturn::finish(0);
diff --git a/engines/alcachofa/sounds.cpp b/engines/alcachofa/sounds.cpp
index 57952a47b44..adeb31f336f 100644
--- a/engines/alcachofa/sounds.cpp
+++ b/engines/alcachofa/sounds.cpp
@@ -122,15 +122,23 @@ static AudioStream *openAudio(const String &fileName) {
 	error("Could not open audio file: %s", fileName.c_str());
 }
 
-SoundID Sounds::playVoice(const String &fileName, byte volume) {
+SoundID Sounds::playSoundInternal(const String &fileName, byte volume, Mixer::SoundType type) {
 	AudioStream *stream = openAudio(fileName);
 	SoundHandle handle;
-	_mixer->playStream(Mixer::kSpeechSoundType, &handle, stream, -1, volume);
+	_mixer->playStream(type, &handle, stream, -1, volume);
 	SoundID id = _nextID++;
-	_playbacks.push_back({ id, handle, Mixer::kSpeechSoundType });
+	_playbacks.push_back({ id, handle, type });
 	return id;
 }
 
+SoundID Sounds::playVoice(const String &fileName, byte volume) {
+	return playSoundInternal(fileName, volume, Mixer::kSpeechSoundType);
+}
+
+SoundID Sounds::playSFX(const String &fileName, byte volume) {
+	return playSoundInternal(fileName, volume, Mixer::kSFXSoundType);
+}
+
 void Sounds::stopVoice() {
 	for (uint i = _playbacks.size(); i > 0; i--) {
 		if (_playbacks[i - 1]._type == Mixer::kSpeechSoundType) {
@@ -152,17 +160,19 @@ void Sounds::setVolume(SoundID id, byte volume) {
 }
 
 void Sounds::setAppropriateVolume(SoundID id,
-	MainCharacterKind processCharacter,
+	MainCharacterKind processCharacterKind,
 	Character *speakingCharacter) {
 	static constexpr byte kAlmostMaxVolume = Mixer::kMaxChannelVolume * 9 / 10;
 
 	auto &player = g_engine->player();
+	auto processCharacter = processCharacterKind == MainCharacterKind::None ? nullptr
+		: &g_engine->world().getMainCharacterByKind(processCharacterKind);
 	byte newVolume;
-	if (player.activeCharacter() == nullptr || player.activeCharacter() == speakingCharacter)
+	if (processCharacter == nullptr || processCharacter == player.activeCharacter())
 		newVolume = Mixer::kMaxChannelVolume;
 	else if (speakingCharacter != nullptr && speakingCharacter->room() == player.currentRoom())
 		newVolume = kAlmostMaxVolume;
-	else if (g_engine->world().getMainCharacterByKind(processCharacter).room() == player.currentRoom())
+	else if (processCharacter->room() == player.currentRoom())
 		newVolume = kAlmostMaxVolume;
 	else
 		newVolume = 0;
@@ -177,4 +187,23 @@ void Sounds::fadeOut(SoundID id, uint32 duration) {
 	}
 }
 
+PlaySoundTask::PlaySoundTask(Process &process, SoundID soundID)
+	: Task(process)
+	, _soundID(soundID) { }
+
+TaskReturn PlaySoundTask::run() {
+	auto &sounds = g_engine->sounds();
+	if (sounds.isAlive(_soundID))
+	{
+		sounds.setAppropriateVolume(_soundID, process().character(), nullptr);
+		return TaskReturn::yield();
+	}
+	else
+		return TaskReturn::finish(1);
+}
+
+void PlaySoundTask::debugPrint() {
+	g_engine->console().debugPrintf("PlaySound %u\n", _soundID);
+}
+
 }
diff --git a/engines/alcachofa/sounds.h b/engines/alcachofa/sounds.h
index 2a2629d2da7..35eb8ccde44 100644
--- a/engines/alcachofa/sounds.h
+++ b/engines/alcachofa/sounds.h
@@ -22,7 +22,7 @@
 #ifndef SOUNDS_H
 #define SOUNDS_H
 
-#include "common.h"
+#include "scheduler.h"
 #include "audio/mixer.h"
 
 namespace Alcachofa {
@@ -38,6 +38,7 @@ public:
 
 	void update();
 	SoundID playVoice(const Common::String &fileName, byte volume = Audio::Mixer::kMaxChannelVolume);
+	SoundID playSFX(const Common::String &fileName, byte volume = Audio::Mixer::kMaxChannelVolume);
 	void stopVoice();
 	void fadeOut(SoundID id, uint32 duration);
 	bool isAlive(SoundID id);
@@ -57,12 +58,21 @@ private:
 			_fadeDuration = 0;
 	};
 	Playback *getPlaybackById(SoundID id);
+	SoundID playSoundInternal(const Common::String &fileName, byte volume, Audio::Mixer::SoundType type);
 
 	Common::Array<Playback> _playbacks;
 	Audio::Mixer *_mixer;
 	SoundID _nextID = 1;
 };
 
+struct PlaySoundTask final : public Task {
+	PlaySoundTask(Process &process, SoundID soundID);
+	virtual TaskReturn run() override;
+	virtual void debugPrint() override;
+private:
+	SoundID _soundID;
+};
+
 }
 
 #endif // SOUNDS_H


Commit: ae5c98228e21523873483f2a2c3bff4cff21c1fc
    https://github.com/scummvm/scummvm/commit/ae5c98228e21523873483f2a2c3bff4cff21c1fc
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:50+02:00

Commit Message:
ALCACHOFA: Add LerpCharacterLodBias kernel call

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


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 2c1d084200d..a1ff35aa985 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -398,6 +398,44 @@ Task *Character::animate(Process &process, ObjectBase *animateObject) {
 	return new AnimateCharacterTask(process, this, animateObject);
 }
 
+struct LerpLodBiasTask final : public Task {
+	LerpLodBiasTask(Process &process, Character *character, float targetLodBias, uint32 durationMs)
+		: Task(process)
+		, _character(character)
+		, _targetLodBias(targetLodBias)
+		, _durationMs(durationMs) { }
+
+	virtual TaskReturn run() override {
+		TASK_BEGIN;
+		_startTime = g_system->getMillis();
+		_sourceLodBias = _character->lodBias();
+		while (g_system->getMillis() - _startTime < _durationMs) {
+			_character->lodBias() = _sourceLodBias + (_targetLodBias - _sourceLodBias) *
+				((g_system->getMillis() - _startTime) / (float)_durationMs);
+			TASK_YIELD;
+		}
+		_character->lodBias() = _targetLodBias;
+		TASK_END;
+	}
+
+	virtual void debugPrint() override {
+		uint32 remaining = g_system->getMillis() - _startTime <= _durationMs
+			? _durationMs - (g_system->getMillis() - _startTime)
+			: 0;
+		g_engine->console().debugPrintf("Lerp lod bias of %s to %f with %ums remaining\n",
+			_character->name().c_str(), _targetLodBias, remaining);
+	}
+
+private:
+	Character *_character;
+	float _sourceLodBias = 0, _targetLodBias;
+	uint32 _startTime = 0, _durationMs;
+};
+
+Task *Character::lerpLodBias(Process &process, float targetLodBias, int32 durationMs) {
+	return new LerpLodBiasTask(process, this, targetLodBias, durationMs);
+}
+
 const char *WalkingCharacter::typeName() const { return "WalkingCharacter"; }
 
 WalkingCharacter::WalkingCharacter(Room *room, ReadStream &stream)
@@ -627,7 +665,7 @@ void WalkingCharacter::draw() {
 	}
 
 	assert(currentGraphic != nullptr);
-	g_engine->drawQueue().add<AnimationDrawRequest>(*currentGraphic, true, BlendMode::AdditiveAlpha);
+	g_engine->drawQueue().add<AnimationDrawRequest>(*currentGraphic, true, BlendMode::AdditiveAlpha, _lodBias);
 }
 
 void WalkingCharacter::drawDebug() {
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 6111ed4f450..262017e3a45 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -393,6 +393,8 @@ public:
 	void resetTalking();
 	void talkUsing(ObjectBase *talkObject);
 	Task *animate(Process &process, ObjectBase *animateObject);
+	Task *lerpLodBias(Process &process, float targetLodBias, int32 durationMs);
+	inline float &lodBias() { return _lodBias; }
 
 protected:
 	friend struct SayTextTask;
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index a1ccdb4800c..809692c2480 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -582,9 +582,20 @@ private:
 			character->room() = targetRoom;
 			return TaskReturn::finish(1);
 		}
-		case ScriptKernelTask::LerpCharacterLodBias:
-			warning("STUB KERNEL CALL: LerpCharacterLodBias");
-			return TaskReturn::finish(0);
+		case ScriptKernelTask::LerpCharacterLodBias: {
+			auto *character = dynamic_cast<Character *>(g_engine->world().globalRoom().getObjectByName(getStringArg(0)));
+			if (character == nullptr)
+				error("Invalid character name: %s", getStringArg(0));
+			float targetLodBias = getNumberArg(1) * 0.01f;
+			int32 durationMs = getNumberArg(2);
+			if (durationMs <= 0)
+			{
+				character->lodBias() = targetLodBias;
+				return TaskReturn::finish(1);
+			}
+			else
+				return TaskReturn::waitFor(character->lerpLodBias(process(), targetLodBias, durationMs));
+		}
 		case ScriptKernelTask::AnimateCharacter: {
 			auto *character = dynamic_cast<Character *>(g_engine->world().getObjectByName(getStringArg(0)));
 			if (character == nullptr)


Commit: 313a811a4007f2863cebd4197441beb1c5506777
    https://github.com/scummvm/scummvm/commit/313a811a4007f2863cebd4197441beb1c5506777
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:50+02:00

Commit Message:
ALCACHOFA: Add ChangeCharacter kernel call

Changed paths:
    engines/alcachofa/common.cpp
    engines/alcachofa/common.h
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/objects.h
    engines/alcachofa/player.cpp
    engines/alcachofa/player.h
    engines/alcachofa/scheduler.cpp
    engines/alcachofa/scheduler.h
    engines/alcachofa/script.cpp
    engines/alcachofa/sounds.cpp
    engines/alcachofa/sounds.h


diff --git a/engines/alcachofa/common.cpp b/engines/alcachofa/common.cpp
index 3f7afe1cc31..27290532b29 100644
--- a/engines/alcachofa/common.cpp
+++ b/engines/alcachofa/common.cpp
@@ -58,10 +58,20 @@ FakeLock::FakeLock(FakeLock &&other) noexcept : _semaphore(other._semaphore) {
 }
 
 FakeLock::~FakeLock() {
+	release();
+}
+
+void FakeLock::operator= (FakeLock &&other) noexcept {
+	_semaphore = other._semaphore;
+	other._semaphore = nullptr;
+}
+
+void FakeLock::release() {
 	if (_semaphore == nullptr)
 		return;
 	assert(_semaphore->_counter > 0);
 	_semaphore->_counter--;
+	_semaphore = nullptr;
 }
 
 Vector3d as3D(const Vector2d &v) {
diff --git a/engines/alcachofa/common.h b/engines/alcachofa/common.h
index 559823d471b..733f609b58b 100644
--- a/engines/alcachofa/common.h
+++ b/engines/alcachofa/common.h
@@ -99,8 +99,10 @@ struct FakeLock {
 	FakeLock(const FakeLock &other);
 	FakeLock(FakeLock &&other) noexcept;
 	~FakeLock();
+	void operator = (FakeLock &&other) noexcept;
+	void release();
 private:
-	FakeSemaphore *_semaphore;
+	FakeSemaphore *_semaphore = nullptr;
 };
 
 float ease(float t, EasingType type);
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index a1ff35aa985..15e3600add9 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -288,6 +288,8 @@ struct SayTextTask final : public Task {
 		_character->_isTalking = true;
 		graphicOf(_character->_curTalkingObject, &_character->_graphicTalking)->start(true);
 		while (true) {
+			g_engine->player().addLastDialogCharacter(_character);
+
 			if (_soundId == kInvalidSoundID)
 			{
 				bool isMortadeloVoice =
@@ -1022,6 +1024,11 @@ Task *MainCharacter::dialogMenu(Process &process) {
 	return new DialogMenuTask(process, this);
 }
 
+void MainCharacter::resetUsingObjectAndDialogMenu() {
+	_currentlyUsingObject = nullptr;
+	_dialogLines.clear();
+}
+
 const char *Background::typeName() const { return "Background"; }
 
 Background::Background(Room *room, const String &animationFileName, int16 scale)
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 262017e3a45..8738d4b6f39 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -510,6 +510,7 @@ public:
 	void addDialogLine(int32 dialogId);
 	void setLastDialogReturnValue(int32 returnValue);
 	Task *dialogMenu(Process &process);
+	void resetUsingObjectAndDialogMenu();
 
 protected:
 	virtual void onArrived() override;
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index 050e548cc6e..479a30009f1 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -254,4 +254,30 @@ void Player::setPermanentFade(bool isFaded) {
 	_isPermanentFaded = isFaded;
 }
 
+// the last dialog character mechanic seems like a hack in the original engine
+// all talking characters (see SayText kernel call) are added to a fixed-size
+// rolling queue and stopped upon killProcesses
+
+void Player::addLastDialogCharacter(Character *character) {
+	auto lastDialogCharactersEnd = _lastDialogCharacters + kMaxLastDialogCharacters;
+	if (Common::find(_lastDialogCharacters, lastDialogCharactersEnd, character) != lastDialogCharactersEnd)
+		return;
+	_lastDialogCharacters[_nextLastDialogCharacter++] = character;
+	_nextLastDialogCharacter %= kMaxLastDialogCharacters;
+}
+
+void Player::stopLastDialogCharacters() {
+	// originally only the isTalking flag is reset, but this seems a bit safer so unless we find a bug
+	for (int i = 0; i < kMaxLastDialogCharacters; i++) {
+		auto character = _lastDialogCharacters[i];
+		if (character != nullptr)
+			character->resetTalking();
+	}
+}
+
+void Player::setActiveCharacter(MainCharacterKind kind) {
+	scumm_assert(kind == MainCharacterKind::Mortadelo || kind == MainCharacterKind::Filemon);
+	_activeCharacter = &g_engine->world().getMainCharacterByKind(kind);
+}
+
 }
diff --git a/engines/alcachofa/player.h b/engines/alcachofa/player.h
index 3d65dce9947..f63f95e1a3e 100644
--- a/engines/alcachofa/player.h
+++ b/engines/alcachofa/player.h
@@ -56,8 +56,13 @@ public:
 	void triggerObject(ObjectBase *object, const char *action);
 	void triggerDoor(const Door *door);
 	void setPermanentFade(bool isFaded);
+	void addLastDialogCharacter(Character *character);
+	void stopLastDialogCharacters();
+	void setActiveCharacter(MainCharacterKind kind);
 
 private:
+	static constexpr const int kMaxLastDialogCharacters = 4;
+
 	Common::ScopedPtr<Animation> _cursorAnimation;
 	FakeSemaphore _semaphore;
 	Room *_currentRoom = nullptr,
@@ -72,6 +77,8 @@ private:
 		_isGameLoaded = true,
 		_didLoadGlobalRooms = false,
 		_isPermanentFaded = false;
+	Character *_lastDialogCharacters[kMaxLastDialogCharacters] = { nullptr };
+	int _nextLastDialogCharacter = 0;
 };
 
 }
diff --git a/engines/alcachofa/scheduler.cpp b/engines/alcachofa/scheduler.cpp
index 8456a520649..631e63671c8 100644
--- a/engines/alcachofa/scheduler.cpp
+++ b/engines/alcachofa/scheduler.cpp
@@ -183,7 +183,7 @@ void Scheduler::killAllProcesses() {
 void Scheduler::killAllProcessesFor(MainCharacterKind characterKind) {
 	// this method can be called during run() so be careful
 	killProcessesForIn(characterKind, processesToRunNext(), 0);
-	killProcessesForIn(characterKind, processesToRun(), _currentProcessI == UINT_MAX ? 0 : _currentProcessI);
+	killProcessesForIn(characterKind, processesToRun(), _currentProcessI == UINT_MAX ? 0 : _currentProcessI + 1);
 }
 
 static Process **getProcessByName(Array<Process *> &_processes, const String &name) {
diff --git a/engines/alcachofa/scheduler.h b/engines/alcachofa/scheduler.h
index 9ea21f9d415..d48b65faec2 100644
--- a/engines/alcachofa/scheduler.h
+++ b/engines/alcachofa/scheduler.h
@@ -131,7 +131,7 @@ public:
 	~Process();
 
 	inline ProcessId pid() const { return _pid; }
-	inline MainCharacterKind character() const { return _character; }
+	inline MainCharacterKind &character() { return _character; } // is changed in changeCharacter
 	inline int32 returnValue() const { return _lastReturnValue; }
 	inline Common::String &name() { return _name; }
 	bool isActiveForPlayer() const; ///< and thus should e.g. draw subtitles or effects
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 809692c2480..380304b6c1e 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -463,13 +463,28 @@ private:
 			g_engine->scheduler().createProcess<ScriptTask>(process().character(), *this);
 			return TaskReturn::finish(0); // 0 means this is the forking process
 		case ScriptKernelTask::KillProcesses:
-			warning("STUB KERNEL CALL: KillProcesses");
-			return TaskReturn::finish(0);
+			killProcessesFor((MainCharacterKind)getNumberArg(0));
+			return TaskReturn::finish(1);
 
 		// player/world state changes
-		case ScriptKernelTask::ChangeCharacter:
-			warning("STUB KERNEL CALL: ChangeCharacter");
-			return TaskReturn::finish(0);
+		case ScriptKernelTask::ChangeCharacter: {
+			MainCharacterKind kind = (MainCharacterKind)getNumberArg(0);
+			killProcessesFor(MainCharacterKind::None); // yes, kill for all characters
+			auto &camera = g_engine->camera();
+			auto &player = g_engine->player();
+			camera.resetRotationAndScale();
+			camera.backup(0);
+			if (kind != MainCharacterKind::None) {
+				player.setActiveCharacter(kind);
+				player.heldItem() = nullptr;
+				camera.setFollow(player.activeCharacter());
+				camera.backup(0);
+			}
+			process().character() = MainCharacterKind::None;
+			assert(player.semaphore().isReleased());
+			_lock = { player.semaphore() };
+			return TaskReturn::finish(1);
+		}
 		case ScriptKernelTask::ChangeRoom:
 			if (strcmpi(getStringArg(0), "SALIR") == 0) {
 				g_engine->quitGame();
@@ -777,6 +792,22 @@ private:
 		}
 	}
 
+	void killProcessesFor(MainCharacterKind kind) {
+		if (kind == MainCharacterKind::None) {
+			killProcessesFor(MainCharacterKind::Mortadelo);
+			killProcessesFor(MainCharacterKind::Filemon);
+			g_engine->scheduler().killAllProcessesFor(kind);
+			return;
+		}
+		g_engine->scheduler().killAllProcessesFor(kind);
+		g_engine->sounds().fadeOutVoiceAndSFX(200);
+		g_engine->player().stopLastDialogCharacters();
+		_lock.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...
+	}
+
 	Script &_script;
 	Stack<StackEntry> _stack;
 	String _name;
diff --git a/engines/alcachofa/sounds.cpp b/engines/alcachofa/sounds.cpp
index adeb31f336f..ec6ad549036 100644
--- a/engines/alcachofa/sounds.cpp
+++ b/engines/alcachofa/sounds.cpp
@@ -38,6 +38,11 @@ namespace Alcachofa {
 Sounds::Playback::Playback(uint32 id, SoundHandle handle, Mixer::SoundType type)
 	: _id(id), _handle(handle), _type(type) {}
 
+void Sounds::Playback::fadeOut(uint32 duration) {
+	_fadeStart = g_system->getMillis();
+	_fadeDuration = MAX<uint32>(duration, 1);
+}
+
 Sounds::Sounds() : _mixer(g_system->getMixer()) {
 	assert(_mixer != nullptr);
 }
@@ -181,9 +186,14 @@ void Sounds::setAppropriateVolume(SoundID id,
 
 void Sounds::fadeOut(SoundID id, uint32 duration) {
 	Playback *playback = getPlaybackById(id);
-	if (playback != nullptr) {
-		playback->_fadeStart = g_system->getMillis();
-		playback->_fadeDuration = MAX<uint32>(duration, 1);
+	if (playback != nullptr)
+		playback->fadeOut(duration);
+}
+
+void Sounds::fadeOutVoiceAndSFX(uint32 duration) {
+	for (auto &playback : _playbacks) {
+		if (playback._type == Mixer::kSpeechSoundType || playback._type == Mixer::kSFXSoundType)
+			playback.fadeOut(duration);
 	}
 }
 
diff --git a/engines/alcachofa/sounds.h b/engines/alcachofa/sounds.h
index 35eb8ccde44..377d1313a59 100644
--- a/engines/alcachofa/sounds.h
+++ b/engines/alcachofa/sounds.h
@@ -41,6 +41,7 @@ public:
 	SoundID playSFX(const Common::String &fileName, byte volume = Audio::Mixer::kMaxChannelVolume);
 	void stopVoice();
 	void fadeOut(SoundID id, uint32 duration);
+	void fadeOutVoiceAndSFX(uint32 duration);
 	bool isAlive(SoundID id);
 	void setVolume(SoundID id, byte volume);
 	void setAppropriateVolume(SoundID id,
@@ -50,6 +51,7 @@ public:
 private:
 	struct Playback {
 		Playback(uint32 id, Audio::SoundHandle handle, Audio::Mixer::SoundType type);
+		void fadeOut(uint32 duration);
 
 		SoundID _id;
 		Audio::SoundHandle _handle;


Commit: 14d6797687fc11ac3b931b177ae55a1e0fc36089
    https://github.com/scummvm/scummvm/commit/14d6797687fc11ac3b931b177ae55a1e0fc36089
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:50+02:00

Commit Message:
ALCACHOFA: Refactor fonts into new GlobalUI component

Changed paths:
  A engines/alcachofa/global-ui.cpp
  A engines/alcachofa/global-ui.h
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/alcachofa.h
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/general-objects.cpp
    engines/alcachofa/module.mk
    engines/alcachofa/rooms.cpp
    engines/alcachofa/rooms.h


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 78f66ca0b8b..682484c2efe 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -35,6 +35,7 @@
 
 #include "rooms.h"
 #include "script.h"
+#include "global-ui.h"
 #include "debug.h"
 
 using namespace Math;
@@ -67,10 +68,11 @@ Common::Error AlcachofaEngine::run() {
 	_world.reset(new World());
 	_script.reset(new Script());
 	_player.reset(new Player());
+	_globalUI.reset(new GlobalUI());
 
-	_script->createProcess(MainCharacterKind::None, "Inicializar_Variables");
-	_player->changeRoom("MINA", true);
-	//_script->createProcess(MainCharacterKind::None, "CREDITOS_INICIALES");
+	//_script->createProcess(MainCharacterKind::None, "Inicializar_Variables");
+	//_player->changeRoom("HORCA", true);
+	_script->createProcess(MainCharacterKind::None, "CREDITOS_INICIALES");
 	_scheduler.run();
 
 	Common::Event e;
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index 0b48f5c01a4..d19634e4f06 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -49,6 +49,7 @@ class IRenderer;
 class DrawQueue;
 class World;
 class Script;
+class GlobalUI;
 struct AlcachofaGameDescription;
 
 class AlcachofaEngine : public Engine {
@@ -70,6 +71,7 @@ public:
 	inline Player &player() { return *_player; }
 	inline World &world() { return *_world; }
 	inline Script &script() { return *_script; }
+	inline GlobalUI &globalUI() { return *_globalUI; }
 	inline Scheduler &scheduler() { return _scheduler; }
 	inline Console &console() { return *_console; }
 	inline bool isDebugModeActive() const { return _debugHandler != nullptr; }
@@ -128,6 +130,7 @@ private:
 	Common::ScopedPtr<World> _world;
 	Common::ScopedPtr<Script> _script;
 	Common::ScopedPtr<Player> _player;
+	Common::ScopedPtr<GlobalUI> _globalUI;
 	Camera _camera;
 	Input _input;
 	Sounds _sounds;
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 15e3600add9..0c46b0fb3b6 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -22,6 +22,7 @@
 #include "objects.h"
 #include "rooms.h"
 #include "script.h"
+#include "global-ui.h"
 #include "alcachofa.h"
 
 using namespace Common;
@@ -306,7 +307,7 @@ struct SayTextTask final : public Task {
 			if (true && // TODO: Add game option for subtitles
 				process().isActiveForPlayer()) {
 				g_engine->drawQueue().add<TextDrawRequest>(
-					g_engine->world().dialogFont(),
+					g_engine->globalUI().dialogFont(),
 					g_engine->world().getDialogLine(_dialogId),
 					Point(g_system->getWidth() / 2, g_system->getHeight() - 200),
 					-1, true, kWhite, -kForegroundOrderCount);
@@ -972,7 +973,7 @@ private:
 		for (auto &itLine : lines) {
 			// we reuse the draw request to measure the actual height without using it to actually draw
 			TextDrawRequest request(
-				g_engine->world().dialogFont(),
+				g_engine->globalUI().dialogFont(),
 				g_engine->world().getDialogLine(itLine._dialogId),
 				Point(kTextXOffset, 0), maxTextWidth(), false, kWhite, 2);
 			itLine._yPosition = request.size().y; // briefly storing line height
@@ -989,7 +990,7 @@ private:
 			auto &itLine = _character->_dialogLines[i - 1];
 			bool isHovered = !isSomethingHovered && _input.mousePos2D().y >= itLine._yPosition - kTextYOffset;
 			g_engine->drawQueue().add<TextDrawRequest>(
-				g_engine->world().dialogFont(),
+				g_engine->globalUI().dialogFont(),
 				g_engine->world().getDialogLine(itLine._dialogId),
 				Point(kTextXOffset, itLine._yPosition),
 				maxTextWidth(), false, isHovered ? Color{ 255, 255, 128, 255 } : kWhite, -kForegroundOrderCount + 2);
diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index cca8b6c27f4..98e75fa0c8c 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -22,6 +22,7 @@
 #include "objects.h"
 #include "rooms.h"
 #include "scheduler.h"
+#include "global-ui.h"
 #include "alcachofa.h"
 
 #include "common/system.h"
@@ -227,7 +228,7 @@ void ShapeObject::onHoverEnd() {
 
 void ShapeObject::onHoverUpdate() {
 	g_engine->drawQueue().add<TextDrawRequest>(
-		g_engine->world().generalFont(),
+		g_engine->globalUI().generalFont(),
 		g_engine->world().getLocalizedName(name()),
 		g_engine->input().mousePos2D() - Point(0, 35),
 		-1, true, kWhite, 0);
diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
new file mode 100644
index 00000000000..67ac1fdadef
--- /dev/null
+++ b/engines/alcachofa/global-ui.cpp
@@ -0,0 +1,48 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "global-ui.h"
+#include "alcachofa.h"
+
+namespace Alcachofa {
+
+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)));
+	_iconMortadeloDisabled.reset(new Animation(world.getGlobalAnimationName(GlobalAnimationKind::MortadeloDisabledIcon)));
+	_iconFilemonDisabled.reset(new Animation(world.getGlobalAnimationName(GlobalAnimationKind::FilemonDisabledIcon)));
+	_iconInventoryDisabled.reset(new Animation(world.getGlobalAnimationName(GlobalAnimationKind::InventoryDisabledIcon)));
+
+	_generalFont->load();
+	_dialogFont->load();
+	_iconMortadelo->load();
+	_iconFilemon->load();
+	_iconInventory->load();
+	_iconMortadeloDisabled->load();
+	_iconFilemonDisabled->load();
+	_iconInventoryDisabled->load();
+}
+
+}
diff --git a/engines/alcachofa/global-ui.h b/engines/alcachofa/global-ui.h
new file mode 100644
index 00000000000..558ff157d9d
--- /dev/null
+++ b/engines/alcachofa/global-ui.h
@@ -0,0 +1,52 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef GLOBAL_UI_H
+#define GLOBAL_UI_H
+
+#include "objects.h"
+
+namespace Alcachofa {
+
+class GlobalUI {
+public:
+	GlobalUI();
+
+	inline Font &generalFont() const { assert(_generalFont != nullptr); return *_generalFont; }
+	inline Font &dialogFont() const { assert(_dialogFont != nullptr); return *_dialogFont; }
+
+private:
+	Common::ScopedPtr<Font>
+		_generalFont,
+		_dialogFont;
+	Common::ScopedPtr<Animation>
+		_iconMortadelo,
+		_iconFilemon,
+		_iconInventory,
+		_iconMortadeloDisabled,
+		_iconFilemonDisabled,
+		_iconInventoryDisabled;
+};
+
+}
+
+
+#endif // GLOBAL_UI_H
diff --git a/engines/alcachofa/module.mk b/engines/alcachofa/module.mk
index bca3bc181a8..9c8300c66ea 100644
--- a/engines/alcachofa/module.mk
+++ b/engines/alcachofa/module.mk
@@ -7,6 +7,7 @@ MODULE_OBJS = \
 	console.o \
 	game-objects.cpp \
 	general-objects.cpp \
+	global-ui.cpp \
 	graphics.cpp \
 	graphics-opengl.cpp \
 	Input.cpp \
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index c04ed7e7c09..bc903af522b 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -22,6 +22,7 @@
 #include "alcachofa.h"
 #include "rooms.h"
 #include "script.h"
+#include "global-ui.h"
 
 #include "common/file.h"
 
@@ -208,10 +209,6 @@ void Room::updateInteraction() {
 	if (updateOpeningInventory())
 		return;
 
-	if (false && player.activeCharacter()->room() != this) { // TODO: Remove active character hack
-		player.activeCharacter()->room() = this;
-	}
-
 	player.selectedObject() = world().globalRoom().getSelectedObject(getSelectedObject());
 	if (player.selectedObject() == nullptr) {
 		if (input.wasMouseLeftPressed() && _activeFloorI >= 0 &&
@@ -403,7 +400,7 @@ bool Inventory::updateInput() {
 		}
 
 		g_engine->drawQueue().add<TextDrawRequest>(
-			g_engine->world().generalFont(),
+			g_engine->globalUI().generalFont(),
 			g_engine->world().getLocalizedName(hoveredItem->name()),
 			input.mousePos2D() + Point(0, -50),
 			-1, true, kWhite, -kForegroundOrderCount + 1);
@@ -538,11 +535,6 @@ World::World() {
 	if (_mortadelo == nullptr)
 		error("Could not find MORTADELO");
 
-	_generalFont.reset(new Font(getGlobalAnimationName(GlobalAnimationKind::GeneralFont)));
-	_generalFont->load();
-	_dialogFont.reset(new Font(getGlobalAnimationName(GlobalAnimationKind::DialogFont)));
-	_dialogFont->load();
-
 	_inventory->initItems();
 }
 
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index cdff4c62ee9..424c6601aa6 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -159,8 +159,6 @@ 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 Font &generalFont() const { assert(_generalFont != nullptr); return *_generalFont; }
-	inline Font &dialogFont() const { assert(_dialogFont != nullptr); return *_dialogFont; }
 	inline const Common::String &initScriptName() const { return _initScriptName; }
 	inline uint8 loadedMapCount() const { return _loadedMapCount; }
 
@@ -197,7 +195,6 @@ private:
 	Room *_globalRoom;
 	Inventory *_inventory;
 	MainCharacter *_filemon, *_mortadelo;
-	Common::ScopedPtr<Font> _generalFont, _dialogFont;
 	uint8 _loadedMapCount = 0;
 	Common::HashMap<const char *, const char *,
 		Common::Hash<const char*>,


Commit: e3625bf330a14c21517e9efb922f8ca5056e41ef
    https://github.com/scummvm/scummvm/commit/e3625bf330a14c21517e9efb922f8ca5056e41ef
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:50+02:00

Commit Message:
ALCACHOFA: Refactor inventory UI triggers into GlobalUI

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


diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index 67ac1fdadef..8f7d8b35eb0 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -22,8 +22,26 @@
 #include "global-ui.h"
 #include "alcachofa.h"
 
+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 I should add key-based controls for it
+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.getGlobalAnimationName(GlobalAnimationKind::GeneralFont)));
@@ -45,4 +63,49 @@ GlobalUI::GlobalUI() {
 	_iconInventoryDisabled->load();
 }
 
+void GlobalUI::startClosingInventory() {
+	_isOpeningInventory = false;
+	_isClosingInventory = true;
+	_timeForInventory = g_system->getMillis();
+}
+
+void GlobalUI::updateClosingInventory() {
+	static constexpr uint32 kDuration = 300;
+	static constexpr float kSpeed = -10 / 3.0f / 1000.0f;
+
+	uint32 deltaTime = g_system->getMillis() - _timeForInventory;
+	if (!_isClosingInventory || deltaTime >= kDuration)
+		_isClosingInventory = false;
+	else
+		g_engine->world().inventory().drawAsOverlay((int32)(g_system->getHeight() * (deltaTime * kSpeed)));
+}
+
+bool GlobalUI::updateOpeningInventory() {
+	static constexpr float kSpeed = 10 / 3.0f / 1000.0f;
+	if (g_engine->player().isOptionsMenuOpen() || !g_engine->player().isGameLoaded())
+		return false;
+
+	if (_isOpeningInventory) {
+		uint32 deltaTime = g_system->getMillis() - _timeForInventory;
+		if (deltaTime >= 1000) {
+			_isOpeningInventory = false;
+			g_engine->world().inventory().open();
+		}
+		else {
+			deltaTime = MIN<uint32>(300, deltaTime);
+			g_engine->world().inventory().drawAsOverlay((int32)(g_system->getHeight() * (deltaTime * kSpeed - 1)));
+		}
+		return true;
+	}
+	else if (openInventoryTriggerBounds().contains(g_engine->input().mousePos2D())) {
+		_isClosingInventory = false;
+		_isOpeningInventory = true;
+		_timeForInventory = g_system->getMillis();
+		g_engine->player().activeCharacter()->stopWalking();
+		g_engine->world().inventory().updateItemsByActiveCharacter();
+		return true;
+	}
+	return false;
+}
+
 }
diff --git a/engines/alcachofa/global-ui.h b/engines/alcachofa/global-ui.h
index 558ff157d9d..c5c5ec7a9e7 100644
--- a/engines/alcachofa/global-ui.h
+++ b/engines/alcachofa/global-ui.h
@@ -26,6 +26,9 @@
 
 namespace Alcachofa {
 
+Common::Rect openInventoryTriggerBounds();
+Common::Rect closeInventoryTriggerBounds();
+
 class GlobalUI {
 public:
 	GlobalUI();
@@ -33,6 +36,10 @@ public:
 	inline Font &generalFont() const { assert(_generalFont != nullptr); return *_generalFont; }
 	inline Font &dialogFont() const { assert(_dialogFont != nullptr); return *_dialogFont; }
 
+	bool updateOpeningInventory();
+	void updateClosingInventory();
+	void startClosingInventory();
+
 private:
 	Common::ScopedPtr<Font>
 		_generalFont,
@@ -44,6 +51,11 @@ private:
 		_iconMortadeloDisabled,
 		_iconFilemonDisabled,
 		_iconInventoryDisabled;
+
+	bool
+		_isOpeningInventory = false,
+		_isClosingInventory = false;
+	uint32 _timeForInventory = 0;
 };
 
 }
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index bc903af522b..c01fc4554be 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -30,21 +30,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.
-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());
-}
-
 Room::Room(World *world, ReadStream &stream) : Room(world, stream, false) {
 }
 
@@ -150,7 +135,7 @@ void Room::update() {
 
 		if (g_engine->player().currentRoom() == this) {
 			updateRoomBounds();
-			updateClosingInventory();
+			g_engine->globalUI().updateClosingInventory();
 			if (!updateInput())
 				return;
 		}
@@ -206,7 +191,7 @@ void Room::updateInteraction() {
 	auto &input = g_engine->input();
 	// TODO: Add interaction with change character button
 
-	if (updateOpeningInventory())
+	if (g_engine->globalUI().updateOpeningInventory())
 		return;
 
 	player.selectedObject() = world().globalRoom().getSelectedObject(getSelectedObject());
@@ -318,51 +303,6 @@ ShapeObject *Room::getSelectedObject(ShapeObject *best) const {
 	return best;
 }
 
-void Room::startClosingInventory() {
-	_isOpeningInventory = false;
-	_isClosingInventory = true;
-	_timeForInventory = g_system->getMillis();
-}
-
-void Room::updateClosingInventory() {
-	static constexpr uint32 kDuration = 300;
-	static constexpr float kSpeed = -10 / 3.0f / 1000.0f;
-
-	uint32 deltaTime = g_system->getMillis() - _timeForInventory;
-	if (!_isClosingInventory || deltaTime >= kDuration)
-		_isClosingInventory = false;
-	else
-		g_engine->world().inventory().drawAsOverlay((int32)(g_system->getHeight() * (deltaTime * kSpeed)));
-}
-
-bool Room::updateOpeningInventory() {
-	static constexpr float kSpeed = 10 / 3.0f / 1000.0f;
-	if (g_engine->player().isOptionsMenuOpen() || !g_engine->player().isGameLoaded())
-		return false;
-
-	if (_isOpeningInventory) {
-		uint32 deltaTime = g_system->getMillis() - _timeForInventory;
-		if (deltaTime >= 1000) {
-			_isOpeningInventory = false;
-			g_engine->world().inventory().open();
-		}
-		else {
-			deltaTime = MIN<uint32>(300, deltaTime);
-			g_engine->world().inventory().drawAsOverlay((int32)(g_system->getHeight() * (deltaTime * kSpeed - 1)));
-		}
-		return true;
-	}
-	else if (openInventoryTriggerBounds().contains(g_engine->input().mousePos2D())) {
-		_isClosingInventory = false;
-		_isOpeningInventory = true;
-		_timeForInventory = g_system->getMillis();
-		g_engine->player().activeCharacter()->stopWalking();
-		g_engine->world().inventory().updateItemsByActiveCharacter();
-		return true;
-	}
-	return false;
-}
-
 OptionsMenu::OptionsMenu(World *world, ReadStream &stream)
 	: Room(world, stream, true) {
 }
@@ -487,7 +427,7 @@ void Inventory::open() {
 void Inventory::close() {
 	g_engine->player().changeRoomToBeforeInventory();
 	g_engine->camera().restore(1);
-	g_engine->player().currentRoom()->startClosingInventory();
+	g_engine->globalUI().startClosingInventory();
 }
 
 void Room::debugPrint(bool withObjects) const {
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index 424c6601aa6..c53c2a1796b 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -60,15 +60,12 @@ public:
 	virtual void serializeSave(Common::Serializer &serializer);
 	ObjectBase *getObjectByName(const Common::String &name) const;
 	void toggleActiveFloor();
-	void startClosingInventory();
 	void debugPrint(bool withObjects) const;
 
 protected:
 	Room(World *world, Common::ReadStream &stream, bool hasUselessByte);
 	void updateScripts();
 	void updateRoomBounds();
-	bool updateOpeningInventory();
-	void updateClosingInventory();
 	void updateInteraction();
 	void updateObjects();
 	void drawObjects();
@@ -78,17 +75,13 @@ protected:
 	World *_world;
 	Common::String _name;
 	PathFindingShape _floors[2];
-	bool
-		_fixedCameraOnEntering,
-		_isOpeningInventory = false,
-		_isClosingInventory = false;
+	bool _fixedCameraOnEntering;
 	int8
 		_musicId,
 		_activeFloorI = -1;
 	uint8
 		_characterAlphaTint,
 		_characterAlphaPremultiplier; ///< for some reason in percent instead of 0-255
-	uint32 _timeForInventory = 0;
 
 	Common::Array<ObjectBase *> _objects;
 };


Commit: 17c4a38686f96c0b74d05a20bfcc9a922e5c13ae
    https://github.com/scummvm/scummvm/commit/17c4a38686f96c0b74d05a20bfcc9a922e5c13ae
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:50+02:00

Commit Message:
ALCACHOFA: Add character change button

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


diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index 8f7d8b35eb0..3a9fb3005be 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -49,18 +49,12 @@ GlobalUI::GlobalUI() {
 	_iconMortadelo.reset(new Animation(world.getGlobalAnimationName(GlobalAnimationKind::MortadeloIcon)));
 	_iconFilemon.reset(new Animation(world.getGlobalAnimationName(GlobalAnimationKind::FilemonIcon)));
 	_iconInventory.reset(new Animation(world.getGlobalAnimationName(GlobalAnimationKind::InventoryIcon)));
-	_iconMortadeloDisabled.reset(new Animation(world.getGlobalAnimationName(GlobalAnimationKind::MortadeloDisabledIcon)));
-	_iconFilemonDisabled.reset(new Animation(world.getGlobalAnimationName(GlobalAnimationKind::FilemonDisabledIcon)));
-	_iconInventoryDisabled.reset(new Animation(world.getGlobalAnimationName(GlobalAnimationKind::InventoryDisabledIcon)));
 
 	_generalFont->load();
 	_dialogFont->load();
 	_iconMortadelo->load();
 	_iconFilemon->load();
 	_iconInventory->load();
-	_iconMortadeloDisabled->load();
-	_iconFilemonDisabled->load();
-	_iconInventoryDisabled->load();
 }
 
 void GlobalUI::startClosingInventory() {
@@ -108,4 +102,82 @@ bool GlobalUI::updateOpeningInventory() {
 	return false;
 }
 
+Animation *GlobalUI::activeAnimation() const {
+	return g_engine->player().activeCharacterKind() == MainCharacterKind::Mortadelo
+		? _iconFilemon.get()
+		: _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 (player.isOptionsMenuOpen() ||
+		!player.isGameLoaded() ||
+		_isOpeningInventory)
+		return false;
+	_changeButton.frameI() = 0;
+
+	if (!isHoveringChangeButton())
+		return false;
+	if (g_engine->input().wasMouseLeftPressed())
+	{
+		player.pressedObject() = &_changeButton;
+		return true;
+	}
+	if (player.pressedObject() != &_changeButton)
+		return true;
+
+	player.setActiveCharacter(player.inactiveCharacter()->kind());
+	player.heldItem() = nullptr;
+	g_engine->camera().setFollow(player.activeCharacter());
+	g_engine->camera().restore(0);
+	player.changeRoom(player.activeCharacter()->room()->name(), false);
+	// TODO: Queue character change jingle
+
+	_changeButton.setAnimation(activeAnimation());
+	_changeButton.start(false);
+	return true;
+}
+
+void GlobalUI::drawChangingButton() {
+	auto &player = g_engine->player();
+	if (player.isOptionsMenuOpen() ||
+		!player.isGameLoaded() ||
+		!player.semaphore().isReleased() ||
+		_isOpeningInventory ||
+		_isClosingInventory)
+		return;
+
+	auto anim = activeAnimation();
+	if (!_changeButton.hasAnimation() || &_changeButton.animation() != anim)
+	{
+		_changeButton.setAnimation(anim);
+		_changeButton.pause();
+		_changeButton.lastTime() = 42 * (anim->frameCount() - 1) + 1;
+	}
+
+	_changeButton.center() = { (int16)(g_system->getWidth() + 2), -2 };
+	if (isHoveringChangeButton() &&
+		g_engine->input().isMouseLeftDown() &&
+		player.pressedObject() == &_changeButton)
+	{
+		_changeButton.center().x -= 2;
+		_changeButton.center().y += 2;
+	}
+
+	_changeButton.order() = -9;
+	_changeButton.update();
+	g_engine->drawQueue().add<AnimationDrawRequest>(_changeButton, false, BlendMode::AdditiveAlpha);
+}
+
 }
diff --git a/engines/alcachofa/global-ui.h b/engines/alcachofa/global-ui.h
index c5c5ec7a9e7..d07dbf86f8d 100644
--- a/engines/alcachofa/global-ui.h
+++ b/engines/alcachofa/global-ui.h
@@ -36,21 +36,24 @@ public:
 	inline Font &generalFont() const { assert(_generalFont != nullptr); return *_generalFont; }
 	inline Font &dialogFont() const { assert(_dialogFont != nullptr); return *_dialogFont; }
 
+	bool updateChangingCharacter();
+	void drawChangingButton();
 	bool updateOpeningInventory();
 	void updateClosingInventory();
 	void startClosingInventory();
 
 private:
+	Animation *activeAnimation() const;
+	bool isHoveringChangeButton() const;
+
+	Graphic _changeButton;
 	Common::ScopedPtr<Font>
 		_generalFont,
 		_dialogFont;
 	Common::ScopedPtr<Animation>
 		_iconMortadelo,
 		_iconFilemon,
-		_iconInventory,
-		_iconMortadeloDisabled,
-		_iconFilemonDisabled,
-		_iconInventoryDisabled;
+		_iconInventory;
 
 	bool
 		_isOpeningInventory = false,
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index a394604ca41..78749514ce2 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -257,7 +257,7 @@ public:
 	inline float &depthScale() { return _depthScale; }
 	inline Color &color() { return _color; }
 	inline int32 &frameI() { return _frameI; }
-	inline uint32 lastTime() const { return _lastTime; }
+	inline uint32 &lastTime() { return _lastTime; }
 	inline bool isPaused() const { return _isPaused; }
 	inline bool hasAnimation() const { return _animation != nullptr; }
 	inline Animation &animation() {
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index 479a30009f1..a873dd870a7 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -49,6 +49,10 @@ void Player::drawScreenStates() {
 		g_engine->drawQueue().add<FadeDrawRequest>(FadeType::ToBlack, 1.0f, -9);
 }
 
+void Player::resetCursor() {
+	_cursorFrameI = 0;
+}
+
 void Player::updateCursor() {
 	if (_isOptionsMenuOpen || !_isGameLoaded)
 		_cursorFrameI = 0;
@@ -75,8 +79,6 @@ void Player::updateCursor() {
 		else if (g_engine->input().isMouseRightDown())
 			_cursorFrameI = 4;
 	}
-
-	drawCursor();
 }
 
 void Player::drawCursor(bool forceDefaultCursor) {
diff --git a/engines/alcachofa/player.h b/engines/alcachofa/player.h
index f63f95e1a3e..1b7139bc0cd 100644
--- a/engines/alcachofa/player.h
+++ b/engines/alcachofa/player.h
@@ -33,7 +33,7 @@ public:
 	inline Room *currentRoom() const { return _currentRoom; }
 	inline MainCharacter *activeCharacter() const { return _activeCharacter; }
     inline ShapeObject *&selectedObject() { return _selectedObject; }
-	inline ShapeObject *&pressedObject() { return _pressedObject; }
+	inline void *&pressedObject() { return _pressedObject; }
 	inline Item *&heldItem() { return _heldItem; }
 	inline FakeSemaphore &semaphore() { return _semaphore; }
 	MainCharacter *inactiveCharacter() const;
@@ -51,6 +51,7 @@ public:
 	void drawScreenStates(); // black borders and/or permanent fade
 	void updateCursor();
 	void drawCursor(bool forceDefaultCursor = false);
+	void resetCursor();
 	void changeRoom(const Common::String &targetRoomName, bool resetCamera);
 	void changeRoomToBeforeInventory();
 	void triggerObject(ObjectBase *object, const char *action);
@@ -69,7 +70,7 @@ private:
 		*_roomBeforeInventory = nullptr;
 	MainCharacter *_activeCharacter;
     ShapeObject *_selectedObject = nullptr;
-    ShapeObject *_pressedObject = nullptr;
+    void *_pressedObject = nullptr; // terrible but GlobalUI wants to store a Graphic pointer
 	Item *_heldItem = nullptr;
 	int32 _cursorFrameI = 0;
 	bool
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index c01fc4554be..4e42b859d8f 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -178,21 +178,26 @@ bool Room::updateInput() {
 	if (player.isOptionsMenuOpen() || !player.isGameLoaded())
 		canInteract = true;
 	if (canInteract) {
-		updateInteraction();
-		player.updateCursor();
+		player.resetCursor();
+		if (!g_engine->globalUI().updateChangingCharacter() &&
+			!g_engine->globalUI().updateOpeningInventory()) {
+			updateInteraction();
+			player.updateCursor();
+		}
+		player.drawCursor();
+	}
+
+	if (player.currentRoom() == this) {
+		g_engine->globalUI().drawChangingButton();
+		// TODO: Add main menu handling
 	}
 
-	// TODO: Add main menu and opening inventory handling
 	return player.currentRoom() == this;
 }
 
 void Room::updateInteraction() {
 	auto &player = g_engine->player();
 	auto &input = g_engine->input();
-	// TODO: Add interaction with change character button
-
-	if (g_engine->globalUI().updateOpeningInventory())
-		return;
 
 	player.selectedObject() = world().globalRoom().getSelectedObject(getSelectedObject());
 	if (player.selectedObject() == nullptr) {
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index c53c2a1796b..6a971bea562 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -131,7 +131,7 @@ enum class GlobalAnimationKind {
 	MortadeloIcon,
 	FilemonIcon,
 	InventoryIcon,
-	MortadeloDisabledIcon,
+	MortadeloDisabledIcon, // only used for multiplayer
 	FilemonDisabledIcon,
 	InventoryDisabledIcon,
 


Commit: c97572c37164f80da3af4ff122c2655d70a5b884
    https://github.com/scummvm/scummvm/commit/c97572c37164f80da3af4ff122c2655d70a5b884
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:50+02:00

Commit Message:
ALCACHOFA: Speed up room transitions by buffering images

Changed paths:
    engines/alcachofa/graphics.cpp


diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index c7cf2885dec..d6a0cad019f 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -26,6 +26,7 @@
 #include "common/system.h"
 #include "common/file.h"
 #include "common/substream.h"
+#include "common/bufferedstream.h"
 #include "image/tga.h"
 
 using namespace Common;
@@ -66,7 +67,7 @@ void AnimationBase::load() {
 	if (_isLoaded)
 		return;
 
-	Common::String fullPath;
+	String fullPath;
 	switch (_folder) {
 	case AnimationFolder::Animations: fullPath = "Animaciones/"; break;
 	case AnimationFolder::Masks: fullPath = "Mascaras/"; break;
@@ -76,7 +77,8 @@ void AnimationBase::load() {
 	if (_fileName.size() < 4 || scumm_strnicmp(_fileName.end() - 4, ".AN0", 4) != 0)
 		_fileName += ".AN0";
 	fullPath += _fileName;
-	Common::File file;
+
+	File file;
 	if (!file.open(fullPath.c_str())) {
 		// original fallback
 		fullPath = "Mascaras/" + _fileName;
@@ -85,16 +87,18 @@ void AnimationBase::load() {
 			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));
 
-	uint spriteCount = file.readUint32LE();
+	uint spriteCount = stream->readUint32LE();
 	assert(spriteCount < kMaxSpriteIDs);
 	_spriteBases.reserve(spriteCount);
 
-	uint imageCount = file.readUint32LE();
+	uint imageCount = stream->readUint32LE();
 	_images.reserve(imageCount);
 	_imageOffsets.reserve(imageCount);
 	for (uint i = 0; i < imageCount; i++) {
-		_images.push_back(readImage(file));
+		_images.push_back(readImage(*stream));
 	}
 
 	// an inconsistency, maybe a historical reason:
@@ -102,32 +106,32 @@ 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(file.readUint32LE());
+		_spriteBases.push_back(stream->readUint32LE());
 		assert(_spriteBases.back() < imageCount);
 	}
 #ifdef _DEBUG
 	for (uint i = spriteCount; i < kMaxSpriteIDs; i++)
-		assert(file.readSint32LE() == 0);
+		assert(stream->readSint32LE() == 0);
 #else
-	file.skip(sizeof(int32) * (kMaxSpriteIDs - spriteCount));
+	stream->skip(sizeof(int32) * (kMaxSpriteIDs - spriteCount));
 #endif
 
 	for (uint i = 0; i < imageCount; i++)
-		_imageOffsets.push_back(readPoint(file));
+		_imageOffsets.push_back(readPoint(*stream));
 	for (uint i = 0; i < kMaxSpriteIDs; i++)
-		_spriteIndexMapping[i] = file.readSint32LE();
+		_spriteIndexMapping[i] = stream->readSint32LE();
 
-	uint frameCount = file.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(file.readUint32LE());
+			_spriteOffsets.push_back(stream->readUint32LE());
 		AnimationFrame frame;
-		frame._center = readPoint(file);
-		frame._offset = readPoint(file);
-		frame._duration = file.readUint32LE();
+		frame._center = readPoint(*stream);
+		frame._offset = readPoint(*stream);
+		frame._duration = stream->readUint32LE();
 		_frames.push_back(frame);
 		_totalDuration += frame._duration;
 	}


Commit: 25e4aebd7988036f0cc24b5ea42e250fd7a6938f
    https://github.com/scummvm/scummvm/commit/25e4aebd7988036f0cc24b5ea42e250fd7a6938f
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:50+02:00

Commit Message:
ALCACHOFA: Disable OpenGL debugging by default

Changed paths:
    engines/alcachofa/graphics-opengl.cpp


diff --git a/engines/alcachofa/graphics-opengl.cpp b/engines/alcachofa/graphics-opengl.cpp
index c3e882f222d..7b1682656eb 100644
--- a/engines/alcachofa/graphics-opengl.cpp
+++ b/engines/alcachofa/graphics-opengl.cpp
@@ -25,7 +25,12 @@
 #include "engines/util.h"
 #include "graphics/managed_surface.h"
 #include "graphics/opengl/system_headers.h"
+
+#ifdef ALCACHOFA_DEBUG_OPENGL // clearing OpenGL errors are very slow, so we only activate the debugging wrapper if necessary
 #include "graphics/opengl/debug.h"
+#else
+#define GL_CALL(call) call
+#endif
 
 using namespace Common;
 using namespace Math;


Commit: 381872dfda36bb0ec5f17e04e94ce128478166ba
    https://github.com/scummvm/scummvm/commit/381872dfda36bb0ec5f17e04e94ce128478166ba
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:50+02:00

Commit Message:
ALCACHOFA: Fix changing character during dialog

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


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 0c46b0fb3b6..4b43948f14b 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -941,6 +941,7 @@ struct DialogMenuTask : public Task {
 			TASK_YIELD;
 			if (g_engine->player().activeCharacter() != _character)
 				continue;
+			g_engine->globalUI().updateChangingCharacter();
 			g_engine->player().heldItem() = nullptr;
 			g_engine->player().drawCursor();
 


Commit: 3cb5cebc183109340b905c23c5f1070325f98234
    https://github.com/scummvm/scummvm/commit/3cb5cebc183109340b905c23c5f1070325f98234
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:51+02:00

Commit Message:
ALCACHOFA: Fix Filemon not being rendered

Changed paths:
    engines/alcachofa/rooms.cpp


diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 4e42b859d8f..f8ff4192b56 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -233,8 +233,7 @@ void Room::updateObjects() {
 
 void Room::drawObjects() {
 	for (auto *object : _objects) {
-		if (object->room() == g_engine->player().currentRoom())
-			object->draw();
+		object->draw();
 	}
 }
 


Commit: cabcd496077a949ddf5cbd3a2f3bfa788212bf9c
    https://github.com/scummvm/scummvm/commit/cabcd496077a949ddf5cbd3a2f3bfa788212bf9c
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:51+02:00

Commit Message:
ALCACHOFA: Fix Drop kernel calls with nullptr item

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


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 4b43948f14b..32973624829 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -893,12 +893,14 @@ void MainCharacter::pickup(const String &name, bool putInHand) {
 }
 
 void MainCharacter::drop(const Common::String &name) {
-	auto item = getItemByName(name);
-	if (item == nullptr)
-		error("Tried to drop unknown item: %s", name.c_str());
-	item->toggle(false);
+	if (!name.empty()) {
+		auto item = getItemByName(name);
+		if (item == nullptr)
+			error("Tried to drop unknown item: %s", name.c_str());
+		item->toggle(false);
+	}
 	if (g_engine->player().activeCharacter() == this) {
-		// TODO: Clear held item for drop
+		g_engine->player().heldItem() = nullptr;
 		g_engine->world().inventory().updateItemsByActiveCharacter();
 	}
 }
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 380304b6c1e..2ef9964be67 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -400,7 +400,7 @@ private:
 		auto entry = getArg(argI);
 		if (entry._type != StackEntryType::String)
 			error("Expected string in argument %u for kernel call", argI);
-		return _script._strings->data() + entry._index;
+		return &_script._strings[entry._index];
 	}
 
 	int32 getNumberOrStringArg(uint argI) {
@@ -408,10 +408,20 @@ private:
 		// as it will be interpreted as a boolean we only care about == 0 / != 0
 		auto entry = getArg(argI);
 		if (entry._type != StackEntryType::Number && entry._type != StackEntryType::String)
-			error("Expected number of string in argument %u for kernel call", argI);
+			error("Expected number or string in argument %u for kernel call", argI);
 		return entry._number;
 	}
 
+	const char *getOptionalStringArg(uint argI) {
+		// another special case: a string that may be zero which is passed as number
+		auto entry = getArg(argI);
+		if (entry._type == StackEntryType::String)
+			return &_script._strings[entry._index];
+		if (entry._type == StackEntryType::Number && entry._number == 0)
+			return nullptr;
+		error("Expected optional string in argument %u for kernel call", argI);
+	}
+
 	MainCharacter &relatedCharacter() {
 		if (process().character() == MainCharacterKind::None)
 			error("Script tried to use character from non-character-related process");
@@ -669,7 +679,7 @@ private:
 			return TaskReturn::finish(1);
 		case ScriptKernelTask::CharacterDrop: {
 			auto &character = g_engine->world().getMainCharacterByKind((MainCharacterKind)getNumberArg(1));
-			character.drop(getStringArg(0));
+			character.drop(getOptionalStringArg(0));
 			return TaskReturn::finish(1);
 		}
 		case ScriptKernelTask::ClearInventory:


Commit: 9e2d1cfbc16aa60a6a16bf30c00f5dd576103e89
    https://github.com/scummvm/scummvm/commit/9e2d1cfbc16aa60a6a16bf30c00f5dd576103e89
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:51+02:00

Commit Message:
ALCACHOFA: Fix a couple corner cases

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


diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index d6a0cad019f..4bdcc44e4aa 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -191,7 +191,8 @@ void AnimationBase::loadMissingAnimation() {
 	// only allow missing animations we know are faulty in the original game
 	if (!_fileName.equalsIgnoreCase("ANIMACION.AN0") &&
 		!_fileName.equalsIgnoreCase("DESPACHO_SUPER2_OL_SOMBRAS2.AN0") &&
-		!_fileName.equalsIgnoreCase("PP_MORTA.AN0"))
+		!_fileName.equalsIgnoreCase("PP_MORTA.AN0") &&
+		!_fileName.equalsIgnoreCase("DESPACHO_SUPER2___FONDO_PP_SUPER.AN0"))
 		error("Could not open animation %s", _fileName.c_str());
 
 	// otherwise setup a functioning but empty animation
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index f8ff4192b56..e5265abf315 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -554,7 +554,8 @@ void World::toggleObject(MainCharacterKind character, const Common::String &objN
 	if (object == nullptr)
 		object = getObjectByNameFromAnyRoom(objName);
 	if (object == nullptr)
-		error("Tried to toggle unknown object: %s", objName.c_str());
+		warning("Tried to toggle unknown object: %s", objName.c_str());
+		// I would have liked an error for this, but original inconsistencies...
 	else
 		object->toggle(isEnabled);
 }
diff --git a/engines/alcachofa/shape.cpp b/engines/alcachofa/shape.cpp
index 5ff9a856ebd..9fc5f7e670d 100644
--- a/engines/alcachofa/shape.cpp
+++ b/engines/alcachofa/shape.cpp
@@ -133,7 +133,6 @@ Point Polygon::closestPointTo(const Common::Point& query, float &distanceSqr) co
 			}
 		}		
 	}
-	assert(contains(bestPoint));
 	return bestPoint;
 }
 
diff --git a/engines/alcachofa/sounds.cpp b/engines/alcachofa/sounds.cpp
index ec6ad549036..4eb45275469 100644
--- a/engines/alcachofa/sounds.cpp
+++ b/engines/alcachofa/sounds.cpp
@@ -124,11 +124,17 @@ static AudioStream *openAudio(const String &fileName) {
 	if (file->open(path.c_str()))
 		return makeWAVStream(file, DisposeAfterUse::YES);
 	delete file;
+
+	// Ignore the known, original wrong filenames given, report the rest
+	if (fileName == "CHAS")
+		return nullptr;
 	error("Could not open audio file: %s", fileName.c_str());
 }
 
 SoundID Sounds::playSoundInternal(const String &fileName, byte volume, Mixer::SoundType type) {
 	AudioStream *stream = openAudio(fileName);
+	if (stream == nullptr)
+		return UINT32_MAX;
 	SoundHandle handle;
 	_mixer->playStream(type, &handle, stream, -1, volume);
 	SoundID id = _nextID++;


Commit: b5c5cfaef9159eb93fc7579f8478d6db1c2671e2
    https://github.com/scummvm/scummvm/commit/b5c5cfaef9159eb93fc7579f8478d6db1c2671e2
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:51+02:00

Commit Message:
ALCACHOFA: Simplify and fix Player::changeRoom

Fixes invalid graphic object error rata_TIA in SERVICIOS_TIA

Changed paths:
    engines/alcachofa/player.cpp


diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index a873dd870a7..d6caf22e61f 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -103,39 +103,39 @@ void Player::changeRoom(const Common::String &targetRoomName, bool resetCamera)
 	debug("Change room to %s", targetRoomName.c_str());
 
 	// 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
+	// Instead we just keep resources loaded for all global rooms and during inventory/room transitions
+
 	if (targetRoomName.equalsIgnoreCase("SALIR")) {
 		_currentRoom = nullptr;
-		return;
+		return; // exiting game entirely
 	}
 
-	Room &inventory = g_engine->world().inventory();
-	bool keepResources;
-	if (_currentRoom == &inventory)
-		keepResources = _roomBeforeInventory != nullptr && _roomBeforeInventory->name().equalsIgnoreCase(targetRoomName);
-	else {
-		keepResources = _currentRoom != nullptr && _currentRoom->name().equalsIgnoreCase(targetRoomName);
-	}
 	_roomBeforeInventory = nullptr;
-	if (targetRoomName.equalsIgnoreCase("inventario")) {
-		keepResources = true;
-		_roomBeforeInventory = _currentRoom;
-	}
-
-	if (!keepResources && _currentRoom != nullptr) {
+	if (_currentRoom != nullptr) {
 		g_engine->scheduler().killProcessByName("ACTUALIZAR_" + _currentRoom->name());
-		_currentRoom->freeResources();
+
+		bool keepResources =
+			_currentRoom->name().equalsIgnoreCase(targetRoomName) ||
+			_currentRoom->name().equalsIgnoreCase("inventario");
+		if (targetRoomName.equalsIgnoreCase("inventario")) {
+			keepResources = true;
+			_roomBeforeInventory = _currentRoom;
+		}
+		if (!keepResources)
+			_currentRoom->freeResources();
 	}
+
 	_currentRoom = g_engine->world().getRoomByName(targetRoomName);
 	if (_currentRoom == nullptr)
 		error("Invalid room name: %s", targetRoomName.c_str());
 
 	if (!_didLoadGlobalRooms) {
 		_didLoadGlobalRooms = true;
-		inventory.loadResources();
+		g_engine->world().inventory().loadResources();
 		g_engine->world().globalRoom().loadResources();
 	}
-	if (!keepResources)
-		_currentRoom->loadResources();
+	_currentRoom->loadResources(); // if we kept resources we loop over a couple noops, that is fine.
 
 	if (resetCamera)
 		g_engine->camera().resetRotationAndScale();


Commit: ce65cf96c2c1f726b4aa85b5ab460d6f17df2daa
    https://github.com/scummvm/scummvm/commit/ce65cf96c2c1f726b4aa85b5ab460d6f17df2daa
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:51+02:00

Commit Message:
ALCACHOFA: Workaround bug with invalid PUT target objecgt

Changed paths:
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 2ef9964be67..2a2b8f24a56 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -588,11 +588,16 @@ private:
 			auto characterObject = g_engine->world().getObjectByName(process().character(), getStringArg(0));
 			auto character = dynamic_cast<WalkingCharacter *>(characterObject);
 			if (character == nullptr)
-				error("Script tried to make invalid character go: %s", getStringArg(0));
-			auto targetObject = g_engine->world().getObjectByName(process().character(), getStringArg(1));
-			auto target = dynamic_cast<PointObject *>(targetObject);
+				error("Script tried to make invalid character put: %s", getStringArg(0));
+			auto target = dynamic_cast<PointObject *>(g_engine->world().getObjectByName(process().character(), getStringArg(1)));
+			if (target == nullptr && !scumm_stricmp("A_Poblado_Indio", getStringArg(1))) {
+				// An original bug, 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
+				target = dynamic_cast<PointObject *>(g_engine->world().getObjectByName(process().character(), "A_Poblado_Indio1"));
+			}
 			if (target == nullptr)
-				error("Script tried to make character go to invalid object %s", getStringArg(1));
+				error("Script tried to make character put to invalid object %s", getStringArg(1));
 			character->setPosition(target->position());
 			return TaskReturn::finish(1);
 		}


Commit: 71988c9edf298d17fa0c03b7a1313734c47dd5cc
    https://github.com/scummvm/scummvm/commit/71988c9edf298d17fa0c03b7a1313734c47dd5cc
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:51+02:00

Commit Message:
ALCACHOFA: Workaround bugs in CASA_FREDDY_ARRIBA

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


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 32973624829..a5028b8387f 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -271,10 +271,14 @@ void Character::onClick() {
 
 void Character::trigger(const char *action) {
 	g_engine->player().activeCharacter()->stopWalking(_interactionDirection);
-	if (scumm_stricmp(action, "iSABANA") == 0 && // Original hack probably to fix some bug :)
+	if (scumm_stricmp(action, "iSABANA") == 0 &&
 		dynamic_cast<MainCharacter *>(this) != nullptr &&
-		room()->name().equalsIgnoreCase("CASA_FREDDY_ARRIBA"))
-		error("Not sure what *should* happen. How do we get here?");
+		!room()->name().equalsIgnoreCase("CASA_FREDDY_ARRIBA")) {
+		// An original hack to check that we use the bed sheet on the main character only in the correct room
+		// There *is* another script variable (es_casa_freddy) that should check this
+		// but, I guess, Alcachofa Soft found a corner case where this does not work?
+		return;
+	}
 	g_engine->player().triggerObject(this, action);
 }
 
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 2a2b8f24a56..1467914ea46 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -422,6 +422,13 @@ private:
 		error("Expected optional string in argument %u for kernel call", argI);
 	}
 
+	template<class TObject = ObjectBase>
+	TObject *getObjectArg(uint argI) {
+		const char *const name = getStringArg(argI);
+		auto *object = g_engine->world().getObjectByName(process().character(), name);
+		return dynamic_cast<TObject*>(object);
+	}
+
 	MainCharacter &relatedCharacter() {
 		if (process().character() == MainCharacterKind::None)
 			error("Script tried to use character from non-character-related process");
@@ -567,12 +574,10 @@ private:
 			return TaskReturn::finish(1);
 		}
 		case ScriptKernelTask::Go: {
-			auto characterObject = g_engine->world().getObjectByName(process().character(), getStringArg(0));
-			auto character = dynamic_cast<WalkingCharacter *>(characterObject);
+			auto character = getObjectArg<WalkingCharacter>(0);
 			if (character == nullptr)
 				error("Script tried to make invalid character go: %s", getStringArg(0));
-			auto targetObject = g_engine->world().getObjectByName(process().character(), getStringArg(1));
-			auto target = dynamic_cast<PointObject *>(targetObject);
+			auto target= getObjectArg<PointObject>(1);
 			if (target == nullptr)
 				error("Script tried to make character go to invalid object %s", getStringArg(1));
 			character->walkTo(target->position());
@@ -588,14 +593,10 @@ private:
 			auto characterObject = g_engine->world().getObjectByName(process().character(), getStringArg(0));
 			auto character = dynamic_cast<WalkingCharacter *>(characterObject);
 			if (character == nullptr)
-				error("Script tried to make invalid character put: %s", getStringArg(0));
+				error("Script tried to put invalid character: %s", getStringArg(0));
 			auto target = dynamic_cast<PointObject *>(g_engine->world().getObjectByName(process().character(), getStringArg(1)));
-			if (target == nullptr && !scumm_stricmp("A_Poblado_Indio", getStringArg(1))) {
-				// An original bug, 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
-				target = dynamic_cast<PointObject *>(g_engine->world().getObjectByName(process().character(), "A_Poblado_Indio1"));
-			}
+			if (target == nullptr && !exceptionsForPut(target, getStringArg(1)))
+				return TaskReturn::finish(2);
 			if (target == nullptr)
 				error("Script tried to make character put to invalid object %s", getStringArg(1));
 			character->setPosition(target->position());
@@ -823,6 +824,36 @@ private:
 		assert(character.semaphore().isReleased()); // this process should be the last to hold a lock if at all...
 	}
 
+	/**
+	 * @brief Check for original bugs related to the Put kernel call and handle them 
+	 * @param target An out reference to the point object (maybe we can find an alternative one)
+	 * @param targetName The given name of the target object
+	 * @return false if the put kernel call should be ignored, true if we set target and want to continue with the kernel call
+	 */
+	bool exceptionsForPut(PointObject *&target, const char *targetName) {
+		assert(target == nullptr); // if not, why did we check for exceptions?
+
+		if (!scumm_stricmp("A_Poblado_Indio", targetName)) {
+			// 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
+			target = dynamic_cast<PointObject *>(g_engine->world().getObjectByName(process().character(), "A_Poblado_Indio1"));
+		}
+
+		if (!scumm_stricmp("PUNTO_VENTANA", targetName)) {
+			// The object is in the previous, now inactive room.
+			// Luckily Mortadelo already is at that point so not further action required
+			return false;
+		}
+
+		if (!scumm_stricmp("Puerta_Casa_Freddy_Intermedia", targetName)) {
+			// Another case of a door being cast into a PointObject
+			return false;
+		}
+
+		return true;
+	}
+
 	Script &_script;
 	Stack<StackEntry> _stack;
 	String _name;


Commit: 7b08ff53d7e7ebb793d6d805f64559e36bbfff9b
    https://github.com/scummvm/scummvm/commit/7b08ff53d7e7ebb793d6d805f64559e36bbfff9b
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:51+02:00

Commit Message:
ALCACHOFA: Workaround bugs in HABITACION_DRACULA and MOTEL_ENTRADA

Changed paths:
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 1467914ea46..84ae89855f5 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -548,6 +548,8 @@ private:
 		case ScriptKernelTask::Animate: {
 			auto object = g_engine->world().getObjectByName(process().character(), getStringArg(0));
 			auto graphicObject = dynamic_cast<GraphicObject *>(object);
+			if (graphicObject == nullptr && !scumm_stricmp("EXPLOSION DISFRAZ", getStringArg(0)))
+				return TaskReturn::finish(1);
 			if (graphicObject == nullptr)
 				error("Script tried to animate invalid graphic object %s", getStringArg(0));
 			if (getNumberOrStringArg(1)) {
@@ -632,6 +634,10 @@ private:
 			if (character == nullptr)
 				error("Invalid character name: %s", getStringArg(0));
 			auto *animObject = g_engine->world().getObjectByName(getStringArg(1));
+			if (animObject == nullptr && (
+				!scumm_stricmp("COGE F DCH", getStringArg(1)) ||
+				!scumm_stricmp("CHIQUITO_IZQ", getStringArg(1))))
+				return TaskReturn::finish(2); // original bug in MOTEL_ENTRADA
 			if (animObject == nullptr)
 				error("Invalid animate object name: %s", getStringArg(1));
 			return TaskReturn::waitFor(character->animate(process(), animObject));


Commit: a2266bd22c3bedd5835346cec15634d6f42c7b75
    https://github.com/scummvm/scummvm/commit/a2266bd22c3bedd5835346cec15634d6f42c7b75
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:51+02:00

Commit Message:
ALCACHOFA: Fix InteractableObject being door target

Changed paths:
    engines/alcachofa/player.cpp


diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index d6caf22e61f..4f822a5fcf9 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -202,11 +202,15 @@ struct DoorTask : public Task {
 		if (_targetRoom == nullptr)
 			error("Invalid door target room: %s", door->targetRoom().c_str());
 
-		_targetDoor = dynamic_cast<Door *>(_targetRoom->getObjectByName(door->targetObject()));
-		if (_targetDoor == nullptr)
+		_targetObject = dynamic_cast<InteractableObject *>(_targetRoom->getObjectByName(door->targetObject()));
+		if (_targetObject == nullptr)
 			error("Invalid door target door: %s", door->targetObject().c_str());
+		auto targetDoor = dynamic_cast<const Door *>(_targetObject);
+		_targetDirection = targetDoor == nullptr
+			? _targetObject->interactionDirection()
+			: targetDoor->characterDirection();
 
-		process.name() = String::format("Door to %s %s", _targetRoom->name().c_str(), _targetDoor->name().c_str());
+		process.name() = String::format("Door to %s %s", _targetRoom->name().c_str(), _targetObject->name().c_str());
 	}
 
 	virtual TaskReturn run() {
@@ -217,11 +221,11 @@ struct DoorTask : public Task {
 		_player.changeRoom(_targetRoom->name(), true);
 
 		if (_targetRoom->fixedCameraOnEntering())
-			g_engine->camera().setPosition(as2D(_targetDoor->interactionPoint()));
+			g_engine->camera().setPosition(as2D(_targetObject->interactionPoint()));
 		else {
 			_character->room() = _targetRoom;
-			_character->setPosition(_targetDoor->interactionPoint());
-			_character->stopWalking(_targetDoor->characterDirection());
+			_character->setPosition(_targetObject->interactionPoint());
+			_character->stopWalking(_targetDirection);
 			g_engine->camera().setFollow(_character, true);
 		}
 
@@ -239,7 +243,9 @@ struct DoorTask : public Task {
 
 private:
 	FakeLock _lock;
-	const Door *_sourceDoor, *_targetDoor;
+	const Door *_sourceDoor;
+	const InteractableObject *_targetObject;
+	Direction _targetDirection;
 	Room *_targetRoom;
 	MainCharacter *_character;
 	Player &_player;


Commit: eced79308fb21e1b7172bb212ec0514d99e290ae
    https://github.com/scummvm/scummvm/commit/eced79308fb21e1b7172bb212ec0514d99e290ae
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:51+02:00

Commit Message:
ALCACHOFA: Use the helper functions more in the kernel procs

Changed paths:
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 84ae89855f5..e26f517638a 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -546,8 +546,7 @@ private:
 			g_engine->world().toggleObject(process().character(), getStringArg(0), false);
 			return TaskReturn::finish(0);
 		case ScriptKernelTask::Animate: {
-			auto object = g_engine->world().getObjectByName(process().character(), getStringArg(0));
-			auto graphicObject = dynamic_cast<GraphicObject *>(object);
+			auto graphicObject = getObjectArg<GraphicObject>(0);
 			if (graphicObject == nullptr && !scumm_stricmp("EXPLOSION DISFRAZ", getStringArg(0)))
 				return TaskReturn::finish(1);
 			if (graphicObject == nullptr)
@@ -563,8 +562,7 @@ private:
 
 		// character control / animation
 		case ScriptKernelTask::StopAndTurn: {
-			auto object = g_engine->world().getObjectByName(process().character(), getStringArg(0));
-			auto character = dynamic_cast<WalkingCharacter *>(object);
+			auto character = getObjectArg<WalkingCharacter>(0);
 			if (character == nullptr)
 				error("Script tried to stop-and-turn unknown character");
 			else
@@ -592,11 +590,10 @@ private:
 				: TaskReturn::waitFor(character->waitForArrival(process()));
 		}
 		case ScriptKernelTask::Put: {
-			auto characterObject = g_engine->world().getObjectByName(process().character(), getStringArg(0));
-			auto character = dynamic_cast<WalkingCharacter *>(characterObject);
+			auto character = getObjectArg<WalkingCharacter>(0);
 			if (character == nullptr)
 				error("Script tried to put invalid character: %s", getStringArg(0));
-			auto target = dynamic_cast<PointObject *>(g_engine->world().getObjectByName(process().character(), getStringArg(1)));
+			auto target = getObjectArg<PointObject>(1);
 			if (target == nullptr && !exceptionsForPut(target, getStringArg(1)))
 				return TaskReturn::finish(2);
 			if (target == nullptr)
@@ -605,7 +602,7 @@ private:
 			return TaskReturn::finish(1);
 		}
 		case ScriptKernelTask::ChangeCharacterRoom: {
-			auto *character = dynamic_cast<Character *>(g_engine->world().globalRoom().getObjectByName(getStringArg(0)));
+			auto *character = getObjectArg<Character>(0);
 			if (character == nullptr)
 				error("Invalid character name: %s", getStringArg(0));
 			auto *targetRoom = g_engine->world().getRoomByName(getStringArg(1));
@@ -616,7 +613,7 @@ private:
 			return TaskReturn::finish(1);
 		}
 		case ScriptKernelTask::LerpCharacterLodBias: {
-			auto *character = dynamic_cast<Character *>(g_engine->world().globalRoom().getObjectByName(getStringArg(0)));
+			auto *character = getObjectArg<Character>(0);
 			if (character == nullptr)
 				error("Invalid character name: %s", getStringArg(0));
 			float targetLodBias = getNumberArg(1) * 0.01f;
@@ -630,10 +627,10 @@ private:
 				return TaskReturn::waitFor(character->lerpLodBias(process(), targetLodBias, durationMs));
 		}
 		case ScriptKernelTask::AnimateCharacter: {
-			auto *character = dynamic_cast<Character *>(g_engine->world().getObjectByName(getStringArg(0)));
+			auto *character = getObjectArg<Character>(0);
 			if (character == nullptr)
 				error("Invalid character name: %s", getStringArg(0));
-			auto *animObject = g_engine->world().getObjectByName(getStringArg(1));
+			auto *animObject = getObjectArg(1);
 			if (animObject == nullptr && (
 				!scumm_stricmp("COGE F DCH", getStringArg(1)) ||
 				!scumm_stricmp("CHIQUITO_IZQ", getStringArg(1))))
@@ -643,14 +640,14 @@ private:
 			return TaskReturn::waitFor(character->animate(process(), animObject));
 		}
 		case ScriptKernelTask::AnimateTalking: {
-			auto *character = dynamic_cast<Character *>(g_engine->world().getObjectByName(getStringArg(0)));
+			auto *character = getObjectArg<Character>(0);
 			if (character == nullptr)
 				error("Invalid character name: %s", getStringArg(0));
 			const char *talkObjectName = getStringArg(1);
 			ObjectBase *talkObject = nullptr;
 			if (talkObjectName != nullptr && *talkObjectName)
 			{
-				talkObject = g_engine->world().getObjectByName(talkObjectName);
+				talkObject = getObjectArg(1);
 				if (talkObject == nullptr)
 					error("Invalid talk object name: %s", talkObjectName);
 			}
@@ -661,21 +658,21 @@ private:
 			const char *characterName = getStringArg(0);
 			int32 dialogId = getNumberArg(1);
 			if (strncmp(characterName, "MENU_", 5) == 0) {
-				g_engine->world().getMainCharacterByKind(process().character()).addDialogLine(dialogId);
+				relatedCharacter().addDialogLine(dialogId);
 				return TaskReturn::finish(1);
 			}
 			Character *_character = strcmp(characterName, "AMBOS") == 0
-				? &g_engine->world().getMainCharacterByKind(process().character())
-				: dynamic_cast<Character *>(g_engine->world().getObjectByName(characterName));
+				? &relatedCharacter()
+				: getObjectArg<Character>(0);
 			if (_character == nullptr)
 				error("Invalid character for sayText: %s", characterName);
 			return TaskReturn::waitFor(_character->sayText(process(), dialogId));
 		};
 		case ScriptKernelTask::SetDialogLineReturn:
-			g_engine->world().getMainCharacterByKind(process().character()).setLastDialogReturnValue(getNumberArg(0));
+			relatedCharacter().setLastDialogReturnValue(getNumberArg(0));
 			return TaskReturn::finish(0);
 		case ScriptKernelTask::DialogMenu:
-			return TaskReturn::waitFor(g_engine->world().getMainCharacterByKind(process().character()).dialogMenu(process()));
+			return TaskReturn::waitFor(relatedCharacter().dialogMenu(process()));
 
 		// Inventory control
 		case ScriptKernelTask::Pickup:
@@ -739,8 +736,7 @@ private:
 		case ScriptKernelTask::LerpCamToObjectKeepingZ: {
 			if (!process().isActiveForPlayer())
 				return TaskReturn::finish(0); // contrary to ...ResettingZ this one does not delay if not active
-			auto object = g_engine->world().getObjectByName(process().character(), getStringArg(0));
-			auto pointObject = dynamic_cast<PointObject *>(object);
+			auto pointObject = getObjectArg<PointObject>(0);
 			if (pointObject == nullptr)
 				error("Invalid target object for LerpCamToObjectKeepingZ: %s", getStringArg(0));
 			return TaskReturn::waitFor(g_engine->camera().lerpPos(process(),
@@ -750,8 +746,7 @@ private:
 		case ScriptKernelTask::LerpCamToObjectResettingZ: {
 			if (!process().isActiveForPlayer())
 				return TaskReturn::waitFor(delay(getNumberArg(1)));
-			auto object = g_engine->world().getObjectByName(process().character(), getStringArg(0));
-			auto pointObject = dynamic_cast<PointObject *>(object);
+			auto pointObject = getObjectArg<PointObject>(0);
 			if (pointObject == nullptr)
 				error("Invalid target object for LerpCamToObjectResettingZ: %s", getStringArg(0));
 			return TaskReturn::waitFor(g_engine->camera().lerpPos(process(),
@@ -761,8 +756,7 @@ private:
 		case ScriptKernelTask::LerpCamToObjectWithScale: {
 			if (!process().isActiveForPlayer())
 				return TaskReturn::waitFor(delay(getNumberArg(2)));
-			auto object = g_engine->world().getObjectByName(process().character(), getStringArg(0));
-			auto pointObject = dynamic_cast<PointObject *>(object);
+			auto pointObject = getObjectArg<PointObject>(0);
 			if (pointObject == nullptr)
 				error("Invalid target object for LerpCamToObjectWithScale: %s", getStringArg(0));
 			return TaskReturn::waitFor(g_engine->camera().lerpPosScale(process(),


Commit: 72391731e51ed3265b198002fcea2bf337bf3039
    https://github.com/scummvm/scummvm/commit/72391731e51ed3265b198002fcea2bf337bf3039
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:51+02:00

Commit Message:
ALCACHOFA: Fix entering ESTOMAGO and DINOSAURIO

Changed paths:
    engines/alcachofa/camera.cpp
    engines/alcachofa/console.cpp
    engines/alcachofa/general-objects.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/rooms.cpp


diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index 3be4477e706..5eb70636814 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -114,7 +114,7 @@ void minmax(Vector3d &min, Vector3d &max, Vector3d val)
 
 Vector3d Camera::setAppliedCenter(Vector3d center) {
 	setupMatricesAround(center);
-	if (g_engine->script().variable("EncuadrarCamara") || true) {
+	if (g_engine->script().variable("EncuadrarCamara")) {
 		const float screenW = g_system->getWidth(), screenH = g_system->getHeight();
 		Vector3d min, max;
 		min = max = transform2Dto3D(Vector3d(0, 0, _roomScale));
@@ -171,6 +171,7 @@ void Camera::update() {
 	if (_catchUp && _cur._followTarget != nullptr) {
 		for (int i = 0; i < 4; i++)
 			updateFollowing(50.0f);
+		_catchUp = false;
 	}
 	else
 		updateFollowing(deltaTime);
diff --git a/engines/alcachofa/console.cpp b/engines/alcachofa/console.cpp
index 247035a44a4..7a2941151a9 100644
--- a/engines/alcachofa/console.cpp
+++ b/engines/alcachofa/console.cpp
@@ -251,7 +251,7 @@ bool Console::cmdTeleport(int argc, const char **args)
 	if (argc > 1)
 	{
 		char *end = nullptr;
-		param = (int32)strtol(args[2], &end, 10);
+		param = (int32)strtol(args[1], &end, 10);
 		if (end == nullptr || *end != '\0')
 		{
 			debugPrintf("Character kind can only be integer");
diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index 98e75fa0c8c..7a4fb5c17dd 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -102,7 +102,7 @@ GraphicObject::GraphicObject(Room *room, const char *name)
 }
 
 void GraphicObject::draw() {
-	if (!isEnabled())
+	if (!isEnabled() || !_graphic.hasAnimation())
 		return;
 	const BlendMode blendMode = _type == GraphicObjectType::Effect
 		? BlendMode::Alpha
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 4bdcc44e4aa..c458aad26bf 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -192,7 +192,8 @@ void AnimationBase::loadMissingAnimation() {
 	if (!_fileName.equalsIgnoreCase("ANIMACION.AN0") &&
 		!_fileName.equalsIgnoreCase("DESPACHO_SUPER2_OL_SOMBRAS2.AN0") &&
 		!_fileName.equalsIgnoreCase("PP_MORTA.AN0") &&
-		!_fileName.equalsIgnoreCase("DESPACHO_SUPER2___FONDO_PP_SUPER.AN0"))
+		!_fileName.equalsIgnoreCase("DESPACHO_SUPER2___FONDO_PP_SUPER.AN0") &&
+		!_fileName.equalsIgnoreCase("ESTOMAGO.AN0"))
 		error("Could not open animation %s", _fileName.c_str());
 
 	// otherwise setup a functioning but empty animation
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index e5265abf315..eaf33ebd210 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -218,8 +218,16 @@ void Room::updateInteraction() {
 void Room::updateRoomBounds() {
 	auto background = getObjectByName("Background");
 	auto graphic = background == nullptr ? nullptr : background->graphic();
-	if (graphic != nullptr)
-		g_engine->camera().setRoomBounds(graphic->animation().imageSize(0), graphic->scale());
+	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 but I find
+		 * this very unstable. Instead a fixed value is used
+		 */
+		if (bgSize == Point(0, 0))
+			bgSize = Point(1024, 768);
+		g_engine->camera().setRoomBounds(bgSize, graphic->scale());
+	}
 }
 
 void Room::updateObjects() {


Commit: 8e7069d3664801d7641de02ff7a3330bcea03159
    https://github.com/scummvm/scummvm/commit/8e7069d3664801d7641de02ff7a3330bcea03159
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:51+02:00

Commit Message:
ALCACHOFA: Fix return value of kernel calls

Changed paths:
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index e26f517638a..cba7f454c22 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -187,16 +187,35 @@ struct ScriptTask : public Task {
 	virtual TaskReturn run() override {
 		if (_isFirstExecution || _returnsFromKernelCall)
 			setCharacterVariables();
-		if (_returnsFromKernelCall)
+		if (_returnsFromKernelCall) {
+			// 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() && _script._instructions[_pc]._op == ScriptOp::PopN);
+			popN(_script._instructions[_pc++]._arg);
 			pushNumber(process().returnValue());
+		}
 		_isFirstExecution = _returnsFromKernelCall = false;
 
 		while (true) {
 			if (_pc >= _script._instructions.size())
 				error("Script process reached instruction out-of-bounds");
 			const auto &instruction = _script._instructions[_pc++];
-			debugC(SCRIPT_DEBUG_LVL_INSTRUCTIONS, kDebugScript, "%u: %5u %-12s %8d",
-				process().pid(), _pc - 1, ScriptOpNames[(int)instruction._op], instruction._arg);
+			if (debugChannelSet(SCRIPT_DEBUG_LVL_INSTRUCTIONS, kDebugScript)) {
+				debugN("%u: %5u %-12s %8d Stack: ",
+					process().pid(), _pc - 1, ScriptOpNames[(int)instruction._op], instruction._arg);
+				if (_stack.empty())
+					debug("empty");
+				else {
+					const auto& top = _stack.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;
+					}
+				}
+			}
 
 			switch (instruction._op) {
 			case ScriptOp::Nop: break;
@@ -215,10 +234,7 @@ struct ScriptTask : public Task {
 				pushNumber(popVariable());
 				break;
 			case ScriptOp::PopN:
-				if (instruction._arg < 0 || (uint)instruction._arg > _stack.size())
-					error("Script tried to pop more entries than are available on the stack");
-				for (int32 i = 0; i < instruction._arg; i++)
-					_stack.pop();
+				popN(instruction._arg);
 				break;
 			case ScriptOp::Store: {
 				int32 value = popNumber();
@@ -383,6 +399,13 @@ private:
 		return entry._index;
 	}
 
+	void popN(int32 count) {
+		if (count < 0 || (uint)count > _stack.size())
+			error("Script tried to pop more entries than are available on the stack");
+		for (int32 i = 0; i < count; i++)
+			_stack.pop();
+	}
+
 	StackEntry getArg(uint argI) {
 		if (_stack.size() < argI + 1)
 			error("Script did not supply enough arguments for kernel call");


Commit: 171e0af2afb165346c17fa91925ae358cb8e0857
    https://github.com/scummvm/scummvm/commit/171e0af2afb165346c17fa91925ae358cb8e0857
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:52+02:00

Commit Message:
ALCACHOFA: Fix ScriptTimerTask

Changed paths:
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index cba7f454c22..bb8ae236818 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -125,12 +125,16 @@ struct ScriptTimerTask : public Task {
 
 	virtual TaskReturn run() override {
 		TASK_BEGIN;
-		if (_durationSec >= (int32)((g_system->getMillis() - g_engine->script()._scriptTimer) / 1000) &&
-			g_engine->script().variable("SeHaPulsadoRaton"))
-			_result = 0;
-		
-		// TODO: Add network behavior for script timer
+		{
+			uint32 timeSinceTimer = g_engine->script()._scriptTimer == 0 ? 0
+				: (g_system->getMillis() - g_engine->script()._scriptTimer) / 1000;
+			if (_durationSec >= (int32)timeSinceTimer)
+				_result = g_engine->script().variable("SeHaPulsadoRaton") ? 0 : 2;
+			else
+				_result = 1;
+		}
 		TASK_YIELD;
+		TASK_RETURN(_result);
 		TASK_END;
 	}
 


Commit: f8e15ea9f43b0fde6d6a3273a1ed9f213f5ad79a
    https://github.com/scummvm/scummvm/commit/f8e15ea9f43b0fde6d6a3273a1ed9f213f5ad79a
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:52+02:00

Commit Message:
ALCACHOFA: Clear screen in order to fix ESTOMAGO

Changed paths:
    engines/alcachofa/graphics-opengl.cpp
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/graphics-opengl.cpp b/engines/alcachofa/graphics-opengl.cpp
index 7b1682656eb..f131bdcca15 100644
--- a/engines/alcachofa/graphics-opengl.cpp
+++ b/engines/alcachofa/graphics-opengl.cpp
@@ -136,7 +136,8 @@ public:
 		_currentTexture = nullptr;
 		_currentBlendMode = (BlendMode)-1;
 
-		// Do not clear the screen as the engine sometimes relies on the old frame to be reused
+		GL_CALL(glClearColor(0.0f, 0.0f, 0.0f, 1.0f));
+		GL_CALL(glClear(GL_COLOR_BUFFER_BIT));
 	}
 
 	virtual void end() override {
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index bb8ae236818..1a7b8b80e9b 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -133,7 +133,7 @@ struct ScriptTimerTask : public Task {
 			else
 				_result = 1;
 		}
-		TASK_YIELD;
+		TASK_YIELD; // Wait a frame to not produce an endless loop
 		TASK_RETURN(_result);
 		TASK_END;
 	}


Commit: 9bf99c3f6aed9b67752a50c3109e68bbeebc79e8
    https://github.com/scummvm/scummvm/commit/9bf99c3f6aed9b67752a50c3109e68bbeebc79e8
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:52+02:00

Commit Message:
ALCACHOFA: Fix text line array being too small

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


diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 78749514ce2..3795d4b486f 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -371,7 +371,7 @@ public:
 	virtual void draw() override;
 
 private:
-	static constexpr uint kMaxLines = 8;
+	static constexpr uint kMaxLines = 12;
 	using TextLine = Common::Span<const byte>; ///< byte to convert 128+ characters to image indices
 
 	Font &_font;
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index eaf33ebd210..95ebec3eead 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -650,7 +650,7 @@ static void loadEncryptedFile(const char *path, Array<char> &output) {
 	File file;
 	if (!file.open(path))
 		error("Could not open text file %s", path);
-	output.resize(file.size() - 5 + 1);
+	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();
@@ -659,7 +659,7 @@ static void loadEncryptedFile(const char *path, Array<char> &output) {
 		error("Could not read text file body");
 	for (auto &ch : output)
 		ch ^= key;
-	output.back() = ' '; // one for good measure and a zero-terminator
+	output.back() = '\0'; // one for good measure and a zero-terminator
 }
 
 static char *trimTrailing(char *start, char *end) {


Commit: 178faf9b47e3d6a51c1258331c4cb086ec56129c
    https://github.com/scummvm/scummvm/commit/178faf9b47e3d6a51c1258331c4cb086ec56129c
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:52+02:00

Commit Message:
ALCACHOFA: More exceptions to make game completable

Changed paths:
    engines/alcachofa/graphics.cpp
    engines/alcachofa/player.cpp
    engines/alcachofa/sounds.cpp


diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index c458aad26bf..cf027ebf9a3 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -193,7 +193,8 @@ void AnimationBase::loadMissingAnimation() {
 		!_fileName.equalsIgnoreCase("DESPACHO_SUPER2_OL_SOMBRAS2.AN0") &&
 		!_fileName.equalsIgnoreCase("PP_MORTA.AN0") &&
 		!_fileName.equalsIgnoreCase("DESPACHO_SUPER2___FONDO_PP_SUPER.AN0") &&
-		!_fileName.equalsIgnoreCase("ESTOMAGO.AN0"))
+		!_fileName.equalsIgnoreCase("ESTOMAGO.AN0") &&
+		!_fileName.equalsIgnoreCase("CREDITOS.AN0"))
 		error("Could not open animation %s", _fileName.c_str());
 
 	// otherwise setup a functioning but empty animation
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index 4f822a5fcf9..c75defede95 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -61,13 +61,13 @@ void Player::updateCursor() {
 	else {
 		auto type = _selectedObject->cursorType();
 		switch (type) {
-		case CursorType::Point: _cursorFrameI = 0; break;
 		case CursorType::LeaveUp: _cursorFrameI = 8; break;
 		case CursorType::LeaveRight: _cursorFrameI = 10; break;
 		case CursorType::LeaveDown: _cursorFrameI = 12; break;
 		case CursorType::LeaveLeft: _cursorFrameI = 14; break;
 		case CursorType::WalkTo: _cursorFrameI = 6; break;
-		default: error("Invalid cursor type %u", (uint)type); break;
+		case CursorType::Point:
+		default: _cursorFrameI = 0; break;
 		}
 
 		if (_cursorFrameI != 0) {
diff --git a/engines/alcachofa/sounds.cpp b/engines/alcachofa/sounds.cpp
index 4eb45275469..dbc583f3ed2 100644
--- a/engines/alcachofa/sounds.cpp
+++ b/engines/alcachofa/sounds.cpp
@@ -126,7 +126,7 @@ static AudioStream *openAudio(const String &fileName) {
 	delete file;
 
 	// Ignore the known, original wrong filenames given, report the rest
-	if (fileName == "CHAS")
+	if (fileName == "CHAS" || fileName == "517")
 		return nullptr;
 	error("Could not open audio file: %s", fileName.c_str());
 }


Commit: 23f316821e33dfd664f73a6c71073ad6f0ea7214
    https://github.com/scummvm/scummvm/commit/23f316821e33dfd664f73a6c71073ad6f0ea7214
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:52+02:00

Commit Message:
ALCACHOFA: Fix out-of-bounds heap access

Changed paths:
    engines/alcachofa/rooms.cpp


diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 95ebec3eead..59c804206f3 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -670,7 +670,7 @@ static char *trimTrailing(char *start, char *end) {
 
 void World::loadLocalizedNames() {
 	loadEncryptedFile("Textos/OBJETOS.nkr", _namesChunk);
-	char *lineStart = _namesChunk.begin(), *fileEnd = _namesChunk.end();
+	char *lineStart = _namesChunk.begin(), *fileEnd = _namesChunk.end() - 1;
 	while (lineStart < fileEnd) {
 		char *lineEnd = find(lineStart, fileEnd, '\n');
 		char *keyEnd = find(lineStart, lineEnd, '#');


Commit: e9c1594ea2af867f55c3abfe822e38df2b4ab3a9
    https://github.com/scummvm/scummvm/commit/e9c1594ea2af867f55c3abfe822e38df2b4ab3a9
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:52+02:00

Commit Message:
ALCACHOFA: Fix case of input.cpp and input.h

Changed paths:
  A engines/alcachofa/input.cpp
  A engines/alcachofa/input.h
  R engines/alcachofa/Input.cpp
  R engines/alcachofa/Input.h
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/module.mk


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 682484c2efe..89c0daf9c0c 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -70,8 +70,6 @@ Common::Error AlcachofaEngine::run() {
 	_player.reset(new Player());
 	_globalUI.reset(new GlobalUI());
 
-	//_script->createProcess(MainCharacterKind::None, "Inicializar_Variables");
-	//_player->changeRoom("HORCA", true);
 	_script->createProcess(MainCharacterKind::None, "CREDITOS_INICIALES");
 	_scheduler.run();
 
diff --git a/engines/alcachofa/Input.cpp b/engines/alcachofa/input.cpp
similarity index 100%
rename from engines/alcachofa/Input.cpp
rename to engines/alcachofa/input.cpp
diff --git a/engines/alcachofa/Input.h b/engines/alcachofa/input.h
similarity index 100%
rename from engines/alcachofa/Input.h
rename to engines/alcachofa/input.h
diff --git a/engines/alcachofa/module.mk b/engines/alcachofa/module.mk
index 9c8300c66ea..71e37f99a1a 100644
--- a/engines/alcachofa/module.mk
+++ b/engines/alcachofa/module.mk
@@ -10,7 +10,7 @@ MODULE_OBJS = \
 	global-ui.cpp \
 	graphics.cpp \
 	graphics-opengl.cpp \
-	Input.cpp \
+	input.cpp \
 	metaengine.o \
 	player.cpp \
 	rooms.cpp \


Commit: e2c661a4076811b132f52e46dffc4ab122375dd5
    https://github.com/scummvm/scummvm/commit/e2c661a4076811b132f52e46dffc4ab122375dd5
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:52+02:00

Commit Message:
ALCACHOFA: Fix texture wrapping

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


diff --git a/engines/alcachofa/graphics-opengl.cpp b/engines/alcachofa/graphics-opengl.cpp
index f131bdcca15..1e32bb7a581 100644
--- a/engines/alcachofa/graphics-opengl.cpp
+++ b/engines/alcachofa/graphics-opengl.cpp
@@ -76,8 +76,7 @@ public:
 		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));
-		GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT));
-		GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT));
+		setMirrorWrap(false);
 	}
 
 	virtual ~OpenGLTexture() override {
@@ -99,11 +98,26 @@ public:
 			GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0));
 	}
 
+	void setMirrorWrap(bool wrap) {
+		if (_mirrorWrap == wrap)
+			return;
+		_mirrorWrap = wrap;
+		GLint wrapMode;
+		if (wrap)
+			wrapMode = OpenGLContext.textureMirrorRepeatSupported ? GL_MIRRORED_REPEAT : GL_REPEAT;
+		else
+			wrapMode = OpenGLContext.textureEdgeClampSupported ? GL_CLAMP_TO_EDGE : GL_CLAMP;
+
+		GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapMode));
+		GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapMode));
+	}
+
 	inline GLuint handle() const { return _handle; }
 
 private:
 	GLuint _handle;
 	bool _withMipmaps;
+	bool _mirrorWrap = true;
 };
 
 class OpenGLRenderer : public IDebugRenderer {
@@ -118,6 +132,10 @@ public:
 		GL_CALL(glDisable(GL_CULL_FACE));
 		GL_CALL(glEnable(GL_BLEND));
 		GL_CALL(glDepthMask(GL_FALSE));
+
+		if (!OpenGLContext.NPOTSupported || !OpenGLContext.textureMirrorRepeatSupported) {
+			g_system->messageBox(LogMessageType::kWarning, "Old OpenGL detected, some graphical errors will occur.");
+		}
 	}
 
 	virtual ScopedPtr<ITexture> createTexture(int32 w, int32 h, bool withMipmaps) override {
@@ -145,23 +163,24 @@ public:
 		g_system->updateScreen();
 	}
 
-	virtual void setTexture(const ITexture *texture) override {
+	virtual void setTexture(ITexture *texture) override {
 		if (texture == _currentTexture)
 			return;
 		else if (texture == nullptr) {
 			GL_CALL(glDisable(GL_TEXTURE_2D));
 			GL_CALL(glDisableClientState(GL_TEXTURE_COORD_ARRAY));
+			_currentTexture = nullptr;
 		}
 		else {
 			if (_currentTexture == nullptr) {
 				GL_CALL(glEnable(GL_TEXTURE_2D));
 				GL_CALL(glEnableClientState(GL_TEXTURE_COORD_ARRAY));
 			}
-			auto glTexture = dynamic_cast<const OpenGLTexture *>(texture);
+			auto glTexture = dynamic_cast<OpenGLTexture *>(texture);
 			assert(glTexture != nullptr);
 			GL_CALL(glBindTexture(GL_TEXTURE_2D, glTexture->handle()));
+			_currentTexture = glTexture;
 		}
-		_currentTexture = texture;
 	}
 
 	virtual void setBlendMode(BlendMode blendMode) override {
@@ -251,6 +270,10 @@ public:
 			{ texMax.getX(), texMax.getY() },
 			{ texMax.getX(), texMin.getY() }
 		};
+		if (_currentTexture != nullptr) {
+			// float equality is fine here, if it was calculated it was not a normal graphic
+			_currentTexture->setMirrorWrap(texMin != Vector2d() || texMax != Vector2d(1, 1));
+		}
 
 		float colors[] = { color.r / 255.0f, color.g / 255.0f, color.b / 255.0f, color.a / 255.0f };
 
@@ -333,7 +356,7 @@ private:
 	}
 
 	Point _resolution;
-	const ITexture *_currentTexture = nullptr;
+	OpenGLTexture *_currentTexture = nullptr;
 	BlendMode _currentBlendMode = (BlendMode)-1;
 	float _currentLodBias = 0.0f;
 };
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 3795d4b486f..d7abaabf463 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -78,7 +78,7 @@ public:
 	virtual Common::ScopedPtr<ITexture> createTexture(int32 w, int32 h, bool withMipmaps = true) = 0;
 
 	virtual void begin() = 0;
-	virtual void setTexture(const ITexture *texture) = 0;
+	virtual void setTexture(ITexture *texture) = 0;
 	virtual void setBlendMode(BlendMode blendMode) = 0;
 	virtual void setLodBias(float lodBias) = 0;
 	virtual void quad(


Commit: 6644e4abde59921112ef824db9a1377a629e879c
    https://github.com/scummvm/scummvm/commit/6644e4abde59921112ef824db9a1377a629e879c
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:52+02:00

Commit Message:
ALCACHOFA: Fix tiling on effect objects

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


diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index cf027ebf9a3..8939a385840 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -377,20 +377,17 @@ void Animation::draw3D(int32 frameI, Vector3d center, float scale, BlendMode ble
 	renderer.quad(as2D(center), size, color, rotation, texMin, texMax);
 }
 
-void Animation::drawEffect(int32 frameI, Vector3d topLeft, Vector2d tiling, Vector2d texOffset, BlendMode blendMode) {
+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(tiling.getX() / _renderedSurface.w, tiling.getY() / _renderedSurface.h);
+	Vector2d texMax((float)bounds.width() / _renderedSurface.w, (float)bounds.height() / _renderedSurface.h);
 
 	topLeft += as3D(totalFrameOffset(frameI));
 	topLeft = g_engine->camera().transform3Dto2D(topLeft);
 	const auto rotation = -g_engine->camera().rotation();
-	Vector2d size(bounds.width(), bounds.height());
-	size *= topLeft.z();
-
-	if (abs(tiling.getX()) > epsilon)
-		size = size * texMax;
+	size(0, 0) *= bounds.width() * topLeft.z() / _renderedSurface.w;
+	size(1, 0) *= bounds.height() * topLeft.z() / _renderedSurface.h;
 
 	auto &renderer = g_engine->renderer();
 	renderer.setTexture(_renderedTexture.get());
@@ -614,14 +611,14 @@ SpecialEffectDrawRequest::SpecialEffectDrawRequest(Graphic &graphic, Point topLe
 	, _animation(&graphic.animation())
 	, _frameI(graphic._frameI)
 	, _topLeft(topLeft.x, topLeft.y, graphic._scale)
-	, _tiling(bottomRight.x - topLeft.x, bottomRight.y - topLeft.y)
+	, _size(bottomRight.x - topLeft.x, bottomRight.y - topLeft.y)
 	, _texOffset(texOffset)
 	, _blendMode(blendMode) {
 	assert(_frameI >= 0 && (uint)_frameI < _animation->frameCount());
 }
 
 void SpecialEffectDrawRequest::draw() {
-	_animation->drawEffect(_frameI, _topLeft, _tiling, _texOffset, _blendMode);
+	_animation->drawEffect(_frameI, _topLeft, _size, _texOffset, _blendMode);
 }
 
 static const byte *trimLeading(const byte *text, const byte *end) {
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index d7abaabf463..ad5dff1ebca 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -351,7 +351,7 @@ private:
 	int32 _frameI;
 	Math::Vector3d _topLeft;
 	Math::Vector2d
-		_tiling,
+		_size,
 		_texOffset;
 	BlendMode _blendMode;
 };


Commit: 6a7bd63da84e885f7256cf70b79a70833c2b9f48
    https://github.com/scummvm/scummvm/commit/6a7bd63da84e885f7256cf70b79a70833c2b9f48
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:52+02:00

Commit Message:
ALCACHOFA: Fix parameter name "center" to "topLeft"

Changed paths:
    engines/alcachofa/console.h
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/general-objects.cpp
    engines/alcachofa/global-ui.cpp
    engines/alcachofa/graphics-opengl.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/graphics.h
    engines/alcachofa/rooms.cpp


diff --git a/engines/alcachofa/console.h b/engines/alcachofa/console.h
index 236e842fdc9..da464885754 100644
--- a/engines/alcachofa/console.h
+++ b/engines/alcachofa/console.h
@@ -57,9 +57,9 @@ private:
 	bool cmdDebugMode(int argc, const char **args);
 	bool cmdTeleport(int argc, const char **args);
 
-	bool _showInteractables = true;
-	bool _showCharacters = true;
-	bool _showFloor = true;
+	bool _showInteractables = false;
+	bool _showCharacters = false;
+	bool _showFloor = false;
 	bool _showFloorEdges = false;
 	bool _showFloorColor = false;
 };
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index a5028b8387f..e7cc03bdf8e 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -171,7 +171,7 @@ void Character::update() {
 
 	Graphic *animateGraphic = graphicOf(_curAnimateObject);
 	if (animateGraphic != nullptr) {
-		animateGraphic->center() = Point(0, 0);
+		animateGraphic->topLeft() = Point(0, 0);
 		animateGraphic->update();
 	}
 	else if (_isTalking)
@@ -477,13 +477,13 @@ void WalkingCharacter::update() {
 		_currentPos = _sourcePos;
 	}
 
-	_graphicNormal.center() = _graphicTalking.center() = _currentPos;
+	_graphicNormal.topLeft() = _graphicTalking.topLeft() = _currentPos;
 	auto animateGraphic = graphicOf(_curAnimateObject);
 	auto talkingGraphic = graphicOf(_curTalkingObject);
 	if (animateGraphic != nullptr)
-		animateGraphic->center() = _currentPos;
+		animateGraphic->topLeft() = _currentPos;
 	if (talkingGraphic != nullptr)
-		talkingGraphic->center() = _currentPos;
+		talkingGraphic->topLeft() = _currentPos;
 	if (room() != &g_engine->world().globalRoom()) {
 		float depth = room()->depthAt(_currentPos);
 		int8 order = room()->orderAt(_currentPos);
@@ -566,7 +566,7 @@ void WalkingCharacter::updateWalking() {
 			_direction = _endWalkingDirection;
 		onArrived();
 	}
-	_graphicNormal.center() = _currentPos;
+	_graphicNormal.topLeft() = _currentPos;
 }
 
 void WalkingCharacter::updateWalkingAnimation()
diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index 7a4fb5c17dd..77715e86c98 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -182,7 +182,7 @@ void SpecialEffectObject::draw() {
 		: BlendMode::AdditiveAlpha;
 	Point topLeft = _topLeft, bottomRight = _bottomRight;
 	if (topLeft.x == bottomRight.x || topLeft.y == bottomRight.y) {
-		topLeft = _graphic.center();
+		topLeft = _graphic.topLeft();
 		bottomRight = topLeft + _graphic.animation().imageSize(0);
 	}
 
diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index 3a9fb3005be..f72a6d59c9a 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -166,13 +166,13 @@ void GlobalUI::drawChangingButton() {
 		_changeButton.lastTime() = 42 * (anim->frameCount() - 1) + 1;
 	}
 
-	_changeButton.center() = { (int16)(g_system->getWidth() + 2), -2 };
+	_changeButton.topLeft() = { (int16)(g_system->getWidth() + 2), -2 };
 	if (isHoveringChangeButton() &&
 		g_engine->input().isMouseLeftDown() &&
 		player.pressedObject() == &_changeButton)
 	{
-		_changeButton.center().x -= 2;
-		_changeButton.center().y += 2;
+		_changeButton.topLeft().x -= 2;
+		_changeButton.topLeft().y += 2;
 	}
 
 	_changeButton.order() = -9;
diff --git a/engines/alcachofa/graphics-opengl.cpp b/engines/alcachofa/graphics-opengl.cpp
index 1e32bb7a581..f341c49f42a 100644
--- a/engines/alcachofa/graphics-opengl.cpp
+++ b/engines/alcachofa/graphics-opengl.cpp
@@ -250,13 +250,11 @@ public:
 		Angle rotation,
 		Vector2d texMin,
 		Vector2d texMax) override {
-		size *= 0.5f;
-		center += size;
 		Vector2d positions[] = {
-			center + Vector2d(-size.getX(), -size.getY()),
-			center + Vector2d(-size.getX(), +size.getY()),
+			center + Vector2d(0,			0),
+			center + Vector2d(0,			+size.getY()),
 			center + Vector2d(+size.getX(), +size.getY()),
-			center + Vector2d(+size.getX(), -size.getY()),
+			center + Vector2d(+size.getX(), 0),
 		};
 		if (abs(rotation.getDegrees()) > epsilon) {
 			const Vector2d zero(0, 0);
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 8939a385840..e53759fefe8 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -343,38 +343,38 @@ void Animation::prerenderFrame(int32 frameI) {
 	_renderedFrameI = frameI;
 }
 
-void Animation::draw2D(int32 frameI, Vector2d center, float scale, BlendMode blendMode, Color color) {
+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);
 
 	Vector2d size(bounds.width(), bounds.height());
-	center += as2D(totalFrameOffset(frameI)) * scale;
+	topLeft += as2D(totalFrameOffset(frameI)) * scale;
 	size *= scale;
 
 	auto &renderer = g_engine->renderer();
 	renderer.setTexture(_renderedTexture.get());
 	renderer.setBlendMode(blendMode);
-	renderer.quad(center, size, color, Angle(), texMin, texMax);
+	renderer.quad(topLeft, size, color, Angle(), texMin, texMax);
 }
 
-void Animation::draw3D(int32 frameI, Vector3d center, float scale, BlendMode blendMode, Color color) {
+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);
 
-	center += as3D(totalFrameOffset(frameI)) * scale;
-	center = g_engine->camera().transform3Dto2D(center);
+	topLeft += as3D(totalFrameOffset(frameI)) * scale;
+	topLeft = g_engine->camera().transform3Dto2D(topLeft);
 	const auto rotation = -g_engine->camera().rotation();
 	Vector2d size(bounds.width(), bounds.height());
-	size *= scale * center.z();
+	size *= scale * topLeft.z();
 
 	auto &renderer = g_engine->renderer();
 	renderer.setTexture(_renderedTexture.get());
 	renderer.setBlendMode(blendMode);
-	renderer.quad(as2D(center), size, color, rotation, texMin, texMax);
+	renderer.quad(as2D(topLeft), size, color, rotation, texMin, texMax);
 }
 
 void Animation::drawEffect(int32 frameI, Vector3d topLeft, Vector2d size, Vector2d texOffset, BlendMode blendMode) {
@@ -473,8 +473,8 @@ Graphic::Graphic() {
 }
 
 Graphic::Graphic(ReadStream &stream) {
-	_center.x = stream.readSint16LE();
-	_center.y = stream.readSint16LE();
+	_topLeft.x = stream.readSint16LE();
+	_topLeft.y = stream.readSint16LE();
 	_scale = stream.readSint16LE();
 	_order = stream.readSByte();
 	auto animationName = readVarString(stream);
@@ -484,7 +484,7 @@ Graphic::Graphic(ReadStream &stream) {
 
 Graphic::Graphic(const Graphic &other)
 	: _animation(other._animation)
-	, _center(other._center)
+	, _topLeft(other._topLeft)
 	, _scale(other._scale)
 	, _order(other._order)
 	, _color(other._color)
@@ -556,7 +556,7 @@ void Graphic::setAnimation(Animation *animation) {
 }
 
 void Graphic::serializeSave(Serializer &serializer) {
-	syncPoint(serializer, _center);
+	syncPoint(serializer, _topLeft);
 	serializer.syncAsSint16LE(_scale);
 	serializer.syncAsUint32LE(_lastTime);
 	serializer.syncAsByte(_isPaused);
@@ -577,7 +577,7 @@ AnimationDrawRequest::AnimationDrawRequest(Graphic &graphic, bool is3D, BlendMod
 	, _is3D(is3D)
 	, _animation(&graphic.animation())
 	, _frameI(graphic._frameI)
-	, _center(graphic._center.x, graphic._center.y, graphic._scale)
+	, _topLeft(graphic._topLeft.x, graphic._topLeft.y, graphic._scale)
 	, _scale(graphic._scale * graphic._depthScale)
 	, _color(graphic.color())
 	, _blendMode(blendMode)
@@ -590,7 +590,7 @@ AnimationDrawRequest::AnimationDrawRequest(Animation *animation, int32 frameI, V
 	, _is3D(false)
 	, _animation(animation)
 	, _frameI(frameI)
-	, _center(as3D(center))
+	, _topLeft(as3D(center))
 	, _scale(kBaseScale)
 	, _color(kWhite)
 	, _blendMode(BlendMode::AdditiveAlpha)
@@ -601,9 +601,9 @@ AnimationDrawRequest::AnimationDrawRequest(Animation *animation, int32 frameI, V
 
 void AnimationDrawRequest::draw() {
 	if (_is3D)
-		_animation->draw3D(_frameI, _center, _scale * kInvBaseScale, _blendMode, _color);
+		_animation->draw3D(_frameI, _topLeft, _scale * kInvBaseScale, _blendMode, _color);
 	else
-		_animation->draw2D(_frameI, as2D(_center), _scale * kInvBaseScale, _blendMode, _color);
+		_animation->draw2D(_frameI, as2D(_topLeft), _scale * kInvBaseScale, _blendMode, _color);
 }
 
 SpecialEffectDrawRequest::SpecialEffectDrawRequest(Graphic &graphic, Point topLeft, Point bottomRight, Vector2d texOffset, BlendMode blendMode)
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index ad5dff1ebca..8deecb306f5 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -251,7 +251,7 @@ public:
 	Graphic(Common::ReadStream &stream);
 	Graphic(const Graphic &other); // animation reference is taken, so keep other alive
 
-	inline Common::Point &center() { return _center; }
+	inline Common::Point &topLeft() { return _topLeft; }
 	inline int8 &order() { return _order; }
 	inline int16 &scale() { return _scale; }
 	inline float &depthScale() { return _depthScale; }
@@ -284,7 +284,7 @@ private:
 	friend class SpecialEffectDrawRequest;
 	Common::ScopedPtr<Animation> _ownedAnimation;
 	Animation *_animation = nullptr;
-	Common::Point _center;
+	Common::Point _topLeft;
 	int16 _scale = kBaseScale;
 	int8 _order = 0;
 	Color _color = kWhite;
@@ -328,7 +328,7 @@ private:
 	bool _is3D;
 	Animation *_animation;
 	int32 _frameI;
-	Math::Vector3d _center;
+	Math::Vector3d _topLeft;
 	float _scale;
 	Color _color;
 	BlendMode _blendMode;
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 59c804206f3..ba8cd2da7dd 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -385,7 +385,7 @@ Item *Inventory::getHoveredItem() {
 		assert(graphic != nullptr);
 		auto bounds = graphic->animation().frameBounds(0);
 		auto totalOffset = graphic->animation().totalFrameOffset(0);
-		auto delta = mousePos - graphic->center() - totalOffset;
+		auto delta = mousePos - graphic->topLeft() - totalOffset;
 		if (delta.x >= 0 && delta.y >= 0 && delta.x <= bounds.width() && delta.y <= bounds.height())
 			return item;
 	}
@@ -418,14 +418,14 @@ void Inventory::drawAsOverlay(int32 scrollY) {
 		if (graphic == nullptr)
 			continue;
 
-		int16 oldY = graphic->center().y;
+		int16 oldY = graphic->topLeft().y;
 		int8 oldOrder = graphic->order();
-		graphic->center().y += scrollY;
+		graphic->topLeft().y += scrollY;
 		graphic->order() = -kForegroundOrderCount;
 		if (object->name().equalsIgnoreCase("Background"))
 			graphic->order()++;
 		object->draw();
-		graphic->center().y = oldY;
+		graphic->topLeft().y = oldY;
 		graphic->order() = oldOrder;
 	}
 }


Commit: d598530b45f57d2db39b050cdae2715d57b39c68
    https://github.com/scummvm/scummvm/commit/d598530b45f57d2db39b050cdae2715d57b39c68
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:52+02:00

Commit Message:
ALCACHOFA: Fix black frames on opening/closing inventory

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


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 89c0daf9c0c..f9a036d1a84 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -88,6 +88,8 @@ Common::Error AlcachofaEngine::run() {
 		_camera.shake() = Vector2d();
 		_player->preUpdate();
 		_player->currentRoom()->update();
+		if (_player->currentRoom() != nullptr)
+			_player->currentRoom()->draw();
 		_player->postUpdate();
 		if (_debugHandler != nullptr)
 			_debugHandler->update();
diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index f72a6d59c9a..429a03db932 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -61,6 +61,7 @@ void GlobalUI::startClosingInventory() {
 	_isOpeningInventory = false;
 	_isClosingInventory = true;
 	_timeForInventory = g_system->getMillis();
+	updateClosingInventory(); // prevents the first frame of closing to not render the inventory overlay
 }
 
 void GlobalUI::updateClosingInventory() {
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index ba8cd2da7dd..11122265235 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -129,33 +129,32 @@ ObjectBase *Room::getObjectByName(const Common::String &name) const {
 }
 
 void Room::update() {
-	if (!g_engine->isDebugModeActive())
-	{
-		updateScripts();
-
-		if (g_engine->player().currentRoom() == this) {
-			updateRoomBounds();
-			g_engine->globalUI().updateClosingInventory();
-			if (!updateInput())
-				return;
-		}
-		if (!g_engine->player().isOptionsMenuOpen() &&
-			g_engine->player().currentRoom() != &g_engine->world().inventory())
-			world().globalRoom().updateObjects();
-		if (g_engine->player().currentRoom() == this)
-			updateObjects();
-	}
+	if (g_engine->isDebugModeActive())
+		return;
+	updateScripts();
 
 	if (g_engine->player().currentRoom() == this) {
-		g_engine->camera().update();
-		drawObjects();
-		world().globalRoom().drawObjects();
-		// TODO: Draw black borders
-		g_engine->player().drawScreenStates();
-		g_engine->drawQueue().draw();
-		drawDebug();
-		world().globalRoom().drawDebug();
+		updateRoomBounds();
+		g_engine->globalUI().updateClosingInventory();
+		if (!updateInput())
+			return;
 	}
+	if (!g_engine->player().isOptionsMenuOpen() &&
+		g_engine->player().currentRoom() != &g_engine->world().inventory())
+		world().globalRoom().updateObjects();
+	if (g_engine->player().currentRoom() == this)
+		updateObjects();
+}
+
+void Room::draw() {
+	g_engine->camera().update();
+	drawObjects();
+	world().globalRoom().drawObjects();
+	// TODO: Draw black borders
+	g_engine->player().drawScreenStates();
+	g_engine->drawQueue().draw();
+	drawDebug();
+	world().globalRoom().drawDebug();
 }
 
 void Room::updateScripts() {
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index 6a971bea562..6d91c797c2b 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -54,6 +54,7 @@ public:
 	inline ObjectIterator endObjects() const { return _objects.end(); }
 
 	void update();
+	void draw();
 	virtual bool updateInput();
 	virtual void loadResources();
 	virtual void freeResources();


Commit: 7e7163768fc04468e253b047b93895d990c8ae1e
    https://github.com/scummvm/scummvm/commit/7e7163768fc04468e253b047b93895d990c8ae1e
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:52+02:00

Commit Message:
ALCACHOFA: Update 3D mouse pos always

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


diff --git a/engines/alcachofa/input.cpp b/engines/alcachofa/input.cpp
index 6f7e38aa7d6..0f72e747fc8 100644
--- a/engines/alcachofa/input.cpp
+++ b/engines/alcachofa/input.cpp
@@ -34,6 +34,7 @@ void Input::nextFrame() {
 	_wasMouseRightPressed = false;
 	_wasMouseLeftReleased = false;
 	_wasMouseRightReleased = false;
+	updateMousePos3D(); // camera transformation might have changed
 }
 
 bool Input::handleEvent(const Common::Event &event) {
@@ -64,8 +65,7 @@ bool Input::handleEvent(const Common::Event &event) {
 		return true;
 	case EVENT_MOUSEMOVE: {
 		_mousePos2D = event.mouse;
-		auto pos3D = g_engine->camera().transform2Dto3D({ (float)_mousePos2D.x, (float)_mousePos2D.y, kBaseScale });
-		_mousePos3D = { (int16)pos3D.x(), (int16)pos3D.y() };
+		updateMousePos3D();
 		return true;
 	}
 	default:
@@ -84,4 +84,9 @@ void Input::toggleDebugInput(bool debugMode) {
 		_debugInput.reset(new Input());
 }
 
+void Input::updateMousePos3D() {
+	auto pos3D = g_engine->camera().transform2Dto3D({ (float)_mousePos2D.x, (float)_mousePos2D.y, kBaseScale });
+	_mousePos3D = { (int16)pos3D.x(), (int16)pos3D.y() };
+}
+
 }
diff --git a/engines/alcachofa/input.h b/engines/alcachofa/input.h
index 9f1f6a5e31c..24af6e2ad4b 100644
--- a/engines/alcachofa/input.h
+++ b/engines/alcachofa/input.h
@@ -47,6 +47,8 @@ public:
 	void toggleDebugInput(bool debugMode); ///< Toggles input debug mode which blocks any input not retrieved with debugInput
 
 private:
+	void updateMousePos3D();
+
 	bool
 		_wasMouseLeftPressed = false,
 		_wasMouseRightPressed = false,


Commit: d72e14505c9937a775e153b21c99900b6a325b07
    https://github.com/scummvm/scummvm/commit/d72e14505c9937a775e153b21c99900b6a325b07
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:52+02:00

Commit Message:
ALCACHOFA: Fix exception for BACTERIO/PULSAR

Changed paths:
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 1a7b8b80e9b..dde24d038c1 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -691,8 +691,11 @@ private:
 			Character *_character = strcmp(characterName, "AMBOS") == 0
 				? &relatedCharacter()
 				: getObjectArg<Character>(0);
-			if (_character == nullptr)
+			if (_character == nullptr) {
+				if (strcmp(characterName, "OFELIA") == 0 && dialogId == 3737)
+					return TaskReturn::finish(1);
 				error("Invalid character for sayText: %s", characterName);
+			}
 			return TaskReturn::waitFor(_character->sayText(process(), dialogId));
 		};
 		case ScriptKernelTask::SetDialogLineReturn:


Commit: bbfcb8f7f5a2d9239797bf035906ef9e8dc1c4e9
    https://github.com/scummvm/scummvm/commit/bbfcb8f7f5a2d9239797bf035906ef9e8dc1c4e9
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:53+02:00

Commit Message:
ALCACHOFA: Fix lens flares outside the western town

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


diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index 77715e86c98..f1e811f2dcd 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -105,7 +105,7 @@ void GraphicObject::draw() {
 	if (!isEnabled() || !_graphic.hasAnimation())
 		return;
 	const BlendMode blendMode = _type == GraphicObjectType::Effect
-		? BlendMode::Alpha
+		? BlendMode::Additive
 		: BlendMode::AdditiveAlpha;
 	const bool is3D = room() != &g_engine->world().inventory();
 	_graphic.update();


Commit: d1c56de46033dccb558dc280d001c61f3b1e6354
    https://github.com/scummvm/scummvm/commit/d1c56de46033dccb558dc280d001c61f3b1e6354
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:53+02:00

Commit Message:
ALCACHOFA: Add main character evasion

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


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index e7cc03bdf8e..92ec03c29a2 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -791,17 +791,43 @@ void MainCharacter::onArrived() {
 }
 
 void MainCharacter::walkTo(
-	const Point &target, Direction endDirection,
+	const Point &target_, Direction endDirection,
 	ITriggerableObject *activateObject, const char *activateAction) {
 	_activateObject = activateObject;
 	_activateAction = activateAction;
-
-	// TODO: Add collision avoidance
+	Point target = target_;
+
+	Point evadeTarget = target;
+	const PathFindingShape *activeFloor = room()->activeFloor();
+	if (activeFloor != nullptr && activeFloor->findPath(_currentPos, target, _pathPoints))
+		evadeTarget = _pathPoints[0];
+
+	MainCharacter *otherCharacter = &g_engine->world().getOtherMainCharacterByKind(_kind);
+	Point otherTarget = otherCharacter->_currentPos;
+	if (otherCharacter->isWalking() && !otherCharacter->_pathPoints.empty())
+		otherTarget = otherCharacter->_pathPoints[0];
+
+	const float activeDepthScale = g_engine->player().activeCharacter()->_graphicNormal.depthScale();
+	const float avoidanceDistSqr = pow(75 * activeDepthScale, 2);
+	const bool willIBeBusy =
+		_activateObject != nullptr &&
+		strcmp(_activateAction, "MIRAR") != 0 &&
+		otherCharacter->currentlyUsing() != dynamic_cast<ObjectBase *>(_activateObject);
+
+	if (otherCharacter->room() == room() && evadeTarget.sqrDist(otherTarget) <= avoidanceDistSqr) {
+		if (!otherCharacter->isBusy()) {
+			if (activeFloor != nullptr && activeFloor->findEvadeTarget(evadeTarget, activeDepthScale, avoidanceDistSqr, evadeTarget))
+				otherCharacter->WalkingCharacter::walkTo(evadeTarget);
+		}
+		else if (!willIBeBusy) {
+			if (activeFloor != nullptr)
+				activeFloor->findEvadeTarget(evadeTarget, activeDepthScale, avoidanceDistSqr, target);
+		}
+	}
 
 	WalkingCharacter::walkTo(target, endDirection, activateObject, activateAction);
-	if (this == g_engine->player().activeCharacter()) {
+	if (this == g_engine->player().activeCharacter())
 		g_engine->camera().setFollow(this);
-	}
 }
 
 void MainCharacter::draw() {
diff --git a/engines/alcachofa/shape.cpp b/engines/alcachofa/shape.cpp
index 9fc5f7e670d..c1c0f43574a 100644
--- a/engines/alcachofa/shape.cpp
+++ b/engines/alcachofa/shape.cpp
@@ -605,6 +605,29 @@ void PathFindingShape::floydWarshallPath(
 	path.push(_linkPoints[fromLink]);
 }
 
+bool PathFindingShape::findEvadeTarget(
+	Point centerTarget,
+	float depthScale, float minDistSqr,
+	Point &evadeTarget) const {
+	// TODO: Check if minDistSqr should just modify tryDistBase
+
+	for (float tryDistBase = 60; tryDistBase < 250; tryDistBase += 10) {
+		for (int tryAngleI = 0; tryAngleI < 6; tryAngleI++) {
+			const float tryAngle = tryAngleI / 3.0f * M_PI + deg2rad(30.0f);
+			const float tryDist = tryDistBase * depthScale;
+			const Point tryPos = evadeTarget + Point(
+				(int16)(cosf(tryAngle) * tryDist),
+				(int16)(sinf(tryAngle) * tryDist));
+
+			if (contains(tryPos) && tryPos.sqrDist(centerTarget) > minDistSqr) {
+				evadeTarget = tryPos;
+				return true;
+			}
+		}
+	}
+	return false;
+}
+
 FloorColorShape::FloorColorShape() {}
 
 FloorColorShape::FloorColorShape(ReadStream &stream) {
diff --git a/engines/alcachofa/shape.h b/engines/alcachofa/shape.h
index b42239edce4..cb53495864e 100644
--- a/engines/alcachofa/shape.h
+++ b/engines/alcachofa/shape.h
@@ -173,6 +173,11 @@ public:
 		const Common::Point &to,
 		Common::Stack<Common::Point> &path) const;
 	int32 edgeTarget(uint polygonI, uint pointI) const;
+	bool findEvadeTarget(
+		Common::Point centerTarget,
+		float depthScale,
+		float minDistSqr,
+		Common::Point& evadeTarget) const;
 
 private:
 	using LinkIndex = Common::Pair<int32, int32>;


Commit: cd0dcde22c46f0093728806620ac80ff4a188f9a
    https://github.com/scummvm/scummvm/commit/cd0dcde22c46f0093728806620ac80ff4a188f9a
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:53+02:00

Commit Message:
ALCACHOFA: Add ShowCenterBottomText kernel call

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


diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index 429a03db932..a103461e91e 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -181,4 +181,49 @@ void GlobalUI::drawChangingButton() {
 	g_engine->drawQueue().add<AnimationDrawRequest>(_changeButton, false, BlendMode::AdditiveAlpha);
 }
 
+struct CenterBottomTextTask : public Task {
+	CenterBottomTextTask(Process &process, int32 dialogId, uint32 durationMs)
+		: Task(process)
+		, _dialogId(dialogId)
+		, _durationMs(durationMs) {
+	}
+
+	TaskReturn run() override
+	{
+		Font &font = g_engine->globalUI().dialogFont();
+		const char *text = g_engine->world().getDialogLine(_dialogId);
+		const Point pos(
+			g_system->getWidth() / 2,
+			g_system->getHeight() - 200
+		);
+
+		TASK_BEGIN;
+		_startTime = g_system->getMillis();
+		while (g_system->getMillis() - _startTime < _durationMs) {
+			if (process().isActiveForPlayer()) {
+				g_engine->drawQueue().add<TextDrawRequest>(
+					font, text, pos, -1, true, kWhite, 1);
+			}
+			TASK_YIELD;
+		}
+		TASK_END;
+	}
+
+	void debugPrint() override
+	{
+		uint32 remaining = g_system->getMillis() - _startTime <= _durationMs
+			? _durationMs - (g_system->getMillis() - _startTime)
+			: 0;
+		g_engine->console().debugPrintf("CenterBottomText (%d) with %ums remaining\n", _dialogId, remaining);
+	}
+
+private:
+	int32 _dialogId;
+	uint32 _startTime = 0, _durationMs;
+};
+
+Task *showCenterBottomText(Process &process, int32 dialogId, uint32 durationMs) {
+	return new CenterBottomTextTask(process, dialogId, durationMs);
+}
+
 }
diff --git a/engines/alcachofa/global-ui.h b/engines/alcachofa/global-ui.h
index d07dbf86f8d..e5522bc97e3 100644
--- a/engines/alcachofa/global-ui.h
+++ b/engines/alcachofa/global-ui.h
@@ -61,6 +61,8 @@ private:
 	uint32 _timeForInventory = 0;
 };
 
+Task *showCenterBottomText(Process &process, int32 dialogId, uint32 durationMs);
+
 }
 
 
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index dde24d038c1..04724635019 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -21,6 +21,7 @@
 
 #include "script.h"
 #include "rooms.h"
+#include "global-ui.h"
 #include "alcachofa.h"
 #include "script-debug.h"
 
@@ -495,8 +496,7 @@ private:
 
 		// Misc / control flow
 		case ScriptKernelTask::ShowCenterBottomText:
-			warning("STUB KERNEL CALL: ShowCenterBottomText");
-			return TaskReturn::finish(0);
+			return TaskReturn::waitFor(showCenterBottomText(process(), getNumberArg(0), (uint32)getNumberArg(1)));
 		case ScriptKernelTask::Delay:
 			return getNumberArg(0) <= 0
 				? TaskReturn::finish(0)


Commit: 24453e1d01109829263f2bf8042b29a3ea20b246
    https://github.com/scummvm/scummvm/commit/24453e1d01109829263f2bf8042b29a3ea20b246
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:53+02:00

Commit Message:
ALCACHOFA: Reduce unnecessary string allocations

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


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 92ec03c29a2..93d8153449c 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -106,7 +106,7 @@ void InteractableObject::trigger(const char *action) {
 
 void InteractableObject::toggle(bool isEnabled) {
 	ObjectBase::toggle(isEnabled);
-	ObjectBase *related = room()->getObjectByName(_relatedObject);
+	ObjectBase *related = room()->getObjectByName(_relatedObject.c_str());
 	if (related != nullptr)
 		related->toggle(isEnabled);
 }
@@ -254,9 +254,9 @@ void Character::syncObjectAsString(Serializer &serializer, ObjectBase *&object)
 		if (name.empty())
 			object = nullptr;
 		else {
-			object = room()->getObjectByName(name);
+			object = room()->getObjectByName(name.c_str());
 			if (object == nullptr)
-				object = room()->world().getObjectByName(name);
+				object = room()->world().getObjectByName(name.c_str());
 			if (object == nullptr)
 				error("Invalid object name \"%s\" saved for \"%s\" in \"%s\"",
 					name.c_str(), this->name().c_str(), room()->name().c_str());
@@ -872,7 +872,7 @@ void MainCharacter::serializeSave(Serializer &serializer) {
 	String roomName = room()->name();
 	serializer.syncString(roomName);
 	if (serializer.isLoading()) {
-		room() = room()->world().getRoomByName(roomName);
+		room() = room()->world().getRoomByName(roomName.c_str());
 		if (room() == nullptr)
 			error("Invalid room name \"%s\" saved for \"%s\"", roomName.c_str(), name().c_str());
 	}
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index c75defede95..ac1721b3e06 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -126,7 +126,7 @@ void Player::changeRoom(const Common::String &targetRoomName, bool resetCamera)
 			_currentRoom->freeResources();
 	}
 
-	_currentRoom = g_engine->world().getRoomByName(targetRoomName);
+	_currentRoom = g_engine->world().getRoomByName(targetRoomName.c_str());
 	if (_currentRoom == nullptr)
 		error("Invalid room name: %s", targetRoomName.c_str());
 
@@ -198,11 +198,11 @@ struct DoorTask : public Task {
 		, _sourceDoor(door)
 		, _character(g_engine->player().activeCharacter())
 		, _player(g_engine->player()) {
-		_targetRoom = g_engine->world().getRoomByName(door->targetRoom());
+		_targetRoom = g_engine->world().getRoomByName(door->targetRoom().c_str());
 		if (_targetRoom == nullptr)
 			error("Invalid door target room: %s", door->targetRoom().c_str());
 
-		_targetObject = dynamic_cast<InteractableObject *>(_targetRoom->getObjectByName(door->targetObject()));
+		_targetObject = dynamic_cast<InteractableObject *>(_targetRoom->getObjectByName(door->targetObject().c_str()));
 		if (_targetObject == nullptr)
 			error("Invalid door target door: %s", door->targetObject().c_str());
 		auto targetDoor = dynamic_cast<const Door *>(_targetObject);
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 11122265235..a2a3f9ea2aa 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -120,7 +120,7 @@ Room::~Room() {
 		delete object;
 }
 
-ObjectBase *Room::getObjectByName(const Common::String &name) const {
+ObjectBase *Room::getObjectByName(const char *name) const {
 	for (auto *object : _objects) {
 		if (object->name().equalsIgnoreCase(name))
 			return object;
@@ -512,7 +512,7 @@ MainCharacter &World::getOtherMainCharacterByKind(MainCharacterKind kind) const
 	}
 }
 
-Room *World::getRoomByName(const Common::String &name) const {
+Room *World::getRoomByName(const char *name) const {
 	for (auto *room : _rooms) {
 		if (room->name().equalsIgnoreCase(name))
 			return room;
@@ -520,7 +520,7 @@ Room *World::getRoomByName(const Common::String &name) const {
 	return nullptr;
 }
 
-ObjectBase *World::getObjectByName(const Common::String &name) const {
+ObjectBase *World::getObjectByName(const char *name) const {
 	ObjectBase *result = nullptr;
 	if (result == nullptr && g_engine->player().currentRoom() != nullptr)
 		result = g_engine->player().currentRoom()->getObjectByName(name);
@@ -531,7 +531,7 @@ ObjectBase *World::getObjectByName(const Common::String &name) const {
 	return result;
 }
 
-ObjectBase *World::getObjectByName(MainCharacterKind character, const Common::String &name) const {
+ObjectBase *World::getObjectByName(MainCharacterKind character, const char *name) const {
 	if (character == MainCharacterKind::None)
 		return getObjectByName(name);
 	const auto &player = g_engine->player();
@@ -547,7 +547,7 @@ ObjectBase *World::getObjectByName(MainCharacterKind character, const Common::St
 	return result;
 }
 
-ObjectBase *World::getObjectByNameFromAnyRoom(const Common::String &name) const {
+ObjectBase *World::getObjectByNameFromAnyRoom(const char *name) const {
 	for (auto *room : _rooms) {
 		ObjectBase *result = room->getObjectByName(name);
 		if (result != nullptr)
@@ -556,12 +556,12 @@ ObjectBase *World::getObjectByNameFromAnyRoom(const Common::String &name) const
 	return nullptr;
 }
 
-void World::toggleObject(MainCharacterKind character, const Common::String &objName, bool isEnabled) {
+void World::toggleObject(MainCharacterKind character, const char *objName, bool isEnabled) {
 	ObjectBase *object = getObjectByName(character, objName);
 	if (object == nullptr)
 		object = getObjectByNameFromAnyRoom(objName);
 	if (object == nullptr)
-		warning("Tried to toggle unknown object: %s", objName.c_str());
+		warning("Tried to toggle unknown object: %s", objName);
 		// I would have liked an error for this, but original inconsistencies...
 	else
 		object->toggle(isEnabled);
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index 6d91c797c2b..039360255bc 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -59,7 +59,7 @@ public:
 	virtual void loadResources();
 	virtual void freeResources();
 	virtual void serializeSave(Common::Serializer &serializer);
-	ObjectBase *getObjectByName(const Common::String &name) const;
+	ObjectBase *getObjectByName(const char *name) const;
 	void toggleActiveFloor();
 	void debugPrint(bool withObjects) const;
 
@@ -163,15 +163,15 @@ public:
 
 	MainCharacter &getMainCharacterByKind(MainCharacterKind kind) const;
 	MainCharacter &getOtherMainCharacterByKind(MainCharacterKind kind) const;
-	Room *getRoomByName(const Common::String &name) const;
-	ObjectBase *getObjectByName(const Common::String &name) const;
-	ObjectBase *getObjectByName(MainCharacterKind character, const Common::String &name) const;
-	ObjectBase *getObjectByNameFromAnyRoom(const Common::String &name) const;
+	Room *getRoomByName(const char *name) const;
+	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 char *getLocalizedName(const Common::String &name) const;
 	const char *getDialogLine(int32 dialogId) const;
 
-	void toggleObject(MainCharacterKind character, const Common::String &objName, bool isEnabled);
+	void toggleObject(MainCharacterKind character, const char *objName, bool isEnabled);
 
 private:
 	bool loadWorldFile(const char *path);


Commit: c107f98062a588479ef9f4afd3ee9e0979258406
    https://github.com/scummvm/scummvm/commit/c107f98062a588479ef9f4afd3ee9e0979258406
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:53+02:00

Commit Message:
ALCACHOFA: Code conventions - Fix indentations

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/alcachofa.h
    engines/alcachofa/camera.cpp
    engines/alcachofa/camera.h
    engines/alcachofa/console.cpp
    engines/alcachofa/debug.h
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/global-ui.cpp
    engines/alcachofa/graphics-opengl.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/graphics.h
    engines/alcachofa/player.cpp
    engines/alcachofa/player.h
    engines/alcachofa/rooms.cpp
    engines/alcachofa/rooms.h
    engines/alcachofa/scheduler.cpp
    engines/alcachofa/scheduler.h
    engines/alcachofa/script-debug.h
    engines/alcachofa/script.cpp
    engines/alcachofa/shape.cpp
    engines/alcachofa/shape.h
    engines/alcachofa/sounds.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index f9a036d1a84..df88a8a776a 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -44,8 +44,10 @@ namespace Alcachofa {
 
 AlcachofaEngine *g_engine;
 
-AlcachofaEngine::AlcachofaEngine(OSystem *syst, const ADGameDescription *gameDesc) : Engine(syst),
-	_gameDescription(gameDesc), _randomSource("Alcachofa") {
+AlcachofaEngine::AlcachofaEngine(OSystem *syst, const ADGameDescription *gameDesc)
+	: Engine(syst)
+	, _gameDescription(gameDesc)
+	, _randomSource("Alcachofa") {
 	g_engine = this;
 }
 
@@ -104,7 +106,7 @@ Common::Error AlcachofaEngine::run() {
 	return Common::kNoError;
 }
 
-void AlcachofaEngine::playVideo(int32 videoId) {	
+void AlcachofaEngine::playVideo(int32 videoId) {
 	Video::MPEGPSDecoder decoder;
 	if (!decoder.loadFile(Common::Path(Common::String::format("Data/DATA%02d.BIN", videoId + 1))))
 		error("Could not find video %d", videoId);
@@ -140,8 +142,7 @@ void AlcachofaEngine::playVideo(int32 videoId) {
 	decoder.stop();
 }
 
-void AlcachofaEngine::setDebugMode(DebugMode mode, int32 param)
-{
+void AlcachofaEngine::setDebugMode(DebugMode mode, int32 param) {
 	switch (mode)
 	{
 	case DebugMode::ClosestFloorPoint:
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index d19634e4f06..84c06c4ae10 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -95,9 +95,9 @@ public:
 
 	bool hasFeature(EngineFeature f) const override {
 		return
-		    (f == kSupportsLoadingDuringRuntime) ||
-		    (f == kSupportsSavingDuringRuntime) ||
-		    (f == kSupportsReturnToLauncher);
+			(f == kSupportsLoadingDuringRuntime) ||
+			(f == kSupportsSavingDuringRuntime) ||
+			(f == kSupportsReturnToLauncher);
 	};
 
 	bool canLoadGameStateCurrently(Common::U32String *msg = nullptr) override {
diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index 5eb70636814..dc9f0ced99f 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -20,8 +20,8 @@
  */
 
 #include "camera.h"
-#include "script.h"
 #include "alcachofa.h"
+#include "script.h"
 
 #include "common/system.h"
 #include "math/vector4d.h"
@@ -57,7 +57,7 @@ void Camera::setFollow(WalkingCharacter *target, bool catchUp) {
 }
 
 void Camera::setPosition(Vector2d v) {
-	setPosition({ v.getX(), v.getY(), _cur._usedCenter.z() });
+	setPosition({v.getX(), v.getY(), _cur._usedCenter.z()});
 }
 
 void Camera::setPosition(Vector3d v) {
@@ -100,8 +100,7 @@ void Camera::setupMatricesAround(Vector3d center) {
 	_mat2Dto3D = matTemp * _mat2Dto3D;
 }
 
-void minmax(Vector3d &min, Vector3d &max, Vector3d val)
-{
+void minmax(Vector3d &min, Vector3d &max, Vector3d val) {
 	min.set(
 		MIN(min.x(), val.x()),
 		MIN(min.y(), val.y()),
@@ -157,8 +156,8 @@ Vector3d Camera::transform3Dto2D(Vector3d v3d) const {
 }
 
 Point Camera::transform3Dto2D(Point p3d) const {
-	auto v2d = transform3Dto2D({ (float)p3d.x, (float)p3d.y, kBaseScale });
-	return { (int16)v2d.x(), (int16)v2d.y() };
+	auto v2d = transform3Dto2D({(float)p3d.x, (float)p3d.y, kBaseScale});
+	return {(int16)v2d.x(), (int16)v2d.y()};
 }
 
 void Camera::update() {
@@ -172,8 +171,7 @@ void Camera::update() {
 		for (int i = 0; i < 4; i++)
 			updateFollowing(50.0f);
 		_catchUp = false;
-	}
-	else
+	} else
 		updateFollowing(deltaTime);
 	setAppliedCenter(_cur._usedCenter + Vector3d(_shake.getX(), _shake.getY(), 0.0f));
 }
@@ -224,8 +222,7 @@ void Camera::updateFollowing(float deltaTime) {
 			_cur._usedCenter = targetCenter;
 			_isChanging = false;
 			_cur._isBraking = false;
-		}
-		else {
+		} else {
 			Vector3d deltaCenter = targetCenter - _cur._usedCenter;
 			deltaCenter.z() = 0.0f;
 			_cur._usedCenter += deltaCenter * moveDistance / distanceToTarget;
@@ -297,8 +294,9 @@ protected:
 
 struct CamLerpPosScaleTask final : public CamLerpTask {
 	CamLerpPosScaleTask(Process &process,
-		Vector3d targetPos, float targetScale,
-		int32 duration, EasingType moveEasingType, EasingType scaleEasingType)
+						Vector3d targetPos, float targetScale,
+						int32 duration,
+						EasingType moveEasingType, EasingType scaleEasingType)
 		: CamLerpTask(process, duration, EasingType::Linear) // linear as we need different ones per component
 		, _fromPos(_camera._appliedCenter)
 		, _deltaPos(targetPos - _camera._appliedCenter)
@@ -352,8 +350,8 @@ private:
 };
 
 Task *Camera::lerpPos(Process &process,
-	Vector2d targetPos,
-	int32 duration, EasingType easingType) {
+					  Vector2d targetPos,
+					  int32 duration, EasingType easingType) {
 	if (!process.isActiveForPlayer()) {
 		warning("stub: non-active camera lerp script invoked");
 		return new DelayTask(process, duration);
@@ -363,8 +361,8 @@ Task *Camera::lerpPos(Process &process,
 }
 
 Task *Camera::lerpPos(Process &process,
-	Vector3d targetPos,
-	int32 duration, EasingType easingType) {
+					  Vector3d targetPos,
+					  int32 duration, EasingType easingType) {
 	if (!process.isActiveForPlayer()) {
 		warning("stub: non-active camera lerp script invoked");
 		return new DelayTask(process, duration);
@@ -374,8 +372,8 @@ Task *Camera::lerpPos(Process &process,
 }
 
 Task *Camera::lerpPosZ(Process &process,
-	float targetPosZ,
-	int32 duration, EasingType easingType) {
+					   float targetPosZ,
+					   int32 duration, EasingType easingType) {
 	if (!process.isActiveForPlayer()) {
 		warning("stub: non-active camera lerp script invoked");
 		return new DelayTask(process, duration);
@@ -385,8 +383,8 @@ Task *Camera::lerpPosZ(Process &process,
 }
 
 Task *Camera::lerpScale(Process &process,
-	float targetScale,
-	int32 duration, EasingType easingType) {
+						float targetScale,
+						int32 duration, EasingType easingType) {
 	if (!process.isActiveForPlayer()) {
 		warning("stub: non-active camera lerp script invoked");
 		return new DelayTask(process, duration);
@@ -395,8 +393,8 @@ Task *Camera::lerpScale(Process &process,
 }
 
 Task *Camera::lerpRotation(Process &process,
-	float targetRotation,
-	int32 duration, EasingType easingType) {
+						   float targetRotation,
+						   int32 duration, EasingType easingType) {
 	if (!process.isActiveForPlayer()) {
 		warning("stub: non-active camera lerp script invoked");
 		return new DelayTask(process, duration);
@@ -405,8 +403,9 @@ Task *Camera::lerpRotation(Process &process,
 }
 
 Task *Camera::lerpPosScale(Process &process,
-	Vector3d targetPos, float targetScale,
-	int32 duration, EasingType moveEasingType, EasingType scaleEasingType) {
+						   Vector3d targetPos, float targetScale,
+						   int32 duration,
+						   EasingType moveEasingType, EasingType scaleEasingType) {
 	if (!process.isActiveForPlayer()) {
 		warning("stub: non-active camera lerp script invoked");
 		return new DelayTask(process, duration);
@@ -418,4 +417,4 @@ Task *Camera::waitToStop(Process &process) {
 	return new CamWaitToStopTask(process);
 }
 
-}
+} // namespace Alcachofa
diff --git a/engines/alcachofa/camera.h b/engines/alcachofa/camera.h
index 7d22cbef8c6..b93e1571385 100644
--- a/engines/alcachofa/camera.h
+++ b/engines/alcachofa/camera.h
@@ -55,7 +55,7 @@ public:
 	Task *lerpPos(Process &process,
 		Math::Vector2d targetPos,
 		int32 duration, EasingType easingType);
-	Task *lerpPos(Process &process, 
+	Task *lerpPos(Process &process,
 		Math::Vector3d targetPos,
 		int32 duration, EasingType easingType);
 	Task *lerpPosZ(Process &process,
diff --git a/engines/alcachofa/console.cpp b/engines/alcachofa/console.cpp
index 7a2941151a9..44a3d70f88d 100644
--- a/engines/alcachofa/console.cpp
+++ b/engines/alcachofa/console.cpp
@@ -49,8 +49,7 @@ Console::Console() : GUI::Debugger() {
 Console::~Console() {
 }
 
-bool Console::isAnyDebugDrawingOn() const
-{
+bool Console::isAnyDebugDrawingOn() const {
 	return
 		g_engine->isDebugModeActive() ||
 		_showInteractables ||
@@ -207,8 +206,7 @@ bool Console::cmdItem(int argc, const char **args) {
 	return true;
 }
 
-bool Console::cmdDebugMode(int argc, const char **args)
-{
+bool Console::cmdDebugMode(int argc, const char **args) {
 	if (argc < 2 || argc > 3) {
 		debugPrintf("usage: debugMode <mode> [<param>]\n");
 		debugPrintf("modes:\n");
@@ -236,8 +234,7 @@ bool Console::cmdDebugMode(int argc, const char **args)
 	return true;
 }
 
-bool Console::cmdTeleport(int argc, const char **args)
-{
+bool Console::cmdTeleport(int argc, const char **args) {
 	if (argc < 1 || argc > 2)
 	{
 		debugPrintf("usagge: tp [<character>]\n");
diff --git a/engines/alcachofa/debug.h b/engines/alcachofa/debug.h
index be9196c9b2f..0e1eef52529 100644
--- a/engines/alcachofa/debug.h
+++ b/engines/alcachofa/debug.h
@@ -40,8 +40,7 @@ class ClosestFloorPointDebugHandler final : public IDebugHandler {
 public:
 	ClosestFloorPointDebugHandler(int32 polygonI) : _polygonI(polygonI) {}
 
-	virtual void update() override
-	{
+	virtual void update() override {
 		auto mousePos2D = g_engine->input().debugInput().mousePos2D();
 		auto mousePos3D = g_engine->input().debugInput().mousePos3D();
 		auto floor = g_engine->player().currentRoom()->activeFloor();
@@ -64,8 +63,7 @@ class FloorIntersectionsDebugHandler final : public IDebugHandler {
 public:
 	FloorIntersectionsDebugHandler(int32 polygonI) : _polygonI(polygonI) {}
 
-	virtual void update() override
-	{
+	virtual void update() override {
 		auto floor = g_engine->player().currentRoom()->activeFloor();
 		auto renderer = dynamic_cast<IDebugRenderer *>(&g_engine->renderer());
 		if (floor == nullptr || renderer == nullptr)
@@ -90,8 +88,7 @@ public:
 private:
 	static constexpr float kMarkerLength = 16;
 
-	void drawIntersectionsFor(const Polygon& polygon, IDebugRenderer* renderer)
-	{
+	void drawIntersectionsFor(const Polygon &polygon, IDebugRenderer *renderer) {
 		auto &camera = g_engine->camera();
 		auto mousePos2D = g_engine->input().debugInput().mousePos2D();
 		auto mousePos3D = g_engine->input().debugInput().mousePos3D();
@@ -104,7 +101,7 @@ private:
 			auto mid = (a + b) / 2;
 			auto length = sqrtf(a.sqrDist(b));
 			auto normal = a - b;
-			normal = { normal.y, (int16)-normal.x};
+			normal = { normal.y, (int16)-normal.x };
 			auto inner = mid + normal * (kMarkerLength / length);
 
 			renderer->debugPolyline(a, b, kDebugGreen);
@@ -118,8 +115,7 @@ class TeleportCharacterDebugHandler final : public IDebugHandler {
 public:
 	TeleportCharacterDebugHandler(int32 kindI) : _kind((MainCharacterKind)kindI) {}
 
-	virtual void update() override
-	{
+	virtual void update() override {
 		g_engine->drawQueue().clear();
 		g_engine->player().drawCursor(true);
 		g_engine->drawQueue().draw();
@@ -149,8 +145,7 @@ public:
 	}
 
 private:
-	void teleport(MainCharacter &character, Point position)
-	{
+	void teleport(MainCharacter &character, Point position) {
 		auto currentRoom = g_engine->player().currentRoom();
 		if (character.room() != currentRoom)
 		{
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 93d8153449c..ddcb400234c 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -65,7 +65,8 @@ void Item::trigger() {
 
 ITriggerableObject::ITriggerableObject(ReadStream &stream)
 	: _interactionPoint(Shape(stream).firstPoint())
-	, _interactionDirection((Direction)stream.readSint32LE()) {}
+	, _interactionDirection((Direction)stream.readSint32LE()) {
+}
 
 void ITriggerableObject::onClick() {
 	auto heldItem = g_engine->player().heldItem();
@@ -286,7 +287,8 @@ struct SayTextTask final : public Task {
 	SayTextTask(Process &process, Character *character, int32 dialogId)
 		: Task(process)
 		, _character(character)
-		, _dialogId(dialogId) { }
+		, _dialogId(dialogId) {
+	}
 
 	virtual TaskReturn run() override {
 		TASK_BEGIN;
@@ -410,7 +412,8 @@ struct LerpLodBiasTask final : public Task {
 		: Task(process)
 		, _character(character)
 		, _targetLodBias(targetLodBias)
-		, _durationMs(durationMs) { }
+		, _durationMs(durationMs) {
+	}
 
 	virtual TaskReturn run() override {
 		TASK_BEGIN;
@@ -558,7 +561,7 @@ void WalkingCharacter::updateWalking() {
 			_lastWalkAnimFrame = 0;
 		}
 	}
-	
+
 	if (_pathPoints.empty()) {
 		_isWalking = false;
 		_currentPos = _sourcePos = targetPos;
@@ -569,8 +572,7 @@ void WalkingCharacter::updateWalking() {
 	_graphicNormal.topLeft() = _currentPos;
 }
 
-void WalkingCharacter::updateWalkingAnimation()
-{
+void WalkingCharacter::updateWalkingAnimation() {
 	_direction = getDirection(_sourcePos, _pathPoints.top());
 	auto animation = walkingAnimation();
 	_graphicNormal.setAnimation(animation);
@@ -721,7 +723,8 @@ void WalkingCharacter::serializeSave(Serializer &serializer) {
 struct ArriveTask : public Task {
 	ArriveTask(Process &process, const WalkingCharacter &character)
 		: Task(process)
-		, _character(character) {}
+		, _character(character) {
+	}
 
 	virtual TaskReturn run() override {
 		return _character.isWalking()
@@ -964,7 +967,8 @@ struct DialogMenuTask : public Task {
 	DialogMenuTask(Process &process, MainCharacter *character)
 		: Task(process)
 		, _input(g_engine->input())
-		, _character(character) {}
+		, _character(character) {
+	}
 
 	virtual TaskReturn run() override {
 		TASK_BEGIN;
@@ -1077,7 +1081,8 @@ const char *FloorColor::typeName() const { return "FloorColor"; }
 
 FloorColor::FloorColor(Room *room, ReadStream &stream)
 	: ObjectBase(room, stream)
-	, _shape(stream) {}
+	, _shape(stream) {
+}
 
 void FloorColor::drawDebug() {
 	auto renderer = dynamic_cast<IDebugRenderer *>(&g_engine->renderer());
diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index a103461e91e..69c41caf53f 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -188,8 +188,7 @@ struct CenterBottomTextTask : public Task {
 		, _durationMs(durationMs) {
 	}
 
-	TaskReturn run() override
-	{
+	TaskReturn run() override {
 		Font &font = g_engine->globalUI().dialogFont();
 		const char *text = g_engine->world().getDialogLine(_dialogId);
 		const Point pos(
@@ -209,8 +208,7 @@ struct CenterBottomTextTask : public Task {
 		TASK_END;
 	}
 
-	void debugPrint() override
-	{
+	void debugPrint() override {
 		uint32 remaining = g_system->getMillis() - _startTime <= _durationMs
 			? _durationMs - (g_system->getMillis() - _startTime)
 			: 0;
diff --git a/engines/alcachofa/graphics-opengl.cpp b/engines/alcachofa/graphics-opengl.cpp
index f341c49f42a..b8929231948 100644
--- a/engines/alcachofa/graphics-opengl.cpp
+++ b/engines/alcachofa/graphics-opengl.cpp
@@ -210,7 +210,7 @@ public:
 		 * SRC0_RGB is TEXTURE
 		 * SRC1_RGB/ALPHA is PRIMARY COLOR
 		 * COMBINE_ALPHA is REPLACE
-		 */ 
+		 */
 		switch (blendMode) {
 		case BlendMode::AdditiveAlpha:
 		case BlendMode::Additive:
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index e53759fefe8..170278ff3c4 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -492,7 +492,8 @@ Graphic::Graphic(const Graphic &other)
 	, _isLooping(other._isLooping)
 	, _lastTime(other._lastTime)
 	, _frameI(other._frameI)
-	, _depthScale(other._depthScale) {}
+	, _depthScale(other._depthScale) {
+}
 
 void Graphic::loadResources() {
 	if (_animation != nullptr)
@@ -745,23 +746,24 @@ void TextDrawRequest::draw() {
 FadeDrawRequest::FadeDrawRequest(FadeType type, float value, int8 order)
 	: IDrawRequest(order)
 	, _type(type)
-	, _value(value) {}
+	, _value(value) {
+}
 
 void FadeDrawRequest::draw() {
 	Color color;
 	const byte valueAsByte = (byte)(_value * 255);
 	switch (_type) {
-		case FadeType::ToBlack:
-			color = { 0, 0, 0, valueAsByte };
-			g_engine->renderer().setBlendMode(BlendMode::AdditiveAlpha);
-			break;
-		case FadeType::ToWhite:
-			color = { valueAsByte, valueAsByte, valueAsByte, valueAsByte };
-			g_engine->renderer().setBlendMode(BlendMode::Additive);
-			break;
-		default:
-			assert(false && "Invalid fade type");
-			return;
+	case FadeType::ToBlack:
+		color = { 0, 0, 0, valueAsByte };
+		g_engine->renderer().setBlendMode(BlendMode::AdditiveAlpha);
+		break;
+	case FadeType::ToWhite:
+		color = { valueAsByte, valueAsByte, valueAsByte, valueAsByte };
+		g_engine->renderer().setBlendMode(BlendMode::Additive);
+		break;
+	default:
+		assert(false && "Invalid fade type");
+		return;
 	}
 	g_engine->renderer().setTexture(nullptr);
 	g_engine->renderer().quad(Vector2d(0, 0), as2D(Point(g_system->getWidth(), g_system->getHeight())), color);
@@ -780,7 +782,8 @@ struct FadeTask : public Task {
 		, _duration(duration)
 		, _easingType(easingType)
 		, _order(order)
-		, _permanentFadeAction(permanentFadeAction){}
+		, _permanentFadeAction(permanentFadeAction) {
+	}
 
 	virtual TaskReturn run() override {
 		TASK_BEGIN;
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 8deecb306f5..866a65cde76 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -109,8 +109,7 @@ public:
 		Color color = kDebugRed
 	);
 
-	inline void debugPolyline(Common::Point a, Common::Point b, Color color = kDebugRed)
-	{
+	inline void debugPolyline(Common::Point a, Common::Point b, Color color = kDebugRed) {
 		Math::Vector2d points[] = { { (float)a.x, (float)a.y }, { (float)b.x, (float)b.y } };
 		debugPolygon({ points, 2 }, color);
 	}
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index ac1721b3e06..a28b1b17654 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -253,7 +253,7 @@ private:
 
 void Player::triggerDoor(const Door *door) {
 	_heldItem = nullptr;
-	
+
 	FakeLock lock(_activeCharacter->semaphore());
 	g_engine->scheduler().createProcess<DoorTask>(activeCharacterKind(), door, move(lock));
 }
diff --git a/engines/alcachofa/player.h b/engines/alcachofa/player.h
index 1b7139bc0cd..5917cd76a00 100644
--- a/engines/alcachofa/player.h
+++ b/engines/alcachofa/player.h
@@ -32,7 +32,7 @@ public:
 
 	inline Room *currentRoom() const { return _currentRoom; }
 	inline MainCharacter *activeCharacter() const { return _activeCharacter; }
-    inline ShapeObject *&selectedObject() { return _selectedObject; }
+	inline ShapeObject *&selectedObject() { return _selectedObject; }
 	inline void *&pressedObject() { return _pressedObject; }
 	inline Item *&heldItem() { return _heldItem; }
 	inline FakeSemaphore &semaphore() { return _semaphore; }
@@ -69,8 +69,8 @@ private:
 	Room *_currentRoom = nullptr,
 		*_roomBeforeInventory = nullptr;
 	MainCharacter *_activeCharacter;
-    ShapeObject *_selectedObject = nullptr;
-    void *_pressedObject = nullptr; // terrible but GlobalUI wants to store a Graphic pointer
+	ShapeObject *_selectedObject = nullptr;
+	void *_pressedObject = nullptr; // terrible but GlobalUI wants to store a Graphic pointer
 	Item *_heldItem = nullptr;
 	int32 _cursorFrameI = 0;
 	bool
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index a2a3f9ea2aa..6455aa904b7 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -560,9 +560,8 @@ void World::toggleObject(MainCharacterKind character, const char *objName, bool
 	ObjectBase *object = getObjectByName(character, objName);
 	if (object == nullptr)
 		object = getObjectByNameFromAnyRoom(objName);
-	if (object == nullptr)
+	if (object == nullptr) // I would have liked an error for this, but original inconsistencies...
 		warning("Tried to toggle unknown object: %s", objName);
-		// I would have liked an error for this, but original inconsistencies...
 	else
 		object->toggle(isEnabled);
 }
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index 039360255bc..84ab6a53102 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -191,7 +191,7 @@ private:
 	MainCharacter *_filemon, *_mortadelo;
 	uint8 _loadedMapCount = 0;
 	Common::HashMap<const char *, const char *,
-		Common::Hash<const char*>,
+		Common::Hash<const char *>,
 		StringEqualTo> _localizedNames;
 	Common::Array<const char *> _dialogLines;
 	Common::Array<char> _namesChunk, _dialogChunk; ///< holds the memory for localizedNames / dialogLines
diff --git a/engines/alcachofa/scheduler.cpp b/engines/alcachofa/scheduler.cpp
index 631e63671c8..822ba5487c9 100644
--- a/engines/alcachofa/scheduler.cpp
+++ b/engines/alcachofa/scheduler.cpp
@@ -57,9 +57,10 @@ Task *Task::delay(uint32 millis) {
 
 DelayTask::DelayTask(Process &process, uint32 millis)
 	: Task(process)
-	, _endTime(millis) {}
+	, _endTime(millis) {
+}
 
-TaskReturn DelayTask::run(){
+TaskReturn DelayTask::run() {
 	TASK_BEGIN;
 	_endTime += g_system->getMillis();
 	while (g_system->getMillis() < _endTime)
@@ -112,7 +113,7 @@ void Process::debugPrint() {
 	const char *characterName;
 	switch (_character) {
 	case MainCharacterKind::None: characterName = "    <none>"; break;
-	case MainCharacterKind::Filemon: characterName =   " Filemon"; break;
+	case MainCharacterKind::Filemon: characterName = " Filemon"; break;
 	case MainCharacterKind::Mortadelo: characterName = "Mortadelo"; break;
 	default: characterName = "<invalid>"; break;
 	}
diff --git a/engines/alcachofa/scheduler.h b/engines/alcachofa/scheduler.h
index d48b65faec2..983aacb7b9d 100644
--- a/engines/alcachofa/scheduler.h
+++ b/engines/alcachofa/scheduler.h
@@ -81,7 +81,7 @@ private:
 
 struct DelayTask : public Task {
 	DelayTask(Process &process, uint32 millis);
-	virtual TaskReturn run() override; 
+	virtual TaskReturn run() override;
 	virtual void debugPrint() override;
 
 private:
diff --git a/engines/alcachofa/script-debug.h b/engines/alcachofa/script-debug.h
index 1e7d9a7a7c1..168903c22f4 100644
--- a/engines/alcachofa/script-debug.h
+++ b/engines/alcachofa/script-debug.h
@@ -24,7 +24,7 @@
 
 namespace Alcachofa {
 
-static const char* const ScriptOpNames[] = {
+static const char *const ScriptOpNames[] = {
 	"Nop",
 	"Dup",
 	"PushAddr",
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 04724635019..3647bbf2649 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -41,7 +41,8 @@ enum ScriptDebugLevel {
 
 ScriptInstruction::ScriptInstruction(ReadStream &stream)
 	: _op((ScriptOp)stream.readSint32LE())
-	, _arg(stream.readSint32LE()) {}
+	, _arg(stream.readSint32LE()) {
+}
 
 Script::Script() {
 	File file;
@@ -122,7 +123,8 @@ bool Script::hasProcedure(const Common::String &procedure) const {
 struct ScriptTimerTask : public Task {
 	ScriptTimerTask(Process &process, int32 durationSec)
 		: Task(process)
-		, _durationSec(durationSec) {}
+		, _durationSec(durationSec) {
+	}
 
 	virtual TaskReturn run() override {
 		TASK_BEGIN;
@@ -211,7 +213,7 @@ struct ScriptTask : public Task {
 				if (_stack.empty())
 					debug("empty");
 				else {
-					const auto& top = _stack.top();
+					const auto &top = _stack.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;
@@ -454,7 +456,7 @@ private:
 	TObject *getObjectArg(uint argI) {
 		const char *const name = getStringArg(argI);
 		auto *object = g_engine->world().getObjectByName(process().character(), name);
-		return dynamic_cast<TObject*>(object);
+		return dynamic_cast<TObject *>(object);
 	}
 
 	MainCharacter &relatedCharacter() {
@@ -564,8 +566,8 @@ private:
 		case ScriptKernelTask::LerpWorldLodBias:
 			warning("STUB KERNEL CALL: LerpWorldLodBias");
 			return TaskReturn::finish(0);
-		
-		// object control / animation
+
+			// object control / animation
 		case ScriptKernelTask::On:
 			g_engine->world().toggleObject(process().character(), getStringArg(0), true);
 			return TaskReturn::finish(0);
@@ -604,7 +606,7 @@ private:
 			auto character = getObjectArg<WalkingCharacter>(0);
 			if (character == nullptr)
 				error("Script tried to make invalid character go: %s", getStringArg(0));
-			auto target= getObjectArg<PointObject>(1);
+			auto target = getObjectArg<PointObject>(1);
 			if (target == nullptr)
 				error("Script tried to make character go to invalid object %s", getStringArg(1));
 			character->walkTo(target->position());
@@ -722,7 +724,7 @@ private:
 			return TaskReturn::finish(1);
 		}
 		case ScriptKernelTask::ClearInventory:
-			switch((MainCharacterKind)getNumberArg(0)) {
+			switch ((MainCharacterKind)getNumberArg(0)) {
 			case MainCharacterKind::Mortadelo: g_engine->world().mortadelo().clearInventory(); break;
 			case MainCharacterKind::Filemon: g_engine->world().filemon().clearInventory(); break;
 			default: error("Script attempted to clear inventory with invalid character kind"); break;
@@ -855,7 +857,7 @@ private:
 	}
 
 	/**
-	 * @brief Check for original bugs related to the Put kernel call and handle them 
+	 * @brief Check for original bugs related to the Put kernel call and handle them
 	 * @param target An out reference to the point object (maybe we can find an alternative one)
 	 * @param targetName The given name of the target object
 	 * @return false if the put kernel call should be ignored, true if we set target and want to continue with the kernel call
diff --git a/engines/alcachofa/shape.cpp b/engines/alcachofa/shape.cpp
index c1c0f43574a..9a651dd8f8b 100644
--- a/engines/alcachofa/shape.cpp
+++ b/engines/alcachofa/shape.cpp
@@ -91,8 +91,7 @@ EdgeDistances Polygon::edgeDistances(uint startPointI, const Point &query) const
 	return distances;
 }
 
-static Point wiggleOnToLine(Point a, Point b, Point q)
-{
+static Point wiggleOnToLine(Point a, Point b, Point q) {
 	// due to rounding errors contains(bestPoint) might be false for on-edge closest points, let's fix that
 	// maybe there is a more mathematical solution to this, but it suffices for now
 	if (sideOfLine(a, b, q) >= 0) return q;
@@ -104,8 +103,7 @@ static Point wiggleOnToLine(Point a, Point b, Point q)
 	return q;
 }
 
-Point Polygon::closestPointTo(const Common::Point& query, float &distanceSqr) const
-{
+Point Polygon::closestPointTo(const Common::Point &query, float &distanceSqr) const {
 	assert(_points.size() > 0);
 	Common::Point bestPoint = {};
 	distanceSqr = std::numeric_limits<float>::infinity();
@@ -123,7 +121,7 @@ Point Polygon::closestPointTo(const Common::Point& query, float &distanceSqr) co
 		}
 		if (edgeDists._onEdge >= 0.0f && edgeDists._onEdge <= edgeDists._edgeLength)
 		{
-			float edgeDistSqr = powf(edgeDists._toEdge , 2.0f);
+			float edgeDistSqr = powf(edgeDists._toEdge, 2.0f);
 			if (edgeDistSqr < distanceSqr)
 			{
 				distanceSqr = edgeDistSqr;
@@ -131,7 +129,7 @@ Point Polygon::closestPointTo(const Common::Point& query, float &distanceSqr) co
 				bestPoint = _points[i] + (_points[j] - _points[i]) * (edgeDists._onEdge / edgeDists._edgeLength);
 				bestPoint = wiggleOnToLine(_points[i], _points[j], bestPoint);
 			}
-		}		
+		}
 	}
 	return bestPoint;
 }
@@ -307,8 +305,7 @@ bool Shape::contains(const Point &query) const {
 	return polygonContaining(query) >= 0;
 }
 
-Point Shape::closestPointTo(const Point &query, int32 &polygonI) const
-{
+Point Shape::closestPointTo(const Point &query, int32 &polygonI) const {
 	assert(_polygons.size() > 0);
 	float bestDistanceSqr = std::numeric_limits<float>::infinity();
 	Point bestPoint = {};
@@ -383,7 +380,7 @@ float PathFindingShape::depthAt(const Point &query) const {
 }
 
 PathFindingShape::LinkPolygonIndices::LinkPolygonIndices() {
-	Common::fill(_points, _points + kPointsPerPolygon, LinkIndex( -1, -1 ));
+	Common::fill(_points, _points + kPointsPerPolygon, LinkIndex(-1, -1));
 }
 
 static Pair<int32, int32> orderPoints(const Polygon &polygon, int32 point1, int32 point2) {
diff --git a/engines/alcachofa/shape.h b/engines/alcachofa/shape.h
index cb53495864e..ad338b30b4f 100644
--- a/engines/alcachofa/shape.h
+++ b/engines/alcachofa/shape.h
@@ -47,7 +47,7 @@ struct Polygon {
 	bool contains(const Common::Point &query) const;
 	bool intersectsEdge(uint startPointI, Common::Point a, Common::Point b) const;
 	EdgeDistances edgeDistances(uint startPointI, const Common::Point &query) const;
-	Common::Point closestPointTo(const Common::Point &query, float& distanceSqr) const;
+	Common::Point closestPointTo(const Common::Point &query, float &distanceSqr) const;
 	inline Common::Point closestPointTo(const Common::Point &query) const {
 		float dummy;
 		return closestPointTo(query, dummy);
@@ -94,14 +94,14 @@ struct PolygonIterator {
 		return tmp;
 	}
 
-	inline bool operator==(const my_type& it) const {
+	inline bool operator==(const my_type &it) const {
 		return &this->_shape == &it._shape && this->_index == it._index;
 	}
 
-	inline bool operator!=(const my_type& it) const {
+	inline bool operator!=(const my_type &it) const {
 		return &this->_shape != &it._shape || this->_index != it._index;
 	}
-	
+
 private:
 	friend typename Common::remove_const_t<TShape>;
 	PolygonIterator(const TShape &shape, uint index = 0)
@@ -177,7 +177,7 @@ public:
 		Common::Point centerTarget,
 		float depthScale,
 		float minDistSqr,
-		Common::Point& evadeTarget) const;
+		Common::Point &evadeTarget) const;
 
 private:
 	using LinkIndex = Common::Pair<int32, int32>;
@@ -213,7 +213,7 @@ private:
 	 */
 	Common::Array<Common::Point> _linkPoints;
 	/**
-	 * For each point of each polygon the index (or -1) to 
+	 * For each point of each polygon the index (or -1) to
 	 * the corresponding link point. The second point is the
 	 * index to the artifical center point
 	 */
@@ -223,7 +223,7 @@ private:
 	};
 	Common::Array<LinkPolygonIndices> _linkIndices;
 	/**
-	 * For the going-straight-through-edges check we need 
+	 * For the going-straight-through-edges check we need
 	 * to know for each shared edge (defined by the starting point)
 	 * into which quad we will walk.
 	 */
diff --git a/engines/alcachofa/sounds.cpp b/engines/alcachofa/sounds.cpp
index dbc583f3ed2..75e8e9eb0ba 100644
--- a/engines/alcachofa/sounds.cpp
+++ b/engines/alcachofa/sounds.cpp
@@ -36,7 +36,8 @@ using namespace Audio;
 namespace Alcachofa {
 
 Sounds::Playback::Playback(uint32 id, SoundHandle handle, Mixer::SoundType type)
-	: _id(id), _handle(handle), _type(type) {}
+	: _id(id), _handle(handle), _type(type) {
+}
 
 void Sounds::Playback::fadeOut(uint32 duration) {
 	_fadeStart = g_system->getMillis();
@@ -205,7 +206,8 @@ void Sounds::fadeOutVoiceAndSFX(uint32 duration) {
 
 PlaySoundTask::PlaySoundTask(Process &process, SoundID soundID)
 	: Task(process)
-	, _soundID(soundID) { }
+	, _soundID(soundID) {
+}
 
 TaskReturn PlaySoundTask::run() {
 	auto &sounds = g_engine->sounds();


Commit: ea8535969395b192cfbf93643b10fcc778e9d63c
    https://github.com/scummvm/scummvm/commit/ea8535969395b192cfbf93643b10fcc778e9d63c
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:53+02:00

Commit Message:
ALCACHOFA: Code conventions - Pass Point by value

Changed paths:
    engines/alcachofa/common.cpp
    engines/alcachofa/common.h
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/graphics.h
    engines/alcachofa/input.h
    engines/alcachofa/objects.h
    engines/alcachofa/rooms.cpp
    engines/alcachofa/rooms.h
    engines/alcachofa/shape.cpp
    engines/alcachofa/shape.h


diff --git a/engines/alcachofa/common.cpp b/engines/alcachofa/common.cpp
index 27290532b29..4d418c3a230 100644
--- a/engines/alcachofa/common.cpp
+++ b/engines/alcachofa/common.cpp
@@ -78,7 +78,7 @@ Vector3d as3D(const Vector2d &v) {
 	return Vector3d(v.getX(), v.getY(), 0.0f);
 }
 
-Vector3d as3D(const Common::Point &p) {
+Vector3d as3D(Common::Point p) {
 	return Vector3d((float)p.x, (float)p.y, 0.0f);
 }
 
@@ -86,7 +86,7 @@ Vector2d as2D(const Vector3d &v) {
 	return Vector2d(v.x(), v.y());
 }
 
-Vector2d as2D(const Point &p) {
+Vector2d as2D(Point p) {
 	return Vector2d((float)p.x, (float)p.y);
 }
 
diff --git a/engines/alcachofa/common.h b/engines/alcachofa/common.h
index 733f609b58b..5d7910e5ac9 100644
--- a/engines/alcachofa/common.h
+++ b/engines/alcachofa/common.h
@@ -108,9 +108,9 @@ private:
 float ease(float t, EasingType type);
 
 Math::Vector3d as3D(const Math::Vector2d &v);
-Math::Vector3d as3D(const Common::Point &p);
+Math::Vector3d as3D(Common::Point p);
 Math::Vector2d as2D(const Math::Vector3d &v);
-Math::Vector2d as2D(const Common::Point &p);
+Math::Vector2d as2D(Common::Point p);
 
 bool readBool(Common::ReadStream &stream);
 Common::Point readPoint(Common::ReadStream &stream);
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index ddcb400234c..1d5a9b143d9 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -514,7 +514,7 @@ void WalkingCharacter::update() {
 	}
 }
 
-static Direction getDirection(const Point &from, const Point &to) {
+static Direction getDirection(Point from, Point to) {
 	Point delta = from - to;
 	if (from.x == to.x)
 		return from.y < to.y ? Direction::Down : Direction::Up;
@@ -622,7 +622,7 @@ void WalkingCharacter::stopWalking(Direction direction) {
 }
 
 void WalkingCharacter::walkTo(
-	const Point &target, Direction endDirection,
+	Point target, Direction endDirection,
 	ITriggerableObject *activateObject, const char *activateAction) {
 	// all the activation parameters are only relevant for MainCharacter
 
@@ -651,7 +651,7 @@ void WalkingCharacter::walkTo(
 	updateWalking();
 }
 
-void WalkingCharacter::setPosition(const Point &target) {
+void WalkingCharacter::setPosition(Point target) {
 	_isWalking = false;
 	_sourcePos = _currentPos = target;
 }
@@ -794,7 +794,7 @@ void MainCharacter::onArrived() {
 }
 
 void MainCharacter::walkTo(
-	const Point &target_, Direction endDirection,
+	Point target_, Direction endDirection,
 	ITriggerableObject *activateObject, const char *activateAction) {
 	_activateObject = activateObject;
 	_activateAction = activateAction;
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 866a65cde76..45b644e1276 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -65,7 +65,7 @@ public:
 	virtual void update(const Graphics::Surface &surface) = 0;
 	inline void update(const Graphics::ManagedSurface &surface) { update(surface.rawSurface()); }
 
-	inline const Common::Point &size() const { return _size; }
+	inline Common::Point size() const { return _size; }
 
 private:
 	Common::Point _size;
@@ -187,7 +187,7 @@ public:
 	inline uint spriteCount() const { return _spriteBases.size(); }
 	inline uint frameCount() const { return _frames.size(); }
 	inline uint32 frameDuration(int32 frameI) const { return _frames[frameI]._duration; }
-	inline const Common::Point &frameCenter(int32 frameI) const { return _frames[frameI]._center; }
+	inline Common::Point frameCenter(int32 frameI) const { return _frames[frameI]._center; }
 	inline uint32 totalDuration() const { return _totalDuration; }
 	inline uint8 &premultiplyAlpha() { return _premultiplyAlpha; }
 	Common::Rect frameBounds(int32 frameI) const;
diff --git a/engines/alcachofa/input.h b/engines/alcachofa/input.h
index 24af6e2ad4b..e80287fd61e 100644
--- a/engines/alcachofa/input.h
+++ b/engines/alcachofa/input.h
@@ -38,8 +38,8 @@ public:
 	inline bool isMouseLeftDown() const { return _isMouseLeftDown; }
 	inline bool isMouseRightDown() const { return _isMouseRightDown; }
 	inline bool isAnyMouseDown() const { return _isMouseLeftDown || _isMouseRightDown; }
-	inline const Common::Point &mousePos2D() const { return _mousePos2D; }
-	inline const Common::Point &mousePos3D() const { return _mousePos3D; }
+	inline Common::Point mousePos2D() const { return _mousePos2D; }
+	inline Common::Point mousePos3D() const { return _mousePos3D; }
 	const Input &debugInput() const { scumm_assert(_debugInput != nullptr); return *_debugInput; }
 
 	void nextFrame();
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 8738d4b6f39..831c99faf57 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -325,7 +325,7 @@ public:
 	ITriggerableObject(Common::ReadStream &stream);
 
 	inline Direction interactionDirection() const { return _interactionDirection; }
-	inline const Common::Point &interactionPoint() const { return _interactionPoint; }
+	inline Common::Point interactionPoint() const { return _interactionPoint; }
 
 	virtual void trigger(const char *action) = 0;
 
@@ -420,7 +420,7 @@ public:
 	virtual ~WalkingCharacter() override = default;
 
 	inline bool isWalking() const { return _isWalking; }
-	inline const Common::Point &position() const { return _currentPos; }
+	inline Common::Point position() const { return _currentPos; }
 	inline float stepSizeFactor() const { return _stepSizeFactor; }
 
 	virtual void update() override;
@@ -430,12 +430,12 @@ public:
 	virtual void freeResources() override;
 	virtual void serializeSave(Common::Serializer &serializer) override;
 	virtual void walkTo(
-		const Common::Point &target,
+		Common::Point target,
 		Direction endDirection = Direction::Invalid,
 		ITriggerableObject *activateObject = nullptr,
 		const char *activateAction = nullptr);
 	void stopWalking(Direction direction = Direction::Invalid);
-	void setPosition(const Common::Point &target);
+	void setPosition(Common::Point target);
 	virtual const char *typeName() const;
 
 	Task *waitForArrival(Process &process);
@@ -497,7 +497,7 @@ public:
 	virtual void serializeSave(Common::Serializer &serializer) override;
 	virtual const char *typeName() const;
 	virtual void walkTo(
-		const Common::Point &target,
+		Common::Point target,
 		Direction endDirection = Direction::Invalid,
 		ITriggerableObject *activateObject = nullptr,
 		const char *activateAction = nullptr) override;
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 6455aa904b7..a4fe4b3ddf5 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -372,7 +372,7 @@ bool Inventory::updateInput() {
 }
 
 Item *Inventory::getHoveredItem() {
-	auto &mousePos = g_engine->input().mousePos2D();
+	auto mousePos = g_engine->input().mousePos2D();
 	for (auto item : _items) {
 		if (!item->isEnabled())
 			continue;
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index 84ab6a53102..8fad2cf1d93 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -39,10 +39,10 @@ public:
 	inline const PathFindingShape *activeFloor() const {
 		return _activeFloorI < 0 ? nullptr : &_floors[_activeFloorI];
 	}
-	inline int8 orderAt(const Common::Point &query) const {
+	inline int8 orderAt(Common::Point query) const {
 		return _activeFloorI < 0 ? 49 : activeFloor()->orderAt(query);
 	}
-	inline float depthAt(const Common::Point &query) const {
+	inline float depthAt(Common::Point query) const {
 		return _activeFloorI < 0 ? 1 : activeFloor()->depthAt(query);
 	}
 	inline uint8 characterAlphaTint() const { return _characterAlphaTint; }
diff --git a/engines/alcachofa/shape.cpp b/engines/alcachofa/shape.cpp
index 9a651dd8f8b..65dae596388 100644
--- a/engines/alcachofa/shape.cpp
+++ b/engines/alcachofa/shape.cpp
@@ -26,17 +26,17 @@ using namespace Math;
 
 namespace Alcachofa {
 
-static int sideOfLine(const Point &a, const Point &b, const Point &q) {
+static int sideOfLine(Point a, Point b, Point q) {
 	return (b.x - a.x) * (q.y - a.y) - (b.y - a.y) * (q.x - a.x);
 }
 
-static bool segmentsIntersect(const Point &a1, const Point &b1, const Point &a2, const Point &b2) {
+static bool segmentsIntersect(Point a1, Point b1, Point a2, Point b2) {
 	// as there are a number of special cases to consider,
 	// this method is a direct translation of the original engine
-	const auto sideOfLine = [](const Point &a, const Point &b, const Point q) {
+	const auto sideOfLine = [](Point a, Point b, const Point q) {
 		return Alcachofa::sideOfLine(a, b, q) > 0;
 	};
-	const auto lineIntersects = [&](const Point &a1, const Point &b1, const Point &a2, const Point &b2) {
+	const auto lineIntersects = [&](Point a1, Point b1, Point a2, Point b2) {
 		return sideOfLine(a1, b1, a2) != sideOfLine(a1, b1, b2);
 	};
 
@@ -54,7 +54,7 @@ static bool segmentsIntersect(const Point &a1, const Point &b1, const Point &a2,
 	}
 }
 
-bool Polygon::contains(const Point &query) const {
+bool Polygon::contains(Point query) const {
 	switch (_points.size()) {
 	case 0: return false;
 	case 1: return query == _points[0];
@@ -74,7 +74,7 @@ bool Polygon::intersectsEdge(uint startPointI, Point a, Point b) const {
 	return segmentsIntersect(_points[startPointI], _points[endPointI], a, b);
 }
 
-EdgeDistances Polygon::edgeDistances(uint startPointI, const Point &query) const {
+EdgeDistances Polygon::edgeDistances(uint startPointI, Point query) const {
 	assert(startPointI < _points.size());
 	uint endPointI = startPointI + 1 == _points.size() ? 0 : startPointI + 1;
 	Vector2d
@@ -103,7 +103,7 @@ static Point wiggleOnToLine(Point a, Point b, Point q) {
 	return q;
 }
 
-Point Polygon::closestPointTo(const Common::Point &query, float &distanceSqr) const {
+Point Polygon::closestPointTo(Point query, float &distanceSqr) const {
 	assert(_points.size() > 0);
 	Common::Point bestPoint = {};
 	distanceSqr = std::numeric_limits<float>::infinity();
@@ -142,11 +142,11 @@ Point Polygon::midPoint() const {
 	return sum / (int16)_points.size();
 }
 
-static float depthAtForLine(const Point &a, const Point &b, const Point &q, int8 depthA, int8 depthB) {
+static float depthAtForLine(Point a, Point b, Point q, int8 depthA, int8 depthB) {
 	return (sqrtf(a.sqrDist(q)) / a.sqrDist(b) * depthB + depthA) * 0.01f;
 }
 
-static float depthAtForConvex(const PathFindingPolygon &p, const Point &q) {
+static float depthAtForConvex(const PathFindingPolygon &p, Point q) {
 	float sumDepths = 0, sumDistances = 0;
 	for (uint i = 0; i < p._points.size(); i++) {
 		uint j = i + 1 == p._points.size() ? 0 : i + 1;
@@ -160,7 +160,7 @@ static float depthAtForConvex(const PathFindingPolygon &p, const Point &q) {
 	return sumDepths / sumDistances * 0.01f;
 }
 
-float PathFindingPolygon::depthAt(const Point &query) const {
+float PathFindingPolygon::depthAt(Point query) const {
 	switch (_points.size()) {
 	case 0:
 	case 1: return 1.0f;
@@ -184,14 +184,14 @@ uint PathFindingPolygon::findSharedPoints(
 	return count;
 }
 
-static Color colorAtForLine(const Point &a, const Point &b, const Point &q, Color colorA, Color colorB) {
+static Color colorAtForLine(Point a, Point b, Point q, Color colorA, Color colorB) {
 	// I highly suspect RGB calculation being very bugged, so for now I just ignore and only calc alpha
 	float phase = sqrtf(q.sqrDist(a)) / a.sqrDist(b);
 	colorA.a += phase * colorB.a;
 	return colorA;
 }
 
-static Color colorAtForConvex(const FloorColorPolygon &p, const Point &query) {
+static Color colorAtForConvex(const FloorColorPolygon &p, Point query) {
 	// This is a quite literal translation of the original engine
 	// There may very well be a better way than this...
 	float weights[FloorColorShape::kPointsPerPolygon];
@@ -235,7 +235,7 @@ static Color colorAtForConvex(const FloorColorPolygon &p, const Point &query) {
 	};
 }
 
-Color FloorColorPolygon::colorAt(const Point &query) const {
+Color FloorColorPolygon::colorAt(Point query) const {
 	switch (_points.size()) {
 	case 0: return kWhite;
 	case 1: return { 255, 255, 255, _pointColors[0].a };
@@ -293,7 +293,7 @@ Polygon Shape::at(uint index) const {
 	return p;
 }
 
-int32 Shape::polygonContaining(const Point &query) const {
+int32 Shape::polygonContaining(Point query) const {
 	for (uint i = 0; i < _polygons.size(); i++) {
 		if (at(i).contains(query))
 			return (int32)i;
@@ -301,11 +301,11 @@ int32 Shape::polygonContaining(const Point &query) const {
 	return -1;
 }
 
-bool Shape::contains(const Point &query) const {
+bool Shape::contains(Point query) const {
 	return polygonContaining(query) >= 0;
 }
 
-Point Shape::closestPointTo(const Point &query, int32 &polygonI) const {
+Point Shape::closestPointTo(Point query, int32 &polygonI) const {
 	assert(_polygons.size() > 0);
 	float bestDistanceSqr = std::numeric_limits<float>::infinity();
 	Point bestPoint = {};
@@ -369,12 +369,12 @@ PathFindingPolygon PathFindingShape::at(uint index) const {
 	return p;
 }
 
-int8 PathFindingShape::orderAt(const Point &query) const {
+int8 PathFindingShape::orderAt(Point query) const {
 	int32 polygon = polygonContaining(query);
 	return polygon < 0 ? 49 : _polygonOrders[polygon];
 }
 
-float PathFindingShape::depthAt(const Point &query) const {
+float PathFindingShape::depthAt(Point query) const {
 	int32 polygon = polygonContaining(query);
 	return polygon < 0 ? 1.0f : at(polygon).depthAt(query);
 }
@@ -507,7 +507,7 @@ void PathFindingShape::calculateFloydWarshall() {
 	assert(find(_previousTarget.begin(), _previousTarget.end(), -1) == _previousTarget.end());
 }
 
-bool PathFindingShape::findPath(const Point &from, const Point &to_, Stack<Point> &path) const {
+bool PathFindingShape::findPath(Point from, Point to_, Stack<Point> &path) const {
 	Point to = to_; // we might want to correct it
 	path.clear();
 
@@ -535,7 +535,7 @@ int32 PathFindingShape::edgeTarget(uint polygonI, uint pointI) const {
 }
 
 bool PathFindingShape::canGoStraightThrough(
-	const Point &from, const Point &to,
+	Point from, Point to,
 	int32 fromContainingI, int32 toContainingI) const {
 	int32 lastContainingI = -1;
 	while (fromContainingI != toContainingI) {
@@ -560,7 +560,7 @@ bool PathFindingShape::canGoStraightThrough(
 }
 
 void PathFindingShape::floydWarshallPath(
-	const Point &from, const Point &to,
+	Point from, Point to,
 	int32 fromContaining, int32 toContaining,
 	Stack<Point> &path) const {
 	path.push(to);
@@ -663,7 +663,7 @@ FloorColorPolygon FloorColorShape::at(uint index) const {
 	return p;
 }
 
-OptionalColor FloorColorShape::colorAt(const Common::Point &query) const {
+OptionalColor FloorColorShape::colorAt(Point query) const {
 	int32 polygon = polygonContaining(query);
 	return polygon < 0
 		? OptionalColor(false, kClear)
diff --git a/engines/alcachofa/shape.h b/engines/alcachofa/shape.h
index ad338b30b4f..2fae40a5ac9 100644
--- a/engines/alcachofa/shape.h
+++ b/engines/alcachofa/shape.h
@@ -44,11 +44,11 @@ struct Polygon {
 	uint _index;
 	Common::Span<const Common::Point> _points;
 
-	bool contains(const Common::Point &query) const;
+	bool contains(Common::Point query) const;
 	bool intersectsEdge(uint startPointI, Common::Point a, Common::Point b) const;
-	EdgeDistances edgeDistances(uint startPointI, const Common::Point &query) const;
-	Common::Point closestPointTo(const Common::Point &query, float &distanceSqr) const;
-	inline Common::Point closestPointTo(const Common::Point &query) const {
+	EdgeDistances edgeDistances(uint startPointI, Common::Point query) const;
+	Common::Point closestPointTo(Common::Point query, float &distanceSqr) const;
+	inline Common::Point closestPointTo(Common::Point query) const {
 		float dummy;
 		return closestPointTo(query, dummy);
 	}
@@ -61,14 +61,14 @@ struct PathFindingPolygon : Polygon {
 
 	using SharedPoint = Common::Pair<uint, uint>;
 
-	float depthAt(const Common::Point &query) const;
+	float depthAt(Common::Point query) const;
 	uint findSharedPoints(const PathFindingPolygon &other, Common::Span<SharedPoint> sharedPoints) const;
 };
 
 struct FloorColorPolygon : Polygon {
 	Common::Span<const Color> _pointColors;
 
-	Color colorAt(const Common::Point &query) const;
+	Color colorAt(Common::Point query) const;
 };
 
 template<class TShape, typename TPolygon>
@@ -127,10 +127,10 @@ public:
 	inline iterator end() const { return { *this, polygonCount() }; }
 
 	Polygon at(uint index) const;
-	int32 polygonContaining(const Common::Point &query) const;
-	bool contains(const Common::Point &query) const;
-	Common::Point closestPointTo(const Common::Point &query, int32 &polygonI) const;
-	inline Common::Point closestPointTo(const Common::Point &query) const {
+	int32 polygonContaining(Common::Point query) const;
+	bool contains(Common::Point query) const;
+	Common::Point closestPointTo(Common::Point query, int32 &polygonI) const;
+	inline Common::Point closestPointTo(Common::Point query) const {
 		int32 dummy;
 		return closestPointTo(query, dummy);
 	}
@@ -166,11 +166,11 @@ public:
 	inline iterator end() const { return { *this, polygonCount() }; }
 
 	PathFindingPolygon at(uint index) const;
-	int8 orderAt(const Common::Point &query) const;
-	float depthAt(const Common::Point &query) const;
+	int8 orderAt(Common::Point query) const;
+	float depthAt(Common::Point query) const;
 	bool findPath(
-		const Common::Point &from,
-		const Common::Point &to,
+		Common::Point from,
+		Common::Point to,
 		Common::Stack<Common::Point> &path) const;
 	int32 edgeTarget(uint polygonI, uint pointI) const;
 	bool findEvadeTarget(
@@ -194,12 +194,12 @@ private:
 	void initializeFloydWarshall();
 	void calculateFloydWarshall();
 	bool canGoStraightThrough(
-		const Common::Point &from,
-		const Common::Point &to,
+		Common::Point from,
+		Common::Point to,
 		int32 fromContaining, int32 toContaining) const;
 	void floydWarshallPath(
-		const Common::Point &from,
-		const Common::Point &to,
+		Common::Point from,
+		Common::Point to,
 		int32 fromContaining, int32 toContaining,
 		Common::Stack<Common::Point> &path) const;
 
@@ -245,7 +245,7 @@ public:
 	inline iterator end() const { return { *this, polygonCount() }; }
 
 	FloorColorPolygon at(uint index) const;
-	OptionalColor colorAt(const Common::Point &query) const;
+	OptionalColor colorAt(Common::Point query) const;
 
 private:
 	Common::Array<Color> _pointColors;


Commit: 54e1201f92e4d91c0b0d48318246fac184c86f22
    https://github.com/scummvm/scummvm/commit/54e1201f92e4d91c0b0d48318246fac184c86f22
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:53+02:00

Commit Message:
ALCACHOFA: Fix taking and combining inventory items

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


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 1d5a9b143d9..8cc4fc6a99a 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -45,6 +45,14 @@ Item::Item(const Item &other)
 	new (&_graphic) Graphic(other._graphic);
 }
 
+void Item::draw() {
+	if (!isEnabled())
+		return;
+	Item* heldItem = g_engine->player().heldItem();
+	if (heldItem == nullptr || !heldItem->name().equalsIgnoreCase(name()))
+		GraphicObject::draw();
+}
+
 void Item::trigger() {
 	auto &player = g_engine->player();
 	auto &heldItem = player.heldItem();
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 831c99faf57..7ce235eac67 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -316,6 +316,7 @@ public:
 	Item(Room *room, Common::ReadStream &stream);
 	Item(const Item &other);
 
+	virtual void draw() override;
 	virtual const char *typeName() const;
 	void trigger();
 };
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index a28b1b17654..81a06220279 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -182,7 +182,9 @@ void Player::triggerObject(ObjectBase *object, const char *action) {
 	auto &script = g_engine->script();
 	if (script.createProcess(activeCharacterKind(), object->name(), action, ScriptFlags::AllowMissing) != nullptr)
 		return;
-	else if (scumm_stricmp(action, "MIRAR") == 0)
+
+	_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


Commit: 7bcb5732070287479acce83fdb84c6b183c6939b
    https://github.com/scummvm/scummvm/commit/7bcb5732070287479acce83fdb84c6b183c6939b
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:53+02:00

Commit Message:
ALCACHOFA: Fix blending modes

This also fixes the characters not being blacked silhouettes in the intro

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


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 8cc4fc6a99a..4f6e9e9f281 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -868,7 +868,7 @@ void MainCharacter::drawInner() {
 	}
 
 	assert(activeGraphic != nullptr);
-	activeGraphic->color() = kWhite; // TODO: Add and use character color
+	activeGraphic->color().a = room()->characterAlphaTint() * 255 / 100;
 	g_engine->drawQueue().add<AnimationDrawRequest>(*activeGraphic, true, BlendMode::AdditiveAlpha, _lodBias);
 
 }
diff --git a/engines/alcachofa/graphics-opengl.cpp b/engines/alcachofa/graphics-opengl.cpp
index b8929231948..5d8428ba6ea 100644
--- a/engines/alcachofa/graphics-opengl.cpp
+++ b/engines/alcachofa/graphics-opengl.cpp
@@ -146,10 +146,6 @@ public:
 	virtual void begin() override {
 		GL_CALL(glEnableClientState(GL_VERTEX_ARRAY));
 		GL_CALL(glDisableClientState(GL_INDEX_ARRAY));
-		GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE));
-		GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_TEXTURE));
-		GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_PRIMARY_COLOR));
-		GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_ALPHA, GL_PRIMARY_COLOR));
 		_currentLodBias = -1000.0f;
 		_currentTexture = nullptr;
 		_currentBlendMode = (BlendMode)-1;
@@ -206,30 +202,45 @@ public:
 		default: assert(false && "Invalid blend mode"); break;
 		}
 
-		/** now the texture stage, mind that this always applies:
-		 * SRC0_RGB is TEXTURE
-		 * SRC1_RGB/ALPHA is PRIMARY COLOR
-		 * COMBINE_ALPHA is REPLACE
-		 */
+		GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE));
 		switch (blendMode) {
 		case BlendMode::AdditiveAlpha:
 		case BlendMode::Additive:
 		case BlendMode::Multiply:
-			// (1 - TintAlpha) * TexColor, TexAlpha
+			// TintAlpha * TexColor, TexAlpha
 			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE));
-			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_ONE_MINUS_SRC_ALPHA));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE));
+
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_TEXTURE));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR));
 			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_ALPHA, GL_TEXTURE));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA));
+
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_PRIMARY_COLOR));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_ALPHA)); // alpha replaces color
 			break;
 		case BlendMode::Alpha:
 			// TexColor, TintAlpha
 			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_REPLACE));
-			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_ALPHA, GL_CONSTANT));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE));
+
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_TEXTURE));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_ALPHA, GL_PRIMARY_COLOR));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA));
 			break;
 		case BlendMode::Tinted:
 			// (TintColor * TintAlpha) * TexColor, TexAlpha
 			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE));
-			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR)); // pre-multiplied with alpha
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE));
+
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_TEXTURE));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR));
 			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_ALPHA, GL_TEXTURE));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA));
+
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_PRIMARY_COLOR));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR)); // we have to pre-multiply
 			break;
 		default: assert(false && "Invalid blend mode"); break;
 		}
@@ -274,12 +285,17 @@ public:
 		}
 
 		float colors[] = { color.r / 255.0f, color.g / 255.0f, color.b / 255.0f, color.a / 255.0f };
+		if (_currentBlendMode == BlendMode::Tinted)
+		{
+			colors[0] *= colors[3];
+			colors[1] *= colors[3];
+			colors[2] *= colors[3];
+		}
 
-		GL_CALL(glColor4f(color.r / 255.0f, color.g / 255.0f, color.b / 255.0f, color.a / 255.0f));
+		GL_CALL(glColor4fv(colors));
 		GL_CALL(glVertexPointer(2, GL_FLOAT, 0, positions));
 		if (_currentTexture != nullptr)
 			GL_CALL(glTexCoordPointer(2, GL_FLOAT, 0, texCoords));
-		GL_CALL(glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, colors));
 		GL_CALL(glDrawArrays(GL_QUADS, 0, 4));
 
 #if _DEBUG
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 170278ff3c4..e242db5e87d 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -317,7 +317,7 @@ int32 Animation::frameAtTime(uint32 time) const {
 
 void Animation::prerenderFrame(int32 frameI) {
 	assert(frameI >= 0 && (uint)frameI < frameCount());
-	if (frameI == _renderedFrameI)
+	if (frameI == _renderedFrameI && _renderedPremultiplyAlpha == _premultiplyAlpha)
 		return;
 	auto bounds = frameBounds(frameI);
 	_renderedSurface.clear();
@@ -331,16 +331,11 @@ void Animation::prerenderFrame(int32 frameI) {
 		fullBlend(*image, _renderedSurface, offsetX, offsetY);
 	}
 
-	/* TODO: Find a situation where this is actually used, otherwise this currently just produces bugs
-	if (_premultiplyAlpha != 100) {
-		byte *itPixel = (byte*)_renderedSurface.getPixels();
-		uint componentCount = _renderedSurface.w * _renderedSurface.h * 4;
-		for (uint32 i = 0; i < componentCount; i++, itPixel++)
-			*itPixel = *itPixel * _premultiplyAlpha / 100;
-	}*/
+	// Here was some alpha premultiplication, but it only produces bugs so is ignored
 
 	_renderedTexture->update(_renderedSurface);
 	_renderedFrameI = frameI;
+	_renderedPremultiplyAlpha = _premultiplyAlpha;
 }
 
 void Animation::draw2D(int32 frameI, Vector2d topLeft, float scale, BlendMode blendMode, Color color) {
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 45b644e1276..351f5508257 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -48,11 +48,11 @@ namespace Alcachofa {
  *
  */
 enum class BlendMode {
-	AdditiveAlpha,
-	Additive,
-	Multiply,
-	Alpha,
-	Tinted
+	AdditiveAlpha, // Normal objects
+	Additive,      // "Effect" objects, fades
+	Multiply,      // Unused in Movie Adventure
+	Alpha,         // Unused in Movie Adventure (used for debugging)
+	Tinted         // Used for fonts
 };
 
 class Shape;
@@ -221,7 +221,8 @@ private:
 	void prerenderFrame(int32 frameI);
 
 	int32_t _renderedFrameI = -1;
-	uint8 _premultiplyAlpha = 100; ///< in percent [0-100] not [0-255]
+	uint8 _premultiplyAlpha = 100, ///< in percent [0-100] not [0-255]
+		_renderedPremultiplyAlpha = 255;
 
 	Graphics::ManagedSurface _renderedSurface;
 	Common::ScopedPtr<ITexture> _renderedTexture;


Commit: 22bace3365b2139f7f5870af9f8d4cc0d29d0b2b
    https://github.com/scummvm/scummvm/commit/22bace3365b2139f7f5870af9f8d4cc0d29d0b2b
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:53+02:00

Commit Message:
ALCACHOFA: Fix invalid door target in LABERINTO

Changed paths:
    engines/alcachofa/player.cpp


diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index 81a06220279..a8a240fe1dd 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -256,6 +256,9 @@ private:
 void Player::triggerDoor(const Door *door) {
 	_heldItem = nullptr;
 
+	if (door->targetRoom() == "LABERINTO" && door->targetObject() == "a_LABERINTO_desde_LABERINTO_2")
+		return; // Original exception
+
 	FakeLock lock(_activeCharacter->semaphore());
 	g_engine->scheduler().createProcess<DoorTask>(activeCharacterKind(), door, move(lock));
 }


Commit: a905192ce6a34cc56dbc9955d7789c1e53ee8ef9
    https://github.com/scummvm/scummvm/commit/a905192ce6a34cc56dbc9955d7789c1e53ee8ef9
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:53+02:00

Commit Message:
ALCACHOFA: Fix draw order of hovered object names

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


diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index f1e811f2dcd..c240db4a245 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -231,7 +231,7 @@ void ShapeObject::onHoverUpdate() {
 		g_engine->globalUI().generalFont(),
 		g_engine->world().getLocalizedName(name()),
 		g_engine->input().mousePos2D() - Point(0, 35),
-		-1, true, kWhite, 0);
+		-1, true, kWhite, -kForegroundOrderCount);
 }
 
 void ShapeObject::onClick() {


Commit: ffedb0376c60fd0c1044545683cdca25a7f9ca0b
    https://github.com/scummvm/scummvm/commit/ffedb0376c60fd0c1044545683cdca25a7f9ca0b
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:53+02:00

Commit Message:
ALCACHOFA: Fix invisible cursor in DINOSAURIO

Changed paths:
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 3647bbf2649..66d1f73ab8f 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -135,6 +135,7 @@ struct ScriptTimerTask : public Task {
 				_result = g_engine->script().variable("SeHaPulsadoRaton") ? 0 : 2;
 			else
 				_result = 1;
+			g_engine->player().drawCursor();
 		}
 		TASK_YIELD; // Wait a frame to not produce an endless loop
 		TASK_RETURN(_result);


Commit: 26a1af2f93165ede9d6550074e9023d0f8901552
    https://github.com/scummvm/scummvm/commit/26a1af2f93165ede9d6550074e9023d0f8901552
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:54+02:00

Commit Message:
ALCACHOFA: Fix crash with zero-size sound files

Changed paths:
    engines/alcachofa/sounds.cpp


diff --git a/engines/alcachofa/sounds.cpp b/engines/alcachofa/sounds.cpp
index 75e8e9eb0ba..e9562bd07b0 100644
--- a/engines/alcachofa/sounds.cpp
+++ b/engines/alcachofa/sounds.cpp
@@ -118,7 +118,9 @@ static AudioStream *openAudio(const String &fileName) {
 	String path = String::format("Sonidos/%s.SND", fileName.c_str());
 	File *file = new File();
 	if (file->open(path.c_str()))
-		return loadSND(file);
+		return file->size() == 0
+			? makeSilentAudioStream(8000, false) // Movie Adventure has some null-size audio files, they are treated like infinite samples
+			: loadSND(file);
 	path.setChar('W', path.size() - 3);
 	path.setChar('A', path.size() - 2);
 	path.setChar('V', path.size() - 1);


Commit: a80d81995166bcf4faff08afeabf3e910fd6c0b4
    https://github.com/scummvm/scummvm/commit/a80d81995166bcf4faff08afeabf3e910fd6c0b4
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:54+02:00

Commit Message:
ALCACHOFA: Fix hieroglyphic display in 16

Changed paths:
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 66d1f73ab8f..20632b49dcd 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -294,10 +294,10 @@ struct ScriptTask : public Task {
 				pushNumber(-popNumber() + popNumber());
 				break;
 			case ScriptOp::Less:
-				pushNumber(popNumber() >= popNumber());
+				pushNumber(popNumber() > popNumber());
 				break;
 			case ScriptOp::Greater:
-				pushNumber(popNumber() <= popNumber());
+				pushNumber(popNumber() < popNumber());
 				break;
 			case ScriptOp::LessEquals:
 				pushNumber(popNumber() >= popNumber());


Commit: 94dc9afec8c104da5ba12b8c9e8476e960ba7192
    https://github.com/scummvm/scummvm/commit/94dc9afec8c104da5ba12b8c9e8476e960ba7192
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:54+02:00

Commit Message:
ALCACHOFA: Fix TELEFRUSKYMATIC in LAB_BACTERIO

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


diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index e242db5e87d..d3bcd839351 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -194,7 +194,8 @@ void AnimationBase::loadMissingAnimation() {
 		!_fileName.equalsIgnoreCase("PP_MORTA.AN0") &&
 		!_fileName.equalsIgnoreCase("DESPACHO_SUPER2___FONDO_PP_SUPER.AN0") &&
 		!_fileName.equalsIgnoreCase("ESTOMAGO.AN0") &&
-		!_fileName.equalsIgnoreCase("CREDITOS.AN0"))
+		!_fileName.equalsIgnoreCase("CREDITOS.AN0") &&
+		!_fileName.equalsIgnoreCase("MONITOR___OL_EFECTO_FONDO.AN0"))
 		error("Could not open animation %s", _fileName.c_str());
 
 	// otherwise setup a functioning but empty animation
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 20632b49dcd..841bd126dcb 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -928,7 +928,7 @@ void Script::updateCommonVariables() {
 
 	variable("EstanAmbos") = g_engine->world().mortadelo().room() == g_engine->world().filemon().room();
 	variable("textoson") = 1; // TODO: Add subtitle option
-	variable("modored") = 1; // this is signalling whether a network connection is established
+	variable("modored") = 0; // this is signalling whether a network connection is established
 }
 
 }


Commit: 181bede30ef7d9994bda99f6c5338c1480f96ffe
    https://github.com/scummvm/scummvm/commit/181bede30ef7d9994bda99f6c5338c1480f96ffe
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:54+02:00

Commit Message:
ALCACHOFA: Fix holding items after pickup

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


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 4f6e9e9f281..8d356fb440b 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -928,7 +928,8 @@ void MainCharacter::pickup(const String &name, bool putInHand) {
 		error("Tried to pickup unknown item: %s", name.c_str());
 	item->toggle(true);
 	if (g_engine->player().activeCharacter() == this) {
-		// TODO: Put item in hand for pickup
+		if (putInHand)
+			g_engine->player().heldItem() = item;
 		g_engine->world().inventory().updateItemsByActiveCharacter();
 	}
 }
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index d3bcd839351..3e5446fb281 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -248,7 +248,7 @@ void Animation::load() {
 	if (_isLoaded)
 		return;
 	AnimationBase::load();
-	const auto withMipmaps = _folder != AnimationFolder::Backgrounds;
+	const bool withMipmaps = _folder != AnimationFolder::Backgrounds;
 	Rect maxBounds = maxFrameBounds();
 	_renderedSurface.create(maxBounds.width(), maxBounds.height(), BlendBlit::getSupportedPixelFormat());
 	_renderedTexture = g_engine->renderer().createTexture(maxBounds.width(), maxBounds.height(), withMipmaps);
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 841bd126dcb..80810fd0c59 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -709,11 +709,11 @@ private:
 
 		// Inventory control
 		case ScriptKernelTask::Pickup:
-			relatedCharacter().pickup(getStringArg(0), getNumberArg(1));
+			relatedCharacter().pickup(getStringArg(0), !getNumberArg(1));
 			return TaskReturn::finish(1);
 		case ScriptKernelTask::CharacterPickup: {
 			auto &character = g_engine->world().getMainCharacterByKind((MainCharacterKind)getNumberArg(1));
-			character.pickup(getStringArg(0), getNumberArg(2));
+			character.pickup(getStringArg(0), !getNumberArg(2));
 			return TaskReturn::finish(1);
 		}
 		case ScriptKernelTask::Drop:


Commit: 904577adb378f5a682d9434d280a86b05001e5d9
    https://github.com/scummvm/scummvm/commit/904577adb378f5a682d9434d280a86b05001e5d9
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:54+02:00

Commit Message:
ALCACHOFA: Fix cursor directions for doors

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


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 8d356fb440b..36fb6796ff4 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -134,7 +134,7 @@ CursorType Door::cursorType() const {
 	CursorType fromObject = ShapeObject::cursorType();
 	if (fromObject != CursorType::Point)
 		return fromObject;
-	switch (_characterDirection) {
+	switch (_interactionDirection) {
 	case Direction::Up: return CursorType::LeaveUp;
 	case Direction::Right: return CursorType::LeaveRight;
 	case Direction::Down: return CursorType::LeaveDown;
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index a8a240fe1dd..a78336acaa1 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -218,7 +218,6 @@ struct DoorTask : public Task {
 	virtual TaskReturn run() {
 		TASK_BEGIN;
 		// TODO: Fade out music on room change
-		// TODO: Fade out/in on room change instead of delay
 		TASK_WAIT(fade(process(), FadeType::ToBlack, 0, 1, 500, EasingType::Out, -5));
 		_player.changeRoom(_targetRoom->name(), true);
 


Commit: d131608699a36a4a62382eea0e678a23b0f191ab
    https://github.com/scummvm/scummvm/commit/d131608699a36a4a62382eea0e678a23b0f191ab
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:54+02:00

Commit Message:
ALCACHOFA: Fix camera transitions on character change/inventory close

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


diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index dc9f0ced99f..0edd8a2b081 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -49,7 +49,8 @@ void Camera::setRoomBounds(Point bgSize, int16 bgScale) {
 }
 
 void Camera::setFollow(WalkingCharacter *target, bool catchUp) {
-	_cur._followTarget = target;
+	_cur._isFollowingTarget = target != nullptr;
+	_followTarget = target;
 	_lastUpdateTime = g_system->getMillis();
 	_catchUp = catchUp;
 	if (target == nullptr)
@@ -167,7 +168,7 @@ void Camera::update() {
 	deltaTime = MAX(0.001f, MIN(0.5f, deltaTime));
 	_lastUpdateTime = now;
 
-	if (_catchUp && _cur._followTarget != nullptr) {
+	if (_catchUp) {
 		for (int i = 0; i < 4; i++)
 			updateFollowing(50.0f);
 		_catchUp = false;
@@ -177,27 +178,27 @@ void Camera::update() {
 }
 
 void Camera::updateFollowing(float deltaTime) {
-	if (_cur._followTarget == nullptr)
+	if (!_cur._isFollowingTarget || _followTarget == nullptr)
 		return;
 	const float resolutionFactor = g_system->getWidth() * 0.00125f;
 	const float acceleration = 460 * resolutionFactor;
 	const float baseDeadZoneSize = 25 * resolutionFactor;
 	const float minSpeed = 20 * resolutionFactor;
 	const float maxSpeed = this->_cur._maxSpeedFactor * resolutionFactor;
-	const float depthScale = _cur._followTarget->graphic()->depthScale();
-	const auto characterPolygon = _cur._followTarget->shape()->at(0);
+	const float depthScale = _followTarget->graphic()->depthScale();
+	const auto characterPolygon = _followTarget->shape()->at(0);
 	const float halfHeight = ABS(characterPolygon._points[0].y - characterPolygon._points[2].y) / 2.0f;
 
 	Vector3d targetCenter = setAppliedCenter({
-		_shake.getX() + _cur._followTarget->position().x,
-		_shake.getY() + _cur._followTarget->position().y - depthScale * 85,
+		_shake.getX() + _followTarget->position().x,
+		_shake.getY() + _followTarget->position().y - depthScale * 85,
 		_cur._usedCenter.z()});
 	targetCenter.y() -= halfHeight;
 	float distanceToTarget = as2D(_cur._usedCenter - targetCenter).getMagnitude();
-	float moveDistance = _cur._followTarget->stepSizeFactor() * _cur._speed * deltaTime;
+	float moveDistance = _followTarget->stepSizeFactor() * _cur._speed * deltaTime;
 
 	float deadZoneSize = baseDeadZoneSize / _cur._scale;
-	if (_cur._followTarget->isWalking() && depthScale > 0.8f)
+	if (_followTarget->isWalking() && depthScale > 0.8f)
 		deadZoneSize = (baseDeadZoneSize + (depthScale - 0.8f) * 200) / _cur._scale;
 	bool isFarAway = false;
 	if (ABS(targetCenter.x() - _cur._usedCenter.x()) > deadZoneSize ||
diff --git a/engines/alcachofa/camera.h b/engines/alcachofa/camera.h
index b93e1571385..e9f6eaa8411 100644
--- a/engines/alcachofa/camera.h
+++ b/engines/alcachofa/camera.h
@@ -38,7 +38,7 @@ class Camera {
 public:
 	inline Math::Angle rotation() const { return _cur._rotation; }
 	inline Math::Vector2d &shake() { return _shake; }
-	inline WalkingCharacter *followTarget() { return _cur._followTarget; }
+	inline WalkingCharacter *followTarget() { return _followTarget; }
 
 	void update();
 	Math::Vector3d transform2Dto3D(Math::Vector3d v) const;
@@ -93,11 +93,12 @@ private:
 			_maxSpeedFactor = 230.0f;
 		Math::Angle _rotation;
 		bool _isBraking = false;
-		WalkingCharacter *_followTarget = nullptr;
+		bool _isFollowingTarget = false;
 	};
 
 	static constexpr uint kStateBackupCount = 2;
 	State _cur, _backups[kStateBackupCount];
+	WalkingCharacter *_followTarget = nullptr;
 	uint32 _lastUpdateTime = 0;
 	bool _isChanging = false,
 		_catchUp = false;
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index a4fe4b3ddf5..4050227db52 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -281,6 +281,10 @@ void Room::drawDebug() {
 void Room::loadResources() {
 	for (auto *object : _objects)
 		object->loadResources();
+
+	// this fixes some camera backups not working when closing the inventory
+	if (g_engine->player().currentRoom() == this)
+		updateRoomBounds();
 }
 
 void Room::freeResources() {


Commit: 8f2345f2d981141a2fc345e792cea0555d927152
    https://github.com/scummvm/scummvm/commit/8f2345f2d981141a2fc345e792cea0555d927152
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:54+02:00

Commit Message:
ALCACHOFA: Fix some camera moves when scaled

Changed paths:
    engines/alcachofa/camera.cpp


diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index 0edd8a2b081..f40a803229f 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -78,11 +78,10 @@ void Camera::restore(uint slot) {
 	_cur = backupState;
 }
 
-static Matrix4 scaleMatrix(float scale) {
+static Matrix4 scale2DMatrix(float scale) {
 	Matrix4 m;
 	m(0, 0) = scale;
 	m(1, 1) = scale;
-	m(2, 2) = scale;
 	return m;
 }
 
@@ -92,13 +91,13 @@ void Camera::setupMatricesAround(Vector3d center) {
 	_mat3Dto2D.setToIdentity();
 	_mat3Dto2D.translate(-center);
 	_mat3Dto2D = matTemp * _mat3Dto2D;
-	_mat3Dto2D = _mat3Dto2D * scaleMatrix(_cur._scale);
+	_mat3Dto2D = scale2DMatrix(_cur._scale) * _mat3Dto2D;
 
 	_mat2Dto3D.setToIdentity();
 	_mat2Dto3D.translate(center);
 	matTemp.buildAroundZ(-_cur._rotation);
-	matTemp = scaleMatrix(1 / _cur._scale) * matTemp;
-	_mat2Dto3D = matTemp * _mat2Dto3D;
+	matTemp = matTemp * scale2DMatrix(1 / _cur._scale);
+	_mat2Dto3D = _mat2Dto3D * matTemp;
 }
 
 void minmax(Vector3d &min, Vector3d &max, Vector3d val) {


Commit: 0764c495ca0200f94fb341cbac3a6af364809e55
    https://github.com/scummvm/scummvm/commit/0764c495ca0200f94fb341cbac3a6af364809e55
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:54+02:00

Commit Message:
ALCACHOFA: Refactor animation-related methods

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 36fb6796ff4..8a75065f218 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -606,12 +606,12 @@ void WalkingCharacter::updateWalkingAnimation() {
 			stepFrameTo = 2 * expectedFrame;
 		}
 		else {
-			stepFrameFrom = 2 * halfFrameCount - 4;
+			stepFrameFrom = 2 * (halfFrameCount - 2);
 			stepFrameTo = 2 * halfFrameCount - 2;
 		}
 	}
 	if (isUnexpectedFrame) {
-		const uint stepSize = (uint)sqrtf(animation->frameCenter(stepFrameFrom).sqrDist(animation->frameCenter(stepFrameTo)));
+		const float stepSize = sqrtf(animation->frameCenter(stepFrameFrom).sqrDist(animation->frameCenter(stepFrameTo)));
 		_walkedDistance += (int32)(stepSize * _stepSizeFactor);
 	}
 	_graphicNormal.frameI() = 2 * expectedFrame; // especially this: wtf?
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 3e5446fb281..6aaf67d3dc4 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -510,15 +510,15 @@ void Graphic::update() {
 		return;
 
 	const uint32 totalDuration = _animation->totalDuration();
-	uint32 curTime = _lastTime;
-	if (!_isPaused)
-		curTime = g_system->getMillis() - curTime;
-	if (curTime > totalDuration && totalDuration > 0) {
-		if (_isLooping)
+	uint32 curTime = _isPaused
+		? _lastTime
+		: g_system->getMillis() - _lastTime;
+	if (curTime > totalDuration) {
+		if (_isLooping && totalDuration > 0)
 			curTime %= totalDuration;
 		else {
 			pause();
-			curTime = _lastTime = totalDuration - 1;
+			curTime = _lastTime = totalDuration ? totalDuration - 1 : 0;
 		}
 	}
 


Commit: 1fca043e6baa9ad417977a1ca3094feddd3bbe4e
    https://github.com/scummvm/scummvm/commit/1fca043e6baa9ad417977a1ca3094feddd3bbe4e
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:54+02:00

Commit Message:
ALCACHOFA: Add letter-boxes

Changed paths:
    engines/alcachofa/global-ui.cpp
    engines/alcachofa/global-ui.h
    engines/alcachofa/graphics.cpp
    engines/alcachofa/graphics.h
    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 69c41caf53f..dab2ecfe071 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -21,6 +21,7 @@
 
 #include "global-ui.h"
 #include "alcachofa.h"
+#include "script.h"
 
 using namespace Common;
 
@@ -224,4 +225,19 @@ Task *showCenterBottomText(Process &process, int32 dialogId, uint32 durationMs)
 	return new CenterBottomTextTask(process, dialogId, durationMs);
 }
 
+void GlobalUI::drawScreenStates() {
+	if (g_engine->player().isOptionsMenuOpen())
+		return;
+
+	auto &drawQueue = g_engine->drawQueue();
+	if (_isPermanentFaded)
+		drawQueue.add<FadeDrawRequest>(FadeType::ToBlack, 1.0f, -9);
+	else if (int32 borderWidth = g_engine->script().variable("BordesNegros")) {
+		int16 width = g_system->getWidth();
+		int16 height = g_system->getHeight();
+		drawQueue.add<BorderDrawRequest>(Rect(0, 0, width, borderWidth), kBlack);
+		drawQueue.add<BorderDrawRequest>(Rect(0, height - borderWidth, width, height), kBlack);
+	}
+}
+
 }
diff --git a/engines/alcachofa/global-ui.h b/engines/alcachofa/global-ui.h
index e5522bc97e3..8892ebf3e27 100644
--- a/engines/alcachofa/global-ui.h
+++ b/engines/alcachofa/global-ui.h
@@ -35,12 +35,14 @@ public:
 
 	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();
 	bool updateOpeningInventory();
 	void updateClosingInventory();
 	void startClosingInventory();
+	void drawScreenStates(); // black borders and/or permanent fade
 
 private:
 	Animation *activeAnimation() const;
@@ -57,7 +59,8 @@ private:
 
 	bool
 		_isOpeningInventory = false,
-		_isClosingInventory = false;
+		_isClosingInventory = false,
+		_isPermanentFaded = false;
 	uint32 _timeForInventory = 0;
 };
 
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 6aaf67d3dc4..23074f63dd3 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -22,6 +22,7 @@
 #include "graphics.h"
 #include "alcachofa.h"
 #include "shape.h"
+#include "global-ui.h"
 
 #include "common/system.h"
 #include "common/file.h"
@@ -784,7 +785,7 @@ struct FadeTask : public Task {
 	virtual TaskReturn run() override {
 		TASK_BEGIN;
 		if (_permanentFadeAction == PermanentFadeAction::UnsetFaded)
-			g_engine->player().setPermanentFade(false);
+			g_engine->globalUI().isPermanentFaded() = false;
 		_startTime = g_system->getMillis();
 		while (g_system->getMillis() - _startTime < _duration) {
 			draw((g_system->getMillis() - _startTime) / (float)_duration);
@@ -792,7 +793,7 @@ struct FadeTask : public Task {
 		}
 		draw(1.0f); // so that during a loading lag the screen is completly black/white
 		if (_permanentFadeAction == PermanentFadeAction::SetFaded)
-			g_engine->player().setPermanentFade(true);
+			g_engine->globalUI().isPermanentFaded() = true;
 		TASK_END;
 	}
 
@@ -828,6 +829,19 @@ Task *fade(Process &process, FadeType fadeType,
 	return new FadeTask(process, fadeType, from, to, duration, easingType, order, permanentFadeAction);
 }
 
+BorderDrawRequest::BorderDrawRequest(Rect rect, Color color)
+	: IDrawRequest(-kForegroundOrderCount)
+	, _rect(rect)
+	, _color(color) {
+}
+
+void BorderDrawRequest::draw() {
+	auto &renderer = g_engine->renderer();
+	renderer.setTexture(nullptr);
+	renderer.setBlendMode(BlendMode::AdditiveAlpha);
+	renderer.quad({ (float)_rect.left, (float)_rect.top }, { (float)_rect.width(), (float)_rect.height() }, _color);
+}
+
 DrawQueue::DrawQueue(IRenderer *renderer)
 	: _renderer(renderer)
 	, _allocator(1024) {
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 351f5508257..784ec361834 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -412,6 +412,17 @@ Task *fade(Process &process, FadeType fadeType,
 	int8 order,
 	PermanentFadeAction permanentFadeAction = PermanentFadeAction::Nothing);
 
+class BorderDrawRequest : public IDrawRequest {
+public:
+	BorderDrawRequest(Common::Rect rect, Color color);
+
+	virtual void draw() override;
+
+private:
+	Common::Rect _rect;
+	Color _color;
+};
+
 class BumpAllocator {
 public:
 	BumpAllocator(size_t pageSize);
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index a78336acaa1..54a1407007f 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -44,11 +44,6 @@ void Player::postUpdate() {
 		_pressedObject = nullptr;
 }
 
-void Player::drawScreenStates() {
-	if (_isPermanentFaded && !_isOptionsMenuOpen)
-		g_engine->drawQueue().add<FadeDrawRequest>(FadeType::ToBlack, 1.0f, -9);
-}
-
 void Player::resetCursor() {
 	_cursorFrameI = 0;
 }
@@ -262,10 +257,6 @@ void Player::triggerDoor(const Door *door) {
 	g_engine->scheduler().createProcess<DoorTask>(activeCharacterKind(), door, move(lock));
 }
 
-void Player::setPermanentFade(bool isFaded) {
-	_isPermanentFaded = isFaded;
-}
-
 // the last dialog character mechanic seems like a hack in the original engine
 // all talking characters (see SayText kernel call) are added to a fixed-size
 // rolling queue and stopped upon killProcesses
diff --git a/engines/alcachofa/player.h b/engines/alcachofa/player.h
index 5917cd76a00..638f62bb207 100644
--- a/engines/alcachofa/player.h
+++ b/engines/alcachofa/player.h
@@ -48,7 +48,6 @@ public:
 
 	void preUpdate();
 	void postUpdate();
-	void drawScreenStates(); // black borders and/or permanent fade
 	void updateCursor();
 	void drawCursor(bool forceDefaultCursor = false);
 	void resetCursor();
@@ -56,7 +55,6 @@ public:
 	void changeRoomToBeforeInventory();
 	void triggerObject(ObjectBase *object, const char *action);
 	void triggerDoor(const Door *door);
-	void setPermanentFade(bool isFaded);
 	void addLastDialogCharacter(Character *character);
 	void stopLastDialogCharacters();
 	void setActiveCharacter(MainCharacterKind kind);
@@ -76,8 +74,7 @@ private:
 	bool
 		_isOptionsMenuOpen = false,
 		_isGameLoaded = true,
-		_didLoadGlobalRooms = false,
-		_isPermanentFaded = false;
+		_didLoadGlobalRooms = false;
 	Character *_lastDialogCharacters[kMaxLastDialogCharacters] = { nullptr };
 	int _nextLastDialogCharacter = 0;
 };
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 4050227db52..14e0fb71de3 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -150,8 +150,7 @@ void Room::draw() {
 	g_engine->camera().update();
 	drawObjects();
 	world().globalRoom().drawObjects();
-	// TODO: Draw black borders
-	g_engine->player().drawScreenStates();
+	g_engine->globalUI().drawScreenStates();
 	g_engine->drawQueue().draw();
 	drawDebug();
 	world().globalRoom().drawDebug();


Commit: e1a0e6a15964d583e079eebf2ccd7ef880851b9b
    https://github.com/scummvm/scummvm/commit/e1a0e6a15964d583e079eebf2ccd7ef880851b9b
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:54+02:00

Commit Message:
ALCACHOFA: Fix floor shapes with lines

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


diff --git a/engines/alcachofa/console.cpp b/engines/alcachofa/console.cpp
index 44a3d70f88d..020a1c6c59a 100644
--- a/engines/alcachofa/console.cpp
+++ b/engines/alcachofa/console.cpp
@@ -44,6 +44,7 @@ Console::Console() : GUI::Debugger() {
 	registerCmd("drop", WRAP_METHOD(Console, cmdItem));
 	registerCmd("debugMode", WRAP_METHOD(Console, cmdDebugMode));
 	registerCmd("tp", WRAP_METHOD(Console, cmdTeleport));
+	registerCmd("toggleRoomFloor", WRAP_METHOD(Console, cmdToggleRoomFloor));
 }
 
 Console::~Console() {
@@ -251,7 +252,7 @@ bool Console::cmdTeleport(int argc, const char **args) {
 		param = (int32)strtol(args[1], &end, 10);
 		if (end == nullptr || *end != '\0')
 		{
-			debugPrintf("Character kind can only be integer");
+			debugPrintf("Character kind can only be integer\n");
 			return true;
 		}
 	}
@@ -260,4 +261,16 @@ bool Console::cmdTeleport(int argc, const char **args) {
 	return false;
 }
 
+bool Console::cmdToggleRoomFloor(int argc, const char **args) {
+	auto room = g_engine->player().currentRoom();
+	if (room == nullptr)
+	{
+		debugPrintf("No room is active");
+		return true;
+	}
+
+	room->toggleActiveFloor();
+	return false;
+}
+
 } // End of namespace Alcachofa
diff --git a/engines/alcachofa/console.h b/engines/alcachofa/console.h
index da464885754..39376311cbb 100644
--- a/engines/alcachofa/console.h
+++ b/engines/alcachofa/console.h
@@ -56,6 +56,7 @@ private:
 	bool cmdItem(int argc, const char **args);
 	bool cmdDebugMode(int argc, const char **args);
 	bool cmdTeleport(int argc, const char **args);
+	bool cmdToggleRoomFloor(int argc, const char **args);
 
 	bool _showInteractables = false;
 	bool _showCharacters = false;
diff --git a/engines/alcachofa/shape.cpp b/engines/alcachofa/shape.cpp
index 65dae596388..e4ed671e3ad 100644
--- a/engines/alcachofa/shape.cpp
+++ b/engines/alcachofa/shape.cpp
@@ -54,10 +54,24 @@ static bool segmentsIntersect(Point a1, Point b1, Point a2, Point b2) {
 	}
 }
 
+EdgeDistances::EdgeDistances(Point edgeA, Point edgeB, Point query) {
+	Vector2d
+		a = as2D(edgeA),
+		b = as2D(edgeB),
+		q = as2D(query);
+	float edgeLength = a.getDistanceTo(b);
+	Vector2d edgeDir = (b - a) / edgeLength;
+	Vector2d edgeNormal(-edgeDir.getY(), edgeDir.getX());
+	_edgeLength = edgeLength;
+	_onEdge = edgeDir.dotProduct(q - a);
+	_toEdge = abs(edgeNormal.dotProduct(q) - edgeNormal.dotProduct(a));
+}
+
 bool Polygon::contains(Point query) const {
 	switch (_points.size()) {
 	case 0: return false;
 	case 1: return query == _points[0];
+	case 2: return edgeDistances(0, query)._toEdge < 2.0f;
 	default:
 		// we assume that the polygon is convex
 		for (uint i = 1; i < _points.size(); i++) {
@@ -77,18 +91,7 @@ bool Polygon::intersectsEdge(uint startPointI, Point a, Point b) const {
 EdgeDistances Polygon::edgeDistances(uint startPointI, Point query) const {
 	assert(startPointI < _points.size());
 	uint endPointI = startPointI + 1 == _points.size() ? 0 : startPointI + 1;
-	Vector2d
-		a = as2D(_points[startPointI]),
-		b = as2D(_points[endPointI]),
-		q = as2D(query);
-	float edgeLength = a.getDistanceTo(b);
-	Vector2d edgeDir = (b - a) / edgeLength;
-	Vector2d edgeNormal(-edgeDir.getY(), edgeDir.getX());
-	EdgeDistances distances;
-	distances._edgeLength = edgeLength;
-	distances._onEdge = edgeDir.dotProduct(q - a);
-	distances._toEdge = abs(edgeNormal.dotProduct(q) - edgeNormal.dotProduct(a));
-	return distances;
+	return EdgeDistances(_points[startPointI], _points[endPointI], query);
 }
 
 static Point wiggleOnToLine(Point a, Point b, Point q) {
diff --git a/engines/alcachofa/shape.h b/engines/alcachofa/shape.h
index 2fae40a5ac9..4174aabc140 100644
--- a/engines/alcachofa/shape.h
+++ b/engines/alcachofa/shape.h
@@ -35,6 +35,8 @@
 namespace Alcachofa {
 
 struct EdgeDistances {
+	EdgeDistances(Common::Point edgeA, Common::Point edgeB, Common::Point query);
+
 	float _edgeLength;
 	float _onEdge;
 	float _toEdge;


Commit: 5c578daf717145f8ac57b89db412577a3a209193
    https://github.com/scummvm/scummvm/commit/5c578daf717145f8ac57b89db412577a3a209193
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:54+02:00

Commit Message:
ALCACHOFA: Fix TGADecoder changes after rebase

Changed paths:
    engines/alcachofa/graphics.cpp


diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 23074f63dd3..4c0a62dac62 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -182,8 +182,9 @@ ManagedSurface *AnimationBase::readImage(SeekableReadStream &stream) const {
 	if (source->w == 2 && source->h == 1)
 		return nullptr;
 
+	const auto &palette = decoder.getPalette();
 	auto target = new ManagedSurface();
-	target->setPalette(decoder.getPalette(), 0, decoder.getPaletteColorCount());
+	target->setPalette(palette.data(), 0, palette.size());
 	target->convertFrom(*source, BlendBlit::getSupportedPixelFormat());
 	return target;
 }


Commit: d2c05d444ab69015c06d9686c19cfbc6dcd3c08d
    https://github.com/scummvm/scummvm/commit/d2c05d444ab69015c06d9686c19cfbc6dcd3c08d
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:54+02:00

Commit Message:
ALCACHOFA: Add camera tasks for inactive player

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


diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index f40a803229f..222b7a71e8c 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -349,12 +349,61 @@ private:
 	Camera &_camera;
 };
 
+struct CamSetInactiveAttributeTask final : public Task {
+	enum Attribute {
+		kPosZ,
+		kScale,
+		kRotation
+	};
+
+	CamSetInactiveAttributeTask(Process &process, Attribute attribute, float value, int32 delay)
+		: Task(process)
+		, _camera(g_engine->camera())
+		, _attribute(attribute)
+		, _value(value)
+		, _delay(delay) {}
+
+	virtual TaskReturn run() override {
+		if (_delay > 0) {
+			uint32 delay = (uint32)_delay;
+			_delay = 0;
+			return TaskReturn::waitFor(new DelayTask(process(), delay));
+		}
+
+		auto &state = _camera._backups[0];
+		switch (_attribute) {
+		case kPosZ: state._usedCenter.z() = _value; break;
+		case kScale: state._scale = _value; break;
+		case kRotation: state._rotation = _value; break;
+		default:
+			warning("Unknown CamSetInactiveAttribute attribute: %d", (int)_attribute);
+			break;
+		}
+	}
+
+	virtual void debugPrint() override {
+		const char *attributeName;
+		switch (_attribute) {
+		case kPosZ: attributeName = "PosZ"; break;
+		case kScale: attributeName = "Scale"; break;
+		case kRotation: attributeName = "Rotation"; break;
+		default: attributeName = "<unknown>"; break;
+		}
+		g_engine->console().debugPrintf("Set inactive camera %s to %f after %dms\n", attributeName, _value, _delay);
+	}
+
+private:
+	Camera &_camera;
+	Attribute _attribute;
+	float _value;
+	int32 _delay;
+};
+
 Task *Camera::lerpPos(Process &process,
 					  Vector2d targetPos,
 					  int32 duration, EasingType easingType) {
 	if (!process.isActiveForPlayer()) {
-		warning("stub: non-active camera lerp script invoked");
-		return new DelayTask(process, duration);
+		return new DelayTask(process, duration); // lerpPos does not handle inactive players
 	}
 	Vector3d targetPos3d(targetPos.getX(), targetPos.getY(), _appliedCenter.z());
 	return new CamLerpPosTask(process, targetPos3d, duration, easingType);
@@ -364,8 +413,7 @@ Task *Camera::lerpPos(Process &process,
 					  Vector3d targetPos,
 					  int32 duration, EasingType easingType) {
 	if (!process.isActiveForPlayer()) {
-		warning("stub: non-active camera lerp script invoked");
-		return new DelayTask(process, duration);
+		return new DelayTask(process, duration); // lerpPos does not handle inactive players
 	}
 	setFollow(nullptr); // 3D position lerping is the only task that resets following
 	return new CamLerpPosTask(process, targetPos, duration, easingType);
@@ -375,8 +423,7 @@ Task *Camera::lerpPosZ(Process &process,
 					   float targetPosZ,
 					   int32 duration, EasingType easingType) {
 	if (!process.isActiveForPlayer()) {
-		warning("stub: non-active camera lerp script invoked");
-		return new DelayTask(process, duration);
+		return new CamSetInactiveAttributeTask(process, CamSetInactiveAttributeTask::kPosZ, targetPosZ, duration);
 	}
 	Vector3d targetPos(_appliedCenter.x(), _appliedCenter.y(), targetPosZ);
 	return new CamLerpPosTask(process, targetPos, duration, easingType);
@@ -386,8 +433,7 @@ Task *Camera::lerpScale(Process &process,
 						float targetScale,
 						int32 duration, EasingType easingType) {
 	if (!process.isActiveForPlayer()) {
-		warning("stub: non-active camera lerp script invoked");
-		return new DelayTask(process, duration);
+		return new CamSetInactiveAttributeTask(process, CamSetInactiveAttributeTask::kScale, targetScale, duration);
 	}
 	return new CamLerpScaleTask(process, targetScale, duration, easingType);
 }
@@ -396,8 +442,7 @@ Task *Camera::lerpRotation(Process &process,
 						   float targetRotation,
 						   int32 duration, EasingType easingType) {
 	if (!process.isActiveForPlayer()) {
-		warning("stub: non-active camera lerp script invoked");
-		return new DelayTask(process, duration);
+		return new CamSetInactiveAttributeTask(process, CamSetInactiveAttributeTask::kRotation, targetRotation, duration);
 	}
 	return new CamLerpRotationTask(process, targetRotation, duration, easingType);
 }
diff --git a/engines/alcachofa/camera.h b/engines/alcachofa/camera.h
index e9f6eaa8411..a1f8eebe645 100644
--- a/engines/alcachofa/camera.h
+++ b/engines/alcachofa/camera.h
@@ -81,6 +81,7 @@ private:
 	friend struct CamLerpRotationTask;
 	//friend struct CamShakeTask;
 	friend struct CamWaitToStopTask;
+	friend struct CamSetInactiveAttributeTask;
 	Math::Vector3d setAppliedCenter(Math::Vector3d center);
 	void setupMatricesAround(Math::Vector3d center);
 	void updateFollowing(float deltaTime);
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 80810fd0c59..1783b27bde2 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -564,9 +564,6 @@ private:
 			else
 				g_engine->world().getMainCharacterByKind(process().character()).room()->toggleActiveFloor();
 			return TaskReturn::finish(1);
-		case ScriptKernelTask::LerpWorldLodBias:
-			warning("STUB KERNEL CALL: LerpWorldLodBias");
-			return TaskReturn::finish(0);
 
 			// object control / animation
 		case ScriptKernelTask::On:
@@ -733,9 +730,6 @@ private:
 			return TaskReturn::finish(1);
 
 		// Camera tasks
-		case ScriptKernelTask::SetMaxCamSpeedFactor:
-			warning("STUB KERNEL CALL: SetMaxCamSpeedFactor");
-			return TaskReturn::finish(0);
 		case ScriptKernelTask::WaitCamStopping:
 			return TaskReturn::waitFor(g_engine->camera().waitToStop(process()));
 		case ScriptKernelTask::CamFollow:
@@ -787,13 +781,15 @@ private:
 				getNumberArg(1), (EasingType)getNumberArg(2)));
 		}
 		case ScriptKernelTask::LerpCamToObjectWithScale: {
+			float targetScale = getNumberArg(1) * 0.01f;
 			if (!process().isActiveForPlayer())
-				return TaskReturn::waitFor(delay(getNumberArg(2)));
+				// 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)
 				error("Invalid target object for LerpCamToObjectWithScale: %s", getStringArg(0));
 			return TaskReturn::waitFor(g_engine->camera().lerpPosScale(process(),
-				as3D(pointObject->position()), getNumberArg(1) * 0.01f,
+				as3D(pointObject->position()), targetScale,
 				getNumberArg(2), (EasingType)getNumberArg(3), (EasingType)getNumberArg(4)));
 		}
 
@@ -823,7 +819,13 @@ private:
 				1.0f, 0.0f, getNumberArg(0), (EasingType)getNumberArg(1), -5,
 				PermanentFadeAction::SetFaded));
 
-		// Unused and useless
+		// Unused and/or useless
+		case ScriptKernelTask::SetMaxCamSpeedFactor:
+			warning("STUB KERNEL CALL: SetMaxCamSpeedFactor");
+			return TaskReturn::finish(0);
+		case ScriptKernelTask::LerpWorldLodBias:
+			warning("STUB KERNEL CALL: LerpWorldLodBias");
+			return TaskReturn::finish(0);
 		case ScriptKernelTask::SetActiveTextureSet:
 			// Fortunately this seems to be unused.
 			warning("STUB KERNEL CALL: SetActiveTextureSet");


Commit: e0145237117be364ea40a0197dcd661a0f5cae70
    https://github.com/scummvm/scummvm/commit/e0145237117be364ea40a0197dcd661a0f5cae70
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:55+02:00

Commit Message:
ALCACHOFA: Add kernel task CamShake

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


diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index 222b7a71e8c..5f9c6d6db4f 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -253,10 +253,11 @@ struct CamLerpTask : public Task {
 		uint32 remaining = g_system->getMillis() - _startTime <= _duration
 			? _duration - (g_system->getMillis() - _startTime)
 			: 0;
-		g_engine->console().debugPrintf("Lerp camera with %ums remaining\n", remaining);
+		g_engine->console().debugPrintf("%s camera with %ums remaining\n", taskName(), remaining);
 	}
 
 protected:
+	virtual const char *taskName() const = 0;
 	virtual void update(float t) = 0;
 
 	Camera &_camera;
@@ -271,6 +272,10 @@ struct CamLerpPosTask final : public CamLerpTask {
 		, _deltaPos(targetPos - _camera._appliedCenter) {}
 
 protected:
+	virtual const char *taskName() const {
+		return "Lerp pos of";
+	}
+
 	virtual void update(float t) override {
 		_camera.setPosition(_fromPos + _deltaPos * t);
 	}
@@ -285,6 +290,10 @@ struct CamLerpScaleTask final : public CamLerpTask {
 		, _deltaScale(targetScale - _camera._cur._scale) {}
 
 protected:
+	virtual const char *taskName() const {
+		return "Lerp scale of";
+	}
+
 	virtual void update(float t) override {
 		_camera._cur._scale = _fromScale + _deltaScale * t;
 	}
@@ -306,6 +315,10 @@ struct CamLerpPosScaleTask final : public CamLerpTask {
 		, _scaleEasingType(scaleEasingType) {}
 
 protected:
+	virtual const char *taskName() const {
+		return "Lerp pos and scale of";
+	}
+
 	virtual void update(float t) override {
 		_camera.setPosition(_fromPos + _deltaPos * ease(t, _moveEasingType));
 		_camera._cur._scale = _fromScale + _deltaScale * ease(t, _scaleEasingType);
@@ -323,6 +336,10 @@ struct CamLerpRotationTask final : public CamLerpTask {
 		, _deltaRotation(targetRotation - _camera._cur._rotation.getDegrees()) {}
 
 protected:
+	virtual const char *taskName() const {
+		return "Lerp rotation of";
+	}
+
 	virtual void update(float t) override {
 		_camera._cur._rotation = Angle(_fromRotation + _deltaRotation * t);
 	}
@@ -330,6 +347,30 @@ protected:
 	float _fromRotation, _deltaRotation;
 };
 
+struct CamShakeTask final : public CamLerpTask {
+	CamShakeTask(Process &process, Vector2d amplitude, Vector2d frequency, int32 duration)
+		: CamLerpTask(process, duration, EasingType::Linear)
+		, _amplitude(amplitude)
+		, _frequency(frequency) { }
+
+protected:
+	virtual const char *taskName() const {
+		return "Shake";
+	}
+
+	virtual void update(float t) override {
+		const Vector2d phase = _frequency * t * (float)M_PI * 2.0f;
+		const float amplTimeFactor = 1.0f / expf(t * 5.0f); // a curve starting at 1, depreciating towards 0 
+		_camera.shake() = {
+			sinf(phase.getX()) * _amplitude.getX() * amplTimeFactor,
+			sinf(phase.getY()) * _amplitude.getY() * amplTimeFactor
+		};
+	}
+
+	Vector2d _amplitude, _frequency;
+
+};
+
 struct CamWaitToStopTask final : public Task {
 	CamWaitToStopTask(Process &process)
 		: Task(process)
@@ -379,6 +420,7 @@ struct CamSetInactiveAttributeTask final : public Task {
 			warning("Unknown CamSetInactiveAttribute attribute: %d", (int)_attribute);
 			break;
 		}
+		return TaskReturn::finish(0);
 	}
 
 	virtual void debugPrint() override {
@@ -452,8 +494,7 @@ Task *Camera::lerpPosScale(Process &process,
 						   int32 duration,
 						   EasingType moveEasingType, EasingType scaleEasingType) {
 	if (!process.isActiveForPlayer()) {
-		warning("stub: non-active camera lerp script invoked");
-		return new DelayTask(process, duration);
+		return new CamSetInactiveAttributeTask(process, CamSetInactiveAttributeTask::kScale, targetScale, duration);
 	}
 	return new CamLerpPosScaleTask(process, targetPos, targetScale, duration, moveEasingType, scaleEasingType);
 }
@@ -462,4 +503,11 @@ Task *Camera::waitToStop(Process &process) {
 	return new CamWaitToStopTask(process);
 }
 
+Task *Camera::shake(Process &process, Math::Vector2d amplitude, Math::Vector2d frequency, int32 duration) {
+	if (!process.isActiveForPlayer()) {
+		return new DelayTask(process, (uint32)duration);
+	}
+	return new CamShakeTask(process, amplitude, frequency, duration);
+}
+
 } // namespace Alcachofa
diff --git a/engines/alcachofa/camera.h b/engines/alcachofa/camera.h
index a1f8eebe645..6e18e069003 100644
--- a/engines/alcachofa/camera.h
+++ b/engines/alcachofa/camera.h
@@ -70,8 +70,8 @@ public:
 	Task *lerpPosScale(Process &process,
 		Math::Vector3d targetPos, float targetScale,
 		int32 duration, EasingType moveEasingType, EasingType scaleEasingType);
-	//Task *shake(Process &process, Math::Vector2d amplitude, Math::Vector2d frequency, int32 duration);
 	Task *waitToStop(Process &process);
+	Task *shake(Process &process, Math::Vector2d amplitude, Math::Vector2d frequency, int32 duration);
 
 private:
 	friend struct CamLerpTask;
@@ -79,7 +79,7 @@ private:
 	friend struct CamLerpScaleTask;
 	friend struct CamLerpPosScaleTask;
 	friend struct CamLerpRotationTask;
-	//friend struct CamShakeTask;
+	friend struct CamShakeTask;
 	friend struct CamWaitToStopTask;
 	friend struct CamSetInactiveAttributeTask;
 	Math::Vector3d setAppliedCenter(Math::Vector3d center);
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 1783b27bde2..1164cded5a3 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -738,8 +738,10 @@ private:
 				getNumberArg(1) != 0);
 			return TaskReturn::finish(1);
 		case ScriptKernelTask::CamShake:
-			warning("STUB KERNEL CALL: CamShake");
-			return TaskReturn::finish(0);
+			return TaskReturn::waitFor(g_engine->camera().shake(process(),
+				Vector2d(getNumberArg(1), getNumberArg(2)),
+				Vector2d(getNumberArg(3), getNumberArg(4)),
+				getNumberArg(0)));
 		case ScriptKernelTask::LerpCamXY:
 			return TaskReturn::waitFor(g_engine->camera().lerpPos(process(),
 				Vector2d(getNumberArg(0), getNumberArg(1)),


Commit: df191096d2176cbbdbbf2404e04ee488f8bb3658
    https://github.com/scummvm/scummvm/commit/df191096d2176cbbdbbf2404e04ee488f8bb3658
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:55+02:00

Commit Message:
ALCACHOFA: Clear heldItem on clearInventory

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


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 8a75065f218..55d947be7d8 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -905,7 +905,8 @@ void MainCharacter::serializeSave(Serializer &serializer) {
 void MainCharacter::clearInventory() {
 	for (auto *item : _items)
 		item->toggle(false);
-	// TODO: Clear held item on clearInventory
+	if (g_engine->player().activeCharacter() == this)
+		g_engine->player().heldItem() = nullptr;
 	g_engine->world().inventory().updateItemsByActiveCharacter();
 }
 
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 784ec361834..ac8f80db2b7 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -386,7 +386,7 @@ private:
 enum class FadeType {
 	ToBlack,
 	ToWhite
-	// TODO: Add CrossFade fade type
+	// Originally there was a CrossFade, but it is unused for now and thus not implemented
 };
 
 enum class PermanentFadeAction {


Commit: 22596145424151ec1e222cff5dca21c2d79878a6
    https://github.com/scummvm/scummvm/commit/22596145424151ec1e222cff5dca21c2d79878a6
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:55+02:00

Commit Message:
ALCACHOFA: Refactor error handling

Changed paths:
  A engines/alcachofa/game-movie-adventure.cpp
  A engines/alcachofa/game.cpp
  A engines/alcachofa/game.h
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/alcachofa.h
    engines/alcachofa/camera.cpp
    engines/alcachofa/detection.cpp
    engines/alcachofa/detection.h
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/metaengine.cpp
    engines/alcachofa/module.mk
    engines/alcachofa/player.cpp
    engines/alcachofa/rooms.cpp
    engines/alcachofa/scheduler.h
    engines/alcachofa/script.cpp
    engines/alcachofa/shape.cpp
    engines/alcachofa/sounds.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index df88a8a776a..c2f55cd00e1 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -19,10 +19,6 @@
  *
  */
 
-#include "alcachofa/alcachofa.h"
-#include "graphics/framelimiter.h"
-#include "alcachofa/detection.h"
-#include "alcachofa/console.h"
 #include "common/scummsys.h"
 #include "common/config-manager.h"
 #include "common/debug-channels.h"
@@ -33,10 +29,14 @@
 #include "graphics/framelimiter.h"
 #include "video/mpegps_decoder.h"
 
+#include "alcachofa.h"
+#include "console.h"
+#include "detection.h"
 #include "rooms.h"
 #include "script.h"
 #include "global-ui.h"
 #include "debug.h"
+#include "game.h"
 
 using namespace Math;
 
@@ -65,6 +65,7 @@ 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)));
 	_drawQueue.reset(new DrawQueue(_renderer.get()));
 	_world.reset(new World());
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index 84c06c4ae10..5d6ceb64cbd 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -41,6 +41,7 @@
 #include "alcachofa/player.h"
 #include "alcachofa/scheduler.h"
 #include "alcachofa/console.h"
+#include "alcachofa/game.h"
 
 namespace Alcachofa {
 
@@ -50,6 +51,7 @@ class DrawQueue;
 class World;
 class Script;
 class GlobalUI;
+class Game;
 struct AlcachofaGameDescription;
 
 class AlcachofaEngine : public Engine {
@@ -74,6 +76,7 @@ public:
 	inline GlobalUI &globalUI() { return *_globalUI; }
 	inline Scheduler &scheduler() { return _scheduler; }
 	inline Console &console() { return *_console; }
+	inline Game &game() { return *_game; }
 	inline bool isDebugModeActive() const { return _debugHandler != nullptr; }
 
 	void playVideo(int32 videoId);
@@ -131,6 +134,7 @@ private:
 	Common::ScopedPtr<Script> _script;
 	Common::ScopedPtr<Player> _player;
 	Common::ScopedPtr<GlobalUI> _globalUI;
+	Common::ScopedPtr<Game> _game;
 	Camera _camera;
 	Input _input;
 	Sounds _sounds;
diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index 5f9c6d6db4f..caa76b97db8 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -417,7 +417,7 @@ struct CamSetInactiveAttributeTask final : public Task {
 		case kScale: state._scale = _value; break;
 		case kRotation: state._rotation = _value; break;
 		default:
-			warning("Unknown CamSetInactiveAttribute attribute: %d", (int)_attribute);
+			g_engine->game().unknownCamSetInactiveAttribute((int)_attribute);
 			break;
 		}
 		return TaskReturn::finish(0);
diff --git a/engines/alcachofa/detection.cpp b/engines/alcachofa/detection.cpp
index 1d0c5a94ad9..ce292b94454 100644
--- a/engines/alcachofa/detection.cpp
+++ b/engines/alcachofa/detection.cpp
@@ -32,6 +32,7 @@
 const DebugChannelDef AlcachofaMetaEngineDetection::debugFlagList[] = {
 	{ Alcachofa::kDebugGraphics, "Graphics", "Graphics debug level" },
 	{ Alcachofa::kDebugScript, "Script", "Enable debug script dump" },
+	{ Alcachofa::kDebugGameplay, "Gameplay", "Gameplay-related tracing" },
 	DEBUG_CHANNEL_END
 };
 
diff --git a/engines/alcachofa/detection.h b/engines/alcachofa/detection.h
index 96a07feed09..20a4f8bfa59 100644
--- a/engines/alcachofa/detection.h
+++ b/engines/alcachofa/detection.h
@@ -29,6 +29,7 @@ namespace Alcachofa {
 enum AlcachofaDebugChannels {
 	kDebugGraphics = 1,
 	kDebugScript,
+	kDebugGameplay
 };
 
 extern const PlainGameDescriptor alcachofaGames[];
diff --git a/engines/alcachofa/game-movie-adventure.cpp b/engines/alcachofa/game-movie-adventure.cpp
new file mode 100644
index 00000000000..281a45adf44
--- /dev/null
+++ b/engines/alcachofa/game-movie-adventure.cpp
@@ -0,0 +1,143 @@
+/* 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.h"
+#include "game.h"
+
+using namespace Common;
+
+namespace Alcachofa {
+
+class GameMovieAdventure : public Game {
+	virtual bool doesRoomHaveBackground(const Room *room) override {
+		return !room->name().equalsIgnoreCase("Global") &&
+			!room->name().equalsIgnoreCase("HABITACION_NEGRA");
+	}
+
+	virtual void invalidDialogLine(uint index) override {
+		if (index != 4542)
+			Game::invalidDialogLine(index);
+	}
+
+	virtual bool shouldCharacterTrigger(const Character *character, const char *action) override {
+		// An original hack to check that bed sheet is used on the other main character only in the correct room
+		// There *is* another script variable (es_casa_freddy) that should check this
+		// but, I guess, Alcachofa Soft found a corner case where this does not work?
+		if (scumm_stricmp(action, "iSABANA") == 0 &&
+			dynamic_cast<const MainCharacter *>(character) != nullptr &&
+			!character->room()->name().equalsIgnoreCase("CASA_FREDDY_ARRIBA")) {
+			return false;
+		}
+
+		return Game::shouldCharacterTrigger(character, action);
+	}
+
+	virtual bool shouldTriggerDoor(const Door *door) {
+		if (door->targetRoom() == "LABERINTO" && door->targetObject() == "a_LABERINTO_desde_LABERINTO_2")
+			return false;
+		return Game::shouldTriggerDoor(door);
+	}
+
+	virtual bool hasMortadeloVoice(const Character *character) override {
+		return Game::hasMortadeloVoice(character) ||
+			character->name().equalsIgnoreCase("MORTADELO_TREN"); // an original hard-coded special case
+	}
+
+	virtual 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",
+			nullptr
+		};
+		for (const char **exemption = exemptions; *exemption != nullptr; exemption++) {
+			if (fileName.equalsIgnoreCase(*exemption)) {
+				debugC(1, kDebugGraphics, "Animation exemption triggered: %s", fileName.c_str());
+				return;
+			}
+		}
+		Game::missingAnimation(fileName);
+	}
+
+	virtual void unknownAnimateObject(const char *name) override {
+		if (!scumm_stricmp("EXPLOSION DISFRAZ", name))
+			return;
+		Game::unknownAnimateObject(name);
+	}
+
+	virtual 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;
+		}
+
+		if (!scumm_stricmp("PUNTO_VENTANA", name)) {
+			// The object is in the previous, now inactive room.
+			// Luckily Mortadelo already is at that point so not further action required
+			return nullptr;
+		}
+
+		if (!scumm_stricmp("Puerta_Casa_Freddy_Intermedia", name)) {
+			// Another case of a door being cast into a PointObject
+			return nullptr;
+		}
+
+		return Game::unknownGoPutTarget(process, action, name);
+	}
+
+	virtual void unknownSayTextCharacter(const char *name, int32 dialogId) override {
+		if (!scumm_stricmp(name, "OFELIA") && dialogId == 3737)
+			return;
+		Game::unknownSayTextCharacter(name, dialogId);
+	}
+
+	virtual void unknownAnimateCharacterObject(const char *name) override {
+		if (!scumm_stricmp(name, "COGE F DCH") || // original bug in MOTEL_ENTRADA
+			!scumm_stricmp(name, "CHIQUITO_IZQ"))
+			return;
+		Game::unknownAnimateCharacterObject(name);
+	}
+
+	virtual void missingSound(const String &fileName) override {
+		if (fileName == "CHAS" || fileName == "517")
+			return;
+		Game::missingSound(fileName);
+	}
+};
+
+Game *Game::createForMovieAdventure() {
+	return new GameMovieAdventure();
+}
+
+}
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 55d947be7d8..67263acaf65 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -267,7 +267,7 @@ void Character::syncObjectAsString(Serializer &serializer, ObjectBase *&object)
 			if (object == nullptr)
 				object = room()->world().getObjectByName(name.c_str());
 			if (object == nullptr)
-				error("Invalid object name \"%s\" saved for \"%s\" in \"%s\"",
+				g_engine->game().unknownSerializedObject(
 					name.c_str(), this->name().c_str(), room()->name().c_str());
 		}
 	}
@@ -280,15 +280,8 @@ void Character::onClick() {
 
 void Character::trigger(const char *action) {
 	g_engine->player().activeCharacter()->stopWalking(_interactionDirection);
-	if (scumm_stricmp(action, "iSABANA") == 0 &&
-		dynamic_cast<MainCharacter *>(this) != nullptr &&
-		!room()->name().equalsIgnoreCase("CASA_FREDDY_ARRIBA")) {
-		// An original hack to check that we use the bed sheet on the main character only in the correct room
-		// There *is* another script variable (es_casa_freddy) that should check this
-		// but, I guess, Alcachofa Soft found a corner case where this does not work?
-		return;
-	}
-	g_engine->player().triggerObject(this, action);
+	if (g_engine->game().shouldCharacterTrigger(this, action))
+		g_engine->player().triggerObject(this, action);
 }
 
 struct SayTextTask final : public Task {
@@ -307,11 +300,9 @@ struct SayTextTask final : public Task {
 
 			if (_soundId == kInvalidSoundID)
 			{
-				bool isMortadeloVoice =
-					_character == &g_engine->world().mortadelo() ||
-					_character->name().equalsIgnoreCase("MORTADELO_TREN"); // an original hard-coded special case
+				bool hasMortadeloVoice = g_engine->game().hasMortadeloVoice(_character);					
 				_soundId = g_engine->sounds().playVoice(
-					String::format(isMortadeloVoice ? "M%04d" : "%04d", _dialogId),
+					String::format(hasMortadeloVoice ? "M%04d" : "%04d", _dialogId),
 					0);
 			}
 			g_engine->sounds().setAppropriateVolume(_soundId, process().character(), _character);
@@ -885,6 +876,7 @@ void MainCharacter::serializeSave(Serializer &serializer) {
 	if (serializer.isLoading()) {
 		room() = room()->world().getRoomByName(roomName.c_str());
 		if (room() == nullptr)
+			// no good way to recover from this
 			error("Invalid room name \"%s\" saved for \"%s\"", roomName.c_str(), name().c_str());
 	}
 
@@ -925,8 +917,10 @@ bool MainCharacter::hasItem(const String &name) const {
 
 void MainCharacter::pickup(const String &name, bool putInHand) {
 	auto item = getItemByName(name);
-	if (item == nullptr)
-		error("Tried to pickup unknown item: %s", name.c_str());
+	if (item == nullptr) {
+		g_engine->game().unknownPickupItem(name.c_str());
+		return;
+	}
 	item->toggle(true);
 	if (g_engine->player().activeCharacter() == this) {
 		if (putInHand)
@@ -939,8 +933,9 @@ void MainCharacter::drop(const Common::String &name) {
 	if (!name.empty()) {
 		auto item = getItemByName(name);
 		if (item == nullptr)
-			error("Tried to drop unknown item: %s", name.c_str());
-		item->toggle(false);
+			g_engine->game().unknownDropItem(name.c_str());
+		else
+			item->toggle(false);
 	}
 	if (g_engine->player().activeCharacter() == this) {
 		g_engine->player().heldItem() = nullptr;
diff --git a/engines/alcachofa/game.cpp b/engines/alcachofa/game.cpp
new file mode 100644
index 00000000000..59bcee82bf0
--- /dev/null
+++ b/engines/alcachofa/game.cpp
@@ -0,0 +1,182 @@
+/* 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.h"
+#include "game.h"
+#include "script.h"
+
+using namespace Common;
+
+namespace Alcachofa {
+
+Game::Game()
+#ifdef _DEBUG // During development let's check out these errors more carefully
+	: _message(error)
+#else // For release builds the game might still work or the user might still be able to save and restart
+	: _message(warning)
+#endif
+{
+}
+
+bool Game::doesRoomHaveBackground(const Room *room) {
+	return true;
+}
+
+void Game::unknownRoomObject(const String &type) {
+	_message("Unknown type for room object: %s", type.c_str());
+}
+
+void Game::unknownDoorTargetRoom(const String &name) {
+	_message("Unknown door target room: %s", name.c_str());
+}
+
+void Game::unknownDoorTargetDoor(const String &room, const String &door) {
+	_message("Unknown door target door: %s in %s", door.c_str(), room.c_str());
+}
+
+void Game::invalidDialogLine(uint index) {
+	_message("Invalid dialog line %u");
+}
+
+void Game::tooManyDialogLines(uint lineCount, uint maxLineCount) {
+	// we set max line count as constant, if some game uses more we just have to adapt the constant
+	// the bug will be not all dialog lines being rendered
+	_message("Text to be rendered has too many lines (%u), check text validity and max line count (%u)", lineCount, maxLineCount);
+}
+
+void Game::tooManyDrawRequests(int order) {
+	// similar, the bug will be some objects not being rendered
+	_message("Too many draw requests in order %d", order);
+}
+
+bool Game::shouldCharacterTrigger(const Character *character, const char *action) {
+	return true;
+}
+
+bool Game::shouldTriggerDoor(const Door *door) {
+	return true;
+}
+
+bool Game::hasMortadeloVoice(const Character *character) {
+	return character == &g_engine->world().mortadelo();
+}
+
+void Game::unknownCamSetInactiveAttribute(int attribute) {
+	// this will be a bug by us, but gameplay should not be affected, so don't error in release builds
+	// it could still happen if an attribute was added/removed in updates so we still want users to report this
+	_message("Unknown CamSetInactiveAttribute attribute: %d", attribute);
+}
+
+void Game::unknownFadeType(int fadeType) {
+	_message("Unknown fade type %d", fadeType);
+}
+
+void Game::unknownSerializedObject(const char *object, const char *owner, const char *room) {
+	// potentially game-breaking for _currentlyUsingObject but should otherwise be just a graphical bug
+	_message("Invalid object name \"%s\" saved for \"%s\" in \"%s\"", object, owner, room);
+}
+
+void Game::unknownPickupItem(const char *name) {
+	_message("Tried to pickup unknown item: %s", name);
+}
+
+void Game::unknownDropItem(const char *name) {
+	_message("Tried to drop unknown item: %s", name);
+}
+
+void Game::unknownVariable(const char *name) {
+	_message("Unknown script variable: %s", name);
+}
+
+void Game::unknownInstruction(const ScriptInstruction &instruction) {
+	const char *type =
+		instruction._op == ScriptOp::Crash5 || // these are defined in the game
+		instruction._op == ScriptOp::Crash8 || // but implemented as crashes
+		instruction._op == ScriptOp::Crash9 ||
+		instruction._op == ScriptOp::Crash12 ||
+		instruction._op == ScriptOp::Crash21 ||
+		instruction._op == ScriptOp::Crash22 ||
+		instruction._op == ScriptOp::Crash33 ||
+		instruction._op == ScriptOp::Crash34 ||
+		instruction._op == ScriptOp::Crash35 ||
+		instruction._op == ScriptOp::Crash36
+		? "crash" : "invalid";
+	_message("Script reached %s instruction: %d %d", type, (int)instruction._op, instruction._arg);
+}
+
+void Game::unknownAnimateObject(const char *name) {
+	_message("Script tried to animated invalid graphic object: %s", name);
+}
+
+void Game::unknownScriptCharacter(const char *action, const char *name) {
+	_message("Script tried to %s using invalid character: %s", action, name);
+}
+
+PointObject *Game::unknownGoPutTarget(const Process &process, const char *action, const char *name) {
+	_message("Script tried to make character %s to invalid object %s", action, name);
+	return nullptr;
+}
+
+void Game::missingAnimation(const String &fileName) {
+	_message("Could not open animation %s", fileName.c_str());
+}
+
+void Game::unknownSayTextCharacter(const char *name, int32) {
+	unknownScriptCharacter("say text", name);
+}
+
+void Game::unknownChangeCharacterRoom(const char *name) {
+	_message("Invalid change character room name: %s", name);
+}
+
+void Game::unknownAnimateCharacterObject(const char *name) {
+	_message("Invalid animate character object: %s", name);
+}
+
+void Game::unknownAnimateTalkingObject(const char *name) {
+	_message("Invalid talk object name: %s", name);
+}
+
+void Game::unknownClearInventoryTarget(int characterKind) {
+	_message("Invalid clear inventory character kind: %d", characterKind);
+}
+
+void Game::unknownCamLerpTarget(const char *action, const char *name) {
+	_message("Invalid target object for %s: %s", action, name);
+}
+
+void Game::unknownKernelTask(int task) {
+	_message("Invalid kernel task: %d", task);
+}
+
+void Game::unknownScriptProcedure(const String &procedure) {
+	_message("Unknown required procedure: %s", procedure.c_str());
+}
+
+void Game::missingSound(const String &fileName) {
+	_message("Missing sound file: %s", fileName.c_str());
+}
+
+void Game::invalidSNDFormat(uint format, uint channels, uint freq, uint bps) {
+	_message("Invalid SND file, format: %u, channels: %u, freq: %u, bps: %u", format, channels, freq, bps);
+}
+
+}
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
new file mode 100644
index 00000000000..94b6bc64c83
--- /dev/null
+++ b/engines/alcachofa/game.h
@@ -0,0 +1,94 @@
+#pragma once
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef GAME_H
+#define GAME_H
+
+#include "common/textconsole.h"
+#include "common/file.h"
+
+namespace Alcachofa {
+
+class ObjectBase;
+class PointObject;
+class Character;
+class Door;
+class Room;
+class Process;
+struct ScriptInstruction;
+
+/**
+ * @brief Provides functionality specific to a game title.
+ * 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
+ */
+class Game {
+	typedef void (*Message)(const char *s, ...);
+public:
+	Game();
+	virtual ~Game() = default;
+
+	virtual bool doesRoomHaveBackground(const Room *room);
+	virtual void unknownRoomObject(const Common::String &type);
+	virtual void unknownDoorTargetRoom(const Common::String &name);
+	virtual void unknownDoorTargetDoor(const Common::String &room, const Common::String &door);
+
+	virtual void invalidDialogLine(uint index);
+	virtual void tooManyDialogLines(uint lineCount, uint maxLineCount);
+	virtual void tooManyDrawRequests(int order);
+
+	virtual bool shouldCharacterTrigger(const Character *character, const char *action);
+	virtual bool shouldTriggerDoor(const Door *door);
+	virtual bool hasMortadeloVoice(const Character *character);
+
+	virtual void unknownCamSetInactiveAttribute(int attribute);
+	virtual void unknownFadeType(int fadeType);
+	virtual void unknownSerializedObject(const char *object, const char *owner, const char *room);
+	virtual void unknownPickupItem(const char *name);
+	virtual void unknownDropItem(const char *name);
+	virtual void unknownVariable(const char *name);
+	virtual void unknownInstruction(const ScriptInstruction &instruction);
+	virtual void unknownAnimateObject(const char *name);
+	virtual void unknownScriptCharacter(const char *action, const char *name);
+	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 void unknownAnimateTalkingObject(const char *name);
+	virtual void unknownClearInventoryTarget(int characterKind);
+	virtual void unknownCamLerpTarget(const char *action, const char *name);
+	virtual void unknownKernelTask(int task);
+	virtual void unknownScriptProcedure(const Common::String &procedure);
+
+	virtual void missingAnimation(const Common::String &fileName);
+	virtual void missingSound(const Common::String &fileName);
+	virtual void invalidSNDFormat(uint format, uint channels, uint freq, uint bps);
+
+	static Game *createForMovieAdventure();
+
+	const Message _message;
+};
+
+}
+
+#endif // GAME_H
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 4c0a62dac62..9102d10868b 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -191,14 +191,7 @@ ManagedSurface *AnimationBase::readImage(SeekableReadStream &stream) const {
 
 void AnimationBase::loadMissingAnimation() {
 	// only allow missing animations we know are faulty in the original game
-	if (!_fileName.equalsIgnoreCase("ANIMACION.AN0") &&
-		!_fileName.equalsIgnoreCase("DESPACHO_SUPER2_OL_SOMBRAS2.AN0") &&
-		!_fileName.equalsIgnoreCase("PP_MORTA.AN0") &&
-		!_fileName.equalsIgnoreCase("DESPACHO_SUPER2___FONDO_PP_SUPER.AN0") &&
-		!_fileName.equalsIgnoreCase("ESTOMAGO.AN0") &&
-		!_fileName.equalsIgnoreCase("CREDITOS.AN0") &&
-		!_fileName.equalsIgnoreCase("MONITOR___OL_EFECTO_FONDO.AN0"))
-		error("Could not open animation %s", _fileName.c_str());
+	g_engine->game().missingAnimation(_fileName);
 
 	// otherwise setup a functioning but empty animation
 	_isLoaded = true;
@@ -444,7 +437,7 @@ void Font::load() {
 	}
 	_texture = g_engine->renderer().createTexture(atlasSurface.w, atlasSurface.h, false);
 	_texture->update(atlasSurface);
-	debug("Rendered font atlas %s at %dx%d", _fileName.c_str(), atlasSurface.w, atlasSurface.h);
+	debugCN(1, kDebugGraphics, "Rendered font atlas %s at %dx%d", _fileName.c_str(), atlasSurface.w, atlasSurface.h);
 }
 
 void Font::freeImages() {
@@ -660,8 +653,10 @@ TextDrawRequest::TextDrawRequest(Font &font, const char *originalText, Point pos
 	const byte *itChar = (byte *)text, *itLine = (byte *)text, *textEnd = itChar + textLen + 1;
 	int lineWidth = 0;
 	while (true) {
-		if (lineCount >= kMaxLines)
-			error("Text to be rendered has too many lines, check text validity and max line count");
+		if (lineCount >= kMaxLines) {
+			g_engine->game().tooManyDialogLines(lineCount, kMaxLines);
+			break;
+		}
 
 		if (*itChar != '\r' && *itChar)
 			lineWidth += characterSize(font, *itChar).x;
@@ -760,7 +755,7 @@ void FadeDrawRequest::draw() {
 		g_engine->renderer().setBlendMode(BlendMode::Additive);
 		break;
 	default:
-		assert(false && "Invalid fade type");
+		g_engine->game().unknownFadeType((int)_type);
 		return;
 	}
 	g_engine->renderer().setTexture(nullptr);
@@ -860,7 +855,7 @@ void DrawQueue::addRequest(IDrawRequest *drawRequest) {
 	if (_requestsPerOrderCount[order] < kMaxDrawRequestsPerOrder)
 		_requestsPerOrder[order][_requestsPerOrderCount[order]++] = drawRequest;
 	else
-		error("Too many draw requests in order %d", order);
+		g_engine->game().tooManyDrawRequests(order);
 }
 
 void DrawQueue::setLodBias(int8 orderFrom, int8 orderTo, float newLodBias) {
diff --git a/engines/alcachofa/metaengine.cpp b/engines/alcachofa/metaengine.cpp
index 8b45c140cf0..a31dbd032d3 100644
--- a/engines/alcachofa/metaengine.cpp
+++ b/engines/alcachofa/metaengine.cpp
@@ -28,7 +28,7 @@ namespace Alcachofa {
 
 static const ADExtraGuiOptionsMap optionsList[] = {
 	{
-		GAMEOPTION_ORIGINAL_SAVELOAD,
+		GAMEOPTION_ORIGINAL_SAVELOAD, // TODO: Remove, this is not really possible
 		{
 			_s("Use original save/load screens"),
 			_s("Use the original save/load screens instead of the ScummVM ones"),
diff --git a/engines/alcachofa/module.mk b/engines/alcachofa/module.mk
index 71e37f99a1a..5d1eec4b4f8 100644
--- a/engines/alcachofa/module.mk
+++ b/engines/alcachofa/module.mk
@@ -5,6 +5,7 @@ MODULE_OBJS = \
 	camera.cpp \
 	common.cpp \
 	console.o \
+	game.cpp \
 	game-objects.cpp \
 	general-objects.cpp \
 	global-ui.cpp \
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index 54a1407007f..d8eecfc1faa 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -95,7 +95,7 @@ void Player::drawCursor(bool forceDefaultCursor) {
 }
 
 void Player::changeRoom(const Common::String &targetRoomName, bool resetCamera) {
-	debug("Change room to %s", targetRoomName.c_str());
+	debugC(1, kDebugGameplay, "Change room to %s", targetRoomName.c_str());
 
 	// 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
@@ -122,7 +122,7 @@ void Player::changeRoom(const Common::String &targetRoomName, bool resetCamera)
 	}
 
 	_currentRoom = g_engine->world().getRoomByName(targetRoomName.c_str());
-	if (_currentRoom == nullptr)
+	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());
 
 	if (!_didLoadGlobalRooms) {
@@ -165,7 +165,7 @@ void Player::triggerObject(ObjectBase *object, const char *action) {
 	assert(object != nullptr && action != nullptr);
 	if (_activeCharacter->isBusy() || _activeCharacter->currentlyUsing() != nullptr)
 		return;
-	debug("Trigger object %s %s with %s", object->typeName(), object->name().c_str(), action);
+	debugC(1, kDebugGameplay, "Trigger object %s %s with %s", object->typeName(), object->name().c_str(), action);
 
 	if (strcmp(action, "MIRAR") == 0 || inactiveCharacter()->currentlyUsing() == object) {
 		action = "MIRAR";
@@ -184,6 +184,7 @@ void Player::triggerObject(ObjectBase *object, const char *action) {
 	//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
 		script.createProcess(activeCharacterKind(), "DefectoUsar");
 }
@@ -196,12 +197,16 @@ struct DoorTask : public Task {
 		, _character(g_engine->player().activeCharacter())
 		, _player(g_engine->player()) {
 		_targetRoom = g_engine->world().getRoomByName(door->targetRoom().c_str());
-		if (_targetRoom == nullptr)
-			error("Invalid door target room: %s", door->targetRoom().c_str());
+		if (_targetRoom == nullptr) {
+			g_engine->game().unknownDoorTargetRoom(door->targetRoom());
+			return;
+		}
 
 		_targetObject = dynamic_cast<InteractableObject *>(_targetRoom->getObjectByName(door->targetObject().c_str()));
-		if (_targetObject == nullptr)
-			error("Invalid door target door: %s", door->targetObject().c_str());
+		if (_targetObject == nullptr) {
+			g_engine->game().unknownDoorTargetDoor(door->targetRoom(), door->targetObject());
+			return;
+		}
 		auto targetDoor = dynamic_cast<const Door *>(_targetObject);
 		_targetDirection = targetDoor == nullptr
 			? _targetObject->interactionDirection()
@@ -212,6 +217,9 @@ struct DoorTask : public Task {
 
 	virtual TaskReturn run() {
 		TASK_BEGIN;
+		if (_targetRoom == nullptr || _targetObject == nullptr)
+			return TaskReturn::finish(1);
+
 		// TODO: Fade out music on room change
 		TASK_WAIT(fade(process(), FadeType::ToBlack, 0, 1, 500, EasingType::Out, -5));
 		_player.changeRoom(_targetRoom->name(), true);
@@ -250,11 +258,10 @@ private:
 void Player::triggerDoor(const Door *door) {
 	_heldItem = nullptr;
 
-	if (door->targetRoom() == "LABERINTO" && door->targetObject() == "a_LABERINTO_desde_LABERINTO_2")
-		return; // Original exception
-
-	FakeLock lock(_activeCharacter->semaphore());
-	g_engine->scheduler().createProcess<DoorTask>(activeCharacterKind(), door, move(lock));
+	if (g_engine->game().shouldTriggerDoor(door)) {
+		FakeLock lock(_activeCharacter->semaphore());
+		g_engine->scheduler().createProcess<DoorTask>(activeCharacterKind(), door, move(lock));
+	}
 }
 
 // the last dialog character mechanic seems like a hack in the original engine
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 14e0fb71de3..a26244c58eb 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -33,8 +33,7 @@ namespace Alcachofa {
 Room::Room(World *world, ReadStream &stream) : Room(world, stream, false) {
 }
 
-static ObjectBase *readRoomObject(Room *room, ReadStream &stream) {
-	const auto type = readVarString(stream);
+static ObjectBase *readRoomObject(Room *room, const String &type, ReadStream &stream) {
 	if (type == ObjectBase::kClassName)
 		return new ObjectBase(room, stream);
 	else if (type == PointObject::kClassName)
@@ -82,7 +81,7 @@ static ObjectBase *readRoomObject(Room *room, ReadStream &stream) {
 	else if (type == FloorColor::kClassName)
 		return new FloorColor(room, stream);
 	else
-		error("Unknown type for room objects: %s", type.c_str());
+		return nullptr; // handled in Room::Room
 }
 
 Room::Room(World *world, ReadStream &stream, bool hasUselessByte)
@@ -91,8 +90,6 @@ Room::Room(World *world, ReadStream &stream, bool hasUselessByte)
 	_musicId = stream.readSByte();
 	_characterAlphaTint = stream.readByte();
 	auto backgroundScale = stream.readSint16LE();
-	if (_name == "MINA")
-		backgroundScale += 0;
 	_floors[0] = PathFindingShape(stream);
 	_floors[1] = PathFindingShape(stream);
 	_fixedCameraOnEntering = readBool(stream);
@@ -104,11 +101,15 @@ Room::Room(World *world, ReadStream &stream, bool hasUselessByte)
 	uint32 objectSize = stream.readUint32LE(); // TODO: Maybe switch to seekablereadstream and assert objectSize?
 	while (objectSize > 0)
 	{
-		_objects.push_back(readRoomObject(this, stream));
+		const auto type = readVarString(stream);
+		auto object = readRoomObject(this, type, stream);
+		if (object == nullptr)
+			// TODO: Make this a warning after using SeekableReadStream in Room::Room
+			g_engine->game().unknownRoomObject(type);
+		_objects.push_back(object);
 		objectSize = stream.readUint32LE();
 	}
-	if (!_name.equalsIgnoreCase("Global") &&
-		!_name.equalsIgnoreCase("HABITACION_NEGRA"))
+	if (g_engine->game().doesRoomHaveBackground(this))
 		_objects.push_back(new Background(this, _name, backgroundScale));
 
 	if (!_floors[0].empty())
@@ -219,8 +220,8 @@ void Room::updateRoomBounds() {
 	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 but I find
-		 * this very unstable. Instead a fixed value is used
+		 * 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);
@@ -694,15 +695,12 @@ void World::loadDialogLines() {
 	while (lineStart < fileEnd) {
 		char *lineEnd = find(lineStart, fileEnd, '\n');
 		char *firstQuote = find(lineStart, lineEnd, '\"');
-		if (firstQuote == lineEnd)
-			error("Invalid dialog line - first quote");
-		char *secondQuote = find(firstQuote + 1, lineEnd, '\"');
-		if (secondQuote == lineEnd) {
-			// unfortunately one invalid line in the game
-			if (_dialogLines.size() != 4542)
-				error("Invalid dialog line - second quote");
-			firstQuote = lineStart; // for the invalid one save an empty string
-			secondQuote = firstQuote + 1;
+		char *secondQuote = firstQuote == lineEnd ? lineEnd : find(firstQuote + 1, lineEnd, '\"');
+
+		if (firstQuote == lineEnd || secondQuote == lineEnd) {
+			g_engine->game().invalidDialogLine(_dialogLines.size());
+			firstQuote = lineStart; // store an empty string
+			secondQuote = lineStart + 1;
 		}
 
 		*secondQuote = 0;
diff --git a/engines/alcachofa/scheduler.h b/engines/alcachofa/scheduler.h
index 983aacb7b9d..5299b029f61 100644
--- a/engines/alcachofa/scheduler.h
+++ b/engines/alcachofa/scheduler.h
@@ -132,6 +132,7 @@ public:
 
 	inline ProcessId pid() const { return _pid; }
 	inline MainCharacterKind &character() { return _character; } // is changed in changeCharacter
+	inline MainCharacterKind character() const { return _character; }
 	inline int32 returnValue() const { return _lastReturnValue; }
 	inline Common::String &name() { return _name; }
 	bool isActiveForPlayer() const; ///< and thus should e.g. draw subtitles or effects
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 1164cded5a3..67617c661f4 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -100,16 +100,19 @@ Script::Script() {
 
 int32 Script::variable(const char *name) const {
 	uint32 index;
-	if (!_variableNames.tryGetVal(name, index))
-		error("Unknown variable: %s", name);
-	return _variables[index];
+	if (_variableNames.tryGetVal(name, index))
+		return _variables[index];
+	g_engine->game().unknownVariable(name);
+	return 0;
 }
 
 int32 &Script::variable(const char *name) {
 	uint32 index;
-	if (!_variableNames.tryGetVal(name, index))
-		error("Unknown variable: %s", name);
-	return _variables[index];
+	if (_variableNames.tryGetVal(name, index))
+		return _variables[index];
+	g_engine->game().unknownVariable(name);
+	static int dummy = 0;
+	return dummy;
 }
 
 bool Script::hasProcedure(const Common::String &behavior, const Common::String &action) const {
@@ -196,11 +199,7 @@ struct ScriptTask : public Task {
 		if (_isFirstExecution || _returnsFromKernelCall)
 			setCharacterVariables();
 		if (_returnsFromKernelCall) {
-			// 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() && _script._instructions[_pc]._op == ScriptOp::PopN);
-			popN(_script._instructions[_pc++]._arg);
-			pushNumber(process().returnValue());
+			handleReturnFromKernelCall(process().returnValue());
 		}
 		_isFirstExecution = _returnsFromKernelCall = false;
 
@@ -264,7 +263,7 @@ struct ScriptTask : public Task {
 					return kernelReturn;
 				}
 				else
-					pushNumber(kernelReturn.returnValue());
+					handleReturnFromKernelCall(kernelReturn.returnValue());
 			}break;
 			case ScriptOp::JumpIfFalse:
 				if (popNumber() == 0)
@@ -325,19 +324,8 @@ struct ScriptTask : public Task {
 				else
 					pushNumber(returnValue);
 			}break;
-			case ScriptOp::Crash5:
-			case ScriptOp::Crash8:
-			case ScriptOp::Crash9:
-			case ScriptOp::Crash12:
-			case ScriptOp::Crash21:
-			case ScriptOp::Crash22:
-			case ScriptOp::Crash33:
-			case ScriptOp::Crash34:
-			case ScriptOp::Crash35:
-			case ScriptOp::Crash36:
-				error("Script reached crash instruction");
 			default:
-				error("Script reached invalid instruction");
+				g_engine->game().unknownInstruction(instruction);
 			}
 		}
 	}
@@ -352,10 +340,20 @@ private:
 		_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() && _script._instructions[_pc]._op == ScriptOp::PopN);
+		popN(_script._instructions[_pc++]._arg);
+		pushNumber(returnValue);
+	}
+
 	void pushNumber(int32 value) {
 		_stack.push({ StackEntryType::Number, value });
 	}
 
+	// For the following methods error recovery is not really viable
+
 	void pushVariable(uint32 offset) {
 		uint32 index = offset / sizeof(int32);
 		if (offset % sizeof(int32) != 0 || index >= _script._variables.size())
@@ -574,10 +572,10 @@ private:
 			return TaskReturn::finish(0);
 		case ScriptKernelTask::Animate: {
 			auto graphicObject = getObjectArg<GraphicObject>(0);
-			if (graphicObject == nullptr && !scumm_stricmp("EXPLOSION DISFRAZ", getStringArg(0)))
+			if (graphicObject == nullptr) {
+				g_engine->game().unknownAnimateObject(getStringArg(0));
 				return TaskReturn::finish(1);
-			if (graphicObject == nullptr)
-				error("Script tried to animate invalid graphic object %s", getStringArg(0));
+			}
 			if (getNumberOrStringArg(1)) {
 				graphicObject->toggle(true);
 				graphicObject->graphic()->start(false);
@@ -591,7 +589,7 @@ private:
 		case ScriptKernelTask::StopAndTurn: {
 			auto character = getObjectArg<WalkingCharacter>(0);
 			if (character == nullptr)
-				error("Script tried to stop-and-turn unknown character");
+				g_engine->game().unknownScriptCharacter("stop-and-turn", getStringArg(0));
 			else
 				character->stopWalking((Direction)getNumberArg(1));
 			return TaskReturn::finish(1);
@@ -602,11 +600,15 @@ private:
 		}
 		case ScriptKernelTask::Go: {
 			auto character = getObjectArg<WalkingCharacter>(0);
-			if (character == nullptr)
-				error("Script tried to make invalid character go: %s", getStringArg(0));
+			if (character == nullptr) {
+				g_engine->game().unknownScriptCharacter("go", getStringArg(0));
+				return TaskReturn::finish(1);
+			}
 			auto target = getObjectArg<PointObject>(1);
 			if (target == nullptr)
-				error("Script tried to make character go to invalid object %s", getStringArg(1));
+				target = g_engine->game().unknownGoPutTarget(process(), "go", getStringArg(1));
+			if (target == nullptr)
+				return TaskReturn::finish(0);
 			character->walkTo(target->position());
 
 			if (getNumberArg(2) & 2)
@@ -618,31 +620,39 @@ private:
 		}
 		case ScriptKernelTask::Put: {
 			auto character = getObjectArg<WalkingCharacter>(0);
-			if (character == nullptr)
-				error("Script tried to put invalid character: %s", getStringArg(0));
+			if (character == nullptr) {
+				g_engine->game().unknownScriptCharacter("put", getStringArg(0));
+				return TaskReturn::finish(1);
+			}
 			auto target = getObjectArg<PointObject>(1);
-			if (target == nullptr && !exceptionsForPut(target, getStringArg(1)))
-				return TaskReturn::finish(2);
 			if (target == nullptr)
-				error("Script tried to make character put to invalid object %s", getStringArg(1));
+				target = g_engine->game().unknownGoPutTarget(process(), "put", getStringArg(1));
+			if (target == nullptr)
+				return TaskReturn::finish(0);
 			character->setPosition(target->position());
 			return TaskReturn::finish(1);
 		}
 		case ScriptKernelTask::ChangeCharacterRoom: {
 			auto *character = getObjectArg<Character>(0);
-			if (character == nullptr)
-				error("Invalid character name: %s", getStringArg(0));
+			if (character == nullptr) {
+				g_engine->game().unknownScriptCharacter("change character room", getStringArg(0));
+				return TaskReturn::finish(1);
+			}
 			auto *targetRoom = g_engine->world().getRoomByName(getStringArg(1));
-			if (targetRoom == nullptr)
-				error("Invalid room name: %s", getStringArg(1));
+			if (targetRoom == nullptr) {
+				g_engine->game().unknownChangeCharacterRoom(getStringArg(1));
+				return TaskReturn::finish(1);
+			}
 			character->resetTalking();
 			character->room() = targetRoom;
 			return TaskReturn::finish(1);
 		}
 		case ScriptKernelTask::LerpCharacterLodBias: {
 			auto *character = getObjectArg<Character>(0);
-			if (character == nullptr)
-				error("Invalid character name: %s", getStringArg(0));
+			if (character == nullptr) {
+				g_engine->game().unknownScriptCharacter("lerp character LOD bias", getStringArg(0));
+				return TaskReturn::finish(1);
+			}
 			float targetLodBias = getNumberArg(1) * 0.01f;
 			int32 durationMs = getNumberArg(2);
 			if (durationMs <= 0)
@@ -655,28 +665,28 @@ private:
 		}
 		case ScriptKernelTask::AnimateCharacter: {
 			auto *character = getObjectArg<Character>(0);
-			if (character == nullptr)
-				error("Invalid character name: %s", getStringArg(0));
+			if (character == nullptr) {
+				g_engine->game().unknownScriptCharacter("animate character", getStringArg(0));
+				return TaskReturn::finish(1);
+			}
 			auto *animObject = getObjectArg(1);
-			if (animObject == nullptr && (
-				!scumm_stricmp("COGE F DCH", getStringArg(1)) ||
-				!scumm_stricmp("CHIQUITO_IZQ", getStringArg(1))))
-				return TaskReturn::finish(2); // original bug in MOTEL_ENTRADA
-			if (animObject == nullptr)
-				error("Invalid animate object name: %s", getStringArg(1));
+			if (animObject == nullptr) {
+				g_engine->game().unknownAnimateCharacterObject(getStringArg(1));
+				return TaskReturn::finish(1);
+			}
 			return TaskReturn::waitFor(character->animate(process(), animObject));
 		}
 		case ScriptKernelTask::AnimateTalking: {
 			auto *character = getObjectArg<Character>(0);
-			if (character == nullptr)
-				error("Invalid character name: %s", getStringArg(0));
-			const char *talkObjectName = getStringArg(1);
-			ObjectBase *talkObject = nullptr;
-			if (talkObjectName != nullptr && *talkObjectName)
+			if (character == nullptr) {
+				g_engine->game().unknownScriptCharacter("talk", getStringArg(0));
+				return TaskReturn::finish(1);
+			}
+			ObjectBase *talkObject = getObjectArg(1);
+			if (talkObject == nullptr && *getStringArg(1) != '\0')
 			{
-				talkObject = getObjectArg(1);
-				if (talkObject == nullptr)
-					error("Invalid talk object name: %s", talkObjectName);
+				g_engine->game().unknownAnimateTalkingObject(getStringArg(1));
+				return TaskReturn::finish(1);
 			}
 			character->talkUsing(talkObject);
 			return TaskReturn::finish(1);
@@ -692,9 +702,8 @@ private:
 				? &relatedCharacter()
 				: getObjectArg<Character>(0);
 			if (_character == nullptr) {
-				if (strcmp(characterName, "OFELIA") == 0 && dialogId == 3737)
-					return TaskReturn::finish(1);
-				error("Invalid character for sayText: %s", characterName);
+				g_engine->game().unknownSayTextCharacter(characterName, dialogId);
+				return TaskReturn::finish(1);
 			}
 			return TaskReturn::waitFor(_character->sayText(process(), dialogId));
 		};
@@ -725,7 +734,7 @@ private:
 			switch ((MainCharacterKind)getNumberArg(0)) {
 			case MainCharacterKind::Mortadelo: g_engine->world().mortadelo().clearInventory(); break;
 			case MainCharacterKind::Filemon: g_engine->world().filemon().clearInventory(); break;
-			default: error("Script attempted to clear inventory with invalid character kind"); break;
+			default: g_engine->game().unknownClearInventoryTarget(getNumberArg(0)); break;
 			}
 			return TaskReturn::finish(1);
 
@@ -766,8 +775,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)
-				error("Invalid target object for LerpCamToObjectKeepingZ: %s", getStringArg(0));
+			if (pointObject == nullptr) {
+				g_engine->game().unknownCamLerpTarget("LerpCamToObjectKeepingZ", getStringArg(0));
+				return TaskReturn::finish(1);
+			}
 			return TaskReturn::waitFor(g_engine->camera().lerpPos(process(),
 				as2D(pointObject->position()),
 				getNumberArg(1), EasingType::Linear));
@@ -776,8 +787,10 @@ private:
 			if (!process().isActiveForPlayer())
 				return TaskReturn::waitFor(delay(getNumberArg(1)));
 			auto pointObject = getObjectArg<PointObject>(0);
-			if (pointObject == nullptr)
-				error("Invalid target object for LerpCamToObjectResettingZ: %s", getStringArg(0));
+			if (pointObject == nullptr) {
+				g_engine->game().unknownCamLerpTarget("LerpCamToObjectResettingZ", getStringArg(0));
+				return TaskReturn::finish(1);
+			}
 			return TaskReturn::waitFor(g_engine->camera().lerpPos(process(),
 				as3D(pointObject->position()),
 				getNumberArg(1), (EasingType)getNumberArg(2)));
@@ -788,8 +801,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)
-				error("Invalid target object for LerpCamToObjectWithScale: %s", getStringArg(0));
+			if (pointObject == nullptr) {
+				g_engine->game().unknownCamLerpTarget("LerpCamToObjectWithScale", getStringArg(0));
+				return TaskReturn::finish(1);
+			}
 			return TaskReturn::waitFor(g_engine->camera().lerpPosScale(process(),
 				as3D(pointObject->position()), targetScale,
 				getNumberArg(2), (EasingType)getNumberArg(3), (EasingType)getNumberArg(4)));
@@ -840,7 +855,7 @@ private:
 		case ScriptKernelTask::Nop34:
 			return TaskReturn::finish(0);
 		default:
-			error("Invalid kernel call");
+			g_engine->game().unknownKernelTask((int)task);
 			return TaskReturn::finish(0);
 		}
 	}
@@ -861,36 +876,6 @@ private:
 		assert(character.semaphore().isReleased()); // this process should be the last to hold a lock if at all...
 	}
 
-	/**
-	 * @brief Check for original bugs related to the Put kernel call and handle them
-	 * @param target An out reference to the point object (maybe we can find an alternative one)
-	 * @param targetName The given name of the target object
-	 * @return false if the put kernel call should be ignored, true if we set target and want to continue with the kernel call
-	 */
-	bool exceptionsForPut(PointObject *&target, const char *targetName) {
-		assert(target == nullptr); // if not, why did we check for exceptions?
-
-		if (!scumm_stricmp("A_Poblado_Indio", targetName)) {
-			// 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
-			target = dynamic_cast<PointObject *>(g_engine->world().getObjectByName(process().character(), "A_Poblado_Indio1"));
-		}
-
-		if (!scumm_stricmp("PUNTO_VENTANA", targetName)) {
-			// The object is in the previous, now inactive room.
-			// Luckily Mortadelo already is at that point so not further action required
-			return false;
-		}
-
-		if (!scumm_stricmp("Puerta_Casa_Freddy_Intermedia", targetName)) {
-			// Another case of a door being cast into a PointObject
-			return false;
-		}
-
-		return true;
-	}
-
 	Script &_script;
 	Stack<StackEntry> _stack;
 	String _name;
@@ -909,7 +894,9 @@ Process *Script::createProcess(MainCharacterKind character, const String &proced
 	if (!_procedures.tryGetVal(procedure, offset)) {
 		if (flags & ScriptFlags::AllowMissing)
 			return nullptr;
-		error("Unknown required procedure: %s", procedure.c_str());
+		// it is currently unnecessary but we could return an empty process to avoid returning nullptr here
+		g_engine->game().unknownScriptProcedure(procedure);
+		return nullptr;
 	}
 	FakeLock lock;
 	if (!(flags & ScriptFlags::IsBackground))
@@ -920,7 +907,7 @@ Process *Script::createProcess(MainCharacterKind character, const String &proced
 }
 
 void Script::updateCommonVariables() {
-	if (g_engine->input().wasAnyMousePressed()) // yes, this variable is never reset by the engine
+	if (g_engine->input().wasAnyMousePressed()) // yes, this variable is never reset by the engine (only by script)
 		variable("SeHaPulsadoRaton") = 1;
 
 	if (variable("CalcularTiempoSinPulsarRaton")) {
diff --git a/engines/alcachofa/shape.cpp b/engines/alcachofa/shape.cpp
index e4ed671e3ad..82ed0baf7e8 100644
--- a/engines/alcachofa/shape.cpp
+++ b/engines/alcachofa/shape.cpp
@@ -609,7 +609,6 @@ bool PathFindingShape::findEvadeTarget(
 	Point centerTarget,
 	float depthScale, float minDistSqr,
 	Point &evadeTarget) const {
-	// TODO: Check if minDistSqr should just modify tryDistBase
 
 	for (float tryDistBase = 60; tryDistBase < 250; tryDistBase += 10) {
 		for (int tryAngleI = 0; tryAngleI < 6; tryAngleI++) {
diff --git a/engines/alcachofa/sounds.cpp b/engines/alcachofa/sounds.cpp
index e9562bd07b0..bf004d8306a 100644
--- a/engines/alcachofa/sounds.cpp
+++ b/engines/alcachofa/sounds.cpp
@@ -108,10 +108,15 @@ static AudioStream *loadSND(File *file) {
 	auto subStream = new SeekableSubReadStream(file, (uint32)file->pos(), (uint32)file->size(), DisposeAfterUse::YES);
 	if (format == 1 && channels <= 2 && (bitsPerSample == 8 || bitsPerSample == 16))
 		return makeRawStream(subStream, (int)freq,
-			(channels == 2 ? FLAG_STEREO : 0) | (bitsPerSample == 16 ? FLAG_16BITS | FLAG_LITTLE_ENDIAN : FLAG_UNSIGNED));
+			(channels == 2 ? FLAG_STEREO : 0) |
+			(bitsPerSample == 16 ? FLAG_16BITS | FLAG_LITTLE_ENDIAN : FLAG_UNSIGNED));
 	else if (format == 17 && channels <= 2)
 		return makeADPCMStream(subStream, DisposeAfterUse::YES, 0, kADPCMMSIma, (int)freq, (int)channels, (uint32)bytesPerBlock);
-	error("Invalid SND file, format: %u, channels: %u, freq: %u, bps: %u", format, channels, freq, bitsPerSample);
+	else {
+		delete file;
+		g_engine->game().invalidSNDFormat(format, channels, freq, bitsPerSample);
+		return nullptr;
+	}
 }
 
 static AudioStream *openAudio(const String &fileName) {
@@ -128,10 +133,8 @@ static AudioStream *openAudio(const String &fileName) {
 		return makeWAVStream(file, DisposeAfterUse::YES);
 	delete file;
 
-	// Ignore the known, original wrong filenames given, report the rest
-	if (fileName == "CHAS" || fileName == "517")
-		return nullptr;
-	error("Could not open audio file: %s", fileName.c_str());
+	g_engine->game().missingSound(fileName);
+	return nullptr;
 }
 
 SoundID Sounds::playSoundInternal(const String &fileName, byte volume, Mixer::SoundType type) {


Commit: 4aea1d5bf96e87470778d3b518126d94e8232a18
    https://github.com/scummvm/scummvm/commit/4aea1d5bf96e87470778d3b518126d94e8232a18
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:55+02:00

Commit Message:
ALCACHOFA: Replace TODO comment in WalkingCharacter::draw

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


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 67263acaf65..a49fa21bb2a 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -668,7 +668,10 @@ void WalkingCharacter::draw() {
 		currentGraphic->pause();
 	}
 	if (currentGraphic == nullptr) {
-		// TODO: draw dialog line
+		// The original game drew the current dialog line at this point,
+		// but I do not know of a scenario where this would be necessary
+		// As long as we cannot test this or have a bug report I rather not implement it
+
 		currentGraphic = graphicOf(_curTalkingObject, &_graphicTalking);
 	}
 


Commit: dbfa3a5600aa0494f18302e6e345523cd30aba73
    https://github.com/scummvm/scummvm/commit/dbfa3a5600aa0494f18302e6e345523cd30aba73
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:55+02:00

Commit Message:
ALCACHOFA: Refactor room reading to assert object sizes

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


diff --git a/engines/alcachofa/game.cpp b/engines/alcachofa/game.cpp
index 59bcee82bf0..f9a041cec4c 100644
--- a/engines/alcachofa/game.cpp
+++ b/engines/alcachofa/game.cpp
@@ -44,6 +44,10 @@ void Game::unknownRoomObject(const String &type) {
 	_message("Unknown type for room object: %s", type.c_str());
 }
 
+void Game::unknownRoomType(const String &type) {
+	_message("Unknown type for room: %s", type.c_str());
+}
+
 void Game::unknownDoorTargetRoom(const String &name) {
 	_message("Unknown door target room: %s", name.c_str());
 }
@@ -179,4 +183,12 @@ void Game::invalidSNDFormat(uint format, uint channels, uint freq, uint bps) {
 	_message("Invalid SND file, format: %u, channels: %u, freq: %u, bps: %u", format, channels, freq, bps);
 }
 
+void Game::notEnoughRoomDataRead(const char *path, int64 filePos, int64 roomEnd) {
+	_message("Did not read enough data (%dll < %dll) for a room in %s", filePos, roomEnd, path);
+}
+
+void Game::notEnoughObjectDataRead(const char *room, int64 filePos, int64 objectEnd) {
+	_message("Did not read enough data (%dll < %dll) for an object in room %s", filePos, objectEnd, room);
+}
+
 }
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index 94b6bc64c83..33737d444af 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -50,6 +50,7 @@ public:
 
 	virtual bool doesRoomHaveBackground(const Room *room);
 	virtual void unknownRoomObject(const Common::String &type);
+	virtual void unknownRoomType(const Common::String &type);
 	virtual void unknownDoorTargetRoom(const Common::String &name);
 	virtual void unknownDoorTargetDoor(const Common::String &room, const Common::String &door);
 
@@ -83,6 +84,8 @@ public:
 	virtual void missingAnimation(const Common::String &fileName);
 	virtual void missingSound(const Common::String &fileName);
 	virtual void invalidSNDFormat(uint format, uint channels, uint freq, uint bps);
+	virtual void notEnoughRoomDataRead(const char *path, int64 filePos, int64 objectEnd);
+	virtual void notEnoughObjectDataRead(const char *room, int64 filePos, int64 objectEnd);
 
 	static Game *createForMovieAdventure();
 
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index a26244c58eb..96ee8d78744 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -30,7 +30,7 @@ using namespace Common;
 
 namespace Alcachofa {
 
-Room::Room(World *world, ReadStream &stream) : Room(world, stream, false) {
+Room::Room(World *world, SeekableReadStream &stream) : Room(world, stream, false) {
 }
 
 static ObjectBase *readRoomObject(Room *room, const String &type, ReadStream &stream) {
@@ -84,7 +84,7 @@ static ObjectBase *readRoomObject(Room *room, const String &type, ReadStream &st
 		return nullptr; // handled in Room::Room
 }
 
-Room::Room(World *world, ReadStream &stream, bool hasUselessByte)
+Room::Room(World *world, SeekableReadStream &stream, bool hasUselessByte)
 	: _world(world) {
 	_name = readVarString(stream);
 	_musicId = stream.readSByte();
@@ -98,16 +98,25 @@ Room::Room(World *world, ReadStream &stream, bool hasUselessByte)
 	if (hasUselessByte)
 		stream.readByte();
 
-	uint32 objectSize = stream.readUint32LE(); // TODO: Maybe switch to seekablereadstream and assert objectSize?
-	while (objectSize > 0)
+	uint32 objectEnd = stream.readUint32LE();
+	while (objectEnd > 0)
 	{
 		const auto type = readVarString(stream);
 		auto object = readRoomObject(this, type, stream);
-		if (object == nullptr)
-			// TODO: Make this a warning after using SeekableReadStream in Room::Room
+		if (object == nullptr) {
 			g_engine->game().unknownRoomObject(type);
-		_objects.push_back(object);
-		objectSize = stream.readUint32LE();
+			stream.seek(objectEnd, SEEK_SET);
+		}
+		else if (stream.pos() < objectEnd) {
+			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 > %dll) in room %s", objectEnd, stream.pos(), _name.c_str());
+
+		if (object != nullptr)
+			_objects.push_back(object);
+		objectEnd = stream.readUint32LE();
 	}
 	if (g_engine->game().doesRoomHaveBackground(this))
 		_objects.push_back(new Background(this, _name, backgroundScale));
@@ -318,19 +327,19 @@ ShapeObject *Room::getSelectedObject(ShapeObject *best) const {
 	return best;
 }
 
-OptionsMenu::OptionsMenu(World *world, ReadStream &stream)
+OptionsMenu::OptionsMenu(World *world, SeekableReadStream &stream)
 	: Room(world, stream, true) {
 }
 
-ConnectMenu::ConnectMenu(World *world, ReadStream &stream)
+ConnectMenu::ConnectMenu(World *world, SeekableReadStream &stream)
 	: Room(world, stream, true) {
 }
 
-ListenMenu::ListenMenu(World *world, ReadStream &stream)
+ListenMenu::ListenMenu(World *world, SeekableReadStream &stream)
 	: Room(world, stream, true) {
 }
 
-Inventory::Inventory(World *world, ReadStream &stream)
+Inventory::Inventory(World *world, SeekableReadStream &stream)
 	: Room(world, stream, true) {
 }
 
@@ -589,7 +598,7 @@ const char *World::getDialogLine(int32 dialogId) const {
 	return _dialogLines[dialogId];
 }
 
-static Room *readRoom(World *world, ReadStream &stream) {
+static Room *readRoom(World *world, SeekableReadStream &stream) {
 	const auto type = readVarString(stream);
 	if (type == Room::kClassName)
 		return new Room(world, stream);
@@ -601,8 +610,10 @@ static Room *readRoom(World *world, ReadStream &stream) {
 		return new ListenMenu(world, stream);
 	else if (type == Inventory::kClassName)
 		return new Inventory(world, stream);
-	else
-		error("Unknown type for room %s", type.c_str());
+	else {
+		g_engine->game().unknownRoomType(type);
+		return nullptr;
+	}
 }
 
 bool World::loadWorldFile(const char *path) {
@@ -632,8 +643,15 @@ bool World::loadWorldFile(const char *path) {
 
 	uint32 roomEnd = file.readUint32LE();
 	while (roomEnd > 0) {
-		_rooms.push_back(readRoom(this, file));
-		assert(file.pos() == roomEnd);
+		Room *room = readRoom(this, file);
+		if (room != nullptr)
+			_rooms.push_back(room);
+		if (file.pos() < roomEnd) {
+			g_engine->game().notEnoughRoomDataRead(path, 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);
 		roomEnd = file.readUint32LE();
 	}
 
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index 8fad2cf1d93..37c5527476d 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -31,7 +31,7 @@ class World;
 class Room {
 public:
 	static constexpr const char *kClassName = "CHabitacion";
-	Room(World *world, Common::ReadStream &stream);
+	Room(World *world, Common::SeekableReadStream &stream);
 	virtual ~Room();
 
 	inline World &world() { return *_world; }
@@ -64,7 +64,7 @@ public:
 	void debugPrint(bool withObjects) const;
 
 protected:
-	Room(World *world, Common::ReadStream &stream, bool hasUselessByte);
+	Room(World *world, Common::SeekableReadStream &stream, bool hasUselessByte);
 	void updateScripts();
 	void updateRoomBounds();
 	void updateInteraction();
@@ -90,25 +90,25 @@ protected:
 class OptionsMenu final : public Room {
 public:
 	static constexpr const char *kClassName = "CHabitacionMenuOpciones";
-	OptionsMenu(World *world, Common::ReadStream &stream);
+	OptionsMenu(World *world, Common::SeekableReadStream &stream);
 };
 
 class ConnectMenu final : public Room {
 public:
 	static constexpr const char *kClassName = "CHabitacionConectar";
-	ConnectMenu(World *world, Common::ReadStream &stream);
+	ConnectMenu(World *world, Common::SeekableReadStream &stream);
 };
 
 class ListenMenu final : public Room {
 public:
 	static constexpr const char *kClassName = "CHabitacionEsperar";
-	ListenMenu(World *world, Common::ReadStream &stream);
+	ListenMenu(World *world, Common::SeekableReadStream &stream);
 };
 
 class Inventory final : public Room {
 public:
 	static constexpr const char *kClassName = "CInventario";
-	Inventory(World *world, Common::ReadStream &stream);
+	Inventory(World *world, Common::SeekableReadStream &stream);
 	virtual ~Inventory() override;
 
 	virtual bool updateInput() override;


Commit: 8f17fceba22c181f6984540dda5a6deae55868f6
    https://github.com/scummvm/scummvm/commit/8f17fceba22c181f6984540dda5a6deae55868f6
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:55+02:00

Commit Message:
ALCACHOFA: Add lip-sync

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


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index a49fa21bb2a..90e3be850d2 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -201,7 +201,8 @@ void Character::updateTalkingAnimation() {
 		talkGraphic->reset();
 		return;
 	}
-	// TODO: Add lip-sync(?) animation behavior
+	if (talkGraphic == &_graphicTalking && !_isSpeaking)
+		talkGraphic->reset();
 	talkGraphic->update();
 }
 
@@ -292,6 +293,8 @@ struct SayTextTask final : public Task {
 	}
 
 	virtual TaskReturn run() override {
+		bool isSoundStillPlaying;
+
 		TASK_BEGIN;
 		_character->_isTalking = true;
 		graphicOf(_character->_curTalkingObject, &_character->_graphicTalking)->start(true);
@@ -305,8 +308,9 @@ struct SayTextTask final : public Task {
 					String::format(hasMortadeloVoice ? "M%04d" : "%04d", _dialogId),
 					0);
 			}
+			isSoundStillPlaying = g_engine->sounds().isAlive(_soundId);
 			g_engine->sounds().setAppropriateVolume(_soundId, process().character(), _character);
-			if (!g_engine->sounds().isAlive(_soundId) || g_engine->input().wasAnyMouseReleased())
+			if (!isSoundStillPlaying || g_engine->input().wasAnyMouseReleased())
 				_character->_isTalking = false;
 
 			if (true && // TODO: Add game option for subtitles
@@ -317,13 +321,15 @@ struct SayTextTask final : public Task {
 					Point(g_system->getWidth() / 2, g_system->getHeight() - 200),
 					-1, true, kWhite, -kForegroundOrderCount);
 			}
-			// TODO: Add lip sync for sayText
 
 			if (!_character->_isTalking) {
 				g_engine->sounds().fadeOut(_soundId, 100);
 				TASK_WAIT(delay(200));
 				TASK_RETURN(0);
 			}
+
+			_character->isSpeaking() = !isSoundStillPlaying ||
+				g_engine->sounds().isNoisy(_soundId, 80.0f, 150.0f);
 			TASK_YIELD;
 		}
 		TASK_END;
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 7ce235eac67..f6f53adbc75 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -396,6 +396,7 @@ public:
 	Task *animate(Process &process, ObjectBase *animateObject);
 	Task *lerpLodBias(Process &process, float targetLodBias, int32 durationMs);
 	inline float &lodBias() { return _lodBias; }
+	inline bool &isSpeaking() { return _isSpeaking; }
 
 protected:
 	friend struct SayTextTask;
@@ -406,7 +407,8 @@ protected:
 	Direction _direction = Direction::Right;
 	Graphic _graphicNormal, _graphicTalking;
 
-	bool _isTalking = false;
+	bool _isTalking = false; ///< as in "in the process of saying a line"
+	bool _isSpeaking = true; ///< as in "actively moving their mouth to produce sounds", used in updateTalkingAnimation
 	int _curDialogId = -1;
 	float _lodBias = 0.0f;
 	ObjectBase
diff --git a/engines/alcachofa/sounds.cpp b/engines/alcachofa/sounds.cpp
index bf004d8306a..0425b35bf08 100644
--- a/engines/alcachofa/sounds.cpp
+++ b/engines/alcachofa/sounds.cpp
@@ -35,10 +35,6 @@ using namespace Audio;
 
 namespace Alcachofa {
 
-Sounds::Playback::Playback(uint32 id, SoundHandle handle, Mixer::SoundType type)
-	: _id(id), _handle(handle), _type(type) {
-}
-
 void Sounds::Playback::fadeOut(uint32 duration) {
 	_fadeStart = g_system->getMillis();
 	_fadeDuration = MAX<uint32>(duration, 1);
@@ -141,11 +137,58 @@ SoundID Sounds::playSoundInternal(const String &fileName, byte volume, Mixer::So
 	AudioStream *stream = openAudio(fileName);
 	if (stream == nullptr)
 		return UINT32_MAX;
-	SoundHandle handle;
-	_mixer->playStream(type, &handle, stream, -1, volume);
-	SoundID id = _nextID++;
-	_playbacks.push_back({ id, handle, type });
-	return id;
+
+	Array<int16> samples;
+	SeekableAudioStream *seekStream = dynamic_cast<SeekableAudioStream *>(stream);
+	if (type == Mixer::kSpeechSoundType && seekStream != nullptr) {
+		// for lip-sync we need access to the samples so we decode the entire stream now
+		int sampleCount = seekStream->getLength().totalNumberOfFrames();
+		if (sampleCount > 0) {
+			// we actually got a length
+			samples.resize((uint)sampleCount);
+			sampleCount = seekStream->readBuffer(samples.data(), sampleCount);
+			if (sampleCount < 0)
+				samples.clear();
+			samples.resize((uint)sampleCount); // we might have gotten less samples
+		}
+		else {
+			// we did not, not it is getting inefficient
+			const int bufferSize = 512;
+			int16 buffer[bufferSize];
+			int chunkSampleCount;
+			do {
+				chunkSampleCount = seekStream->readBuffer(buffer, bufferSize);
+				if (chunkSampleCount <= 0)
+					break;
+				samples.resize(samples.size() + chunkSampleCount);
+				copy(buffer, buffer + chunkSampleCount, samples.data() + sampleCount);
+				sampleCount += chunkSampleCount;
+			} while (chunkSampleCount >= bufferSize);
+		}
+
+		if (sampleCount > 0) {
+			stream = makeRawStream(
+				(byte *)samples.data(),
+				samples.size() * sizeof(int16),
+				seekStream->getRate(),
+				FLAG_16BITS |
+#ifdef SCUMM_LITTLE_ENDIAN
+				FLAG_LITTLE_ENDIAN | // readBuffer returns native endian
+#endif
+				(seekStream->isStereo() ? FLAG_STEREO : 0),
+				DisposeAfterUse::NO);
+			delete seekStream;
+		}
+	}
+
+	Playback playback;
+	_mixer->playStream(type, &playback._handle, stream, -1, volume);
+	playback._id = _nextID++;
+	playback._type = type;
+	playback._inputRate = stream->getRate();
+	playback._samples = std::move(samples);
+	_playbacks.push_back(std::move(playback));
+	return playback._id;
 }
 
 SoundID Sounds::playVoice(const String &fileName, byte volume) {
@@ -209,6 +252,36 @@ void Sounds::fadeOutVoiceAndSFX(uint32 duration) {
 	}
 }
 
+bool Sounds::isNoisy(SoundID id, float windowSize, float minDifferences) {
+	assert(windowSize > 0 && minDifferences > 0);
+	const Playback *playback = getPlaybackById(id);
+	if (playback == nullptr ||
+		playback->_samples.empty() ||
+		!_mixer->isSoundHandleActive(playback->_handle))
+		return false;
+
+	minDifferences *= windowSize;
+	uint windowSizeInSamples = (uint)(windowSize * 0.001f * playback->_inputRate);
+	uint samplePosition = (uint)_mixer->getElapsedTime(playback->_handle)
+		.convertToFramerate(playback->_inputRate)
+		.totalNumberOfFrames();
+	uint endPosition = MIN(playback->_samples.size(), samplePosition + windowSizeInSamples);
+	if (samplePosition >= endPosition)
+		return false;
+
+	/* While both ScummVM and the original engine use signed int16 samples
+	 * For this noise detection the samples are reinterpret as uint16
+	 * This causes changes going through zero to be much more significant.
+	 */
+	float sumOfDifferences = 0;
+	const uint16 *samplePtr = (const uint16 *)playback->_samples.data();
+	for (uint i = samplePosition; i < endPosition - 1; i++)
+		// cast to int before to not be constrained by uint16
+		sumOfDifferences += ABS((int)samplePtr[i + 1] - samplePtr[i]);
+
+	return sumOfDifferences / 256.0f >= minDifferences;
+}
+
 PlaySoundTask::PlaySoundTask(Process &process, SoundID soundID)
 	: Task(process)
 	, _soundID(soundID) {
diff --git a/engines/alcachofa/sounds.h b/engines/alcachofa/sounds.h
index 377d1313a59..ca320fb427f 100644
--- a/engines/alcachofa/sounds.h
+++ b/engines/alcachofa/sounds.h
@@ -24,6 +24,7 @@
 
 #include "scheduler.h"
 #include "audio/mixer.h"
+#include "audio/audiostream.h"
 
 namespace Alcachofa {
 
@@ -47,17 +48,19 @@ public:
 	void setAppropriateVolume(SoundID id,
 		MainCharacterKind processCharacter,
 		Character *speakingCharacter);
+	bool isNoisy(SoundID id, float windowSize, float minDifferences); ///< used for lip-sync
 
 private:
-	struct Playback {
-		Playback(uint32 id, Audio::SoundHandle handle, Audio::Mixer::SoundType type);
+	struct Playback {;
 		void fadeOut(uint32 duration);
 
-		SoundID _id;
+		SoundID _id = 0;
 		Audio::SoundHandle _handle;
-		Audio::Mixer::SoundType _type;
+		Audio::Mixer::SoundType _type = Audio::Mixer::SoundType::kPlainSoundType;
 		uint32 _fadeStart = 0,
 			_fadeDuration = 0;
+		int _inputRate;
+		Common::Array<int16> _samples; ///< might not be filled, only voice samples are preloaded for lip-sync
 	};
 	Playback *getPlaybackById(SoundID id);
 	SoundID playSoundInternal(const Common::String &fileName, byte volume, Audio::Mixer::SoundType type);


Commit: 0f167489aee1882c2eefbfa075f6754c03dfb807
    https://github.com/scummvm/scummvm/commit/0f167489aee1882c2eefbfa075f6754c03dfb807
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:55+02:00

Commit Message:
ALCACHOFA: Fix character direction after triggering doors

Changed paths:
    engines/alcachofa/player.cpp


diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index d8eecfc1faa..4d264512982 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -207,10 +207,7 @@ struct DoorTask : public Task {
 			g_engine->game().unknownDoorTargetDoor(door->targetRoom(), door->targetObject());
 			return;
 		}
-		auto targetDoor = dynamic_cast<const Door *>(_targetObject);
-		_targetDirection = targetDoor == nullptr
-			? _targetObject->interactionDirection()
-			: targetDoor->characterDirection();
+		_targetDirection = door->characterDirection();
 
 		process.name() = String::format("Door to %s %s", _targetRoom->name().c_str(), _targetObject->name().c_str());
 	}


Commit: 0f8d9f70bab9931fa9ed7a53dc25f13e8cd36b72
    https://github.com/scummvm/scummvm/commit/0f8d9f70bab9931fa9ed7a53dc25f13e8cd36b72
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:55+02:00

Commit Message:
ALCACHOFA: Add FloorColorDebugHandler

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/console.cpp
    engines/alcachofa/console.h
    engines/alcachofa/debug.h
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/graphics-opengl.cpp
    engines/alcachofa/graphics.h
    engines/alcachofa/objects.h
    engines/alcachofa/player.cpp
    engines/alcachofa/rooms.h


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index c2f55cd00e1..fa9636fb0df 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -155,6 +155,12 @@ void AlcachofaEngine::setDebugMode(DebugMode mode, int32 param) {
 	case DebugMode::TeleportCharacter:
 		_debugHandler.reset(new TeleportCharacterDebugHandler(param));
 		break;
+	case DebugMode::FloorAlpha:
+		_debugHandler.reset(FloorColorDebugHandler::create(param, false));
+		break;
+	case DebugMode::FloorColor:
+		_debugHandler.reset(FloorColorDebugHandler::create(param, true));
+		break;
 	default: _debugHandler.reset(nullptr);
 	}
 	_input.toggleDebugInput(isDebugModeActive());
diff --git a/engines/alcachofa/console.cpp b/engines/alcachofa/console.cpp
index 020a1c6c59a..dbb268fa43f 100644
--- a/engines/alcachofa/console.cpp
+++ b/engines/alcachofa/console.cpp
@@ -215,6 +215,8 @@ bool Console::cmdDebugMode(int argc, const char **args) {
 		debugPrintf("  1 - Closest floor point, param limits to polygon\n");
 		debugPrintf("  2 - Floor edge intersections, param limits to polygon\n");
 		debugPrintf("  3 - Teleport character to mouse click, param selects character\n");
+		debugPrintf("  4 - Show floor alpha, param selects index of floor color object\n");
+		debugPrintf("  5 - Show floor color, param selects index of floor color object\n");
 		return true;
 	}
 
diff --git a/engines/alcachofa/console.h b/engines/alcachofa/console.h
index 39376311cbb..b4d8af84a73 100644
--- a/engines/alcachofa/console.h
+++ b/engines/alcachofa/console.h
@@ -32,6 +32,8 @@ enum class DebugMode {
 	ClosestFloorPoint,
 	FloorIntersections,
 	TeleportCharacter,
+	FloorAlpha,
+	FloorColor
 };
 
 class Console : public GUI::Debugger {
diff --git a/engines/alcachofa/debug.h b/engines/alcachofa/debug.h
index 0e1eef52529..cdfc42c8c93 100644
--- a/engines/alcachofa/debug.h
+++ b/engines/alcachofa/debug.h
@@ -66,8 +66,11 @@ public:
 	virtual void update() override {
 		auto floor = g_engine->player().currentRoom()->activeFloor();
 		auto renderer = dynamic_cast<IDebugRenderer *>(&g_engine->renderer());
-		if (floor == nullptr || renderer == nullptr)
+		if (floor == nullptr || renderer == nullptr) {
+			g_engine->console().attach("Either the room has no floor or the renderer is not a debug renderer");
+			g_engine->setDebugMode(DebugMode::None, 0);
 			return;
+		}
 
 		if (g_engine->input().debugInput().wasMouseLeftPressed())
 			_fromPos3D = g_engine->input().debugInput().mousePos3D();
@@ -156,6 +159,77 @@ private:
 	}
 };
 
+class FloorColorDebugHandler final : public IDebugHandler {
+	const FloorColorShape &_shape;
+	const bool _useColor;
+	Color _curColor = kDebugGreen;
+	bool _isOnFloor = false;
+	static constexpr size_t kBufferSize = 64;
+	char _buffer[kBufferSize];
+
+	FloorColorDebugHandler(const FloorColorShape &shape, bool useColor)
+		: _shape(shape)
+		, _useColor(useColor) {}
+public:
+	static FloorColorDebugHandler *create(int32 objectI, bool useColor) {
+		const Room *room = g_engine->player().currentRoom();
+		uint floorCount = 0;
+		for (auto itObject = room->beginObjects(); itObject != room->endObjects(); ++itObject) {
+			FloorColor *floor = dynamic_cast<FloorColor*>(*itObject);
+			if (floor == nullptr)
+				continue;
+			if (objectI <= 0)
+				// dynamic_cast is not possible due to Shape not having virtual methods
+				return new FloorColorDebugHandler(*(FloorColorShape*)(floor->shape()), useColor);
+			floorCount++;
+			objectI--;
+		}
+
+		g_engine->console().debugPrintf("Invalid floor color index, there are %u floors in this room\n", floorCount);
+		return nullptr;
+	}
+
+	virtual void update() override {
+		auto &input = g_engine->input().debugInput();
+		if (input.wasMouseRightPressed()) {
+			g_engine->setDebugMode(DebugMode::None, 0);
+			return;
+		}
+
+		if (input.isMouseLeftDown()) {
+			auto optColor = _shape.colorAt(input.mousePos3D());
+			_isOnFloor = optColor.first;
+			if (!_isOnFloor) {
+				uint8 roomAlpha = (uint)(g_engine->player().currentRoom()->characterAlphaTint() * 255 / 100);
+				optColor.second = Color{ 255, 255, 255, roomAlpha };
+			}
+
+			_curColor = _useColor
+				? Color{ optColor.second.r, optColor.second.g, optColor.second.b, 255 }
+				: Color{ optColor.second.a, optColor.second.a, optColor.second.a, 255 };
+			g_engine->world().mortadelo().color() =
+				g_engine->world().filemon().color() =
+				_useColor ? optColor.second : Color{ 255, 255, 255, optColor.second.a };
+		}
+
+		snprintf(_buffer, kBufferSize, "r:%3d g:%3d b:%3d a:%3d",
+			(int)_curColor.r, (int)_curColor.g, (int)_curColor.b, (int)_curColor.a);
+
+		auto *debugRenderer = dynamic_cast<IDebugRenderer *>(&g_engine->renderer());
+		g_engine->drawQueue().clear();
+		g_engine->player().drawCursor(true);
+		g_engine->renderer().setTexture(nullptr);
+		g_engine->renderer().quad({ 0, 0 }, { 50, 50 }, _isOnFloor ? _curColor : kDebugGreen);
+		g_engine->drawQueue().add<TextDrawRequest>(
+			g_engine->globalUI().dialogFont(), _buffer, Point { 70, 20 }, 500, false, kWhite, -kForegroundOrderCount + 1);
+		if (!_isOnFloor)
+			g_engine->renderer().quad({ 5, 5 }, { 40, 40 }, _curColor);
+		if (debugRenderer != nullptr)
+			debugRenderer->debugShape(_shape, kDebugBlue);
+		g_engine->drawQueue().draw();
+	}
+};
+
 }
 
 #endif // DEBUG_H
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 90e3be850d2..5b22b62a451 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -784,7 +784,9 @@ void MainCharacter::update() {
 		_currentPos.x - halfWidth, _currentPos.y - height,
 		_currentPos.x + halfWidth, _currentPos.y));
 
-	// TODO: Update character alpha tint
+	// These are set as members as FloorColor might want to change them
+	_alphaPremultiplier = room()->characterAlphaPremultiplier();
+	_color = { 255, 255, 255, (uint8)(room()->characterAlphaTint() * 255 / 100) };
 }
 
 void MainCharacter::onArrived() {
@@ -860,17 +862,16 @@ void MainCharacter::drawInner() {
 	Graphic *activeGraphic = graphicOf(_curAnimateObject);
 	if (activeGraphic == nullptr && _isWalking) {
 		activeGraphic = &_graphicNormal;
-		_graphicNormal.premultiplyAlpha() = room()->characterAlphaPremultiplier();
+		_graphicNormal.premultiplyAlpha() = _alphaPremultiplier;
 	}
 	if (activeGraphic == nullptr) {
 		activeGraphic = graphicOf(_curTalkingObject, &_graphicTalking);
-		_graphicTalking.premultiplyAlpha() = room()->characterAlphaPremultiplier();
+		_graphicTalking.premultiplyAlpha() = _alphaPremultiplier;
 	}
 
 	assert(activeGraphic != nullptr);
-	activeGraphic->color().a = room()->characterAlphaTint() * 255 / 100;
+	activeGraphic->color() = _color;
 	g_engine->drawQueue().add<AnimationDrawRequest>(*activeGraphic, true, BlendMode::AdditiveAlpha, _lodBias);
-
 }
 
 void syncDialogMenuLine(Serializer &serializer, DialogMenuLine &line) {
diff --git a/engines/alcachofa/graphics-opengl.cpp b/engines/alcachofa/graphics-opengl.cpp
index 5d8428ba6ea..bee04e2338b 100644
--- a/engines/alcachofa/graphics-opengl.cpp
+++ b/engines/alcachofa/graphics-opengl.cpp
@@ -255,17 +255,17 @@ public:
 	}
 
 	virtual void quad(
-		Vector2d center,
+		Vector2d topLeft,
 		Vector2d size,
 		Color color,
 		Angle rotation,
 		Vector2d texMin,
 		Vector2d texMax) override {
 		Vector2d positions[] = {
-			center + Vector2d(0,			0),
-			center + Vector2d(0,			+size.getY()),
-			center + Vector2d(+size.getX(), +size.getY()),
-			center + Vector2d(+size.getX(), 0),
+			topLeft + Vector2d(0,			0),
+			topLeft + Vector2d(0,			+size.getY()),
+			topLeft + Vector2d(+size.getX(), +size.getY()),
+			topLeft + Vector2d(+size.getX(), 0),
 		};
 		if (abs(rotation.getDegrees()) > epsilon) {
 			const Vector2d zero(0, 0);
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index ac8f80db2b7..788dc583f3b 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -82,7 +82,7 @@ public:
 	virtual void setBlendMode(BlendMode blendMode) = 0;
 	virtual void setLodBias(float lodBias) = 0;
 	virtual void quad(
-		Math::Vector2d center, // TOOD: Use topLeft&size instead of center&size
+		Math::Vector2d topLeft,
 		Math::Vector2d size,
 		Color color = kWhite,
 		Math::Angle rotation = Math::Angle(),
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index f6f53adbc75..3af9ef3e4a2 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -492,6 +492,8 @@ public:
 	inline MainCharacterKind kind() const { return _kind; }
 	inline ObjectBase *&currentlyUsing() { return _currentlyUsingObject; }
 	inline ObjectBase *currentlyUsing() const { return _currentlyUsingObject; }
+	inline Color &color() { return _color; }
+	inline uint8 &alphaPremultiplier() { return _alphaPremultiplier; }
 	inline FakeSemaphore &semaphore() { return _semaphore; }
 	bool isBusy() const;
 
@@ -531,6 +533,8 @@ private:
 	FakeSemaphore _semaphore;
 	ITriggerableObject *_activateObject = nullptr;
 	const char *_activateAction = nullptr;
+	Color _color = kWhite;
+	uint8 _alphaPremultiplier = 255;
 };
 
 class Background final : public GraphicObject {
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index 4d264512982..adcc5e26713 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -195,7 +195,9 @@ struct DoorTask : public Task {
 		, _lock(move(lock))
 		, _sourceDoor(door)
 		, _character(g_engine->player().activeCharacter())
-		, _player(g_engine->player()) {
+		, _player(g_engine->player())
+		, _targetObject(nullptr)
+		, _targetDirection(Direction::Invalid) {
 		_targetRoom = g_engine->world().getRoomByName(door->targetRoom().c_str());
 		if (_targetRoom == nullptr) {
 			g_engine->game().unknownDoorTargetRoom(door->targetRoom());
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index 37c5527476d..976ad66fd28 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -49,7 +49,7 @@ public:
 	inline uint8 characterAlphaPremultiplier() const { return _characterAlphaPremultiplier; }
 	inline bool fixedCameraOnEntering() const { return _fixedCameraOnEntering; }
 
-	using ObjectIterator = Common::Array<const ObjectBase *>::const_iterator;
+	using ObjectIterator = Common::Array<ObjectBase *>::const_iterator;
 	inline ObjectIterator beginObjects() const { return _objects.begin(); }
 	inline ObjectIterator endObjects() const { return _objects.end(); }
 


Commit: 536eb12185871cacc1f12753e65da8320beeb8cd
    https://github.com/scummvm/scummvm/commit/536eb12185871cacc1f12753e65da8320beeb8cd
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:55+02:00

Commit Message:
ALCACHOFA: Fix reading floor brightness

Changed paths:
    engines/alcachofa/shape.cpp


diff --git a/engines/alcachofa/shape.cpp b/engines/alcachofa/shape.cpp
index 82ed0baf7e8..71b847550a6 100644
--- a/engines/alcachofa/shape.cpp
+++ b/engines/alcachofa/shape.cpp
@@ -198,11 +198,11 @@ static Color colorAtForConvex(const FloorColorPolygon &p, Point query) {
 	// This is a quite literal translation of the original engine
 	// There may very well be a better way than this...
 	float weights[FloorColorShape::kPointsPerPolygon];
-	memset(weights, 0, sizeof(weights));
+	fill(weights, weights + FloorColorShape::kPointsPerPolygon, 0.0f);
 
 	for (uint i = 0; i < p._points.size(); i++) {
 		EdgeDistances distances = p.edgeDistances(i, query);
-		float edgeWeight = distances._toEdge * distances._onEdge / distances._edgeLength;
+		float edgeWeight = distances._toEdge * ABS(distances._onEdge) / distances._edgeLength;
 		if (distances._edgeLength > 1) {
 			weights[i] += edgeWeight;
 			weights[i + 1 == p._points.size() ? 0 : i + 1] += edgeWeight;
@@ -638,16 +638,19 @@ FloorColorShape::FloorColorShape(ReadStream &stream) {
 	for (int i = 0; i < polygonCount; i++) {
 		for (int j = 0; j < kPointsPerPolygon; j++)
 			_points.push_back(readPoint(stream));
-		Color color; // RGB and A components are stored separately
+
+		// 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
+		int firstColorI = _pointColors.size();
+		_pointColors.resize(_pointColors.size() + kPointsPerPolygon);
 		for (int j = 0; j < kPointsPerPolygon; j++)
-			color.a = stream.readByte();
+			_pointColors[firstColorI + j].a = (uint8)MIN(255, stream.readByte() * 255 / 100);
 		for (int j = 0; j < kPointsPerPolygon; j++) {
-			color.r = stream.readByte();
-			color.g = stream.readByte();
-			color.b = stream.readByte();
+			_pointColors[firstColorI + j].r = stream.readByte();
+			_pointColors[firstColorI + j].g = stream.readByte();
+			_pointColors[firstColorI + j].b = stream.readByte();
 			stream.readByte(); // second alpha value is ignored
 		}
-		_pointColors.push_back(color);
 		stream.readByte(); // unused byte per polygon
 
 		uint pointCount = addPolygon(kPointsPerPolygon);


Commit: 39d0d54da030bdb50c4836a02ab9dfa3e62c27c1
    https://github.com/scummvm/scummvm/commit/39d0d54da030bdb50c4836a02ab9dfa3e62c27c1
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:55+02:00

Commit Message:
ALCACHOFA: Increase read buffer for less latency on playing voice lines

Changed paths:
    engines/alcachofa/sounds.cpp


diff --git a/engines/alcachofa/sounds.cpp b/engines/alcachofa/sounds.cpp
index 0425b35bf08..433eadf9326 100644
--- a/engines/alcachofa/sounds.cpp
+++ b/engines/alcachofa/sounds.cpp
@@ -152,8 +152,8 @@ SoundID Sounds::playSoundInternal(const String &fileName, byte volume, Mixer::So
 			samples.resize((uint)sampleCount); // we might have gotten less samples
 		}
 		else {
-			// we did not, not it is getting inefficient
-			const int bufferSize = 512;
+			// we did not, now it is getting inefficient
+			const int bufferSize = 2048;
 			int16 buffer[bufferSize];
 			int chunkSampleCount;
 			do {


Commit: 2c4810728866f14f05d2ba8d8df0f88a1ebdb3a9
    https://github.com/scummvm/scummvm/commit/2c4810728866f14f05d2ba8d8df0f88a1ebdb3a9
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:55+02:00

Commit Message:
ALCACHOFA: Add character lighting

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


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 5b22b62a451..709d76053a9 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -1099,6 +1099,18 @@ FloorColor::FloorColor(Room *room, ReadStream &stream)
 	, _shape(stream) {
 }
 
+void FloorColor::update() {
+	auto updateFor = [&] (MainCharacter &character) {
+		if (character.room() == room()) {
+			const auto result = _shape.colorAt(character.position());
+			if (result.first)
+				character.color() = { 255, 255, 255, result.second.a };
+		}
+	};
+	updateFor(g_engine->world().mortadelo());
+	updateFor(g_engine->world().filemon());
+}
+
 void FloorColor::drawDebug() {
 	auto renderer = dynamic_cast<IDebugRenderer *>(&g_engine->renderer());
 	if (!g_engine->console().showFloorColor() || renderer == nullptr || !isEnabled())
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 3af9ef3e4a2..21bf8d042e0 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -549,6 +549,7 @@ public:
 	FloorColor(Room *room, Common::ReadStream &stream);
 	virtual ~FloorColor() override = default;
 
+	virtual void update() override;
 	virtual void drawDebug() override;
 	virtual Shape *shape() override;
 	virtual const char *typeName() const;


Commit: 1322ef52ca42c48082f1c9160ce012fbea2bc209
    https://github.com/scummvm/scummvm/commit/1322ef52ca42c48082f1c9160ce012fbea2bc209
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:56+02:00

Commit Message:
ALCACHOFA: Add music

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/detection.cpp
    engines/alcachofa/detection.h
    engines/alcachofa/global-ui.cpp
    engines/alcachofa/player.cpp
    engines/alcachofa/rooms.cpp
    engines/alcachofa/rooms.h
    engines/alcachofa/script.cpp
    engines/alcachofa/sounds.cpp
    engines/alcachofa/sounds.h


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index fa9636fb0df..b9437cac40c 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -111,6 +111,7 @@ void AlcachofaEngine::playVideo(int32 videoId) {
 	Video::MPEGPSDecoder decoder;
 	if (!decoder.loadFile(Common::Path(Common::String::format("Data/DATA%02d.BIN", videoId + 1))))
 		error("Could not find video %d", videoId);
+	_sounds.stopAll();
 	auto texture = _renderer->createTexture(decoder.getWidth(), decoder.getHeight(), false);
 	decoder.start();
 
diff --git a/engines/alcachofa/detection.cpp b/engines/alcachofa/detection.cpp
index ce292b94454..23b2d7543d6 100644
--- a/engines/alcachofa/detection.cpp
+++ b/engines/alcachofa/detection.cpp
@@ -33,6 +33,7 @@ const DebugChannelDef AlcachofaMetaEngineDetection::debugFlagList[] = {
 	{ Alcachofa::kDebugGraphics, "Graphics", "Graphics debug level" },
 	{ Alcachofa::kDebugScript, "Script", "Enable debug script dump" },
 	{ Alcachofa::kDebugGameplay, "Gameplay", "Gameplay-related tracing" },
+	{ Alcachofa::kDebugSounds, "Sounds", "Sound- and Music-related tracing" },
 	DEBUG_CHANNEL_END
 };
 
diff --git a/engines/alcachofa/detection.h b/engines/alcachofa/detection.h
index 20a4f8bfa59..dfe83a6b8dc 100644
--- a/engines/alcachofa/detection.h
+++ b/engines/alcachofa/detection.h
@@ -29,7 +29,8 @@ namespace Alcachofa {
 enum AlcachofaDebugChannels {
 	kDebugGraphics = 1,
 	kDebugScript,
-	kDebugGameplay
+	kDebugGameplay,
+	kDebugSounds,
 };
 
 extern const PlainGameDescriptor alcachofaGames[];
diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index dab2ecfe071..297a3598c54 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -144,7 +144,14 @@ bool GlobalUI::updateChangingCharacter() {
 	g_engine->camera().setFollow(player.activeCharacter());
 	g_engine->camera().restore(0);
 	player.changeRoom(player.activeCharacter()->room()->name(), false);
-	// TODO: Queue character change jingle
+
+	int32 characterJingle = g_engine->script().variable(
+		player.activeCharacterKind() == MainCharacterKind::Mortadelo
+		? "PistaMorta"
+		: "PistaFile"
+	);
+	g_engine->sounds().startMusic(characterJingle);
+	g_engine->sounds().queueMusic(player.currentRoom()->musicID());
 
 	_changeButton.setAnimation(activeAnimation());
 	_changeButton.start(false);
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index adcc5e26713..afd09b36646 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -215,11 +215,15 @@ struct DoorTask : public Task {
 	}
 
 	virtual TaskReturn run() {
+		FakeLock musicLock;
+
 		TASK_BEGIN;
 		if (_targetRoom == nullptr || _targetObject == nullptr)
 			return TaskReturn::finish(1);
 
-		// TODO: Fade out music on room change
+		musicLock = FakeLock(g_engine->sounds().musicSemaphore());
+		if (g_engine->sounds().musicID() != _targetRoom->musicID())
+			g_engine->sounds().fadeMusic();
 		TASK_WAIT(fade(process(), FadeType::ToBlack, 0, 1, 500, EasingType::Out, -5));
 		_player.changeRoom(_targetRoom->name(), true);
 
@@ -232,7 +236,9 @@ struct DoorTask : public Task {
 			g_engine->camera().setFollow(_character, true);
 		}
 
-		// TODO: Start music on room change
+		g_engine->sounds().setMusicToRoom(_targetRoom->musicID());
+		musicLock.release();
+
 		if (g_engine->script().createProcess(_character->kind(), "ENTRAR_" + _targetRoom->name(), ScriptFlags::AllowMissing))
 			TASK_YIELD;
 		else
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 96ee8d78744..3d4d9bad547 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -87,7 +87,7 @@ static ObjectBase *readRoomObject(Room *room, const String &type, ReadStream &st
 Room::Room(World *world, SeekableReadStream &stream, bool hasUselessByte)
 	: _world(world) {
 	_name = readVarString(stream);
-	_musicId = stream.readSByte();
+	_musicId = (int)stream.readByte();
 	_characterAlphaTint = stream.readByte();
 	auto backgroundScale = stream.readSint16LE();
 	_floors[0] = PathFindingShape(stream);
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index 976ad66fd28..a35b2293a9c 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -48,6 +48,7 @@ public:
 	inline uint8 characterAlphaTint() const { return _characterAlphaTint; }
 	inline uint8 characterAlphaPremultiplier() const { return _characterAlphaPremultiplier; }
 	inline bool fixedCameraOnEntering() const { return _fixedCameraOnEntering; }
+	inline int musicID() const { return _musicId; }
 
 	using ObjectIterator = Common::Array<ObjectBase *>::const_iterator;
 	inline ObjectIterator beginObjects() const { return _objects.begin(); }
@@ -77,9 +78,8 @@ protected:
 	Common::String _name;
 	PathFindingShape _floors[2];
 	bool _fixedCameraOnEntering;
-	int8
-		_musicId,
-		_activeFloorI = -1;
+	int8 _activeFloorI = -1;
+	int _musicId = -1;
 	uint8
 		_characterAlphaTint,
 		_characterAlphaPremultiplier; ///< for some reason in percent instead of 0-255
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 67617c661f4..52cc49bb07c 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -486,10 +486,12 @@ private:
 				: TaskReturn::finish(1);
 		}
 		case ScriptKernelTask::PlayMusic:
-			warning("STUB KERNEL CALL: PlayMusic");
+			if (process().isActiveForPlayer())
+				g_engine->sounds().startMusic((int)getNumberArg(0));
 			return TaskReturn::finish(0);
 		case ScriptKernelTask::StopMusic:
-			warning("STUB KERNEL CALL: StopMusic");
+			if (process().isActiveForPlayer())
+				g_engine->sounds().fadeMusic();
 			return TaskReturn::finish(0);
 		case ScriptKernelTask::WaitForMusicToEnd:
 			warning("STUB KERNEL CALL: WaitForMusicToEnd");
@@ -549,7 +551,7 @@ private:
 						g_engine->world().inventory().open();
 					else
 						g_engine->player().changeRoom(targetRoom->name(), true);
-					// TODO: Change music on kernel change room
+					g_engine->sounds().setMusicToRoom(targetRoom->musicID());
 				}
 				g_engine->script().createProcess(process().character(), "ENTRAR_" + targetRoom->name(), ScriptFlags::AllowMissing);
 			}
diff --git a/engines/alcachofa/sounds.cpp b/engines/alcachofa/sounds.cpp
index 433eadf9326..90e5ae56b31 100644
--- a/engines/alcachofa/sounds.cpp
+++ b/engines/alcachofa/sounds.cpp
@@ -22,6 +22,7 @@
 #include "sounds.h"
 #include "rooms.h"
 #include "alcachofa.h"
+#include "detection.h"
 
 #include "common/file.h"
 #include "common/substream.h"
@@ -49,24 +50,19 @@ Sounds::~Sounds() {
 }
 
 Sounds::Playback *Sounds::getPlaybackById(SoundID id) {
-	if (_playbacks.empty())
-		return nullptr;
-	uint first = 0, last = _playbacks.size() - 1;
-	while (first < last) {
-		uint mid = (first + last) / 2;
-		if (_playbacks[mid]._id == id)
-			return _playbacks.data() + mid;
-		else if (_playbacks[mid]._id < id)
-			first = mid + 1;
-		else
-			last = mid == 0 ? 0 : mid - 1;
-	}
-	return first == last && first < _playbacks.size()
-		? _playbacks.data() + first
-		: nullptr;
+	auto itPlayback = find_if(_playbacks.begin(), _playbacks.end(),
+		[&] (const Playback &playback) { return playback._id == id; });
+	return itPlayback == _playbacks.end() ? nullptr : itPlayback;
 }
 
 void Sounds::update() {
+	if (_isMusicPlaying && !isAlive(_musicSoundID)) {
+		if (_nextMusicID < 0)
+			fadeMusic();
+		else
+			startMusic(_nextMusicID);
+	}
+
 	for (uint i = _playbacks.size(); i > 0; i--) {
 		Playback &playback = _playbacks[i - 1];
 		if (!_mixer->isSoundHandleActive(playback._handle))
@@ -115,12 +111,12 @@ static AudioStream *loadSND(File *file) {
 	}
 }
 
-static AudioStream *openAudio(const String &fileName) {
-	String path = String::format("Sonidos/%s.SND", fileName.c_str());
+static AudioStream *openAudio(const char *fileName) {
+	String path = String::format("Sonidos/%s.SND", fileName);
 	File *file = new File();
 	if (file->open(path.c_str()))
-		return file->size() == 0
-			? makeSilentAudioStream(8000, false) // Movie Adventure has some null-size audio files, they are treated like infinite samples
+		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);
@@ -133,7 +129,7 @@ static AudioStream *openAudio(const String &fileName) {
 	return nullptr;
 }
 
-SoundID Sounds::playSoundInternal(const String &fileName, byte volume, Mixer::SoundType type) {
+SoundID Sounds::playSoundInternal(const char *fileName, byte volume, Mixer::SoundType type) {
 	AudioStream *stream = openAudio(fileName);
 	if (stream == nullptr)
 		return UINT32_MAX;
@@ -192,14 +188,23 @@ SoundID Sounds::playSoundInternal(const String &fileName, byte volume, Mixer::So
 }
 
 SoundID Sounds::playVoice(const String &fileName, byte volume) {
-	return playSoundInternal(fileName, volume, Mixer::kSpeechSoundType);
+	debugC(1, kDebugSounds, "Play voice: %s at %d", fileName.c_str(), (int)volume);
+	return playSoundInternal(fileName.c_str(), volume, Mixer::kSpeechSoundType);
 }
 
 SoundID Sounds::playSFX(const String &fileName, byte volume) {
-	return playSoundInternal(fileName, volume, Mixer::kSFXSoundType);
+	debugC(1, kDebugSounds, "Play SFX: %s at %d", fileName.c_str(), (int)volume);
+	return playSoundInternal(fileName.c_str(), volume, Mixer::kSFXSoundType);
+}
+
+void Sounds::stopAll() {
+	debugC(1, kDebugSounds, "Stop all sounds");
+	_mixer->stopAll();
+	_playbacks.clear();
 }
 
 void Sounds::stopVoice() {
+	debugC(1, kDebugSounds, "Stop all voices");
 	for (uint i = _playbacks.size(); i > 0; i--) {
 		if (_playbacks[i - 1]._type == Mixer::kSpeechSoundType) {
 			_mixer->stopHandle(_playbacks[i - 1]._handle);
@@ -282,6 +287,49 @@ bool Sounds::isNoisy(SoundID id, float windowSize, float minDifferences) {
 	return sumOfDifferences / 256.0f >= minDifferences;
 }
 
+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);
+	_isMusicPlaying = true;
+	_nextMusicID = musicId;
+}
+
+void Sounds::queueMusic(int musicId) {
+	debugC(2, kDebugSounds, "queueMusic %d", musicId);
+	_nextMusicID = musicId;
+}
+
+void Sounds::fadeMusic(uint32 duration) {
+	debugC(2, kDebugSounds, "fadeMusic");
+	fadeOut(_musicSoundID, duration);
+	_isMusicPlaying = false;
+	_nextMusicID = -1;
+	_musicSoundID = {};
+}
+
+void Sounds::setMusicToRoom(int roomMusicId) {
+	// Alcachofa Soft used IDs > 200 to mean "no change in music"
+	if (roomMusicId == _nextMusicID || roomMusicId > 200) {
+		debugC(1, kDebugSounds, "setMusicToRoom: from %d to %d, not executed", _nextMusicID, roomMusicId);
+		return;
+	}
+	debugC(1, kDebugSounds, "setMusicToRoom: from %d to %d", _nextMusicID, roomMusicId);
+	if (roomMusicId > 0)
+		startMusic(roomMusicId);
+	else
+		fadeMusic();
+}
+
+Task *Sounds::waitForMusicToEnd(Process &process) {
+	FakeLock lock(_musicSemaphore);
+	return new WaitForMusicTask(process, std::move(lock));
+}
+
 PlaySoundTask::PlaySoundTask(Process &process, SoundID soundID)
 	: Task(process)
 	, _soundID(soundID) {
@@ -302,4 +350,19 @@ void PlaySoundTask::debugPrint() {
 	g_engine->console().debugPrintf("PlaySound %u\n", _soundID);
 }
 
+WaitForMusicTask::WaitForMusicTask(Process &process, FakeLock &&lock)
+	: Task(process)
+	, _lock(std::move(lock)) {}
+
+TaskReturn WaitForMusicTask::run() {
+	g_engine->sounds().queueMusic(-1);
+	return g_engine->sounds().isMusicPlaying()
+		? TaskReturn::yield()
+		: TaskReturn::finish(0);
+}
+
+void WaitForMusicTask::debugPrint() {
+	g_engine->console().debugPrintf("WaitForMusic\n");
+}
+
 }
diff --git a/engines/alcachofa/sounds.h b/engines/alcachofa/sounds.h
index ca320fb427f..c34f1571d32 100644
--- a/engines/alcachofa/sounds.h
+++ b/engines/alcachofa/sounds.h
@@ -40,6 +40,7 @@ public:
 	void update();
 	SoundID playVoice(const Common::String &fileName, byte volume = Audio::Mixer::kMaxChannelVolume);
 	SoundID playSFX(const Common::String &fileName, byte volume = Audio::Mixer::kMaxChannelVolume);
+	void stopAll();
 	void stopVoice();
 	void fadeOut(SoundID id, uint32 duration);
 	void fadeOutVoiceAndSFX(uint32 duration);
@@ -50,6 +51,15 @@ public:
 		Character *speakingCharacter);
 	bool isNoisy(SoundID id, float windowSize, float minDifferences); ///< used for lip-sync
 
+	void startMusic(int musicId);
+	void queueMusic(int musicId);
+	void fadeMusic(uint32 duration = 500);
+	void setMusicToRoom(int roomMusicId);
+	Task *waitForMusicToEnd(Process &processd);
+	inline bool isMusicPlaying() const { return _isMusicPlaying; }
+	inline int musicID() const { return _nextMusicID; }
+	inline FakeSemaphore &musicSemaphore() { return _musicSemaphore; }
+
 private:
 	struct Playback {;
 		void fadeOut(uint32 duration);
@@ -63,11 +73,16 @@ private:
 		Common::Array<int16> _samples; ///< might not be filled, only voice samples are preloaded for lip-sync
 	};
 	Playback *getPlaybackById(SoundID id);
-	SoundID playSoundInternal(const Common::String &fileName, byte volume, Audio::Mixer::SoundType type);
+	SoundID playSoundInternal(const char *fileName, byte volume, Audio::Mixer::SoundType type);
 
 	Common::Array<Playback> _playbacks;
 	Audio::Mixer *_mixer;
 	SoundID _nextID = 1;
+
+	SoundID _musicSoundID = kInvalidSoundID; // we use another soundID to reuse fading
+	bool _isMusicPlaying = false;
+	int _nextMusicID = -1;
+	FakeSemaphore _musicSemaphore;
 };
 
 struct PlaySoundTask final : public Task {
@@ -78,6 +93,14 @@ private:
 	SoundID _soundID;
 };
 
+struct WaitForMusicTask final : public Task {
+	WaitForMusicTask(Process &process, FakeLock &&lock);
+	virtual TaskReturn run() override;
+	virtual void debugPrint() override;
+private:
+	FakeLock _lock;
+};
+
 }
 
 #endif // SOUNDS_H


Commit: 4ad1f1f806f429ddaddbc3e1e6ff5a5d1793c3af
    https://github.com/scummvm/scummvm/commit/4ad1f1f806f429ddaddbc3e1e6ff5a5d1793c3af
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:56+02:00

Commit Message:
ALCACHOFA: Add keymap and input handling to open menu

Changed paths:
    engines/alcachofa/global-ui.cpp
    engines/alcachofa/global-ui.h
    engines/alcachofa/input.cpp
    engines/alcachofa/input.h
    engines/alcachofa/metaengine.cpp
    engines/alcachofa/metaengine.h
    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 297a3598c54..7a3521addf4 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -247,4 +247,16 @@ void GlobalUI::drawScreenStates() {
 	}
 }
 
+void GlobalUI::updateOpeningMenu() {
+	if (_openMenuAtNextFrame) {
+		_openMenuAtNextFrame = false;
+		debug("Open menu");
+		// TODO: Actually open menu
+		return;
+	}
+	_openMenuAtNextFrame =
+		g_engine->input().wasMenuKeyPressed() &&
+		g_engine->player().isAllowedToOpenMenu();
+}
+
 }
diff --git a/engines/alcachofa/global-ui.h b/engines/alcachofa/global-ui.h
index 8892ebf3e27..19fb164a242 100644
--- a/engines/alcachofa/global-ui.h
+++ b/engines/alcachofa/global-ui.h
@@ -42,6 +42,7 @@ public:
 	bool updateOpeningInventory();
 	void updateClosingInventory();
 	void startClosingInventory();
+	void updateOpeningMenu();
 	void drawScreenStates(); // black borders and/or permanent fade
 
 private:
@@ -60,7 +61,8 @@ private:
 	bool
 		_isOpeningInventory = false,
 		_isClosingInventory = false,
-		_isPermanentFaded = false;
+		_isPermanentFaded = false,
+		_openMenuAtNextFrame = false;
 	uint32 _timeForInventory = 0;
 };
 
diff --git a/engines/alcachofa/input.cpp b/engines/alcachofa/input.cpp
index 0f72e747fc8..7d92310d54d 100644
--- a/engines/alcachofa/input.cpp
+++ b/engines/alcachofa/input.cpp
@@ -21,6 +21,7 @@
 
 #include "input.h"
 #include "alcachofa.h"
+#include "metaengine.h"
 
 using namespace Common;
 
@@ -34,6 +35,7 @@ void Input::nextFrame() {
 	_wasMouseRightPressed = false;
 	_wasMouseLeftReleased = false;
 	_wasMouseRightReleased = false;
+	_wasMenuKeyPressed = false;
 	updateMousePos3D(); // camera transformation might have changed
 }
 
@@ -67,6 +69,12 @@ bool Input::handleEvent(const Common::Event &event) {
 		_mousePos2D = event.mouse;
 		updateMousePos3D();
 		return true;
+	case EVENT_CUSTOM_ENGINE_ACTION_START:
+		switch ((InputAction)event.customType) {
+		case InputAction::Menu:
+			_wasMenuKeyPressed = true;
+			return true;
+		}
 	}
 	default:
 		return false;
diff --git a/engines/alcachofa/input.h b/engines/alcachofa/input.h
index e80287fd61e..770a0c213bd 100644
--- a/engines/alcachofa/input.h
+++ b/engines/alcachofa/input.h
@@ -38,6 +38,7 @@ public:
 	inline bool isMouseLeftDown() const { return _isMouseLeftDown; }
 	inline bool isMouseRightDown() const { return _isMouseRightDown; }
 	inline bool isAnyMouseDown() const { return _isMouseLeftDown || _isMouseRightDown; }
+	inline bool wasMenuKeyPressed() const { return _wasMenuKeyPressed; }
 	inline Common::Point mousePos2D() const { return _mousePos2D; }
 	inline Common::Point mousePos3D() const { return _mousePos3D; }
 	const Input &debugInput() const { scumm_assert(_debugInput != nullptr); return *_debugInput; }
@@ -55,7 +56,8 @@ private:
 		_wasMouseLeftReleased = false,
 		_wasMouseRightReleased = false,
 		_isMouseLeftDown = false,
-		_isMouseRightDown = false;
+		_isMouseRightDown = false,
+		_wasMenuKeyPressed = false;
 	Common::Point
 		_mousePos2D,
 		_mousePos3D;
diff --git a/engines/alcachofa/metaengine.cpp b/engines/alcachofa/metaengine.cpp
index a31dbd032d3..d392c26959b 100644
--- a/engines/alcachofa/metaengine.cpp
+++ b/engines/alcachofa/metaengine.cpp
@@ -20,10 +20,16 @@
  */
 
 #include "common/translation.h"
+#include "backends/keymapper/keymapper.h"
+#include "backends/keymapper/action.h"
+#include "backends/keymapper/standard-actions.h"
 
 #include "alcachofa/metaengine.h"
 #include "alcachofa/alcachofa.h"
 
+using namespace Common;
+using namespace Alcachofa;
+
 namespace Alcachofa {
 
 static const ADExtraGuiOptionsMap optionsList[] = {
@@ -51,9 +57,9 @@ const ADExtraGuiOptionsMap *AlcachofaMetaEngine::getAdvancedExtraGuiOptions() co
 	return Alcachofa::optionsList;
 }
 
-Common::Error AlcachofaMetaEngine::createInstance(OSystem *syst, Engine **engine, const ADGameDescription *desc) const {
+Error AlcachofaMetaEngine::createInstance(OSystem *syst, Engine **engine, const ADGameDescription *desc) const {
 	*engine = new Alcachofa::AlcachofaEngine(syst, desc);
-	return Common::kNoError;
+	return kNoError;
 }
 
 bool AlcachofaMetaEngine::hasFeature(MetaEngineFeature f) const {
@@ -61,6 +67,32 @@ bool AlcachofaMetaEngine::hasFeature(MetaEngineFeature f) const {
 		(f == kSupportsLoadingDuringStartup);
 }
 
+KeymapArray AlcachofaMetaEngine::initKeymaps(const char *target) const {
+	Keymap *keymap = new Keymap(Keymap::kKeymapTypeGame, "alcachofa-default", _("Default keymappings"));
+
+	Action *act;
+
+	act = new Action(kStandardActionLeftClick, _("Activate"));
+	act->setLeftClickEvent();
+	act->addDefaultInputMapping("MOUSE_LEFT");
+	act->addDefaultInputMapping("JOY_A");
+	keymap->addAction(act);
+
+	act = new Action(kStandardActionRightClick, _("Look at"));
+	act->setRightClickEvent();
+	act->addDefaultInputMapping("MOUSE_RIGHT");
+	act->addDefaultInputMapping("JOY_B");
+	keymap->addAction(act);
+
+	act = new Action("MENU", _("Menu"));
+	act->setCustomEngineActionEvent((CustomEventType)InputAction::Menu);
+	act->addDefaultInputMapping("ESCAPE");
+	act->addDefaultInputMapping("JOY_START");
+	keymap->addAction(act);
+
+	return Keymap::arrayOf(keymap);
+}
+
 #if PLUGIN_ENABLED_DYNAMIC(ALCACHOFA)
 REGISTER_PLUGIN_DYNAMIC(ALCACHOFA, PLUGIN_TYPE_ENGINE, AlcachofaMetaEngine);
 #else
diff --git a/engines/alcachofa/metaengine.h b/engines/alcachofa/metaengine.h
index e50800e10ca..58d9f56607f 100644
--- a/engines/alcachofa/metaengine.h
+++ b/engines/alcachofa/metaengine.h
@@ -24,6 +24,14 @@
 
 #include "engines/advancedDetector.h"
 
+namespace Alcachofa {
+
+enum class InputAction {
+	Menu
+};
+
+}
+
 class AlcachofaMetaEngine : public AdvancedMetaEngine<ADGameDescription> {
 public:
 	const char *getName() const override;
@@ -38,6 +46,10 @@ public:
 	bool hasFeature(MetaEngineFeature f) const override;
 
 	const ADExtraGuiOptionsMap *getAdvancedExtraGuiOptions() const override;
+
+	Common::KeymapArray initKeymaps(const char *target) const override;
+
+	
 };
 
 #endif // ALCACHOFA_METAENGINE_H
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index afd09b36646..bb3201504ce 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -295,4 +295,12 @@ void Player::setActiveCharacter(MainCharacterKind kind) {
 	_activeCharacter = &g_engine->world().getMainCharacterByKind(kind);
 }
 
+bool Player::isAllowedToOpenMenu() {
+	return
+		isGameLoaded() &&
+		!isOptionsMenuOpen() &&
+		g_engine->sounds().musicSemaphore().isReleased() &&
+		!g_engine->script().variable("prohibirESC");
+}
+
 }
diff --git a/engines/alcachofa/player.h b/engines/alcachofa/player.h
index 638f62bb207..5de1eaeed63 100644
--- a/engines/alcachofa/player.h
+++ b/engines/alcachofa/player.h
@@ -58,6 +58,7 @@ public:
 	void addLastDialogCharacter(Character *character);
 	void stopLastDialogCharacters();
 	void setActiveCharacter(MainCharacterKind kind);
+	bool isAllowedToOpenMenu();
 
 private:
 	static constexpr const int kMaxLastDialogCharacters = 4;
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 3d4d9bad547..80910a5f47c 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -197,7 +197,7 @@ bool Room::updateInput() {
 
 	if (player.currentRoom() == this) {
 		g_engine->globalUI().drawChangingButton();
-		// TODO: Add main menu handling
+		g_engine->globalUI().updateOpeningMenu();
 	}
 
 	return player.currentRoom() == this;


Commit: 446ca8765ba2ae8b8f3d14244024b086599a291d
    https://github.com/scummvm/scummvm/commit/446ca8765ba2ae8b8f3d14244024b086599a291d
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:56+02:00

Commit Message:
ALCACHOFA: Open main menu and add MenuButtton

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


diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index 7a3521addf4..5b9055b89d8 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -78,7 +78,7 @@ void GlobalUI::updateClosingInventory() {
 
 bool GlobalUI::updateOpeningInventory() {
 	static constexpr float kSpeed = 10 / 3.0f / 1000.0f;
-	if (g_engine->player().isOptionsMenuOpen() || !g_engine->player().isGameLoaded())
+	if (g_engine->player().isMenuOpen() || !g_engine->player().isGameLoaded())
 		return false;
 
 	if (_isOpeningInventory) {
@@ -123,7 +123,7 @@ bool GlobalUI::isHoveringChangeButton() const {
 
 bool GlobalUI::updateChangingCharacter() {
 	auto &player = g_engine->player();
-	if (player.isOptionsMenuOpen() ||
+	if (player.isMenuOpen() ||
 		!player.isGameLoaded() ||
 		_isOpeningInventory)
 		return false;
@@ -160,7 +160,7 @@ bool GlobalUI::updateChangingCharacter() {
 
 void GlobalUI::drawChangingButton() {
 	auto &player = g_engine->player();
-	if (player.isOptionsMenuOpen() ||
+	if (player.isMenuOpen() ||
 		!player.isGameLoaded() ||
 		!player.semaphore().isReleased() ||
 		_isOpeningInventory ||
@@ -233,7 +233,7 @@ Task *showCenterBottomText(Process &process, int32 dialogId, uint32 durationMs)
 }
 
 void GlobalUI::drawScreenStates() {
-	if (g_engine->player().isOptionsMenuOpen())
+	if (g_engine->player().isMenuOpen())
 		return;
 
 	auto &drawQueue = g_engine->drawQueue();
@@ -248,15 +248,28 @@ void GlobalUI::drawScreenStates() {
 }
 
 void GlobalUI::updateOpeningMenu() {
-	if (_openMenuAtNextFrame) {
-		_openMenuAtNextFrame = false;
-		debug("Open menu");
-		// TODO: Actually open menu
+	if (!_openMenuAtNextFrame) {
+		_openMenuAtNextFrame =
+			g_engine->input().wasMenuKeyPressed() &&
+			g_engine->player().isAllowedToOpenMenu();
 		return;
 	}
-	_openMenuAtNextFrame =
-		g_engine->input().wasMenuKeyPressed() &&
-		g_engine->player().isAllowedToOpenMenu();
+	_openMenuAtNextFrame = false;
+
+	g_engine->sounds().pauseAll(true);
+	// TODO: Add game time behaviour on opening menu
+	g_engine->player().isMenuOpen() = true;
+	// TODO: Render thumbnail
+	g_engine->player().changeRoom("MENUPRINCIPAL", true);
+	// TODO: Check original read lastSaveFileFileId and read options.cfg, we do not need that right?
+
+	g_engine->player().heldItem() = nullptr;
+	g_engine->scheduler().backupContext();
+	g_engine->camera().backup(1);
+	g_engine->camera().setPosition(Math::Vector3d(
+		g_system->getWidth() / 2.0f, g_system->getHeight() / 2.0f, 0.0f));
+
+	// TODO: Load thumbnail into capture graphic object
 }
 
 }
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 21bf8d042e0..7adb67c313b 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -162,9 +162,24 @@ public:
 	virtual ~MenuButton() override = default;
 
 	inline int32 actionId() const { return _actionId; }
+	inline bool &isInteractable() { return _isInteractable; }
+
+	virtual void draw() override;
+	virtual void update() override;
+	virtual void loadResources() override;
+	virtual void freeResources() override;
+	virtual void onHoverStart();
+	virtual void onHoverEnd();
+	virtual void onClick() override;
 	virtual const char *typeName() const;
+	virtual void trigger();
 
 private:
+	bool
+		_isInteractable = true,
+		_isClicked = false,
+		_isHovered = false,
+		_triggerNextFrame = false;
 	int32 _actionId;
 	Graphic
 		_graphicNormal,
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index bb3201504ce..b87085c4834 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -49,7 +49,7 @@ void Player::resetCursor() {
 }
 
 void Player::updateCursor() {
-	if (_isOptionsMenuOpen || !_isGameLoaded)
+	if (_isMenuOpen || !_isGameLoaded)
 		_cursorFrameI = 0;
 	else if (_selectedObject == nullptr)
 		_cursorFrameI = !g_engine->input().isMouseLeftDown() || _pressedObject != nullptr ? 6 : 7;
@@ -298,7 +298,7 @@ void Player::setActiveCharacter(MainCharacterKind kind) {
 bool Player::isAllowedToOpenMenu() {
 	return
 		isGameLoaded() &&
-		!isOptionsMenuOpen() &&
+		!isMenuOpen() &&
 		g_engine->sounds().musicSemaphore().isReleased() &&
 		!g_engine->script().variable("prohibirESC");
 }
diff --git a/engines/alcachofa/player.h b/engines/alcachofa/player.h
index 5de1eaeed63..52f41016eb7 100644
--- a/engines/alcachofa/player.h
+++ b/engines/alcachofa/player.h
@@ -39,7 +39,7 @@ public:
 	MainCharacter *inactiveCharacter() const;
 	FakeSemaphore &semaphoreFor(MainCharacterKind kind);
 
-	inline bool &isOptionsMenuOpen() { return _isOptionsMenuOpen; }
+	inline bool &isMenuOpen() { return _isMenuOpen; }
 	inline bool &isGameLoaded() { return _isGameLoaded; }
 
 	inline MainCharacterKind activeCharacterKind() const {
@@ -73,7 +73,7 @@ private:
 	Item *_heldItem = nullptr;
 	int32 _cursorFrameI = 0;
 	bool
-		_isOptionsMenuOpen = false,
+		_isMenuOpen = false,
 		_isGameLoaded = true,
 		_didLoadGlobalRooms = false;
 	Character *_lastDialogCharacters[kMaxLastDialogCharacters] = { nullptr };
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 80910a5f47c..0159dc58cbb 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -149,7 +149,7 @@ void Room::update() {
 		if (!updateInput())
 			return;
 	}
-	if (!g_engine->player().isOptionsMenuOpen() &&
+	if (!g_engine->player().isMenuOpen() &&
 		g_engine->player().currentRoom() != &g_engine->world().inventory())
 		world().globalRoom().updateObjects();
 	if (g_engine->player().currentRoom() == this)
@@ -183,7 +183,7 @@ bool Room::updateInput() {
 
 	bool canInteract = !player.activeCharacter()->isBusy();
 	// A complicated network condition can prevent interaction at this point
-	if (player.isOptionsMenuOpen() || !player.isGameLoaded())
+	if (player.isMenuOpen() || !player.isGameLoaded())
 		canInteract = true;
 	if (canInteract) {
 		player.resetCursor();
diff --git a/engines/alcachofa/sounds.cpp b/engines/alcachofa/sounds.cpp
index 90e5ae56b31..2646659c287 100644
--- a/engines/alcachofa/sounds.cpp
+++ b/engines/alcachofa/sounds.cpp
@@ -213,6 +213,10 @@ void Sounds::stopVoice() {
 	}
 }
 
+void Sounds::pauseAll(bool paused) {
+	_mixer->pauseAll(paused);
+}
+
 bool Sounds::isAlive(SoundID id) {
 	Playback *playback = getPlaybackById(id);
 	return playback != nullptr && _mixer->isSoundHandleActive(playback->_handle);
diff --git a/engines/alcachofa/sounds.h b/engines/alcachofa/sounds.h
index c34f1571d32..0cdf8250405 100644
--- a/engines/alcachofa/sounds.h
+++ b/engines/alcachofa/sounds.h
@@ -42,6 +42,7 @@ public:
 	SoundID playSFX(const Common::String &fileName, byte volume = Audio::Mixer::kMaxChannelVolume);
 	void stopAll();
 	void stopVoice();
+	void pauseAll(bool paused);
 	void fadeOut(SoundID id, uint32 duration);
 	void fadeOutVoiceAndSFX(uint32 duration);
 	bool isAlive(SoundID id);
diff --git a/engines/alcachofa/ui-objects.cpp b/engines/alcachofa/ui-objects.cpp
index 926d960d96b..cc12494e87a 100644
--- a/engines/alcachofa/ui-objects.cpp
+++ b/engines/alcachofa/ui-objects.cpp
@@ -19,6 +19,7 @@
  *
  */
 
+#include "alcachofa.h"
 #include "objects.h"
 #include "rooms.h"
 
@@ -37,6 +38,75 @@ MenuButton::MenuButton(Room *room, ReadStream &stream)
 	, _graphicDisabled(stream) {
 }
 
+void MenuButton::draw() {
+	if (!isEnabled())
+		return;
+	Graphic &graphic =
+		!_isInteractable ? _graphicDisabled
+		: _isClicked ? _graphicClicked
+		: _isHovered ? _graphicHovered
+		: _graphicNormal;
+	graphic.update();
+	g_engine->drawQueue().add<AnimationDrawRequest>(graphic, true, BlendMode::AdditiveAlpha);
+}
+
+void MenuButton::update() {
+	PhysicalObject::update();
+	if (!_isClicked)
+		return;
+
+	_graphicClicked.update();
+	if (!_graphicClicked.isPaused())
+		return;
+
+	if (!_triggerNextFrame) {
+		// another delay probably to show the last frame of animation
+		_triggerNextFrame = true;
+		return;
+	}
+
+	_triggerNextFrame = false;
+	_isClicked = false;
+	trigger();
+}
+
+void MenuButton::loadResources() {
+	_graphicNormal.loadResources();
+	_graphicHovered.loadResources();
+	_graphicClicked.loadResources();
+	_graphicDisabled.loadResources();
+}
+
+void MenuButton::freeResources() {
+	_graphicNormal.freeResources();
+	_graphicHovered.freeResources();
+	_graphicClicked.freeResources();
+	_graphicDisabled.freeResources();
+}
+
+void MenuButton::onHoverStart() {
+	PhysicalObject::onHoverStart();
+	_isHovered = true;
+}
+
+void MenuButton::onHoverEnd() {
+	PhysicalObject::onHoverEnd();
+	_isHovered = false;
+}
+
+void MenuButton::onClick() {
+	if (_isInteractable) {
+		_isClicked = true;
+		_triggerNextFrame = false;
+		_graphicClicked.start(false);
+	}
+}
+
+void MenuButton::trigger() {
+	// all menu buttons should be inherited and override trigger
+	warning("Unimplemented %s %s action %d", typeName(), name().c_str(), _actionId);
+}
+
 const char *InternetMenuButton::typeName() const { return "InternetMenuButton"; }
 
 InternetMenuButton::InternetMenuButton(Room *room, ReadStream &stream)


Commit: 65a7d3154150491db72743129aeba866136eb224
    https://github.com/scummvm/scummvm/commit/65a7d3154150491db72743129aeba866136eb224
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:56+02:00

Commit Message:
ALCACHOFA: Add first main menu actions
 - ContinueGame
 - InternetMenu
 - Exit
 - NewGame

Changed paths:
  A engines/alcachofa/menu.cpp
  A engines/alcachofa/menu.h
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/alcachofa.h
    engines/alcachofa/game-movie-adventure.cpp
    engines/alcachofa/global-ui.cpp
    engines/alcachofa/global-ui.h
    engines/alcachofa/graphics-opengl.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/module.mk
    engines/alcachofa/objects.h
    engines/alcachofa/player.cpp
    engines/alcachofa/player.h
    engines/alcachofa/rooms.cpp
    engines/alcachofa/ui-objects.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index b9437cac40c..3dbb24b126c 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -32,9 +32,11 @@
 #include "alcachofa.h"
 #include "console.h"
 #include "detection.h"
+#include "player.h"
 #include "rooms.h"
 #include "script.h"
 #include "global-ui.h"
+#include "menu.h"
 #include "debug.h"
 #include "game.h"
 
@@ -42,6 +44,8 @@ using namespace Math;
 
 namespace Alcachofa {
 
+constexpr uint kDefaultFramerate = 100; // the original target framerate, not critical
+
 AlcachofaEngine *g_engine;
 
 AlcachofaEngine::AlcachofaEngine(OSystem *syst, const ADGameDescription *gameDesc)
@@ -72,12 +76,13 @@ Common::Error AlcachofaEngine::run() {
 	_script.reset(new Script());
 	_player.reset(new Player());
 	_globalUI.reset(new GlobalUI());
+	_menu.reset(new Menu());
 
 	_script->createProcess(MainCharacterKind::None, "CREDITOS_INICIALES");
 	_scheduler.run();
 
 	Common::Event e;
-	Graphics::FrameLimiter limiter(g_system, 120);
+	Graphics::FrameLimiter limiter(g_system, kDefaultFramerate, false);
 	while (!shouldQuit()) {
 		_input.nextFrame();
 		while (g_system->getEventManager()->pollEvent(e)) {
@@ -135,7 +140,7 @@ void AlcachofaEngine::playVideo(int32 videoId) {
 			if (_input.handleEvent(e))
 				continue;
 		}
-		if (_input.wasAnyMouseReleased())
+		if (_input.wasAnyMouseReleased() || _input.wasMenuKeyPressed())
 			break;
 
 		g_system->updateScreen();
@@ -144,6 +149,36 @@ void AlcachofaEngine::playVideo(int32 videoId) {
 	decoder.stop();
 }
 
+void AlcachofaEngine::fadeExit() {
+	constexpr uint kFadeOutDuration = 1000;
+	Event e;
+	Graphics::FrameLimiter limiter(g_system, kDefaultFramerate, false);
+	uint32 startTime = g_system->getMillis();
+
+	_renderer->end(); // we were in a frame, let's exit
+	while (g_system->getMillis() - startTime < kFadeOutDuration && !shouldQuit()) {
+		_input.nextFrame();
+		while (g_system->getEventManager()->pollEvent(e)) {
+			if (_input.handleEvent(e))
+				continue;
+		}
+
+		_renderer->begin();
+		_drawQueue->clear();
+		float t = ((float)(g_system->getMillis() - startTime)) / kFadeOutDuration;
+		// TODO: Implement cross-fade and add to fadeExit
+		_drawQueue->add<FadeDrawRequest>(FadeType::ToBlack, t, -kForegroundOrderCount);
+		_drawQueue->draw();
+		_renderer->end();
+
+		limiter.delayBeforeSwap();
+		limiter.startFrame();
+	}
+
+	quitGame();
+	player().changeRoom("SALIR", false); // this skips some update steps along the way
+}
+
 void AlcachofaEngine::setDebugMode(DebugMode mode, int32 param) {
 	switch (mode)
 	{
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index 5d6ceb64cbd..d01dd27d106 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -51,6 +51,7 @@ class DrawQueue;
 class World;
 class Script;
 class GlobalUI;
+class Menu;
 class Game;
 struct AlcachofaGameDescription;
 
@@ -74,12 +75,14 @@ public:
 	inline World &world() { return *_world; }
 	inline Script &script() { return *_script; }
 	inline GlobalUI &globalUI() { return *_globalUI; }
+	inline Menu &menu() { return *_menu; }
 	inline Scheduler &scheduler() { return _scheduler; }
 	inline Console &console() { return *_console; }
 	inline Game &game() { return *_game; }
 	inline bool isDebugModeActive() const { return _debugHandler != nullptr; }
 
 	void playVideo(int32 videoId);
+	void fadeExit();
 	void setDebugMode(DebugMode debugMode, int32 param);
 
 	uint32 getFeatures() const;
@@ -134,6 +137,7 @@ private:
 	Common::ScopedPtr<Script> _script;
 	Common::ScopedPtr<Player> _player;
 	Common::ScopedPtr<GlobalUI> _globalUI;
+	Common::ScopedPtr<Menu> _menu;
 	Common::ScopedPtr<Game> _game;
 	Camera _camera;
 	Input _input;
diff --git a/engines/alcachofa/game-movie-adventure.cpp b/engines/alcachofa/game-movie-adventure.cpp
index 281a45adf44..2c5225a6219 100644
--- a/engines/alcachofa/game-movie-adventure.cpp
+++ b/engines/alcachofa/game-movie-adventure.cpp
@@ -51,6 +51,7 @@ class GameMovieAdventure : public Game {
 	}
 
 	virtual bool shouldTriggerDoor(const Door *door) {
+		// An invalid door target, the character will go to the door and then ignore it (also in original engine)
 		if (door->targetRoom() == "LABERINTO" && door->targetObject() == "a_LABERINTO_desde_LABERINTO_2")
 			return false;
 		return Game::shouldTriggerDoor(door);
diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index 5b9055b89d8..9cf45228071 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -20,6 +20,7 @@
  */
 
 #include "global-ui.h"
+#include "menu.h"
 #include "alcachofa.h"
 #include "script.h"
 
@@ -78,7 +79,7 @@ void GlobalUI::updateClosingInventory() {
 
 bool GlobalUI::updateOpeningInventory() {
 	static constexpr float kSpeed = 10 / 3.0f / 1000.0f;
-	if (g_engine->player().isMenuOpen() || !g_engine->player().isGameLoaded())
+	if (g_engine->menu().isOpen() || !g_engine->player().isGameLoaded())
 		return false;
 
 	if (_isOpeningInventory) {
@@ -123,7 +124,7 @@ bool GlobalUI::isHoveringChangeButton() const {
 
 bool GlobalUI::updateChangingCharacter() {
 	auto &player = g_engine->player();
-	if (player.isMenuOpen() ||
+	if (g_engine->menu().isOpen() ||
 		!player.isGameLoaded() ||
 		_isOpeningInventory)
 		return false;
@@ -160,7 +161,7 @@ bool GlobalUI::updateChangingCharacter() {
 
 void GlobalUI::drawChangingButton() {
 	auto &player = g_engine->player();
-	if (player.isMenuOpen() ||
+	if (g_engine->menu().isOpen() ||
 		!player.isGameLoaded() ||
 		!player.semaphore().isReleased() ||
 		_isOpeningInventory ||
@@ -233,7 +234,7 @@ Task *showCenterBottomText(Process &process, int32 dialogId, uint32 durationMs)
 }
 
 void GlobalUI::drawScreenStates() {
-	if (g_engine->player().isMenuOpen())
+	if (g_engine->menu().isOpen())
 		return;
 
 	auto &drawQueue = g_engine->drawQueue();
@@ -247,29 +248,4 @@ void GlobalUI::drawScreenStates() {
 	}
 }
 
-void GlobalUI::updateOpeningMenu() {
-	if (!_openMenuAtNextFrame) {
-		_openMenuAtNextFrame =
-			g_engine->input().wasMenuKeyPressed() &&
-			g_engine->player().isAllowedToOpenMenu();
-		return;
-	}
-	_openMenuAtNextFrame = false;
-
-	g_engine->sounds().pauseAll(true);
-	// TODO: Add game time behaviour on opening menu
-	g_engine->player().isMenuOpen() = true;
-	// TODO: Render thumbnail
-	g_engine->player().changeRoom("MENUPRINCIPAL", true);
-	// TODO: Check original read lastSaveFileFileId and read options.cfg, we do not need that right?
-
-	g_engine->player().heldItem() = nullptr;
-	g_engine->scheduler().backupContext();
-	g_engine->camera().backup(1);
-	g_engine->camera().setPosition(Math::Vector3d(
-		g_system->getWidth() / 2.0f, g_system->getHeight() / 2.0f, 0.0f));
-
-	// TODO: Load thumbnail into capture graphic object
-}
-
 }
diff --git a/engines/alcachofa/global-ui.h b/engines/alcachofa/global-ui.h
index 19fb164a242..8892ebf3e27 100644
--- a/engines/alcachofa/global-ui.h
+++ b/engines/alcachofa/global-ui.h
@@ -42,7 +42,6 @@ public:
 	bool updateOpeningInventory();
 	void updateClosingInventory();
 	void startClosingInventory();
-	void updateOpeningMenu();
 	void drawScreenStates(); // black borders and/or permanent fade
 
 private:
@@ -61,8 +60,7 @@ private:
 	bool
 		_isOpeningInventory = false,
 		_isClosingInventory = false,
-		_isPermanentFaded = false,
-		_openMenuAtNextFrame = false;
+		_isPermanentFaded = false;
 	uint32 _timeForInventory = 0;
 };
 
diff --git a/engines/alcachofa/graphics-opengl.cpp b/engines/alcachofa/graphics-opengl.cpp
index bee04e2338b..91eebb10fa5 100644
--- a/engines/alcachofa/graphics-opengl.cpp
+++ b/engines/alcachofa/graphics-opengl.cpp
@@ -146,12 +146,11 @@ public:
 	virtual void begin() override {
 		GL_CALL(glEnableClientState(GL_VERTEX_ARRAY));
 		GL_CALL(glDisableClientState(GL_INDEX_ARRAY));
+		GL_CALL(glDisableClientState(GL_TEXTURE_COORD_ARRAY));
 		_currentLodBias = -1000.0f;
 		_currentTexture = nullptr;
 		_currentBlendMode = (BlendMode)-1;
-
-		GL_CALL(glClearColor(0.0f, 0.0f, 0.0f, 1.0f));
-		GL_CALL(glClear(GL_COLOR_BUFFER_BIT));
+		_isFirstDrawCommand = true;
 	}
 
 	virtual void end() override {
@@ -292,6 +291,7 @@ public:
 			colors[2] *= colors[3];
 		}
 
+		checkFirstDrawCommand();
 		GL_CALL(glColor4fv(colors));
 		GL_CALL(glVertexPointer(2, GL_FLOAT, 0, positions));
 		if (_currentTexture != nullptr)
@@ -309,6 +309,7 @@ public:
 		Span<Vector2d> points,
 		Color color
 	) override {
+		checkFirstDrawCommand();
 		setTexture(nullptr);
 		setBlendMode(BlendMode::Alpha);
 		GL_CALL(glVertexPointer(2, GL_FLOAT, 0, points.data()));
@@ -334,6 +335,7 @@ public:
 		Span<Vector2d> points,
 		Color color
 	) override {
+		checkFirstDrawCommand();
 		setTexture(nullptr);
 		setBlendMode(BlendMode::Alpha);
 		GL_CALL(glVertexPointer(2, GL_FLOAT, 0, points.data()));
@@ -369,10 +371,21 @@ private:
 		GL_CALL(glLoadIdentity());
 	}
 
+	void checkFirstDrawCommand() {
+		// We delay clearing the screen. It is much easier for the game to switch to a
+		// framebuffer before 
+		if (!_isFirstDrawCommand)
+			return;
+		_isFirstDrawCommand = false;
+		GL_CALL(glClearColor(0.0f, 0.0f, 0.0f, 1.0f));
+		GL_CALL(glClear(GL_COLOR_BUFFER_BIT));
+	}
+
 	Point _resolution;
 	OpenGLTexture *_currentTexture = nullptr;
 	BlendMode _currentBlendMode = (BlendMode)-1;
 	float _currentLodBias = 0.0f;
+	bool _isFirstDrawCommand = false;
 };
 
 IRenderer *IRenderer::createOpenGLRenderer(Point resolution) {
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 9102d10868b..4da9cba4a89 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -845,6 +845,7 @@ DrawQueue::DrawQueue(IRenderer *renderer)
 }
 
 void DrawQueue::clear() {
+	_allocator.deallocateAll();
 	memset(_requestsPerOrderCount, 0, sizeof(_requestsPerOrderCount));
 	memset(_lodBiasPerOrder, 0, sizeof(_lodBiasPerOrder));
 }
diff --git a/engines/alcachofa/menu.cpp b/engines/alcachofa/menu.cpp
new file mode 100644
index 00000000000..8f7c6f63dd3
--- /dev/null
+++ b/engines/alcachofa/menu.cpp
@@ -0,0 +1,72 @@
+/* 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.h"
+#include "menu.h"
+#include "player.h"
+#include "script.h"
+
+namespace Alcachofa {
+
+void Menu::updateOpeningMenu() {
+	if (!_openAtNextFrame) {
+		_openAtNextFrame =
+			g_engine->input().wasMenuKeyPressed() &&
+			g_engine->player().isAllowedToOpenMenu();
+		return;
+	}
+	_openAtNextFrame = false;
+
+	g_engine->sounds().pauseAll(true);
+	// TODO: Add game time behaviour on opening menu
+	_previousRoom = g_engine->player().currentRoom();
+	_isOpen = true;
+	// TODO: Render thumbnail
+	g_engine->player().changeRoom("MENUPRINCIPAL", true);
+	// TODO: Check original read lastSaveFileFileId and read options.cfg, we do not need that right?
+
+	g_engine->player().heldItem() = nullptr;
+	g_engine->scheduler().backupContext();
+	g_engine->camera().backup(1);
+	g_engine->camera().setPosition(Math::Vector3d(
+		g_system->getWidth() / 2.0f, g_system->getHeight() / 2.0f, 0.0f));
+
+	// TODO: Load thumbnail into capture graphic object
+}
+
+void Menu::continueGame() {
+	assert(_previousRoom != nullptr);
+	_isOpen = false;
+	g_engine->input().nextFrame(); // presumably to clear all was* flags
+	g_engine->player().changeRoom(_previousRoom->name(), true);
+	g_engine->sounds().pauseAll(false);
+	g_engine->camera().restore(1);
+	g_engine->scheduler().restoreContext();
+	// TODO: Reset time on continueing game
+}
+
+void Menu::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());
+}
+
+}
diff --git a/engines/alcachofa/menu.h b/engines/alcachofa/menu.h
new file mode 100644
index 00000000000..2516257a30d
--- /dev/null
+++ b/engines/alcachofa/menu.h
@@ -0,0 +1,49 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MENU_H
+#define MENU_H
+
+#include "common/scummsys.h"
+
+namespace Alcachofa {
+
+class Room;
+
+class Menu {
+public:
+	inline bool isOpen() const { return _isOpen; }
+
+	void updateOpeningMenu();
+	void continueGame();
+	void newGame();
+
+private:
+	bool
+		_isOpen = false,
+		_openAtNextFrame = false;
+
+	Room *_previousRoom = nullptr;
+};
+
+}
+
+#endif // MENU_H
diff --git a/engines/alcachofa/module.mk b/engines/alcachofa/module.mk
index 5d1eec4b4f8..574ac87bd65 100644
--- a/engines/alcachofa/module.mk
+++ b/engines/alcachofa/module.mk
@@ -2,24 +2,25 @@ MODULE := engines/alcachofa
 
 MODULE_OBJS = \
 	alcachofa.o \
-	camera.cpp \
-	common.cpp \
+	camera.o \
+	common.o \
 	console.o \
-	game.cpp \
-	game-objects.cpp \
-	general-objects.cpp \
-	global-ui.cpp \
-	graphics.cpp \
-	graphics-opengl.cpp \
-	input.cpp \
+	game.o \
+	game-objects.o \
+	general-objects.o \
+	global-ui.o \
+	graphics.o \
+	graphics-opengl.o \
+	input.o \
+	menu.o \
 	metaengine.o \
-	player.cpp \
-	rooms.cpp \
-	scheduler.cpp \
-	script.cpp \
-	shape.cpp \
-	sounds.cpp \
-	ui-objects.cpp \
+	player.o \
+	rooms.o \
+	scheduler.o \
+	script.o \
+	shape.o \
+	sounds.o \
+	ui-objects.o
 
 
 # This module can be built as a plugin
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 7adb67c313b..c6739b0cc2b 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -171,8 +171,8 @@ public:
 	virtual void onHoverStart();
 	virtual void onHoverEnd();
 	virtual void onClick() override;
-	virtual const char *typeName() const;
 	virtual void trigger();
+	virtual const char *typeName() const;
 
 private:
 	bool
@@ -209,6 +209,8 @@ public:
 	static constexpr const char *kClassName = "CBotonMenuPrincipal";
 	MainMenuButton(Room *room, Common::ReadStream &stream);
 
+	virtual void update() override;
+	virtual void trigger() override;
 	virtual const char *typeName() const;
 };
 
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index b87085c4834..4ec1cddde44 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -22,6 +22,7 @@
 #include "player.h"
 #include "script.h"
 #include "alcachofa.h"
+#include "menu.h"
 
 using namespace Common;
 
@@ -49,7 +50,7 @@ void Player::resetCursor() {
 }
 
 void Player::updateCursor() {
-	if (_isMenuOpen || !_isGameLoaded)
+	if (g_engine->menu().isOpen() || !_isGameLoaded)
 		_cursorFrameI = 0;
 	else if (_selectedObject == nullptr)
 		_cursorFrameI = !g_engine->input().isMouseLeftDown() || _pressedObject != nullptr ? 6 : 7;
@@ -298,7 +299,7 @@ void Player::setActiveCharacter(MainCharacterKind kind) {
 bool Player::isAllowedToOpenMenu() {
 	return
 		isGameLoaded() &&
-		!isMenuOpen() &&
+		!g_engine->menu().isOpen() &&
 		g_engine->sounds().musicSemaphore().isReleased() &&
 		!g_engine->script().variable("prohibirESC");
 }
diff --git a/engines/alcachofa/player.h b/engines/alcachofa/player.h
index 52f41016eb7..2541406d5f1 100644
--- a/engines/alcachofa/player.h
+++ b/engines/alcachofa/player.h
@@ -39,7 +39,6 @@ public:
 	MainCharacter *inactiveCharacter() const;
 	FakeSemaphore &semaphoreFor(MainCharacterKind kind);
 
-	inline bool &isMenuOpen() { return _isMenuOpen; }
 	inline bool &isGameLoaded() { return _isGameLoaded; }
 
 	inline MainCharacterKind activeCharacterKind() const {
@@ -73,7 +72,6 @@ private:
 	Item *_heldItem = nullptr;
 	int32 _cursorFrameI = 0;
 	bool
-		_isMenuOpen = false,
 		_isGameLoaded = true,
 		_didLoadGlobalRooms = false;
 	Character *_lastDialogCharacters[kMaxLastDialogCharacters] = { nullptr };
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 0159dc58cbb..4dacd66371d 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -23,6 +23,7 @@
 #include "rooms.h"
 #include "script.h"
 #include "global-ui.h"
+#include "menu.h"
 
 #include "common/file.h"
 
@@ -149,7 +150,7 @@ void Room::update() {
 		if (!updateInput())
 			return;
 	}
-	if (!g_engine->player().isMenuOpen() &&
+	if (!g_engine->menu().isOpen() &&
 		g_engine->player().currentRoom() != &g_engine->world().inventory())
 		world().globalRoom().updateObjects();
 	if (g_engine->player().currentRoom() == this)
@@ -183,7 +184,7 @@ bool Room::updateInput() {
 
 	bool canInteract = !player.activeCharacter()->isBusy();
 	// A complicated network condition can prevent interaction at this point
-	if (player.isMenuOpen() || !player.isGameLoaded())
+	if (g_engine->menu().isOpen() || !player.isGameLoaded())
 		canInteract = true;
 	if (canInteract) {
 		player.resetCursor();
@@ -197,7 +198,7 @@ bool Room::updateInput() {
 
 	if (player.currentRoom() == this) {
 		g_engine->globalUI().drawChangingButton();
-		g_engine->globalUI().updateOpeningMenu();
+		g_engine->menu().updateOpeningMenu();
 	}
 
 	return player.currentRoom() == this;
diff --git a/engines/alcachofa/ui-objects.cpp b/engines/alcachofa/ui-objects.cpp
index cc12494e87a..33fd81e73f3 100644
--- a/engines/alcachofa/ui-objects.cpp
+++ b/engines/alcachofa/ui-objects.cpp
@@ -20,6 +20,9 @@
  */
 
 #include "alcachofa.h"
+#include "script.h"
+#include "global-ui.h"
+#include "menu.h"
 #include "objects.h"
 #include "rooms.h"
 
@@ -27,6 +30,19 @@ using namespace Common;
 
 namespace Alcachofa {
 
+enum class MainMenuAction : int32 {
+	ContinueGame = 0,
+	Save,
+	Load,
+	InternetMenu,
+	OptionsMenu,
+	Exit,
+	NextSave,
+	PrevSave,
+	NewGame,
+	AlsoExit // there seems to be no difference to Exit
+};
+
 const char *MenuButton::typeName() const { return "MenuButton"; }
 
 MenuButton::MenuButton(Room *room, ReadStream &stream)
@@ -125,6 +141,51 @@ MainMenuButton::MainMenuButton(Room *room, ReadStream &stream)
 	: MenuButton(room, stream) {
 }
 
+void MainMenuButton::update() {
+	MenuButton::update();
+	const auto action = (MainMenuAction)actionId();
+	if (g_engine->input().wasMenuKeyPressed() &&
+		(action == MainMenuAction::ContinueGame || action == MainMenuAction::NewGame))
+		onClick();
+}
+
+void MainMenuButton::trigger() {
+	switch ((MainMenuAction)actionId()) {
+	case MainMenuAction::ContinueGame:
+		g_engine->menu().continueGame();
+		break;
+	case MainMenuAction::Save:
+		warning("STUB: MainMenuAction Save");
+		break;
+	case MainMenuAction::Load:
+		warning("STUB: MainMenuAction Load");
+		break;
+	case MainMenuAction::InternetMenu:
+		g_system->messageBox(LogMessageType::kWarning, "Multiplayer is not implemented in this ScummVM version.");
+		break;
+	case MainMenuAction::OptionsMenu:
+		//g_engine->menu().openOptionsMenu();
+		break;
+	case MainMenuAction::Exit:
+	case MainMenuAction::AlsoExit:
+		// implemented in AlcachofaEngine as it has its own event loop
+		g_engine->fadeExit();
+		break;
+	case MainMenuAction::NextSave:
+		warning("STUB: MainMenuAction NextSave");
+		break;
+	case MainMenuAction::PrevSave:
+		warning("STUB: MainMenuAction PrevSave");
+		break;
+	case MainMenuAction::NewGame:
+		g_engine->menu().newGame();
+		break;
+	default:
+		warning("Unknown main menu action: %d", actionId());
+		break;
+	}
+}
+
 const char *PushButton::typeName() const { return "PushButton"; }
 
 PushButton::PushButton(Room *room, ReadStream &stream)


Commit: ee1808a5d1c86ad1ab2eb3f46bbda34b767ecf6e
    https://github.com/scummvm/scummvm/commit/ee1808a5d1c86ad1ab2eb3f46bbda34b767ecf6e
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:56+02:00

Commit Message:
ALCACHOFA: Add Config class and GUI options

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/alcachofa.h
    engines/alcachofa/detection.h
    engines/alcachofa/detection_tables.h
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/metaengine.cpp
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 3dbb24b126c..70a6e159e59 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -213,4 +213,27 @@ Common::Error AlcachofaEngine::syncGame(Common::Serializer &s) {
 	return Common::kNoError;
 }
 
+Config::Config() {
+	loadFromScummVM();
+}
+
+void Config::loadFromScummVM() {
+	_musicVolume = (uint8)CLIP(ConfMan.getInt("music_volume"), 0, 255);
+	_speechVolume = (uint8)CLIP(ConfMan.getInt("speech_volume"), 0, 255);
+	_subtitles = ConfMan.getBool("subtitles");
+	_highQuality = ConfMan.getBool("high_quality");
+	_bits32 = ConfMan.getBool("32_bits");
+}
+
+void Config::saveToScummVM() {
+	ConfMan.setBool("subtitles", _subtitles);
+	ConfMan.setBool("high_quality", _highQuality);
+	ConfMan.setBool("32_bits", _bits32);
+	ConfMan.setInt("music_volume", _musicVolume);
+	ConfMan.setInt("speech_volume", _speechVolume);
+	ConfMan.setInt("sfx_volume", _speechVolume);
+	// ^ a bit unfortunate, that means if you change in-game it overrides.
+	// if you set it in ScummVMs dialog it sticks
+}
+
 } // End of namespace Alcachofa
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index d01dd27d106..812ac68ee86 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -55,6 +55,29 @@ class Menu;
 class Game;
 struct AlcachofaGameDescription;
 
+class Config {
+public:
+	Config();
+
+	inline bool &subtitles() { return _subtitles; }
+	inline bool &highQuality() { return _highQuality; }
+	inline bool &bits32() { return _bits32; }
+	inline uint8 &musicVolume() { return _musicVolume; }
+	inline uint8 &speechVolume() { return _speechVolume; }
+
+	void loadFromScummVM();
+	void saveToScummVM();
+
+private:
+	bool
+		_subtitles = true,
+		_highQuality = true,
+		_bits32 = true;
+	uint8
+		_musicVolume = 255,
+		_speechVolume = 255;
+};
+
 class AlcachofaEngine : public Engine {
 private:
 	const ADGameDescription *_gameDescription;
@@ -79,6 +102,7 @@ public:
 	inline Scheduler &scheduler() { return _scheduler; }
 	inline Console &console() { return *_console; }
 	inline Game &game() { return *_game; }
+	inline Config &config() { return _config; }
 	inline bool isDebugModeActive() const { return _debugHandler != nullptr; }
 
 	void playVideo(int32 videoId);
@@ -143,6 +167,7 @@ private:
 	Input _input;
 	Sounds _sounds;
 	Scheduler _scheduler;
+	Config _config;
 };
 
 extern AlcachofaEngine *g_engine;
diff --git a/engines/alcachofa/detection.h b/engines/alcachofa/detection.h
index dfe83a6b8dc..8e37d4b99d3 100644
--- a/engines/alcachofa/detection.h
+++ b/engines/alcachofa/detection.h
@@ -37,7 +37,8 @@ extern const PlainGameDescriptor alcachofaGames[];
 
 extern const ADGameDescription gameDescriptions[];
 
-#define GAMEOPTION_ORIGINAL_SAVELOAD GUIO_GAMEOPTIONS1
+#define GAMEOPTION_HIGH_QUALITY GUIO_GAMEOPTIONS1 // I should comment what this does, but I don't know
+#define GAMEOPTION_32BITS GUIO_GAMEOPTIONS2
 
 } // End of namespace Alcachofa
 
diff --git a/engines/alcachofa/detection_tables.h b/engines/alcachofa/detection_tables.h
index 49e2ad33df4..5f577de7259 100644
--- a/engines/alcachofa/detection_tables.h
+++ b/engines/alcachofa/detection_tables.h
@@ -34,7 +34,7 @@ const ADGameDescription gameDescriptions[] = {
 		Common::DE_DEU,
 		Common::kPlatformWindows,
 		ADGF_UNSTABLE,
-		GUIO1(GUIO_NONE)
+		GUIO2(GAMEOPTION_32BITS, GAMEOPTION_HIGH_QUALITY)
 	},
 
 	AD_TABLE_END_MARKER
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 709d76053a9..2cba43125bd 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -313,7 +313,7 @@ struct SayTextTask final : public Task {
 			if (!isSoundStillPlaying || g_engine->input().wasAnyMouseReleased())
 				_character->_isTalking = false;
 
-			if (true && // TODO: Add game option for subtitles
+			if (g_engine->config().subtitles() &&
 				process().isActiveForPlayer()) {
 				g_engine->drawQueue().add<TextDrawRequest>(
 					g_engine->globalUI().dialogFont(),
diff --git a/engines/alcachofa/metaengine.cpp b/engines/alcachofa/metaengine.cpp
index d392c26959b..336f589ea9f 100644
--- a/engines/alcachofa/metaengine.cpp
+++ b/engines/alcachofa/metaengine.cpp
@@ -34,12 +34,23 @@ namespace Alcachofa {
 
 static const ADExtraGuiOptionsMap optionsList[] = {
 	{
-		GAMEOPTION_ORIGINAL_SAVELOAD, // TODO: Remove, this is not really possible
+		GAMEOPTION_HIGH_QUALITY,
 		{
-			_s("Use original save/load screens"),
-			_s("Use the original save/load screens instead of the ScummVM ones"),
-			"original_menus",
-			false,
+			_s("High Quality"),
+			_s("TODO: Explain what this does"),
+			_s("high_quality"),
+			true,
+			0,
+			0
+		}
+	},
+	{
+		GAMEOPTION_32BITS,
+		{
+			_s("32 Bits"),
+			_s("TODO: Also explain this, and implement it maybe"),
+			_s("32_bits"),
+			true,
 			0,
 			0
 		}
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 52cc49bb07c..74efcdc26c0 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -920,7 +920,7 @@ void Script::updateCommonVariables() {
 		_scriptTimer = 0;
 
 	variable("EstanAmbos") = g_engine->world().mortadelo().room() == g_engine->world().filemon().room();
-	variable("textoson") = 1; // TODO: Add subtitle option
+	variable("textoson") = g_engine->config().subtitles() ? 1 : 0;
 	variable("modored") = 0; // this is signalling whether a network connection is established
 }
 


Commit: 74512630d6c1d9b3fe14bb83a3d567b4c4f4a421
    https://github.com/scummvm/scummvm/commit/74512630d6c1d9b3fe14bb83a3d567b4c4f4a421
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:56+02:00

Commit Message:
ALCACHOFA: Add CheckBox and initial options menu

Changed paths:
    engines/alcachofa/menu.cpp
    engines/alcachofa/menu.h
    engines/alcachofa/objects.h
    engines/alcachofa/ui-objects.cpp


diff --git a/engines/alcachofa/menu.cpp b/engines/alcachofa/menu.cpp
index 8f7c6f63dd3..89d8b01e9b1 100644
--- a/engines/alcachofa/menu.cpp
+++ b/engines/alcachofa/menu.cpp
@@ -69,4 +69,39 @@ void Menu::newGame() {
 	g_engine->script().createProcess(MainCharacterKind::None, g_engine->world().initScriptName());
 }
 
+void Menu::openOptionsMenu() {
+	setOptionsState();
+	g_engine->player().changeRoom("MENUOPCIONES", true);
+}
+
+void Menu::setOptionsState() {
+	Config &config = g_engine->config();
+	Room *optionsMenu = g_engine->world().getRoomByName("MENUOPCIONES");
+	scumm_assert(optionsMenu != nullptr);
+
+	// TODO: Set music/sound volume
+
+	if (!config.bits32())
+		config.highQuality() = false;
+	auto getCheckBox = [&] (const char *name) {
+		CheckBox *checkBox = dynamic_cast<CheckBox *>(optionsMenu->getObjectByName(name));
+		scumm_assert(checkBox != nullptr);
+		return checkBox;
+	};
+	CheckBox
+		*checkSubtitlesOn = getCheckBox("Boton ON"),
+		*checkSubtitlesOff = getCheckBox("Boton OFF"),
+		*check32Bits = getCheckBox("Boton 32 Bits"),
+		*check16Bits = getCheckBox("Boton 16 Bits"),
+		*checkHighQuality = getCheckBox("Boton Alta"),
+		*checkLowQuality = getCheckBox("Boton Baja");
+	checkSubtitlesOn->isChecked() = config.subtitles();
+	checkSubtitlesOff->isChecked() = !config.subtitles();
+	check32Bits->isChecked() = config.bits32();
+	check16Bits->isChecked() = !config.bits32();
+	checkHighQuality->isChecked() = config.highQuality();
+	checkLowQuality->isChecked() = !config.highQuality();
+	checkHighQuality->toggle(config.bits32());
+}
+
 }
diff --git a/engines/alcachofa/menu.h b/engines/alcachofa/menu.h
index 2516257a30d..143b6317401 100644
--- a/engines/alcachofa/menu.h
+++ b/engines/alcachofa/menu.h
@@ -36,11 +36,14 @@ public:
 	void continueGame();
 	void newGame();
 
+	void openOptionsMenu();
+
 private:
+	void setOptionsState();
+
 	bool
 		_isOpen = false,
 		_openAtNextFrame = false;
-
 	Room *_previousRoom = nullptr;
 };
 
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index c6739b0cc2b..d8ea6937ad7 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -125,6 +125,7 @@ public:
 	virtual ~ShapeObject() override = default;
 
 	inline int8 order() const { return _order; }
+	inline bool isSelected() const { return _isSelected; }
 
 	virtual void update() override;
 	virtual void serializeSave(Common::Serializer &serializer) override;
@@ -168,8 +169,8 @@ public:
 	virtual void update() override;
 	virtual void loadResources() override;
 	virtual void freeResources() override;
-	virtual void onHoverStart();
-	virtual void onHoverEnd();
+	virtual void onHoverStart() override;
+	virtual void onHoverEnd() override;
 	virtual void onClick() override;
 	virtual void trigger();
 	virtual const char *typeName() const;
@@ -251,25 +252,30 @@ public:
 	CheckBox(Room *room, Common::ReadStream &stream);
 	virtual ~CheckBox() override = default;
 
+	inline bool &isChecked() { return _isChecked; }
+	inline int32 actionId() const { return _actionId; }
+
+	virtual void update() override; // also takes the role of draw for some reason
+	virtual void loadResources() override;
+	virtual void freeResources() override;
+	virtual void onHoverStart() override;
+	virtual void onHoverEnd() override;
+	virtual void onClick() override;
+	virtual void trigger();
 	virtual const char *typeName() const;
 
 private:
-	// TODO: Reverse engineer CheckBox
-	bool b1;
+	bool
+		_isChecked = false,
+		_wasClicked = false,
+		_isHovered = false;
 	Graphic
-		_graph1,
-		_graph2,
-		_graph3,
-		_graph4;
-	int32 _valueId;
-};
-
-class CheckBoxAutoAdjustNoise final : public CheckBox {
-public:
-	static constexpr const char *kClassName = "CCheckBoxAutoAjustarRuido";
-	CheckBoxAutoAdjustNoise(Room *room, Common::ReadStream &stream);
-
-	virtual const char *typeName() const;
+		_graphicUnchecked,
+		_graphicChecked,
+		_graphicHovered,
+		_graphicClicked;
+	int32 _actionId = 0;
+	uint32 _clickTime = 0;
 };
 
 class SlideButton final : public ObjectBase {
@@ -290,6 +296,17 @@ private:
 		_graph3;
 };
 
+// the next UI elements are only used for the multiplayer menus
+// so are currently not needed
+
+class CheckBoxAutoAdjustNoise final : public CheckBox {
+public:
+	static constexpr const char *kClassName = "CCheckBoxAutoAjustarRuido";
+	CheckBoxAutoAdjustNoise(Room *room, Common::ReadStream &stream);
+
+	virtual const char *typeName() const;
+};
+
 class IRCWindow final : public ObjectBase {
 public:
 	static constexpr const char *kClassName = "CVentanaIRC";
@@ -310,7 +327,6 @@ public:
 	virtual const char *typeName() const;
 
 private:
-	// TODO: Reverse engineer MessageBox
 	Graphic
 		_graph1,
 		_graph2,
diff --git a/engines/alcachofa/ui-objects.cpp b/engines/alcachofa/ui-objects.cpp
index 33fd81e73f3..089c52a3f0c 100644
--- a/engines/alcachofa/ui-objects.cpp
+++ b/engines/alcachofa/ui-objects.cpp
@@ -164,7 +164,7 @@ void MainMenuButton::trigger() {
 		g_system->messageBox(LogMessageType::kWarning, "Multiplayer is not implemented in this ScummVM version.");
 		break;
 	case MainMenuAction::OptionsMenu:
-		//g_engine->menu().openOptionsMenu();
+		g_engine->menu().openOptionsMenu();
 		break;
 	case MainMenuAction::Exit:
 	case MainMenuAction::AlsoExit:
@@ -214,12 +214,71 @@ const char *CheckBox::typeName() const { return "CheckBox"; }
 
 CheckBox::CheckBox(Room *room, ReadStream &stream)
 	: PhysicalObject(room, stream)
-	, b1(readBool(stream))
-	, _graph1(stream)
-	, _graph2(stream)
-	, _graph3(stream)
-	, _graph4(stream)
-	, _valueId(stream.readSint32LE()) {
+	, _isChecked(readBool(stream))
+	, _graphicUnchecked(stream)
+	, _graphicChecked(stream)
+	, _graphicHovered(stream)
+	, _graphicClicked(stream)
+	, _actionId(stream.readSint32LE()) {
+}
+
+void CheckBox::update() {
+	PhysicalObject::update();
+	if (!isEnabled())
+		return;
+	Graphic &baseGraphic = _isChecked ? _graphicChecked : _graphicUnchecked;
+	baseGraphic.update();
+	g_engine->drawQueue().add<AnimationDrawRequest>(baseGraphic, true, BlendMode::AdditiveAlpha);
+
+	if (_wasClicked) {
+		if (g_system->getMillis() - _clickTime > 500) {
+			_wasClicked = false;
+			trigger();
+		}
+	}
+	if (_isHovered) {
+		Graphic &hoverGraphic = _wasClicked ? _graphicClicked : _graphicHovered;
+		hoverGraphic.update();
+		g_engine->drawQueue().add<AnimationDrawRequest>(hoverGraphic, true, BlendMode::AdditiveAlpha);
+	}
+
+	// the original engine would stall the application as click delay.
+	// this would prevent bacterios arm in movie adventure being rendered twice for multiple checkboxes
+	// we can instead check the hovered state and prevent the arm (clicked/hovered graphic) being drawn
+}
+
+void CheckBox::loadResources() {
+	_wasClicked = _isHovered = false;
+	_graphicUnchecked.loadResources();
+	_graphicChecked.loadResources();
+	_graphicHovered.loadResources();
+	_graphicClicked.loadResources();
+}
+
+void CheckBox::freeResources() {
+	_graphicUnchecked.freeResources();
+	_graphicChecked.freeResources();
+	_graphicHovered.freeResources();
+	_graphicClicked.freeResources();
+}
+
+void CheckBox::onHoverStart() {
+	PhysicalObject::onHoverStart();
+	_isHovered = true;
+}
+
+void CheckBox::onHoverEnd() {
+	PhysicalObject::onHoverEnd();
+	_isHovered = false;
+}
+
+void CheckBox::onClick() {
+	_wasClicked = true;
+	_clickTime = g_system->getMillis();
+}
+
+void CheckBox::trigger() {
+	debug("CheckBox %d", _actionId);
 }
 
 const char *CheckBoxAutoAdjustNoise::typeName() const { return "CheckBoxAutoAdjustNoise"; }


Commit: 563bc6e7cf5749a1d119113a9a29ce0253ce0523
    https://github.com/scummvm/scummvm/commit/563bc6e7cf5749a1d119113a9a29ce0253ce0523
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:56+02:00

Commit Message:
ALCACHOFA: Implement options menu actions

Changed paths:
    engines/alcachofa/menu.cpp
    engines/alcachofa/menu.h
    engines/alcachofa/objects.h
    engines/alcachofa/ui-objects.cpp


diff --git a/engines/alcachofa/menu.cpp b/engines/alcachofa/menu.cpp
index 89d8b01e9b1..0c50fb2e86c 100644
--- a/engines/alcachofa/menu.cpp
+++ b/engines/alcachofa/menu.cpp
@@ -63,10 +63,43 @@ void Menu::continueGame() {
 	// TODO: Reset time on continueing game
 }
 
-void Menu::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());
+void Menu::triggerMainMenuAction(MainMenuAction action) {
+	switch (action) {
+	case MainMenuAction::ContinueGame:
+		g_engine->menu().continueGame();
+		break;
+	case MainMenuAction::Save:
+		warning("STUB: MainMenuAction Save");
+		break;
+	case MainMenuAction::Load:
+		warning("STUB: MainMenuAction Load");
+		break;
+	case MainMenuAction::InternetMenu:
+		g_system->messageBox(LogMessageType::kWarning, "Multiplayer is not implemented in this ScummVM version.");
+		break;
+	case MainMenuAction::OptionsMenu:
+		g_engine->menu().openOptionsMenu();
+		break;
+	case MainMenuAction::Exit:
+	case MainMenuAction::AlsoExit:
+		// implemented in AlcachofaEngine as it has its own event loop
+		g_engine->fadeExit();
+		break;
+	case MainMenuAction::NextSave:
+		warning("STUB: MainMenuAction NextSave");
+		break;
+	case MainMenuAction::PrevSave:
+		warning("STUB: MainMenuAction PrevSave");
+		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:
+		warning("Unknown main menu action: %d", (int32)action);
+		break;
+	}
 }
 
 void Menu::openOptionsMenu() {
@@ -104,4 +137,45 @@ void Menu::setOptionsState() {
 	checkHighQuality->toggle(config.bits32());
 }
 
+void Menu::triggerOptionsAction(OptionsMenuAction action) {
+	Config &config = g_engine->config();
+	switch (action) {
+	case OptionsMenuAction::SubtitlesOn:
+		config.subtitles() = true;
+		break;
+	case OptionsMenuAction::SubtitlesOff:
+		config.subtitles() = false;
+		break;
+	case OptionsMenuAction::HighQuality:
+		config.highQuality() = true;
+		break;
+	case OptionsMenuAction::LowQuality:
+		config.highQuality() = false;
+		break;
+	case OptionsMenuAction::Bits32:
+		config.bits32() = true;
+		config.highQuality() = true;
+		break;
+	case OptionsMenuAction::Bits16:
+		config.bits32() = false;
+		break;
+	case OptionsMenuAction::MainMenu:
+		continueMainMenu();
+		break;
+	default:
+		warning("Unknown check box action: %d", (int32)action);
+	}
+	setOptionsState();
+}
+
+void Menu::continueMainMenu() {
+	g_engine->config().saveToScummVM();
+	g_engine->syncSoundSettings();
+	g_engine->player().changeRoom(
+		g_engine->player().isGameLoaded() ? "MENUPRINCIPAL" : "MENUPRINCIPALINICIO",
+		true
+	);
+	// TODO: Update menu state and thumbanil
+}
+
 }
diff --git a/engines/alcachofa/menu.h b/engines/alcachofa/menu.h
index 143b6317401..e35a29e0cf5 100644
--- a/engines/alcachofa/menu.h
+++ b/engines/alcachofa/menu.h
@@ -28,17 +28,42 @@ namespace Alcachofa {
 
 class Room;
 
+enum class MainMenuAction : int32 {
+	ContinueGame = 0,
+	Save,
+	Load,
+	InternetMenu,
+	OptionsMenu,
+	Exit,
+	NextSave,
+	PrevSave,
+	NewGame,
+	AlsoExit // there seems to be no difference to Exit
+};
+
+enum class OptionsMenuAction : int32 {
+	SubtitlesOn = 0,
+	SubtitlesOff,
+	HighQuality,
+	LowQuality,
+	Bits32,
+	Bits16,
+	MainMenu
+};
+
 class Menu {
 public:
 	inline bool isOpen() const { return _isOpen; }
 
 	void updateOpeningMenu();
-	void continueGame();
-	void newGame();
+	void triggerMainMenuAction(MainMenuAction action);
 
 	void openOptionsMenu();
+	void triggerOptionsAction(OptionsMenuAction action);
 
 private:
+	void continueGame();
+	void continueMainMenu();
 	void setOptionsState();
 
 	bool
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index d8ea6937ad7..7a2f106e480 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -189,6 +189,9 @@ private:
 		_graphicDisabled;
 };
 
+// some of the UI elements are only used for the multiplayer menus
+// so are currently not needed
+
 class InternetMenuButton final : public MenuButton {
 public:
 	static constexpr const char *kClassName = "CBotonMenuInternet";
@@ -202,6 +205,8 @@ public:
 	static constexpr const char *kClassName = "CBotonMenuOpciones";
 	OptionsMenuButton(Room *room, Common::ReadStream &stream);
 
+	virtual void update() override;
+	virtual void trigger() override;
 	virtual const char *typeName() const;
 };
 
@@ -223,7 +228,6 @@ public:
 	virtual const char *typeName() const;
 
 private:
-	// TODO: Reverse engineer PushButton
 	bool _alwaysVisible;
 	Graphic _graphic1, _graphic2;
 	int32 _actionId;
@@ -237,7 +241,6 @@ public:
 	virtual const char *typeName() const;
 
 private:
-	// TODO: Reverse engineer EditBox
 	int32 i1;
 	Common::Point p1;
 	Common::String _labelId;
@@ -255,7 +258,8 @@ public:
 	inline bool &isChecked() { return _isChecked; }
 	inline int32 actionId() const { return _actionId; }
 
-	virtual void update() override; // also takes the role of draw for some reason
+	virtual void draw() override;
+	virtual void update() override;
 	virtual void loadResources() override;
 	virtual void freeResources() override;
 	virtual void onHoverStart() override;
@@ -296,9 +300,6 @@ private:
 		_graph3;
 };
 
-// the next UI elements are only used for the multiplayer menus
-// so are currently not needed
-
 class CheckBoxAutoAdjustNoise final : public CheckBox {
 public:
 	static constexpr const char *kClassName = "CCheckBoxAutoAjustarRuido";
diff --git a/engines/alcachofa/ui-objects.cpp b/engines/alcachofa/ui-objects.cpp
index 089c52a3f0c..3750453467a 100644
--- a/engines/alcachofa/ui-objects.cpp
+++ b/engines/alcachofa/ui-objects.cpp
@@ -30,19 +30,6 @@ using namespace Common;
 
 namespace Alcachofa {
 
-enum class MainMenuAction : int32 {
-	ContinueGame = 0,
-	Save,
-	Load,
-	InternetMenu,
-	OptionsMenu,
-	Exit,
-	NextSave,
-	PrevSave,
-	NewGame,
-	AlsoExit // there seems to be no difference to Exit
-};
-
 const char *MenuButton::typeName() const { return "MenuButton"; }
 
 MenuButton::MenuButton(Room *room, ReadStream &stream)
@@ -135,6 +122,17 @@ OptionsMenuButton::OptionsMenuButton(Room *room, ReadStream &stream)
 	: MenuButton(room, stream) {
 }
 
+void OptionsMenuButton::update() {
+	MenuButton::update();
+	const auto action = (OptionsMenuAction)actionId();
+	if (action == OptionsMenuAction::MainMenu && g_engine->input().wasMenuKeyPressed())
+		onClick();
+}
+
+void OptionsMenuButton::trigger() {
+	g_engine->menu().triggerOptionsAction((OptionsMenuAction)actionId());
+}
+
 const char *MainMenuButton::typeName() const { return "MainMenuButton"; }
 
 MainMenuButton::MainMenuButton(Room *room, ReadStream &stream)
@@ -150,40 +148,7 @@ void MainMenuButton::update() {
 }
 
 void MainMenuButton::trigger() {
-	switch ((MainMenuAction)actionId()) {
-	case MainMenuAction::ContinueGame:
-		g_engine->menu().continueGame();
-		break;
-	case MainMenuAction::Save:
-		warning("STUB: MainMenuAction Save");
-		break;
-	case MainMenuAction::Load:
-		warning("STUB: MainMenuAction Load");
-		break;
-	case MainMenuAction::InternetMenu:
-		g_system->messageBox(LogMessageType::kWarning, "Multiplayer is not implemented in this ScummVM version.");
-		break;
-	case MainMenuAction::OptionsMenu:
-		g_engine->menu().openOptionsMenu();
-		break;
-	case MainMenuAction::Exit:
-	case MainMenuAction::AlsoExit:
-		// implemented in AlcachofaEngine as it has its own event loop
-		g_engine->fadeExit();
-		break;
-	case MainMenuAction::NextSave:
-		warning("STUB: MainMenuAction NextSave");
-		break;
-	case MainMenuAction::PrevSave:
-		warning("STUB: MainMenuAction PrevSave");
-		break;
-	case MainMenuAction::NewGame:
-		g_engine->menu().newGame();
-		break;
-	default:
-		warning("Unknown main menu action: %d", actionId());
-		break;
-	}
+	g_engine->menu().triggerMainMenuAction((MainMenuAction)actionId());
 }
 
 const char *PushButton::typeName() const { return "PushButton"; }
@@ -222,25 +187,28 @@ CheckBox::CheckBox(Room *room, ReadStream &stream)
 	, _actionId(stream.readSint32LE()) {
 }
 
-void CheckBox::update() {
-	PhysicalObject::update();
+void CheckBox::draw() {
 	if (!isEnabled())
 		return;
 	Graphic &baseGraphic = _isChecked ? _graphicChecked : _graphicUnchecked;
 	baseGraphic.update();
 	g_engine->drawQueue().add<AnimationDrawRequest>(baseGraphic, true, BlendMode::AdditiveAlpha);
 
+	if (_isHovered) {
+		Graphic &hoverGraphic = _wasClicked ? _graphicClicked : _graphicHovered;
+		hoverGraphic.update();
+		g_engine->drawQueue().add<AnimationDrawRequest>(hoverGraphic, true, BlendMode::AdditiveAlpha);
+	}
+}
+
+void CheckBox::update() {
+	PhysicalObject::update();
 	if (_wasClicked) {
 		if (g_system->getMillis() - _clickTime > 500) {
 			_wasClicked = false;
 			trigger();
 		}
 	}
-	if (_isHovered) {
-		Graphic &hoverGraphic = _wasClicked ? _graphicClicked : _graphicHovered;
-		hoverGraphic.update();
-		g_engine->drawQueue().add<AnimationDrawRequest>(hoverGraphic, true, BlendMode::AdditiveAlpha);
-	}
 
 	// the original engine would stall the application as click delay.
 	// this would prevent bacterios arm in movie adventure being rendered twice for multiple checkboxes
@@ -278,7 +246,7 @@ void CheckBox::onClick() {
 }
 
 void CheckBox::trigger() {
-	debug("CheckBox %d", _actionId);
+	g_engine->menu().triggerOptionsAction((OptionsMenuAction)actionId());
 }
 
 const char *CheckBoxAutoAdjustNoise::typeName() const { return "CheckBoxAutoAdjustNoise"; }


Commit: 5eaa3e56c6f491e1777ae60453a6484b81257c08
    https://github.com/scummvm/scummvm/commit/5eaa3e56c6f491e1777ae60453a6484b81257c08
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:56+02:00

Commit Message:
ALCACHOFA: Fix options menu arm

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


diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index c240db4a245..4c5e2448c23 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -202,7 +202,7 @@ void ShapeObject::update() {
 	if (isEnabled())
 		updateSelection();
 	else {
-		_isSelected = false;
+		_isNewlySelected = false;
 		_wasSelected = false;
 	}
 }
@@ -239,12 +239,12 @@ void ShapeObject::onClick() {
 }
 
 void ShapeObject::markSelected() {
-	_isSelected = true;
+	_isNewlySelected = true;
 }
 
 void ShapeObject::updateSelection() {
-	if (_isSelected) {
-		_isSelected = false;
+	if (_isNewlySelected) {
+		_isNewlySelected = false;
 		if (_wasSelected) {
 			if (g_engine->input().wasAnyMouseReleased() && g_engine->player().selectedObject() == this)
 				onClick();
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 7a2f106e480..29983523514 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -125,7 +125,8 @@ public:
 	virtual ~ShapeObject() override = default;
 
 	inline int8 order() const { return _order; }
-	inline bool isSelected() const { return _isSelected; }
+	inline bool isNewlySelected() const { return _isNewlySelected; }
+	inline bool wasSelected() const { return _wasSelected; }
 
 	virtual void update() override;
 	virtual void serializeSave(Common::Serializer &serializer) override;
@@ -146,7 +147,7 @@ protected:
 private:
 	Shape _shape;
 	CursorType _cursorType;
-	bool _isSelected = false,
+	bool _isNewlySelected = false,
 		_wasSelected = false;
 };
 
@@ -169,8 +170,7 @@ public:
 	virtual void update() override;
 	virtual void loadResources() override;
 	virtual void freeResources() override;
-	virtual void onHoverStart() override;
-	virtual void onHoverEnd() override;
+	virtual void onHoverUpdate() override;
 	virtual void onClick() override;
 	virtual void trigger();
 	virtual const char *typeName() const;
@@ -179,7 +179,6 @@ private:
 	bool
 		_isInteractable = true,
 		_isClicked = false,
-		_isHovered = false,
 		_triggerNextFrame = false;
 	int32 _actionId;
 	Graphic
@@ -262,8 +261,7 @@ public:
 	virtual void update() override;
 	virtual void loadResources() override;
 	virtual void freeResources() override;
-	virtual void onHoverStart() override;
-	virtual void onHoverEnd() override;
+	virtual void onHoverUpdate() override;
 	virtual void onClick() override;
 	virtual void trigger();
 	virtual const char *typeName() const;
@@ -271,8 +269,7 @@ public:
 private:
 	bool
 		_isChecked = false,
-		_wasClicked = false,
-		_isHovered = false;
+		_wasClicked = false;
 	Graphic
 		_graphicUnchecked,
 		_graphicChecked,
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 4dacd66371d..bcfaedff8ee 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -332,6 +332,33 @@ OptionsMenu::OptionsMenu(World *world, SeekableReadStream &stream)
 	: Room(world, stream, true) {
 }
 
+bool OptionsMenu::updateInput() {
+	if (!Room::updateInput())
+		return false;
+
+	auto currentSelectedObject = g_engine->player().selectedObject();
+	if (currentSelectedObject == nullptr) {
+		if (_lastSelectedObject == nullptr) {
+			if (_idleArm != nullptr)
+				_idleArm->toggle(true);
+			return true;
+		}
+
+		_lastSelectedObject->markSelected();
+	}
+	else
+		_lastSelectedObject = currentSelectedObject;
+	if (_idleArm != nullptr)
+		_idleArm->toggle(false);
+	return true;
+}
+
+void OptionsMenu::loadResources() {
+	Room::loadResources();
+	_lastSelectedObject = nullptr;
+	_idleArm = getObjectByName("Brazo");
+}
+
 ConnectMenu::ConnectMenu(World *world, SeekableReadStream &stream)
 	: Room(world, stream, true) {
 }
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index a35b2293a9c..945d6b65cf2 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -91,6 +91,13 @@ class OptionsMenu final : public Room {
 public:
 	static constexpr const char *kClassName = "CHabitacionMenuOpciones";
 	OptionsMenu(World *world, Common::SeekableReadStream &stream);
+
+	virtual bool updateInput() override;
+	virtual void loadResources() override;
+
+private:
+	ShapeObject *_lastSelectedObject = nullptr;
+	ObjectBase *_idleArm = nullptr;
 };
 
 class ConnectMenu final : public Room {
diff --git a/engines/alcachofa/ui-objects.cpp b/engines/alcachofa/ui-objects.cpp
index 3750453467a..44aef0ba05e 100644
--- a/engines/alcachofa/ui-objects.cpp
+++ b/engines/alcachofa/ui-objects.cpp
@@ -47,7 +47,7 @@ void MenuButton::draw() {
 	Graphic &graphic =
 		!_isInteractable ? _graphicDisabled
 		: _isClicked ? _graphicClicked
-		: _isHovered ? _graphicHovered
+		: wasSelected() ? _graphicHovered
 		: _graphicNormal;
 	graphic.update();
 	g_engine->drawQueue().add<AnimationDrawRequest>(graphic, true, BlendMode::AdditiveAlpha);
@@ -87,15 +87,7 @@ void MenuButton::freeResources() {
 	_graphicDisabled.freeResources();
 }
 
-void MenuButton::onHoverStart() {
-	PhysicalObject::onHoverStart();
-	_isHovered = true;
-}
-
-void MenuButton::onHoverEnd() {
-	PhysicalObject::onHoverEnd();
-	_isHovered = false;
-}
+void MenuButton::onHoverUpdate() {}
 
 void MenuButton::onClick() {
 	if (_isInteractable) {
@@ -194,7 +186,7 @@ void CheckBox::draw() {
 	baseGraphic.update();
 	g_engine->drawQueue().add<AnimationDrawRequest>(baseGraphic, true, BlendMode::AdditiveAlpha);
 
-	if (_isHovered) {
+	if (wasSelected()) {
 		Graphic &hoverGraphic = _wasClicked ? _graphicClicked : _graphicHovered;
 		hoverGraphic.update();
 		g_engine->drawQueue().add<AnimationDrawRequest>(hoverGraphic, true, BlendMode::AdditiveAlpha);
@@ -216,7 +208,7 @@ void CheckBox::update() {
 }
 
 void CheckBox::loadResources() {
-	_wasClicked = _isHovered = false;
+	_wasClicked = false;
 	_graphicUnchecked.loadResources();
 	_graphicChecked.loadResources();
 	_graphicHovered.loadResources();
@@ -230,15 +222,7 @@ void CheckBox::freeResources() {
 	_graphicClicked.freeResources();
 }
 
-void CheckBox::onHoverStart() {
-	PhysicalObject::onHoverStart();
-	_isHovered = true;
-}
-
-void CheckBox::onHoverEnd() {
-	PhysicalObject::onHoverEnd();
-	_isHovered = false;
-}
+void CheckBox::onHoverUpdate() {}
 
 void CheckBox::onClick() {
 	_wasClicked = true;


Commit: cfa7f7081149df22d991d7bcec432f3e9825a7f5
    https://github.com/scummvm/scummvm/commit/cfa7f7081149df22d991d7bcec432f3e9825a7f5
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:56+02:00

Commit Message:
ALCACHOFA: Add slide buttons

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


diff --git a/engines/alcachofa/menu.cpp b/engines/alcachofa/menu.cpp
index 0c50fb2e86c..8dfac1bd136 100644
--- a/engines/alcachofa/menu.cpp
+++ b/engines/alcachofa/menu.cpp
@@ -112,7 +112,16 @@ void Menu::setOptionsState() {
 	Room *optionsMenu = g_engine->world().getRoomByName("MENUOPCIONES");
 	scumm_assert(optionsMenu != nullptr);
 
-	// TODO: Set music/sound volume
+	auto getSlideButton = [&] (const char *name) {
+		SlideButton *slideButton = dynamic_cast<SlideButton *>(optionsMenu->getObjectByName(name));
+		scumm_assert(slideButton != nullptr);
+		return slideButton;
+	};
+	SlideButton
+		*slideMusicVolume = getSlideButton("Slider Musica"),
+		*slideSpeechVolume = getSlideButton("Slider Sonido");
+	slideMusicVolume->value() = config.musicVolume() / 255.0f;
+	slideSpeechVolume->value() = config.speechVolume() / 255.0f;
 
 	if (!config.bits32())
 		config.highQuality() = false;
@@ -163,7 +172,24 @@ void Menu::triggerOptionsAction(OptionsMenuAction action) {
 		continueMainMenu();
 		break;
 	default:
-		warning("Unknown check box action: %d", (int32)action);
+		warning("Unknown options menu action: %d", (int32)action);
+		break;
+	}
+	setOptionsState();
+}
+
+void Menu::triggerOptionsValue(OptionsMenuValue valueId, float value) {
+	Config &config = g_engine->config();
+	switch (valueId) {
+	case OptionsMenuValue::Music:
+		config.musicVolume() = CLIP<uint8>((uint8)(value * 255), 0, 255);
+		break;
+	case OptionsMenuValue::Speech:
+		config.speechVolume() = CLIP<uint8>((uint8)(value * 255), 0, 255);
+		break;
+	default:
+		warning("Unknown options menu value: %d", (int32)valueId);
+		break;
 	}
 	setOptionsState();
 }
diff --git a/engines/alcachofa/menu.h b/engines/alcachofa/menu.h
index e35a29e0cf5..f4293862852 100644
--- a/engines/alcachofa/menu.h
+++ b/engines/alcachofa/menu.h
@@ -51,6 +51,11 @@ enum class OptionsMenuAction : int32 {
 	MainMenu
 };
 
+enum class OptionsMenuValue : int32 {
+	Music = 0,
+	Speech = 1
+};
+
 class Menu {
 public:
 	inline bool isOpen() const { return _isOpen; }
@@ -60,6 +65,7 @@ public:
 
 	void openOptionsMenu();
 	void triggerOptionsAction(OptionsMenuAction action);
+	void triggerOptionsValue(OptionsMenuValue valueId, float value);
 
 private:
 	void continueGame();
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 29983523514..c47ba4fbd4c 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -285,16 +285,24 @@ public:
 	SlideButton(Room *room, Common::ReadStream &stream);
 	virtual ~SlideButton() override = default;
 
+	inline float &value() { return _value; }
+
+	virtual void draw() override;
+	virtual void update() override;
+	virtual void loadResources() override;
+	virtual void freeResources() override;
 	virtual const char *typeName() const;
 
 private:
-	// TODO: Reverse engineer SlideButton
-	int32 i1;
-	Common::Point p1, p2;
+	bool isMouseOver() const;
+
+	float _value = 0;
+	int32 _valueId;
+	Common::Point _minPos, _maxPos;
 	Graphic
-		_graph1,
-		_graph2,
-		_graph3;
+		_graphicIdle,
+		_graphicHovered,
+		_graphicClicked;
 };
 
 class CheckBoxAutoAdjustNoise final : public CheckBox {
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index bcfaedff8ee..0b2eb1d2dcf 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -356,9 +356,14 @@ bool OptionsMenu::updateInput() {
 void OptionsMenu::loadResources() {
 	Room::loadResources();
 	_lastSelectedObject = nullptr;
+	_currentSlideButton = nullptr;
 	_idleArm = getObjectByName("Brazo");
 }
 
+void OptionsMenu::clearLastSelectedObject() {
+	_lastSelectedObject = nullptr;
+}
+
 ConnectMenu::ConnectMenu(World *world, SeekableReadStream &stream)
 	: Room(world, stream, true) {
 }
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index 945d6b65cf2..3f0e0535397 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -95,9 +95,13 @@ public:
 	virtual bool updateInput() override;
 	virtual void loadResources() override;
 
+	void clearLastSelectedObject(); // to reset arm animation
+	inline SlideButton *&currentSlideButton() { return _currentSlideButton; }
+
 private:
 	ShapeObject *_lastSelectedObject = nullptr;
 	ObjectBase *_idleArm = nullptr;
+	SlideButton *_currentSlideButton;
 };
 
 class ConnectMenu final : public Room {
diff --git a/engines/alcachofa/ui-objects.cpp b/engines/alcachofa/ui-objects.cpp
index 44aef0ba05e..10e733fcdf2 100644
--- a/engines/alcachofa/ui-objects.cpp
+++ b/engines/alcachofa/ui-objects.cpp
@@ -244,12 +244,75 @@ const char *SlideButton::typeName() const { return "SlideButton"; }
 
 SlideButton::SlideButton(Room *room, ReadStream &stream)
 	: ObjectBase(room, stream)
-	, i1(stream.readSint32LE())
-	, p1(Shape(stream).firstPoint())
-	, p2(Shape(stream).firstPoint())
-	, _graph1(stream)
-	, _graph2(stream)
-	, _graph3(stream) {
+	, _valueId(stream.readSint32LE())
+	, _minPos(Shape(stream).firstPoint())
+	, _maxPos(Shape(stream).firstPoint())
+	, _graphicIdle(stream)
+	, _graphicHovered(stream)
+	, _graphicClicked(stream) {
+}
+
+void SlideButton::draw() {
+	auto *optionsMenu = dynamic_cast<OptionsMenu *>(room());
+	assert(optionsMenu != nullptr);
+
+	Graphic *activeGraphic;
+	if (optionsMenu->currentSlideButton() == this && g_engine->input().isMouseLeftDown())
+		activeGraphic = &_graphicClicked;
+	else
+		activeGraphic = isMouseOver() ? &_graphicHovered : &_graphicIdle;
+	activeGraphic->update();
+	g_engine->drawQueue().add<AnimationDrawRequest>(*activeGraphic, true, BlendMode::AdditiveAlpha);
+}
+
+void SlideButton::update() {
+	const auto mousePos = g_engine->input().mousePos2D();
+	auto *optionsMenu = dynamic_cast<OptionsMenu *>(room());
+	assert(optionsMenu != nullptr);
+
+	if (optionsMenu->currentSlideButton() == this) {
+		if (!g_engine->input().isMouseLeftDown()) {
+			optionsMenu->currentSlideButton() = nullptr;
+			g_engine->menu().triggerOptionsValue((OptionsMenuValue)_valueId, _value);
+			update(); // to update the position
+		}
+		else {
+			int clippedMousePosY = CLIP(mousePos.y, _minPos.y, _maxPos.y);
+			_value = (_maxPos.y - clippedMousePosY) / (float)(_maxPos.y - _minPos.y);
+			_graphicClicked.topLeft() = Point((_minPos.x + _maxPos.x) / 2, clippedMousePosY);
+		}
+	}
+	else {
+		_graphicIdle.topLeft() = Point(
+			(_minPos.x + _maxPos.x) / 2,
+			(int16)(_maxPos.y - _value * (_maxPos.y - _minPos.y)));
+		if (!isMouseOver())
+			return;
+		_graphicHovered.topLeft() = _graphicIdle.topLeft();
+		if (g_engine->input().wasMouseLeftPressed())
+			optionsMenu->currentSlideButton() = this;
+		optionsMenu->clearLastSelectedObject();
+		g_engine->player().selectedObject() = nullptr;
+	}
+}
+
+void SlideButton::loadResources() {
+	_graphicIdle.loadResources();
+	_graphicHovered.loadResources();
+	_graphicClicked.loadResources();
+}
+
+void SlideButton::freeResources() {
+	_graphicIdle.freeResources();
+	_graphicHovered.freeResources();
+	_graphicClicked.freeResources();
+}
+
+bool SlideButton::isMouseOver() const {
+	const auto mousePos = g_engine->input().mousePos2D();
+	return
+		mousePos.x >= _minPos.x && mousePos.y >= _minPos.y &&
+		mousePos.x <= _maxPos.x && mousePos.y <= _maxPos.y;
 }
 
 const char *IRCWindow::typeName() const { return "IRCWindow"; }


Commit: 7165397f7078b7d175a0379cf5cd5aabfa0fec25
    https://github.com/scummvm/scummvm/commit/7165397f7078b7d175a0379cf5cd5aabfa0fec25
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:56+02:00

Commit Message:
ALCACHOFA: Replace SoundID with SoundHandle

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


diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 2cba43125bd..903002568a1 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -301,15 +301,15 @@ struct SayTextTask final : public Task {
 		while (true) {
 			g_engine->player().addLastDialogCharacter(_character);
 
-			if (_soundId == kInvalidSoundID)
+			if (_soundHandle == SoundHandle {})
 			{
 				bool hasMortadeloVoice = g_engine->game().hasMortadeloVoice(_character);					
-				_soundId = g_engine->sounds().playVoice(
+				_soundHandle = g_engine->sounds().playVoice(
 					String::format(hasMortadeloVoice ? "M%04d" : "%04d", _dialogId),
 					0);
 			}
-			isSoundStillPlaying = g_engine->sounds().isAlive(_soundId);
-			g_engine->sounds().setAppropriateVolume(_soundId, process().character(), _character);
+			isSoundStillPlaying = g_engine->sounds().isAlive(_soundHandle);
+			g_engine->sounds().setAppropriateVolume(_soundHandle, process().character(), _character);
 			if (!isSoundStillPlaying || g_engine->input().wasAnyMouseReleased())
 				_character->_isTalking = false;
 
@@ -323,13 +323,13 @@ struct SayTextTask final : public Task {
 			}
 
 			if (!_character->_isTalking) {
-				g_engine->sounds().fadeOut(_soundId, 100);
+				g_engine->sounds().fadeOut(_soundHandle, 100);
 				TASK_WAIT(delay(200));
 				TASK_RETURN(0);
 			}
 
 			_character->isSpeaking() = !isSoundStillPlaying ||
-				g_engine->sounds().isNoisy(_soundId, 80.0f, 150.0f);
+				g_engine->sounds().isNoisy(_soundHandle, 80.0f, 150.0f);
 			TASK_YIELD;
 		}
 		TASK_END;
@@ -342,7 +342,7 @@ struct SayTextTask final : public Task {
 private:
 	Character *_character;
 	int32 _dialogId;
-	SoundID _soundId = kInvalidSoundID;
+	SoundHandle _soundHandle = {};
 };
 
 Task *Character::sayText(Process &process, int32 dialogId) {
diff --git a/engines/alcachofa/sounds.cpp b/engines/alcachofa/sounds.cpp
index 2646659c287..f4ad498203f 100644
--- a/engines/alcachofa/sounds.cpp
+++ b/engines/alcachofa/sounds.cpp
@@ -49,9 +49,9 @@ Sounds::~Sounds() {
 	_mixer->stopAll();
 }
 
-Sounds::Playback *Sounds::getPlaybackById(SoundID id) {
+Sounds::Playback *Sounds::getPlaybackById(SoundHandle id) {
 	auto itPlayback = find_if(_playbacks.begin(), _playbacks.end(),
-		[&] (const Playback &playback) { return playback._id == id; });
+		[&] (const Playback &playback) { return playback._handle == id; });
 	return itPlayback == _playbacks.end() ? nullptr : itPlayback;
 }
 
@@ -129,10 +129,10 @@ static AudioStream *openAudio(const char *fileName) {
 	return nullptr;
 }
 
-SoundID Sounds::playSoundInternal(const char *fileName, byte volume, Mixer::SoundType type) {
+SoundHandle Sounds::playSoundInternal(const char *fileName, byte volume, Mixer::SoundType type) {
 	AudioStream *stream = openAudio(fileName);
 	if (stream == nullptr)
-		return UINT32_MAX;
+		return {};
 
 	Array<int16> samples;
 	SeekableAudioStream *seekStream = dynamic_cast<SeekableAudioStream *>(stream);
@@ -179,20 +179,19 @@ SoundID Sounds::playSoundInternal(const char *fileName, byte volume, Mixer::Soun
 
 	Playback playback;
 	_mixer->playStream(type, &playback._handle, stream, -1, volume);
-	playback._id = _nextID++;
 	playback._type = type;
 	playback._inputRate = stream->getRate();
 	playback._samples = std::move(samples);
 	_playbacks.push_back(std::move(playback));
-	return playback._id;
+	return playback._handle;
 }
 
-SoundID Sounds::playVoice(const String &fileName, byte volume) {
+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);
 }
 
-SoundID Sounds::playSFX(const String &fileName, byte volume) {
+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);
 }
@@ -217,18 +216,18 @@ void Sounds::pauseAll(bool paused) {
 	_mixer->pauseAll(paused);
 }
 
-bool Sounds::isAlive(SoundID id) {
+bool Sounds::isAlive(SoundHandle id) {
 	Playback *playback = getPlaybackById(id);
 	return playback != nullptr && _mixer->isSoundHandleActive(playback->_handle);
 }
 
-void Sounds::setVolume(SoundID id, byte volume) {
+void Sounds::setVolume(SoundHandle id, byte volume) {
 	Playback *playback = getPlaybackById(id);
 	if (playback != nullptr)
 		_mixer->setChannelVolume(playback->_handle, volume);
 }
 
-void Sounds::setAppropriateVolume(SoundID id,
+void Sounds::setAppropriateVolume(SoundHandle id,
 	MainCharacterKind processCharacterKind,
 	Character *speakingCharacter) {
 	static constexpr byte kAlmostMaxVolume = Mixer::kMaxChannelVolume * 9 / 10;
@@ -248,7 +247,7 @@ void Sounds::setAppropriateVolume(SoundID id,
 	setVolume(id, newVolume);
 }
 
-void Sounds::fadeOut(SoundID id, uint32 duration) {
+void Sounds::fadeOut(SoundHandle id, uint32 duration) {
 	Playback *playback = getPlaybackById(id);
 	if (playback != nullptr)
 		playback->fadeOut(duration);
@@ -261,7 +260,7 @@ void Sounds::fadeOutVoiceAndSFX(uint32 duration) {
 	}
 }
 
-bool Sounds::isNoisy(SoundID id, float windowSize, float minDifferences) {
+bool Sounds::isNoisy(SoundHandle id, float windowSize, float minDifferences) {
 	assert(windowSize > 0 && minDifferences > 0);
 	const Playback *playback = getPlaybackById(id);
 	if (playback == nullptr ||
@@ -334,16 +333,16 @@ Task *Sounds::waitForMusicToEnd(Process &process) {
 	return new WaitForMusicTask(process, std::move(lock));
 }
 
-PlaySoundTask::PlaySoundTask(Process &process, SoundID soundID)
+PlaySoundTask::PlaySoundTask(Process &process, SoundHandle SoundHandle)
 	: Task(process)
-	, _soundID(soundID) {
+	, _soundHandle(SoundHandle) {
 }
 
 TaskReturn PlaySoundTask::run() {
 	auto &sounds = g_engine->sounds();
-	if (sounds.isAlive(_soundID))
+	if (sounds.isAlive(_soundHandle))
 	{
-		sounds.setAppropriateVolume(_soundID, process().character(), nullptr);
+		sounds.setAppropriateVolume(_soundHandle, process().character(), nullptr);
 		return TaskReturn::yield();
 	}
 	else
@@ -351,7 +350,7 @@ TaskReturn PlaySoundTask::run() {
 }
 
 void PlaySoundTask::debugPrint() {
-	g_engine->console().debugPrintf("PlaySound %u\n", _soundID);
+	g_engine->console().debugPrintf("PlaySound %u\n", _soundHandle);
 }
 
 WaitForMusicTask::WaitForMusicTask(Process &process, FakeLock &&lock)
diff --git a/engines/alcachofa/sounds.h b/engines/alcachofa/sounds.h
index 0cdf8250405..1d636cb32c7 100644
--- a/engines/alcachofa/sounds.h
+++ b/engines/alcachofa/sounds.h
@@ -29,28 +29,27 @@
 namespace Alcachofa {
 
 class Character;
+using ::Audio::SoundHandle;
 
-using SoundID = uint32;
-static constexpr SoundID kInvalidSoundID = 0;
 class Sounds {
 public:
 	Sounds();
 	~Sounds();
 
 	void update();
-	SoundID playVoice(const Common::String &fileName, byte volume = Audio::Mixer::kMaxChannelVolume);
-	SoundID playSFX(const Common::String &fileName, byte volume = Audio::Mixer::kMaxChannelVolume);
+	SoundHandle playVoice(const Common::String &fileName, byte volume = Audio::Mixer::kMaxChannelVolume);
+	SoundHandle playSFX(const Common::String &fileName, byte volume = Audio::Mixer::kMaxChannelVolume);
 	void stopAll();
 	void stopVoice();
 	void pauseAll(bool paused);
-	void fadeOut(SoundID id, uint32 duration);
+	void fadeOut(SoundHandle id, uint32 duration);
 	void fadeOutVoiceAndSFX(uint32 duration);
-	bool isAlive(SoundID id);
-	void setVolume(SoundID id, byte volume);
-	void setAppropriateVolume(SoundID id,
+	bool isAlive(SoundHandle id);
+	void setVolume(SoundHandle id, byte volume);
+	void setAppropriateVolume(SoundHandle id,
 		MainCharacterKind processCharacter,
 		Character *speakingCharacter);
-	bool isNoisy(SoundID id, float windowSize, float minDifferences); ///< used for lip-sync
+	bool isNoisy(SoundHandle id, float windowSize, float minDifferences); ///< used for lip-sync
 
 	void startMusic(int musicId);
 	void queueMusic(int musicId);
@@ -65,7 +64,6 @@ private:
 	struct Playback {;
 		void fadeOut(uint32 duration);
 
-		SoundID _id = 0;
 		Audio::SoundHandle _handle;
 		Audio::Mixer::SoundType _type = Audio::Mixer::SoundType::kPlainSoundType;
 		uint32 _fadeStart = 0,
@@ -73,25 +71,24 @@ private:
 		int _inputRate;
 		Common::Array<int16> _samples; ///< might not be filled, only voice samples are preloaded for lip-sync
 	};
-	Playback *getPlaybackById(SoundID id);
-	SoundID playSoundInternal(const char *fileName, byte volume, Audio::Mixer::SoundType type);
+	Playback *getPlaybackById(SoundHandle id);
+	SoundHandle playSoundInternal(const char *fileName, byte volume, Audio::Mixer::SoundType type);
 
 	Common::Array<Playback> _playbacks;
 	Audio::Mixer *_mixer;
-	SoundID _nextID = 1;
 
-	SoundID _musicSoundID = kInvalidSoundID; // we use another soundID to reuse fading
+	SoundHandle _musicSoundID = {}; // we use another soundID to reuse fading
 	bool _isMusicPlaying = false;
 	int _nextMusicID = -1;
 	FakeSemaphore _musicSemaphore;
 };
 
 struct PlaySoundTask final : public Task {
-	PlaySoundTask(Process &process, SoundID soundID);
+	PlaySoundTask(Process &process, SoundHandle soundHandle);
 	virtual TaskReturn run() override;
 	virtual void debugPrint() override;
 private:
-	SoundID _soundID;
+	SoundHandle _soundHandle;
 };
 
 struct WaitForMusicTask final : public Task {


Commit: c9d1bd172543bb0ac921776515a9b027e3c58439
    https://github.com/scummvm/scummvm/commit/c9d1bd172543bb0ac921776515a9b027e3c58439
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:56+02:00

Commit Message:
ALCACHOFA: Fix pausing on game menu and ScummVM menus

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/alcachofa.h
    engines/alcachofa/camera.cpp
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/general-objects.cpp
    engines/alcachofa/global-ui.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/menu.cpp
    engines/alcachofa/menu.h
    engines/alcachofa/scheduler.cpp
    engines/alcachofa/script.cpp
    engines/alcachofa/ui-objects.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 70a6e159e59..0b651c154e4 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -77,6 +77,7 @@ Common::Error AlcachofaEngine::run() {
 	_player.reset(new Player());
 	_globalUI.reset(new GlobalUI());
 	_menu.reset(new Menu());
+	setMillis(0);
 
 	_script->createProcess(MainCharacterKind::None, "CREDITOS_INICIALES");
 	_scheduler.run();
@@ -202,6 +203,36 @@ void AlcachofaEngine::setDebugMode(DebugMode mode, int32 param) {
 	_input.toggleDebugInput(isDebugModeActive());
 }
 
+uint32 AlcachofaEngine::getMillis() const {
+	// Time is stored in savestate at various points e.g. to persist animation progress
+	// We wrap the system-provided time to offset it to the expected game-time
+	// This would also double as playtime
+	return g_system->getMillis() - _timeNegOffset + _timePosOffset;
+}
+
+void AlcachofaEngine::setMillis(uint32 newMillis) {
+	const uint32 sysMillis = g_system->getMillis();
+	if (newMillis > sysMillis) {
+		_timeNegOffset = 0;
+		_timePosOffset = newMillis - sysMillis;
+	}
+	else {
+		_timeNegOffset = sysMillis - newMillis;
+		_timePosOffset = 0;
+	}
+}
+
+void AlcachofaEngine::pauseEngineIntern(bool pause) {
+	// Audio::Mixer also implements recursive pausing,
+	// so ScummVM pausing and Menu pausing will not conflict
+	_sounds.pauseAll(pause);
+
+	if (pause)
+		_timeBeforePause = getMillis();
+	else
+		setMillis(_timeBeforePause);
+}
+
 Common::Error AlcachofaEngine::syncGame(Common::Serializer &s) {
 	// The Serializer has methods isLoading() and isSaving()
 	// if you need to specific steps; for example setting
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index 812ac68ee86..5d923b3586d 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -105,6 +105,9 @@ public:
 	inline Config &config() { return _config; }
 	inline bool isDebugModeActive() const { return _debugHandler != nullptr; }
 
+	uint32 getMillis() const;
+	void setMillis(uint32 newMillis);
+	virtual void pauseEngineIntern(bool pause);
 	void playVideo(int32 videoId);
 	void fadeExit();
 	void setDebugMode(DebugMode debugMode, int32 param);
@@ -168,6 +171,9 @@ private:
 	Sounds _sounds;
 	Scheduler _scheduler;
 	Config _config;
+
+	uint32 _timeNegOffset = 0, _timePosOffset = 0;
+	uint32 _timeBeforePause = 0;
 };
 
 extern AlcachofaEngine *g_engine;
diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index caa76b97db8..ec35e9ff70a 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -51,7 +51,7 @@ void Camera::setRoomBounds(Point bgSize, int16 bgScale) {
 void Camera::setFollow(WalkingCharacter *target, bool catchUp) {
 	_cur._isFollowingTarget = target != nullptr;
 	_followTarget = target;
-	_lastUpdateTime = g_system->getMillis();
+	_lastUpdateTime = g_engine->getMillis();
 	_catchUp = catchUp;
 	if (target == nullptr)
 		_isChanging = false;
@@ -162,7 +162,7 @@ Point Camera::transform3Dto2D(Point p3d) const {
 
 void Camera::update() {
 	// original would be some smoothing of delta times, let's not.
-	uint32 now = g_system->getMillis();
+	uint32 now = g_engine->getMillis();
 	float deltaTime = (now - _lastUpdateTime) / 1000.0f;
 	deltaTime = MAX(0.001f, MIN(0.5f, deltaTime));
 	_lastUpdateTime = now;
@@ -239,9 +239,9 @@ struct CamLerpTask : public Task {
 
 	virtual TaskReturn run() override {
 		TASK_BEGIN;
-		_startTime = g_system->getMillis();
-		while (g_system->getMillis() - _startTime < _duration) {
-			update(ease((g_system->getMillis() - _startTime) / (float)_duration, _easingType));
+		_startTime = g_engine->getMillis();
+		while (g_engine->getMillis() - _startTime < _duration) {
+			update(ease((g_engine->getMillis() - _startTime) / (float)_duration, _easingType));
 			_camera._isChanging = true;
 			TASK_YIELD;
 		}
@@ -250,8 +250,8 @@ struct CamLerpTask : public Task {
 	}
 
 	virtual void debugPrint() override {
-		uint32 remaining = g_system->getMillis() - _startTime <= _duration
-			? _duration - (g_system->getMillis() - _startTime)
+		uint32 remaining = g_engine->getMillis() - _startTime <= _duration
+			? _duration - (g_engine->getMillis() - _startTime)
 			: 0;
 		g_engine->console().debugPrintf("%s camera with %ums remaining\n", taskName(), remaining);
 	}
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 903002568a1..0ae1d957338 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -144,11 +144,11 @@ CursorType Door::cursorType() const {
 }
 
 void Door::onClick() {
-	if (g_system->getMillis() - _lastClickTime < 500 && g_engine->player().activeCharacter()->clearTargetIf(this))
+	if (g_engine->getMillis() - _lastClickTime < 500 && g_engine->player().activeCharacter()->clearTargetIf(this))
 		trigger(nullptr);
 	else {
 		InteractableObject::onClick();
-		_lastClickTime = g_system->getMillis();
+		_lastClickTime = g_engine->getMillis();
 	}
 }
 
@@ -422,11 +422,11 @@ struct LerpLodBiasTask final : public Task {
 
 	virtual TaskReturn run() override {
 		TASK_BEGIN;
-		_startTime = g_system->getMillis();
+		_startTime = g_engine->getMillis();
 		_sourceLodBias = _character->lodBias();
-		while (g_system->getMillis() - _startTime < _durationMs) {
+		while (g_engine->getMillis() - _startTime < _durationMs) {
 			_character->lodBias() = _sourceLodBias + (_targetLodBias - _sourceLodBias) *
-				((g_system->getMillis() - _startTime) / (float)_durationMs);
+				((g_engine->getMillis() - _startTime) / (float)_durationMs);
 			TASK_YIELD;
 		}
 		_character->lodBias() = _targetLodBias;
@@ -434,8 +434,8 @@ struct LerpLodBiasTask final : public Task {
 	}
 
 	virtual void debugPrint() override {
-		uint32 remaining = g_system->getMillis() - _startTime <= _durationMs
-			? _durationMs - (g_system->getMillis() - _startTime)
+		uint32 remaining = g_engine->getMillis() - _startTime <= _durationMs
+			? _durationMs - (g_engine->getMillis() - _startTime)
 			: 0;
 		g_engine->console().debugPrintf("Lerp lod bias of %s to %f with %ums remaining\n",
 			_character->name().c_str(), _targetLodBias, remaining);
@@ -584,7 +584,7 @@ void WalkingCharacter::updateWalkingAnimation() {
 
 	// this is very confusing. Let's see what it does
 	const int32 halfFrameCount = (int32)animation->frameCount() / 2;
-	int32 expectedFrame = (int32)(g_system->getMillis() - _graphicNormal.lastTime()) * 12 / 1000;
+	int32 expectedFrame = (int32)(g_engine->getMillis() - _graphicNormal.lastTime()) * 12 / 1000;
 	const bool isUnexpectedFrame = expectedFrame != _lastWalkAnimFrame;
 	int32 stepFrameFrom, stepFrameTo;
 	if (expectedFrame < halfFrameCount - 1) {
diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index 4c5e2448c23..5e0e2d1060d 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -176,7 +176,7 @@ SpecialEffectObject::SpecialEffectObject(Room *room, ReadStream &stream)
 void SpecialEffectObject::draw() {
 	if (!isEnabled()) // TODO: Add high quality check
 		return;
-	const auto texOffset = g_system->getMillis() * 0.001f * _texShift;
+	const auto texOffset = g_engine->getMillis() * 0.001f * _texShift;
 	const BlendMode blendMode = _type == GraphicObjectType::Effect
 		? BlendMode::Additive
 		: BlendMode::AdditiveAlpha;
diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index 9cf45228071..5c2b5dbaa30 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -62,7 +62,7 @@ GlobalUI::GlobalUI() {
 void GlobalUI::startClosingInventory() {
 	_isOpeningInventory = false;
 	_isClosingInventory = true;
-	_timeForInventory = g_system->getMillis();
+	_timeForInventory = g_engine->getMillis();
 	updateClosingInventory(); // prevents the first frame of closing to not render the inventory overlay
 }
 
@@ -70,7 +70,7 @@ void GlobalUI::updateClosingInventory() {
 	static constexpr uint32 kDuration = 300;
 	static constexpr float kSpeed = -10 / 3.0f / 1000.0f;
 
-	uint32 deltaTime = g_system->getMillis() - _timeForInventory;
+	uint32 deltaTime = g_engine->getMillis() - _timeForInventory;
 	if (!_isClosingInventory || deltaTime >= kDuration)
 		_isClosingInventory = false;
 	else
@@ -83,7 +83,7 @@ bool GlobalUI::updateOpeningInventory() {
 		return false;
 
 	if (_isOpeningInventory) {
-		uint32 deltaTime = g_system->getMillis() - _timeForInventory;
+		uint32 deltaTime = g_engine->getMillis() - _timeForInventory;
 		if (deltaTime >= 1000) {
 			_isOpeningInventory = false;
 			g_engine->world().inventory().open();
@@ -97,7 +97,7 @@ bool GlobalUI::updateOpeningInventory() {
 	else if (openInventoryTriggerBounds().contains(g_engine->input().mousePos2D())) {
 		_isClosingInventory = false;
 		_isOpeningInventory = true;
-		_timeForInventory = g_system->getMillis();
+		_timeForInventory = g_engine->getMillis();
 		g_engine->player().activeCharacter()->stopWalking();
 		g_engine->world().inventory().updateItemsByActiveCharacter();
 		return true;
@@ -206,8 +206,8 @@ struct CenterBottomTextTask : public Task {
 		);
 
 		TASK_BEGIN;
-		_startTime = g_system->getMillis();
-		while (g_system->getMillis() - _startTime < _durationMs) {
+		_startTime = g_engine->getMillis();
+		while (g_engine->getMillis() - _startTime < _durationMs) {
 			if (process().isActiveForPlayer()) {
 				g_engine->drawQueue().add<TextDrawRequest>(
 					font, text, pos, -1, true, kWhite, 1);
@@ -218,8 +218,8 @@ struct CenterBottomTextTask : public Task {
 	}
 
 	void debugPrint() override {
-		uint32 remaining = g_system->getMillis() - _startTime <= _durationMs
-			? _durationMs - (g_system->getMillis() - _startTime)
+		uint32 remaining = g_engine->getMillis() - _startTime <= _durationMs
+			? _durationMs - (g_engine->getMillis() - _startTime)
 			: 0;
 		g_engine->console().debugPrintf("CenterBottomText (%d) with %ums remaining\n", _dialogId, remaining);
 	}
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 4da9cba4a89..3ccf06c6258 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -507,7 +507,7 @@ void Graphic::update() {
 	const uint32 totalDuration = _animation->totalDuration();
 	uint32 curTime = _isPaused
 		? _lastTime
-		: g_system->getMillis() - _lastTime;
+		: g_engine->getMillis() - _lastTime;
 	if (curTime > totalDuration) {
 		if (_isLooping && totalDuration > 0)
 			curTime %= totalDuration;
@@ -524,18 +524,18 @@ void Graphic::update() {
 void Graphic::start(bool isLooping) {
 	_isPaused = false;
 	_isLooping = isLooping;
-	_lastTime = g_system->getMillis();
+	_lastTime = g_engine->getMillis();
 }
 
 void Graphic::pause() {
 	_isPaused = true;
 	_isLooping = false;
-	_lastTime = g_system->getMillis() - _lastTime;
+	_lastTime = g_engine->getMillis() - _lastTime;
 }
 
 void Graphic::reset() {
 	_frameI = 0;
-	_lastTime = _isPaused ? 0 : g_system->getMillis();
+	_lastTime = _isPaused ? 0 : g_engine->getMillis();
 }
 
 void Graphic::setAnimation(const Common::String &fileName, AnimationFolder folder) {
@@ -782,9 +782,9 @@ struct FadeTask : public Task {
 		TASK_BEGIN;
 		if (_permanentFadeAction == PermanentFadeAction::UnsetFaded)
 			g_engine->globalUI().isPermanentFaded() = false;
-		_startTime = g_system->getMillis();
-		while (g_system->getMillis() - _startTime < _duration) {
-			draw((g_system->getMillis() - _startTime) / (float)_duration);
+		_startTime = g_engine->getMillis();
+		while (g_engine->getMillis() - _startTime < _duration) {
+			draw((g_engine->getMillis() - _startTime) / (float)_duration);
 			TASK_YIELD;
 		}
 		draw(1.0f); // so that during a loading lag the screen is completly black/white
@@ -794,8 +794,8 @@ struct FadeTask : public Task {
 	}
 
 	virtual void debugPrint() override {
-		uint32 remaining = g_system->getMillis() - _startTime <= _duration
-			? _duration - (g_system->getMillis() - _startTime)
+		uint32 remaining = g_engine->getMillis() - _startTime <= _duration
+			? _duration - (g_engine->getMillis() - _startTime)
 			: 0;
 		g_engine->console().debugPrintf("Fade (%d) from %.2f to %.2f with %ums remaining\n", (int)_fadeType, _from, _to, remaining);
 	}
diff --git a/engines/alcachofa/menu.cpp b/engines/alcachofa/menu.cpp
index 8dfac1bd136..b0abf19233c 100644
--- a/engines/alcachofa/menu.cpp
+++ b/engines/alcachofa/menu.cpp
@@ -36,7 +36,7 @@ void Menu::updateOpeningMenu() {
 	_openAtNextFrame = false;
 
 	g_engine->sounds().pauseAll(true);
-	// TODO: Add game time behaviour on opening menu
+	_timeBeforeMenu = g_engine->getMillis();
 	_previousRoom = g_engine->player().currentRoom();
 	_isOpen = true;
 	// TODO: Render thumbnail
@@ -60,7 +60,7 @@ void Menu::continueGame() {
 	g_engine->sounds().pauseAll(false);
 	g_engine->camera().restore(1);
 	g_engine->scheduler().restoreContext();
-	// TODO: Reset time on continueing game
+	g_engine->setMillis(_timeBeforeMenu);
 }
 
 void Menu::triggerMainMenuAction(MainMenuAction action) {
diff --git a/engines/alcachofa/menu.h b/engines/alcachofa/menu.h
index f4293862852..712a74ff219 100644
--- a/engines/alcachofa/menu.h
+++ b/engines/alcachofa/menu.h
@@ -75,6 +75,7 @@ private:
 	bool
 		_isOpen = false,
 		_openAtNextFrame = false;
+	uint32 _timeBeforeMenu = 0;
 	Room *_previousRoom = nullptr;
 };
 
diff --git a/engines/alcachofa/scheduler.cpp b/engines/alcachofa/scheduler.cpp
index 822ba5487c9..982515ea664 100644
--- a/engines/alcachofa/scheduler.cpp
+++ b/engines/alcachofa/scheduler.cpp
@@ -62,14 +62,14 @@ DelayTask::DelayTask(Process &process, uint32 millis)
 
 TaskReturn DelayTask::run() {
 	TASK_BEGIN;
-	_endTime += g_system->getMillis();
-	while (g_system->getMillis() < _endTime)
+	_endTime += g_engine->getMillis();
+	while (g_engine->getMillis() < _endTime)
 		TASK_YIELD;
 	TASK_END;
 }
 
 void DelayTask::debugPrint() {
-	uint32 remaining = g_system->getMillis() <= _endTime ? _endTime - g_system->getMillis() : 0;
+	uint32 remaining = g_engine->getMillis() <= _endTime ? _endTime - g_engine->getMillis() : 0;
 	g_engine->getDebugger()->debugPrintf("Delay for further %ums\n", remaining);
 }
 
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 74efcdc26c0..61f05ba0833 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -133,7 +133,7 @@ struct ScriptTimerTask : public Task {
 		TASK_BEGIN;
 		{
 			uint32 timeSinceTimer = g_engine->script()._scriptTimer == 0 ? 0
-				: (g_system->getMillis() - g_engine->script()._scriptTimer) / 1000;
+				: (g_engine->getMillis() - g_engine->script()._scriptTimer) / 1000;
 			if (_durationSec >= (int32)timeSinceTimer)
 				_result = g_engine->script().variable("SeHaPulsadoRaton") ? 0 : 2;
 			else
@@ -914,7 +914,7 @@ void Script::updateCommonVariables() {
 
 	if (variable("CalcularTiempoSinPulsarRaton")) {
 		if (_scriptTimer == 0)
-			_scriptTimer = g_system->getMillis();
+			_scriptTimer = g_engine->getMillis();
 	}
 	else
 		_scriptTimer = 0;
diff --git a/engines/alcachofa/ui-objects.cpp b/engines/alcachofa/ui-objects.cpp
index 10e733fcdf2..482312fb9ef 100644
--- a/engines/alcachofa/ui-objects.cpp
+++ b/engines/alcachofa/ui-objects.cpp
@@ -196,7 +196,7 @@ void CheckBox::draw() {
 void CheckBox::update() {
 	PhysicalObject::update();
 	if (_wasClicked) {
-		if (g_system->getMillis() - _clickTime > 500) {
+		if (g_engine->getMillis() - _clickTime > 500) {
 			_wasClicked = false;
 			trigger();
 		}
@@ -226,7 +226,7 @@ void CheckBox::onHoverUpdate() {}
 
 void CheckBox::onClick() {
 	_wasClicked = true;
-	_clickTime = g_system->getMillis();
+	_clickTime = g_engine->getMillis();
 }
 
 void CheckBox::trigger() {


Commit: 7e819190f4c6d0801a94f6b2087e51cba978fc4a
    https://github.com/scummvm/scummvm/commit/7e819190f4c6d0801a94f6b2087e51cba978fc4a
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:57+02:00

Commit Message:
ALCACHOFA: Add most of syncGame subroutines

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


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 0b651c154e4..3dfc51f9e7d 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -233,13 +233,32 @@ void AlcachofaEngine::pauseEngineIntern(bool pause) {
 		setMillis(_timeBeforePause);
 }
 
-Common::Error AlcachofaEngine::syncGame(Common::Serializer &s) {
-	// The Serializer has methods isLoading() and isSaving()
-	// if you need to specific steps; for example setting
-	// an array size after reading it's length, whereas
-	// for saving it would write the existing array's length
-	int dummy = 0;
-	s.syncAsUint32LE(dummy);
+Common::Error AlcachofaEngine::syncGame(Serializer &s) {
+	s.syncVersion((Serializer::Version)SaveVersion::Initial);
+
+	uint32 millis = getMillis();
+	s.syncAsUint32LE(millis);
+	if (s.isLoading())
+		setMillis(millis);
+
+	/* Some notes about the order:
+	 * 1. The scheduler should come first due to our FakeSemaphore
+	 *    By destructing all previous processes we also release all locks and
+	 *    can assert that the semaphores are released on loading.
+	 * 2. The player should come last as it changes the room
+	 */
+
+	//scheduler().syncGame(s);
+	world().syncGame(s);
+	camera().syncGame(s);
+	script().syncGame(s);
+	globalUI().syncGame(s);
+	player().syncGame(s);
+
+	if (s.isLoading()) {
+		sounds().stopAll();
+		sounds().setMusicToRoom(player().currentRoom()->musicID());
+	}
 
 	return Common::kNoError;
 }
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index 5d923b3586d..2c30ae6496d 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -55,6 +55,10 @@ class Menu;
 class Game;
 struct AlcachofaGameDescription;
 
+enum class SaveVersion : Common::Serializer::Version {
+	Initial = 0
+};
+
 class Config {
 public:
 	Config();
@@ -140,12 +144,7 @@ public:
 		return true;
 	}
 
-	/**
-	 * Uses a serializer to allow implementing savegame
-	 * loading and saving using a single method
-	 */
 	Common::Error syncGame(Common::Serializer &s);
-
 	Common::Error saveGameStream(Common::WriteStream *stream, bool isAutosave = false) override {
 		Common::Serializer s(nullptr, stream);
 		return syncGame(s);
diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index ec35e9ff70a..109c62cdb74 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -230,6 +230,60 @@ void Camera::updateFollowing(float deltaTime) {
 	}
 }
 
+static void syncMatrix(Serializer &s, Matrix4 &m) {
+	float *data = m.getData();
+	for (int i = 0; i < 16; i++)
+		s.syncAsFloatLE(data[i]);
+}
+
+static void syncVector(Serializer &s, Vector3d &v) {
+	s.syncAsFloatLE(v.x());
+	s.syncAsFloatLE(v.y());
+	s.syncAsFloatLE(v.z());
+}
+
+void Camera::State::syncGame(Serializer &s) {
+	syncVector(s, _usedCenter);
+	s.syncAsFloatLE(_scale);
+	s.syncAsFloatLE(_speed);
+	s.syncAsFloatLE(_maxSpeedFactor);
+	float rotationDegs = _rotation.getDegrees();
+	s.syncAsFloatLE(rotationDegs);
+	_rotation.setDegrees(rotationDegs);
+	s.syncAsByte(_isBraking);
+	s.syncAsByte(_isFollowingTarget);
+}
+
+void Camera::syncGame(Serializer &s) {
+	syncMatrix(s, _mat3Dto2D);
+	syncMatrix(s, _mat2Dto3D);
+	syncVector(s, _appliedCenter);
+	s.syncAsUint32LE(_lastUpdateTime);
+	s.syncAsByte(_isChanging);
+	_cur.syncGame(s);
+	for (uint i = 0; i < kStateBackupCount; i++)
+		_backups[i].syncGame(s);
+
+	// originally the follow object is also searched for before changing the room
+	// so that would practically mean only the main characters could be reasonably found
+	// instead we fall back to global search
+	String name;
+	if (_followTarget != nullptr)
+		name = _followTarget->name();
+	s.syncString(name);
+	if (s.isLoading()) {
+		if (name.empty())
+			_followTarget = nullptr;
+		else {
+			_followTarget = dynamic_cast<WalkingCharacter *>(g_engine->world().getObjectByName(name.c_str()));
+			if (_followTarget == nullptr)
+				_followTarget = dynamic_cast<WalkingCharacter *>(g_engine->world().getObjectByNameFromAnyRoom(name.c_str()));
+			if (_followTarget == nullptr)
+				warning("Camera follow target from savestate was not found: %s", name.c_str());
+		}
+	}
+}
+
 struct CamLerpTask : public Task {
 	CamLerpTask(Process &process, uint32 duration, EasingType easingType)
 		: Task(process)
diff --git a/engines/alcachofa/camera.h b/engines/alcachofa/camera.h
index 6e18e069003..f3099eef5d0 100644
--- a/engines/alcachofa/camera.h
+++ b/engines/alcachofa/camera.h
@@ -51,6 +51,7 @@ public:
 	void setPosition(Math::Vector3d v);
 	void backup(uint slot);
 	void restore(uint slot);
+	void syncGame(Common::Serializer &s);
 
 	Task *lerpPos(Process &process,
 		Math::Vector2d targetPos,
@@ -95,6 +96,8 @@ private:
 		Math::Angle _rotation;
 		bool _isBraking = false;
 		bool _isFollowingTarget = false;
+
+		void syncGame(Common::Serializer &s);
 	};
 
 	static constexpr uint kStateBackupCount = 2;
diff --git a/engines/alcachofa/common.cpp b/engines/alcachofa/common.cpp
index 4d418c3a230..57be9a3d27e 100644
--- a/engines/alcachofa/common.cpp
+++ b/engines/alcachofa/common.cpp
@@ -42,6 +42,16 @@ FakeSemaphore::~FakeSemaphore() {
 	assert(_counter == 0);
 }
 
+void FakeSemaphore::sync(Serializer &s, FakeSemaphore &semaphore) {
+	// if we are still holding locks during loading these locks will
+	// try to decrease the counter which will fail, let's find this out already here
+	assert(s.isSaving() || semaphore.isReleased());
+
+	uint semaphoreCounter = semaphore.counter();
+	s.syncAsSint32LE(semaphoreCounter);
+	semaphore = FakeSemaphore(semaphoreCounter);
+}
+
 FakeLock::FakeLock() : _semaphore(nullptr) {}
 
 FakeLock::FakeLock(FakeSemaphore &semaphore) : _semaphore(&semaphore) {
diff --git a/engines/alcachofa/common.h b/engines/alcachofa/common.h
index 5d7910e5ac9..02f38cedc6c 100644
--- a/engines/alcachofa/common.h
+++ b/engines/alcachofa/common.h
@@ -88,6 +88,8 @@ struct FakeSemaphore {
 
 	inline bool isReleased() const { return _counter == 0; }
 	inline uint counter() const { return _counter; }
+
+	static void sync(Common::Serializer &s, FakeSemaphore &semaphore);
 private:
 	friend struct FakeLock;
 	uint _counter = 0;
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 0ae1d957338..42af33c97c2 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -233,26 +233,17 @@ void Character::freeResources() {
 	_graphicTalking.freeResources();
 }
 
-void Character::serializeSave(Serializer &serializer) {
-	ShapeObject::serializeSave(serializer);
+void Character::syncGame(Serializer &serializer) {
+	ShapeObject::syncGame(serializer);
 	serializer.syncAsByte(_isTalking);
 	serializer.syncAsSint32LE(_curDialogId);
-	_graphicNormal.serializeSave(serializer);
-	_graphicTalking.serializeSave(serializer);
+	_graphicNormal.syncGame(serializer);
+	_graphicTalking.syncGame(serializer);
 	syncObjectAsString(serializer, _curAnimateObject);
 	syncObjectAsString(serializer, _curTalkingObject);
 	serializer.syncAsFloatLE(_lodBias);
 }
 
-Graphic *Character::graphic() {
-	Graphic *activeGraphic = graphicOf(_curAnimateObject);
-	if (activeGraphic == nullptr && (_isTalking || g_engine->world().somebodyUsing(this)))
-		activeGraphic = graphicOf(_curTalkingObject, &_graphicTalking);
-	if (activeGraphic == nullptr)
-		activeGraphic = &_graphicNormal;
-	return activeGraphic;
-}
-
 void Character::syncObjectAsString(Serializer &serializer, ObjectBase *&object) {
 	String name;
 	if (serializer.isSaving() && object != nullptr)
@@ -274,6 +265,15 @@ void Character::syncObjectAsString(Serializer &serializer, ObjectBase *&object)
 	}
 }
 
+Graphic *Character::graphic() {
+	Graphic *activeGraphic = graphicOf(_curAnimateObject);
+	if (activeGraphic == nullptr && (_isTalking || g_engine->world().somebodyUsing(this)))
+		activeGraphic = graphicOf(_curTalkingObject, &_graphicTalking);
+	if (activeGraphic == nullptr)
+		activeGraphic = &_graphicNormal;
+	return activeGraphic;
+}
+
 void Character::onClick() {
 	ITriggerableObject::onClick();
 	onHoverUpdate();
@@ -717,8 +717,8 @@ void WalkingCharacter::freeResources() {
 	}
 }
 
-void WalkingCharacter::serializeSave(Serializer &serializer) {
-	Character::serializeSave(serializer);
+void WalkingCharacter::syncGame(Serializer &serializer) {
+	Character::syncGame(serializer);
 	serializer.syncAsSint32LE(_lastWalkAnimFrame);
 	serializer.syncAsSint32LE(_walkedDistance);
 	syncPoint(serializer, _sourcePos);
@@ -880,7 +880,7 @@ void syncDialogMenuLine(Serializer &serializer, DialogMenuLine &line) {
 	serializer.syncAsSint32LE(line._returnValue);
 }
 
-void MainCharacter::serializeSave(Serializer &serializer) {
+void MainCharacter::syncGame(Serializer &serializer) {
 	String roomName = room()->name();
 	serializer.syncString(roomName);
 	if (serializer.isLoading()) {
@@ -890,10 +890,8 @@ void MainCharacter::serializeSave(Serializer &serializer) {
 			error("Invalid room name \"%s\" saved for \"%s\"", roomName.c_str(), name().c_str());
 	}
 
-	Character::serializeSave(serializer);
-	uint semaphoreCounter = _semaphore.counter();
-	serializer.syncAsSint32LE(semaphoreCounter);
-	_semaphore = FakeSemaphore(semaphoreCounter);
+	WalkingCharacter::syncGame(serializer);
+	FakeSemaphore::sync(serializer, _semaphore);
 	syncArray(serializer, _dialogLines, syncDialogMenuLine);
 	syncObjectAsString(serializer, _currentlyUsingObject);
 
diff --git a/engines/alcachofa/game.cpp b/engines/alcachofa/game.cpp
index f9a041cec4c..f55fc805246 100644
--- a/engines/alcachofa/game.cpp
+++ b/engines/alcachofa/game.cpp
@@ -94,7 +94,7 @@ void Game::unknownFadeType(int fadeType) {
 }
 
 void Game::unknownSerializedObject(const char *object, const char *owner, const char *room) {
-	// potentially game-breaking for _currentlyUsingObject but should otherwise be just a graphical bug
+	// potentially game-breaking for _currentlyUsingObject but might otherwise be just a graphical bug
 	_message("Invalid object name \"%s\" saved for \"%s\" in \"%s\"", object, owner, room);
 }
 
diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index 5e0e2d1060d..e55d08fd2ee 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -66,7 +66,7 @@ void ObjectBase::loadResources() {
 void ObjectBase::freeResources() {
 }
 
-void ObjectBase::serializeSave(Serializer &serializer) {
+void ObjectBase::syncGame(Serializer &serializer) {
 	serializer.syncAsByte(_isEnabled);
 }
 
@@ -120,9 +120,9 @@ void GraphicObject::freeResources() {
 	_graphic.freeResources();
 }
 
-void GraphicObject::serializeSave(Serializer &serializer) {
-	ObjectBase::serializeSave(serializer);
-	_graphic.serializeSave(serializer);
+void GraphicObject::syncGame(Serializer &serializer) {
+	ObjectBase::syncGame(serializer);
+	_graphic.syncGame(serializer);
 }
 
 Graphic *GraphicObject::graphic() {
@@ -207,8 +207,10 @@ void ShapeObject::update() {
 	}
 }
 
-void ShapeObject::serializeSave(Serializer &serializer) {
+void ShapeObject::syncGame(Serializer &serializer) {
 	serializer.syncAsSByte(_order);
+	_isNewlySelected = false;
+	_wasSelected = false;
 }
 
 Shape *ShapeObject::shape() {
diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index 5c2b5dbaa30..fbb13b4d6a7 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -248,4 +248,8 @@ void GlobalUI::drawScreenStates() {
 	}
 }
 
+void GlobalUI::syncGame(Serializer &s) {
+	s.syncAsByte(_isPermanentFaded);
+}
+
 }
diff --git a/engines/alcachofa/global-ui.h b/engines/alcachofa/global-ui.h
index 8892ebf3e27..44964edeb6b 100644
--- a/engines/alcachofa/global-ui.h
+++ b/engines/alcachofa/global-ui.h
@@ -43,6 +43,7 @@ public:
 	void updateClosingInventory();
 	void startClosingInventory();
 	void drawScreenStates(); // black borders and/or permanent fade
+	void syncGame(Common::Serializer &s);
 
 private:
 	Animation *activeAnimation() const;
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 3ccf06c6258..8a87cbb65e9 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -547,7 +547,7 @@ void Graphic::setAnimation(Animation *animation) {
 	_animation = animation;
 }
 
-void Graphic::serializeSave(Serializer &serializer) {
+void Graphic::syncGame(Serializer &serializer) {
 	syncPoint(serializer, _topLeft);
 	serializer.syncAsSint16LE(_scale);
 	serializer.syncAsUint32LE(_lastTime);
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 788dc583f3b..a39763f6cfa 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -277,7 +277,7 @@ public:
 	void reset();
 	void setAnimation(const Common::String &fileName, AnimationFolder folder);
 	void setAnimation(Animation *animation); ///< no memory ownership is given, but for prerendering it has to be mutable
-	void serializeSave(Common::Serializer &serializer);
+	void syncGame(Common::Serializer &serializer);
 
 private:
 	friend class AnimationDrawRequest;
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index c47ba4fbd4c..5b1f552eb27 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -51,7 +51,7 @@ public:
 	virtual void update();
 	virtual void loadResources();
 	virtual void freeResources();
-	virtual void serializeSave(Common::Serializer &serializer);
+	virtual void syncGame(Common::Serializer &serializer);
 	virtual Graphic *graphic();
 	virtual Shape *shape();
 	virtual const char *typeName() const;
@@ -91,7 +91,7 @@ public:
 	virtual void draw() override;
 	virtual void loadResources() override;
 	virtual void freeResources() override;
-	virtual void serializeSave(Common::Serializer &serializer) override;
+	virtual void syncGame(Common::Serializer &serializer) override;
 	virtual Graphic *graphic() override;
 	virtual const char *typeName() const;
 
@@ -129,7 +129,7 @@ public:
 	inline bool wasSelected() const { return _wasSelected; }
 
 	virtual void update() override;
-	virtual void serializeSave(Common::Serializer &serializer) override;
+	virtual void syncGame(Common::Serializer &serializer) override;
 	virtual Shape *shape() override;
 	virtual CursorType cursorType() const;
 	virtual void onHoverStart();
@@ -423,7 +423,7 @@ public:
 	virtual void drawDebug() override;
 	virtual void loadResources() override;
 	virtual void freeResources() override;
-	virtual void serializeSave(Common::Serializer &serializer) override;
+	virtual void syncGame(Common::Serializer &serializer) override;
 	virtual Graphic *graphic() override;
 	virtual void onClick() override;
 	virtual void trigger(const char *action) override;
@@ -470,7 +470,7 @@ public:
 	virtual void drawDebug() override;
 	virtual void loadResources() override;
 	virtual void freeResources() override;
-	virtual void serializeSave(Common::Serializer &serializer) override;
+	virtual void syncGame(Common::Serializer &serializer) override;
 	virtual void walkTo(
 		Common::Point target,
 		Direction endDirection = Direction::Invalid,
@@ -538,7 +538,7 @@ public:
 
 	virtual void update() override;
 	virtual void draw() override;
-	virtual void serializeSave(Common::Serializer &serializer) override;
+	virtual void syncGame(Common::Serializer &serializer) override;
 	virtual const char *typeName() const;
 	virtual void walkTo(
 		Common::Point target,
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index 4ec1cddde44..015d60cb85f 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -304,4 +304,46 @@ bool Player::isAllowedToOpenMenu() {
 		!g_engine->script().variable("prohibirESC");
 }
 
+void Player::syncGame(Serializer &s) {
+	auto characterKind = activeCharacterKind();
+	syncEnum(s, characterKind);
+	switch (characterKind) {
+	case MainCharacterKind::None:
+		_activeCharacter = nullptr;
+		break;
+	case MainCharacterKind::Mortadelo:
+	case MainCharacterKind::Filemon:
+		_activeCharacter = &g_engine->world().getMainCharacterByKind(characterKind);
+		break;
+	default:
+		error("Invalid character kind in savestate: %d", (int)characterKind);
+	}
+
+	FakeSemaphore::sync(s, _semaphore);
+
+	String roomName;
+	if (_roomBeforeInventory != nullptr)
+		roomName = _roomBeforeInventory->name();
+	s.syncString(roomName);
+	if (s.isLoading()) {
+		if (roomName.empty())
+			_roomBeforeInventory = nullptr;
+		else {
+			_roomBeforeInventory = g_engine->world().getRoomByName(roomName.c_str());
+			scumm_assert(_roomBeforeInventory != nullptr);
+		}
+	}
+
+	roomName = currentRoom()->name();
+	s.syncString(roomName);
+	if (s.isLoading()) {
+		_selectedObject = nullptr;
+		_pressedObject = nullptr;
+		_heldItem = nullptr;
+		_nextLastDialogCharacter = 0;
+		fill(_lastDialogCharacters, _lastDialogCharacters + kMaxLastDialogCharacters, nullptr);
+		changeRoom(roomName, true);
+	}
+}
+
 }
diff --git a/engines/alcachofa/player.h b/engines/alcachofa/player.h
index 2541406d5f1..ee0259c2362 100644
--- a/engines/alcachofa/player.h
+++ b/engines/alcachofa/player.h
@@ -58,6 +58,7 @@ public:
 	void stopLastDialogCharacters();
 	void setActiveCharacter(MainCharacterKind kind);
 	bool isAllowedToOpenMenu();
+	void syncGame(Common::Serializer &s);
 
 private:
 	static constexpr const int kMaxLastDialogCharacters = 4;
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 0b2eb1d2dcf..69cc16a4fde 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -302,11 +302,10 @@ void Room::freeResources() {
 		object->freeResources();
 }
 
-void Room::serializeSave(Serializer &serializer) {
-	serializer.syncAsSByte(_musicId);
+void Room::syncGame(Serializer &serializer) {
 	serializer.syncAsSByte(_activeFloorI);
 	for (auto *object : _objects)
-		object->serializeSave(serializer);
+		object->syncGame(serializer);
 }
 
 void Room::toggleActiveFloor() {
@@ -760,4 +759,9 @@ void World::loadDialogLines() {
 	}
 }
 
+void World::syncGame(Serializer &s) {
+	for (Room *room : _rooms)
+		room->syncGame(s);
+}
+
 }
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index 3f0e0535397..8d753f81aeb 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -59,7 +59,7 @@ public:
 	virtual bool updateInput();
 	virtual void loadResources();
 	virtual void freeResources();
-	virtual void serializeSave(Common::Serializer &serializer);
+	virtual void syncGame(Common::Serializer &serializer);
 	ObjectBase *getObjectByName(const char *name) const;
 	void toggleActiveFloor();
 	void debugPrint(bool withObjects) const;
@@ -183,6 +183,7 @@ public:
 	const char *getDialogLine(int32 dialogId) const;
 
 	void toggleObject(MainCharacterKind character, const char *objName, bool isEnabled);
+	void syncGame(Common::Serializer &s);
 
 private:
 	bool loadWorldFile(const char *path);
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 61f05ba0833..51184d13ba7 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -98,6 +98,14 @@ Script::Script() {
 		_instructions.push_back(ScriptInstruction(file));
 }
 
+static void syncAsSint32LE(Serializer &s, int32 &value) {
+	s.syncAsSint32LE(value);
+}
+
+void Script::syncGame(Serializer &s) {
+	s.syncArray(_variables.data(), _variables.size(), syncAsSint32LE);
+}
+
 int32 Script::variable(const char *name) const {
 	uint32 index;
 	if (_variableNames.tryGetVal(name, index))
diff --git a/engines/alcachofa/script.h b/engines/alcachofa/script.h
index e1b614a4999..0b8337a3430 100644
--- a/engines/alcachofa/script.h
+++ b/engines/alcachofa/script.h
@@ -156,6 +156,7 @@ class Script {
 public:
 	Script();
 
+	void syncGame(Common::Serializer &s);
 	void updateCommonVariables();
 	int32 variable(const char *name) const;
 	int32 &variable(const char *name);


Commit: 3fb9469d2a449bc177a67113397c6f132b9f3615
    https://github.com/scummvm/scummvm/commit/3fb9469d2a449bc177a67113397c6f132b9f3615
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:57+02:00

Commit Message:
ALCACHOFA: Add syncGame for scheduler and all tasks

Changed paths:
  A engines/alcachofa/tasks.h
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/camera.cpp
    engines/alcachofa/common.h
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/general-objects.cpp
    engines/alcachofa/global-ui.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/player.cpp
    engines/alcachofa/rooms.cpp
    engines/alcachofa/scheduler.cpp
    engines/alcachofa/scheduler.h
    engines/alcachofa/script.cpp
    engines/alcachofa/sounds.cpp
    engines/alcachofa/sounds.h


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 3dfc51f9e7d..c9062244fd9 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -242,18 +242,20 @@ Common::Error AlcachofaEngine::syncGame(Serializer &s) {
 		setMillis(millis);
 
 	/* Some notes about the order:
-	 * 1. The scheduler should come first due to our FakeSemaphore
+	 * 1. The scheduler should prepare due to our FakeSemaphores
 	 *    By destructing all previous processes we also release all locks and
 	 *    can assert that the semaphores are released on loading.
-	 * 2. The player should come last as it changes the room
+	 * 2. The player should come late as it changes the room
+	 * 3. With the room current, the tasks can now better find the referenced objects
 	 */
 
-	//scheduler().syncGame(s);
+	scheduler().prepareSyncGame(s);
 	world().syncGame(s);
 	camera().syncGame(s);
 	script().syncGame(s);
 	globalUI().syncGame(s);
 	player().syncGame(s);
+	scheduler().syncGame(s);
 
 	if (s.isLoading()) {
 		sounds().stopAll();
diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index 109c62cdb74..fb4c4de1d87 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -285,7 +285,7 @@ void Camera::syncGame(Serializer &s) {
 }
 
 struct CamLerpTask : public Task {
-	CamLerpTask(Process &process, uint32 duration, EasingType easingType)
+	CamLerpTask(Process &process, uint32 duration = 0, EasingType easingType = EasingType::Linear)
 		: Task(process)
 		, _camera(g_engine->camera())
 		, _duration(duration)
@@ -297,7 +297,7 @@ struct CamLerpTask : public Task {
 		while (g_engine->getMillis() - _startTime < _duration) {
 			update(ease((g_engine->getMillis() - _startTime) / (float)_duration, _easingType));
 			_camera._isChanging = true;
-			TASK_YIELD;
+			TASK_YIELD(1);
 		}
 		update(1.0f);
 		TASK_END;
@@ -310,8 +310,14 @@ struct CamLerpTask : public Task {
 		g_engine->console().debugPrintf("%s camera with %ums remaining\n", taskName(), remaining);
 	}
 
+	virtual void syncGame(Serializer &s) override {
+		Task::syncGame(s);
+		s.syncAsUint32LE(_startTime);
+		s.syncAsUint32LE(_duration);
+		syncEnum(s, _easingType);
+	}
+
 protected:
-	virtual const char *taskName() const = 0;
 	virtual void update(float t) = 0;
 
 	Camera &_camera;
@@ -325,17 +331,27 @@ struct CamLerpPosTask final : public CamLerpTask {
 		, _fromPos(_camera._appliedCenter)
 		, _deltaPos(targetPos - _camera._appliedCenter) {}
 
-protected:
-	virtual const char *taskName() const {
-		return "Lerp pos of";
+	CamLerpPosTask(Process &process, Serializer &s)
+		: CamLerpTask(process) {
+		syncGame(s);
+	}
+
+	virtual void syncGame(Serializer &s) override {
+		CamLerpTask::syncGame(s);
+		syncVector(s, _fromPos);
+		syncVector(s, _deltaPos);
 	}
 
+	virtual const char *taskName() const override;
+
+protected:
 	virtual void update(float t) override {
 		_camera.setPosition(_fromPos + _deltaPos * t);
 	}
 
 	Vector3d _fromPos, _deltaPos;
 };
+DECLARE_TASK(CamLerpPosTask);
 
 struct CamLerpScaleTask final : public CamLerpTask {
 	CamLerpScaleTask(Process &process, float targetScale, int32 duration, EasingType easingType)
@@ -343,17 +359,27 @@ struct CamLerpScaleTask final : public CamLerpTask {
 		, _fromScale(_camera._cur._scale)
 		, _deltaScale(targetScale - _camera._cur._scale) {}
 
-protected:
-	virtual const char *taskName() const {
-		return "Lerp scale of";
+	CamLerpScaleTask(Process &process, Serializer &s)
+		: CamLerpTask(process) {
+		syncGame(s);
+	}
+
+	virtual void syncGame(Serializer &s) override {
+		CamLerpTask::syncGame(s);
+		s.syncAsFloatLE(_fromScale);
+		s.syncAsFloatLE(_deltaScale);
 	}
 
+	virtual const char *taskName() const override;
+
+protected:
 	virtual void update(float t) override {
 		_camera._cur._scale = _fromScale + _deltaScale * t;
 	}
 
 	float _fromScale, _deltaScale;
 };
+DECLARE_TASK(CamLerpScaleTask);
 
 struct CamLerpPosScaleTask final : public CamLerpTask {
 	CamLerpPosScaleTask(Process &process,
@@ -368,11 +394,24 @@ struct CamLerpPosScaleTask final : public CamLerpTask {
 		, _moveEasingType(moveEasingType)
 		, _scaleEasingType(scaleEasingType) {}
 
-protected:
-	virtual const char *taskName() const {
-		return "Lerp pos and scale of";
+	CamLerpPosScaleTask(Process &process, Serializer &s)
+		: CamLerpTask(process) {
+		syncGame(s);
+	}
+
+	virtual void syncGame(Serializer &s) override {
+		CamLerpTask::syncGame(s);
+		syncVector(s, _fromPos);
+		syncVector(s, _deltaPos);
+		s.syncAsFloatLE(_fromScale);
+		s.syncAsFloatLE(_deltaScale);
+		syncEnum(s, _moveEasingType);
+		syncEnum(s, _scaleEasingType);
 	}
 
+	virtual const char *taskName() const override;
+
+protected:
 	virtual void update(float t) override {
 		_camera.setPosition(_fromPos + _deltaPos * ease(t, _moveEasingType));
 		_camera._cur._scale = _fromScale + _deltaScale * ease(t, _scaleEasingType);
@@ -382,6 +421,7 @@ protected:
 	float _fromScale, _deltaScale;
 	EasingType _moveEasingType, _scaleEasingType;
 };
+DECLARE_TASK(CamLerpPosScaleTask);
 
 struct CamLerpRotationTask final : public CamLerpTask {
 	CamLerpRotationTask(Process &process, float targetRotation, int32 duration, EasingType easingType)
@@ -389,17 +429,33 @@ struct CamLerpRotationTask final : public CamLerpTask {
 		, _fromRotation(_camera._cur._rotation.getDegrees())
 		, _deltaRotation(targetRotation - _camera._cur._rotation.getDegrees()) {}
 
-protected:
-	virtual const char *taskName() const {
-		return "Lerp rotation of";
+	CamLerpRotationTask(Process &process, Serializer &s)
+		: CamLerpTask(process) {
+		syncGame(s);
+	}
+
+	virtual void syncGame(Serializer &s) override {
+		CamLerpTask::syncGame(s);
+		s.syncAsFloatLE(_fromRotation);
+		s.syncAsFloatLE(_deltaRotation);
 	}
 
+	virtual const char *taskName() const override;
+
+protected:
 	virtual void update(float t) override {
 		_camera._cur._rotation = Angle(_fromRotation + _deltaRotation * t);
 	}
 
 	float _fromRotation, _deltaRotation;
 };
+DECLARE_TASK(CamLerpRotationTask);
+
+static void syncVector(Serializer &s, Vector2d &v) {
+	float *data = v.getData();
+	s.syncAsFloatLE(data[0]);
+	s.syncAsFloatLE(data[1]);
+}
 
 struct CamShakeTask final : public CamLerpTask {
 	CamShakeTask(Process &process, Vector2d amplitude, Vector2d frequency, int32 duration)
@@ -407,11 +463,20 @@ struct CamShakeTask final : public CamLerpTask {
 		, _amplitude(amplitude)
 		, _frequency(frequency) { }
 
-protected:
-	virtual const char *taskName() const {
-		return "Shake";
+	CamShakeTask(Process &process, Serializer &s)
+		: CamLerpTask(process) {
+		syncGame(s);
 	}
 
+	virtual void syncGame(Serializer &s) override {
+		CamLerpTask::syncGame(s);
+		syncVector(s, _amplitude);
+		syncVector(s, _frequency);
+	}
+
+	virtual const char *taskName() const override;
+
+protected:
 	virtual void update(float t) override {
 		const Vector2d phase = _frequency * t * (float)M_PI * 2.0f;
 		const float amplTimeFactor = 1.0f / expf(t * 5.0f); // a curve starting at 1, depreciating towards 0 
@@ -422,14 +487,20 @@ protected:
 	}
 
 	Vector2d _amplitude, _frequency;
-
 };
+DECLARE_TASK(CamShakeTask);
 
 struct CamWaitToStopTask final : public Task {
 	CamWaitToStopTask(Process &process)
 		: Task(process)
 		, _camera(g_engine->camera()) {}
 
+	CamWaitToStopTask(Process &process, Serializer &s)
+		: Task(process)
+		, _camera(g_engine->camera()) {
+		syncGame(s);
+	}
+
 	virtual TaskReturn run() override {
 		return _camera._isChanging
 			? TaskReturn::yield()
@@ -440,9 +511,12 @@ struct CamWaitToStopTask final : public Task {
 		g_engine->console().debugPrintf("Wait for camera to stop moving\n");
 	}
 
+	virtual const char *taskName() const override;
+
 private:
 	Camera &_camera;
 };
+DECLARE_TASK(CamWaitToStopTask);
 
 struct CamSetInactiveAttributeTask final : public Task {
 	enum Attribute {
@@ -458,6 +532,12 @@ struct CamSetInactiveAttributeTask final : public Task {
 		, _value(value)
 		, _delay(delay) {}
 
+	CamSetInactiveAttributeTask(Process &process, Serializer &s)
+		: Task(process)
+		, _camera(g_engine->camera()) {
+		syncGame(s);
+	}
+
 	virtual TaskReturn run() override {
 		if (_delay > 0) {
 			uint32 delay = (uint32)_delay;
@@ -488,12 +568,22 @@ struct CamSetInactiveAttributeTask final : public Task {
 		g_engine->console().debugPrintf("Set inactive camera %s to %f after %dms\n", attributeName, _value, _delay);
 	}
 
+	virtual void syncGame(Serializer &s) override {
+		Task::syncGame(s);
+		syncEnum(s, _attribute);
+		s.syncAsFloatLE(_value);
+		s.syncAsSint32LE(_delay);
+	}
+
+	virtual const char *taskName() const override;
+
 private:
 	Camera &_camera;
 	Attribute _attribute;
 	float _value;
 	int32 _delay;
 };
+DECLARE_TASK(CamSetInactiveAttributeTask);
 
 Task *Camera::lerpPos(Process &process,
 					  Vector2d targetPos,
diff --git a/engines/alcachofa/common.h b/engines/alcachofa/common.h
index 02f38cedc6c..7631bf81838 100644
--- a/engines/alcachofa/common.h
+++ b/engines/alcachofa/common.h
@@ -103,6 +103,8 @@ struct FakeLock {
 	~FakeLock();
 	void operator = (FakeLock &&other) noexcept;
 	void release();
+	
+	inline bool isReleased() const { return _semaphore == nullptr; }
 private:
 	FakeSemaphore *_semaphore = nullptr;
 };
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 42af33c97c2..5bbeefa21b0 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -292,6 +292,11 @@ struct SayTextTask final : public Task {
 		, _dialogId(dialogId) {
 	}
 
+	SayTextTask(Process &process, Serializer &s)
+		: Task(process) {
+		syncGame(s);
+	}
+
 	virtual TaskReturn run() override {
 		bool isSoundStillPlaying;
 
@@ -324,13 +329,13 @@ struct SayTextTask final : public Task {
 
 			if (!_character->_isTalking) {
 				g_engine->sounds().fadeOut(_soundHandle, 100);
-				TASK_WAIT(delay(200));
+				TASK_WAIT(1, delay(200));
 				TASK_RETURN(0);
 			}
 
 			_character->isSpeaking() = !isSoundStillPlaying ||
 				g_engine->sounds().isNoisy(_soundHandle, 80.0f, 150.0f);
-			TASK_YIELD;
+			TASK_YIELD(2);
 		}
 		TASK_END;
 	}
@@ -339,11 +344,20 @@ struct SayTextTask final : public Task {
 		g_engine->console().debugPrintf("SayText %s, %d\n", _character->name().c_str(), _dialogId);
 	}
 
+	virtual void syncGame(Serializer &s) override {
+		Task::syncGame(s);
+		syncObjectAsString(s, _character);
+		s.syncAsSint32LE(_dialogId);
+	}
+
+	virtual const char *taskName() const override;
+
 private:
 	Character *_character;
 	int32 _dialogId;
 	SoundHandle _soundHandle = {};
 };
+DECLARE_TASK(SayTextTask);
 
 Task *Character::sayText(Process &process, int32 dialogId) {
 	return new SayTextTask(process, this, dialogId);
@@ -376,10 +390,15 @@ struct AnimateCharacterTask final : public Task {
 		scumm_assert(_graphic != nullptr);
 	}
 
+	AnimateCharacterTask(Process &process, Serializer &s)
+		: Task(process) {
+		syncGame(s);
+	}
+
 	virtual TaskReturn run() override {
 		TASK_BEGIN;
 		while (_character->_curAnimateObject != nullptr)
-			TASK_YIELD;
+			TASK_YIELD(1);
 
 		_character->_curAnimateObject = _animateObject;
 		_graphic->start(false);
@@ -387,7 +406,7 @@ struct AnimateCharacterTask final : public Task {
 			_graphic->update();
 		do
 		{
-			TASK_YIELD;
+			TASK_YIELD(2);
 			if (process().isActiveForPlayer() && g_engine->input().wasAnyMouseReleased())
 				_graphic->pause();
 		} while (!_graphic->isPaused());
@@ -401,11 +420,22 @@ struct AnimateCharacterTask final : public Task {
 		g_engine->console().debugPrintf("AnimateCharacter %s, %s\n", _character->name().c_str(), _animateObject->name().c_str());
 	}
 
+	virtual void syncGame(Serializer &s) override {
+		Task::syncGame(s);
+		syncObjectAsString(s, _character);
+		syncObjectAsString(s, _animateObject);
+		_graphic = _animateObject->graphic();
+		scumm_assert(_graphic != nullptr);
+	}
+
+	virtual const char *taskName() const override;
+
 private:
 	Character *_character;
 	ObjectBase *_animateObject;
 	Graphic *_graphic;
 };
+DECLARE_TASK(AnimateCharacterTask);
 
 Task *Character::animate(Process &process, ObjectBase *animateObject) {
 	assert(animateObject != nullptr);
@@ -420,6 +450,11 @@ struct LerpLodBiasTask final : public Task {
 		, _durationMs(durationMs) {
 	}
 
+	LerpLodBiasTask(Process &process, Serializer &s)
+		: Task(process) {
+		syncGame(s);
+	}
+
 	virtual TaskReturn run() override {
 		TASK_BEGIN;
 		_startTime = g_engine->getMillis();
@@ -427,7 +462,7 @@ struct LerpLodBiasTask final : public Task {
 		while (g_engine->getMillis() - _startTime < _durationMs) {
 			_character->lodBias() = _sourceLodBias + (_targetLodBias - _sourceLodBias) *
 				((g_engine->getMillis() - _startTime) / (float)_durationMs);
-			TASK_YIELD;
+			TASK_YIELD(1);
 		}
 		_character->lodBias() = _targetLodBias;
 		TASK_END;
@@ -441,11 +476,23 @@ struct LerpLodBiasTask final : public Task {
 			_character->name().c_str(), _targetLodBias, remaining);
 	}
 
+	virtual void syncGame(Serializer &s) override {
+		Task::syncGame(s);
+		syncObjectAsString(s, _character);
+		s.syncAsFloatLE(_sourceLodBias);
+		s.syncAsFloatLE(_targetLodBias);
+		s.syncAsUint32LE(_startTime);
+		s.syncAsUint32LE(_durationMs);
+	}
+
+	virtual const char *taskName() const override;
+
 private:
 	Character *_character;
 	float _sourceLodBias = 0, _targetLodBias;
 	uint32 _startTime = 0, _durationMs;
 };
+DECLARE_TASK(LerpLodBiasTask);
 
 Task *Character::lerpLodBias(Process &process, float targetLodBias, int32 durationMs) {
 	return new LerpLodBiasTask(process, this, targetLodBias, durationMs);
@@ -729,26 +776,38 @@ void WalkingCharacter::syncGame(Serializer &serializer) {
 }
 
 struct ArriveTask : public Task {
-	ArriveTask(Process &process, const WalkingCharacter &character)
+	ArriveTask(Process &process, const WalkingCharacter *character)
 		: Task(process)
 		, _character(character) {
 	}
 
+	ArriveTask(Process &process, Serializer &s)
+		: Task(process) {
+		syncGame(s);		
+	}
+
 	virtual TaskReturn run() override {
-		return _character.isWalking()
+		return _character->isWalking()
 			? TaskReturn::yield()
 			: TaskReturn::finish(1);
 	}
 
 	virtual void debugPrint() override {
-		g_engine->getDebugger()->debugPrintf("Wait for %s to arrive", _character.name().c_str());
+		g_engine->getDebugger()->debugPrintf("Wait for %s to arrive", _character->name().c_str());
 	}
+
+	virtual void syncGame(Serializer &s) override {
+		syncObjectAsString(s, _character);
+	}
+
+	virtual const char *taskName() const override;
 private:
-	const WalkingCharacter &_character;
+	const WalkingCharacter *_character;
 };
+DECLARE_TASK(ArriveTask);
 
 Task *WalkingCharacter::waitForArrival(Process &process) {
-	return new ArriveTask(process, *this);
+	return new ArriveTask(process, this);
 }
 
 const char *MainCharacter::typeName() const { return "MainCharacter"; }
@@ -983,11 +1042,17 @@ struct DialogMenuTask : public Task {
 		, _character(character) {
 	}
 
+	DialogMenuTask(Process &process, Serializer &s)
+		: Task(process)
+		, _input(g_engine->input()) {
+		syncGame(s);
+	}
+
 	virtual TaskReturn run() override {
 		TASK_BEGIN;
 		layoutLines();
 		while (true) {
-			TASK_YIELD;
+			TASK_YIELD(1);
 			if (g_engine->player().activeCharacter() != _character)
 				continue;
 			g_engine->globalUI().updateChangingCharacter();
@@ -996,8 +1061,8 @@ struct DialogMenuTask : public Task {
 
 			_clickedLineI = updateLines();
 			if (_clickedLineI != UINT_MAX) {
-				TASK_YIELD;
-				TASK_WAIT(_character->sayText(process(), _character->_dialogLines[_clickedLineI]._dialogId));
+				TASK_YIELD(2);
+				TASK_WAIT(3, _character->sayText(process(), _character->_dialogLines[_clickedLineI]._dialogId));
 				int32 returnValue = _character->_dialogLines[_clickedLineI]._returnValue;
 				_character->_dialogLines.clear();
 				TASK_RETURN(returnValue);
@@ -1011,6 +1076,14 @@ struct DialogMenuTask : public Task {
 			_character->name().c_str(), _character->_dialogLines.size());
 	}
 
+	virtual void syncGame(Serializer &s) override {
+		Task::syncGame(s);
+		syncObjectAsString(s, _character);
+		s.syncAsUint32LE(_clickedLineI);
+	}
+
+	virtual const char *taskName() const override;
+
 private:
 	static constexpr int kTextXOffset = 5;
 	static constexpr int kTextYOffset = 10;
@@ -1055,6 +1128,7 @@ private:
 	MainCharacter *_character;
 	uint _clickedLineI = UINT_MAX;
 };
+DECLARE_TASK(DialogMenuTask);
 
 void MainCharacter::addDialogLine(int32 dialogId) {
 	assert(dialogId >= 0);
diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index e55d08fd2ee..6795d6ebf05 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -135,15 +135,20 @@ struct AnimateTask : public Task {
 		, _object(object) {
 		assert(_object != nullptr);
 		_graphic = object->graphic();
-		assert(_graphic != nullptr);
+		scumm_assert(_graphic != nullptr);
 		_duration = _graphic->animation().totalDuration();
 	}
 
+	AnimateTask(Process &process, Serializer &s)
+		: Task(process) {
+		syncGame(s);
+	}
+
 	virtual TaskReturn run() override {
 		TASK_BEGIN;
 		_object->toggle(true);
 		_graphic->start(false);
-		TASK_WAIT(delay(_duration));
+		TASK_WAIT(1, delay(_duration));
 		_object->toggle(false);
 		TASK_END;
 	}
@@ -152,11 +157,23 @@ struct AnimateTask : public Task {
 		g_engine->getDebugger()->debugPrintf("Animate \"%s\" for %ums", _object->name().c_str(), _duration);
 	}
 
+	virtual void syncGame(Serializer &s) override {
+		Task::syncGame(s);
+		s.syncAsUint32LE(_duration);
+		syncObjectAsString(s, _object);
+		_graphic = _object->graphic();
+		scumm_assert(_graphic != nullptr);
+
+	}
+
+	virtual const char *taskName() const override;
+
 private:
 	GraphicObject *_object;
 	Graphic *_graphic;
 	uint32 _duration;
 };
+DECLARE_TASK(AnimateTask);
 
 Task *GraphicObject::animate(Process &process) {
 	return new AnimateTask(process, this);
diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index fbb13b4d6a7..06d7a09dee3 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -197,6 +197,11 @@ struct CenterBottomTextTask : public Task {
 		, _durationMs(durationMs) {
 	}
 
+	CenterBottomTextTask(Process &process, Serializer &s)
+		: Task(process) {
+		syncGame(s);
+	}
+
 	TaskReturn run() override {
 		Font &font = g_engine->globalUI().dialogFont();
 		const char *text = g_engine->world().getDialogLine(_dialogId);
@@ -212,22 +217,32 @@ struct CenterBottomTextTask : public Task {
 				g_engine->drawQueue().add<TextDrawRequest>(
 					font, text, pos, -1, true, kWhite, 1);
 			}
-			TASK_YIELD;
+			TASK_YIELD(1);
 		}
 		TASK_END;
 	}
 
-	void debugPrint() override {
+	virtual void debugPrint() override {
 		uint32 remaining = g_engine->getMillis() - _startTime <= _durationMs
 			? _durationMs - (g_engine->getMillis() - _startTime)
 			: 0;
 		g_engine->console().debugPrintf("CenterBottomText (%d) with %ums remaining\n", _dialogId, remaining);
 	}
 
+	virtual void syncGame(Serializer &s) override {
+		Task::syncGame(s);
+		s.syncAsSint32LE(_dialogId);
+		s.syncAsUint32LE(_startTime);
+		s.syncAsUint32LE(_durationMs);
+	} 
+
+	virtual const char *taskName() const override;
+
 private:
 	int32 _dialogId;
 	uint32 _startTime = 0, _durationMs;
 };
+DECLARE_TASK(CenterBottomTextTask);
 
 Task *showCenterBottomText(Process &process, int32 dialogId, uint32 durationMs) {
 	return new CenterBottomTextTask(process, dialogId, durationMs);
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 8a87cbb65e9..cf47dac4ec5 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -778,6 +778,11 @@ struct FadeTask : public Task {
 		, _permanentFadeAction(permanentFadeAction) {
 	}
 
+	FadeTask(Process &process, Serializer &s)
+		: Task(process) {
+		syncGame(s);
+	}
+
 	virtual TaskReturn run() override {
 		TASK_BEGIN;
 		if (_permanentFadeAction == PermanentFadeAction::UnsetFaded)
@@ -785,7 +790,7 @@ struct FadeTask : public Task {
 		_startTime = g_engine->getMillis();
 		while (g_engine->getMillis() - _startTime < _duration) {
 			draw((g_engine->getMillis() - _startTime) / (float)_duration);
-			TASK_YIELD;
+			TASK_YIELD(1);
 		}
 		draw(1.0f); // so that during a loading lag the screen is completly black/white
 		if (_permanentFadeAction == PermanentFadeAction::SetFaded)
@@ -800,6 +805,20 @@ struct FadeTask : public Task {
 		g_engine->console().debugPrintf("Fade (%d) from %.2f to %.2f with %ums remaining\n", (int)_fadeType, _from, _to, remaining);
 	}
 
+	void syncGame(Serializer &s) override {
+		Task::syncGame(s);
+		syncEnum(s, _fadeType);
+		syncEnum(s, _easingType);
+		syncEnum(s, _permanentFadeAction);
+		s.syncAsFloatLE(_from);
+		s.syncAsFloatLE(_to);
+		s.syncAsUint32LE(_startTime);
+		s.syncAsUint32LE(_duration);
+		s.syncAsSByte(_order);
+	}
+
+	virtual const char *taskName() const override;
+
 private:
 	void draw(float t) {
 		g_engine->drawQueue().add<FadeDrawRequest>(_fadeType, _from + (_to - _from) * ease(t, _easingType), _order);
@@ -812,6 +831,7 @@ private:
 	int8 _order;
 	PermanentFadeAction _permanentFadeAction;
 };
+DECLARE_TASK(FadeTask);
 
 Task *fade(Process &process, FadeType fadeType,
 	float from, float to,
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index 015d60cb85f..a9fecdb049a 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -199,33 +199,25 @@ struct DoorTask : public Task {
 		, _player(g_engine->player())
 		, _targetObject(nullptr)
 		, _targetDirection(Direction::Invalid) {
-		_targetRoom = g_engine->world().getRoomByName(door->targetRoom().c_str());
-		if (_targetRoom == nullptr) {
-			g_engine->game().unknownDoorTargetRoom(door->targetRoom());
-			return;
-		}
-
-		_targetObject = dynamic_cast<InteractableObject *>(_targetRoom->getObjectByName(door->targetObject().c_str()));
-		if (_targetObject == nullptr) {
-			g_engine->game().unknownDoorTargetDoor(door->targetRoom(), door->targetObject());
-			return;
-		}
-		_targetDirection = door->characterDirection();
-
+		findTarget();
 		process.name() = String::format("Door to %s %s", _targetRoom->name().c_str(), _targetObject->name().c_str());
 	}
 
-	virtual TaskReturn run() {
-		FakeLock musicLock;
+	DoorTask(Process &process, Serializer &s)
+		: Task(process)
+		, _player(g_engine->player()) {
+		syncGame(s);
+	}
 
+	virtual TaskReturn run() {
 		TASK_BEGIN;
 		if (_targetRoom == nullptr || _targetObject == nullptr)
 			return TaskReturn::finish(1);
 
-		musicLock = FakeLock(g_engine->sounds().musicSemaphore());
+		_musicLock = FakeLock(g_engine->sounds().musicSemaphore());
 		if (g_engine->sounds().musicID() != _targetRoom->musicID())
 			g_engine->sounds().fadeMusic();
-		TASK_WAIT(fade(process(), FadeType::ToBlack, 0, 1, 500, EasingType::Out, -5));
+		TASK_WAIT(1, fade(process(), FadeType::ToBlack, 0, 1, 500, EasingType::Out, -5));
 		_player.changeRoom(_targetRoom->name(), true);
 
 		if (_targetRoom->fixedCameraOnEntering())
@@ -238,12 +230,12 @@ struct DoorTask : public Task {
 		}
 
 		g_engine->sounds().setMusicToRoom(_targetRoom->musicID());
-		musicLock.release();
+		_musicLock.release();
 
 		if (g_engine->script().createProcess(_character->kind(), "ENTRAR_" + _targetRoom->name(), ScriptFlags::AllowMissing))
-			TASK_YIELD;
+			TASK_YIELD(2);
 		else
-			TASK_WAIT(fade(process(), FadeType::ToBlack, 1, 0, 500, EasingType::Out, -5));
+			TASK_WAIT(3, fade(process(), FadeType::ToBlack, 1, 0, 500, EasingType::Out, -5));
 		TASK_END;
 	}
 
@@ -251,8 +243,41 @@ struct DoorTask : public Task {
 		g_engine->console().debugPrintf("%s\n", process().name().c_str());
 	}
 
+	void syncGame(Serializer &s) override {
+		assert(s.isSaving() || (_lock.isReleased() && _musicLock.isReleased()));
+
+		Task::syncGame(s);
+		syncObjectAsString(s, _sourceDoor);
+		syncObjectAsString(s, _character);
+		bool hasMusicLock = !_musicLock.isReleased();
+		s.syncAsByte(hasMusicLock);
+		if (s.isLoading() && hasMusicLock)
+			_musicLock = FakeLock(g_engine->sounds().musicSemaphore());
+		
+		_lock = FakeLock(_character->semaphore());
+		findTarget();
+	}
+
+	virtual const char *taskName() const override;
+
 private:
-	FakeLock _lock;
+	void findTarget() {
+		_targetRoom = g_engine->world().getRoomByName(_sourceDoor->targetRoom().c_str());
+		if (_targetRoom == nullptr) {
+			g_engine->game().unknownDoorTargetRoom(_sourceDoor->targetRoom());
+			return;
+		}
+
+		_targetObject = dynamic_cast<InteractableObject *>(_targetRoom->getObjectByName(_sourceDoor->targetObject().c_str()));
+		if (_targetObject == nullptr) {
+			g_engine->game().unknownDoorTargetDoor(_sourceDoor->targetRoom(), _sourceDoor->targetObject());
+			return;
+		}
+
+		_targetDirection = _sourceDoor->characterDirection();
+	}
+
+	FakeLock _lock, _musicLock;
 	const Door *_sourceDoor;
 	const InteractableObject *_targetObject;
 	Direction _targetDirection;
@@ -260,6 +285,7 @@ private:
 	MainCharacter *_character;
 	Player &_player;
 };
+DECLARE_TASK(DoorTask);
 
 void Player::triggerDoor(const Door *door) {
 	_heldItem = nullptr;
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 69cc16a4fde..e67f8575e86 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -558,6 +558,9 @@ MainCharacter &World::getOtherMainCharacterByKind(MainCharacterKind kind) const
 }
 
 Room *World::getRoomByName(const char *name) const {
+	assert(name != nullptr);
+	if (*name == '\0')
+		return nullptr;
 	for (auto *room : _rooms) {
 		if (room->name().equalsIgnoreCase(name))
 			return room;
@@ -593,6 +596,9 @@ ObjectBase *World::getObjectByName(MainCharacterKind character, const char *name
 }
 
 ObjectBase *World::getObjectByNameFromAnyRoom(const char *name) const {
+	assert(name != nullptr);
+	if (*name == '\0')
+		return nullptr;
 	for (auto *room : _rooms) {
 		ObjectBase *result = room->getObjectByName(name);
 		if (result != nullptr)
diff --git a/engines/alcachofa/scheduler.cpp b/engines/alcachofa/scheduler.cpp
index 982515ea664..31ab38b8e1a 100644
--- a/engines/alcachofa/scheduler.cpp
+++ b/engines/alcachofa/scheduler.cpp
@@ -55,16 +55,42 @@ Task *Task::delay(uint32 millis) {
 	return new DelayTask(process(), millis);
 }
 
+void Task::syncGame(Serializer &s) {
+	s.syncAsUint32LE(_stage);
+}
+
+void Task::syncObjectAsString(Serializer &s, ObjectBase *&object, bool optional) {
+	String objectName, roomName;
+	if (object != nullptr) {
+		roomName = object->room()->name();
+		objectName = object->name();
+	}
+	s.syncString(roomName);
+	s.syncString(objectName);
+	if (s.isSaving())
+		return;
+	Room *room = g_engine->world().getRoomByName(roomName.c_str());
+	object = room == nullptr ? nullptr : room->getObjectByName(objectName.c_str());
+	if ((object == nullptr && !optional) || !roomName.empty() || !objectName.empty())
+		error("Invalid object name \"%s\" in room \"%s\" in savestate for task %s",
+			objectName.c_str(), roomName.c_str(), taskName());
+}
+
 DelayTask::DelayTask(Process &process, uint32 millis)
 	: Task(process)
 	, _endTime(millis) {
 }
 
+DelayTask::DelayTask(Process &process, Serializer &s)
+	: Task(process) {
+	syncGame(s);
+}
+
 TaskReturn DelayTask::run() {
 	TASK_BEGIN;
 	_endTime += g_engine->getMillis();
 	while (g_engine->getMillis() < _endTime)
-		TASK_YIELD;
+		TASK_YIELD(1);
 	TASK_END;
 }
 
@@ -73,12 +99,23 @@ void DelayTask::debugPrint() {
 	g_engine->getDebugger()->debugPrintf("Delay for further %ums\n", remaining);
 }
 
+void DelayTask::syncGame(Serializer &s) {
+	Task::syncGame(s);
+	s.syncAsUint32LE(_endTime);
+}
+
+DECLARE_TASK(DelayTask);
+
 Process::Process(ProcessId pid, MainCharacterKind characterKind)
 	: _pid(pid)
 	, _character(characterKind)
 	, _name("Unnamed process") {
 }
 
+Process::Process(Serializer &s) {
+	syncGame(s);
+}
+
 Process::~Process() {
 	while (!_tasks.empty())
 		delete _tasks.pop();
@@ -125,6 +162,42 @@ void Process::debugPrint() {
 	}
 }
 
+#define DEFINE_TASK(TaskName) \
+	extern Task *constructTask_##TaskName(Process &process, Serializer &s);
+#include "tasks.h"
+
+static Task *readTask(Process &process, Serializer &s) {
+	assert(s.isLoading());
+	String taskName;
+	s.syncString(taskName);
+
+#define DEFINE_TASK(TaskName) \
+	if (taskName == #TaskName) \
+		return constructTask_##TaskName(process, s);
+#include "tasks.h"
+
+	error("Invalid task type in savestate: %s", taskName.c_str());
+}
+
+void Process::syncGame(Serializer &s) {
+	s.syncAsUint32LE(_pid);
+	syncEnum(s, _character);
+	s.syncString(_name);
+	s.syncAsSint32LE(_lastReturnValue);
+
+	uint count = _tasks.size();
+	s.syncAsUint32LE(count);
+	if (s.isLoading()) {
+		assert(_tasks.empty());
+		for (uint i = 0; i < count; i++)
+			_tasks.push(readTask(*this, s));
+	}
+	else {
+		for (uint i = 0; i < count; i++)
+			_tasks[i]->syncGame(s);
+	}
+}
+
 static void killProcessesForIn(MainCharacterKind characterKind, Array<Process *> &processes, uint firstIndex) {
 	assert(firstIndex <= processes.size());
 	for (uint i = 0; i < processes.size() - firstIndex; i++) {
@@ -252,4 +325,32 @@ void Scheduler::debugPrint() {
 		console.debugPrintf("No processes running or backed up\n");
 }
 
+void Scheduler::prepareSyncGame(Serializer &s) {
+	if (s.isLoading()) {
+		killAllProcesses();
+		killProcessesForIn(MainCharacterKind::None, _backupProcesses, 0);
+	}
+}
+
+void Scheduler::syncGame(Serializer &s) {
+	assert(_currentProcessI == UINT_MAX); // let's not sync during ::run
+	assert(s.isSaving() || _backupProcesses.empty());
+
+	// we only sync the backupProcesses as these are the ones pertaining to the gameplay
+	// the other arrays would be used for the menu
+
+	s.syncAsUint32LE(_nextPid);
+	uint32 count = _backupProcesses.size();
+	s.syncAsUint32LE(count);
+	if (s.isLoading()) {
+		_backupProcesses.reserve(count);
+		for (uint32 i = 0; i < count; i++)
+			_backupProcesses.push_back(new Process(s));
+	}
+	else {
+		for (Process *process : _backupProcesses)
+			process->syncGame(s);
+	}
+}
+
 }
diff --git a/engines/alcachofa/scheduler.h b/engines/alcachofa/scheduler.h
index 5299b029f61..3ba95f298fc 100644
--- a/engines/alcachofa/scheduler.h
+++ b/engines/alcachofa/scheduler.h
@@ -26,12 +26,30 @@
 
 #include "common/stack.h"
 #include "common/str.h"
+#include "common/type_traits.h"
 
 namespace Alcachofa {
 
+/* Tasks are generally written as coroutines however the common coroutines
+ * cannot be used for two reasons:
+ * 1. The scheduler is too limited in managing when to run what coroutines
+ *    E.g. for the inventory/menu we need to pause a set of coroutines and
+ *    continue them later on
+ * 2. We need to save and load the state of coroutines
+ *    For this we either write the state machine ourselves or we use
+ *    the following careful macros where the state ID is explicitly written
+ *    This way it is stable and if it has to change we can migrate
+ *    savestates upon loading.
+ *
+ * Tasks are usually private, so in order to load them they:
+ *   - need a constructor MyPrivateTask(Process &, Serializer &)
+ *   - need call the macro DECLARE_TASK(MyPrivateTask)
+ *   - they have to listed in tasks.h
+ */
+
 struct Task;
 class Process;
-
+class ObjectBase;
 
 enum class TaskReturnType {
 	Yield,
@@ -68,26 +86,52 @@ struct Task {
 	virtual ~Task() = default;
 	virtual TaskReturn run() = 0;
 	virtual void debugPrint() = 0;
+	virtual void syncGame(Common::Serializer &s);
+	virtual const char *taskName() const = 0; // implemented by DECLARE_TASK
 
 	inline Process &process() const { return _process; }
 
 protected:
 	Task *delay(uint32 millis);
 
-	uint32 _line = 0;
+	void syncObjectAsString(Common::Serializer &s, ObjectBase *&object, bool optional = false);
+	template<class TObject>
+	void syncObjectAsString(Common::Serializer &s, TObject *&object, bool optional = false) {
+		// We could add is_const and therefore true_type, false_type, integral_constant 
+		// or we could just use const_cast and promise that we won't modify
+		ObjectBase *base = const_cast<Common::remove_const_t<TObject> *>(object);
+		syncObjectAsString(s, base, optional);
+		object = dynamic_cast<TObject*>(base);
+		if (object == nullptr && base != nullptr)
+			error("Unexpected type of object %s in savestate for task %s (got a %s)",
+				base->name().c_str(), taskName(), base->typeName());
+	}
+
+	uint32 _stage = 0;
 private:
 	Process &_process;
 };
 
 struct DelayTask : public Task {
 	DelayTask(Process &process, uint32 millis);
+	DelayTask(Process &process, Common::Serializer &s);
 	virtual TaskReturn run() override;
 	virtual void debugPrint() override;
+	virtual void syncGame(Common::Serializer &s) override;
+	virtual const char *taskName() const override;
 
 private:
 	uint32 _endTime;
 };
 
+#define DECLARE_TASK(TaskName) \
+	extern Task *constructTask_##TaskName(Process &process, Serializer &s) { \
+		return new TaskName(process, s); \
+	} \
+	const char *TaskName::taskName() const { \
+		return #TaskName; \
+	}
+
 // TODO: This probably should be scummvm common
 #if __cplusplus >= 201703L
 #define TASK_BREAK_FALLTHROUGH [[fallthrough]];
@@ -96,8 +140,7 @@ private:
 #endif
 
 #define TASK_BEGIN \
-	enum { TASK_COUNTER_BASE = __COUNTER__ }; \
-	switch(_line) { \
+	switch(_stage) { \
 	case 0:; \
 
 #define TASK_END \
@@ -106,28 +149,28 @@ private:
 	default: assert(false && "Invalid line in task"); \
 	} return TaskReturn::finish(0)
 
-#define TASK_INTERNAL_BREAK(ret) \
+#define TASK_INTERNAL_BREAK(stage, ret) \
 	do { \
-		enum { TASK_COUNTER = __COUNTER__ - TASK_COUNTER_BASE }; \
-		_line = TASK_COUNTER; \
+		_stage = stage; \
 		return ret; \
 		TASK_BREAK_FALLTHROUGH \
-		case TASK_COUNTER:; \
+		case stage:; \
 	} while(0)
 
-#define TASK_YIELD TASK_INTERNAL_BREAK(TaskReturn::yield())
-#define TASK_WAIT(task) TASK_INTERNAL_BREAK(TaskReturn::waitFor(task))
+#define TASK_YIELD(stage) TASK_INTERNAL_BREAK((stage), TaskReturn::yield())
+#define TASK_WAIT(stage, task) TASK_INTERNAL_BREAK((stage), TaskReturn::waitFor(task))
 
 #define TASK_RETURN(value) \
 	do { \
 		return TaskReturn::finish(value); \
-		_line = UINT_MAX; \
+		_stage = UINT_MAX; \
 	} while(0)
 
-using ProcessId = uint;
+using ProcessId = uint32;
 class Process {
 public:
 	Process(ProcessId pid, MainCharacterKind characterKind);
+	Process(Common::Serializer &s);
 	~Process();
 
 	inline ProcessId pid() const { return _pid; }
@@ -139,6 +182,7 @@ public:
 
 	TaskReturnType run();
 	void debugPrint();
+	void syncGame(Common::Serializer &s);
 
 private:
 	friend class Scheduler;
@@ -161,6 +205,8 @@ public:
 	void killProcessByName(const Common::String &name);
 	bool hasProcessWithName(const Common::String &name);
 	void debugPrint();
+	void prepareSyncGame(Common::Serializer &s);
+	void syncGame(Common::Serializer &s);
 
 	template<typename TTask, typename... TaskArgs>
 	Process *createProcess(MainCharacterKind character, TaskArgs&&... args) {
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 51184d13ba7..9cea4464da3 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -137,6 +137,11 @@ struct ScriptTimerTask : public Task {
 		, _durationSec(durationSec) {
 	}
 
+	ScriptTimerTask(Process &process, Serializer &s)
+		: Task(process) {
+		syncGame(s);
+	}
+
 	virtual TaskReturn run() override {
 		TASK_BEGIN;
 		{
@@ -148,7 +153,7 @@ struct ScriptTimerTask : public Task {
 				_result = 1;
 			g_engine->player().drawCursor();
 		}
-		TASK_YIELD; // Wait a frame to not produce an endless loop
+		TASK_YIELD(1); // Wait a frame to not produce an endless loop
 		TASK_RETURN(_result);
 		TASK_END;
 	}
@@ -157,10 +162,19 @@ struct ScriptTimerTask : public Task {
 		g_engine->getDebugger()->debugPrintf("Check input timer for %dsecs", _durationSec);
 	}
 
+	void syncGame(Serializer &s) override {
+		Task::syncGame(s);
+		s.syncAsSint32LE(_durationSec);
+		s.syncAsSint32LE(_result);
+	}
+
+	virtual const char *taskName() const override;
+
 private:
 	int32 _durationSec;
 	int32 _result = 1;
 };
+DECLARE_TASK(ScriptTimerTask);
 
 enum class StackEntryType {
 	Number,
@@ -172,6 +186,15 @@ enum class StackEntryType {
 struct StackEntry {
 	StackEntry(StackEntryType type, int32 number) : _type(type), _number(number) {}
 	StackEntry(StackEntryType type, uint32 index) : _type(type), _index(index) {}
+	StackEntry(Serializer &s) { syncGame(s); }
+
+	void syncGame(Serializer &s) {
+		syncEnum(s, _type);
+		if (_type == StackEntryType::Number)
+			s.syncAsSint32LE(_number);
+		else
+			s.syncAsUint32LE(_index);
+	}
 
 	StackEntryType _type;
 	union {
@@ -203,6 +226,12 @@ struct ScriptTask : public Task {
 		debugC(SCRIPT_DEBUG_LVL_TASKS, kDebugScript, "%u: Script fork from %u at %u", process.pid(), forkParent.process().pid(), _pc);
 	}
 
+	ScriptTask(Process &process, Serializer &s)
+		: Task(process)
+		, _script(g_engine->script()) {
+		syncGame(s);
+	}
+
 	virtual TaskReturn run() override {
 		if (_isFirstExecution || _returnsFromKernelCall)
 			setCharacterVariables();
@@ -342,6 +371,33 @@ struct ScriptTask : public Task {
 		g_engine->getDebugger()->debugPrintf("\"%s\" at %u\n", _name.c_str(), _pc);
 	}
 
+	void syncGame(Serializer &s) override {
+		assert(s.isSaving() || (_lock.isReleased() && _stack.empty()));
+
+		s.syncString(_name);
+		s.syncAsUint32LE(_pc);
+		s.syncAsByte(_returnsFromKernelCall);
+		s.syncAsByte(_isFirstExecution);
+
+		uint count = _stack.size();
+		s.syncAsUint32LE(count);
+		if (s.isLoading()) {
+			for (uint i = 0; i < count; i++)
+				_stack.push(StackEntry(s));
+		}
+		else {
+			for (uint i = 0; i < count; i++)
+				_stack[i].syncGame(s);
+		}
+
+		bool hasLock = !_lock.isReleased();
+		s.syncAsByte(hasLock);
+		if (hasLock)
+			_lock = FakeLock(g_engine->player().semaphoreFor(process().character()));
+	}
+
+	virtual const char *taskName() const override;
+
 private:
 	void setCharacterVariables() {
 		_script.variable("m_o_f") = (int32)process().character();
@@ -894,6 +950,7 @@ private:
 	bool _isFirstExecution = true;
 	FakeLock _lock;
 };
+DECLARE_TASK(ScriptTask);
 
 Process *Script::createProcess(MainCharacterKind character, const String &behavior, const String &action, ScriptFlags flags) {
 	return createProcess(character, behavior + '/' + action, flags);
@@ -910,7 +967,7 @@ Process *Script::createProcess(MainCharacterKind character, const String &proced
 	}
 	FakeLock lock;
 	if (!(flags & ScriptFlags::IsBackground))
-		new (&lock) FakeLock(g_engine->player().semaphoreFor(character));
+		lock = FakeLock(g_engine->player().semaphoreFor(character));
 	Process *process = g_engine->scheduler().createProcess<ScriptTask>(character, procedure, offset, Common::move(lock));
 	process->name() = procedure;
 	return process;
diff --git a/engines/alcachofa/sounds.cpp b/engines/alcachofa/sounds.cpp
index f4ad498203f..6d719b44e16 100644
--- a/engines/alcachofa/sounds.cpp
+++ b/engines/alcachofa/sounds.cpp
@@ -329,8 +329,7 @@ void Sounds::setMusicToRoom(int roomMusicId) {
 }
 
 Task *Sounds::waitForMusicToEnd(Process &process) {
-	FakeLock lock(_musicSemaphore);
-	return new WaitForMusicTask(process, std::move(lock));
+	return new WaitForMusicTask(process);
 }
 
 PlaySoundTask::PlaySoundTask(Process &process, SoundHandle SoundHandle)
@@ -338,6 +337,14 @@ PlaySoundTask::PlaySoundTask(Process &process, SoundHandle SoundHandle)
 	, _soundHandle(SoundHandle) {
 }
 
+PlaySoundTask::PlaySoundTask(Process &process, Serializer &s)
+	: Task(process)
+	, _soundHandle({}) {
+	// playing sounds are not persisted in the savestates,
+	// this task will stop at the next frame
+	syncGame(s);
+}
+
 TaskReturn PlaySoundTask::run() {
 	auto &sounds = g_engine->sounds();
 	if (sounds.isAlive(_soundHandle))
@@ -353,9 +360,17 @@ void PlaySoundTask::debugPrint() {
 	g_engine->console().debugPrintf("PlaySound %u\n", _soundHandle);
 }
 
-WaitForMusicTask::WaitForMusicTask(Process &process, FakeLock &&lock)
+DECLARE_TASK(PlaySoundTask);
+
+WaitForMusicTask::WaitForMusicTask(Process &process)
 	: Task(process)
-	, _lock(std::move(lock)) {}
+	, _lock(g_engine->sounds().musicSemaphore()) {}
+
+WaitForMusicTask::WaitForMusicTask(Process &process, Serializer &s)
+	: Task(process)
+	, _lock(g_engine->sounds().musicSemaphore()) {
+	syncGame(s);
+}
 
 TaskReturn WaitForMusicTask::run() {
 	g_engine->sounds().queueMusic(-1);
@@ -368,4 +383,6 @@ void WaitForMusicTask::debugPrint() {
 	g_engine->console().debugPrintf("WaitForMusic\n");
 }
 
+DECLARE_TASK(WaitForMusicTask);
+
 }
diff --git a/engines/alcachofa/sounds.h b/engines/alcachofa/sounds.h
index 1d636cb32c7..2ebe71ad4b8 100644
--- a/engines/alcachofa/sounds.h
+++ b/engines/alcachofa/sounds.h
@@ -85,16 +85,20 @@ private:
 
 struct PlaySoundTask final : public Task {
 	PlaySoundTask(Process &process, SoundHandle soundHandle);
+	PlaySoundTask(Process &process, Common::Serializer &s);
 	virtual TaskReturn run() override;
 	virtual void debugPrint() override;
+	virtual const char *taskName() const override;
 private:
 	SoundHandle _soundHandle;
 };
 
 struct WaitForMusicTask final : public Task {
-	WaitForMusicTask(Process &process, FakeLock &&lock);
+	WaitForMusicTask(Process &process);
+	WaitForMusicTask(Process &process, Common::Serializer &s);
 	virtual TaskReturn run() override;
 	virtual void debugPrint() override;
+	virtual const char *taskName() const override;
 private:
 	FakeLock _lock;
 };
diff --git a/engines/alcachofa/tasks.h b/engines/alcachofa/tasks.h
new file mode 100644
index 00000000000..163fed7166f
--- /dev/null
+++ b/engines/alcachofa/tasks.h
@@ -0,0 +1,56 @@
+/* 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/>.
+ *
+ */
+
+/* The task loading works as follows:
+ *   - the task has a constructor MyPrivateTask(Process &, Serializer &)
+ *   - DECLARE_TASK implements a global function to call that constructor
+ *     void constructTask_MyPrivateTask(Process &, Serializer
+ *   - in Process::syncGame we first forward-declare that function
+ *   - we go through every task to compare task name and call the factory
+ */
+
+#ifndef DEFINE_TASK
+#define DEFINE_TASK(TaskName)
+#endif
+
+DEFINE_TASK(CamLerpPosTask);
+DEFINE_TASK(CamLerpScaleTask);
+DEFINE_TASK(CamLerpPosScaleTask);
+DEFINE_TASK(CamLerpRotationTask);
+DEFINE_TASK(CamShakeTask);
+DEFINE_TASK(CamWaitToStopTask);
+DEFINE_TASK(CamSetInactiveAttributeTask);
+DEFINE_TASK(SayTextTask);
+DEFINE_TASK(AnimateCharacterTask);
+DEFINE_TASK(LerpLodBiasTask);
+DEFINE_TASK(ArriveTask)
+DEFINE_TASK(DialogMenuTask)
+DEFINE_TASK(AnimateTask)
+DEFINE_TASK(CenterBottomTextTask)
+DEFINE_TASK(FadeTask)
+DEFINE_TASK(DoorTask)
+DEFINE_TASK(DelayTask)
+DEFINE_TASK(ScriptTimerTask)
+DEFINE_TASK(ScriptTask)
+DEFINE_TASK(PlaySoundTask)
+DEFINE_TASK(WaitForMusicTask)
+
+#undef DEFINE_TASK


Commit: 700eee9de451169e2c74cb2c2b9191616d963d68
    https://github.com/scummvm/scummvm/commit/700eee9de451169e2c74cb2c2b9191616d963d68
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:57+02:00

Commit Message:
ALCACHOFA: Remove virtual on overridden methods

Changed paths:
    engines/alcachofa/camera.cpp
    engines/alcachofa/debug.h
    engines/alcachofa/game-movie-adventure.cpp
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/general-objects.cpp
    engines/alcachofa/global-ui.cpp
    engines/alcachofa/graphics-opengl.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/graphics.h
    engines/alcachofa/objects.h
    engines/alcachofa/rooms.h
    engines/alcachofa/scheduler.h
    engines/alcachofa/script.cpp
    engines/alcachofa/sounds.h


diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index fb4c4de1d87..3c77a4d99f7 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -291,7 +291,7 @@ struct CamLerpTask : public Task {
 		, _duration(duration)
 		, _easingType(easingType) {}
 
-	virtual TaskReturn run() override {
+	TaskReturn run() override {
 		TASK_BEGIN;
 		_startTime = g_engine->getMillis();
 		while (g_engine->getMillis() - _startTime < _duration) {
@@ -303,14 +303,14 @@ struct CamLerpTask : public Task {
 		TASK_END;
 	}
 
-	virtual void debugPrint() override {
+	void debugPrint() override {
 		uint32 remaining = g_engine->getMillis() - _startTime <= _duration
 			? _duration - (g_engine->getMillis() - _startTime)
 			: 0;
 		g_engine->console().debugPrintf("%s camera with %ums remaining\n", taskName(), remaining);
 	}
 
-	virtual void syncGame(Serializer &s) override {
+	void syncGame(Serializer &s) override {
 		Task::syncGame(s);
 		s.syncAsUint32LE(_startTime);
 		s.syncAsUint32LE(_duration);
@@ -336,7 +336,7 @@ struct CamLerpPosTask final : public CamLerpTask {
 		syncGame(s);
 	}
 
-	virtual void syncGame(Serializer &s) override {
+	void syncGame(Serializer &s) override {
 		CamLerpTask::syncGame(s);
 		syncVector(s, _fromPos);
 		syncVector(s, _deltaPos);
@@ -345,7 +345,7 @@ struct CamLerpPosTask final : public CamLerpTask {
 	virtual const char *taskName() const override;
 
 protected:
-	virtual void update(float t) override {
+	void update(float t) override {
 		_camera.setPosition(_fromPos + _deltaPos * t);
 	}
 
@@ -364,7 +364,7 @@ struct CamLerpScaleTask final : public CamLerpTask {
 		syncGame(s);
 	}
 
-	virtual void syncGame(Serializer &s) override {
+	void syncGame(Serializer &s) override {
 		CamLerpTask::syncGame(s);
 		s.syncAsFloatLE(_fromScale);
 		s.syncAsFloatLE(_deltaScale);
@@ -373,7 +373,7 @@ struct CamLerpScaleTask final : public CamLerpTask {
 	virtual const char *taskName() const override;
 
 protected:
-	virtual void update(float t) override {
+	void update(float t) override {
 		_camera._cur._scale = _fromScale + _deltaScale * t;
 	}
 
@@ -399,7 +399,7 @@ struct CamLerpPosScaleTask final : public CamLerpTask {
 		syncGame(s);
 	}
 
-	virtual void syncGame(Serializer &s) override {
+	void syncGame(Serializer &s) override {
 		CamLerpTask::syncGame(s);
 		syncVector(s, _fromPos);
 		syncVector(s, _deltaPos);
@@ -412,7 +412,7 @@ struct CamLerpPosScaleTask final : public CamLerpTask {
 	virtual const char *taskName() const override;
 
 protected:
-	virtual void update(float t) override {
+	void update(float t) override {
 		_camera.setPosition(_fromPos + _deltaPos * ease(t, _moveEasingType));
 		_camera._cur._scale = _fromScale + _deltaScale * ease(t, _scaleEasingType);
 	}
@@ -434,7 +434,7 @@ struct CamLerpRotationTask final : public CamLerpTask {
 		syncGame(s);
 	}
 
-	virtual void syncGame(Serializer &s) override {
+	void syncGame(Serializer &s) override {
 		CamLerpTask::syncGame(s);
 		s.syncAsFloatLE(_fromRotation);
 		s.syncAsFloatLE(_deltaRotation);
@@ -443,7 +443,7 @@ struct CamLerpRotationTask final : public CamLerpTask {
 	virtual const char *taskName() const override;
 
 protected:
-	virtual void update(float t) override {
+	void update(float t) override {
 		_camera._cur._rotation = Angle(_fromRotation + _deltaRotation * t);
 	}
 
@@ -468,7 +468,7 @@ struct CamShakeTask final : public CamLerpTask {
 		syncGame(s);
 	}
 
-	virtual void syncGame(Serializer &s) override {
+	void syncGame(Serializer &s) override {
 		CamLerpTask::syncGame(s);
 		syncVector(s, _amplitude);
 		syncVector(s, _frequency);
@@ -477,7 +477,7 @@ struct CamShakeTask final : public CamLerpTask {
 	virtual const char *taskName() const override;
 
 protected:
-	virtual void update(float t) override {
+	void update(float t) override {
 		const Vector2d phase = _frequency * t * (float)M_PI * 2.0f;
 		const float amplTimeFactor = 1.0f / expf(t * 5.0f); // a curve starting at 1, depreciating towards 0 
 		_camera.shake() = {
@@ -501,13 +501,13 @@ struct CamWaitToStopTask final : public Task {
 		syncGame(s);
 	}
 
-	virtual TaskReturn run() override {
+	TaskReturn run() override {
 		return _camera._isChanging
 			? TaskReturn::yield()
 			: TaskReturn::finish(1);
 	}
 
-	virtual void debugPrint() override {
+	void debugPrint() override {
 		g_engine->console().debugPrintf("Wait for camera to stop moving\n");
 	}
 
@@ -538,7 +538,7 @@ struct CamSetInactiveAttributeTask final : public Task {
 		syncGame(s);
 	}
 
-	virtual TaskReturn run() override {
+	TaskReturn run() override {
 		if (_delay > 0) {
 			uint32 delay = (uint32)_delay;
 			_delay = 0;
@@ -557,7 +557,7 @@ struct CamSetInactiveAttributeTask final : public Task {
 		return TaskReturn::finish(0);
 	}
 
-	virtual void debugPrint() override {
+	void debugPrint() override {
 		const char *attributeName;
 		switch (_attribute) {
 		case kPosZ: attributeName = "PosZ"; break;
@@ -568,7 +568,7 @@ struct CamSetInactiveAttributeTask final : public Task {
 		g_engine->console().debugPrintf("Set inactive camera %s to %f after %dms\n", attributeName, _value, _delay);
 	}
 
-	virtual void syncGame(Serializer &s) override {
+	void syncGame(Serializer &s) override {
 		Task::syncGame(s);
 		syncEnum(s, _attribute);
 		s.syncAsFloatLE(_value);
diff --git a/engines/alcachofa/debug.h b/engines/alcachofa/debug.h
index cdfc42c8c93..561b3683f09 100644
--- a/engines/alcachofa/debug.h
+++ b/engines/alcachofa/debug.h
@@ -40,7 +40,7 @@ class ClosestFloorPointDebugHandler final : public IDebugHandler {
 public:
 	ClosestFloorPointDebugHandler(int32 polygonI) : _polygonI(polygonI) {}
 
-	virtual void update() override {
+	void update() override {
 		auto mousePos2D = g_engine->input().debugInput().mousePos2D();
 		auto mousePos3D = g_engine->input().debugInput().mousePos3D();
 		auto floor = g_engine->player().currentRoom()->activeFloor();
@@ -63,7 +63,7 @@ class FloorIntersectionsDebugHandler final : public IDebugHandler {
 public:
 	FloorIntersectionsDebugHandler(int32 polygonI) : _polygonI(polygonI) {}
 
-	virtual void update() override {
+	void update() override {
 		auto floor = g_engine->player().currentRoom()->activeFloor();
 		auto renderer = dynamic_cast<IDebugRenderer *>(&g_engine->renderer());
 		if (floor == nullptr || renderer == nullptr) {
@@ -118,7 +118,7 @@ class TeleportCharacterDebugHandler final : public IDebugHandler {
 public:
 	TeleportCharacterDebugHandler(int32 kindI) : _kind((MainCharacterKind)kindI) {}
 
-	virtual void update() override {
+	void update() override {
 		g_engine->drawQueue().clear();
 		g_engine->player().drawCursor(true);
 		g_engine->drawQueue().draw();
@@ -189,7 +189,7 @@ public:
 		return nullptr;
 	}
 
-	virtual void update() override {
+	void update() override {
 		auto &input = g_engine->input().debugInput();
 		if (input.wasMouseRightPressed()) {
 			g_engine->setDebugMode(DebugMode::None, 0);
diff --git a/engines/alcachofa/game-movie-adventure.cpp b/engines/alcachofa/game-movie-adventure.cpp
index 2c5225a6219..b8c4104a923 100644
--- a/engines/alcachofa/game-movie-adventure.cpp
+++ b/engines/alcachofa/game-movie-adventure.cpp
@@ -27,17 +27,17 @@ using namespace Common;
 namespace Alcachofa {
 
 class GameMovieAdventure : public Game {
-	virtual bool doesRoomHaveBackground(const Room *room) override {
+	bool doesRoomHaveBackground(const Room *room) override {
 		return !room->name().equalsIgnoreCase("Global") &&
 			!room->name().equalsIgnoreCase("HABITACION_NEGRA");
 	}
 
-	virtual void invalidDialogLine(uint index) override {
+	void invalidDialogLine(uint index) override {
 		if (index != 4542)
 			Game::invalidDialogLine(index);
 	}
 
-	virtual bool shouldCharacterTrigger(const Character *character, const char *action) override {
+	bool shouldCharacterTrigger(const Character *character, const char *action) override {
 		// An original hack to check that bed sheet is used on the other main character only in the correct room
 		// There *is* another script variable (es_casa_freddy) that should check this
 		// but, I guess, Alcachofa Soft found a corner case where this does not work?
@@ -57,12 +57,12 @@ class GameMovieAdventure : public Game {
 		return Game::shouldTriggerDoor(door);
 	}
 
-	virtual bool hasMortadeloVoice(const Character *character) override {
+	bool hasMortadeloVoice(const Character *character) override {
 		return Game::hasMortadeloVoice(character) ||
 			character->name().equalsIgnoreCase("MORTADELO_TREN"); // an original hard-coded special case
 	}
 
-	virtual void missingAnimation(const String &fileName) override {
+	void missingAnimation(const String &fileName) override {
 		static const char *exemptions[] = {
 			"ANIMACION.AN0",
 			"DESPACHO_SUPER2_OL_SOMBRAS2.AN0",
@@ -82,13 +82,13 @@ class GameMovieAdventure : public Game {
 		Game::missingAnimation(fileName);
 	}
 
-	virtual void unknownAnimateObject(const char *name) override {
+	void unknownAnimateObject(const char *name) override {
 		if (!scumm_stricmp("EXPLOSION DISFRAZ", name))
 			return;
 		Game::unknownAnimateObject(name);
 	}
 
-	virtual PointObject *unknownGoPutTarget(const Process &process, const char *action, const char *name) override {
+	PointObject *unknownGoPutTarget(const Process &process, const char *action, const char *name) override {
 		if (scumm_stricmp(action, "put"))
 			return Game::unknownGoPutTarget(process, action, name);
 
@@ -117,20 +117,20 @@ class GameMovieAdventure : public Game {
 		return Game::unknownGoPutTarget(process, action, name);
 	}
 
-	virtual void unknownSayTextCharacter(const char *name, int32 dialogId) override {
+	void unknownSayTextCharacter(const char *name, int32 dialogId) override {
 		if (!scumm_stricmp(name, "OFELIA") && dialogId == 3737)
 			return;
 		Game::unknownSayTextCharacter(name, dialogId);
 	}
 
-	virtual void unknownAnimateCharacterObject(const char *name) override {
+	void unknownAnimateCharacterObject(const char *name) override {
 		if (!scumm_stricmp(name, "COGE F DCH") || // original bug in MOTEL_ENTRADA
 			!scumm_stricmp(name, "CHIQUITO_IZQ"))
 			return;
 		Game::unknownAnimateCharacterObject(name);
 	}
 
-	virtual void missingSound(const String &fileName) override {
+	void missingSound(const String &fileName) override {
 		if (fileName == "CHAS" || fileName == "517")
 			return;
 		Game::missingSound(fileName);
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 5bbeefa21b0..3dc0e585bbd 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -297,7 +297,7 @@ struct SayTextTask final : public Task {
 		syncGame(s);
 	}
 
-	virtual TaskReturn run() override {
+	TaskReturn run() override {
 		bool isSoundStillPlaying;
 
 		TASK_BEGIN;
@@ -340,11 +340,11 @@ struct SayTextTask final : public Task {
 		TASK_END;
 	}
 
-	virtual void debugPrint() override {
+	void debugPrint() override {
 		g_engine->console().debugPrintf("SayText %s, %d\n", _character->name().c_str(), _dialogId);
 	}
 
-	virtual void syncGame(Serializer &s) override {
+	void syncGame(Serializer &s) override {
 		Task::syncGame(s);
 		syncObjectAsString(s, _character);
 		s.syncAsSint32LE(_dialogId);
@@ -395,7 +395,7 @@ struct AnimateCharacterTask final : public Task {
 		syncGame(s);
 	}
 
-	virtual TaskReturn run() override {
+	TaskReturn run() override {
 		TASK_BEGIN;
 		while (_character->_curAnimateObject != nullptr)
 			TASK_YIELD(1);
@@ -416,11 +416,11 @@ struct AnimateCharacterTask final : public Task {
 		TASK_END;
 	}
 
-	virtual void debugPrint() override {
+	void debugPrint() override {
 		g_engine->console().debugPrintf("AnimateCharacter %s, %s\n", _character->name().c_str(), _animateObject->name().c_str());
 	}
 
-	virtual void syncGame(Serializer &s) override {
+	void syncGame(Serializer &s) override {
 		Task::syncGame(s);
 		syncObjectAsString(s, _character);
 		syncObjectAsString(s, _animateObject);
@@ -455,7 +455,7 @@ struct LerpLodBiasTask final : public Task {
 		syncGame(s);
 	}
 
-	virtual TaskReturn run() override {
+	TaskReturn run() override {
 		TASK_BEGIN;
 		_startTime = g_engine->getMillis();
 		_sourceLodBias = _character->lodBias();
@@ -468,7 +468,7 @@ struct LerpLodBiasTask final : public Task {
 		TASK_END;
 	}
 
-	virtual void debugPrint() override {
+	void debugPrint() override {
 		uint32 remaining = g_engine->getMillis() - _startTime <= _durationMs
 			? _durationMs - (g_engine->getMillis() - _startTime)
 			: 0;
@@ -476,7 +476,7 @@ struct LerpLodBiasTask final : public Task {
 			_character->name().c_str(), _targetLodBias, remaining);
 	}
 
-	virtual void syncGame(Serializer &s) override {
+	void syncGame(Serializer &s) override {
 		Task::syncGame(s);
 		syncObjectAsString(s, _character);
 		s.syncAsFloatLE(_sourceLodBias);
@@ -786,17 +786,17 @@ struct ArriveTask : public Task {
 		syncGame(s);		
 	}
 
-	virtual TaskReturn run() override {
+	TaskReturn run() override {
 		return _character->isWalking()
 			? TaskReturn::yield()
 			: TaskReturn::finish(1);
 	}
 
-	virtual void debugPrint() override {
+	void debugPrint() override {
 		g_engine->getDebugger()->debugPrintf("Wait for %s to arrive", _character->name().c_str());
 	}
 
-	virtual void syncGame(Serializer &s) override {
+	void syncGame(Serializer &s) override {
 		syncObjectAsString(s, _character);
 	}
 
@@ -1048,7 +1048,7 @@ struct DialogMenuTask : public Task {
 		syncGame(s);
 	}
 
-	virtual TaskReturn run() override {
+	TaskReturn run() override {
 		TASK_BEGIN;
 		layoutLines();
 		while (true) {
@@ -1071,12 +1071,12 @@ struct DialogMenuTask : public Task {
 		TASK_END;
 	}
 
-	virtual void debugPrint() override {
+	void debugPrint() override {
 		g_engine->console().debugPrintf("DialogMenu for %s with %u lines\n",
 			_character->name().c_str(), _character->_dialogLines.size());
 	}
 
-	virtual void syncGame(Serializer &s) override {
+	void syncGame(Serializer &s) override {
 		Task::syncGame(s);
 		syncObjectAsString(s, _character);
 		s.syncAsUint32LE(_clickedLineI);
diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index 6795d6ebf05..be14b9c99b2 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -144,7 +144,7 @@ struct AnimateTask : public Task {
 		syncGame(s);
 	}
 
-	virtual TaskReturn run() override {
+	TaskReturn run() override {
 		TASK_BEGIN;
 		_object->toggle(true);
 		_graphic->start(false);
@@ -153,11 +153,11 @@ struct AnimateTask : public Task {
 		TASK_END;
 	}
 
-	virtual void debugPrint() override {
+	void debugPrint() override {
 		g_engine->getDebugger()->debugPrintf("Animate \"%s\" for %ums", _object->name().c_str(), _duration);
 	}
 
-	virtual void syncGame(Serializer &s) override {
+	void syncGame(Serializer &s) override {
 		Task::syncGame(s);
 		s.syncAsUint32LE(_duration);
 		syncObjectAsString(s, _object);
diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index 06d7a09dee3..6e379c5622b 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -222,14 +222,14 @@ struct CenterBottomTextTask : public Task {
 		TASK_END;
 	}
 
-	virtual void debugPrint() override {
+	void debugPrint() override {
 		uint32 remaining = g_engine->getMillis() - _startTime <= _durationMs
 			? _durationMs - (g_engine->getMillis() - _startTime)
 			: 0;
 		g_engine->console().debugPrintf("CenterBottomText (%d) with %ums remaining\n", _dialogId, remaining);
 	}
 
-	virtual void syncGame(Serializer &s) override {
+	void syncGame(Serializer &s) override {
 		Task::syncGame(s);
 		s.syncAsSint32LE(_dialogId);
 		s.syncAsUint32LE(_startTime);
diff --git a/engines/alcachofa/graphics-opengl.cpp b/engines/alcachofa/graphics-opengl.cpp
index 91eebb10fa5..e1e88278278 100644
--- a/engines/alcachofa/graphics-opengl.cpp
+++ b/engines/alcachofa/graphics-opengl.cpp
@@ -79,7 +79,7 @@ public:
 		setMirrorWrap(false);
 	}
 
-	virtual ~OpenGLTexture() override {
+	~OpenGLTexture() override {
 		if (_handle != 0)
 			GL_CALL(glDeleteTextures(1, &_handle));
 	}
@@ -138,12 +138,12 @@ public:
 		}
 	}
 
-	virtual ScopedPtr<ITexture> createTexture(int32 w, int32 h, bool withMipmaps) override {
+	ScopedPtr<ITexture> createTexture(int32 w, int32 h, bool withMipmaps) override {
 		assert(w >= 0 && h >= 0);
 		return ScopedPtr<ITexture>(new OpenGLTexture(w, h, withMipmaps));
 	}
 
-	virtual void begin() override {
+	void begin() override {
 		GL_CALL(glEnableClientState(GL_VERTEX_ARRAY));
 		GL_CALL(glDisableClientState(GL_INDEX_ARRAY));
 		GL_CALL(glDisableClientState(GL_TEXTURE_COORD_ARRAY));
@@ -153,12 +153,12 @@ public:
 		_isFirstDrawCommand = true;
 	}
 
-	virtual void end() override {
+	void end() override {
 		GL_CALL(glFlush());
 		g_system->updateScreen();
 	}
 
-	virtual void setTexture(ITexture *texture) override {
+	void setTexture(ITexture *texture) override {
 		if (texture == _currentTexture)
 			return;
 		else if (texture == nullptr) {
@@ -178,7 +178,7 @@ public:
 		}
 	}
 
-	virtual void setBlendMode(BlendMode blendMode) override {
+	void setBlendMode(BlendMode blendMode) override {
 		if (blendMode == _currentBlendMode)
 			return;
 		// first the blend func
@@ -246,7 +246,7 @@ public:
 		_currentBlendMode = blendMode;
 	}
 
-	virtual void setLodBias(float lodBias) override {
+	void setLodBias(float lodBias) override {
 		if (abs(_currentLodBias - lodBias) < epsilon)
 			return;
 		GL_CALL(glTexEnvf(GL_TEXTURE_FILTER_CONTROL, GL_TEXTURE_LOD_BIAS, lodBias));
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index cf47dac4ec5..242ce7c67d9 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -783,7 +783,7 @@ struct FadeTask : public Task {
 		syncGame(s);
 	}
 
-	virtual TaskReturn run() override {
+	TaskReturn run() override {
 		TASK_BEGIN;
 		if (_permanentFadeAction == PermanentFadeAction::UnsetFaded)
 			g_engine->globalUI().isPermanentFaded() = false;
@@ -798,7 +798,7 @@ struct FadeTask : public Task {
 		TASK_END;
 	}
 
-	virtual void debugPrint() override {
+	void debugPrint() override {
 		uint32 remaining = g_engine->getMillis() - _startTime <= _duration
 			? _duration - (g_engine->getMillis() - _startTime)
 			: 0;
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index a39763f6cfa..8520938da57 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -322,7 +322,7 @@ public:
 		int8 order
 	);
 
-	virtual void draw() override;
+	void draw() override;
 
 private:
 	bool _is3D;
@@ -344,7 +344,7 @@ public:
 		Math::Vector2d texOffset,
 		BlendMode blendMode);
 
-	virtual void draw() override;
+	void draw() override;
 
 private:
 	Animation *_animation;
@@ -368,7 +368,7 @@ public:
 		int8 order);
 
 	inline Common::Point size() const { return { (int16)_width, (int16)_height }; }
-	virtual void draw() override;
+	void draw() override;
 
 private:
 	static constexpr uint kMaxLines = 12;
@@ -399,7 +399,7 @@ class FadeDrawRequest : public IDrawRequest {
 public:
 	FadeDrawRequest(FadeType type, float value, int8 order);
 
-	virtual void draw() override;
+	void draw() override;
 
 private:
 	FadeType _type;
@@ -416,7 +416,7 @@ class BorderDrawRequest : public IDrawRequest {
 public:
 	BorderDrawRequest(Common::Rect rect, Color color);
 
-	virtual void draw() override;
+	void draw() override;
 
 private:
 	Common::Rect _rect;
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 5b1f552eb27..e225c64d48c 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -86,13 +86,13 @@ class GraphicObject : public ObjectBase {
 public:
 	static constexpr const char *kClassName = "CObjetoGrafico";
 	GraphicObject(Room *room, Common::ReadStream &stream);
-	virtual ~GraphicObject() override = default;
+	~GraphicObject() override = default;
 
-	virtual void draw() override;
-	virtual void loadResources() override;
-	virtual void freeResources() override;
-	virtual void syncGame(Common::Serializer &serializer) override;
-	virtual Graphic *graphic() override;
+	void draw() override;
+	void loadResources() override;
+	void freeResources() override;
+	void syncGame(Common::Serializer &serializer) override;
+	Graphic *graphic() override;
 	virtual const char *typeName() const;
 
 	Task *animate(Process &process);
@@ -110,7 +110,7 @@ public:
 	static constexpr const char *kClassName = "CObjetoGraficoMuare";
 	SpecialEffectObject(Room *room, Common::ReadStream &stream);
 
-	virtual void draw() override;
+	void draw() override;
 	virtual const char *typeName() const;
 
 private:
@@ -122,15 +122,15 @@ private:
 class ShapeObject : public ObjectBase {
 public:
 	ShapeObject(Room *room, Common::ReadStream &stream);
-	virtual ~ShapeObject() override = default;
+	~ShapeObject() override = default;
 
 	inline int8 order() const { return _order; }
 	inline bool isNewlySelected() const { return _isNewlySelected; }
 	inline bool wasSelected() const { return _wasSelected; }
 
-	virtual void update() override;
-	virtual void syncGame(Common::Serializer &serializer) override;
-	virtual Shape *shape() override;
+	void update() override;
+	void syncGame(Common::Serializer &serializer) override;
+	Shape *shape() override;
 	virtual CursorType cursorType() const;
 	virtual void onHoverStart();
 	virtual void onHoverEnd();
@@ -161,17 +161,17 @@ class MenuButton : public PhysicalObject {
 public:
 	static constexpr const char *kClassName = "CBotonMenu";
 	MenuButton(Room *room, Common::ReadStream &stream);
-	virtual ~MenuButton() override = default;
+	~MenuButton() override = default;
 
 	inline int32 actionId() const { return _actionId; }
 	inline bool &isInteractable() { return _isInteractable; }
 
-	virtual void draw() override;
-	virtual void update() override;
-	virtual void loadResources() override;
-	virtual void freeResources() override;
-	virtual void onHoverUpdate() override;
-	virtual void onClick() override;
+	void draw() override;
+	void update() override;
+	void loadResources() override;
+	void freeResources() override;
+	void onHoverUpdate() override;
+	void onClick() override;
 	virtual void trigger();
 	virtual const char *typeName() const;
 
@@ -204,8 +204,8 @@ public:
 	static constexpr const char *kClassName = "CBotonMenuOpciones";
 	OptionsMenuButton(Room *room, Common::ReadStream &stream);
 
-	virtual void update() override;
-	virtual void trigger() override;
+	void update() override;
+	void trigger() override;
 	virtual const char *typeName() const;
 };
 
@@ -214,8 +214,8 @@ public:
 	static constexpr const char *kClassName = "CBotonMenuPrincipal";
 	MainMenuButton(Room *room, Common::ReadStream &stream);
 
-	virtual void update() override;
-	virtual void trigger() override;
+	void update() override;
+	void trigger() override;
 	virtual const char *typeName() const;
 };
 
@@ -252,17 +252,17 @@ class CheckBox : public PhysicalObject {
 public:
 	static constexpr const char *kClassName = "CCheckBox";
 	CheckBox(Room *room, Common::ReadStream &stream);
-	virtual ~CheckBox() override = default;
+	~CheckBox() override = default;
 
 	inline bool &isChecked() { return _isChecked; }
 	inline int32 actionId() const { return _actionId; }
 
-	virtual void draw() override;
-	virtual void update() override;
-	virtual void loadResources() override;
-	virtual void freeResources() override;
-	virtual void onHoverUpdate() override;
-	virtual void onClick() override;
+	void draw() override;
+	void update() override;
+	void loadResources() override;
+	void freeResources() override;
+	void onHoverUpdate() override;
+	void onClick() override;
 	virtual void trigger();
 	virtual const char *typeName() const;
 
@@ -283,14 +283,14 @@ class SlideButton final : public ObjectBase {
 public:
 	static constexpr const char *kClassName = "CSlideButton";
 	SlideButton(Room *room, Common::ReadStream &stream);
-	virtual ~SlideButton() override = default;
+	~SlideButton() override = default;
 
 	inline float &value() { return _value; }
 
-	virtual void draw() override;
-	virtual void update() override;
-	virtual void loadResources() override;
-	virtual void freeResources() override;
+	void draw() override;
+	void update() override;
+	void loadResources() override;
+	void freeResources() override;
 	virtual const char *typeName() const;
 
 private:
@@ -328,7 +328,7 @@ class MessageBox final : public ObjectBase {
 public:
 	static constexpr const char *kClassName = "CMessageBox";
 	MessageBox(Room *room, Common::ReadStream &stream);
-	virtual ~MessageBox() override = default;
+	~MessageBox() override = default;
 
 	virtual const char *typeName() const;
 
@@ -355,7 +355,7 @@ public:
 	Item(Room *room, Common::ReadStream &stream);
 	Item(const Item &other);
 
-	virtual void draw() override;
+	void draw() override;
 	virtual const char *typeName() const;
 	void trigger();
 };
@@ -380,12 +380,12 @@ class InteractableObject : public PhysicalObject, public ITriggerableObject {
 public:
 	static constexpr const char *kClassName = "CObjetoTipico";
 	InteractableObject(Room *room, Common::ReadStream &stream);
-	virtual ~InteractableObject() override = default;
+	~InteractableObject() override = default;
 
-	virtual void drawDebug() override;
-	virtual void onClick() override;
-	virtual void trigger(const char *action) override;
-	virtual void toggle(bool isEnabled) override;
+	void drawDebug() override;
+	void onClick() override;
+	void trigger(const char *action) override;
+	void toggle(bool isEnabled) override;
 	virtual const char *typeName() const;
 
 private:
@@ -402,8 +402,8 @@ public:
 	inline Direction characterDirection() const { return _characterDirection; }
 
 	virtual CursorType cursorType() const override;
-	virtual void onClick() override;
-	virtual void trigger(const char *action) override;
+	void onClick() override;
+	void trigger(const char *action) override;
 	virtual const char *typeName() const;
 
 private:
@@ -416,17 +416,17 @@ class Character : public ShapeObject, public ITriggerableObject {
 public:
 	static constexpr const char *kClassName = "CPersonaje";
 	Character(Room *room, Common::ReadStream &stream);
-	virtual ~Character() override = default;
-
-	virtual void update() override;
-	virtual void draw() override;
-	virtual void drawDebug() override;
-	virtual void loadResources() override;
-	virtual void freeResources() override;
-	virtual void syncGame(Common::Serializer &serializer) override;
-	virtual Graphic *graphic() override;
-	virtual void onClick() override;
-	virtual void trigger(const char *action) override;
+	~Character() override = default;
+
+	void update() override;
+	void draw() override;
+	void drawDebug() override;
+	void loadResources() override;
+	void freeResources() override;
+	void syncGame(Common::Serializer &serializer) override;
+	Graphic *graphic() override;
+	void onClick() override;
+	void trigger(const char *action) override;
 	virtual const char *typeName() const;
 
 	Task *sayText(Process &process, int32 dialogId);
@@ -459,18 +459,18 @@ class WalkingCharacter : public Character {
 public:
 	static constexpr const char *kClassName = "CPersonajeAnda";
 	WalkingCharacter(Room *room, Common::ReadStream &stream);
-	virtual ~WalkingCharacter() override = default;
+	~WalkingCharacter() override = default;
 
 	inline bool isWalking() const { return _isWalking; }
 	inline Common::Point position() const { return _currentPos; }
 	inline float stepSizeFactor() const { return _stepSizeFactor; }
 
-	virtual void update() override;
-	virtual void draw() override;
-	virtual void drawDebug() override;
-	virtual void loadResources() override;
-	virtual void freeResources() override;
-	virtual void syncGame(Common::Serializer &serializer) override;
+	void update() override;
+	void draw() override;
+	void drawDebug() override;
+	void loadResources() override;
+	void freeResources() override;
+	void syncGame(Common::Serializer &serializer) override;
 	virtual void walkTo(
 		Common::Point target,
 		Direction endDirection = Direction::Invalid,
@@ -526,7 +526,7 @@ class MainCharacter final : public WalkingCharacter {
 public:
 	static constexpr const char *kClassName = "CPersonajePrincipal";
 	MainCharacter(Room *room, Common::ReadStream &stream);
-	virtual ~MainCharacter() override;
+	~MainCharacter() override;
 
 	inline MainCharacterKind kind() const { return _kind; }
 	inline ObjectBase *&currentlyUsing() { return _currentlyUsingObject; }
@@ -536,9 +536,9 @@ public:
 	inline FakeSemaphore &semaphore() { return _semaphore; }
 	bool isBusy() const;
 
-	virtual void update() override;
-	virtual void draw() override;
-	virtual void syncGame(Common::Serializer &serializer) override;
+	void update() override;
+	void draw() override;
+	void syncGame(Common::Serializer &serializer) override;
 	virtual const char *typeName() const;
 	virtual void walkTo(
 		Common::Point target,
@@ -557,7 +557,7 @@ public:
 	void resetUsingObjectAndDialogMenu();
 
 protected:
-	virtual void onArrived() override;
+	void onArrived() override;
 
 private:
 	friend class Inventory;
@@ -586,11 +586,11 @@ class FloorColor final : public ObjectBase {
 public:
 	static constexpr const char *kClassName = "CSueloColor";
 	FloorColor(Room *room, Common::ReadStream &stream);
-	virtual ~FloorColor() override = default;
+	~FloorColor() override = default;
 
-	virtual void update() override;
-	virtual void drawDebug() override;
-	virtual Shape *shape() override;
+	void update() override;
+	void drawDebug() override;
+	Shape *shape() override;
 	virtual const char *typeName() const;
 
 private:
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index 8d753f81aeb..e08ae07ec4b 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -92,8 +92,8 @@ public:
 	static constexpr const char *kClassName = "CHabitacionMenuOpciones";
 	OptionsMenu(World *world, Common::SeekableReadStream &stream);
 
-	virtual bool updateInput() override;
-	virtual void loadResources() override;
+	bool updateInput() override;
+	void loadResources() override;
 
 	void clearLastSelectedObject(); // to reset arm animation
 	inline SlideButton *&currentSlideButton() { return _currentSlideButton; }
@@ -120,9 +120,9 @@ class Inventory final : public Room {
 public:
 	static constexpr const char *kClassName = "CInventario";
 	Inventory(World *world, Common::SeekableReadStream &stream);
-	virtual ~Inventory() override;
+	~Inventory() override;
 
-	virtual bool updateInput() override;
+	bool updateInput() override;
 
 	void initItems();
 	void updateItemsByActiveCharacter();
diff --git a/engines/alcachofa/scheduler.h b/engines/alcachofa/scheduler.h
index 3ba95f298fc..1a83a620e76 100644
--- a/engines/alcachofa/scheduler.h
+++ b/engines/alcachofa/scheduler.h
@@ -115,9 +115,9 @@ private:
 struct DelayTask : public Task {
 	DelayTask(Process &process, uint32 millis);
 	DelayTask(Process &process, Common::Serializer &s);
-	virtual TaskReturn run() override;
-	virtual void debugPrint() override;
-	virtual void syncGame(Common::Serializer &s) override;
+	TaskReturn run() override;
+	void debugPrint() override;
+	void syncGame(Common::Serializer &s) override;
 	virtual const char *taskName() const override;
 
 private:
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 9cea4464da3..e376644a7c6 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -142,7 +142,7 @@ struct ScriptTimerTask : public Task {
 		syncGame(s);
 	}
 
-	virtual TaskReturn run() override {
+	TaskReturn run() override {
 		TASK_BEGIN;
 		{
 			uint32 timeSinceTimer = g_engine->script()._scriptTimer == 0 ? 0
@@ -158,7 +158,7 @@ struct ScriptTimerTask : public Task {
 		TASK_END;
 	}
 
-	virtual void debugPrint() override {
+	void debugPrint() override {
 		g_engine->getDebugger()->debugPrintf("Check input timer for %dsecs", _durationSec);
 	}
 
@@ -232,7 +232,7 @@ struct ScriptTask : public Task {
 		syncGame(s);
 	}
 
-	virtual TaskReturn run() override {
+	TaskReturn run() override {
 		if (_isFirstExecution || _returnsFromKernelCall)
 			setCharacterVariables();
 		if (_returnsFromKernelCall) {
diff --git a/engines/alcachofa/sounds.h b/engines/alcachofa/sounds.h
index 2ebe71ad4b8..02380d7a5d0 100644
--- a/engines/alcachofa/sounds.h
+++ b/engines/alcachofa/sounds.h
@@ -86,8 +86,8 @@ private:
 struct PlaySoundTask final : public Task {
 	PlaySoundTask(Process &process, SoundHandle soundHandle);
 	PlaySoundTask(Process &process, Common::Serializer &s);
-	virtual TaskReturn run() override;
-	virtual void debugPrint() override;
+	TaskReturn run() override;
+	void debugPrint() override;
 	virtual const char *taskName() const override;
 private:
 	SoundHandle _soundHandle;
@@ -96,8 +96,8 @@ private:
 struct WaitForMusicTask final : public Task {
 	WaitForMusicTask(Process &process);
 	WaitForMusicTask(Process &process, Common::Serializer &s);
-	virtual TaskReturn run() override;
-	virtual void debugPrint() override;
+	TaskReturn run() override;
+	void debugPrint() override;
 	virtual const char *taskName() const override;
 private:
 	FakeLock _lock;


Commit: e31e933cb365114d7ffbbac7d25d30a722297c4e
    https://github.com/scummvm/scummvm/commit/e31e933cb365114d7ffbbac7d25d30a722297c4e
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:57+02:00

Commit Message:
ALCACHOFA: Fix various bugs related to saving/loading

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/alcachofa.h
    engines/alcachofa/camera.cpp
    engines/alcachofa/common.cpp
    engines/alcachofa/common.h
    engines/alcachofa/detection.cpp
    engines/alcachofa/detection.h
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/general-objects.cpp
    engines/alcachofa/global-ui.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/menu.cpp
    engines/alcachofa/menu.h
    engines/alcachofa/metaengine.cpp
    engines/alcachofa/metaengine.h
    engines/alcachofa/player.cpp
    engines/alcachofa/scheduler.cpp
    engines/alcachofa/scheduler.h
    engines/alcachofa/script.cpp
    engines/alcachofa/sounds.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index c9062244fd9..b1562fc6bb9 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -23,6 +23,7 @@
 #include "common/config-manager.h"
 #include "common/debug-channels.h"
 #include "common/events.h"
+#include "common/savefile.h"
 #include "common/system.h"
 #include "engines/util.h"
 #include "graphics/paletteman.h"
@@ -79,8 +80,11 @@ Common::Error AlcachofaEngine::run() {
 	_menu.reset(new Menu());
 	setMillis(0);
 
-	_script->createProcess(MainCharacterKind::None, "CREDITOS_INICIALES");
-	_scheduler.run();
+	if (!tryLoadFromLauncher()) {
+		_script->createProcess(MainCharacterKind::None, "CREDITOS_INICIALES");
+		_scheduler.run();
+		// we run once to set the initial room, otherwise we could run into currentRoom == nullptr
+	}
 
 	Common::Event e;
 	Graphics::FrameLimiter limiter(g_system, kDefaultFramerate, false);
@@ -236,7 +240,9 @@ void AlcachofaEngine::pauseEngineIntern(bool pause) {
 Common::Error AlcachofaEngine::syncGame(Serializer &s) {
 	s.syncVersion((Serializer::Version)SaveVersion::Initial);
 
-	uint32 millis = getMillis();
+	uint32 millis = menu().isOpen()
+		? menu().millisBeforeMenu()
+		: getMillis();
 	s.syncAsUint32LE(millis);
 	if (s.isLoading())
 		setMillis(millis);
@@ -247,6 +253,7 @@ Common::Error AlcachofaEngine::syncGame(Serializer &s) {
 	 *    can assert that the semaphores are released on loading.
 	 * 2. The player should come late as it changes the room
 	 * 3. With the room current, the tasks can now better find the referenced objects
+	 * 4. Redundant: The world has to be synced before the tasks to reset the semaphores to 0
 	 */
 
 	scheduler().prepareSyncGame(s);
@@ -258,6 +265,7 @@ Common::Error AlcachofaEngine::syncGame(Serializer &s) {
 	scheduler().syncGame(s);
 
 	if (s.isLoading()) {
+		menu().resetAfterLoad();
 		sounds().stopAll();
 		sounds().setMusicToRoom(player().currentRoom()->musicID());
 	}
@@ -265,6 +273,19 @@ Common::Error AlcachofaEngine::syncGame(Serializer &s) {
 	return Common::kNoError;
 }
 
+bool AlcachofaEngine::tryLoadFromLauncher() {
+	int saveSlot = ConfMan.getInt("save_slot");
+	if (!ConfMan.hasKey("save_slot") || saveSlot < 0)
+		return false;
+	auto *saveFileMgr = g_system->getSavefileManager();
+	auto *saveFile = saveFileMgr->openForLoading(getSaveStateName(saveSlot));
+	if (saveFile == nullptr)
+		return false;
+	bool result = loadGameStream(saveFile).getCode() == kNoError;
+	delete saveFile;
+	return result;
+}
+
 Config::Config() {
 	loadFromScummVM();
 }
@@ -284,6 +305,7 @@ void Config::saveToScummVM() {
 	ConfMan.setInt("music_volume", _musicVolume);
 	ConfMan.setInt("speech_volume", _speechVolume);
 	ConfMan.setInt("sfx_volume", _speechVolume);
+	ConfMan.flushToDisk();
 	// ^ a bit unfortunate, that means if you change in-game it overrides.
 	// if you set it in ScummVMs dialog it sticks
 }
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index 2c30ae6496d..02ab4373a9c 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -138,9 +138,11 @@ public:
 	};
 
 	bool canLoadGameStateCurrently(Common::U32String *msg = nullptr) override {
+		// TODO: Implement
 		return true;
 	}
 	bool canSaveGameStateCurrently(Common::U32String *msg = nullptr) override {
+		// TODO: Implement
 		return true;
 	}
 
@@ -155,6 +157,8 @@ public:
 	}
 
 private:
+	bool tryLoadFromLauncher();
+
 	Console *_console = new Console();
 	Common::ScopedPtr<IDebugHandler> _debugHandler;
 	Common::ScopedPtr<IRenderer> _renderer;
diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index 3c77a4d99f7..1b89a38c45c 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -377,7 +377,7 @@ protected:
 		_camera._cur._scale = _fromScale + _deltaScale * t;
 	}
 
-	float _fromScale, _deltaScale;
+	float _fromScale = 0, _deltaScale = 0;
 };
 DECLARE_TASK(CamLerpScaleTask);
 
@@ -418,8 +418,8 @@ protected:
 	}
 
 	Vector3d _fromPos, _deltaPos;
-	float _fromScale, _deltaScale;
-	EasingType _moveEasingType, _scaleEasingType;
+	float _fromScale = 0, _deltaScale = 0;
+	EasingType _moveEasingType = {}, _scaleEasingType = {};
 };
 DECLARE_TASK(CamLerpPosScaleTask);
 
@@ -447,7 +447,7 @@ protected:
 		_camera._cur._rotation = Angle(_fromRotation + _deltaRotation * t);
 	}
 
-	float _fromRotation, _deltaRotation;
+	float _fromRotation = 0, _deltaRotation = 0;
 };
 DECLARE_TASK(CamLerpRotationTask);
 
@@ -579,9 +579,9 @@ struct CamSetInactiveAttributeTask final : public Task {
 
 private:
 	Camera &_camera;
-	Attribute _attribute;
-	float _value;
-	int32 _delay;
+	Attribute _attribute = {};
+	float _value = 0;
+	int32 _delay = 0;
 };
 DECLARE_TASK(CamSetInactiveAttributeTask);
 
diff --git a/engines/alcachofa/common.cpp b/engines/alcachofa/common.cpp
index 57be9a3d27e..1b1e5564466 100644
--- a/engines/alcachofa/common.cpp
+++ b/engines/alcachofa/common.cpp
@@ -20,6 +20,7 @@
  */
 
 #include "common.h"
+#include "detection.h"
 
 using namespace Common;
 using namespace Math;
@@ -36,7 +37,9 @@ float ease(float t, EasingType type) {
 	}
 }
 
-FakeSemaphore::FakeSemaphore(uint initialCount) : _counter(initialCount) {}
+FakeSemaphore::FakeSemaphore(const char *name, uint initialCount)
+	: _name(name)
+	, _counter(initialCount) {}
 
 FakeSemaphore::~FakeSemaphore() {
 	assert(_counter == 0);
@@ -46,21 +49,23 @@ void FakeSemaphore::sync(Serializer &s, FakeSemaphore &semaphore) {
 	// if we are still holding locks during loading these locks will
 	// try to decrease the counter which will fail, let's find this out already here
 	assert(s.isSaving() || semaphore.isReleased());
+	(void)(s, semaphore);
 
-	uint semaphoreCounter = semaphore.counter();
-	s.syncAsSint32LE(semaphoreCounter);
-	semaphore = FakeSemaphore(semaphoreCounter);
+	// We should not actually serialize the counter, just make sure it is empty
+	// When the locks are loaded, they will increase the counter themselves
 }
 
 FakeLock::FakeLock() : _semaphore(nullptr) {}
 
 FakeLock::FakeLock(FakeSemaphore &semaphore) : _semaphore(&semaphore) {
 	_semaphore->_counter++;
+	debugC(kDebugSemaphores, "Lock ctor %s to %u", _semaphore->_name, _semaphore->_counter);
 }
 
 FakeLock::FakeLock(const FakeLock &other) : _semaphore(other._semaphore) {
 	assert(_semaphore != nullptr);
 	_semaphore->_counter++;
+	debugC(kDebugSemaphores, "Lock copy %s to %u", _semaphore->_name, _semaphore->_counter);
 }
 
 FakeLock::FakeLock(FakeLock &&other) noexcept : _semaphore(other._semaphore) {
@@ -80,6 +85,7 @@ void FakeLock::release() {
 	if (_semaphore == nullptr)
 		return;
 	assert(_semaphore->_counter > 0);
+	debugC(kDebugSemaphores, "Lock dtor %s to %u", _semaphore->_name, _semaphore->_counter - 1);
 	_semaphore->_counter--;
 	_semaphore = nullptr;
 }
diff --git a/engines/alcachofa/common.h b/engines/alcachofa/common.h
index 7631bf81838..0394653f657 100644
--- a/engines/alcachofa/common.h
+++ b/engines/alcachofa/common.h
@@ -83,7 +83,7 @@ static constexpr const Color kDebugLightBlue = { 80, 80, 255, 190 };
  * It is used as a safer option for a simple "isBusy" counter
  */
 struct FakeSemaphore {
-	FakeSemaphore(uint initialCount = 0);
+	FakeSemaphore(const char *name, uint initialCount = 0);
 	~FakeSemaphore();
 
 	inline bool isReleased() const { return _counter == 0; }
@@ -92,6 +92,7 @@ struct FakeSemaphore {
 	static void sync(Common::Serializer &s, FakeSemaphore &semaphore);
 private:
 	friend struct FakeLock;
+	const char *const _name;
 	uint _counter = 0;
 };
 
diff --git a/engines/alcachofa/detection.cpp b/engines/alcachofa/detection.cpp
index 23b2d7543d6..f8e59c3ddf8 100644
--- a/engines/alcachofa/detection.cpp
+++ b/engines/alcachofa/detection.cpp
@@ -34,6 +34,7 @@ const DebugChannelDef AlcachofaMetaEngineDetection::debugFlagList[] = {
 	{ Alcachofa::kDebugScript, "Script", "Enable debug script dump" },
 	{ Alcachofa::kDebugGameplay, "Gameplay", "Gameplay-related tracing" },
 	{ Alcachofa::kDebugSounds, "Sounds", "Sound- and Music-related tracing" },
+	{ Alcachofa::kDebugSemaphores, "Semaphores", "Tracing operations on semaphores" },
 	DEBUG_CHANNEL_END
 };
 
diff --git a/engines/alcachofa/detection.h b/engines/alcachofa/detection.h
index 8e37d4b99d3..982cc618159 100644
--- a/engines/alcachofa/detection.h
+++ b/engines/alcachofa/detection.h
@@ -31,6 +31,7 @@ enum AlcachofaDebugChannels {
 	kDebugScript,
 	kDebugGameplay,
 	kDebugSounds,
+	kDebugSemaphores
 };
 
 extern const PlainGameDescriptor alcachofaGames[];
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 3dc0e585bbd..9a4f47128eb 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -353,8 +353,8 @@ struct SayTextTask final : public Task {
 	virtual const char *taskName() const override;
 
 private:
-	Character *_character;
-	int32 _dialogId;
+	Character *_character = nullptr;
+	int32 _dialogId = 0;
 	SoundHandle _soundHandle = {};
 };
 DECLARE_TASK(SayTextTask);
@@ -431,9 +431,9 @@ struct AnimateCharacterTask final : public Task {
 	virtual const char *taskName() const override;
 
 private:
-	Character *_character;
-	ObjectBase *_animateObject;
-	Graphic *_graphic;
+	Character *_character = nullptr;
+	ObjectBase *_animateObject = nullptr;
+	Graphic *_graphic = nullptr;
 };
 DECLARE_TASK(AnimateCharacterTask);
 
@@ -488,9 +488,9 @@ struct LerpLodBiasTask final : public Task {
 	virtual const char *taskName() const override;
 
 private:
-	Character *_character;
-	float _sourceLodBias = 0, _targetLodBias;
-	uint32 _startTime = 0, _durationMs;
+	Character *_character = nullptr;
+	float _sourceLodBias = 0, _targetLodBias = 0;
+	uint32 _startTime = 0, _durationMs = 0;
 };
 DECLARE_TASK(LerpLodBiasTask);
 
@@ -802,7 +802,7 @@ struct ArriveTask : public Task {
 
 	virtual const char *taskName() const override;
 private:
-	const WalkingCharacter *_character;
+	const WalkingCharacter *_character = nullptr;
 };
 DECLARE_TASK(ArriveTask);
 
@@ -813,7 +813,8 @@ Task *WalkingCharacter::waitForArrival(Process &process) {
 const char *MainCharacter::typeName() const { return "MainCharacter"; }
 
 MainCharacter::MainCharacter(Room *room, ReadStream &stream)
-	: WalkingCharacter(room, stream) {
+	: WalkingCharacter(room, stream)
+	, _semaphore(name().firstChar() == 'M' ? "mortadelo" : "filemon") {
 	stream.readByte(); // unused byte
 	_order = 100;
 
@@ -1125,7 +1126,7 @@ private:
 	}
 
 	Input &_input;
-	MainCharacter *_character;
+	MainCharacter *_character = nullptr;
 	uint _clickedLineI = UINT_MAX;
 };
 DECLARE_TASK(DialogMenuTask);
diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index be14b9c99b2..ca99e56bb3a 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -169,9 +169,9 @@ struct AnimateTask : public Task {
 	virtual const char *taskName() const override;
 
 private:
-	GraphicObject *_object;
-	Graphic *_graphic;
-	uint32 _duration;
+	GraphicObject *_object = nullptr;
+	Graphic *_graphic = nullptr;
+	uint32 _duration = 0;
 };
 DECLARE_TASK(AnimateTask);
 
diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index 6e379c5622b..0d842c56986 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -239,8 +239,8 @@ struct CenterBottomTextTask : public Task {
 	virtual const char *taskName() const override;
 
 private:
-	int32 _dialogId;
-	uint32 _startTime = 0, _durationMs;
+	int32 _dialogId = 0;
+	uint32 _startTime = 0, _durationMs = 0;
 };
 DECLARE_TASK(CenterBottomTextTask);
 
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 242ce7c67d9..86332477c09 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -824,12 +824,12 @@ private:
 		g_engine->drawQueue().add<FadeDrawRequest>(_fadeType, _from + (_to - _from) * ease(t, _easingType), _order);
 	}
 
-	FadeType _fadeType;
-	float _from, _to;
-	uint32 _startTime = 0, _duration;
-	EasingType _easingType;
-	int8 _order;
-	PermanentFadeAction _permanentFadeAction;
+	FadeType _fadeType = {};
+	float _from = 0, _to = 0;
+	uint32 _startTime = 0, _duration = 0;
+	EasingType _easingType = {};
+	int8 _order = 0;
+	PermanentFadeAction _permanentFadeAction = {};
 };
 DECLARE_TASK(FadeTask);
 
diff --git a/engines/alcachofa/menu.cpp b/engines/alcachofa/menu.cpp
index b0abf19233c..61369a3b747 100644
--- a/engines/alcachofa/menu.cpp
+++ b/engines/alcachofa/menu.cpp
@@ -26,6 +26,12 @@
 
 namespace Alcachofa {
 
+void Menu::resetAfterLoad() {
+	_isOpen = false;
+	_openAtNextFrame = false;
+	_previousRoom = nullptr;
+}
+
 void Menu::updateOpeningMenu() {
 	if (!_openAtNextFrame) {
 		_openAtNextFrame =
@@ -36,7 +42,7 @@ void Menu::updateOpeningMenu() {
 	_openAtNextFrame = false;
 
 	g_engine->sounds().pauseAll(true);
-	_timeBeforeMenu = g_engine->getMillis();
+	_millisBeforeMenu = g_engine->getMillis();
 	_previousRoom = g_engine->player().currentRoom();
 	_isOpen = true;
 	// TODO: Render thumbnail
@@ -60,7 +66,7 @@ void Menu::continueGame() {
 	g_engine->sounds().pauseAll(false);
 	g_engine->camera().restore(1);
 	g_engine->scheduler().restoreContext();
-	g_engine->setMillis(_timeBeforeMenu);
+	g_engine->setMillis(_millisBeforeMenu);
 }
 
 void Menu::triggerMainMenuAction(MainMenuAction action) {
diff --git a/engines/alcachofa/menu.h b/engines/alcachofa/menu.h
index 712a74ff219..18e234e56fb 100644
--- a/engines/alcachofa/menu.h
+++ b/engines/alcachofa/menu.h
@@ -59,7 +59,10 @@ enum class OptionsMenuValue : int32 {
 class Menu {
 public:
 	inline bool isOpen() const { return _isOpen; }
+	inline uint32 millisBeforeMenu() const { return _millisBeforeMenu; }
+	inline Room *previousRoom() { return _previousRoom; }
 
+	void resetAfterLoad();
 	void updateOpeningMenu();
 	void triggerMainMenuAction(MainMenuAction action);
 
@@ -75,7 +78,7 @@ private:
 	bool
 		_isOpen = false,
 		_openAtNextFrame = false;
-	uint32 _timeBeforeMenu = 0;
+	uint32 _millisBeforeMenu = 0;
 	Room *_previousRoom = nullptr;
 };
 
diff --git a/engines/alcachofa/metaengine.cpp b/engines/alcachofa/metaengine.cpp
index 336f589ea9f..15fbbfa5cb7 100644
--- a/engines/alcachofa/metaengine.cpp
+++ b/engines/alcachofa/metaengine.cpp
@@ -28,6 +28,7 @@
 #include "alcachofa/alcachofa.h"
 
 using namespace Common;
+using namespace Graphics;
 using namespace Alcachofa;
 
 namespace Alcachofa {
@@ -104,6 +105,11 @@ KeymapArray AlcachofaMetaEngine::initKeymaps(const char *target) const {
 	return Keymap::arrayOf(keymap);
 }
 
+void AlcachofaMetaEngine::getSavegameThumbnail(Surface &surf) {
+	// TODO: Implement
+	surf.create(160, 120, PixelFormat::createFormatRGBA32());
+}
+
 #if PLUGIN_ENABLED_DYNAMIC(ALCACHOFA)
 REGISTER_PLUGIN_DYNAMIC(ALCACHOFA, PLUGIN_TYPE_ENGINE, AlcachofaMetaEngine);
 #else
diff --git a/engines/alcachofa/metaengine.h b/engines/alcachofa/metaengine.h
index 58d9f56607f..5edef077507 100644
--- a/engines/alcachofa/metaengine.h
+++ b/engines/alcachofa/metaengine.h
@@ -49,6 +49,8 @@ public:
 
 	Common::KeymapArray initKeymaps(const char *target) const override;
 
+	void getSavegameThumbnail(Graphics::Surface &thumb) override;
+
 	
 };
 
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index a9fecdb049a..2dc95a755da 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -29,7 +29,8 @@ using namespace Common;
 namespace Alcachofa {
 
 Player::Player()
-	: _activeCharacter(&g_engine->world().mortadelo()) {
+	: _activeCharacter(&g_engine->world().mortadelo())
+	, _semaphore("player") {
 	const auto &cursorPath = g_engine->world().getGlobalAnimationName(GlobalAnimationKind::Cursor);
 	_cursorAnimation.reset(new Animation(cursorPath));
 	_cursorAnimation->load();
@@ -153,7 +154,7 @@ MainCharacter *Player::inactiveCharacter() const {
 }
 
 FakeSemaphore &Player::semaphoreFor(MainCharacterKind kind) {
-	static FakeSemaphore dummySemaphore;
+	static FakeSemaphore dummySemaphore("dummy");
 	switch (kind) {
 	case MainCharacterKind::None: return _semaphore;
 	case MainCharacterKind::Mortadelo: return g_engine->world().mortadelo().semaphore();
@@ -278,11 +279,11 @@ private:
 	}
 
 	FakeLock _lock, _musicLock;
-	const Door *_sourceDoor;
-	const InteractableObject *_targetObject;
-	Direction _targetDirection;
-	Room *_targetRoom;
-	MainCharacter *_character;
+	const Door *_sourceDoor = nullptr;
+	const InteractableObject *_targetObject = nullptr;
+	Direction _targetDirection = {};
+	Room *_targetRoom = nullptr;
+	MainCharacter *_character = nullptr;
 	Player &_player;
 };
 DECLARE_TASK(DoorTask);
@@ -346,27 +347,22 @@ void Player::syncGame(Serializer &s) {
 	}
 
 	FakeSemaphore::sync(s, _semaphore);
-
+	
 	String roomName;
-	if (_roomBeforeInventory != nullptr)
-		roomName = _roomBeforeInventory->name();
-	s.syncString(roomName);
-	if (s.isLoading()) {
-		if (roomName.empty())
-			_roomBeforeInventory = nullptr;
-		else {
-			_roomBeforeInventory = g_engine->world().getRoomByName(roomName.c_str());
-			scumm_assert(_roomBeforeInventory != nullptr);
-		}
+	if (s.isSaving()) {
+		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
+			: currentRoom()->name(); // save from ScumnmVM global menu or autosave in normal gameplay
 	}
-
-	roomName = currentRoom()->name();
 	s.syncString(roomName);
 	if (s.isLoading()) {
 		_selectedObject = nullptr;
 		_pressedObject = nullptr;
 		_heldItem = nullptr;
 		_nextLastDialogCharacter = 0;
+		_isGameLoaded = true;
+		_roomBeforeInventory = nullptr;
 		fill(_lastDialogCharacters, _lastDialogCharacters + kMaxLastDialogCharacters, nullptr);
 		changeRoom(roomName, true);
 	}
diff --git a/engines/alcachofa/scheduler.cpp b/engines/alcachofa/scheduler.cpp
index 31ab38b8e1a..b9feb436154 100644
--- a/engines/alcachofa/scheduler.cpp
+++ b/engines/alcachofa/scheduler.cpp
@@ -23,6 +23,7 @@
 
 #include "common/system.h"
 #include "alcachofa.h"
+#include "menu.h"
 
 using namespace Common;
 
@@ -59,7 +60,7 @@ void Task::syncGame(Serializer &s) {
 	s.syncAsUint32LE(_stage);
 }
 
-void Task::syncObjectAsString(Serializer &s, ObjectBase *&object, bool optional) {
+void Task::syncObjectAsString(Serializer &s, ObjectBase *&object, bool optional) const {
 	String objectName, roomName;
 	if (object != nullptr) {
 		roomName = object->room()->name();
@@ -71,7 +72,9 @@ void Task::syncObjectAsString(Serializer &s, ObjectBase *&object, bool optional)
 		return;
 	Room *room = g_engine->world().getRoomByName(roomName.c_str());
 	object = room == nullptr ? nullptr : room->getObjectByName(objectName.c_str());
-	if ((object == nullptr && !optional) || !roomName.empty() || !objectName.empty())
+	if (object == nullptr) // main characters are not linked by the room they are in
+		object = g_engine->world().globalRoom().getObjectByName(objectName.c_str());
+	if (object == nullptr && !optional)
 		error("Invalid object name \"%s\" in room \"%s\" in savestate for task %s",
 			objectName.c_str(), roomName.c_str(), taskName());
 }
@@ -193,8 +196,12 @@ void Process::syncGame(Serializer &s) {
 			_tasks.push(readTask(*this, s));
 	}
 	else {
-		for (uint i = 0; i < count; i++)
+		String taskName;
+		for (uint i = 0; i < count; i++) {
+			taskName = _tasks[i]->taskName();
+			s.syncString(taskName);
 			_tasks[i]->syncGame(s);
+		}
 	}
 }
 
@@ -336,19 +343,23 @@ void Scheduler::syncGame(Serializer &s) {
 	assert(_currentProcessI == UINT_MAX); // let's not sync during ::run
 	assert(s.isSaving() || _backupProcesses.empty());
 
+	Common::Array<Process *> *processes = s.isSaving() && g_engine->menu().isOpen()
+		? &_backupProcesses
+		: &processesToRunNext();
+
 	// we only sync the backupProcesses as these are the ones pertaining to the gameplay
 	// the other arrays would be used for the menu
 
 	s.syncAsUint32LE(_nextPid);
-	uint32 count = _backupProcesses.size();
+	uint32 count = processes->size();
 	s.syncAsUint32LE(count);
 	if (s.isLoading()) {
-		_backupProcesses.reserve(count);
+		processes->reserve(count);
 		for (uint32 i = 0; i < count; i++)
-			_backupProcesses.push_back(new Process(s));
+			processes->push_back(new Process(s));
 	}
 	else {
-		for (Process *process : _backupProcesses)
+		for (Process *process : *processes)
 			process->syncGame(s);
 	}
 }
diff --git a/engines/alcachofa/scheduler.h b/engines/alcachofa/scheduler.h
index 1a83a620e76..bb8fc261bb5 100644
--- a/engines/alcachofa/scheduler.h
+++ b/engines/alcachofa/scheduler.h
@@ -94,11 +94,11 @@ struct Task {
 protected:
 	Task *delay(uint32 millis);
 
-	void syncObjectAsString(Common::Serializer &s, ObjectBase *&object, bool optional = false);
+	void syncObjectAsString(Common::Serializer &s, ObjectBase *&object, bool optional = false) const;
 	template<class TObject>
-	void syncObjectAsString(Common::Serializer &s, TObject *&object, bool optional = false) {
+	void syncObjectAsString(Common::Serializer &s, TObject *&object, bool optional = false) const {
 		// We could add is_const and therefore true_type, false_type, integral_constant 
-		// or we could just use const_cast and promise that we won't modify
+		// or we could just use const_cast and promise that we won't modify the object itself
 		ObjectBase *base = const_cast<Common::remove_const_t<TObject> *>(object);
 		syncObjectAsString(s, base, optional);
 		object = dynamic_cast<TObject*>(base);
@@ -121,7 +121,7 @@ struct DelayTask : public Task {
 	virtual const char *taskName() const override;
 
 private:
-	uint32 _endTime;
+	uint32 _endTime = 0;
 };
 
 #define DECLARE_TASK(TaskName) \
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index e376644a7c6..2deddae1472 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -171,7 +171,7 @@ struct ScriptTimerTask : public Task {
 	virtual const char *taskName() const override;
 
 private:
-	int32 _durationSec;
+	int32 _durationSec = 0;
 	int32 _result = 1;
 };
 DECLARE_TASK(ScriptTimerTask);
@@ -392,7 +392,7 @@ struct ScriptTask : public Task {
 
 		bool hasLock = !_lock.isReleased();
 		s.syncAsByte(hasLock);
-		if (hasLock)
+		if (s.isLoading() && hasLock)
 			_lock = FakeLock(g_engine->player().semaphoreFor(process().character()));
 	}
 
@@ -945,7 +945,7 @@ private:
 	Script &_script;
 	Stack<StackEntry> _stack;
 	String _name;
-	uint32 _pc;
+	uint32 _pc = 0;
 	bool _returnsFromKernelCall = false;
 	bool _isFirstExecution = true;
 	FakeLock _lock;
diff --git a/engines/alcachofa/sounds.cpp b/engines/alcachofa/sounds.cpp
index 6d719b44e16..f3451db1ccf 100644
--- a/engines/alcachofa/sounds.cpp
+++ b/engines/alcachofa/sounds.cpp
@@ -41,7 +41,9 @@ void Sounds::Playback::fadeOut(uint32 duration) {
 	_fadeDuration = MAX<uint32>(duration, 1);
 }
 
-Sounds::Sounds() : _mixer(g_system->getMixer()) {
+Sounds::Sounds()
+	: _mixer(g_system->getMixer())
+	, _musicSemaphore("music") {
 	assert(_mixer != nullptr);
 }
 


Commit: 6a49931fed2839c4b3812fe26f2bc8e64d41bcd8
    https://github.com/scummvm/scummvm/commit/6a49931fed2839c4b3812fe26f2bc8e64d41bcd8
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:57+02:00

Commit Message:
ALCACHOFA: Implement canLoadGameStateCurrently

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/alcachofa.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 b1562fc6bb9..53ece3b3834 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -52,7 +52,8 @@ AlcachofaEngine *g_engine;
 AlcachofaEngine::AlcachofaEngine(OSystem *syst, const ADGameDescription *gameDesc)
 	: Engine(syst)
 	, _gameDescription(gameDesc)
-	, _randomSource("Alcachofa") {
+	, _randomSource("Alcachofa")
+	, _eventLoopSemaphore("engine") {
 	g_engine = this;
 }
 
@@ -118,6 +119,7 @@ Common::Error AlcachofaEngine::run() {
 }
 
 void AlcachofaEngine::playVideo(int32 videoId) {
+	FakeLock lock(_eventLoopSemaphore);
 	Video::MPEGPSDecoder decoder;
 	if (!decoder.loadFile(Common::Path(Common::String::format("Data/DATA%02d.BIN", videoId + 1))))
 		error("Could not find video %d", videoId);
@@ -156,6 +158,7 @@ void AlcachofaEngine::playVideo(int32 videoId) {
 
 void AlcachofaEngine::fadeExit() {
 	constexpr uint kFadeOutDuration = 1000;
+	FakeLock lock(_eventLoopSemaphore);
 	Event e;
 	Graphics::FrameLimiter limiter(g_system, kDefaultFramerate, false);
 	uint32 startTime = g_system->getMillis();
@@ -237,6 +240,14 @@ void AlcachofaEngine::pauseEngineIntern(bool pause) {
 		setMillis(_timeBeforePause);
 }
 
+bool AlcachofaEngine::canLoadGameStateCurrently(U32String *msg) {
+	if (!_eventLoopSemaphore.isReleased())
+		return false;
+	return
+		(menu().isOpen() && menu().interactionSemaphore().isReleased()) ||
+		player().isAllowedToOpenMenu();
+}
+
 Common::Error AlcachofaEngine::syncGame(Serializer &s) {
 	s.syncVersion((Serializer::Version)SaveVersion::Initial);
 
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index 02ab4373a9c..4508dba9ae3 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -137,13 +137,9 @@ public:
 			(f == kSupportsReturnToLauncher);
 	};
 
-	bool canLoadGameStateCurrently(Common::U32String *msg = nullptr) override {
-		// TODO: Implement
-		return true;
-	}
+	bool canLoadGameStateCurrently(Common::U32String *msg = nullptr) override;
 	bool canSaveGameStateCurrently(Common::U32String *msg = nullptr) override {
-		// TODO: Implement
-		return true;
+		return canLoadGameStateCurrently(msg);
 	}
 
 	Common::Error syncGame(Common::Serializer &s);
@@ -175,6 +171,7 @@ private:
 	Scheduler _scheduler;
 	Config _config;
 
+	FakeSemaphore _eventLoopSemaphore; // for special states like playVideo and fadeExit
 	uint32 _timeNegOffset = 0, _timePosOffset = 0;
 	uint32 _timeBeforePause = 0;
 };
diff --git a/engines/alcachofa/menu.cpp b/engines/alcachofa/menu.cpp
index 61369a3b747..512ced75f15 100644
--- a/engines/alcachofa/menu.cpp
+++ b/engines/alcachofa/menu.cpp
@@ -26,6 +26,8 @@
 
 namespace Alcachofa {
 
+Menu::Menu() : _interactionSemaphore("menu") {}
+
 void Menu::resetAfterLoad() {
 	_isOpen = false;
 	_openAtNextFrame = false;
diff --git a/engines/alcachofa/menu.h b/engines/alcachofa/menu.h
index 18e234e56fb..79fe44a8342 100644
--- a/engines/alcachofa/menu.h
+++ b/engines/alcachofa/menu.h
@@ -58,9 +58,12 @@ enum class OptionsMenuValue : int32 {
 
 class Menu {
 public:
+	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();
@@ -80,6 +83,7 @@ private:
 		_openAtNextFrame = false;
 	uint32 _millisBeforeMenu = 0;
 	Room *_previousRoom = nullptr;
+	FakeSemaphore _interactionSemaphore; // to prevent ScummVM loading during button clicks
 };
 
 }
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index e225c64d48c..a0c133eb000 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -186,6 +186,7 @@ private:
 		_graphicHovered,
 		_graphicClicked,
 		_graphicDisabled;
+	FakeLock _interactionLock;
 };
 
 // some of the UI elements are only used for the multiplayer menus
diff --git a/engines/alcachofa/ui-objects.cpp b/engines/alcachofa/ui-objects.cpp
index 482312fb9ef..dd310c81592 100644
--- a/engines/alcachofa/ui-objects.cpp
+++ b/engines/alcachofa/ui-objects.cpp
@@ -68,6 +68,7 @@ void MenuButton::update() {
 		return;
 	}
 
+	_interactionLock.release();
 	_triggerNextFrame = false;
 	_isClicked = false;
 	trigger();
@@ -90,7 +91,8 @@ void MenuButton::freeResources() {
 void MenuButton::onHoverUpdate() {}
 
 void MenuButton::onClick() {
-	if (_isInteractable) {
+	if (_isInteractable && _interactionLock.isReleased()) {
+		_interactionLock = g_engine->menu().interactionSemaphore();
 		_isClicked = true;
 		_triggerNextFrame = false;
 		_graphicClicked.start(false);


Commit: 1650b7610c70bbf6af51eb73f10300d425349e59
    https://github.com/scummvm/scummvm/commit/1650b7610c70bbf6af51eb73f10300d425349e59
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:57+02:00

Commit Message:
ALCACHOFA: Add loading from in-game menu

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/alcachofa.h
    engines/alcachofa/input.cpp
    engines/alcachofa/menu.cpp
    engines/alcachofa/menu.h
    engines/alcachofa/metaengine.cpp
    engines/alcachofa/metaengine.h
    engines/alcachofa/scheduler.h


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 53ece3b3834..fcee80cff6f 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -31,6 +31,7 @@
 #include "video/mpegps_decoder.h"
 
 #include "alcachofa.h"
+#include "metaengine.h"
 #include "console.h"
 #include "detection.h"
 #include "player.h"
@@ -94,6 +95,9 @@ Common::Error AlcachofaEngine::run() {
 		while (g_system->getEventManager()->pollEvent(e)) {
 			if (_input.handleEvent(e))
 				continue;
+			if (e.type == EVENT_CUSTOM_ENGINE_ACTION_START &&
+				e.customType == (CustomEventType)EventAction::LoadFromMenu)
+				menu().triggerLoad();
 		}
 
 		_sounds.update();
@@ -248,6 +252,10 @@ bool AlcachofaEngine::canLoadGameStateCurrently(U32String *msg) {
 		player().isAllowedToOpenMenu();
 }
 
+Common::String AlcachofaEngine::getSaveStatePattern() {
+	return getMetaEngine()->getSavegameFilePattern(_targetName.c_str());
+}
+
 Common::Error AlcachofaEngine::syncGame(Serializer &s) {
 	s.syncVersion((Serializer::Version)SaveVersion::Initial);
 
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index 4508dba9ae3..975de0b50ce 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -142,6 +142,7 @@ public:
 		return canLoadGameStateCurrently(msg);
 	}
 
+	Common::String getSaveStatePattern();
 	Common::Error syncGame(Common::Serializer &s);
 	Common::Error saveGameStream(Common::WriteStream *stream, bool isAutosave = false) override {
 		Common::Serializer s(nullptr, stream);
diff --git a/engines/alcachofa/input.cpp b/engines/alcachofa/input.cpp
index 7d92310d54d..bfa2391c360 100644
--- a/engines/alcachofa/input.cpp
+++ b/engines/alcachofa/input.cpp
@@ -70,8 +70,8 @@ bool Input::handleEvent(const Common::Event &event) {
 		updateMousePos3D();
 		return true;
 	case EVENT_CUSTOM_ENGINE_ACTION_START:
-		switch ((InputAction)event.customType) {
-		case InputAction::Menu:
+		switch ((EventAction)event.customType) {
+		case EventAction::InputMenu:
 			_wasMenuKeyPressed = true;
 			return true;
 		}
diff --git a/engines/alcachofa/menu.cpp b/engines/alcachofa/menu.cpp
index 512ced75f15..10f0ab1a8e9 100644
--- a/engines/alcachofa/menu.cpp
+++ b/engines/alcachofa/menu.cpp
@@ -20,13 +20,18 @@
  */
 
 #include "alcachofa.h"
+#include "metaengine.h"
 #include "menu.h"
 #include "player.h"
 #include "script.h"
 
+using namespace Common;
+
 namespace Alcachofa {
 
-Menu::Menu() : _interactionSemaphore("menu") {}
+Menu::Menu()
+	: _interactionSemaphore("menu")
+	, _saveFileMgr(g_system->getSavefileManager()) {}
 
 void Menu::resetAfterLoad() {
 	_isOpen = false;
@@ -49,15 +54,30 @@ void Menu::updateOpeningMenu() {
 	_isOpen = true;
 	// TODO: Render thumbnail
 	g_engine->player().changeRoom("MENUPRINCIPAL", true);
-	// TODO: Check original read lastSaveFileFileId and read options.cfg, we do not need that right?
+	_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();
+	updateSelectedSavefile();
 
 	g_engine->player().heldItem() = nullptr;
 	g_engine->scheduler().backupContext();
 	g_engine->camera().backup(1);
 	g_engine->camera().setPosition(Math::Vector3d(
 		g_system->getWidth() / 2.0f, g_system->getHeight() / 2.0f, 0.0f));
+}
+
+void Menu::updateSelectedSavefile() {
+	auto getButton = [] (const char *name) {
+		MenuButton *button = dynamic_cast<MenuButton *>(g_engine->player().currentRoom()->getObjectByName(name));
+		scumm_assert(button != nullptr);
+		return button;
+	};
 
-	// TODO: Load thumbnail into capture graphic object
+	getButton("CARGAR")->isInteractable() = _selectedSavefileI < _savefiles.size();
+	getButton("ANTERIOR")->toggle(_selectedSavefileI > 0);
+	getButton("SIGUIENTE")->toggle(_selectedSavefileI < _savefiles.size());
+
+	// TODO: Update thumbnail animation
 }
 
 void Menu::continueGame() {
@@ -79,9 +99,14 @@ void Menu::triggerMainMenuAction(MainMenuAction action) {
 	case MainMenuAction::Save:
 		warning("STUB: MainMenuAction Save");
 		break;
-	case MainMenuAction::Load:
-		warning("STUB: MainMenuAction Load");
-		break;
+	case MainMenuAction::Load: {
+		// we are in some update loop, let's load next frame upon event handling
+		// that should be safer
+		Event ev;
+		ev.type = EVENT_CUSTOM_ENGINE_ACTION_START;
+		ev.customType = (CustomEventType)EventAction::LoadFromMenu;
+		g_system->getEventManager()->pushEvent(ev);
+	}break;
 	case MainMenuAction::InternetMenu:
 		g_system->messageBox(LogMessageType::kWarning, "Multiplayer is not implemented in this ScummVM version.");
 		break;
@@ -94,10 +119,16 @@ void Menu::triggerMainMenuAction(MainMenuAction action) {
 		g_engine->fadeExit();
 		break;
 	case MainMenuAction::NextSave:
-		warning("STUB: MainMenuAction NextSave");
+		if (_selectedSavefileI < _savefiles.size()) {
+			_selectedSavefileI++;
+			updateSelectedSavefile();
+		}
 		break;
 	case MainMenuAction::PrevSave:
-		warning("STUB: MainMenuAction PrevSave");
+		if (_selectedSavefileI > 0) {
+			_selectedSavefileI--;
+			updateSelectedSavefile();
+		}
 		break;
 	case MainMenuAction::NewGame:
 		// this action might be unused just like the only room it would appear: MENUPRINCIPALINICIO
@@ -110,6 +141,12 @@ void Menu::triggerMainMenuAction(MainMenuAction action) {
 	}
 }
 
+void Menu::triggerLoad() {
+	auto *savefile = _saveFileMgr->openForLoading(_savefiles[_selectedSavefileI]);
+	g_engine->loadGameStream(savefile);
+	delete savefile;
+}
+
 void Menu::openOptionsMenu() {
 	setOptionsState();
 	g_engine->player().changeRoom("MENUOPCIONES", true);
diff --git a/engines/alcachofa/menu.h b/engines/alcachofa/menu.h
index 79fe44a8342..7665e2a6a56 100644
--- a/engines/alcachofa/menu.h
+++ b/engines/alcachofa/menu.h
@@ -23,6 +23,7 @@
 #define MENU_H
 
 #include "common/scummsys.h"
+#include "common/savefile.h"
 
 namespace Alcachofa {
 
@@ -68,12 +69,14 @@ public:
 	void resetAfterLoad();
 	void updateOpeningMenu();
 	void triggerMainMenuAction(MainMenuAction action);
+	void triggerLoad();
 
 	void openOptionsMenu();
 	void triggerOptionsAction(OptionsMenuAction action);
 	void triggerOptionsValue(OptionsMenuValue valueId, float value);
 
 private:
+	void updateSelectedSavefile();
 	void continueGame();
 	void continueMainMenu();
 	void setOptionsState();
@@ -81,9 +84,13 @@ private:
 	bool
 		_isOpen = false,
 		_openAtNextFrame = false;
-	uint32 _millisBeforeMenu = 0;
+	uint32
+		_millisBeforeMenu = 0,
+		_selectedSavefileI = 0;
 	Room *_previousRoom = nullptr;
 	FakeSemaphore _interactionSemaphore; // to prevent ScummVM loading during button clicks
+	Common::Array<Common::String> _savefiles;
+	Common::SaveFileManager *_saveFileMgr;
 };
 
 }
diff --git a/engines/alcachofa/metaengine.cpp b/engines/alcachofa/metaengine.cpp
index 15fbbfa5cb7..32dfe97f682 100644
--- a/engines/alcachofa/metaengine.cpp
+++ b/engines/alcachofa/metaengine.cpp
@@ -97,7 +97,7 @@ KeymapArray AlcachofaMetaEngine::initKeymaps(const char *target) const {
 	keymap->addAction(act);
 
 	act = new Action("MENU", _("Menu"));
-	act->setCustomEngineActionEvent((CustomEventType)InputAction::Menu);
+	act->setCustomEngineActionEvent((CustomEventType)EventAction::InputMenu);
 	act->addDefaultInputMapping("ESCAPE");
 	act->addDefaultInputMapping("JOY_START");
 	keymap->addAction(act);
diff --git a/engines/alcachofa/metaengine.h b/engines/alcachofa/metaengine.h
index 5edef077507..5e40ccf00df 100644
--- a/engines/alcachofa/metaengine.h
+++ b/engines/alcachofa/metaengine.h
@@ -26,8 +26,9 @@
 
 namespace Alcachofa {
 
-enum class InputAction {
-	Menu
+enum class EventAction {
+	LoadFromMenu,
+	InputMenu
 };
 
 }
diff --git a/engines/alcachofa/scheduler.h b/engines/alcachofa/scheduler.h
index bb8fc261bb5..9a8fab8d396 100644
--- a/engines/alcachofa/scheduler.h
+++ b/engines/alcachofa/scheduler.h
@@ -132,7 +132,6 @@ private:
 		return #TaskName; \
 	}
 
-// TODO: This probably should be scummvm common
 #if __cplusplus >= 201703L
 #define TASK_BREAK_FALLTHROUGH [[fallthrough]];
 #else


Commit: 59303318d7076d7ab006a3ff5b48c2389a138f0d
    https://github.com/scummvm/scummvm/commit/59303318d7076d7ab006a3ff5b48c2389a138f0d
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:57+02:00

Commit Message:
ALCACHOFA: Fix assert when loading while walking through door

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/common.cpp
    engines/alcachofa/common.h
    engines/alcachofa/player.cpp
    engines/alcachofa/script.cpp
    engines/alcachofa/sounds.cpp
    engines/alcachofa/ui-objects.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index fcee80cff6f..a434994a028 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -123,7 +123,7 @@ Common::Error AlcachofaEngine::run() {
 }
 
 void AlcachofaEngine::playVideo(int32 videoId) {
-	FakeLock lock(_eventLoopSemaphore);
+	FakeLock lock("playVideo", _eventLoopSemaphore);
 	Video::MPEGPSDecoder decoder;
 	if (!decoder.loadFile(Common::Path(Common::String::format("Data/DATA%02d.BIN", videoId + 1))))
 		error("Could not find video %d", videoId);
@@ -162,7 +162,7 @@ void AlcachofaEngine::playVideo(int32 videoId) {
 
 void AlcachofaEngine::fadeExit() {
 	constexpr uint kFadeOutDuration = 1000;
-	FakeLock lock(_eventLoopSemaphore);
+	FakeLock lock("fadeExit", _eventLoopSemaphore);
 	Event e;
 	Graphics::FrameLimiter limiter(g_system, kDefaultFramerate, false);
 	uint32 startTime = g_system->getMillis();
diff --git a/engines/alcachofa/common.cpp b/engines/alcachofa/common.cpp
index 1b1e5564466..ecc2d061a45 100644
--- a/engines/alcachofa/common.cpp
+++ b/engines/alcachofa/common.cpp
@@ -55,21 +55,30 @@ void FakeSemaphore::sync(Serializer &s, FakeSemaphore &semaphore) {
 	// When the locks are loaded, they will increase the counter themselves
 }
 
-FakeLock::FakeLock() : _semaphore(nullptr) {}
+FakeLock::FakeLock() {}
 
-FakeLock::FakeLock(FakeSemaphore &semaphore) : _semaphore(&semaphore) {
+FakeLock::FakeLock(const char *name, FakeSemaphore &semaphore)
+	: _name(name)
+	, _semaphore(&semaphore) {
 	_semaphore->_counter++;
-	debugC(kDebugSemaphores, "Lock ctor %s to %u", _semaphore->_name, _semaphore->_counter);
+	debug("ctor");
 }
 
-FakeLock::FakeLock(const FakeLock &other) : _semaphore(other._semaphore) {
+FakeLock::FakeLock(const FakeLock &other)
+	: _name(other._name)
+	, _semaphore(other._semaphore) {
 	assert(_semaphore != nullptr);
 	_semaphore->_counter++;
-	debugC(kDebugSemaphores, "Lock copy %s to %u", _semaphore->_name, _semaphore->_counter);
+	debug("copy");
 }
 
-FakeLock::FakeLock(FakeLock &&other) noexcept : _semaphore(other._semaphore) {
+FakeLock::FakeLock(FakeLock &&other) noexcept
+	: _name(other._name)
+	, _semaphore(other._semaphore) {
+	other._name = "<moved>";
 	other._semaphore = nullptr;
+	if (_semaphore != nullptr)
+		debug("move-ctor");
 }
 
 FakeLock::~FakeLock() {
@@ -77,19 +86,31 @@ FakeLock::~FakeLock() {
 }
 
 void FakeLock::operator= (FakeLock &&other) noexcept {
+	release();
+	_name = other._name;
 	_semaphore = other._semaphore;
+	other._name = "<moved>";
 	other._semaphore = nullptr;
+	debug("move-assign");
 }
 
 void FakeLock::release() {
 	if (_semaphore == nullptr)
 		return;
 	assert(_semaphore->_counter > 0);
-	debugC(kDebugSemaphores, "Lock dtor %s to %u", _semaphore->_name, _semaphore->_counter - 1);
 	_semaphore->_counter--;
+	debug("release");
 	_semaphore = nullptr;
 }
 
+void FakeLock::debug(const char *action) {
+	const char *myName = _name == nullptr ? "<null>" : _name;
+	if (_semaphore == nullptr)
+		debugC(kDebugSemaphores, "Lock %s %s nullptr", myName, action);
+	else
+		debugC(kDebugSemaphores, "Lock %s %s %s at %u", myName, action, _semaphore->_name, _semaphore->_counter);
+}
+
 Vector3d as3D(const Vector2d &v) {
 	return Vector3d(v.getX(), v.getY(), 0.0f);
 }
diff --git a/engines/alcachofa/common.h b/engines/alcachofa/common.h
index 0394653f657..dd793893f13 100644
--- a/engines/alcachofa/common.h
+++ b/engines/alcachofa/common.h
@@ -98,7 +98,7 @@ private:
 
 struct FakeLock {
 	FakeLock();
-	FakeLock(FakeSemaphore &semaphore);
+	FakeLock(const char *name, FakeSemaphore &semaphore);
 	FakeLock(const FakeLock &other);
 	FakeLock(FakeLock &&other) noexcept;
 	~FakeLock();
@@ -107,6 +107,9 @@ struct FakeLock {
 	
 	inline bool isReleased() const { return _semaphore == nullptr; }
 private:
+	void debug(const char *action);
+
+	const char *_name = "<uninitialized>";
 	FakeSemaphore *_semaphore = nullptr;
 };
 
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index 2dc95a755da..3af856cacb7 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -215,7 +215,7 @@ struct DoorTask : public Task {
 		if (_targetRoom == nullptr || _targetObject == nullptr)
 			return TaskReturn::finish(1);
 
-		_musicLock = FakeLock(g_engine->sounds().musicSemaphore());
+		_musicLock = FakeLock("door-music", g_engine->sounds().musicSemaphore());
 		if (g_engine->sounds().musicID() != _targetRoom->musicID())
 			g_engine->sounds().fadeMusic();
 		TASK_WAIT(1, fade(process(), FadeType::ToBlack, 0, 1, 500, EasingType::Out, -5));
@@ -253,9 +253,9 @@ struct DoorTask : public Task {
 		bool hasMusicLock = !_musicLock.isReleased();
 		s.syncAsByte(hasMusicLock);
 		if (s.isLoading() && hasMusicLock)
-			_musicLock = FakeLock(g_engine->sounds().musicSemaphore());
+			_musicLock = FakeLock("door-music", g_engine->sounds().musicSemaphore());
 		
-		_lock = FakeLock(_character->semaphore());
+		_lock = FakeLock("door", _character->semaphore());
 		findTarget();
 	}
 
@@ -292,7 +292,7 @@ void Player::triggerDoor(const Door *door) {
 	_heldItem = nullptr;
 
 	if (g_engine->game().shouldTriggerDoor(door)) {
-		FakeLock lock(_activeCharacter->semaphore());
+		FakeLock lock("door", _activeCharacter->semaphore());
 		g_engine->scheduler().createProcess<DoorTask>(activeCharacterKind(), door, move(lock));
 	}
 }
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 2deddae1472..861b8c61ee5 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -393,7 +393,7 @@ struct ScriptTask : public Task {
 		bool hasLock = !_lock.isReleased();
 		s.syncAsByte(hasLock);
 		if (s.isLoading() && hasLock)
-			_lock = FakeLock(g_engine->player().semaphoreFor(process().character()));
+			_lock = FakeLock("script", g_engine->player().semaphoreFor(process().character()));
 	}
 
 	virtual const char *taskName() const override;
@@ -593,7 +593,7 @@ private:
 			}
 			process().character() = MainCharacterKind::None;
 			assert(player.semaphore().isReleased());
-			_lock = { player.semaphore() };
+			_lock = FakeLock("script", player.semaphore());
 			return TaskReturn::finish(1);
 		}
 		case ScriptKernelTask::ChangeRoom:
@@ -967,7 +967,7 @@ Process *Script::createProcess(MainCharacterKind character, const String &proced
 	}
 	FakeLock lock;
 	if (!(flags & ScriptFlags::IsBackground))
-		lock = FakeLock(g_engine->player().semaphoreFor(character));
+		lock = FakeLock("script", g_engine->player().semaphoreFor(character));
 	Process *process = g_engine->scheduler().createProcess<ScriptTask>(character, procedure, offset, Common::move(lock));
 	process->name() = procedure;
 	return process;
diff --git a/engines/alcachofa/sounds.cpp b/engines/alcachofa/sounds.cpp
index f3451db1ccf..6b8fa80620f 100644
--- a/engines/alcachofa/sounds.cpp
+++ b/engines/alcachofa/sounds.cpp
@@ -366,11 +366,11 @@ DECLARE_TASK(PlaySoundTask);
 
 WaitForMusicTask::WaitForMusicTask(Process &process)
 	: Task(process)
-	, _lock(g_engine->sounds().musicSemaphore()) {}
+	, _lock("wait-for-music", g_engine->sounds().musicSemaphore()) { }
 
 WaitForMusicTask::WaitForMusicTask(Process &process, Serializer &s)
 	: Task(process)
-	, _lock(g_engine->sounds().musicSemaphore()) {
+	, _lock("wait-for-music", g_engine->sounds().musicSemaphore()) {
 	syncGame(s);
 }
 
diff --git a/engines/alcachofa/ui-objects.cpp b/engines/alcachofa/ui-objects.cpp
index dd310c81592..5ef0961504c 100644
--- a/engines/alcachofa/ui-objects.cpp
+++ b/engines/alcachofa/ui-objects.cpp
@@ -92,7 +92,7 @@ void MenuButton::onHoverUpdate() {}
 
 void MenuButton::onClick() {
 	if (_isInteractable && _interactionLock.isReleased()) {
-		_interactionLock = g_engine->menu().interactionSemaphore();
+		_interactionLock = FakeLock("button", g_engine->menu().interactionSemaphore());
 		_isClicked = true;
 		_triggerNextFrame = false;
 		_graphicClicked.start(false);


Commit: 3143836091d373767270a427f768db926332b0bd
    https://github.com/scummvm/scummvm/commit/3143836091d373767270a427f768db926332b0bd
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:57+02:00

Commit Message:
ALCACHOFA: Use MessageDialog for multiplayer warning

Changed paths:
    engines/alcachofa/menu.cpp


diff --git a/engines/alcachofa/menu.cpp b/engines/alcachofa/menu.cpp
index 10f0ab1a8e9..ed54a1c933a 100644
--- a/engines/alcachofa/menu.cpp
+++ b/engines/alcachofa/menu.cpp
@@ -19,6 +19,8 @@
  *
  */
 
+#include "gui/message.h"
+
 #include "alcachofa.h"
 #include "metaengine.h"
 #include "menu.h"
@@ -107,9 +109,10 @@ void Menu::triggerMainMenuAction(MainMenuAction action) {
 		ev.customType = (CustomEventType)EventAction::LoadFromMenu;
 		g_system->getEventManager()->pushEvent(ev);
 	}break;
-	case MainMenuAction::InternetMenu:
-		g_system->messageBox(LogMessageType::kWarning, "Multiplayer is not implemented in this ScummVM version.");
-		break;
+	case MainMenuAction::InternetMenu: {
+		GUI::MessageDialog dialog("Multiplayer is not implemented in this ScummVM version.");
+		dialog.runModal();
+	}break;
 	case MainMenuAction::OptionsMenu:
 		g_engine->menu().openOptionsMenu();
 		break;


Commit: a8d936fcd20e16f55323efcff03a491357f9db6f
    https://github.com/scummvm/scummvm/commit/a8d936fcd20e16f55323efcff03a491357f9db6f
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:57+02:00

Commit Message:
ALCACHOFA: Add saving with in-game menu

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


diff --git a/engines/alcachofa/menu.cpp b/engines/alcachofa/menu.cpp
index ed54a1c933a..a0afe911cca 100644
--- a/engines/alcachofa/menu.cpp
+++ b/engines/alcachofa/menu.cpp
@@ -68,6 +68,12 @@ void Menu::updateOpeningMenu() {
 		g_system->getWidth() / 2.0f, g_system->getHeight() / 2.0f, 0.0f));
 }
 
+static int parseSavestateSlot(const String &filename) {
+	if (filename.size() < 5) // minimal name would be "t.###"
+		return 1;
+	return atoi(filename.c_str() + filename.size() - 3);
+}
+
 void Menu::updateSelectedSavefile() {
 	auto getButton = [] (const char *name) {
 		MenuButton *button = dynamic_cast<MenuButton *>(g_engine->player().currentRoom()->getObjectByName(name));
@@ -75,11 +81,24 @@ void Menu::updateSelectedSavefile() {
 		return button;
 	};
 
-	getButton("CARGAR")->isInteractable() = _selectedSavefileI < _savefiles.size();
+	bool isOldSavefile = _selectedSavefileI < _savefiles.size();
+	getButton("CARGAR")->isInteractable() = isOldSavefile;
 	getButton("ANTERIOR")->toggle(_selectedSavefileI > 0);
-	getButton("SIGUIENTE")->toggle(_selectedSavefileI < _savefiles.size());
+	getButton("SIGUIENTE")->toggle(isOldSavefile);
+
+	if (isOldSavefile) {
+		ExtendedSavegameHeader header;
+		auto savefile = ScopedPtr<InSaveFile>(
+			_saveFileMgr->openForLoading(_savefiles[_selectedSavefileI]));
+		if (savefile != nullptr &&
+			g_engine->getMetaEngine()->readSavegameHeader(savefile.get(), &header, false))
+			_selectedSavefileDescription = header.description;
+		else // Fallback to generated description
+			_selectedSavefileDescription = String::format("Savestate %d",
+				parseSavestateSlot(_savefiles[_selectedSavefileI]));
+	}
 
-	// TODO: Update thumbnail animation
+	// TODO: Update thumbnail animation;
 }
 
 void Menu::continueGame() {
@@ -99,7 +118,7 @@ void Menu::triggerMainMenuAction(MainMenuAction action) {
 		g_engine->menu().continueGame();
 		break;
 	case MainMenuAction::Save:
-		warning("STUB: MainMenuAction Save");
+		triggerSave();
 		break;
 	case MainMenuAction::Load: {
 		// we are in some update loop, let's load next frame upon event handling
@@ -150,6 +169,34 @@ void Menu::triggerLoad() {
 	delete savefile;
 }
 
+void Menu::triggerSave() {
+	String fileName, desc;
+	if (_selectedSavefileI < _savefiles.size()) {
+		fileName = _savefiles[_selectedSavefileI]; // overwrite a previous save
+		desc = _selectedSavefileDescription;
+	}
+	else {
+		// 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);
+		desc = String::format("Savestate %d", nextSlot);
+		_savefiles.push_back(fileName);
+	}
+
+	auto savefile = ScopedPtr<OutSaveFile>(_saveFileMgr->openForSaving(fileName));
+	if (savefile == nullptr) {
+		GUI::MessageDialog dialog("Could not open savefile");
+		dialog.runModal();
+		return;
+	}
+	if (g_engine->saveGameStream(savefile.get()).getCode() == kNoError)
+		g_engine->getMetaEngine()->appendExtendedSave(savefile.get(), g_engine->getTotalPlayTime(), desc, false);
+
+	updateSelectedSavefile();
+}
+
 void Menu::openOptionsMenu() {
 	setOptionsState();
 	g_engine->player().changeRoom("MENUOPCIONES", true);
diff --git a/engines/alcachofa/menu.h b/engines/alcachofa/menu.h
index 7665e2a6a56..8b24bd7f25b 100644
--- a/engines/alcachofa/menu.h
+++ b/engines/alcachofa/menu.h
@@ -76,6 +76,7 @@ public:
 	void triggerOptionsValue(OptionsMenuValue valueId, float value);
 
 private:
+	void triggerSave();
 	void updateSelectedSavefile();
 	void continueGame();
 	void continueMainMenu();
@@ -89,6 +90,7 @@ private:
 		_selectedSavefileI = 0;
 	Room *_previousRoom = nullptr;
 	FakeSemaphore _interactionSemaphore; // to prevent ScummVM loading during button clicks
+	Common::String _selectedSavefileDescription = "<unset>";
 	Common::Array<Common::String> _savefiles;
 	Common::SaveFileManager *_saveFileMgr;
 };


Commit: c58bd40362527f1213b59c3321d03922dc8213ea
    https://github.com/scummvm/scummvm/commit/c58bd40362527f1213b59c3321d03922dc8213ea
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:57+02:00

Commit Message:
ALCACHOFA: Remove getRandomNumber from template

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


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index a434994a028..769aa77be8a 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -53,7 +53,6 @@ AlcachofaEngine *g_engine;
 AlcachofaEngine::AlcachofaEngine(OSystem *syst, const ADGameDescription *gameDesc)
 	: Engine(syst)
 	, _gameDescription(gameDesc)
-	, _randomSource("Alcachofa")
 	, _eventLoopSemaphore("engine") {
 	g_engine = this;
 }
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index 975de0b50ce..5fc69bdc62a 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -83,9 +83,6 @@ private:
 };
 
 class AlcachofaEngine : public Engine {
-private:
-	const ADGameDescription *_gameDescription;
-	Common::RandomSource _randomSource;
 protected:
 	// Engine APIs
 	Common::Error run() override;
@@ -117,19 +114,8 @@ public:
 	void setDebugMode(DebugMode debugMode, int32 param);
 
 	uint32 getFeatures() const;
-
-	/**
-	 * Returns the game Id
-	 */
 	Common::String getGameId() const;
 
-	/**
-	 * Gets a random number
-	 */
-	uint32 getRandomNumber(uint maxNum) {
-		return _randomSource.getRandomNumber(maxNum);
-	}
-
 	bool hasFeature(EngineFeature f) const override {
 		return
 			(f == kSupportsLoadingDuringRuntime) ||
@@ -156,6 +142,7 @@ public:
 private:
 	bool tryLoadFromLauncher();
 
+	const ADGameDescription *_gameDescription;
 	Console *_console = new Console();
 	Common::ScopedPtr<IDebugHandler> _debugHandler;
 	Common::ScopedPtr<IRenderer> _renderer;


Commit: 6458f3e8710094aa23e8d9e4f9f56715a5b8aabc
    https://github.com/scummvm/scummvm/commit/6458f3e8710094aa23e8d9e4f9f56715a5b8aabc
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:57+02:00

Commit Message:
ALCACHOFA: Replace std::move with Common::move

Changed paths:
    engines/alcachofa/sounds.cpp


diff --git a/engines/alcachofa/sounds.cpp b/engines/alcachofa/sounds.cpp
index 6b8fa80620f..3c141ca2fca 100644
--- a/engines/alcachofa/sounds.cpp
+++ b/engines/alcachofa/sounds.cpp
@@ -183,8 +183,8 @@ SoundHandle Sounds::playSoundInternal(const char *fileName, byte volume, Mixer::
 	_mixer->playStream(type, &playback._handle, stream, -1, volume);
 	playback._type = type;
 	playback._inputRate = stream->getRate();
-	playback._samples = std::move(samples);
-	_playbacks.push_back(std::move(playback));
+	playback._samples = Common::move(samples);
+	_playbacks.push_back(Common::move(playback));
 	return playback._handle;
 }
 


Commit: a73b70addef04ee2071f803fefd343dec326e07f
    https://github.com/scummvm/scummvm/commit/a73b70addef04ee2071f803fefd343dec326e07f
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:58+02:00

Commit Message:
ALCACHOFA: Add showGraphics debug flag

Changed paths:
    engines/alcachofa/console.cpp
    engines/alcachofa/console.h
    engines/alcachofa/general-objects.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/graphics.h
    engines/alcachofa/objects.h


diff --git a/engines/alcachofa/console.cpp b/engines/alcachofa/console.cpp
index dbb268fa43f..c0489bf8ab7 100644
--- a/engines/alcachofa/console.cpp
+++ b/engines/alcachofa/console.cpp
@@ -28,6 +28,7 @@ using namespace Common;
 namespace Alcachofa {
 
 Console::Console() : GUI::Debugger() {
+	registerVar("showGraphics", &_showGraphics);
 	registerVar("showInteractables", &_showInteractables);
 	registerVar("showCharacters", &_showCharacters);
 	registerVar("showFloorShape", &_showFloor);
@@ -53,6 +54,7 @@ Console::~Console() {
 bool Console::isAnyDebugDrawingOn() const {
 	return
 		g_engine->isDebugModeActive() ||
+		_showGraphics ||
 		_showInteractables ||
 		_showCharacters ||
 		_showFloor ||
diff --git a/engines/alcachofa/console.h b/engines/alcachofa/console.h
index b4d8af84a73..3d6e5e0b6ac 100644
--- a/engines/alcachofa/console.h
+++ b/engines/alcachofa/console.h
@@ -41,6 +41,7 @@ public:
 	Console();
 	~Console() override;
 
+	inline bool showGraphics() const { return _showGraphics; }
 	inline bool showInteractables() const { return _showInteractables; }
 	inline bool showCharacters() const { return _showCharacters; }
 	inline bool showFloor() const { return _showFloor; }
@@ -60,6 +61,7 @@ private:
 	bool cmdTeleport(int argc, const char **args);
 	bool cmdToggleRoomFloor(int argc, const char **args);
 
+	bool _showGraphics = false;
 	bool _showInteractables = false;
 	bool _showCharacters = false;
 	bool _showFloor = false;
diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index ca99e56bb3a..4b6674e8c1a 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -28,6 +28,7 @@
 #include "common/system.h"
 
 using namespace Common;
+using namespace Math;
 
 namespace Alcachofa {
 
@@ -112,6 +113,34 @@ void GraphicObject::draw() {
 	g_engine->drawQueue().add<AnimationDrawRequest>(_graphic, is3D, blendMode);
 }
 
+void GraphicObject::drawDebug() {
+	auto *renderer = dynamic_cast<IDebugRenderer *>(&g_engine->renderer());
+	if (!isEnabled() || !_graphic.hasAnimation() || !g_engine->console().showGraphics() || renderer == nullptr)
+		return;
+
+	const bool is3D = room() != &g_engine->world().inventory();
+	Vector2d topLeft(as2D(_graphic.topLeft()));
+	float scale = _graphic.scale() * _graphic.depthScale() * kInvBaseScale;
+	Vector2d size;
+	if (is3D) {
+		Vector3d topLeftTmp = as3D(topLeft);
+		topLeftTmp.z() = _graphic.scale();
+		_graphic.animation().outputRect3D(_graphic.frameI(), scale, topLeftTmp, size);
+		topLeft = as2D(topLeftTmp);
+	}
+	else
+		_graphic.animation().outputRect2D(_graphic.frameI(), scale, topLeft, size);
+
+	Vector2d points[] = {
+		topLeft,
+		topLeft + Vector2d(size.getX(), 0.0f),
+		topLeft + Vector2d(size.getX(), size.getY()),
+		topLeft + Vector2d(0.0f, size.getY()),
+		topLeft
+	};
+	renderer->debugPolyline({ points, 5 }, kDebugGreen);
+}
+
 void GraphicObject::loadResources() {
 	_graphic.loadResources();
 }
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 86332477c09..46c9fc04845 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -334,15 +334,20 @@ void Animation::prerenderFrame(int32 frameI) {
 	_renderedPremultiplyAlpha = _premultiplyAlpha;
 }
 
+void Animation::outputRect2D(int32 frameI, float scale, Vector2d &topLeft, Vector2d &size) const {
+	auto bounds = frameBounds(frameI);
+	topLeft += as2D(totalFrameOffset(frameI)) * scale;
+	size = Vector2d(bounds.width(), bounds.height()) * scale;
+}
+
 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);
 
-	Vector2d size(bounds.width(), bounds.height());
-	topLeft += as2D(totalFrameOffset(frameI)) * scale;
-	size *= scale;
+	Vector2d size;
+	outputRect2D(frameI, scale, topLeft, size);
 
 	auto &renderer = g_engine->renderer();
 	renderer.setTexture(_renderedTexture.get());
@@ -350,17 +355,22 @@ void Animation::draw2D(int32 frameI, Vector2d topLeft, float scale, BlendMode bl
 	renderer.quad(topLeft, size, color, Angle(), texMin, texMax);
 }
 
+void Animation::outputRect3D(int32 frameI, float scale, Vector3d &topLeft, Vector2d &size) const {
+	auto bounds = frameBounds(frameI);
+	topLeft += as3D(totalFrameOffset(frameI)) * scale;
+	topLeft = g_engine->camera().transform3Dto2D(topLeft);
+	size = Vector2d(bounds.width(), bounds.height()) * scale * topLeft.z();
+}
+
 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);
 
-	topLeft += as3D(totalFrameOffset(frameI)) * scale;
-	topLeft = g_engine->camera().transform3Dto2D(topLeft);
+	Vector2d size;
+	outputRect3D(frameI, scale, topLeft, size);
 	const auto rotation = -g_engine->camera().rotation();
-	Vector2d size(bounds.width(), bounds.height());
-	size *= scale * topLeft.z();
 
 	auto &renderer = g_engine->renderer();
 	renderer.setTexture(_renderedTexture.get());
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 8520938da57..89d20f7578d 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -195,22 +195,26 @@ public:
 	int32 frameAtTime(uint32 time) const;
 	int32 imageIndex(int32 frameI, int32 spriteI) const;
 	using AnimationBase::imageSize;
+	void outputRect2D(int32 frameI, float scale, Math::Vector2d &topLeft, Math::Vector2d &size) const;
+	void outputRect3D(int32 frameI, float scale, Math::Vector3d &topLeft, Math::Vector2d &size) const;
+
+	void overrideTexture(const Graphics::ManagedSurface &surface);
 
 	void draw2D(
 		int32 frameI,
-		Math::Vector2d center,
+		Math::Vector2d topLeft,
 		float scale,
 		BlendMode blendMode,
 		Color color);
 	void draw3D(
 		int32 frameI,
-		Math::Vector3d center,
+		Math::Vector3d topLeft,
 		float scale,
 		BlendMode blendMode,
 		Color color);
 	void drawEffect(
 		int32 frameI,
-		Math::Vector3d center,
+		Math::Vector3d topLeft,
 		Math::Vector2d tiling,
 		Math::Vector2d texOffset,
 		BlendMode blendMode);
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index a0c133eb000..e90353b233b 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -89,6 +89,7 @@ public:
 	~GraphicObject() override = default;
 
 	void draw() override;
+	void drawDebug() override;
 	void loadResources() override;
 	void freeResources() override;
 	void syncGame(Common::Serializer &serializer) override;


Commit: 3d3dda8d639928e9461740095e30dd2baae05030
    https://github.com/scummvm/scummvm/commit/3d3dda8d639928e9461740095e30dd2baae05030
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:58+02:00

Commit Message:
ALCACHOFA: Render-to-texture and initial savestate thumbnails

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/alcachofa.h
    engines/alcachofa/graphics-opengl.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/graphics.h
    engines/alcachofa/menu.cpp
    engines/alcachofa/menu.h
    engines/alcachofa/metaengine.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 769aa77be8a..74d89522b5d 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -28,6 +28,8 @@
 #include "engines/util.h"
 #include "graphics/paletteman.h"
 #include "graphics/framelimiter.h"
+#include "graphics/thumbnail.h"
+#include "image/png.h"
 #include "video/mpegps_decoder.h"
 
 #include "alcachofa.h"
@@ -114,7 +116,12 @@ Common::Error AlcachofaEngine::run() {
 
 		// Delay for a bit. All events loops should have a delay
 		// to prevent the system being unduly loaded
-		limiter.delayBeforeSwap();
+		if (!_renderer->hasOutput()) {
+			limiter.delayBeforeSwap();
+			g_system->updateScreen();
+		}
+		// else we just rendered to some surface and will use it in the next frame
+		// no need to update the screen or wait 
 		limiter.startFrame();
 	}
 
@@ -143,6 +150,7 @@ void AlcachofaEngine::playVideo(int32 videoId) {
 			_renderer->setTexture(texture.get());
 			_renderer->quad({}, { (float)g_system->getWidth(), (float)g_system->getHeight() });
 			_renderer->end();
+			g_system->updateScreen();
 		}
 
 		_input.nextFrame();
@@ -153,8 +161,7 @@ void AlcachofaEngine::playVideo(int32 videoId) {
 		if (_input.wasAnyMouseReleased() || _input.wasMenuKeyPressed())
 			break;
 
-		g_system->updateScreen();
-		g_system->delayMillis(decoder.getTimeToNextFrame() / 2);
+		g_system->delayMillis(decoder.getTimeToNextFrame());
 	}
 	decoder.stop();
 }
@@ -255,8 +262,19 @@ Common::String AlcachofaEngine::getSaveStatePattern() {
 	return getMetaEngine()->getSavegameFilePattern(_targetName.c_str());
 }
 
-Common::Error AlcachofaEngine::syncGame(Serializer &s) {
-	s.syncVersion((Serializer::Version)SaveVersion::Initial);
+Common::Error AlcachofaEngine::syncGame(MySerializer &s) {
+	if (!s.syncVersion((Serializer::Version)kCurrentSaveVersion))
+		return { kUnknownError, "Gamestate version is higher than expected" };
+
+	Graphics::ManagedSurface *thumbnail = nullptr;
+	if (s.isSaving()) {
+		thumbnail = new Graphics::ManagedSurface();
+		getSavegameThumbnail(*thumbnail->surfacePtr());
+	}
+	if (!syncThumbnail(s, thumbnail))
+		return { kUnknownError, "Could not read thumbnail" };
+	if (thumbnail != nullptr)
+		delete thumbnail;
 
 	uint32 millis = menu().isOpen()
 		? menu().millisBeforeMenu()
@@ -291,6 +309,65 @@ Common::Error AlcachofaEngine::syncGame(Serializer &s) {
 	return Common::kNoError;
 }
 
+static constexpr uint32 kNoThumbnailMagicValue = 0xBADBAD;
+
+bool AlcachofaEngine::syncThumbnail(MySerializer &s, Graphics::ManagedSurface *thumbnail) {
+	if (s.isLoading()) {
+		Graphics::Surface *readThumbnail = nullptr;
+		if (Graphics::loadThumbnail(s.readStream(), readThumbnail, thumbnail == nullptr) && readThumbnail != nullptr) {
+			if (thumbnail != nullptr) {
+				thumbnail->free();
+				*thumbnail->surfacePtr() = *readThumbnail;
+			}
+		}
+		else {
+			// If we do not get a thumbnail, maybe we get at least the marker that there is no thumbnail
+			uint32 magicValue = 0;
+			s.syncAsUint32LE(magicValue);
+			if (magicValue != kNoThumbnailMagicValue)
+				return false; // the savegame is not valid
+			else // this is not an error, just a pity
+				warning("No thumbnail stored in in-game savestate");
+		}
+	}
+	else {
+		if (thumbnail == nullptr ||
+			thumbnail->getPixels() == nullptr ||
+			!Graphics::saveThumbnail(s.writeStream(), *thumbnail)) {
+			// We were not able to get a thumbnail, save a value that denotes that situation
+			warning("Could not save in-game thumbnail");
+			uint32 magicValue = kNoThumbnailMagicValue;
+			s.syncAsUint32LE(magicValue);
+		}
+	}
+	return true;
+}
+
+void AlcachofaEngine::getSavegameThumbnail(Graphics::Surface &thumbnail) {
+	thumbnail.free();
+
+	auto *bigThumbnail = g_engine->menu().getBigThumbnail();
+	if (bigThumbnail != nullptr) {
+		// we still have a one from the in-game menu opening, reuse that
+		thumbnail.copyFrom(*bigThumbnail);
+		return;
+	}
+
+	// otherwise we have to rerender
+	thumbnail.create(kBigThumbnailWidth, kBigThumbnailHeight, Graphics::PixelFormat::createFormatRGBA32());
+	if (g_engine->player().currentRoom() == nullptr)
+		return; // but without a room we would render only black anyway
+
+	g_engine->drawQueue().clear();
+	g_engine->renderer().begin();
+	g_engine->renderer().setOutput(thumbnail);
+	g_engine->player().currentRoom()->draw();
+	g_engine->drawQueue().draw();
+	g_engine->renderer().end();
+
+	// we should be within the event loop. as such it is quite safe to mess with the drawQueue or renderer
+}
+
 bool AlcachofaEngine::tryLoadFromLauncher() {
 	int saveSlot = ConfMan.getInt("save_slot");
 	if (!ConfMan.hasKey("save_slot") || saveSlot < 0)
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index 5fc69bdc62a..6f828076d13 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -55,9 +55,31 @@ class Menu;
 class Game;
 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;
+
+
 enum class SaveVersion : Common::Serializer::Version {
 	Initial = 0
 };
+static constexpr SaveVersion kCurrentSaveVersion = SaveVersion::Initial;
+
+class MySerializer : public Common::Serializer {
+public:
+	using Common::Serializer::Serializer;
+
+	Common::SeekableReadStream &readStream() {
+		assert(isLoading() && _loadStream != nullptr);
+		return *_loadStream;
+	}
+
+	Common::WriteStream &writeStream() {
+		assert(isSaving() && _saveStream != nullptr);
+		return *_saveStream;
+	}
+};
 
 class Config {
 public:
@@ -129,15 +151,19 @@ public:
 	}
 
 	Common::String getSaveStatePattern();
-	Common::Error syncGame(Common::Serializer &s);
+	Common::Error syncGame(MySerializer &s);
 	Common::Error saveGameStream(Common::WriteStream *stream, bool isAutosave = false) override {
-		Common::Serializer s(nullptr, stream);
+		assert(stream != nullptr);
+		MySerializer s(nullptr, stream);
 		return syncGame(s);
 	}
 	Common::Error loadGameStream(Common::SeekableReadStream *stream) override {
-		Common::Serializer s(stream, nullptr);
+		assert(stream != nullptr);
+		MySerializer s(stream, nullptr);
 		return syncGame(s);
 	}
+	bool syncThumbnail(MySerializer &s, Graphics::ManagedSurface *thumbnail);
+	void getSavegameThumbnail(Graphics::Surface &thumbnail);
 
 private:
 	bool tryLoadFromLauncher();
diff --git a/engines/alcachofa/graphics-opengl.cpp b/engines/alcachofa/graphics-opengl.cpp
index e1e88278278..1cd4efd2ce6 100644
--- a/engines/alcachofa/graphics-opengl.cpp
+++ b/engines/alcachofa/graphics-opengl.cpp
@@ -20,6 +20,7 @@
  */
 
 #include "graphics.h"
+#include "detection.h"
 
 #include "common/system.h"
 #include "engines/util.h"
@@ -124,7 +125,8 @@ class OpenGLRenderer : public IDebugRenderer {
 public:
 	OpenGLRenderer(Point resolution)
 		: _resolution(resolution) {
-		initViewportAndMatrices();
+		setViewportToScreen();
+
 		GL_CALL(glDisable(GL_LIGHTING));
 		GL_CALL(glDisable(GL_DEPTH_TEST));
 		GL_CALL(glDisable(GL_SCISSOR_TEST));
@@ -147,6 +149,8 @@ public:
 		GL_CALL(glEnableClientState(GL_VERTEX_ARRAY));
 		GL_CALL(glDisableClientState(GL_INDEX_ARRAY));
 		GL_CALL(glDisableClientState(GL_TEXTURE_COORD_ARRAY));
+		setViewportToScreen();
+		_currentOutput = nullptr;
 		_currentLodBias = -1000.0f;
 		_currentTexture = nullptr;
 		_currentBlendMode = (BlendMode)-1;
@@ -155,7 +159,20 @@ public:
 
 	void end() override {
 		GL_CALL(glFlush());
-		g_system->updateScreen();
+
+		if (_currentOutput != nullptr) {
+			g_system->presentBuffer();
+			auto format = getOpenGLFormatOf(_currentOutput->format);
+			GL_CALL(glReadPixels(
+				0,
+				0,
+				_outputSize.x,
+				_outputSize.y,
+				format._format,
+				format._type,
+				_currentOutput->getPixels()
+			));
+		}
 	}
 
 	void setTexture(ITexture *texture) override {
@@ -253,6 +270,37 @@ public:
 		_currentLodBias = lodBias;
 	}
 
+	void setOutput(Surface &output) override {
+		assert(_isFirstDrawCommand);
+		setViewportToRect(output.w, output.h);
+		_currentOutput = &output;
+
+		// just debug warnings as it will only produce a graphical glitch while
+		// there is some chance the resolution could change from here to ::end
+		// and this is per-frame so maybe don't spam the console with the same message
+
+		if (output.w > g_system->getWidth() || output.h > g_system->getHeight())
+			debugC(0, kDebugGraphics, "Output is larger than screen, output will be cropped (%d, %d) > (%d, %d)",
+				output.w, output.h, g_system->getWidth(), g_system->getHeight());
+
+		auto format = getOpenGLFormatOf(output.format);
+		if (format._format == GL_NONE) {
+			auto formatString = output.format.toString();
+			debugC(0, kDebugGraphics, "Cannot use pixelformat of given output surface: %s", formatString.c_str());
+			_currentOutput = nullptr;
+		}
+
+		if (output.pitch != output.format.bytesPerPixel * output.w) {
+			// Maybe there would be a way with glPixelStore
+			debugC(0, kDebugGraphics, "Incompatible output surface pitch");
+			_currentOutput = nullptr;
+		}
+	}
+
+	bool hasOutput() const override {
+		return _currentOutput != nullptr;
+	}
+
 	virtual void quad(
 		Vector2d topLeft,
 		Vector2d size,
@@ -353,7 +401,18 @@ public:
 	}
 
 private:
-	void initViewportAndMatrices() {
+	void setMatrices(bool flipped) {
+		float bottom = flipped ? _resolution.y : 0.0f;
+		float top = flipped ? 0.0f : _resolution.y;
+
+		GL_CALL(glMatrixMode(GL_PROJECTION));
+		GL_CALL(glLoadIdentity());
+		GL_CALL(glOrtho(0.0f, _resolution.x, bottom, top, -1.0f, 1.0f));
+		GL_CALL(glMatrixMode(GL_MODELVIEW));
+		GL_CALL(glLoadIdentity());
+	}
+
+	void setViewportToScreen() {
 		int32 screenWidth = g_system->getWidth();
 		int32 screenHeight = g_system->getHeight();
 		Rect viewport(
@@ -364,16 +423,19 @@ private:
 			(screenHeight - viewport.height()) / 2);
 
 		GL_CALL(glViewport(viewport.left, viewport.top, viewport.width(), viewport.height()));
-		GL_CALL(glMatrixMode(GL_PROJECTION));
-		GL_CALL(glLoadIdentity());
-		GL_CALL(glOrtho(0.0f, _resolution.x, _resolution.y, 0.0f, -1.0f, 1.0f));
-		GL_CALL(glMatrixMode(GL_MODELVIEW));
-		GL_CALL(glLoadIdentity());
+		setMatrices(true);
+	}
+
+	void setViewportToRect(int16 outputWidth, int16 outputHeight) {
+		_outputSize.x = MIN(outputWidth, g_system->getWidth());
+		_outputSize.y = MIN(outputHeight, g_system->getHeight());
+		GL_CALL(glViewport(0, 0, _outputSize.x, _outputSize.y));
+		setMatrices(false);
 	}
 
 	void checkFirstDrawCommand() {
-		// We delay clearing the screen. It is much easier for the game to switch to a
-		// framebuffer before 
+		// We delay clearing the screen. It is much easier for the game
+		// to switch to a framebuffer before
 		if (!_isFirstDrawCommand)
 			return;
 		_isFirstDrawCommand = false;
@@ -381,7 +443,8 @@ private:
 		GL_CALL(glClear(GL_COLOR_BUFFER_BIT));
 	}
 
-	Point _resolution;
+	Point _resolution, _outputSize;
+	Surface *_currentOutput = nullptr;
 	OpenGLTexture *_currentTexture = nullptr;
 	BlendMode _currentBlendMode = (BlendMode)-1;
 	float _currentLodBias = 0.0f;
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 46c9fc04845..6f12ea7133e 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -243,10 +243,13 @@ void Animation::load() {
 	if (_isLoaded)
 		return;
 	AnimationBase::load();
-	const bool withMipmaps = _folder != AnimationFolder::Backgrounds;
 	Rect maxBounds = maxFrameBounds();
 	_renderedSurface.create(maxBounds.width(), maxBounds.height(), BlendBlit::getSupportedPixelFormat());
-	_renderedTexture = g_engine->renderer().createTexture(maxBounds.width(), maxBounds.height(), withMipmaps);
+	_renderedTexture = g_engine->renderer().createTexture(maxBounds.width(), maxBounds.height(), true);
+
+	// We always create mipmaps, even for the backgrounds that usually do not scale much,
+	// the exception to this is the thumbnails for the savestates.
+	// If we need to reduce graphics memory usage in the future, we can change it right here
 }
 
 void Animation::freeImages() {
@@ -311,6 +314,25 @@ int32 Animation::frameAtTime(uint32 time) const {
 	return -1;
 }
 
+void Animation::overrideTexture(const ManagedSurface &surface) {
+	// In order to really use the overridden surface we have to override all
+	// values used for calculating the output size
+	_renderedFrameI = 0;
+	_renderedPremultiplyAlpha = _premultiplyAlpha;
+	_renderedSurface.free();
+	_renderedSurface.w = surface.w;
+	_renderedSurface.h = surface.h;
+	_images[0]->free();
+	_images[0]->w = surface.w;
+	_images[0]->h = surface.h;
+
+	if (_renderedTexture->size() != Point(surface.w, surface.h)) {
+		_renderedTexture = Common::move(
+			g_engine->renderer().createTexture(surface.w, surface.h, false));
+	}
+	_renderedTexture->update(surface);
+}
+
 void Animation::prerenderFrame(int32 frameI) {
 	assert(frameI >= 0 && (uint)frameI < frameCount());
 	if (frameI == _renderedFrameI && _renderedPremultiplyAlpha == _premultiplyAlpha)
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 89d20f7578d..3c547dd9ee8 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -81,6 +81,8 @@ public:
 	virtual void setTexture(ITexture *texture) = 0;
 	virtual void setBlendMode(BlendMode blendMode) = 0;
 	virtual void setLodBias(float lodBias) = 0;
+	virtual void setOutput(Graphics::Surface &surface) = 0;
+	virtual bool hasOutput() const = 0;
 	virtual void quad(
 		Math::Vector2d topLeft,
 		Math::Vector2d size,
diff --git a/engines/alcachofa/menu.cpp b/engines/alcachofa/menu.cpp
index a0afe911cca..7606585b97a 100644
--- a/engines/alcachofa/menu.cpp
+++ b/engines/alcachofa/menu.cpp
@@ -20,6 +20,7 @@
  */
 
 #include "gui/message.h"
+#include "graphics/thumbnail.h"
 
 #include "alcachofa.h"
 #include "metaengine.h"
@@ -28,8 +29,36 @@
 #include "script.h"
 
 using namespace Common;
+using namespace Graphics;
 
 namespace Alcachofa {
+	
+static void createThumbnail(ManagedSurface &surface) {
+	surface.create(kBigThumbnailWidth, kBigThumbnailHeight, PixelFormat::createFormatRGBA32());
+}
+
+static void convertToGrayscale(ManagedSurface &surface) {
+	assert(!surface.empty());
+	assert(surface.format == PixelFormat::createFormatRGBA32());
+	uint32 rgbMask = ~(uint32(0xff) << surface.format.aShift);
+
+	for (int y = 0; y < surface.h; y++) {
+		union {
+			uint32 *pixel;
+			uint8 *components;
+		};
+		pixel = (uint32 *)surface.getBasePtr(0, y);
+		for (int x = 0; x < surface.w; x++, pixel++) {
+			*pixel &= rgbMask;
+			byte gray = (components[0] + components[1] + components[2] + components[3]) / 3;
+			*pixel =
+				(uint32(gray) << surface.format.rShift) |
+				(uint32(gray) << surface.format.gShift) |
+				(uint32(gray) << surface.format.bShift) |
+				(uint32(0xff) << surface.format.aShift);
+		}
+	}
+}
 
 Menu::Menu()
 	: _interactionSemaphore("menu")
@@ -39,13 +68,17 @@ void Menu::resetAfterLoad() {
 	_isOpen = false;
 	_openAtNextFrame = false;
 	_previousRoom = nullptr;
+	_bigThumbnail.free();
+	_selectedThumbnail.free();
 }
 
 void Menu::updateOpeningMenu() {
 	if (!_openAtNextFrame) {
-		_openAtNextFrame =
-			g_engine->input().wasMenuKeyPressed() &&
-			g_engine->player().isAllowedToOpenMenu();
+		if (g_engine->input().wasMenuKeyPressed() && g_engine->player().isAllowedToOpenMenu()) {
+			_openAtNextFrame = true;
+			createThumbnail(_bigThumbnail);
+			g_engine->renderer().setOutput(*_bigThumbnail.surfacePtr());
+		}
 		return;
 	}
 	_openAtNextFrame = false;
@@ -54,7 +87,6 @@ void Menu::updateOpeningMenu() {
 	_millisBeforeMenu = g_engine->getMillis();
 	_previousRoom = g_engine->player().currentRoom();
 	_isOpen = true;
-	// TODO: Render thumbnail
 	g_engine->player().changeRoom("MENUPRINCIPAL", true);
 	_savefiles = _saveFileMgr->listSavefiles(g_engine->getSaveStatePattern());
 	sort(_savefiles.begin(), _savefiles.end()); // the pattern ensures that the last file has the greatest slot
@@ -87,23 +119,48 @@ void Menu::updateSelectedSavefile() {
 	getButton("SIGUIENTE")->toggle(isOldSavefile);
 
 	if (isOldSavefile) {
-		ExtendedSavegameHeader header;
-		auto savefile = ScopedPtr<InSaveFile>(
-			_saveFileMgr->openForLoading(_savefiles[_selectedSavefileI]));
-		if (savefile != nullptr &&
-			g_engine->getMetaEngine()->readSavegameHeader(savefile.get(), &header, false))
-			_selectedSavefileDescription = header.description;
-		else // Fallback to generated description
+		if (!tryReadOldSavefile()) {
 			_selectedSavefileDescription = String::format("Savestate %d",
 				parseSavestateSlot(_savefiles[_selectedSavefileI]));
+			createThumbnail(_selectedThumbnail);
+		}
 	}
+	else {
+		_selectedThumbnail.copyFrom(_bigThumbnail);
+		//convertToGrayscale(_selectedThumbnail);
+	}
+
+	ObjectBase *captureObject = g_engine->player().currentRoom()->getObjectByName("Capture");
+	scumm_assert(captureObject);
+	Graphic *captureGraphic = captureObject->graphic();
+	scumm_assert(captureGraphic);
+	captureGraphic->animation().overrideTexture(_selectedThumbnail);
+}
 
-	// TODO: Update thumbnail animation;
+bool Menu::tryReadOldSavefile() {
+	auto savefile = ScopedPtr<InSaveFile>(
+		_saveFileMgr->openForLoading(_savefiles[_selectedSavefileI]));
+	if (savefile == nullptr)
+		return false;
+
+	ExtendedSavegameHeader header;
+	if (!g_engine->getMetaEngine()->readSavegameHeader(savefile.get(), &header, false))
+		return false;
+	_selectedSavefileDescription = header.description;
+
+	MySerializer serializer(savefile.get(), nullptr);
+	if (!serializer.syncVersion((Serializer::Version)kCurrentSaveVersion) ||
+		!g_engine->syncThumbnail(serializer, &_selectedThumbnail))
+		return false;
+
+	return true;
 }
 
 void Menu::continueGame() {
 	assert(_previousRoom != nullptr);
 	_isOpen = false;
+	_bigThumbnail.free();
+	_selectedThumbnail.free();
 	g_engine->input().nextFrame(); // presumably to clear all was* flags
 	g_engine->player().changeRoom(_previousRoom->name(), true);
 	g_engine->sounds().pauseAll(false);
@@ -165,8 +222,13 @@ void Menu::triggerMainMenuAction(MainMenuAction action) {
 
 void Menu::triggerLoad() {
 	auto *savefile = _saveFileMgr->openForLoading(_savefiles[_selectedSavefileI]);
-	g_engine->loadGameStream(savefile);
+	auto result = g_engine->loadGameStream(savefile);
 	delete savefile;
+	if (result.getCode() != kNoError) {
+		GUI::MessageDialog dialog(result.getTranslatedDesc());
+		dialog.runModal();
+		return;
+	}
 }
 
 void Menu::triggerSave() {
@@ -182,19 +244,23 @@ void Menu::triggerSave() {
 			: parseSavestateSlot(_savefiles.back()) + 1;
 		fileName = g_engine->getSaveStateName(nextSlot);
 		desc = String::format("Savestate %d", nextSlot);
-		_savefiles.push_back(fileName);
 	}
 
+	Error error(kNoError);
 	auto savefile = ScopedPtr<OutSaveFile>(_saveFileMgr->openForSaving(fileName));
-	if (savefile == nullptr) {
-		GUI::MessageDialog dialog("Could not open savefile");
+	if (savefile == nullptr)
+		error = Error(kReadingFailed);
+	else
+		error = g_engine->saveGameStream(savefile.get());
+	if (error.getCode() == kNoError) {
+		g_engine->getMetaEngine()->appendExtendedSave(savefile.get(), g_engine->getTotalPlayTime(), desc, false);
+		_savefiles.push_back(fileName);
+		updateSelectedSavefile();
+	}
+	else {
+		GUI::MessageDialog dialog(error.getTranslatedDesc());
 		dialog.runModal();
-		return;
 	}
-	if (g_engine->saveGameStream(savefile.get()).getCode() == kNoError)
-		g_engine->getMetaEngine()->appendExtendedSave(savefile.get(), g_engine->getTotalPlayTime(), desc, false);
-
-	updateSelectedSavefile();
 }
 
 void Menu::openOptionsMenu() {
@@ -296,7 +362,12 @@ void Menu::continueMainMenu() {
 		g_engine->player().isGameLoaded() ? "MENUPRINCIPAL" : "MENUPRINCIPALINICIO",
 		true
 	);
-	// TODO: Update menu state and thumbanil
+
+	updateSelectedSavefile();
+}
+
+const Graphics::Surface *Menu::getBigThumbnail() const {
+	return _bigThumbnail.empty() ? nullptr : &_bigThumbnail.rawSurface();
 }
 
 }
diff --git a/engines/alcachofa/menu.h b/engines/alcachofa/menu.h
index 8b24bd7f25b..81f962f74c2 100644
--- a/engines/alcachofa/menu.h
+++ b/engines/alcachofa/menu.h
@@ -75,9 +75,15 @@ public:
 	void triggerOptionsAction(OptionsMenuAction action);
 	void triggerOptionsValue(OptionsMenuValue valueId, float value);
 
+	// if we do still have a big thumbnail, any autosaves, ScummVM-saves, ingame-saves
+	// do not have to render themselves, they can just reuse the one we have.
+	// as such - may return nullptr
+	const Graphics::Surface *getBigThumbnail() const;
+
 private:
 	void triggerSave();
 	void updateSelectedSavefile();
+	bool tryReadOldSavefile();
 	void continueGame();
 	void continueMainMenu();
 	void setOptionsState();
@@ -92,6 +98,9 @@ private:
 	FakeSemaphore _interactionSemaphore; // to prevent ScummVM loading during button clicks
 	Common::String _selectedSavefileDescription = "<unset>";
 	Common::Array<Common::String> _savefiles;
+	Graphics::ManagedSurface
+		_bigThumbnail, // big because it is for the in-game menu, not for ScummVM
+		_selectedThumbnail;
 	Common::SaveFileManager *_saveFileMgr;
 };
 
diff --git a/engines/alcachofa/metaengine.cpp b/engines/alcachofa/metaengine.cpp
index 32dfe97f682..5c9a906abbb 100644
--- a/engines/alcachofa/metaengine.cpp
+++ b/engines/alcachofa/metaengine.cpp
@@ -106,8 +106,10 @@ KeymapArray AlcachofaMetaEngine::initKeymaps(const char *target) const {
 }
 
 void AlcachofaMetaEngine::getSavegameThumbnail(Surface &surf) {
-	// TODO: Implement
-	surf.create(160, 120, PixelFormat::createFormatRGBA32());
+	if (Alcachofa::g_engine == nullptr)
+		surf.create(160, 120, PixelFormat::createFormatRGBA32());
+	else
+		Alcachofa::g_engine->getSavegameThumbnail(surf);
 }
 
 #if PLUGIN_ENABLED_DYNAMIC(ALCACHOFA)


Commit: 4495dc5d90ec79b4618884eec92f88dc9be95a02
    https://github.com/scummvm/scummvm/commit/4495dc5d90ec79b4618884eec92f88dc9be95a02
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:58+02:00

Commit Message:
ALCACHOFA: Decrease savestate size

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/menu.cpp
    engines/alcachofa/metaengine.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 74d89522b5d..982198f34d8 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -313,17 +313,18 @@ static constexpr uint32 kNoThumbnailMagicValue = 0xBADBAD;
 
 bool AlcachofaEngine::syncThumbnail(MySerializer &s, Graphics::ManagedSurface *thumbnail) {
 	if (s.isLoading()) {
-		Graphics::Surface *readThumbnail = nullptr;
-		if (Graphics::loadThumbnail(s.readStream(), readThumbnail, thumbnail == nullptr) && readThumbnail != nullptr) {
+		auto prevPosition = s.readStream().pos();
+		Image::PNGDecoder pngDecoder;
+		if (pngDecoder.loadStream(s.readStream()) && pngDecoder.getSurface () != nullptr) {
 			if (thumbnail != nullptr) {
 				thumbnail->free();
-				*thumbnail->surfacePtr() = *readThumbnail;
+				thumbnail->copyFrom(*pngDecoder.getSurface());
 			}
 		}
 		else {
 			// If we do not get a thumbnail, maybe we get at least the marker that there is no thumbnail
-			uint32 magicValue = 0;
-			s.syncAsUint32LE(magicValue);
+			s.readStream().seek(prevPosition, SEEK_SET);
+			uint32 magicValue = s.readStream().readUint32LE();
 			if (magicValue != kNoThumbnailMagicValue)
 				return false; // the savegame is not valid
 			else // this is not an error, just a pity
@@ -333,11 +334,10 @@ bool AlcachofaEngine::syncThumbnail(MySerializer &s, Graphics::ManagedSurface *t
 	else {
 		if (thumbnail == nullptr ||
 			thumbnail->getPixels() == nullptr ||
-			!Graphics::saveThumbnail(s.writeStream(), *thumbnail)) {
+			!Image::writePNG(s.writeStream(), *thumbnail)) {
 			// We were not able to get a thumbnail, save a value that denotes that situation
 			warning("Could not save in-game thumbnail");
-			uint32 magicValue = kNoThumbnailMagicValue;
-			s.syncAsUint32LE(magicValue);
+			s.writeStream().writeUint32LE(kNoThumbnailMagicValue);
 		}
 	}
 	return true;
diff --git a/engines/alcachofa/menu.cpp b/engines/alcachofa/menu.cpp
index 7606585b97a..194d3d4b445 100644
--- a/engines/alcachofa/menu.cpp
+++ b/engines/alcachofa/menu.cpp
@@ -127,7 +127,7 @@ void Menu::updateSelectedSavefile() {
 	}
 	else {
 		_selectedThumbnail.copyFrom(_bigThumbnail);
-		//convertToGrayscale(_selectedThumbnail);
+		convertToGrayscale(_selectedThumbnail);
 	}
 
 	ObjectBase *captureObject = g_engine->player().currentRoom()->getObjectByName("Capture");
diff --git a/engines/alcachofa/metaengine.cpp b/engines/alcachofa/metaengine.cpp
index 5c9a906abbb..14e1f28a525 100644
--- a/engines/alcachofa/metaengine.cpp
+++ b/engines/alcachofa/metaengine.cpp
@@ -106,10 +106,15 @@ KeymapArray AlcachofaMetaEngine::initKeymaps(const char *target) const {
 }
 
 void AlcachofaMetaEngine::getSavegameThumbnail(Surface &surf) {
-	if (Alcachofa::g_engine == nullptr)
-		surf.create(160, 120, PixelFormat::createFormatRGBA32());
-	else
-		Alcachofa::g_engine->getSavegameThumbnail(surf);
+	if (Alcachofa::g_engine != nullptr) {
+		Surface bigThumbnail;
+		Alcachofa::g_engine->getSavegameThumbnail(bigThumbnail);
+		if (bigThumbnail.getPixels() != nullptr) {
+			surf = *bigThumbnail.scale(kSmallThumbnailWidth, kSmallThumbnailHeight, true);
+			bigThumbnail.free();
+		}
+	}
+	// if not, ScummVM will output an appropriate warning
 }
 
 #if PLUGIN_ENABLED_DYNAMIC(ALCACHOFA)


Commit: bb50a4b39422c35d04e373ec07e5233892568d7c
    https://github.com/scummvm/scummvm/commit/bb50a4b39422c35d04e373ec07e5233892568d7c
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:58+02:00

Commit Message:
ALCACHOFA: Fix fadeExit

Changed paths:
    engines/alcachofa/alcachofa.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 982198f34d8..35431a6b226 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -173,6 +173,7 @@ void AlcachofaEngine::fadeExit() {
 	Graphics::FrameLimiter limiter(g_system, kDefaultFramerate, false);
 	uint32 startTime = g_system->getMillis();
 
+	Room *room = g_engine->player().currentRoom();
 	_renderer->end(); // we were in a frame, let's exit
 	while (g_system->getMillis() - startTime < kFadeOutDuration && !shouldQuit()) {
 		_input.nextFrame();
@@ -184,12 +185,14 @@ void AlcachofaEngine::fadeExit() {
 		_renderer->begin();
 		_drawQueue->clear();
 		float t = ((float)(g_system->getMillis() - startTime)) / kFadeOutDuration;
-		// TODO: Implement cross-fade and add to fadeExit
+		if (room != nullptr)
+			room->draw();
 		_drawQueue->add<FadeDrawRequest>(FadeType::ToBlack, t, -kForegroundOrderCount);
 		_drawQueue->draw();
 		_renderer->end();
 
 		limiter.delayBeforeSwap();
+		g_system->updateScreen();
 		limiter.startFrame();
 	}
 


Commit: af7cc2c587965f74e620ab23dd6e121216b4cc7a
    https://github.com/scummvm/scummvm/commit/af7cc2c587965f74e620ab23dd6e121216b4cc7a
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:58+02:00

Commit Message:
ALCACHOFA: Add high-quality check

Changed paths:
    engines/alcachofa/common.cpp
    engines/alcachofa/general-objects.cpp


diff --git a/engines/alcachofa/common.cpp b/engines/alcachofa/common.cpp
index ecc2d061a45..2ff0e05fad0 100644
--- a/engines/alcachofa/common.cpp
+++ b/engines/alcachofa/common.cpp
@@ -150,7 +150,6 @@ String readVarString(ReadStream &stream) {
 	if (length == 0)
 		return Common::String();
 
-	// TODO: Being able to resize a string would avoid the double-allocation :/
 	char *buffer = new char[length];
 	if (buffer == nullptr)
 		error("Out of memory in readVarString");
diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index 4b6674e8c1a..7d33e19838d 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -220,7 +220,7 @@ SpecialEffectObject::SpecialEffectObject(Room *room, ReadStream &stream)
 }
 
 void SpecialEffectObject::draw() {
-	if (!isEnabled()) // TODO: Add high quality check
+	if (!isEnabled() || !g_engine->config().highQuality())
 		return;
 	const auto texOffset = g_engine->getMillis() * 0.001f * _texShift;
 	const BlendMode blendMode = _type == GraphicObjectType::Effect


Commit: 27781598301a09132a6d21ade472b029695ce152
    https://github.com/scummvm/scummvm/commit/27781598301a09132a6d21ade472b029695ce152
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:58+02:00

Commit Message:
ALCACHOFA: Add key inputs for opening/closing inventory

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


diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index 0d842c56986..bec45d35f52 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -31,7 +31,7 @@ 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 I should add key-based controls for it
+// 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() {
@@ -82,6 +82,10 @@ bool GlobalUI::updateOpeningInventory() {
 	if (g_engine->menu().isOpen() || !g_engine->player().isGameLoaded())
 		return false;
 
+	const bool userWantsToOpenInventory =
+		openInventoryTriggerBounds().contains(g_engine->input().mousePos2D()) ||
+		g_engine->input().wasInventoryKeyPressed();
+
 	if (_isOpeningInventory) {
 		uint32 deltaTime = g_engine->getMillis() - _timeForInventory;
 		if (deltaTime >= 1000) {
@@ -94,7 +98,7 @@ bool GlobalUI::updateOpeningInventory() {
 		}
 		return true;
 	}
-	else if (openInventoryTriggerBounds().contains(g_engine->input().mousePos2D())) {
+	else if (userWantsToOpenInventory) {
 		_isClosingInventory = false;
 		_isOpeningInventory = true;
 		_timeForInventory = g_engine->getMillis();
diff --git a/engines/alcachofa/input.cpp b/engines/alcachofa/input.cpp
index bfa2391c360..41e10b91176 100644
--- a/engines/alcachofa/input.cpp
+++ b/engines/alcachofa/input.cpp
@@ -36,6 +36,7 @@ void Input::nextFrame() {
 	_wasMouseLeftReleased = false;
 	_wasMouseRightReleased = false;
 	_wasMenuKeyPressed = false;
+	_wasInventoryKeyPressed = false;
 	updateMousePos3D(); // camera transformation might have changed
 }
 
@@ -74,6 +75,9 @@ bool Input::handleEvent(const Common::Event &event) {
 		case EventAction::InputMenu:
 			_wasMenuKeyPressed = true;
 			return true;
+		case EventAction::InputInventory:
+			_wasInventoryKeyPressed = true;
+			return true;
 		}
 	}
 	default:
diff --git a/engines/alcachofa/input.h b/engines/alcachofa/input.h
index 770a0c213bd..798f2e0369b 100644
--- a/engines/alcachofa/input.h
+++ b/engines/alcachofa/input.h
@@ -39,6 +39,7 @@ public:
 	inline bool isMouseRightDown() const { return _isMouseRightDown; }
 	inline bool isAnyMouseDown() const { return _isMouseLeftDown || _isMouseRightDown; }
 	inline bool wasMenuKeyPressed() const { return _wasMenuKeyPressed; }
+	inline bool wasInventoryKeyPressed() const { return _wasInventoryKeyPressed; }
 	inline Common::Point mousePos2D() const { return _mousePos2D; }
 	inline Common::Point mousePos3D() const { return _mousePos3D; }
 	const Input &debugInput() const { scumm_assert(_debugInput != nullptr); return *_debugInput; }
@@ -57,7 +58,8 @@ private:
 		_wasMouseRightReleased = false,
 		_isMouseLeftDown = false,
 		_isMouseRightDown = false,
-		_wasMenuKeyPressed = false;
+		_wasMenuKeyPressed = false,
+		_wasInventoryKeyPressed = false;
 	Common::Point
 		_mousePos2D,
 		_mousePos3D;
diff --git a/engines/alcachofa/metaengine.cpp b/engines/alcachofa/metaengine.cpp
index 14e1f28a525..f76c34d7143 100644
--- a/engines/alcachofa/metaengine.cpp
+++ b/engines/alcachofa/metaengine.cpp
@@ -38,7 +38,7 @@ static const ADExtraGuiOptionsMap optionsList[] = {
 		GAMEOPTION_HIGH_QUALITY,
 		{
 			_s("High Quality"),
-			_s("TODO: Explain what this does"),
+			_s("Toggles some optional graphical effects"),
 			_s("high_quality"),
 			true,
 			0,
@@ -49,7 +49,7 @@ static const ADExtraGuiOptionsMap optionsList[] = {
 		GAMEOPTION_32BITS,
 		{
 			_s("32 Bits"),
-			_s("TODO: Also explain this, and implement it maybe"),
+			_s("Uses 32bit textures instead of 16bit ones (currently not implemented)"),
 			_s("32_bits"),
 			true,
 			0,
@@ -102,6 +102,12 @@ KeymapArray AlcachofaMetaEngine::initKeymaps(const char *target) const {
 	act->addDefaultInputMapping("JOY_START");
 	keymap->addAction(act);
 
+	act = new Action("INVENTORY", _("Inventory"));
+	act->setCustomEngineActionEvent((CustomEventType)EventAction::InputInventory);
+	act->addDefaultInputMapping("SPACE");
+	act->addDefaultInputMapping("JOY_B");
+	keymap->addAction(act);
+
 	return Keymap::arrayOf(keymap);
 }
 
diff --git a/engines/alcachofa/metaengine.h b/engines/alcachofa/metaengine.h
index 5e40ccf00df..b79dd84a250 100644
--- a/engines/alcachofa/metaengine.h
+++ b/engines/alcachofa/metaengine.h
@@ -28,7 +28,8 @@ namespace Alcachofa {
 
 enum class EventAction {
 	LoadFromMenu,
-	InputMenu
+	InputMenu,
+	InputInventory
 };
 
 }
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index e67f8575e86..ac25469d4aa 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -402,8 +402,12 @@ bool Inventory::updateInput() {
 			-1, true, kWhite, -kForegroundOrderCount + 1);
 	}
 
+	const bool userWantsToCloseInventory =
+		closeInventoryTriggerBounds().contains(input.mousePos2D()) ||
+		input.wasMenuKeyPressed() ||
+		input.wasInventoryKeyPressed();
 	if (!player.activeCharacter()->isBusy() &&
-		closeInventoryTriggerBounds().contains(input.mousePos2D()))
+		userWantsToCloseInventory)
 		close();
 
 	if (!player.activeCharacter()->isBusy() &&


Commit: f4bbb6551aa47f4d7acbc2b0203d1db045752b3f
    https://github.com/scummvm/scummvm/commit/f4bbb6551aa47f4d7acbc2b0203d1db045752b3f
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:58+02:00

Commit Message:
ALCACHOFA: Mark typeName methods as override

Changed paths:
    engines/alcachofa/objects.h


diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index e90353b233b..06bbea56b8b 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -69,7 +69,7 @@ public:
 
 	inline Common::Point &position() { return _pos; }
 	inline Common::Point position() const { return _pos; }
-	virtual const char *typeName() const;
+	const char *typeName() const override;
 
 private:
 	Common::Point _pos;
@@ -94,7 +94,7 @@ public:
 	void freeResources() override;
 	void syncGame(Common::Serializer &serializer) override;
 	Graphic *graphic() override;
-	virtual const char *typeName() const;
+	const char *typeName() const override;
 
 	Task *animate(Process &process);
 
@@ -112,7 +112,7 @@ public:
 	SpecialEffectObject(Room *room, Common::ReadStream &stream);
 
 	void draw() override;
-	virtual const char *typeName() const;
+	const char *typeName() const override;
 
 private:
 	static constexpr const float kShiftSpeed = 1 / 256.0f;
@@ -137,7 +137,7 @@ public:
 	virtual void onHoverEnd();
 	virtual void onHoverUpdate();
 	virtual void onClick();
-	virtual const char *typeName() const;
+	const char *typeName() const override;
 	void markSelected();
 
 protected:
@@ -155,7 +155,7 @@ private:
 class PhysicalObject : public ShapeObject {
 public:
 	PhysicalObject(Room *room, Common::ReadStream &stream);
-	virtual const char *typeName() const;
+	const char *typeName() const override;
 };
 
 class MenuButton : public PhysicalObject {
@@ -174,7 +174,7 @@ public:
 	void onHoverUpdate() override;
 	void onClick() override;
 	virtual void trigger();
-	virtual const char *typeName() const;
+	const char *typeName() const override;
 
 private:
 	bool
@@ -198,7 +198,7 @@ public:
 	static constexpr const char *kClassName = "CBotonMenuInternet";
 	InternetMenuButton(Room *room, Common::ReadStream &stream);
 
-	virtual const char *typeName() const;
+	const char *typeName() const override;
 };
 
 class OptionsMenuButton final : public MenuButton {
@@ -208,7 +208,7 @@ public:
 
 	void update() override;
 	void trigger() override;
-	virtual const char *typeName() const;
+	const char *typeName() const override;
 };
 
 class MainMenuButton final : public MenuButton {
@@ -218,7 +218,7 @@ public:
 
 	void update() override;
 	void trigger() override;
-	virtual const char *typeName() const;
+	const char *typeName() const override;
 };
 
 class PushButton final : public PhysicalObject {
@@ -226,7 +226,7 @@ public:
 	static constexpr const char *kClassName = "CPushButton";
 	PushButton(Room *room, Common::ReadStream &stream);
 
-	virtual const char *typeName() const;
+	const char *typeName() const override;
 
 private:
 	bool _alwaysVisible;
@@ -239,7 +239,7 @@ public:
 	static constexpr const char *kClassName = "CEditBox";
 	EditBox(Room *room, Common::ReadStream &stream);
 
-	virtual const char *typeName() const;
+	const char *typeName() const override;
 
 private:
 	int32 i1;
@@ -266,7 +266,7 @@ public:
 	void onHoverUpdate() override;
 	void onClick() override;
 	virtual void trigger();
-	virtual const char *typeName() const;
+	const char *typeName() const override;
 
 private:
 	bool
@@ -293,7 +293,7 @@ public:
 	void update() override;
 	void loadResources() override;
 	void freeResources() override;
-	virtual const char *typeName() const;
+	const char *typeName() const override;
 
 private:
 	bool isMouseOver() const;
@@ -312,7 +312,7 @@ public:
 	static constexpr const char *kClassName = "CCheckBoxAutoAjustarRuido";
 	CheckBoxAutoAdjustNoise(Room *room, Common::ReadStream &stream);
 
-	virtual const char *typeName() const;
+	const char *typeName() const override;
 };
 
 class IRCWindow final : public ObjectBase {
@@ -320,7 +320,7 @@ public:
 	static constexpr const char *kClassName = "CVentanaIRC";
 	IRCWindow(Room *room, Common::ReadStream &stream);
 
-	virtual const char *typeName() const;
+	const char *typeName() const override;
 
 private:
 	Common::Point _p1, _p2;
@@ -332,7 +332,7 @@ public:
 	MessageBox(Room *room, Common::ReadStream &stream);
 	~MessageBox() override = default;
 
-	virtual const char *typeName() const;
+	const char *typeName() const override;
 
 private:
 	Graphic
@@ -348,7 +348,7 @@ public:
 	static constexpr const char *kClassName = "CVuMeter";
 	VoiceMeter(Room *room, Common::ReadStream &stream);
 
-	virtual const char *typeName() const;
+	const char *typeName() const override;
 };
 
 class Item : public GraphicObject {
@@ -358,7 +358,7 @@ public:
 	Item(const Item &other);
 
 	void draw() override;
-	virtual const char *typeName() const;
+	const char *typeName() const override;
 	void trigger();
 };
 
@@ -388,7 +388,7 @@ public:
 	void onClick() override;
 	void trigger(const char *action) override;
 	void toggle(bool isEnabled) override;
-	virtual const char *typeName() const;
+	const char *typeName() const override;
 
 private:
 	Common::String _relatedObject;
@@ -406,7 +406,7 @@ public:
 	virtual CursorType cursorType() const override;
 	void onClick() override;
 	void trigger(const char *action) override;
-	virtual const char *typeName() const;
+	const char *typeName() const override;
 
 private:
 	Common::String _targetRoom, _targetObject;
@@ -429,7 +429,7 @@ public:
 	Graphic *graphic() override;
 	void onClick() override;
 	void trigger(const char *action) override;
-	virtual const char *typeName() const;
+	const char *typeName() const override;
 
 	Task *sayText(Process &process, int32 dialogId);
 	void resetTalking();
@@ -480,7 +480,7 @@ public:
 		const char *activateAction = nullptr);
 	void stopWalking(Direction direction = Direction::Invalid);
 	void setPosition(Common::Point target);
-	virtual const char *typeName() const;
+	const char *typeName() const override;
 
 	Task *waitForArrival(Process &process);
 
@@ -541,7 +541,7 @@ public:
 	void update() override;
 	void draw() override;
 	void syncGame(Common::Serializer &serializer) override;
-	virtual const char *typeName() const;
+	const char *typeName() const override;
 	virtual void walkTo(
 		Common::Point target,
 		Direction endDirection = Direction::Invalid,
@@ -581,7 +581,7 @@ private:
 class Background final : public GraphicObject {
 public:
 	Background(Room *room, const Common::String &animationFileName, int16 scale);
-	virtual const char *typeName() const;
+	const char *typeName() const override;
 };
 
 class FloorColor final : public ObjectBase {
@@ -593,7 +593,7 @@ public:
 	void update() override;
 	void drawDebug() override;
 	Shape *shape() override;
-	virtual const char *typeName() const;
+	const char *typeName() const override;
 
 private:
 	FloorColorShape _shape;


Commit: 1ec9365bd2bc56da1ffa5d9b9e6b50466668ac3c
    https://github.com/scummvm/scummvm/commit/1ec9365bd2bc56da1ffa5d9b9e6b50466668ac3c
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:58+02:00

Commit Message:
ALCACHOFA: Fix compilation warnings and CI errors

Changed paths:
    engines/alcachofa/alcachofa.h
    engines/alcachofa/debug.h
    engines/alcachofa/graphics.h
    engines/alcachofa/module.mk
    engines/alcachofa/objects.h
    engines/alcachofa/scheduler.cpp
    engines/alcachofa/scheduler.h
    engines/alcachofa/sounds.h


diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index 6f828076d13..df2a6e224f1 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -130,7 +130,7 @@ public:
 
 	uint32 getMillis() const;
 	void setMillis(uint32 newMillis);
-	virtual void pauseEngineIntern(bool pause);
+	void pauseEngineIntern(bool pause) override;
 	void playVideo(int32 videoId);
 	void fadeExit();
 	void setDebugMode(DebugMode debugMode, int32 param);
diff --git a/engines/alcachofa/debug.h b/engines/alcachofa/debug.h
index 561b3683f09..abe6cc8ff02 100644
--- a/engines/alcachofa/debug.h
+++ b/engines/alcachofa/debug.h
@@ -93,7 +93,6 @@ private:
 
 	void drawIntersectionsFor(const Polygon &polygon, IDebugRenderer *renderer) {
 		auto &camera = g_engine->camera();
-		auto mousePos2D = g_engine->input().debugInput().mousePos2D();
 		auto mousePos3D = g_engine->input().debugInput().mousePos3D();
 		for (uint i = 0; i < polygon._points.size(); i++)
 		{
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 3c547dd9ee8..cd6c681d7a8 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -469,7 +469,7 @@ private:
 	static constexpr const uint kMaxDrawRequestsPerOrder = 50;
 	IRenderer *const _renderer;
 	BumpAllocator _allocator;
-	IDrawRequest *_requestsPerOrder[kOrderCount][kMaxDrawRequestsPerOrder] = { 0 };
+	IDrawRequest *_requestsPerOrder[kOrderCount][kMaxDrawRequestsPerOrder] = { { 0 } };
 	uint8 _requestsPerOrderCount[kOrderCount] = { 0 };
 	float _lodBiasPerOrder[kOrderCount] = { 0 };
 };
diff --git a/engines/alcachofa/module.mk b/engines/alcachofa/module.mk
index 574ac87bd65..71f29b4833f 100644
--- a/engines/alcachofa/module.mk
+++ b/engines/alcachofa/module.mk
@@ -6,6 +6,7 @@ MODULE_OBJS = \
 	common.o \
 	console.o \
 	game.o \
+	game-movie-adventure.o \
 	game-objects.o \
 	general-objects.o \
 	global-ui.o \
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 06bbea56b8b..b539fc24fb3 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -365,6 +365,7 @@ public:
 class ITriggerableObject {
 public:
 	ITriggerableObject(Common::ReadStream &stream);
+	virtual ~ITriggerableObject() = default;
 
 	inline Direction interactionDirection() const { return _interactionDirection; }
 	inline Common::Point interactionPoint() const { return _interactionPoint; }
diff --git a/engines/alcachofa/scheduler.cpp b/engines/alcachofa/scheduler.cpp
index b9feb436154..a6aa9514df9 100644
--- a/engines/alcachofa/scheduler.cpp
+++ b/engines/alcachofa/scheduler.cpp
@@ -79,6 +79,12 @@ void Task::syncObjectAsString(Serializer &s, ObjectBase *&object, bool optional)
 			objectName.c_str(), roomName.c_str(), taskName());
 }
 
+void Task::errorForUnexpectedObjectType(const ObjectBase *base) const {
+	// Implemented as separate function in order to access ObjectBase methods
+	error("Unexpected type of object %s in savestate for task %s (got a %s)",
+		base->name().c_str(), taskName(), base->typeName());
+}
+
 DelayTask::DelayTask(Process &process, uint32 millis)
 	: Task(process)
 	, _endTime(millis) {
diff --git a/engines/alcachofa/scheduler.h b/engines/alcachofa/scheduler.h
index 9a8fab8d396..2366a631d25 100644
--- a/engines/alcachofa/scheduler.h
+++ b/engines/alcachofa/scheduler.h
@@ -103,12 +103,13 @@ protected:
 		syncObjectAsString(s, base, optional);
 		object = dynamic_cast<TObject*>(base);
 		if (object == nullptr && base != nullptr)
-			error("Unexpected type of object %s in savestate for task %s (got a %s)",
-				base->name().c_str(), taskName(), base->typeName());
+			errorForUnexpectedObjectType(base);
 	}
 
 	uint32 _stage = 0;
 private:
+	void errorForUnexpectedObjectType(const ObjectBase *base) const;
+
 	Process &_process;
 };
 
@@ -132,19 +133,12 @@ private:
 		return #TaskName; \
 	}
 
-#if __cplusplus >= 201703L
-#define TASK_BREAK_FALLTHROUGH [[fallthrough]];
-#else
-#define TASK_BREAK_FALLTHROUGH
-#endif
-
 #define TASK_BEGIN \
 	switch(_stage) { \
 	case 0:; \
 
 #define TASK_END \
 	TASK_RETURN(0); \
-	TASK_BREAK_FALLTHROUGH \
 	default: assert(false && "Invalid line in task"); \
 	} return TaskReturn::finish(0)
 
@@ -152,7 +146,6 @@ private:
 	do { \
 		_stage = stage; \
 		return ret; \
-		TASK_BREAK_FALLTHROUGH \
 		case stage:; \
 	} while(0)
 
@@ -161,8 +154,8 @@ private:
 
 #define TASK_RETURN(value) \
 	do { \
-		return TaskReturn::finish(value); \
 		_stage = UINT_MAX; \
+		return TaskReturn::finish(value); \
 	} while(0)
 
 using ProcessId = uint32;
diff --git a/engines/alcachofa/sounds.h b/engines/alcachofa/sounds.h
index 02380d7a5d0..08570714586 100644
--- a/engines/alcachofa/sounds.h
+++ b/engines/alcachofa/sounds.h
@@ -61,7 +61,7 @@ public:
 	inline FakeSemaphore &musicSemaphore() { return _musicSemaphore; }
 
 private:
-	struct Playback {;
+	struct Playback {
 		void fadeOut(uint32 duration);
 
 		Audio::SoundHandle _handle;


Commit: a22af48933d3e99e1a38e9ab0ab7be19367d1f64
    https://github.com/scummvm/scummvm/commit/a22af48933d3e99e1a38e9ab0ab7be19367d1f64
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:58+02:00

Commit Message:
ALCACHOFA: Fix additional CI errors

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


diff --git a/engines/alcachofa/common.cpp b/engines/alcachofa/common.cpp
index 2ff0e05fad0..ec8f0fc565e 100644
--- a/engines/alcachofa/common.cpp
+++ b/engines/alcachofa/common.cpp
@@ -49,7 +49,8 @@ void FakeSemaphore::sync(Serializer &s, FakeSemaphore &semaphore) {
 	// if we are still holding locks during loading these locks will
 	// try to decrease the counter which will fail, let's find this out already here
 	assert(s.isSaving() || semaphore.isReleased());
-	(void)(s, semaphore);
+	(void)s;
+	(void)semaphore;
 
 	// We should not actually serialize the counter, just make sure it is empty
 	// When the locks are loaded, they will increase the counter themselves
diff --git a/engines/alcachofa/console.cpp b/engines/alcachofa/console.cpp
index c0489bf8ab7..b2fa6b98f15 100644
--- a/engines/alcachofa/console.cpp
+++ b/engines/alcachofa/console.cpp
@@ -183,9 +183,9 @@ bool Console::cmdItem(int argc, const char **args) {
 	const char *itemName = args[1];
 	if (argc == 3) {
 		itemName = args[2];
-		if (strcmpi(args[1], "mortadelo") == 0 || strcmpi(args[1], "m") == 0)
+		if (scumm_stricmp(args[1], "mortadelo") == 0 || scumm_stricmp(args[1], "m") == 0)
 			active = &mortadelo;
-		else if (strcmpi(args[1], "filemon") == 0 || strcmpi(args[1], "f") == 0)
+		else if (scumm_stricmp(args[1], "filemon") == 0 || scumm_stricmp(args[1], "f") == 0)
 			active = &filemon;
 		else {
 			debugPrintf("Invalid character name \"%s\", has to be either \"mortadelo\" or \"filemon\"\n", args[1]);


Commit: 6cd7965a9d7acf4c0c36c4c9f9ed64f15c98c401
    https://github.com/scummvm/scummvm/commit/6cd7965a9d7acf4c0c36c4c9f9ed64f15c98c401
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:58+02:00

Commit Message:
ALCACHOFA: Fix even more CI warnings

Changed paths:
    engines/alcachofa/game-movie-adventure.cpp
    engines/alcachofa/graphics-opengl.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/input.cpp
    engines/alcachofa/rooms.cpp
    engines/alcachofa/script.cpp
    engines/alcachofa/shape.cpp


diff --git a/engines/alcachofa/game-movie-adventure.cpp b/engines/alcachofa/game-movie-adventure.cpp
index b8c4104a923..14b40a0954d 100644
--- a/engines/alcachofa/game-movie-adventure.cpp
+++ b/engines/alcachofa/game-movie-adventure.cpp
@@ -50,7 +50,7 @@ class GameMovieAdventure : public Game {
 		return Game::shouldCharacterTrigger(character, action);
 	}
 
-	virtual bool shouldTriggerDoor(const Door *door) {
+	bool shouldTriggerDoor(const Door *door) override {
 		// An invalid door target, the character will go to the door and then ignore it (also in original engine)
 		if (door->targetRoom() == "LABERINTO" && door->targetObject() == "a_LABERINTO_desde_LABERINTO_2")
 			return false;
diff --git a/engines/alcachofa/graphics-opengl.cpp b/engines/alcachofa/graphics-opengl.cpp
index 1cd4efd2ce6..fae918c52ea 100644
--- a/engines/alcachofa/graphics-opengl.cpp
+++ b/engines/alcachofa/graphics-opengl.cpp
@@ -85,7 +85,7 @@ public:
 			GL_CALL(glDeleteTextures(1, &_handle));
 	}
 
-	virtual void update(const Surface &surface) {
+	void update(const Surface &surface) override {
 		OpenGLFormat format = getOpenGLFormatOf(surface.format);
 		assert(surface.w == size().x && surface.h == size().y);
 		assert(format.isValid());
@@ -301,7 +301,7 @@ public:
 		return _currentOutput != nullptr;
 	}
 
-	virtual void quad(
+	void quad(
 		Vector2d topLeft,
 		Vector2d size,
 		Color color,
@@ -353,7 +353,7 @@ public:
 #endif
 	}
 
-	virtual void debugPolygon(
+	void debugPolygon(
 		Span<Vector2d> points,
 		Color color
 	) override {
@@ -379,7 +379,7 @@ public:
 			GL_CALL(glDrawArrays(GL_POINTS, 0, points.size()));
 	}
 
-	virtual void debugPolyline(
+	void debugPolyline(
 		Span<Vector2d> points,
 		Color color
 	) override {
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 6f12ea7133e..d29ff6ab6ce 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -212,7 +212,7 @@ void AnimationBase::fullBlend(const ManagedSurface &source, ManagedSurface &dest
 	assert(offsetX >= 0 && offsetX + source.w <= destination.w);
 	assert(offsetY >= 0 && offsetY + source.h <= destination.h);
 
-	const byte *sourceLine = (byte *)source.getPixels();
+	const byte *sourceLine = (const byte *)source.getPixels();
 	byte *destinationLine = (byte *)destination.getPixels() + offsetY * destination.pitch + offsetX * 4;
 	for (int y = 0; y < source.h; y++) {
 		const byte *sourcePixel = sourceLine;
@@ -624,6 +624,7 @@ AnimationDrawRequest::AnimationDrawRequest(Animation *animation, int32 frameI, V
 }
 
 void AnimationDrawRequest::draw() {
+	g_engine->renderer().setLodBias(_lodBias);
 	if (_is3D)
 		_animation->draw3D(_frameI, _topLeft, _scale * kInvBaseScale, _blendMode, _color);
 	else
diff --git a/engines/alcachofa/input.cpp b/engines/alcachofa/input.cpp
index 41e10b91176..00245b6c6a8 100644
--- a/engines/alcachofa/input.cpp
+++ b/engines/alcachofa/input.cpp
@@ -78,6 +78,8 @@ bool Input::handleEvent(const Common::Event &event) {
 		case EventAction::InputInventory:
 			_wasInventoryKeyPressed = true;
 			return true;
+		default:
+			return false;
 		}
 	}
 	default:
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index ac25469d4aa..09d044f18fa 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -113,7 +113,7 @@ Room::Room(World *world, SeekableReadStream &stream, bool hasUselessByte)
 			stream.seek(objectEnd, SEEK_SET);
 		}
 		else if (stream.pos() > objectEnd) // this is probably not recoverable
-			error("Read past the object data (%u > %dll) in room %s", objectEnd, stream.pos(), _name.c_str());
+			error("Read past the object data (%u > %lld) in room %s", objectEnd, stream.pos(), _name.c_str());
 
 		if (object != nullptr)
 			_objects.push_back(object);
@@ -388,8 +388,8 @@ bool Inventory::updateInput() {
 		player.drawCursor(0);
 
 	if (hoveredItem != nullptr && !player.activeCharacter()->isBusy()) {
-		if (input.wasMouseLeftPressed() && player.heldItem() == nullptr ||
-			input.wasMouseLeftReleased() && player.heldItem() != nullptr ||
+		if ((input.wasMouseLeftPressed() && player.heldItem() == nullptr) ||
+			(input.wasMouseLeftReleased() && player.heldItem() != nullptr) ||
 			input.wasMouseRightReleased()) {
 			hoveredItem->trigger();
 			player.pressedObject() = nullptr;
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 861b8c61ee5..22485121496 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -597,11 +597,11 @@ private:
 			return TaskReturn::finish(1);
 		}
 		case ScriptKernelTask::ChangeRoom:
-			if (strcmpi(getStringArg(0), "SALIR") == 0) {
+			if (scumm_stricmp(getStringArg(0), "SALIR") == 0) {
 				g_engine->quitGame();
 				g_engine->player().changeRoom("SALIR", true);
 			}
-			else if (strcmpi(getStringArg(0), "MENUPRINCIPALINICIO") == 0)
+			else if (scumm_stricmp(getStringArg(0), "MENUPRINCIPALINICIO") == 0)
 				warning("STUB: change room to MenuPrincipalInicio special case");
 			else {
 				auto targetRoom = g_engine->world().getRoomByName(getStringArg(0));
diff --git a/engines/alcachofa/shape.cpp b/engines/alcachofa/shape.cpp
index 71b847550a6..c64c618f8b6 100644
--- a/engines/alcachofa/shape.cpp
+++ b/engines/alcachofa/shape.cpp
@@ -30,16 +30,14 @@ static int sideOfLine(Point a, Point b, Point q) {
 	return (b.x - a.x) * (q.y - a.y) - (b.y - a.y) * (q.x - a.x);
 }
 
+static bool lineIntersects(Point a1, Point b1, Point a2, Point b2) {
+	return (sideOfLine(a1, b1, a2) > 0) != (sideOfLine(a1, b1, b2) > 0); 
+}
+
 static bool segmentsIntersect(Point a1, Point b1, Point a2, Point b2) {
 	// as there are a number of special cases to consider,
 	// this method is a direct translation of the original engine
-	const auto sideOfLine = [](Point a, Point b, const Point q) {
-		return Alcachofa::sideOfLine(a, b, q) > 0;
-	};
-	const auto lineIntersects = [&](Point a1, Point b1, Point a2, Point b2) {
-		return sideOfLine(a1, b1, a2) != sideOfLine(a1, b1, b2);
-	};
-
+	// rather than using common Math:: code
 	if (a2.x > b2.x) {
 		if (a1.x > b1.x)
 			return lineIntersects(b1, a1, b2, a2) && lineIntersects(b2, a2, b1, a1);
@@ -250,9 +248,9 @@ Color FloorColorPolygon::colorAt(Point query) const {
 Shape::Shape() {}
 
 Shape::Shape(ReadStream &stream) {
-	auto complexity = stream.readByte();
+	byte complexity = stream.readByte();
 	uint8 pointsPerPolygon;
-	if (complexity < 0 || complexity > 3)
+	if (complexity > 3)
 		error("Invalid shape complexity %d", complexity);
 	else if (complexity == 3)
 		pointsPerPolygon = 0; // read in per polygon
@@ -346,10 +344,10 @@ PathFindingShape::PathFindingShape(ReadStream &stream) {
 	_pointDepths.reserve(polygonCount * kPointsPerPolygon);
 
 	for (int i = 0; i < polygonCount; i++) {
-		for (int j = 0; j < kPointsPerPolygon; j++)
+		for (uint j = 0; j < kPointsPerPolygon; j++)
 			_points.push_back(readPoint(stream));
 		_polygonOrders.push_back(stream.readSByte());
-		for (int j = 0; j < kPointsPerPolygon; j++)
+		for (uint j = 0; j < kPointsPerPolygon; j++)
 			_pointDepths.push_back(stream.readByte());
 
 		uint pointCount = addPolygon(kPointsPerPolygon);
@@ -636,16 +634,16 @@ FloorColorShape::FloorColorShape(ReadStream &stream) {
 	_pointColors.reserve(polygonCount * kPointsPerPolygon);
 
 	for (int i = 0; i < polygonCount; i++) {
-		for (int j = 0; j < kPointsPerPolygon; j++)
+		for (uint j = 0; j < kPointsPerPolygon; j++)
 			_points.push_back(readPoint(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
 		int firstColorI = _pointColors.size();
 		_pointColors.resize(_pointColors.size() + kPointsPerPolygon);
-		for (int j = 0; j < kPointsPerPolygon; j++)
+		for (uint j = 0; j < kPointsPerPolygon; j++)
 			_pointColors[firstColorI + j].a = (uint8)MIN(255, stream.readByte() * 255 / 100);
-		for (int j = 0; j < kPointsPerPolygon; j++) {
+		for (uint j = 0; j < kPointsPerPolygon; j++) {
 			_pointColors[firstColorI + j].r = stream.readByte();
 			_pointColors[firstColorI + j].g = stream.readByte();
 			_pointColors[firstColorI + j].b = stream.readByte();


Commit: 344d8c9944868bd263f7aaeaaa059012fd530773
    https://github.com/scummvm/scummvm/commit/344d8c9944868bd263f7aaeaaa059012fd530773
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:58+02:00

Commit Message:
ALCACHOFA: Remove redundant semicolon after DECLARE_TASK

Changed paths:
    engines/alcachofa/camera.cpp
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/general-objects.cpp
    engines/alcachofa/global-ui.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/player.cpp
    engines/alcachofa/scheduler.cpp
    engines/alcachofa/script.cpp
    engines/alcachofa/sounds.cpp


diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index 1b89a38c45c..3d0ddcaaaf2 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -351,7 +351,7 @@ protected:
 
 	Vector3d _fromPos, _deltaPos;
 };
-DECLARE_TASK(CamLerpPosTask);
+DECLARE_TASK(CamLerpPosTask)
 
 struct CamLerpScaleTask final : public CamLerpTask {
 	CamLerpScaleTask(Process &process, float targetScale, int32 duration, EasingType easingType)
@@ -379,7 +379,7 @@ protected:
 
 	float _fromScale = 0, _deltaScale = 0;
 };
-DECLARE_TASK(CamLerpScaleTask);
+DECLARE_TASK(CamLerpScaleTask)
 
 struct CamLerpPosScaleTask final : public CamLerpTask {
 	CamLerpPosScaleTask(Process &process,
@@ -421,7 +421,7 @@ protected:
 	float _fromScale = 0, _deltaScale = 0;
 	EasingType _moveEasingType = {}, _scaleEasingType = {};
 };
-DECLARE_TASK(CamLerpPosScaleTask);
+DECLARE_TASK(CamLerpPosScaleTask)
 
 struct CamLerpRotationTask final : public CamLerpTask {
 	CamLerpRotationTask(Process &process, float targetRotation, int32 duration, EasingType easingType)
@@ -449,7 +449,7 @@ protected:
 
 	float _fromRotation = 0, _deltaRotation = 0;
 };
-DECLARE_TASK(CamLerpRotationTask);
+DECLARE_TASK(CamLerpRotationTask)
 
 static void syncVector(Serializer &s, Vector2d &v) {
 	float *data = v.getData();
@@ -488,7 +488,7 @@ protected:
 
 	Vector2d _amplitude, _frequency;
 };
-DECLARE_TASK(CamShakeTask);
+DECLARE_TASK(CamShakeTask)
 
 struct CamWaitToStopTask final : public Task {
 	CamWaitToStopTask(Process &process)
@@ -516,7 +516,7 @@ struct CamWaitToStopTask final : public Task {
 private:
 	Camera &_camera;
 };
-DECLARE_TASK(CamWaitToStopTask);
+DECLARE_TASK(CamWaitToStopTask)
 
 struct CamSetInactiveAttributeTask final : public Task {
 	enum Attribute {
@@ -583,7 +583,7 @@ private:
 	float _value = 0;
 	int32 _delay = 0;
 };
-DECLARE_TASK(CamSetInactiveAttributeTask);
+DECLARE_TASK(CamSetInactiveAttributeTask)
 
 Task *Camera::lerpPos(Process &process,
 					  Vector2d targetPos,
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 9a4f47128eb..25f85320477 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -357,7 +357,7 @@ private:
 	int32 _dialogId = 0;
 	SoundHandle _soundHandle = {};
 };
-DECLARE_TASK(SayTextTask);
+DECLARE_TASK(SayTextTask)
 
 Task *Character::sayText(Process &process, int32 dialogId) {
 	return new SayTextTask(process, this, dialogId);
@@ -435,7 +435,7 @@ private:
 	ObjectBase *_animateObject = nullptr;
 	Graphic *_graphic = nullptr;
 };
-DECLARE_TASK(AnimateCharacterTask);
+DECLARE_TASK(AnimateCharacterTask)
 
 Task *Character::animate(Process &process, ObjectBase *animateObject) {
 	assert(animateObject != nullptr);
@@ -492,7 +492,7 @@ private:
 	float _sourceLodBias = 0, _targetLodBias = 0;
 	uint32 _startTime = 0, _durationMs = 0;
 };
-DECLARE_TASK(LerpLodBiasTask);
+DECLARE_TASK(LerpLodBiasTask)
 
 Task *Character::lerpLodBias(Process &process, float targetLodBias, int32 durationMs) {
 	return new LerpLodBiasTask(process, this, targetLodBias, durationMs);
@@ -804,7 +804,7 @@ struct ArriveTask : public Task {
 private:
 	const WalkingCharacter *_character = nullptr;
 };
-DECLARE_TASK(ArriveTask);
+DECLARE_TASK(ArriveTask)
 
 Task *WalkingCharacter::waitForArrival(Process &process) {
 	return new ArriveTask(process, this);
@@ -1129,7 +1129,7 @@ private:
 	MainCharacter *_character = nullptr;
 	uint _clickedLineI = UINT_MAX;
 };
-DECLARE_TASK(DialogMenuTask);
+DECLARE_TASK(DialogMenuTask)
 
 void MainCharacter::addDialogLine(int32 dialogId) {
 	assert(dialogId >= 0);
diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index 7d33e19838d..799382d8019 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -202,7 +202,7 @@ private:
 	Graphic *_graphic = nullptr;
 	uint32 _duration = 0;
 };
-DECLARE_TASK(AnimateTask);
+DECLARE_TASK(AnimateTask)
 
 Task *GraphicObject::animate(Process &process) {
 	return new AnimateTask(process, this);
diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index bec45d35f52..334cf5c4d28 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -246,7 +246,7 @@ private:
 	int32 _dialogId = 0;
 	uint32 _startTime = 0, _durationMs = 0;
 };
-DECLARE_TASK(CenterBottomTextTask);
+DECLARE_TASK(CenterBottomTextTask)
 
 Task *showCenterBottomText(Process &process, int32 dialogId, uint32 durationMs) {
 	return new CenterBottomTextTask(process, dialogId, durationMs);
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index d29ff6ab6ce..d9d9d13e7bb 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -864,7 +864,7 @@ private:
 	int8 _order = 0;
 	PermanentFadeAction _permanentFadeAction = {};
 };
-DECLARE_TASK(FadeTask);
+DECLARE_TASK(FadeTask)
 
 Task *fade(Process &process, FadeType fadeType,
 	float from, float to,
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index 3af856cacb7..d09a05a6d78 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -286,7 +286,7 @@ private:
 	MainCharacter *_character = nullptr;
 	Player &_player;
 };
-DECLARE_TASK(DoorTask);
+DECLARE_TASK(DoorTask)
 
 void Player::triggerDoor(const Door *door) {
 	_heldItem = nullptr;
diff --git a/engines/alcachofa/scheduler.cpp b/engines/alcachofa/scheduler.cpp
index a6aa9514df9..3a0c194c12e 100644
--- a/engines/alcachofa/scheduler.cpp
+++ b/engines/alcachofa/scheduler.cpp
@@ -113,7 +113,7 @@ void DelayTask::syncGame(Serializer &s) {
 	s.syncAsUint32LE(_endTime);
 }
 
-DECLARE_TASK(DelayTask);
+DECLARE_TASK(DelayTask)
 
 Process::Process(ProcessId pid, MainCharacterKind characterKind)
 	: _pid(pid)
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 22485121496..79c3ec5eba3 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -174,7 +174,7 @@ private:
 	int32 _durationSec = 0;
 	int32 _result = 1;
 };
-DECLARE_TASK(ScriptTimerTask);
+DECLARE_TASK(ScriptTimerTask)
 
 enum class StackEntryType {
 	Number,
@@ -950,7 +950,7 @@ private:
 	bool _isFirstExecution = true;
 	FakeLock _lock;
 };
-DECLARE_TASK(ScriptTask);
+DECLARE_TASK(ScriptTask)
 
 Process *Script::createProcess(MainCharacterKind character, const String &behavior, const String &action, ScriptFlags flags) {
 	return createProcess(character, behavior + '/' + action, flags);
diff --git a/engines/alcachofa/sounds.cpp b/engines/alcachofa/sounds.cpp
index 3c141ca2fca..57edccce836 100644
--- a/engines/alcachofa/sounds.cpp
+++ b/engines/alcachofa/sounds.cpp
@@ -362,7 +362,7 @@ void PlaySoundTask::debugPrint() {
 	g_engine->console().debugPrintf("PlaySound %u\n", _soundHandle);
 }
 
-DECLARE_TASK(PlaySoundTask);
+DECLARE_TASK(PlaySoundTask)
 
 WaitForMusicTask::WaitForMusicTask(Process &process)
 	: Task(process)
@@ -385,6 +385,6 @@ void WaitForMusicTask::debugPrint() {
 	g_engine->console().debugPrintf("WaitForMusic\n");
 }
 
-DECLARE_TASK(WaitForMusicTask);
+DECLARE_TASK(WaitForMusicTask)
 
 }


Commit: 83c49b1a9508e711a52344ff95688fddd4c8334d
    https://github.com/scummvm/scummvm/commit/83c49b1a9508e711a52344ff95688fddd4c8334d
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:58+02:00

Commit Message:
ALCACHOFA: Fix two CI warnings

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


diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 09d044f18fa..ee4eb3fcf9c 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -113,7 +113,7 @@ Room::Room(World *world, SeekableReadStream &stream, bool hasUselessByte)
 			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, stream.pos(), _name.c_str());
+			error("Read past the object data (%u > %ld) in room %s", objectEnd, stream.pos(), _name.c_str());
 
 		if (object != nullptr)
 			_objects.push_back(object);
diff --git a/engines/alcachofa/sounds.cpp b/engines/alcachofa/sounds.cpp
index 57edccce836..f13b8ca0188 100644
--- a/engines/alcachofa/sounds.cpp
+++ b/engines/alcachofa/sounds.cpp
@@ -359,7 +359,8 @@ TaskReturn PlaySoundTask::run() {
 }
 
 void PlaySoundTask::debugPrint() {
-	g_engine->console().debugPrintf("PlaySound %u\n", _soundHandle);
+	// unfortunately SoundHandle is not castable to something we could display here safely
+	g_engine->console().debugPrintf("PlaySound\n");
 }
 
 DECLARE_TASK(PlaySoundTask)


Commit: 8a2c772bcc9fc4bf6edfaac499802666bed9eace
    https://github.com/scummvm/scummvm/commit/8a2c772bcc9fc4bf6edfaac499802666bed9eace
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:59+02:00

Commit Message:
ALCACHOFA: Remove remaining virtual keyword on overridden methods

Changed paths:
    engines/alcachofa/camera.cpp
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/general-objects.cpp
    engines/alcachofa/global-ui.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/objects.h
    engines/alcachofa/player.cpp
    engines/alcachofa/scheduler.h
    engines/alcachofa/script.cpp
    engines/alcachofa/sounds.h


diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index 3d0ddcaaaf2..d611a2a9d04 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -342,7 +342,7 @@ struct CamLerpPosTask final : public CamLerpTask {
 		syncVector(s, _deltaPos);
 	}
 
-	virtual const char *taskName() const override;
+	const char *taskName() const override;
 
 protected:
 	void update(float t) override {
@@ -370,7 +370,7 @@ struct CamLerpScaleTask final : public CamLerpTask {
 		s.syncAsFloatLE(_deltaScale);
 	}
 
-	virtual const char *taskName() const override;
+	const char *taskName() const override;
 
 protected:
 	void update(float t) override {
@@ -409,7 +409,7 @@ struct CamLerpPosScaleTask final : public CamLerpTask {
 		syncEnum(s, _scaleEasingType);
 	}
 
-	virtual const char *taskName() const override;
+	const char *taskName() const override;
 
 protected:
 	void update(float t) override {
@@ -440,7 +440,7 @@ struct CamLerpRotationTask final : public CamLerpTask {
 		s.syncAsFloatLE(_deltaRotation);
 	}
 
-	virtual const char *taskName() const override;
+	const char *taskName() const override;
 
 protected:
 	void update(float t) override {
@@ -474,7 +474,7 @@ struct CamShakeTask final : public CamLerpTask {
 		syncVector(s, _frequency);
 	}
 
-	virtual const char *taskName() const override;
+	const char *taskName() const override;
 
 protected:
 	void update(float t) override {
@@ -511,7 +511,7 @@ struct CamWaitToStopTask final : public Task {
 		g_engine->console().debugPrintf("Wait for camera to stop moving\n");
 	}
 
-	virtual const char *taskName() const override;
+	const char *taskName() const override;
 
 private:
 	Camera &_camera;
@@ -575,7 +575,7 @@ struct CamSetInactiveAttributeTask final : public Task {
 		s.syncAsSint32LE(_delay);
 	}
 
-	virtual const char *taskName() const override;
+	const char *taskName() const override;
 
 private:
 	Camera &_camera;
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 25f85320477..bbac1210204 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -350,7 +350,7 @@ struct SayTextTask final : public Task {
 		s.syncAsSint32LE(_dialogId);
 	}
 
-	virtual const char *taskName() const override;
+	const char *taskName() const override;
 
 private:
 	Character *_character = nullptr;
@@ -428,7 +428,7 @@ struct AnimateCharacterTask final : public Task {
 		scumm_assert(_graphic != nullptr);
 	}
 
-	virtual const char *taskName() const override;
+	const char *taskName() const override;
 
 private:
 	Character *_character = nullptr;
@@ -485,7 +485,7 @@ struct LerpLodBiasTask final : public Task {
 		s.syncAsUint32LE(_durationMs);
 	}
 
-	virtual const char *taskName() const override;
+	const char *taskName() const override;
 
 private:
 	Character *_character = nullptr;
@@ -800,7 +800,7 @@ struct ArriveTask : public Task {
 		syncObjectAsString(s, _character);
 	}
 
-	virtual const char *taskName() const override;
+	const char *taskName() const override;
 private:
 	const WalkingCharacter *_character = nullptr;
 };
@@ -1083,7 +1083,7 @@ struct DialogMenuTask : public Task {
 		s.syncAsUint32LE(_clickedLineI);
 	}
 
-	virtual const char *taskName() const override;
+	const char *taskName() const override;
 
 private:
 	static constexpr int kTextXOffset = 5;
diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index 799382d8019..3bcc889d587 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -195,7 +195,7 @@ struct AnimateTask : public Task {
 
 	}
 
-	virtual const char *taskName() const override;
+	const char *taskName() const override;
 
 private:
 	GraphicObject *_object = nullptr;
diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index 334cf5c4d28..f7b531f277d 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -240,7 +240,7 @@ struct CenterBottomTextTask : public Task {
 		s.syncAsUint32LE(_durationMs);
 	} 
 
-	virtual const char *taskName() const override;
+	const char *taskName() const override;
 
 private:
 	int32 _dialogId = 0;
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index d9d9d13e7bb..a275ed99848 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -850,7 +850,7 @@ struct FadeTask : public Task {
 		s.syncAsSByte(_order);
 	}
 
-	virtual const char *taskName() const override;
+	const char *taskName() const override;
 
 private:
 	void draw(float t) {
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index b539fc24fb3..65210acc974 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -404,7 +404,7 @@ public:
 	inline const Common::String &targetObject() const { return _targetObject; }
 	inline Direction characterDirection() const { return _characterDirection; }
 
-	virtual CursorType cursorType() const override;
+	CursorType cursorType() const override;
 	void onClick() override;
 	void trigger(const char *action) override;
 	const char *typeName() const override;
@@ -543,7 +543,7 @@ public:
 	void draw() override;
 	void syncGame(Common::Serializer &serializer) override;
 	const char *typeName() const override;
-	virtual void walkTo(
+	void walkTo(
 		Common::Point target,
 		Direction endDirection = Direction::Invalid,
 		ITriggerableObject *activateObject = nullptr,
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index d09a05a6d78..65f5c2bba05 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -210,7 +210,7 @@ struct DoorTask : public Task {
 		syncGame(s);
 	}
 
-	virtual TaskReturn run() {
+	TaskReturn run() override {
 		TASK_BEGIN;
 		if (_targetRoom == nullptr || _targetObject == nullptr)
 			return TaskReturn::finish(1);
@@ -240,7 +240,7 @@ struct DoorTask : public Task {
 		TASK_END;
 	}
 
-	virtual void debugPrint() {
+	void debugPrint() override {
 		g_engine->console().debugPrintf("%s\n", process().name().c_str());
 	}
 
@@ -259,7 +259,7 @@ struct DoorTask : public Task {
 		findTarget();
 	}
 
-	virtual const char *taskName() const override;
+	const char *taskName() const override;
 
 private:
 	void findTarget() {
diff --git a/engines/alcachofa/scheduler.h b/engines/alcachofa/scheduler.h
index 2366a631d25..c8131e3fefa 100644
--- a/engines/alcachofa/scheduler.h
+++ b/engines/alcachofa/scheduler.h
@@ -119,7 +119,7 @@ struct DelayTask : public Task {
 	TaskReturn run() override;
 	void debugPrint() override;
 	void syncGame(Common::Serializer &s) override;
-	virtual const char *taskName() const override;
+	const char *taskName() const override;
 
 private:
 	uint32 _endTime = 0;
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 79c3ec5eba3..4ccd9eecc2e 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -168,7 +168,7 @@ struct ScriptTimerTask : public Task {
 		s.syncAsSint32LE(_result);
 	}
 
-	virtual const char *taskName() const override;
+	const char *taskName() const override;
 
 private:
 	int32 _durationSec = 0;
@@ -367,7 +367,7 @@ struct ScriptTask : public Task {
 		}
 	}
 
-	virtual void debugPrint() {
+	void debugPrint() override {
 		g_engine->getDebugger()->debugPrintf("\"%s\" at %u\n", _name.c_str(), _pc);
 	}
 
@@ -396,7 +396,7 @@ struct ScriptTask : public Task {
 			_lock = FakeLock("script", g_engine->player().semaphoreFor(process().character()));
 	}
 
-	virtual const char *taskName() const override;
+	const char *taskName() const override;
 
 private:
 	void setCharacterVariables() {
diff --git a/engines/alcachofa/sounds.h b/engines/alcachofa/sounds.h
index 08570714586..99978cdfe45 100644
--- a/engines/alcachofa/sounds.h
+++ b/engines/alcachofa/sounds.h
@@ -88,7 +88,7 @@ struct PlaySoundTask final : public Task {
 	PlaySoundTask(Process &process, Common::Serializer &s);
 	TaskReturn run() override;
 	void debugPrint() override;
-	virtual const char *taskName() const override;
+	const char *taskName() const override;
 private:
 	SoundHandle _soundHandle;
 };
@@ -98,7 +98,7 @@ struct WaitForMusicTask final : public Task {
 	WaitForMusicTask(Process &process, Common::Serializer &s);
 	TaskReturn run() override;
 	void debugPrint() override;
-	virtual const char *taskName() const override;
+	const char *taskName() const override;
 private:
 	FakeLock _lock;
 };


Commit: 395f1d91114d96e4380b8353b6b393cc19437fb6
    https://github.com/scummvm/scummvm/commit/395f1d91114d96e4380b8353b6b393cc19437fb6
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:59+02:00

Commit Message:
ALCACHOFA: Fix MSVC Analysis warnings

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


diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index ee4eb3fcf9c..956b2a8dd9a 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -113,7 +113,7 @@ Room::Room(World *world, SeekableReadStream &stream, bool hasUselessByte)
 			stream.seek(objectEnd, SEEK_SET);
 		}
 		else if (stream.pos() > objectEnd) // this is probably not recoverable
-			error("Read past the object data (%u > %ld) in room %s", objectEnd, 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);
diff --git a/engines/alcachofa/shape.cpp b/engines/alcachofa/shape.cpp
index c64c618f8b6..494b36cd654 100644
--- a/engines/alcachofa/shape.cpp
+++ b/engines/alcachofa/shape.cpp
@@ -466,12 +466,12 @@ void PathFindingShape::initializeFloydWarshall() {
 	for (const auto &linkPolygon : _linkIndices) {
 		for (uint i = 0; i < 2 * kPointsPerPolygon; i++) {
 			LinkIndex linkFrom = linkPolygon._points[i / 2];
-			int32 linkFromI = i % 2 ? linkFrom.second : linkFrom.first;
+			int32 linkFromI = (i % 2) ? linkFrom.second : linkFrom.first;
 			if (linkFromI < 0)
 				continue;
 			for (uint j = i + 1; j < 2 * kPointsPerPolygon; j++) {
 				LinkIndex linkTo = linkPolygon._points[j / 2];
-				int32 linkToI = j % 2 ? linkTo.second : linkTo.first;
+				int32 linkToI = (j % 2) ? linkTo.second : linkTo.first;
 				if (linkToI >= 0) {
 					const int32 linkFromFullI = linkFromI * _linkPoints.size() + linkToI;
 					const int32 linkToFullI = linkToI * _linkPoints.size() + linkFromI;
@@ -571,14 +571,14 @@ void PathFindingShape::floydWarshallPath(
 	const auto &toIndices = _linkIndices[toContaining];
 	for (uint i = 0; i < 2 * kPointsPerPolygon; i++) {
 		const auto &curFromPoint = fromIndices._points[i / 2];
-		int32 curFromLink = i % 2 ? curFromPoint.second : curFromPoint.first;
+		int32 curFromLink = (i % 2) ? curFromPoint.second : curFromPoint.first;
 		if (curFromLink < 0)
 			continue;
 		uint curFromDistance = (uint)sqrtf(from.sqrDist(_linkPoints[curFromLink]) + 0.5f);
 
 		for (uint j = 0; j < 2 * kPointsPerPolygon; j++) {
 			const auto &curToPoint = toIndices._points[j / 2];
-			int32 curToLink = j % 2 ? curToPoint.second : curToPoint.first;
+			int32 curToLink = (j % 2) ? curToPoint.second : curToPoint.first;
 			if (curToLink < 0)
 				continue;
 			uint totalDistance =


Commit: 42fe7869d8dee2e475177523f30a205ba5e90c09
    https://github.com/scummvm/scummvm/commit/42fe7869d8dee2e475177523f30a205ba5e90c09
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:59+02:00

Commit Message:
ALCACHOFA: Fix configure.engine

Changed paths:
    engines/alcachofa/configure.engine
    engines/alcachofa/graphics-opengl.cpp


diff --git a/engines/alcachofa/configure.engine b/engines/alcachofa/configure.engine
index a5decb9ba59..6e58d75f3ec 100644
--- a/engines/alcachofa/configure.engine
+++ b/engines/alcachofa/configure.engine
@@ -1,3 +1,3 @@
 # This file is included from the main "configure" script
 # add_engine [name] [desc] [build-by-default] [subengines] [base games] [deps]
-add_engine alcachofa "Alcachofa" no "" "" ""
+add_engine alcachofa "Alcachofa" no "" "" "highres opengl_game_classic mpeg2 3d"
diff --git a/engines/alcachofa/graphics-opengl.cpp b/engines/alcachofa/graphics-opengl.cpp
index fae918c52ea..befaa7b35e0 100644
--- a/engines/alcachofa/graphics-opengl.cpp
+++ b/engines/alcachofa/graphics-opengl.cpp
@@ -26,12 +26,7 @@
 #include "engines/util.h"
 #include "graphics/managed_surface.h"
 #include "graphics/opengl/system_headers.h"
-
-#ifdef ALCACHOFA_DEBUG_OPENGL // clearing OpenGL errors are very slow, so we only activate the debugging wrapper if necessary
 #include "graphics/opengl/debug.h"
-#else
-#define GL_CALL(call) call
-#endif
 
 using namespace Common;
 using namespace Math;


Commit: 4a1b0a647372e565ba64d9e85af47e34f9e7c829
    https://github.com/scummvm/scummvm/commit/4a1b0a647372e565ba64d9e85af47e34f9e7c829
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:59+02:00

Commit Message:
ALCACHOFA: Fix black thumbnail after saving in-game

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


diff --git a/engines/alcachofa/menu.cpp b/engines/alcachofa/menu.cpp
index 194d3d4b445..e7988c15f80 100644
--- a/engines/alcachofa/menu.cpp
+++ b/engines/alcachofa/menu.cpp
@@ -91,7 +91,7 @@ void Menu::updateOpeningMenu() {
 	_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();
-	updateSelectedSavefile();
+	updateSelectedSavefile(false);
 
 	g_engine->player().heldItem() = nullptr;
 	g_engine->scheduler().backupContext();
@@ -106,7 +106,7 @@ static int parseSavestateSlot(const String &filename) {
 	return atoi(filename.c_str() + filename.size() - 3);
 }
 
-void Menu::updateSelectedSavefile() {
+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);
@@ -118,7 +118,11 @@ void Menu::updateSelectedSavefile() {
 	getButton("ANTERIOR")->toggle(_selectedSavefileI > 0);
 	getButton("SIGUIENTE")->toggle(isOldSavefile);
 
-	if (isOldSavefile) {
+	if (hasJustSaved) {
+		// we just saved in-game so we also still have the correct thumbnail in memory
+		_selectedThumbnail.copyFrom(_bigThumbnail);
+	}
+	else if (isOldSavefile) {
 		if (!tryReadOldSavefile()) {
 			_selectedSavefileDescription = String::format("Savestate %d",
 				parseSavestateSlot(_savefiles[_selectedSavefileI]));
@@ -126,6 +130,7 @@ void Menu::updateSelectedSavefile() {
 		}
 	}
 	else {
+		// the unsaved gamestate is shown as grayscale
 		_selectedThumbnail.copyFrom(_bigThumbnail);
 		convertToGrayscale(_selectedThumbnail);
 	}
@@ -200,13 +205,13 @@ void Menu::triggerMainMenuAction(MainMenuAction action) {
 	case MainMenuAction::NextSave:
 		if (_selectedSavefileI < _savefiles.size()) {
 			_selectedSavefileI++;
-			updateSelectedSavefile();
+			updateSelectedSavefile(false);
 		}
 		break;
 	case MainMenuAction::PrevSave:
 		if (_selectedSavefileI > 0) {
 			_selectedSavefileI--;
-			updateSelectedSavefile();
+			updateSelectedSavefile(false);
 		}
 		break;
 	case MainMenuAction::NewGame:
@@ -232,10 +237,9 @@ void Menu::triggerLoad() {
 }
 
 void Menu::triggerSave() {
-	String fileName, desc;
+	String fileName;
 	if (_selectedSavefileI < _savefiles.size()) {
 		fileName = _savefiles[_selectedSavefileI]; // overwrite a previous save
-		desc = _selectedSavefileDescription;
 	}
 	else {
 		// for a new savefile we figure out the next slot index
@@ -243,7 +247,7 @@ void Menu::triggerSave() {
 			? 1 // start at one to keep autosave alone
 			: parseSavestateSlot(_savefiles.back()) + 1;
 		fileName = g_engine->getSaveStateName(nextSlot);
-		desc = String::format("Savestate %d", nextSlot);
+		_selectedSavefileDescription = String::format("Savestate %d", nextSlot);
 	}
 
 	Error error(kNoError);
@@ -253,9 +257,9 @@ void Menu::triggerSave() {
 	else
 		error = g_engine->saveGameStream(savefile.get());
 	if (error.getCode() == kNoError) {
-		g_engine->getMetaEngine()->appendExtendedSave(savefile.get(), g_engine->getTotalPlayTime(), desc, false);
+		g_engine->getMetaEngine()->appendExtendedSave(savefile.get(), g_engine->getTotalPlayTime(), _selectedSavefileDescription, false);
 		_savefiles.push_back(fileName);
-		updateSelectedSavefile();
+		updateSelectedSavefile(true);
 	}
 	else {
 		GUI::MessageDialog dialog(error.getTranslatedDesc());
@@ -363,7 +367,7 @@ void Menu::continueMainMenu() {
 		true
 	);
 
-	updateSelectedSavefile();
+	updateSelectedSavefile(false);
 }
 
 const Graphics::Surface *Menu::getBigThumbnail() const {
diff --git a/engines/alcachofa/menu.h b/engines/alcachofa/menu.h
index 81f962f74c2..5e51f724058 100644
--- a/engines/alcachofa/menu.h
+++ b/engines/alcachofa/menu.h
@@ -82,7 +82,7 @@ public:
 
 private:
 	void triggerSave();
-	void updateSelectedSavefile();
+	void updateSelectedSavefile(bool hasJustSaved);
 	bool tryReadOldSavefile();
 	void continueGame();
 	void continueMainMenu();


Commit: 04240a15b726afc222f93223c22c3b4f413305c8
    https://github.com/scummvm/scummvm/commit/04240a15b726afc222f93223c22c3b4f413305c8
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:59+02:00

Commit Message:
ALCACHOFA: Fix filemon being able to leave POBALDO_INDIO

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


diff --git a/engines/alcachofa/game-movie-adventure.cpp b/engines/alcachofa/game-movie-adventure.cpp
index 14b40a0954d..27508edf3e5 100644
--- a/engines/alcachofa/game-movie-adventure.cpp
+++ b/engines/alcachofa/game-movie-adventure.cpp
@@ -21,6 +21,7 @@
 
 #include "alcachofa.h"
 #include "game.h"
+#include "script.h"
 
 using namespace Common;
 
@@ -57,6 +58,15 @@ class GameMovieAdventure : public Game {
 		return Game::shouldTriggerDoor(door);
 	}
 
+	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");
+	}
+
 	bool hasMortadeloVoice(const Character *character) override {
 		return Game::hasMortadeloVoice(character) ||
 			character->name().equalsIgnoreCase("MORTADELO_TREN"); // an original hard-coded special case
diff --git a/engines/alcachofa/game.cpp b/engines/alcachofa/game.cpp
index f55fc805246..8134330820e 100644
--- a/engines/alcachofa/game.cpp
+++ b/engines/alcachofa/game.cpp
@@ -79,6 +79,8 @@ bool Game::shouldTriggerDoor(const Door *door) {
 	return true;
 }
 
+void Game::onUserChangedCharacter() { }
+
 bool Game::hasMortadeloVoice(const Character *character) {
 	return character == &g_engine->world().mortadelo();
 }
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index 33737d444af..e6de8e45c88 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -61,6 +61,7 @@ public:
 	virtual bool shouldCharacterTrigger(const Character *character, const char *action);
 	virtual bool shouldTriggerDoor(const Door *door);
 	virtual bool hasMortadeloVoice(const Character *character);
+	virtual void onUserChangedCharacter();
 
 	virtual void unknownCamSetInactiveAttribute(int attribute);
 	virtual void unknownFadeType(int fadeType);
diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index f7b531f277d..855baa9b273 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -149,6 +149,7 @@ bool GlobalUI::updateChangingCharacter() {
 	g_engine->camera().setFollow(player.activeCharacter());
 	g_engine->camera().restore(0);
 	player.changeRoom(player.activeCharacter()->room()->name(), false);
+	g_engine->game().onUserChangedCharacter();
 
 	int32 characterJingle = g_engine->script().variable(
 		player.activeCharacterKind() == MainCharacterKind::Mortadelo


Commit: 330d81a940535e693748504a5bbfbad4c46f74c1
    https://github.com/scummvm/scummvm/commit/330d81a940535e693748504a5bbfbad4c46f74c1
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:59+02:00

Commit Message:
ALCACHOFA: Fix some objects being enabled after load

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


diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index 3bcc889d587..c7fdd8000dd 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -254,6 +254,7 @@ void ShapeObject::update() {
 }
 
 void ShapeObject::syncGame(Serializer &serializer) {
+	ObjectBase::syncGame(serializer);
 	serializer.syncAsSByte(_order);
 	_isNewlySelected = false;
 	_wasSelected = false;


Commit: 1adac7a86fd1d23ab17e8668370fb238eef258b2
    https://github.com/scummvm/scummvm/commit/1adac7a86fd1d23ab17e8668370fb238eef258b2
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:59+02:00

Commit Message:
ALCACHOFA: Add spanish and english steam versions

Changed paths:
    engines/alcachofa/detection_tables.h


diff --git a/engines/alcachofa/detection_tables.h b/engines/alcachofa/detection_tables.h
index 5f577de7259..5c838a1aab5 100644
--- a/engines/alcachofa/detection_tables.h
+++ b/engines/alcachofa/detection_tables.h
@@ -22,18 +22,41 @@
 namespace Alcachofa {
 
 const PlainGameDescriptor alcachofaGames[] = {
-	{ "mort_phil_adventura_de_cine", "Mort&Phil: A movie adventure" },
+	{ "mort_phil_adventura_de_cine", "Mort & Phil: A Movie Adventure" },
 	{ 0, 0 }
 };
 
 const ADGameDescription gameDescriptions[] = {
+	//
+	// A Movie Adventure
+	//
 	{
 		"mort_phil_adventura_de_cine",
-		nullptr,
+		"Clever & Smart - A Movie Adventure",
 		AD_ENTRY1s("Textos/Objetos.nkr", "a2b1deff5ca7187f2ebf7f2ab20747e9", 17606),
 		Common::DE_DEU,
 		Common::kPlatformWindows,
-		ADGF_UNSTABLE,
+		ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE,
+		GUIO2(GAMEOPTION_32BITS, GAMEOPTION_HIGH_QUALITY)
+	},
+
+	// The "english" version is just the spanish version with english subtitles...
+	{
+		"mort_phil_adventura_de_cine",
+		u8"Mortadelo y Filemón: Una Aventura de Cine - Edición Especial",
+		AD_ENTRY1s("Textos/Objetos.nkr", "ad3cb78ad7a51cfe63ee6f84768c7e66", 15895),
+		Common::EN_ANY,
+		Common::kPlatformWindows,
+		ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE,
+		GUIO2(GAMEOPTION_32BITS, GAMEOPTION_HIGH_QUALITY)
+	},
+	{
+		"mort_phil_adventura_de_cine",
+		u8"Mortadelo y Filemón: Una Aventura de Cine - Edición Especial",
+		AD_ENTRY1s("Textos/Objetos.nkr", "93331e4cc8d2f8f8a0007bfb5140dff5", 16403),
+		Common::ES_ESP,
+		Common::kPlatformWindows,
+		ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE,
 		GUIO2(GAMEOPTION_32BITS, GAMEOPTION_HIGH_QUALITY)
 	},
 


Commit: 7cfe04ad780252f09b1b76aae1c7de39078d0d3f
    https://github.com/scummvm/scummvm/commit/7cfe04ad780252f09b1b76aae1c7de39078d0d3f
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:59+02:00

Commit Message:
ALCACHOFA: Handle more dialog line formats

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


diff --git a/engines/alcachofa/game-movie-adventure.cpp b/engines/alcachofa/game-movie-adventure.cpp
index 27508edf3e5..13793546476 100644
--- a/engines/alcachofa/game-movie-adventure.cpp
+++ b/engines/alcachofa/game-movie-adventure.cpp
@@ -33,11 +33,6 @@ class GameMovieAdventure : public Game {
 			!room->name().equalsIgnoreCase("HABITACION_NEGRA");
 	}
 
-	void invalidDialogLine(uint index) override {
-		if (index != 4542)
-			Game::invalidDialogLine(index);
-	}
-
 	bool shouldCharacterTrigger(const Character *character, const char *action) override {
 		// An original hack to check that bed sheet is used on the other main character only in the correct room
 		// There *is* another script variable (es_casa_freddy) that should check this
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 956b2a8dd9a..1252024ee50 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -724,6 +724,18 @@ static void loadEncryptedFile(const char *path, Array<char> &output) {
 	output.back() = '\0'; // one for good measure and a zero-terminator
 }
 
+static char *trimLeading(char *start, char *end) {
+	while (start < end && isSpace(*start))
+		start++;
+	return start;
+}
+
+static char *skipWord(char *start, char *end) {
+	while (start < end && !isSpace(*start))
+		start++;
+	return start;
+}
+
 static char *trimTrailing(char *start, char *end) {
 	while (start < end && isSpace(end[-1]))
 		end--;
@@ -739,32 +751,52 @@ void World::loadLocalizedNames() {
 		if (keyEnd == lineStart || keyEnd == lineEnd || keyEnd + 1 == lineEnd)
 			error("Invalid localized name line separator");
 		char *valueEnd = trimTrailing(keyEnd + 1, lineEnd);
-		if (valueEnd == keyEnd + 1)
-			error("Invalid localized name value");
 
 		*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;
 	}
 }
 
 void World::loadDialogLines() {
+	/* This "encrypted" file contains lines in any of the following formats:
+	 * Name 123, "This is the dialog line"\r\n
+	 * Name 123, "This is the dialog line\r\n
+	 *     Name     123   This is the dialog line    \r\n
+	 *
+	 * - 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);
-	char *lineStart = _dialogChunk.begin(), *fileEnd = _dialogChunk.end();
+	char *lineStart = _dialogChunk.begin(), *fileEnd = _dialogChunk.end() - 1;
 	while (lineStart < fileEnd) {
 		char *lineEnd = find(lineStart, fileEnd, '\n');
-		char *firstQuote = find(lineStart, lineEnd, '\"');
-		char *secondQuote = firstQuote == lineEnd ? lineEnd : find(firstQuote + 1, lineEnd, '\"');
 
-		if (firstQuote == lineEnd || secondQuote == lineEnd) {
+		char *cursor = trimLeading(lineStart, lineEnd); // space before the name
+		cursor = skipWord(cursor, lineEnd); // the name
+		cursor = trimLeading(cursor, lineEnd); // space between dialog id
+		cursor = skipWord(cursor, lineEnd); // the dialog id
+		cursor = trimLeading(cursor, lineEnd); // space between id and line
+		char *dialogLineEnd = trimTrailing(cursor, lineEnd);
+		if (*cursor == '\"')
+			cursor++;
+		if (dialogLineEnd > cursor && dialogLineEnd[-1] == '\"')
+			dialogLineEnd--;
+
+		if (cursor >= dialogLineEnd) {
 			g_engine->game().invalidDialogLine(_dialogLines.size());
-			firstQuote = lineStart; // store an empty string
-			secondQuote = lineStart + 1;
+			cursor = lineStart; // store an empty string
+			dialogLineEnd = lineStart + 1;
 		}
 
-		*secondQuote = 0;
-		_dialogLines.push_back(firstQuote + 1);
+		*dialogLineEnd = 0;
+		_dialogLines.push_back(cursor);
 		lineStart = lineEnd + 1;
 	}
 }


Commit: 268a6538bb83d8620faec576b9294ce38f7e1b5f
    https://github.com/scummvm/scummvm/commit/268a6538bb83d8620faec576b9294ce38f7e1b5f
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:59+02:00

Commit Message:
ALCACHOFA: Fix draw order of benter bottom text

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


diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index 855baa9b273..88ff5fb292b 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -220,7 +220,7 @@ struct CenterBottomTextTask : public Task {
 		while (g_engine->getMillis() - _startTime < _durationMs) {
 			if (process().isActiveForPlayer()) {
 				g_engine->drawQueue().add<TextDrawRequest>(
-					font, text, pos, -1, true, kWhite, 1);
+					font, text, pos, -1, true, kWhite, -kForegroundOrderCount + 1);
 			}
 			TASK_YIELD(1);
 		}


Commit: 4fa0c5a590ec29b64bee95ef798ea5f093418557
    https://github.com/scummvm/scummvm/commit/4fa0c5a590ec29b64bee95ef798ea5f093418557
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:59+02:00

Commit Message:
ALCACHOFA: Handle video playback errors more gracefully

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


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 35431a6b226..46759091b71 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -30,6 +30,7 @@
 #include "graphics/framelimiter.h"
 #include "graphics/thumbnail.h"
 #include "image/png.h"
+#include "video/avi_decoder.h"
 #include "video/mpegps_decoder.h"
 
 #include "alcachofa.h"
@@ -56,6 +57,7 @@ AlcachofaEngine::AlcachofaEngine(OSystem *syst, const ADGameDescription *gameDes
 	: Engine(syst)
 	, _gameDescription(gameDesc)
 	, _eventLoopSemaphore("engine") {
+	assert(gameDesc != nullptr);
 	g_engine = this;
 }
 
@@ -129,19 +131,37 @@ Common::Error AlcachofaEngine::run() {
 }
 
 void AlcachofaEngine::playVideo(int32 videoId) {
+	// Video files are either MPEG PS or AVI
 	FakeLock lock("playVideo", _eventLoopSemaphore);
-	Video::MPEGPSDecoder decoder;
-	if (!decoder.loadFile(Common::Path(Common::String::format("Data/DATA%02d.BIN", videoId + 1))))
-		error("Could not find video %d", videoId);
-	_sounds.stopAll();
-	auto texture = _renderer->createTexture(decoder.getWidth(), decoder.getHeight(), false);
-	decoder.start();
+	File *file = new File();
+	if (!file->open(Path(Common::String::format("Data/DATA%02d.BIN", videoId + 1)))) {
+		game().invalidVideo(videoId, "open file");
+		return;
+	}
+	char magic[4];
+	if (file->read(magic, sizeof(magic)) != sizeof(magic) || !file->seek(0)) {
+		delete file;
+		game().invalidVideo(videoId, "read magic");
+		return;
+	}
+	ScopedPtr<Video::VideoDecoder> decoder;
+	if (memcmp(magic, "RIFF", sizeof(magic)) == 0)
+		decoder.reset(new Video::AVIDecoder());
+	else
+		decoder.reset(new Video::MPEGPSDecoder());
+	if (!decoder->loadStream(file)) {
+		game().invalidVideo(videoId, "decode video");
+		return;
+	}
 
+	_sounds.stopAll();
+	auto texture = _renderer->createTexture(decoder->getWidth(), decoder->getHeight(), false);
 	Common::Event e;
-	while (!decoder.endOfVideo() && !shouldQuit()) {
-		if (decoder.needsUpdate())
+	decoder->start();
+	while (!decoder->endOfVideo() && !shouldQuit()) {
+		if (decoder->needsUpdate())
 		{
-			auto surface = decoder.decodeNextFrame();
+			auto surface = decoder->decodeNextFrame();
 			if (surface)
 				texture->update(*surface);
 			_renderer->begin();
@@ -161,9 +181,9 @@ void AlcachofaEngine::playVideo(int32 videoId) {
 		if (_input.wasAnyMouseReleased() || _input.wasMenuKeyPressed())
 			break;
 
-		g_system->delayMillis(decoder.getTimeToNextFrame());
+		g_system->delayMillis(decoder->getTimeToNextFrame());
 	}
-	decoder.stop();
+	decoder->stop();
 }
 
 void AlcachofaEngine::fadeExit() {
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index df2a6e224f1..a9e888dfb46 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -112,6 +112,7 @@ public:
 	AlcachofaEngine(OSystem *syst, const ADGameDescription *gameDesc);
 	~AlcachofaEngine() override;
 
+	inline const ADGameDescription &gameDescription() const { return *_gameDescription; }
 	inline IRenderer &renderer() { return *_renderer; }
 	inline DrawQueue &drawQueue() { return *_drawQueue; }
 	inline Camera &camera() { return _camera; }
diff --git a/engines/alcachofa/console.cpp b/engines/alcachofa/console.cpp
index b2fa6b98f15..cd298942bd1 100644
--- a/engines/alcachofa/console.cpp
+++ b/engines/alcachofa/console.cpp
@@ -19,6 +19,10 @@
  *
  */
 
+#ifndef USE_TEXT_CONSOLE_FOR_DEBUGGER
+#include "gui/console.h"
+#endif
+
 #include "console.h"
 #include "script.h"
 #include "alcachofa.h"
@@ -46,6 +50,7 @@ Console::Console() : GUI::Debugger() {
 	registerCmd("debugMode", WRAP_METHOD(Console, cmdDebugMode));
 	registerCmd("tp", WRAP_METHOD(Console, cmdTeleport));
 	registerCmd("toggleRoomFloor", WRAP_METHOD(Console, cmdToggleRoomFloor));
+	registerCmd("playVideo", WRAP_METHOD(Console, cmdPlayVideo));
 }
 
 Console::~Console() {
@@ -223,12 +228,10 @@ bool Console::cmdDebugMode(int argc, const char **args) {
 	}
 
 	int32 param = -1;
-	if (argc > 2)
-	{
+	if (argc > 2) {
 		char *end = nullptr;
 		param = (int32)strtol(args[2], &end, 10);
-		if (end == nullptr || *end != '\0')
-		{
+		if (end == nullptr || *end != '\0') {
 			debugPrintf("Debug mode parameter can only be integers");
 			return true;
 		}
@@ -240,9 +243,8 @@ bool Console::cmdDebugMode(int argc, const char **args) {
 }
 
 bool Console::cmdTeleport(int argc, const char **args) {
-	if (argc < 1 || argc > 2)
-	{
-		debugPrintf("usagge: tp [<character>]\n");
+	if (argc < 1 || argc > 2) {
+		debugPrintf("usage: tp [<character>]\n");
 		debugPrintf("characters:\n");
 		debugPrintf("  0 - Both\n");
 		debugPrintf("  1 - Mortadelo\n");
@@ -250,13 +252,11 @@ bool Console::cmdTeleport(int argc, const char **args) {
 	}
 
 	int32 param = 0;
-	if (argc > 1)
-	{
+	if (argc > 1) {
 		char *end = nullptr;
 		param = (int32)strtol(args[1], &end, 10);
-		if (end == nullptr || *end != '\0')
-		{
-			debugPrintf("Character kind can only be integer\n");
+		if (end == nullptr || *end != '\0') {
+			debugPrintf("Character kind can only be an integer\n");
 			return true;
 		}
 	}
@@ -267,8 +267,7 @@ bool Console::cmdTeleport(int argc, const char **args) {
 
 bool Console::cmdToggleRoomFloor(int argc, const char **args) {
 	auto room = g_engine->player().currentRoom();
-	if (room == nullptr)
-	{
+	if (room == nullptr) {
 		debugPrintf("No room is active");
 		return true;
 	}
@@ -277,4 +276,27 @@ bool Console::cmdToggleRoomFloor(int argc, const char **args) {
 	return false;
 }
 
+bool Console::cmdPlayVideo(int argc, const char **args) {
+	if (argc == 2) {
+		char *end = nullptr;
+		int32 videoId = (int32)strtol(args[1], &end, 10);
+		if (end == nullptr || *end != '\0') {
+			debugPrintf("Video ID can only be an integer\n");
+			return true;
+		}
+		
+#ifndef USE_TEXT_CONSOLE_FOR_DEBUGGER
+		// we have to close the console *now* to properly see the video
+		_debuggerDialog->close();
+		g_system->clearOverlay();
+#endif
+
+		g_engine->playVideo(videoId);
+		return false;
+	}
+	else
+		debugPrintf("usage: playVideo <id>\n");
+	return true;
+}
+
 } // End of namespace Alcachofa
diff --git a/engines/alcachofa/console.h b/engines/alcachofa/console.h
index 3d6e5e0b6ac..acfbb4fea6f 100644
--- a/engines/alcachofa/console.h
+++ b/engines/alcachofa/console.h
@@ -60,6 +60,7 @@ private:
 	bool cmdDebugMode(int argc, const char **args);
 	bool cmdTeleport(int argc, const char **args);
 	bool cmdToggleRoomFloor(int argc, const char **args);
+	bool cmdPlayVideo(int argc, const char **args);
 
 	bool _showGraphics = false;
 	bool _showInteractables = false;
diff --git a/engines/alcachofa/game-movie-adventure.cpp b/engines/alcachofa/game-movie-adventure.cpp
index 13793546476..57611258608 100644
--- a/engines/alcachofa/game-movie-adventure.cpp
+++ b/engines/alcachofa/game-movie-adventure.cpp
@@ -140,6 +140,14 @@ class GameMovieAdventure : public Game {
 			return;
 		Game::missingSound(fileName);
 	}
+
+	void invalidVideo(int32 videoId, const char *context) override {
+		// for the one, known AVI problem, let's not block development
+		if (videoId == 1 && g_engine->gameDescription().language != DE_DEU)
+			warning("Could not play video %d (%s) (known problem with AVI decoder)", videoId, context);
+		else
+			Game::invalidVideo(videoId, context);
+	}
 };
 
 Game *Game::createForMovieAdventure() {
diff --git a/engines/alcachofa/game.cpp b/engines/alcachofa/game.cpp
index 8134330820e..b80749dbaf0 100644
--- a/engines/alcachofa/game.cpp
+++ b/engines/alcachofa/game.cpp
@@ -193,4 +193,8 @@ void Game::notEnoughObjectDataRead(const char *room, int64 filePos, int64 object
 	_message("Did not read enough data (%dll < %dll) for an object in room %s", filePos, objectEnd, room);
 }
 
+void Game::invalidVideo(int32 videoId, const char *context) {
+	_message("Could not play video %d (%s)", videoId, context);
+}
+
 }
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index e6de8e45c88..039bf12f99c 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -87,6 +87,7 @@ public:
 	virtual void invalidSNDFormat(uint format, uint channels, uint freq, uint bps);
 	virtual void notEnoughRoomDataRead(const char *path, int64 filePos, int64 objectEnd);
 	virtual void notEnoughObjectDataRead(const char *room, int64 filePos, int64 objectEnd);
+	virtual void invalidVideo(int32 videoId, const char *context);
 
 	static Game *createForMovieAdventure();
 


Commit: 7275e191a93ac355bfa44c8840413fc91ea424e5
    https://github.com/scummvm/scummvm/commit/7275e191a93ac355bfa44c8840413fc91ea424e5
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:04:59+02:00

Commit Message:
ALCACHOFA: Fix loading empty dialog lines

Changed paths:
    engines/alcachofa/rooms.cpp


diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 1252024ee50..0d1040282c8 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -790,7 +790,8 @@ void World::loadDialogLines() {
 			dialogLineEnd--;
 
 		if (cursor >= dialogLineEnd) {
-			g_engine->game().invalidDialogLine(_dialogLines.size());
+			if (cursor > dialogLineEnd)
+				g_engine->game().invalidDialogLine(_dialogLines.size());
 			cursor = lineStart; // store an empty string
 			dialogLineEnd = lineStart + 1;
 		}


Commit: e5400d11909abb95e79fe232a9a285f645d0c558
    https://github.com/scummvm/scummvm/commit/e5400d11909abb95e79fe232a9a285f645d0c558
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:05:00+02:00

Commit Message:
ALCACHOFA: Add support for german demo

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


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 46759091b71..0894808a9ec 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -84,6 +84,7 @@ Common::Error AlcachofaEngine::run() {
 	_globalUI.reset(new GlobalUI());
 	_menu.reset(new Menu());
 	setMillis(0);
+	game().onLoadedGameFiles();
 
 	if (!tryLoadFromLauncher()) {
 		_script->createProcess(MainCharacterKind::None, "CREDITOS_INICIALES");
@@ -131,6 +132,11 @@ Common::Error AlcachofaEngine::run() {
 }
 
 void AlcachofaEngine::playVideo(int32 videoId) {
+	if (game().isKnownBadVideo(videoId)) {
+		warning("Skipping known bad video %d", videoId);
+		return;
+	}
+
 	// Video files are either MPEG PS or AVI
 	FakeLock lock("playVideo", _eventLoopSemaphore);
 	File *file = new File();
diff --git a/engines/alcachofa/detection_tables.h b/engines/alcachofa/detection_tables.h
index 5c838a1aab5..14c9ab54e4e 100644
--- a/engines/alcachofa/detection_tables.h
+++ b/engines/alcachofa/detection_tables.h
@@ -39,6 +39,16 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE,
 		GUIO2(GAMEOPTION_32BITS, GAMEOPTION_HIGH_QUALITY)
 	},
+	{
+		"mort_phil_adventura_de_cine",
+		"Clever & Smart - A Movie Adventure",
+		AD_ENTRY1s("Textos/Objetos.nkr", "8dce25494470209d4882bf12f1a5ea42", 19208),
+		Common::DE_DEU,
+		Common::kPlatformWindows,
+		ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE | ADGF_DEMO,
+		GUIO2(GAMEOPTION_32BITS, GAMEOPTION_HIGH_QUALITY)
+	},
+
 
 	// The "english" version is just the spanish version with english subtitles...
 	{
diff --git a/engines/alcachofa/game-movie-adventure.cpp b/engines/alcachofa/game-movie-adventure.cpp
index 57611258608..7c9b0190e8f 100644
--- a/engines/alcachofa/game-movie-adventure.cpp
+++ b/engines/alcachofa/game-movie-adventure.cpp
@@ -28,6 +28,14 @@ using namespace Common;
 namespace Alcachofa {
 
 class GameMovieAdventure : public Game {
+	void onLoadedGameFiles() override {
+		// this notifies the script whether we are a demo
+		if (g_engine->world().loadedMapCount() == 2)
+			g_engine->script().variable("EsJuegoCompleto") = 2;
+		else if (g_engine->world().loadedMapCount() == 3) // I don't know this demo
+			g_engine->script().variable("EsJuegoCompleto") = 1;
+	}
+
 	bool doesRoomHaveBackground(const Room *room) override {
 		return !room->name().equalsIgnoreCase("Global") &&
 			!room->name().equalsIgnoreCase("HABITACION_NEGRA");
@@ -78,13 +86,35 @@ class GameMovieAdventure : public Game {
 			"MONITOR___OL_EFECTO_FONDO.AN0",
 			nullptr
 		};
-		for (const char **exemption = exemptions; *exemption != nullptr; exemption++) {
-			if (fileName.equalsIgnoreCase(*exemption)) {
-				debugC(1, kDebugGraphics, "Animation exemption triggered: %s", fileName.c_str());
-				return;
+
+		// 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",
+			nullptr
+		};
+
+		const auto isInExemptions = [&] (const char *const *const list) {
+			for (const char *const *exemption = list; *exemption != nullptr; exemption++) {
+				if (fileName.equalsIgnoreCase(*exemption))
+					return true;
 			}
-		}
-		Game::missingAnimation(fileName);
+			return false;
+		};
+
+		if (isInExemptions(exemptions) ||
+			((g_engine->gameDescription().flags & ADGF_DEMO) && isInExemptions(demoExemptions)))
+			debugC(1, kDebugGraphics, "Animation exemption triggered: %s", fileName.c_str());
+		else
+			Game::missingAnimation(fileName);
 	}
 
 	void unknownAnimateObject(const char *name) override {
@@ -138,13 +168,23 @@ class GameMovieAdventure : public Game {
 	void missingSound(const String &fileName) override {
 		if (fileName == "CHAS" || fileName == "517")
 			return;
+		if ((g_engine->gameDescription().flags & ADGF_DEMO) && (
+			fileName == "M4996" ||
+			fileName == "T40"))
+			return;
 		Game::missingSound(fileName);
 	}
 
+	bool isKnownBadVideo(int32 videoId) override {
+		return
+			(videoId == 3 && (g_engine->gameDescription().flags & ADGF_DEMO)) || // problem with MPEG PS decoding
+			Game::isKnownBadVideo(videoId);
+	}
+
 	void invalidVideo(int32 videoId, const char *context) override {
 		// for the one, known AVI problem, let's not block development
 		if (videoId == 1 && g_engine->gameDescription().language != DE_DEU)
-			warning("Could not play video %d (%s) (known problem with AVI decoder)", videoId, context);
+			warning("Could not play video %d (%s) (WMV not supported)", videoId, context);
 		else
 			Game::invalidVideo(videoId, context);
 	}
diff --git a/engines/alcachofa/game.cpp b/engines/alcachofa/game.cpp
index b80749dbaf0..0201d5cec57 100644
--- a/engines/alcachofa/game.cpp
+++ b/engines/alcachofa/game.cpp
@@ -36,6 +36,8 @@ Game::Game()
 {
 }
 
+void Game::onLoadedGameFiles() {}
+
 bool Game::doesRoomHaveBackground(const Room *room) {
 	return true;
 }
@@ -193,6 +195,10 @@ void Game::notEnoughObjectDataRead(const char *room, int64 filePos, int64 object
 	_message("Did not read enough data (%dll < %dll) for an object in room %s", filePos, objectEnd, room);
 }
 
+bool Game::isKnownBadVideo(int32 videoId) {
+	return false;
+}
+
 void Game::invalidVideo(int32 videoId, const char *context) {
 	_message("Could not play video %d (%s)", videoId, context);
 }
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index 039bf12f99c..43a056abd5f 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -48,6 +48,8 @@ public:
 	Game();
 	virtual ~Game() = default;
 
+	virtual void onLoadedGameFiles();
+
 	virtual bool doesRoomHaveBackground(const Room *room);
 	virtual void unknownRoomObject(const Common::String &type);
 	virtual void unknownRoomType(const Common::String &type);
@@ -87,6 +89,7 @@ public:
 	virtual void invalidSNDFormat(uint format, uint channels, uint freq, uint bps);
 	virtual void notEnoughRoomDataRead(const char *path, int64 filePos, int64 objectEnd);
 	virtual void notEnoughObjectDataRead(const char *room, int64 filePos, int64 objectEnd);
+	virtual bool isKnownBadVideo(int32 videoId);
 	virtual void invalidVideo(int32 videoId, const char *context);
 
 	static Game *createForMovieAdventure();


Commit: 45514217d96a15d404a384355fb9ab5620166fb8
    https://github.com/scummvm/scummvm/commit/45514217d96a15d404a384355fb9ab5620166fb8
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:05:00+02:00

Commit Message:
ALCACHOFA: Rename header guards

Changed paths:
    engines/alcachofa/camera.h
    engines/alcachofa/common.h
    engines/alcachofa/debug.h
    engines/alcachofa/game.h
    engines/alcachofa/global-ui.h
    engines/alcachofa/graphics.h
    engines/alcachofa/input.h
    engines/alcachofa/menu.h
    engines/alcachofa/objects.h
    engines/alcachofa/player.h
    engines/alcachofa/rooms.h
    engines/alcachofa/scheduler.h
    engines/alcachofa/script-debug.h
    engines/alcachofa/script.h
    engines/alcachofa/shape.h
    engines/alcachofa/sounds.h


diff --git a/engines/alcachofa/camera.h b/engines/alcachofa/camera.h
index f3099eef5d0..912b4fa14b3 100644
--- a/engines/alcachofa/camera.h
+++ b/engines/alcachofa/camera.h
@@ -19,8 +19,8 @@
  *
  */
 
-#ifndef CAMERA_H
-#define CAMERA_H
+#ifndef ALCACHOFA_CAMERA_H
+#define ALCACHOFA_CAMERA_H
 
 #include "common.h"
 #include "math/matrix4.h"
@@ -119,4 +119,4 @@ private:
 
 }
 
-#endif // CAMERA_H
+#endif // ALCACHOFA_CAMERA_H
diff --git a/engines/alcachofa/common.h b/engines/alcachofa/common.h
index dd793893f13..f8c43c165d4 100644
--- a/engines/alcachofa/common.h
+++ b/engines/alcachofa/common.h
@@ -19,8 +19,8 @@
  *
  */
 
-#ifndef COMMON_H
-#define COMMON_H
+#ifndef ALCACHOFA_COMMON_H
+#define ALCACHOFA_COMMON_H
 
 #include "common/scummsys.h"
 #include "common/rect.h"
diff --git a/engines/alcachofa/debug.h b/engines/alcachofa/debug.h
index abe6cc8ff02..766da50f01a 100644
--- a/engines/alcachofa/debug.h
+++ b/engines/alcachofa/debug.h
@@ -19,8 +19,8 @@
  *
  */
 
-#ifndef DEBUG_H
-#define DEBUG_H
+#ifndef ALCACHOFA_DEBUG_H
+#define ALCACHOFA_DEBUG_H
 
 #include "alcachofa.h"
 
@@ -231,4 +231,4 @@ public:
 
 }
 
-#endif // DEBUG_H
+#endif // ALCACHOFA_DEBUG_H
diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index 43a056abd5f..65b99e52671 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -20,8 +20,8 @@
  *
  */
 
-#ifndef GAME_H
-#define GAME_H
+#ifndef ALCACHOFA_GAME_H
+#define ALCACHOFA_GAME_H
 
 #include "common/textconsole.h"
 #include "common/file.h"
@@ -99,4 +99,4 @@ public:
 
 }
 
-#endif // GAME_H
+#endif // ALCACHOFA_GAME_H
diff --git a/engines/alcachofa/global-ui.h b/engines/alcachofa/global-ui.h
index 44964edeb6b..1c0b786b8c8 100644
--- a/engines/alcachofa/global-ui.h
+++ b/engines/alcachofa/global-ui.h
@@ -19,8 +19,8 @@
  *
  */
 
-#ifndef GLOBAL_UI_H
-#define GLOBAL_UI_H
+#ifndef ALCACHOFA_GLOBAL_UI_H
+#define ALCACHOFA_GLOBAL_UI_H
 
 #include "objects.h"
 
@@ -70,4 +70,4 @@ Task *showCenterBottomText(Process &process, int32 dialogId, uint32 durationMs);
 }
 
 
-#endif // GLOBAL_UI_H
+#endif // ALCACHOFA_GLOBAL_UI_H
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index cd6c681d7a8..22d35d86454 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -19,8 +19,8 @@
  *
  */
 
-#ifndef GRAPHICS_H
-#define GRAPHICS_H
+#ifndef ALCACHOFA_GRAPHICS_H
+#define ALCACHOFA_GRAPHICS_H
 
 #include "common/ptr.h"
 #include "common/stream.h"
@@ -476,4 +476,4 @@ private:
 
 }
 
-#endif
+#endif // ALCACHOFA_ENGINE_H
diff --git a/engines/alcachofa/input.h b/engines/alcachofa/input.h
index 798f2e0369b..4f02eb13d7e 100644
--- a/engines/alcachofa/input.h
+++ b/engines/alcachofa/input.h
@@ -19,8 +19,8 @@
  *
  */
 
-#ifndef INPUT_H
-#define INPUT_H
+#ifndef ALCACHOFA_INPUT_H
+#define ALCACHOFA_INPUT_H
 
 #include "common/events.h"
 #include "common/ptr.h"
@@ -68,4 +68,4 @@ private:
 
 }
 
-#endif // INPUT_H
+#endif // ALCACHOFA_INPUT_H
diff --git a/engines/alcachofa/menu.h b/engines/alcachofa/menu.h
index 5e51f724058..207ca2ee8f6 100644
--- a/engines/alcachofa/menu.h
+++ b/engines/alcachofa/menu.h
@@ -19,8 +19,8 @@
  *
  */
 
-#ifndef MENU_H
-#define MENU_H
+#ifndef ALCACHOFA_MENU_H
+#define ALCACHOFA_MENU_H
 
 #include "common/scummsys.h"
 #include "common/savefile.h"
@@ -106,4 +106,4 @@ private:
 
 }
 
-#endif // MENU_H
+#endif // ALCACHOFA_MENU_H
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index 65210acc974..d4fd2fbc48c 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -19,8 +19,8 @@
  *
  */
 
-#ifndef OBJECTS_H
-#define OBJECTS_H
+#ifndef ALCACHOFA_OBJECTS_H
+#define ALCACHOFA_OBJECTS_H
 
 #include "shape.h"
 #include "graphics.h"
@@ -602,4 +602,4 @@ private:
 
 }
 
-#endif // OBJECTS_H
+#endif // ALCACHOFA_OBJECTS_H
diff --git a/engines/alcachofa/player.h b/engines/alcachofa/player.h
index ee0259c2362..c1d5baefe96 100644
--- a/engines/alcachofa/player.h
+++ b/engines/alcachofa/player.h
@@ -19,8 +19,8 @@
  *
  */
 
-#ifndef PLAYER_H
-#define PLAYER_H
+#ifndef ALCACHOFA_PLAYER_H
+#define ALCACHOFA_PLAYER_H
 
 #include "rooms.h"
 
@@ -81,4 +81,4 @@ private:
 
 }
 
-#endif // PLAYER_H
+#endif // ALCACHOFA_PLAYER_H
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index e08ae07ec4b..b62e0f656e0 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -19,8 +19,8 @@
  *
  */
 
-#ifndef ROOMS_H
-#define ROOMS_H
+#ifndef ALCACHOFA_ROOMS_H
+#define ALCACHOFA_ROOMS_H
 
 #include "objects.h"
 
@@ -211,4 +211,4 @@ private:
 
 }
 
-#endif // ROOMS_H
+#endif // ALCACHOFA_ROOMS_H
diff --git a/engines/alcachofa/scheduler.h b/engines/alcachofa/scheduler.h
index c8131e3fefa..d1739c3a9dc 100644
--- a/engines/alcachofa/scheduler.h
+++ b/engines/alcachofa/scheduler.h
@@ -19,8 +19,8 @@
  *
  */
 
-#ifndef SCHEDULER_H
-#define SCHEDULER_H
+#ifndef ALCACHOFA_SCHEDULER_H
+#define ALCACHOFA_SCHEDULER_H
 
 #include "common.h"
 
@@ -222,4 +222,4 @@ private:
 
 }
 
-#endif // SCHEDULER_H
+#endif // ALCACHOFA_SCHEDULER_H
diff --git a/engines/alcachofa/script-debug.h b/engines/alcachofa/script-debug.h
index 168903c22f4..573d502f4b4 100644
--- a/engines/alcachofa/script-debug.h
+++ b/engines/alcachofa/script-debug.h
@@ -19,8 +19,8 @@
  *
  */
 
-#ifndef SCRIPT_DEBUG_H
-#define SCRIPT_DEBUG_H
+#ifndef ALCACHOFA_SCRIPT_DEBUG_H
+#define ALCACHOFA_SCRIPT_DEBUG_H
 
 namespace Alcachofa {
 
@@ -127,4 +127,4 @@ static const char *const KernelCallNames[] = {
 
 }
 
-#endif // SCRIPT_DEBUG_H
+#endif // ALCACHOFA_SCRIPT_DEBUG_H
diff --git a/engines/alcachofa/script.h b/engines/alcachofa/script.h
index 0b8337a3430..bb93318b6b7 100644
--- a/engines/alcachofa/script.h
+++ b/engines/alcachofa/script.h
@@ -19,8 +19,8 @@
  *
  */
 
-#ifndef SCRIPT_H
-#define SCRIPT_H
+#ifndef ALCACHOFA_SCRIPT_H
+#define ALCACHOFA_SCRIPT_H
 
 #include "common.h"
 
@@ -190,4 +190,4 @@ private:
 
 }
 
-#endif // SCRIPT_H
+#endif // ALCACHOFA_SCRIPT_H
diff --git a/engines/alcachofa/shape.h b/engines/alcachofa/shape.h
index 4174aabc140..0c6b1f5f62e 100644
--- a/engines/alcachofa/shape.h
+++ b/engines/alcachofa/shape.h
@@ -19,8 +19,8 @@
  *
  */
 
-#ifndef SHAPE_H
-#define SHAPE_H
+#ifndef ALCACHOFA_SHAPE_H
+#define ALCACHOFA_SHAPE_H
 
 #include "common/stream.h"
 #include "common/array.h"
@@ -255,4 +255,4 @@ private:
 
 }
 
-#endif // SHAPE_H
+#endif // ALCACHOFA_SHAPE_H
diff --git a/engines/alcachofa/sounds.h b/engines/alcachofa/sounds.h
index 99978cdfe45..305c1879f0f 100644
--- a/engines/alcachofa/sounds.h
+++ b/engines/alcachofa/sounds.h
@@ -19,8 +19,8 @@
  *
  */
 
-#ifndef SOUNDS_H
-#define SOUNDS_H
+#ifndef ALCACHOFA_SOUNDS_H
+#define ALCACHOFA_SOUNDS_H
 
 #include "scheduler.h"
 #include "audio/mixer.h"
@@ -105,4 +105,4 @@ private:
 
 }
 
-#endif // SOUNDS_H
+#endif // ALCACHOFA_SOUNDS_H


Commit: d4fbf73b72adf454e25351f60a4b83848cc8f7e5
    https://github.com/scummvm/scummvm/commit/d4fbf73b72adf454e25351f60a4b83848cc8f7e5
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:05:00+02:00

Commit Message:
ALCACHOFA: Remove redundant semicolons

Changed paths:
    engines/alcachofa/tasks.h


diff --git a/engines/alcachofa/tasks.h b/engines/alcachofa/tasks.h
index 163fed7166f..2145b14a01f 100644
--- a/engines/alcachofa/tasks.h
+++ b/engines/alcachofa/tasks.h
@@ -31,16 +31,16 @@
 #define DEFINE_TASK(TaskName)
 #endif
 
-DEFINE_TASK(CamLerpPosTask);
-DEFINE_TASK(CamLerpScaleTask);
-DEFINE_TASK(CamLerpPosScaleTask);
-DEFINE_TASK(CamLerpRotationTask);
-DEFINE_TASK(CamShakeTask);
-DEFINE_TASK(CamWaitToStopTask);
-DEFINE_TASK(CamSetInactiveAttributeTask);
-DEFINE_TASK(SayTextTask);
-DEFINE_TASK(AnimateCharacterTask);
-DEFINE_TASK(LerpLodBiasTask);
+DEFINE_TASK(CamLerpPosTask)
+DEFINE_TASK(CamLerpScaleTask)
+DEFINE_TASK(CamLerpPosScaleTask)
+DEFINE_TASK(CamLerpRotationTask)
+DEFINE_TASK(CamShakeTask)
+DEFINE_TASK(CamWaitToStopTask)
+DEFINE_TASK(CamSetInactiveAttributeTask)
+DEFINE_TASK(SayTextTask)
+DEFINE_TASK(AnimateCharacterTask)
+DEFINE_TASK(LerpLodBiasTask)
 DEFINE_TASK(ArriveTask)
 DEFINE_TASK(DialogMenuTask)
 DEFINE_TASK(AnimateTask)


Commit: 59960db11405fc57e994d2034044b63b56a0fe24
    https://github.com/scummvm/scummvm/commit/59960db11405fc57e994d2034044b63b56a0fe24
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:05:00+02:00

Commit Message:
ALCACHOFA: Fix crash in fadeExit with gcc builds

Changed paths:
    engines/alcachofa/alcachofa.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 0894808a9ec..1e9559c54f0 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -211,10 +211,9 @@ void AlcachofaEngine::fadeExit() {
 		_renderer->begin();
 		_drawQueue->clear();
 		float t = ((float)(g_system->getMillis() - startTime)) / kFadeOutDuration;
-		if (room != nullptr)
-			room->draw();
 		_drawQueue->add<FadeDrawRequest>(FadeType::ToBlack, t, -kForegroundOrderCount);
-		_drawQueue->draw();
+		if (room != nullptr)
+			room->draw(); // this executes the drawQueue as well
 		_renderer->end();
 
 		limiter.delayBeforeSwap();
@@ -390,8 +389,7 @@ void AlcachofaEngine::getSavegameThumbnail(Graphics::Surface &thumbnail) {
 	g_engine->drawQueue().clear();
 	g_engine->renderer().begin();
 	g_engine->renderer().setOutput(thumbnail);
-	g_engine->player().currentRoom()->draw();
-	g_engine->drawQueue().draw();
+	g_engine->player().currentRoom()->draw(); // drawQueue is drawn here as well
 	g_engine->renderer().end();
 
 	// we should be within the event loop. as such it is quite safe to mess with the drawQueue or renderer


Commit: bdc4cd1d386192f71c398a092d6cb9bccc69c487
    https://github.com/scummvm/scummvm/commit/bdc4cd1d386192f71c398a092d6cb9bccc69c487
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:05:00+02:00

Commit Message:
ALCACHOFA: Handle missing voice/music files better

Changed paths:
    engines/alcachofa/sounds.cpp


diff --git a/engines/alcachofa/sounds.cpp b/engines/alcachofa/sounds.cpp
index f13b8ca0188..79205fda38a 100644
--- a/engines/alcachofa/sounds.cpp
+++ b/engines/alcachofa/sounds.cpp
@@ -133,6 +133,15 @@ static AudioStream *openAudio(const char *fileName) {
 
 SoundHandle Sounds::playSoundInternal(const char *fileName, byte volume, Mixer::SoundType type) {
 	AudioStream *stream = openAudio(fileName);
+	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
+		 * But only do this for speech as there is no skipping for sound effects
+		 * so those would live on forever and block up mixer channels
+		 * Music is fine as well as we clean up the music playack explicitly
+		 */
+		stream = makeSilentAudioStream(8000, false);
+	}
 	if (stream == nullptr)
 		return {};
 


Commit: d2e767e75b3c4463edcd9b69930b16896fa80682
    https://github.com/scummvm/scummvm/commit/d2e767e75b3c4463edcd9b69930b16896fa80682
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:05:00+02:00

Commit Message:
ALCACHOFA: Probably fix two compiler warnings

it is a never-ending battle...

Changed paths:
    engines/alcachofa/graphics-opengl.cpp
    engines/alcachofa/tasks.h


diff --git a/engines/alcachofa/graphics-opengl.cpp b/engines/alcachofa/graphics-opengl.cpp
index befaa7b35e0..658a34d916f 100644
--- a/engines/alcachofa/graphics-opengl.cpp
+++ b/engines/alcachofa/graphics-opengl.cpp
@@ -341,7 +341,7 @@ public:
 			GL_CALL(glTexCoordPointer(2, GL_FLOAT, 0, texCoords));
 		GL_CALL(glDrawArrays(GL_QUADS, 0, 4));
 
-#if _DEBUG
+#ifdef _DEBUG
 		// make sure we crash instead of someone using our stack arrays
 		GL_CALL(glVertexPointer(2, GL_FLOAT, sizeof(Vector2d), nullptr));
 		GL_CALL(glTexCoordPointer(2, GL_FLOAT, sizeof(Vector2d), nullptr));
diff --git a/engines/alcachofa/tasks.h b/engines/alcachofa/tasks.h
index 2145b14a01f..74cbabd9817 100644
--- a/engines/alcachofa/tasks.h
+++ b/engines/alcachofa/tasks.h
@@ -47,10 +47,13 @@ DEFINE_TASK(AnimateTask)
 DEFINE_TASK(CenterBottomTextTask)
 DEFINE_TASK(FadeTask)
 DEFINE_TASK(DoorTask)
-DEFINE_TASK(DelayTask)
 DEFINE_TASK(ScriptTimerTask)
 DEFINE_TASK(ScriptTask)
 DEFINE_TASK(PlaySoundTask)
 DEFINE_TASK(WaitForMusicTask)
 
+// this one is special as the implementation is in the same TU as the signature
+// which causes a warning on some pedantic compiler
+//DEFINE_TASK(DelayTask)  
+
 #undef DEFINE_TASK


Commit: 603264e965ef794bcbb2bca5c9c00b14013d72d2
    https://github.com/scummvm/scummvm/commit/603264e965ef794bcbb2bca5c9c00b14013d72d2
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:05:00+02:00

Commit Message:
ALCACHOFA: Use engine name in include paths

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/camera.cpp
    engines/alcachofa/camera.h
    engines/alcachofa/common.cpp
    engines/alcachofa/console.cpp
    engines/alcachofa/debug.h
    engines/alcachofa/game-movie-adventure.cpp
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/game.cpp
    engines/alcachofa/general-objects.cpp
    engines/alcachofa/global-ui.cpp
    engines/alcachofa/global-ui.h
    engines/alcachofa/graphics-opengl.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/graphics.h
    engines/alcachofa/input.cpp
    engines/alcachofa/menu.cpp
    engines/alcachofa/objects.h
    engines/alcachofa/player.cpp
    engines/alcachofa/player.h
    engines/alcachofa/rooms.cpp
    engines/alcachofa/rooms.h
    engines/alcachofa/scheduler.cpp
    engines/alcachofa/scheduler.h
    engines/alcachofa/script.cpp
    engines/alcachofa/script.h
    engines/alcachofa/shape.cpp
    engines/alcachofa/shape.h
    engines/alcachofa/sounds.cpp
    engines/alcachofa/sounds.h
    engines/alcachofa/ui-objects.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 1e9559c54f0..418fad1404e 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -33,17 +33,17 @@
 #include "video/avi_decoder.h"
 #include "video/mpegps_decoder.h"
 
-#include "alcachofa.h"
-#include "metaengine.h"
-#include "console.h"
-#include "detection.h"
-#include "player.h"
-#include "rooms.h"
-#include "script.h"
-#include "global-ui.h"
-#include "menu.h"
-#include "debug.h"
-#include "game.h"
+#include "alcachofa/alcachofa.h"
+#include "alcachofa/metaengine.h"
+#include "alcachofa/console.h"
+#include "alcachofa/detection.h"
+#include "alcachofa/player.h"
+#include "alcachofa/rooms.h"
+#include "alcachofa/script.h"
+#include "alcachofa/global-ui.h"
+#include "alcachofa/menu.h"
+#include "alcachofa/debug.h"
+#include "alcachofa/game.h"
 
 using namespace Math;
 
diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index d611a2a9d04..484cb608dc4 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -19,9 +19,9 @@
  *
  */
 
-#include "camera.h"
-#include "alcachofa.h"
-#include "script.h"
+#include "alcachofa/camera.h"
+#include "alcachofa/alcachofa.h"
+#include "alcachofa/script.h"
 
 #include "common/system.h"
 #include "math/vector4d.h"
diff --git a/engines/alcachofa/camera.h b/engines/alcachofa/camera.h
index 912b4fa14b3..5a091cf2a06 100644
--- a/engines/alcachofa/camera.h
+++ b/engines/alcachofa/camera.h
@@ -22,7 +22,7 @@
 #ifndef ALCACHOFA_CAMERA_H
 #define ALCACHOFA_CAMERA_H
 
-#include "common.h"
+#include "alcachofa/common.h"
 #include "math/matrix4.h"
 
 namespace Alcachofa {
diff --git a/engines/alcachofa/common.cpp b/engines/alcachofa/common.cpp
index ec8f0fc565e..57df1e46042 100644
--- a/engines/alcachofa/common.cpp
+++ b/engines/alcachofa/common.cpp
@@ -19,8 +19,8 @@
  *
  */
 
-#include "common.h"
-#include "detection.h"
+#include "alcachofa/common.h"
+#include "alcachofa/detection.h"
 
 using namespace Common;
 using namespace Math;
diff --git a/engines/alcachofa/console.cpp b/engines/alcachofa/console.cpp
index cd298942bd1..bd22334c168 100644
--- a/engines/alcachofa/console.cpp
+++ b/engines/alcachofa/console.cpp
@@ -23,9 +23,9 @@
 #include "gui/console.h"
 #endif
 
-#include "console.h"
-#include "script.h"
-#include "alcachofa.h"
+#include "alcachofa/console.h"
+#include "alcachofa/script.h"
+#include "alcachofa/alcachofa.h"
 
 using namespace Common;
 
diff --git a/engines/alcachofa/debug.h b/engines/alcachofa/debug.h
index 766da50f01a..91150d6285e 100644
--- a/engines/alcachofa/debug.h
+++ b/engines/alcachofa/debug.h
@@ -22,7 +22,7 @@
 #ifndef ALCACHOFA_DEBUG_H
 #define ALCACHOFA_DEBUG_H
 
-#include "alcachofa.h"
+#include "alcachofa/alcachofa.h"
 
 using namespace Common;
 
diff --git a/engines/alcachofa/game-movie-adventure.cpp b/engines/alcachofa/game-movie-adventure.cpp
index 7c9b0190e8f..66073d08bb8 100644
--- a/engines/alcachofa/game-movie-adventure.cpp
+++ b/engines/alcachofa/game-movie-adventure.cpp
@@ -19,9 +19,9 @@
  *
  */
 
-#include "alcachofa.h"
-#include "game.h"
-#include "script.h"
+#include "alcachofa/alcachofa.h"
+#include "alcachofa/game.h"
+#include "alcachofa/script.h"
 
 using namespace Common;
 
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index bbac1210204..735c4bf7673 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -19,11 +19,11 @@
  *
  */
 
-#include "objects.h"
-#include "rooms.h"
-#include "script.h"
-#include "global-ui.h"
-#include "alcachofa.h"
+#include "alcachofa/objects.h"
+#include "alcachofa/rooms.h"
+#include "alcachofa/script.h"
+#include "alcachofa/global-ui.h"
+#include "alcachofa/alcachofa.h"
 
 using namespace Common;
 using namespace Math;
diff --git a/engines/alcachofa/game.cpp b/engines/alcachofa/game.cpp
index 0201d5cec57..f7bc9bd7c8e 100644
--- a/engines/alcachofa/game.cpp
+++ b/engines/alcachofa/game.cpp
@@ -19,9 +19,9 @@
  *
  */
 
-#include "alcachofa.h"
-#include "game.h"
-#include "script.h"
+#include "alcachofa/alcachofa.h"
+#include "alcachofa/game.h"
+#include "alcachofa/script.h"
 
 using namespace Common;
 
diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index c7fdd8000dd..997f550649e 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -19,11 +19,11 @@
  *
  */
 
-#include "objects.h"
-#include "rooms.h"
-#include "scheduler.h"
-#include "global-ui.h"
-#include "alcachofa.h"
+#include "alcachofa/objects.h"
+#include "alcachofa/rooms.h"
+#include "alcachofa/scheduler.h"
+#include "alcachofa/global-ui.h"
+#include "alcachofa/alcachofa.h"
 
 #include "common/system.h"
 
diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index 88ff5fb292b..0d5d1004b23 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -19,10 +19,10 @@
  *
  */
 
-#include "global-ui.h"
-#include "menu.h"
-#include "alcachofa.h"
-#include "script.h"
+#include "alcachofa/global-ui.h"
+#include "alcachofa/menu.h"
+#include "alcachofa/alcachofa.h"
+#include "alcachofa/script.h"
 
 using namespace Common;
 
diff --git a/engines/alcachofa/global-ui.h b/engines/alcachofa/global-ui.h
index 1c0b786b8c8..dd3722a30cc 100644
--- a/engines/alcachofa/global-ui.h
+++ b/engines/alcachofa/global-ui.h
@@ -22,7 +22,7 @@
 #ifndef ALCACHOFA_GLOBAL_UI_H
 #define ALCACHOFA_GLOBAL_UI_H
 
-#include "objects.h"
+#include "alcachofa/objects.h"
 
 namespace Alcachofa {
 
diff --git a/engines/alcachofa/graphics-opengl.cpp b/engines/alcachofa/graphics-opengl.cpp
index 658a34d916f..bc27347e6d4 100644
--- a/engines/alcachofa/graphics-opengl.cpp
+++ b/engines/alcachofa/graphics-opengl.cpp
@@ -19,8 +19,8 @@
  *
  */
 
-#include "graphics.h"
-#include "detection.h"
+#include "alcachofa/graphics.h"
+#include "alcachofa/detection.h"
 
 #include "common/system.h"
 #include "engines/util.h"
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index a275ed99848..40b5375797f 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -19,10 +19,10 @@
  *
  */
 
-#include "graphics.h"
-#include "alcachofa.h"
-#include "shape.h"
-#include "global-ui.h"
+#include "alcachofa/graphics.h"
+#include "alcachofa/alcachofa.h"
+#include "alcachofa/shape.h"
+#include "alcachofa/global-ui.h"
 
 #include "common/system.h"
 #include "common/file.h"
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 22d35d86454..4f9ce4f3901 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -30,8 +30,8 @@
 #include "math/vector2d.h"
 #include "graphics/managed_surface.h"
 
-#include "camera.h"
-#include "common.h"
+#include "alcachofa/camera.h"
+#include "alcachofa/common.h"
 
 namespace Alcachofa {
 
diff --git a/engines/alcachofa/input.cpp b/engines/alcachofa/input.cpp
index 00245b6c6a8..56ad040d213 100644
--- a/engines/alcachofa/input.cpp
+++ b/engines/alcachofa/input.cpp
@@ -19,9 +19,9 @@
  *
  */
 
-#include "input.h"
-#include "alcachofa.h"
-#include "metaengine.h"
+#include "alcachofa/input.h"
+#include "alcachofa/alcachofa.h"
+#include "alcachofa/metaengine.h"
 
 using namespace Common;
 
diff --git a/engines/alcachofa/menu.cpp b/engines/alcachofa/menu.cpp
index e7988c15f80..3a23778f670 100644
--- a/engines/alcachofa/menu.cpp
+++ b/engines/alcachofa/menu.cpp
@@ -22,11 +22,11 @@
 #include "gui/message.h"
 #include "graphics/thumbnail.h"
 
-#include "alcachofa.h"
-#include "metaengine.h"
-#include "menu.h"
-#include "player.h"
-#include "script.h"
+#include "alcachofa/alcachofa.h"
+#include "alcachofa/metaengine.h"
+#include "alcachofa/menu.h"
+#include "alcachofa/player.h"
+#include "alcachofa/script.h"
 
 using namespace Common;
 using namespace Graphics;
diff --git a/engines/alcachofa/objects.h b/engines/alcachofa/objects.h
index d4fd2fbc48c..d5150d8bddf 100644
--- a/engines/alcachofa/objects.h
+++ b/engines/alcachofa/objects.h
@@ -22,8 +22,8 @@
 #ifndef ALCACHOFA_OBJECTS_H
 #define ALCACHOFA_OBJECTS_H
 
-#include "shape.h"
-#include "graphics.h"
+#include "alcachofa/shape.h"
+#include "alcachofa/graphics.h"
 
 #include "common/serializer.h"
 
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index 65f5c2bba05..8360d51bab2 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -19,10 +19,10 @@
  *
  */
 
-#include "player.h"
-#include "script.h"
-#include "alcachofa.h"
-#include "menu.h"
+#include "alcachofa/player.h"
+#include "alcachofa/script.h"
+#include "alcachofa/alcachofa.h"
+#include "alcachofa/menu.h"
 
 using namespace Common;
 
diff --git a/engines/alcachofa/player.h b/engines/alcachofa/player.h
index c1d5baefe96..72651ff016d 100644
--- a/engines/alcachofa/player.h
+++ b/engines/alcachofa/player.h
@@ -22,7 +22,7 @@
 #ifndef ALCACHOFA_PLAYER_H
 #define ALCACHOFA_PLAYER_H
 
-#include "rooms.h"
+#include "alcachofa/rooms.h"
 
 namespace Alcachofa {
 
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 0d1040282c8..0d162b797cc 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -19,11 +19,11 @@
  *
  */
 
-#include "alcachofa.h"
-#include "rooms.h"
-#include "script.h"
-#include "global-ui.h"
-#include "menu.h"
+#include "alcachofa/alcachofa.h"
+#include "alcachofa/rooms.h"
+#include "alcachofa/script.h"
+#include "alcachofa/global-ui.h"
+#include "alcachofa/menu.h"
 
 #include "common/file.h"
 
diff --git a/engines/alcachofa/rooms.h b/engines/alcachofa/rooms.h
index b62e0f656e0..649c1944c40 100644
--- a/engines/alcachofa/rooms.h
+++ b/engines/alcachofa/rooms.h
@@ -22,7 +22,7 @@
 #ifndef ALCACHOFA_ROOMS_H
 #define ALCACHOFA_ROOMS_H
 
-#include "objects.h"
+#include "alcachofa/objects.h"
 
 namespace Alcachofa {
 
diff --git a/engines/alcachofa/scheduler.cpp b/engines/alcachofa/scheduler.cpp
index 3a0c194c12e..fbc83ea14df 100644
--- a/engines/alcachofa/scheduler.cpp
+++ b/engines/alcachofa/scheduler.cpp
@@ -19,11 +19,11 @@
  *
  */
 
-#include "scheduler.h"
+#include "alcachofa/scheduler.h"
 
 #include "common/system.h"
-#include "alcachofa.h"
-#include "menu.h"
+#include "alcachofa/alcachofa.h"
+#include "alcachofa/menu.h"
 
 using namespace Common;
 
@@ -173,7 +173,7 @@ void Process::debugPrint() {
 
 #define DEFINE_TASK(TaskName) \
 	extern Task *constructTask_##TaskName(Process &process, Serializer &s);
-#include "tasks.h"
+#include "alcachofa/tasks.h"
 
 static Task *readTask(Process &process, Serializer &s) {
 	assert(s.isLoading());
@@ -183,7 +183,7 @@ static Task *readTask(Process &process, Serializer &s) {
 #define DEFINE_TASK(TaskName) \
 	if (taskName == #TaskName) \
 		return constructTask_##TaskName(process, s);
-#include "tasks.h"
+#include "alcachofa/tasks.h"
 
 	error("Invalid task type in savestate: %s", taskName.c_str());
 }
diff --git a/engines/alcachofa/scheduler.h b/engines/alcachofa/scheduler.h
index d1739c3a9dc..1f9c79b34f0 100644
--- a/engines/alcachofa/scheduler.h
+++ b/engines/alcachofa/scheduler.h
@@ -22,7 +22,7 @@
 #ifndef ALCACHOFA_SCHEDULER_H
 #define ALCACHOFA_SCHEDULER_H
 
-#include "common.h"
+#include "alcachofa/common.h"
 
 #include "common/stack.h"
 #include "common/str.h"
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 4ccd9eecc2e..1a246fc1801 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -19,11 +19,11 @@
  *
  */
 
-#include "script.h"
-#include "rooms.h"
-#include "global-ui.h"
-#include "alcachofa.h"
-#include "script-debug.h"
+#include "alcachofa/script.h"
+#include "alcachofa/rooms.h"
+#include "alcachofa/global-ui.h"
+#include "alcachofa/alcachofa.h"
+#include "alcachofa/script-debug.h"
 
 #include "common/file.h"
 
diff --git a/engines/alcachofa/script.h b/engines/alcachofa/script.h
index bb93318b6b7..8122ec2954a 100644
--- a/engines/alcachofa/script.h
+++ b/engines/alcachofa/script.h
@@ -22,7 +22,7 @@
 #ifndef ALCACHOFA_SCRIPT_H
 #define ALCACHOFA_SCRIPT_H
 
-#include "common.h"
+#include "alcachofa/common.h"
 
 #include "common/hashmap.h"
 #include "common/span.h"
diff --git a/engines/alcachofa/shape.cpp b/engines/alcachofa/shape.cpp
index 494b36cd654..0459a6bfa99 100644
--- a/engines/alcachofa/shape.cpp
+++ b/engines/alcachofa/shape.cpp
@@ -19,7 +19,7 @@
  *
  */
 
-#include "shape.h"
+#include "alcachofa/shape.h"
 
 using namespace Common;
 using namespace Math;
diff --git a/engines/alcachofa/shape.h b/engines/alcachofa/shape.h
index 0c6b1f5f62e..3ac64f41e19 100644
--- a/engines/alcachofa/shape.h
+++ b/engines/alcachofa/shape.h
@@ -30,7 +30,7 @@
 #include "common/util.h"
 #include "math/vector2d.h"
 
-#include "common.h"
+#include "alcachofa/common.h"
 
 namespace Alcachofa {
 
diff --git a/engines/alcachofa/sounds.cpp b/engines/alcachofa/sounds.cpp
index 79205fda38a..1e2a70edea7 100644
--- a/engines/alcachofa/sounds.cpp
+++ b/engines/alcachofa/sounds.cpp
@@ -19,10 +19,10 @@
  *
  */
 
-#include "sounds.h"
-#include "rooms.h"
-#include "alcachofa.h"
-#include "detection.h"
+#include "alcachofa/sounds.h"
+#include "alcachofa/rooms.h"
+#include "alcachofa/alcachofa.h"
+#include "alcachofa/detection.h"
 
 #include "common/file.h"
 #include "common/substream.h"
diff --git a/engines/alcachofa/sounds.h b/engines/alcachofa/sounds.h
index 305c1879f0f..895b0bf5e3c 100644
--- a/engines/alcachofa/sounds.h
+++ b/engines/alcachofa/sounds.h
@@ -22,7 +22,7 @@
 #ifndef ALCACHOFA_SOUNDS_H
 #define ALCACHOFA_SOUNDS_H
 
-#include "scheduler.h"
+#include "alcachofa/scheduler.h"
 #include "audio/mixer.h"
 #include "audio/audiostream.h"
 
diff --git a/engines/alcachofa/ui-objects.cpp b/engines/alcachofa/ui-objects.cpp
index 5ef0961504c..6e68b87fac7 100644
--- a/engines/alcachofa/ui-objects.cpp
+++ b/engines/alcachofa/ui-objects.cpp
@@ -19,12 +19,12 @@
  *
  */
 
-#include "alcachofa.h"
-#include "script.h"
-#include "global-ui.h"
-#include "menu.h"
-#include "objects.h"
-#include "rooms.h"
+#include "alcachofa/alcachofa.h"
+#include "alcachofa/script.h"
+#include "alcachofa/global-ui.h"
+#include "alcachofa/menu.h"
+#include "alcachofa/objects.h"
+#include "alcachofa/rooms.h"
 
 using namespace Common;
 


Commit: 820ba7894b7150ff176a04fcb37358bdffee9c92
    https://github.com/scummvm/scummvm/commit/820ba7894b7150ff176a04fcb37358bdffee9c92
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:05:00+02:00

Commit Message:
ALCACHOFA: Remove scummsys include

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


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 418fad1404e..1d147ab2c80 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -19,7 +19,6 @@
  *
  */
 
-#include "common/scummsys.h"
 #include "common/config-manager.h"
 #include "common/debug-channels.h"
 #include "common/events.h"
diff --git a/engines/alcachofa/alcachofa.h b/engines/alcachofa/alcachofa.h
index a9e888dfb46..c5e29336707 100644
--- a/engines/alcachofa/alcachofa.h
+++ b/engines/alcachofa/alcachofa.h
@@ -22,7 +22,6 @@
 #ifndef ALCACHOFA_H
 #define ALCACHOFA_H
 
-#include "common/scummsys.h"
 #include "common/system.h"
 #include "common/error.h"
 #include "common/fs.h"
diff --git a/engines/alcachofa/common.h b/engines/alcachofa/common.h
index f8c43c165d4..d94fa196718 100644
--- a/engines/alcachofa/common.h
+++ b/engines/alcachofa/common.h
@@ -22,7 +22,6 @@
 #ifndef ALCACHOFA_COMMON_H
 #define ALCACHOFA_COMMON_H
 
-#include "common/scummsys.h"
 #include "common/rect.h"
 #include "common/serializer.h"
 #include "common/stream.h"
diff --git a/engines/alcachofa/menu.h b/engines/alcachofa/menu.h
index 207ca2ee8f6..2a3dc7fd736 100644
--- a/engines/alcachofa/menu.h
+++ b/engines/alcachofa/menu.h
@@ -22,7 +22,6 @@
 #ifndef ALCACHOFA_MENU_H
 #define ALCACHOFA_MENU_H
 
-#include "common/scummsys.h"
 #include "common/savefile.h"
 
 namespace Alcachofa {


Commit: 85897a520d9581b31ab986b66acb3fd94539bcef
    https://github.com/scummvm/scummvm/commit/85897a520d9581b31ab986b66acb3fd94539bcef
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:05:00+02:00

Commit Message:
ALCACHOFA: Fix bracing style

Changed paths:
    engines/alcachofa/script.cpp


diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 1a246fc1801..64b6437d85e 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -804,7 +804,7 @@ private:
 			}
 			return TaskReturn::finish(1);
 
-		// Camera tasks
+			// Camera tasks
 		case ScriptKernelTask::WaitCamStopping:
 			return TaskReturn::waitFor(g_engine->camera().waitToStop(process()));
 		case ScriptKernelTask::CamFollow:


Commit: 9f774019ae7afb16a34bb2c951cbb4705c8a02b1
    https://github.com/scummvm/scummvm/commit/9f774019ae7afb16a34bb2c951cbb4705c8a02b1
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:05:00+02:00

Commit Message:
ALCACHOFA: Break up one-liners in switches

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/camera.cpp
    engines/alcachofa/common.cpp
    engines/alcachofa/common.h
    engines/alcachofa/console.cpp
    engines/alcachofa/debug.h
    engines/alcachofa/game-objects.cpp
    engines/alcachofa/game.cpp
    engines/alcachofa/general-objects.cpp
    engines/alcachofa/global-ui.cpp
    engines/alcachofa/graphics-opengl.cpp
    engines/alcachofa/graphics.cpp
    engines/alcachofa/input.cpp
    engines/alcachofa/menu.cpp
    engines/alcachofa/metaengine.h
    engines/alcachofa/player.cpp
    engines/alcachofa/rooms.cpp
    engines/alcachofa/scheduler.cpp
    engines/alcachofa/scheduler.h
    engines/alcachofa/script.cpp
    engines/alcachofa/shape.cpp
    engines/alcachofa/sounds.cpp
    engines/alcachofa/ui-objects.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 1d147ab2c80..27a8d94803c 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -164,8 +164,7 @@ void AlcachofaEngine::playVideo(int32 videoId) {
 	Common::Event e;
 	decoder->start();
 	while (!decoder->endOfVideo() && !shouldQuit()) {
-		if (decoder->needsUpdate())
-		{
+		if (decoder->needsUpdate()) {
 			auto surface = decoder->decodeNextFrame();
 			if (surface)
 				texture->update(*surface);
@@ -225,8 +224,7 @@ void AlcachofaEngine::fadeExit() {
 }
 
 void AlcachofaEngine::setDebugMode(DebugMode mode, int32 param) {
-	switch (mode)
-	{
+	switch (mode) {
 	case DebugMode::ClosestFloorPoint:
 		_debugHandler.reset(new ClosestFloorPointDebugHandler(param));
 		break;
@@ -242,7 +240,9 @@ void AlcachofaEngine::setDebugMode(DebugMode mode, int32 param) {
 	case DebugMode::FloorColor:
 		_debugHandler.reset(FloorColorDebugHandler::create(param, true));
 		break;
-	default: _debugHandler.reset(nullptr);
+	default:
+		_debugHandler.reset(nullptr);
+		break;
 	}
 	_input.toggleDebugInput(isDebugModeActive());
 }
@@ -259,8 +259,7 @@ void AlcachofaEngine::setMillis(uint32 newMillis) {
 	if (newMillis > sysMillis) {
 		_timeNegOffset = 0;
 		_timePosOffset = newMillis - sysMillis;
-	}
-	else {
+	} else {
 		_timeNegOffset = sysMillis - newMillis;
 		_timePosOffset = 0;
 	}
@@ -342,13 +341,12 @@ bool AlcachofaEngine::syncThumbnail(MySerializer &s, Graphics::ManagedSurface *t
 	if (s.isLoading()) {
 		auto prevPosition = s.readStream().pos();
 		Image::PNGDecoder pngDecoder;
-		if (pngDecoder.loadStream(s.readStream()) && pngDecoder.getSurface () != nullptr) {
+		if (pngDecoder.loadStream(s.readStream()) && pngDecoder.getSurface() != nullptr) {
 			if (thumbnail != nullptr) {
 				thumbnail->free();
 				thumbnail->copyFrom(*pngDecoder.getSurface());
 			}
-		}
-		else {
+		} else {
 			// If we do not get a thumbnail, maybe we get at least the marker that there is no thumbnail
 			s.readStream().seek(prevPosition, SEEK_SET);
 			uint32 magicValue = s.readStream().readUint32LE();
@@ -357,8 +355,7 @@ bool AlcachofaEngine::syncThumbnail(MySerializer &s, Graphics::ManagedSurface *t
 			else // this is not an error, just a pity
 				warning("No thumbnail stored in in-game savestate");
 		}
-	}
-	else {
+	} else {
 		if (thumbnail == nullptr ||
 			thumbnail->getPixels() == nullptr ||
 			!Image::writePNG(s.writeStream(), *thumbnail)) {
diff --git a/engines/alcachofa/camera.cpp b/engines/alcachofa/camera.cpp
index 484cb608dc4..5f9752717e9 100644
--- a/engines/alcachofa/camera.cpp
+++ b/engines/alcachofa/camera.cpp
@@ -58,7 +58,7 @@ void Camera::setFollow(WalkingCharacter *target, bool catchUp) {
 }
 
 void Camera::setPosition(Vector2d v) {
-	setPosition({v.getX(), v.getY(), _cur._usedCenter.z()});
+	setPosition({ v.getX(), v.getY(), _cur._usedCenter.z() });
 }
 
 void Camera::setPosition(Vector3d v) {
@@ -156,8 +156,8 @@ Vector3d Camera::transform3Dto2D(Vector3d v3d) const {
 }
 
 Point Camera::transform3Dto2D(Point p3d) const {
-	auto v2d = transform3Dto2D({(float)p3d.x, (float)p3d.y, kBaseScale});
-	return {(int16)v2d.x(), (int16)v2d.y()};
+	auto v2d = transform3Dto2D({ (float)p3d.x, (float)p3d.y, kBaseScale });
+	return { (int16)v2d.x(), (int16)v2d.y() };
 }
 
 void Camera::update() {
@@ -191,7 +191,7 @@ void Camera::updateFollowing(float deltaTime) {
 	Vector3d targetCenter = setAppliedCenter({
 		_shake.getX() + _followTarget->position().x,
 		_shake.getY() + _followTarget->position().y - depthScale * 85,
-		_cur._usedCenter.z()});
+		_cur._usedCenter.z() });
 	targetCenter.y() -= halfHeight;
 	float distanceToTarget = as2D(_cur._usedCenter - targetCenter).getMagnitude();
 	float moveDistance = _followTarget->stepSizeFactor() * _cur._speed * deltaTime;
@@ -461,7 +461,7 @@ struct CamShakeTask final : public CamLerpTask {
 	CamShakeTask(Process &process, Vector2d amplitude, Vector2d frequency, int32 duration)
 		: CamLerpTask(process, duration, EasingType::Linear)
 		, _amplitude(amplitude)
-		, _frequency(frequency) { }
+		, _frequency(frequency) {}
 
 	CamShakeTask(Process &process, Serializer &s)
 		: CamLerpTask(process) {
@@ -547,9 +547,15 @@ struct CamSetInactiveAttributeTask final : public Task {
 
 		auto &state = _camera._backups[0];
 		switch (_attribute) {
-		case kPosZ: state._usedCenter.z() = _value; break;
-		case kScale: state._scale = _value; break;
-		case kRotation: state._rotation = _value; break;
+		case kPosZ:
+			state._usedCenter.z() = _value;
+			break;
+		case kScale:
+			state._scale = _value;
+			break;
+		case kRotation:
+			state._rotation = _value;
+			break;
 		default:
 			g_engine->game().unknownCamSetInactiveAttribute((int)_attribute);
 			break;
@@ -560,10 +566,18 @@ struct CamSetInactiveAttributeTask final : public Task {
 	void debugPrint() override {
 		const char *attributeName;
 		switch (_attribute) {
-		case kPosZ: attributeName = "PosZ"; break;
-		case kScale: attributeName = "Scale"; break;
-		case kRotation: attributeName = "Rotation"; break;
-		default: attributeName = "<unknown>"; break;
+		case kPosZ:
+			attributeName = "PosZ";
+			break;
+		case kScale:
+			attributeName = "Scale";
+			break;
+		case kRotation:
+			attributeName = "Rotation";
+			break;
+		default:
+			attributeName = "<unknown>";
+			break;
 		}
 		g_engine->console().debugPrintf("Set inactive camera %s to %f after %dms\n", attributeName, _value, _delay);
 	}
diff --git a/engines/alcachofa/common.cpp b/engines/alcachofa/common.cpp
index 57df1e46042..1f202eee319 100644
--- a/engines/alcachofa/common.cpp
+++ b/engines/alcachofa/common.cpp
@@ -29,11 +29,16 @@ namespace Alcachofa {
 
 float ease(float t, EasingType type) {
 	switch (type) {
-	case EasingType::Linear: return t;
-	case EasingType::InOut: return (1 - cosf(t * M_PI)) * 0.5f;
-	case EasingType::In: return 1 - cosf(t * M_PI * 0.5f);
-	case EasingType::Out: return sinf(t * M_PI * 0.5f);
-	default: return 0.0f;
+	case EasingType::Linear:
+		return t;
+	case EasingType::InOut:
+		return (1 - cosf(t * M_PI)) * 0.5f;
+	case EasingType::In:
+		return 1 - cosf(t * M_PI * 0.5f);
+	case EasingType::Out:
+		return sinf(t * M_PI * 0.5f);
+	default:
+		return 0.0f;
 	}
 }
 
diff --git a/engines/alcachofa/common.h b/engines/alcachofa/common.h
index d94fa196718..8690dd66241 100644
--- a/engines/alcachofa/common.h
+++ b/engines/alcachofa/common.h
@@ -103,7 +103,7 @@ struct FakeLock {
 	~FakeLock();
 	void operator = (FakeLock &&other) noexcept;
 	void release();
-	
+
 	inline bool isReleased() const { return _semaphore == nullptr; }
 private:
 	void debug(const char *action);
@@ -143,8 +143,7 @@ inline void syncStack(Common::Serializer &serializer, Common::Stack<T> &stack, v
 			serializeFunction(serializer, value);
 			stack.push(value);
 		}
-	}
-	else {
+	} else {
 		for (uint i = 0; i < size; i++)
 			serializeFunction(serializer, stack[i]);
 	}
diff --git a/engines/alcachofa/console.cpp b/engines/alcachofa/console.cpp
index bd22334c168..a6228a67a6a 100644
--- a/engines/alcachofa/console.cpp
+++ b/engines/alcachofa/console.cpp
@@ -53,8 +53,7 @@ Console::Console() : GUI::Debugger() {
 	registerCmd("playVideo", WRAP_METHOD(Console, cmdPlayVideo));
 }
 
-Console::~Console() {
-}
+Console::~Console() {}
 
 bool Console::isAnyDebugDrawingOn() const {
 	return
@@ -80,8 +79,7 @@ bool Console::cmdVar(int argc, const char **args) {
 			debugPrintf("Invalid variable name: %s", args[1]);
 		else
 			script.variable(args[1]) = value;
-	}
-	else if (argc == 2) {
+	} else if (argc == 2) {
 		bool hadSomeMatch = false;
 		for (auto it = script.beginVariables(); it != script.endVariables(); it++) {
 			if (matchString(it->_key.c_str(), args[1], true)) {
@@ -112,8 +110,7 @@ bool Console::cmdRoom(int argc, const char **args) {
 			debugPrintf("Player is currently in no room, cannot print details\n");
 			return true;
 		}
-	}
-	else {
+	} else {
 		room = g_engine->world().getRoomByName(args[1]);
 		if (room == nullptr) {
 			debugPrintf("Could not find room with exact name: %s\n", args[1]);
@@ -147,8 +144,7 @@ bool Console::cmdChangeRoom(int argc, const char **args) {
 	else if (argc == 1) {
 		Room *current = g_engine->player().currentRoom();
 		debugPrintf("Current room: %s\n", current == nullptr ? "<null>" : current->name().c_str());
-	}
-	else if (g_engine->world().getRoomByName(args[1]) == nullptr)
+	} else if (g_engine->world().getRoomByName(args[1]) == nullptr)
 		debugPrintf("Invalid room name: %s\n", args[1]);
 	else {
 		g_engine->player().changeRoom(args[1], true);
@@ -284,7 +280,7 @@ bool Console::cmdPlayVideo(int argc, const char **args) {
 			debugPrintf("Video ID can only be an integer\n");
 			return true;
 		}
-		
+
 #ifndef USE_TEXT_CONSOLE_FOR_DEBUGGER
 		// we have to close the console *now* to properly see the video
 		_debuggerDialog->close();
@@ -293,8 +289,7 @@ bool Console::cmdPlayVideo(int argc, const char **args) {
 
 		g_engine->playVideo(videoId);
 		return false;
-	}
-	else
+	} else
 		debugPrintf("usage: playVideo <id>\n");
 	return true;
 }
diff --git a/engines/alcachofa/debug.h b/engines/alcachofa/debug.h
index 91150d6285e..027ed96a6b8 100644
--- a/engines/alcachofa/debug.h
+++ b/engines/alcachofa/debug.h
@@ -81,8 +81,7 @@ public:
 
 		if (_polygonI >= 0 && (uint)_polygonI < floor->polygonCount())
 			drawIntersectionsFor(floor->at((uint)_polygonI), renderer);
-		else
-		{
+		else {
 			for (uint i = 0; i < floor->polygonCount(); i++)
 				drawIntersectionsFor(floor->at(i), renderer);
 		}
@@ -94,8 +93,7 @@ private:
 	void drawIntersectionsFor(const Polygon &polygon, IDebugRenderer *renderer) {
 		auto &camera = g_engine->camera();
 		auto mousePos3D = g_engine->input().debugInput().mousePos3D();
-		for (uint i = 0; i < polygon._points.size(); i++)
-		{
+		for (uint i = 0; i < polygon._points.size(); i++) {
 			if (!polygon.intersectsEdge(i, _fromPos3D, mousePos3D))
 				continue;
 			auto a = camera.transform3Dto2D(polygon._points[i]);
@@ -123,8 +121,7 @@ public:
 		g_engine->drawQueue().draw();
 
 		auto &input = g_engine->input().debugInput();
-		if (input.wasMouseRightPressed())
-		{
+		if (input.wasMouseRightPressed()) {
 			g_engine->setDebugMode(DebugMode::None, 0);
 			return;
 		}
@@ -149,8 +146,7 @@ public:
 private:
 	void teleport(MainCharacter &character, Point position) {
 		auto currentRoom = g_engine->player().currentRoom();
-		if (character.room() != currentRoom)
-		{
+		if (character.room() != currentRoom) {
 			character.resetTalking();
 			character.room() = currentRoom;
 		}
@@ -174,12 +170,12 @@ public:
 		const Room *room = g_engine->player().currentRoom();
 		uint floorCount = 0;
 		for (auto itObject = room->beginObjects(); itObject != room->endObjects(); ++itObject) {
-			FloorColor *floor = dynamic_cast<FloorColor*>(*itObject);
+			FloorColor *floor = dynamic_cast<FloorColor *>(*itObject);
 			if (floor == nullptr)
 				continue;
 			if (objectI <= 0)
 				// dynamic_cast is not possible due to Shape not having virtual methods
-				return new FloorColorDebugHandler(*(FloorColorShape*)(floor->shape()), useColor);
+				return new FloorColorDebugHandler(*(FloorColorShape *)(floor->shape()), useColor);
 			floorCount++;
 			objectI--;
 		}
@@ -200,15 +196,15 @@ public:
 			_isOnFloor = optColor.first;
 			if (!_isOnFloor) {
 				uint8 roomAlpha = (uint)(g_engine->player().currentRoom()->characterAlphaTint() * 255 / 100);
-				optColor.second = Color{ 255, 255, 255, roomAlpha };
+				optColor.second = Color { 255, 255, 255, roomAlpha };
 			}
 
 			_curColor = _useColor
-				? Color{ optColor.second.r, optColor.second.g, optColor.second.b, 255 }
-				: Color{ optColor.second.a, optColor.second.a, optColor.second.a, 255 };
+				? Color { optColor.second.r, optColor.second.g, optColor.second.b, 255 }
+			: Color { optColor.second.a, optColor.second.a, optColor.second.a, 255 };
 			g_engine->world().mortadelo().color() =
 				g_engine->world().filemon().color() =
-				_useColor ? optColor.second : Color{ 255, 255, 255, optColor.second.a };
+				_useColor ? optColor.second : Color { 255, 255, 255, optColor.second.a };
 		}
 
 		snprintf(_buffer, kBufferSize, "r:%3d g:%3d b:%3d a:%3d",
diff --git a/engines/alcachofa/game-objects.cpp b/engines/alcachofa/game-objects.cpp
index 735c4bf7673..1954551be68 100644
--- a/engines/alcachofa/game-objects.cpp
+++ b/engines/alcachofa/game-objects.cpp
@@ -48,7 +48,7 @@ Item::Item(const Item &other)
 void Item::draw() {
 	if (!isEnabled())
 		return;
-	Item* heldItem = g_engine->player().heldItem();
+	Item *heldItem = g_engine->player().heldItem();
 	if (heldItem == nullptr || !heldItem->name().equalsIgnoreCase(name()))
 		GraphicObject::draw();
 }
@@ -61,8 +61,7 @@ void Item::trigger() {
 			player.triggerObject(this, "MIRAR");
 		else
 			heldItem = nullptr;
-	}
-	else if (heldItem == nullptr)
+	} else if (heldItem == nullptr)
 		heldItem = this;
 	else if (g_engine->script().hasProcedure(name(), heldItem->name()) ||
 		!g_engine->script().hasProcedure(heldItem->name(), name()))
@@ -73,8 +72,7 @@ void Item::trigger() {
 
 ITriggerableObject::ITriggerableObject(ReadStream &stream)
 	: _interactionPoint(Shape(stream).firstPoint())
-	, _interactionDirection((Direction)stream.readSint32LE()) {
-}
+	, _interactionDirection((Direction)stream.readSint32LE()) {}
 
 void ITriggerableObject::onClick() {
 	auto heldItem = g_engine->player().heldItem();
@@ -135,11 +133,17 @@ CursorType Door::cursorType() const {
 	if (fromObject != CursorType::Point)
 		return fromObject;
 	switch (_interactionDirection) {
-	case Direction::Up: return CursorType::LeaveUp;
-	case Direction::Right: return CursorType::LeaveRight;
-	case Direction::Down: return CursorType::LeaveDown;
-	case Direction::Left: return CursorType::LeaveLeft;
-	default: assert(false && "Invalid door character direction"); return fromObject;
+	case Direction::Up:
+		return CursorType::LeaveUp;
+	case Direction::Right:
+		return CursorType::LeaveRight;
+	case Direction::Down:
+		return CursorType::LeaveDown;
+	case Direction::Left:
+		return CursorType::LeaveLeft;
+	default:
+		assert(false && "Invalid door character direction");
+		return fromObject;
 	}
 }
 
@@ -182,16 +186,14 @@ void Character::update() {
 	if (animateGraphic != nullptr) {
 		animateGraphic->topLeft() = Point(0, 0);
 		animateGraphic->update();
-	}
-	else if (_isTalking)
+	} else if (_isTalking)
 		updateTalkingAnimation();
 	else if (g_engine->world().somebodyUsing(this)) {
 		Graphic *talkGraphic = graphicOf(_curTalkingObject, &_graphicTalking);
 		talkGraphic->start(true);
 		talkGraphic->pause();
 		talkGraphic->update();
-	}
-	else
+	} else
 		_graphicNormal.update();
 }
 
@@ -289,8 +291,7 @@ struct SayTextTask final : public Task {
 	SayTextTask(Process &process, Character *character, int32 dialogId)
 		: Task(process)
 		, _character(character)
-		, _dialogId(dialogId) {
-	}
+		, _dialogId(dialogId) {}
 
 	SayTextTask(Process &process, Serializer &s)
 		: Task(process) {
@@ -306,9 +307,8 @@ struct SayTextTask final : public Task {
 		while (true) {
 			g_engine->player().addLastDialogCharacter(_character);
 
-			if (_soundHandle == SoundHandle {})
-			{
-				bool hasMortadeloVoice = g_engine->game().hasMortadeloVoice(_character);					
+			if (_soundHandle == SoundHandle {}) {
+				bool hasMortadeloVoice = g_engine->game().hasMortadeloVoice(_character);
 				_soundHandle = g_engine->sounds().playVoice(
 					String::format(hasMortadeloVoice ? "M%04d" : "%04d", _dialogId),
 					0);
@@ -404,8 +404,7 @@ struct AnimateCharacterTask final : public Task {
 		_graphic->start(false);
 		if (_character->room() == g_engine->player().currentRoom())
 			_graphic->update();
-		do
-		{
+		do {
 			TASK_YIELD(2);
 			if (process().isActiveForPlayer() && g_engine->input().wasAnyMouseReleased())
 				_graphic->pause();
@@ -447,8 +446,7 @@ struct LerpLodBiasTask final : public Task {
 		: Task(process)
 		, _character(character)
 		, _targetLodBias(targetLodBias)
-		, _durationMs(durationMs) {
-	}
+		, _durationMs(durationMs) {}
 
 	LerpLodBiasTask(Process &process, Serializer &s)
 		: Task(process) {
@@ -575,8 +573,7 @@ static Direction getDirection(Point from, Point to) {
 		return slope > 1000 ? Direction::Up
 			: slope < -1000 ? Direction::Down
 			: Direction::Right;
-	}
-	else { // from.x > to.x
+	} else { // from.x > to.x
 		int slope = 1000 * delta.y / delta.x;
 		return slope > 1000 ? Direction::Up
 			: slope < -1000 ? Direction::Down
@@ -597,16 +594,14 @@ void WalkingCharacter::updateWalking() {
 	if (_sourcePos == targetPos) {
 		_currentPos = targetPos;
 		_pathPoints.pop();
-	}
-	else {
+	} else {
 		updateWalkingAnimation();
 		const int32 distanceToTarget = (int32)(sqrtf(_sourcePos.sqrDist(targetPos)));
 		if (_walkedDistance < distanceToTarget) {
 			// separated because having only 16 bits and multiplications seems dangerous
 			_currentPos.x = _sourcePos.x + _walkedDistance * (targetPos.x - _sourcePos.x) / distanceToTarget;
 			_currentPos.y = _sourcePos.y + _walkedDistance * (targetPos.y - _sourcePos.y) / distanceToTarget;
-		}
-		else {
+		} else {
 			_sourcePos = _currentPos = targetPos;
 			_pathPoints.pop();
 			_walkedDistance = 1;
@@ -638,8 +633,7 @@ void WalkingCharacter::updateWalkingAnimation() {
 		_lastWalkAnimFrame = expectedFrame;
 		stepFrameFrom = 2 * expectedFrame - 2;
 		stepFrameTo = 2 * expectedFrame;
-	}
-	else {
+	} else {
 		const int32 frameThreshold = _lastWalkAnimFrame <= halfFrameCount - 1
 			? _lastWalkAnimFrame
 			: (_lastWalkAnimFrame - halfFrameCount + 1) % (halfFrameCount - 2) + 1;
@@ -648,8 +642,7 @@ void WalkingCharacter::updateWalkingAnimation() {
 		if (expectedFrame >= frameThreshold) {
 			stepFrameFrom = 2 * expectedFrame - 2;
 			stepFrameTo = 2 * expectedFrame;
-		}
-		else {
+		} else {
 			stepFrameFrom = 2 * (halfFrameCount - 2);
 			stepFrameTo = 2 * halfFrameCount - 2;
 		}
@@ -661,8 +654,7 @@ void WalkingCharacter::updateWalkingAnimation() {
 	_graphicNormal.frameI() = 2 * expectedFrame; // especially this: wtf?
 }
 
-void WalkingCharacter::onArrived() {
-}
+void WalkingCharacter::onArrived() {}
 
 void WalkingCharacter::stopWalking(Direction direction) {
 	// be careful, the original engine had two versions of this method
@@ -778,12 +770,11 @@ void WalkingCharacter::syncGame(Serializer &serializer) {
 struct ArriveTask : public Task {
 	ArriveTask(Process &process, const WalkingCharacter *character)
 		: Task(process)
-		, _character(character) {
-	}
+		, _character(character) {}
 
 	ArriveTask(Process &process, Serializer &s)
 		: Task(process) {
-		syncGame(s);		
+		syncGame(s);
 	}
 
 	TaskReturn run() override {
@@ -891,8 +882,7 @@ void MainCharacter::walkTo(
 		if (!otherCharacter->isBusy()) {
 			if (activeFloor != nullptr && activeFloor->findEvadeTarget(evadeTarget, activeDepthScale, avoidanceDistSqr, evadeTarget))
 				otherCharacter->WalkingCharacter::walkTo(evadeTarget);
-		}
-		else if (!willIBeBusy) {
+		} else if (!willIBeBusy) {
 			if (activeFloor != nullptr)
 				activeFloor->findEvadeTarget(evadeTarget, activeDepthScale, avoidanceDistSqr, target);
 		}
@@ -908,8 +898,7 @@ void MainCharacter::draw() {
 		if (_currentPos.y <= g_engine->world().filemon()._currentPos.y) {
 			g_engine->world().mortadelo().drawInner();
 			g_engine->world().filemon().drawInner();
-		}
-		else {
+		} else {
 			g_engine->world().filemon().drawInner();
 			g_engine->world().mortadelo().drawInner();
 		}
@@ -1040,8 +1029,7 @@ struct DialogMenuTask : public Task {
 	DialogMenuTask(Process &process, MainCharacter *character)
 		: Task(process)
 		, _input(g_engine->input())
-		, _character(character) {
-	}
+		, _character(character) {}
 
 	DialogMenuTask(Process &process, Serializer &s)
 		: Task(process)
@@ -1117,7 +1105,7 @@ private:
 				g_engine->globalUI().dialogFont(),
 				g_engine->world().getDialogLine(itLine._dialogId),
 				Point(kTextXOffset, itLine._yPosition),
-				maxTextWidth(), false, isHovered ? Color{ 255, 255, 128, 255 } : kWhite, -kForegroundOrderCount + 2);
+				maxTextWidth(), false, isHovered ? Color { 255, 255, 128, 255 } : kWhite, -kForegroundOrderCount + 2);
 			isSomethingHovered = isSomethingHovered || isHovered;
 			if (isHovered && _input.wasMouseLeftReleased())
 				return i - 1;
@@ -1169,8 +1157,7 @@ const char *FloorColor::typeName() const { return "FloorColor"; }
 
 FloorColor::FloorColor(Room *room, ReadStream &stream)
 	: ObjectBase(room, stream)
-	, _shape(stream) {
-}
+	, _shape(stream) {}
 
 void FloorColor::update() {
 	auto updateFor = [&] (MainCharacter &character) {
diff --git a/engines/alcachofa/game.cpp b/engines/alcachofa/game.cpp
index f7bc9bd7c8e..8ea1da03f07 100644
--- a/engines/alcachofa/game.cpp
+++ b/engines/alcachofa/game.cpp
@@ -33,8 +33,7 @@ Game::Game()
 #else // For release builds the game might still work or the user might still be able to save and restart
 	: _message(warning)
 #endif
-{
-}
+{}
 
 void Game::onLoadedGameFiles() {}
 
@@ -81,7 +80,7 @@ bool Game::shouldTriggerDoor(const Door *door) {
 	return true;
 }
 
-void Game::onUserChangedCharacter() { }
+void Game::onUserChangedCharacter() {}
 
 bool Game::hasMortadeloVoice(const Character *character) {
 	return character == &g_engine->world().mortadelo();
diff --git a/engines/alcachofa/general-objects.cpp b/engines/alcachofa/general-objects.cpp
index 997f550649e..fde931008eb 100644
--- a/engines/alcachofa/general-objects.cpp
+++ b/engines/alcachofa/general-objects.cpp
@@ -52,20 +52,15 @@ void ObjectBase::toggle(bool isEnabled) {
 	_isEnabled = isEnabled;
 }
 
-void ObjectBase::draw() {
-}
+void ObjectBase::draw() {}
 
-void ObjectBase::drawDebug() {
-}
+void ObjectBase::drawDebug() {}
 
-void ObjectBase::update() {
-}
+void ObjectBase::update() {}
 
-void ObjectBase::loadResources() {
-}
+void ObjectBase::loadResources() {}
 
-void ObjectBase::freeResources() {
-}
+void ObjectBase::freeResources() {}
 
 void ObjectBase::syncGame(Serializer &serializer) {
 	serializer.syncAsByte(_isEnabled);
@@ -99,8 +94,7 @@ GraphicObject::GraphicObject(Room *room, ReadStream &stream)
 GraphicObject::GraphicObject(Room *room, const char *name)
 	: ObjectBase(room, name)
 	, _type(GraphicObjectType::Normal)
-	, _posterizeAlpha(0) {
-}
+	, _posterizeAlpha(0) {}
 
 void GraphicObject::draw() {
 	if (!isEnabled() || !_graphic.hasAnimation())
@@ -127,8 +121,7 @@ void GraphicObject::drawDebug() {
 		topLeftTmp.z() = _graphic.scale();
 		_graphic.animation().outputRect3D(_graphic.frameI(), scale, topLeftTmp, size);
 		topLeft = as2D(topLeftTmp);
-	}
-	else
+	} else
 		_graphic.animation().outputRect2D(_graphic.frameI(), scale, topLeft, size);
 
 	Vector2d points[] = {
@@ -241,8 +234,7 @@ const char *ShapeObject::typeName() const { return "ShapeObject"; }
 ShapeObject::ShapeObject(Room *room, ReadStream &stream)
 	: ObjectBase(room, stream)
 	, _shape(stream)
-	, _cursorType((CursorType)stream.readSint32LE()) {
-}
+	, _cursorType((CursorType)stream.readSint32LE()) {}
 
 void ShapeObject::update() {
 	if (isEnabled())
@@ -272,8 +264,7 @@ void ShapeObject::onHoverStart() {
 	onHoverUpdate();
 }
 
-void ShapeObject::onHoverEnd() {
-}
+void ShapeObject::onHoverEnd() {}
 
 void ShapeObject::onHoverUpdate() {
 	g_engine->drawQueue().add<TextDrawRequest>(
@@ -299,13 +290,11 @@ void ShapeObject::updateSelection() {
 				onClick();
 			else
 				onHoverUpdate();
-		}
-		else {
+		} else {
 			_wasSelected = true;
 			onHoverStart();
 		}
-	}
-	else if (_wasSelected) {
+	} else if (_wasSelected) {
 		_wasSelected = false;
 		onHoverEnd();
 	}
diff --git a/engines/alcachofa/global-ui.cpp b/engines/alcachofa/global-ui.cpp
index 0d5d1004b23..938fefd8fc1 100644
--- a/engines/alcachofa/global-ui.cpp
+++ b/engines/alcachofa/global-ui.cpp
@@ -91,14 +91,12 @@ bool GlobalUI::updateOpeningInventory() {
 		if (deltaTime >= 1000) {
 			_isOpeningInventory = false;
 			g_engine->world().inventory().open();
-		}
-		else {
+		} else {
 			deltaTime = MIN<uint32>(300, deltaTime);
 			g_engine->world().inventory().drawAsOverlay((int32)(g_system->getHeight() * (deltaTime * kSpeed - 1)));
 		}
 		return true;
-	}
-	else if (userWantsToOpenInventory) {
+	} else if (userWantsToOpenInventory) {
 		_isClosingInventory = false;
 		_isOpeningInventory = true;
 		_timeForInventory = g_engine->getMillis();
@@ -136,8 +134,7 @@ bool GlobalUI::updateChangingCharacter() {
 
 	if (!isHoveringChangeButton())
 		return false;
-	if (g_engine->input().wasMouseLeftPressed())
-	{
+	if (g_engine->input().wasMouseLeftPressed()) {
 		player.pressedObject() = &_changeButton;
 		return true;
 	}
@@ -174,8 +171,7 @@ void GlobalUI::drawChangingButton() {
 		return;
 
 	auto anim = activeAnimation();
-	if (!_changeButton.hasAnimation() || &_changeButton.animation() != anim)
-	{
+	if (!_changeButton.hasAnimation() || &_changeButton.animation() != anim) {
 		_changeButton.setAnimation(anim);
 		_changeButton.pause();
 		_changeButton.lastTime() = 42 * (anim->frameCount() - 1) + 1;
@@ -184,8 +180,7 @@ void GlobalUI::drawChangingButton() {
 	_changeButton.topLeft() = { (int16)(g_system->getWidth() + 2), -2 };
 	if (isHoveringChangeButton() &&
 		g_engine->input().isMouseLeftDown() &&
-		player.pressedObject() == &_changeButton)
-	{
+		player.pressedObject() == &_changeButton) {
 		_changeButton.topLeft().x -= 2;
 		_changeButton.topLeft().y += 2;
 	}
@@ -199,8 +194,7 @@ struct CenterBottomTextTask : public Task {
 	CenterBottomTextTask(Process &process, int32 dialogId, uint32 durationMs)
 		: Task(process)
 		, _dialogId(dialogId)
-		, _durationMs(durationMs) {
-	}
+		, _durationMs(durationMs) {}
 
 	CenterBottomTextTask(Process &process, Serializer &s)
 		: Task(process) {
@@ -239,7 +233,7 @@ struct CenterBottomTextTask : public Task {
 		s.syncAsSint32LE(_dialogId);
 		s.syncAsUint32LE(_startTime);
 		s.syncAsUint32LE(_durationMs);
-	} 
+	}
 
 	const char *taskName() const override;
 
diff --git a/engines/alcachofa/graphics-opengl.cpp b/engines/alcachofa/graphics-opengl.cpp
index bc27347e6d4..e9881b189c7 100644
--- a/engines/alcachofa/graphics-opengl.cpp
+++ b/engines/alcachofa/graphics-opengl.cpp
@@ -177,8 +177,7 @@ public:
 			GL_CALL(glDisable(GL_TEXTURE_2D));
 			GL_CALL(glDisableClientState(GL_TEXTURE_COORD_ARRAY));
 			_currentTexture = nullptr;
-		}
-		else {
+		} else {
 			if (_currentTexture == nullptr) {
 				GL_CALL(glEnable(GL_TEXTURE_2D));
 				GL_CALL(glEnableClientState(GL_TEXTURE_COORD_ARRAY));
@@ -253,7 +252,9 @@ public:
 			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_PRIMARY_COLOR));
 			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR)); // we have to pre-multiply
 			break;
-		default: assert(false && "Invalid blend mode"); break;
+		default:
+			assert(false && "Invalid blend mode");
+			break;
 		}
 		_currentBlendMode = blendMode;
 	}
@@ -327,8 +328,7 @@ public:
 		}
 
 		float colors[] = { color.r / 255.0f, color.g / 255.0f, color.b / 255.0f, color.a / 255.0f };
-		if (_currentBlendMode == BlendMode::Tinted)
-		{
+		if (_currentBlendMode == BlendMode::Tinted) {
 			colors[0] *= colors[3];
 			colors[1] *= colors[3];
 			colors[2] *= colors[3];
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 40b5375797f..593b617bf57 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -57,8 +57,7 @@ void IDebugRenderer::debugShape(const Shape &shape, Color color) {
 
 AnimationBase::AnimationBase(String fileName, AnimationFolder folder)
 	: _fileName(move(fileName))
-	, _folder(folder) {
-}
+	, _folder(folder) {}
 
 AnimationBase::~AnimationBase() {
 	freeImages();
@@ -70,10 +69,18 @@ void AnimationBase::load() {
 
 	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");
+	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 (_fileName.size() < 4 || scumm_strnicmp(_fileName.end() - 4, ".AN0", 4) != 0)
 		_fileName += ".AN0";
@@ -236,8 +243,7 @@ Point AnimationBase::imageSize(int32 imageI) const {
 }
 
 Animation::Animation(String fileName, AnimationFolder folder)
-	: AnimationBase(fileName, folder) {
-}
+	: AnimationBase(fileName, folder) {}
 
 void Animation::load() {
 	if (_isLoaded)
@@ -492,8 +498,7 @@ void Font::drawCharacter(int32 imageI, Point centerPoint, Color color) {
 	renderer.quad(center, size, color, Angle(), _texMins[imageI], _texMaxs[imageI]);
 }
 
-Graphic::Graphic() {
-}
+Graphic::Graphic() {}
 
 Graphic::Graphic(ReadStream &stream) {
 	_topLeft.x = stream.readSint16LE();
@@ -515,8 +520,7 @@ Graphic::Graphic(const Graphic &other)
 	, _isLooping(other._isLooping)
 	, _lastTime(other._lastTime)
 	, _frameI(other._frameI)
-	, _depthScale(other._depthScale) {
-}
+	, _depthScale(other._depthScale) {}
 
 void Graphic::loadResources() {
 	if (_animation != nullptr)
@@ -593,8 +597,7 @@ static int8 shiftAndClampOrder(int8 order) {
 }
 
 IDrawRequest::IDrawRequest(int8 order)
-	: _order(shiftAndClampOrder(order)) {
-}
+	: _order(shiftAndClampOrder(order)) {}
 
 AnimationDrawRequest::AnimationDrawRequest(Graphic &graphic, bool is3D, BlendMode blendMode, float lodBias)
 	: IDrawRequest(graphic._order)
@@ -678,7 +681,7 @@ TextDrawRequest::TextDrawRequest(Font &font, const char *originalText, Point pos
 	// allocate on drawQueue to prevent having destruct it
 	assert(originalText != nullptr);
 	auto textLen = strlen(originalText);
-	char *text = (char*)g_engine->drawQueue().allocator().allocateRaw(textLen + 1, 1);
+	char *text = (char *)g_engine->drawQueue().allocator().allocateRaw(textLen + 1, 1);
 	memcpy(text, originalText, textLen + 1);
 
 	// split into trimmed lines
@@ -705,8 +708,7 @@ TextDrawRequest::TextDrawRequest(Font &font, const char *originalText, Point pos
 			itChar = trimTrailing(itChar, itLine, true) + 1;
 			itLine = trimLeading(itLine, itChar);
 			_allLines[lineCount] = TextLine(itLine, itChar - itLine);
-		}
-		else
+		} else
 			_allLines[lineCount] = TextLine(itLine, itChar - itLine);
 		itChar = trimLeading(itChar, textEnd);
 		lineCount++;
@@ -739,8 +741,7 @@ TextDrawRequest::TextDrawRequest(Font &font, const char *originalText, Point pos
 			pos.x = screenW - _width / 2 - 1;
 		for (auto &linePosX : _posX)
 			linePosX = pos.x - linePosX / 2;
-	}
-	else
+	} else
 		fill(_posX.begin(), _posX.end(), pos.x);
 
 	// setup height and y position
@@ -772,8 +773,7 @@ void TextDrawRequest::draw() {
 FadeDrawRequest::FadeDrawRequest(FadeType type, float value, int8 order)
 	: IDrawRequest(order)
 	, _type(type)
-	, _value(value) {
-}
+	, _value(value) {}
 
 void FadeDrawRequest::draw() {
 	Color color;
@@ -808,8 +808,7 @@ struct FadeTask : public Task {
 		, _duration(duration)
 		, _easingType(easingType)
 		, _order(order)
-		, _permanentFadeAction(permanentFadeAction) {
-	}
+		, _permanentFadeAction(permanentFadeAction) {}
 
 	FadeTask(Process &process, Serializer &s)
 		: Task(process) {
@@ -881,8 +880,7 @@ Task *fade(Process &process, FadeType fadeType,
 BorderDrawRequest::BorderDrawRequest(Rect rect, Color color)
 	: IDrawRequest(-kForegroundOrderCount)
 	, _rect(rect)
-	, _color(color) {
-}
+	, _color(color) {}
 
 void BorderDrawRequest::draw() {
 	auto &renderer = g_engine->renderer();
diff --git a/engines/alcachofa/input.cpp b/engines/alcachofa/input.cpp
index 56ad040d213..bacbd725e26 100644
--- a/engines/alcachofa/input.cpp
+++ b/engines/alcachofa/input.cpp
@@ -41,8 +41,7 @@ void Input::nextFrame() {
 }
 
 bool Input::handleEvent(const Common::Event &event) {
-	if (_debugInput != nullptr)
-	{
+	if (_debugInput != nullptr) {
 		auto result = _debugInput->handleEvent(event);
 		_mousePos2D = _debugInput->mousePos2D(); // even for debug input we want to e.g. draw a cursor
 		_mousePos3D = _debugInput->mousePos3D();
diff --git a/engines/alcachofa/menu.cpp b/engines/alcachofa/menu.cpp
index 3a23778f670..d9ebcb78606 100644
--- a/engines/alcachofa/menu.cpp
+++ b/engines/alcachofa/menu.cpp
@@ -32,7 +32,7 @@ using namespace Common;
 using namespace Graphics;
 
 namespace Alcachofa {
-	
+
 static void createThumbnail(ManagedSurface &surface) {
 	surface.create(kBigThumbnailWidth, kBigThumbnailHeight, PixelFormat::createFormatRGBA32());
 }
@@ -121,15 +121,13 @@ void Menu::updateSelectedSavefile(bool hasJustSaved) {
 	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 (isOldSavefile) {
 		if (!tryReadOldSavefile()) {
 			_selectedSavefileDescription = String::format("Savestate %d",
 				parseSavestateSlot(_savefiles[_selectedSavefileI]));
 			createThumbnail(_selectedThumbnail);
 		}
-	}
-	else {
+	} else {
 		// the unsaved gamestate is shown as grayscale
 		_selectedThumbnail.copyFrom(_bigThumbnail);
 		convertToGrayscale(_selectedThumbnail);
@@ -240,8 +238,7 @@ void Menu::triggerSave() {
 	String fileName;
 	if (_selectedSavefileI < _savefiles.size()) {
 		fileName = _savefiles[_selectedSavefileI]; // overwrite a previous save
-	}
-	else {
+	} else {
 		// for a new savefile we figure out the next slot index
 		int nextSlot = _savefiles.empty()
 			? 1 // start at one to keep autosave alone
@@ -260,8 +257,7 @@ void Menu::triggerSave() {
 		g_engine->getMetaEngine()->appendExtendedSave(savefile.get(), g_engine->getTotalPlayTime(), _selectedSavefileDescription, false);
 		_savefiles.push_back(fileName);
 		updateSelectedSavefile(true);
-	}
-	else {
+	} else {
 		GUI::MessageDialog dialog(error.getTranslatedDesc());
 		dialog.runModal();
 	}
diff --git a/engines/alcachofa/metaengine.h b/engines/alcachofa/metaengine.h
index b79dd84a250..ada41267045 100644
--- a/engines/alcachofa/metaengine.h
+++ b/engines/alcachofa/metaengine.h
@@ -53,7 +53,7 @@ public:
 
 	void getSavegameThumbnail(Graphics::Surface &thumb) override;
 
-	
+
 };
 
 #endif // ALCACHOFA_METAENGINE_H
diff --git a/engines/alcachofa/player.cpp b/engines/alcachofa/player.cpp
index 8360d51bab2..13757fd92e8 100644
--- a/engines/alcachofa/player.cpp
+++ b/engines/alcachofa/player.cpp
@@ -58,20 +58,31 @@ void Player::updateCursor() {
 	else {
 		auto type = _selectedObject->cursorType();
 		switch (type) {
-		case CursorType::LeaveUp: _cursorFrameI = 8; break;
-		case CursorType::LeaveRight: _cursorFrameI = 10; break;
-		case CursorType::LeaveDown: _cursorFrameI = 12; break;
-		case CursorType::LeaveLeft: _cursorFrameI = 14; break;
-		case CursorType::WalkTo: _cursorFrameI = 6; break;
+		case CursorType::LeaveUp:
+			_cursorFrameI = 8;
+			break;
+		case CursorType::LeaveRight:
+			_cursorFrameI = 10;
+			break;
+		case CursorType::LeaveDown:
+			_cursorFrameI = 12;
+			break;
+		case CursorType::LeaveLeft:
+			_cursorFrameI = 14;
+			break;
+		case CursorType::WalkTo:
+			_cursorFrameI = 6;
+			break;
 		case CursorType::Point:
-		default: _cursorFrameI = 0; break;
+		default:
+			_cursorFrameI = 0;
+			break;
 		}
 
 		if (_cursorFrameI != 0) {
 			if (g_engine->input().isAnyMouseDown() && _pressedObject == _selectedObject)
 				_cursorFrameI++;
-		}
-		else if (g_engine->input().isMouseLeftDown())
+		} else if (g_engine->input().isMouseLeftDown())
 			_cursorFrameI = 2;
 		else if (g_engine->input().isMouseRightDown())
 			_cursorFrameI = 4;
@@ -84,8 +95,7 @@ void Player::drawCursor(bool forceDefaultCursor) {
 		if (forceDefaultCursor)
 			_cursorFrameI = 0;
 		g_engine->drawQueue().add<AnimationDrawRequest>(_cursorAnimation.get(), _cursorFrameI, as2D(cursorPos), -10);
-	}
-	else {
+	} else {
 		auto itemGraphic = _heldItem->graphic();
 		assert(itemGraphic != nullptr);
 		auto &animation = itemGraphic->animation();
@@ -156,10 +166,15 @@ MainCharacter *Player::inactiveCharacter() const {
 FakeSemaphore &Player::semaphoreFor(MainCharacterKind kind) {
 	static FakeSemaphore dummySemaphore("dummy");
 	switch (kind) {
-	case MainCharacterKind::None: return _semaphore;
-	case MainCharacterKind::Mortadelo: return g_engine->world().mortadelo().semaphore();
-	case MainCharacterKind::Filemon: return g_engine->world().filemon().semaphore();
-	default: assert(false && "Invalid main character kind"); return dummySemaphore;
+	case MainCharacterKind::None:
+		return _semaphore;
+	case MainCharacterKind::Mortadelo:
+		return g_engine->world().mortadelo().semaphore();
+	case MainCharacterKind::Filemon:
+		return g_engine->world().filemon().semaphore();
+	default:
+		assert(false && "Invalid main character kind");
+		return dummySemaphore;
 	}
 }
 
@@ -172,8 +187,7 @@ void Player::triggerObject(ObjectBase *object, const char *action) {
 	if (strcmp(action, "MIRAR") == 0 || inactiveCharacter()->currentlyUsing() == object) {
 		action = "MIRAR";
 		_activeCharacter->currentlyUsing() = nullptr;
-	}
-	else
+	} else
 		_activeCharacter->currentlyUsing() = object;
 
 	auto &script = g_engine->script();
@@ -254,7 +268,7 @@ struct DoorTask : public Task {
 		s.syncAsByte(hasMusicLock);
 		if (s.isLoading() && hasMusicLock)
 			_musicLock = FakeLock("door-music", g_engine->sounds().musicSemaphore());
-		
+
 		_lock = FakeLock("door", _character->semaphore());
 		findTarget();
 	}
@@ -347,7 +361,7 @@ void Player::syncGame(Serializer &s) {
 	}
 
 	FakeSemaphore::sync(s, _semaphore);
-	
+
 	String roomName;
 	if (s.isSaving()) {
 		roomName =
diff --git a/engines/alcachofa/rooms.cpp b/engines/alcachofa/rooms.cpp
index 0d162b797cc..3764a82664e 100644
--- a/engines/alcachofa/rooms.cpp
+++ b/engines/alcachofa/rooms.cpp
@@ -31,8 +31,7 @@ using namespace Common;
 
 namespace Alcachofa {
 
-Room::Room(World *world, SeekableReadStream &stream) : Room(world, stream, false) {
-}
+Room::Room(World *world, SeekableReadStream &stream) : Room(world, stream, false) {}
 
 static ObjectBase *readRoomObject(Room *room, const String &type, ReadStream &stream) {
 	if (type == ObjectBase::kClassName)
@@ -100,19 +99,16 @@ Room::Room(World *world, SeekableReadStream &stream, bool hasUselessByte)
 		stream.readByte();
 
 	uint32 objectEnd = stream.readUint32LE();
-	while (objectEnd > 0)
-	{
+	while (objectEnd > 0) {
 		const auto type = readVarString(stream);
 		auto object = readRoomObject(this, type, stream);
 		if (object == nullptr) {
 			g_engine->game().unknownRoomObject(type);
 			stream.seek(objectEnd, SEEK_SET);
-		}
-		else if (stream.pos() < objectEnd) {
+		} else if (stream.pos() < objectEnd) {
 			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
+		} 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());
 
 		if (object != nullptr)
@@ -216,8 +212,7 @@ void Room::updateInteraction() {
 			player.activeCharacter()->walkToMouse();
 			g_engine->camera().setFollow(player.activeCharacter());
 		}
-	}
-	else {
+	} else {
 		player.selectedObject()->markSelected();
 		if (input.wasAnyMousePressed())
 			player.pressedObject() = player.selectedObject();
@@ -270,11 +265,9 @@ void Room::drawDebug() {
 
 	if (g_engine->console().showFloorEdges()) {
 		auto &camera = g_engine->camera();
-		for (uint polygonI = 0; polygonI < floor.polygonCount(); polygonI++)
-		{
+		for (uint polygonI = 0; polygonI < floor.polygonCount(); polygonI++) {
 			auto polygon = floor.at(polygonI);
-			for (uint pointI = 0; pointI < polygon._points.size(); pointI++)
-			{
+			for (uint pointI = 0; pointI < polygon._points.size(); pointI++) {
 				int32 targetI = floor.edgeTarget(polygonI, pointI);
 				if (targetI < 0)
 					continue;
@@ -328,8 +321,7 @@ ShapeObject *Room::getSelectedObject(ShapeObject *best) const {
 }
 
 OptionsMenu::OptionsMenu(World *world, SeekableReadStream &stream)
-	: Room(world, stream, true) {
-}
+	: Room(world, stream, true) {}
 
 bool OptionsMenu::updateInput() {
 	if (!Room::updateInput())
@@ -344,8 +336,7 @@ bool OptionsMenu::updateInput() {
 		}
 
 		_lastSelectedObject->markSelected();
-	}
-	else
+	} else
 		_lastSelectedObject = currentSelectedObject;
 	if (_idleArm != nullptr)
 		_idleArm->toggle(false);
@@ -364,16 +355,13 @@ void OptionsMenu::clearLastSelectedObject() {
 }
 
 ConnectMenu::ConnectMenu(World *world, SeekableReadStream &stream)
-	: Room(world, stream, true) {
-}
+	: Room(world, stream, true) {}
 
 ListenMenu::ListenMenu(World *world, SeekableReadStream &stream)
-	: Room(world, stream, true) {
-}
+	: Room(world, stream, true) {}
 
 Inventory::Inventory(World *world, SeekableReadStream &stream)
-	: Room(world, stream, true) {
-}
+	: Room(world, stream, true) {}
 
 Inventory::~Inventory() {
 	// No need to delete items, they are room objects and thus deleted in Room::~Room
@@ -545,8 +533,10 @@ World::~World() {
 
 MainCharacter &World::getMainCharacterByKind(MainCharacterKind kind) const {
 	switch (kind) {
-	case MainCharacterKind::Mortadelo: return *_mortadelo;
-	case MainCharacterKind::Filemon: return *_filemon;
+	case MainCharacterKind::Mortadelo:
+		return *_mortadelo;
+	case MainCharacterKind::Filemon:
+		return *_filemon;
 	default:
 		error("Invalid character kind given to getMainCharacterByKind");
 	}
@@ -554,8 +544,10 @@ MainCharacter &World::getMainCharacterByKind(MainCharacterKind kind) const {
 
 MainCharacter &World::getOtherMainCharacterByKind(MainCharacterKind kind) const {
 	switch (kind) {
-	case MainCharacterKind::Mortadelo: return *_filemon;
-	case MainCharacterKind::Filemon: return *_mortadelo;
+	case MainCharacterKind::Mortadelo:
+		return *_filemon;
+	case MainCharacterKind::Filemon:
+		return *_mortadelo;
 	default:
 		error("Invalid character kind given to getOtherMainCharacterByKind");
 	}
@@ -691,8 +683,7 @@ bool World::loadWorldFile(const char *path) {
 		if (file.pos() < roomEnd) {
 			g_engine->game().notEnoughRoomDataRead(path, file.pos(), roomEnd);
 			file.seek(roomEnd, SEEK_SET);
-		}
-		else if (file.pos() > roomEnd) // this surely is not recoverable
+		} else if (file.pos() > roomEnd) // this surely is not recoverable
 			error("Read past the room data for world %s", path);
 		roomEnd = file.readUint32LE();
 	}
@@ -770,7 +761,7 @@ void World::loadDialogLines() {
 	 * Name 123, "This is the dialog line\r\n
 	 *     Name     123   This is the dialog line    \r\n
 	 *
-	 * - The ID does not have to be correct, it is ignored by the original engine. 
+	 * - 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);
diff --git a/engines/alcachofa/scheduler.cpp b/engines/alcachofa/scheduler.cpp
index fbc83ea14df..d58cb44f681 100644
--- a/engines/alcachofa/scheduler.cpp
+++ b/engines/alcachofa/scheduler.cpp
@@ -87,8 +87,7 @@ void Task::errorForUnexpectedObjectType(const ObjectBase *base) const {
 
 DelayTask::DelayTask(Process &process, uint32 millis)
 	: Task(process)
-	, _endTime(millis) {
-}
+	, _endTime(millis) {}
 
 DelayTask::DelayTask(Process &process, Serializer &s)
 	: Task(process) {
@@ -118,8 +117,7 @@ DECLARE_TASK(DelayTask)
 Process::Process(ProcessId pid, MainCharacterKind characterKind)
 	: _pid(pid)
 	, _character(characterKind)
-	, _name("Unnamed process") {
-}
+	, _name("Unnamed process") {}
 
 Process::Process(Serializer &s) {
 	syncGame(s);
@@ -138,7 +136,8 @@ TaskReturnType Process::run() {
 	while (!_tasks.empty()) {
 		TaskReturn ret = _tasks.top()->run();
 		switch (ret.type()) {
-		case TaskReturnType::Yield: return TaskReturnType::Yield;
+		case TaskReturnType::Yield:
+			return TaskReturnType::Yield;
 		case TaskReturnType::Waiting:
 			_tasks.push(ret.taskToWaitFor());
 			break;
@@ -158,10 +157,18 @@ void Process::debugPrint() {
 	auto *debugger = g_engine->getDebugger();
 	const char *characterName;
 	switch (_character) {
-	case MainCharacterKind::None: characterName = "    <none>"; break;
-	case MainCharacterKind::Filemon: characterName = " Filemon"; break;
-	case MainCharacterKind::Mortadelo: characterName = "Mortadelo"; break;
-	default: characterName = "<invalid>"; break;
+	case MainCharacterKind::None:
+		characterName = "    <none>";
+		break;
+	case MainCharacterKind::Filemon:
+		characterName = " Filemon";
+		break;
+	case MainCharacterKind::Mortadelo:
+		characterName = "Mortadelo";
+		break;
+	default:
+		characterName = "<invalid>";
+		break;
 	}
 	debugger->debugPrintf("pid: %3u char: %s ret: %2d \"%s\"\n", _pid, characterName, _lastReturnValue, _name.c_str());
 
@@ -200,8 +207,7 @@ void Process::syncGame(Serializer &s) {
 		assert(_tasks.empty());
 		for (uint i = 0; i < count; i++)
 			_tasks.push(readTask(*this, s));
-	}
-	else {
+	} else {
 		String taskName;
 		for (uint i = 0; i < count; i++) {
 			taskName = _tasks[i]->taskName();
@@ -363,8 +369,7 @@ void Scheduler::syncGame(Serializer &s) {
 		processes->reserve(count);
 		for (uint32 i = 0; i < count; i++)
 			processes->push_back(new Process(s));
-	}
-	else {
+	} else {
 		for (Process *process : *processes)
 			process->syncGame(s);
 	}
diff --git a/engines/alcachofa/scheduler.h b/engines/alcachofa/scheduler.h
index 1f9c79b34f0..50c685c5a65 100644
--- a/engines/alcachofa/scheduler.h
+++ b/engines/alcachofa/scheduler.h
@@ -101,7 +101,7 @@ protected:
 		// or we could just use const_cast and promise that we won't modify the object itself
 		ObjectBase *base = const_cast<Common::remove_const_t<TObject> *>(object);
 		syncObjectAsString(s, base, optional);
-		object = dynamic_cast<TObject*>(base);
+		object = dynamic_cast<TObject *>(base);
 		if (object == nullptr && base != nullptr)
 			errorForUnexpectedObjectType(base);
 	}
diff --git a/engines/alcachofa/script.cpp b/engines/alcachofa/script.cpp
index 64b6437d85e..9d505bd0217 100644
--- a/engines/alcachofa/script.cpp
+++ b/engines/alcachofa/script.cpp
@@ -41,8 +41,7 @@ enum ScriptDebugLevel {
 
 ScriptInstruction::ScriptInstruction(ReadStream &stream)
 	: _op((ScriptOp)stream.readSint32LE())
-	, _arg(stream.readSint32LE()) {
-}
+	, _arg(stream.readSint32LE()) {}
 
 Script::Script() {
 	File file;
@@ -134,8 +133,7 @@ bool Script::hasProcedure(const Common::String &procedure) const {
 struct ScriptTimerTask : public Task {
 	ScriptTimerTask(Process &process, int32 durationSec)
 		: Task(process)
-		, _durationSec(durationSec) {
-	}
+		, _durationSec(durationSec) {}
 
 	ScriptTimerTask(Process &process, Serializer &s)
 		: Task(process) {
@@ -252,11 +250,21 @@ struct ScriptTask : public Task {
 				else {
 					const auto &top = _stack.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;
+					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;
 					}
 				}
 			}
@@ -298,8 +306,7 @@ struct ScriptTask : public Task {
 				if (kernelReturn.type() == TaskReturnType::Waiting) {
 					_returnsFromKernelCall = true;
 					return kernelReturn;
-				}
-				else
+				} else
 					handleReturnFromKernelCall(kernelReturn.returnValue());
 			}break;
 			case ScriptOp::JumpIfFalse:
@@ -363,6 +370,7 @@ struct ScriptTask : public Task {
 			}break;
 			default:
 				g_engine->game().unknownInstruction(instruction);
+				break;
 			}
 		}
 	}
@@ -384,8 +392,7 @@ struct ScriptTask : public Task {
 		if (s.isLoading()) {
 			for (uint i = 0; i < count; i++)
 				_stack.push(StackEntry(s));
-		}
-		else {
+		} else {
 			for (uint i = 0; i < count; i++)
 				_stack[i].syncGame(s);
 		}
@@ -600,8 +607,7 @@ private:
 			if (scumm_stricmp(getStringArg(0), "SALIR") == 0) {
 				g_engine->quitGame();
 				g_engine->player().changeRoom("SALIR", true);
-			}
-			else if (scumm_stricmp(getStringArg(0), "MENUPRINCIPALINICIO") == 0)
+			} else if (scumm_stricmp(getStringArg(0), "MENUPRINCIPALINICIO") == 0)
 				warning("STUB: change room to MenuPrincipalInicio special case");
 			else {
 				auto targetRoom = g_engine->world().getRoomByName(getStringArg(0));
@@ -624,8 +630,7 @@ private:
 			if (process().character() == MainCharacterKind::None) {
 				if (g_engine->player().currentRoom() != nullptr)
 					g_engine->player().currentRoom()->toggleActiveFloor();
-			}
-			else
+			} else
 				g_engine->world().getMainCharacterByKind(process().character()).room()->toggleActiveFloor();
 			return TaskReturn::finish(1);
 
@@ -646,8 +651,7 @@ private:
 				graphicObject->toggle(true);
 				graphicObject->graphic()->start(false);
 				return TaskReturn::finish(1);
-			}
-			else
+			} else
 				return TaskReturn::waitFor(graphicObject->animate(process()));
 		}
 
@@ -721,12 +725,10 @@ private:
 			}
 			float targetLodBias = getNumberArg(1) * 0.01f;
 			int32 durationMs = getNumberArg(2);
-			if (durationMs <= 0)
-			{
+			if (durationMs <= 0) {
 				character->lodBias() = targetLodBias;
 				return TaskReturn::finish(1);
-			}
-			else
+			} else
 				return TaskReturn::waitFor(character->lerpLodBias(process(), targetLodBias, durationMs));
 		}
 		case ScriptKernelTask::AnimateCharacter: {
@@ -749,8 +751,7 @@ private:
 				return TaskReturn::finish(1);
 			}
 			ObjectBase *talkObject = getObjectArg(1);
-			if (talkObject == nullptr && *getStringArg(1) != '\0')
-			{
+			if (talkObject == nullptr && *getStringArg(1) != '\0') {
 				g_engine->game().unknownAnimateTalkingObject(getStringArg(1));
 				return TaskReturn::finish(1);
 			}
@@ -798,13 +799,19 @@ private:
 		}
 		case ScriptKernelTask::ClearInventory:
 			switch ((MainCharacterKind)getNumberArg(0)) {
-			case MainCharacterKind::Mortadelo: g_engine->world().mortadelo().clearInventory(); break;
-			case MainCharacterKind::Filemon: g_engine->world().filemon().clearInventory(); break;
-			default: g_engine->game().unknownClearInventoryTarget(getNumberArg(0)); break;
+			case MainCharacterKind::Mortadelo:
+				g_engine->world().mortadelo().clearInventory();
+				break;
+			case MainCharacterKind::Filemon:
+				g_engine->world().filemon().clearInventory();
+				break;
+			default:
+				g_engine->game().unknownClearInventoryTarget(getNumberArg(0));
+				break;
 			}
 			return TaskReturn::finish(1);
 
-			// Camera tasks
+		// Camera tasks
 		case ScriptKernelTask::WaitCamStopping:
 			return TaskReturn::waitFor(g_engine->camera().waitToStop(process()));
 		case ScriptKernelTask::CamFollow:
@@ -980,8 +987,7 @@ void Script::updateCommonVariables() {
 	if (variable("CalcularTiempoSinPulsarRaton")) {
 		if (_scriptTimer == 0)
 			_scriptTimer = g_engine->getMillis();
-	}
-	else
+	} else
 		_scriptTimer = 0;
 
 	variable("EstanAmbos") = g_engine->world().mortadelo().room() == g_engine->world().filemon().room();
diff --git a/engines/alcachofa/shape.cpp b/engines/alcachofa/shape.cpp
index 0459a6bfa99..8d52dd036dd 100644
--- a/engines/alcachofa/shape.cpp
+++ b/engines/alcachofa/shape.cpp
@@ -31,7 +31,7 @@ static int sideOfLine(Point a, Point b, Point q) {
 }
 
 static bool lineIntersects(Point a1, Point b1, Point a2, Point b2) {
-	return (sideOfLine(a1, b1, a2) > 0) != (sideOfLine(a1, b1, b2) > 0); 
+	return (sideOfLine(a1, b1, a2) > 0) != (sideOfLine(a1, b1, b2) > 0);
 }
 
 static bool segmentsIntersect(Point a1, Point b1, Point a2, Point b2) {
@@ -43,8 +43,7 @@ static bool segmentsIntersect(Point a1, Point b1, Point a2, Point b2) {
 			return lineIntersects(b1, a1, b2, a2) && lineIntersects(b2, a2, b1, a1);
 		else
 			return lineIntersects(a1, b1, b2, a2) && lineIntersects(b2, a2, a1, b1);
-	}
-	else {
+	} else {
 		if (a1.x > b1.x)
 			return lineIntersects(b1, a1, a2, b2) && lineIntersects(a2, b2, b1, a1);
 		else
@@ -67,9 +66,12 @@ EdgeDistances::EdgeDistances(Point edgeA, Point edgeB, Point query) {
 
 bool Polygon::contains(Point query) const {
 	switch (_points.size()) {
-	case 0: return false;
-	case 1: return query == _points[0];
-	case 2: return edgeDistances(0, query)._toEdge < 2.0f;
+	case 0:
+		return false;
+	case 1:
+		return query == _points[0];
+	case 2:
+		return edgeDistances(0, query)._toEdge < 2.0f;
 	default:
 		// we assume that the polygon is convex
 		for (uint i = 1; i < _points.size(); i++) {
@@ -108,23 +110,18 @@ Point Polygon::closestPointTo(Point query, float &distanceSqr) const {
 	assert(_points.size() > 0);
 	Common::Point bestPoint = {};
 	distanceSqr = std::numeric_limits<float>::infinity();
-	for (uint i = 0; i < _points.size(); i++)
-	{
+	for (uint i = 0; i < _points.size(); i++) {
 		auto edgeDists = edgeDistances(i, query);
-		if (edgeDists._onEdge < 0.0f)
-		{
+		if (edgeDists._onEdge < 0.0f) {
 			float pointDistSqr = as2D(query - _points[i]).getSquareMagnitude();
-			if (pointDistSqr < distanceSqr)
-			{
+			if (pointDistSqr < distanceSqr) {
 				bestPoint = _points[i];
 				distanceSqr = pointDistSqr;
 			}
 		}
-		if (edgeDists._onEdge >= 0.0f && edgeDists._onEdge <= edgeDists._edgeLength)
-		{
+		if (edgeDists._onEdge >= 0.0f && edgeDists._onEdge <= edgeDists._edgeLength) {
 			float edgeDistSqr = powf(edgeDists._toEdge, 2.0f);
-			if (edgeDistSqr < distanceSqr)
-			{
+			if (edgeDistSqr < distanceSqr) {
 				distanceSqr = edgeDistSqr;
 				uint j = (i + 1) % _points.size();
 				bestPoint = _points[i] + (_points[j] - _points[i]) * (edgeDists._onEdge / edgeDists._edgeLength);
@@ -164,9 +161,12 @@ static float depthAtForConvex(const PathFindingPolygon &p, Point q) {
 float PathFindingPolygon::depthAt(Point query) const {
 	switch (_points.size()) {
 	case 0:
-	case 1: return 1.0f;
-	case 2: return depthAtForLine(_points[0], _points[1], query, _pointDepths[0], _pointDepths[1]);
-	default: return depthAtForConvex(*this, query);
+	case 1:
+		return 1.0f;
+	case 2:
+		return depthAtForLine(_points[0], _points[1], query, _pointDepths[0], _pointDepths[1]);
+	default:
+		return depthAtForConvex(*this, query);
 	}
 }
 
@@ -238,10 +238,14 @@ static Color colorAtForConvex(const FloorColorPolygon &p, Point query) {
 
 Color FloorColorPolygon::colorAt(Point query) const {
 	switch (_points.size()) {
-	case 0: return kWhite;
-	case 1: return { 255, 255, 255, _pointColors[0].a };
-	case 2: return colorAtForLine(_points[0], _points[1], query, _pointColors[0], _pointColors[1]);
-	default: return colorAtForConvex(*this, query);
+	case 0:
+		return kWhite;
+	case 1:
+		return { 255, 255, 255, _pointColors[0].a };
+	case 2:
+		return colorAtForLine(_points[0], _points[1], query, _pointColors[0], _pointColors[1]);
+	default:
+		return colorAtForConvex(*this, query);
 	}
 }
 
@@ -310,12 +314,10 @@ Point Shape::closestPointTo(Point query, int32 &polygonI) const {
 	assert(_polygons.size() > 0);
 	float bestDistanceSqr = std::numeric_limits<float>::infinity();
 	Point bestPoint = {};
-	for (uint i = 0; i < _polygons.size(); i++)
-	{
+	for (uint i = 0; i < _polygons.size(); i++) {
 		float curDistanceSqr = std::numeric_limits<float>::infinity();
 		Point curPoint = at(i).closestPointTo(query, curDistanceSqr);
-		if (curDistanceSqr < bestDistanceSqr)
-		{
+		if (curDistanceSqr < bestDistanceSqr) {
 			bestDistanceSqr = curDistanceSqr;
 			bestPoint = curPoint;
 			polygonI = (int32)i;
@@ -486,10 +488,10 @@ void PathFindingShape::initializeFloydWarshall() {
 }
 
 void PathFindingShape::calculateFloydWarshall() {
-	const auto distance = [&](uint a, uint b) -> uint& {
+	const auto distance = [&] (uint a, uint b) -> uint &{
 		return _distanceMatrix[a * _linkPoints.size() + b];
 	};
-	const auto previousTarget = [&](uint a, uint b) -> int32& {
+	const auto previousTarget = [&] (uint a, uint b) -> int32 &{
 		return _previousTarget[a * _linkPoints.size() + b];
 	};
 	for (uint over = 0; over < _linkPoints.size(); over++) {
diff --git a/engines/alcachofa/sounds.cpp b/engines/alcachofa/sounds.cpp
index 1e2a70edea7..dd14db3e5ab 100644
--- a/engines/alcachofa/sounds.cpp
+++ b/engines/alcachofa/sounds.cpp
@@ -73,8 +73,7 @@ void Sounds::update() {
 			if (g_system->getMillis() >= playback._fadeStart + playback._fadeDuration) {
 				_mixer->stopHandle(playback._handle);
 				_playbacks.erase(_playbacks.begin() + i - 1);
-			}
-			else {
+			} else {
 				byte newVolume = (g_system->getMillis() - playback._fadeStart) * Mixer::kMaxChannelVolume / playback._fadeDuration;
 				_mixer->setChannelVolume(playback._handle, Mixer::kMaxChannelVolume - newVolume);
 			}
@@ -157,8 +156,7 @@ SoundHandle Sounds::playSoundInternal(const char *fileName, byte volume, Mixer::
 			if (sampleCount < 0)
 				samples.clear();
 			samples.resize((uint)sampleCount); // we might have gotten less samples
-		}
-		else {
+		} else {
 			// we did not, now it is getting inefficient
 			const int bufferSize = 2048;
 			int16 buffer[bufferSize];
@@ -345,8 +343,7 @@ Task *Sounds::waitForMusicToEnd(Process &process) {
 
 PlaySoundTask::PlaySoundTask(Process &process, SoundHandle SoundHandle)
 	: Task(process)
-	, _soundHandle(SoundHandle) {
-}
+	, _soundHandle(SoundHandle) {}
 
 PlaySoundTask::PlaySoundTask(Process &process, Serializer &s)
 	: Task(process)
@@ -358,12 +355,10 @@ PlaySoundTask::PlaySoundTask(Process &process, Serializer &s)
 
 TaskReturn PlaySoundTask::run() {
 	auto &sounds = g_engine->sounds();
-	if (sounds.isAlive(_soundHandle))
-	{
+	if (sounds.isAlive(_soundHandle)) {
 		sounds.setAppropriateVolume(_soundHandle, process().character(), nullptr);
 		return TaskReturn::yield();
-	}
-	else
+	} else
 		return TaskReturn::finish(1);
 }
 
@@ -376,7 +371,7 @@ DECLARE_TASK(PlaySoundTask)
 
 WaitForMusicTask::WaitForMusicTask(Process &process)
 	: Task(process)
-	, _lock("wait-for-music", g_engine->sounds().musicSemaphore()) { }
+	, _lock("wait-for-music", g_engine->sounds().musicSemaphore()) {}
 
 WaitForMusicTask::WaitForMusicTask(Process &process, Serializer &s)
 	: Task(process)
diff --git a/engines/alcachofa/ui-objects.cpp b/engines/alcachofa/ui-objects.cpp
index 6e68b87fac7..ca0b73b67f8 100644
--- a/engines/alcachofa/ui-objects.cpp
+++ b/engines/alcachofa/ui-objects.cpp
@@ -38,8 +38,7 @@ MenuButton::MenuButton(Room *room, ReadStream &stream)
 	, _graphicNormal(stream)
 	, _graphicHovered(stream)
 	, _graphicClicked(stream)
-	, _graphicDisabled(stream) {
-}
+	, _graphicDisabled(stream) {}
 
 void MenuButton::draw() {
 	if (!isEnabled())
@@ -107,14 +106,12 @@ void MenuButton::trigger() {
 const char *InternetMenuButton::typeName() const { return "InternetMenuButton"; }
 
 InternetMenuButton::InternetMenuButton(Room *room, ReadStream &stream)
-	: MenuButton(room, stream) {
-}
+	: MenuButton(room, stream) {}
 
 const char *OptionsMenuButton::typeName() const { return "OptionsMenuButton"; }
 
 OptionsMenuButton::OptionsMenuButton(Room *room, ReadStream &stream)
-	: MenuButton(room, stream) {
-}
+	: MenuButton(room, stream) {}
 
 void OptionsMenuButton::update() {
 	MenuButton::update();
@@ -130,8 +127,7 @@ void OptionsMenuButton::trigger() {
 const char *MainMenuButton::typeName() const { return "MainMenuButton"; }
 
 MainMenuButton::MainMenuButton(Room *room, ReadStream &stream)
-	: MenuButton(room, stream) {
-}
+	: MenuButton(room, stream) {}
 
 void MainMenuButton::update() {
 	MenuButton::update();
@@ -152,8 +148,7 @@ PushButton::PushButton(Room *room, ReadStream &stream)
 	, _alwaysVisible(readBool(stream))
 	, _graphic1(stream)
 	, _graphic2(stream)
-	, _actionId(stream.readSint32LE()) {
-}
+	, _actionId(stream.readSint32LE()) {}
 
 const char *EditBox::typeName() const { return "EditBox"; }
 
@@ -166,8 +161,7 @@ EditBox::EditBox(Room *room, ReadStream &stream)
 	, i3(stream.readSint32LE())
 	, i4(stream.readSint32LE())
 	, i5(stream.readSint32LE())
-	, _fontId(stream.readSint32LE()) {
-}
+	, _fontId(stream.readSint32LE()) {}
 
 const char *CheckBox::typeName() const { return "CheckBox"; }
 
@@ -178,8 +172,7 @@ CheckBox::CheckBox(Room *room, ReadStream &stream)
 	, _graphicChecked(stream)
 	, _graphicHovered(stream)
 	, _graphicClicked(stream)
-	, _actionId(stream.readSint32LE()) {
-}
+	, _actionId(stream.readSint32LE()) {}
 
 void CheckBox::draw() {
 	if (!isEnabled())
@@ -251,8 +244,7 @@ SlideButton::SlideButton(Room *room, ReadStream &stream)
 	, _maxPos(Shape(stream).firstPoint())
 	, _graphicIdle(stream)
 	, _graphicHovered(stream)
-	, _graphicClicked(stream) {
-}
+	, _graphicClicked(stream) {}
 
 void SlideButton::draw() {
 	auto *optionsMenu = dynamic_cast<OptionsMenu *>(room());
@@ -277,14 +269,12 @@ void SlideButton::update() {
 			optionsMenu->currentSlideButton() = nullptr;
 			g_engine->menu().triggerOptionsValue((OptionsMenuValue)_valueId, _value);
 			update(); // to update the position
-		}
-		else {
+		} else {
 			int clippedMousePosY = CLIP(mousePos.y, _minPos.y, _maxPos.y);
 			_value = (_maxPos.y - clippedMousePosY) / (float)(_maxPos.y - _minPos.y);
 			_graphicClicked.topLeft() = Point((_minPos.x + _maxPos.x) / 2, clippedMousePosY);
 		}
-	}
-	else {
+	} else {
 		_graphicIdle.topLeft() = Point(
 			(_minPos.x + _maxPos.x) / 2,
 			(int16)(_maxPos.y - _value * (_maxPos.y - _minPos.y)));
@@ -322,8 +312,7 @@ const char *IRCWindow::typeName() const { return "IRCWindow"; }
 IRCWindow::IRCWindow(Room *room, ReadStream &stream)
 	: ObjectBase(room, stream)
 	, _p1(Shape(stream).firstPoint())
-	, _p2(Shape(stream).firstPoint()) {
-}
+	, _p2(Shape(stream).firstPoint()) {}
 
 const char *MessageBox::typeName() const { return "MessageBox"; }
 


Commit: e5e42cc7b1d1e3d4e8379d83f5508417ac41688a
    https://github.com/scummvm/scummvm/commit/e5e42cc7b1d1e3d4e8379d83f5508417ac41688a
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:05:00+02:00

Commit Message:
ALCACHOFA: Remove u8 string prefix

Changed paths:
    engines/alcachofa/detection_tables.h


diff --git a/engines/alcachofa/detection_tables.h b/engines/alcachofa/detection_tables.h
index 14c9ab54e4e..cfc7bc04132 100644
--- a/engines/alcachofa/detection_tables.h
+++ b/engines/alcachofa/detection_tables.h
@@ -53,7 +53,7 @@ const ADGameDescription gameDescriptions[] = {
 	// The "english" version is just the spanish version with english subtitles...
 	{
 		"mort_phil_adventura_de_cine",
-		u8"Mortadelo y Filemón: Una Aventura de Cine - Edición Especial",
+		"Mortadelo y Filemón: Una Aventura de Cine - Edición Especial",
 		AD_ENTRY1s("Textos/Objetos.nkr", "ad3cb78ad7a51cfe63ee6f84768c7e66", 15895),
 		Common::EN_ANY,
 		Common::kPlatformWindows,
@@ -62,7 +62,7 @@ const ADGameDescription gameDescriptions[] = {
 	},
 	{
 		"mort_phil_adventura_de_cine",
-		u8"Mortadelo y Filemón: Una Aventura de Cine - Edición Especial",
+		"Mortadelo y Filemón: Una Aventura de Cine - Edición Especial",
 		AD_ENTRY1s("Textos/Objetos.nkr", "93331e4cc8d2f8f8a0007bfb5140dff5", 16403),
 		Common::ES_ESP,
 		Common::kPlatformWindows,


Commit: b3368f57429e37ec632743002221ff6f8efbf58b
    https://github.com/scummvm/scummvm/commit/b3368f57429e37ec632743002221ff6f8efbf58b
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:05:00+02:00

Commit Message:
ALCACHOFA: Rename gameid to aventuradecine

Changed paths:
    engines/alcachofa/detection_tables.h


diff --git a/engines/alcachofa/detection_tables.h b/engines/alcachofa/detection_tables.h
index cfc7bc04132..ad4ad6940ac 100644
--- a/engines/alcachofa/detection_tables.h
+++ b/engines/alcachofa/detection_tables.h
@@ -22,7 +22,7 @@
 namespace Alcachofa {
 
 const PlainGameDescriptor alcachofaGames[] = {
-	{ "mort_phil_adventura_de_cine", "Mort & Phil: A Movie Adventure" },
+	{ "aventuradecine", "Mort & Phil: A Movie Adventure" },
 	{ 0, 0 }
 };
 
@@ -31,7 +31,7 @@ const ADGameDescription gameDescriptions[] = {
 	// A Movie Adventure
 	//
 	{
-		"mort_phil_adventura_de_cine",
+		"aventuradecine",
 		"Clever & Smart - A Movie Adventure",
 		AD_ENTRY1s("Textos/Objetos.nkr", "a2b1deff5ca7187f2ebf7f2ab20747e9", 17606),
 		Common::DE_DEU,
@@ -40,7 +40,7 @@ const ADGameDescription gameDescriptions[] = {
 		GUIO2(GAMEOPTION_32BITS, GAMEOPTION_HIGH_QUALITY)
 	},
 	{
-		"mort_phil_adventura_de_cine",
+		"aventuradecine",
 		"Clever & Smart - A Movie Adventure",
 		AD_ENTRY1s("Textos/Objetos.nkr", "8dce25494470209d4882bf12f1a5ea42", 19208),
 		Common::DE_DEU,
@@ -52,7 +52,7 @@ const ADGameDescription gameDescriptions[] = {
 
 	// The "english" version is just the spanish version with english subtitles...
 	{
-		"mort_phil_adventura_de_cine",
+		"aventuradecine",
 		"Mortadelo y Filemón: Una Aventura de Cine - Edición Especial",
 		AD_ENTRY1s("Textos/Objetos.nkr", "ad3cb78ad7a51cfe63ee6f84768c7e66", 15895),
 		Common::EN_ANY,
@@ -61,7 +61,7 @@ const ADGameDescription gameDescriptions[] = {
 		GUIO2(GAMEOPTION_32BITS, GAMEOPTION_HIGH_QUALITY)
 	},
 	{
-		"mort_phil_adventura_de_cine",
+		"aventuradecine",
 		"Mortadelo y Filemón: Una Aventura de Cine - Edición Especial",
 		AD_ENTRY1s("Textos/Objetos.nkr", "93331e4cc8d2f8f8a0007bfb5140dff5", 16403),
 		Common::ES_ESP,


Commit: 999a5b01677f31451b8bada4f1fb885b1941f689
    https://github.com/scummvm/scummvm/commit/999a5b01677f31451b8bada4f1fb885b1941f689
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:05:01+02:00

Commit Message:
ALCACHOFA: Add ADGF_REMASTERED in preparation for edicion original

Changed paths:
    engines/alcachofa/detection_tables.h


diff --git a/engines/alcachofa/detection_tables.h b/engines/alcachofa/detection_tables.h
index ad4ad6940ac..32cca449889 100644
--- a/engines/alcachofa/detection_tables.h
+++ b/engines/alcachofa/detection_tables.h
@@ -36,7 +36,7 @@ const ADGameDescription gameDescriptions[] = {
 		AD_ENTRY1s("Textos/Objetos.nkr", "a2b1deff5ca7187f2ebf7f2ab20747e9", 17606),
 		Common::DE_DEU,
 		Common::kPlatformWindows,
-		ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE,
+		ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE | ADGF_REMASTERED,
 		GUIO2(GAMEOPTION_32BITS, GAMEOPTION_HIGH_QUALITY)
 	},
 	{
@@ -45,7 +45,7 @@ const ADGameDescription gameDescriptions[] = {
 		AD_ENTRY1s("Textos/Objetos.nkr", "8dce25494470209d4882bf12f1a5ea42", 19208),
 		Common::DE_DEU,
 		Common::kPlatformWindows,
-		ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE | ADGF_DEMO,
+		ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE | ADGF_REMASTERED | ADGF_DEMO,
 		GUIO2(GAMEOPTION_32BITS, GAMEOPTION_HIGH_QUALITY)
 	},
 
@@ -57,7 +57,7 @@ const ADGameDescription gameDescriptions[] = {
 		AD_ENTRY1s("Textos/Objetos.nkr", "ad3cb78ad7a51cfe63ee6f84768c7e66", 15895),
 		Common::EN_ANY,
 		Common::kPlatformWindows,
-		ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE,
+		ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE | ADGF_REMASTERED,
 		GUIO2(GAMEOPTION_32BITS, GAMEOPTION_HIGH_QUALITY)
 	},
 	{
@@ -66,7 +66,7 @@ const ADGameDescription gameDescriptions[] = {
 		AD_ENTRY1s("Textos/Objetos.nkr", "93331e4cc8d2f8f8a0007bfb5140dff5", 16403),
 		Common::ES_ESP,
 		Common::kPlatformWindows,
-		ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE,
+		ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE | ADGF_REMASTERED,
 		GUIO2(GAMEOPTION_32BITS, GAMEOPTION_HIGH_QUALITY)
 	},
 


Commit: 07a39679ddc731efcc592a3eef2eac4a99643f82
    https://github.com/scummvm/scummvm/commit/07a39679ddc731efcc592a3eef2eac4a99643f82
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:05:01+02:00

Commit Message:
ALCACHOFA: Remove temporary saveFileMgr variable

Changed paths:
    engines/alcachofa/alcachofa.cpp


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 27a8d94803c..19805ed1ab1 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -395,8 +395,7 @@ bool AlcachofaEngine::tryLoadFromLauncher() {
 	int saveSlot = ConfMan.getInt("save_slot");
 	if (!ConfMan.hasKey("save_slot") || saveSlot < 0)
 		return false;
-	auto *saveFileMgr = g_system->getSavefileManager();
-	auto *saveFile = saveFileMgr->openForLoading(getSaveStateName(saveSlot));
+	auto *saveFile = g_system->getSavefileManager()->openForLoading(getSaveStateName(saveSlot));
 	if (saveFile == nullptr)
 		return false;
 	bool result = loadGameStream(saveFile).getCode() == kNoError;


Commit: ac843c9fd03a85d98429663186aa3efbec3f0d40
    https://github.com/scummvm/scummvm/commit/ac843c9fd03a85d98429663186aa3efbec3f0d40
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:05:01+02:00

Commit Message:
ALCACHOFA: Remove redundant pragma once

Changed paths:
    engines/alcachofa/game.h


diff --git a/engines/alcachofa/game.h b/engines/alcachofa/game.h
index 65b99e52671..548935d43eb 100644
--- a/engines/alcachofa/game.h
+++ b/engines/alcachofa/game.h
@@ -1,4 +1,3 @@
-#pragma once
 /* ScummVM - Graphic Adventure Engine
  *
  * ScummVM is the legal property of its developers, whose names


Commit: 917ceb51156acf6bbfd75078a685a6d5743e2dd5
    https://github.com/scummvm/scummvm/commit/917ceb51156acf6bbfd75078a685a6d5743e2dd5
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:05:01+02:00

Commit Message:
ALCACHOFA: Replace _DEBUG macro usage

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


diff --git a/engines/alcachofa/game.cpp b/engines/alcachofa/game.cpp
index 8ea1da03f07..b663a5380fc 100644
--- a/engines/alcachofa/game.cpp
+++ b/engines/alcachofa/game.cpp
@@ -28,7 +28,7 @@ using namespace Common;
 namespace Alcachofa {
 
 Game::Game()
-#ifdef _DEBUG // During development let's check out these errors more carefully
+#ifdef ALCACHOFA_DEBUG // During development let's check out these errors more carefully
 	: _message(error)
 #else // For release builds the game might still work or the user might still be able to save and restart
 	: _message(warning)
diff --git a/engines/alcachofa/graphics-opengl.cpp b/engines/alcachofa/graphics-opengl.cpp
index e9881b189c7..6a3e1a0ad95 100644
--- a/engines/alcachofa/graphics-opengl.cpp
+++ b/engines/alcachofa/graphics-opengl.cpp
@@ -341,7 +341,7 @@ public:
 			GL_CALL(glTexCoordPointer(2, GL_FLOAT, 0, texCoords));
 		GL_CALL(glDrawArrays(GL_QUADS, 0, 4));
 
-#ifdef _DEBUG
+#ifdef ALCACHOFA_DEBUG
 		// make sure we crash instead of someone using our stack arrays
 		GL_CALL(glVertexPointer(2, GL_FLOAT, sizeof(Vector2d), nullptr));
 		GL_CALL(glTexCoordPointer(2, GL_FLOAT, sizeof(Vector2d), nullptr));
diff --git a/engines/alcachofa/graphics.cpp b/engines/alcachofa/graphics.cpp
index 593b617bf57..ebbf2e6e8c3 100644
--- a/engines/alcachofa/graphics.cpp
+++ b/engines/alcachofa/graphics.cpp
@@ -117,7 +117,7 @@ void AnimationBase::load() {
 		_spriteBases.push_back(stream->readUint32LE());
 		assert(_spriteBases.back() < imageCount);
 	}
-#ifdef _DEBUG
+#ifdef ALCACHOFA_DEBUG
 	for (uint i = spriteCount; i < kMaxSpriteIDs; i++)
 		assert(stream->readSint32LE() == 0);
 #else


Commit: 32c4ebc08ac8cd294f9d20f1e7ba412af9454f41
    https://github.com/scummvm/scummvm/commit/32c4ebc08ac8cd294f9d20f1e7ba412af9454f41
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:05:01+02:00

Commit Message:
ALCACHOFA: Fix end-of-file comment

Changed paths:
    engines/alcachofa/graphics.h


diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index 4f9ce4f3901..a3b0727c8f2 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -476,4 +476,4 @@ private:
 
 }
 
-#endif // ALCACHOFA_ENGINE_H
+#endif // ALCACHOFA_GRAPHICS_H


Commit: 92d06884297e446f17ae85389e484604d0400fb7
    https://github.com/scummvm/scummvm/commit/92d06884297e446f17ae85389e484604d0400fb7
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:05:01+02:00

Commit Message:
ALCACHOFA: Use luminance for grayscaled thumbnails

Changed paths:
    engines/alcachofa/menu.cpp


diff --git a/engines/alcachofa/menu.cpp b/engines/alcachofa/menu.cpp
index d9ebcb78606..faed5b72891 100644
--- a/engines/alcachofa/menu.cpp
+++ b/engines/alcachofa/menu.cpp
@@ -50,7 +50,7 @@ static void convertToGrayscale(ManagedSurface &surface) {
 		pixel = (uint32 *)surface.getBasePtr(0, y);
 		for (int x = 0; x < surface.w; x++, pixel++) {
 			*pixel &= rgbMask;
-			byte gray = (components[0] + components[1] + components[2] + components[3]) / 3;
+			byte gray = (byte)CLIP(0.29f * components[0] + 0.58f * components[1] + 0.11f * components[2], 0.0f, 255.0f);
 			*pixel =
 				(uint32(gray) << surface.format.rShift) |
 				(uint32(gray) << surface.format.gShift) |


Commit: 17175d0de9c3e0994d1f276de07ae5620adabc7a
    https://github.com/scummvm/scummvm/commit/17175d0de9c3e0994d1f276de07ae5620adabc7a
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:05:01+02:00

Commit Message:
ALCACHOFA: Fix regression error when loading a DelayTask

Changed paths:
    engines/alcachofa/alcachofa.cpp
    engines/alcachofa/scheduler.cpp
    engines/alcachofa/scheduler.h
    engines/alcachofa/tasks.h


diff --git a/engines/alcachofa/alcachofa.cpp b/engines/alcachofa/alcachofa.cpp
index 19805ed1ab1..fa25a74926b 100644
--- a/engines/alcachofa/alcachofa.cpp
+++ b/engines/alcachofa/alcachofa.cpp
@@ -427,4 +427,33 @@ void Config::saveToScummVM() {
 	// if you set it in ScummVMs dialog it sticks
 }
 
+DelayTask::DelayTask(Process &process, uint32 millis)
+	: Task(process)
+	, _endTime(millis) {}
+
+DelayTask::DelayTask(Process &process, Serializer &s)
+	: Task(process) {
+	syncGame(s);
+}
+
+TaskReturn DelayTask::run() {
+	TASK_BEGIN;
+	_endTime += g_engine->getMillis();
+	while (g_engine->getMillis() < _endTime)
+		TASK_YIELD(1);
+	TASK_END;
+}
+
+void DelayTask::debugPrint() {
+	uint32 remaining = g_engine->getMillis() <= _endTime ? _endTime - g_engine->getMillis() : 0;
+	g_engine->getDebugger()->debugPrintf("Delay for further %ums\n", remaining);
+}
+
+void DelayTask::syncGame(Serializer &s) {
+	Task::syncGame(s);
+	s.syncAsUint32LE(_endTime);
+}
+
+DECLARE_TASK(DelayTask)
+
 } // End of namespace Alcachofa
diff --git a/engines/alcachofa/scheduler.cpp b/engines/alcachofa/scheduler.cpp
index d58cb44f681..81b5f38bc9e 100644
--- a/engines/alcachofa/scheduler.cpp
+++ b/engines/alcachofa/scheduler.cpp
@@ -85,35 +85,6 @@ void Task::errorForUnexpectedObjectType(const ObjectBase *base) const {
 		base->name().c_str(), taskName(), base->typeName());
 }
 
-DelayTask::DelayTask(Process &process, uint32 millis)
-	: Task(process)
-	, _endTime(millis) {}
-
-DelayTask::DelayTask(Process &process, Serializer &s)
-	: Task(process) {
-	syncGame(s);
-}
-
-TaskReturn DelayTask::run() {
-	TASK_BEGIN;
-	_endTime += g_engine->getMillis();
-	while (g_engine->getMillis() < _endTime)
-		TASK_YIELD(1);
-	TASK_END;
-}
-
-void DelayTask::debugPrint() {
-	uint32 remaining = g_engine->getMillis() <= _endTime ? _endTime - g_engine->getMillis() : 0;
-	g_engine->getDebugger()->debugPrintf("Delay for further %ums\n", remaining);
-}
-
-void DelayTask::syncGame(Serializer &s) {
-	Task::syncGame(s);
-	s.syncAsUint32LE(_endTime);
-}
-
-DECLARE_TASK(DelayTask)
-
 Process::Process(ProcessId pid, MainCharacterKind characterKind)
 	: _pid(pid)
 	, _character(characterKind)
diff --git a/engines/alcachofa/scheduler.h b/engines/alcachofa/scheduler.h
index 50c685c5a65..55bb113259c 100644
--- a/engines/alcachofa/scheduler.h
+++ b/engines/alcachofa/scheduler.h
@@ -113,6 +113,8 @@ private:
 	Process &_process;
 };
 
+// implemented in alcachofa.cpp to prevent a compiler warning when
+// the declaration of the construct function comes after the definition
 struct DelayTask : public Task {
 	DelayTask(Process &process, uint32 millis);
 	DelayTask(Process &process, Common::Serializer &s);
diff --git a/engines/alcachofa/tasks.h b/engines/alcachofa/tasks.h
index 74cbabd9817..8c95f719033 100644
--- a/engines/alcachofa/tasks.h
+++ b/engines/alcachofa/tasks.h
@@ -51,9 +51,6 @@ DEFINE_TASK(ScriptTimerTask)
 DEFINE_TASK(ScriptTask)
 DEFINE_TASK(PlaySoundTask)
 DEFINE_TASK(WaitForMusicTask)
-
-// this one is special as the implementation is in the same TU as the signature
-// which causes a warning on some pedantic compiler
-//DEFINE_TASK(DelayTask)  
+DEFINE_TASK(DelayTask)  
 
 #undef DEFINE_TASK


Commit: e45cb342be6e56ef301dc01fca3c0ac2dfe19e7a
    https://github.com/scummvm/scummvm/commit/e45cb342be6e56ef301dc01fca3c0ac2dfe19e7a
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:05:01+02:00

Commit Message:
ALCACHOFA: Remove feature 3d

Changed paths:
    engines/alcachofa/configure.engine


diff --git a/engines/alcachofa/configure.engine b/engines/alcachofa/configure.engine
index 6e58d75f3ec..3904b6543c3 100644
--- a/engines/alcachofa/configure.engine
+++ b/engines/alcachofa/configure.engine
@@ -1,3 +1,3 @@
 # This file is included from the main "configure" script
 # add_engine [name] [desc] [build-by-default] [subengines] [base games] [deps]
-add_engine alcachofa "Alcachofa" no "" "" "highres opengl_game_classic mpeg2 3d"
+add_engine alcachofa "Alcachofa" no "" "" "highres opengl_game_classic mpeg2"


Commit: eb8499e9021732db00b0f9d40e0b02806dfb078e
    https://github.com/scummvm/scummvm/commit/eb8499e9021732db00b0f9d40e0b02806dfb078e
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:05:01+02:00

Commit Message:
ALCACHOFA: Split up OpenGL renderer

Changed paths:
  A engines/alcachofa/graphics-opengl-classic.cpp
  A engines/alcachofa/graphics-opengl.h
    engines/alcachofa/graphics-opengl.cpp
    engines/alcachofa/graphics.h
    engines/alcachofa/module.mk


diff --git a/engines/alcachofa/graphics-opengl-classic.cpp b/engines/alcachofa/graphics-opengl-classic.cpp
new file mode 100644
index 00000000000..578d1e067a9
--- /dev/null
+++ b/engines/alcachofa/graphics-opengl-classic.cpp
@@ -0,0 +1,238 @@
+/* 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/graphics-opengl.h"
+#include "alcachofa/detection.h"
+
+#include "common/system.h"
+#include "engines/util.h"
+
+using namespace Common;
+using namespace Math;
+using namespace Graphics;
+
+namespace Alcachofa {
+
+class OpenGLRendererClassic : public OpenGLRenderer, public virtual IDebugRenderer {
+public:
+	using OpenGLRenderer::OpenGLRenderer;
+
+	void begin() override {
+		GL_CALL(glEnableClientState(GL_VERTEX_ARRAY));
+		GL_CALL(glDisableClientState(GL_INDEX_ARRAY));
+		GL_CALL(glDisableClientState(GL_TEXTURE_COORD_ARRAY));
+		resetState();
+	}
+
+	void setTexture(ITexture *texture) override {
+		if (texture == _currentTexture)
+			return;
+		else if (texture == nullptr) {
+			GL_CALL(glDisable(GL_TEXTURE_2D));
+			GL_CALL(glDisableClientState(GL_TEXTURE_COORD_ARRAY));
+			_currentTexture = nullptr;
+		} else {
+			if (_currentTexture == nullptr) {
+				GL_CALL(glEnable(GL_TEXTURE_2D));
+				GL_CALL(glEnableClientState(GL_TEXTURE_COORD_ARRAY));
+			}
+			auto glTexture = dynamic_cast<OpenGLTexture *>(texture);
+			assert(glTexture != nullptr);
+			GL_CALL(glBindTexture(GL_TEXTURE_2D, glTexture->handle()));
+			_currentTexture = glTexture;
+		}
+	}
+
+	void setBlendMode(BlendMode blendMode) override {
+		if (blendMode == _currentBlendMode)
+			return;
+		setBlendFunc(blendMode);
+
+		GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE));
+		switch (blendMode) {
+		case BlendMode::AdditiveAlpha:
+		case BlendMode::Additive:
+		case BlendMode::Multiply:
+			// TintAlpha * TexColor, TexAlpha
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE));
+
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_TEXTURE));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_ALPHA, GL_TEXTURE));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA));
+
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_PRIMARY_COLOR));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_ALPHA)); // alpha replaces color
+			break;
+		case BlendMode::Alpha:
+			// TexColor, TintAlpha
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_REPLACE));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE));
+
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_TEXTURE));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_ALPHA, GL_PRIMARY_COLOR));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA));
+			break;
+		case BlendMode::Tinted:
+			// (TintColor * TintAlpha) * TexColor, TexAlpha
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE));
+
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_TEXTURE));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_ALPHA, GL_TEXTURE));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA));
+
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_PRIMARY_COLOR));
+			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR)); // we have to pre-multiply
+			break;
+		default:
+			assert(false && "Invalid blend mode");
+			break;
+		}
+		_currentBlendMode = blendMode;
+	}
+
+	void setLodBias(float lodBias) override {
+		if (abs(_currentLodBias - lodBias) < epsilon)
+			return;
+		GL_CALL(glTexEnvf(GL_TEXTURE_FILTER_CONTROL, GL_TEXTURE_LOD_BIAS, lodBias));
+		_currentLodBias = lodBias;
+	}
+
+	void quad(
+		Vector2d topLeft,
+		Vector2d size,
+		Color color,
+		Angle rotation,
+		Vector2d texMin,
+		Vector2d texMax) override {
+		Vector2d positions[] = {
+			topLeft + Vector2d(0,			0),
+			topLeft + Vector2d(0,			+size.getY()),
+			topLeft + Vector2d(+size.getX(), +size.getY()),
+			topLeft + Vector2d(+size.getX(), 0),
+		};
+		if (abs(rotation.getDegrees()) > epsilon) {
+			const Vector2d zero(0, 0);
+			for (int i = 0; i < 4; i++)
+				positions[i].rotateAround(zero, rotation);
+		}
+
+		Vector2d texCoords[] = {
+			{ texMin.getX(), texMin.getY() },
+			{ texMin.getX(), texMax.getY() },
+			{ texMax.getX(), texMax.getY() },
+			{ texMax.getX(), texMin.getY() }
+		};
+		if (_currentTexture != nullptr) {
+			// float equality is fine here, if it was calculated it was not a normal graphic
+			_currentTexture->setMirrorWrap(texMin != Vector2d() || texMax != Vector2d(1, 1));
+		}
+
+		float colors[] = { color.r / 255.0f, color.g / 255.0f, color.b / 255.0f, color.a / 255.0f };
+		if (_currentBlendMode == BlendMode::Tinted) {
+			colors[0] *= colors[3];
+			colors[1] *= colors[3];
+			colors[2] *= colors[3];
+		}
+
+		checkFirstDrawCommand();
+		GL_CALL(glColor4fv(colors));
+		GL_CALL(glVertexPointer(2, GL_FLOAT, 0, positions));
+		if (_currentTexture != nullptr)
+			GL_CALL(glTexCoordPointer(2, GL_FLOAT, 0, texCoords));
+		GL_CALL(glDrawArrays(GL_QUADS, 0, 4));
+
+#ifdef ALCACHOFA_DEBUG
+		// make sure we crash instead of someone using our stack arrays
+		GL_CALL(glVertexPointer(2, GL_FLOAT, sizeof(Vector2d), nullptr));
+		GL_CALL(glTexCoordPointer(2, GL_FLOAT, sizeof(Vector2d), nullptr));
+#endif
+	}
+
+	void debugPolygon(
+		Span<Vector2d> points,
+		Color color
+	) override {
+		checkFirstDrawCommand();
+		setTexture(nullptr);
+		setBlendMode(BlendMode::Alpha);
+		GL_CALL(glVertexPointer(2, GL_FLOAT, 0, points.data()));
+		GL_CALL(glLineWidth(4.0f));
+		GL_CALL(glPointSize(8.0f));
+
+		GL_CALL(glColor4ub(color.r, color.g, color.b, color.a));
+		if (points.size() > 2)
+			GL_CALL(glDrawArrays(GL_POLYGON, 0, points.size()));
+
+		color.a = (byte)(MIN(255.0f, color.a * 1.3f));
+		GL_CALL(glColor4ub(color.r, color.g, color.b, color.a));
+		if (points.size() > 1)
+			GL_CALL(glDrawArrays(GL_LINE_LOOP, 0, points.size()));
+
+		color.a = (byte)(MIN(255.0f, color.a * 1.3f));
+		GL_CALL(glColor4ub(color.r, color.g, color.b, color.a));
+		if (points.size() > 0)
+			GL_CALL(glDrawArrays(GL_POINTS, 0, points.size()));
+	}
+
+	void debugPolyline(
+		Span<Vector2d> points,
+		Color color
+	) override {
+		checkFirstDrawCommand();
+		setTexture(nullptr);
+		setBlendMode(BlendMode::Alpha);
+		GL_CALL(glVertexPointer(2, GL_FLOAT, 0, points.data()));
+		GL_CALL(glLineWidth(4.0f));
+		GL_CALL(glPointSize(8.0f));
+
+		GL_CALL(glColor4ub(color.r, color.g, color.b, color.a));
+		if (points.size() > 1)
+			GL_CALL(glDrawArrays(GL_LINE_STRIP, 0, points.size()));
+
+		color.a = (byte)(MIN(255.0f, color.a * 1.3f));
+		GL_CALL(glColor4ub(color.r, color.g, color.b, color.a));
+		if (points.size() > 0)
+			GL_CALL(glDrawArrays(GL_POINTS, 0, points.size()));
+	}
+
+	void setMatrices(bool flipped) override {
+		float bottom = flipped ? _resolution.y : 0.0f;
+		float top = flipped ? 0.0f : _resolution.y;
+
+		GL_CALL(glMatrixMode(GL_PROJECTION));
+		GL_CALL(glLoadIdentity());
+		GL_CALL(glOrtho(0.0f, _resolution.x, bottom, top, -1.0f, 1.0f));
+		GL_CALL(glMatrixMode(GL_MODELVIEW));
+		GL_CALL(glLoadIdentity());
+	}
+};
+
+IRenderer *IRenderer::createOpenGLRenderer(Point resolution) {
+	initGraphics3d(resolution.x, resolution.y);
+	return new OpenGLRendererClassic(resolution);
+}
+
+}
diff --git a/engines/alcachofa/graphics-opengl.cpp b/engines/alcachofa/graphics-opengl.cpp
index 6a3e1a0ad95..8c51e92f1d7 100644
--- a/engines/alcachofa/graphics-opengl.cpp
+++ b/engines/alcachofa/graphics-opengl.cpp
@@ -21,12 +21,10 @@
 
 #include "alcachofa/graphics.h"
 #include "alcachofa/detection.h"
+#include "alcachofa/graphics-opengl.h"
 
 #include "common/system.h"
 #include "engines/util.h"
-#include "graphics/managed_surface.h"
-#include "graphics/opengl/system_headers.h"
-#include "graphics/opengl/debug.h"
 
 using namespace Common;
 using namespace Math;
@@ -34,18 +32,13 @@ using namespace Graphics;
 
 namespace Alcachofa {
 
-struct OpenGLFormat {
-	GLenum _format, _type;
-	inline bool isValid() const { return _format != GL_NONE; }
-};
-
 static bool areComponentsInOrder(const PixelFormat &format, int r, int g, int b, int a) {
 	return format == (a < 0
 		? PixelFormat(3, 8, 8, 8, 0, r * 8, g * 8, b * 8, 0)
 		: PixelFormat(4, 8, 8, 8, 8, r * 8, g * 8, b * 8, a * 8));
 }
 
-static OpenGLFormat getOpenGLFormatOf(const PixelFormat &format) {
+OpenGLFormat getOpenGLFormatOf(const PixelFormat &format) {
 	if (areComponentsInOrder(format, 0, 1, 2, 3))
 		return { GL_RGBA, GL_UNSIGNED_BYTE };
 	else if (areComponentsInOrder(format, 3, 2, 1, 0))
@@ -61,394 +54,177 @@ static OpenGLFormat getOpenGLFormatOf(const PixelFormat &format) {
 		return { GL_NONE, GL_NONE };
 }
 
+OpenGLTexture::OpenGLTexture(int32 w, int32 h, bool withMipmaps)
+	: ITexture({ (int16)w, (int16)h })
+	, _withMipmaps(withMipmaps) {
+	GL_CALL(glEnable(GL_TEXTURE_2D));
+	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));
+	setMirrorWrap(false);
+}
 
-class OpenGLTexture : public ITexture {
-public:
-	OpenGLTexture(int32 w, int32 h, bool withMipmaps)
-		: ITexture({ (int16)w, (int16)h })
-		, _withMipmaps(withMipmaps) {
-		GL_CALL(glEnable(GL_TEXTURE_2D));
-		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));
-		setMirrorWrap(false);
-	}
-
-	~OpenGLTexture() override {
-		if (_handle != 0)
-			GL_CALL(glDeleteTextures(1, &_handle));
-	}
-
-	void update(const Surface &surface) override {
-		OpenGLFormat format = getOpenGLFormatOf(surface.format);
-		assert(surface.w == size().x && surface.h == size().y);
-		assert(format.isValid());
-
-		GL_CALL(glEnable(GL_TEXTURE_2D));
-		GL_CALL(glBindTexture(GL_TEXTURE_2D, _handle));
-		GL_CALL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, surface.w, surface.h, 0, format._format, format._type, surface.getPixels()));
-		if (_withMipmaps)
-			GL_CALL(glGenerateMipmap(GL_TEXTURE_2D));
-		else
-			GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0));
-	}
-
-	void setMirrorWrap(bool wrap) {
-		if (_mirrorWrap == wrap)
-			return;
-		_mirrorWrap = wrap;
-		GLint wrapMode;
-		if (wrap)
-			wrapMode = OpenGLContext.textureMirrorRepeatSupported ? GL_MIRRORED_REPEAT : GL_REPEAT;
-		else
-			wrapMode = OpenGLContext.textureEdgeClampSupported ? GL_CLAMP_TO_EDGE : GL_CLAMP;
-
-		GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapMode));
-		GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapMode));
-	}
-
-	inline GLuint handle() const { return _handle; }
-
-private:
-	GLuint _handle;
-	bool _withMipmaps;
-	bool _mirrorWrap = true;
-};
-
-class OpenGLRenderer : public IDebugRenderer {
-public:
-	OpenGLRenderer(Point resolution)
-		: _resolution(resolution) {
-		setViewportToScreen();
-
-		GL_CALL(glDisable(GL_LIGHTING));
-		GL_CALL(glDisable(GL_DEPTH_TEST));
-		GL_CALL(glDisable(GL_SCISSOR_TEST));
-		GL_CALL(glDisable(GL_STENCIL_TEST));
-		GL_CALL(glDisable(GL_CULL_FACE));
-		GL_CALL(glEnable(GL_BLEND));
-		GL_CALL(glDepthMask(GL_FALSE));
-
-		if (!OpenGLContext.NPOTSupported || !OpenGLContext.textureMirrorRepeatSupported) {
-			g_system->messageBox(LogMessageType::kWarning, "Old OpenGL detected, some graphical errors will occur.");
-		}
-	}
-
-	ScopedPtr<ITexture> createTexture(int32 w, int32 h, bool withMipmaps) override {
-		assert(w >= 0 && h >= 0);
-		return ScopedPtr<ITexture>(new OpenGLTexture(w, h, withMipmaps));
-	}
-
-	void begin() override {
-		GL_CALL(glEnableClientState(GL_VERTEX_ARRAY));
-		GL_CALL(glDisableClientState(GL_INDEX_ARRAY));
-		GL_CALL(glDisableClientState(GL_TEXTURE_COORD_ARRAY));
-		setViewportToScreen();
-		_currentOutput = nullptr;
-		_currentLodBias = -1000.0f;
-		_currentTexture = nullptr;
-		_currentBlendMode = (BlendMode)-1;
-		_isFirstDrawCommand = true;
-	}
-
-	void end() override {
-		GL_CALL(glFlush());
-
-		if (_currentOutput != nullptr) {
-			g_system->presentBuffer();
-			auto format = getOpenGLFormatOf(_currentOutput->format);
-			GL_CALL(glReadPixels(
-				0,
-				0,
-				_outputSize.x,
-				_outputSize.y,
-				format._format,
-				format._type,
-				_currentOutput->getPixels()
-			));
-		}
-	}
-
-	void setTexture(ITexture *texture) override {
-		if (texture == _currentTexture)
-			return;
-		else if (texture == nullptr) {
-			GL_CALL(glDisable(GL_TEXTURE_2D));
-			GL_CALL(glDisableClientState(GL_TEXTURE_COORD_ARRAY));
-			_currentTexture = nullptr;
-		} else {
-			if (_currentTexture == nullptr) {
-				GL_CALL(glEnable(GL_TEXTURE_2D));
-				GL_CALL(glEnableClientState(GL_TEXTURE_COORD_ARRAY));
-			}
-			auto glTexture = dynamic_cast<OpenGLTexture *>(texture);
-			assert(glTexture != nullptr);
-			GL_CALL(glBindTexture(GL_TEXTURE_2D, glTexture->handle()));
-			_currentTexture = glTexture;
-		}
-	}
-
-	void setBlendMode(BlendMode blendMode) override {
-		if (blendMode == _currentBlendMode)
-			return;
-		// first the blend func
-		switch (blendMode) {
-		case BlendMode::AdditiveAlpha:
-			GL_CALL(glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA));
-			break;
-		case BlendMode::Additive:
-			GL_CALL(glBlendFunc(GL_ONE, GL_ONE));
-			break;
-		case BlendMode::Multiply:
-			GL_CALL(glBlendFunc(GL_DST_COLOR, GL_ONE));
-			break;
-		case BlendMode::Alpha:
-			GL_CALL(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
-			break;
-		case BlendMode::Tinted:
-			GL_CALL(glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA));
-			break;
-		default: assert(false && "Invalid blend mode"); break;
-		}
-
-		GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE));
-		switch (blendMode) {
-		case BlendMode::AdditiveAlpha:
-		case BlendMode::Additive:
-		case BlendMode::Multiply:
-			// TintAlpha * TexColor, TexAlpha
-			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE));
-			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE));
+OpenGLTexture::~OpenGLTexture() {
+	if (_handle != 0)
+		GL_CALL(glDeleteTextures(1, &_handle));
+}
 
-			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_TEXTURE));
-			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR));
-			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_ALPHA, GL_TEXTURE));
-			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA));
+void OpenGLTexture::update(const Surface &surface) {
+	OpenGLFormat format = getOpenGLFormatOf(surface.format);
+	assert(surface.w == size().x && surface.h == size().y);
+	assert(format.isValid());
 
-			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_PRIMARY_COLOR));
-			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_ALPHA)); // alpha replaces color
-			break;
-		case BlendMode::Alpha:
-			// TexColor, TintAlpha
-			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_REPLACE));
-			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE));
+	GL_CALL(glEnable(GL_TEXTURE_2D));
+	GL_CALL(glBindTexture(GL_TEXTURE_2D, _handle));
+	GL_CALL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, surface.w, surface.h, 0, format._format, format._type, surface.getPixels()));
+	if (_withMipmaps)
+		GL_CALL(glGenerateMipmap(GL_TEXTURE_2D));
+	else
+		GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0));
+}
 
-			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_TEXTURE));
-			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR));
-			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_ALPHA, GL_PRIMARY_COLOR));
-			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA));
-			break;
-		case BlendMode::Tinted:
-			// (TintColor * TintAlpha) * TexColor, TexAlpha
-			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE));
-			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE));
+void OpenGLTexture::setMirrorWrap(bool wrap) {
+	if (_mirrorWrap == wrap)
+		return;
+	_mirrorWrap = wrap;
+	GLint wrapMode;
+	if (wrap)
+		wrapMode = OpenGLContext.textureMirrorRepeatSupported ? GL_MIRRORED_REPEAT : GL_REPEAT;
+	else
+		wrapMode = OpenGLContext.textureEdgeClampSupported ? GL_CLAMP_TO_EDGE : GL_CLAMP;
 
-			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_TEXTURE));
-			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR));
-			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_ALPHA, GL_TEXTURE));
-			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA));
+	GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapMode));
+	GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapMode));
+}
 
-			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_PRIMARY_COLOR));
-			GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR)); // we have to pre-multiply
-			break;
-		default:
-			assert(false && "Invalid blend mode");
-			break;
-		}
-		_currentBlendMode = blendMode;
-	}
+OpenGLRenderer::OpenGLRenderer(Point resolution) : _resolution(resolution) {
+	GL_CALL(glDisable(GL_LIGHTING));
+	GL_CALL(glDisable(GL_DEPTH_TEST));
+	GL_CALL(glDisable(GL_SCISSOR_TEST));
+	GL_CALL(glDisable(GL_STENCIL_TEST));
+	GL_CALL(glDisable(GL_CULL_FACE));
+	GL_CALL(glEnable(GL_BLEND));
+	GL_CALL(glDepthMask(GL_FALSE));
 
-	void setLodBias(float lodBias) override {
-		if (abs(_currentLodBias - lodBias) < epsilon)
-			return;
-		GL_CALL(glTexEnvf(GL_TEXTURE_FILTER_CONTROL, GL_TEXTURE_LOD_BIAS, lodBias));
-		_currentLodBias = lodBias;
+	if (!OpenGLContext.NPOTSupported || !OpenGLContext.textureMirrorRepeatSupported) {
+		g_system->messageBox(LogMessageType::kWarning, "Old OpenGL detected, some graphical errors will occur.");
 	}
+}
 
-	void setOutput(Surface &output) override {
-		assert(_isFirstDrawCommand);
-		setViewportToRect(output.w, output.h);
-		_currentOutput = &output;
-
-		// just debug warnings as it will only produce a graphical glitch while
-		// there is some chance the resolution could change from here to ::end
-		// and this is per-frame so maybe don't spam the console with the same message
-
-		if (output.w > g_system->getWidth() || output.h > g_system->getHeight())
-			debugC(0, kDebugGraphics, "Output is larger than screen, output will be cropped (%d, %d) > (%d, %d)",
-				output.w, output.h, g_system->getWidth(), g_system->getHeight());
-
-		auto format = getOpenGLFormatOf(output.format);
-		if (format._format == GL_NONE) {
-			auto formatString = output.format.toString();
-			debugC(0, kDebugGraphics, "Cannot use pixelformat of given output surface: %s", formatString.c_str());
-			_currentOutput = nullptr;
-		}
+ScopedPtr<ITexture> OpenGLRenderer::createTexture(int32 w, int32 h, bool withMipmaps) {
+	assert(w >= 0 && h >= 0);
+	return ScopedPtr<ITexture>(new OpenGLTexture(w, h, withMipmaps));
+}
 
-		if (output.pitch != output.format.bytesPerPixel * output.w) {
-			// Maybe there would be a way with glPixelStore
-			debugC(0, kDebugGraphics, "Incompatible output surface pitch");
-			_currentOutput = nullptr;
-		}
-	}
+void OpenGLRenderer::resetState() {
+	setViewportToScreen();
+	_currentOutput = nullptr;
+	_currentLodBias = -1000.0f;
+	_currentTexture = nullptr;
+	_currentBlendMode = (BlendMode)-1;
+	_isFirstDrawCommand = true;
+}
 
-	bool hasOutput() const override {
-		return _currentOutput != nullptr;
+void OpenGLRenderer::end() {
+	GL_CALL(glFlush());
+
+	if (_currentOutput != nullptr) {
+		g_system->presentBuffer();
+		auto format = getOpenGLFormatOf(_currentOutput->format);
+		GL_CALL(glReadPixels(
+			0,
+			0,
+			_outputSize.x,
+			_outputSize.y,
+			format._format,
+			format._type,
+			_currentOutput->getPixels()
+		));
 	}
+}
 
-	void quad(
-		Vector2d topLeft,
-		Vector2d size,
-		Color color,
-		Angle rotation,
-		Vector2d texMin,
-		Vector2d texMax) override {
-		Vector2d positions[] = {
-			topLeft + Vector2d(0,			0),
-			topLeft + Vector2d(0,			+size.getY()),
-			topLeft + Vector2d(+size.getX(), +size.getY()),
-			topLeft + Vector2d(+size.getX(), 0),
-		};
-		if (abs(rotation.getDegrees()) > epsilon) {
-			const Vector2d zero(0, 0);
-			for (int i = 0; i < 4; i++)
-				positions[i].rotateAround(zero, rotation);
-		}
-
-		Vector2d texCoords[] = {
-			{ texMin.getX(), texMin.getY() },
-			{ texMin.getX(), texMax.getY() },
-			{ texMax.getX(), texMax.getY() },
-			{ texMax.getX(), texMin.getY() }
-		};
-		if (_currentTexture != nullptr) {
-			// float equality is fine here, if it was calculated it was not a normal graphic
-			_currentTexture->setMirrorWrap(texMin != Vector2d() || texMax != Vector2d(1, 1));
-		}
-
-		float colors[] = { color.r / 255.0f, color.g / 255.0f, color.b / 255.0f, color.a / 255.0f };
-		if (_currentBlendMode == BlendMode::Tinted) {
-			colors[0] *= colors[3];
-			colors[1] *= colors[3];
-			colors[2] *= colors[3];
-		}
-
-		checkFirstDrawCommand();
-		GL_CALL(glColor4fv(colors));
-		GL_CALL(glVertexPointer(2, GL_FLOAT, 0, positions));
-		if (_currentTexture != nullptr)
-			GL_CALL(glTexCoordPointer(2, GL_FLOAT, 0, texCoords));
-		GL_CALL(glDrawArrays(GL_QUADS, 0, 4));
-
-#ifdef ALCACHOFA_DEBUG
-		// make sure we crash instead of someone using our stack arrays
-		GL_CALL(glVertexPointer(2, GL_FLOAT, sizeof(Vector2d), nullptr));
-		GL_CALL(glTexCoordPointer(2, GL_FLOAT, sizeof(Vector2d), nullptr));
-#endif
+void OpenGLRenderer::setBlendFunc(BlendMode blendMode) {
+	switch (blendMode) {
+	case BlendMode::AdditiveAlpha:
+		GL_CALL(glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA));
+		break;
+	case BlendMode::Additive:
+		GL_CALL(glBlendFunc(GL_ONE, GL_ONE));
+		break;
+	case BlendMode::Multiply:
+		GL_CALL(glBlendFunc(GL_DST_COLOR, GL_ONE));
+		break;
+	case BlendMode::Alpha:
+		GL_CALL(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
+		break;
+	case BlendMode::Tinted:
+		GL_CALL(glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA));
+		break;
+	default: assert(false && "Invalid blend mode"); break;
 	}
+}
 
-	void debugPolygon(
-		Span<Vector2d> points,
-		Color color
-	) override {
-		checkFirstDrawCommand();
-		setTexture(nullptr);
-		setBlendMode(BlendMode::Alpha);
-		GL_CALL(glVertexPointer(2, GL_FLOAT, 0, points.data()));
-		GL_CALL(glLineWidth(4.0f));
-		GL_CALL(glPointSize(8.0f));
-
-		GL_CALL(glColor4ub(color.r, color.g, color.b, color.a));
-		if (points.size() > 2)
-			GL_CALL(glDrawArrays(GL_POLYGON, 0, points.size()));
-
-		color.a = (byte)(MIN(255.0f, color.a * 1.3f));
-		GL_CALL(glColor4ub(color.r, color.g, color.b, color.a));
-		if (points.size() > 1)
-			GL_CALL(glDrawArrays(GL_LINE_LOOP, 0, points.size()));
-
-		color.a = (byte)(MIN(255.0f, color.a * 1.3f));
-		GL_CALL(glColor4ub(color.r, color.g, color.b, color.a));
-		if (points.size() > 0)
-			GL_CALL(glDrawArrays(GL_POINTS, 0, points.size()));
-	}
+void OpenGLRenderer::setOutput(Surface &output) {
+	assert(_isFirstDrawCommand);
+	setViewportToRect(output.w, output.h);
+	_currentOutput = &output;
 
-	void debugPolyline(
-		Span<Vector2d> points,
-		Color color
-	) override {
-		checkFirstDrawCommand();
-		setTexture(nullptr);
-		setBlendMode(BlendMode::Alpha);
-		GL_CALL(glVertexPointer(2, GL_FLOAT, 0, points.data()));
-		GL_CALL(glLineWidth(4.0f));
-		GL_CALL(glPointSize(8.0f));
+	// just debug warnings as it will only produce a graphical glitch while
+	// there is some chance the resolution could change from here to ::end
+	// and this is per-frame so maybe don't spam the console with the same message
 
-		GL_CALL(glColor4ub(color.r, color.g, color.b, color.a));
-		if (points.size() > 1)
-			GL_CALL(glDrawArrays(GL_LINE_STRIP, 0, points.size()));
+	if (output.w > g_system->getWidth() || output.h > g_system->getHeight())
+		debugC(0, kDebugGraphics, "Output is larger than screen, output will be cropped (%d, %d) > (%d, %d)",
+			output.w, output.h, g_system->getWidth(), g_system->getHeight());
 
-		color.a = (byte)(MIN(255.0f, color.a * 1.3f));
-		GL_CALL(glColor4ub(color.r, color.g, color.b, color.a));
-		if (points.size() > 0)
-			GL_CALL(glDrawArrays(GL_POINTS, 0, points.size()));
+	auto format = getOpenGLFormatOf(output.format);
+	if (format._format == GL_NONE) {
+		auto formatString = output.format.toString();
+		debugC(0, kDebugGraphics, "Cannot use pixelformat of given output surface: %s", formatString.c_str());
+		_currentOutput = nullptr;
 	}
 
-private:
-	void setMatrices(bool flipped) {
-		float bottom = flipped ? _resolution.y : 0.0f;
-		float top = flipped ? 0.0f : _resolution.y;
-
-		GL_CALL(glMatrixMode(GL_PROJECTION));
-		GL_CALL(glLoadIdentity());
-		GL_CALL(glOrtho(0.0f, _resolution.x, bottom, top, -1.0f, 1.0f));
-		GL_CALL(glMatrixMode(GL_MODELVIEW));
-		GL_CALL(glLoadIdentity());
+	if (output.pitch != output.format.bytesPerPixel * output.w) {
+		// Maybe there would be a way with glPixelStore
+		debugC(0, kDebugGraphics, "Incompatible output surface pitch");
+		_currentOutput = nullptr;
 	}
+}
 
-	void setViewportToScreen() {
-		int32 screenWidth = g_system->getWidth();
-		int32 screenHeight = g_system->getHeight();
-		Rect viewport(
-			MIN<int32>(screenWidth, screenHeight * (float)_resolution.x / _resolution.y),
-			MIN<int32>(screenHeight, screenWidth * (float)_resolution.y / _resolution.x));
-		viewport.translate(
-			(screenWidth - viewport.width()) / 2,
-			(screenHeight - viewport.height()) / 2);
+bool OpenGLRenderer::hasOutput() const {
+	return _currentOutput != nullptr;
+}
 
-		GL_CALL(glViewport(viewport.left, viewport.top, viewport.width(), viewport.height()));
-		setMatrices(true);
-	}
+void OpenGLRenderer::setViewportToScreen() {
+	int32 screenWidth = g_system->getWidth();
+	int32 screenHeight = g_system->getHeight();
+	Rect viewport(
+		MIN<int32>(screenWidth, screenHeight * (float)_resolution.x / _resolution.y),
+		MIN<int32>(screenHeight, screenWidth * (float)_resolution.y / _resolution.x));
+	viewport.translate(
+		(screenWidth - viewport.width()) / 2,
+		(screenHeight - viewport.height()) / 2);
+
+	GL_CALL(glViewport(viewport.left, viewport.top, viewport.width(), viewport.height()));
+	setMatrices(true);
+}
 
-	void setViewportToRect(int16 outputWidth, int16 outputHeight) {
-		_outputSize.x = MIN(outputWidth, g_system->getWidth());
-		_outputSize.y = MIN(outputHeight, g_system->getHeight());
-		GL_CALL(glViewport(0, 0, _outputSize.x, _outputSize.y));
-		setMatrices(false);
-	}
+void OpenGLRenderer::setViewportToRect(int16 outputWidth, int16 outputHeight) {
+	_outputSize.x = MIN(outputWidth, g_system->getWidth());
+	_outputSize.y = MIN(outputHeight, g_system->getHeight());
+	GL_CALL(glViewport(0, 0, _outputSize.x, _outputSize.y));
+	setMatrices(false);
+}
 
-	void checkFirstDrawCommand() {
-		// We delay clearing the screen. It is much easier for the game
+void OpenGLRenderer::checkFirstDrawCommand() {
+	// We delay clearing the screen. It is much easier for the game
 		// to switch to a framebuffer before
-		if (!_isFirstDrawCommand)
-			return;
-		_isFirstDrawCommand = false;
-		GL_CALL(glClearColor(0.0f, 0.0f, 0.0f, 1.0f));
-		GL_CALL(glClear(GL_COLOR_BUFFER_BIT));
-	}
-
-	Point _resolution, _outputSize;
-	Surface *_currentOutput = nullptr;
-	OpenGLTexture *_currentTexture = nullptr;
-	BlendMode _currentBlendMode = (BlendMode)-1;
-	float _currentLodBias = 0.0f;
-	bool _isFirstDrawCommand = false;
-};
-
-IRenderer *IRenderer::createOpenGLRenderer(Point resolution) {
-	initGraphics3d(resolution.x, resolution.y);
-	return new OpenGLRenderer(resolution);
+	if (!_isFirstDrawCommand)
+		return;
+	_isFirstDrawCommand = false;
+	GL_CALL(glClearColor(0.0f, 0.0f, 0.0f, 1.0f));
+	GL_CALL(glClear(GL_COLOR_BUFFER_BIT));
 }
 
 }
diff --git a/engines/alcachofa/graphics-opengl.h b/engines/alcachofa/graphics-opengl.h
new file mode 100644
index 00000000000..adafff8a8f9
--- /dev/null
+++ b/engines/alcachofa/graphics-opengl.h
@@ -0,0 +1,82 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef ALCACHOFA_GRAPHICS_OPENGL_H
+#define ALCACHOFA_GRAPHICS_OPENGL_H
+
+#include "alcachofa/graphics.h"
+
+#include "graphics/managed_surface.h"
+#include "graphics/opengl/system_headers.h"
+#include "graphics/opengl/debug.h"
+
+namespace Alcachofa {
+
+struct OpenGLFormat {
+	GLenum _format, _type;
+	inline bool isValid() const { return _format != GL_NONE; }
+};
+
+OpenGLFormat getOpenGLFormatOf(const Graphics::PixelFormat &format);
+
+class OpenGLTexture : public ITexture {
+public:
+	OpenGLTexture(int32 w, int32 h, bool withMipmaps);
+	~OpenGLTexture() override;
+	void update(const Graphics::Surface &surface) override;
+	void setMirrorWrap(bool wrap);
+
+	inline GLuint handle() const { return _handle; }
+
+private:
+	GLuint _handle;
+	bool _withMipmaps;
+	bool _mirrorWrap = true;
+};
+
+class OpenGLRenderer : public virtual IRenderer {
+public:
+	OpenGLRenderer(Common::Point resolution);
+
+	Common::ScopedPtr<ITexture> createTexture(int32 w, int32 h, bool withMipmaps) override;
+	void end() override;
+	void setOutput(Graphics::Surface &output) override;
+	bool hasOutput() const override;
+
+protected:
+	void resetState();
+	void setBlendFunc(BlendMode blendMode); ///< just the blend-func, not texenv/shader uniform
+	void setViewportToScreen();
+	void setViewportToRect(int16 outputWidth, int16 outputHeight);
+	virtual void setMatrices(bool flipped) = 0;
+	void checkFirstDrawCommand();
+
+	Common::Point _resolution, _outputSize;
+	Graphics::Surface *_currentOutput = nullptr;
+	OpenGLTexture *_currentTexture = nullptr;
+	BlendMode _currentBlendMode = (BlendMode)-1;
+	float _currentLodBias = 0.0f;
+	bool _isFirstDrawCommand = false;
+};
+
+}
+
+#endif // ALCACHOFA_GRAPHICS_OPENGL_H
diff --git a/engines/alcachofa/graphics.h b/engines/alcachofa/graphics.h
index a3b0727c8f2..7687084cc48 100644
--- a/engines/alcachofa/graphics.h
+++ b/engines/alcachofa/graphics.h
@@ -95,7 +95,7 @@ public:
 	static IRenderer *createOpenGLRenderer(Common::Point resolution);
 };
 
-class IDebugRenderer : public IRenderer {
+class IDebugRenderer : public virtual IRenderer {
 public:
 	virtual void debugPolygon(
 		Common::Span<Math::Vector2d> points,
diff --git a/engines/alcachofa/module.mk b/engines/alcachofa/module.mk
index 71f29b4833f..4b145a163cc 100644
--- a/engines/alcachofa/module.mk
+++ b/engines/alcachofa/module.mk
@@ -23,6 +23,11 @@ MODULE_OBJS = \
 	sounds.o \
 	ui-objects.o
 
+ifdef USE_OPENGL_GAME
+MODULE_OBJS += \
+	graphics-opengl-classic.o
+endif
+
 
 # This module can be built as a plugin
 ifeq ($(ENABLE_ALCACHOFA), DYNAMIC_PLUGIN)


Commit: 4cd127c9ff2c35bd5f7abb32a1fb0ef00830932c
    https://github.com/scummvm/scummvm/commit/4cd127c9ff2c35bd5f7abb32a1fb0ef00830932c
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:05:01+02:00

Commit Message:
ALCACHOFA: Add OpenGL shaders support for GLES2 platforms

Changed paths:
  A engines/alcachofa/graphics-opengl-shaders.cpp
    engines/alcachofa/graphics-opengl-classic.cpp
    engines/alcachofa/graphics-opengl.cpp
    engines/alcachofa/graphics-opengl.h
    engines/alcachofa/module.mk


diff --git a/engines/alcachofa/graphics-opengl-classic.cpp b/engines/alcachofa/graphics-opengl-classic.cpp
index 578d1e067a9..53e21ce3bc1 100644
--- a/engines/alcachofa/graphics-opengl-classic.cpp
+++ b/engines/alcachofa/graphics-opengl-classic.cpp
@@ -127,24 +127,10 @@ public:
 		Angle rotation,
 		Vector2d texMin,
 		Vector2d texMax) override {
-		Vector2d positions[] = {
-			topLeft + Vector2d(0,			0),
-			topLeft + Vector2d(0,			+size.getY()),
-			topLeft + Vector2d(+size.getX(), +size.getY()),
-			topLeft + Vector2d(+size.getX(), 0),
-		};
-		if (abs(rotation.getDegrees()) > epsilon) {
-			const Vector2d zero(0, 0);
-			for (int i = 0; i < 4; i++)
-				positions[i].rotateAround(zero, rotation);
-		}
+		Vector2d positions[4], texCoords[4];
+		getQuadPositions(topLeft, size, rotation, positions);
+		getQuadTexCoords(texMin, texMax, texCoords);
 
-		Vector2d texCoords[] = {
-			{ texMin.getX(), texMin.getY() },
-			{ texMin.getX(), texMax.getY() },
-			{ texMax.getX(), texMax.getY() },
-			{ texMax.getX(), texMin.getY() }
-		};
 		if (_currentTexture != nullptr) {
 			// float equality is fine here, if it was calculated it was not a normal graphic
 			_currentTexture->setMirrorWrap(texMin != Vector2d() || texMax != Vector2d(1, 1));
@@ -230,8 +216,8 @@ public:
 	}
 };
 
-IRenderer *IRenderer::createOpenGLRenderer(Point resolution) {
-	initGraphics3d(resolution.x, resolution.y);
+IRenderer *createOpenGLRendererClassic(Point resolution) {
+	debug("Use OpenGL classic renderer");
 	return new OpenGLRendererClassic(resolution);
 }
 
diff --git a/engines/alcachofa/graphics-opengl-shaders.cpp b/engines/alcachofa/graphics-opengl-shaders.cpp
new file mode 100644
index 00000000000..c3ff94009bb
--- /dev/null
+++ b/engines/alcachofa/graphics-opengl-shaders.cpp
@@ -0,0 +1,255 @@
+/* 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/graphics-opengl.h"
+#include "alcachofa/detection.h"
+
+#include "common/system.h"
+#include "engines/util.h"
+#include "graphics/opengl/shader.h"
+
+using namespace Common;
+using namespace Math;
+using namespace Graphics;
+using namespace OpenGL;
+
+namespace Alcachofa {
+
+class OpenGLRendererShaders : public OpenGLRenderer {
+	struct Vertex {
+		Vector2d pos;
+		Vector2d uv;
+		Color color;
+	};
+
+	struct VBO {
+		VBO(GLuint bufferId) : _bufferId(bufferId) {}
+		~VBO() {
+			Shader::freeBuffer(_bufferId);
+		}
+
+		GLuint _bufferId;
+		uint _capacity = 0;
+	};
+public:
+	OpenGLRendererShaders(Point resolution)
+		: OpenGLRenderer(resolution) {
+		if (!_shader.loadFromStrings("alcachofa", kVertexShader, kFragmentShader, kAttributes))
+			error("Could not load shader");
+
+		// we use more than one VBO to reduce implicit synchronization
+		for (int i = 0; i < 4; i++)
+			_vbos.emplace_back(Shader::createBuffer(GL_ARRAY_BUFFER, 0, nullptr, GL_STREAM_DRAW));
+
+		_vertices.resize(8 * 6);
+
+		_whiteTexture.reset(new OpenGLTexture(1, 1, false));
+		const byte whiteData[] = { 0xff, 0xff, 0xff, 0xff };
+		GL_CALL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, whiteData));
+	}
+
+	void begin() override {
+		resetState();
+		_needsNewBatch = true;
+	}
+
+	void end() override {
+		checkFirstDrawCommand();
+		OpenGLRenderer::end();
+	}
+
+	void setTexture(ITexture *texture) override {
+		if (texture == _currentTexture)
+			return;
+		_needsNewBatch = true;
+
+		if (texture == nullptr)
+			_currentTexture = nullptr;
+		else {
+			_currentTexture = dynamic_cast<OpenGLTexture *>(texture);
+			assert(_currentTexture != nullptr);
+		}
+	}
+
+	void setBlendMode(BlendMode blendMode) override {
+		if (blendMode == _currentBlendMode)
+			return;
+		_needsNewBatch = true;
+		_currentBlendMode = blendMode;
+	}
+
+	void setLodBias(float lodBias) override {
+		if (abs(_currentLodBias - lodBias) < epsilon)
+			return;
+		_needsNewBatch = true;
+		_currentLodBias = lodBias;
+	}
+
+	void quad(
+		Vector2d topLeft,
+		Vector2d size,
+		Color color,
+		Angle rotation,
+		Vector2d texMin,
+		Vector2d texMax) override {
+		if (_needsNewBatch) {
+			_needsNewBatch = false;
+			checkFirstDrawCommand();
+		}
+
+		if (_currentTexture != nullptr) {
+			// float equality is fine here, if it was calculated it was not a normal graphic
+			_currentTexture->setMirrorWrap(texMin != Vector2d() || texMax != Vector2d(1, 1));
+		}
+
+		Vector2d positions[4], texCoords[4];
+		getQuadPositions(topLeft, size, rotation, positions);
+		getQuadTexCoords(texMin, texMax, texCoords);
+		_vertices.push_back({ positions[0], texCoords[0], color });
+		_vertices.push_back({ positions[1], texCoords[1], color });
+		_vertices.push_back({ positions[2], texCoords[2], color });
+		_vertices.push_back({ positions[0], texCoords[0], color });
+		_vertices.push_back({ positions[2], texCoords[2], color });
+		_vertices.push_back({ positions[3], texCoords[3], color });
+	}
+
+	void setMatrices(bool flipped) override {
+		// adapted from https://en.wikipedia.org/wiki/Orthographic_projection
+		const float left = 0.0f;
+		const float right = _resolution.x;
+		const float bottom = flipped ? _resolution.y : 0.0f;
+		const float top = flipped ? 0.0f : _resolution.y;
+		const float near = -1.0f;
+		const float far = 1.0f;
+
+		_projection.setToIdentity();
+		_projection(0, 0) = 2.0f / (right - left);
+		_projection(1, 1) = 2.0f / (top - bottom);
+		_projection(2, 2) = -2.0f / (far - near);
+		_projection(3, 0) = -(right + left) / (right - left);
+		_projection(3, 1) = -(top + bottom) / (top - bottom);
+		_projection(3, 2) = -(far + near) / (far - near);
+	}
+
+private:
+	void checkFirstDrawCommand() {
+		OpenGLRenderer::checkFirstDrawCommand();
+
+		// submit batch
+		if (!_vertices.empty()) {
+			auto &vbo = _vbos[_curVBO];
+			_curVBO = (_curVBO + 1) % _vbos.size();
+
+			_shader.enableVertexAttribute("in_pos", vbo._bufferId, 2, GL_FLOAT, false, sizeof(Vertex), offsetof(Vertex, pos));
+			_shader.enableVertexAttribute("in_uv", vbo._bufferId, 2, GL_FLOAT, false, sizeof(Vertex), offsetof(Vertex, uv));
+			_shader.enableVertexAttribute("in_color", vbo._bufferId, 4, GL_UNSIGNED_BYTE, true, sizeof(Vertex), offsetof(Vertex, color));
+			_shader.use(true);
+
+			GL_CALL(glBindTexture(GL_TEXTURE_2D, _batchTexture == nullptr
+				? _whiteTexture->handle()
+				: _batchTexture->handle()));
+			GL_CALL(glBindBuffer(GL_ARRAY_BUFFER, vbo._bufferId));
+			if (vbo._capacity < _vertices.size()) {
+				vbo._capacity = _vertices.size();
+				glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex) * _vertices.size(), _vertices.data(), GL_STREAM_DRAW);
+			} else
+				glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(Vertex) * _vertices.size(), _vertices.data());
+			glDrawArrays(GL_TRIANGLES, 0, _vertices.size());
+			_vertices.clear();
+		}
+
+		// setup next batch
+		setBlendFunc(_currentBlendMode);
+		_shader.setUniform("projection", _projection);
+		_shader.setUniform("blendMode", _currentTexture == nullptr ? 5 : (int)_currentBlendMode);
+		_shader.setUniform1f("lodBias", _currentLodBias);
+		_shader.setUniform("texture", 0);
+		_batchTexture = _currentTexture;
+	}
+
+	Matrix4 _projection;
+	Shader _shader;
+	Array<VBO> _vbos;
+	Array<Vertex> _vertices;
+	uint _curVBO = 0;
+	uint _usedTextureUnits = 0;
+	bool _needsNewBatch = false;
+	OpenGLTexture *_batchTexture = nullptr;
+	ScopedPtr<OpenGLTexture> _whiteTexture;
+
+	static constexpr const char *const kAttributes[] = {
+		"in_pos",
+		"in_uv",
+		"in_color",
+		nullptr
+	};
+
+	static constexpr const char *const kVertexShader = R"(
+		uniform mat4 projection;
+
+		attribute vec2 in_pos;
+		attribute vec2 in_uv;
+		attribute vec4 in_color;
+
+		varying vec2 var_uv;
+		varying vec4 var_color;
+
+		void main() {
+			gl_Position = projection * vec4(in_pos, 0.0, 1.0);
+			var_uv = in_uv;
+			var_color = in_color;
+		})";
+
+	static constexpr const char *const kFragmentShader = R"(
+		#ifdef GL_ES
+			precision mediump float;
+		#endif
+
+		uniform sampler2D texture;
+		uniform int blendMode;
+		uniform float lodBias;
+
+		varying vec2 var_uv;
+		varying vec4 var_color;
+
+		void main() {
+			vec4 tex_color = texture2D(texture, var_uv, lodBias);
+			if (blendMode <= 2) { // AdditiveAlpha, Additive and Multiply
+				gl_FragColor.rgb = tex_color.rgb * var_color.a;
+				gl_FragColor.a = tex_color.a;
+			} else if (blendMode == 3) { // Alpha
+				gl_FragColor.rgb = tex_color.rgb;
+				gl_FragColor.a = var_color.a;
+			} else if (blendMode == 4) { // Tinted
+				gl_FragColor.rgb = var_color.rgb * var_color.a * tex_color.rgb;
+				gl_FragColor.a = tex_color.a;
+			} else { // Disabled texture
+				gl_FragColor = var_color;
+			}
+		})";
+};
+
+IRenderer *createOpenGLRendererShaders(Point resolution) {
+	debug("Use OpenGL shaders renderer");
+	return new OpenGLRendererShaders(resolution);
+}
+
+}
diff --git a/engines/alcachofa/graphics-opengl.cpp b/engines/alcachofa/graphics-opengl.cpp
index 8c51e92f1d7..cb288c05155 100644
--- a/engines/alcachofa/graphics-opengl.cpp
+++ b/engines/alcachofa/graphics-opengl.cpp
@@ -24,7 +24,9 @@
 #include "alcachofa/graphics-opengl.h"
 
 #include "common/system.h"
+#include "common/config-manager.h"
 #include "engines/util.h"
+#include "graphics/renderer.h"
 
 using namespace Common;
 using namespace Math;
@@ -33,31 +35,19 @@ using namespace Graphics;
 namespace Alcachofa {
 
 static bool areComponentsInOrder(const PixelFormat &format, int r, int g, int b, int a) {
-	return format == (a < 0
-		? PixelFormat(3, 8, 8, 8, 0, r * 8, g * 8, b * 8, 0)
-		: PixelFormat(4, 8, 8, 8, 8, r * 8, g * 8, b * 8, a * 8));
-}
-
-OpenGLFormat getOpenGLFormatOf(const PixelFormat &format) {
-	if (areComponentsInOrder(format, 0, 1, 2, 3))
-		return { GL_RGBA, GL_UNSIGNED_BYTE };
-	else if (areComponentsInOrder(format, 3, 2, 1, 0))
-		return { GL_RGBA, GL_UNSIGNED_INT_8_8_8_8 };
-	else if (areComponentsInOrder(format, 0, 1, 2, -1))
-		return { GL_RGB, GL_UNSIGNED_BYTE };
-	else if (areComponentsInOrder(format, 2, 1, 0, 3))
-		return { GL_BGRA, GL_UNSIGNED_BYTE };
-	else if (areComponentsInOrder(format, 2, 1, 0, -1))
-		return { GL_BGR, GL_UNSIGNED_BYTE };
-	// we could look for packed formats here as well in the future
-	else
-		return { GL_NONE, GL_NONE };
+	return format == PixelFormat(4, 8, 8, 8, 8, r * 8, g * 8, b * 8, a * 8);
+}
+
+static bool isCompatibleFormat(const PixelFormat &format) {
+	return areComponentsInOrder(format, 0, 1, 2, 3) ||
+		areComponentsInOrder(format, 3, 2, 1, 0);
 }
 
 OpenGLTexture::OpenGLTexture(int32 w, int32 h, bool withMipmaps)
 	: ITexture({ (int16)w, (int16)h })
 	, _withMipmaps(withMipmaps) {
-	GL_CALL(glEnable(GL_TEXTURE_2D));
+	glEnable(GL_TEXTURE_2D); // will error on GLES2, but that is okay
+	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));
@@ -71,17 +61,43 @@ OpenGLTexture::~OpenGLTexture() {
 }
 
 void OpenGLTexture::update(const Surface &surface) {
-	OpenGLFormat format = getOpenGLFormatOf(surface.format);
+	assert(isCompatibleFormat(surface.format));
 	assert(surface.w == size().x && surface.h == size().y);
-	assert(format.isValid());
 
-	GL_CALL(glEnable(GL_TEXTURE_2D));
+	// GLES2 only supports GL_RGBA but we need BlendBlit::getSupportedPixelFormat to use blendBlit
+	// We also do not want to keep surface memory for textures that are not updated repeatedly
+	const void *pixels;
+	if (!areComponentsInOrder(surface.format, 0, 1, 2, 3)) {
+		if (_tmpSurface.empty())
+			_tmpSurface.create(surface.w, surface.h, PixelFormat::createFormatRGBA32());
+		crossBlit(
+			(byte *)_tmpSurface.getPixels(),
+			(const byte *)surface.getPixels(),
+			_tmpSurface.pitch,
+			surface.pitch,
+			surface.w,
+			surface.h,
+			_tmpSurface.format,
+			surface.format);
+		pixels = _tmpSurface.getPixels();
+	} else {
+		glEnable(GL_TEXTURE_2D);
+		OpenGL::clearGLError();
+		pixels = surface.getPixels();
+	}
+
 	GL_CALL(glBindTexture(GL_TEXTURE_2D, _handle));
-	GL_CALL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, surface.w, surface.h, 0, format._format, format._type, surface.getPixels()));
+	GL_CALL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, surface.w, surface.h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels));
 	if (_withMipmaps)
 		GL_CALL(glGenerateMipmap(GL_TEXTURE_2D));
 	else
 		GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0));
+
+	if (!_tmpSurface.empty()) {
+		if (!_didConvertOnce)
+			_tmpSurface.free();
+		_didConvertOnce = true;
+	}
 }
 
 void OpenGLTexture::setMirrorWrap(bool wrap) {
@@ -94,12 +110,13 @@ void OpenGLTexture::setMirrorWrap(bool wrap) {
 	else
 		wrapMode = OpenGLContext.textureEdgeClampSupported ? GL_CLAMP_TO_EDGE : GL_CLAMP;
 
+	GL_CALL(glBindTexture(GL_TEXTURE_2D, _handle));
 	GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapMode));
 	GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapMode));
 }
 
 OpenGLRenderer::OpenGLRenderer(Point resolution) : _resolution(resolution) {
-	GL_CALL(glDisable(GL_LIGHTING));
+	initGraphics3d(resolution.x, resolution.y);
 	GL_CALL(glDisable(GL_DEPTH_TEST));
 	GL_CALL(glDisable(GL_SCISSOR_TEST));
 	GL_CALL(glDisable(GL_STENCIL_TEST));
@@ -131,16 +148,20 @@ void OpenGLRenderer::end() {
 
 	if (_currentOutput != nullptr) {
 		g_system->presentBuffer();
-		auto format = getOpenGLFormatOf(_currentOutput->format);
 		GL_CALL(glReadPixels(
 			0,
 			0,
 			_outputSize.x,
 			_outputSize.y,
-			format._format,
-			format._type,
+			GL_RGBA,
+			GL_UNSIGNED_BYTE,
 			_currentOutput->getPixels()
 		));
+		if (_currentOutput->format != PixelFormat::createFormatRGBA32()) {
+			auto targetFormat = _currentOutput->format;
+			_currentOutput->format = PixelFormat::createFormatRGBA32();
+			_currentOutput->convertToInPlace(targetFormat);
+		}
 	}
 }
 
@@ -178,8 +199,7 @@ void OpenGLRenderer::setOutput(Surface &output) {
 		debugC(0, kDebugGraphics, "Output is larger than screen, output will be cropped (%d, %d) > (%d, %d)",
 			output.w, output.h, g_system->getWidth(), g_system->getHeight());
 
-	auto format = getOpenGLFormatOf(output.format);
-	if (format._format == GL_NONE) {
+	if (!isCompatibleFormat(output.format)) {
 		auto formatString = output.format.toString();
 		debugC(0, kDebugGraphics, "Cannot use pixelformat of given output surface: %s", formatString.c_str());
 		_currentOutput = nullptr;
@@ -227,4 +247,64 @@ void OpenGLRenderer::checkFirstDrawCommand() {
 	GL_CALL(glClear(GL_COLOR_BUFFER_BIT));
 }
 
+void OpenGLRenderer::getQuadPositions(Vector2d topLeft, Vector2d size, Angle rotation, Vector2d positions[]) const {
+	positions[0] = topLeft + Vector2d(0, 0);
+	positions[1] = topLeft + Vector2d(0, +size.getY());
+	positions[2] = topLeft + Vector2d(+size.getX(), +size.getY());
+	positions[3] = topLeft + Vector2d(+size.getX(), 0);
+	if (abs(rotation.getDegrees()) > epsilon) {
+		const Vector2d zero(0, 0);
+		for (int i = 0; i < 4; i++)
+			positions[i].rotateAround(zero, rotation);
+	}
+}
+
+void OpenGLRenderer::getQuadTexCoords(Vector2d texMin, Vector2d texMax, Vector2d texCoords[]) const {
+	texCoords[0] = { texMin.getX(), texMin.getY() };
+	texCoords[1] = { texMin.getX(), texMax.getY() };
+	texCoords[2] = { texMax.getX(), texMax.getY() };
+	texCoords[3] = { texMax.getX(), texMin.getY() };
+}
+
+IRenderer *IRenderer::createOpenGLRenderer(Point resolution) {
+	const auto available = Renderer::getAvailableTypes() & ~kRendererTypeTinyGL;
+	const auto &rendererCode = ConfMan.get("renderer");
+	RendererType rendererType = Renderer::parseTypeCode(rendererCode);
+	rendererType = (RendererType)(rendererType & available);
+
+	IRenderer *renderer = nullptr;
+	switch (rendererType) {
+	case kRendererTypeOpenGLShaders:
+		renderer = createOpenGLRendererShaders(resolution);
+		break;
+	case kRendererTypeOpenGL:
+		renderer = createOpenGLRendererClassic(resolution);
+		break;
+	default:
+		if (available & kRendererTypeOpenGLShaders)
+			renderer = createOpenGLRendererShaders(resolution);
+		else if (available & kRendererTypeOpenGL)
+			renderer = createOpenGLRendererClassic(resolution);
+		break;
+	}
+
+	if (renderer == nullptr)
+		error("Could not create a renderer, GL context type: %d", (int)OpenGLContext.type);
+	return renderer;
+}
+
+#ifndef USE_OPENGL_SHADERS
+IRenderer *createOpenGLRendererShaders(Point _) {
+	(void)_;
+	return nullptr;
+}
+#endif
+
+#ifndef USE_OPENGL_GAME
+IRenderer *createOpenGLRendererClassic(Point _) {
+	(void)_;
+	return nullptr;
+}
+#endif
+
 }
diff --git a/engines/alcachofa/graphics-opengl.h b/engines/alcachofa/graphics-opengl.h
index adafff8a8f9..92c842a4826 100644
--- a/engines/alcachofa/graphics-opengl.h
+++ b/engines/alcachofa/graphics-opengl.h
@@ -30,13 +30,6 @@
 
 namespace Alcachofa {
 
-struct OpenGLFormat {
-	GLenum _format, _type;
-	inline bool isValid() const { return _format != GL_NONE; }
-};
-
-OpenGLFormat getOpenGLFormatOf(const Graphics::PixelFormat &format);
-
 class OpenGLTexture : public ITexture {
 public:
 	OpenGLTexture(int32 w, int32 h, bool withMipmaps);
@@ -50,6 +43,8 @@ private:
 	GLuint _handle;
 	bool _withMipmaps;
 	bool _mirrorWrap = true;
+	bool _didConvertOnce = false;
+	Graphics::ManagedSurface _tmpSurface;
 };
 
 class OpenGLRenderer : public virtual IRenderer {
@@ -68,6 +63,8 @@ protected:
 	void setViewportToRect(int16 outputWidth, int16 outputHeight);
 	virtual void setMatrices(bool flipped) = 0;
 	void checkFirstDrawCommand();
+	void getQuadPositions(Math::Vector2d topLeft, Math::Vector2d size, Math::Angle rotation, Math::Vector2d positions[]) const;
+	void getQuadTexCoords(Math::Vector2d texMin, Math::Vector2d texMax, Math::Vector2d texCoords[]) const;
 
 	Common::Point _resolution, _outputSize;
 	Graphics::Surface *_currentOutput = nullptr;
@@ -77,6 +74,9 @@ protected:
 	bool _isFirstDrawCommand = false;
 };
 
+IRenderer *createOpenGLRendererClassic(Common::Point resolution);
+IRenderer *createOpenGLRendererShaders(Common::Point resolution);
+
 }
 
 #endif // ALCACHOFA_GRAPHICS_OPENGL_H
diff --git a/engines/alcachofa/module.mk b/engines/alcachofa/module.mk
index 4b145a163cc..575ad2fa363 100644
--- a/engines/alcachofa/module.mk
+++ b/engines/alcachofa/module.mk
@@ -28,6 +28,10 @@ MODULE_OBJS += \
 	graphics-opengl-classic.o
 endif
 
+ifdef USE_OPENGL_SHADERS
+MODULE_OBJS += \
+	graphics-opengl-shaders.o
+endif
 
 # This module can be built as a plugin
 ifeq ($(ENABLE_ALCACHOFA), DYNAMIC_PLUGIN)


Commit: 007741ef41d3cce2d9ec09352c6e654f0c6cc3ff
    https://github.com/scummvm/scummvm/commit/007741ef41d3cce2d9ec09352c6e654f0c6cc3ff
Author: Helco (hermann.noll at hotmail.com)
Date: 2025-09-02T22:05:01+02:00

Commit Message:
ALCACHOFA: Fix invalid ODR-usage

Changed paths:
    engines/alcachofa/graphics-opengl-shaders.cpp


diff --git a/engines/alcachofa/graphics-opengl-shaders.cpp b/engines/alcachofa/graphics-opengl-shaders.cpp
index c3ff94009bb..ecec9016e4d 100644
--- a/engines/alcachofa/graphics-opengl-shaders.cpp
+++ b/engines/alcachofa/graphics-opengl-shaders.cpp
@@ -52,6 +52,12 @@ class OpenGLRendererShaders : public OpenGLRenderer {
 public:
 	OpenGLRendererShaders(Point resolution)
 		: OpenGLRenderer(resolution) {
+		static constexpr const char *const kAttributes[] = {
+			"in_pos",
+			"in_uv",
+			"in_color",
+			nullptr
+		};
 		if (!_shader.loadFromStrings("alcachofa", kVertexShader, kFragmentShader, kAttributes))
 			error("Could not load shader");
 
@@ -195,13 +201,6 @@ private:
 	OpenGLTexture *_batchTexture = nullptr;
 	ScopedPtr<OpenGLTexture> _whiteTexture;
 
-	static constexpr const char *const kAttributes[] = {
-		"in_pos",
-		"in_uv",
-		"in_color",
-		nullptr
-	};
-
 	static constexpr const char *const kVertexShader = R"(
 		uniform mat4 projection;
 




More information about the Scummvm-git-logs mailing list