[Scummvm-git-logs] scummvm master -> 111cc673fc7edb49681f4eddd0b2c1cd26b6f3e8
sev-
noreply at scummvm.org
Sun Jun 21 22:23:44 UTC 2026
This automated email contains information about 191 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
9c9bc8afce EEM: initial code with detection
c271f98d82 EEM: image loading and first intro image
f6a6833108 EEM: basic per case flow, with initial code for font loading
8ccba0b347 EEM: correct font rendering using char to glyph table
f349111de1 EEM: use Font API to reduce code duplication
17d95660b8 EEM: completed kCharToGlyph
57dc88dafe EEM: show mouse when selecting character
84a82d5580 EEM: improved initial menu following the original implementation
17288a6014 EEM: update pointer during main menu
d3e517a575 EEM: correct ballon dialog placement
b60f6c5570 EEM: draw the big map and allow player to move between locations
3fe1b2e115 EEM: improved character animations when arriving to locations
30367c4690 EEM: zoom/overview map initial implementation
a4c4170605 EEM: load the correct sites and make sure they are labeled
83ab937183 EEM: show NPC on sites
a5cefd6871 EEM: implement clickable areas on maps
0fe0c95dfd EEM: improved NPC handling
ca825ec5b1 EEM: added more features to the PDA
b36297d2a5 EEM: polish PDA features: buttons now works correctly
7be086ee1b EEM: polish PDA features: help interface added
d075832f54 EEM: polish PDA features: acusations
2ae70bdb92 EEM: basic parners animations
30a356bc07 EEM: more parners animations, for intro and overview map
0f7bf385b7 EEM: refactored the code following the original symbols
7bb7d3b82e EEM: enforce code standards
a4dd19f31d EEM: removed lambdas
2fac0e6820 EEM: removed static and use only EEM namespace
d44daf096d EEM: implemented midi music
ffc4f5135b EEM: implemented digitalized sound
f7b94e5721 EEM: profile handling in save games
df22a74a07 EEM: improved animations and added missing UI features
9c88ee5bc9 EEM: fixed the map animation
ef46d69023 EEM: fixed missing frames in partner animations
d7268b5a91 EEM: better handling of saves
e87d591b96 EEM: remove unused screen
253e2f0bb2 EEM: improve the setup screen with actually triggers to the code relevant
ae83ff8a9f EEM: help screen in setup screen implemented
754e723072 EEM: setup screen accesible in zoom map screen
92c6607481 EEM: improved navigation on PDA
d87214f047 EEM: basic accusation flow in the PDA
3b1738c38b EEM: basic scrapbook code
b4dc4f6274 EEM: added more missing animations
256eff166c EEM: stop music/sound in certain points of the game
fb9922f887 EEM: added missing text bubbles here and there
27d73fdbfe EEM: dedicated code for accusation window
4b19ad71a3 EEM: missing background when accusation resulted in the correct answer
1e6b78b869 EEM: implement wrong accusation
ddb7949f39 EEM: fixed some issues in the win screen/newspaper
3761f15c6e EEM: fixed missing screen updates
f9a5f920da EEM: correctly load game when a mystery is solved
1131e4825f EEM: implemented screen to load mysteries from different books
c3f24c4280 EEM: unlock different scrapbooks until the end
ab8cd8380c EEM: fixed animation issue
62063295f0 EEM: unseen hotspot effect
8fa2ec9202 EEM: make sure sound effect played are correct (or don't play any)
15962d464e EEM: removed debug keys
7a9ad68ef6 EEM: use blit API
0ccaaeb7c4 EEM: initial support for floppy version
a55adf77b7 EEM: implement conversations for the floppy version
3ba1ac49ac EEM: npc conversations for the floppy version
20eba94c78 EEM: improved hints and audio in the floppy version
4992f3c704 EEM: text pagination for some conversations
fe87003b43 EEM: draw enter indicators for conversations in floppy version
bfdd2c33ec EEM: initial support for spanish release and fixes for the floppy version
9c41e30159 EEM: make sure all the clues are there
c5336a89fd EEM: setup screen for floppy screen
021009a600 EEM: correctly validate clues in floppy version
7aa9357fb6 EEM: simplify savegame handling
2b45b48e89 EEM: fix invalid state transition in the handling of the setup screen
1cf324b52d EEM: correctly save and reload viewed clues
ec75e6ec08 EEM: avoid saving slot 0
2a0ec6f7e2 EEM: bring virtual keyboard when typing name
8a575a0b4f EEM: correctly render background during enter animation
dcb37c0a03 EEM: original mouse pointer and new feature to hide highlight boxes
00c6438de3 EEM: correctly blit npc pictures
057711225d EEM: implemented the quit dialog
6ce915b5a9 EEM: implemented the (im)patience animation
3e9ba587e4 EEM: improved scrapbook code
5042edef3a EEM: highlight scrapbook interactive zones
916c34e2b2 EEM: highlight pda interactive zones
3354f1af87 EEM: handle initial clues for the CD version
7ac0106b65 EEM: improved animations
9653bf3792 EEM: do not close the game after the end of the scrapbook
5c5a2d1ccf EEM: honor the music config
a1b4c596bc EEM: added missing voices when selecting clues
6318196285 EEM: don't allow to select more than 5 clues in accusation mode
a0d9c69340 EEM: fixed regression on music handling during enter animation
33e2ff6fda EEM: implemented badge handling when a case is over
1423b3442b EEM: shiny map markers
9e532d8f96 EEM: allow to come back from zoomed map to the overview
ba0b1afbc3 EEM: mt32 music
fd806599d2 EEM: message sliding animation for some screens
6494a01769 EEM: complete handler for screen profile
a1da71fd4d EEM: missing button code to reload the current profile
d3fcc8c249 EEM: implement missing animations for floppy release
2e25ab2f46 EEM: corrected text size
e84bf0e798 EEM: keep proper track of clues in floppy version
821f9a0ba7 EEM: keep proper track of visited places in floppy version
f00c88734c EEM: blit accuse partner in floppy version
f2d1372a85 EEM: use the correct initial menu screens
2398f57221 EEM: refresh the correct part of the screen during case introductions
d00eeb0022 EEM: optional better fitted balloon dialog code
8ca7479633 EEM: only some keys will advance dialog
794eeec90c EEM: strips leading spaces
3a87e8fc48 EEM: refactory doGallery into several functions
b25891a687 EEM: openMainMenuDialog when using ESC in most of the screens
a6c48ae179 EEM: improved animation precision with correct framerate and full script table
28556038df EEM: use simpleBlitFrom
b47044e739 EEM: more simpleBlitFrom
e40f51e2dc EEM: use transBlitFrom
a2554f53bd EEM: stop conversations when you skip the last phrase
9ca6f602c4 EEM: fixed highlighting bug when the cursor was moved during conversations
b39c08a950 EEM: added palette fadeout for the intro
59304750cf EEM: reviewed and improved comments across the engine
37e6dc1ba6 EEM: enforced code standards, mostly removed one-lined statements
c3c78f3e10 EEM: removed lambdas in favor of explicit functions
b8ebea90ad EEM: correct implementation of OpenColorCycle
ad29adf750 EEM: fixed bug when picking up the phone earlier than expected
b69b12c244 EEM: fixed bug in Nancy animation
24a3e5a40c EEM: fixed global-constructors from kHappyZones
12776d655a EEM: removed the 'create new' workaround
613dac165b EEM: improved animation in the PDA
23baebb9a8 EEM: removed incorrect attribution in the headers
d901f38d66 EEM: reduced code duplication using transBlitFrom
e34c624ee4 EEM: reduced code duplication in the PDA rendering
40c2459237 EEM: some comment fixes and clarifications
0a11cfeede EEM: use named constants here and there
9cd879dd5c EEM: removed useless usage of static
f789071470 CI: temporary enable CI in github
ba065aa1a9 CI: reverted temporary enable CI in github commit
8bcc62ad5d EEM: removed duplicated definitions
79188e6bd2 EEM: renamed _ASM_Decompress to avoid confusion
2b9c008c66 EEM: add end of notes in the PDA rendering
c9a5bbc56c EEM: add an option to skip repeated cases
f2c5be745a EEM: make sure screenshots contain all the elements from sites
24fe380eed EEM: fixed invalid reference to a sound clip in practice mystery
32e5d42d20 EEM: implemented restored content from the floppy data into the CD one
02418babc4 EEM: fixes and missing code for the floppy release
558a00c546 EEM: use skip api instead of (void)
209d590655 EEM: moved cleanMysterySound after sdx/sdb declarations
a789df41d3 EEM: reduced some technical comments
c63d19aea9 EEM: remove dead code related with fallback pointer
79bf202d8c EEM: boy -> jake, girl -> jenny
8f54582d7d EEM: EEM2 proof-of-concept (intro)
ff0025ab96 EEM: Added a more complete intro and character selection for EEM2
3fb2a3ea44 EEM: load case menu is working in EEM2
a477a58ccf EEM: add more code in the partner selection screen
e69c835219 EEM: corrected palette
29bef8261a EEM: play correctly initial animation in London
24d75f3cc7 EEM: training case in London plays renders the first site
ea2453f9bc EEM: improved animation in London
dd0a8761aa EEM: added more animation from London
a5d0ea9233 EEM: mouse cursor change from London
27f48984c6 EEM: jump effect from applyClue in London
17cf0205f9 EEM: correct parner handling in London
b8f606891d EEM: implemented traveling animation in London
279d74a541 EEM: implemented edutaintment intro video of some locations in London
6ddfb2227b EEM: fix big map markers in London
a35a1dc2d5 EEM: implement player creation/loading in London
a252c9afa9 EEM: proper selection of partner in London
ad1a941360 EEM: proper case selection in London
3f0cccf90a EEM: more side effects for clues in London
66393f6f85 EEM: implement game progression in London
7ca4b9f330 EEM: added missing musical cue in London
53af04e55c EEM: added missing conversation opcodes in London
076d0f0633 EEM: improved TRAVIS UI in London
1f0ad1bf77 EEM: improved hints voices/text in London
d7a7f62671 EEM: new doPuzzle implementation for London
a029ae2241 EEM: doPuzzle highlights for London
c53f9e018e EEM: acusation workflow for London
aca47008bd EEM: doApproach fixes for London
b749d688e8 EEM: enter the name screen fixes for London
f490186308 EEM: make sure the screen is updated during animations
e9c44065c0 EEM: added missing NPC in London cases intro
2372aaf018 EEM: implement setup screen in London
44e61cbf7f EEM: fixed regression on clean background for London
a79edd41f2 EEM: missing music cue in London scrapbook
3555a18bdb EEM: reduce the verbosity of the comments accross the engine
215d297ebd EEM: enable fit dialog ballons for London
670bb5b820 EEM: fixed partner animation that was one frame longer in London
3b86f3eb9a EEM: do not render sublocations in London map
c021df15c9 EEM: added missing music in London
1234998c14 EEM: corrected palette for the water in London map
0e72f80441 EEM: autosave every time we talk or get a new clue in London
846a1b5452 EEM: show notes for each suspect in the gallery correctly in London
8d9720ef74 EEM: deduction flow fixes for London
cfed4b9586 EEM: play the correct animation before showing the scrapbook in London
1161071df3 EEM: concurrent partner animations during conversations
ce99471d14 EEM: improved partner animations reducing flickering
24173e3211 EEM: properly mark interactive zones in London
111cc673fc EEM: completed implementation for setup screen in London
Commit: 9c9bc8afce4d8d965e00ebe085f194fd4c5d2b03
https://github.com/scummvm/scummvm/commit/9c9bc8afce4d8d965e00ebe085f194fd4c5d2b03
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:32+02:00
Commit Message:
EEM: initial code with detection
Changed paths:
A engines/eem/POTFILES
A engines/eem/configure.engine
A engines/eem/console.cpp
A engines/eem/console.h
A engines/eem/detection.cpp
A engines/eem/detection.h
A engines/eem/eem.cpp
A engines/eem/eem.h
A engines/eem/metaengine.cpp
A engines/eem/module.mk
diff --git a/engines/eem/POTFILES b/engines/eem/POTFILES
new file mode 100644
index 00000000000..df6847f5e2b
--- /dev/null
+++ b/engines/eem/POTFILES
@@ -0,0 +1 @@
+engines/eem/metaengine.cpp
diff --git a/engines/eem/configure.engine b/engines/eem/configure.engine
new file mode 100644
index 00000000000..fdf8aa7aeed
--- /dev/null
+++ b/engines/eem/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] [components]
+add_engine eem "Eagle Eye Mysteries" no
diff --git a/engines/eem/console.cpp b/engines/eem/console.cpp
new file mode 100644
index 00000000000..80501e6da06
--- /dev/null
+++ b/engines/eem/console.cpp
@@ -0,0 +1,29 @@
+/* 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 "eem/console.h"
+
+namespace EEM {
+
+Console::Console() : GUI::Debugger() {
+}
+
+} // End of namespace EEM
diff --git a/engines/eem/console.h b/engines/eem/console.h
new file mode 100644
index 00000000000..04c06b19959
--- /dev/null
+++ b/engines/eem/console.h
@@ -0,0 +1,37 @@
+/* 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 EEM_CONSOLE_H
+#define EEM_CONSOLE_H
+
+#include "gui/debugger.h"
+
+namespace EEM {
+
+class Console : public GUI::Debugger {
+public:
+ Console();
+ ~Console() override {}
+};
+
+} // End of namespace EEM
+
+#endif
diff --git a/engines/eem/detection.cpp b/engines/eem/detection.cpp
new file mode 100644
index 00000000000..0666827a3db
--- /dev/null
+++ b/engines/eem/detection.cpp
@@ -0,0 +1,83 @@
+/* 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 "engines/advancedDetector.h"
+
+#include "eem/detection.h"
+
+namespace EEM {
+
+static const PlainGameDescriptor eemGames[] = {
+ { "eem", "Eagle Eye Mysteries" },
+ { nullptr, nullptr }
+};
+
+const ADGameDescription gameDescriptions[] = {
+ {
+ "eem",
+ "CD",
+ AD_ENTRY2s("EEMCD.EXE", "286d586113863ceb0e12df5b36fd8504", 272608,
+ "PICS.DBD", "cc05ec256cd5a04df9019e9aebfb7c32", 994057),
+ Common::EN_ANY,
+ Common::kPlatformDOS,
+ ADGF_NO_FLAGS,
+ GUIO1(GUIO_NONE)
+ },
+
+ AD_TABLE_END_MARKER
+};
+
+static const DebugChannelDef debugFlagList[] = {
+ { kDebugGeneral, "general", "General debug" },
+ { kDebugScript, "script", "Script execution" },
+ { kDebugMystery, "mystery", "Mystery loading and state" },
+ { kDebugSite, "site", "Site rendering and hot-spots" },
+ { kDebugGfx, "gfx", "Graphics, palette, animations" },
+ { kDebugSound, "sound", "Sound and music" },
+ DEBUG_CHANNEL_END
+};
+
+} // End of namespace EEM
+
+class EEMMetaEngineDetection : public AdvancedMetaEngineDetection<ADGameDescription> {
+public:
+ EEMMetaEngineDetection() : AdvancedMetaEngineDetection(EEM::gameDescriptions, EEM::eemGames) {
+ }
+
+ const char *getName() const override {
+ return "eem";
+ }
+
+ const char *getEngineName() const override {
+ return "Eagle Eye Mysteries";
+ }
+
+ const char *getOriginalCopyright() const override {
+ return "Eagle Eye Mysteries (C) 1994 EA Kids";
+ }
+
+ const DebugChannelDef *getDebugChannels() const override {
+ return EEM::debugFlagList;
+ }
+};
+
+REGISTER_PLUGIN_STATIC(EEM_DETECTION, PLUGIN_TYPE_ENGINE_DETECTION, EEMMetaEngineDetection);
diff --git a/engines/eem/detection.h b/engines/eem/detection.h
new file mode 100644
index 00000000000..395daab1c77
--- /dev/null
+++ b/engines/eem/detection.h
@@ -0,0 +1,42 @@
+/* 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 EEM_DETECTION_H
+#define EEM_DETECTION_H
+
+#include "engines/advancedDetector.h"
+
+namespace EEM {
+
+enum EEMDebugChannels {
+ kDebugGeneral = 1 << 0,
+ kDebugScript = 1 << 1,
+ kDebugMystery = 1 << 2,
+ kDebugSite = 1 << 3,
+ kDebugGfx = 1 << 4,
+ kDebugSound = 1 << 5
+};
+
+extern const ADGameDescription gameDescriptions[];
+
+} // End of namespace EEM
+
+#endif
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
new file mode 100644
index 00000000000..365c73fa389
--- /dev/null
+++ b/engines/eem/eem.cpp
@@ -0,0 +1,108 @@
+/* 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/debug.h"
+#include "common/debug-channels.h"
+#include "common/error.h"
+#include "common/events.h"
+#include "common/system.h"
+
+#include "engines/util.h"
+
+#include "graphics/paletteman.h"
+
+#include "eem/console.h"
+#include "eem/detection.h"
+#include "eem/eem.h"
+
+namespace EEM {
+
+EEMEngine::EEMEngine(OSystem *syst, const ADGameDescription *gameDesc)
+ : Engine(syst), _gameDescription(gameDesc), _console(nullptr),
+ _rng("eem"), _lastScreen(kScreenInvalid), _nextScreen(kScreenTitle) {
+}
+
+EEMEngine::~EEMEngine() {
+ // _console is owned by the Engine base class.
+}
+
+Common::Error EEMEngine::run() {
+ // Original _main @ 1a35:0f59 enters mode 13h via _SetMode13X (320x200x256).
+ initGraphics(320, 200);
+
+ _console = new Console();
+ setDebugger(_console);
+
+ // _main's startup paints the screen black via _AllBlack @ 172b:0d4b before
+ // the first screen handler runs; we do the same here.
+ byte palette[3 * 256] = { 0 };
+ g_system->getPaletteManager()->setPalette(palette, 0, 256);
+
+ debugC(1, kDebugGeneral, "EEM engine starting; first screen = 0x%02X", _nextScreen);
+
+ screenDriver();
+
+ debugC(1, kDebugGeneral, "EEM engine exiting");
+ return Common::kNoError;
+}
+
+void EEMEngine::screenDriver() {
+ // Mirrors _ScreenDriver @ 1a35:0dc1. The original walks a 14-entry table at
+ // 1a35:0e5e of (id, handler) pairs; we use a switch as we port handlers in.
+ while (_nextScreen != kScreenInvalid && !shouldQuit()) {
+ ScreenId next = static_cast<ScreenId>(_nextScreen);
+ switch (next) {
+ case kScreenTitle:
+ // TODO(M3): port _ShowTitlePage @ 1a35:06b7
+ warning("Screen 0x%02X (title) not implemented yet", next);
+ _lastScreen = _nextScreen;
+ _nextScreen = kScreenInvalid;
+ break;
+ default:
+ warning("Unknown screen id 0x%02X; exiting", next);
+ _nextScreen = kScreenInvalid;
+ break;
+ }
+
+ // Until handlers run their own event loops, pump events here so the
+ // engine remains responsive and the user can quit.
+ if (!pollEvents())
+ break;
+ }
+}
+
+bool EEMEngine::pollEvents() {
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ switch (event.type) {
+ case Common::EVENT_QUIT:
+ case Common::EVENT_RETURN_TO_LAUNCHER:
+ return false;
+ default:
+ break;
+ }
+ }
+ g_system->updateScreen();
+ g_system->delayMillis(10);
+ return true;
+}
+
+} // End of namespace EEM
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
new file mode 100644
index 00000000000..ac8d6b14931
--- /dev/null
+++ b/engines/eem/eem.h
@@ -0,0 +1,81 @@
+/* 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/>.
+ *
+ * Based on the original engine code by EA Kids / Storm Software (1994).
+ *
+ */
+
+#ifndef EEM_EEM_H
+#define EEM_EEM_H
+
+#include "common/platform.h"
+#include "common/random.h"
+#include "common/scummsys.h"
+
+#include "engines/advancedDetector.h"
+#include "engines/engine.h"
+
+namespace EEM {
+
+class Console;
+
+/**
+ * Screen IDs used by the original ScreenDriver dispatch table at 1a35:0e5e.
+ * The table holds 14 (id, handler) entries; the loop iterates until it finds
+ * a matching id and calls its handler. ID 0xFFFF is the exit sentinel.
+ */
+enum ScreenId {
+ kScreenInvalid = 0xFFFF,
+ kScreenTitle = 0x0B, ///< _ShowTitlePage @ 1a35:06b7
+ kScreenNext = 0x08 ///< follow-up after title (case selection); to be confirmed
+};
+
+class EEMEngine : public Engine {
+public:
+ EEMEngine(OSystem *syst, const ADGameDescription *gameDesc);
+ ~EEMEngine() override;
+
+ Common::Error run() override;
+
+ const char *getGameId() const;
+ Common::Platform getPlatform() const;
+
+ const ADGameDescription *_gameDescription;
+
+private:
+ /**
+ * Central dispatch loop matching the original _ScreenDriver @ 1a35:0dc1.
+ * Each iteration restores video mode and calls the screen handler that
+ * matches _nextScreen. Handlers update _lastScreen / _nextScreen and
+ * return; the loop exits when _nextScreen == kScreenInvalid.
+ */
+ void screenDriver();
+
+ bool pollEvents();
+
+ Console *_console;
+ Common::RandomSource _rng;
+
+ uint16 _lastScreen; ///< Mirrors _LastScreen @ 2d5d:3f24
+ uint16 _nextScreen; ///< Mirrors _NextScreen @ 2d5d:3f26
+};
+
+} // End of namespace EEM
+
+#endif
diff --git a/engines/eem/metaengine.cpp b/engines/eem/metaengine.cpp
new file mode 100644
index 00000000000..0d61cf6a944
--- /dev/null
+++ b/engines/eem/metaengine.cpp
@@ -0,0 +1,59 @@
+/* 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 "engines/advancedDetector.h"
+
+#include "eem/eem.h"
+
+namespace EEM {
+
+const char *EEMEngine::getGameId() const {
+ return _gameDescription->gameId;
+}
+
+Common::Platform EEMEngine::getPlatform() const {
+ return _gameDescription->platform;
+}
+
+} // End of namespace EEM
+
+class EEMMetaEngine : public AdvancedMetaEngine<ADGameDescription> {
+public:
+ const char *getName() const override {
+ return "eem";
+ }
+
+ Common::Error createInstance(OSystem *syst, Engine **engine, const ADGameDescription *desc) const override {
+ *engine = new EEM::EEMEngine(syst, desc);
+ return Common::kNoError;
+ }
+
+ bool hasFeature(MetaEngineFeature f) const override {
+ return false;
+ }
+};
+
+#if PLUGIN_ENABLED_DYNAMIC(EEM)
+REGISTER_PLUGIN_DYNAMIC(EEM, PLUGIN_TYPE_ENGINE, EEMMetaEngine);
+#else
+REGISTER_PLUGIN_STATIC(EEM, PLUGIN_TYPE_ENGINE, EEMMetaEngine);
+#endif
diff --git a/engines/eem/module.mk b/engines/eem/module.mk
new file mode 100644
index 00000000000..792a53959ed
--- /dev/null
+++ b/engines/eem/module.mk
@@ -0,0 +1,17 @@
+MODULE := engines/eem
+
+MODULE_OBJS = \
+ console.o \
+ eem.o \
+ metaengine.o
+
+# This module can be built as a plugin
+ifeq ($(ENABLE_EEM), DYNAMIC_PLUGIN)
+PLUGIN := 1
+endif
+
+# Include common rules
+include $(srcdir)/rules.mk
+
+# Detection objects
+DETECT_OBJS += $(MODULE)/detection.o
Commit: c271f98d82c3cd62abf9b2f4d16b43c22eeaefb5
https://github.com/scummvm/scummvm/commit/c271f98d82c3cd62abf9b2f4d16b43c22eeaefb5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:32+02:00
Commit Message:
EEM: image loading and first intro image
Changed paths:
A engines/eem/resource.cpp
A engines/eem/resource.h
engines/eem/console.cpp
engines/eem/console.h
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/module.mk
diff --git a/engines/eem/console.cpp b/engines/eem/console.cpp
index 80501e6da06..bddd6e98fac 100644
--- a/engines/eem/console.cpp
+++ b/engines/eem/console.cpp
@@ -20,10 +20,34 @@
*/
#include "eem/console.h"
+#include "eem/eem.h"
+#include "eem/resource.h"
namespace EEM {
-Console::Console() : GUI::Debugger() {
+Console::Console(EEMEngine *vm) : GUI::Debugger(), _vm(vm) {
+ registerCmd("pic", WRAP_METHOD(Console, cmdPic));
+}
+
+bool Console::cmdPic(int argc, const char **argv) {
+ if (argc < 2) {
+ debugPrintf("Usage: pic <number>\n");
+ debugPrintf("Loads picture <number> from PICS.DBD and reports its dimensions.\n");
+ debugPrintf("PICS.DBX has %u entries.\n", _vm->getPics().size());
+ return true;
+ }
+
+ const uint num = (uint)atoi(argv[1]);
+ Picture pic;
+ if (!_vm->getPics().getPicture(num, pic)) {
+ debugPrintf("pic %u: load failed\n", num);
+ return true;
+ }
+
+ debugPrintf("pic %u (idx %u): %dx%d, compsize=%u, flags=0x%04x, miscflags=0x%04x, rowoff=%u\n",
+ num, num - 1, pic.surface.w, pic.surface.h,
+ pic.compsize, pic.flags, pic.miscflags, pic.rowoff);
+ return true;
}
} // End of namespace EEM
diff --git a/engines/eem/console.h b/engines/eem/console.h
index 04c06b19959..e3c89cdd54c 100644
--- a/engines/eem/console.h
+++ b/engines/eem/console.h
@@ -26,10 +26,17 @@
namespace EEM {
+class EEMEngine;
+
class Console : public GUI::Debugger {
public:
- Console();
+ explicit Console(EEMEngine *vm);
~Console() override {}
+
+private:
+ EEMEngine *_vm;
+
+ bool cmdPic(int argc, const char **argv);
};
} // End of namespace EEM
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 365c73fa389..a58748773e3 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -23,7 +23,10 @@
#include "common/debug-channels.h"
#include "common/error.h"
#include "common/events.h"
+#include "common/file.h"
+#include "common/path.h"
#include "common/system.h"
+#include "common/textconsole.h"
#include "engines/util.h"
@@ -33,6 +36,11 @@
#include "eem/detection.h"
#include "eem/eem.h"
+namespace {
+const uint kPalSize = 768; ///< 256 colors * 3 bytes
+const uint kNumSitePals = 40; ///< SITEPALS holds 40 palettes (40 * 768 = 30720)
+} // anonymous namespace
+
namespace EEM {
EEMEngine::EEMEngine(OSystem *syst, const ADGameDescription *gameDesc)
@@ -48,7 +56,7 @@ Common::Error EEMEngine::run() {
// Original _main @ 1a35:0f59 enters mode 13h via _SetMode13X (320x200x256).
initGraphics(320, 200);
- _console = new Console();
+ _console = new Console(this);
setDebugger(_console);
// _main's startup paints the screen black via _AllBlack @ 172b:0d4b before
@@ -56,8 +64,60 @@ Common::Error EEMEngine::run() {
byte palette[3 * 256] = { 0 };
g_system->getPaletteManager()->setPalette(palette, 0, 256);
+ // Mirrors _main's `_picsFile = _fopen("PICS.DBD", ...)` plus
+ // _InitGraphicsSystem's PICS.DBX index parse (172b:0145).
+ if (!_picsArchive.open(Common::Path("PICS.DBD"), Common::Path("PICS.DBX"))) {
+ return Common::Error(Common::kReadingFailed, "PICS archive missing");
+ }
+
+ // Mirrors _ReadPalettes @ 172b:0d89 â slurp SITEPALS in one read.
+ Common::File palFile;
+ if (!palFile.open(Common::Path("SITEPALS"))) {
+ return Common::Error(Common::kReadingFailed, "SITEPALS missing");
+ }
+ _sitePals.resize(palFile.size());
+ if (palFile.read(_sitePals.data(), _sitePals.size()) != _sitePals.size()) {
+ return Common::Error(Common::kReadingFailed, "SITEPALS short read");
+ }
+ palFile.close();
+ debugC(1, kDebugGfx, "Loaded %u SITEPALS palettes", (uint)(_sitePals.size() / kPalSize));
+
debugC(1, kDebugGeneral, "EEM engine starting; first screen = 0x%02X", _nextScreen);
+ // Show the first intro image (EA Kids logo) â mirrors the opening of
+ // _ShowEAKids @ 2520:05f0: GetPicture(0x54), MemoryCopy to 0xa000:0,
+ // GetPalette(0x25), setmany(_fpal, 0). Skipped: color-cycle loop.
+ {
+ Picture eakids;
+ if (!_picsArchive.getPicture(0x54, eakids)) {
+ return Common::Error(Common::kReadingFailed, "EA Kids logo (picture #0x54) load failed");
+ }
+ debugC(1, kDebugGfx, "EA Kids logo: %dx%d", eakids.surface.w, eakids.surface.h);
+ blitFullScreen(eakids);
+ setSitePalette(0x25);
+ g_system->updateScreen();
+
+ // Hold the image for up to 3 s or until the user clicks/keys/quits.
+ const uint32 startMs = g_system->getMillis();
+ while (g_system->getMillis() - startMs < 3000) {
+ Common::Event event;
+ bool stop = false;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ if (event.type == Common::EVENT_QUIT ||
+ event.type == Common::EVENT_RETURN_TO_LAUNCHER ||
+ event.type == Common::EVENT_LBUTTONDOWN ||
+ event.type == Common::EVENT_KEYDOWN) {
+ stop = true;
+ break;
+ }
+ }
+ if (stop)
+ break;
+ g_system->updateScreen();
+ g_system->delayMillis(10);
+ }
+ }
+
screenDriver();
debugC(1, kDebugGeneral, "EEM engine exiting");
@@ -89,6 +149,28 @@ void EEMEngine::screenDriver() {
}
}
+void EEMEngine::setSitePalette(uint num) {
+ if (num >= kNumSitePals || _sitePals.size() < (num + 1) * kPalSize) {
+ warning("setSitePalette: index %u out of range", num);
+ return;
+ }
+ // SITEPALS stores 6-bit VGA-DAC values (0..63); ScummVM expects 8-bit
+ // (0..255), so left-shift by 2 like the original VGA hardware did.
+ const byte *src = _sitePals.data() + num * kPalSize;
+ byte expanded[kPalSize];
+ for (uint i = 0; i < kPalSize; i++) {
+ expanded[i] = (byte)(src[i] << 2);
+ }
+ g_system->getPaletteManager()->setPalette(expanded, 0, 256);
+}
+
+void EEMEngine::blitFullScreen(const Picture &pic) {
+ // _MemoryCopy(0, 0xa000, srcOff, srcSeg) in _ShowEAKids dumps the picture
+ // straight into VGA's 320x200 framebuffer.
+ g_system->copyRectToScreen(pic.surface.getPixels(), pic.surface.pitch,
+ 0, 0, pic.surface.w, pic.surface.h);
+}
+
bool EEMEngine::pollEvents() {
Common::Event event;
while (g_system->getEventManager()->pollEvent(event)) {
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index ac8d6b14931..67a4b0967db 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -31,6 +31,8 @@
#include "engines/advancedDetector.h"
#include "engines/engine.h"
+#include "eem/resource.h"
+
namespace EEM {
class Console;
@@ -58,6 +60,18 @@ public:
const ADGameDescription *_gameDescription;
+ DBDArchive &getPics() { return _picsArchive; }
+
+ /**
+ * Upload palette index @p num (one of 40 stored in SITEPALS) to the
+ * screen, with the VGA-DAC 6-bit-to-8-bit shift. Mirrors _GetPalette
+ * @ 172b:0e80 followed by _setmany @ 1000:0930.
+ */
+ void setSitePalette(uint num);
+
+ /** Blit @p pic to the screen at (0,0), expecting a 320x200 picture. */
+ void blitFullScreen(const Picture &pic);
+
private:
/**
* Central dispatch loop matching the original _ScreenDriver @ 1a35:0dc1.
@@ -72,6 +86,9 @@ private:
Console *_console;
Common::RandomSource _rng;
+ DBDArchive _picsArchive; ///< PICS.DBD/.DBX (mouse, buttons, markers, balloons sprites)
+ Common::Array<byte> _sitePals; ///< 40 x 768 bytes of 6-bit VGA palettes from SITEPALS
+
uint16 _lastScreen; ///< Mirrors _LastScreen @ 2d5d:3f24
uint16 _nextScreen; ///< Mirrors _NextScreen @ 2d5d:3f26
};
diff --git a/engines/eem/module.mk b/engines/eem/module.mk
index 792a53959ed..34247658998 100644
--- a/engines/eem/module.mk
+++ b/engines/eem/module.mk
@@ -3,7 +3,8 @@ MODULE := engines/eem
MODULE_OBJS = \
console.o \
eem.o \
- metaengine.o
+ metaengine.o \
+ resource.o
# This module can be built as a plugin
ifeq ($(ENABLE_EEM), DYNAMIC_PLUGIN)
diff --git a/engines/eem/resource.cpp b/engines/eem/resource.cpp
new file mode 100644
index 00000000000..67c9ab8a78e
--- /dev/null
+++ b/engines/eem/resource.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 "common/compression/dcl.h"
+#include "common/debug.h"
+#include "common/file.h"
+#include "common/textconsole.h"
+
+#include "graphics/pixelformat.h"
+
+#include "eem/detection.h"
+#include "eem/resource.h"
+
+namespace EEM {
+
+DBDArchive::DBDArchive() {
+}
+
+DBDArchive::~DBDArchive() {
+ close();
+}
+
+bool DBDArchive::open(const Common::Path &dbdName, const Common::Path &dbxName) {
+ close();
+
+ if (!_dbd.open(dbdName)) {
+ warning("DBDArchive: cannot open %s", dbdName.toString().c_str());
+ return false;
+ }
+
+ Common::File dbx;
+ if (!dbx.open(dbxName)) {
+ warning("DBDArchive: cannot open %s", dbxName.toString().c_str());
+ _dbd.close();
+ return false;
+ }
+
+ // _InitGraphicsSystem @ 172b:0145 reads 10 bytes per entry until EOF.
+ const int32 dbxSize = dbx.size();
+ _index.reserve(dbxSize / 10);
+ while (dbx.pos() + 10 <= dbxSize) {
+ DBEntry entry;
+ entry.offset = dbx.readUint32LE();
+ entry.compressed = dbx.readUint16LE();
+ entry.size = dbx.readUint32LE();
+ _index.push_back(entry);
+ }
+ dbx.close();
+
+ debugC(1, kDebugGfx, "DBDArchive: opened %s (%u entries)",
+ dbdName.toString().c_str(), _index.size());
+ return true;
+}
+
+void DBDArchive::close() {
+ if (_dbd.isOpen())
+ _dbd.close();
+ _index.clear();
+}
+
+bool DBDArchive::loadEntry(uint num, Picture &out) {
+ if (num >= _index.size()) {
+ warning("DBDArchive::loadEntry: %u out of range (max %u)", num, _index.size());
+ return false;
+ }
+
+ const DBEntry &entry = _index[num];
+ if (!_dbd.seek(entry.offset)) {
+ warning("DBDArchive::loadEntry: seek to 0x%08x failed", entry.offset);
+ return false;
+ }
+
+ // Mirrors _GetFromDB @ 172b:105d:
+ // _fread(i) // 2-byte skip word (purpose unclear; always 0x0001)
+ // _fread(pic, 1, 12) // 12-byte header
+ (void)_dbd.readUint16LE();
+ out.flags = _dbd.readUint16LE();
+ const uint16 height = _dbd.readUint16LE();
+ const uint16 width = _dbd.readUint16LE();
+ out.rowoff = _dbd.readUint16LE();
+ out.miscflags = _dbd.readUint16LE();
+ out.compsize = _dbd.readUint16LE();
+
+ if (width == 0 || height == 0) {
+ warning("DBDArchive::loadEntry: %u has zero dimensions (%ux%u)",
+ num, width, height);
+ return false;
+ }
+
+ out.surface.create(width, height, Graphics::PixelFormat::createFormatCLUT8());
+
+ if (entry.compressed == 0) {
+ // Raw pixel data â read width*height bytes verbatim.
+ const uint32 pixelCount = (uint32)width * (uint32)height;
+ if (_dbd.read(out.surface.getPixels(), pixelCount) != pixelCount) {
+ warning("DBDArchive::loadEntry: short raw read on %u", num);
+ return false;
+ }
+ return true;
+ }
+
+ // Compressed payload: feed the .DBD stream straight into the DCL
+ // decoder, matching the pattern used by Neverhood's BLB archive.
+ const uint32 unpacked = (uint32)width * (uint32)height;
+ if (!Common::decompressDCL(&_dbd, (byte *)out.surface.getPixels(),
+ out.compsize, unpacked)) {
+ warning("DBDArchive::loadEntry: DCL decompression failed on %u "
+ "(%u packed -> %u pixels)", num, out.compsize, unpacked);
+ return false;
+ }
+ return true;
+}
+
+} // End of namespace EEM
diff --git a/engines/eem/resource.h b/engines/eem/resource.h
new file mode 100644
index 00000000000..a48deb85518
--- /dev/null
+++ b/engines/eem/resource.h
@@ -0,0 +1,106 @@
+/* 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 EEM_RESOURCE_H
+#define EEM_RESOURCE_H
+
+#include "common/array.h"
+#include "common/file.h"
+#include "common/path.h"
+#include "common/scummsys.h"
+
+#include "graphics/managed_surface.h"
+
+namespace EEM {
+
+/**
+ * Index entry for a .DBD/.DBX archive pair (10 bytes on disk).
+ *
+ * Mirrors the original `dbi` struct read by _InitGraphicsSystem @ 172b:0145
+ * in 10-byte chunks until EOF. Each entry locates one resource blob in the
+ * companion .DBD container.
+ */
+struct DBEntry {
+ uint32 offset; ///< Byte offset of the entry in the .DBD file.
+ uint16 compressed; ///< Non-zero if the payload is PKWARE DCL ("Implode") packed.
+ uint32 size; ///< Total size of the entry on disk (including 14-byte header).
+};
+
+/**
+ * 8-bit indexed picture decoded from a .DBD entry.
+ *
+ * The original engine's PicData is a 16-byte struct; we keep the descriptive
+ * fields here and let `Graphics::ManagedSurface` own the pixel data so the
+ * rest of the engine can blit/scale/clip with the standard API.
+ */
+struct Picture {
+ uint16 flags = 0; ///< +0 high byte = sub-mode used by some sprites
+ uint16 rowoff = 0; ///< +6 row offset (used by some clipped sprites)
+ uint16 miscflags = 0; ///< +8 high byte = transparent-mask flag
+ uint16 compsize = 0; ///< +10 packed payload size on disk
+ Graphics::ManagedSurface surface;
+};
+
+/**
+ * Reader for a .DBD + .DBX archive pair.
+ *
+ * The original engine has five such pairs: PICS, SITES, ANI, BALLOON, BUTTON.
+ * Each .DBX is parsed once into an in-memory `_index`; reads of individual
+ * entries seek into the .DBD on demand and (when flagged) decompress with
+ * `Common::decompressDCL`.
+ */
+class DBDArchive {
+public:
+ DBDArchive();
+ ~DBDArchive();
+
+ /**
+ * Open both halves of an archive. @p dbdName / @p dbxName are looked up
+ * via SearchMan, so case is normalized for us. Returns false if either
+ * file is missing or the index is malformed.
+ */
+ bool open(const Common::Path &dbdName, const Common::Path &dbxName);
+ void close();
+
+ /** Number of entries in the index. */
+ uint32 size() const { return _index.size(); }
+
+ /**
+ * Load entry @p num (0-based index), decompressing if needed.
+ * Returns true on success. Mirrors _GetFromDB @ 172b:105d.
+ */
+ bool loadEntry(uint num, Picture &out);
+
+ /**
+ * Convenience wrapper that mirrors the engine's 1-based picture API:
+ * `_GetPicture(num)` calls `_GetFromDB(..., num - 1)`. Use this when
+ * porting code that references picture IDs by their original number.
+ */
+ bool getPicture(uint num, Picture &out) { return loadEntry(num - 1, out); }
+
+private:
+ Common::File _dbd;
+ Common::Array<DBEntry> _index;
+};
+
+} // End of namespace EEM
+
+#endif
Commit: f6a6833108d61f79ddf03d743d9be80cc1d40a88
https://github.com/scummvm/scummvm/commit/f6a6833108d61f79ddf03d743d9be80cc1d40a88
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:32+02:00
Commit Message:
EEM: basic per case flow, with initial code for font loading
Changed paths:
A engines/eem/animation.cpp
A engines/eem/animation.h
A engines/eem/font.cpp
A engines/eem/font.h
A engines/eem/mystery.cpp
A engines/eem/mystery.h
A engines/eem/site.cpp
A engines/eem/site.h
R engines/eem/console.cpp
R engines/eem/console.h
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/metaengine.cpp
engines/eem/module.mk
engines/eem/resource.cpp
engines/eem/resource.h
diff --git a/engines/eem/animation.cpp b/engines/eem/animation.cpp
new file mode 100644
index 00000000000..929fc18025e
--- /dev/null
+++ b/engines/eem/animation.cpp
@@ -0,0 +1,162 @@
+/* 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/debug.h"
+#include "common/textconsole.h"
+
+#include "eem/animation.h"
+#include "eem/detection.h"
+
+namespace EEM {
+
+void asmDecompress(const byte *src, uint srcSize, byte *dst, uint dstSize) {
+ const byte *srcEnd = src + srcSize;
+ byte *dstEnd = dst + dstSize;
+
+ while (dst < dstEnd && src < srcEnd) {
+ const byte op = *src++;
+
+ if (op == 0x80) {
+ // Extended opcode: read 16-bit length argument.
+ if (src + 2 > srcEnd)
+ break;
+ const uint16 n = (uint16)src[0] | ((uint16)src[1] << 8);
+ src += 2;
+
+ if ((n & 0x8000) == 0) {
+ // Skip n bytes â preserves previous-frame pixels.
+ dst += n;
+ } else if ((n & 0x4000) == 0) {
+ // Long literal copy: (n & 0x7FFF) bytes from src to dst.
+ uint16 cnt = n & 0x7FFF;
+ while (cnt-- && dst < dstEnd && src < srcEnd)
+ *dst++ = *src++;
+ } else {
+ // Long fill: (n & 0x3FFF) bytes with the next single byte.
+ if (src >= srcEnd)
+ break;
+ const byte val = *src++;
+ uint16 cnt = n & 0x3FFF;
+ while (cnt-- && dst < dstEnd)
+ *dst++ = val;
+ }
+ } else if (op == 0) {
+ // Short fill: byte count, then byte value.
+ if (src + 2 > srcEnd)
+ break;
+ const byte cnt = src[0];
+ const byte val = src[1];
+ src += 2;
+ for (byte i = 0; i < cnt && dst < dstEnd; i++)
+ *dst++ = val;
+ } else if ((op & 0x80) == 0) {
+ // Short literal copy: `op` bytes (1..0x7F) from src to dst.
+ byte cnt = op;
+ while (cnt-- && dst < dstEnd && src < srcEnd)
+ *dst++ = *src++;
+ } else {
+ // Short skip: (op & 0x7F) bytes â preserves previous-frame pixels.
+ dst += op & 0x7F;
+ }
+ }
+}
+
+bool ANMDecoder::open(const Common::Path &path) {
+ close();
+
+ if (!_file.open(path)) {
+ warning("ANMDecoder: cannot open %s", path.toString().c_str());
+ return false;
+ }
+
+ if (_file.read(_palette, sizeof(_palette)) != sizeof(_palette)) {
+ warning("ANMDecoder: short palette read on %s", path.toString().c_str());
+ close();
+ return false;
+ }
+
+ _frameCount = _file.readUint16LE();
+ if (_frameCount == 0 || _frameCount > 1024) {
+ warning("ANMDecoder: implausible frame count %u in %s",
+ _frameCount, path.toString().c_str());
+ close();
+ return false;
+ }
+
+ (void)_file.readUint16LE(); // header[+0]: ignored
+ _height = _file.readUint16LE(); // header[+2]
+ _width = _file.readUint16LE(); // header[+4]
+ (void)_file.readUint16LE(); // header[+6]
+ (void)_file.readUint16LE(); // header[+8]
+ (void)_file.readUint16LE(); // header[+10]
+
+ if (_width == 0 || _height == 0) {
+ warning("ANMDecoder: zero dimensions in %s", path.toString().c_str());
+ close();
+ return false;
+ }
+
+ _lengths.resize(_frameCount);
+ for (uint16 i = 0; i < _frameCount; i++)
+ _lengths[i] = _file.readUint16LE();
+
+ _buffer.resize((uint32)_width * _height);
+ memset(_buffer.data(), 0, _buffer.size());
+ _nextFrameIdx = 0;
+
+ debugC(1, kDebugGfx, "ANMDecoder: %s opened, %u frames, %ux%u",
+ path.toString().c_str(), _frameCount, _width, _height);
+ return true;
+}
+
+void ANMDecoder::close() {
+ if (_file.isOpen())
+ _file.close();
+ _lengths.clear();
+ _buffer.clear();
+ _packed.clear();
+ _frameCount = _width = _height = _nextFrameIdx = 0;
+}
+
+void ANMDecoder::getPalette8(byte *out) const {
+ for (uint i = 0; i < sizeof(_palette); i++)
+ out[i] = (byte)(_palette[i] << 2);
+}
+
+const byte *ANMDecoder::nextFrame() {
+ if (_nextFrameIdx >= _frameCount)
+ return nullptr;
+
+ const uint16 packedSize = _lengths[_nextFrameIdx];
+ if (_packed.size() < packedSize)
+ _packed.resize(packedSize);
+
+ if (_file.read(_packed.data(), packedSize) != packedSize) {
+ warning("ANMDecoder: short read on frame %u", _nextFrameIdx);
+ return nullptr;
+ }
+
+ asmDecompress(_packed.data(), packedSize, _buffer.data(), _buffer.size());
+ _nextFrameIdx++;
+ return _buffer.data();
+}
+
+} // End of namespace EEM
diff --git a/engines/eem/animation.h b/engines/eem/animation.h
new file mode 100644
index 00000000000..9397872e2fd
--- /dev/null
+++ b/engines/eem/animation.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 EEM_ANIMATION_H
+#define EEM_ANIMATION_H
+
+#include "common/array.h"
+#include "common/file.h"
+#include "common/path.h"
+#include "common/scummsys.h"
+
+namespace EEM {
+
+/**
+ * Decoder for the engine's full-screen difference animations.
+ *
+ * Used for `BOLT.ANM`, `TITLE.ANM`, `ANIM01.A` .. `ANIM20.A`. The format is
+ * documented by Load_Sequence @ 2503:0006 / OpenDifferenceAnimation @ 2520:0337:
+ *
+ * - 0x300 bytes : 6-bit VGA palette
+ * - u16 : frame count
+ * - 12 bytes : header (height @ +2, width @ +4, rest unused)
+ * - frames*u16 : packed length per frame
+ * - per frame : `lengths[i]` bytes of RLE-packed delta data
+ *
+ * Each packed frame is unpacked by the custom `_ASM_Decompress` RLE
+ * (1000:0953) into the persistent `_buffer`; skip opcodes preserve pixels
+ * from the previous frame, which is how the difference encoding works.
+ */
+class ANMDecoder {
+public:
+ ANMDecoder() = default;
+ ~ANMDecoder() { close(); }
+
+ /// Open @p path and parse the file header. Returns false on error.
+ bool open(const Common::Path &path);
+
+ /// Release the open file and the persistent frame buffer.
+ void close();
+
+ /// Number of frames in the animation.
+ uint16 frameCount() const { return _frameCount; }
+ uint16 width() const { return _width; }
+ uint16 height() const { return _height; }
+
+ /// 768 bytes of 6-bit VGA palette (already shifted into 8-bit on getPalette8).
+ const byte *palette6() const { return _palette; }
+
+ /// Fill @p out (768 bytes) with the 8-bit-shifted palette. Convenience.
+ void getPalette8(byte *out) const;
+
+ /**
+ * Decode the next frame in place into the internal buffer. Returns a
+ * pointer to the @c width()*@c height() byte image, or nullptr at EOF.
+ */
+ const byte *nextFrame();
+
+private:
+ Common::File _file;
+ Common::Array<uint16> _lengths;
+ Common::Array<byte> _buffer; ///< Persistent unpacked frame, width*height bytes.
+ Common::Array<byte> _packed; ///< Per-frame scratch packed buffer.
+
+ byte _palette[768] = {};
+ uint16 _frameCount = 0;
+ uint16 _width = 0;
+ uint16 _height = 0;
+ uint16 _nextFrameIdx = 0;
+};
+
+/**
+ * Decompress a single frame's RLE payload in place. Mirrors _ASM_Decompress
+ * @ 1000:0953 byte-for-byte. @p dst already holds the previous frame; skip
+ * opcodes leave those pixels untouched.
+ */
+void asmDecompress(const byte *src, uint srcSize, byte *dst, uint dstSize);
+
+} // End of namespace EEM
+
+#endif
diff --git a/engines/eem/console.cpp b/engines/eem/console.cpp
deleted file mode 100644
index bddd6e98fac..00000000000
--- a/engines/eem/console.cpp
+++ /dev/null
@@ -1,53 +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/>.
- *
- */
-
-#include "eem/console.h"
-#include "eem/eem.h"
-#include "eem/resource.h"
-
-namespace EEM {
-
-Console::Console(EEMEngine *vm) : GUI::Debugger(), _vm(vm) {
- registerCmd("pic", WRAP_METHOD(Console, cmdPic));
-}
-
-bool Console::cmdPic(int argc, const char **argv) {
- if (argc < 2) {
- debugPrintf("Usage: pic <number>\n");
- debugPrintf("Loads picture <number> from PICS.DBD and reports its dimensions.\n");
- debugPrintf("PICS.DBX has %u entries.\n", _vm->getPics().size());
- return true;
- }
-
- const uint num = (uint)atoi(argv[1]);
- Picture pic;
- if (!_vm->getPics().getPicture(num, pic)) {
- debugPrintf("pic %u: load failed\n", num);
- return true;
- }
-
- debugPrintf("pic %u (idx %u): %dx%d, compsize=%u, flags=0x%04x, miscflags=0x%04x, rowoff=%u\n",
- num, num - 1, pic.surface.w, pic.surface.h,
- pic.compsize, pic.flags, pic.miscflags, pic.rowoff);
- return true;
-}
-
-} // End of namespace EEM
diff --git a/engines/eem/console.h b/engines/eem/console.h
deleted file mode 100644
index e3c89cdd54c..00000000000
--- a/engines/eem/console.h
+++ /dev/null
@@ -1,44 +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 EEM_CONSOLE_H
-#define EEM_CONSOLE_H
-
-#include "gui/debugger.h"
-
-namespace EEM {
-
-class EEMEngine;
-
-class Console : public GUI::Debugger {
-public:
- explicit Console(EEMEngine *vm);
- ~Console() override {}
-
-private:
- EEMEngine *_vm;
-
- bool cmdPic(int argc, const char **argv);
-};
-
-} // End of namespace EEM
-
-#endif
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index a58748773e3..4bd0da9430a 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -32,159 +32,1821 @@
#include "graphics/paletteman.h"
-#include "eem/console.h"
#include "eem/detection.h"
#include "eem/eem.h"
+#include "eem/site.h"
+
+#include "common/config-manager.h"
+#include "common/savefile.h"
+#include "common/serializer.h"
+
+#include "graphics/managed_surface.h"
+
+namespace EEM {
namespace {
const uint kPalSize = 768; ///< 256 colors * 3 bytes
const uint kNumSitePals = 40; ///< SITEPALS holds 40 palettes (40 * 768 = 30720)
-} // anonymous namespace
-namespace EEM {
+// Picture / palette IDs from the original code (1-based picture IDs).
+const uint kPicEAKidsLogo = 0x54; ///< _ShowEAKids: GetPicture(0x54)
+const uint kPicHighScoreLogo = 0x20c; ///< _ShowHScoreLogo: GetPicture(0x20c)
+const uint kPicChooseBackground = 0x8c; ///< _DoChoosePartner: GetBackground(0x8c)
+const uint kPalEAKids = 0x25;
+const uint kPalHighScore = 0x27;
+
+// Animation IDs (0-based per ANI.DBX). _DoChoosePartner uses GetAnimation(8/9).
+const uint kAniBoy = 8;
+const uint kAniGirl = 9;
+
+// On-screen positions for the boy and girl partner sprites, from
+// _DoChoosePartner: NewAnimation(0xe2, 0x62, ...) and (0x42, 0x60, ...).
+const int kBoyX = 0xe2; // 226
+const int kBoyY = 0x62; // 98
+const int kGirlX = 0x42; // 66
+const int kGirlY = 0x60; // 96
+} // anonymous namespace
EEMEngine::EEMEngine(OSystem *syst, const ADGameDescription *gameDesc)
- : Engine(syst), _gameDescription(gameDesc), _console(nullptr),
- _rng("eem"), _lastScreen(kScreenInvalid), _nextScreen(kScreenTitle) {
+ : Engine(syst), _gameDescription(gameDesc), _rng("eem"),
+ _playerName("Detective"),
+ _lastScreen(kScreenInvalid), _nextScreen(kScreenTitle), _partner(0) {
}
EEMEngine::~EEMEngine() {
- // _console is owned by the Engine base class.
}
Common::Error EEMEngine::run() {
- // Original _main @ 1a35:0f59 enters mode 13h via _SetMode13X (320x200x256).
+ // _SetMode13X @ 1000:0358 enters VGA mode 13h (320x200x256).
initGraphics(320, 200);
- _console = new Console(this);
- setDebugger(_console);
+ if (!openArchives())
+ return Common::Error(Common::kReadingFailed, "EEM archive open failed");
- // _main's startup paints the screen black via _AllBlack @ 172b:0d4b before
- // the first screen handler runs; we do the same here.
- byte palette[3 * 256] = { 0 };
- g_system->getPaletteManager()->setPalette(palette, 0, 256);
+ if (!loadSitePalettes())
+ return Common::Error(Common::kReadingFailed, "SITEPALS load failed");
+
+ // _LoadFont @ 1b66:023c â main 8 px bitmap font.
+ if (!_font.load(Common::Path("FONT.FNT")))
+ warning("FONT.FNT failed to load; text will not render");
- // Mirrors _main's `_picsFile = _fopen("PICS.DBD", ...)` plus
- // _InitGraphicsSystem's PICS.DBX index parse (172b:0145).
+ // _AllBlack @ 172b:0d4b paints the screen black before the first handler.
+ byte black[3 * 256] = { 0 };
+ g_system->getPaletteManager()->setPalette(black, 0, 256);
+
+ debugC(1, kDebugGeneral, "EEM engine starting");
+
+ // If the user chose "Load" before pressing Play, the framework
+ // invokes `loadGameState` which sets up `_mystery` and `_partner`.
+ // Honour that by skipping the intros and going straight to the
+ // loaded mystery's site loop.
+ const int wantedSave = ConfMan.hasKey("save_slot")
+ ? ConfMan.getInt("save_slot") : -1;
+ if (wantedSave >= 0) {
+ const Common::Error err = loadGameState(wantedSave);
+ if (err.getCode() == Common::kNoError && _mystery.isLoaded()) {
+ debugC(1, kDebugGeneral, "Resuming from slot %d at mystery %u",
+ wantedSave, _mystery.number());
+ doInitClues();
+ doSiteLoop();
+ while (!shouldQuit()) {
+ doCaseSelection();
+ if (!_mystery.isLoaded()) break;
+ doInitClues();
+ doSiteLoop();
+ }
+ return Common::kNoError;
+ }
+ }
+
+ // Reproduces _DoOpeningAnims @ 2520:082a (sans audio):
+ // EA Kids logo (PIC) -> HighScore Productions logo (PIC) ->
+ // Storm Software logo (BOLT.ANM) -> 20 character-intro animations
+ // (ANIM01.A .. ANIM20.A) -> TITLE.ANM. Each can be skipped with a
+ // click or any key.
+ showEAKidsLogo();
+ if (!shouldQuit())
+ showHighScoreLogo();
+ if (!shouldQuit())
+ playAnm(Common::Path("BOLT.ANM"));
+ for (int i = 1; i <= 20 && !shouldQuit(); i++) {
+ Common::String name = Common::String::format("ANIM%02d.A", i);
+ playAnm(Common::Path(name));
+ // Between anims the original plays a voice clip via _SpoolSound;
+ // without audio we still want a beat so each scene reads.
+ if (!shouldQuit() && i != 20)
+ waitForInput(2000);
+ }
+ if (!shouldQuit())
+ playAnm(Common::Path("TITLE.ANM"), 120, /*holdLastFrame=*/true);
+
+ // After the title chain, the original goes Title (B) -> screen 8
+ // (NewPlayer / saved-record selection) -> screen 9 (ChoosePartner) ->
+ // screen A (CaseSelection) -> site loop. We mirror the same order.
+ if (!shouldQuit())
+ doNewPlayer();
+ if (!shouldQuit())
+ doChoosePartner();
+ if (!shouldQuit())
+ doCaseSelection();
+ if (!shouldQuit() && _mystery.isLoaded()) {
+ // Mark the starting site as active and display the case briefing.
+ // `_DoInitClues` @ 1a35:0411 â case briefing.
+ doInitClues();
+ doSiteLoop();
+
+ // After a case, loop back to CaseSelection.
+ while (!shouldQuit()) {
+ doCaseSelection();
+ if (!_mystery.isLoaded())
+ break;
+ doInitClues();
+ doSiteLoop();
+ }
+ }
+
+ debugC(1, kDebugGeneral, "EEM engine exiting");
+ return Common::kNoError;
+}
+
+bool EEMEngine::openArchives() {
+ // _InitGraphicsSystem @ 172b:0145 opens these five .DBD/.DBX pairs.
if (!_picsArchive.open(Common::Path("PICS.DBD"), Common::Path("PICS.DBX"))) {
- return Common::Error(Common::kReadingFailed, "PICS archive missing");
+ warning("PICS archive missing");
+ return false;
+ }
+ if (!_aniArchive.open(Common::Path("ANI.DBD"), Common::Path("ANI.DBX"))) {
+ warning("ANI archive missing");
+ return false;
}
+ // SITES + BALLOON are optional for the boot path but needed for site
+ // rendering and clue display.
+ if (!_sitesArchive.open(Common::Path("SITES.DBD"), Common::Path("SITES.DBX")))
+ warning("SITES archive missing â site backgrounds disabled");
+ if (!_balloonArchive.open(Common::Path("BALLOON.DBD"), Common::Path("BALLOON.DBX")))
+ warning("BALLOON archive missing â clue text will lack balloons");
+ return true;
+}
- // Mirrors _ReadPalettes @ 172b:0d89 â slurp SITEPALS in one read.
- Common::File palFile;
- if (!palFile.open(Common::Path("SITEPALS"))) {
- return Common::Error(Common::kReadingFailed, "SITEPALS missing");
+bool EEMEngine::loadSitePalettes() {
+ Common::File f;
+ if (!f.open(Common::Path("SITEPALS"))) {
+ warning("SITEPALS missing");
+ return false;
}
- _sitePals.resize(palFile.size());
- if (palFile.read(_sitePals.data(), _sitePals.size()) != _sitePals.size()) {
- return Common::Error(Common::kReadingFailed, "SITEPALS short read");
+ _sitePals.resize(f.size());
+ if (f.read(_sitePals.data(), _sitePals.size()) != _sitePals.size()) {
+ warning("SITEPALS short read");
+ return false;
}
- palFile.close();
- debugC(1, kDebugGfx, "Loaded %u SITEPALS palettes", (uint)(_sitePals.size() / kPalSize));
+ debugC(1, kDebugGfx, "Loaded %u SITEPALS palettes",
+ (uint)(_sitePals.size() / kPalSize));
+ return true;
+}
- debugC(1, kDebugGeneral, "EEM engine starting; first screen = 0x%02X", _nextScreen);
+void EEMEngine::setSitePalette(uint num) {
+ if (num >= kNumSitePals || _sitePals.size() < (num + 1) * kPalSize) {
+ warning("setSitePalette: index %u out of range", num);
+ return;
+ }
+ // SITEPALS stores 6-bit VGA-DAC values (0..63); ScummVM expects 8-bit
+ // (0..255), so left-shift by 2 like the original VGA hardware did.
+ const byte *src = _sitePals.data() + num * kPalSize;
+ byte expanded[kPalSize];
+ for (uint i = 0; i < kPalSize; i++)
+ expanded[i] = (byte)(src[i] << 2);
+ g_system->getPaletteManager()->setPalette(expanded, 0, 256);
+}
- // Show the first intro image (EA Kids logo) â mirrors the opening of
- // _ShowEAKids @ 2520:05f0: GetPicture(0x54), MemoryCopy to 0xa000:0,
- // GetPalette(0x25), setmany(_fpal, 0). Skipped: color-cycle loop.
- {
- Picture eakids;
- if (!_picsArchive.getPicture(0x54, eakids)) {
- return Common::Error(Common::kReadingFailed, "EA Kids logo (picture #0x54) load failed");
- }
- debugC(1, kDebugGfx, "EA Kids logo: %dx%d", eakids.surface.w, eakids.surface.h);
- blitFullScreen(eakids);
- setSitePalette(0x25);
+bool EEMEngine::setAnmPalette(const Common::Path &anmPath) {
+ Common::File f;
+ if (!f.open(anmPath)) {
+ warning("setAnmPalette: cannot open %s", anmPath.toString().c_str());
+ return false;
+ }
+ byte raw[kPalSize];
+ if (f.read(raw, kPalSize) != kPalSize) {
+ warning("setAnmPalette: short read on %s", anmPath.toString().c_str());
+ return false;
+ }
+ byte expanded[kPalSize];
+ for (uint i = 0; i < kPalSize; i++)
+ expanded[i] = (byte)(raw[i] << 2);
+ g_system->getPaletteManager()->setPalette(expanded, 0, 256);
+ return true;
+}
+
+void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs, bool holdLastFrame) {
+ ANMDecoder anm;
+ if (!anm.open(path)) {
+ warning("playAnm: %s missing", path.toString().c_str());
+ return;
+ }
+
+ byte palette[768];
+ anm.getPalette8(palette);
+ g_system->getPaletteManager()->setPalette(palette, 0, 256);
+
+ const uint16 w = anm.width();
+ const uint16 h = anm.height();
+
+ while (!shouldQuit()) {
+ const byte *frame = anm.nextFrame();
+ if (!frame)
+ break;
+
+ g_system->copyRectToScreen(frame, w, 0, 0, w, h);
g_system->updateScreen();
- // Hold the image for up to 3 s or until the user clicks/keys/quits.
- const uint32 startMs = g_system->getMillis();
- while (g_system->getMillis() - startMs < 3000) {
+ // Drain events and let the user skip with click/key. The original
+ // uses _CheckFrameRate / _kbhit; we use a simple fixed delay until
+ // the frame-rate calibration logic from _GetSpeedRating is wired up.
+ const uint32 frameStart = g_system->getMillis();
+ bool aborted = false;
+ while (g_system->getMillis() - frameStart < frameDelayMs && !aborted) {
Common::Event event;
- bool stop = false;
while (g_system->getEventManager()->pollEvent(event)) {
if (event.type == Common::EVENT_QUIT ||
event.type == Common::EVENT_RETURN_TO_LAUNCHER ||
event.type == Common::EVENT_LBUTTONDOWN ||
event.type == Common::EVENT_KEYDOWN) {
- stop = true;
+ aborted = true;
+ break;
+ }
+ }
+ g_system->delayMillis(5);
+ }
+ if (aborted)
+ break;
+ }
+
+ if (holdLastFrame && !shouldQuit()) {
+ // Mirror the wait-loop at the end of `_DoOpeningAnims`:
+ // while (!keyDataAvailable) ;
+ // We accept either a click or a key.
+ while (!shouldQuit()) {
+ Common::Event ev;
+ bool clicked = false;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER ||
+ ev.type == Common::EVENT_LBUTTONDOWN ||
+ ev.type == Common::EVENT_KEYDOWN) {
+ clicked = true;
break;
}
}
- if (stop)
+ if (clicked)
break;
g_system->updateScreen();
- g_system->delayMillis(10);
+ g_system->delayMillis(20);
}
}
+}
- screenDriver();
+void EEMEngine::blitAt(const Picture &pic, int x, int y) {
+ // Clip against the 320x200 frame buffer.
+ const int w = MIN<int>(pic.surface.w, 320 - x);
+ const int h = MIN<int>(pic.surface.h, 200 - y);
+ if (w <= 0 || h <= 0)
+ return;
+ g_system->copyRectToScreen(pic.surface.getPixels(), pic.surface.pitch,
+ x, y, w, h);
+}
- debugC(1, kDebugGeneral, "EEM engine exiting");
- return Common::kNoError;
+void EEMEngine::waitForInput(uint32 maxMs) {
+ const uint32 startMs = g_system->getMillis();
+ while (!shouldQuit() && (g_system->getMillis() - startMs < maxMs)) {
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ if (event.type == Common::EVENT_QUIT ||
+ event.type == Common::EVENT_RETURN_TO_LAUNCHER ||
+ event.type == Common::EVENT_LBUTTONDOWN ||
+ event.type == Common::EVENT_KEYDOWN) {
+ return;
+ }
+ }
+ g_system->updateScreen();
+ g_system->delayMillis(10);
+ }
}
-void EEMEngine::screenDriver() {
- // Mirrors _ScreenDriver @ 1a35:0dc1. The original walks a 14-entry table at
- // 1a35:0e5e of (id, handler) pairs; we use a switch as we port handlers in.
- while (_nextScreen != kScreenInvalid && !shouldQuit()) {
- ScreenId next = static_cast<ScreenId>(_nextScreen);
- switch (next) {
- case kScreenTitle:
- // TODO(M3): port _ShowTitlePage @ 1a35:06b7
- warning("Screen 0x%02X (title) not implemented yet", next);
- _lastScreen = _nextScreen;
- _nextScreen = kScreenInvalid;
+void EEMEngine::showEAKidsLogo() {
+ // Mirrors _ShowEAKids @ 2520:05f0 (without the color-cycle loop):
+ // GetPicture(0x54), MemoryCopy to VGA, GetPalette(0x25), setmany.
+ Picture pic;
+ if (!_picsArchive.getPicture(kPicEAKidsLogo, pic)) {
+ warning("EA Kids logo (%u) load failed", kPicEAKidsLogo);
+ return;
+ }
+ blitAt(pic, 0, 0);
+ setSitePalette(kPalEAKids);
+ g_system->updateScreen();
+ waitForInput(2500);
+}
+
+void EEMEngine::showHighScoreLogo() {
+ // Mirrors _ShowHScoreLogo @ 2520:0799 (without the wait-loop):
+ // GetPicture(0x20c), MemoryCopy to VGA, GetPalette(0x27), FadeIn.
+ Picture pic;
+ if (!_picsArchive.getPicture(kPicHighScoreLogo, pic)) {
+ warning("HighScore logo (%u) load failed", kPicHighScoreLogo);
+ return;
+ }
+ blitAt(pic, 0, 0);
+ setSitePalette(kPalHighScore);
+ g_system->updateScreen();
+ waitForInput(2500);
+}
+
+void EEMEngine::doNewPlayer() {
+ // Mirrors `_NewPlayer` @ 1c33:0dda. The original draws background
+ // 0x104 + character peek pic 0x107, then shows "Please type your
+ // name" and accepts up to 12 characters until Enter. We render a
+ // minimal version: black screen + prompt.
+ if (!_font.isLoaded()) {
+ _playerName = "Detective";
+ return;
+ }
+
+ Common::String name;
+ const int maxChars = 12;
+
+ // Mirror the original: load PIC 0x104 as the name-entry backdrop.
+ // The original also slides in PIC 0x107 (a peeking character).
+ Picture bg;
+ const bool haveBG = _picsArchive.getPicture(0x104, bg);
+
+ auto draw = [&]() {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ if (haveBG) {
+ const int w = MIN<int>(bg.surface.w, 320);
+ const int h = MIN<int>(bg.surface.h, 200);
+ for (int row = 0; row < h; row++)
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)bg.surface.getBasePtr(0, row), w);
+ }
+ _font.drawString(&scratch, 40, 24,
+ "Welcome to Eagle Eye Mysteries!", 0xF);
+ _font.drawString(&scratch, 40, 40, "Please type your name:", 0xF);
+ _font.drawString(&scratch, 40, 60,
+ "(Backspace to delete, Enter to confirm)", 0xF);
+ Common::String shown = name + "_";
+ _font.drawString(&scratch, 40, 90, shown, 0xF);
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ };
+ draw();
+
+ while (!shouldQuit()) {
+ Common::Event ev;
+ bool dirty = false;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return;
+ if (ev.type != Common::EVENT_KEYDOWN)
+ continue;
+ const Common::KeyCode k = ev.kbd.keycode;
+ if (k == Common::KEYCODE_RETURN) {
+ if (name.empty())
+ name = "Detective";
+ _playerName = name;
+ return;
+ }
+ if (k == Common::KEYCODE_ESCAPE) {
+ _playerName = "Detective";
+ return;
+ }
+ if (k == Common::KEYCODE_BACKSPACE) {
+ if (!name.empty()) {
+ name.deleteLastChar();
+ dirty = true;
+ }
+ continue;
+ }
+ if (ev.kbd.ascii >= ' ' && ev.kbd.ascii < 127 &&
+ (int)name.size() < maxChars) {
+ name += (char)ev.kbd.ascii;
+ dirty = true;
+ }
+ }
+ if (dirty)
+ draw();
+ g_system->delayMillis(15);
+ }
+}
+
+void EEMEngine::doChoosePartner() {
+ // Mirrors _DoChoosePartner @ 1a35:0756. The original places boy + girl
+ // animations on a backdrop and polls four click rectangles (two per
+ // character) for the player's choice. We approximate by splitting the
+ // screen at x=160: left half = girl (Jenny), right half = boy (Jake).
+ Picture background;
+ if (!_picsArchive.getPicture(kPicChooseBackground, background)) {
+ warning("ChoosePartner background (%u) load failed", kPicChooseBackground);
+ return;
+ }
+
+ Animation boyAnim;
+ if (!_aniArchive.loadAnimation(kAniBoy, boyAnim) || boyAnim.empty()) {
+ warning("Boy animation (%u) load failed", kAniBoy);
+ return;
+ }
+ Animation girlAnim;
+ if (!_aniArchive.loadAnimation(kAniGirl, girlAnim) || girlAnim.empty()) {
+ warning("Girl animation (%u) load failed", kAniGirl);
+ return;
+ }
+
+ setAnmPalette(Common::Path("TITLE.ANM"));
+ blitAt(background, 0, 0);
+ blitAt(girlAnim[0], kGirlX, kGirlY);
+ blitAt(boyAnim[0], kBoyX, kBoyY);
+ g_system->updateScreen();
+
+ debugC(1, kDebugGeneral, "ChoosePartner: %u boy frames at (%d,%d), "
+ "%u girl frames at (%d,%d)",
+ (uint)boyAnim.size(), kBoyX, kBoyY,
+ (uint)girlAnim.size(), kGirlX, kGirlY);
+
+ uint frame = 0;
+ uint32 lastTick = g_system->getMillis();
+ while (!shouldQuit()) {
+ // Advance frame at ~5 Hz so the animations cycle gently.
+ if (g_system->getMillis() - lastTick > 200) {
+ lastTick = g_system->getMillis();
+ frame++;
+ blitAt(background, 0, 0);
+ blitAt(girlAnim[frame % girlAnim.size()], kGirlX, kGirlY);
+ blitAt(boyAnim[frame % boyAnim.size()], kBoyX, kBoyY);
+ g_system->updateScreen();
+ }
+
+ Common::Event ev;
+ bool done = false;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ done = true;
+ break;
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ _partner = (ev.mouse.x >= 160) ? 0 : 1;
+ debugC(1, kDebugGeneral, "Partner picked: %s",
+ _partner == 0 ? "Jake" : "Jennifer");
+ done = true;
+ break;
+ }
+ if (ev.type == Common::EVENT_KEYDOWN) {
+ if (ev.kbd.keycode == Common::KEYCODE_LEFT) {
+ _partner = 1; done = true; break;
+ }
+ if (ev.kbd.keycode == Common::KEYCODE_RIGHT) {
+ _partner = 0; done = true; break;
+ }
+ if (ev.kbd.keycode == Common::KEYCODE_RETURN ||
+ ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ done = true; break;
+ }
+ }
+ }
+ if (done)
break;
- default:
- warning("Unknown screen id 0x%02X; exiting", next);
- _nextScreen = kScreenInvalid;
+ g_system->updateScreen();
+ g_system->delayMillis(20);
+ }
+}
+
+void EEMEngine::doCaseSelection() {
+ // Mirrors `_CaseSelection` @ 1c33:0a87. The original draws PIC 0x41
+ // (chooser background) and a paginated list of mystery names rendered
+ // from M<n>.BIN headers, then calls `_DoChoose` to read a selection.
+ // We approximate with a numeric prompt (0..9 for first ten mysteries,
+ // Tab to cycle, Enter to load).
+ const uint kMaxMystery = 54;
+ // Default selection = the next unsolved mystery so post-win the
+ // player doesn't have to scroll to find what's left.
+ uint sel = 0;
+ for (uint i = 0; i <= kMaxMystery; i++) {
+ if (i < sizeof(_mysteriesSolved) && !_mysteriesSolved[i]) {
+ sel = i;
break;
}
+ }
- // Until handlers run their own event loops, pump events here so the
- // engine remains responsive and the user can quit.
- if (!pollEvents())
- break;
+ // Mirrors `_CaseSelection`: load PIC 0x41 as the chooser backdrop.
+ Picture caseBg;
+ const bool haveCaseBg = _picsArchive.getPicture(0x41, caseBg);
+
+ auto draw = [&]() {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ if (haveCaseBg) {
+ const int w = MIN<int>(caseBg.surface.w, 320);
+ const int h = MIN<int>(caseBg.surface.h, 200);
+ for (int row = 0; row < h; row++) {
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)caseBg.surface.getBasePtr(0, row), w);
+ }
+ }
+ if (_font.isLoaded()) {
+ _font.drawString(&scratch, 8, 4,
+ Common::String::format("EAGLE EYE - %s", _playerName.c_str()), 0xF);
+
+ // Solved count.
+ uint solved = 0, perfectSolved = 0;
+ for (uint i = 0; i < sizeof(_mysteriesSolved); i++) {
+ if (_mysteriesSolved[i] >= 1) solved++;
+ if (_mysteriesSolved[i] == 2) perfectSolved++;
+ }
+ _font.drawString(&scratch, 200, 4,
+ Common::String::format("solved %u (1st try %u)",
+ solved, perfectSolved), 0xF);
+ if (perfectSolved >= 55) {
+ _font.drawString(&scratch, 8, 168,
+ "** PERFECT MASTER SLEUTH! **", 0xF);
+ } else if (solved >= 55) {
+ _font.drawString(&scratch, 8, 168,
+ "** ALL MYSTERIES SOLVED! **", 0xF);
+ }
+
+ char marker = ' ';
+ if (sel < sizeof(_mysteriesSolved)) {
+ if (_mysteriesSolved[sel] == 2) marker = '*';
+ else if (_mysteriesSolved[sel] == 1) marker = '+';
+ }
+ // Per the original tiers: 0 (tutorial), 1-24 (Junior),
+ // 25-48 (Senior), 49-54 (Master).
+ const char *tier = "Tutorial";
+ if (sel >= 1 && sel <= 24) tier = "Junior Sleuth";
+ else if (sel >= 25 && sel <= 48) tier = "Senior Sleuth";
+ else if (sel >= 49 && sel <= 54) tier = "Master Sleuth";
+ _font.drawString(&scratch, 8, 24,
+ Common::String::format("Mystery %u %c [%s]",
+ sel, marker, tier), 0xF);
+ _font.drawString(&scratch, 8, 40,
+ " 0..9 quick select", 0xF);
+ _font.drawString(&scratch, 8, 52,
+ " Tab / + next mystery", 0xF);
+ _font.drawString(&scratch, 8, 64,
+ " Shift+Tab prev mystery", 0xF);
+ _font.drawString(&scratch, 8, 76,
+ " PgUp/PgDn jump 10", 0xF);
+ _font.drawString(&scratch, 8, 88,
+ " Home/End first/last", 0xF);
+ _font.drawString(&scratch, 8, 100,
+ " Enter start mystery", 0xF);
+ _font.drawString(&scratch, 8, 112,
+ " F5 save / load (ScummVM)", 0xF);
+ _font.drawString(&scratch, 8, 124,
+ " ESC quit", 0xF);
+ _font.drawString(&scratch, 8, 144,
+ " * solved on first try", 0xF);
+ _font.drawString(&scratch, 8, 156,
+ " + solved", 0xF);
+ }
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ };
+
+ draw();
+
+ bool confirmed = false;
+ while (!confirmed && !shouldQuit()) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return;
+ if (ev.type != Common::EVENT_KEYDOWN)
+ continue;
+ const Common::KeyCode k = ev.kbd.keycode;
+ if (k == Common::KEYCODE_ESCAPE) {
+ _mystery.clear();
+ _nextScreen = kScreenInvalid;
+ return;
+ }
+ if (k == Common::KEYCODE_RETURN) {
+ confirmed = true;
+ break;
+ }
+ if (k >= Common::KEYCODE_0 && k <= Common::KEYCODE_9) {
+ sel = (uint)(k - Common::KEYCODE_0);
+ draw();
+ continue;
+ }
+ if (k == Common::KEYCODE_TAB || k == Common::KEYCODE_PLUS ||
+ k == Common::KEYCODE_RIGHT || k == Common::KEYCODE_DOWN) {
+ const bool back = (ev.kbd.flags & Common::KBD_SHIFT) ||
+ k == Common::KEYCODE_LEFT;
+ if (back)
+ sel = (sel == 0) ? kMaxMystery : sel - 1;
+ else
+ sel = (sel >= kMaxMystery) ? 0 : sel + 1;
+ draw();
+ continue;
+ }
+ if (k == Common::KEYCODE_LEFT || k == Common::KEYCODE_UP ||
+ k == Common::KEYCODE_MINUS) {
+ sel = (sel == 0) ? kMaxMystery : sel - 1;
+ draw();
+ continue;
+ }
+ if (k == Common::KEYCODE_PAGEDOWN) {
+ sel = (sel + 10 > kMaxMystery) ? kMaxMystery : sel + 10;
+ draw();
+ continue;
+ }
+ if (k == Common::KEYCODE_PAGEUP) {
+ sel = (sel < 10) ? 0 : sel - 10;
+ draw();
+ continue;
+ }
+ if (k == Common::KEYCODE_HOME) {
+ sel = 0;
+ draw();
+ continue;
+ }
+ if (k == Common::KEYCODE_END) {
+ sel = kMaxMystery;
+ draw();
+ continue;
+ }
+ }
+ g_system->delayMillis(15);
+ }
+
+ if (!_mystery.load(sel, &_rng)) {
+ warning("doCaseSelection: failed to load mystery %u", sel);
+ _mystery.clear();
+ return;
}
+ debugC(1, kDebugMystery, "Mystery %u loaded; %u sites, %u suspects",
+ sel, _mystery.numSites(), _mystery.numSuspects());
}
-void EEMEngine::setSitePalette(uint num) {
- if (num >= kNumSitePals || _sitePals.size() < (num + 1) * kPalSize) {
- warning("setSitePalette: index %u out of range", num);
+void EEMEngine::doInitClues() {
+ // Mirrors `_DoInitClues` @ 1a35:0411. Sets BG 0x52 + palette 0x22,
+ // blits the Goblindroid game and book first frames, then displays
+ // the case briefing ClueBlock at InitBlock + 4. Marks the starting
+ // site (InitBlock word[1]) on `_OnSites`.
+ if (!_mystery.isLoaded())
return;
+
+ const byte *ib = _mystery.initBlock();
+ if (!ib)
+ return;
+
+ const uint16 startSite = READ_LE_UINT16(ib + 2);
+ if (startSite < Mystery::kVisitedSiteCap)
+ _mystery._onSites[startSite] = 1;
+
+ setSitePalette(0x22);
+ Picture bg;
+ if (_picsArchive.getPicture(0x52, bg))
+ blitAt(bg, 0, 0);
+
+ const uint gameAni = _partner == 0 ? 0x17 : 0x3b;
+ const uint bookAni = _partner == 0 ? 0x18 : 0x3c;
+ Animation game, book;
+ if (_aniArchive.loadAnimation(gameAni, game) && !game.empty())
+ blitAt(game[0], 0xcd, 0x6c);
+ if (_aniArchive.loadAnimation(bookAni, book) && !book.empty())
+ blitAt(book[0], 0, 99);
+
+ // Case type 1 also places "Nancy" (a third character) at (0x68, 0x8b)
+ // per `_DoInitClues`.
+ const uint16 caseType = READ_LE_UINT16(ib);
+ if (caseType == 1) {
+ Animation nancy;
+ if (_aniArchive.loadAnimation(0x19, nancy) && !nancy.empty())
+ blitAt(nancy[0], 0x68, 0x8b);
}
- // SITEPALS stores 6-bit VGA-DAC values (0..63); ScummVM expects 8-bit
- // (0..255), so left-shift by 2 like the original VGA hardware did.
- const byte *src = _sitePals.data() + num * kPalSize;
- byte expanded[kPalSize];
- for (uint i = 0; i < kPalSize; i++) {
- expanded[i] = (byte)(src[i] << 2);
+
+ displayClue(ib + 4);
+}
+
+void EEMEngine::doSiteLoop() {
+ // Mirrors the per-mystery site loop. SiteScreen::run() handles
+ // hotspot clicks plus M (map), N (notebook), G (gallery), A (accuse),
+ // Tab (next site), ESC (exit).
+ SiteScreen screen(this, &_mystery);
+ screen.run();
+}
+
+/// Mirror `_ParseString` @ 1b66:07c3 â substitute the control bytes that
+/// the original engine uses as placeholders. Only the two we encounter most
+/// often (player name = 0x80, partner first name = 0x82) are substituted;
+/// other 0x8N opcodes are stripped. The original engine also handles
+/// hyphenation marks and a hint placeholder (0x89) we ignore for now.
+static Common::String parseString(const Common::String &raw,
+ const Common::String &playerName,
+ const Common::String &partnerName) {
+ Common::String out;
+ for (uint i = 0; i < raw.size(); i++) {
+ const byte c = (byte)raw[i];
+ if (c == 0x80) {
+ out += playerName;
+ } else if (c == 0x82) {
+ out += partnerName;
+ } else if (c >= 0x80 && c < 0x8A) {
+ // Other control opcodes: eat them silently for now.
+ } else if (c == 0 || c == '\r') {
+ // stop on NUL, ignore CR
+ if (c == 0)
+ break;
+ } else {
+ out += (char)c;
+ }
}
- g_system->getPaletteManager()->setPalette(expanded, 0, 256);
+ return out;
}
-void EEMEngine::blitFullScreen(const Picture &pic) {
- // _MemoryCopy(0, 0xa000, srcOff, srcSeg) in _ShowEAKids dumps the picture
- // straight into VGA's 320x200 framebuffer.
- g_system->copyRectToScreen(pic.surface.getPixels(), pic.surface.pitch,
- 0, 0, pic.surface.w, pic.surface.h);
+void EEMEngine::applyClueSideEffects(const byte *c) {
+ for (uint j = 0; j < 5; j++) {
+ const uint16 note = READ_LE_UINT16(c + 0x30 + j * 2);
+ if (note != 0xFFFF && note < Mystery::kCluesFoundCap)
+ _mystery._cluesFound[note] = 1;
+
+ const uint16 galIdx = READ_LE_UINT16(c + 0x26 + j * 2);
+ if (galIdx != 0xFFFF && galIdx < Mystery::kGalleryCap) {
+ const uint8 phys = _mystery._newOrder[galIdx];
+ if (phys < Mystery::kGalleryCap)
+ _mystery._inGallery[phys] = 1;
+ }
+
+ const uint16 siteIdx = READ_LE_UINT16(c + 0x1c + j * 2);
+ if (siteIdx != 0xFFFF) {
+ const uint16 siteVal = siteIdx & 0x7FFF;
+ if (siteVal < Mystery::kVisitedSiteCap)
+ _mystery._onSites[siteVal] = 1;
+ if (siteIdx & 0x8000)
+ _mystery._sawCONSITEs = true;
+ }
+ }
}
-bool EEMEngine::pollEvents() {
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- switch (event.type) {
- case Common::EVENT_QUIT:
- case Common::EVENT_RETURN_TO_LAUNCHER:
- return false;
- default:
- break;
+void EEMEngine::displayClue(const byte *clueBlock) {
+ if (!clueBlock || !_mystery.isLoaded())
+ return;
+
+ // ClueBlock layout (verified against M0.BIN):
+ // +0..1: number (entry count)
+ // +2..3: pic ID for entry 0 (entry N>0 uses prev entry's last 2 bytes)
+ // +4..: array of 62-byte entries
+ const uint16 number = READ_LE_UINT16(clueBlock);
+ debugC(1, kDebugScript, "displayClue: %u entries", number);
+ if (number == 0 || number > 32) {
+ // number==0 = no briefing (e.g. mystery 0 case-type 4); >32 is a
+ // guard against bad pointers.
+ return;
+ }
+
+ // Snapshot the current screen as the BG so character pics from
+ // earlier entries don't stack on top of each other.
+ Graphics::ManagedSurface bg(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ bg.clear();
+ {
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (screen) {
+ for (int row = 0; row < 200; row++) {
+ memcpy((byte *)bg.getBasePtr(0, row),
+ (const byte *)screen->getBasePtr(0, row), 320);
+ }
+ g_system->unlockScreen();
+ }
+ }
+
+ for (uint i = 0; i < number && !shouldQuit(); i++) {
+ // Restore BG before drawing this entry's portrait + balloon.
+ g_system->copyRectToScreen(bg.getPixels(), bg.pitch, 0, 0, 320, 200);
+ const byte *c = clueBlock + 4 + i * 62;
+ // Per-partner fields:
+ // +0..1, +2..3: tx, ty (partner 0)
+ // +4..5, +6..7: tx, ty (partner 1)
+ // +8..9, +10..11: bubText offset for partner 0/1 (rel. TextBlock)
+ // +12..13, +14..15: balloon picture ID for partner 0/1
+ // +16..17, +18..19: bubX, bubY
+ // Per `_DisplayClue` @ 2404:05e6: partner 1 uses its own field
+ // set ONLY when bubText1 is not -1; otherwise it falls back to
+ // the partner 0 fields entirely. Partner 0 always uses field 0.
+ const bool useP1 = (_partner == 1) &&
+ (READ_LE_UINT16(c + 10) != 0xFFFF);
+ const uint partner = useP1 ? 1 : 0;
+ const uint16 textOff = READ_LE_UINT16(c + 8 + partner * 2);
+ const bool hasText = (textOff != 0xFFFF);
+ // Partner 1 bubX/bubY at +0x14/+0x16; partner 0 at +0x10/+0x12.
+ const uint16 bubX = READ_LE_UINT16(c + (useP1 ? 0x14 : 0x10));
+ const uint16 bubY = READ_LE_UINT16(c + (useP1 ? 0x16 : 0x12));
+ const uint16 bubNum = READ_LE_UINT16(c + (useP1 ? 0x0E : 0x0C));
+ const char *raw = hasText ? _mystery.textAt(textOff) : "";
+
+ // Speaker portrait. Mirrors `_DisplayClue`'s `pic[clues+i*62-2]`:
+ // for entry 0 the pic ID is in the ClueBlock header at +2; for
+ // later entries it sits in the previous entry's last 2 bytes.
+ // Speaker portrait position uses partner 0 fields (+0..+3) when
+ // _partner==0 or when partner 1 falls back; otherwise partner 1
+ // fields (+4..+7). Same logic as the original.
+ const uint16 charX = READ_LE_UINT16(c + (useP1 ? 4 : 0));
+ const uint16 charY = READ_LE_UINT16(c + (useP1 ? 6 : 2));
+ const uint16 charPicId = (i == 0)
+ ? READ_LE_UINT16(clueBlock + 2)
+ : READ_LE_UINT16(c - 2);
+ if (charPicId != 0 && charPicId != 0xFFFF) {
+ Picture charPic;
+ if (_picsArchive.getPicture(charPicId, charPic) &&
+ charX < 320 && charY < 200) {
+ const int w = MIN<int>(charPic.surface.w, 320 - charX);
+ const int h = MIN<int>(charPic.surface.h, 200 - charY);
+ if (w > 0 && h > 0)
+ g_system->copyRectToScreen(charPic.surface.getPixels(),
+ charPic.surface.pitch, charX, charY, w, h);
+ }
+ }
+
+ // Substitute placeholder control bytes with the entered player
+ // name and the chosen partner's first name (Jake / Jennifer).
+ const Common::String partnerName = (_partner == 0) ? "Jake" : "Jennifer";
+ const Common::String text = parseString(raw ? raw : "",
+ _playerName, partnerName);
+
+ // Speech balloon. Mirrors `_GetBalloon` + `_AddPicBackground` in
+ // `_DisplayClue`. The original looks up per-balloon text-area
+ // metadata in a table at offset 0x875 (within `_DisplayClue`'s
+ // segment); we don't have that table decoded yet, so we use a
+ // fixed inset of 8 px from the balloon's top-left.
+ Picture balloon;
+ const uint16 balloonId = bubNum & 0x7F;
+ const bool haveBalloon = bubNum != 0xFFFF &&
+ _balloonArchive.size() > balloonId &&
+ _balloonArchive.loadEntry(balloonId, balloon);
+
+ if (_font.isLoaded() && !text.empty()) {
+ // Snapshot the current screen, overlay balloon + text, then
+ // copy the changed band back. This preserves the site BG
+ // underneath unchanged regions.
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (!screen) break;
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ for (int row = 0; row < 200; row++) {
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)screen->getBasePtr(0, row), 320);
+ }
+ g_system->unlockScreen();
+
+ int textX = bubX;
+ int textY = bubY;
+ int textW = MIN<int>(320 - bubX, 200);
+ int copyY = bubY;
+ int copyH = _font.height() * 4 + 8;
+
+ if (haveBalloon) {
+ const int bw = MIN<int>(balloon.surface.w, 320 - bubX);
+ const int bh = MIN<int>(balloon.surface.h, 200 - bubY);
+ if (bw > 0 && bh > 0) {
+ for (int row = 0; row < bh; row++) {
+ memcpy((byte *)scratch.getBasePtr(bubX, bubY + row),
+ (const byte *)balloon.surface.getBasePtr(0, row),
+ bw);
+ }
+ }
+ // Per-balloon metadata table at 29be:0875 in the original
+ // uses (textX=6, textY=4) inset across all entries; we
+ // adopt the same constants instead of approximating with 8.
+ textX = bubX + 6;
+ textY = bubY + 4;
+ textW = bw - 12;
+ copyH = bh;
+ } else {
+ // No balloon â clear a band so old pixels don't bleed.
+ const Common::Rect band(0, bubY, 320,
+ MIN<int>(bubY + copyH, 200));
+ scratch.fillRect(band, 0);
+ copyY = bubY;
+ }
+
+ _font.drawWordWrapped(&scratch, textX, textY,
+ MAX<int>(8, textW), text, 0xF);
+
+ g_system->copyRectToScreen(scratch.getBasePtr(0, copyY),
+ scratch.pitch, 0, copyY, 320,
+ MIN<int>(copyH, 200 - copyY));
+ g_system->updateScreen();
+ }
+
+ // Wait for click/key to advance â only if we drew something.
+ // ESC skips the entire dialogue rather than just one entry.
+ if (hasText || (charPicId != 0 && charPicId != 0xFFFF)) {
+ bool advance = false;
+ bool skipAll = false;
+ while (!advance && !shouldQuit()) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ advance = true;
+ break;
+ }
+ if (ev.type == Common::EVENT_KEYDOWN &&
+ ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ advance = true;
+ skipAll = true;
+ break;
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN ||
+ ev.type == Common::EVENT_KEYDOWN) {
+ advance = true;
+ break;
+ }
+ }
+ g_system->delayMillis(10);
+ }
+ if (skipAll) {
+ // Apply remaining side-effects without rendering. The
+ // original silently runs the state updates even when the
+ // player skips ahead.
+ for (uint k = i; k < number; k++)
+ applyClueSideEffects(clueBlock + 4 + k * 62);
+ return;
+ }
}
+
+ applyClueSideEffects(c);
}
+}
+
+void EEMEngine::doNotebook() {
+ // Mirrors `_DrawNotes` @ 161e:01d0 + `_HandleNoteButton`. We list every
+ // found clue with its NoteIndex point value and let the player toggle
+ // "selected" with number keys 1..9 (paged in groups of 9). The total
+ // points of selected clues feed `_SolvedCheck` during accuse.
+ if (!_font.isLoaded())
+ return;
+
+ int page = 0;
+ const int kPerPage = 9;
+
+ auto draw = [&]() {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ _font.drawString(&scratch, 8, 4, "NOTEBOOK", 0xF);
+ _font.drawString(&scratch, 200, 4,
+ Common::String::format("pts: %d", _mystery.selectedPoints()), 0xF);
+
+ // Build a list of found-clue indices.
+ Common::Array<uint> found;
+ for (uint i = 0; i < Mystery::kCluesFoundCap; i++)
+ if (_mystery._cluesFound[i])
+ found.push_back(i);
+ const int total = (int)found.size();
+ const int pages = MAX<int>(1, (total + kPerPage - 1) / kPerPage);
+ page = MIN<int>(page, pages - 1);
+
+ _font.drawString(&scratch, 200, 16,
+ Common::String::format("page %d/%d", page + 1, pages), 0xF);
+
+ const byte *ni = _mystery.noteIndex();
+ const uint16 niCount = _mystery.noteIndexCount();
+ const Common::String partnerName = (_partner == 0) ? "Jake" : "Jennifer";
+ int y = 4 + _font.height() * 2 + 4;
+ for (int slot = 0; slot < kPerPage; slot++) {
+ const int idx = page * kPerPage + slot;
+ if (idx >= total)
+ break;
+ const uint clueId = found[idx];
+ Common::String text;
+ int pts = 0;
+ if (ni && clueId < niCount) {
+ const uint16 textOff = READ_LE_UINT16(ni + clueId * 4);
+ const uint16 ptsRaw = READ_LE_UINT16(ni + clueId * 4 + 2);
+ pts = (int)(int16)ptsRaw;
+ const Common::String raw = _mystery.textAt(textOff);
+ text = parseString(raw, _playerName, partnerName);
+ }
+ if (text.empty())
+ text = Common::String::format("clue %u", clueId);
+
+ const char selMark = _mystery._noteSelected[clueId] ? '*' : ' ';
+ Common::String line = Common::String::format(
+ "%d [%c] (%d pts) %s", slot + 1, selMark, pts, text.c_str());
+ const int used = _font.drawWordWrapped(&scratch, 8, y, 304, line, 0xF);
+ y += used + 2;
+ if (y >= 192) break;
+ }
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ };
+
+ draw();
+ while (!shouldQuit()) {
+ Common::Event ev;
+ bool dirty = false;
+ bool exit = false;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) { exit = true; break; }
+ if (ev.type == Common::EVENT_KEYDOWN) {
+ if (ev.kbd.keycode == Common::KEYCODE_ESCAPE) { exit = true; break; }
+ if (ev.kbd.keycode >= Common::KEYCODE_1 && ev.kbd.keycode <= Common::KEYCODE_9) {
+ const int slot = (int)(ev.kbd.keycode - Common::KEYCODE_1);
+ Common::Array<uint> found;
+ for (uint i = 0; i < Mystery::kCluesFoundCap; i++)
+ if (_mystery._cluesFound[i])
+ found.push_back(i);
+ const int idx = page * kPerPage + slot;
+ if (idx < (int)found.size()) {
+ const uint clueId = found[idx];
+ _mystery._noteSelected[clueId] ^= 1;
+ dirty = true;
+ }
+ } else if (ev.kbd.keycode == Common::KEYCODE_TAB ||
+ ev.kbd.keycode == Common::KEYCODE_RIGHT) {
+ page++; dirty = true;
+ } else if (ev.kbd.keycode == Common::KEYCODE_LEFT) {
+ if (page > 0) page--;
+ dirty = true;
+ }
+ }
+ }
+ if (exit) break;
+ if (dirty) draw();
+ g_system->delayMillis(15);
+ }
+}
+
+void EEMEngine::doGallery() {
+ // Mirrors `_DrawGallery` @ 158f:0046. The original loops `_NumSuspects`
+ // gallery entries (0x46 = 70 bytes each in `_GalleryData`); the first
+ // u16 of each entry is the PIC picture ID for that suspect. We render
+ // them in a row across the screen.
+ if (!_mystery.isLoaded())
+ return;
+
+ const byte *gd = _mystery.galleryData();
+ if (!gd) {
+ warning("doGallery: no GalleryData in mystery %u", _mystery.number());
+ return;
+ }
+
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+
+ // Use PIC 0x3f as the gallery backdrop, matching `_DoAccuseGallery`.
+ Picture galBg;
+ if (_picsArchive.getPicture(0x3f, galBg)) {
+ const int w = MIN<int>(galBg.surface.w, 320);
+ const int h = MIN<int>(galBg.surface.h, 200);
+ for (int row = 0; row < h; row++) {
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)galBg.surface.getBasePtr(0, row), w);
+ }
+ }
+
+ if (_font.isLoaded())
+ _font.drawString(&scratch, 8, 4, "GALLERY", 0xF);
+
+ const uint8 num = _mystery.numSuspects();
+ int slotX = 8;
+ const int slotY = 24;
+ const int slotStep = 320 / MAX<uint8>(1, num);
+ for (uint i = 0; i < num; i++) {
+ const uint16 picId = READ_LE_UINT16(gd + i * 0x46);
+ if (picId == 0)
+ continue;
+ Picture portrait;
+ if (!_picsArchive.getPicture(picId, portrait))
+ continue;
+ const int placeX = slotX + (slotStep - portrait.surface.w) / 2;
+ const int placeY = slotY;
+ const int w = MIN<int>(portrait.surface.w, 320 - placeX);
+ const int h = MIN<int>(portrait.surface.h, 200 - placeY);
+ if (w > 0 && h > 0) {
+ for (int row = 0; row < h; row++) {
+ memcpy((byte *)scratch.getBasePtr(placeX, placeY + row),
+ (const byte *)portrait.surface.getBasePtr(0, row), w);
+ }
+ }
+ // Suspect number + discovered marker under the portrait.
+ if (_font.isLoaded()) {
+ const bool discovered = (i < Mystery::kGalleryCap) &&
+ _mystery._inGallery[i];
+ Common::String label = Common::String::format("%u%s",
+ i + 1, discovered ? " *" : "");
+ _font.drawString(&scratch, placeX + 4, placeY + h + 2, label, 0xF);
+ }
+ slotX += slotStep;
+ }
+
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
g_system->updateScreen();
- g_system->delayMillis(10);
- return true;
+ waitForInput(60000);
+}
+
+void EEMEngine::doBigMap() {
+ // Mirrors `_DoBigMap` @ 20fe:09e7. The original lays out a 0xe9 x 0xab
+ // map window inside frame PIC 0x42 with per-site clickable markers
+ // scrolling under it. We render BIGMAP.PIC at top-left, draw an
+ // overlay listing the sites that the player can travel to (per
+ // `_OnSites`), and accept either number keys or clicks on the
+ // overlay rows to travel.
+ Common::File f;
+ if (!f.open(Common::Path("BIGMAP.PIC"))) {
+ warning("doBigMap: BIGMAP.PIC missing");
+ return;
+ }
+ const uint16 mapH = f.readUint16LE();
+ const uint16 mapW = f.readUint16LE();
+ if (mapW == 0 || mapH == 0)
+ return;
+
+ Common::Array<byte> mapPixels((uint32)mapW * mapH);
+ if (f.read(mapPixels.data(), mapPixels.size()) != mapPixels.size()) {
+ warning("doBigMap: short read on BIGMAP.PIC");
+ return;
+ }
+
+ // Approximate inner map window from the original `_DoBigMap`:
+ // if (sx < 0x75) sx = 0; else sx -= 0x74; // 0x74 = 116
+ // if (mapW < sx + 0xe9) sx = mapW - 0xe9; // window width = 233
+ // if (sy < 0x56) sy = 0; else sy -= 0x55;
+ // if (mapH < sy + 0xab) sy = mapH - 0xab; // window height = 171
+ const int kMapWinW = 0xe9; // 233
+ const int kMapWinH = 0xab; // 171
+ const int kMapWinX = 4;
+ const int kMapWinY = 4;
+
+ int scrollX = 0;
+ int scrollY = 0;
+
+ // Auto-scroll to centre the current site, if known.
+ if (_mystery.isLoaded()) {
+ const byte *entry = _mystery.mapEntry(_mystery._siteNumber);
+ if (entry) {
+ const uint16 mx = READ_LE_UINT16(entry + 4);
+ const uint16 my = READ_LE_UINT16(entry + 6);
+ scrollX = MAX<int>(0, MIN<int>(mapW - kMapWinW,
+ (int)mx - kMapWinW / 2));
+ scrollY = MAX<int>(0, MIN<int>(mapH - kMapWinH,
+ (int)my - kMapWinH / 2));
+ }
+ }
+
+ auto draw = [&]() {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+
+ // Frame from PIC 0x42 (`_GetPicture(0x42)` in `_DoBigMap`).
+ Picture frame;
+ if (_picsArchive.getPicture(0x42, frame)) {
+ const int w = MIN<int>(frame.surface.w, 320);
+ const int h = MIN<int>(frame.surface.h, 200);
+ for (int row = 0; row < h; row++) {
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)frame.surface.getBasePtr(0, row), w);
+ }
+ }
+
+ // Map content clipped into the inner window, applying scroll.
+ const int copyW = MIN<int>(mapW - scrollX, kMapWinW);
+ const int copyH = MIN<int>(mapH - scrollY, kMapWinH);
+ for (int row = 0; row < copyH; row++) {
+ memcpy((byte *)scratch.getBasePtr(kMapWinX, kMapWinY + row),
+ mapPixels.data() + (scrollY + row) * mapW + scrollX,
+ copyW);
+ }
+
+ // Site markers from MapData. Each per-mystery MapData entry is
+ // 14 bytes; bytes +4..+5 / +6..+7 hold an (x, y) pair on the big
+ // map. We draw a small filled square for each accessible site.
+ if (_mystery.isLoaded()) {
+ for (uint i = 0; i < _mystery.numSites(); i++) {
+ if (!_mystery._onSites[i] && i != _mystery._siteNumber)
+ continue;
+ const byte *entry = _mystery.mapEntry(i);
+ if (!entry)
+ continue;
+ const uint16 mx = READ_LE_UINT16(entry + 4);
+ const uint16 my = READ_LE_UINT16(entry + 6);
+ const int sx = (int)mx - scrollX + kMapWinX;
+ const int sy = (int)my - scrollY + kMapWinY;
+ if (sx < kMapWinX || sx >= kMapWinX + kMapWinW ||
+ sy < kMapWinY || sy >= kMapWinY + kMapWinH)
+ continue;
+ const byte color = (i == _mystery._siteNumber) ? 0x0E : 0x0F;
+ const Common::Rect mark(sx - 2, sy - 2, sx + 3, sy + 3);
+ scratch.fillRect(mark, color);
+ if (_font.isLoaded()) {
+ Common::String num = Common::String::format("%u", i);
+ _font.drawString(&scratch, sx + 4, sy - 4, num, color);
+ }
+ }
+ }
+
+ // Travel-target overlay (right-side panel).
+ if (_font.isLoaded() && _mystery.isLoaded()) {
+ const int panelX = kMapWinX + kMapWinW + 4;
+ int y = 4;
+ _font.drawString(&scratch, panelX, y, "TRAVEL", 0xF);
+ y += _font.height() + 4;
+ for (uint i = 0; i < _mystery.numSites() && y < 192; i++) {
+ if (!_mystery._onSites[i] && i != _mystery._siteNumber)
+ continue;
+ const char marker = (i == _mystery._siteNumber) ? '>' : ' ';
+ Common::String label = Common::String::format(
+ "%c %u", marker, i);
+ _font.drawString(&scratch, panelX, y, label, 0x0F);
+ y += _font.height() + 1;
+ }
+ _font.drawString(&scratch, panelX, 188,
+ "Esc", 0x0F);
+ }
+
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ };
+ draw();
+
+ while (!shouldQuit()) {
+ Common::Event ev;
+ bool dirty = false;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return;
+ if (ev.type == Common::EVENT_KEYDOWN) {
+ if (ev.kbd.keycode == Common::KEYCODE_ESCAPE)
+ return;
+ const int kStep = 16;
+ if (ev.kbd.keycode == Common::KEYCODE_LEFT) {
+ scrollX = MAX<int>(0, scrollX - kStep);
+ dirty = true;
+ } else if (ev.kbd.keycode == Common::KEYCODE_RIGHT) {
+ scrollX = MIN<int>(MAX<int>(0, mapW - kMapWinW), scrollX + kStep);
+ dirty = true;
+ } else if (ev.kbd.keycode == Common::KEYCODE_UP) {
+ scrollY = MAX<int>(0, scrollY - kStep);
+ dirty = true;
+ } else if (ev.kbd.keycode == Common::KEYCODE_DOWN) {
+ scrollY = MIN<int>(MAX<int>(0, mapH - kMapWinH), scrollY + kStep);
+ dirty = true;
+ }
+ if (ev.kbd.keycode >= Common::KEYCODE_0 &&
+ ev.kbd.keycode <= Common::KEYCODE_9) {
+ const uint target = (uint)(ev.kbd.keycode - Common::KEYCODE_0);
+ if (_mystery.isLoaded() &&
+ target < _mystery.numSites() &&
+ _mystery._onSites[target]) {
+ _mystery._lastSite = _mystery._siteNumber;
+ _mystery._siteNumber = (uint16)target;
+ return;
+ }
+ }
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ // Click on a map marker?
+ if (_mystery.isLoaded() &&
+ ev.mouse.x >= kMapWinX &&
+ ev.mouse.x < kMapWinX + kMapWinW &&
+ ev.mouse.y >= kMapWinY &&
+ ev.mouse.y < kMapWinY + kMapWinH) {
+ for (uint i = 0; i < _mystery.numSites(); i++) {
+ if (!_mystery._onSites[i] &&
+ i != _mystery._siteNumber)
+ continue;
+ const byte *entry = _mystery.mapEntry(i);
+ if (!entry) continue;
+ const uint16 mx = READ_LE_UINT16(entry + 4);
+ const uint16 my = READ_LE_UINT16(entry + 6);
+ const int sx = (int)mx - scrollX + kMapWinX;
+ const int sy = (int)my - scrollY + kMapWinY;
+ if (ABS(ev.mouse.x - sx) <= 5 &&
+ ABS(ev.mouse.y - sy) <= 5) {
+ _mystery._lastSite = _mystery._siteNumber;
+ _mystery._siteNumber = (uint16)i;
+ return;
+ }
+ }
+ }
+
+ // Click in the right panel: travel to that row.
+ const int panelX = kMapWinX + kMapWinW + 4;
+ if (_font.isLoaded() && _mystery.isLoaded() &&
+ ev.mouse.x >= panelX) {
+ const int row = (ev.mouse.y - 4 - _font.height() - 4) /
+ (_font.height() + 1);
+ int seen = 0;
+ for (uint i = 0; i < _mystery.numSites(); i++) {
+ if (!_mystery._onSites[i] && i != _mystery._siteNumber)
+ continue;
+ if (seen == row) {
+ _mystery._lastSite = _mystery._siteNumber;
+ _mystery._siteNumber = (uint16)i;
+ return;
+ }
+ seen++;
+ }
+ }
+ }
+ }
+ if (dirty)
+ draw();
+ g_system->delayMillis(10);
+ }
+}
+
+void EEMEngine::doHelp() {
+ // `_KDHelp` reads two hint TextBlock offsets from `_KDTextIndex`:
+ // word @ +0xe : first-time hint
+ // word @ +0x10: second-time hint (cycles back to first if missing)
+ // `_SawHelpHint` toggles between them.
+ if (!_mystery.isLoaded() || !_font.isLoaded())
+ return;
+
+ const byte *kd = _mystery.kdTextIndex();
+ if (!kd)
+ return;
+
+ const uint16 hintFirst = READ_LE_UINT16(kd + 0x0e);
+ const uint16 hintSecond = READ_LE_UINT16(kd + 0x10);
+ uint16 use = _mystery._sawHelpHint && hintSecond != 0xFFFF ? hintSecond : hintFirst;
+ if (use == 0xFFFF) {
+ debugC(1, kDebugScript, "doHelp: no hint configured");
+ return;
+ }
+ if (!_mystery._sawHelpHint && hintFirst != 0xFFFF)
+ _mystery._sawHelpHint = true;
+
+ const Common::String raw = _mystery.textAt(use);
+ const Common::String partnerName = (_partner == 0) ? "Jake" : "Jennifer";
+ const Common::String text = parseString(raw, _playerName, partnerName);
+
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ _font.drawString(&scratch, 8, 4, "HELP", 0xF);
+ _font.drawWordWrapped(&scratch, 8, 24, 304, text, 0xF);
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ waitForInput(60000);
+}
+
+bool EEMEngine::areYouSure() {
+ // Mirrors `_AreYouSure` @ 1a35:0a5c. Original loads PIC 0x136 for the
+ // dialog body and PIC 0x1FD/0x1FE for YES/NO. We render a minimal
+ // text dialog that preserves the screen behind it.
+ if (!_font.isLoaded())
+ return true;
+
+ Graphics::Surface *screen = g_system->lockScreen();
+ Graphics::ManagedSurface saved(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ if (screen) {
+ for (int row = 0; row < 200; row++) {
+ memcpy((byte *)saved.getBasePtr(0, row),
+ (const byte *)screen->getBasePtr(0, row), 320);
+ }
+ g_system->unlockScreen();
+ }
+
+ const Common::Rect dlg(60, 70, 260, 140);
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ for (int row = 0; row < 200; row++)
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)saved.getBasePtr(0, row), 320);
+ scratch.fillRect(dlg, 0);
+ scratch.frameRect(dlg, 0xF);
+ _font.drawString(&scratch, dlg.left + 8, dlg.top + 8,
+ "Are you sure you want to quit?", 0xF);
+ _font.drawString(&scratch, dlg.left + 16, dlg.top + 36, "Y - Yes", 0xF);
+ _font.drawString(&scratch, dlg.left + 100, dlg.top + 36, "N - No", 0xF);
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+
+ bool result = false;
+ while (!shouldQuit()) {
+ Common::Event ev;
+ bool decided = false;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ result = true;
+ decided = true;
+ break;
+ }
+ if (ev.type == Common::EVENT_KEYDOWN) {
+ if (ev.kbd.keycode == Common::KEYCODE_y ||
+ ev.kbd.keycode == Common::KEYCODE_RETURN) {
+ result = true; decided = true; break;
+ }
+ if (ev.kbd.keycode == Common::KEYCODE_n ||
+ ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ result = false; decided = true; break;
+ }
+ }
+ }
+ if (decided)
+ break;
+ g_system->delayMillis(15);
+ }
+
+ // Restore the screen so the caller's UI is intact.
+ g_system->copyRectToScreen(saved.getPixels(), saved.pitch, 0, 0, 320, 200);
+ g_system->updateScreen();
+ return result;
+}
+
+void EEMEngine::doAccuse() {
+ if (!_mystery.isLoaded())
+ return;
+
+ // Mirrors `_DoAccuseGallery` @ 1df2:0a31. Render gallery + prompt,
+ // accept either keyboard 1..N or a click on a suspect's portrait.
+ const uint8 num = _mystery.numSuspects();
+ if (num == 0)
+ return;
+
+ const byte *gd = _mystery.galleryData();
+ const int slotStep = 320 / MAX<uint8>(1, num);
+ const int slotY = 24;
+
+ // Mirrors `_DoAccuseGallery`: load PIC 0x3f as the accuse backdrop.
+ Picture accuseBg;
+ const bool haveAccuseBg = _picsArchive.getPicture(0x3f, accuseBg);
+
+ auto drawGallery = [&]() {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ if (haveAccuseBg) {
+ const int w = MIN<int>(accuseBg.surface.w, 320);
+ const int h = MIN<int>(accuseBg.surface.h, 200);
+ for (int row = 0; row < h; row++) {
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)accuseBg.surface.getBasePtr(0, row), w);
+ }
+ }
+ if (_font.isLoaded())
+ _font.drawString(&scratch, 8, 4, "ACCUSE", 0xF);
+
+ for (uint i = 0; i < num; i++) {
+ if (!gd) continue;
+ const uint16 picId = READ_LE_UINT16(gd + i * 0x46);
+ if (picId == 0)
+ continue;
+ Picture portrait;
+ if (!_picsArchive.getPicture(picId, portrait))
+ continue;
+ const int placeX = i * slotStep +
+ (slotStep - portrait.surface.w) / 2;
+ const int placeY = slotY;
+ const int w = MIN<int>(portrait.surface.w, 320 - placeX);
+ const int h = MIN<int>(portrait.surface.h, 200 - placeY);
+ if (w > 0 && h > 0) {
+ for (int row = 0; row < h; row++)
+ memcpy((byte *)scratch.getBasePtr(placeX, placeY + row),
+ (const byte *)portrait.surface.getBasePtr(0, row), w);
+ }
+ if (_font.isLoaded()) {
+ Common::String label = Common::String::format("%u", i + 1);
+ _font.drawString(&scratch, placeX + 4,
+ placeY + h + 2, label, 0xF);
+ }
+ }
+ if (_font.isLoaded()) {
+ _font.drawString(&scratch, 8, 180,
+ "Click a suspect or press 1..N - ESC to cancel", 0xF);
+ }
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ };
+ drawGallery();
+
+ int picked = -1;
+ while (picked < 0 && !shouldQuit()) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return;
+ if (ev.type == Common::EVENT_KEYDOWN) {
+ if (ev.kbd.keycode == Common::KEYCODE_ESCAPE)
+ return;
+ const int k = (int)ev.kbd.keycode;
+ if (k >= Common::KEYCODE_1 && k <= Common::KEYCODE_9) {
+ const int idx = k - Common::KEYCODE_1;
+ if (idx < num)
+ picked = idx;
+ }
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ const int slot = ev.mouse.x / slotStep;
+ if (slot >= 0 && slot < (int)num &&
+ ev.mouse.y >= slotY && ev.mouse.y < slotY + 120)
+ picked = slot;
+ }
+ }
+ g_system->delayMillis(10);
+ }
+ if (picked < 0)
+ return;
+
+ // Real chain evaluation: sum point values of clues the player marked
+ // "selected" in the notebook. Mirrors `_SolvedCheck` @ 1df2:00ec.
+ const int points = _mystery.selectedPoints();
+ const bool guessedRight = _mystery.solvedCheck();
+ debugC(1, kDebugScript, "doAccuse: picked=%d selectedPts=%d -> %s",
+ picked, points, guessedRight ? "correct" : "wrong");
+
+ // If the player hasn't marked any evidence yet, give them a hint
+ // rather than an instant fail. Mirrors the original "We're not ready
+ // to solve this mystery yet..." string at 29be:10f0.
+ if (points == 0 && _font.isLoaded()) {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ _font.drawWordWrapped(&scratch, 16, 80, 288,
+ "We're not ready to solve this mystery yet. "
+ "Let's keep investigating until we have some "
+ "more solid evidence to make our case! "
+ "(Press N in the site screen to mark clues.)",
+ 0xF);
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ waitForInput(15000);
+ return;
+ }
+
+ // Pick the ending based on the chain. For a correct accusation the
+ // original would call `_DisplayClue(_Mystery + AChain[0])`, play
+ // SCRAPBK.ANI and save progress. We load the matching `E<n>.BIN`
+ // ending text and render its pages with prev/next navigation.
+ const int endingNum = guessedRight ? picked : 0;
+ const Common::String fname = Common::String::format("E%d.BIN", endingNum);
+ Common::File f;
+ if (!f.open(Common::Path(fname))) {
+ warning("doAccuse: %s missing", fname.c_str());
+ return;
+ }
+
+ // E<n>.BIN format (verified against `_DisplayEndingPage` @ 1df2:044c):
+ // u16 numPages
+ // per page (10 bytes header + NUL-string):
+ // u16 picNum
+ // u16 x1, y1, x2, y2 (story rect)
+ // bytes[] NUL-terminated text
+ const uint32 fileLen = f.size();
+ Common::Array<byte> blob(fileLen);
+ if (f.read(blob.data(), fileLen) != fileLen)
+ return;
+ const byte *e = blob.data();
+ const uint16 pages = READ_LE_UINT16(e);
+
+ const Common::String partnerName = (_partner == 0) ? "Jake" : "Jennifer";
+ uint pageIdx = 0;
+
+ while (!shouldQuit()) {
+ // Walk to pageIdx.
+ uint pos = 2;
+ uint cur = 0;
+ while (cur < pageIdx && pos + 10 < fileLen) {
+ const char *t = (const char *)(e + pos + 10);
+ pos += 10 + strlen(t) + 1;
+ cur++;
+ }
+ if (pos + 10 >= fileLen)
+ break;
+
+ const uint16 picNum = READ_LE_UINT16(e + pos + 0);
+ const uint16 x1 = READ_LE_UINT16(e + pos + 2);
+ const uint16 y1 = READ_LE_UINT16(e + pos + 4);
+ const uint16 x2 = READ_LE_UINT16(e + pos + 6);
+ const uint16 y2 = READ_LE_UINT16(e + pos + 8);
+ const char *raw = (const char *)(e + pos + 10);
+ const Common::String txt = parseString(raw, _playerName, partnerName);
+
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+
+ // Page background.
+ if (picNum != 0) {
+ Picture bg;
+ if (_picsArchive.getPicture(picNum, bg)) {
+ const int w = MIN<int>(bg.surface.w, 320);
+ const int h = MIN<int>(bg.surface.h, 200);
+ for (int row = 0; row < h; row++) {
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)bg.surface.getBasePtr(0, row), w);
+ }
+ }
+ }
+
+ if (_font.isLoaded()) {
+ Common::String banner = "Not enough evidence";
+ if (guessedRight)
+ banner = _mystery._firstTry ? "CORRECT - FIRST TRY!" : "CORRECT!";
+ _font.drawString(&scratch, 8, 4, banner, 0xF);
+ _font.drawString(&scratch, 8, 16,
+ Common::String::format("Evidence: %d/100 Suspect: %d",
+ points, picked + 1), 0xF);
+ const int wrapW = MAX<int>(16, x2 - x1);
+ const int wrapY = MAX<int>(28, (int)y1);
+ (void)y2;
+ _font.drawWordWrapped(&scratch, x1, wrapY, wrapW, txt, 0xF);
+ _font.drawString(&scratch, 8, 188,
+ Common::String::format("page %u/%u (Left/Right or click)",
+ pageIdx + 1, pages), 0xF);
+ }
+
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+
+ // Page navigation.
+ bool advance = false;
+ bool back = false;
+ bool exit = false;
+ while (!advance && !back && !exit && !shouldQuit()) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ exit = true; break;
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ advance = true; break;
+ }
+ if (ev.type == Common::EVENT_KEYDOWN) {
+ if (ev.kbd.keycode == Common::KEYCODE_ESCAPE)
+ exit = true;
+ else if (ev.kbd.keycode == Common::KEYCODE_LEFT)
+ back = true;
+ else
+ advance = true;
+ break;
+ }
+ }
+ g_system->delayMillis(15);
+ }
+ if (exit) break;
+ if (advance) {
+ if (pageIdx + 1 >= pages) break;
+ pageIdx++;
+ } else if (back) {
+ if (pageIdx > 0) pageIdx--;
+ }
+ }
+
+ // Mirror `_DisplayCorrect`'s scrap-book animation + solved tracking +
+ // auto-save (the original calls `_SavePlayerRecord` after a win).
+ if (guessedRight) {
+ const uint mn = _mystery.number();
+ if (mn < sizeof(_mysteriesSolved)) {
+ _mysteriesSolved[mn] = _mystery._firstTry ? 2 : 1;
+ }
+ playAnm(Common::Path("SCRAPBK.ANI"), 120, true);
+
+ // Auto-save into slot 0 (the engine's quicksave slot).
+ const Common::String desc = Common::String::format(
+ "%s â solved mystery %u", _playerName.c_str(), mn);
+ Common::Error err = saveGameState(0, desc, true);
+ if (err.getCode() != Common::kNoError)
+ warning("auto-save after solve failed: %s",
+ err.getDesc().c_str());
+ } else {
+ _mystery._firstTry = false;
+ }
+}
+
+// -------------------- save / load --------------------
+
+namespace {
+const uint32 kSaveMagic = MKTAG('E', 'E', 'M', '0');
+const byte kSaveVer = 3; ///< v2: _mysteriesSolved tracker; v3: player name
+} // anonymous namespace
+
+bool EEMEngine::hasFeature(EngineFeature f) const {
+ // We support saving any time but loading only at startup (via the
+ // `--save-slot=N` resume path or a slot picked from the launcher).
+ // Runtime loads would replace `_mystery._data` while pointers into
+ // it are alive on the stack inside `displayClue` etc.
+ return f == kSupportsSavingDuringRuntime ||
+ f == kSupportsReturnToLauncher;
+}
+
+bool EEMEngine::canLoadGameStateCurrently(Common::U32String *) {
+ return false; // Loading is startup-only.
+}
+
+bool EEMEngine::canSaveGameStateCurrently(Common::U32String *) {
+ return _mystery.isLoaded();
+}
+
+Common::Error EEMEngine::saveGameState(int slot, const Common::String &desc, bool isAutosave) {
+ Common::OutSaveFile *out = getSaveFileManager()->openForSaving(getSaveStateName(slot));
+ if (!out)
+ return Common::kCreatingFileFailed;
+
+ out->writeUint32BE(kSaveMagic);
+ out->writeByte(kSaveVer);
+
+ // Header: description + ScummVM extended save metadata are appended
+ // automatically when `EngineFeature::kSavesUseExtendedFormat` is set;
+ // our save body just carries the engine state.
+ (void)desc;
+ (void)isAutosave;
+
+ uint16 mysteryNum = (uint16)_mystery.number();
+ out->writeUint16LE(mysteryNum);
+ out->writeByte(_partner);
+ out->write(_mysteriesSolved, sizeof(_mysteriesSolved));
+
+ // v3: persist the player name so save-slot resume restores it.
+ out->writeUint16LE((uint16)_playerName.size());
+ out->writeString(_playerName);
+
+ debugC(1, kDebugGeneral,
+ "Saved slot %d: mystery=%u partner=%u name=%s autosave=%d",
+ slot, mysteryNum, _partner, _playerName.c_str(), isAutosave ? 1 : 0);
+
+ Common::Serializer s(nullptr, out);
+ s.setVersion(kSaveVer);
+ _mystery.syncState(s);
+
+ out->finalize();
+ delete out;
+ return Common::kNoError;
+}
+
+Common::Error EEMEngine::loadGameState(int slot) {
+ Common::InSaveFile *in = getSaveFileManager()->openForLoading(getSaveStateName(slot));
+ if (!in)
+ return Common::kReadingFailed;
+
+ if (in->readUint32BE() != kSaveMagic) {
+ delete in;
+ return Common::kUnknownError;
+ }
+ const byte ver = in->readByte();
+ if (ver > kSaveVer) {
+ delete in;
+ return Common::kUnknownError;
+ }
+
+ const uint16 mysteryNum = in->readUint16LE();
+ _partner = in->readByte();
+ if (ver >= 2)
+ in->read(_mysteriesSolved, sizeof(_mysteriesSolved));
+ else
+ memset(_mysteriesSolved, 0, sizeof(_mysteriesSolved));
+
+ if (ver >= 3) {
+ const uint16 nameLen = in->readUint16LE();
+ Common::String name;
+ for (uint16 i = 0; i < nameLen && i < 64; i++)
+ name += (char)in->readByte();
+ _playerName = name.empty() ? Common::String("Detective") : name;
+ }
+
+ if (!_mystery.load(mysteryNum, &_rng)) {
+ _mystery.clear();
+ delete in;
+ return Common::kReadingFailed;
+ }
+
+ Common::Serializer s(in, nullptr);
+ s.setVersion(ver);
+ _mystery.syncState(s);
+
+ delete in;
+ debugC(1, kDebugGeneral,
+ "Loaded slot %d: mystery=%u partner=%u name=%s",
+ slot, mysteryNum, _partner, _playerName.c_str());
+ return Common::kNoError;
+}
+
+void EEMEngine::screenDriver() {
+ // Placeholder for the eventual dispatch table (one entry per ScreenId).
+ // run() currently calls handlers directly until the title path lands.
}
} // End of namespace EEM
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 67a4b0967db..b73ef2ac2bc 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -24,6 +24,7 @@
#ifndef EEM_EEM_H
#define EEM_EEM_H
+#include "common/array.h"
#include "common/platform.h"
#include "common/random.h"
#include "common/scummsys.h"
@@ -31,21 +32,22 @@
#include "engines/advancedDetector.h"
#include "engines/engine.h"
+#include "eem/animation.h"
+#include "eem/font.h"
+#include "eem/mystery.h"
#include "eem/resource.h"
namespace EEM {
-class Console;
-
/**
* Screen IDs used by the original ScreenDriver dispatch table at 1a35:0e5e.
* The table holds 14 (id, handler) entries; the loop iterates until it finds
* a matching id and calls its handler. ID 0xFFFF is the exit sentinel.
*/
enum ScreenId {
- kScreenInvalid = 0xFFFF,
- kScreenTitle = 0x0B, ///< _ShowTitlePage @ 1a35:06b7
- kScreenNext = 0x08 ///< follow-up after title (case selection); to be confirmed
+ kScreenInvalid = 0xFFFF,
+ kScreenChoosePartner = 0x09, ///< _DoChoosePartner @ 1a35:0756 (boy/girl picker)
+ kScreenTitle = 0x0B ///< _ShowTitlePage @ 1a35:06b7
};
class EEMEngine : public Engine {
@@ -58,9 +60,69 @@ public:
const char *getGameId() const;
Common::Platform getPlatform() const;
+ bool hasFeature(EngineFeature f) const override;
+ bool canLoadGameStateCurrently(Common::U32String *msg = nullptr) override;
+ bool canSaveGameStateCurrently(Common::U32String *msg = nullptr) override;
+ Common::Error loadGameState(int slot) override;
+ Common::Error saveGameState(int slot, const Common::String &desc, bool isAutosave) override;
+
const ADGameDescription *_gameDescription;
- DBDArchive &getPics() { return _picsArchive; }
+ DBDArchive &getPics() { return _picsArchive; }
+ DBDArchive &getAni() { return _aniArchive; }
+ DBDArchive &getSites() { return _sitesArchive; }
+ DBDArchive &getBalloons(){ return _balloonArchive; }
+ Mystery &getMystery() { return _mystery; }
+ const EEMFont &getFont() const { return _font; }
+
+ /// Display one ClueBlock. @p clueBlock points at the u16 frame count
+ /// followed by 62-byte ClueEntries. Mirrors _DisplayClue @ 2404:05e6.
+ void displayClue(const byte *clueBlock);
+
+ /// Apply a single ClueEntry's side effects â notebook adds, gallery
+ /// updates, site flags. Called both by `displayClue` after a normal
+ /// click-through and when the player ESC-skips a multi-entry clue.
+ void applyClueSideEffects(const byte *entry);
+
+ /// Show clue/notebook screen. Mirrors `_DrawNotes` @ 161e:01d0.
+ void doNotebook();
+
+ /// Show suspect gallery. Mirrors `_DrawGallery` @ 158f:0046.
+ void doGallery();
+
+ /// Show big map; click chooses next site. Mirrors `_DoBigMap` @ 20fe:09e7.
+ void doBigMap();
+
+ /// Run the accuse flow (pick suspect, evaluate chains, show ending).
+ /// Mirrors `_DoAccuseGallery` @ 1df2:0a31 + `_DisplayEnding` @ 1df2:0548.
+ void doAccuse();
+
+ /// Show a host hint from `KDTextIndex`. Mirrors `_KDHelp` @ 1560:010a +
+ /// `_DisplayHint` @ 1560:0009. Cycles between the two hint slots that
+ /// the original engine tracks via `_SawHelpHint`.
+ void doHelp();
+
+ /// "Are you sure?" yes/no dialog. Mirrors `_AreYouSure` @ 1a35:0a5c.
+ /// Returns true if the user picked YES.
+ bool areYouSure();
+
+private:
+ /**
+ * Central dispatch loop matching the original _ScreenDriver @ 1a35:0dc1.
+ * Each iteration calls the screen handler that matches _nextScreen.
+ * Handlers update _lastScreen / _nextScreen and return; the loop exits
+ * when _nextScreen == kScreenInvalid.
+ */
+ void screenDriver();
+
+ /**
+ * Open the five .DBD/.DBX archive pairs the way _InitGraphicsSystem
+ * @ 172b:0145 does at boot.
+ */
+ bool openArchives();
+
+ /** Slurp SITEPALS into @c _sitePals. Mirrors _ReadPalettes @ 172b:0d89. */
+ bool loadSitePalettes();
/**
* Upload palette index @p num (one of 40 stored in SITEPALS) to the
@@ -69,28 +131,73 @@ public:
*/
void setSitePalette(uint num);
- /** Blit @p pic to the screen at (0,0), expecting a 320x200 picture. */
- void blitFullScreen(const Picture &pic);
+ /**
+ * Upload a 6-bit VGA palette read from the head of an .ANM file (the
+ * first 0x300 bytes per Load_Sequence @ 2503:0006). Used until the
+ * full title-page animation chain is wired in.
+ */
+ bool setAnmPalette(const Common::Path &anmPath);
+public:
+ /// Public so SiteScreen can switch palettes per site.
+ void setSitePaletteForSite(uint siteNum) { setSitePalette(siteNum + 1); }
private:
+
+ /** Blit @p pic to @p x, @p y on screen. */
+ void blitAt(const Picture &pic, int x, int y);
+
+ /** Hold the current frame for up to @p maxMs or until the user inputs. */
+public:
+ void waitForInput(uint32 maxMs);
+private:
+
/**
- * Central dispatch loop matching the original _ScreenDriver @ 1a35:0dc1.
- * Each iteration restores video mode and calls the screen handler that
- * matches _nextScreen. Handlers update _lastScreen / _nextScreen and
- * return; the loop exits when _nextScreen == kScreenInvalid.
+ * Play a difference-encoded animation file (.ANM / .A) on the full
+ * 320x200 screen. Mirrors the data flow of `OpenDifferenceAnimation`
+ * @ 2520:0337 â `Load_Sequence` + `Play_Sequence`. Audio cues are
+ * skipped for now. The default frame delay is 120 ms to match the
+ * original FRAME_RATE = 0x78 used by `_DoOpeningAnims`.
+ *
+ * If @p holdLastFrame is true the call blocks on the final frame
+ * until the user clicks or hits a key â used for the title screen.
*/
- void screenDriver();
+ void playAnm(const Common::Path &path, uint frameDelayMs = 120,
+ bool holdLastFrame = false);
- bool pollEvents();
+ // Screen handlers â port targets in screens/ later.
+ void showEAKidsLogo();
+ void showHighScoreLogo();
+ void doNewPlayer(); ///< Mirrors `_NewPlayer` @ 1c33:0dda
+ void doChoosePartner();
+ void doCaseSelection();
+ void doSiteLoop();
+
+ /// Render the case briefing background + game/book decorations and
+ /// display the briefing ClueBlock. Mirrors `_DoInitClues` @ 1a35:0411
+ /// minus the live ANI sequence playback.
+ void doInitClues();
+
+ Common::String _playerName; ///< Substituted into 0x80 placeholders
+
+ /// Per-mystery solved state. 0 = unsolved, 1 = solved, 2 = solved
+ /// on first try. Mirrors `_PlayerRecord.SolvedMysteries[55]` in the
+ /// original `_DisplayCorrect` flow.
+ uint8 _mysteriesSolved[55] = {};
- Console *_console;
Common::RandomSource _rng;
- DBDArchive _picsArchive; ///< PICS.DBD/.DBX (mouse, buttons, markers, balloons sprites)
- Common::Array<byte> _sitePals; ///< 40 x 768 bytes of 6-bit VGA palettes from SITEPALS
+ DBDArchive _picsArchive; ///< PICS.DBD/.DBX (sprites, buttons, frame backgrounds)
+ DBDArchive _aniArchive; ///< ANI.DBD/.DBX (multi-frame character animations)
+ DBDArchive _sitesArchive; ///< SITES.DBD/.DBX (one full-screen scene per site)
+ DBDArchive _balloonArchive; ///< BALLOON.DBD/.DBX (speech-balloon sprites)
+ Mystery _mystery; ///< Currently-loaded case file (M<n>.BIN)
+ EEMFont _font; ///< FONT.FNT - main 8 px font
+
+ Common::Array<byte> _sitePals; ///< 40 x 768 bytes of 6-bit VGA palettes
uint16 _lastScreen; ///< Mirrors _LastScreen @ 2d5d:3f24
uint16 _nextScreen; ///< Mirrors _NextScreen @ 2d5d:3f26
+ uint8 _partner; ///< Mirrors _Partner: 0 = boy (Jake), 1 = girl (Jenny)
};
} // End of namespace EEM
diff --git a/engines/eem/font.cpp b/engines/eem/font.cpp
new file mode 100644
index 00000000000..f8ca1865c8d
--- /dev/null
+++ b/engines/eem/font.cpp
@@ -0,0 +1,139 @@
+/* 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/debug.h"
+#include "common/file.h"
+#include "common/textconsole.h"
+#include "common/tokenizer.h"
+
+#include "graphics/managed_surface.h"
+#include "graphics/surface.h"
+
+#include "eem/detection.h"
+#include "eem/font.h"
+
+namespace EEM {
+
+bool EEMFont::load(const Common::Path &path) {
+ Common::File f;
+ if (!f.open(path)) {
+ warning("EEMFont::load: cannot open %s", path.toString().c_str());
+ return false;
+ }
+
+ const uint16 numChars = f.readUint16LE();
+ if (numChars == 0 || numChars > 256) {
+ warning("EEMFont::load: %s reports %u chars", path.toString().c_str(), numChars);
+ return false;
+ }
+ _glyphs.resize(numChars);
+
+ for (uint i = 0; i < numChars; i++) {
+ FontGlyph &g = _glyphs[i];
+ g.height = f.readByte();
+ g.widthBits = f.readByte();
+ g.sizeBytes = f.readByte();
+ if (g.sizeBytes > 0) {
+ g.bitmap.resize(g.sizeBytes);
+ if (f.read(g.bitmap.data(), g.sizeBytes) != g.sizeBytes) {
+ warning("EEMFont::load: short bitmap read at glyph %u", i);
+ return false;
+ }
+ }
+ if (g.height > _maxHeight)
+ _maxHeight = g.height;
+ }
+
+ debugC(1, kDebugGfx, "Font %s loaded: %u glyphs, max h=%u",
+ path.toString().c_str(), numChars, _maxHeight);
+ return true;
+}
+
+int EEMFont::charWidth(byte c) const {
+ if (c >= _glyphs.size())
+ return 0;
+ return _glyphs[c].widthBits;
+}
+
+int EEMFont::stringWidth(const Common::String &s) const {
+ int w = 0;
+ for (uint i = 0; i < s.size(); i++)
+ w += charWidth((byte)s[i]);
+ return w;
+}
+
+int EEMFont::drawChar(Graphics::ManagedSurface *dst, int x, int y, byte c, byte color) const {
+ if (!dst || c >= _glyphs.size())
+ return 0;
+ const FontGlyph &g = _glyphs[c];
+ if (g.bitmap.empty())
+ return g.widthBits;
+
+ const uint bytesPerRow = (g.widthBits + 7) / 8;
+ for (uint row = 0; row < g.height; row++) {
+ const int dstY = y + (int)row;
+ if (dstY < 0 || dstY >= dst->h)
+ continue;
+ const byte *srcRow = g.bitmap.data() + row * bytesPerRow;
+ byte *dstRow = (byte *)dst->getBasePtr(0, dstY);
+ for (uint bit = 0; bit < g.widthBits; bit++) {
+ const int dstX = x + (int)bit;
+ if (dstX < 0 || dstX >= dst->w)
+ continue;
+ const byte mask = (byte)(0x80 >> (bit & 7));
+ if (srcRow[bit / 8] & mask)
+ dstRow[dstX] = color;
+ }
+ }
+ return g.widthBits;
+}
+
+int EEMFont::drawString(Graphics::ManagedSurface *dst, int x, int y,
+ const Common::String &s, byte color) const {
+ int penX = x;
+ for (uint i = 0; i < s.size(); i++)
+ penX += drawChar(dst, penX, y, (byte)s[i], color);
+ return penX - x;
+}
+
+int EEMFont::drawWordWrapped(Graphics::ManagedSurface *dst, int x, int y, int width,
+ const Common::String &s, byte color) const {
+ Common::StringTokenizer tok(s, " \t");
+ int penY = y;
+ Common::String line;
+
+ while (!tok.empty()) {
+ const Common::String word = tok.nextToken();
+ Common::String trial = line.empty() ? word : line + " " + word;
+ if (stringWidth(trial) <= width) {
+ line = trial;
+ } else {
+ drawString(dst, x, penY, line, color);
+ penY += _maxHeight + 1;
+ line = word;
+ }
+ }
+ if (!line.empty())
+ drawString(dst, x, penY, line, color);
+ return penY + _maxHeight - y;
+}
+
+} // End of namespace EEM
diff --git a/engines/eem/font.h b/engines/eem/font.h
new file mode 100644
index 00000000000..f175d5360fa
--- /dev/null
+++ b/engines/eem/font.h
@@ -0,0 +1,90 @@
+/* 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 EEM_FONT_H
+#define EEM_FONT_H
+
+#include "common/array.h"
+#include "common/path.h"
+#include "common/scummsys.h"
+#include "common/str.h"
+
+namespace Graphics {
+class ManagedSurface;
+}
+
+namespace EEM {
+
+/// One bitmap glyph, 1 bit per pixel, MSB first.
+struct FontGlyph {
+ uint8 height = 0;
+ uint8 widthBits = 0;
+ uint8 sizeBytes = 0;
+ Common::Array<byte> bitmap;
+};
+
+/**
+ * Loader for the engine's `.FNT` files (FONT.FNT, SYSTEM.FNT, TINY.FNT,
+ * 8PNTTHIN.FNT). Mirrors `_LoadFont` @ 1b66:023c.
+ *
+ * File layout:
+ * - u16 numChars
+ * - per char: u8 height, u8 widthBits, u8 sizeBytes, bytes[sizeBytes] bitmap
+ *
+ * Drawing mirrors `_ShowChar` @ 1b66:0346: each set bit becomes `fontColor`
+ * on the destination surface; clear bits are transparent.
+ */
+class EEMFont {
+public:
+ EEMFont() = default;
+
+ bool load(const Common::Path &path);
+
+ uint16 height() const { return _maxHeight; }
+
+ int charWidth(byte c) const;
+
+ /// Total pixel width of @p s when rendered (no shadow).
+ int stringWidth(const Common::String &s) const;
+
+ /// Draw @p c at (@p x, @p y) on @p dst with foreground @p color.
+ /// Returns the advance width.
+ int drawChar(Graphics::ManagedSurface *dst, int x, int y, byte c, byte color) const;
+
+ /// Draw @p s at (@p x, @p y) and return total advance width.
+ int drawString(Graphics::ManagedSurface *dst, int x, int y,
+ const Common::String &s, byte color) const;
+
+ /// Word-wrap @p s into the rect (x..x+width, y..) and draw line by line.
+ /// Mirrors the data flow of `_DoWordWrap` @ 1b66:04a7.
+ int drawWordWrapped(Graphics::ManagedSurface *dst, int x, int y, int width,
+ const Common::String &s, byte color) const;
+
+ bool isLoaded() const { return !_glyphs.empty(); }
+
+private:
+ Common::Array<FontGlyph> _glyphs;
+ uint16 _maxHeight = 0;
+};
+
+} // End of namespace EEM
+
+#endif
diff --git a/engines/eem/metaengine.cpp b/engines/eem/metaengine.cpp
index 0d61cf6a944..21dee682b00 100644
--- a/engines/eem/metaengine.cpp
+++ b/engines/eem/metaengine.cpp
@@ -24,6 +24,8 @@
#include "eem/eem.h"
+#include "common/system.h"
+
namespace EEM {
const char *EEMEngine::getGameId() const {
@@ -48,7 +50,8 @@ public:
}
bool hasFeature(MetaEngineFeature f) const override {
- return false;
+ return checkExtendedSaves(f) ||
+ f == kSupportsLoadingDuringStartup;
}
};
diff --git a/engines/eem/module.mk b/engines/eem/module.mk
index 34247658998..047409575c9 100644
--- a/engines/eem/module.mk
+++ b/engines/eem/module.mk
@@ -1,10 +1,13 @@
MODULE := engines/eem
MODULE_OBJS = \
- console.o \
+ animation.o \
eem.o \
+ font.o \
metaengine.o \
- resource.o
+ mystery.o \
+ resource.o \
+ site.o
# This module can be built as a plugin
ifeq ($(ENABLE_EEM), DYNAMIC_PLUGIN)
diff --git a/engines/eem/mystery.cpp b/engines/eem/mystery.cpp
new file mode 100644
index 00000000000..e3b4051a14a
--- /dev/null
+++ b/engines/eem/mystery.cpp
@@ -0,0 +1,261 @@
+/* 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/debug.h"
+#include "common/file.h"
+#include "common/random.h"
+#include "common/serializer.h"
+#include "common/textconsole.h"
+
+#include "eem/detection.h"
+#include "eem/mystery.h"
+
+namespace EEM {
+
+uint16 Mystery::readU16(uint offset) const {
+ if (offset + 2 > _data.size())
+ return 0;
+ return READ_LE_UINT16(_data.data() + offset);
+}
+
+void Mystery::clear() {
+ _data.clear();
+ _number = 0;
+ _initOffset = _mapOffset = _siteIndexOffset = _textOffset = 0;
+ _noteOffset = _galleryOffset = _kdTextOffset = 0;
+ _solvedOffset = _hintOffset = 0;
+ _numSites = 0;
+ _numSuspects = _numCONSITEs = _numCOFFSITEs = 0;
+ memset(_aChain, 0, sizeof(_aChain));
+ memset(_bChain, 0, sizeof(_bChain));
+ memset(_cChain, 0, sizeof(_cChain));
+ memset(_cluesFound, 0, sizeof(_cluesFound));
+ memset(_noteSelected, 0, sizeof(_noteSelected));
+ memset(_hotSpotsSeen, 0, sizeof(_hotSpotsSeen));
+ memset(_inGallery, 0, sizeof(_inGallery));
+ memset(_newOrder, 0, sizeof(_newOrder));
+ memset(_visitedSite, 0, sizeof(_visitedSite));
+ memset(_onSites, 0, sizeof(_onSites));
+ _sawCOFFSITEs = _sawCONSITEs = _sawHelpHint = _solvedPuzzle = false;
+ _firstTry = true;
+ _searchLocationNumber = _siteNumber = 0xFFFF;
+ _lastSite = 0x1B;
+}
+
+bool Mystery::load(uint num, Common::RandomSource *rng) {
+ const Common::String fname = Common::String::format("M%u.BIN", num);
+ Common::File f;
+ if (!f.open(Common::Path(fname))) {
+ warning("Mystery::load: cannot open %s", fname.c_str());
+ return false;
+ }
+
+ const int32 size = f.size();
+ if (size <= 64) {
+ warning("Mystery::load: %s too small (%d bytes)", fname.c_str(), size);
+ return false;
+ }
+
+ // Stage to a temporary buffer so a short read leaves the previous
+ // mystery state intact instead of half-clobbering `_data`.
+ Common::Array<byte> staging(size);
+ if (f.read(staging.data(), size) != (uint32)size) {
+ warning("Mystery::load: short read on %s", fname.c_str());
+ return false;
+ }
+ f.close();
+
+ _data = staging;
+ _number = num;
+
+ // Header is 16-bit-word indexed (matches `int *piVar1 = __Mystery; piVar1[N]`).
+ _initOffset = readU16(0 * 2);
+ _mapOffset = readU16(2 * 2);
+ _siteIndexOffset = readU16(3 * 2);
+ _textOffset = readU16(4 * 2);
+ _noteOffset = readU16(5 * 2);
+ _galleryOffset = readU16(6 * 2);
+ _kdTextOffset = readU16(7 * 2);
+ _solvedOffset = readU16(8 * 2);
+ _hintOffset = readU16(9 * 2);
+
+ _numSites = readU16(10 * 2);
+ _numSuspects = (uint8)readU16(13 * 2);
+ _numCONSITEs = (uint8)readU16(14 * 2);
+ _numCOFFSITEs = (uint8)readU16(15 * 2);
+
+ for (uint i = 0; i < kChainLen; i++) {
+ _aChain[i] = readU16((16 + i) * 2);
+ _bChain[i] = readU16((21 + i) * 2);
+ _cChain[i] = readU16((26 + i) * 2);
+ }
+
+ // Per-mystery runtime state â `_ReadMystery` zeroes these at load.
+ memset(_cluesFound, 0, sizeof(_cluesFound));
+ memset(_noteSelected, 0, sizeof(_noteSelected));
+ memset(_hotSpotsSeen, 0, sizeof(_hotSpotsSeen));
+ memset(_inGallery, 0, sizeof(_inGallery));
+ // `_NewOrder` in the original randomly cycles the gallery positions
+ // per playthrough. For consistency between clue side-effects (which
+ // write to `_inGallery[_newOrder[galIdx]]`) and gallery rendering
+ // (which iterates logical indices), we keep the identity mapping.
+ // If the original's randomized positioning is required later, both
+ // the side-effect path AND the rendering path need to use it together.
+ (void)rng;
+ for (uint i = 0; i < kGalleryCap; i++)
+ _newOrder[i] = (uint8)i;
+ memset(_visitedSite, 0, sizeof(_visitedSite));
+ memset(_onSites, 0, sizeof(_onSites));
+ _sawCOFFSITEs = _sawCONSITEs = _sawHelpHint = _solvedPuzzle = false;
+ _firstTry = true;
+ _searchLocationNumber = _siteNumber = 0xFFFF;
+ _lastSite = 0x1B; // Sentinel matching _ReadMystery's `_LastSite = 0x1b`.
+
+ debugC(1, kDebugMystery, "Loaded %s (%d B): %u sites, %u suspects, "
+ "CON=%u COFF=%u, init=0x%04x site=0x%04x text=0x%04x",
+ fname.c_str(), size, _numSites, _numSuspects,
+ _numCONSITEs, _numCOFFSITEs,
+ _initOffset, _siteIndexOffset, _textOffset);
+ return true;
+}
+
+const byte *Mystery::siteIndexEntry(uint siteNum) const {
+ if (!isLoaded() || siteNum >= _numSites)
+ return nullptr;
+ const uint off = _siteIndexOffset + siteNum * 6;
+ if (off + 6 > _data.size())
+ return nullptr;
+ return _data.data() + off;
+}
+
+const byte *Mystery::siteData(uint siteNum) const {
+ const byte *idx = siteIndexEntry(siteNum);
+ if (!idx)
+ return nullptr;
+ const uint16 dataOff = READ_LE_UINT16(idx);
+ if (dataOff >= _data.size())
+ return nullptr;
+ return _data.data() + dataOff;
+}
+
+const byte *Mystery::hotspots(uint siteNum) const {
+ const byte *idx = siteIndexEntry(siteNum);
+ if (!idx)
+ return nullptr;
+ const uint16 hotspotOff = READ_LE_UINT16(idx + 4);
+ if (hotspotOff >= _data.size())
+ return nullptr;
+ return _data.data() + hotspotOff;
+}
+
+uint16 Mystery::hotspotCount(uint siteNum) const {
+ const byte *site = siteData(siteNum);
+ if (!site || (size_t)(site - _data.data()) + 8 > _data.size())
+ return 0;
+ return READ_LE_UINT16(site + 6);
+}
+
+const char *Mystery::textAt(uint16 offset) const {
+ if (!isLoaded())
+ return "";
+ const uint pos = _textOffset + offset;
+ if (pos >= _data.size())
+ return "";
+ return (const char *)(_data.data() + pos);
+}
+
+const byte *Mystery::initBlock() const {
+ if (!isLoaded() || _initOffset >= _data.size())
+ return nullptr;
+ return _data.data() + _initOffset;
+}
+
+const byte *Mystery::galleryData() const {
+ if (!isLoaded() || _galleryOffset >= _data.size())
+ return nullptr;
+ return _data.data() + _galleryOffset;
+}
+
+const byte *Mystery::noteIndex() const {
+ if (!isLoaded() || _noteOffset >= _data.size())
+ return nullptr;
+ return _data.data() + _noteOffset;
+}
+
+uint16 Mystery::noteIndexCount() const {
+ if (!isLoaded())
+ return 0;
+ // NoteIndex runs from _noteOffset to the start of GalleryData.
+ if (_galleryOffset <= _noteOffset)
+ return 0;
+ return (uint16)((_galleryOffset - _noteOffset) / 4);
+}
+
+const byte *Mystery::kdTextIndex() const {
+ if (!isLoaded() || _kdTextOffset >= _data.size())
+ return nullptr;
+ return _data.data() + _kdTextOffset;
+}
+
+const byte *Mystery::mapEntry(uint siteNum) const {
+ if (!isLoaded() || siteNum >= _numSites)
+ return nullptr;
+ const uint off = _mapOffset + siteNum * 14;
+ if (off + 14 > _data.size())
+ return nullptr;
+ return _data.data() + off;
+}
+
+int Mystery::selectedPoints() const {
+ const byte *ni = noteIndex();
+ const uint16 cnt = noteIndexCount();
+ if (!ni || cnt == 0)
+ return 0;
+ int total = 0;
+ for (uint i = 0; i < cnt && i < kCluesFoundCap; i++) {
+ if (!_noteSelected[i])
+ continue;
+ // Each NoteIndex entry is 4 bytes: u16 textOff + u16 points.
+ const uint16 pts = READ_LE_UINT16(ni + i * 4 + 2);
+ total += (int)(int16)pts;
+ }
+ return total;
+}
+
+void Mystery::syncState(Common::Serializer &s) {
+ s.syncBytes(_cluesFound, kCluesFoundCap);
+ s.syncBytes(_noteSelected, kCluesFoundCap);
+ s.syncArray(_hotSpotsSeen, kHotSpotsCap, Common::Serializer::Uint16LE);
+ s.syncArray(_inGallery, kGalleryCap, Common::Serializer::Uint16LE);
+ s.syncBytes(_newOrder, kGalleryCap);
+ s.syncArray(_visitedSite, kVisitedSiteCap, Common::Serializer::Uint16LE);
+ s.syncArray(_onSites, kVisitedSiteCap, Common::Serializer::Uint16LE);
+ s.syncAsByte(_sawCOFFSITEs);
+ s.syncAsByte(_sawCONSITEs);
+ s.syncAsByte(_sawHelpHint);
+ s.syncAsByte(_solvedPuzzle);
+ s.syncAsByte(_firstTry);
+ s.syncAsUint16LE(_searchLocationNumber);
+ s.syncAsUint16LE(_siteNumber);
+ s.syncAsUint16LE(_lastSite);
+}
+
+} // End of namespace EEM
diff --git a/engines/eem/mystery.h b/engines/eem/mystery.h
new file mode 100644
index 00000000000..b0ea78677e6
--- /dev/null
+++ b/engines/eem/mystery.h
@@ -0,0 +1,188 @@
+/* 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 EEM_MYSTERY_H
+#define EEM_MYSTERY_H
+
+#include "common/array.h"
+#include "common/path.h"
+#include "common/scummsys.h"
+#include "common/serializer.h"
+#include "common/str.h"
+
+namespace EEM {
+
+/**
+ * One mystery (case file) loaded from `M<n>.BIN`.
+ *
+ * Mirrors the layout established by `_ReadMystery` @ 2404:008f:
+ *
+ * word[0] = InitBlock byte offset
+ * word[2] = MapData byte offset
+ * word[3] = SiteIndex byte offset
+ * word[4] = TextBlock byte offset
+ * word[5] = NoteIndex byte offset
+ * word[6] = GalleryData byte offset
+ * word[7] = KDTextIndex byte offset
+ * word[8] = SolvedClues byte offset
+ * word[9] = HintBlock byte offset
+ * word[10] = NumSites + start of MysteryStats
+ * word[13] = NumSuspects (low byte)
+ * word[14] = NumCONSITEs
+ * word[15] = NumCOFFSITEs
+ * word[16..20] = AChain (5 words)
+ * word[21..25] = BChain
+ * word[26..30] = CChain
+ *
+ * Per-mystery state is reset every time a mystery is loaded; chains and
+ * indices are pointers into the in-memory `_data` blob.
+ */
+class Mystery {
+public:
+ static const uint kNumChains = 3;
+ static const uint kChainLen = 5;
+ static const uint kCluesFoundCap = 100;
+ static const uint kHotSpotsCap = 100;
+ static const uint kGalleryCap = 5;
+ static const uint kVisitedSiteCap = 20;
+
+ Mystery() = default;
+ ~Mystery() = default;
+
+ /// Load `M<num>.BIN` and reset per-mystery state. Returns false on error.
+ bool load(uint num, class Common::RandomSource *rng = nullptr);
+
+ /// Drop the loaded mystery and zero per-mystery state. Safe to call
+ /// at any time; `isLoaded()` returns false afterward.
+ void clear();
+
+ /// True once `load()` succeeded and offsets are valid.
+ bool isLoaded() const { return !_data.empty(); }
+
+ uint number() const { return _number; }
+ uint16 numSites() const { return _numSites; }
+ uint8 numSuspects() const { return _numSuspects; }
+ uint8 numCONSITEs() const { return _numCONSITEs; }
+ uint8 numCOFFSITEs() const { return _numCOFFSITEs; }
+
+ /// Pointer to the InitBlock (case briefing).
+ /// _InitBlock @ 2d5d:?? = mystery + word[0] in `_ReadMystery`.
+ const byte *initBlock() const;
+
+ /// Pointer to the GalleryData; one 0x46-byte entry per suspect.
+ /// First u16 of each entry is the PIC picture ID for that suspect.
+ const byte *galleryData() const;
+
+ /// Pointer to the NoteIndex array (4 bytes per entry: u16 textOff + u16 pts).
+ const byte *noteIndex() const;
+
+ /// Number of entries in NoteIndex.
+ uint16 noteIndexCount() const;
+
+ /// Pointer to the KDTextIndex; first u16s are TextBlock offsets for
+ /// host hint lines.
+ const byte *kdTextIndex() const;
+
+ /// Pointer to the MapData entry for site @p siteNum (14 bytes per
+ /// entry; first u16 = sitepic, +4..7 = (x, y) on the big map).
+ const byte *mapEntry(uint siteNum) const;
+
+ /// Pointer to the SiteIndex entry for site @p siteNum (6 bytes per site).
+ const byte *siteIndexEntry(uint siteNum) const;
+
+ /// Pointer to the SiteData (sitepic, travel, hotspot count, ...)
+ /// referenced by SiteIndex[@p siteNum].
+ const byte *siteData(uint siteNum) const;
+
+ /// Pointer to the hotspot rectangle array for site @p siteNum.
+ /// Each rect is 14 bytes: x1, y1, x2, y2, then 6 bytes of clue data.
+ const byte *hotspots(uint siteNum) const;
+
+ /// Number of hotspots in site @p siteNum.
+ uint16 hotspotCount(uint siteNum) const;
+
+ /// Pointer to a NUL-terminated string at TextBlock+ at p offset.
+ const char *textAt(uint16 offset) const;
+
+ /// Pointer at byte offset @p offset within the mystery blob, or null
+ /// if out of range. Used to chase ClueBlock pointers stored in
+ /// hotspot data.
+ const byte *blobAt(uint32 offset) const {
+ return offset < _data.size() ? _data.data() + offset : nullptr;
+ }
+
+ /// Synchronize the per-mystery runtime state for save/load. The fixed
+ /// arrays serialize first, then the booleans and counters.
+ void syncState(Common::Serializer &s);
+
+ /// Sum of point values of every selected notebook entry. Mirrors
+ /// `_GetSelectedPoints` @ 1df2:00bd.
+ int selectedPoints() const;
+
+ /// True when `selectedPoints() > 99`. Mirrors `_SolvedCheck`.
+ bool solvedCheck() const { return selectedPoints() > 99; }
+
+ /// Per-mystery runtime state, zeroed at load time.
+ uint8 _cluesFound[kCluesFoundCap] = {};
+ uint8 _noteSelected[kCluesFoundCap] = {}; ///< Mirror `_NoteSelected`
+ uint16 _hotSpotsSeen[kHotSpotsCap] = {};
+ uint16 _inGallery[kGalleryCap] = {};
+ uint8 _newOrder[kGalleryCap] = {};
+ uint16 _visitedSite[kVisitedSiteCap] = {};
+ uint16 _onSites[kVisitedSiteCap] = {};
+ bool _sawCOFFSITEs = false;
+ bool _sawCONSITEs = false;
+ bool _sawHelpHint = false;
+ bool _solvedPuzzle = false;
+ bool _firstTry = true;
+ uint16 _searchLocationNumber = 0xFFFF;
+ uint16 _siteNumber = 0xFFFF;
+ uint16 _lastSite = 0xFFFF;
+
+private:
+ Common::Array<byte> _data;
+ uint _number = 0;
+
+ uint16 _initOffset = 0;
+ uint16 _mapOffset = 0;
+ uint16 _siteIndexOffset = 0;
+ uint16 _textOffset = 0;
+ uint16 _noteOffset = 0;
+ uint16 _galleryOffset = 0;
+ uint16 _kdTextOffset = 0;
+ uint16 _solvedOffset = 0;
+ uint16 _hintOffset = 0;
+
+ uint16 _numSites = 0;
+ uint8 _numSuspects = 0;
+ uint8 _numCONSITEs = 0;
+ uint8 _numCOFFSITEs = 0;
+
+ uint16 _aChain[kChainLen] = {};
+ uint16 _bChain[kChainLen] = {};
+ uint16 _cChain[kChainLen] = {};
+
+ uint16 readU16(uint offset) const;
+};
+
+} // End of namespace EEM
+
+#endif
diff --git a/engines/eem/resource.cpp b/engines/eem/resource.cpp
index 67c9ab8a78e..15fc9981d46 100644
--- a/engines/eem/resource.cpp
+++ b/engines/eem/resource.cpp
@@ -76,6 +76,43 @@ void DBDArchive::close() {
_index.clear();
}
+/**
+ * Read one 12-byte frame header + payload at the current stream position.
+ * Shared between picture and animation loaders since the layout is the same.
+ */
+static bool readFrame(Common::SeekableReadStream &stream, bool compressed, Picture &out) {
+ out.flags = stream.readUint16LE();
+ const uint16 height = stream.readUint16LE();
+ const uint16 width = stream.readUint16LE();
+ out.rowoff = stream.readUint16LE();
+ out.miscflags = stream.readUint16LE();
+ out.compsize = stream.readUint16LE();
+
+ if (width == 0 || height == 0) {
+ warning("readFrame: zero dimensions (%ux%u)", width, height);
+ return false;
+ }
+
+ out.surface.create(width, height, Graphics::PixelFormat::createFormatCLUT8());
+ const uint32 unpacked = (uint32)width * (uint32)height;
+
+ if (!compressed) {
+ if (stream.read(out.surface.getPixels(), unpacked) != unpacked) {
+ warning("readFrame: short raw read (%u bytes)", unpacked);
+ return false;
+ }
+ return true;
+ }
+
+ if (!Common::decompressDCL(&stream, (byte *)out.surface.getPixels(),
+ out.compsize, unpacked)) {
+ warning("readFrame: DCL decompression failed (%u packed -> %u pixels)",
+ out.compsize, unpacked);
+ return false;
+ }
+ return true;
+}
+
bool DBDArchive::loadEntry(uint num, Picture &out) {
if (num >= _index.size()) {
warning("DBDArchive::loadEntry: %u out of range (max %u)", num, _index.size());
@@ -88,44 +125,40 @@ bool DBDArchive::loadEntry(uint num, Picture &out) {
return false;
}
- // Mirrors _GetFromDB @ 172b:105d:
- // _fread(i) // 2-byte skip word (purpose unclear; always 0x0001)
- // _fread(pic, 1, 12) // 12-byte header
+ // Mirrors _GetFromDB @ 172b:105d. The 2-byte word read first matches
+ // loadAnimation's frame-count read; for picture entries it is always 1.
(void)_dbd.readUint16LE();
- out.flags = _dbd.readUint16LE();
- const uint16 height = _dbd.readUint16LE();
- const uint16 width = _dbd.readUint16LE();
- out.rowoff = _dbd.readUint16LE();
- out.miscflags = _dbd.readUint16LE();
- out.compsize = _dbd.readUint16LE();
+ return readFrame(_dbd, entry.compressed != 0, out);
+}
- if (width == 0 || height == 0) {
- warning("DBDArchive::loadEntry: %u has zero dimensions (%ux%u)",
- num, width, height);
+bool DBDArchive::loadAnimation(uint num, Animation &out) {
+ if (num >= _index.size()) {
+ warning("DBDArchive::loadAnimation: %u out of range (max %u)", num, _index.size());
return false;
}
- out.surface.create(width, height, Graphics::PixelFormat::createFormatCLUT8());
-
- if (entry.compressed == 0) {
- // Raw pixel data â read width*height bytes verbatim.
- const uint32 pixelCount = (uint32)width * (uint32)height;
- if (_dbd.read(out.surface.getPixels(), pixelCount) != pixelCount) {
- warning("DBDArchive::loadEntry: short raw read on %u", num);
- return false;
- }
- return true;
+ const DBEntry &entry = _index[num];
+ if (!_dbd.seek(entry.offset)) {
+ warning("DBDArchive::loadAnimation: seek to 0x%08x failed", entry.offset);
+ return false;
}
- // Compressed payload: feed the .DBD stream straight into the DCL
- // decoder, matching the pattern used by Neverhood's BLB archive.
- const uint32 unpacked = (uint32)width * (uint32)height;
- if (!Common::decompressDCL(&_dbd, (byte *)out.surface.getPixels(),
- out.compsize, unpacked)) {
- warning("DBDArchive::loadEntry: DCL decompression failed on %u "
- "(%u packed -> %u pixels)", num, out.compsize, unpacked);
+ // Mirrors _GetAnimation @ 172b:163a: u16 frame count, then N frames.
+ const uint16 frameCount = _dbd.readUint16LE();
+ if (frameCount == 0 || frameCount > 256) {
+ warning("DBDArchive::loadAnimation: %u has implausible frame count %u",
+ num, frameCount);
return false;
}
+
+ out.resize(frameCount);
+ for (uint16 i = 0; i < frameCount; i++) {
+ if (!readFrame(_dbd, entry.compressed != 0, out[i])) {
+ warning("DBDArchive::loadAnimation: frame %u/%u failed in entry %u",
+ i, frameCount, num);
+ return false;
+ }
+ }
return true;
}
diff --git a/engines/eem/resource.h b/engines/eem/resource.h
index a48deb85518..846bf0f1135 100644
--- a/engines/eem/resource.h
+++ b/engines/eem/resource.h
@@ -59,6 +59,9 @@ struct Picture {
Graphics::ManagedSurface surface;
};
+/// Multi-frame animation as stored in ANI.DBD â a sequence of Pictures.
+typedef Common::Array<Picture> Animation;
+
/**
* Reader for a .DBD + .DBX archive pair.
*
@@ -96,6 +99,13 @@ public:
*/
bool getPicture(uint num, Picture &out) { return loadEntry(num - 1, out); }
+ /**
+ * Load a multi-frame animation entry. Mirrors _GetAnimation @ 172b:163a:
+ * read u16 frameCount, then for each frame read a 12-byte header and
+ * decompress the payload. Used for ANI.DBD entries.
+ */
+ bool loadAnimation(uint num, Animation &out);
+
private:
Common::File _dbd;
Common::Array<DBEntry> _index;
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
new file mode 100644
index 00000000000..b20a92a96ea
--- /dev/null
+++ b/engines/eem/site.cpp
@@ -0,0 +1,341 @@
+/* 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/debug.h"
+#include "common/events.h"
+#include "common/system.h"
+#include "common/textconsole.h"
+
+#include "eem/detection.h"
+#include "eem/eem.h"
+#include "eem/mystery.h"
+#include "eem/site.h"
+
+namespace EEM {
+
+void SiteScreen::enter(uint siteNum) {
+ if (!_mystery || !_mystery->isLoaded()) {
+ warning("SiteScreen::enter: no mystery loaded");
+ return;
+ }
+ if (siteNum >= _mystery->numSites()) {
+ warning("SiteScreen::enter: site %u out of range (max %u)",
+ siteNum, _mystery->numSites());
+ return;
+ }
+
+ _mystery->_siteNumber = siteNum;
+ if (siteNum < Mystery::kVisitedSiteCap)
+ _mystery->_visitedSite[siteNum] = 1;
+ debugC(1, kDebugSite, "Entering site %u (%u hotspots)",
+ siteNum, _mystery->hotspotCount(siteNum));
+
+ // Palette: original `_BuildBackground` calls `GetPalette(sitenum + 1)`
+ // where sitenum is the global SITES.DBD index (= the per-mystery
+ // `sitepic` field), not the per-mystery site index.
+ const byte *sd = _mystery->siteData(siteNum);
+ const uint16 sitepic = sd ? READ_LE_UINT16(sd) : 0;
+ _vm->setSitePaletteForSite(sitepic);
+
+ renderBackground(siteNum);
+ renderHotspots(siteNum);
+ g_system->updateScreen();
+}
+
+void SiteScreen::run() {
+ if (!_mystery || !_mystery->isLoaded())
+ return;
+
+ uint cur = 0;
+ enter(cur);
+
+ while (!_vm->shouldQuit()) {
+ Common::Event event;
+ bool exitRequested = false;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ switch (event.type) {
+ case Common::EVENT_QUIT:
+ case Common::EVENT_RETURN_TO_LAUNCHER:
+ return;
+
+ case Common::EVENT_LBUTTONDOWN: {
+ const int idx = hotspotAtPoint(cur, event.mouse.x, event.mouse.y);
+ if (idx >= 0) {
+ onHotspotClicked(cur, (uint)idx);
+ // Restore the site BG after the clue overlay.
+ enter(cur);
+ }
+ break;
+ }
+
+ case Common::EVENT_KEYDOWN:
+ switch (event.kbd.keycode) {
+ case Common::KEYCODE_ESCAPE:
+ if (_vm->areYouSure())
+ return;
+ enter(cur);
+ break;
+ case Common::KEYCODE_m:
+ _vm->doBigMap();
+ // Either way the map covered the site â re-render.
+ if (_mystery->_siteNumber < _mystery->numSites())
+ cur = _mystery->_siteNumber;
+ enter(cur);
+ break;
+ case Common::KEYCODE_n:
+ _vm->doNotebook();
+ enter(cur);
+ break;
+ case Common::KEYCODE_g:
+ _vm->doGallery();
+ enter(cur);
+ break;
+ case Common::KEYCODE_a:
+ _vm->doAccuse();
+ exitRequested = true;
+ break;
+ case Common::KEYCODE_h:
+ _vm->doHelp();
+ enter(cur);
+ break;
+ case Common::KEYCODE_v:
+ _showHotspots = !_showHotspots;
+ enter(cur);
+ break;
+ case Common::KEYCODE_r:
+ // Restart the mystery from scratch (mirrors `_ReloadMystery`).
+ if (_mystery->load(_mystery->number())) {
+ cur = 0;
+ enter(cur);
+ }
+ break;
+ case Common::KEYCODE_QUESTION:
+ case Common::KEYCODE_F1: {
+ if (_vm->getFont().isLoaded()) {
+ Graphics::ManagedSurface help(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ help.clear();
+ const EEMFont &fnt = _vm->getFont();
+ int y = 8;
+ const char *lines[] = {
+ "EAGLE EYE MYSTERIES â keys",
+ "",
+ " click search a hotspot",
+ " V toggle hotspot outlines",
+ " M map (travel between sites)",
+ " N notebook (mark evidence with 1..9)",
+ " G gallery (suspect portraits)",
+ " H hint from the case host",
+ " A accuse a suspect",
+ " R restart current mystery",
+ " Tab next site (cycle)",
+ " F5 save / load (ScummVM dialog)",
+ " ? / F1 this help",
+ " Esc quit (with confirm)",
+ "",
+ "Notebook: select evidence with 1..9.",
+ "Selected-points > 99 wins the case."
+ };
+ for (uint i = 0; i < sizeof(lines)/sizeof(lines[0]); i++) {
+ fnt.drawString(&help, 8, y, lines[i], 0xF);
+ y += fnt.height() + 1;
+ }
+ g_system->copyRectToScreen(help.getPixels(),
+ help.pitch, 0, 0, 320, 200);
+ g_system->updateScreen();
+ _vm->waitForInput(60000);
+ enter(cur);
+ }
+ break;
+ }
+ case Common::KEYCODE_TAB:
+ _mystery->_lastSite = cur;
+ cur = (cur + 1) % _mystery->numSites();
+ enter(cur);
+ break;
+ default:
+ break;
+ }
+ break;
+
+ default:
+ break;
+ }
+ }
+ if (exitRequested)
+ return;
+ g_system->updateScreen();
+ g_system->delayMillis(10);
+ }
+}
+
+void SiteScreen::renderBackground(uint siteNum) {
+ // Mirrors `_BuildBackground` @ 172b:13e2 (simplified):
+ // 1. Load PIC 0x3d (the site frame) from PICS.DBD.
+ // 2. Load entry @p siteNum from SITES.DBD (the site scene).
+ // 3. Composite scene into the frame at the position carried in the
+ // SiteData fields.
+ // 4. Set palette to (siteNum + 1) â per-site palettes start at 1.
+ // We render frame + scene at (0,0); the original positions the scene
+ // at (x,y) read from the SiteData but we don't have the offsets fully
+ // decoded yet so a top-left placement will do.
+
+ // Frame.
+ Picture frame;
+ if (_vm->getPics().getPicture(0x3d, frame)) {
+ g_system->copyRectToScreen(frame.surface.getPixels(),
+ frame.surface.pitch,
+ 0, 0, frame.surface.w, frame.surface.h);
+ }
+
+ // Scene from SITES.DBD: indexed by `sitepic` from SiteData (global
+ // SITES.DBD entry, NOT the per-mystery site index). Falls back to
+ // `_GetPicture(sitepic)` if SITES is unavailable.
+ const byte *site = _mystery->siteData(siteNum);
+ const uint16 sitepic = site ? READ_LE_UINT16(site) : 0;
+ Picture scene;
+ bool haveScene = false;
+ bool fromPics = false;
+ if (sitepic > 0 && _vm->getSites().size() > sitepic - 1)
+ haveScene = _vm->getSites().loadEntry(sitepic - 1, scene);
+ if (!haveScene && sitepic > 0) {
+ haveScene = _vm->getPics().getPicture(sitepic, scene);
+ fromPics = haveScene;
+ }
+ if (haveScene) {
+ const int w = MIN<int>(scene.surface.w, 320);
+ const int h = MIN<int>(scene.surface.h, 200);
+ // Full-screen pictures (sitepic fallback) go at (0, 0); smaller
+ // SITES.DBD scenes are centred horizontally with the top below
+ // the HUD bar so progress info stays visible.
+ const int x = fromPics ? 0 : (320 - w) / 2;
+ const int y = fromPics ? 0 : (h < 180 ? 4 : 0);
+ g_system->copyRectToScreen(scene.surface.getPixels(),
+ scene.surface.pitch, x, y, w, h);
+ }
+}
+
+void SiteScreen::renderHotspots(uint siteNum) {
+ // HUD overlay: site number, found clues, selected points. Drawn at
+ // the BOTTOM of the screen so the scene's top row stays visible â
+ // 320x200 mode 13h has a small bottom strip that the original engine
+ // uses for tool buttons; we repurpose it for the HUD.
+ if (_vm->getFont().isLoaded()) {
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (screen) {
+ uint cluesFound = 0;
+ for (uint i = 0; i < Mystery::kCluesFoundCap; i++)
+ if (_mystery->_cluesFound[i])
+ cluesFound++;
+ Common::String hud = Common::String::format(
+ "Site %u Clues %u Pts %d M N G H A R V Tab ?",
+ siteNum, cluesFound, _mystery->selectedPoints());
+ const int hudY = 192;
+ screen->fillRect(Common::Rect(0, hudY, 320, 200), 0);
+ Graphics::ManagedSurface mgr(320, 9,
+ Graphics::PixelFormat::createFormatCLUT8());
+ mgr.clear();
+ _vm->getFont().drawString(&mgr, 4, 0, hud, 0x0F);
+ for (int row = 0; row < 8; row++) {
+ memcpy((byte *)screen->getBasePtr(0, hudY + row),
+ (const byte *)mgr.getBasePtr(0, row), 320);
+ }
+ g_system->unlockScreen();
+ }
+ }
+
+ // Hotspot outlines (`_DrawSearchButtons`): toggle via V.
+ if (!_showHotspots)
+ return;
+
+ const byte *spots = _mystery->hotspots(siteNum);
+ const uint16 count = _mystery->hotspotCount(siteNum);
+ if (!spots)
+ return;
+
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (!screen)
+ return;
+
+ for (uint i = 0; i < count; i++) {
+ const byte *r = spots + i * 14;
+ const int16 x1 = (int16)READ_LE_UINT16(r + 0);
+ const int16 y1 = (int16)READ_LE_UINT16(r + 2);
+ const int16 x2 = (int16)READ_LE_UINT16(r + 4);
+ const int16 y2 = (int16)READ_LE_UINT16(r + 6);
+ const Common::Rect rect(MAX<int>(0, x1), MAX<int>(0, y1),
+ MIN<int>(screen->w, x2),
+ MIN<int>(screen->h, y2));
+ // Hotspots flagged as "seen" get a different colour so the
+ // player knows they've already searched them.
+ const byte color =
+ (i < Mystery::kHotSpotsCap && _mystery->_hotSpotsSeen[i]) ? 0x07 : 0x0F;
+ screen->frameRect(rect, color);
+ }
+
+ g_system->unlockScreen();
+}
+
+int SiteScreen::hotspotAtPoint(uint siteNum, int x, int y) const {
+ const byte *spots = _mystery->hotspots(siteNum);
+ const uint16 count = _mystery->hotspotCount(siteNum);
+ if (!spots)
+ return -1;
+
+ for (uint i = 0; i < count; i++) {
+ const byte *r = spots + i * 14;
+ const int16 x1 = (int16)READ_LE_UINT16(r + 0);
+ const int16 y1 = (int16)READ_LE_UINT16(r + 2);
+ const int16 x2 = (int16)READ_LE_UINT16(r + 4);
+ const int16 y2 = (int16)READ_LE_UINT16(r + 6);
+ if (x >= x1 && x < x2 && y >= y1 && y < y2)
+ return (int)i;
+ }
+ return -1;
+}
+
+void SiteScreen::onHotspotClicked(uint siteNum, uint hotIdx) {
+ debugC(1, kDebugSite, "Site %u: hotspot %u clicked", siteNum, hotIdx);
+
+ // Mark the hotspot itself as seen.
+ if (hotIdx < Mystery::kHotSpotsCap)
+ _mystery->_hotSpotsSeen[hotIdx] = 1;
+ _mystery->_searchLocationNumber = (uint16)hotIdx;
+
+ // Bytes 8..9 of each 14-byte hotspot rect = byte offset within the
+ // mystery blob pointing at a ClueBlock. Verified against M0.BIN:
+ // site 0 hotspot 0 -> 0x0502 -> "Hi! I think somebody's playing a
+ // trick on us...". `displayClue` runs the entry's side effects
+ // (`_AddNotebook` for ClueEntry +0x30..+0x39, gallery +0x26..+0x2f,
+ // onsite +0x1c..+0x25) so we don't need to touch `_cluesFound` here.
+ const byte *spots = _mystery->hotspots(siteNum);
+ if (spots) {
+ const uint16 clueOff = READ_LE_UINT16(spots + hotIdx * 14 + 8);
+ debugC(2, kDebugSite, " hotspot %u -> clue offset 0x%04x",
+ hotIdx, clueOff);
+ const byte *clueBlock = _mystery->blobAt(clueOff);
+ if (clueBlock)
+ _vm->displayClue(clueBlock);
+ }
+ // Caller (`SiteScreen::run`) re-renders the site after this returns.
+}
+
+} // End of namespace EEM
diff --git a/engines/eem/site.h b/engines/eem/site.h
new file mode 100644
index 00000000000..ce450bb1905
--- /dev/null
+++ b/engines/eem/site.h
@@ -0,0 +1,75 @@
+/* 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 EEM_SITE_H
+#define EEM_SITE_H
+
+#include "common/rect.h"
+#include "common/scummsys.h"
+
+namespace EEM {
+
+class EEMEngine;
+class Mystery;
+
+/// One hotspot (search rectangle) within a site, 14 bytes on disk.
+struct Hotspot {
+ int16 x1, y1, x2, y2; ///< rectangle in screen coordinates
+ uint16 clueOffset; ///< +8: byte offset of ClueBlock in the mystery file
+ uint16 hotspotIndex; ///< +10: 1-based hotspot ordinal within the site
+ uint16 extra; ///< +12: unknown (zero in M0..M54)
+
+ Common::Rect rect() const { return Common::Rect(x1, y1, x2, y2); }
+};
+
+/**
+ * Site / scene controller.
+ *
+ * Walks the mystery's SiteIndex to render one site at a time, polls hotspots
+ * for the player's search clicks, and dispatches clue display. Mirrors the
+ * site loop driven by `_DrawSearchButtons` @ 2404:0a8f and `_SearchButtons`
+ * @ 2404:0bfb.
+ */
+class SiteScreen {
+public:
+ SiteScreen(EEMEngine *vm, Mystery *mystery)
+ : _vm(vm), _mystery(mystery) {}
+
+ /** Enter site @p siteNum. Renders BG + hotspots; runs the input loop. */
+ void enter(uint siteNum);
+
+ /// Run the per-mystery loop: site -> map -> next site, ESC exits.
+ void run();
+
+private:
+ void renderBackground(uint siteNum);
+ void renderHotspots(uint siteNum);
+ int hotspotAtPoint(uint siteNum, int x, int y) const;
+ void onHotspotClicked(uint siteNum, uint hotIdx);
+
+ EEMEngine *_vm;
+ Mystery *_mystery;
+ bool _showHotspots = true; ///< Toggle outlines with V key.
+};
+
+} // End of namespace EEM
+
+#endif
Commit: 8ccba0b3474d69485c07cd04d14e572b1c319078
https://github.com/scummvm/scummvm/commit/8ccba0b3474d69485c07cd04d14e572b1c319078
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:33+02:00
Commit Message:
EEM: correct font rendering using char to glyph table
Changed paths:
engines/eem/eem.cpp
engines/eem/font.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 4bd0da9430a..3ac46881d01 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -753,7 +753,11 @@ static Common::String parseString(const Common::String &raw,
const byte c = (byte)raw[i];
if (c == 0x80) {
out += playerName;
- } else if (c == 0x82) {
+ } else if (c == 0x81 || c == 0x82) {
+ // Both forms substitute the partner's name. The original
+ // likely has a casual ("Jake/Jenny") and a formal
+ // ("Jake/Jennifer") variant; we render the same partner
+ // name in both spots â close enough for natural reading.
out += partnerName;
} else if (c >= 0x80 && c < 0x8A) {
// Other control opcodes: eat them silently for now.
diff --git a/engines/eem/font.cpp b/engines/eem/font.cpp
index f8ca1865c8d..45d4a12a894 100644
--- a/engines/eem/font.cpp
+++ b/engines/eem/font.cpp
@@ -32,6 +32,34 @@
namespace EEM {
+// Character -> glyph translation table from CHR2FNT segment (29b6:0000).
+// 128 bytes mapping ASCII 0..127 to a glyph index in FONT.FNT (which only
+// stores 85 glyphs covering ' ', '!'..'9', ':', 'A'..'Z'). Lowercase
+// letters map to uppercase glyphs; chars without a glyph map to 0.
+static const byte kCharToGlyph[128] = {
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // 0x20..0x27 ' '..'\''
+ 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, // 0x28..0x2F '('..'/'
+ 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, // 0x30..0x37 '0'..'7'
+ 0x18, 0x19, 0x1A, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x38..0x3F '8','9',':'..
+ 0x00, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, // 0x40..0x47 '@','A'..'G'
+ 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, // 0x48..0x4F 'H'..'O'
+ 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, // 0x50..0x57 'P'..'W'
+ 0x52, 0x53, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x58..0x5F 'X','Y','Z'..
+ 0x00, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, // 0x60..0x67 '`','a'..'g'
+ 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, // 0x68..0x6F 'h'..'o'
+ 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, // 0x70..0x77 'p'..'w'
+ 0x52, 0x53, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00 // 0x78..0x7F 'x','y','z'..
+};
+
+static inline byte mapChar(byte c) {
+ return c < 128 ? kCharToGlyph[c] : 0;
+}
+
+
bool EEMFont::load(const Common::Path &path) {
Common::File f;
if (!f.open(path)) {
@@ -68,9 +96,10 @@ bool EEMFont::load(const Common::Path &path) {
}
int EEMFont::charWidth(byte c) const {
- if (c >= _glyphs.size())
+ const byte g = mapChar(c);
+ if (g >= _glyphs.size())
return 0;
- return _glyphs[c].widthBits;
+ return _glyphs[g].widthBits;
}
int EEMFont::stringWidth(const Common::String &s) const {
@@ -81,9 +110,12 @@ int EEMFont::stringWidth(const Common::String &s) const {
}
int EEMFont::drawChar(Graphics::ManagedSurface *dst, int x, int y, byte c, byte color) const {
- if (!dst || c >= _glyphs.size())
+ if (!dst)
+ return 0;
+ const byte gi = mapChar(c);
+ if (gi >= _glyphs.size())
return 0;
- const FontGlyph &g = _glyphs[c];
+ const FontGlyph &g = _glyphs[gi];
if (g.bitmap.empty())
return g.widthBits;
Commit: f349111de13c1c9ced40cd28a13ebcf2120f10e6
https://github.com/scummvm/scummvm/commit/f349111de13c1c9ced40cd28a13ebcf2120f10e6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:33+02:00
Commit Message:
EEM: use Font API to reduce code duplication
Changed paths:
engines/eem/eem.cpp
engines/eem/font.cpp
engines/eem/font.h
engines/eem/site.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 3ac46881d01..793f1b6505a 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -388,13 +388,11 @@ void EEMEngine::doNewPlayer() {
memcpy((byte *)scratch.getBasePtr(0, row),
(const byte *)bg.surface.getBasePtr(0, row), w);
}
- _font.drawString(&scratch, 40, 24,
- "Welcome to Eagle Eye Mysteries!", 0xF);
- _font.drawString(&scratch, 40, 40, "Please type your name:", 0xF);
- _font.drawString(&scratch, 40, 60,
- "(Backspace to delete, Enter to confirm)", 0xF);
+ _font.drawString(&scratch, "Welcome to Eagle Eye Mysteries!", 40, 24, 320, 0xF);
+ _font.drawString(&scratch, "Please type your name:", 40, 40, 320, 0xF);
+ _font.drawString(&scratch, "(Backspace to delete, Enter to confirm)", 40, 60, 320, 0xF);
Common::String shown = name + "_";
- _font.drawString(&scratch, 40, 90, shown, 0xF);
+ _font.drawString(&scratch, shown, 40, 90, 320, 0xF);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
g_system->updateScreen();
@@ -555,8 +553,7 @@ void EEMEngine::doCaseSelection() {
}
}
if (_font.isLoaded()) {
- _font.drawString(&scratch, 8, 4,
- Common::String::format("EAGLE EYE - %s", _playerName.c_str()), 0xF);
+ _font.drawString(&scratch, Common::String::format("EAGLE EYE - %s", _playerName.c_str()), 8, 4, 320, 0xF);
// Solved count.
uint solved = 0, perfectSolved = 0;
@@ -564,15 +561,12 @@ void EEMEngine::doCaseSelection() {
if (_mysteriesSolved[i] >= 1) solved++;
if (_mysteriesSolved[i] == 2) perfectSolved++;
}
- _font.drawString(&scratch, 200, 4,
- Common::String::format("solved %u (1st try %u)",
- solved, perfectSolved), 0xF);
+ _font.drawString(&scratch, Common::String::format("solved %u (1st try %u)",
+ solved, perfectSolved), 200, 4, 320, 0xF);
if (perfectSolved >= 55) {
- _font.drawString(&scratch, 8, 168,
- "** PERFECT MASTER SLEUTH! **", 0xF);
+ _font.drawString(&scratch, "** PERFECT MASTER SLEUTH! **", 8, 168, 320, 0xF);
} else if (solved >= 55) {
- _font.drawString(&scratch, 8, 168,
- "** ALL MYSTERIES SOLVED! **", 0xF);
+ _font.drawString(&scratch, "** ALL MYSTERIES SOLVED! **", 8, 168, 320, 0xF);
}
char marker = ' ';
@@ -586,29 +580,18 @@ void EEMEngine::doCaseSelection() {
if (sel >= 1 && sel <= 24) tier = "Junior Sleuth";
else if (sel >= 25 && sel <= 48) tier = "Senior Sleuth";
else if (sel >= 49 && sel <= 54) tier = "Master Sleuth";
- _font.drawString(&scratch, 8, 24,
- Common::String::format("Mystery %u %c [%s]",
- sel, marker, tier), 0xF);
- _font.drawString(&scratch, 8, 40,
- " 0..9 quick select", 0xF);
- _font.drawString(&scratch, 8, 52,
- " Tab / + next mystery", 0xF);
- _font.drawString(&scratch, 8, 64,
- " Shift+Tab prev mystery", 0xF);
- _font.drawString(&scratch, 8, 76,
- " PgUp/PgDn jump 10", 0xF);
- _font.drawString(&scratch, 8, 88,
- " Home/End first/last", 0xF);
- _font.drawString(&scratch, 8, 100,
- " Enter start mystery", 0xF);
- _font.drawString(&scratch, 8, 112,
- " F5 save / load (ScummVM)", 0xF);
- _font.drawString(&scratch, 8, 124,
- " ESC quit", 0xF);
- _font.drawString(&scratch, 8, 144,
- " * solved on first try", 0xF);
- _font.drawString(&scratch, 8, 156,
- " + solved", 0xF);
+ _font.drawString(&scratch, Common::String::format("Mystery %u %c [%s]",
+ sel, marker, tier), 8, 24, 320, 0xF);
+ _font.drawString(&scratch, " 0..9 quick select", 8, 40, 320, 0xF);
+ _font.drawString(&scratch, " Tab / + next mystery", 8, 52, 320, 0xF);
+ _font.drawString(&scratch, " Shift+Tab prev mystery", 8, 64, 320, 0xF);
+ _font.drawString(&scratch, " PgUp/PgDn jump 10", 8, 76, 320, 0xF);
+ _font.drawString(&scratch, " Home/End first/last", 8, 88, 320, 0xF);
+ _font.drawString(&scratch, " Enter start mystery", 8, 100, 320, 0xF);
+ _font.drawString(&scratch, " F5 save / load (ScummVM)", 8, 112, 320, 0xF);
+ _font.drawString(&scratch, " ESC quit", 8, 124, 320, 0xF);
+ _font.drawString(&scratch, " * solved on first try", 8, 144, 320, 0xF);
+ _font.drawString(&scratch, " + solved", 8, 156, 320, 0xF);
}
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
@@ -910,7 +893,7 @@ void EEMEngine::displayClue(const byte *clueBlock) {
int textY = bubY;
int textW = MIN<int>(320 - bubX, 200);
int copyY = bubY;
- int copyH = _font.height() * 4 + 8;
+ int copyH = _font.getFontHeight() * 4 + 8;
if (haveBalloon) {
const int bw = MIN<int>(balloon.surface.w, 320 - bubX);
@@ -1002,9 +985,8 @@ void EEMEngine::doNotebook() {
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
- _font.drawString(&scratch, 8, 4, "NOTEBOOK", 0xF);
- _font.drawString(&scratch, 200, 4,
- Common::String::format("pts: %d", _mystery.selectedPoints()), 0xF);
+ _font.drawString(&scratch, "NOTEBOOK", 8, 4, 320, 0xF);
+ _font.drawString(&scratch, Common::String::format("pts: %d", _mystery.selectedPoints()), 200, 4, 320, 0xF);
// Build a list of found-clue indices.
Common::Array<uint> found;
@@ -1015,13 +997,12 @@ void EEMEngine::doNotebook() {
const int pages = MAX<int>(1, (total + kPerPage - 1) / kPerPage);
page = MIN<int>(page, pages - 1);
- _font.drawString(&scratch, 200, 16,
- Common::String::format("page %d/%d", page + 1, pages), 0xF);
+ _font.drawString(&scratch, Common::String::format("page %d/%d", page + 1, pages), 200, 16, 320, 0xF);
const byte *ni = _mystery.noteIndex();
const uint16 niCount = _mystery.noteIndexCount();
const Common::String partnerName = (_partner == 0) ? "Jake" : "Jennifer";
- int y = 4 + _font.height() * 2 + 4;
+ int y = 4 + _font.getFontHeight() * 2 + 4;
for (int slot = 0; slot < kPerPage; slot++) {
const int idx = page * kPerPage + slot;
if (idx >= total)
@@ -1118,7 +1099,7 @@ void EEMEngine::doGallery() {
}
if (_font.isLoaded())
- _font.drawString(&scratch, 8, 4, "GALLERY", 0xF);
+ _font.drawString(&scratch, "GALLERY", 8, 4, 320, 0xF);
const uint8 num = _mystery.numSuspects();
int slotX = 8;
@@ -1147,7 +1128,7 @@ void EEMEngine::doGallery() {
_mystery._inGallery[i];
Common::String label = Common::String::format("%u%s",
i + 1, discovered ? " *" : "");
- _font.drawString(&scratch, placeX + 4, placeY + h + 2, label, 0xF);
+ _font.drawString(&scratch, label, placeX + 4, placeY + h + 2, 320, 0xF);
}
slotX += slotStep;
}
@@ -1254,7 +1235,7 @@ void EEMEngine::doBigMap() {
scratch.fillRect(mark, color);
if (_font.isLoaded()) {
Common::String num = Common::String::format("%u", i);
- _font.drawString(&scratch, sx + 4, sy - 4, num, color);
+ _font.drawString(&scratch, num, sx + 4, sy - 4, 320, color);
}
}
}
@@ -1263,19 +1244,18 @@ void EEMEngine::doBigMap() {
if (_font.isLoaded() && _mystery.isLoaded()) {
const int panelX = kMapWinX + kMapWinW + 4;
int y = 4;
- _font.drawString(&scratch, panelX, y, "TRAVEL", 0xF);
- y += _font.height() + 4;
+ _font.drawString(&scratch, "TRAVEL", panelX, y, 320, 0xF);
+ y += _font.getFontHeight() + 4;
for (uint i = 0; i < _mystery.numSites() && y < 192; i++) {
if (!_mystery._onSites[i] && i != _mystery._siteNumber)
continue;
const char marker = (i == _mystery._siteNumber) ? '>' : ' ';
Common::String label = Common::String::format(
"%c %u", marker, i);
- _font.drawString(&scratch, panelX, y, label, 0x0F);
- y += _font.height() + 1;
+ _font.drawString(&scratch, label, panelX, y, 320, 0x0F);
+ y += _font.getFontHeight() + 1;
}
- _font.drawString(&scratch, panelX, 188,
- "Esc", 0x0F);
+ _font.drawString(&scratch, "Esc", panelX, 188, 320, 0x0F);
}
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
@@ -1350,8 +1330,8 @@ void EEMEngine::doBigMap() {
const int panelX = kMapWinX + kMapWinW + 4;
if (_font.isLoaded() && _mystery.isLoaded() &&
ev.mouse.x >= panelX) {
- const int row = (ev.mouse.y - 4 - _font.height() - 4) /
- (_font.height() + 1);
+ const int row = (ev.mouse.y - 4 - _font.getFontHeight() - 4) /
+ (_font.getFontHeight() + 1);
int seen = 0;
for (uint i = 0; i < _mystery.numSites(); i++) {
if (!_mystery._onSites[i] && i != _mystery._siteNumber)
@@ -1401,7 +1381,7 @@ void EEMEngine::doHelp() {
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
- _font.drawString(&scratch, 8, 4, "HELP", 0xF);
+ _font.drawString(&scratch, "HELP", 8, 4, 320, 0xF);
_font.drawWordWrapped(&scratch, 8, 24, 304, text, 0xF);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
@@ -1435,10 +1415,9 @@ bool EEMEngine::areYouSure() {
(const byte *)saved.getBasePtr(0, row), 320);
scratch.fillRect(dlg, 0);
scratch.frameRect(dlg, 0xF);
- _font.drawString(&scratch, dlg.left + 8, dlg.top + 8,
- "Are you sure you want to quit?", 0xF);
- _font.drawString(&scratch, dlg.left + 16, dlg.top + 36, "Y - Yes", 0xF);
- _font.drawString(&scratch, dlg.left + 100, dlg.top + 36, "N - No", 0xF);
+ _font.drawString(&scratch, "Are you sure you want to quit?", dlg.left + 8, dlg.top + 8, 320, 0xF);
+ _font.drawString(&scratch, "Y - Yes", dlg.left + 16, dlg.top + 36, 320, 0xF);
+ _font.drawString(&scratch, "N - No", dlg.left + 100, dlg.top + 36, 320, 0xF);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
g_system->updateScreen();
@@ -1507,7 +1486,7 @@ void EEMEngine::doAccuse() {
}
}
if (_font.isLoaded())
- _font.drawString(&scratch, 8, 4, "ACCUSE", 0xF);
+ _font.drawString(&scratch, "ACCUSE", 8, 4, 320, 0xF);
for (uint i = 0; i < num; i++) {
if (!gd) continue;
@@ -1529,13 +1508,11 @@ void EEMEngine::doAccuse() {
}
if (_font.isLoaded()) {
Common::String label = Common::String::format("%u", i + 1);
- _font.drawString(&scratch, placeX + 4,
- placeY + h + 2, label, 0xF);
+ _font.drawString(&scratch, label, placeX + 4, placeY + h + 2, 320, 0xF);
}
}
if (_font.isLoaded()) {
- _font.drawString(&scratch, 8, 180,
- "Click a suspect or press 1..N - ESC to cancel", 0xF);
+ _font.drawString(&scratch, "Click a suspect or press 1..N - ESC to cancel", 8, 180, 320, 0xF);
}
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
@@ -1668,17 +1645,15 @@ void EEMEngine::doAccuse() {
Common::String banner = "Not enough evidence";
if (guessedRight)
banner = _mystery._firstTry ? "CORRECT - FIRST TRY!" : "CORRECT!";
- _font.drawString(&scratch, 8, 4, banner, 0xF);
- _font.drawString(&scratch, 8, 16,
- Common::String::format("Evidence: %d/100 Suspect: %d",
- points, picked + 1), 0xF);
+ _font.drawString(&scratch, banner, 8, 4, 320, 0xF);
+ _font.drawString(&scratch, Common::String::format("Evidence: %d/100 Suspect: %d",
+ points, picked + 1), 8, 16, 320, 0xF);
const int wrapW = MAX<int>(16, x2 - x1);
const int wrapY = MAX<int>(28, (int)y1);
(void)y2;
_font.drawWordWrapped(&scratch, x1, wrapY, wrapW, txt, 0xF);
- _font.drawString(&scratch, 8, 188,
- Common::String::format("page %u/%u (Left/Right or click)",
- pageIdx + 1, pages), 0xF);
+ _font.drawString(&scratch, Common::String::format("page %u/%u (Left/Right or click)",
+ pageIdx + 1, pages), 8, 188, 320, 0xF);
}
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
diff --git a/engines/eem/font.cpp b/engines/eem/font.cpp
index 45d4a12a894..d01e22d3a0e 100644
--- a/engines/eem/font.cpp
+++ b/engines/eem/font.cpp
@@ -22,9 +22,7 @@
#include "common/debug.h"
#include "common/file.h"
#include "common/textconsole.h"
-#include "common/tokenizer.h"
-#include "graphics/managed_surface.h"
#include "graphics/surface.h"
#include "eem/detection.h"
@@ -32,9 +30,9 @@
namespace EEM {
-// Character -> glyph translation table from CHR2FNT segment (29b6:0000).
-// 128 bytes mapping ASCII 0..127 to a glyph index in FONT.FNT (which only
-// stores 85 glyphs covering ' ', '!'..'9', ':', 'A'..'Z'). Lowercase
+// Character â glyph translation table from segment CHR2FNT (29b6:0000).
+// 128 bytes mapping ASCII 0..127 to a glyph index in FONT.FNT (which
+// only stores 85 glyphs covering ' ', '!'..'9', ':', 'A'..'Z'). Lowercase
// letters map to uppercase glyphs; chars without a glyph map to 0.
static const byte kCharToGlyph[128] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
@@ -55,11 +53,10 @@ static const byte kCharToGlyph[128] = {
0x52, 0x53, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00 // 0x78..0x7F 'x','y','z'..
};
-static inline byte mapChar(byte c) {
+static inline byte mapChar(uint32 c) {
return c < 128 ? kCharToGlyph[c] : 0;
}
-
bool EEMFont::load(const Common::Path &path) {
Common::File f;
if (!f.open(path)) {
@@ -69,10 +66,12 @@ bool EEMFont::load(const Common::Path &path) {
const uint16 numChars = f.readUint16LE();
if (numChars == 0 || numChars > 256) {
- warning("EEMFont::load: %s reports %u chars", path.toString().c_str(), numChars);
+ warning("EEMFont::load: %s reports %u chars",
+ path.toString().c_str(), numChars);
return false;
}
_glyphs.resize(numChars);
+ _maxHeight = _maxWidth = 0;
for (uint i = 0; i < numChars; i++) {
FontGlyph &g = _glyphs[i];
@@ -88,36 +87,43 @@ bool EEMFont::load(const Common::Path &path) {
}
if (g.height > _maxHeight)
_maxHeight = g.height;
+ if (g.widthBits > _maxWidth)
+ _maxWidth = g.widthBits;
}
- debugC(1, kDebugGfx, "Font %s loaded: %u glyphs, max h=%u",
- path.toString().c_str(), numChars, _maxHeight);
+ debugC(1, kDebugGfx, "Font %s loaded: %u glyphs, max %ux%u",
+ path.toString().c_str(), numChars, _maxWidth, _maxHeight);
return true;
}
-int EEMFont::charWidth(byte c) const {
- const byte g = mapChar(c);
- if (g >= _glyphs.size())
+int EEMFont::getCharWidth(uint32 chr) const {
+ const byte gi = mapChar(chr);
+ if (gi >= _glyphs.size())
return 0;
- return _glyphs[g].widthBits;
+ return _glyphs[gi].widthBits;
}
-int EEMFont::stringWidth(const Common::String &s) const {
- int w = 0;
- for (uint i = 0; i < s.size(); i++)
- w += charWidth((byte)s[i]);
- return w;
+int EEMFont::drawWordWrapped(Graphics::ManagedSurface *dst, int x, int y,
+ int width, const Common::String &s,
+ uint32 color) const {
+ Common::Array<Common::String> lines;
+ wordWrapText(s, width, lines);
+ const int lineH = getFontHeight() + 1;
+ for (uint i = 0; i < lines.size(); i++)
+ drawString(dst, lines[i], x, y + (int)i * lineH, width, color);
+ return (int)lines.size() * lineH;
}
-int EEMFont::drawChar(Graphics::ManagedSurface *dst, int x, int y, byte c, byte color) const {
+void EEMFont::drawChar(Graphics::Surface *dst, uint32 chr, int x, int y,
+ uint32 color) const {
if (!dst)
- return 0;
- const byte gi = mapChar(c);
+ return;
+ const byte gi = mapChar(chr);
if (gi >= _glyphs.size())
- return 0;
+ return;
const FontGlyph &g = _glyphs[gi];
if (g.bitmap.empty())
- return g.widthBits;
+ return;
const uint bytesPerRow = (g.widthBits + 7) / 8;
for (uint row = 0; row < g.height; row++) {
@@ -132,40 +138,9 @@ int EEMFont::drawChar(Graphics::ManagedSurface *dst, int x, int y, byte c, byte
continue;
const byte mask = (byte)(0x80 >> (bit & 7));
if (srcRow[bit / 8] & mask)
- dstRow[dstX] = color;
- }
- }
- return g.widthBits;
-}
-
-int EEMFont::drawString(Graphics::ManagedSurface *dst, int x, int y,
- const Common::String &s, byte color) const {
- int penX = x;
- for (uint i = 0; i < s.size(); i++)
- penX += drawChar(dst, penX, y, (byte)s[i], color);
- return penX - x;
-}
-
-int EEMFont::drawWordWrapped(Graphics::ManagedSurface *dst, int x, int y, int width,
- const Common::String &s, byte color) const {
- Common::StringTokenizer tok(s, " \t");
- int penY = y;
- Common::String line;
-
- while (!tok.empty()) {
- const Common::String word = tok.nextToken();
- Common::String trial = line.empty() ? word : line + " " + word;
- if (stringWidth(trial) <= width) {
- line = trial;
- } else {
- drawString(dst, x, penY, line, color);
- penY += _maxHeight + 1;
- line = word;
+ dstRow[dstX] = (byte)color;
}
}
- if (!line.empty())
- drawString(dst, x, penY, line, color);
- return penY + _maxHeight - y;
}
} // End of namespace EEM
diff --git a/engines/eem/font.h b/engines/eem/font.h
index f175d5360fa..6ccfd6d4946 100644
--- a/engines/eem/font.h
+++ b/engines/eem/font.h
@@ -27,9 +27,7 @@
#include "common/scummsys.h"
#include "common/str.h"
-namespace Graphics {
-class ManagedSurface;
-}
+#include "graphics/font.h"
namespace EEM {
@@ -49,40 +47,37 @@ struct FontGlyph {
* - u16 numChars
* - per char: u8 height, u8 widthBits, u8 sizeBytes, bytes[sizeBytes] bitmap
*
- * Drawing mirrors `_ShowChar` @ 1b66:0346: each set bit becomes `fontColor`
- * on the destination surface; clear bits are transparent.
+ * Subclasses `Graphics::Font` so callers can use the standard
+ * `drawString` / `drawStringUnboxed` / `wordWrapText` helpers without
+ * us reimplementing them. Lookups go through a 128-byte charâglyph
+ * translation table extracted from CHR2FNT (segment 29b6:0000) â the
+ * font is uppercase-only with lowercase aliased to uppercase glyphs.
*/
-class EEMFont {
+class EEMFont : public Graphics::Font {
public:
EEMFont() = default;
bool load(const Common::Path &path);
+ bool isLoaded() const { return !_glyphs.empty(); }
- uint16 height() const { return _maxHeight; }
-
- int charWidth(byte c) const;
-
- /// Total pixel width of @p s when rendered (no shadow).
- int stringWidth(const Common::String &s) const;
-
- /// Draw @p c at (@p x, @p y) on @p dst with foreground @p color.
- /// Returns the advance width.
- int drawChar(Graphics::ManagedSurface *dst, int x, int y, byte c, byte color) const;
-
- /// Draw @p s at (@p x, @p y) and return total advance width.
- int drawString(Graphics::ManagedSurface *dst, int x, int y,
- const Common::String &s, byte color) const;
-
- /// Word-wrap @p s into the rect (x..x+width, y..) and draw line by line.
- /// Mirrors the data flow of `_DoWordWrap` @ 1b66:04a7.
- int drawWordWrapped(Graphics::ManagedSurface *dst, int x, int y, int width,
- const Common::String &s, byte color) const;
+ // --- Graphics::Font overrides ---
+ int getFontHeight() const override { return _maxHeight; }
+ int getMaxCharWidth() const override { return _maxWidth; }
+ int getCharWidth(uint32 chr) const override;
+ void drawChar(Graphics::Surface *dst, uint32 chr, int x, int y,
+ uint32 color) const override;
+ using Graphics::Font::drawChar; // keep ManagedSurface overload
- bool isLoaded() const { return !_glyphs.empty(); }
+ /// Convenience wrap-and-draw helper that uses the inherited
+ /// `wordWrapText` to break @p s into lines and then `drawString`
+ /// to render each. Returns the total height drawn.
+ int drawWordWrapped(Graphics::ManagedSurface *dst, int x, int y,
+ int width, const Common::String &s, uint32 color) const;
private:
Common::Array<FontGlyph> _glyphs;
uint16 _maxHeight = 0;
+ uint16 _maxWidth = 0;
};
} // End of namespace EEM
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index b20a92a96ea..319f31b3f68 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -155,8 +155,8 @@ void SiteScreen::run() {
"Selected-points > 99 wins the case."
};
for (uint i = 0; i < sizeof(lines)/sizeof(lines[0]); i++) {
- fnt.drawString(&help, 8, y, lines[i], 0xF);
- y += fnt.height() + 1;
+ fnt.drawString(&help, lines[i], 8, y, 320, 0xF);
+ y += fnt.getFontHeight() + 1;
}
g_system->copyRectToScreen(help.getPixels(),
help.pitch, 0, 0, 320, 200);
@@ -253,7 +253,7 @@ void SiteScreen::renderHotspots(uint siteNum) {
Graphics::ManagedSurface mgr(320, 9,
Graphics::PixelFormat::createFormatCLUT8());
mgr.clear();
- _vm->getFont().drawString(&mgr, 4, 0, hud, 0x0F);
+ _vm->getFont().drawString(&mgr, hud, 4, 0, 320, 0x0F);
for (int row = 0; row < 8; row++) {
memcpy((byte *)screen->getBasePtr(0, hudY + row),
(const byte *)mgr.getBasePtr(0, row), 320);
Commit: 17d95660b896e97b384b14d0101af3945b8c5f70
https://github.com/scummvm/scummvm/commit/17d95660b896e97b384b14d0101af3945b8c5f70
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:33+02:00
Commit Message:
EEM: completed kCharToGlyph
Changed paths:
engines/eem/eem.cpp
engines/eem/font.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 793f1b6505a..981329193e0 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -388,11 +388,12 @@ void EEMEngine::doNewPlayer() {
memcpy((byte *)scratch.getBasePtr(0, row),
(const byte *)bg.surface.getBasePtr(0, row), w);
}
- _font.drawString(&scratch, "Welcome to Eagle Eye Mysteries!", 40, 24, 320, 0xF);
- _font.drawString(&scratch, "Please type your name:", 40, 40, 320, 0xF);
- _font.drawString(&scratch, "(Backspace to delete, Enter to confirm)", 40, 60, 320, 0xF);
+ // Match the original `_NewPlayer`: `_Show_String(rw=0x28, cl=0x50)`
+ // for the prompt, then `_ShowChar(0x50, x, â¦)` for typed input.
+ // (rw=row=y, cl=col=x.) Prompt at (y=40, x=80), input at (y=80, x=80).
+ _font.drawString(&scratch, "Please type your name:", 80, 40, 240, 0xF);
Common::String shown = name + "_";
- _font.drawString(&scratch, shown, 40, 90, 320, 0xF);
+ _font.drawString(&scratch, shown, 80, 80, 240, 0xF);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
g_system->updateScreen();
diff --git a/engines/eem/font.cpp b/engines/eem/font.cpp
index d01e22d3a0e..19f40c1b77e 100644
--- a/engines/eem/font.cpp
+++ b/engines/eem/font.cpp
@@ -30,10 +30,18 @@
namespace EEM {
-// Character â glyph translation table from segment CHR2FNT (29b6:0000).
-// 128 bytes mapping ASCII 0..127 to a glyph index in FONT.FNT (which
-// only stores 85 glyphs covering ' ', '!'..'9', ':', 'A'..'Z'). Lowercase
-// letters map to uppercase glyphs; chars without a glyph map to 0.
+// Character â glyph translation table.
+//
+// FONT.FNT layout (verified by dumping glyph bitmaps):
+// index 0..26 : ' ' .. ':' (punctuation, digits)
+// index 27..32 : ';' '<' '=' '>' '?' '@'
+// index 33..58 : UPPERCASE A..Z
+// index 59..84 : lowercase a..z
+//
+// The CHR2FNT segment table at 29b6:0000 in the original maps both 'A'
+// and 'a' to the lowercase glyph (so the original engine renders all
+// text in lowercase). We route uppercase ASCII letters to the uppercase
+// glyph slots (33..58) for proper mixed-case rendering.
static const byte kCharToGlyph[128] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
@@ -42,11 +50,11 @@ static const byte kCharToGlyph[128] = {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // 0x20..0x27 ' '..'\''
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, // 0x28..0x2F '('..'/'
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, // 0x30..0x37 '0'..'7'
- 0x18, 0x19, 0x1A, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x38..0x3F '8','9',':'..
- 0x00, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, // 0x40..0x47 '@','A'..'G'
- 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, // 0x48..0x4F 'H'..'O'
- 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, // 0x50..0x57 'P'..'W'
- 0x52, 0x53, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x58..0x5F 'X','Y','Z'..
+ 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, // 0x38..0x3F '8','9',':',';','<','=','>','?'
+ 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, // 0x40..0x47 '@','A'..'G'
+ 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, // 0x48..0x4F 'H'..'O'
+ 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // 0x50..0x57 'P'..'W'
+ 0x38, 0x39, 0x3A, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x58..0x5F 'X','Y','Z'..
0x00, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, // 0x60..0x67 '`','a'..'g'
0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, // 0x68..0x6F 'h'..'o'
0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, // 0x70..0x77 'p'..'w'
Commit: 57dc88dafe89fdec344074a193af45a011293ecb
https://github.com/scummvm/scummvm/commit/57dc88dafe89fdec344074a193af45a011293ecb
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:34+02:00
Commit Message:
EEM: show mouse when selecting character
Changed paths:
engines/eem/eem.cpp
engines/eem/eem.h
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 981329193e0..add9de7d1ef 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -30,6 +30,7 @@
#include "engines/util.h"
+#include "graphics/cursorman.h"
#include "graphics/paletteman.h"
#include "eem/detection.h"
@@ -65,6 +66,35 @@ const int kBoyX = 0xe2; // 226
const int kBoyY = 0x62; // 98
const int kGirlX = 0x42; // 66
const int kGirlY = 0x60; // 96
+
+// 11x16 mouse cursor â replaces the DOS hardware cursor wired in by
+// _InitMouse @ 152d:018b (INT 33h). The original game sets the cursor
+// visible/hidden via _MouseCursor; we leave it on once the screens
+// that need it (ChoosePartner, CaseSelection, sites) are reached.
+// 0 = transparent, 1 = black outline, 2 = white fill
+const byte kCursorBitmap[11 * 16] = {
+ 1,1,0,0,0,0,0,0,0,0,0,
+ 1,2,1,0,0,0,0,0,0,0,0,
+ 1,2,2,1,0,0,0,0,0,0,0,
+ 1,2,2,2,1,0,0,0,0,0,0,
+ 1,2,2,2,2,1,0,0,0,0,0,
+ 1,2,2,2,2,2,1,0,0,0,0,
+ 1,2,2,2,2,2,2,1,0,0,0,
+ 1,2,2,2,2,2,2,2,1,0,0,
+ 1,2,2,2,2,2,2,2,2,1,0,
+ 1,2,2,2,2,2,2,2,2,2,1,
+ 1,2,2,2,2,2,1,0,0,0,0,
+ 1,2,1,0,1,2,2,1,0,0,0,
+ 1,1,0,0,1,2,2,1,0,0,0,
+ 0,0,0,0,0,1,2,2,1,0,0,
+ 0,0,0,0,0,1,2,2,1,0,0,
+ 0,0,0,0,0,0,1,2,2,1,0
+};
+const byte kCursorPalette[] = {
+ 0x00, 0x00, 0x00, // 0 â transparent (key)
+ 0x00, 0x00, 0x00, // 1 â outline
+ 0xFF, 0xFF, 0xFF // 2 â fill
+};
} // anonymous namespace
EEMEngine::EEMEngine(OSystem *syst, const ADGameDescription *gameDesc)
@@ -90,6 +120,14 @@ Common::Error EEMEngine::run() {
if (!_font.load(Common::Path("FONT.FNT")))
warning("FONT.FNT failed to load; text will not render");
+ // _InitMouse @ 152d:018b in the original â install our 11x16 arrow,
+ // using palette index 0 as the transparency key. The cursor is left
+ // hidden through the opening anims and switched on at NewPlayer /
+ // ChoosePartner where the player actually clicks.
+ CursorMan.replaceCursor(kCursorBitmap, 11, 16, 0, 0, 0);
+ CursorMan.replaceCursorPalette(kCursorPalette, 0, 3);
+ CursorMan.showMouse(false);
+
// _AllBlack @ 172b:0d4b paints the screen black before the first handler.
byte black[3 * 256] = { 0 };
g_system->getPaletteManager()->setPalette(black, 0, 256);
@@ -107,6 +145,7 @@ Common::Error EEMEngine::run() {
if (err.getCode() == Common::kNoError && _mystery.isLoaded()) {
debugC(1, kDebugGeneral, "Resuming from slot %d at mystery %u",
wantedSave, _mystery.number());
+ CursorMan.showMouse(true);
doInitClues();
doSiteLoop();
while (!shouldQuit()) {
@@ -122,27 +161,34 @@ Common::Error EEMEngine::run() {
// Reproduces _DoOpeningAnims @ 2520:082a (sans audio):
// EA Kids logo (PIC) -> HighScore Productions logo (PIC) ->
// Storm Software logo (BOLT.ANM) -> 20 character-intro animations
- // (ANIM01.A .. ANIM20.A) -> TITLE.ANM. Each can be skipped with a
- // click or any key.
+ // (ANIM01.A .. ANIM20.A) -> TITLE.ANM. Click / any key skips a
+ // single clip; ESC skips the rest of the chain (waitForInput /
+ // playAnm raise `_skipIntro` so each subsequent step bails out).
+ _skipIntro = false;
showEAKidsLogo();
- if (!shouldQuit())
+ if (!shouldQuit() && !_skipIntro)
showHighScoreLogo();
- if (!shouldQuit())
+ if (!shouldQuit() && !_skipIntro)
playAnm(Common::Path("BOLT.ANM"));
- for (int i = 1; i <= 20 && !shouldQuit(); i++) {
+ for (int i = 1; i <= 20 && !shouldQuit() && !_skipIntro; i++) {
Common::String name = Common::String::format("ANIM%02d.A", i);
playAnm(Common::Path(name));
// Between anims the original plays a voice clip via _SpoolSound;
// without audio we still want a beat so each scene reads.
- if (!shouldQuit() && i != 20)
+ if (!shouldQuit() && !_skipIntro && i != 20)
waitForInput(2000);
}
- if (!shouldQuit())
+ if (!shouldQuit() && !_skipIntro)
playAnm(Common::Path("TITLE.ANM"), 120, /*holdLastFrame=*/true);
+ _skipIntro = false;
// After the title chain, the original goes Title (B) -> screen 8
// (NewPlayer / saved-record selection) -> screen 9 (ChoosePartner) ->
// screen A (CaseSelection) -> site loop. We mirror the same order.
+ // Mouse stays hidden through the opening anims; show it now for
+ // the interactive screens (matches `_MouseCursor = 1` at the tail
+ // of `_NewPlayer`).
+ CursorMan.showMouse(true);
if (!shouldQuit())
doNewPlayer();
if (!shouldQuit())
@@ -261,6 +307,9 @@ void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs, bool holdLa
// Drain events and let the user skip with click/key. The original
// uses _CheckFrameRate / _kbhit; we use a simple fixed delay until
// the frame-rate calibration logic from _GetSpeedRating is wired up.
+ // ESC additionally sets `_skipIntro` so the opening-anim chain in
+ // run() bails out of the whole sequence instead of advancing to
+ // the next clip.
const uint32 frameStart = g_system->getMillis();
bool aborted = false;
while (g_system->getMillis() - frameStart < frameDelayMs && !aborted) {
@@ -268,8 +317,13 @@ void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs, bool holdLa
while (g_system->getEventManager()->pollEvent(event)) {
if (event.type == Common::EVENT_QUIT ||
event.type == Common::EVENT_RETURN_TO_LAUNCHER ||
- event.type == Common::EVENT_LBUTTONDOWN ||
- event.type == Common::EVENT_KEYDOWN) {
+ event.type == Common::EVENT_LBUTTONDOWN) {
+ aborted = true;
+ break;
+ }
+ if (event.type == Common::EVENT_KEYDOWN) {
+ if (event.kbd.keycode == Common::KEYCODE_ESCAPE)
+ _skipIntro = true;
aborted = true;
break;
}
@@ -280,7 +334,7 @@ void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs, bool holdLa
break;
}
- if (holdLastFrame && !shouldQuit()) {
+ if (holdLastFrame && !shouldQuit() && !_skipIntro) {
// Mirror the wait-loop at the end of `_DoOpeningAnims`:
// while (!keyDataAvailable) ;
// We accept either a click or a key.
@@ -290,8 +344,13 @@ void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs, bool holdLa
while (g_system->getEventManager()->pollEvent(ev)) {
if (ev.type == Common::EVENT_QUIT ||
ev.type == Common::EVENT_RETURN_TO_LAUNCHER ||
- ev.type == Common::EVENT_LBUTTONDOWN ||
- ev.type == Common::EVENT_KEYDOWN) {
+ ev.type == Common::EVENT_LBUTTONDOWN) {
+ clicked = true;
+ break;
+ }
+ if (ev.type == Common::EVENT_KEYDOWN) {
+ if (ev.kbd.keycode == Common::KEYCODE_ESCAPE)
+ _skipIntro = true;
clicked = true;
break;
}
@@ -315,14 +374,20 @@ void EEMEngine::blitAt(const Picture &pic, int x, int y) {
}
void EEMEngine::waitForInput(uint32 maxMs) {
+ // ESC additionally raises `_skipIntro` so the opening-anim chain
+ // can fast-forward past the rest of the sequence.
const uint32 startMs = g_system->getMillis();
while (!shouldQuit() && (g_system->getMillis() - startMs < maxMs)) {
Common::Event event;
while (g_system->getEventManager()->pollEvent(event)) {
if (event.type == Common::EVENT_QUIT ||
event.type == Common::EVENT_RETURN_TO_LAUNCHER ||
- event.type == Common::EVENT_LBUTTONDOWN ||
- event.type == Common::EVENT_KEYDOWN) {
+ event.type == Common::EVENT_LBUTTONDOWN) {
+ return;
+ }
+ if (event.type == Common::EVENT_KEYDOWN) {
+ if (event.kbd.keycode == Common::KEYCODE_ESCAPE)
+ _skipIntro = true;
return;
}
}
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index b73ef2ac2bc..ed75d78e5ec 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -198,6 +198,11 @@ private:
uint16 _lastScreen; ///< Mirrors _LastScreen @ 2d5d:3f24
uint16 _nextScreen; ///< Mirrors _NextScreen @ 2d5d:3f26
uint8 _partner; ///< Mirrors _Partner: 0 = boy (Jake), 1 = girl (Jenny)
+
+ /// Set when ESC is pressed during an intro animation or logo. Tells
+ /// the opening-anim loop in run() to skip the rest of the chain
+ /// instead of asking the user to click through every screen.
+ bool _skipIntro = false;
};
} // End of namespace EEM
Commit: 84a82d5580235109bfdbab0d47a27159e7c6ed6c
https://github.com/scummvm/scummvm/commit/84a82d5580235109bfdbab0d47a27159e7c6ed6c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:34+02:00
Commit Message:
EEM: improved initial menu following the original implementation
Changed paths:
engines/eem/eem.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index add9de7d1ef..c14c56b4f10 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -587,20 +587,57 @@ void EEMEngine::doChoosePartner() {
void EEMEngine::doCaseSelection() {
// Mirrors `_CaseSelection` @ 1c33:0a87. The original draws PIC 0x41
- // (chooser background) and a paginated list of mystery names rendered
- // from M<n>.BIN headers, then calls `_DoChoose` to read a selection.
- // We approximate with a numeric prompt (0..9 for first ten mysteries,
- // Tab to cycle, Enter to load).
+ // (chooser background) plus a centred "Book %d" / "Challenge Book"
+ // header at (y=12) and then calls `_DoChoose(list)` to render the
+ // menu via `DrawList` @ 1c33:040d at (_TextBox+3, DAT_29be_0d02) =
+ // (61, 35), 12 rows à 10 px line height. The menu list itself is
+ // the static array at 29be:0d6a (verified via `push 0x0d6a` at
+ // 1c33:1ab4). Strings are at 29be:0ef4 onwards. Layout:
+ // list[0] = "----------------------------------"
+ // list[1] = " Choose A Mystery"
+ // list[2..10] = alternating menu items + separators
+ // Five selectable items: Choose A Mystery / Practice Mystery /
+ // See ScrapBook 1/2/3.
const uint kMaxMystery = 54;
- // Default selection = the next unsolved mystery so post-win the
- // player doesn't have to scroll to find what's left.
- uint sel = 0;
- for (uint i = 0; i <= kMaxMystery; i++) {
- if (i < sizeof(_mysteriesSolved) && !_mysteriesSolved[i]) {
- sel = i;
- break;
- }
- }
+
+ enum MenuPick {
+ kPickChoose = 0,
+ kPickPractice,
+ kPickScrap1,
+ kPickScrap2,
+ kPickScrap3,
+ kNumPicks
+ };
+ const char *kPickLabel[kNumPicks] = {
+ " Choose A Mystery",
+ " Practice Mystery",
+ " See ScrapBook 1",
+ " See ScrapBook 2",
+ " See ScrapBook 3"
+ };
+ // ScrapBooks aren't implemented yet â grey them so the player can't
+ // stop on them, mirroring the original `_Greys` mask.
+ const bool kPickEnabled[kNumPicks] = { true, true, false, false, false };
+ uint pick = kPickChoose;
+
+ const char *kSeparator = "----------------------------------";
+
+ // Click rectangles from the original `_DoChoose` @ 1c33:0514 â each
+ // `_InRect(_MouseX, _MouseY, addr, 0x29be)` reads one 4Ãu16 rect at
+ // the listed offset in segment 29be ({x1, y1, x2, y2}). We use
+ // `Common::Rect` (left/top/right/bottom) which also gives us
+ // `contains(x, y)` for hit testing.
+ const Common::Rect kOkRect ( 12, 63, 41, 87); // 29be:0cd8 confirm
+ const Common::Rect kHelpRect ( 12, 100, 41, 124); // 29be:0ce0 help
+ const Common::Rect kExitRect ( 12, 137, 41, 161); // 29be:0ce8 cancel
+ const Common::Rect kUpArrowRect (240, 31, 250, 43); // 29be:0cf0 scroll up
+ const Common::Rect kDnArrowRect (240, 148, 250, 159); // 29be:0cf8 scroll dn
+ const Common::Rect kListRect ( 58, 35, 238, 158); // 29be:0d00 list panel
+
+ // The original `_NewPlayer` set `_MouseCursor = 1` on exit; the
+ // chain of screens after it expects the cursor to stay visible.
+ // Reassert here in case anything between hid it.
+ CursorMan.showMouse(true);
// Mirrors `_CaseSelection`: load PIC 0x41 as the chooser backdrop.
Picture caseBg;
@@ -619,53 +656,215 @@ void EEMEngine::doCaseSelection() {
}
}
if (_font.isLoaded()) {
- _font.drawString(&scratch, Common::String::format("EAGLE EYE - %s", _playerName.c_str()), 8, 4, 320, 0xF);
+ // `DrawList` @ 1c33:040d coordinates: `_TextBox + 3` for x
+ // and `DAT_29be_0d02` for y. `_TextBox` @ 29be:0d00 holds
+ // {x=58, y=35, x2=238, y2=158}. Matches the blue panel.
+ const int kListX = 58 + 3;
+ const int kListW = 238 - kListX;
+ const int kListY0 = 35;
+ const int kLineH = 10;
+
+ // Top centred "Book %d" / "Challenge Book" title â sprintf
+ // format strings at 29be:0deb / 29be:0dfa shown via
+ // `_Show_String(0xc, (0xba - width)/2 + 0x3c, â¦)` in the
+ // original. We don't track challenge tier yet so always
+ // show "Book 1".
+ const Common::String book = "Book 1";
+ const int titleW = _font.getStringWidth(book);
+ const int titleX = (0xba - titleW) / 2 + 0x3c;
+ _font.drawString(&scratch, book, titleX, 12, 320, 0xF);
+
+ // Render 11 list rows: separator + menu item pairs.
+ // row 0 separator
+ // row 1 Choose A Mystery
+ // row 2 separator
+ // row 3 Practice Mystery
+ // ...
+ // row 9 See ScrapBook 3
+ // row 10 separator
+ for (int r = 0; r < 11; r++) {
+ const int y = kListY0 + r * kLineH;
+ if ((r & 1) == 0) {
+ _font.drawString(&scratch, kSeparator, kListX, y, kListW, 0x7);
+ continue;
+ }
+ const uint mp = (uint)(r >> 1);
+ const bool isSel = (mp == pick);
+ const byte color = isSel ? 0xF :
+ kPickEnabled[mp] ? 0x7 : 0x8;
+ _font.drawString(&scratch, kPickLabel[mp], kListX, y, kListW, color);
+ }
+ }
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ };
+
+ auto pickPrev = [&]() {
+ for (int i = 0; i < (int)kNumPicks; i++) {
+ pick = (pick == 0) ? (uint)(kNumPicks - 1) : pick - 1;
+ if (kPickEnabled[pick])
+ break;
+ }
+ };
+ auto pickNext = [&]() {
+ for (int i = 0; i < (int)kNumPicks; i++) {
+ pick = (pick + 1) % kNumPicks;
+ if (kPickEnabled[pick])
+ break;
+ }
+ };
+
+ draw();
- // Solved count.
- uint solved = 0, perfectSolved = 0;
- for (uint i = 0; i < sizeof(_mysteriesSolved); i++) {
- if (_mysteriesSolved[i] >= 1) solved++;
- if (_mysteriesSolved[i] == 2) perfectSolved++;
+ bool exitChosen = false;
+ while (!shouldQuit()) {
+ Common::Event ev;
+ bool confirmed = false;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return;
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ // OK / EXIT / HELP buttons (rectangles from `_DoChoose`).
+ if (kOkRect.contains(ev.mouse.x, ev.mouse.y)) {
+ confirmed = true;
+ break;
+ }
+ if (kExitRect.contains(ev.mouse.x, ev.mouse.y)) {
+ exitChosen = true;
+ confirmed = true;
+ break;
+ }
+ if (kHelpRect.contains(ev.mouse.x, ev.mouse.y)) {
+ // HELP placeholder â original calls `_DisplayHint`;
+ // our help screen is wired to `H` later in the flow.
+ continue;
+ }
+ // List panel: click on a non-separator row selects the
+ // menu entry under the cursor.
+ if (kListRect.contains(ev.mouse.x, ev.mouse.y)) {
+ const int kLineH = 10;
+ const int row = (ev.mouse.y - kListRect.top) / kLineH;
+ if ((row & 1) == 1) {
+ const uint mp = (uint)(row >> 1);
+ if (mp < kNumPicks && kPickEnabled[mp]) {
+ pick = mp;
+ draw();
+ continue;
+ }
+ }
+ }
}
- _font.drawString(&scratch, Common::String::format("solved %u (1st try %u)",
- solved, perfectSolved), 200, 4, 320, 0xF);
- if (perfectSolved >= 55) {
- _font.drawString(&scratch, "** PERFECT MASTER SLEUTH! **", 8, 168, 320, 0xF);
- } else if (solved >= 55) {
- _font.drawString(&scratch, "** ALL MYSTERIES SOLVED! **", 8, 168, 320, 0xF);
+ if (ev.type != Common::EVENT_KEYDOWN)
+ continue;
+ const Common::KeyCode k = ev.kbd.keycode;
+ if (k == Common::KEYCODE_ESCAPE) {
+ exitChosen = true;
+ confirmed = true;
+ break;
+ }
+ if (k == Common::KEYCODE_RETURN) {
+ confirmed = true;
+ break;
+ }
+ if (k == Common::KEYCODE_UP || k == Common::KEYCODE_LEFT) {
+ pickPrev();
+ draw();
+ continue;
}
+ if (k == Common::KEYCODE_DOWN || k == Common::KEYCODE_RIGHT ||
+ k == Common::KEYCODE_TAB) {
+ pickNext();
+ draw();
+ continue;
+ }
+ }
+ if (confirmed) {
+ draw();
+ break;
+ }
+ g_system->delayMillis(15);
+ }
+
+ if (shouldQuit())
+ return;
- char marker = ' ';
- if (sel < sizeof(_mysteriesSolved)) {
- if (_mysteriesSolved[sel] == 2) marker = '*';
- else if (_mysteriesSolved[sel] == 1) marker = '+';
+ if (exitChosen) {
+ _mystery.clear();
+ _nextScreen = kScreenInvalid;
+ return;
+ }
+
+ // "Practice Mystery" is the tutorial â mystery 0.
+ if (pick == kPickPractice) {
+ if (!_mystery.load(0, &_rng)) {
+ warning("doCaseSelection: failed to load practice mystery");
+ _mystery.clear();
+ }
+ return;
+ }
+
+ if (pick != kPickChoose) {
+ // ScrapBooks aren't implemented; bail back to the menu loop.
+ _mystery.clear();
+ return;
+ }
+
+ // "Choose A Mystery" sub-screen: pick a specific case from the
+ // 55-mystery roster. The original opens a different list here;
+ // we approximate with the tier-aware numeric chooser we used
+ // before. Default to the first unsolved mystery.
+ uint sel = 0;
+ for (uint i = 0; i <= kMaxMystery; i++) {
+ if (i < sizeof(_mysteriesSolved) && !_mysteriesSolved[i]) {
+ sel = i;
+ break;
+ }
+ }
+
+ auto drawSubmenu = [&]() {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ if (haveCaseBg) {
+ const int w = MIN<int>(caseBg.surface.w, 320);
+ const int h = MIN<int>(caseBg.surface.h, 200);
+ for (int row = 0; row < h; row++)
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)caseBg.surface.getBasePtr(0, row), w);
+ }
+ if (_font.isLoaded()) {
+ const int kListX = 61;
+ const int kListW = 238 - kListX;
+ const int kListY0 = 35;
+ const int kLineH = 10;
+ const int kVisible = 12;
+ int top = (int)sel - kVisible / 2;
+ if (top < 0) top = 0;
+ if (top + kVisible > (int)kMaxMystery + 1)
+ top = (int)kMaxMystery + 1 - kVisible;
+ for (int r = 0; r < kVisible; r++) {
+ const int idx = top + r;
+ if (idx > (int)kMaxMystery)
+ break;
+ char marker = ' ';
+ if ((uint)idx < sizeof(_mysteriesSolved)) {
+ if (_mysteriesSolved[idx] == 2) marker = '*';
+ else if (_mysteriesSolved[idx] == 1) marker = '+';
+ }
+ const char arrow = ((uint)idx == sel) ? '>' : ' ';
+ _font.drawString(&scratch,
+ Common::String::format("%c %c Mystery %d", arrow, marker, idx),
+ kListX, kListY0 + r * kLineH, kListW, 0xF);
}
- // Per the original tiers: 0 (tutorial), 1-24 (Junior),
- // 25-48 (Senior), 49-54 (Master).
- const char *tier = "Tutorial";
- if (sel >= 1 && sel <= 24) tier = "Junior Sleuth";
- else if (sel >= 25 && sel <= 48) tier = "Senior Sleuth";
- else if (sel >= 49 && sel <= 54) tier = "Master Sleuth";
- _font.drawString(&scratch, Common::String::format("Mystery %u %c [%s]",
- sel, marker, tier), 8, 24, 320, 0xF);
- _font.drawString(&scratch, " 0..9 quick select", 8, 40, 320, 0xF);
- _font.drawString(&scratch, " Tab / + next mystery", 8, 52, 320, 0xF);
- _font.drawString(&scratch, " Shift+Tab prev mystery", 8, 64, 320, 0xF);
- _font.drawString(&scratch, " PgUp/PgDn jump 10", 8, 76, 320, 0xF);
- _font.drawString(&scratch, " Home/End first/last", 8, 88, 320, 0xF);
- _font.drawString(&scratch, " Enter start mystery", 8, 100, 320, 0xF);
- _font.drawString(&scratch, " F5 save / load (ScummVM)", 8, 112, 320, 0xF);
- _font.drawString(&scratch, " ESC quit", 8, 124, 320, 0xF);
- _font.drawString(&scratch, " * solved on first try", 8, 144, 320, 0xF);
- _font.drawString(&scratch, " + solved", 8, 156, 320, 0xF);
}
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
g_system->updateScreen();
};
- draw();
-
+ drawSubmenu();
bool confirmed = false;
while (!confirmed && !shouldQuit()) {
Common::Event ev;
@@ -673,12 +872,48 @@ void EEMEngine::doCaseSelection() {
if (ev.type == Common::EVENT_QUIT ||
ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
return;
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ // Same `_DoChoose` rectangles as the top-level menu.
+ if (kOkRect.contains(ev.mouse.x, ev.mouse.y)) {
+ confirmed = true;
+ break;
+ }
+ if (kExitRect.contains(ev.mouse.x, ev.mouse.y)) {
+ _mystery.clear();
+ return;
+ }
+ if (kUpArrowRect.contains(ev.mouse.x, ev.mouse.y)) {
+ sel = (sel == 0) ? kMaxMystery : sel - 1;
+ drawSubmenu();
+ continue;
+ }
+ if (kDnArrowRect.contains(ev.mouse.x, ev.mouse.y)) {
+ sel = (sel >= kMaxMystery) ? 0 : sel + 1;
+ drawSubmenu();
+ continue;
+ }
+ if (kListRect.contains(ev.mouse.x, ev.mouse.y)) {
+ // Pick the row under the cursor.
+ const int kLineH = 10;
+ const int kVisible = 12;
+ int top = (int)sel - kVisible / 2;
+ if (top < 0) top = 0;
+ if (top + kVisible > (int)kMaxMystery + 1)
+ top = (int)kMaxMystery + 1 - kVisible;
+ const int row = (ev.mouse.y - kListRect.top) / kLineH;
+ const int idx = top + row;
+ if (idx >= 0 && idx <= (int)kMaxMystery) {
+ sel = (uint)idx;
+ drawSubmenu();
+ }
+ continue;
+ }
+ }
if (ev.type != Common::EVENT_KEYDOWN)
continue;
const Common::KeyCode k = ev.kbd.keycode;
if (k == Common::KEYCODE_ESCAPE) {
- _mystery.clear();
- _nextScreen = kScreenInvalid;
+ _mystery.clear();
return;
}
if (k == Common::KEYCODE_RETURN) {
@@ -687,46 +922,31 @@ void EEMEngine::doCaseSelection() {
}
if (k >= Common::KEYCODE_0 && k <= Common::KEYCODE_9) {
sel = (uint)(k - Common::KEYCODE_0);
- draw();
+ drawSubmenu();
continue;
}
- if (k == Common::KEYCODE_TAB || k == Common::KEYCODE_PLUS ||
- k == Common::KEYCODE_RIGHT || k == Common::KEYCODE_DOWN) {
- const bool back = (ev.kbd.flags & Common::KBD_SHIFT) ||
- k == Common::KEYCODE_LEFT;
- if (back)
- sel = (sel == 0) ? kMaxMystery : sel - 1;
- else
- sel = (sel >= kMaxMystery) ? 0 : sel + 1;
- draw();
+ if (k == Common::KEYCODE_DOWN || k == Common::KEYCODE_TAB) {
+ sel = (sel >= kMaxMystery) ? 0 : sel + 1;
+ drawSubmenu();
continue;
}
- if (k == Common::KEYCODE_LEFT || k == Common::KEYCODE_UP ||
- k == Common::KEYCODE_MINUS) {
+ if (k == Common::KEYCODE_UP) {
sel = (sel == 0) ? kMaxMystery : sel - 1;
- draw();
+ drawSubmenu();
continue;
}
if (k == Common::KEYCODE_PAGEDOWN) {
sel = (sel + 10 > kMaxMystery) ? kMaxMystery : sel + 10;
- draw();
+ drawSubmenu();
continue;
}
if (k == Common::KEYCODE_PAGEUP) {
sel = (sel < 10) ? 0 : sel - 10;
- draw();
- continue;
- }
- if (k == Common::KEYCODE_HOME) {
- sel = 0;
- draw();
- continue;
- }
- if (k == Common::KEYCODE_END) {
- sel = kMaxMystery;
- draw();
+ drawSubmenu();
continue;
}
+ if (k == Common::KEYCODE_HOME) { sel = 0; drawSubmenu(); continue; }
+ if (k == Common::KEYCODE_END) { sel = kMaxMystery; drawSubmenu(); continue; }
}
g_system->delayMillis(15);
}
Commit: 17288a601407fe4b999357d3659adfdbd701af00
https://github.com/scummvm/scummvm/commit/17288a601407fe4b999357d3659adfdbd701af00
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:35+02:00
Commit Message:
EEM: update pointer during main menu
Changed paths:
engines/eem/eem.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index c14c56b4f10..c6e96ccd8ca 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -500,6 +500,7 @@ void EEMEngine::doNewPlayer() {
}
if (dirty)
draw();
+ g_system->updateScreen();
g_system->delayMillis(15);
}
}
@@ -784,6 +785,7 @@ void EEMEngine::doCaseSelection() {
draw();
break;
}
+ g_system->updateScreen();
g_system->delayMillis(15);
}
@@ -948,6 +950,7 @@ void EEMEngine::doCaseSelection() {
if (k == Common::KEYCODE_HOME) { sel = 0; drawSubmenu(); continue; }
if (k == Common::KEYCODE_END) { sel = kMaxMystery; drawSubmenu(); continue; }
}
+ g_system->updateScreen();
g_system->delayMillis(15);
}
@@ -1184,11 +1187,21 @@ void EEMEngine::displayClue(const byte *clueBlock) {
if (haveBalloon) {
const int bw = MIN<int>(balloon.surface.w, 320 - bubX);
const int bh = MIN<int>(balloon.surface.h, 200 - bubY);
+ // `_AddPicBackground` passes `pic->miscflags >> 8` as
+ // the transparent colour to `_Rect_Move_Mask`. Without
+ // that mask, the balloon's tail/corner padding bleeds
+ // into the surrounding scene. The on-disk u16 at file
+ // offset 0 maps to our `Picture::flags` field.
+ const byte transp = (byte)(balloon.flags >> 8);
if (bw > 0 && bh > 0) {
for (int row = 0; row < bh; row++) {
- memcpy((byte *)scratch.getBasePtr(bubX, bubY + row),
- (const byte *)balloon.surface.getBasePtr(0, row),
- bw);
+ const byte *src =
+ (const byte *)balloon.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(bubX, bubY + row);
+ for (int col = 0; col < bw; col++) {
+ if (src[col] != transp)
+ dst[col] = src[col];
+ }
}
}
// Per-balloon metadata table at 29be:0875 in the original
@@ -1240,6 +1253,10 @@ void EEMEngine::displayClue(const byte *clueBlock) {
break;
}
}
+ // Tick the screen so the OSystem cursor follows the
+ // mouse â ScummVM redraws the cursor overlay only on
+ // updateScreen.
+ g_system->updateScreen();
g_system->delayMillis(10);
}
if (skipAll) {
@@ -1351,6 +1368,7 @@ void EEMEngine::doNotebook() {
}
if (exit) break;
if (dirty) draw();
+ g_system->updateScreen();
g_system->delayMillis(15);
}
}
@@ -1634,6 +1652,7 @@ void EEMEngine::doBigMap() {
}
if (dirty)
draw();
+ g_system->updateScreen();
g_system->delayMillis(10);
}
}
@@ -1732,6 +1751,7 @@ bool EEMEngine::areYouSure() {
}
if (decided)
break;
+ g_system->updateScreen();
g_system->delayMillis(15);
}
@@ -1830,6 +1850,7 @@ void EEMEngine::doAccuse() {
picked = slot;
}
}
+ g_system->updateScreen();
g_system->delayMillis(10);
}
if (picked < 0)
@@ -1970,6 +1991,7 @@ void EEMEngine::doAccuse() {
break;
}
}
+ g_system->updateScreen();
g_system->delayMillis(15);
}
if (exit) break;
Commit: d3e517a57579c9b310cfd8b62eb09c3e85d4a8d8
https://github.com/scummvm/scummvm/commit/d3e517a57579c9b310cfd8b62eb09c3e85d4a8d8
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:35+02:00
Commit Message:
EEM: correct ballon dialog placement
Changed paths:
engines/eem/eem.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index c6e96ccd8ca..b46d1a1842c 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -1019,26 +1019,56 @@ void EEMEngine::doSiteLoop() {
/// hyphenation marks and a hint placeholder (0x89) we ignore for now.
static Common::String parseString(const Common::String &raw,
const Common::String &playerName,
- const Common::String &partnerName) {
+ uint partner) {
+ // Substitution opcodes from `_ParseString` @ 1b66:07c3, jump-table
+ // at 1b66:0cbe. Each handler reads `_Partner` (16-bit at 0x7918)
+ // and indexes the name table at 29be:0c28 ({Jake, Jennifer, he,
+ // she, him, her, his} as far pointers).
+ // 0x80 â player's typed name (auto-cap word starts) â uses _PlayerRecord
+ // 0x81 â _Partner == 0 ? "Jake" : "Jennifer" (chosen detective)
+ // 0x82 â _Partner == 0 ? "Jennifer" : "Jake" (the OTHER one)
+ // 0x83 â _Partner == 0 ? "he" : "she"
+ // 0x84 â _Partner == 0 ? "him" : "her"
+ // 0x85 â _Partner == 0 ? "his" : "her"
+ // 0x86..0x88 read a different gender flag at 0x7985 â left alone
+ // until that flag's source is traced.
+ // 0x89 â KD hint placeholder (handled by caller).
+ const bool isJake = (partner == 0);
Common::String out;
for (uint i = 0; i < raw.size(); i++) {
const byte c = (byte)raw[i];
- if (c == 0x80) {
+ switch (c) {
+ case 0x80:
out += playerName;
- } else if (c == 0x81 || c == 0x82) {
- // Both forms substitute the partner's name. The original
- // likely has a casual ("Jake/Jenny") and a formal
- // ("Jake/Jennifer") variant; we render the same partner
- // name in both spots â close enough for natural reading.
- out += partnerName;
- } else if (c >= 0x80 && c < 0x8A) {
- // Other control opcodes: eat them silently for now.
- } else if (c == 0 || c == '\r') {
- // stop on NUL, ignore CR
- if (c == 0)
- break;
- } else {
+ break;
+ case 0x81:
+ out += isJake ? "Jake" : "Jennifer";
+ break;
+ case 0x82:
+ out += isJake ? "Jennifer" : "Jake";
+ break;
+ case 0x83:
+ out += isJake ? "he" : "she";
+ break;
+ case 0x84:
+ out += isJake ? "him" : "her";
+ break;
+ case 0x85:
+ out += isJake ? "his" : "her";
+ break;
+ case 0x86:
+ case 0x87:
+ case 0x88:
+ case 0x89:
+ // Eaten silently â see comment above.
+ break;
+ case 0:
+ return out;
+ case '\r':
+ break;
+ default:
out += (char)c;
+ break;
}
}
return out;
@@ -1147,11 +1177,10 @@ void EEMEngine::displayClue(const byte *clueBlock) {
}
}
- // Substitute placeholder control bytes with the entered player
- // name and the chosen partner's first name (Jake / Jennifer).
- const Common::String partnerName = (_partner == 0) ? "Jake" : "Jennifer";
+ // Substitute control bytes (0x80..0x89) â see `parseString` for
+ // the table. 0x81 = chosen detective, 0x82 = the other one.
const Common::String text = parseString(raw ? raw : "",
- _playerName, partnerName);
+ _playerName, _partner);
// Speech balloon. Mirrors `_GetBalloon` + `_AddPicBackground` in
// `_DisplayClue`. The original looks up per-balloon text-area
@@ -1188,25 +1217,33 @@ void EEMEngine::displayClue(const byte *clueBlock) {
const int bw = MIN<int>(balloon.surface.w, 320 - bubX);
const int bh = MIN<int>(balloon.surface.h, 200 - bubY);
// `_AddPicBackground` passes `pic->miscflags >> 8` as
- // the transparent colour to `_Rect_Move_Mask`. Without
- // that mask, the balloon's tail/corner padding bleeds
- // into the surrounding scene. The on-disk u16 at file
- // offset 0 maps to our `Picture::flags` field.
+ // the transparent colour to `_Rect_Move_Mask`. The
+ // on-disk u16 at file offset 0 maps to `Picture::flags`.
const byte transp = (byte)(balloon.flags >> 8);
+ // `_GetBalloon @ 172b:1d7d` mirrors the picture horizontally
+ // when `(bubNum & 0x80)` is set â used for right-side
+ // speakers so the tail points the other way.
+ const bool flipBalloon = (bubNum & 0x80) != 0;
if (bw > 0 && bh > 0) {
for (int row = 0; row < bh; row++) {
const byte *src =
(const byte *)balloon.surface.getBasePtr(0, row);
byte *dst = (byte *)scratch.getBasePtr(bubX, bubY + row);
for (int col = 0; col < bw; col++) {
- if (src[col] != transp)
- dst[col] = src[col];
+ const int srcCol = flipBalloon
+ ? (balloon.surface.w - 1 - col)
+ : col;
+ const byte px = src[srcCol];
+ if (px != transp)
+ dst[col] = px;
}
}
}
- // Per-balloon metadata table at 29be:0875 in the original
- // uses (textX=6, textY=4) inset across all entries; we
- // adopt the same constants instead of approximating with 8.
+ // Per-balloon metadata table at 29be:0875 â 10-byte
+ // entries indexed by `(bubNum & 0x7f)`. Layout:
+ // +0..1 textX inset, +2..3 textY inset, +4..5 textWidth.
+ // All entries use textX=6, textY=4 so we hard-code those
+ // constants; textWidth is read live from the table.
textX = bubX + 6;
textY = bubY + 4;
textW = bw - 12;
@@ -1219,8 +1256,11 @@ void EEMEngine::displayClue(const byte *clueBlock) {
copyY = bubY;
}
+ // `_DisplayClue` @ 2404:07fe passes fontColor=0 (palette
+ // index 0 of the case-briefing palette 0x22) to `_WordWrap`.
+ // Hard-coding 0xF here gave the wrong colour.
_font.drawWordWrapped(&scratch, textX, textY,
- MAX<int>(8, textW), text, 0xF);
+ MAX<int>(8, textW), text, 0);
g_system->copyRectToScreen(scratch.getBasePtr(0, copyY),
scratch.pitch, 0, copyY, 320,
@@ -1304,7 +1344,6 @@ void EEMEngine::doNotebook() {
const byte *ni = _mystery.noteIndex();
const uint16 niCount = _mystery.noteIndexCount();
- const Common::String partnerName = (_partner == 0) ? "Jake" : "Jennifer";
int y = 4 + _font.getFontHeight() * 2 + 4;
for (int slot = 0; slot < kPerPage; slot++) {
const int idx = page * kPerPage + slot;
@@ -1318,7 +1357,7 @@ void EEMEngine::doNotebook() {
const uint16 ptsRaw = READ_LE_UINT16(ni + clueId * 4 + 2);
pts = (int)(int16)ptsRaw;
const Common::String raw = _mystery.textAt(textOff);
- text = parseString(raw, _playerName, partnerName);
+ text = parseString(raw, _playerName, _partner);
}
if (text.empty())
text = Common::String::format("clue %u", clueId);
@@ -1680,8 +1719,7 @@ void EEMEngine::doHelp() {
_mystery._sawHelpHint = true;
const Common::String raw = _mystery.textAt(use);
- const Common::String partnerName = (_partner == 0) ? "Jake" : "Jennifer";
- const Common::String text = parseString(raw, _playerName, partnerName);
+ const Common::String text = parseString(raw, _playerName, _partner);
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
@@ -1908,7 +1946,6 @@ void EEMEngine::doAccuse() {
const byte *e = blob.data();
const uint16 pages = READ_LE_UINT16(e);
- const Common::String partnerName = (_partner == 0) ? "Jake" : "Jennifer";
uint pageIdx = 0;
while (!shouldQuit()) {
@@ -1929,7 +1966,7 @@ void EEMEngine::doAccuse() {
const uint16 x2 = READ_LE_UINT16(e + pos + 6);
const uint16 y2 = READ_LE_UINT16(e + pos + 8);
const char *raw = (const char *)(e + pos + 10);
- const Common::String txt = parseString(raw, _playerName, partnerName);
+ const Common::String txt = parseString(raw, _playerName, _partner);
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
Commit: b60f6c5570c0ab4ccb94ef18b806617db515d7fa
https://github.com/scummvm/scummvm/commit/b60f6c5570c0ab4ccb94ef18b806617db515d7fa
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:35+02:00
Commit Message:
EEM: draw the big map and allow player to move between locations
Changed paths:
engines/eem/eem.cpp
engines/eem/site.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index b46d1a1842c..f906aec7d69 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -147,12 +147,19 @@ Common::Error EEMEngine::run() {
wantedSave, _mystery.number());
CursorMan.showMouse(true);
doInitClues();
- doSiteLoop();
+ // Original screen 0 â screen 1: after the briefing the
+ // game opens the map (function at 20fe:120b â _DoBigMap)
+ // and only enters a site once the player clicks on one.
+ doBigMap();
+ if (_mystery.isLoaded())
+ doSiteLoop();
while (!shouldQuit()) {
doCaseSelection();
if (!_mystery.isLoaded()) break;
doInitClues();
- doSiteLoop();
+ doBigMap();
+ if (_mystery.isLoaded())
+ doSiteLoop();
}
return Common::kNoError;
}
@@ -199,7 +206,12 @@ Common::Error EEMEngine::run() {
// Mark the starting site as active and display the case briefing.
// `_DoInitClues` @ 1a35:0411 â case briefing.
doInitClues();
- doSiteLoop();
+ // Original screen 0 â screen 1: after the briefing the game
+ // opens the map (function at 20fe:120b â `_DoBigMap`) and only
+ // enters a site once the player clicks on one.
+ doBigMap();
+ if (_mystery.isLoaded())
+ doSiteLoop();
// After a case, loop back to CaseSelection.
while (!shouldQuit()) {
@@ -207,7 +219,12 @@ Common::Error EEMEngine::run() {
if (!_mystery.isLoaded())
break;
doInitClues();
- doSiteLoop();
+ // Original screen 0 â screen 1: after the briefing the
+ // game opens the map (function at 20fe:120b â _DoBigMap)
+ // and only enters a site once the player clicks on one.
+ doBigMap();
+ if (_mystery.isLoaded())
+ doSiteLoop();
}
}
@@ -978,6 +995,13 @@ void EEMEngine::doInitClues() {
const uint16 startSite = READ_LE_UINT16(ib + 2);
if (startSite < Mystery::kVisitedSiteCap)
_mystery._onSites[startSite] = 1;
+ // Mirror the original: at briefing time the player isn't actually
+ // at any site yet â they pick from the map next. Set _siteNumber
+ // to the start site so the map opens centred on the only initially
+ // accessible location and the post-map site loop has a sensible
+ // resume point.
+ _mystery._siteNumber = startSite;
+ _mystery._lastSite = startSite;
setSitePalette(0x22);
Picture bg;
@@ -1483,12 +1507,21 @@ void EEMEngine::doGallery() {
}
void EEMEngine::doBigMap() {
- // Mirrors `_DoBigMap` @ 20fe:09e7. The original lays out a 0xe9 x 0xab
- // map window inside frame PIC 0x42 with per-site clickable markers
- // scrolling under it. We render BIGMAP.PIC at top-left, draw an
- // overlay listing the sites that the player can travel to (per
- // `_OnSites`), and accept either number keys or clicks on the
- // overlay rows to travel.
+ // Mirrors the small/scrollable map view used inside the site loop â
+ // the original game draws a 0xe9 Ã 0xab viewport at (2, 2) into
+ // `_MapBitMap` (= BIGMAP.PIC) via `DrawMap @ 20fe:1058`; on top of
+ // that, `_StampButtons @ 20fe:0d2f` bakes site icons into the
+ // bitmap at MapData offsets (+4, +6). We render BIGMAP.PIC into a
+ // 233Ã171 viewport and overlay markers ourselves.
+ //
+ // MapData entry layout (14 bytes), confirmed from
+ // `_DrawBigMapButtons @ 20fe:0877` and `_StampButtons @ 20fe:0d2f`:
+ // +0..1 BigMap X (overview frame PIC 0x42)
+ // +2..3 BigMap Y
+ // +4..5 SmallMap X (stamped into _MapBitMap at this position)
+ // +6..7 SmallMap Y
+ // +8..9 crime-flag (DAT_2d5d_5436): non-zero â crime-scene marker
+ // +10..13 unused
Common::File f;
if (!f.open(Common::Path("BIGMAP.PIC"))) {
warning("doBigMap: BIGMAP.PIC missing");
@@ -1505,11 +1538,8 @@ void EEMEngine::doBigMap() {
return;
}
- // Approximate inner map window from the original `_DoBigMap`:
- // if (sx < 0x75) sx = 0; else sx -= 0x74; // 0x74 = 116
- // if (mapW < sx + 0xe9) sx = mapW - 0xe9; // window width = 233
- // if (sy < 0x56) sy = 0; else sy -= 0x55;
- // if (mapH < sy + 0xab) sy = mapH - 0xab; // window height = 171
+ // Viewport size from `DrawMap`: 0xe9 Ã 0xab at screen (2, 2).
+ // We use (4, 4) so a 1-pixel inset preserves the frame border.
const int kMapWinW = 0xe9; // 233
const int kMapWinH = 0xab; // 171
const int kMapWinX = 4;
@@ -1518,7 +1548,7 @@ void EEMEngine::doBigMap() {
int scrollX = 0;
int scrollY = 0;
- // Auto-scroll to centre the current site, if known.
+ // Auto-scroll to centre the current site (using SmallMap coords).
if (_mystery.isLoaded()) {
const byte *entry = _mystery.mapEntry(_mystery._siteNumber);
if (entry) {
@@ -1531,6 +1561,14 @@ void EEMEngine::doBigMap() {
}
}
+ CursorMan.showMouse(true);
+
+ // `_DoBigMap @ 20fe:09e7` calls `_GetPalette(0x24)` after loading
+ // the frame. Without this, the map would render under whatever
+ // palette the prior site set, which makes BIGMAP.PIC look like
+ // noise / incomplete colours.
+ setSitePalette(0x24);
+
auto draw = [&]() {
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
@@ -1556,9 +1594,12 @@ void EEMEngine::doBigMap() {
copyW);
}
- // Site markers from MapData. Each per-mystery MapData entry is
- // 14 bytes; bytes +4..+5 / +6..+7 hold an (x, y) pair on the big
- // map. We draw a small filled square for each accessible site.
+ // Site markers â three states from `_DrawBigMapButtons`:
+ // _DoneMarker if `SaveSiteComplete[i]` (already searched)
+ // _CrimeMarker if MapData[+8] != 0 (crime scene)
+ // _SiteMarker otherwise
+ // We don't have the marker PICs traced yet, so draw small filled
+ // squares with three colours that match the original semantics.
if (_mystery.isLoaded()) {
for (uint i = 0; i < _mystery.numSites(); i++) {
if (!_mystery._onSites[i] && i != _mystery._siteNumber)
@@ -1568,37 +1609,26 @@ void EEMEngine::doBigMap() {
continue;
const uint16 mx = READ_LE_UINT16(entry + 4);
const uint16 my = READ_LE_UINT16(entry + 6);
+ const uint16 crime = READ_LE_UINT16(entry + 8);
const int sx = (int)mx - scrollX + kMapWinX;
const int sy = (int)my - scrollY + kMapWinY;
if (sx < kMapWinX || sx >= kMapWinX + kMapWinW ||
sy < kMapWinY || sy >= kMapWinY + kMapWinH)
continue;
- const byte color = (i == _mystery._siteNumber) ? 0x0E : 0x0F;
- const Common::Rect mark(sx - 2, sy - 2, sx + 3, sy + 3);
- scratch.fillRect(mark, color);
- if (_font.isLoaded()) {
- Common::String num = Common::String::format("%u", i);
- _font.drawString(&scratch, num, sx + 4, sy - 4, 320, color);
- }
- }
- }
- // Travel-target overlay (right-side panel).
- if (_font.isLoaded() && _mystery.isLoaded()) {
- const int panelX = kMapWinX + kMapWinW + 4;
- int y = 4;
- _font.drawString(&scratch, "TRAVEL", panelX, y, 320, 0xF);
- y += _font.getFontHeight() + 4;
- for (uint i = 0; i < _mystery.numSites() && y < 192; i++) {
- if (!_mystery._onSites[i] && i != _mystery._siteNumber)
- continue;
- const char marker = (i == _mystery._siteNumber) ? '>' : ' ';
- Common::String label = Common::String::format(
- "%c %u", marker, i);
- _font.drawString(&scratch, label, panelX, y, 320, 0x0F);
- y += _font.getFontHeight() + 1;
+ byte color;
+ if (i < Mystery::kVisitedSiteCap && _mystery._visitedSite[i])
+ color = 0x07; // searched (DoneMarker analogue)
+ else if (crime != 0)
+ color = 0x0C; // crime scene (CrimeMarker analogue)
+ else
+ color = 0x0F; // available (SiteMarker analogue)
+ if (i == _mystery._siteNumber)
+ color = 0x0E; // current site â bright yellow
+
+ const Common::Rect mark(sx - 3, sy - 3, sx + 4, sy + 4);
+ scratch.fillRect(mark, color);
}
- _font.drawString(&scratch, "Esc", panelX, 188, 320, 0x0F);
}
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
@@ -1669,24 +1699,6 @@ void EEMEngine::doBigMap() {
}
}
- // Click in the right panel: travel to that row.
- const int panelX = kMapWinX + kMapWinW + 4;
- if (_font.isLoaded() && _mystery.isLoaded() &&
- ev.mouse.x >= panelX) {
- const int row = (ev.mouse.y - 4 - _font.getFontHeight() - 4) /
- (_font.getFontHeight() + 1);
- int seen = 0;
- for (uint i = 0; i < _mystery.numSites(); i++) {
- if (!_mystery._onSites[i] && i != _mystery._siteNumber)
- continue;
- if (seen == row) {
- _mystery._lastSite = _mystery._siteNumber;
- _mystery._siteNumber = (uint16)i;
- return;
- }
- seen++;
- }
- }
}
}
if (dirty)
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 319f31b3f68..8fa94de5ae5 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -64,7 +64,14 @@ void SiteScreen::run() {
if (!_mystery || !_mystery->isLoaded())
return;
- uint cur = 0;
+ // The caller (run() in eem.cpp) is responsible for bringing the
+ // player into a site via the map first, so `_siteNumber` is
+ // already set to the destination they picked. Resuming a save
+ // also restores `_siteNumber`. Start there instead of forcing
+ // site 0 each time.
+ uint cur = _mystery->_siteNumber;
+ if (cur >= _mystery->numSites())
+ cur = 0;
enter(cur);
while (!_vm->shouldQuit()) {
@@ -188,19 +195,26 @@ void SiteScreen::run() {
}
void SiteScreen::renderBackground(uint siteNum) {
- // Mirrors `_BuildBackground` @ 172b:13e2 (simplified):
- // 1. Load PIC 0x3d (the site frame) from PICS.DBD.
- // 2. Load entry @p siteNum from SITES.DBD (the site scene).
- // 3. Composite scene into the frame at the position carried in the
- // SiteData fields.
- // 4. Set palette to (siteNum + 1) â per-site palettes start at 1.
- // We render frame + scene at (0,0); the original positions the scene
- // at (x,y) read from the SiteData but we don't have the offsets fully
- // decoded yet so a top-left placement will do.
+ // Site loop entry `screen 1` calls into a function at 20fe:120b in
+ // the original; that function opens `_GetPicture(0x43)` (PIC 0x43,
+ // 1-based) as the screen frame and `_GetPalette(0x23)` as the base
+ // palette. The case-briefing's `_BuildBackground @ 172b:13e2`
+ // instead uses entry 0x3d directly and palette `sitenum + 1` â but
+ // that path is only used by `_DisplayCorrect` (winner scene), not
+ // by the regular per-site renderer.
+ //
+ // We follow the regular site loop here:
+ // 1. PIC 0x43 frame at (0, 0).
+ // 2. SITES.DBD entry indexed by SiteData[+0] (1-based sitepic) at
+ // (x, y) from the original `_Rect_Move` composition.
+ // 3. Per-site palette (`sitenum + 1`) â set by the caller in
+ // SiteScreen::enter via `setSitePaletteForSite`.
- // Frame.
Picture frame;
- if (_vm->getPics().getPicture(0x3d, frame)) {
+ bool haveFrame = _vm->getPics().getPicture(0x43, frame);
+ if (!haveFrame)
+ haveFrame = _vm->getPics().getPicture(0x3d, frame);
+ if (haveFrame) {
g_system->copyRectToScreen(frame.surface.getPixels(),
frame.surface.pitch,
0, 0, frame.surface.w, frame.surface.h);
Commit: 3fe1b2e115c9d504281bb706e8b6fc2daecf3261
https://github.com/scummvm/scummvm/commit/3fe1b2e115c9d504281bb706e8b6fc2daecf3261
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:36+02:00
Commit Message:
EEM: improved character animations when arriving to locations
Changed paths:
engines/eem/eem.h
engines/eem/site.cpp
engines/eem/site.h
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index ed75d78e5ec..24bddf6f644 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -74,6 +74,7 @@ public:
DBDArchive &getBalloons(){ return _balloonArchive; }
Mystery &getMystery() { return _mystery; }
const EEMFont &getFont() const { return _font; }
+ uint8 getPartnerIndex() const { return _partner; }
/// Display one ClueBlock. @p clueBlock points at the u16 frame count
/// followed by 62-byte ClueEntries. Mirrors _DisplayClue @ 2404:05e6.
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 8fa94de5ae5..2ee29ea63ab 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -56,6 +56,23 @@ void SiteScreen::enter(uint siteNum) {
_vm->setSitePaletteForSite(sitepic);
renderBackground(siteNum);
+
+ // `_DoSiteLoop @ 168d:03f4` plays `_EnterSiteAnim` whenever
+ // `_LastSite != _SiteNumber`. We track the last site we *played*
+ // the arrival on so re-entries (after notebook/map/etc.) don't
+ // repeat the animation.
+ if ((int)siteNum != _lastSiteAnim) {
+ enterSiteAnim();
+ _lastSiteAnim = (int)siteNum;
+ // Re-paint the BG; the arrival animation drew on top of it.
+ renderBackground(siteNum);
+ }
+
+ // Persistent partner sprite (`_NewAnimation` at the tail of
+ // `_DoSiteLoop`). Drawn after the BG so the hotspot outlines and
+ // HUD that follow stay on top of it.
+ renderPartner(siteNum);
+
renderHotspots(siteNum);
g_system->updateScreen();
}
@@ -194,56 +211,245 @@ void SiteScreen::run() {
}
}
-void SiteScreen::renderBackground(uint siteNum) {
- // Site loop entry `screen 1` calls into a function at 20fe:120b in
- // the original; that function opens `_GetPicture(0x43)` (PIC 0x43,
- // 1-based) as the screen frame and `_GetPalette(0x23)` as the base
- // palette. The case-briefing's `_BuildBackground @ 172b:13e2`
- // instead uses entry 0x3d directly and palette `sitenum + 1` â but
- // that path is only used by `_DisplayCorrect` (winner scene), not
- // by the regular per-site renderer.
- //
- // We follow the regular site loop here:
- // 1. PIC 0x43 frame at (0, 0).
- // 2. SITES.DBD entry indexed by SiteData[+0] (1-based sitepic) at
- // (x, y) from the original `_Rect_Move` composition.
- // 3. Per-site palette (`sitenum + 1`) â set by the caller in
- // SiteScreen::enter via `setSitePaletteForSite`.
+void SiteScreen::enterSiteAnim() {
+ // Mirrors `_EnterSiteAnim @ 1000:9b21`. Two phases, both partner
+ // dependent:
+ // Phase 1 â skateboard scroll: anim 6 (Jake) / 0xe (Jenny). Sprite
+ // starts at (320 - sprite_w, 199 - sprite_h) and slides
+ // left until off-screen.
+ // Phase 2 â KD slide-in: anim 7 (Jake) / 0xf (Jenny). Sprite enters
+ // from x = -sprite_w at y = 0x8b (Jake) / 0x8e (Jenny)
+ // and slides until x = 0.
+ // Original cycles frames every `_MoveSkateBoardPixels` worth of
+ // motion (a runtime-calibrated speed value); we use a fixed 4 px
+ // per tick which feels close to the DOS pacing.
+ if (!_vm || !_mystery)
+ return;
+ const uint8 partner = _vm->getPartnerIndex();
+ const uint kSkateAni = (partner == 0) ? 6 : 0xe;
+ const uint kKDAni = (partner == 0) ? 7 : 0xf;
+ const int kKDY = (partner == 0) ? 0x8b : 0x8e;
+
+ // Snapshot the current screen so we can restore between frames.
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (!screen)
+ return;
+ Graphics::ManagedSurface bg(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ for (int row = 0; row < 200; row++) {
+ memcpy((byte *)bg.getBasePtr(0, row),
+ (const byte *)screen->getBasePtr(0, row), 320);
+ }
+ g_system->unlockScreen();
+
+ auto blitFrame = [](Graphics::ManagedSurface &dst, const Picture &p,
+ int x, int y, byte transp) {
+ const int w = p.surface.w, h = p.surface.h;
+ for (int row = 0; row < h; row++) {
+ const int dstY = y + row;
+ if (dstY < 0 || dstY >= 200)
+ continue;
+ const byte *src = (const byte *)p.surface.getBasePtr(0, row);
+ byte *out = (byte *)dst.getBasePtr(0, dstY);
+ for (int col = 0; col < w; col++) {
+ const int dstX = x + col;
+ if (dstX < 0 || dstX >= 320)
+ continue;
+ if (src[col] != transp)
+ out[dstX] = src[col];
+ }
+ }
+ };
+
+ // Phase 1 â skateboard scroll. `_GetAnimation(6 | 0xe)`.
+ Animation skate;
+ if (_vm->getAni().loadAnimation(kSkateAni, skate) && !skate.empty()) {
+ // `iVar4 = 199 - sprite_h`, `uVar5 = 320 - sprite_w` from the
+ // original; sprite_h/w come from the FIRST frame.
+ const int spriteH = skate[0].surface.h;
+ const int spriteW = skate[0].surface.w;
+ int x = (320 - spriteW) & ~3; // 4-px aligned (mode-X)
+ const int y = 199 - spriteH;
+ const byte transp = (byte)(skate[0].flags >> 8);
+ uint frameIdx = 0;
+ int distSinceTick = 0;
+ const int kStep = 4; // _MoveSkateBoardPixels analogue
+ const int kFrameTicks = 0xc; // original switches frame at 12 px
+
+ while (x + spriteW > 0 && !_vm->shouldQuit()) {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ for (int row = 0; row < 200; row++) {
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)bg.getBasePtr(0, row), 320);
+ }
+ blitFrame(scratch, skate[frameIdx], x, y, transp);
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_KEYDOWN ||
+ ev.type == Common::EVENT_LBUTTONDOWN) {
+ return; // user-skip â bail out of the animation
+ }
+ }
+
+ x -= kStep;
+ distSinceTick += kStep;
+ if (distSinceTick >= kFrameTicks) {
+ frameIdx = (frameIdx + 1) % skate.size();
+ distSinceTick = 0;
+ }
+ g_system->delayMillis(40);
+ }
+ }
+
+ // Phase 2 â KD slide-in. From `_EnterSiteAnim` each frame is blitted
+ // at its OWN anchor offsets (the sprite "walks in" because the
+ // frame-by-frame anchors decrease as the animation progresses):
+ // destX = -frame.miscflags (on-disk byte 8 = anchor X)
+ // destY = kKDY - frame.rowoff (on-disk byte 6 = anchor Y)
+ // Each frame waits one `_CheckFrameRate` tick â we use 80 ms which
+ // matches the original's ~12 FPS pacing.
+ Animation kd;
+ if (_vm->getAni().loadAnimation(kKDAni, kd) && !kd.empty()) {
+ for (uint frameIdx = 0;
+ frameIdx < kd.size() && !_vm->shouldQuit();
+ frameIdx++) {
+ const Picture &fr = kd[frameIdx];
+ const byte transp = (byte)(fr.flags >> 8);
+ const int destX = -(int)fr.miscflags;
+ const int destY = kKDY - (int)fr.rowoff;
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ for (int row = 0; row < 200; row++) {
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)bg.getBasePtr(0, row), 320);
+ }
+ blitFrame(scratch, fr, destX, destY, transp);
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_KEYDOWN ||
+ ev.type == Common::EVENT_LBUTTONDOWN) {
+ return;
+ }
+ }
+ g_system->delayMillis(80);
+ }
+ }
+}
+
+void SiteScreen::renderPartner(uint siteNum) {
+ // `_DoSiteLoop @ 168d:03f4` reads `siteData[+8]` as the speaker
+ // table index, then for each (speaker à partner) loads
+ // anim = WaitAnims[speakerIdx].anim[partner]
+ // x = WaitAnims[speakerIdx].x[partner]
+ // y = WaitAnims[speakerIdx].y[partner]
+ // from the table at `_WaitAnims @ 29be:021c`. Each entry is
+ // 12 bytes / 6 u16:
+ // +0..1 anim Jake, +2..3 anim Jenny,
+ // +4..5 x Jake, +6..7 x Jenny,
+ // +8..9 y Jake, +10..11 y Jenny.
+ // Verbatim copy of the bytes Ghidra dumped at 29be:021c so we
+ // don't need to ship the original data segment.
+ static const uint16 kWaitAnims[][6] = {
+ { 0x00, 0x0a, 0x06, 0x06, 0x50, 0x50 }, // 0
+ { 0x03, 0x0c, 0x06, 0x06, 0x50, 0x50 }, // 1
+ { 0x01, 0x0b, 0x06, 0x06, 0x50, 0x50 }, // 2
+ { 0x04, 0x0d, 0x06, 0x06, 0x50, 0x50 }, // 3
+ { 0x02, 0x10, 0x06, 0x06, 0x50, 0x50 }, // 4
+ { 0x05, 0x05, 0x06, 0x06, 0x50, 0x50 }, // 5
+ { 0x06, 0x06, 0x06, 0x06, 0x50, 0x50 }, // 6
+ { 0x00, 0x00, 0x23, 0x6f, 0x38, 0x88 }, // 7 â special pos
+ { 0x07, 0xb1, 0x39, 0xc8, 0x88, 0xae }, // 8 â likely junk; 0xb1 anim id is suspect
+ { 0x9d, 0xbe, 0xa3, 0xae, 0xb8, 0xbe } // 9 â likely junk
+ };
+
+ const byte *site = _mystery->siteData(siteNum);
+ if (!site)
+ return;
+ const uint16 speaker = READ_LE_UINT16(site + 8);
+ if (speaker >= ARRAYSIZE(kWaitAnims))
+ return;
+
+ const uint8 partner = _vm->getPartnerIndex();
+ const uint animId = kWaitAnims[speaker][0 + partner];
+ const int x = (int)(int16)kWaitAnims[speaker][2 + partner];
+ const int y = (int)(int16)kWaitAnims[speaker][4 + partner];
+
+ Animation anim;
+ if (!_vm->getAni().loadAnimation(animId, anim) || anim.empty())
+ return;
+
+ // Show the first frame as a static sprite. The original updates it
+ // each `_CheckFrameRate` tick; we don't have a frame pump in the
+ // site loop yet so a static pose is enough for now.
+ const Picture &fr = anim[0];
+ const byte transp = (byte)(fr.flags >> 8);
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (!screen)
+ return;
+ for (int row = 0; row < fr.surface.h; row++) {
+ const int dstY = y + row;
+ if (dstY < 0 || dstY >= 200)
+ continue;
+ const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
+ byte *dst = (byte *)screen->getBasePtr(0, dstY);
+ for (int col = 0; col < fr.surface.w; col++) {
+ const int dstX = x + col;
+ if (dstX < 0 || dstX >= 320)
+ continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ g_system->unlockScreen();
+}
+
+void SiteScreen::renderBackground(uint siteNum) {
+ // Mirrors `_BuildBackground(sitepic, 0x42, 0x14)` as called from
+ // `_DoSiteLoop @ 168d:03f4` and `_DisplayCorrect`. The original:
+ // 1. Loads frame via `_GetFromDB(_PicIndex, 0x3d)` â that's
+ // DBI entry 0x3d (0-based). Note this is NOT the same as
+ // `_GetPicture(0x3d)` which would index entry 0x3c. Our
+ // `loadEntry(0x3d)` matches the original directly.
+ // 2. Loads SITES.DBD entry by `sitepic` (0-based, from
+ // SiteData[+0..+1]).
+ // 3. `_Rect_Move(... x, y, 48000, ...)` composes the scene at
+ // (x, y) = (0x42, 0x14) = (66, 20) on top of the frame.
+ // 4. `_GetPalette(sitepic + 1)` â per-site palette.
Picture frame;
- bool haveFrame = _vm->getPics().getPicture(0x43, frame);
- if (!haveFrame)
- haveFrame = _vm->getPics().getPicture(0x3d, frame);
- if (haveFrame) {
+ if (_vm->getPics().loadEntry(0x3d, frame)) {
g_system->copyRectToScreen(frame.surface.getPixels(),
frame.surface.pitch,
0, 0, frame.surface.w, frame.surface.h);
}
- // Scene from SITES.DBD: indexed by `sitepic` from SiteData (global
- // SITES.DBD entry, NOT the per-mystery site index). Falls back to
- // `_GetPicture(sitepic)` if SITES is unavailable.
const byte *site = _mystery->siteData(siteNum);
const uint16 sitepic = site ? READ_LE_UINT16(site) : 0;
Picture scene;
bool haveScene = false;
- bool fromPics = false;
if (sitepic > 0 && _vm->getSites().size() > sitepic - 1)
haveScene = _vm->getSites().loadEntry(sitepic - 1, scene);
- if (!haveScene && sitepic > 0) {
+ if (!haveScene && sitepic > 0)
haveScene = _vm->getPics().getPicture(sitepic, scene);
- fromPics = haveScene;
- }
if (haveScene) {
- const int w = MIN<int>(scene.surface.w, 320);
- const int h = MIN<int>(scene.surface.h, 200);
- // Full-screen pictures (sitepic fallback) go at (0, 0); smaller
- // SITES.DBD scenes are centred horizontally with the top below
- // the HUD bar so progress info stays visible.
- const int x = fromPics ? 0 : (320 - w) / 2;
- const int y = fromPics ? 0 : (h < 180 ? 4 : 0);
- g_system->copyRectToScreen(scene.surface.getPixels(),
- scene.surface.pitch, x, y, w, h);
+ // Hard-coded composition position from `_BuildBackground`:
+ // `_Rect_Move(0, 0, h, ..., 0x42, 0x14, 48000, h, w)`.
+ const int x = 0x42;
+ const int y = 0x14;
+ const int w = MIN<int>(scene.surface.w, 320 - x);
+ const int h = MIN<int>(scene.surface.h, 200 - y);
+ if (w > 0 && h > 0)
+ g_system->copyRectToScreen(scene.surface.getPixels(),
+ scene.surface.pitch, x, y, w, h);
}
}
diff --git a/engines/eem/site.h b/engines/eem/site.h
index ce450bb1905..2346f5e5a77 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -65,9 +65,22 @@ private:
int hotspotAtPoint(uint siteNum, int x, int y) const;
void onHotspotClicked(uint siteNum, uint hotIdx);
+ /// Play the partner's site-arrival sequence once `_LastSite !=
+ /// _SiteNumber`. Mirrors `_EnterSiteAnim @ 1000:9b21` â animation
+ /// 6 (Jake) / 14 (Jenny) skateboards in from the right edge along
+ /// the bottom, then animation 7 / 15 slides KD in from the left.
+ void enterSiteAnim();
+
+ /// Draw the persistent in-site partner sprite (Jake or Jenny
+ /// standing/idling) at the position from `_WaitAnims` @ 29be:021c.
+ /// Mirrors the `_GetAnimation` + `_NewAnimation` block at the tail
+ /// of `_DoSiteLoop @ 168d:03f4`.
+ void renderPartner(uint siteNum);
+
EEMEngine *_vm;
Mystery *_mystery;
bool _showHotspots = true; ///< Toggle outlines with V key.
+ int _lastSiteAnim = -1; ///< Last site we played the arrival on.
};
} // End of namespace EEM
Commit: 30367c4690af026d7a3889ebbf7530ba2bfbe2d7
https://github.com/scummvm/scummvm/commit/30367c4690af026d7a3889ebbf7530ba2bfbe2d7
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:36+02:00
Commit Message:
EEM: zoom/overview map initial implementation
Changed paths:
engines/eem/eem.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index f906aec7d69..98539e94427 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -1507,85 +1507,197 @@ void EEMEngine::doGallery() {
}
void EEMEngine::doBigMap() {
- // Mirrors the small/scrollable map view used inside the site loop â
- // the original game draws a 0xe9 Ã 0xab viewport at (2, 2) into
- // `_MapBitMap` (= BIGMAP.PIC) via `DrawMap @ 20fe:1058`; on top of
- // that, `_StampButtons @ 20fe:0d2f` bakes site icons into the
- // bitmap at MapData offsets (+4, +6). We render BIGMAP.PIC into a
- // 233Ã171 viewport and overlay markers ourselves.
+ // Two-stage flow that mirrors the original screen-1 wrapper at
+ // 20fe:120b and `_DoBigMap @ 20fe:09e7`:
//
- // MapData entry layout (14 bytes), confirmed from
- // `_DrawBigMapButtons @ 20fe:0877` and `_StampButtons @ 20fe:0d2f`:
- // +0..1 BigMap X (overview frame PIC 0x42)
- // +2..3 BigMap Y
- // +4..5 SmallMap X (stamped into _MapBitMap at this position)
- // +6..7 SmallMap Y
- // +8..9 crime-flag (DAT_2d5d_5436): non-zero â crime-scene marker
- // +10..13 unused
+ // STAGE 1 â Overview. PIC 0x42 + site icons drawn via the
+ // `_DrawBigMapButtons` algorithm at BigMap coords MapData[+4/+6].
+ // The original `_DoBigMap` returns sx/sy = (mouseX*2 - 0x74,
+ // mouseY*2 - 0x55) when the player clicks inside `BigMapWindow`,
+ // which is the scroll position into the SmallMap.
+ //
+ // STAGE 2 â Detail zoom. PIC 0x43 frame + a 0xe9 Ã 0xab viewport
+ // into BIGMAP.PIC at (2, 2), drawn by `DrawMap @ 20fe:1058` with
+ // the (sx, sy) returned from stage 1. Site icons are stamped at
+ // SmallMap coords MapData[+8/+0xa] via `_StampButtons`. Click on
+ // a site icon â travel.
+ //
+ // MapData entry layout (14 bytes), verified directly from the
+ // disassembly of `_DrawBigMapButtons @ 20fe:0877` (`PUSH ES:[BX+4]`
+ // for X, `PUSH ES:[BX+6]` for Y, `CMP ES:[BX+0xc], 0` for crime)
+ // and `_StampButtons @ 20fe:0d2f` (`MOV AX, ES:[BX+8]`,
+ // `MOV AX, ES:[BX+0xa]`):
+ // +0..3 ??? (not yet decoded)
+ // +4..5 BigMap X
+ // +6..7 BigMap Y
+ // +8..9 SmallMap X
+ // +0xa..b SmallMap Y
+ // +0xc..d crime-flag
+
+ if (!_mystery.isLoaded())
+ return;
+
+ CursorMan.showMouse(true);
+
+ // `_GetPalette(0x24)` per `_DoBigMap @ 20fe:09e7`.
+ setSitePalette(0x24);
+
+ const Common::Rect kSetupRect(0xc7, 0x12, 0xc7 + 0x32, 0x12 + 0xa); // approx; original from globals
+ (void)kSetupRect; // not yet wired into our overlay
+
+ // ------------------------------------------------------------------
+ // STAGE 1 â Overview: PIC 0x42 + clickable site icons.
+ // ------------------------------------------------------------------
+
+ auto drawOverview = [&]() {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+
+ Picture frame;
+ if (_picsArchive.getPicture(0x42, frame)) {
+ const int w = MIN<int>(frame.surface.w, 320);
+ const int h = MIN<int>(frame.surface.h, 200);
+ for (int row = 0; row < h; row++)
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)frame.surface.getBasePtr(0, row), w);
+ }
+
+ // Site icons at BigMap coords (+4, +6). Three colours by state:
+ // visited â DoneMarker analogue, crime flag â CrimeMarker,
+ // else â SiteMarker. Current site gets a bright highlight.
+ for (uint i = 0; i < _mystery.numSites(); i++) {
+ if (!_mystery._onSites[i] && i != _mystery._siteNumber)
+ continue;
+ const byte *entry = _mystery.mapEntry(i);
+ if (!entry)
+ continue;
+ const uint16 mx = READ_LE_UINT16(entry + 0x4);
+ const uint16 my = READ_LE_UINT16(entry + 0x6);
+ const uint16 crime = READ_LE_UINT16(entry + 0xc);
+
+ byte color;
+ if (i < Mystery::kVisitedSiteCap && _mystery._visitedSite[i])
+ color = 0x07;
+ else if (crime != 0)
+ color = 0x0C;
+ else
+ color = 0x0F;
+ if (i == _mystery._siteNumber)
+ color = 0x0E;
+
+ const Common::Rect mark(mx - 3, my - 3, mx + 4, my + 4);
+ scratch.fillRect(mark, color);
+ }
+
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ };
+
+ auto findSiteAt = [&](int x, int y) -> int {
+ // Hit-test the icons drawn in stage 1. Generous radius matches
+ // the marker square plus a couple of pixels of slop.
+ for (uint i = 0; i < _mystery.numSites(); i++) {
+ if (!_mystery._onSites[i] && i != _mystery._siteNumber)
+ continue;
+ const byte *entry = _mystery.mapEntry(i);
+ if (!entry) continue;
+ const int mx = (int)READ_LE_UINT16(entry + 0x4);
+ const int my = (int)READ_LE_UINT16(entry + 0x6);
+ if (ABS(x - mx) <= 6 && ABS(y - my) <= 6)
+ return (int)i;
+ }
+ return -1;
+ };
+
+ drawOverview();
+
+ bool wantZoom = false;
+ int zoomX = 0, zoomY = 0;
+ while (!shouldQuit()) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return;
+ if (ev.type == Common::EVENT_KEYDOWN &&
+ ev.kbd.keycode == Common::KEYCODE_ESCAPE)
+ return;
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ const int hit = findSiteAt(ev.mouse.x, ev.mouse.y);
+ if (hit >= 0) {
+ // Direct click on a site marker â travel.
+ _mystery._lastSite = _mystery._siteNumber;
+ _mystery._siteNumber = (uint16)hit;
+ return;
+ }
+ // `_DoBigMap` rule for clicks elsewhere: convert mouse
+ // coords to a SmallMap scroll position
+ // sx = mouseX * 2 - 0x74
+ // sy = mouseY * 2 - 0x55
+ // then transition to the detail zoom.
+ int sx = ev.mouse.x * 2;
+ int sy = ev.mouse.y * 2;
+ sx = (sx < 0x75) ? 0 : sx - 0x74;
+ sy = (sy < 0x56) ? 0 : sy - 0x55;
+ zoomX = sx;
+ zoomY = sy;
+ wantZoom = true;
+ break;
+ }
+ }
+ if (wantZoom)
+ break;
+ g_system->updateScreen();
+ g_system->delayMillis(10);
+ }
+
+ if (!wantZoom)
+ return;
+
+ // ------------------------------------------------------------------
+ // STAGE 2 â Detail zoom: PIC 0x43 frame + scrollable BIGMAP.PIC
+ // viewport at (2, 2), 0xe9 Ã 0xab. Click on a stamped icon â travel.
+ // ------------------------------------------------------------------
+
Common::File f;
if (!f.open(Common::Path("BIGMAP.PIC"))) {
- warning("doBigMap: BIGMAP.PIC missing");
+ warning("doBigMap: BIGMAP.PIC missing for detail view");
return;
}
const uint16 mapH = f.readUint16LE();
const uint16 mapW = f.readUint16LE();
if (mapW == 0 || mapH == 0)
return;
-
Common::Array<byte> mapPixels((uint32)mapW * mapH);
if (f.read(mapPixels.data(), mapPixels.size()) != mapPixels.size()) {
- warning("doBigMap: short read on BIGMAP.PIC");
+ warning("doBigMap: short read on BIGMAP.PIC for detail view");
return;
}
- // Viewport size from `DrawMap`: 0xe9 Ã 0xab at screen (2, 2).
- // We use (4, 4) so a 1-pixel inset preserves the frame border.
const int kMapWinW = 0xe9; // 233
const int kMapWinH = 0xab; // 171
- const int kMapWinX = 4;
- const int kMapWinY = 4;
-
- int scrollX = 0;
- int scrollY = 0;
-
- // Auto-scroll to centre the current site (using SmallMap coords).
- if (_mystery.isLoaded()) {
- const byte *entry = _mystery.mapEntry(_mystery._siteNumber);
- if (entry) {
- const uint16 mx = READ_LE_UINT16(entry + 4);
- const uint16 my = READ_LE_UINT16(entry + 6);
- scrollX = MAX<int>(0, MIN<int>(mapW - kMapWinW,
- (int)mx - kMapWinW / 2));
- scrollY = MAX<int>(0, MIN<int>(mapH - kMapWinH,
- (int)my - kMapWinH / 2));
- }
- }
+ const int kMapWinX = 2;
+ const int kMapWinY = 2;
- CursorMan.showMouse(true);
-
- // `_DoBigMap @ 20fe:09e7` calls `_GetPalette(0x24)` after loading
- // the frame. Without this, the map would render under whatever
- // palette the prior site set, which makes BIGMAP.PIC look like
- // noise / incomplete colours.
- setSitePalette(0x24);
+ int scrollX = MAX<int>(0, MIN<int>(mapW - kMapWinW, zoomX));
+ int scrollY = MAX<int>(0, MIN<int>(mapH - kMapWinH, zoomY));
- auto draw = [&]() {
+ auto drawDetail = [&]() {
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
- // Frame from PIC 0x42 (`_GetPicture(0x42)` in `_DoBigMap`).
Picture frame;
- if (_picsArchive.getPicture(0x42, frame)) {
+ if (_picsArchive.getPicture(0x43, frame)) {
const int w = MIN<int>(frame.surface.w, 320);
const int h = MIN<int>(frame.surface.h, 200);
- for (int row = 0; row < h; row++) {
+ for (int row = 0; row < h; row++)
memcpy((byte *)scratch.getBasePtr(0, row),
(const byte *)frame.surface.getBasePtr(0, row), w);
- }
}
- // Map content clipped into the inner window, applying scroll.
const int copyW = MIN<int>(mapW - scrollX, kMapWinW);
const int copyH = MIN<int>(mapH - scrollY, kMapWinH);
for (int row = 0; row < copyH; row++) {
@@ -1594,48 +1706,42 @@ void EEMEngine::doBigMap() {
copyW);
}
- // Site markers â three states from `_DrawBigMapButtons`:
- // _DoneMarker if `SaveSiteComplete[i]` (already searched)
- // _CrimeMarker if MapData[+8] != 0 (crime scene)
- // _SiteMarker otherwise
- // We don't have the marker PICs traced yet, so draw small filled
- // squares with three colours that match the original semantics.
- if (_mystery.isLoaded()) {
- for (uint i = 0; i < _mystery.numSites(); i++) {
- if (!_mystery._onSites[i] && i != _mystery._siteNumber)
- continue;
- const byte *entry = _mystery.mapEntry(i);
- if (!entry)
- continue;
- const uint16 mx = READ_LE_UINT16(entry + 4);
- const uint16 my = READ_LE_UINT16(entry + 6);
- const uint16 crime = READ_LE_UINT16(entry + 8);
- const int sx = (int)mx - scrollX + kMapWinX;
- const int sy = (int)my - scrollY + kMapWinY;
- if (sx < kMapWinX || sx >= kMapWinX + kMapWinW ||
- sy < kMapWinY || sy >= kMapWinY + kMapWinH)
- continue;
+ // Stamped site icons at SmallMap coords (+8, +0xa). Same colour
+ // scheme as the overview so the player can tell them apart.
+ for (uint i = 0; i < _mystery.numSites(); i++) {
+ if (!_mystery._onSites[i] && i != _mystery._siteNumber)
+ continue;
+ const byte *entry = _mystery.mapEntry(i);
+ if (!entry) continue;
+ const uint16 mx = READ_LE_UINT16(entry + 0x8);
+ const uint16 my = READ_LE_UINT16(entry + 0xa);
+ const uint16 crime = READ_LE_UINT16(entry + 0xc);
+ const int sx = (int)mx - scrollX + kMapWinX;
+ const int sy = (int)my - scrollY + kMapWinY;
+ if (sx < kMapWinX || sx >= kMapWinX + kMapWinW ||
+ sy < kMapWinY || sy >= kMapWinY + kMapWinH)
+ continue;
- byte color;
- if (i < Mystery::kVisitedSiteCap && _mystery._visitedSite[i])
- color = 0x07; // searched (DoneMarker analogue)
- else if (crime != 0)
- color = 0x0C; // crime scene (CrimeMarker analogue)
- else
- color = 0x0F; // available (SiteMarker analogue)
- if (i == _mystery._siteNumber)
- color = 0x0E; // current site â bright yellow
-
- const Common::Rect mark(sx - 3, sy - 3, sx + 4, sy + 4);
- scratch.fillRect(mark, color);
- }
+ byte color;
+ if (i < Mystery::kVisitedSiteCap && _mystery._visitedSite[i])
+ color = 0x07;
+ else if (crime != 0)
+ color = 0x0C;
+ else
+ color = 0x0F;
+ if (i == _mystery._siteNumber)
+ color = 0x0E;
+
+ const Common::Rect mark(sx - 3, sy - 3, sx + 4, sy + 4);
+ scratch.fillRect(mark, color);
}
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
g_system->updateScreen();
};
- draw();
+
+ drawDetail();
while (!shouldQuit()) {
Common::Event ev;
@@ -1646,37 +1752,26 @@ void EEMEngine::doBigMap() {
return;
if (ev.type == Common::EVENT_KEYDOWN) {
if (ev.kbd.keycode == Common::KEYCODE_ESCAPE)
- return;
+ return; // exit detail back to caller (site loop / engine)
const int kStep = 16;
if (ev.kbd.keycode == Common::KEYCODE_LEFT) {
scrollX = MAX<int>(0, scrollX - kStep);
dirty = true;
} else if (ev.kbd.keycode == Common::KEYCODE_RIGHT) {
- scrollX = MIN<int>(MAX<int>(0, mapW - kMapWinW), scrollX + kStep);
+ scrollX = MIN<int>(MAX<int>(0, mapW - kMapWinW),
+ scrollX + kStep);
dirty = true;
} else if (ev.kbd.keycode == Common::KEYCODE_UP) {
scrollY = MAX<int>(0, scrollY - kStep);
dirty = true;
} else if (ev.kbd.keycode == Common::KEYCODE_DOWN) {
- scrollY = MIN<int>(MAX<int>(0, mapH - kMapWinH), scrollY + kStep);
+ scrollY = MIN<int>(MAX<int>(0, mapH - kMapWinH),
+ scrollY + kStep);
dirty = true;
}
- if (ev.kbd.keycode >= Common::KEYCODE_0 &&
- ev.kbd.keycode <= Common::KEYCODE_9) {
- const uint target = (uint)(ev.kbd.keycode - Common::KEYCODE_0);
- if (_mystery.isLoaded() &&
- target < _mystery.numSites() &&
- _mystery._onSites[target]) {
- _mystery._lastSite = _mystery._siteNumber;
- _mystery._siteNumber = (uint16)target;
- return;
- }
- }
}
if (ev.type == Common::EVENT_LBUTTONDOWN) {
- // Click on a map marker?
- if (_mystery.isLoaded() &&
- ev.mouse.x >= kMapWinX &&
+ if (ev.mouse.x >= kMapWinX &&
ev.mouse.x < kMapWinX + kMapWinW &&
ev.mouse.y >= kMapWinY &&
ev.mouse.y < kMapWinY + kMapWinH) {
@@ -1686,23 +1781,22 @@ void EEMEngine::doBigMap() {
continue;
const byte *entry = _mystery.mapEntry(i);
if (!entry) continue;
- const uint16 mx = READ_LE_UINT16(entry + 4);
- const uint16 my = READ_LE_UINT16(entry + 6);
+ const uint16 mx = READ_LE_UINT16(entry + 0x8);
+ const uint16 my = READ_LE_UINT16(entry + 0xa);
const int sx = (int)mx - scrollX + kMapWinX;
const int sy = (int)my - scrollY + kMapWinY;
- if (ABS(ev.mouse.x - sx) <= 5 &&
- ABS(ev.mouse.y - sy) <= 5) {
+ if (ABS(ev.mouse.x - sx) <= 6 &&
+ ABS(ev.mouse.y - sy) <= 6) {
_mystery._lastSite = _mystery._siteNumber;
_mystery._siteNumber = (uint16)i;
return;
}
}
}
-
}
}
if (dirty)
- draw();
+ drawDetail();
g_system->updateScreen();
g_system->delayMillis(10);
}
Commit: a4c4170605ca1557ef49e108465c07e0e62d2fa8
https://github.com/scummvm/scummvm/commit/a4c4170605ca1557ef49e108465c07e0e62d2fa8
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:36+02:00
Commit Message:
EEM: load the correct sites and make sure they are labeled
Changed paths:
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/site.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 98539e94427..e79f9723286 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -248,6 +248,12 @@ bool EEMEngine::openArchives() {
warning("SITES archive missing â site backgrounds disabled");
if (!_balloonArchive.open(Common::Path("BALLOON.DBD"), Common::Path("BALLOON.DBX")))
warning("BALLOON archive missing â clue text will lack balloons");
+ // `_GetButton @ 172b:199d` reads from this archive (see strings
+ // 'button.dbd' / 'Button.DBX' at 29be:06bf / 29be:04bb). Each
+ // per-site map marker (used by `_StampButtons @ 20fe:0d2f` and
+ // looked up via MapData[+0]) lives here.
+ if (!_buttonArchive.open(Common::Path("BUTTON.DBD"), Common::Path("BUTTON.DBX")))
+ warning("BUTTON archive missing â map markers will be unlabelled");
return true;
}
@@ -1595,22 +1601,6 @@ void EEMEngine::doBigMap() {
g_system->updateScreen();
};
- auto findSiteAt = [&](int x, int y) -> int {
- // Hit-test the icons drawn in stage 1. Generous radius matches
- // the marker square plus a couple of pixels of slop.
- for (uint i = 0; i < _mystery.numSites(); i++) {
- if (!_mystery._onSites[i] && i != _mystery._siteNumber)
- continue;
- const byte *entry = _mystery.mapEntry(i);
- if (!entry) continue;
- const int mx = (int)READ_LE_UINT16(entry + 0x4);
- const int my = (int)READ_LE_UINT16(entry + 0x6);
- if (ABS(x - mx) <= 6 && ABS(y - my) <= 6)
- return (int)i;
- }
- return -1;
- };
-
drawOverview();
bool wantZoom = false;
@@ -1625,18 +1615,11 @@ void EEMEngine::doBigMap() {
ev.kbd.keycode == Common::KEYCODE_ESCAPE)
return;
if (ev.type == Common::EVENT_LBUTTONDOWN) {
- const int hit = findSiteAt(ev.mouse.x, ev.mouse.y);
- if (hit >= 0) {
- // Direct click on a site marker â travel.
- _mystery._lastSite = _mystery._siteNumber;
- _mystery._siteNumber = (uint16)hit;
- return;
- }
- // `_DoBigMap` rule for clicks elsewhere: convert mouse
- // coords to a SmallMap scroll position
- // sx = mouseX * 2 - 0x74
- // sy = mouseY * 2 - 0x55
- // then transition to the detail zoom.
+ // `_DoBigMap @ 20fe:09e7` does NOT travel directly when
+ // the player clicks an overview icon â it always returns
+ // `(sx, sy) = (mouseX*2 - 0x74, mouseY*2 - 0x55)` so the
+ // caller can switch to the detail zoom centred there.
+ // Travel happens after a SECOND click in the detail view.
int sx = ev.mouse.x * 2;
int sy = ev.mouse.y * 2;
sx = (sx < 0x75) ? 0 : sx - 0x74;
@@ -1706,34 +1689,42 @@ void EEMEngine::doBigMap() {
copyW);
}
- // Stamped site icons at SmallMap coords (+8, +0xa). Same colour
- // scheme as the overview so the player can tell them apart.
+ // Stamped site buttons. `_StampButtons @ 20fe:0d2f` does:
+ // button = _GetButton(MapData[+0]) // BUTTON.DBD entry
+ // destX = MapData[+8], destY = MapData[+0xa]
+ // then bakes the button PIC into the map bitmap. Each button
+ // sprite carries the site name baked in. We blit them on top
+ // of the BIGMAP.PIC viewport at the same SmallMap coords.
for (uint i = 0; i < _mystery.numSites(); i++) {
if (!_mystery._onSites[i] && i != _mystery._siteNumber)
continue;
const byte *entry = _mystery.mapEntry(i);
if (!entry) continue;
- const uint16 mx = READ_LE_UINT16(entry + 0x8);
- const uint16 my = READ_LE_UINT16(entry + 0xa);
- const uint16 crime = READ_LE_UINT16(entry + 0xc);
+ const uint16 buttonId = READ_LE_UINT16(entry + 0x0);
+ const uint16 mx = READ_LE_UINT16(entry + 0x8);
+ const uint16 my = READ_LE_UINT16(entry + 0xa);
+
+ Picture button;
+ if (!_buttonArchive.loadEntry(buttonId, button))
+ continue;
const int sx = (int)mx - scrollX + kMapWinX;
const int sy = (int)my - scrollY + kMapWinY;
- if (sx < kMapWinX || sx >= kMapWinX + kMapWinW ||
- sy < kMapWinY || sy >= kMapWinY + kMapWinH)
- continue;
-
- byte color;
- if (i < Mystery::kVisitedSiteCap && _mystery._visitedSite[i])
- color = 0x07;
- else if (crime != 0)
- color = 0x0C;
- else
- color = 0x0F;
- if (i == _mystery._siteNumber)
- color = 0x0E;
-
- const Common::Rect mark(sx - 3, sy - 3, sx + 4, sy + 4);
- scratch.fillRect(mark, color);
+ const byte transp = (byte)(button.flags >> 8);
+
+ // Crop blit against the viewport.
+ const int x0 = MAX<int>(sx, kMapWinX);
+ const int y0 = MAX<int>(sy, kMapWinY);
+ const int x1 = MIN<int>(sx + button.surface.w, kMapWinX + kMapWinW);
+ const int y1 = MIN<int>(sy + button.surface.h, kMapWinY + kMapWinH);
+ for (int row = y0; row < y1; row++) {
+ const byte *src = (const byte *)button.surface.getBasePtr(0, row - sy);
+ byte *dst = (byte *)scratch.getBasePtr(0, row);
+ for (int col = x0; col < x1; col++) {
+ const byte px = src[col - sx];
+ if (px != transp)
+ dst[col] = px;
+ }
+ }
}
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
@@ -1775,18 +1766,28 @@ void EEMEngine::doBigMap() {
ev.mouse.x < kMapWinX + kMapWinW &&
ev.mouse.y >= kMapWinY &&
ev.mouse.y < kMapWinY + kMapWinH) {
+ // Hit-test the per-site button at its actual bbox
+ // (`_StampButtons` records the rect at SmallMap +8/+0xa
+ // with the button PIC's width/height).
for (uint i = 0; i < _mystery.numSites(); i++) {
if (!_mystery._onSites[i] &&
i != _mystery._siteNumber)
continue;
const byte *entry = _mystery.mapEntry(i);
if (!entry) continue;
- const uint16 mx = READ_LE_UINT16(entry + 0x8);
- const uint16 my = READ_LE_UINT16(entry + 0xa);
+ const uint16 buttonId = READ_LE_UINT16(entry + 0x0);
+ const uint16 mx = READ_LE_UINT16(entry + 0x8);
+ const uint16 my = READ_LE_UINT16(entry + 0xa);
+ Picture button;
+ int bw = 16, bh = 16;
+ if (_buttonArchive.loadEntry(buttonId, button)) {
+ bw = button.surface.w;
+ bh = button.surface.h;
+ }
const int sx = (int)mx - scrollX + kMapWinX;
const int sy = (int)my - scrollY + kMapWinY;
- if (ABS(ev.mouse.x - sx) <= 6 &&
- ABS(ev.mouse.y - sy) <= 6) {
+ if (ev.mouse.x >= sx && ev.mouse.x < sx + bw &&
+ ev.mouse.y >= sy && ev.mouse.y < sy + bh) {
_mystery._lastSite = _mystery._siteNumber;
_mystery._siteNumber = (uint16)i;
return;
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 24bddf6f644..ec8e9d27b35 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -72,6 +72,7 @@ public:
DBDArchive &getAni() { return _aniArchive; }
DBDArchive &getSites() { return _sitesArchive; }
DBDArchive &getBalloons(){ return _balloonArchive; }
+ DBDArchive &getButtons() { return _buttonArchive; }
Mystery &getMystery() { return _mystery; }
const EEMFont &getFont() const { return _font; }
uint8 getPartnerIndex() const { return _partner; }
@@ -191,6 +192,7 @@ private:
DBDArchive _aniArchive; ///< ANI.DBD/.DBX (multi-frame character animations)
DBDArchive _sitesArchive; ///< SITES.DBD/.DBX (one full-screen scene per site)
DBDArchive _balloonArchive; ///< BALLOON.DBD/.DBX (speech-balloon sprites)
+ DBDArchive _buttonArchive; ///< BUTTON.DBD/.DBX (per-site labeled map buttons; `_GetButton`)
Mystery _mystery; ///< Currently-loaded case file (M<n>.BIN)
EEMFont _font; ///< FONT.FNT - main 8 px font
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 2ee29ea63ab..0a65bf95b68 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -432,14 +432,22 @@ void SiteScreen::renderBackground(uint siteNum) {
0, 0, frame.surface.w, frame.surface.h);
}
+ // `_BuildBackground @ 172b:13e2` calls
+ // `_GetFromDB(_siteFile, &_SiteDBIndex, sitenum)`
+ // with the value at SiteData[+0] passed straight through. The
+ // `_GetFromDB` callee uses that as a **0-based** index into
+ // SITES.DBD (verified at 172b:14c8: `MOV BX, [BP+0x6]; IMUL BX, BX, 0xa`
+ // â the dbi entry stride is 10 bytes, no -1 adjustment). Our
+ // previous `loadEntry(sitepic - 1)` was off by one, which is why
+ // the tutorial mystery rendered scenes from neighbouring cases.
const byte *site = _mystery->siteData(siteNum);
const uint16 sitepic = site ? READ_LE_UINT16(site) : 0;
Picture scene;
bool haveScene = false;
- if (sitepic > 0 && _vm->getSites().size() > sitepic - 1)
- haveScene = _vm->getSites().loadEntry(sitepic - 1, scene);
- if (!haveScene && sitepic > 0)
- haveScene = _vm->getPics().getPicture(sitepic, scene);
+ if (sitepic < _vm->getSites().size())
+ haveScene = _vm->getSites().loadEntry(sitepic, scene);
+ if (!haveScene)
+ haveScene = _vm->getPics().getPicture(sitepic + 1, scene);
if (haveScene) {
// Hard-coded composition position from `_BuildBackground`:
// `_Rect_Move(0, 0, h, ..., 0x42, 0x14, 48000, h, w)`.
Commit: 83ab9371832178fbc586a3cfef5918cc31f00f7a
https://github.com/scummvm/scummvm/commit/83ab9371832178fbc586a3cfef5918cc31f00f7a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:37+02:00
Commit Message:
EEM: show NPC on sites
Changed paths:
engines/eem/eem.cpp
engines/eem/site.cpp
engines/eem/site.h
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index e79f9723286..414180c0e75 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -1569,9 +1569,36 @@ void EEMEngine::doBigMap() {
(const byte *)frame.surface.getBasePtr(0, row), w);
}
- // Site icons at BigMap coords (+4, +6). Three colours by state:
- // visited â DoneMarker analogue, crime flag â CrimeMarker,
- // else â SiteMarker. Current site gets a bright highlight.
+ // Marker PICs from `_main @ 1a35:0f59`. Three globals are filled
+ // once at boot via `_GetPicture` (1-based IDs â entries N-1):
+ // _DoneMarker = PIC 0x20d (already-searched site)
+ // _SiteMarker = PIC 0xc5 (default available site)
+ // _CrimeMarker = PIC 0xc6 (crime-scene flag set)
+ // Picked per-site by `_DrawBigMapButtons @ 20fe:0877`:
+ // 1. SaveSiteComplete[i] â DoneMarker
+ // 2. else MapData[+0xc] != 0 â CrimeMarker
+ // 3. else SiteMarker
+ Picture done, normal, crimeM;
+ const bool haveDone = _picsArchive.getPicture(0x20d, done);
+ const bool haveNormal = _picsArchive.getPicture(0xc5, normal);
+ const bool haveCrime = _picsArchive.getPicture(0xc6, crimeM);
+
+ auto blitMarker = [&](const Picture &m, int x, int y) {
+ const byte transp = (byte)(m.flags >> 8);
+ for (int row = 0; row < m.surface.h; row++) {
+ const int dstY = y + row;
+ if (dstY < 0 || dstY >= 200) continue;
+ const byte *src = (const byte *)m.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, dstY);
+ for (int col = 0; col < m.surface.w; col++) {
+ const int dstX = x + col;
+ if (dstX < 0 || dstX >= 320) continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ };
+
for (uint i = 0; i < _mystery.numSites(); i++) {
if (!_mystery._onSites[i] && i != _mystery._siteNumber)
continue;
@@ -1581,19 +1608,21 @@ void EEMEngine::doBigMap() {
const uint16 mx = READ_LE_UINT16(entry + 0x4);
const uint16 my = READ_LE_UINT16(entry + 0x6);
const uint16 crime = READ_LE_UINT16(entry + 0xc);
-
- byte color;
- if (i < Mystery::kVisitedSiteCap && _mystery._visitedSite[i])
- color = 0x07;
- else if (crime != 0)
- color = 0x0C;
- else
- color = 0x0F;
- if (i == _mystery._siteNumber)
- color = 0x0E;
-
- const Common::Rect mark(mx - 3, my - 3, mx + 4, my + 4);
- scratch.fillRect(mark, color);
+ const bool done_ = (i < Mystery::kVisitedSiteCap)
+ && _mystery._visitedSite[i];
+
+ const Picture *m = nullptr;
+ if (done_ && haveDone) m = &done;
+ else if (crime != 0 && haveCrime) m = &crimeM;
+ else if (haveNormal) m = &normal;
+
+ if (m)
+ blitMarker(*m, (int)mx, (int)my);
+ else {
+ // Fallback if the markers couldn't be loaded.
+ const Common::Rect mark(mx - 3, my - 3, mx + 4, my + 4);
+ scratch.fillRect(mark, 0x0F);
+ }
}
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 0a65bf95b68..2250b7523ce 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -42,9 +42,14 @@ void SiteScreen::enter(uint siteNum) {
return;
}
+ // Capture whether this is the first time the player enters this
+ // site BEFORE we mark it visited â `_DoSiteLoop @ 168d:03f4`
+ // uses the same check to decide whether to play the arrival
+ // dialog: `if (_VisitedSite[_SiteNumber] == 0) _DisplayClue(...)`.
+ const bool firstVisit = (siteNum < Mystery::kVisitedSiteCap)
+ && (_mystery->_visitedSite[siteNum] == 0);
+
_mystery->_siteNumber = siteNum;
- if (siteNum < Mystery::kVisitedSiteCap)
- _mystery->_visitedSite[siteNum] = 1;
debugC(1, kDebugSite, "Entering site %u (%u hotspots)",
siteNum, _mystery->hotspotCount(siteNum));
@@ -68,6 +73,12 @@ void SiteScreen::enter(uint siteNum) {
renderBackground(siteNum);
}
+ // Per-site NPCs / decorative animations (`_DoSiteLoop` reads
+ // `siteData[+0xa]` as the drop count and iterates 6-byte entries
+ // from `siteData[+0x48]`). Drawn before the partner sprite so
+ // the partner appears on top of any background NPCs.
+ renderDrops(siteNum);
+
// Persistent partner sprite (`_NewAnimation` at the tail of
// `_DoSiteLoop`). Drawn after the BG so the hotspot outlines and
// HUD that follow stay on top of it.
@@ -75,6 +86,39 @@ void SiteScreen::enter(uint siteNum) {
renderHotspots(siteNum);
g_system->updateScreen();
+
+ // First-visit dialog. `_DoSiteLoop @ 168d:03f4` does:
+ // if (_VisitedSite[_SiteNumber] == 0) {
+ // _DisplayClue(_Mystery + SiteIndex[siteNum*6 + 2], 1);
+ // _VisitedSite[_SiteNumber] = 1;
+ // }
+ // `SiteIndex[+2..+3]` is the byte offset (within the mystery
+ // buffer) of a ClueBlock that holds the partner's arrival
+ // dialogue. We've kept the +2 field undocumented up to now â
+ // this confirms it's the entry-clue offset.
+ if (firstVisit) {
+ const byte *idx = _mystery->siteIndexEntry(siteNum);
+ if (idx) {
+ const uint16 clueOff = READ_LE_UINT16(idx + 2);
+ if (clueOff != 0xFFFF) {
+ const byte *clueBlock = _mystery->blobAt(clueOff);
+ if (clueBlock)
+ _vm->displayClue(clueBlock);
+ }
+ }
+ if (siteNum < Mystery::kVisitedSiteCap)
+ _mystery->_visitedSite[siteNum] = 1;
+ // The dialog overlay will have left the screen with portrait /
+ // balloon residues; refresh the site so the player returns to
+ // a clean state.
+ renderBackground(siteNum);
+ renderDrops(siteNum);
+ renderPartner(siteNum);
+ renderHotspots(siteNum);
+ g_system->updateScreen();
+ } else if (siteNum < Mystery::kVisitedSiteCap) {
+ _mystery->_visitedSite[siteNum] = 1;
+ }
}
void SiteScreen::run() {
@@ -346,6 +390,92 @@ void SiteScreen::enterSiteAnim() {
}
}
+void SiteScreen::renderDrops(uint siteNum) {
+ // `_DoSiteLoop @ 168d:03f4` runs TWO per-site loops, both feeding
+ // the visible NPCs / decorations on the site BG:
+ //
+ // Loop 1 (animations / animated NPCs)
+ // bound: siteData[+0xa]
+ // per entry at siteData[+0x48 + i*6]: {animId, x, y}
+ // if animId == -1: a `_ColorCycle(x, y)` palette range each tick
+ // else: `_GetAnimation(animId)` + `_NewAnimation(x, y, ...)` â
+ // added inactive (arg5=0), updated by `_UpdateAnimations`.
+ //
+ // Loop 2 (static drops)
+ // bound: siteData[+0x4] (verified at 168d:05c0:
+ // `MOV ES:[BX+0x4], DI; CMP ES:[BX+0x4], DI`)
+ // per entry at siteData[+0xc + i*6]: {picId, x, y}
+ // each â `_AddDrop(picId, x, y)` which loads PIC `picId-1`
+ // from PICS.DBD and blits it at (x, y) onto offscreen 32000.
+ //
+ // Both contribute to the visible scene. We render Loop 2 statics
+ // directly and Loop 1's first frame as a static sprite (per-tick
+ // animation cycling is still TODO).
+ if (!_mystery)
+ return;
+ const byte *site = _mystery->siteData(siteNum);
+ if (!site)
+ return;
+ const uint16 numStatic = READ_LE_UINT16(site + 0x4);
+ const uint16 numAnims = READ_LE_UINT16(site + 0xa);
+
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (!screen)
+ return;
+
+ auto blitMasked = [&](const Picture &p, int x, int y) {
+ const byte transp = (byte)(p.flags >> 8);
+ for (int row = 0; row < p.surface.h; row++) {
+ const int dstY = y + row;
+ if (dstY < 0 || dstY >= screen->h) continue;
+ const byte *src = (const byte *)p.surface.getBasePtr(0, row);
+ byte *dst = (byte *)screen->getBasePtr(0, dstY);
+ for (int col = 0; col < p.surface.w; col++) {
+ const int dstX = x + col;
+ if (dstX < 0 || dstX >= screen->w) continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ };
+
+ // Loop 2 â `_AddDrop(picId, x, y)`. PIC IDs are 1-based per
+ // `_AddDrop @ 172b:1a77` (it does `_GetFromDB(.., number - 1)`).
+ if (numStatic > 0 && numStatic <= 16) {
+ for (uint i = 0; i < numStatic; i++) {
+ const uint dropOff = 0xc + i * 6;
+ const uint16 picId = READ_LE_UINT16(site + dropOff + 0);
+ const int16 x = (int16)READ_LE_UINT16(site + dropOff + 2);
+ const int16 y = (int16)READ_LE_UINT16(site + dropOff + 4);
+ if (picId == 0)
+ continue;
+ Picture pic;
+ if (!_vm->getPics().getPicture(picId, pic))
+ continue;
+ blitMasked(pic, x, y);
+ }
+ }
+
+ // Loop 1 â animation drops. Use frame 0 as a static placeholder
+ // for non-`-1` entries.
+ if (numAnims > 0 && numAnims <= 16) {
+ for (uint i = 0; i < numAnims; i++) {
+ const uint dropOff = 0x48 + i * 6;
+ const int16 animId = (int16)READ_LE_UINT16(site + dropOff + 0);
+ if (animId < 0)
+ continue; // -1 â ColorCycle entry, handled per tick
+ const int16 x = (int16)READ_LE_UINT16(site + dropOff + 2);
+ const int16 y = (int16)READ_LE_UINT16(site + dropOff + 4);
+ Animation anim;
+ if (!_vm->getAni().loadAnimation((uint)animId, anim) || anim.empty())
+ continue;
+ blitMasked(anim[0], x, y);
+ }
+ }
+
+ g_system->unlockScreen();
+}
+
void SiteScreen::renderPartner(uint siteNum) {
// `_DoSiteLoop @ 168d:03f4` reads `siteData[+8]` as the speaker
// table index, then for each (speaker à partner) loads
diff --git a/engines/eem/site.h b/engines/eem/site.h
index 2346f5e5a77..125a6cd853a 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -77,6 +77,15 @@ private:
/// of `_DoSiteLoop @ 168d:03f4`.
void renderPartner(uint siteNum);
+ /// Draw the per-site NPC "drops" (the locals you click on to
+ /// trigger a clue). `_DoSiteLoop` reads `siteData[+0xa]` as the
+ /// drop count, then iterates 6-byte entries at `siteData[+0x48]`:
+ /// {anim_id (-1 = ColorCycle), x, y}.
+ /// Each non-(-1) entry is a `_NewAnimation(x, y, animId)`. We blit
+ /// the first frame as a static sprite; the original cycles via
+ /// `_UpdateAnimations`.
+ void renderDrops(uint siteNum);
+
EEMEngine *_vm;
Mystery *_mystery;
bool _showHotspots = true; ///< Toggle outlines with V key.
Commit: a5cefd687110f156ae779c6983b1bbf7c6b17010
https://github.com/scummvm/scummvm/commit/a5cefd687110f156ae779c6983b1bbf7c6b17010
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:37+02:00
Commit Message:
EEM: implement clickable areas on maps
Changed paths:
engines/eem/eem.cpp
engines/eem/site.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 414180c0e75..6143f54daf9 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -1632,6 +1632,11 @@ void EEMEngine::doBigMap() {
drawOverview();
+ // Static rectangles read directly from the binary at the labelled
+ // addresses (29be:0x1596 onwards). Format is {x1, y1, x2, y2}.
+ const Common::Rect kBigMapWindow ( 0, 0, 247, 192); // 29be:1596
+ const Common::Rect kSetupBtnRect (252, 4, 315, 42); // 29be:15ce
+
bool wantZoom = false;
int zoomX = 0, zoomY = 0;
while (!shouldQuit()) {
@@ -1644,19 +1649,27 @@ void EEMEngine::doBigMap() {
ev.kbd.keycode == Common::KEYCODE_ESCAPE)
return;
if (ev.type == Common::EVENT_LBUTTONDOWN) {
- // `_DoBigMap @ 20fe:09e7` does NOT travel directly when
- // the player clicks an overview icon â it always returns
- // `(sx, sy) = (mouseX*2 - 0x74, mouseY*2 - 0x55)` so the
- // caller can switch to the detail zoom centred there.
- // Travel happens after a SECOND click in the detail view.
- int sx = ev.mouse.x * 2;
- int sy = ev.mouse.y * 2;
- sx = (sx < 0x75) ? 0 : sx - 0x74;
- sy = (sy < 0x56) ? 0 : sy - 0x55;
- zoomX = sx;
- zoomY = sy;
- wantZoom = true;
- break;
+ // SetupButtonRect â `_NextScreen = 6` (the original's
+ // settings screen). We use it as "back to menu":
+ // abandon the current mystery and return to case
+ // selection.
+ if (kSetupBtnRect.contains(ev.mouse.x, ev.mouse.y)) {
+ _mystery.clear();
+ _nextScreen = kScreenInvalid;
+ return;
+ }
+ // Click in the BigMapWindow â zoom. Original formula:
+ // sx = mouseX*2 - 0x74; sy = mouseY*2 - 0x55
+ if (kBigMapWindow.contains(ev.mouse.x, ev.mouse.y)) {
+ int sx = ev.mouse.x * 2;
+ int sy = ev.mouse.y * 2;
+ sx = (sx < 0x75) ? 0 : sx - 0x74;
+ sy = (sy < 0x56) ? 0 : sy - 0x55;
+ zoomX = sx;
+ zoomY = sy;
+ wantZoom = true;
+ break;
+ }
}
}
if (wantZoom)
@@ -1791,10 +1804,67 @@ void EEMEngine::doBigMap() {
}
}
if (ev.type == Common::EVENT_LBUTTONDOWN) {
- if (ev.mouse.x >= kMapWinX &&
- ev.mouse.x < kMapWinX + kMapWinW &&
- ev.mouse.y >= kMapWinY &&
- ev.mouse.y < kMapWinY + kMapWinH) {
+ // Scroll arrows + slider rects live in `SmallMapButtons`
+ // at 29be:0x159e (six 8-byte rects in order: Y-up, Y-down,
+ // X-left, X-right, right-panel, top-right) plus the
+ // dedicated `XSliderRect @ 29be:15d6` and
+ // `YSliderRect @ 29be:15de`. Format {x1,y1,x2,y2}.
+ const Common::Rect kArrowYUp (237, 2, 247, 11);
+ const Common::Rect kArrowYDown (237, 163, 247, 172);
+ const Common::Rect kArrowXLeft ( 2, 175, 12, 185);
+ const Common::Rect kArrowXRight(224, 175, 234, 185);
+ const Common::Rect kXSlider ( 15, 175, 221, 185);
+ const Common::Rect kYSlider (237, 14, 247, 160);
+ const Common::Rect kSetupBtn (252, 4, 315, 42);
+
+ const int kArrowStep = 16;
+ const int kSliderRange = mapW - kMapWinW;
+ const int kSliderRangeY = mapH - kMapWinH;
+
+ if (kSetupBtn.contains(ev.mouse.x, ev.mouse.y)) {
+ // Setup button on detail too â `_NextScreen = 6` in
+ // the original. We treat it the same way: bail back
+ // to case selection.
+ _mystery.clear();
+ _nextScreen = kScreenInvalid;
+ return;
+ }
+ if (kArrowYUp.contains(ev.mouse.x, ev.mouse.y)) {
+ scrollY = MAX<int>(0, scrollY - kArrowStep);
+ dirty = true;
+ } else if (kArrowYDown.contains(ev.mouse.x, ev.mouse.y)) {
+ scrollY = MIN<int>(MAX<int>(0, kSliderRangeY),
+ scrollY + kArrowStep);
+ dirty = true;
+ } else if (kArrowXLeft.contains(ev.mouse.x, ev.mouse.y)) {
+ scrollX = MAX<int>(0, scrollX - kArrowStep);
+ dirty = true;
+ } else if (kArrowXRight.contains(ev.mouse.x, ev.mouse.y)) {
+ scrollX = MIN<int>(MAX<int>(0, kSliderRange),
+ scrollX + kArrowStep);
+ dirty = true;
+ } else if (kXSlider.contains(ev.mouse.x, ev.mouse.y)) {
+ // Click on X slider track â jump scrollX so the
+ // click position maps proportionally into the map.
+ if (kSliderRange > 0) {
+ const int t = ev.mouse.x - kXSlider.left;
+ const int tw = kXSlider.width();
+ scrollX = MAX<int>(0, MIN<int>(kSliderRange,
+ t * kSliderRange / MAX<int>(1, tw)));
+ dirty = true;
+ }
+ } else if (kYSlider.contains(ev.mouse.x, ev.mouse.y)) {
+ if (kSliderRangeY > 0) {
+ const int t = ev.mouse.y - kYSlider.top;
+ const int th = kYSlider.height();
+ scrollY = MAX<int>(0, MIN<int>(kSliderRangeY,
+ t * kSliderRangeY / MAX<int>(1, th)));
+ dirty = true;
+ }
+ } else if (ev.mouse.x >= kMapWinX &&
+ ev.mouse.x < kMapWinX + kMapWinW &&
+ ev.mouse.y >= kMapWinY &&
+ ev.mouse.y < kMapWinY + kMapWinH) {
// Hit-test the per-site button at its actual bbox
// (`_StampButtons` records the rect at SmallMap +8/+0xa
// with the button PIC's width/height).
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 2250b7523ce..49f7340ac2c 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -145,6 +145,31 @@ void SiteScreen::run() {
return;
case Common::EVENT_LBUTTONDOWN: {
+ // On-screen UI buttons. `_DoSiteLoop @ 168d:03f4` calls
+ // _FindButton(&SiteButtons, 2, MouseX, MouseY)
+ // where `SiteButtons` is two 8-byte rectangles at
+ // 29be:0x274 (verified via 168d:0729-0848):
+ // Button 0: (35, 111) - (56, 136) â notebook
+ // (`_NextScreen = 4`)
+ // Button 1: (7, 177) - (57, 200) â map
+ // (`_NextScreen = 1`)
+ // Test the buttons before falling through to hotspots so
+ // a click on the PDA / map icon doesn't accidentally
+ // trigger a hotspot underneath.
+ const Common::Rect kBtnNotebook(35, 111, 56, 136);
+ const Common::Rect kBtnMap ( 7, 177, 57, 200);
+ if (kBtnNotebook.contains(event.mouse.x, event.mouse.y)) {
+ _vm->doNotebook();
+ enter(cur);
+ break;
+ }
+ if (kBtnMap.contains(event.mouse.x, event.mouse.y)) {
+ _vm->doBigMap();
+ if (_mystery->_siteNumber < _mystery->numSites())
+ cur = _mystery->_siteNumber;
+ enter(cur);
+ break;
+ }
const int idx = hotspotAtPoint(cur, event.mouse.x, event.mouse.y);
if (idx >= 0) {
onHotspotClicked(cur, (uint)idx);
Commit: 0fe0c95dfdbdb01b029c42871173348e6b5ccf9f
https://github.com/scummvm/scummvm/commit/0fe0c95dfdbdb01b029c42871173348e6b5ccf9f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:37+02:00
Commit Message:
EEM: improved NPC handling
Changed paths:
engines/eem/site.cpp
engines/eem/site.h
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 49f7340ac2c..30f97bc29c0 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -73,16 +73,21 @@ void SiteScreen::enter(uint siteNum) {
renderBackground(siteNum);
}
- // Per-site NPCs / decorative animations (`_DoSiteLoop` reads
- // `siteData[+0xa]` as the drop count and iterates 6-byte entries
- // from `siteData[+0x48]`). Drawn before the partner sprite so
- // the partner appears on top of any background NPCs.
- renderDrops(siteNum);
-
- // Persistent partner sprite (`_NewAnimation` at the tail of
- // `_DoSiteLoop`). Drawn after the BG so the hotspot outlines and
- // HUD that follow stay on top of it.
- renderPartner(siteNum);
+ // Static drops (Loop 2 from `_DoSiteLoop`) â no animation, baked
+ // into the BG snapshot the run() pump uses to restore.
+ renderStaticDrops(siteNum);
+
+ // Snapshot the static layers so per-tick animation re-blits don't
+ // have to re-load PIC 0x43, the SITES.DBD scene, or each
+ // `_AddDrop` PIC every frame.
+ captureBgSnapshot();
+ _snapshotSite = (int)siteNum;
+
+ // Animated NPCs (Loop 1) and the persistent partner sit on top of
+ // the snapshot. Initial frame goes at tickMs=now.
+ const uint32 now = g_system->getMillis();
+ renderAnimatedDrops(siteNum, now);
+ renderPartner(siteNum, now);
renderHotspots(siteNum);
g_system->updateScreen();
@@ -110,10 +115,14 @@ void SiteScreen::enter(uint siteNum) {
_mystery->_visitedSite[siteNum] = 1;
// The dialog overlay will have left the screen with portrait /
// balloon residues; refresh the site so the player returns to
- // a clean state.
+ // a clean state. Re-build the snapshot too.
renderBackground(siteNum);
- renderDrops(siteNum);
- renderPartner(siteNum);
+ renderStaticDrops(siteNum);
+ captureBgSnapshot();
+ _snapshotSite = (int)siteNum;
+ const uint32 nowAfter = g_system->getMillis();
+ renderAnimatedDrops(siteNum, nowAfter);
+ renderPartner(siteNum, nowAfter);
renderHotspots(siteNum);
g_system->updateScreen();
} else if (siteNum < Mystery::kVisitedSiteCap) {
@@ -275,6 +284,21 @@ void SiteScreen::run() {
}
if (exitRequested)
return;
+
+ // Per-tick frame pump (mirrors `_CheckFrameRate` +
+ // `_UpdateAnimations` at the top of `_DoSiteLoop`'s main loop).
+ // Restore the static BG snapshot, redraw animated NPCs +
+ // partner at the current frame, then re-render hotspots on
+ // top. We tick at 100 ms (~10 FPS) which is in the same ball
+ // park as the original.
+ const uint32 now = g_system->getMillis();
+ if (_snapshotSite == (int)cur && now - _lastTickMs >= 100) {
+ restoreBgSnapshot();
+ renderAnimatedDrops(cur, now);
+ renderPartner(cur, now);
+ renderHotspots(cur);
+ _lastTickMs = now;
+ }
g_system->updateScreen();
g_system->delayMillis(10);
}
@@ -415,93 +439,131 @@ void SiteScreen::enterSiteAnim() {
}
}
-void SiteScreen::renderDrops(uint siteNum) {
- // `_DoSiteLoop @ 168d:03f4` runs TWO per-site loops, both feeding
- // the visible NPCs / decorations on the site BG:
- //
- // Loop 1 (animations / animated NPCs)
- // bound: siteData[+0xa]
- // per entry at siteData[+0x48 + i*6]: {animId, x, y}
- // if animId == -1: a `_ColorCycle(x, y)` palette range each tick
- // else: `_GetAnimation(animId)` + `_NewAnimation(x, y, ...)` â
- // added inactive (arg5=0), updated by `_UpdateAnimations`.
- //
- // Loop 2 (static drops)
- // bound: siteData[+0x4] (verified at 168d:05c0:
- // `MOV ES:[BX+0x4], DI; CMP ES:[BX+0x4], DI`)
- // per entry at siteData[+0xc + i*6]: {picId, x, y}
- // each â `_AddDrop(picId, x, y)` which loads PIC `picId-1`
- // from PICS.DBD and blits it at (x, y) onto offscreen 32000.
- //
- // Both contribute to the visible scene. We render Loop 2 statics
- // directly and Loop 1's first frame as a static sprite (per-tick
- // animation cycling is still TODO).
+// Mask-aware blit from a Picture into a Graphics::Surface.
+static void blitMaskedSurface(Graphics::Surface *screen,
+ const Picture &p, int x, int y) {
+ if (!screen)
+ return;
+ const byte transp = (byte)(p.flags >> 8);
+ for (int row = 0; row < p.surface.h; row++) {
+ const int dstY = y + row;
+ if (dstY < 0 || dstY >= screen->h) continue;
+ const byte *src = (const byte *)p.surface.getBasePtr(0, row);
+ byte *dst = (byte *)screen->getBasePtr(0, dstY);
+ for (int col = 0; col < p.surface.w; col++) {
+ const int dstX = x + col;
+ if (dstX < 0 || dstX >= screen->w) continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+}
+
+void SiteScreen::renderStaticDrops(uint siteNum) {
+ // Loop 2 from `_DoSiteLoop @ 168d:03f4`:
+ // bound: siteData[+0x4] (verified at 168d:05c0:
+ // `MOV ES:[BX+0x4], DI; CMP ES:[BX+0x4], DI`)
+ // per entry at siteData[+0xc + i*6]: {picId, x, y}
+ // each â `_AddDrop(picId, x, y)` (`_AddDrop @ 172b:1a77`):
+ // loads PIC `picId-1` from PICS.DBD and blits with miscflags
+ // high-byte as the transparent colour. These NEVER cycle, so
+ // they belong to the BG snapshot.
if (!_mystery)
return;
const byte *site = _mystery->siteData(siteNum);
if (!site)
return;
const uint16 numStatic = READ_LE_UINT16(site + 0x4);
- const uint16 numAnims = READ_LE_UINT16(site + 0xa);
+ if (numStatic == 0 || numStatic > 16)
+ return;
Graphics::Surface *screen = g_system->lockScreen();
if (!screen)
return;
- auto blitMasked = [&](const Picture &p, int x, int y) {
- const byte transp = (byte)(p.flags >> 8);
- for (int row = 0; row < p.surface.h; row++) {
- const int dstY = y + row;
- if (dstY < 0 || dstY >= screen->h) continue;
- const byte *src = (const byte *)p.surface.getBasePtr(0, row);
- byte *dst = (byte *)screen->getBasePtr(0, dstY);
- for (int col = 0; col < p.surface.w; col++) {
- const int dstX = x + col;
- if (dstX < 0 || dstX >= screen->w) continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
- };
-
- // Loop 2 â `_AddDrop(picId, x, y)`. PIC IDs are 1-based per
- // `_AddDrop @ 172b:1a77` (it does `_GetFromDB(.., number - 1)`).
- if (numStatic > 0 && numStatic <= 16) {
- for (uint i = 0; i < numStatic; i++) {
- const uint dropOff = 0xc + i * 6;
- const uint16 picId = READ_LE_UINT16(site + dropOff + 0);
- const int16 x = (int16)READ_LE_UINT16(site + dropOff + 2);
- const int16 y = (int16)READ_LE_UINT16(site + dropOff + 4);
- if (picId == 0)
- continue;
- Picture pic;
- if (!_vm->getPics().getPicture(picId, pic))
- continue;
- blitMasked(pic, x, y);
- }
+ for (uint i = 0; i < numStatic; i++) {
+ const uint dropOff = 0xc + i * 6;
+ const uint16 picId = READ_LE_UINT16(site + dropOff + 0);
+ const int16 x = (int16)READ_LE_UINT16(site + dropOff + 2);
+ const int16 y = (int16)READ_LE_UINT16(site + dropOff + 4);
+ if (picId == 0)
+ continue;
+ Picture pic;
+ if (!_vm->getPics().getPicture(picId, pic))
+ continue;
+ blitMaskedSurface(screen, pic, x, y);
}
- // Loop 1 â animation drops. Use frame 0 as a static placeholder
- // for non-`-1` entries.
- if (numAnims > 0 && numAnims <= 16) {
- for (uint i = 0; i < numAnims; i++) {
- const uint dropOff = 0x48 + i * 6;
- const int16 animId = (int16)READ_LE_UINT16(site + dropOff + 0);
- if (animId < 0)
- continue; // -1 â ColorCycle entry, handled per tick
- const int16 x = (int16)READ_LE_UINT16(site + dropOff + 2);
- const int16 y = (int16)READ_LE_UINT16(site + dropOff + 4);
- Animation anim;
- if (!_vm->getAni().loadAnimation((uint)animId, anim) || anim.empty())
- continue;
- blitMasked(anim[0], x, y);
- }
+ g_system->unlockScreen();
+}
+
+void SiteScreen::renderAnimatedDrops(uint siteNum, uint32 tickMs) {
+ // Loop 1 from `_DoSiteLoop @ 168d:03f4`:
+ // bound: siteData[+0xa]
+ // per entry at siteData[+0x48 + i*6]: {animId, x, y}
+ // animId == -1 â `_ColorCycle(x, y)` palette range (handled
+ // in the run() loop's frame pump as palette
+ // rotation; not yet implemented).
+ // else â `_GetAnimation(animId)` + `_NewAnimation` then
+ // `_UpdateAnimations @ 172b:09c1` walks a sequence
+ // script (entries are frame indices; 0x80 = end-of-loop,
+ // 0x81 = jump command). We don't have the sequence-script
+ // structure decoded yet, so for now we cycle through the
+ // raw animation frames in order using a global tick.
+ if (!_mystery)
+ return;
+ const byte *site = _mystery->siteData(siteNum);
+ if (!site)
+ return;
+ const uint16 numAnims = READ_LE_UINT16(site + 0xa);
+ if (numAnims == 0 || numAnims > 16)
+ return;
+
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (!screen)
+ return;
+
+ const uint32 kFramePeriodMs = 100; // ~10 FPS, in line with `_CheckFrameRate`.
+
+ for (uint i = 0; i < numAnims; i++) {
+ const uint dropOff = 0x48 + i * 6;
+ const int16 animId = (int16)READ_LE_UINT16(site + dropOff + 0);
+ if (animId < 0)
+ continue;
+ const int16 x = (int16)READ_LE_UINT16(site + dropOff + 2);
+ const int16 y = (int16)READ_LE_UINT16(site + dropOff + 4);
+ Animation anim;
+ if (!_vm->getAni().loadAnimation((uint)animId, anim) || anim.empty())
+ continue;
+ const uint frameIdx = (uint)((tickMs / kFramePeriodMs) % anim.size());
+ blitMaskedSurface(screen, anim[frameIdx], x, y);
}
g_system->unlockScreen();
}
-void SiteScreen::renderPartner(uint siteNum) {
+void SiteScreen::captureBgSnapshot() {
+ _bgSnapshot.create(320, 200, Graphics::PixelFormat::createFormatCLUT8());
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (!screen) {
+ _snapshotSite = -1;
+ return;
+ }
+ for (int row = 0; row < 200; row++) {
+ memcpy((byte *)_bgSnapshot.getBasePtr(0, row),
+ (const byte *)screen->getBasePtr(0, row), 320);
+ }
+ g_system->unlockScreen();
+}
+
+void SiteScreen::restoreBgSnapshot() {
+ if (_bgSnapshot.w != 320 || _bgSnapshot.h != 200)
+ return;
+ g_system->copyRectToScreen(_bgSnapshot.getPixels(), _bgSnapshot.pitch,
+ 0, 0, 320, 200);
+}
+
+void SiteScreen::renderPartner(uint siteNum, uint32 tickMs) {
// `_DoSiteLoop @ 168d:03f4` reads `siteData[+8]` as the speaker
// table index, then for each (speaker à partner) loads
// anim = WaitAnims[speakerIdx].anim[partner]
@@ -543,28 +605,16 @@ void SiteScreen::renderPartner(uint siteNum) {
if (!_vm->getAni().loadAnimation(animId, anim) || anim.empty())
return;
- // Show the first frame as a static sprite. The original updates it
- // each `_CheckFrameRate` tick; we don't have a frame pump in the
- // site loop yet so a static pose is enough for now.
- const Picture &fr = anim[0];
- const byte transp = (byte)(fr.flags >> 8);
+ // `_UpdateAnimations @ 172b:09c1` advances the partner's frame
+ // every `_CheckFrameRate` tick. We pick the frame from a global
+ // 100 ms clock so the partner cycles in sync with the animated
+ // drops.
+ const uint32 kFramePeriodMs = 100;
+ const uint frameIdx = (uint)((tickMs / kFramePeriodMs) % anim.size());
Graphics::Surface *screen = g_system->lockScreen();
if (!screen)
return;
- for (int row = 0; row < fr.surface.h; row++) {
- const int dstY = y + row;
- if (dstY < 0 || dstY >= 200)
- continue;
- const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
- byte *dst = (byte *)screen->getBasePtr(0, dstY);
- for (int col = 0; col < fr.surface.w; col++) {
- const int dstX = x + col;
- if (dstX < 0 || dstX >= 320)
- continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
+ blitMaskedSurface(screen, anim[frameIdx], x, y);
g_system->unlockScreen();
}
diff --git a/engines/eem/site.h b/engines/eem/site.h
index 125a6cd853a..bae8dc53cc6 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -25,6 +25,8 @@
#include "common/rect.h"
#include "common/scummsys.h"
+#include "graphics/managed_surface.h"
+
namespace EEM {
class EEMEngine;
@@ -74,22 +76,39 @@ private:
/// Draw the persistent in-site partner sprite (Jake or Jenny
/// standing/idling) at the position from `_WaitAnims` @ 29be:021c.
/// Mirrors the `_GetAnimation` + `_NewAnimation` block at the tail
- /// of `_DoSiteLoop @ 168d:03f4`.
- void renderPartner(uint siteNum);
-
- /// Draw the per-site NPC "drops" (the locals you click on to
- /// trigger a clue). `_DoSiteLoop` reads `siteData[+0xa]` as the
- /// drop count, then iterates 6-byte entries at `siteData[+0x48]`:
- /// {anim_id (-1 = ColorCycle), x, y}.
- /// Each non-(-1) entry is a `_NewAnimation(x, y, animId)`. We blit
- /// the first frame as a static sprite; the original cycles via
- /// `_UpdateAnimations`.
- void renderDrops(uint siteNum);
+ /// of `_DoSiteLoop @ 168d:03f4`. `tickMs` selects which frame of
+ /// the partner's animation to render; in the original, frames
+ /// advance per `_CheckFrameRate` tick via `_UpdateAnimations`.
+ void renderPartner(uint siteNum, uint32 tickMs);
+
+ /// Draw the per-site `_AddDrop` static decorations (Loop 2).
+ /// `_DoSiteLoop` runs this loop with bound siteData[+0x4] and
+ /// 6-byte entries at siteData[+0xc]: {picId, x, y}. These never
+ /// animate so they go in the BG snapshot.
+ void renderStaticDrops(uint siteNum);
+
+ /// Draw the per-site animated NPCs (Loop 1) at the current tick.
+ /// `_DoSiteLoop` registers each via `_NewAnimation` (siteData[+0xa]
+ /// entries at siteData[+0x48]: {animId (-1 = ColorCycle), x, y})
+ /// and `_UpdateAnimations @ 172b:09c1` advances frame indices each
+ /// tick. We use a millis-based frame index so all NPCs cycle in
+ /// step with the global clock.
+ void renderAnimatedDrops(uint siteNum, uint32 tickMs);
+
+ /// Snapshot the post-BG, post-static-drops screen so the per-tick
+ /// frame pump can restore it without reloading PICS.DBD entries.
+ void captureBgSnapshot();
+
+ /// Restore the snapshot taken at `captureBgSnapshot` time.
+ void restoreBgSnapshot();
EEMEngine *_vm;
Mystery *_mystery;
- bool _showHotspots = true; ///< Toggle outlines with V key.
- int _lastSiteAnim = -1; ///< Last site we played the arrival on.
+ bool _showHotspots = true; ///< Toggle outlines with V key.
+ int _lastSiteAnim = -1; ///< Last site we played the arrival on.
+ int _snapshotSite = -1; ///< Site number the snapshot belongs to.
+ Graphics::ManagedSurface _bgSnapshot;
+ uint32 _lastTickMs = 0; ///< Last frame-pump tick in ms.
};
} // End of namespace EEM
Commit: ca825ec5b1b8e203da3defda1275ba22bd0e07c3
https://github.com/scummvm/scummvm/commit/ca825ec5b1b8e203da3defda1275ba22bd0e07c3
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:38+02:00
Commit Message:
EEM: added more features to the PDA
Changed paths:
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/site.cpp
engines/eem/site.h
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 6143f54daf9..578f3290a6f 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -987,10 +987,23 @@ void EEMEngine::doCaseSelection() {
}
void EEMEngine::doInitClues() {
- // Mirrors `_DoInitClues` @ 1a35:0411. Sets BG 0x52 + palette 0x22,
- // blits the Goblindroid game and book first frames, then displays
- // the case briefing ClueBlock at InitBlock + 4. Marks the starting
- // site (InitBlock word[1]) on `_OnSites`.
+ // Mirrors `_DoInitClues` @ 1a35:0411. The original does:
+ // 1. _AllBlack(); _GetBackground(0x52); _GetPalette(0x22);
+ // 2. _GetAnimation(gameAni); _NewAnimation(0xcd, 0x6c, ...)
+ // _GetAnimation(bookAni); _NewAnimation(0, 99, ...)
+ // (case type 1 also: _NewAnimation(0x68, 0x8b, nancyAni))
+ // 3. _UpdateAnimations(); _FadeIn();
+ // 4. while (frame != gameNum) { _CheckFrameRate(); _UpdateAnimations(); }
+ // â cycles through the entire game animation once. Click skips.
+ // 5. _PlayInSequence(seqId, ...) â plays a follow-up sequence based
+ // on partner + case type.
+ // 6. _DisplayClue(InitBlock + 2, 1) â the briefing dialogue.
+ // 7. _OnSites[startSite] = 1.
+ //
+ // gameAni / bookAni / nancyAni values verified directly from Ghidra:
+ // gameAni = 0x17 (Jake) / 0x3b (Jenny)
+ // bookAni = 0x18 (Jake) / 0x3c (Jenny)
+ // nancyAni = 0x19 (case type 1 only)
if (!_mystery.isLoaded())
return;
@@ -1001,11 +1014,6 @@ void EEMEngine::doInitClues() {
const uint16 startSite = READ_LE_UINT16(ib + 2);
if (startSite < Mystery::kVisitedSiteCap)
_mystery._onSites[startSite] = 1;
- // Mirror the original: at briefing time the player isn't actually
- // at any site yet â they pick from the map next. Set _siteNumber
- // to the start site so the map opens centred on the only initially
- // accessible location and the post-map site loop has a sensible
- // resume point.
_mystery._siteNumber = startSite;
_mystery._lastSite = startSite;
@@ -1016,21 +1024,82 @@ void EEMEngine::doInitClues() {
const uint gameAni = _partner == 0 ? 0x17 : 0x3b;
const uint bookAni = _partner == 0 ? 0x18 : 0x3c;
- Animation game, book;
- if (_aniArchive.loadAnimation(gameAni, game) && !game.empty())
- blitAt(game[0], 0xcd, 0x6c);
- if (_aniArchive.loadAnimation(bookAni, book) && !book.empty())
- blitAt(book[0], 0, 99);
-
- // Case type 1 also places "Nancy" (a third character) at (0x68, 0x8b)
- // per `_DoInitClues`.
+ Animation game, book, nancy;
+ const bool haveGame = _aniArchive.loadAnimation(gameAni, game) && !game.empty();
+ const bool haveBook = _aniArchive.loadAnimation(bookAni, book) && !book.empty();
+
const uint16 caseType = READ_LE_UINT16(ib);
- if (caseType == 1) {
- Animation nancy;
- if (_aniArchive.loadAnimation(0x19, nancy) && !nancy.empty())
- blitAt(nancy[0], 0x68, 0x8b);
+ const bool haveNancy = (caseType == 1)
+ && _aniArchive.loadAnimation(0x19, nancy)
+ && !nancy.empty();
+
+ auto blitMaskedAt = [&](const Picture &p, int x, int y) {
+ const byte transp = (byte)(p.flags >> 8);
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (!screen) return;
+ for (int row = 0; row < p.surface.h; row++) {
+ const int dstY = y + row;
+ if (dstY < 0 || dstY >= screen->h) continue;
+ const byte *src = (const byte *)p.surface.getBasePtr(0, row);
+ byte *dst = (byte *)screen->getBasePtr(0, dstY);
+ for (int col = 0; col < p.surface.w; col++) {
+ const int dstX = x + col;
+ if (dstX < 0 || dstX >= screen->w) continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ g_system->unlockScreen();
+ };
+
+ // Step 4 â cycle through the game animation once before the briefing.
+ // Mirrors the `while (uVar9 != gameNum)` loop. The original calls
+ // `_UpdateAnimations` per `_CheckFrameRate` tick (~10 fps). We use
+ // 100 ms ticks for the same cadence. Click / key skips.
+ if (haveGame || haveBook || haveNancy) {
+ const uint frameCount = haveGame ? game.size() : 8;
+ bool skip = false;
+ for (uint frame = 0; frame < frameCount && !shouldQuit() && !skip; frame++) {
+ // Restore BG + advance frame.
+ if (_picsArchive.getPicture(0x52, bg))
+ blitAt(bg, 0, 0);
+ if (haveGame)
+ blitMaskedAt(game[frame % game.size()], 0xcd, 0x6c);
+ if (haveBook)
+ blitMaskedAt(book[frame % book.size()], 0, 99);
+ if (haveNancy)
+ blitMaskedAt(nancy[frame % nancy.size()], 0x68, 0x8b);
+ g_system->updateScreen();
+
+ // Wait 100 ms or until input.
+ const uint32 wakeup = g_system->getMillis() + 100;
+ while (g_system->getMillis() < wakeup && !shouldQuit() && !skip) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_LBUTTONDOWN ||
+ ev.type == Common::EVENT_KEYDOWN) {
+ skip = true;
+ break;
+ }
+ }
+ g_system->delayMillis(10);
+ }
+ }
}
+ // Composite the final frames (or first frames if skipped) so the BG
+ // is in a sensible state when displayClue overlays the speaker.
+ if (_picsArchive.getPicture(0x52, bg))
+ blitAt(bg, 0, 0);
+ if (haveGame)
+ blitMaskedAt(game[0], 0xcd, 0x6c);
+ if (haveBook)
+ blitMaskedAt(book[0], 0, 99);
+ if (haveNancy)
+ blitMaskedAt(nancy[0], 0x68, 0x8b);
+ g_system->updateScreen();
+
+ // Step 6 â case briefing dialogue.
displayClue(ib + 4);
}
@@ -1269,14 +1338,39 @@ void EEMEngine::displayClue(const byte *clueBlock) {
}
}
}
- // Per-balloon metadata table at 29be:0875 â 10-byte
- // entries indexed by `(bubNum & 0x7f)`. Layout:
- // +0..1 textX inset, +2..3 textY inset, +4..5 textWidth.
- // All entries use textX=6, textY=4 so we hard-code those
- // constants; textWidth is read live from the table.
- textX = bubX + 6;
- textY = bubY + 4;
- textW = bw - 12;
+ // Per-balloon metadata table verified from 29be:0875 â
+ // 10-byte entries indexed by `(bubNum & 0x7f)`. Layout:
+ // +0..1 textX inset, +2..3 textY inset, +4..5 width,
+ // +6..7 height, +8..9 tail offset.
+ // 52 entries total; insets vary (3, 5, 6, or 8 px).
+ // The original `_DisplayClue` does:
+ // _WordWrap(bubX + table[bubNum].x, bubY + table[bubNum].y,
+ // table[bubNum].w, ...);
+ static const struct { uint16 x, y, w; } kBalloonTable[] = {
+ { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 },
+ { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 },
+ { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 },
+ { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 },
+ { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 },
+ { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 },
+ { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 },
+ { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 },
+ { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 },
+ { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 },
+ { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 },
+ { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 },
+ { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 },
+ { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 },
+ { 5, 8, 158 }, { 5, 8, 176 }, { 8, 7, 142 }
+ };
+ const uint kBalloonTableSize = sizeof(kBalloonTable) /
+ sizeof(kBalloonTable[0]);
+ const uint balloonIdx = balloonId < kBalloonTableSize
+ ? balloonId : 0;
+ const auto &bm = kBalloonTable[balloonIdx];
+ textX = bubX + bm.x;
+ textY = bubY + bm.y;
+ textW = bm.w;
copyH = bh;
} else {
// No balloon â clear a band so old pixels don't bleed.
@@ -1344,109 +1438,340 @@ void EEMEngine::displayClue(const byte *clueBlock) {
}
void EEMEngine::doNotebook() {
- // Mirrors `_DrawNotes` @ 161e:01d0 + `_HandleNoteButton`. We list every
- // found clue with its NoteIndex point value and let the player toggle
- // "selected" with number keys 1..9 (paged in groups of 9). The total
- // points of selected clues feed `_SolvedCheck` during accuse.
- if (!_font.isLoaded())
+ // Mirrors `_DoNotebook @ 161e:0500` + `_DrawNotes @ 161e:01d0` +
+ // `_HandleNoteButton @ 161e:03cb`.
+ //
+ // Layout (verified from Ghidra labels in 29be:013f / 29be:0147):
+ // _NotebookRect = (78, 12, 288, 152) â note display rectangle.
+ // _NoteButtons (11 entries, 8 bytes each, at 29be:0147):
+ // [0] (134, 174, 155, 190) decorative â `_HandleNoteButton(0)`
+ // returns immediately (i-1 unsigned > 9).
+ // [1] (93, 174, 115, 190) â `_InterfaceHelp(0)` (handler 0x3f9)
+ // [2] (157, 174, 178, 190) â handler 0x477 (page nav)
+ // [3] (5, 80, 44, 110) â `_KDHelp` (host hint, 0x403)
+ // [4] (180, 174, 201, 190) â solve / accuse (0x436)
+ // [5] (204, 174, 224, 190) â `_NextScreen = 5` (gallery, 0x489)
+ // [6] (226, 174, 247, 190) â handler 0x4ab
+ // [7] (7, 177, 57, 200) â handler 0x480 (back to map)
+ // [8] (35, 111, 56, 136) â `_NextScreen = 3` (site)
+ // [9] (0, 0, 0, 0) â same exit as [8]
+ // [10] (66, 79, 267, 174) â `_InterfaceHelp(0)` (note area)
+ // Background: PIC 0x3f.
+ // Partner anim: anim 1 (Jake) / 0xb (Jenny) at (5, 80).
+ if (!_mystery.isLoaded() || !_font.isLoaded())
return;
+ const Common::Rect kNotebookRect(78, 12, 288, 152);
+ const Common::Rect kBtnHelp1 ( 93, 174, 115, 190); // [1]
+ const Common::Rect kBtnPagePrev(157, 174, 178, 190); // [2]
+ const Common::Rect kBtnPartner ( 5, 80, 44, 110); // [3]
+ const Common::Rect kBtnAccuse (180, 174, 201, 190); // [4]
+ const Common::Rect kBtnGallery (204, 174, 224, 190); // [5]
+ const Common::Rect kBtnPageNext(226, 174, 247, 190); // [6]
+ const Common::Rect kBtnMap ( 7, 177, 57, 200); // [7]
+ const Common::Rect kBtnSite ( 35, 111, 56, 136); // [8]
+ const Common::Rect kNoteArea ( 66, 79, 267, 174); // [10]
+ (void)kBtnHelp1; (void)kBtnPagePrev; (void)kBtnPageNext;
+
+ CursorMan.showMouse(true);
+
int page = 0;
- const int kPerPage = 9;
+ int hoveredNoteSlot = -1;
+
+ // Build a list of found-clue indices, identical ordering to the
+ // original's iteration through `_CluesFound[]`.
+ auto buildFound = [&]() {
+ Common::Array<uint> found;
+ for (uint i = 0; i < Mystery::kCluesFoundCap; i++)
+ if (_mystery._cluesFound[i])
+ found.push_back(i);
+ return found;
+ };
auto draw = [&]() {
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
- _font.drawString(&scratch, "NOTEBOOK", 8, 4, 320, 0xF);
- _font.drawString(&scratch, Common::String::format("pts: %d", _mystery.selectedPoints()), 200, 4, 320, 0xF);
- // Build a list of found-clue indices.
- Common::Array<uint> found;
- for (uint i = 0; i < Mystery::kCluesFoundCap; i++)
- if (_mystery._cluesFound[i])
- found.push_back(i);
- const int total = (int)found.size();
- const int pages = MAX<int>(1, (total + kPerPage - 1) / kPerPage);
- page = MIN<int>(page, pages - 1);
+ // PIC 0x3f frame.
+ Picture frame;
+ if (_picsArchive.getPicture(0x3f, frame)) {
+ const int w = MIN<int>(frame.surface.w, 320);
+ const int h = MIN<int>(frame.surface.h, 200);
+ for (int row = 0; row < h; row++)
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)frame.surface.getBasePtr(0, row), w);
+ }
- _font.drawString(&scratch, Common::String::format("page %d/%d", page + 1, pages), 200, 16, 320, 0xF);
+ // Partner sprite at (5, 80). Anim 1 for Jake, 0xb (11) for Jenny.
+ const uint partnerAnim = (_partner == 0) ? 1 : 0xb;
+ Animation partnerAni;
+ if (_aniArchive.loadAnimation(partnerAnim, partnerAni) && !partnerAni.empty()) {
+ const uint32 now = g_system->getMillis();
+ const uint frameIdx = (uint)((now / 100) % partnerAni.size());
+ const Picture &fr = partnerAni[frameIdx];
+ const byte transp = (byte)(fr.flags >> 8);
+ for (int row = 0; row < fr.surface.h; row++) {
+ const int dstY = 80 + row;
+ if (dstY < 0 || dstY >= 200) continue;
+ const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, dstY);
+ for (int col = 0; col < fr.surface.w; col++) {
+ const int dstX = 5 + col;
+ if (dstX < 0 || dstX >= 320) continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ }
+ // Notes â `_DrawNotes` walks `_NoteIndex` for the current page,
+ // rendering each found clue's text inside `_NotebookRect` with
+ // word-wrap. Selected clues are highlighted (color 0x3c in the
+ // original's case-briefing palette).
+ const Common::Array<uint> found = buildFound();
const byte *ni = _mystery.noteIndex();
const uint16 niCount = _mystery.noteIndexCount();
- int y = 4 + _font.getFontHeight() * 2 + 4;
- for (int slot = 0; slot < kPerPage; slot++) {
- const int idx = page * kPerPage + slot;
- if (idx >= total)
- break;
- const uint clueId = found[idx];
- Common::String text;
- int pts = 0;
+
+ const int kRectX = kNotebookRect.left;
+ const int kRectY = kNotebookRect.top;
+ const int kRectW = kNotebookRect.width();
+ const int kRectH = kNotebookRect.height();
+
+ // Walk forward to the start clue of the current page.
+ // Each page renders as many clues as fit in `kRectH`.
+ int clueCursor = 0;
+ Common::Array<int> pageStarts;
+ pageStarts.push_back(0);
+ {
+ const int lineH = _font.getFontHeight() + 1;
+ int y = kRectY;
+ while (clueCursor < (int)found.size()) {
+ const uint clueId = found[clueCursor];
+ Common::String txt;
+ if (ni && clueId < niCount) {
+ const uint16 textOff = READ_LE_UINT16(ni + clueId * 4);
+ txt = parseString(_mystery.textAt(textOff),
+ _playerName, _partner);
+ }
+ // Measure height by wrapping the text without drawing.
+ Common::Array<Common::String> wrapped;
+ _font.wordWrapText(txt, kRectW, wrapped);
+ const int h = (int)wrapped.size() * lineH;
+ if (y + h + 7 > kRectY + kRectH) {
+ // Page break before this clue.
+ y = kRectY;
+ pageStarts.push_back(clueCursor);
+ }
+ y += h + 7;
+ clueCursor++;
+ }
+ if (page >= (int)pageStarts.size())
+ page = (int)pageStarts.size() - 1;
+ if (page < 0)
+ page = 0;
+ }
+
+ // Track per-slot rectangles so the click handler can map a
+ // click in `kNoteArea` back to a clue index.
+ Common::Array<Common::Rect> slotRects;
+ Common::Array<uint> slotClues;
+
+ const int startClue = (page < (int)pageStarts.size())
+ ? pageStarts[page] : 0;
+ const int endClue = (page + 1 < (int)pageStarts.size())
+ ? pageStarts[page + 1] : (int)found.size();
+
+ int y = kRectY;
+ for (int i = startClue; i < endClue; i++) {
+ const uint clueId = found[i];
+ Common::String txt;
if (ni && clueId < niCount) {
const uint16 textOff = READ_LE_UINT16(ni + clueId * 4);
- const uint16 ptsRaw = READ_LE_UINT16(ni + clueId * 4 + 2);
- pts = (int)(int16)ptsRaw;
- const Common::String raw = _mystery.textAt(textOff);
- text = parseString(raw, _playerName, _partner);
- }
- if (text.empty())
- text = Common::String::format("clue %u", clueId);
-
- const char selMark = _mystery._noteSelected[clueId] ? '*' : ' ';
- Common::String line = Common::String::format(
- "%d [%c] (%d pts) %s", slot + 1, selMark, pts, text.c_str());
- const int used = _font.drawWordWrapped(&scratch, 8, y, 304, line, 0xF);
- y += used + 2;
- if (y >= 192) break;
+ txt = parseString(_mystery.textAt(textOff),
+ _playerName, _partner);
+ }
+ if (txt.empty())
+ txt = Common::String::format("clue %u", clueId);
+ // Compute wrapped lines first to know the rect.
+ Common::Array<Common::String> wrapped;
+ _font.wordWrapText(txt, kRectW, wrapped);
+ const int lineH = _font.getFontHeight() + 1;
+ const int h = (int)wrapped.size() * lineH;
+
+ // Per `_DrawNotes @ 161e:01d0`: text uses
+ // `_NoteUnselectedColor` (0x5c=cyan) for unselected and 0x3c
+ // (light yellow-white) for selected. Paint a dark "paper"
+ // rectangle behind the text first â without this, the
+ // notebook BG (PIC 0x3f) bleeds through and makes the cyan
+ // text hard to read on lighter pixel runs. The fill colour
+ // 0x20 maps to a dark navy across all site palettes.
+ scratch.fillRect(Common::Rect(kRectX - 2, y - 1,
+ kRectX + kRectW + 2, y + h + 1), 0x20);
+
+ const byte color = _mystery._noteSelected[clueId] ? 0x3C : 0x5C;
+ for (uint li = 0; li < wrapped.size(); li++) {
+ _font.drawString(&scratch, wrapped[li], kRectX,
+ y + (int)li * lineH, kRectW, color);
+ }
+ slotRects.push_back(Common::Rect(kRectX, y,
+ kRectX + kRectW, y + h));
+ slotClues.push_back(clueId);
+ y += h + 7;
}
+
+ // Page indicator + selected-points counter. Paint a small dark
+ // strip behind them too so the text reads on the PDA frame's
+ // upper-right corner.
+ scratch.fillRect(Common::Rect(266, 0, 320, 24), 0x20);
+ _font.drawString(&scratch, Common::String::format("p%d/%d",
+ page + 1, (int)pageStarts.size()),
+ 270, 4, 320, 0x5C);
+ _font.drawString(&scratch, Common::String::format("%d pts",
+ _mystery.selectedPoints()),
+ 270, 14, 320, 0x5C);
+ (void)hoveredNoteSlot;
+
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
g_system->updateScreen();
+
+ // Stash slot info on the captures so the click handler below
+ // can use it via the closure.
+ _notebookSlotRects = slotRects;
+ _notebookSlotClues = slotClues;
};
draw();
+
+ uint32 lastDraw = g_system->getMillis();
+
while (!shouldQuit()) {
Common::Event ev;
bool dirty = false;
- bool exit = false;
+ bool exitFlag = false;
while (g_system->getEventManager()->pollEvent(ev)) {
if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER) { exit = true; break; }
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ exitFlag = true;
+ break;
+ }
if (ev.type == Common::EVENT_KEYDOWN) {
- if (ev.kbd.keycode == Common::KEYCODE_ESCAPE) { exit = true; break; }
- if (ev.kbd.keycode >= Common::KEYCODE_1 && ev.kbd.keycode <= Common::KEYCODE_9) {
- const int slot = (int)(ev.kbd.keycode - Common::KEYCODE_1);
- Common::Array<uint> found;
- for (uint i = 0; i < Mystery::kCluesFoundCap; i++)
- if (_mystery._cluesFound[i])
- found.push_back(i);
- const int idx = page * kPerPage + slot;
- if (idx < (int)found.size()) {
- const uint clueId = found[idx];
- _mystery._noteSelected[clueId] ^= 1;
- dirty = true;
- }
- } else if (ev.kbd.keycode == Common::KEYCODE_TAB ||
- ev.kbd.keycode == Common::KEYCODE_RIGHT) {
- page++; dirty = true;
- } else if (ev.kbd.keycode == Common::KEYCODE_LEFT) {
+ if (ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ exitFlag = true;
+ break;
+ }
+ if (ev.kbd.keycode == Common::KEYCODE_LEFT ||
+ ev.kbd.keycode == Common::KEYCODE_PAGEUP) {
if (page > 0) page--;
dirty = true;
+ } else if (ev.kbd.keycode == Common::KEYCODE_RIGHT ||
+ ev.kbd.keycode == Common::KEYCODE_PAGEDOWN ||
+ ev.kbd.keycode == Common::KEYCODE_TAB) {
+ page++;
+ dirty = true;
+ } else if (ev.kbd.keycode == Common::KEYCODE_h) {
+ doHelp();
+ dirty = true;
+ }
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ // Test buttons in the order the original would â
+ // button 0 / 9 are dead zones, so check the actionable
+ // rects directly. Earlier rects "win" when overlapping
+ // (matches `_FindButton`).
+ if (kBtnSite.contains(ev.mouse.x, ev.mouse.y)) {
+ exitFlag = true;
+ break; // back to site
+ }
+ if (kBtnMap.contains(ev.mouse.x, ev.mouse.y)) {
+ doBigMap();
+ exitFlag = true;
+ break;
+ }
+ if (kBtnPartner.contains(ev.mouse.x, ev.mouse.y)) {
+ doHelp(); // _KDHelp = host hint
+ dirty = true;
+ continue;
+ }
+ if (kBtnAccuse.contains(ev.mouse.x, ev.mouse.y)) {
+ doAccuse();
+ exitFlag = true;
+ break;
+ }
+ if (kBtnGallery.contains(ev.mouse.x, ev.mouse.y)) {
+ doGallery();
+ dirty = true;
+ continue;
+ }
+ if (kBtnPagePrev.contains(ev.mouse.x, ev.mouse.y)) {
+ if (page > 0) page--;
+ dirty = true;
+ continue;
+ }
+ if (kBtnPageNext.contains(ev.mouse.x, ev.mouse.y)) {
+ page++;
+ dirty = true;
+ continue;
+ }
+ if (kNoteArea.contains(ev.mouse.x, ev.mouse.y)) {
+ // Toggle the selection on whichever clue's text
+ // the click landed in. The original calls
+ // `_InterfaceHelp` here; that's the help screen,
+ // not selection â selection is in the Accuse
+ // screen. We use the area for selection because
+ // keyboard 1..9 toggling is awkward, and the
+ // resulting `_NoteSelected` state is what
+ // `_SolvedCheck` reads.
+ for (uint i = 0; i < _notebookSlotRects.size(); i++) {
+ if (_notebookSlotRects[i].contains(ev.mouse.x,
+ ev.mouse.y)) {
+ const uint clueId = _notebookSlotClues[i];
+ _mystery._noteSelected[clueId] ^= 1;
+ dirty = true;
+ break;
+ }
+ }
+ continue;
}
}
}
- if (exit) break;
- if (dirty) draw();
+ if (exitFlag)
+ break;
+
+ const uint32 now = g_system->getMillis();
+ // Re-render every 100 ms so the partner sprite cycles frames.
+ if (dirty || now - lastDraw >= 100) {
+ draw();
+ lastDraw = now;
+ }
g_system->updateScreen();
g_system->delayMillis(15);
}
}
void EEMEngine::doGallery() {
- // Mirrors `_DrawGallery` @ 158f:0046. The original loops `_NumSuspects`
- // gallery entries (0x46 = 70 bytes each in `_GalleryData`); the first
- // u16 of each entry is the PIC picture ID for that suspect. We render
- // them in a row across the screen.
+ // Mirrors `_DoGallery @ 158f:065b` and `_DrawGallery @ 158f:0046`.
+ // Verified directly from the disassembly:
+ // * Background: PIC 0x3f (same as PDA).
+ // * Partner sprite at (5, 0x50): anim 2 (Jake) / 0x10 (Jenny).
+ // `_NewAnimation(5, 0x50, ...)`. NOTE: gallery uses anim 2/0x10,
+ // PDA uses 1/0xb â different sprites.
+ // * Five fixed slot positions at `29be:0x116` (4 bytes per slot,
+ // `{u16 x, u16 y}`):
+ // slot 0 = ( 83, 14) slot 3 = (119, 90)
+ // slot 1 = (155, 14) slot 4 = (191, 90)
+ // slot 2 = (227, 14)
+ // * For each logical suspect i in 0..NumSuspects-1:
+ // picId = `*(u16 *)(_GalleryData + i * 0x46)` (entry +0).
+ // visible = `_InGallery[_NewOrder[i]] != 0`.
+ // drawX = positions[_NewOrder[i]].x
+ // drawY = positions[_NewOrder[i]].y + (0x48 - pic.height)
+ // So portraits are BOTTOM-aligned to baselines 0x48 + pos.y.
+ // * Click on portrait via `_SearchSuspects` â `MoreInfo(i)` shows
+ // the suspect detail page. ESC returns to PDA.
+ // * Frame-cycled @ 100ms via `_CheckFrameRate` + `_UpdateAnimations`
+ // + `_GizmoColorCycle`.
if (!_mystery.isLoaded())
return;
@@ -1456,60 +1781,323 @@ void EEMEngine::doGallery() {
return;
}
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
+ CursorMan.showMouse(true);
+
+ struct Slot { int x; int y; };
+ static const Slot kGallerySlots[5] = {
+ { 83, 14 }, // 0
+ { 155, 14 }, // 1
+ { 227, 14 }, // 2
+ { 119, 90 }, // 3
+ { 191, 90 } // 4
+ };
- // Use PIC 0x3f as the gallery backdrop, matching `_DoAccuseGallery`.
+ // Pre-load static elements once.
Picture galBg;
- if (_picsArchive.getPicture(0x3f, galBg)) {
- const int w = MIN<int>(galBg.surface.w, 320);
- const int h = MIN<int>(galBg.surface.h, 200);
- for (int row = 0; row < h; row++) {
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)galBg.surface.getBasePtr(0, row), w);
- }
- }
+ const bool haveBg = _picsArchive.getPicture(0x3f, galBg);
- if (_font.isLoaded())
- _font.drawString(&scratch, "GALLERY", 8, 4, 320, 0xF);
+ // Gallery partner anim â `_DoGallery` calls `_GetAnimation(uVar6)` with
+ // uVar6 = 2 (Jake) / 0x10 (Jenny). Different from PDA (1 / 0xb).
+ const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
+ Animation partnerAni;
+ const bool havePartner = _aniArchive.loadAnimation(partnerAnim, partnerAni)
+ && !partnerAni.empty();
const uint8 num = _mystery.numSuspects();
- int slotX = 8;
- const int slotY = 24;
- const int slotStep = 320 / MAX<uint8>(1, num);
+
+ // Cache slot rects for click hit-testing.
+ Common::Array<Common::Rect> slotRects;
+ Common::Array<int> slotSuspect; // logical suspect index in [0, num)
+ slotRects.resize(num);
+ slotSuspect.resize(num);
for (uint i = 0; i < num; i++) {
- const uint16 picId = READ_LE_UINT16(gd + i * 0x46);
- if (picId == 0)
- continue;
- Picture portrait;
- if (!_picsArchive.getPicture(picId, portrait))
- continue;
- const int placeX = slotX + (slotStep - portrait.surface.w) / 2;
- const int placeY = slotY;
- const int w = MIN<int>(portrait.surface.w, 320 - placeX);
- const int h = MIN<int>(portrait.surface.h, 200 - placeY);
- if (w > 0 && h > 0) {
+ slotSuspect[i] = -1;
+ }
+
+ auto drawFrame = [&]() {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+
+ if (haveBg) {
+ const int bw = MIN<int>(galBg.surface.w, 320);
+ const int bh = MIN<int>(galBg.surface.h, 200);
+ for (int row = 0; row < bh; row++) {
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)galBg.surface.getBasePtr(0, row), bw);
+ }
+ }
+
+ // Partner sprite frame @ (5, 0x50).
+ if (havePartner) {
+ const uint32 now = g_system->getMillis();
+ const uint frameIdx = (uint)((now / 100) % partnerAni.size());
+ const Picture &fr = partnerAni[frameIdx];
+ const byte transp = (byte)(fr.flags >> 8);
+ const int px = 5, py = 0x50;
+ for (int row = 0; row < fr.surface.h; row++) {
+ const int dstY = py + row;
+ if (dstY < 0 || dstY >= 200) continue;
+ const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, dstY);
+ for (int col = 0; col < fr.surface.w; col++) {
+ const int dstX = px + col;
+ if (dstX < 0 || dstX >= 320) continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ }
+
+ // Portraits.
+ for (uint i = 0; i < num && i < Mystery::kGalleryCap; i++) {
+ slotRects[i] = Common::Rect(); // empty
+ slotSuspect[i] = -1;
+
+ const uint8 phys = _mystery._newOrder[i];
+ if (phys >= 5)
+ continue;
+ const bool discovered = _mystery._inGallery[phys] != 0;
+ if (!discovered)
+ continue;
+
+ const uint16 picId = READ_LE_UINT16(gd + i * 0x46);
+ if (picId == 0)
+ continue;
+ Picture portrait;
+ if (!_picsArchive.getPicture(picId, portrait))
+ continue;
+
+ const Slot &s = kGallerySlots[phys];
+ const int placeX = s.x;
+ const int placeY = s.y + (0x48 - portrait.surface.h);
+ const byte transp = (byte)(portrait.flags >> 8);
+
+ const int w = MIN<int>(portrait.surface.w, 320 - placeX);
+ const int h = MIN<int>(portrait.surface.h, 200 - placeY);
+ if (w <= 0 || h <= 0)
+ continue;
for (int row = 0; row < h; row++) {
- memcpy((byte *)scratch.getBasePtr(placeX, placeY + row),
- (const byte *)portrait.surface.getBasePtr(0, row), w);
+ const int dstY = placeY + row;
+ if (dstY < 0) continue;
+ const byte *src =
+ (const byte *)portrait.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, dstY);
+ for (int col = 0; col < w; col++) {
+ const int dstX = placeX + col;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+
+ // Cache rect for hit-test.
+ slotRects[i] = Common::Rect(placeX, placeY,
+ placeX + w, placeY + h);
+ slotSuspect[i] = (int)i;
+
+ // Index label below portrait â original doesn't draw labels
+ // (the detail page does), but useful while MoreInfo isn't
+ // implemented.
+ if (_font.isLoaded()) {
+ Common::String label = Common::String::format("%u", i + 1);
+ _font.drawString(&scratch, label,
+ placeX + portrait.surface.w / 2 - 3,
+ placeY + portrait.surface.h + 2,
+ 320, 0x5C);
}
}
- // Suspect number + discovered marker under the portrait.
+
+ // Header / hint line â KD's hint balloon would normally show here
+ // (`_DoAccuseGallery` plays a balloon at top), but the standalone
+ // `_DoGallery` doesn't. We add a small header for clarity.
if (_font.isLoaded()) {
- const bool discovered = (i < Mystery::kGalleryCap) &&
- _mystery._inGallery[i];
- Common::String label = Common::String::format("%u%s",
- i + 1, discovered ? " *" : "");
- _font.drawString(&scratch, label, placeX + 4, placeY + h + 2, 320, 0xF);
+ _font.drawString(&scratch, "GALLERY", 60, 4, 256, 0x5C);
+ _font.drawString(&scratch, "Click suspect | ESC", 60, 188, 256, 0x5C);
}
- slotX += slotStep;
- }
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
- waitForInput(60000);
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ };
+
+ drawFrame();
+ uint32 lastDraw = g_system->getMillis();
+
+ while (!shouldQuit()) {
+ Common::Event ev;
+ bool exitFlag = false;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ return;
+ }
+ if (ev.type == Common::EVENT_KEYDOWN) {
+ if (ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ exitFlag = true;
+ break;
+ }
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ // `_SearchSuspects` walks the per-slot rects and returns
+ // the suspect index. We mirror that with cached rects.
+ bool clicked = false;
+ for (uint i = 0; i < slotRects.size(); i++) {
+ if (slotSuspect[i] < 0) continue;
+ if (slotRects[i].contains(ev.mouse.x, ev.mouse.y)) {
+ // `MoreInfo(i)` â show the suspect detail page.
+ // Mirrors `MoreInfo @ 158f:0419`:
+ // _RefreshGalleryBackground();
+ // _GetPicture(*(u16*)(gd + i*0x46));
+ // _AddPicBackground(pic, 0x94, 0xf);
+ // _DrawGalleryNotes(gd + i*0x46);
+ // loop until ESC or button click.
+ // Suspect data layout (verified against M1):
+ // +0..1: picId (used here AND for gallery slot)
+ // +8..9: number of clues for this suspect
+ // +0xa..??: array of u16 clue IDs (terminated
+ // by 0xFFFF if shorter than count).
+ const uint suspectIdx = (uint)slotSuspect[i];
+ const byte *suspect = gd + suspectIdx * 0x46;
+ const uint16 detailPic =
+ READ_LE_UINT16(suspect + 0);
+ const uint16 clueCount =
+ READ_LE_UINT16(suspect + 8);
+
+ Graphics::ManagedSurface ms(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ ms.clear();
+ if (haveBg) {
+ const int bw = MIN<int>(galBg.surface.w, 320);
+ const int bh = MIN<int>(galBg.surface.h, 200);
+ for (int row = 0; row < bh; row++) {
+ memcpy((byte *)ms.getBasePtr(0, row),
+ (const byte *)galBg.surface.getBasePtr(0, row), bw);
+ }
+ }
+ // Full suspect picture at (0x94, 0xf).
+ Picture detail;
+ if (_picsArchive.getPicture(detailPic, detail)) {
+ const byte transp =
+ (byte)(detail.flags >> 8);
+ const int dx = 0x94, dy = 0x0f;
+ const int dw = MIN<int>(detail.surface.w, 320 - dx);
+ const int dh = MIN<int>(detail.surface.h, 200 - dy);
+ for (int row = 0; row < dh; row++) {
+ const byte *src =
+ (const byte *)detail.surface.getBasePtr(0, row);
+ byte *dst =
+ (byte *)ms.getBasePtr(0, dy + row);
+ for (int col = 0; col < dw; col++) {
+ if (src[col] != transp)
+ dst[dx + col] = src[col];
+ }
+ }
+ }
+ // Suspect's clue notes inside _GalleryNoteRect
+ // = (78, 93, 288, 152), per 29be:0100.
+ const int rx = 78, ry = 93;
+ const int rw = 288 - 78, rh = 152 - 93;
+ // Paint a dark fill behind the entire notes area
+ // so cyan text on a possibly-cyan PIC background
+ // stays readable. Color 0x20 = dark navy in all
+ // site palettes.
+ ms.fillRect(Common::Rect(rx - 2, ry - 12,
+ rx + rw + 2, ry + rh + 12), 0x20);
+
+ const byte *ni = _mystery.noteIndex();
+ const uint16 niCount = _mystery.noteIndexCount();
+ int yPos = ry;
+ const int lineH = _font.getFontHeight() + 1;
+ bool drewAny = false;
+ for (uint k = 0; k < clueCount && k < 30; k++) {
+ const uint16 clueId =
+ READ_LE_UINT16(suspect + 0xa + k * 2);
+ if (clueId == 0xFFFF) break;
+ if (clueId >= Mystery::kCluesFoundCap ||
+ !_mystery._cluesFound[clueId])
+ continue;
+ if (!ni || clueId >= niCount) continue;
+ const uint16 textOff =
+ READ_LE_UINT16(ni + clueId * 4);
+ Common::String txt =
+ parseString(_mystery.textAt(textOff),
+ _playerName, _partner);
+ if (txt.empty()) continue;
+ const byte color =
+ _mystery._noteSelected[clueId] ? 0x3C : 0x5C;
+ const int hLine = _font.drawWordWrapped(
+ &ms, rx, yPos, rw, txt, color);
+ yPos += hLine + 7;
+ drewAny = true;
+ if (yPos + lineH > ry + rh) break;
+ }
+ if (!drewAny && _font.isLoaded()) {
+ _font.drawString(&ms,
+ "No clues yet for this suspect.",
+ rx, ry, rw, 0x5C);
+ }
+ // Header / footer text.
+ if (_font.isLoaded()) {
+ _font.drawString(&ms, "SUSPECT FILE",
+ rx, ry - 11, rw, 0x3C);
+ _font.drawString(&ms, "(click / ESC: back)",
+ rx, ry + rh + 2, rw, 0x3C);
+ }
+ g_system->copyRectToScreen(ms.getPixels(),
+ ms.pitch, 0, 0, 320, 200);
+ g_system->updateScreen();
+
+ // Wait for click or ESC. Drain the queued
+ // LBUTTONDOWN that triggered this MoreInfo first
+ // so we don't immediately accept it as the
+ // dismiss event.
+ g_system->delayMillis(150);
+ {
+ Common::Event drain;
+ while (g_system->getEventManager()->pollEvent(drain)) {
+ if (drain.type == Common::EVENT_QUIT ||
+ drain.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return;
+ }
+ }
+ bool back = false;
+ while (!back && !shouldQuit()) {
+ Common::Event e2;
+ while (g_system->getEventManager()->pollEvent(e2)) {
+ if (e2.type == Common::EVENT_LBUTTONDOWN ||
+ (e2.type == Common::EVENT_KEYDOWN &&
+ (e2.kbd.keycode == Common::KEYCODE_ESCAPE ||
+ e2.kbd.keycode == Common::KEYCODE_RETURN))) {
+ back = true;
+ break;
+ }
+ if (e2.type == Common::EVENT_QUIT ||
+ e2.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return;
+ }
+ g_system->delayMillis(20);
+ }
+ // Force gallery redraw immediately so the
+ // player isn't left looking at the dismissed
+ // MoreInfo screen until the next 100 ms tick.
+ drawFrame();
+ lastDraw = g_system->getMillis();
+ clicked = true;
+ break;
+ }
+ }
+ (void)clicked;
+ }
+ }
+ if (exitFlag) break;
+
+ const uint32 now = g_system->getMillis();
+ if (now - lastDraw >= 100) {
+ drawFrame();
+ lastDraw = now;
+ }
+ g_system->delayMillis(15);
+ }
}
void EEMEngine::doBigMap() {
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index ec8e9d27b35..85c25b26c22 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -206,6 +206,13 @@ private:
/// the opening-anim loop in run() to skip the rest of the chain
/// instead of asking the user to click through every screen.
bool _skipIntro = false;
+
+ /// Per-slot rectangles + clue IDs from the most recent notebook
+ /// render, populated by the `draw` lambda inside `doNotebook` and
+ /// consumed by the click handler. The original walks the notes
+ /// inline; we cache the layout to keep click hit-testing simple.
+ Common::Array<Common::Rect> _notebookSlotRects;
+ Common::Array<uint> _notebookSlotClues;
};
} // End of namespace EEM
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 30f97bc29c0..964f7707eaa 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -24,6 +24,8 @@
#include "common/system.h"
#include "common/textconsole.h"
+#include "graphics/paletteman.h"
+
#include "eem/detection.h"
#include "eem/eem.h"
#include "eem/mystery.h"
@@ -83,6 +85,11 @@ void SiteScreen::enter(uint siteNum) {
captureBgSnapshot();
_snapshotSite = (int)siteNum;
+ // Cache ColorCycle palette ranges for this site so the per-tick
+ // frame pump can rotate them. Mirrors the init scan at the top of
+ // `_DoSiteLoop @ 168d:03f4`.
+ scanColorCycles(siteNum);
+
// Animated NPCs (Loop 1) and the persistent partner sit on top of
// the snapshot. Initial frame goes at tickMs=now.
const uint32 now = g_system->getMillis();
@@ -297,6 +304,10 @@ void SiteScreen::run() {
renderAnimatedDrops(cur, now);
renderPartner(cur, now);
renderHotspots(cur);
+ // Per-tick palette rotation for ColorCycle entries +
+ // hotspot marching ants. Matches `_ColorCycle(start, end)`
+ // calls inside `_DoSiteLoop @ 168d:03f4`'s main loop.
+ applyColorCycles();
_lastTickMs = now;
}
g_system->updateScreen();
@@ -542,6 +553,67 @@ void SiteScreen::renderAnimatedDrops(uint siteNum, uint32 tickMs) {
g_system->unlockScreen();
}
+void SiteScreen::scanColorCycles(uint siteNum) {
+ // `_DoSiteLoop @ 168d:03f4` walks Loop 1 entries (siteData[+0xa]
+ // count, 6-byte entries at siteData[+0x48]) and stores each entry
+ // with `animId == -1` into a 5-slot table:
+ // start palette idx = entry +2
+ // end palette idx = entry +4
+ // We mirror the layout exactly. Up to 5 entries are tracked (the
+ // original's `[unaff_BP + -0x12]` and `[unaff_BP + -0x1c]` arrays
+ // are 5 Ã u16 each).
+ _colorCycles.clear();
+ if (!_mystery)
+ return;
+ const byte *site = _mystery->siteData(siteNum);
+ if (!site)
+ return;
+ const uint16 numAnims = READ_LE_UINT16(site + 0xa);
+ for (uint i = 0; i < numAnims && _colorCycles.size() < 5; i++) {
+ const int16 animId = (int16)READ_LE_UINT16(site + 0x48 + i * 6);
+ if (animId != -1)
+ continue;
+ const uint16 startPal = READ_LE_UINT16(site + 0x48 + i * 6 + 2);
+ const uint16 endPal = READ_LE_UINT16(site + 0x48 + i * 6 + 4);
+ ColorCycleRange r;
+ r.start = (uint8)startPal;
+ r.end = (uint8)endPal;
+ if (r.end > r.start)
+ _colorCycles.push_back(r);
+ }
+}
+
+void SiteScreen::applyColorCycles() {
+ // `_ColorCycle @ 172b:2015` rotates `_fpal[start..end]` by one
+ // palette slot â saves [start], shifts [start..end-1] = [start+1..
+ // end], restores saved at [end] â then re-uploads via `_Set_Palette`.
+ // We do the same against ScummVM's palette manager. Always rotate
+ // 0xf9..0xfe for hotspot marching ants (the `_ColorCycle(0xf9,
+ // 0xfe)` call at the bottom of `_DoSiteLoop`'s main loop).
+ auto rotate = [&](uint8 start, uint8 end) {
+ if (end <= start) return;
+ const uint count = (uint)end - (uint)start + 1;
+ byte buf[256 * 3];
+ g_system->getPaletteManager()->grabPalette(buf, start, count);
+ // Save first triplet, shift, restore at end.
+ byte savedR = buf[0], savedG = buf[1], savedB = buf[2];
+ for (uint i = 0; i + 1 < count; i++) {
+ buf[i * 3 + 0] = buf[(i + 1) * 3 + 0];
+ buf[i * 3 + 1] = buf[(i + 1) * 3 + 1];
+ buf[i * 3 + 2] = buf[(i + 1) * 3 + 2];
+ }
+ buf[(count - 1) * 3 + 0] = savedR;
+ buf[(count - 1) * 3 + 1] = savedG;
+ buf[(count - 1) * 3 + 2] = savedB;
+ g_system->getPaletteManager()->setPalette(buf, start, count);
+ };
+ for (uint i = 0; i < _colorCycles.size(); i++) {
+ rotate(_colorCycles[i].start, _colorCycles[i].end);
+ }
+ // Hotspot marching ants â always cycled.
+ rotate(0xF9, 0xFE);
+}
+
void SiteScreen::captureBgSnapshot() {
_bgSnapshot.create(320, 200, Graphics::PixelFormat::createFormatCLUT8());
Graphics::Surface *screen = g_system->lockScreen();
@@ -708,6 +780,19 @@ void SiteScreen::renderHotspots(uint siteNum) {
if (!screen)
return;
+ // Mirrors `_DrawSearchButtons @ 2404:0a8f`:
+ // for each hotspot:
+ // if `_Sawit(theSite, loc)` (= _SaveBuffer[hotspot[+0xa]] != 0)
+ // _DrawRect(rect) // outline in cycling colors 0xF9..0xFE
+ // else
+ // _DrawSolidRect(rect) // outline in solid white 0xFF
+ // `_DrawRect`'s cycling colors produce a "marching ants" effect that
+ // makes already-searched hotspots visually distinct without hiding
+ // them. We approximate the cycling by rotating the start color via
+ // the global tick.
+ const uint32 tickMs = g_system->getMillis();
+ const byte cyclePhase = (byte)((tickMs / 80) & 0x07); // 0..7
+
for (uint i = 0; i < count; i++) {
const byte *r = spots + i * 14;
const int16 x1 = (int16)READ_LE_UINT16(r + 0);
@@ -717,11 +802,43 @@ void SiteScreen::renderHotspots(uint siteNum) {
const Common::Rect rect(MAX<int>(0, x1), MAX<int>(0, y1),
MIN<int>(screen->w, x2),
MIN<int>(screen->h, y2));
- // Hotspots flagged as "seen" get a different colour so the
- // player knows they've already searched them.
- const byte color =
- (i < Mystery::kHotSpotsCap && _mystery->_hotSpotsSeen[i]) ? 0x07 : 0x0F;
- screen->frameRect(rect, color);
+ const bool seen = (i < Mystery::kHotSpotsCap)
+ && _mystery->_hotSpotsSeen[i];
+ if (!seen) {
+ // `_DrawSolidRect` â solid white outline (color 0xFF).
+ screen->frameRect(rect, 0xFF);
+ } else {
+ // `_DrawRect` â cycling colors 0xF9..0xFE on each pixel of
+ // the outline. We approximate per-pixel cycling with a
+ // per-rect phase shift so the rects look animated. Start
+ // color is rotated via the global clock.
+ const byte palette[6] = { 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE };
+ byte color = palette[cyclePhase % 6];
+ // Top edge
+ for (int x = rect.left; x < rect.right; x++) {
+ if (x >= 0 && x < screen->w && rect.top >= 0 && rect.top < screen->h)
+ *(byte *)screen->getBasePtr(x, rect.top) = color;
+ color = palette[(color - 0xF9 + 1) % 6];
+ }
+ // Right edge
+ for (int y = rect.top; y < rect.bottom; y++) {
+ if (rect.right - 1 >= 0 && rect.right - 1 < screen->w && y >= 0 && y < screen->h)
+ *(byte *)screen->getBasePtr(rect.right - 1, y) = color;
+ color = palette[(color - 0xF9 + 1) % 6];
+ }
+ // Bottom edge
+ for (int x = rect.right - 1; x >= rect.left; x--) {
+ if (x >= 0 && x < screen->w && rect.bottom - 1 >= 0 && rect.bottom - 1 < screen->h)
+ *(byte *)screen->getBasePtr(x, rect.bottom - 1) = color;
+ color = palette[(color - 0xF9 + 1) % 6];
+ }
+ // Left edge
+ for (int y = rect.bottom - 1; y >= rect.top; y--) {
+ if (rect.left >= 0 && rect.left < screen->w && y >= 0 && y < screen->h)
+ *(byte *)screen->getBasePtr(rect.left, y) = color;
+ color = palette[(color - 0xF9 + 1) % 6];
+ }
+ }
}
g_system->unlockScreen();
@@ -748,9 +865,21 @@ int SiteScreen::hotspotAtPoint(uint siteNum, int x, int y) const {
void SiteScreen::onHotspotClicked(uint siteNum, uint hotIdx) {
debugC(1, kDebugSite, "Site %u: hotspot %u clicked", siteNum, hotIdx);
- // Mark the hotspot itself as seen.
+ // `_DoSiteLoop @ 168d:03f4` (after _DisplayClue):
+ // _HotSpotsSeen[hotspot[+0xa] * 2] = _HotSpotComplete;
+ // The "seen" key is the hotspotIndex field (+0xa) â the 1-based
+ // ordinal â NOT the array index. Two hotspots can share an ordinal
+ // across sites (e.g., a partner's clue you can re-read), so this
+ // matters for cross-site state.
+ const byte *spots = _mystery->hotspots(siteNum);
+ uint hotOrdinal = hotIdx; // fallback to array index
+ if (spots) {
+ hotOrdinal = READ_LE_UINT16(spots + hotIdx * 14 + 0xa);
+ }
+ if (hotOrdinal < Mystery::kHotSpotsCap)
+ _mystery->_hotSpotsSeen[hotOrdinal] = 1;
if (hotIdx < Mystery::kHotSpotsCap)
- _mystery->_hotSpotsSeen[hotIdx] = 1;
+ _mystery->_hotSpotsSeen[hotIdx] = 1; // also mark by array idx for our render
_mystery->_searchLocationNumber = (uint16)hotIdx;
// Bytes 8..9 of each 14-byte hotspot rect = byte offset within the
@@ -759,7 +888,6 @@ void SiteScreen::onHotspotClicked(uint siteNum, uint hotIdx) {
// trick on us...". `displayClue` runs the entry's side effects
// (`_AddNotebook` for ClueEntry +0x30..+0x39, gallery +0x26..+0x2f,
// onsite +0x1c..+0x25) so we don't need to touch `_cluesFound` here.
- const byte *spots = _mystery->hotspots(siteNum);
if (spots) {
const uint16 clueOff = READ_LE_UINT16(spots + hotIdx * 14 + 8);
debugC(2, kDebugSite, " hotspot %u -> clue offset 0x%04x",
diff --git a/engines/eem/site.h b/engines/eem/site.h
index bae8dc53cc6..00c1e647ea0 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -102,6 +102,18 @@ private:
/// Restore the snapshot taken at `captureBgSnapshot` time.
void restoreBgSnapshot();
+ /// Scan the site's Loop 1 entries for ColorCycle entries (animId
+ /// == -1) and cache their (start, end) palette ranges. Called from
+ /// `enter()`. Mirrors `_DoSiteLoop @ 168d:03f4`'s init scan.
+ void scanColorCycles(uint siteNum);
+
+ /// Rotate cached ColorCycle palette ranges (and 0xf9..0xfe for
+ /// hotspot marching ants) one step. Mirrors the original's per-tick
+ /// `_ColorCycle(start, end)` calls inside `_DoSiteLoop`'s main
+ /// loop. ScummVM's palette manager grabs current 8-bit RGB and
+ /// writes back the rotated values.
+ void applyColorCycles();
+
EEMEngine *_vm;
Mystery *_mystery;
bool _showHotspots = true; ///< Toggle outlines with V key.
@@ -109,6 +121,11 @@ private:
int _snapshotSite = -1; ///< Site number the snapshot belongs to.
Graphics::ManagedSurface _bgSnapshot;
uint32 _lastTickMs = 0; ///< Last frame-pump tick in ms.
+
+ /// Per-site cached ColorCycle ranges. Up to 5 (matching the
+ /// original's 5-slot animation table).
+ struct ColorCycleRange { uint8 start, end; };
+ Common::Array<ColorCycleRange> _colorCycles;
};
} // End of namespace EEM
Commit: b36297d2a5918762f6f4a4e19efd9ed5f587f130
https://github.com/scummvm/scummvm/commit/b36297d2a5918762f6f4a4e19efd9ed5f587f130
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:38+02:00
Commit Message:
EEM: polish PDA features: buttons now works correctly
Changed paths:
engines/eem/eem.cpp
engines/eem/mystery.h
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 578f3290a6f..554c588ab32 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -1099,6 +1099,74 @@ void EEMEngine::doInitClues() {
blitMaskedAt(nancy[0], 0x68, 0x8b);
g_system->updateScreen();
+ // Step 5 â `_PlayInSequence(animSeq, 0xcd, animY)` per Ghidra:
+ // Jake (partner=0):
+ // caseType=1 â anim 0x38 at (0xcd, 0x6d)
+ // caseType=2 â anim 0x37 at (0xcd, 0x6c)
+ // caseType=3 â anim 0x39 at (0xcd, 0x6c)
+ // Jenny (partner=1):
+ // caseType=2 â anim 0x3a at (0xcd, 0x6c)
+ // caseType=3 â anim 0x3d at (0xcd, 0x6c)
+ // `_PlayInSequence @ 172b:2d03` plays each frame at (sx-w, sy-rowoff)
+ // with mask blit, advancing one frame per `_CheckFrameRate` tick.
+ uint16 seqAni = 0xFFFF;
+ uint16 seqY = 0x6c;
+ if (_partner == 0) {
+ switch (caseType) {
+ case 1: seqAni = 0x38; seqY = 0x6d; break;
+ case 2: seqAni = 0x37; seqY = 0x6c; break;
+ case 3: seqAni = 0x39; seqY = 0x6c; break;
+ default: break;
+ }
+ } else {
+ switch (caseType) {
+ case 2: seqAni = 0x3a; seqY = 0x6c; break;
+ case 3: seqAni = 0x3d; seqY = 0x6c; break;
+ default: break;
+ }
+ }
+ if (seqAni != 0xFFFF) {
+ Animation seq;
+ if (_aniArchive.loadAnimation(seqAni, seq) && !seq.empty()) {
+ bool skip = false;
+ for (uint frame = 0; frame < seq.size() && !shouldQuit() && !skip;
+ frame++) {
+ const Picture &fr = seq[frame];
+ // Restore BG + base anim frames so each new frame
+ // composites cleanly.
+ if (_picsArchive.getPicture(0x52, bg))
+ blitAt(bg, 0, 0);
+ if (haveGame)
+ blitMaskedAt(game[frame % game.size()], 0xcd, 0x6c);
+ if (haveBook)
+ blitMaskedAt(book[frame % book.size()], 0, 99);
+ if (haveNancy)
+ blitMaskedAt(nancy[frame % nancy.size()], 0x68, 0x8b);
+ // Anchor: original blits at `(sx - frame.width,
+ // sy - frame.rowoff)`. `frame.rowoff` is the y-anchor
+ // in our PicData. We use width/height directly since
+ // loadAnimation places anchor at (0, 0).
+ const int dstX = (int)0xcd - (int)fr.surface.w;
+ const int dstY = (int)seqY - (int)fr.rowoff;
+ blitMaskedAt(fr, dstX, dstY);
+ g_system->updateScreen();
+ const uint32 wakeup = g_system->getMillis() + 100;
+ while (g_system->getMillis() < wakeup &&
+ !shouldQuit() && !skip) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_LBUTTONDOWN ||
+ ev.type == Common::EVENT_KEYDOWN) {
+ skip = true;
+ break;
+ }
+ }
+ g_system->delayMillis(10);
+ }
+ }
+ }
+ }
+
// Step 6 â case briefing dialogue.
displayClue(ib + 4);
}
@@ -1461,17 +1529,31 @@ void EEMEngine::doNotebook() {
if (!_mystery.isLoaded() || !_font.isLoaded())
return;
+ // Button rects from `_NoteButtons @ 29be:0147` matched to handler
+ // addresses via the jump table at `_HandleNoteButton + 0xec` (i.e.
+ // 161e:04ec). Decoded handlers (i = rect_index, dispatch = handler[i-1]):
+ // rect 0 (134,155) â no handler (i-1 underflows; original treats
+ // this as a decorative/no-op slot)
+ // rect 1 (93,115) â 0x03f9 = `_InterfaceHelp(0)` (HELP)
+ // rect 2 (157,178) â 0x0477 = `_NextScreen = 5` (GALLERY)
+ // rect 3 (5,80) â 0x0403 = `_KDHelp` (host hint)
+ // rect 4 (180,201) â 0x0436 = `_SolvedCheck` -> NextScreen=7 (SOLVE)
+ // rect 5 (204,224) â 0x0489 = `_EraseNotes` + `_DrawNotes` (PAGE NEXT)
+ // rect 6 (226,247) â 0x04ab = decrement CurrentPage + redraw (PAGE PREV)
+ // rect 7 (7,177) â 0x0480 = `_NextScreen = 2` (MAP)
+ // rect 8 (35,111) â 0x03ed = `_NextScreen = 3` (SITE)
+ // rect 9 (0,0) â 0x03ed = same as rect 8
+ // rect 10 (66,79) â 0x03f9 = `_InterfaceHelp(0)` (note-area help)
const Common::Rect kNotebookRect(78, 12, 288, 152);
- const Common::Rect kBtnHelp1 ( 93, 174, 115, 190); // [1]
- const Common::Rect kBtnPagePrev(157, 174, 178, 190); // [2]
- const Common::Rect kBtnPartner ( 5, 80, 44, 110); // [3]
- const Common::Rect kBtnAccuse (180, 174, 201, 190); // [4]
- const Common::Rect kBtnGallery (204, 174, 224, 190); // [5]
- const Common::Rect kBtnPageNext(226, 174, 247, 190); // [6]
- const Common::Rect kBtnMap ( 7, 177, 57, 200); // [7]
- const Common::Rect kBtnSite ( 35, 111, 56, 136); // [8]
- const Common::Rect kNoteArea ( 66, 79, 267, 174); // [10]
- (void)kBtnHelp1; (void)kBtnPagePrev; (void)kBtnPageNext;
+ const Common::Rect kBtnHelp1 ( 93, 174, 115, 190); // [1] HELP
+ const Common::Rect kBtnGallery (157, 174, 178, 190); // [2] GALLERY
+ const Common::Rect kBtnPartner ( 5, 80, 44, 110); // [3] KD HELP
+ const Common::Rect kBtnAccuse (180, 174, 201, 190); // [4] SOLVE
+ const Common::Rect kBtnPageNext(204, 174, 224, 190); // [5] PAGE NEXT
+ const Common::Rect kBtnPagePrev(226, 174, 247, 190); // [6] PAGE PREV
+ const Common::Rect kBtnMap ( 7, 177, 57, 200); // [7] MAP
+ const Common::Rect kBtnSite ( 35, 111, 56, 136); // [8] SITE
+ const Common::Rect kNoteArea ( 66, 79, 267, 174); // [10] note area
CursorMan.showMouse(true);
@@ -1593,22 +1675,16 @@ void EEMEngine::doNotebook() {
}
if (txt.empty())
txt = Common::String::format("clue %u", clueId);
- // Compute wrapped lines first to know the rect.
+ // Per `_DrawNotes @ 161e:01d0`: text uses
+ // `_NoteUnselectedColor` (0x5c=cyan) for unselected and 0x3c
+ // (light yellow-white) for selected. Both contrast cleanly
+ // with the PDA screen's natural blue, so we draw text
+ // directly on PIC 0x3f without an extra fill rectangle â
+ // matches the original design.
Common::Array<Common::String> wrapped;
_font.wordWrapText(txt, kRectW, wrapped);
const int lineH = _font.getFontHeight() + 1;
const int h = (int)wrapped.size() * lineH;
-
- // Per `_DrawNotes @ 161e:01d0`: text uses
- // `_NoteUnselectedColor` (0x5c=cyan) for unselected and 0x3c
- // (light yellow-white) for selected. Paint a dark "paper"
- // rectangle behind the text first â without this, the
- // notebook BG (PIC 0x3f) bleeds through and makes the cyan
- // text hard to read on lighter pixel runs. The fill colour
- // 0x20 maps to a dark navy across all site palettes.
- scratch.fillRect(Common::Rect(kRectX - 2, y - 1,
- kRectX + kRectW + 2, y + h + 1), 0x20);
-
const byte color = _mystery._noteSelected[clueId] ? 0x3C : 0x5C;
for (uint li = 0; li < wrapped.size(); li++) {
_font.drawString(&scratch, wrapped[li], kRectX,
@@ -1620,10 +1696,7 @@ void EEMEngine::doNotebook() {
y += h + 7;
}
- // Page indicator + selected-points counter. Paint a small dark
- // strip behind them too so the text reads on the PDA frame's
- // upper-right corner.
- scratch.fillRect(Common::Rect(266, 0, 320, 24), 0x20);
+ // Page indicator + selected-points counter directly on PIC.
_font.drawString(&scratch, Common::String::format("p%d/%d",
page + 1, (int)pageStarts.size()),
270, 4, 320, 0x5C);
@@ -1704,6 +1777,13 @@ void EEMEngine::doNotebook() {
dirty = true;
continue;
}
+ if (kBtnHelp1.contains(ev.mouse.x, ev.mouse.y)) {
+ // rect 1 â `_InterfaceHelp(0)`. We use doHelp() to
+ // surface the same kind of hint pane.
+ doHelp();
+ dirty = true;
+ continue;
+ }
if (kBtnPagePrev.contains(ev.mouse.x, ev.mouse.y)) {
if (page > 0) page--;
dirty = true;
@@ -1849,71 +1929,71 @@ void EEMEngine::doGallery() {
}
}
- // Portraits.
+ // Portraits â `_DrawGallery @ 158f:0046` walks suspects 0..N-1
+ // and only renders those flagged in `_InGallery[NewOrder[i]]`.
+ // Undiscovered slots are left empty in the original. We render
+ // a darkened placeholder + "?" so the player has visual feedback
+ // that suspects exist but are still unknown.
+ uint discoveredCount = 0;
for (uint i = 0; i < num && i < Mystery::kGalleryCap; i++) {
- slotRects[i] = Common::Rect(); // empty
+ slotRects[i] = Common::Rect();
slotSuspect[i] = -1;
const uint8 phys = _mystery._newOrder[i];
if (phys >= 5)
continue;
- const bool discovered = _mystery._inGallery[phys] != 0;
- if (!discovered)
- continue;
-
- const uint16 picId = READ_LE_UINT16(gd + i * 0x46);
- if (picId == 0)
- continue;
- Picture portrait;
- if (!_picsArchive.getPicture(picId, portrait))
- continue;
-
const Slot &s = kGallerySlots[phys];
- const int placeX = s.x;
- const int placeY = s.y + (0x48 - portrait.surface.h);
- const byte transp = (byte)(portrait.flags >> 8);
- const int w = MIN<int>(portrait.surface.w, 320 - placeX);
- const int h = MIN<int>(portrait.surface.h, 200 - placeY);
- if (w <= 0 || h <= 0)
- continue;
- for (int row = 0; row < h; row++) {
- const int dstY = placeY + row;
- if (dstY < 0) continue;
- const byte *src =
- (const byte *)portrait.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, dstY);
- for (int col = 0; col < w; col++) {
- const int dstX = placeX + col;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
-
- // Cache rect for hit-test.
- slotRects[i] = Common::Rect(placeX, placeY,
- placeX + w, placeY + h);
- slotSuspect[i] = (int)i;
+ const bool discovered = _mystery._inGallery[phys] != 0;
+ if (discovered) {
+ const uint16 picId = READ_LE_UINT16(gd + i * 0x46);
+ Picture portrait;
+ if (picId == 0 ||
+ !_picsArchive.getPicture(picId, portrait))
+ continue;
- // Index label below portrait â original doesn't draw labels
- // (the detail page does), but useful while MoreInfo isn't
- // implemented.
- if (_font.isLoaded()) {
- Common::String label = Common::String::format("%u", i + 1);
- _font.drawString(&scratch, label,
- placeX + portrait.surface.w / 2 - 3,
- placeY + portrait.surface.h + 2,
- 320, 0x5C);
+ const int placeX = s.x;
+ const int placeY = s.y + (0x48 - portrait.surface.h);
+ const byte transp = (byte)(portrait.flags >> 8);
+ const int w = MIN<int>(portrait.surface.w, 320 - placeX);
+ const int h = MIN<int>(portrait.surface.h, 200 - placeY);
+ if (w <= 0 || h <= 0)
+ continue;
+ for (int row = 0; row < h; row++) {
+ const int dstY = placeY + row;
+ if (dstY < 0) continue;
+ const byte *src =
+ (const byte *)portrait.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, dstY);
+ for (int col = 0; col < w; col++) {
+ const int dstX = placeX + col;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ slotRects[i] = Common::Rect(placeX, placeY,
+ placeX + w, placeY + h);
+ slotSuspect[i] = (int)i;
+ discoveredCount++;
+ } else {
+ // Undiscovered placeholder â small framed "?" box at
+ // (s.x, s.y) sized 0x40 Ã 0x48 (typical portrait size).
+ const int phW = 0x40, phH = 0x48;
+ const int phX = s.x, phY = s.y;
+ if (phX + phW <= 320 && phY + phH <= 200) {
+ scratch.fillRect(Common::Rect(phX, phY,
+ phX + phW, phY + phH), 0x20);
+ scratch.frameRect(Common::Rect(phX, phY,
+ phX + phW, phY + phH), 0x5C);
+ if (_font.isLoaded()) {
+ _font.drawString(&scratch, "?",
+ phX + phW / 2 - 3,
+ phY + phH / 2 - 4, phW, 0x5C);
+ }
+ }
}
}
-
- // Header / hint line â KD's hint balloon would normally show here
- // (`_DoAccuseGallery` plays a balloon at top), but the standalone
- // `_DoGallery` doesn't. We add a small header for clarity.
- if (_font.isLoaded()) {
- _font.drawString(&scratch, "GALLERY", 60, 4, 256, 0x5C);
- _font.drawString(&scratch, "Click suspect | ESC", 60, 188, 256, 0x5C);
- }
+ (void)discoveredCount;
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
@@ -1938,6 +2018,52 @@ void EEMEngine::doGallery() {
}
}
if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ // PDA bottom-bar buttons mirror `_NoteButtons @ 29be:0147`.
+ // `_DoGallery @ 158f:065b` shares the SAME button table
+ // with `_DoNotebook` (both call `_FindButton` against the
+ // 11-entry table at 0x147). `_HandleGalleryButton @
+ // 158f:05c0` dispatches via a different jump table
+ // (158f:0645). Verified gallery button mapping:
+ // rect 0 (134,155) â 0x05ef = `_NextScreen = 4` (NOTEBOOK)
+ // rect 1 (93,115) â 0x0625 = `_InterfaceHelp` (HELP)
+ // rect 2 (157,178) â 0x0638 = generic exit (no-op)
+ // rect 3 (5,80) â 0x061e = `_KDHelp` (host hint)
+ // rect 4 (180,201) â 0x05ff = `_SolvedCheck` -> SOLVE
+ // rect 5 (204,224) â 0x0638 = generic exit
+ // rect 6 (226,247) â 0x0638 = generic exit
+ // rect 7 (7,177) â 0x05f7 = `_NextScreen = 2` (MAP)
+ // rect 8 (35,111) â 0x05e4 = `_NextScreen = 3` (SITE)
+ const Common::Rect kBtnSite ( 35, 111, 56, 136); // [8] SITE
+ const Common::Rect kBtnMap ( 7, 177, 57, 200); // [7] MAP
+ const Common::Rect kBtnAccuse (180, 174, 201, 190); // [4] SOLVE
+ const Common::Rect kBtnNotebook(134, 174, 155, 190); // [0] NOTEBOOK (back to PDA notes)
+ const Common::Rect kBtnHelp ( 93, 174, 115, 190); // [1] HELP
+ const Common::Rect kBtnPartner ( 5, 80, 44, 110); // [3] KD HELP
+ if (kBtnSite.contains(ev.mouse.x, ev.mouse.y)) {
+ exitFlag = true; break;
+ }
+ if (kBtnMap.contains(ev.mouse.x, ev.mouse.y)) {
+ doBigMap();
+ exitFlag = true; break;
+ }
+ if (kBtnAccuse.contains(ev.mouse.x, ev.mouse.y)) {
+ doAccuse();
+ exitFlag = true; break;
+ }
+ if (kBtnNotebook.contains(ev.mouse.x, ev.mouse.y)) {
+ // Already came from notebook; exiting returns to it.
+ exitFlag = true; break;
+ }
+ if (kBtnHelp.contains(ev.mouse.x, ev.mouse.y)) {
+ doHelp();
+ lastDraw = 0;
+ continue;
+ }
+ if (kBtnPartner.contains(ev.mouse.x, ev.mouse.y)) {
+ doHelp();
+ lastDraw = 0;
+ continue;
+ }
// `_SearchSuspects` walks the per-slot rects and returns
// the suspect index. We mirror that with cached rects.
bool clicked = false;
@@ -1994,15 +2120,11 @@ void EEMEngine::doGallery() {
}
}
// Suspect's clue notes inside _GalleryNoteRect
- // = (78, 93, 288, 152), per 29be:0100.
+ // = (78, 93, 288, 152), per 29be:0100. Cyan text
+ // renders directly on the PDA's natural blue
+ // screen â matches `_DrawGalleryNotes @ 158f:01f4`.
const int rx = 78, ry = 93;
const int rw = 288 - 78, rh = 152 - 93;
- // Paint a dark fill behind the entire notes area
- // so cyan text on a possibly-cyan PIC background
- // stays readable. Color 0x20 = dark navy in all
- // site palettes.
- ms.fillRect(Common::Rect(rx - 2, ry - 12,
- rx + rw + 2, ry + rh + 12), 0x20);
const byte *ni = _mystery.noteIndex();
const uint16 niCount = _mystery.noteIndexCount();
@@ -2597,65 +2719,127 @@ void EEMEngine::doAccuse() {
if (!_mystery.isLoaded())
return;
- // Mirrors `_DoAccuseGallery` @ 1df2:0a31. Render gallery + prompt,
- // accept either keyboard 1..N or a click on a suspect's portrait.
+ // Mirrors `_DoAccuseGallery @ 1df2:0a31`:
+ // 1. Show KD's hint balloon (KDTextIndex[+8] text).
+ // 2. `_GetBackground(0x3f)` â same backdrop as PDA / gallery.
+ // 3. `_DrawGallery()` â renders portraits at the standard 5 slots
+ // (positions verified at 29be:0x116, bottom-aligned baseline 0x48).
+ // 4. Click loop dispatching on `_NoteButtons` (same table as PDA)
+ // with a separate `_HandleAccuseNoteButton` jump table.
const uint8 num = _mystery.numSuspects();
if (num == 0)
return;
const byte *gd = _mystery.galleryData();
- const int slotStep = 320 / MAX<uint8>(1, num);
- const int slotY = 24;
- // Mirrors `_DoAccuseGallery`: load PIC 0x3f as the accuse backdrop.
+ // Verbatim from 29be:0x116 â same five suspect slot positions as
+ // `_DrawGallery @ 158f:0046`.
+ struct Slot { int x; int y; };
+ static const Slot kGallerySlots[5] = {
+ { 83, 14 }, { 155, 14 }, { 227, 14 },
+ { 119, 90 }, { 191, 90 }
+ };
+
Picture accuseBg;
const bool haveAccuseBg = _picsArchive.getPicture(0x3f, accuseBg);
+ Common::Array<Common::Rect> slotRects;
+ Common::Array<int> slotSuspect;
+ slotRects.resize(num);
+ slotSuspect.resize(num);
+ for (uint i = 0; i < num; i++)
+ slotSuspect[i] = -1;
+
auto drawGallery = [&]() {
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
if (haveAccuseBg) {
- const int w = MIN<int>(accuseBg.surface.w, 320);
- const int h = MIN<int>(accuseBg.surface.h, 200);
- for (int row = 0; row < h; row++) {
+ const int bw = MIN<int>(accuseBg.surface.w, 320);
+ const int bh = MIN<int>(accuseBg.surface.h, 200);
+ for (int row = 0; row < bh; row++) {
memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)accuseBg.surface.getBasePtr(0, row), w);
+ (const byte *)accuseBg.surface.getBasePtr(0, row), bw);
}
}
- if (_font.isLoaded())
- _font.drawString(&scratch, "ACCUSE", 8, 4, 320, 0xF);
- for (uint i = 0; i < num; i++) {
+ for (uint i = 0; i < num && i < Mystery::kGalleryCap; i++) {
+ slotRects[i] = Common::Rect();
+ slotSuspect[i] = -1;
if (!gd) continue;
+ const uint8 phys = _mystery._newOrder[i];
+ if (phys >= 5) continue;
+ const Slot &s = kGallerySlots[phys];
+
const uint16 picId = READ_LE_UINT16(gd + i * 0x46);
- if (picId == 0)
- continue;
+ if (picId == 0) continue;
Picture portrait;
if (!_picsArchive.getPicture(picId, portrait))
continue;
- const int placeX = i * slotStep +
- (slotStep - portrait.surface.w) / 2;
- const int placeY = slotY;
+
+ const int placeX = s.x;
+ const int placeY = s.y + (0x48 - portrait.surface.h);
+ const byte transp = (byte)(portrait.flags >> 8);
const int w = MIN<int>(portrait.surface.w, 320 - placeX);
const int h = MIN<int>(portrait.surface.h, 200 - placeY);
- if (w > 0 && h > 0) {
- for (int row = 0; row < h; row++)
- memcpy((byte *)scratch.getBasePtr(placeX, placeY + row),
- (const byte *)portrait.surface.getBasePtr(0, row), w);
- }
- if (_font.isLoaded()) {
- Common::String label = Common::String::format("%u", i + 1);
- _font.drawString(&scratch, label, placeX + 4, placeY + h + 2, 320, 0xF);
+ if (w <= 0 || h <= 0) continue;
+ for (int row = 0; row < h; row++) {
+ const int dstY = placeY + row;
+ if (dstY < 0) continue;
+ const byte *src =
+ (const byte *)portrait.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, dstY);
+ for (int col = 0; col < w; col++) {
+ const int dstX = placeX + col;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
}
+ slotRects[i] = Common::Rect(placeX, placeY,
+ placeX + w, placeY + h);
+ slotSuspect[i] = (int)i;
}
- if (_font.isLoaded()) {
- _font.drawString(&scratch, "Click a suspect or press 1..N - ESC to cancel", 8, 180, 320, 0xF);
- }
+
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
g_system->updateScreen();
};
+
+ // Step 1 â KD hint balloon. `KDTextIndex[+8]` = third hint slot
+ // (offset 8 from the index array).
+ const byte *kdIdx = _mystery.kdTextIndex();
+ if (kdIdx) {
+ const int16 textOff = (int16)READ_LE_UINT16(kdIdx + 8);
+ if (textOff != -1) {
+ const char *raw = _mystery.textAt((uint16)textOff);
+ Common::String hint =
+ parseString(raw ? raw : "", _playerName, _partner);
+ if (!hint.empty()) {
+ // Mini ClueBlock: 1 entry, partner-0 fields. Compose
+ // just the balloon at (0x21, 0x10) (default position
+ // from `_DoAccuseGallery`).
+ Graphics::ManagedSurface ms(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ ms.clear();
+ if (haveAccuseBg) {
+ const int bw = MIN<int>(accuseBg.surface.w, 320);
+ const int bh = MIN<int>(accuseBg.surface.h, 200);
+ for (int row = 0; row < bh; row++) {
+ memcpy((byte *)ms.getBasePtr(0, row),
+ (const byte *)accuseBg.surface.getBasePtr(0, row), bw);
+ }
+ }
+ if (_font.isLoaded()) {
+ _font.drawWordWrapped(&ms, 0x52, 0x14, 218, hint, 0x5C);
+ }
+ g_system->copyRectToScreen(ms.getPixels(), ms.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ waitForInput(8000);
+ }
+ }
+ }
+
drawGallery();
int picked = -1;
@@ -2676,10 +2860,13 @@ void EEMEngine::doAccuse() {
}
}
if (ev.type == Common::EVENT_LBUTTONDOWN) {
- const int slot = ev.mouse.x / slotStep;
- if (slot >= 0 && slot < (int)num &&
- ev.mouse.y >= slotY && ev.mouse.y < slotY + 120)
- picked = slot;
+ for (uint i = 0; i < slotRects.size(); i++) {
+ if (slotSuspect[i] < 0) continue;
+ if (slotRects[i].contains(ev.mouse.x, ev.mouse.y)) {
+ picked = (int)i;
+ break;
+ }
+ }
}
}
g_system->updateScreen();
diff --git a/engines/eem/mystery.h b/engines/eem/mystery.h
index b0ea78677e6..96da8666f1b 100644
--- a/engines/eem/mystery.h
+++ b/engines/eem/mystery.h
@@ -101,6 +101,18 @@ public:
/// host hint lines.
const byte *kdTextIndex() const;
+ /// Pointer to the HintBlock; per-clue hint TextBlock offsets indexed
+ /// by `_aChain[i]` (the Nth required clue). Mirrors the
+ /// `_HintBlock` global read in `_KDHelp @ 1560:010a`.
+ const byte *hintBlock() const;
+
+ /// Read entry @p i from `_aChain` (the required-clue chain). Returns
+ /// 0xFFFF when no entry exists. Used by `_KDHelp` to walk unfound
+ /// clues for hints.
+ uint16 aChain(uint i) const {
+ return i < kChainLen ? _aChain[i] : 0xFFFF;
+ }
+
/// Pointer to the MapData entry for site @p siteNum (14 bytes per
/// entry; first u16 = sitepic, +4..7 = (x, y) on the big map).
const byte *mapEntry(uint siteNum) const;
Commit: 7be086ee1b8206cebbfe0ec0a9a065431c0c100b
https://github.com/scummvm/scummvm/commit/7be086ee1b8206cebbfe0ec0a9a065431c0c100b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:38+02:00
Commit Message:
EEM: polish PDA features: help interface added
Changed paths:
engines/eem/eem.cpp
engines/eem/eem.h
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 554c588ab32..618c43862a3 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -1778,9 +1778,9 @@ void EEMEngine::doNotebook() {
continue;
}
if (kBtnHelp1.contains(ev.mouse.x, ev.mouse.y)) {
- // rect 1 â `_InterfaceHelp(0)`. We use doHelp() to
- // surface the same kind of hint pane.
- doHelp();
+ // rect 1 â `_InterfaceHelp(0)`: walks `HelpData[0]` and
+ // blits PICs 0x63 / 0x1ae fullscreen for click-through.
+ doInterfaceHelp(0);
dirty = true;
continue;
}
@@ -2055,7 +2055,10 @@ void EEMEngine::doGallery() {
exitFlag = true; break;
}
if (kBtnHelp.contains(ev.mouse.x, ev.mouse.y)) {
- doHelp();
+ // Gallery rect 1 â `_InterfaceHelp(0)` per jmp table at
+ // 158f:0625 (HandleGalleryButton). Same picture sequence
+ // as the notebook HELP button.
+ doInterfaceHelp(0);
lastDraw = 0;
continue;
}
@@ -2648,6 +2651,91 @@ void EEMEngine::doHelp() {
waitForInput(60000);
}
+void EEMEngine::doInterfaceHelp(uint num) {
+ // Mirrors `_InterfaceHelp(num)` @ 1560:0205. The original walks
+ // `HelpData @ 29be:00c8` (5-byte entries: u8 count, then up to 2
+ // u16 picIds), `_GetPicture`s each one, blits it fullscreen via
+ // `_Rect_Move_Mask(0, 0, ...)`, and waits for click / key. ESC ends
+ // the cycle; any other input advances to the next pic.
+ //
+ // Verified from Ghidra HelpData bytes:
+ // entry 0 (PDA / gallery HELP button): count=2, picIds = 0x0063, 0x01ae
+ // entry 1: count=2, picIds = 0x0192, 0x01b1
+ // Only entry 0 is reachable from the PDA notebook (rect 1) and the
+ // gallery (rect 1) â both call `_InterfaceHelp(0)`.
+ static const uint16 kHelpPics[][2] = {
+ { 0x0063, 0x01ae },
+ { 0x0192, 0x01b1 },
+ };
+ if (num >= ARRAYSIZE(kHelpPics))
+ return;
+
+ debugC(1, kDebugScript, "doInterfaceHelp(%u): showing pics 0x%x, 0x%x",
+ num, kHelpPics[num][0], kHelpPics[num][1]);
+
+ for (uint i = 0; i < 2; i++) {
+ const uint16 picId = kHelpPics[num][i];
+ Picture pic;
+ if (!_picsArchive.getPicture(picId, pic)) {
+ warning("doInterfaceHelp: getPicture(0x%x) failed", picId);
+ continue;
+ }
+ debugC(1, kDebugScript, "doInterfaceHelp: pic 0x%x = %dx%d flags=0x%x",
+ picId, pic.surface.w, pic.surface.h, pic.flags);
+
+ // Compose a 320x200 frame (cleared) and blit the help pic at (0,0)
+ // with the original's masked-blit semantics: pixels equal to the
+ // pic's sub-mode (high byte of `pic[0]`, see `_Rect_Move_Mask`
+ // param_10 at 1000:03fc) are treated as transparent and skipped.
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ const byte transp = (byte)(pic.flags >> 8);
+ const int w = MIN<int>(pic.surface.w, 320);
+ const int h = MIN<int>(pic.surface.h, 200);
+ for (int row = 0; row < h; row++) {
+ const byte *src = (const byte *)pic.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, row);
+ for (int col = 0; col < w; col++) {
+ if (src[col] != transp)
+ dst[col] = src[col];
+ }
+ }
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+
+ bool escape = false;
+ while (!shouldQuit() && !escape) {
+ Common::Event ev;
+ bool advance = false;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ return;
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ advance = true;
+ break;
+ }
+ if (ev.type == Common::EVENT_KEYDOWN) {
+ if (ev.kbd.keycode == Common::KEYCODE_ESCAPE)
+ escape = true;
+ else
+ advance = true;
+ break;
+ }
+ }
+ if (advance || escape)
+ break;
+ g_system->updateScreen();
+ g_system->delayMillis(15);
+ }
+ if (escape)
+ break;
+ }
+}
+
bool EEMEngine::areYouSure() {
// Mirrors `_AreYouSure` @ 1a35:0a5c. Original loads PIC 0x136 for the
// dialog body and PIC 0x1FD/0x1FE for YES/NO. We render a minimal
@@ -2805,8 +2893,24 @@ void EEMEngine::doAccuse() {
g_system->updateScreen();
};
- // Step 1 â KD hint balloon. `KDTextIndex[+8]` = third hint slot
- // (offset 8 from the index array).
+ // Step 1 â KD hint balloon. Mirrors `_DoAccuseGallery @ 1df2:0a31`
+ // (1df2:0a4c-1df2:0afe):
+ // text = TextBlock + KDTextIndex[+8]
+ // bub = _GetKDTextBalloon(text) â 1df2:0105
+ // GetBalloon(bub) â pic in [BP-6:-8]
+ // if (pic.h < 0x4e) y = (0x50 - pic.h) >> 1 else y = 1 (1df2:0a8b)
+ // AddPicBackground(pic, 0x21, y) (1df2:0aab)
+ // WordWrap(0x21 + tbl[bub].x, y + tbl[bub].y, tbl[bub].w, text, color=0, -1)
+ // tbl base = 29be:0875, 10-byte entries: x at +0, y at +2, w at +4
+ //
+ // `_GetKDTextBalloon` (1df2:0105): when (ctype[firstChar] & 2) == 0 â
+ // bub = *(u16*)29be:1068 = 0x0017 (verified)
+ // else â table lookup
+ // bub = *(u16*)(29be:0fe6 + 0x1e + firstChar*2)
+ // Bit 1 in Borland's ctype = punctuation, so non-punctuation (most
+ // letters / control bytes / digits) takes the constant-balloon path.
+ // We use 0x17 as the default; the table-lookup path covers a small
+ // minority of texts (those starting with `,` `.` `:` etc.).
const byte *kdIdx = _mystery.kdTextIndex();
if (kdIdx) {
const int16 textOff = (int16)READ_LE_UINT16(kdIdx + 8);
@@ -2815,9 +2919,19 @@ void EEMEngine::doAccuse() {
Common::String hint =
parseString(raw ? raw : "", _playerName, _partner);
if (!hint.empty()) {
- // Mini ClueBlock: 1 entry, partner-0 fields. Compose
- // just the balloon at (0x21, 0x10) (default position
- // from `_DoAccuseGallery`).
+ // 29be:1068 = 0x0017, the non-punctuation default.
+ const uint16 bubNum = 0x17;
+ Picture balloon;
+ const bool haveBalloon =
+ _balloonArchive.size() > (bubNum & 0x7F) &&
+ _balloonArchive.loadEntry(bubNum & 0x7F, balloon);
+
+ // 1df2:0a8b-1df2:0aa5: y = (h < 0x4e) ? (0x50-h)>>1 : 1
+ const int balloonX = 0x21;
+ int balloonY = 1;
+ if (haveBalloon && balloon.surface.h < 0x4e)
+ balloonY = (0x50 - balloon.surface.h) / 2;
+
Graphics::ManagedSurface ms(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
ms.clear();
@@ -2829,8 +2943,29 @@ void EEMEngine::doAccuse() {
(const byte *)accuseBg.surface.getBasePtr(0, row), bw);
}
}
- if (_font.isLoaded()) {
- _font.drawWordWrapped(&ms, 0x52, 0x14, 218, hint, 0x5C);
+ // `_Rect_Move_Mask` (1000:03fc) â pixels == pic[0]>>8 are
+ // transparent (verified at displayClue site).
+ if (haveBalloon) {
+ const byte transp = (byte)(balloon.flags >> 8);
+ const int bw = MIN<int>(balloon.surface.w, 320 - balloonX);
+ const int bh = MIN<int>(balloon.surface.h, 200 - balloonY);
+ for (int row = 0; row < bh; row++) {
+ const byte *src = (const byte *)balloon.surface.getBasePtr(0, row);
+ byte *dst = (byte *)ms.getBasePtr(balloonX, balloonY + row);
+ for (int col = 0; col < bw; col++) {
+ if (src[col] != transp)
+ dst[col] = src[col];
+ }
+ }
+ }
+ // 29be:0875 entry 23 (= 0x17): bytes `05 00 04 00 9b 00 96 00 23 00`
+ // â x=5, y=4, w=155 (verified). 1df2:0acb pushes color=0.
+ if (haveBalloon && _font.isLoaded()) {
+ _font.drawWordWrapped(&ms, balloonX + 5, balloonY + 4, 155,
+ hint, 0);
+ } else if (_font.isLoaded()) {
+ _font.drawWordWrapped(&ms, balloonX + 5, balloonY + 4, 155,
+ hint, 0xF);
}
g_system->copyRectToScreen(ms.getPixels(), ms.pitch,
0, 0, 320, 200);
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 85c25b26c22..2e69fd2347c 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -104,6 +104,11 @@ public:
/// the original engine tracks via `_SawHelpHint`.
void doHelp();
+ /// Display the interface-help picture sequence. Mirrors `_InterfaceHelp`
+ /// @ 1560:0205 â walks `HelpData @ 29be:00c8`, blits each pic fullscreen,
+ /// and waits for click / key (ESC ends the cycle).
+ void doInterfaceHelp(uint num = 0);
+
/// "Are you sure?" yes/no dialog. Mirrors `_AreYouSure` @ 1a35:0a5c.
/// Returns true if the user picked YES.
bool areYouSure();
Commit: d075832f54e402f46ab2e01b0905cfe9f0777914
https://github.com/scummvm/scummvm/commit/d075832f54e402f46ab2e01b0905cfe9f0777914
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:39+02:00
Commit Message:
EEM: polish PDA features: acusations
Changed paths:
engines/eem/eem.cpp
engines/eem/eem.h
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 618c43862a3..2a4778f0d76 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -2736,6 +2736,60 @@ void EEMEngine::doInterfaceHelp(uint num) {
}
}
+uint16 EEMEngine::getKDTextBalloon(byte firstChar) const {
+ // Mirrors `_GetKDTextBalloon @ 1df2:0105`:
+ // if ((ctype[firstChar] & 2) == 0) bub = *(u16*)29be:1068 = 0x17
+ // else bub = *(u16*)(29be:0fe6+0x1e+c*2)
+ // `ctype` is Borland's `_ctype_` array at `29be:2be1`. Bit 1 (0x02) is
+ // set only for digits '0'..'9' (verified by reading the table â '0'..'9'
+ // each map to byte 0x02; everything else has bit 1 clear).
+ // Lookup table at 29be:1064 (= 29be:0fe6 + 0x1e + '0'*2):
+ // '0'â0x15 '1'â0x16 '2'â0x17 '3'â0x18 '4'â0x19
+ // '5'â0x1a '6'â0x20 '7'â0x21 '8'â0x22 '9'â0x1e
+ // Note `*(u16*)29be:1068` (= entry for '2') is the same byte the
+ // non-digit fallback returns â the original encodes the constant by
+ // reusing the digit-2 slot.
+ if (firstChar < '0' || firstChar > '9')
+ return 0x17;
+ static const uint16 kDigitBalloons[10] = {
+ 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x20, 0x21, 0x22, 0x1e
+ };
+ return kDigitBalloons[firstChar - '0'];
+}
+
+bool EEMEngine::getBalloonInsets(uint16 bubNum, uint16 &xInset,
+ uint16 &yInset, uint16 &textW) const {
+ // 52-entry, 10-bytes-each balloon-metadata table at `29be:0875`.
+ // Used at 1df2:0aef-0af9 (accuse hint) and `_DisplayClue` to position
+ // `_WordWrap` text inside the balloon. Only +0/+2/+4 are read here:
+ // +0..1 = text X inset, +2..3 = Y inset, +4..5 = max wrap width
+ // (+6/+8 = balloon h / tail offset, both unused for text layout).
+ static const struct { uint16 x, y, w; } kTable[] = {
+ { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 },
+ { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 },
+ { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 },
+ { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 },
+ { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 },
+ { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 },
+ { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 },
+ { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 },
+ { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 },
+ { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 },
+ { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 },
+ { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 },
+ { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 },
+ { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 },
+ { 5, 8, 158 }, { 5, 8, 176 }, { 8, 7, 142 }
+ };
+ const uint idx = bubNum & 0x7F;
+ if (idx >= ARRAYSIZE(kTable))
+ return false;
+ xInset = kTable[idx].x;
+ yInset = kTable[idx].y;
+ textW = kTable[idx].w;
+ return true;
+}
+
bool EEMEngine::areYouSure() {
// Mirrors `_AreYouSure` @ 1a35:0a5c. Original loads PIC 0x136 for the
// dialog body and PIC 0x1FD/0x1FE for YES/NO. We render a minimal
@@ -2838,6 +2892,7 @@ void EEMEngine::doAccuse() {
for (uint i = 0; i < num; i++)
slotSuspect[i] = -1;
+ int highlighted = 0;
auto drawGallery = [&]() {
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
@@ -2857,6 +2912,11 @@ void EEMEngine::doAccuse() {
if (!gd) continue;
const uint8 phys = _mystery._newOrder[i];
if (phys >= 5) continue;
+ // `_DrawGallery @ 158f:00b9` skips suspects whose
+ // `_InGallery[phys]` flag is 0 â that's the original gate
+ // (some suspects only become visible after being met or
+ // stay hidden after a wrong accusation removes them).
+ if (_mystery._inGallery[phys] == 0) continue;
const Slot &s = kGallerySlots[phys];
const uint16 picId = READ_LE_UINT16(gd + i * 0x46);
@@ -2888,6 +2948,19 @@ void EEMEngine::doAccuse() {
slotSuspect[i] = (int)i;
}
+ // Highlight indicator. The original moves the mouse cursor
+ // to the centre of the highlighted suspect via `_PutMouseInRect`
+ // (1df2:0b8e) â we draw a 1px outline in palette index 0xFE
+ // (within the marching-ants cycle range 0xF9..0xFE) which is
+ // unambiguously visible under any palette without warping the
+ // player's cursor.
+ if (highlighted >= 0 && highlighted < (int)slotRects.size() &&
+ !slotRects[highlighted].isEmpty()) {
+ Common::Rect r = slotRects[highlighted];
+ r.grow(1);
+ scratch.frameRect(r, 0xFE);
+ }
+
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
g_system->updateScreen();
@@ -2895,22 +2968,13 @@ void EEMEngine::doAccuse() {
// Step 1 â KD hint balloon. Mirrors `_DoAccuseGallery @ 1df2:0a31`
// (1df2:0a4c-1df2:0afe):
- // text = TextBlock + KDTextIndex[+8]
- // bub = _GetKDTextBalloon(text) â 1df2:0105
- // GetBalloon(bub) â pic in [BP-6:-8]
- // if (pic.h < 0x4e) y = (0x50 - pic.h) >> 1 else y = 1 (1df2:0a8b)
+ // text = TextBlock + KDTextIndex[+8] (1df2:0a4c-0a57)
+ // bub = _GetKDTextBalloon(text[0]) (1df2:0a6d)
+ // GetBalloon(bub) (1df2:0a7c)
+ // y = (h < 0x4e) ? (0x50 - h) >> 1 : 1 (1df2:0a8b-0aa5)
// AddPicBackground(pic, 0x21, y) (1df2:0aab)
- // WordWrap(0x21 + tbl[bub].x, y + tbl[bub].y, tbl[bub].w, text, color=0, -1)
- // tbl base = 29be:0875, 10-byte entries: x at +0, y at +2, w at +4
- //
- // `_GetKDTextBalloon` (1df2:0105): when (ctype[firstChar] & 2) == 0 â
- // bub = *(u16*)29be:1068 = 0x0017 (verified)
- // else â table lookup
- // bub = *(u16*)(29be:0fe6 + 0x1e + firstChar*2)
- // Bit 1 in Borland's ctype = punctuation, so non-punctuation (most
- // letters / control bytes / digits) takes the constant-balloon path.
- // We use 0x17 as the default; the table-lookup path covers a small
- // minority of texts (those starting with `,` `.` `:` etc.).
+ // WordWrap(0x21+tbl[bub].x, y+tbl[bub].y, tbl[bub].w, text, color=0)
+ // tbl @ 29be:0875, 10-byte entries (1df2:0ad6-0af1)
const byte *kdIdx = _mystery.kdTextIndex();
if (kdIdx) {
const int16 textOff = (int16)READ_LE_UINT16(kdIdx + 8);
@@ -2919,8 +2983,14 @@ void EEMEngine::doAccuse() {
Common::String hint =
parseString(raw ? raw : "", _playerName, _partner);
if (!hint.empty()) {
- // 29be:1068 = 0x0017, the non-punctuation default.
- const uint16 bubNum = 0x17;
+ // First-char dispatch via getKDTextBalloon (1df2:0105).
+ // Note: we pass the *parsed* first char; the original
+ // reads it BEFORE `_ParseString`, but the player-name /
+ // partner-name substitutions never start with digits, so
+ // the dispatch result is the same either way.
+ const byte firstChar =
+ hint.empty() ? (byte)0 : (byte)hint[0];
+ const uint16 bubNum = getKDTextBalloon(firstChar);
Picture balloon;
const bool haveBalloon =
_balloonArchive.size() > (bubNum & 0x7F) &&
@@ -2943,8 +3013,8 @@ void EEMEngine::doAccuse() {
(const byte *)accuseBg.surface.getBasePtr(0, row), bw);
}
}
- // `_Rect_Move_Mask` (1000:03fc) â pixels == pic[0]>>8 are
- // transparent (verified at displayClue site).
+ // Masked balloon blit â `_Rect_Move_Mask` (1000:03fc)
+ // skips pixels equal to `pic[0] >> 8`.
if (haveBalloon) {
const byte transp = (byte)(balloon.flags >> 8);
const int bw = MIN<int>(balloon.surface.w, 320 - balloonX);
@@ -2958,14 +3028,13 @@ void EEMEngine::doAccuse() {
}
}
}
- // 29be:0875 entry 23 (= 0x17): bytes `05 00 04 00 9b 00 96 00 23 00`
- // â x=5, y=4, w=155 (verified). 1df2:0acb pushes color=0.
- if (haveBalloon && _font.isLoaded()) {
- _font.drawWordWrapped(&ms, balloonX + 5, balloonY + 4, 155,
- hint, 0);
- } else if (_font.isLoaded()) {
- _font.drawWordWrapped(&ms, balloonX + 5, balloonY + 4, 155,
- hint, 0xF);
+ // Inset table @ 29be:0875 â 1df2:0acb pushes color=0.
+ uint16 tx = 5, ty = 4, tw = 155;
+ getBalloonInsets(bubNum, tx, ty, tw);
+ if (_font.isLoaded()) {
+ _font.drawWordWrapped(&ms, balloonX + tx,
+ balloonY + ty, tw, hint,
+ haveBalloon ? 0 : 0xF);
}
g_system->copyRectToScreen(ms.getPixels(), ms.pitch,
0, 0, 320, 200);
@@ -2975,9 +3044,42 @@ void EEMEngine::doAccuse() {
}
}
+ // Helper to find the next "alive" slot (one whose `_inGallery[phys]`
+ // flag is still set so a portrait was actually drawn). Mirrors the
+ // way the original wraps DI past empty slots.
+ auto nextLiveSlot = [&](int from, int dir) -> int {
+ const int n = (int)slotRects.size();
+ if (n <= 0) return 0;
+ for (int step = 1; step <= n; step++) {
+ int idx = (from + dir * step) % n;
+ if (idx < 0) idx += n;
+ if (!slotRects[idx].isEmpty())
+ return idx;
+ }
+ return from;
+ };
+ if (slotRects[highlighted].isEmpty())
+ highlighted = nextLiveSlot(highlighted, +1);
+
drawGallery();
+ // Wait-for-pick loop. Mirrors `_DoAccuseGallery` 1df2:0b26-1df2:0bc8:
+ // * `_CheckFrameRate` + `_UpdateAnimations` per tick (1df2:0b2a-0b33)
+ // * 5-entry input dispatch table @ 1df2:0bc9:
+ // 0x09 (TAB) â handler 0x0b94 (cycle highlight)
+ // 0x0d (Enter) â handler 0x0b72 (pick = _SearchSuspects)
+ // 0x4b (LEFT) â handler 0x0b94
+ // 0x4d (RIGHT) â handler 0x0b94
+ // 0xFFFF (mb) â handler 0x0b72
+ // * 0x0b94: `INC DI` + wraparound + `_PutMouseInRect(&Guys[DI])`,
+ // i.e. advance highlight and warp cursor (1df2:0b94-0bb1).
+ // * 0x0b72: `_SearchSuspects` (158f:0584) â mouse-rect hit-test;
+ // if non-0xFFFF, pick that suspect.
+ // We don't warp the cursor (unfriendly under SDL); instead the
+ // highlight is drawn as a 1px outline and Enter picks it.
int picked = -1;
+ uint32 lastTick = g_system->getMillis();
+ bool dirty = false;
while (picked < 0 && !shouldQuit()) {
Common::Event ev;
while (g_system->getEventManager()->pollEvent(ev)) {
@@ -2985,13 +3087,39 @@ void EEMEngine::doAccuse() {
ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
return;
if (ev.type == Common::EVENT_KEYDOWN) {
- if (ev.kbd.keycode == Common::KEYCODE_ESCAPE)
+ switch (ev.kbd.keycode) {
+ case Common::KEYCODE_ESCAPE:
return;
- const int k = (int)ev.kbd.keycode;
- if (k >= Common::KEYCODE_1 && k <= Common::KEYCODE_9) {
- const int idx = k - Common::KEYCODE_1;
- if (idx < num)
- picked = idx;
+ case Common::KEYCODE_TAB:
+ case Common::KEYCODE_RIGHT:
+ highlighted = nextLiveSlot(highlighted, +1);
+ dirty = true;
+ break;
+ case Common::KEYCODE_LEFT:
+ // 1df2:0b94 increments DI for LEFT too â but a
+ // keyboard-driven UX is friendlier with separate
+ // directions, so we mirror Right=+1 / Left=-1.
+ highlighted = nextLiveSlot(highlighted, -1);
+ dirty = true;
+ break;
+ case Common::KEYCODE_RETURN:
+ case Common::KEYCODE_KP_ENTER:
+ if (highlighted >= 0 &&
+ highlighted < (int)slotRects.size() &&
+ !slotRects[highlighted].isEmpty()) {
+ picked = highlighted;
+ }
+ break;
+ default: {
+ const int k = (int)ev.kbd.keycode;
+ if (k >= Common::KEYCODE_1 && k <= Common::KEYCODE_9) {
+ const int idx = k - Common::KEYCODE_1;
+ if (idx < num &&
+ !slotRects[idx].isEmpty())
+ picked = idx;
+ }
+ break;
+ }
}
}
if (ev.type == Common::EVENT_LBUTTONDOWN) {
@@ -3004,6 +3132,16 @@ void EEMEngine::doAccuse() {
}
}
}
+ // 100 ms tick â the original calls `_UpdateAnimations` per
+ // `_CheckFrameRate` (1df2:0b33). The accuse screen has no
+ // animations registered, so the tick is just a redraw cadence.
+ // We still re-render whenever the highlight moves (`dirty`).
+ const uint32 now = g_system->getMillis();
+ if (dirty || now - lastTick >= 100) {
+ drawGallery();
+ lastTick = now;
+ dirty = false;
+ }
g_system->updateScreen();
g_system->delayMillis(10);
}
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 2e69fd2347c..2bc741352ff 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -109,6 +109,20 @@ public:
/// and waits for click / key (ESC ends the cycle).
void doInterfaceHelp(uint num = 0);
+ /// First-char-dispatch balloon picker. Mirrors `_GetKDTextBalloon @
+ /// 1df2:0105`. For digit first chars (0..9) returns balloon from the
+ /// table at `29be:1064`; for any other char returns the constant
+ /// `*(u16*)29be:1068 = 0x17`. The original branch is on Borland's
+ /// ctype-bit-1 (= digit) at `29be:2be1 + char`.
+ uint16 getKDTextBalloon(byte firstChar) const;
+
+ /// Look up balloon-text-inset metadata. Mirrors the 52-entry table at
+ /// `29be:0875`, indexed by `(bubNum & 0x7F)`. 10 bytes per entry; only
+ /// the first 3 fields (x inset, y inset, text width) are used for
+ /// rendering. Returns false if `bubNum` is outside the table.
+ bool getBalloonInsets(uint16 bubNum, uint16 &xInset, uint16 &yInset,
+ uint16 &textW) const;
+
/// "Are you sure?" yes/no dialog. Mirrors `_AreYouSure` @ 1a35:0a5c.
/// Returns true if the user picked YES.
bool areYouSure();
Commit: 2ae70bdb92a852327c9ecca085393194145902f0
https://github.com/scummvm/scummvm/commit/2ae70bdb92a852327c9ecca085393194145902f0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:39+02:00
Commit Message:
EEM: basic parners animations
Changed paths:
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/site.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 2a4778f0d76..cedef8690b7 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -1307,9 +1307,20 @@ void EEMEngine::displayClue(const byte *clueBlock) {
// +8..9, +10..11: bubText offset for partner 0/1 (rel. TextBlock)
// +12..13, +14..15: balloon picture ID for partner 0/1
// +16..17, +18..19: bubX, bubY
+ // +0x3a..+0x3b: KD-anim number (-1 = none)
// Per `_DisplayClue` @ 2404:05e6: partner 1 uses its own field
// set ONLY when bubText1 is not -1; otherwise it falls back to
// the partner 0 fields entirely. Partner 0 always uses field 0.
+
+ // Per-clue partner reaction animation. `_DisplayClue` @
+ // 2404:0635-064b checks `clueEntry[+0x3a]` and, when not -1,
+ // calls `_DoKDAnim(num)` BEFORE drawing the speaker portrait.
+ // This is what surfaces "Jenny takes a picture with a camera"
+ // (and the matching Jake gestures) during NPC searches.
+ const int16 kdAnimNum = (int16)READ_LE_UINT16(c + 0x3a);
+ if (kdAnimNum != -1)
+ playKdAnim((uint16)kdAnimNum);
+
const bool useP1 = (_partner == 1) &&
(READ_LE_UINT16(c + 10) != 0xFFFF);
const uint partner = useP1 ? 1 : 0;
@@ -2757,6 +2768,190 @@ uint16 EEMEngine::getKDTextBalloon(byte firstChar) const {
return kDigitBalloons[firstChar - '0'];
}
+void EEMEngine::setPartnerEraseBg(const Graphics::ManagedSurface *bg) {
+ if (bg && bg->w == 320 && bg->h == 200) {
+ _partnerEraseBg.create(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ for (int row = 0; row < 200; row++) {
+ memcpy((byte *)_partnerEraseBg.getBasePtr(0, row),
+ (const byte *)bg->getBasePtr(0, row), 320);
+ }
+ } else {
+ _partnerEraseBg.free();
+ }
+}
+
+void EEMEngine::playKdAnim(uint16 num) {
+ // Mirrors `_DoKDAnim(num) @ 168d:028a` + `_PlayAnimation @ 172b:1f46`:
+ // _SuspendAnimation(WaitHandle);
+ // anim = WaitAnims[1+num].anim[partner] (table @ 29be:0228)
+ // x = WaitAnims[1+num].x[partner]
+ // y = WaitAnims[1+num].y[partner]
+ // _PlayAnimation(anim, x, y, WaitHandle)
+ // â registers a state-4 (one-shot) animation slot and lets
+ // `_UpdateAnimations` walk the sequence script until 0x80,
+ // then frees this slot and re-activates `WaitHandle`.
+ // Our port renders the partner's idle inline in each redraw rather
+ // than via a slot system, so we play the one-shot synchronously here
+ // (blocking) and resume normal idle rendering when the caller
+ // returns. That matches the user-visible effect: the partner's
+ // gesture (Jenny taking a picture, etc.) finishes before the
+ // speaker portrait + speech balloon appear.
+ //
+ // Six valid kdAnimNum entries (0..5). Verified bytes from
+ // `29be:0228`. Layout per entry: { animJake, animJenny, xJake,
+ // xJenny, yJake, yJenny }. Position is (6, 80) in every entry.
+ static const uint16 kKdAnimTable[6][6] = {
+ { 0x03, 0x0c, 6, 6, 80, 80 }, // 0 â speaker idx 1 wait anim
+ { 0x01, 0x0b, 6, 6, 80, 80 }, // 1 â same as PDA idle
+ { 0x04, 0x0d, 6, 6, 80, 80 }, // 2
+ { 0x02, 0x10, 6, 6, 80, 80 }, // 3 â same as gallery
+ { 0x05, 0x05, 6, 6, 80, 80 }, // 4 â same anim both partners
+ { 0x06, 0x06, 6, 6, 80, 80 }, // 5 â same anim both partners
+ };
+ if (num >= ARRAYSIZE(kKdAnimTable))
+ return;
+
+ const uint partner = (_partner == 0) ? 0 : 1;
+ const uint16 animId = kKdAnimTable[num][partner];
+ const int px = (int)kKdAnimTable[num][2 + partner];
+ const int py = (int)kKdAnimTable[num][4 + partner];
+
+ Animation anim;
+ if (!_aniArchive.loadAnimation(animId, anim) || anim.empty()) {
+ warning("playKdAnim(%u): anim %u failed to load", num, animId);
+ return;
+ }
+
+ // Sequence-script lookup. Entries copied verbatim from
+ // `_AnimationSequences @ 29be:22d4` walked through to the next 0x80.
+ // Each script is a u16[] of frame indices terminated by 0x80; we
+ // don't yet handle 0x81 jumps (none of the kdAnim sequences use
+ // them â verified). seqnum == animId for these calls (per
+ // `_PlayAnimation` 172b:1f5d push order).
+ struct Script {
+ uint16 seqnum;
+ uint8 len;
+ uint8 frames[20]; // long enough for any kdAnim script
+ };
+ static const Script kScripts[] = {
+ // seqnum 1 (29be:188a) â head bob
+ { 0x01, 15, { 0,1,2,0,1,0,2,1,0,1,0,1,2,1,0 } },
+ // seqnum 2 (29be:18aa) â short blip then long pause
+ { 0x02, 16, { 0,1,2,1,0,0,0,0,0,0,0,0,0,0,0,0 } },
+ // seqnum 3 (29be:18e0) â Jake "lift, hold, lower" gesture
+ { 0x03, 9, { 0,1,2,3,2,2,2,1,0 } },
+ // seqnum 4 (29be:18f4) â bigger gesture (camera flash-style)
+ { 0x04, 13, { 0,1,2,3,4,5,4,4,4,3,2,1,0 } },
+ // seqnum 5 (29be:1910) â held idle with a single peak
+ { 0x05, 13, { 0,0,0,1,2,3,2,1,0,0,0,0,0 } },
+ // seqnum 6 (29be:192c) â empty (immediate END)
+ { 0x06, 0, { 0 } },
+ // seqnum 0xb (29be:188a, same as 1) â Jenny PDA idle
+ { 0x0b, 15, { 0,1,2,0,1,0,2,1,0,1,0,1,2,1,0 } },
+ // seqnum 0xc (29be:18e0, same as 3) â Jenny "take a picture"
+ { 0x0c, 9, { 0,1,2,3,2,2,2,1,0 } },
+ // seqnum 0xd (29be:18f4, same as 4) â Jenny big gesture
+ { 0x0d, 13, { 0,1,2,3,4,5,4,4,4,3,2,1,0 } },
+ // seqnum 0x10 (29be:1956) â Jenny short anim
+ { 0x10, 9, { 0,0,0,1,0,0,0,0,0 } },
+ };
+ const uint8 *frames = nullptr;
+ uint frameCount = 0;
+ for (uint i = 0; i < ARRAYSIZE(kScripts); i++) {
+ if (kScripts[i].seqnum == animId) {
+ frames = kScripts[i].frames;
+ frameCount = kScripts[i].len;
+ break;
+ }
+ }
+ if (frameCount == 0) {
+ // Fallback: linear playback through anim cells (better than
+ // nothing if a future kdAnim references an unscripted anim).
+ frameCount = (uint)anim.size();
+ }
+
+ // Erase-source for between-frame redraw. Prefer the partner-less
+ // backdrop the caller stashed via `setPartnerEraseBg` (e.g. the
+ // site's `_bgSnapshot`, which has the static drops + frame but no
+ // partner sprite). Without that, fall back to whatever's currently
+ // on screen â which works for full-screen contexts (PDA / accuse /
+ // briefing) where there is no separate idle partner overlay to
+ // erase, but produces visible "ghosting" against the site's idle
+ // partner cell at (6, 80) because it has the resting pose baked in.
+ Graphics::ManagedSurface bg(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ if (_partnerEraseBg.w == 320 && _partnerEraseBg.h == 200) {
+ for (int row = 0; row < 200; row++) {
+ memcpy((byte *)bg.getBasePtr(0, row),
+ (const byte *)_partnerEraseBg.getBasePtr(0, row), 320);
+ }
+ } else {
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (!screen)
+ return;
+ for (int row = 0; row < 200; row++) {
+ memcpy((byte *)bg.getBasePtr(0, row),
+ (const byte *)screen->getBasePtr(0, row), 320);
+ }
+ g_system->unlockScreen();
+ }
+
+ for (uint i = 0; i < frameCount && !shouldQuit(); i++) {
+ const uint frameIdx = frames ? (uint)frames[i] : i;
+ if (frameIdx >= anim.size())
+ continue;
+ const Picture &fr = anim[frameIdx];
+ const byte transp = (byte)(fr.flags >> 8);
+
+ // Restore BG, then masked-blit the next frame.
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ for (int row = 0; row < 200; row++) {
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)bg.getBasePtr(0, row), 320);
+ }
+ const int w = MIN<int>(fr.surface.w, 320 - px);
+ const int h = MIN<int>(fr.surface.h, 200 - py);
+ for (int row = 0; row < h; row++) {
+ const int dstY = py + row;
+ if (dstY < 0) continue;
+ const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, dstY);
+ for (int col = 0; col < w; col++) {
+ const int dstX = px + col;
+ if (dstX < 0) continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+
+ // One frame per `_CheckFrameRate` tick. The original calibrates
+ // this to ~10 fps; 100 ms matches what the rest of the engine
+ // uses for partner / NPC frame cycling.
+ const uint32 wakeup = g_system->getMillis() + 100;
+ while (g_system->getMillis() < wakeup && !shouldQuit()) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ // Drain events but don't allow skipping mid-animation â
+ // the speaker portrait + balloon haven't been drawn yet
+ // and a click would otherwise eat the upcoming clue.
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return;
+ }
+ g_system->delayMillis(10);
+ }
+ }
+
+ // Restore BG so the next caller (speaker portrait blit) starts clean.
+ g_system->copyRectToScreen(bg.getPixels(), bg.pitch, 0, 0, 320, 200);
+ g_system->updateScreen();
+}
+
bool EEMEngine::getBalloonInsets(uint16 bubNum, uint16 &xInset,
uint16 &yInset, uint16 &textW) const {
// 52-entry, 10-bytes-each balloon-metadata table at `29be:0875`.
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 2bc741352ff..7b5c26a4be7 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -116,6 +116,26 @@ public:
/// ctype-bit-1 (= digit) at `29be:2be1 + char`.
uint16 getKDTextBalloon(byte firstChar) const;
+ /// Play the partner's one-shot reaction animation slot @num. Mirrors
+ /// `_DoKDAnim @ 168d:028a` + `_PlayAnimation @ 172b:1f46`. The
+ /// per-partner (animId, x, y) come from `_WaitAnims[1+num] @ 29be:0228`,
+ /// and the per-frame timing follows the sequence script that the
+ /// original would index at `_AnimationSequences[seqnum=animId]`. Used
+ /// by `displayClue` when a ClueEntry's KD-anim field (+0x3a) is set â
+ /// e.g. Jenny's "take a picture" gesture when the player searches an
+ /// NPC. Blocks until the script's first 0x80 marker.
+ void playKdAnim(uint16 num);
+
+ /// Provide a "clean" 320x200 backdrop for the next `playKdAnim` (and
+ /// any future blocking partner-anim playback) to use as the
+ /// background-erase source. Without this, the camera animation would
+ /// composite on top of the static partner sprite from the screen and
+ /// the previous resting frame would bleed through transparent pixels.
+ /// `SiteScreen` calls this with its `_bgSnapshot` (site BG + static
+ /// drops, no NPCs / partner) before invoking `displayClue` from a
+ /// hotspot click. Pass `nullptr` to clear.
+ void setPartnerEraseBg(const Graphics::ManagedSurface *bg);
+
/// Look up balloon-text-inset metadata. Mirrors the 52-entry table at
/// `29be:0875`, indexed by `(bubNum & 0x7F)`. 10 bytes per entry; only
/// the first 3 fields (x inset, y inset, text width) are used for
@@ -232,6 +252,13 @@ private:
/// inline; we cache the layout to keep click hit-testing simple.
Common::Array<Common::Rect> _notebookSlotRects;
Common::Array<uint> _notebookSlotClues;
+
+ /// Optional clean BG (no partner / NPC sprites) used by `playKdAnim`
+ /// to erase the partner's resting frame between camera-anim cells.
+ /// Empty when no caller has provided one (PDA / case briefing /
+ /// accuse contexts use their own composed backdrops). See
+ /// `setPartnerEraseBg`.
+ Graphics::ManagedSurface _partnerEraseBg;
};
} // End of namespace EEM
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 964f7707eaa..6cd2dd402c8 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -114,8 +114,15 @@ void SiteScreen::enter(uint siteNum) {
const uint16 clueOff = READ_LE_UINT16(idx + 2);
if (clueOff != 0xFFFF) {
const byte *clueBlock = _mystery->blobAt(clueOff);
- if (clueBlock)
+ if (clueBlock) {
+ // See onHotspotClicked â supply a partner-less BG
+ // so KD-anim playback (e.g. the partner's arrival
+ // camera gesture) doesn't ghost over the resting
+ // idle frame.
+ _vm->setPartnerEraseBg(&_bgSnapshot);
_vm->displayClue(clueBlock);
+ _vm->setPartnerEraseBg(nullptr);
+ }
}
}
if (siteNum < Mystery::kVisitedSiteCap)
@@ -893,8 +900,18 @@ void SiteScreen::onHotspotClicked(uint siteNum, uint hotIdx) {
debugC(2, kDebugSite, " hotspot %u -> clue offset 0x%04x",
hotIdx, clueOff);
const byte *clueBlock = _mystery->blobAt(clueOff);
- if (clueBlock)
+ if (clueBlock) {
+ // Hand the engine our partner-less backdrop so that
+ // `_DoKDAnim` / `playKdAnim` (the camera-style reaction
+ // animation that fires when a ClueEntry has +0x3a != -1)
+ // can erase the partner's resting frame between cells
+ // instead of compositing over it. Cleared after the call
+ // so accuse / briefing contexts fall back to their own
+ // snapshots.
+ _vm->setPartnerEraseBg(&_bgSnapshot);
_vm->displayClue(clueBlock);
+ _vm->setPartnerEraseBg(nullptr);
+ }
}
// Caller (`SiteScreen::run`) re-renders the site after this returns.
}
Commit: 30a356bc07e43935d404112d7a87a002c13ab407
https://github.com/scummvm/scummvm/commit/30a356bc07e43935d404112d7a87a002c13ab407
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:39+02:00
Commit Message:
EEM: more parners animations, for intro and overview map
Changed paths:
engines/eem/eem.cpp
engines/eem/site.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index cedef8690b7..594fba894ef 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -551,27 +551,74 @@ void EEMEngine::doChoosePartner() {
}
setAnmPalette(Common::Path("TITLE.ANM"));
- blitAt(background, 0, 0);
- blitAt(girlAnim[0], kGirlX, kGirlY);
- blitAt(boyAnim[0], kBoyX, kBoyY);
- g_system->updateScreen();
+
+ // `_DoHappiness @ 172b:27b5`: the cursor's X column picks one of 4
+ // rects (29be:030f, all full-height); past rect 3 â "level 4".
+ // Each level swaps the partner's sequence script to a more / less
+ // "happy" cycle. Boy seqs at 29be:0337 (5 Ã 0x14 bytes), girl seqs
+ // at 29be:039b. Both cycle through 9 frames (the boy/girl anim
+ // cells contain 10 cells = pairs of "neutral, smile" at increasing
+ // intensity). Lifted verbatim from the binary so the gestures
+ // match the original beat-for-beat.
+ static const Common::Rect kHappyZones[4] = {
+ Common::Rect( 0, 0, 70, 200), // far left â girl very happy, boy neutral
+ Common::Rect( 70, 0, 126, 200), // girl's column
+ Common::Rect(126, 0, 182, 200), // middle
+ Common::Rect(182, 0, 235, 200), // boy's column
+ };
+ static const uint8 kBoySeqs[5][9] = {
+ { 0,0,0,0,0,0,0,1,0 }, // level 0
+ { 2,2,2,2,2,2,2,3,2 }, // level 1
+ { 4,4,4,4,4,4,4,5,4 }, // level 2
+ { 6,6,6,6,6,6,7,6,6 }, // level 3
+ { 8,8,8,8,8,8,8,8,9 }, // level 4 (cursor past zone 3)
+ };
+ static const uint8 kGirlSeqs[5][9] = {
+ { 8,9,8,8,8,8,8,8,8 },
+ { 6,6,6,7,6,6,6,6,6 },
+ { 4,4,5,4,4,4,4,4,4 },
+ { 2,2,2,2,2,2,3,2,2 },
+ { 0,0,0,0,0,1,0,0,0 },
+ };
+ auto happinessLevel = [](int x) {
+ for (uint i = 0; i < ARRAYSIZE(kHappyZones); i++) {
+ if (kHappyZones[i].contains(x, 100))
+ return (uint)i;
+ }
+ return 4u; // past zone 3 â max level
+ };
+
+ int curMouseX = 0xa0; // _DoChoosePartner sets `_SetMousePos(0xa0, 0x96)` on entry
+ int curMouseY = 0x96;
+ uint level = happinessLevel(curMouseX);
+ uint seqIdx = 0; // step within the 9-frame seq
+
+ auto draw = [&]() {
+ blitAt(background, 0, 0);
+ const uint girlFrame = kGirlSeqs[level][seqIdx % 9];
+ const uint boyFrame = kBoySeqs [level][seqIdx % 9];
+ blitAt(girlAnim[girlFrame % girlAnim.size()], kGirlX, kGirlY);
+ blitAt(boyAnim[boyFrame % boyAnim.size()], kBoyX, kBoyY);
+ g_system->updateScreen();
+ };
+ draw();
debugC(1, kDebugGeneral, "ChoosePartner: %u boy frames at (%d,%d), "
"%u girl frames at (%d,%d)",
(uint)boyAnim.size(), kBoyX, kBoyY,
(uint)girlAnim.size(), kGirlX, kGirlY);
- uint frame = 0;
uint32 lastTick = g_system->getMillis();
while (!shouldQuit()) {
- // Advance frame at ~5 Hz so the animations cycle gently.
- if (g_system->getMillis() - lastTick > 200) {
+ // Advance through the 9-frame seq at 100 ms â `_CheckFrameRate`
+ // cadence. The seq is short and loops; matches the original
+ // `_UpdateAnimations` which restarts at curIdx=0 on the 0x80
+ // marker. Mirrors `_DoHappiness`'s rewriting of `curIdx = 0xFFFF`
+ // when the cursor crosses zones (we restart `seqIdx` instead).
+ if (g_system->getMillis() - lastTick > 100) {
lastTick = g_system->getMillis();
- frame++;
- blitAt(background, 0, 0);
- blitAt(girlAnim[frame % girlAnim.size()], kGirlX, kGirlY);
- blitAt(boyAnim[frame % boyAnim.size()], kBoyX, kBoyY);
- g_system->updateScreen();
+ seqIdx = (seqIdx + 1) % 9;
+ draw();
}
Common::Event ev;
@@ -582,6 +629,16 @@ void EEMEngine::doChoosePartner() {
done = true;
break;
}
+ if (ev.type == Common::EVENT_MOUSEMOVE) {
+ curMouseX = ev.mouse.x;
+ curMouseY = ev.mouse.y;
+ const uint newLevel = happinessLevel(curMouseX);
+ if (newLevel != level) {
+ level = newLevel;
+ seqIdx = 0; // restart cycle so the gesture pops
+ draw();
+ }
+ }
if (ev.type == Common::EVENT_LBUTTONDOWN) {
_partner = (ev.mouse.x >= 160) ? 0 : 1;
debugC(1, kDebugGeneral, "Partner picked: %s",
@@ -667,6 +724,19 @@ void EEMEngine::doCaseSelection() {
Picture caseBg;
const bool haveCaseBg = _picsArchive.getPicture(0x41, caseBg);
+ // KD greeter sprite. `_CaseSelection @ 1c33:0a87` (1c33:0b7e-0ba1)
+ // loads anim 0x15 (Jake-paired) or 0x16 (Jenny-paired) and registers
+ // `_NewAnimation(0x112, 0x50, ..., seqnum=0x15, prior=1)` â partner-
+ // dependent because the host KD changes who's "with him" on the
+ // briefing intro frame. Runs continuously through the menu loop via
+ // `_UpdateAnimations`. We approximate with millis-based frame cycling.
+ const uint kKdAniId = (_partner == 0) ? 0x15 : 0x16;
+ Animation kdAnim;
+ const bool haveKdAnim = _aniArchive.loadAnimation(kKdAniId, kdAnim)
+ && !kdAnim.empty();
+ const int kKdAnimX = 0x112;
+ const int kKdAnimY = 0x50;
+
auto draw = [&]() {
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
@@ -679,6 +749,27 @@ void EEMEngine::doCaseSelection() {
(const byte *)caseBg.surface.getBasePtr(0, row), w);
}
}
+
+ // KD greeter frame â masked-blit current animation cell at
+ // (0x112, 0x50). 100 ms tick matches the engine's `_CheckFrameRate`.
+ if (haveKdAnim) {
+ const uint32 now = g_system->getMillis();
+ const uint frameIdx = (uint)((now / 100) % kdAnim.size());
+ const Picture &fr = kdAnim[frameIdx];
+ const byte transp = (byte)(fr.flags >> 8);
+ for (int row = 0; row < fr.surface.h; row++) {
+ const int dstY = kKdAnimY + row;
+ if (dstY < 0 || dstY >= 200) continue;
+ const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, dstY);
+ for (int col = 0; col < fr.surface.w; col++) {
+ const int dstX = kKdAnimX + col;
+ if (dstX < 0 || dstX >= 320) continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ }
if (_font.isLoaded()) {
// `DrawList` @ 1c33:040d coordinates: `_TextBox + 3` for x
// and `DAT_29be_0d02` for y. `_TextBox` @ 29be:0d00 holds
@@ -740,11 +831,19 @@ void EEMEngine::doCaseSelection() {
};
draw();
+ uint32 lastTick = g_system->getMillis();
bool exitChosen = false;
while (!shouldQuit()) {
Common::Event ev;
bool confirmed = false;
+ // Redraw every 100 ms so the KD greeter cycles. Mirrors the
+ // `_CheckFrameRate` cadence in `_CaseSelection`'s main loop.
+ const uint32 now = g_system->getMillis();
+ if (haveKdAnim && now - lastTick >= 100) {
+ lastTick = now;
+ draw();
+ }
while (g_system->getEventManager()->pollEvent(ev)) {
if (ev.type == Common::EVENT_QUIT ||
ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
@@ -2279,6 +2378,22 @@ void EEMEngine::doBigMap() {
// STAGE 1 â Overview: PIC 0x42 + clickable site icons.
// ------------------------------------------------------------------
+ // `_DoBigMap @ 20fe:09e7` (20fe:0a44-0a99) registers a partner sprite
+ // on the overview frame. The animation depends on `_LastScreen`:
+ // * When LastScreen == 2 (came from the site loop) the original
+ // plays an entrance anim (`anum-1` for Jake / Jenny) at
+ // (0x102, 0x50), then on END swaps to the idle anim at (0xfd,
+ // 0x50). We don't track LastScreen finely enough to distinguish,
+ // so we render the IDLE pose at (0xfd, 0x50) which is what the
+ // player sees the rest of the time anyway.
+ // * Idle anim ID: Jake = 0x14 (20), Jenny = 0x12 (18).
+ const uint kMapAniId = (_partner == 0) ? 0x14 : 0x12;
+ Animation mapAnim;
+ const bool haveMapAnim = _aniArchive.loadAnimation(kMapAniId, mapAnim)
+ && !mapAnim.empty();
+ const int kMapAnimX = 0xfd;
+ const int kMapAnimY = 0x50;
+
auto drawOverview = [&]() {
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
@@ -2349,12 +2464,36 @@ void EEMEngine::doBigMap() {
}
}
+ // Partner sprite â masked-blit at (0xfd, 0x50). Same per-tick
+ // idle the original would run via `_UpdateAnimations` once the
+ // entrance one-shot transitions out (see `_DoBigMap` 0xae3-0xae7
+ // where it swaps animId on the 0x80 marker).
+ if (haveMapAnim) {
+ const uint32 now = g_system->getMillis();
+ const uint frameIdx = (uint)((now / 100) % mapAnim.size());
+ const Picture &fr = mapAnim[frameIdx];
+ const byte transp = (byte)(fr.flags >> 8);
+ for (int row = 0; row < fr.surface.h; row++) {
+ const int dstY = kMapAnimY + row;
+ if (dstY < 0 || dstY >= 200) continue;
+ const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, dstY);
+ for (int col = 0; col < fr.surface.w; col++) {
+ const int dstX = kMapAnimX + col;
+ if (dstX < 0 || dstX >= 320) continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ }
+
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
g_system->updateScreen();
};
drawOverview();
+ uint32 mapLastTick = g_system->getMillis();
// Static rectangles read directly from the binary at the labelled
// addresses (29be:0x1596 onwards). Format is {x1, y1, x2, y2}.
@@ -2398,6 +2537,13 @@ void EEMEngine::doBigMap() {
}
if (wantZoom)
break;
+ // Cycle the partner-sprite frame every 100 ms (matching the
+ // original's `_CheckFrameRate` cadence inside `_DoBigMap`).
+ const uint32 now = g_system->getMillis();
+ if (haveMapAnim && now - mapLastTick >= 100) {
+ mapLastTick = now;
+ drawOverview();
+ }
g_system->updateScreen();
g_system->delayMillis(10);
}
@@ -2433,6 +2579,19 @@ void EEMEngine::doBigMap() {
int scrollX = MAX<int>(0, MIN<int>(mapW - kMapWinW, zoomX));
int scrollY = MAX<int>(0, MIN<int>(mapH - kMapWinH, zoomY));
+ // `_DoMapScreen @ 20fe:120b` (20fe:12cd-12f0): partner sprite on
+ // the detail-zoom screen. Jake = anim 0x13 (19), Jenny = anim 0x11
+ // (17). Position (0x101, 0x50) = (257, 80), seqnum 0x13. The cells
+ // here have a "looking at the map" pose, distinct from the BigMap
+ // overview entrance/idle.
+ const uint kDetailAniId = (_partner == 0) ? 0x13 : 0x11;
+ Animation detailAnim;
+ const bool haveDetailAnim = _aniArchive.loadAnimation(kDetailAniId,
+ detailAnim)
+ && !detailAnim.empty();
+ const int kDetailAnimX = 0x101;
+ const int kDetailAnimY = 0x50;
+
auto drawDetail = [&]() {
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
@@ -2493,12 +2652,35 @@ void EEMEngine::doBigMap() {
}
}
+ // Partner sprite on the detail map. Drawn last so it sits over
+ // the frame and the BIGMAP.PIC viewport.
+ if (haveDetailAnim) {
+ const uint32 now = g_system->getMillis();
+ const uint frameIdx =
+ (uint)((now / 100) % detailAnim.size());
+ const Picture &fr = detailAnim[frameIdx];
+ const byte transp = (byte)(fr.flags >> 8);
+ for (int row = 0; row < fr.surface.h; row++) {
+ const int dstY = kDetailAnimY + row;
+ if (dstY < 0 || dstY >= 200) continue;
+ const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, dstY);
+ for (int col = 0; col < fr.surface.w; col++) {
+ const int dstX = kDetailAnimX + col;
+ if (dstX < 0 || dstX >= 320) continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ }
+
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
g_system->updateScreen();
};
drawDetail();
+ uint32 detailLastTick = g_system->getMillis();
while (!shouldQuit()) {
Common::Event ev;
@@ -2619,6 +2801,13 @@ void EEMEngine::doBigMap() {
}
}
}
+ // Cycle the partner sprite at 100 ms ticks (same cadence as
+ // `_DoMapScreen`'s `_CheckFrameRate` + `_UpdateAnimations` loop).
+ const uint32 now = g_system->getMillis();
+ if (haveDetailAnim && now - detailLastTick >= 100) {
+ detailLastTick = now;
+ dirty = true;
+ }
if (dirty)
drawDetail();
g_system->updateScreen();
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 6cd2dd402c8..8e119ab07f3 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -653,9 +653,12 @@ void SiteScreen::renderPartner(uint siteNum, uint32 tickMs) {
// +0..1 anim Jake, +2..3 anim Jenny,
// +4..5 x Jake, +6..7 x Jenny,
// +8..9 y Jake, +10..11 y Jenny.
- // Verbatim copy of the bytes Ghidra dumped at 29be:021c so we
- // don't need to ship the original data segment.
- static const uint16 kWaitAnims[][6] = {
+ // Seven valid entries verified against the bytes at 29be:021c.
+ // Anything past entry 6 in the binary is `_SiteButtons` rect data
+ // that follows the table in memory â NOT continuation entries â
+ // so we cap the table here and skip rendering for siteData[+8] >= 7
+ // (which would indicate corrupt mystery data anyway).
+ static const uint16 kWaitAnims[7][6] = {
{ 0x00, 0x0a, 0x06, 0x06, 0x50, 0x50 }, // 0
{ 0x03, 0x0c, 0x06, 0x06, 0x50, 0x50 }, // 1
{ 0x01, 0x0b, 0x06, 0x06, 0x50, 0x50 }, // 2
@@ -663,17 +666,17 @@ void SiteScreen::renderPartner(uint siteNum, uint32 tickMs) {
{ 0x02, 0x10, 0x06, 0x06, 0x50, 0x50 }, // 4
{ 0x05, 0x05, 0x06, 0x06, 0x50, 0x50 }, // 5
{ 0x06, 0x06, 0x06, 0x06, 0x50, 0x50 }, // 6
- { 0x00, 0x00, 0x23, 0x6f, 0x38, 0x88 }, // 7 â special pos
- { 0x07, 0xb1, 0x39, 0xc8, 0x88, 0xae }, // 8 â likely junk; 0xb1 anim id is suspect
- { 0x9d, 0xbe, 0xa3, 0xae, 0xb8, 0xbe } // 9 â likely junk
};
const byte *site = _mystery->siteData(siteNum);
if (!site)
return;
const uint16 speaker = READ_LE_UINT16(site + 8);
- if (speaker >= ARRAYSIZE(kWaitAnims))
+ if (speaker >= ARRAYSIZE(kWaitAnims)) {
+ warning("renderPartner: site %u has speakerIdx=%u out of range",
+ siteNum, speaker);
return;
+ }
const uint8 partner = _vm->getPartnerIndex();
const uint animId = kWaitAnims[speaker][0 + partner];
Commit: 0f7bf385b7d57e6e4e98df854fbfb483c84640ef
https://github.com/scummvm/scummvm/commit/0f7bf385b7d57e6e4e98df854fbfb483c84640ef
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:40+02:00
Commit Message:
EEM: refactored the code following the original symbols
Changed paths:
A engines/eem/clues.cpp
A engines/eem/graphics.cpp
A engines/eem/ui.cpp
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/module.mk
engines/eem/site.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
new file mode 100644
index 00000000000..a12136c304f
--- /dev/null
+++ b/engines/eem/clues.cpp
@@ -0,0 +1,784 @@
+/* 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/debug.h"
+#include "common/events.h"
+#include "common/file.h"
+#include "common/path.h"
+#include "common/savefile.h"
+#include "common/system.h"
+#include "common/textconsole.h"
+
+#include "graphics/cursorman.h"
+#include "graphics/managed_surface.h"
+
+#include "eem/detection.h"
+#include "eem/eem.h"
+
+// EEM â clue / briefing pipeline (SCRIPT.C clue parts + KD.C briefing parts).
+// Everything that drives a `ClueBlock` through the displayClue / portrait /
+// balloon / notebook-side-effect flow plus the case-briefing intro that
+// preambles into the same flow.
+
+namespace EEM {
+
+namespace {
+// Picture / animation IDs verified against `_DoChoosePartner @ 1a35:0756`.
+const uint kPicChooseBackground = 0x8c; ///< `_GetBackground(0x8c)`
+const uint kAniBoy = 8; ///< `_GetAnimation(8)` (Jake)
+const uint kAniGirl = 9; ///< `_GetAnimation(9)` (Jenny)
+
+// On-screen positions verified from `_NewAnimation` calls @ 1a35:07b9 / 07d5.
+const int kBoyX = 0xe2; // 226
+const int kBoyY = 0x62; // 98
+const int kGirlX = 0x42; // 66
+const int kGirlY = 0x60; // 96
+} // anonymous namespace
+
+void EEMEngine::doChoosePartner() {
+ // Mirrors _DoChoosePartner @ 1a35:0756. The original places boy + girl
+ // animations on a backdrop and polls four click rectangles (two per
+ // character) for the player's choice. We approximate by splitting the
+ // screen at x=160: left half = girl (Jenny), right half = boy (Jake).
+ Picture background;
+ if (!_picsArchive.getPicture(kPicChooseBackground, background)) {
+ warning("ChoosePartner background (%u) load failed", kPicChooseBackground);
+ return;
+ }
+
+ Animation boyAnim;
+ if (!_aniArchive.loadAnimation(kAniBoy, boyAnim) || boyAnim.empty()) {
+ warning("Boy animation (%u) load failed", kAniBoy);
+ return;
+ }
+ Animation girlAnim;
+ if (!_aniArchive.loadAnimation(kAniGirl, girlAnim) || girlAnim.empty()) {
+ warning("Girl animation (%u) load failed", kAniGirl);
+ return;
+ }
+
+ setAnmPalette(Common::Path("TITLE.ANM"));
+
+ // `_DoHappiness @ 172b:27b5`: the cursor's X column picks one of 4
+ // rects (29be:030f, all full-height); past rect 3 â "level 4".
+ // Each level swaps the partner's sequence script to a more / less
+ // "happy" cycle. Boy seqs at 29be:0337 (5 Ã 0x14 bytes), girl seqs
+ // at 29be:039b. Both cycle through 9 frames (the boy/girl anim
+ // cells contain 10 cells = pairs of "neutral, smile" at increasing
+ // intensity). Lifted verbatim from the binary so the gestures
+ // match the original beat-for-beat.
+ static const Common::Rect kHappyZones[4] = {
+ Common::Rect( 0, 0, 70, 200), // far left â girl very happy, boy neutral
+ Common::Rect( 70, 0, 126, 200), // girl's column
+ Common::Rect(126, 0, 182, 200), // middle
+ Common::Rect(182, 0, 235, 200), // boy's column
+ };
+ static const uint8 kBoySeqs[5][9] = {
+ { 0,0,0,0,0,0,0,1,0 }, // level 0
+ { 2,2,2,2,2,2,2,3,2 }, // level 1
+ { 4,4,4,4,4,4,4,5,4 }, // level 2
+ { 6,6,6,6,6,6,7,6,6 }, // level 3
+ { 8,8,8,8,8,8,8,8,9 }, // level 4 (cursor past zone 3)
+ };
+ static const uint8 kGirlSeqs[5][9] = {
+ { 8,9,8,8,8,8,8,8,8 },
+ { 6,6,6,7,6,6,6,6,6 },
+ { 4,4,5,4,4,4,4,4,4 },
+ { 2,2,2,2,2,2,3,2,2 },
+ { 0,0,0,0,0,1,0,0,0 },
+ };
+ auto happinessLevel = [](int x) {
+ for (uint i = 0; i < ARRAYSIZE(kHappyZones); i++) {
+ if (kHappyZones[i].contains(x, 100))
+ return (uint)i;
+ }
+ return 4u; // past zone 3 â max level
+ };
+
+ // `_DoChoosePartner` opens with `_SetMousePos(0xa0, 0x96)` so the
+ // cursor lands centred between the two partners â start the
+ // happiness level from that initial X.
+ int curMouseX = 0xa0;
+ uint level = happinessLevel(curMouseX);
+ uint seqIdx = 0; // step within the 9-frame seq
+
+ auto draw = [&]() {
+ blitAt(background, 0, 0);
+ const uint girlFrame = kGirlSeqs[level][seqIdx % 9];
+ const uint boyFrame = kBoySeqs [level][seqIdx % 9];
+ blitAt(girlAnim[girlFrame % girlAnim.size()], kGirlX, kGirlY);
+ blitAt(boyAnim[boyFrame % boyAnim.size()], kBoyX, kBoyY);
+ g_system->updateScreen();
+ };
+ draw();
+
+ debugC(1, kDebugGeneral, "ChoosePartner: %u boy frames at (%d,%d), "
+ "%u girl frames at (%d,%d)",
+ (uint)boyAnim.size(), kBoyX, kBoyY,
+ (uint)girlAnim.size(), kGirlX, kGirlY);
+
+ uint32 lastTick = g_system->getMillis();
+ while (!shouldQuit()) {
+ // Advance through the 9-frame seq at 100 ms â `_CheckFrameRate`
+ // cadence. The seq is short and loops; matches the original
+ // `_UpdateAnimations` which restarts at curIdx=0 on the 0x80
+ // marker. Mirrors `_DoHappiness`'s rewriting of `curIdx = 0xFFFF`
+ // when the cursor crosses zones (we restart `seqIdx` instead).
+ if (g_system->getMillis() - lastTick > 100) {
+ lastTick = g_system->getMillis();
+ seqIdx = (seqIdx + 1) % 9;
+ draw();
+ }
+
+ Common::Event ev;
+ bool done = false;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ done = true;
+ break;
+ }
+ if (ev.type == Common::EVENT_MOUSEMOVE) {
+ curMouseX = ev.mouse.x;
+ const uint newLevel = happinessLevel(curMouseX);
+ if (newLevel != level) {
+ level = newLevel;
+ seqIdx = 0; // restart cycle so the gesture pops
+ draw();
+ }
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ _partner = (ev.mouse.x >= 160) ? 0 : 1;
+ debugC(1, kDebugGeneral, "Partner picked: %s",
+ _partner == 0 ? "Jake" : "Jennifer");
+ done = true;
+ break;
+ }
+ if (ev.type == Common::EVENT_KEYDOWN) {
+ if (ev.kbd.keycode == Common::KEYCODE_LEFT) {
+ _partner = 1; done = true; break;
+ }
+ if (ev.kbd.keycode == Common::KEYCODE_RIGHT) {
+ _partner = 0; done = true; break;
+ }
+ if (ev.kbd.keycode == Common::KEYCODE_RETURN ||
+ ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ done = true; break;
+ }
+ }
+ }
+ if (done)
+ break;
+ g_system->updateScreen();
+ g_system->delayMillis(20);
+ }
+}
+
+void EEMEngine::doInitClues() {
+ // Mirrors `_DoInitClues` @ 1a35:0411. The original does:
+ // 1. _AllBlack(); _GetBackground(0x52); _GetPalette(0x22);
+ // 2. _GetAnimation(gameAni); _NewAnimation(0xcd, 0x6c, ...)
+ // _GetAnimation(bookAni); _NewAnimation(0, 99, ...)
+ // (case type 1 also: _NewAnimation(0x68, 0x8b, nancyAni))
+ // 3. _UpdateAnimations(); _FadeIn();
+ // 4. while (frame != gameNum) { _CheckFrameRate(); _UpdateAnimations(); }
+ // â cycles through the entire game animation once. Click skips.
+ // 5. _PlayInSequence(seqId, ...) â plays a follow-up sequence based
+ // on partner + case type.
+ // 6. _DisplayClue(InitBlock + 2, 1) â the briefing dialogue.
+ // 7. _OnSites[startSite] = 1.
+ //
+ // gameAni / bookAni / nancyAni values verified directly from Ghidra:
+ // gameAni = 0x17 (Jake) / 0x3b (Jenny)
+ // bookAni = 0x18 (Jake) / 0x3c (Jenny)
+ // nancyAni = 0x19 (case type 1 only)
+ if (!_mystery.isLoaded())
+ return;
+
+ const byte *ib = _mystery.initBlock();
+ if (!ib)
+ return;
+
+ const uint16 startSite = READ_LE_UINT16(ib + 2);
+ if (startSite < Mystery::kVisitedSiteCap)
+ _mystery._onSites[startSite] = 1;
+ _mystery._siteNumber = startSite;
+ _mystery._lastSite = startSite;
+
+ setSitePalette(0x22);
+ Picture bg;
+ if (_picsArchive.getPicture(0x52, bg))
+ blitAt(bg, 0, 0);
+
+ const uint gameAni = _partner == 0 ? 0x17 : 0x3b;
+ const uint bookAni = _partner == 0 ? 0x18 : 0x3c;
+ Animation game, book, nancy;
+ const bool haveGame = _aniArchive.loadAnimation(gameAni, game) && !game.empty();
+ const bool haveBook = _aniArchive.loadAnimation(bookAni, book) && !book.empty();
+
+ const uint16 caseType = READ_LE_UINT16(ib);
+ const bool haveNancy = (caseType == 1)
+ && _aniArchive.loadAnimation(0x19, nancy)
+ && !nancy.empty();
+
+ auto blitMaskedAt = [&](const Picture &p, int x, int y) {
+ const byte transp = (byte)(p.flags >> 8);
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (!screen) return;
+ for (int row = 0; row < p.surface.h; row++) {
+ const int dstY = y + row;
+ if (dstY < 0 || dstY >= screen->h) continue;
+ const byte *src = (const byte *)p.surface.getBasePtr(0, row);
+ byte *dst = (byte *)screen->getBasePtr(0, dstY);
+ for (int col = 0; col < p.surface.w; col++) {
+ const int dstX = x + col;
+ if (dstX < 0 || dstX >= screen->w) continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ g_system->unlockScreen();
+ };
+
+ // Step 4 â cycle through the game animation once before the briefing.
+ // Mirrors the `while (uVar9 != gameNum)` loop. The original calls
+ // `_UpdateAnimations` per `_CheckFrameRate` tick (~10 fps). We use
+ // 100 ms ticks for the same cadence. Click / key skips.
+ if (haveGame || haveBook || haveNancy) {
+ const uint frameCount = haveGame ? game.size() : 8;
+ bool skip = false;
+ for (uint frame = 0; frame < frameCount && !shouldQuit() && !skip; frame++) {
+ // Restore BG + advance frame.
+ if (_picsArchive.getPicture(0x52, bg))
+ blitAt(bg, 0, 0);
+ if (haveGame)
+ blitMaskedAt(game[frame % game.size()], 0xcd, 0x6c);
+ if (haveBook)
+ blitMaskedAt(book[frame % book.size()], 0, 99);
+ if (haveNancy)
+ blitMaskedAt(nancy[frame % nancy.size()], 0x68, 0x8b);
+ g_system->updateScreen();
+
+ // Wait 100 ms or until input.
+ const uint32 wakeup = g_system->getMillis() + 100;
+ while (g_system->getMillis() < wakeup && !shouldQuit() && !skip) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_LBUTTONDOWN ||
+ ev.type == Common::EVENT_KEYDOWN) {
+ skip = true;
+ break;
+ }
+ }
+ g_system->delayMillis(10);
+ }
+ }
+ }
+
+ // Composite the final frames (or first frames if skipped) so the BG
+ // is in a sensible state when displayClue overlays the speaker.
+ if (_picsArchive.getPicture(0x52, bg))
+ blitAt(bg, 0, 0);
+ if (haveGame)
+ blitMaskedAt(game[0], 0xcd, 0x6c);
+ if (haveBook)
+ blitMaskedAt(book[0], 0, 99);
+ if (haveNancy)
+ blitMaskedAt(nancy[0], 0x68, 0x8b);
+ g_system->updateScreen();
+
+ // Step 5 â `_PlayInSequence(animSeq, 0xcd, animY)` per Ghidra:
+ // Jake (partner=0):
+ // caseType=1 â anim 0x38 at (0xcd, 0x6d)
+ // caseType=2 â anim 0x37 at (0xcd, 0x6c)
+ // caseType=3 â anim 0x39 at (0xcd, 0x6c)
+ // Jenny (partner=1):
+ // caseType=2 â anim 0x3a at (0xcd, 0x6c)
+ // caseType=3 â anim 0x3d at (0xcd, 0x6c)
+ // `_PlayInSequence @ 172b:2d03` plays each frame at (sx-w, sy-rowoff)
+ // with mask blit, advancing one frame per `_CheckFrameRate` tick.
+ uint16 seqAni = 0xFFFF;
+ uint16 seqY = 0x6c;
+ if (_partner == 0) {
+ switch (caseType) {
+ case 1: seqAni = 0x38; seqY = 0x6d; break;
+ case 2: seqAni = 0x37; seqY = 0x6c; break;
+ case 3: seqAni = 0x39; seqY = 0x6c; break;
+ default: break;
+ }
+ } else {
+ switch (caseType) {
+ case 2: seqAni = 0x3a; seqY = 0x6c; break;
+ case 3: seqAni = 0x3d; seqY = 0x6c; break;
+ default: break;
+ }
+ }
+ if (seqAni != 0xFFFF) {
+ Animation seq;
+ if (_aniArchive.loadAnimation(seqAni, seq) && !seq.empty()) {
+ bool skip = false;
+ for (uint frame = 0; frame < seq.size() && !shouldQuit() && !skip;
+ frame++) {
+ const Picture &fr = seq[frame];
+ // Restore BG + base anim frames so each new frame
+ // composites cleanly.
+ if (_picsArchive.getPicture(0x52, bg))
+ blitAt(bg, 0, 0);
+ if (haveGame)
+ blitMaskedAt(game[frame % game.size()], 0xcd, 0x6c);
+ if (haveBook)
+ blitMaskedAt(book[frame % book.size()], 0, 99);
+ if (haveNancy)
+ blitMaskedAt(nancy[frame % nancy.size()], 0x68, 0x8b);
+ // Anchor: original blits at `(sx - frame.width,
+ // sy - frame.rowoff)`. `frame.rowoff` is the y-anchor
+ // in our PicData. We use width/height directly since
+ // loadAnimation places anchor at (0, 0).
+ const int dstX = (int)0xcd - (int)fr.surface.w;
+ const int dstY = (int)seqY - (int)fr.rowoff;
+ blitMaskedAt(fr, dstX, dstY);
+ g_system->updateScreen();
+ const uint32 wakeup = g_system->getMillis() + 100;
+ while (g_system->getMillis() < wakeup &&
+ !shouldQuit() && !skip) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_LBUTTONDOWN ||
+ ev.type == Common::EVENT_KEYDOWN) {
+ skip = true;
+ break;
+ }
+ }
+ g_system->delayMillis(10);
+ }
+ }
+ }
+ }
+
+ // Step 6 â case briefing dialogue.
+ displayClue(ib + 4);
+}
+
+/// Mirror `_ParseString` @ 1b66:07c3 â substitute the control bytes that
+/// the original engine uses as placeholders. Only the two we encounter most
+/// often (player name = 0x80, partner first name = 0x82) are substituted;
+/// other 0x8N opcodes are stripped. The original engine also handles
+/// hyphenation marks and a hint placeholder (0x89) we ignore for now.
+Common::String EEMEngine::parseString(const Common::String &raw,
+ const Common::String &playerName,
+ uint partner) const {
+ // Substitution opcodes from `_ParseString` @ 1b66:07c3, jump-table
+ // at 1b66:0cbe. Each handler reads `_Partner` (16-bit at 0x7918)
+ // and indexes the name table at 29be:0c28 ({Jake, Jennifer, he,
+ // she, him, her, his} as far pointers).
+ // 0x80 â player's typed name (auto-cap word starts) â uses _PlayerRecord
+ // 0x81 â _Partner == 0 ? "Jake" : "Jennifer" (chosen detective)
+ // 0x82 â _Partner == 0 ? "Jennifer" : "Jake" (the OTHER one)
+ // 0x83 â _Partner == 0 ? "he" : "she"
+ // 0x84 â _Partner == 0 ? "him" : "her"
+ // 0x85 â _Partner == 0 ? "his" : "her"
+ // 0x86..0x88 read a different gender flag at 0x7985 â left alone
+ // until that flag's source is traced.
+ // 0x89 â KD hint placeholder (handled by caller).
+ const bool isJake = (partner == 0);
+ Common::String out;
+ for (uint i = 0; i < raw.size(); i++) {
+ const byte c = (byte)raw[i];
+ switch (c) {
+ case 0x80:
+ out += playerName;
+ break;
+ case 0x81:
+ out += isJake ? "Jake" : "Jennifer";
+ break;
+ case 0x82:
+ out += isJake ? "Jennifer" : "Jake";
+ break;
+ case 0x83:
+ out += isJake ? "he" : "she";
+ break;
+ case 0x84:
+ out += isJake ? "him" : "her";
+ break;
+ case 0x85:
+ out += isJake ? "his" : "her";
+ break;
+ case 0x86:
+ case 0x87:
+ case 0x88:
+ case 0x89:
+ // Eaten silently â see comment above.
+ break;
+ case 0:
+ return out;
+ case '\r':
+ break;
+ default:
+ out += (char)c;
+ break;
+ }
+ }
+ return out;
+}
+
+void EEMEngine::applyClueSideEffects(const byte *c) {
+ for (uint j = 0; j < 5; j++) {
+ const uint16 note = READ_LE_UINT16(c + 0x30 + j * 2);
+ if (note != 0xFFFF && note < Mystery::kCluesFoundCap)
+ _mystery._cluesFound[note] = 1;
+
+ const uint16 galIdx = READ_LE_UINT16(c + 0x26 + j * 2);
+ if (galIdx != 0xFFFF && galIdx < Mystery::kGalleryCap) {
+ const uint8 phys = _mystery._newOrder[galIdx];
+ if (phys < Mystery::kGalleryCap)
+ _mystery._inGallery[phys] = 1;
+ }
+
+ const uint16 siteIdx = READ_LE_UINT16(c + 0x1c + j * 2);
+ if (siteIdx != 0xFFFF) {
+ const uint16 siteVal = siteIdx & 0x7FFF;
+ if (siteVal < Mystery::kVisitedSiteCap)
+ _mystery._onSites[siteVal] = 1;
+ if (siteIdx & 0x8000)
+ _mystery._sawCONSITEs = true;
+ }
+ }
+}
+
+void EEMEngine::displayClue(const byte *clueBlock) {
+ if (!clueBlock || !_mystery.isLoaded())
+ return;
+
+ // ClueBlock layout (verified against M0.BIN):
+ // +0..1: number (entry count)
+ // +2..3: pic ID for entry 0 (entry N>0 uses prev entry's last 2 bytes)
+ // +4..: array of 62-byte entries
+ const uint16 number = READ_LE_UINT16(clueBlock);
+ debugC(1, kDebugScript, "displayClue: %u entries", number);
+ if (number == 0 || number > 32) {
+ // number==0 = no briefing (e.g. mystery 0 case-type 4); >32 is a
+ // guard against bad pointers.
+ return;
+ }
+
+ // Snapshot the current screen as the BG so character pics from
+ // earlier entries don't stack on top of each other.
+ Graphics::ManagedSurface bg(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ bg.clear();
+ {
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (screen) {
+ for (int row = 0; row < 200; row++) {
+ memcpy((byte *)bg.getBasePtr(0, row),
+ (const byte *)screen->getBasePtr(0, row), 320);
+ }
+ g_system->unlockScreen();
+ }
+ }
+
+ for (uint i = 0; i < number && !shouldQuit(); i++) {
+ // Restore BG before drawing this entry's portrait + balloon.
+ g_system->copyRectToScreen(bg.getPixels(), bg.pitch, 0, 0, 320, 200);
+ const byte *c = clueBlock + 4 + i * 62;
+ // Per-partner fields:
+ // +0..1, +2..3: tx, ty (partner 0)
+ // +4..5, +6..7: tx, ty (partner 1)
+ // +8..9, +10..11: bubText offset for partner 0/1 (rel. TextBlock)
+ // +12..13, +14..15: balloon picture ID for partner 0/1
+ // +16..17, +18..19: bubX, bubY
+ // +0x3a..+0x3b: KD-anim number (-1 = none)
+ // Per `_DisplayClue` @ 2404:05e6: partner 1 uses its own field
+ // set ONLY when bubText1 is not -1; otherwise it falls back to
+ // the partner 0 fields entirely. Partner 0 always uses field 0.
+
+ // Per-clue partner reaction animation. `_DisplayClue` @
+ // 2404:0635-064b checks `clueEntry[+0x3a]` and, when not -1,
+ // calls `_DoKDAnim(num)` BEFORE drawing the speaker portrait.
+ // This is what surfaces "Jenny takes a picture with a camera"
+ // (and the matching Jake gestures) during NPC searches.
+ const int16 kdAnimNum = (int16)READ_LE_UINT16(c + 0x3a);
+ if (kdAnimNum != -1)
+ playKdAnim((uint16)kdAnimNum);
+
+ const bool useP1 = (_partner == 1) &&
+ (READ_LE_UINT16(c + 10) != 0xFFFF);
+ const uint partner = useP1 ? 1 : 0;
+ const uint16 textOff = READ_LE_UINT16(c + 8 + partner * 2);
+ const bool hasText = (textOff != 0xFFFF);
+ // Partner 1 bubX/bubY at +0x14/+0x16; partner 0 at +0x10/+0x12.
+ const uint16 bubX = READ_LE_UINT16(c + (useP1 ? 0x14 : 0x10));
+ const uint16 bubY = READ_LE_UINT16(c + (useP1 ? 0x16 : 0x12));
+ const uint16 bubNum = READ_LE_UINT16(c + (useP1 ? 0x0E : 0x0C));
+ const char *raw = hasText ? _mystery.textAt(textOff) : "";
+
+ // Speaker portrait. Mirrors `_DisplayClue`'s `pic[clues+i*62-2]`:
+ // for entry 0 the pic ID is in the ClueBlock header at +2; for
+ // later entries it sits in the previous entry's last 2 bytes.
+ // Speaker portrait position uses partner 0 fields (+0..+3) when
+ // _partner==0 or when partner 1 falls back; otherwise partner 1
+ // fields (+4..+7). Same logic as the original.
+ const uint16 charX = READ_LE_UINT16(c + (useP1 ? 4 : 0));
+ const uint16 charY = READ_LE_UINT16(c + (useP1 ? 6 : 2));
+ const uint16 charPicId = (i == 0)
+ ? READ_LE_UINT16(clueBlock + 2)
+ : READ_LE_UINT16(c - 2);
+ if (charPicId != 0 && charPicId != 0xFFFF) {
+ Picture charPic;
+ if (_picsArchive.getPicture(charPicId, charPic) &&
+ charX < 320 && charY < 200) {
+ const int w = MIN<int>(charPic.surface.w, 320 - charX);
+ const int h = MIN<int>(charPic.surface.h, 200 - charY);
+ if (w > 0 && h > 0)
+ g_system->copyRectToScreen(charPic.surface.getPixels(),
+ charPic.surface.pitch, charX, charY, w, h);
+ }
+ }
+
+ // Substitute control bytes (0x80..0x89) â see `parseString` for
+ // the table. 0x81 = chosen detective, 0x82 = the other one.
+ const Common::String text = parseString(raw ? raw : "",
+ _playerName, _partner);
+
+ // Speech balloon. Mirrors `_GetBalloon` + `_AddPicBackground` in
+ // `_DisplayClue`. The original looks up per-balloon text-area
+ // metadata in a table at offset 0x875 (within `_DisplayClue`'s
+ // segment); we don't have that table decoded yet, so we use a
+ // fixed inset of 8 px from the balloon's top-left.
+ Picture balloon;
+ const uint16 balloonId = bubNum & 0x7F;
+ const bool haveBalloon = bubNum != 0xFFFF &&
+ _balloonArchive.size() > balloonId &&
+ _balloonArchive.loadEntry(balloonId, balloon);
+
+ if (_font.isLoaded() && !text.empty()) {
+ // Snapshot the current screen, overlay balloon + text, then
+ // copy the changed band back. This preserves the site BG
+ // underneath unchanged regions.
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (!screen) break;
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ for (int row = 0; row < 200; row++) {
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)screen->getBasePtr(0, row), 320);
+ }
+ g_system->unlockScreen();
+
+ int textX = bubX;
+ int textY = bubY;
+ int textW = MIN<int>(320 - bubX, 200);
+ int copyY = bubY;
+ int copyH = _font.getFontHeight() * 4 + 8;
+
+ if (haveBalloon) {
+ const int bw = MIN<int>(balloon.surface.w, 320 - bubX);
+ const int bh = MIN<int>(balloon.surface.h, 200 - bubY);
+ // `_AddPicBackground` passes `pic->miscflags >> 8` as
+ // the transparent colour to `_Rect_Move_Mask`. The
+ // on-disk u16 at file offset 0 maps to `Picture::flags`.
+ const byte transp = (byte)(balloon.flags >> 8);
+ // `_GetBalloon @ 172b:1d7d` mirrors the picture horizontally
+ // when `(bubNum & 0x80)` is set â used for right-side
+ // speakers so the tail points the other way.
+ const bool flipBalloon = (bubNum & 0x80) != 0;
+ if (bw > 0 && bh > 0) {
+ for (int row = 0; row < bh; row++) {
+ const byte *src =
+ (const byte *)balloon.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(bubX, bubY + row);
+ for (int col = 0; col < bw; col++) {
+ const int srcCol = flipBalloon
+ ? (balloon.surface.w - 1 - col)
+ : col;
+ const byte px = src[srcCol];
+ if (px != transp)
+ dst[col] = px;
+ }
+ }
+ }
+ // Per-balloon metadata table verified from 29be:0875 â
+ // 10-byte entries indexed by `(bubNum & 0x7f)`. Layout:
+ // +0..1 textX inset, +2..3 textY inset, +4..5 width,
+ // +6..7 height, +8..9 tail offset.
+ // 52 entries total; insets vary (3, 5, 6, or 8 px).
+ // The original `_DisplayClue` does:
+ // _WordWrap(bubX + table[bubNum].x, bubY + table[bubNum].y,
+ // table[bubNum].w, ...);
+ static const struct { uint16 x, y, w; } kBalloonTable[] = {
+ { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 },
+ { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 },
+ { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 },
+ { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 },
+ { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 },
+ { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 },
+ { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 },
+ { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 },
+ { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 },
+ { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 },
+ { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 },
+ { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 },
+ { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 },
+ { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 },
+ { 5, 8, 158 }, { 5, 8, 176 }, { 8, 7, 142 }
+ };
+ const uint kBalloonTableSize = sizeof(kBalloonTable) /
+ sizeof(kBalloonTable[0]);
+ const uint balloonIdx = balloonId < kBalloonTableSize
+ ? balloonId : 0;
+ const auto &bm = kBalloonTable[balloonIdx];
+ textX = bubX + bm.x;
+ textY = bubY + bm.y;
+ textW = bm.w;
+ copyH = bh;
+ } else {
+ // No balloon â clear a band so old pixels don't bleed.
+ const Common::Rect band(0, bubY, 320,
+ MIN<int>(bubY + copyH, 200));
+ scratch.fillRect(band, 0);
+ copyY = bubY;
+ }
+
+ // `_DisplayClue` @ 2404:07fe passes fontColor=0 (palette
+ // index 0 of the case-briefing palette 0x22) to `_WordWrap`.
+ // Hard-coding 0xF here gave the wrong colour.
+ _font.drawWordWrapped(&scratch, textX, textY,
+ MAX<int>(8, textW), text, 0);
+
+ g_system->copyRectToScreen(scratch.getBasePtr(0, copyY),
+ scratch.pitch, 0, copyY, 320,
+ MIN<int>(copyH, 200 - copyY));
+ g_system->updateScreen();
+ }
+
+ // Wait for click/key to advance â only if we drew something.
+ // ESC skips the entire dialogue rather than just one entry.
+ if (hasText || (charPicId != 0 && charPicId != 0xFFFF)) {
+ bool advance = false;
+ bool skipAll = false;
+ while (!advance && !shouldQuit()) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ advance = true;
+ break;
+ }
+ if (ev.type == Common::EVENT_KEYDOWN &&
+ ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ advance = true;
+ skipAll = true;
+ break;
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN ||
+ ev.type == Common::EVENT_KEYDOWN) {
+ advance = true;
+ break;
+ }
+ }
+ // Tick the screen so the OSystem cursor follows the
+ // mouse â ScummVM redraws the cursor overlay only on
+ // updateScreen.
+ g_system->updateScreen();
+ g_system->delayMillis(10);
+ }
+ if (skipAll) {
+ // Apply remaining side-effects without rendering. The
+ // original silently runs the state updates even when the
+ // player skips ahead.
+ for (uint k = i; k < number; k++)
+ applyClueSideEffects(clueBlock + 4 + k * 62);
+ return;
+ }
+ }
+
+ applyClueSideEffects(c);
+ }
+}
+
+bool EEMEngine::areYouSure() {
+ // Mirrors `_AreYouSure` @ 1a35:0a5c. Original loads PIC 0x136 for the
+ // dialog body and PIC 0x1FD/0x1FE for YES/NO. We render a minimal
+ // text dialog that preserves the screen behind it.
+ if (!_font.isLoaded())
+ return true;
+
+ Graphics::Surface *screen = g_system->lockScreen();
+ Graphics::ManagedSurface saved(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ if (screen) {
+ for (int row = 0; row < 200; row++) {
+ memcpy((byte *)saved.getBasePtr(0, row),
+ (const byte *)screen->getBasePtr(0, row), 320);
+ }
+ g_system->unlockScreen();
+ }
+
+ const Common::Rect dlg(60, 70, 260, 140);
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ for (int row = 0; row < 200; row++)
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)saved.getBasePtr(0, row), 320);
+ scratch.fillRect(dlg, 0);
+ scratch.frameRect(dlg, 0xF);
+ _font.drawString(&scratch, "Are you sure you want to quit?", dlg.left + 8, dlg.top + 8, 320, 0xF);
+ _font.drawString(&scratch, "Y - Yes", dlg.left + 16, dlg.top + 36, 320, 0xF);
+ _font.drawString(&scratch, "N - No", dlg.left + 100, dlg.top + 36, 320, 0xF);
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+
+ bool result = false;
+ while (!shouldQuit()) {
+ Common::Event ev;
+ bool decided = false;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ result = true;
+ decided = true;
+ break;
+ }
+ if (ev.type == Common::EVENT_KEYDOWN) {
+ if (ev.kbd.keycode == Common::KEYCODE_y ||
+ ev.kbd.keycode == Common::KEYCODE_RETURN) {
+ result = true; decided = true; break;
+ }
+ if (ev.kbd.keycode == Common::KEYCODE_n ||
+ ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ result = false; decided = true; break;
+ }
+ }
+ }
+ if (decided)
+ break;
+ g_system->updateScreen();
+ g_system->delayMillis(15);
+ }
+
+ // Restore the screen so the caller's UI is intact.
+ g_system->copyRectToScreen(saved.getPixels(), saved.pitch, 0, 0, 320, 200);
+ g_system->updateScreen();
+ return result;
+}
+
+} // End of namespace EEM
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 594fba894ef..cdbc7e4ac37 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -52,20 +52,12 @@ const uint kNumSitePals = 40; ///< SITEPALS holds 40 palettes (40 * 768 = 30720
// Picture / palette IDs from the original code (1-based picture IDs).
const uint kPicEAKidsLogo = 0x54; ///< _ShowEAKids: GetPicture(0x54)
const uint kPicHighScoreLogo = 0x20c; ///< _ShowHScoreLogo: GetPicture(0x20c)
-const uint kPicChooseBackground = 0x8c; ///< _DoChoosePartner: GetBackground(0x8c)
const uint kPalEAKids = 0x25;
const uint kPalHighScore = 0x27;
-// Animation IDs (0-based per ANI.DBX). _DoChoosePartner uses GetAnimation(8/9).
-const uint kAniBoy = 8;
-const uint kAniGirl = 9;
-
-// On-screen positions for the boy and girl partner sprites, from
-// _DoChoosePartner: NewAnimation(0xe2, 0x62, ...) and (0x42, 0x60, ...).
-const int kBoyX = 0xe2; // 226
-const int kBoyY = 0x62; // 98
-const int kGirlX = 0x42; // 66
-const int kGirlY = 0x60; // 96
+// Save format. Used by `saveGameState` / `loadGameState`.
+const uint32 kSaveMagic = MKTAG('E', 'E', 'M', '0');
+const byte kSaveVer = 3; ///< v2: _mysteriesSolved tracker; v3: player name
// 11x16 mouse cursor â replaces the DOS hardware cursor wired in by
// _InitMouse @ 152d:018b (INT 33h). The original game sets the cursor
@@ -447,829 +439,6 @@ void EEMEngine::showHighScoreLogo() {
waitForInput(2500);
}
-void EEMEngine::doNewPlayer() {
- // Mirrors `_NewPlayer` @ 1c33:0dda. The original draws background
- // 0x104 + character peek pic 0x107, then shows "Please type your
- // name" and accepts up to 12 characters until Enter. We render a
- // minimal version: black screen + prompt.
- if (!_font.isLoaded()) {
- _playerName = "Detective";
- return;
- }
-
- Common::String name;
- const int maxChars = 12;
-
- // Mirror the original: load PIC 0x104 as the name-entry backdrop.
- // The original also slides in PIC 0x107 (a peeking character).
- Picture bg;
- const bool haveBG = _picsArchive.getPicture(0x104, bg);
-
- auto draw = [&]() {
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
- if (haveBG) {
- const int w = MIN<int>(bg.surface.w, 320);
- const int h = MIN<int>(bg.surface.h, 200);
- for (int row = 0; row < h; row++)
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)bg.surface.getBasePtr(0, row), w);
- }
- // Match the original `_NewPlayer`: `_Show_String(rw=0x28, cl=0x50)`
- // for the prompt, then `_ShowChar(0x50, x, â¦)` for typed input.
- // (rw=row=y, cl=col=x.) Prompt at (y=40, x=80), input at (y=80, x=80).
- _font.drawString(&scratch, "Please type your name:", 80, 40, 240, 0xF);
- Common::String shown = name + "_";
- _font.drawString(&scratch, shown, 80, 80, 240, 0xF);
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
- };
- draw();
-
- while (!shouldQuit()) {
- Common::Event ev;
- bool dirty = false;
- while (g_system->getEventManager()->pollEvent(ev)) {
- if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
- return;
- if (ev.type != Common::EVENT_KEYDOWN)
- continue;
- const Common::KeyCode k = ev.kbd.keycode;
- if (k == Common::KEYCODE_RETURN) {
- if (name.empty())
- name = "Detective";
- _playerName = name;
- return;
- }
- if (k == Common::KEYCODE_ESCAPE) {
- _playerName = "Detective";
- return;
- }
- if (k == Common::KEYCODE_BACKSPACE) {
- if (!name.empty()) {
- name.deleteLastChar();
- dirty = true;
- }
- continue;
- }
- if (ev.kbd.ascii >= ' ' && ev.kbd.ascii < 127 &&
- (int)name.size() < maxChars) {
- name += (char)ev.kbd.ascii;
- dirty = true;
- }
- }
- if (dirty)
- draw();
- g_system->updateScreen();
- g_system->delayMillis(15);
- }
-}
-
-void EEMEngine::doChoosePartner() {
- // Mirrors _DoChoosePartner @ 1a35:0756. The original places boy + girl
- // animations on a backdrop and polls four click rectangles (two per
- // character) for the player's choice. We approximate by splitting the
- // screen at x=160: left half = girl (Jenny), right half = boy (Jake).
- Picture background;
- if (!_picsArchive.getPicture(kPicChooseBackground, background)) {
- warning("ChoosePartner background (%u) load failed", kPicChooseBackground);
- return;
- }
-
- Animation boyAnim;
- if (!_aniArchive.loadAnimation(kAniBoy, boyAnim) || boyAnim.empty()) {
- warning("Boy animation (%u) load failed", kAniBoy);
- return;
- }
- Animation girlAnim;
- if (!_aniArchive.loadAnimation(kAniGirl, girlAnim) || girlAnim.empty()) {
- warning("Girl animation (%u) load failed", kAniGirl);
- return;
- }
-
- setAnmPalette(Common::Path("TITLE.ANM"));
-
- // `_DoHappiness @ 172b:27b5`: the cursor's X column picks one of 4
- // rects (29be:030f, all full-height); past rect 3 â "level 4".
- // Each level swaps the partner's sequence script to a more / less
- // "happy" cycle. Boy seqs at 29be:0337 (5 Ã 0x14 bytes), girl seqs
- // at 29be:039b. Both cycle through 9 frames (the boy/girl anim
- // cells contain 10 cells = pairs of "neutral, smile" at increasing
- // intensity). Lifted verbatim from the binary so the gestures
- // match the original beat-for-beat.
- static const Common::Rect kHappyZones[4] = {
- Common::Rect( 0, 0, 70, 200), // far left â girl very happy, boy neutral
- Common::Rect( 70, 0, 126, 200), // girl's column
- Common::Rect(126, 0, 182, 200), // middle
- Common::Rect(182, 0, 235, 200), // boy's column
- };
- static const uint8 kBoySeqs[5][9] = {
- { 0,0,0,0,0,0,0,1,0 }, // level 0
- { 2,2,2,2,2,2,2,3,2 }, // level 1
- { 4,4,4,4,4,4,4,5,4 }, // level 2
- { 6,6,6,6,6,6,7,6,6 }, // level 3
- { 8,8,8,8,8,8,8,8,9 }, // level 4 (cursor past zone 3)
- };
- static const uint8 kGirlSeqs[5][9] = {
- { 8,9,8,8,8,8,8,8,8 },
- { 6,6,6,7,6,6,6,6,6 },
- { 4,4,5,4,4,4,4,4,4 },
- { 2,2,2,2,2,2,3,2,2 },
- { 0,0,0,0,0,1,0,0,0 },
- };
- auto happinessLevel = [](int x) {
- for (uint i = 0; i < ARRAYSIZE(kHappyZones); i++) {
- if (kHappyZones[i].contains(x, 100))
- return (uint)i;
- }
- return 4u; // past zone 3 â max level
- };
-
- int curMouseX = 0xa0; // _DoChoosePartner sets `_SetMousePos(0xa0, 0x96)` on entry
- int curMouseY = 0x96;
- uint level = happinessLevel(curMouseX);
- uint seqIdx = 0; // step within the 9-frame seq
-
- auto draw = [&]() {
- blitAt(background, 0, 0);
- const uint girlFrame = kGirlSeqs[level][seqIdx % 9];
- const uint boyFrame = kBoySeqs [level][seqIdx % 9];
- blitAt(girlAnim[girlFrame % girlAnim.size()], kGirlX, kGirlY);
- blitAt(boyAnim[boyFrame % boyAnim.size()], kBoyX, kBoyY);
- g_system->updateScreen();
- };
- draw();
-
- debugC(1, kDebugGeneral, "ChoosePartner: %u boy frames at (%d,%d), "
- "%u girl frames at (%d,%d)",
- (uint)boyAnim.size(), kBoyX, kBoyY,
- (uint)girlAnim.size(), kGirlX, kGirlY);
-
- uint32 lastTick = g_system->getMillis();
- while (!shouldQuit()) {
- // Advance through the 9-frame seq at 100 ms â `_CheckFrameRate`
- // cadence. The seq is short and loops; matches the original
- // `_UpdateAnimations` which restarts at curIdx=0 on the 0x80
- // marker. Mirrors `_DoHappiness`'s rewriting of `curIdx = 0xFFFF`
- // when the cursor crosses zones (we restart `seqIdx` instead).
- if (g_system->getMillis() - lastTick > 100) {
- lastTick = g_system->getMillis();
- seqIdx = (seqIdx + 1) % 9;
- draw();
- }
-
- Common::Event ev;
- bool done = false;
- while (g_system->getEventManager()->pollEvent(ev)) {
- if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
- done = true;
- break;
- }
- if (ev.type == Common::EVENT_MOUSEMOVE) {
- curMouseX = ev.mouse.x;
- curMouseY = ev.mouse.y;
- const uint newLevel = happinessLevel(curMouseX);
- if (newLevel != level) {
- level = newLevel;
- seqIdx = 0; // restart cycle so the gesture pops
- draw();
- }
- }
- if (ev.type == Common::EVENT_LBUTTONDOWN) {
- _partner = (ev.mouse.x >= 160) ? 0 : 1;
- debugC(1, kDebugGeneral, "Partner picked: %s",
- _partner == 0 ? "Jake" : "Jennifer");
- done = true;
- break;
- }
- if (ev.type == Common::EVENT_KEYDOWN) {
- if (ev.kbd.keycode == Common::KEYCODE_LEFT) {
- _partner = 1; done = true; break;
- }
- if (ev.kbd.keycode == Common::KEYCODE_RIGHT) {
- _partner = 0; done = true; break;
- }
- if (ev.kbd.keycode == Common::KEYCODE_RETURN ||
- ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
- done = true; break;
- }
- }
- }
- if (done)
- break;
- g_system->updateScreen();
- g_system->delayMillis(20);
- }
-}
-
-void EEMEngine::doCaseSelection() {
- // Mirrors `_CaseSelection` @ 1c33:0a87. The original draws PIC 0x41
- // (chooser background) plus a centred "Book %d" / "Challenge Book"
- // header at (y=12) and then calls `_DoChoose(list)` to render the
- // menu via `DrawList` @ 1c33:040d at (_TextBox+3, DAT_29be_0d02) =
- // (61, 35), 12 rows à 10 px line height. The menu list itself is
- // the static array at 29be:0d6a (verified via `push 0x0d6a` at
- // 1c33:1ab4). Strings are at 29be:0ef4 onwards. Layout:
- // list[0] = "----------------------------------"
- // list[1] = " Choose A Mystery"
- // list[2..10] = alternating menu items + separators
- // Five selectable items: Choose A Mystery / Practice Mystery /
- // See ScrapBook 1/2/3.
- const uint kMaxMystery = 54;
-
- enum MenuPick {
- kPickChoose = 0,
- kPickPractice,
- kPickScrap1,
- kPickScrap2,
- kPickScrap3,
- kNumPicks
- };
- const char *kPickLabel[kNumPicks] = {
- " Choose A Mystery",
- " Practice Mystery",
- " See ScrapBook 1",
- " See ScrapBook 2",
- " See ScrapBook 3"
- };
- // ScrapBooks aren't implemented yet â grey them so the player can't
- // stop on them, mirroring the original `_Greys` mask.
- const bool kPickEnabled[kNumPicks] = { true, true, false, false, false };
- uint pick = kPickChoose;
-
- const char *kSeparator = "----------------------------------";
-
- // Click rectangles from the original `_DoChoose` @ 1c33:0514 â each
- // `_InRect(_MouseX, _MouseY, addr, 0x29be)` reads one 4Ãu16 rect at
- // the listed offset in segment 29be ({x1, y1, x2, y2}). We use
- // `Common::Rect` (left/top/right/bottom) which also gives us
- // `contains(x, y)` for hit testing.
- const Common::Rect kOkRect ( 12, 63, 41, 87); // 29be:0cd8 confirm
- const Common::Rect kHelpRect ( 12, 100, 41, 124); // 29be:0ce0 help
- const Common::Rect kExitRect ( 12, 137, 41, 161); // 29be:0ce8 cancel
- const Common::Rect kUpArrowRect (240, 31, 250, 43); // 29be:0cf0 scroll up
- const Common::Rect kDnArrowRect (240, 148, 250, 159); // 29be:0cf8 scroll dn
- const Common::Rect kListRect ( 58, 35, 238, 158); // 29be:0d00 list panel
-
- // The original `_NewPlayer` set `_MouseCursor = 1` on exit; the
- // chain of screens after it expects the cursor to stay visible.
- // Reassert here in case anything between hid it.
- CursorMan.showMouse(true);
-
- // Mirrors `_CaseSelection`: load PIC 0x41 as the chooser backdrop.
- Picture caseBg;
- const bool haveCaseBg = _picsArchive.getPicture(0x41, caseBg);
-
- // KD greeter sprite. `_CaseSelection @ 1c33:0a87` (1c33:0b7e-0ba1)
- // loads anim 0x15 (Jake-paired) or 0x16 (Jenny-paired) and registers
- // `_NewAnimation(0x112, 0x50, ..., seqnum=0x15, prior=1)` â partner-
- // dependent because the host KD changes who's "with him" on the
- // briefing intro frame. Runs continuously through the menu loop via
- // `_UpdateAnimations`. We approximate with millis-based frame cycling.
- const uint kKdAniId = (_partner == 0) ? 0x15 : 0x16;
- Animation kdAnim;
- const bool haveKdAnim = _aniArchive.loadAnimation(kKdAniId, kdAnim)
- && !kdAnim.empty();
- const int kKdAnimX = 0x112;
- const int kKdAnimY = 0x50;
-
- auto draw = [&]() {
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
- if (haveCaseBg) {
- const int w = MIN<int>(caseBg.surface.w, 320);
- const int h = MIN<int>(caseBg.surface.h, 200);
- for (int row = 0; row < h; row++) {
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)caseBg.surface.getBasePtr(0, row), w);
- }
- }
-
- // KD greeter frame â masked-blit current animation cell at
- // (0x112, 0x50). 100 ms tick matches the engine's `_CheckFrameRate`.
- if (haveKdAnim) {
- const uint32 now = g_system->getMillis();
- const uint frameIdx = (uint)((now / 100) % kdAnim.size());
- const Picture &fr = kdAnim[frameIdx];
- const byte transp = (byte)(fr.flags >> 8);
- for (int row = 0; row < fr.surface.h; row++) {
- const int dstY = kKdAnimY + row;
- if (dstY < 0 || dstY >= 200) continue;
- const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, dstY);
- for (int col = 0; col < fr.surface.w; col++) {
- const int dstX = kKdAnimX + col;
- if (dstX < 0 || dstX >= 320) continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
- }
- if (_font.isLoaded()) {
- // `DrawList` @ 1c33:040d coordinates: `_TextBox + 3` for x
- // and `DAT_29be_0d02` for y. `_TextBox` @ 29be:0d00 holds
- // {x=58, y=35, x2=238, y2=158}. Matches the blue panel.
- const int kListX = 58 + 3;
- const int kListW = 238 - kListX;
- const int kListY0 = 35;
- const int kLineH = 10;
-
- // Top centred "Book %d" / "Challenge Book" title â sprintf
- // format strings at 29be:0deb / 29be:0dfa shown via
- // `_Show_String(0xc, (0xba - width)/2 + 0x3c, â¦)` in the
- // original. We don't track challenge tier yet so always
- // show "Book 1".
- const Common::String book = "Book 1";
- const int titleW = _font.getStringWidth(book);
- const int titleX = (0xba - titleW) / 2 + 0x3c;
- _font.drawString(&scratch, book, titleX, 12, 320, 0xF);
-
- // Render 11 list rows: separator + menu item pairs.
- // row 0 separator
- // row 1 Choose A Mystery
- // row 2 separator
- // row 3 Practice Mystery
- // ...
- // row 9 See ScrapBook 3
- // row 10 separator
- for (int r = 0; r < 11; r++) {
- const int y = kListY0 + r * kLineH;
- if ((r & 1) == 0) {
- _font.drawString(&scratch, kSeparator, kListX, y, kListW, 0x7);
- continue;
- }
- const uint mp = (uint)(r >> 1);
- const bool isSel = (mp == pick);
- const byte color = isSel ? 0xF :
- kPickEnabled[mp] ? 0x7 : 0x8;
- _font.drawString(&scratch, kPickLabel[mp], kListX, y, kListW, color);
- }
- }
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
- };
-
- auto pickPrev = [&]() {
- for (int i = 0; i < (int)kNumPicks; i++) {
- pick = (pick == 0) ? (uint)(kNumPicks - 1) : pick - 1;
- if (kPickEnabled[pick])
- break;
- }
- };
- auto pickNext = [&]() {
- for (int i = 0; i < (int)kNumPicks; i++) {
- pick = (pick + 1) % kNumPicks;
- if (kPickEnabled[pick])
- break;
- }
- };
-
- draw();
- uint32 lastTick = g_system->getMillis();
-
- bool exitChosen = false;
- while (!shouldQuit()) {
- Common::Event ev;
- bool confirmed = false;
- // Redraw every 100 ms so the KD greeter cycles. Mirrors the
- // `_CheckFrameRate` cadence in `_CaseSelection`'s main loop.
- const uint32 now = g_system->getMillis();
- if (haveKdAnim && now - lastTick >= 100) {
- lastTick = now;
- draw();
- }
- while (g_system->getEventManager()->pollEvent(ev)) {
- if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
- return;
- if (ev.type == Common::EVENT_LBUTTONDOWN) {
- // OK / EXIT / HELP buttons (rectangles from `_DoChoose`).
- if (kOkRect.contains(ev.mouse.x, ev.mouse.y)) {
- confirmed = true;
- break;
- }
- if (kExitRect.contains(ev.mouse.x, ev.mouse.y)) {
- exitChosen = true;
- confirmed = true;
- break;
- }
- if (kHelpRect.contains(ev.mouse.x, ev.mouse.y)) {
- // HELP placeholder â original calls `_DisplayHint`;
- // our help screen is wired to `H` later in the flow.
- continue;
- }
- // List panel: click on a non-separator row selects the
- // menu entry under the cursor.
- if (kListRect.contains(ev.mouse.x, ev.mouse.y)) {
- const int kLineH = 10;
- const int row = (ev.mouse.y - kListRect.top) / kLineH;
- if ((row & 1) == 1) {
- const uint mp = (uint)(row >> 1);
- if (mp < kNumPicks && kPickEnabled[mp]) {
- pick = mp;
- draw();
- continue;
- }
- }
- }
- }
- if (ev.type != Common::EVENT_KEYDOWN)
- continue;
- const Common::KeyCode k = ev.kbd.keycode;
- if (k == Common::KEYCODE_ESCAPE) {
- exitChosen = true;
- confirmed = true;
- break;
- }
- if (k == Common::KEYCODE_RETURN) {
- confirmed = true;
- break;
- }
- if (k == Common::KEYCODE_UP || k == Common::KEYCODE_LEFT) {
- pickPrev();
- draw();
- continue;
- }
- if (k == Common::KEYCODE_DOWN || k == Common::KEYCODE_RIGHT ||
- k == Common::KEYCODE_TAB) {
- pickNext();
- draw();
- continue;
- }
- }
- if (confirmed) {
- draw();
- break;
- }
- g_system->updateScreen();
- g_system->delayMillis(15);
- }
-
- if (shouldQuit())
- return;
-
- if (exitChosen) {
- _mystery.clear();
- _nextScreen = kScreenInvalid;
- return;
- }
-
- // "Practice Mystery" is the tutorial â mystery 0.
- if (pick == kPickPractice) {
- if (!_mystery.load(0, &_rng)) {
- warning("doCaseSelection: failed to load practice mystery");
- _mystery.clear();
- }
- return;
- }
-
- if (pick != kPickChoose) {
- // ScrapBooks aren't implemented; bail back to the menu loop.
- _mystery.clear();
- return;
- }
-
- // "Choose A Mystery" sub-screen: pick a specific case from the
- // 55-mystery roster. The original opens a different list here;
- // we approximate with the tier-aware numeric chooser we used
- // before. Default to the first unsolved mystery.
- uint sel = 0;
- for (uint i = 0; i <= kMaxMystery; i++) {
- if (i < sizeof(_mysteriesSolved) && !_mysteriesSolved[i]) {
- sel = i;
- break;
- }
- }
-
- auto drawSubmenu = [&]() {
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
- if (haveCaseBg) {
- const int w = MIN<int>(caseBg.surface.w, 320);
- const int h = MIN<int>(caseBg.surface.h, 200);
- for (int row = 0; row < h; row++)
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)caseBg.surface.getBasePtr(0, row), w);
- }
- if (_font.isLoaded()) {
- const int kListX = 61;
- const int kListW = 238 - kListX;
- const int kListY0 = 35;
- const int kLineH = 10;
- const int kVisible = 12;
- int top = (int)sel - kVisible / 2;
- if (top < 0) top = 0;
- if (top + kVisible > (int)kMaxMystery + 1)
- top = (int)kMaxMystery + 1 - kVisible;
- for (int r = 0; r < kVisible; r++) {
- const int idx = top + r;
- if (idx > (int)kMaxMystery)
- break;
- char marker = ' ';
- if ((uint)idx < sizeof(_mysteriesSolved)) {
- if (_mysteriesSolved[idx] == 2) marker = '*';
- else if (_mysteriesSolved[idx] == 1) marker = '+';
- }
- const char arrow = ((uint)idx == sel) ? '>' : ' ';
- _font.drawString(&scratch,
- Common::String::format("%c %c Mystery %d", arrow, marker, idx),
- kListX, kListY0 + r * kLineH, kListW, 0xF);
- }
- }
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
- };
-
- drawSubmenu();
- bool confirmed = false;
- while (!confirmed && !shouldQuit()) {
- Common::Event ev;
- while (g_system->getEventManager()->pollEvent(ev)) {
- if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
- return;
- if (ev.type == Common::EVENT_LBUTTONDOWN) {
- // Same `_DoChoose` rectangles as the top-level menu.
- if (kOkRect.contains(ev.mouse.x, ev.mouse.y)) {
- confirmed = true;
- break;
- }
- if (kExitRect.contains(ev.mouse.x, ev.mouse.y)) {
- _mystery.clear();
- return;
- }
- if (kUpArrowRect.contains(ev.mouse.x, ev.mouse.y)) {
- sel = (sel == 0) ? kMaxMystery : sel - 1;
- drawSubmenu();
- continue;
- }
- if (kDnArrowRect.contains(ev.mouse.x, ev.mouse.y)) {
- sel = (sel >= kMaxMystery) ? 0 : sel + 1;
- drawSubmenu();
- continue;
- }
- if (kListRect.contains(ev.mouse.x, ev.mouse.y)) {
- // Pick the row under the cursor.
- const int kLineH = 10;
- const int kVisible = 12;
- int top = (int)sel - kVisible / 2;
- if (top < 0) top = 0;
- if (top + kVisible > (int)kMaxMystery + 1)
- top = (int)kMaxMystery + 1 - kVisible;
- const int row = (ev.mouse.y - kListRect.top) / kLineH;
- const int idx = top + row;
- if (idx >= 0 && idx <= (int)kMaxMystery) {
- sel = (uint)idx;
- drawSubmenu();
- }
- continue;
- }
- }
- if (ev.type != Common::EVENT_KEYDOWN)
- continue;
- const Common::KeyCode k = ev.kbd.keycode;
- if (k == Common::KEYCODE_ESCAPE) {
- _mystery.clear();
- return;
- }
- if (k == Common::KEYCODE_RETURN) {
- confirmed = true;
- break;
- }
- if (k >= Common::KEYCODE_0 && k <= Common::KEYCODE_9) {
- sel = (uint)(k - Common::KEYCODE_0);
- drawSubmenu();
- continue;
- }
- if (k == Common::KEYCODE_DOWN || k == Common::KEYCODE_TAB) {
- sel = (sel >= kMaxMystery) ? 0 : sel + 1;
- drawSubmenu();
- continue;
- }
- if (k == Common::KEYCODE_UP) {
- sel = (sel == 0) ? kMaxMystery : sel - 1;
- drawSubmenu();
- continue;
- }
- if (k == Common::KEYCODE_PAGEDOWN) {
- sel = (sel + 10 > kMaxMystery) ? kMaxMystery : sel + 10;
- drawSubmenu();
- continue;
- }
- if (k == Common::KEYCODE_PAGEUP) {
- sel = (sel < 10) ? 0 : sel - 10;
- drawSubmenu();
- continue;
- }
- if (k == Common::KEYCODE_HOME) { sel = 0; drawSubmenu(); continue; }
- if (k == Common::KEYCODE_END) { sel = kMaxMystery; drawSubmenu(); continue; }
- }
- g_system->updateScreen();
- g_system->delayMillis(15);
- }
-
- if (!_mystery.load(sel, &_rng)) {
- warning("doCaseSelection: failed to load mystery %u", sel);
- _mystery.clear();
- return;
- }
- debugC(1, kDebugMystery, "Mystery %u loaded; %u sites, %u suspects",
- sel, _mystery.numSites(), _mystery.numSuspects());
-}
-
-void EEMEngine::doInitClues() {
- // Mirrors `_DoInitClues` @ 1a35:0411. The original does:
- // 1. _AllBlack(); _GetBackground(0x52); _GetPalette(0x22);
- // 2. _GetAnimation(gameAni); _NewAnimation(0xcd, 0x6c, ...)
- // _GetAnimation(bookAni); _NewAnimation(0, 99, ...)
- // (case type 1 also: _NewAnimation(0x68, 0x8b, nancyAni))
- // 3. _UpdateAnimations(); _FadeIn();
- // 4. while (frame != gameNum) { _CheckFrameRate(); _UpdateAnimations(); }
- // â cycles through the entire game animation once. Click skips.
- // 5. _PlayInSequence(seqId, ...) â plays a follow-up sequence based
- // on partner + case type.
- // 6. _DisplayClue(InitBlock + 2, 1) â the briefing dialogue.
- // 7. _OnSites[startSite] = 1.
- //
- // gameAni / bookAni / nancyAni values verified directly from Ghidra:
- // gameAni = 0x17 (Jake) / 0x3b (Jenny)
- // bookAni = 0x18 (Jake) / 0x3c (Jenny)
- // nancyAni = 0x19 (case type 1 only)
- if (!_mystery.isLoaded())
- return;
-
- const byte *ib = _mystery.initBlock();
- if (!ib)
- return;
-
- const uint16 startSite = READ_LE_UINT16(ib + 2);
- if (startSite < Mystery::kVisitedSiteCap)
- _mystery._onSites[startSite] = 1;
- _mystery._siteNumber = startSite;
- _mystery._lastSite = startSite;
-
- setSitePalette(0x22);
- Picture bg;
- if (_picsArchive.getPicture(0x52, bg))
- blitAt(bg, 0, 0);
-
- const uint gameAni = _partner == 0 ? 0x17 : 0x3b;
- const uint bookAni = _partner == 0 ? 0x18 : 0x3c;
- Animation game, book, nancy;
- const bool haveGame = _aniArchive.loadAnimation(gameAni, game) && !game.empty();
- const bool haveBook = _aniArchive.loadAnimation(bookAni, book) && !book.empty();
-
- const uint16 caseType = READ_LE_UINT16(ib);
- const bool haveNancy = (caseType == 1)
- && _aniArchive.loadAnimation(0x19, nancy)
- && !nancy.empty();
-
- auto blitMaskedAt = [&](const Picture &p, int x, int y) {
- const byte transp = (byte)(p.flags >> 8);
- Graphics::Surface *screen = g_system->lockScreen();
- if (!screen) return;
- for (int row = 0; row < p.surface.h; row++) {
- const int dstY = y + row;
- if (dstY < 0 || dstY >= screen->h) continue;
- const byte *src = (const byte *)p.surface.getBasePtr(0, row);
- byte *dst = (byte *)screen->getBasePtr(0, dstY);
- for (int col = 0; col < p.surface.w; col++) {
- const int dstX = x + col;
- if (dstX < 0 || dstX >= screen->w) continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
- g_system->unlockScreen();
- };
-
- // Step 4 â cycle through the game animation once before the briefing.
- // Mirrors the `while (uVar9 != gameNum)` loop. The original calls
- // `_UpdateAnimations` per `_CheckFrameRate` tick (~10 fps). We use
- // 100 ms ticks for the same cadence. Click / key skips.
- if (haveGame || haveBook || haveNancy) {
- const uint frameCount = haveGame ? game.size() : 8;
- bool skip = false;
- for (uint frame = 0; frame < frameCount && !shouldQuit() && !skip; frame++) {
- // Restore BG + advance frame.
- if (_picsArchive.getPicture(0x52, bg))
- blitAt(bg, 0, 0);
- if (haveGame)
- blitMaskedAt(game[frame % game.size()], 0xcd, 0x6c);
- if (haveBook)
- blitMaskedAt(book[frame % book.size()], 0, 99);
- if (haveNancy)
- blitMaskedAt(nancy[frame % nancy.size()], 0x68, 0x8b);
- g_system->updateScreen();
-
- // Wait 100 ms or until input.
- const uint32 wakeup = g_system->getMillis() + 100;
- while (g_system->getMillis() < wakeup && !shouldQuit() && !skip) {
- Common::Event ev;
- while (g_system->getEventManager()->pollEvent(ev)) {
- if (ev.type == Common::EVENT_LBUTTONDOWN ||
- ev.type == Common::EVENT_KEYDOWN) {
- skip = true;
- break;
- }
- }
- g_system->delayMillis(10);
- }
- }
- }
-
- // Composite the final frames (or first frames if skipped) so the BG
- // is in a sensible state when displayClue overlays the speaker.
- if (_picsArchive.getPicture(0x52, bg))
- blitAt(bg, 0, 0);
- if (haveGame)
- blitMaskedAt(game[0], 0xcd, 0x6c);
- if (haveBook)
- blitMaskedAt(book[0], 0, 99);
- if (haveNancy)
- blitMaskedAt(nancy[0], 0x68, 0x8b);
- g_system->updateScreen();
-
- // Step 5 â `_PlayInSequence(animSeq, 0xcd, animY)` per Ghidra:
- // Jake (partner=0):
- // caseType=1 â anim 0x38 at (0xcd, 0x6d)
- // caseType=2 â anim 0x37 at (0xcd, 0x6c)
- // caseType=3 â anim 0x39 at (0xcd, 0x6c)
- // Jenny (partner=1):
- // caseType=2 â anim 0x3a at (0xcd, 0x6c)
- // caseType=3 â anim 0x3d at (0xcd, 0x6c)
- // `_PlayInSequence @ 172b:2d03` plays each frame at (sx-w, sy-rowoff)
- // with mask blit, advancing one frame per `_CheckFrameRate` tick.
- uint16 seqAni = 0xFFFF;
- uint16 seqY = 0x6c;
- if (_partner == 0) {
- switch (caseType) {
- case 1: seqAni = 0x38; seqY = 0x6d; break;
- case 2: seqAni = 0x37; seqY = 0x6c; break;
- case 3: seqAni = 0x39; seqY = 0x6c; break;
- default: break;
- }
- } else {
- switch (caseType) {
- case 2: seqAni = 0x3a; seqY = 0x6c; break;
- case 3: seqAni = 0x3d; seqY = 0x6c; break;
- default: break;
- }
- }
- if (seqAni != 0xFFFF) {
- Animation seq;
- if (_aniArchive.loadAnimation(seqAni, seq) && !seq.empty()) {
- bool skip = false;
- for (uint frame = 0; frame < seq.size() && !shouldQuit() && !skip;
- frame++) {
- const Picture &fr = seq[frame];
- // Restore BG + base anim frames so each new frame
- // composites cleanly.
- if (_picsArchive.getPicture(0x52, bg))
- blitAt(bg, 0, 0);
- if (haveGame)
- blitMaskedAt(game[frame % game.size()], 0xcd, 0x6c);
- if (haveBook)
- blitMaskedAt(book[frame % book.size()], 0, 99);
- if (haveNancy)
- blitMaskedAt(nancy[frame % nancy.size()], 0x68, 0x8b);
- // Anchor: original blits at `(sx - frame.width,
- // sy - frame.rowoff)`. `frame.rowoff` is the y-anchor
- // in our PicData. We use width/height directly since
- // loadAnimation places anchor at (0, 0).
- const int dstX = (int)0xcd - (int)fr.surface.w;
- const int dstY = (int)seqY - (int)fr.rowoff;
- blitMaskedAt(fr, dstX, dstY);
- g_system->updateScreen();
- const uint32 wakeup = g_system->getMillis() + 100;
- while (g_system->getMillis() < wakeup &&
- !shouldQuit() && !skip) {
- Common::Event ev;
- while (g_system->getEventManager()->pollEvent(ev)) {
- if (ev.type == Common::EVENT_LBUTTONDOWN ||
- ev.type == Common::EVENT_KEYDOWN) {
- skip = true;
- break;
- }
- }
- g_system->delayMillis(10);
- }
- }
- }
- }
-
- // Step 6 â case briefing dialogue.
- displayClue(ib + 4);
-}
-
void EEMEngine::doSiteLoop() {
// Mirrors the per-mystery site loop. SiteScreen::run() handles
// hotspot clicks plus M (map), N (notebook), G (gallery), A (accuse),
@@ -1278,2434 +447,6 @@ void EEMEngine::doSiteLoop() {
screen.run();
}
-/// Mirror `_ParseString` @ 1b66:07c3 â substitute the control bytes that
-/// the original engine uses as placeholders. Only the two we encounter most
-/// often (player name = 0x80, partner first name = 0x82) are substituted;
-/// other 0x8N opcodes are stripped. The original engine also handles
-/// hyphenation marks and a hint placeholder (0x89) we ignore for now.
-static Common::String parseString(const Common::String &raw,
- const Common::String &playerName,
- uint partner) {
- // Substitution opcodes from `_ParseString` @ 1b66:07c3, jump-table
- // at 1b66:0cbe. Each handler reads `_Partner` (16-bit at 0x7918)
- // and indexes the name table at 29be:0c28 ({Jake, Jennifer, he,
- // she, him, her, his} as far pointers).
- // 0x80 â player's typed name (auto-cap word starts) â uses _PlayerRecord
- // 0x81 â _Partner == 0 ? "Jake" : "Jennifer" (chosen detective)
- // 0x82 â _Partner == 0 ? "Jennifer" : "Jake" (the OTHER one)
- // 0x83 â _Partner == 0 ? "he" : "she"
- // 0x84 â _Partner == 0 ? "him" : "her"
- // 0x85 â _Partner == 0 ? "his" : "her"
- // 0x86..0x88 read a different gender flag at 0x7985 â left alone
- // until that flag's source is traced.
- // 0x89 â KD hint placeholder (handled by caller).
- const bool isJake = (partner == 0);
- Common::String out;
- for (uint i = 0; i < raw.size(); i++) {
- const byte c = (byte)raw[i];
- switch (c) {
- case 0x80:
- out += playerName;
- break;
- case 0x81:
- out += isJake ? "Jake" : "Jennifer";
- break;
- case 0x82:
- out += isJake ? "Jennifer" : "Jake";
- break;
- case 0x83:
- out += isJake ? "he" : "she";
- break;
- case 0x84:
- out += isJake ? "him" : "her";
- break;
- case 0x85:
- out += isJake ? "his" : "her";
- break;
- case 0x86:
- case 0x87:
- case 0x88:
- case 0x89:
- // Eaten silently â see comment above.
- break;
- case 0:
- return out;
- case '\r':
- break;
- default:
- out += (char)c;
- break;
- }
- }
- return out;
-}
-
-void EEMEngine::applyClueSideEffects(const byte *c) {
- for (uint j = 0; j < 5; j++) {
- const uint16 note = READ_LE_UINT16(c + 0x30 + j * 2);
- if (note != 0xFFFF && note < Mystery::kCluesFoundCap)
- _mystery._cluesFound[note] = 1;
-
- const uint16 galIdx = READ_LE_UINT16(c + 0x26 + j * 2);
- if (galIdx != 0xFFFF && galIdx < Mystery::kGalleryCap) {
- const uint8 phys = _mystery._newOrder[galIdx];
- if (phys < Mystery::kGalleryCap)
- _mystery._inGallery[phys] = 1;
- }
-
- const uint16 siteIdx = READ_LE_UINT16(c + 0x1c + j * 2);
- if (siteIdx != 0xFFFF) {
- const uint16 siteVal = siteIdx & 0x7FFF;
- if (siteVal < Mystery::kVisitedSiteCap)
- _mystery._onSites[siteVal] = 1;
- if (siteIdx & 0x8000)
- _mystery._sawCONSITEs = true;
- }
- }
-}
-
-void EEMEngine::displayClue(const byte *clueBlock) {
- if (!clueBlock || !_mystery.isLoaded())
- return;
-
- // ClueBlock layout (verified against M0.BIN):
- // +0..1: number (entry count)
- // +2..3: pic ID for entry 0 (entry N>0 uses prev entry's last 2 bytes)
- // +4..: array of 62-byte entries
- const uint16 number = READ_LE_UINT16(clueBlock);
- debugC(1, kDebugScript, "displayClue: %u entries", number);
- if (number == 0 || number > 32) {
- // number==0 = no briefing (e.g. mystery 0 case-type 4); >32 is a
- // guard against bad pointers.
- return;
- }
-
- // Snapshot the current screen as the BG so character pics from
- // earlier entries don't stack on top of each other.
- Graphics::ManagedSurface bg(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- bg.clear();
- {
- Graphics::Surface *screen = g_system->lockScreen();
- if (screen) {
- for (int row = 0; row < 200; row++) {
- memcpy((byte *)bg.getBasePtr(0, row),
- (const byte *)screen->getBasePtr(0, row), 320);
- }
- g_system->unlockScreen();
- }
- }
-
- for (uint i = 0; i < number && !shouldQuit(); i++) {
- // Restore BG before drawing this entry's portrait + balloon.
- g_system->copyRectToScreen(bg.getPixels(), bg.pitch, 0, 0, 320, 200);
- const byte *c = clueBlock + 4 + i * 62;
- // Per-partner fields:
- // +0..1, +2..3: tx, ty (partner 0)
- // +4..5, +6..7: tx, ty (partner 1)
- // +8..9, +10..11: bubText offset for partner 0/1 (rel. TextBlock)
- // +12..13, +14..15: balloon picture ID for partner 0/1
- // +16..17, +18..19: bubX, bubY
- // +0x3a..+0x3b: KD-anim number (-1 = none)
- // Per `_DisplayClue` @ 2404:05e6: partner 1 uses its own field
- // set ONLY when bubText1 is not -1; otherwise it falls back to
- // the partner 0 fields entirely. Partner 0 always uses field 0.
-
- // Per-clue partner reaction animation. `_DisplayClue` @
- // 2404:0635-064b checks `clueEntry[+0x3a]` and, when not -1,
- // calls `_DoKDAnim(num)` BEFORE drawing the speaker portrait.
- // This is what surfaces "Jenny takes a picture with a camera"
- // (and the matching Jake gestures) during NPC searches.
- const int16 kdAnimNum = (int16)READ_LE_UINT16(c + 0x3a);
- if (kdAnimNum != -1)
- playKdAnim((uint16)kdAnimNum);
-
- const bool useP1 = (_partner == 1) &&
- (READ_LE_UINT16(c + 10) != 0xFFFF);
- const uint partner = useP1 ? 1 : 0;
- const uint16 textOff = READ_LE_UINT16(c + 8 + partner * 2);
- const bool hasText = (textOff != 0xFFFF);
- // Partner 1 bubX/bubY at +0x14/+0x16; partner 0 at +0x10/+0x12.
- const uint16 bubX = READ_LE_UINT16(c + (useP1 ? 0x14 : 0x10));
- const uint16 bubY = READ_LE_UINT16(c + (useP1 ? 0x16 : 0x12));
- const uint16 bubNum = READ_LE_UINT16(c + (useP1 ? 0x0E : 0x0C));
- const char *raw = hasText ? _mystery.textAt(textOff) : "";
-
- // Speaker portrait. Mirrors `_DisplayClue`'s `pic[clues+i*62-2]`:
- // for entry 0 the pic ID is in the ClueBlock header at +2; for
- // later entries it sits in the previous entry's last 2 bytes.
- // Speaker portrait position uses partner 0 fields (+0..+3) when
- // _partner==0 or when partner 1 falls back; otherwise partner 1
- // fields (+4..+7). Same logic as the original.
- const uint16 charX = READ_LE_UINT16(c + (useP1 ? 4 : 0));
- const uint16 charY = READ_LE_UINT16(c + (useP1 ? 6 : 2));
- const uint16 charPicId = (i == 0)
- ? READ_LE_UINT16(clueBlock + 2)
- : READ_LE_UINT16(c - 2);
- if (charPicId != 0 && charPicId != 0xFFFF) {
- Picture charPic;
- if (_picsArchive.getPicture(charPicId, charPic) &&
- charX < 320 && charY < 200) {
- const int w = MIN<int>(charPic.surface.w, 320 - charX);
- const int h = MIN<int>(charPic.surface.h, 200 - charY);
- if (w > 0 && h > 0)
- g_system->copyRectToScreen(charPic.surface.getPixels(),
- charPic.surface.pitch, charX, charY, w, h);
- }
- }
-
- // Substitute control bytes (0x80..0x89) â see `parseString` for
- // the table. 0x81 = chosen detective, 0x82 = the other one.
- const Common::String text = parseString(raw ? raw : "",
- _playerName, _partner);
-
- // Speech balloon. Mirrors `_GetBalloon` + `_AddPicBackground` in
- // `_DisplayClue`. The original looks up per-balloon text-area
- // metadata in a table at offset 0x875 (within `_DisplayClue`'s
- // segment); we don't have that table decoded yet, so we use a
- // fixed inset of 8 px from the balloon's top-left.
- Picture balloon;
- const uint16 balloonId = bubNum & 0x7F;
- const bool haveBalloon = bubNum != 0xFFFF &&
- _balloonArchive.size() > balloonId &&
- _balloonArchive.loadEntry(balloonId, balloon);
-
- if (_font.isLoaded() && !text.empty()) {
- // Snapshot the current screen, overlay balloon + text, then
- // copy the changed band back. This preserves the site BG
- // underneath unchanged regions.
- Graphics::Surface *screen = g_system->lockScreen();
- if (!screen) break;
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- for (int row = 0; row < 200; row++) {
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)screen->getBasePtr(0, row), 320);
- }
- g_system->unlockScreen();
-
- int textX = bubX;
- int textY = bubY;
- int textW = MIN<int>(320 - bubX, 200);
- int copyY = bubY;
- int copyH = _font.getFontHeight() * 4 + 8;
-
- if (haveBalloon) {
- const int bw = MIN<int>(balloon.surface.w, 320 - bubX);
- const int bh = MIN<int>(balloon.surface.h, 200 - bubY);
- // `_AddPicBackground` passes `pic->miscflags >> 8` as
- // the transparent colour to `_Rect_Move_Mask`. The
- // on-disk u16 at file offset 0 maps to `Picture::flags`.
- const byte transp = (byte)(balloon.flags >> 8);
- // `_GetBalloon @ 172b:1d7d` mirrors the picture horizontally
- // when `(bubNum & 0x80)` is set â used for right-side
- // speakers so the tail points the other way.
- const bool flipBalloon = (bubNum & 0x80) != 0;
- if (bw > 0 && bh > 0) {
- for (int row = 0; row < bh; row++) {
- const byte *src =
- (const byte *)balloon.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(bubX, bubY + row);
- for (int col = 0; col < bw; col++) {
- const int srcCol = flipBalloon
- ? (balloon.surface.w - 1 - col)
- : col;
- const byte px = src[srcCol];
- if (px != transp)
- dst[col] = px;
- }
- }
- }
- // Per-balloon metadata table verified from 29be:0875 â
- // 10-byte entries indexed by `(bubNum & 0x7f)`. Layout:
- // +0..1 textX inset, +2..3 textY inset, +4..5 width,
- // +6..7 height, +8..9 tail offset.
- // 52 entries total; insets vary (3, 5, 6, or 8 px).
- // The original `_DisplayClue` does:
- // _WordWrap(bubX + table[bubNum].x, bubY + table[bubNum].y,
- // table[bubNum].w, ...);
- static const struct { uint16 x, y, w; } kBalloonTable[] = {
- { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 },
- { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 },
- { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 },
- { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 },
- { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 },
- { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 },
- { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 },
- { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 },
- { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 },
- { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 },
- { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 },
- { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 },
- { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 },
- { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 },
- { 5, 8, 158 }, { 5, 8, 176 }, { 8, 7, 142 }
- };
- const uint kBalloonTableSize = sizeof(kBalloonTable) /
- sizeof(kBalloonTable[0]);
- const uint balloonIdx = balloonId < kBalloonTableSize
- ? balloonId : 0;
- const auto &bm = kBalloonTable[balloonIdx];
- textX = bubX + bm.x;
- textY = bubY + bm.y;
- textW = bm.w;
- copyH = bh;
- } else {
- // No balloon â clear a band so old pixels don't bleed.
- const Common::Rect band(0, bubY, 320,
- MIN<int>(bubY + copyH, 200));
- scratch.fillRect(band, 0);
- copyY = bubY;
- }
-
- // `_DisplayClue` @ 2404:07fe passes fontColor=0 (palette
- // index 0 of the case-briefing palette 0x22) to `_WordWrap`.
- // Hard-coding 0xF here gave the wrong colour.
- _font.drawWordWrapped(&scratch, textX, textY,
- MAX<int>(8, textW), text, 0);
-
- g_system->copyRectToScreen(scratch.getBasePtr(0, copyY),
- scratch.pitch, 0, copyY, 320,
- MIN<int>(copyH, 200 - copyY));
- g_system->updateScreen();
- }
-
- // Wait for click/key to advance â only if we drew something.
- // ESC skips the entire dialogue rather than just one entry.
- if (hasText || (charPicId != 0 && charPicId != 0xFFFF)) {
- bool advance = false;
- bool skipAll = false;
- while (!advance && !shouldQuit()) {
- Common::Event ev;
- while (g_system->getEventManager()->pollEvent(ev)) {
- if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
- advance = true;
- break;
- }
- if (ev.type == Common::EVENT_KEYDOWN &&
- ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
- advance = true;
- skipAll = true;
- break;
- }
- if (ev.type == Common::EVENT_LBUTTONDOWN ||
- ev.type == Common::EVENT_KEYDOWN) {
- advance = true;
- break;
- }
- }
- // Tick the screen so the OSystem cursor follows the
- // mouse â ScummVM redraws the cursor overlay only on
- // updateScreen.
- g_system->updateScreen();
- g_system->delayMillis(10);
- }
- if (skipAll) {
- // Apply remaining side-effects without rendering. The
- // original silently runs the state updates even when the
- // player skips ahead.
- for (uint k = i; k < number; k++)
- applyClueSideEffects(clueBlock + 4 + k * 62);
- return;
- }
- }
-
- applyClueSideEffects(c);
- }
-}
-
-void EEMEngine::doNotebook() {
- // Mirrors `_DoNotebook @ 161e:0500` + `_DrawNotes @ 161e:01d0` +
- // `_HandleNoteButton @ 161e:03cb`.
- //
- // Layout (verified from Ghidra labels in 29be:013f / 29be:0147):
- // _NotebookRect = (78, 12, 288, 152) â note display rectangle.
- // _NoteButtons (11 entries, 8 bytes each, at 29be:0147):
- // [0] (134, 174, 155, 190) decorative â `_HandleNoteButton(0)`
- // returns immediately (i-1 unsigned > 9).
- // [1] (93, 174, 115, 190) â `_InterfaceHelp(0)` (handler 0x3f9)
- // [2] (157, 174, 178, 190) â handler 0x477 (page nav)
- // [3] (5, 80, 44, 110) â `_KDHelp` (host hint, 0x403)
- // [4] (180, 174, 201, 190) â solve / accuse (0x436)
- // [5] (204, 174, 224, 190) â `_NextScreen = 5` (gallery, 0x489)
- // [6] (226, 174, 247, 190) â handler 0x4ab
- // [7] (7, 177, 57, 200) â handler 0x480 (back to map)
- // [8] (35, 111, 56, 136) â `_NextScreen = 3` (site)
- // [9] (0, 0, 0, 0) â same exit as [8]
- // [10] (66, 79, 267, 174) â `_InterfaceHelp(0)` (note area)
- // Background: PIC 0x3f.
- // Partner anim: anim 1 (Jake) / 0xb (Jenny) at (5, 80).
- if (!_mystery.isLoaded() || !_font.isLoaded())
- return;
-
- // Button rects from `_NoteButtons @ 29be:0147` matched to handler
- // addresses via the jump table at `_HandleNoteButton + 0xec` (i.e.
- // 161e:04ec). Decoded handlers (i = rect_index, dispatch = handler[i-1]):
- // rect 0 (134,155) â no handler (i-1 underflows; original treats
- // this as a decorative/no-op slot)
- // rect 1 (93,115) â 0x03f9 = `_InterfaceHelp(0)` (HELP)
- // rect 2 (157,178) â 0x0477 = `_NextScreen = 5` (GALLERY)
- // rect 3 (5,80) â 0x0403 = `_KDHelp` (host hint)
- // rect 4 (180,201) â 0x0436 = `_SolvedCheck` -> NextScreen=7 (SOLVE)
- // rect 5 (204,224) â 0x0489 = `_EraseNotes` + `_DrawNotes` (PAGE NEXT)
- // rect 6 (226,247) â 0x04ab = decrement CurrentPage + redraw (PAGE PREV)
- // rect 7 (7,177) â 0x0480 = `_NextScreen = 2` (MAP)
- // rect 8 (35,111) â 0x03ed = `_NextScreen = 3` (SITE)
- // rect 9 (0,0) â 0x03ed = same as rect 8
- // rect 10 (66,79) â 0x03f9 = `_InterfaceHelp(0)` (note-area help)
- const Common::Rect kNotebookRect(78, 12, 288, 152);
- const Common::Rect kBtnHelp1 ( 93, 174, 115, 190); // [1] HELP
- const Common::Rect kBtnGallery (157, 174, 178, 190); // [2] GALLERY
- const Common::Rect kBtnPartner ( 5, 80, 44, 110); // [3] KD HELP
- const Common::Rect kBtnAccuse (180, 174, 201, 190); // [4] SOLVE
- const Common::Rect kBtnPageNext(204, 174, 224, 190); // [5] PAGE NEXT
- const Common::Rect kBtnPagePrev(226, 174, 247, 190); // [6] PAGE PREV
- const Common::Rect kBtnMap ( 7, 177, 57, 200); // [7] MAP
- const Common::Rect kBtnSite ( 35, 111, 56, 136); // [8] SITE
- const Common::Rect kNoteArea ( 66, 79, 267, 174); // [10] note area
-
- CursorMan.showMouse(true);
-
- int page = 0;
- int hoveredNoteSlot = -1;
-
- // Build a list of found-clue indices, identical ordering to the
- // original's iteration through `_CluesFound[]`.
- auto buildFound = [&]() {
- Common::Array<uint> found;
- for (uint i = 0; i < Mystery::kCluesFoundCap; i++)
- if (_mystery._cluesFound[i])
- found.push_back(i);
- return found;
- };
-
- auto draw = [&]() {
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
-
- // PIC 0x3f frame.
- Picture frame;
- if (_picsArchive.getPicture(0x3f, frame)) {
- const int w = MIN<int>(frame.surface.w, 320);
- const int h = MIN<int>(frame.surface.h, 200);
- for (int row = 0; row < h; row++)
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)frame.surface.getBasePtr(0, row), w);
- }
-
- // Partner sprite at (5, 80). Anim 1 for Jake, 0xb (11) for Jenny.
- const uint partnerAnim = (_partner == 0) ? 1 : 0xb;
- Animation partnerAni;
- if (_aniArchive.loadAnimation(partnerAnim, partnerAni) && !partnerAni.empty()) {
- const uint32 now = g_system->getMillis();
- const uint frameIdx = (uint)((now / 100) % partnerAni.size());
- const Picture &fr = partnerAni[frameIdx];
- const byte transp = (byte)(fr.flags >> 8);
- for (int row = 0; row < fr.surface.h; row++) {
- const int dstY = 80 + row;
- if (dstY < 0 || dstY >= 200) continue;
- const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, dstY);
- for (int col = 0; col < fr.surface.w; col++) {
- const int dstX = 5 + col;
- if (dstX < 0 || dstX >= 320) continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
- }
-
- // Notes â `_DrawNotes` walks `_NoteIndex` for the current page,
- // rendering each found clue's text inside `_NotebookRect` with
- // word-wrap. Selected clues are highlighted (color 0x3c in the
- // original's case-briefing palette).
- const Common::Array<uint> found = buildFound();
- const byte *ni = _mystery.noteIndex();
- const uint16 niCount = _mystery.noteIndexCount();
-
- const int kRectX = kNotebookRect.left;
- const int kRectY = kNotebookRect.top;
- const int kRectW = kNotebookRect.width();
- const int kRectH = kNotebookRect.height();
-
- // Walk forward to the start clue of the current page.
- // Each page renders as many clues as fit in `kRectH`.
- int clueCursor = 0;
- Common::Array<int> pageStarts;
- pageStarts.push_back(0);
- {
- const int lineH = _font.getFontHeight() + 1;
- int y = kRectY;
- while (clueCursor < (int)found.size()) {
- const uint clueId = found[clueCursor];
- Common::String txt;
- if (ni && clueId < niCount) {
- const uint16 textOff = READ_LE_UINT16(ni + clueId * 4);
- txt = parseString(_mystery.textAt(textOff),
- _playerName, _partner);
- }
- // Measure height by wrapping the text without drawing.
- Common::Array<Common::String> wrapped;
- _font.wordWrapText(txt, kRectW, wrapped);
- const int h = (int)wrapped.size() * lineH;
- if (y + h + 7 > kRectY + kRectH) {
- // Page break before this clue.
- y = kRectY;
- pageStarts.push_back(clueCursor);
- }
- y += h + 7;
- clueCursor++;
- }
- if (page >= (int)pageStarts.size())
- page = (int)pageStarts.size() - 1;
- if (page < 0)
- page = 0;
- }
-
- // Track per-slot rectangles so the click handler can map a
- // click in `kNoteArea` back to a clue index.
- Common::Array<Common::Rect> slotRects;
- Common::Array<uint> slotClues;
-
- const int startClue = (page < (int)pageStarts.size())
- ? pageStarts[page] : 0;
- const int endClue = (page + 1 < (int)pageStarts.size())
- ? pageStarts[page + 1] : (int)found.size();
-
- int y = kRectY;
- for (int i = startClue; i < endClue; i++) {
- const uint clueId = found[i];
- Common::String txt;
- if (ni && clueId < niCount) {
- const uint16 textOff = READ_LE_UINT16(ni + clueId * 4);
- txt = parseString(_mystery.textAt(textOff),
- _playerName, _partner);
- }
- if (txt.empty())
- txt = Common::String::format("clue %u", clueId);
- // Per `_DrawNotes @ 161e:01d0`: text uses
- // `_NoteUnselectedColor` (0x5c=cyan) for unselected and 0x3c
- // (light yellow-white) for selected. Both contrast cleanly
- // with the PDA screen's natural blue, so we draw text
- // directly on PIC 0x3f without an extra fill rectangle â
- // matches the original design.
- Common::Array<Common::String> wrapped;
- _font.wordWrapText(txt, kRectW, wrapped);
- const int lineH = _font.getFontHeight() + 1;
- const int h = (int)wrapped.size() * lineH;
- const byte color = _mystery._noteSelected[clueId] ? 0x3C : 0x5C;
- for (uint li = 0; li < wrapped.size(); li++) {
- _font.drawString(&scratch, wrapped[li], kRectX,
- y + (int)li * lineH, kRectW, color);
- }
- slotRects.push_back(Common::Rect(kRectX, y,
- kRectX + kRectW, y + h));
- slotClues.push_back(clueId);
- y += h + 7;
- }
-
- // Page indicator + selected-points counter directly on PIC.
- _font.drawString(&scratch, Common::String::format("p%d/%d",
- page + 1, (int)pageStarts.size()),
- 270, 4, 320, 0x5C);
- _font.drawString(&scratch, Common::String::format("%d pts",
- _mystery.selectedPoints()),
- 270, 14, 320, 0x5C);
- (void)hoveredNoteSlot;
-
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
-
- // Stash slot info on the captures so the click handler below
- // can use it via the closure.
- _notebookSlotRects = slotRects;
- _notebookSlotClues = slotClues;
- };
-
- draw();
-
- uint32 lastDraw = g_system->getMillis();
-
- while (!shouldQuit()) {
- Common::Event ev;
- bool dirty = false;
- bool exitFlag = false;
- while (g_system->getEventManager()->pollEvent(ev)) {
- if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
- exitFlag = true;
- break;
- }
- if (ev.type == Common::EVENT_KEYDOWN) {
- if (ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
- exitFlag = true;
- break;
- }
- if (ev.kbd.keycode == Common::KEYCODE_LEFT ||
- ev.kbd.keycode == Common::KEYCODE_PAGEUP) {
- if (page > 0) page--;
- dirty = true;
- } else if (ev.kbd.keycode == Common::KEYCODE_RIGHT ||
- ev.kbd.keycode == Common::KEYCODE_PAGEDOWN ||
- ev.kbd.keycode == Common::KEYCODE_TAB) {
- page++;
- dirty = true;
- } else if (ev.kbd.keycode == Common::KEYCODE_h) {
- doHelp();
- dirty = true;
- }
- }
- if (ev.type == Common::EVENT_LBUTTONDOWN) {
- // Test buttons in the order the original would â
- // button 0 / 9 are dead zones, so check the actionable
- // rects directly. Earlier rects "win" when overlapping
- // (matches `_FindButton`).
- if (kBtnSite.contains(ev.mouse.x, ev.mouse.y)) {
- exitFlag = true;
- break; // back to site
- }
- if (kBtnMap.contains(ev.mouse.x, ev.mouse.y)) {
- doBigMap();
- exitFlag = true;
- break;
- }
- if (kBtnPartner.contains(ev.mouse.x, ev.mouse.y)) {
- doHelp(); // _KDHelp = host hint
- dirty = true;
- continue;
- }
- if (kBtnAccuse.contains(ev.mouse.x, ev.mouse.y)) {
- doAccuse();
- exitFlag = true;
- break;
- }
- if (kBtnGallery.contains(ev.mouse.x, ev.mouse.y)) {
- doGallery();
- dirty = true;
- continue;
- }
- if (kBtnHelp1.contains(ev.mouse.x, ev.mouse.y)) {
- // rect 1 â `_InterfaceHelp(0)`: walks `HelpData[0]` and
- // blits PICs 0x63 / 0x1ae fullscreen for click-through.
- doInterfaceHelp(0);
- dirty = true;
- continue;
- }
- if (kBtnPagePrev.contains(ev.mouse.x, ev.mouse.y)) {
- if (page > 0) page--;
- dirty = true;
- continue;
- }
- if (kBtnPageNext.contains(ev.mouse.x, ev.mouse.y)) {
- page++;
- dirty = true;
- continue;
- }
- if (kNoteArea.contains(ev.mouse.x, ev.mouse.y)) {
- // Toggle the selection on whichever clue's text
- // the click landed in. The original calls
- // `_InterfaceHelp` here; that's the help screen,
- // not selection â selection is in the Accuse
- // screen. We use the area for selection because
- // keyboard 1..9 toggling is awkward, and the
- // resulting `_NoteSelected` state is what
- // `_SolvedCheck` reads.
- for (uint i = 0; i < _notebookSlotRects.size(); i++) {
- if (_notebookSlotRects[i].contains(ev.mouse.x,
- ev.mouse.y)) {
- const uint clueId = _notebookSlotClues[i];
- _mystery._noteSelected[clueId] ^= 1;
- dirty = true;
- break;
- }
- }
- continue;
- }
- }
- }
- if (exitFlag)
- break;
-
- const uint32 now = g_system->getMillis();
- // Re-render every 100 ms so the partner sprite cycles frames.
- if (dirty || now - lastDraw >= 100) {
- draw();
- lastDraw = now;
- }
- g_system->updateScreen();
- g_system->delayMillis(15);
- }
-}
-
-void EEMEngine::doGallery() {
- // Mirrors `_DoGallery @ 158f:065b` and `_DrawGallery @ 158f:0046`.
- // Verified directly from the disassembly:
- // * Background: PIC 0x3f (same as PDA).
- // * Partner sprite at (5, 0x50): anim 2 (Jake) / 0x10 (Jenny).
- // `_NewAnimation(5, 0x50, ...)`. NOTE: gallery uses anim 2/0x10,
- // PDA uses 1/0xb â different sprites.
- // * Five fixed slot positions at `29be:0x116` (4 bytes per slot,
- // `{u16 x, u16 y}`):
- // slot 0 = ( 83, 14) slot 3 = (119, 90)
- // slot 1 = (155, 14) slot 4 = (191, 90)
- // slot 2 = (227, 14)
- // * For each logical suspect i in 0..NumSuspects-1:
- // picId = `*(u16 *)(_GalleryData + i * 0x46)` (entry +0).
- // visible = `_InGallery[_NewOrder[i]] != 0`.
- // drawX = positions[_NewOrder[i]].x
- // drawY = positions[_NewOrder[i]].y + (0x48 - pic.height)
- // So portraits are BOTTOM-aligned to baselines 0x48 + pos.y.
- // * Click on portrait via `_SearchSuspects` â `MoreInfo(i)` shows
- // the suspect detail page. ESC returns to PDA.
- // * Frame-cycled @ 100ms via `_CheckFrameRate` + `_UpdateAnimations`
- // + `_GizmoColorCycle`.
- if (!_mystery.isLoaded())
- return;
-
- const byte *gd = _mystery.galleryData();
- if (!gd) {
- warning("doGallery: no GalleryData in mystery %u", _mystery.number());
- return;
- }
-
- CursorMan.showMouse(true);
-
- struct Slot { int x; int y; };
- static const Slot kGallerySlots[5] = {
- { 83, 14 }, // 0
- { 155, 14 }, // 1
- { 227, 14 }, // 2
- { 119, 90 }, // 3
- { 191, 90 } // 4
- };
-
- // Pre-load static elements once.
- Picture galBg;
- const bool haveBg = _picsArchive.getPicture(0x3f, galBg);
-
- // Gallery partner anim â `_DoGallery` calls `_GetAnimation(uVar6)` with
- // uVar6 = 2 (Jake) / 0x10 (Jenny). Different from PDA (1 / 0xb).
- const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
- Animation partnerAni;
- const bool havePartner = _aniArchive.loadAnimation(partnerAnim, partnerAni)
- && !partnerAni.empty();
-
- const uint8 num = _mystery.numSuspects();
-
- // Cache slot rects for click hit-testing.
- Common::Array<Common::Rect> slotRects;
- Common::Array<int> slotSuspect; // logical suspect index in [0, num)
- slotRects.resize(num);
- slotSuspect.resize(num);
- for (uint i = 0; i < num; i++) {
- slotSuspect[i] = -1;
- }
-
- auto drawFrame = [&]() {
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
-
- if (haveBg) {
- const int bw = MIN<int>(galBg.surface.w, 320);
- const int bh = MIN<int>(galBg.surface.h, 200);
- for (int row = 0; row < bh; row++) {
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)galBg.surface.getBasePtr(0, row), bw);
- }
- }
-
- // Partner sprite frame @ (5, 0x50).
- if (havePartner) {
- const uint32 now = g_system->getMillis();
- const uint frameIdx = (uint)((now / 100) % partnerAni.size());
- const Picture &fr = partnerAni[frameIdx];
- const byte transp = (byte)(fr.flags >> 8);
- const int px = 5, py = 0x50;
- for (int row = 0; row < fr.surface.h; row++) {
- const int dstY = py + row;
- if (dstY < 0 || dstY >= 200) continue;
- const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, dstY);
- for (int col = 0; col < fr.surface.w; col++) {
- const int dstX = px + col;
- if (dstX < 0 || dstX >= 320) continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
- }
-
- // Portraits â `_DrawGallery @ 158f:0046` walks suspects 0..N-1
- // and only renders those flagged in `_InGallery[NewOrder[i]]`.
- // Undiscovered slots are left empty in the original. We render
- // a darkened placeholder + "?" so the player has visual feedback
- // that suspects exist but are still unknown.
- uint discoveredCount = 0;
- for (uint i = 0; i < num && i < Mystery::kGalleryCap; i++) {
- slotRects[i] = Common::Rect();
- slotSuspect[i] = -1;
-
- const uint8 phys = _mystery._newOrder[i];
- if (phys >= 5)
- continue;
- const Slot &s = kGallerySlots[phys];
-
- const bool discovered = _mystery._inGallery[phys] != 0;
- if (discovered) {
- const uint16 picId = READ_LE_UINT16(gd + i * 0x46);
- Picture portrait;
- if (picId == 0 ||
- !_picsArchive.getPicture(picId, portrait))
- continue;
-
- const int placeX = s.x;
- const int placeY = s.y + (0x48 - portrait.surface.h);
- const byte transp = (byte)(portrait.flags >> 8);
- const int w = MIN<int>(portrait.surface.w, 320 - placeX);
- const int h = MIN<int>(portrait.surface.h, 200 - placeY);
- if (w <= 0 || h <= 0)
- continue;
- for (int row = 0; row < h; row++) {
- const int dstY = placeY + row;
- if (dstY < 0) continue;
- const byte *src =
- (const byte *)portrait.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, dstY);
- for (int col = 0; col < w; col++) {
- const int dstX = placeX + col;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
- slotRects[i] = Common::Rect(placeX, placeY,
- placeX + w, placeY + h);
- slotSuspect[i] = (int)i;
- discoveredCount++;
- } else {
- // Undiscovered placeholder â small framed "?" box at
- // (s.x, s.y) sized 0x40 Ã 0x48 (typical portrait size).
- const int phW = 0x40, phH = 0x48;
- const int phX = s.x, phY = s.y;
- if (phX + phW <= 320 && phY + phH <= 200) {
- scratch.fillRect(Common::Rect(phX, phY,
- phX + phW, phY + phH), 0x20);
- scratch.frameRect(Common::Rect(phX, phY,
- phX + phW, phY + phH), 0x5C);
- if (_font.isLoaded()) {
- _font.drawString(&scratch, "?",
- phX + phW / 2 - 3,
- phY + phH / 2 - 4, phW, 0x5C);
- }
- }
- }
- }
- (void)discoveredCount;
-
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
- };
-
- drawFrame();
- uint32 lastDraw = g_system->getMillis();
-
- while (!shouldQuit()) {
- Common::Event ev;
- bool exitFlag = false;
- while (g_system->getEventManager()->pollEvent(ev)) {
- if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
- return;
- }
- if (ev.type == Common::EVENT_KEYDOWN) {
- if (ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
- exitFlag = true;
- break;
- }
- }
- if (ev.type == Common::EVENT_LBUTTONDOWN) {
- // PDA bottom-bar buttons mirror `_NoteButtons @ 29be:0147`.
- // `_DoGallery @ 158f:065b` shares the SAME button table
- // with `_DoNotebook` (both call `_FindButton` against the
- // 11-entry table at 0x147). `_HandleGalleryButton @
- // 158f:05c0` dispatches via a different jump table
- // (158f:0645). Verified gallery button mapping:
- // rect 0 (134,155) â 0x05ef = `_NextScreen = 4` (NOTEBOOK)
- // rect 1 (93,115) â 0x0625 = `_InterfaceHelp` (HELP)
- // rect 2 (157,178) â 0x0638 = generic exit (no-op)
- // rect 3 (5,80) â 0x061e = `_KDHelp` (host hint)
- // rect 4 (180,201) â 0x05ff = `_SolvedCheck` -> SOLVE
- // rect 5 (204,224) â 0x0638 = generic exit
- // rect 6 (226,247) â 0x0638 = generic exit
- // rect 7 (7,177) â 0x05f7 = `_NextScreen = 2` (MAP)
- // rect 8 (35,111) â 0x05e4 = `_NextScreen = 3` (SITE)
- const Common::Rect kBtnSite ( 35, 111, 56, 136); // [8] SITE
- const Common::Rect kBtnMap ( 7, 177, 57, 200); // [7] MAP
- const Common::Rect kBtnAccuse (180, 174, 201, 190); // [4] SOLVE
- const Common::Rect kBtnNotebook(134, 174, 155, 190); // [0] NOTEBOOK (back to PDA notes)
- const Common::Rect kBtnHelp ( 93, 174, 115, 190); // [1] HELP
- const Common::Rect kBtnPartner ( 5, 80, 44, 110); // [3] KD HELP
- if (kBtnSite.contains(ev.mouse.x, ev.mouse.y)) {
- exitFlag = true; break;
- }
- if (kBtnMap.contains(ev.mouse.x, ev.mouse.y)) {
- doBigMap();
- exitFlag = true; break;
- }
- if (kBtnAccuse.contains(ev.mouse.x, ev.mouse.y)) {
- doAccuse();
- exitFlag = true; break;
- }
- if (kBtnNotebook.contains(ev.mouse.x, ev.mouse.y)) {
- // Already came from notebook; exiting returns to it.
- exitFlag = true; break;
- }
- if (kBtnHelp.contains(ev.mouse.x, ev.mouse.y)) {
- // Gallery rect 1 â `_InterfaceHelp(0)` per jmp table at
- // 158f:0625 (HandleGalleryButton). Same picture sequence
- // as the notebook HELP button.
- doInterfaceHelp(0);
- lastDraw = 0;
- continue;
- }
- if (kBtnPartner.contains(ev.mouse.x, ev.mouse.y)) {
- doHelp();
- lastDraw = 0;
- continue;
- }
- // `_SearchSuspects` walks the per-slot rects and returns
- // the suspect index. We mirror that with cached rects.
- bool clicked = false;
- for (uint i = 0; i < slotRects.size(); i++) {
- if (slotSuspect[i] < 0) continue;
- if (slotRects[i].contains(ev.mouse.x, ev.mouse.y)) {
- // `MoreInfo(i)` â show the suspect detail page.
- // Mirrors `MoreInfo @ 158f:0419`:
- // _RefreshGalleryBackground();
- // _GetPicture(*(u16*)(gd + i*0x46));
- // _AddPicBackground(pic, 0x94, 0xf);
- // _DrawGalleryNotes(gd + i*0x46);
- // loop until ESC or button click.
- // Suspect data layout (verified against M1):
- // +0..1: picId (used here AND for gallery slot)
- // +8..9: number of clues for this suspect
- // +0xa..??: array of u16 clue IDs (terminated
- // by 0xFFFF if shorter than count).
- const uint suspectIdx = (uint)slotSuspect[i];
- const byte *suspect = gd + suspectIdx * 0x46;
- const uint16 detailPic =
- READ_LE_UINT16(suspect + 0);
- const uint16 clueCount =
- READ_LE_UINT16(suspect + 8);
-
- Graphics::ManagedSurface ms(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- ms.clear();
- if (haveBg) {
- const int bw = MIN<int>(galBg.surface.w, 320);
- const int bh = MIN<int>(galBg.surface.h, 200);
- for (int row = 0; row < bh; row++) {
- memcpy((byte *)ms.getBasePtr(0, row),
- (const byte *)galBg.surface.getBasePtr(0, row), bw);
- }
- }
- // Full suspect picture at (0x94, 0xf).
- Picture detail;
- if (_picsArchive.getPicture(detailPic, detail)) {
- const byte transp =
- (byte)(detail.flags >> 8);
- const int dx = 0x94, dy = 0x0f;
- const int dw = MIN<int>(detail.surface.w, 320 - dx);
- const int dh = MIN<int>(detail.surface.h, 200 - dy);
- for (int row = 0; row < dh; row++) {
- const byte *src =
- (const byte *)detail.surface.getBasePtr(0, row);
- byte *dst =
- (byte *)ms.getBasePtr(0, dy + row);
- for (int col = 0; col < dw; col++) {
- if (src[col] != transp)
- dst[dx + col] = src[col];
- }
- }
- }
- // Suspect's clue notes inside _GalleryNoteRect
- // = (78, 93, 288, 152), per 29be:0100. Cyan text
- // renders directly on the PDA's natural blue
- // screen â matches `_DrawGalleryNotes @ 158f:01f4`.
- const int rx = 78, ry = 93;
- const int rw = 288 - 78, rh = 152 - 93;
-
- const byte *ni = _mystery.noteIndex();
- const uint16 niCount = _mystery.noteIndexCount();
- int yPos = ry;
- const int lineH = _font.getFontHeight() + 1;
- bool drewAny = false;
- for (uint k = 0; k < clueCount && k < 30; k++) {
- const uint16 clueId =
- READ_LE_UINT16(suspect + 0xa + k * 2);
- if (clueId == 0xFFFF) break;
- if (clueId >= Mystery::kCluesFoundCap ||
- !_mystery._cluesFound[clueId])
- continue;
- if (!ni || clueId >= niCount) continue;
- const uint16 textOff =
- READ_LE_UINT16(ni + clueId * 4);
- Common::String txt =
- parseString(_mystery.textAt(textOff),
- _playerName, _partner);
- if (txt.empty()) continue;
- const byte color =
- _mystery._noteSelected[clueId] ? 0x3C : 0x5C;
- const int hLine = _font.drawWordWrapped(
- &ms, rx, yPos, rw, txt, color);
- yPos += hLine + 7;
- drewAny = true;
- if (yPos + lineH > ry + rh) break;
- }
- if (!drewAny && _font.isLoaded()) {
- _font.drawString(&ms,
- "No clues yet for this suspect.",
- rx, ry, rw, 0x5C);
- }
- // Header / footer text.
- if (_font.isLoaded()) {
- _font.drawString(&ms, "SUSPECT FILE",
- rx, ry - 11, rw, 0x3C);
- _font.drawString(&ms, "(click / ESC: back)",
- rx, ry + rh + 2, rw, 0x3C);
- }
- g_system->copyRectToScreen(ms.getPixels(),
- ms.pitch, 0, 0, 320, 200);
- g_system->updateScreen();
-
- // Wait for click or ESC. Drain the queued
- // LBUTTONDOWN that triggered this MoreInfo first
- // so we don't immediately accept it as the
- // dismiss event.
- g_system->delayMillis(150);
- {
- Common::Event drain;
- while (g_system->getEventManager()->pollEvent(drain)) {
- if (drain.type == Common::EVENT_QUIT ||
- drain.type == Common::EVENT_RETURN_TO_LAUNCHER)
- return;
- }
- }
- bool back = false;
- while (!back && !shouldQuit()) {
- Common::Event e2;
- while (g_system->getEventManager()->pollEvent(e2)) {
- if (e2.type == Common::EVENT_LBUTTONDOWN ||
- (e2.type == Common::EVENT_KEYDOWN &&
- (e2.kbd.keycode == Common::KEYCODE_ESCAPE ||
- e2.kbd.keycode == Common::KEYCODE_RETURN))) {
- back = true;
- break;
- }
- if (e2.type == Common::EVENT_QUIT ||
- e2.type == Common::EVENT_RETURN_TO_LAUNCHER)
- return;
- }
- g_system->delayMillis(20);
- }
- // Force gallery redraw immediately so the
- // player isn't left looking at the dismissed
- // MoreInfo screen until the next 100 ms tick.
- drawFrame();
- lastDraw = g_system->getMillis();
- clicked = true;
- break;
- }
- }
- (void)clicked;
- }
- }
- if (exitFlag) break;
-
- const uint32 now = g_system->getMillis();
- if (now - lastDraw >= 100) {
- drawFrame();
- lastDraw = now;
- }
- g_system->delayMillis(15);
- }
-}
-
-void EEMEngine::doBigMap() {
- // Two-stage flow that mirrors the original screen-1 wrapper at
- // 20fe:120b and `_DoBigMap @ 20fe:09e7`:
- //
- // STAGE 1 â Overview. PIC 0x42 + site icons drawn via the
- // `_DrawBigMapButtons` algorithm at BigMap coords MapData[+4/+6].
- // The original `_DoBigMap` returns sx/sy = (mouseX*2 - 0x74,
- // mouseY*2 - 0x55) when the player clicks inside `BigMapWindow`,
- // which is the scroll position into the SmallMap.
- //
- // STAGE 2 â Detail zoom. PIC 0x43 frame + a 0xe9 Ã 0xab viewport
- // into BIGMAP.PIC at (2, 2), drawn by `DrawMap @ 20fe:1058` with
- // the (sx, sy) returned from stage 1. Site icons are stamped at
- // SmallMap coords MapData[+8/+0xa] via `_StampButtons`. Click on
- // a site icon â travel.
- //
- // MapData entry layout (14 bytes), verified directly from the
- // disassembly of `_DrawBigMapButtons @ 20fe:0877` (`PUSH ES:[BX+4]`
- // for X, `PUSH ES:[BX+6]` for Y, `CMP ES:[BX+0xc], 0` for crime)
- // and `_StampButtons @ 20fe:0d2f` (`MOV AX, ES:[BX+8]`,
- // `MOV AX, ES:[BX+0xa]`):
- // +0..3 ??? (not yet decoded)
- // +4..5 BigMap X
- // +6..7 BigMap Y
- // +8..9 SmallMap X
- // +0xa..b SmallMap Y
- // +0xc..d crime-flag
-
- if (!_mystery.isLoaded())
- return;
-
- CursorMan.showMouse(true);
-
- // `_GetPalette(0x24)` per `_DoBigMap @ 20fe:09e7`.
- setSitePalette(0x24);
-
- const Common::Rect kSetupRect(0xc7, 0x12, 0xc7 + 0x32, 0x12 + 0xa); // approx; original from globals
- (void)kSetupRect; // not yet wired into our overlay
-
- // ------------------------------------------------------------------
- // STAGE 1 â Overview: PIC 0x42 + clickable site icons.
- // ------------------------------------------------------------------
-
- // `_DoBigMap @ 20fe:09e7` (20fe:0a44-0a99) registers a partner sprite
- // on the overview frame. The animation depends on `_LastScreen`:
- // * When LastScreen == 2 (came from the site loop) the original
- // plays an entrance anim (`anum-1` for Jake / Jenny) at
- // (0x102, 0x50), then on END swaps to the idle anim at (0xfd,
- // 0x50). We don't track LastScreen finely enough to distinguish,
- // so we render the IDLE pose at (0xfd, 0x50) which is what the
- // player sees the rest of the time anyway.
- // * Idle anim ID: Jake = 0x14 (20), Jenny = 0x12 (18).
- const uint kMapAniId = (_partner == 0) ? 0x14 : 0x12;
- Animation mapAnim;
- const bool haveMapAnim = _aniArchive.loadAnimation(kMapAniId, mapAnim)
- && !mapAnim.empty();
- const int kMapAnimX = 0xfd;
- const int kMapAnimY = 0x50;
-
- auto drawOverview = [&]() {
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
-
- Picture frame;
- if (_picsArchive.getPicture(0x42, frame)) {
- const int w = MIN<int>(frame.surface.w, 320);
- const int h = MIN<int>(frame.surface.h, 200);
- for (int row = 0; row < h; row++)
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)frame.surface.getBasePtr(0, row), w);
- }
-
- // Marker PICs from `_main @ 1a35:0f59`. Three globals are filled
- // once at boot via `_GetPicture` (1-based IDs â entries N-1):
- // _DoneMarker = PIC 0x20d (already-searched site)
- // _SiteMarker = PIC 0xc5 (default available site)
- // _CrimeMarker = PIC 0xc6 (crime-scene flag set)
- // Picked per-site by `_DrawBigMapButtons @ 20fe:0877`:
- // 1. SaveSiteComplete[i] â DoneMarker
- // 2. else MapData[+0xc] != 0 â CrimeMarker
- // 3. else SiteMarker
- Picture done, normal, crimeM;
- const bool haveDone = _picsArchive.getPicture(0x20d, done);
- const bool haveNormal = _picsArchive.getPicture(0xc5, normal);
- const bool haveCrime = _picsArchive.getPicture(0xc6, crimeM);
-
- auto blitMarker = [&](const Picture &m, int x, int y) {
- const byte transp = (byte)(m.flags >> 8);
- for (int row = 0; row < m.surface.h; row++) {
- const int dstY = y + row;
- if (dstY < 0 || dstY >= 200) continue;
- const byte *src = (const byte *)m.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, dstY);
- for (int col = 0; col < m.surface.w; col++) {
- const int dstX = x + col;
- if (dstX < 0 || dstX >= 320) continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
- };
-
- for (uint i = 0; i < _mystery.numSites(); i++) {
- if (!_mystery._onSites[i] && i != _mystery._siteNumber)
- continue;
- const byte *entry = _mystery.mapEntry(i);
- if (!entry)
- continue;
- const uint16 mx = READ_LE_UINT16(entry + 0x4);
- const uint16 my = READ_LE_UINT16(entry + 0x6);
- const uint16 crime = READ_LE_UINT16(entry + 0xc);
- const bool done_ = (i < Mystery::kVisitedSiteCap)
- && _mystery._visitedSite[i];
-
- const Picture *m = nullptr;
- if (done_ && haveDone) m = &done;
- else if (crime != 0 && haveCrime) m = &crimeM;
- else if (haveNormal) m = &normal;
-
- if (m)
- blitMarker(*m, (int)mx, (int)my);
- else {
- // Fallback if the markers couldn't be loaded.
- const Common::Rect mark(mx - 3, my - 3, mx + 4, my + 4);
- scratch.fillRect(mark, 0x0F);
- }
- }
-
- // Partner sprite â masked-blit at (0xfd, 0x50). Same per-tick
- // idle the original would run via `_UpdateAnimations` once the
- // entrance one-shot transitions out (see `_DoBigMap` 0xae3-0xae7
- // where it swaps animId on the 0x80 marker).
- if (haveMapAnim) {
- const uint32 now = g_system->getMillis();
- const uint frameIdx = (uint)((now / 100) % mapAnim.size());
- const Picture &fr = mapAnim[frameIdx];
- const byte transp = (byte)(fr.flags >> 8);
- for (int row = 0; row < fr.surface.h; row++) {
- const int dstY = kMapAnimY + row;
- if (dstY < 0 || dstY >= 200) continue;
- const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, dstY);
- for (int col = 0; col < fr.surface.w; col++) {
- const int dstX = kMapAnimX + col;
- if (dstX < 0 || dstX >= 320) continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
- }
-
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
- };
-
- drawOverview();
- uint32 mapLastTick = g_system->getMillis();
-
- // Static rectangles read directly from the binary at the labelled
- // addresses (29be:0x1596 onwards). Format is {x1, y1, x2, y2}.
- const Common::Rect kBigMapWindow ( 0, 0, 247, 192); // 29be:1596
- const Common::Rect kSetupBtnRect (252, 4, 315, 42); // 29be:15ce
-
- bool wantZoom = false;
- int zoomX = 0, zoomY = 0;
- while (!shouldQuit()) {
- Common::Event ev;
- while (g_system->getEventManager()->pollEvent(ev)) {
- if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
- return;
- if (ev.type == Common::EVENT_KEYDOWN &&
- ev.kbd.keycode == Common::KEYCODE_ESCAPE)
- return;
- if (ev.type == Common::EVENT_LBUTTONDOWN) {
- // SetupButtonRect â `_NextScreen = 6` (the original's
- // settings screen). We use it as "back to menu":
- // abandon the current mystery and return to case
- // selection.
- if (kSetupBtnRect.contains(ev.mouse.x, ev.mouse.y)) {
- _mystery.clear();
- _nextScreen = kScreenInvalid;
- return;
- }
- // Click in the BigMapWindow â zoom. Original formula:
- // sx = mouseX*2 - 0x74; sy = mouseY*2 - 0x55
- if (kBigMapWindow.contains(ev.mouse.x, ev.mouse.y)) {
- int sx = ev.mouse.x * 2;
- int sy = ev.mouse.y * 2;
- sx = (sx < 0x75) ? 0 : sx - 0x74;
- sy = (sy < 0x56) ? 0 : sy - 0x55;
- zoomX = sx;
- zoomY = sy;
- wantZoom = true;
- break;
- }
- }
- }
- if (wantZoom)
- break;
- // Cycle the partner-sprite frame every 100 ms (matching the
- // original's `_CheckFrameRate` cadence inside `_DoBigMap`).
- const uint32 now = g_system->getMillis();
- if (haveMapAnim && now - mapLastTick >= 100) {
- mapLastTick = now;
- drawOverview();
- }
- g_system->updateScreen();
- g_system->delayMillis(10);
- }
-
- if (!wantZoom)
- return;
-
- // ------------------------------------------------------------------
- // STAGE 2 â Detail zoom: PIC 0x43 frame + scrollable BIGMAP.PIC
- // viewport at (2, 2), 0xe9 Ã 0xab. Click on a stamped icon â travel.
- // ------------------------------------------------------------------
-
- Common::File f;
- if (!f.open(Common::Path("BIGMAP.PIC"))) {
- warning("doBigMap: BIGMAP.PIC missing for detail view");
- return;
- }
- const uint16 mapH = f.readUint16LE();
- const uint16 mapW = f.readUint16LE();
- if (mapW == 0 || mapH == 0)
- return;
- Common::Array<byte> mapPixels((uint32)mapW * mapH);
- if (f.read(mapPixels.data(), mapPixels.size()) != mapPixels.size()) {
- warning("doBigMap: short read on BIGMAP.PIC for detail view");
- return;
- }
-
- const int kMapWinW = 0xe9; // 233
- const int kMapWinH = 0xab; // 171
- const int kMapWinX = 2;
- const int kMapWinY = 2;
-
- int scrollX = MAX<int>(0, MIN<int>(mapW - kMapWinW, zoomX));
- int scrollY = MAX<int>(0, MIN<int>(mapH - kMapWinH, zoomY));
-
- // `_DoMapScreen @ 20fe:120b` (20fe:12cd-12f0): partner sprite on
- // the detail-zoom screen. Jake = anim 0x13 (19), Jenny = anim 0x11
- // (17). Position (0x101, 0x50) = (257, 80), seqnum 0x13. The cells
- // here have a "looking at the map" pose, distinct from the BigMap
- // overview entrance/idle.
- const uint kDetailAniId = (_partner == 0) ? 0x13 : 0x11;
- Animation detailAnim;
- const bool haveDetailAnim = _aniArchive.loadAnimation(kDetailAniId,
- detailAnim)
- && !detailAnim.empty();
- const int kDetailAnimX = 0x101;
- const int kDetailAnimY = 0x50;
-
- auto drawDetail = [&]() {
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
-
- Picture frame;
- if (_picsArchive.getPicture(0x43, frame)) {
- const int w = MIN<int>(frame.surface.w, 320);
- const int h = MIN<int>(frame.surface.h, 200);
- for (int row = 0; row < h; row++)
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)frame.surface.getBasePtr(0, row), w);
- }
-
- const int copyW = MIN<int>(mapW - scrollX, kMapWinW);
- const int copyH = MIN<int>(mapH - scrollY, kMapWinH);
- for (int row = 0; row < copyH; row++) {
- memcpy((byte *)scratch.getBasePtr(kMapWinX, kMapWinY + row),
- mapPixels.data() + (scrollY + row) * mapW + scrollX,
- copyW);
- }
-
- // Stamped site buttons. `_StampButtons @ 20fe:0d2f` does:
- // button = _GetButton(MapData[+0]) // BUTTON.DBD entry
- // destX = MapData[+8], destY = MapData[+0xa]
- // then bakes the button PIC into the map bitmap. Each button
- // sprite carries the site name baked in. We blit them on top
- // of the BIGMAP.PIC viewport at the same SmallMap coords.
- for (uint i = 0; i < _mystery.numSites(); i++) {
- if (!_mystery._onSites[i] && i != _mystery._siteNumber)
- continue;
- const byte *entry = _mystery.mapEntry(i);
- if (!entry) continue;
- const uint16 buttonId = READ_LE_UINT16(entry + 0x0);
- const uint16 mx = READ_LE_UINT16(entry + 0x8);
- const uint16 my = READ_LE_UINT16(entry + 0xa);
-
- Picture button;
- if (!_buttonArchive.loadEntry(buttonId, button))
- continue;
- const int sx = (int)mx - scrollX + kMapWinX;
- const int sy = (int)my - scrollY + kMapWinY;
- const byte transp = (byte)(button.flags >> 8);
-
- // Crop blit against the viewport.
- const int x0 = MAX<int>(sx, kMapWinX);
- const int y0 = MAX<int>(sy, kMapWinY);
- const int x1 = MIN<int>(sx + button.surface.w, kMapWinX + kMapWinW);
- const int y1 = MIN<int>(sy + button.surface.h, kMapWinY + kMapWinH);
- for (int row = y0; row < y1; row++) {
- const byte *src = (const byte *)button.surface.getBasePtr(0, row - sy);
- byte *dst = (byte *)scratch.getBasePtr(0, row);
- for (int col = x0; col < x1; col++) {
- const byte px = src[col - sx];
- if (px != transp)
- dst[col] = px;
- }
- }
- }
-
- // Partner sprite on the detail map. Drawn last so it sits over
- // the frame and the BIGMAP.PIC viewport.
- if (haveDetailAnim) {
- const uint32 now = g_system->getMillis();
- const uint frameIdx =
- (uint)((now / 100) % detailAnim.size());
- const Picture &fr = detailAnim[frameIdx];
- const byte transp = (byte)(fr.flags >> 8);
- for (int row = 0; row < fr.surface.h; row++) {
- const int dstY = kDetailAnimY + row;
- if (dstY < 0 || dstY >= 200) continue;
- const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, dstY);
- for (int col = 0; col < fr.surface.w; col++) {
- const int dstX = kDetailAnimX + col;
- if (dstX < 0 || dstX >= 320) continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
- }
-
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
- };
-
- drawDetail();
- uint32 detailLastTick = g_system->getMillis();
-
- while (!shouldQuit()) {
- Common::Event ev;
- bool dirty = false;
- while (g_system->getEventManager()->pollEvent(ev)) {
- if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
- return;
- if (ev.type == Common::EVENT_KEYDOWN) {
- if (ev.kbd.keycode == Common::KEYCODE_ESCAPE)
- return; // exit detail back to caller (site loop / engine)
- const int kStep = 16;
- if (ev.kbd.keycode == Common::KEYCODE_LEFT) {
- scrollX = MAX<int>(0, scrollX - kStep);
- dirty = true;
- } else if (ev.kbd.keycode == Common::KEYCODE_RIGHT) {
- scrollX = MIN<int>(MAX<int>(0, mapW - kMapWinW),
- scrollX + kStep);
- dirty = true;
- } else if (ev.kbd.keycode == Common::KEYCODE_UP) {
- scrollY = MAX<int>(0, scrollY - kStep);
- dirty = true;
- } else if (ev.kbd.keycode == Common::KEYCODE_DOWN) {
- scrollY = MIN<int>(MAX<int>(0, mapH - kMapWinH),
- scrollY + kStep);
- dirty = true;
- }
- }
- if (ev.type == Common::EVENT_LBUTTONDOWN) {
- // Scroll arrows + slider rects live in `SmallMapButtons`
- // at 29be:0x159e (six 8-byte rects in order: Y-up, Y-down,
- // X-left, X-right, right-panel, top-right) plus the
- // dedicated `XSliderRect @ 29be:15d6` and
- // `YSliderRect @ 29be:15de`. Format {x1,y1,x2,y2}.
- const Common::Rect kArrowYUp (237, 2, 247, 11);
- const Common::Rect kArrowYDown (237, 163, 247, 172);
- const Common::Rect kArrowXLeft ( 2, 175, 12, 185);
- const Common::Rect kArrowXRight(224, 175, 234, 185);
- const Common::Rect kXSlider ( 15, 175, 221, 185);
- const Common::Rect kYSlider (237, 14, 247, 160);
- const Common::Rect kSetupBtn (252, 4, 315, 42);
-
- const int kArrowStep = 16;
- const int kSliderRange = mapW - kMapWinW;
- const int kSliderRangeY = mapH - kMapWinH;
-
- if (kSetupBtn.contains(ev.mouse.x, ev.mouse.y)) {
- // Setup button on detail too â `_NextScreen = 6` in
- // the original. We treat it the same way: bail back
- // to case selection.
- _mystery.clear();
- _nextScreen = kScreenInvalid;
- return;
- }
- if (kArrowYUp.contains(ev.mouse.x, ev.mouse.y)) {
- scrollY = MAX<int>(0, scrollY - kArrowStep);
- dirty = true;
- } else if (kArrowYDown.contains(ev.mouse.x, ev.mouse.y)) {
- scrollY = MIN<int>(MAX<int>(0, kSliderRangeY),
- scrollY + kArrowStep);
- dirty = true;
- } else if (kArrowXLeft.contains(ev.mouse.x, ev.mouse.y)) {
- scrollX = MAX<int>(0, scrollX - kArrowStep);
- dirty = true;
- } else if (kArrowXRight.contains(ev.mouse.x, ev.mouse.y)) {
- scrollX = MIN<int>(MAX<int>(0, kSliderRange),
- scrollX + kArrowStep);
- dirty = true;
- } else if (kXSlider.contains(ev.mouse.x, ev.mouse.y)) {
- // Click on X slider track â jump scrollX so the
- // click position maps proportionally into the map.
- if (kSliderRange > 0) {
- const int t = ev.mouse.x - kXSlider.left;
- const int tw = kXSlider.width();
- scrollX = MAX<int>(0, MIN<int>(kSliderRange,
- t * kSliderRange / MAX<int>(1, tw)));
- dirty = true;
- }
- } else if (kYSlider.contains(ev.mouse.x, ev.mouse.y)) {
- if (kSliderRangeY > 0) {
- const int t = ev.mouse.y - kYSlider.top;
- const int th = kYSlider.height();
- scrollY = MAX<int>(0, MIN<int>(kSliderRangeY,
- t * kSliderRangeY / MAX<int>(1, th)));
- dirty = true;
- }
- } else if (ev.mouse.x >= kMapWinX &&
- ev.mouse.x < kMapWinX + kMapWinW &&
- ev.mouse.y >= kMapWinY &&
- ev.mouse.y < kMapWinY + kMapWinH) {
- // Hit-test the per-site button at its actual bbox
- // (`_StampButtons` records the rect at SmallMap +8/+0xa
- // with the button PIC's width/height).
- for (uint i = 0; i < _mystery.numSites(); i++) {
- if (!_mystery._onSites[i] &&
- i != _mystery._siteNumber)
- continue;
- const byte *entry = _mystery.mapEntry(i);
- if (!entry) continue;
- const uint16 buttonId = READ_LE_UINT16(entry + 0x0);
- const uint16 mx = READ_LE_UINT16(entry + 0x8);
- const uint16 my = READ_LE_UINT16(entry + 0xa);
- Picture button;
- int bw = 16, bh = 16;
- if (_buttonArchive.loadEntry(buttonId, button)) {
- bw = button.surface.w;
- bh = button.surface.h;
- }
- const int sx = (int)mx - scrollX + kMapWinX;
- const int sy = (int)my - scrollY + kMapWinY;
- if (ev.mouse.x >= sx && ev.mouse.x < sx + bw &&
- ev.mouse.y >= sy && ev.mouse.y < sy + bh) {
- _mystery._lastSite = _mystery._siteNumber;
- _mystery._siteNumber = (uint16)i;
- return;
- }
- }
- }
- }
- }
- // Cycle the partner sprite at 100 ms ticks (same cadence as
- // `_DoMapScreen`'s `_CheckFrameRate` + `_UpdateAnimations` loop).
- const uint32 now = g_system->getMillis();
- if (haveDetailAnim && now - detailLastTick >= 100) {
- detailLastTick = now;
- dirty = true;
- }
- if (dirty)
- drawDetail();
- g_system->updateScreen();
- g_system->delayMillis(10);
- }
-}
-
-void EEMEngine::doHelp() {
- // `_KDHelp` reads two hint TextBlock offsets from `_KDTextIndex`:
- // word @ +0xe : first-time hint
- // word @ +0x10: second-time hint (cycles back to first if missing)
- // `_SawHelpHint` toggles between them.
- if (!_mystery.isLoaded() || !_font.isLoaded())
- return;
-
- const byte *kd = _mystery.kdTextIndex();
- if (!kd)
- return;
-
- const uint16 hintFirst = READ_LE_UINT16(kd + 0x0e);
- const uint16 hintSecond = READ_LE_UINT16(kd + 0x10);
- uint16 use = _mystery._sawHelpHint && hintSecond != 0xFFFF ? hintSecond : hintFirst;
- if (use == 0xFFFF) {
- debugC(1, kDebugScript, "doHelp: no hint configured");
- return;
- }
- if (!_mystery._sawHelpHint && hintFirst != 0xFFFF)
- _mystery._sawHelpHint = true;
-
- const Common::String raw = _mystery.textAt(use);
- const Common::String text = parseString(raw, _playerName, _partner);
-
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
- _font.drawString(&scratch, "HELP", 8, 4, 320, 0xF);
- _font.drawWordWrapped(&scratch, 8, 24, 304, text, 0xF);
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
- waitForInput(60000);
-}
-
-void EEMEngine::doInterfaceHelp(uint num) {
- // Mirrors `_InterfaceHelp(num)` @ 1560:0205. The original walks
- // `HelpData @ 29be:00c8` (5-byte entries: u8 count, then up to 2
- // u16 picIds), `_GetPicture`s each one, blits it fullscreen via
- // `_Rect_Move_Mask(0, 0, ...)`, and waits for click / key. ESC ends
- // the cycle; any other input advances to the next pic.
- //
- // Verified from Ghidra HelpData bytes:
- // entry 0 (PDA / gallery HELP button): count=2, picIds = 0x0063, 0x01ae
- // entry 1: count=2, picIds = 0x0192, 0x01b1
- // Only entry 0 is reachable from the PDA notebook (rect 1) and the
- // gallery (rect 1) â both call `_InterfaceHelp(0)`.
- static const uint16 kHelpPics[][2] = {
- { 0x0063, 0x01ae },
- { 0x0192, 0x01b1 },
- };
- if (num >= ARRAYSIZE(kHelpPics))
- return;
-
- debugC(1, kDebugScript, "doInterfaceHelp(%u): showing pics 0x%x, 0x%x",
- num, kHelpPics[num][0], kHelpPics[num][1]);
-
- for (uint i = 0; i < 2; i++) {
- const uint16 picId = kHelpPics[num][i];
- Picture pic;
- if (!_picsArchive.getPicture(picId, pic)) {
- warning("doInterfaceHelp: getPicture(0x%x) failed", picId);
- continue;
- }
- debugC(1, kDebugScript, "doInterfaceHelp: pic 0x%x = %dx%d flags=0x%x",
- picId, pic.surface.w, pic.surface.h, pic.flags);
-
- // Compose a 320x200 frame (cleared) and blit the help pic at (0,0)
- // with the original's masked-blit semantics: pixels equal to the
- // pic's sub-mode (high byte of `pic[0]`, see `_Rect_Move_Mask`
- // param_10 at 1000:03fc) are treated as transparent and skipped.
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
- const byte transp = (byte)(pic.flags >> 8);
- const int w = MIN<int>(pic.surface.w, 320);
- const int h = MIN<int>(pic.surface.h, 200);
- for (int row = 0; row < h; row++) {
- const byte *src = (const byte *)pic.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, row);
- for (int col = 0; col < w; col++) {
- if (src[col] != transp)
- dst[col] = src[col];
- }
- }
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
-
- bool escape = false;
- while (!shouldQuit() && !escape) {
- Common::Event ev;
- bool advance = false;
- while (g_system->getEventManager()->pollEvent(ev)) {
- if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
- return;
- }
- if (ev.type == Common::EVENT_LBUTTONDOWN) {
- advance = true;
- break;
- }
- if (ev.type == Common::EVENT_KEYDOWN) {
- if (ev.kbd.keycode == Common::KEYCODE_ESCAPE)
- escape = true;
- else
- advance = true;
- break;
- }
- }
- if (advance || escape)
- break;
- g_system->updateScreen();
- g_system->delayMillis(15);
- }
- if (escape)
- break;
- }
-}
-
-uint16 EEMEngine::getKDTextBalloon(byte firstChar) const {
- // Mirrors `_GetKDTextBalloon @ 1df2:0105`:
- // if ((ctype[firstChar] & 2) == 0) bub = *(u16*)29be:1068 = 0x17
- // else bub = *(u16*)(29be:0fe6+0x1e+c*2)
- // `ctype` is Borland's `_ctype_` array at `29be:2be1`. Bit 1 (0x02) is
- // set only for digits '0'..'9' (verified by reading the table â '0'..'9'
- // each map to byte 0x02; everything else has bit 1 clear).
- // Lookup table at 29be:1064 (= 29be:0fe6 + 0x1e + '0'*2):
- // '0'â0x15 '1'â0x16 '2'â0x17 '3'â0x18 '4'â0x19
- // '5'â0x1a '6'â0x20 '7'â0x21 '8'â0x22 '9'â0x1e
- // Note `*(u16*)29be:1068` (= entry for '2') is the same byte the
- // non-digit fallback returns â the original encodes the constant by
- // reusing the digit-2 slot.
- if (firstChar < '0' || firstChar > '9')
- return 0x17;
- static const uint16 kDigitBalloons[10] = {
- 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x20, 0x21, 0x22, 0x1e
- };
- return kDigitBalloons[firstChar - '0'];
-}
-
-void EEMEngine::setPartnerEraseBg(const Graphics::ManagedSurface *bg) {
- if (bg && bg->w == 320 && bg->h == 200) {
- _partnerEraseBg.create(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- for (int row = 0; row < 200; row++) {
- memcpy((byte *)_partnerEraseBg.getBasePtr(0, row),
- (const byte *)bg->getBasePtr(0, row), 320);
- }
- } else {
- _partnerEraseBg.free();
- }
-}
-
-void EEMEngine::playKdAnim(uint16 num) {
- // Mirrors `_DoKDAnim(num) @ 168d:028a` + `_PlayAnimation @ 172b:1f46`:
- // _SuspendAnimation(WaitHandle);
- // anim = WaitAnims[1+num].anim[partner] (table @ 29be:0228)
- // x = WaitAnims[1+num].x[partner]
- // y = WaitAnims[1+num].y[partner]
- // _PlayAnimation(anim, x, y, WaitHandle)
- // â registers a state-4 (one-shot) animation slot and lets
- // `_UpdateAnimations` walk the sequence script until 0x80,
- // then frees this slot and re-activates `WaitHandle`.
- // Our port renders the partner's idle inline in each redraw rather
- // than via a slot system, so we play the one-shot synchronously here
- // (blocking) and resume normal idle rendering when the caller
- // returns. That matches the user-visible effect: the partner's
- // gesture (Jenny taking a picture, etc.) finishes before the
- // speaker portrait + speech balloon appear.
- //
- // Six valid kdAnimNum entries (0..5). Verified bytes from
- // `29be:0228`. Layout per entry: { animJake, animJenny, xJake,
- // xJenny, yJake, yJenny }. Position is (6, 80) in every entry.
- static const uint16 kKdAnimTable[6][6] = {
- { 0x03, 0x0c, 6, 6, 80, 80 }, // 0 â speaker idx 1 wait anim
- { 0x01, 0x0b, 6, 6, 80, 80 }, // 1 â same as PDA idle
- { 0x04, 0x0d, 6, 6, 80, 80 }, // 2
- { 0x02, 0x10, 6, 6, 80, 80 }, // 3 â same as gallery
- { 0x05, 0x05, 6, 6, 80, 80 }, // 4 â same anim both partners
- { 0x06, 0x06, 6, 6, 80, 80 }, // 5 â same anim both partners
- };
- if (num >= ARRAYSIZE(kKdAnimTable))
- return;
-
- const uint partner = (_partner == 0) ? 0 : 1;
- const uint16 animId = kKdAnimTable[num][partner];
- const int px = (int)kKdAnimTable[num][2 + partner];
- const int py = (int)kKdAnimTable[num][4 + partner];
-
- Animation anim;
- if (!_aniArchive.loadAnimation(animId, anim) || anim.empty()) {
- warning("playKdAnim(%u): anim %u failed to load", num, animId);
- return;
- }
-
- // Sequence-script lookup. Entries copied verbatim from
- // `_AnimationSequences @ 29be:22d4` walked through to the next 0x80.
- // Each script is a u16[] of frame indices terminated by 0x80; we
- // don't yet handle 0x81 jumps (none of the kdAnim sequences use
- // them â verified). seqnum == animId for these calls (per
- // `_PlayAnimation` 172b:1f5d push order).
- struct Script {
- uint16 seqnum;
- uint8 len;
- uint8 frames[20]; // long enough for any kdAnim script
- };
- static const Script kScripts[] = {
- // seqnum 1 (29be:188a) â head bob
- { 0x01, 15, { 0,1,2,0,1,0,2,1,0,1,0,1,2,1,0 } },
- // seqnum 2 (29be:18aa) â short blip then long pause
- { 0x02, 16, { 0,1,2,1,0,0,0,0,0,0,0,0,0,0,0,0 } },
- // seqnum 3 (29be:18e0) â Jake "lift, hold, lower" gesture
- { 0x03, 9, { 0,1,2,3,2,2,2,1,0 } },
- // seqnum 4 (29be:18f4) â bigger gesture (camera flash-style)
- { 0x04, 13, { 0,1,2,3,4,5,4,4,4,3,2,1,0 } },
- // seqnum 5 (29be:1910) â held idle with a single peak
- { 0x05, 13, { 0,0,0,1,2,3,2,1,0,0,0,0,0 } },
- // seqnum 6 (29be:192c) â empty (immediate END)
- { 0x06, 0, { 0 } },
- // seqnum 0xb (29be:188a, same as 1) â Jenny PDA idle
- { 0x0b, 15, { 0,1,2,0,1,0,2,1,0,1,0,1,2,1,0 } },
- // seqnum 0xc (29be:18e0, same as 3) â Jenny "take a picture"
- { 0x0c, 9, { 0,1,2,3,2,2,2,1,0 } },
- // seqnum 0xd (29be:18f4, same as 4) â Jenny big gesture
- { 0x0d, 13, { 0,1,2,3,4,5,4,4,4,3,2,1,0 } },
- // seqnum 0x10 (29be:1956) â Jenny short anim
- { 0x10, 9, { 0,0,0,1,0,0,0,0,0 } },
- };
- const uint8 *frames = nullptr;
- uint frameCount = 0;
- for (uint i = 0; i < ARRAYSIZE(kScripts); i++) {
- if (kScripts[i].seqnum == animId) {
- frames = kScripts[i].frames;
- frameCount = kScripts[i].len;
- break;
- }
- }
- if (frameCount == 0) {
- // Fallback: linear playback through anim cells (better than
- // nothing if a future kdAnim references an unscripted anim).
- frameCount = (uint)anim.size();
- }
-
- // Erase-source for between-frame redraw. Prefer the partner-less
- // backdrop the caller stashed via `setPartnerEraseBg` (e.g. the
- // site's `_bgSnapshot`, which has the static drops + frame but no
- // partner sprite). Without that, fall back to whatever's currently
- // on screen â which works for full-screen contexts (PDA / accuse /
- // briefing) where there is no separate idle partner overlay to
- // erase, but produces visible "ghosting" against the site's idle
- // partner cell at (6, 80) because it has the resting pose baked in.
- Graphics::ManagedSurface bg(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- if (_partnerEraseBg.w == 320 && _partnerEraseBg.h == 200) {
- for (int row = 0; row < 200; row++) {
- memcpy((byte *)bg.getBasePtr(0, row),
- (const byte *)_partnerEraseBg.getBasePtr(0, row), 320);
- }
- } else {
- Graphics::Surface *screen = g_system->lockScreen();
- if (!screen)
- return;
- for (int row = 0; row < 200; row++) {
- memcpy((byte *)bg.getBasePtr(0, row),
- (const byte *)screen->getBasePtr(0, row), 320);
- }
- g_system->unlockScreen();
- }
-
- for (uint i = 0; i < frameCount && !shouldQuit(); i++) {
- const uint frameIdx = frames ? (uint)frames[i] : i;
- if (frameIdx >= anim.size())
- continue;
- const Picture &fr = anim[frameIdx];
- const byte transp = (byte)(fr.flags >> 8);
-
- // Restore BG, then masked-blit the next frame.
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- for (int row = 0; row < 200; row++) {
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)bg.getBasePtr(0, row), 320);
- }
- const int w = MIN<int>(fr.surface.w, 320 - px);
- const int h = MIN<int>(fr.surface.h, 200 - py);
- for (int row = 0; row < h; row++) {
- const int dstY = py + row;
- if (dstY < 0) continue;
- const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, dstY);
- for (int col = 0; col < w; col++) {
- const int dstX = px + col;
- if (dstX < 0) continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
-
- // One frame per `_CheckFrameRate` tick. The original calibrates
- // this to ~10 fps; 100 ms matches what the rest of the engine
- // uses for partner / NPC frame cycling.
- const uint32 wakeup = g_system->getMillis() + 100;
- while (g_system->getMillis() < wakeup && !shouldQuit()) {
- Common::Event ev;
- while (g_system->getEventManager()->pollEvent(ev)) {
- // Drain events but don't allow skipping mid-animation â
- // the speaker portrait + balloon haven't been drawn yet
- // and a click would otherwise eat the upcoming clue.
- if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
- return;
- }
- g_system->delayMillis(10);
- }
- }
-
- // Restore BG so the next caller (speaker portrait blit) starts clean.
- g_system->copyRectToScreen(bg.getPixels(), bg.pitch, 0, 0, 320, 200);
- g_system->updateScreen();
-}
-
-bool EEMEngine::getBalloonInsets(uint16 bubNum, uint16 &xInset,
- uint16 &yInset, uint16 &textW) const {
- // 52-entry, 10-bytes-each balloon-metadata table at `29be:0875`.
- // Used at 1df2:0aef-0af9 (accuse hint) and `_DisplayClue` to position
- // `_WordWrap` text inside the balloon. Only +0/+2/+4 are read here:
- // +0..1 = text X inset, +2..3 = Y inset, +4..5 = max wrap width
- // (+6/+8 = balloon h / tail offset, both unused for text layout).
- static const struct { uint16 x, y, w; } kTable[] = {
- { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 },
- { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 },
- { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 },
- { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 },
- { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 },
- { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 },
- { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 },
- { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 },
- { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 },
- { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 },
- { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 },
- { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 },
- { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 },
- { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 },
- { 5, 8, 158 }, { 5, 8, 176 }, { 8, 7, 142 }
- };
- const uint idx = bubNum & 0x7F;
- if (idx >= ARRAYSIZE(kTable))
- return false;
- xInset = kTable[idx].x;
- yInset = kTable[idx].y;
- textW = kTable[idx].w;
- return true;
-}
-
-bool EEMEngine::areYouSure() {
- // Mirrors `_AreYouSure` @ 1a35:0a5c. Original loads PIC 0x136 for the
- // dialog body and PIC 0x1FD/0x1FE for YES/NO. We render a minimal
- // text dialog that preserves the screen behind it.
- if (!_font.isLoaded())
- return true;
-
- Graphics::Surface *screen = g_system->lockScreen();
- Graphics::ManagedSurface saved(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- if (screen) {
- for (int row = 0; row < 200; row++) {
- memcpy((byte *)saved.getBasePtr(0, row),
- (const byte *)screen->getBasePtr(0, row), 320);
- }
- g_system->unlockScreen();
- }
-
- const Common::Rect dlg(60, 70, 260, 140);
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- for (int row = 0; row < 200; row++)
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)saved.getBasePtr(0, row), 320);
- scratch.fillRect(dlg, 0);
- scratch.frameRect(dlg, 0xF);
- _font.drawString(&scratch, "Are you sure you want to quit?", dlg.left + 8, dlg.top + 8, 320, 0xF);
- _font.drawString(&scratch, "Y - Yes", dlg.left + 16, dlg.top + 36, 320, 0xF);
- _font.drawString(&scratch, "N - No", dlg.left + 100, dlg.top + 36, 320, 0xF);
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
-
- bool result = false;
- while (!shouldQuit()) {
- Common::Event ev;
- bool decided = false;
- while (g_system->getEventManager()->pollEvent(ev)) {
- if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
- result = true;
- decided = true;
- break;
- }
- if (ev.type == Common::EVENT_KEYDOWN) {
- if (ev.kbd.keycode == Common::KEYCODE_y ||
- ev.kbd.keycode == Common::KEYCODE_RETURN) {
- result = true; decided = true; break;
- }
- if (ev.kbd.keycode == Common::KEYCODE_n ||
- ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
- result = false; decided = true; break;
- }
- }
- }
- if (decided)
- break;
- g_system->updateScreen();
- g_system->delayMillis(15);
- }
-
- // Restore the screen so the caller's UI is intact.
- g_system->copyRectToScreen(saved.getPixels(), saved.pitch, 0, 0, 320, 200);
- g_system->updateScreen();
- return result;
-}
-
-void EEMEngine::doAccuse() {
- if (!_mystery.isLoaded())
- return;
-
- // Mirrors `_DoAccuseGallery @ 1df2:0a31`:
- // 1. Show KD's hint balloon (KDTextIndex[+8] text).
- // 2. `_GetBackground(0x3f)` â same backdrop as PDA / gallery.
- // 3. `_DrawGallery()` â renders portraits at the standard 5 slots
- // (positions verified at 29be:0x116, bottom-aligned baseline 0x48).
- // 4. Click loop dispatching on `_NoteButtons` (same table as PDA)
- // with a separate `_HandleAccuseNoteButton` jump table.
- const uint8 num = _mystery.numSuspects();
- if (num == 0)
- return;
-
- const byte *gd = _mystery.galleryData();
-
- // Verbatim from 29be:0x116 â same five suspect slot positions as
- // `_DrawGallery @ 158f:0046`.
- struct Slot { int x; int y; };
- static const Slot kGallerySlots[5] = {
- { 83, 14 }, { 155, 14 }, { 227, 14 },
- { 119, 90 }, { 191, 90 }
- };
-
- Picture accuseBg;
- const bool haveAccuseBg = _picsArchive.getPicture(0x3f, accuseBg);
-
- Common::Array<Common::Rect> slotRects;
- Common::Array<int> slotSuspect;
- slotRects.resize(num);
- slotSuspect.resize(num);
- for (uint i = 0; i < num; i++)
- slotSuspect[i] = -1;
-
- int highlighted = 0;
- auto drawGallery = [&]() {
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
- if (haveAccuseBg) {
- const int bw = MIN<int>(accuseBg.surface.w, 320);
- const int bh = MIN<int>(accuseBg.surface.h, 200);
- for (int row = 0; row < bh; row++) {
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)accuseBg.surface.getBasePtr(0, row), bw);
- }
- }
-
- for (uint i = 0; i < num && i < Mystery::kGalleryCap; i++) {
- slotRects[i] = Common::Rect();
- slotSuspect[i] = -1;
- if (!gd) continue;
- const uint8 phys = _mystery._newOrder[i];
- if (phys >= 5) continue;
- // `_DrawGallery @ 158f:00b9` skips suspects whose
- // `_InGallery[phys]` flag is 0 â that's the original gate
- // (some suspects only become visible after being met or
- // stay hidden after a wrong accusation removes them).
- if (_mystery._inGallery[phys] == 0) continue;
- const Slot &s = kGallerySlots[phys];
-
- const uint16 picId = READ_LE_UINT16(gd + i * 0x46);
- if (picId == 0) continue;
- Picture portrait;
- if (!_picsArchive.getPicture(picId, portrait))
- continue;
-
- const int placeX = s.x;
- const int placeY = s.y + (0x48 - portrait.surface.h);
- const byte transp = (byte)(portrait.flags >> 8);
- const int w = MIN<int>(portrait.surface.w, 320 - placeX);
- const int h = MIN<int>(portrait.surface.h, 200 - placeY);
- if (w <= 0 || h <= 0) continue;
- for (int row = 0; row < h; row++) {
- const int dstY = placeY + row;
- if (dstY < 0) continue;
- const byte *src =
- (const byte *)portrait.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, dstY);
- for (int col = 0; col < w; col++) {
- const int dstX = placeX + col;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
- slotRects[i] = Common::Rect(placeX, placeY,
- placeX + w, placeY + h);
- slotSuspect[i] = (int)i;
- }
-
- // Highlight indicator. The original moves the mouse cursor
- // to the centre of the highlighted suspect via `_PutMouseInRect`
- // (1df2:0b8e) â we draw a 1px outline in palette index 0xFE
- // (within the marching-ants cycle range 0xF9..0xFE) which is
- // unambiguously visible under any palette without warping the
- // player's cursor.
- if (highlighted >= 0 && highlighted < (int)slotRects.size() &&
- !slotRects[highlighted].isEmpty()) {
- Common::Rect r = slotRects[highlighted];
- r.grow(1);
- scratch.frameRect(r, 0xFE);
- }
-
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
- };
-
- // Step 1 â KD hint balloon. Mirrors `_DoAccuseGallery @ 1df2:0a31`
- // (1df2:0a4c-1df2:0afe):
- // text = TextBlock + KDTextIndex[+8] (1df2:0a4c-0a57)
- // bub = _GetKDTextBalloon(text[0]) (1df2:0a6d)
- // GetBalloon(bub) (1df2:0a7c)
- // y = (h < 0x4e) ? (0x50 - h) >> 1 : 1 (1df2:0a8b-0aa5)
- // AddPicBackground(pic, 0x21, y) (1df2:0aab)
- // WordWrap(0x21+tbl[bub].x, y+tbl[bub].y, tbl[bub].w, text, color=0)
- // tbl @ 29be:0875, 10-byte entries (1df2:0ad6-0af1)
- const byte *kdIdx = _mystery.kdTextIndex();
- if (kdIdx) {
- const int16 textOff = (int16)READ_LE_UINT16(kdIdx + 8);
- if (textOff != -1) {
- const char *raw = _mystery.textAt((uint16)textOff);
- Common::String hint =
- parseString(raw ? raw : "", _playerName, _partner);
- if (!hint.empty()) {
- // First-char dispatch via getKDTextBalloon (1df2:0105).
- // Note: we pass the *parsed* first char; the original
- // reads it BEFORE `_ParseString`, but the player-name /
- // partner-name substitutions never start with digits, so
- // the dispatch result is the same either way.
- const byte firstChar =
- hint.empty() ? (byte)0 : (byte)hint[0];
- const uint16 bubNum = getKDTextBalloon(firstChar);
- Picture balloon;
- const bool haveBalloon =
- _balloonArchive.size() > (bubNum & 0x7F) &&
- _balloonArchive.loadEntry(bubNum & 0x7F, balloon);
-
- // 1df2:0a8b-1df2:0aa5: y = (h < 0x4e) ? (0x50-h)>>1 : 1
- const int balloonX = 0x21;
- int balloonY = 1;
- if (haveBalloon && balloon.surface.h < 0x4e)
- balloonY = (0x50 - balloon.surface.h) / 2;
-
- Graphics::ManagedSurface ms(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- ms.clear();
- if (haveAccuseBg) {
- const int bw = MIN<int>(accuseBg.surface.w, 320);
- const int bh = MIN<int>(accuseBg.surface.h, 200);
- for (int row = 0; row < bh; row++) {
- memcpy((byte *)ms.getBasePtr(0, row),
- (const byte *)accuseBg.surface.getBasePtr(0, row), bw);
- }
- }
- // Masked balloon blit â `_Rect_Move_Mask` (1000:03fc)
- // skips pixels equal to `pic[0] >> 8`.
- if (haveBalloon) {
- const byte transp = (byte)(balloon.flags >> 8);
- const int bw = MIN<int>(balloon.surface.w, 320 - balloonX);
- const int bh = MIN<int>(balloon.surface.h, 200 - balloonY);
- for (int row = 0; row < bh; row++) {
- const byte *src = (const byte *)balloon.surface.getBasePtr(0, row);
- byte *dst = (byte *)ms.getBasePtr(balloonX, balloonY + row);
- for (int col = 0; col < bw; col++) {
- if (src[col] != transp)
- dst[col] = src[col];
- }
- }
- }
- // Inset table @ 29be:0875 â 1df2:0acb pushes color=0.
- uint16 tx = 5, ty = 4, tw = 155;
- getBalloonInsets(bubNum, tx, ty, tw);
- if (_font.isLoaded()) {
- _font.drawWordWrapped(&ms, balloonX + tx,
- balloonY + ty, tw, hint,
- haveBalloon ? 0 : 0xF);
- }
- g_system->copyRectToScreen(ms.getPixels(), ms.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
- waitForInput(8000);
- }
- }
- }
-
- // Helper to find the next "alive" slot (one whose `_inGallery[phys]`
- // flag is still set so a portrait was actually drawn). Mirrors the
- // way the original wraps DI past empty slots.
- auto nextLiveSlot = [&](int from, int dir) -> int {
- const int n = (int)slotRects.size();
- if (n <= 0) return 0;
- for (int step = 1; step <= n; step++) {
- int idx = (from + dir * step) % n;
- if (idx < 0) idx += n;
- if (!slotRects[idx].isEmpty())
- return idx;
- }
- return from;
- };
- if (slotRects[highlighted].isEmpty())
- highlighted = nextLiveSlot(highlighted, +1);
-
- drawGallery();
-
- // Wait-for-pick loop. Mirrors `_DoAccuseGallery` 1df2:0b26-1df2:0bc8:
- // * `_CheckFrameRate` + `_UpdateAnimations` per tick (1df2:0b2a-0b33)
- // * 5-entry input dispatch table @ 1df2:0bc9:
- // 0x09 (TAB) â handler 0x0b94 (cycle highlight)
- // 0x0d (Enter) â handler 0x0b72 (pick = _SearchSuspects)
- // 0x4b (LEFT) â handler 0x0b94
- // 0x4d (RIGHT) â handler 0x0b94
- // 0xFFFF (mb) â handler 0x0b72
- // * 0x0b94: `INC DI` + wraparound + `_PutMouseInRect(&Guys[DI])`,
- // i.e. advance highlight and warp cursor (1df2:0b94-0bb1).
- // * 0x0b72: `_SearchSuspects` (158f:0584) â mouse-rect hit-test;
- // if non-0xFFFF, pick that suspect.
- // We don't warp the cursor (unfriendly under SDL); instead the
- // highlight is drawn as a 1px outline and Enter picks it.
- int picked = -1;
- uint32 lastTick = g_system->getMillis();
- bool dirty = false;
- while (picked < 0 && !shouldQuit()) {
- Common::Event ev;
- while (g_system->getEventManager()->pollEvent(ev)) {
- if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
- return;
- if (ev.type == Common::EVENT_KEYDOWN) {
- switch (ev.kbd.keycode) {
- case Common::KEYCODE_ESCAPE:
- return;
- case Common::KEYCODE_TAB:
- case Common::KEYCODE_RIGHT:
- highlighted = nextLiveSlot(highlighted, +1);
- dirty = true;
- break;
- case Common::KEYCODE_LEFT:
- // 1df2:0b94 increments DI for LEFT too â but a
- // keyboard-driven UX is friendlier with separate
- // directions, so we mirror Right=+1 / Left=-1.
- highlighted = nextLiveSlot(highlighted, -1);
- dirty = true;
- break;
- case Common::KEYCODE_RETURN:
- case Common::KEYCODE_KP_ENTER:
- if (highlighted >= 0 &&
- highlighted < (int)slotRects.size() &&
- !slotRects[highlighted].isEmpty()) {
- picked = highlighted;
- }
- break;
- default: {
- const int k = (int)ev.kbd.keycode;
- if (k >= Common::KEYCODE_1 && k <= Common::KEYCODE_9) {
- const int idx = k - Common::KEYCODE_1;
- if (idx < num &&
- !slotRects[idx].isEmpty())
- picked = idx;
- }
- break;
- }
- }
- }
- if (ev.type == Common::EVENT_LBUTTONDOWN) {
- for (uint i = 0; i < slotRects.size(); i++) {
- if (slotSuspect[i] < 0) continue;
- if (slotRects[i].contains(ev.mouse.x, ev.mouse.y)) {
- picked = (int)i;
- break;
- }
- }
- }
- }
- // 100 ms tick â the original calls `_UpdateAnimations` per
- // `_CheckFrameRate` (1df2:0b33). The accuse screen has no
- // animations registered, so the tick is just a redraw cadence.
- // We still re-render whenever the highlight moves (`dirty`).
- const uint32 now = g_system->getMillis();
- if (dirty || now - lastTick >= 100) {
- drawGallery();
- lastTick = now;
- dirty = false;
- }
- g_system->updateScreen();
- g_system->delayMillis(10);
- }
- if (picked < 0)
- return;
-
- // Real chain evaluation: sum point values of clues the player marked
- // "selected" in the notebook. Mirrors `_SolvedCheck` @ 1df2:00ec.
- const int points = _mystery.selectedPoints();
- const bool guessedRight = _mystery.solvedCheck();
- debugC(1, kDebugScript, "doAccuse: picked=%d selectedPts=%d -> %s",
- picked, points, guessedRight ? "correct" : "wrong");
-
- // If the player hasn't marked any evidence yet, give them a hint
- // rather than an instant fail. Mirrors the original "We're not ready
- // to solve this mystery yet..." string at 29be:10f0.
- if (points == 0 && _font.isLoaded()) {
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
- _font.drawWordWrapped(&scratch, 16, 80, 288,
- "We're not ready to solve this mystery yet. "
- "Let's keep investigating until we have some "
- "more solid evidence to make our case! "
- "(Press N in the site screen to mark clues.)",
- 0xF);
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
- waitForInput(15000);
- return;
- }
-
- // Pick the ending based on the chain. For a correct accusation the
- // original would call `_DisplayClue(_Mystery + AChain[0])`, play
- // SCRAPBK.ANI and save progress. We load the matching `E<n>.BIN`
- // ending text and render its pages with prev/next navigation.
- const int endingNum = guessedRight ? picked : 0;
- const Common::String fname = Common::String::format("E%d.BIN", endingNum);
- Common::File f;
- if (!f.open(Common::Path(fname))) {
- warning("doAccuse: %s missing", fname.c_str());
- return;
- }
-
- // E<n>.BIN format (verified against `_DisplayEndingPage` @ 1df2:044c):
- // u16 numPages
- // per page (10 bytes header + NUL-string):
- // u16 picNum
- // u16 x1, y1, x2, y2 (story rect)
- // bytes[] NUL-terminated text
- const uint32 fileLen = f.size();
- Common::Array<byte> blob(fileLen);
- if (f.read(blob.data(), fileLen) != fileLen)
- return;
- const byte *e = blob.data();
- const uint16 pages = READ_LE_UINT16(e);
-
- uint pageIdx = 0;
-
- while (!shouldQuit()) {
- // Walk to pageIdx.
- uint pos = 2;
- uint cur = 0;
- while (cur < pageIdx && pos + 10 < fileLen) {
- const char *t = (const char *)(e + pos + 10);
- pos += 10 + strlen(t) + 1;
- cur++;
- }
- if (pos + 10 >= fileLen)
- break;
-
- const uint16 picNum = READ_LE_UINT16(e + pos + 0);
- const uint16 x1 = READ_LE_UINT16(e + pos + 2);
- const uint16 y1 = READ_LE_UINT16(e + pos + 4);
- const uint16 x2 = READ_LE_UINT16(e + pos + 6);
- const uint16 y2 = READ_LE_UINT16(e + pos + 8);
- const char *raw = (const char *)(e + pos + 10);
- const Common::String txt = parseString(raw, _playerName, _partner);
-
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
-
- // Page background.
- if (picNum != 0) {
- Picture bg;
- if (_picsArchive.getPicture(picNum, bg)) {
- const int w = MIN<int>(bg.surface.w, 320);
- const int h = MIN<int>(bg.surface.h, 200);
- for (int row = 0; row < h; row++) {
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)bg.surface.getBasePtr(0, row), w);
- }
- }
- }
-
- if (_font.isLoaded()) {
- Common::String banner = "Not enough evidence";
- if (guessedRight)
- banner = _mystery._firstTry ? "CORRECT - FIRST TRY!" : "CORRECT!";
- _font.drawString(&scratch, banner, 8, 4, 320, 0xF);
- _font.drawString(&scratch, Common::String::format("Evidence: %d/100 Suspect: %d",
- points, picked + 1), 8, 16, 320, 0xF);
- const int wrapW = MAX<int>(16, x2 - x1);
- const int wrapY = MAX<int>(28, (int)y1);
- (void)y2;
- _font.drawWordWrapped(&scratch, x1, wrapY, wrapW, txt, 0xF);
- _font.drawString(&scratch, Common::String::format("page %u/%u (Left/Right or click)",
- pageIdx + 1, pages), 8, 188, 320, 0xF);
- }
-
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
-
- // Page navigation.
- bool advance = false;
- bool back = false;
- bool exit = false;
- while (!advance && !back && !exit && !shouldQuit()) {
- Common::Event ev;
- while (g_system->getEventManager()->pollEvent(ev)) {
- if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
- exit = true; break;
- }
- if (ev.type == Common::EVENT_LBUTTONDOWN) {
- advance = true; break;
- }
- if (ev.type == Common::EVENT_KEYDOWN) {
- if (ev.kbd.keycode == Common::KEYCODE_ESCAPE)
- exit = true;
- else if (ev.kbd.keycode == Common::KEYCODE_LEFT)
- back = true;
- else
- advance = true;
- break;
- }
- }
- g_system->updateScreen();
- g_system->delayMillis(15);
- }
- if (exit) break;
- if (advance) {
- if (pageIdx + 1 >= pages) break;
- pageIdx++;
- } else if (back) {
- if (pageIdx > 0) pageIdx--;
- }
- }
-
- // Mirror `_DisplayCorrect`'s scrap-book animation + solved tracking +
- // auto-save (the original calls `_SavePlayerRecord` after a win).
- if (guessedRight) {
- const uint mn = _mystery.number();
- if (mn < sizeof(_mysteriesSolved)) {
- _mysteriesSolved[mn] = _mystery._firstTry ? 2 : 1;
- }
- playAnm(Common::Path("SCRAPBK.ANI"), 120, true);
-
- // Auto-save into slot 0 (the engine's quicksave slot).
- const Common::String desc = Common::String::format(
- "%s â solved mystery %u", _playerName.c_str(), mn);
- Common::Error err = saveGameState(0, desc, true);
- if (err.getCode() != Common::kNoError)
- warning("auto-save after solve failed: %s",
- err.getDesc().c_str());
- } else {
- _mystery._firstTry = false;
- }
-}
-
-// -------------------- save / load --------------------
-
-namespace {
-const uint32 kSaveMagic = MKTAG('E', 'E', 'M', '0');
-const byte kSaveVer = 3; ///< v2: _mysteriesSolved tracker; v3: player name
-} // anonymous namespace
-
bool EEMEngine::hasFeature(EngineFeature f) const {
// We support saving any time but loading only at startup (via the
// `--save-slot=N` resume path or a slot picked from the launcher).
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 7b5c26a4be7..3777e5ac6ca 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -116,6 +116,13 @@ public:
/// ctype-bit-1 (= digit) at `29be:2be1 + char`.
uint16 getKDTextBalloon(byte firstChar) const;
+ /// Substitute the 0x80..0x89 control bytes the engine uses inside
+ /// `TextBlock` strings. Mirrors `_ParseString @ 1b66:07c3`; jump
+ /// table at 1b66:0cbe. Used by every clue / hint / balloon caller.
+ Common::String parseString(const Common::String &raw,
+ const Common::String &playerName,
+ uint partner) const;
+
/// Play the partner's one-shot reaction animation slot @num. Mirrors
/// `_DoKDAnim @ 168d:028a` + `_PlayAnimation @ 172b:1f46`. The
/// per-partner (animId, x, y) come from `_WaitAnims[1+num] @ 29be:0228`,
diff --git a/engines/eem/graphics.cpp b/engines/eem/graphics.cpp
new file mode 100644
index 00000000000..857dcb80ecc
--- /dev/null
+++ b/engines/eem/graphics.cpp
@@ -0,0 +1,209 @@
+/* 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/debug.h"
+#include "common/events.h"
+#include "common/file.h"
+#include "common/path.h"
+#include "common/savefile.h"
+#include "common/system.h"
+#include "common/textconsole.h"
+
+#include "graphics/cursorman.h"
+#include "graphics/managed_surface.h"
+
+#include "eem/detection.h"
+#include "eem/eem.h"
+
+// EEM â graphics helpers (KDGRAPH.C + KDHELP.C, the latter merged because
+// it has only three functions). Animation playback, balloon-table lookup,
+// and the help-screen primitives that share the same balloon machinery.
+
+namespace EEM {
+
+void EEMEngine::doHelp() {
+ // `_KDHelp` reads two hint TextBlock offsets from `_KDTextIndex`:
+ // word @ +0xe : first-time hint
+ // word @ +0x10: second-time hint (cycles back to first if missing)
+ // `_SawHelpHint` toggles between them.
+ if (!_mystery.isLoaded() || !_font.isLoaded())
+ return;
+
+ const byte *kd = _mystery.kdTextIndex();
+ if (!kd)
+ return;
+
+ const uint16 hintFirst = READ_LE_UINT16(kd + 0x0e);
+ const uint16 hintSecond = READ_LE_UINT16(kd + 0x10);
+ uint16 use = _mystery._sawHelpHint && hintSecond != 0xFFFF ? hintSecond : hintFirst;
+ if (use == 0xFFFF) {
+ debugC(1, kDebugScript, "doHelp: no hint configured");
+ return;
+ }
+ if (!_mystery._sawHelpHint && hintFirst != 0xFFFF)
+ _mystery._sawHelpHint = true;
+
+ const Common::String raw = _mystery.textAt(use);
+ const Common::String text = parseString(raw, _playerName, _partner);
+
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ _font.drawString(&scratch, "HELP", 8, 4, 320, 0xF);
+ _font.drawWordWrapped(&scratch, 8, 24, 304, text, 0xF);
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ waitForInput(60000);
+}
+
+void EEMEngine::doInterfaceHelp(uint num) {
+ // Mirrors `_InterfaceHelp(num)` @ 1560:0205. The original walks
+ // `HelpData @ 29be:00c8` (5-byte entries: u8 count, then up to 2
+ // u16 picIds), `_GetPicture`s each one, blits it fullscreen via
+ // `_Rect_Move_Mask(0, 0, ...)`, and waits for click / key. ESC ends
+ // the cycle; any other input advances to the next pic.
+ //
+ // Verified from Ghidra HelpData bytes:
+ // entry 0 (PDA / gallery HELP button): count=2, picIds = 0x0063, 0x01ae
+ // entry 1: count=2, picIds = 0x0192, 0x01b1
+ // Only entry 0 is reachable from the PDA notebook (rect 1) and the
+ // gallery (rect 1) â both call `_InterfaceHelp(0)`.
+ static const uint16 kHelpPics[][2] = {
+ { 0x0063, 0x01ae },
+ { 0x0192, 0x01b1 },
+ };
+ if (num >= ARRAYSIZE(kHelpPics))
+ return;
+
+ debugC(1, kDebugScript, "doInterfaceHelp(%u): showing pics 0x%x, 0x%x",
+ num, kHelpPics[num][0], kHelpPics[num][1]);
+
+ for (uint i = 0; i < 2; i++) {
+ const uint16 picId = kHelpPics[num][i];
+ Picture pic;
+ if (!_picsArchive.getPicture(picId, pic)) {
+ warning("doInterfaceHelp: getPicture(0x%x) failed", picId);
+ continue;
+ }
+ debugC(1, kDebugScript, "doInterfaceHelp: pic 0x%x = %dx%d flags=0x%x",
+ picId, pic.surface.w, pic.surface.h, pic.flags);
+
+ // Compose a 320x200 frame (cleared) and blit the help pic at (0,0)
+ // with the original's masked-blit semantics: pixels equal to the
+ // pic's sub-mode (high byte of `pic[0]`, see `_Rect_Move_Mask`
+ // param_10 at 1000:03fc) are treated as transparent and skipped.
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ const byte transp = (byte)(pic.flags >> 8);
+ const int w = MIN<int>(pic.surface.w, 320);
+ const int h = MIN<int>(pic.surface.h, 200);
+ for (int row = 0; row < h; row++) {
+ const byte *src = (const byte *)pic.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, row);
+ for (int col = 0; col < w; col++) {
+ if (src[col] != transp)
+ dst[col] = src[col];
+ }
+ }
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+
+ bool escape = false;
+ while (!shouldQuit() && !escape) {
+ Common::Event ev;
+ bool advance = false;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ return;
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ advance = true;
+ break;
+ }
+ if (ev.type == Common::EVENT_KEYDOWN) {
+ if (ev.kbd.keycode == Common::KEYCODE_ESCAPE)
+ escape = true;
+ else
+ advance = true;
+ break;
+ }
+ }
+ if (advance || escape)
+ break;
+ g_system->updateScreen();
+ g_system->delayMillis(15);
+ }
+ if (escape)
+ break;
+ }
+}
+
+void EEMEngine::setPartnerEraseBg(const Graphics::ManagedSurface *bg) {
+ if (bg && bg->w == 320 && bg->h == 200) {
+ _partnerEraseBg.create(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ for (int row = 0; row < 200; row++) {
+ memcpy((byte *)_partnerEraseBg.getBasePtr(0, row),
+ (const byte *)bg->getBasePtr(0, row), 320);
+ }
+ } else {
+ _partnerEraseBg.free();
+ }
+}
+
+bool EEMEngine::getBalloonInsets(uint16 bubNum, uint16 &xInset,
+ uint16 &yInset, uint16 &textW) const {
+ // 52-entry, 10-bytes-each balloon-metadata table at `29be:0875`.
+ // Used at 1df2:0aef-0af9 (accuse hint) and `_DisplayClue` to position
+ // `_WordWrap` text inside the balloon. Only +0/+2/+4 are read here:
+ // +0..1 = text X inset, +2..3 = Y inset, +4..5 = max wrap width
+ // (+6/+8 = balloon h / tail offset, both unused for text layout).
+ static const struct { uint16 x, y, w; } kTable[] = {
+ { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 },
+ { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 },
+ { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 },
+ { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 },
+ { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 },
+ { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 },
+ { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 },
+ { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 },
+ { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 },
+ { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 },
+ { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 },
+ { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 },
+ { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 },
+ { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 },
+ { 5, 8, 158 }, { 5, 8, 176 }, { 8, 7, 142 }
+ };
+ const uint idx = bubNum & 0x7F;
+ if (idx >= ARRAYSIZE(kTable))
+ return false;
+ xInset = kTable[idx].x;
+ yInset = kTable[idx].y;
+ textW = kTable[idx].w;
+ return true;
+}
+
+} // End of namespace EEM
diff --git a/engines/eem/module.mk b/engines/eem/module.mk
index 047409575c9..5583335ffd2 100644
--- a/engines/eem/module.mk
+++ b/engines/eem/module.mk
@@ -2,12 +2,15 @@ MODULE := engines/eem
MODULE_OBJS = \
animation.o \
+ clues.o \
eem.o \
font.o \
+ graphics.o \
metaengine.o \
mystery.o \
resource.o \
- site.o
+ site.o \
+ ui.o
# This module can be built as a plugin
ifeq ($(ENABLE_EEM), DYNAMIC_PLUGIN)
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 8e119ab07f3..1e7f388e6eb 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -919,4 +919,174 @@ void SiteScreen::onHotspotClicked(uint siteNum, uint hotIdx) {
// Caller (`SiteScreen::run`) re-renders the site after this returns.
}
+void EEMEngine::playKdAnim(uint16 num) {
+ // Mirrors `_DoKDAnim(num) @ 168d:028a` + `_PlayAnimation @ 172b:1f46`:
+ // _SuspendAnimation(WaitHandle);
+ // anim = WaitAnims[1+num].anim[partner] (table @ 29be:0228)
+ // x = WaitAnims[1+num].x[partner]
+ // y = WaitAnims[1+num].y[partner]
+ // _PlayAnimation(anim, x, y, WaitHandle)
+ // â registers a state-4 (one-shot) animation slot and lets
+ // `_UpdateAnimations` walk the sequence script until 0x80,
+ // then frees this slot and re-activates `WaitHandle`.
+ // Our port renders the partner's idle inline in each redraw rather
+ // than via a slot system, so we play the one-shot synchronously here
+ // (blocking) and resume normal idle rendering when the caller
+ // returns. That matches the user-visible effect: the partner's
+ // gesture (Jenny taking a picture, etc.) finishes before the
+ // speaker portrait + speech balloon appear.
+ //
+ // Six valid kdAnimNum entries (0..5). Verified bytes from
+ // `29be:0228`. Layout per entry: { animJake, animJenny, xJake,
+ // xJenny, yJake, yJenny }. Position is (6, 80) in every entry.
+ static const uint16 kKdAnimTable[6][6] = {
+ { 0x03, 0x0c, 6, 6, 80, 80 }, // 0 â speaker idx 1 wait anim
+ { 0x01, 0x0b, 6, 6, 80, 80 }, // 1 â same as PDA idle
+ { 0x04, 0x0d, 6, 6, 80, 80 }, // 2
+ { 0x02, 0x10, 6, 6, 80, 80 }, // 3 â same as gallery
+ { 0x05, 0x05, 6, 6, 80, 80 }, // 4 â same anim both partners
+ { 0x06, 0x06, 6, 6, 80, 80 }, // 5 â same anim both partners
+ };
+ if (num >= ARRAYSIZE(kKdAnimTable))
+ return;
+
+ const uint partner = (_partner == 0) ? 0 : 1;
+ const uint16 animId = kKdAnimTable[num][partner];
+ const int px = (int)kKdAnimTable[num][2 + partner];
+ const int py = (int)kKdAnimTable[num][4 + partner];
+
+ Animation anim;
+ if (!_aniArchive.loadAnimation(animId, anim) || anim.empty()) {
+ warning("playKdAnim(%u): anim %u failed to load", num, animId);
+ return;
+ }
+
+ // Sequence-script lookup. Entries copied verbatim from
+ // `_AnimationSequences @ 29be:22d4` walked through to the next 0x80.
+ // Each script is a u16[] of frame indices terminated by 0x80; we
+ // don't yet handle 0x81 jumps (none of the kdAnim sequences use
+ // them â verified). seqnum == animId for these calls (per
+ // `_PlayAnimation` 172b:1f5d push order).
+ struct Script {
+ uint16 seqnum;
+ uint8 len;
+ uint8 frames[20]; // long enough for any kdAnim script
+ };
+ static const Script kScripts[] = {
+ // seqnum 1 (29be:188a) â head bob
+ { 0x01, 15, { 0,1,2,0,1,0,2,1,0,1,0,1,2,1,0 } },
+ // seqnum 2 (29be:18aa) â short blip then long pause
+ { 0x02, 16, { 0,1,2,1,0,0,0,0,0,0,0,0,0,0,0,0 } },
+ // seqnum 3 (29be:18e0) â Jake "lift, hold, lower" gesture
+ { 0x03, 9, { 0,1,2,3,2,2,2,1,0 } },
+ // seqnum 4 (29be:18f4) â bigger gesture (camera flash-style)
+ { 0x04, 13, { 0,1,2,3,4,5,4,4,4,3,2,1,0 } },
+ // seqnum 5 (29be:1910) â held idle with a single peak
+ { 0x05, 13, { 0,0,0,1,2,3,2,1,0,0,0,0,0 } },
+ // seqnum 6 (29be:192c) â empty (immediate END)
+ { 0x06, 0, { 0 } },
+ // seqnum 0xb (29be:188a, same as 1) â Jenny PDA idle
+ { 0x0b, 15, { 0,1,2,0,1,0,2,1,0,1,0,1,2,1,0 } },
+ // seqnum 0xc (29be:18e0, same as 3) â Jenny "take a picture"
+ { 0x0c, 9, { 0,1,2,3,2,2,2,1,0 } },
+ // seqnum 0xd (29be:18f4, same as 4) â Jenny big gesture
+ { 0x0d, 13, { 0,1,2,3,4,5,4,4,4,3,2,1,0 } },
+ // seqnum 0x10 (29be:1956) â Jenny short anim
+ { 0x10, 9, { 0,0,0,1,0,0,0,0,0 } },
+ };
+ const uint8 *frames = nullptr;
+ uint frameCount = 0;
+ for (uint i = 0; i < ARRAYSIZE(kScripts); i++) {
+ if (kScripts[i].seqnum == animId) {
+ frames = kScripts[i].frames;
+ frameCount = kScripts[i].len;
+ break;
+ }
+ }
+ if (frameCount == 0) {
+ // Fallback: linear playback through anim cells (better than
+ // nothing if a future kdAnim references an unscripted anim).
+ frameCount = (uint)anim.size();
+ }
+
+ // Erase-source for between-frame redraw. Prefer the partner-less
+ // backdrop the caller stashed via `setPartnerEraseBg` (e.g. the
+ // site's `_bgSnapshot`, which has the static drops + frame but no
+ // partner sprite). Without that, fall back to whatever's currently
+ // on screen â which works for full-screen contexts (PDA / accuse /
+ // briefing) where there is no separate idle partner overlay to
+ // erase, but produces visible "ghosting" against the site's idle
+ // partner cell at (6, 80) because it has the resting pose baked in.
+ Graphics::ManagedSurface bg(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ if (_partnerEraseBg.w == 320 && _partnerEraseBg.h == 200) {
+ for (int row = 0; row < 200; row++) {
+ memcpy((byte *)bg.getBasePtr(0, row),
+ (const byte *)_partnerEraseBg.getBasePtr(0, row), 320);
+ }
+ } else {
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (!screen)
+ return;
+ for (int row = 0; row < 200; row++) {
+ memcpy((byte *)bg.getBasePtr(0, row),
+ (const byte *)screen->getBasePtr(0, row), 320);
+ }
+ g_system->unlockScreen();
+ }
+
+ for (uint i = 0; i < frameCount && !shouldQuit(); i++) {
+ const uint frameIdx = frames ? (uint)frames[i] : i;
+ if (frameIdx >= anim.size())
+ continue;
+ const Picture &fr = anim[frameIdx];
+ const byte transp = (byte)(fr.flags >> 8);
+
+ // Restore BG, then masked-blit the next frame.
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ for (int row = 0; row < 200; row++) {
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)bg.getBasePtr(0, row), 320);
+ }
+ const int w = MIN<int>(fr.surface.w, 320 - px);
+ const int h = MIN<int>(fr.surface.h, 200 - py);
+ for (int row = 0; row < h; row++) {
+ const int dstY = py + row;
+ if (dstY < 0) continue;
+ const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, dstY);
+ for (int col = 0; col < w; col++) {
+ const int dstX = px + col;
+ if (dstX < 0) continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+
+ // One frame per `_CheckFrameRate` tick. The original calibrates
+ // this to ~10 fps; 100 ms matches what the rest of the engine
+ // uses for partner / NPC frame cycling.
+ const uint32 wakeup = g_system->getMillis() + 100;
+ while (g_system->getMillis() < wakeup && !shouldQuit()) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ // Drain events but don't allow skipping mid-animation â
+ // the speaker portrait + balloon haven't been drawn yet
+ // and a click would otherwise eat the upcoming clue.
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return;
+ }
+ g_system->delayMillis(10);
+ }
+ }
+
+ // Restore BG so the next caller (speaker portrait blit) starts clean.
+ g_system->copyRectToScreen(bg.getPixels(), bg.pitch, 0, 0, 320, 200);
+ g_system->updateScreen();
+}
} // End of namespace EEM
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
new file mode 100644
index 00000000000..03bd77f609d
--- /dev/null
+++ b/engines/eem/ui.cpp
@@ -0,0 +1,2222 @@
+/* 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/debug.h"
+#include "common/events.h"
+#include "common/file.h"
+#include "common/path.h"
+#include "common/savefile.h"
+#include "common/system.h"
+#include "common/textconsole.h"
+
+#include "graphics/cursorman.h"
+#include "graphics/managed_surface.h"
+
+#include "eem/detection.h"
+#include "eem/eem.h"
+
+// EEM â UI screens (NOTE.C, GALLERY.C, ACCUSE.C, MAP.C, CHOOSE.C combined).
+// Each function is a self-contained modal `EEMEngine::doX()` reachable from
+// the site loop. They share the same wait-for-input idiom and PIC 0x3f /
+// 0x41 / 0x42 / 0x43 backdrops.
+
+namespace EEM {
+
+void EEMEngine::doNewPlayer() {
+ // Mirrors `_NewPlayer` @ 1c33:0dda. The original draws background
+ // 0x104 + character peek pic 0x107, then shows "Please type your
+ // name" and accepts up to 12 characters until Enter. We render a
+ // minimal version: black screen + prompt.
+ if (!_font.isLoaded()) {
+ _playerName = "Detective";
+ return;
+ }
+
+ Common::String name;
+ const int maxChars = 12;
+
+ // Mirror the original: load PIC 0x104 as the name-entry backdrop.
+ // The original also slides in PIC 0x107 (a peeking character).
+ Picture bg;
+ const bool haveBG = _picsArchive.getPicture(0x104, bg);
+
+ auto draw = [&]() {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ if (haveBG) {
+ const int w = MIN<int>(bg.surface.w, 320);
+ const int h = MIN<int>(bg.surface.h, 200);
+ for (int row = 0; row < h; row++)
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)bg.surface.getBasePtr(0, row), w);
+ }
+ // Match the original `_NewPlayer`: `_Show_String(rw=0x28, cl=0x50)`
+ // for the prompt, then `_ShowChar(0x50, x, â¦)` for typed input.
+ // (rw=row=y, cl=col=x.) Prompt at (y=40, x=80), input at (y=80, x=80).
+ _font.drawString(&scratch, "Please type your name:", 80, 40, 240, 0xF);
+ Common::String shown = name + "_";
+ _font.drawString(&scratch, shown, 80, 80, 240, 0xF);
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ };
+ draw();
+
+ while (!shouldQuit()) {
+ Common::Event ev;
+ bool dirty = false;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return;
+ if (ev.type != Common::EVENT_KEYDOWN)
+ continue;
+ const Common::KeyCode k = ev.kbd.keycode;
+ if (k == Common::KEYCODE_RETURN) {
+ if (name.empty())
+ name = "Detective";
+ _playerName = name;
+ return;
+ }
+ if (k == Common::KEYCODE_ESCAPE) {
+ _playerName = "Detective";
+ return;
+ }
+ if (k == Common::KEYCODE_BACKSPACE) {
+ if (!name.empty()) {
+ name.deleteLastChar();
+ dirty = true;
+ }
+ continue;
+ }
+ if (ev.kbd.ascii >= ' ' && ev.kbd.ascii < 127 &&
+ (int)name.size() < maxChars) {
+ name += (char)ev.kbd.ascii;
+ dirty = true;
+ }
+ }
+ if (dirty)
+ draw();
+ g_system->updateScreen();
+ g_system->delayMillis(15);
+ }
+}
+
+void EEMEngine::doCaseSelection() {
+ // Mirrors `_CaseSelection` @ 1c33:0a87. The original draws PIC 0x41
+ // (chooser background) plus a centred "Book %d" / "Challenge Book"
+ // header at (y=12) and then calls `_DoChoose(list)` to render the
+ // menu via `DrawList` @ 1c33:040d at (_TextBox+3, DAT_29be_0d02) =
+ // (61, 35), 12 rows à 10 px line height. The menu list itself is
+ // the static array at 29be:0d6a (verified via `push 0x0d6a` at
+ // 1c33:1ab4). Strings are at 29be:0ef4 onwards. Layout:
+ // list[0] = "----------------------------------"
+ // list[1] = " Choose A Mystery"
+ // list[2..10] = alternating menu items + separators
+ // Five selectable items: Choose A Mystery / Practice Mystery /
+ // See ScrapBook 1/2/3.
+ const uint kMaxMystery = 54;
+
+ enum MenuPick {
+ kPickChoose = 0,
+ kPickPractice,
+ kPickScrap1,
+ kPickScrap2,
+ kPickScrap3,
+ kNumPicks
+ };
+ const char *kPickLabel[kNumPicks] = {
+ " Choose A Mystery",
+ " Practice Mystery",
+ " See ScrapBook 1",
+ " See ScrapBook 2",
+ " See ScrapBook 3"
+ };
+ // ScrapBooks aren't implemented yet â grey them so the player can't
+ // stop on them, mirroring the original `_Greys` mask.
+ const bool kPickEnabled[kNumPicks] = { true, true, false, false, false };
+ uint pick = kPickChoose;
+
+ const char *kSeparator = "----------------------------------";
+
+ // Click rectangles from the original `_DoChoose` @ 1c33:0514 â each
+ // `_InRect(_MouseX, _MouseY, addr, 0x29be)` reads one 4Ãu16 rect at
+ // the listed offset in segment 29be ({x1, y1, x2, y2}). We use
+ // `Common::Rect` (left/top/right/bottom) which also gives us
+ // `contains(x, y)` for hit testing.
+ const Common::Rect kOkRect ( 12, 63, 41, 87); // 29be:0cd8 confirm
+ const Common::Rect kHelpRect ( 12, 100, 41, 124); // 29be:0ce0 help
+ const Common::Rect kExitRect ( 12, 137, 41, 161); // 29be:0ce8 cancel
+ const Common::Rect kUpArrowRect (240, 31, 250, 43); // 29be:0cf0 scroll up
+ const Common::Rect kDnArrowRect (240, 148, 250, 159); // 29be:0cf8 scroll dn
+ const Common::Rect kListRect ( 58, 35, 238, 158); // 29be:0d00 list panel
+
+ // The original `_NewPlayer` set `_MouseCursor = 1` on exit; the
+ // chain of screens after it expects the cursor to stay visible.
+ // Reassert here in case anything between hid it.
+ CursorMan.showMouse(true);
+
+ // Mirrors `_CaseSelection`: load PIC 0x41 as the chooser backdrop.
+ Picture caseBg;
+ const bool haveCaseBg = _picsArchive.getPicture(0x41, caseBg);
+
+ // KD greeter sprite. `_CaseSelection @ 1c33:0a87` (1c33:0b7e-0ba1)
+ // loads anim 0x15 (Jake-paired) or 0x16 (Jenny-paired) and registers
+ // `_NewAnimation(0x112, 0x50, ..., seqnum=0x15, prior=1)` â partner-
+ // dependent because the host KD changes who's "with him" on the
+ // briefing intro frame. Runs continuously through the menu loop via
+ // `_UpdateAnimations`. We approximate with millis-based frame cycling.
+ const uint kKdAniId = (_partner == 0) ? 0x15 : 0x16;
+ Animation kdAnim;
+ const bool haveKdAnim = _aniArchive.loadAnimation(kKdAniId, kdAnim)
+ && !kdAnim.empty();
+ const int kKdAnimX = 0x112;
+ const int kKdAnimY = 0x50;
+
+ auto draw = [&]() {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ if (haveCaseBg) {
+ const int w = MIN<int>(caseBg.surface.w, 320);
+ const int h = MIN<int>(caseBg.surface.h, 200);
+ for (int row = 0; row < h; row++) {
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)caseBg.surface.getBasePtr(0, row), w);
+ }
+ }
+
+ // KD greeter frame â masked-blit current animation cell at
+ // (0x112, 0x50). 100 ms tick matches the engine's `_CheckFrameRate`.
+ if (haveKdAnim) {
+ const uint32 now = g_system->getMillis();
+ const uint frameIdx = (uint)((now / 100) % kdAnim.size());
+ const Picture &fr = kdAnim[frameIdx];
+ const byte transp = (byte)(fr.flags >> 8);
+ for (int row = 0; row < fr.surface.h; row++) {
+ const int dstY = kKdAnimY + row;
+ if (dstY < 0 || dstY >= 200) continue;
+ const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, dstY);
+ for (int col = 0; col < fr.surface.w; col++) {
+ const int dstX = kKdAnimX + col;
+ if (dstX < 0 || dstX >= 320) continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ }
+ if (_font.isLoaded()) {
+ // `DrawList` @ 1c33:040d coordinates: `_TextBox + 3` for x
+ // and `DAT_29be_0d02` for y. `_TextBox` @ 29be:0d00 holds
+ // {x=58, y=35, x2=238, y2=158}. Matches the blue panel.
+ const int kListX = 58 + 3;
+ const int kListW = 238 - kListX;
+ const int kListY0 = 35;
+ const int kLineH = 10;
+
+ // Top centred "Book %d" / "Challenge Book" title â sprintf
+ // format strings at 29be:0deb / 29be:0dfa shown via
+ // `_Show_String(0xc, (0xba - width)/2 + 0x3c, â¦)` in the
+ // original. We don't track challenge tier yet so always
+ // show "Book 1".
+ const Common::String book = "Book 1";
+ const int titleW = _font.getStringWidth(book);
+ const int titleX = (0xba - titleW) / 2 + 0x3c;
+ _font.drawString(&scratch, book, titleX, 12, 320, 0xF);
+
+ // Render 11 list rows: separator + menu item pairs.
+ // row 0 separator
+ // row 1 Choose A Mystery
+ // row 2 separator
+ // row 3 Practice Mystery
+ // ...
+ // row 9 See ScrapBook 3
+ // row 10 separator
+ for (int r = 0; r < 11; r++) {
+ const int y = kListY0 + r * kLineH;
+ if ((r & 1) == 0) {
+ _font.drawString(&scratch, kSeparator, kListX, y, kListW, 0x7);
+ continue;
+ }
+ const uint mp = (uint)(r >> 1);
+ const bool isSel = (mp == pick);
+ const byte color = isSel ? 0xF :
+ kPickEnabled[mp] ? 0x7 : 0x8;
+ _font.drawString(&scratch, kPickLabel[mp], kListX, y, kListW, color);
+ }
+ }
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ };
+
+ auto pickPrev = [&]() {
+ for (int i = 0; i < (int)kNumPicks; i++) {
+ pick = (pick == 0) ? (uint)(kNumPicks - 1) : pick - 1;
+ if (kPickEnabled[pick])
+ break;
+ }
+ };
+ auto pickNext = [&]() {
+ for (int i = 0; i < (int)kNumPicks; i++) {
+ pick = (pick + 1) % kNumPicks;
+ if (kPickEnabled[pick])
+ break;
+ }
+ };
+
+ draw();
+ uint32 lastTick = g_system->getMillis();
+
+ bool exitChosen = false;
+ while (!shouldQuit()) {
+ Common::Event ev;
+ bool confirmed = false;
+ // Redraw every 100 ms so the KD greeter cycles. Mirrors the
+ // `_CheckFrameRate` cadence in `_CaseSelection`'s main loop.
+ const uint32 now = g_system->getMillis();
+ if (haveKdAnim && now - lastTick >= 100) {
+ lastTick = now;
+ draw();
+ }
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return;
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ // OK / EXIT / HELP buttons (rectangles from `_DoChoose`).
+ if (kOkRect.contains(ev.mouse.x, ev.mouse.y)) {
+ confirmed = true;
+ break;
+ }
+ if (kExitRect.contains(ev.mouse.x, ev.mouse.y)) {
+ exitChosen = true;
+ confirmed = true;
+ break;
+ }
+ if (kHelpRect.contains(ev.mouse.x, ev.mouse.y)) {
+ // HELP placeholder â original calls `_DisplayHint`;
+ // our help screen is wired to `H` later in the flow.
+ continue;
+ }
+ // List panel: click on a non-separator row selects the
+ // menu entry under the cursor.
+ if (kListRect.contains(ev.mouse.x, ev.mouse.y)) {
+ const int kLineH = 10;
+ const int row = (ev.mouse.y - kListRect.top) / kLineH;
+ if ((row & 1) == 1) {
+ const uint mp = (uint)(row >> 1);
+ if (mp < kNumPicks && kPickEnabled[mp]) {
+ pick = mp;
+ draw();
+ continue;
+ }
+ }
+ }
+ }
+ if (ev.type != Common::EVENT_KEYDOWN)
+ continue;
+ const Common::KeyCode k = ev.kbd.keycode;
+ if (k == Common::KEYCODE_ESCAPE) {
+ exitChosen = true;
+ confirmed = true;
+ break;
+ }
+ if (k == Common::KEYCODE_RETURN) {
+ confirmed = true;
+ break;
+ }
+ if (k == Common::KEYCODE_UP || k == Common::KEYCODE_LEFT) {
+ pickPrev();
+ draw();
+ continue;
+ }
+ if (k == Common::KEYCODE_DOWN || k == Common::KEYCODE_RIGHT ||
+ k == Common::KEYCODE_TAB) {
+ pickNext();
+ draw();
+ continue;
+ }
+ }
+ if (confirmed) {
+ draw();
+ break;
+ }
+ g_system->updateScreen();
+ g_system->delayMillis(15);
+ }
+
+ if (shouldQuit())
+ return;
+
+ if (exitChosen) {
+ _mystery.clear();
+ _nextScreen = kScreenInvalid;
+ return;
+ }
+
+ // "Practice Mystery" is the tutorial â mystery 0.
+ if (pick == kPickPractice) {
+ if (!_mystery.load(0, &_rng)) {
+ warning("doCaseSelection: failed to load practice mystery");
+ _mystery.clear();
+ }
+ return;
+ }
+
+ if (pick != kPickChoose) {
+ // ScrapBooks aren't implemented; bail back to the menu loop.
+ _mystery.clear();
+ return;
+ }
+
+ // "Choose A Mystery" sub-screen: pick a specific case from the
+ // 55-mystery roster. The original opens a different list here;
+ // we approximate with the tier-aware numeric chooser we used
+ // before. Default to the first unsolved mystery.
+ uint sel = 0;
+ for (uint i = 0; i <= kMaxMystery; i++) {
+ if (i < sizeof(_mysteriesSolved) && !_mysteriesSolved[i]) {
+ sel = i;
+ break;
+ }
+ }
+
+ auto drawSubmenu = [&]() {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ if (haveCaseBg) {
+ const int w = MIN<int>(caseBg.surface.w, 320);
+ const int h = MIN<int>(caseBg.surface.h, 200);
+ for (int row = 0; row < h; row++)
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)caseBg.surface.getBasePtr(0, row), w);
+ }
+ if (_font.isLoaded()) {
+ const int kListX = 61;
+ const int kListW = 238 - kListX;
+ const int kListY0 = 35;
+ const int kLineH = 10;
+ const int kVisible = 12;
+ int top = (int)sel - kVisible / 2;
+ if (top < 0) top = 0;
+ if (top + kVisible > (int)kMaxMystery + 1)
+ top = (int)kMaxMystery + 1 - kVisible;
+ for (int r = 0; r < kVisible; r++) {
+ const int idx = top + r;
+ if (idx > (int)kMaxMystery)
+ break;
+ char marker = ' ';
+ if ((uint)idx < sizeof(_mysteriesSolved)) {
+ if (_mysteriesSolved[idx] == 2) marker = '*';
+ else if (_mysteriesSolved[idx] == 1) marker = '+';
+ }
+ const char arrow = ((uint)idx == sel) ? '>' : ' ';
+ _font.drawString(&scratch,
+ Common::String::format("%c %c Mystery %d", arrow, marker, idx),
+ kListX, kListY0 + r * kLineH, kListW, 0xF);
+ }
+ }
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ };
+
+ drawSubmenu();
+ bool confirmed = false;
+ while (!confirmed && !shouldQuit()) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return;
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ // Same `_DoChoose` rectangles as the top-level menu.
+ if (kOkRect.contains(ev.mouse.x, ev.mouse.y)) {
+ confirmed = true;
+ break;
+ }
+ if (kExitRect.contains(ev.mouse.x, ev.mouse.y)) {
+ _mystery.clear();
+ return;
+ }
+ if (kUpArrowRect.contains(ev.mouse.x, ev.mouse.y)) {
+ sel = (sel == 0) ? kMaxMystery : sel - 1;
+ drawSubmenu();
+ continue;
+ }
+ if (kDnArrowRect.contains(ev.mouse.x, ev.mouse.y)) {
+ sel = (sel >= kMaxMystery) ? 0 : sel + 1;
+ drawSubmenu();
+ continue;
+ }
+ if (kListRect.contains(ev.mouse.x, ev.mouse.y)) {
+ // Pick the row under the cursor.
+ const int kLineH = 10;
+ const int kVisible = 12;
+ int top = (int)sel - kVisible / 2;
+ if (top < 0) top = 0;
+ if (top + kVisible > (int)kMaxMystery + 1)
+ top = (int)kMaxMystery + 1 - kVisible;
+ const int row = (ev.mouse.y - kListRect.top) / kLineH;
+ const int idx = top + row;
+ if (idx >= 0 && idx <= (int)kMaxMystery) {
+ sel = (uint)idx;
+ drawSubmenu();
+ }
+ continue;
+ }
+ }
+ if (ev.type != Common::EVENT_KEYDOWN)
+ continue;
+ const Common::KeyCode k = ev.kbd.keycode;
+ if (k == Common::KEYCODE_ESCAPE) {
+ _mystery.clear();
+ return;
+ }
+ if (k == Common::KEYCODE_RETURN) {
+ confirmed = true;
+ break;
+ }
+ if (k >= Common::KEYCODE_0 && k <= Common::KEYCODE_9) {
+ sel = (uint)(k - Common::KEYCODE_0);
+ drawSubmenu();
+ continue;
+ }
+ if (k == Common::KEYCODE_DOWN || k == Common::KEYCODE_TAB) {
+ sel = (sel >= kMaxMystery) ? 0 : sel + 1;
+ drawSubmenu();
+ continue;
+ }
+ if (k == Common::KEYCODE_UP) {
+ sel = (sel == 0) ? kMaxMystery : sel - 1;
+ drawSubmenu();
+ continue;
+ }
+ if (k == Common::KEYCODE_PAGEDOWN) {
+ sel = (sel + 10 > kMaxMystery) ? kMaxMystery : sel + 10;
+ drawSubmenu();
+ continue;
+ }
+ if (k == Common::KEYCODE_PAGEUP) {
+ sel = (sel < 10) ? 0 : sel - 10;
+ drawSubmenu();
+ continue;
+ }
+ if (k == Common::KEYCODE_HOME) { sel = 0; drawSubmenu(); continue; }
+ if (k == Common::KEYCODE_END) { sel = kMaxMystery; drawSubmenu(); continue; }
+ }
+ g_system->updateScreen();
+ g_system->delayMillis(15);
+ }
+
+ if (!_mystery.load(sel, &_rng)) {
+ warning("doCaseSelection: failed to load mystery %u", sel);
+ _mystery.clear();
+ return;
+ }
+ debugC(1, kDebugMystery, "Mystery %u loaded; %u sites, %u suspects",
+ sel, _mystery.numSites(), _mystery.numSuspects());
+}
+
+void EEMEngine::doNotebook() {
+ // Mirrors `_DoNotebook @ 161e:0500` + `_DrawNotes @ 161e:01d0` +
+ // `_HandleNoteButton @ 161e:03cb`.
+ //
+ // Layout (verified from Ghidra labels in 29be:013f / 29be:0147):
+ // _NotebookRect = (78, 12, 288, 152) â note display rectangle.
+ // _NoteButtons (11 entries, 8 bytes each, at 29be:0147):
+ // [0] (134, 174, 155, 190) decorative â `_HandleNoteButton(0)`
+ // returns immediately (i-1 unsigned > 9).
+ // [1] (93, 174, 115, 190) â `_InterfaceHelp(0)` (handler 0x3f9)
+ // [2] (157, 174, 178, 190) â handler 0x477 (page nav)
+ // [3] (5, 80, 44, 110) â `_KDHelp` (host hint, 0x403)
+ // [4] (180, 174, 201, 190) â solve / accuse (0x436)
+ // [5] (204, 174, 224, 190) â `_NextScreen = 5` (gallery, 0x489)
+ // [6] (226, 174, 247, 190) â handler 0x4ab
+ // [7] (7, 177, 57, 200) â handler 0x480 (back to map)
+ // [8] (35, 111, 56, 136) â `_NextScreen = 3` (site)
+ // [9] (0, 0, 0, 0) â same exit as [8]
+ // [10] (66, 79, 267, 174) â `_InterfaceHelp(0)` (note area)
+ // Background: PIC 0x3f.
+ // Partner anim: anim 1 (Jake) / 0xb (Jenny) at (5, 80).
+ if (!_mystery.isLoaded() || !_font.isLoaded())
+ return;
+
+ // Button rects from `_NoteButtons @ 29be:0147` matched to handler
+ // addresses via the jump table at `_HandleNoteButton + 0xec` (i.e.
+ // 161e:04ec). Decoded handlers (i = rect_index, dispatch = handler[i-1]):
+ // rect 0 (134,155) â no handler (i-1 underflows; original treats
+ // this as a decorative/no-op slot)
+ // rect 1 (93,115) â 0x03f9 = `_InterfaceHelp(0)` (HELP)
+ // rect 2 (157,178) â 0x0477 = `_NextScreen = 5` (GALLERY)
+ // rect 3 (5,80) â 0x0403 = `_KDHelp` (host hint)
+ // rect 4 (180,201) â 0x0436 = `_SolvedCheck` -> NextScreen=7 (SOLVE)
+ // rect 5 (204,224) â 0x0489 = `_EraseNotes` + `_DrawNotes` (PAGE NEXT)
+ // rect 6 (226,247) â 0x04ab = decrement CurrentPage + redraw (PAGE PREV)
+ // rect 7 (7,177) â 0x0480 = `_NextScreen = 2` (MAP)
+ // rect 8 (35,111) â 0x03ed = `_NextScreen = 3` (SITE)
+ // rect 9 (0,0) â 0x03ed = same as rect 8
+ // rect 10 (66,79) â 0x03f9 = `_InterfaceHelp(0)` (note-area help)
+ const Common::Rect kNotebookRect(78, 12, 288, 152);
+ const Common::Rect kBtnHelp1 ( 93, 174, 115, 190); // [1] HELP
+ const Common::Rect kBtnGallery (157, 174, 178, 190); // [2] GALLERY
+ const Common::Rect kBtnPartner ( 5, 80, 44, 110); // [3] KD HELP
+ const Common::Rect kBtnAccuse (180, 174, 201, 190); // [4] SOLVE
+ const Common::Rect kBtnPageNext(204, 174, 224, 190); // [5] PAGE NEXT
+ const Common::Rect kBtnPagePrev(226, 174, 247, 190); // [6] PAGE PREV
+ const Common::Rect kBtnMap ( 7, 177, 57, 200); // [7] MAP
+ const Common::Rect kBtnSite ( 35, 111, 56, 136); // [8] SITE
+ const Common::Rect kNoteArea ( 66, 79, 267, 174); // [10] note area
+
+ CursorMan.showMouse(true);
+
+ int page = 0;
+ int hoveredNoteSlot = -1;
+
+ // Build a list of found-clue indices, identical ordering to the
+ // original's iteration through `_CluesFound[]`.
+ auto buildFound = [&]() {
+ Common::Array<uint> found;
+ for (uint i = 0; i < Mystery::kCluesFoundCap; i++)
+ if (_mystery._cluesFound[i])
+ found.push_back(i);
+ return found;
+ };
+
+ auto draw = [&]() {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+
+ // PIC 0x3f frame.
+ Picture frame;
+ if (_picsArchive.getPicture(0x3f, frame)) {
+ const int w = MIN<int>(frame.surface.w, 320);
+ const int h = MIN<int>(frame.surface.h, 200);
+ for (int row = 0; row < h; row++)
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)frame.surface.getBasePtr(0, row), w);
+ }
+
+ // Partner sprite at (5, 80). Anim 1 for Jake, 0xb (11) for Jenny.
+ const uint partnerAnim = (_partner == 0) ? 1 : 0xb;
+ Animation partnerAni;
+ if (_aniArchive.loadAnimation(partnerAnim, partnerAni) && !partnerAni.empty()) {
+ const uint32 now = g_system->getMillis();
+ const uint frameIdx = (uint)((now / 100) % partnerAni.size());
+ const Picture &fr = partnerAni[frameIdx];
+ const byte transp = (byte)(fr.flags >> 8);
+ for (int row = 0; row < fr.surface.h; row++) {
+ const int dstY = 80 + row;
+ if (dstY < 0 || dstY >= 200) continue;
+ const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, dstY);
+ for (int col = 0; col < fr.surface.w; col++) {
+ const int dstX = 5 + col;
+ if (dstX < 0 || dstX >= 320) continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ }
+
+ // Notes â `_DrawNotes` walks `_NoteIndex` for the current page,
+ // rendering each found clue's text inside `_NotebookRect` with
+ // word-wrap. Selected clues are highlighted (color 0x3c in the
+ // original's case-briefing palette).
+ const Common::Array<uint> found = buildFound();
+ const byte *ni = _mystery.noteIndex();
+ const uint16 niCount = _mystery.noteIndexCount();
+
+ const int kRectX = kNotebookRect.left;
+ const int kRectY = kNotebookRect.top;
+ const int kRectW = kNotebookRect.width();
+ const int kRectH = kNotebookRect.height();
+
+ // Walk forward to the start clue of the current page.
+ // Each page renders as many clues as fit in `kRectH`.
+ int clueCursor = 0;
+ Common::Array<int> pageStarts;
+ pageStarts.push_back(0);
+ {
+ const int lineH = _font.getFontHeight() + 1;
+ int y = kRectY;
+ while (clueCursor < (int)found.size()) {
+ const uint clueId = found[clueCursor];
+ Common::String txt;
+ if (ni && clueId < niCount) {
+ const uint16 textOff = READ_LE_UINT16(ni + clueId * 4);
+ txt = parseString(_mystery.textAt(textOff),
+ _playerName, _partner);
+ }
+ // Measure height by wrapping the text without drawing.
+ Common::Array<Common::String> wrapped;
+ _font.wordWrapText(txt, kRectW, wrapped);
+ const int h = (int)wrapped.size() * lineH;
+ if (y + h + 7 > kRectY + kRectH) {
+ // Page break before this clue.
+ y = kRectY;
+ pageStarts.push_back(clueCursor);
+ }
+ y += h + 7;
+ clueCursor++;
+ }
+ if (page >= (int)pageStarts.size())
+ page = (int)pageStarts.size() - 1;
+ if (page < 0)
+ page = 0;
+ }
+
+ // Track per-slot rectangles so the click handler can map a
+ // click in `kNoteArea` back to a clue index.
+ Common::Array<Common::Rect> slotRects;
+ Common::Array<uint> slotClues;
+
+ const int startClue = (page < (int)pageStarts.size())
+ ? pageStarts[page] : 0;
+ const int endClue = (page + 1 < (int)pageStarts.size())
+ ? pageStarts[page + 1] : (int)found.size();
+
+ int y = kRectY;
+ for (int i = startClue; i < endClue; i++) {
+ const uint clueId = found[i];
+ Common::String txt;
+ if (ni && clueId < niCount) {
+ const uint16 textOff = READ_LE_UINT16(ni + clueId * 4);
+ txt = parseString(_mystery.textAt(textOff),
+ _playerName, _partner);
+ }
+ if (txt.empty())
+ txt = Common::String::format("clue %u", clueId);
+ // Per `_DrawNotes @ 161e:01d0`: text uses
+ // `_NoteUnselectedColor` (0x5c=cyan) for unselected and 0x3c
+ // (light yellow-white) for selected. Both contrast cleanly
+ // with the PDA screen's natural blue, so we draw text
+ // directly on PIC 0x3f without an extra fill rectangle â
+ // matches the original design.
+ Common::Array<Common::String> wrapped;
+ _font.wordWrapText(txt, kRectW, wrapped);
+ const int lineH = _font.getFontHeight() + 1;
+ const int h = (int)wrapped.size() * lineH;
+ const byte color = _mystery._noteSelected[clueId] ? 0x3C : 0x5C;
+ for (uint li = 0; li < wrapped.size(); li++) {
+ _font.drawString(&scratch, wrapped[li], kRectX,
+ y + (int)li * lineH, kRectW, color);
+ }
+ slotRects.push_back(Common::Rect(kRectX, y,
+ kRectX + kRectW, y + h));
+ slotClues.push_back(clueId);
+ y += h + 7;
+ }
+
+ // Page indicator + selected-points counter directly on PIC.
+ _font.drawString(&scratch, Common::String::format("p%d/%d",
+ page + 1, (int)pageStarts.size()),
+ 270, 4, 320, 0x5C);
+ _font.drawString(&scratch, Common::String::format("%d pts",
+ _mystery.selectedPoints()),
+ 270, 14, 320, 0x5C);
+ (void)hoveredNoteSlot;
+
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+
+ // Stash slot info on the captures so the click handler below
+ // can use it via the closure.
+ _notebookSlotRects = slotRects;
+ _notebookSlotClues = slotClues;
+ };
+
+ draw();
+
+ uint32 lastDraw = g_system->getMillis();
+
+ while (!shouldQuit()) {
+ Common::Event ev;
+ bool dirty = false;
+ bool exitFlag = false;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ exitFlag = true;
+ break;
+ }
+ if (ev.type == Common::EVENT_KEYDOWN) {
+ if (ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ exitFlag = true;
+ break;
+ }
+ if (ev.kbd.keycode == Common::KEYCODE_LEFT ||
+ ev.kbd.keycode == Common::KEYCODE_PAGEUP) {
+ if (page > 0) page--;
+ dirty = true;
+ } else if (ev.kbd.keycode == Common::KEYCODE_RIGHT ||
+ ev.kbd.keycode == Common::KEYCODE_PAGEDOWN ||
+ ev.kbd.keycode == Common::KEYCODE_TAB) {
+ page++;
+ dirty = true;
+ } else if (ev.kbd.keycode == Common::KEYCODE_h) {
+ doHelp();
+ dirty = true;
+ }
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ // Test buttons in the order the original would â
+ // button 0 / 9 are dead zones, so check the actionable
+ // rects directly. Earlier rects "win" when overlapping
+ // (matches `_FindButton`).
+ if (kBtnSite.contains(ev.mouse.x, ev.mouse.y)) {
+ exitFlag = true;
+ break; // back to site
+ }
+ if (kBtnMap.contains(ev.mouse.x, ev.mouse.y)) {
+ doBigMap();
+ exitFlag = true;
+ break;
+ }
+ if (kBtnPartner.contains(ev.mouse.x, ev.mouse.y)) {
+ doHelp(); // _KDHelp = host hint
+ dirty = true;
+ continue;
+ }
+ if (kBtnAccuse.contains(ev.mouse.x, ev.mouse.y)) {
+ doAccuse();
+ exitFlag = true;
+ break;
+ }
+ if (kBtnGallery.contains(ev.mouse.x, ev.mouse.y)) {
+ doGallery();
+ dirty = true;
+ continue;
+ }
+ if (kBtnHelp1.contains(ev.mouse.x, ev.mouse.y)) {
+ // rect 1 â `_InterfaceHelp(0)`: walks `HelpData[0]` and
+ // blits PICs 0x63 / 0x1ae fullscreen for click-through.
+ doInterfaceHelp(0);
+ dirty = true;
+ continue;
+ }
+ if (kBtnPagePrev.contains(ev.mouse.x, ev.mouse.y)) {
+ if (page > 0) page--;
+ dirty = true;
+ continue;
+ }
+ if (kBtnPageNext.contains(ev.mouse.x, ev.mouse.y)) {
+ page++;
+ dirty = true;
+ continue;
+ }
+ if (kNoteArea.contains(ev.mouse.x, ev.mouse.y)) {
+ // Toggle the selection on whichever clue's text
+ // the click landed in. The original calls
+ // `_InterfaceHelp` here; that's the help screen,
+ // not selection â selection is in the Accuse
+ // screen. We use the area for selection because
+ // keyboard 1..9 toggling is awkward, and the
+ // resulting `_NoteSelected` state is what
+ // `_SolvedCheck` reads.
+ for (uint i = 0; i < _notebookSlotRects.size(); i++) {
+ if (_notebookSlotRects[i].contains(ev.mouse.x,
+ ev.mouse.y)) {
+ const uint clueId = _notebookSlotClues[i];
+ _mystery._noteSelected[clueId] ^= 1;
+ dirty = true;
+ break;
+ }
+ }
+ continue;
+ }
+ }
+ }
+ if (exitFlag)
+ break;
+
+ const uint32 now = g_system->getMillis();
+ // Re-render every 100 ms so the partner sprite cycles frames.
+ if (dirty || now - lastDraw >= 100) {
+ draw();
+ lastDraw = now;
+ }
+ g_system->updateScreen();
+ g_system->delayMillis(15);
+ }
+}
+
+void EEMEngine::doGallery() {
+ // Mirrors `_DoGallery @ 158f:065b` and `_DrawGallery @ 158f:0046`.
+ // Verified directly from the disassembly:
+ // * Background: PIC 0x3f (same as PDA).
+ // * Partner sprite at (5, 0x50): anim 2 (Jake) / 0x10 (Jenny).
+ // `_NewAnimation(5, 0x50, ...)`. NOTE: gallery uses anim 2/0x10,
+ // PDA uses 1/0xb â different sprites.
+ // * Five fixed slot positions at `29be:0x116` (4 bytes per slot,
+ // `{u16 x, u16 y}`):
+ // slot 0 = ( 83, 14) slot 3 = (119, 90)
+ // slot 1 = (155, 14) slot 4 = (191, 90)
+ // slot 2 = (227, 14)
+ // * For each logical suspect i in 0..NumSuspects-1:
+ // picId = `*(u16 *)(_GalleryData + i * 0x46)` (entry +0).
+ // visible = `_InGallery[_NewOrder[i]] != 0`.
+ // drawX = positions[_NewOrder[i]].x
+ // drawY = positions[_NewOrder[i]].y + (0x48 - pic.height)
+ // So portraits are BOTTOM-aligned to baselines 0x48 + pos.y.
+ // * Click on portrait via `_SearchSuspects` â `MoreInfo(i)` shows
+ // the suspect detail page. ESC returns to PDA.
+ // * Frame-cycled @ 100ms via `_CheckFrameRate` + `_UpdateAnimations`
+ // + `_GizmoColorCycle`.
+ if (!_mystery.isLoaded())
+ return;
+
+ const byte *gd = _mystery.galleryData();
+ if (!gd) {
+ warning("doGallery: no GalleryData in mystery %u", _mystery.number());
+ return;
+ }
+
+ CursorMan.showMouse(true);
+
+ struct Slot { int x; int y; };
+ static const Slot kGallerySlots[5] = {
+ { 83, 14 }, // 0
+ { 155, 14 }, // 1
+ { 227, 14 }, // 2
+ { 119, 90 }, // 3
+ { 191, 90 } // 4
+ };
+
+ // Pre-load static elements once.
+ Picture galBg;
+ const bool haveBg = _picsArchive.getPicture(0x3f, galBg);
+
+ // Gallery partner anim â `_DoGallery` calls `_GetAnimation(uVar6)` with
+ // uVar6 = 2 (Jake) / 0x10 (Jenny). Different from PDA (1 / 0xb).
+ const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
+ Animation partnerAni;
+ const bool havePartner = _aniArchive.loadAnimation(partnerAnim, partnerAni)
+ && !partnerAni.empty();
+
+ const uint8 num = _mystery.numSuspects();
+
+ // Cache slot rects for click hit-testing.
+ Common::Array<Common::Rect> slotRects;
+ Common::Array<int> slotSuspect; // logical suspect index in [0, num)
+ slotRects.resize(num);
+ slotSuspect.resize(num);
+ for (uint i = 0; i < num; i++) {
+ slotSuspect[i] = -1;
+ }
+
+ auto drawFrame = [&]() {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+
+ if (haveBg) {
+ const int bw = MIN<int>(galBg.surface.w, 320);
+ const int bh = MIN<int>(galBg.surface.h, 200);
+ for (int row = 0; row < bh; row++) {
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)galBg.surface.getBasePtr(0, row), bw);
+ }
+ }
+
+ // Partner sprite frame @ (5, 0x50).
+ if (havePartner) {
+ const uint32 now = g_system->getMillis();
+ const uint frameIdx = (uint)((now / 100) % partnerAni.size());
+ const Picture &fr = partnerAni[frameIdx];
+ const byte transp = (byte)(fr.flags >> 8);
+ const int px = 5, py = 0x50;
+ for (int row = 0; row < fr.surface.h; row++) {
+ const int dstY = py + row;
+ if (dstY < 0 || dstY >= 200) continue;
+ const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, dstY);
+ for (int col = 0; col < fr.surface.w; col++) {
+ const int dstX = px + col;
+ if (dstX < 0 || dstX >= 320) continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ }
+
+ // Portraits â `_DrawGallery @ 158f:0046` walks suspects 0..N-1
+ // and only renders those flagged in `_InGallery[NewOrder[i]]`.
+ // Undiscovered slots are left empty in the original. We render
+ // a darkened placeholder + "?" so the player has visual feedback
+ // that suspects exist but are still unknown.
+ uint discoveredCount = 0;
+ for (uint i = 0; i < num && i < Mystery::kGalleryCap; i++) {
+ slotRects[i] = Common::Rect();
+ slotSuspect[i] = -1;
+
+ const uint8 phys = _mystery._newOrder[i];
+ if (phys >= 5)
+ continue;
+ const Slot &s = kGallerySlots[phys];
+
+ const bool discovered = _mystery._inGallery[phys] != 0;
+ if (discovered) {
+ const uint16 picId = READ_LE_UINT16(gd + i * 0x46);
+ Picture portrait;
+ if (picId == 0 ||
+ !_picsArchive.getPicture(picId, portrait))
+ continue;
+
+ const int placeX = s.x;
+ const int placeY = s.y + (0x48 - portrait.surface.h);
+ const byte transp = (byte)(portrait.flags >> 8);
+ const int w = MIN<int>(portrait.surface.w, 320 - placeX);
+ const int h = MIN<int>(portrait.surface.h, 200 - placeY);
+ if (w <= 0 || h <= 0)
+ continue;
+ for (int row = 0; row < h; row++) {
+ const int dstY = placeY + row;
+ if (dstY < 0) continue;
+ const byte *src =
+ (const byte *)portrait.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, dstY);
+ for (int col = 0; col < w; col++) {
+ const int dstX = placeX + col;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ slotRects[i] = Common::Rect(placeX, placeY,
+ placeX + w, placeY + h);
+ slotSuspect[i] = (int)i;
+ discoveredCount++;
+ } else {
+ // Undiscovered placeholder â small framed "?" box at
+ // (s.x, s.y) sized 0x40 Ã 0x48 (typical portrait size).
+ const int phW = 0x40, phH = 0x48;
+ const int phX = s.x, phY = s.y;
+ if (phX + phW <= 320 && phY + phH <= 200) {
+ scratch.fillRect(Common::Rect(phX, phY,
+ phX + phW, phY + phH), 0x20);
+ scratch.frameRect(Common::Rect(phX, phY,
+ phX + phW, phY + phH), 0x5C);
+ if (_font.isLoaded()) {
+ _font.drawString(&scratch, "?",
+ phX + phW / 2 - 3,
+ phY + phH / 2 - 4, phW, 0x5C);
+ }
+ }
+ }
+ }
+ (void)discoveredCount;
+
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ };
+
+ drawFrame();
+ uint32 lastDraw = g_system->getMillis();
+
+ while (!shouldQuit()) {
+ Common::Event ev;
+ bool exitFlag = false;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ return;
+ }
+ if (ev.type == Common::EVENT_KEYDOWN) {
+ if (ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ exitFlag = true;
+ break;
+ }
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ // PDA bottom-bar buttons mirror `_NoteButtons @ 29be:0147`.
+ // `_DoGallery @ 158f:065b` shares the SAME button table
+ // with `_DoNotebook` (both call `_FindButton` against the
+ // 11-entry table at 0x147). `_HandleGalleryButton @
+ // 158f:05c0` dispatches via a different jump table
+ // (158f:0645). Verified gallery button mapping:
+ // rect 0 (134,155) â 0x05ef = `_NextScreen = 4` (NOTEBOOK)
+ // rect 1 (93,115) â 0x0625 = `_InterfaceHelp` (HELP)
+ // rect 2 (157,178) â 0x0638 = generic exit (no-op)
+ // rect 3 (5,80) â 0x061e = `_KDHelp` (host hint)
+ // rect 4 (180,201) â 0x05ff = `_SolvedCheck` -> SOLVE
+ // rect 5 (204,224) â 0x0638 = generic exit
+ // rect 6 (226,247) â 0x0638 = generic exit
+ // rect 7 (7,177) â 0x05f7 = `_NextScreen = 2` (MAP)
+ // rect 8 (35,111) â 0x05e4 = `_NextScreen = 3` (SITE)
+ const Common::Rect kBtnSite ( 35, 111, 56, 136); // [8] SITE
+ const Common::Rect kBtnMap ( 7, 177, 57, 200); // [7] MAP
+ const Common::Rect kBtnAccuse (180, 174, 201, 190); // [4] SOLVE
+ const Common::Rect kBtnNotebook(134, 174, 155, 190); // [0] NOTEBOOK (back to PDA notes)
+ const Common::Rect kBtnHelp ( 93, 174, 115, 190); // [1] HELP
+ const Common::Rect kBtnPartner ( 5, 80, 44, 110); // [3] KD HELP
+ if (kBtnSite.contains(ev.mouse.x, ev.mouse.y)) {
+ exitFlag = true; break;
+ }
+ if (kBtnMap.contains(ev.mouse.x, ev.mouse.y)) {
+ doBigMap();
+ exitFlag = true; break;
+ }
+ if (kBtnAccuse.contains(ev.mouse.x, ev.mouse.y)) {
+ doAccuse();
+ exitFlag = true; break;
+ }
+ if (kBtnNotebook.contains(ev.mouse.x, ev.mouse.y)) {
+ // Already came from notebook; exiting returns to it.
+ exitFlag = true; break;
+ }
+ if (kBtnHelp.contains(ev.mouse.x, ev.mouse.y)) {
+ // Gallery rect 1 â `_InterfaceHelp(0)` per jmp table at
+ // 158f:0625 (HandleGalleryButton). Same picture sequence
+ // as the notebook HELP button.
+ doInterfaceHelp(0);
+ lastDraw = 0;
+ continue;
+ }
+ if (kBtnPartner.contains(ev.mouse.x, ev.mouse.y)) {
+ doHelp();
+ lastDraw = 0;
+ continue;
+ }
+ // `_SearchSuspects` walks the per-slot rects and returns
+ // the suspect index. We mirror that with cached rects.
+ bool clicked = false;
+ for (uint i = 0; i < slotRects.size(); i++) {
+ if (slotSuspect[i] < 0) continue;
+ if (slotRects[i].contains(ev.mouse.x, ev.mouse.y)) {
+ // `MoreInfo(i)` â show the suspect detail page.
+ // Mirrors `MoreInfo @ 158f:0419`:
+ // _RefreshGalleryBackground();
+ // _GetPicture(*(u16*)(gd + i*0x46));
+ // _AddPicBackground(pic, 0x94, 0xf);
+ // _DrawGalleryNotes(gd + i*0x46);
+ // loop until ESC or button click.
+ // Suspect data layout (verified against M1):
+ // +0..1: picId (used here AND for gallery slot)
+ // +8..9: number of clues for this suspect
+ // +0xa..??: array of u16 clue IDs (terminated
+ // by 0xFFFF if shorter than count).
+ const uint suspectIdx = (uint)slotSuspect[i];
+ const byte *suspect = gd + suspectIdx * 0x46;
+ const uint16 detailPic =
+ READ_LE_UINT16(suspect + 0);
+ const uint16 clueCount =
+ READ_LE_UINT16(suspect + 8);
+
+ Graphics::ManagedSurface ms(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ ms.clear();
+ if (haveBg) {
+ const int bw = MIN<int>(galBg.surface.w, 320);
+ const int bh = MIN<int>(galBg.surface.h, 200);
+ for (int row = 0; row < bh; row++) {
+ memcpy((byte *)ms.getBasePtr(0, row),
+ (const byte *)galBg.surface.getBasePtr(0, row), bw);
+ }
+ }
+ // Full suspect picture at (0x94, 0xf).
+ Picture detail;
+ if (_picsArchive.getPicture(detailPic, detail)) {
+ const byte transp =
+ (byte)(detail.flags >> 8);
+ const int dx = 0x94, dy = 0x0f;
+ const int dw = MIN<int>(detail.surface.w, 320 - dx);
+ const int dh = MIN<int>(detail.surface.h, 200 - dy);
+ for (int row = 0; row < dh; row++) {
+ const byte *src =
+ (const byte *)detail.surface.getBasePtr(0, row);
+ byte *dst =
+ (byte *)ms.getBasePtr(0, dy + row);
+ for (int col = 0; col < dw; col++) {
+ if (src[col] != transp)
+ dst[dx + col] = src[col];
+ }
+ }
+ }
+ // Suspect's clue notes inside _GalleryNoteRect
+ // = (78, 93, 288, 152), per 29be:0100. Cyan text
+ // renders directly on the PDA's natural blue
+ // screen â matches `_DrawGalleryNotes @ 158f:01f4`.
+ const int rx = 78, ry = 93;
+ const int rw = 288 - 78, rh = 152 - 93;
+
+ const byte *ni = _mystery.noteIndex();
+ const uint16 niCount = _mystery.noteIndexCount();
+ int yPos = ry;
+ const int lineH = _font.getFontHeight() + 1;
+ bool drewAny = false;
+ for (uint k = 0; k < clueCount && k < 30; k++) {
+ const uint16 clueId =
+ READ_LE_UINT16(suspect + 0xa + k * 2);
+ if (clueId == 0xFFFF) break;
+ if (clueId >= Mystery::kCluesFoundCap ||
+ !_mystery._cluesFound[clueId])
+ continue;
+ if (!ni || clueId >= niCount) continue;
+ const uint16 textOff =
+ READ_LE_UINT16(ni + clueId * 4);
+ Common::String txt =
+ parseString(_mystery.textAt(textOff),
+ _playerName, _partner);
+ if (txt.empty()) continue;
+ const byte color =
+ _mystery._noteSelected[clueId] ? 0x3C : 0x5C;
+ const int hLine = _font.drawWordWrapped(
+ &ms, rx, yPos, rw, txt, color);
+ yPos += hLine + 7;
+ drewAny = true;
+ if (yPos + lineH > ry + rh) break;
+ }
+ if (!drewAny && _font.isLoaded()) {
+ _font.drawString(&ms,
+ "No clues yet for this suspect.",
+ rx, ry, rw, 0x5C);
+ }
+ // Header / footer text.
+ if (_font.isLoaded()) {
+ _font.drawString(&ms, "SUSPECT FILE",
+ rx, ry - 11, rw, 0x3C);
+ _font.drawString(&ms, "(click / ESC: back)",
+ rx, ry + rh + 2, rw, 0x3C);
+ }
+ g_system->copyRectToScreen(ms.getPixels(),
+ ms.pitch, 0, 0, 320, 200);
+ g_system->updateScreen();
+
+ // Wait for click or ESC. Drain the queued
+ // LBUTTONDOWN that triggered this MoreInfo first
+ // so we don't immediately accept it as the
+ // dismiss event.
+ g_system->delayMillis(150);
+ {
+ Common::Event drain;
+ while (g_system->getEventManager()->pollEvent(drain)) {
+ if (drain.type == Common::EVENT_QUIT ||
+ drain.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return;
+ }
+ }
+ bool back = false;
+ while (!back && !shouldQuit()) {
+ Common::Event e2;
+ while (g_system->getEventManager()->pollEvent(e2)) {
+ if (e2.type == Common::EVENT_LBUTTONDOWN ||
+ (e2.type == Common::EVENT_KEYDOWN &&
+ (e2.kbd.keycode == Common::KEYCODE_ESCAPE ||
+ e2.kbd.keycode == Common::KEYCODE_RETURN))) {
+ back = true;
+ break;
+ }
+ if (e2.type == Common::EVENT_QUIT ||
+ e2.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return;
+ }
+ g_system->delayMillis(20);
+ }
+ // Force gallery redraw immediately so the
+ // player isn't left looking at the dismissed
+ // MoreInfo screen until the next 100 ms tick.
+ drawFrame();
+ lastDraw = g_system->getMillis();
+ clicked = true;
+ break;
+ }
+ }
+ (void)clicked;
+ }
+ }
+ if (exitFlag) break;
+
+ const uint32 now = g_system->getMillis();
+ if (now - lastDraw >= 100) {
+ drawFrame();
+ lastDraw = now;
+ }
+ g_system->delayMillis(15);
+ }
+}
+
+void EEMEngine::doBigMap() {
+ // Two-stage flow that mirrors the original screen-1 wrapper at
+ // 20fe:120b and `_DoBigMap @ 20fe:09e7`:
+ //
+ // STAGE 1 â Overview. PIC 0x42 + site icons drawn via the
+ // `_DrawBigMapButtons` algorithm at BigMap coords MapData[+4/+6].
+ // The original `_DoBigMap` returns sx/sy = (mouseX*2 - 0x74,
+ // mouseY*2 - 0x55) when the player clicks inside `BigMapWindow`,
+ // which is the scroll position into the SmallMap.
+ //
+ // STAGE 2 â Detail zoom. PIC 0x43 frame + a 0xe9 Ã 0xab viewport
+ // into BIGMAP.PIC at (2, 2), drawn by `DrawMap @ 20fe:1058` with
+ // the (sx, sy) returned from stage 1. Site icons are stamped at
+ // SmallMap coords MapData[+8/+0xa] via `_StampButtons`. Click on
+ // a site icon â travel.
+ //
+ // MapData entry layout (14 bytes), verified directly from the
+ // disassembly of `_DrawBigMapButtons @ 20fe:0877` (`PUSH ES:[BX+4]`
+ // for X, `PUSH ES:[BX+6]` for Y, `CMP ES:[BX+0xc], 0` for crime)
+ // and `_StampButtons @ 20fe:0d2f` (`MOV AX, ES:[BX+8]`,
+ // `MOV AX, ES:[BX+0xa]`):
+ // +0..3 ??? (not yet decoded)
+ // +4..5 BigMap X
+ // +6..7 BigMap Y
+ // +8..9 SmallMap X
+ // +0xa..b SmallMap Y
+ // +0xc..d crime-flag
+
+ if (!_mystery.isLoaded())
+ return;
+
+ CursorMan.showMouse(true);
+
+ // `_GetPalette(0x24)` per `_DoBigMap @ 20fe:09e7`.
+ setSitePalette(0x24);
+
+ const Common::Rect kSetupRect(0xc7, 0x12, 0xc7 + 0x32, 0x12 + 0xa); // approx; original from globals
+ (void)kSetupRect; // not yet wired into our overlay
+
+ // ------------------------------------------------------------------
+ // STAGE 1 â Overview: PIC 0x42 + clickable site icons.
+ // ------------------------------------------------------------------
+
+ // `_DoBigMap @ 20fe:09e7` (20fe:0a44-0a99) registers a partner sprite
+ // on the overview frame. The animation depends on `_LastScreen`:
+ // * When LastScreen == 2 (came from the site loop) the original
+ // plays an entrance anim (`anum-1` for Jake / Jenny) at
+ // (0x102, 0x50), then on END swaps to the idle anim at (0xfd,
+ // 0x50). We don't track LastScreen finely enough to distinguish,
+ // so we render the IDLE pose at (0xfd, 0x50) which is what the
+ // player sees the rest of the time anyway.
+ // * Idle anim ID: Jake = 0x14 (20), Jenny = 0x12 (18).
+ const uint kMapAniId = (_partner == 0) ? 0x14 : 0x12;
+ Animation mapAnim;
+ const bool haveMapAnim = _aniArchive.loadAnimation(kMapAniId, mapAnim)
+ && !mapAnim.empty();
+ const int kMapAnimX = 0xfd;
+ const int kMapAnimY = 0x50;
+
+ auto drawOverview = [&]() {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+
+ Picture frame;
+ if (_picsArchive.getPicture(0x42, frame)) {
+ const int w = MIN<int>(frame.surface.w, 320);
+ const int h = MIN<int>(frame.surface.h, 200);
+ for (int row = 0; row < h; row++)
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)frame.surface.getBasePtr(0, row), w);
+ }
+
+ // Marker PICs from `_main @ 1a35:0f59`. Three globals are filled
+ // once at boot via `_GetPicture` (1-based IDs â entries N-1):
+ // _DoneMarker = PIC 0x20d (already-searched site)
+ // _SiteMarker = PIC 0xc5 (default available site)
+ // _CrimeMarker = PIC 0xc6 (crime-scene flag set)
+ // Picked per-site by `_DrawBigMapButtons @ 20fe:0877`:
+ // 1. SaveSiteComplete[i] â DoneMarker
+ // 2. else MapData[+0xc] != 0 â CrimeMarker
+ // 3. else SiteMarker
+ Picture done, normal, crimeM;
+ const bool haveDone = _picsArchive.getPicture(0x20d, done);
+ const bool haveNormal = _picsArchive.getPicture(0xc5, normal);
+ const bool haveCrime = _picsArchive.getPicture(0xc6, crimeM);
+
+ auto blitMarker = [&](const Picture &m, int x, int y) {
+ const byte transp = (byte)(m.flags >> 8);
+ for (int row = 0; row < m.surface.h; row++) {
+ const int dstY = y + row;
+ if (dstY < 0 || dstY >= 200) continue;
+ const byte *src = (const byte *)m.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, dstY);
+ for (int col = 0; col < m.surface.w; col++) {
+ const int dstX = x + col;
+ if (dstX < 0 || dstX >= 320) continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ };
+
+ for (uint i = 0; i < _mystery.numSites(); i++) {
+ if (!_mystery._onSites[i] && i != _mystery._siteNumber)
+ continue;
+ const byte *entry = _mystery.mapEntry(i);
+ if (!entry)
+ continue;
+ const uint16 mx = READ_LE_UINT16(entry + 0x4);
+ const uint16 my = READ_LE_UINT16(entry + 0x6);
+ const uint16 crime = READ_LE_UINT16(entry + 0xc);
+ const bool done_ = (i < Mystery::kVisitedSiteCap)
+ && _mystery._visitedSite[i];
+
+ const Picture *m = nullptr;
+ if (done_ && haveDone) m = &done;
+ else if (crime != 0 && haveCrime) m = &crimeM;
+ else if (haveNormal) m = &normal;
+
+ if (m)
+ blitMarker(*m, (int)mx, (int)my);
+ else {
+ // Fallback if the markers couldn't be loaded.
+ const Common::Rect mark(mx - 3, my - 3, mx + 4, my + 4);
+ scratch.fillRect(mark, 0x0F);
+ }
+ }
+
+ // Partner sprite â masked-blit at (0xfd, 0x50). Same per-tick
+ // idle the original would run via `_UpdateAnimations` once the
+ // entrance one-shot transitions out (see `_DoBigMap` 0xae3-0xae7
+ // where it swaps animId on the 0x80 marker).
+ if (haveMapAnim) {
+ const uint32 now = g_system->getMillis();
+ const uint frameIdx = (uint)((now / 100) % mapAnim.size());
+ const Picture &fr = mapAnim[frameIdx];
+ const byte transp = (byte)(fr.flags >> 8);
+ for (int row = 0; row < fr.surface.h; row++) {
+ const int dstY = kMapAnimY + row;
+ if (dstY < 0 || dstY >= 200) continue;
+ const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, dstY);
+ for (int col = 0; col < fr.surface.w; col++) {
+ const int dstX = kMapAnimX + col;
+ if (dstX < 0 || dstX >= 320) continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ }
+
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ };
+
+ drawOverview();
+ uint32 mapLastTick = g_system->getMillis();
+
+ // Static rectangles read directly from the binary at the labelled
+ // addresses (29be:0x1596 onwards). Format is {x1, y1, x2, y2}.
+ const Common::Rect kBigMapWindow ( 0, 0, 247, 192); // 29be:1596
+ const Common::Rect kSetupBtnRect (252, 4, 315, 42); // 29be:15ce
+
+ bool wantZoom = false;
+ int zoomX = 0, zoomY = 0;
+ while (!shouldQuit()) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return;
+ if (ev.type == Common::EVENT_KEYDOWN &&
+ ev.kbd.keycode == Common::KEYCODE_ESCAPE)
+ return;
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ // SetupButtonRect â `_NextScreen = 6` (the original's
+ // settings screen). We use it as "back to menu":
+ // abandon the current mystery and return to case
+ // selection.
+ if (kSetupBtnRect.contains(ev.mouse.x, ev.mouse.y)) {
+ _mystery.clear();
+ _nextScreen = kScreenInvalid;
+ return;
+ }
+ // Click in the BigMapWindow â zoom. Original formula:
+ // sx = mouseX*2 - 0x74; sy = mouseY*2 - 0x55
+ if (kBigMapWindow.contains(ev.mouse.x, ev.mouse.y)) {
+ int sx = ev.mouse.x * 2;
+ int sy = ev.mouse.y * 2;
+ sx = (sx < 0x75) ? 0 : sx - 0x74;
+ sy = (sy < 0x56) ? 0 : sy - 0x55;
+ zoomX = sx;
+ zoomY = sy;
+ wantZoom = true;
+ break;
+ }
+ }
+ }
+ if (wantZoom)
+ break;
+ // Cycle the partner-sprite frame every 100 ms (matching the
+ // original's `_CheckFrameRate` cadence inside `_DoBigMap`).
+ const uint32 now = g_system->getMillis();
+ if (haveMapAnim && now - mapLastTick >= 100) {
+ mapLastTick = now;
+ drawOverview();
+ }
+ g_system->updateScreen();
+ g_system->delayMillis(10);
+ }
+
+ if (!wantZoom)
+ return;
+
+ // ------------------------------------------------------------------
+ // STAGE 2 â Detail zoom: PIC 0x43 frame + scrollable BIGMAP.PIC
+ // viewport at (2, 2), 0xe9 Ã 0xab. Click on a stamped icon â travel.
+ // ------------------------------------------------------------------
+
+ Common::File f;
+ if (!f.open(Common::Path("BIGMAP.PIC"))) {
+ warning("doBigMap: BIGMAP.PIC missing for detail view");
+ return;
+ }
+ const uint16 mapH = f.readUint16LE();
+ const uint16 mapW = f.readUint16LE();
+ if (mapW == 0 || mapH == 0)
+ return;
+ Common::Array<byte> mapPixels((uint32)mapW * mapH);
+ if (f.read(mapPixels.data(), mapPixels.size()) != mapPixels.size()) {
+ warning("doBigMap: short read on BIGMAP.PIC for detail view");
+ return;
+ }
+
+ const int kMapWinW = 0xe9; // 233
+ const int kMapWinH = 0xab; // 171
+ const int kMapWinX = 2;
+ const int kMapWinY = 2;
+
+ int scrollX = MAX<int>(0, MIN<int>(mapW - kMapWinW, zoomX));
+ int scrollY = MAX<int>(0, MIN<int>(mapH - kMapWinH, zoomY));
+
+ // `_DoMapScreen @ 20fe:120b` (20fe:12cd-12f0): partner sprite on
+ // the detail-zoom screen. Jake = anim 0x13 (19), Jenny = anim 0x11
+ // (17). Position (0x101, 0x50) = (257, 80), seqnum 0x13. The cells
+ // here have a "looking at the map" pose, distinct from the BigMap
+ // overview entrance/idle.
+ const uint kDetailAniId = (_partner == 0) ? 0x13 : 0x11;
+ Animation detailAnim;
+ const bool haveDetailAnim = _aniArchive.loadAnimation(kDetailAniId,
+ detailAnim)
+ && !detailAnim.empty();
+ const int kDetailAnimX = 0x101;
+ const int kDetailAnimY = 0x50;
+
+ auto drawDetail = [&]() {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+
+ Picture frame;
+ if (_picsArchive.getPicture(0x43, frame)) {
+ const int w = MIN<int>(frame.surface.w, 320);
+ const int h = MIN<int>(frame.surface.h, 200);
+ for (int row = 0; row < h; row++)
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)frame.surface.getBasePtr(0, row), w);
+ }
+
+ const int copyW = MIN<int>(mapW - scrollX, kMapWinW);
+ const int copyH = MIN<int>(mapH - scrollY, kMapWinH);
+ for (int row = 0; row < copyH; row++) {
+ memcpy((byte *)scratch.getBasePtr(kMapWinX, kMapWinY + row),
+ mapPixels.data() + (scrollY + row) * mapW + scrollX,
+ copyW);
+ }
+
+ // Stamped site buttons. `_StampButtons @ 20fe:0d2f` does:
+ // button = _GetButton(MapData[+0]) // BUTTON.DBD entry
+ // destX = MapData[+8], destY = MapData[+0xa]
+ // then bakes the button PIC into the map bitmap. Each button
+ // sprite carries the site name baked in. We blit them on top
+ // of the BIGMAP.PIC viewport at the same SmallMap coords.
+ for (uint i = 0; i < _mystery.numSites(); i++) {
+ if (!_mystery._onSites[i] && i != _mystery._siteNumber)
+ continue;
+ const byte *entry = _mystery.mapEntry(i);
+ if (!entry) continue;
+ const uint16 buttonId = READ_LE_UINT16(entry + 0x0);
+ const uint16 mx = READ_LE_UINT16(entry + 0x8);
+ const uint16 my = READ_LE_UINT16(entry + 0xa);
+
+ Picture button;
+ if (!_buttonArchive.loadEntry(buttonId, button))
+ continue;
+ const int sx = (int)mx - scrollX + kMapWinX;
+ const int sy = (int)my - scrollY + kMapWinY;
+ const byte transp = (byte)(button.flags >> 8);
+
+ // Crop blit against the viewport.
+ const int x0 = MAX<int>(sx, kMapWinX);
+ const int y0 = MAX<int>(sy, kMapWinY);
+ const int x1 = MIN<int>(sx + button.surface.w, kMapWinX + kMapWinW);
+ const int y1 = MIN<int>(sy + button.surface.h, kMapWinY + kMapWinH);
+ for (int row = y0; row < y1; row++) {
+ const byte *src = (const byte *)button.surface.getBasePtr(0, row - sy);
+ byte *dst = (byte *)scratch.getBasePtr(0, row);
+ for (int col = x0; col < x1; col++) {
+ const byte px = src[col - sx];
+ if (px != transp)
+ dst[col] = px;
+ }
+ }
+ }
+
+ // Partner sprite on the detail map. Drawn last so it sits over
+ // the frame and the BIGMAP.PIC viewport.
+ if (haveDetailAnim) {
+ const uint32 now = g_system->getMillis();
+ const uint frameIdx =
+ (uint)((now / 100) % detailAnim.size());
+ const Picture &fr = detailAnim[frameIdx];
+ const byte transp = (byte)(fr.flags >> 8);
+ for (int row = 0; row < fr.surface.h; row++) {
+ const int dstY = kDetailAnimY + row;
+ if (dstY < 0 || dstY >= 200) continue;
+ const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, dstY);
+ for (int col = 0; col < fr.surface.w; col++) {
+ const int dstX = kDetailAnimX + col;
+ if (dstX < 0 || dstX >= 320) continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ }
+
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ };
+
+ drawDetail();
+ uint32 detailLastTick = g_system->getMillis();
+
+ while (!shouldQuit()) {
+ Common::Event ev;
+ bool dirty = false;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return;
+ if (ev.type == Common::EVENT_KEYDOWN) {
+ if (ev.kbd.keycode == Common::KEYCODE_ESCAPE)
+ return; // exit detail back to caller (site loop / engine)
+ const int kStep = 16;
+ if (ev.kbd.keycode == Common::KEYCODE_LEFT) {
+ scrollX = MAX<int>(0, scrollX - kStep);
+ dirty = true;
+ } else if (ev.kbd.keycode == Common::KEYCODE_RIGHT) {
+ scrollX = MIN<int>(MAX<int>(0, mapW - kMapWinW),
+ scrollX + kStep);
+ dirty = true;
+ } else if (ev.kbd.keycode == Common::KEYCODE_UP) {
+ scrollY = MAX<int>(0, scrollY - kStep);
+ dirty = true;
+ } else if (ev.kbd.keycode == Common::KEYCODE_DOWN) {
+ scrollY = MIN<int>(MAX<int>(0, mapH - kMapWinH),
+ scrollY + kStep);
+ dirty = true;
+ }
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ // Scroll arrows + slider rects live in `SmallMapButtons`
+ // at 29be:0x159e (six 8-byte rects in order: Y-up, Y-down,
+ // X-left, X-right, right-panel, top-right) plus the
+ // dedicated `XSliderRect @ 29be:15d6` and
+ // `YSliderRect @ 29be:15de`. Format {x1,y1,x2,y2}.
+ const Common::Rect kArrowYUp (237, 2, 247, 11);
+ const Common::Rect kArrowYDown (237, 163, 247, 172);
+ const Common::Rect kArrowXLeft ( 2, 175, 12, 185);
+ const Common::Rect kArrowXRight(224, 175, 234, 185);
+ const Common::Rect kXSlider ( 15, 175, 221, 185);
+ const Common::Rect kYSlider (237, 14, 247, 160);
+ const Common::Rect kSetupBtn (252, 4, 315, 42);
+
+ const int kArrowStep = 16;
+ const int kSliderRange = mapW - kMapWinW;
+ const int kSliderRangeY = mapH - kMapWinH;
+
+ if (kSetupBtn.contains(ev.mouse.x, ev.mouse.y)) {
+ // Setup button on detail too â `_NextScreen = 6` in
+ // the original. We treat it the same way: bail back
+ // to case selection.
+ _mystery.clear();
+ _nextScreen = kScreenInvalid;
+ return;
+ }
+ if (kArrowYUp.contains(ev.mouse.x, ev.mouse.y)) {
+ scrollY = MAX<int>(0, scrollY - kArrowStep);
+ dirty = true;
+ } else if (kArrowYDown.contains(ev.mouse.x, ev.mouse.y)) {
+ scrollY = MIN<int>(MAX<int>(0, kSliderRangeY),
+ scrollY + kArrowStep);
+ dirty = true;
+ } else if (kArrowXLeft.contains(ev.mouse.x, ev.mouse.y)) {
+ scrollX = MAX<int>(0, scrollX - kArrowStep);
+ dirty = true;
+ } else if (kArrowXRight.contains(ev.mouse.x, ev.mouse.y)) {
+ scrollX = MIN<int>(MAX<int>(0, kSliderRange),
+ scrollX + kArrowStep);
+ dirty = true;
+ } else if (kXSlider.contains(ev.mouse.x, ev.mouse.y)) {
+ // Click on X slider track â jump scrollX so the
+ // click position maps proportionally into the map.
+ if (kSliderRange > 0) {
+ const int t = ev.mouse.x - kXSlider.left;
+ const int tw = kXSlider.width();
+ scrollX = MAX<int>(0, MIN<int>(kSliderRange,
+ t * kSliderRange / MAX<int>(1, tw)));
+ dirty = true;
+ }
+ } else if (kYSlider.contains(ev.mouse.x, ev.mouse.y)) {
+ if (kSliderRangeY > 0) {
+ const int t = ev.mouse.y - kYSlider.top;
+ const int th = kYSlider.height();
+ scrollY = MAX<int>(0, MIN<int>(kSliderRangeY,
+ t * kSliderRangeY / MAX<int>(1, th)));
+ dirty = true;
+ }
+ } else if (ev.mouse.x >= kMapWinX &&
+ ev.mouse.x < kMapWinX + kMapWinW &&
+ ev.mouse.y >= kMapWinY &&
+ ev.mouse.y < kMapWinY + kMapWinH) {
+ // Hit-test the per-site button at its actual bbox
+ // (`_StampButtons` records the rect at SmallMap +8/+0xa
+ // with the button PIC's width/height).
+ for (uint i = 0; i < _mystery.numSites(); i++) {
+ if (!_mystery._onSites[i] &&
+ i != _mystery._siteNumber)
+ continue;
+ const byte *entry = _mystery.mapEntry(i);
+ if (!entry) continue;
+ const uint16 buttonId = READ_LE_UINT16(entry + 0x0);
+ const uint16 mx = READ_LE_UINT16(entry + 0x8);
+ const uint16 my = READ_LE_UINT16(entry + 0xa);
+ Picture button;
+ int bw = 16, bh = 16;
+ if (_buttonArchive.loadEntry(buttonId, button)) {
+ bw = button.surface.w;
+ bh = button.surface.h;
+ }
+ const int sx = (int)mx - scrollX + kMapWinX;
+ const int sy = (int)my - scrollY + kMapWinY;
+ if (ev.mouse.x >= sx && ev.mouse.x < sx + bw &&
+ ev.mouse.y >= sy && ev.mouse.y < sy + bh) {
+ _mystery._lastSite = _mystery._siteNumber;
+ _mystery._siteNumber = (uint16)i;
+ return;
+ }
+ }
+ }
+ }
+ }
+ // Cycle the partner sprite at 100 ms ticks (same cadence as
+ // `_DoMapScreen`'s `_CheckFrameRate` + `_UpdateAnimations` loop).
+ const uint32 now = g_system->getMillis();
+ if (haveDetailAnim && now - detailLastTick >= 100) {
+ detailLastTick = now;
+ dirty = true;
+ }
+ if (dirty)
+ drawDetail();
+ g_system->updateScreen();
+ g_system->delayMillis(10);
+ }
+}
+
+uint16 EEMEngine::getKDTextBalloon(byte firstChar) const {
+ // Mirrors `_GetKDTextBalloon @ 1df2:0105`:
+ // if ((ctype[firstChar] & 2) == 0) bub = *(u16*)29be:1068 = 0x17
+ // else bub = *(u16*)(29be:0fe6+0x1e+c*2)
+ // `ctype` is Borland's `_ctype_` array at `29be:2be1`. Bit 1 (0x02) is
+ // set only for digits '0'..'9' (verified by reading the table â '0'..'9'
+ // each map to byte 0x02; everything else has bit 1 clear).
+ // Lookup table at 29be:1064 (= 29be:0fe6 + 0x1e + '0'*2):
+ // '0'â0x15 '1'â0x16 '2'â0x17 '3'â0x18 '4'â0x19
+ // '5'â0x1a '6'â0x20 '7'â0x21 '8'â0x22 '9'â0x1e
+ // Note `*(u16*)29be:1068` (= entry for '2') is the same byte the
+ // non-digit fallback returns â the original encodes the constant by
+ // reusing the digit-2 slot.
+ if (firstChar < '0' || firstChar > '9')
+ return 0x17;
+ static const uint16 kDigitBalloons[10] = {
+ 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x20, 0x21, 0x22, 0x1e
+ };
+ return kDigitBalloons[firstChar - '0'];
+}
+
+void EEMEngine::doAccuse() {
+ if (!_mystery.isLoaded())
+ return;
+
+ // Mirrors `_DoAccuseGallery @ 1df2:0a31`:
+ // 1. Show KD's hint balloon (KDTextIndex[+8] text).
+ // 2. `_GetBackground(0x3f)` â same backdrop as PDA / gallery.
+ // 3. `_DrawGallery()` â renders portraits at the standard 5 slots
+ // (positions verified at 29be:0x116, bottom-aligned baseline 0x48).
+ // 4. Click loop dispatching on `_NoteButtons` (same table as PDA)
+ // with a separate `_HandleAccuseNoteButton` jump table.
+ const uint8 num = _mystery.numSuspects();
+ if (num == 0)
+ return;
+
+ const byte *gd = _mystery.galleryData();
+
+ // Verbatim from 29be:0x116 â same five suspect slot positions as
+ // `_DrawGallery @ 158f:0046`.
+ struct Slot { int x; int y; };
+ static const Slot kGallerySlots[5] = {
+ { 83, 14 }, { 155, 14 }, { 227, 14 },
+ { 119, 90 }, { 191, 90 }
+ };
+
+ Picture accuseBg;
+ const bool haveAccuseBg = _picsArchive.getPicture(0x3f, accuseBg);
+
+ Common::Array<Common::Rect> slotRects;
+ Common::Array<int> slotSuspect;
+ slotRects.resize(num);
+ slotSuspect.resize(num);
+ for (uint i = 0; i < num; i++)
+ slotSuspect[i] = -1;
+
+ int highlighted = 0;
+ auto drawGallery = [&]() {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ if (haveAccuseBg) {
+ const int bw = MIN<int>(accuseBg.surface.w, 320);
+ const int bh = MIN<int>(accuseBg.surface.h, 200);
+ for (int row = 0; row < bh; row++) {
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)accuseBg.surface.getBasePtr(0, row), bw);
+ }
+ }
+
+ for (uint i = 0; i < num && i < Mystery::kGalleryCap; i++) {
+ slotRects[i] = Common::Rect();
+ slotSuspect[i] = -1;
+ if (!gd) continue;
+ const uint8 phys = _mystery._newOrder[i];
+ if (phys >= 5) continue;
+ // `_DrawGallery @ 158f:00b9` skips suspects whose
+ // `_InGallery[phys]` flag is 0 â that's the original gate
+ // (some suspects only become visible after being met or
+ // stay hidden after a wrong accusation removes them).
+ if (_mystery._inGallery[phys] == 0) continue;
+ const Slot &s = kGallerySlots[phys];
+
+ const uint16 picId = READ_LE_UINT16(gd + i * 0x46);
+ if (picId == 0) continue;
+ Picture portrait;
+ if (!_picsArchive.getPicture(picId, portrait))
+ continue;
+
+ const int placeX = s.x;
+ const int placeY = s.y + (0x48 - portrait.surface.h);
+ const byte transp = (byte)(portrait.flags >> 8);
+ const int w = MIN<int>(portrait.surface.w, 320 - placeX);
+ const int h = MIN<int>(portrait.surface.h, 200 - placeY);
+ if (w <= 0 || h <= 0) continue;
+ for (int row = 0; row < h; row++) {
+ const int dstY = placeY + row;
+ if (dstY < 0) continue;
+ const byte *src =
+ (const byte *)portrait.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, dstY);
+ for (int col = 0; col < w; col++) {
+ const int dstX = placeX + col;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ slotRects[i] = Common::Rect(placeX, placeY,
+ placeX + w, placeY + h);
+ slotSuspect[i] = (int)i;
+ }
+
+ // Highlight indicator. The original moves the mouse cursor
+ // to the centre of the highlighted suspect via `_PutMouseInRect`
+ // (1df2:0b8e) â we draw a 1px outline in palette index 0xFE
+ // (within the marching-ants cycle range 0xF9..0xFE) which is
+ // unambiguously visible under any palette without warping the
+ // player's cursor.
+ if (highlighted >= 0 && highlighted < (int)slotRects.size() &&
+ !slotRects[highlighted].isEmpty()) {
+ Common::Rect r = slotRects[highlighted];
+ r.grow(1);
+ scratch.frameRect(r, 0xFE);
+ }
+
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ };
+
+ // Step 1 â KD hint balloon. Mirrors `_DoAccuseGallery @ 1df2:0a31`
+ // (1df2:0a4c-1df2:0afe):
+ // text = TextBlock + KDTextIndex[+8] (1df2:0a4c-0a57)
+ // bub = _GetKDTextBalloon(text[0]) (1df2:0a6d)
+ // GetBalloon(bub) (1df2:0a7c)
+ // y = (h < 0x4e) ? (0x50 - h) >> 1 : 1 (1df2:0a8b-0aa5)
+ // AddPicBackground(pic, 0x21, y) (1df2:0aab)
+ // WordWrap(0x21+tbl[bub].x, y+tbl[bub].y, tbl[bub].w, text, color=0)
+ // tbl @ 29be:0875, 10-byte entries (1df2:0ad6-0af1)
+ const byte *kdIdx = _mystery.kdTextIndex();
+ if (kdIdx) {
+ const int16 textOff = (int16)READ_LE_UINT16(kdIdx + 8);
+ if (textOff != -1) {
+ const char *raw = _mystery.textAt((uint16)textOff);
+ Common::String hint =
+ parseString(raw ? raw : "", _playerName, _partner);
+ if (!hint.empty()) {
+ // First-char dispatch via getKDTextBalloon (1df2:0105).
+ // Note: we pass the *parsed* first char; the original
+ // reads it BEFORE `_ParseString`, but the player-name /
+ // partner-name substitutions never start with digits, so
+ // the dispatch result is the same either way.
+ const byte firstChar =
+ hint.empty() ? (byte)0 : (byte)hint[0];
+ const uint16 bubNum = getKDTextBalloon(firstChar);
+ Picture balloon;
+ const bool haveBalloon =
+ _balloonArchive.size() > (bubNum & 0x7F) &&
+ _balloonArchive.loadEntry(bubNum & 0x7F, balloon);
+
+ // 1df2:0a8b-1df2:0aa5: y = (h < 0x4e) ? (0x50-h)>>1 : 1
+ const int balloonX = 0x21;
+ int balloonY = 1;
+ if (haveBalloon && balloon.surface.h < 0x4e)
+ balloonY = (0x50 - balloon.surface.h) / 2;
+
+ Graphics::ManagedSurface ms(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ ms.clear();
+ if (haveAccuseBg) {
+ const int bw = MIN<int>(accuseBg.surface.w, 320);
+ const int bh = MIN<int>(accuseBg.surface.h, 200);
+ for (int row = 0; row < bh; row++) {
+ memcpy((byte *)ms.getBasePtr(0, row),
+ (const byte *)accuseBg.surface.getBasePtr(0, row), bw);
+ }
+ }
+ // Masked balloon blit â `_Rect_Move_Mask` (1000:03fc)
+ // skips pixels equal to `pic[0] >> 8`.
+ if (haveBalloon) {
+ const byte transp = (byte)(balloon.flags >> 8);
+ const int bw = MIN<int>(balloon.surface.w, 320 - balloonX);
+ const int bh = MIN<int>(balloon.surface.h, 200 - balloonY);
+ for (int row = 0; row < bh; row++) {
+ const byte *src = (const byte *)balloon.surface.getBasePtr(0, row);
+ byte *dst = (byte *)ms.getBasePtr(balloonX, balloonY + row);
+ for (int col = 0; col < bw; col++) {
+ if (src[col] != transp)
+ dst[col] = src[col];
+ }
+ }
+ }
+ // Inset table @ 29be:0875 â 1df2:0acb pushes color=0.
+ uint16 tx = 5, ty = 4, tw = 155;
+ getBalloonInsets(bubNum, tx, ty, tw);
+ if (_font.isLoaded()) {
+ _font.drawWordWrapped(&ms, balloonX + tx,
+ balloonY + ty, tw, hint,
+ haveBalloon ? 0 : 0xF);
+ }
+ g_system->copyRectToScreen(ms.getPixels(), ms.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ waitForInput(8000);
+ }
+ }
+ }
+
+ // Helper to find the next "alive" slot (one whose `_inGallery[phys]`
+ // flag is still set so a portrait was actually drawn). Mirrors the
+ // way the original wraps DI past empty slots.
+ auto nextLiveSlot = [&](int from, int dir) -> int {
+ const int n = (int)slotRects.size();
+ if (n <= 0) return 0;
+ for (int step = 1; step <= n; step++) {
+ int idx = (from + dir * step) % n;
+ if (idx < 0) idx += n;
+ if (!slotRects[idx].isEmpty())
+ return idx;
+ }
+ return from;
+ };
+ if (slotRects[highlighted].isEmpty())
+ highlighted = nextLiveSlot(highlighted, +1);
+
+ drawGallery();
+
+ // Wait-for-pick loop. Mirrors `_DoAccuseGallery` 1df2:0b26-1df2:0bc8:
+ // * `_CheckFrameRate` + `_UpdateAnimations` per tick (1df2:0b2a-0b33)
+ // * 5-entry input dispatch table @ 1df2:0bc9:
+ // 0x09 (TAB) â handler 0x0b94 (cycle highlight)
+ // 0x0d (Enter) â handler 0x0b72 (pick = _SearchSuspects)
+ // 0x4b (LEFT) â handler 0x0b94
+ // 0x4d (RIGHT) â handler 0x0b94
+ // 0xFFFF (mb) â handler 0x0b72
+ // * 0x0b94: `INC DI` + wraparound + `_PutMouseInRect(&Guys[DI])`,
+ // i.e. advance highlight and warp cursor (1df2:0b94-0bb1).
+ // * 0x0b72: `_SearchSuspects` (158f:0584) â mouse-rect hit-test;
+ // if non-0xFFFF, pick that suspect.
+ // We don't warp the cursor (unfriendly under SDL); instead the
+ // highlight is drawn as a 1px outline and Enter picks it.
+ int picked = -1;
+ uint32 lastTick = g_system->getMillis();
+ bool dirty = false;
+ while (picked < 0 && !shouldQuit()) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return;
+ if (ev.type == Common::EVENT_KEYDOWN) {
+ switch (ev.kbd.keycode) {
+ case Common::KEYCODE_ESCAPE:
+ return;
+ case Common::KEYCODE_TAB:
+ case Common::KEYCODE_RIGHT:
+ highlighted = nextLiveSlot(highlighted, +1);
+ dirty = true;
+ break;
+ case Common::KEYCODE_LEFT:
+ // 1df2:0b94 increments DI for LEFT too â but a
+ // keyboard-driven UX is friendlier with separate
+ // directions, so we mirror Right=+1 / Left=-1.
+ highlighted = nextLiveSlot(highlighted, -1);
+ dirty = true;
+ break;
+ case Common::KEYCODE_RETURN:
+ case Common::KEYCODE_KP_ENTER:
+ if (highlighted >= 0 &&
+ highlighted < (int)slotRects.size() &&
+ !slotRects[highlighted].isEmpty()) {
+ picked = highlighted;
+ }
+ break;
+ default: {
+ const int k = (int)ev.kbd.keycode;
+ if (k >= Common::KEYCODE_1 && k <= Common::KEYCODE_9) {
+ const int idx = k - Common::KEYCODE_1;
+ if (idx < num &&
+ !slotRects[idx].isEmpty())
+ picked = idx;
+ }
+ break;
+ }
+ }
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ for (uint i = 0; i < slotRects.size(); i++) {
+ if (slotSuspect[i] < 0) continue;
+ if (slotRects[i].contains(ev.mouse.x, ev.mouse.y)) {
+ picked = (int)i;
+ break;
+ }
+ }
+ }
+ }
+ // 100 ms tick â the original calls `_UpdateAnimations` per
+ // `_CheckFrameRate` (1df2:0b33). The accuse screen has no
+ // animations registered, so the tick is just a redraw cadence.
+ // We still re-render whenever the highlight moves (`dirty`).
+ const uint32 now = g_system->getMillis();
+ if (dirty || now - lastTick >= 100) {
+ drawGallery();
+ lastTick = now;
+ dirty = false;
+ }
+ g_system->updateScreen();
+ g_system->delayMillis(10);
+ }
+ if (picked < 0)
+ return;
+
+ // Real chain evaluation: sum point values of clues the player marked
+ // "selected" in the notebook. Mirrors `_SolvedCheck` @ 1df2:00ec.
+ const int points = _mystery.selectedPoints();
+ const bool guessedRight = _mystery.solvedCheck();
+ debugC(1, kDebugScript, "doAccuse: picked=%d selectedPts=%d -> %s",
+ picked, points, guessedRight ? "correct" : "wrong");
+
+ // If the player hasn't marked any evidence yet, give them a hint
+ // rather than an instant fail. Mirrors the original "We're not ready
+ // to solve this mystery yet..." string at 29be:10f0.
+ if (points == 0 && _font.isLoaded()) {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ _font.drawWordWrapped(&scratch, 16, 80, 288,
+ "We're not ready to solve this mystery yet. "
+ "Let's keep investigating until we have some "
+ "more solid evidence to make our case! "
+ "(Press N in the site screen to mark clues.)",
+ 0xF);
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ waitForInput(15000);
+ return;
+ }
+
+ // Pick the ending based on the chain. For a correct accusation the
+ // original would call `_DisplayClue(_Mystery + AChain[0])`, play
+ // SCRAPBK.ANI and save progress. We load the matching `E<n>.BIN`
+ // ending text and render its pages with prev/next navigation.
+ const int endingNum = guessedRight ? picked : 0;
+ const Common::String fname = Common::String::format("E%d.BIN", endingNum);
+ Common::File f;
+ if (!f.open(Common::Path(fname))) {
+ warning("doAccuse: %s missing", fname.c_str());
+ return;
+ }
+
+ // E<n>.BIN format (verified against `_DisplayEndingPage` @ 1df2:044c):
+ // u16 numPages
+ // per page (10 bytes header + NUL-string):
+ // u16 picNum
+ // u16 x1, y1, x2, y2 (story rect)
+ // bytes[] NUL-terminated text
+ const uint32 fileLen = f.size();
+ Common::Array<byte> blob(fileLen);
+ if (f.read(blob.data(), fileLen) != fileLen)
+ return;
+ const byte *e = blob.data();
+ const uint16 pages = READ_LE_UINT16(e);
+
+ uint pageIdx = 0;
+
+ while (!shouldQuit()) {
+ // Walk to pageIdx.
+ uint pos = 2;
+ uint cur = 0;
+ while (cur < pageIdx && pos + 10 < fileLen) {
+ const char *t = (const char *)(e + pos + 10);
+ pos += 10 + strlen(t) + 1;
+ cur++;
+ }
+ if (pos + 10 >= fileLen)
+ break;
+
+ const uint16 picNum = READ_LE_UINT16(e + pos + 0);
+ const uint16 x1 = READ_LE_UINT16(e + pos + 2);
+ const uint16 y1 = READ_LE_UINT16(e + pos + 4);
+ const uint16 x2 = READ_LE_UINT16(e + pos + 6);
+ const uint16 y2 = READ_LE_UINT16(e + pos + 8);
+ const char *raw = (const char *)(e + pos + 10);
+ const Common::String txt = parseString(raw, _playerName, _partner);
+
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+
+ // Page background.
+ if (picNum != 0) {
+ Picture bg;
+ if (_picsArchive.getPicture(picNum, bg)) {
+ const int w = MIN<int>(bg.surface.w, 320);
+ const int h = MIN<int>(bg.surface.h, 200);
+ for (int row = 0; row < h; row++) {
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)bg.surface.getBasePtr(0, row), w);
+ }
+ }
+ }
+
+ if (_font.isLoaded()) {
+ Common::String banner = "Not enough evidence";
+ if (guessedRight)
+ banner = _mystery._firstTry ? "CORRECT - FIRST TRY!" : "CORRECT!";
+ _font.drawString(&scratch, banner, 8, 4, 320, 0xF);
+ _font.drawString(&scratch, Common::String::format("Evidence: %d/100 Suspect: %d",
+ points, picked + 1), 8, 16, 320, 0xF);
+ const int wrapW = MAX<int>(16, x2 - x1);
+ const int wrapY = MAX<int>(28, (int)y1);
+ (void)y2;
+ _font.drawWordWrapped(&scratch, x1, wrapY, wrapW, txt, 0xF);
+ _font.drawString(&scratch, Common::String::format("page %u/%u (Left/Right or click)",
+ pageIdx + 1, pages), 8, 188, 320, 0xF);
+ }
+
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+
+ // Page navigation.
+ bool advance = false;
+ bool back = false;
+ bool exit = false;
+ while (!advance && !back && !exit && !shouldQuit()) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ exit = true; break;
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ advance = true; break;
+ }
+ if (ev.type == Common::EVENT_KEYDOWN) {
+ if (ev.kbd.keycode == Common::KEYCODE_ESCAPE)
+ exit = true;
+ else if (ev.kbd.keycode == Common::KEYCODE_LEFT)
+ back = true;
+ else
+ advance = true;
+ break;
+ }
+ }
+ g_system->updateScreen();
+ g_system->delayMillis(15);
+ }
+ if (exit) break;
+ if (advance) {
+ if (pageIdx + 1 >= pages) break;
+ pageIdx++;
+ } else if (back) {
+ if (pageIdx > 0) pageIdx--;
+ }
+ }
+
+ // Mirror `_DisplayCorrect`'s scrap-book animation + solved tracking +
+ // auto-save (the original calls `_SavePlayerRecord` after a win).
+ if (guessedRight) {
+ const uint mn = _mystery.number();
+ if (mn < sizeof(_mysteriesSolved)) {
+ _mysteriesSolved[mn] = _mystery._firstTry ? 2 : 1;
+ }
+ playAnm(Common::Path("SCRAPBK.ANI"), 120, true);
+
+ // Auto-save into slot 0 (the engine's quicksave slot).
+ const Common::String desc = Common::String::format(
+ "%s â solved mystery %u", _playerName.c_str(), mn);
+ Common::Error err = saveGameState(0, desc, true);
+ if (err.getCode() != Common::kNoError)
+ warning("auto-save after solve failed: %s",
+ err.getDesc().c_str());
+ } else {
+ _mystery._firstTry = false;
+ }
+}
+
+} // End of namespace EEM
Commit: 7bb7d3b82e69e044876a35ca8ddff7964cddd5e4
https://github.com/scummvm/scummvm/commit/7bb7d3b82e69e044876a35ca8ddff7964cddd5e4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:40+02:00
Commit Message:
EEM: enforce code standards
Changed paths:
engines/eem/clues.cpp
engines/eem/eem.cpp
engines/eem/site.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index a12136c304f..f667ce49a73 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -242,15 +242,18 @@ void EEMEngine::doInitClues() {
auto blitMaskedAt = [&](const Picture &p, int x, int y) {
const byte transp = (byte)(p.flags >> 8);
Graphics::Surface *screen = g_system->lockScreen();
- if (!screen) return;
+ if (!screen)
+ return;
for (int row = 0; row < p.surface.h; row++) {
const int dstY = y + row;
- if (dstY < 0 || dstY >= screen->h) continue;
+ if (dstY < 0 || dstY >= screen->h)
+ continue;
const byte *src = (const byte *)p.surface.getBasePtr(0, row);
byte *dst = (byte *)screen->getBasePtr(0, dstY);
for (int col = 0; col < p.surface.w; col++) {
const int dstX = x + col;
- if (dstX < 0 || dstX >= screen->w) continue;
+ if (dstX < 0 || dstX >= screen->w)
+ continue;
if (src[col] != transp)
dst[dstX] = src[col];
}
@@ -319,16 +322,33 @@ void EEMEngine::doInitClues() {
uint16 seqY = 0x6c;
if (_partner == 0) {
switch (caseType) {
- case 1: seqAni = 0x38; seqY = 0x6d; break;
- case 2: seqAni = 0x37; seqY = 0x6c; break;
- case 3: seqAni = 0x39; seqY = 0x6c; break;
- default: break;
+ case 1:
+ seqAni = 0x38;
+ seqY = 0x6d;
+ break;
+ case 2:
+ seqAni = 0x37;
+ seqY = 0x6c;
+ break;
+ case 3:
+ seqAni = 0x39;
+ seqY = 0x6c;
+ break;
+ default:
+ break;
}
} else {
switch (caseType) {
- case 2: seqAni = 0x3a; seqY = 0x6c; break;
- case 3: seqAni = 0x3d; seqY = 0x6c; break;
- default: break;
+ case 2:
+ seqAni = 0x3a;
+ seqY = 0x6c;
+ break;
+ case 3:
+ seqAni = 0x3d;
+ seqY = 0x6c;
+ break;
+ default:
+ break;
}
}
if (seqAni != 0xFFFF) {
@@ -574,7 +594,8 @@ void EEMEngine::displayClue(const byte *clueBlock) {
// copy the changed band back. This preserves the site BG
// underneath unchanged regions.
Graphics::Surface *screen = g_system->lockScreen();
- if (!screen) break;
+ if (!screen)
+ break;
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
for (int row = 0; row < 200; row++) {
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index cdbc7e4ac37..b33fb699ae1 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -147,7 +147,8 @@ Common::Error EEMEngine::run() {
doSiteLoop();
while (!shouldQuit()) {
doCaseSelection();
- if (!_mystery.isLoaded()) break;
+ if (!_mystery.isLoaded())
+ break;
doInitClues();
doBigMap();
if (_mystery.isLoaded())
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 1e7f388e6eb..f0eb4342801 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -465,12 +465,14 @@ static void blitMaskedSurface(Graphics::Surface *screen,
const byte transp = (byte)(p.flags >> 8);
for (int row = 0; row < p.surface.h; row++) {
const int dstY = y + row;
- if (dstY < 0 || dstY >= screen->h) continue;
+ if (dstY < 0 || dstY >= screen->h)
+ continue;
const byte *src = (const byte *)p.surface.getBasePtr(0, row);
byte *dst = (byte *)screen->getBasePtr(0, dstY);
for (int col = 0; col < p.surface.w; col++) {
const int dstX = x + col;
- if (dstX < 0 || dstX >= screen->w) continue;
+ if (dstX < 0 || dstX >= screen->w)
+ continue;
if (src[col] != transp)
dst[dstX] = src[col];
}
@@ -598,12 +600,15 @@ void SiteScreen::applyColorCycles() {
// 0xf9..0xfe for hotspot marching ants (the `_ColorCycle(0xf9,
// 0xfe)` call at the bottom of `_DoSiteLoop`'s main loop).
auto rotate = [&](uint8 start, uint8 end) {
- if (end <= start) return;
+ if (end <= start)
+ return;
const uint count = (uint)end - (uint)start + 1;
byte buf[256 * 3];
g_system->getPaletteManager()->grabPalette(buf, start, count);
// Save first triplet, shift, restore at end.
- byte savedR = buf[0], savedG = buf[1], savedB = buf[2];
+ const byte savedR = buf[0];
+ const byte savedG = buf[1];
+ const byte savedB = buf[2];
for (uint i = 0; i + 1 < count; i++) {
buf[i * 3 + 0] = buf[(i + 1) * 3 + 0];
buf[i * 3 + 1] = buf[(i + 1) * 3 + 1];
@@ -1053,12 +1058,14 @@ void EEMEngine::playKdAnim(uint16 num) {
const int h = MIN<int>(fr.surface.h, 200 - py);
for (int row = 0; row < h; row++) {
const int dstY = py + row;
- if (dstY < 0) continue;
+ if (dstY < 0)
+ continue;
const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
byte *dst = (byte *)scratch.getBasePtr(0, dstY);
for (int col = 0; col < w; col++) {
const int dstX = px + col;
- if (dstX < 0) continue;
+ if (dstX < 0)
+ continue;
if (src[col] != transp)
dst[dstX] = src[col];
}
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 03bd77f609d..724e88e81ca 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -214,12 +214,14 @@ void EEMEngine::doCaseSelection() {
const byte transp = (byte)(fr.flags >> 8);
for (int row = 0; row < fr.surface.h; row++) {
const int dstY = kKdAnimY + row;
- if (dstY < 0 || dstY >= 200) continue;
+ if (dstY < 0 || dstY >= 200)
+ continue;
const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
byte *dst = (byte *)scratch.getBasePtr(0, dstY);
for (int col = 0; col < fr.surface.w; col++) {
const int dstX = kKdAnimX + col;
- if (dstX < 0 || dstX >= 320) continue;
+ if (dstX < 0 || dstX >= 320)
+ continue;
if (src[col] != transp)
dst[dstX] = src[col];
}
@@ -420,7 +422,8 @@ void EEMEngine::doCaseSelection() {
const int kLineH = 10;
const int kVisible = 12;
int top = (int)sel - kVisible / 2;
- if (top < 0) top = 0;
+ if (top < 0)
+ top = 0;
if (top + kVisible > (int)kMaxMystery + 1)
top = (int)kMaxMystery + 1 - kVisible;
for (int r = 0; r < kVisible; r++) {
@@ -429,8 +432,10 @@ void EEMEngine::doCaseSelection() {
break;
char marker = ' ';
if ((uint)idx < sizeof(_mysteriesSolved)) {
- if (_mysteriesSolved[idx] == 2) marker = '*';
- else if (_mysteriesSolved[idx] == 1) marker = '+';
+ if (_mysteriesSolved[idx] == 2)
+ marker = '*';
+ else if (_mysteriesSolved[idx] == 1)
+ marker = '+';
}
const char arrow = ((uint)idx == sel) ? '>' : ' ';
_font.drawString(&scratch,
@@ -476,7 +481,8 @@ void EEMEngine::doCaseSelection() {
const int kLineH = 10;
const int kVisible = 12;
int top = (int)sel - kVisible / 2;
- if (top < 0) top = 0;
+ if (top < 0)
+ top = 0;
if (top + kVisible > (int)kMaxMystery + 1)
top = (int)kMaxMystery + 1 - kVisible;
const int row = (ev.mouse.y - kListRect.top) / kLineH;
@@ -630,12 +636,14 @@ void EEMEngine::doNotebook() {
const byte transp = (byte)(fr.flags >> 8);
for (int row = 0; row < fr.surface.h; row++) {
const int dstY = 80 + row;
- if (dstY < 0 || dstY >= 200) continue;
+ if (dstY < 0 || dstY >= 200)
+ continue;
const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
byte *dst = (byte *)scratch.getBasePtr(0, dstY);
for (int col = 0; col < fr.surface.w; col++) {
const int dstX = 5 + col;
- if (dstX < 0 || dstX >= 320) continue;
+ if (dstX < 0 || dstX >= 320)
+ continue;
if (src[col] != transp)
dst[dstX] = src[col];
}
@@ -771,7 +779,8 @@ void EEMEngine::doNotebook() {
}
if (ev.kbd.keycode == Common::KEYCODE_LEFT ||
ev.kbd.keycode == Common::KEYCODE_PAGEUP) {
- if (page > 0) page--;
+ if (page > 0)
+ page--;
dirty = true;
} else if (ev.kbd.keycode == Common::KEYCODE_RIGHT ||
ev.kbd.keycode == Common::KEYCODE_PAGEDOWN ||
@@ -820,7 +829,8 @@ void EEMEngine::doNotebook() {
continue;
}
if (kBtnPagePrev.contains(ev.mouse.x, ev.mouse.y)) {
- if (page > 0) page--;
+ if (page > 0)
+ page--;
dirty = true;
continue;
}
@@ -952,12 +962,14 @@ void EEMEngine::doGallery() {
const int px = 5, py = 0x50;
for (int row = 0; row < fr.surface.h; row++) {
const int dstY = py + row;
- if (dstY < 0 || dstY >= 200) continue;
+ if (dstY < 0 || dstY >= 200)
+ continue;
const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
byte *dst = (byte *)scratch.getBasePtr(0, dstY);
for (int col = 0; col < fr.surface.w; col++) {
const int dstX = px + col;
- if (dstX < 0 || dstX >= 320) continue;
+ if (dstX < 0 || dstX >= 320)
+ continue;
if (src[col] != transp)
dst[dstX] = src[col];
}
@@ -996,7 +1008,8 @@ void EEMEngine::doGallery() {
continue;
for (int row = 0; row < h; row++) {
const int dstY = placeY + row;
- if (dstY < 0) continue;
+ if (dstY < 0)
+ continue;
const byte *src =
(const byte *)portrait.surface.getBasePtr(0, row);
byte *dst = (byte *)scratch.getBasePtr(0, dstY);
@@ -1106,7 +1119,8 @@ void EEMEngine::doGallery() {
// the suspect index. We mirror that with cached rects.
bool clicked = false;
for (uint i = 0; i < slotRects.size(); i++) {
- if (slotSuspect[i] < 0) continue;
+ if (slotSuspect[i] < 0)
+ continue;
if (slotRects[i].contains(ev.mouse.x, ev.mouse.y)) {
// `MoreInfo(i)` â show the suspect detail page.
// Mirrors `MoreInfo @ 158f:0419`:
@@ -1172,24 +1186,28 @@ void EEMEngine::doGallery() {
for (uint k = 0; k < clueCount && k < 30; k++) {
const uint16 clueId =
READ_LE_UINT16(suspect + 0xa + k * 2);
- if (clueId == 0xFFFF) break;
+ if (clueId == 0xFFFF)
+ break;
if (clueId >= Mystery::kCluesFoundCap ||
!_mystery._cluesFound[clueId])
continue;
- if (!ni || clueId >= niCount) continue;
+ if (!ni || clueId >= niCount)
+ continue;
const uint16 textOff =
READ_LE_UINT16(ni + clueId * 4);
Common::String txt =
parseString(_mystery.textAt(textOff),
_playerName, _partner);
- if (txt.empty()) continue;
+ if (txt.empty())
+ continue;
const byte color =
_mystery._noteSelected[clueId] ? 0x3C : 0x5C;
const int hLine = _font.drawWordWrapped(
&ms, rx, yPos, rw, txt, color);
yPos += hLine + 7;
drewAny = true;
- if (yPos + lineH > ry + rh) break;
+ if (yPos + lineH > ry + rh)
+ break;
}
if (!drewAny && _font.isLoaded()) {
_font.drawString(&ms,
@@ -1249,7 +1267,8 @@ void EEMEngine::doGallery() {
(void)clicked;
}
}
- if (exitFlag) break;
+ if (exitFlag)
+ break;
const uint32 now = g_system->getMillis();
if (now - lastDraw >= 100) {
@@ -1351,12 +1370,14 @@ void EEMEngine::doBigMap() {
const byte transp = (byte)(m.flags >> 8);
for (int row = 0; row < m.surface.h; row++) {
const int dstY = y + row;
- if (dstY < 0 || dstY >= 200) continue;
+ if (dstY < 0 || dstY >= 200)
+ continue;
const byte *src = (const byte *)m.surface.getBasePtr(0, row);
byte *dst = (byte *)scratch.getBasePtr(0, dstY);
for (int col = 0; col < m.surface.w; col++) {
const int dstX = x + col;
- if (dstX < 0 || dstX >= 320) continue;
+ if (dstX < 0 || dstX >= 320)
+ continue;
if (src[col] != transp)
dst[dstX] = src[col];
}
@@ -1376,9 +1397,12 @@ void EEMEngine::doBigMap() {
&& _mystery._visitedSite[i];
const Picture *m = nullptr;
- if (done_ && haveDone) m = &done;
- else if (crime != 0 && haveCrime) m = &crimeM;
- else if (haveNormal) m = &normal;
+ if (done_ && haveDone)
+ m = &done;
+ else if (crime != 0 && haveCrime)
+ m = &crimeM;
+ else if (haveNormal)
+ m = &normal;
if (m)
blitMarker(*m, (int)mx, (int)my);
@@ -1400,12 +1424,14 @@ void EEMEngine::doBigMap() {
const byte transp = (byte)(fr.flags >> 8);
for (int row = 0; row < fr.surface.h; row++) {
const int dstY = kMapAnimY + row;
- if (dstY < 0 || dstY >= 200) continue;
+ if (dstY < 0 || dstY >= 200)
+ continue;
const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
byte *dst = (byte *)scratch.getBasePtr(0, dstY);
for (int col = 0; col < fr.surface.w; col++) {
const int dstX = kMapAnimX + col;
- if (dstX < 0 || dstX >= 320) continue;
+ if (dstX < 0 || dstX >= 320)
+ continue;
if (src[col] != transp)
dst[dstX] = src[col];
}
@@ -1426,7 +1452,8 @@ void EEMEngine::doBigMap() {
const Common::Rect kSetupBtnRect (252, 4, 315, 42); // 29be:15ce
bool wantZoom = false;
- int zoomX = 0, zoomY = 0;
+ int zoomX = 0;
+ int zoomY = 0;
while (!shouldQuit()) {
Common::Event ev;
while (g_system->getEventManager()->pollEvent(ev)) {
@@ -1549,7 +1576,8 @@ void EEMEngine::doBigMap() {
if (!_mystery._onSites[i] && i != _mystery._siteNumber)
continue;
const byte *entry = _mystery.mapEntry(i);
- if (!entry) continue;
+ if (!entry)
+ continue;
const uint16 buttonId = READ_LE_UINT16(entry + 0x0);
const uint16 mx = READ_LE_UINT16(entry + 0x8);
const uint16 my = READ_LE_UINT16(entry + 0xa);
@@ -1587,12 +1615,14 @@ void EEMEngine::doBigMap() {
const byte transp = (byte)(fr.flags >> 8);
for (int row = 0; row < fr.surface.h; row++) {
const int dstY = kDetailAnimY + row;
- if (dstY < 0 || dstY >= 200) continue;
+ if (dstY < 0 || dstY >= 200)
+ continue;
const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
byte *dst = (byte *)scratch.getBasePtr(0, dstY);
for (int col = 0; col < fr.surface.w; col++) {
const int dstX = kDetailAnimX + col;
- if (dstX < 0 || dstX >= 320) continue;
+ if (dstX < 0 || dstX >= 320)
+ continue;
if (src[col] != transp)
dst[dstX] = src[col];
}
@@ -1704,12 +1734,14 @@ void EEMEngine::doBigMap() {
i != _mystery._siteNumber)
continue;
const byte *entry = _mystery.mapEntry(i);
- if (!entry) continue;
+ if (!entry)
+ continue;
const uint16 buttonId = READ_LE_UINT16(entry + 0x0);
const uint16 mx = READ_LE_UINT16(entry + 0x8);
const uint16 my = READ_LE_UINT16(entry + 0xa);
Picture button;
- int bw = 16, bh = 16;
+ int bw = 16;
+ int bh = 16;
if (_buttonArchive.loadEntry(buttonId, button)) {
bw = button.surface.w;
bh = button.surface.h;
@@ -1813,18 +1845,22 @@ void EEMEngine::doAccuse() {
for (uint i = 0; i < num && i < Mystery::kGalleryCap; i++) {
slotRects[i] = Common::Rect();
slotSuspect[i] = -1;
- if (!gd) continue;
+ if (!gd)
+ continue;
const uint8 phys = _mystery._newOrder[i];
- if (phys >= 5) continue;
+ if (phys >= 5)
+ continue;
// `_DrawGallery @ 158f:00b9` skips suspects whose
// `_InGallery[phys]` flag is 0 â that's the original gate
// (some suspects only become visible after being met or
// stay hidden after a wrong accusation removes them).
- if (_mystery._inGallery[phys] == 0) continue;
+ if (_mystery._inGallery[phys] == 0)
+ continue;
const Slot &s = kGallerySlots[phys];
const uint16 picId = READ_LE_UINT16(gd + i * 0x46);
- if (picId == 0) continue;
+ if (picId == 0)
+ continue;
Picture portrait;
if (!_picsArchive.getPicture(picId, portrait))
continue;
@@ -1834,10 +1870,12 @@ void EEMEngine::doAccuse() {
const byte transp = (byte)(portrait.flags >> 8);
const int w = MIN<int>(portrait.surface.w, 320 - placeX);
const int h = MIN<int>(portrait.surface.h, 200 - placeY);
- if (w <= 0 || h <= 0) continue;
+ if (w <= 0 || h <= 0)
+ continue;
for (int row = 0; row < h; row++) {
const int dstY = placeY + row;
- if (dstY < 0) continue;
+ if (dstY < 0)
+ continue;
const byte *src =
(const byte *)portrait.surface.getBasePtr(0, row);
byte *dst = (byte *)scratch.getBasePtr(0, dstY);
@@ -1933,7 +1971,9 @@ void EEMEngine::doAccuse() {
}
}
// Inset table @ 29be:0875 â 1df2:0acb pushes color=0.
- uint16 tx = 5, ty = 4, tw = 155;
+ uint16 tx = 5;
+ uint16 ty = 4;
+ uint16 tw = 155;
getBalloonInsets(bubNum, tx, ty, tw);
if (_font.isLoaded()) {
_font.drawWordWrapped(&ms, balloonX + tx,
@@ -1953,10 +1993,12 @@ void EEMEngine::doAccuse() {
// way the original wraps DI past empty slots.
auto nextLiveSlot = [&](int from, int dir) -> int {
const int n = (int)slotRects.size();
- if (n <= 0) return 0;
+ if (n <= 0)
+ return 0;
for (int step = 1; step <= n; step++) {
int idx = (from + dir * step) % n;
- if (idx < 0) idx += n;
+ if (idx < 0)
+ idx += n;
if (!slotRects[idx].isEmpty())
return idx;
}
@@ -2028,7 +2070,8 @@ void EEMEngine::doAccuse() {
}
if (ev.type == Common::EVENT_LBUTTONDOWN) {
for (uint i = 0; i < slotRects.size(); i++) {
- if (slotSuspect[i] < 0) continue;
+ if (slotSuspect[i] < 0)
+ continue;
if (slotRects[i].contains(ev.mouse.x, ev.mouse.y)) {
picked = (int)i;
break;
@@ -2189,12 +2232,15 @@ void EEMEngine::doAccuse() {
g_system->updateScreen();
g_system->delayMillis(15);
}
- if (exit) break;
+ if (exit)
+ break;
if (advance) {
- if (pageIdx + 1 >= pages) break;
+ if (pageIdx + 1 >= pages)
+ break;
pageIdx++;
} else if (back) {
- if (pageIdx > 0) pageIdx--;
+ if (pageIdx > 0)
+ pageIdx--;
}
}
Commit: a4dd19f31d7285fa9e00ac8d78a339a76cae1751
https://github.com/scummvm/scummvm/commit/a4dd19f31d7285fa9e00ac8d78a339a76cae1751
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:40+02:00
Commit Message:
EEM: removed lambdas
Changed paths:
engines/eem/clues.cpp
engines/eem/eem.h
engines/eem/site.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index f667ce49a73..da14c8990ca 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -46,6 +46,47 @@ const uint kPicChooseBackground = 0x8c; ///< `_GetBackground(0x8c)`
const uint kAniBoy = 8; ///< `_GetAnimation(8)` (Jake)
const uint kAniGirl = 9; ///< `_GetAnimation(9)` (Jenny)
+// `_DoHappiness @ 172b:27b5`: cursor X picks one of 4 rects; past
+// rect 3 is treated as level 4. Verbatim from `29be:030f`.
+const Common::Rect kHappyZones[4] = {
+ Common::Rect( 0, 0, 70, 200), // far left â girl very happy, boy neutral
+ Common::Rect( 70, 0, 126, 200), // girl's column
+ Common::Rect(126, 0, 182, 200), // middle
+ Common::Rect(182, 0, 235, 200), // boy's column
+};
+
+uint happinessLevel(int x) {
+ for (uint i = 0; i < ARRAYSIZE(kHappyZones); i++) {
+ if (kHappyZones[i].contains(x, 100))
+ return i;
+ }
+ return 4; // past zone 3 â max level
+}
+
+// Lock the framebuffer, masked-blit `p` at (x, y), unlock. The transparent
+// colour is the high byte of `p.flags` per `_Rect_Move_Mask @ 1000:03fc`.
+void blitMaskedToScreen(const Picture &p, int x, int y) {
+ const byte transp = (byte)(p.flags >> 8);
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (!screen)
+ return;
+ for (int row = 0; row < p.surface.h; row++) {
+ const int dstY = y + row;
+ if (dstY < 0 || dstY >= screen->h)
+ continue;
+ const byte *src = (const byte *)p.surface.getBasePtr(0, row);
+ byte *dst = (byte *)screen->getBasePtr(0, dstY);
+ for (int col = 0; col < p.surface.w; col++) {
+ const int dstX = x + col;
+ if (dstX < 0 || dstX >= screen->w)
+ continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ g_system->unlockScreen();
+}
+
// On-screen positions verified from `_NewAnimation` calls @ 1a35:07b9 / 07d5.
const int kBoyX = 0xe2; // 226
const int kBoyY = 0x62; // 98
@@ -85,12 +126,6 @@ void EEMEngine::doChoosePartner() {
// cells contain 10 cells = pairs of "neutral, smile" at increasing
// intensity). Lifted verbatim from the binary so the gestures
// match the original beat-for-beat.
- static const Common::Rect kHappyZones[4] = {
- Common::Rect( 0, 0, 70, 200), // far left â girl very happy, boy neutral
- Common::Rect( 70, 0, 126, 200), // girl's column
- Common::Rect(126, 0, 182, 200), // middle
- Common::Rect(182, 0, 235, 200), // boy's column
- };
static const uint8 kBoySeqs[5][9] = {
{ 0,0,0,0,0,0,0,1,0 }, // level 0
{ 2,2,2,2,2,2,2,3,2 }, // level 1
@@ -105,13 +140,6 @@ void EEMEngine::doChoosePartner() {
{ 2,2,2,2,2,2,3,2,2 },
{ 0,0,0,0,0,1,0,0,0 },
};
- auto happinessLevel = [](int x) {
- for (uint i = 0; i < ARRAYSIZE(kHappyZones); i++) {
- if (kHappyZones[i].contains(x, 100))
- return (uint)i;
- }
- return 4u; // past zone 3 â max level
- };
// `_DoChoosePartner` opens with `_SetMousePos(0xa0, 0x96)` so the
// cursor lands centred between the two partners â start the
@@ -120,15 +148,13 @@ void EEMEngine::doChoosePartner() {
uint level = happinessLevel(curMouseX);
uint seqIdx = 0; // step within the 9-frame seq
- auto draw = [&]() {
- blitAt(background, 0, 0);
- const uint girlFrame = kGirlSeqs[level][seqIdx % 9];
- const uint boyFrame = kBoySeqs [level][seqIdx % 9];
- blitAt(girlAnim[girlFrame % girlAnim.size()], kGirlX, kGirlY);
- blitAt(boyAnim[boyFrame % boyAnim.size()], kBoyX, kBoyY);
- g_system->updateScreen();
- };
- draw();
+ // Initial render â pose 0 of whichever zone the cursor opens in.
+ blitAt(background, 0, 0);
+ blitAt(girlAnim[kGirlSeqs[level][seqIdx % 9] % girlAnim.size()],
+ kGirlX, kGirlY);
+ blitAt(boyAnim [kBoySeqs [level][seqIdx % 9] % boyAnim.size()],
+ kBoyX, kBoyY);
+ g_system->updateScreen();
debugC(1, kDebugGeneral, "ChoosePartner: %u boy frames at (%d,%d), "
"%u girl frames at (%d,%d)",
@@ -145,7 +171,12 @@ void EEMEngine::doChoosePartner() {
if (g_system->getMillis() - lastTick > 100) {
lastTick = g_system->getMillis();
seqIdx = (seqIdx + 1) % 9;
- draw();
+ blitAt(background, 0, 0);
+ blitAt(girlAnim[kGirlSeqs[level][seqIdx % 9] % girlAnim.size()],
+ kGirlX, kGirlY);
+ blitAt(boyAnim [kBoySeqs [level][seqIdx % 9] % boyAnim.size()],
+ kBoyX, kBoyY);
+ g_system->updateScreen();
}
Common::Event ev;
@@ -162,7 +193,12 @@ void EEMEngine::doChoosePartner() {
if (newLevel != level) {
level = newLevel;
seqIdx = 0; // restart cycle so the gesture pops
- draw();
+ blitAt(background, 0, 0);
+ blitAt(girlAnim[kGirlSeqs[level][seqIdx % 9] % girlAnim.size()],
+ kGirlX, kGirlY);
+ blitAt(boyAnim [kBoySeqs [level][seqIdx % 9] % boyAnim.size()],
+ kBoyX, kBoyY);
+ g_system->updateScreen();
}
}
if (ev.type == Common::EVENT_LBUTTONDOWN) {
@@ -239,28 +275,6 @@ void EEMEngine::doInitClues() {
&& _aniArchive.loadAnimation(0x19, nancy)
&& !nancy.empty();
- auto blitMaskedAt = [&](const Picture &p, int x, int y) {
- const byte transp = (byte)(p.flags >> 8);
- Graphics::Surface *screen = g_system->lockScreen();
- if (!screen)
- return;
- for (int row = 0; row < p.surface.h; row++) {
- const int dstY = y + row;
- if (dstY < 0 || dstY >= screen->h)
- continue;
- const byte *src = (const byte *)p.surface.getBasePtr(0, row);
- byte *dst = (byte *)screen->getBasePtr(0, dstY);
- for (int col = 0; col < p.surface.w; col++) {
- const int dstX = x + col;
- if (dstX < 0 || dstX >= screen->w)
- continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
- g_system->unlockScreen();
- };
-
// Step 4 â cycle through the game animation once before the briefing.
// Mirrors the `while (uVar9 != gameNum)` loop. The original calls
// `_UpdateAnimations` per `_CheckFrameRate` tick (~10 fps). We use
@@ -273,11 +287,11 @@ void EEMEngine::doInitClues() {
if (_picsArchive.getPicture(0x52, bg))
blitAt(bg, 0, 0);
if (haveGame)
- blitMaskedAt(game[frame % game.size()], 0xcd, 0x6c);
+ blitMaskedToScreen(game[frame % game.size()], 0xcd, 0x6c);
if (haveBook)
- blitMaskedAt(book[frame % book.size()], 0, 99);
+ blitMaskedToScreen(book[frame % book.size()], 0, 99);
if (haveNancy)
- blitMaskedAt(nancy[frame % nancy.size()], 0x68, 0x8b);
+ blitMaskedToScreen(nancy[frame % nancy.size()], 0x68, 0x8b);
g_system->updateScreen();
// Wait 100 ms or until input.
@@ -301,11 +315,11 @@ void EEMEngine::doInitClues() {
if (_picsArchive.getPicture(0x52, bg))
blitAt(bg, 0, 0);
if (haveGame)
- blitMaskedAt(game[0], 0xcd, 0x6c);
+ blitMaskedToScreen(game[0], 0xcd, 0x6c);
if (haveBook)
- blitMaskedAt(book[0], 0, 99);
+ blitMaskedToScreen(book[0], 0, 99);
if (haveNancy)
- blitMaskedAt(nancy[0], 0x68, 0x8b);
+ blitMaskedToScreen(nancy[0], 0x68, 0x8b);
g_system->updateScreen();
// Step 5 â `_PlayInSequence(animSeq, 0xcd, animY)` per Ghidra:
@@ -363,18 +377,18 @@ void EEMEngine::doInitClues() {
if (_picsArchive.getPicture(0x52, bg))
blitAt(bg, 0, 0);
if (haveGame)
- blitMaskedAt(game[frame % game.size()], 0xcd, 0x6c);
+ blitMaskedToScreen(game[frame % game.size()], 0xcd, 0x6c);
if (haveBook)
- blitMaskedAt(book[frame % book.size()], 0, 99);
+ blitMaskedToScreen(book[frame % book.size()], 0, 99);
if (haveNancy)
- blitMaskedAt(nancy[frame % nancy.size()], 0x68, 0x8b);
+ blitMaskedToScreen(nancy[frame % nancy.size()], 0x68, 0x8b);
// Anchor: original blits at `(sx - frame.width,
// sy - frame.rowoff)`. `frame.rowoff` is the y-anchor
// in our PicData. We use width/height directly since
// loadAnimation places anchor at (0, 0).
const int dstX = (int)0xcd - (int)fr.surface.w;
const int dstY = (int)seqY - (int)fr.rowoff;
- blitMaskedAt(fr, dstX, dstY);
+ blitMaskedToScreen(fr, dstX, dstY);
g_system->updateScreen();
const uint32 wakeup = g_system->getMillis() + 100;
while (g_system->getMillis() < wakeup &&
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 3777e5ac6ca..35bf5100ef4 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -163,6 +163,24 @@ private:
*/
void screenDriver();
+ /// Re-render helpers used by the corresponding `doX()` modal screens.
+ /// Each replaces what would otherwise be a `[&]()` capture-everything
+ /// lambda inside the `doX()` body; called from the screen's redraw
+ /// triggers (input changes, frame ticks). The state these need is
+ /// passed via reference parameters or read off engine members.
+ void drawNotebookFrame(int &page);
+ void drawGalleryFrame(const byte *gd, uint8 numSuspects,
+ Common::Array<Common::Rect> &slotRects,
+ Common::Array<int> &slotSuspect);
+ void drawBigMapOverview();
+ void drawBigMapDetail(int scrollX, int scrollY,
+ const Common::Array<byte> &mapPixels,
+ uint16 mapW, uint16 mapH);
+ void drawAccuseGallery(uint8 numSuspects, const byte *gd,
+ int highlighted,
+ Common::Array<Common::Rect> &slotRects,
+ Common::Array<int> &slotSuspect);
+
/**
* Open the five .DBD/.DBX archive pairs the way _InitGraphicsSystem
* @ 172b:0145 does at boot.
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index f0eb4342801..df3bc56cbd3 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -33,6 +33,58 @@
namespace EEM {
+namespace {
+
+// Masked blit a Picture into a ManagedSurface. Pixels equal to `transp`
+// (the high byte of `pic.flags`, per `_Rect_Move_Mask @ 1000:03fc`) are
+// skipped. Used by `enterSiteAnim` for both skateboard + KD slide-in
+// passes; the surface is the in-memory frame buffer that gets pushed
+// to the screen each tick.
+void blitFrame(Graphics::ManagedSurface &dst, const Picture &p,
+ int x, int y, byte transp) {
+ const int w = p.surface.w;
+ const int h = p.surface.h;
+ for (int row = 0; row < h; row++) {
+ const int dstY = y + row;
+ if (dstY < 0 || dstY >= 200)
+ continue;
+ const byte *src = (const byte *)p.surface.getBasePtr(0, row);
+ byte *out = (byte *)dst.getBasePtr(0, dstY);
+ for (int col = 0; col < w; col++) {
+ const int dstX = x + col;
+ if (dstX < 0 || dstX >= 320)
+ continue;
+ if (src[col] != transp)
+ out[dstX] = src[col];
+ }
+ }
+}
+
+// Rotate one VGA palette range by one slot. Mirrors `_ColorCycle @
+// 172b:2015` â used by both the per-site Loop-1 ColorCycle entries and
+// the always-on hotspot marching-ants range 0xF9..0xFE.
+void cyclePaletteRange(uint8 start, uint8 end) {
+ if (end <= start)
+ return;
+ const uint count = (uint)end - (uint)start + 1;
+ byte buf[256 * 3];
+ g_system->getPaletteManager()->grabPalette(buf, start, count);
+ const byte savedR = buf[0];
+ const byte savedG = buf[1];
+ const byte savedB = buf[2];
+ for (uint i = 0; i + 1 < count; i++) {
+ buf[i * 3 + 0] = buf[(i + 1) * 3 + 0];
+ buf[i * 3 + 1] = buf[(i + 1) * 3 + 1];
+ buf[i * 3 + 2] = buf[(i + 1) * 3 + 2];
+ }
+ buf[(count - 1) * 3 + 0] = savedR;
+ buf[(count - 1) * 3 + 1] = savedG;
+ buf[(count - 1) * 3 + 2] = savedB;
+ g_system->getPaletteManager()->setPalette(buf, start, count);
+}
+
+} // anonymous namespace
+
void SiteScreen::enter(uint siteNum) {
if (!_mystery || !_mystery->isLoaded()) {
warning("SiteScreen::enter: no mystery loaded");
@@ -353,25 +405,6 @@ void SiteScreen::enterSiteAnim() {
}
g_system->unlockScreen();
- auto blitFrame = [](Graphics::ManagedSurface &dst, const Picture &p,
- int x, int y, byte transp) {
- const int w = p.surface.w, h = p.surface.h;
- for (int row = 0; row < h; row++) {
- const int dstY = y + row;
- if (dstY < 0 || dstY >= 200)
- continue;
- const byte *src = (const byte *)p.surface.getBasePtr(0, row);
- byte *out = (byte *)dst.getBasePtr(0, dstY);
- for (int col = 0; col < w; col++) {
- const int dstX = x + col;
- if (dstX < 0 || dstX >= 320)
- continue;
- if (src[col] != transp)
- out[dstX] = src[col];
- }
- }
- };
-
// Phase 1 â skateboard scroll. `_GetAnimation(6 | 0xe)`.
Animation skate;
if (_vm->getAni().loadAnimation(kSkateAni, skate) && !skate.empty()) {
@@ -599,31 +632,11 @@ void SiteScreen::applyColorCycles() {
// We do the same against ScummVM's palette manager. Always rotate
// 0xf9..0xfe for hotspot marching ants (the `_ColorCycle(0xf9,
// 0xfe)` call at the bottom of `_DoSiteLoop`'s main loop).
- auto rotate = [&](uint8 start, uint8 end) {
- if (end <= start)
- return;
- const uint count = (uint)end - (uint)start + 1;
- byte buf[256 * 3];
- g_system->getPaletteManager()->grabPalette(buf, start, count);
- // Save first triplet, shift, restore at end.
- const byte savedR = buf[0];
- const byte savedG = buf[1];
- const byte savedB = buf[2];
- for (uint i = 0; i + 1 < count; i++) {
- buf[i * 3 + 0] = buf[(i + 1) * 3 + 0];
- buf[i * 3 + 1] = buf[(i + 1) * 3 + 1];
- buf[i * 3 + 2] = buf[(i + 1) * 3 + 2];
- }
- buf[(count - 1) * 3 + 0] = savedR;
- buf[(count - 1) * 3 + 1] = savedG;
- buf[(count - 1) * 3 + 2] = savedB;
- g_system->getPaletteManager()->setPalette(buf, start, count);
- };
for (uint i = 0; i < _colorCycles.size(); i++) {
- rotate(_colorCycles[i].start, _colorCycles[i].end);
+ cyclePaletteRange(_colorCycles[i].start, _colorCycles[i].end);
}
// Hotspot marching ants â always cycled.
- rotate(0xF9, 0xFE);
+ cyclePaletteRange(0xF9, 0xFE);
}
void SiteScreen::captureBgSnapshot() {
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 724e88e81ca..2ea76273497 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -40,6 +40,183 @@
namespace EEM {
+namespace {
+
+// Return the next non-empty slot in `slotRects` starting from `from`,
+// stepping by `dir` (+1 or -1) with wraparound. Used by the accuse
+// gallery's keyboard-cycle (TAB / arrow keys) â mirrors the way
+// `_PutMouseInRect` skips eliminated suspects in the original.
+int nextLiveSlot(const Common::Array<Common::Rect> &slotRects,
+ int from, int dir) {
+ const int n = (int)slotRects.size();
+ if (n <= 0)
+ return 0;
+ for (int step = 1; step <= n; step++) {
+ int idx = (from + dir * step) % n;
+ if (idx < 0)
+ idx += n;
+ if (!slotRects[idx].isEmpty())
+ return idx;
+ }
+ return from;
+}
+
+// Snapshot of `doCaseSelection`'s captured locals, used by
+// `drawCaseSelectionFrame` (which replaces the original lambda). Lives
+// on the stack inside `doCaseSelection`; never escapes.
+struct CaseSelectionView {
+ EEMEngine *vm;
+ const Picture *caseBg;
+ bool haveCaseBg;
+ const Animation *kdAnim;
+ bool haveKdAnim;
+ int kdAnimX;
+ int kdAnimY;
+ const char *separator;
+ const char *const *pickLabel;
+ const bool *pickEnabled;
+ uint pick;
+};
+
+// Per-mystery sub-chooser ("Choose A Mystery") view.
+struct CaseSubmenuView {
+ EEMEngine *vm;
+ const Picture *caseBg;
+ bool haveCaseBg;
+ const byte *mysteriesSolved;
+ uint mysteriesSolvedSize;
+ uint sel;
+ uint maxMystery;
+};
+
+void drawCaseSubmenu(const CaseSubmenuView &v) {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ if (v.haveCaseBg) {
+ const int w = MIN<int>(v.caseBg->surface.w, 320);
+ const int h = MIN<int>(v.caseBg->surface.h, 200);
+ for (int row = 0; row < h; row++)
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)v.caseBg->surface.getBasePtr(0, row), w);
+ }
+ if (v.vm->getFont().isLoaded()) {
+ const int kListX = 61;
+ const int kListW = 238 - kListX;
+ const int kListY0 = 35;
+ const int kLineH = 10;
+ const int kVisible = 12;
+ int top = (int)v.sel - kVisible / 2;
+ if (top < 0)
+ top = 0;
+ if (top + kVisible > (int)v.maxMystery + 1)
+ top = (int)v.maxMystery + 1 - kVisible;
+ for (int r = 0; r < kVisible; r++) {
+ const int idx = top + r;
+ if (idx > (int)v.maxMystery)
+ break;
+ char marker = ' ';
+ if ((uint)idx < v.mysteriesSolvedSize) {
+ if (v.mysteriesSolved[idx] == 2)
+ marker = '*';
+ else if (v.mysteriesSolved[idx] == 1)
+ marker = '+';
+ }
+ const char arrow = ((uint)idx == v.sel) ? '>' : ' ';
+ v.vm->getFont().drawString(&scratch,
+ Common::String::format("%c %c Mystery %d", arrow, marker, idx),
+ kListX, kListY0 + r * kLineH, kListW, 0xF);
+ }
+ }
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+}
+
+void drawCaseSelectionFrame(const CaseSelectionView &v) {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ if (v.haveCaseBg) {
+ const int w = MIN<int>(v.caseBg->surface.w, 320);
+ const int h = MIN<int>(v.caseBg->surface.h, 200);
+ for (int row = 0; row < h; row++) {
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)v.caseBg->surface.getBasePtr(0, row), w);
+ }
+ }
+
+ // KD greeter frame â masked-blit current animation cell at
+ // (0x112, 0x50). 100 ms tick matches the engine's `_CheckFrameRate`.
+ if (v.haveKdAnim) {
+ const uint32 now = g_system->getMillis();
+ const uint frameIdx = (uint)((now / 100) % v.kdAnim->size());
+ const Picture &fr = (*v.kdAnim)[frameIdx];
+ const byte transp = (byte)(fr.flags >> 8);
+ for (int row = 0; row < fr.surface.h; row++) {
+ const int dstY = v.kdAnimY + row;
+ if (dstY < 0 || dstY >= 200)
+ continue;
+ const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, dstY);
+ for (int col = 0; col < fr.surface.w; col++) {
+ const int dstX = v.kdAnimX + col;
+ if (dstX < 0 || dstX >= 320)
+ continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ }
+ if (v.vm->getFont().isLoaded()) {
+ // `DrawList` @ 1c33:040d coordinates: `_TextBox + 3` for x
+ // and `DAT_29be_0d02` for y. `_TextBox` @ 29be:0d00 holds
+ // {x=58, y=35, x2=238, y2=158}. Matches the blue panel.
+ const int kListX = 58 + 3;
+ const int kListW = 238 - kListX;
+ const int kListY0 = 35;
+ const int kLineH = 10;
+
+ // Top centred "Book %d" / "Challenge Book" title â sprintf
+ // format strings at 29be:0deb / 29be:0dfa shown via
+ // `_Show_String(0xc, (0xba - width)/2 + 0x3c, â¦)` in the
+ // original. We don't track challenge tier yet so always
+ // show "Book 1".
+ const Common::String book = "Book 1";
+ const int titleW = v.vm->getFont().getStringWidth(book);
+ const int titleX = (0xba - titleW) / 2 + 0x3c;
+ v.vm->getFont().drawString(&scratch, book, titleX, 12, 320, 0xF);
+
+ // Render 11 list rows: separator + menu item pairs.
+ // row 0 separator
+ // row 1 Choose A Mystery
+ // row 2 separator
+ // row 3 Practice Mystery
+ // ...
+ // row 9 See ScrapBook 3
+ // row 10 separator
+ for (int r = 0; r < 11; r++) {
+ const int y = kListY0 + r * kLineH;
+ if ((r & 1) == 0) {
+ v.vm->getFont().drawString(&scratch, v.separator,
+ kListX, y, kListW, 0x7);
+ continue;
+ }
+ const uint mp = (uint)(r >> 1);
+ const bool isSel = (mp == v.pick);
+ const byte color = isSel ? 0xF :
+ v.pickEnabled[mp] ? 0x7 : 0x8;
+ v.vm->getFont().drawString(&scratch, v.pickLabel[mp],
+ kListX, y, kListW, color);
+ }
+ }
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+}
+
+} // anonymous namespace
+
void EEMEngine::doNewPlayer() {
// Mirrors `_NewPlayer` @ 1c33:0dda. The original draws background
// 0x104 + character peek pic 0x107, then shows "Please type your
@@ -58,28 +235,24 @@ void EEMEngine::doNewPlayer() {
Picture bg;
const bool haveBG = _picsArchive.getPicture(0x104, bg);
- auto draw = [&]() {
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
- if (haveBG) {
- const int w = MIN<int>(bg.surface.w, 320);
- const int h = MIN<int>(bg.surface.h, 200);
- for (int row = 0; row < h; row++)
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)bg.surface.getBasePtr(0, row), w);
- }
- // Match the original `_NewPlayer`: `_Show_String(rw=0x28, cl=0x50)`
- // for the prompt, then `_ShowChar(0x50, x, â¦)` for typed input.
- // (rw=row=y, cl=col=x.) Prompt at (y=40, x=80), input at (y=80, x=80).
- _font.drawString(&scratch, "Please type your name:", 80, 40, 240, 0xF);
- Common::String shown = name + "_";
- _font.drawString(&scratch, shown, 80, 80, 240, 0xF);
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
- };
- draw();
+ // Match the original `_NewPlayer`: `_Show_String(rw=0x28, cl=0x50)`
+ // for the prompt, then `_ShowChar(0x50, x, â¦)` for typed input.
+ // (rw=row=y, cl=col=x.) Prompt at (y=40, x=80), input at (y=80, x=80).
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ if (haveBG) {
+ const int w = MIN<int>(bg.surface.w, 320);
+ const int h = MIN<int>(bg.surface.h, 200);
+ for (int row = 0; row < h; row++)
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)bg.surface.getBasePtr(0, row), w);
+ }
+ _font.drawString(&scratch, "Please type your name:", 80, 40, 240, 0xF);
+ _font.drawString(&scratch, name + "_", 80, 80, 240, 0xF);
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
while (!shouldQuit()) {
Common::Event ev;
@@ -114,8 +287,24 @@ void EEMEngine::doNewPlayer() {
dirty = true;
}
}
- if (dirty)
- draw();
+ if (dirty) {
+ // Re-render with the updated `name`. Same body as the
+ // initial render above â only `name + "_"` changes.
+ scratch.clear();
+ if (haveBG) {
+ const int w = MIN<int>(bg.surface.w, 320);
+ const int h = MIN<int>(bg.surface.h, 200);
+ for (int row = 0; row < h; row++)
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)bg.surface.getBasePtr(0, row), w);
+ }
+ _font.drawString(&scratch, "Please type your name:",
+ 80, 40, 240, 0xF);
+ _font.drawString(&scratch, name + "_", 80, 80, 240, 0xF);
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ }
g_system->updateScreen();
g_system->delayMillis(15);
}
@@ -192,102 +381,20 @@ void EEMEngine::doCaseSelection() {
const int kKdAnimX = 0x112;
const int kKdAnimY = 0x50;
- auto draw = [&]() {
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
- if (haveCaseBg) {
- const int w = MIN<int>(caseBg.surface.w, 320);
- const int h = MIN<int>(caseBg.surface.h, 200);
- for (int row = 0; row < h; row++) {
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)caseBg.surface.getBasePtr(0, row), w);
- }
- }
-
- // KD greeter frame â masked-blit current animation cell at
- // (0x112, 0x50). 100 ms tick matches the engine's `_CheckFrameRate`.
- if (haveKdAnim) {
- const uint32 now = g_system->getMillis();
- const uint frameIdx = (uint)((now / 100) % kdAnim.size());
- const Picture &fr = kdAnim[frameIdx];
- const byte transp = (byte)(fr.flags >> 8);
- for (int row = 0; row < fr.surface.h; row++) {
- const int dstY = kKdAnimY + row;
- if (dstY < 0 || dstY >= 200)
- continue;
- const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, dstY);
- for (int col = 0; col < fr.surface.w; col++) {
- const int dstX = kKdAnimX + col;
- if (dstX < 0 || dstX >= 320)
- continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
- }
- if (_font.isLoaded()) {
- // `DrawList` @ 1c33:040d coordinates: `_TextBox + 3` for x
- // and `DAT_29be_0d02` for y. `_TextBox` @ 29be:0d00 holds
- // {x=58, y=35, x2=238, y2=158}. Matches the blue panel.
- const int kListX = 58 + 3;
- const int kListW = 238 - kListX;
- const int kListY0 = 35;
- const int kLineH = 10;
-
- // Top centred "Book %d" / "Challenge Book" title â sprintf
- // format strings at 29be:0deb / 29be:0dfa shown via
- // `_Show_String(0xc, (0xba - width)/2 + 0x3c, â¦)` in the
- // original. We don't track challenge tier yet so always
- // show "Book 1".
- const Common::String book = "Book 1";
- const int titleW = _font.getStringWidth(book);
- const int titleX = (0xba - titleW) / 2 + 0x3c;
- _font.drawString(&scratch, book, titleX, 12, 320, 0xF);
-
- // Render 11 list rows: separator + menu item pairs.
- // row 0 separator
- // row 1 Choose A Mystery
- // row 2 separator
- // row 3 Practice Mystery
- // ...
- // row 9 See ScrapBook 3
- // row 10 separator
- for (int r = 0; r < 11; r++) {
- const int y = kListY0 + r * kLineH;
- if ((r & 1) == 0) {
- _font.drawString(&scratch, kSeparator, kListX, y, kListW, 0x7);
- continue;
- }
- const uint mp = (uint)(r >> 1);
- const bool isSel = (mp == pick);
- const byte color = isSel ? 0xF :
- kPickEnabled[mp] ? 0x7 : 0x8;
- _font.drawString(&scratch, kPickLabel[mp], kListX, y, kListW, color);
- }
- }
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
- };
-
- auto pickPrev = [&]() {
- for (int i = 0; i < (int)kNumPicks; i++) {
- pick = (pick == 0) ? (uint)(kNumPicks - 1) : pick - 1;
- if (kPickEnabled[pick])
- break;
- }
- };
- auto pickNext = [&]() {
- for (int i = 0; i < (int)kNumPicks; i++) {
- pick = (pick + 1) % kNumPicks;
- if (kPickEnabled[pick])
- break;
- }
- };
-
- draw();
+ CaseSelectionView v;
+ v.vm = this;
+ v.caseBg = &caseBg;
+ v.haveCaseBg = haveCaseBg;
+ v.kdAnim = &kdAnim;
+ v.haveKdAnim = haveKdAnim;
+ v.kdAnimX = kKdAnimX;
+ v.kdAnimY = kKdAnimY;
+ v.separator = kSeparator;
+ v.pickLabel = kPickLabel;
+ v.pickEnabled = kPickEnabled;
+ v.pick = pick;
+
+ drawCaseSelectionFrame(v);
uint32 lastTick = g_system->getMillis();
bool exitChosen = false;
@@ -299,7 +406,8 @@ void EEMEngine::doCaseSelection() {
const uint32 now = g_system->getMillis();
if (haveKdAnim && now - lastTick >= 100) {
lastTick = now;
- draw();
+ v.pick = pick;
+ drawCaseSelectionFrame(v);
}
while (g_system->getEventManager()->pollEvent(ev)) {
if (ev.type == Common::EVENT_QUIT ||
@@ -330,7 +438,8 @@ void EEMEngine::doCaseSelection() {
const uint mp = (uint)(row >> 1);
if (mp < kNumPicks && kPickEnabled[mp]) {
pick = mp;
- draw();
+ v.pick = pick;
+ drawCaseSelectionFrame(v);
continue;
}
}
@@ -349,19 +458,34 @@ void EEMEngine::doCaseSelection() {
break;
}
if (k == Common::KEYCODE_UP || k == Common::KEYCODE_LEFT) {
- pickPrev();
- draw();
+ // Cycle backwards through enabled picks (mirrors the
+ // `_DoChoose` arrow handlers @ 1c33:0514). Loop is
+ // bounded by `kNumPicks` so a row of all-disabled picks
+ // can't spin forever.
+ for (int i = 0; i < (int)kNumPicks; i++) {
+ pick = (pick == 0) ? (uint)(kNumPicks - 1) : pick - 1;
+ if (kPickEnabled[pick])
+ break;
+ }
+ v.pick = pick;
+ drawCaseSelectionFrame(v);
continue;
}
if (k == Common::KEYCODE_DOWN || k == Common::KEYCODE_RIGHT ||
k == Common::KEYCODE_TAB) {
- pickNext();
- draw();
+ for (int i = 0; i < (int)kNumPicks; i++) {
+ pick = (pick + 1) % kNumPicks;
+ if (kPickEnabled[pick])
+ break;
+ }
+ v.pick = pick;
+ drawCaseSelectionFrame(v);
continue;
}
}
if (confirmed) {
- draw();
+ v.pick = pick;
+ drawCaseSelectionFrame(v);
break;
}
g_system->updateScreen();
@@ -404,51 +528,16 @@ void EEMEngine::doCaseSelection() {
}
}
- auto drawSubmenu = [&]() {
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
- if (haveCaseBg) {
- const int w = MIN<int>(caseBg.surface.w, 320);
- const int h = MIN<int>(caseBg.surface.h, 200);
- for (int row = 0; row < h; row++)
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)caseBg.surface.getBasePtr(0, row), w);
- }
- if (_font.isLoaded()) {
- const int kListX = 61;
- const int kListW = 238 - kListX;
- const int kListY0 = 35;
- const int kLineH = 10;
- const int kVisible = 12;
- int top = (int)sel - kVisible / 2;
- if (top < 0)
- top = 0;
- if (top + kVisible > (int)kMaxMystery + 1)
- top = (int)kMaxMystery + 1 - kVisible;
- for (int r = 0; r < kVisible; r++) {
- const int idx = top + r;
- if (idx > (int)kMaxMystery)
- break;
- char marker = ' ';
- if ((uint)idx < sizeof(_mysteriesSolved)) {
- if (_mysteriesSolved[idx] == 2)
- marker = '*';
- else if (_mysteriesSolved[idx] == 1)
- marker = '+';
- }
- const char arrow = ((uint)idx == sel) ? '>' : ' ';
- _font.drawString(&scratch,
- Common::String::format("%c %c Mystery %d", arrow, marker, idx),
- kListX, kListY0 + r * kLineH, kListW, 0xF);
- }
- }
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
- };
+ CaseSubmenuView sv;
+ sv.vm = this;
+ sv.caseBg = &caseBg;
+ sv.haveCaseBg = haveCaseBg;
+ sv.mysteriesSolved = _mysteriesSolved;
+ sv.mysteriesSolvedSize = sizeof(_mysteriesSolved);
+ sv.sel = sel;
+ sv.maxMystery = kMaxMystery;
- drawSubmenu();
+ drawCaseSubmenu(sv);
bool confirmed = false;
while (!confirmed && !shouldQuit()) {
Common::Event ev;
@@ -468,12 +557,14 @@ void EEMEngine::doCaseSelection() {
}
if (kUpArrowRect.contains(ev.mouse.x, ev.mouse.y)) {
sel = (sel == 0) ? kMaxMystery : sel - 1;
- drawSubmenu();
+ sv.sel = sel;
+ drawCaseSubmenu(sv);
continue;
}
if (kDnArrowRect.contains(ev.mouse.x, ev.mouse.y)) {
sel = (sel >= kMaxMystery) ? 0 : sel + 1;
- drawSubmenu();
+ sv.sel = sel;
+ drawCaseSubmenu(sv);
continue;
}
if (kListRect.contains(ev.mouse.x, ev.mouse.y)) {
@@ -489,7 +580,8 @@ void EEMEngine::doCaseSelection() {
const int idx = top + row;
if (idx >= 0 && idx <= (int)kMaxMystery) {
sel = (uint)idx;
- drawSubmenu();
+ sv.sel = sel;
+ drawCaseSubmenu(sv);
}
continue;
}
@@ -507,31 +599,36 @@ void EEMEngine::doCaseSelection() {
}
if (k >= Common::KEYCODE_0 && k <= Common::KEYCODE_9) {
sel = (uint)(k - Common::KEYCODE_0);
- drawSubmenu();
+ sv.sel = sel;
+ drawCaseSubmenu(sv);
continue;
}
if (k == Common::KEYCODE_DOWN || k == Common::KEYCODE_TAB) {
sel = (sel >= kMaxMystery) ? 0 : sel + 1;
- drawSubmenu();
+ sv.sel = sel;
+ drawCaseSubmenu(sv);
continue;
}
if (k == Common::KEYCODE_UP) {
sel = (sel == 0) ? kMaxMystery : sel - 1;
- drawSubmenu();
+ sv.sel = sel;
+ drawCaseSubmenu(sv);
continue;
}
if (k == Common::KEYCODE_PAGEDOWN) {
sel = (sel + 10 > kMaxMystery) ? kMaxMystery : sel + 10;
- drawSubmenu();
+ sv.sel = sel;
+ drawCaseSubmenu(sv);
continue;
}
if (k == Common::KEYCODE_PAGEUP) {
sel = (sel < 10) ? 0 : sel - 10;
- drawSubmenu();
+ sv.sel = sel;
+ drawCaseSubmenu(sv);
continue;
}
- if (k == Common::KEYCODE_HOME) { sel = 0; drawSubmenu(); continue; }
- if (k == Common::KEYCODE_END) { sel = kMaxMystery; drawSubmenu(); continue; }
+ if (k == Common::KEYCODE_HOME) { sel = 0; sv.sel = sel; drawCaseSubmenu(sv); continue; }
+ if (k == Common::KEYCODE_END) { sel = kMaxMystery; sv.sel = sel; drawCaseSubmenu(sv); continue; }
}
g_system->updateScreen();
g_system->delayMillis(15);
@@ -585,7 +682,6 @@ void EEMEngine::doNotebook() {
// rect 8 (35,111) â 0x03ed = `_NextScreen = 3` (SITE)
// rect 9 (0,0) â 0x03ed = same as rect 8
// rect 10 (66,79) â 0x03f9 = `_InterfaceHelp(0)` (note-area help)
- const Common::Rect kNotebookRect(78, 12, 288, 152);
const Common::Rect kBtnHelp1 ( 93, 174, 115, 190); // [1] HELP
const Common::Rect kBtnGallery (157, 174, 178, 190); // [2] GALLERY
const Common::Rect kBtnPartner ( 5, 80, 44, 110); // [3] KD HELP
@@ -600,165 +696,9 @@ void EEMEngine::doNotebook() {
int page = 0;
int hoveredNoteSlot = -1;
+ (void)hoveredNoteSlot;
- // Build a list of found-clue indices, identical ordering to the
- // original's iteration through `_CluesFound[]`.
- auto buildFound = [&]() {
- Common::Array<uint> found;
- for (uint i = 0; i < Mystery::kCluesFoundCap; i++)
- if (_mystery._cluesFound[i])
- found.push_back(i);
- return found;
- };
-
- auto draw = [&]() {
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
-
- // PIC 0x3f frame.
- Picture frame;
- if (_picsArchive.getPicture(0x3f, frame)) {
- const int w = MIN<int>(frame.surface.w, 320);
- const int h = MIN<int>(frame.surface.h, 200);
- for (int row = 0; row < h; row++)
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)frame.surface.getBasePtr(0, row), w);
- }
-
- // Partner sprite at (5, 80). Anim 1 for Jake, 0xb (11) for Jenny.
- const uint partnerAnim = (_partner == 0) ? 1 : 0xb;
- Animation partnerAni;
- if (_aniArchive.loadAnimation(partnerAnim, partnerAni) && !partnerAni.empty()) {
- const uint32 now = g_system->getMillis();
- const uint frameIdx = (uint)((now / 100) % partnerAni.size());
- const Picture &fr = partnerAni[frameIdx];
- const byte transp = (byte)(fr.flags >> 8);
- for (int row = 0; row < fr.surface.h; row++) {
- const int dstY = 80 + row;
- if (dstY < 0 || dstY >= 200)
- continue;
- const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, dstY);
- for (int col = 0; col < fr.surface.w; col++) {
- const int dstX = 5 + col;
- if (dstX < 0 || dstX >= 320)
- continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
- }
-
- // Notes â `_DrawNotes` walks `_NoteIndex` for the current page,
- // rendering each found clue's text inside `_NotebookRect` with
- // word-wrap. Selected clues are highlighted (color 0x3c in the
- // original's case-briefing palette).
- const Common::Array<uint> found = buildFound();
- const byte *ni = _mystery.noteIndex();
- const uint16 niCount = _mystery.noteIndexCount();
-
- const int kRectX = kNotebookRect.left;
- const int kRectY = kNotebookRect.top;
- const int kRectW = kNotebookRect.width();
- const int kRectH = kNotebookRect.height();
-
- // Walk forward to the start clue of the current page.
- // Each page renders as many clues as fit in `kRectH`.
- int clueCursor = 0;
- Common::Array<int> pageStarts;
- pageStarts.push_back(0);
- {
- const int lineH = _font.getFontHeight() + 1;
- int y = kRectY;
- while (clueCursor < (int)found.size()) {
- const uint clueId = found[clueCursor];
- Common::String txt;
- if (ni && clueId < niCount) {
- const uint16 textOff = READ_LE_UINT16(ni + clueId * 4);
- txt = parseString(_mystery.textAt(textOff),
- _playerName, _partner);
- }
- // Measure height by wrapping the text without drawing.
- Common::Array<Common::String> wrapped;
- _font.wordWrapText(txt, kRectW, wrapped);
- const int h = (int)wrapped.size() * lineH;
- if (y + h + 7 > kRectY + kRectH) {
- // Page break before this clue.
- y = kRectY;
- pageStarts.push_back(clueCursor);
- }
- y += h + 7;
- clueCursor++;
- }
- if (page >= (int)pageStarts.size())
- page = (int)pageStarts.size() - 1;
- if (page < 0)
- page = 0;
- }
-
- // Track per-slot rectangles so the click handler can map a
- // click in `kNoteArea` back to a clue index.
- Common::Array<Common::Rect> slotRects;
- Common::Array<uint> slotClues;
-
- const int startClue = (page < (int)pageStarts.size())
- ? pageStarts[page] : 0;
- const int endClue = (page + 1 < (int)pageStarts.size())
- ? pageStarts[page + 1] : (int)found.size();
-
- int y = kRectY;
- for (int i = startClue; i < endClue; i++) {
- const uint clueId = found[i];
- Common::String txt;
- if (ni && clueId < niCount) {
- const uint16 textOff = READ_LE_UINT16(ni + clueId * 4);
- txt = parseString(_mystery.textAt(textOff),
- _playerName, _partner);
- }
- if (txt.empty())
- txt = Common::String::format("clue %u", clueId);
- // Per `_DrawNotes @ 161e:01d0`: text uses
- // `_NoteUnselectedColor` (0x5c=cyan) for unselected and 0x3c
- // (light yellow-white) for selected. Both contrast cleanly
- // with the PDA screen's natural blue, so we draw text
- // directly on PIC 0x3f without an extra fill rectangle â
- // matches the original design.
- Common::Array<Common::String> wrapped;
- _font.wordWrapText(txt, kRectW, wrapped);
- const int lineH = _font.getFontHeight() + 1;
- const int h = (int)wrapped.size() * lineH;
- const byte color = _mystery._noteSelected[clueId] ? 0x3C : 0x5C;
- for (uint li = 0; li < wrapped.size(); li++) {
- _font.drawString(&scratch, wrapped[li], kRectX,
- y + (int)li * lineH, kRectW, color);
- }
- slotRects.push_back(Common::Rect(kRectX, y,
- kRectX + kRectW, y + h));
- slotClues.push_back(clueId);
- y += h + 7;
- }
-
- // Page indicator + selected-points counter directly on PIC.
- _font.drawString(&scratch, Common::String::format("p%d/%d",
- page + 1, (int)pageStarts.size()),
- 270, 4, 320, 0x5C);
- _font.drawString(&scratch, Common::String::format("%d pts",
- _mystery.selectedPoints()),
- 270, 14, 320, 0x5C);
- (void)hoveredNoteSlot;
-
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
-
- // Stash slot info on the captures so the click handler below
- // can use it via the closure.
- _notebookSlotRects = slotRects;
- _notebookSlotClues = slotClues;
- };
-
- draw();
+ drawNotebookFrame(page);
uint32 lastDraw = g_system->getMillis();
@@ -867,7 +807,7 @@ void EEMEngine::doNotebook() {
const uint32 now = g_system->getMillis();
// Re-render every 100 ms so the partner sprite cycles frames.
if (dirty || now - lastDraw >= 100) {
- draw();
+ drawNotebookFrame(page);
lastDraw = now;
}
g_system->updateScreen();
@@ -875,6 +815,165 @@ void EEMEngine::doNotebook() {
}
}
+void EEMEngine::drawNotebookFrame(int &page) {
+ // PDA notebook redraw â formerly the `draw` lambda inside `doNotebook`.
+ // Mirrors `_DrawNotes @ 161e:01d0` for the per-page note layout, plus
+ // the partner-sprite blit at (5, 80) (`_NewAnimation` from
+ // `_DoNotebook @ 161e:0500`). Uses `_notebookSlotRects` /
+ // `_notebookSlotClues` to publish the per-page slot layout to the
+ // click handler in `doNotebook`.
+ const Common::Rect kNotebookRect(78, 12, 288, 152);
+
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+
+ // PIC 0x3f frame.
+ Picture frame;
+ if (_picsArchive.getPicture(0x3f, frame)) {
+ const int w = MIN<int>(frame.surface.w, 320);
+ const int h = MIN<int>(frame.surface.h, 200);
+ for (int row = 0; row < h; row++)
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)frame.surface.getBasePtr(0, row), w);
+ }
+
+ // Partner sprite at (5, 80). Anim 1 for Jake, 0xb (11) for Jenny.
+ const uint partnerAnim = (_partner == 0) ? 1 : 0xb;
+ Animation partnerAni;
+ if (_aniArchive.loadAnimation(partnerAnim, partnerAni) && !partnerAni.empty()) {
+ const uint32 now = g_system->getMillis();
+ const uint frameIdx = (uint)((now / 100) % partnerAni.size());
+ const Picture &fr = partnerAni[frameIdx];
+ const byte transp = (byte)(fr.flags >> 8);
+ for (int row = 0; row < fr.surface.h; row++) {
+ const int dstY = 80 + row;
+ if (dstY < 0 || dstY >= 200)
+ continue;
+ const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, dstY);
+ for (int col = 0; col < fr.surface.w; col++) {
+ const int dstX = 5 + col;
+ if (dstX < 0 || dstX >= 320)
+ continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ }
+
+ // Notes â `_DrawNotes` walks `_NoteIndex` for the current page,
+ // rendering each found clue's text inside `_NotebookRect` with
+ // word-wrap. Selected clues are highlighted (color 0x3c in the
+ // original's case-briefing palette).
+ // Build a list of found-clue indices, identical ordering to the
+ // original's iteration through `_CluesFound[]`.
+ Common::Array<uint> found;
+ for (uint i = 0; i < Mystery::kCluesFoundCap; i++) {
+ if (_mystery._cluesFound[i])
+ found.push_back(i);
+ }
+ const byte *ni = _mystery.noteIndex();
+ const uint16 niCount = _mystery.noteIndexCount();
+
+ const int kRectX = kNotebookRect.left;
+ const int kRectY = kNotebookRect.top;
+ const int kRectW = kNotebookRect.width();
+ const int kRectH = kNotebookRect.height();
+
+ // Walk forward to the start clue of the current page.
+ // Each page renders as many clues as fit in `kRectH`.
+ int clueCursor = 0;
+ Common::Array<int> pageStarts;
+ pageStarts.push_back(0);
+ {
+ const int lineH = _font.getFontHeight() + 1;
+ int y = kRectY;
+ while (clueCursor < (int)found.size()) {
+ const uint clueId = found[clueCursor];
+ Common::String txt;
+ if (ni && clueId < niCount) {
+ const uint16 textOff = READ_LE_UINT16(ni + clueId * 4);
+ txt = parseString(_mystery.textAt(textOff),
+ _playerName, _partner);
+ }
+ // Measure height by wrapping the text without drawing.
+ Common::Array<Common::String> wrapped;
+ _font.wordWrapText(txt, kRectW, wrapped);
+ const int h = (int)wrapped.size() * lineH;
+ if (y + h + 7 > kRectY + kRectH) {
+ // Page break before this clue.
+ y = kRectY;
+ pageStarts.push_back(clueCursor);
+ }
+ y += h + 7;
+ clueCursor++;
+ }
+ if (page >= (int)pageStarts.size())
+ page = (int)pageStarts.size() - 1;
+ if (page < 0)
+ page = 0;
+ }
+
+ // Track per-slot rectangles so the click handler can map a
+ // click in `kNoteArea` back to a clue index.
+ Common::Array<Common::Rect> slotRects;
+ Common::Array<uint> slotClues;
+
+ const int startClue = (page < (int)pageStarts.size())
+ ? pageStarts[page] : 0;
+ const int endClue = (page + 1 < (int)pageStarts.size())
+ ? pageStarts[page + 1] : (int)found.size();
+
+ int y = kRectY;
+ for (int i = startClue; i < endClue; i++) {
+ const uint clueId = found[i];
+ Common::String txt;
+ if (ni && clueId < niCount) {
+ const uint16 textOff = READ_LE_UINT16(ni + clueId * 4);
+ txt = parseString(_mystery.textAt(textOff),
+ _playerName, _partner);
+ }
+ if (txt.empty())
+ txt = Common::String::format("clue %u", clueId);
+ // Per `_DrawNotes @ 161e:01d0`: text uses
+ // `_NoteUnselectedColor` (0x5c=cyan) for unselected and 0x3c
+ // (light yellow-white) for selected. Both contrast cleanly
+ // with the PDA screen's natural blue, so we draw text
+ // directly on PIC 0x3f without an extra fill rectangle â
+ // matches the original design.
+ Common::Array<Common::String> wrapped;
+ _font.wordWrapText(txt, kRectW, wrapped);
+ const int lineH = _font.getFontHeight() + 1;
+ const int h = (int)wrapped.size() * lineH;
+ const byte color = _mystery._noteSelected[clueId] ? 0x3C : 0x5C;
+ for (uint li = 0; li < wrapped.size(); li++) {
+ _font.drawString(&scratch, wrapped[li], kRectX,
+ y + (int)li * lineH, kRectW, color);
+ }
+ slotRects.push_back(Common::Rect(kRectX, y,
+ kRectX + kRectW, y + h));
+ slotClues.push_back(clueId);
+ y += h + 7;
+ }
+
+ // Page indicator + selected-points counter directly on PIC.
+ _font.drawString(&scratch, Common::String::format("p%d/%d",
+ page + 1, (int)pageStarts.size()),
+ 270, 4, 320, 0x5C);
+ _font.drawString(&scratch, Common::String::format("%d pts",
+ _mystery.selectedPoints()),
+ 270, 14, 320, 0x5C);
+
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+
+ // Publish slot info to `doNotebook`'s click handler.
+ _notebookSlotRects = slotRects;
+ _notebookSlotClues = slotClues;
+}
+
void EEMEngine::doGallery() {
// Mirrors `_DoGallery @ 158f:065b` and `_DrawGallery @ 158f:0046`.
// Verified directly from the disassembly:
@@ -908,26 +1007,11 @@ void EEMEngine::doGallery() {
CursorMan.showMouse(true);
- struct Slot { int x; int y; };
- static const Slot kGallerySlots[5] = {
- { 83, 14 }, // 0
- { 155, 14 }, // 1
- { 227, 14 }, // 2
- { 119, 90 }, // 3
- { 191, 90 } // 4
- };
-
- // Pre-load static elements once.
+ // Pre-load PIC 0x3f for the MoreInfo backdrop blit further down.
+ // (`drawGalleryFrame` reloads it on its own per-call too.)
Picture galBg;
const bool haveBg = _picsArchive.getPicture(0x3f, galBg);
- // Gallery partner anim â `_DoGallery` calls `_GetAnimation(uVar6)` with
- // uVar6 = 2 (Jake) / 0x10 (Jenny). Different from PDA (1 / 0xb).
- const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
- Animation partnerAni;
- const bool havePartner = _aniArchive.loadAnimation(partnerAnim, partnerAni)
- && !partnerAni.empty();
-
const uint8 num = _mystery.numSuspects();
// Cache slot rects for click hit-testing.
@@ -939,116 +1023,7 @@ void EEMEngine::doGallery() {
slotSuspect[i] = -1;
}
- auto drawFrame = [&]() {
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
-
- if (haveBg) {
- const int bw = MIN<int>(galBg.surface.w, 320);
- const int bh = MIN<int>(galBg.surface.h, 200);
- for (int row = 0; row < bh; row++) {
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)galBg.surface.getBasePtr(0, row), bw);
- }
- }
-
- // Partner sprite frame @ (5, 0x50).
- if (havePartner) {
- const uint32 now = g_system->getMillis();
- const uint frameIdx = (uint)((now / 100) % partnerAni.size());
- const Picture &fr = partnerAni[frameIdx];
- const byte transp = (byte)(fr.flags >> 8);
- const int px = 5, py = 0x50;
- for (int row = 0; row < fr.surface.h; row++) {
- const int dstY = py + row;
- if (dstY < 0 || dstY >= 200)
- continue;
- const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, dstY);
- for (int col = 0; col < fr.surface.w; col++) {
- const int dstX = px + col;
- if (dstX < 0 || dstX >= 320)
- continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
- }
-
- // Portraits â `_DrawGallery @ 158f:0046` walks suspects 0..N-1
- // and only renders those flagged in `_InGallery[NewOrder[i]]`.
- // Undiscovered slots are left empty in the original. We render
- // a darkened placeholder + "?" so the player has visual feedback
- // that suspects exist but are still unknown.
- uint discoveredCount = 0;
- for (uint i = 0; i < num && i < Mystery::kGalleryCap; i++) {
- slotRects[i] = Common::Rect();
- slotSuspect[i] = -1;
-
- const uint8 phys = _mystery._newOrder[i];
- if (phys >= 5)
- continue;
- const Slot &s = kGallerySlots[phys];
-
- const bool discovered = _mystery._inGallery[phys] != 0;
- if (discovered) {
- const uint16 picId = READ_LE_UINT16(gd + i * 0x46);
- Picture portrait;
- if (picId == 0 ||
- !_picsArchive.getPicture(picId, portrait))
- continue;
-
- const int placeX = s.x;
- const int placeY = s.y + (0x48 - portrait.surface.h);
- const byte transp = (byte)(portrait.flags >> 8);
- const int w = MIN<int>(portrait.surface.w, 320 - placeX);
- const int h = MIN<int>(portrait.surface.h, 200 - placeY);
- if (w <= 0 || h <= 0)
- continue;
- for (int row = 0; row < h; row++) {
- const int dstY = placeY + row;
- if (dstY < 0)
- continue;
- const byte *src =
- (const byte *)portrait.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, dstY);
- for (int col = 0; col < w; col++) {
- const int dstX = placeX + col;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
- slotRects[i] = Common::Rect(placeX, placeY,
- placeX + w, placeY + h);
- slotSuspect[i] = (int)i;
- discoveredCount++;
- } else {
- // Undiscovered placeholder â small framed "?" box at
- // (s.x, s.y) sized 0x40 Ã 0x48 (typical portrait size).
- const int phW = 0x40, phH = 0x48;
- const int phX = s.x, phY = s.y;
- if (phX + phW <= 320 && phY + phH <= 200) {
- scratch.fillRect(Common::Rect(phX, phY,
- phX + phW, phY + phH), 0x20);
- scratch.frameRect(Common::Rect(phX, phY,
- phX + phW, phY + phH), 0x5C);
- if (_font.isLoaded()) {
- _font.drawString(&scratch, "?",
- phX + phW / 2 - 3,
- phY + phH / 2 - 4, phW, 0x5C);
- }
- }
- }
- }
- (void)discoveredCount;
-
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
- };
-
- drawFrame();
+ drawGalleryFrame(gd, num, slotRects, slotSuspect);
uint32 lastDraw = g_system->getMillis();
while (!shouldQuit()) {
@@ -1258,7 +1233,7 @@ void EEMEngine::doGallery() {
// Force gallery redraw immediately so the
// player isn't left looking at the dismissed
// MoreInfo screen until the next 100 ms tick.
- drawFrame();
+ drawGalleryFrame(gd, num, slotRects, slotSuspect);
lastDraw = g_system->getMillis();
clicked = true;
break;
@@ -1272,13 +1247,139 @@ void EEMEngine::doGallery() {
const uint32 now = g_system->getMillis();
if (now - lastDraw >= 100) {
- drawFrame();
+ drawGalleryFrame(gd, num, slotRects, slotSuspect);
lastDraw = now;
}
g_system->delayMillis(15);
}
}
+void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
+ Common::Array<Common::Rect> &slotRects,
+ Common::Array<int> &slotSuspect) {
+ // Gallery redraw â formerly the `drawFrame` lambda inside `doGallery`.
+ // Mirrors `_DrawGallery @ 158f:0046`: PIC 0x3f frame + partner sprite
+ // at (5, 0x50) + suspect portraits in their `_NewOrder` slots.
+ struct Slot { int x; int y; };
+ static const Slot kGallerySlots[5] = {
+ { 83, 14 }, // 0
+ { 155, 14 }, // 1
+ { 227, 14 }, // 2
+ { 119, 90 }, // 3
+ { 191, 90 } // 4
+ };
+
+ Picture galBg;
+ const bool haveBg = _picsArchive.getPicture(0x3f, galBg);
+ const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
+ Animation partnerAni;
+ const bool havePartner = _aniArchive.loadAnimation(partnerAnim, partnerAni)
+ && !partnerAni.empty();
+
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+
+ if (haveBg) {
+ const int bw = MIN<int>(galBg.surface.w, 320);
+ const int bh = MIN<int>(galBg.surface.h, 200);
+ for (int row = 0; row < bh; row++) {
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)galBg.surface.getBasePtr(0, row), bw);
+ }
+ }
+
+ // Partner sprite frame @ (5, 0x50).
+ if (havePartner) {
+ const uint32 now = g_system->getMillis();
+ const uint frameIdx = (uint)((now / 100) % partnerAni.size());
+ const Picture &fr = partnerAni[frameIdx];
+ const byte transp = (byte)(fr.flags >> 8);
+ const int px = 5;
+ const int py = 0x50;
+ for (int row = 0; row < fr.surface.h; row++) {
+ const int dstY = py + row;
+ if (dstY < 0 || dstY >= 200)
+ continue;
+ const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, dstY);
+ for (int col = 0; col < fr.surface.w; col++) {
+ const int dstX = px + col;
+ if (dstX < 0 || dstX >= 320)
+ continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ }
+
+ // Portraits â `_DrawGallery @ 158f:0046` walks suspects 0..N-1 and
+ // only renders those flagged in `_InGallery[NewOrder[i]]`.
+ for (uint i = 0; i < numSuspects && i < Mystery::kGalleryCap; i++) {
+ slotRects[i] = Common::Rect();
+ slotSuspect[i] = -1;
+
+ const uint8 phys = _mystery._newOrder[i];
+ if (phys >= 5)
+ continue;
+ const Slot &s = kGallerySlots[phys];
+
+ const bool discovered = _mystery._inGallery[phys] != 0;
+ if (discovered) {
+ const uint16 picId = READ_LE_UINT16(gd + i * 0x46);
+ Picture portrait;
+ if (picId == 0 ||
+ !_picsArchive.getPicture(picId, portrait))
+ continue;
+
+ const int placeX = s.x;
+ const int placeY = s.y + (0x48 - portrait.surface.h);
+ const byte transp = (byte)(portrait.flags >> 8);
+ const int w = MIN<int>(portrait.surface.w, 320 - placeX);
+ const int h = MIN<int>(portrait.surface.h, 200 - placeY);
+ if (w <= 0 || h <= 0)
+ continue;
+ for (int row = 0; row < h; row++) {
+ const int dstY = placeY + row;
+ if (dstY < 0)
+ continue;
+ const byte *src =
+ (const byte *)portrait.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, dstY);
+ for (int col = 0; col < w; col++) {
+ const int dstX = placeX + col;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ slotRects[i] = Common::Rect(placeX, placeY,
+ placeX + w, placeY + h);
+ slotSuspect[i] = (int)i;
+ } else {
+ // Undiscovered placeholder â small framed "?" box.
+ const int phW = 0x40;
+ const int phH = 0x48;
+ const int phX = s.x;
+ const int phY = s.y;
+ if (phX + phW <= 320 && phY + phH <= 200) {
+ scratch.fillRect(Common::Rect(phX, phY,
+ phX + phW, phY + phH), 0x20);
+ scratch.frameRect(Common::Rect(phX, phY,
+ phX + phW, phY + phH), 0x5C);
+ if (_font.isLoaded()) {
+ _font.drawString(&scratch, "?",
+ phX + phW / 2 - 3,
+ phY + phH / 2 - 4, phW, 0x5C);
+ }
+ }
+ }
+ }
+
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+}
+
void EEMEngine::doBigMap() {
// Two-stage flow that mirrors the original screen-1 wrapper at
// 20fe:120b and `_DoBigMap @ 20fe:09e7`:
@@ -1322,128 +1423,7 @@ void EEMEngine::doBigMap() {
// STAGE 1 â Overview: PIC 0x42 + clickable site icons.
// ------------------------------------------------------------------
- // `_DoBigMap @ 20fe:09e7` (20fe:0a44-0a99) registers a partner sprite
- // on the overview frame. The animation depends on `_LastScreen`:
- // * When LastScreen == 2 (came from the site loop) the original
- // plays an entrance anim (`anum-1` for Jake / Jenny) at
- // (0x102, 0x50), then on END swaps to the idle anim at (0xfd,
- // 0x50). We don't track LastScreen finely enough to distinguish,
- // so we render the IDLE pose at (0xfd, 0x50) which is what the
- // player sees the rest of the time anyway.
- // * Idle anim ID: Jake = 0x14 (20), Jenny = 0x12 (18).
- const uint kMapAniId = (_partner == 0) ? 0x14 : 0x12;
- Animation mapAnim;
- const bool haveMapAnim = _aniArchive.loadAnimation(kMapAniId, mapAnim)
- && !mapAnim.empty();
- const int kMapAnimX = 0xfd;
- const int kMapAnimY = 0x50;
-
- auto drawOverview = [&]() {
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
-
- Picture frame;
- if (_picsArchive.getPicture(0x42, frame)) {
- const int w = MIN<int>(frame.surface.w, 320);
- const int h = MIN<int>(frame.surface.h, 200);
- for (int row = 0; row < h; row++)
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)frame.surface.getBasePtr(0, row), w);
- }
-
- // Marker PICs from `_main @ 1a35:0f59`. Three globals are filled
- // once at boot via `_GetPicture` (1-based IDs â entries N-1):
- // _DoneMarker = PIC 0x20d (already-searched site)
- // _SiteMarker = PIC 0xc5 (default available site)
- // _CrimeMarker = PIC 0xc6 (crime-scene flag set)
- // Picked per-site by `_DrawBigMapButtons @ 20fe:0877`:
- // 1. SaveSiteComplete[i] â DoneMarker
- // 2. else MapData[+0xc] != 0 â CrimeMarker
- // 3. else SiteMarker
- Picture done, normal, crimeM;
- const bool haveDone = _picsArchive.getPicture(0x20d, done);
- const bool haveNormal = _picsArchive.getPicture(0xc5, normal);
- const bool haveCrime = _picsArchive.getPicture(0xc6, crimeM);
-
- auto blitMarker = [&](const Picture &m, int x, int y) {
- const byte transp = (byte)(m.flags >> 8);
- for (int row = 0; row < m.surface.h; row++) {
- const int dstY = y + row;
- if (dstY < 0 || dstY >= 200)
- continue;
- const byte *src = (const byte *)m.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, dstY);
- for (int col = 0; col < m.surface.w; col++) {
- const int dstX = x + col;
- if (dstX < 0 || dstX >= 320)
- continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
- };
-
- for (uint i = 0; i < _mystery.numSites(); i++) {
- if (!_mystery._onSites[i] && i != _mystery._siteNumber)
- continue;
- const byte *entry = _mystery.mapEntry(i);
- if (!entry)
- continue;
- const uint16 mx = READ_LE_UINT16(entry + 0x4);
- const uint16 my = READ_LE_UINT16(entry + 0x6);
- const uint16 crime = READ_LE_UINT16(entry + 0xc);
- const bool done_ = (i < Mystery::kVisitedSiteCap)
- && _mystery._visitedSite[i];
-
- const Picture *m = nullptr;
- if (done_ && haveDone)
- m = &done;
- else if (crime != 0 && haveCrime)
- m = &crimeM;
- else if (haveNormal)
- m = &normal;
-
- if (m)
- blitMarker(*m, (int)mx, (int)my);
- else {
- // Fallback if the markers couldn't be loaded.
- const Common::Rect mark(mx - 3, my - 3, mx + 4, my + 4);
- scratch.fillRect(mark, 0x0F);
- }
- }
-
- // Partner sprite â masked-blit at (0xfd, 0x50). Same per-tick
- // idle the original would run via `_UpdateAnimations` once the
- // entrance one-shot transitions out (see `_DoBigMap` 0xae3-0xae7
- // where it swaps animId on the 0x80 marker).
- if (haveMapAnim) {
- const uint32 now = g_system->getMillis();
- const uint frameIdx = (uint)((now / 100) % mapAnim.size());
- const Picture &fr = mapAnim[frameIdx];
- const byte transp = (byte)(fr.flags >> 8);
- for (int row = 0; row < fr.surface.h; row++) {
- const int dstY = kMapAnimY + row;
- if (dstY < 0 || dstY >= 200)
- continue;
- const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, dstY);
- for (int col = 0; col < fr.surface.w; col++) {
- const int dstX = kMapAnimX + col;
- if (dstX < 0 || dstX >= 320)
- continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
- }
-
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
- };
-
- drawOverview();
+ drawBigMapOverview();
uint32 mapLastTick = g_system->getMillis();
// Static rectangles read directly from the binary at the labelled
@@ -1492,9 +1472,9 @@ void EEMEngine::doBigMap() {
// Cycle the partner-sprite frame every 100 ms (matching the
// original's `_CheckFrameRate` cadence inside `_DoBigMap`).
const uint32 now = g_system->getMillis();
- if (haveMapAnim && now - mapLastTick >= 100) {
+ if (now - mapLastTick >= 100) {
mapLastTick = now;
- drawOverview();
+ drawBigMapOverview();
}
g_system->updateScreen();
g_system->delayMillis(10);
@@ -1531,110 +1511,7 @@ void EEMEngine::doBigMap() {
int scrollX = MAX<int>(0, MIN<int>(mapW - kMapWinW, zoomX));
int scrollY = MAX<int>(0, MIN<int>(mapH - kMapWinH, zoomY));
- // `_DoMapScreen @ 20fe:120b` (20fe:12cd-12f0): partner sprite on
- // the detail-zoom screen. Jake = anim 0x13 (19), Jenny = anim 0x11
- // (17). Position (0x101, 0x50) = (257, 80), seqnum 0x13. The cells
- // here have a "looking at the map" pose, distinct from the BigMap
- // overview entrance/idle.
- const uint kDetailAniId = (_partner == 0) ? 0x13 : 0x11;
- Animation detailAnim;
- const bool haveDetailAnim = _aniArchive.loadAnimation(kDetailAniId,
- detailAnim)
- && !detailAnim.empty();
- const int kDetailAnimX = 0x101;
- const int kDetailAnimY = 0x50;
-
- auto drawDetail = [&]() {
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
-
- Picture frame;
- if (_picsArchive.getPicture(0x43, frame)) {
- const int w = MIN<int>(frame.surface.w, 320);
- const int h = MIN<int>(frame.surface.h, 200);
- for (int row = 0; row < h; row++)
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)frame.surface.getBasePtr(0, row), w);
- }
-
- const int copyW = MIN<int>(mapW - scrollX, kMapWinW);
- const int copyH = MIN<int>(mapH - scrollY, kMapWinH);
- for (int row = 0; row < copyH; row++) {
- memcpy((byte *)scratch.getBasePtr(kMapWinX, kMapWinY + row),
- mapPixels.data() + (scrollY + row) * mapW + scrollX,
- copyW);
- }
-
- // Stamped site buttons. `_StampButtons @ 20fe:0d2f` does:
- // button = _GetButton(MapData[+0]) // BUTTON.DBD entry
- // destX = MapData[+8], destY = MapData[+0xa]
- // then bakes the button PIC into the map bitmap. Each button
- // sprite carries the site name baked in. We blit them on top
- // of the BIGMAP.PIC viewport at the same SmallMap coords.
- for (uint i = 0; i < _mystery.numSites(); i++) {
- if (!_mystery._onSites[i] && i != _mystery._siteNumber)
- continue;
- const byte *entry = _mystery.mapEntry(i);
- if (!entry)
- continue;
- const uint16 buttonId = READ_LE_UINT16(entry + 0x0);
- const uint16 mx = READ_LE_UINT16(entry + 0x8);
- const uint16 my = READ_LE_UINT16(entry + 0xa);
-
- Picture button;
- if (!_buttonArchive.loadEntry(buttonId, button))
- continue;
- const int sx = (int)mx - scrollX + kMapWinX;
- const int sy = (int)my - scrollY + kMapWinY;
- const byte transp = (byte)(button.flags >> 8);
-
- // Crop blit against the viewport.
- const int x0 = MAX<int>(sx, kMapWinX);
- const int y0 = MAX<int>(sy, kMapWinY);
- const int x1 = MIN<int>(sx + button.surface.w, kMapWinX + kMapWinW);
- const int y1 = MIN<int>(sy + button.surface.h, kMapWinY + kMapWinH);
- for (int row = y0; row < y1; row++) {
- const byte *src = (const byte *)button.surface.getBasePtr(0, row - sy);
- byte *dst = (byte *)scratch.getBasePtr(0, row);
- for (int col = x0; col < x1; col++) {
- const byte px = src[col - sx];
- if (px != transp)
- dst[col] = px;
- }
- }
- }
-
- // Partner sprite on the detail map. Drawn last so it sits over
- // the frame and the BIGMAP.PIC viewport.
- if (haveDetailAnim) {
- const uint32 now = g_system->getMillis();
- const uint frameIdx =
- (uint)((now / 100) % detailAnim.size());
- const Picture &fr = detailAnim[frameIdx];
- const byte transp = (byte)(fr.flags >> 8);
- for (int row = 0; row < fr.surface.h; row++) {
- const int dstY = kDetailAnimY + row;
- if (dstY < 0 || dstY >= 200)
- continue;
- const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, dstY);
- for (int col = 0; col < fr.surface.w; col++) {
- const int dstX = kDetailAnimX + col;
- if (dstX < 0 || dstX >= 320)
- continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
- }
-
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
- };
-
- drawDetail();
+ drawBigMapDetail(scrollX, scrollY, mapPixels, mapW, mapH);
uint32 detailLastTick = g_system->getMillis();
while (!shouldQuit()) {
@@ -1761,17 +1638,231 @@ void EEMEngine::doBigMap() {
// Cycle the partner sprite at 100 ms ticks (same cadence as
// `_DoMapScreen`'s `_CheckFrameRate` + `_UpdateAnimations` loop).
const uint32 now = g_system->getMillis();
- if (haveDetailAnim && now - detailLastTick >= 100) {
+ if (now - detailLastTick >= 100) {
detailLastTick = now;
dirty = true;
}
if (dirty)
- drawDetail();
+ drawBigMapDetail(scrollX, scrollY, mapPixels, mapW, mapH);
g_system->updateScreen();
g_system->delayMillis(10);
}
}
+void EEMEngine::drawBigMapOverview() {
+ // Map-overview redraw â formerly the `drawOverview` lambda inside
+ // `doBigMap`. PIC 0x42 frame + per-site marker (Done / Crime / Site
+ // per `_DrawBigMapButtons @ 20fe:0877`) + the partner idle sprite.
+ // `_DoBigMap @ 20fe:09e7` (`_NewAnimation` block at 20fe:0a44-0a99)
+ // registers the partner: when `_LastScreen == 2` it plays an
+ // entrance one-shot at (0x102, 0x50) and on END swaps to the idle
+ // at (0xfd, 0x50). We don't track LastScreen finely enough so we
+ // always render the IDLE pose at (0xfd, 0x50). Idle anim ID:
+ // Jake = 0x14 (20), Jenny = 0x12 (18).
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+
+ Picture frame;
+ if (_picsArchive.getPicture(0x42, frame)) {
+ const int w = MIN<int>(frame.surface.w, 320);
+ const int h = MIN<int>(frame.surface.h, 200);
+ for (int row = 0; row < h; row++)
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)frame.surface.getBasePtr(0, row), w);
+ }
+
+ // Marker PICs from `_main @ 1a35:0f59`. Three globals are filled
+ // once at boot via `_GetPicture` (1-based IDs):
+ // _DoneMarker = PIC 0x20d (already-searched site)
+ // _SiteMarker = PIC 0xc5 (default available site)
+ // _CrimeMarker = PIC 0xc6 (crime-scene flag set)
+ Picture done;
+ Picture normal;
+ Picture crimeM;
+ const bool haveDone = _picsArchive.getPicture(0x20d, done);
+ const bool haveNormal = _picsArchive.getPicture(0xc5, normal);
+ const bool haveCrime = _picsArchive.getPicture(0xc6, crimeM);
+
+ for (uint i = 0; i < _mystery.numSites(); i++) {
+ if (!_mystery._onSites[i] && i != _mystery._siteNumber)
+ continue;
+ const byte *entry = _mystery.mapEntry(i);
+ if (!entry)
+ continue;
+ const uint16 mx = READ_LE_UINT16(entry + 0x4);
+ const uint16 my = READ_LE_UINT16(entry + 0x6);
+ const uint16 crime = READ_LE_UINT16(entry + 0xc);
+ const bool done_ = (i < Mystery::kVisitedSiteCap)
+ && _mystery._visitedSite[i];
+
+ const Picture *m = nullptr;
+ if (done_ && haveDone)
+ m = &done;
+ else if (crime != 0 && haveCrime)
+ m = &crimeM;
+ else if (haveNormal)
+ m = &normal;
+
+ if (m) {
+ // Masked-blit the marker PIC.
+ const byte transp = (byte)(m->flags >> 8);
+ for (int row = 0; row < m->surface.h; row++) {
+ const int dstY = (int)my + row;
+ if (dstY < 0 || dstY >= 200)
+ continue;
+ const byte *src = (const byte *)m->surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, dstY);
+ for (int col = 0; col < m->surface.w; col++) {
+ const int dstX = (int)mx + col;
+ if (dstX < 0 || dstX >= 320)
+ continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ } else {
+ // Fallback if the markers couldn't be loaded.
+ const Common::Rect mark(mx - 3, my - 3, mx + 4, my + 4);
+ scratch.fillRect(mark, 0x0F);
+ }
+ }
+
+ // Partner idle sprite at (0xfd, 0x50). Jake = anim 0x14, Jenny = 0x12.
+ const uint kMapAniId = (_partner == 0) ? 0x14 : 0x12;
+ Animation mapAnim;
+ if (_aniArchive.loadAnimation(kMapAniId, mapAnim) && !mapAnim.empty()) {
+ const uint32 now = g_system->getMillis();
+ const uint frameIdx = (uint)((now / 100) % mapAnim.size());
+ const Picture &fr = mapAnim[frameIdx];
+ const byte transp = (byte)(fr.flags >> 8);
+ const int kMapAnimX = 0xfd;
+ const int kMapAnimY = 0x50;
+ for (int row = 0; row < fr.surface.h; row++) {
+ const int dstY = kMapAnimY + row;
+ if (dstY < 0 || dstY >= 200)
+ continue;
+ const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, dstY);
+ for (int col = 0; col < fr.surface.w; col++) {
+ const int dstX = kMapAnimX + col;
+ if (dstX < 0 || dstX >= 320)
+ continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ }
+
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+}
+
+void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
+ const Common::Array<byte> &mapPixels,
+ uint16 mapW, uint16 mapH) {
+ // Map-detail redraw â formerly the `drawDetail` lambda inside
+ // `doBigMap`. PIC 0x43 frame + a 0xe9 Ã 0xab BIGMAP.PIC viewport at
+ // (2, 2), stamped site buttons, and the partner sprite at (0x101,
+ // 0x50) â `_DoMapScreen @ 20fe:120b` (`_NewAnimation` at
+ // 20fe:12cd-12f0, anim 0x13 Jake / 0x11 Jenny, seqnum 0x13).
+ const int kMapWinW = 0xe9;
+ const int kMapWinH = 0xab;
+ const int kMapWinX = 2;
+ const int kMapWinY = 2;
+
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+
+ Picture frame;
+ if (_picsArchive.getPicture(0x43, frame)) {
+ const int w = MIN<int>(frame.surface.w, 320);
+ const int h = MIN<int>(frame.surface.h, 200);
+ for (int row = 0; row < h; row++)
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)frame.surface.getBasePtr(0, row), w);
+ }
+
+ const int copyW = MIN<int>(mapW - scrollX, kMapWinW);
+ const int copyH = MIN<int>(mapH - scrollY, kMapWinH);
+ for (int row = 0; row < copyH; row++) {
+ memcpy((byte *)scratch.getBasePtr(kMapWinX, kMapWinY + row),
+ mapPixels.data() + (scrollY + row) * mapW + scrollX,
+ copyW);
+ }
+
+ // Stamped site buttons. `_StampButtons @ 20fe:0d2f`:
+ // button = _GetButton(MapData[+0])
+ // destX = MapData[+8]
+ // destY = MapData[+0xa]
+ for (uint i = 0; i < _mystery.numSites(); i++) {
+ if (!_mystery._onSites[i] && i != _mystery._siteNumber)
+ continue;
+ const byte *entry = _mystery.mapEntry(i);
+ if (!entry)
+ continue;
+ const uint16 buttonId = READ_LE_UINT16(entry + 0x0);
+ const uint16 mx = READ_LE_UINT16(entry + 0x8);
+ const uint16 my = READ_LE_UINT16(entry + 0xa);
+
+ Picture button;
+ if (!_buttonArchive.loadEntry(buttonId, button))
+ continue;
+ const int sx = (int)mx - scrollX + kMapWinX;
+ const int sy = (int)my - scrollY + kMapWinY;
+ const byte transp = (byte)(button.flags >> 8);
+
+ // Crop blit against the viewport.
+ const int x0 = MAX<int>(sx, kMapWinX);
+ const int y0 = MAX<int>(sy, kMapWinY);
+ const int x1 = MIN<int>(sx + button.surface.w, kMapWinX + kMapWinW);
+ const int y1 = MIN<int>(sy + button.surface.h, kMapWinY + kMapWinH);
+ for (int row = y0; row < y1; row++) {
+ const byte *src = (const byte *)button.surface.getBasePtr(0, row - sy);
+ byte *dst = (byte *)scratch.getBasePtr(0, row);
+ for (int col = x0; col < x1; col++) {
+ const byte px = src[col - sx];
+ if (px != transp)
+ dst[col] = px;
+ }
+ }
+ }
+
+ // Partner sprite on the detail map (drawn last to sit over the
+ // frame and the BIGMAP.PIC viewport).
+ const uint kDetailAniId = (_partner == 0) ? 0x13 : 0x11;
+ Animation detailAnim;
+ if (_aniArchive.loadAnimation(kDetailAniId, detailAnim) &&
+ !detailAnim.empty()) {
+ const uint32 now = g_system->getMillis();
+ const uint frameIdx = (uint)((now / 100) % detailAnim.size());
+ const Picture &fr = detailAnim[frameIdx];
+ const byte transp = (byte)(fr.flags >> 8);
+ const int kDetailAnimX = 0x101;
+ const int kDetailAnimY = 0x50;
+ for (int row = 0; row < fr.surface.h; row++) {
+ const int dstY = kDetailAnimY + row;
+ if (dstY < 0 || dstY >= 200)
+ continue;
+ const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, dstY);
+ for (int col = 0; col < fr.surface.w; col++) {
+ const int dstX = kDetailAnimX + col;
+ if (dstX < 0 || dstX >= 320)
+ continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ }
+
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+}
+
uint16 EEMEngine::getKDTextBalloon(byte firstChar) const {
// Mirrors `_GetKDTextBalloon @ 1df2:0105`:
// if ((ctype[firstChar] & 2) == 0) bub = *(u16*)29be:1068 = 0x17
@@ -1812,12 +1903,6 @@ void EEMEngine::doAccuse() {
// Verbatim from 29be:0x116 â same five suspect slot positions as
// `_DrawGallery @ 158f:0046`.
- struct Slot { int x; int y; };
- static const Slot kGallerySlots[5] = {
- { 83, 14 }, { 155, 14 }, { 227, 14 },
- { 119, 90 }, { 191, 90 }
- };
-
Picture accuseBg;
const bool haveAccuseBg = _picsArchive.getPicture(0x3f, accuseBg);
@@ -1829,84 +1914,6 @@ void EEMEngine::doAccuse() {
slotSuspect[i] = -1;
int highlighted = 0;
- auto drawGallery = [&]() {
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
- if (haveAccuseBg) {
- const int bw = MIN<int>(accuseBg.surface.w, 320);
- const int bh = MIN<int>(accuseBg.surface.h, 200);
- for (int row = 0; row < bh; row++) {
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)accuseBg.surface.getBasePtr(0, row), bw);
- }
- }
-
- for (uint i = 0; i < num && i < Mystery::kGalleryCap; i++) {
- slotRects[i] = Common::Rect();
- slotSuspect[i] = -1;
- if (!gd)
- continue;
- const uint8 phys = _mystery._newOrder[i];
- if (phys >= 5)
- continue;
- // `_DrawGallery @ 158f:00b9` skips suspects whose
- // `_InGallery[phys]` flag is 0 â that's the original gate
- // (some suspects only become visible after being met or
- // stay hidden after a wrong accusation removes them).
- if (_mystery._inGallery[phys] == 0)
- continue;
- const Slot &s = kGallerySlots[phys];
-
- const uint16 picId = READ_LE_UINT16(gd + i * 0x46);
- if (picId == 0)
- continue;
- Picture portrait;
- if (!_picsArchive.getPicture(picId, portrait))
- continue;
-
- const int placeX = s.x;
- const int placeY = s.y + (0x48 - portrait.surface.h);
- const byte transp = (byte)(portrait.flags >> 8);
- const int w = MIN<int>(portrait.surface.w, 320 - placeX);
- const int h = MIN<int>(portrait.surface.h, 200 - placeY);
- if (w <= 0 || h <= 0)
- continue;
- for (int row = 0; row < h; row++) {
- const int dstY = placeY + row;
- if (dstY < 0)
- continue;
- const byte *src =
- (const byte *)portrait.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, dstY);
- for (int col = 0; col < w; col++) {
- const int dstX = placeX + col;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
- slotRects[i] = Common::Rect(placeX, placeY,
- placeX + w, placeY + h);
- slotSuspect[i] = (int)i;
- }
-
- // Highlight indicator. The original moves the mouse cursor
- // to the centre of the highlighted suspect via `_PutMouseInRect`
- // (1df2:0b8e) â we draw a 1px outline in palette index 0xFE
- // (within the marching-ants cycle range 0xF9..0xFE) which is
- // unambiguously visible under any palette without warping the
- // player's cursor.
- if (highlighted >= 0 && highlighted < (int)slotRects.size() &&
- !slotRects[highlighted].isEmpty()) {
- Common::Rect r = slotRects[highlighted];
- r.grow(1);
- scratch.frameRect(r, 0xFE);
- }
-
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
- };
// Step 1 â KD hint balloon. Mirrors `_DoAccuseGallery @ 1df2:0a31`
// (1df2:0a4c-1df2:0afe):
@@ -1991,23 +1998,10 @@ void EEMEngine::doAccuse() {
// Helper to find the next "alive" slot (one whose `_inGallery[phys]`
// flag is still set so a portrait was actually drawn). Mirrors the
// way the original wraps DI past empty slots.
- auto nextLiveSlot = [&](int from, int dir) -> int {
- const int n = (int)slotRects.size();
- if (n <= 0)
- return 0;
- for (int step = 1; step <= n; step++) {
- int idx = (from + dir * step) % n;
- if (idx < 0)
- idx += n;
- if (!slotRects[idx].isEmpty())
- return idx;
- }
- return from;
- };
if (slotRects[highlighted].isEmpty())
- highlighted = nextLiveSlot(highlighted, +1);
+ highlighted = nextLiveSlot(slotRects, highlighted, +1);
- drawGallery();
+ drawAccuseGallery(num, gd, highlighted, slotRects, slotSuspect);
// Wait-for-pick loop. Mirrors `_DoAccuseGallery` 1df2:0b26-1df2:0bc8:
// * `_CheckFrameRate` + `_UpdateAnimations` per tick (1df2:0b2a-0b33)
@@ -2038,14 +2032,14 @@ void EEMEngine::doAccuse() {
return;
case Common::KEYCODE_TAB:
case Common::KEYCODE_RIGHT:
- highlighted = nextLiveSlot(highlighted, +1);
+ highlighted = nextLiveSlot(slotRects, highlighted, +1);
dirty = true;
break;
case Common::KEYCODE_LEFT:
// 1df2:0b94 increments DI for LEFT too â but a
// keyboard-driven UX is friendlier with separate
// directions, so we mirror Right=+1 / Left=-1.
- highlighted = nextLiveSlot(highlighted, -1);
+ highlighted = nextLiveSlot(slotRects, highlighted, -1);
dirty = true;
break;
case Common::KEYCODE_RETURN:
@@ -2085,7 +2079,7 @@ void EEMEngine::doAccuse() {
// We still re-render whenever the highlight moves (`dirty`).
const uint32 now = g_system->getMillis();
if (dirty || now - lastTick >= 100) {
- drawGallery();
+ drawAccuseGallery(num, gd, highlighted, slotRects, slotSuspect);
lastTick = now;
dirty = false;
}
@@ -2265,4 +2259,96 @@ void EEMEngine::doAccuse() {
}
}
+void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
+ int highlighted,
+ Common::Array<Common::Rect> &slotRects,
+ Common::Array<int> &slotSuspect) {
+ // Accuse-gallery redraw â formerly the `drawGallery` lambda inside
+ // `doAccuse`. Mirrors `_DoAccuseGallery @ 1df2:0a31` portrait grid:
+ // PIC 0x3f backdrop, suspect portraits at the 5 fixed slots, and a
+ // 1-px outline (palette index 0xFE) around the highlighted slot.
+ struct Slot { int x; int y; };
+ static const Slot kGallerySlots[5] = {
+ { 83, 14 }, { 155, 14 }, { 227, 14 },
+ { 119, 90 }, { 191, 90 }
+ };
+
+ Picture accuseBg;
+ const bool haveAccuseBg = _picsArchive.getPicture(0x3f, accuseBg);
+
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ if (haveAccuseBg) {
+ const int bw = MIN<int>(accuseBg.surface.w, 320);
+ const int bh = MIN<int>(accuseBg.surface.h, 200);
+ for (int row = 0; row < bh; row++) {
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)accuseBg.surface.getBasePtr(0, row), bw);
+ }
+ }
+
+ for (uint i = 0; i < numSuspects && i < Mystery::kGalleryCap; i++) {
+ slotRects[i] = Common::Rect();
+ slotSuspect[i] = -1;
+ if (!gd)
+ continue;
+ const uint8 phys = _mystery._newOrder[i];
+ if (phys >= 5)
+ continue;
+ // `_DrawGallery @ 158f:00b9` skips suspects whose
+ // `_InGallery[phys]` flag is 0 â that's the original gate.
+ if (_mystery._inGallery[phys] == 0)
+ continue;
+ const Slot &s = kGallerySlots[phys];
+
+ const uint16 picId = READ_LE_UINT16(gd + i * 0x46);
+ if (picId == 0)
+ continue;
+ Picture portrait;
+ if (!_picsArchive.getPicture(picId, portrait))
+ continue;
+
+ const int placeX = s.x;
+ const int placeY = s.y + (0x48 - portrait.surface.h);
+ const byte transp = (byte)(portrait.flags >> 8);
+ const int w = MIN<int>(portrait.surface.w, 320 - placeX);
+ const int h = MIN<int>(portrait.surface.h, 200 - placeY);
+ if (w <= 0 || h <= 0)
+ continue;
+ for (int row = 0; row < h; row++) {
+ const int dstY = placeY + row;
+ if (dstY < 0)
+ continue;
+ const byte *src =
+ (const byte *)portrait.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scratch.getBasePtr(0, dstY);
+ for (int col = 0; col < w; col++) {
+ const int dstX = placeX + col;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+ slotRects[i] = Common::Rect(placeX, placeY,
+ placeX + w, placeY + h);
+ slotSuspect[i] = (int)i;
+ }
+
+ // Highlight indicator. The original moves the mouse cursor to the
+ // centre of the highlighted suspect via `_PutMouseInRect` (1df2:0b8e);
+ // we draw a 1-px outline in palette index 0xFE instead, which sits
+ // inside the marching-ants cycle range 0xF9..0xFE and is visible
+ // under any palette without warping the player's cursor.
+ if (highlighted >= 0 && highlighted < (int)slotRects.size() &&
+ !slotRects[highlighted].isEmpty()) {
+ Common::Rect r = slotRects[highlighted];
+ r.grow(1);
+ scratch.frameRect(r, 0xFE);
+ }
+
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+}
+
} // End of namespace EEM
Commit: 2fac0e6820d5d8b739c839f9d3e3718455c254f3
https://github.com/scummvm/scummvm/commit/2fac0e6820d5d8b739c839f9d3e3718455c254f3
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:41+02:00
Commit Message:
EEM: removed static and use only EEM namespace
Changed paths:
engines/eem/clues.cpp
engines/eem/detection.cpp
engines/eem/eem.cpp
engines/eem/font.cpp
engines/eem/graphics.cpp
engines/eem/resource.cpp
engines/eem/site.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index da14c8990ca..e5fc4df255c 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -40,8 +40,9 @@
namespace EEM {
-namespace {
// Picture / animation IDs verified against `_DoChoosePartner @ 1a35:0756`.
+// `const` at namespace scope already implies internal linkage in C++,
+// so no `static` needed.
const uint kPicChooseBackground = 0x8c; ///< `_GetBackground(0x8c)`
const uint kAniBoy = 8; ///< `_GetAnimation(8)` (Jake)
const uint kAniGirl = 9; ///< `_GetAnimation(9)` (Jenny)
@@ -55,6 +56,32 @@ const Common::Rect kHappyZones[4] = {
Common::Rect(182, 0, 235, 200), // boy's column
};
+// On-screen positions verified from `_NewAnimation` calls @ 1a35:07b9 / 07d5.
+const int kBoyX = 0xe2; // 226
+const int kBoyY = 0x62; // 98
+const int kGirlX = 0x42; // 66
+const int kGirlY = 0x60; // 96
+
+// `_DoHappiness @ 172b:27b5`: each cursor zone swaps the partner's
+// sequence script to a more / less "happy" cycle. Boy seqs lifted
+// verbatim from `29be:0337` (5 Ã 0x14 bytes), girl seqs from
+// `29be:039b`. Both cycle through 9 frames (the boy/girl anim cells
+// contain 10 cells = pairs of "neutral, smile" at increasing intensity).
+const uint8 kBoySeqs[5][9] = {
+ { 0,0,0,0,0,0,0,1,0 }, // level 0
+ { 2,2,2,2,2,2,2,3,2 }, // level 1
+ { 4,4,4,4,4,4,4,5,4 }, // level 2
+ { 6,6,6,6,6,6,7,6,6 }, // level 3
+ { 8,8,8,8,8,8,8,8,9 }, // level 4 (cursor past zone 3)
+};
+const uint8 kGirlSeqs[5][9] = {
+ { 8,9,8,8,8,8,8,8,8 },
+ { 6,6,6,7,6,6,6,6,6 },
+ { 4,4,5,4,4,4,4,4,4 },
+ { 2,2,2,2,2,2,3,2,2 },
+ { 0,0,0,0,0,1,0,0,0 },
+};
+
uint happinessLevel(int x) {
for (uint i = 0; i < ARRAYSIZE(kHappyZones); i++) {
if (kHappyZones[i].contains(x, 100))
@@ -87,13 +114,6 @@ void blitMaskedToScreen(const Picture &p, int x, int y) {
g_system->unlockScreen();
}
-// On-screen positions verified from `_NewAnimation` calls @ 1a35:07b9 / 07d5.
-const int kBoyX = 0xe2; // 226
-const int kBoyY = 0x62; // 98
-const int kGirlX = 0x42; // 66
-const int kGirlY = 0x60; // 96
-} // anonymous namespace
-
void EEMEngine::doChoosePartner() {
// Mirrors _DoChoosePartner @ 1a35:0756. The original places boy + girl
// animations on a backdrop and polls four click rectangles (two per
@@ -119,27 +139,9 @@ void EEMEngine::doChoosePartner() {
setAnmPalette(Common::Path("TITLE.ANM"));
// `_DoHappiness @ 172b:27b5`: the cursor's X column picks one of 4
- // rects (29be:030f, all full-height); past rect 3 â "level 4".
- // Each level swaps the partner's sequence script to a more / less
- // "happy" cycle. Boy seqs at 29be:0337 (5 Ã 0x14 bytes), girl seqs
- // at 29be:039b. Both cycle through 9 frames (the boy/girl anim
- // cells contain 10 cells = pairs of "neutral, smile" at increasing
- // intensity). Lifted verbatim from the binary so the gestures
- // match the original beat-for-beat.
- static const uint8 kBoySeqs[5][9] = {
- { 0,0,0,0,0,0,0,1,0 }, // level 0
- { 2,2,2,2,2,2,2,3,2 }, // level 1
- { 4,4,4,4,4,4,4,5,4 }, // level 2
- { 6,6,6,6,6,6,7,6,6 }, // level 3
- { 8,8,8,8,8,8,8,8,9 }, // level 4 (cursor past zone 3)
- };
- static const uint8 kGirlSeqs[5][9] = {
- { 8,9,8,8,8,8,8,8,8 },
- { 6,6,6,7,6,6,6,6,6 },
- { 4,4,5,4,4,4,4,4,4 },
- { 2,2,2,2,2,2,3,2,2 },
- { 0,0,0,0,0,1,0,0,0 },
- };
+ // rects (29be:030f, all full-height); past rect 3 â "level 4". The
+ // per-zone sequence scripts (`kBoySeqs` / `kGirlSeqs`) live at file
+ // scope above so the gestures match the original beat-for-beat.
// `_DoChoosePartner` opens with `_SetMousePos(0xa0, 0x96)` so the
// cursor lands centred between the two partners â start the
@@ -650,39 +652,19 @@ void EEMEngine::displayClue(const byte *clueBlock) {
}
}
}
- // Per-balloon metadata table verified from 29be:0875 â
- // 10-byte entries indexed by `(bubNum & 0x7f)`. Layout:
- // +0..1 textX inset, +2..3 textY inset, +4..5 width,
- // +6..7 height, +8..9 tail offset.
- // 52 entries total; insets vary (3, 5, 6, or 8 px).
- // The original `_DisplayClue` does:
- // _WordWrap(bubX + table[bubNum].x, bubY + table[bubNum].y,
- // table[bubNum].w, ...);
- static const struct { uint16 x, y, w; } kBalloonTable[] = {
- { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 },
- { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 },
- { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 },
- { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 },
- { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 },
- { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 },
- { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 },
- { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 },
- { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 },
- { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 },
- { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 },
- { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 },
- { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 },
- { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 },
- { 5, 8, 158 }, { 5, 8, 176 }, { 8, 7, 142 }
- };
- const uint kBalloonTableSize = sizeof(kBalloonTable) /
- sizeof(kBalloonTable[0]);
- const uint balloonIdx = balloonId < kBalloonTableSize
- ? balloonId : 0;
- const auto &bm = kBalloonTable[balloonIdx];
- textX = bubX + bm.x;
- textY = bubY + bm.y;
- textW = bm.w;
+ // Per-balloon metadata from `29be:0875` (52 Ã 10 bytes,
+ // indexed by `bubNum & 0x7F`). The original `_DisplayClue`
+ // does `_WordWrap(bubX + table[bub].x, bubY + table[bub].y,
+ // table[bub].w, ...)`. `getBalloonInsets` is the shared
+ // accessor (defined in `graphics.cpp`); fall back to the
+ // (5, 4, 155) entry-23 inset if the lookup fails.
+ uint16 bx = 5;
+ uint16 by = 4;
+ uint16 bw_ = 155;
+ getBalloonInsets(balloonId, bx, by, bw_);
+ textX = bubX + bx;
+ textY = bubY + by;
+ textW = bw_;
copyH = bh;
} else {
// No balloon â clear a band so old pixels don't bleed.
diff --git a/engines/eem/detection.cpp b/engines/eem/detection.cpp
index 0666827a3db..27371f0c29e 100644
--- a/engines/eem/detection.cpp
+++ b/engines/eem/detection.cpp
@@ -26,7 +26,7 @@
namespace EEM {
-static const PlainGameDescriptor eemGames[] = {
+const PlainGameDescriptor eemGames[] = {
{ "eem", "Eagle Eye Mysteries" },
{ nullptr, nullptr }
};
@@ -46,7 +46,7 @@ const ADGameDescription gameDescriptions[] = {
AD_TABLE_END_MARKER
};
-static const DebugChannelDef debugFlagList[] = {
+const DebugChannelDef debugFlagList[] = {
{ kDebugGeneral, "general", "General debug" },
{ kDebugScript, "script", "Script execution" },
{ kDebugMystery, "mystery", "Mystery loading and state" },
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index b33fb699ae1..86ddd0a9096 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -45,7 +45,6 @@
namespace EEM {
-namespace {
const uint kPalSize = 768; ///< 256 colors * 3 bytes
const uint kNumSitePals = 40; ///< SITEPALS holds 40 palettes (40 * 768 = 30720)
@@ -87,7 +86,6 @@ const byte kCursorPalette[] = {
0x00, 0x00, 0x00, // 1 â outline
0xFF, 0xFF, 0xFF // 2 â fill
};
-} // anonymous namespace
EEMEngine::EEMEngine(OSystem *syst, const ADGameDescription *gameDesc)
: Engine(syst), _gameDescription(gameDesc), _rng("eem"),
diff --git a/engines/eem/font.cpp b/engines/eem/font.cpp
index 19f40c1b77e..4d00ab79fb1 100644
--- a/engines/eem/font.cpp
+++ b/engines/eem/font.cpp
@@ -42,7 +42,7 @@ namespace EEM {
// and 'a' to the lowercase glyph (so the original engine renders all
// text in lowercase). We route uppercase ASCII letters to the uppercase
// glyph slots (33..58) for proper mixed-case rendering.
-static const byte kCharToGlyph[128] = {
+const byte kCharToGlyph[128] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
@@ -61,7 +61,7 @@ static const byte kCharToGlyph[128] = {
0x52, 0x53, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00 // 0x78..0x7F 'x','y','z'..
};
-static inline byte mapChar(uint32 c) {
+inline byte mapChar(uint32 c) {
return c < 128 ? kCharToGlyph[c] : 0;
}
diff --git a/engines/eem/graphics.cpp b/engines/eem/graphics.cpp
index 857dcb80ecc..3955914eb98 100644
--- a/engines/eem/graphics.cpp
+++ b/engines/eem/graphics.cpp
@@ -39,6 +39,42 @@
namespace EEM {
+// `_InterfaceHelp(num)` @ 1560:0205 reads `HelpData @ 29be:00c8` (5-byte
+// entries: u8 count, then up to 2 u16 picIds). Verified bytes:
+// entry 0 (PDA / gallery HELP button): count=2, picIds = 0x0063, 0x01ae
+// entry 1: count=2, picIds = 0x0192, 0x01b1
+// Only entry 0 is reachable from the PDA notebook (rect 1) and the
+// gallery (rect 1) â both call `_InterfaceHelp(0)`.
+const uint16 kHelpPics[][2] = {
+ { 0x0063, 0x01ae },
+ { 0x0192, 0x01b1 },
+};
+
+// 52-entry, 10-bytes-each balloon-metadata table at `29be:0875`.
+// Used at 1df2:0aef-0af9 (accuse hint) and `_DisplayClue` to position
+// `_WordWrap` text inside the balloon. Only +0/+2/+4 are read by
+// `getBalloonInsets`:
+// +0..1 = text X inset, +2..3 = Y inset, +4..5 = max wrap width
+// (+6/+8 = balloon h / tail offset, both unused for text layout).
+struct BalloonInsets { uint16 x; uint16 y; uint16 w; };
+const BalloonInsets kBalloonInsetTable[] = {
+ { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 },
+ { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 },
+ { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 },
+ { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 },
+ { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 },
+ { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 },
+ { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 },
+ { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 },
+ { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 },
+ { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 },
+ { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 },
+ { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 },
+ { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 },
+ { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 },
+ { 5, 8, 158 }, { 5, 8, 176 }, { 8, 7, 142 }
+};
+
void EEMEngine::doHelp() {
// `_KDHelp` reads two hint TextBlock offsets from `_KDTextIndex`:
// word @ +0xe : first-time hint
@@ -82,15 +118,8 @@ void EEMEngine::doInterfaceHelp(uint num) {
// `_Rect_Move_Mask(0, 0, ...)`, and waits for click / key. ESC ends
// the cycle; any other input advances to the next pic.
//
- // Verified from Ghidra HelpData bytes:
- // entry 0 (PDA / gallery HELP button): count=2, picIds = 0x0063, 0x01ae
- // entry 1: count=2, picIds = 0x0192, 0x01b1
- // Only entry 0 is reachable from the PDA notebook (rect 1) and the
- // gallery (rect 1) â both call `_InterfaceHelp(0)`.
- static const uint16 kHelpPics[][2] = {
- { 0x0063, 0x01ae },
- { 0x0192, 0x01b1 },
- };
+ // `kHelpPics` lives at file scope above; see comment there for the
+ // HelpData decoding.
if (num >= ARRAYSIZE(kHelpPics))
return;
@@ -175,34 +204,13 @@ void EEMEngine::setPartnerEraseBg(const Graphics::ManagedSurface *bg) {
bool EEMEngine::getBalloonInsets(uint16 bubNum, uint16 &xInset,
uint16 &yInset, uint16 &textW) const {
- // 52-entry, 10-bytes-each balloon-metadata table at `29be:0875`.
- // Used at 1df2:0aef-0af9 (accuse hint) and `_DisplayClue` to position
- // `_WordWrap` text inside the balloon. Only +0/+2/+4 are read here:
- // +0..1 = text X inset, +2..3 = Y inset, +4..5 = max wrap width
- // (+6/+8 = balloon h / tail offset, both unused for text layout).
- static const struct { uint16 x, y, w; } kTable[] = {
- { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 },
- { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 },
- { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 },
- { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 },
- { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 },
- { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 },
- { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 },
- { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 },
- { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 },
- { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 },
- { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 },
- { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 },
- { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 },
- { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 },
- { 5, 8, 158 }, { 5, 8, 176 }, { 8, 7, 142 }
- };
+ // `kBalloonInsetTable` lives at file scope above; see comment there.
const uint idx = bubNum & 0x7F;
- if (idx >= ARRAYSIZE(kTable))
+ if (idx >= ARRAYSIZE(kBalloonInsetTable))
return false;
- xInset = kTable[idx].x;
- yInset = kTable[idx].y;
- textW = kTable[idx].w;
+ xInset = kBalloonInsetTable[idx].x;
+ yInset = kBalloonInsetTable[idx].y;
+ textW = kBalloonInsetTable[idx].w;
return true;
}
diff --git a/engines/eem/resource.cpp b/engines/eem/resource.cpp
index 15fc9981d46..91886e837fb 100644
--- a/engines/eem/resource.cpp
+++ b/engines/eem/resource.cpp
@@ -80,7 +80,7 @@ void DBDArchive::close() {
* Read one 12-byte frame header + payload at the current stream position.
* Shared between picture and animation loaders since the layout is the same.
*/
-static bool readFrame(Common::SeekableReadStream &stream, bool compressed, Picture &out) {
+bool readFrame(Common::SeekableReadStream &stream, bool compressed, Picture &out) {
out.flags = stream.readUint16LE();
const uint16 height = stream.readUint16LE();
const uint16 width = stream.readUint16LE();
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index df3bc56cbd3..4b641dc441c 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -33,8 +33,6 @@
namespace EEM {
-namespace {
-
// Masked blit a Picture into a ManagedSurface. Pixels equal to `transp`
// (the high byte of `pic.flags`, per `_Rect_Move_Mask @ 1000:03fc`) are
// skipped. Used by `enterSiteAnim` for both skateboard + KD slide-in
@@ -60,6 +58,31 @@ void blitFrame(Graphics::ManagedSurface &dst, const Picture &p,
}
}
+// Mask-aware blit from a Picture into a `Graphics::Surface` (the
+// locked framebuffer). Same pixel-mask semantics as `blitFrame`.
+// Used by hotspot/NPC rendering inside `SiteScreen::renderHotspots`
+// and `renderStaticDrops`.
+void blitMaskedSurface(Graphics::Surface *screen, const Picture &p,
+ int x, int y) {
+ if (!screen)
+ return;
+ const byte transp = (byte)(p.flags >> 8);
+ for (int row = 0; row < p.surface.h; row++) {
+ const int dstY = y + row;
+ if (dstY < 0 || dstY >= screen->h)
+ continue;
+ const byte *src = (const byte *)p.surface.getBasePtr(0, row);
+ byte *dst = (byte *)screen->getBasePtr(0, dstY);
+ for (int col = 0; col < p.surface.w; col++) {
+ const int dstX = x + col;
+ if (dstX < 0 || dstX >= screen->w)
+ continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+}
+
// Rotate one VGA palette range by one slot. Mirrors `_ColorCycle @
// 172b:2015` â used by both the per-site Loop-1 ColorCycle entries and
// the always-on hotspot marching-ants range 0xF9..0xFE.
@@ -83,7 +106,68 @@ void cyclePaletteRange(uint8 start, uint8 end) {
g_system->getPaletteManager()->setPalette(buf, start, count);
}
-} // anonymous namespace
+// Per-speaker partner-position table verified against `_WaitAnims @
+// 29be:021c`. 12 bytes per entry, indexed by `siteData[+8]`. Layout:
+// +0..1 anim Jake, +2..3 anim Jenny,
+// +4..5 x Jake, +6..7 x Jenny,
+// +8..9 y Jake, +10..11 y Jenny.
+// Seven valid entries â anything past entry 6 in the binary is
+// `_SiteButtons` rect data that follows the table in memory.
+const uint16 kWaitAnims[7][6] = {
+ { 0x00, 0x0a, 0x06, 0x06, 0x50, 0x50 }, // 0
+ { 0x03, 0x0c, 0x06, 0x06, 0x50, 0x50 }, // 1
+ { 0x01, 0x0b, 0x06, 0x06, 0x50, 0x50 }, // 2
+ { 0x04, 0x0d, 0x06, 0x06, 0x50, 0x50 }, // 3
+ { 0x02, 0x10, 0x06, 0x06, 0x50, 0x50 }, // 4
+ { 0x05, 0x05, 0x06, 0x06, 0x50, 0x50 }, // 5
+ { 0x06, 0x06, 0x06, 0x06, 0x50, 0x50 }, // 6
+};
+
+// `_DoKDAnim` lookup table. Six valid kdAnimNum entries (0..5)
+// verified from `29be:0228`. Layout per entry: { animJake, animJenny,
+// xJake, xJenny, yJake, yJenny }. Position is (6, 80) in every entry.
+const uint16 kKdAnimTable[6][6] = {
+ { 0x03, 0x0c, 6, 6, 80, 80 }, // 0 â speaker idx 1 wait anim
+ { 0x01, 0x0b, 6, 6, 80, 80 }, // 1 â same as PDA idle
+ { 0x04, 0x0d, 6, 6, 80, 80 }, // 2
+ { 0x02, 0x10, 6, 6, 80, 80 }, // 3 â same as gallery
+ { 0x05, 0x05, 6, 6, 80, 80 }, // 4 â same anim both partners
+ { 0x06, 0x06, 6, 6, 80, 80 }, // 5 â same anim both partners
+};
+
+// Sequence-script lookup. Entries copied verbatim from
+// `_AnimationSequences @ 29be:22d4` walked through to the next 0x80.
+// Each script is a u16[] of frame indices terminated by 0x80; we
+// don't yet handle 0x81 jumps (none of the kdAnim sequences use
+// them â verified). seqnum == animId for these calls (per
+// `_PlayAnimation` 172b:1f5d push order).
+struct KdScript {
+ uint16 seqnum;
+ uint8 len;
+ uint8 frames[20]; // long enough for any kdAnim script
+};
+const KdScript kKdScripts[] = {
+ // seqnum 1 (29be:188a) â head bob
+ { 0x01, 15, { 0,1,2,0,1,0,2,1,0,1,0,1,2,1,0 } },
+ // seqnum 2 (29be:18aa) â short blip then long pause
+ { 0x02, 16, { 0,1,2,1,0,0,0,0,0,0,0,0,0,0,0,0 } },
+ // seqnum 3 (29be:18e0) â Jake "lift, hold, lower" gesture
+ { 0x03, 9, { 0,1,2,3,2,2,2,1,0 } },
+ // seqnum 4 (29be:18f4) â bigger gesture (camera flash-style)
+ { 0x04, 13, { 0,1,2,3,4,5,4,4,4,3,2,1,0 } },
+ // seqnum 5 (29be:1910) â held idle with a single peak
+ { 0x05, 13, { 0,0,0,1,2,3,2,1,0,0,0,0,0 } },
+ // seqnum 6 (29be:192c) â empty (immediate END)
+ { 0x06, 0, { 0 } },
+ // seqnum 0xb (29be:188a, same as 1) â Jenny PDA idle
+ { 0x0b, 15, { 0,1,2,0,1,0,2,1,0,1,0,1,2,1,0 } },
+ // seqnum 0xc (29be:18e0, same as 3) â Jenny "take a picture"
+ { 0x0c, 9, { 0,1,2,3,2,2,2,1,0 } },
+ // seqnum 0xd (29be:18f4, same as 4) â Jenny big gesture
+ { 0x0d, 13, { 0,1,2,3,4,5,4,4,4,3,2,1,0 } },
+ // seqnum 0x10 (29be:1956) â Jenny short anim
+ { 0x10, 9, { 0,0,0,1,0,0,0,0,0 } },
+};
void SiteScreen::enter(uint siteNum) {
if (!_mystery || !_mystery->isLoaded()) {
@@ -490,28 +574,6 @@ void SiteScreen::enterSiteAnim() {
}
}
-// Mask-aware blit from a Picture into a Graphics::Surface.
-static void blitMaskedSurface(Graphics::Surface *screen,
- const Picture &p, int x, int y) {
- if (!screen)
- return;
- const byte transp = (byte)(p.flags >> 8);
- for (int row = 0; row < p.surface.h; row++) {
- const int dstY = y + row;
- if (dstY < 0 || dstY >= screen->h)
- continue;
- const byte *src = (const byte *)p.surface.getBasePtr(0, row);
- byte *dst = (byte *)screen->getBasePtr(0, dstY);
- for (int col = 0; col < p.surface.w; col++) {
- const int dstX = x + col;
- if (dstX < 0 || dstX >= screen->w)
- continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
-}
-
void SiteScreen::renderStaticDrops(uint siteNum) {
// Loop 2 from `_DoSiteLoop @ 168d:03f4`:
// bound: siteData[+0x4] (verified at 168d:05c0:
@@ -671,21 +733,9 @@ void SiteScreen::renderPartner(uint siteNum, uint32 tickMs) {
// +0..1 anim Jake, +2..3 anim Jenny,
// +4..5 x Jake, +6..7 x Jenny,
// +8..9 y Jake, +10..11 y Jenny.
- // Seven valid entries verified against the bytes at 29be:021c.
- // Anything past entry 6 in the binary is `_SiteButtons` rect data
- // that follows the table in memory â NOT continuation entries â
- // so we cap the table here and skip rendering for siteData[+8] >= 7
- // (which would indicate corrupt mystery data anyway).
- static const uint16 kWaitAnims[7][6] = {
- { 0x00, 0x0a, 0x06, 0x06, 0x50, 0x50 }, // 0
- { 0x03, 0x0c, 0x06, 0x06, 0x50, 0x50 }, // 1
- { 0x01, 0x0b, 0x06, 0x06, 0x50, 0x50 }, // 2
- { 0x04, 0x0d, 0x06, 0x06, 0x50, 0x50 }, // 3
- { 0x02, 0x10, 0x06, 0x06, 0x50, 0x50 }, // 4
- { 0x05, 0x05, 0x06, 0x06, 0x50, 0x50 }, // 5
- { 0x06, 0x06, 0x06, 0x06, 0x50, 0x50 }, // 6
- };
-
+ // `kWaitAnims` lives at file scope above; we cap rendering at
+ // `speaker < 7` since anything past entry 6 is the `_SiteButtons`
+ // rect data that follows the table in the binary.
const byte *site = _mystery->siteData(siteNum);
if (!site)
return;
@@ -954,17 +1004,7 @@ void EEMEngine::playKdAnim(uint16 num) {
// gesture (Jenny taking a picture, etc.) finishes before the
// speaker portrait + speech balloon appear.
//
- // Six valid kdAnimNum entries (0..5). Verified bytes from
- // `29be:0228`. Layout per entry: { animJake, animJenny, xJake,
- // xJenny, yJake, yJenny }. Position is (6, 80) in every entry.
- static const uint16 kKdAnimTable[6][6] = {
- { 0x03, 0x0c, 6, 6, 80, 80 }, // 0 â speaker idx 1 wait anim
- { 0x01, 0x0b, 6, 6, 80, 80 }, // 1 â same as PDA idle
- { 0x04, 0x0d, 6, 6, 80, 80 }, // 2
- { 0x02, 0x10, 6, 6, 80, 80 }, // 3 â same as gallery
- { 0x05, 0x05, 6, 6, 80, 80 }, // 4 â same anim both partners
- { 0x06, 0x06, 6, 6, 80, 80 }, // 5 â same anim both partners
- };
+ // `kKdAnimTable` and `kKdScripts` live at file scope above.
if (num >= ARRAYSIZE(kKdAnimTable))
return;
@@ -979,45 +1019,12 @@ void EEMEngine::playKdAnim(uint16 num) {
return;
}
- // Sequence-script lookup. Entries copied verbatim from
- // `_AnimationSequences @ 29be:22d4` walked through to the next 0x80.
- // Each script is a u16[] of frame indices terminated by 0x80; we
- // don't yet handle 0x81 jumps (none of the kdAnim sequences use
- // them â verified). seqnum == animId for these calls (per
- // `_PlayAnimation` 172b:1f5d push order).
- struct Script {
- uint16 seqnum;
- uint8 len;
- uint8 frames[20]; // long enough for any kdAnim script
- };
- static const Script kScripts[] = {
- // seqnum 1 (29be:188a) â head bob
- { 0x01, 15, { 0,1,2,0,1,0,2,1,0,1,0,1,2,1,0 } },
- // seqnum 2 (29be:18aa) â short blip then long pause
- { 0x02, 16, { 0,1,2,1,0,0,0,0,0,0,0,0,0,0,0,0 } },
- // seqnum 3 (29be:18e0) â Jake "lift, hold, lower" gesture
- { 0x03, 9, { 0,1,2,3,2,2,2,1,0 } },
- // seqnum 4 (29be:18f4) â bigger gesture (camera flash-style)
- { 0x04, 13, { 0,1,2,3,4,5,4,4,4,3,2,1,0 } },
- // seqnum 5 (29be:1910) â held idle with a single peak
- { 0x05, 13, { 0,0,0,1,2,3,2,1,0,0,0,0,0 } },
- // seqnum 6 (29be:192c) â empty (immediate END)
- { 0x06, 0, { 0 } },
- // seqnum 0xb (29be:188a, same as 1) â Jenny PDA idle
- { 0x0b, 15, { 0,1,2,0,1,0,2,1,0,1,0,1,2,1,0 } },
- // seqnum 0xc (29be:18e0, same as 3) â Jenny "take a picture"
- { 0x0c, 9, { 0,1,2,3,2,2,2,1,0 } },
- // seqnum 0xd (29be:18f4, same as 4) â Jenny big gesture
- { 0x0d, 13, { 0,1,2,3,4,5,4,4,4,3,2,1,0 } },
- // seqnum 0x10 (29be:1956) â Jenny short anim
- { 0x10, 9, { 0,0,0,1,0,0,0,0,0 } },
- };
const uint8 *frames = nullptr;
uint frameCount = 0;
- for (uint i = 0; i < ARRAYSIZE(kScripts); i++) {
- if (kScripts[i].seqnum == animId) {
- frames = kScripts[i].frames;
- frameCount = kScripts[i].len;
+ for (uint i = 0; i < ARRAYSIZE(kKdScripts); i++) {
+ if (kKdScripts[i].seqnum == animId) {
+ frames = kKdScripts[i].frames;
+ frameCount = kKdScripts[i].len;
break;
}
}
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 2ea76273497..7c3d181afdd 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -40,7 +40,24 @@
namespace EEM {
-namespace {
+// Five fixed gallery slot positions verified at `29be:0x116`. Used by
+// both `_DrawGallery @ 158f:0046` (notebook gallery) and the accuse
+// portrait grid; the layout is identical so we share the table.
+struct GallerySlot { int x; int y; };
+const GallerySlot kGallerySlots[5] = {
+ { 83, 14 }, // 0
+ { 155, 14 }, // 1
+ { 227, 14 }, // 2
+ { 119, 90 }, // 3
+ { 191, 90 } // 4
+};
+
+// `_GetKDTextBalloon @ 1df2:0105` digit-balloon table @ `29be:1064`:
+// '0'â0x15 '1'â0x16 '2'â0x17 '3'â0x18 '4'â0x19
+// '5'â0x1a '6'â0x20 '7'â0x21 '8'â0x22 '9'â0x1e
+const uint16 kDigitBalloons[10] = {
+ 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x20, 0x21, 0x22, 0x1e
+};
// Return the next non-empty slot in `slotRects` starting from `from`,
// stepping by `dir` (+1 or -1) with wraparound. Used by the accuse
@@ -215,8 +232,6 @@ void drawCaseSelectionFrame(const CaseSelectionView &v) {
g_system->updateScreen();
}
-} // anonymous namespace
-
void EEMEngine::doNewPlayer() {
// Mirrors `_NewPlayer` @ 1c33:0dda. The original draws background
// 0x104 + character peek pic 0x107, then shows "Please type your
@@ -1259,16 +1274,8 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
Common::Array<int> &slotSuspect) {
// Gallery redraw â formerly the `drawFrame` lambda inside `doGallery`.
// Mirrors `_DrawGallery @ 158f:0046`: PIC 0x3f frame + partner sprite
- // at (5, 0x50) + suspect portraits in their `_NewOrder` slots.
- struct Slot { int x; int y; };
- static const Slot kGallerySlots[5] = {
- { 83, 14 }, // 0
- { 155, 14 }, // 1
- { 227, 14 }, // 2
- { 119, 90 }, // 3
- { 191, 90 } // 4
- };
-
+ // at (5, 0x50) + suspect portraits in their `_NewOrder` slots. Slot
+ // positions live in `kGallerySlots` in this file's anon namespace.
Picture galBg;
const bool haveBg = _picsArchive.getPicture(0x3f, galBg);
const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
@@ -1322,7 +1329,7 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
const uint8 phys = _mystery._newOrder[i];
if (phys >= 5)
continue;
- const Slot &s = kGallerySlots[phys];
+ const GallerySlot &s = kGallerySlots[phys];
const bool discovered = _mystery._inGallery[phys] != 0;
if (discovered) {
@@ -1878,9 +1885,7 @@ uint16 EEMEngine::getKDTextBalloon(byte firstChar) const {
// reusing the digit-2 slot.
if (firstChar < '0' || firstChar > '9')
return 0x17;
- static const uint16 kDigitBalloons[10] = {
- 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x20, 0x21, 0x22, 0x1e
- };
+ // `kDigitBalloons` lives at file scope above.
return kDigitBalloons[firstChar - '0'];
}
@@ -2265,14 +2270,9 @@ void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
Common::Array<int> &slotSuspect) {
// Accuse-gallery redraw â formerly the `drawGallery` lambda inside
// `doAccuse`. Mirrors `_DoAccuseGallery @ 1df2:0a31` portrait grid:
- // PIC 0x3f backdrop, suspect portraits at the 5 fixed slots, and a
- // 1-px outline (palette index 0xFE) around the highlighted slot.
- struct Slot { int x; int y; };
- static const Slot kGallerySlots[5] = {
- { 83, 14 }, { 155, 14 }, { 227, 14 },
- { 119, 90 }, { 191, 90 }
- };
-
+ // PIC 0x3f backdrop, suspect portraits at the 5 fixed slots
+ // (`kGallerySlots` in this file's anon namespace), and a 1-px
+ // outline (palette index 0xFE) around the highlighted slot.
Picture accuseBg;
const bool haveAccuseBg = _picsArchive.getPicture(0x3f, accuseBg);
@@ -2300,7 +2300,7 @@ void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
// `_InGallery[phys]` flag is 0 â that's the original gate.
if (_mystery._inGallery[phys] == 0)
continue;
- const Slot &s = kGallerySlots[phys];
+ const GallerySlot &s = kGallerySlots[phys];
const uint16 picId = READ_LE_UINT16(gd + i * 0x46);
if (picId == 0)
Commit: d44daf096d7f970c7ed8f165b5f98edcd9974878
https://github.com/scummvm/scummvm/commit/d44daf096d7f970c7ed8f165b5f98edcd9974878
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:41+02:00
Commit Message:
EEM: implemented midi music
Changed paths:
A engines/eem/music.cpp
A engines/eem/music.h
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/module.mk
engines/eem/site.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 86ddd0a9096..0895b869273 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -35,6 +35,7 @@
#include "eem/detection.h"
#include "eem/eem.h"
+#include "eem/music.h"
#include "eem/site.h"
#include "common/config-manager.h"
@@ -94,6 +95,7 @@ EEMEngine::EEMEngine(OSystem *syst, const ADGameDescription *gameDesc)
}
EEMEngine::~EEMEngine() {
+ delete _music;
}
Common::Error EEMEngine::run() {
@@ -110,6 +112,10 @@ Common::Error EEMEngine::run() {
if (!_font.load(Common::Path("FONT.FNT")))
warning("FONT.FNT failed to load; text will not render");
+ // MIDI music player. Mirrors `_InitMIDI @ 20a2:013a`. Constructed
+ // here (after `initGraphics` so the OSystem's timer/mixer is up).
+ _music = new MusicPlayer();
+
// _InitMouse @ 152d:018b in the original â install our 11x16 arrow,
// using palette index 0 as the transparency key. The cursor is left
// hidden through the opening anims and switched on at NewPlayer /
@@ -156,18 +162,34 @@ Common::Error EEMEngine::run() {
}
}
- // Reproduces _DoOpeningAnims @ 2520:082a (sans audio):
+ // Reproduces _DoOpeningAnims @ 2520:082a:
// EA Kids logo (PIC) -> HighScore Productions logo (PIC) ->
- // Storm Software logo (BOLT.ANM) -> 20 character-intro animations
- // (ANIM01.A .. ANIM20.A) -> TITLE.ANM. Click / any key skips a
- // single clip; ESC skips the rest of the chain (waitForInput /
- // playAnm raise `_skipIntro` so each subsequent step bails out).
+ // Storm Software logo (BOLT.ANM) -> [music starts] -> 20
+ // character-intro animations (ANIM01.A..ANIM20.A) -> [music
+ // restarts] -> TITLE.ANM. Click / any key skips a single clip;
+ // ESC skips the rest of the chain (waitForInput / playAnm raise
+ // `_skipIntro` so each subsequent step bails out).
+ //
+ // Music timing (verified at 2520:0883 and 2520:0918):
+ // - The three logos and `_InitMysterySounds(0x3c)` all run BEFORE
+ // any `_MIDIPlayFile` call â those segments are voice-only.
+ // - Theme starts with `_LoopMIDI = 0x7fff` right before the
+ // ANIM01..ANIM20 loop (2520:0883).
+ // - After the loop the original calls `_CleanMysterySounds` and
+ // then `_MIDIPlayFile("theme.xmi")` again with `_LoopMIDI =
+ // 0xffff` (2520:0918) to restart the theme for TITLE.ANM.
+ // - `_StopMIDI()` runs on keypress at the title screen
+ // (2520:094c).
_skipIntro = false;
showEAKidsLogo();
if (!shouldQuit() && !_skipIntro)
showHighScoreLogo();
if (!shouldQuit() && !_skipIntro)
playAnm(Common::Path("BOLT.ANM"));
+ // Theme begins HERE â after the three silent logos, before the
+ // character-intro reel.
+ if (!shouldQuit() && !_skipIntro && _music)
+ _music->playFile(Common::Path("THEME.XMI"), /*loop=*/true);
for (int i = 1; i <= 20 && !shouldQuit() && !_skipIntro; i++) {
Common::String name = Common::String::format("ANIM%02d.A", i);
playAnm(Common::Path(name));
@@ -176,6 +198,10 @@ Common::Error EEMEngine::run() {
if (!shouldQuit() && !_skipIntro && i != 20)
waitForInput(2000);
}
+ // Restart the theme for TITLE.ANM â matches the second
+ // `_MIDIPlayFile("theme.xmi")` call at 2520:0918.
+ if (!shouldQuit() && !_skipIntro && _music)
+ _music->playFile(Common::Path("THEME.XMI"), /*loop=*/true);
if (!shouldQuit() && !_skipIntro)
playAnm(Common::Path("TITLE.ANM"), 120, /*holdLastFrame=*/true);
_skipIntro = false;
@@ -187,6 +213,12 @@ Common::Error EEMEngine::run() {
// the interactive screens (matches `_MouseCursor = 1` at the tail
// of `_NewPlayer`).
CursorMan.showMouse(true);
+
+ // Stop the title music â the original `_NewPlayer / _DoChoosePartner`
+ // screens have no music until the briefing's `_PlayInSequence` /
+ // per-mystery `_StartTravelMusic` kicks in.
+ if (_music)
+ _music->stop();
if (!shouldQuit())
doNewPlayer();
if (!shouldQuit())
@@ -446,6 +478,30 @@ void EEMEngine::doSiteLoop() {
screen.run();
}
+void EEMEngine::startTravelMusic() {
+ // Mirrors `_StartTravelMusic @ 20a2:0595`:
+ //
+ // for (num = _SiteNumber; num > 4; num -= 5) {}
+ // if (_MIDIAvailable && _MusicEnabled) {
+ // if (_IsMIDIPlaying()) _StopMIDI();
+ // _MIDIPlay(num);
+ // }
+ //
+ // Five travel tracks: MUS00000.XMI .. MUS00004.XMI, picked by
+ // `_SiteNumber % 5`. The original always loops travel music (the
+ // `_LoopMIDI` global isn't reset between site changes).
+ if (!_music || !_mystery.isLoaded())
+ return;
+ const uint num = _mystery._siteNumber % 5;
+ _music->playMus(num, /*loop=*/true);
+}
+
+void EEMEngine::syncSoundSettings() {
+ Engine::syncSoundSettings();
+ if (_music)
+ _music->syncVolume();
+}
+
bool EEMEngine::hasFeature(EngineFeature f) const {
// We support saving any time but loading only at startup (via the
// `--save-slot=N` resume path or a slot picked from the launcher).
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 35bf5100ef4..e2f3e0c2ce4 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -39,6 +39,8 @@
namespace EEM {
+class MusicPlayer;
+
/**
* Screen IDs used by the original ScreenDriver dispatch table at 1a35:0e5e.
* The table holds 14 (id, handler) entries; the loop iterates until it finds
@@ -243,6 +245,21 @@ private:
/// minus the live ANI sequence playback.
void doInitClues();
+public:
+ /// Mirrors `_StartTravelMusic @ 20a2:0595`. Picks `MUS%05d.XMI`
+ /// based on `_mystery._siteNumber % 5` and starts it (looping). The
+ /// site loop calls this each time `enter(siteNum)` runs so the
+ /// music changes as the player travels between sites.
+ void startTravelMusic();
+
+ /// Forwarded from `Engine::syncSoundSettings`. Re-pulls the user's
+ /// `music_volume` slider into the MIDI player's `_masterVolume`,
+ /// otherwise the AdLib output stays at whatever the slider was at
+ /// the moment `_music` was constructed (and the live launcher
+ /// changes to the volume slider have no effect).
+ void syncSoundSettings() override;
+private:
+
Common::String _playerName; ///< Substituted into 0x80 placeholders
/// Per-mystery solved state. 0 = unsolved, 1 = solved, 2 = solved
@@ -284,6 +301,12 @@ private:
/// accuse contexts use their own composed backdrops). See
/// `setPartnerEraseBg`.
Graphics::ManagedSurface _partnerEraseBg;
+
+ /// XMIDI music player. Mirrors the original `MIDI.C` family
+ /// (`_MIDIPlayFile`, `_MIDIPlay`, `_StopMIDI`, `_StartTravelMusic`
+ /// at 20a2:00e2-05c9). Constructed lazily during `run()` once the
+ /// MIDI driver / timer system is up.
+ MusicPlayer *_music = nullptr;
};
} // End of namespace EEM
diff --git a/engines/eem/module.mk b/engines/eem/module.mk
index 5583335ffd2..ce7975e1edc 100644
--- a/engines/eem/module.mk
+++ b/engines/eem/module.mk
@@ -7,6 +7,7 @@ MODULE_OBJS = \
font.o \
graphics.o \
metaengine.o \
+ music.o \
mystery.o \
resource.o \
site.o \
diff --git a/engines/eem/music.cpp b/engines/eem/music.cpp
new file mode 100644
index 00000000000..a1a40adb60e
--- /dev/null
+++ b/engines/eem/music.cpp
@@ -0,0 +1,174 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "audio/midiparser.h"
+#include "audio/miles.h"
+
+#include "common/debug.h"
+#include "common/file.h"
+#include "common/textconsole.h"
+
+#include "eem/detection.h"
+#include "eem/music.h"
+
+namespace EEM {
+
+MusicPlayer::MusicPlayer() {
+ // Mirrors `_InitMIDI @ 20a2:013a` which used `_AIL_register_driver`
+ // to walk the .ADV files (ADLIB.ADV, SBFM.ADV, MT32MPU.ADV, etc.)
+ // and pick a backend. We honour the launcher's "Music driver"
+ // setting and only force AdLib / MT-32 paths through Miles when the
+ // detected device matches.
+ const MidiDriver::DeviceHandle dev =
+ MidiDriver::detectDevice(MDT_MIDI | MDT_ADLIB);
+ const MusicType musicType = MidiDriver::getMusicType(dev);
+
+ switch (musicType) {
+ case MT_ADLIB:
+ // `_MIDIPlayFile` (20a2:024c) opens "SAMPLE.AD" (29be:14d6) and
+ // installs every patch the sequence requests via
+ // `_AIL_install_timbre`. ScummVM's Miles AdLib driver does the
+ // same: it reads SAMPLE.AD on construction and serves timbres
+ // out of that bank, which is what makes the notes match the
+ // 1993 release. SAMPLE.OPL would be the OPL3 variant; the game
+ // only ships SAMPLE.AD, so an empty path falls back to OPL2.
+ _milesAudioMode = true;
+ _driver = Audio::MidiDriver_Miles_AdLib_create(
+ Common::Path("SAMPLE.AD"), Common::Path());
+ break;
+ case MT_MT32:
+ // `MT32MPU.ADV` was the original MT-32 driver; ScummVM has no
+ // Miles MT-32 instrument bank for EEM, so we use the standard
+ // MT-32 driver and let the XMIDI's own program changes drive
+ // the patch selection.
+ _milesAudioMode = true;
+ _driver = Audio::MidiDriver_Miles_MT32_create(Common::Path());
+ break;
+ default:
+ _milesAudioMode = false;
+ createDriver(MDT_MIDI | MDT_ADLIB);
+ break;
+ }
+
+ if (_driver) {
+ const int ret = _driver->open();
+ if (ret != 0) {
+ warning("MusicPlayer: MidiDriver::open() failed (%d)", ret);
+ delete _driver;
+ _driver = nullptr;
+ } else {
+ // No GM/MT-32 reset for AdLib (Miles AdLib handles its own
+ // state); for MT-32/GM the original would've sent its own
+ // initialisation patches via `_AIL_install_timbre`, but we
+ // don't have those banks for non-AdLib devices.
+ if (musicType != MT_ADLIB) {
+ if (musicType == MT_MT32 || _nativeMT32)
+ _driver->sendMT32Reset();
+ else
+ _driver->sendGMReset();
+ }
+ _driver->setTimerCallback(this, &timerCallback);
+ }
+ } else {
+ debugC(1, kDebugSound, "MusicPlayer: no MIDI driver â music disabled");
+ }
+}
+
+void MusicPlayer::send(uint32 b) {
+ // Miles drivers (both AdLib and MT-32) implement their own per-
+ // source-channel mixing and timbre installation, so just forward
+ // the raw event. Going through `MidiPlayer::send` would re-wrap
+ // CC 7 against `_masterVolume` AND remap the source channel via
+ // `sendToChannel`/`allocateChannel`, both of which the Miles driver
+ // already handles internally (and break the timbre selection if
+ // double-applied).
+ if (_milesAudioMode) {
+ _driver->send(b);
+ return;
+ }
+ Audio::MidiPlayer::send(b);
+}
+
+void MusicPlayer::playFile(const Common::Path &xmiPath, bool loop) {
+ if (!_driver)
+ return;
+
+ Common::StackLock lock(_mutex);
+ stop();
+
+ // Mirrors `_MIDIPlayFile`'s `_fopen` + `_fread` (20a2:024c-029e).
+ Common::File f;
+ if (!f.open(xmiPath)) {
+ warning("MusicPlayer: %s missing", xmiPath.toString().c_str());
+ return;
+ }
+ const uint32 size = f.size();
+ if (size == 0) {
+ warning("MusicPlayer: %s is empty", xmiPath.toString().c_str());
+ return;
+ }
+ _xmiData.resize(size);
+ if (f.read(_xmiData.data(), size) != size) {
+ warning("MusicPlayer: short read on %s",
+ xmiPath.toString().c_str());
+ _xmiData.clear();
+ return;
+ }
+
+ _parser = MidiParser::createParser_XMIDI(nullptr, nullptr, 0);
+ _parser->setMidiDriver(this);
+ _parser->setTimerRate(_driver->getBaseTempo());
+ _parser->property(MidiParser::mpCenterPitchWheelOnUnload, 1);
+ _parser->property(MidiParser::mpSendSustainOffOnNotesOff, 1);
+
+ if (!_parser->loadMusic(_xmiData.data(), _xmiData.size())) {
+ warning("MusicPlayer: XMIDI parser rejected %s",
+ xmiPath.toString().c_str());
+ delete _parser;
+ _parser = nullptr;
+ _xmiData.clear();
+ return;
+ }
+
+ // Mirrors `_LoopMIDI = 0xFFFF` (the count register the original
+ // engine uses for indefinite looping in `_DoOpeningAnims`).
+ _isLooping = loop;
+ _parser->property(MidiParser::mpAutoLoop, loop ? 1 : 0);
+ _parser->setTrack(0);
+
+ // Pull the launcher's music_volume slider into `_masterVolume` so
+ // the non-Miles `Audio::MidiPlayer::send` path scales correctly.
+ // (Miles drivers do their own volume handling on the Multisource
+ // path, but they also honour `MidiDriver::syncSoundSettings` which
+ // `Engine::syncSoundSettings` triggers.)
+ syncVolume();
+ _isPlaying = true;
+ debugC(1, kDebugSound, "MusicPlayer: playing %s (%u bytes, loop=%d, miles=%d)",
+ xmiPath.toString().c_str(), size, loop, _milesAudioMode);
+}
+
+void MusicPlayer::playMus(uint num, bool loop) {
+ // Format string verified at `29be:1525` ("mus%05d.xmi").
+ const Common::String name = Common::String::format("MUS%05u.XMI", num);
+ playFile(Common::Path(name), loop);
+}
+
+} // End of namespace EEM
diff --git a/engines/eem/music.h b/engines/eem/music.h
new file mode 100644
index 00000000000..f02830cf5cf
--- /dev/null
+++ b/engines/eem/music.h
@@ -0,0 +1,95 @@
+/* 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 EEM_MUSIC_H
+#define EEM_MUSIC_H
+
+#include "audio/midiplayer.h"
+
+#include "common/array.h"
+#include "common/path.h"
+#include "common/scummsys.h"
+
+namespace EEM {
+
+/**
+ * MIDI music player. Mirrors the original `MIDI.C` source file in
+ * `EEMCD.EXE` (the `_MIDIPlayFile / _MIDIPlay / _StopMIDI /
+ * _IsMIDIPlaying / _StartTravelMusic` family at `20a2:00e2-05c9`).
+ *
+ * The original engine uses Miles Audio Interface Library (AIL):
+ *
+ * - `_InitMIDI @ 20a2:013a` calls `_AIL_register_driver` against
+ * `ADLIB.ADV` / `SBFM.ADV` / `MT32MPU.ADV` and reserves a timbre
+ * cache via `_AIL_define_timbre_cache`.
+ * - `_MIDIPlayFile @ 20a2:024c` opens the .XMI, calls
+ * `_AIL_register_sequence`, then loops over the sequence's
+ * `_AIL_timbre_request` results. For every (bank, patch) pair the
+ * driver asks for, it pulls the AdLib instrument definition from
+ * **`SAMPLE.AD`** (string at `29be:14d6`) via `_load_global_timbre`
+ * and installs it through `_AIL_install_timbre`. Only after every
+ * patch is loaded does it call `_AIL_start_sequence`.
+ *
+ * Without those custom timbres, ScummVM's generic AdLib synth falls
+ * back to its built-in default timbre table â same notes, very
+ * different timbres. ScummVM ships a Miles AdLib driver
+ * (`Audio::MidiDriver_Miles_AdLib_create`) that loads `SAMPLE.AD` and
+ * implements the same install-on-demand workflow, so we use it for
+ * AdLib output. MT-32 / GM fall back to the generic driver via
+ * `Audio::MidiPlayer::createDriver`.
+ *
+ * Available music files in the game directory:
+ * - THEME.XMI â opening anims (looping) + title screen
+ * - MUS00000.XMI..MUS00004.XMI â per-site travel music
+ * (`_StartTravelMusic` picks one via `_SiteNumber % 5`)
+ * - MUS00005.XMI â winner ending (`_DisplayCorrect` @ 1df2:0789)
+ * - MUS00006.XMI â loser ending (`_DisplayAlibi` @ 1df2:018a)
+ */
+class MusicPlayer : public Audio::MidiPlayer {
+public:
+ MusicPlayer();
+
+ /// Mirrors `_MIDIPlayFile @ 20a2:024c`. Reads the .XMI from the game
+ /// directory and starts playing. `loop=true` mirrors the
+ /// `_LoopMIDI = 0xFFFF` writes inside `_DoOpeningAnims` (theme music).
+ void playFile(const Common::Path &xmiPath, bool loop = false);
+
+ /// Mirrors `_MIDIPlay(num) @ 20a2:047d`. Composes the filename
+ /// "MUS%05u.XMI" and plays it. Used by `_StartTravelMusic`,
+ /// `_DisplayCorrect` (winner), `_DisplayAlibi` (loser).
+ void playMus(uint num, bool loop = false);
+
+ // In Miles AdLib mode the driver allocates its own AdLib voice
+ // pool and consumes the XMIDI's source-channel byte directly, so we
+ // must NOT route through `Audio::MidiPlayer::sendToChannel` (which
+ // remaps every source channel through `allocateChannel()` and
+ // breaks the timbre selection / volume scaling the Miles driver
+ // performs internally). Same workaround Toltecs / SAGA use.
+ void send(uint32 b) override;
+
+private:
+ bool _milesAudioMode = false;
+ Common::Array<byte> _xmiData;
+};
+
+} // End of namespace EEM
+
+#endif
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 4b641dc441c..ea50d7f56c5 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -191,6 +191,11 @@ void SiteScreen::enter(uint siteNum) {
debugC(1, kDebugSite, "Entering site %u (%u hotspots)",
siteNum, _mystery->hotspotCount(siteNum));
+ // `_DoTravel @ 168d:02da` calls `_StartTravelMusic` after the
+ // destination is set. We do the same here so the music swaps as
+ // the player moves between sites.
+ _vm->startTravelMusic();
+
// Palette: original `_BuildBackground` calls `GetPalette(sitenum + 1)`
// where sitenum is the global SITES.DBD index (= the per-mystery
// `sitepic` field), not the per-mystery site index.
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 7c3d181afdd..fe306ef463d 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -32,6 +32,7 @@
#include "eem/detection.h"
#include "eem/eem.h"
+#include "eem/music.h"
// EEM â UI screens (NOTE.C, GALLERY.C, ACCUSE.C, MAP.C, CHOOSE.C combined).
// Each function is a self-contained modal `EEMEngine::doX()` reachable from
@@ -2250,6 +2251,12 @@ void EEMEngine::doAccuse() {
if (mn < sizeof(_mysteriesSolved)) {
_mysteriesSolved[mn] = _mystery._firstTry ? 2 : 1;
}
+
+ // `_DisplayCorrect @ 1df2:073c` calls `_MIDIPlay(5)` (1df2:0789)
+ // before `_DifferenceAnimation("scrapbk.ani")` to swap from the
+ // travel music to the winner cue.
+ if (_music)
+ _music->playMus(5, /*loop=*/false);
playAnm(Common::Path("SCRAPBK.ANI"), 120, true);
// Auto-save into slot 0 (the engine's quicksave slot).
Commit: ffc4f5135bce80fbc259c39850fa74c3f8d13187
https://github.com/scummvm/scummvm/commit/ffc4f5135bce80fbc259c39850fa74c3f8d13187
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:41+02:00
Commit Message:
EEM: implemented digitalized sound
Changed paths:
A engines/eem/audio.cpp
A engines/eem/audio.h
engines/eem/clues.cpp
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/module.mk
engines/eem/site.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/audio.cpp b/engines/eem/audio.cpp
new file mode 100644
index 00000000000..fab34529b71
--- /dev/null
+++ b/engines/eem/audio.cpp
@@ -0,0 +1,345 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "audio/decoders/raw.h"
+#include "audio/decoders/voc.h"
+
+#include "common/compression/dcl.h"
+#include "common/debug.h"
+#include "common/endian.h"
+#include "common/events.h"
+#include "common/memstream.h"
+#include "common/substream.h"
+#include "common/system.h"
+#include "common/textconsole.h"
+
+#include "eem/audio.h"
+#include "eem/detection.h"
+#include "eem/eem.h"
+
+namespace EEM {
+
+AudioPlayer::AudioPlayer(EEMEngine *vm) :
+ _vm(vm), _mixer(g_system->getMixer()) {
+}
+
+AudioPlayer::~AudioPlayer() {
+ stopAll();
+}
+
+void AudioPlayer::stopAll() {
+ stopVoice();
+ stopSpool();
+ cleanMysterySounds();
+}
+
+// VOC playback --------------------------------------------------------
+
+void AudioPlayer::playVoc(const Common::Path &vocPath) {
+ stopVoice();
+
+ // Mirrors `_LoadSoundName`'s `_fopen` (1ff1:02ac).
+ Common::File *f = new Common::File();
+ if (!f->open(vocPath)) {
+ warning("AudioPlayer: %s missing", vocPath.toString().c_str());
+ delete f;
+ return;
+ }
+
+ Audio::SeekableAudioStream *stream =
+ Audio::makeVOCStream(f, Audio::FLAG_UNSIGNED, DisposeAfterUse::YES);
+ if (!stream) {
+ warning("AudioPlayer: %s is not a valid VOC",
+ vocPath.toString().c_str());
+ return;
+ }
+
+ // `_PlayVoice` (1ff1:023e) goes through `_AIL_play_VOC_file` on the
+ // digital channel â we route through `kSpeechSoundType` so the
+ // launcher's "Speech volume" slider applies.
+ _mixer->playStream(Audio::Mixer::kSpeechSoundType, &_voiceHandle,
+ stream, -1, Audio::Mixer::kMaxChannelVolume,
+ 0, DisposeAfterUse::YES);
+ debugC(1, kDebugSound, "AudioPlayer: playVoc(%s)",
+ vocPath.toString().c_str());
+}
+
+bool AudioPlayer::isVoicePlaying() const {
+ return _mixer->isSoundHandleActive(_voiceHandle);
+}
+
+void AudioPlayer::waitForVoiceDone(uint32 maxMs) {
+ // Mirrors the wait loop at `_WaitForVoiceDone @ 1ff1:0221` â pumps
+ // events (so animations + abort-on-click still work) while waiting
+ // for the AIL voice channel to drain.
+ const uint32 startMs = g_system->getMillis();
+ while (isVoicePlaying() && !_vm->shouldQuit() &&
+ g_system->getMillis() - startMs < maxMs) {
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ if (event.type == Common::EVENT_QUIT ||
+ event.type == Common::EVENT_RETURN_TO_LAUNCHER ||
+ event.type == Common::EVENT_LBUTTONDOWN ||
+ event.type == Common::EVENT_KEYDOWN) {
+ stopVoice();
+ return;
+ }
+ }
+ g_system->updateScreen();
+ g_system->delayMillis(10);
+ }
+}
+
+void AudioPlayer::stopVoice() {
+ if (_mixer->isSoundHandleActive(_voiceHandle))
+ _mixer->stopHandle(_voiceHandle);
+}
+
+// Spool sound ---------------------------------------------------------
+
+bool AudioPlayer::readSdxIndex(const Common::Path &sdxPath) {
+ // Mirrors `_ReadLong(&MysterySounds, size, sdx)` at 202f:0647 â
+ // reads the entire .SDX into memory; each 12-byte entry is
+ // (u32 offset, u32 compressed_size, u32 uncompressed_size).
+ Common::File f;
+ if (!f.open(sdxPath)) {
+ warning("AudioPlayer: %s missing", sdxPath.toString().c_str());
+ return false;
+ }
+ const uint32 size = f.size();
+ if (size == 0 || (size % 12) != 0) {
+ warning("AudioPlayer: %s has invalid size %u",
+ sdxPath.toString().c_str(), size);
+ return false;
+ }
+ const uint count = size / 12;
+ _sdxIndex.resize(count);
+ for (uint i = 0; i < count; i++) {
+ _sdxIndex[i].offset = f.readUint32LE();
+ _sdxIndex[i].compressedSize = f.readUint32LE();
+ _sdxIndex[i].uncompressedSize = f.readUint32LE();
+ }
+ return true;
+}
+
+bool AudioPlayer::initMysterySounds(uint mysteryNum) {
+ // Mirrors `_InitMysterySounds @ 202f:05cb` â calls
+ // `_CleanMysterySounds` first, then sprintf-opens `m%u.sdx` (string
+ // at 29be:144f) and `m%u.sdb` (29be:145b).
+ cleanMysterySounds();
+
+ const Common::String sdxName = Common::String::format("M%u.SDX", mysteryNum);
+ const Common::String sdbName = Common::String::format("M%u.SDB", mysteryNum);
+ const Common::Path sdxPath(sdxName);
+ const Common::Path sdbPath(sdbName);
+
+ if (!readSdxIndex(sdxPath)) {
+ _sdxIndex.clear();
+ return false;
+ }
+ _sdbPath = sdbPath;
+ _currentMystery = (int)mysteryNum;
+ debugC(1, kDebugSound, "AudioPlayer: mystery %u â %u sounds",
+ mysteryNum, (uint)_sdxIndex.size());
+ return true;
+}
+
+void AudioPlayer::cleanMysterySounds() {
+ stopSpool();
+ _sdxIndex.clear();
+ _sdbPath = Common::Path();
+ _currentMystery = -1;
+}
+
+void AudioPlayer::playPcmBuffer(byte *pcm, uint32 size, uint sampleRate,
+ Audio::SoundHandle &handle,
+ Audio::Mixer::SoundType type) {
+ // `Audio::makeRawStream` takes ownership and `free()`s the buffer
+ // on stream destruction â so the caller must `malloc` (not `new`)
+ // the PCM buffer.
+ Audio::SeekableAudioStream *stream =
+ Audio::makeRawStream(pcm, size, sampleRate, Audio::FLAG_UNSIGNED,
+ DisposeAfterUse::YES);
+ if (!stream) {
+ free(pcm);
+ warning("AudioPlayer: makeRawStream failed");
+ return;
+ }
+ _mixer->playStream(type, &handle, stream, -1,
+ Audio::Mixer::kMaxChannelVolume, 0,
+ DisposeAfterUse::YES);
+}
+
+void AudioPlayer::spoolSound(uint num) {
+ if (_currentMystery < 0 || num >= _sdxIndex.size()) {
+ warning("AudioPlayer: spoolSound(%u) â invalid index (%d, %u)",
+ num, _currentMystery, (uint)_sdxIndex.size());
+ return;
+ }
+ const SoundEntry &entry = _sdxIndex[num];
+
+ stopSpool();
+
+ Common::File sdb;
+ if (!sdb.open(_sdbPath)) {
+ warning("AudioPlayer: %s missing", _sdbPath.toString().c_str());
+ return;
+ }
+ if (!sdb.seek(entry.offset)) {
+ warning("AudioPlayer: %s seek to %u failed",
+ _sdbPath.toString().c_str(), entry.offset);
+ return;
+ }
+
+ // Mirrors the two `_fgetc(in)` reads at 202f:02da-e1: byte 0 =
+ // Sound Blaster Time Constant, byte 1 = total AIL playback blocks.
+ // Convert TC -> sample rate via the standard SB formula. The block
+ // count is only used internally by the AIL DDS pipeline; ScummVM's
+ // mixer doesn't need it.
+ const byte tc = sdb.readByte();
+ (void)sdb.readByte(); // total blocks â unused outside AIL
+
+ // SB Time Constant: rate = 1000000 / (256 - tc). e.g. tc=0xD2 â
+ // 22 kHz. Guard against the degenerate tc=0xFF (would divide by 1
+ // â 1 MHz, well above what the mixer can resample sanely).
+ const uint sampleRate = (tc < 0xFF)
+ ? (uint)(1000000u / (256u - tc))
+ : 44100u;
+
+ byte *pcm = nullptr;
+ uint32 audioSize = 0;
+
+ if (entry.compressedSize == entry.uncompressedSize) {
+ // `_UncompressedSound @ 202f:03e6` â already raw PCM. The
+ // `_SpoolSound` equality check at 202f:06e6 is `comp == uncomp`;
+ // in that case `len = uncompressed_size` bytes follow the
+ // 2-byte header. Original reads in 16 KB chunks; we slurp the
+ // lot at once.
+ audioSize = entry.uncompressedSize;
+ pcm = (byte *)malloc(audioSize);
+ if (!pcm) {
+ warning("AudioPlayer: spoolSound %u oom (%u bytes)",
+ num, audioSize);
+ return;
+ }
+ if (sdb.read(pcm, audioSize) != audioSize) {
+ warning("AudioPlayer: short read on uncompressed sound %u", num);
+ free(pcm);
+ return;
+ }
+ } else {
+ // `_DeCompressSound @ 202f:02ad` â `EXPLODE @ 25c6:0d01`
+ // (PKWARE DCL "Implode") with READDISKSOUND/WRITESOUND
+ // callbacks. EXPLODE drives both ends via its OWN end-of-stream
+ // marker (length token 519); the SDX `compressed_size` /
+ // `uncompressed_size` are loose hints, NOT exact lengths â
+ // 202f:0332 even computes `destSize - 2` and never reads it.
+ // ScummVM's fixed-size `decompressDCL` overload errors when
+ // the actual output exceeds our pre-allocated buffer (which
+ // we saw on every M0 clue voice). Use the dynamic-sized
+ // overload instead â it lets the DCL stream terminate at its
+ // own marker. Source is bounded to the rest of the SDB so
+ // the bit reader can't fall off the end.
+ const uint32 streamStart = (uint32)sdb.pos();
+ Common::SeekableSubReadStream sub(&sdb, streamStart,
+ (uint32)sdb.size(),
+ DisposeAfterUse::NO);
+ Common::SeekableReadStream *out = Common::decompressDCL(&sub);
+ if (!out) {
+ warning("AudioPlayer: DCL decompression failed on sound %u "
+ "(comp=%u, uncomp=%u)",
+ num, entry.compressedSize, entry.uncompressedSize);
+ return;
+ }
+ audioSize = (uint32)out->size();
+ pcm = (byte *)malloc(audioSize);
+ if (!pcm) {
+ warning("AudioPlayer: spoolSound %u oom after DCL (%u bytes)",
+ num, audioSize);
+ delete out;
+ return;
+ }
+ out->read(pcm, audioSize);
+ delete out;
+ }
+
+ debugC(1, kDebugSound,
+ "AudioPlayer: spoolSound(%u) tc=0x%02x rate=%u size=%u %s",
+ num, tc, sampleRate, audioSize,
+ entry.compressedSize == entry.uncompressedSize ? "raw" : "DCL");
+
+ // `_AIL_start_digital_playback` at 202f:040c â we route through
+ // `kSFXSoundType` so the launcher's "SFX volume" slider applies
+ // (this is the same slider the original would've targeted via
+ // `_AIL_set_digital_master_volume`). Voice clips on the spool path
+ // are gameplay SFX, not the speech-only VOC stream.
+ playPcmBuffer(pcm, audioSize, sampleRate, _spoolHandle,
+ Audio::Mixer::kSFXSoundType);
+}
+
+bool AudioPlayer::isSpoolPlaying() const {
+ return _mixer->isSoundHandleActive(_spoolHandle);
+}
+
+void AudioPlayer::waitForSpoolDone(uint32 maxMs) {
+ const uint32 startMs = g_system->getMillis();
+ while (isSpoolPlaying() && !_vm->shouldQuit() &&
+ g_system->getMillis() - startMs < maxMs) {
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ if (event.type == Common::EVENT_QUIT ||
+ event.type == Common::EVENT_RETURN_TO_LAUNCHER ||
+ event.type == Common::EVENT_LBUTTONDOWN ||
+ event.type == Common::EVENT_KEYDOWN) {
+ stopSpool();
+ return;
+ }
+ }
+ g_system->updateScreen();
+ g_system->delayMillis(10);
+ }
+}
+
+void AudioPlayer::stopSpool() {
+ if (_mixer->isSoundHandleActive(_spoolHandle))
+ _mixer->stopHandle(_spoolHandle);
+}
+
+void AudioPlayer::sayKDDigital(const byte *kdTextIndex, uint kdspeak,
+ uint partner) {
+ if (!kdTextIndex || _currentMystery < 0)
+ return;
+ // `_SayKDDigital @ 2404:0fbc`:
+ // iVar1 = kdspeak * 2;
+ // if (_Partner == 0) iVar1++; // Jake offset
+ // sound = *(u16 *)(KDDigitalIndex + (iVar1 + 1) * 2) - 1;
+ // _SpoolSound(sound);
+ // KDDigitalIndex sits 18 bytes (`+ 0x12`) after KDTextIndex per
+ // `_ReadMystery` 2404:0163-0167.
+ const byte *digital = kdTextIndex + 0x12;
+ const uint slot = (kdspeak * 2) + (partner == 0 ? 1u : 0u) + 1u;
+ const uint16 raw = READ_LE_UINT16(digital + slot * 2);
+ if (raw == 0 || raw == 0xFFFF)
+ return;
+ spoolSound((uint)(raw - 1));
+}
+
+} // End of namespace EEM
diff --git a/engines/eem/audio.h b/engines/eem/audio.h
new file mode 100644
index 00000000000..c9238b0b3f4
--- /dev/null
+++ b/engines/eem/audio.h
@@ -0,0 +1,159 @@
+/* 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 EEM_AUDIO_H
+#define EEM_AUDIO_H
+
+#include "audio/mixer.h"
+#include "audio/audiostream.h"
+
+#include "common/array.h"
+#include "common/file.h"
+#include "common/path.h"
+#include "common/scummsys.h"
+
+namespace EEM {
+
+class EEMEngine;
+
+/**
+ * Non-MIDI audio (digitised voice + sound effects). Mirrors the
+ * original `SOUND.C` / `SPOOLSND.C` source files in `EEMCD.EXE` â
+ * the AIL digital playback path used for both standalone .VOC files
+ * and the per-mystery `M%d.SDB` spool stream.
+ *
+ * Two pathways:
+ *
+ * 1. **VOC playback** â `_LoadSoundName @ 1ff1:0299` reads a Creative
+ * Voice File into memory and `_PlayVoice @ 1ff1:023e` hands it to
+ * `_AIL_play_VOC_file`. Used for THUNDER.VOC (Storm logo),
+ * PHONE.VOC (briefing), JEN.VOC / JAKE.VOC (partner choose).
+ *
+ * 2. **Spool stream** â `_InitMysterySounds @ 202f:05cb` opens
+ * `m%d.sdx` (29be:144f) and `m%d.sdb` (29be:145b) for the active
+ * mystery. `_SpoolSound(num) @ 202f:068d` indexes into the SDX
+ * table (12 bytes per entry: u32 file_offset, u32 compressed_size,
+ * u32 uncompressed_size). If sizes match it streams via
+ * `_UncompressedSound @ 202f:03e6`; otherwise it EXPLODE-decompresses
+ * via `_DeCompressSound @ 202f:02ad`.
+ *
+ * Each entry's data starts with 2 metadata bytes â Sound Blaster
+ * Time Constant (`rate = 1000000 / (256 - tc)`) and the AIL block
+ * count â followed by the (optionally) PKWARE-DCL-compressed
+ * 8-bit unsigned PCM stream. We use ScummVM's `Common::decompressDCL`
+ * + `Audio::makeRawStream` to reach the same audio at the same
+ * sample rate.
+ *
+ * Mystery 60 (`M60.SDB/SDX`) holds the 19 voiceovers played between
+ * ANIM01..ANIM20 in `_DoOpeningAnims` (it loads `_InitMysterySounds(0x3c)`
+ * before the loop and `_SpoolSound(uVar3 - 1)` between every clip).
+ * Mysteries 0..55 hold each case's per-clue voice plus the partner's
+ * digital lines (`_KDDigitalIndex` table within the .SD blob).
+ */
+class AudioPlayer {
+public:
+ explicit AudioPlayer(EEMEngine *vm);
+ ~AudioPlayer();
+
+ // VOC playback ----------------------------------------------------
+
+ /// Mirrors `_LoadSoundName` + `_PlayVoice`. Loads the named .VOC
+ /// from the game directory and hands it to the speech mixer
+ /// channel. A new `playVoc` cancels any prior voice.
+ void playVoc(const Common::Path &vocPath);
+
+ /// Mirrors `_VoicePlaying @ 1ff1:01f9`.
+ bool isVoicePlaying() const;
+
+ /// Mirrors `_WaitForVoiceDone @ 1ff1:0221`. Blocks (with frame /
+ /// event pumping, like the rest of the engine's busy-loops) until
+ /// the voice clip finishes. Returns early if the user clicks /
+ /// presses a key â same abort behaviour the original
+ /// `_AIL_stop_digital_playback` callback installed.
+ void waitForVoiceDone(uint32 maxMs = 60000);
+
+ /// Mirrors `_StopTheVoice @ 1ff1:0283`.
+ void stopVoice();
+
+ // Mystery sound spool ---------------------------------------------
+
+ /// Mirrors `_InitMysterySounds @ 202f:05cb`. Loads `M%u.SDX` into
+ /// memory and remembers the corresponding `M%u.SDB` path.
+ bool initMysterySounds(uint mysteryNum);
+
+ /// Mirrors `_CleanMysterySounds @ 202f:05a5`.
+ void cleanMysterySounds();
+
+ /// Mirrors `_SpoolSound @ 202f:068d`. Reads + decompresses entry
+ /// `num` from the active SDB and queues it for SFX playback. The
+ /// original blocks until playback finishes â for ScummVM we let
+ /// the mixer run asynchronously and expose `waitForSpoolDone` so
+ /// callers that need the original "block-then-continue" semantics
+ /// can opt in.
+ void spoolSound(uint num);
+
+ /// Mirrors the abort-on-input wait loop inside `_UncompressedSound`
+ /// / `_DeCompressSound`. Returns when the spool clip finishes or
+ /// the user clicks / presses a key.
+ void waitForSpoolDone(uint32 maxMs = 60000);
+
+ /// Mirrors the immediate `_AIL_stop_digital_playback` exit.
+ void stopSpool();
+
+ bool isSpoolPlaying() const;
+
+ /// Mirrors `_SayKDDigital(kdspeak) @ 2404:0fbc`. Each mystery
+ /// embeds a `KDDigitalIndex` table immediately after the 18-byte
+ /// `KDTextIndex` header (set up by `_ReadMystery @ 2404:008f`:
+ /// `_KDDigitalIndex = _KDTextIndex + 0x12`). The table is two
+ /// 1-based sound indices per `kdspeak` slot â Jen at +2, Jake at
+ /// +4 from each entry's start (+1 word for the unused header
+ /// slot). Pass the mystery's `kdTextIndex()` pointer.
+ void sayKDDigital(const byte *kdTextIndex, uint kdspeak, uint partner);
+
+ /// Mirrors `_QuitSounds @ 1ff1:03c5`.
+ void stopAll();
+
+private:
+ struct SoundEntry {
+ uint32 offset;
+ uint32 compressedSize;
+ uint32 uncompressedSize;
+ };
+
+ bool readSdxIndex(const Common::Path &sdxPath);
+ void playPcmBuffer(byte *pcm, uint32 size, uint sampleRate,
+ Audio::SoundHandle &handle,
+ Audio::Mixer::SoundType type);
+
+ EEMEngine *_vm = nullptr;
+ Audio::Mixer *_mixer = nullptr;
+ Audio::SoundHandle _voiceHandle;
+ Audio::SoundHandle _spoolHandle;
+
+ Common::Array<SoundEntry> _sdxIndex;
+ Common::Path _sdbPath;
+ int _currentMystery = -1;
+};
+
+} // End of namespace EEM
+
+#endif
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index e5fc4df255c..a3907d32ef3 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -30,6 +30,7 @@
#include "graphics/cursorman.h"
#include "graphics/managed_surface.h"
+#include "eem/audio.h"
#include "eem/detection.h"
#include "eem/eem.h"
@@ -228,6 +229,15 @@ void EEMEngine::doChoosePartner() {
g_system->updateScreen();
g_system->delayMillis(20);
}
+
+ // Mirrors the tail of `_DoChoosePartner @ 1a35:097f` â once the
+ // player commits to a partner, load and play their intro VOC
+ // (`jen.voc` for Jenny, `jake.voc` for Jake; strings at 29be:0af1 /
+ // 29be:0af9) and block on `_WaitForVoiceDone`.
+ if (_audio) {
+ _audio->playVoc(Common::Path(_partner == 0 ? "JAKE.VOC" : "JEN.VOC"));
+ _audio->waitForVoiceDone();
+ }
}
void EEMEngine::doInitClues() {
@@ -409,6 +419,15 @@ void EEMEngine::doInitClues() {
}
}
+ // `_DoInitClues` plays `phone.voc` (29be:0acc) ONLY when caseType == 2
+ // (the "incoming call" briefing variant). Verified at 1a35:05a2 â
+ // the gate is `iVar1 == 2 && _VoiceAvailable`. Other case types open
+ // straight into the briefing dialogue without it.
+ if (caseType == 2 && _audio) {
+ _audio->playVoc(Common::Path("PHONE.VOC"));
+ _audio->waitForVoiceDone();
+ }
+
// Step 6 â case briefing dialogue.
displayClue(ib + 4);
}
@@ -686,6 +705,20 @@ void EEMEngine::displayClue(const byte *clueBlock) {
g_system->updateScreen();
}
+ // `_DisplayClue` @ 2404:0833-085a â after the balloon is drawn,
+ // spool the per-clue voice. Each ClueEntry stores two 1-based
+ // sound indices: `+0x18` for partner=Jenny and `+0x1a` for
+ // partner=Jake (verified against 2404:0823-0834). Index 0 / -1
+ // = no audio. The original blocks until the line ends; we run
+ // async (the wait happens implicitly while the player reads).
+ if (_audio) {
+ const uint16 voiceJenny = READ_LE_UINT16(c + 0x18);
+ const uint16 voiceJake = READ_LE_UINT16(c + 0x1a);
+ const uint16 voice = (_partner == 0) ? voiceJake : voiceJenny;
+ if (voice != 0 && voice != 0xFFFF)
+ _audio->spoolSound((uint)(voice - 1));
+ }
+
// Wait for click/key to advance â only if we drew something.
// ESC skips the entire dialogue rather than just one entry.
if (hasText || (charPicId != 0 && charPicId != 0xFFFF)) {
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 0895b869273..c6e2552cf79 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -33,6 +33,7 @@
#include "graphics/cursorman.h"
#include "graphics/paletteman.h"
+#include "eem/audio.h"
#include "eem/detection.h"
#include "eem/eem.h"
#include "eem/music.h"
@@ -95,6 +96,7 @@ EEMEngine::EEMEngine(OSystem *syst, const ADGameDescription *gameDesc)
}
EEMEngine::~EEMEngine() {
+ delete _audio;
delete _music;
}
@@ -116,6 +118,12 @@ Common::Error EEMEngine::run() {
// here (after `initGraphics` so the OSystem's timer/mixer is up).
_music = new MusicPlayer();
+ // Digital audio (VOC + spool). Mirrors `_InitDrivers @ 1ff1:0368`
+ // which `_AIL_register_driver`s SBDIG.ADV / PASDIG.ADV alongside
+ // the MIDI driver.
+ _audio = new AudioPlayer(this);
+ syncSoundSettings();
+
// _InitMouse @ 152d:018b in the original â install our 11x16 arrow,
// using palette index 0 as the transparency key. The cursor is left
// hidden through the opening anims and switched on at NewPlayer /
@@ -184,8 +192,22 @@ Common::Error EEMEngine::run() {
showEAKidsLogo();
if (!shouldQuit() && !_skipIntro)
showHighScoreLogo();
- if (!shouldQuit() && !_skipIntro)
+ // Storm Software logo: voice + animation. The original at
+ // `_ShowStormLogo @ 2520:0707` calls `_LoadSoundName("thunder.voc")`
+ // (29be:177d) and passes the buffer to `OpenDifferenceAnimation_Sound`
+ // so the thunder roar plays alongside the lightning bolt.
+ if (!shouldQuit() && !_skipIntro) {
+ if (_audio)
+ _audio->playVoc(Common::Path("THUNDER.VOC"));
playAnm(Common::Path("BOLT.ANM"));
+ if (_audio)
+ _audio->stopVoice();
+ }
+ // `_InitMysterySounds(0x3c)` at 2520:086a â load M60.SDX/SDB so
+ // `_SpoolSound(uVar3 - 1)` between the ANIM01..ANIM20 anims has
+ // data to draw from.
+ if (!shouldQuit() && !_skipIntro && _audio)
+ _audio->initMysterySounds(60);
// Theme begins HERE â after the three silent logos, before the
// character-intro reel.
if (!shouldQuit() && !_skipIntro && _music)
@@ -193,11 +215,18 @@ Common::Error EEMEngine::run() {
for (int i = 1; i <= 20 && !shouldQuit() && !_skipIntro; i++) {
Common::String name = Common::String::format("ANIM%02d.A", i);
playAnm(Common::Path(name));
- // Between anims the original plays a voice clip via _SpoolSound;
- // without audio we still want a beat so each scene reads.
- if (!shouldQuit() && !_skipIntro && i != 20)
- waitForInput(2000);
+ // `_SpoolSound(uVar3 - 1)` at 2520:08c2 â the per-character VO
+ // plays AFTER each anim except the last (`if (uVar3 != 0x14)`
+ // at 2520:08a8). Original blocks until done; we run async and
+ // wait so the next anim doesn't start before the line ends.
+ if (!shouldQuit() && !_skipIntro && i != 20 && _audio) {
+ _audio->spoolSound((uint)(i - 1));
+ _audio->waitForSpoolDone();
+ }
}
+ // `_CleanMysterySounds` at 2520:0903 â release M60 before the title.
+ if (_audio)
+ _audio->cleanMysterySounds();
// Restart the theme for TITLE.ANM â matches the second
// `_MIDIPlayFile("theme.xmi")` call at 2520:0918.
if (!shouldQuit() && !_skipIntro && _music)
@@ -590,6 +619,11 @@ Common::Error EEMEngine::loadGameState(int slot) {
delete in;
return Common::kReadingFailed;
}
+ // `_ReadMystery @ 2404:008f` calls `_InitMysterySounds(_MysteryNumber)`
+ // at the tail (2404:0298) so the SDB index is in place for clue and
+ // partner-speech spool sounds.
+ if (_audio)
+ _audio->initMysterySounds(mysteryNum);
Common::Serializer s(in, nullptr);
s.setVersion(ver);
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index e2f3e0c2ce4..4eded42e068 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -39,6 +39,7 @@
namespace EEM {
+class AudioPlayer;
class MusicPlayer;
/**
@@ -307,6 +308,13 @@ private:
/// at 20a2:00e2-05c9). Constructed lazily during `run()` once the
/// MIDI driver / timer system is up.
MusicPlayer *_music = nullptr;
+
+ /// Digitised audio (voice + SFX). Mirrors `SOUND.C` / `SPOOLSND.C`
+ /// â VOC playback (`_PlayVoice @ 1ff1:023e`) and the per-mystery
+ /// SDB spool (`_SpoolSound @ 202f:068d` / `_InitMysterySounds @
+ /// 202f:05cb`). Constructed alongside `_music` in `run()`.
+public:
+ AudioPlayer *_audio = nullptr;
};
} // End of namespace EEM
diff --git a/engines/eem/module.mk b/engines/eem/module.mk
index ce7975e1edc..6879b600e85 100644
--- a/engines/eem/module.mk
+++ b/engines/eem/module.mk
@@ -2,6 +2,7 @@ MODULE := engines/eem
MODULE_OBJS = \
animation.o \
+ audio.o \
clues.o \
eem.o \
font.o \
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index ea50d7f56c5..e6122b5d6eb 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -26,6 +26,7 @@
#include "graphics/paletteman.h"
+#include "eem/audio.h"
#include "eem/detection.h"
#include "eem/eem.h"
#include "eem/mystery.h"
@@ -380,6 +381,8 @@ void SiteScreen::run() {
case Common::KEYCODE_r:
// Restart the mystery from scratch (mirrors `_ReloadMystery`).
if (_mystery->load(_mystery->number())) {
+ if (_vm->_audio)
+ _vm->_audio->initMysterySounds(_mystery->number());
cur = 0;
enter(cur);
}
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index fe306ef463d..121a83d440f 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -30,6 +30,7 @@
#include "graphics/cursorman.h"
#include "graphics/managed_surface.h"
+#include "eem/audio.h"
#include "eem/detection.h"
#include "eem/eem.h"
#include "eem/music.h"
@@ -522,6 +523,8 @@ void EEMEngine::doCaseSelection() {
if (!_mystery.load(0, &_rng)) {
warning("doCaseSelection: failed to load practice mystery");
_mystery.clear();
+ } else if (_audio) {
+ _audio->initMysterySounds(0);
}
return;
}
@@ -655,6 +658,8 @@ void EEMEngine::doCaseSelection() {
_mystery.clear();
return;
}
+ if (_audio)
+ _audio->initMysterySounds(sel);
debugC(1, kDebugMystery, "Mystery %u loaded; %u sites, %u suspects",
sel, _mystery.numSites(), _mystery.numSuspects());
}
Commit: f7b94e57211894e3e0858fc49a441e62e7f69f83
https://github.com/scummvm/scummvm/commit/f7b94e57211894e3e0858fc49a441e62e7f69f83
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:42+02:00
Commit Message:
EEM: profile handling in save games
Changed paths:
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/ui.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index c6e2552cf79..cade4889101 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -56,9 +56,15 @@ const uint kPicHighScoreLogo = 0x20c; ///< _ShowHScoreLogo: GetPicture(0x20c)
const uint kPalEAKids = 0x25;
const uint kPalHighScore = 0x27;
-// Save format. Used by `saveGameState` / `loadGameState`.
-const uint32 kSaveMagic = MKTAG('E', 'E', 'M', '0');
-const byte kSaveVer = 3; ///< v2: _mysteriesSolved tracker; v3: player name
+// Save body version, used by the `Common::Serializer` inside
+// `saveGameStream`/`loadGameStream`. The framework's extended-save
+// header (description / thumbnail / playtime) is appended/parsed
+// separately by `Engine::saveGameState` / `MetaEngine::readSavegameHeader`,
+// so we don't need a magic word or our own metadata fields.
+//
+// v1 â initial schema: name + mysteriesSolved + partner +
+// optional mystery sub-state.
+const byte kSaveBodyVer = 1;
// 11x16 mouse cursor â replaces the DOS hardware cursor wired in by
// _InitMouse @ 152d:018b (INT 33h). The original game sets the cursor
@@ -541,99 +547,176 @@ bool EEMEngine::hasFeature(EngineFeature f) const {
}
bool EEMEngine::canLoadGameStateCurrently(Common::U32String *) {
- return false; // Loading is startup-only.
+ // Loading mid-mystery would replace `_mystery._data` while
+ // pointers into it are alive on the stack inside `displayClue`
+ // etc. Profile picking still works via `loadProfile` from the
+ // menu screens before a mystery loads.
+ return !_mystery.isLoaded();
}
bool EEMEngine::canSaveGameStateCurrently(Common::U32String *) {
- return _mystery.isLoaded();
+ // Profile saves (no mystery loaded) are always OK; mid-mystery
+ // snapshots only after the active case has fully initialised.
+ return true;
}
-Common::Error EEMEngine::saveGameState(int slot, const Common::String &desc, bool isAutosave) {
- Common::OutSaveFile *out = getSaveFileManager()->openForSaving(getSaveStateName(slot));
- if (!out)
- return Common::kCreatingFileFailed;
-
- out->writeUint32BE(kSaveMagic);
- out->writeByte(kSaveVer);
-
- // Header: description + ScummVM extended save metadata are appended
- // automatically when `EngineFeature::kSavesUseExtendedFormat` is set;
- // our save body just carries the engine state.
- (void)desc;
+Common::Error EEMEngine::saveGameStream(Common::WriteStream *stream,
+ bool isAutosave) {
(void)isAutosave;
- uint16 mysteryNum = (uint16)_mystery.number();
- out->writeUint16LE(mysteryNum);
- out->writeByte(_partner);
- out->write(_mysteriesSolved, sizeof(_mysteriesSolved));
-
- // v3: persist the player name so save-slot resume restores it.
- out->writeUint16LE((uint16)_playerName.size());
- out->writeString(_playerName);
+ Common::Serializer s(nullptr, stream);
+ s.setVersion(kSaveBodyVer);
+
+ // Profile-level state â mirrors the original `_PlayerRecord` body
+ // at `2d5d:3f6a` (159 bytes, written by `_SavePlayerRecord @
+ // 1c33:034f`). The `_PlayerRecord` layout is:
+ // +0x00..+0x0b : player name (12 chars, null-padded)
+ // +0x0c..+0x1f : random ID bytes used by `_GenerateFilename`
+ // (29be:0dbf "C:\EEMCDSAV\%s.PLR") â irrelevant to
+ // ScummVM saves which key on slot, not filename.
+ // +0x20..+0x28 : derived 8-char .PLR basename â likewise unused.
+ // +0x2d : voice-enable flag (`DAT_2d5d_3f97`, default 1).
+ // +0x2f : chain stage (`DAT_2d5d_3f99`, 1=A, 2=B, 3=C â
+ // `_DisplayCorrect` advances it once every case
+ // in the current set is solved).
+ // +0x31..+0xa6 : `mysteriesSolved[55]` u16 (0=unsolved, 1=solved,
+ // 2=solved on first try) â `_DisplayCorrect`
+ // writes 1 always, 2 when `_FirstTry != 0`.
+ //
+ // We persist the gameplay-meaningful subset (name + solved table +
+ // partner) and skip the filename-derivation bytes. The voice /
+ // chain-stage fields aren't yet wired into the C++ port; we save
+ // space for them so they slot in without a version bump.
+ s.syncString(_playerName);
+ s.syncBytes(_mysteriesSolved, sizeof(_mysteriesSolved));
+ s.syncAsByte(_partner);
+
+ // ScummVM-only extension: persist the in-progress mystery so the
+ // player can resume mid-case. The original engine has no such
+ // notion â `_LoadGame @ 2404:0dc7` simply loads a fresh mystery,
+ // it doesn't preserve site progress. The flag lets a profile save
+ // stay valid even when no mystery is loaded (e.g. fresh profile).
+ bool hasMystery = _mystery.isLoaded();
+ s.syncAsByte(hasMystery);
+ if (hasMystery) {
+ uint16 mysteryNum = (uint16)_mystery.number();
+ s.syncAsUint16LE(mysteryNum);
+ _mystery.syncState(s);
+ }
debugC(1, kDebugGeneral,
- "Saved slot %d: mystery=%u partner=%u name=%s autosave=%d",
- slot, mysteryNum, _partner, _playerName.c_str(), isAutosave ? 1 : 0);
+ "Saved profile name=%s partner=%u mystery=%d autosave=%d",
+ _playerName.c_str(), _partner,
+ hasMystery ? (int)_mystery.number() : -1,
+ isAutosave ? 1 : 0);
+ return Common::kNoError;
+}
- Common::Serializer s(nullptr, out);
- s.setVersion(kSaveVer);
- _mystery.syncState(s);
+Common::Error EEMEngine::loadGameStream(Common::SeekableReadStream *stream) {
+ Common::Serializer s(stream, nullptr);
+ s.setVersion(kSaveBodyVer);
+
+ s.syncString(_playerName);
+ if (_playerName.empty())
+ _playerName = "Detective";
+
+ s.syncBytes(_mysteriesSolved, sizeof(_mysteriesSolved));
+ s.syncAsByte(_partner);
+
+ bool hasMystery = false;
+ s.syncAsByte(hasMystery);
+ if (hasMystery) {
+ uint16 mysteryNum = 0;
+ s.syncAsUint16LE(mysteryNum);
+ if (!_mystery.load(mysteryNum, &_rng)) {
+ _mystery.clear();
+ return Common::kReadingFailed;
+ }
+ // `_ReadMystery @ 2404:008f` calls `_InitMysterySounds` at the
+ // tail (2404:0298) so the SDB index is in place for clue and
+ // partner-speech spool sounds.
+ if (_audio)
+ _audio->initMysterySounds(mysteryNum);
+ _mystery.syncState(s);
+ } else {
+ _mystery.clear();
+ }
- out->finalize();
- delete out;
+ debugC(1, kDebugGeneral,
+ "Loaded profile name=%s partner=%u mystery=%d",
+ _playerName.c_str(), _partner,
+ _mystery.isLoaded() ? (int)_mystery.number() : -1);
return Common::kNoError;
}
-Common::Error EEMEngine::loadGameState(int slot) {
- Common::InSaveFile *in = getSaveFileManager()->openForLoading(getSaveStateName(slot));
- if (!in)
- return Common::kReadingFailed;
+SaveStateList EEMEngine::listProfiles() const {
+ // Mirrors `_findfirst("*.PLR")` in `screen8_handler @ 1c33:1012`.
+ return getMetaEngine()->listSaves(_targetName.c_str());
+}
- if (in->readUint32BE() != kSaveMagic) {
- delete in;
- return Common::kUnknownError;
- }
- const byte ver = in->readByte();
- if (ver > kSaveVer) {
- delete in;
- return Common::kUnknownError;
- }
+Common::Error EEMEngine::saveProfile(const Common::String &name) {
+ if (name.empty())
+ return Common::kCreatingFileFailed;
- const uint16 mysteryNum = in->readUint16LE();
- _partner = in->readByte();
- if (ver >= 2)
- in->read(_mysteriesSolved, sizeof(_mysteriesSolved));
- else
- memset(_mysteriesSolved, 0, sizeof(_mysteriesSolved));
-
- if (ver >= 3) {
- const uint16 nameLen = in->readUint16LE();
- Common::String name;
- for (uint16 i = 0; i < nameLen && i < 64; i++)
- name += (char)in->readByte();
- _playerName = name.empty() ? Common::String("Detective") : name;
+ const SaveStateList saves = listProfiles();
+
+ // Slot lookup by description: if a save with this profile name
+ // already exists, overwrite it. Same as Wetlands' `saveProfile`.
+ int slot = -1;
+ for (auto &s : saves) {
+ if (s.getDescription() == name) {
+ slot = s.getSaveSlot();
+ break;
+ }
}
- if (!_mystery.load(mysteryNum, &_rng)) {
- _mystery.clear();
- delete in;
- return Common::kReadingFailed;
+ // New profile â pick the lowest unused slot. The MetaEngine caps
+ // us at 99 by default (`getMaximumSaveSlot`); 25 was the DOS
+ // original's limit (`screen8_handler` walks `*.PLR` up to 25
+ // entries in `local_8c[0x19][2]`).
+ if (slot < 0) {
+ const int maxSlot = getMetaEngine()->getMaximumSaveSlot();
+ Common::Array<bool> used(maxSlot + 1);
+ for (auto &s : saves) {
+ const int sl = s.getSaveSlot();
+ if (sl >= 0 && sl <= maxSlot)
+ used[sl] = true;
+ }
+ for (int i = 0; i <= maxSlot; i++) {
+ if (!used[i]) {
+ slot = i;
+ break;
+ }
+ }
+ if (slot < 0)
+ return Common::kCreatingFileFailed;
}
- // `_ReadMystery @ 2404:008f` calls `_InitMysterySounds(_MysteryNumber)`
- // at the tail (2404:0298) so the SDB index is in place for clue and
- // partner-speech spool sounds.
- if (_audio)
- _audio->initMysterySounds(mysteryNum);
- Common::Serializer s(in, nullptr);
- s.setVersion(ver);
- _mystery.syncState(s);
+ _playerName = name;
+ debugC(1, kDebugGeneral, "saveProfile(%s) -> slot %d",
+ name.c_str(), slot);
+ return saveGameState(slot, name, /*isAutosave=*/false);
+}
- delete in;
- debugC(1, kDebugGeneral,
- "Loaded slot %d: mystery=%u partner=%u name=%s",
- slot, mysteryNum, _partner, _playerName.c_str());
- return Common::kNoError;
+bool EEMEngine::loadProfile(const Common::String &name) {
+ if (name.empty())
+ return false;
+
+ const SaveStateList saves = listProfiles();
+ for (auto &s : saves) {
+ if (s.getDescription() == name) {
+ const Common::Error err = loadGameState(s.getSaveSlot());
+ if (err.getCode() == Common::kNoError) {
+ debugC(1, kDebugGeneral, "loadProfile(%s) <- slot %d",
+ name.c_str(), s.getSaveSlot());
+ return true;
+ }
+ break;
+ }
+ }
+ debugC(1, kDebugGeneral, "loadProfile(%s) â no matching slot",
+ name.c_str());
+ return false;
}
void EEMEngine::screenDriver() {
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 4eded42e068..f1aa061d57d 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -29,8 +29,11 @@
#include "common/random.h"
#include "common/scummsys.h"
+#include "common/serializer.h"
+
#include "engines/advancedDetector.h"
#include "engines/engine.h"
+#include "engines/savestate.h"
#include "eem/animation.h"
#include "eem/font.h"
@@ -66,8 +69,37 @@ public:
bool hasFeature(EngineFeature f) const override;
bool canLoadGameStateCurrently(Common::U32String *msg = nullptr) override;
bool canSaveGameStateCurrently(Common::U32String *msg = nullptr) override;
- Common::Error loadGameState(int slot) override;
- Common::Error saveGameState(int slot, const Common::String &desc, bool isAutosave) override;
+
+ // ScummVM extended-save hooks. The base `Engine::saveGameState` /
+ // `loadGameState` write/read the framework header (description,
+ // thumbnail, playtime, version) around our body via these
+ // streams. We keep all per-profile state in the body, with a
+ // single `Common::Serializer` version so future field additions
+ // stay backward-compatible.
+ Common::Error saveGameStream(Common::WriteStream *stream,
+ bool isAutosave = false) override;
+ Common::Error loadGameStream(Common::SeekableReadStream *stream) override;
+
+ // Per-profile save helpers. The original `_PlayerRecord` lives at
+ // `2d5d:3f6a` (159 bytes) and is written by `_SavePlayerRecord @
+ // 1c33:034f` to `C:\EEMCDSAV\<name>.PLR`. The DOS launcher screen
+ // `screen8_handler @ 1c33:1012` walks `*.PLR`, lets the player
+ // pick a profile, and calls `_LoadPlayerRecord`. We mirror the
+ // pattern by mapping each ScummVM save slot to one profile (slot
+ // description = player name) â same approach Wetlands uses.
+
+ /// Mirrors `_SavePlayerRecord @ 1c33:034f`. Saves into the slot
+ /// whose description matches @p name, or the lowest unused slot
+ /// if no match. Returns the kNoError on success.
+ Common::Error saveProfile(const Common::String &name);
+
+ /// Mirrors `_LoadPlayerRecord @ 1c33:03a6`. Returns false if no
+ /// slot has @p name as its description.
+ bool loadProfile(const Common::String &name);
+
+ /// Mirrors the `_findfirst("*.PLR")` walk inside
+ /// `screen8_handler`. Sorted by slot.
+ SaveStateList listProfiles() const;
const ADGameDescription *_gameDescription;
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 121a83d440f..c78ea11d37d 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -284,7 +284,19 @@ void EEMEngine::doNewPlayer() {
if (k == Common::KEYCODE_RETURN) {
if (name.empty())
name = "Detective";
- _playerName = name;
+ // Mirrors `_NewPlayer @ 1c33:0dda` tail (1c33:0fa0+):
+ // after the name is committed, try `_LoadPlayerRecord`
+ // â if it returns 0 (no existing .PLR), zero out the
+ // per-profile state and call `_SavePlayerRecord` to
+ // create a fresh profile file. Same flow here, mapped
+ // onto ScummVM save slots via name â description.
+ if (!loadProfile(name)) {
+ _playerName = name;
+ memset(_mysteriesSolved, 0, sizeof(_mysteriesSolved));
+ _mystery.clear();
+ _partner = 0;
+ saveProfile(name);
+ }
return;
}
if (k == Common::KEYCODE_ESCAPE) {
@@ -2264,12 +2276,16 @@ void EEMEngine::doAccuse() {
_music->playMus(5, /*loop=*/false);
playAnm(Common::Path("SCRAPBK.ANI"), 120, true);
- // Auto-save into slot 0 (the engine's quicksave slot).
- const Common::String desc = Common::String::format(
- "%s â solved mystery %u", _playerName.c_str(), mn);
- Common::Error err = saveGameState(0, desc, true);
+ // Mirrors `_SavePlayerRecord` at 1df2:0857 â once the
+ // `_mysteriesSolved` table is updated, the original
+ // immediately persists the player record so the win sticks
+ // even if the player quits before reaching the menu. We do
+ // the same by writing back to the active profile (the slot
+ // keyed on `_playerName`) rather than clobbering slot 0 like
+ // a generic quicksave.
+ const Common::Error err = saveProfile(_playerName);
if (err.getCode() != Common::kNoError)
- warning("auto-save after solve failed: %s",
+ warning("saveProfile after solve failed: %s",
err.getDesc().c_str());
} else {
_mystery._firstTry = false;
Commit: df22a74a072253524335ade26962be591e1c6bb5
https://github.com/scummvm/scummvm/commit/df22a74a072253524335ade26962be591e1c6bb5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:42+02:00
Commit Message:
EEM: improved animations and added missing UI features
Changed paths:
engines/eem/audio.cpp
engines/eem/audio.h
engines/eem/clues.cpp
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/site.cpp
engines/eem/site.h
engines/eem/ui.cpp
diff --git a/engines/eem/audio.cpp b/engines/eem/audio.cpp
index fab34529b71..f4719af0bb1 100644
--- a/engines/eem/audio.cpp
+++ b/engines/eem/audio.cpp
@@ -54,6 +54,17 @@ void AudioPlayer::stopAll() {
// VOC playback --------------------------------------------------------
void AudioPlayer::playVoc(const Common::Path &vocPath) {
+ // Voice / digital audio is gated by the `DAT_2d5d_3f97` flag in
+ // the original â verified at every callsite (`_DoChoosePartner @
+ // 1a35:098c`, `_DisplayClue @ 2404:0845`, `_DoOpeningAnims @
+ // 2520:08a8`, etc.). Setup-screen toggle and `_NewPlayer` fresh-
+ // profile init both rewrite that flag. We pull it into the audio
+ // player so callers don't have to duplicate the check.
+ if (!_voiceEnabled) {
+ debugC(2, kDebugSound, "AudioPlayer: voice disabled, skipping %s",
+ vocPath.toString().c_str());
+ return;
+ }
stopVoice();
// Mirrors `_LoadSoundName`'s `_fopen` (1ff1:02ac).
@@ -189,6 +200,11 @@ void AudioPlayer::playPcmBuffer(byte *pcm, uint32 size, uint sampleRate,
}
void AudioPlayer::spoolSound(uint num) {
+ if (!_voiceEnabled) {
+ debugC(2, kDebugSound,
+ "AudioPlayer: voice disabled, skipping spoolSound(%u)", num);
+ return;
+ }
if (_currentMystery < 0 || num >= _sdxIndex.size()) {
warning("AudioPlayer: spoolSound(%u) â invalid index (%d, %u)",
num, _currentMystery, (uint)_sdxIndex.size());
diff --git a/engines/eem/audio.h b/engines/eem/audio.h
index c9238b0b3f4..921e00e900b 100644
--- a/engines/eem/audio.h
+++ b/engines/eem/audio.h
@@ -73,6 +73,17 @@ public:
explicit AudioPlayer(EEMEngine *vm);
~AudioPlayer();
+ /// Mirrors the gate in every original audio call site
+ /// (`_DisplayClue` 2404:0845, `_DoChoosePartner` 1a35:098c,
+ /// `_DoOpeningAnims` 2520:08a8, `_DisplayCorrect` 1df2:0780, ...)
+ /// â every `_PlayVoice` / `_SpoolSound` is wrapped in
+ /// `if ((DAT_2d5d_3f97 != 0) && (_VoiceAvailable != 0))`. The
+ /// engine pulls `_voiceOn` (= `DAT_2d5d_3f97`) into here so we
+ /// can early-return at the audio boundary instead of duplicating
+ /// the gate at every call site.
+ void setVoiceEnabled(bool enabled) { _voiceEnabled = enabled; }
+ bool voiceEnabled() const { return _voiceEnabled; }
+
// VOC playback ----------------------------------------------------
/// Mirrors `_LoadSoundName` + `_PlayVoice`. Loads the named .VOC
@@ -152,6 +163,7 @@ private:
Common::Array<SoundEntry> _sdxIndex;
Common::Path _sdbPath;
int _currentMystery = -1;
+ bool _voiceEnabled = true;
};
} // End of namespace EEM
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index a3907d32ef3..1ad73525d3d 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -33,6 +33,7 @@
#include "eem/audio.h"
#include "eem/detection.h"
#include "eem/eem.h"
+#include "eem/site.h"
// EEM â clue / briefing pipeline (SCRIPT.C clue parts + KD.C briefing parts).
// Everything that drives a `ClueBlock` through the displayClue / portrait /
@@ -295,15 +296,44 @@ void EEMEngine::doInitClues() {
const uint frameCount = haveGame ? game.size() : 8;
bool skip = false;
for (uint frame = 0; frame < frameCount && !shouldQuit() && !skip; frame++) {
- // Restore BG + advance frame.
+ // Restore BG + advance frame. The original always uses
+ // Jake's anim IDs (0x17/0x18/0x19) as the SCRIPT keys
+ // even when Jenny's CELL data is loaded â verified at
+ // `_DoInitClues @ 1a35:0507`/`0541` where
+ // `_NewAnimation(..., (PicData *)CONCAT22(0x17, ...), ...)`
+ // hard-codes the script index to 0x17. So we look up
+ // `partnerFrameAtTick(0x17, ...)` regardless of partner.
+ // This gives us the correct cadence â book holds on its
+ // "thinking" pose (cell 8) for 16 ticks instead of
+ // flipbook-cycling, and nancy waits 18 ticks before her
+ // late-arrival count-up.
if (_picsArchive.getPicture(0x52, bg))
blitAt(bg, 0, 0);
- if (haveGame)
- blitMaskedToScreen(game[frame % game.size()], 0xcd, 0x6c);
- if (haveBook)
- blitMaskedToScreen(book[frame % book.size()], 0, 99);
- if (haveNancy)
- blitMaskedToScreen(nancy[frame % nancy.size()], 0x68, 0x8b);
+ const uint32 t = frame * 100;
+ // All three briefing anims (game/book/nancy) go through
+ // the original `_NewAnimation` path so per-frame anchors
+ // apply. Use `blitAnimFrameAnchored` against a locked
+ // screen surface so the briefing partner / book / nancy
+ // translate cleanly between cells instead of pinning at
+ // the same top-left.
+ Graphics::Surface *scr = g_system->lockScreen();
+ if (!scr) {
+ skip = true;
+ break;
+ }
+ if (haveGame) {
+ const uint f = partnerFrameAtTick(0x17, (uint)game.size(), t);
+ blitAnimFrameAnchored(scr, game[f], 0xcd, 0x6c);
+ }
+ if (haveBook) {
+ const uint f = partnerFrameAtTick(0x18, (uint)book.size(), t);
+ blitAnimFrameAnchored(scr, book[f], 0, 99);
+ }
+ if (haveNancy) {
+ const uint f = partnerFrameAtTick(0x19, (uint)nancy.size(), t);
+ blitAnimFrameAnchored(scr, nancy[f], 0x68, 0x8b);
+ }
+ g_system->unlockScreen();
g_system->updateScreen();
// Wait 100 ms or until input.
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index cade4889101..fa895d640a6 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -61,10 +61,7 @@ const uint kPalHighScore = 0x27;
// header (description / thumbnail / playtime) is appended/parsed
// separately by `Engine::saveGameState` / `MetaEngine::readSavegameHeader`,
// so we don't need a magic word or our own metadata fields.
-//
-// v1 â initial schema: name + mysteriesSolved + partner +
-// optional mystery sub-state.
-const byte kSaveBodyVer = 1;
+const byte kSaveBodyVer = 3;
// 11x16 mouse cursor â replaces the DOS hardware cursor wired in by
// _InitMouse @ 152d:018b (INT 33h). The original game sets the cursor
@@ -128,6 +125,7 @@ Common::Error EEMEngine::run() {
// which `_AIL_register_driver`s SBDIG.ADV / PASDIG.ADV alongside
// the MIDI driver.
_audio = new AudioPlayer(this);
+ _audio->setVoiceEnabled(_voiceOn);
syncSoundSettings();
// _InitMouse @ 152d:018b in the original â install our 11x16 arrow,
@@ -146,36 +144,31 @@ Common::Error EEMEngine::run() {
// If the user chose "Load" before pressing Play, the framework
// invokes `loadGameState` which sets up `_mystery` and `_partner`.
- // Honour that by skipping the intros and going straight to the
- // loaded mystery's site loop.
+ // Honour that by skipping the intros and dropping into the screen-
+ // driver loop just past the briefing â the loaded mystery already
+ // knows its current site, so we resume at MAP (the original's
+ // post-briefing state, set by handler 0 at 1a35:0e1d).
const int wantedSave = ConfMan.hasKey("save_slot")
? ConfMan.getInt("save_slot") : -1;
+ bool resumed = false;
if (wantedSave >= 0) {
const Common::Error err = loadGameState(wantedSave);
if (err.getCode() == Common::kNoError && _mystery.isLoaded()) {
debugC(1, kDebugGeneral, "Resuming from slot %d at mystery %u",
wantedSave, _mystery.number());
CursorMan.showMouse(true);
- doInitClues();
- // Original screen 0 â screen 1: after the briefing the
- // game opens the map (function at 20fe:120b â _DoBigMap)
- // and only enters a site once the player clicks on one.
- doBigMap();
- if (_mystery.isLoaded())
- doSiteLoop();
- while (!shouldQuit()) {
- doCaseSelection();
- if (!_mystery.isLoaded())
- break;
- doInitClues();
- doBigMap();
- if (_mystery.isLoaded())
- doSiteLoop();
- }
- return Common::kNoError;
+ _nextScreen = kScreenMap;
+ resumed = true;
}
}
+ // Skip the entire intro chain (logos + anims + name entry +
+ // partner pick) when resuming a saved profile â the partner is
+ // already known, the player has already named themselves, and the
+ // loaded mystery's site loop is what they want to see again.
+ if (resumed)
+ goto screen_loop;
+
// Reproduces _DoOpeningAnims @ 2520:082a:
// EA Kids logo (PIC) -> HighScore Productions logo (PIC) ->
// Storm Software logo (BOLT.ANM) -> [music starts] -> 20
@@ -254,36 +247,120 @@ Common::Error EEMEngine::run() {
// per-mystery `_StartTravelMusic` kicks in.
if (_music)
_music->stop();
+ // Profile pick (or fresh creation) â `screen8_handler @ 1c33:1012`.
+ // `doProfilePicker` lists existing profiles via `listProfiles()`
+ // and falls through to `doNewPlayer` if none exist or the user
+ // picks "[New Player]".
if (!shouldQuit())
- doNewPlayer();
+ doProfilePicker();
if (!shouldQuit())
doChoosePartner();
- if (!shouldQuit())
- doCaseSelection();
- if (!shouldQuit() && _mystery.isLoaded()) {
- // Mark the starting site as active and display the case briefing.
- // `_DoInitClues` @ 1a35:0411 â case briefing.
- doInitClues();
- // Original screen 0 â screen 1: after the briefing the game
- // opens the map (function at 20fe:120b â `_DoBigMap`) and only
- // enters a site once the player clicks on one.
- doBigMap();
- if (_mystery.isLoaded())
- doSiteLoop();
- // After a case, loop back to CaseSelection.
- while (!shouldQuit()) {
+ // Now drop into the screen-driver state machine â same pattern as
+ // `_ScreenDriver @ 1a35:0dc1` + the per-screen handlers in the
+ // table at 1a35:0e5e. The original sets `_NextScreen` either
+ // directly (e.g. `_DisplayCorrect` writes 12 = ACTION) or via the
+ // jumptable handlers (e.g. handler 0 calls `_DoInitClues` then
+ // writes 1 = MAP). The handlers here mirror that exactly: each
+ // case body runs the screen and updates `_nextScreen` for the next
+ // iteration. Sentinel `kScreenInvalid` (0xFFFF) ends the loop â
+ // same as the original's table-end marker.
+ //
+ // Initial value `kScreenAction` matches the original flow at the
+ // tail of `_DoChoosePartner @ 1a35:099d` which sets
+ // `_NextScreen = 0xc` once the partner has been picked. (The
+ // resume path above bypasses this and seeds `kScreenMap` instead.)
+ //
+ // Mid-mystery profile resume: if the profile picker loaded a
+ // save whose `hasMystery` flag was set, `_mystery.isLoaded()` is
+ // true here and the player just re-picked their partner. Drop
+ // straight to MAP rather than ACTION so they don't have to walk
+ // back through the case picker (which would `_mystery.load()`
+ // fresh and discard their site / clue progress). The original
+ // has no equivalent â it persists only profile-level state via
+ // `_PlayerRecord`, not in-progress mysteries â so this is a
+ // ScummVM-only ergonomics improvement.
+ if (!shouldQuit() && !resumed)
+ _nextScreen = _mystery.isLoaded() ? kScreenMap : kScreenAction;
+screen_loop:
+ while (!shouldQuit() && _nextScreen != kScreenInvalid) {
+ const ScreenId current = (ScreenId)_nextScreen;
+ debugC(1, kDebugGeneral, "screenDriver: id=%d", (int)current);
+
+ switch (current) {
+ case kScreenAction:
+ // Post-mystery menu. `_ActionScreen` sets _NextScreen via
+ // its action jumptable (1c33:1be1) â see `doActionScreen`.
+ doActionScreen();
+ break;
+
+ case kScreenChooseMystery:
+ // Handler 10 at 1a35:0e0e calls `_DoChooseMystery` which
+ // presets `_NextScreen = 0` (INIT_CLUES) before
+ // `_CaseSelection`. If the picker bails out without
+ // loading a mystery (no `_ReadMystery` call), drop back
+ // to ACTION instead of falling into a missing case.
doCaseSelection();
- if (!_mystery.isLoaded())
- break;
+ _nextScreen = _mystery.isLoaded() ? kScreenInitClues
+ : kScreenAction;
+ break;
+
+ case kScreenInitClues:
+ // Handler 0 at 1a35:0e14 runs `_PreLoad` + `_DoInitClues`
+ // then writes `_NextScreen = 1` (MAP).
doInitClues();
- // Original screen 0 â screen 1: after the briefing the
- // game opens the map (function at 20fe:120b â _DoBigMap)
- // and only enters a site once the player clicks on one.
+ _nextScreen = _mystery.isLoaded() ? kScreenMap
+ : kScreenAction;
+ break;
+
+ case kScreenMap:
+ case kScreenMapAlt:
+ // Handler 1/2 at 1a35:0e25 calls `_DoMapScreen @
+ // 20fe:120b` which manages its own `_NextScreen` writes â
+ // 3 (a site was clicked), 6 (setup), or 0xffff (quit).
+ // Our `doBigMap` keeps the original's "click site, then
+ // enter the site loop" behaviour inline; once it returns
+ // the natural next state is SITE.
doBigMap();
- if (_mystery.isLoaded())
- doSiteLoop();
+ if (!_mystery.isLoaded())
+ _nextScreen = kScreenAction;
+ else if (_nextScreen == current)
+ _nextScreen = kScreenSite;
+ break;
+
+ case kScreenSite:
+ // Handler 3 at 1a35:0e2c calls `_DoSiteLoop @
+ // 168d:03f4`. Our `doSiteLoop` is a complete loop â
+ // notebook / gallery / accuse / map are dispatched
+ // inline within `SiteScreen::run`. The accusation tail
+ // in `doAccuse` (see ui.cpp) writes the next screen:
+ // kScreenAction on win (matches `_DisplayCorrect @
+ // 1df2:0895` writing 0xc) or kScreenSite on lose
+ // (matches `_DisplayAlibi @ 1df2:043f` snapping back
+ // via `_LastScreen`).
+ doSiteLoop();
+ if (!_mystery.isLoaded())
+ _nextScreen = kScreenAction;
+ else if (_nextScreen == current)
+ _nextScreen = kScreenInvalid; // user quit
+ break;
+
+ case kScreenSetup:
+ // Handler 6 at 1a35:0e48 calls `_DoSetup @ 1f78:044e`.
+ // Reachable via the BigMap setup button which writes
+ // `_NextScreen = 6` (verified at 20fe:0c33). The
+ // original sets `_NextScreen = _LastScreen` on entry,
+ // then the toggle UI returns when ESC / Back is hit;
+ // `doSetup` sets `_nextScreen` itself.
+ doSetup();
+ break;
+
+ default:
+ warning("screenDriver: unhandled screen id %d", (int)current);
+ _nextScreen = kScreenInvalid;
+ break;
}
+ _lastScreen = current;
}
debugC(1, kDebugGeneral, "EEM engine exiting");
@@ -564,8 +641,14 @@ Common::Error EEMEngine::saveGameStream(Common::WriteStream *stream,
bool isAutosave) {
(void)isAutosave;
+ // Body header: one byte version. `Common::Serializer::setVersion`
+ // alone doesn't write/read the version â we emit it explicitly so
+ // `loadGameStream` knows which fields are present. Older saves
+ // (v1) lack `_chainStage`; newer ones include it.
Common::Serializer s(nullptr, stream);
s.setVersion(kSaveBodyVer);
+ byte ver = kSaveBodyVer;
+ s.syncAsByte(ver);
// Profile-level state â mirrors the original `_PlayerRecord` body
// at `2d5d:3f6a` (159 bytes, written by `_SavePlayerRecord @
@@ -590,6 +673,10 @@ Common::Error EEMEngine::saveGameStream(Common::WriteStream *stream,
s.syncString(_playerName);
s.syncBytes(_mysteriesSolved, sizeof(_mysteriesSolved));
s.syncAsByte(_partner);
+ // v2+: chain-stage tier (1=Junior, 2=Senior, 3=Master).
+ s.syncAsByte(_chainStage);
+ // v3+: voice on/off flag (DAT_2d5d_3f97).
+ s.syncAsByte(_voiceOn);
// ScummVM-only extension: persist the in-progress mystery so the
// player can resume mid-case. The original engine has no such
@@ -605,8 +692,8 @@ Common::Error EEMEngine::saveGameStream(Common::WriteStream *stream,
}
debugC(1, kDebugGeneral,
- "Saved profile name=%s partner=%u mystery=%d autosave=%d",
- _playerName.c_str(), _partner,
+ "Saved profile name=%s partner=%u stage=%u mystery=%d autosave=%d",
+ _playerName.c_str(), _partner, _chainStage,
hasMystery ? (int)_mystery.number() : -1,
isAutosave ? 1 : 0);
return Common::kNoError;
@@ -614,7 +701,14 @@ Common::Error EEMEngine::saveGameStream(Common::WriteStream *stream,
Common::Error EEMEngine::loadGameStream(Common::SeekableReadStream *stream) {
Common::Serializer s(stream, nullptr);
- s.setVersion(kSaveBodyVer);
+ byte ver = 0;
+ s.syncAsByte(ver);
+ if (ver > kSaveBodyVer) {
+ warning("loadGameStream: save body version %u newer than %u â refusing",
+ ver, kSaveBodyVer);
+ return Common::kReadingFailed;
+ }
+ s.setVersion(ver);
s.syncString(_playerName);
if (_playerName.empty())
@@ -622,6 +716,10 @@ Common::Error EEMEngine::loadGameStream(Common::SeekableReadStream *stream) {
s.syncBytes(_mysteriesSolved, sizeof(_mysteriesSolved));
s.syncAsByte(_partner);
+ s.syncAsByte(_chainStage);
+ s.syncAsByte(_voiceOn);
+ if (_audio)
+ _audio->setVoiceEnabled(_voiceOn);
bool hasMystery = false;
s.syncAsByte(hasMystery);
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index f1aa061d57d..e97bd592712 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -46,14 +46,59 @@ class AudioPlayer;
class MusicPlayer;
/**
- * Screen IDs used by the original ScreenDriver dispatch table at 1a35:0e5e.
- * The table holds 14 (id, handler) entries; the loop iterates until it finds
- * a matching id and calls its handler. ID 0xFFFF is the exit sentinel.
+ * Screen IDs used by the original `_ScreenDriver` dispatch table at
+ * 1a35:0e5e (and the fallback at 1a35:0e54). 14 entries total: each
+ * one is a screen ID + a near function pointer at offset +0x1c. The
+ * driver does `JMP word ptr CS:[BX + 0x1c]`, so handlers tail-call â
+ * each one runs the screen and updates `_NextScreen` for the next
+ * trip through the dispatcher.
+ *
+ * IDs and their handlers (verified from disassembly at 1a35:0dec..0e4f):
+ *
+ * 0 INIT_CLUES â `_PreLoad` + `_DoInitClues`, sets _NextScreen=1
+ * 1 MAP â `_DoMapScreen` @ 20fe:120b (sets _NextScreen
+ * inside; 3 = a site was clicked, 6 = setup, etc.)
+ * 2 MAP â same handler as 1 (alternate entry, used when
+ * `_LastScreen == 2` to swap the briefcase anim)
+ * 3 SITE â `_DoSiteLoop` @ 168d:03f4
+ * 4 NOTEBOOK â `_DoNotebook` @ 161e:0500
+ * 5 GALLERY â `_DoGallery` @ 158f:065b
+ * 6 SETUP â `_DoSetup` @ 1f78:044e
+ * 7 ACCUSE â `_DoAccuse` @ 1df2:0bdd (win â 12, lose â last)
+ * 8 PROFILE â `screen8_handler` @ 1c33:1012; tail sets =9
+ * 9 PARTNER â `_DoChoosePartner` @ 1a35:0756; sets =0xc inside
+ * 10 (0xa) CHOOSE_MYSTERY â `_DoChooseMystery` + `_CaseSelection`;
+ * starts with _NextScreen=0 so a successful pick
+ * falls through to INIT_CLUES.
+ * 11 (0xb) TITLE â set _NextScreen=8 then dispatch (TITLE.ANM is
+ * actually shown earlier by `_DoOpeningAnims`, this
+ * handler is the post-intro "fall into PROFILE"
+ * redirect)
+ * 12 (0xc) ACTION â `_ActionScreen` @ 1c33:195b â post-mystery menu
+ * ("Solve a Mystery", scrapbook, more mysteries,
+ * setup). Action 1 sets =10 (CHOOSE_MYSTERY).
+ * 0xFFFF SENTINEL â exit loop
+ *
+ * Screen-driver state writes verified via xrefs to `_NextScreen @
+ * 2d5d:3f26`: `_DisplayCorrect` writes 0xc (winner returns to ACTION),
+ * `_DisplayAlibi` writes `_LastScreen` (loser snaps back), and
+ * `_DoSiteLoop` writes 1/3/4 plus 0xffff on ESC.
*/
enum ScreenId {
- kScreenInvalid = 0xFFFF,
- kScreenChoosePartner = 0x09, ///< _DoChoosePartner @ 1a35:0756 (boy/girl picker)
- kScreenTitle = 0x0B ///< _ShowTitlePage @ 1a35:06b7
+ kScreenInvalid = 0xFFFF,
+ kScreenInitClues = 0x00,
+ kScreenMap = 0x01,
+ kScreenMapAlt = 0x02,
+ kScreenSite = 0x03,
+ kScreenNotebook = 0x04,
+ kScreenGallery = 0x05,
+ kScreenSetup = 0x06,
+ kScreenAccuse = 0x07,
+ kScreenProfile = 0x08,
+ kScreenChoosePartner = 0x09,
+ kScreenChooseMystery = 0x0A,
+ kScreenTitle = 0x0B,
+ kScreenAction = 0x0C
};
class EEMEngine : public Engine {
@@ -268,11 +313,55 @@ private:
// Screen handlers â port targets in screens/ later.
void showEAKidsLogo();
void showHighScoreLogo();
+
+ /// Profile selector â mirrors `screen8_handler @ 1c33:1012`.
+ /// Walks `listProfiles()`, draws the list of existing profile
+ /// names plus a "[New Player]" entry, and either calls
+ /// `loadProfile(name)` on a click or falls through to
+ /// `doNewPlayer()` if the user picks "New". When no profiles
+ /// exist, behaves identically to `doNewPlayer()` (the original
+ /// also bypasses the picker when `local_20 == 0` â see
+ /// 1c33:1170: `if (saves == 0) _NewPlayer();`).
+ void doProfilePicker();
void doNewPlayer(); ///< Mirrors `_NewPlayer` @ 1c33:0dda
void doChoosePartner();
+
+ /// Display the per-mystery ending pages from `E<num>.BIN`.
+ /// Mirrors `_DisplayEnding @ 1df2:0548` + `_DisplayEndingPage
+ /// @ 1df2:044c`. The file format is a 2-byte page count followed
+ /// by N pages, each `{ u16 picNum, u16 x1, u16 y1, u16 x2, u16 y2,
+ /// char text[] (null-terminated, ParseString placeholders) }`.
+ /// Blocks until the player clicks past the last page or hits ESC.
+ /// `_ShowOneScrap @ 1f78:0773` is just `_DisplayEnding(num, 1)`,
+ /// so this same call covers the post-mystery scrapbook view from
+ /// the action menu.
+ void doShowEnding(uint num);
void doCaseSelection();
void doSiteLoop();
+ /// Post-mystery action menu. Mirrors `_ActionScreen @ 1c33:195b` â
+ /// the screen the original returns to after `_DisplayCorrect`
+ /// (winner) or after the player explicitly leaves a case via the
+ /// PROFILE â PARTNER chain. The original offers up to 5 choices
+ /// gated on the player's chain stage (`DAT_2d5d_3f99`):
+ /// 1: "Solve a Mystery" (set _NextScreen=10 â CHOOSE_MYSTERY)
+ /// 3: replay the last solved case (`_ReloadMystery(0)` callsite)
+ /// 5: scrapbook viewer (`_ShowOneScrap(0, 1)` callsite)
+ /// 7: chain-stage advance (cmp `_3f99 == 1`)
+ /// 9: chain-stage advance (cmp `_3f99 == 2` / `== 3`)
+ /// sentinel: exit / back to PARTNER
+ /// We start with just option 1 wired (the practical loop) plus
+ /// quit; the others slot in as their underlying screens land.
+ void doActionScreen();
+
+ /// Setup / preferences screen. Mirrors `_DoSetup @ 1f78:044e` â
+ /// per-profile preferences (voice on/off via `DAT_2d5d_3f97`,
+ /// partner pick via SwapColors on Kid1/Kid2 rects). Reachable
+ /// from BigMap's setup button (sets `_NextScreen = 6` per
+ /// `_DoBigMap @ 20fe:0c33`). Returns to whatever
+ /// `_lastScreen` was â typically MAP.
+ void doSetup();
+
/// Render the case briefing background + game/book decorations and
/// display the briefing ClueBlock. Mirrors `_DoInitClues` @ 1a35:0411
/// minus the live ANI sequence playback.
@@ -300,6 +389,28 @@ private:
/// original `_DisplayCorrect` flow.
uint8 _mysteriesSolved[55] = {};
+ /// Current chain/tier the player is at â mirrors `DAT_2d5d_3f99`
+ /// (`_PlayerRecord +0x2f`):
+ /// 1 = Junior detective (mysteries 1 .. 24, "A chain")
+ /// 2 = Senior detective (mysteries 25 .. 48, "B chain")
+ /// 3 = Master detective (mysteries 49 .. 54, "C chain")
+ /// Initialized to 1 in `_NewPlayer @ 1c33:0fa3` and bumped by
+ /// `_DisplayCorrect @ 1df2:0853` once every mystery in the current
+ /// tier is solved (range checks at 1df2:080d / 0824 / 0837). The
+ /// value also gates `_CaseSelection`'s book label and selection
+ /// list (1c33:0a87 onwards).
+ uint8 _chainStage = 1;
+
+ /// Voice / digital-audio enable flag. Mirrors `DAT_2d5d_3f97`
+ /// (`_PlayerRecord +0x2d`). Set to 1 by `_NewPlayer @ 1c33:0fa3`,
+ /// toggled by the SoundOn / SoundOff hot-rects in `_DoSetup @
+ /// 1f78:044e` (verified at `_SetupSettings` 1f78:0076 reading
+ /// the same byte to colour the on/off labels). Gates every
+ /// `_PlayVoice` and `_SpoolSound` call site (clue voices,
+ /// partner speech, intro VO etc. â see `_DoChoosePartner`,
+ /// `_DisplayClue`, `_SayKDDigital` xrefs).
+ bool _voiceOn = true;
+
Common::RandomSource _rng;
DBDArchive _picsArchive; ///< PICS.DBD/.DBX (sprites, buttons, frame backgrounds)
@@ -347,6 +458,12 @@ private:
/// 202f:05cb`). Constructed alongside `_music` in `run()`.
public:
AudioPlayer *_audio = nullptr;
+
+ /// Public setter for `_nextScreen` so site loop / inline screens
+ /// can drive the screen-driver state machine without making the
+ /// member itself public. Mirrors the original's direct write to
+ /// `_NextScreen @ 2d5d:3f26` from anywhere in the engine.
+ void setNextScreen(ScreenId s) { _nextScreen = s; }
};
} // End of namespace EEM
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index e6122b5d6eb..42c5429d44e 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -65,6 +65,12 @@ void blitFrame(Graphics::ManagedSurface &dst, const Picture &p,
// and `renderStaticDrops`.
void blitMaskedSurface(Graphics::Surface *screen, const Picture &p,
int x, int y) {
+ // Top-left semantics. Used for static drops (`_AddDrop @
+ // 172b:1a77` which calls `_Rect_Move_Mask(..., x, y, ...)` with
+ // the raw (x, y) and ignores per-frame anchors) and any other
+ // non-animated overlay. Animation rendering should go through
+ // `blitAnimFrameAnchored` instead so per-frame anchor offsets
+ // (miscflags = X, rowoff = Y) apply correctly.
if (!screen)
return;
const byte transp = (byte)(p.flags >> 8);
@@ -84,6 +90,40 @@ void blitMaskedSurface(Graphics::Surface *screen, const Picture &p,
}
}
+void blitAnimFrameAnchored(Graphics::Surface *screen, const Picture &p,
+ int anchorX, int anchorY) {
+ // `_UpdateAnimations @ 172b:09c1` blits each animation frame at
+ // `(anchor_x - puVar5[4], anchor_y - puVar5[3])` where puVar5[3]/[4]
+ // are the per-frame `rowoff` / `miscflags` values from the
+ // 16-byte PicData header. Both are SIGNED 16-bit anchor offsets
+ // â when frames have varying anchors (anim 0x14 BigMap walk-
+ // cycle has miscflags = -2 per cell, anim 0x07 has rowoff up to
+ // 61), the sprite actually translates across the screen as it
+ // cycles through cells. Without this, the partner "shakes in
+ // place" instead of walking. (Transparency still comes from
+ // `flags >> 8`, verified at the `_Rect_Move_Mask(..., *thePic >>
+ // 8)` call â NOT from miscflags as an earlier comment claimed.)
+ if (!screen)
+ return;
+ const int blitX = anchorX - (int)(int16)p.miscflags;
+ const int blitY = anchorY - (int)(int16)p.rowoff;
+ const byte transp = (byte)(p.flags >> 8);
+ for (int row = 0; row < p.surface.h; row++) {
+ const int dstY = blitY + row;
+ if (dstY < 0 || dstY >= screen->h)
+ continue;
+ const byte *src = (const byte *)p.surface.getBasePtr(0, row);
+ byte *dst = (byte *)screen->getBasePtr(0, dstY);
+ for (int col = 0; col < p.surface.w; col++) {
+ const int dstX = blitX + col;
+ if (dstX < 0 || dstX >= screen->w)
+ continue;
+ if (src[col] != transp)
+ dst[dstX] = src[col];
+ }
+ }
+}
+
// Rotate one VGA palette range by one slot. Mirrors `_ColorCycle @
// 172b:2015` â used by both the per-site Loop-1 ColorCycle entries and
// the always-on hotspot marching-ants range 0xF9..0xFE.
@@ -136,40 +176,287 @@ const uint16 kKdAnimTable[6][6] = {
{ 0x06, 0x06, 6, 6, 80, 80 }, // 5 â same anim both partners
};
-// Sequence-script lookup. Entries copied verbatim from
-// `_AnimationSequences @ 29be:22d4` walked through to the next 0x80.
-// Each script is a u16[] of frame indices terminated by 0x80; we
-// don't yet handle 0x81 jumps (none of the kdAnim sequences use
-// them â verified). seqnum == animId for these calls (per
-// `_PlayAnimation` 172b:1f5d push order).
-struct KdScript {
+// Animation script table. Mirrors `_AnimationSequences @ 29be:22d4`
+// (a 55-entry table of far ptrs, each pointing to a u16-frame-index
+// stream terminated by 0x80; 0x81 marks a jump that we don't see in
+// the partner subset and so don't yet implement).
+//
+// `_NewAnimation @ 172b:06e1` reads the script via
+// `_AnimationSequences[anim_id]` and stores the pointer in
+// `DAT_2d5d_3eaf[i*0xb]`. `_UpdateAnimations @ 172b:09c1` then walks
+// it one entry per `_CheckFrameRate` tick (~100 ms): the value at
+// `script[index]` is the frame to render; 0x80 resets index to 0
+// (loop). So a script like `[0,0,0,0,0,0,0,0,0,2]` renders nine ticks
+// of frame 0 then one tick of frame 2 â the natural "blink with long
+// idle hold" cadence.
+//
+// We use the same scripts for the wait anims (`renderPartner`) AND
+// the kd-clue reaction anims (`playKdAnim`), since both call
+// `_NewAnimation` in the original â only the state field differs (1
+// = looping, 4 = one-shot). seqnum == animId per `_PlayAnimation`
+// 172b:1f5d push order.
+//
+// Each entry was read directly from the EXE via Ghidra; cross-checked
+// against `_NewAnimation`'s read. Frame counts include only the
+// playable frames (the trailing 0x80 is the terminator, not a frame).
+struct AnimScript {
uint16 seqnum;
uint8 len;
- uint8 frames[20]; // long enough for any kdAnim script
+ uint8 frames[28]; // longest partner-subset script is 26 frames (anim 2)
};
-const KdScript kKdScripts[] = {
- // seqnum 1 (29be:188a) â head bob
+const AnimScript kAnimScripts[] = {
+ // 0x00 (29be:185e) â Jake speaker-0 wait: nine idle, one blink, loop
+ { 0x00, 10, { 0,0,0,0,0,0,0,0,0,2 } },
+ // 0x01 (29be:188a) â Jake PDA idle: alternating head bob with peak
{ 0x01, 15, { 0,1,2,0,1,0,2,1,0,1,0,1,2,1,0 } },
- // seqnum 2 (29be:18aa) â short blip then long pause
- { 0x02, 16, { 0,1,2,1,0,0,0,0,0,0,0,0,0,0,0,0 } },
- // seqnum 3 (29be:18e0) â Jake "lift, hold, lower" gesture
+ // 0x02 (29be:18aa) â Jake gallery: brief wave, long hold, second
+ // wave, hold (CONFIRMED 26 frames â earlier table was truncated to
+ // 16 which dropped the second wave cycle).
+ { 0x02, 26, { 0,1,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,1,0,0,0,0,0,0 } },
+ // 0x03 (29be:18e0) â Jake "lift, hold, lower" gesture
{ 0x03, 9, { 0,1,2,3,2,2,2,1,0 } },
- // seqnum 4 (29be:18f4) â bigger gesture (camera flash-style)
+ // 0x04 (29be:18f4) â Jake bigger gesture (camera flash-style)
{ 0x04, 13, { 0,1,2,3,4,5,4,4,4,3,2,1,0 } },
- // seqnum 5 (29be:1910) â held idle with a single peak
+ // 0x05 (29be:1910) â Jake/Jenny shared (speaker 5): held idle, peak
{ 0x05, 13, { 0,0,0,1,2,3,2,1,0,0,0,0,0 } },
- // seqnum 6 (29be:192c) â empty (immediate END)
+ // 0x06 (29be:192c) â speaker 6 partner: empty (immediate END,
+ // renders nothing â verified at 29be:192c byte 0 = 0x80)
{ 0x06, 0, { 0 } },
- // seqnum 0xb (29be:188a, same as 1) â Jenny PDA idle
+ // 0x07 (29be:192e) â Jake walk-cycle (10 frames: 0..9)
+ { 0x07, 10, { 0,1,2,3,4,5,6,7,8,9 } },
+ // 0x08 (29be:1944) â Jake stand-still with very late blink
+ { 0x08, 8, { 0,0,0,0,0,0,0,1 } },
+ // 0x09 (29be:1956) â short blip animation
+ { 0x09, 9, { 0,0,0,1,0,0,0,0,0 } },
+ // 0x0a â Jenny speaker-0 wait (alias of 0x00 in the binary)
+ { 0x0a, 10, { 0,0,0,0,0,0,0,0,0,2 } },
+ // 0x0b â Jenny PDA idle (alias of 0x01)
{ 0x0b, 15, { 0,1,2,0,1,0,2,1,0,1,0,1,2,1,0 } },
- // seqnum 0xc (29be:18e0, same as 3) â Jenny "take a picture"
+ // 0x0c â Jenny "take a picture" (alias of 0x03)
{ 0x0c, 9, { 0,1,2,3,2,2,2,1,0 } },
- // seqnum 0xd (29be:18f4, same as 4) â Jenny big gesture
+ // 0x0d â Jenny big gesture (alias of 0x04)
{ 0x0d, 13, { 0,1,2,3,4,5,4,4,4,3,2,1,0 } },
- // seqnum 0x10 (29be:1956) â Jenny short anim
+ // 0x0e â alias of 0x06 (empty)
+ { 0x0e, 0, { 0 } },
+ // 0x0f â alias of 0x09
+ { 0x0f, 9, { 0,0,0,1,0,0,0,0,0 } },
+ // 0x10 â Jenny gallery wait (alias of 0x09 â verified at 29be:1956)
{ 0x10, 9, { 0,0,0,1,0,0,0,0,0 } },
+ // 0x11 (29be:1992) â Jenny entrance count-up: 0..7. Used by
+ // `_DoBigMap` when `_LastScreen == 2` (BigMap entrance one-shot).
+ { 0x11, 8, { 0,1,2,3,4,5,6,7 } },
+ // 0x12 (29be:197e) â Jake entrance count-down 8..0. Used by
+ // `_DoBigMap` (entrance one-shot, partner-specific exit cell).
+ { 0x12, 9, { 8,7,6,5,4,3,2,1,0 } },
+ // 0x13 (29be:1992, alias of 0x11) â Jake walk-cycle 0..7,
+ // looped during BigMap idle.
+ { 0x13, 8, { 0,1,2,3,4,5,6,7 } },
+ // 0x14 (29be:196a) â BigMap idle walk-cycle 0..8 (9 cells),
+ // partner shifts feet while you pick a site.
+ { 0x14, 9, { 0,1,2,3,4,5,6,7,8 } },
+ // 0x15 (29be:185e, alias of 0x00) â Jake CaseSelection greeter:
+ // nine idle, one blink, loop. Same blink cadence as the site
+ // loop's wait anim (animID 0x00).
+ { 0x15, 10, { 0,0,0,0,0,0,0,0,0,2 } },
+ // 0x16 (29be:185e, alias of 0x00) â Jenny CaseSelection greeter,
+ // same blink script as 0x15.
+ { 0x16, 10, { 0,0,0,0,0,0,0,0,0,2 } },
+ // Briefing animations â `_DoInitClues @ 1a35:0411` calls
+ // `_NewAnimation(..., (PicData *)CONCAT22(0x17, ...), 1, ...)`
+ // for the game animation (always anim ID 0x17 â even Jenny's
+ // briefing reuses Jake's SCRIPT, even though the loaded ANI.DBD
+ // cells come from her partner-specific entry 0x3b). Same pattern
+ // for book (0x18 always) and nancy (0x19 always).
+ //
+ // AnimScript len was 28 â these scripts overflow that. Bump
+ // `frames[]` is fine because we just need to fit 30 frames per
+ // briefing entry. We size `frames` to 36 so all five scripts fit
+ // (longest is 0x18 at 30 frames).
+};
+static_assert(true, "see kAnimScriptsLong below for >28-frame scripts");
+
+// Scripts longer than 28 frames live here so the main `kAnimScripts`
+// table can keep its tight `frames[28]` storage (the lookup in
+// `findAnimScript` checks both arrays). Used for the briefing
+// animations whose original scripts run 30 frames each.
+struct AnimScriptLong {
+ uint16 seqnum;
+ uint8 len;
+ uint8 frames[36];
+};
+const AnimScriptLong kAnimScriptsLong[] = {
+ // 0x17 (29be:221a) â briefing game count-up 0..29 (30 frames),
+ // drives the per-tick frame walk of the game piece animation
+ // during `_DoInitClues`.
+ { 0x17, 30, { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,
+ 20,21,22,23,24,25,26,27,28,29 } },
+ // 0x18 (29be:2296) â briefing book: counts up to cell 8 then
+ // holds for 16 ticks (the "thinking" pose) then count up 9..15.
+ { 0x18, 30, { 0,1,2,3,4,5,6,7,8,8,8,8,8,8,8,8,
+ 8,8,8,8,8,8,8,9,10,11,12,13,14,15 } },
+ // 0x19 (29be:2258) â briefing nancy: 18 idle ticks then
+ // count-up 1..12 (the late-arriving sidekick pose).
+ { 0x19, 30, { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+ 1,2,3,4,5,6,7,8,9,10,11,12 } },
};
+// Look up the script for `seqnum`. Returns the frame array + length,
+// or `(nullptr, 0)` if no script is known â caller falls back to
+// flipbook cycling so unknown anims still animate (just without idle
+// holds).
+struct AnimScriptRef {
+ const uint8 *frames;
+ uint8 len;
+};
+static AnimScriptRef findAnimScript(uint16 seqnum) {
+ for (uint i = 0; i < ARRAYSIZE(kAnimScripts); i++) {
+ if (kAnimScripts[i].seqnum == seqnum) {
+ AnimScriptRef r;
+ r.frames = kAnimScripts[i].frames;
+ r.len = kAnimScripts[i].len;
+ return r;
+ }
+ }
+ for (uint i = 0; i < ARRAYSIZE(kAnimScriptsLong); i++) {
+ if (kAnimScriptsLong[i].seqnum == seqnum) {
+ AnimScriptRef r;
+ r.frames = kAnimScriptsLong[i].frames;
+ r.len = kAnimScriptsLong[i].len;
+ return r;
+ }
+ }
+ AnimScriptRef r;
+ r.frames = nullptr;
+ r.len = 0;
+ return r;
+}
+
+void auditPartnerAnims(EEMEngine *vm) {
+ // Cross-check every registered partner-subset script against the
+ // underlying ANI.DBD entry it references. If the script asks for
+ // a frame past the anim's actual frame count, the visible result
+ // is "missing frames" â that's the user-reported symptom we
+ // want to catch and fix here, not paper over with a clamp.
+ if (!vm)
+ return;
+ DBDArchive &ani = vm->getAni();
+
+ // Helper: audit one (id, frames, len) tuple â log a warning if
+ // the script asks for a frame the ANI.DBD entry doesn't have.
+ struct Walker {
+ static void check(DBDArchive &ani, uint16 id, const uint8 *frames, uint8 len) {
+ if (len == 0)
+ return;
+ Animation a;
+ if (!ani.loadAnimation(id, a) || a.empty()) {
+ debugC(1, kDebugSite,
+ "auditPartnerAnims: anim 0x%02x failed to load", id);
+ return;
+ }
+ uint maxRequested = 0;
+ for (uint j = 0; j < len; j++)
+ if (frames[j] > maxRequested)
+ maxRequested = frames[j];
+ if (maxRequested >= a.size()) {
+ warning("anim 0x%02x: script wants frame %u but ANI.DBD has "
+ "only %u â frames will be clamped (verify script "
+ "reading from `_AnimationSequences[0x%02x]` against "
+ "Ghidra)",
+ id, maxRequested, (uint)a.size(), id);
+ } else {
+ debugC(2, kDebugSite,
+ "anim 0x%02x: %u cells, script max=%u, len=%u",
+ id, (uint)a.size(), maxRequested, len);
+ }
+ }
+ };
+
+ for (uint i = 0; i < ARRAYSIZE(kAnimScripts); i++)
+ Walker::check(ani, kAnimScripts[i].seqnum,
+ kAnimScripts[i].frames, kAnimScripts[i].len);
+ for (uint i = 0; i < ARRAYSIZE(kAnimScriptsLong); i++)
+ Walker::check(ani, kAnimScriptsLong[i].seqnum,
+ kAnimScriptsLong[i].frames, kAnimScriptsLong[i].len);
+
+ // Per-frame anchor-offset audit. The original `_UpdateAnimations
+ // @ 172b:09c1` blits each frame at `(anchor_x - frame.miscflags,
+ // anchor_y - frame.rowoff)`. Our `blitMaskedSurface` ignores
+ // those offsets â it treats the WaitAnims (anchor_x, anchor_y)
+ // as the top-left. That's fine when every frame has
+ // `miscflags == 0 && rowoff == 0`; if any frame has a non-zero
+ // anchor, the partner sprite jumps between cells. Log the
+ // offending IDs so we know whether to plumb anchors through
+ // `partnerFrameAtTick` callers.
+ // Audit covers every animID we register a script for â the
+ // partner-subset (0x00..0x16, used by the wait anims, kd-clue
+ // reactions, BigMap, Notebook, Gallery, CaseSelection greeter)
+ // AND the briefing-subset (0x17..0x19, the
+ // game/book/nancy anims driven by `_DoInitClues @ 1a35:0411`).
+ const uint16 partnerIds[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
+ 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c,
+ 0x0d, 0x0f, 0x10, 0x11, 0x12, 0x13,
+ 0x14, 0x15, 0x16, 0x17, 0x18, 0x19 };
+ for (uint i = 0; i < ARRAYSIZE(partnerIds); i++) {
+ const uint16 id = partnerIds[i];
+ Animation a;
+ if (!ani.loadAnimation(id, a) || a.empty())
+ continue;
+ bool anyAnchor = false;
+ int rowMin = 0, rowMax = 0, miscMin = 0, miscMax = 0;
+ for (uint f = 0; f < a.size(); f++) {
+ const Picture &fr = a[f];
+ const int sRow = (int)(int16)fr.rowoff;
+ const int sMisc = (int)(int16)fr.miscflags;
+ if (sRow != 0 || sMisc != 0) {
+ if (!anyAnchor) {
+ rowMin = rowMax = sRow;
+ miscMin = miscMax = sMisc;
+ anyAnchor = true;
+ } else {
+ rowMin = MIN(rowMin, sRow);
+ rowMax = MAX(rowMax, sRow);
+ miscMin = MIN(miscMin, sMisc);
+ miscMax = MAX(miscMax, sMisc);
+ }
+ }
+ }
+ if (anyAnchor) {
+ // `_UpdateAnimations @ 172b:09c1` reads these as signed
+ // 16-bit values via `puVar5[3]/[4]`, so log them with
+ // the sign preserved â earlier the log printed unsigned
+ // 65534 instead of -2.
+ debugC(1, kDebugSite,
+ "anim 0x%02x: per-frame anchor (rowoff [%d..%d], "
+ "miscflags [%d..%d]) â handled by "
+ "`blitAnimFrameAnchored`",
+ id, rowMin, rowMax, miscMin, miscMax);
+ }
+ }
+}
+
+// Pick the frame index to render at `tickMs` for the looping
+// animation `seqnum` whose underlying ANI.DBD entry has `numFrames`
+// frames. Mirrors the looping path of `_UpdateAnimations`: walk the
+// script one entry per ~100 ms `_CheckFrameRate` tick, wrap on the
+// 0x80 terminator. Exposed (non-static) so the BigMap, CaseSelection
+// greeter, Notebook, and Gallery render paths in `ui.cpp` can use the
+// same cadence â without this, every off-site partner rendering
+// flipbook-cycles ALL cells of the ANI entry (no idle holds, no
+// timing variations) which is the user-reported "constantly looping"
+// symptom.
+uint partnerFrameAtTick(uint16 seqnum, uint numFrames, uint32 tickMs) {
+ const AnimScriptRef s = findAnimScript(seqnum);
+ const uint kFramePeriodMs = 100;
+ if (!s.frames || s.len == 0)
+ return numFrames > 0 ? (uint)((tickMs / kFramePeriodMs) % numFrames) : 0;
+ const uint scriptIdx = (uint)((tickMs / kFramePeriodMs) % s.len);
+ const uint frame = s.frames[scriptIdx];
+ // The script can in theory request a frame that's outside the
+ // animation's actual frame count (a misencoded script). Clamp so
+ // we don't read past `anim[]` in the caller.
+ return (numFrames > 0) ? MIN<uint>(frame, numFrames - 1) : 0;
+}
+
void SiteScreen::enter(uint siteNum) {
if (!_mystery || !_mystery->isLoaded()) {
warning("SiteScreen::enter: no mystery loaded");
@@ -181,6 +468,13 @@ void SiteScreen::enter(uint siteNum) {
return;
}
+ // Reset the wait-anim phase so the partner starts fresh from
+ // script[0] when entering. Mirrors `_DoSiteLoop @ 168d:0436`
+ // where `_NewAnimation` sets the new slot's frame index to
+ // 0xffff (= -1, becomes 0 on the first `_UpdateAnimations`
+ // tick).
+ _waitPhaseAnchor = g_system->getMillis();
+
// Capture whether this is the first time the player enters this
// site BEFORE we mark it visited â `_DoSiteLoop @ 168d:03f4`
// uses the same check to decide whether to play the arrival
@@ -347,8 +641,18 @@ void SiteScreen::run() {
case Common::EVENT_KEYDOWN:
switch (event.kbd.keycode) {
case Common::KEYCODE_ESCAPE:
- if (_vm->areYouSure())
+ if (_vm->areYouSure()) {
+ // Mirrors `_DoSiteLoop @ 168d:07b7` ESC path:
+ // `_NextScreen = 1` (back to MAP) after the
+ // areYouSure confirm. Without explicitly
+ // writing it here, the run() loop would see
+ // _nextScreen unchanged and treat it as a
+ // quit-engine signal â abandoning the case.
+ // Going to MAP keeps the case alive so the
+ // player can continue from a different site.
+ _vm->setNextScreen(kScreenMap);
return;
+ }
enter(cur);
break;
case Common::KEYCODE_m:
@@ -646,8 +950,6 @@ void SiteScreen::renderAnimatedDrops(uint siteNum, uint32 tickMs) {
if (!screen)
return;
- const uint32 kFramePeriodMs = 100; // ~10 FPS, in line with `_CheckFrameRate`.
-
for (uint i = 0; i < numAnims; i++) {
const uint dropOff = 0x48 + i * 6;
const int16 animId = (int16)READ_LE_UINT16(site + dropOff + 0);
@@ -658,8 +960,12 @@ void SiteScreen::renderAnimatedDrops(uint siteNum, uint32 tickMs) {
Animation anim;
if (!_vm->getAni().loadAnimation((uint)animId, anim) || anim.empty())
continue;
- const uint frameIdx = (uint)((tickMs / kFramePeriodMs) % anim.size());
- blitMaskedSurface(screen, anim[frameIdx], x, y);
+ const uint frameIdx = partnerFrameAtTick((uint16)animId,
+ (uint)anim.size(), tickMs);
+ // Animated drops go through `_NewAnimation` in the original,
+ // so `_UpdateAnimations` applies per-frame anchor offsets â
+ // route through the anchored blitter.
+ blitAnimFrameAnchored(screen, anim[frameIdx], x, y);
}
g_system->unlockScreen();
@@ -763,16 +1069,31 @@ void SiteScreen::renderPartner(uint siteNum, uint32 tickMs) {
if (!_vm->getAni().loadAnimation(animId, anim) || anim.empty())
return;
- // `_UpdateAnimations @ 172b:09c1` advances the partner's frame
- // every `_CheckFrameRate` tick. We pick the frame from a global
- // 100 ms clock so the partner cycles in sync with the animated
- // drops.
- const uint32 kFramePeriodMs = 100;
- const uint frameIdx = (uint)((tickMs / kFramePeriodMs) % anim.size());
+ // `_UpdateAnimations @ 172b:09c1` walks the per-anim script (from
+ // `_AnimationSequences[seqnum]`) one entry per `_CheckFrameRate`
+ // tick (~100 ms): render `script[index]`, advance, wrap on 0x80.
+ // That's how the original gets long idle holds with brief blinks
+ // â naive flipbook cycling (`tick % nFrames`) loses those pauses
+ // and makes the partner constantly fidget. `partnerFrameAt` picks
+ // the right frame; if no script is registered for this anim it
+ // falls back to flipbook so unknown anims still move.
+ // Use the relative phase anchor instead of the raw `tickMs` so
+ // the wait anim resumes from script[0] after each kdAnim
+ // one-shot ends â matching the original's `_PlayAnimation @
+ // 172b:1f5d` resetting the resumed slot's frame index to
+ // 0xffff. Without this, the wait anim snaps mid-cycle every
+ // time we return from a clue display.
+ const uint32 elapsed = (tickMs >= _waitPhaseAnchor)
+ ? (tickMs - _waitPhaseAnchor)
+ : tickMs;
+ const uint frameIdx = partnerFrameAtTick((uint16)animId,
+ (uint)anim.size(), elapsed);
Graphics::Surface *screen = g_system->lockScreen();
if (!screen)
return;
- blitMaskedSurface(screen, anim[frameIdx], x, y);
+ // Partner sprite â anchor-aware blit so per-frame `miscflags` /
+ // `rowoff` apply (e.g. the BigMap walk-cycle's -2 px shift).
+ blitAnimFrameAnchored(screen, anim[frameIdx], x, y);
g_system->unlockScreen();
}
@@ -1012,7 +1333,7 @@ void EEMEngine::playKdAnim(uint16 num) {
// gesture (Jenny taking a picture, etc.) finishes before the
// speaker portrait + speech balloon appear.
//
- // `kKdAnimTable` and `kKdScripts` live at file scope above.
+ // `kKdAnimTable` and `kAnimScripts` live at file scope above.
if (num >= ARRAYSIZE(kKdAnimTable))
return;
@@ -1027,15 +1348,14 @@ void EEMEngine::playKdAnim(uint16 num) {
return;
}
- const uint8 *frames = nullptr;
- uint frameCount = 0;
- for (uint i = 0; i < ARRAYSIZE(kKdScripts); i++) {
- if (kKdScripts[i].seqnum == animId) {
- frames = kKdScripts[i].frames;
- frameCount = kKdScripts[i].len;
- break;
- }
- }
+ // `_DoKDAnim` (168d:028a) calls `_PlayAnimation` with state=4 (one-
+ // shot), which `_UpdateAnimations` walks until it sees the 0x80
+ // terminator and then frees the slot. The same script the
+ // site-loop wait anim uses (looping) is what the one-shot plays
+ // through ONCE here.
+ const AnimScriptRef s = findAnimScript(animId);
+ const uint8 *frames = s.frames;
+ uint frameCount = s.len;
if (frameCount == 0) {
// Fallback: linear playback through anim cells (better than
// nothing if a future kdAnim references an unscripted anim).
@@ -1082,22 +1402,13 @@ void EEMEngine::playKdAnim(uint16 num) {
memcpy((byte *)scratch.getBasePtr(0, row),
(const byte *)bg.getBasePtr(0, row), 320);
}
- const int w = MIN<int>(fr.surface.w, 320 - px);
- const int h = MIN<int>(fr.surface.h, 200 - py);
- for (int row = 0; row < h; row++) {
- const int dstY = py + row;
- if (dstY < 0)
- continue;
- const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, dstY);
- for (int col = 0; col < w; col++) {
- const int dstX = px + col;
- if (dstX < 0)
- continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
+ // Anchor-aware: kdAnim cells (0x03/0x04/0x0c/0x0d ...) have
+ // non-zero per-frame `miscflags`/`rowoff` (anim 0x03 has
+ // rowoff up to 9, anim 0x04 has miscflags = -2). Without
+ // applying those, the camera-flash gesture pop-up appears
+ // at a fixed pixel rather than translating across cells.
+ (void)transp; // anchored blitter recomputes from p.flags
+ blitAnimFrameAnchored(scratch.surfacePtr(), fr, px, py);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
g_system->updateScreen();
diff --git a/engines/eem/site.h b/engines/eem/site.h
index 00c1e647ea0..51129bfed74 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -27,11 +27,35 @@
#include "graphics/managed_surface.h"
+#include "eem/resource.h" // Picture / Animation
+
namespace EEM {
class EEMEngine;
class Mystery;
+/// Pick the frame index to render at `tickMs` for animation
+/// `seqnum`. Walks the script registered in `kAnimScripts` (mirrors
+/// `_UpdateAnimations @ 172b:09c1`'s looping path) at one entry per
+/// 100 ms tick, wrapping on the script's 0x80 terminator. Falls
+/// back to flipbook (`tick % numFrames`) when no script is
+/// registered. `numFrames` is the underlying ANI.DBD entry's cell
+/// count â used both for the fallback path and to clamp script
+/// values that point past the asset.
+uint partnerFrameAtTick(uint16 seqnum, uint numFrames, uint32 tickMs);
+
+/// Anchor-aware masked blit. Mirrors the per-frame anchor offset
+/// math in `_UpdateAnimations @ 172b:09c1`:
+/// `blit_x = anchor_x - frame.miscflags`, `blit_y = anchor_y -
+/// frame.rowoff`. Use for any animation rendered through the
+/// `_NewAnimation` path in the original (partner sprites, animated
+/// drops, briefing animations) â without it, frames with non-zero
+/// per-cell anchors (e.g. anim 0x14 BigMap walk-cycle's miscflags
+/// = -2 shift) "shake in place" instead of translating across
+/// the screen as they're meant to.
+void blitAnimFrameAnchored(Graphics::Surface *screen, const Picture &p,
+ int anchorX, int anchorY);
+
/// One hotspot (search rectangle) within a site, 14 bytes on disk.
struct Hotspot {
int16 x1, y1, x2, y2; ///< rectangle in screen coordinates
@@ -122,6 +146,22 @@ private:
Graphics::ManagedSurface _bgSnapshot;
uint32 _lastTickMs = 0; ///< Last frame-pump tick in ms.
+ /// Wall-clock timestamp at which the partner's wait animation
+ /// "started" (or last restarted). The site loop renders the
+ /// wait sprite at `partnerFrameAtTick(animId, ..., now -
+ /// _waitPhaseAnchor)` so the script position is RELATIVE to
+ /// this anchor, not the global clock. Bump it on:
+ /// - entry to a new site (mirrors `_NewAnimation` setting
+ /// the slot's frame index to 0xffff at site setup, see
+ /// `_DoSiteLoop @ 168d:0436`)
+ /// - return from a one-shot kdAnim (mirrors `_PlayAnimation
+ /// @ 172b:1f5d` writing 0xffff to the resumed slot's frame
+ /// index when state=4 chains back to WaitHandle)
+ /// Without this, the wait anim "snaps" to wherever the global
+ /// clock dictates each time the partner reappears, instead of
+ /// resuming from script[0].
+ uint32 _waitPhaseAnchor = 0;
+
/// Per-site cached ColorCycle ranges. Up to 5 (matching the
/// original's 5-slot animation table).
struct ColorCycleRange { uint8 start, end; };
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index c78ea11d37d..55554a7ff30 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -34,6 +34,7 @@
#include "eem/detection.h"
#include "eem/eem.h"
#include "eem/music.h"
+#include "eem/site.h"
// EEM â UI screens (NOTE.C, GALLERY.C, ACCUSE.C, MAP.C, CHOOSE.C combined).
// Each function is a self-contained modal `EEMEngine::doX()` reachable from
@@ -89,6 +90,7 @@ struct CaseSelectionView {
bool haveCaseBg;
const Animation *kdAnim;
bool haveKdAnim;
+ uint16 kdAnimId; ///< 0x15 / 0x16 â looked up in kAnimScripts
int kdAnimX;
int kdAnimY;
const char *separator;
@@ -166,26 +168,24 @@ void drawCaseSelectionFrame(const CaseSelectionView &v) {
}
// KD greeter frame â masked-blit current animation cell at
- // (0x112, 0x50). 100 ms tick matches the engine's `_CheckFrameRate`.
+ // (0x112, 0x50). 100 ms tick matches `_CheckFrameRate`. The
+ // original `_CaseSelection @ 1c33:0a87` calls `_NewAnimation(...,
+ // CONCAT22(0x15, ...), ..., seqnum=0x15, ...)` so the script
+ // key is 0x15 regardless of partner â even Jenny's CELLS (loaded
+ // via animID 0x16 = ANI.DBD slot) get driven by Jake's 0x15
+ // blink script. Both 0x15 and 0x16 are aliases of 0x00 in our
+ // table so the result is identical, but routing through 0x15
+ // matches the binary.
if (v.haveKdAnim) {
const uint32 now = g_system->getMillis();
- const uint frameIdx = (uint)((now / 100) % v.kdAnim->size());
- const Picture &fr = (*v.kdAnim)[frameIdx];
- const byte transp = (byte)(fr.flags >> 8);
- for (int row = 0; row < fr.surface.h; row++) {
- const int dstY = v.kdAnimY + row;
- if (dstY < 0 || dstY >= 200)
- continue;
- const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, dstY);
- for (int col = 0; col < fr.surface.w; col++) {
- const int dstX = v.kdAnimX + col;
- if (dstX < 0 || dstX >= 320)
- continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
+ const uint frameIdx = partnerFrameAtTick(0x15,
+ (uint)v.kdAnim->size(), now);
+ // Anchor-aware blit. Same rendering path used everywhere
+ // the partner is registered through `_NewAnimation` in the
+ // original.
+ blitAnimFrameAnchored(scratch.surfacePtr(),
+ (*v.kdAnim)[frameIdx],
+ v.kdAnimX, v.kdAnimY);
}
if (v.vm->getFont().isLoaded()) {
// `DrawList` @ 1c33:040d coordinates: `_TextBox + 3` for x
@@ -234,6 +234,138 @@ void drawCaseSelectionFrame(const CaseSelectionView &v) {
g_system->updateScreen();
}
+void EEMEngine::doProfilePicker() {
+ // Mirrors `screen8_handler @ 1c33:1012`. The original walks
+ // `*.PLR` files in `C:\EEMCDSAV\` (max 25), reads the first 12
+ // bytes of each (the player-name field of `_PlayerRecord`), and
+ // hands the list to `_DoChoose`. If no profiles exist (loop hits
+ // `local_20 == 0` at 1c33:1170), it falls straight into
+ // `_NewPlayer`. Selecting an entry calls `_LoadPlayerRecord` and
+ // returns; selecting the "exit" sentinel goes back to title.
+ const SaveStateList saves = listProfiles();
+ if (saves.empty()) {
+ doNewPlayer();
+ return;
+ }
+
+ if (!_font.isLoaded()) {
+ // No font means we can't render the picker â fall through.
+ doNewPlayer();
+ return;
+ }
+
+ // Build the visible list: existing profile names + "[New Player]".
+ struct Entry {
+ Common::String label;
+ int slot; ///< -1 means "create new"
+ };
+ Common::Array<Entry> entries;
+ for (const SaveStateDescriptor &s : saves) {
+ Entry e;
+ e.label = s.getDescription();
+ e.slot = s.getSaveSlot();
+ entries.push_back(e);
+ }
+ Entry newEntry;
+ newEntry.label = "[New Player]";
+ newEntry.slot = -1;
+ entries.push_back(newEntry);
+
+ int sel = 0;
+ bool done = false;
+
+ auto draw = [&]() {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ Picture bg;
+ if (_picsArchive.getPicture(0x104, bg)) {
+ const int w = MIN<int>(bg.surface.w, 320);
+ const int h = MIN<int>(bg.surface.h, 200);
+ for (int row = 0; row < h; row++)
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)bg.surface.getBasePtr(0, row), w);
+ }
+ _font.drawString(&scratch, "Pick a player:", 80, 30, 220, 0xF);
+ const int kLineH = 12;
+ for (uint i = 0; i < entries.size(); i++) {
+ const byte color = ((int)i == sel) ? 0xF : 0x8;
+ _font.drawString(&scratch, entries[i].label,
+ 80, 60 + (int)i * kLineH, 220, color);
+ }
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ };
+ draw();
+
+ while (!done && !shouldQuit()) {
+ Common::Event ev;
+ bool dirty = false;
+ bool committed = false;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ _playerName = "Detective";
+ return;
+ }
+ if (ev.type == Common::EVENT_KEYDOWN) {
+ switch (ev.kbd.keycode) {
+ case Common::KEYCODE_UP:
+ sel = (sel + (int)entries.size() - 1) % (int)entries.size();
+ dirty = true;
+ break;
+ case Common::KEYCODE_DOWN:
+ sel = (sel + 1) % (int)entries.size();
+ dirty = true;
+ break;
+ case Common::KEYCODE_RETURN:
+ case Common::KEYCODE_KP_ENTER:
+ committed = true;
+ break;
+ case Common::KEYCODE_ESCAPE:
+ _playerName = "Detective";
+ return;
+ default:
+ break;
+ }
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ const int kLineH = 12;
+ const int hit = (ev.mouse.y - 60) / kLineH;
+ if (hit >= 0 && hit < (int)entries.size()) {
+ sel = hit;
+ committed = true;
+ }
+ }
+ if (committed)
+ break;
+ }
+ if (committed) {
+ done = true;
+ break;
+ }
+ if (dirty)
+ draw();
+ g_system->updateScreen();
+ g_system->delayMillis(15);
+ }
+
+ const Entry &e = entries[sel];
+ if (e.slot < 0) {
+ doNewPlayer();
+ } else {
+ // Mirrors `_LoadPlayerRecord` at 1c33:1281 â slot found,
+ // load it. The save body re-fills `_playerName`, partner,
+ // chain stage, mysteriesSolved.
+ if (!loadProfile(e.label)) {
+ warning("doProfilePicker: failed to load profile '%s' at slot %d",
+ e.label.c_str(), e.slot);
+ doNewPlayer();
+ }
+ }
+}
+
void EEMEngine::doNewPlayer() {
// Mirrors `_NewPlayer` @ 1c33:0dda. The original draws background
// 0x104 + character peek pic 0x107, then shows "Please type your
@@ -295,6 +427,10 @@ void EEMEngine::doNewPlayer() {
memset(_mysteriesSolved, 0, sizeof(_mysteriesSolved));
_mystery.clear();
_partner = 0;
+ // `_NewPlayer @ 1c33:0fa3` writes
+ // `DAT_2d5d_3f99 = 1` â fresh profiles always
+ // start at the Junior tier.
+ _chainStage = 1;
saveProfile(name);
}
return;
@@ -339,6 +475,395 @@ void EEMEngine::doNewPlayer() {
}
}
+void EEMEngine::doShowEnding(uint num) {
+ // Mirrors `_DisplayEnding @ 1df2:0548` + `_DisplayEndingPage @
+ // 1df2:044c`. File format (verified by reading E0.BIN's bytes):
+ // u16 pageCount
+ // for each page:
+ // u16 picNum
+ // u16 x1, y1, x2, y2 (story rect â passed to WordWrap)
+ // char text[] (null-terminated, ParseString opcodes)
+ //
+ // The original walks pages with PrevPage / NextPage rects (29be:
+ // 0x..); we approximate with mouse-click / Enter = next, ESC =
+ // exit. ParseString substitutes `\x80` (player name) and the rest
+ // of the 0x80..0x89 placeholder family so the rendered text
+ // reads as the correct player + partner combo.
+ const Common::String fname = Common::String::format("E%u.BIN", num);
+ Common::File f;
+ if (!f.open(Common::Path(fname))) {
+ warning("doShowEnding: %s missing", fname.c_str());
+ return;
+ }
+ const uint32 size = f.size();
+ if (size < 2) {
+ warning("doShowEnding: %s too small (%u bytes)",
+ fname.c_str(), size);
+ return;
+ }
+ Common::Array<byte> buf(size);
+ if (f.read(buf.data(), size) != size) {
+ warning("doShowEnding: %s short read", fname.c_str());
+ return;
+ }
+
+ const uint16 pageCount = READ_LE_UINT16(buf.data());
+ if (pageCount == 0)
+ return;
+
+ // Walk page records. Each page header is 10 bytes; text is
+ // null-terminated and follows the header.
+ uint pageOffsets[8]; // ENDING_RANGE_MAX from `_DisplayEnding`
+ const uint kMaxPages = MIN<uint>(pageCount,
+ (uint)(sizeof(pageOffsets) / sizeof(uint)));
+ uint cursor = 2;
+ for (uint p = 0; p < kMaxPages; p++) {
+ pageOffsets[p] = cursor;
+ if (cursor + 10 >= size)
+ break;
+ // Skip the 10-byte header and find the null terminator.
+ cursor += 10;
+ while (cursor < size && buf[cursor] != 0)
+ cursor++;
+ cursor++; // past the null
+ }
+
+ uint pageIdx = 0;
+ bool dirty = true;
+ while (!shouldQuit() && pageIdx < kMaxPages) {
+ if (dirty) {
+ const uint off = pageOffsets[pageIdx];
+ if (off + 10 >= size)
+ break;
+ const uint16 picNum = READ_LE_UINT16(buf.data() + off);
+ const uint16 x1 = READ_LE_UINT16(buf.data() + off + 2);
+ const uint16 y1 = READ_LE_UINT16(buf.data() + off + 4);
+ const uint16 x2 = READ_LE_UINT16(buf.data() + off + 6);
+ (void)READ_LE_UINT16(buf.data() + off + 8); // y2 (unused â WordWrap2 takes width only)
+
+ // Halve the rect coords: ending pages use 320x400 logical
+ // coords (x2=0x128=296, y2=0xa8=168 in our test file
+ // E0.BIN â both already 320x200 mode 13h). No conversion.
+ Picture bg;
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ if (_picsArchive.getPicture(picNum, bg)) {
+ const int w = MIN<int>(bg.surface.w, 320);
+ const int h = MIN<int>(bg.surface.h, 200);
+ for (int row = 0; row < h; row++)
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)bg.surface.getBasePtr(0, row), w);
+ }
+
+ // Story text. The bytes are a null-terminated string with
+ // `_ParseString` placeholders (0x80 = player name, 0x82
+ // = partner first name, etc.).
+ const char *raw = (const char *)buf.data() + off + 10;
+ const Common::String text = parseString(raw, _playerName, _partner);
+
+ if (_font.isLoaded() && x2 > x1) {
+ const int textW = MIN<int>((int)x2 - (int)x1, 320 - (int)x1);
+ _font.drawWordWrapped(&scratch, (int)x1, (int)y1,
+ textW, text, 0xF);
+ }
+
+ // Page indicator at top-right ("page 1/3").
+ if (_font.isLoaded() && kMaxPages > 1) {
+ const Common::String hdr = Common::String::format(
+ "%u/%u", (unsigned)pageIdx + 1, (unsigned)kMaxPages);
+ _font.drawString(&scratch, hdr, 280, 4, 32, 0xF);
+ }
+
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ dirty = false;
+ }
+
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return;
+ if (ev.type == Common::EVENT_KEYDOWN) {
+ switch (ev.kbd.keycode) {
+ case Common::KEYCODE_ESCAPE:
+ return;
+ case Common::KEYCODE_LEFT:
+ case Common::KEYCODE_PAGEUP:
+ if (pageIdx > 0) {
+ pageIdx--;
+ dirty = true;
+ }
+ break;
+ case Common::KEYCODE_RIGHT:
+ case Common::KEYCODE_PAGEDOWN:
+ case Common::KEYCODE_RETURN:
+ case Common::KEYCODE_KP_ENTER:
+ case Common::KEYCODE_SPACE:
+ pageIdx++;
+ dirty = true;
+ break;
+ default:
+ break;
+ }
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ pageIdx++;
+ dirty = true;
+ }
+ }
+ g_system->updateScreen();
+ g_system->delayMillis(15);
+ }
+}
+
+void EEMEngine::doSetup() {
+ // Mirrors `_DoSetup @ 1f78:044e`. Loads BG pic 0x40 +
+ // state-button pics 0x9b/0x9c/0x9d/0x9e. The original wires 13
+ // hot-rects (`_SetupButtons @ 29be:1218`) but only two of them
+ // drive persistent state â `Kid1`/`Kid2` (partner) and
+ // `SoundOn`/`SoundOff` (voice flag, `DAT_2d5d_3f97`). The rest
+ // are reset/help/return buttons. We render a minimal text
+ // version: two toggle lines, click to flip, ESC to leave.
+ if (!_font.isLoaded()) {
+ _nextScreen = (ScreenId)_lastScreen;
+ return;
+ }
+
+ const Common::Rect kBackBtn(120, 170, 200, 188);
+ const Common::Rect kPartnerToggle(40, 60, 280, 78);
+ const Common::Rect kVoiceToggle (40, 90, 280, 108);
+
+ auto draw = [&]() {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ Picture bg;
+ if (_picsArchive.getPicture(0x40, bg)) {
+ const int w = MIN<int>(bg.surface.w, 320);
+ const int h = MIN<int>(bg.surface.h, 200);
+ for (int row = 0; row < h; row++)
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)bg.surface.getBasePtr(0, row), w);
+ }
+ _font.drawString(&scratch, "Setup", 140, 30, 80, 0xF);
+ const Common::String partnerLine = Common::String::format(
+ "Partner: %s (click to switch)",
+ _partner == 0 ? "Jake" : "Jenny");
+ _font.drawString(&scratch, partnerLine, 50, 64, 240, 0xF);
+ const Common::String voiceLine = Common::String::format(
+ "Voice: %s (click to toggle)",
+ _voiceOn ? "ON" : "OFF");
+ _font.drawString(&scratch, voiceLine, 50, 94, 240, 0xF);
+ _font.drawString(&scratch, "[ Back ]", 130, 174, 80, 0xF);
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ };
+ draw();
+
+ while (!shouldQuit()) {
+ Common::Event ev;
+ bool dirty = false;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ _nextScreen = kScreenInvalid;
+ return;
+ }
+ if (ev.type == Common::EVENT_KEYDOWN) {
+ if (ev.kbd.keycode == Common::KEYCODE_ESCAPE ||
+ ev.kbd.keycode == Common::KEYCODE_RETURN) {
+ // `_DoSetup @ 1f78:044a` writes `_NextScreen =
+ // _LastScreen` on entry. Returning means we just
+ // dispatch back to whichever screen called us.
+ _nextScreen = (ScreenId)_lastScreen;
+ if (_nextScreen == kScreenSetup ||
+ _nextScreen == kScreenInvalid)
+ _nextScreen = kScreenMap;
+ if (_voiceOn)
+ saveProfile(_playerName);
+ return;
+ }
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ if (kBackBtn.contains(ev.mouse.x, ev.mouse.y)) {
+ _nextScreen = (ScreenId)_lastScreen;
+ if (_nextScreen == kScreenSetup ||
+ _nextScreen == kScreenInvalid)
+ _nextScreen = kScreenMap;
+ saveProfile(_playerName);
+ return;
+ }
+ if (kPartnerToggle.contains(ev.mouse.x, ev.mouse.y)) {
+ _partner = _partner == 0 ? 1 : 0;
+ dirty = true;
+ }
+ if (kVoiceToggle.contains(ev.mouse.x, ev.mouse.y)) {
+ _voiceOn = !_voiceOn;
+ if (_audio)
+ _audio->setVoiceEnabled(_voiceOn);
+ dirty = true;
+ }
+ }
+ }
+ if (dirty)
+ draw();
+ g_system->updateScreen();
+ g_system->delayMillis(15);
+ }
+}
+
+void EEMEngine::doActionScreen() {
+ // Mirrors `_ActionScreen @ 1c33:195b` â the post-mystery menu the
+ // original loops back to after `_DisplayCorrect` (winner). The
+ // original draws PIC 0x122 (Cok), 0x124 (Cexit), then calls
+ // `_DoChoose(ActionNames)` and dispatches the result via the
+ // jumptable at 1c33:1be1 (verified to set `_NextScreen` for each
+ // action â entry 1 â 10 = CHOOSE_MYSTERY, entry 3 â
+ // `_ReloadMystery`, etc.).
+ //
+ // We render a minimal text version of the menu â the original's
+ // background pic + "Cok" / "Cexit" overlays land once the
+ // `_DoChoose` UI lands. For now: two practical entries â "Solve a
+ // Mystery" (the only one whose underlying screen is wired) and
+ // "Quit". Sets `_nextScreen` exactly the same way the original
+ // jumptable does:
+ // "Solve a Mystery" â kScreenChooseMystery (matches handler 1
+ // at 1c33:1add: `MOV [_NextScreen], 0xa`)
+ // "Quit" â kScreenInvalid (matches the sentinel
+ // handler at 1c33:1afa)
+ if (!_font.isLoaded()) {
+ _nextScreen = kScreenChooseMystery;
+ return;
+ }
+
+ enum ActionKind {
+ kActSolve, // â kScreenChooseMystery (action 1, 1c33:1add)
+ kActScrapbook, // â doShowEnding(lastSolved) (action 5, 1c33:1b13)
+ kActQuit
+ };
+ struct Entry {
+ const char *label;
+ ActionKind kind;
+ };
+ // Locate the highest-numbered solved mystery â that's the one
+ // the player most recently completed, and what the action-menu
+ // "Look at My Books" entry replays. If none solved yet, hide
+ // the entry (matches the original's grey-out at 1c33:19f3 where
+ // `local_24[5] = 1` before the chain-stage check toggles it).
+ int lastSolved = -1;
+ for (int i = (int)sizeof(_mysteriesSolved) - 1; i >= 0; i--) {
+ if (_mysteriesSolved[i] != 0) {
+ lastSolved = i;
+ break;
+ }
+ }
+
+ Common::Array<Entry> entries;
+ Entry solveEntry; solveEntry.label = "Solve a Mystery"; solveEntry.kind = kActSolve;
+ entries.push_back(solveEntry);
+ if (lastSolved >= 0) {
+ Entry sb; sb.label = "Look at My Books"; sb.kind = kActScrapbook;
+ entries.push_back(sb);
+ }
+ Entry quitEntry; quitEntry.label = "Quit"; quitEntry.kind = kActQuit;
+ entries.push_back(quitEntry);
+ const int kCount = (int)entries.size();
+
+ int sel = 0;
+ auto draw = [&]() {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ Picture bg;
+ if (_picsArchive.getPicture(0x104, bg)) {
+ const int w = MIN<int>(bg.surface.w, 320);
+ const int h = MIN<int>(bg.surface.h, 200);
+ for (int row = 0; row < h; row++)
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)bg.surface.getBasePtr(0, row), w);
+ }
+ _font.drawString(&scratch, "What now?", 100, 30, 220, 0xF);
+ for (int i = 0; i < kCount; i++) {
+ const byte color = (i == sel) ? 0xF : 0x8;
+ _font.drawString(&scratch, entries[i].label,
+ 100, 60 + i * 14, 220, color);
+ }
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ };
+ draw();
+
+ while (!shouldQuit()) {
+ Common::Event ev;
+ bool dirty = false;
+ bool committed = false;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ _nextScreen = kScreenInvalid;
+ return;
+ }
+ if (ev.type == Common::EVENT_KEYDOWN) {
+ switch (ev.kbd.keycode) {
+ case Common::KEYCODE_UP:
+ sel = (sel + kCount - 1) % kCount;
+ dirty = true;
+ break;
+ case Common::KEYCODE_DOWN:
+ sel = (sel + 1) % kCount;
+ dirty = true;
+ break;
+ case Common::KEYCODE_RETURN:
+ case Common::KEYCODE_KP_ENTER:
+ committed = true;
+ break;
+ case Common::KEYCODE_ESCAPE:
+ _nextScreen = kScreenInvalid;
+ return;
+ default:
+ break;
+ }
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ const int hit = (ev.mouse.y - 60) / 14;
+ if (hit >= 0 && hit < kCount) {
+ sel = hit;
+ committed = true;
+ }
+ }
+ if (committed)
+ break;
+ }
+ if (committed) {
+ switch (entries[sel].kind) {
+ case kActSolve:
+ _nextScreen = kScreenChooseMystery;
+ return;
+ case kActScrapbook:
+ if (lastSolved >= 0)
+ doShowEnding((uint)lastSolved);
+ // Fall through to redraw the menu after viewing.
+ draw();
+ continue;
+ case kActQuit:
+ _nextScreen = kScreenInvalid;
+ return;
+ }
+ return;
+ }
+ if (dirty)
+ draw();
+ g_system->updateScreen();
+ g_system->delayMillis(15);
+ }
+ _nextScreen = kScreenInvalid;
+}
+
void EEMEngine::doCaseSelection() {
// Mirrors `_CaseSelection` @ 1c33:0a87. The original draws PIC 0x41
// (chooser background) plus a centred "Book %d" / "Challenge Book"
@@ -416,6 +941,7 @@ void EEMEngine::doCaseSelection() {
v.haveCaseBg = haveCaseBg;
v.kdAnim = &kdAnim;
v.haveKdAnim = haveKdAnim;
+ v.kdAnimId = (uint16)kKdAniId;
v.kdAnimX = kKdAnimX;
v.kdAnimY = kKdAnimY;
v.separator = kSeparator;
@@ -548,11 +1074,25 @@ void EEMEngine::doCaseSelection() {
}
// "Choose A Mystery" sub-screen: pick a specific case from the
- // 55-mystery roster. The original opens a different list here;
- // we approximate with the tier-aware numeric chooser we used
- // before. Default to the first unsolved mystery.
- uint sel = 0;
- for (uint i = 0; i <= kMaxMystery; i++) {
+ // 55-mystery roster. `_CaseSelection @ 1c33:0a87` only shows
+ // mysteries in the player's current chain stage:
+ // stage 1 (Junior) â 1..24 (start = `iVar1 = 1` @ 1c33:0aff)
+ // stage 2 (Senior) â 25..48 (start = `iVar1 = 0x19`)
+ // stage 3 (Master) â 49..54 (start = `iVar1 = 0x31`)
+ // The original passes `&DAT_2d5d_3f9b + iVar1` as the grey-mask
+ // pointer so DoChoose lights up only the tier-relevant entries.
+ // We clamp `sel` to the tier range and pre-seed it to the first
+ // unsolved case in that range.
+ uint stageLo = 1, stageHi = 0x18;
+ switch (_chainStage) {
+ case 2: stageLo = 0x19; stageHi = 0x30; break;
+ case 3: stageLo = 0x31; stageHi = 0x36; break;
+ default: break; // stage 1 (or fallback)
+ }
+ if (stageHi > kMaxMystery)
+ stageHi = kMaxMystery;
+ uint sel = stageLo;
+ for (uint i = stageLo; i <= stageHi; i++) {
if (i < sizeof(_mysteriesSolved) && !_mysteriesSolved[i]) {
sel = i;
break;
@@ -587,13 +1127,13 @@ void EEMEngine::doCaseSelection() {
return;
}
if (kUpArrowRect.contains(ev.mouse.x, ev.mouse.y)) {
- sel = (sel == 0) ? kMaxMystery : sel - 1;
+ sel = (sel <= stageLo) ? stageHi : sel - 1;
sv.sel = sel;
drawCaseSubmenu(sv);
continue;
}
if (kDnArrowRect.contains(ev.mouse.x, ev.mouse.y)) {
- sel = (sel >= kMaxMystery) ? 0 : sel + 1;
+ sel = (sel >= stageHi) ? stageLo : sel + 1;
sv.sel = sel;
drawCaseSubmenu(sv);
continue;
@@ -635,31 +1175,31 @@ void EEMEngine::doCaseSelection() {
continue;
}
if (k == Common::KEYCODE_DOWN || k == Common::KEYCODE_TAB) {
- sel = (sel >= kMaxMystery) ? 0 : sel + 1;
+ sel = (sel >= stageHi) ? stageLo : sel + 1;
sv.sel = sel;
drawCaseSubmenu(sv);
continue;
}
if (k == Common::KEYCODE_UP) {
- sel = (sel == 0) ? kMaxMystery : sel - 1;
+ sel = (sel <= stageLo) ? stageHi : sel - 1;
sv.sel = sel;
drawCaseSubmenu(sv);
continue;
}
if (k == Common::KEYCODE_PAGEDOWN) {
- sel = (sel + 10 > kMaxMystery) ? kMaxMystery : sel + 10;
+ sel = (sel + 10 > stageHi) ? stageHi : sel + 10;
sv.sel = sel;
drawCaseSubmenu(sv);
continue;
}
if (k == Common::KEYCODE_PAGEUP) {
- sel = (sel < 10) ? 0 : sel - 10;
+ sel = (sel < stageLo + 10) ? stageLo : sel - 10;
sv.sel = sel;
drawCaseSubmenu(sv);
continue;
}
- if (k == Common::KEYCODE_HOME) { sel = 0; sv.sel = sel; drawCaseSubmenu(sv); continue; }
- if (k == Common::KEYCODE_END) { sel = kMaxMystery; sv.sel = sel; drawCaseSubmenu(sv); continue; }
+ if (k == Common::KEYCODE_HOME) { sel = stageLo; sv.sel = sel; drawCaseSubmenu(sv); continue; }
+ if (k == Common::KEYCODE_END) { sel = stageHi; sv.sel = sel; drawCaseSubmenu(sv); continue; }
}
g_system->updateScreen();
g_system->delayMillis(15);
@@ -871,28 +1411,25 @@ void EEMEngine::drawNotebookFrame(int &page) {
(const byte *)frame.surface.getBasePtr(0, row), w);
}
- // Partner sprite at (5, 80). Anim 1 for Jake, 0xb (11) for Jenny.
+ // Partner sprite at (5, 80). Anim 1 for Jake, 0xb (11) for Jenny
+ // for CELLS, but the original `_DoNotebook @ 161e:0500` always
+ // uses script 0x01 (verified by `CONCAT22(1, ...)` in its
+ // `_NewAnimation` call at 161e:054c). Both 0x01 and 0x0b have
+ // the SAME script in `kAnimScripts` (alias), so both lookups
+ // produce identical results â but routing through 0x01
+ // matches the original verbatim.
const uint partnerAnim = (_partner == 0) ? 1 : 0xb;
Animation partnerAni;
if (_aniArchive.loadAnimation(partnerAnim, partnerAni) && !partnerAni.empty()) {
const uint32 now = g_system->getMillis();
- const uint frameIdx = (uint)((now / 100) % partnerAni.size());
- const Picture &fr = partnerAni[frameIdx];
- const byte transp = (byte)(fr.flags >> 8);
- for (int row = 0; row < fr.surface.h; row++) {
- const int dstY = 80 + row;
- if (dstY < 0 || dstY >= 200)
- continue;
- const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, dstY);
- for (int col = 0; col < fr.surface.w; col++) {
- const int dstX = 5 + col;
- if (dstX < 0 || dstX >= 320)
- continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
+ const uint frameIdx = partnerFrameAtTick(0x01,
+ (uint)partnerAni.size(), now);
+ // Anchor-aware blit. The PDA partner (anim 0x01/0x0b) cells
+ // have miscflags = rowoff = 0 in the audit, but routing
+ // through `blitAnimFrameAnchored` is harmless and keeps the
+ // rendering path consistent with the BigMap partner.
+ blitAnimFrameAnchored(scratch.surfacePtr(),
+ partnerAni[frameIdx], 5, 80);
}
// Notes â `_DrawNotes` walks `_NoteIndex` for the current page,
@@ -1314,28 +1851,23 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
}
}
- // Partner sprite frame @ (5, 0x50).
+ // Partner sprite frame @ (5, 0x50). The original `_DoGallery @
+ // 158f:065b` registers `_NewAnimation(..., CONCAT22(2, ...), ...)`
+ // â script key 0x02 regardless of partner. Jake's 0x02 script
+ // (26 frames, brief wave + long hold + second wave) is what
+ // drives BOTH partners' cells. Earlier our port used 0x10 for
+ // Jenny, which is a 9-frame short blip â so Jenny was missing
+ // 17 frames of the wave-and-pause cadence that Jake has.
if (havePartner) {
const uint32 now = g_system->getMillis();
- const uint frameIdx = (uint)((now / 100) % partnerAni.size());
- const Picture &fr = partnerAni[frameIdx];
- const byte transp = (byte)(fr.flags >> 8);
- const int px = 5;
- const int py = 0x50;
- for (int row = 0; row < fr.surface.h; row++) {
- const int dstY = py + row;
- if (dstY < 0 || dstY >= 200)
- continue;
- const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, dstY);
- for (int col = 0; col < fr.surface.w; col++) {
- const int dstX = px + col;
- if (dstX < 0 || dstX >= 320)
- continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
+ const uint frameIdx = partnerFrameAtTick(0x02,
+ (uint)partnerAni.size(), now);
+ // Anchor-aware blit, consistent with site-loop / BigMap
+ // rendering paths. Anim 0x02 has rowoff = miscflags = 0
+ // per the audit but the anchored blitter is still the
+ // right semantic for an `_NewAnimation`-rendered sprite.
+ blitAnimFrameAnchored(scratch.surfacePtr(),
+ partnerAni[frameIdx], 5, 0x50);
}
// Portraits â `_DrawGallery @ 158f:0046` walks suspects 0..N-1 and
@@ -1470,12 +2002,13 @@ void EEMEngine::doBigMap() {
return;
if (ev.type == Common::EVENT_LBUTTONDOWN) {
// SetupButtonRect â `_NextScreen = 6` (the original's
- // settings screen). We use it as "back to menu":
- // abandon the current mystery and return to case
- // selection.
+ // settings screen, mirrors `_DoBigMap @ 20fe:0c33`
+ // where it pushes `_PressButton` then writes
+ // `_NextScreen = 6`). Now wired to the actual
+ // `doSetup` handler instead of dropping the player
+ // out to the launcher.
if (kSetupBtnRect.contains(ev.mouse.x, ev.mouse.y)) {
- _mystery.clear();
- _nextScreen = kScreenInvalid;
+ _nextScreen = kScreenSetup;
return;
}
// Click in the BigMapWindow â zoom. Original formula:
@@ -1753,30 +2286,24 @@ void EEMEngine::drawBigMapOverview() {
}
}
- // Partner idle sprite at (0xfd, 0x50). Jake = anim 0x14, Jenny = 0x12.
+ // Partner idle sprite at (0xfd, 0x50). Jake = anim 0x14, Jenny = 0x12
+ // for the loaded CELLS, but the original `_DoBigMap @ 20fe:0a47`
+ // always passes `CONCAT22(0x14, ...)` to `_NewAnimation` so the
+ // SCRIPT key is 0x14 (`[0..8]` count-up) regardless of partner.
+ // Without this, Jenny was running 0x12's count-DOWN script
+ // `[8..0]` over her cells â visually backwards from the original.
const uint kMapAniId = (_partner == 0) ? 0x14 : 0x12;
Animation mapAnim;
if (_aniArchive.loadAnimation(kMapAniId, mapAnim) && !mapAnim.empty()) {
const uint32 now = g_system->getMillis();
- const uint frameIdx = (uint)((now / 100) % mapAnim.size());
- const Picture &fr = mapAnim[frameIdx];
- const byte transp = (byte)(fr.flags >> 8);
- const int kMapAnimX = 0xfd;
- const int kMapAnimY = 0x50;
- for (int row = 0; row < fr.surface.h; row++) {
- const int dstY = kMapAnimY + row;
- if (dstY < 0 || dstY >= 200)
- continue;
- const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, dstY);
- for (int col = 0; col < fr.surface.w; col++) {
- const int dstX = kMapAnimX + col;
- if (dstX < 0 || dstX >= 320)
- continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
+ const uint frameIdx = partnerFrameAtTick(0x14,
+ (uint)mapAnim.size(), now);
+ // Anchor-aware: the BigMap walk-cycle has miscflags = -2 per
+ // cell, so the partner shifts left as it cycles â without the
+ // anchor adjustment the sprite "shakes in place" instead of
+ // walking forward.
+ blitAnimFrameAnchored(scratch.surfacePtr(), mapAnim[frameIdx],
+ 0xfd, 0x50);
}
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
@@ -1856,31 +2383,20 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
}
// Partner sprite on the detail map (drawn last to sit over the
- // frame and the BIGMAP.PIC viewport).
+ // frame and the BIGMAP.PIC viewport). The original always passes
+ // `CONCAT22(0x13, ...)` to `_NewAnimation` (i.e. script ID 0x13)
+ // regardless of partner â verified at `_DoBigMap @ 20fe:0a47`.
+ // So we look up script 0x13 for both partners while still
+ // loading the partner-specific CELLS via `kDetailAniId`.
const uint kDetailAniId = (_partner == 0) ? 0x13 : 0x11;
Animation detailAnim;
if (_aniArchive.loadAnimation(kDetailAniId, detailAnim) &&
!detailAnim.empty()) {
const uint32 now = g_system->getMillis();
- const uint frameIdx = (uint)((now / 100) % detailAnim.size());
- const Picture &fr = detailAnim[frameIdx];
- const byte transp = (byte)(fr.flags >> 8);
- const int kDetailAnimX = 0x101;
- const int kDetailAnimY = 0x50;
- for (int row = 0; row < fr.surface.h; row++) {
- const int dstY = kDetailAnimY + row;
- if (dstY < 0 || dstY >= 200)
- continue;
- const byte *src = (const byte *)fr.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, dstY);
- for (int col = 0; col < fr.surface.w; col++) {
- const int dstX = kDetailAnimX + col;
- if (dstX < 0 || dstX >= 320)
- continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
+ const uint frameIdx = partnerFrameAtTick(0x13,
+ (uint)detailAnim.size(), now);
+ blitAnimFrameAnchored(scratch.surfacePtr(),
+ detailAnim[frameIdx], 0x101, 0x50);
}
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
@@ -2269,6 +2785,37 @@ void EEMEngine::doAccuse() {
_mysteriesSolved[mn] = _mystery._firstTry ? 2 : 1;
}
+ // Mirrors the chain-advancement loop at `_DisplayCorrect @
+ // 1df2:0824-0850`. Skip mystery 0 (the practice case) per the
+ // `if (_MysteryNumber != 0)` guard at 1df2:080d, then check
+ // every mystery in the current tier:
+ // stage 1 â check 1..0x18 (24 mysteries, "A chain")
+ // stage 2 â check 0x19..0x30 (24 mysteries, "B chain")
+ // stage 3 â check 0x31..0x36 (6 mysteries, "C chain")
+ // If every solve flag in that range is non-zero, bump the
+ // stage. Original increments unconditionally (3â4 is harmless
+ // since no further range covers it); we cap at 3 for clarity.
+ if (mn != 0) {
+ uint lo = 0, hi = 0;
+ switch (_chainStage) {
+ case 1: lo = 1; hi = 0x18; break;
+ case 2: lo = 0x19; hi = 0x30; break;
+ case 3: lo = 0x31; hi = 0x36; break;
+ default: break;
+ }
+ bool allSolved = (hi >= lo);
+ for (uint i = lo; i <= hi && allSolved; i++) {
+ if (i >= sizeof(_mysteriesSolved) || _mysteriesSolved[i] == 0)
+ allSolved = false;
+ }
+ if (allSolved && _chainStage < 3) {
+ _chainStage++;
+ debugC(1, kDebugMystery,
+ "chainStage advanced to %u after solving mystery %u",
+ _chainStage, mn);
+ }
+ }
+
// `_DisplayCorrect @ 1df2:073c` calls `_MIDIPlay(5)` (1df2:0789)
// before `_DifferenceAnimation("scrapbk.ani")` to swap from the
// travel music to the winner cue.
@@ -2276,6 +2823,17 @@ void EEMEngine::doAccuse() {
_music->playMus(5, /*loop=*/false);
playAnm(Common::Path("SCRAPBK.ANI"), 120, true);
+ // `_DisplayCorrect @ 1df2:07ac` then calls `_DisplayClue`
+ // against the briefing's winning ClueBlock at
+ // `_Mystery + _MysteryIndex[0x10]` BEFORE `_ShowOneScrap`.
+ // We don't render that intermediate ClueBlock yet (it ties
+ // into the chain-A/B/C result selection); skip straight to
+ // the per-mystery ending pages â the same screen
+ // `_ShowOneScrap @ 1f78:0773` displays via
+ // `_DisplayEnding(num, 1)`. Players see the per-mystery
+ // resolution text on top of the ending pic.
+ doShowEnding(mn);
+
// Mirrors `_SavePlayerRecord` at 1df2:0857 â once the
// `_mysteriesSolved` table is updated, the original
// immediately persists the player record so the win sticks
@@ -2287,8 +2845,22 @@ void EEMEngine::doAccuse() {
if (err.getCode() != Common::kNoError)
warning("saveProfile after solve failed: %s",
err.getDesc().c_str());
+
+ // `_DisplayCorrect @ 1df2:0895` writes `_NextScreen = 0xc`
+ // â the winner returns to the post-mystery `_ActionScreen`.
+ // Free the mystery first so the loop can break out cleanly:
+ // `_DeleteSavedGame` at 1df2:0851 + `_FreeMystery` at
+ // 1df2:08a4 do the same.
+ _mystery.clear();
+ _nextScreen = kScreenAction;
} else {
_mystery._firstTry = false;
+ // `_DisplayAlibi @ 1df2:043f` writes `_NextScreen =
+ // _LastScreen` â drop back to wherever the player accused
+ // from. With no mystery state to unload, the site loop
+ // resumes naturally.
+ _nextScreen = _lastScreen != kScreenInvalid
+ ? (ScreenId)_lastScreen : kScreenSite;
}
}
Commit: 9c88ee5bc94b0f42d1f1edc32ced135c61533386
https://github.com/scummvm/scummvm/commit/9c88ee5bc94b0f42d1f1edc32ced135c61533386
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:42+02:00
Commit Message:
EEM: fixed the map animation
Changed paths:
engines/eem/eem.h
engines/eem/site.cpp
engines/eem/site.h
engines/eem/ui.cpp
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index e97bd592712..af19e0c43fc 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -252,10 +252,11 @@ private:
void drawGalleryFrame(const byte *gd, uint8 numSuspects,
Common::Array<Common::Rect> &slotRects,
Common::Array<int> &slotSuspect);
- void drawBigMapOverview();
+ void drawBigMapOverview(uint32 elapsedMs);
void drawBigMapDetail(int scrollX, int scrollY,
const Common::Array<byte> &mapPixels,
- uint16 mapW, uint16 mapH);
+ uint16 mapW, uint16 mapH,
+ uint32 elapsedMs);
void drawAccuseGallery(uint8 numSuspects, const byte *gd,
int highlighted,
Common::Array<Common::Rect> &slotRects,
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 42c5429d44e..54d3673f501 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -457,6 +457,49 @@ uint partnerFrameAtTick(uint16 seqnum, uint numFrames, uint32 tickMs) {
return (numFrames > 0) ? MIN<uint>(frame, numFrames - 1) : 0;
}
+// Generic "play `unfold` once, then loop `waitSeq` forever" walker.
+// Mirrors the original's slot-script-swap idiom: the entrance script
+// runs to its 0x80 terminator, then the slot's script pointer is
+// rewritten to a looping wait sequence (e.g. `_BigMapWaitSeq @
+// 29be:1574`, `_SmallMapWaitSeq @ 29be:1548`). `partnerFrameAtTick`
+// can't model that swap on its own (it always wraps on the same
+// script), hence this helper.
+static uint oneShotThenLoopFrameAtTick(const uint8 *unfold, uint unfoldLen,
+ const uint8 *waitSeq, uint waitSeqLen,
+ uint numFrames, uint32 elapsedMs) {
+ const uint kFramePeriodMs = 100;
+ const uint tick = elapsedMs / kFramePeriodMs;
+ const uint frame = (tick < unfoldLen)
+ ? unfold[tick]
+ : waitSeq[(tick - unfoldLen) % waitSeqLen];
+ return (numFrames > 0) ? MIN<uint>(frame, numFrames - 1) : 0;
+}
+
+uint bigMapPartnerFrameAtTick(uint numFrames, uint32 elapsedMs) {
+ // Script 0x14 @ 29be:196a (count-up 0..8, 0x80) â on terminator,
+ // `_DoBigMap` swaps to `_BigMapWaitSeq` @ 29be:1574
+ // (9,9,9,9,10,9,9,9,9, 0x80) â open-map hold with a fidget.
+ static const uint8 kUnfold[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8 };
+ static const uint8 kWaitSeq[] = { 9, 9, 9, 9, 10, 9, 9, 9, 9 };
+ return oneShotThenLoopFrameAtTick(kUnfold, ARRAYSIZE(kUnfold),
+ kWaitSeq, ARRAYSIZE(kWaitSeq),
+ numFrames, elapsedMs);
+}
+
+uint bigMapDetailPartnerFrameAtTick(uint numFrames, uint32 elapsedMs) {
+ // Script 0x13 @ 29be:1992 (count-up 0..7, 0x80) â on terminator,
+ // `_DoMapScreen @ 20fe:1390` swaps to `_SmallMapWaitSeq` @ 29be:1548
+ // (18 entries: hold cell 7 with a single cell-10 fidget) â fidget
+ // every ~1.8 s.
+ static const uint8 kUnfold[] = { 0, 1, 2, 3, 4, 5, 6, 7 };
+ static const uint8 kWaitSeq[] = {
+ 7, 7, 7, 10, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7
+ };
+ return oneShotThenLoopFrameAtTick(kUnfold, ARRAYSIZE(kUnfold),
+ kWaitSeq, ARRAYSIZE(kWaitSeq),
+ numFrames, elapsedMs);
+}
+
void SiteScreen::enter(uint siteNum) {
if (!_mystery || !_mystery->isLoaded()) {
warning("SiteScreen::enter: no mystery loaded");
diff --git a/engines/eem/site.h b/engines/eem/site.h
index 51129bfed74..33f6f23ab55 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -44,6 +44,26 @@ class Mystery;
/// values that point past the asset.
uint partnerFrameAtTick(uint16 seqnum, uint numFrames, uint32 tickMs);
+/// BigMap-overview partner frame: plays the count-up 0..8 once (the
+/// "unfold the map" pose), then loops the `_BigMapWaitSeq` hold
+/// (9,9,9,9,10,9,9,9,9). Mirrors `_DoBigMap @ 20fe:09e7`'s two-phase
+/// dispatch â the slot starts on script 0x14 (count-up @ 29be:196a)
+/// and on the 0x80 terminator the slot's script pointer is rewritten
+/// to `_BigMapWaitSeq @ 29be:1574`. `partnerFrameAtTick` can't model
+/// that swap on its own (it always loops), so without this helper the
+/// unfold cycles forever instead of resting on the open-map pose.
+/// `elapsedMs` is the time since the BigMap was opened.
+uint bigMapPartnerFrameAtTick(uint numFrames, uint32 elapsedMs);
+
+/// BigMap-detail (zoomed view) partner frame. Same two-phase shape as
+/// `bigMapPartnerFrameAtTick`, but the original `_DoMapScreen @ 20fe:120b`
+/// uses script 0x13 (count-up 0..7 @ 29be:1992) for the unfold and
+/// swaps the slot pointer to `_SmallMapWaitSeq @ 29be:1548` (an
+/// 18-frame hold of cell 7 with a single cell-10 fidget) on the
+/// terminator (`MOV [BX+0x789f],0x1548` at 20fe:1390). `elapsedMs`
+/// is the time since the detail screen was opened.
+uint bigMapDetailPartnerFrameAtTick(uint numFrames, uint32 elapsedMs);
+
/// Anchor-aware masked blit. Mirrors the per-frame anchor offset
/// math in `_UpdateAnimations @ 172b:09c1`:
/// `blit_x = anchor_x - frame.miscflags`, `blit_y = anchor_y -
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 55554a7ff30..7d8666b36ca 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -1980,8 +1980,15 @@ void EEMEngine::doBigMap() {
// STAGE 1 â Overview: PIC 0x42 + clickable site icons.
// ------------------------------------------------------------------
- drawBigMapOverview();
- uint32 mapLastTick = g_system->getMillis();
+ // Anchor for the partner-sprite timeline. `_DoBigMap`'s
+ // `_NewAnimation` call seeds the slot's frame index to 0xffff so
+ // the first `_UpdateAnimations` tick starts at script[0]; we mirror
+ // that by passing elapsed-since-open (zero on the first paint) into
+ // `bigMapPartnerFrameAtTick`, which plays the unfold once and then
+ // loops the wait sequence.
+ const uint32 mapStartTick = g_system->getMillis();
+ drawBigMapOverview(0);
+ uint32 mapLastTick = mapStartTick;
// Static rectangles read directly from the binary at the labelled
// addresses (29be:0x1596 onwards). Format is {x1, y1, x2, y2}.
@@ -2032,7 +2039,7 @@ void EEMEngine::doBigMap() {
const uint32 now = g_system->getMillis();
if (now - mapLastTick >= 100) {
mapLastTick = now;
- drawBigMapOverview();
+ drawBigMapOverview(now - mapStartTick);
}
g_system->updateScreen();
g_system->delayMillis(10);
@@ -2069,8 +2076,12 @@ void EEMEngine::doBigMap() {
int scrollX = MAX<int>(0, MIN<int>(mapW - kMapWinW, zoomX));
int scrollY = MAX<int>(0, MIN<int>(mapH - kMapWinH, zoomY));
- drawBigMapDetail(scrollX, scrollY, mapPixels, mapW, mapH);
- uint32 detailLastTick = g_system->getMillis();
+ // Anchor the detail-screen partner timeline (mirrors `_DoMapScreen`'s
+ // `_NewAnimation` seeding the slot's frame index to 0xffff). The
+ // unfold (script 0x13) plays once, then `_SmallMapWaitSeq` loops.
+ const uint32 detailStartTick = g_system->getMillis();
+ drawBigMapDetail(scrollX, scrollY, mapPixels, mapW, mapH, 0);
+ uint32 detailLastTick = detailStartTick;
while (!shouldQuit()) {
Common::Event ev;
@@ -2201,13 +2212,14 @@ void EEMEngine::doBigMap() {
dirty = true;
}
if (dirty)
- drawBigMapDetail(scrollX, scrollY, mapPixels, mapW, mapH);
+ drawBigMapDetail(scrollX, scrollY, mapPixels, mapW, mapH,
+ now - detailStartTick);
g_system->updateScreen();
g_system->delayMillis(10);
}
}
-void EEMEngine::drawBigMapOverview() {
+void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
// Map-overview redraw â formerly the `drawOverview` lambda inside
// `doBigMap`. PIC 0x42 frame + per-site marker (Done / Crime / Site
// per `_DrawBigMapButtons @ 20fe:0877`) + the partner idle sprite.
@@ -2217,6 +2229,11 @@ void EEMEngine::drawBigMapOverview() {
// at (0xfd, 0x50). We don't track LastScreen finely enough so we
// always render the IDLE pose at (0xfd, 0x50). Idle anim ID:
// Jake = 0x14 (20), Jenny = 0x12 (18).
+ //
+ // `elapsedMs` is the time since `doBigMap` opened â the partner-sprite
+ // timeline anchor. `bigMapPartnerFrameAtTick` uses it to play the
+ // unfold script (0..8) once, then loop `_BigMapWaitSeq` (the open-map
+ // hold). Without that anchor the unfold would loop indefinitely.
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
@@ -2295,9 +2312,8 @@ void EEMEngine::drawBigMapOverview() {
const uint kMapAniId = (_partner == 0) ? 0x14 : 0x12;
Animation mapAnim;
if (_aniArchive.loadAnimation(kMapAniId, mapAnim) && !mapAnim.empty()) {
- const uint32 now = g_system->getMillis();
- const uint frameIdx = partnerFrameAtTick(0x14,
- (uint)mapAnim.size(), now);
+ const uint frameIdx = bigMapPartnerFrameAtTick((uint)mapAnim.size(),
+ elapsedMs);
// Anchor-aware: the BigMap walk-cycle has miscflags = -2 per
// cell, so the partner shifts left as it cycles â without the
// anchor adjustment the sprite "shakes in place" instead of
@@ -2313,12 +2329,17 @@ void EEMEngine::drawBigMapOverview() {
void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
const Common::Array<byte> &mapPixels,
- uint16 mapW, uint16 mapH) {
+ uint16 mapW, uint16 mapH,
+ uint32 elapsedMs) {
// Map-detail redraw â formerly the `drawDetail` lambda inside
// `doBigMap`. PIC 0x43 frame + a 0xe9 Ã 0xab BIGMAP.PIC viewport at
// (2, 2), stamped site buttons, and the partner sprite at (0x101,
// 0x50) â `_DoMapScreen @ 20fe:120b` (`_NewAnimation` at
// 20fe:12cd-12f0, anim 0x13 Jake / 0x11 Jenny, seqnum 0x13).
+ //
+ // `elapsedMs` is the time since the detail screen was opened â
+ // `bigMapDetailPartnerFrameAtTick` uses it to play the unfold once
+ // and then loop `_SmallMapWaitSeq`.
const int kMapWinW = 0xe9;
const int kMapWinH = 0xab;
const int kMapWinX = 2;
@@ -2392,9 +2413,8 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
Animation detailAnim;
if (_aniArchive.loadAnimation(kDetailAniId, detailAnim) &&
!detailAnim.empty()) {
- const uint32 now = g_system->getMillis();
- const uint frameIdx = partnerFrameAtTick(0x13,
- (uint)detailAnim.size(), now);
+ const uint frameIdx = bigMapDetailPartnerFrameAtTick(
+ (uint)detailAnim.size(), elapsedMs);
blitAnimFrameAnchored(scratch.surfacePtr(),
detailAnim[frameIdx], 0x101, 0x50);
}
Commit: ef46d69023cf56dd56aeef76f538ba43d8fcef99
https://github.com/scummvm/scummvm/commit/ef46d69023cf56dd56aeef76f538ba43d8fcef99
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:43+02:00
Commit Message:
EEM: fixed missing frames in partner animations
Changed paths:
engines/eem/clues.cpp
engines/eem/site.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 1ad73525d3d..d2635e3823d 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -601,8 +601,22 @@ void EEMEngine::displayClue(const byte *clueBlock) {
// This is what surfaces "Jenny takes a picture with a camera"
// (and the matching Jake gestures) during NPC searches.
const int16 kdAnimNum = (int16)READ_LE_UINT16(c + 0x3a);
- if (kdAnimNum != -1)
+ if (kdAnimNum != -1) {
playKdAnim((uint16)kdAnimNum);
+ // `playKdAnim` leaves the screen on the partner-less
+ // `_partnerEraseBg`, mirroring its between-frame erase
+ // source. Re-stamp `bg` so the partner sprite captured at
+ // `displayClue` entry returns before we draw the speaker
+ // portrait + balloon. Without this the bubble lands on a
+ // partner-less screen and the partner appears invisible
+ // for the entire first iteration. Mirrors the original's
+ // `_UpdateAnimations @ 172b:09c1` swap on `0x80`: the
+ // kdAnim slot is freed and the WaitHandle (partner) slot
+ // is reactivated with frame index 0xffff, so on the next
+ // tick the partner renders at script[0] of its wait anim.
+ g_system->copyRectToScreen(bg.getPixels(), bg.pitch,
+ 0, 0, 320, 200);
+ }
const bool useP1 = (_partner == 1) &&
(READ_LE_UINT16(c + 10) != 0xFFFF);
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 54d3673f501..062f81f04ba 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -894,6 +894,13 @@ void SiteScreen::enterSiteAnim() {
// frame-by-frame anchors decrease as the animation progresses):
// destX = -frame.miscflags (on-disk byte 8 = anchor X)
// destY = kKDY - frame.rowoff (on-disk byte 6 = anchor Y)
+ // Both anchors are SIGNED int16 (mirrors `blitAnimFrameAnchored` /
+ // `_UpdateAnimations @ 172b:09c1`). The original's `-pPVar8->width`
+ // negation wraps in 16-bit on DOS, so a frame with `miscflags = -2`
+ // (0xFFFE) lands at destX = +2 in the original. Without the int16
+ // re-cast our 32-bit negation produces destX = -65534 and the
+ // second-to-last frame (which has a non-zero anchor in anim 7/0xf)
+ // clips entirely off-screen, leaving a one-tick partner-less gap.
// Each frame waits one `_CheckFrameRate` tick â we use 80 ms which
// matches the original's ~12 FPS pacing.
Animation kd;
@@ -903,8 +910,8 @@ void SiteScreen::enterSiteAnim() {
frameIdx++) {
const Picture &fr = kd[frameIdx];
const byte transp = (byte)(fr.flags >> 8);
- const int destX = -(int)fr.miscflags;
- const int destY = kKDY - (int)fr.rowoff;
+ const int destX = -(int)(int16)fr.miscflags;
+ const int destY = kKDY - (int)(int16)fr.rowoff;
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
Commit: d7268b5a91f241aea153b8685404f40cc2b1a8a7
https://github.com/scummvm/scummvm/commit/d7268b5a91f241aea153b8685404f40cc2b1a8a7
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:43+02:00
Commit Message:
EEM: better handling of saves
Changed paths:
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/ui.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index fa895d640a6..529970dcbdf 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -749,7 +749,20 @@ Common::Error EEMEngine::loadGameStream(Common::SeekableReadStream *stream) {
SaveStateList EEMEngine::listProfiles() const {
// Mirrors `_findfirst("*.PLR")` in `screen8_handler @ 1c33:1012`.
- return getMetaEngine()->listSaves(_targetName.c_str());
+ // We disable autosave (`getAutosaveSlot()` returns -1) but a
+ // pre-existing slot-0 file from a previous run, or one written by
+ // another engine using the same target, would still show up here
+ // and pollute the picker. Filter it out so the picker treats slot
+ // 0 as if it didn't exist â matching the original which never
+ // has an autosave concept.
+ SaveStateList saves = getMetaEngine()->listSaves(_targetName.c_str());
+ for (uint i = 0; i < saves.size(); ) {
+ if (saves[i].getSaveSlot() == 0)
+ saves.remove_at(i);
+ else
+ i++;
+ }
+ return saves;
}
Common::Error EEMEngine::saveProfile(const Common::String &name) {
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index af19e0c43fc..97b8b162ece 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -115,6 +115,15 @@ public:
bool canLoadGameStateCurrently(Common::U32String *msg = nullptr) override;
bool canSaveGameStateCurrently(Common::U32String *msg = nullptr) override;
+ // Disable ScummVM's periodic autosave. The original engine's
+ // `screen8_handler @ 1c33:1012` builds the profile picker by
+ // walking every `*.PLR` file in the save dir, and we mirror that
+ // via `listProfiles() â MetaEngine::listSaves`. Letting the
+ // framework write a slot-0 autosave creates a phantom profile
+ // that shows up on the picker as a real save. Returning -1 tells
+ // the framework to skip autosave entirely.
+ int getAutosaveSlot() const override { return -1; }
+
// ScummVM extended-save hooks. The base `Engine::saveGameState` /
// `loadGameState` write/read the framework header (description,
// thumbnail, playtime, version) around our body via these
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 7d8666b36ca..c4d9c5282f4 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -242,6 +242,15 @@ void EEMEngine::doProfilePicker() {
// `local_20 == 0` at 1c33:1170), it falls straight into
// `_NewPlayer`. Selecting an entry calls `_LoadPlayerRecord` and
// returns; selecting the "exit" sentinel goes back to title.
+
+ // Palette reset. `screen8_handler` runs `_FadeOut(); _GetPalette(0);
+ // _GetBackground(0x104);` before the picker, so the BG always
+ // renders against SITEPALS index 0 regardless of which intro
+ // palette was active last. Without this, skipping out of an intro
+ // anim (THEME / ANIM01..20 / TITLE) leaves the previous video's
+ // palette in place and the picker draws with the wrong colours.
+ setSitePalette(0);
+
const SaveStateList saves = listProfiles();
if (saves.empty()) {
doNewPlayer();
@@ -274,6 +283,16 @@ void EEMEngine::doProfilePicker() {
int sel = 0;
bool done = false;
+ // Picker geometry: `DrawList @ 1c33:040d` is called from
+ // `screen8_handler @ 1c33:1012` with `(_TextBox + 3, DAT_29be_0d02)`.
+ // `_TextBox @ 29be:0d00` holds {x1=58, y1=35, x2=238, y2=158} so
+ // the list origin is (61, 35), 10 px per row, max 12 visible
+ // rows. The "Pick a player" caption is part of the BG (PIC 0x104)
+ // â `screen8_handler` never draws it as text â so an extra
+ // `drawString` would overlay on top of the baked-in heading.
+ const int kListX = 61;
+ const int kListY = 35;
+ const int kLineH = 10;
auto draw = [&]() {
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
@@ -286,12 +305,10 @@ void EEMEngine::doProfilePicker() {
memcpy((byte *)scratch.getBasePtr(0, row),
(const byte *)bg.surface.getBasePtr(0, row), w);
}
- _font.drawString(&scratch, "Pick a player:", 80, 30, 220, 0xF);
- const int kLineH = 12;
for (uint i = 0; i < entries.size(); i++) {
const byte color = ((int)i == sel) ? 0xF : 0x8;
_font.drawString(&scratch, entries[i].label,
- 80, 60 + (int)i * kLineH, 220, color);
+ kListX, kListY + (int)i * kLineH, 220, color);
}
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
@@ -331,8 +348,7 @@ void EEMEngine::doProfilePicker() {
}
}
if (ev.type == Common::EVENT_LBUTTONDOWN) {
- const int kLineH = 12;
- const int hit = (ev.mouse.y - 60) / kLineH;
+ const int hit = (ev.mouse.y - kListY) / kLineH;
if (hit >= 0 && hit < (int)entries.size()) {
sel = hit;
committed = true;
Commit: e87d591b963cb98745dd48c701b119690865f74f
https://github.com/scummvm/scummvm/commit/e87d591b963cb98745dd48c701b119690865f74f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:43+02:00
Commit Message:
EEM: remove unused screen
Changed paths:
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/ui.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 529970dcbdf..46fa0497f45 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -266,22 +266,31 @@ Common::Error EEMEngine::run() {
// iteration. Sentinel `kScreenInvalid` (0xFFFF) ends the loop â
// same as the original's table-end marker.
//
- // Initial value `kScreenAction` matches the original flow at the
- // tail of `_DoChoosePartner @ 1a35:099d` which sets
- // `_NextScreen = 0xc` once the partner has been picked. (The
- // resume path above bypasses this and seeds `kScreenMap` instead.)
+ // `_DoChoosePartner @ 1a35:099d` sets `_NextScreen = 0xc` (= the
+ // original `_ActionScreen` â "Choose A Mystery / Practice Mystery /
+ // See ScrapBook 1..3"). In our port that menu lives inside
+ // `doCaseSelection`, which already mirrors `_ActionScreen`'s 5-entry
+ // list (verified against `ActionNames @ 29be:0d6a`) and rolls the
+ // individual-mystery picker (`_CaseSelection @ 1c33:0a87`) into the
+ // same flow. So we route straight to `kScreenChooseMystery`; the
+ // `kScreenAction` (= 0xc) state still exists for the post-win path
+ // (`_DisplayCorrect @ 1df2:0895` writes 0xc) but its handler also
+ // dispatches `doCaseSelection`. The previous `doActionScreen` was a
+ // synthetic stub ("What now?" / "Solve a Mystery" / "Look at My
+ // Books") whose strings are nowhere in the binary â confirmed by a
+ // `search_strings` for "What" returning zero matches.
//
// Mid-mystery profile resume: if the profile picker loaded a
// save whose `hasMystery` flag was set, `_mystery.isLoaded()` is
// true here and the player just re-picked their partner. Drop
- // straight to MAP rather than ACTION so they don't have to walk
- // back through the case picker (which would `_mystery.load()`
- // fresh and discard their site / clue progress). The original
- // has no equivalent â it persists only profile-level state via
- // `_PlayerRecord`, not in-progress mysteries â so this is a
- // ScummVM-only ergonomics improvement.
+ // straight to MAP rather than the action menu so they don't have
+ // to walk back through the case picker (which would
+ // `_mystery.load()` fresh and discard their site / clue
+ // progress). The original has no equivalent â it persists only
+ // profile-level state via `_PlayerRecord`, not in-progress
+ // mysteries â so this is a ScummVM-only ergonomics improvement.
if (!shouldQuit() && !resumed)
- _nextScreen = _mystery.isLoaded() ? kScreenMap : kScreenAction;
+ _nextScreen = _mystery.isLoaded() ? kScreenMap : kScreenChooseMystery;
screen_loop:
while (!shouldQuit() && _nextScreen != kScreenInvalid) {
const ScreenId current = (ScreenId)_nextScreen;
@@ -289,20 +298,27 @@ screen_loop:
switch (current) {
case kScreenAction:
- // Post-mystery menu. `_ActionScreen` sets _NextScreen via
- // its action jumptable (1c33:1be1) â see `doActionScreen`.
- doActionScreen();
+ // Post-mystery menu. The original's `_ActionScreen @
+ // 1c33:195b` shows the 5-entry "Choose A Mystery /
+ // Practice / ScrapBook" picker; `doCaseSelection` is
+ // our port of that exact menu (plus the individual-case
+ // sub-picker the original handles in `_CaseSelection @
+ // 1c33:0a87`). After the player picks, fall through to
+ // the same routing the `kScreenChooseMystery` case uses.
+ // Reachable from `_DisplayCorrect`'s 0xc write after a
+ // solve (see `ui.cpp` `_nextScreen = kScreenAction`).
+ doCaseSelection();
+ _nextScreen = _mystery.isLoaded() ? kScreenInitClues
+ : kScreenInvalid;
break;
case kScreenChooseMystery:
// Handler 10 at 1a35:0e0e calls `_DoChooseMystery` which
// presets `_NextScreen = 0` (INIT_CLUES) before
- // `_CaseSelection`. If the picker bails out without
- // loading a mystery (no `_ReadMystery` call), drop back
- // to ACTION instead of falling into a missing case.
+ // `_CaseSelection`. Same dispatch as `kScreenAction`.
doCaseSelection();
_nextScreen = _mystery.isLoaded() ? kScreenInitClues
- : kScreenAction;
+ : kScreenInvalid;
break;
case kScreenInitClues:
@@ -310,7 +326,7 @@ screen_loop:
// then writes `_NextScreen = 1` (MAP).
doInitClues();
_nextScreen = _mystery.isLoaded() ? kScreenMap
- : kScreenAction;
+ : kScreenChooseMystery;
break;
case kScreenMap:
@@ -323,7 +339,7 @@ screen_loop:
// the natural next state is SITE.
doBigMap();
if (!_mystery.isLoaded())
- _nextScreen = kScreenAction;
+ _nextScreen = kScreenChooseMystery;
else if (_nextScreen == current)
_nextScreen = kScreenSite;
break;
@@ -340,7 +356,7 @@ screen_loop:
// via `_LastScreen`).
doSiteLoop();
if (!_mystery.isLoaded())
- _nextScreen = kScreenAction;
+ _nextScreen = kScreenChooseMystery;
else if (_nextScreen == current)
_nextScreen = kScreenInvalid; // user quit
break;
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 97b8b162ece..ab7389a4b9e 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -349,21 +349,6 @@ private:
void doCaseSelection();
void doSiteLoop();
- /// Post-mystery action menu. Mirrors `_ActionScreen @ 1c33:195b` â
- /// the screen the original returns to after `_DisplayCorrect`
- /// (winner) or after the player explicitly leaves a case via the
- /// PROFILE â PARTNER chain. The original offers up to 5 choices
- /// gated on the player's chain stage (`DAT_2d5d_3f99`):
- /// 1: "Solve a Mystery" (set _NextScreen=10 â CHOOSE_MYSTERY)
- /// 3: replay the last solved case (`_ReloadMystery(0)` callsite)
- /// 5: scrapbook viewer (`_ShowOneScrap(0, 1)` callsite)
- /// 7: chain-stage advance (cmp `_3f99 == 1`)
- /// 9: chain-stage advance (cmp `_3f99 == 2` / `== 3`)
- /// sentinel: exit / back to PARTNER
- /// We start with just option 1 wired (the practical loop) plus
- /// quit; the others slot in as their underlying screens land.
- void doActionScreen();
-
/// Setup / preferences screen. Mirrors `_DoSetup @ 1f78:044e` â
/// per-profile preferences (voice on/off via `DAT_2d5d_3f97`,
/// partner pick via SwapColors on Kid1/Kid2 rects). Reachable
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index c4d9c5282f4..f91ae57ffa7 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -732,154 +732,6 @@ void EEMEngine::doSetup() {
}
}
-void EEMEngine::doActionScreen() {
- // Mirrors `_ActionScreen @ 1c33:195b` â the post-mystery menu the
- // original loops back to after `_DisplayCorrect` (winner). The
- // original draws PIC 0x122 (Cok), 0x124 (Cexit), then calls
- // `_DoChoose(ActionNames)` and dispatches the result via the
- // jumptable at 1c33:1be1 (verified to set `_NextScreen` for each
- // action â entry 1 â 10 = CHOOSE_MYSTERY, entry 3 â
- // `_ReloadMystery`, etc.).
- //
- // We render a minimal text version of the menu â the original's
- // background pic + "Cok" / "Cexit" overlays land once the
- // `_DoChoose` UI lands. For now: two practical entries â "Solve a
- // Mystery" (the only one whose underlying screen is wired) and
- // "Quit". Sets `_nextScreen` exactly the same way the original
- // jumptable does:
- // "Solve a Mystery" â kScreenChooseMystery (matches handler 1
- // at 1c33:1add: `MOV [_NextScreen], 0xa`)
- // "Quit" â kScreenInvalid (matches the sentinel
- // handler at 1c33:1afa)
- if (!_font.isLoaded()) {
- _nextScreen = kScreenChooseMystery;
- return;
- }
-
- enum ActionKind {
- kActSolve, // â kScreenChooseMystery (action 1, 1c33:1add)
- kActScrapbook, // â doShowEnding(lastSolved) (action 5, 1c33:1b13)
- kActQuit
- };
- struct Entry {
- const char *label;
- ActionKind kind;
- };
- // Locate the highest-numbered solved mystery â that's the one
- // the player most recently completed, and what the action-menu
- // "Look at My Books" entry replays. If none solved yet, hide
- // the entry (matches the original's grey-out at 1c33:19f3 where
- // `local_24[5] = 1` before the chain-stage check toggles it).
- int lastSolved = -1;
- for (int i = (int)sizeof(_mysteriesSolved) - 1; i >= 0; i--) {
- if (_mysteriesSolved[i] != 0) {
- lastSolved = i;
- break;
- }
- }
-
- Common::Array<Entry> entries;
- Entry solveEntry; solveEntry.label = "Solve a Mystery"; solveEntry.kind = kActSolve;
- entries.push_back(solveEntry);
- if (lastSolved >= 0) {
- Entry sb; sb.label = "Look at My Books"; sb.kind = kActScrapbook;
- entries.push_back(sb);
- }
- Entry quitEntry; quitEntry.label = "Quit"; quitEntry.kind = kActQuit;
- entries.push_back(quitEntry);
- const int kCount = (int)entries.size();
-
- int sel = 0;
- auto draw = [&]() {
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
- Picture bg;
- if (_picsArchive.getPicture(0x104, bg)) {
- const int w = MIN<int>(bg.surface.w, 320);
- const int h = MIN<int>(bg.surface.h, 200);
- for (int row = 0; row < h; row++)
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)bg.surface.getBasePtr(0, row), w);
- }
- _font.drawString(&scratch, "What now?", 100, 30, 220, 0xF);
- for (int i = 0; i < kCount; i++) {
- const byte color = (i == sel) ? 0xF : 0x8;
- _font.drawString(&scratch, entries[i].label,
- 100, 60 + i * 14, 220, color);
- }
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
- };
- draw();
-
- while (!shouldQuit()) {
- Common::Event ev;
- bool dirty = false;
- bool committed = false;
- while (g_system->getEventManager()->pollEvent(ev)) {
- if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
- _nextScreen = kScreenInvalid;
- return;
- }
- if (ev.type == Common::EVENT_KEYDOWN) {
- switch (ev.kbd.keycode) {
- case Common::KEYCODE_UP:
- sel = (sel + kCount - 1) % kCount;
- dirty = true;
- break;
- case Common::KEYCODE_DOWN:
- sel = (sel + 1) % kCount;
- dirty = true;
- break;
- case Common::KEYCODE_RETURN:
- case Common::KEYCODE_KP_ENTER:
- committed = true;
- break;
- case Common::KEYCODE_ESCAPE:
- _nextScreen = kScreenInvalid;
- return;
- default:
- break;
- }
- }
- if (ev.type == Common::EVENT_LBUTTONDOWN) {
- const int hit = (ev.mouse.y - 60) / 14;
- if (hit >= 0 && hit < kCount) {
- sel = hit;
- committed = true;
- }
- }
- if (committed)
- break;
- }
- if (committed) {
- switch (entries[sel].kind) {
- case kActSolve:
- _nextScreen = kScreenChooseMystery;
- return;
- case kActScrapbook:
- if (lastSolved >= 0)
- doShowEnding((uint)lastSolved);
- // Fall through to redraw the menu after viewing.
- draw();
- continue;
- case kActQuit:
- _nextScreen = kScreenInvalid;
- return;
- }
- return;
- }
- if (dirty)
- draw();
- g_system->updateScreen();
- g_system->delayMillis(15);
- }
- _nextScreen = kScreenInvalid;
-}
-
void EEMEngine::doCaseSelection() {
// Mirrors `_CaseSelection` @ 1c33:0a87. The original draws PIC 0x41
// (chooser background) plus a centred "Book %d" / "Challenge Book"
Commit: 253e2f0bb20aad598b1e3948dde3439cba853301
https://github.com/scummvm/scummvm/commit/253e2f0bb20aad598b1e3948dde3439cba853301
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:44+02:00
Commit Message:
EEM: improve the setup screen with actually triggers to the code relevant
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index f91ae57ffa7..d857dce9877 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -636,21 +636,79 @@ void EEMEngine::doShowEnding(uint num) {
}
void EEMEngine::doSetup() {
- // Mirrors `_DoSetup @ 1f78:044e`. Loads BG pic 0x40 +
- // state-button pics 0x9b/0x9c/0x9d/0x9e. The original wires 13
- // hot-rects (`_SetupButtons @ 29be:1218`) but only two of them
- // drive persistent state â `Kid1`/`Kid2` (partner) and
- // `SoundOn`/`SoundOff` (voice flag, `DAT_2d5d_3f97`). The rest
- // are reset/help/return buttons. We render a minimal text
- // version: two toggle lines, click to flip, ESC to leave.
+ // Mirrors `_DoSetup @ 1f78:044e`. The setup screen is BG `PIC 0x40`
+ // (loaded once on entry) with every label baked in â "Setup",
+ // "Partner", "Sound", "Music", the "Jake"/"Jenny"/"On"/"Off"
+ // option strings, etc. â all rendered in palette key `0xFE`. The
+ // original then runs `_SetupSettings @ 1f78:000d` which uses
+ // `_SwapColors @ 172b:1d2a` to recolour those `0xFE` pixels per
+ // label rect: `0x15` for the active state, `0` for the inactive
+ // one. So nothing is drawn as text; the visible state of each
+ // toggle is purely a per-rect colour swap on top of `PIC 0x40`.
+ //
+ // Click hit-tests go through `_SetupButtons @ 29be:1218` â 13Ã
+ // 8-byte rects. Each click runs `HandleSetupButton @ 1f78:0158`,
+ // which dispatches via the 12-entry jumptable at `1f78:0436`.
+ // Verified handler map (decompiled at each jumptable target):
+ // [0] ( 20, 44, 39, 61) Partner toggle (1f78:017a)
+ // [1] ( 20, 87, 39,104) Voice toggle (1f78:0196 â DAT_2d5d_3f97)
+ // [2] ( 20,127, 39,144) back to profile (NextScreen=8)
+ // [3] (281, 43,299, 60) ScrapBook 1 (_ShowScrapbook(0,1))
+ // [4] (281, 62,299, 79) ScrapBook 2 gated chainStage>=2
+ // [5] (281, 81,299, 98) ScrapBook 3 gated chainStage>=3
+ // [6] (281,108,299,125) Save game (_SaveGame @ 2404:0c87)
+ // [7] (281,127,299,144) New Case (NextScreen=0xa)
+ // [8] ( 53,153,108,183) Done (SI=1, exit)
+ // [9] (145,163,174,187) Help (_InterfaceHelp(1))
+ // [10] (212,153,266,184) Quit (_AreYouSure â NextScreen=0xffff)
+ // [11] ( 81, 25,238, 37) Credits (PIC 0x208 fullscreen)
+ // [12] ( 11, 1, 3, 3) debug placeholder
+ // Highlight rects (`Kid1 @ 29be:1320` / `Kid2 @ 29be:1328` /
+ // `SoundOn @ 29be:1330` / `SoundOff @ 29be:1338`) drive
+ // `_SwapColors`; they're not click targets in the original.
if (!_font.isLoaded()) {
_nextScreen = (ScreenId)_lastScreen;
return;
}
- const Common::Rect kBackBtn(120, 170, 200, 188);
- const Common::Rect kPartnerToggle(40, 60, 280, 78);
- const Common::Rect kVoiceToggle (40, 90, 280, 108);
+ // Original button rects (`_SetupButtons` indices wired here).
+ const Common::Rect kPartnerBtn ( 20, 44, 39, 61); // [0]
+ const Common::Rect kVoiceBtn ( 20, 87, 39, 104); // [1]
+ const Common::Rect kProfileBtn ( 20, 127, 39, 144); // [2]
+ const Common::Rect kScrap1Btn (281, 43, 299, 60); // [3]
+ const Common::Rect kScrap2Btn (281, 62, 299, 79); // [4]
+ const Common::Rect kScrap3Btn (281, 81, 299, 98); // [5]
+ const Common::Rect kSaveBtn (281, 108, 299, 125); // [6]
+ const Common::Rect kNewCaseBtn (281, 127, 299, 144); // [7]
+ const Common::Rect kDoneBtn ( 53, 153, 108, 183); // [8]
+ const Common::Rect kHelpBtn (145, 163, 174, 187); // [9]
+ const Common::Rect kQuitBtn (212, 153, 266, 184); // [10]
+ const Common::Rect kCreditsBtn ( 81, 25, 238, 37); // [11]
+ // Highlight / fallback-click rects.
+ const Common::Rect kKid1Rect ( 99, 44, 148, 52);
+ const Common::Rect kKid2Rect ( 99, 54, 148, 62);
+ const Common::Rect kSoundOnRect (106, 86, 125, 94);
+ const Common::Rect kSoundOffRect (106, 96, 125, 104);
+
+ // Pixel-level color-key swap. Mirrors `_SwapColors @ 172b:1d2a`:
+ // for each pixel in `r` whose value equals `from`, replace with
+ // `to`. `0xFE` is the BG's text-key color; `0x15` is the active
+ // (bright) palette index, `0x00` the inactive one â both verified
+ // in `_SetupSettings @ 1f78:000d`.
+ auto swapColors = [](Graphics::ManagedSurface &dst,
+ const Common::Rect &r, byte from, byte to) {
+ const int x1 = MAX<int>(0, r.left);
+ const int y1 = MAX<int>(0, r.top);
+ const int x2 = MIN<int>(320, r.right);
+ const int y2 = MIN<int>(200, r.bottom);
+ for (int y = y1; y < y2; y++) {
+ byte *row = (byte *)dst.getBasePtr(0, y);
+ for (int x = x1; x < x2; x++) {
+ if (row[x] == from)
+ row[x] = to;
+ }
+ }
+ };
auto draw = [&]() {
Graphics::ManagedSurface scratch(320, 200,
@@ -664,22 +722,121 @@ void EEMEngine::doSetup() {
memcpy((byte *)scratch.getBasePtr(0, row),
(const byte *)bg.surface.getBasePtr(0, row), w);
}
- _font.drawString(&scratch, "Setup", 140, 30, 80, 0xF);
- const Common::String partnerLine = Common::String::format(
- "Partner: %s (click to switch)",
- _partner == 0 ? "Jake" : "Jenny");
- _font.drawString(&scratch, partnerLine, 50, 64, 240, 0xF);
- const Common::String voiceLine = Common::String::format(
- "Voice: %s (click to toggle)",
- _voiceOn ? "ON" : "OFF");
- _font.drawString(&scratch, voiceLine, 50, 94, 240, 0xF);
- _font.drawString(&scratch, "[ Back ]", 130, 174, 80, 0xF);
+
+ const byte kKey = 0xFE;
+ const byte kBright = 0x15;
+ const byte kDim = 0x00;
+ swapColors(scratch, kKid1Rect, kKey,
+ _partner == 0 ? kBright : kDim);
+ swapColors(scratch, kKid2Rect, kKey,
+ _partner == 1 ? kBright : kDim);
+ swapColors(scratch, kSoundOnRect, kKey,
+ _voiceOn ? kBright : kDim);
+ swapColors(scratch, kSoundOffRect, kKey,
+ _voiceOn ? kDim : kBright);
+
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
g_system->updateScreen();
};
draw();
+ // Modal "Are you sure?" yes/no prompt. Mirrors `_AreYouSure @
+ // 1a35:0a5c` â the original draws a centred message, listens for
+ // Y/Enter (confirm) or N/ESC (cancel). We render a minimal
+ // overlay with Y / N keys (and click on left/right halves) so
+ // the Quit button gives the player a chance to back out.
+ auto areYouSure = [&]() -> bool {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ Graphics::Surface *cur = g_system->lockScreen();
+ if (cur) {
+ for (int row = 0; row < 200; row++)
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)cur->getBasePtr(0, row), 320);
+ g_system->unlockScreen();
+ }
+ const Common::Rect kBox(80, 80, 240, 120);
+ scratch.fillRect(kBox, 0x00);
+ _font.drawString(&scratch, "Are you sure?", 100, 88, 200, 0xF);
+ _font.drawString(&scratch, "Y = yes N = no", 100, 102, 200, 0xF);
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ while (!shouldQuit()) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return true;
+ if (ev.type == Common::EVENT_KEYDOWN) {
+ const Common::KeyCode k = ev.kbd.keycode;
+ if (k == Common::KEYCODE_y ||
+ k == Common::KEYCODE_RETURN)
+ return true;
+ if (k == Common::KEYCODE_n ||
+ k == Common::KEYCODE_ESCAPE)
+ return false;
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ return ev.mouse.x < 160;
+ }
+ }
+ g_system->delayMillis(15);
+ }
+ return false;
+ };
+
+ // Fullscreen-pic modal. Mirrors `_InterfaceHelp @ 1560:0205`'s
+ // per-frame loop: blit pic, wait for click/key, advance / exit.
+ // Used by Credits (single PIC 0x208) and Help (we reuse for a
+ // minimal stub since the help-pic table at `_InterfaceHelp`'s
+ // offset isn't fully decoded yet).
+ auto showFullscreenPic = [&](uint16 picId) {
+ Picture pic;
+ if (!_picsArchive.getPicture(picId, pic)) {
+ warning("doSetup: PIC %u missing", (uint)picId);
+ return;
+ }
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ const int w = MIN<int>(pic.surface.w, 320);
+ const int h = MIN<int>(pic.surface.h, 200);
+ for (int row = 0; row < h; row++)
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)pic.surface.getBasePtr(0, row), w);
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ while (!shouldQuit()) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER ||
+ ev.type == Common::EVENT_KEYDOWN ||
+ ev.type == Common::EVENT_LBUTTONDOWN)
+ return;
+ }
+ g_system->delayMillis(15);
+ }
+ };
+
+ auto leaveSetup = [&]() {
+ // `_DoSetup`'s entry writes `_NextScreen = _LastScreen`. We
+ // honor any handler that has already overridden `_nextScreen`
+ // (Credits / Save don't, but New Case / Quit do). Otherwise
+ // fall back to `_lastScreen`.
+ if (_nextScreen == kScreenSetup) {
+ _nextScreen = (ScreenId)_lastScreen;
+ if (_nextScreen == kScreenSetup ||
+ _nextScreen == kScreenInvalid)
+ _nextScreen = kScreenMap;
+ }
+ saveProfile(_playerName);
+ };
+
+ _nextScreen = kScreenSetup; // sentinel â leaveSetup picks the real target
while (!shouldQuit()) {
Common::Event ev;
bool dirty = false;
@@ -692,38 +849,131 @@ void EEMEngine::doSetup() {
if (ev.type == Common::EVENT_KEYDOWN) {
if (ev.kbd.keycode == Common::KEYCODE_ESCAPE ||
ev.kbd.keycode == Common::KEYCODE_RETURN) {
- // `_DoSetup @ 1f78:044a` writes `_NextScreen =
- // _LastScreen` on entry. Returning means we just
- // dispatch back to whichever screen called us.
- _nextScreen = (ScreenId)_lastScreen;
- if (_nextScreen == kScreenSetup ||
- _nextScreen == kScreenInvalid)
- _nextScreen = kScreenMap;
- if (_voiceOn)
- saveProfile(_playerName);
+ leaveSetup();
return;
}
}
- if (ev.type == Common::EVENT_LBUTTONDOWN) {
- if (kBackBtn.contains(ev.mouse.x, ev.mouse.y)) {
- _nextScreen = (ScreenId)_lastScreen;
- if (_nextScreen == kScreenSetup ||
- _nextScreen == kScreenInvalid)
- _nextScreen = kScreenMap;
- saveProfile(_playerName);
- return;
- }
- if (kPartnerToggle.contains(ev.mouse.x, ev.mouse.y)) {
- _partner = _partner == 0 ? 1 : 0;
+ if (ev.type != Common::EVENT_LBUTTONDOWN)
+ continue;
+ const int mx = ev.mouse.x;
+ const int my = ev.mouse.y;
+
+ // Partner toggle (button [0]) â original has no symmetric
+ // right-side button (the [3] rect is ScrapBook 1, not a
+ // partner arrow). Direct clicks on the Jake/Jenny labels
+ // are accepted as a more intuitive fallback.
+ if (kPartnerBtn.contains(mx, my)) {
+ _partner = _partner == 0 ? 1 : 0;
+ dirty = true;
+ continue;
+ }
+ if (kKid1Rect.contains(mx, my)) {
+ if (_partner != 0) { _partner = 0; dirty = true; }
+ continue;
+ }
+ if (kKid2Rect.contains(mx, my)) {
+ if (_partner != 1) { _partner = 1; dirty = true; }
+ continue;
+ }
+
+ // Voice toggle (button [1]).
+ if (kVoiceBtn.contains(mx, my)) {
+ _voiceOn = !_voiceOn;
+ if (_audio)
+ _audio->setVoiceEnabled(_voiceOn);
+ dirty = true;
+ continue;
+ }
+ if (kSoundOnRect.contains(mx, my)) {
+ if (!_voiceOn) {
+ _voiceOn = true;
+ if (_audio)
+ _audio->setVoiceEnabled(_voiceOn);
dirty = true;
}
- if (kVoiceToggle.contains(ev.mouse.x, ev.mouse.y)) {
- _voiceOn = !_voiceOn;
+ continue;
+ }
+ if (kSoundOffRect.contains(mx, my)) {
+ if (_voiceOn) {
+ _voiceOn = false;
if (_audio)
_audio->setVoiceEnabled(_voiceOn);
dirty = true;
}
+ continue;
+ }
+
+ // New Case (button [7]). Original handler at 1f78:01ad
+ // sets `_NextScreen = 0xa` (CHOOSE_MYSTERY) and exits the
+ // dispatch loop with SI=1.
+ if (kNewCaseBtn.contains(mx, my)) {
+ saveProfile(_playerName);
+ _nextScreen = kScreenChooseMystery;
+ return;
+ }
+
+ // Save (button [6]). Original calls `_SaveGame @
+ // 2404:0c87` and stays in the setup loop. Our save is
+ // profile-scoped (one slot per player name) â same effect.
+ if (kSaveBtn.contains(mx, my)) {
+ saveProfile(_playerName);
+ continue;
+ }
+
+ // Done (button [8]). Original handler is just `MOV SI,1;
+ // JMP exit` â `_NextScreen` stays at whatever entry set it
+ // to (= `_LastScreen`).
+ if (kDoneBtn.contains(mx, my)) {
+ leaveSetup();
+ return;
+ }
+
+ // Quit (button [10]). Original: `_AreYouSure(0)` â
+ // confirmed â `_NextScreen = 0xffff` (sentinel quit).
+ if (kQuitBtn.contains(mx, my)) {
+ if (areYouSure()) {
+ _nextScreen = kScreenInvalid;
+ return;
+ }
+ dirty = true; // restore the BG after the prompt
+ continue;
}
+
+ // Help (button [9]). Original calls `_InterfaceHelp(1)`,
+ // which walks a sequence of help-pic IDs from a runtime
+ // table we haven't fully decoded. PIC 0x4F is the in-game
+ // help backdrop the briefing chain reuses; we show that
+ // as a stub until the full table is wired.
+ if (kHelpBtn.contains(mx, my)) {
+ showFullscreenPic(0x4F);
+ dirty = true;
+ continue;
+ }
+
+ // Credits (button [11]). Original handler at 1f78:025a
+ // loads PIC 0x208 and blits it fullscreen, then waits
+ // for any input.
+ if (kCreditsBtn.contains(mx, my)) {
+ showFullscreenPic(0x208);
+ // PIC 0x208 has its own palette baked into the BG
+ // dump via `_GetPicture`; the original restores via
+ // `_GetPalette` on return. Reset to setup palette
+ // (SITEPALS index 0) so the setup BG renders right.
+ setSitePalette(0);
+ dirty = true;
+ continue;
+ }
+
+ // Profile (button [2]). Goes back to the profile picker
+ // in the original (`_NextScreen = 8`). Treat the same way
+ // as Done for now â switching profiles mid-game isn't
+ // wired in our port and would discard mystery state.
+ if (kProfileBtn.contains(mx, my)) {
+ leaveSetup();
+ return;
+ }
+
+ (void)kScrap1Btn; (void)kScrap2Btn; (void)kScrap3Btn;
}
if (dirty)
draw();
Commit: ae83ff8a9f033760539254101d11b6718943bc25
https://github.com/scummvm/scummvm/commit/ae83ff8a9f033760539254101d11b6718943bc25
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:44+02:00
Commit Message:
EEM: help screen in setup screen implemented
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index d857dce9877..9822cf9f53d 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -787,25 +787,40 @@ void EEMEngine::doSetup() {
return false;
};
- // Fullscreen-pic modal. Mirrors `_InterfaceHelp @ 1560:0205`'s
- // per-frame loop: blit pic, wait for click/key, advance / exit.
- // Used by Credits (single PIC 0x208) and Help (we reuse for a
- // minimal stub since the help-pic table at `_InterfaceHelp`'s
- // offset isn't fully decoded yet).
- auto showFullscreenPic = [&](uint16 picId) {
+ // Render `picId` and block until click/key. Returns the pressed
+ // keycode (KEYCODE_ESCAPE for an explicit bail, KEYCODE_INVALID
+ // for a click or any other key). When `transparent` is true,
+ // preserve the current screen behind and overlay `picId` with
+ // its transparent colour key â mirrors `_InterfaceHelp @
+ // 1560:0205` calling `_Rect_Move_Mask` with the pic's
+ // `miscflags >> 8` as the transp byte. When false, do a raw
+ // fullscreen blit â mirrors the credits handler at 1f78:0281
+ // using `_vga_fbuffvid`.
+ auto showFullscreenPic = [&](uint16 picId,
+ bool transparent) -> Common::KeyCode {
Picture pic;
if (!_picsArchive.getPicture(picId, pic)) {
warning("doSetup: PIC %u missing", (uint)picId);
- return;
+ return Common::KEYCODE_INVALID;
}
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
- const int w = MIN<int>(pic.surface.w, 320);
- const int h = MIN<int>(pic.surface.h, 200);
- for (int row = 0; row < h; row++)
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)pic.surface.getBasePtr(0, row), w);
+ if (transparent) {
+ // Preserve the current screen so the help PIC's
+ // transparent pixels show the setup BG underneath.
+ Graphics::Surface *cur = g_system->lockScreen();
+ if (cur) {
+ for (int row = 0; row < 200; row++)
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)cur->getBasePtr(0, row), 320);
+ g_system->unlockScreen();
+ }
+ const byte transp = (byte)(pic.flags >> 8);
+ scratch.transBlitFrom(pic.surface, (uint32)transp);
+ } else {
+ scratch.clear();
+ scratch.simpleBlitFrom(pic.surface);
+ }
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
g_system->updateScreen();
@@ -813,13 +828,16 @@ void EEMEngine::doSetup() {
Common::Event ev;
while (g_system->getEventManager()->pollEvent(ev)) {
if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER ||
- ev.type == Common::EVENT_KEYDOWN ||
- ev.type == Common::EVENT_LBUTTONDOWN)
- return;
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return Common::KEYCODE_ESCAPE;
+ if (ev.type == Common::EVENT_KEYDOWN)
+ return ev.kbd.keycode;
+ if (ev.type == Common::EVENT_LBUTTONDOWN)
+ return Common::KEYCODE_INVALID;
}
g_system->delayMillis(15);
}
+ return Common::KEYCODE_ESCAPE;
};
auto leaveSetup = [&]() {
@@ -939,22 +957,48 @@ void EEMEngine::doSetup() {
continue;
}
- // Help (button [9]). Original calls `_InterfaceHelp(1)`,
- // which walks a sequence of help-pic IDs from a runtime
- // table we haven't fully decoded. PIC 0x4F is the in-game
- // help backdrop the briefing chain reuses; we show that
- // as a stub until the full table is wired.
+ // Help (button [9]). Original `_InterfaceHelp(1) @
+ // 1560:0205` walks the help-pic table at `29be:00c8`:
+ // each `num` slot is 5 bytes â count + two u16 PIC IDs.
+ // For num=1: count=2, pics = {0x0192, 0x01B1}. The
+ // original blits each pic with `_Rect_Move_Mask` â a
+ // MASKED blit whose transparent colour is the pic's
+ // `miscflags >> 8`, so the setup BG shows through. It
+ // also hides the cursor (`MOV [0x3a00], 0` + `_RemoveMouse`
+ // at the top of `_InterfaceHelp`, 1560:0216-021c). ESC
+ // at any point breaks out (1560:02b3 sets uVar5 = count).
if (kHelpBtn.contains(mx, my)) {
- showFullscreenPic(0x4F);
+ static const uint16 kHelp1Pics[] = { 0x0192, 0x01B1 };
+ CursorMan.showMouse(false);
+ for (uint i = 0; i < ARRAYSIZE(kHelp1Pics); i++) {
+ // Re-render the setup BG before each help PIC so
+ // each one overlays a clean canvas. Without this,
+ // `showFullscreenPic`'s `lockScreen` snapshot would
+ // pick up the previous PIC and the two help cards
+ // would composite together. Mirrors the original's
+ // `_vga_fvidvid(0)` call at the tail of every
+ // `_InterfaceHelp` iteration (1560:02e5), which
+ // restores the back-buffer BG between cards.
+ draw();
+ const Common::KeyCode k =
+ showFullscreenPic(kHelp1Pics[i], /*transparent=*/true);
+ if (k == Common::KEYCODE_ESCAPE)
+ break;
+ }
+ CursorMan.showMouse(true);
dirty = true;
continue;
}
// Credits (button [11]). Original handler at 1f78:025a
- // loads PIC 0x208 and blits it fullscreen, then waits
- // for any input.
+ // loads PIC 0x208, hides the cursor (`MOV [0x3a00], 0`
+ // at 1f78:0269 + `_RemoveMouse @ 1000:542f` at 1f78:026F),
+ // blits it fullscreen via `_vga_fbuffvid` (raw copy, no
+ // mask), then waits for any input.
if (kCreditsBtn.contains(mx, my)) {
- showFullscreenPic(0x208);
+ CursorMan.showMouse(false);
+ showFullscreenPic(0x208, /*transparent=*/false);
+ CursorMan.showMouse(true);
// PIC 0x208 has its own palette baked into the BG
// dump via `_GetPicture`; the original restores via
// `_GetPalette` on return. Reset to setup palette
Commit: 754e723072acc7bbc79c86a451dcecfa1d5d858d
https://github.com/scummvm/scummvm/commit/754e723072acc7bbc79c86a451dcecfa1d5d858d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:44+02:00
Commit Message:
EEM: setup screen accesible in zoom map screen
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 9822cf9f53d..87ab366765e 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -2291,11 +2291,18 @@ void EEMEngine::doBigMap() {
const int kSliderRangeY = mapH - kMapWinH;
if (kSetupBtn.contains(ev.mouse.x, ev.mouse.y)) {
- // Setup button on detail too â `_NextScreen = 6` in
- // the original. We treat it the same way: bail back
- // to case selection.
- _mystery.clear();
- _nextScreen = kScreenInvalid;
+ // `_DoMapScreen @ 20fe:1560` writes `_NextScreen
+ // = 6` (= kScreenSetup) and `INC [BP-8]` to bail
+ // out of the detail loop â verified via the byte
+ // search for `c7 06 16 79 06 00`, which finds the
+ // imm at exactly this site and `_DoBigMap @
+ // 20fe:0c33`. Same `SetupButtonRect @ 29be:15ce`
+ // rect used by both the overview and the detail
+ // (no per-screen rect duplication in the binary).
+ // The detail/zoom state is lost on return because
+ // the screen driver re-enters BigMap at stage 1 â
+ // this matches the original behaviour.
+ _nextScreen = kScreenSetup;
return;
}
if (kArrowYUp.contains(ev.mouse.x, ev.mouse.y)) {
Commit: 92c6607481ab1258822aa61cde27ff0116e09e8f
https://github.com/scummvm/scummvm/commit/92c6607481ab1258822aa61cde27ff0116e09e8f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:45+02:00
Commit Message:
EEM: improved navigation on PDA
Changed paths:
engines/eem/graphics.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/graphics.cpp b/engines/eem/graphics.cpp
index 3955914eb98..351036153f0 100644
--- a/engines/eem/graphics.cpp
+++ b/engines/eem/graphics.cpp
@@ -114,9 +114,13 @@ void EEMEngine::doHelp() {
void EEMEngine::doInterfaceHelp(uint num) {
// Mirrors `_InterfaceHelp(num)` @ 1560:0205. The original walks
// `HelpData @ 29be:00c8` (5-byte entries: u8 count, then up to 2
- // u16 picIds), `_GetPicture`s each one, blits it fullscreen via
- // `_Rect_Move_Mask(0, 0, ...)`, and waits for click / key. ESC ends
- // the cycle; any other input advances to the next pic.
+ // u16 picIds), `_GetPicture`s each one, blits it via
+ // `_Rect_Move_Mask(0, 0, ...)` (a MASKED blit on top of the
+ // existing screen â transparent pixels show the caller's BG), and
+ // waits for click / key. ESC at `1560:02b3` skips to end. The
+ // function also hides the cursor at the top (`MOV [0x3a00], 0` at
+ // 1560:0216 + `_RemoveMouse @ 1000:542f` at 1560:021c) and
+ // restores it at the tail (`_DrawMouse @ 1000:5429` at 1560:02e8).
//
// `kHelpPics` lives at file scope above; see comment there for the
// HelpData decoding.
@@ -126,6 +130,26 @@ void EEMEngine::doInterfaceHelp(uint num) {
debugC(1, kDebugScript, "doInterfaceHelp(%u): showing pics 0x%x, 0x%x",
num, kHelpPics[num][0], kHelpPics[num][1]);
+ // Snapshot the caller's screen ONCE so each help PIC overlays the
+ // same clean BG. Without this, after the first PIC is dismissed the
+ // second snapshot would include the first PIC's pixels and the two
+ // would composite together â same gotcha as the setup-screen help
+ // loop fix in `doSetup`.
+ Graphics::ManagedSurface bg(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ {
+ Graphics::Surface *cur = g_system->lockScreen();
+ if (cur) {
+ for (int row = 0; row < 200; row++)
+ memcpy((byte *)bg.getBasePtr(0, row),
+ (const byte *)cur->getBasePtr(0, row), 320);
+ g_system->unlockScreen();
+ }
+ }
+
+ const bool wasShown = CursorMan.isVisible();
+ CursorMan.showMouse(false);
+
for (uint i = 0; i < 2; i++) {
const uint16 picId = kHelpPics[num][i];
Picture pic;
@@ -136,24 +160,15 @@ void EEMEngine::doInterfaceHelp(uint num) {
debugC(1, kDebugScript, "doInterfaceHelp: pic 0x%x = %dx%d flags=0x%x",
picId, pic.surface.w, pic.surface.h, pic.flags);
- // Compose a 320x200 frame (cleared) and blit the help pic at (0,0)
- // with the original's masked-blit semantics: pixels equal to the
- // pic's sub-mode (high byte of `pic[0]`, see `_Rect_Move_Mask`
- // param_10 at 1000:03fc) are treated as transparent and skipped.
+ // Compose a 320x200 frame from the clean BG snapshot and overlay
+ // the help pic with `transBlitFrom` â `Graphics::ManagedSurface`'s
+ // masked blit (transparent colour = the pic's `flags >> 8`,
+ // matching `_Rect_Move_Mask`'s param_10 at 1000:03fc).
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
+ scratch.simpleBlitFrom(bg);
const byte transp = (byte)(pic.flags >> 8);
- const int w = MIN<int>(pic.surface.w, 320);
- const int h = MIN<int>(pic.surface.h, 200);
- for (int row = 0; row < h; row++) {
- const byte *src = (const byte *)pic.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, row);
- for (int col = 0; col < w; col++) {
- if (src[col] != transp)
- dst[col] = src[col];
- }
- }
+ scratch.transBlitFrom(pic.surface, (uint32)transp);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
g_system->updateScreen();
@@ -165,6 +180,8 @@ void EEMEngine::doInterfaceHelp(uint num) {
while (g_system->getEventManager()->pollEvent(ev)) {
if (ev.type == Common::EVENT_QUIT ||
ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ if (wasShown)
+ CursorMan.showMouse(true);
return;
}
if (ev.type == Common::EVENT_LBUTTONDOWN) {
@@ -187,6 +204,9 @@ void EEMEngine::doInterfaceHelp(uint num) {
if (escape)
break;
}
+
+ if (wasShown)
+ CursorMan.showMouse(true);
}
void EEMEngine::setPartnerEraseBg(const Graphics::ManagedSurface *bg) {
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 87ab366765e..9375f894a11 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -1425,7 +1425,13 @@ void EEMEngine::doNotebook() {
const Common::Rect kBtnPagePrev(226, 174, 247, 190); // [6] PAGE PREV
const Common::Rect kBtnMap ( 7, 177, 57, 200); // [7] MAP
const Common::Rect kBtnSite ( 35, 111, 56, 136); // [8] SITE
- const Common::Rect kNoteArea ( 66, 79, 267, 174); // [10] note area
+ const Common::Rect kBtnHelp2 (267, 174, 288, 190); // [10] extra HELP
+ // (`_NoteButtons @ 29be:0147` actually has rect [10] at
+ // (267,174,288,190) â small button on the right of the bottom
+ // bar that the original handler dispatch table at 161e:04ec
+ // routes to `_InterfaceHelp(0)` again. Earlier this rect was
+ // mis-noted as a "note area" of (66,79,267,174) â that
+ // rectangle exists nowhere in the binary's button table.)
CursorMan.showMouse(true);
@@ -1514,26 +1520,31 @@ void EEMEngine::doNotebook() {
dirty = true;
continue;
}
- if (kNoteArea.contains(ev.mouse.x, ev.mouse.y)) {
- // Toggle the selection on whichever clue's text
- // the click landed in. The original calls
- // `_InterfaceHelp` here; that's the help screen,
- // not selection â selection is in the Accuse
- // screen. We use the area for selection because
- // keyboard 1..9 toggling is awkward, and the
- // resulting `_NoteSelected` state is what
- // `_SolvedCheck` reads.
- for (uint i = 0; i < _notebookSlotRects.size(); i++) {
- if (_notebookSlotRects[i].contains(ev.mouse.x,
- ev.mouse.y)) {
- const uint clueId = _notebookSlotClues[i];
- _mystery._noteSelected[clueId] ^= 1;
- dirty = true;
- break;
- }
- }
+ if (kBtnHelp2.contains(ev.mouse.x, ev.mouse.y)) {
+ // `_NoteButtons[10]` â handler 0x03f9 = same
+ // `_InterfaceHelp(0)` as button [1].
+ doInterfaceHelp(0);
+ dirty = true;
continue;
}
+ // Click on a clue's slot rect â toggle selection. The
+ // original `_DoNotebook` doesn't do this â note
+ // selection lives in the accuse screen there â but
+ // keyboard 1..9 toggling is awkward, and the resulting
+ // `_NoteSelected` state is what `_SolvedCheck` reads
+ // either way. Slot rects are the per-clue rectangles
+ // `drawNotebookFrame` publishes, so this just
+ // reproduces the visible-text-bbox click without the
+ // previous bogus outer-area gate.
+ for (uint i = 0; i < _notebookSlotRects.size(); i++) {
+ if (_notebookSlotRects[i].contains(ev.mouse.x,
+ ev.mouse.y)) {
+ const uint clueId = _notebookSlotClues[i];
+ _mystery._noteSelected[clueId] ^= 1;
+ dirty = true;
+ break;
+ }
+ }
}
}
if (exitFlag)
@@ -1982,6 +1993,12 @@ void EEMEngine::doGallery() {
drawGalleryFrame(gd, num, slotRects, slotSuspect);
lastDraw = now;
}
+ // `g_system->updateScreen()` is what tells the framework to
+ // re-render the cursor at its current mouse position; without
+ // it here, the cursor only refreshes when `drawGalleryFrame`
+ // runs (every 100 ms) and visibly lags the mouse. Match
+ // `doNotebook`'s per-tick `updateScreen()` cadence (line 1548).
+ g_system->updateScreen();
g_system->delayMillis(15);
}
}
Commit: d87214f047517949fbaa941fa107d740d0cd19f6
https://github.com/scummvm/scummvm/commit/d87214f047517949fbaa941fa107d740d0cd19f6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:45+02:00
Commit Message:
EEM: basic accusation flow in the PDA
Changed paths:
engines/eem/graphics.cpp
engines/eem/mystery.cpp
engines/eem/mystery.h
engines/eem/ui.cpp
diff --git a/engines/eem/graphics.cpp b/engines/eem/graphics.cpp
index 351036153f0..0718f6c1471 100644
--- a/engines/eem/graphics.cpp
+++ b/engines/eem/graphics.cpp
@@ -163,12 +163,20 @@ void EEMEngine::doInterfaceHelp(uint num) {
// Compose a 320x200 frame from the clean BG snapshot and overlay
// the help pic with `transBlitFrom` â `Graphics::ManagedSurface`'s
// masked blit (transparent colour = the pic's `flags >> 8`,
- // matching `_Rect_Move_Mask`'s param_10 at 1000:03fc).
+ // matching `_Rect_Move_Mask`'s param_10 at 1000:03fc). Pass an
+ // explicit `destPos` of (0, 0) â the no-destPos overload at
+ // managed_surface.cpp:738 scales src to fill `this`'s rect,
+ // stretching the help PIC to 320x200 instead of placing it at
+ // native size. The original `_Rect_Move_Mask` passes destX=0,
+ // destY=0 with copy-width = pic[+4] (= `pic.surface.w`) and
+ // copy-height = pic[+2] (= `pic.surface.h`) â i.e. native size,
+ // not stretched.
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
scratch.simpleBlitFrom(bg);
const byte transp = (byte)(pic.flags >> 8);
- scratch.transBlitFrom(pic.surface, (uint32)transp);
+ scratch.transBlitFrom(pic.surface, Common::Point(0, 0),
+ (uint32)transp);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
g_system->updateScreen();
diff --git a/engines/eem/mystery.cpp b/engines/eem/mystery.cpp
index e3b4051a14a..39c854535d9 100644
--- a/engines/eem/mystery.cpp
+++ b/engines/eem/mystery.cpp
@@ -224,6 +224,31 @@ const byte *Mystery::mapEntry(uint siteNum) const {
return _data.data() + off;
}
+bool Mystery::isGuilty(uint suspectIdx) const {
+ // `_WITCH @ 1df2:089f`: `if (GalleryData[i*0x46 + 0x02] == -1)
+ // _DisplayCorrect(); else _DisplayAlibi(...)`. Innocent suspects
+ // store their alibi-text TextBlock offset at +0x02; the guilty
+ // one stores the sentinel 0xFFFF.
+ const byte *gd = galleryData();
+ if (!gd || suspectIdx >= _numSuspects)
+ return false;
+ const uint16 off = READ_LE_UINT16(gd + suspectIdx * 0x46 + 0x02);
+ return off == 0xFFFF;
+}
+
+uint16 Mystery::alibiTextOffset(uint suspectIdx) const {
+ const byte *gd = galleryData();
+ if (!gd || suspectIdx >= _numSuspects)
+ return 0xFFFF;
+ return READ_LE_UINT16(gd + suspectIdx * 0x46 + 0x02);
+}
+
+const byte *Mystery::solvedClueBlock() const {
+ if (!isLoaded() || _solvedOffset == 0 || _solvedOffset >= _data.size())
+ return nullptr;
+ return _data.data() + _solvedOffset;
+}
+
int Mystery::selectedPoints() const {
const byte *ni = noteIndex();
const uint16 cnt = noteIndexCount();
diff --git a/engines/eem/mystery.h b/engines/eem/mystery.h
index 96da8666f1b..dadaec00a75 100644
--- a/engines/eem/mystery.h
+++ b/engines/eem/mystery.h
@@ -152,6 +152,23 @@ public:
/// True when `selectedPoints() > 99`. Mirrors `_SolvedCheck`.
bool solvedCheck() const { return selectedPoints() > 99; }
+ /// True iff suspect @p suspectIdx is the case's guilty party. The
+ /// guilty marker is `GalleryData[suspectIdx * 0x46 + 0x02] ==
+ /// 0xFFFF` â innocent suspects store their alibi text offset there;
+ /// the guilty suspect uses the sentinel. Verified at `_WITCH @
+ /// 1df2:089f` (`if (psVar1->field_0x2 == -1) _DisplayCorrect();
+ /// else _DisplayAlibi(...)`).
+ bool isGuilty(uint suspectIdx) const;
+
+ /// TextBlock offset of suspect @p suspectIdx's alibi text. Returns
+ /// 0xFFFF for the guilty suspect (no alibi).
+ uint16 alibiTextOffset(uint suspectIdx) const;
+
+ /// Pointer to the win-clueblock (`MysteryIndex[+0x10]` =
+ /// `_solvedOffset`). Mirrors `_DisplayCorrect`'s
+ /// `_DisplayClue(_Mystery + MysteryIndex[+0x10], 0)` at 1df2:0769.
+ const byte *solvedClueBlock() const;
+
/// Per-mystery runtime state, zeroed at load time.
uint8 _cluesFound[kCluesFoundCap] = {};
uint8 _noteSelected[kCluesFoundCap] = {}; ///< Mirror `_NoteSelected`
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 9375f894a11..fa27ce1cf86 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -816,7 +816,12 @@ void EEMEngine::doSetup() {
g_system->unlockScreen();
}
const byte transp = (byte)(pic.flags >> 8);
- scratch.transBlitFrom(pic.surface, (uint32)transp);
+ // Explicit destPos â the no-destPos overload of
+ // `transBlitFrom` (managed_surface.cpp:738) stretches
+ // src to dst dimensions, scaling the PIC to 320x200.
+ // The original `_Rect_Move_Mask` blits at native size.
+ scratch.transBlitFrom(pic.surface, Common::Point(0, 0),
+ (uint32)transp);
} else {
scratch.clear();
scratch.simpleBlitFrom(pic.surface);
@@ -2834,25 +2839,43 @@ void EEMEngine::doAccuse() {
if (picked < 0)
return;
- // Real chain evaluation: sum point values of clues the player marked
- // "selected" in the notebook. Mirrors `_SolvedCheck` @ 1df2:00ec.
- const int points = _mystery.selectedPoints();
- const bool guessedRight = _mystery.solvedCheck();
- debugC(1, kDebugScript, "doAccuse: picked=%d selectedPts=%d -> %s",
- picked, points, guessedRight ? "correct" : "wrong");
-
- // If the player hasn't marked any evidence yet, give them a hint
- // rather than an instant fail. Mirrors the original "We're not ready
- // to solve this mystery yet..." string at 29be:10f0.
- if (points == 0 && _font.isLoaded()) {
+ // Real chain evaluation. Mirrors the original two-gate accusation:
+ // 1. `_AccuseEntry @ 1df2:0ff8` checks `_GetFoundPoints() >= 100`
+ // â gates whether the suspect picker is even reachable. We
+ // replicate this with `_SolvedCheck â selectedPoints > 99`,
+ // since our port marks clues in the notebook (port deviation;
+ // the original gates on the auto-sorted top-5 found clues
+ // via `_GetFoundPoints @ 1df2:0098`).
+ // 2. `_WITCH @ 1df2:089f` checks `GalleryData[picked*0x46+0x02] ==
+ // 0xFFFF`. Innocent suspects store an alibi-text TextBlock
+ // offset there; the guilty one uses the sentinel.
+ // Both must hold for `_DisplayCorrect`; otherwise it's an alibi.
+ const int points = _mystery.selectedPoints();
+ const bool enoughEvidence = _mystery.solvedCheck();
+ const bool pickedGuilty = _mystery.isGuilty((uint)picked);
+ const bool guessedRight = enoughEvidence && pickedGuilty;
+ debugC(1, kDebugScript,
+ "doAccuse: picked=%d selectedPts=%d evidence=%s guilty=%s -> %s",
+ picked, points,
+ enoughEvidence ? "yes" : "no",
+ pickedGuilty ? "yes" : "no",
+ guessedRight ? "correct" : "wrong");
+
+ // Insufficient evidence: same hint the original shows when
+ // `_AccuseEntry` returns 0 (string at `_KDTextIndex[0]`).
+ if (!enoughEvidence && _font.isLoaded()) {
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
_font.drawWordWrapped(&scratch, 16, 80, 288,
- "We're not ready to solve this mystery yet. "
- "Let's keep investigating until we have some "
- "more solid evidence to make our case! "
- "(Press N in the site screen to mark clues.)",
+ points == 0
+ ? "We're not ready to solve this mystery yet. "
+ "Let's keep investigating until we have some "
+ "more solid evidence to make our case! "
+ "(Press N in the site screen to mark clues.)"
+ : "We don't have quite enough solid evidence yet. "
+ "Let's review our notes and find a few more "
+ "clues before we accuse anyone.",
0xF);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
@@ -2861,131 +2884,72 @@ void EEMEngine::doAccuse() {
return;
}
- // Pick the ending based on the chain. For a correct accusation the
- // original would call `_DisplayClue(_Mystery + AChain[0])`, play
- // SCRAPBK.ANI and save progress. We load the matching `E<n>.BIN`
- // ending text and render its pages with prev/next navigation.
- const int endingNum = guessedRight ? picked : 0;
- const Common::String fname = Common::String::format("E%d.BIN", endingNum);
- Common::File f;
- if (!f.open(Common::Path(fname))) {
- warning("doAccuse: %s missing", fname.c_str());
- return;
- }
-
- // E<n>.BIN format (verified against `_DisplayEndingPage` @ 1df2:044c):
- // u16 numPages
- // per page (10 bytes header + NUL-string):
- // u16 picNum
- // u16 x1, y1, x2, y2 (story rect)
- // bytes[] NUL-terminated text
- const uint32 fileLen = f.size();
- Common::Array<byte> blob(fileLen);
- if (f.read(blob.data(), fileLen) != fileLen)
- return;
- const byte *e = blob.data();
- const uint16 pages = READ_LE_UINT16(e);
-
- uint pageIdx = 0;
-
- while (!shouldQuit()) {
- // Walk to pageIdx.
- uint pos = 2;
- uint cur = 0;
- while (cur < pageIdx && pos + 10 < fileLen) {
- const char *t = (const char *)(e + pos + 10);
- pos += 10 + strlen(t) + 1;
- cur++;
+ // Wrong suspect: render alibi flow. Mirrors `_DisplayAlibi @
+ // 1df2:0145` â shows the suspect's alibi text + their picture on
+ // PIC 0x3e, then returns to the accuse gallery for another try.
+ // Skips MIDI 6 (alibi music) and the per-partner voice line â the
+ // text rendering alone is enough to convey "this suspect is
+ // innocent". `_FirstTry` is cleared so a subsequent correct pick
+ // no longer counts as a first-try win (1df2:0445 -> _FirstTry=0).
+ if (!guessedRight) {
+ const uint16 alibiOff = _mystery.alibiTextOffset((uint)picked);
+ Common::String alibi;
+ if (gd && alibiOff != 0xFFFF) {
+ const char *raw = _mystery.textAt(alibiOff);
+ if (raw)
+ alibi = parseString(raw, _playerName, _partner);
}
- if (pos + 10 >= fileLen)
- break;
-
- const uint16 picNum = READ_LE_UINT16(e + pos + 0);
- const uint16 x1 = READ_LE_UINT16(e + pos + 2);
- const uint16 y1 = READ_LE_UINT16(e + pos + 4);
- const uint16 x2 = READ_LE_UINT16(e + pos + 6);
- const uint16 y2 = READ_LE_UINT16(e + pos + 8);
- const char *raw = (const char *)(e + pos + 10);
- const Common::String txt = parseString(raw, _playerName, _partner);
+ Picture alibiBg;
+ const bool haveAlibiBg =
+ _picsArchive.getPicture(0x3e, alibiBg);
+ Picture suspect;
+ const uint16 picId = gd
+ ? READ_LE_UINT16(gd + (uint)picked * 0x46)
+ : 0;
+ const bool haveSuspect = picId != 0 &&
+ _picsArchive.getPicture(picId, suspect);
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
-
- // Page background.
- if (picNum != 0) {
- Picture bg;
- if (_picsArchive.getPicture(picNum, bg)) {
- const int w = MIN<int>(bg.surface.w, 320);
- const int h = MIN<int>(bg.surface.h, 200);
- for (int row = 0; row < h; row++) {
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)bg.surface.getBasePtr(0, row), w);
- }
- }
+ if (haveAlibiBg) {
+ scratch.simpleBlitFrom(alibiBg.surface);
}
-
- if (_font.isLoaded()) {
- Common::String banner = "Not enough evidence";
- if (guessedRight)
- banner = _mystery._firstTry ? "CORRECT - FIRST TRY!" : "CORRECT!";
- _font.drawString(&scratch, banner, 8, 4, 320, 0xF);
- _font.drawString(&scratch, Common::String::format("Evidence: %d/100 Suspect: %d",
- points, picked + 1), 8, 16, 320, 0xF);
- const int wrapW = MAX<int>(16, x2 - x1);
- const int wrapY = MAX<int>(28, (int)y1);
- (void)y2;
- _font.drawWordWrapped(&scratch, x1, wrapY, wrapW, txt, 0xF);
- _font.drawString(&scratch, Common::String::format("page %u/%u (Left/Right or click)",
- pageIdx + 1, pages), 8, 188, 320, 0xF);
+ if (haveSuspect) {
+ // Original `_DisplayAlibi` blits the suspect at (0x82, py)
+ // where py varies by balloon size; we use a fixed centred
+ // position since we don't render the balloon shapes yet.
+ const byte transp = (byte)(suspect.flags >> 8);
+ scratch.transBlitFrom(suspect.surface,
+ Common::Point(0x82, 0x40),
+ (uint32)transp);
+ }
+ if (_font.isLoaded() && !alibi.empty()) {
+ _font.drawWordWrapped(&scratch, 16, 8, 200, alibi, 0xF);
+ _font.drawString(&scratch, "(click / ESC: back)",
+ 16, 188, 200, 0xF);
}
-
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
g_system->updateScreen();
+ waitForInput(20000);
- // Page navigation.
- bool advance = false;
- bool back = false;
- bool exit = false;
- while (!advance && !back && !exit && !shouldQuit()) {
- Common::Event ev;
- while (g_system->getEventManager()->pollEvent(ev)) {
- if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
- exit = true; break;
- }
- if (ev.type == Common::EVENT_LBUTTONDOWN) {
- advance = true; break;
- }
- if (ev.type == Common::EVENT_KEYDOWN) {
- if (ev.kbd.keycode == Common::KEYCODE_ESCAPE)
- exit = true;
- else if (ev.kbd.keycode == Common::KEYCODE_LEFT)
- back = true;
- else
- advance = true;
- break;
- }
- }
- g_system->updateScreen();
- g_system->delayMillis(15);
- }
- if (exit)
- break;
- if (advance) {
- if (pageIdx + 1 >= pages)
- break;
- pageIdx++;
- } else if (back) {
- if (pageIdx > 0)
- pageIdx--;
- }
+ _mystery._firstTry = false;
+ // `_DisplayAlibi @ 1df2:043f` writes `_NextScreen =
+ // _LastScreen` so the player drops back to wherever they
+ // accused from (typically the site loop). With no mystery
+ // state to unload, the site loop resumes naturally.
+ _nextScreen = _lastScreen != kScreenInvalid
+ ? (ScreenId)_lastScreen : kScreenSite;
+ return;
}
- // Mirror `_DisplayCorrect`'s scrap-book animation + solved tracking +
- // auto-save (the original calls `_SavePlayerRecord` after a win).
- if (guessedRight) {
+ // Right suspect â full win flow. Mirrors `_DisplayCorrect @
+ // 1df2:073c`: mark mystery solved, advance chain stage if the
+ // tier is complete, swap MIDI to the win cue, run SCRAPBK.ANI,
+ // show the per-mystery ending, save the profile, return to the
+ // action menu (`_NextScreen = 0xc` at 1df2:0895).
+ {
const uint mn = _mystery.number();
if (mn < sizeof(_mysteriesSolved)) {
_mysteriesSolved[mn] = _mystery._firstTry ? 2 : 1;
@@ -3059,14 +3023,6 @@ void EEMEngine::doAccuse() {
// 1df2:08a4 do the same.
_mystery.clear();
_nextScreen = kScreenAction;
- } else {
- _mystery._firstTry = false;
- // `_DisplayAlibi @ 1df2:043f` writes `_NextScreen =
- // _LastScreen` â drop back to wherever the player accused
- // from. With no mystery state to unload, the site loop
- // resumes naturally.
- _nextScreen = _lastScreen != kScreenInvalid
- ? (ScreenId)_lastScreen : kScreenSite;
}
}
@@ -3079,6 +3035,15 @@ void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
// PIC 0x3f backdrop, suspect portraits at the 5 fixed slots
// (`kGallerySlots` in this file's anon namespace), and a 1-px
// outline (palette index 0xFE) around the highlighted slot.
+ //
+ // Partner sprite at (5, 0x50): the original `_DoAccuse @ 1df2:0bdd`
+ // registers `_NewAnimation(5, 0x50, partnerCells, script=2, prior=1)`
+ // (1df2:0c30) BEFORE calling `_DoAccuseGallery`, then `_DrawGallery`
+ // calls `_DrawActiveAnimations` (158f:00a3) which re-renders the
+ // slot every frame. Without an explicit blit here, our port's
+ // accuse screen comes out partner-less. Anim CELLS are 2 (Jake) /
+ // 0x10 (Jenny); SCRIPT key is 0x02 for both partners (matches the
+ // `CONCAT22(2, ...)` arg verified at 1df2:0c2e).
Picture accuseBg;
const bool haveAccuseBg = _picsArchive.getPicture(0x3f, accuseBg);
@@ -3094,6 +3059,21 @@ void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
}
}
+ // Partner sprite, drawn BEFORE portraits so the suspect grid
+ // covers it where they overlap (the gallery slots start at
+ // y=14 / y=90, partner is at y=0x50=80 â no overlap, so order
+ // is purely defensive).
+ const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
+ Animation partnerAni;
+ if (_aniArchive.loadAnimation(partnerAnim, partnerAni) &&
+ !partnerAni.empty()) {
+ const uint32 now = g_system->getMillis();
+ const uint frameIdx = partnerFrameAtTick(0x02,
+ (uint)partnerAni.size(), now);
+ blitAnimFrameAnchored(scratch.surfacePtr(),
+ partnerAni[frameIdx], 5, 0x50);
+ }
+
for (uint i = 0; i < numSuspects && i < Mystery::kGalleryCap; i++) {
slotRects[i] = Common::Rect();
slotSuspect[i] = -1;
Commit: 3b1738c38bf9c7dfa7603961bec4a65af09a597d
https://github.com/scummvm/scummvm/commit/3b1738c38bf9c7dfa7603961bec4a65af09a597d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:45+02:00
Commit Message:
EEM: basic scrapbook code
Changed paths:
engines/eem/eem.h
engines/eem/ui.cpp
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index ab7389a4b9e..345a6f8a36a 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -346,6 +346,17 @@ private:
/// so this same call covers the post-mystery scrapbook view from
/// the action menu.
void doShowEnding(uint num);
+
+ /// Walk every solved mystery in tier @p stage (1=Junior, 2=Senior,
+ /// 3=Master) and display each one's ending pages in sequence.
+ /// Mirrors `_ShowScrapbook(stage, 0) @ 1f78:0642`: the original
+ /// computes the mystery range from `(stage - 1) * 0x18 + 1` to
+ /// `(stage - 1) * 0x18 + 0x18` and skips entries whose
+ /// `_3f9b[i] == 0` (unsolved) so the scrapbook only contains
+ /// completed cases. Used by both the setup-screen ScrapBook
+ /// buttons and the action-menu "See ScrapBook 1/2/3" entries.
+ void doShowScrapbook(uint stage);
+
void doCaseSelection();
void doSiteLoop();
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index fa27ce1cf86..4d092f292cb 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -635,6 +635,39 @@ void EEMEngine::doShowEnding(uint num) {
}
}
+void EEMEngine::doShowScrapbook(uint stage) {
+ // Mirrors `_ShowScrapbook(stage, 0) @ 1f78:0642`. Walk the
+ // stage's mystery range and call `_DisplayEnding` on each solved
+ // mystery. The original splits the 55 cases into three tiers:
+ // stage 1 (Junior) â mysteries 1..0x18 (24 cases)
+ // stage 2 (Senior) â mysteries 0x19..0x30 (24 cases)
+ // stage 3 (Master) â mysteries 0x31..0x36 (6 cases)
+ // Each tier's range is `lo = (stage-1)*0x18 + 1`, `hi = lo + 0x17`
+ // (verified at 1f78:064b: `iVar1 = (param_1 - 1) * 0x18; uVar2 =
+ // iVar1 + 1`). The current-stage filter at 1f78:065e
+ // (`if (DAT_2d5d_3f99 == param_1)`) skips unsolved mysteries
+ // inside the player's CURRENT tier â completed tiers show every
+ // mystery regardless. We mirror that exactly.
+ if (stage < 1 || stage > 3)
+ return;
+ const uint lo = (stage - 1) * 0x18 + 1;
+ const uint hi = lo + 0x17;
+ const bool currentTier = (stage == _chainStage);
+
+ for (uint m = lo; m <= hi; m++) {
+ if (m >= sizeof(_mysteriesSolved))
+ break;
+ // Current-tier filter (1f78:0664). Completed tiers show all
+ // 24 entries; the active tier hides unsolved ones because
+ // the player hasn't earned that scrapbook page yet.
+ if (currentTier && _mysteriesSolved[m] == 0)
+ continue;
+ doShowEnding(m);
+ if (shouldQuit())
+ return;
+ }
+}
+
void EEMEngine::doSetup() {
// Mirrors `_DoSetup @ 1f78:044e`. The setup screen is BG `PIC 0x40`
// (loaded once on entry) with every label baked in â "Setup",
@@ -1022,7 +1055,35 @@ void EEMEngine::doSetup() {
return;
}
- (void)kScrap1Btn; (void)kScrap2Btn; (void)kScrap3Btn;
+ // ScrapBook 1 / 2 / 3 (buttons [3] / [4] / [5]). Original
+ // handlers at 1f78:021F (`_ShowScrapbook(0, 1)`) /
+ // 1f78:022E (gated chain >= 2 / `_ShowScrapbook(0, 2)`) /
+ // 1f78:0244 (gated chain >= 3 / `_ShowScrapbook(0, 3)`).
+ // Convert the original's `(0, stage)` invocation into our
+ // `doShowScrapbook(stage)` (we collapse the param_1=0
+ // "no-current-mystery" indirection â relevant only for
+ // the post-win callsite).
+ auto runScrapbook = [&](uint stage) {
+ CursorMan.showMouse(false);
+ doShowScrapbook(stage);
+ CursorMan.showMouse(true);
+ setSitePalette(0);
+ };
+ if (kScrap1Btn.contains(mx, my)) {
+ runScrapbook(1);
+ dirty = true;
+ continue;
+ }
+ if (kScrap2Btn.contains(mx, my) && _chainStage >= 2) {
+ runScrapbook(2);
+ dirty = true;
+ continue;
+ }
+ if (kScrap3Btn.contains(mx, my) && _chainStage >= 3) {
+ runScrapbook(3);
+ dirty = true;
+ continue;
+ }
}
if (dirty)
draw();
@@ -1061,9 +1122,24 @@ void EEMEngine::doCaseSelection() {
" See ScrapBook 2",
" See ScrapBook 3"
};
- // ScrapBooks aren't implemented yet â grey them so the player can't
- // stop on them, mirroring the original `_Greys` mask.
- const bool kPickEnabled[kNumPicks] = { true, true, false, false, false };
+ // ScrapBook entries are gated by chain stage exactly as the
+ // original `_ActionScreen @ 1c33:195b` does at 1c33:19f3-19f7:
+ // stage 1 + nothing solved â ScrapBook 1/2/3 all greyed
+ // stage 1 + â¥1 solved â ScrapBook 1 enabled, 2/3 greyed
+ // stage 2 â ScrapBook 1 enabled, 2 enabled, 3 greyed
+ // stage 3 â all three enabled
+ // (`_3f9b[i] != 0` over the tier's mystery range is the per-tier
+ // gate; we approximate "any in tier" by checking _chainStage and
+ // any solved flag.)
+ bool anySolved1 = false;
+ for (uint i = 1; i <= 0x18 && i < sizeof(_mysteriesSolved); i++)
+ if (_mysteriesSolved[i]) { anySolved1 = true; break; }
+ const bool scrap1On = anySolved1 || _chainStage >= 2;
+ const bool scrap2On = _chainStage >= 2;
+ const bool scrap3On = _chainStage >= 3;
+ const bool kPickEnabled[kNumPicks] = {
+ true, true, scrap1On, scrap2On, scrap3On
+ };
uint pick = kPickChoose;
const char *kSeparator = "----------------------------------";
@@ -1234,8 +1310,19 @@ void EEMEngine::doCaseSelection() {
return;
}
- if (pick != kPickChoose) {
- // ScrapBooks aren't implemented; bail back to the menu loop.
+ if (pick == kPickScrap1 || pick == kPickScrap2 || pick == kPickScrap3) {
+ // `_ActionScreen` handlers at 1c33:1B13 / 1B26 / 1B40 each
+ // call `_ShowScrapbook(0, stage)` for the matching tier
+ // (verified at the action-handler jumptable bytes
+ // `01 03 05 07 09 ff` paired with handlers at 1c33:1be1).
+ // The picker here is meant to LEAVE the mystery state untouched
+ // â viewing the scrapbook never starts a new case.
+ const uint stage = (pick == kPickScrap1) ? 1
+ : (pick == kPickScrap2) ? 2 : 3;
+ CursorMan.showMouse(false);
+ doShowScrapbook(stage);
+ CursorMan.showMouse(true);
+ setSitePalette(0);
_mystery.clear();
return;
}
@@ -1976,6 +2063,12 @@ void EEMEngine::doGallery() {
e2.type == Common::EVENT_RETURN_TO_LAUNCHER)
return;
}
+ // Per-tick `updateScreen()` so the SDL cursor
+ // follows the mouse â without it the cursor
+ // freezes on entry to the MoreInfo screen
+ // (we never repaint here, so the cursor never
+ // gets drawn at its current position).
+ g_system->updateScreen();
g_system->delayMillis(20);
}
// Force gallery redraw immediately so the
Commit: b4dc4f62742265b7b62335720033115b3e37eeba
https://github.com/scummvm/scummvm/commit/b4dc4f62742265b7b62335720033115b3e37eeba
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:46+02:00
Commit Message:
EEM: added more missing animations
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 4d092f292cb..45cea187910 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -1962,6 +1962,32 @@ void EEMEngine::doGallery() {
(const byte *)galBg.surface.getBasePtr(0, row), bw);
}
}
+ // Partner sprite at (5, 0x50). The original
+ // `MoreInfo @ 158f:0419` calls
+ // `_RefreshGalleryBackground` (clears the
+ // portrait grid) but the partner anim slot
+ // registered by `_DoGallery` keeps painting
+ // at every `_UpdateAnimations` tick â the
+ // suspect detail pic covers the right side
+ // only (drawn at 0x94, 0xf), so the partner
+ // stays visible on the left. Without this
+ // blit the suspect-detail screen has no
+ // partner.
+ {
+ const uint partnerAnim =
+ (_partner == 0) ? 2 : 0x10;
+ Animation partnerAni;
+ if (_aniArchive.loadAnimation(partnerAnim,
+ partnerAni) &&
+ !partnerAni.empty()) {
+ const uint32 now = g_system->getMillis();
+ const uint frameIdx =
+ partnerFrameAtTick(0x02,
+ (uint)partnerAni.size(), now);
+ blitAnimFrameAnchored(ms.surfacePtr(),
+ partnerAni[frameIdx], 5, 0x50);
+ }
+ }
// Full suspect picture at (0x94, 0xf).
Picture detail;
if (_picsArchive.getPicture(detailPic, detail)) {
@@ -2794,15 +2820,39 @@ void EEMEngine::doAccuse() {
if (haveBalloon && balloon.surface.h < 0x4e)
balloonY = (0x50 - balloon.surface.h) / 2;
+ // Render the gallery FIRST so the balloon snapshot
+ // includes the partner sprite. The original
+ // `_DoAccuseGallery @ 1df2:0a31` does this implicitly:
+ // `_NewAnimation` registered the partner slot at
+ // (5, 0x50) before reaching this point, then
+ // `_GetBackground(0x3f)` + `_DrawGallery` paint the
+ // portraits, and `_UpdateAnimations` keeps the partner
+ // visible underneath the balloon overlay. Without this,
+ // the player sees an 8-second partner-less screen
+ // while reading the hint.
+ drawAccuseGallery(num, gd, /*highlighted=*/-1,
+ slotRects, slotSuspect);
+
Graphics::ManagedSurface ms(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
ms.clear();
- if (haveAccuseBg) {
- const int bw = MIN<int>(accuseBg.surface.w, 320);
- const int bh = MIN<int>(accuseBg.surface.h, 200);
- for (int row = 0; row < bh; row++) {
- memcpy((byte *)ms.getBasePtr(0, row),
- (const byte *)accuseBg.surface.getBasePtr(0, row), bw);
+ {
+ Graphics::Surface *cur = g_system->lockScreen();
+ if (cur) {
+ for (int row = 0; row < 200; row++)
+ memcpy((byte *)ms.getBasePtr(0, row),
+ (const byte *)cur->getBasePtr(0, row), 320);
+ g_system->unlockScreen();
+ } else if (haveAccuseBg) {
+ // Fallback: lockScreen failed somehow; at least
+ // fill from PIC 0x3f so we don't render against
+ // stale memory.
+ const int bw = MIN<int>(accuseBg.surface.w, 320);
+ const int bh = MIN<int>(accuseBg.surface.h, 200);
+ for (int row = 0; row < bh; row++) {
+ memcpy((byte *)ms.getBasePtr(0, row),
+ (const byte *)accuseBg.surface.getBasePtr(0, row), bw);
+ }
}
}
// Masked balloon blit â `_Rect_Move_Mask` (1000:03fc)
Commit: 256eff166c411dc493e6636e730ac726eef5f38a
https://github.com/scummvm/scummvm/commit/256eff166c411dc493e6636e730ac726eef5f38a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:46+02:00
Commit Message:
EEM: stop music/sound in certain points of the game
Changed paths:
engines/eem/clues.cpp
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/site.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index d2635e3823d..69a805fffd8 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -336,11 +336,21 @@ void EEMEngine::doInitClues() {
g_system->unlockScreen();
g_system->updateScreen();
- // Wait 100 ms or until input.
+ // Wait 100 ms or until input. ESC also stops any pending
+ // voice / spool so audio doesn't bleed past the briefing
+ // when the player skips early â without this the per-clue
+ // voice line that `displayClue` would spool keeps playing
+ // after we've moved on to the MAP.
const uint32 wakeup = g_system->getMillis() + 100;
while (g_system->getMillis() < wakeup && !shouldQuit() && !skip) {
Common::Event ev;
while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_KEYDOWN &&
+ ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ interruptAudio();
+ skip = true;
+ break;
+ }
if (ev.type == Common::EVENT_LBUTTONDOWN ||
ev.type == Common::EVENT_KEYDOWN) {
skip = true;
@@ -437,6 +447,12 @@ void EEMEngine::doInitClues() {
!shouldQuit() && !skip) {
Common::Event ev;
while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_KEYDOWN &&
+ ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ interruptAudio();
+ skip = true;
+ break;
+ }
if (ev.type == Common::EVENT_LBUTTONDOWN ||
ev.type == Common::EVENT_KEYDOWN) {
skip = true;
@@ -780,6 +796,13 @@ void EEMEngine::displayClue(const byte *clueBlock) {
ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
advance = true;
skipAll = true;
+ // Cut the per-clue voice line that was just
+ // spooled â without this the voice keeps
+ // playing past the dialog dismissal and
+ // bleeds into the next screen (e.g. the
+ // case-briefing voice still talking on the
+ // MAP after ESC).
+ interruptAudio();
break;
}
if (ev.type == Common::EVENT_LBUTTONDOWN ||
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 46fa0497f45..9cf6ab97843 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -456,6 +456,22 @@ bool EEMEngine::setAnmPalette(const Common::Path &anmPath) {
return true;
}
+void EEMEngine::interruptAudio() {
+ // Mirrors `_CleanMysterySounds @ 202f:05a5` + `_StopMIDI @
+ // 20a2:0512` â the original calls both whenever the player aborts
+ // the opening-anim chain or dismisses the title (`_DoOpeningAnims
+ // @ 2520:082a` writes `_LoopMIDI = 0; _StopMIDI();` after the
+ // title-input loop). We expose the same combined stop on every
+ // ESC handler so currently-playing music + voice + spool actually
+ // halt instead of bleeding through into the next screen.
+ if (_audio) {
+ _audio->stopVoice();
+ _audio->stopSpool();
+ }
+ if (_music)
+ _music->stop();
+}
+
void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs, bool holdLastFrame) {
ANMDecoder anm;
if (!anm.open(path)) {
@@ -483,7 +499,8 @@ void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs, bool holdLa
// the frame-rate calibration logic from _GetSpeedRating is wired up.
// ESC additionally sets `_skipIntro` so the opening-anim chain in
// run() bails out of the whole sequence instead of advancing to
- // the next clip.
+ // the next clip â and stops every active audio channel so the
+ // theme music / voice spool don't bleed past the abort.
const uint32 frameStart = g_system->getMillis();
bool aborted = false;
while (g_system->getMillis() - frameStart < frameDelayMs && !aborted) {
@@ -496,8 +513,10 @@ void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs, bool holdLa
break;
}
if (event.type == Common::EVENT_KEYDOWN) {
- if (event.kbd.keycode == Common::KEYCODE_ESCAPE)
+ if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
_skipIntro = true;
+ interruptAudio();
+ }
aborted = true;
break;
}
@@ -523,8 +542,10 @@ void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs, bool holdLa
break;
}
if (ev.type == Common::EVENT_KEYDOWN) {
- if (ev.kbd.keycode == Common::KEYCODE_ESCAPE)
+ if (ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
_skipIntro = true;
+ interruptAudio();
+ }
clicked = true;
break;
}
@@ -534,6 +555,12 @@ void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs, bool holdLa
g_system->updateScreen();
g_system->delayMillis(20);
}
+ // `_DoOpeningAnims @ 2520:0945` writes `_LoopMIDI = 0;
+ // _StopMIDI();` once the title-input loop exits â so the
+ // theme stops the moment the player dismisses the title,
+ // regardless of whether they used ESC or clicked.
+ if (_music)
+ _music->stop();
}
}
@@ -549,7 +576,10 @@ void EEMEngine::blitAt(const Picture &pic, int x, int y) {
void EEMEngine::waitForInput(uint32 maxMs) {
// ESC additionally raises `_skipIntro` so the opening-anim chain
- // can fast-forward past the rest of the sequence.
+ // can fast-forward past the rest of the sequence, and stops any
+ // active audio so the theme / voice / spool don't bleed past
+ // the abort. Mirrors the `_CleanMysterySounds` + `_StopMIDI`
+ // pair around the title wait in `_DoOpeningAnims`.
const uint32 startMs = g_system->getMillis();
while (!shouldQuit() && (g_system->getMillis() - startMs < maxMs)) {
Common::Event event;
@@ -560,8 +590,10 @@ void EEMEngine::waitForInput(uint32 maxMs) {
return;
}
if (event.type == Common::EVENT_KEYDOWN) {
- if (event.kbd.keycode == Common::KEYCODE_ESCAPE)
+ if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
_skipIntro = true;
+ interruptAudio();
+ }
return;
}
}
@@ -616,12 +648,25 @@ void EEMEngine::startTravelMusic() {
// }
//
// Five travel tracks: MUS00000.XMI .. MUS00004.XMI, picked by
- // `_SiteNumber % 5`. The original always loops travel music (the
- // `_LoopMIDI` global isn't reset between site changes).
+ // `_SiteNumber % 5`. ONE-SHOT â `_DoOpeningAnims @ 2520:0945`
+ // resets `_LoopMIDI = 0` after the title-screen wait, and
+ // `_StartTravelMusic` doesn't write to it; combined with
+ // `_DoSiteLoop @ 168d:06c0` which waits for the track to play
+ // out and then calls `_StopMIDI()` before the interactive phase
+ // begins, the original effectively plays travel music ONCE
+ // during the entrance animation only â the site investigation
+ // itself runs without music. Our previous `loop=true` made the
+ // music never end, leaving travel music droning through site
+ // investigation, accuse, gallery, etc.
if (!_music || !_mystery.isLoaded())
return;
const uint num = _mystery._siteNumber % 5;
- _music->playMus(num, /*loop=*/true);
+ _music->playMus(num, /*loop=*/false);
+}
+
+void EEMEngine::stopMusic() {
+ if (_music)
+ _music->stop();
}
void EEMEngine::syncSoundSettings() {
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 345a6f8a36a..2d95c0102a1 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -320,6 +320,18 @@ private:
void playAnm(const Common::Path &path, uint frameDelayMs = 120,
bool holdLastFrame = false);
+ /// Stop every active audio channel â voice, sound spool, and
+ /// MIDI. Mirrors the `_CleanMysterySounds @ 202f:05a5` +
+ /// `_StopMIDI @ 20a2:0512` pair the original triggers when the
+ /// player aborts the opening-anim chain or dismisses the title
+ /// (`_DoOpeningAnims @ 2520:082a` writes `_LoopMIDI = 0;
+ /// _StopMIDI();` after the title-input loop, and
+ /// `_CleanMysterySounds` is called twice â once after the loop
+ /// and once before TITLE.ANM). Called from every ESC handler in
+ /// the intro / title chain so the theme music + voice spool
+ /// don't bleed past the abort.
+ void interruptAudio();
+
// Screen handlers â port targets in screens/ later.
void showEAKidsLogo();
void showHighScoreLogo();
@@ -380,6 +392,12 @@ public:
/// music changes as the player travels between sites.
void startTravelMusic();
+ /// Stop any currently playing MIDI track. Mirrors `_StopMIDI @
+ /// 20a2:0512` â used by `_DoSiteLoop @ 168d:06c0` after the
+ /// one-shot travel track plays out and by `_DisplayCorrect /
+ /// _DisplayAlibi` between MIDI cues.
+ void stopMusic();
+
/// Forwarded from `Engine::syncSoundSettings`. Re-pulls the user's
/// `music_volume` slider into the MIDI player's `_masterVolume`,
/// otherwise the AdLib output stays at whatever the slider was at
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 062f81f04ba..34a354085f1 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -554,6 +554,14 @@ void SiteScreen::enter(uint siteNum) {
renderBackground(siteNum);
}
+ // Stop the travel music explicitly. `_DoSiteLoop @ 168d:06c0`
+ // waits for the one-shot travel track to play out and then calls
+ // `_StopMIDI()` before the interactive site phase begins â
+ // blocking the engine while it spins. We just stop now so the
+ // site investigation runs without music (matches the original
+ // silent-investigation phase).
+ _vm->stopMusic();
+
// Static drops (Loop 2 from `_DoSiteLoop`) â no animation, baked
// into the BG snapshot the run() pump uses to restore.
renderStaticDrops(siteNum);
Commit: fb9922f887b5af752198719e6043a983dbb5f653
https://github.com/scummvm/scummvm/commit/fb9922f887b5af752198719e6043a983dbb5f653
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:46+02:00
Commit Message:
EEM: added missing text bubbles here and there
Changed paths:
engines/eem/graphics.cpp
engines/eem/mystery.cpp
engines/eem/site.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/graphics.cpp b/engines/eem/graphics.cpp
index 0718f6c1471..ed8955c555b 100644
--- a/engines/eem/graphics.cpp
+++ b/engines/eem/graphics.cpp
@@ -30,6 +30,7 @@
#include "graphics/cursorman.h"
#include "graphics/managed_surface.h"
+#include "eem/audio.h"
#include "eem/detection.h"
#include "eem/eem.h"
@@ -76,38 +77,167 @@ const BalloonInsets kBalloonInsetTable[] = {
};
void EEMEngine::doHelp() {
- // `_KDHelp` reads two hint TextBlock offsets from `_KDTextIndex`:
- // word @ +0xe : first-time hint
- // word @ +0x10: second-time hint (cycles back to first if missing)
- // `_SawHelpHint` toggles between them.
+ // Mirrors `_KDHelp @ 1560:010a`. The original walks the first two
+ // entries of `_AChain` (the puzzle's required-clue chain â the
+ // "spine" of evidence the player must collect):
+ //
+ // for (i = 0; i < 2; i++) {
+ // if (_AChain[i] != -1 && _HintBlock[i] != -1 &&
+ // _CluesFound[_AChain[i]] == 0) {
+ // _DisplayHint(TextBlock + _HintBlock[i], i + 10);
+ // shown++; break;
+ // }
+ // if (_HintBlock[i] != -1) defined++;
+ // }
+ // if (!shown) {
+ // // Fall back to the generic KD hint: KDTextIndex[+0xe]
+ // // (first time) / KDTextIndex[+0x10] (second time, toggled
+ // // by _SawHelpHint). If neither chain entry had a hint
+ // // defined, show the global "no hints" sentinel instead.
+ // _DisplayHint(...);
+ // }
+ //
+ // So this is a SMART per-puzzle hint: the partner points the
+ // player at whichever chain clue they haven't yet found, only
+ // falling back to the generic "let's keep looking" line when
+ // every chain hint has been triggered already.
if (!_mystery.isLoaded() || !_font.isLoaded())
return;
- const byte *kd = _mystery.kdTextIndex();
+ const byte *kd = _mystery.kdTextIndex();
+ const byte *hb = _mystery.hintBlock();
if (!kd)
return;
- const uint16 hintFirst = READ_LE_UINT16(kd + 0x0e);
- const uint16 hintSecond = READ_LE_UINT16(kd + 0x10);
- uint16 use = _mystery._sawHelpHint && hintSecond != 0xFFFF ? hintSecond : hintFirst;
- if (use == 0xFFFF) {
- debugC(1, kDebugScript, "doHelp: no hint configured");
+ uint16 chosenText = 0xFFFF;
+ int soundNum = 0;
+ bool anyHintDefined = false;
+
+ if (hb) {
+ for (uint i = 0; i < 2; i++) {
+ const uint16 chainClue = _mystery.aChain(i);
+ if (chainClue == 0xFFFF)
+ continue;
+ const uint16 hintOff = READ_LE_UINT16(hb + i * 2);
+ if (hintOff == 0xFFFF)
+ continue;
+ anyHintDefined = true;
+ if (chainClue < Mystery::kCluesFoundCap &&
+ _mystery._cluesFound[chainClue] == 0) {
+ chosenText = hintOff;
+ soundNum = (int)i + 10;
+ break;
+ }
+ }
+ }
+
+ if (chosenText == 0xFFFF) {
+ // No unfound chain clue had a hint to give â fall back to the
+ // generic KD hint (or the "no hints defined" sentinel if the
+ // chain has no hints at all). Mirrors the second arm of
+ // `_KDHelp` (1560:0152-019b).
+ if (anyHintDefined) {
+ const uint16 hintFirst = READ_LE_UINT16(kd + 0x0e);
+ const uint16 hintSecond = READ_LE_UINT16(kd + 0x10);
+ if (!_mystery._sawHelpHint && hintFirst != 0xFFFF) {
+ chosenText = hintFirst;
+ soundNum = 7;
+ _mystery._sawHelpHint = true;
+ } else if (hintSecond != 0xFFFF) {
+ chosenText = hintSecond;
+ soundNum = 8;
+ }
+ }
+ // Else: keep chosenText == 0xFFFF â original would render
+ // `NoHints` (a "There are no hints defined for this Mystery"
+ // string at 29be:00d3); we just bail.
+ }
+
+ if (chosenText == 0xFFFF) {
+ debugC(1, kDebugScript, "doHelp: no hint available");
return;
}
- if (!_mystery._sawHelpHint && hintFirst != 0xFFFF)
- _mystery._sawHelpHint = true;
- const Common::String raw = _mystery.textAt(use);
- const Common::String text = parseString(raw, _playerName, _partner);
+ const Common::String raw = _mystery.textAt(chosenText);
+ Common::String text = parseString(raw, _playerName, _partner);
- Graphics::ManagedSurface scratch(320, 200,
+ // Render as a speech-balloon overlay, exactly mirroring
+ // `_DisplayHint @ 1560:0009`:
+ //
+ // _GetKDTextBalloon(text, &bub); // first-char dispatch
+ // _GetBalloon(bub); // load balloon pic
+ // y = (h < 0x4e) ? (0x50 - h) >> 1 : 1; // vertical centre
+ // _AddPicBackground(balloon, 0x21, y); // overlay on screen
+ // _WordWrap(0x21+tbl[bub].x, y+tbl[bub].y, // text inside balloon
+ // tbl[bub].w, text, -1, color=0);
+ // _SayKDDigital(snd); // partner voice
+ // _Wait();
+ //
+ // The balloon BG is the caller's CURRENT screen â site / PDA /
+ // gallery â not a cleared scratch.
+ Graphics::ManagedSurface ms(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
- _font.drawString(&scratch, "HELP", 8, 4, 320, 0xF);
- _font.drawWordWrapped(&scratch, 8, 24, 304, text, 0xF);
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ ms.clear();
+ {
+ Graphics::Surface *cur = g_system->lockScreen();
+ if (cur) {
+ for (int row = 0; row < 200; row++)
+ memcpy((byte *)ms.getBasePtr(0, row),
+ (const byte *)cur->getBasePtr(0, row), 320);
+ g_system->unlockScreen();
+ }
+ }
+
+ // Balloon shape dispatch via `_GetKDTextBalloon @ 1df2:0105` â
+ // based on the first char of the parsed text. Digits select a
+ // specific balloon variant; non-digit defaults to `0x17`. The
+ // digit, when present, is THEN consumed from the displayed
+ // text â mirrors `_DisplayAlibi @ 1df2:0145`'s `str = pbVar7 + 1`
+ // advance after using `*str` for `bindx`. Without this the hint
+ // renders like "1Try checking the kitchen..." with a stray
+ // leading digit. `_GetKDTextBalloon` itself doesn't strip it
+ // (verified at 1df2:0105 â it just reads `*str`), so the caller
+ // has to.
+ const byte firstChar =
+ text.empty() ? (byte)0 : (byte)text[0];
+ const uint16 bubNum = getKDTextBalloon(firstChar);
+ if (firstChar >= '0' && firstChar <= '9')
+ text.deleteChar(0);
+ Picture balloon;
+ const bool haveBalloon =
+ _balloonArchive.size() > (bubNum & 0x7F) &&
+ _balloonArchive.loadEntry(bubNum & 0x7F, balloon);
+
+ const int balloonX = 0x21;
+ int balloonY = 1;
+ if (haveBalloon && balloon.surface.h < 0x4e)
+ balloonY = (0x50 - balloon.surface.h) / 2;
+
+ if (haveBalloon) {
+ const byte transp = (byte)(balloon.flags >> 8);
+ ms.transBlitFrom(balloon.surface,
+ Common::Point(balloonX, balloonY),
+ (uint32)transp);
+ }
+
+ // Balloon-relative text insets from the table at `29be:0875`
+ // (10 bytes per entry: x, y, max-width, ...).
+ uint16 tx = 5, ty = 4, tw = 155;
+ getBalloonInsets(bubNum, tx, ty, tw);
+ _font.drawWordWrapped(&ms, balloonX + tx, balloonY + ty, tw, text,
+ haveBalloon ? 0 : 0xF);
+
+ g_system->copyRectToScreen(ms.getPixels(), ms.pitch,
0, 0, 320, 200);
g_system->updateScreen();
+
+ // `_DisplayHint @ 1560:0009` plays `_SayKDDigital(soundnum)` â
+ // partner-specific voice line keyed to which hint type fired (10
+ // = first chain hint, 11 = second, 7 / 8 = generic KD).
+ if (_audio && _mystery.kdTextIndex() && soundNum > 0)
+ _audio->sayKDDigital(_mystery.kdTextIndex(), (uint)soundNum,
+ _partner);
+
waitForInput(60000);
}
diff --git a/engines/eem/mystery.cpp b/engines/eem/mystery.cpp
index 39c854535d9..6b6bfbfc56f 100644
--- a/engines/eem/mystery.cpp
+++ b/engines/eem/mystery.cpp
@@ -243,6 +243,16 @@ uint16 Mystery::alibiTextOffset(uint suspectIdx) const {
return READ_LE_UINT16(gd + suspectIdx * 0x46 + 0x02);
}
+const byte *Mystery::hintBlock() const {
+ // Header word at index 9 (`_hintOffset`) â used by `_KDHelp @
+ // 1560:010a`'s per-chain-clue hint table. Each pair-of-bytes is
+ // a TextBlock offset for the corresponding `_AChain` entry, or
+ // `0xFFFF` if no hint is defined for that chain position.
+ if (!isLoaded() || _hintOffset == 0 || _hintOffset >= _data.size())
+ return nullptr;
+ return _data.data() + _hintOffset;
+}
+
const byte *Mystery::solvedClueBlock() const {
if (!isLoaded() || _solvedOffset == 0 || _solvedOffset >= _data.size())
return nullptr;
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 34a354085f1..e444fc4b8b7 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -668,6 +668,16 @@ void SiteScreen::run() {
// trigger a hotspot underneath.
const Common::Rect kBtnNotebook(35, 111, 56, 136);
const Common::Rect kBtnMap ( 7, 177, 57, 200);
+ // Partner area â port-only enhancement so the player
+ // can click the host sprite for a hint, mirroring the
+ // PDA's rect-3 / gallery's rect-3 behaviour. The
+ // original site loop's `_FindButton(&SiteButtons, 2,
+ // ...)` only checks notebook + map, but the same
+ // partner-click â `_KDHelp` shortcut is wired in
+ // `_HandleNoteButton[3]` (0x0403) and
+ // `_HandleGalleryButton[3]` (0x061e). Rect matches
+ // the PDA / gallery `kBtnPartner` (5, 80, 44, 110).
+ const Common::Rect kBtnPartner ( 5, 80, 44, 110);
if (kBtnNotebook.contains(event.mouse.x, event.mouse.y)) {
_vm->doNotebook();
enter(cur);
@@ -680,6 +690,11 @@ void SiteScreen::run() {
enter(cur);
break;
}
+ if (kBtnPartner.contains(event.mouse.x, event.mouse.y)) {
+ _vm->doHelp();
+ enter(cur);
+ break;
+ }
const int idx = hotspotAtPoint(cur, event.mouse.x, event.mouse.y);
if (idx >= 0) {
onHotspotClicked(cur, (uint)idx);
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 45cea187910..4c6efe1b600 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -2757,19 +2757,115 @@ void EEMEngine::doAccuse() {
if (!_mystery.isLoaded())
return;
- // Mirrors `_DoAccuseGallery @ 1df2:0a31`:
- // 1. Show KD's hint balloon (KDTextIndex[+8] text).
- // 2. `_GetBackground(0x3f)` â same backdrop as PDA / gallery.
- // 3. `_DrawGallery()` â renders portraits at the standard 5 slots
- // (positions verified at 29be:0x116, bottom-aligned baseline 0x48).
- // 4. Click loop dispatching on `_NoteButtons` (same table as PDA)
- // with a separate `_HandleAccuseNoteButton` jump table.
+ // Mirrors `_DoAccuse @ 1df2:0bdd` + `_DoAccuseGallery @ 1df2:0a31`:
+ // 1. The original notes-selection screen runs first (PIC 0x1A7).
+ // We skip it because note-selection lives in the PDA in our
+ // port â the accuse path is reached via the SOLVE button.
+ // 2. EVIDENCE GATE: `_DoAccuse @ 1df2:0bdd` calls `_SolvedCheck`
+ // (1df2:0c75) before `_DoAccuseGallery`. If `selectedPoints
+ // <= 99`, the original shows a partner balloon
+ // (`KDTextIndex[+6]` text + `_SayKDDigital(3)`) and returns
+ // to `_LastScreen` â the suspect picker is NEVER opened.
+ // Our previous flow opened the picker and only checked after
+ // the player committed a suspect; that let the player accuse
+ // without the required evidence.
+ // 3. If the gate passes:
+ // a. KD intro balloon (`KDTextIndex[+8]` text + `_SayKDDigital(4)`).
+ // b. `_GetBackground(0x3f)` + `_DrawGallery()` â portraits at
+ // the 5 fixed slots (`29be:0x116`).
+ // c. Click loop on portraits â `_WITCH(picked)` â guilty/alibi.
const uint8 num = _mystery.numSuspects();
if (num == 0)
return;
const byte *gd = _mystery.galleryData();
+ // Evidence gate. `_DoAccuse @ 1df2:0c75` runs `_SolvedCheck`
+ // before opening the suspect picker; on failure it renders a
+ // partner balloon over the CURRENT screen (the PDA in the
+ // original) and returns. We render the same hint over the
+ // caller's screen and bail back to `_lastScreen` without ever
+ // touching the gallery BG.
+ if (!_mystery.solvedCheck()) {
+ const byte *kdIdx = _mystery.kdTextIndex();
+ const int16 hintOff = kdIdx
+ ? (int16)READ_LE_UINT16(kdIdx + 6)
+ : -1;
+ Common::String hint;
+ if (hintOff != -1)
+ hint = parseString(_mystery.textAt((uint16)hintOff),
+ _playerName, _partner);
+ if (hint.empty()) {
+ // Fallback if `KDTextIndex[+6]` isn't set in this mystery.
+ hint = (_mystery.selectedPoints() == 0)
+ ? "We're not ready to solve this mystery yet. "
+ "Let's keep investigating until we have some "
+ "more solid evidence."
+ : "We don't have quite enough evidence yet. "
+ "Let's review our notes and find a few more "
+ "clues before we accuse anyone.";
+ }
+
+ // Compose balloon overlay on the current screen. Mirrors the
+ // `_GetKDTextBalloon` + `_GetBalloon` + `_AddPicBackground`
+ // + `_WordWrap` sequence at 1df2:0c8d-0cd1.
+ Graphics::ManagedSurface ms(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ ms.clear();
+ Graphics::Surface *cur = g_system->lockScreen();
+ if (cur) {
+ for (int row = 0; row < 200; row++)
+ memcpy((byte *)ms.getBasePtr(0, row),
+ (const byte *)cur->getBasePtr(0, row), 320);
+ g_system->unlockScreen();
+ }
+ const byte firstChar =
+ hint.empty() ? (byte)0 : (byte)hint[0];
+ const uint16 bubNum = getKDTextBalloon(firstChar);
+ // Strip the digit prefix used for balloon dispatch â it's
+ // consumed by the original at `_DisplayAlibi @ 1df2:0163`
+ // (`str = pbVar7 + 1`) and shouldn't appear in the rendered
+ // text. `_GetKDTextBalloon` itself doesn't advance past it.
+ if (firstChar >= '0' && firstChar <= '9')
+ hint.deleteChar(0);
+ Picture balloon;
+ const bool haveBalloon =
+ _balloonArchive.size() > (bubNum & 0x7F) &&
+ _balloonArchive.loadEntry(bubNum & 0x7F, balloon);
+ const int balloonX = 0x21;
+ int balloonY = 1;
+ if (haveBalloon && balloon.surface.h < 0x4e)
+ balloonY = (0x50 - balloon.surface.h) / 2;
+ if (haveBalloon) {
+ const byte transp = (byte)(balloon.flags >> 8);
+ ms.transBlitFrom(balloon.surface,
+ Common::Point(balloonX, balloonY),
+ (uint32)transp);
+ }
+ uint16 tx = 5, ty = 4, tw = 155;
+ getBalloonInsets(bubNum, tx, ty, tw);
+ if (_font.isLoaded()) {
+ _font.drawWordWrapped(&ms, balloonX + tx,
+ balloonY + ty, tw, hint,
+ haveBalloon ? 0 : 0xF);
+ }
+ g_system->copyRectToScreen(ms.getPixels(), ms.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+
+ // `_DoAccuse @ 1df2:0cd9` plays `_SayKDDigital(3)` â
+ // partner-specific "not enough evidence" voice line.
+ if (_audio && kdIdx)
+ _audio->sayKDDigital(kdIdx, 3, _partner);
+
+ waitForInput(20000);
+ // `_DoAccuse @ 1df2:0ce5` writes `_NextScreen = _LastScreen`
+ // so the player drops back where they came from.
+ _nextScreen = _lastScreen != kScreenInvalid
+ ? (ScreenId)_lastScreen : kScreenSite;
+ return;
+ }
+
// Verbatim from 29be:0x116 â same five suspect slot positions as
// `_DrawGallery @ 158f:0046`.
Picture accuseBg;
@@ -2809,6 +2905,14 @@ void EEMEngine::doAccuse() {
const byte firstChar =
hint.empty() ? (byte)0 : (byte)hint[0];
const uint16 bubNum = getKDTextBalloon(firstChar);
+ // Strip the digit prefix used for balloon dispatch.
+ // `_DisplayAlibi @ 1df2:0163` does `str = pbVar7 + 1`
+ // after using `*str` for `bindx`. Same pattern used by
+ // `_DisplayHint`: digit picks the bubble shape AND is
+ // then consumed from the rendered text. Without this
+ // the intro balloon shows e.g. "1Ready to solve?".
+ if (firstChar >= '0' && firstChar <= '9')
+ hint.deleteChar(0);
Picture balloon;
const bool haveBalloon =
_balloonArchive.size() > (bubNum & 0x7F) &&
@@ -2985,48 +3089,22 @@ void EEMEngine::doAccuse() {
// Real chain evaluation. Mirrors the original two-gate accusation:
// 1. `_AccuseEntry @ 1df2:0ff8` checks `_GetFoundPoints() >= 100`
// â gates whether the suspect picker is even reachable. We
- // replicate this with `_SolvedCheck â selectedPoints > 99`,
- // since our port marks clues in the notebook (port deviation;
- // the original gates on the auto-sorted top-5 found clues
- // via `_GetFoundPoints @ 1df2:0098`).
+ // `_SolvedCheck â selectedPoints > 99` is now gated at the
+ // TOP of `doAccuse` â by the time we reach this point we
+ // already know `solvedCheck()` was true (the picker wouldn't
+ // have opened otherwise).
// 2. `_WITCH @ 1df2:089f` checks `GalleryData[picked*0x46+0x02] ==
// 0xFFFF`. Innocent suspects store an alibi-text TextBlock
// offset there; the guilty one uses the sentinel.
- // Both must hold for `_DisplayCorrect`; otherwise it's an alibi.
const int points = _mystery.selectedPoints();
- const bool enoughEvidence = _mystery.solvedCheck();
const bool pickedGuilty = _mystery.isGuilty((uint)picked);
- const bool guessedRight = enoughEvidence && pickedGuilty;
+ const bool guessedRight = pickedGuilty;
debugC(1, kDebugScript,
- "doAccuse: picked=%d selectedPts=%d evidence=%s guilty=%s -> %s",
+ "doAccuse: picked=%d selectedPts=%d guilty=%s -> %s",
picked, points,
- enoughEvidence ? "yes" : "no",
pickedGuilty ? "yes" : "no",
guessedRight ? "correct" : "wrong");
- // Insufficient evidence: same hint the original shows when
- // `_AccuseEntry` returns 0 (string at `_KDTextIndex[0]`).
- if (!enoughEvidence && _font.isLoaded()) {
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
- _font.drawWordWrapped(&scratch, 16, 80, 288,
- points == 0
- ? "We're not ready to solve this mystery yet. "
- "Let's keep investigating until we have some "
- "more solid evidence to make our case! "
- "(Press N in the site screen to mark clues.)"
- : "We don't have quite enough solid evidence yet. "
- "Let's review our notes and find a few more "
- "clues before we accuse anyone.",
- 0xF);
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
- waitForInput(15000);
- return;
- }
-
// Wrong suspect: render alibi flow. Mirrors `_DisplayAlibi @
// 1df2:0145` â shows the suspect's alibi text + their picture on
// PIC 0x3e, then returns to the accuse gallery for another try.
@@ -3130,21 +3208,31 @@ void EEMEngine::doAccuse() {
}
// `_DisplayCorrect @ 1df2:073c` calls `_MIDIPlay(5)` (1df2:0789)
- // before `_DifferenceAnimation("scrapbk.ani")` to swap from the
- // travel music to the winner cue.
+ // before `_DisplayClue` to swap from the travel music to the
+ // winner cue.
if (_music)
_music->playMus(5, /*loop=*/false);
+
+ // `_DisplayCorrect @ 1df2:07ac` calls
+ // `_DisplayClue(_Mystery + _MysteryIndex[0x10], 0)` BEFORE the
+ // scrapbook animation. That clueblock is the partner's
+ // chain-by-chain RECAP â they enumerate every required clue
+ // (`Look at this â the suspect was here at 8pm`, `... and
+ // remember the broken vase from the kitchen`, `... so it had
+ // to be X!`) and arrive at the conclusion. Without rendering
+ // it the player goes straight from suspect-pick to the
+ // scrapbook anim and misses the deduction entirely.
+ const byte *solved = _mystery.solvedClueBlock();
+ if (solved)
+ displayClue(solved);
+
+ // `_DifferenceAnimation("scrapbk.ani")` (1df2:0848) â the
+ // physical scrapbook flip animation that introduces the
+ // per-mystery ending pages.
playAnm(Common::Path("SCRAPBK.ANI"), 120, true);
- // `_DisplayCorrect @ 1df2:07ac` then calls `_DisplayClue`
- // against the briefing's winning ClueBlock at
- // `_Mystery + _MysteryIndex[0x10]` BEFORE `_ShowOneScrap`.
- // We don't render that intermediate ClueBlock yet (it ties
- // into the chain-A/B/C result selection); skip straight to
- // the per-mystery ending pages â the same screen
- // `_ShowOneScrap @ 1f78:0773` displays via
- // `_DisplayEnding(num, 1)`. Players see the per-mystery
- // resolution text on top of the ending pic.
+ // `_ShowOneScrap @ 1f78:0773` is `_DisplayEnding(num, 1)` â
+ // the multi-page per-mystery ending narrative.
doShowEnding(mn);
// Mirrors `_SavePlayerRecord` at 1df2:0857 â once the
Commit: 27d73fdbfe92cb2107975cfb780c5c7ba5237b8c
https://github.com/scummvm/scummvm/commit/27d73fdbfe92cb2107975cfb780c5c7ba5237b8c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:47+02:00
Commit Message:
EEM: dedicated code for accusation window
Changed paths:
engines/eem/eem.h
engines/eem/ui.cpp
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 2d95c0102a1..525cf4773f7 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -188,6 +188,14 @@ public:
/// Mirrors `_DoAccuseGallery` @ 1df2:0a31 + `_DisplayEnding` @ 1df2:0548.
void doAccuse();
+ /// Show the accuse-notes screen (PIC 0x1A7, the red "accuse-mode"
+ /// BG with selectable clue list + "N clues" remaining counter).
+ /// Mirrors the outer loop of `_DoAccuse @ 1df2:0bdd`. Returns
+ /// true if the player committed (selected the chain-required
+ /// number of clues and clicked SOLVE), false if they exited via
+ /// ESC / back. Called from `doAccuse` before the evidence gate.
+ bool doAccuseNotes();
+
/// Show a host hint from `KDTextIndex`. Mirrors `_KDHelp` @ 1560:010a +
/// `_DisplayHint` @ 1560:0009. Cycles between the two hint slots that
/// the original engine tracks via `_SawHelpHint`.
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 4c6efe1b600..9956e137bd9 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -1792,13 +1792,15 @@ void EEMEngine::drawNotebookFrame(int &page) {
y += h + 7;
}
- // Page indicator + selected-points counter directly on PIC.
+ // Page indicator only â the original `_DrawNotes @ 161e:01d0`
+ // has no points display in the PDA notebook (the per-clue point
+ // values are SPOILERS for the chain weighting that the engine
+ // uses internally for `_GetSelectedPoints`). Showing the running
+ // total tells the player exactly when they have enough evidence
+ // to solve, which deflates the deduction step.
_font.drawString(&scratch, Common::String::format("p%d/%d",
page + 1, (int)pageStarts.size()),
270, 4, 320, 0x5C);
- _font.drawString(&scratch, Common::String::format("%d pts",
- _mystery.selectedPoints()),
- 270, 14, 320, 0x5C);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
@@ -2753,33 +2755,340 @@ uint16 EEMEngine::getKDTextBalloon(byte firstChar) const {
return kDigitBalloons[firstChar - '0'];
}
+bool EEMEngine::doAccuseNotes() {
+ // Mirrors the accuse-notes screen at the head of `_DoAccuse @
+ // 1df2:0bdd`:
+ // * BG: PIC 0x1A7 (the red "accuse-mode" backdrop).
+ // * `_AccuseNoteRect @ 29be:1048` = (79, 27, 304, 159) holds
+ // the rendered clue list.
+ // * Counter at `(0xd1, 0xb)` = `(209, 11)` shows "N clue(s)"
+ // remaining (`_UpdateSelectionCount @ 1df2:08dd`,
+ // `_Show_String(0xb, 0xd1, ...)`).
+ // * Expected count = `6 - DAT_2d5d_3f99`:
+ // chainStage 1 â 5 clues, 2 â 4 clues, 3 â 3 clues.
+ // * `_NoteUnselectedColor = 1` (red) for unselected, `0x3c`
+ // for selected (1df2:0c2c sets it on entry).
+ // * Click on a clue toggles its selection
+ // (`_SearchNoteAreas` + `_SwapColors`).
+ // * Click `_NoteButtons[2]` (rect at `(157, 174, 178, 190)`,
+ // the original's "go to gallery" button) jumps to the
+ // evidence check; `_HandleAccuseNoteButton(2)` returns 2
+ // and the outer loop forces `uStack_8 = uStack_a` to
+ // trigger `_SolvedCheck`.
+ // * ESC sets `_NextScreen = 3` and exits.
+ if (!_mystery.isLoaded() || !_font.isLoaded())
+ return false;
+ const byte *ni = _mystery.noteIndex();
+ const uint16 niCount = _mystery.noteIndexCount();
+ if (!ni)
+ return false;
+
+ Picture accuseBg;
+ const bool haveBg = _picsArchive.getPicture(0x1a7, accuseBg);
+
+ // Required count for solving â `6 - chainStage`.
+ const uint expected = (_chainStage >= 1 && _chainStage <= 3)
+ ? (uint)(6 - _chainStage)
+ : 5;
+
+ // Build the list of FOUND clue IDs (in clue-ID order; the
+ // original's `_DrawNotes(NULL, 100, ...)` walks `_CluesFound[]`
+ // the same way).
+ Common::Array<uint> found;
+ for (uint i = 0; i < niCount && i < Mystery::kCluesFoundCap; i++) {
+ if (_mystery._cluesFound[i])
+ found.push_back(i);
+ }
+
+ // `_AccuseNoteRect` (79, 27, 304, 159) â text wrap area.
+ const int rectX = 79;
+ const int rectY = 27;
+ const int rectW = 304 - 79;
+ const int rectH = 159 - 27;
+
+ // `_NoteButtons` rects (verified at `29be:0147`). `_DoAccuse`
+ // re-uses the same table as `_DoNotebook`, but only SOLVE /
+ // PAGE NEXT / PAGE PREV do anything; others sit inert.
+ // `_HandleAccuseNoteButton @ 1df2:0990` returns `DI` (initialised
+ // to 0) and only sets `DI = 2` in the `i == 4` branch (asm:
+ // `1df2:09b2: MOV DI, 0x2`). The outer loop's `iVar6 == 2` test
+ // at `1df2:0db2` is checking the HANDLER'S RETURN VALUE, not the
+ // button INDEX â earlier comment had this backwards. So the
+ // SOLVE rect is `[4]` (180, 174, 201, 190), the same icon the
+ // PDA uses to trigger the accuse flow in the first place.
+ const Common::Rect kBtnSolve (180, 174, 201, 190); // [4] SOLVE
+ const Common::Rect kBtnPageNext(204, 174, 224, 190); // [5] PAGE NEXT
+ const Common::Rect kBtnPagePrev(226, 174, 247, 190); // [6] PAGE PREV
+ const Common::Rect kBtnPartner ( 5, 80, 44, 110); // [3] KD HELP
+
+ // Per-page slot rects + their clue IDs (for click hit-testing).
+ Common::Array<Common::Rect> slotRects;
+ Common::Array<uint> slotClues;
+
+ int page = 0;
+ int pageBreaks[16];
+ int numPages = 1;
+ pageBreaks[0] = 0;
+
+ auto rebuildPagination = [&]() {
+ numPages = 1;
+ pageBreaks[0] = 0;
+ const int lineH = _font.getFontHeight() + 1;
+ int y = rectY;
+ for (uint i = 0; i < found.size(); i++) {
+ const uint clueId = found[i];
+ Common::String txt;
+ if (clueId < niCount) {
+ const uint16 textOff = READ_LE_UINT16(ni + clueId * 4);
+ txt = parseString(_mystery.textAt(textOff),
+ _playerName, _partner);
+ }
+ Common::Array<Common::String> wrapped;
+ _font.wordWrapText(txt, rectW, wrapped);
+ const int h = (int)wrapped.size() * lineH;
+ if (y + h + 7 > rectY + rectH) {
+ if (numPages < (int)ARRAYSIZE(pageBreaks)) {
+ pageBreaks[numPages++] = (int)i;
+ y = rectY;
+ }
+ }
+ y += h + 7;
+ }
+ if (page >= numPages)
+ page = numPages - 1;
+ if (page < 0)
+ page = 0;
+ };
+
+ auto draw = [&]() {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ if (haveBg)
+ scratch.simpleBlitFrom(accuseBg.surface);
+
+ // Partner sprite at (5, 0x50). The original `_DoAccuse @
+ // 1df2:0c2c` does `_NewAnimation(5, 0x50, partnerCells,
+ // script=2, prior=1)` â same anim cells as the gallery
+ // (anim 2 for Jake / 0x10 for Jenny) with script 0x02. The
+ // `_UpdateAnimations` loop in `_DoAccuse @ 1df2:0bfa` keeps
+ // the slot painting through the entire selection screen;
+ // without an explicit blit here the player sees a partner-
+ // less accuse-mode screen.
+ const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
+ Animation partnerAni;
+ if (_aniArchive.loadAnimation(partnerAnim, partnerAni) &&
+ !partnerAni.empty()) {
+ const uint32 now = g_system->getMillis();
+ const uint frameIdx = partnerFrameAtTick(0x02,
+ (uint)partnerAni.size(), now);
+ blitAnimFrameAnchored(scratch.surfacePtr(),
+ partnerAni[frameIdx], 5, 0x50);
+ }
+
+ // Clue list inside `_AccuseNoteRect`. Selected = 0x3c (yellow),
+ // unselected = 1 (red), per `_NoteUnselectedColor = 1` set at
+ // 1df2:0c25 â the red colour is what gives the screen its
+ // "accuse-mode" look together with PIC 0x1A7.
+ slotRects.clear();
+ slotClues.clear();
+ const int lineH = _font.getFontHeight() + 1;
+ const int startIdx = pageBreaks[page];
+ const int endIdx = (page + 1 < numPages)
+ ? pageBreaks[page + 1]
+ : (int)found.size();
+ int y = rectY;
+ uint selectedCount = 0;
+ for (uint i = 0; i < found.size(); i++) {
+ if (_mystery._noteSelected[found[i]])
+ selectedCount++;
+ }
+ for (int i = startIdx; i < endIdx; i++) {
+ const uint clueId = found[i];
+ Common::String txt;
+ if (clueId < niCount) {
+ const uint16 textOff = READ_LE_UINT16(ni + clueId * 4);
+ txt = parseString(_mystery.textAt(textOff),
+ _playerName, _partner);
+ }
+ if (txt.empty())
+ txt = Common::String::format("clue %u", clueId);
+ Common::Array<Common::String> wrapped;
+ _font.wordWrapText(txt, rectW, wrapped);
+ const int h = (int)wrapped.size() * lineH;
+ const byte color = _mystery._noteSelected[clueId] ? 0x3c : 0x01;
+ for (uint li = 0; li < wrapped.size(); li++) {
+ _font.drawString(&scratch, wrapped[li], rectX,
+ y + (int)li * lineH, rectW, color);
+ }
+ slotRects.push_back(Common::Rect(rectX, y,
+ rectX + rectW, y + h));
+ slotClues.push_back(clueId);
+ y += h + 7;
+ }
+
+ // Counter â `_UpdateSelectionCount(remaining)` at (0xd1, 0xb).
+ const uint remaining = (selectedCount < expected)
+ ? expected - selectedCount
+ : 0;
+ const Common::String counter = Common::String::format("%u %s",
+ remaining, remaining == 1 ? "clue" : "clues");
+ _font.drawString(&scratch, counter, 209, 11, 100, 0x0F);
+
+ if (numPages > 1) {
+ _font.drawString(&scratch,
+ Common::String::format("p%d/%d", page + 1, numPages),
+ rectX, 11, 60, 0x0F);
+ }
+
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ };
+
+ rebuildPagination();
+ draw();
+
+ while (!shouldQuit()) {
+ Common::Event ev;
+ bool dirty = false;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return false;
+ if (ev.type == Common::EVENT_KEYDOWN) {
+ if (ev.kbd.keycode == Common::KEYCODE_ESCAPE)
+ return false;
+ if (ev.kbd.keycode == Common::KEYCODE_LEFT &&
+ page > 0) {
+ page--;
+ dirty = true;
+ } else if (ev.kbd.keycode == Common::KEYCODE_RIGHT &&
+ page + 1 < numPages) {
+ page++;
+ dirty = true;
+ }
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ const int mx = ev.mouse.x;
+ const int my = ev.mouse.y;
+ // Page navigation â `_NoteButtons[5]` / `[6]`,
+ // dispatched in `_HandleAccuseNoteButton @
+ // 1df2:0990`. Only effective if there's another
+ // page in that direction; mirrors the
+ // `1 < _CurrentPage` guard at 1df2:09a8 and the
+ // `_NextClue != -1` guard at 1df2:099e.
+ if (kBtnPageNext.contains(mx, my)) {
+ if (page + 1 < numPages) {
+ page++;
+ dirty = true;
+ }
+ continue;
+ }
+ if (kBtnPagePrev.contains(mx, my)) {
+ if (page > 0) {
+ page--;
+ dirty = true;
+ }
+ continue;
+ }
+ // Partner click â `_NoteButtons[3]`. Original
+ // `_HandleAccuseNoteButton` doesn't dispatch this
+ // (no case for i == 3), but the rect is still in
+ // the table; we wire it to the puzzle hint so the
+ // player can ask the partner what to look for
+ // without leaving accuse mode.
+ if (kBtnPartner.contains(mx, my)) {
+ doHelp();
+ dirty = true;
+ continue;
+ }
+ if (kBtnSolve.contains(mx, my)) {
+ // Count selected.
+ uint selected = 0;
+ for (uint i = 0; i < found.size(); i++) {
+ if (_mystery._noteSelected[found[i]])
+ selected++;
+ }
+ if (selected == expected) {
+ // Commit â let the caller do the
+ // `_SolvedCheck` + suspect picker dance.
+ return true;
+ }
+ // Wrong count â `_DoAccuse` only triggers the
+ // check when `uStack_8 == uStack_a`; we just
+ // stay in the loop so the player can keep
+ // adjusting.
+ continue;
+ }
+ // Toggle clue under cursor.
+ for (uint i = 0; i < slotRects.size(); i++) {
+ if (slotRects[i].contains(mx, my)) {
+ const uint clueId = slotClues[i];
+ _mystery._noteSelected[clueId] ^= 1;
+ dirty = true;
+ break;
+ }
+ }
+ }
+ }
+ if (dirty)
+ draw();
+ // Per-tick redraw so the partner sprite cycles. Same
+ // 100 ms cadence as `_CheckFrameRate` + `_UpdateAnimations`
+ // in the original (1df2:0bfa).
+ static uint32 sLastTick = 0;
+ const uint32 now = g_system->getMillis();
+ if (now - sLastTick >= 100) {
+ sLastTick = now;
+ draw();
+ }
+ g_system->updateScreen();
+ g_system->delayMillis(15);
+ }
+ return false;
+}
+
void EEMEngine::doAccuse() {
- if (!_mystery.isLoaded())
+ if (!_mystery.isLoaded() || !_font.isLoaded())
return;
// Mirrors `_DoAccuse @ 1df2:0bdd` + `_DoAccuseGallery @ 1df2:0a31`:
- // 1. The original notes-selection screen runs first (PIC 0x1A7).
- // We skip it because note-selection lives in the PDA in our
- // port â the accuse path is reached via the SOLVE button.
- // 2. EVIDENCE GATE: `_DoAccuse @ 1df2:0bdd` calls `_SolvedCheck`
- // (1df2:0c75) before `_DoAccuseGallery`. If `selectedPoints
- // <= 99`, the original shows a partner balloon
- // (`KDTextIndex[+6]` text + `_SayKDDigital(3)`) and returns
- // to `_LastScreen` â the suspect picker is NEVER opened.
- // Our previous flow opened the picker and only checked after
- // the player committed a suspect; that let the player accuse
- // without the required evidence.
- // 3. If the gate passes:
- // a. KD intro balloon (`KDTextIndex[+8]` text + `_SayKDDigital(4)`).
- // b. `_GetBackground(0x3f)` + `_DrawGallery()` â portraits at
- // the 5 fixed slots (`29be:0x116`).
- // c. Click loop on portraits â `_WITCH(picked)` â guilty/alibi.
+ // 1. ACCUSE-NOTES SCREEN (PIC 0x1A7, the red "accuse-mode" BG):
+ // `_DrawNotes(_AccuseNoteRect, NULL, 100, _NoteSelected)`
+ // lists every found clue inside the rect at `29be:1048` =
+ // `(79, 27, 304, 159)`. `_UpdateSelectionCount(remaining)`
+ // shows "N clue(s)" at `(209, 11)` (ASM: `_Show_String(0xb,
+ // 0xd1, ...)` at 1df2:0907). `_NoteUnselectedColor = 1` is
+ // the dim red used for unselected entries; selected ones
+ // get `0x3c`. Click toggles via `_SearchNoteAreas` +
+ // `_SwapColors`. Expected count = `6 - DAT_2d5d_3f99`
+ // (= 6 - chainStage):
+ // stage 1 â 5 clues, stage 2 â 4, stage 3 â 3.
+ // When the count matches, `_SolvedCheck` decides:
+ // fail â KD "not enough evidence" balloon â return.
+ // pass â `_DoAccuseGallery()` (suspect picker).
+ // 2. KD intro balloon (`KDTextIndex[+8]` + `_SayKDDigital(4)`).
+ // 3. `_GetBackground(0x3f)` + `_DrawGallery()` â portraits at
+ // the 5 fixed slots (`29be:0x116`).
+ // 4. Click loop on portraits â `_WITCH(picked)` â guilty/alibi.
const uint8 num = _mystery.numSuspects();
if (num == 0)
return;
const byte *gd = _mystery.galleryData();
+ // ACCUSE-NOTES SCREEN â let the player commit which N clues they
+ // believe solve the case. Mirrors the click-driven selection of
+ // `_DoAccuse @ 1df2:0bdd`'s outer loop. ESC / cancel returns to
+ // the site (matches `_DoAccuse @ 1df2:0c11` writing
+ // `_NextScreen = 3` on ESC).
+ if (!doAccuseNotes()) {
+ _nextScreen = _lastScreen != kScreenInvalid
+ ? (ScreenId)_lastScreen : kScreenSite;
+ return;
+ }
+
// Evidence gate. `_DoAccuse @ 1df2:0c75` runs `_SolvedCheck`
// before opening the suspect picker; on failure it renders a
// partner balloon over the CURRENT screen (the PDA in the
Commit: 4b19ad71a33e7cbb764e5ada33cb4c4662a5301e
https://github.com/scummvm/scummvm/commit/4b19ad71a33e7cbb764e5ada33cb4c4662a5301e
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:47+02:00
Commit Message:
EEM: missing background when accusation resulted in the correct answer
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 9956e137bd9..962e06210e4 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -3516,19 +3516,50 @@ void EEMEngine::doAccuse() {
}
}
- // `_DisplayCorrect @ 1df2:073c` calls `_MIDIPlay(5)` (1df2:0789)
- // before `_DisplayClue` to swap from the travel music to the
- // winner cue.
+ // `_DisplayCorrect @ 1df2:073c` order:
+ // 1df2:0773 _AllBlack();
+ // 1df2:0776 _BuildBackground(5, 0x42, 0x14); // conclusion BG
+ // 1df2:0780 _FadeIn();
+ // 1df2:0789 _MIDIPlay(5); // win music
+ // 1df2:07ac _DisplayClue(MysteryIndex[+0x10]); // chain recap
+ // `_BuildBackground @ 172b:13e2` loads PIC 0x3D (the standard
+ // frame) and overlays SITES.DBD entry 5 at (0x42, 0x14), then
+ // sets palette via `_GetPalette(sitenum + 1)` = palette 6.
+ // Without this BG the chain-recap balloons render on top of
+ // the accuse-gallery BG (PIC 0x3F + suspect portraits), which
+ // is visually jarring â the conclusion is supposed to play
+ // against the dedicated "office / desk" scene.
+ Graphics::Surface *blk = g_system->lockScreen();
+ if (blk) {
+ memset(blk->getPixels(), 0, 320 * 200);
+ g_system->unlockScreen();
+ }
+ setSitePalette(6); // sitenum + 1 per `_GetPalette` call
+ Picture frame, scene;
+ if (_picsArchive.loadEntry(0x3d, frame)) {
+ g_system->copyRectToScreen(frame.surface.getPixels(),
+ frame.surface.pitch, 0, 0,
+ frame.surface.w, frame.surface.h);
+ }
+ if (5 < _sitesArchive.size() &&
+ _sitesArchive.loadEntry(5, scene)) {
+ const int sx = 0x42, sy = 0x14;
+ const int sw = MIN<int>(scene.surface.w, 320 - sx);
+ const int sh = MIN<int>(scene.surface.h, 200 - sy);
+ if (sw > 0 && sh > 0)
+ g_system->copyRectToScreen(scene.surface.getPixels(),
+ scene.surface.pitch, sx, sy,
+ sw, sh);
+ }
+ g_system->updateScreen();
+
if (_music)
_music->playMus(5, /*loop=*/false);
- // `_DisplayCorrect @ 1df2:07ac` calls
- // `_DisplayClue(_Mystery + _MysteryIndex[0x10], 0)` BEFORE the
- // scrapbook animation. That clueblock is the partner's
- // chain-by-chain RECAP â they enumerate every required clue
- // (`Look at this â the suspect was here at 8pm`, `... and
- // remember the broken vase from the kitchen`, `... so it had
- // to be X!`) and arrive at the conclusion. Without rendering
+ // Chain-by-chain RECAP. Partner enumerates every required
+ // clue ("Look at this â the suspect was here at 8pm", "... and
+ // remember the broken vase from the kitchen", "... so it had
+ // to be X!") and arrives at the conclusion. Without rendering
// it the player goes straight from suspect-pick to the
// scrapbook anim and misses the deduction entirely.
const byte *solved = _mystery.solvedClueBlock();
Commit: 1e6b78b869f2d96642e1e34fd9286cda2db590ab
https://github.com/scummvm/scummvm/commit/1e6b78b869f2d96642e1e34fd9286cda2db590ab
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:47+02:00
Commit Message:
EEM: implement wrong accusation
Changed paths:
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/site.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 9cf6ab97843..5b419853d44 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -143,21 +143,36 @@ Common::Error EEMEngine::run() {
debugC(1, kDebugGeneral, "EEM engine starting");
// If the user chose "Load" before pressing Play, the framework
- // invokes `loadGameState` which sets up `_mystery` and `_partner`.
- // Honour that by skipping the intros and dropping into the screen-
- // driver loop just past the briefing â the loaded mystery already
- // knows its current site, so we resume at MAP (the original's
- // post-briefing state, set by handler 0 at 1a35:0e1d).
+ // invokes `loadGameState` which sets up `_playerName`, `_partner`,
+ // `_mysteriesSolved`, and (optionally) `_mystery`. Honour that by
+ // skipping the intros â the player has already typed their name
+ // and picked a partner, so the title chain + profile picker +
+ // partner picker would all be redundant.
+ //
+ // * Save HAS a mystery in progress â resume at MAP (mirrors the
+ // original's post-briefing state, handler 0 at 1a35:0e1d).
+ // * Save has NO mystery â drop into the case-selection screen
+ // (`kScreenChooseMystery`) so the player can pick which case
+ // to play. This matches what the original `_ActionScreen`
+ // leads to â without the redundant action menu in front.
const int wantedSave = ConfMan.hasKey("save_slot")
? ConfMan.getInt("save_slot") : -1;
bool resumed = false;
if (wantedSave >= 0) {
const Common::Error err = loadGameState(wantedSave);
- if (err.getCode() == Common::kNoError && _mystery.isLoaded()) {
- debugC(1, kDebugGeneral, "Resuming from slot %d at mystery %u",
- wantedSave, _mystery.number());
+ if (err.getCode() == Common::kNoError) {
CursorMan.showMouse(true);
- _nextScreen = kScreenMap;
+ if (_mystery.isLoaded()) {
+ debugC(1, kDebugGeneral,
+ "Resuming from slot %d at mystery %u",
+ wantedSave, _mystery.number());
+ _nextScreen = kScreenMap;
+ } else {
+ debugC(1, kDebugGeneral,
+ "Resuming profile from slot %d (no mystery â "
+ "â case selection)", wantedSave);
+ _nextScreen = kScreenChooseMystery;
+ }
resumed = true;
}
}
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 525cf4773f7..d58629c9061 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -170,6 +170,9 @@ public:
/// followed by 62-byte ClueEntries. Mirrors _DisplayClue @ 2404:05e6.
void displayClue(const byte *clueBlock);
+ /// Active player name (saved as the profile-save description).
+ const Common::String &playerName() const { return _playerName; }
+
/// Apply a single ClueEntry's side effects â notebook adds, gallery
/// updates, site flags. Called both by `displayClue` after a normal
/// click-through and when the player ESC-skips a multi-entry clue.
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index e444fc4b8b7..b453150eb3c 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -1374,6 +1374,12 @@ void SiteScreen::onHotspotClicked(uint siteNum, uint hotIdx) {
hotIdx, clueOff);
const byte *clueBlock = _mystery->blobAt(clueOff);
if (clueBlock) {
+ // Snapshot `_cluesFound` BEFORE the clue display so we
+ // can detect if any new clue was actually collected
+ // (vs. a re-read of an already-found clue) â only worth
+ // auto-saving when the player makes progress.
+ byte before[Mystery::kCluesFoundCap];
+ memcpy(before, _mystery->_cluesFound, sizeof(before));
// Hand the engine our partner-less backdrop so that
// `_DoKDAnim` / `playKdAnim` (the camera-style reaction
// animation that fires when a ClueEntry has +0x3a != -1)
@@ -1384,6 +1390,26 @@ void SiteScreen::onHotspotClicked(uint siteNum, uint hotIdx) {
_vm->setPartnerEraseBg(&_bgSnapshot);
_vm->displayClue(clueBlock);
_vm->setPartnerEraseBg(nullptr);
+ // Auto-save when a new clue is found. The original
+ // engine has no autosave (saving is a manual SETUP
+ // button, `_SaveGame @ 2404:0c87`); we add the autosave
+ // here so the player never loses mystery progress.
+ // Detected via 0â1 transition in `_cluesFound[]` (set
+ // by `applyClueSideEffects` inside `displayClue`).
+ bool foundNewClue = false;
+ for (uint i = 0; i < Mystery::kCluesFoundCap; i++) {
+ if (!before[i] && _mystery->_cluesFound[i]) {
+ foundNewClue = true;
+ break;
+ }
+ }
+ if (foundNewClue) {
+ const Common::Error err =
+ _vm->saveProfile(_vm->playerName());
+ if (err.getCode() != Common::kNoError)
+ warning("auto-save after clue failed: %s",
+ err.getDesc().c_str());
+ }
}
}
// Caller (`SiteScreen::run`) re-renders the site after this returns.
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 962e06210e4..9ee920fb720 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -3414,14 +3414,31 @@ void EEMEngine::doAccuse() {
pickedGuilty ? "yes" : "no",
guessedRight ? "correct" : "wrong");
- // Wrong suspect: render alibi flow. Mirrors `_DisplayAlibi @
- // 1df2:0145` â shows the suspect's alibi text + their picture on
- // PIC 0x3e, then returns to the accuse gallery for another try.
- // Skips MIDI 6 (alibi music) and the per-partner voice line â the
- // text rendering alone is enough to convey "this suspect is
- // innocent". `_FirstTry` is cleared so a subsequent correct pick
- // no longer counts as a first-try win (1df2:0445 -> _FirstTry=0).
+ // Wrong suspect: full alibi flow. Mirrors `_DisplayAlibi @
+ // 1df2:0145`:
+ // 1. Plays MIDI 6 (loser sting) and waits for it to finish while
+ // the gallery is still on screen (1df2:0184-1df2:0192).
+ // 2. Draws PIC 0x3e + the suspect's speech balloon + their
+ // portrait at (0x82, py), where the balloon shape comes from
+ // `AlibiBubbles[bindx]` (table @ 29be:1050) and bindx is the
+ // digit-prefix on the alibi text (else 2). bindx<8 centres the
+ // balloon horizontally; bindx>=8 pins it at x=0x21.
+ // 3. Plays the suspect's voice via `_SpoolSound(talk - 1)` where
+ // `talk = (Partner == 0) ? gd[+0x6] : gd[+0x0]` (1df2:0258),
+ // then waits for a click.
+ // 4. Overlays the partner's reaction balloon (text @
+ // `KDTextIndex[+10]`) at (0x21, y) and plays
+ // `_SayKDDigital(5)`.
+ // 5. Clears `_FirstTry` (1df2:0447) and returns to LastScreen.
if (!guessedRight) {
+ // Balloon-shape table @ 29be:1050 â 16 entries à u16.
+ static const uint16 kAlibiBubbles[16] = {
+ 0x002B, 0x002C, 0x002D, 0x002E,
+ 0x00AB, 0x00AC, 0x00AD, 0x00AE,
+ 0x001D, 0x001E, 0x0015, 0x0016,
+ 0x0017, 0x0018, 0x0019, 0x001A,
+ };
+
const uint16 alibiOff = _mystery.alibiTextOffset((uint)picked);
Common::String alibi;
if (gd && alibiOff != 0xFFFF) {
@@ -3429,46 +3446,230 @@ void EEMEngine::doAccuse() {
if (raw)
alibi = parseString(raw, _playerName, _partner);
}
+ // Digit-prefix dispatch â `_DisplayAlibi @ 1df2:0163` reads
+ // `*str` for `bindx` and advances `str = pbVar7 + 1` so the
+ // digit doesn't reach the renderer. Non-digit first chars fall
+ // through to the default bindx=2 (1df2:015e).
+ uint bindx = 2;
+ const byte firstChar = alibi.empty() ? (byte)0 : (byte)alibi[0];
+ if (firstChar >= '0' && firstChar <= '9') {
+ bindx = (uint)(firstChar - '0');
+ alibi.deleteChar(0);
+ }
+ if (bindx >= 16)
+ bindx = 2;
+ const uint16 bubNum = kAlibiBubbles[bindx];
+
Picture alibiBg;
- const bool haveAlibiBg =
- _picsArchive.getPicture(0x3e, alibiBg);
+ const bool haveAlibiBg = _picsArchive.getPicture(0x3e, alibiBg);
Picture suspect;
const uint16 picId = gd
? READ_LE_UINT16(gd + (uint)picked * 0x46)
: 0;
const bool haveSuspect = picId != 0 &&
_picsArchive.getPicture(picId, suspect);
+ Picture balloon;
+ const bool haveBalloon =
+ _balloonArchive.size() > (bubNum & 0x7F) &&
+ _balloonArchive.loadEntry(bubNum & 0x7F, balloon);
- Graphics::ManagedSurface scratch(320, 200,
+ // Position math from 1df2:01a4-1df2:0207. py is the suspect
+ // portrait's Y; defaults to 0x5a, only overridden in the
+ // bindx<8 branch when the balloon is too tall to fit.
+ int balloonX = 0x21;
+ int balloonY = 1;
+ int py = 0x5a;
+ if (bindx < 8) {
+ const int bw = haveBalloon ? balloon.surface.w : 0;
+ const int bh = haveBalloon ? balloon.surface.h : 0;
+ balloonX = (320 - bw) / 2;
+ if (bh < 0x5a) {
+ balloonY = (0x5a - bh) / 2;
+ } else {
+ balloonY = 1;
+ py = bh;
+ }
+ } else {
+ const int bh = haveBalloon ? balloon.surface.h : 0;
+ balloonX = 0x21;
+ balloonY = (bh < 0x4f) ? (0x50 - bh) / 2 : 1;
+ }
+
+ // `base` = BG + suspect + partner sprite â the persistent layer
+ // that survives across both balloon phases. The original engine
+ // keeps PIC 0x3e in the master BG buffer (16000), `_AddPicBackground`
+ // commits the suspect there, and the partner animation
+ // registered by `_DoAccuse @ 1df2:0c30` is re-blitted by every
+ // `_Repaint` via `_DrawActiveAnimations`. We don't have a
+ // slot-based animation system, so we manually keep a "base"
+ // surface and re-draw the partner frame for each phase.
+ Graphics::ManagedSurface base(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
- if (haveAlibiBg) {
- scratch.simpleBlitFrom(alibiBg.surface);
- }
+ base.clear();
+ if (haveAlibiBg)
+ base.simpleBlitFrom(alibiBg.surface);
if (haveSuspect) {
- // Original `_DisplayAlibi` blits the suspect at (0x82, py)
- // where py varies by balloon size; we use a fixed centred
- // position since we don't render the balloon shapes yet.
const byte transp = (byte)(suspect.flags >> 8);
- scratch.transBlitFrom(suspect.surface,
- Common::Point(0x82, 0x40),
+ base.transBlitFrom(suspect.surface,
+ Common::Point(0x82, py),
+ (uint32)transp);
+ }
+ // Partner sprite at (5, 0x50). Anim cells: 2 (Jake) / 0x10
+ // (Jenny); script key 0x02 â same indices `_DoAccuse @
+ // 1df2:0c30` uses for its `_NewAnimation` call. Partner is
+ // drawn AFTER the suspect so it doesn't get clipped by the
+ // portrait if their bounding boxes graze.
+ const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
+ Animation partnerAni;
+ const bool havePartner =
+ _aniArchive.loadAnimation(partnerAnim, partnerAni) &&
+ !partnerAni.empty();
+
+ // Alibi-phase scratch = base + alibi balloon + alibi text +
+ // partner sprite (animation slot drawn on top per
+ // `_DrawActiveAnimations`).
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.simpleBlitFrom(base);
+ if (haveBalloon) {
+ const byte transp = (byte)(balloon.flags >> 8);
+ scratch.transBlitFrom(balloon.surface,
+ Common::Point(balloonX, balloonY),
(uint32)transp);
}
+ // Balloon-text inset table @ 29be:0875 â same dispatch as KD
+ // balloons. WordWrap color is 0 inside a balloon (1df2:0240).
+ uint16 tx = 5, ty = 4, tw = 155;
+ getBalloonInsets(bubNum, tx, ty, tw);
if (_font.isLoaded() && !alibi.empty()) {
- _font.drawWordWrapped(&scratch, 16, 8, 200, alibi, 0xF);
- _font.drawString(&scratch, "(click / ESC: back)",
- 16, 188, 200, 0xF);
+ _font.drawWordWrapped(&scratch, balloonX + tx,
+ balloonY + ty, tw, alibi,
+ haveBalloon ? 0 : 0xF);
+ }
+ if (havePartner) {
+ const uint frameIdx = partnerFrameAtTick(0x02,
+ (uint)partnerAni.size(), g_system->getMillis());
+ blitAnimFrameAnchored(scratch.surfacePtr(),
+ partnerAni[frameIdx], 5, 0x50);
}
+
+ // Step 1 â alibi music. Original blocks until MIDI 6 ends with
+ // the gallery still on screen. We poll `_music->isPlaying`;
+ // click/ESC aborts early.
+ if (_music) {
+ _music->playMus(6, /*loop=*/false);
+ const uint32 musStart = g_system->getMillis();
+ bool aborted = false;
+ while (_music->isPlaying() && !shouldQuit() && !aborted) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return;
+ if (ev.type == Common::EVENT_KEYDOWN ||
+ ev.type == Common::EVENT_LBUTTONDOWN) {
+ aborted = true;
+ break;
+ }
+ }
+ // Hard cap so we never get stuck if MIDI never reports
+ // finish (some sound configurations).
+ if (g_system->getMillis() - musStart > 10000)
+ break;
+ g_system->updateScreen();
+ g_system->delayMillis(20);
+ }
+ _music->stop();
+ }
+
+ // Step 2 â flip the alibi scene to screen + play suspect voice.
+ // `talk = (Partner==0) ? gd[+0x6] : gd[+0x0]` (1df2:0252-0258);
+ // indices are 1-based so subtract 1 before SpoolSound.
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
g_system->updateScreen();
- waitForInput(20000);
+ if (_audio && gd) {
+ const uint16 alibiVoice =
+ READ_LE_UINT16(gd + (uint)picked * 0x46 + 0x00);
+ const uint16 jakeVoice =
+ READ_LE_UINT16(gd + (uint)picked * 0x46 + 0x06);
+ const uint16 talk =
+ (_partner == 0) ? jakeVoice : alibiVoice;
+ if (talk != 0)
+ _audio->spoolSound((uint)(talk - 1));
+ }
+ waitForInput(60000);
+
+ // Step 3 â partner reaction balloon. Mirrors 1df2:026e-1df2:02b6.
+ // Rebuild scratch from `base` (BG + suspect + partner sprite)
+ // so the alibi balloon + text don't bleed through. The
+ // original's `_Repaint` re-renders the master BG (which still
+ // has the alibi balloon committed), but the engine ALSO calls
+ // `_GetBackground(0x3e)` again before `_AddPicBackground` for
+ // each new balloon â flushing the master back to a clean state.
+ // We achieve the same end result by restoring `base` here.
+ // `_SayKDDigital(5)` auto-cancels the still-playing alibi voice
+ // (spoolSound calls stopSpool internally) so no explicit stop.
+ const byte *reactIdx = _mystery.kdTextIndex();
+ if (reactIdx) {
+ const int16 reactOff = (int16)READ_LE_UINT16(reactIdx + 10);
+ Common::String react;
+ if (reactOff != -1) {
+ const char *raw = _mystery.textAt((uint16)reactOff);
+ if (raw)
+ react = parseString(raw, _playerName, _partner);
+ }
+ if (!react.empty()) {
+ const byte rChar = (byte)react[0];
+ const uint16 rBub = getKDTextBalloon(rChar);
+ if (rChar >= '0' && rChar <= '9')
+ react.deleteChar(0);
+ Picture rBalloon;
+ const bool haveR =
+ _balloonArchive.size() > (rBub & 0x7F) &&
+ _balloonArchive.loadEntry(rBub & 0x7F, rBalloon);
+ const int rX = 0x21;
+ int rY = 1;
+ if (haveR && rBalloon.surface.h < 0x4e)
+ rY = (0x50 - rBalloon.surface.h) / 2;
+
+ // Reset to a clean BG + suspect, then layer the new
+ // balloon and the (refreshed) partner frame on top.
+ scratch.simpleBlitFrom(base);
+ if (haveR) {
+ const byte transp = (byte)(rBalloon.flags >> 8);
+ scratch.transBlitFrom(rBalloon.surface,
+ Common::Point(rX, rY),
+ (uint32)transp);
+ }
+ uint16 rtx = 5, rty = 4, rtw = 155;
+ getBalloonInsets(rBub, rtx, rty, rtw);
+ if (_font.isLoaded()) {
+ _font.drawWordWrapped(&scratch, rX + rtx,
+ rY + rty, rtw, react,
+ haveR ? 0 : 0xF);
+ }
+ if (havePartner) {
+ const uint frameIdx = partnerFrameAtTick(0x02,
+ (uint)partnerAni.size(),
+ g_system->getMillis());
+ blitAnimFrameAnchored(scratch.surfacePtr(),
+ partnerAni[frameIdx],
+ 5, 0x50);
+ }
+ g_system->copyRectToScreen(scratch.getPixels(),
+ scratch.pitch, 0, 0, 320, 200);
+ g_system->updateScreen();
+ if (_audio)
+ _audio->sayKDDigital(reactIdx, 5, _partner);
+ }
+ }
+ waitForInput(60000);
_mystery._firstTry = false;
// `_DisplayAlibi @ 1df2:043f` writes `_NextScreen =
- // _LastScreen` so the player drops back to wherever they
- // accused from (typically the site loop). With no mystery
- // state to unload, the site loop resumes naturally.
+ // _LastScreen`. The original returns the player to the caller
+ // (PDA / site / map) for another try.
_nextScreen = _lastScreen != kScreenInvalid
? (ScreenId)_lastScreen : kScreenSite;
return;
Commit: ddb7949f39880d791315b1b621db0b8437bdaa54
https://github.com/scummvm/scummvm/commit/ddb7949f39880d791315b1b621db0b8437bdaa54
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:48+02:00
Commit Message:
EEM: fixed some issues in the win screen/newspaper
Changed paths:
engines/eem/eem.h
engines/eem/site.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index d58629c9061..875d62f608c 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -368,7 +368,19 @@ private:
/// `_ShowOneScrap @ 1f78:0773` is just `_DisplayEnding(num, 1)`,
/// so this same call covers the post-mystery scrapbook view from
/// the action menu.
- void doShowEnding(uint num);
+ ///
+ /// `firstPage=true` opens at page 0; `false` opens at the last
+ /// page (used by `doShowScrapbook` when navigating backwards
+ /// between mysteries â mirrors the `local_8 = 0` write at
+ /// `_ShowScrapbook @ 1f78:067e`).
+ ///
+ /// Returns the direction the user wants the caller to navigate:
+ /// -1 â previous mystery (LEFT pressed on the first page or
+ /// click in `PrevPageRect` while on first page),
+ /// 0 â exit the scrapbook (ESC or click outside both rects),
+ /// +1 â next mystery (RIGHT/SPACE/Enter/click on last page).
+ /// Mirrors `_DisplayEnding`'s `[BP-0x18]` return at 1df2:0723.
+ int doShowEnding(uint num, bool firstPage = true);
/// Walk every solved mystery in tier @p stage (1=Junior, 2=Senior,
/// 3=Master) and display each one's ending pages in sequence.
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index b453150eb3c..4131e6dfafb 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -813,6 +813,15 @@ void SiteScreen::run() {
if (exitRequested)
return;
+ // `doAccuse` on a win clears the mystery (so the screen driver
+ // can route to the post-mystery menu). Notebook / Gallery /
+ // hotspot paths route through this same loop, so a transitive
+ // `doAccuse` may have wiped `_mystery` underneath us â exit
+ // immediately rather than tick another frame against stale BG
+ // snapshots / hotspot tables.
+ if (!_mystery || !_mystery->isLoaded())
+ return;
+
// Per-tick frame pump (mirrors `_CheckFrameRate` +
// `_UpdateAnimations` at the top of `_DoSiteLoop`'s main loop).
// Restore the static BG snapshot, redraw animated NPCs +
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 9ee920fb720..07430ce1012 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -491,7 +491,7 @@ void EEMEngine::doNewPlayer() {
}
}
-void EEMEngine::doShowEnding(uint num) {
+int EEMEngine::doShowEnding(uint num, bool firstPage) {
// Mirrors `_DisplayEnding @ 1df2:0548` + `_DisplayEndingPage @
// 1df2:044c`. File format (verified by reading E0.BIN's bytes):
// u16 pageCount
@@ -500,32 +500,63 @@ void EEMEngine::doShowEnding(uint num) {
// u16 x1, y1, x2, y2 (story rect â passed to WordWrap)
// char text[] (null-terminated, ParseString opcodes)
//
- // The original walks pages with PrevPage / NextPage rects (29be:
- // 0x..); we approximate with mouse-click / Enter = next, ESC =
- // exit. ParseString substitutes `\x80` (player name) and the rest
- // of the 0x80..0x89 placeholder family so the rendered text
- // reads as the correct player + partner combo.
+ // The original swaps the font: `_FreeFont(); _LoadFont("tiny.fnt")`
+ // at 1df2:055f-1df2:0563, calls `_GetPalette(0)` (site palette 0),
+ // then for each page `_GetBackground(picNum)` +
+ // `_WordWrap2(x1, y1, x2-x1, text, fontColor=0, dropColor=-1)`. The
+ // fontColor=0 draws in palette index 0 (the newspaper's body-text
+ // colour), with no drop shadow. Verified at the call site asm
+ // 1df2:04cf-1df2:04f4 (Ghidra mis-paired the two trailing args).
+ //
+ // Page navigation mirrors the original key/click handlers
+ // (1df2:0689 / 1df2:06a0): LEFT decrements pageIdx, RIGHT (or
+ // SPACE / Enter / click) increments it. Hitting the boundary
+ // (LEFT on page 0, RIGHT on last page) sets `[BP-0x18]` to -1 / 1
+ // respectively and exits â that return value is what
+ // `_ShowScrapbook` uses to walk forward / backward through
+ // solved mysteries (see 1f78:0664-1f78:069c). ESC and clicks
+ // outside both PrevPage / NextPage rects exit with `[BP-0x18]=0`.
+ //
+ // `firstPage=false` opens the ending at the LAST page (used by
+ // `doShowScrapbook` after a "previous mystery" navigation â
+ // matches `local_8 = 0` written before the back-step at
+ // 1f78:067e).
const Common::String fname = Common::String::format("E%u.BIN", num);
Common::File f;
if (!f.open(Common::Path(fname))) {
warning("doShowEnding: %s missing", fname.c_str());
- return;
+ return 0;
}
const uint32 size = f.size();
if (size < 2) {
warning("doShowEnding: %s too small (%u bytes)",
fname.c_str(), size);
- return;
+ return 0;
}
Common::Array<byte> buf(size);
if (f.read(buf.data(), size) != size) {
warning("doShowEnding: %s short read", fname.c_str());
- return;
+ return 0;
}
const uint16 pageCount = READ_LE_UINT16(buf.data());
if (pageCount == 0)
- return;
+ return 0;
+
+ // Mirrors 1df2:0558-1df2:056a â `_FreeFont(); _LoadFont(tiny.fnt)`.
+ // The newspaper layout uses TINY.FNT (smaller glyphs) so the body
+ // copy fits in the columns. `_LoadFont(font.fnt)` is restored at
+ // 1df2:0625 after the page loop.
+ EEMFont tinyFont;
+ const bool haveTinyFont = tinyFont.load(Common::Path("TINY.FNT"));
+ if (!haveTinyFont)
+ warning("doShowEnding: TINY.FNT failed to load â falling back");
+
+ // Mirrors 1df2:055f `_GetPalette(0)` â site palette 0 is the
+ // shared "newspaper" CLUT for ending pages. The newspaper body
+ // text in particular is palette index 0 (= newspaper black) so we
+ // MUST switch palettes before rendering.
+ setSitePalette(0);
// Walk page records. Each page header is 10 bytes; text is
// null-terminated and follows the header.
@@ -544,9 +575,11 @@ void EEMEngine::doShowEnding(uint num) {
cursor++; // past the null
}
- uint pageIdx = 0;
+ uint pageIdx = firstPage ? 0 : (kMaxPages - 1);
+ int direction = 0; // -1 / 0 / +1, see header doc.
+ bool exitLoop = false;
bool dirty = true;
- while (!shouldQuit() && pageIdx < kMaxPages) {
+ while (!shouldQuit() && !exitLoop) {
if (dirty) {
const uint off = pageOffsets[pageIdx];
if (off + 10 >= size)
@@ -557,9 +590,6 @@ void EEMEngine::doShowEnding(uint num) {
const uint16 x2 = READ_LE_UINT16(buf.data() + off + 6);
(void)READ_LE_UINT16(buf.data() + off + 8); // y2 (unused â WordWrap2 takes width only)
- // Halve the rect coords: ending pages use 320x400 logical
- // coords (x2=0x128=296, y2=0xa8=168 in our test file
- // E0.BIN â both already 320x200 mode 13h). No conversion.
Picture bg;
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
@@ -578,13 +608,20 @@ void EEMEngine::doShowEnding(uint num) {
const char *raw = (const char *)buf.data() + off + 10;
const Common::String text = parseString(raw, _playerName, _partner);
- if (_font.isLoaded() && x2 > x1) {
+ // Use TINY.FNT (`_LoadFont(@29be:10a5)` at 1df2:055f-0563)
+ // and color 0 (`_WordWrap2(...,0,-1)` per asm at 1df2:04cf,
+ // not 0xF as Ghidra's decompile output suggests). Falls
+ // back to the main font if TINY.FNT failed to load.
+ const EEMFont &renderFont = haveTinyFont ? tinyFont : _font;
+ if (renderFont.isLoaded() && x2 > x1) {
const int textW = MIN<int>((int)x2 - (int)x1, 320 - (int)x1);
- _font.drawWordWrapped(&scratch, (int)x1, (int)y1,
- textW, text, 0xF);
+ renderFont.drawWordWrapped(&scratch, (int)x1, (int)y1,
+ textW, text, 0);
}
- // Page indicator at top-right ("page 1/3").
+ // Page indicator at top-right ("page 1/3"). Stays in the
+ // main font + color 0xF so it doesn't blend into the
+ // newspaper masthead.
if (_font.isLoaded() && kMaxPages > 1) {
const Common::String hdr = Common::String::format(
"%u/%u", (unsigned)pageIdx + 1, (unsigned)kMaxPages);
@@ -600,14 +637,25 @@ void EEMEngine::doShowEnding(uint num) {
Common::Event ev;
while (g_system->getEventManager()->pollEvent(ev)) {
if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
- return;
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ return 0;
+ }
if (ev.type == Common::EVENT_KEYDOWN) {
switch (ev.kbd.keycode) {
case Common::KEYCODE_ESCAPE:
- return;
+ // ESC is the ONLY way out of the newspaper view.
+ // Departs from the original `_DisplayEnding`, which
+ // also exits on boundary arrow keys / clicks
+ // (1df2:0689 / 1df2:06a0); the boundary-exit path is
+ // what fed `[BP-0x18]` to `_ShowScrapbook` for
+ // per-mystery scrapbook navigation. We don't expose
+ // that â clicking ESC closes the scrapbook entirely.
+ direction = 0;
+ exitLoop = true;
+ break;
case Common::KEYCODE_LEFT:
case Common::KEYCODE_PAGEUP:
+ // Clamp at page 0 â never exit on LEFT.
if (pageIdx > 0) {
pageIdx--;
dirty = true;
@@ -618,21 +666,45 @@ void EEMEngine::doShowEnding(uint num) {
case Common::KEYCODE_RETURN:
case Common::KEYCODE_KP_ENTER:
case Common::KEYCODE_SPACE:
- pageIdx++;
- dirty = true;
+ case Common::KEYCODE_TAB:
+ // Clamp at last page â never exit on RIGHT either.
+ if (pageIdx + 1 < kMaxPages) {
+ pageIdx++;
+ dirty = true;
+ }
break;
default:
break;
}
+ if (exitLoop)
+ break;
}
if (ev.type == Common::EVENT_LBUTTONDOWN) {
- pageIdx++;
- dirty = true;
+ // Mouse clicks shift pages too â never exit on click.
+ // Use the original PrevPage/NextPage rect split for
+ // click direction (29be:1078 / 29be:1080); clicks
+ // outside both rects fall through to next-page so the
+ // player still gets some feedback.
+ const Common::Rect kPrevPageRect(0, 0, 27, 200);
+ if (kPrevPageRect.contains(ev.mouse.x, ev.mouse.y)) {
+ if (pageIdx > 0) {
+ pageIdx--;
+ dirty = true;
+ }
+ } else {
+ if (pageIdx + 1 < kMaxPages) {
+ pageIdx++;
+ dirty = true;
+ }
+ }
+ if (exitLoop)
+ break;
}
}
g_system->updateScreen();
g_system->delayMillis(15);
}
+ return direction;
}
void EEMEngine::doShowScrapbook(uint stage) {
@@ -654,7 +726,14 @@ void EEMEngine::doShowScrapbook(uint stage) {
const uint hi = lo + 0x17;
const bool currentTier = (stage == _chainStage);
- for (uint m = lo; m <= hi; m++) {
+ // `doShowEnding` only reports `direction = 0` now (ESC), so we
+ // can't use the original 1f78:067e/0698 forward-backward walk.
+ // Instead iterate every solved mystery in the tier linearly:
+ // each ESC closes the current newspaper and moves us to the
+ // next solved entry. Departs from `_ShowScrapbook @ 1f78:0642`
+ // (which let the player browse via the boundary keys), but
+ // matches the user-requested "ESC is the only exit" rule.
+ for (uint m = lo; m <= hi && !shouldQuit(); m++) {
if (m >= sizeof(_mysteriesSolved))
break;
// Current-tier filter (1f78:0664). Completed tiers show all
@@ -662,9 +741,7 @@ void EEMEngine::doShowScrapbook(uint stage) {
// the player hasn't earned that scrapbook page yet.
if (currentTier && _mysteriesSolved[m] == 0)
continue;
- doShowEnding(m);
- if (shouldQuit())
- return;
+ (void)doShowEnding(m, /*firstPage=*/true);
}
}
@@ -3752,6 +3829,30 @@ void EEMEngine::doAccuse() {
scene.surface.pitch, sx, sy,
sw, sh);
}
+
+ // Partner sprite at (5, 0x50). The original `_DoAccuse @
+ // 1df2:0c30` registered the partner anim BEFORE entering the
+ // gallery; that slot stays active across `_DisplayCorrect`'s
+ // `_BuildBackground` (which calls `_Repaint` â
+ // `_DrawActiveAnimations` and re-blits the partner over the
+ // fresh BG). We don't have a slot system, so manually stamp
+ // the resting frame here. `displayClue` snapshots the screen
+ // on entry, so the partner ends up baked into its BG and is
+ // preserved across every clue iteration.
+ const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
+ Animation partnerAni;
+ if (_aniArchive.loadAnimation(partnerAnim, partnerAni) &&
+ !partnerAni.empty()) {
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (screen) {
+ const uint frameIdx = partnerFrameAtTick(0x02,
+ (uint)partnerAni.size(),
+ g_system->getMillis());
+ blitAnimFrameAnchored(screen, partnerAni[frameIdx],
+ 5, 0x50);
+ g_system->unlockScreen();
+ }
+ }
g_system->updateScreen();
if (_music)
Commit: 3761f15c6e38792ba88b59f34f36e1eb5b6042c7
https://github.com/scummvm/scummvm/commit/3761f15c6e38792ba88b59f34f36e1eb5b6042c7
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:48+02:00
Commit Message:
EEM: fixed missing screen updates
Changed paths:
engines/eem/eem.cpp
engines/eem/site.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 5b419853d44..8386f08be13 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -536,6 +536,12 @@ void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs, bool holdLa
break;
}
}
+ // Refresh ScummVM's cursor overlay every tick â without
+ // this the cursor only redraws when the next frame is
+ // blitted (every `frameDelayMs` ms, ~8 Hz at 120 ms),
+ // which the user perceives as choppy / unresponsive
+ // during long animations like SCRAPBK.ANI.
+ g_system->updateScreen();
g_system->delayMillis(5);
}
if (aborted)
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 4131e6dfafb..5869b827d53 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -1523,7 +1523,11 @@ void EEMEngine::playKdAnim(uint16 num) {
// One frame per `_CheckFrameRate` tick. The original calibrates
// this to ~10 fps; 100 ms matches what the rest of the engine
- // uses for partner / NPC frame cycling.
+ // uses for partner / NPC frame cycling. Pump `updateScreen`
+ // inside the inner wait so ScummVM's cursor overlay refreshes
+ // at the same rate as the wait granularity (10 ms â 100 Hz)
+ // rather than only on the per-frame redraw â without this the
+ // cursor only refreshes once every 100 ms during the anim.
const uint32 wakeup = g_system->getMillis() + 100;
while (g_system->getMillis() < wakeup && !shouldQuit()) {
Common::Event ev;
@@ -1535,6 +1539,7 @@ void EEMEngine::playKdAnim(uint16 num) {
ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
return;
}
+ g_system->updateScreen();
g_system->delayMillis(10);
}
}
Commit: f9a5f920da384227cedd99d5f7b924dd5e3abbe7
https://github.com/scummvm/scummvm/commit/f9a5f920da384227cedd99d5f7b924dd5e3abbe7
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:48+02:00
Commit Message:
EEM: correctly load game when a mystery is solved
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 07430ce1012..1d0dc8091d9 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -1238,6 +1238,14 @@ void EEMEngine::doCaseSelection() {
// Reassert here in case anything between hid it.
CursorMan.showMouse(true);
+ // Reassert site palette 0 (the case-selection / chooser CLUT). In
+ // the normal flow `doProfilePicker` (or the post-screen reset paths
+ // at lines 1402 / 1147 / 1121) leaves us on palette 0 already, but
+ // the launcher-resume path jumps straight here from `_AllBlack`
+ // (palette = all-zero) â without this the BG renders into a black
+ // CLUT and the player sees an empty screen.
+ setSitePalette(0);
+
// Mirrors `_CaseSelection`: load PIC 0x41 as the chooser backdrop.
Picture caseBg;
const bool haveCaseBg = _picsArchive.getPicture(0x41, caseBg);
@@ -3880,21 +3888,23 @@ void EEMEngine::doAccuse() {
// Mirrors `_SavePlayerRecord` at 1df2:0857 â once the
// `_mysteriesSolved` table is updated, the original
// immediately persists the player record so the win sticks
- // even if the player quits before reaching the menu. We do
- // the same by writing back to the active profile (the slot
- // keyed on `_playerName`) rather than clobbering slot 0 like
- // a generic quicksave.
+ // even if the player quits before reaching the menu.
+ //
+ // Order matters: `_mystery.clear()` BEFORE `saveProfile` so the
+ // save records `hasMystery=false`. Otherwise the next load of
+ // this profile sees the just-won mystery still loaded and the
+ // screen driver routes to its map (forcing the player to
+ // replay the win flow). Mirrors `_DisplayCorrect @ 1df2:0851`
+ // (`_DeleteSavedGame` removes the in-progress save before
+ // `_SavePlayerRecord` writes the post-win profile).
+ _mystery.clear();
const Common::Error err = saveProfile(_playerName);
if (err.getCode() != Common::kNoError)
warning("saveProfile after solve failed: %s",
err.getDesc().c_str());
- // `_DisplayCorrect @ 1df2:0895` writes `_NextScreen = 0xc`
- // â the winner returns to the post-mystery `_ActionScreen`.
- // Free the mystery first so the loop can break out cleanly:
- // `_DeleteSavedGame` at 1df2:0851 + `_FreeMystery` at
- // 1df2:08a4 do the same.
- _mystery.clear();
+ // `_DisplayCorrect @ 1df2:0895` writes `_NextScreen = 0xc` â
+ // the winner returns to the post-mystery `_ActionScreen`.
_nextScreen = kScreenAction;
}
}
Commit: 1131e4825f98fc8f307bc9dd2c0dd43506747398
https://github.com/scummvm/scummvm/commit/1131e4825f98fc8f307bc9dd2c0dd43506747398
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:49+02:00
Commit Message:
EEM: implemented screen to load mysteries from different books
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 1d0dc8091d9..1737c1d60ac 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -99,17 +99,65 @@ struct CaseSelectionView {
uint pick;
};
+// Mystery list shown in the "Choose A Mystery" sub-screen. Mirrors
+// `_DoChooseMystery @ 1a35:02b7`: opens BOOK%u.NME (CRLF-separated
+// ASCII strings, last entry is whitespace = sentinel), reads up to 25
+// lines à 40 bytes each, hands the array to `_CaseSelection`. We
+// preserve the trailing whitespace line as the original sentinel
+// since `_DoChoose @ 1c33:0514` walks until `*piVar3 == 0 && piVar3[1]
+// == 0` â but for our renderer we just keep the names array.
+Common::StringArray loadBookNames(uint book) {
+ Common::StringArray names;
+ const Common::String fname = Common::String::format("BOOK%u.NME", book);
+ Common::File f;
+ if (!f.open(Common::Path(fname))) {
+ warning("loadBookNames: %s missing", fname.c_str());
+ return names;
+ }
+ while (!f.eos()) {
+ Common::String line = f.readLine();
+ if (f.eos() && line.empty())
+ break;
+ // `_fgets` in the original reads CRLF terminators with the line;
+ // `Common::File::readLine` strips them, so `line` here is the
+ // text only. Trim trailing whitespace so the sentinel " "
+ // last entry doesn't render as a blank scrollable row.
+ while (!line.empty() &&
+ (line.lastChar() == ' ' || line.lastChar() == '\t' ||
+ line.lastChar() == '\r'))
+ line.deleteLastChar();
+ if (line.empty())
+ continue;
+ names.push_back(line);
+ }
+ return names;
+}
+
// Per-mystery sub-chooser ("Choose A Mystery") view.
+//
+// `names` are the entries from BOOK%d.NME (in display order â index 0
+// = first case in the tier, mystery number = `tierLo + index`).
+// `solvedFlags` is a parallel bool array indicating which entries are
+// already solved (greyed and unselectable in `_DoChoose`).
+// `topRow` is the scroll position; up to 12 entries are visible.
+// `selRow` is the highlighted row (0-based within the names array).
struct CaseSubmenuView {
EEMEngine *vm;
const Picture *caseBg;
bool haveCaseBg;
- const byte *mysteriesSolved;
- uint mysteriesSolvedSize;
- uint sel;
- uint maxMystery;
+ const Common::StringArray *names;
+ const Common::Array<bool> *solvedFlags;
+ uint topRow;
+ uint selRow;
+ uint book; ///< 1..3 â for the "Book N" / "Challenge Book" title
};
+// Mirrors `_DoChoose`'s `DrawList @ 1c33:040d`. 12 visible rows à 10 px
+// at (61, 35); colour palette: 0x13 = highlighted (selected), 0x1B =
+// greyed (already solved), 0x5C = default. We approximate with the
+// closest indices of site palette 0 â 0xF (white) / 0x7 (medium grey)
+// / 0x8 (dark grey) â since we don't decode the original CLUT byte
+// ramp.
void drawCaseSubmenu(const CaseSubmenuView &v) {
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
@@ -121,34 +169,70 @@ void drawCaseSubmenu(const CaseSubmenuView &v) {
memcpy((byte *)scratch.getBasePtr(0, row),
(const byte *)v.caseBg->surface.getBasePtr(0, row), w);
}
- if (v.vm->getFont().isLoaded()) {
- const int kListX = 61;
- const int kListW = 238 - kListX;
- const int kListY0 = 35;
- const int kLineH = 10;
- const int kVisible = 12;
- int top = (int)v.sel - kVisible / 2;
- if (top < 0)
- top = 0;
- if (top + kVisible > (int)v.maxMystery + 1)
- top = (int)v.maxMystery + 1 - kVisible;
- for (int r = 0; r < kVisible; r++) {
- const int idx = top + r;
- if (idx > (int)v.maxMystery)
- break;
- char marker = ' ';
- if ((uint)idx < v.mysteriesSolvedSize) {
- if (v.mysteriesSolved[idx] == 2)
- marker = '*';
- else if (v.mysteriesSolved[idx] == 1)
- marker = '+';
- }
- const char arrow = ((uint)idx == v.sel) ? '>' : ' ';
- v.vm->getFont().drawString(&scratch,
- Common::String::format("%c %c Mystery %d", arrow, marker, idx),
- kListX, kListY0 + r * kLineH, kListW, 0xF);
+ if (!v.vm->getFont().isLoaded() || !v.names)
+ return;
+
+ // Top centred title. `_CaseSelection @ 1c33:0aa3` formats "Book %d"
+ // for tiers 1/2 and "Challenge Book" (sprintf with no arg) for
+ // tier 3. `_Show_String(0xc, (0xba - width)/2 + 0x3c, â¦, 0x10)`
+ // places it horizontally centred over the panel.
+ const Common::String title = (v.book == 3)
+ ? Common::String("Challenge Book")
+ : Common::String::format("Book %u", v.book);
+ const int titleW = v.vm->getFont().getStringWidth(title);
+ const int titleX = (0xba - titleW) / 2 + 0x3c;
+ v.vm->getFont().drawString(&scratch, title, titleX, 12, 320, 0xF);
+
+ const int kListX = 61;
+ const int kListW = 238 - kListX;
+ const int kListY0 = 35;
+ const int kLineH = 10;
+ const int kVisible = 12;
+ const uint count = (uint)v.names->size();
+
+ for (int r = 0; r < kVisible; r++) {
+ const uint idx = v.topRow + (uint)r;
+ if (idx >= count)
+ break;
+ const Common::String &name = (*v.names)[idx];
+ byte color = 0xF; // default
+ if (idx == v.selRow) {
+ color = 0xF; // highlighted
+ } else if (v.solvedFlags && idx < v.solvedFlags->size() &&
+ (*v.solvedFlags)[idx]) {
+ color = 0x8; // greyed (already solved)
+ } else {
+ color = 0x7; // normal
}
+ v.vm->getFont().drawString(&scratch, name,
+ kListX, kListY0 + r * kLineH, kListW, color);
+ }
+
+ // Selection arrow at the left edge of the highlighted row â the
+ // original highlights via colour change but adding an arrow makes
+ // the keyboard-driven path obvious.
+ if (v.selRow >= v.topRow && v.selRow < v.topRow + (uint)kVisible) {
+ const int r = (int)(v.selRow - v.topRow);
+ v.vm->getFont().drawString(&scratch, ">",
+ kListX - 6, kListY0 + r * kLineH, 6, 0xF);
}
+
+ // Scrollbar thumb. `DrawThumb @ 1c33:????` renders a thumb at
+ // (240, 45..146) proportional to scroll position. We draw a
+ // 1-px outlined block to indicate the same range.
+ if (count > (uint)kVisible) {
+ const int trackY0 = 45;
+ const int trackH = 146 - 45;
+ const int thumbH = MAX<int>(8, (trackH * kVisible) / (int)count);
+ const int travel = trackH - thumbH;
+ const int pos = (int)v.topRow * travel /
+ MAX<int>(1, (int)count - kVisible);
+ const Common::Rect thumb(240, trackY0 + pos,
+ 250, trackY0 + pos + thumbH);
+ scratch.fillRect(thumb, 0x8);
+ scratch.frameRect(thumb, 0xF);
+ }
+
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
g_system->updateScreen();
@@ -1413,52 +1497,94 @@ void EEMEngine::doCaseSelection() {
}
// "Choose A Mystery" sub-screen: pick a specific case from the
- // 55-mystery roster. `_CaseSelection @ 1c33:0a87` only shows
- // mysteries in the player's current chain stage:
- // stage 1 (Junior) â 1..24 (start = `iVar1 = 1` @ 1c33:0aff)
- // stage 2 (Senior) â 25..48 (start = `iVar1 = 0x19`)
- // stage 3 (Master) â 49..54 (start = `iVar1 = 0x31`)
- // The original passes `&DAT_2d5d_3f9b + iVar1` as the grey-mask
- // pointer so DoChoose lights up only the tier-relevant entries.
- // We clamp `sel` to the tier range and pre-seed it to the first
- // unsolved case in that range.
+ // chain-stage's roster. Mirrors `_DoChooseMystery @ 1a35:02b7` +
+ // `_CaseSelection @ 1c33:0a87`:
+ // stage 1 (Junior, BOOK1.NME) â mysteries 1..24
+ // stage 2 (Senior, BOOK2.NME) â mysteries 25..48
+ // stage 3 (Master, BOOK3.NME) â mysteries 49..54
+ // `_DoChooseMystery` opens BOOK<stage>.NME and reads up to 25
+ // CRLF-terminated lines into a 25-entry FAR-pointer array passed
+ // to `_CaseSelection`. The grey mask `_Greys = &mysteriesSolved +
+ // stageLo` (1c33:0b22) makes already-solved entries unselectable.
uint stageLo = 1, stageHi = 0x18;
+ uint book = 1;
switch (_chainStage) {
- case 2: stageLo = 0x19; stageHi = 0x30; break;
- case 3: stageLo = 0x31; stageHi = 0x36; break;
+ case 2: stageLo = 0x19; stageHi = 0x30; book = 2; break;
+ case 3: stageLo = 0x31; stageHi = 0x36; book = 3; break;
default: break; // stage 1 (or fallback)
}
if (stageHi > kMaxMystery)
stageHi = kMaxMystery;
- uint sel = stageLo;
- for (uint i = stageLo; i <= stageHi; i++) {
- if (i < sizeof(_mysteriesSolved) && !_mysteriesSolved[i]) {
- sel = i;
- break;
- }
+
+ const Common::StringArray names = loadBookNames(book);
+ if (names.empty()) {
+ warning("doCaseSelection: BOOK%u.NME failed to load â bailing",
+ book);
+ _mystery.clear();
+ return;
}
+ const uint listLen = MIN<uint>((uint)names.size(), stageHi - stageLo + 1);
+
+ // Per-row solved flags. `_DoChoose @ 1c33:0521` skips solved entries
+ // when seeding the initial selection (`while *_Greys[select] != 0`)
+ // and again per-click via the same mask check.
+ Common::Array<bool> solvedFlags;
+ solvedFlags.resize(listLen);
+ for (uint i = 0; i < listLen; i++) {
+ const uint mn = stageLo + i;
+ solvedFlags[i] =
+ mn < sizeof(_mysteriesSolved) && _mysteriesSolved[mn] != 0;
+ }
+
+ // Seed the selection at the first unsolved entry â same as
+ // `_DoChoose`'s `while (*Greys[select] != 0) select++;` loop at
+ // 1c33:0524.
+ uint selRow = 0;
+ while (selRow < listLen && solvedFlags[selRow])
+ selRow++;
+ if (selRow >= listLen)
+ selRow = 0; // every case solved â let player re-pick
+ uint topRow = 0;
+ const uint kVisible = 12;
+ if (selRow >= kVisible) {
+ topRow = selRow - kVisible / 2;
+ if (topRow + kVisible > listLen)
+ topRow = listLen > kVisible ? listLen - kVisible : 0;
+ }
+
+ auto clampTopRow = [&](uint &t) {
+ if (listLen <= kVisible) {
+ t = 0;
+ return;
+ }
+ const uint maxTop = listLen - kVisible;
+ if (t > maxTop)
+ t = maxTop;
+ };
CaseSubmenuView sv;
sv.vm = this;
sv.caseBg = &caseBg;
sv.haveCaseBg = haveCaseBg;
- sv.mysteriesSolved = _mysteriesSolved;
- sv.mysteriesSolvedSize = sizeof(_mysteriesSolved);
- sv.sel = sel;
- sv.maxMystery = kMaxMystery;
+ sv.names = &names;
+ sv.solvedFlags = &solvedFlags;
+ sv.topRow = topRow;
+ sv.selRow = selRow;
+ sv.book = book;
drawCaseSubmenu(sv);
bool confirmed = false;
while (!confirmed && !shouldQuit()) {
Common::Event ev;
+ bool dirty = false;
while (g_system->getEventManager()->pollEvent(ev)) {
if (ev.type == Common::EVENT_QUIT ||
ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
return;
if (ev.type == Common::EVENT_LBUTTONDOWN) {
- // Same `_DoChoose` rectangles as the top-level menu.
if (kOkRect.contains(ev.mouse.x, ev.mouse.y)) {
- confirmed = true;
+ if (selRow < listLen && !solvedFlags[selRow])
+ confirmed = true;
break;
}
if (kExitRect.contains(ev.mouse.x, ev.mouse.y)) {
@@ -1466,35 +1592,32 @@ void EEMEngine::doCaseSelection() {
return;
}
if (kUpArrowRect.contains(ev.mouse.x, ev.mouse.y)) {
- sel = (sel <= stageLo) ? stageHi : sel - 1;
- sv.sel = sel;
- drawCaseSubmenu(sv);
+ if (topRow > 0) { topRow--; dirty = true; }
continue;
}
if (kDnArrowRect.contains(ev.mouse.x, ev.mouse.y)) {
- sel = (sel >= stageHi) ? stageLo : sel + 1;
- sv.sel = sel;
- drawCaseSubmenu(sv);
+ topRow++;
+ clampTopRow(topRow);
+ dirty = true;
continue;
}
if (kListRect.contains(ev.mouse.x, ev.mouse.y)) {
- // Pick the row under the cursor.
+ // Pick the row under the cursor â mirrors
+ // 1c33:0635 `i = (MouseY - DAT_29be_0d02) / 10;`.
const int kLineH = 10;
- const int kVisible = 12;
- int top = (int)sel - kVisible / 2;
- if (top < 0)
- top = 0;
- if (top + kVisible > (int)kMaxMystery + 1)
- top = (int)kMaxMystery + 1 - kVisible;
- const int row = (ev.mouse.y - kListRect.top) / kLineH;
- const int idx = top + row;
- if (idx >= 0 && idx <= (int)kMaxMystery) {
- sel = (uint)idx;
- sv.sel = sel;
- drawCaseSubmenu(sv);
- }
+ const int row = (ev.mouse.y - 35) / kLineH;
+ if (row < 0 || row >= (int)kVisible)
+ continue;
+ const uint idx = topRow + (uint)row;
+ if (idx >= listLen)
+ continue;
+ if (solvedFlags[idx])
+ continue; // greyed entries ignore clicks
+ selRow = idx;
+ dirty = true;
continue;
}
+ continue;
}
if (ev.type != Common::EVENT_KEYDOWN)
continue;
@@ -1503,56 +1626,80 @@ void EEMEngine::doCaseSelection() {
_mystery.clear();
return;
}
- if (k == Common::KEYCODE_RETURN) {
- confirmed = true;
+ if (k == Common::KEYCODE_RETURN ||
+ k == Common::KEYCODE_KP_ENTER) {
+ if (selRow < listLen && !solvedFlags[selRow])
+ confirmed = true;
break;
}
- if (k >= Common::KEYCODE_0 && k <= Common::KEYCODE_9) {
- sel = (uint)(k - Common::KEYCODE_0);
- sv.sel = sel;
- drawCaseSubmenu(sv);
- continue;
- }
if (k == Common::KEYCODE_DOWN || k == Common::KEYCODE_TAB) {
- sel = (sel >= stageHi) ? stageLo : sel + 1;
- sv.sel = sel;
- drawCaseSubmenu(sv);
+ if (selRow + 1 < listLen) {
+ selRow++;
+ if (selRow >= topRow + kVisible) {
+ topRow = selRow - kVisible + 1;
+ clampTopRow(topRow);
+ }
+ dirty = true;
+ }
continue;
}
if (k == Common::KEYCODE_UP) {
- sel = (sel <= stageLo) ? stageHi : sel - 1;
- sv.sel = sel;
- drawCaseSubmenu(sv);
+ if (selRow > 0) {
+ selRow--;
+ if (selRow < topRow)
+ topRow = selRow;
+ dirty = true;
+ }
continue;
}
if (k == Common::KEYCODE_PAGEDOWN) {
- sel = (sel + 10 > stageHi) ? stageHi : sel + 10;
- sv.sel = sel;
- drawCaseSubmenu(sv);
+ selRow = MIN<uint>(selRow + kVisible, listLen - 1);
+ if (selRow >= topRow + kVisible) {
+ topRow = selRow - kVisible + 1;
+ clampTopRow(topRow);
+ }
+ dirty = true;
continue;
}
if (k == Common::KEYCODE_PAGEUP) {
- sel = (sel < stageLo + 10) ? stageLo : sel - 10;
- sv.sel = sel;
- drawCaseSubmenu(sv);
+ selRow = (selRow >= kVisible) ? selRow - kVisible : 0;
+ if (selRow < topRow)
+ topRow = selRow;
+ dirty = true;
continue;
}
- if (k == Common::KEYCODE_HOME) { sel = stageLo; sv.sel = sel; drawCaseSubmenu(sv); continue; }
- if (k == Common::KEYCODE_END) { sel = stageHi; sv.sel = sel; drawCaseSubmenu(sv); continue; }
+ if (k == Common::KEYCODE_HOME) {
+ selRow = 0;
+ topRow = 0;
+ dirty = true;
+ continue;
+ }
+ if (k == Common::KEYCODE_END) {
+ selRow = listLen - 1;
+ topRow = listLen > kVisible ? listLen - kVisible : 0;
+ dirty = true;
+ continue;
+ }
+ }
+ if (dirty) {
+ sv.topRow = topRow;
+ sv.selRow = selRow;
+ drawCaseSubmenu(sv);
}
g_system->updateScreen();
g_system->delayMillis(15);
}
- if (!_mystery.load(sel, &_rng)) {
- warning("doCaseSelection: failed to load mystery %u", sel);
+ const uint mn = stageLo + selRow;
+ if (!_mystery.load(mn, &_rng)) {
+ warning("doCaseSelection: failed to load mystery %u", mn);
_mystery.clear();
return;
}
if (_audio)
- _audio->initMysterySounds(sel);
+ _audio->initMysterySounds(mn);
debugC(1, kDebugMystery, "Mystery %u loaded; %u sites, %u suspects",
- sel, _mystery.numSites(), _mystery.numSuspects());
+ mn, _mystery.numSites(), _mystery.numSuspects());
}
void EEMEngine::doNotebook() {
Commit: c3f24c4280f65bc095c473e6363785e09e9076f9
https://github.com/scummvm/scummvm/commit/c3f24c4280f65bc095c473e6363785e09e9076f9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:49+02:00
Commit Message:
EEM: unlock different scrapbooks until the end
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 1737c1d60ac..1622c1cf8cb 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -1283,25 +1283,44 @@ void EEMEngine::doCaseSelection() {
" See ScrapBook 2",
" See ScrapBook 3"
};
- // ScrapBook entries are gated by chain stage exactly as the
- // original `_ActionScreen @ 1c33:195b` does at 1c33:19f3-19f7:
- // stage 1 + nothing solved â ScrapBook 1/2/3 all greyed
- // stage 1 + â¥1 solved â ScrapBook 1 enabled, 2/3 greyed
- // stage 2 â ScrapBook 1 enabled, 2 enabled, 3 greyed
- // stage 3 â all three enabled
- // (`_3f9b[i] != 0` over the tier's mystery range is the per-tier
- // gate; we approximate "any in tier" by checking _chainStage and
- // any solved flag.)
+ // Menu entry gating per `_ActionScreen @ 1c33:195b` â the asm at
+ // 1c33:19d1-1a70 sets greys[] based on chain stage AND per-tier
+ // solve count:
+ // stage 1 â grey ScrapBook 2/3; grey ScrapBook 1 if no tier-1 solves
+ // stage 2 â grey Practice + ScrapBook 3; grey ScrapBook 2 if no tier-2 solves
+ // stage 3 â grey Practice; grey ScrapBook 3 if no tier-3 solves
+ // stage 4 â grey Choose + Practice (post-completion read-only state)
+ // In other words: each tier's ScrapBook unlocks as soon as you've
+ // solved your first case in that tier. Practice Mystery is only
+ // available at stage 1. Choose A Mystery is greyed once every case
+ // in every tier is solved (stage 4).
bool anySolved1 = false;
for (uint i = 1; i <= 0x18 && i < sizeof(_mysteriesSolved); i++)
if (_mysteriesSolved[i]) { anySolved1 = true; break; }
- const bool scrap1On = anySolved1 || _chainStage >= 2;
- const bool scrap2On = _chainStage >= 2;
- const bool scrap3On = _chainStage >= 3;
+ bool anySolved2 = false;
+ for (uint i = 0x19; i <= 0x30 && i < sizeof(_mysteriesSolved); i++)
+ if (_mysteriesSolved[i]) { anySolved2 = true; break; }
+ bool anySolved3 = false;
+ for (uint i = 0x31; i <= 0x36 && i < sizeof(_mysteriesSolved); i++)
+ if (_mysteriesSolved[i]) { anySolved3 = true; break; }
+
+ const bool chooseOn = _chainStage < 4;
+ const bool practiceOn = _chainStage <= 1;
+ const bool scrap1On =
+ _chainStage >= 2 || (_chainStage == 1 && anySolved1);
+ const bool scrap2On =
+ _chainStage >= 3 || (_chainStage == 2 && anySolved2);
+ const bool scrap3On =
+ _chainStage >= 4 || (_chainStage == 3 && anySolved3);
const bool kPickEnabled[kNumPicks] = {
- true, true, scrap1On, scrap2On, scrap3On
+ chooseOn, practiceOn, scrap1On, scrap2On, scrap3On
};
- uint pick = kPickChoose;
+ // Seed selection on the first enabled entry â at stage 4 the
+ // `Choose A Mystery` default is greyed, so we land on ScrapBook 1.
+ uint pick = 0;
+ for (uint i = 0; i < kNumPicks; i++) {
+ if (kPickEnabled[i]) { pick = i; break; }
+ }
const char *kSeparator = "----------------------------------";
@@ -1383,7 +1402,11 @@ void EEMEngine::doCaseSelection() {
if (ev.type == Common::EVENT_LBUTTONDOWN) {
// OK / EXIT / HELP buttons (rectangles from `_DoChoose`).
if (kOkRect.contains(ev.mouse.x, ev.mouse.y)) {
- confirmed = true;
+ // Greyed entries can't be confirmed (mirrors
+ // `_DoChoose @ 1c33:0635` â clicks on a `_Greys[i]
+ // != 0` row are ignored before `select` is set).
+ if (kPickEnabled[pick])
+ confirmed = true;
break;
}
if (kExitRect.contains(ev.mouse.x, ev.mouse.y)) {
@@ -1420,8 +1443,10 @@ void EEMEngine::doCaseSelection() {
confirmed = true;
break;
}
- if (k == Common::KEYCODE_RETURN) {
- confirmed = true;
+ if (k == Common::KEYCODE_RETURN ||
+ k == Common::KEYCODE_KP_ENTER) {
+ if (kPickEnabled[pick])
+ confirmed = true;
break;
}
if (k == Common::KEYCODE_UP || k == Common::KEYCODE_LEFT) {
@@ -3941,7 +3966,13 @@ void EEMEngine::doAccuse() {
if (i >= sizeof(_mysteriesSolved) || _mysteriesSolved[i] == 0)
allSolved = false;
}
- if (allSolved && _chainStage < 3) {
+ // `_DisplayCorrect @ 1df2:0852` increments unconditionally
+ // when every case in the current tier is solved â including
+ // past stage 3 (so a stage-4 endgame state exists in the
+ // `.PLR` save format). `_ActionScreen @ 1c33:19d1` gates
+ // the menu on `if (3 < _chainStage)` to grey
+ // Choose-A-Mystery and Practice once everything's solved.
+ if (allSolved && _chainStage < 4) {
_chainStage++;
debugC(1, kDebugMystery,
"chainStage advanced to %u after solving mystery %u",
Commit: ab8cd8380c3799ffec83050126e667d367d65994
https://github.com/scummvm/scummvm/commit/ab8cd8380c3799ffec83050126e667d367d65994
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:49+02:00
Commit Message:
EEM: fixed animation issue
Changed paths:
engines/eem/clues.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 69a805fffd8..cd9362494e9 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -434,12 +434,18 @@ void EEMEngine::doInitClues() {
blitMaskedToScreen(book[frame % book.size()], 0, 99);
if (haveNancy)
blitMaskedToScreen(nancy[frame % nancy.size()], 0x68, 0x8b);
- // Anchor: original blits at `(sx - frame.width,
- // sy - frame.rowoff)`. `frame.rowoff` is the y-anchor
- // in our PicData. We use width/height directly since
- // loadAnimation places anchor at (0, 0).
- const int dstX = (int)0xcd - (int)fr.surface.w;
- const int dstY = (int)seqY - (int)fr.rowoff;
+ // Anchor: `_PlayInSequence @ 172b:2d35-2d50` does
+ // dstX = sx - cell[+0x8] ; miscflags (signed)
+ // dstY = sy - cell[+0x6] ; rowoff (signed)
+ // Ghidra's C decompile mis-labels `cell[+0x8]` as
+ // `width`; the actual asm is `SUB AX, ES:[BX+0x8]`,
+ // which is the per-frame X anchor offset stored in our
+ // `Picture::miscflags` field. Using `fr.surface.w`
+ // shifted every frame by the cell width and made the
+ // briefing partner appear duplicated next to the BG-
+ // baked figure (mystery 1 office briefing).
+ const int dstX = (int)0xcd - (int)(int16)fr.miscflags;
+ const int dstY = (int)seqY - (int)(int16)fr.rowoff;
blitMaskedToScreen(fr, dstX, dstY);
g_system->updateScreen();
const uint32 wakeup = g_system->getMillis() + 100;
Commit: 62063295f02fce3abad575c1b7ae25c1518322e8
https://github.com/scummvm/scummvm/commit/62063295f02fce3abad575c1b7ae25c1518322e8
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:50+02:00
Commit Message:
EEM: unseen hotspot effect
Changed paths:
engines/eem/site.cpp
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 5869b827d53..4ebb6389f0d 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -541,6 +541,29 @@ void SiteScreen::enter(uint siteNum) {
const uint16 sitepic = sd ? READ_LE_UINT16(sd) : 0;
_vm->setSitePaletteForSite(sitepic);
+ // SITEPALS ships with palette indices 0xF9..0xFE all set to a
+ // uniform yellow (`3F 3E 00` = R=63 G=62 B=0 for every site palette
+ // in the file), so the original `_DrawRect`'s cycling colour
+ // pattern + `_ColorCycle(0xF9, 0xFE)` produced a uniformly-coloured
+ // outline with no visible movement â the "marching ants" pattern
+ // was a placeholder that never lit up. Override those entries with
+ // a 6-step yellow ramp here so the existing per-tick rotation
+ // (`applyColorCycles â cyclePaletteRange(0xF9, 0xFE)`) creates a
+ // pulsing glow on unsearched hotspots. Done after `setSitePalette`
+ // so the per-site palette load doesn't clobber the ramp.
+ {
+ // 6-step yellow glow: dark â bright â dim â ...
+ static const byte kAntsGlow[6 * 3] = {
+ 0x40, 0x40, 0x00, // F9 â dim
+ 0x80, 0x80, 0x00, // FA
+ 0xC0, 0xC0, 0x00, // FB
+ 0xFF, 0xFF, 0x40, // FC â peak
+ 0xC0, 0xC0, 0x00, // FD
+ 0x80, 0x80, 0x00, // FE
+ };
+ g_system->getPaletteManager()->setPalette(kAntsGlow, 0xF9, 6);
+ }
+
renderBackground(siteNum);
// `_DoSiteLoop @ 168d:03f4` plays `_EnterSiteAnim` whenever
@@ -1271,16 +1294,24 @@ void SiteScreen::renderHotspots(uint siteNum) {
// Mirrors `_DrawSearchButtons @ 2404:0a8f`:
// for each hotspot:
- // if `_Sawit(theSite, loc)` (= _SaveBuffer[hotspot[+0xa]] != 0)
- // _DrawRect(rect) // outline in cycling colors 0xF9..0xFE
- // else
- // _DrawSolidRect(rect) // outline in solid white 0xFF
- // `_DrawRect`'s cycling colors produce a "marching ants" effect that
- // makes already-searched hotspots visually distinct without hiding
- // them. We approximate the cycling by rotating the start color via
- // the global tick.
+ // if `_Sawit(theSite, loc) == 0` (NOT seen yet):
+ // `_DrawRect(rect)` â outline in cycling colors
+ // 0xF9..0xFE; `_ColorCycle(0xF9,
+ // 0xFE)` rotates them every tick â
+ // "marching ants" glow that draws
+ // the player's eye to unsearched
+ // spots.
+ // else (seen):
+ // `_DrawSolidRect(rect)` â outline in solid colour 0xFF.
+ // (Verified at the actual asm `2404:0af6 OR AX,AX; 2404:0af8 JZ` â
+ // the C-level decompile mis-reordered the if/else branches.)
+ //
+ // We don't need per-pixel colour cycling here because palette
+ // `0xF9..0xFE` is already rotated by `applyColorCycles` each tick;
+ // drawing the outline with any single colour in that range will
+ // pulse on its own. We pick a phased start so adjacent hotspots
+ // don't all glow in lock-step.
const uint32 tickMs = g_system->getMillis();
- const byte cyclePhase = (byte)((tickMs / 80) & 0x07); // 0..7
for (uint i = 0; i < count; i++) {
const byte *r = spots + i * 14;
@@ -1293,39 +1324,44 @@ void SiteScreen::renderHotspots(uint siteNum) {
MIN<int>(screen->h, y2));
const bool seen = (i < Mystery::kHotSpotsCap)
&& _mystery->_hotSpotsSeen[i];
- if (!seen) {
- // `_DrawSolidRect` â solid white outline (color 0xFF).
+ if (seen) {
+ // `_DrawSolidRect` (172b:0506) â outline in palette
+ // index 0xFF (a fixed, non-cycling colour) so already-
+ // found hotspots visually retreat into the BG.
screen->frameRect(rect, 0xFF);
} else {
- // `_DrawRect` â cycling colors 0xF9..0xFE on each pixel of
- // the outline. We approximate per-pixel cycling with a
- // per-rect phase shift so the rects look animated. Start
- // color is rotated via the global clock.
- const byte palette[6] = { 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE };
- byte color = palette[cyclePhase % 6];
+ // `_DrawRect` (172b:03e2) â outline in palette indices
+ // 0xF9..0xFE which `_ColorCycle(0xF9, 0xFE)` rotates
+ // every tick. Walk all four edges incrementing the
+ // colour per pixel, exactly like the original.
+ byte color = (byte)(0xF9 + ((i + (tickMs / 80)) & 0x07) % 6);
+ auto bumpColor = [&]() {
+ const byte next = (byte)(color + 1);
+ color = (next > 0xFE) ? (byte)0xF9 : next;
+ };
// Top edge
for (int x = rect.left; x < rect.right; x++) {
if (x >= 0 && x < screen->w && rect.top >= 0 && rect.top < screen->h)
*(byte *)screen->getBasePtr(x, rect.top) = color;
- color = palette[(color - 0xF9 + 1) % 6];
+ bumpColor();
}
// Right edge
for (int y = rect.top; y < rect.bottom; y++) {
if (rect.right - 1 >= 0 && rect.right - 1 < screen->w && y >= 0 && y < screen->h)
*(byte *)screen->getBasePtr(rect.right - 1, y) = color;
- color = palette[(color - 0xF9 + 1) % 6];
+ bumpColor();
}
// Bottom edge
for (int x = rect.right - 1; x >= rect.left; x--) {
if (x >= 0 && x < screen->w && rect.bottom - 1 >= 0 && rect.bottom - 1 < screen->h)
*(byte *)screen->getBasePtr(x, rect.bottom - 1) = color;
- color = palette[(color - 0xF9 + 1) % 6];
+ bumpColor();
}
// Left edge
for (int y = rect.bottom - 1; y >= rect.top; y--) {
if (rect.left >= 0 && rect.left < screen->w && y >= 0 && y < screen->h)
*(byte *)screen->getBasePtr(rect.left, y) = color;
- color = palette[(color - 0xF9 + 1) % 6];
+ bumpColor();
}
}
}
Commit: 8fa2ec9202bfa34f4d52fb99bfca3aa87e795ae0
https://github.com/scummvm/scummvm/commit/8fa2ec9202bfa34f4d52fb99bfca3aa87e795ae0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:50+02:00
Commit Message:
EEM: make sure sound effect played are correct (or don't play any)
Changed paths:
engines/eem/clues.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index cd9362494e9..3b460988ce2 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -773,16 +773,28 @@ void EEMEngine::displayClue(const byte *clueBlock) {
// `_DisplayClue` @ 2404:0833-085a â after the balloon is drawn,
// spool the per-clue voice. Each ClueEntry stores two 1-based
- // sound indices: `+0x18` for partner=Jenny and `+0x1a` for
- // partner=Jake (verified against 2404:0823-0834). Index 0 / -1
- // = no audio. The original blocks until the line ends; we run
- // async (the wait happens implicitly while the player reads).
+ // sound indices: `+0x18` (Jenny voice) and `+0x1a` (Jake voice).
+ //
+ // Critical gate (verified at 2404:0833):
+ // if (clue[+0x18] != 0 && voiceOn && voiceAvail) {
+ // iVar6 = clue[+0x18]; // Jenny default
+ // if (Partner == 0) iVar6 = clue[+0x1a]; // Jake override
+ // _SpoolSound(iVar6 - 1);
+ // }
+ // The condition gates on the JENNY slot regardless of partner.
+ // Some hotspot ClueBlocks define `+0x1a` (Jake voice) but leave
+ // `+0x18` at 0 â for those, the original engine plays nothing
+ // and the entry is text-only. Our previous code gated on the
+ // partner-selected slot and ended up firing unrelated voices
+ // (e.g., a "no audio" entry triggering Jake's spoolSound).
if (_audio) {
const uint16 voiceJenny = READ_LE_UINT16(c + 0x18);
- const uint16 voiceJake = READ_LE_UINT16(c + 0x1a);
- const uint16 voice = (_partner == 0) ? voiceJake : voiceJenny;
- if (voice != 0 && voice != 0xFFFF)
- _audio->spoolSound((uint)(voice - 1));
+ if (voiceJenny != 0 && voiceJenny != 0xFFFF) {
+ const uint16 voiceJake = READ_LE_UINT16(c + 0x1a);
+ const uint16 voice = (_partner == 0) ? voiceJake : voiceJenny;
+ if (voice != 0 && voice != 0xFFFF)
+ _audio->spoolSound((uint)(voice - 1));
+ }
}
// Wait for click/key to advance â only if we drew something.
Commit: 15962d464e03a707fda1ccd69f5fca471567028a
https://github.com/scummvm/scummvm/commit/15962d464e03a707fda1ccd69f5fca471567028a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:50+02:00
Commit Message:
EEM: removed debug keys
Changed paths:
engines/eem/site.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 4ebb6389f0d..f7a605baf4e 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -728,104 +728,20 @@ void SiteScreen::run() {
}
case Common::EVENT_KEYDOWN:
- switch (event.kbd.keycode) {
- case Common::KEYCODE_ESCAPE:
+ // `_DoSiteLoop @ 168d:07e1` only dispatches on the
+ // 6-entry table at `168d:09d5` (TAB / ENTER / arrow
+ // keys for hotspot cursor cycling) plus ESC handled
+ // separately at 168d:07a9. We don't implement the
+ // hotspot cursor cycling â clicks are the primary
+ // interaction â so the only keyboard binding kept
+ // here is ESC (matches `_ESCHit` â "Are you sure?"
+ // â MAP).
+ if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
if (_vm->areYouSure()) {
- // Mirrors `_DoSiteLoop @ 168d:07b7` ESC path:
- // `_NextScreen = 1` (back to MAP) after the
- // areYouSure confirm. Without explicitly
- // writing it here, the run() loop would see
- // _nextScreen unchanged and treat it as a
- // quit-engine signal â abandoning the case.
- // Going to MAP keeps the case alive so the
- // player can continue from a different site.
_vm->setNextScreen(kScreenMap);
return;
}
enter(cur);
- break;
- case Common::KEYCODE_m:
- _vm->doBigMap();
- // Either way the map covered the site â re-render.
- if (_mystery->_siteNumber < _mystery->numSites())
- cur = _mystery->_siteNumber;
- enter(cur);
- break;
- case Common::KEYCODE_n:
- _vm->doNotebook();
- enter(cur);
- break;
- case Common::KEYCODE_g:
- _vm->doGallery();
- enter(cur);
- break;
- case Common::KEYCODE_a:
- _vm->doAccuse();
- exitRequested = true;
- break;
- case Common::KEYCODE_h:
- _vm->doHelp();
- enter(cur);
- break;
- case Common::KEYCODE_v:
- _showHotspots = !_showHotspots;
- enter(cur);
- break;
- case Common::KEYCODE_r:
- // Restart the mystery from scratch (mirrors `_ReloadMystery`).
- if (_mystery->load(_mystery->number())) {
- if (_vm->_audio)
- _vm->_audio->initMysterySounds(_mystery->number());
- cur = 0;
- enter(cur);
- }
- break;
- case Common::KEYCODE_QUESTION:
- case Common::KEYCODE_F1: {
- if (_vm->getFont().isLoaded()) {
- Graphics::ManagedSurface help(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- help.clear();
- const EEMFont &fnt = _vm->getFont();
- int y = 8;
- const char *lines[] = {
- "EAGLE EYE MYSTERIES â keys",
- "",
- " click search a hotspot",
- " V toggle hotspot outlines",
- " M map (travel between sites)",
- " N notebook (mark evidence with 1..9)",
- " G gallery (suspect portraits)",
- " H hint from the case host",
- " A accuse a suspect",
- " R restart current mystery",
- " Tab next site (cycle)",
- " F5 save / load (ScummVM dialog)",
- " ? / F1 this help",
- " Esc quit (with confirm)",
- "",
- "Notebook: select evidence with 1..9.",
- "Selected-points > 99 wins the case."
- };
- for (uint i = 0; i < sizeof(lines)/sizeof(lines[0]); i++) {
- fnt.drawString(&help, lines[i], 8, y, 320, 0xF);
- y += fnt.getFontHeight() + 1;
- }
- g_system->copyRectToScreen(help.getPixels(),
- help.pitch, 0, 0, 320, 200);
- g_system->updateScreen();
- _vm->waitForInput(60000);
- enter(cur);
- }
- break;
- }
- case Common::KEYCODE_TAB:
- _mystery->_lastSite = cur;
- cur = (cur + 1) % _mystery->numSites();
- enter(cur);
- break;
- default:
- break;
}
break;
@@ -1251,34 +1167,6 @@ void SiteScreen::renderBackground(uint siteNum) {
}
void SiteScreen::renderHotspots(uint siteNum) {
- // HUD overlay: site number, found clues, selected points. Drawn at
- // the BOTTOM of the screen so the scene's top row stays visible â
- // 320x200 mode 13h has a small bottom strip that the original engine
- // uses for tool buttons; we repurpose it for the HUD.
- if (_vm->getFont().isLoaded()) {
- Graphics::Surface *screen = g_system->lockScreen();
- if (screen) {
- uint cluesFound = 0;
- for (uint i = 0; i < Mystery::kCluesFoundCap; i++)
- if (_mystery->_cluesFound[i])
- cluesFound++;
- Common::String hud = Common::String::format(
- "Site %u Clues %u Pts %d M N G H A R V Tab ?",
- siteNum, cluesFound, _mystery->selectedPoints());
- const int hudY = 192;
- screen->fillRect(Common::Rect(0, hudY, 320, 200), 0);
- Graphics::ManagedSurface mgr(320, 9,
- Graphics::PixelFormat::createFormatCLUT8());
- mgr.clear();
- _vm->getFont().drawString(&mgr, hud, 4, 0, 320, 0x0F);
- for (int row = 0; row < 8; row++) {
- memcpy((byte *)screen->getBasePtr(0, hudY + row),
- (const byte *)mgr.getBasePtr(0, row), 320);
- }
- g_system->unlockScreen();
- }
- }
-
// Hotspot outlines (`_DrawSearchButtons`): toggle via V.
if (!_showHotspots)
return;
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 1622c1cf8cb..e9db217e7be 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -1817,9 +1817,6 @@ void EEMEngine::doNotebook() {
ev.kbd.keycode == Common::KEYCODE_TAB) {
page++;
dirty = true;
- } else if (ev.kbd.keycode == Common::KEYCODE_h) {
- doHelp();
- dirty = true;
}
}
if (ev.type == Common::EVENT_LBUTTONDOWN) {
Commit: 7a9ad68ef6ea6bf7c9ca8772f293802bc0305c42
https://github.com/scummvm/scummvm/commit/7a9ad68ef6ea6bf7c9ca8772f293802bc0305c42
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:51+02:00
Commit Message:
EEM: use blit API
Changed paths:
engines/eem/graphics.cpp
engines/eem/site.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/graphics.cpp b/engines/eem/graphics.cpp
index ed8955c555b..33a3536c454 100644
--- a/engines/eem/graphics.cpp
+++ b/engines/eem/graphics.cpp
@@ -181,9 +181,7 @@ void EEMEngine::doHelp() {
{
Graphics::Surface *cur = g_system->lockScreen();
if (cur) {
- for (int row = 0; row < 200; row++)
- memcpy((byte *)ms.getBasePtr(0, row),
- (const byte *)cur->getBasePtr(0, row), 320);
+ ms.simpleBlitFrom(*cur);
g_system->unlockScreen();
}
}
@@ -270,9 +268,7 @@ void EEMEngine::doInterfaceHelp(uint num) {
{
Graphics::Surface *cur = g_system->lockScreen();
if (cur) {
- for (int row = 0; row < 200; row++)
- memcpy((byte *)bg.getBasePtr(0, row),
- (const byte *)cur->getBasePtr(0, row), 320);
+ bg.simpleBlitFrom(*cur);
g_system->unlockScreen();
}
}
@@ -351,10 +347,7 @@ void EEMEngine::setPartnerEraseBg(const Graphics::ManagedSurface *bg) {
if (bg && bg->w == 320 && bg->h == 200) {
_partnerEraseBg.create(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
- for (int row = 0; row < 200; row++) {
- memcpy((byte *)_partnerEraseBg.getBasePtr(0, row),
- (const byte *)bg->getBasePtr(0, row), 320);
- }
+ _partnerEraseBg.simpleBlitFrom(*bg);
} else {
_partnerEraseBg.free();
}
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index f7a605baf4e..35adeb52f90 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -41,22 +41,7 @@ namespace EEM {
// to the screen each tick.
void blitFrame(Graphics::ManagedSurface &dst, const Picture &p,
int x, int y, byte transp) {
- const int w = p.surface.w;
- const int h = p.surface.h;
- for (int row = 0; row < h; row++) {
- const int dstY = y + row;
- if (dstY < 0 || dstY >= 200)
- continue;
- const byte *src = (const byte *)p.surface.getBasePtr(0, row);
- byte *out = (byte *)dst.getBasePtr(0, dstY);
- for (int col = 0; col < w; col++) {
- const int dstX = x + col;
- if (dstX < 0 || dstX >= 320)
- continue;
- if (src[col] != transp)
- out[dstX] = src[col];
- }
- }
+ dst.transBlitFrom(p.surface, Common::Point(x, y), (uint32)transp);
}
// Mask-aware blit from a Picture into a `Graphics::Surface` (the
@@ -809,10 +794,7 @@ void SiteScreen::enterSiteAnim() {
return;
Graphics::ManagedSurface bg(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
- for (int row = 0; row < 200; row++) {
- memcpy((byte *)bg.getBasePtr(0, row),
- (const byte *)screen->getBasePtr(0, row), 320);
- }
+ bg.simpleBlitFrom(*screen);
g_system->unlockScreen();
// Phase 1 â skateboard scroll. `_GetAnimation(6 | 0xe)`.
@@ -833,10 +815,7 @@ void SiteScreen::enterSiteAnim() {
while (x + spriteW > 0 && !_vm->shouldQuit()) {
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
- for (int row = 0; row < 200; row++) {
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)bg.getBasePtr(0, row), 320);
- }
+ scratch.simpleBlitFrom(bg);
blitFrame(scratch, skate[frameIdx], x, y, transp);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
@@ -886,10 +865,7 @@ void SiteScreen::enterSiteAnim() {
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
- for (int row = 0; row < 200; row++) {
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)bg.getBasePtr(0, row), 320);
- }
+ scratch.simpleBlitFrom(bg);
blitFrame(scratch, fr, destX, destY, transp);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
@@ -1043,10 +1019,7 @@ void SiteScreen::captureBgSnapshot() {
_snapshotSite = -1;
return;
}
- for (int row = 0; row < 200; row++) {
- memcpy((byte *)_bgSnapshot.getBasePtr(0, row),
- (const byte *)screen->getBasePtr(0, row), 320);
- }
+ _bgSnapshot.simpleBlitFrom(*screen);
g_system->unlockScreen();
}
@@ -1405,18 +1378,12 @@ void EEMEngine::playKdAnim(uint16 num) {
Graphics::ManagedSurface bg(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
if (_partnerEraseBg.w == 320 && _partnerEraseBg.h == 200) {
- for (int row = 0; row < 200; row++) {
- memcpy((byte *)bg.getBasePtr(0, row),
- (const byte *)_partnerEraseBg.getBasePtr(0, row), 320);
- }
+ bg.simpleBlitFrom(_partnerEraseBg);
} else {
Graphics::Surface *screen = g_system->lockScreen();
if (!screen)
return;
- for (int row = 0; row < 200; row++) {
- memcpy((byte *)bg.getBasePtr(0, row),
- (const byte *)screen->getBasePtr(0, row), 320);
- }
+ bg.simpleBlitFrom(*screen);
g_system->unlockScreen();
}
@@ -1430,10 +1397,7 @@ void EEMEngine::playKdAnim(uint16 num) {
// Restore BG, then masked-blit the next frame.
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
- for (int row = 0; row < 200; row++) {
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)bg.getBasePtr(0, row), 320);
- }
+ scratch.simpleBlitFrom(bg);
// Anchor-aware: kdAnim cells (0x03/0x04/0x0c/0x0d ...) have
// non-zero per-frame `miscflags`/`rowoff` (anim 0x03 has
// rowoff up to 9, anim 0x04 has miscflags = -2). Without
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index e9db217e7be..86dbe5af528 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -162,13 +162,8 @@ void drawCaseSubmenu(const CaseSubmenuView &v) {
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
- if (v.haveCaseBg) {
- const int w = MIN<int>(v.caseBg->surface.w, 320);
- const int h = MIN<int>(v.caseBg->surface.h, 200);
- for (int row = 0; row < h; row++)
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)v.caseBg->surface.getBasePtr(0, row), w);
- }
+ if (v.haveCaseBg)
+ scratch.simpleBlitFrom(v.caseBg->surface);
if (!v.vm->getFont().isLoaded() || !v.names)
return;
@@ -242,14 +237,8 @@ void drawCaseSelectionFrame(const CaseSelectionView &v) {
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
- if (v.haveCaseBg) {
- const int w = MIN<int>(v.caseBg->surface.w, 320);
- const int h = MIN<int>(v.caseBg->surface.h, 200);
- for (int row = 0; row < h; row++) {
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)v.caseBg->surface.getBasePtr(0, row), w);
- }
- }
+ if (v.haveCaseBg)
+ scratch.simpleBlitFrom(v.caseBg->surface);
// KD greeter frame â masked-blit current animation cell at
// (0x112, 0x50). 100 ms tick matches `_CheckFrameRate`. The
@@ -382,13 +371,8 @@ void EEMEngine::doProfilePicker() {
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
Picture bg;
- if (_picsArchive.getPicture(0x104, bg)) {
- const int w = MIN<int>(bg.surface.w, 320);
- const int h = MIN<int>(bg.surface.h, 200);
- for (int row = 0; row < h; row++)
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)bg.surface.getBasePtr(0, row), w);
- }
+ if (_picsArchive.getPicture(0x104, bg))
+ scratch.simpleBlitFrom(bg.surface);
for (uint i = 0; i < entries.size(); i++) {
const byte color = ((int)i == sel) ? 0xF : 0x8;
_font.drawString(&scratch, entries[i].label,
@@ -490,13 +474,8 @@ void EEMEngine::doNewPlayer() {
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
- if (haveBG) {
- const int w = MIN<int>(bg.surface.w, 320);
- const int h = MIN<int>(bg.surface.h, 200);
- for (int row = 0; row < h; row++)
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)bg.surface.getBasePtr(0, row), w);
- }
+ if (haveBG)
+ scratch.simpleBlitFrom(bg.surface);
_font.drawString(&scratch, "Please type your name:", 80, 40, 240, 0xF);
_font.drawString(&scratch, name + "_", 80, 80, 240, 0xF);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
@@ -556,13 +535,8 @@ void EEMEngine::doNewPlayer() {
// Re-render with the updated `name`. Same body as the
// initial render above â only `name + "_"` changes.
scratch.clear();
- if (haveBG) {
- const int w = MIN<int>(bg.surface.w, 320);
- const int h = MIN<int>(bg.surface.h, 200);
- for (int row = 0; row < h; row++)
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)bg.surface.getBasePtr(0, row), w);
- }
+ if (haveBG)
+ scratch.simpleBlitFrom(bg.surface);
_font.drawString(&scratch, "Please type your name:",
80, 40, 240, 0xF);
_font.drawString(&scratch, name + "_", 80, 80, 240, 0xF);
@@ -678,13 +652,8 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
- if (_picsArchive.getPicture(picNum, bg)) {
- const int w = MIN<int>(bg.surface.w, 320);
- const int h = MIN<int>(bg.surface.h, 200);
- for (int row = 0; row < h; row++)
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)bg.surface.getBasePtr(0, row), w);
- }
+ if (_picsArchive.getPicture(picNum, bg))
+ scratch.simpleBlitFrom(bg.surface);
// Story text. The bytes are a null-terminated string with
// `_ParseString` placeholders (0x80 = player name, 0x82
@@ -909,13 +878,8 @@ void EEMEngine::doSetup() {
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
Picture bg;
- if (_picsArchive.getPicture(0x40, bg)) {
- const int w = MIN<int>(bg.surface.w, 320);
- const int h = MIN<int>(bg.surface.h, 200);
- for (int row = 0; row < h; row++)
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)bg.surface.getBasePtr(0, row), w);
- }
+ if (_picsArchive.getPicture(0x40, bg))
+ scratch.simpleBlitFrom(bg.surface);
const byte kKey = 0xFE;
const byte kBright = 0x15;
@@ -1922,13 +1886,8 @@ void EEMEngine::drawNotebookFrame(int &page) {
// PIC 0x3f frame.
Picture frame;
- if (_picsArchive.getPicture(0x3f, frame)) {
- const int w = MIN<int>(frame.surface.w, 320);
- const int h = MIN<int>(frame.surface.h, 200);
- for (int row = 0; row < h; row++)
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)frame.surface.getBasePtr(0, row), w);
- }
+ if (_picsArchive.getPicture(0x3f, frame))
+ scratch.simpleBlitFrom(frame.surface);
// Partner sprite at (5, 80). Anim 1 for Jake, 0xb (11) for Jenny
// for CELLS, but the original `_DoNotebook @ 161e:0500` always
@@ -2805,13 +2764,8 @@ void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
scratch.clear();
Picture frame;
- if (_picsArchive.getPicture(0x42, frame)) {
- const int w = MIN<int>(frame.surface.w, 320);
- const int h = MIN<int>(frame.surface.h, 200);
- for (int row = 0; row < h; row++)
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)frame.surface.getBasePtr(0, row), w);
- }
+ if (_picsArchive.getPicture(0x42, frame))
+ scratch.simpleBlitFrom(frame.surface);
// Marker PICs from `_main @ 1a35:0f59`. Three globals are filled
// once at boot via `_GetPicture` (1-based IDs):
@@ -2916,13 +2870,8 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
scratch.clear();
Picture frame;
- if (_picsArchive.getPicture(0x43, frame)) {
- const int w = MIN<int>(frame.surface.w, 320);
- const int h = MIN<int>(frame.surface.h, 200);
- for (int row = 0; row < h; row++)
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)frame.surface.getBasePtr(0, row), w);
- }
+ if (_picsArchive.getPicture(0x43, frame))
+ scratch.simpleBlitFrom(frame.surface);
const int copyW = MIN<int>(mapW - scrollX, kMapWinW);
const int copyH = MIN<int>(mapH - scrollY, kMapWinH);
Commit: 0ccaaeb7c40435f712a55d22b502c6e3c52fae1e
https://github.com/scummvm/scummvm/commit/0ccaaeb7c40435f712a55d22b502c6e3c52fae1e
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:51+02:00
Commit Message:
EEM: initial support for floppy version
Changed paths:
engines/eem/audio.cpp
engines/eem/clues.cpp
engines/eem/detection.cpp
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/music.cpp
engines/eem/music.h
engines/eem/mystery.cpp
engines/eem/mystery.h
engines/eem/resource.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/audio.cpp b/engines/eem/audio.cpp
index f4719af0bb1..4c2e2a4bb15 100644
--- a/engines/eem/audio.cpp
+++ b/engines/eem/audio.cpp
@@ -205,9 +205,17 @@ void AudioPlayer::spoolSound(uint num) {
"AudioPlayer: voice disabled, skipping spoolSound(%u)", num);
return;
}
- if (_currentMystery < 0 || num >= _sdxIndex.size()) {
- warning("AudioPlayer: spoolSound(%u) â invalid index (%d, %u)",
- num, _currentMystery, (uint)_sdxIndex.size());
+ if (_currentMystery < 0) {
+ // No SDB/SDX bundle is loaded â floppy install (no `M*.SDB`)
+ // or pre-mystery state. Silently no-op; per-voice VOC playback
+ // for floppy lives elsewhere (TODO).
+ debugC(2, kDebugSound,
+ "AudioPlayer: spoolSound(%u) skipped (no mystery sounds)", num);
+ return;
+ }
+ if (num >= _sdxIndex.size()) {
+ warning("AudioPlayer: spoolSound(%u) â index out of range (%u)",
+ num, (uint)_sdxIndex.size());
return;
}
const SoundEntry &entry = _sdxIndex[num];
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 3b460988ce2..1c893ae553b 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -236,7 +236,21 @@ void EEMEngine::doChoosePartner() {
// (`jen.voc` for Jenny, `jake.voc` for Jake; strings at 29be:0af1 /
// 29be:0af9) and block on `_WaitForVoiceDone`.
if (_audio) {
- _audio->playVoc(Common::Path(_partner == 0 ? "JAKE.VOC" : "JEN.VOC"));
+ // CD: standalone clips `JAKE.VOC` / `JEN.VOC`. Floppy uses
+ // per-partner-and-event voice tables at `2608:0F0E` (Jake) /
+ // `2608:0F76` (Jenny), each 25 entries à FAR ptr to a VOC
+ // filename. After a partner pick, `FUN_19bb_0858 @ 19bb:0858`
+ // calls `FUN_1f4e_0305(0x14)` which loads voice slot 20 from
+ // the table for the chosen partner:
+ // Jake slot 20 â `2608:116B = "m-0113sl.voc"`
+ // Jenny slot 20 â `2608:12AE = "f-0140sl.voc"`
+ Common::String voc;
+ if (isFloppy()) {
+ voc = (_partner == 0) ? "M-0113SL.VOC" : "F-0140SL.VOC";
+ } else {
+ voc = (_partner == 0) ? "JAKE.VOC" : "JEN.VOC";
+ }
+ _audio->playVoc(Common::Path(voc));
_audio->waitForVoiceDone();
}
}
@@ -266,11 +280,28 @@ void EEMEngine::doInitClues() {
if (!ib)
return;
- const uint16 startSite = READ_LE_UINT16(ib + 2);
- if (startSite < Mystery::kVisitedSiteCap)
- _mystery._onSites[startSite] = 1;
- _mystery._siteNumber = startSite;
- _mystery._lastSite = startSite;
+ // CD InitBlock starts with `u16 caseType; u16 startSite; <clue block>`.
+ // Floppy InitBlock starts with `u8 caseType; u8 nSubjects; subjects[];
+ // u8 nDialog; dialog_records[]` â no embedded `startSite`. Verified in
+ // `FUN_19bb_042f` (floppy briefing) where `cVar1 = *(buffer +
+ // initOffset)` reads caseType as a single byte and the dialog loop
+ // uses `local_e = byte[initOffset + 1 + nSubjects + 1]`.
+ const bool floppy = isFloppy();
+ const uint16 caseType = floppy ? (uint16)ib[0] : READ_LE_UINT16(ib);
+
+ if (!floppy) {
+ const uint16 startSite = READ_LE_UINT16(ib + 2);
+ if (startSite < Mystery::kVisitedSiteCap)
+ _mystery._onSites[startSite] = 1;
+ _mystery._siteNumber = startSite;
+ _mystery._lastSite = startSite;
+ } else {
+ // Floppy doesn't store a startSite in the InitBlock; default to 0
+ // so the first BigMap entry is reachable.
+ _mystery._onSites[0] = 1;
+ _mystery._siteNumber = 0;
+ _mystery._lastSite = 0;
+ }
setSitePalette(0x22);
Picture bg;
@@ -283,7 +314,6 @@ void EEMEngine::doInitClues() {
const bool haveGame = _aniArchive.loadAnimation(gameAni, game) && !game.empty();
const bool haveBook = _aniArchive.loadAnimation(bookAni, book) && !book.empty();
- const uint16 caseType = READ_LE_UINT16(ib);
const bool haveNancy = (caseType == 1)
&& _aniArchive.loadAnimation(0x19, nancy)
&& !nancy.empty();
@@ -476,12 +506,24 @@ void EEMEngine::doInitClues() {
// the gate is `iVar1 == 2 && _VoiceAvailable`. Other case types open
// straight into the briefing dialogue without it.
if (caseType == 2 && _audio) {
- _audio->playVoc(Common::Path("PHONE.VOC"));
+ // Floppy ships `PHONESL.VOC` instead of CD's `PHONE.VOC` (the
+ // `_LoadSoundName` call site at 2608:1107-110c hands the
+ // floppy filename to the same loader).
+ _audio->playVoc(Common::Path(isFloppy() ? "PHONESL.VOC" : "PHONE.VOC"));
_audio->waitForVoiceDone();
}
- // Step 6 â case briefing dialogue.
- displayClue(ib + 4);
+ // Step 6 â case briefing dialogue. CD InitBlock has the clue block
+ // at +4 (after `u16 caseType; u16 startSite`); floppy uses an
+ // entirely different format â `u8 caseType; u8 nSubjects;
+ // subjects[nSubjects]; u8 nDialog; dialog_records[nDialog]` â
+ // dispatched via `FUN_22dc_05c8 @ 22dc:05c8` per record (a
+ // different on-screen format from the CD clue blocks we render
+ // here). Skip the briefing display on floppy until the dialog-record
+ // renderer is ported; the briefing animations + composited final
+ // frames remain on screen until the user clicks past the BigMap.
+ if (!floppy)
+ displayClue(ib + 4);
}
/// Mirror `_ParseString` @ 1b66:07c3 â substitute the control bytes that
diff --git a/engines/eem/detection.cpp b/engines/eem/detection.cpp
index 27371f0c29e..402a5a89cef 100644
--- a/engines/eem/detection.cpp
+++ b/engines/eem/detection.cpp
@@ -42,6 +42,16 @@ const ADGameDescription gameDescriptions[] = {
ADGF_NO_FLAGS,
GUIO1(GUIO_NONE)
},
+ {
+ "eem",
+ "Floppy",
+ AD_ENTRY2s("EEM.EXE", "692a5e6e7f4516d6e40c1f80cbc1b2cc", 109542,
+ "PICS.DBD", "26b97e8586f798ea90440e88d3d527cd", 959160),
+ Common::EN_ANY,
+ Common::kPlatformDOS,
+ ADGF_NO_FLAGS,
+ GUIO1(GUIO_NONE)
+ },
AD_TABLE_END_MARKER
};
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 8386f08be13..7ecc68da136 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -96,6 +96,13 @@ EEMEngine::EEMEngine(OSystem *syst, const ADGameDescription *gameDesc)
: Engine(syst), _gameDescription(gameDesc), _rng("eem"),
_playerName("Detective"),
_lastScreen(kScreenInvalid), _nextScreen(kScreenTitle), _partner(0) {
+ // `ADGameDescription::extra` is set by the matching entry in
+ // `gameDescriptions[]` ("CD" or "Floppy"). Keep variant detection
+ // purely string-based so a future re-release with a different
+ // `extra` tag falls back to CD-style asset paths.
+ _variant = (gameDesc && gameDesc->extra &&
+ Common::String(gameDesc->extra).contains("Floppy"))
+ ? kVariantFloppy : kVariantCD;
}
EEMEngine::~EEMEngine() {
@@ -119,7 +126,7 @@ Common::Error EEMEngine::run() {
// MIDI music player. Mirrors `_InitMIDI @ 20a2:013a`. Constructed
// here (after `initGraphics` so the OSystem's timer/mixer is up).
- _music = new MusicPlayer();
+ _music = new MusicPlayer(isFloppy());
// Digital audio (VOC + spool). Mirrors `_InitDrivers @ 1ff1:0368`
// which `_AIL_register_driver`s SBDIG.ADV / PASDIG.ADV alongside
@@ -203,50 +210,70 @@ Common::Error EEMEngine::run() {
// - `_StopMIDI()` runs on keypress at the title screen
// (2520:094c).
_skipIntro = false;
- showEAKidsLogo();
- if (!shouldQuit() && !_skipIntro)
- showHighScoreLogo();
- // Storm Software logo: voice + animation. The original at
- // `_ShowStormLogo @ 2520:0707` calls `_LoadSoundName("thunder.voc")`
- // (29be:177d) and passes the buffer to `OpenDifferenceAnimation_Sound`
- // so the thunder roar plays alongside the lightning bolt.
- if (!shouldQuit() && !_skipIntro) {
- if (_audio)
- _audio->playVoc(Common::Path("THUNDER.VOC"));
- playAnm(Common::Path("BOLT.ANM"));
- if (_audio)
- _audio->stopVoice();
- }
- // `_InitMysterySounds(0x3c)` at 2520:086a â load M60.SDX/SDB so
- // `_SpoolSound(uVar3 - 1)` between the ANIM01..ANIM20 anims has
- // data to draw from.
- if (!shouldQuit() && !_skipIntro && _audio)
- _audio->initMysterySounds(60);
- // Theme begins HERE â after the three silent logos, before the
- // character-intro reel.
- if (!shouldQuit() && !_skipIntro && _music)
- _music->playFile(Common::Path("THEME.XMI"), /*loop=*/true);
- for (int i = 1; i <= 20 && !shouldQuit() && !_skipIntro; i++) {
- Common::String name = Common::String::format("ANIM%02d.A", i);
- playAnm(Common::Path(name));
- // `_SpoolSound(uVar3 - 1)` at 2520:08c2 â the per-character VO
- // plays AFTER each anim except the last (`if (uVar3 != 0x14)`
- // at 2520:08a8). Original blocks until done; we run async and
- // wait so the next anim doesn't start before the line ends.
- if (!shouldQuit() && !_skipIntro && i != 20 && _audio) {
- _audio->spoolSound((uint)(i - 1));
- _audio->waitForSpoolDone();
+ if (isFloppy()) {
+ // Floppy opening â strings verified via Ghidra of `EEM.EXE`
+ // floppy at `2608:1513` ("movie.anm"), `2608:14F2` ("title.anm"),
+ // `2608:151D` ("theme.xmi"). The floppy ships only those three
+ // intro assets (no `BOLT.ANM`, no `ANIM01..20.A` reel, no
+ // `THUNDER.VOC`). Order: `MOVIE.ANM` plays as the intro
+ // cinematic with theme music, then `TITLE.ANM` holds the title
+ // screen until the player clicks.
+ if (!shouldQuit() && !_skipIntro && _music)
+ _music->playFile(Common::Path("THEME.XMI"), /*loop=*/true);
+ if (!shouldQuit() && !_skipIntro)
+ playAnm(Common::Path("MOVIE.ANM"), 120,
+ /*holdLastFrame=*/false);
+ if (!shouldQuit() && !_skipIntro)
+ playAnm(Common::Path("TITLE.ANM"), 120,
+ /*holdLastFrame=*/true);
+ } else {
+ showEAKidsLogo();
+ if (!shouldQuit() && !_skipIntro)
+ showHighScoreLogo();
+ // Storm Software logo: voice + animation. The original at
+ // `_ShowStormLogo @ 2520:0707` calls `_LoadSoundName(
+ // "thunder.voc")` (29be:177d) and passes the buffer to
+ // `OpenDifferenceAnimation_Sound` so the thunder roar plays
+ // alongside the lightning bolt.
+ if (!shouldQuit() && !_skipIntro) {
+ if (_audio)
+ _audio->playVoc(Common::Path("THUNDER.VOC"));
+ playAnm(Common::Path("BOLT.ANM"));
+ if (_audio)
+ _audio->stopVoice();
}
+ // `_InitMysterySounds(0x3c)` at 2520:086a â load M60.SDX/SDB
+ // so `_SpoolSound(uVar3 - 1)` between the ANIM01..ANIM20 anims
+ // has data to draw from.
+ if (!shouldQuit() && !_skipIntro && _audio)
+ _audio->initMysterySounds(60);
+ // Theme begins HERE â after the three silent logos, before
+ // the character-intro reel.
+ if (!shouldQuit() && !_skipIntro && _music)
+ _music->playFile(Common::Path("THEME.XMI"), /*loop=*/true);
+ for (int i = 1; i <= 20 && !shouldQuit() && !_skipIntro; i++) {
+ Common::String name = Common::String::format("ANIM%02d.A", i);
+ playAnm(Common::Path(name));
+ // `_SpoolSound(uVar3 - 1)` at 2520:08c2 â per-character
+ // VO after each anim except the last (`if (uVar3 != 0x14)`
+ // at 2520:08a8). Original blocks until done; we run async
+ // and wait so the next anim doesn't start prematurely.
+ if (!shouldQuit() && !_skipIntro && i != 20 && _audio) {
+ _audio->spoolSound((uint)(i - 1));
+ _audio->waitForSpoolDone();
+ }
+ }
+ // `_CleanMysterySounds` at 2520:0903 â release M60 before the
+ // title.
+ if (_audio)
+ _audio->cleanMysterySounds();
+ // Restart the theme for TITLE.ANM â matches the second
+ // `_MIDIPlayFile("theme.xmi")` call at 2520:0918.
+ if (!shouldQuit() && !_skipIntro && _music)
+ _music->playFile(Common::Path("THEME.XMI"), /*loop=*/true);
+ if (!shouldQuit() && !_skipIntro)
+ playAnm(Common::Path("TITLE.ANM"), 120, /*holdLastFrame=*/true);
}
- // `_CleanMysterySounds` at 2520:0903 â release M60 before the title.
- if (_audio)
- _audio->cleanMysterySounds();
- // Restart the theme for TITLE.ANM â matches the second
- // `_MIDIPlayFile("theme.xmi")` call at 2520:0918.
- if (!shouldQuit() && !_skipIntro && _music)
- _music->playFile(Common::Path("THEME.XMI"), /*loop=*/true);
- if (!shouldQuit() && !_skipIntro)
- playAnm(Common::Path("TITLE.ANM"), 120, /*holdLastFrame=*/true);
_skipIntro = false;
// After the title chain, the original goes Title (B) -> screen 8
@@ -814,8 +841,12 @@ Common::Error EEMEngine::loadGameStream(Common::SeekableReadStream *stream) {
}
// `_ReadMystery @ 2404:008f` calls `_InitMysterySounds` at the
// tail (2404:0298) so the SDB index is in place for clue and
- // partner-speech spool sounds.
- if (_audio)
+ // partner-speech spool sounds. Floppy ships individual
+ // `M-XXXX.VOC` files instead of the bundled SDB / SDX archive,
+ // so skip the init there to avoid spamming "missing" warnings;
+ // `spoolSound` then silently no-ops via the `_currentMystery <
+ // 0` guard until the per-voice VOC mapping is wired up.
+ if (_audio && !isFloppy())
_audio->initMysterySounds(mysteryNum);
_mystery.syncState(s);
} else {
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 875d62f608c..1e54e852c45 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -101,6 +101,18 @@ enum ScreenId {
kScreenAction = 0x0C
};
+/// Distribution variant. Selected at engine init from the
+/// `ADGameDescription::extra` field set by `gameDescriptions[]` in
+/// `detection.cpp`. Used to gate filename selection (TRAVEL-N.XMI
+/// vs MUS%05u.XMI, FANFARE2.XMI vs MUS00005.XMI, PHONESL.VOC vs
+/// PHONE.VOC), opening-anim flow (MOVIE.ANM vs ANIM01..20.A) and
+/// per-variant sound effects (DING.VOC / NEWSCAN.VOC only ship with
+/// the floppy release).
+enum Variant {
+ kVariantCD = 0,
+ kVariantFloppy = 1,
+};
+
class EEMEngine : public Engine {
public:
EEMEngine(OSystem *syst, const ADGameDescription *gameDesc);
@@ -110,6 +122,8 @@ public:
const char *getGameId() const;
Common::Platform getPlatform() const;
+ Variant getVariant() const { return _variant; }
+ bool isFloppy() const { return _variant == kVariantFloppy; }
bool hasFeature(EngineFeature f) const override;
bool canLoadGameStateCurrently(Common::U32String *msg = nullptr) override;
@@ -156,6 +170,7 @@ public:
SaveStateList listProfiles() const;
const ADGameDescription *_gameDescription;
+ Variant _variant = kVariantCD;
DBDArchive &getPics() { return _picsArchive; }
DBDArchive &getAni() { return _aniArchive; }
diff --git a/engines/eem/music.cpp b/engines/eem/music.cpp
index a1a40adb60e..80842663a09 100644
--- a/engines/eem/music.cpp
+++ b/engines/eem/music.cpp
@@ -31,7 +31,7 @@
namespace EEM {
-MusicPlayer::MusicPlayer() {
+MusicPlayer::MusicPlayer(bool isFloppy) : _isFloppy(isFloppy) {
// Mirrors `_InitMIDI @ 20a2:013a` which used `_AIL_register_driver`
// to walk the .ADV files (ADLIB.ADV, SBFM.ADV, MT32MPU.ADV, etc.)
// and pick a backend. We honour the launcher's "Music driver"
@@ -166,7 +166,31 @@ void MusicPlayer::playFile(const Common::Path &xmiPath, bool loop) {
}
void MusicPlayer::playMus(uint num, bool loop) {
- // Format string verified at `29be:1525` ("mus%05d.xmi").
+ // CD format string verified at `29be:1525` ("mus%05d.xmi").
+ // Floppy maps the same numeric slots to its own filenames:
+ // 0..4 â travel music. The floppy table at 2608:1399-13cd holds
+ // 5 entries (Travel-6, Travel-4, Travel-7, Travel-1,
+ // Travel-8) used by `_StartTravelMusic` via
+ // `siteNumber % 5`.
+ // 5 â FANFARE2.XMI (winner). String at 2608:0c64.
+ // 6 â no equivalent in floppy install (the loser sting in
+ // `_DisplayAlibi` is CD-only); skip.
+ if (_isFloppy) {
+ static const char *const kTravelTracks[5] = {
+ "Travel-6.XMI", "Travel-4.XMI", "Travel-7.XMI",
+ "Travel-1.XMI", "Travel-8.XMI",
+ };
+ Common::String name;
+ if (num < 5) {
+ name = kTravelTracks[num];
+ } else if (num == 5) {
+ name = "FANFARE2.XMI";
+ } else {
+ return; // num == 6 (loser sting): not present on floppy
+ }
+ playFile(Common::Path(name), loop);
+ return;
+ }
const Common::String name = Common::String::format("MUS%05u.XMI", num);
playFile(Common::Path(name), loop);
}
diff --git a/engines/eem/music.h b/engines/eem/music.h
index f02830cf5cf..50dedeb9eb7 100644
--- a/engines/eem/music.h
+++ b/engines/eem/music.h
@@ -65,7 +65,7 @@ namespace EEM {
*/
class MusicPlayer : public Audio::MidiPlayer {
public:
- MusicPlayer();
+ explicit MusicPlayer(bool isFloppy = false);
/// Mirrors `_MIDIPlayFile @ 20a2:024c`. Reads the .XMI from the game
/// directory and starts playing. `loop=true` mirrors the
@@ -73,7 +73,8 @@ public:
void playFile(const Common::Path &xmiPath, bool loop = false);
/// Mirrors `_MIDIPlay(num) @ 20a2:047d`. Composes the filename
- /// "MUS%05u.XMI" and plays it. Used by `_StartTravelMusic`,
+ /// "MUS%05u.XMI" (CD) or maps to TRAVEL-N.XMI / FANFARE2.XMI
+ /// (floppy) and plays it. Used by `_StartTravelMusic`,
/// `_DisplayCorrect` (winner), `_DisplayAlibi` (loser).
void playMus(uint num, bool loop = false);
@@ -87,6 +88,7 @@ public:
private:
bool _milesAudioMode = false;
+ const bool _isFloppy;
Common::Array<byte> _xmiData;
};
diff --git a/engines/eem/mystery.cpp b/engines/eem/mystery.cpp
index 6b6bfbfc56f..c94b7aa9180 100644
--- a/engines/eem/mystery.cpp
+++ b/engines/eem/mystery.cpp
@@ -44,6 +44,10 @@ void Mystery::clear() {
_solvedOffset = _hintOffset = 0;
_numSites = 0;
_numSuspects = _numCONSITEs = _numCOFFSITEs = 0;
+ _isFloppy = false;
+ _floppySuspectsOff = _floppyHintBlockOff = _floppyNoteIndexOff = 0;
+ _floppyGalleryOff = _floppyTextOff = _floppyKDTextOff = 0;
+ _floppySolvedOff = 0;
memset(_aChain, 0, sizeof(_aChain));
memset(_bChain, 0, sizeof(_bChain));
memset(_cChain, 0, sizeof(_cChain));
@@ -86,6 +90,118 @@ bool Mystery::load(uint num, Common::RandomSource *rng) {
_data = staging;
_number = num;
+ // Floppy `M*.BIN` uses a completely different header layout from
+ // the CD release. Verified via Ghidra of `EEM.EXE` floppy:
+ //
+ // `_ReadMystery_Floppy @ 22dc:0178` parses M0..M54 into pointers
+ // held in a global table at 28da:3c87+. The header offsets are:
+ //
+ // header[+0..+1] ???
+ // header[+2..+3] ???
+ // header[+4..+5] pointer â SUSPECTS section
+ // (count byte, then 0xb-byte entries; entry[+4] =
+ // pic ID, entry[+10] = recolor flag)
+ // (`_FloppySuspectsPtr` @ 28da:3c8b)
+ // header[+6..+7] pointer â ??? (28da:3c9f)
+ // header[+8..+9] pointer â NOTES section
+ // (7-byte entries indexed by clue ID; used by
+ // floppy `_DrawNotes` @ 15e0:01e8)
+ // (`DAT_28da_3c9b`)
+ // header[+0xa..+b] pointer â GALLERY-PORTRAITS section
+ // (count byte, then variable-length entries
+ // `5 + name_len` bytes; entry[+0..+1] = u16
+ // picID, entry[+4] = name length)
+ // (`_FloppyGalleryPtr` @ 28da:3c87, count =
+ // `_FloppyNumSuspects` @ 28da:004b)
+ // header[+0xc..+d] pointer â TEXT block (alibi text base; used in
+ // floppy `_DisplayAlibi` @ 1d40:00df)
+ // header[+0x10..1] pointer â KDTextIndex (`_FloppyKDTextIndexPtr`
+ // @ 28da:3c93)
+ // header[+0x12..3] pointer â ??? (28da:3c8f)
+ //
+ // There is NO fixed-offset numSites / numSuspects / numCONSITEs
+ // field â counts are stored as the FIRST byte of each section.
+ // The CD release refactored this into a flat header at fixed
+ // offsets; our `Mystery::load` here parses the CD layout.
+ //
+ // Detect the variant from the first u16: CD M0 starts with `0x003e`
+ // (initOffset = 62), floppy M0 starts with `0x2286` (a section
+ // pointer near end-of-file). When the first u16 is too high to be
+ // a CD `_initOffset`, parse as floppy.
+ if (readU16(0) > 0x100) {
+ _isFloppy = true;
+ // Section-pointer header verified via Ghidra of floppy
+ // `_ReadMystery_Floppy @ 22dc:0178`,
+ // `_DoSiteLoop_Floppy @ 1652:03a3`,
+ // and `FUN_1fed_07ed` (BigMap site iteration):
+ //
+ // header[+4] â SITES section
+ // count byte + 0xb-byte entries; entry[+4] =
+ // pic ID for BigMap marker, [+6..7] = u16 X,
+ // [+8..9] = u16 Y, [+10] = recolor flag.
+ // header[+6] â SITE INDEX (array of u16 offsets to per-site
+ // data structs; site[+0]=picOff,
+ // site[+2]=clueBlockOff, site[+8]=speakerInfo)
+ // header[+8] â NOTES (7-byte entries / clue ID)
+ // header[+0xa] â SUSPECTS / GALLERY portraits
+ // header[+0xc] â TEXT block base
+ // header[+0x10] â KDTextIndex
+ // header[+0x12] â SOLVED CLUE CHAIN
+ _floppySuspectsOff = readU16(0x04); // SITES
+ _floppyHintBlockOff = readU16(0x06); // SITE INDEX
+ _floppyNoteIndexOff = readU16(0x08);
+ _floppyGalleryOff = readU16(0x0a); // SUSPECTS
+ _floppyTextOff = readU16(0x0c);
+ _floppyKDTextOff = readU16(0x10);
+ _floppySolvedOff = readU16(0x12);
+
+ // header[+0] (the first u16) holds the InitBlock byte offset on
+ // floppy too â verified at `FUN_19bb_042f` where `*DAT_28da_3ca5`
+ // (deref'd as int *) reads the first u16 of the buffer and uses
+ // it as `cVar1 = *(buffer + initOffset)` (caseType byte).
+ _initOffset = readU16(0x00);
+
+ // Counts: first byte of each section. Verified at
+ // `FUN_1fed_07ed` (`uVar3 = *_FloppySuspectsPtr` then iterates)
+ // and `FUN_154e_0045` (`DAT_28da_004b = *DAT_28da_3c87`).
+ const byte *sitesSec = (_floppySuspectsOff < _data.size())
+ ? _data.data() + _floppySuspectsOff : nullptr;
+ const byte *susSec = (_floppyGalleryOff < _data.size())
+ ? _data.data() + _floppyGalleryOff : nullptr;
+ _numSites = sitesSec ? *sitesSec : 0;
+ _numSuspects = susSec ? *susSec : 0;
+ _numCONSITEs = 0;
+ _numCOFFSITEs = 0;
+
+ // Point CD-shaped accessor offsets at the floppy equivalents
+ // so existing accessors return the right base for floppy:
+ // siteIndexEntry() â floppy site index (header[+6])
+ // noteIndex() â floppy notes (header[+8])
+ // galleryData() â floppy suspects (header[+0xa])
+ // textAt() â floppy text block (header[+0xc])
+ // kdTextIndex() â floppy KDTextIndex (header[+0x10])
+ // solvedClueBlock() â floppy solved chain (header[+0x12])
+ // Per-section LAYOUTS still differ from CD, so consumers
+ // walking entries need `isFloppy()` branches.
+ _siteIndexOffset = _floppyHintBlockOff;
+ _noteOffset = _floppyNoteIndexOff;
+ _galleryOffset = _floppyGalleryOff;
+ _textOffset = _floppyTextOff;
+ _kdTextOffset = _floppyKDTextOff;
+ _solvedOffset = _floppySolvedOff;
+ _hintOffset = _floppyHintBlockOff;
+
+ debugC(1, kDebugMystery,
+ "Mystery::load(%u) floppy: sites=0x%04x siteIdx=0x%04x "
+ "notes=0x%04x suspects=0x%04x text=0x%04x kd=0x%04x "
+ "solved=0x%04x numSites=%u numSuspects=%u",
+ num, _floppySuspectsOff, _floppyHintBlockOff,
+ _floppyNoteIndexOff, _floppyGalleryOff, _floppyTextOff,
+ _floppyKDTextOff, _floppySolvedOff,
+ _numSites, _numSuspects);
+ return true;
+ }
+
// Header is 16-bit-word indexed (matches `int *piVar1 = __Mystery; piVar1[N]`).
_initOffset = readU16(0 * 2);
_mapOffset = readU16(2 * 2);
@@ -102,6 +218,16 @@ bool Mystery::load(uint num, Common::RandomSource *rng) {
_numCONSITEs = (uint8)readU16(14 * 2);
_numCOFFSITEs = (uint8)readU16(15 * 2);
+ // Defensive clamp. The floppy mystery file format uses a different
+ // header layout (verified by comparing M0.BIN: CD has `numSites =
+ // readU16(0x14) = 3`; floppy has `readU16(0x14) = 0x1925`,
+ // obviously not a site count). Without a clamp, downstream loops
+ // over `_onSites` / `_visitedSite` (capacity 20) blow past the
+ // array end. Until the floppy format is fully supported, cap at
+ // the array capacity so the engine fails gracefully.
+ if (_numSites > kVisitedSiteCap)
+ _numSites = kVisitedSiteCap;
+
for (uint i = 0; i < kChainLen; i++) {
_aChain[i] = readU16((16 + i) * 2);
_bChain[i] = readU16((21 + i) * 2);
@@ -218,6 +344,18 @@ const byte *Mystery::kdTextIndex() const {
const byte *Mystery::mapEntry(uint siteNum) const {
if (!isLoaded() || siteNum >= _numSites)
return nullptr;
+ if (_isFloppy) {
+ // Floppy SITES section: byte[0] = count, then 11-byte entries.
+ // Verified at `FUN_1fed_07ed` (BigMap site iteration) where
+ // `pcVar2 = _FloppySuspectsPtr` (header[+4]) and the loop reads
+ // `*(int *)(pcVar2 + i*0xb + 7)` (X) and `*(int *)(pcVar2 + i*0xb
+ // + 9)` (Y) â the +7/+9 offsets are 1-based because pcVar2[0]
+ // holds the count, so entry stride 11 starts at byte 1.
+ const uint off = _floppySuspectsOff + 1 + siteNum * 11;
+ if (off + 11 > _data.size())
+ return nullptr;
+ return _data.data() + off;
+ }
const uint off = _mapOffset + siteNum * 14;
if (off + 14 > _data.size())
return nullptr;
diff --git a/engines/eem/mystery.h b/engines/eem/mystery.h
index dadaec00a75..4b23fa4ea27 100644
--- a/engines/eem/mystery.h
+++ b/engines/eem/mystery.h
@@ -209,6 +209,20 @@ private:
uint16 _bChain[kChainLen] = {};
uint16 _cChain[kChainLen] = {};
+ // Floppy variant uses a completely different header (see comment
+ // in `Mystery::load`). When `_isFloppy` is true, the CD-shaped
+ // `_initOffset / _siteIndexOffset / etc.` fields are unset and the
+ // floppy section pointers below are populated from the floppy
+ // header offsets verified at `_ReadMystery_Floppy @ 22dc:0178`.
+ bool _isFloppy = false;
+ uint16 _floppySuspectsOff = 0; ///< header[+4] â suspects
+ uint16 _floppyHintBlockOff = 0; ///< header[+6] â hintâclue table
+ uint16 _floppyNoteIndexOff = 0; ///< header[+8] â notes (7B/clue)
+ uint16 _floppyGalleryOff = 0; ///< header[+0xa] â gallery portraits
+ uint16 _floppyTextOff = 0; ///< header[+0xc] â text block
+ uint16 _floppyKDTextOff = 0; ///< header[+0x10] â KDTextIndex
+ uint16 _floppySolvedOff = 0; ///< header[+0x12] â solved clue chain
+
uint16 readU16(uint offset) const;
};
diff --git a/engines/eem/resource.cpp b/engines/eem/resource.cpp
index 91886e837fb..a08ce19f85f 100644
--- a/engines/eem/resource.cpp
+++ b/engines/eem/resource.cpp
@@ -115,7 +115,15 @@ bool readFrame(Common::SeekableReadStream &stream, bool compressed, Picture &out
bool DBDArchive::loadEntry(uint num, Picture &out) {
if (num >= _index.size()) {
- warning("DBDArchive::loadEntry: %u out of range (max %u)", num, _index.size());
+ // Out-of-range picture IDs are non-fatal â every caller already
+ // checks the return value (e.g., `haveDone`/`haveCrime` in
+ // `drawBigMapOverview`). The floppy's PICS.DBD ships fewer
+ // entries than the CD (e.g., the BigMap done-marker `0x20D` is
+ // CD-only), so this fires routinely on floppy and shouldn't
+ // be a `warning`.
+ debugC(2, kDebugGfx,
+ "DBDArchive::loadEntry: %u out of range (max %u)",
+ num, (uint)_index.size());
return false;
}
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 86dbe5af528..460217ce9d0 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -1462,7 +1462,7 @@ void EEMEngine::doCaseSelection() {
if (!_mystery.load(0, &_rng)) {
warning("doCaseSelection: failed to load practice mystery");
_mystery.clear();
- } else if (_audio) {
+ } else if (_audio && !isFloppy()) {
_audio->initMysterySounds(0);
}
return;
@@ -1685,7 +1685,7 @@ void EEMEngine::doCaseSelection() {
_mystery.clear();
return;
}
- if (_audio)
+ if (_audio && !isFloppy())
_audio->initMysterySounds(mn);
debugC(1, kDebugMystery, "Mystery %u loaded; %u sites, %u suspects",
mn, _mystery.numSites(), _mystery.numSuspects());
@@ -2699,8 +2699,11 @@ void EEMEngine::doBigMap() {
ev.mouse.y < kMapWinY + kMapWinH) {
// Hit-test the per-site button at its actual bbox
// (`_StampButtons` records the rect at SmallMap +8/+0xa
- // with the button PIC's width/height).
- for (uint i = 0; i < _mystery.numSites(); i++) {
+ // with the button PIC's width/height). Floppy entries
+ // have a different shape so skip the SmallMap hit-test
+ // (we still get clicks via the BigMap overview path).
+ const bool fmap = _mystery.isLoaded() && isFloppy();
+ for (uint i = 0; !fmap && i < _mystery.numSites(); i++) {
if (!_mystery._onSites[i] &&
i != _mystery._siteNumber)
continue;
@@ -2785,9 +2788,20 @@ void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
const byte *entry = _mystery.mapEntry(i);
if (!entry)
continue;
- const uint16 mx = READ_LE_UINT16(entry + 0x4);
- const uint16 my = READ_LE_UINT16(entry + 0x6);
- const uint16 crime = READ_LE_UINT16(entry + 0xc);
+ // CD entries are 14 bytes: X at +4, Y at +6, crime at +12.
+ // Floppy entries are 11 bytes: X at +6, Y at +8, recolor at +10.
+ // Floppy layout verified at `FUN_1fed_07ed` (BigMap iteration):
+ // `*(int *)(pcVar2 + i*0xb + 7)` (= entry+6, X u16)
+ // `*(int *)(pcVar2 + i*0xb + 9)` (= entry+8, Y u16)
+ // `pcVar2[i*0xb + 0xb]` (= entry+10, recolor flag â non-zero
+ // selects the crime-marker PIC over the regular site marker).
+ const bool floppy = _mystery.isLoaded() && isFloppy();
+ const uint16 mx = floppy ? READ_LE_UINT16(entry + 0x6)
+ : READ_LE_UINT16(entry + 0x4);
+ const uint16 my = floppy ? READ_LE_UINT16(entry + 0x8)
+ : READ_LE_UINT16(entry + 0x6);
+ const uint16 crime = floppy ? (uint16)entry[0xa]
+ : READ_LE_UINT16(entry + 0xc);
const bool done_ = (i < Mystery::kVisitedSiteCap)
&& _mystery._visitedSite[i];
@@ -2885,7 +2899,12 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
// button = _GetButton(MapData[+0])
// destX = MapData[+8]
// destY = MapData[+0xa]
- for (uint i = 0; i < _mystery.numSites(); i++) {
+ // Floppy mystery 0 ships no SmallMap detail layer, but mapEntry() still
+ // returns the floppy SITES row which has a different shape (no
+ // per-button PIC ID, X/Y at +6/+8) â skip the per-site stamp on floppy
+ // to avoid stamping garbage button IDs from the wrong field offsets.
+ const bool floppyMap = _mystery.isLoaded() && isFloppy();
+ for (uint i = 0; !floppyMap && i < _mystery.numSites(); i++) {
if (!_mystery._onSites[i] && i != _mystery._siteNumber)
continue;
const byte *entry = _mystery.mapEntry(i);
Commit: a55adf77b7cd802486f60a28d8171a6dfd7407aa
https://github.com/scummvm/scummvm/commit/a55adf77b7cd802486f60a28d8171a6dfd7407aa
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:51+02:00
Commit Message:
EEM: implement conversations for the floppy version
Changed paths:
engines/eem/clues.cpp
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/mystery.cpp
engines/eem/site.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 1c893ae553b..e765bff0107 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -296,9 +296,15 @@ void EEMEngine::doInitClues() {
_mystery._siteNumber = startSite;
_mystery._lastSite = startSite;
} else {
- // Floppy doesn't store a startSite in the InitBlock; default to 0
- // so the first BigMap entry is reachable.
- _mystery._onSites[0] = 1;
+ // Floppy InitBlock has no startSite. The floppy BigMap iterator
+ // `FUN_1fed_07ed` walks every site in the SITES section
+ // unconditionally (no `_OnSites` gate) â verified at the floppy
+ // `_DoMapScreen @ 1fed:1060` which stamps every site marker
+ // before the interaction loop. Mark every loaded site as visible
+ // so our `_onSites`-gated overview stamps the same set.
+ const uint sites = _mystery.numSites();
+ for (uint s = 0; s < sites && s < Mystery::kVisitedSiteCap; s++)
+ _mystery._onSites[s] = 1;
_mystery._siteNumber = 0;
_mystery._lastSite = 0;
}
@@ -514,15 +520,14 @@ void EEMEngine::doInitClues() {
}
// Step 6 â case briefing dialogue. CD InitBlock has the clue block
- // at +4 (after `u16 caseType; u16 startSite`); floppy uses an
- // entirely different format â `u8 caseType; u8 nSubjects;
- // subjects[nSubjects]; u8 nDialog; dialog_records[nDialog]` â
- // dispatched via `FUN_22dc_05c8 @ 22dc:05c8` per record (a
- // different on-screen format from the CD clue blocks we render
- // here). Skip the briefing display on floppy until the dialog-record
- // renderer is ported; the briefing animations + composited final
- // frames remain on screen until the user clicks past the BigMap.
- if (!floppy)
+ // at +4 (after `u16 caseType; u16 startSite`); floppy uses
+ // `u8 caseType; u8 nSubjects; subjects[nSubjects]; u8 nDialog;
+ // dialog_records[nDialog]` (each record `11 + textCount` bytes),
+ // dispatched via `FUN_22dc_05c8 @ 22dc:05c8`. We render dialog
+ // records ourselves on floppy.
+ if (floppy)
+ displayFloppyBriefing(ib);
+ else
displayClue(ib + 4);
}
@@ -891,6 +896,193 @@ void EEMEngine::displayClue(const byte *clueBlock) {
}
}
+void EEMEngine::displayFloppyBriefing(const byte *initBlock) {
+ // Floppy briefing â mirrors the dialog loop at the tail of
+ // `FUN_19bb_042f @ 19bb:042f`:
+ // nSubjects = ib[1]; nDialog = ib[2 + nSubjects];
+ // records = ib + 3 + nSubjects;
+ // Each record is dispatched through `FUN_22dc_05c8 @ 22dc:05c8`,
+ // which reads:
+ // u16 picID @ +0 (character portrait, 0 = skip pic)
+ // u16 picX @ +2
+ // u8 picY @ +4
+ // u8 balloon @ +5 (low 7 bits = balloon idx, +0x80 = mirror)
+ // u16 ballX @ +6
+ // u8 ballY @ +8
+ // u8 sound @ +9 (high bit = play voice, low 7 bits = slot)
+ // u8 textCount@ +10
+ // u8 textIdx[]@ +11 (1 byte per â low 7 bits = NOTES idx)
+ // Text offsets in NOTES are ABSOLUTE byte offsets into the mystery
+ // buffer (verified by note 0 of M0.BIN at file offset 0xd0 holding
+ // "Hello, ..., I'm ... Eagle!"), so we read text via
+ // `mystery.blobAt(noteEntry[+2..3])` for Jake / `+4..5` for Jenny.
+ if (!initBlock || !isFloppy() || !_font.isLoaded())
+ return;
+
+ const uint8 nSubjects = initBlock[1];
+ const uint8 nDialog = initBlock[2 + nSubjects];
+ const byte *rec = initBlock + 3 + nSubjects;
+
+ const byte *notes = _mystery.noteIndex();
+ const byte *bufBase = _mystery.blobAt(0);
+ if (!notes || !bufBase)
+ return;
+
+ // Snapshot the current screen so each record restores the
+ // briefing background between bubbles.
+ Graphics::ManagedSurface bg(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ {
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (screen) {
+ for (int row = 0; row < 200; row++) {
+ memcpy((byte *)bg.getBasePtr(0, row),
+ (const byte *)screen->getBasePtr(0, row), 320);
+ }
+ g_system->unlockScreen();
+ }
+ }
+
+ for (uint i = 0; i < nDialog && !shouldQuit(); i++) {
+ const uint16 picID = READ_LE_UINT16(rec + 0);
+ const uint16 picX = READ_LE_UINT16(rec + 2);
+ const uint8 picY = rec[4];
+ const uint8 balByte = rec[5];
+ const uint16 ballX = READ_LE_UINT16(rec + 6);
+ const uint8 ballY = rec[8];
+ const uint8 textCount= rec[10];
+
+ // Build the full text by concatenating each note's per-partner
+ // text string (parsed for `%s`-style placeholders).
+ Common::String raw;
+ for (uint t = 0; t < textCount; t++) {
+ const uint8 idx = rec[11 + t] & 0x7f;
+ const uint16 textOff = (_partner == 0)
+ ? READ_LE_UINT16(notes + idx * 7 + 2)
+ : READ_LE_UINT16(notes + idx * 7 + 4);
+ const char *line = (const char *)(bufBase + textOff);
+ if (t > 0)
+ raw += ' ';
+ raw += line;
+ }
+ const Common::String text = parseString(raw, _playerName, _partner);
+
+ // Restore briefing BG before drawing this bubble.
+ g_system->copyRectToScreen(bg.getPixels(), bg.pitch, 0, 0, 320, 200);
+
+ // Optional character portrait.
+ if (picID != 0 && picID != 0xFFFF) {
+ Picture pic;
+ if (_picsArchive.getPicture(picID, pic))
+ blitAt(pic, picX, picY);
+ }
+
+ // Compose balloon + text on a scratch surface.
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ {
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (screen) {
+ for (int row = 0; row < 200; row++) {
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)screen->getBasePtr(0, row), 320);
+ }
+ g_system->unlockScreen();
+ }
+ }
+
+ Picture balloon;
+ const uint16 balloonId = balByte & 0x7F;
+ const bool flipBall = (balByte & 0x80) != 0;
+ const bool haveBalloon = balByte != 0xFF &&
+ _balloonArchive.size() > balloonId &&
+ _balloonArchive.loadEntry(balloonId, balloon);
+ uint16 textWidth = 142;
+ uint16 textXIns = 6;
+ uint16 textYIns = 4;
+ if (haveBalloon) {
+ const int bw = MIN<int>(balloon.surface.w, 320 - ballX);
+ const int bh = MIN<int>(balloon.surface.h, 200 - ballY);
+ const byte transp = (byte)(balloon.flags >> 8);
+ for (int row = 0; row < bh; row++) {
+ const byte *src =
+ (const byte *)balloon.surface.getBasePtr(0, row);
+ byte *dst =
+ (byte *)scratch.getBasePtr(ballX, ballY + row);
+ for (int col = 0; col < bw; col++) {
+ const int srcCol = flipBall
+ ? (balloon.surface.w - 1 - col) : col;
+ const byte px = src[srcCol];
+ if (px != transp)
+ dst[col] = px;
+ }
+ }
+ getBalloonInsets(balloonId, textXIns, textYIns, textWidth);
+ }
+
+ const int textX = ballX + textXIns;
+ const int textY = ballY + textYIns;
+ _font.drawWordWrapped(&scratch, textX, textY,
+ MAX<int>(8, (int)textWidth), text, 0);
+
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+
+ // Drain pending events from the previous bubble (or upstream
+ // animation skip) so a single Enter press doesn't burst-advance
+ // through multiple bubbles. Without this, key-repeat or stacked
+ // KEYUP/KEYDOWN events from one keystroke could roll past
+ // several records before the user could even read them.
+ {
+ Common::Event drain;
+ while (g_system->getEventManager()->pollEvent(drain)) {}
+ }
+ // Minimum visible time per bubble â guards against accidental
+ // rapid-fire advances from accumulated input.
+ const uint32 minVisibleMs = 250;
+ const uint32 startedAt = g_system->getMillis();
+
+ // Wait for click / key. ESC skips the rest of the briefing.
+ // Inside the loop we ignore input until at least `minVisibleMs`
+ // has elapsed since the bubble was drawn â combined with the
+ // pre-loop event drain, this prevents one keystroke (or its
+ // auto-repeat tail) from advancing several bubbles back to back.
+ bool advance = false;
+ bool skipAll = false;
+ while (!advance && !shouldQuit()) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ advance = true;
+ break;
+ }
+ if (g_system->getMillis() - startedAt < minVisibleMs)
+ continue;
+ if (ev.type == Common::EVENT_KEYDOWN &&
+ ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ advance = true;
+ skipAll = true;
+ interruptAudio();
+ break;
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN ||
+ ev.type == Common::EVENT_KEYDOWN) {
+ advance = true;
+ break;
+ }
+ }
+ g_system->updateScreen();
+ g_system->delayMillis(10);
+ }
+ if (skipAll)
+ return;
+
+ rec += 11 + textCount;
+ }
+}
+
bool EEMEngine::areYouSure() {
// Mirrors `_AreYouSure` @ 1a35:0a5c. Original loads PIC 0x136 for the
// dialog body and PIC 0x1FD/0x1FE for YES/NO. We render a minimal
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 7ecc68da136..23a34e6201f 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -53,8 +53,10 @@ const uint kNumSitePals = 40; ///< SITEPALS holds 40 palettes (40 * 768 = 30720
// Picture / palette IDs from the original code (1-based picture IDs).
const uint kPicEAKidsLogo = 0x54; ///< _ShowEAKids: GetPicture(0x54)
const uint kPicHighScoreLogo = 0x20c; ///< _ShowHScoreLogo: GetPicture(0x20c)
+const uint kPicStormLogo = 0x20b; ///< Floppy storm-logo still: PIC 0x20b
const uint kPalEAKids = 0x25;
const uint kPalHighScore = 0x27;
+const uint kPalStormLogo = 0x26; ///< Floppy `FUN_23d2_0605` palette idx
// Save body version, used by the `Common::Serializer` inside
// `saveGameStream`/`loadGameStream`. The framework's extended-save
@@ -211,15 +213,35 @@ Common::Error EEMEngine::run() {
// (2520:094c).
_skipIntro = false;
if (isFloppy()) {
- // Floppy opening â strings verified via Ghidra of `EEM.EXE`
- // floppy at `2608:1513` ("movie.anm"), `2608:14F2` ("title.anm"),
- // `2608:151D` ("theme.xmi"). The floppy ships only those three
- // intro assets (no `BOLT.ANM`, no `ANIM01..20.A` reel, no
- // `THUNDER.VOC`). Order: `MOVIE.ANM` plays as the intro
- // cinematic with theme music, then `TITLE.ANM` holds the title
- // screen until the player clicks.
+ // Floppy opening â driven by `FUN_23d2_039c @ 23d2:039c`:
+ // FUN_23d2_0170() â clear palette
+ // FUN_23d2_004b() â set up timer
+ // FUN_23d2_050c() â show PIC 0x54 (EA Kids logo, palette 0x25)
+ // FUN_23d2_06c6() â show PIC 0x20c (High Score logo, palette 0x27)
+ // FUN_23d2_0605() â show PIC 0x20b (Storm Software, palette
+ // 0x26) AND play voice slot 25 (thunder.voc
+ // â verified via the Jake voice table at
+ // `2608:0f0e` slot 25 â `2608:11ac` =
+ // "thunder.voc").
+ // _MIDIPlayFile("theme.xmi", loop=1)
+ // _PlayANM(idx=0) â CHAT.ANM (filename table at `2608:14fe`
+ // index 0 â "chat.anm" at `2608:150a`).
+ // _PlayANM(idx=1) â MOVIE.ANM (table index 1 â `2608:1513`).
+ // `TITLE.ANM` is shown later by screen-`0xb` handler `@
+ // 19bb:0ebc` once the intro driver returns. The thunder VOC
+ // alongside the storm logo is the "intro voice" the user heard
+ // missing â without it the lightning-bolt logo plays silently.
+ if (!shouldQuit() && !_skipIntro)
+ showEAKidsLogo();
+ if (!shouldQuit() && !_skipIntro)
+ showHighScoreLogo();
+ if (!shouldQuit() && !_skipIntro)
+ showFloppyStormLogo();
if (!shouldQuit() && !_skipIntro && _music)
_music->playFile(Common::Path("THEME.XMI"), /*loop=*/true);
+ if (!shouldQuit() && !_skipIntro)
+ playAnm(Common::Path("CHAT.ANM"), 120,
+ /*holdLastFrame=*/false);
if (!shouldQuit() && !_skipIntro)
playAnm(Common::Path("MOVIE.ANM"), 120,
/*holdLastFrame=*/false);
@@ -678,6 +700,29 @@ void EEMEngine::showHighScoreLogo() {
waitForInput(2500);
}
+void EEMEngine::showFloppyStormLogo() {
+ // Floppy storm-logo splash â `FUN_23d2_0605 @ 23d2:0605`:
+ // GetPicture(0x20b); BlitToVGA;
+ // if (sound) { LoadVOC(slot 25 = "thunder.voc"); PlayVOC(...); }
+ // GetPalette(0x26); FadeIn; wait 50 ticks; FadeOut.
+ // CD plays `BOLT.ANM` at this slot with `THUNDER.VOC` overlaid; the
+ // floppy uses a static still + the same VOC. Without the VOC the
+ // lightning logo plays silently â the user noticed.
+ Picture pic;
+ if (!_picsArchive.getPicture(kPicStormLogo, pic)) {
+ warning("Storm logo (%u) load failed", kPicStormLogo);
+ return;
+ }
+ blitAt(pic, 0, 0);
+ setSitePalette(kPalStormLogo);
+ g_system->updateScreen();
+ if (_audio)
+ _audio->playVoc(Common::Path("THUNDER.VOC"));
+ waitForInput(2500);
+ if (_audio)
+ _audio->stopVoice();
+}
+
void EEMEngine::doSiteLoop() {
// Mirrors the per-mystery site loop. SiteScreen::run() handles
// hotspot clicks plus M (map), N (notebook), G (gallery), A (accuse),
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 1e54e852c45..74046faec8d 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -361,6 +361,7 @@ private:
// Screen handlers â port targets in screens/ later.
void showEAKidsLogo();
void showHighScoreLogo();
+ void showFloppyStormLogo();
/// Profile selector â mirrors `screen8_handler @ 1c33:1012`.
/// Walks `listProfiles()`, draws the list of existing profile
@@ -423,6 +424,21 @@ private:
/// minus the live ANI sequence playback.
void doInitClues();
+ /// Floppy variant of the briefing dialog renderer. Walks the dialog
+ /// record list at the tail of the floppy InitBlock (per
+ /// `FUN_19bb_042f` and `FUN_22dc_05c8 @ 22dc:05c8`) and renders one
+ /// speech balloon per record. Each record is `11 + textCount` bytes:
+ /// +0..1 picID (character portrait, 0 = none)
+ /// +2..3 picX
+ /// +4 picY
+ /// +5 balloonId | (mirror_flag << 7)
+ /// +6..7 balloonX
+ /// +8 balloonY
+ /// +9 soundFlag (high bit) | sound slot (low 7 bits)
+ /// +10 textCount
+ /// +11.. text indices (1 byte each, low 7 bits = NOTES idx)
+ void displayFloppyBriefing(const byte *initBlock);
+
public:
/// Mirrors `_StartTravelMusic @ 20a2:0595`. Picks `MUS%05d.XMI`
/// based on `_mystery._siteNumber % 5` and starts it (looping). The
diff --git a/engines/eem/mystery.cpp b/engines/eem/mystery.cpp
index c94b7aa9180..437a8f62499 100644
--- a/engines/eem/mystery.cpp
+++ b/engines/eem/mystery.cpp
@@ -266,8 +266,12 @@ bool Mystery::load(uint num, Common::RandomSource *rng) {
const byte *Mystery::siteIndexEntry(uint siteNum) const {
if (!isLoaded() || siteNum >= _numSites)
return nullptr;
- const uint off = _siteIndexOffset + siteNum * 6;
- if (off + 6 > _data.size())
+ // Floppy site index uses 2-byte (u16) entries â verified at
+ // `_DoSiteLoop_Floppy @ 1652:03d2` reading `*(int *)
+ // ((int)_FloppySiteIndexPtr + siteNum * 2)`. CD uses 6-byte rows.
+ const uint stride = _isFloppy ? 2 : 6;
+ const uint off = _siteIndexOffset + siteNum * stride;
+ if (off + stride > _data.size())
return nullptr;
return _data.data() + off;
}
@@ -286,6 +290,14 @@ const byte *Mystery::hotspots(uint siteNum) const {
const byte *idx = siteIndexEntry(siteNum);
if (!idx)
return nullptr;
+ // Floppy site index is only 2 bytes per entry; `idx + 4` would
+ // read into the NEXT entry's offset, returning garbage. The
+ // floppy hotspot list lives inside the site-data sub-blob (see
+ // `_DoSiteLoop_Floppy @ 1652:03a3` walking 5-byte drop entries
+ // from `*site_data + 2`); don't fake a CD-style result here â
+ // SiteScreen guards floppy paths separately.
+ if (_isFloppy)
+ return nullptr;
const uint16 hotspotOff = READ_LE_UINT16(idx + 4);
if (hotspotOff >= _data.size())
return nullptr;
@@ -293,6 +305,8 @@ const byte *Mystery::hotspots(uint siteNum) const {
}
uint16 Mystery::hotspotCount(uint siteNum) const {
+ if (_isFloppy)
+ return 0;
const byte *site = siteData(siteNum);
if (!site || (size_t)(site - _data.data()) + 8 > _data.size())
return 0;
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 35adeb52f90..80610200252 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -522,8 +522,25 @@ void SiteScreen::enter(uint siteNum) {
// Palette: original `_BuildBackground` calls `GetPalette(sitenum + 1)`
// where sitenum is the global SITES.DBD index (= the per-mystery
// `sitepic` field), not the per-mystery site index.
+ //
+ // Floppy site_data layout differs (per `_DoSiteLoop_Floppy @
+ // 1652:03f4`): the FIRST u16 is the *offset* to a drops sub-struct
+ // whose byte 0 is the small SITES.DBD picID. Without this branch
+ // we'd dereference the offset directly and call setSitePalette
+ // with a value in the thousands â the user reported `index 9275
+ // out of range` for site 2.
const byte *sd = _mystery->siteData(siteNum);
- const uint16 sitepic = sd ? READ_LE_UINT16(sd) : 0;
+ uint16 sitepic = 0;
+ if (sd) {
+ if (_vm->isFloppy()) {
+ const uint16 dropsOff = READ_LE_UINT16(sd);
+ const byte *drops = _mystery->blobAt(dropsOff);
+ if (drops)
+ sitepic = (uint16)drops[0];
+ } else {
+ sitepic = READ_LE_UINT16(sd);
+ }
+ }
_vm->setSitePaletteForSite(sitepic);
// SITEPALS ships with palette indices 0xF9..0xFE all set to a
@@ -980,6 +997,16 @@ void SiteScreen::scanColorCycles(uint siteNum) {
_colorCycles.clear();
if (!_mystery)
return;
+ // Floppy site data has a different layout (per `_DoSiteLoop_Floppy
+ // @ 1652:03a3`: site index is 2-byte u16 entries, site data starts
+ // with an offset to a sub-structure with picID + 5-byte hotspot
+ // entries â there's no `[+0xa]` anim count at the same place). The
+ // CD-shaped offsets read garbage and run past the buffer end. Until
+ // the floppy color-cycle layout is reverse-engineered, skip the
+ // scan: the floppy still does palette F9..FE rotation in its own
+ // driver, but our hotspot palette override works without it.
+ if (_vm && _vm->isFloppy())
+ return;
const byte *site = _mystery->siteData(siteNum);
if (!site)
return;
@@ -1044,6 +1071,15 @@ void SiteScreen::renderPartner(uint siteNum, uint32 tickMs) {
// `kWaitAnims` lives at file scope above; we cap rendering at
// `speaker < 7` since anything past entry 6 is the `_SiteButtons`
// rect data that follows the table in the binary.
+ // Floppy site data has a different shape: site_data+8 is a u16
+ // OFFSET to a 10-byte speakerInfo struct (verified at
+ // `_DoSiteLoop_Floppy @ 1652:042b` reading `*(undefined2 *)
+ // (DAT_28da_0172) = anim_id_jake` etc.), not an index into the
+ // `kWaitAnims` table the CD uses. Skip the partner render until
+ // the floppy speakerInfo is wired up â without this guard we
+ // dereference garbage entries past the table end.
+ if (_vm && _vm->isFloppy())
+ return;
const byte *site = _mystery->siteData(siteNum);
if (!site)
return;
@@ -1118,8 +1154,25 @@ void SiteScreen::renderBackground(uint siteNum) {
// â the dbi entry stride is 10 bytes, no -1 adjustment). Our
// previous `loadEntry(sitepic - 1)` was off by one, which is why
// the tutorial mystery rendered scenes from neighbouring cases.
+ // Floppy site_data stores the picID one indirection deeper:
+ // `*site_data` (u16) â drops sub-struct, `drops[0]` (byte) is the
+ // SITES.DBD entry. CD uses the u16 directly. Verified at
+ // `FUN_16e2_12fd @ 16e2:12fd` (called as
+ // `FUN_16e2_12fd(*local_12, 0x42, 0x14)` from
+ // `_DoSiteLoop_Floppy`, where `*local_12` is the byte at
+ // drops_struct+0 via the `(undefined1 *)` cast).
const byte *site = _mystery->siteData(siteNum);
- const uint16 sitepic = site ? READ_LE_UINT16(site) : 0;
+ uint16 sitepic = 0;
+ if (site) {
+ if (_vm->isFloppy()) {
+ const uint16 dropsOff = READ_LE_UINT16(site);
+ const byte *drops = _mystery->blobAt(dropsOff);
+ if (drops)
+ sitepic = (uint16)drops[0];
+ } else {
+ sitepic = READ_LE_UINT16(site);
+ }
+ }
Picture scene;
bool haveScene = false;
if (sitepic < _vm->getSites().size())
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 460217ce9d0..7e7e9de005f 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -2699,24 +2699,45 @@ void EEMEngine::doBigMap() {
ev.mouse.y < kMapWinY + kMapWinH) {
// Hit-test the per-site button at its actual bbox
// (`_StampButtons` records the rect at SmallMap +8/+0xa
- // with the button PIC's width/height). Floppy entries
- // have a different shape so skip the SmallMap hit-test
- // (we still get clicks via the BigMap overview path).
+ // with the button PIC's width/height).
const bool fmap = _mystery.isLoaded() && isFloppy();
- for (uint i = 0; !fmap && i < _mystery.numSites(); i++) {
+ for (uint i = 0; i < _mystery.numSites(); i++) {
if (!_mystery._onSites[i] &&
i != _mystery._siteNumber)
continue;
const byte *entry = _mystery.mapEntry(i);
if (!entry)
continue;
- const uint16 buttonId = READ_LE_UINT16(entry + 0x0);
- const uint16 mx = READ_LE_UINT16(entry + 0x8);
- const uint16 my = READ_LE_UINT16(entry + 0xa);
+ uint16 mx;
+ uint16 my;
+ uint16 buttonId = 0;
+ if (fmap) {
+ // Floppy detail view: click rect on
+ // BIGMAP.PIC at (+0, +2), per
+ // `FUN_1fed_0c3e`'s write to
+ // DAT_28da_3aee/DAT_28da_3af0.
+ mx = READ_LE_UINT16(entry + 0x0);
+ my = READ_LE_UINT16(entry + 0x2);
+ } else {
+ buttonId = READ_LE_UINT16(entry + 0x0);
+ mx = READ_LE_UINT16(entry + 0x8);
+ my = READ_LE_UINT16(entry + 0xa);
+ }
Picture button;
int bw = 16;
int bh = 16;
- if (_buttonArchive.loadEntry(buttonId, button)) {
+ if (fmap) {
+ // Floppy uses the global site-marker PIC for
+ // every site; the recolor flag at +10 picks
+ // the crime variant. Sized off whichever PIC
+ // is loaded successfully.
+ Picture m;
+ const uint pic = (entry[0xa] != 0) ? 0xc6 : 0xc5;
+ if (_picsArchive.getPicture(pic, m)) {
+ bw = m.surface.w;
+ bh = m.surface.h;
+ }
+ } else if (_buttonArchive.loadEntry(buttonId, button)) {
bw = button.surface.w;
bh = button.surface.h;
}
@@ -2899,24 +2920,50 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
// button = _GetButton(MapData[+0])
// destX = MapData[+8]
// destY = MapData[+0xa]
- // Floppy mystery 0 ships no SmallMap detail layer, but mapEntry() still
- // returns the floppy SITES row which has a different shape (no
- // per-button PIC ID, X/Y at +6/+8) â skip the per-site stamp on floppy
- // to avoid stamping garbage button IDs from the wrong field offsets.
+ // Floppy SITES rows are 11 bytes â no per-button PIC ID. Use the
+ // regular site / crime marker PICs (0xc5 / 0xc6) keyed off the
+ // recolor flag at +10, same logic as the overview stamp.
const bool floppyMap = _mystery.isLoaded() && isFloppy();
- for (uint i = 0; !floppyMap && i < _mystery.numSites(); i++) {
+ Picture floppySiteM;
+ Picture floppyCrimeM;
+ const bool haveFloppySite = floppyMap && _picsArchive.getPicture(0xc5, floppySiteM);
+ const bool haveFloppyCrime = floppyMap && _picsArchive.getPicture(0xc6, floppyCrimeM);
+ for (uint i = 0; i < _mystery.numSites(); i++) {
if (!_mystery._onSites[i] && i != _mystery._siteNumber)
continue;
const byte *entry = _mystery.mapEntry(i);
if (!entry)
continue;
- const uint16 buttonId = READ_LE_UINT16(entry + 0x0);
- const uint16 mx = READ_LE_UINT16(entry + 0x8);
- const uint16 my = READ_LE_UINT16(entry + 0xa);
-
+ uint16 mx;
+ uint16 my;
Picture button;
- if (!_buttonArchive.loadEntry(buttonId, button))
- continue;
+ if (floppyMap) {
+ // Floppy SITES rows carry TWO position pairs:
+ // (+0, +2) = position on BIGMAP.PIC (the zoomed view).
+ // Used by `FUN_1fed_0c3e @ 1fed:0c3e` for
+ // suspect-portrait stamping AND as the
+ // click bbox on the zoomed map.
+ // (+6, +8) = position on the overview PIC 0x42.
+ // Used by `FUN_1fed_07ed @ 1fed:07ed`.
+ // The detail view scrolls BIGMAP.PIC, so we need the
+ // (+0, +2) coords here. Recolor flag at +10 still
+ // selects crime vs site marker.
+ mx = READ_LE_UINT16(entry + 0x0);
+ my = READ_LE_UINT16(entry + 0x2);
+ const bool useCrime = entry[0xa] != 0;
+ if (useCrime && haveFloppyCrime)
+ button = floppyCrimeM;
+ else if (haveFloppySite)
+ button = floppySiteM;
+ else
+ continue;
+ } else {
+ const uint16 buttonId = READ_LE_UINT16(entry + 0x0);
+ mx = READ_LE_UINT16(entry + 0x8);
+ my = READ_LE_UINT16(entry + 0xa);
+ if (!_buttonArchive.loadEntry(buttonId, button))
+ continue;
+ }
const int sx = (int)mx - scrollX + kMapWinX;
const int sy = (int)my - scrollY + kMapWinY;
const byte transp = (byte)(button.flags >> 8);
Commit: 3ba1ac49ac1ddc11fa16766af84344b5dec53a16
https://github.com/scummvm/scummvm/commit/3ba1ac49ac1ddc11fa16766af84344b5dec53a16
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:52+02:00
Commit Message:
EEM: npc conversations for the floppy version
Changed paths:
engines/eem/clues.cpp
engines/eem/eem.h
engines/eem/mystery.cpp
engines/eem/mystery.h
engines/eem/site.cpp
engines/eem/site.h
engines/eem/ui.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index e765bff0107..8e4cd5798d1 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -896,13 +896,9 @@ void EEMEngine::displayClue(const byte *clueBlock) {
}
}
-void EEMEngine::displayFloppyBriefing(const byte *initBlock) {
- // Floppy briefing â mirrors the dialog loop at the tail of
- // `FUN_19bb_042f @ 19bb:042f`:
- // nSubjects = ib[1]; nDialog = ib[2 + nSubjects];
- // records = ib + 3 + nSubjects;
- // Each record is dispatched through `FUN_22dc_05c8 @ 22dc:05c8`,
- // which reads:
+void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count) {
+ // Render `count` consecutive floppy dialog records starting at
+ // `rec`. Per `FUN_22dc_05c8 @ 22dc:05c8`, each record is:
// u16 picID @ +0 (character portrait, 0 = skip pic)
// u16 picX @ +2
// u8 picY @ +4
@@ -916,13 +912,9 @@ void EEMEngine::displayFloppyBriefing(const byte *initBlock) {
// buffer (verified by note 0 of M0.BIN at file offset 0xd0 holding
// "Hello, ..., I'm ... Eagle!"), so we read text via
// `mystery.blobAt(noteEntry[+2..3])` for Jake / `+4..5` for Jenny.
- if (!initBlock || !isFloppy() || !_font.isLoaded())
+ if (!rec || !isFloppy() || !_font.isLoaded() || count == 0)
return;
- const uint8 nSubjects = initBlock[1];
- const uint8 nDialog = initBlock[2 + nSubjects];
- const byte *rec = initBlock + 3 + nSubjects;
-
const byte *notes = _mystery.noteIndex();
const byte *bufBase = _mystery.blobAt(0);
if (!notes || !bufBase)
@@ -943,7 +935,7 @@ void EEMEngine::displayFloppyBriefing(const byte *initBlock) {
}
}
- for (uint i = 0; i < nDialog && !shouldQuit(); i++) {
+ for (uint i = 0; i < count && !shouldQuit(); i++) {
const uint16 picID = READ_LE_UINT16(rec + 0);
const uint16 picX = READ_LE_UINT16(rec + 2);
const uint8 picY = rec[4];
@@ -953,17 +945,35 @@ void EEMEngine::displayFloppyBriefing(const byte *initBlock) {
const uint8 textCount= rec[10];
// Build the full text by concatenating each note's per-partner
- // text string (parsed for `%s`-style placeholders).
+ // text string (parsed for `%s`-style placeholders). Bounds-
+ // check every offset against the mystery blob; if any byte
+ // looks out of range we just stop appending â corrupt offsets
+ // from a misparsed record would otherwise dereference past the
+ // buffer and SIGBUS inside `strlen`.
+ const uint32 dsz = _mystery.dataSize();
+ const uint32 notesBase = (uint32)(notes - bufBase);
Common::String raw;
for (uint t = 0; t < textCount; t++) {
- const uint8 idx = rec[11 + t] & 0x7f;
+ const uint8 idx = rec[11 + t] & 0x7f;
+ const uint32 noteAbs = notesBase + (uint32)idx * 7;
+ if (noteAbs + 6 > dsz)
+ break;
const uint16 textOff = (_partner == 0)
? READ_LE_UINT16(notes + idx * 7 + 2)
: READ_LE_UINT16(notes + idx * 7 + 4);
+ if (textOff >= dsz)
+ break;
const char *line = (const char *)(bufBase + textOff);
+ // Find the NUL terminator within the blob â refuse to
+ // strlen past the end.
+ uint32 lineLen = 0;
+ while (textOff + lineLen < dsz && line[lineLen] != 0)
+ lineLen++;
+ if (textOff + lineLen >= dsz)
+ break;
if (t > 0)
raw += ' ';
- raw += line;
+ raw += Common::String(line, lineLen);
}
const Common::String text = parseString(raw, _playerName, _partner);
@@ -1083,6 +1093,105 @@ void EEMEngine::displayFloppyBriefing(const byte *initBlock) {
}
}
+void EEMEngine::displayFloppyBriefing(const byte *initBlock) {
+ // Floppy briefing â `FUN_19bb_042f @ 19bb:042f` walks
+ // `nDialog = ib[2 + nSubjects]` records starting at
+ // `ib + 3 + nSubjects`. Each record is rendered identically to a
+ // hotspot dialog record (same `FUN_22dc_05c8` callee), so we
+ // share `displayFloppyDialogRecords`.
+ if (!initBlock || !isFloppy())
+ return;
+ const uint8 nSubjects = initBlock[1];
+ const uint8 nDialog = initBlock[2 + nSubjects];
+ const byte *rec = initBlock + 3 + nSubjects;
+ displayFloppyDialogRecords(rec, nDialog);
+}
+
+void EEMEngine::displayFloppyHotspotDialog(uint siteNum, uint hotIdx) {
+ // Floppy hotspot click â mirrors `FUN_22dc_0b80 @ 22dc:0b80` +
+ // `FUN_1652_00e6 @ 1652:00e6` + `FUN_1652_006c @ 1652:006c`.
+ // Each site stores a per-hotspot dialog list at
+ // `site_data[+6..7]`. The list is laid out as:
+ // for each hotspot in order:
+ // main record (11 + textCount bytes)
+ // u8 contFlags (low 7 bits = continuation count, high bit
+ // used by FUN_1652_00e6 to drive the partner
+ // pose â irrelevant for text rendering)
+ // contCount à { record (11 + textCount bytes) }
+ // We walk past `hotIdx` hotspots, then dispatch the matched main
+ // record + its continuation chain through the same renderer
+ // `displayFloppyBriefing` uses.
+ if (!_mystery.isLoaded() || !isFloppy())
+ return;
+ const byte *site = _mystery.siteData(siteNum);
+ if (!site)
+ return;
+ const uint16 dlgListOff = READ_LE_UINT16(site + 6);
+ const byte *bufBase = _mystery.blobAt(0);
+ if (!bufBase || dlgListOff == 0 || dlgListOff >= _mystery.dataSize())
+ return;
+ uint32 off = dlgListOff;
+ for (uint h = 0; h < hotIdx; h++) {
+ // Skip main record.
+ const byte *rec = bufBase + off;
+ off += 11 + rec[10];
+ // Read continuation count and skip those records.
+ const uint contCount = bufBase[off] & 0x7F;
+ off += 1;
+ for (uint c = 0; c < contCount; c++) {
+ const byte *cr = bufBase + off;
+ off += 11 + cr[10];
+ }
+ }
+ if (off >= _mystery.dataSize())
+ return;
+ // Layout per hotspot is:
+ // main record (11 + textCount bytes)
+ // 1 byte: continuation count (high bit = partner-pose flag,
+ // low 7 bits = number of follow-up records)
+ // continuation records (each 11 + textCount bytes, tightly packed)
+ // `displayFloppyDialogRecords` walks tightly so we have to call it
+ // twice â once for the main record, once for the continuations â
+ // otherwise the second iteration treats the cont-count byte as a
+ // record header and runs off the buffer.
+ //
+ // Each `displayFloppyDialogRecords` call snapshots the screen to
+ // know what to redraw between bubbles. If we just call it twice
+ // back-to-back the second snapshot includes the first call's last
+ // bubble, so the second bubble draws on top of the first one (the
+ // "background isn't redrawn" glitch the user reported). Capture
+ // the clean site BG here and restore it between the two calls so
+ // every record snapshots a bubble-free background.
+ Graphics::ManagedSurface siteBG(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ {
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (screen) {
+ siteBG.simpleBlitFrom(*screen);
+ g_system->unlockScreen();
+ }
+ }
+ const byte *mainRec = bufBase + off;
+ displayFloppyDialogRecords(mainRec, 1);
+ const uint mainLen = 11u + (uint)mainRec[10];
+ if (off + mainLen >= _mystery.dataSize())
+ return;
+ const uint contCount = (uint)(bufBase[off + mainLen] & 0x7F);
+ if (contCount == 0)
+ return;
+ const uint32 contOff = off + mainLen + 1;
+ if (contOff >= _mystery.dataSize())
+ return;
+ // Wipe the main bubble before the continuation chain snapshots the
+ // screen â otherwise the first continuation bubble treats the
+ // post-main-bubble image as its background and the main balloon
+ // pixels persist behind every following balloon.
+ g_system->copyRectToScreen(siteBG.getPixels(), siteBG.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ displayFloppyDialogRecords(bufBase + contOff, contCount);
+}
+
bool EEMEngine::areYouSure() {
// Mirrors `_AreYouSure` @ 1a35:0a5c. Original loads PIC 0x136 for the
// dialog body and PIC 0x1FD/0x1FE for YES/NO. We render a minimal
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 74046faec8d..0b926e5655b 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -185,6 +185,13 @@ public:
/// followed by 62-byte ClueEntries. Mirrors _DisplayClue @ 2404:05e6.
void displayClue(const byte *clueBlock);
+ /// Floppy hotspot click â locate the dialog records for the
+ /// clicked hotspot in the site's per-hotspot dialog list at
+ /// `site_data[+6]` and dispatch them through
+ /// `displayFloppyDialogRecords`. Mirrors `FUN_22dc_0b80 +
+ /// FUN_1652_00e6 + FUN_1652_006c`.
+ void displayFloppyHotspotDialog(uint siteNum, uint hotIdx);
+
/// Active player name (saved as the profile-save description).
const Common::String &playerName() const { return _playerName; }
@@ -439,6 +446,12 @@ private:
/// +11.. text indices (1 byte each, low 7 bits = NOTES idx)
void displayFloppyBriefing(const byte *initBlock);
+ /// Render `count` consecutive floppy dialog records starting at
+ /// `rec`. Shared between briefing and hotspot click handlers since
+ /// the original engine uses the same `FUN_22dc_05c8 @ 22dc:05c8`
+ /// renderer in both contexts.
+ void displayFloppyDialogRecords(const byte *rec, uint count);
+
public:
/// Mirrors `_StartTravelMusic @ 20a2:0595`. Picks `MUS%05d.XMI`
/// based on `_mystery._siteNumber % 5` and starts it (looping). The
diff --git a/engines/eem/mystery.cpp b/engines/eem/mystery.cpp
index 437a8f62499..98f48a1d78e 100644
--- a/engines/eem/mystery.cpp
+++ b/engines/eem/mystery.cpp
@@ -287,17 +287,25 @@ const byte *Mystery::siteData(uint siteNum) const {
}
const byte *Mystery::hotspots(uint siteNum) const {
+ if (_isFloppy) {
+ // Floppy: hotspot table sits inside the per-site sub-blob.
+ // `site_data[+4..5]` is a u16 file offset to a header byte
+ // (count) + NÃ8-byte rectangles (x1, y1, x2, y2 as u16s) â
+ // verified at `FUN_22dc_0b80 @ 22dc:0b80` (the click hit-test
+ // loop reads `*(byte *)(buf + site_data[+4])` for the count
+ // then `FUN_14c9_0039(... buf + site_data[+4] + 1 + i*8)`
+ // for each rectangle).
+ const byte *site = siteData(siteNum);
+ if (!site || (size_t)(site - _data.data()) + 6 > _data.size())
+ return nullptr;
+ const uint16 hotspotOff = READ_LE_UINT16(site + 4);
+ if (hotspotOff == 0 || hotspotOff + 1 > _data.size())
+ return nullptr;
+ return _data.data() + hotspotOff + 1;
+ }
const byte *idx = siteIndexEntry(siteNum);
if (!idx)
return nullptr;
- // Floppy site index is only 2 bytes per entry; `idx + 4` would
- // read into the NEXT entry's offset, returning garbage. The
- // floppy hotspot list lives inside the site-data sub-blob (see
- // `_DoSiteLoop_Floppy @ 1652:03a3` walking 5-byte drop entries
- // from `*site_data + 2`); don't fake a CD-style result here â
- // SiteScreen guards floppy paths separately.
- if (_isFloppy)
- return nullptr;
const uint16 hotspotOff = READ_LE_UINT16(idx + 4);
if (hotspotOff >= _data.size())
return nullptr;
@@ -305,8 +313,15 @@ const byte *Mystery::hotspots(uint siteNum) const {
}
uint16 Mystery::hotspotCount(uint siteNum) const {
- if (_isFloppy)
- return 0;
+ if (_isFloppy) {
+ const byte *site = siteData(siteNum);
+ if (!site || (size_t)(site - _data.data()) + 6 > _data.size())
+ return 0;
+ const uint16 hotspotOff = READ_LE_UINT16(site + 4);
+ if (hotspotOff == 0 || hotspotOff >= _data.size())
+ return 0;
+ return (uint16)_data[hotspotOff];
+ }
const byte *site = siteData(siteNum);
if (!site || (size_t)(site - _data.data()) + 8 > _data.size())
return 0;
diff --git a/engines/eem/mystery.h b/engines/eem/mystery.h
index 4b23fa4ea27..251320604aa 100644
--- a/engines/eem/mystery.h
+++ b/engines/eem/mystery.h
@@ -141,6 +141,9 @@ public:
return offset < _data.size() ? _data.data() + offset : nullptr;
}
+ /// Total mystery blob size in bytes (for bounds checks).
+ uint32 dataSize() const { return (uint32)_data.size(); }
+
/// Synchronize the per-mystery runtime state for save/load. The fixed
/// arrays serialize first, then the booleans and counters.
void syncState(Common::Serializer &s);
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 80610200252..1add3abbd4c 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -588,8 +588,13 @@ void SiteScreen::enter(uint siteNum) {
_vm->stopMusic();
// Static drops (Loop 2 from `_DoSiteLoop`) â no animation, baked
- // into the BG snapshot the run() pump uses to restore.
- renderStaticDrops(siteNum);
+ // into the BG snapshot the run() pump uses to restore. Floppy
+ // stores them in a different shape (drops sub-struct after the
+ // site_data offset), so dispatch on `isFloppy()`.
+ if (_vm->isFloppy())
+ renderFloppyDrops(siteNum);
+ else
+ renderStaticDrops(siteNum);
// Snapshot the static layers so per-tick animation re-blits don't
// have to re-load PIC 0x43, the SITES.DBD scene, or each
@@ -643,7 +648,10 @@ void SiteScreen::enter(uint siteNum) {
// balloon residues; refresh the site so the player returns to
// a clean state. Re-build the snapshot too.
renderBackground(siteNum);
- renderStaticDrops(siteNum);
+ if (_vm->isFloppy())
+ renderFloppyDrops(siteNum);
+ else
+ renderStaticDrops(siteNum);
captureBgSnapshot();
_snapshotSite = (int)siteNum;
const uint32 nowAfter = g_system->getMillis();
@@ -938,6 +946,53 @@ void SiteScreen::renderStaticDrops(uint siteNum) {
g_system->unlockScreen();
}
+void SiteScreen::renderFloppyDrops(uint siteNum) {
+ // Floppy drops live inside the drops sub-struct pointed to by
+ // `*site_data` (u16 offset). Verified from the call site in
+ // `_DoSiteLoop_Floppy @ 1652:0418`:
+ // FUN_16e2_18eb(
+ // *(u16 *)(local_1a + i*5), // arg1 = u16 picID @ +0..1
+ // *(u16 *)(local_1a + i*5 + 2), // arg2 = u16 X @ +2..3
+ // local_1a[i*5 + 4] // arg3 = u8 Y @ +4
+ // );
+ // Inside `FUN_16e2_18eb @ 16e2:18eb`, `arg1 - 1` indexes the
+ // PICS.DBX table at `2608:4537` (loaded from `PICS.DBX` by
+ // `FUN_16e2_0149 @ 16e2:0149`); `arg2/arg3` become destX/destY.
+ // drops_struct[0] = BG picID (rendered separately by
+ // `renderBackground`), drops_struct[1] = drop count.
+ if (!_mystery)
+ return;
+ const byte *site = _mystery->siteData(siteNum);
+ if (!site)
+ return;
+ const uint16 dropsOff = READ_LE_UINT16(site);
+ const byte *drops = _mystery->blobAt(dropsOff);
+ if (!drops)
+ return;
+ const uint8 count = drops[1];
+
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (!screen)
+ return;
+
+ for (uint i = 0; i < count; i++) {
+ const byte *e = drops + 2 + i * 5;
+ const uint16 picID = READ_LE_UINT16(e + 0);
+ const int16 x = (int16)READ_LE_UINT16(e + 2);
+ const int16 y = (int16)e[4];
+ if (picID == 0)
+ continue;
+ // `getPicture(num)` already does `loadEntry(num - 1)` (see
+ // `resource.h:100`), matching the `picID - 1` index the
+ // original passes to PICS.DBD.
+ Picture pic;
+ if (!_vm->getPics().getPicture((uint)picID, pic))
+ continue;
+ blitMaskedSurface(screen, pic, x, y);
+ }
+ g_system->unlockScreen();
+}
+
void SiteScreen::renderAnimatedDrops(uint siteNum, uint32 tickMs) {
// Loop 1 from `_DoSiteLoop @ 168d:03f4`:
// bound: siteData[+0xa]
@@ -1071,29 +1126,46 @@ void SiteScreen::renderPartner(uint siteNum, uint32 tickMs) {
// `kWaitAnims` lives at file scope above; we cap rendering at
// `speaker < 7` since anything past entry 6 is the `_SiteButtons`
// rect data that follows the table in the binary.
- // Floppy site data has a different shape: site_data+8 is a u16
- // OFFSET to a 10-byte speakerInfo struct (verified at
- // `_DoSiteLoop_Floppy @ 1652:042b` reading `*(undefined2 *)
- // (DAT_28da_0172) = anim_id_jake` etc.), not an index into the
- // `kWaitAnims` table the CD uses. Skip the partner render until
- // the floppy speakerInfo is wired up â without this guard we
- // dereference garbage entries past the table end.
- if (_vm && _vm->isFloppy())
- return;
const byte *site = _mystery->siteData(siteNum);
if (!site)
return;
- const uint16 speaker = READ_LE_UINT16(site + 8);
- if (speaker >= ARRAYSIZE(kWaitAnims)) {
- warning("renderPartner: site %u has speakerIdx=%u out of range",
- siteNum, speaker);
- return;
- }
-
const uint8 partner = _vm->getPartnerIndex();
- const uint animId = kWaitAnims[speaker][0 + partner];
- const int x = (int)(int16)kWaitAnims[speaker][2 + partner];
- const int y = (int)(int16)kWaitAnims[speaker][4 + partner];
+ uint animId;
+ int x;
+ int y;
+ if (_vm->isFloppy()) {
+ // Floppy: site_data+8 is a u16 OFFSET to a 10-byte
+ // speakerInfo struct (per `_DoSiteLoop_Floppy @ 1652:042b`):
+ // bytes 0..1 Jake anim ID (u16)
+ // bytes 2..3 Jake X (u16)
+ // byte 4 Jake Y (u8)
+ // bytes 5..6 Jenny anim ID (u16)
+ // bytes 7..8 Jenny X (u16)
+ // byte 9 Jenny Y (u8)
+ const uint16 spkOff = READ_LE_UINT16(site + 8);
+ const byte *spk = _mystery->blobAt(spkOff);
+ if (!spk)
+ return;
+ if (partner == 0) {
+ animId = READ_LE_UINT16(spk + 0);
+ x = (int)READ_LE_UINT16(spk + 2);
+ y = (int)spk[4];
+ } else {
+ animId = READ_LE_UINT16(spk + 5);
+ x = (int)READ_LE_UINT16(spk + 7);
+ y = (int)spk[9];
+ }
+ } else {
+ const uint16 speaker = READ_LE_UINT16(site + 8);
+ if (speaker >= ARRAYSIZE(kWaitAnims)) {
+ warning("renderPartner: site %u has speakerIdx=%u out of range",
+ siteNum, speaker);
+ return;
+ }
+ animId = kWaitAnims[speaker][0 + partner];
+ x = (int)(int16)kWaitAnims[speaker][2 + partner];
+ y = (int)(int16)kWaitAnims[speaker][4 + partner];
+ }
Animation anim;
if (!_vm->getAni().loadAnimation(animId, anim) || anim.empty())
@@ -1227,8 +1299,13 @@ void SiteScreen::renderHotspots(uint siteNum) {
// don't all glow in lock-step.
const uint32 tickMs = g_system->getMillis();
+ // CD hotspot rows are 14 bytes each (rect + 6 bytes of clue
+ // metadata). Floppy stores plain 8-byte rectangles only â clue
+ // data lives in a separate dialog-record list at `site_data[+6]`,
+ // keyed by hotspot index. Verified at `FUN_22dc_0b80 @ 22dc:0b80`.
+ const uint stride = _vm && _vm->isFloppy() ? 8 : 14;
for (uint i = 0; i < count; i++) {
- const byte *r = spots + i * 14;
+ const byte *r = spots + i * stride;
const int16 x1 = (int16)READ_LE_UINT16(r + 0);
const int16 y1 = (int16)READ_LE_UINT16(r + 2);
const int16 x2 = (int16)READ_LE_UINT16(r + 4);
@@ -1289,8 +1366,9 @@ int SiteScreen::hotspotAtPoint(uint siteNum, int x, int y) const {
if (!spots)
return -1;
+ const uint stride = _vm && _vm->isFloppy() ? 8 : 14;
for (uint i = 0; i < count; i++) {
- const byte *r = spots + i * 14;
+ const byte *r = spots + i * stride;
const int16 x1 = (int16)READ_LE_UINT16(r + 0);
const int16 y1 = (int16)READ_LE_UINT16(r + 2);
const int16 x2 = (int16)READ_LE_UINT16(r + 4);
@@ -1304,6 +1382,21 @@ int SiteScreen::hotspotAtPoint(uint siteNum, int x, int y) const {
void SiteScreen::onHotspotClicked(uint siteNum, uint hotIdx) {
debugC(1, kDebugSite, "Site %u: hotspot %u clicked", siteNum, hotIdx);
+ // Floppy: hotspot rectangles are plain 8-byte rects (no clue
+ // metadata at +0xa or +8); the dialog records live in a separate
+ // per-hotspot list at `site_data[+6]`. Dispatch through the
+ // floppy-specific renderer and mark the click rectangle seen by
+ // array index (the only ordinal we have on floppy).
+ if (_vm->isFloppy()) {
+ if (hotIdx < Mystery::kHotSpotsCap)
+ _mystery->_hotSpotsSeen[hotIdx] = 1;
+ _mystery->_searchLocationNumber = (uint16)hotIdx;
+ _vm->setPartnerEraseBg(&_bgSnapshot);
+ _vm->displayFloppyHotspotDialog(siteNum, hotIdx);
+ _vm->setPartnerEraseBg(nullptr);
+ return;
+ }
+
// `_DoSiteLoop @ 168d:03f4` (after _DisplayClue):
// _HotSpotsSeen[hotspot[+0xa] * 2] = _HotSpotComplete;
// The "seen" key is the hotspotIndex field (+0xa) â the 1-based
diff --git a/engines/eem/site.h b/engines/eem/site.h
index 33f6f23ab55..23621409806 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -131,6 +131,13 @@ private:
/// animate so they go in the BG snapshot.
void renderStaticDrops(uint siteNum);
+ /// Floppy variant: drops live inside the drops sub-struct
+ /// (`*site_data` â drops; `drops[1]` = count; entries at
+ /// `drops + 2` are 5 bytes each: {u16 X, u16 Y, byte picID}).
+ /// PIC entries are loaded from PICS.DBD with `picID - 1`. Per
+ /// `_DoSiteLoop_Floppy @ 1652:0418` and `FUN_16e2_18eb`.
+ void renderFloppyDrops(uint siteNum);
+
/// Draw the per-site animated NPCs (Loop 1) at the current tick.
/// `_DoSiteLoop` registers each via `_NewAnimation` (siteData[+0xa]
/// entries at siteData[+0x48]: {animId (-1 = ColorCycle), x, y})
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 7e7e9de005f..a7589a566f6 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -2710,14 +2710,15 @@ void EEMEngine::doBigMap() {
continue;
uint16 mx;
uint16 my;
- uint16 buttonId = 0;
+ uint16 buttonId;
if (fmap) {
// Floppy detail view: click rect on
- // BIGMAP.PIC at (+0, +2), per
- // `FUN_1fed_0c3e`'s write to
- // DAT_28da_3aee/DAT_28da_3af0.
+ // BIGMAP.PIC at (+0, +2), labelled BUTTON.DBD
+ // entry ID at entry+4 (per
+ // `FUN_1fed_0c3e @ 1fed:0c3e`).
mx = READ_LE_UINT16(entry + 0x0);
my = READ_LE_UINT16(entry + 0x2);
+ buttonId = (uint16)entry[0x4];
} else {
buttonId = READ_LE_UINT16(entry + 0x0);
mx = READ_LE_UINT16(entry + 0x8);
@@ -2726,18 +2727,7 @@ void EEMEngine::doBigMap() {
Picture button;
int bw = 16;
int bh = 16;
- if (fmap) {
- // Floppy uses the global site-marker PIC for
- // every site; the recolor flag at +10 picks
- // the crime variant. Sized off whichever PIC
- // is loaded successfully.
- Picture m;
- const uint pic = (entry[0xa] != 0) ? 0xc6 : 0xc5;
- if (_picsArchive.getPicture(pic, m)) {
- bw = m.surface.w;
- bh = m.surface.h;
- }
- } else if (_buttonArchive.loadEntry(buttonId, button)) {
+ if (_buttonArchive.loadEntry(buttonId, button)) {
bw = button.surface.w;
bh = button.surface.h;
}
@@ -2916,18 +2906,18 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
copyW);
}
- // Stamped site buttons. `_StampButtons @ 20fe:0d2f`:
+ // Stamped site buttons. `_StampButtons @ 20fe:0d2f` (CD):
// button = _GetButton(MapData[+0])
// destX = MapData[+8]
// destY = MapData[+0xa]
- // Floppy SITES rows are 11 bytes â no per-button PIC ID. Use the
- // regular site / crime marker PICs (0xc5 / 0xc6) keyed off the
- // recolor flag at +10, same logic as the overview stamp.
+ // Floppy uses `FUN_1fed_0c3e @ 1fed:0c3e`: for each SITES row, the
+ // byte at entry+4 is a BUTTON.DBD entry ID (loaded via
+ // `FUN_16e2_1838 @ 16e2:1838`, which opens `button.dbd` â string
+ // at `2608:0558`). The labelled button is stamped at
+ // `(entry+0..1, entry+2..3)` on BIGMAP.PIC. These are the same
+ // per-site labelled buttons the CD uses, just keyed off a
+ // different field offset.
const bool floppyMap = _mystery.isLoaded() && isFloppy();
- Picture floppySiteM;
- Picture floppyCrimeM;
- const bool haveFloppySite = floppyMap && _picsArchive.getPicture(0xc5, floppySiteM);
- const bool haveFloppyCrime = floppyMap && _picsArchive.getPicture(0xc6, floppyCrimeM);
for (uint i = 0; i < _mystery.numSites(); i++) {
if (!_mystery._onSites[i] && i != _mystery._siteNumber)
continue;
@@ -2938,24 +2928,10 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
uint16 my;
Picture button;
if (floppyMap) {
- // Floppy SITES rows carry TWO position pairs:
- // (+0, +2) = position on BIGMAP.PIC (the zoomed view).
- // Used by `FUN_1fed_0c3e @ 1fed:0c3e` for
- // suspect-portrait stamping AND as the
- // click bbox on the zoomed map.
- // (+6, +8) = position on the overview PIC 0x42.
- // Used by `FUN_1fed_07ed @ 1fed:07ed`.
- // The detail view scrolls BIGMAP.PIC, so we need the
- // (+0, +2) coords here. Recolor flag at +10 still
- // selects crime vs site marker.
mx = READ_LE_UINT16(entry + 0x0);
my = READ_LE_UINT16(entry + 0x2);
- const bool useCrime = entry[0xa] != 0;
- if (useCrime && haveFloppyCrime)
- button = floppyCrimeM;
- else if (haveFloppySite)
- button = floppySiteM;
- else
+ const uint16 buttonId = (uint16)entry[0x4];
+ if (!_buttonArchive.loadEntry(buttonId, button))
continue;
} else {
const uint16 buttonId = READ_LE_UINT16(entry + 0x0);
Commit: 20eba94c78e88e73d1a33ee7d41f1bf23186f9fd
https://github.com/scummvm/scummvm/commit/20eba94c78e88e73d1a33ee7d41f1bf23186f9fd
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:52+02:00
Commit Message:
EEM: improved hints and audio in the floppy version
Changed paths:
engines/eem/audio.cpp
engines/eem/audio.h
engines/eem/clues.cpp
engines/eem/graphics.cpp
engines/eem/mystery.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/audio.cpp b/engines/eem/audio.cpp
index 4c2e2a4bb15..6ed89dcdf0c 100644
--- a/engines/eem/audio.cpp
+++ b/engines/eem/audio.cpp
@@ -199,6 +199,39 @@ void AudioPlayer::playPcmBuffer(byte *pcm, uint32 size, uint sampleRate,
DisposeAfterUse::YES);
}
+// Floppy per-partner voice table â verified by reading the raw filename
+// pointers at `2608:0f0e` (Jake) and `2608:0f76` (Jenny) in EEM.EXE,
+// each 26 Ã FAR-ptr to a NUL-terminated `*.voc` filename. Indexed by
+// `_LoadSoundName_Floppy @ 1f4e:0305`. Slots match across partners:
+// 12 = PHONESL.VOC, 20 = partner intro, 25 = THUNDER.VOC.
+static const char *const kFloppyJakeVoiceTable[26] = {
+ "DING.VOC", "M-0083SL.VOC", "M-0085SL.VOC", "NEWSCAN.VOC",
+ "M-0089SL.VOC", "M-0091SL.VOC", "M-0092SL.VOC", "NEWSSHRT.VOC",
+ "M-0096SL.VOC", "M-0102SL.VOC", "M-0104SL.VOC", "M-0107SL.VOC",
+ "PHONESL.VOC", "M-0113SL.VOC", "DING.VOC", "DING.VOC",
+ "M-0054SL.VOC", "M-0014SL.VOC", "M-0012SL.VOC", "SQUAK2SL.VOC",
+ "M-0113SL.VOC", "B-0006SL.VOC", "B-0003SL.VOC", "B-0004SL.VOC",
+ "M-0163SL.VOC", "THUNDER.VOC",
+};
+static const char *const kFloppyJennyVoiceTable[26] = {
+ "DING.VOC", "F-0194SL.VOC", "F-0191SL.VOC", "NEWSCAN.VOC",
+ "F-0187SL.VOC", "F-0184SL.VOC", "F-0181SL.VOC", "NEWSSHRT.VOC",
+ "F-0177SL.VOC", "F-0170SL.VOC", "F-0168SL.VOC", "F-0166SL.VOC",
+ "PHONESL.VOC", "F-0161SL.VOC", "DING.VOC", "DING.VOC",
+ "F-0067SL.VOC", "F-0016SL.VOC", "F-0013SL.VOC", "SQUAK2SL.VOC",
+ "F-0140SL.VOC", "B-0006SL.VOC", "B-0003SL.VOC", "B-0004SL.VOC",
+ "F-0165SL.VOC", "THUNDER.VOC",
+};
+
+void AudioPlayer::playFloppyVoiceSlot(uint slot, uint partner) {
+ if (slot >= 26)
+ slot = 0; // mirrors `_LoadSoundName_Floppy`'s `if (0x19 < slot) slot = 0;`
+ const char *name = (partner == 0)
+ ? kFloppyJakeVoiceTable[slot]
+ : kFloppyJennyVoiceTable[slot];
+ playVoc(Common::Path(name));
+}
+
void AudioPlayer::spoolSound(uint num) {
if (!_voiceEnabled) {
debugC(2, kDebugSound,
diff --git a/engines/eem/audio.h b/engines/eem/audio.h
index 921e00e900b..12667f1179d 100644
--- a/engines/eem/audio.h
+++ b/engines/eem/audio.h
@@ -104,6 +104,13 @@ public:
/// Mirrors `_StopTheVoice @ 1ff1:0283`.
void stopVoice();
+ /// Floppy variant: play a VOC by 0..25 slot index in the per-partner
+ /// voice table. Mirrors `_LoadSoundName_Floppy @ 1f4e:0305` which
+ /// indexes the table at `2608:0f0e` (Jake) / `2608:0f76` (Jenny).
+ /// `partner` is 0 for Jake, 1 for Jenny. Common slots: 12 =
+ /// PHONESL.VOC, 20 = partner intro voice, 25 = THUNDER.VOC.
+ void playFloppyVoiceSlot(uint slot, uint partner);
+
// Mystery sound spool ---------------------------------------------
/// Mirrors `_InitMysterySounds @ 202f:05cb`. Loads `M%u.SDX` into
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 8e4cd5798d1..d53fb7f57ae 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -236,21 +236,16 @@ void EEMEngine::doChoosePartner() {
// (`jen.voc` for Jenny, `jake.voc` for Jake; strings at 29be:0af1 /
// 29be:0af9) and block on `_WaitForVoiceDone`.
if (_audio) {
- // CD: standalone clips `JAKE.VOC` / `JEN.VOC`. Floppy uses
- // per-partner-and-event voice tables at `2608:0F0E` (Jake) /
- // `2608:0F76` (Jenny), each 25 entries à FAR ptr to a VOC
- // filename. After a partner pick, `FUN_19bb_0858 @ 19bb:0858`
- // calls `FUN_1f4e_0305(0x14)` which loads voice slot 20 from
- // the table for the chosen partner:
- // Jake slot 20 â `2608:116B = "m-0113sl.voc"`
- // Jenny slot 20 â `2608:12AE = "f-0140sl.voc"`
- Common::String voc;
if (isFloppy()) {
- voc = (_partner == 0) ? "M-0113SL.VOC" : "F-0140SL.VOC";
+ // Floppy `_DoChoosePartner_Floppy @ 19bb:0a8e` calls
+ // `_LoadSoundName_Floppy(0x14)` (= slot 20) which the
+ // per-partner table at `2608:0f0e` / `2608:0f76` resolves
+ // to `m-0113sl.voc` (Jake) or `f-0140sl.voc` (Jenny).
+ _audio->playFloppyVoiceSlot(0x14, _partner);
} else {
- voc = (_partner == 0) ? "JAKE.VOC" : "JEN.VOC";
+ _audio->playVoc(Common::Path(
+ (_partner == 0) ? "JAKE.VOC" : "JEN.VOC"));
}
- _audio->playVoc(Common::Path(voc));
_audio->waitForVoiceDone();
}
}
@@ -507,16 +502,25 @@ void EEMEngine::doInitClues() {
}
}
- // `_DoInitClues` plays `phone.voc` (29be:0acc) ONLY when caseType == 2
- // (the "incoming call" briefing variant). Verified at 1a35:05a2 â
- // the gate is `iVar1 == 2 && _VoiceAvailable`. Other case types open
- // straight into the briefing dialogue without it.
- if (caseType == 2 && _audio) {
- // Floppy ships `PHONESL.VOC` instead of CD's `PHONE.VOC` (the
- // `_LoadSoundName` call site at 2608:1107-110c hands the
- // floppy filename to the same loader).
- _audio->playVoc(Common::Path(isFloppy() ? "PHONESL.VOC" : "PHONE.VOC"));
- _audio->waitForVoiceDone();
+ // `_DoInitClues` plays a setup-voice ONLY for caseType 2/3.
+ // CD: caseType 2 â PHONE.VOC. caseType 3 â no voice.
+ // Floppy `_DoInitClues_Floppy @ 19bb:042f`:
+ // caseType 2 â `_LoadSoundName_Floppy(slot 0xc)` = PHONESL.VOC
+ // caseType 3 â `_LoadSoundName_Floppy(slot 3)` = NEWSCAN.VOC
+ // (newspaper-scanner sting for the "TV/news anchor" briefing
+ // variant). Other case types open straight into the briefing
+ // dialogue without it.
+ if (_audio) {
+ if (caseType == 2) {
+ if (floppy)
+ _audio->playFloppyVoiceSlot(0x0c, _partner);
+ else
+ _audio->playVoc(Common::Path("PHONE.VOC"));
+ _audio->waitForVoiceDone();
+ } else if (caseType == 3 && floppy) {
+ _audio->playFloppyVoiceSlot(0x03, _partner);
+ _audio->waitForVoiceDone();
+ }
}
// Step 6 â case briefing dialogue. CD InitBlock has the clue block
@@ -944,17 +948,39 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count) {
const uint8 ballY = rec[8];
const uint8 textCount= rec[10];
+ // Per-record voice playback. `FUN_22dc_05c8 @ 22dc:05c8`:
+ // if ((rec[+9] & 0x80) && voiceOption && voiceAvail) {
+ // slot = rec[+9] & 0x7f;
+ // _LoadSoundName_Floppy(slot); _PlayVoc(...);
+ // }
+ // The slot indexes the per-partner table at `2608:0f0e` /
+ // `2608:0f76` (= our `playFloppyVoiceSlot`). Sound is started
+ // BEFORE the bubble renders so it plays while the player reads.
+ if ((rec[9] & 0x80) != 0 && _audio) {
+ const uint slot = rec[9] & 0x7f;
+ _audio->playFloppyVoiceSlot(slot, _partner);
+ }
+
// Build the full text by concatenating each note's per-partner
// text string (parsed for `%s`-style placeholders). Bounds-
// check every offset against the mystery blob; if any byte
// looks out of range we just stop appending â corrupt offsets
// from a misparsed record would otherwise dereference past the
// buffer and SIGBUS inside `strlen`.
+ //
+ // Each text index also flags `_cluesFound[idx]` so the PDA /
+ // notebook ("DrawNotes") later renders the clue. Mirrors
+ // `FUN_22dc_05c8 @ 22dc:091a`:
+ // ((undefined1 *)&DAT_28da_3c08)[textIdx & 0x7f] = 1;
+ // `DAT_28da_3c08` is the floppy `TextSeen` array; we reuse the
+ // `_cluesFound` flag store since both index 0..127 by note id.
const uint32 dsz = _mystery.dataSize();
const uint32 notesBase = (uint32)(notes - bufBase);
Common::String raw;
for (uint t = 0; t < textCount; t++) {
const uint8 idx = rec[11 + t] & 0x7f;
+ if (idx < Mystery::kCluesFoundCap)
+ _mystery._cluesFound[idx] = 1;
const uint32 noteAbs = notesBase + (uint32)idx * 7;
if (noteAbs + 6 > dsz)
break;
@@ -975,6 +1001,23 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count) {
raw += ' ';
raw += Common::String(line, lineLen);
}
+
+ // Suspect/gallery side effect â `FUN_22dc_05c8 @ 22dc:08eb`:
+ // if ((rec[+9] & 0x80) == 0 && rec[+9] != 0)
+ // *(u16 *)(0x5d20 + table[0x2d65 + rec[+9]] * 2) = 1;
+ // Byte 9 doubles as the voice slot (when high bit set) or as a
+ // suspect identifier (low 7 bits, when high bit clear). The
+ // 0x5d20 array is the per-mystery "suspect found in gallery"
+ // flag table that `FUN_154e_0045` reads at gallery render time.
+ // We don't model the per-mystery shuffle table at 0x2d65
+ // (`_NewOrder` is set to identity in `_ReadMystery_Floppy` for
+ // our port), so byte9-1 indexes `_inGallery` directly.
+ const uint8 b9 = rec[9];
+ if ((b9 & 0x80) == 0 && b9 != 0) {
+ const uint slot = (uint)b9 - 1;
+ if (slot < Mystery::kGalleryCap)
+ _mystery._inGallery[slot] = 1;
+ }
const Common::String text = parseString(raw, _playerName, _partner);
// Restore briefing BG before drawing this bubble.
diff --git a/engines/eem/graphics.cpp b/engines/eem/graphics.cpp
index 33a3536c454..5af91578ac5 100644
--- a/engines/eem/graphics.cpp
+++ b/engines/eem/graphics.cpp
@@ -76,7 +76,211 @@ const BalloonInsets kBalloonInsetTable[] = {
{ 5, 8, 158 }, { 5, 8, 176 }, { 8, 7, 142 }
};
+// Floppy KDHelp hotspot-searched check. Mirrors
+// `FUN_22dc_096c @ 22dc:096c`: walks the per-site dialog records at
+// `site_data[+6]` to skip `hotspotIdx` hotspots, then returns the
+// `_cluesFound` flag for that hotspot's first text index.
+static bool floppyHotspotSearched(EEM::Mystery &mystery, uint siteIdx,
+ uint hotspotIdx) {
+ const byte *site = mystery.siteData(siteIdx);
+ if (!site)
+ return false;
+ const uint16 dlgListOff = READ_LE_UINT16(site + 6);
+ const byte *bufBase = mystery.blobAt(0);
+ if (!bufBase || dlgListOff == 0 || dlgListOff >= mystery.dataSize())
+ return false;
+ uint32 off = dlgListOff;
+ for (uint h = 0; h < hotspotIdx; h++) {
+ const byte *rec = bufBase + off;
+ off += 11u + (uint)rec[10];
+ if (off >= mystery.dataSize())
+ return false;
+ const uint contCount = (uint)(bufBase[off] & 0x7F);
+ off += 1;
+ for (uint c = 0; c < contCount; c++) {
+ const byte *cr = bufBase + off;
+ off += 11u + (uint)cr[10];
+ if (off >= mystery.dataSize())
+ return false;
+ }
+ }
+ if (off + 11 >= mystery.dataSize())
+ return false;
+ const byte *mainRec = bufBase + off;
+ const uint8 textIdx = mainRec[11] & 0x7F;
+ return textIdx < EEM::Mystery::kCluesFoundCap &&
+ mystery._cluesFound[textIdx] != 0;
+}
+
void EEMEngine::doHelp() {
+ // Floppy uses a totally different hint mechanism â per-mystery
+ // `H<n>.BIN` data files (one per case). Format verified at
+ // `FUN_1503_0001 @ 1503:0001` (loader, format string at
+ // `2608:0154` = "h%d.bin") + `FUN_1503_01a5 @ 1503:01a5`
+ // (consumer):
+ // byte 0 = numChainHints
+ // numChainHints à { byte siteIdx; byte hotspotIdx; }
+ // byte = numExtraHints
+ // numExtraHints à { byte siteIdx; byte hotspotIdx; }
+ // asciiz string 1 ("[balloon-digit]Let's go to <site>...")
+ // asciiz string 2 (alternate hint)
+ // asciiz string 3 (post-solve hint, used when score ⥠100)
+ // Selection logic: if any chain hotspot is unsearched â string 1.
+ // Else if any extra hotspot is unsearched â string 2. Else if
+ // `selectedPoints() ⥠100` â string 3.
+ if (isFloppy() && _mystery.isLoaded()) {
+ const Common::String filename = Common::String::format("H%u.BIN",
+ _mystery.number());
+ Common::File hf;
+ if (!hf.open(Common::Path(filename))) {
+ warning("doHelp: cannot open %s", filename.c_str());
+ return;
+ }
+ const uint32 hsz = hf.size();
+ Common::Array<byte> hbuf;
+ hbuf.resize(hsz);
+ if (hf.read(hbuf.data(), hsz) != hsz)
+ return;
+ const byte *hd = hbuf.data();
+
+ const uint chainCount = hd[0];
+ uint off = 1;
+ uint chainEnd = off + chainCount * 2;
+ if (chainEnd >= hsz)
+ return;
+ const uint extraCount = hd[chainEnd];
+ uint extraStart = chainEnd + 1;
+ uint extraEnd = extraStart + extraCount * 2;
+ if (extraEnd >= hsz)
+ return;
+ // Three NUL-terminated strings follow.
+ const char *str1 = (const char *)(hd + extraEnd);
+ const char *str2 = nullptr;
+ const char *str3 = nullptr;
+ const char *p = str1;
+ while ((uint)((const byte *)p - hd) < hsz && *p != 0) p++;
+ if ((uint)((const byte *)p - hd) >= hsz) return;
+ str2 = p + 1;
+ p = str2;
+ while ((uint)((const byte *)p - hd) < hsz && *p != 0) p++;
+ if ((uint)((const byte *)p - hd) >= hsz) return;
+ str3 = p + 1;
+
+ const char *chosen = nullptr;
+ bool anyChainUnseen = false;
+ for (uint i = 0; i < chainCount; i++) {
+ const uint8 siteIdx = hd[off + i * 2 + 0];
+ const uint8 hotspotIdx = hd[off + i * 2 + 1];
+ if (!floppyHotspotSearched(_mystery, siteIdx, hotspotIdx)) {
+ anyChainUnseen = true;
+ break;
+ }
+ }
+ bool anyExtraUnseen = false;
+ if (!anyChainUnseen) {
+ for (uint i = 0; i < extraCount; i++) {
+ const uint8 siteIdx = hd[extraStart + i * 2 + 0];
+ const uint8 hotspotIdx = hd[extraStart + i * 2 + 1];
+ if (!floppyHotspotSearched(_mystery, siteIdx, hotspotIdx)) {
+ anyExtraUnseen = true;
+ break;
+ }
+ }
+ }
+ if (anyChainUnseen)
+ chosen = str1;
+ else if (anyExtraUnseen)
+ chosen = str2;
+ else if (_mystery.selectedPoints() >= 100)
+ chosen = str3;
+ if (!chosen || *chosen == 0)
+ return;
+
+ // Strip leading balloon-digit byte. `_GetKDTextBalloon @
+ // 1df2:0105` (= floppy `FUN_1d40_009f`) doesn't take the
+ // digit's *value* â it indexes the per-character table at
+ // `2608:0c14` by the literal byte, so '0'..'9' map to a
+ // non-trivial balloon-id sequence. Verified bytes at
+ // `2608:0c44` (= 0xc14 + '0'):
+ // '0'â0x15, '1'â0x16, '2'â0x17, '3'â0x18, '4'â0x19,
+ // '5'â0x1a, '6'â0x1c, '7'â0x1d, '8'â0x1e, '9'â0x0a.
+ // Without this map the previous (digit - '0') version asked
+ // `getBalloonInsets` for balloon 0 (text width 142) instead of
+ // the correct balloon 21 (text width 155), which is why the
+ // hint bubble rendered narrower than the original.
+ static const uint8 kFloppyDigitToBalloon[10] = {
+ 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1c, 0x1d, 0x1e, 0x0a
+ };
+ uint balloonIdx = 0x17;
+ const char *txt = chosen;
+ if (*txt >= '0' && *txt <= '9') {
+ balloonIdx = kFloppyDigitToBalloon[(int)(*txt - '0')];
+ txt++;
+ }
+
+ Common::String text = parseString(Common::String(txt),
+ _playerName, _partner);
+ Graphics::ManagedSurface ms(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ ms.clear();
+ {
+ Graphics::Surface *cur = g_system->lockScreen();
+ if (cur) {
+ ms.simpleBlitFrom(*cur);
+ g_system->unlockScreen();
+ }
+ }
+ Picture balloon;
+ const bool haveBalloon = _balloonArchive.size() > balloonIdx &&
+ _balloonArchive.loadEntry(balloonIdx, balloon);
+ uint16 balloonY = 1;
+ if (haveBalloon) {
+ const uint h = (uint)balloon.surface.h;
+ if (h < 0x4e)
+ balloonY = (uint16)((0x50 - h) >> 1);
+ const byte transp = (byte)(balloon.flags >> 8);
+ for (int row = 0; row < balloon.surface.h && balloonY + row < 200;
+ row++) {
+ const byte *src =
+ (const byte *)balloon.surface.getBasePtr(0, row);
+ byte *dst = (byte *)ms.getBasePtr(0x21, balloonY + row);
+ for (int col = 0; col < balloon.surface.w && 0x21 + col < 320;
+ col++) {
+ if (src[col] != transp)
+ dst[col] = src[col];
+ }
+ }
+ }
+ uint16 bx = 5;
+ uint16 by = 4;
+ uint16 bw = 142;
+ getBalloonInsets(balloonIdx, bx, by, bw);
+ _font.drawWordWrapped(&ms, 0x21 + bx, balloonY + by,
+ MAX<int>(8, (int)bw), text, 0);
+ g_system->copyRectToScreen(ms.getPixels(), ms.pitch, 0, 0, 320, 200);
+ g_system->updateScreen();
+
+ // Wait for click â KD hint dismisses on any input.
+ while (!shouldQuit()) {
+ Common::Event ev;
+ bool advance = false;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER ||
+ ev.type == Common::EVENT_LBUTTONDOWN ||
+ ev.type == Common::EVENT_KEYDOWN) {
+ advance = true;
+ break;
+ }
+ }
+ if (advance)
+ break;
+ g_system->updateScreen();
+ g_system->delayMillis(10);
+ }
+ return;
+ }
+
// Mirrors `_KDHelp @ 1560:010a`. The original walks the first two
// entries of `_AChain` (the puzzle's required-clue chain â the
// "spine" of evidence the player must collect):
diff --git a/engines/eem/mystery.cpp b/engines/eem/mystery.cpp
index 98f48a1d78e..781d81895a0 100644
--- a/engines/eem/mystery.cpp
+++ b/engines/eem/mystery.cpp
@@ -359,9 +359,14 @@ uint16 Mystery::noteIndexCount() const {
if (!isLoaded())
return 0;
// NoteIndex runs from _noteOffset to the start of GalleryData.
+ // CD entries are 4 bytes (`u16 textOff; u16 points`); floppy
+ // entries are 7 bytes (`u16 ?; u16 jakeOff; u16 jennyOff; u8
+ // score`) â verified at `FUN_22dc_05c8 @ 22dc:0843` reading
+ // `*(int *)(notes + idx*7 + 2)` (Jake) / `+4` (Jenny).
if (_galleryOffset <= _noteOffset)
return 0;
- return (uint16)((_galleryOffset - _noteOffset) / 4);
+ const uint stride = _isFloppy ? 7 : 4;
+ return (uint16)((_galleryOffset - _noteOffset) / stride);
}
const byte *Mystery::kdTextIndex() const {
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index a7589a566f6..42321a6c626 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -1934,17 +1934,41 @@ void EEMEngine::drawNotebookFrame(int &page) {
int clueCursor = 0;
Common::Array<int> pageStarts;
pageStarts.push_back(0);
+ // Floppy NoteIndex entries are 7 bytes (`u16 ?; u16 jakeOff; u16
+ // jennyOff; u8 score`) with ABSOLUTE byte offsets into the mystery
+ // blob, while CD entries are 4 bytes with offsets relative to the
+ // TextBlock at header[+0xc]. Resolve the right text for the active
+ // partner / variant once per render.
+ const bool floppyNb = isFloppy();
+ const byte *bufBase = _mystery.blobAt(0);
+ const uint32 mysSz = _mystery.dataSize();
+ auto noteText = [&](uint clueId) -> Common::String {
+ if (!ni || clueId >= niCount)
+ return Common::String();
+ if (floppyNb && bufBase) {
+ const uint stride = 7;
+ const uint16 textOff = (_partner == 0)
+ ? READ_LE_UINT16(ni + clueId * stride + 2)
+ : READ_LE_UINT16(ni + clueId * stride + 4);
+ if (textOff == 0 || textOff >= mysSz)
+ return Common::String();
+ const char *p = (const char *)(bufBase + textOff);
+ uint32 len = 0;
+ while (textOff + len < mysSz && p[len] != 0)
+ len++;
+ return parseString(Common::String(p, len),
+ _playerName, _partner);
+ }
+ const uint16 textOff = READ_LE_UINT16(ni + clueId * 4);
+ return parseString(_mystery.textAt(textOff),
+ _playerName, _partner);
+ };
{
const int lineH = _font.getFontHeight() + 1;
int y = kRectY;
while (clueCursor < (int)found.size()) {
const uint clueId = found[clueCursor];
- Common::String txt;
- if (ni && clueId < niCount) {
- const uint16 textOff = READ_LE_UINT16(ni + clueId * 4);
- txt = parseString(_mystery.textAt(textOff),
- _playerName, _partner);
- }
+ Common::String txt = noteText(clueId);
// Measure height by wrapping the text without drawing.
Common::Array<Common::String> wrapped;
_font.wordWrapText(txt, kRectW, wrapped);
@@ -1976,12 +2000,7 @@ void EEMEngine::drawNotebookFrame(int &page) {
int y = kRectY;
for (int i = startClue; i < endClue; i++) {
const uint clueId = found[i];
- Common::String txt;
- if (ni && clueId < niCount) {
- const uint16 textOff = READ_LE_UINT16(ni + clueId * 4);
- txt = parseString(_mystery.textAt(textOff),
- _playerName, _partner);
- }
+ Common::String txt = noteText(clueId);
if (txt.empty())
txt = Common::String::format("clue %u", clueId);
// Per `_DrawNotes @ 161e:01d0`: text uses
Commit: 4992f3c7045f98da9019e034cdab0719746cbeb8
https://github.com/scummvm/scummvm/commit/4992f3c7045f98da9019e034cdab0719746cbeb8
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:52+02:00
Commit Message:
EEM: text pagination for some conversations
Changed paths:
engines/eem/clues.cpp
engines/eem/font.cpp
engines/eem/font.h
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index d53fb7f57ae..645d6086112 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -589,6 +589,19 @@ Common::String EEMEngine::parseString(const Common::String &raw,
return out;
case '\r':
break;
+ case '^':
+ // `_WordWrap @ 1b03:0456` treats `^` as a forced line
+ // break (sets `cur_width = max_width`, forcing the next
+ // loop turn to wrap at the previous space and skip the
+ // `^` itself). Without this conversion the caret falls
+ // through the default case and renders as a literal,
+ // pushing the line past the balloon's visual edge â the
+ // "bubbles aren't large enough" symptom. Promote it to
+ // `\n` so ScummVM's `Font::wordWrapText` (which honours
+ // embedded newlines) picks the same break point the
+ // original engine did.
+ out += '\n';
+ break;
default:
out += (char)c;
break;
@@ -939,6 +952,39 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count) {
}
}
+ const uint32 dsz = _mystery.dataSize();
+ const uint32 notesBase = (uint32)(notes - bufBase);
+
+ auto waitForClick = [&]() -> bool {
+ // Drain pending events first so a previous keystroke's tail
+ // doesn't auto-advance the new page.
+ Common::Event drain;
+ while (g_system->getEventManager()->pollEvent(drain)) {}
+ const uint32 minVisibleMs = 250;
+ const uint32 startedAt = g_system->getMillis();
+ while (!shouldQuit()) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return true; // skip
+ if (g_system->getMillis() - startedAt < minVisibleMs)
+ continue;
+ if (ev.type == Common::EVENT_KEYDOWN &&
+ ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ interruptAudio();
+ return true; // skip
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN ||
+ ev.type == Common::EVENT_KEYDOWN)
+ return false; // advance one page
+ }
+ g_system->updateScreen();
+ g_system->delayMillis(10);
+ }
+ return true;
+ };
+
for (uint i = 0; i < count && !shouldQuit(); i++) {
const uint16 picID = READ_LE_UINT16(rec + 0);
const uint16 picX = READ_LE_UINT16(rec + 2);
@@ -948,102 +994,22 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count) {
const uint8 ballY = rec[8];
const uint8 textCount= rec[10];
- // Per-record voice playback. `FUN_22dc_05c8 @ 22dc:05c8`:
- // if ((rec[+9] & 0x80) && voiceOption && voiceAvail) {
- // slot = rec[+9] & 0x7f;
- // _LoadSoundName_Floppy(slot); _PlayVoc(...);
- // }
- // The slot indexes the per-partner table at `2608:0f0e` /
- // `2608:0f76` (= our `playFloppyVoiceSlot`). Sound is started
- // BEFORE the bubble renders so it plays while the player reads.
+ // Per-record voice (byte 9 high bit) â see comment in original
+ // header.
if ((rec[9] & 0x80) != 0 && _audio) {
const uint slot = rec[9] & 0x7f;
_audio->playFloppyVoiceSlot(slot, _partner);
}
-
- // Build the full text by concatenating each note's per-partner
- // text string (parsed for `%s`-style placeholders). Bounds-
- // check every offset against the mystery blob; if any byte
- // looks out of range we just stop appending â corrupt offsets
- // from a misparsed record would otherwise dereference past the
- // buffer and SIGBUS inside `strlen`.
- //
- // Each text index also flags `_cluesFound[idx]` so the PDA /
- // notebook ("DrawNotes") later renders the clue. Mirrors
- // `FUN_22dc_05c8 @ 22dc:091a`:
- // ((undefined1 *)&DAT_28da_3c08)[textIdx & 0x7f] = 1;
- // `DAT_28da_3c08` is the floppy `TextSeen` array; we reuse the
- // `_cluesFound` flag store since both index 0..127 by note id.
- const uint32 dsz = _mystery.dataSize();
- const uint32 notesBase = (uint32)(notes - bufBase);
- Common::String raw;
- for (uint t = 0; t < textCount; t++) {
- const uint8 idx = rec[11 + t] & 0x7f;
- if (idx < Mystery::kCluesFoundCap)
- _mystery._cluesFound[idx] = 1;
- const uint32 noteAbs = notesBase + (uint32)idx * 7;
- if (noteAbs + 6 > dsz)
- break;
- const uint16 textOff = (_partner == 0)
- ? READ_LE_UINT16(notes + idx * 7 + 2)
- : READ_LE_UINT16(notes + idx * 7 + 4);
- if (textOff >= dsz)
- break;
- const char *line = (const char *)(bufBase + textOff);
- // Find the NUL terminator within the blob â refuse to
- // strlen past the end.
- uint32 lineLen = 0;
- while (textOff + lineLen < dsz && line[lineLen] != 0)
- lineLen++;
- if (textOff + lineLen >= dsz)
- break;
- if (t > 0)
- raw += ' ';
- raw += Common::String(line, lineLen);
- }
-
- // Suspect/gallery side effect â `FUN_22dc_05c8 @ 22dc:08eb`:
- // if ((rec[+9] & 0x80) == 0 && rec[+9] != 0)
- // *(u16 *)(0x5d20 + table[0x2d65 + rec[+9]] * 2) = 1;
- // Byte 9 doubles as the voice slot (when high bit set) or as a
- // suspect identifier (low 7 bits, when high bit clear). The
- // 0x5d20 array is the per-mystery "suspect found in gallery"
- // flag table that `FUN_154e_0045` reads at gallery render time.
- // We don't model the per-mystery shuffle table at 0x2d65
- // (`_NewOrder` is set to identity in `_ReadMystery_Floppy` for
- // our port), so byte9-1 indexes `_inGallery` directly.
+ // Suspect-found side effect for byte 9 with high bit clear.
const uint8 b9 = rec[9];
if ((b9 & 0x80) == 0 && b9 != 0) {
const uint slot = (uint)b9 - 1;
if (slot < Mystery::kGalleryCap)
_mystery._inGallery[slot] = 1;
}
- const Common::String text = parseString(raw, _playerName, _partner);
-
- // Restore briefing BG before drawing this bubble.
- g_system->copyRectToScreen(bg.getPixels(), bg.pitch, 0, 0, 320, 200);
-
- // Optional character portrait.
- if (picID != 0 && picID != 0xFFFF) {
- Picture pic;
- if (_picsArchive.getPicture(picID, pic))
- blitAt(pic, picX, picY);
- }
-
- // Compose balloon + text on a scratch surface.
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- {
- Graphics::Surface *screen = g_system->lockScreen();
- if (screen) {
- for (int row = 0; row < 200; row++) {
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)screen->getBasePtr(0, row), 320);
- }
- g_system->unlockScreen();
- }
- }
+ // Pre-load balloon picture + insets once per record (constant
+ // across all paginated text indices).
Picture balloon;
const uint16 balloonId = balByte & 0x7F;
const bool flipBall = (balByte & 0x80) != 0;
@@ -1053,81 +1019,134 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count) {
uint16 textWidth = 142;
uint16 textXIns = 6;
uint16 textYIns = 4;
- if (haveBalloon) {
- const int bw = MIN<int>(balloon.surface.w, 320 - ballX);
- const int bh = MIN<int>(balloon.surface.h, 200 - ballY);
- const byte transp = (byte)(balloon.flags >> 8);
- for (int row = 0; row < bh; row++) {
- const byte *src =
- (const byte *)balloon.surface.getBasePtr(0, row);
- byte *dst =
- (byte *)scratch.getBasePtr(ballX, ballY + row);
- for (int col = 0; col < bw; col++) {
- const int srcCol = flipBall
- ? (balloon.surface.w - 1 - col) : col;
- const byte px = src[srcCol];
- if (px != transp)
- dst[col] = px;
- }
- }
+ if (haveBalloon)
getBalloonInsets(balloonId, textXIns, textYIns, textWidth);
- }
-
const int textX = ballX + textXIns;
- const int textY = ballY + textYIns;
- _font.drawWordWrapped(&scratch, textX, textY,
- MAX<int>(8, (int)textWidth), text, 0);
+ const int balloonH = haveBalloon ? balloon.surface.h : 200;
+ const int lineH = _font.getFontHeight() + 1;
+
+ // Pagination state â `FUN_22dc_05c8`'s text-idx loop uses
+ // `local_1c` (set from the PREVIOUS text's flag bit) to decide
+ // between "fresh page" (redraw balloon, restart Y at top) and
+ // "continuation" (append below previous lines). We mirror that
+ // state machine so multi-text records render the same way the
+ // original does â without it our impl concatenates every text
+ // idx into ONE balloon and the result spills out the bottom
+ // (the user's "bubbles aren't large enough" screenshot).
+ bool firstPage = true;
+ int cursorY = ballY + textYIns;
+ bool skipAll = false;
+
+ for (uint t = 0; t < textCount && !shouldQuit() && !skipAll; t++) {
+ const uint8 idxByte = rec[11 + t];
+ const uint8 idx = idxByte & 0x7f;
+ if (idx < Mystery::kCluesFoundCap)
+ _mystery._cluesFound[idx] = 1;
+ const uint32 noteAbs = notesBase + (uint32)idx * 7;
+ if (noteAbs + 6 > dsz)
+ break;
+ const uint16 textOff = (_partner == 0)
+ ? READ_LE_UINT16(notes + idx * 7 + 2)
+ : READ_LE_UINT16(notes + idx * 7 + 4);
+ if (textOff >= dsz)
+ break;
+ const char *linePtr = (const char *)(bufBase + textOff);
+ uint32 lineLen = 0;
+ while (textOff + lineLen < dsz && linePtr[lineLen] != 0)
+ lineLen++;
+ Common::String raw(linePtr, lineLen);
+ const Common::String text =
+ parseString(raw, _playerName, _partner);
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
+ // Render this text page.
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.simpleBlitFrom(*bg.surfacePtr());
+
+ if (firstPage) {
+ // Optional character portrait â only draws once per
+ // fresh page (matches the original which only redraws
+ // the balloon on `local_1c == 0`).
+ if (picID != 0 && picID != 0xFFFF) {
+ Picture pic;
+ if (_picsArchive.getPicture(picID, pic)) {
+ const byte transpC = (byte)(pic.flags >> 8);
+ const int pw = MIN<int>(pic.surface.w, 320 - picX);
+ const int ph = MIN<int>(pic.surface.h, 200 - picY);
+ for (int row = 0; row < ph; row++) {
+ const byte *src = (const byte *)
+ pic.surface.getBasePtr(0, row);
+ byte *dst = (byte *)
+ scratch.getBasePtr(picX, picY + row);
+ for (int col = 0; col < pw; col++) {
+ if (src[col] != transpC)
+ dst[col] = src[col];
+ }
+ }
+ }
+ }
+ if (haveBalloon) {
+ const int bw = MIN<int>(balloon.surface.w, 320 - ballX);
+ const int bh = MIN<int>(balloonH, 200 - ballY);
+ const byte transp = (byte)(balloon.flags >> 8);
+ for (int row = 0; row < bh; row++) {
+ const byte *src = (const byte *)
+ balloon.surface.getBasePtr(0, row);
+ byte *dst = (byte *)
+ scratch.getBasePtr(ballX, ballY + row);
+ for (int col = 0; col < bw; col++) {
+ const int srcCol = flipBall
+ ? (balloon.surface.w - 1 - col)
+ : col;
+ const byte px = src[srcCol];
+ if (px != transp)
+ dst[col] = px;
+ }
+ }
+ }
+ cursorY = ballY + textYIns;
+ }
- // Drain pending events from the previous bubble (or upstream
- // animation skip) so a single Enter press doesn't burst-advance
- // through multiple bubbles. Without this, key-repeat or stacked
- // KEYUP/KEYDOWN events from one keystroke could roll past
- // several records before the user could even read them.
- {
- Common::Event drain;
- while (g_system->getEventManager()->pollEvent(drain)) {}
- }
- // Minimum visible time per bubble â guards against accidental
- // rapid-fire advances from accumulated input.
- const uint32 minVisibleMs = 250;
- const uint32 startedAt = g_system->getMillis();
+ // Wrap text into lines and draw each at cursorY.
+ Common::Array<Common::String> lines;
+ _font.wordWrapText(text, MAX<int>(8, (int)textWidth), lines);
+ for (uint l = 0; l < lines.size(); l++) {
+ _font.drawString(&scratch, lines[l], textX,
+ cursorY + (int)l * lineH,
+ MAX<int>(8, (int)textWidth), 0);
+ }
+ cursorY += (int)lines.size() * lineH;
- // Wait for click / key. ESC skips the rest of the briefing.
- // Inside the loop we ignore input until at least `minVisibleMs`
- // has elapsed since the bubble was drawn â combined with the
- // pre-loop event drain, this prevents one keystroke (or its
- // auto-repeat tail) from advancing several bubbles back to back.
- bool advance = false;
- bool skipAll = false;
- while (!advance && !shouldQuit()) {
- Common::Event ev;
- while (g_system->getEventManager()->pollEvent(ev)) {
- if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
- advance = true;
- break;
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+
+ // Decide pagination for the NEXT text idx based on THIS
+ // text's high bit.
+ const bool textHighBit = (idxByte & 0x80) != 0;
+ const bool isLastText = (t + 1 == textCount);
+
+ if (!isLastText) {
+ if (textHighBit) {
+ // Continuation flag â next text appends below
+ // without waiting.
+ firstPage = false;
+ } else {
+ // New page next: wait for click, then redraw
+ // balloon for the next text.
+ if (waitForClick()) {
+ skipAll = true;
+ break;
+ }
+ firstPage = true;
}
- if (g_system->getMillis() - startedAt < minVisibleMs)
- continue;
- if (ev.type == Common::EVENT_KEYDOWN &&
- ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
- advance = true;
+ } else {
+ // Last text in record â wait for the user's click
+ // before moving on (mirrors the caller's
+ // `FUN_16e2_1a7f()` after every `FUN_22dc_05c8` call).
+ if (waitForClick())
skipAll = true;
- interruptAudio();
- break;
- }
- if (ev.type == Common::EVENT_LBUTTONDOWN ||
- ev.type == Common::EVENT_KEYDOWN) {
- advance = true;
- break;
- }
}
- g_system->updateScreen();
- g_system->delayMillis(10);
}
if (skipAll)
return;
diff --git a/engines/eem/font.cpp b/engines/eem/font.cpp
index 4d00ab79fb1..c8c42360550 100644
--- a/engines/eem/font.cpp
+++ b/engines/eem/font.cpp
@@ -80,6 +80,7 @@ bool EEMFont::load(const Common::Path &path) {
}
_glyphs.resize(numChars);
_maxHeight = _maxWidth = 0;
+ _lineHeight = 0;
for (uint i = 0; i < numChars; i++) {
FontGlyph &g = _glyphs[i];
@@ -99,6 +100,19 @@ bool EEMFont::load(const Common::Path &path) {
_maxWidth = g.widthBits;
}
+ // `_LoadFont @ 1b03:0220` sets the line stride to the FIRST
+ // glyph's height (= the space character) â `DAT_28da_30ca =
+ // DAT_28da_30ce` (the first byte of glyph 0 = its height). Tall
+ // descender glyphs ('g', 'j', 'p', 'q', 'y') hang into the next
+ // row by design, which is how the original engine keeps balloon
+ // text dense enough to fit. Using `_maxHeight` (the descender-
+ // inflated value) added 2â3 px per line and made every multi-
+ // line bubble overflow its graphic vertically â verbatim user-
+ // reported "bubbles aren't large enough" symptom.
+ _lineHeight = !_glyphs.empty() ? _glyphs[0].height : _maxHeight;
+ if (_lineHeight == 0)
+ _lineHeight = _maxHeight;
+
debugC(1, kDebugGfx, "Font %s loaded: %u glyphs, max %ux%u",
path.toString().c_str(), numChars, _maxWidth, _maxHeight);
return true;
diff --git a/engines/eem/font.h b/engines/eem/font.h
index 6ccfd6d4946..ae5a0638d7b 100644
--- a/engines/eem/font.h
+++ b/engines/eem/font.h
@@ -61,7 +61,9 @@ public:
bool isLoaded() const { return !_glyphs.empty(); }
// --- Graphics::Font overrides ---
- int getFontHeight() const override { return _maxHeight; }
+ int getFontHeight() const override {
+ return _lineHeight ? _lineHeight : _maxHeight;
+ }
int getMaxCharWidth() const override { return _maxWidth; }
int getCharWidth(uint32 chr) const override;
void drawChar(Graphics::Surface *dst, uint32 chr, int x, int y,
@@ -76,8 +78,9 @@ public:
private:
Common::Array<FontGlyph> _glyphs;
- uint16 _maxHeight = 0;
- uint16 _maxWidth = 0;
+ uint16 _maxHeight = 0;
+ uint16 _maxWidth = 0;
+ uint16 _lineHeight = 0; ///< First glyph height (= original line stride)
};
} // End of namespace EEM
Commit: fe87003b43856887061690a2c8fd6e6d919bf733
https://github.com/scummvm/scummvm/commit/fe87003b43856887061690a2c8fd6e6d919bf733
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:53+02:00
Commit Message:
EEM: draw enter indicators for conversations in floppy version
Changed paths:
engines/eem/clues.cpp
engines/eem/eem.h
engines/eem/graphics.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 645d6086112..2034ed40127 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -913,7 +913,8 @@ void EEMEngine::displayClue(const byte *clueBlock) {
}
}
-void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count) {
+void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
+ uint lastIndicator) {
// Render `count` consecutive floppy dialog records starting at
// `rec`. Per `FUN_22dc_05c8 @ 22dc:05c8`, each record is:
// u16 picID @ +0 (character portrait, 0 = skip pic)
@@ -1117,35 +1118,68 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count) {
}
cursorY += (int)lines.size() * lineH;
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
-
// Decide pagination for the NEXT text idx based on THIS
// text's high bit.
const bool textHighBit = (idxByte & 0x80) != 0;
const bool isLastText = (t + 1 == textCount);
-
+ const bool isLastRec = (i + 1 == count);
+
+ // Stamp the "click to continue" indicator (PIC 0xa0
+ // "more" arrow / PIC 0xa1 end indicator) before
+ // flipping to wait. Mirrors `_DisplayHotspotClue_Floppy
+ // @ 22dc:08aa` (mid-page) and `@ 22dc:08c0`
+ // (end-of-record). We skip drawing it on the very last
+ // click of the very last record when the caller passes
+ // `lastIndicator == 0` â that's the original `param_2 ==
+ // 0` "no indicator" case.
+ bool waitNeeded = false;
+ bool drawArrow = false;
+ bool useEndPic = false;
if (!isLastText) {
if (textHighBit) {
- // Continuation flag â next text appends below
- // without waiting.
+ // continuation â no wait, no indicator
firstPage = false;
} else {
- // New page next: wait for click, then redraw
- // balloon for the next text.
- if (waitForClick()) {
- skipAll = true;
- break;
- }
- firstPage = true;
+ waitNeeded = true;
+ drawArrow = true; // PIC 0xa0
+ useEndPic = false;
}
} else {
- // Last text in record â wait for the user's click
- // before moving on (mirrors the caller's
- // `FUN_16e2_1a7f()` after every `FUN_22dc_05c8` call).
- if (waitForClick())
+ // Last text in this record.
+ waitNeeded = true;
+ if (!isLastRec) {
+ // More records follow â original passes
+ // `param_2 = 1` â PIC 0xa0.
+ drawArrow = true;
+ useEndPic = false;
+ } else {
+ // Last record. Use caller-supplied indicator.
+ if (lastIndicator == 1) {
+ drawArrow = true;
+ useEndPic = false;
+ } else if (lastIndicator == 2) {
+ drawArrow = true;
+ useEndPic = true; // PIC 0xa1
+ }
+ }
+ }
+
+ if (drawArrow) {
+ drawFloppyBubbleIndicator(scratch, balloonId, ballX,
+ ballY, useEndPic);
+ }
+
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+
+ if (waitNeeded) {
+ if (waitForClick()) {
skipAll = true;
+ break;
+ }
+ if (!isLastText && !textHighBit)
+ firstPage = true;
}
}
if (skipAll)
@@ -1234,11 +1268,23 @@ void EEMEngine::displayFloppyHotspotDialog(uint siteNum, uint hotIdx) {
}
}
const byte *mainRec = bufBase + off;
- displayFloppyDialogRecords(mainRec, 1);
const uint mainLen = 11u + (uint)mainRec[10];
- if (off + mainLen >= _mystery.dataSize())
- return;
- const uint contCount = (uint)(bufBase[off + mainLen] & 0x7F);
+ uint contCount = 0;
+ uint contFlagsByte = 0;
+ if (off + mainLen < _mystery.dataSize()) {
+ contFlagsByte = bufBase[off + mainLen];
+ contCount = contFlagsByte & 0x7F;
+ }
+ // `_HandleHotspotClick_Floppy @ 1652:00e6` derives the main
+ // record's `param_2` from the continuation byte:
+ // contFlagsByte == 0 â 0 (no indicator, only one record)
+ // high bit set â 1 (PIC 0xa0, "more" arrow)
+ // low 7 bits non-zero â 2 (PIC 0xa1, alternate end)
+ uint mainIndicator = 0;
+ if (contFlagsByte != 0) {
+ mainIndicator = (contFlagsByte & 0x80) ? 1 : 2;
+ }
+ displayFloppyDialogRecords(mainRec, 1, mainIndicator);
if (contCount == 0)
return;
const uint32 contOff = off + mainLen + 1;
@@ -1251,7 +1297,13 @@ void EEMEngine::displayFloppyHotspotDialog(uint siteNum, uint hotIdx) {
g_system->copyRectToScreen(siteBG.getPixels(), siteBG.pitch,
0, 0, 320, 200);
g_system->updateScreen();
- displayFloppyDialogRecords(bufBase + contOff, contCount);
+ // Continuation chain: `_DisplayDialogContinuations_Floppy @
+ // 1652:006c` passes `param_2 + -1 != 0` (= 1 if more records
+ // follow, 0 on the last). Our renderer maps that automatically
+ // because mid-batch records always get PIC 0xa0; the last record
+ // uses our `lastIndicator` argument (= 0, no indicator on the
+ // final continuation).
+ displayFloppyDialogRecords(bufBase + contOff, contCount, 0);
}
bool EEMEngine::areYouSure() {
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 0b926e5655b..4be4198ad29 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -266,11 +266,24 @@ public:
void setPartnerEraseBg(const Graphics::ManagedSurface *bg);
/// Look up balloon-text-inset metadata. Mirrors the 52-entry table at
- /// `29be:0875`, indexed by `(bubNum & 0x7F)`. 10 bytes per entry; only
- /// the first 3 fields (x inset, y inset, text width) are used for
- /// rendering. Returns false if `bubNum` is outside the table.
+ /// `29be:0875` (CD) / `2608:05f9` (floppy), indexed by
+ /// `(bubNum & 0x7F)`. 10 bytes per entry; the first 3 fields
+ /// (x inset, y inset, text width) are used for text wrap, the last
+ /// 2 (indicator dX/dY) by `drawFloppyBubbleIndicator`. Returns
+ /// false if `bubNum` is outside the table.
bool getBalloonInsets(uint16 bubNum, uint16 &xInset, uint16 &yInset,
uint16 &textW) const;
+ bool getBalloonIndicatorPos(uint16 bubNum, uint16 &dx,
+ uint16 &dy) const;
+
+ /// Stamp the floppy "click to continue" indicator (PIC 0xa0 for
+ /// `endIndicator==false`, PIC 0xa1 otherwise) onto @p dst at the
+ /// position derived from the balloon-inset table. Mirrors
+ /// `FUN_22dc_05c8 @ 22dc:08aa` (mid-page) and `@ 22dc:08c0`
+ /// (end-of-record).
+ void drawFloppyBubbleIndicator(Graphics::ManagedSurface &dst,
+ uint16 bubNum, int ballX, int ballY,
+ bool endIndicator);
/// "Are you sure?" yes/no dialog. Mirrors `_AreYouSure` @ 1a35:0a5c.
/// Returns true if the user picked YES.
@@ -449,8 +462,13 @@ private:
/// Render `count` consecutive floppy dialog records starting at
/// `rec`. Shared between briefing and hotspot click handlers since
/// the original engine uses the same `FUN_22dc_05c8 @ 22dc:05c8`
- /// renderer in both contexts.
- void displayFloppyDialogRecords(const byte *rec, uint count);
+ /// renderer in both contexts. `lastIndicator` is the `param_2`
+ /// equivalent for the LAST record in the batch â 0 = no
+ /// indicator, 1 = PIC 0xa0 (red "more" arrow), 2 = PIC 0xa1
+ /// (alternate end indicator). Records before the last always get
+ /// PIC 0xa0.
+ void displayFloppyDialogRecords(const byte *rec, uint count,
+ uint lastIndicator = 0);
public:
/// Mirrors `_StartTravelMusic @ 20a2:0595`. Picks `MUS%05d.XMI`
diff --git a/engines/eem/graphics.cpp b/engines/eem/graphics.cpp
index 5af91578ac5..4b9eae7cbf8 100644
--- a/engines/eem/graphics.cpp
+++ b/engines/eem/graphics.cpp
@@ -51,29 +51,38 @@ const uint16 kHelpPics[][2] = {
{ 0x0192, 0x01b1 },
};
-// 52-entry, 10-bytes-each balloon-metadata table at `29be:0875`.
-// Used at 1df2:0aef-0af9 (accuse hint) and `_DisplayClue` to position
-// `_WordWrap` text inside the balloon. Only +0/+2/+4 are read by
-// `getBalloonInsets`:
-// +0..1 = text X inset, +2..3 = Y inset, +4..5 = max wrap width
-// (+6/+8 = balloon h / tail offset, both unused for text layout).
-struct BalloonInsets { uint16 x; uint16 y; uint16 w; };
+// 52-entry, 10-bytes-each balloon-metadata table at `29be:0875` (CD)
+// / `2608:05f9` (floppy). Used by `_DisplayClue` to position
+// `_WordWrap` text inside the balloon AND to position the
+// "click to continue" indicator drawn by
+// `_DisplayHotspotClue_Floppy @ 22dc:05c8`. Layout per entry:
+// +0..1 = text X inset, +2..3 = text Y inset
+// +4..5 = wrap width
+// +6..7 = "more / end" indicator X within the balloon
+// +8..9 = "more / end" indicator Y within the balloon
+struct BalloonInsets {
+ uint16 x;
+ uint16 y;
+ uint16 w;
+ uint16 indDX;
+ uint16 indDY;
+};
const BalloonInsets kBalloonInsetTable[] = {
- { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 },
- { 6, 4, 142 }, { 6, 4, 142 }, { 6, 4, 142 },
- { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 },
- { 6, 4, 224 }, { 6, 4, 224 }, { 6, 4, 224 },
- { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 },
- { 6, 4, 291 }, { 6, 4, 291 }, { 6, 4, 291 },
- { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 },
- { 5, 4, 155 }, { 5, 4, 155 }, { 5, 4, 155 },
- { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 },
- { 5, 4, 237 }, { 5, 4, 237 }, { 5, 4, 237 },
- { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 },
- { 3, 4, 155 }, { 3, 4, 155 }, { 3, 4, 155 },
- { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 },
- { 5, 4, 238 }, { 5, 4, 238 }, { 5, 4, 238 },
- { 5, 8, 158 }, { 5, 8, 176 }, { 8, 7, 142 }
+ { 6, 4, 142, 150, 6 }, { 6, 4, 142, 150, 14 }, { 6, 4, 142, 150, 22 }, { 6, 4, 142, 150, 40 },
+ { 6, 4, 142, 150, 50 }, { 6, 4, 142, 150, 70 }, { 6, 4, 142, 150, 86 },
+ { 6, 4, 224, 233, 6 }, { 6, 4, 224, 233, 14 }, { 6, 4, 224, 233, 23 }, { 6, 4, 224, 233, 41 },
+ { 6, 4, 224, 233, 59 }, { 6, 4, 224, 233, 68 }, { 6, 4, 224, 233, 77 },
+ { 6, 4, 291, 300, 6 }, { 6, 4, 291, 300, 14 }, { 6, 4, 291, 300, 23 }, { 6, 4, 291, 300, 32 },
+ { 6, 4, 291, 300, 50 }, { 6, 4, 291, 300, 69 }, { 6, 4, 291, 300, 77 },
+ { 5, 4, 155, 150, 14 }, { 5, 4, 155, 150, 25 }, { 5, 4, 155, 150, 35 }, { 5, 4, 155, 150, 52 },
+ { 5, 4, 155, 150, 61 }, { 5, 4, 155, 150, 78 }, { 5, 4, 155, 150, 81 },
+ { 5, 4, 237, 233, 15 }, { 5, 4, 237, 233, 24 }, { 5, 4, 237, 233, 42 }, { 5, 4, 237, 233, 50 },
+ { 5, 4, 237, 233, 69 }, { 5, 4, 237, 233, 80 }, { 5, 4, 237, 233, 80 },
+ { 3, 4, 155, 150, 15 }, { 3, 4, 155, 150, 24 }, { 3, 4, 155, 150, 34 }, { 3, 4, 155, 150, 40 },
+ { 3, 4, 155, 150, 61 }, { 3, 4, 155, 150, 82 }, { 3, 4, 155, 150, 88 },
+ { 5, 4, 238, 232, 16 }, { 5, 4, 238, 232, 25 }, { 5, 4, 238, 232, 34 }, { 5, 4, 238, 232, 51 },
+ { 5, 4, 238, 232, 66 }, { 5, 4, 238, 232, 71 }, { 5, 4, 238, 232, 95 },
+ { 5, 8, 158, 160, 45 }, { 5, 8, 176, 176, 50 }, { 8, 7, 142, 142, 71 }
};
// Floppy KDHelp hotspot-searched check. Mirrors
@@ -569,4 +578,48 @@ bool EEMEngine::getBalloonInsets(uint16 bubNum, uint16 &xInset,
return true;
}
+bool EEMEngine::getBalloonIndicatorPos(uint16 bubNum, uint16 &dx,
+ uint16 &dy) const {
+ const uint idx = bubNum & 0x7F;
+ if (idx >= ARRAYSIZE(kBalloonInsetTable))
+ return false;
+ dx = kBalloonInsetTable[idx].indDX;
+ dy = kBalloonInsetTable[idx].indDY;
+ return true;
+}
+
+void EEMEngine::drawFloppyBubbleIndicator(Graphics::ManagedSurface &dst,
+ uint16 bubNum, int ballX, int ballY,
+ bool endIndicator) {
+ // Mirrors `_DisplayHotspotClue_Floppy @ 22dc:08c0` (end-of-record)
+ // and `@ 22dc:08aa` (mid-pagination). Both grab a pre-loaded PIC
+ // (`DAT_28da_3034 = PIC 0xa0` for "more pages",
+ // `DAT_28da_3030 = PIC 0xa1` for "end indicator") and stamp it at
+ // `(ballX + insetTable[bubNum].indDX,
+ // ballY + insetTable[bubNum].indDY)` via `_AddPicBackground`.
+ uint16 dx = 0;
+ uint16 dy = 0;
+ if (!getBalloonIndicatorPos(bubNum, dx, dy))
+ return;
+ const uint picId = endIndicator ? 0xa1 : 0xa0;
+ Picture pic;
+ if (!_picsArchive.getPicture(picId, pic))
+ return;
+ const int x = ballX + (int)dx;
+ const int y = ballY + (int)dy;
+ const byte transp = (byte)(pic.flags >> 8);
+ const int pw = MIN<int>(pic.surface.w, 320 - x);
+ const int ph = MIN<int>(pic.surface.h, 200 - y);
+ if (x < 0 || y < 0 || pw <= 0 || ph <= 0)
+ return;
+ for (int row = 0; row < ph; row++) {
+ const byte *src = (const byte *)pic.surface.getBasePtr(0, row);
+ byte *out = (byte *)dst.getBasePtr(x, y + row);
+ for (int col = 0; col < pw; col++) {
+ if (src[col] != transp)
+ out[col] = src[col];
+ }
+ }
+}
+
} // End of namespace EEM
Commit: bfdd2c33ecfb0363422d914db71b1a5ad40a3101
https://github.com/scummvm/scummvm/commit/bfdd2c33ecfb0363422d914db71b1a5ad40a3101
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:53+02:00
Commit Message:
EEM: initial support for spanish release and fixes for the floppy version
Changed paths:
engines/eem/clues.cpp
engines/eem/detection.cpp
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/mystery.cpp
engines/eem/mystery.h
engines/eem/ui.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 2034ed40127..677566620eb 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -1002,11 +1002,25 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
_audio->playFloppyVoiceSlot(slot, _partner);
}
// Suspect-found side effect for byte 9 with high bit clear.
+ // Mirrors `_DisplayHotspotClue_Floppy @ 22dc:0908..0922`:
+ // if ((rec[9] & 0x80) == 0 && rec[9] != 0)
+ // _InGallery_Floppy[_GalleryShuffleSeed_Floppy[rec[9]]] = 1;
+ // `_GalleryShuffleSeed_Floppy` is a byte array at DS:0x2d65 that
+ // overlaps `_NewOrder_Floppy` at DS:0x2d66 by one byte: i.e.
+ // `_GalleryShuffleSeed[i+1] == _NewOrder[i]` (verified by
+ // comparing the two writes in `_ReadMystery_Floppy @ 22dc:0178`).
+ // So in our terms the mapping is `_inGallery[_newOrder[b9 - 1]]`.
+ // Skipping `_newOrder` (as we did before) drew the right portrait
+ // only when `_newOrder` happened to be identity â randomized
+ // shuffles dropped one of the two M0 suspects.
const uint8 b9 = rec[9];
if ((b9 & 0x80) == 0 && b9 != 0) {
- const uint slot = (uint)b9 - 1;
- if (slot < Mystery::kGalleryCap)
- _mystery._inGallery[slot] = 1;
+ const uint logicalIdx = (uint)b9 - 1;
+ if (logicalIdx < Mystery::kGalleryCap) {
+ const uint8 slot = _mystery._newOrder[logicalIdx];
+ if (slot < Mystery::kGalleryCap)
+ _mystery._inGallery[slot] = 1;
+ }
}
// Pre-load balloon picture + insets once per record (constant
@@ -1332,8 +1346,13 @@ bool EEMEngine::areYouSure() {
(const byte *)saved.getBasePtr(0, row), 320);
scratch.fillRect(dlg, 0);
scratch.frameRect(dlg, 0xF);
- _font.drawString(&scratch, "Are you sure you want to quit?", dlg.left + 8, dlg.top + 8, 320, 0xF);
- _font.drawString(&scratch, "Y - Yes", dlg.left + 16, dlg.top + 36, 320, 0xF);
+ _font.drawString(&scratch,
+ isSpanish() ? "Estas seguro que quieres salir?"
+ : "Are you sure you want to quit?",
+ dlg.left + 8, dlg.top + 8, 320, 0xF);
+ _font.drawString(&scratch,
+ isSpanish() ? "S - Si" : "Y - Yes",
+ dlg.left + 16, dlg.top + 36, 320, 0xF);
_font.drawString(&scratch, "N - No", dlg.left + 100, dlg.top + 36, 320, 0xF);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
@@ -1351,7 +1370,9 @@ bool EEMEngine::areYouSure() {
break;
}
if (ev.type == Common::EVENT_KEYDOWN) {
+ // Spanish prompt is "S - Si" â accept both Y and S.
if (ev.kbd.keycode == Common::KEYCODE_y ||
+ ev.kbd.keycode == Common::KEYCODE_s ||
ev.kbd.keycode == Common::KEYCODE_RETURN) {
result = true; decided = true; break;
}
diff --git a/engines/eem/detection.cpp b/engines/eem/detection.cpp
index 402a5a89cef..60967493537 100644
--- a/engines/eem/detection.cpp
+++ b/engines/eem/detection.cpp
@@ -52,6 +52,20 @@ const ADGameDescription gameDescriptions[] = {
ADGF_NO_FLAGS,
GUIO1(GUIO_NONE)
},
+ {
+ // Spanish floppy release â same EEM.EXE binary as the English
+ // floppy (the engine code is identical), with a localised
+ // PICS.DBD that swaps the embedded English image text for
+ // Spanish equivalents.
+ "eem",
+ "Floppy",
+ AD_ENTRY2s("EEM.EXE", "692a5e6e7f4516d6e40c1f80cbc1b2cc", 109542,
+ "PICS.DBD", "199150e7d612f87477814bc5f4a1967a", 955332),
+ Common::ES_ESP,
+ Common::kPlatformDOS,
+ ADGF_NO_FLAGS,
+ GUIO1(GUIO_NONE)
+ },
AD_TABLE_END_MARKER
};
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 23a34e6201f..03a2449d26b 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -105,6 +105,7 @@ EEMEngine::EEMEngine(OSystem *syst, const ADGameDescription *gameDesc)
_variant = (gameDesc && gameDesc->extra &&
Common::String(gameDesc->extra).contains("Floppy"))
? kVariantFloppy : kVariantCD;
+ _language = gameDesc ? gameDesc->language : Common::EN_ANY;
}
EEMEngine::~EEMEngine() {
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 4be4198ad29..e78e44066c3 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -25,6 +25,7 @@
#define EEM_EEM_H
#include "common/array.h"
+#include "common/language.h"
#include "common/platform.h"
#include "common/random.h"
#include "common/scummsys.h"
@@ -171,6 +172,13 @@ public:
const ADGameDescription *_gameDescription;
Variant _variant = kVariantCD;
+ Common::Language _language = Common::EN_ANY;
+
+ /// True for the Spanish floppy release. Used to swap hardcoded
+ /// English UI strings (menus, name prompt, notebook labels,
+ /// fallback hint copy) for their Spanish equivalents extracted
+ /// from EEM.EXE in `eem-full-game/floppy-es/`.
+ bool isSpanish() const { return _language == Common::ES_ESP; }
DBDArchive &getPics() { return _picsArchive; }
DBDArchive &getAni() { return _aniArchive; }
@@ -212,6 +220,7 @@ public:
/// Run the accuse flow (pick suspect, evaluate chains, show ending).
/// Mirrors `_DoAccuseGallery` @ 1df2:0a31 + `_DisplayEnding` @ 1df2:0548.
void doAccuse();
+ void doAccuseFloppy();
/// Show the accuse-notes screen (PIC 0x1A7, the red "accuse-mode"
/// BG with selectable clue list + "N clues" remaining counter).
diff --git a/engines/eem/mystery.cpp b/engines/eem/mystery.cpp
index 781d81895a0..06ce1d53f7b 100644
--- a/engines/eem/mystery.cpp
+++ b/engines/eem/mystery.cpp
@@ -191,6 +191,30 @@ bool Mystery::load(uint num, Common::RandomSource *rng) {
_solvedOffset = _floppySolvedOff;
_hintOffset = _floppyHintBlockOff;
+ // Per-mystery runtime state â mirror `_ReadMystery_Floppy @
+ // 22dc:0178` zeroing `_TextSeen_Floppy` / `_InGallery_Floppy` and
+ // seeding `_NewOrder_Floppy[0]` (random) + the `+1`-shift loop
+ // that fills the rest. We use the identity mapping `[0..N-1]` so
+ // dialog `byte9` (1-based logical idx) maps to `_inGallery[idx -
+ // 1]` and the gallery render iterates the same indices. Without
+ // this init the floppy load path returns with `_newOrder` left
+ // at whatever `clear()` zeroed it to, so every byte9>0 dialog
+ // resolves through `_newOrder[logicalIdx]==0` and the second
+ // suspect overwrites the first slot.
+ memset(_cluesFound, 0, sizeof(_cluesFound));
+ memset(_noteSelected, 0, sizeof(_noteSelected));
+ memset(_hotSpotsSeen, 0, sizeof(_hotSpotsSeen));
+ memset(_inGallery, 0, sizeof(_inGallery));
+ (void)rng;
+ for (uint i = 0; i < kGalleryCap; i++)
+ _newOrder[i] = (uint8)i;
+ memset(_visitedSite, 0, sizeof(_visitedSite));
+ memset(_onSites, 0, sizeof(_onSites));
+ _sawCOFFSITEs = _sawCONSITEs = _sawHelpHint = _solvedPuzzle = false;
+ _firstTry = true;
+ _searchLocationNumber = _siteNumber = 0xFFFF;
+ _lastSite = 0x1B;
+
debugC(1, kDebugMystery,
"Mystery::load(%u) floppy: sites=0x%04x siteIdx=0x%04x "
"notes=0x%04x suspects=0x%04x text=0x%04x kd=0x%04x "
@@ -396,11 +420,46 @@ const byte *Mystery::mapEntry(uint siteNum) const {
return _data.data() + off;
}
+const byte *Mystery::floppySuspectEntry(uint suspectIdx) const {
+ // Floppy gallery section: byte[0] = numSuspects, then per suspect a
+ // variable-size record of `5 + nameLen` bytes (verified at
+ // `_DrawGallery_Floppy @ 154e:00b6` advancing `iVar7 += pbVar4[4]
+ // + 5`):
+ // u16 +0 picID (gallery portrait, BUTTON.DBD entry)
+ // u16 +2 alibi marker (0xFFFF = guilty; else high byte = index
+ // into TEXT_BLOCK alibi-offset table at header[+0xc])
+ // u8 +4 name length
+ // u8 +5..+5+nameLen-1 name string
+ if (!_isFloppy || !isLoaded())
+ return nullptr;
+ const byte *gd = _data.data() + _galleryOffset;
+ if (gd + 1 > _data.data() + _data.size())
+ return nullptr;
+ const uint8 numSus = gd[0];
+ if (suspectIdx >= numSus)
+ return nullptr;
+ const byte *p = gd + 1;
+ const byte *bufEnd = _data.data() + _data.size();
+ for (uint i = 0; i < suspectIdx; i++) {
+ if (p + 5 > bufEnd)
+ return nullptr;
+ p += 5 + (uint)p[4];
+ }
+ if (p + 5 > bufEnd)
+ return nullptr;
+ return p;
+}
+
bool Mystery::isGuilty(uint suspectIdx) const {
- // `_WITCH @ 1df2:089f`: `if (GalleryData[i*0x46 + 0x02] == -1)
+ // `_WITCH @ 1df2:089f` (CD): `if (GalleryData[i*0x46 + 0x02] == -1)
// _DisplayCorrect(); else _DisplayAlibi(...)`. Innocent suspects
// store their alibi-text TextBlock offset at +0x02; the guilty
- // one stores the sentinel 0xFFFF.
+ // one stores the sentinel 0xFFFF. Floppy uses the same convention
+ // at suspect entry +2..3 but with variable-stride entries.
+ if (_isFloppy) {
+ const byte *e = floppySuspectEntry(suspectIdx);
+ return e && READ_LE_UINT16(e + 2) == 0xFFFF;
+ }
const byte *gd = galleryData();
if (!gd || suspectIdx >= _numSuspects)
return false;
@@ -409,6 +468,26 @@ bool Mystery::isGuilty(uint suspectIdx) const {
}
uint16 Mystery::alibiTextOffset(uint suspectIdx) const {
+ if (_isFloppy) {
+ // Floppy alibi: u16 at suspect +2..3 carries TWO things:
+ // 0xFFFF = guilty, otherwise the HIGH BYTE indexes the
+ // TEXT_BLOCK table (header[+0xc]), each entry u16 = absolute
+ // alibi-text offset in the buffer. Verified at
+ // `_DisplayAlibi_Floppy @ 1d40:0145` reading
+ // `*(int *)(textBlock + ((byte *)entry)[3] * 2)`. The result
+ // is an ABSOLUTE offset; caller reads via `blobAt(off)`.
+ const byte *e = floppySuspectEntry(suspectIdx);
+ if (!e)
+ return 0xFFFF;
+ const uint16 alibi = READ_LE_UINT16(e + 2);
+ if (alibi == 0xFFFF)
+ return 0xFFFF;
+ const uint8 idx = (uint8)(alibi >> 8);
+ const uint32 base = _textOffset;
+ if ((uint32)idx * 2 + 2 > _data.size() - base)
+ return 0xFFFF;
+ return READ_LE_UINT16(_data.data() + base + (uint32)idx * 2);
+ }
const byte *gd = galleryData();
if (!gd || suspectIdx >= _numSuspects)
return 0xFFFF;
@@ -436,6 +515,38 @@ int Mystery::selectedPoints() const {
const uint16 cnt = noteIndexCount();
if (!ni || cnt == 0)
return 0;
+ if (_isFloppy) {
+ // Floppy `_GetSelectedPoints_Floppy @ 1d40:0c23`: collect the
+ // per-note score (note +6 byte) for every clue with TextSeen
+ // set, sort descending, sum the top 5. Mirrors
+ // `_SortNotesByScore_Floppy @ 1d40:000e` + the top-5 fold.
+ uint8 scores[Mystery::kCluesFoundCap] = {};
+ uint scoreCount = 0;
+ const uint maxIdx = MIN<uint>(cnt, kCluesFoundCap);
+ for (uint i = 0; i < maxIdx; i++) {
+ if (_cluesFound[i] == 0)
+ continue;
+ scores[scoreCount++] = ni[i * 7 + 6];
+ }
+ // Partial selection sort for top 5.
+ const uint topN = MIN<uint>(5u, scoreCount);
+ for (uint k = 0; k < topN; k++) {
+ uint best = k;
+ for (uint j = k + 1; j < scoreCount; j++) {
+ if (scores[j] > scores[best])
+ best = j;
+ }
+ if (best != k) {
+ uint8 tmp = scores[k];
+ scores[k] = scores[best];
+ scores[best] = tmp;
+ }
+ }
+ int total = 0;
+ for (uint k = 0; k < topN; k++)
+ total += scores[k];
+ return total;
+ }
int total = 0;
for (uint i = 0; i < cnt && i < kCluesFoundCap; i++) {
if (!_noteSelected[i])
@@ -453,6 +564,22 @@ void Mystery::syncState(Common::Serializer &s) {
s.syncArray(_hotSpotsSeen, kHotSpotsCap, Common::Serializer::Uint16LE);
s.syncArray(_inGallery, kGalleryCap, Common::Serializer::Uint16LE);
s.syncBytes(_newOrder, kGalleryCap);
+ // Save profiles created before the floppy `_newOrder` identity init
+ // was added stored all-zeros here. Loading that back over the
+ // identity set by `Mystery::load()` makes every dialog with byte9>0
+ // resolve to slot 0 (so the second suspect found at a CONSITE
+ // overwrites the first). Detect the legacy zero-fill on read and
+ // rebuild the identity mapping that the loader produced.
+ if (s.isLoading()) {
+ bool allZero = true;
+ for (uint i = 0; i < kGalleryCap; i++) {
+ if (_newOrder[i] != 0) { allZero = false; break; }
+ }
+ if (allZero) {
+ for (uint i = 0; i < kGalleryCap; i++)
+ _newOrder[i] = (uint8)i;
+ }
+ }
s.syncArray(_visitedSite, kVisitedSiteCap, Common::Serializer::Uint16LE);
s.syncArray(_onSites, kVisitedSiteCap, Common::Serializer::Uint16LE);
s.syncAsByte(_sawCOFFSITEs);
diff --git a/engines/eem/mystery.h b/engines/eem/mystery.h
index 251320604aa..82c881803cd 100644
--- a/engines/eem/mystery.h
+++ b/engines/eem/mystery.h
@@ -91,6 +91,13 @@ public:
/// First u16 of each entry is the PIC picture ID for that suspect.
const byte *galleryData() const;
+ /// Floppy variable-stride suspect record. Returns nullptr on CD or
+ /// when @p suspectIdx is out of range. Walks the gallery section
+ /// (`5 + nameLen` bytes per suspect) to land on the requested entry.
+ /// Layout: u16 picID, u16 alibiMarker (0xFFFF = guilty),
+ /// u8 nameLen, nameLen bytes of name.
+ const byte *floppySuspectEntry(uint suspectIdx) const;
+
/// Pointer to the NoteIndex array (4 bytes per entry: u16 textOff + u16 pts).
const byte *noteIndex() const;
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 42321a6c626..fc5b53f4b0a 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -55,6 +55,19 @@ const GallerySlot kGallerySlots[5] = {
{ 191, 90 } // 4
};
+// Floppy gallery slot positions verified at `2608:0x16c` (5 Ã
+// {u16 x, u16 y}) â read by `_DrawGallery_Floppy @ 154e:0045`'s
+// `[BX + 0x16c]` (x) and `[BX + 0x16e]` (y) loads. The floppy
+// layout is shifted left ~0x30 px relative to CD: row 0 starts at
+// x=0x53 (vs 0x53â0x83 on CD) and the bottom row at x=0x77 / 0xbf.
+const GallerySlot kFloppyGallerySlots[5] = {
+ { 0x53, 0x0e }, // 0
+ { 0x9b, 0x0e }, // 1
+ { 0xe3, 0x0e }, // 2
+ { 0x77, 0x5a }, // 3
+ { 0xbf, 0x5a } // 4
+};
+
// `_GetKDTextBalloon @ 1df2:0105` digit-balloon table @ `29be:1064`:
// '0'â0x15 '1'â0x16 '2'â0x17 '3'â0x18 '4'â0x19
// '5'â0x1a '6'â0x20 '7'â0x21 '8'â0x22 '9'â0x1e
@@ -170,10 +183,12 @@ void drawCaseSubmenu(const CaseSubmenuView &v) {
// Top centred title. `_CaseSelection @ 1c33:0aa3` formats "Book %d"
// for tiers 1/2 and "Challenge Book" (sprintf with no arg) for
// tier 3. `_Show_String(0xc, (0xba - width)/2 + 0x3c, â¦, 0x10)`
- // places it horizontally centred over the panel.
+ // places it horizontally centred over the panel. Spanish floppy
+ // uses "Lib. %u" / "Libro de Retos" (verified in Spanish EEM.EXE).
+ const bool spanish = v.vm && v.vm->isSpanish();
const Common::String title = (v.book == 3)
- ? Common::String("Challenge Book")
- : Common::String::format("Book %u", v.book);
+ ? Common::String(spanish ? "Libro de Retos" : "Challenge Book")
+ : Common::String::format(spanish ? "Lib. %u" : "Book %u", v.book);
const int titleW = v.vm->getFont().getStringWidth(title);
const int titleX = (0xba - titleW) / 2 + 0x3c;
v.vm->getFont().drawString(&scratch, title, titleX, 12, 320, 0xF);
@@ -274,7 +289,8 @@ void drawCaseSelectionFrame(const CaseSelectionView &v) {
// `_Show_String(0xc, (0xba - width)/2 + 0x3c, â¦)` in the
// original. We don't track challenge tier yet so always
// show "Book 1".
- const Common::String book = "Book 1";
+ const Common::String book = v.vm->isSpanish()
+ ? Common::String("Lib. 1") : Common::String("Book 1");
const int titleW = v.vm->getFont().getStringWidth(book);
const int titleX = (0xba - titleW) / 2 + 0x3c;
v.vm->getFont().drawString(&scratch, book, titleX, 12, 320, 0xF);
@@ -468,6 +484,12 @@ void EEMEngine::doNewPlayer() {
Picture bg;
const bool haveBG = _picsArchive.getPicture(0x104, bg);
+ // Localized name-entry prompt. Spanish text is taken from the
+ // Spanish floppy EEM.EXE ("Teclea tu nombre"). The colon suffix is
+ // our own â the original DOS prompt has none.
+ const char *prompt = isSpanish()
+ ? "Teclea tu nombre:" : "Please type your name:";
+
// Match the original `_NewPlayer`: `_Show_String(rw=0x28, cl=0x50)`
// for the prompt, then `_ShowChar(0x50, x, â¦)` for typed input.
// (rw=row=y, cl=col=x.) Prompt at (y=40, x=80), input at (y=80, x=80).
@@ -476,7 +498,7 @@ void EEMEngine::doNewPlayer() {
scratch.clear();
if (haveBG)
scratch.simpleBlitFrom(bg.surface);
- _font.drawString(&scratch, "Please type your name:", 80, 40, 240, 0xF);
+ _font.drawString(&scratch, prompt, 80, 40, 240, 0xF);
_font.drawString(&scratch, name + "_", 80, 80, 240, 0xF);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
@@ -537,8 +559,7 @@ void EEMEngine::doNewPlayer() {
scratch.clear();
if (haveBG)
scratch.simpleBlitFrom(bg.surface);
- _font.drawString(&scratch, "Please type your name:",
- 80, 40, 240, 0xF);
+ _font.drawString(&scratch, prompt, 80, 40, 240, 0xF);
_font.drawString(&scratch, name + "_", 80, 80, 240, 0xF);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
@@ -916,8 +937,12 @@ void EEMEngine::doSetup() {
}
const Common::Rect kBox(80, 80, 240, 120);
scratch.fillRect(kBox, 0x00);
- _font.drawString(&scratch, "Are you sure?", 100, 88, 200, 0xF);
- _font.drawString(&scratch, "Y = yes N = no", 100, 102, 200, 0xF);
+ _font.drawString(&scratch,
+ isSpanish() ? "Estas seguro?" : "Are you sure?",
+ 100, 88, 200, 0xF);
+ _font.drawString(&scratch,
+ isSpanish() ? "S = si N = no" : "Y = yes N = no",
+ 100, 102, 200, 0xF);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
g_system->updateScreen();
@@ -929,7 +954,9 @@ void EEMEngine::doSetup() {
return true;
if (ev.type == Common::EVENT_KEYDOWN) {
const Common::KeyCode k = ev.kbd.keycode;
+ // Spanish prompt is "S = si" â accept both Y and S.
if (k == Common::KEYCODE_y ||
+ k == Common::KEYCODE_s ||
k == Common::KEYCODE_RETURN)
return true;
if (k == Common::KEYCODE_n ||
@@ -1240,13 +1267,24 @@ void EEMEngine::doCaseSelection() {
kPickScrap3,
kNumPicks
};
- const char *kPickLabel[kNumPicks] = {
+ // Localized action-menu labels. Spanish text is taken verbatim
+ // from the Spanish floppy EEM.EXE (`eem-full-game/floppy-es/`):
+ // "Elegir Misterio" / "Caso de Practica" / "Ver Recortes 1..3".
+ const char *kPickLabelEN[kNumPicks] = {
" Choose A Mystery",
" Practice Mystery",
" See ScrapBook 1",
" See ScrapBook 2",
" See ScrapBook 3"
};
+ const char *kPickLabelES[kNumPicks] = {
+ " Elegir Misterio ",
+ " Caso de Practica",
+ " Ver Recortes 1",
+ " Ver Recortes 2",
+ " Ver Recortes 3"
+ };
+ const char * const *kPickLabel = isSpanish() ? kPickLabelES : kPickLabelEN;
// Menu entry gating per `_ActionScreen @ 1c33:195b` â the asm at
// 1c33:19d1-1a70 sets greys[] based on chain stage AND per-tier
// solve count:
@@ -2002,7 +2040,8 @@ void EEMEngine::drawNotebookFrame(int &page) {
const uint clueId = found[i];
Common::String txt = noteText(clueId);
if (txt.empty())
- txt = Common::String::format("clue %u", clueId);
+ txt = Common::String::format(
+ isSpanish() ? "nota %u" : "clue %u", clueId);
// Per `_DrawNotes @ 161e:01d0`: text uses
// `_NoteUnselectedColor` (0x5c=cyan) for unselected and 0x3c
// (light yellow-white) for selected. Both contrast cleanly
@@ -2173,17 +2212,34 @@ void EEMEngine::doGallery() {
// _AddPicBackground(pic, 0x94, 0xf);
// _DrawGalleryNotes(gd + i*0x46);
// loop until ESC or button click.
- // Suspect data layout (verified against M1):
- // +0..1: picId (used here AND for gallery slot)
- // +8..9: number of clues for this suspect
- // +0xa..??: array of u16 clue IDs (terminated
- // by 0xFFFF if shorter than count).
+ // Suspect data layout differs by variant:
+ // * CD (`158f:0419`): fixed 0x46-byte stride.
+ // +0..1 picId
+ // +8..9 clue count (u16)
+ // +0xa.. array of u16 clue IDs (max 30,
+ // terminated by 0xFFFF if short).
+ // * Floppy (`_MoreInfo_Floppy = 154e:042b` â
+ // `FUN_154e_0201` â `FUN_15e0_01e8`):
+ // variable-stride.
+ // +0..1 picId
+ // +2..3 alibi (0xFFFF = guilty)
+ // +4 clue count (u8)
+ // +5.. u8 clue IDs (per the asm at
+ // 154e:020e..0282 which calls
+ // `FUN_15e0_01e8(rect, entry+5,
+ // entry[4], NULL)`).
+ const bool floppyMI = isFloppy();
const uint suspectIdx = (uint)slotSuspect[i];
- const byte *suspect = gd + suspectIdx * 0x46;
+ const byte *suspect = floppyMI
+ ? _mystery.floppySuspectEntry(suspectIdx)
+ : gd + suspectIdx * 0x46;
+ if (!suspect)
+ break;
const uint16 detailPic =
READ_LE_UINT16(suspect + 0);
- const uint16 clueCount =
- READ_LE_UINT16(suspect + 8);
+ const uint clueCount = floppyMI
+ ? (uint)suspect[4]
+ : READ_LE_UINT16(suspect + 8);
Graphics::ManagedSurface ms(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
@@ -2253,18 +2309,33 @@ void EEMEngine::doGallery() {
int yPos = ry;
const int lineH = _font.getFontHeight() + 1;
bool drewAny = false;
- for (uint k = 0; k < clueCount && k < 30; k++) {
- const uint16 clueId =
- READ_LE_UINT16(suspect + 0xa + k * 2);
- if (clueId == 0xFFFF)
+ const uint clueMax = floppyMI ? clueCount : 30u;
+ for (uint k = 0; k < clueCount && k < clueMax; k++) {
+ const uint16 clueId = floppyMI
+ ? (uint16)suspect[5 + k]
+ : READ_LE_UINT16(suspect + 0xa + k * 2);
+ if (!floppyMI && clueId == 0xFFFF)
break;
if (clueId >= Mystery::kCluesFoundCap ||
!_mystery._cluesFound[clueId])
continue;
if (!ni || clueId >= niCount)
continue;
- const uint16 textOff =
- READ_LE_UINT16(ni + clueId * 4);
+ // Floppy notes are 7-byte entries: u16 ?,
+ // u16 jakeOff, u16 jennyOff, u8 score. The
+ // partner-specific text offset is at +2
+ // (Jake) or +4 (Jenny) â verified at
+ // `FUN_22dc_05c8 @ 22dc:0843`. CD notes are
+ // 4 bytes: u16 textOff, u16 score.
+ uint16 textOff;
+ if (floppyMI) {
+ const uint partnerByte = (_partner == 0)
+ ? 2 : 4;
+ textOff = READ_LE_UINT16(
+ ni + clueId * 7 + partnerByte);
+ } else {
+ textOff = READ_LE_UINT16(ni + clueId * 4);
+ }
Common::String txt =
parseString(_mystery.textAt(textOff),
_playerName, _partner);
@@ -2281,15 +2352,20 @@ void EEMEngine::doGallery() {
}
if (!drewAny && _font.isLoaded()) {
_font.drawString(&ms,
- "No clues yet for this suspect.",
+ isSpanish()
+ ? "Aun no hay pistas para este sospechoso."
+ : "No clues yet for this suspect.",
rx, ry, rw, 0x5C);
}
// Header / footer text.
if (_font.isLoaded()) {
- _font.drawString(&ms, "SUSPECT FILE",
- rx, ry - 11, rw, 0x3C);
- _font.drawString(&ms, "(click / ESC: back)",
- rx, ry + rh + 2, rw, 0x3C);
+ _font.drawString(&ms,
+ isSpanish() ? "EXPEDIENTE" : "SUSPECT FILE",
+ rx, ry - 11, rw, 0x3C);
+ _font.drawString(&ms,
+ isSpanish() ? "(clic / ESC: volver)"
+ : "(click / ESC: back)",
+ rx, ry + rh + 2, rw, 0x3C);
}
g_system->copyRectToScreen(ms.getPixels(),
ms.pitch, 0, 0, 320, 200);
@@ -2407,8 +2483,17 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
partnerAni[frameIdx], 5, 0x50);
}
- // Portraits â `_DrawGallery @ 158f:0046` walks suspects 0..N-1 and
- // only renders those flagged in `_InGallery[NewOrder[i]]`.
+ // Portraits â `_DrawGallery @ 158f:0046` (CD) /
+ // `_DrawGallery_Floppy @ 154e:0045` (floppy) walks suspects 0..N-1
+ // and only renders those flagged in `_InGallery[NewOrder[i]]`.
+ // Layout differs by variant:
+ // * CD: fixed 0x46-byte stride, slot positions at `kGallerySlots`.
+ // * Floppy: variable-stride entries (5 + entry[4] bytes per
+ // suspect), slot positions at `kFloppyGallerySlots` (verified
+ // at `2608:0x16c`).
+ const bool floppy = isFloppy();
+ const GallerySlot * const slots =
+ floppy ? kFloppyGallerySlots : kGallerySlots;
for (uint i = 0; i < numSuspects && i < Mystery::kGalleryCap; i++) {
slotRects[i] = Common::Rect();
slotSuspect[i] = -1;
@@ -2416,11 +2501,16 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
const uint8 phys = _mystery._newOrder[i];
if (phys >= 5)
continue;
- const GallerySlot &s = kGallerySlots[phys];
+ const GallerySlot &s = slots[phys];
const bool discovered = _mystery._inGallery[phys] != 0;
if (discovered) {
- const uint16 picId = READ_LE_UINT16(gd + i * 0x46);
+ const byte *entry = floppy
+ ? _mystery.floppySuspectEntry(i)
+ : gd + i * 0x46;
+ if (!entry)
+ continue;
+ const uint16 picId = READ_LE_UINT16(entry);
Picture portrait;
if (picId == 0 ||
!_picsArchive.getPicture(picId, portrait))
@@ -3176,7 +3266,8 @@ bool EEMEngine::doAccuseNotes() {
_playerName, _partner);
}
if (txt.empty())
- txt = Common::String::format("clue %u", clueId);
+ txt = Common::String::format(
+ isSpanish() ? "nota %u" : "clue %u", clueId);
Common::Array<Common::String> wrapped;
_font.wordWrapText(txt, rectW, wrapped);
const int h = (int)wrapped.size() * lineH;
@@ -3195,8 +3286,12 @@ bool EEMEngine::doAccuseNotes() {
const uint remaining = (selectedCount < expected)
? expected - selectedCount
: 0;
- const Common::String counter = Common::String::format("%u %s",
- remaining, remaining == 1 ? "clue" : "clues");
+ // Spanish floppy uses "nota/notas" (verified in Spanish EEM.EXE).
+ const char *clueWord = isSpanish()
+ ? (remaining == 1 ? "nota" : "notas")
+ : (remaining == 1 ? "clue" : "clues");
+ const Common::String counter =
+ Common::String::format("%u %s", remaining, clueWord);
_font.drawString(&scratch, counter, 209, 11, 100, 0x0F);
if (numPages > 1) {
@@ -3317,6 +3412,22 @@ void EEMEngine::doAccuse() {
if (!_mystery.isLoaded() || !_font.isLoaded())
return;
+ // Floppy accusation flow is structurally different from the CD's:
+ // the suspect gallery section uses variable-stride entries (5 +
+ // nameLen bytes, NOT 0x46), the score is computed from the top-5
+ // note scores in `_TextSeen` (not `_NoteSelected`), and the win
+ // path renders the solved-chain dialog records at header[+0x12]
+ // rather than a single ClueBlock. Dispatch to a dedicated handler
+ // that mirrors `_KDHelp_Floppy` + `_HandleNoteButton_Floppy` (button
+ // 4 â `FUN_1d40_11fd` "ready to solve" gate) +
+ // `FUN_1d40_0c79` (gallery picker) + `_DisplayCorrect_Floppy` /
+ // `_DisplayAlibi_Floppy`.
+ if (isFloppy()) {
+ doAccuseFloppy();
+ return;
+ }
+
+
// Mirrors `_DoAccuse @ 1df2:0bdd` + `_DoAccuseGallery @ 1df2:0a31`:
// 1. ACCUSE-NOTES SCREEN (PIC 0x1A7, the red "accuse-mode" BG):
// `_DrawNotes(_AccuseNoteRect, NULL, 100, _NoteSelected)`
@@ -3370,13 +3481,22 @@ void EEMEngine::doAccuse() {
_playerName, _partner);
if (hint.empty()) {
// Fallback if `KDTextIndex[+6]` isn't set in this mystery.
- hint = (_mystery.selectedPoints() == 0)
- ? "We're not ready to solve this mystery yet. "
- "Let's keep investigating until we have some "
- "more solid evidence."
- : "We don't have quite enough evidence yet. "
- "Let's review our notes and find a few more "
- "clues before we accuse anyone.";
+ // Spanish text from the Spanish floppy EEM.EXE â there's
+ // only the single "1Necesitamos buscar pistas..." string
+ // in the binary, so use it for both the zero-points and
+ // some-points cases.
+ if (isSpanish()) {
+ hint = "Necesitamos buscar pistas antes de resolver "
+ "el misterio. Investiguemos un poco mas!";
+ } else {
+ hint = (_mystery.selectedPoints() == 0)
+ ? "We're not ready to solve this mystery yet. "
+ "Let's keep investigating until we have some "
+ "more solid evidence."
+ : "We don't have quite enough evidence yet. "
+ "Let's review our notes and find a few more "
+ "clues before we accuse anyone.";
+ }
}
// Compose balloon overlay on the current screen. Mirrors the
@@ -4094,6 +4214,417 @@ void EEMEngine::doAccuse() {
}
}
+void EEMEngine::doAccuseFloppy() {
+ // Floppy accuse flow â mirrors:
+ // `_KDHelp_Floppy / FUN_1d40_11fd @ 1d40:11fd` (score gate +
+ // partner "ready / not ready" balloon)
+ // `FUN_1d40_0e07 @ 1d40:0e07` (clue-selection screen â skipped
+ // in this MVP; selectedPoints() already takes the top-5)
+ // `FUN_1d40_0c79 @ 1d40:0c79` (gallery picker)
+ // `_DisplayCorrect_Floppy @ 1d40:0894`
+ // `_DisplayAlibi_Floppy @ 1d40:00df`
+ const byte *kdIdx = _mystery.kdTextIndex();
+ const byte *bufBase = _mystery.blobAt(0);
+ const uint32 mysSize = _mystery.dataSize();
+ if (!kdIdx || !bufBase)
+ return;
+
+ // Helper: render one KDTextIndex string as a centred KD balloon
+ // over the current screen, mirroring `_DisplayHint_Floppy @
+ // 1503:00ca` (= `FUN_1d40_11fd`'s body). `kdSlot` is the index
+ // into KDTextIndex (0..N) â entries are u16 absolute text
+ // offsets.
+ auto showFloppyKDHint = [&](uint kdSlot) {
+ if ((uint)(kdSlot * 2) + 2 > (uint)(mysSize - (kdIdx - bufBase)))
+ return;
+ const uint16 textOff = READ_LE_UINT16(kdIdx + kdSlot * 2);
+ if (textOff == 0 || textOff >= mysSize)
+ return;
+ const char *p = (const char *)(bufBase + textOff);
+ uint32 lineLen = 0;
+ while (textOff + lineLen < mysSize && p[lineLen] != 0)
+ lineLen++;
+ if (lineLen == 0)
+ return;
+ Common::String raw(p, lineLen);
+ // Digit prefix â balloon variant (per `_GetKDTextBalloon_Floppy
+ // @ 1d40:009f` and the `_KDBalloonByChar_Floppy` table at
+ // `2608:0c14 + char`).
+ static const uint8 kDigitToBalloon[10] = {
+ 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1c, 0x1d, 0x1e, 0x0a
+ };
+ uint balloonIdx = 0x17;
+ const char *txt = raw.c_str();
+ if (*txt >= '0' && *txt <= '9') {
+ balloonIdx = kDigitToBalloon[(int)(*txt - '0')];
+ txt++;
+ }
+ Common::String text =
+ parseString(Common::String(txt), _playerName, _partner);
+ Graphics::ManagedSurface ms(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ Graphics::Surface *cur = g_system->lockScreen();
+ if (cur) {
+ ms.simpleBlitFrom(*cur);
+ g_system->unlockScreen();
+ }
+ Picture balloon;
+ const bool haveBalloon = _balloonArchive.size() > balloonIdx &&
+ _balloonArchive.loadEntry(balloonIdx, balloon);
+ uint16 balloonY = 1;
+ if (haveBalloon) {
+ const uint h = (uint)balloon.surface.h;
+ if (h < 0x4e)
+ balloonY = (uint16)((0x50 - h) >> 1);
+ const byte transp = (byte)(balloon.flags >> 8);
+ for (int row = 0; row < balloon.surface.h && balloonY + row < 200;
+ row++) {
+ const byte *src =
+ (const byte *)balloon.surface.getBasePtr(0, row);
+ byte *dst = (byte *)ms.getBasePtr(0x21, balloonY + row);
+ for (int col = 0; col < balloon.surface.w && 0x21 + col < 320;
+ col++) {
+ if (src[col] != transp)
+ dst[col] = src[col];
+ }
+ }
+ }
+ uint16 bx = 5;
+ uint16 by = 4;
+ uint16 bw = 142;
+ getBalloonInsets(balloonIdx, bx, by, bw);
+ _font.drawWordWrapped(&ms, 0x21 + bx, balloonY + by,
+ MAX<int>(8, (int)bw), text, 0);
+ g_system->copyRectToScreen(ms.getPixels(), ms.pitch, 0, 0, 320, 200);
+ g_system->updateScreen();
+ // Wait for click.
+ while (!shouldQuit()) {
+ Common::Event ev;
+ bool advance = false;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER ||
+ ev.type == Common::EVENT_LBUTTONDOWN ||
+ ev.type == Common::EVENT_KEYDOWN) {
+ advance = true;
+ break;
+ }
+ }
+ if (advance)
+ break;
+ g_system->updateScreen();
+ g_system->delayMillis(10);
+ }
+ };
+
+ // `FUN_1d40_11fd` score gate. Picks one of three KD strings and
+ // returns 1 only when score >= 100.
+ const int score = _mystery.selectedPoints();
+ uint kdSlot;
+ bool readyToSolve;
+ if (score < 50) {
+ kdSlot = 0; // KDTextIndex[0] â "we've barely started"
+ readyToSolve = false;
+ } else if (score < 100) {
+ kdSlot = 1; // KDTextIndex[1] â "getting closer"
+ readyToSolve = false;
+ } else {
+ kdSlot = 2; // KDTextIndex[2] â "ready to solve"
+ readyToSolve = true;
+ }
+ showFloppyKDHint(kdSlot);
+ if (!readyToSolve) {
+ _nextScreen = _lastScreen != kScreenInvalid
+ ? (ScreenId)_lastScreen : kScreenSite;
+ return;
+ }
+
+ // `FUN_1d40_0c79` â gallery picker. Show "Which suspect?" KD
+ // hint at slot 4 (KDTextIndex[+8]/2 = entry index 4), then
+ // render the gallery and wait for click.
+ showFloppyKDHint(4);
+
+ // Build slot list. Floppy gallery section: byte0 = numSuspects,
+ // then variable-stride entries.
+ const uint8 num = _mystery.numSuspects();
+ if (num == 0) {
+ _nextScreen = _lastScreen != kScreenInvalid
+ ? (ScreenId)_lastScreen : kScreenSite;
+ return;
+ }
+
+ // Verbatim from `_DrawGallery_Floppy @ 154e:0050` â slot positions
+ // at `2608:016c` (5 Ã {u16 x, u16 y}).
+ struct GallerySlot { int x, y; };
+ static const GallerySlot kFloppySlots[5] = {
+ { 0x53, 0x0e }, { 0x9b, 0x0e }, { 0xe3, 0x0e },
+ { 0x77, 0x5a }, { 0xbf, 0x5a },
+ };
+
+ Picture accuseBg;
+ const bool haveAccuseBg = _picsArchive.getPicture(0x3f, accuseBg);
+
+ auto drawGallery = [&](int highlighted,
+ Common::Array<Common::Rect> &rects,
+ Common::Array<int> &suspects) {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ if (haveAccuseBg)
+ scratch.simpleBlitFrom(accuseBg.surface);
+
+ // Partner sprite (anim 2 / 0x10) â same as CD doAccuse.
+ const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
+ Animation partnerAni;
+ if (_aniArchive.loadAnimation(partnerAnim, partnerAni) &&
+ !partnerAni.empty()) {
+ const uint32 now = g_system->getMillis();
+ const uint frameIdx = partnerFrameAtTick(0x02,
+ (uint)partnerAni.size(), now);
+ blitAnimFrameAnchored(scratch.surfacePtr(),
+ partnerAni[frameIdx], 5, 0x50);
+ }
+
+ rects.resize(num);
+ suspects.resize(num);
+ for (uint i = 0; i < num; i++) {
+ rects[i] = Common::Rect();
+ suspects[i] = -1;
+ const uint8 phys = _mystery._newOrder[i];
+ if (phys >= 5)
+ continue;
+ if (_mystery._inGallery[phys] == 0)
+ continue;
+ const byte *e = _mystery.floppySuspectEntry(i);
+ if (!e)
+ continue;
+ const uint16 picId = READ_LE_UINT16(e + 0);
+ if (picId == 0)
+ continue;
+ Picture portrait;
+ if (!_picsArchive.getPicture(picId, portrait))
+ continue;
+ const GallerySlot &s = kFloppySlots[phys];
+ // `_DrawGallery_Floppy @ 154e:00ed` bottom-aligns to baseline
+ // 0x48 (= 72), same as CD: `placeY = s.y + (0x48 - h)`.
+ const int placeX = s.x;
+ const int placeY = s.y + (0x48 - portrait.surface.h);
+ const byte transp = (byte)(portrait.flags >> 8);
+ scratch.transBlitFrom(portrait.surface,
+ Common::Point(placeX, placeY),
+ (uint32)transp);
+ rects[i] = Common::Rect(placeX, placeY,
+ placeX + portrait.surface.w,
+ placeY + portrait.surface.h);
+ suspects[i] = (int)i;
+ if (highlighted == (int)i) {
+ scratch.frameRect(rects[i], 0xFE);
+ }
+ }
+
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ };
+
+ Common::Array<Common::Rect> slotRects;
+ Common::Array<int> slotSuspect;
+ int highlighted = 0;
+ int picked = -1;
+ drawGallery(highlighted, slotRects, slotSuspect);
+
+ uint32 lastTick = g_system->getMillis();
+ while (picked < 0 && !shouldQuit()) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return;
+ if (ev.type == Common::EVENT_KEYDOWN) {
+ if (ev.kbd.keycode == Common::KEYCODE_ESCAPE)
+ return;
+ if (ev.kbd.keycode == Common::KEYCODE_TAB ||
+ ev.kbd.keycode == Common::KEYCODE_RIGHT) {
+ highlighted = (highlighted + 1) % MAX<int>(1, (int)num);
+ drawGallery(highlighted, slotRects, slotSuspect);
+ } else if (ev.kbd.keycode == Common::KEYCODE_LEFT) {
+ highlighted = (highlighted + (int)num - 1) %
+ MAX<int>(1, (int)num);
+ drawGallery(highlighted, slotRects, slotSuspect);
+ } else if ((ev.kbd.keycode == Common::KEYCODE_RETURN ||
+ ev.kbd.keycode == Common::KEYCODE_KP_ENTER) &&
+ highlighted < (int)slotRects.size() &&
+ !slotRects[highlighted].isEmpty()) {
+ picked = slotSuspect[highlighted];
+ }
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ for (uint i = 0; i < slotRects.size(); i++) {
+ if (slotSuspect[i] >= 0 &&
+ slotRects[i].contains(ev.mouse.x, ev.mouse.y)) {
+ picked = slotSuspect[i];
+ break;
+ }
+ }
+ }
+ }
+ const uint32 now = g_system->getMillis();
+ if (now - lastTick >= 100) {
+ drawGallery(highlighted, slotRects, slotSuspect);
+ lastTick = now;
+ }
+ g_system->updateScreen();
+ g_system->delayMillis(10);
+ }
+ if (picked < 0)
+ return;
+
+ const bool guilty = _mystery.isGuilty((uint)picked);
+
+ if (guilty) {
+ // `_DisplayCorrect_Floppy @ 1d40:0894` â load PIC 5 (win BG),
+ // walk the solved-clue chain at header[+0x12]: byte0 = count,
+ // then `count` dialog records (same FUN_22dc_05c8 layout). We
+ // reuse `displayFloppyDialogRecords` for the rendering.
+ Picture winBg;
+ if (_picsArchive.getPicture(5, winBg))
+ blitAt(winBg, 0, 0);
+ setSitePalette(0x22);
+ g_system->updateScreen();
+
+ const byte *chain = _mystery.solvedClueBlock();
+ if (chain) {
+ const uint count = chain[0];
+ displayFloppyDialogRecords(chain + 1, count, 0);
+ }
+
+ // Mark mystery solved â equivalent to
+ // `((u16 *)0x5d20)... actually 0x3054[mysteryNum] = 1` which we
+ // translate to clearing the in-progress mystery + saving the
+ // profile (matches CD `doAccuse` win path).
+ if (_partner == 0)
+ _mystery._firstTry = false;
+ _mystery._solvedPuzzle = true;
+ _mystery.clear();
+ (void)saveProfile(_playerName);
+ _nextScreen = kScreenAction;
+ return;
+ }
+
+ // Innocent â `_DisplayAlibi_Floppy @ 1d40:00df`:
+ // 1. Load PIC 0x3e (alibi BG), draw at (0x42, 0x14).
+ // 2. Load suspect picID, draw portrait at (0x82, 0x5a).
+ // 3. Read alibi text via TEXT_BLOCK[suspect[+3] * 2]; render in
+ // a balloon picked by digit-prefix dispatch.
+ // 4. Voice from per-partner table at suspect_entry[+5..]
+ // (we approximate using slot 13/14 â Jake/Jenny "wrong"
+ // sting; original picks per-suspect).
+ // 5. Wait for click, then KD reaction balloon
+ // (KDTextIndex[+0x10]/2 = slot 8) + voice slot 5.
+ const byte *susp = _mystery.floppySuspectEntry((uint)picked);
+ uint16 picId = 0;
+ uint16 alibiOff = 0xFFFF;
+ if (susp) {
+ picId = READ_LE_UINT16(susp + 0);
+ alibiOff = _mystery.alibiTextOffset((uint)picked);
+ }
+
+ Picture alibiBg;
+ const bool haveAlibiBg = _picsArchive.getPicture(0x3e, alibiBg);
+ Picture suspectPic;
+ const bool haveSuspect = picId != 0 &&
+ _picsArchive.getPicture(picId, suspectPic);
+
+ Common::String alibi;
+ if (alibiOff != 0xFFFF && alibiOff < mysSize) {
+ const char *raw = (const char *)(bufBase + alibiOff);
+ uint32 lineLen = 0;
+ while (alibiOff + lineLen < mysSize && raw[lineLen] != 0)
+ lineLen++;
+ alibi = parseString(Common::String(raw, lineLen),
+ _playerName, _partner);
+ }
+
+ // Digit-prefix â alibi balloon idx. CD has its own table at
+ // 29be:1050; floppy uses the same KD digitâballoon mapping for
+ // the alibi too (verified by inspecting `_DisplayAlibi_Floppy`'s
+ // `_GetKDTextBalloon_Floppy` call).
+ static const uint8 kDigitToBalloon[10] = {
+ 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1c, 0x1d, 0x1e, 0x0a
+ };
+ uint balloonIdx = 0x17;
+ if (!alibi.empty() && alibi[0] >= '0' && alibi[0] <= '9') {
+ balloonIdx = kDigitToBalloon[(int)(alibi[0] - '0')];
+ alibi.deleteChar(0);
+ }
+
+ // Compose alibi screen.
+ Graphics::ManagedSurface scene(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scene.clear();
+ if (haveAlibiBg)
+ scene.simpleBlitFrom(alibiBg.surface);
+ if (haveSuspect) {
+ const byte transp = (byte)(suspectPic.flags >> 8);
+ scene.transBlitFrom(suspectPic.surface,
+ Common::Point(0x82, 0x5a),
+ (uint32)transp);
+ }
+ Picture balloon;
+ const bool haveBalloon = _balloonArchive.size() > balloonIdx &&
+ _balloonArchive.loadEntry(balloonIdx, balloon);
+ int balloonX = 0x21;
+ int balloonY = 1;
+ if (haveBalloon) {
+ if (balloon.surface.h < 0x4e)
+ balloonY = (0x50 - balloon.surface.h) / 2;
+ const byte transp = (byte)(balloon.flags >> 8);
+ scene.transBlitFrom(balloon.surface,
+ Common::Point(balloonX, balloonY),
+ (uint32)transp);
+ }
+ uint16 tx = 5, ty = 4, tw = 155;
+ getBalloonInsets(balloonIdx, tx, ty, tw);
+ if (!alibi.empty()) {
+ _font.drawWordWrapped(&scene, balloonX + tx, balloonY + ty,
+ MAX<int>(8, (int)tw), alibi, 0);
+ }
+ g_system->copyRectToScreen(scene.getPixels(), scene.pitch, 0, 0, 320, 200);
+ g_system->updateScreen();
+
+ // Voice â suspect alibi voice. The original picks per-suspect from
+ // the alibi voice table at `2608:0c5e + suspectIdx`; we don't have
+ // the table indexed, so play slot 13 (= F-0161SL.VOC / M-0113SL.VOC)
+ // as a generic alibi cue.
+ if (_audio)
+ _audio->playFloppyVoiceSlot(13, _partner);
+
+ while (!shouldQuit()) {
+ Common::Event ev;
+ bool advance = false;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER ||
+ ev.type == Common::EVENT_LBUTTONDOWN ||
+ ev.type == Common::EVENT_KEYDOWN) {
+ advance = true;
+ break;
+ }
+ }
+ if (advance)
+ break;
+ g_system->updateScreen();
+ g_system->delayMillis(10);
+ }
+
+ // Partner reaction â KDTextIndex slot 8 (= +0x10).
+ showFloppyKDHint(8);
+
+ _mystery._firstTry = false;
+ _nextScreen = _lastScreen != kScreenInvalid
+ ? (ScreenId)_lastScreen : kScreenSite;
+}
+
void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
int highlighted,
Common::Array<Common::Rect> &slotRects,
Commit: 9c41e30159c5cdce8fdf1120bf5df3deeea5f953
https://github.com/scummvm/scummvm/commit/9c41e30159c5cdce8fdf1120bf5df3deeea5f953
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:53+02:00
Commit Message:
EEM: make sure all the clues are there
Changed paths:
engines/eem/clues.cpp
engines/eem/site.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 677566620eb..1420e324a7f 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -956,6 +956,29 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
const uint32 dsz = _mystery.dataSize();
const uint32 notesBase = (uint32)(notes - bufBase);
+ // Pre-mark every text index across all records as "seen" up front.
+ // `_DisplayHotspotClue_Floppy @ 22dc:05c8` writes
+ // `_TextSeen_Floppy[idx] = 1` inside its render loop, but the
+ // original's `_WaitForClick` blocks until input â so the loop always
+ // runs to completion and every text gets marked. Our `waitForClick`
+ // honours ESC as "skip all" (sets `skipAll`/break), which matches
+ // player expectations for fast-forward but used to drop the seen-bit
+ // for every text after the ESC point. Pre-marking restores parity:
+ // ESC fast-forwards the *visual* but the notebook always reflects
+ // every clue the player would have seen if they'd clicked through.
+ {
+ const byte *r = rec;
+ for (uint i = 0; i < count; i++) {
+ const uint8 tc = r[10];
+ for (uint t = 0; t < tc; t++) {
+ const uint8 idx = r[11 + t] & 0x7f;
+ if (idx < Mystery::kCluesFoundCap)
+ _mystery._cluesFound[idx] = 1;
+ }
+ r += 11 + tc;
+ }
+ }
+
auto waitForClick = [&]() -> bool {
// Drain pending events first so a previous keystroke's tail
// doesn't auto-advance the new page.
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 1add3abbd4c..9b07bfb5d49 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -1391,9 +1391,31 @@ void SiteScreen::onHotspotClicked(uint siteNum, uint hotIdx) {
if (hotIdx < Mystery::kHotSpotsCap)
_mystery->_hotSpotsSeen[hotIdx] = 1;
_mystery->_searchLocationNumber = (uint16)hotIdx;
+ // Snapshot `_cluesFound` BEFORE the floppy dialog so we can
+ // auto-save only when a new note (= clue text idx) is added.
+ // The floppy clue-side-effect path lives in
+ // `displayFloppyDialogRecords` (see clues.cpp), not in
+ // `displayClue` / `applyClueSideEffects`, so the CD-side
+ // autosave below would never trigger for floppy.
+ byte before[Mystery::kCluesFoundCap];
+ memcpy(before, _mystery->_cluesFound, sizeof(before));
_vm->setPartnerEraseBg(&_bgSnapshot);
_vm->displayFloppyHotspotDialog(siteNum, hotIdx);
_vm->setPartnerEraseBg(nullptr);
+ bool foundNewClue = false;
+ for (uint i = 0; i < Mystery::kCluesFoundCap; i++) {
+ if (!before[i] && _mystery->_cluesFound[i]) {
+ foundNewClue = true;
+ break;
+ }
+ }
+ if (foundNewClue) {
+ const Common::Error err =
+ _vm->saveProfile(_vm->playerName());
+ if (err.getCode() != Common::kNoError)
+ warning("auto-save after floppy clue failed: %s",
+ err.getDesc().c_str());
+ }
return;
}
Commit: c5336a89fde423fede72417d7583c5b3aa3b124f
https://github.com/scummvm/scummvm/commit/c5336a89fde423fede72417d7583c5b3aa3b124f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:54+02:00
Commit Message:
EEM: setup screen for floppy screen
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index fc5b53f4b0a..289a90a3e06 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -820,15 +820,26 @@ void EEMEngine::doShowScrapbook(uint stage) {
}
void EEMEngine::doSetup() {
- // Mirrors `_DoSetup @ 1f78:044e`. The setup screen is BG `PIC 0x40`
- // (loaded once on entry) with every label baked in â "Setup",
- // "Partner", "Sound", "Music", the "Jake"/"Jenny"/"On"/"Off"
- // option strings, etc. â all rendered in palette key `0xFE`. The
- // original then runs `_SetupSettings @ 1f78:000d` which uses
- // `_SwapColors @ 172b:1d2a` to recolour those `0xFE` pixels per
- // label rect: `0x15` for the active state, `0` for the inactive
- // one. So nothing is drawn as text; the visible state of each
- // toggle is purely a per-rect colour swap on top of `PIC 0x40`.
+ // Mirrors `_DoSetup @ 1f78:044e` (CD) and `_DoSetup_Floppy @
+ // 1ee2:0387` (floppy). Both variants share the same PIC 0x40 BG
+ // with text labels baked in, the same 13 button rectangles at
+ // `_SetupButtons @ 29be:1218` (CD) / `2608:0d8c` (floppy), and the
+ // same 4 highlight rectangles at `0xe94..0xeb3` (Kid1 / Kid2 /
+ // SoundOn / SoundOff). Floppy additionally pre-loads a few overlay
+ // PICs (0x9b..0x9e + 0x1fa) that the CD uses on demand, but the
+ // behaviour is identical â colour-key swap on `0xFE` to indicate
+ // active state, click dispatch via the same 12-entry handler
+ // jumptable. So a single shared handler covers both variants.
+ //
+ // The setup screen is BG `PIC 0x40` (loaded once on entry) with
+ // every label baked in â "Setup", "Partner", "Sound", "Music",
+ // the "Jake"/"Jenny"/"On"/"Off" option strings, etc. â all
+ // rendered in palette key `0xFE`. The original then runs
+ // `_SetupSettings @ 1f78:000d` which uses `_SwapColors @
+ // 172b:1d2a` to recolour those `0xFE` pixels per label rect:
+ // `0x15` for the active state, `0` for the inactive one. So
+ // nothing is drawn as text; the visible state of each toggle is
+ // purely a per-rect colour swap on top of `PIC 0x40`.
//
// Click hit-tests go through `_SetupButtons @ 29be:1218` â 13Ã
// 8-byte rects. Each click runs `HandleSetupButton @ 1f78:0158`,
@@ -2618,9 +2629,18 @@ void EEMEngine::doBigMap() {
uint32 mapLastTick = mapStartTick;
// Static rectangles read directly from the binary at the labelled
- // addresses (29be:0x1596 onwards). Format is {x1, y1, x2, y2}.
+ // addresses (CD `29be:0x1596` / floppy `2608:0x13fe..0x143e`).
+ // Format is {x1, y1, x2, y2}. The floppy click table at
+ // `2608:1436` (verified at `_BigMapInteractionLoop_Floppy @
+ // 1fed:0a3a`) puts the setup button at (251, 3, 315, 42) â 1 px
+ // up and 1 px left of the CD's (252, 4, 315, 42). The floppy
+ // PIC 0x42 BG paints the visible button border at the same
+ // pixels, so use the variant-specific rect to match the
+ // hit-test region the original uses for that variant.
const Common::Rect kBigMapWindow ( 0, 0, 247, 192); // 29be:1596
- const Common::Rect kSetupBtnRect (252, 4, 315, 42); // 29be:15ce
+ const Common::Rect kSetupBtnRect = isFloppy()
+ ? Common::Rect(251, 3, 315, 42) // 2608:1436
+ : Common::Rect(252, 4, 315, 42); // 29be:15ce
bool wantZoom = false;
int zoomX = 0;
@@ -2749,7 +2769,9 @@ void EEMEngine::doBigMap() {
const Common::Rect kArrowXRight(224, 175, 234, 185);
const Common::Rect kXSlider ( 15, 175, 221, 185);
const Common::Rect kYSlider (237, 14, 247, 160);
- const Common::Rect kSetupBtn (252, 4, 315, 42);
+ const Common::Rect kSetupBtn = isFloppy()
+ ? Common::Rect(251, 3, 315, 42) // 2608:1436
+ : Common::Rect(252, 4, 315, 42); // 29be:15ce
const int kArrowStep = 16;
const int kSliderRange = mapW - kMapWinW;
Commit: 021009a600d68e647c7195cf6bc31a590e2e32a9
https://github.com/scummvm/scummvm/commit/021009a600d68e647c7195cf6bc31a590e2e32a9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:54+02:00
Commit Message:
EEM: correctly validate clues in floppy version
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 289a90a3e06..479c062e623 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -1983,11 +1983,24 @@ void EEMEngine::drawNotebookFrame(int &page) {
int clueCursor = 0;
Common::Array<int> pageStarts;
pageStarts.push_back(0);
- // Floppy NoteIndex entries are 7 bytes (`u16 ?; u16 jakeOff; u16
- // jennyOff; u8 score`) with ABSOLUTE byte offsets into the mystery
- // blob, while CD entries are 4 bytes with offsets relative to the
- // TextBlock at header[+0xc]. Resolve the right text for the active
- // partner / variant once per render.
+ // Floppy NoteIndex entries are 7 bytes:
+ // +0..1 clue text offset (ABSOLUTE byte offset into the
+ // mystery blob; partner-agnostic clue statement â
+ // this is what the notebook displays).
+ // +2..3 Jake's spoken line offset (used by
+ // `_DisplayHotspotClue_Floppy` when Jake narrates a
+ // dialog record â NOT for the notebook).
+ // +4..5 Jenny's spoken line offset (same, for Jenny).
+ // +6 score.
+ // Verified at `_DrawNotes_Floppy / FUN_15e0_01e8`'s
+ // `*(int *)(notes + idx * 7)` access (no `+2`/`+4` shift â
+ // always reads byte +0). The earlier port used the dialog
+ // spoken line for the notebook, which showed the partner's
+ // "Hi, Jake! Hi, Jenny!" intro instead of the actual clue
+ // statement ("We received a note that says..."). CD entries
+ // are 4 bytes with offsets relative to the TextBlock at
+ // header[+0xc]. Resolve the right text for the active variant
+ // once per render.
const bool floppyNb = isFloppy();
const byte *bufBase = _mystery.blobAt(0);
const uint32 mysSz = _mystery.dataSize();
@@ -1995,10 +2008,7 @@ void EEMEngine::drawNotebookFrame(int &page) {
if (!ni || clueId >= niCount)
return Common::String();
if (floppyNb && bufBase) {
- const uint stride = 7;
- const uint16 textOff = (_partner == 0)
- ? READ_LE_UINT16(ni + clueId * stride + 2)
- : READ_LE_UINT16(ni + clueId * stride + 4);
+ const uint16 textOff = READ_LE_UINT16(ni + clueId * 7);
if (textOff == 0 || textOff >= mysSz)
return Common::String();
const char *p = (const char *)(bufBase + textOff);
@@ -2332,24 +2342,46 @@ void EEMEngine::doGallery() {
continue;
if (!ni || clueId >= niCount)
continue;
- // Floppy notes are 7-byte entries: u16 ?,
- // u16 jakeOff, u16 jennyOff, u8 score. The
- // partner-specific text offset is at +2
- // (Jake) or +4 (Jenny) â verified at
- // `FUN_22dc_05c8 @ 22dc:0843`. CD notes are
- // 4 bytes: u16 textOff, u16 score.
- uint16 textOff;
+ // Floppy notes are 7-byte entries:
+ // +0..1 clue text (absolute offset)
+ // +2..3 Jake spoken line
+ // +4..5 Jenny spoken line
+ // +6 score
+ // `_DrawNotes_Floppy / FUN_15e0_01e8`'s
+ // `*(int *)(notes + idx * 7)` shows the
+ // notebook always uses +0 â the partner-
+ // agnostic clue statement. The +2/+4
+ // offsets are the partner spoken lines used
+ // by `FUN_22dc_05c8` when rendering dialog
+ // records (NOT this notebook view). CD
+ // notes are 4 bytes: u16 textOff, u16
+ // score.
+ Common::String txt;
if (floppyMI) {
- const uint partnerByte = (_partner == 0)
- ? 2 : 4;
- textOff = READ_LE_UINT16(
- ni + clueId * 7 + partnerByte);
+ const uint16 textOff =
+ READ_LE_UINT16(ni + clueId * 7);
+ const byte *bb = _mystery.blobAt(0);
+ const uint32 dsz =
+ _mystery.dataSize();
+ if (bb && textOff != 0 &&
+ textOff < dsz) {
+ const char *p =
+ (const char *)(bb + textOff);
+ uint32 len = 0;
+ while (textOff + len < dsz &&
+ p[len] != 0)
+ len++;
+ txt = parseString(
+ Common::String(p, len),
+ _playerName, _partner);
+ }
} else {
- textOff = READ_LE_UINT16(ni + clueId * 4);
+ const uint16 textOff =
+ READ_LE_UINT16(ni + clueId * 4);
+ txt = parseString(
+ _mystery.textAt(textOff),
+ _playerName, _partner);
}
- Common::String txt =
- parseString(_mystery.textAt(textOff),
- _playerName, _partner);
if (txt.empty())
continue;
const byte color =
@@ -3162,6 +3194,13 @@ bool EEMEngine::doAccuseNotes() {
Picture accuseBg;
const bool haveBg = _picsArchive.getPicture(0x1a7, accuseBg);
+ // Reset selection on entry. `FUN_1d40_0e07 @ 1d40:0e34` (floppy)
+ // explicitly zeroes the 0x7f-byte `_NoteSelected_Floppy` array
+ // before drawing, so the player always starts with a clean board.
+ // Without this, leftover selections from a failed attempt persist
+ // and the post-selection 100-point gate gets the wrong sum.
+ memset(_mystery._noteSelected, 0, sizeof(_mystery._noteSelected));
+
// Required count for solving â `6 - chainStage`.
const uint expected = (_chainStage >= 1 && _chainStage <= 3)
? (uint)(6 - _chainStage)
@@ -3206,6 +3245,37 @@ bool EEMEngine::doAccuseNotes() {
int numPages = 1;
pageBreaks[0] = 0;
+ // Variant-aware text resolver. CD note entries are 4 bytes
+ // (u16 textOff RELATIVE to TextBlock, u16 score), so the offset
+ // is added to `_textOffset` via `Mystery::textAt`. Floppy
+ // entries are 7 bytes:
+ // +0..1 clue text offset (ABSOLUTE byte offset into the
+ // mystery blob â partner-agnostic clue statement;
+ // this is what the notebook displays).
+ // +2..3 Jake spoken-line offset (`FUN_22dc_05c8 @ 22dc:0843`).
+ // +4..5 Jenny spoken-line offset.
+ // +6 score (`FUN_1d40_0c48`).
+ // `_DrawNotes_Floppy / FUN_15e0_01e8` reads `*(int *)(notes +
+ // idx * 7)` â always byte +0 â for the notebook text.
+ // Reading +2/+4 here showed the partner's "Hi, Jake! Hi,
+ // Jenny!" intro instead of the actual clue statement.
+ const bool floppyNote = isFloppy();
+ const byte *bufBaseNotes = _mystery.blobAt(0);
+ auto noteText = [&](uint clueId) -> Common::String {
+ if (floppyNote) {
+ const uint16 textOff = READ_LE_UINT16(ni + clueId * 7);
+ if (textOff == 0 || textOff >= _mystery.dataSize() ||
+ !bufBaseNotes)
+ return Common::String();
+ return parseString(
+ (const char *)(bufBaseNotes + textOff),
+ _playerName, _partner);
+ }
+ const uint16 textOff = READ_LE_UINT16(ni + clueId * 4);
+ return parseString(_mystery.textAt(textOff),
+ _playerName, _partner);
+ };
+
auto rebuildPagination = [&]() {
numPages = 1;
pageBreaks[0] = 0;
@@ -3214,11 +3284,8 @@ bool EEMEngine::doAccuseNotes() {
for (uint i = 0; i < found.size(); i++) {
const uint clueId = found[i];
Common::String txt;
- if (clueId < niCount) {
- const uint16 textOff = READ_LE_UINT16(ni + clueId * 4);
- txt = parseString(_mystery.textAt(textOff),
- _playerName, _partner);
- }
+ if (clueId < niCount)
+ txt = noteText(clueId);
Common::Array<Common::String> wrapped;
_font.wordWrapText(txt, rectW, wrapped);
const int h = (int)wrapped.size() * lineH;
@@ -3282,11 +3349,8 @@ bool EEMEngine::doAccuseNotes() {
for (int i = startIdx; i < endIdx; i++) {
const uint clueId = found[i];
Common::String txt;
- if (clueId < niCount) {
- const uint16 textOff = READ_LE_UINT16(ni + clueId * 4);
- txt = parseString(_mystery.textAt(textOff),
- _playerName, _partner);
- }
+ if (clueId < niCount)
+ txt = noteText(clueId);
if (txt.empty())
txt = Common::String::format(
isSpanish() ? "nota %u" : "clue %u", clueId);
@@ -3408,6 +3472,40 @@ bool EEMEngine::doAccuseNotes() {
const uint clueId = slotClues[i];
_mystery._noteSelected[clueId] ^= 1;
dirty = true;
+
+ // Debug: dump current user-selected score so
+ // the post-selection 100-point gate behaviour
+ // is visible while picking. Mirrors the
+ // floppy `FUN_1d40_0c48` (sum of `note[+6]`
+ // across `_NoteSelected != 0`) and the CD
+ // `selectedPoints()` (sum of `note[+2]` u16
+ // across `_NoteSelected != 0`).
+ {
+ int total = 0;
+ uint selectedCount = 0;
+ const uint maxIdx = MIN<uint>(niCount,
+ Mystery::kCluesFoundCap);
+ const bool floppy = isFloppy();
+ for (uint j = 0; j < maxIdx; j++) {
+ if (!_mystery._noteSelected[j])
+ continue;
+ selectedCount++;
+ if (floppy) {
+ total += (int)ni[j * 7 + 6];
+ } else {
+ const int16 pts =
+ (int16)READ_LE_UINT16(
+ ni + j * 4 + 2);
+ total += (int)pts;
+ }
+ }
+ warning("EEM accuse: clue %u %s "
+ "(selected=%u points=%d)",
+ clueId,
+ _mystery._noteSelected[clueId]
+ ? "ON" : "OFF",
+ selectedCount, total);
+ }
break;
}
}
@@ -4361,6 +4459,53 @@ void EEMEngine::doAccuseFloppy() {
return;
}
+ // Clue selection screen â `FUN_1d40_0e07 @ 1d40:0e07`. The user
+ // must pick exactly `6 - chainStage` clues from the found list,
+ // rendered on the red accuse-mode BG (PIC 0x1A7). Verified by
+ // the asm at 1d40:0e34 reading `local_c = 6 - DAT_28da_3052`,
+ // matching the CD's `_DoAccuse @ 1df2:0bdd` expected count.
+ // `doAccuseNotes()` already handles this UI for both variants
+ // (note text reading is variant-aware via `noteTextOff`); it
+ // returns true on commit, false on ESC.
+ if (!doAccuseNotes()) {
+ _nextScreen = _lastScreen != kScreenInvalid
+ ? (ScreenId)_lastScreen : kScreenSite;
+ return;
+ }
+
+ // Second score gate â based on the CLUES THE USER ACTUALLY
+ // PICKED. `_DoAccuse_Floppy @ 1d40:0e07` (after the commit
+ // branch at 1d40:0f3a) reads `local_a = FUN_1d40_0c48()` (=
+ // sum of byte +6 across selected note entries) and:
+ // if (local_a < 100) â KDTextIndex[+6] hint (slot 3),
+ // bail back to last screen.
+ // else â call FUN_1d40_0c79 (gallery picker).
+ // Note this is distinct from the entry gate on
+ // `_GetSelectedPoints_Floppy @ 1d40:0c23` (which uses the
+ // auto-top-5 of FOUND clues â what `Mystery::selectedPoints`
+ // returns on floppy). A player can be "ready to solve" by score
+ // yet still pick the wrong subset; that's the case this gate
+ // catches.
+ int userSelectedScore = 0;
+ {
+ const byte *ni2 = _mystery.noteIndex();
+ const uint16 niCount2 = _mystery.noteIndexCount();
+ if (ni2) {
+ const uint maxIdx = MIN<uint>(niCount2,
+ Mystery::kCluesFoundCap);
+ for (uint i = 0; i < maxIdx; i++) {
+ if (_mystery._noteSelected[i])
+ userSelectedScore += (int)ni2[i * 7 + 6];
+ }
+ }
+ }
+ if (userSelectedScore < 100) {
+ showFloppyKDHint(3);
+ _nextScreen = _lastScreen != kScreenInvalid
+ ? (ScreenId)_lastScreen : kScreenSite;
+ return;
+ }
+
// `FUN_1d40_0c79` â gallery picker. Show "Which suspect?" KD
// hint at slot 4 (KDTextIndex[+8]/2 = entry index 4), then
// render the gallery and wait for click.
@@ -4504,28 +4649,146 @@ void EEMEngine::doAccuseFloppy() {
const bool guilty = _mystery.isGuilty((uint)picked);
if (guilty) {
- // `_DisplayCorrect_Floppy @ 1d40:0894` â load PIC 5 (win BG),
- // walk the solved-clue chain at header[+0x12]: byte0 = count,
- // then `count` dialog records (same FUN_22dc_05c8 layout). We
- // reuse `displayFloppyDialogRecords` for the rendering.
- Picture winBg;
- if (_picsArchive.getPicture(5, winBg))
- blitAt(winBg, 0, 0);
- setSitePalette(0x22);
- g_system->updateScreen();
+ // Win path. Mirrors `_DisplayCorrect_Floppy @ 1d40:0894`:
+ // 1d40:08a0 _BuildBackground_Floppy(5, 0x42, 0x14);
+ // â PIC 0x3d frame + SITES entry 5 at (0x42,
+ // 0x14), palette = sitenum + 1 = 6.
+ // 1d40:08b1 _FadeIn();
+ // 1d40:08c0 _MIDIPlayFile("travel-2.xmi");
+ // 1d40:08d0 walk solved chain via _DisplayHotspotClue_Floppy
+ // + _WaitForClick per record. Mid-recap: when
+ // only 3 records remain, play TITLE.ANM(0)
+ // (transition graphic; not yet ported).
+ // 1d40:0939 ((u16 *)0x3054)[mysteryNum] =
+ // _firstTry ? 2 : 1;
+ // 1d40:0941 tier-promotion check (advance _chainStage when
+ // every mystery in the current tier is solved).
+ // 1d40:0982 _SavePlayerRecord (= saveProfile)
+ // 1d40:0985 _DeleteMysteryFile (= mystery cleanup)
+ // 1d40:09b0 _NextScreen = 0xc.
+ const uint mn = _mystery.number();
+
+ // Conclusion BG composition. `_BuildBackground_Floppy @
+ // 16e2:12fd` blits PIC 0x3d (the desk frame) onto a cleared
+ // page, then overlays SITES.DBD entry 5 (the conclusion
+ // artwork) at (param_2, param_3) = (0x42, 0x14). Palette
+ // becomes `sitenum + 1` = 6. Same composition the CD `doAccuse`
+ // win flow performs (see ui.cpp:4145-4166), just driven from
+ // the floppy data archives.
+ {
+ Graphics::Surface *blk = g_system->lockScreen();
+ if (blk) {
+ memset(blk->getPixels(), 0, 320 * 200);
+ g_system->unlockScreen();
+ }
+ setSitePalette(6);
+ Picture frame;
+ if (_picsArchive.loadEntry(0x3d, frame)) {
+ g_system->copyRectToScreen(frame.surface.getPixels(),
+ frame.surface.pitch, 0, 0,
+ frame.surface.w, frame.surface.h);
+ }
+ Picture scene;
+ if (5 < _sitesArchive.size() &&
+ _sitesArchive.loadEntry(5, scene)) {
+ const int sx = 0x42, sy = 0x14;
+ const int sw = MIN<int>(scene.surface.w, 320 - sx);
+ const int sh = MIN<int>(scene.surface.h, 200 - sy);
+ if (sw > 0 && sh > 0)
+ g_system->copyRectToScreen(scene.surface.getPixels(),
+ scene.surface.pitch, sx, sy, sw, sh);
+ }
+ // Partner sprite at (5, 0x50). The original keeps its
+ // `_NewAnimation` slot active across `_DisplayCorrect`'s
+ // `_BuildBackground_Floppy`, so `_UpdateAnimations` keeps
+ // re-stamping the partner over the fresh BG. We don't have
+ // a slot system, so manually bake the resting frame into
+ // the BG before the recap kicks off â `displayFloppyDialog
+ // Records` snapshots the screen on entry and the partner
+ // stays visible across every chain record. Without this
+ // the win sequence ends up partner-less. Anim 2 (Jake) /
+ // 0x10 (Jenny), script key 0x02 â same as the gallery
+ // picker and the CD accuse path (ui.cpp:4177).
+ const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
+ Animation partnerAni;
+ if (_aniArchive.loadAnimation(partnerAnim, partnerAni) &&
+ !partnerAni.empty()) {
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (screen) {
+ const uint frameIdx = partnerFrameAtTick(0x02,
+ (uint)partnerAni.size(),
+ g_system->getMillis());
+ blitAnimFrameAnchored(screen, partnerAni[frameIdx],
+ 5, 0x50);
+ g_system->unlockScreen();
+ }
+ }
+ g_system->updateScreen();
+ }
+
+ // Win music â TRAVEL-2.XMI (`2608:0c84`, played via
+ // `_MIDIPlayFile` at `_DisplayCorrect_Floppy @ 1d40:08c0`).
+ // The CD path uses internal MUS index 5; the floppy plays the
+ // XMI directly by filename. `_voiceOn` (= `DAT_28da_3050`) is
+ // the gate the original checks first.
+ if (_music && _voiceOn)
+ _music->playFile(Common::Path("travel-2.xmi"), false);
+
+ // Walk the solved-clue chain. Header[+0x12] points at a
+ // `count` byte followed by `count` dialog records (same layout
+ // as hotspot dialogs).
const byte *chain = _mystery.solvedClueBlock();
if (chain) {
const uint count = chain[0];
displayFloppyDialogRecords(chain + 1, count, 0);
}
- // Mark mystery solved â equivalent to
- // `((u16 *)0x5d20)... actually 0x3054[mysteryNum] = 1` which we
- // translate to clearing the in-progress mystery + saving the
- // profile (matches CD `doAccuse` win path).
- if (_partner == 0)
- _mystery._firstTry = false;
+ // Mark mystery solved with first-try bonus tracking.
+ // `_DisplayCorrect_Floppy @ 1d40:0939`:
+ // ((u16 *)0x3054)[mysteryNum] = 1;
+ // if (DAT_28da_35df != 0)
+ // ((u16 *)0x3054)[iVar5] = 2;
+ // `DAT_28da_35df` is `_firstTry`; it starts at 1 on
+ // `_ReadMystery_Floppy` and is cleared to 0 by
+ // `_DisplayAlibi_Floppy` on a wrong accusation. So 2 = won on
+ // first try, 1 = won after at least one alibi.
+ if (mn < sizeof(_mysteriesSolved))
+ _mysteriesSolved[mn] = _mystery._firstTry ? 2 : 1;
+
+ // Tier-promotion check. `_DisplayCorrect_Floppy @
+ // 1d40:0941..0978` walks the current tier's mystery range and
+ // advances `DAT_28da_3052` (= `_chainStage`) when every entry
+ // is non-zero. Skip mystery 0 (practice case) per the
+ // `if (DAT_2608_149c != 0)` guard at 1d40:093f.
+ if (mn != 0) {
+ uint lo = 0, hi = 0;
+ switch (_chainStage) {
+ case 1: lo = 1; hi = 0x18; break;
+ case 2: lo = 0x19; hi = 0x30; break;
+ case 3: lo = 0x31; hi = 0x36; break;
+ default: break;
+ }
+ bool allSolved = (hi >= lo);
+ for (uint i = lo; i <= hi && allSolved; i++) {
+ if (i >= sizeof(_mysteriesSolved) ||
+ _mysteriesSolved[i] == 0)
+ allSolved = false;
+ }
+ if (allSolved && _chainStage < 4) {
+ _chainStage++;
+ debugC(1, kDebugMystery,
+ "chainStage advanced to %u after solving mystery %u",
+ _chainStage, mn);
+ }
+ }
+
+ // Persist progress before clearing the in-progress mystery.
+ // `_DisplayCorrect_Floppy @ 1d40:0982` calls
+ // `_SavePlayerRecord` then `FUN_22dc_0dbd` (which deletes the
+ // per-mystery save file via DOS int 21h). Order matters here
+ // for the same reason as the CD path â clear â save so the
+ // profile records `hasMystery=false`.
_mystery._solvedPuzzle = true;
_mystery.clear();
(void)saveProfile(_playerName);
@@ -4534,15 +4797,24 @@ void EEMEngine::doAccuseFloppy() {
}
// Innocent â `_DisplayAlibi_Floppy @ 1d40:00df`:
- // 1. Load PIC 0x3e (alibi BG), draw at (0x42, 0x14).
- // 2. Load suspect picID, draw portrait at (0x82, 0x5a).
- // 3. Read alibi text via TEXT_BLOCK[suspect[+3] * 2]; render in
- // a balloon picked by digit-prefix dispatch.
- // 4. Voice from per-partner table at suspect_entry[+5..]
- // (we approximate using slot 13/14 â Jake/Jenny "wrong"
- // sting; original picks per-suspect).
- // 5. Wait for click, then KD reaction balloon
- // (KDTextIndex[+0x10]/2 = slot 8) + voice slot 5.
+ // 1d40:00fb _GetBackground(0x3e);
+ // 1d40:0103 _GetPicture(suspect.picID);
+ // 1d40:010c _AddPicBackground(susp_pic, 0x82, 0x5a);
+ // 1d40:0125 _MIDIPlayFile("fanfare2.xmi"); // alibi sting
+ // 1d40:0145 alibi-balloon table at `2608:0c0a + first_char`
+ // (separate from the KD digitâballoon table at
+ // `2608:0c14` â yes, 0xc0a, NOT 0xc14, verified
+ // via the `*(char *)(*local_a + 0xc0a)` access).
+ // Default = `DAT_2608_0c3c` = 0x2c.
+ // 1d40:01a3 balloon centred:
+ // bx = (0x140 - balloon.w) / 2;
+ // by = (0x5a - balloon.h) / 2;
+ // 1d40:01d5 WordWrap alibi text inside balloon, _WaitForClick.
+ // 1d40:01ee KD reaction at KDTextIndex[+10] = slot 5
+ // (NOT slot 8 â slot 8 is the gallery prompt).
+ // Balloon at (0x21, (0x50 - h) / 2).
+ // 1d40:0247 _NextScreen = _LastScreen;
+ // 1d40:024b _firstTry = 0;
const byte *susp = _mystery.floppySuspectEntry((uint)picked);
uint16 picId = 0;
uint16 alibiOff = 0xFFFF;
@@ -4551,6 +4823,11 @@ void EEMEngine::doAccuseFloppy() {
alibiOff = _mystery.alibiTextOffset((uint)picked);
}
+ // Alibi-screen MIDI sting (FANFARE2.XMI). `_MIDIPlayFile` is
+ // gated on `_voiceOn` (= `DAT_28da_3050`) in the original.
+ if (_music && _voiceOn)
+ _music->playFile(Common::Path("fanfare2.xmi"), false);
+
Picture alibiBg;
const bool haveAlibiBg = _picsArchive.getPicture(0x3e, alibiBg);
Picture suspectPic;
@@ -4567,18 +4844,24 @@ void EEMEngine::doAccuseFloppy() {
_playerName, _partner);
}
- // Digit-prefix â alibi balloon idx. CD has its own table at
- // 29be:1050; floppy uses the same KD digitâballoon mapping for
- // the alibi too (verified by inspecting `_DisplayAlibi_Floppy`'s
- // `_GetKDTextBalloon_Floppy` call).
- static const uint8 kDigitToBalloon[10] = {
- 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1c, 0x1d, 0x1e, 0x0a
+ // Alibi balloon dispatch. The original reads the alibi-balloon
+ // table at `2608:0c0a + first_char` â a SEPARATE table from the
+ // KD-hint table at `2608:0c14`. For digits '0'..'9' the values at
+ // `2608:0c3a..0c43` are { 0x2a, 0x2b, 0x2c, 0x2d, 0xaa, 0xab,
+ // 0xac, 0xad, 0x09, 0x0a }; default (non-digit char) is
+ // `DAT_2608_0c3c` = 0x2c. The high-bit values (0xAA..0xAD) are
+ // `_GetBalloon`'s "mirrored" flag â the low 7 bits are the
+ // balloon idx, top bit flips horizontally.
+ static const uint8 kFloppyAlibiBalloonByDigit[10] = {
+ 0x2a, 0x2b, 0x2c, 0x2d, 0xaa, 0xab, 0xac, 0xad, 0x09, 0x0a
};
- uint balloonIdx = 0x17;
+ uint balloonRaw = 0x2c; // default `DAT_2608_0c3c`
if (!alibi.empty() && alibi[0] >= '0' && alibi[0] <= '9') {
- balloonIdx = kDigitToBalloon[(int)(alibi[0] - '0')];
+ balloonRaw = kFloppyAlibiBalloonByDigit[(int)(alibi[0] - '0')];
alibi.deleteChar(0);
}
+ const uint balloonIdx = balloonRaw & 0x7F;
+ const bool flipBalloon = (balloonRaw & 0x80) != 0;
// Compose alibi screen.
Graphics::ManagedSurface scene(320, 200,
@@ -4595,15 +4878,35 @@ void EEMEngine::doAccuseFloppy() {
Picture balloon;
const bool haveBalloon = _balloonArchive.size() > balloonIdx &&
_balloonArchive.loadEntry(balloonIdx, balloon);
+ // Centred per `_DisplayAlibi_Floppy @ 1d40:01a0`:
+ // uVar2 = (0x140 - balloon.w) / 2; // x
+ // uVar3 = (0x5a - balloon.h) / 2; // y (anchor in top-half)
int balloonX = 0x21;
int balloonY = 1;
if (haveBalloon) {
- if (balloon.surface.h < 0x4e)
- balloonY = (0x50 - balloon.surface.h) / 2;
+ balloonX = (320 - balloon.surface.w) / 2;
+ balloonY = (0x5a - balloon.surface.h) / 2;
+ if (balloonX < 0) balloonX = 0;
+ if (balloonY < 0) balloonY = 0;
const byte transp = (byte)(balloon.flags >> 8);
- scene.transBlitFrom(balloon.surface,
- Common::Point(balloonX, balloonY),
- (uint32)transp);
+ // `_GetBalloon`'s mirror flag (high bit of the table value)
+ // flips the balloon horizontally â the original applies it
+ // inside the blit primitive. We emulate by reading the source
+ // row in reverse.
+ for (int row = 0; row < balloon.surface.h && balloonY + row < 200;
+ row++) {
+ const byte *src =
+ (const byte *)balloon.surface.getBasePtr(0, row);
+ byte *dst = (byte *)scene.getBasePtr(balloonX, balloonY + row);
+ for (int col = 0;
+ col < balloon.surface.w && balloonX + col < 320; col++) {
+ const int srcCol = flipBalloon
+ ? (balloon.surface.w - 1 - col) : col;
+ const byte px = src[srcCol];
+ if (px != transp)
+ dst[col] = px;
+ }
+ }
}
uint16 tx = 5, ty = 4, tw = 155;
getBalloonInsets(balloonIdx, tx, ty, tw);
@@ -4614,12 +4917,11 @@ void EEMEngine::doAccuseFloppy() {
g_system->copyRectToScreen(scene.getPixels(), scene.pitch, 0, 0, 320, 200);
g_system->updateScreen();
- // Voice â suspect alibi voice. The original picks per-suspect from
- // the alibi voice table at `2608:0c5e + suspectIdx`; we don't have
- // the table indexed, so play slot 13 (= F-0161SL.VOC / M-0113SL.VOC)
- // as a generic alibi cue.
- if (_audio)
- _audio->playFloppyVoiceSlot(13, _partner);
+ // `_DisplayAlibi_Floppy` does NOT play any per-suspect VOC â the
+ // alibi table at `2608:0c5e` (3 bytes: 0x15, 0x16, 0x17) is
+ // referenced by the post-win scrapbook (`FUN_1d40_05b7`), not the
+ // alibi screen. The MIDI sting we kicked off above carries the
+ // audio.
while (!shouldQuit()) {
Common::Event ev;
@@ -4639,8 +4941,13 @@ void EEMEngine::doAccuseFloppy() {
g_system->delayMillis(10);
}
- // Partner reaction â KDTextIndex slot 8 (= +0x10).
- showFloppyKDHint(8);
+ // Partner reaction â `KDTextIndex[+10]` is byte offset 10 = u16
+ // stride entry 5 (NOT 8). Verified at
+ // `_DisplayAlibi_Floppy @ 1d40:01ee`:
+ // `iVar4 = MysteryOff + *(KDTextIndex + 10);`
+ // Our `showFloppyKDHint(slot)` reads `kdIdx + slot * 2`, so slot
+ // 5 is the right argument here.
+ showFloppyKDHint(5);
_mystery._firstTry = false;
_nextScreen = _lastScreen != kScreenInvalid
Commit: 7aa9357fb6ba37941294d886d596224b222a2cab
https://github.com/scummvm/scummvm/commit/7aa9357fb6ba37941294d886d596224b222a2cab
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:54+02:00
Commit Message:
EEM: simplify savegame handling
Changed paths:
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/mystery.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 03a2449d26b..5f1d15f0c15 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -58,12 +58,7 @@ const uint kPalEAKids = 0x25;
const uint kPalHighScore = 0x27;
const uint kPalStormLogo = 0x26; ///< Floppy `FUN_23d2_0605` palette idx
-// Save body version, used by the `Common::Serializer` inside
-// `saveGameStream`/`loadGameStream`. The framework's extended-save
-// header (description / thumbnail / playtime) is appended/parsed
-// separately by `Engine::saveGameState` / `MetaEngine::readSavegameHeader`,
-// so we don't need a magic word or our own metadata fields.
-const byte kSaveBodyVer = 3;
+const byte kSaveBodyVer = 1;
// 11x16 mouse cursor â replaces the DOS hardware cursor wired in by
// _InitMouse @ 152d:018b (INT 33h). The original game sets the cursor
@@ -797,9 +792,8 @@ Common::Error EEMEngine::saveGameStream(Common::WriteStream *stream,
(void)isAutosave;
// Body header: one byte version. `Common::Serializer::setVersion`
- // alone doesn't write/read the version â we emit it explicitly so
- // `loadGameStream` knows which fields are present. Older saves
- // (v1) lack `_chainStage`; newer ones include it.
+ // alone doesn't write/read the version, so emit it explicitly and
+ // require an exact match on load.
Common::Serializer s(nullptr, stream);
s.setVersion(kSaveBodyVer);
byte ver = kSaveBodyVer;
@@ -821,16 +815,12 @@ Common::Error EEMEngine::saveGameStream(Common::WriteStream *stream,
// 2=solved on first try) â `_DisplayCorrect`
// writes 1 always, 2 when `_FirstTry != 0`.
//
- // We persist the gameplay-meaningful subset (name + solved table +
- // partner) and skip the filename-derivation bytes. The voice /
- // chain-stage fields aren't yet wired into the C++ port; we save
- // space for them so they slot in without a version bump.
+ // We persist the gameplay-meaningful subset and skip the original
+ // filename-derivation bytes.
s.syncString(_playerName);
s.syncBytes(_mysteriesSolved, sizeof(_mysteriesSolved));
s.syncAsByte(_partner);
- // v2+: chain-stage tier (1=Junior, 2=Senior, 3=Master).
s.syncAsByte(_chainStage);
- // v3+: voice on/off flag (DAT_2d5d_3f97).
s.syncAsByte(_voiceOn);
// ScummVM-only extension: persist the in-progress mystery so the
@@ -858,8 +848,8 @@ Common::Error EEMEngine::loadGameStream(Common::SeekableReadStream *stream) {
Common::Serializer s(stream, nullptr);
byte ver = 0;
s.syncAsByte(ver);
- if (ver > kSaveBodyVer) {
- warning("loadGameStream: save body version %u newer than %u â refusing",
+ if (ver != kSaveBodyVer) {
+ warning("loadGameStream: unsupported save body version %u (expected %u)",
ver, kSaveBodyVer);
return Common::kReadingFailed;
}
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index e78e44066c3..44a90e4fdf4 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -142,9 +142,8 @@ public:
// ScummVM extended-save hooks. The base `Engine::saveGameState` /
// `loadGameState` write/read the framework header (description,
// thumbnail, playtime, version) around our body via these
- // streams. We keep all per-profile state in the body, with a
- // single `Common::Serializer` version so future field additions
- // stay backward-compatible.
+ // streams. We keep all per-profile state in the body and only
+ // accept the current private body layout.
Common::Error saveGameStream(Common::WriteStream *stream,
bool isAutosave = false) override;
Common::Error loadGameStream(Common::SeekableReadStream *stream) override;
diff --git a/engines/eem/mystery.cpp b/engines/eem/mystery.cpp
index 06ce1d53f7b..3b6cb6b3c98 100644
--- a/engines/eem/mystery.cpp
+++ b/engines/eem/mystery.cpp
@@ -564,22 +564,6 @@ void Mystery::syncState(Common::Serializer &s) {
s.syncArray(_hotSpotsSeen, kHotSpotsCap, Common::Serializer::Uint16LE);
s.syncArray(_inGallery, kGalleryCap, Common::Serializer::Uint16LE);
s.syncBytes(_newOrder, kGalleryCap);
- // Save profiles created before the floppy `_newOrder` identity init
- // was added stored all-zeros here. Loading that back over the
- // identity set by `Mystery::load()` makes every dialog with byte9>0
- // resolve to slot 0 (so the second suspect found at a CONSITE
- // overwrites the first). Detect the legacy zero-fill on read and
- // rebuild the identity mapping that the loader produced.
- if (s.isLoading()) {
- bool allZero = true;
- for (uint i = 0; i < kGalleryCap; i++) {
- if (_newOrder[i] != 0) { allZero = false; break; }
- }
- if (allZero) {
- for (uint i = 0; i < kGalleryCap; i++)
- _newOrder[i] = (uint8)i;
- }
- }
s.syncArray(_visitedSite, kVisitedSiteCap, Common::Serializer::Uint16LE);
s.syncArray(_onSites, kVisitedSiteCap, Common::Serializer::Uint16LE);
s.syncAsByte(_sawCOFFSITEs);
Commit: 2b45b48e89ec1b28769d043fe3a43feb879ea9cd
https://github.com/scummvm/scummvm/commit/2b45b48e89ec1b28769d043fe3a43feb879ea9cd
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:55+02:00
Commit Message:
EEM: fix invalid state transition in the handling of the setup screen
Changed paths:
engines/eem/eem.cpp
engines/eem/site.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 5f1d15f0c15..9be01f7b182 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -392,8 +392,9 @@ screen_loop:
case kScreenMap:
case kScreenMapAlt:
// Handler 1/2 at 1a35:0e25 calls `_DoMapScreen @
- // 20fe:120b` which manages its own `_NextScreen` writes â
- // 3 (a site was clicked), 6 (setup), or 0xffff (quit).
+ // 20fe:120b` (floppy: 19bb:0ef3 -> 1fed map code),
+ // which manages its own `_NextScreen` writes â 3 (a site
+ // was clicked), 6 (setup), or 0xffff (quit).
// Our `doBigMap` keeps the original's "click site, then
// enter the site loop" behaviour inline; once it returns
// the natural next state is SITE.
@@ -406,14 +407,9 @@ screen_loop:
case kScreenSite:
// Handler 3 at 1a35:0e2c calls `_DoSiteLoop @
- // 168d:03f4`. Our `doSiteLoop` is a complete loop â
- // notebook / gallery / accuse / map are dispatched
- // inline within `SiteScreen::run`. The accusation tail
- // in `doAccuse` (see ui.cpp) writes the next screen:
- // kScreenAction on win (matches `_DisplayCorrect @
- // 1df2:0895` writing 0xc) or kScreenSite on lose
- // (matches `_DisplayAlibi @ 1df2:043f` snapping back
- // via `_LastScreen`).
+ // 168d:03f4` (floppy's equivalent dispatches through
+ // 1652). Site writes `_NextScreen` for PDA / map rather
+ // than entering those screens as nested modals.
doSiteLoop();
if (!_mystery.isLoaded())
_nextScreen = kScreenChooseMystery;
@@ -421,6 +417,28 @@ screen_loop:
_nextScreen = kScreenInvalid; // user quit
break;
+ case kScreenNotebook:
+ // Handler 4 calls the PDA notebook screen. Its button
+ // handler writes 2 (map), 3 (site), 5 (gallery), or 7
+ // (accuse) and then returns to this dispatcher.
+ doNotebook();
+ if (!_mystery.isLoaded() && _nextScreen != kScreenAction)
+ _nextScreen = kScreenChooseMystery;
+ else if (_nextScreen == current)
+ _nextScreen = kScreenSite;
+ break;
+
+ case kScreenGallery:
+ // Handler 5 calls the suspect gallery. Like the original,
+ // ESC and the site button write 3, the map button writes
+ // 2, and the PDA button writes 4.
+ doGallery();
+ if (!_mystery.isLoaded() && _nextScreen != kScreenAction)
+ _nextScreen = kScreenChooseMystery;
+ else if (_nextScreen == current)
+ _nextScreen = kScreenSite;
+ break;
+
case kScreenSetup:
// Handler 6 at 1a35:0e48 calls `_DoSetup @ 1f78:044e`.
// Reachable via the BigMap setup button which writes
@@ -431,6 +449,17 @@ screen_loop:
doSetup();
break;
+ case kScreenAccuse:
+ // Handler 7 runs the accusation flow. A failed accusation
+ // returns to `_LastScreen`; a correct solution writes
+ // 0xc (ACTION).
+ doAccuse();
+ if (!_mystery.isLoaded() && _nextScreen != kScreenAction)
+ _nextScreen = kScreenChooseMystery;
+ else if (_nextScreen == current)
+ _nextScreen = kScreenSite;
+ break;
+
default:
warning("screenDriver: unhandled screen id %d", (int)current);
_nextScreen = kScreenInvalid;
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 9b07bfb5d49..0938072384b 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -695,7 +695,8 @@ void SiteScreen::run() {
// Button 0: (35, 111) - (56, 136) â notebook
// (`_NextScreen = 4`)
// Button 1: (7, 177) - (57, 200) â map
- // (`_NextScreen = 1`)
+ // (CD `_NextScreen = 1`,
+ // floppy = 2)
// Test the buttons before falling through to hotspots so
// a click on the PDA / map icon doesn't accidentally
// trigger a hotspot underneath.
@@ -712,16 +713,14 @@ void SiteScreen::run() {
// the PDA / gallery `kBtnPartner` (5, 80, 44, 110).
const Common::Rect kBtnPartner ( 5, 80, 44, 110);
if (kBtnNotebook.contains(event.mouse.x, event.mouse.y)) {
- _vm->doNotebook();
- enter(cur);
- break;
+ _vm->setNextScreen(kScreenNotebook);
+ return;
}
if (kBtnMap.contains(event.mouse.x, event.mouse.y)) {
- _vm->doBigMap();
- if (_mystery->_siteNumber < _mystery->numSites())
- cur = _mystery->_siteNumber;
- enter(cur);
- break;
+ // CD writes `_NextScreen = 1`; floppy writes 2.
+ _vm->setNextScreen(_vm->isFloppy() ? kScreenMapAlt
+ : kScreenMap);
+ return;
}
if (kBtnPartner.contains(event.mouse.x, event.mouse.y)) {
_vm->doHelp();
@@ -748,7 +747,8 @@ void SiteScreen::run() {
// â MAP).
if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
if (_vm->areYouSure()) {
- _vm->setNextScreen(kScreenMap);
+ _vm->setNextScreen(_vm->isFloppy() ? kScreenMapAlt
+ : kScreenMap);
return;
}
enter(cur);
@@ -762,10 +762,7 @@ void SiteScreen::run() {
if (exitRequested)
return;
- // `doAccuse` on a win clears the mystery (so the screen driver
- // can route to the post-mystery menu). Notebook / Gallery /
- // hotspot paths route through this same loop, so a transitive
- // `doAccuse` may have wiped `_mystery` underneath us â exit
+ // Hotspot side effects can invalidate the active mystery; exit
// immediately rather than tick another frame against stale BG
// snapshots / hotspot tables.
if (!_mystery || !_mystery->isLoaded())
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 479c062e623..06c0377e833 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -1812,11 +1812,13 @@ void EEMEngine::doNotebook() {
while (g_system->getEventManager()->pollEvent(ev)) {
if (ev.type == Common::EVENT_QUIT ||
ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ _nextScreen = kScreenInvalid;
exitFlag = true;
break;
}
if (ev.type == Common::EVENT_KEYDOWN) {
if (ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ _nextScreen = kScreenSite;
exitFlag = true;
break;
}
@@ -1838,11 +1840,12 @@ void EEMEngine::doNotebook() {
// rects directly. Earlier rects "win" when overlapping
// (matches `_FindButton`).
if (kBtnSite.contains(ev.mouse.x, ev.mouse.y)) {
+ _nextScreen = kScreenSite;
exitFlag = true;
break; // back to site
}
if (kBtnMap.contains(ev.mouse.x, ev.mouse.y)) {
- doBigMap();
+ _nextScreen = kScreenMapAlt;
exitFlag = true;
break;
}
@@ -1852,14 +1855,14 @@ void EEMEngine::doNotebook() {
continue;
}
if (kBtnAccuse.contains(ev.mouse.x, ev.mouse.y)) {
- doAccuse();
+ _nextScreen = kScreenAccuse;
exitFlag = true;
break;
}
if (kBtnGallery.contains(ev.mouse.x, ev.mouse.y)) {
- doGallery();
- dirty = true;
- continue;
+ _nextScreen = kScreenGallery;
+ exitFlag = true;
+ break;
}
if (kBtnHelp1.contains(ev.mouse.x, ev.mouse.y)) {
// rect 1 â `_InterfaceHelp(0)`: walks `HelpData[0]` and
@@ -2161,10 +2164,12 @@ void EEMEngine::doGallery() {
while (g_system->getEventManager()->pollEvent(ev)) {
if (ev.type == Common::EVENT_QUIT ||
ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ _nextScreen = kScreenInvalid;
return;
}
if (ev.type == Common::EVENT_KEYDOWN) {
if (ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ _nextScreen = kScreenSite;
exitFlag = true;
break;
}
@@ -2192,18 +2197,19 @@ void EEMEngine::doGallery() {
const Common::Rect kBtnHelp ( 93, 174, 115, 190); // [1] HELP
const Common::Rect kBtnPartner ( 5, 80, 44, 110); // [3] KD HELP
if (kBtnSite.contains(ev.mouse.x, ev.mouse.y)) {
+ _nextScreen = kScreenSite;
exitFlag = true; break;
}
if (kBtnMap.contains(ev.mouse.x, ev.mouse.y)) {
- doBigMap();
+ _nextScreen = kScreenMapAlt;
exitFlag = true; break;
}
if (kBtnAccuse.contains(ev.mouse.x, ev.mouse.y)) {
- doAccuse();
+ _nextScreen = kScreenAccuse;
exitFlag = true; break;
}
if (kBtnNotebook.contains(ev.mouse.x, ev.mouse.y)) {
- // Already came from notebook; exiting returns to it.
+ _nextScreen = kScreenNotebook;
exitFlag = true; break;
}
if (kBtnHelp.contains(ev.mouse.x, ev.mouse.y)) {
Commit: 1cf324b52d5caa884d4ba69809121b48d356202e
https://github.com/scummvm/scummvm/commit/1cf324b52d5caa884d4ba69809121b48d356202e
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:55+02:00
Commit Message:
EEM: correctly save and reload viewed clues
Changed paths:
engines/eem/site.cpp
engines/eem/site.h
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 0938072384b..a36c0188248 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -1297,10 +1297,11 @@ void SiteScreen::renderHotspots(uint siteNum) {
const uint32 tickMs = g_system->getMillis();
// CD hotspot rows are 14 bytes each (rect + 6 bytes of clue
- // metadata). Floppy stores plain 8-byte rectangles only â clue
- // data lives in a separate dialog-record list at `site_data[+6]`,
- // keyed by hotspot index. Verified at `FUN_22dc_0b80 @ 22dc:0b80`.
- const uint stride = _vm && _vm->isFloppy() ? 8 : 14;
+ // metadata). The seen key is the zero-based mystery-wide ordinal
+ // at +0xa, not this site's local row index. Floppy stores plain
+ // 8-byte rectangles only, so its seen key remains the row index.
+ const bool floppy = _vm && _vm->isFloppy();
+ const uint stride = floppy ? 8 : 14;
for (uint i = 0; i < count; i++) {
const byte *r = spots + i * stride;
const int16 x1 = (int16)READ_LE_UINT16(r + 0);
@@ -1310,8 +1311,9 @@ void SiteScreen::renderHotspots(uint siteNum) {
const Common::Rect rect(MAX<int>(0, x1), MAX<int>(0, y1),
MIN<int>(screen->w, x2),
MIN<int>(screen->h, y2));
- const bool seen = (i < Mystery::kHotSpotsCap)
- && _mystery->_hotSpotsSeen[i];
+ const uint seenKey = floppy ? i : READ_LE_UINT16(r + 0xa);
+ const bool seen = seenKey < Mystery::kHotSpotsCap &&
+ _mystery->_hotSpotsSeen[seenKey];
if (seen) {
// `_DrawSolidRect` (172b:0506) â outline in palette
// index 0xFF (a fixed, non-cycling colour) so already-
@@ -1418,10 +1420,10 @@ void SiteScreen::onHotspotClicked(uint siteNum, uint hotIdx) {
// `_DoSiteLoop @ 168d:03f4` (after _DisplayClue):
// _HotSpotsSeen[hotspot[+0xa] * 2] = _HotSpotComplete;
- // The "seen" key is the hotspotIndex field (+0xa) â the 1-based
- // ordinal â NOT the array index. Two hotspots can share an ordinal
- // across sites (e.g., a partner's clue you can re-read), so this
- // matters for cross-site state.
+ // The seen key is the hotspotIndex field (+0xa) â a zero-based
+ // mystery-wide ordinal â NOT the site's local row index. Using the
+ // local index makes unrelated hotspots on later sites inherit the
+ // first site's seen state after travel or reload.
const byte *spots = _mystery->hotspots(siteNum);
uint hotOrdinal = hotIdx; // fallback to array index
if (spots) {
@@ -1429,8 +1431,6 @@ void SiteScreen::onHotspotClicked(uint siteNum, uint hotIdx) {
}
if (hotOrdinal < Mystery::kHotSpotsCap)
_mystery->_hotSpotsSeen[hotOrdinal] = 1;
- if (hotIdx < Mystery::kHotSpotsCap)
- _mystery->_hotSpotsSeen[hotIdx] = 1; // also mark by array idx for our render
_mystery->_searchLocationNumber = (uint16)hotIdx;
// Bytes 8..9 of each 14-byte hotspot rect = byte offset within the
diff --git a/engines/eem/site.h b/engines/eem/site.h
index 23621409806..97e83c087e4 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -80,7 +80,7 @@ void blitAnimFrameAnchored(Graphics::Surface *screen, const Picture &p,
struct Hotspot {
int16 x1, y1, x2, y2; ///< rectangle in screen coordinates
uint16 clueOffset; ///< +8: byte offset of ClueBlock in the mystery file
- uint16 hotspotIndex; ///< +10: 1-based hotspot ordinal within the site
+ uint16 hotspotIndex; ///< +10: zero-based mystery-wide seen ordinal
uint16 extra; ///< +12: unknown (zero in M0..M54)
Common::Rect rect() const { return Common::Rect(x1, y1, x2, y2); }
Commit: ec75e6ec08ee26087cb3fdd2ee290504ede16aa3
https://github.com/scummvm/scummvm/commit/ec75e6ec08ee26087cb3fdd2ee290504ede16aa3
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:55+02:00
Commit Message:
EEM: avoid saving slot 0
Changed paths:
engines/eem/eem.cpp
engines/eem/eem.h
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 9be01f7b182..8326cf54c36 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -959,10 +959,13 @@ Common::Error EEMEngine::saveProfile(const Common::String &name) {
}
}
- // New profile â pick the lowest unused slot. The MetaEngine caps
- // us at 99 by default (`getMaximumSaveSlot`); 25 was the DOS
- // original's limit (`screen8_handler` walks `*.PLR` up to 25
- // entries in `local_8c[0x19][2]`).
+ // New profile â pick the lowest unused visible slot. Slot 0 is
+ // filtered out by `listProfiles()` because it is ScummVM's
+ // conventional autosave slot, so allocating a new profile there
+ // makes the profile disappear from the picker on the next refresh.
+ // The MetaEngine caps us at 99 by default (`getMaximumSaveSlot`);
+ // 25 was the DOS original's limit (`screen8_handler` walks `*.PLR`
+ // up to 25 entries in `local_8c[0x19][2]`).
if (slot < 0) {
const int maxSlot = getMetaEngine()->getMaximumSaveSlot();
Common::Array<bool> used(maxSlot + 1);
@@ -971,7 +974,7 @@ Common::Error EEMEngine::saveProfile(const Common::String &name) {
if (sl >= 0 && sl <= maxSlot)
used[sl] = true;
}
- for (int i = 0; i <= maxSlot; i++) {
+ for (int i = 1; i <= maxSlot; i++) {
if (!used[i]) {
slot = i;
break;
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 44a90e4fdf4..f73c751f87e 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -157,8 +157,8 @@ public:
// description = player name) â same approach Wetlands uses.
/// Mirrors `_SavePlayerRecord @ 1c33:034f`. Saves into the slot
- /// whose description matches @p name, or the lowest unused slot
- /// if no match. Returns the kNoError on success.
+ /// whose description matches @p name, or the lowest unused visible
+ /// slot if no match. Returns the kNoError on success.
Common::Error saveProfile(const Common::String &name);
/// Mirrors `_LoadPlayerRecord @ 1c33:03a6`. Returns false if no
Commit: 2a0ec6f7e2ab4d704e0ac3b2ab32d7e9dad85f1e
https://github.com/scummvm/scummvm/commit/2a0ec6f7e2ab4d704e0ac3b2ab32d7e9dad85f1e
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:56+02:00
Commit Message:
EEM: bring virtual keyboard when typing name
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 06c0377e833..7f045894eba 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -504,13 +504,18 @@ void EEMEngine::doNewPlayer() {
0, 0, 320, 200);
g_system->updateScreen();
- while (!shouldQuit()) {
+ g_system->setFeatureState(OSystem::kFeatureVirtualKeyboard, true);
+
+ bool done = false;
+ while (!done && !shouldQuit()) {
Common::Event ev;
bool dirty = false;
while (g_system->getEventManager()->pollEvent(ev)) {
if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
- return;
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ done = true;
+ break;
+ }
if (ev.type != Common::EVENT_KEYDOWN)
continue;
const Common::KeyCode k = ev.kbd.keycode;
@@ -534,11 +539,13 @@ void EEMEngine::doNewPlayer() {
_chainStage = 1;
saveProfile(name);
}
- return;
+ done = true;
+ break;
}
if (k == Common::KEYCODE_ESCAPE) {
_playerName = "Detective";
- return;
+ done = true;
+ break;
}
if (k == Common::KEYCODE_BACKSPACE) {
if (!name.empty()) {
@@ -568,6 +575,8 @@ void EEMEngine::doNewPlayer() {
g_system->updateScreen();
g_system->delayMillis(15);
}
+
+ g_system->setFeatureState(OSystem::kFeatureVirtualKeyboard, false);
}
int EEMEngine::doShowEnding(uint num, bool firstPage) {
Commit: 8a575a0b4f4e2a1dbbdc9e3ef0e959f16286ad04
https://github.com/scummvm/scummvm/commit/8a575a0b4f4e2a1dbbdc9e3ef0e959f16286ad04
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:56+02:00
Commit Message:
EEM: correctly render background during enter animation
Changed paths:
engines/eem/site.cpp
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index a36c0188248..1672ef45a61 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -573,9 +573,19 @@ void SiteScreen::enter(uint siteNum) {
// the arrival on so re-entries (after notebook/map/etc.) don't
// repeat the animation.
if ((int)siteNum != _lastSiteAnim) {
+ // `_EnterSiteAnim` snapshots the current screen, so populate
+ // that temporary background with the same site layers that
+ // should already be visible behind the arriving partner.
+ if (_vm->isFloppy())
+ renderFloppyDrops(siteNum);
+ else
+ renderStaticDrops(siteNum);
+ renderAnimatedDrops(siteNum, g_system->getMillis());
enterSiteAnim();
_lastSiteAnim = (int)siteNum;
- // Re-paint the BG; the arrival animation drew on top of it.
+ // Re-paint the BG; the normal snapshot below should contain
+ // only the static layers, while animated NPCs are redrawn per
+ // tick by the frame pump.
renderBackground(siteNum);
}
Commit: dcb37c0a03cf1ea2246acdac995e0379a13ee515
https://github.com/scummvm/scummvm/commit/dcb37c0a03cf1ea2246acdac995e0379a13ee515
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:56+02:00
Commit Message:
EEM: original mouse pointer and new feature to hide highlight boxes
Changed paths:
engines/eem/detection.cpp
engines/eem/detection.h
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/metaengine.cpp
engines/eem/site.cpp
engines/eem/site.h
diff --git a/engines/eem/detection.cpp b/engines/eem/detection.cpp
index 60967493537..d0b9b5800f2 100644
--- a/engines/eem/detection.cpp
+++ b/engines/eem/detection.cpp
@@ -40,7 +40,7 @@ const ADGameDescription gameDescriptions[] = {
Common::EN_ANY,
Common::kPlatformDOS,
ADGF_NO_FLAGS,
- GUIO1(GUIO_NONE)
+ GUIO1(GAMEOPTION_HIDE_HIGHLIGHT_BOXES)
},
{
"eem",
@@ -50,7 +50,7 @@ const ADGameDescription gameDescriptions[] = {
Common::EN_ANY,
Common::kPlatformDOS,
ADGF_NO_FLAGS,
- GUIO1(GUIO_NONE)
+ GUIO1(GAMEOPTION_HIDE_HIGHLIGHT_BOXES)
},
{
// Spanish floppy release â same EEM.EXE binary as the English
@@ -64,7 +64,7 @@ const ADGameDescription gameDescriptions[] = {
Common::ES_ESP,
Common::kPlatformDOS,
ADGF_NO_FLAGS,
- GUIO1(GUIO_NONE)
+ GUIO1(GAMEOPTION_HIDE_HIGHLIGHT_BOXES)
},
AD_TABLE_END_MARKER
diff --git a/engines/eem/detection.h b/engines/eem/detection.h
index 395daab1c77..7d74db6cd7f 100644
--- a/engines/eem/detection.h
+++ b/engines/eem/detection.h
@@ -26,6 +26,8 @@
namespace EEM {
+#define GAMEOPTION_HIDE_HIGHLIGHT_BOXES GUIO_GAMEOPTIONS1
+
enum EEMDebugChannels {
kDebugGeneral = 1 << 0,
kDebugScript = 1 << 1,
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 8326cf54c36..8959705a931 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -57,13 +57,14 @@ const uint kPicStormLogo = 0x20b; ///< Floppy storm-logo still: PIC 0x20b
const uint kPalEAKids = 0x25;
const uint kPalHighScore = 0x27;
const uint kPalStormLogo = 0x26; ///< Floppy `FUN_23d2_0605` palette idx
+const uint kPicMousePointer = 0x50; ///< Original startup pointer; 0x51 is the wait cursor
const byte kSaveBodyVer = 1;
-// 11x16 mouse cursor â replaces the DOS hardware cursor wired in by
-// _InitMouse @ 152d:018b (INT 33h). The original game sets the cursor
-// visible/hidden via _MouseCursor; we leave it on once the screens
-// that need it (ChoosePartner, CaseSelection, sites) are reached.
+// Fallback 11x16 mouse cursor used if the selected PIC pointer cannot be
+// loaded. The original game sets the cursor visible/hidden via
+// _MouseCursor; we leave it on once the screens that need it
+// (ChoosePartner, CaseSelection, sites) are reached.
// 0 = transparent, 1 = black outline, 2 = white fill
const byte kCursorBitmap[11 * 16] = {
1,1,0,0,0,0,0,0,0,0,0,
@@ -88,11 +89,85 @@ const byte kCursorPalette[] = {
0x00, 0x00, 0x00, // 1 â outline
0xFF, 0xFF, 0xFF // 2 â fill
};
+const byte kCursorHotspotPalette[] = {
+ 0x00, 0x00, 0x00, // 0 â transparent (key)
+ 0xFF, 0x00, 0x00, // 1 â red outline
+ 0xFF, 0xFF, 0xFF // 2 â white fill
+};
+
+static void setHotspotCursorPalette(const Picture &cursor, byte transparent) {
+ byte palette[kPalSize];
+ bool used[256];
+ memset(used, 0, sizeof(used));
+
+ g_system->getPaletteManager()->grabPalette(palette, 0, 256);
+
+ for (int y = 0; y < cursor.surface.h; y++) {
+ const byte *src = (const byte *)cursor.surface.getBasePtr(0, y);
+ for (int x = 0; x < cursor.surface.w; x++) {
+ if (src[x] != transparent)
+ used[src[x]] = true;
+ }
+ }
+
+ int minLuma = 255;
+ int maxLuma = 0;
+ for (uint i = 0; i < 256; i++) {
+ if (!used[i])
+ continue;
+ const byte *rgb = palette + i * 3;
+ const int luma = (rgb[0] * 30 + rgb[1] * 59 + rgb[2] * 11) / 100;
+ minLuma = MIN(minLuma, luma);
+ maxLuma = MAX(maxLuma, luma);
+ }
+ const int outlineThreshold = (minLuma + maxLuma) / 2;
+
+ for (uint i = 0; i < 256; i++) {
+ if (!used[i])
+ continue;
+ byte *rgb = palette + i * 3;
+ const int luma = (rgb[0] * 30 + rgb[1] * 59 + rgb[2] * 11) / 100;
+ if (luma <= outlineThreshold) {
+ rgb[0] = 0xFF;
+ rgb[1] = 0x00;
+ rgb[2] = 0x00;
+ } else {
+ rgb[0] = 0xFF;
+ rgb[1] = 0xFF;
+ rgb[2] = 0xFF;
+ }
+ }
+
+ CursorMan.replaceCursorPalette(palette, 0, 256);
+}
+
+static void installMouseCursor(DBDArchive &pics, bool hotspot) {
+ Picture cursor;
+ if (pics.getPicture(kPicMousePointer, cursor) && !cursor.surface.empty()) {
+ const byte transparent = (byte)(cursor.flags >> 8);
+ CursorMan.replaceCursor(cursor.surface.rawSurface(), 0, 0,
+ transparent);
+ if (hotspot)
+ setHotspotCursorPalette(cursor, transparent);
+ else
+ CursorMan.replaceCursorPalette(nullptr, 0, 0);
+ return;
+ }
+
+ warning("EEM: mouse cursor PIC 0x%x missing; using fallback cursor",
+ kPicMousePointer);
+ CursorMan.replaceCursor(kCursorBitmap, 11, 16, 0, 0, 0);
+ CursorMan.replaceCursorPalette(hotspot ? kCursorHotspotPalette
+ : kCursorPalette,
+ 0, 3);
+}
EEMEngine::EEMEngine(OSystem *syst, const ADGameDescription *gameDesc)
: Engine(syst), _gameDescription(gameDesc), _rng("eem"),
_playerName("Detective"),
_lastScreen(kScreenInvalid), _nextScreen(kScreenTitle), _partner(0) {
+ ConfMan.registerDefault("hide_highlight_boxes", false);
+
// `ADGameDescription::extra` is set by the matching entry in
// `gameDescriptions[]` ("CD" or "Floppy"). Keep variant detection
// purely string-based so a future re-release with a different
@@ -133,12 +208,14 @@ Common::Error EEMEngine::run() {
_audio->setVoiceEnabled(_voiceOn);
syncSoundSettings();
- // _InitMouse @ 152d:018b in the original â install our 11x16 arrow,
- // using palette index 0 as the transparency key. The cursor is left
- // hidden through the opening anims and switched on at NewPlayer /
- // ChoosePartner where the player actually clicks.
- CursorMan.replaceCursor(kCursorBitmap, 11, 16, 0, 0, 0);
- CursorMan.replaceCursorPalette(kCursorPalette, 0, 3);
+ // CD `_main @ 1a35:0f59` and floppy `_main_Floppy @ 19bb:1012`
+ // both load `_GetPicture(0x50)` as the active mouse pointer before
+ // calling `_InitMouse`. PIC 0x51 is present in both archives but has
+ // no executable xrefs and appears to be the wait cursor.
+ // CD's `_SwitchMouse` supports swapping to a hotspot cursor ID stored
+ // at search record +0x0c, but the shipped CD mystery data only uses
+ // cursor 0; floppy search records have no cursor-id field.
+ installMouseCursor(_picsArchive, false);
CursorMan.showMouse(false);
// _AllBlack @ 172b:0d4b paints the screen black before the first handler.
@@ -472,6 +549,15 @@ screen_loop:
return Common::kNoError;
}
+void EEMEngine::setHotspotMouseCursor(bool active) {
+ active = active && ConfMan.getBool("hide_highlight_boxes");
+ if (_hotspotMouseCursor == active)
+ return;
+
+ _hotspotMouseCursor = active;
+ installMouseCursor(_picsArchive, active);
+}
+
bool EEMEngine::openArchives() {
// _InitGraphicsSystem @ 172b:0145 opens these five .DBD/.DBX pairs.
if (!_picsArchive.open(Common::Path("PICS.DBD"), Common::Path("PICS.DBX"))) {
@@ -754,6 +840,7 @@ void EEMEngine::doSiteLoop() {
// Tab (next site), ESC (exit).
SiteScreen screen(this, &_mystery);
screen.run();
+ setHotspotMouseCursor(false);
}
void EEMEngine::startTravelMusic() {
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index f73c751f87e..3cf5f0e11f1 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -188,6 +188,10 @@ public:
const EEMFont &getFont() const { return _font; }
uint8 getPartnerIndex() const { return _partner; }
+ /// Switch to a lightly tinted cursor while the mouse is over a
+ /// searchable hotspot and the highlight boxes are hidden.
+ void setHotspotMouseCursor(bool active);
+
/// Display one ClueBlock. @p clueBlock points at the u16 frame count
/// followed by 62-byte ClueEntries. Mirrors _DisplayClue @ 2404:05e6.
void displayClue(const byte *clueBlock);
@@ -563,6 +567,8 @@ private:
/// `setPartnerEraseBg`.
Graphics::ManagedSurface _partnerEraseBg;
+ bool _hotspotMouseCursor = false;
+
/// XMIDI music player. Mirrors the original `MIDI.C` family
/// (`_MIDIPlayFile`, `_MIDIPlay`, `_StopMIDI`, `_StartTravelMusic`
/// at 20a2:00e2-05c9). Constructed lazily during `run()` once the
diff --git a/engines/eem/metaengine.cpp b/engines/eem/metaengine.cpp
index 21dee682b00..52b2c5e78b5 100644
--- a/engines/eem/metaengine.cpp
+++ b/engines/eem/metaengine.cpp
@@ -22,12 +22,31 @@
#include "base/plugins.h"
#include "engines/advancedDetector.h"
+#include "common/translation.h"
+
+#include "eem/detection.h"
#include "eem/eem.h"
#include "common/system.h"
namespace EEM {
+static const ADExtraGuiOptionsMap optionsList[] = {
+ {
+ GAMEOPTION_HIDE_HIGHLIGHT_BOXES,
+ {
+ _s("Hide the highlight boxes"),
+ _s("Hide the boxes that highlight searchable clue locations."),
+ "hide_highlight_boxes",
+ false,
+ 0,
+ 0
+ }
+ },
+
+ AD_EXTRA_GUI_OPTIONS_TERMINATOR
+};
+
const char *EEMEngine::getGameId() const {
return _gameDescription->gameId;
}
@@ -44,6 +63,10 @@ public:
return "eem";
}
+ const ADExtraGuiOptionsMap *getAdvancedExtraGuiOptions() const override {
+ return EEM::optionsList;
+ }
+
Common::Error createInstance(OSystem *syst, Engine **engine, const ADGameDescription *desc) const override {
*engine = new EEM::EEMEngine(syst, desc);
return Common::kNoError;
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 1672ef45a61..8a1cddc1ee2 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -19,6 +19,7 @@
*
*/
+#include "common/config-manager.h"
#include "common/debug.h"
#include "common/events.h"
#include "common/system.h"
@@ -687,6 +688,8 @@ void SiteScreen::run() {
if (cur >= _mystery->numSites())
cur = 0;
enter(cur);
+ Common::Point mouse = g_system->getEventManager()->getMousePos();
+ updateHotspotCursor(cur, mouse.x, mouse.y);
while (!_vm->shouldQuit()) {
Common::Event event;
@@ -695,8 +698,13 @@ void SiteScreen::run() {
switch (event.type) {
case Common::EVENT_QUIT:
case Common::EVENT_RETURN_TO_LAUNCHER:
+ _vm->setHotspotMouseCursor(false);
return;
+ case Common::EVENT_MOUSEMOVE:
+ updateHotspotCursor(cur, event.mouse.x, event.mouse.y);
+ break;
+
case Common::EVENT_LBUTTONDOWN: {
// On-screen UI buttons. `_DoSiteLoop @ 168d:03f4` calls
// _FindButton(&SiteButtons, 2, MouseX, MouseY)
@@ -723,25 +731,31 @@ void SiteScreen::run() {
// the PDA / gallery `kBtnPartner` (5, 80, 44, 110).
const Common::Rect kBtnPartner ( 5, 80, 44, 110);
if (kBtnNotebook.contains(event.mouse.x, event.mouse.y)) {
+ _vm->setHotspotMouseCursor(false);
_vm->setNextScreen(kScreenNotebook);
return;
}
if (kBtnMap.contains(event.mouse.x, event.mouse.y)) {
+ _vm->setHotspotMouseCursor(false);
// CD writes `_NextScreen = 1`; floppy writes 2.
_vm->setNextScreen(_vm->isFloppy() ? kScreenMapAlt
: kScreenMap);
return;
}
if (kBtnPartner.contains(event.mouse.x, event.mouse.y)) {
+ _vm->setHotspotMouseCursor(false);
_vm->doHelp();
enter(cur);
+ updateHotspotCursor(cur, event.mouse.x, event.mouse.y);
break;
}
const int idx = hotspotAtPoint(cur, event.mouse.x, event.mouse.y);
if (idx >= 0) {
+ _vm->setHotspotMouseCursor(false);
onHotspotClicked(cur, (uint)idx);
// Restore the site BG after the clue overlay.
enter(cur);
+ updateHotspotCursor(cur, event.mouse.x, event.mouse.y);
}
break;
}
@@ -756,12 +770,15 @@ void SiteScreen::run() {
// here is ESC (matches `_ESCHit` â "Are you sure?"
// â MAP).
if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ _vm->setHotspotMouseCursor(false);
if (_vm->areYouSure()) {
_vm->setNextScreen(_vm->isFloppy() ? kScreenMapAlt
: kScreenMap);
return;
}
enter(cur);
+ mouse = g_system->getEventManager()->getMousePos();
+ updateHotspotCursor(cur, mouse.x, mouse.y);
}
break;
@@ -1272,8 +1289,10 @@ void SiteScreen::renderBackground(uint siteNum) {
}
void SiteScreen::renderHotspots(uint siteNum) {
- // Hotspot outlines (`_DrawSearchButtons`): toggle via V.
- if (!_showHotspots)
+ // Hotspot outlines (`_DrawSearchButtons`). The original always
+ // draws these; the port exposes an optional game setting to hide
+ // them for players who do not want location hints.
+ if (ConfMan.getBool("hide_highlight_boxes"))
return;
const byte *spots = _mystery->hotspots(siteNum);
@@ -1388,6 +1407,12 @@ int SiteScreen::hotspotAtPoint(uint siteNum, int x, int y) const {
return -1;
}
+void SiteScreen::updateHotspotCursor(uint siteNum, int x, int y) {
+ if (!_vm)
+ return;
+ _vm->setHotspotMouseCursor(hotspotAtPoint(siteNum, x, y) >= 0);
+}
+
void SiteScreen::onHotspotClicked(uint siteNum, uint hotIdx) {
debugC(1, kDebugSite, "Site %u: hotspot %u clicked", siteNum, hotIdx);
diff --git a/engines/eem/site.h b/engines/eem/site.h
index 97e83c087e4..112092edcda 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -81,7 +81,7 @@ struct Hotspot {
int16 x1, y1, x2, y2; ///< rectangle in screen coordinates
uint16 clueOffset; ///< +8: byte offset of ClueBlock in the mystery file
uint16 hotspotIndex; ///< +10: zero-based mystery-wide seen ordinal
- uint16 extra; ///< +12: unknown (zero in M0..M54)
+ uint16 extra; ///< +12: CD cursor ID for _SwitchMouse; shipped data uses 0
Common::Rect rect() const { return Common::Rect(x1, y1, x2, y2); }
};
@@ -109,6 +109,7 @@ private:
void renderBackground(uint siteNum);
void renderHotspots(uint siteNum);
int hotspotAtPoint(uint siteNum, int x, int y) const;
+ void updateHotspotCursor(uint siteNum, int x, int y);
void onHotspotClicked(uint siteNum, uint hotIdx);
/// Play the partner's site-arrival sequence once `_LastSite !=
@@ -167,7 +168,6 @@ private:
EEMEngine *_vm;
Mystery *_mystery;
- bool _showHotspots = true; ///< Toggle outlines with V key.
int _lastSiteAnim = -1; ///< Last site we played the arrival on.
int _snapshotSite = -1; ///< Site number the snapshot belongs to.
Graphics::ManagedSurface _bgSnapshot;
Commit: 00c6438de3a7ea38aa01d5784664fdf5aaf65103
https://github.com/scummvm/scummvm/commit/00c6438de3a7ea38aa01d5784664fdf5aaf65103
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:57+02:00
Commit Message:
EEM: correctly blit npc pictures
Changed paths:
engines/eem/clues.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 1420e324a7f..27efcc6c2f6 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -93,7 +93,8 @@ uint happinessLevel(int x) {
}
// Lock the framebuffer, masked-blit `p` at (x, y), unlock. The transparent
-// colour is the high byte of `p.flags` per `_Rect_Move_Mask @ 1000:03fc`.
+// colour is the high byte of `p.flags`; `_AddPicBackground @ 172b:0ed4`
+// pushes `word ptr ES:[BX] >> 8` as `_Rect_Move_Mask`'s mask byte.
void blitMaskedToScreen(const Picture &p, int x, int y) {
const byte transp = (byte)(p.flags >> 8);
Graphics::Surface *screen = g_system->lockScreen();
@@ -730,11 +731,7 @@ void EEMEngine::displayClue(const byte *clueBlock) {
Picture charPic;
if (_picsArchive.getPicture(charPicId, charPic) &&
charX < 320 && charY < 200) {
- const int w = MIN<int>(charPic.surface.w, 320 - charX);
- const int h = MIN<int>(charPic.surface.h, 200 - charY);
- if (w > 0 && h > 0)
- g_system->copyRectToScreen(charPic.surface.getPixels(),
- charPic.surface.pitch, charX, charY, w, h);
+ blitMaskedToScreen(charPic, charX, charY);
}
}
Commit: 057711225d26c5bb95abef5475121c5ffe8a7c6b
https://github.com/scummvm/scummvm/commit/057711225d26c5bb95abef5475121c5ffe8a7c6b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:57+02:00
Commit Message:
EEM: implemented the quit dialog
Changed paths:
engines/eem/clues.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 27efcc6c2f6..62cf4047ec2 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -117,6 +117,16 @@ void blitMaskedToScreen(const Picture &p, int x, int y) {
g_system->unlockScreen();
}
+void blitRawToScreen(const Picture &p, int x, int y) {
+ const int w = MIN<int>(p.surface.w, 320 - x);
+ const int h = MIN<int>(p.surface.h, 200 - y);
+ if (x < 0 || y < 0 || w <= 0 || h <= 0)
+ return;
+
+ g_system->copyRectToScreen(p.surface.getPixels(), p.surface.pitch,
+ x, y, w, h);
+}
+
void EEMEngine::doChoosePartner() {
// Mirrors _DoChoosePartner @ 1a35:0756. The original places boy + girl
// animations on a backdrop and polls four click rectangles (two per
@@ -1341,12 +1351,6 @@ void EEMEngine::displayFloppyHotspotDialog(uint siteNum, uint hotIdx) {
}
bool EEMEngine::areYouSure() {
- // Mirrors `_AreYouSure` @ 1a35:0a5c. Original loads PIC 0x136 for the
- // dialog body and PIC 0x1FD/0x1FE for YES/NO. We render a minimal
- // text dialog that preserves the screen behind it.
- if (!_font.isLoaded())
- return true;
-
Graphics::Surface *screen = g_system->lockScreen();
Graphics::ManagedSurface saved(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
@@ -1358,24 +1362,65 @@ bool EEMEngine::areYouSure() {
g_system->unlockScreen();
}
- const Common::Rect dlg(60, 70, 260, 140);
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- for (int row = 0; row < 200; row++)
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)saved.getBasePtr(0, row), 320);
- scratch.fillRect(dlg, 0);
- scratch.frameRect(dlg, 0xF);
- _font.drawString(&scratch,
- isSpanish() ? "Estas seguro que quieres salir?"
- : "Are you sure you want to quit?",
- dlg.left + 8, dlg.top + 8, 320, 0xF);
- _font.drawString(&scratch,
- isSpanish() ? "S - Si" : "Y - Yes",
- dlg.left + 16, dlg.top + 36, 320, 0xF);
- _font.drawString(&scratch, "N - No", dlg.left + 100, dlg.top + 36, 320, 0xF);
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
+ // CD `_AreYouSure @ 1a35:0a5c` and floppy `FUN_19bb_0b43` both:
+ // * load PIC 0x136 as the dialog body,
+ // * load PIC 0x1fd / 0x1fe as the pressed YES / NO button images,
+ // * center the dialog,
+ // * hit-test YES at (x+0x0c,y+0x23)-(x+0x20,y+0x32),
+ // and NO at (x+0x60,y+0x23)-(x+0x74,y+0x32).
+ Picture dialogPic;
+ Picture yesPic;
+ Picture noPic;
+ const bool haveOriginalDialog =
+ _picsArchive.getPicture(0x136, dialogPic) &&
+ _picsArchive.getPicture(0x1fd, yesPic) &&
+ _picsArchive.getPicture(0x1fe, noPic);
+
+ Common::Rect yesRect;
+ Common::Rect noRect;
+ int yesX = 0;
+ int yesY = 0;
+ int noX = 0;
+ int noY = 0;
+
+ if (haveOriginalDialog) {
+ const int x = (320 - dialogPic.surface.w) / 2;
+ const int y = (200 - dialogPic.surface.h) / 2;
+ yesX = x + 0x0c;
+ yesY = y + 0x23;
+ noX = x + 0x60;
+ noY = y + 0x23;
+ yesRect = Common::Rect(yesX, yesY, x + 0x20 + 1, y + 0x32 + 1);
+ noRect = Common::Rect(noX, noY, x + 0x74 + 1, y + 0x32 + 1);
+ blitMaskedToScreen(dialogPic, x, y);
+ } else if (_font.isLoaded()) {
+ const Common::Rect dlg(60, 70, 260, 140);
+ yesRect = Common::Rect(dlg.left + 16, dlg.top + 34,
+ dlg.left + 84, dlg.top + 54);
+ noRect = Common::Rect(dlg.left + 100, dlg.top + 34,
+ dlg.left + 160, dlg.top + 54);
+
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ for (int row = 0; row < 200; row++)
+ memcpy((byte *)scratch.getBasePtr(0, row),
+ (const byte *)saved.getBasePtr(0, row), 320);
+ scratch.fillRect(dlg, 0);
+ scratch.frameRect(dlg, 0xF);
+ _font.drawString(&scratch,
+ isSpanish() ? "Estas seguro que quieres salir?"
+ : "Are you sure you want to quit?",
+ dlg.left + 8, dlg.top + 8, 320, 0xF);
+ _font.drawString(&scratch,
+ isSpanish() ? "S - Si" : "Y - Yes",
+ dlg.left + 16, dlg.top + 36, 320, 0xF);
+ _font.drawString(&scratch, "N - No", dlg.left + 100,
+ dlg.top + 36, 320, 0xF);
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ } else {
+ return true;
+ }
g_system->updateScreen();
bool result = false;
@@ -1401,6 +1446,24 @@ bool EEMEngine::areYouSure() {
result = false; decided = true; break;
}
}
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ if (yesRect.contains(ev.mouse.x, ev.mouse.y)) {
+ if (haveOriginalDialog) {
+ blitRawToScreen(yesPic, yesX, yesY);
+ g_system->updateScreen();
+ g_system->delayMillis(90);
+ }
+ result = true; decided = true; break;
+ }
+ if (noRect.contains(ev.mouse.x, ev.mouse.y)) {
+ if (haveOriginalDialog) {
+ blitRawToScreen(noPic, noX, noY);
+ g_system->updateScreen();
+ g_system->delayMillis(90);
+ }
+ result = false; decided = true; break;
+ }
+ }
}
if (decided)
break;
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 7f045894eba..ca62f428557 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -940,58 +940,6 @@ void EEMEngine::doSetup() {
};
draw();
- // Modal "Are you sure?" yes/no prompt. Mirrors `_AreYouSure @
- // 1a35:0a5c` â the original draws a centred message, listens for
- // Y/Enter (confirm) or N/ESC (cancel). We render a minimal
- // overlay with Y / N keys (and click on left/right halves) so
- // the Quit button gives the player a chance to back out.
- auto areYouSure = [&]() -> bool {
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- Graphics::Surface *cur = g_system->lockScreen();
- if (cur) {
- for (int row = 0; row < 200; row++)
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)cur->getBasePtr(0, row), 320);
- g_system->unlockScreen();
- }
- const Common::Rect kBox(80, 80, 240, 120);
- scratch.fillRect(kBox, 0x00);
- _font.drawString(&scratch,
- isSpanish() ? "Estas seguro?" : "Are you sure?",
- 100, 88, 200, 0xF);
- _font.drawString(&scratch,
- isSpanish() ? "S = si N = no" : "Y = yes N = no",
- 100, 102, 200, 0xF);
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
- while (!shouldQuit()) {
- Common::Event ev;
- while (g_system->getEventManager()->pollEvent(ev)) {
- if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
- return true;
- if (ev.type == Common::EVENT_KEYDOWN) {
- const Common::KeyCode k = ev.kbd.keycode;
- // Spanish prompt is "S = si" â accept both Y and S.
- if (k == Common::KEYCODE_y ||
- k == Common::KEYCODE_s ||
- k == Common::KEYCODE_RETURN)
- return true;
- if (k == Common::KEYCODE_n ||
- k == Common::KEYCODE_ESCAPE)
- return false;
- }
- if (ev.type == Common::EVENT_LBUTTONDOWN) {
- return ev.mouse.x < 160;
- }
- }
- g_system->delayMillis(15);
- }
- return false;
- };
-
// Render `picId` and block until click/key. Returns the pressed
// keycode (KEYCODE_ESCAPE for an explicit bail, KEYCODE_INVALID
// for a click or any other key). When `transparent` is true,
Commit: 6ce915b5a9648c1dfb93e79ae687719840a6d84d
https://github.com/scummvm/scummvm/commit/6ce915b5a9648c1dfb93e79ae687719840a6d84d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:57+02:00
Commit Message:
EEM: implemented the (im)patience animation
Changed paths:
engines/eem/site.cpp
engines/eem/site.h
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 8a1cddc1ee2..c47e8882f4d 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -286,6 +286,18 @@ const AnimScriptLong kAnimScriptsLong[] = {
1,2,3,4,5,6,7,8,9,10,11,12 } },
};
+// `_PatientSequence` and `_ImpatientSequence` are standalone script
+// pointers, not entries in `_AnimationSequences`. CD has the data but
+// never calls the switchers; floppy calls them from `_DoSiteLoop_Floppy`.
+// We intentionally enable the same switch for both builds.
+static const uint8 kPatientSequence[] = { 0,0,0,0,0,0,0,0,0,2 };
+static const uint8 kImpatientSequence[] = { 0,1,0,1,0,1,0,1,2,1 };
+
+// Test-shortened impatience delay. The original stores an hour-rounded
+// wall-clock value via DOS gettime; this keeps the same reset/switch
+// behavior but makes the feature observable during normal testing.
+static const uint32 kImpatienceDelayMs = 60 * 1000;
+
// Look up the script for `seqnum`. Returns the frame array + length,
// or `(nullptr, 0)` if no script is known â caller falls back to
// flipbook cycling so unknown anims still animate (just without idle
@@ -317,6 +329,16 @@ static AnimScriptRef findAnimScript(uint16 seqnum) {
return r;
}
+static uint frameFromScriptAtTick(const uint8 *frames, uint len,
+ uint numFrames, uint32 tickMs) {
+ const uint kFramePeriodMs = 100;
+ if (!frames || len == 0)
+ return numFrames > 0 ? (uint)((tickMs / kFramePeriodMs) % numFrames) : 0;
+ const uint scriptIdx = (uint)((tickMs / kFramePeriodMs) % len);
+ const uint frame = frames[scriptIdx];
+ return (numFrames > 0) ? MIN<uint>(frame, numFrames - 1) : 0;
+}
+
void auditPartnerAnims(EEMEngine *vm) {
// Cross-check every registered partner-subset script against the
// underlying ANI.DBD entry it references. If the script asks for
@@ -432,15 +454,10 @@ void auditPartnerAnims(EEMEngine *vm) {
// symptom.
uint partnerFrameAtTick(uint16 seqnum, uint numFrames, uint32 tickMs) {
const AnimScriptRef s = findAnimScript(seqnum);
- const uint kFramePeriodMs = 100;
- if (!s.frames || s.len == 0)
- return numFrames > 0 ? (uint)((tickMs / kFramePeriodMs) % numFrames) : 0;
- const uint scriptIdx = (uint)((tickMs / kFramePeriodMs) % s.len);
- const uint frame = s.frames[scriptIdx];
// The script can in theory request a frame that's outside the
// animation's actual frame count (a misencoded script). Clamp so
// we don't read past `anim[]` in the caller.
- return (numFrames > 0) ? MIN<uint>(frame, numFrames - 1) : 0;
+ return frameFromScriptAtTick(s.frames, s.len, numFrames, tickMs);
}
// Generic "play `unfold` once, then loop `waitSeq` forever" walker.
@@ -486,7 +503,7 @@ uint bigMapDetailPartnerFrameAtTick(uint numFrames, uint32 elapsedMs) {
numFrames, elapsedMs);
}
-void SiteScreen::enter(uint siteNum) {
+void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
if (!_mystery || !_mystery->isLoaded()) {
warning("SiteScreen::enter: no mystery loaded");
return;
@@ -503,6 +520,10 @@ void SiteScreen::enter(uint siteNum) {
// 0xffff (= -1, becomes 0 on the first `_UpdateAnimations`
// tick).
_waitPhaseAnchor = g_system->getMillis();
+ if (resetPartnerMood) {
+ _partnerWaitMood = kPartnerWaitDefault;
+ initImpatienceCounter();
+ }
// Capture whether this is the first time the player enters this
// site BEFORE we mark it visited â `_DoSiteLoop @ 168d:03f4`
@@ -675,6 +696,30 @@ void SiteScreen::enter(uint siteNum) {
}
}
+void SiteScreen::initImpatienceCounter() {
+ _impatientDeadlineMs = g_system->getMillis() + kImpatienceDelayMs;
+}
+
+bool SiteScreen::checkImpatienceCounter() {
+ const uint32 now = g_system->getMillis();
+ const bool impatient = (int32)(now - _impatientDeadlineMs) >= 0;
+ if (impatient)
+ initImpatienceCounter();
+ return impatient;
+}
+
+void SiteScreen::notePartnerActivity() {
+ // Mirrors `_Switch2Patient(WaitHandle)` plus
+ // `_InitImpatientCounter()` in the floppy site loop, but only for
+ // deliberate actions in the port: clicks and key presses. Passive
+ // mouse movement should not make impatience impossible to see.
+ const bool wasImpatient = _partnerWaitMood == kPartnerWaitImpatient;
+ _partnerWaitMood = kPartnerWaitPatient;
+ initImpatienceCounter();
+ if (wasImpatient)
+ debugC(1, kDebugSite, "Partner impatience: reset to patient");
+}
+
void SiteScreen::run() {
if (!_mystery || !_mystery->isLoaded())
return;
@@ -731,11 +776,13 @@ void SiteScreen::run() {
// the PDA / gallery `kBtnPartner` (5, 80, 44, 110).
const Common::Rect kBtnPartner ( 5, 80, 44, 110);
if (kBtnNotebook.contains(event.mouse.x, event.mouse.y)) {
+ notePartnerActivity();
_vm->setHotspotMouseCursor(false);
_vm->setNextScreen(kScreenNotebook);
return;
}
if (kBtnMap.contains(event.mouse.x, event.mouse.y)) {
+ notePartnerActivity();
_vm->setHotspotMouseCursor(false);
// CD writes `_NextScreen = 1`; floppy writes 2.
_vm->setNextScreen(_vm->isFloppy() ? kScreenMapAlt
@@ -745,7 +792,8 @@ void SiteScreen::run() {
if (kBtnPartner.contains(event.mouse.x, event.mouse.y)) {
_vm->setHotspotMouseCursor(false);
_vm->doHelp();
- enter(cur);
+ notePartnerActivity();
+ enter(cur, false);
updateHotspotCursor(cur, event.mouse.x, event.mouse.y);
break;
}
@@ -754,13 +802,17 @@ void SiteScreen::run() {
_vm->setHotspotMouseCursor(false);
onHotspotClicked(cur, (uint)idx);
// Restore the site BG after the clue overlay.
- enter(cur);
+ notePartnerActivity();
+ enter(cur, false);
updateHotspotCursor(cur, event.mouse.x, event.mouse.y);
+ } else {
+ notePartnerActivity();
}
break;
}
case Common::EVENT_KEYDOWN:
+ notePartnerActivity();
// `_DoSiteLoop @ 168d:07e1` only dispatches on the
// 6-entry table at `168d:09d5` (TAB / ENTER / arrow
// keys for hotspot cursor cycling) plus ESC handled
@@ -776,7 +828,7 @@ void SiteScreen::run() {
: kScreenMap);
return;
}
- enter(cur);
+ enter(cur, false);
mouse = g_system->getEventManager()->getMousePos();
updateHotspotCursor(cur, mouse.x, mouse.y);
}
@@ -803,6 +855,10 @@ void SiteScreen::run() {
// park as the original.
const uint32 now = g_system->getMillis();
if (_snapshotSite == (int)cur && now - _lastTickMs >= 100) {
+ if (checkImpatienceCounter()) {
+ _partnerWaitMood = kPartnerWaitImpatient;
+ debugC(1, kDebugSite, "Partner impatience: switched to impatient");
+ }
restoreBgSnapshot();
renderAnimatedDrops(cur, now);
renderPartner(cur, now);
@@ -1212,8 +1268,19 @@ void SiteScreen::renderPartner(uint siteNum, uint32 tickMs) {
const uint32 elapsed = (tickMs >= _waitPhaseAnchor)
? (tickMs - _waitPhaseAnchor)
: tickMs;
- const uint frameIdx = partnerFrameAtTick((uint16)animId,
- (uint)anim.size(), elapsed);
+ uint frameIdx = 0;
+ if (_partnerWaitMood == kPartnerWaitImpatient) {
+ frameIdx = frameFromScriptAtTick(kImpatientSequence,
+ ARRAYSIZE(kImpatientSequence),
+ (uint)anim.size(), elapsed);
+ } else if (_partnerWaitMood == kPartnerWaitPatient) {
+ frameIdx = frameFromScriptAtTick(kPatientSequence,
+ ARRAYSIZE(kPatientSequence),
+ (uint)anim.size(), elapsed);
+ } else {
+ frameIdx = partnerFrameAtTick((uint16)animId,
+ (uint)anim.size(), elapsed);
+ }
Graphics::Surface *screen = g_system->lockScreen();
if (!screen)
return;
diff --git a/engines/eem/site.h b/engines/eem/site.h
index 112092edcda..06bc8595369 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -100,7 +100,7 @@ public:
: _vm(vm), _mystery(mystery) {}
/** Enter site @p siteNum. Renders BG + hotspots; runs the input loop. */
- void enter(uint siteNum);
+ void enter(uint siteNum, bool resetPartnerMood = true);
/// Run the per-mystery loop: site -> map -> next site, ESC exits.
void run();
@@ -111,6 +111,9 @@ private:
int hotspotAtPoint(uint siteNum, int x, int y) const;
void updateHotspotCursor(uint siteNum, int x, int y);
void onHotspotClicked(uint siteNum, uint hotIdx);
+ void initImpatienceCounter();
+ bool checkImpatienceCounter();
+ void notePartnerActivity();
/// Play the partner's site-arrival sequence once `_LastSite !=
/// _SiteNumber`. Mirrors `_EnterSiteAnim @ 1000:9b21` â animation
@@ -168,10 +171,17 @@ private:
EEMEngine *_vm;
Mystery *_mystery;
+ enum PartnerWaitMood {
+ kPartnerWaitDefault,
+ kPartnerWaitPatient,
+ kPartnerWaitImpatient
+ };
int _lastSiteAnim = -1; ///< Last site we played the arrival on.
int _snapshotSite = -1; ///< Site number the snapshot belongs to.
Graphics::ManagedSurface _bgSnapshot;
uint32 _lastTickMs = 0; ///< Last frame-pump tick in ms.
+ uint32 _impatientDeadlineMs = 0; ///< Test-shortened impatience deadline.
+ PartnerWaitMood _partnerWaitMood = kPartnerWaitDefault;
/// Wall-clock timestamp at which the partner's wait animation
/// "started" (or last restarted). The site loop renders the
Commit: 3e9ba587e4e062b839b93940b8f8c410d44e4876
https://github.com/scummvm/scummvm/commit/3e9ba587e4e062b839b93940b8f8c410d44e4876
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:58+02:00
Commit Message:
EEM: improved scrapbook code
Changed paths:
engines/eem/eem.h
engines/eem/ui.cpp
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 3cf5f0e11f1..792db1288d2 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -412,7 +412,8 @@ private:
/// @ 1df2:044c`. The file format is a 2-byte page count followed
/// by N pages, each `{ u16 picNum, u16 x1, u16 y1, u16 x2, u16 y2,
/// char text[] (null-terminated, ParseString placeholders) }`.
- /// Blocks until the player clicks past the last page or hits ESC.
+ /// Blocks until the player exits the ending view or crosses either
+ /// page boundary.
/// `_ShowOneScrap @ 1f78:0773` is just `_DisplayEnding(num, 1)`,
/// so this same call covers the post-mystery scrapbook view from
/// the action menu.
@@ -430,14 +431,15 @@ private:
/// Mirrors `_DisplayEnding`'s `[BP-0x18]` return at 1df2:0723.
int doShowEnding(uint num, bool firstPage = true);
- /// Walk every solved mystery in tier @p stage (1=Junior, 2=Senior,
- /// 3=Master) and display each one's ending pages in sequence.
+ /// Browse the scrapbook tier @p stage (1=Junior, 2=Senior,
+ /// 3=Master), moving between mystery endings with the original
+ /// `_DisplayEnding` return direction.
/// Mirrors `_ShowScrapbook(stage, 0) @ 1f78:0642`: the original
/// computes the mystery range from `(stage - 1) * 0x18 + 1` to
- /// `(stage - 1) * 0x18 + 0x18` and skips entries whose
- /// `_3f9b[i] == 0` (unsolved) so the scrapbook only contains
- /// completed cases. Used by both the setup-screen ScrapBook
- /// buttons and the action-menu "See ScrapBook 1/2/3" entries.
+ /// `(stage - 1) * 0x18 + 0x18`. If this is the current chain stage,
+ /// unsolved entries are skipped; completed tiers are already solved.
+ /// Used by both the setup-screen ScrapBook buttons and the
+ /// action-menu "See ScrapBook 1/2/3" entries.
void doShowScrapbook(uint stage);
void doCaseSelection();
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index ca62f428557..b746dd267bc 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -649,21 +649,24 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
// Walk page records. Each page header is 10 bytes; text is
// null-terminated and follows the header.
uint pageOffsets[8]; // ENDING_RANGE_MAX from `_DisplayEnding`
- const uint kMaxPages = MIN<uint>(pageCount,
- (uint)(sizeof(pageOffsets) / sizeof(uint)));
+ const uint maxPages = MIN<uint>(pageCount,
+ (uint)(sizeof(pageOffsets) / sizeof(uint)));
uint cursor = 2;
- for (uint p = 0; p < kMaxPages; p++) {
- pageOffsets[p] = cursor;
+ uint validPages = 0;
+ for (uint p = 0; p < maxPages; p++) {
if (cursor + 10 >= size)
break;
+ pageOffsets[validPages++] = cursor;
// Skip the 10-byte header and find the null terminator.
cursor += 10;
while (cursor < size && buf[cursor] != 0)
cursor++;
cursor++; // past the null
}
+ if (validPages == 0)
+ return 0;
- uint pageIdx = firstPage ? 0 : (kMaxPages - 1);
+ uint pageIdx = firstPage ? 0 : (validPages - 1);
int direction = 0; // -1 / 0 / +1, see header doc.
bool exitLoop = false;
bool dirty = true;
@@ -702,15 +705,6 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
textW, text, 0);
}
- // Page indicator at top-right ("page 1/3"). Stays in the
- // main font + color 0xF so it doesn't blend into the
- // newspaper masthead.
- if (_font.isLoaded() && kMaxPages > 1) {
- const Common::String hdr = Common::String::format(
- "%u/%u", (unsigned)pageIdx + 1, (unsigned)kMaxPages);
- _font.drawString(&scratch, hdr, 280, 4, 32, 0xF);
- }
-
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
g_system->updateScreen();
@@ -726,22 +720,17 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
if (ev.type == Common::EVENT_KEYDOWN) {
switch (ev.kbd.keycode) {
case Common::KEYCODE_ESCAPE:
- // ESC is the ONLY way out of the newspaper view.
- // Departs from the original `_DisplayEnding`, which
- // also exits on boundary arrow keys / clicks
- // (1df2:0689 / 1df2:06a0); the boundary-exit path is
- // what fed `[BP-0x18]` to `_ShowScrapbook` for
- // per-mystery scrapbook navigation. We don't expose
- // that â clicking ESC closes the scrapbook entirely.
direction = 0;
exitLoop = true;
break;
case Common::KEYCODE_LEFT:
case Common::KEYCODE_PAGEUP:
- // Clamp at page 0 â never exit on LEFT.
if (pageIdx > 0) {
pageIdx--;
dirty = true;
+ } else {
+ direction = -1;
+ exitLoop = true;
}
break;
case Common::KEYCODE_RIGHT:
@@ -750,10 +739,12 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
case Common::KEYCODE_KP_ENTER:
case Common::KEYCODE_SPACE:
case Common::KEYCODE_TAB:
- // Clamp at last page â never exit on RIGHT either.
- if (pageIdx + 1 < kMaxPages) {
+ if (pageIdx + 1 < validPages) {
pageIdx++;
dirty = true;
+ } else {
+ direction = 1;
+ exitLoop = true;
}
break;
default:
@@ -763,22 +754,30 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
break;
}
if (ev.type == Common::EVENT_LBUTTONDOWN) {
- // Mouse clicks shift pages too â never exit on click.
- // Use the original PrevPage/NextPage rect split for
- // click direction (29be:1078 / 29be:1080); clicks
- // outside both rects fall through to next-page so the
- // player still gets some feedback.
- const Common::Rect kPrevPageRect(0, 0, 27, 200);
+ // Original PrevPage/NextPage rects at 29be:1078 /
+ // 29be:1080. Clicks outside both rects exit with
+ // direction 0.
+ const Common::Rect kPrevPageRect(0, 0, 28, 200);
+ const Common::Rect kNextPageRect(28, 7, 317, 197);
if (kPrevPageRect.contains(ev.mouse.x, ev.mouse.y)) {
if (pageIdx > 0) {
pageIdx--;
dirty = true;
+ } else {
+ direction = -1;
+ exitLoop = true;
}
- } else {
- if (pageIdx + 1 < kMaxPages) {
+ } else if (kNextPageRect.contains(ev.mouse.x, ev.mouse.y)) {
+ if (pageIdx + 1 < validPages) {
pageIdx++;
dirty = true;
+ } else {
+ direction = 1;
+ exitLoop = true;
}
+ } else {
+ direction = 0;
+ exitLoop = true;
}
if (exitLoop)
break;
@@ -791,40 +790,53 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
}
void EEMEngine::doShowScrapbook(uint stage) {
- // Mirrors `_ShowScrapbook(stage, 0) @ 1f78:0642`. Walk the
- // stage's mystery range and call `_DisplayEnding` on each solved
- // mystery. The original splits the 55 cases into three tiers:
- // stage 1 (Junior) â mysteries 1..0x18 (24 cases)
- // stage 2 (Senior) â mysteries 0x19..0x30 (24 cases)
- // stage 3 (Master) â mysteries 0x31..0x36 (6 cases)
- // Each tier's range is `lo = (stage-1)*0x18 + 1`, `hi = lo + 0x17`
- // (verified at 1f78:064b: `iVar1 = (param_1 - 1) * 0x18; uVar2 =
- // iVar1 + 1`). The current-stage filter at 1f78:065e
- // (`if (DAT_2d5d_3f99 == param_1)`) skips unsolved mysteries
- // inside the player's CURRENT tier â completed tiers show every
- // mystery regardless. We mirror that exactly.
+ // Mirrors `_ShowScrapbook(stage, 0) @ 1f78:0642`. The original
+ // splits the cases into 24-entry tiers, then feeds each ending
+ // viewer return direction back into the tier walk:
+ // -1 -> previous mystery, opened at its last page
+ // 0 -> close scrapbook
+ // +1 -> next mystery, opened at its first page
+ // When the requested tier is the player's current chain stage, the
+ // original skips unsolved entries; completed tiers are already solved,
+ // so they are shown as a full range.
if (stage < 1 || stage > 3)
return;
- const uint lo = (stage - 1) * 0x18 + 1;
- const uint hi = lo + 0x17;
+ const int solvedCount =
+ (int)(sizeof(_mysteriesSolved) / sizeof(_mysteriesSolved[0]));
+ const int lo = (int)(stage - 1) * 0x18 + 1;
+ const int hi = MIN<int>(lo + 0x18, solvedCount);
+ if (lo >= hi)
+ return;
const bool currentTier = (stage == _chainStage);
- // `doShowEnding` only reports `direction = 0` now (ESC), so we
- // can't use the original 1f78:067e/0698 forward-backward walk.
- // Instead iterate every solved mystery in the tier linearly:
- // each ESC closes the current newspaper and moves us to the
- // next solved entry. Departs from `_ShowScrapbook @ 1f78:0642`
- // (which let the player browse via the boundary keys), but
- // matches the user-requested "ESC is the only exit" rule.
- for (uint m = lo; m <= hi && !shouldQuit(); m++) {
- if (m >= sizeof(_mysteriesSolved))
+ int mystery = lo;
+ if (currentTier) {
+ while (mystery < hi && _mysteriesSolved[mystery] == 0)
+ mystery++;
+ }
+
+ bool firstPage = true;
+ while (!shouldQuit() && mystery >= lo && mystery < hi) {
+ const int direction = doShowEnding((uint)mystery, firstPage);
+ if (direction < 0) {
+ if (mystery == lo)
+ break;
+ mystery--;
+ if (currentTier) {
+ while (mystery >= lo && _mysteriesSolved[mystery] == 0)
+ mystery--;
+ }
+ firstPage = false;
+ } else if (direction > 0) {
+ mystery++;
+ if (currentTier) {
+ while (mystery < hi && _mysteriesSolved[mystery] == 0)
+ mystery++;
+ }
+ firstPage = true;
+ } else {
break;
- // Current-tier filter (1f78:0664). Completed tiers show all
- // 24 entries; the active tier hides unsolved ones because
- // the player hasn't earned that scrapbook page yet.
- if (currentTier && _mysteriesSolved[m] == 0)
- continue;
- (void)doShowEnding(m, /*firstPage=*/true);
+ }
}
}
@@ -1176,31 +1188,29 @@ void EEMEngine::doSetup() {
}
// ScrapBook 1 / 2 / 3 (buttons [3] / [4] / [5]). Original
- // handlers at 1f78:021F (`_ShowScrapbook(0, 1)`) /
- // 1f78:022E (gated chain >= 2 / `_ShowScrapbook(0, 2)`) /
- // 1f78:0244 (gated chain >= 3 / `_ShowScrapbook(0, 3)`).
- // Convert the original's `(0, stage)` invocation into our
- // `doShowScrapbook(stage)` (we collapse the param_1=0
- // "no-current-mystery" indirection â relevant only for
- // the post-win callsite).
- auto runScrapbook = [&](uint stage) {
+ // handlers call `_ShowScrapbook(stage, 0)`, with stages 2
+ // and 3 gated by chain progress.
+ if (kScrap1Btn.contains(mx, my)) {
CursorMan.showMouse(false);
- doShowScrapbook(stage);
+ doShowScrapbook(1);
CursorMan.showMouse(true);
setSitePalette(0);
- };
- if (kScrap1Btn.contains(mx, my)) {
- runScrapbook(1);
dirty = true;
continue;
}
if (kScrap2Btn.contains(mx, my) && _chainStage >= 2) {
- runScrapbook(2);
+ CursorMan.showMouse(false);
+ doShowScrapbook(2);
+ CursorMan.showMouse(true);
+ setSitePalette(0);
dirty = true;
continue;
}
if (kScrap3Btn.contains(mx, my) && _chainStage >= 3) {
- runScrapbook(3);
+ CursorMan.showMouse(false);
+ doShowScrapbook(3);
+ CursorMan.showMouse(true);
+ setSitePalette(0);
dirty = true;
continue;
}
Commit: 5042edef3af445d4d7d272f15601179d6a51b6ac
https://github.com/scummvm/scummvm/commit/5042edef3af445d4d7d272f15601179d6a51b6ac
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:58+02:00
Commit Message:
EEM: highlight scrapbook interactive zones
Changed paths:
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/ui.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 8959705a931..6ca48a2a385 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -61,6 +61,10 @@ const uint kPicMousePointer = 0x50; ///< Original startup pointer; 0x51 is t
const byte kSaveBodyVer = 1;
+// Internal test switch: populate ScrapBook 1 at startup without exposing a
+// game option or changing save format. Set false before release.
+const bool kDebugPopulateScrapbook1AtStartup = false;
+
// Fallback 11x16 mouse cursor used if the selected PIC pointer cannot be
// loaded. The original game sets the cursor visible/hidden via
// _MouseCursor; we leave it on once the screens that need it
@@ -89,13 +93,13 @@ const byte kCursorPalette[] = {
0x00, 0x00, 0x00, // 1 â outline
0xFF, 0xFF, 0xFF // 2 â fill
};
-const byte kCursorHotspotPalette[] = {
+const byte kCursorInteractivePalette[] = {
0x00, 0x00, 0x00, // 0 â transparent (key)
0xFF, 0x00, 0x00, // 1 â red outline
0xFF, 0xFF, 0xFF // 2 â white fill
};
-static void setHotspotCursorPalette(const Picture &cursor, byte transparent) {
+static void setInteractiveCursorPalette(const Picture &cursor, byte transparent) {
byte palette[kPalSize];
bool used[256];
memset(used, 0, sizeof(used));
@@ -141,14 +145,14 @@ static void setHotspotCursorPalette(const Picture &cursor, byte transparent) {
CursorMan.replaceCursorPalette(palette, 0, 256);
}
-static void installMouseCursor(DBDArchive &pics, bool hotspot) {
+static void installMouseCursor(DBDArchive &pics, bool interactive) {
Picture cursor;
if (pics.getPicture(kPicMousePointer, cursor) && !cursor.surface.empty()) {
const byte transparent = (byte)(cursor.flags >> 8);
CursorMan.replaceCursor(cursor.surface.rawSurface(), 0, 0,
transparent);
- if (hotspot)
- setHotspotCursorPalette(cursor, transparent);
+ if (interactive)
+ setInteractiveCursorPalette(cursor, transparent);
else
CursorMan.replaceCursorPalette(nullptr, 0, 0);
return;
@@ -157,8 +161,8 @@ static void installMouseCursor(DBDArchive &pics, bool hotspot) {
warning("EEM: mouse cursor PIC 0x%x missing; using fallback cursor",
kPicMousePointer);
CursorMan.replaceCursor(kCursorBitmap, 11, 16, 0, 0, 0);
- CursorMan.replaceCursorPalette(hotspot ? kCursorHotspotPalette
- : kCursorPalette,
+ CursorMan.replaceCursorPalette(interactive ? kCursorInteractivePalette
+ : kCursorPalette,
0, 3);
}
@@ -183,6 +187,17 @@ EEMEngine::~EEMEngine() {
delete _music;
}
+void EEMEngine::applyStartupTestOverrides() {
+ if (!kDebugPopulateScrapbook1AtStartup)
+ return;
+
+ for (uint i = 1; i <= 0x18 && i < sizeof(_mysteriesSolved); i++)
+ _mysteriesSolved[i] = 1;
+
+ debugC(1, kDebugGeneral,
+ "startup test override: populated ScrapBook 1 mystery flags");
+}
+
Common::Error EEMEngine::run() {
// _SetMode13X @ 1000:0358 enters VGA mode 13h (320x200x256).
initGraphics(320, 200);
@@ -243,6 +258,7 @@ Common::Error EEMEngine::run() {
if (wantedSave >= 0) {
const Common::Error err = loadGameState(wantedSave);
if (err.getCode() == Common::kNoError) {
+ applyStartupTestOverrides();
CursorMan.showMouse(true);
if (_mystery.isLoaded()) {
debugC(1, kDebugGeneral,
@@ -390,6 +406,8 @@ Common::Error EEMEngine::run() {
// picks "[New Player]".
if (!shouldQuit())
doProfilePicker();
+ if (!shouldQuit())
+ applyStartupTestOverrides();
if (!shouldQuit())
doChoosePartner();
@@ -549,15 +567,18 @@ screen_loop:
return Common::kNoError;
}
-void EEMEngine::setHotspotMouseCursor(bool active) {
- active = active && ConfMan.getBool("hide_highlight_boxes");
- if (_hotspotMouseCursor == active)
+void EEMEngine::setInteractiveMouseCursor(bool active) {
+ if (_interactiveMouseCursor == active)
return;
- _hotspotMouseCursor = active;
+ _interactiveMouseCursor = active;
installMouseCursor(_picsArchive, active);
}
+void EEMEngine::setHotspotMouseCursor(bool active) {
+ setInteractiveMouseCursor(active);
+}
+
bool EEMEngine::openArchives() {
// _InitGraphicsSystem @ 172b:0145 opens these five .DBD/.DBX pairs.
if (!_picsArchive.open(Common::Path("PICS.DBD"), Common::Path("PICS.DBX"))) {
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 792db1288d2..d6080d93fbd 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -188,8 +188,12 @@ public:
const EEMFont &getFont() const { return _font; }
uint8 getPartnerIndex() const { return _partner; }
- /// Switch to a lightly tinted cursor while the mouse is over a
- /// searchable hotspot and the highlight boxes are hidden.
+ /// Switch to the red-outline cursor used to hint at interactive
+ /// regions that do not have their own original highlight art.
+ void setInteractiveMouseCursor(bool active);
+
+ /// Switch to the interactive cursor while the mouse is over a
+ /// searchable hotspot.
void setHotspotMouseCursor(bool active);
/// Display one ClueBlock. @p clueBlock points at the u16 frame count
@@ -302,6 +306,8 @@ public:
bool areYouSure();
private:
+ void applyStartupTestOverrides();
+
/**
* Central dispatch loop matching the original _ScreenDriver @ 1a35:0dc1.
* Each iteration calls the screen handler that matches _nextScreen.
@@ -426,7 +432,7 @@ private:
/// Returns the direction the user wants the caller to navigate:
/// -1 â previous mystery (LEFT pressed on the first page or
/// click in `PrevPageRect` while on first page),
- /// 0 â exit the scrapbook (ESC or click outside both rects),
+ /// 0 â exit the scrapbook (ESC / quit; central clicks are ignored),
/// +1 â next mystery (RIGHT/SPACE/Enter/click on last page).
/// Mirrors `_DisplayEnding`'s `[BP-0x18]` return at 1df2:0723.
int doShowEnding(uint num, bool firstPage = true);
@@ -569,7 +575,7 @@ private:
/// `setPartnerEraseBg`.
Graphics::ManagedSurface _partnerEraseBg;
- bool _hotspotMouseCursor = false;
+ bool _interactiveMouseCursor = false;
/// XMIDI music player. Mirrors the original `MIDI.C` family
/// (`_MIDIPlayFile`, `_MIDIPlay`, `_StopMIDI`, `_StartTravelMusic`
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index b746dd267bc..d7dd55bf19d 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -55,6 +55,9 @@ const GallerySlot kGallerySlots[5] = {
{ 191, 90 } // 4
};
+constexpr Common::Rect kEndingPrevPageRect(Common::Point(0, 0), 28, 200);
+constexpr Common::Rect kEndingNextPageRect(Common::Point(292, 0), 28, 200);
+
// Floppy gallery slot positions verified at `2608:0x16c` (5 Ã
// {u16 x, u16 y}) â read by `_DrawGallery_Floppy @ 154e:0045`'s
// `[BX + 0x16c]` (x) and `[BX + 0x16e]` (y) loads. The floppy
@@ -596,14 +599,14 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
// colour), with no drop shadow. Verified at the call site asm
// 1df2:04cf-1df2:04f4 (Ghidra mis-paired the two trailing args).
//
- // Page navigation mirrors the original key/click handlers
+ // Keyboard page navigation mirrors the original handlers
// (1df2:0689 / 1df2:06a0): LEFT decrements pageIdx, RIGHT (or
- // SPACE / Enter / click) increments it. Hitting the boundary
- // (LEFT on page 0, RIGHT on last page) sets `[BP-0x18]` to -1 / 1
- // respectively and exits â that return value is what
- // `_ShowScrapbook` uses to walk forward / backward through
- // solved mysteries (see 1f78:0664-1f78:069c). ESC and clicks
- // outside both PrevPage / NextPage rects exit with `[BP-0x18]=0`.
+ // SPACE / Enter) increments it. Hitting the boundary (LEFT on page
+ // 0, RIGHT on last page) sets `[BP-0x18]` to -1 / 1 respectively
+ // and exits â that return value is what `_ShowScrapbook` uses to
+ // walk forward / backward through solved mysteries (see
+ // 1f78:0664-1f78:069c). Mouse navigation is intentionally limited
+ // to the red-highlighted edge rects so central page clicks are ignored.
//
// `firstPage=false` opens the ending at the LAST page (used by
// `doShowScrapbook` after a "previous mystery" navigation â
@@ -645,6 +648,7 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
// text in particular is palette index 0 (= newspaper black) so we
// MUST switch palettes before rendering.
setSitePalette(0);
+ CursorMan.showMouse(true);
// Walk page records. Each page header is 10 bytes; text is
// null-terminated and follows the header.
@@ -670,6 +674,11 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
int direction = 0; // -1 / 0 / +1, see header doc.
bool exitLoop = false;
bool dirty = true;
+ const Common::Point mousePos = g_system->getEventManager()->getMousePos();
+ setInteractiveMouseCursor(kEndingPrevPageRect.contains(mousePos.x,
+ mousePos.y) ||
+ kEndingNextPageRect.contains(mousePos.x,
+ mousePos.y));
while (!shouldQuit() && !exitLoop) {
if (dirty) {
const uint off = pageOffsets[pageIdx];
@@ -715,8 +724,14 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
while (g_system->getEventManager()->pollEvent(ev)) {
if (ev.type == Common::EVENT_QUIT ||
ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
- return 0;
+ direction = 0;
+ exitLoop = true;
+ break;
}
+ if (ev.type == Common::EVENT_MOUSEMOVE)
+ setInteractiveMouseCursor(
+ kEndingPrevPageRect.contains(ev.mouse.x, ev.mouse.y) ||
+ kEndingNextPageRect.contains(ev.mouse.x, ev.mouse.y));
if (ev.type == Common::EVENT_KEYDOWN) {
switch (ev.kbd.keycode) {
case Common::KEYCODE_ESCAPE:
@@ -754,12 +769,10 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
break;
}
if (ev.type == Common::EVENT_LBUTTONDOWN) {
- // Original PrevPage/NextPage rects at 29be:1078 /
- // 29be:1080. Clicks outside both rects exit with
- // direction 0.
- const Common::Rect kPrevPageRect(0, 0, 28, 200);
- const Common::Rect kNextPageRect(28, 7, 317, 197);
- if (kPrevPageRect.contains(ev.mouse.x, ev.mouse.y)) {
+ setInteractiveMouseCursor(
+ kEndingPrevPageRect.contains(ev.mouse.x, ev.mouse.y) ||
+ kEndingNextPageRect.contains(ev.mouse.x, ev.mouse.y));
+ if (kEndingPrevPageRect.contains(ev.mouse.x, ev.mouse.y)) {
if (pageIdx > 0) {
pageIdx--;
dirty = true;
@@ -767,7 +780,7 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
direction = -1;
exitLoop = true;
}
- } else if (kNextPageRect.contains(ev.mouse.x, ev.mouse.y)) {
+ } else if (kEndingNextPageRect.contains(ev.mouse.x, ev.mouse.y)) {
if (pageIdx + 1 < validPages) {
pageIdx++;
dirty = true;
@@ -775,9 +788,6 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
direction = 1;
exitLoop = true;
}
- } else {
- direction = 0;
- exitLoop = true;
}
if (exitLoop)
break;
@@ -786,6 +796,7 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
g_system->updateScreen();
g_system->delayMillis(15);
}
+ setInteractiveMouseCursor(false);
return direction;
}
@@ -1191,25 +1202,19 @@ void EEMEngine::doSetup() {
// handlers call `_ShowScrapbook(stage, 0)`, with stages 2
// and 3 gated by chain progress.
if (kScrap1Btn.contains(mx, my)) {
- CursorMan.showMouse(false);
doShowScrapbook(1);
- CursorMan.showMouse(true);
setSitePalette(0);
dirty = true;
continue;
}
if (kScrap2Btn.contains(mx, my) && _chainStage >= 2) {
- CursorMan.showMouse(false);
doShowScrapbook(2);
- CursorMan.showMouse(true);
setSitePalette(0);
dirty = true;
continue;
}
if (kScrap3Btn.contains(mx, my) && _chainStage >= 3) {
- CursorMan.showMouse(false);
doShowScrapbook(3);
- CursorMan.showMouse(true);
setSitePalette(0);
dirty = true;
continue;
@@ -1493,9 +1498,7 @@ void EEMEngine::doCaseSelection() {
// â viewing the scrapbook never starts a new case.
const uint stage = (pick == kPickScrap1) ? 1
: (pick == kPickScrap2) ? 2 : 3;
- CursorMan.showMouse(false);
doShowScrapbook(stage);
- CursorMan.showMouse(true);
setSitePalette(0);
_mystery.clear();
return;
Commit: 916c34e2b2163222ad0cba2f06518cc53a604cd4
https://github.com/scummvm/scummvm/commit/916c34e2b2163222ad0cba2f06518cc53a604cd4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:58+02:00
Commit Message:
EEM: highlight pda interactive zones
Changed paths:
engines/eem/site.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index c47e8882f4d..1a71f83e417 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -35,6 +35,10 @@
namespace EEM {
+constexpr Common::Rect kSitePdaRect(Common::Point(35, 111), 21, 25);
+constexpr Common::Rect kSitePartnerFootMapRect(Common::Point(7, 177), 50, 23);
+constexpr Common::Rect kSitePartnerHeadHintRect(Common::Point(5, 80), 39, 30);
+
// Masked blit a Picture into a ManagedSurface. Pixels equal to `transp`
// (the high byte of `pic.flags`, per `_Rect_Move_Mask @ 1000:03fc`) are
// skipped. Used by `enterSiteAnim` for both skateboard + KD slide-in
@@ -761,27 +765,24 @@ void SiteScreen::run() {
// (CD `_NextScreen = 1`,
// floppy = 2)
// Test the buttons before falling through to hotspots so
- // a click on the PDA / map icon doesn't accidentally
+ // a click on the PDA / partner foot doesn't accidentally
// trigger a hotspot underneath.
- const Common::Rect kBtnNotebook(35, 111, 56, 136);
- const Common::Rect kBtnMap ( 7, 177, 57, 200);
- // Partner area â port-only enhancement so the player
+ // Partner head is a port-only enhancement so the player
// can click the host sprite for a hint, mirroring the
// PDA's rect-3 / gallery's rect-3 behaviour. The
// original site loop's `_FindButton(&SiteButtons, 2,
// ...)` only checks notebook + map, but the same
- // partner-click â `_KDHelp` shortcut is wired in
+ // partner-click -> `_KDHelp` shortcut is wired in
// `_HandleNoteButton[3]` (0x0403) and
// `_HandleGalleryButton[3]` (0x061e). Rect matches
// the PDA / gallery `kBtnPartner` (5, 80, 44, 110).
- const Common::Rect kBtnPartner ( 5, 80, 44, 110);
- if (kBtnNotebook.contains(event.mouse.x, event.mouse.y)) {
+ if (kSitePdaRect.contains(event.mouse.x, event.mouse.y)) {
notePartnerActivity();
_vm->setHotspotMouseCursor(false);
_vm->setNextScreen(kScreenNotebook);
return;
}
- if (kBtnMap.contains(event.mouse.x, event.mouse.y)) {
+ if (kSitePartnerFootMapRect.contains(event.mouse.x, event.mouse.y)) {
notePartnerActivity();
_vm->setHotspotMouseCursor(false);
// CD writes `_NextScreen = 1`; floppy writes 2.
@@ -789,7 +790,7 @@ void SiteScreen::run() {
: kScreenMap);
return;
}
- if (kBtnPartner.contains(event.mouse.x, event.mouse.y)) {
+ if (kSitePartnerHeadHintRect.contains(event.mouse.x, event.mouse.y)) {
_vm->setHotspotMouseCursor(false);
_vm->doHelp();
notePartnerActivity();
@@ -1477,7 +1478,10 @@ int SiteScreen::hotspotAtPoint(uint siteNum, int x, int y) const {
void SiteScreen::updateHotspotCursor(uint siteNum, int x, int y) {
if (!_vm)
return;
- _vm->setHotspotMouseCursor(hotspotAtPoint(siteNum, x, y) >= 0);
+ const bool siteControl = kSitePdaRect.contains(x, y) ||
+ kSitePartnerFootMapRect.contains(x, y) ||
+ kSitePartnerHeadHintRect.contains(x, y);
+ _vm->setHotspotMouseCursor(siteControl || hotspotAtPoint(siteNum, x, y) >= 0);
}
void SiteScreen::onHotspotClicked(uint siteNum, uint hotIdx) {
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index d7dd55bf19d..0eec0d2fe5e 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -58,6 +58,55 @@ const GallerySlot kGallerySlots[5] = {
constexpr Common::Rect kEndingPrevPageRect(Common::Point(0, 0), 28, 200);
constexpr Common::Rect kEndingNextPageRect(Common::Point(292, 0), 28, 200);
+constexpr Common::Rect kPdaHelpRect(Common::Point(93, 174), 22, 16);
+constexpr Common::Rect kPdaNotebookRect(Common::Point(134, 174), 21, 16);
+constexpr Common::Rect kPdaGalleryRect(Common::Point(157, 174), 21, 16);
+constexpr Common::Rect kPdaPartnerHeadHintRect(Common::Point(5, 80), 39, 30);
+constexpr Common::Rect kPdaAccuseRect(Common::Point(180, 174), 21, 16);
+constexpr Common::Rect kPdaPageNextRect(Common::Point(204, 174), 20, 16);
+constexpr Common::Rect kPdaPagePrevRect(Common::Point(226, 174), 21, 16);
+constexpr Common::Rect kPdaHelp2Rect(Common::Point(267, 174), 21, 16);
+constexpr Common::Rect kPdaPartnerFootMapRect(Common::Point(7, 177), 50, 23);
+constexpr Common::Rect kPdaSiteRect(Common::Point(35, 111), 21, 25);
+
+bool notebookButtonAt(int x, int y) {
+ return kPdaHelpRect.contains(x, y) ||
+ kPdaGalleryRect.contains(x, y) ||
+ kPdaPartnerHeadHintRect.contains(x, y) ||
+ kPdaAccuseRect.contains(x, y) ||
+ kPdaPageNextRect.contains(x, y) ||
+ kPdaPagePrevRect.contains(x, y) ||
+ kPdaHelp2Rect.contains(x, y) ||
+ kPdaPartnerFootMapRect.contains(x, y) ||
+ kPdaSiteRect.contains(x, y);
+}
+
+bool galleryButtonAt(int x, int y) {
+ return kPdaSiteRect.contains(x, y) ||
+ kPdaPartnerFootMapRect.contains(x, y) ||
+ kPdaAccuseRect.contains(x, y) ||
+ kPdaNotebookRect.contains(x, y) ||
+ kPdaHelpRect.contains(x, y) ||
+ kPdaPartnerHeadHintRect.contains(x, y);
+}
+
+bool rectListContains(const Common::Array<Common::Rect> &rects, int x, int y) {
+ for (uint i = 0; i < rects.size(); i++) {
+ if (rects[i].contains(x, y))
+ return true;
+ }
+ return false;
+}
+
+bool gallerySlotAt(const Common::Array<Common::Rect> &rects,
+ const Common::Array<int> &suspects, int x, int y) {
+ for (uint i = 0; i < rects.size() && i < suspects.size(); i++) {
+ if (suspects[i] >= 0 && rects[i].contains(x, y))
+ return true;
+ }
+ return false;
+}
+
// Floppy gallery slot positions verified at `2608:0x16c` (5 Ã
// {u16 x, u16 y}) â read by `_DrawGallery_Floppy @ 154e:0045`'s
// `[BX + 0x16c]` (x) and `[BX + 0x16e]` (y) loads. The floppy
@@ -1749,15 +1798,6 @@ void EEMEngine::doNotebook() {
// rect 8 (35,111) â 0x03ed = `_NextScreen = 3` (SITE)
// rect 9 (0,0) â 0x03ed = same as rect 8
// rect 10 (66,79) â 0x03f9 = `_InterfaceHelp(0)` (note-area help)
- const Common::Rect kBtnHelp1 ( 93, 174, 115, 190); // [1] HELP
- const Common::Rect kBtnGallery (157, 174, 178, 190); // [2] GALLERY
- const Common::Rect kBtnPartner ( 5, 80, 44, 110); // [3] KD HELP
- const Common::Rect kBtnAccuse (180, 174, 201, 190); // [4] SOLVE
- const Common::Rect kBtnPageNext(204, 174, 224, 190); // [5] PAGE NEXT
- const Common::Rect kBtnPagePrev(226, 174, 247, 190); // [6] PAGE PREV
- const Common::Rect kBtnMap ( 7, 177, 57, 200); // [7] MAP
- const Common::Rect kBtnSite ( 35, 111, 56, 136); // [8] SITE
- const Common::Rect kBtnHelp2 (267, 174, 288, 190); // [10] extra HELP
// (`_NoteButtons @ 29be:0147` actually has rect [10] at
// (267,174,288,190) â small button on the right of the bottom
// bar that the original handler dispatch table at 161e:04ec
@@ -1772,6 +1812,10 @@ void EEMEngine::doNotebook() {
(void)hoveredNoteSlot;
drawNotebookFrame(page);
+ Common::Point mouse = g_system->getEventManager()->getMousePos();
+ setInteractiveMouseCursor(notebookButtonAt(mouse.x, mouse.y) ||
+ rectListContains(_notebookSlotRects,
+ mouse.x, mouse.y));
uint32 lastDraw = g_system->getMillis();
@@ -1786,6 +1830,13 @@ void EEMEngine::doNotebook() {
exitFlag = true;
break;
}
+ if (ev.type == Common::EVENT_MOUSEMOVE) {
+ setInteractiveMouseCursor(notebookButtonAt(ev.mouse.x,
+ ev.mouse.y) ||
+ rectListContains(_notebookSlotRects,
+ ev.mouse.x,
+ ev.mouse.y));
+ }
if (ev.type == Common::EVENT_KEYDOWN) {
if (ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
_nextScreen = kScreenSite;
@@ -1809,52 +1860,55 @@ void EEMEngine::doNotebook() {
// button 0 / 9 are dead zones, so check the actionable
// rects directly. Earlier rects "win" when overlapping
// (matches `_FindButton`).
- if (kBtnSite.contains(ev.mouse.x, ev.mouse.y)) {
+ if (kPdaSiteRect.contains(ev.mouse.x, ev.mouse.y)) {
_nextScreen = kScreenSite;
exitFlag = true;
break; // back to site
}
- if (kBtnMap.contains(ev.mouse.x, ev.mouse.y)) {
+ if (kPdaPartnerFootMapRect.contains(ev.mouse.x, ev.mouse.y)) {
_nextScreen = kScreenMapAlt;
exitFlag = true;
break;
}
- if (kBtnPartner.contains(ev.mouse.x, ev.mouse.y)) {
+ if (kPdaPartnerHeadHintRect.contains(ev.mouse.x, ev.mouse.y)) {
+ setInteractiveMouseCursor(false);
doHelp(); // _KDHelp = host hint
dirty = true;
continue;
}
- if (kBtnAccuse.contains(ev.mouse.x, ev.mouse.y)) {
+ if (kPdaAccuseRect.contains(ev.mouse.x, ev.mouse.y)) {
_nextScreen = kScreenAccuse;
exitFlag = true;
break;
}
- if (kBtnGallery.contains(ev.mouse.x, ev.mouse.y)) {
+ if (kPdaGalleryRect.contains(ev.mouse.x, ev.mouse.y)) {
_nextScreen = kScreenGallery;
exitFlag = true;
break;
}
- if (kBtnHelp1.contains(ev.mouse.x, ev.mouse.y)) {
+ if (kPdaHelpRect.contains(ev.mouse.x, ev.mouse.y)) {
// rect 1 â `_InterfaceHelp(0)`: walks `HelpData[0]` and
// blits PICs 0x63 / 0x1ae fullscreen for click-through.
+ setInteractiveMouseCursor(false);
doInterfaceHelp(0);
dirty = true;
continue;
}
- if (kBtnPagePrev.contains(ev.mouse.x, ev.mouse.y)) {
+ if (kPdaPagePrevRect.contains(ev.mouse.x, ev.mouse.y)) {
if (page > 0)
page--;
dirty = true;
continue;
}
- if (kBtnPageNext.contains(ev.mouse.x, ev.mouse.y)) {
+ if (kPdaPageNextRect.contains(ev.mouse.x, ev.mouse.y)) {
page++;
dirty = true;
continue;
}
- if (kBtnHelp2.contains(ev.mouse.x, ev.mouse.y)) {
+ if (kPdaHelp2Rect.contains(ev.mouse.x, ev.mouse.y)) {
// `_NoteButtons[10]` â handler 0x03f9 = same
// `_InterfaceHelp(0)` as button [1].
+ setInteractiveMouseCursor(false);
doInterfaceHelp(0);
dirty = true;
continue;
@@ -1887,10 +1941,15 @@ void EEMEngine::doNotebook() {
if (dirty || now - lastDraw >= 100) {
drawNotebookFrame(page);
lastDraw = now;
+ mouse = g_system->getEventManager()->getMousePos();
+ setInteractiveMouseCursor(notebookButtonAt(mouse.x, mouse.y) ||
+ rectListContains(_notebookSlotRects,
+ mouse.x, mouse.y));
}
g_system->updateScreen();
g_system->delayMillis(15);
}
+ setInteractiveMouseCursor(false);
}
void EEMEngine::drawNotebookFrame(int &page) {
@@ -2126,6 +2185,10 @@ void EEMEngine::doGallery() {
}
drawGalleryFrame(gd, num, slotRects, slotSuspect);
+ Common::Point mouse = g_system->getEventManager()->getMousePos();
+ setInteractiveMouseCursor(galleryButtonAt(mouse.x, mouse.y) ||
+ gallerySlotAt(slotRects, slotSuspect,
+ mouse.x, mouse.y));
uint32 lastDraw = g_system->getMillis();
while (!shouldQuit()) {
@@ -2135,8 +2198,17 @@ void EEMEngine::doGallery() {
if (ev.type == Common::EVENT_QUIT ||
ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
_nextScreen = kScreenInvalid;
+ setInteractiveMouseCursor(false);
return;
}
+ if (ev.type == Common::EVENT_MOUSEMOVE) {
+ setInteractiveMouseCursor(galleryButtonAt(ev.mouse.x,
+ ev.mouse.y) ||
+ gallerySlotAt(slotRects,
+ slotSuspect,
+ ev.mouse.x,
+ ev.mouse.y));
+ }
if (ev.type == Common::EVENT_KEYDOWN) {
if (ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
_nextScreen = kScreenSite;
@@ -2160,37 +2232,37 @@ void EEMEngine::doGallery() {
// rect 6 (226,247) â 0x0638 = generic exit
// rect 7 (7,177) â 0x05f7 = `_NextScreen = 2` (MAP)
// rect 8 (35,111) â 0x05e4 = `_NextScreen = 3` (SITE)
- const Common::Rect kBtnSite ( 35, 111, 56, 136); // [8] SITE
- const Common::Rect kBtnMap ( 7, 177, 57, 200); // [7] MAP
- const Common::Rect kBtnAccuse (180, 174, 201, 190); // [4] SOLVE
- const Common::Rect kBtnNotebook(134, 174, 155, 190); // [0] NOTEBOOK (back to PDA notes)
- const Common::Rect kBtnHelp ( 93, 174, 115, 190); // [1] HELP
- const Common::Rect kBtnPartner ( 5, 80, 44, 110); // [3] KD HELP
- if (kBtnSite.contains(ev.mouse.x, ev.mouse.y)) {
+ if (kPdaSiteRect.contains(ev.mouse.x, ev.mouse.y)) {
_nextScreen = kScreenSite;
- exitFlag = true; break;
+ exitFlag = true;
+ break;
}
- if (kBtnMap.contains(ev.mouse.x, ev.mouse.y)) {
+ if (kPdaPartnerFootMapRect.contains(ev.mouse.x, ev.mouse.y)) {
_nextScreen = kScreenMapAlt;
- exitFlag = true; break;
+ exitFlag = true;
+ break;
}
- if (kBtnAccuse.contains(ev.mouse.x, ev.mouse.y)) {
+ if (kPdaAccuseRect.contains(ev.mouse.x, ev.mouse.y)) {
_nextScreen = kScreenAccuse;
- exitFlag = true; break;
+ exitFlag = true;
+ break;
}
- if (kBtnNotebook.contains(ev.mouse.x, ev.mouse.y)) {
+ if (kPdaNotebookRect.contains(ev.mouse.x, ev.mouse.y)) {
_nextScreen = kScreenNotebook;
- exitFlag = true; break;
+ exitFlag = true;
+ break;
}
- if (kBtnHelp.contains(ev.mouse.x, ev.mouse.y)) {
+ if (kPdaHelpRect.contains(ev.mouse.x, ev.mouse.y)) {
// Gallery rect 1 â `_InterfaceHelp(0)` per jmp table at
// 158f:0625 (HandleGalleryButton). Same picture sequence
// as the notebook HELP button.
+ setInteractiveMouseCursor(false);
doInterfaceHelp(0);
lastDraw = 0;
continue;
}
- if (kBtnPartner.contains(ev.mouse.x, ev.mouse.y)) {
+ if (kPdaPartnerHeadHintRect.contains(ev.mouse.x, ev.mouse.y)) {
+ setInteractiveMouseCursor(false);
doHelp();
lastDraw = 0;
continue;
@@ -2228,18 +2300,18 @@ void EEMEngine::doGallery() {
const bool floppyMI = isFloppy();
const uint suspectIdx = (uint)slotSuspect[i];
const byte *suspect = floppyMI
- ? _mystery.floppySuspectEntry(suspectIdx)
- : gd + suspectIdx * 0x46;
+ ? _mystery.floppySuspectEntry(suspectIdx)
+ : gd + suspectIdx * 0x46;
if (!suspect)
break;
const uint16 detailPic =
READ_LE_UINT16(suspect + 0);
const uint clueCount = floppyMI
- ? (uint)suspect[4]
- : READ_LE_UINT16(suspect + 8);
+ ? (uint)suspect[4]
+ : READ_LE_UINT16(suspect + 8);
Graphics::ManagedSurface ms(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
+ Graphics::PixelFormat::createFormatCLUT8());
ms.clear();
if (haveBg) {
const int bw = MIN<int>(galBg.surface.w, 320);
@@ -2260,19 +2332,20 @@ void EEMEngine::doGallery() {
// stays visible on the left. Without this
// blit the suspect-detail screen has no
// partner.
+ setInteractiveMouseCursor(false);
{
const uint partnerAnim =
(_partner == 0) ? 2 : 0x10;
Animation partnerAni;
if (_aniArchive.loadAnimation(partnerAnim,
- partnerAni) &&
+ partnerAni) &&
!partnerAni.empty()) {
const uint32 now = g_system->getMillis();
const uint frameIdx =
partnerFrameAtTick(0x02,
- (uint)partnerAni.size(), now);
+ (uint)partnerAni.size(), now);
blitAnimFrameAnchored(ms.surfacePtr(),
- partnerAni[frameIdx], 5, 0x50);
+ partnerAni[frameIdx], 5, 0x50);
}
}
// Full suspect picture at (0x94, 0xf).
@@ -2399,8 +2472,10 @@ void EEMEngine::doGallery() {
Common::Event drain;
while (g_system->getEventManager()->pollEvent(drain)) {
if (drain.type == Common::EVENT_QUIT ||
- drain.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ drain.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ setInteractiveMouseCursor(false);
return;
+ }
}
}
bool back = false;
@@ -2415,8 +2490,10 @@ void EEMEngine::doGallery() {
break;
}
if (e2.type == Common::EVENT_QUIT ||
- e2.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ e2.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ setInteractiveMouseCursor(false);
return;
+ }
}
// Per-tick `updateScreen()` so the SDL cursor
// follows the mouse â without it the cursor
@@ -2431,6 +2508,13 @@ void EEMEngine::doGallery() {
// MoreInfo screen until the next 100 ms tick.
drawGalleryFrame(gd, num, slotRects, slotSuspect);
lastDraw = g_system->getMillis();
+ mouse = g_system->getEventManager()->getMousePos();
+ setInteractiveMouseCursor(galleryButtonAt(mouse.x,
+ mouse.y) ||
+ gallerySlotAt(slotRects,
+ slotSuspect,
+ mouse.x,
+ mouse.y));
clicked = true;
break;
}
@@ -2445,6 +2529,10 @@ void EEMEngine::doGallery() {
if (now - lastDraw >= 100) {
drawGalleryFrame(gd, num, slotRects, slotSuspect);
lastDraw = now;
+ mouse = g_system->getEventManager()->getMousePos();
+ setInteractiveMouseCursor(galleryButtonAt(mouse.x, mouse.y) ||
+ gallerySlotAt(slotRects, slotSuspect,
+ mouse.x, mouse.y));
}
// `g_system->updateScreen()` is what tells the framework to
// re-render the cursor at its current mouse position; without
@@ -2454,6 +2542,7 @@ void EEMEngine::doGallery() {
g_system->updateScreen();
g_system->delayMillis(15);
}
+ setInteractiveMouseCursor(false);
}
void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
@@ -4283,7 +4372,9 @@ void EEMEngine::doAccuse() {
playAnm(Common::Path("SCRAPBK.ANI"), 120, true);
// `_ShowOneScrap @ 1f78:0773` is `_DisplayEnding(num, 1)` â
- // the multi-page per-mystery ending narrative.
+ // the multi-page per-mystery ending narrative. This shares the
+ // same red edge cursor + edge-only mouse navigation used by the
+ // scrapbook browser.
doShowEnding(mn);
// Mirrors `_SavePlayerRecord` at 1df2:0857 â once the
Commit: 3354f1af87df5f24b18641559fee9c4ca1f54e54
https://github.com/scummvm/scummvm/commit/3354f1af87df5f24b18641559fee9c4ca1f54e54
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:59+02:00
Commit Message:
EEM: handle initial clues for the CD version
Changed paths:
engines/eem/clues.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 62cf4047ec2..35396daedfc 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -64,6 +64,29 @@ const int kBoyY = 0x62; // 98
const int kGirlX = 0x42; // 66
const int kGirlY = 0x60; // 96
+uint markClueBlockNotebookEntries(Mystery &mystery, const byte *clueBlock) {
+ if (!clueBlock)
+ return 0;
+
+ const uint16 number = READ_LE_UINT16(clueBlock);
+ if (number == 0 || number > 32)
+ return 0;
+
+ uint marked = 0;
+ for (uint i = 0; i < number; i++) {
+ const byte *entry = clueBlock + 4 + i * 62;
+ for (uint j = 0; j < 5; j++) {
+ const uint16 note = READ_LE_UINT16(entry + 0x30 + j * 2);
+ if (note != 0xFFFF && note < Mystery::kCluesFoundCap &&
+ mystery._cluesFound[note] == 0) {
+ mystery._cluesFound[note] = 1;
+ marked++;
+ }
+ }
+ }
+ return marked;
+}
+
// `_DoHappiness @ 172b:27b5`: each cursor zone swaps the partner's
// sequence script to a more / less "happy" cycle. Boy seqs lifted
// verbatim from `29be:0337` (5 Ã 0x14 bytes), girl seqs from
@@ -540,10 +563,24 @@ void EEMEngine::doInitClues() {
// dialog_records[nDialog]` (each record `11 + textCount` bytes),
// dispatched via `FUN_22dc_05c8 @ 22dc:05c8`. We render dialog
// records ourselves on floppy.
- if (floppy)
+ if (floppy) {
displayFloppyBriefing(ib);
- else
- displayClue(ib + 4);
+ } else {
+ const byte *briefingClues = ib + 4;
+ // Ghidra confirms CD `_DoInitClues` enters the normal
+ // `_DisplayClue(_InitBlock + 2, 1)` path, and `_DisplayClue`
+ // calls `_AddNotebook` for each ClueEntry note list at
+ // +0x30..+0x39. These starting notes are required before the
+ // first PDA visit, so mark them explicitly here. The regular
+ // displayClue side-effect pass is idempotent and still handles
+ // gallery/site updates in the original order.
+ const uint marked = markClueBlockNotebookEntries(_mystery, briefingClues);
+ if (marked != 0)
+ debugC(1, kDebugScript,
+ "doInitClues: marked %u CD briefing notebook entries",
+ marked);
+ displayClue(briefingClues);
+ }
}
/// Mirror `_ParseString` @ 1b66:07c3 â substitute the control bytes that
Commit: 7ac0106b653f132bfecd216dd58e2834786184ff
https://github.com/scummvm/scummvm/commit/7ac0106b653f132bfecd216dd58e2834786184ff
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:59+02:00
Commit Message:
EEM: improved animations
Changed paths:
engines/eem/animation.cpp
engines/eem/animation.h
engines/eem/clues.cpp
engines/eem/eem.cpp
engines/eem/eem.h
diff --git a/engines/eem/animation.cpp b/engines/eem/animation.cpp
index 929fc18025e..34364c6bf12 100644
--- a/engines/eem/animation.cpp
+++ b/engines/eem/animation.cpp
@@ -141,6 +141,14 @@ void ANMDecoder::getPalette8(byte *out) const {
out[i] = (byte)(_palette[i] << 2);
}
+void ANMDecoder::seedFrameBuffer(const byte *pixels, uint pitch) {
+ if (!pixels || _buffer.empty() || _width == 0 || _height == 0)
+ return;
+
+ for (uint y = 0; y < _height; y++)
+ memcpy(_buffer.data() + y * _width, pixels + y * pitch, _width);
+}
+
const byte *ANMDecoder::nextFrame() {
if (_nextFrameIdx >= _frameCount)
return nullptr;
diff --git a/engines/eem/animation.h b/engines/eem/animation.h
index 9397872e2fd..09b6e0406d1 100644
--- a/engines/eem/animation.h
+++ b/engines/eem/animation.h
@@ -67,6 +67,9 @@ public:
/// Fill @p out (768 bytes) with the 8-bit-shifted palette. Convenience.
void getPalette8(byte *out) const;
+ /// Seed the persistent frame buffer from the current 8-bit screen pixels.
+ void seedFrameBuffer(const byte *pixels, uint pitch);
+
/**
* Decode the next frame in place into the internal buffer. Returns a
* pointer to the @c width()*@c height() byte image, or nullptr at EOF.
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 35396daedfc..82805c95635 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -427,17 +427,23 @@ void EEMEngine::doInitClues() {
}
}
- // Composite the final frames (or first frames if skipped) so the BG
- // is in a sensible state when displayClue overlays the speaker.
- if (_picsArchive.getPicture(0x52, bg))
- blitAt(bg, 0, 0);
- if (haveGame)
- blitMaskedToScreen(game[0], 0xcd, 0x6c);
- if (haveBook)
- blitMaskedToScreen(book[0], 0, 99);
- if (haveNancy)
- blitMaskedToScreen(nancy[0], 0x68, 0x8b);
- g_system->updateScreen();
+ // Freeze the completed setup animation as the base for
+ // `_PlayInSequence`. The original clears the registered animations
+ // before playing the short case-type overlay, so the game/book/nancy
+ // cells do not keep cycling or wrap back to cell 0 underneath it.
+ Graphics::ManagedSurface briefingBase(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ briefingBase.clear();
+ {
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (screen) {
+ for (int row = 0; row < 200; row++) {
+ memcpy((byte *)briefingBase.getBasePtr(0, row),
+ (const byte *)screen->getBasePtr(0, row), 320);
+ }
+ g_system->unlockScreen();
+ }
+ }
// Step 5 â `_PlayInSequence(animSeq, 0xcd, animY)` per Ghidra:
// Jake (partner=0):
@@ -489,16 +495,11 @@ void EEMEngine::doInitClues() {
for (uint frame = 0; frame < seq.size() && !shouldQuit() && !skip;
frame++) {
const Picture &fr = seq[frame];
- // Restore BG + base anim frames so each new frame
- // composites cleanly.
- if (_picsArchive.getPicture(0x52, bg))
- blitAt(bg, 0, 0);
- if (haveGame)
- blitMaskedToScreen(game[frame % game.size()], 0xcd, 0x6c);
- if (haveBook)
- blitMaskedToScreen(book[frame % book.size()], 0, 99);
- if (haveNancy)
- blitMaskedToScreen(nancy[frame % nancy.size()], 0x68, 0x8b);
+ // Restore the frozen setup frame so the short overlay
+ // does not make the setup animation wrap to frame 0.
+ g_system->copyRectToScreen(briefingBase.getPixels(),
+ briefingBase.pitch, 0, 0,
+ 320, 200);
// Anchor: `_PlayInSequence @ 172b:2d35-2d50` does
// dstX = sx - cell[+0x8] ; miscflags (signed)
// dstY = sy - cell[+0x6] ; rowoff (signed)
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 6ca48a2a385..58dcfaa572b 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -99,6 +99,34 @@ const byte kCursorInteractivePalette[] = {
0xFF, 0xFF, 0xFF // 2 â white fill
};
+static void fadeCurrentPaletteToBlack(uint delayMs = 8) {
+ byte start[kPalSize];
+ byte stepPal[kPalSize];
+ g_system->getPaletteManager()->grabPalette(start, 0, 256);
+
+ for (int step = 15; step >= 0; step--) {
+ for (uint i = 0; i < kPalSize; i++)
+ stepPal[i] = (byte)((uint)start[i] * step / 16);
+ g_system->getPaletteManager()->setPalette(stepPal, 0, 256);
+ g_system->updateScreen();
+ if (delayMs)
+ g_system->delayMillis(delayMs);
+ }
+}
+
+static void fadePaletteFromBlack(const byte *target, uint delayMs = 8) {
+ byte stepPal[kPalSize];
+
+ for (uint step = 1; step <= 16; step++) {
+ for (uint i = 0; i < kPalSize; i++)
+ stepPal[i] = (byte)((uint)target[i] * step / 16);
+ g_system->getPaletteManager()->setPalette(stepPal, 0, 256);
+ g_system->updateScreen();
+ if (delayMs)
+ g_system->delayMillis(delayMs);
+ }
+}
+
static void setInteractiveCursorPalette(const Picture &cursor, byte transparent) {
byte palette[kPalSize];
bool used[256];
@@ -349,7 +377,10 @@ Common::Error EEMEngine::run() {
if (!shouldQuit() && !_skipIntro) {
if (_audio)
_audio->playVoc(Common::Path("THUNDER.VOC"));
- playAnm(Common::Path("BOLT.ANM"));
+ playAnm(Common::Path("BOLT.ANM"), 120,
+ /*holdLastFrame=*/false, /*fadeIn=*/true);
+ waitForInput(1800);
+ fadeCurrentPaletteToBlack();
if (_audio)
_audio->stopVoice();
}
@@ -363,8 +394,12 @@ Common::Error EEMEngine::run() {
if (!shouldQuit() && !_skipIntro && _music)
_music->playFile(Common::Path("THEME.XMI"), /*loop=*/true);
for (int i = 1; i <= 20 && !shouldQuit() && !_skipIntro; i++) {
+ const bool fadeIn = (i == 1 || i == 5);
+ if (i == 5)
+ fadeCurrentPaletteToBlack();
Common::String name = Common::String::format("ANIM%02d.A", i);
- playAnm(Common::Path(name));
+ playAnm(Common::Path(name), 120,
+ /*holdLastFrame=*/false, fadeIn);
// `_SpoolSound(uVar3 - 1)` at 2520:08c2 â per-character
// VO after each anim except the last (`if (uVar3 != 0x14)`
// at 2520:08a8). Original blocks until done; we run async
@@ -383,7 +418,8 @@ Common::Error EEMEngine::run() {
if (!shouldQuit() && !_skipIntro && _music)
_music->playFile(Common::Path("THEME.XMI"), /*loop=*/true);
if (!shouldQuit() && !_skipIntro)
- playAnm(Common::Path("TITLE.ANM"), 120, /*holdLastFrame=*/true);
+ playAnm(Common::Path("TITLE.ANM"), 120,
+ /*holdLastFrame=*/true, /*fadeIn=*/true);
}
_skipIntro = false;
@@ -668,7 +704,8 @@ void EEMEngine::interruptAudio() {
_music->stop();
}
-void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs, bool holdLastFrame) {
+void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs,
+ bool holdLastFrame, bool fadeIn) {
ANMDecoder anm;
if (!anm.open(path)) {
warning("playAnm: %s missing", path.toString().c_str());
@@ -677,17 +714,32 @@ void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs, bool holdLa
byte palette[768];
anm.getPalette8(palette);
- g_system->getPaletteManager()->setPalette(palette, 0, 256);
const uint16 w = anm.width();
const uint16 h = anm.height();
+ {
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (screen) {
+ if (screen->w >= w && screen->h >= h)
+ anm.seedFrameBuffer((const byte *)screen->getBasePtr(0, 0), screen->pitch);
+ g_system->unlockScreen();
+ }
+ }
+ bool paletteApplied = false;
while (!shouldQuit()) {
const byte *frame = anm.nextFrame();
if (!frame)
break;
g_system->copyRectToScreen(frame, w, 0, 0, w, h);
+ if (!paletteApplied) {
+ if (fadeIn)
+ fadePaletteFromBlack(palette);
+ else
+ g_system->getPaletteManager()->setPalette(palette, 0, 256);
+ paletteApplied = true;
+ }
g_system->updateScreen();
// Drain events and let the user skip with click/key. The original
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index d6080d93fbd..6f463b4da69 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -380,9 +380,12 @@ private:
*
* If @p holdLastFrame is true the call blocks on the final frame
* until the user clicks or hits a key â used for the title screen.
+ * If @p fadeIn is true the first decoded frame is copied while the
+ * palette is black, then the animation palette is ramped in like
+ * `_OpenFadeIn`.
*/
void playAnm(const Common::Path &path, uint frameDelayMs = 120,
- bool holdLastFrame = false);
+ bool holdLastFrame = false, bool fadeIn = false);
/// Stop every active audio channel â voice, sound spool, and
/// MIDI. Mirrors the `_CleanMysterySounds @ 202f:05a5` +
Commit: 9653bf379238d61f7faab0be2d4a5eaa9ad72abe
https://github.com/scummvm/scummvm/commit/9653bf379238d61f7faab0be2d4a5eaa9ad72abe
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:20:59+02:00
Commit Message:
EEM: do not close the game after the end of the scrapbook
Changed paths:
engines/eem/eem.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 58dcfaa572b..35b76dab571 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -498,18 +498,20 @@ screen_loop:
// the same routing the `kScreenChooseMystery` case uses.
// Reachable from `_DisplayCorrect`'s 0xc write after a
// solve (see `ui.cpp` `_nextScreen = kScreenAction`).
+ _nextScreen = kScreenInvalid;
doCaseSelection();
- _nextScreen = _mystery.isLoaded() ? kScreenInitClues
- : kScreenInvalid;
+ if (_mystery.isLoaded())
+ _nextScreen = kScreenInitClues;
break;
case kScreenChooseMystery:
// Handler 10 at 1a35:0e0e calls `_DoChooseMystery` which
// presets `_NextScreen = 0` (INIT_CLUES) before
// `_CaseSelection`. Same dispatch as `kScreenAction`.
+ _nextScreen = kScreenInvalid;
doCaseSelection();
- _nextScreen = _mystery.isLoaded() ? kScreenInitClues
- : kScreenInvalid;
+ if (_mystery.isLoaded())
+ _nextScreen = kScreenInitClues;
break;
case kScreenInitClues:
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 0eec0d2fe5e..caaf8bcdeed 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -879,20 +879,28 @@ void EEMEngine::doShowScrapbook(uint stage) {
while (!shouldQuit() && mystery >= lo && mystery < hi) {
const int direction = doShowEnding((uint)mystery, firstPage);
if (direction < 0) {
- if (mystery == lo)
- break;
- mystery--;
+ int prevMystery = mystery - 1;
if (currentTier) {
- while (mystery >= lo && _mysteriesSolved[mystery] == 0)
- mystery--;
+ while (prevMystery >= lo && _mysteriesSolved[prevMystery] == 0)
+ prevMystery--;
+ }
+ if (prevMystery < lo) {
+ firstPage = true;
+ continue;
}
+ mystery = prevMystery;
firstPage = false;
} else if (direction > 0) {
- mystery++;
+ int nextMystery = mystery + 1;
if (currentTier) {
- while (mystery < hi && _mysteriesSolved[mystery] == 0)
- mystery++;
+ while (nextMystery < hi && _mysteriesSolved[nextMystery] == 0)
+ nextMystery++;
+ }
+ if (nextMystery >= hi) {
+ firstPage = false;
+ continue;
}
+ mystery = nextMystery;
firstPage = true;
} else {
break;
@@ -1550,6 +1558,7 @@ void EEMEngine::doCaseSelection() {
doShowScrapbook(stage);
setSitePalette(0);
_mystery.clear();
+ _nextScreen = kScreenChooseMystery;
return;
}
Commit: 5c5a2d1ccf54671d91256011c89f7151b89f328c
https://github.com/scummvm/scummvm/commit/5c5a2d1ccf54671d91256011c89f7151b89f328c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:00+02:00
Commit Message:
EEM: honor the music config
Changed paths:
engines/eem/clues.cpp
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/site.cpp
engines/eem/site.h
engines/eem/ui.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 82805c95635..708754fbd11 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -1065,7 +1065,8 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
// Per-record voice (byte 9 high bit) â see comment in original
// header.
- if ((rec[9] & 0x80) != 0 && _audio) {
+ const bool playedRecordVoice = (rec[9] & 0x80) != 0 && _audio;
+ if (playedRecordVoice) {
const uint slot = rec[9] & 0x7f;
_audio->playFloppyVoiceSlot(slot, _partner);
}
@@ -1264,8 +1265,14 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
firstPage = true;
}
}
- if (skipAll)
+ if (skipAll) {
+ if (playedRecordVoice)
+ _audio->stopVoice();
return;
+ }
+
+ if (playedRecordVoice)
+ _audio->stopVoice();
rec += 11 + textCount;
}
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 35b76dab571..4513d010beb 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -938,12 +938,33 @@ void EEMEngine::startTravelMusic() {
// itself runs without music. Our previous `loop=true` made the
// music never end, leaving travel music droning through site
// investigation, accuse, gallery, etc.
- if (!_music || !_mystery.isLoaded())
+ if (!_music || !_mystery.isLoaded() || !_voiceOn)
return;
const uint num = _mystery._siteNumber % 5;
_music->playMus(num, /*loop=*/false);
}
+void EEMEngine::waitForMusicDone(uint32 maxMs) {
+ if (!_music)
+ return;
+
+ const uint32 startMs = g_system->getMillis();
+ while (_music->isPlaying() && !shouldQuit() &&
+ g_system->getMillis() - startMs < maxMs) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ stopMusic();
+ return;
+ }
+ }
+ g_system->updateScreen();
+ g_system->delayMillis(20);
+ }
+ stopMusic();
+}
+
void EEMEngine::stopMusic() {
if (_music)
_music->stop();
@@ -1064,6 +1085,7 @@ Common::Error EEMEngine::loadGameStream(Common::SeekableReadStream *stream) {
s.syncAsUint16LE(mysteryNum);
if (!_mystery.load(mysteryNum, &_rng)) {
_mystery.clear();
+ resetSiteArrivalState();
return Common::kReadingFailed;
}
// `_ReadMystery @ 2404:008f` calls `_InitMysterySounds` at the
@@ -1076,8 +1098,13 @@ Common::Error EEMEngine::loadGameStream(Common::SeekableReadStream *stream) {
if (_audio && !isFloppy())
_audio->initMysterySounds(mysteryNum);
_mystery.syncState(s);
+ if (_mystery._siteNumber < _mystery.numSites())
+ setSiteArrivalState(_mystery._siteNumber);
+ else
+ resetSiteArrivalState();
} else {
_mystery.clear();
+ resetSiteArrivalState();
}
debugC(1, kDebugGeneral,
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 6f463b4da69..78f7a5557bb 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -495,11 +495,16 @@ private:
public:
/// Mirrors `_StartTravelMusic @ 20a2:0595`. Picks `MUS%05d.XMI`
- /// based on `_mystery._siteNumber % 5` and starts it (looping). The
+ /// based on `_mystery._siteNumber % 5` and starts it one-shot. The
/// site loop calls this each time `enter(siteNum)` runs so the
/// music changes as the player travels between sites.
void startTravelMusic();
+ /// Block until the current MIDI cue finishes, then stop/unload it.
+ /// Mirrors the `_IsMIDIPlaying` spin + `_StopMIDI` cleanup in
+ /// `_DoSiteLoop` after the CD travel cue.
+ void waitForMusicDone(uint32 maxMs = 60000);
+
/// Stop any currently playing MIDI track. Mirrors `_StopMIDI @
/// 20a2:0512` â used by `_DoSiteLoop @ 168d:06c0` after the
/// one-shot travel track plays out and by `_DisplayCorrect /
@@ -580,6 +585,11 @@ private:
bool _interactiveMouseCursor = false;
+ /// Site whose entrance animation has already played in the current
+ /// mystery. Kept on the engine, not SiteScreen, because PDA/gallery
+ /// screens destroy and recreate SiteScreen when returning to the site.
+ int _lastSiteArrivalAnim = -1;
+
/// XMIDI music player. Mirrors the original `MIDI.C` family
/// (`_MIDIPlayFile`, `_MIDIPlay`, `_StopMIDI`, `_StartTravelMusic`
/// at 20a2:00e2-05c9). Constructed lazily during `run()` once the
@@ -598,6 +608,18 @@ public:
/// member itself public. Mirrors the original's direct write to
/// `_NextScreen @ 2d5d:3f26` from anywhere in the engine.
void setNextScreen(ScreenId s) { _nextScreen = s; }
+ bool shouldPlaySiteArrival(uint siteNum) const {
+ return _lastSiteArrivalAnim != (int)siteNum;
+ }
+ void markSiteArrivalPlayed(uint siteNum) {
+ _lastSiteArrivalAnim = (int)siteNum;
+ }
+ void resetSiteArrivalState() {
+ _lastSiteArrivalAnim = -1;
+ }
+ void setSiteArrivalState(uint siteNum) {
+ _lastSiteArrivalAnim = (int)siteNum;
+ }
};
} // End of namespace EEM
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 1a71f83e417..eeb7d78bd3d 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -540,10 +540,13 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
debugC(1, kDebugSite, "Entering site %u (%u hotspots)",
siteNum, _mystery->hotspotCount(siteNum));
+ const bool playArrival = _vm->shouldPlaySiteArrival(siteNum);
+
// `_DoTravel @ 168d:02da` calls `_StartTravelMusic` after the
// destination is set. We do the same here so the music swaps as
// the player moves between sites.
- _vm->startTravelMusic();
+ if (playArrival)
+ _vm->startTravelMusic();
// Palette: original `_BuildBackground` calls `GetPalette(sitenum + 1)`
// where sitenum is the global SITES.DBD index (= the per-mystery
@@ -595,10 +598,10 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
renderBackground(siteNum);
// `_DoSiteLoop @ 168d:03f4` plays `_EnterSiteAnim` whenever
- // `_LastSite != _SiteNumber`. We track the last site we *played*
- // the arrival on so re-entries (after notebook/map/etc.) don't
- // repeat the animation.
- if ((int)siteNum != _lastSiteAnim) {
+ // `_LastSite != _SiteNumber`. Keep our guard on the engine rather
+ // than on SiteScreen, because PDA/gallery returns recreate
+ // SiteScreen and must not replay the arrival.
+ if (playArrival) {
// `_EnterSiteAnim` snapshots the current screen, so populate
// that temporary background with the same site layers that
// should already be visible behind the arriving partner.
@@ -608,21 +611,15 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
renderStaticDrops(siteNum);
renderAnimatedDrops(siteNum, g_system->getMillis());
enterSiteAnim();
- _lastSiteAnim = (int)siteNum;
+ _vm->markSiteArrivalPlayed(siteNum);
+ if (!_vm->isFloppy())
+ _vm->waitForMusicDone();
// Re-paint the BG; the normal snapshot below should contain
// only the static layers, while animated NPCs are redrawn per
// tick by the frame pump.
renderBackground(siteNum);
}
- // Stop the travel music explicitly. `_DoSiteLoop @ 168d:06c0`
- // waits for the one-shot travel track to play out and then calls
- // `_StopMIDI()` before the interactive site phase begins â
- // blocking the engine while it spins. We just stop now so the
- // site investigation runs without music (matches the original
- // silent-investigation phase).
- _vm->stopMusic();
-
// Static drops (Loop 2 from `_DoSiteLoop`) â no animation, baked
// into the BG snapshot the run() pump uses to restore. Floppy
// stores them in a different shape (drops sub-struct after the
@@ -742,12 +739,12 @@ void SiteScreen::run() {
while (!_vm->shouldQuit()) {
Common::Event event;
- bool exitRequested = false;
while (g_system->getEventManager()->pollEvent(event)) {
switch (event.type) {
case Common::EVENT_QUIT:
case Common::EVENT_RETURN_TO_LAUNCHER:
_vm->setHotspotMouseCursor(false);
+ _vm->stopMusic();
return;
case Common::EVENT_MOUSEMOVE:
@@ -759,9 +756,9 @@ void SiteScreen::run() {
// _FindButton(&SiteButtons, 2, MouseX, MouseY)
// where `SiteButtons` is two 8-byte rectangles at
// 29be:0x274 (verified via 168d:0729-0848):
- // Button 0: (35, 111) - (56, 136) â notebook
+ // Button 0: (35, 111) - (56, 136) -> notebook
// (`_NextScreen = 4`)
- // Button 1: (7, 177) - (57, 200) â map
+ // Button 1: (7, 177) - (57, 200) -> map
// (CD `_NextScreen = 1`,
// floppy = 2)
// Test the buttons before falling through to hotspots so
@@ -780,6 +777,7 @@ void SiteScreen::run() {
notePartnerActivity();
_vm->setHotspotMouseCursor(false);
_vm->setNextScreen(kScreenNotebook);
+ _vm->stopMusic();
return;
}
if (kSitePartnerFootMapRect.contains(event.mouse.x, event.mouse.y)) {
@@ -788,6 +786,7 @@ void SiteScreen::run() {
// CD writes `_NextScreen = 1`; floppy writes 2.
_vm->setNextScreen(_vm->isFloppy() ? kScreenMapAlt
: kScreenMap);
+ _vm->stopMusic();
return;
}
if (kSitePartnerHeadHintRect.contains(event.mouse.x, event.mouse.y)) {
@@ -818,15 +817,14 @@ void SiteScreen::run() {
// 6-entry table at `168d:09d5` (TAB / ENTER / arrow
// keys for hotspot cursor cycling) plus ESC handled
// separately at 168d:07a9. We don't implement the
- // hotspot cursor cycling â clicks are the primary
- // interaction â so the only keyboard binding kept
- // here is ESC (matches `_ESCHit` â "Are you sure?"
- // â MAP).
+ // hotspot cursor cycling, so the only keyboard binding kept
+ // here is ESC (matches `_ESCHit` -> "Are you sure?" -> MAP).
if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
_vm->setHotspotMouseCursor(false);
if (_vm->areYouSure()) {
_vm->setNextScreen(_vm->isFloppy() ? kScreenMapAlt
: kScreenMap);
+ _vm->stopMusic();
return;
}
enter(cur, false);
@@ -839,14 +837,14 @@ void SiteScreen::run() {
break;
}
}
- if (exitRequested)
- return;
// Hotspot side effects can invalidate the active mystery; exit
// immediately rather than tick another frame against stale BG
// snapshots / hotspot tables.
- if (!_mystery || !_mystery->isLoaded())
+ if (!_mystery || !_mystery->isLoaded()) {
+ _vm->stopMusic();
return;
+ }
// Per-tick frame pump (mirrors `_CheckFrameRate` +
// `_UpdateAnimations` at the top of `_DoSiteLoop`'s main loop).
diff --git a/engines/eem/site.h b/engines/eem/site.h
index 06bc8595369..d6f6557c381 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -176,7 +176,6 @@ private:
kPartnerWaitPatient,
kPartnerWaitImpatient
};
- int _lastSiteAnim = -1; ///< Last site we played the arrival on.
int _snapshotSite = -1; ///< Site number the snapshot belongs to.
Graphics::ManagedSurface _bgSnapshot;
uint32 _lastTickMs = 0; ///< Last frame-pump tick in ms.
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index caaf8bcdeed..fdc71dc6695 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -1540,8 +1540,11 @@ void EEMEngine::doCaseSelection() {
if (!_mystery.load(0, &_rng)) {
warning("doCaseSelection: failed to load practice mystery");
_mystery.clear();
- } else if (_audio && !isFloppy()) {
- _audio->initMysterySounds(0);
+ resetSiteArrivalState();
+ } else {
+ resetSiteArrivalState();
+ if (_audio && !isFloppy())
+ _audio->initMysterySounds(0);
}
return;
}
@@ -1762,6 +1765,7 @@ void EEMEngine::doCaseSelection() {
_mystery.clear();
return;
}
+ resetSiteArrivalState();
if (_audio && !isFloppy())
_audio->initMysterySounds(mn);
debugC(1, kDebugMystery, "Mystery %u loaded; %u sites, %u suspects",
@@ -4134,7 +4138,7 @@ void EEMEngine::doAccuse() {
// Step 1 â alibi music. Original blocks until MIDI 6 ends with
// the gallery still on screen. We poll `_music->isPlaying`;
// click/ESC aborts early.
- if (_music) {
+ if (_music && _voiceOn) {
_music->playMus(6, /*loop=*/false);
const uint32 musStart = g_system->getMillis();
bool aborted = false;
@@ -4142,8 +4146,10 @@ void EEMEngine::doAccuse() {
Common::Event ev;
while (g_system->getEventManager()->pollEvent(ev)) {
if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ _music->stop();
return;
+ }
if (ev.type == Common::EVENT_KEYDOWN ||
ev.type == Common::EVENT_LBUTTONDOWN) {
aborted = true;
@@ -4362,7 +4368,7 @@ void EEMEngine::doAccuse() {
}
g_system->updateScreen();
- if (_music)
+ if (_music && _voiceOn)
_music->playMus(5, /*loop=*/false);
// Chain-by-chain RECAP. Partner enumerates every required
@@ -4374,6 +4380,8 @@ void EEMEngine::doAccuse() {
const byte *solved = _mystery.solvedClueBlock();
if (solved)
displayClue(solved);
+ if (_music && _voiceOn)
+ _music->stop();
// `_DifferenceAnimation("scrapbk.ani")` (1df2:0848) â the
// physical scrapbook flip animation that introduces the
@@ -4741,6 +4749,7 @@ void EEMEngine::doAccuseFloppy() {
// every mystery in the current tier is solved).
// 1d40:0982 _SavePlayerRecord (= saveProfile)
// 1d40:0985 _DeleteMysteryFile (= mystery cleanup)
+ // 1d40:0991 MakeSolvedSound = 1; ShowOneScrap(mystery)
// 1d40:09b0 _NextScreen = 0xc.
const uint mn = _mystery.number();
@@ -4819,6 +4828,8 @@ void EEMEngine::doAccuseFloppy() {
const uint count = chain[0];
displayFloppyDialogRecords(chain + 1, count, 0);
}
+ if (_music && _voiceOn)
+ _music->stop();
// Mark mystery solved with first-try bonus tracking.
// `_DisplayCorrect_Floppy @ 1d40:0939`:
@@ -4859,6 +4870,27 @@ void EEMEngine::doAccuseFloppy() {
}
}
+ // `_DisplayCorrect_Floppy` sets `MakeSolvedSound` before the
+ // newly solved ending is shown. `FUN_1d40_05b7` reads byte 0 of
+ // `E<num>.BIN` and maps values 0..2 through the table at
+ // `2608:0c5e` to VOC slots 0x15, 0x16, 0x17.
+ if (_audio && _voiceOn) {
+ Common::File ending;
+ const Common::String fname =
+ Common::String::format("E%u.BIN", mn);
+ if (ending.open(Common::Path(fname)) && ending.size() > 0) {
+ static const uint8 kSolvedVoiceSlot[3] = {
+ 0x15, 0x16, 0x17
+ };
+ const byte type = ending.readByte();
+ if (type < ARRAYSIZE(kSolvedVoiceSlot)) {
+ _audio->playFloppyVoiceSlot(kSolvedVoiceSlot[type],
+ _partner);
+ _audio->waitForVoiceDone();
+ }
+ }
+ }
+
// Persist progress before clearing the in-progress mystery.
// `_DisplayCorrect_Floppy @ 1d40:0982` calls
// `_SavePlayerRecord` then `FUN_22dc_0dbd` (which deletes the
@@ -5024,6 +5056,8 @@ void EEMEngine::doAccuseFloppy() {
// Our `showFloppyKDHint(slot)` reads `kdIdx + slot * 2`, so slot
// 5 is the right argument here.
showFloppyKDHint(5);
+ if (_music && _voiceOn)
+ _music->stop();
_mystery._firstTry = false;
_nextScreen = _lastScreen != kScreenInvalid
Commit: a1b4c596bc9ce82061e417eb84ff6816f12f411f
https://github.com/scummvm/scummvm/commit/a1b4c596bc9ce82061e417eb84ff6816f12f411f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:00+02:00
Commit Message:
EEM: added missing voices when selecting clues
Changed paths:
engines/eem/mystery.cpp
engines/eem/mystery.h
engines/eem/ui.cpp
diff --git a/engines/eem/mystery.cpp b/engines/eem/mystery.cpp
index 3b6cb6b3c98..efbada6cab1 100644
--- a/engines/eem/mystery.cpp
+++ b/engines/eem/mystery.cpp
@@ -558,6 +558,43 @@ int Mystery::selectedPoints() const {
return total;
}
+int Mystery::foundPoints() const {
+ const byte *ni = noteIndex();
+ const uint16 cnt = noteIndexCount();
+ if (!ni || cnt == 0)
+ return 0;
+
+ uint16 scores[Mystery::kCluesFoundCap] = {};
+ uint scoreCount = 0;
+ const uint maxIdx = MIN<uint>(cnt, kCluesFoundCap);
+ for (uint i = 0; i < maxIdx; i++) {
+ if (_cluesFound[i] == 0)
+ continue;
+ const uint16 pts = _isFloppy ? ni[i * 7 + 6]
+ : READ_LE_UINT16(ni + i * 4 + 2);
+ scores[scoreCount++] = pts;
+ }
+
+ const uint topN = MIN<uint>(5u, scoreCount);
+ for (uint k = 0; k < topN; k++) {
+ uint best = k;
+ for (uint j = k + 1; j < scoreCount; j++) {
+ if (scores[j] > scores[best])
+ best = j;
+ }
+ if (best != k) {
+ const uint16 tmp = scores[k];
+ scores[k] = scores[best];
+ scores[best] = tmp;
+ }
+ }
+
+ int total = 0;
+ for (uint k = 0; k < topN; k++)
+ total += scores[k];
+ return total;
+}
+
void Mystery::syncState(Common::Serializer &s) {
s.syncBytes(_cluesFound, kCluesFoundCap);
s.syncBytes(_noteSelected, kCluesFoundCap);
diff --git a/engines/eem/mystery.h b/engines/eem/mystery.h
index 82c881803cd..c9694d1965e 100644
--- a/engines/eem/mystery.h
+++ b/engines/eem/mystery.h
@@ -159,6 +159,10 @@ public:
/// `_GetSelectedPoints` @ 1df2:00bd.
int selectedPoints() const;
+ /// Sum of the top five point values among found notebook entries.
+ /// Mirrors `_GetFoundPoints` @ 1df2:0098.
+ int foundPoints() const;
+
/// True when `selectedPoints() > 99`. Mirrors `_SolvedCheck`.
bool solvedCheck() const { return selectedPoints() > 99; }
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index fdc71dc6695..5e1b23a8c12 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -3625,6 +3625,83 @@ void EEMEngine::doAccuse() {
return;
}
+ // `_AccuseEntry @ 1df2:0ff8` runs before the red accuse-notes
+ // picker. It scores the top five FOUND clues, says how close the
+ // player is, and only lets `_DoAccuse` continue when that score is
+ // at least 0x65.
+ const byte *entryKdIdx = _mystery.kdTextIndex();
+ if (!entryKdIdx)
+ return;
+ const int foundPoints = _mystery.foundPoints();
+ uint entryKDSpeak = 0;
+ uint16 entryTextOff = 0xFFFF;
+ bool canAccuse = false;
+ Common::String entryText;
+ if (foundPoints == 0) {
+ entryKDSpeak = 9;
+ entryText = "3We're not ready to solve this mystery yet. "
+ "Let's keep investigating until we have some more solid "
+ "evidence to make our case!";
+ } else if (foundPoints < 0x32) {
+ entryKDSpeak = 0;
+ entryTextOff = READ_LE_UINT16(entryKdIdx + 0);
+ } else if (foundPoints < 0x65) {
+ entryKDSpeak = 1;
+ entryTextOff = READ_LE_UINT16(entryKdIdx + 2);
+ } else {
+ entryKDSpeak = 2;
+ entryTextOff = READ_LE_UINT16(entryKdIdx + 4);
+ canAccuse = true;
+ }
+ if (entryText.empty() && entryTextOff != 0xFFFF) {
+ entryText = parseString(_mystery.textAt(entryTextOff),
+ _playerName, _partner);
+ }
+ if (!entryText.empty()) {
+ Graphics::ManagedSurface ms(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ ms.clear();
+ Graphics::Surface *cur = g_system->lockScreen();
+ if (cur) {
+ ms.simpleBlitFrom(*cur);
+ g_system->unlockScreen();
+ }
+ const byte firstChar = (byte)entryText[0];
+ const uint16 bubNum = getKDTextBalloon(firstChar);
+ if (firstChar >= '0' && firstChar <= '9')
+ entryText.deleteChar(0);
+ Picture balloon;
+ const bool haveBalloon =
+ _balloonArchive.size() > (bubNum & 0x7F) &&
+ _balloonArchive.loadEntry(bubNum & 0x7F, balloon);
+ const int balloonX = 0x21;
+ int balloonY = 1;
+ if (haveBalloon && balloon.surface.h < 0x4e)
+ balloonY = (0x50 - balloon.surface.h) / 2;
+ if (haveBalloon) {
+ const byte transp = (byte)(balloon.flags >> 8);
+ ms.transBlitFrom(balloon.surface,
+ Common::Point(balloonX, balloonY),
+ (uint32)transp);
+ }
+ uint16 tx = 5, ty = 4, tw = 155;
+ getBalloonInsets(bubNum, tx, ty, tw);
+ if (_font.isLoaded()) {
+ _font.drawWordWrapped(&ms, balloonX + tx, balloonY + ty,
+ tw, entryText, haveBalloon ? 0 : 0xF);
+ }
+ g_system->copyRectToScreen(ms.getPixels(), ms.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ if (_audio)
+ _audio->sayKDDigital(entryKdIdx, entryKDSpeak, _partner);
+ waitForInput(60000);
+ }
+ if (!canAccuse) {
+ _nextScreen = _lastScreen != kScreenInvalid
+ ? (ScreenId)_lastScreen : kScreenSite;
+ return;
+ }
// Mirrors `_DoAccuse @ 1df2:0bdd` + `_DoAccuseGallery @ 1df2:0a31`:
// 1. ACCUSE-NOTES SCREEN (PIC 0x1A7, the red "accuse-mode" BG):
@@ -3780,6 +3857,7 @@ void EEMEngine::doAccuse() {
// AddPicBackground(pic, 0x21, y) (1df2:0aab)
// WordWrap(0x21+tbl[bub].x, y+tbl[bub].y, tbl[bub].w, text, color=0)
// tbl @ 29be:0875, 10-byte entries (1df2:0ad6-0af1)
+ // _SayKDDigital(4); _Wait(); (1df2:0b09-0b11)
const byte *kdIdx = _mystery.kdTextIndex();
if (kdIdx) {
const int16 textOff = (int16)READ_LE_UINT16(kdIdx + 8);
@@ -3878,6 +3956,8 @@ void EEMEngine::doAccuse() {
g_system->copyRectToScreen(ms.getPixels(), ms.pitch,
0, 0, 320, 200);
g_system->updateScreen();
+ if (_audio)
+ _audio->sayKDDigital(kdIdx, 4, _partner);
waitForInput(8000);
}
}
@@ -3978,13 +4058,13 @@ void EEMEngine::doAccuse() {
return;
// Real chain evaluation. Mirrors the original two-gate accusation:
- // 1. `_AccuseEntry @ 1df2:0ff8` checks `_GetFoundPoints() >= 100`
- // â gates whether the suspect picker is even reachable. We
- // `_SolvedCheck â selectedPoints > 99` is now gated at the
- // TOP of `doAccuse` â by the time we reach this point we
- // already know `solvedCheck()` was true (the picker wouldn't
- // have opened otherwise).
- // 2. `_WITCH @ 1df2:089f` checks `GalleryData[picked*0x46+0x02] ==
+ // 1. `_AccuseEntry @ 1df2:0ff8` checked top-five found points
+ // before the note picker, so by this point the player has
+ // enough evidence to attempt an accusation.
+ // 2. `_SolvedCheck @ 1df2:00ec` checked selectedPoints > 99
+ // before opening this suspect picker, so the clue set is
+ // valid if we got here.
+ // 3. `_WITCH @ 1df2:089f` checks `GalleryData[picked*0x46+0x02] ==
// 0xFFFF`. Innocent suspects store an alibi-text TextBlock
// offset there; the guilty one uses the sentinel.
const int points = _mystery.selectedPoints();
Commit: 631819628568d080bcc5a8548a45b444f92c6b2d
https://github.com/scummvm/scummvm/commit/631819628568d080bcc5a8548a45b444f92c6b2d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:00+02:00
Commit Message:
EEM: don't allow to select more than 5 clues in accusation mode
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 5e1b23a8c12..2ff97c65045 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -3256,12 +3256,13 @@ bool EEMEngine::doAccuseNotes() {
// for selected (1df2:0c2c sets it on entry).
// * Click on a clue toggles its selection
// (`_SearchNoteAreas` + `_SwapColors`).
- // * Click `_NoteButtons[2]` (rect at `(157, 174, 178, 190)`,
- // the original's "go to gallery" button) jumps to the
- // evidence check; `_HandleAccuseNoteButton(2)` returns 2
- // and the outer loop forces `uStack_8 = uStack_a` to
- // trigger `_SolvedCheck`.
- // * ESC sets `_NextScreen = 3` and exits.
+ // * Click `_NoteButtons[4]` (rect at `(180, 174, 201, 190)`,
+ // the original's solve button) jumps to the evidence check;
+ // `_HandleAccuseNoteButton(4)` returns 2 and the outer loop
+ // forces `uStack_8 = uStack_a` to trigger `_SolvedCheck`.
+ // * CD and floppy originals only cancel this screen through ESC.
+ // ScummVM also wires the visible PDA navigation buttons so the
+ // accusation can be abandoned without a keyboard.
if (!_mystery.isLoaded() || !_font.isLoaded())
return false;
const byte *ni = _mystery.noteIndex();
@@ -3300,8 +3301,10 @@ bool EEMEngine::doAccuseNotes() {
const int rectH = 159 - 27;
// `_NoteButtons` rects (verified at `29be:0147`). `_DoAccuse`
- // re-uses the same table as `_DoNotebook`, but only SOLVE /
- // PAGE NEXT / PAGE PREV do anything; others sit inert.
+ // re-uses the same table as `_DoNotebook`, but the original
+ // handler only routes SOLVE / PAGE NEXT / PAGE PREV; ScummVM
+ // additionally routes the visible site/map/notebook/gallery
+ // buttons below for pointer-only cancellation.
// `_HandleAccuseNoteButton @ 1df2:0990` returns `DI` (initialised
// to 0) and only sets `DI = 2` in the `i == 4` branch (asm:
// `1df2:09b2: MOV DI, 0x2`). The outer loop's `iVar6 == 2` test
@@ -3471,17 +3474,31 @@ bool EEMEngine::doAccuseNotes() {
rebuildPagination();
draw();
+ Common::Point mouse = g_system->getEventManager()->getMousePos();
+ setInteractiveMouseCursor(notebookButtonAt(mouse.x, mouse.y) ||
+ kPdaNotebookRect.contains(mouse.x, mouse.y) ||
+ rectListContains(slotRects, mouse.x, mouse.y));
while (!shouldQuit()) {
Common::Event ev;
bool dirty = false;
while (g_system->getEventManager()->pollEvent(ev)) {
if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ _nextScreen = kScreenInvalid;
return false;
+ }
+ if (ev.type == Common::EVENT_MOUSEMOVE) {
+ setInteractiveMouseCursor(
+ notebookButtonAt(ev.mouse.x, ev.mouse.y) ||
+ kPdaNotebookRect.contains(ev.mouse.x, ev.mouse.y) ||
+ rectListContains(slotRects, ev.mouse.x, ev.mouse.y));
+ }
if (ev.type == Common::EVENT_KEYDOWN) {
- if (ev.kbd.keycode == Common::KEYCODE_ESCAPE)
+ if (ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ _nextScreen = kScreenSite;
return false;
+ }
if (ev.kbd.keycode == Common::KEYCODE_LEFT &&
page > 0) {
page--;
@@ -3495,6 +3512,29 @@ bool EEMEngine::doAccuseNotes() {
if (ev.type == Common::EVENT_LBUTTONDOWN) {
const int mx = ev.mouse.x;
const int my = ev.mouse.y;
+ if (kPdaSiteRect.contains(mx, my)) {
+ _nextScreen = kScreenSite;
+ return false;
+ }
+ if (kPdaPartnerFootMapRect.contains(mx, my)) {
+ _nextScreen = kScreenMapAlt;
+ return false;
+ }
+ if (kPdaNotebookRect.contains(mx, my)) {
+ _nextScreen = kScreenNotebook;
+ return false;
+ }
+ if (kPdaGalleryRect.contains(mx, my)) {
+ _nextScreen = kScreenGallery;
+ return false;
+ }
+ if (kPdaHelpRect.contains(mx, my) ||
+ kPdaHelp2Rect.contains(mx, my)) {
+ setInteractiveMouseCursor(false);
+ doInterfaceHelp(0);
+ dirty = true;
+ continue;
+ }
// Page navigation â `_NoteButtons[5]` / `[6]`,
// dispatched in `_HandleAccuseNoteButton @
// 1df2:0990`. Only effective if there's another
@@ -3548,42 +3588,18 @@ bool EEMEngine::doAccuseNotes() {
for (uint i = 0; i < slotRects.size(); i++) {
if (slotRects[i].contains(mx, my)) {
const uint clueId = slotClues[i];
- _mystery._noteSelected[clueId] ^= 1;
- dirty = true;
-
- // Debug: dump current user-selected score so
- // the post-selection 100-point gate behaviour
- // is visible while picking. Mirrors the
- // floppy `FUN_1d40_0c48` (sum of `note[+6]`
- // across `_NoteSelected != 0`) and the CD
- // `selectedPoints()` (sum of `note[+2]` u16
- // across `_NoteSelected != 0`).
- {
- int total = 0;
- uint selectedCount = 0;
- const uint maxIdx = MIN<uint>(niCount,
- Mystery::kCluesFoundCap);
- const bool floppy = isFloppy();
- for (uint j = 0; j < maxIdx; j++) {
- if (!_mystery._noteSelected[j])
- continue;
- selectedCount++;
- if (floppy) {
- total += (int)ni[j * 7 + 6];
- } else {
- const int16 pts =
- (int16)READ_LE_UINT16(
- ni + j * 4 + 2);
- total += (int)pts;
- }
+ if (!_mystery._noteSelected[clueId]) {
+ uint selected = 0;
+ for (uint j = 0; j < found.size(); j++) {
+ if (_mystery._noteSelected[found[j]])
+ selected++;
}
- warning("EEM accuse: clue %u %s "
- "(selected=%u points=%d)",
- clueId,
- _mystery._noteSelected[clueId]
- ? "ON" : "OFF",
- selectedCount, total);
+ if (selected >= expected)
+ break;
}
+ _mystery._noteSelected[clueId] =
+ _mystery._noteSelected[clueId] ? 0 : 1;
+ dirty = true;
break;
}
}
@@ -3730,12 +3746,14 @@ void EEMEngine::doAccuse() {
// ACCUSE-NOTES SCREEN â let the player commit which N clues they
// believe solve the case. Mirrors the click-driven selection of
- // `_DoAccuse @ 1df2:0bdd`'s outer loop. ESC / cancel returns to
- // the site (matches `_DoAccuse @ 1df2:0c11` writing
- // `_NextScreen = 3` on ESC).
+ // `_DoAccuse @ 1df2:0bdd`'s outer loop. ESC returns to the site
+ // (matches `_DoAccuse @ 1df2:0c11` writing `_NextScreen = 3`);
+ // pointer navigation can also leave for the PDA, gallery, or map.
if (!doAccuseNotes()) {
- _nextScreen = _lastScreen != kScreenInvalid
- ? (ScreenId)_lastScreen : kScreenSite;
+ if (_nextScreen == kScreenAccuse) {
+ _nextScreen = _lastScreen != kScreenInvalid
+ ? (ScreenId)_lastScreen : kScreenSite;
+ }
return;
}
@@ -4630,10 +4648,12 @@ void EEMEngine::doAccuseFloppy() {
// matching the CD's `_DoAccuse @ 1df2:0bdd` expected count.
// `doAccuseNotes()` already handles this UI for both variants
// (note text reading is variant-aware via `noteTextOff`); it
- // returns true on commit, false on ESC.
+ // returns true on commit, false on ESC / pointer navigation.
if (!doAccuseNotes()) {
- _nextScreen = _lastScreen != kScreenInvalid
- ? (ScreenId)_lastScreen : kScreenSite;
+ if (_nextScreen == kScreenAccuse) {
+ _nextScreen = _lastScreen != kScreenInvalid
+ ? (ScreenId)_lastScreen : kScreenSite;
+ }
return;
}
Commit: a0d9c693402c09a30564a36fc98ad223c16591b7
https://github.com/scummvm/scummvm/commit/a0d9c693402c09a30564a36fc98ad223c16591b7
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:01+02:00
Commit Message:
EEM: fixed regression on music handling during enter animation
Changed paths:
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/site.cpp
engines/eem/site.h
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 4513d010beb..f7523be122b 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -958,6 +958,11 @@ void EEMEngine::waitForMusicDone(uint32 maxMs) {
stopMusic();
return;
}
+ if (ev.type == Common::EVENT_KEYDOWN ||
+ ev.type == Common::EVENT_LBUTTONDOWN) {
+ stopMusic();
+ return;
+ }
}
g_system->updateScreen();
g_system->delayMillis(20);
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 78f7a5557bb..ae1a6de44d8 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -500,9 +500,9 @@ public:
/// music changes as the player travels between sites.
void startTravelMusic();
- /// Block until the current MIDI cue finishes, then stop/unload it.
- /// Mirrors the `_IsMIDIPlaying` spin + `_StopMIDI` cleanup in
- /// `_DoSiteLoop` after the CD travel cue.
+ /// Block until the current MIDI cue finishes or the player skips it,
+ /// then stop/unload it. Mirrors the `_IsMIDIPlaying` spin +
+ /// `_StopMIDI` cleanup in `_DoSiteLoop` after the CD travel cue.
void waitForMusicDone(uint32 maxMs = 60000);
/// Stop any currently playing MIDI track. Mirrors `_StopMIDI @
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index eeb7d78bd3d..a5051688000 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -610,10 +610,14 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
else
renderStaticDrops(siteNum);
renderAnimatedDrops(siteNum, g_system->getMillis());
- enterSiteAnim();
+ const bool skippedArrival = enterSiteAnim();
_vm->markSiteArrivalPlayed(siteNum);
- if (!_vm->isFloppy())
- _vm->waitForMusicDone();
+ if (!_vm->isFloppy()) {
+ if (skippedArrival)
+ _vm->stopMusic();
+ else
+ _vm->waitForMusicDone();
+ }
// Re-paint the BG; the normal snapshot below should contain
// only the static layers, while animated NPCs are redrawn per
// tick by the frame pump.
@@ -873,7 +877,7 @@ void SiteScreen::run() {
}
}
-void SiteScreen::enterSiteAnim() {
+bool SiteScreen::enterSiteAnim() {
// Mirrors `_EnterSiteAnim @ 1000:9b21`. Two phases, both partner
// dependent:
// Phase 1 â skateboard scroll: anim 6 (Jake) / 0xe (Jenny). Sprite
@@ -886,7 +890,7 @@ void SiteScreen::enterSiteAnim() {
// motion (a runtime-calibrated speed value); we use a fixed 4 px
// per tick which feels close to the DOS pacing.
if (!_vm || !_mystery)
- return;
+ return false;
const uint8 partner = _vm->getPartnerIndex();
const uint kSkateAni = (partner == 0) ? 6 : 0xe;
const uint kKDAni = (partner == 0) ? 7 : 0xf;
@@ -895,7 +899,7 @@ void SiteScreen::enterSiteAnim() {
// Snapshot the current screen so we can restore between frames.
Graphics::Surface *screen = g_system->lockScreen();
if (!screen)
- return;
+ return false;
Graphics::ManagedSurface bg(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
bg.simpleBlitFrom(*screen);
@@ -929,7 +933,7 @@ void SiteScreen::enterSiteAnim() {
while (g_system->getEventManager()->pollEvent(ev)) {
if (ev.type == Common::EVENT_KEYDOWN ||
ev.type == Common::EVENT_LBUTTONDOWN) {
- return; // user-skip â bail out of the animation
+ return true; // user-skip
}
}
@@ -979,12 +983,13 @@ void SiteScreen::enterSiteAnim() {
while (g_system->getEventManager()->pollEvent(ev)) {
if (ev.type == Common::EVENT_KEYDOWN ||
ev.type == Common::EVENT_LBUTTONDOWN) {
- return;
+ return true;
}
}
g_system->delayMillis(80);
}
}
+ return false;
}
void SiteScreen::renderStaticDrops(uint siteNum) {
diff --git a/engines/eem/site.h b/engines/eem/site.h
index d6f6557c381..07b34874d07 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -119,7 +119,8 @@ private:
/// _SiteNumber`. Mirrors `_EnterSiteAnim @ 1000:9b21` â animation
/// 6 (Jake) / 14 (Jenny) skateboards in from the right edge along
/// the bottom, then animation 7 / 15 slides KD in from the left.
- void enterSiteAnim();
+ /// Returns true when the player skipped it with input.
+ bool enterSiteAnim();
/// Draw the persistent in-site partner sprite (Jake or Jenny
/// standing/idling) at the position from `_WaitAnims` @ 29be:021c.
Commit: 33e2ff6fdad0c6c961f09bd403367bd04b75be34
https://github.com/scummvm/scummvm/commit/33e2ff6fdad0c6c961f09bd403367bd04b75be34
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:01+02:00
Commit Message:
EEM: implemented badge handling when a case is over
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 2ff97c65045..30dfb17f7f6 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -57,6 +57,9 @@ const GallerySlot kGallerySlots[5] = {
constexpr Common::Rect kEndingPrevPageRect(Common::Point(0, 0), 28, 200);
constexpr Common::Rect kEndingNextPageRect(Common::Point(292, 0), 28, 200);
+constexpr uint16 kFloppyEndingBackgroundPic = 0x8b;
+constexpr uint16 kFirstTryBadgePic = 0x205;
+constexpr Common::Point kFirstTryBadgePos(0x1e, 9);
constexpr Common::Rect kPdaHelpRect(Common::Point(93, 174), 22, 16);
constexpr Common::Rect kPdaNotebookRect(Common::Point(134, 174), 21, 16);
@@ -633,12 +636,15 @@ void EEMEngine::doNewPlayer() {
int EEMEngine::doShowEnding(uint num, bool firstPage) {
// Mirrors `_DisplayEnding @ 1df2:0548` + `_DisplayEndingPage @
- // 1df2:044c`. File format (verified by reading E0.BIN's bytes):
+ // 1df2:044c` on CD and `FUN_1d40_05b7` + `FUN_1d40_031e` on
+ // floppy. CD ending file format:
// u16 pageCount
// for each page:
// u16 picNum
// u16 x1, y1, x2, y2 (story rect â passed to WordWrap)
// char text[] (null-terminated, ParseString opcodes)
+ // Floppy files prepend a small title header and use a shared
+ // newspaper background (PIC 0x8b) plus per-page overlay pictures.
//
// The original swaps the font: `_FreeFont(); _LoadFont("tiny.fnt")`
// at 1df2:055f-1df2:0563, calls `_GetPalette(0)` (site palette 0),
@@ -679,10 +685,6 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
return 0;
}
- const uint16 pageCount = READ_LE_UINT16(buf.data());
- if (pageCount == 0)
- return 0;
-
// Mirrors 1df2:0558-1df2:056a â `_FreeFont(); _LoadFont(tiny.fnt)`.
// The newspaper layout uses TINY.FNT (smaller glyphs) so the body
// copy fits in the columns. `_LoadFont(font.fnt)` is restored at
@@ -699,26 +701,69 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
setSitePalette(0);
CursorMan.showMouse(true);
- // Walk page records. Each page header is 10 bytes; text is
- // null-terminated and follows the header.
+ const bool floppyEnding = isFloppy();
uint pageOffsets[8]; // ENDING_RANGE_MAX from `_DisplayEnding`
- const uint maxPages = MIN<uint>(pageCount,
- (uint)(sizeof(pageOffsets) / sizeof(uint)));
- uint cursor = 2;
+ const uint pageOffsetCap =
+ (uint)(sizeof(pageOffsets) / sizeof(pageOffsets[0]));
uint validPages = 0;
- for (uint p = 0; p < maxPages; p++) {
- if (cursor + 10 >= size)
- break;
- pageOffsets[validPages++] = cursor;
- // Skip the 10-byte header and find the null terminator.
- cursor += 10;
- while (cursor < size && buf[cursor] != 0)
- cursor++;
- cursor++; // past the null
+
+ if (floppyEnding) {
+ // Floppy `E<num>.BIN` starts with:
+ // u8 type, 3 bytes of title metadata, char title[], u8 pageCount
+ // followed by pages:
+ // u8 overlayCount, N * { u16 picNum, u16 x, u8 y },
+ // u16 x1, y1, x2, y2, char text[].
+ uint titleEnd = 4;
+ while (titleEnd < size && buf[titleEnd] != 0)
+ titleEnd++;
+ if (titleEnd + 2 >= size)
+ return 0;
+ const uint pageCount = buf[titleEnd + 1];
+ uint cursor = titleEnd + 2;
+ const uint maxPages = MIN<uint>(pageCount, pageOffsetCap);
+ for (uint p = 0; p < maxPages; p++) {
+ if (cursor >= size)
+ break;
+ const uint pageStart = cursor;
+ const uint overlayCount = buf[cursor++];
+ const uint overlaysSize = overlayCount * 5;
+ if (cursor + overlaysSize + 8 >= size)
+ break;
+ cursor += overlaysSize + 8;
+ while (cursor < size && buf[cursor] != 0)
+ cursor++;
+ if (cursor >= size)
+ break;
+ pageOffsets[validPages++] = pageStart;
+ cursor++; // past the null
+ }
+ } else {
+ const uint16 pageCount = READ_LE_UINT16(buf.data());
+ if (pageCount == 0)
+ return 0;
+ const uint maxPages = MIN<uint>(pageCount, pageOffsetCap);
+ uint cursor = 2;
+ for (uint p = 0; p < maxPages; p++) {
+ if (cursor + 10 >= size)
+ break;
+ pageOffsets[validPages++] = cursor;
+ // Skip the 10-byte header and find the null terminator.
+ cursor += 10;
+ while (cursor < size && buf[cursor] != 0)
+ cursor++;
+ cursor++; // past the null
+ }
}
if (validPages == 0)
return 0;
+ const bool showFirstTryBadge =
+ num < sizeof(_mysteriesSolved) && _mysteriesSolved[num] == 2;
+ Picture firstTryBadge;
+ const bool haveFirstTryBadge =
+ showFirstTryBadge &&
+ _picsArchive.getPicture(kFirstTryBadgePic, firstTryBadge);
+
uint pageIdx = firstPage ? 0 : (validPages - 1);
int direction = 0; // -1 / 0 / +1, see header doc.
bool exitLoop = false;
@@ -731,25 +776,68 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
while (!shouldQuit() && !exitLoop) {
if (dirty) {
const uint off = pageOffsets[pageIdx];
- if (off + 10 >= size)
- break;
- const uint16 picNum = READ_LE_UINT16(buf.data() + off);
- const uint16 x1 = READ_LE_UINT16(buf.data() + off + 2);
- const uint16 y1 = READ_LE_UINT16(buf.data() + off + 4);
- const uint16 x2 = READ_LE_UINT16(buf.data() + off + 6);
- (void)READ_LE_UINT16(buf.data() + off + 8); // y2 (unused â WordWrap2 takes width only)
-
- Picture bg;
+ uint16 x1 = 0;
+ uint16 y1 = 0;
+ uint16 x2 = 0;
+ const char *raw = nullptr;
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
- if (_picsArchive.getPicture(picNum, bg))
- scratch.simpleBlitFrom(bg.surface);
+
+ if (floppyEnding) {
+ Picture bg;
+ if (_picsArchive.getPicture(kFloppyEndingBackgroundPic, bg))
+ scratch.simpleBlitFrom(bg.surface);
+
+ uint cursor = off;
+ if (cursor >= size)
+ break;
+ const uint overlayCount = buf[cursor++];
+ for (uint i = 0; i < overlayCount; i++) {
+ if (cursor + 5 > size)
+ break;
+ const uint16 picNum = READ_LE_UINT16(buf.data() + cursor);
+ const uint16 px = READ_LE_UINT16(buf.data() + cursor + 2);
+ const byte py = buf[cursor + 4];
+ Picture overlay;
+ if (_picsArchive.getPicture(picNum, overlay)) {
+ const byte transp = (byte)(overlay.flags >> 8);
+ scratch.transBlitFrom(overlay.surface,
+ Common::Point(px, py), transp);
+ }
+ cursor += 5;
+ }
+ if (cursor + 8 >= size)
+ break;
+ x1 = READ_LE_UINT16(buf.data() + cursor);
+ y1 = READ_LE_UINT16(buf.data() + cursor + 2);
+ x2 = READ_LE_UINT16(buf.data() + cursor + 4);
+ (void)READ_LE_UINT16(buf.data() + cursor + 6);
+ raw = (const char *)buf.data() + cursor + 8;
+ } else {
+ if (off + 10 >= size)
+ break;
+ const uint16 picNum = READ_LE_UINT16(buf.data() + off);
+ x1 = READ_LE_UINT16(buf.data() + off + 2);
+ y1 = READ_LE_UINT16(buf.data() + off + 4);
+ x2 = READ_LE_UINT16(buf.data() + off + 6);
+ (void)READ_LE_UINT16(buf.data() + off + 8); // y2 (unused â WordWrap2 takes width only)
+
+ Picture bg;
+ if (_picsArchive.getPicture(picNum, bg))
+ scratch.simpleBlitFrom(bg.surface);
+ raw = (const char *)buf.data() + off + 10;
+ }
+
+ if (pageIdx == 0 && haveFirstTryBadge) {
+ const byte transp = (byte)(firstTryBadge.flags >> 8);
+ scratch.transBlitFrom(firstTryBadge.surface,
+ kFirstTryBadgePos, transp);
+ }
// Story text. The bytes are a null-terminated string with
// `_ParseString` placeholders (0x80 = player name, 0x82
// = partner first name, etc.).
- const char *raw = (const char *)buf.data() + off + 10;
const Common::String text = parseString(raw, _playerName, _partner);
// Use TINY.FNT (`_LoadFont(@29be:10a5)` at 1df2:055f-0563)
@@ -4991,6 +5079,13 @@ void EEMEngine::doAccuseFloppy() {
}
}
+ // `_DisplayCorrect_Floppy @ 1d40:0991` calls
+ // `FUN_1ee2_06ac(mystery)`, whose body is just
+ // `FUN_1d40_05b7(mystery, 1)` plus font cleanup. That is the
+ // floppy ending/scrapbook viewer, and it receives the
+ // first-try badge state from the solved table entry above.
+ doShowEnding(mn);
+
// Persist progress before clearing the in-progress mystery.
// `_DisplayCorrect_Floppy @ 1d40:0982` calls
// `_SavePlayerRecord` then `FUN_22dc_0dbd` (which deletes the
Commit: 1423b3442bf1f4ac52bd25aa65dc419ac14ea055
https://github.com/scummvm/scummvm/commit/1423b3442bf1f4ac52bd25aa65dc419ac14ea055
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:01+02:00
Commit Message:
EEM: shiny map markers
Changed paths:
engines/eem/site.h
engines/eem/ui.cpp
diff --git a/engines/eem/site.h b/engines/eem/site.h
index 07b34874d07..110c560df05 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -76,6 +76,11 @@ uint bigMapDetailPartnerFrameAtTick(uint numFrames, uint32 elapsedMs);
void blitAnimFrameAnchored(Graphics::Surface *screen, const Picture &p,
int anchorX, int anchorY);
+/// Rotate one VGA palette range by one slot. Mirrors `_ColorCycle`
+/// and is used by site color cycles, hotspot marching ants, and the
+/// BigMap marker shine.
+void cyclePaletteRange(uint8 start, uint8 end);
+
/// One hotspot (search rectangle) within a site, 14 bytes on disk.
struct Hotspot {
int16 x1, y1, x2, y2; ///< rectangle in screen coordinates
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 30dfb17f7f6..a5612b345a0 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -2885,6 +2885,8 @@ void EEMEngine::doBigMap() {
if (now - mapLastTick >= 100) {
mapLastTick = now;
drawBigMapOverview(now - mapStartTick);
+ cyclePaletteRange(0xf7, 0xfa);
+ cyclePaletteRange(0xfb, 0xfe);
}
g_system->updateScreen();
g_system->delayMillis(10);
Commit: 9e532d8f9691ea295ab990fcaa42adb912630cea
https://github.com/scummvm/scummvm/commit/9e532d8f9691ea295ab990fcaa42adb912630cea
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:02+02:00
Commit Message:
EEM: allow to come back from zoomed map to the overview
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index a5612b345a0..7ed2cac176e 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -2806,286 +2806,313 @@ void EEMEngine::doBigMap() {
CursorMan.showMouse(true);
- // `_GetPalette(0x24)` per `_DoBigMap @ 20fe:09e7`.
- setSitePalette(0x24);
-
- const Common::Rect kSetupRect(0xc7, 0x12, 0xc7 + 0x32, 0x12 + 0xa); // approx; original from globals
- (void)kSetupRect; // not yet wired into our overlay
-
- // ------------------------------------------------------------------
- // STAGE 1 â Overview: PIC 0x42 + clickable site icons.
- // ------------------------------------------------------------------
-
- // Anchor for the partner-sprite timeline. `_DoBigMap`'s
- // `_NewAnimation` call seeds the slot's frame index to 0xffff so
- // the first `_UpdateAnimations` tick starts at script[0]; we mirror
- // that by passing elapsed-since-open (zero on the first paint) into
- // `bigMapPartnerFrameAtTick`, which plays the unfold once and then
- // loops the wait sequence.
- const uint32 mapStartTick = g_system->getMillis();
- drawBigMapOverview(0);
- uint32 mapLastTick = mapStartTick;
-
- // Static rectangles read directly from the binary at the labelled
- // addresses (CD `29be:0x1596` / floppy `2608:0x13fe..0x143e`).
- // Format is {x1, y1, x2, y2}. The floppy click table at
- // `2608:1436` (verified at `_BigMapInteractionLoop_Floppy @
- // 1fed:0a3a`) puts the setup button at (251, 3, 315, 42) â 1 px
- // up and 1 px left of the CD's (252, 4, 315, 42). The floppy
- // PIC 0x42 BG paints the visible button border at the same
- // pixels, so use the variant-specific rect to match the
- // hit-test region the original uses for that variant.
- const Common::Rect kBigMapWindow ( 0, 0, 247, 192); // 29be:1596
- const Common::Rect kSetupBtnRect = isFloppy()
- ? Common::Rect(251, 3, 315, 42) // 2608:1436
- : Common::Rect(252, 4, 315, 42); // 29be:15ce
-
- bool wantZoom = false;
- int zoomX = 0;
- int zoomY = 0;
while (!shouldQuit()) {
- Common::Event ev;
- while (g_system->getEventManager()->pollEvent(ev)) {
- if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
- return;
- if (ev.type == Common::EVENT_KEYDOWN &&
- ev.kbd.keycode == Common::KEYCODE_ESCAPE)
- return;
- if (ev.type == Common::EVENT_LBUTTONDOWN) {
- // SetupButtonRect â `_NextScreen = 6` (the original's
- // settings screen, mirrors `_DoBigMap @ 20fe:0c33`
- // where it pushes `_PressButton` then writes
- // `_NextScreen = 6`). Now wired to the actual
- // `doSetup` handler instead of dropping the player
- // out to the launcher.
- if (kSetupBtnRect.contains(ev.mouse.x, ev.mouse.y)) {
- _nextScreen = kScreenSetup;
+ setInteractiveMouseCursor(false);
+ setSitePalette(0x24); // `_GetPalette(0x24)` per `_DoBigMap @ 20fe:09e7`.
+
+ // ------------------------------------------------------------------
+ // STAGE 1 â Overview: PIC 0x42 + clickable site icons.
+ // ------------------------------------------------------------------
+
+ // Anchor for the partner-sprite timeline. `_DoBigMap`'s
+ // `_NewAnimation` call seeds the slot's frame index to 0xffff so
+ // the first `_UpdateAnimations` tick starts at script[0]; we mirror
+ // that by passing elapsed-since-open (zero on the first paint) into
+ // `bigMapPartnerFrameAtTick`, which plays the unfold once and then
+ // loops the wait sequence.
+ const uint32 mapStartTick = g_system->getMillis();
+ drawBigMapOverview(0);
+ uint32 mapLastTick = mapStartTick;
+
+ // Static rectangles read directly from the binary at the labelled
+ // addresses (CD `29be:0x1596` / floppy `2608:0x13fe..0x143e`).
+ // Format is {x1, y1, x2, y2}. The floppy click table at
+ // `2608:1436` (verified at `_BigMapInteractionLoop_Floppy @
+ // 1fed:0a3a`) puts the setup button at (251, 3, 315, 42) â 1 px
+ // up and 1 px left of the CD's (252, 4, 315, 42). The floppy
+ // PIC 0x42 BG paints the visible button border at the same
+ // pixels, so use the variant-specific rect to match the
+ // hit-test region the original uses for that variant.
+ const Common::Rect kBigMapWindow(0, 0, 247, 192); // 29be:1596
+ const Common::Rect kSetupBtnRect = isFloppy()
+ ? Common::Rect(251, 3, 315, 42) // 2608:1436
+ : Common::Rect(252, 4, 315, 42); // 29be:15ce
+
+ bool wantZoom = false;
+ int zoomX = 0;
+ int zoomY = 0;
+ while (!shouldQuit()) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
return;
- }
- // Click in the BigMapWindow â zoom. Original formula:
- // sx = mouseX*2 - 0x74; sy = mouseY*2 - 0x55
- if (kBigMapWindow.contains(ev.mouse.x, ev.mouse.y)) {
- int sx = ev.mouse.x * 2;
- int sy = ev.mouse.y * 2;
- sx = (sx < 0x75) ? 0 : sx - 0x74;
- sy = (sy < 0x56) ? 0 : sy - 0x55;
- zoomX = sx;
- zoomY = sy;
- wantZoom = true;
- break;
+ if (ev.type == Common::EVENT_KEYDOWN &&
+ ev.kbd.keycode == Common::KEYCODE_ESCAPE)
+ return;
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ // SetupButtonRect â `_NextScreen = 6` (the original's
+ // settings screen, mirrors `_DoBigMap @ 20fe:0c33`
+ // where it pushes `_PressButton` then writes
+ // `_NextScreen = 6`). Now wired to the actual
+ // `doSetup` handler instead of dropping the player
+ // out to the launcher.
+ if (kSetupBtnRect.contains(ev.mouse.x, ev.mouse.y)) {
+ _nextScreen = kScreenSetup;
+ return;
+ }
+ // Click in the BigMapWindow â zoom. Original formula:
+ // sx = mouseX*2 - 0x74; sy = mouseY*2 - 0x55
+ if (kBigMapWindow.contains(ev.mouse.x, ev.mouse.y)) {
+ int sx = ev.mouse.x * 2;
+ int sy = ev.mouse.y * 2;
+ sx = (sx < 0x75) ? 0 : sx - 0x74;
+ sy = (sy < 0x56) ? 0 : sy - 0x55;
+ zoomX = sx;
+ zoomY = sy;
+ wantZoom = true;
+ break;
+ }
}
}
- }
- if (wantZoom)
- break;
- // Cycle the partner-sprite frame every 100 ms (matching the
- // original's `_CheckFrameRate` cadence inside `_DoBigMap`).
- const uint32 now = g_system->getMillis();
- if (now - mapLastTick >= 100) {
- mapLastTick = now;
- drawBigMapOverview(now - mapStartTick);
- cyclePaletteRange(0xf7, 0xfa);
- cyclePaletteRange(0xfb, 0xfe);
- }
- g_system->updateScreen();
- g_system->delayMillis(10);
- }
-
- if (!wantZoom)
- return;
-
- // ------------------------------------------------------------------
- // STAGE 2 â Detail zoom: PIC 0x43 frame + scrollable BIGMAP.PIC
- // viewport at (2, 2), 0xe9 Ã 0xab. Click on a stamped icon â travel.
- // ------------------------------------------------------------------
+ if (wantZoom)
+ break;
- Common::File f;
- if (!f.open(Common::Path("BIGMAP.PIC"))) {
- warning("doBigMap: BIGMAP.PIC missing for detail view");
- return;
- }
- const uint16 mapH = f.readUint16LE();
- const uint16 mapW = f.readUint16LE();
- if (mapW == 0 || mapH == 0)
- return;
- Common::Array<byte> mapPixels((uint32)mapW * mapH);
- if (f.read(mapPixels.data(), mapPixels.size()) != mapPixels.size()) {
- warning("doBigMap: short read on BIGMAP.PIC for detail view");
- return;
- }
+ // Cycle the partner-sprite frame every 100 ms (matching the
+ // original's `_CheckFrameRate` cadence inside `_DoBigMap`).
+ const uint32 now = g_system->getMillis();
+ if (now - mapLastTick >= 100) {
+ mapLastTick = now;
+ drawBigMapOverview(now - mapStartTick);
+ cyclePaletteRange(0xf7, 0xfa);
+ cyclePaletteRange(0xfb, 0xfe);
+ }
+ g_system->updateScreen();
+ g_system->delayMillis(10);
+ }
- const int kMapWinW = 0xe9; // 233
- const int kMapWinH = 0xab; // 171
- const int kMapWinX = 2;
- const int kMapWinY = 2;
+ if (!wantZoom)
+ return;
- int scrollX = MAX<int>(0, MIN<int>(mapW - kMapWinW, zoomX));
- int scrollY = MAX<int>(0, MIN<int>(mapH - kMapWinH, zoomY));
+ // ------------------------------------------------------------------
+ // STAGE 2 â Detail zoom: PIC 0x43 frame + scrollable BIGMAP.PIC
+ // viewport at (2, 2), 0xe9 Ã 0xab. Click on a stamped icon â travel.
+ // ------------------------------------------------------------------
- // Anchor the detail-screen partner timeline (mirrors `_DoMapScreen`'s
- // `_NewAnimation` seeding the slot's frame index to 0xffff). The
- // unfold (script 0x13) plays once, then `_SmallMapWaitSeq` loops.
- const uint32 detailStartTick = g_system->getMillis();
- drawBigMapDetail(scrollX, scrollY, mapPixels, mapW, mapH, 0);
- uint32 detailLastTick = detailStartTick;
+ Common::File f;
+ if (!f.open(Common::Path("BIGMAP.PIC"))) {
+ warning("doBigMap: BIGMAP.PIC missing for detail view");
+ return;
+ }
+ const uint16 mapH = f.readUint16LE();
+ const uint16 mapW = f.readUint16LE();
+ if (mapW == 0 || mapH == 0)
+ return;
+ Common::Array<byte> mapPixels((uint32)mapW * mapH);
+ if (f.read(mapPixels.data(), mapPixels.size()) != mapPixels.size()) {
+ warning("doBigMap: short read on BIGMAP.PIC for detail view");
+ return;
+ }
- while (!shouldQuit()) {
- Common::Event ev;
- bool dirty = false;
- while (g_system->getEventManager()->pollEvent(ev)) {
- if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
- return;
- if (ev.type == Common::EVENT_KEYDOWN) {
- if (ev.kbd.keycode == Common::KEYCODE_ESCAPE)
- return; // exit detail back to caller (site loop / engine)
- const int kStep = 16;
- if (ev.kbd.keycode == Common::KEYCODE_LEFT) {
- scrollX = MAX<int>(0, scrollX - kStep);
- dirty = true;
- } else if (ev.kbd.keycode == Common::KEYCODE_RIGHT) {
- scrollX = MIN<int>(MAX<int>(0, mapW - kMapWinW),
- scrollX + kStep);
- dirty = true;
- } else if (ev.kbd.keycode == Common::KEYCODE_UP) {
- scrollY = MAX<int>(0, scrollY - kStep);
- dirty = true;
- } else if (ev.kbd.keycode == Common::KEYCODE_DOWN) {
- scrollY = MIN<int>(MAX<int>(0, mapH - kMapWinH),
- scrollY + kStep);
- dirty = true;
- }
- }
- if (ev.type == Common::EVENT_LBUTTONDOWN) {
- // Scroll arrows + slider rects live in `SmallMapButtons`
- // at 29be:0x159e (six 8-byte rects in order: Y-up, Y-down,
- // X-left, X-right, right-panel, top-right) plus the
- // dedicated `XSliderRect @ 29be:15d6` and
- // `YSliderRect @ 29be:15de`. Format {x1,y1,x2,y2}.
- const Common::Rect kArrowYUp (237, 2, 247, 11);
- const Common::Rect kArrowYDown (237, 163, 247, 172);
- const Common::Rect kArrowXLeft ( 2, 175, 12, 185);
- const Common::Rect kArrowXRight(224, 175, 234, 185);
- const Common::Rect kXSlider ( 15, 175, 221, 185);
- const Common::Rect kYSlider (237, 14, 247, 160);
- const Common::Rect kSetupBtn = isFloppy()
- ? Common::Rect(251, 3, 315, 42) // 2608:1436
- : Common::Rect(252, 4, 315, 42); // 29be:15ce
-
- const int kArrowStep = 16;
- const int kSliderRange = mapW - kMapWinW;
- const int kSliderRangeY = mapH - kMapWinH;
-
- if (kSetupBtn.contains(ev.mouse.x, ev.mouse.y)) {
- // `_DoMapScreen @ 20fe:1560` writes `_NextScreen
- // = 6` (= kScreenSetup) and `INC [BP-8]` to bail
- // out of the detail loop â verified via the byte
- // search for `c7 06 16 79 06 00`, which finds the
- // imm at exactly this site and `_DoBigMap @
- // 20fe:0c33`. Same `SetupButtonRect @ 29be:15ce`
- // rect used by both the overview and the detail
- // (no per-screen rect duplication in the binary).
- // The detail/zoom state is lost on return because
- // the screen driver re-enters BigMap at stage 1 â
- // this matches the original behaviour.
- _nextScreen = kScreenSetup;
+ const int kMapWinW = 0xe9; // 233
+ const int kMapWinH = 0xab; // 171
+ const int kMapWinX = 2;
+ const int kMapWinY = 2;
+
+ int scrollX = MAX<int>(0, MIN<int>(mapW - kMapWinW, zoomX));
+ int scrollY = MAX<int>(0, MIN<int>(mapH - kMapWinH, zoomY));
+
+ // Anchor the detail-screen partner timeline (mirrors `_DoMapScreen`'s
+ // `_NewAnimation` seeding the slot's frame index to 0xffff). The
+ // unfold (script 0x13) plays once, then `_SmallMapWaitSeq` loops.
+ const uint32 detailStartTick = g_system->getMillis();
+ drawBigMapDetail(scrollX, scrollY, mapPixels, mapW, mapH, 0);
+ uint32 detailLastTick = detailStartTick;
+ bool returnToOverview = false;
+
+ // `SmallMapButtons[4]` is the large right-side panel below setup:
+ // CD/floppy both store `(252, 43, 320, 200)`. Its handler at
+ // CD `20fe:156c` kills the zoom animation, sets `_NextScreen = 1`
+ // and calls `_DoBigMap` again, so mouse players can return to the
+ // overview without leaving the map screen.
+ const Common::Rect kBigMapReturnRect(252, 43, 320, 200);
+ const Common::Rect kArrowYUp(237, 2, 247, 11);
+ const Common::Rect kArrowYDown(237, 163, 247, 172);
+ const Common::Rect kArrowXLeft(2, 175, 12, 185);
+ const Common::Rect kArrowXRight(224, 175, 234, 185);
+ const Common::Rect kXSlider(15, 175, 221, 185);
+ const Common::Rect kYSlider(237, 14, 247, 160);
+ const Common::Rect kDetailSetupBtn = isFloppy()
+ ? Common::Rect(251, 3, 315, 42) // 2608:1436
+ : Common::Rect(252, 4, 315, 42); // 29be:15ce
+ const int kArrowStep = 16;
+ const int kSliderRange = mapW - kMapWinW;
+ const int kSliderRangeY = mapH - kMapWinH;
+ const Common::Point detailMouse =
+ g_system->getEventManager()->getMousePos();
+ setInteractiveMouseCursor(
+ kBigMapReturnRect.contains(detailMouse.x, detailMouse.y) ||
+ kDetailSetupBtn.contains(detailMouse.x, detailMouse.y));
+
+ while (!shouldQuit() && !returnToOverview) {
+ Common::Event ev;
+ bool dirty = false;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ setInteractiveMouseCursor(false);
return;
}
- if (kArrowYUp.contains(ev.mouse.x, ev.mouse.y)) {
- scrollY = MAX<int>(0, scrollY - kArrowStep);
- dirty = true;
- } else if (kArrowYDown.contains(ev.mouse.x, ev.mouse.y)) {
- scrollY = MIN<int>(MAX<int>(0, kSliderRangeY),
- scrollY + kArrowStep);
- dirty = true;
- } else if (kArrowXLeft.contains(ev.mouse.x, ev.mouse.y)) {
- scrollX = MAX<int>(0, scrollX - kArrowStep);
- dirty = true;
- } else if (kArrowXRight.contains(ev.mouse.x, ev.mouse.y)) {
- scrollX = MIN<int>(MAX<int>(0, kSliderRange),
- scrollX + kArrowStep);
- dirty = true;
- } else if (kXSlider.contains(ev.mouse.x, ev.mouse.y)) {
- // Click on X slider track â jump scrollX so the
- // click position maps proportionally into the map.
- if (kSliderRange > 0) {
- const int t = ev.mouse.x - kXSlider.left;
- const int tw = kXSlider.width();
- scrollX = MAX<int>(0, MIN<int>(kSliderRange,
- t * kSliderRange / MAX<int>(1, tw)));
- dirty = true;
+ if (ev.type == Common::EVENT_KEYDOWN) {
+ if (ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ setInteractiveMouseCursor(false);
+ return; // exit detail back to caller (site loop / engine)
}
- } else if (kYSlider.contains(ev.mouse.x, ev.mouse.y)) {
- if (kSliderRangeY > 0) {
- const int t = ev.mouse.y - kYSlider.top;
- const int th = kYSlider.height();
- scrollY = MAX<int>(0, MIN<int>(kSliderRangeY,
- t * kSliderRangeY / MAX<int>(1, th)));
+ const int kStep = 16;
+ if (ev.kbd.keycode == Common::KEYCODE_LEFT) {
+ scrollX = MAX<int>(0, scrollX - kStep);
+ dirty = true;
+ } else if (ev.kbd.keycode == Common::KEYCODE_RIGHT) {
+ scrollX = MIN<int>(MAX<int>(0, mapW - kMapWinW),
+ scrollX + kStep);
+ dirty = true;
+ } else if (ev.kbd.keycode == Common::KEYCODE_UP) {
+ scrollY = MAX<int>(0, scrollY - kStep);
+ dirty = true;
+ } else if (ev.kbd.keycode == Common::KEYCODE_DOWN) {
+ scrollY = MIN<int>(MAX<int>(0, mapH - kMapWinH),
+ scrollY + kStep);
dirty = true;
}
- } else if (ev.mouse.x >= kMapWinX &&
- ev.mouse.x < kMapWinX + kMapWinW &&
- ev.mouse.y >= kMapWinY &&
- ev.mouse.y < kMapWinY + kMapWinH) {
- // Hit-test the per-site button at its actual bbox
- // (`_StampButtons` records the rect at SmallMap +8/+0xa
- // with the button PIC's width/height).
- const bool fmap = _mystery.isLoaded() && isFloppy();
- for (uint i = 0; i < _mystery.numSites(); i++) {
- if (!_mystery._onSites[i] &&
- i != _mystery._siteNumber)
- continue;
- const byte *entry = _mystery.mapEntry(i);
- if (!entry)
- continue;
- uint16 mx;
- uint16 my;
- uint16 buttonId;
- if (fmap) {
- // Floppy detail view: click rect on
- // BIGMAP.PIC at (+0, +2), labelled BUTTON.DBD
- // entry ID at entry+4 (per
- // `FUN_1fed_0c3e @ 1fed:0c3e`).
- mx = READ_LE_UINT16(entry + 0x0);
- my = READ_LE_UINT16(entry + 0x2);
- buttonId = (uint16)entry[0x4];
- } else {
- buttonId = READ_LE_UINT16(entry + 0x0);
- mx = READ_LE_UINT16(entry + 0x8);
- my = READ_LE_UINT16(entry + 0xa);
+ }
+ if (ev.type == Common::EVENT_MOUSEMOVE)
+ setInteractiveMouseCursor(
+ kBigMapReturnRect.contains(ev.mouse.x, ev.mouse.y) ||
+ kDetailSetupBtn.contains(ev.mouse.x, ev.mouse.y));
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ setInteractiveMouseCursor(
+ kBigMapReturnRect.contains(ev.mouse.x, ev.mouse.y) ||
+ kDetailSetupBtn.contains(ev.mouse.x, ev.mouse.y));
+ if (kDetailSetupBtn.contains(ev.mouse.x, ev.mouse.y)) {
+ // `_DoMapScreen @ 20fe:1560` writes `_NextScreen
+ // = 6` (= kScreenSetup) and `INC [BP-8]` to bail
+ // out of the detail loop â verified via the byte
+ // search for `c7 06 16 79 06 00`, which finds the
+ // imm at exactly this site and `_DoBigMap @
+ // 20fe:0c33`. Same `SetupButtonRect @ 29be:15ce`
+ // rect used by both the overview and the detail
+ // (no per-screen rect duplication in the binary).
+ // The detail/zoom state is lost on return because
+ // the screen driver re-enters BigMap at stage 1 â
+ // this matches the original behaviour.
+ _nextScreen = kScreenSetup;
+ setInteractiveMouseCursor(false);
+ return;
+ }
+ if (kBigMapReturnRect.contains(ev.mouse.x, ev.mouse.y)) {
+ returnToOverview = true;
+ break;
+ } else if (kArrowYUp.contains(ev.mouse.x, ev.mouse.y)) {
+ scrollY = MAX<int>(0, scrollY - kArrowStep);
+ dirty = true;
+ } else if (kArrowYDown.contains(ev.mouse.x, ev.mouse.y)) {
+ scrollY = MIN<int>(MAX<int>(0, kSliderRangeY),
+ scrollY + kArrowStep);
+ dirty = true;
+ } else if (kArrowXLeft.contains(ev.mouse.x, ev.mouse.y)) {
+ scrollX = MAX<int>(0, scrollX - kArrowStep);
+ dirty = true;
+ } else if (kArrowXRight.contains(ev.mouse.x, ev.mouse.y)) {
+ scrollX = MIN<int>(MAX<int>(0, kSliderRange),
+ scrollX + kArrowStep);
+ dirty = true;
+ } else if (kXSlider.contains(ev.mouse.x, ev.mouse.y)) {
+ // Click on X slider track â jump scrollX so the
+ // click position maps proportionally into the map.
+ if (kSliderRange > 0) {
+ const int t = ev.mouse.x - kXSlider.left;
+ const int tw = kXSlider.width();
+ scrollX = MAX<int>(0, MIN<int>(kSliderRange,
+ t * kSliderRange / MAX<int>(1, tw)));
+ dirty = true;
}
- Picture button;
- int bw = 16;
- int bh = 16;
- if (_buttonArchive.loadEntry(buttonId, button)) {
- bw = button.surface.w;
- bh = button.surface.h;
+ } else if (kYSlider.contains(ev.mouse.x, ev.mouse.y)) {
+ if (kSliderRangeY > 0) {
+ const int t = ev.mouse.y - kYSlider.top;
+ const int th = kYSlider.height();
+ scrollY = MAX<int>(0, MIN<int>(kSliderRangeY,
+ t * kSliderRangeY / MAX<int>(1, th)));
+ dirty = true;
}
- const int sx = (int)mx - scrollX + kMapWinX;
- const int sy = (int)my - scrollY + kMapWinY;
- if (ev.mouse.x >= sx && ev.mouse.x < sx + bw &&
- ev.mouse.y >= sy && ev.mouse.y < sy + bh) {
- _mystery._lastSite = _mystery._siteNumber;
- _mystery._siteNumber = (uint16)i;
- return;
+ } else if (ev.mouse.x >= kMapWinX &&
+ ev.mouse.x < kMapWinX + kMapWinW &&
+ ev.mouse.y >= kMapWinY &&
+ ev.mouse.y < kMapWinY + kMapWinH) {
+ // Hit-test the per-site button at its actual bbox
+ // (`_StampButtons` records the rect at SmallMap +8/+0xa
+ // with the button PIC's width/height).
+ const bool fmap = _mystery.isLoaded() && isFloppy();
+ for (uint i = 0; i < _mystery.numSites(); i++) {
+ if (!_mystery._onSites[i] &&
+ i != _mystery._siteNumber)
+ continue;
+ const byte *entry = _mystery.mapEntry(i);
+ if (!entry)
+ continue;
+ uint16 mx;
+ uint16 my;
+ uint16 buttonId;
+ if (fmap) {
+ // Floppy detail view: click rect on
+ // BIGMAP.PIC at (+0, +2), labelled BUTTON.DBD
+ // entry ID at entry+4 (per
+ // `FUN_1fed_0c3e @ 1fed:0c3e`).
+ mx = READ_LE_UINT16(entry + 0x0);
+ my = READ_LE_UINT16(entry + 0x2);
+ buttonId = (uint16)entry[0x4];
+ } else {
+ buttonId = READ_LE_UINT16(entry + 0x0);
+ mx = READ_LE_UINT16(entry + 0x8);
+ my = READ_LE_UINT16(entry + 0xa);
+ }
+ Picture button;
+ int bw = 16;
+ int bh = 16;
+ if (_buttonArchive.loadEntry(buttonId, button)) {
+ bw = button.surface.w;
+ bh = button.surface.h;
+ }
+ const int sx = (int)mx - scrollX + kMapWinX;
+ const int sy = (int)my - scrollY + kMapWinY;
+ if (ev.mouse.x >= sx && ev.mouse.x < sx + bw &&
+ ev.mouse.y >= sy && ev.mouse.y < sy + bh) {
+ _mystery._lastSite = _mystery._siteNumber;
+ _mystery._siteNumber = (uint16)i;
+ setInteractiveMouseCursor(false);
+ return;
+ }
}
}
}
}
+ if (returnToOverview)
+ break;
+
+ // Cycle the partner sprite at 100 ms ticks (same cadence as
+ // `_DoMapScreen`'s `_CheckFrameRate` + `_UpdateAnimations` loop).
+ const uint32 now = g_system->getMillis();
+ if (now - detailLastTick >= 100) {
+ detailLastTick = now;
+ dirty = true;
+ }
+ if (dirty)
+ drawBigMapDetail(scrollX, scrollY, mapPixels, mapW, mapH,
+ now - detailStartTick);
+ g_system->updateScreen();
+ g_system->delayMillis(10);
}
- // Cycle the partner sprite at 100 ms ticks (same cadence as
- // `_DoMapScreen`'s `_CheckFrameRate` + `_UpdateAnimations` loop).
- const uint32 now = g_system->getMillis();
- if (now - detailLastTick >= 100) {
- detailLastTick = now;
- dirty = true;
- }
- if (dirty)
- drawBigMapDetail(scrollX, scrollY, mapPixels, mapW, mapH,
- now - detailStartTick);
- g_system->updateScreen();
- g_system->delayMillis(10);
+ if (!returnToOverview)
+ return;
}
}
Commit: ba0b1afbc3d03fcd8782a0e8ed0aa7f06c040fba
https://github.com/scummvm/scummvm/commit/ba0b1afbc3d03fcd8782a0e8ed0aa7f06c040fba
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:02+02:00
Commit Message:
EEM: mt32 music
Changed paths:
engines/eem/detection.cpp
engines/eem/music.cpp
engines/eem/music.h
diff --git a/engines/eem/detection.cpp b/engines/eem/detection.cpp
index d0b9b5800f2..fd620dd89f4 100644
--- a/engines/eem/detection.cpp
+++ b/engines/eem/detection.cpp
@@ -31,6 +31,8 @@ const PlainGameDescriptor eemGames[] = {
{ nullptr, nullptr }
};
+#define GUI_OPTIONS_EEM GUIO3(GAMEOPTION_HIDE_HIGHLIGHT_BOXES, GUIO_MIDIADLIB, GUIO_MIDIMT32)
+
const ADGameDescription gameDescriptions[] = {
{
"eem",
@@ -40,7 +42,7 @@ const ADGameDescription gameDescriptions[] = {
Common::EN_ANY,
Common::kPlatformDOS,
ADGF_NO_FLAGS,
- GUIO1(GAMEOPTION_HIDE_HIGHLIGHT_BOXES)
+ GUI_OPTIONS_EEM
},
{
"eem",
@@ -50,7 +52,7 @@ const ADGameDescription gameDescriptions[] = {
Common::EN_ANY,
Common::kPlatformDOS,
ADGF_NO_FLAGS,
- GUIO1(GAMEOPTION_HIDE_HIGHLIGHT_BOXES)
+ GUI_OPTIONS_EEM
},
{
// Spanish floppy release â same EEM.EXE binary as the English
@@ -64,7 +66,7 @@ const ADGameDescription gameDescriptions[] = {
Common::ES_ESP,
Common::kPlatformDOS,
ADGF_NO_FLAGS,
- GUIO1(GAMEOPTION_HIDE_HIGHLIGHT_BOXES)
+ GUI_OPTIONS_EEM
},
AD_TABLE_END_MARKER
diff --git a/engines/eem/music.cpp b/engines/eem/music.cpp
index 80842663a09..1bd50f83cce 100644
--- a/engines/eem/music.cpp
+++ b/engines/eem/music.cpp
@@ -22,6 +22,7 @@
#include "audio/midiparser.h"
#include "audio/miles.h"
+#include "common/config-manager.h"
#include "common/debug.h"
#include "common/file.h"
#include "common/textconsole.h"
@@ -31,15 +32,23 @@
namespace EEM {
+namespace {
+
+const int kMidiDriverFlags = MDT_MIDI | MDT_ADLIB | MDT_PREFER_MT32;
+
+} // End of anonymous namespace
+
MusicPlayer::MusicPlayer(bool isFloppy) : _isFloppy(isFloppy) {
// Mirrors `_InitMIDI @ 20a2:013a` which used `_AIL_register_driver`
// to walk the .ADV files (ADLIB.ADV, SBFM.ADV, MT32MPU.ADV, etc.)
// and pick a backend. We honour the launcher's "Music driver"
- // setting and only force AdLib / MT-32 paths through Miles when the
- // detected device matches.
+ // setting while preferring MT-32 when no concrete device was chosen,
+ // like other ScummVM engines with native MT-32 scores.
const MidiDriver::DeviceHandle dev =
- MidiDriver::detectDevice(MDT_MIDI | MDT_ADLIB);
- const MusicType musicType = MidiDriver::getMusicType(dev);
+ MidiDriver::detectDevice(kMidiDriverFlags);
+ MusicType musicType = MidiDriver::getMusicType(dev);
+ if (musicType == MT_GM && ConfMan.getBool("native_mt32"))
+ musicType = MT_MT32;
switch (musicType) {
case MT_ADLIB:
@@ -64,7 +73,7 @@ MusicPlayer::MusicPlayer(bool isFloppy) : _isFloppy(isFloppy) {
break;
default:
_milesAudioMode = false;
- createDriver(MDT_MIDI | MDT_ADLIB);
+ createDriver(kMidiDriverFlags);
break;
}
diff --git a/engines/eem/music.h b/engines/eem/music.h
index 50dedeb9eb7..7e8c1fc817d 100644
--- a/engines/eem/music.h
+++ b/engines/eem/music.h
@@ -53,8 +53,8 @@ namespace EEM {
* different timbres. ScummVM ships a Miles AdLib driver
* (`Audio::MidiDriver_Miles_AdLib_create`) that loads `SAMPLE.AD` and
* implements the same install-on-demand workflow, so we use it for
- * AdLib output. MT-32 / GM fall back to the generic driver via
- * `Audio::MidiPlayer::createDriver`.
+ * AdLib output. MT-32 uses ScummVM's Miles MT-32 driver path, and any
+ * other MIDI output falls back to `Audio::MidiPlayer::createDriver`.
*
* Available music files in the game directory:
* - THEME.XMI â opening anims (looping) + title screen
Commit: fd806599d283c57063b40afa2a092f3993df2828
https://github.com/scummvm/scummvm/commit/fd806599d283c57063b40afa2a092f3993df2828
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:02+02:00
Commit Message:
EEM: message sliding animation for some screens
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 7ed2cac176e..af1a43a8767 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -72,6 +72,18 @@ constexpr Common::Rect kPdaHelp2Rect(Common::Point(267, 174), 21, 16);
constexpr Common::Rect kPdaPartnerFootMapRect(Common::Point(7, 177), 50, 23);
constexpr Common::Rect kPdaSiteRect(Common::Point(35, 111), 21, 25);
+constexpr uint16 kProfilePickerRevealPic = 0x105;
+constexpr int kProfilePickerRevealX = 0x3e;
+constexpr int kProfilePickerRevealY = 0xb3;
+
+constexpr uint16 kNameEntryPeekPic = 0x107;
+constexpr int kNameEntryPeekX = 0x3e;
+constexpr int kNameEntryPeekY = 0xb3;
+
+constexpr uint16 kCaseSelectionRevealPic = 0x53;
+constexpr int kCaseSelectionRevealX = 0x3e;
+constexpr int kCaseSelectionRevealY = 0xb2;
+
bool notebookButtonAt(int x, int y) {
return kPdaHelpRect.contains(x, y) ||
kPdaGalleryRect.contains(x, y) ||
@@ -149,6 +161,143 @@ int nextLiveSlot(const Common::Array<Common::Rect> &slotRects,
return from;
}
+void copyToScreen(Graphics::ManagedSurface &scratch) {
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+}
+
+void blitMaskedPicSlice(Graphics::ManagedSurface &dst, const Picture &pic,
+ int srcX, int srcY, int w, int h,
+ int dstX, int dstY) {
+ if (pic.surface.empty() || w <= 0 || h <= 0)
+ return;
+
+ const byte transp = (byte)(pic.flags >> 8);
+ for (int row = 0; row < h; row++) {
+ const int sy = srcY + row;
+ const int dy = dstY + row;
+ if (sy < 0 || sy >= pic.surface.h || dy < 0 || dy >= dst.h)
+ continue;
+ for (int col = 0; col < w; col++) {
+ const int sx = srcX + col;
+ const int dx = dstX + col;
+ if (sx < 0 || sx >= pic.surface.w || dx < 0 || dx >= dst.w)
+ continue;
+ const byte c = *(const byte *)pic.surface.getBasePtr(sx, sy);
+ if (c != transp)
+ *(byte *)dst.getBasePtr(dx, dy) = c;
+ }
+ }
+}
+
+void blitMaskedPic(Graphics::ManagedSurface &dst, const Picture &pic,
+ int x, int y) {
+ blitMaskedPicSlice(dst, pic, 0, 0, pic.surface.w, pic.surface.h, x, y);
+}
+
+void blitMaskedPicRightReveal(Graphics::ManagedSurface &dst,
+ const Picture &pic, int x, int y,
+ int visibleW) {
+ const int w = CLIP<int>(visibleW, 0, pic.surface.w);
+ if (w == 0)
+ return;
+ blitMaskedPicSlice(dst, pic, 0, 0, w, pic.surface.h,
+ x + pic.surface.w - w, y);
+}
+
+void blitMaskedPicBottomReveal(Graphics::ManagedSurface &dst,
+ const Picture &pic, int x, int y,
+ int visibleH) {
+ const int h = CLIP<int>(visibleH, 0, pic.surface.h);
+ if (h == 0)
+ return;
+ blitMaskedPicSlice(dst, pic, 0, 0, pic.surface.w, h,
+ x, y + pic.surface.h - h);
+}
+
+bool pumpQuitEvents(EEMEngine *vm) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return true;
+ }
+ return vm && vm->shouldQuit();
+}
+
+void drawCaseBookTitle(Graphics::ManagedSurface &scratch, const EEMEngine *vm,
+ uint book) {
+ if (!vm || !vm->getFont().isLoaded())
+ return;
+
+ const bool spanish = vm->isSpanish();
+ const Common::String title = (book == 3)
+ ? Common::String(spanish ? "Libro de Retos" : "Challenge Book")
+ : Common::String::format(spanish ? "Lib. %u" : "Book %u", book);
+ const int titleW = vm->getFont().getStringWidth(title);
+ const int titleX = (0xba - titleW) / 2 + 0x3c;
+ vm->getFont().drawString(&scratch, title, titleX, 12, 320, 0xF);
+}
+
+void drawNameEntryFrame(EEMEngine *vm, const Picture *bg, bool haveBG,
+ const Picture *peek, const Common::String &name,
+ const char *prompt) {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ if (haveBG)
+ scratch.simpleBlitFrom(bg->surface);
+ if (peek)
+ blitMaskedPic(scratch, *peek, kNameEntryPeekX, kNameEntryPeekY);
+ vm->getFont().drawString(&scratch, prompt, 80, 40, 240, 0xF);
+ vm->getFont().drawString(&scratch, name + "_", 80, 80, 240, 0xF);
+ copyToScreen(scratch);
+}
+
+bool animateNameEntryPeek(EEMEngine *vm, const Picture *bg, bool haveBG,
+ const Picture *peek) {
+ if (!peek || peek->surface.empty())
+ return false;
+
+ for (int w = 1; w <= peek->surface.w; w++) {
+ if (pumpQuitEvents(vm))
+ return true;
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ if (haveBG)
+ scratch.simpleBlitFrom(bg->surface);
+ blitMaskedPicRightReveal(scratch, *peek,
+ kNameEntryPeekX, kNameEntryPeekY, w);
+ copyToScreen(scratch);
+ g_system->delayMillis(10);
+ }
+ return false;
+}
+
+bool animateProfilePickerReveal(EEMEngine *vm, const Picture *bg,
+ bool haveBG, const Picture *reveal) {
+ if (!reveal || reveal->surface.empty())
+ return false;
+
+ for (int h = 1; h <= reveal->surface.h; h++) {
+ if (pumpQuitEvents(vm))
+ return true;
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ if (haveBG)
+ scratch.simpleBlitFrom(bg->surface);
+ blitMaskedPicBottomReveal(scratch, *reveal,
+ kProfilePickerRevealX,
+ kProfilePickerRevealY, h);
+ copyToScreen(scratch);
+ g_system->delayMillis(10);
+ }
+ return false;
+}
+
// Snapshot of `doCaseSelection`'s captured locals, used by
// `drawCaseSelectionFrame` (which replaces the original lambda). Lives
// on the stack inside `doCaseSelection`; never escapes.
@@ -156,6 +305,8 @@ struct CaseSelectionView {
EEMEngine *vm;
const Picture *caseBg;
bool haveCaseBg;
+ const Picture *revealPic;
+ bool haveRevealPic;
const Animation *kdAnim;
bool haveKdAnim;
uint16 kdAnimId; ///< 0x15 / 0x16 â looked up in kAnimScripts
@@ -165,6 +316,7 @@ struct CaseSelectionView {
const char *const *pickLabel;
const bool *pickEnabled;
uint pick;
+ uint book;
};
// Mystery list shown in the "Choose A Mystery" sub-screen. Mirrors
@@ -213,6 +365,12 @@ struct CaseSubmenuView {
EEMEngine *vm;
const Picture *caseBg;
bool haveCaseBg;
+ const Picture *revealPic;
+ bool haveRevealPic;
+ const Animation *kdAnim;
+ bool haveKdAnim;
+ int kdAnimX;
+ int kdAnimY;
const Common::StringArray *names;
const Common::Array<bool> *solvedFlags;
uint topRow;
@@ -220,6 +378,72 @@ struct CaseSubmenuView {
uint book; ///< 1..3 â for the "Book N" / "Challenge Book" title
};
+void drawCaseGreeter(Graphics::ManagedSurface &scratch,
+ const Animation *kdAnim, bool haveKdAnim,
+ int kdAnimX, int kdAnimY) {
+ if (!haveKdAnim || !kdAnim || kdAnim->empty())
+ return;
+
+ // `_CaseSelection` registers the partner-specific ANI slot, but
+ // drives it with script 0x15 regardless of partner. The chooser
+ // loop advances it through `_UpdateAnimations` after each
+ // `_CheckFrameRate` tick.
+ const uint32 now = g_system->getMillis();
+ const uint frameIdx = partnerFrameAtTick(0x15, (uint)kdAnim->size(), now);
+ blitAnimFrameAnchored(scratch.surfacePtr(), (*kdAnim)[frameIdx],
+ kdAnimX, kdAnimY);
+}
+
+void drawCaseBase(Graphics::ManagedSurface &scratch, EEMEngine *vm,
+ const Picture *caseBg, bool haveCaseBg,
+ const Picture *revealPic, bool haveRevealPic,
+ const Animation *kdAnim, bool haveKdAnim,
+ int kdAnimX, int kdAnimY, uint book) {
+ scratch.clear();
+ if (haveCaseBg && caseBg)
+ scratch.simpleBlitFrom(caseBg->surface);
+ if (haveRevealPic && revealPic)
+ blitMaskedPic(scratch, *revealPic,
+ kCaseSelectionRevealX, kCaseSelectionRevealY);
+ drawCaseBookTitle(scratch, vm, book);
+ drawCaseGreeter(scratch, kdAnim, haveKdAnim, kdAnimX, kdAnimY);
+}
+
+bool animateCaseSelectionReveal(EEMEngine *vm, const Picture *caseBg,
+ bool haveCaseBg, const Picture *revealPic,
+ bool haveRevealPic, const Animation *kdAnim,
+ bool haveKdAnim, int kdAnimX, int kdAnimY,
+ uint book) {
+ if (!haveRevealPic || !revealPic || revealPic->surface.empty())
+ return false;
+
+ const int steps = vm && vm->isFloppy()
+ ? revealPic->surface.w : revealPic->surface.h;
+ for (int i = 1; i <= steps; i++) {
+ if (pumpQuitEvents(vm))
+ return true;
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ if (haveCaseBg && caseBg)
+ scratch.simpleBlitFrom(caseBg->surface);
+ if (vm && vm->isFloppy()) {
+ blitMaskedPicRightReveal(scratch, *revealPic,
+ kCaseSelectionRevealX,
+ kCaseSelectionRevealY, i);
+ } else {
+ blitMaskedPicBottomReveal(scratch, *revealPic,
+ kCaseSelectionRevealX,
+ kCaseSelectionRevealY, i);
+ }
+ drawCaseBookTitle(scratch, vm, book);
+ drawCaseGreeter(scratch, kdAnim, haveKdAnim, kdAnimX, kdAnimY);
+ copyToScreen(scratch);
+ g_system->delayMillis(8);
+ }
+ return false;
+}
+
// Mirrors `_DoChoose`'s `DrawList @ 1c33:040d`. 12 visible rows à 10 px
// at (61, 35); colour palette: 0x13 = highlighted (selected), 0x1B =
// greyed (already solved), 0x5C = default. We approximate with the
@@ -229,25 +453,12 @@ struct CaseSubmenuView {
void drawCaseSubmenu(const CaseSubmenuView &v) {
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
- if (v.haveCaseBg)
- scratch.simpleBlitFrom(v.caseBg->surface);
+ drawCaseBase(scratch, v.vm, v.caseBg, v.haveCaseBg,
+ v.revealPic, v.haveRevealPic,
+ v.kdAnim, v.haveKdAnim, v.kdAnimX, v.kdAnimY, v.book);
if (!v.vm->getFont().isLoaded() || !v.names)
return;
- // Top centred title. `_CaseSelection @ 1c33:0aa3` formats "Book %d"
- // for tiers 1/2 and "Challenge Book" (sprintf with no arg) for
- // tier 3. `_Show_String(0xc, (0xba - width)/2 + 0x3c, â¦, 0x10)`
- // places it horizontally centred over the panel. Spanish floppy
- // uses "Lib. %u" / "Libro de Retos" (verified in Spanish EEM.EXE).
- const bool spanish = v.vm && v.vm->isSpanish();
- const Common::String title = (v.book == 3)
- ? Common::String(spanish ? "Libro de Retos" : "Challenge Book")
- : Common::String::format(spanish ? "Lib. %u" : "Book %u", v.book);
- const int titleW = v.vm->getFont().getStringWidth(title);
- const int titleX = (0xba - titleW) / 2 + 0x3c;
- v.vm->getFont().drawString(&scratch, title, titleX, 12, 320, 0xF);
-
const int kListX = 61;
const int kListW = 238 - kListX;
const int kListY0 = 35;
@@ -298,38 +509,16 @@ void drawCaseSubmenu(const CaseSubmenuView &v) {
scratch.frameRect(thumb, 0xF);
}
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
+ copyToScreen(scratch);
}
void drawCaseSelectionFrame(const CaseSelectionView &v) {
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
- if (v.haveCaseBg)
- scratch.simpleBlitFrom(v.caseBg->surface);
-
- // KD greeter frame â masked-blit current animation cell at
- // (0x112, 0x50). 100 ms tick matches `_CheckFrameRate`. The
- // original `_CaseSelection @ 1c33:0a87` calls `_NewAnimation(...,
- // CONCAT22(0x15, ...), ..., seqnum=0x15, ...)` so the script
- // key is 0x15 regardless of partner â even Jenny's CELLS (loaded
- // via animID 0x16 = ANI.DBD slot) get driven by Jake's 0x15
- // blink script. Both 0x15 and 0x16 are aliases of 0x00 in our
- // table so the result is identical, but routing through 0x15
- // matches the binary.
- if (v.haveKdAnim) {
- const uint32 now = g_system->getMillis();
- const uint frameIdx = partnerFrameAtTick(0x15,
- (uint)v.kdAnim->size(), now);
- // Anchor-aware blit. Same rendering path used everywhere
- // the partner is registered through `_NewAnimation` in the
- // original.
- blitAnimFrameAnchored(scratch.surfacePtr(),
- (*v.kdAnim)[frameIdx],
- v.kdAnimX, v.kdAnimY);
- }
+ drawCaseBase(scratch, v.vm, v.caseBg, v.haveCaseBg,
+ v.revealPic, v.haveRevealPic,
+ v.kdAnim, v.haveKdAnim, v.kdAnimX, v.kdAnimY, v.book);
+
if (v.vm->getFont().isLoaded()) {
// `DrawList` @ 1c33:040d coordinates: `_TextBox + 3` for x
// and `DAT_29be_0d02` for y. `_TextBox` @ 29be:0d00 holds
@@ -339,17 +528,6 @@ void drawCaseSelectionFrame(const CaseSelectionView &v) {
const int kListY0 = 35;
const int kLineH = 10;
- // Top centred "Book %d" / "Challenge Book" title â sprintf
- // format strings at 29be:0deb / 29be:0dfa shown via
- // `_Show_String(0xc, (0xba - width)/2 + 0x3c, â¦)` in the
- // original. We don't track challenge tier yet so always
- // show "Book 1".
- const Common::String book = v.vm->isSpanish()
- ? Common::String("Lib. 1") : Common::String("Book 1");
- const int titleW = v.vm->getFont().getStringWidth(book);
- const int titleX = (0xba - titleW) / 2 + 0x3c;
- v.vm->getFont().drawString(&scratch, book, titleX, 12, 320, 0xF);
-
// Render 11 list rows: separator + menu item pairs.
// row 0 separator
// row 1 Choose A Mystery
@@ -373,9 +551,7 @@ void drawCaseSelectionFrame(const CaseSelectionView &v) {
kListX, y, kListW, color);
}
}
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
+ copyToScreen(scratch);
}
void EEMEngine::doProfilePicker() {
@@ -426,14 +602,18 @@ void EEMEngine::doProfilePicker() {
int sel = 0;
bool done = false;
+ Picture bg;
+ const bool haveBG = _picsArchive.getPicture(0x104, bg);
+ Picture reveal;
+ const bool haveReveal =
+ _picsArchive.getPicture(kProfilePickerRevealPic, reveal);
// Picker geometry: `DrawList @ 1c33:040d` is called from
// `screen8_handler @ 1c33:1012` with `(_TextBox + 3, DAT_29be_0d02)`.
// `_TextBox @ 29be:0d00` holds {x1=58, y1=35, x2=238, y2=158} so
// the list origin is (61, 35), 10 px per row, max 12 visible
- // rows. The "Pick a player" caption is part of the BG (PIC 0x104)
- // â `screen8_handler` never draws it as text â so an extra
- // `drawString` would overlay on top of the baked-in heading.
+ // rows. `screen8_handler` slides PIC 0x105 into the lower strip
+ // before calling `_DoChoose`; it does not draw that caption as text.
const int kListX = 61;
const int kListY = 35;
const int kLineH = 10;
@@ -441,18 +621,21 @@ void EEMEngine::doProfilePicker() {
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
- Picture bg;
- if (_picsArchive.getPicture(0x104, bg))
+ if (haveBG)
scratch.simpleBlitFrom(bg.surface);
+ if (haveReveal)
+ blitMaskedPic(scratch, reveal,
+ kProfilePickerRevealX, kProfilePickerRevealY);
for (uint i = 0; i < entries.size(); i++) {
const byte color = ((int)i == sel) ? 0xF : 0x8;
_font.drawString(&scratch, entries[i].label,
kListX, kListY + (int)i * kLineH, 220, color);
}
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
+ copyToScreen(scratch);
};
+ if (animateProfilePickerReveal(this, &bg, haveBG,
+ haveReveal ? &reveal : nullptr))
+ return;
draw();
while (!done && !shouldQuit()) {
@@ -524,8 +707,7 @@ void EEMEngine::doProfilePicker() {
void EEMEngine::doNewPlayer() {
// Mirrors `_NewPlayer` @ 1c33:0dda. The original draws background
// 0x104 + character peek pic 0x107, then shows "Please type your
- // name" and accepts up to 12 characters until Enter. We render a
- // minimal version: black screen + prompt.
+ // name" and accepts up to 12 characters until Enter.
if (!_font.isLoaded()) {
_playerName = "Detective";
return;
@@ -538,6 +720,8 @@ void EEMEngine::doNewPlayer() {
// The original also slides in PIC 0x107 (a peeking character).
Picture bg;
const bool haveBG = _picsArchive.getPicture(0x104, bg);
+ Picture peek;
+ const bool havePeek = _picsArchive.getPicture(kNameEntryPeekPic, peek);
// Localized name-entry prompt. Spanish text is taken from the
// Spanish floppy EEM.EXE ("Teclea tu nombre"). The colon suffix is
@@ -545,19 +729,13 @@ void EEMEngine::doNewPlayer() {
const char *prompt = isSpanish()
? "Teclea tu nombre:" : "Please type your name:";
+ if (animateNameEntryPeek(this, &bg, haveBG, havePeek ? &peek : nullptr))
+ return;
// Match the original `_NewPlayer`: `_Show_String(rw=0x28, cl=0x50)`
// for the prompt, then `_ShowChar(0x50, x, â¦)` for typed input.
// (rw=row=y, cl=col=x.) Prompt at (y=40, x=80), input at (y=80, x=80).
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
- if (haveBG)
- scratch.simpleBlitFrom(bg.surface);
- _font.drawString(&scratch, prompt, 80, 40, 240, 0xF);
- _font.drawString(&scratch, name + "_", 80, 80, 240, 0xF);
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
+ drawNameEntryFrame(this, &bg, haveBG, havePeek ? &peek : nullptr,
+ name, prompt);
g_system->setFeatureState(OSystem::kFeatureVirtualKeyboard, true);
@@ -616,16 +794,8 @@ void EEMEngine::doNewPlayer() {
}
}
if (dirty) {
- // Re-render with the updated `name`. Same body as the
- // initial render above â only `name + "_"` changes.
- scratch.clear();
- if (haveBG)
- scratch.simpleBlitFrom(bg.surface);
- _font.drawString(&scratch, prompt, 80, 40, 240, 0xF);
- _font.drawString(&scratch, name + "_", 80, 80, 240, 0xF);
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
+ drawNameEntryFrame(this, &bg, haveBG, havePeek ? &peek : nullptr,
+ name, prompt);
}
g_system->updateScreen();
g_system->delayMillis(15);
@@ -1482,6 +1652,9 @@ void EEMEngine::doCaseSelection() {
// Mirrors `_CaseSelection`: load PIC 0x41 as the chooser backdrop.
Picture caseBg;
const bool haveCaseBg = _picsArchive.getPicture(0x41, caseBg);
+ Picture revealPic;
+ const bool haveRevealPic =
+ _picsArchive.getPicture(kCaseSelectionRevealPic, revealPic);
// KD greeter sprite. `_CaseSelection @ 1c33:0a87` (1c33:0b7e-0ba1)
// loads anim 0x15 (Jake-paired) or 0x16 (Jenny-paired) and registers
@@ -1495,11 +1668,15 @@ void EEMEngine::doCaseSelection() {
&& !kdAnim.empty();
const int kKdAnimX = 0x112;
const int kKdAnimY = 0x50;
+ const uint caseBook = (_chainStage == 3) ? 3 :
+ (_chainStage == 2) ? 2 : 1;
CaseSelectionView v;
v.vm = this;
v.caseBg = &caseBg;
v.haveCaseBg = haveCaseBg;
+ v.revealPic = &revealPic;
+ v.haveRevealPic = haveRevealPic;
v.kdAnim = &kdAnim;
v.haveKdAnim = haveKdAnim;
v.kdAnimId = (uint16)kKdAniId;
@@ -1509,7 +1686,13 @@ void EEMEngine::doCaseSelection() {
v.pickLabel = kPickLabel;
v.pickEnabled = kPickEnabled;
v.pick = pick;
+ v.book = caseBook;
+ if (animateCaseSelectionReveal(this, &caseBg, haveCaseBg,
+ &revealPic, haveRevealPic,
+ &kdAnim, haveKdAnim,
+ kKdAnimX, kKdAnimY, caseBook))
+ return;
drawCaseSelectionFrame(v);
uint32 lastTick = g_system->getMillis();
@@ -1723,13 +1906,25 @@ void EEMEngine::doCaseSelection() {
sv.vm = this;
sv.caseBg = &caseBg;
sv.haveCaseBg = haveCaseBg;
+ sv.revealPic = &revealPic;
+ sv.haveRevealPic = haveRevealPic;
+ sv.kdAnim = &kdAnim;
+ sv.haveKdAnim = haveKdAnim;
+ sv.kdAnimX = kKdAnimX;
+ sv.kdAnimY = kKdAnimY;
sv.names = &names;
sv.solvedFlags = &solvedFlags;
sv.topRow = topRow;
sv.selRow = selRow;
sv.book = book;
+ if (animateCaseSelectionReveal(this, &caseBg, haveCaseBg,
+ &revealPic, haveRevealPic,
+ &kdAnim, haveKdAnim,
+ kKdAnimX, kKdAnimY, book))
+ return;
drawCaseSubmenu(sv);
+ uint32 submenuLastTick = g_system->getMillis();
bool confirmed = false;
while (!confirmed && !shouldQuit()) {
Common::Event ev;
@@ -1838,7 +2033,11 @@ void EEMEngine::doCaseSelection() {
continue;
}
}
- if (dirty) {
+ const uint32 now = g_system->getMillis();
+ const bool animTick = haveKdAnim && now - submenuLastTick >= 100;
+ if (animTick)
+ submenuLastTick = now;
+ if (dirty || animTick) {
sv.topRow = topRow;
sv.selRow = selRow;
drawCaseSubmenu(sv);
Commit: 6494a017697d0d4e9bd35559e0801598bd51c591
https://github.com/scummvm/scummvm/commit/6494a017697d0d4e9bd35559e0801598bd51c591
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:03+02:00
Commit Message:
EEM: complete handler for screen profile
Changed paths:
engines/eem/eem.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index f7523be122b..dba23490674 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -582,6 +582,27 @@ screen_loop:
doSetup();
break;
+ case kScreenProfile:
+ // Handler 8 is the player/profile picker. CD
+ // `screen8_handler @ 1c33:1012` loads an existing player
+ // record or runs `_NewPlayer`; floppy
+ // `_HandleScreen8_NewPlayer_Floppy @ 19bb:0ec2` then
+ // writes screen 9. Mirror that route inline: after the
+ // profile is selected, choose a partner, then continue to
+ // the selected profile's loaded case if ScummVM save state
+ // had one, otherwise to case selection.
+ _nextScreen = kScreenInvalid;
+ _mystery.clear();
+ doProfilePicker();
+ if (!shouldQuit())
+ applyStartupTestOverrides();
+ if (!shouldQuit())
+ doChoosePartner();
+ if (!shouldQuit())
+ _nextScreen = _mystery.isLoaded() ? kScreenMap
+ : kScreenChooseMystery;
+ break;
+
case kScreenAccuse:
// Handler 7 runs the accusation flow. A failed accusation
// returns to `_LastScreen`; a correct solution writes
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index af1a43a8767..8a2fab8286a 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -75,6 +75,18 @@ constexpr Common::Rect kPdaSiteRect(Common::Point(35, 111), 21, 25);
constexpr uint16 kProfilePickerRevealPic = 0x105;
constexpr int kProfilePickerRevealX = 0x3e;
constexpr int kProfilePickerRevealY = 0xb3;
+constexpr int kProfileListX = 61;
+constexpr int kProfileListY = 35;
+constexpr int kProfileListW = 220;
+constexpr int kProfileLineH = 10;
+constexpr int kProfileVisibleRows = 12;
+constexpr Common::Rect kChooserOkRect(Common::Point(12, 63), 29, 24);
+constexpr Common::Rect kChooserHelpRect(Common::Point(12, 100), 29, 24);
+constexpr Common::Rect kChooserExitRect(Common::Point(12, 137), 29, 24);
+constexpr Common::Rect kChooserUpArrowRect(Common::Point(240, 31), 10, 12);
+constexpr Common::Rect kChooserDnArrowRect(Common::Point(240, 148), 10, 11);
+constexpr Common::Rect kChooserListRect(Common::Point(58, 35), 180, 123);
+constexpr Common::Rect kChooserNewPlayerRect(Common::Point(61, 176), 185, 15);
constexpr uint16 kNameEntryPeekPic = 0x107;
constexpr int kNameEntryPeekX = 0x3e;
@@ -298,6 +310,62 @@ bool animateProfilePickerReveal(EEMEngine *vm, const Picture *bg,
return false;
}
+struct ProfilePickerEntry {
+ Common::String label;
+ int slot; ///< -1 means "create new"
+};
+
+struct ProfilePickerView {
+ EEMEngine *vm;
+ const Picture *bg;
+ bool haveBG;
+ const Picture *reveal;
+ bool haveReveal;
+ const Common::Array<ProfilePickerEntry> *entries;
+ int selected;
+ int start;
+};
+
+void clampProfileScroll(int &selected, int &start, int count) {
+ if (count <= 0) {
+ selected = 0;
+ start = 0;
+ return;
+ }
+ selected = CLIP<int>(selected, 0, count - 1);
+ const int maxStart = MAX<int>(0, count - kProfileVisibleRows);
+ start = CLIP<int>(start, 0, maxStart);
+ if (selected < start)
+ start = selected;
+ if (selected >= start + kProfileVisibleRows)
+ start = selected - kProfileVisibleRows + 1;
+ start = CLIP<int>(start, 0, maxStart);
+}
+
+void drawProfilePickerFrame(const ProfilePickerView &v) {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ if (v.haveBG)
+ scratch.simpleBlitFrom(v.bg->surface);
+ if (v.haveReveal)
+ blitMaskedPic(scratch, *v.reveal,
+ kProfilePickerRevealX, kProfilePickerRevealY);
+
+ const int count = v.entries ? (int)v.entries->size() : 0;
+ for (int row = 0; row < kProfileVisibleRows; row++) {
+ const int idx = v.start + row;
+ if (idx >= count)
+ break;
+ const byte color = idx == v.selected ? 0xF : 0x8;
+ v.vm->getFont().drawString(&scratch, (*v.entries)[idx].label,
+ kProfileListX,
+ kProfileListY + row * kProfileLineH,
+ kProfileListW, color);
+ }
+ copyToScreen(scratch);
+}
+
// Snapshot of `doCaseSelection`'s captured locals, used by
// `drawCaseSelectionFrame` (which replaces the original lambda). Lives
// on the stack inside `doCaseSelection`; never escapes.
@@ -561,7 +629,8 @@ void EEMEngine::doProfilePicker() {
// hands the list to `_DoChoose`. If no profiles exist (loop hits
// `local_20 == 0` at 1c33:1170), it falls straight into
// `_NewPlayer`. Selecting an entry calls `_LoadPlayerRecord` and
- // returns; selecting the "exit" sentinel goes back to title.
+ // returns; the 0xfffe / 0xffff chooser sentinels both enter
+ // `_NewPlayer` in this screen.
// Palette reset. `screen8_handler` runs `_FadeOut(); _GetPalette(0);
// _GetBackground(0x104);` before the picker, so the BG always
@@ -584,24 +653,26 @@ void EEMEngine::doProfilePicker() {
}
// Build the visible list: existing profile names + "[New Player]".
- struct Entry {
- Common::String label;
- int slot; ///< -1 means "create new"
- };
- Common::Array<Entry> entries;
+ // The DOS picker also has a bottom click area at 29be:0d08 that
+ // returns 0xfffe and immediately enters `_NewPlayer`; keeping the
+ // explicit row makes that affordance visible in ScummVM while the
+ // bottom rect remains active too.
+ Common::Array<ProfilePickerEntry> entries;
for (const SaveStateDescriptor &s : saves) {
- Entry e;
+ ProfilePickerEntry e;
e.label = s.getDescription();
e.slot = s.getSaveSlot();
entries.push_back(e);
}
- Entry newEntry;
+ ProfilePickerEntry newEntry;
newEntry.label = "[New Player]";
newEntry.slot = -1;
entries.push_back(newEntry);
int sel = 0;
+ int start = 0;
bool done = false;
+ bool createNew = false;
Picture bg;
const bool haveBG = _picsArchive.getPicture(0x104, bg);
Picture reveal;
@@ -614,29 +685,19 @@ void EEMEngine::doProfilePicker() {
// the list origin is (61, 35), 10 px per row, max 12 visible
// rows. `screen8_handler` slides PIC 0x105 into the lower strip
// before calling `_DoChoose`; it does not draw that caption as text.
- const int kListX = 61;
- const int kListY = 35;
- const int kLineH = 10;
- auto draw = [&]() {
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
- if (haveBG)
- scratch.simpleBlitFrom(bg.surface);
- if (haveReveal)
- blitMaskedPic(scratch, reveal,
- kProfilePickerRevealX, kProfilePickerRevealY);
- for (uint i = 0; i < entries.size(); i++) {
- const byte color = ((int)i == sel) ? 0xF : 0x8;
- _font.drawString(&scratch, entries[i].label,
- kListX, kListY + (int)i * kLineH, 220, color);
- }
- copyToScreen(scratch);
- };
+ ProfilePickerView view;
+ view.vm = this;
+ view.bg = &bg;
+ view.haveBG = haveBG;
+ view.reveal = &reveal;
+ view.haveReveal = haveReveal;
+ view.entries = &entries;
+ view.selected = sel;
+ view.start = start;
if (animateProfilePickerReveal(this, &bg, haveBG,
haveReveal ? &reveal : nullptr))
return;
- draw();
+ drawProfilePickerFrame(view);
while (!done && !shouldQuit()) {
Common::Event ev;
@@ -652,10 +713,24 @@ void EEMEngine::doProfilePicker() {
switch (ev.kbd.keycode) {
case Common::KEYCODE_UP:
sel = (sel + (int)entries.size() - 1) % (int)entries.size();
+ clampProfileScroll(sel, start, (int)entries.size());
dirty = true;
break;
case Common::KEYCODE_DOWN:
sel = (sel + 1) % (int)entries.size();
+ clampProfileScroll(sel, start, (int)entries.size());
+ dirty = true;
+ break;
+ case Common::KEYCODE_PAGEUP:
+ start -= kProfileVisibleRows;
+ sel = start;
+ clampProfileScroll(sel, start, (int)entries.size());
+ dirty = true;
+ break;
+ case Common::KEYCODE_PAGEDOWN:
+ start += kProfileVisibleRows;
+ sel = start;
+ clampProfileScroll(sel, start, (int)entries.size());
dirty = true;
break;
case Common::KEYCODE_RETURN:
@@ -663,17 +738,64 @@ void EEMEngine::doProfilePicker() {
committed = true;
break;
case Common::KEYCODE_ESCAPE:
- _playerName = "Detective";
- return;
+ createNew = true;
+ committed = true;
+ break;
default:
break;
}
}
if (ev.type == Common::EVENT_LBUTTONDOWN) {
- const int hit = (ev.mouse.y - kListY) / kLineH;
- if (hit >= 0 && hit < (int)entries.size()) {
- sel = hit;
+ if (kChooserOkRect.contains(ev.mouse.x, ev.mouse.y)) {
committed = true;
+ break;
+ }
+ if (kChooserExitRect.contains(ev.mouse.x, ev.mouse.y) ||
+ kChooserNewPlayerRect.contains(ev.mouse.x, ev.mouse.y)) {
+ createNew = true;
+ committed = true;
+ break;
+ }
+ if (kChooserUpArrowRect.contains(ev.mouse.x, ev.mouse.y)) {
+ if (start > 0) {
+ start--;
+ if (sel >= start + kProfileVisibleRows)
+ sel = start + kProfileVisibleRows - 1;
+ clampProfileScroll(sel, start, (int)entries.size());
+ dirty = true;
+ }
+ break;
+ }
+ if (kChooserDnArrowRect.contains(ev.mouse.x, ev.mouse.y)) {
+ const int maxStart = MAX<int>(0,
+ (int)entries.size() - kProfileVisibleRows);
+ if (start < maxStart) {
+ start++;
+ if (sel < start)
+ sel = start;
+ clampProfileScroll(sel, start, (int)entries.size());
+ dirty = true;
+ }
+ break;
+ }
+ if (kChooserHelpRect.contains(ev.mouse.x, ev.mouse.y)) {
+ // `screen8_handler` sets `Chelp = 0`, so the
+ // original ignores this chooser button here.
+ break;
+ }
+ if (kChooserListRect.contains(ev.mouse.x, ev.mouse.y)) {
+ const int hit = (ev.mouse.y - kProfileListY) /
+ kProfileLineH;
+ const int idx = start + hit;
+ if (hit >= 0 && hit < kProfileVisibleRows &&
+ idx >= 0 && idx < (int)entries.size()) {
+ if (idx == sel) {
+ committed = true;
+ break;
+ }
+ sel = idx;
+ dirty = true;
+ }
}
}
if (committed)
@@ -683,13 +805,21 @@ void EEMEngine::doProfilePicker() {
done = true;
break;
}
- if (dirty)
- draw();
+ if (dirty) {
+ view.selected = sel;
+ view.start = start;
+ drawProfilePickerFrame(view);
+ }
g_system->updateScreen();
g_system->delayMillis(15);
}
- const Entry &e = entries[sel];
+ if (createNew) {
+ doNewPlayer();
+ return;
+ }
+
+ const ProfilePickerEntry &e = entries[sel];
if (e.slot < 0) {
doNewPlayer();
} else {
@@ -1504,12 +1634,13 @@ void EEMEngine::doSetup() {
continue;
}
- // Profile (button [2]). Goes back to the profile picker
- // in the original (`_NextScreen = 8`). Treat the same way
- // as Done for now â switching profiles mid-game isn't
- // wired in our port and would discard mystery state.
+ // Profile (button [2]). Original handler writes
+ // `_NextScreen = 8`, returning to the player/profile
+ // picker. Save the current profile settings first, then
+ // let the screen driver run profile â partner â case/map.
if (kProfileBtn.contains(mx, my)) {
- leaveSetup();
+ saveProfile(_playerName);
+ _nextScreen = kScreenProfile;
return;
}
Commit: a1da71fd4dea62eee29dc3530910a2cb6f07fa99
https://github.com/scummvm/scummvm/commit/a1da71fd4dea62eee29dc3530910a2cb6f07fa99
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:03+02:00
Commit Message:
EEM: missing button code to reload the current profile
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 8a2fab8286a..986ee11a41f 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -1859,8 +1859,8 @@ void EEMEngine::doCaseSelection() {
break;
}
if (kHelpRect.contains(ev.mouse.x, ev.mouse.y)) {
- // HELP placeholder â original calls `_DisplayHint`;
- // our help screen is wired to `H` later in the flow.
+ // `_ActionScreen` sets `Chelp = 0`, so `_DoChoose`
+ // ignores this middle button on the top-level menu.
continue;
}
// List panel: click on a non-separator row selects the
@@ -2074,6 +2074,18 @@ void EEMEngine::doCaseSelection() {
_mystery.clear();
return;
}
+ if (kHelpRect.contains(ev.mouse.x, ev.mouse.y)) {
+ // In original `_CaseSelection`, this button is
+ // PIC 0x123 and returns 0xfffe, which calls
+ // `_ChooseSavedGame`. The ScummVM port stores the
+ // in-progress case in the profile save instead of
+ // original per-mystery files, so route to screen 8
+ // (profile picker) as the load/resume path.
+ saveProfile(_playerName);
+ _mystery.clear();
+ _nextScreen = kScreenProfile;
+ return;
+ }
if (kUpArrowRect.contains(ev.mouse.x, ev.mouse.y)) {
if (topRow > 0) { topRow--; dirty = true; }
continue;
Commit: d3fcc8c249b223d7ff8407e8e0f77237b9edb8aa
https://github.com/scummvm/scummvm/commit/d3fcc8c249b223d7ff8407e8e0f77237b9edb8aa
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:03+02:00
Commit Message:
EEM: implement missing animations for floppy release
Changed paths:
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/mystery.cpp
engines/eem/mystery.h
engines/eem/site.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index dba23490674..12596f8fe42 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -283,6 +283,7 @@ Common::Error EEMEngine::run() {
const int wantedSave = ConfMan.hasKey("save_slot")
? ConfMan.getInt("save_slot") : -1;
bool resumed = false;
+ bool skippedIntro = false;
if (wantedSave >= 0) {
const Common::Error err = loadGameState(wantedSave);
if (err.getCode() == Common::kNoError) {
@@ -362,9 +363,6 @@ Common::Error EEMEngine::run() {
if (!shouldQuit() && !_skipIntro)
playAnm(Common::Path("MOVIE.ANM"), 120,
/*holdLastFrame=*/false);
- if (!shouldQuit() && !_skipIntro)
- playAnm(Common::Path("TITLE.ANM"), 120,
- /*holdLastFrame=*/true);
} else {
showEAKidsLogo();
if (!shouldQuit() && !_skipIntro)
@@ -421,8 +419,14 @@ Common::Error EEMEngine::run() {
playAnm(Common::Path("TITLE.ANM"), 120,
/*holdLastFrame=*/true, /*fadeIn=*/true);
}
+ skippedIntro = _skipIntro;
_skipIntro = false;
+ if (isFloppy() && !shouldQuit() && !skippedIntro) {
+ _nextScreen = kScreenTitle;
+ goto screen_loop;
+ }
+
// After the title chain, the original goes Title (B) -> screen 8
// (NewPlayer / saved-record selection) -> screen 9 (ChoosePartner) ->
// screen A (CaseSelection) -> site loop. We mirror the same order.
@@ -488,6 +492,22 @@ screen_loop:
debugC(1, kDebugGeneral, "screenDriver: id=%d", (int)current);
switch (current) {
+ case kScreenTitle:
+ // Floppy handler 0xb (`_HandleScreen11_Title_Floppy`) calls
+ // `_DoTitle_Floppy`, whose `_PlayTitleANM_Floppy(1)` file
+ // table entry is `TITLE.ANM`. The opening driver stops after
+ // `MOVIE.ANM`; this live screen owns the title wait and then
+ // writes `_NextScreen = 8` for the profile picker.
+ _nextScreen = kScreenProfile;
+ if (isFloppy()) {
+ CursorMan.showMouse(false);
+ playAnm(Common::Path("TITLE.ANM"), 120,
+ /*holdLastFrame=*/true, /*fadeIn=*/true);
+ _skipIntro = false;
+ CursorMan.showMouse(true);
+ }
+ break;
+
case kScreenAction:
// Post-mystery menu. The original's `_ActionScreen @
// 1c33:195b` shows the 5-entry "Choose A Mystery /
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index ae1a6de44d8..44c5bc4a905 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -71,10 +71,10 @@ class MusicPlayer;
* 10 (0xa) CHOOSE_MYSTERY â `_DoChooseMystery` + `_CaseSelection`;
* starts with _NextScreen=0 so a successful pick
* falls through to INIT_CLUES.
- * 11 (0xb) TITLE â set _NextScreen=8 then dispatch (TITLE.ANM is
- * actually shown earlier by `_DoOpeningAnims`, this
- * handler is the post-intro "fall into PROFILE"
- * redirect)
+ * 11 (0xb) TITLE â floppy `_DoTitle_Floppy` plays TITLE.ANM, waits
+ * for input, then writes =8. CD shows TITLE.ANM in
+ * `_DoOpeningAnims`, so it usually never enters this
+ * handler.
* 12 (0xc) ACTION â `_ActionScreen` @ 1c33:195b â post-mystery menu
* ("Solve a Mystery", scrapbook, more mysteries,
* setup). Action 1 sets =10 (CHOOSE_MYSTERY).
diff --git a/engines/eem/mystery.cpp b/engines/eem/mystery.cpp
index efbada6cab1..c6079dac3f7 100644
--- a/engines/eem/mystery.cpp
+++ b/engines/eem/mystery.cpp
@@ -48,6 +48,8 @@ void Mystery::clear() {
_floppySuspectsOff = _floppyHintBlockOff = _floppyNoteIndexOff = 0;
_floppyGalleryOff = _floppyTextOff = _floppyKDTextOff = 0;
_floppySolvedOff = 0;
+ _floppySiteAnimData.clear();
+ memset(_floppySiteAnimSiteOff, 0, sizeof(_floppySiteAnimSiteOff));
memset(_aChain, 0, sizeof(_aChain));
memset(_bChain, 0, sizeof(_bChain));
memset(_cChain, 0, sizeof(_cChain));
@@ -214,6 +216,7 @@ bool Mystery::load(uint num, Common::RandomSource *rng) {
_firstTry = true;
_searchLocationNumber = _siteNumber = 0xFFFF;
_lastSite = 0x1B;
+ loadFloppySiteAnimData();
debugC(1, kDebugMystery,
"Mystery::load(%u) floppy: sites=0x%04x siteIdx=0x%04x "
@@ -310,6 +313,74 @@ const byte *Mystery::siteData(uint siteNum) const {
return _data.data() + dataOff;
}
+const byte *Mystery::floppySiteAnimData(uint siteNum) const {
+ if (!_isFloppy || siteNum >= kVisitedSiteCap)
+ return nullptr;
+ const uint16 off = _floppySiteAnimSiteOff[siteNum];
+ if (off == 0 || off >= _floppySiteAnimData.size())
+ return nullptr;
+ return _floppySiteAnimData.data() + off;
+}
+
+void Mystery::loadFloppySiteAnimData() {
+ _floppySiteAnimData.clear();
+ memset(_floppySiteAnimSiteOff, 0, sizeof(_floppySiteAnimSiteOff));
+
+ Common::File f;
+ if (!f.open(Common::Path("ANI.BIN"))) {
+ warning("Mystery::loadFloppySiteAnimData: ANI.BIN missing");
+ return;
+ }
+
+ const int32 size = f.size();
+ if (size <= 0 || size > 0xFFFF) {
+ warning("Mystery::loadFloppySiteAnimData: invalid ANI.BIN size %d",
+ size);
+ return;
+ }
+
+ _floppySiteAnimData.resize((uint)size);
+ if (f.read(_floppySiteAnimData.data(), (uint32)size) != (uint32)size) {
+ warning("Mystery::loadFloppySiteAnimData: short ANI.BIN read");
+ _floppySiteAnimData.clear();
+ return;
+ }
+
+ const uint tableOff = _number * 2;
+ if (tableOff + 2 > _floppySiteAnimData.size())
+ return;
+ uint32 pos = READ_LE_UINT16(_floppySiteAnimData.data() + tableOff);
+ if (pos == 0 || pos >= _floppySiteAnimData.size())
+ return;
+
+ for (uint site = 0; site < _numSites && site < kVisitedSiteCap; site++) {
+ const uint32 start = pos;
+ if (pos + 1 > _floppySiteAnimData.size())
+ break;
+ const uint cycles = _floppySiteAnimData[pos++];
+ if (pos + cycles * 2 + 1 > _floppySiteAnimData.size()) {
+ warning("Mystery::loadFloppySiteAnimData: malformed cycles "
+ "for mystery %u site %u", _number, site);
+ break;
+ }
+ pos += cycles * 2;
+
+ const uint anims = _floppySiteAnimData[pos++];
+ if (pos + anims * 4 > _floppySiteAnimData.size()) {
+ warning("Mystery::loadFloppySiteAnimData: malformed anims "
+ "for mystery %u site %u", _number, site);
+ break;
+ }
+ _floppySiteAnimSiteOff[site] = (uint16)start;
+ pos += anims * 4;
+ }
+
+ debugC(1, kDebugMystery,
+ "Mystery::loadFloppySiteAnimData(%u): base=0x%04x sites=%u",
+ _number, READ_LE_UINT16(_floppySiteAnimData.data() + tableOff),
+ _numSites);
+}
+
const byte *Mystery::hotspots(uint siteNum) const {
if (_isFloppy) {
// Floppy: hotspot table sits inside the per-site sub-blob.
diff --git a/engines/eem/mystery.h b/engines/eem/mystery.h
index c9694d1965e..eed403b0bdb 100644
--- a/engines/eem/mystery.h
+++ b/engines/eem/mystery.h
@@ -131,6 +131,11 @@ public:
/// referenced by SiteIndex[@p siteNum].
const byte *siteData(uint siteNum) const;
+ /// Floppy-only pointer to the matching `ANI.BIN` per-site animation
+ /// block. Layout: u8 cycleCount, cycleCount à {u8 start, u8 end},
+ /// u8 animCount, animCount à {u8 animId, u16 x, u8 y}.
+ const byte *floppySiteAnimData(uint siteNum) const;
+
/// Pointer to the hotspot rectangle array for site @p siteNum.
/// Each rect is 14 bytes: x1, y1, x2, y2, then 6 bytes of clue data.
const byte *hotspots(uint siteNum) const;
@@ -236,8 +241,11 @@ private:
uint16 _floppyTextOff = 0; ///< header[+0xc] â text block
uint16 _floppyKDTextOff = 0; ///< header[+0x10] â KDTextIndex
uint16 _floppySolvedOff = 0; ///< header[+0x12] â solved clue chain
+ Common::Array<byte> _floppySiteAnimData;
+ uint16 _floppySiteAnimSiteOff[kVisitedSiteCap] = {};
uint16 readU16(uint offset) const;
+ void loadFloppySiteAnimData();
};
} // End of namespace EEM
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index a5051688000..f48d44dc276 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -1083,7 +1083,7 @@ void SiteScreen::renderAnimatedDrops(uint siteNum, uint32 tickMs) {
// per entry at siteData[+0x48 + i*6]: {animId, x, y}
// animId == -1 â `_ColorCycle(x, y)` palette range (handled
// in the run() loop's frame pump as palette
- // rotation; not yet implemented).
+ // rotation).
// else â `_GetAnimation(animId)` + `_NewAnimation` then
// `_UpdateAnimations @ 172b:09c1` walks a sequence
// script (entries are frame indices; 0x80 = end-of-loop,
@@ -1092,6 +1092,49 @@ void SiteScreen::renderAnimatedDrops(uint siteNum, uint32 tickMs) {
// raw animation frames in order using a global tick.
if (!_mystery)
return;
+
+ if (_vm && _vm->isFloppy()) {
+ // Floppy extra site anims live in `ANI.BIN`, not in the
+ // mystery's site_data. `_ReadMystery_Floppy` asks
+ // `_GetSiteAnimData_Floppy(mystery)` for the case block, then
+ // walks one entry per site:
+ // u8 cycleCount, cycleCount à {u8 start, u8 end},
+ // u8 animCount, animCount à {u8 animId, u16 x, u8 y}.
+ // `_DoSiteLoop_Floppy` registers at most four of these in local
+ // animation slots, so cap the draw loop the same way.
+ const byte *siteAnim = _mystery->floppySiteAnimData(siteNum);
+ if (!siteAnim)
+ return;
+ const uint cycles = siteAnim[0];
+ const byte *animList = siteAnim + 1 + cycles * 2;
+ const uint animCount = animList[0];
+ animList++;
+ if (animCount == 0)
+ return;
+
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (!screen)
+ return;
+
+ const uint maxAnims = MIN<uint>(animCount, 4);
+ for (uint i = 0; i < maxAnims; i++) {
+ const byte *e = animList + i * 4;
+ const uint animId = e[0];
+ const int16 x = (int16)READ_LE_UINT16(e + 1);
+ const int16 y = (int16)e[3];
+ Animation anim;
+ if (!_vm->getAni().loadAnimation(animId, anim) || anim.empty())
+ continue;
+ const uint frameIdx = partnerFrameAtTick((uint16)animId,
+ (uint)anim.size(),
+ tickMs);
+ blitAnimFrameAnchored(screen, anim[frameIdx], x, y);
+ }
+
+ g_system->unlockScreen();
+ return;
+ }
+
const byte *site = _mystery->siteData(siteNum);
if (!site)
return;
@@ -1136,16 +1179,22 @@ void SiteScreen::scanColorCycles(uint siteNum) {
_colorCycles.clear();
if (!_mystery)
return;
- // Floppy site data has a different layout (per `_DoSiteLoop_Floppy
- // @ 1652:03a3`: site index is 2-byte u16 entries, site data starts
- // with an offset to a sub-structure with picID + 5-byte hotspot
- // entries â there's no `[+0xa]` anim count at the same place). The
- // CD-shaped offsets read garbage and run past the buffer end. Until
- // the floppy color-cycle layout is reverse-engineered, skip the
- // scan: the floppy still does palette F9..FE rotation in its own
- // driver, but our hotspot palette override works without it.
- if (_vm && _vm->isFloppy())
+
+ if (_vm && _vm->isFloppy()) {
+ const byte *siteAnim = _mystery->floppySiteAnimData(siteNum);
+ if (!siteAnim)
+ return;
+ const uint cycles = siteAnim[0];
+ for (uint i = 0; i < cycles && _colorCycles.size() < 5; i++) {
+ ColorCycleRange r;
+ r.start = siteAnim[1 + i * 2];
+ r.end = siteAnim[1 + i * 2 + 1];
+ if (r.end > r.start)
+ _colorCycles.push_back(r);
+ }
return;
+ }
+
const byte *site = _mystery->siteData(siteNum);
if (!site)
return;
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 986ee11a41f..eeb01b38ac1 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -134,6 +134,19 @@ bool gallerySlotAt(const Common::Array<Common::Rect> &rects,
return false;
}
+const byte *advanceFloppyDialogRecords(const byte *rec, uint count,
+ const byte *end) {
+ for (uint i = 0; i < count; i++) {
+ if (!rec || rec + 11 > end)
+ return nullptr;
+ const uint len = 11u + rec[10];
+ if (rec + len > end)
+ return nullptr;
+ rec += len;
+ }
+ return rec;
+}
+
// Floppy gallery slot positions verified at `2608:0x16c` (5 Ã
// {u16 x, u16 y}) â read by `_DrawGallery_Floppy @ 154e:0045`'s
// `[BX + 0x16c]` (x) and `[BX + 0x16e]` (y) loads. The floppy
@@ -5300,8 +5313,8 @@ void EEMEngine::doAccuseFloppy() {
// 1d40:08c0 _MIDIPlayFile("travel-2.xmi");
// 1d40:08d0 walk solved chain via _DisplayHotspotClue_Floppy
// + _WaitForClick per record. Mid-recap: when
- // only 3 records remain, play TITLE.ANM(0)
- // (transition graphic; not yet ported).
+ // only 3 records remain, play
+ // _PlayTitleANM_Floppy(0) = SCRAPBK.ANI.
// 1d40:0939 ((u16 *)0x3054)[mysteryNum] =
// _firstTry ? 2 : 1;
// 1d40:0941 tier-promotion check (advance _chainStage when
@@ -5381,11 +5394,34 @@ void EEMEngine::doAccuseFloppy() {
// Walk the solved-clue chain. Header[+0x12] points at a
// `count` byte followed by `count` dialog records (same layout
- // as hotspot dialogs).
+ // as hotspot dialogs). When only three records remain, the
+ // original clears animation slots and calls
+ // `_PlayTitleANM_Floppy(0)`. The title helper's file table maps
+ // index 0 to `SCRAPBK.ANI` and index 1 to `TITLE.ANM`, so this
+ // is the same scrapbook flip transition used by the CD win flow,
+ // just inserted before the last three floppy recap records.
const byte *chain = _mystery.solvedClueBlock();
if (chain) {
const uint count = chain[0];
- displayFloppyDialogRecords(chain + 1, count, 0);
+ const byte *records = chain + 1;
+ const byte *end = bufBase + mysSize;
+ if (count > 3) {
+ const uint beforeScrapbook = count - 3;
+ const byte *tail =
+ advanceFloppyDialogRecords(records, beforeScrapbook,
+ end);
+ if (tail) {
+ displayFloppyDialogRecords(records, beforeScrapbook, 1);
+ playAnm(Common::Path("SCRAPBK.ANI"), 120,
+ /*holdLastFrame=*/false, /*fadeIn=*/true);
+ displayFloppyDialogRecords(tail, 3, 1);
+ } else {
+ warning("doAccuseFloppy: malformed solved chain");
+ displayFloppyDialogRecords(records, count, 1);
+ }
+ } else {
+ displayFloppyDialogRecords(records, count, 1);
+ }
}
if (_music && _voiceOn)
_music->stop();
Commit: 2e25ab2f46f3701803e1dbd02f20cf1fb3515b84
https://github.com/scummvm/scummvm/commit/2e25ab2f46f3701803e1dbd02f20cf1fb3515b84
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:04+02:00
Commit Message:
EEM: corrected text size
Changed paths:
engines/eem/clues.cpp
engines/eem/font.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 708754fbd11..2d72d4e45b1 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -1107,7 +1107,7 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
getBalloonInsets(balloonId, textXIns, textYIns, textWidth);
const int textX = ballX + textXIns;
const int balloonH = haveBalloon ? balloon.surface.h : 200;
- const int lineH = _font.getFontHeight() + 1;
+ const int lineH = _font.getFontHeight();
// Pagination state â `FUN_22dc_05c8`'s text-idx loop uses
// `local_1c` (set from the PREVIOUS text's flag bit) to decide
diff --git a/engines/eem/font.cpp b/engines/eem/font.cpp
index c8c42360550..36d584c62b5 100644
--- a/engines/eem/font.cpp
+++ b/engines/eem/font.cpp
@@ -130,7 +130,7 @@ int EEMFont::drawWordWrapped(Graphics::ManagedSurface *dst, int x, int y,
uint32 color) const {
Common::Array<Common::String> lines;
wordWrapText(s, width, lines);
- const int lineH = getFontHeight() + 1;
+ const int lineH = getFontHeight();
for (uint i = 0; i < lines.size(); i++)
drawString(dst, lines[i], x, y + (int)i * lineH, width, color);
return (int)lines.size() * lineH;
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index eeb01b38ac1..61854c997bf 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -2511,7 +2511,7 @@ void EEMEngine::drawNotebookFrame(int &page) {
_playerName, _partner);
};
{
- const int lineH = _font.getFontHeight() + 1;
+ const int lineH = _font.getFontHeight();
int y = kRectY;
while (clueCursor < (int)found.size()) {
const uint clueId = found[clueCursor];
@@ -2559,7 +2559,7 @@ void EEMEngine::drawNotebookFrame(int &page) {
// matches the original design.
Common::Array<Common::String> wrapped;
_font.wordWrapText(txt, kRectW, wrapped);
- const int lineH = _font.getFontHeight() + 1;
+ const int lineH = _font.getFontHeight();
const int h = (int)wrapped.size() * lineH;
const byte color = _mystery._noteSelected[clueId] ? 0x3C : 0x5C;
for (uint li = 0; li < wrapped.size(); li++) {
@@ -2833,7 +2833,7 @@ void EEMEngine::doGallery() {
const byte *ni = _mystery.noteIndex();
const uint16 niCount = _mystery.noteIndexCount();
int yPos = ry;
- const int lineH = _font.getFontHeight() + 1;
+ const int lineH = _font.getFontHeight();
bool drewAny = false;
const uint clueMax = floppyMI ? clueCount : 30u;
for (uint k = 0; k < clueCount && k < clueMax; k++) {
@@ -3832,7 +3832,7 @@ bool EEMEngine::doAccuseNotes() {
auto rebuildPagination = [&]() {
numPages = 1;
pageBreaks[0] = 0;
- const int lineH = _font.getFontHeight() + 1;
+ const int lineH = _font.getFontHeight();
int y = rectY;
for (uint i = 0; i < found.size(); i++) {
const uint clueId = found[i];
@@ -3888,7 +3888,7 @@ bool EEMEngine::doAccuseNotes() {
// "accuse-mode" look together with PIC 0x1A7.
slotRects.clear();
slotClues.clear();
- const int lineH = _font.getFontHeight() + 1;
+ const int lineH = _font.getFontHeight();
const int startIdx = pageBreaks[page];
const int endIdx = (page + 1 < numPages)
? pageBreaks[page + 1]
Commit: e84bf0e7981aeae007259442c6d73c90f4297919
https://github.com/scummvm/scummvm/commit/e84bf0e7981aeae007259442c6d73c90f4297919
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:04+02:00
Commit Message:
EEM: keep proper track of clues in floppy version
Changed paths:
engines/eem/mystery.cpp
engines/eem/mystery.h
engines/eem/ui.cpp
diff --git a/engines/eem/mystery.cpp b/engines/eem/mystery.cpp
index c6079dac3f7..5982e5c5fc5 100644
--- a/engines/eem/mystery.cpp
+++ b/engines/eem/mystery.cpp
@@ -464,6 +464,23 @@ uint16 Mystery::noteIndexCount() const {
return (uint16)((_galleryOffset - _noteOffset) / stride);
}
+bool Mystery::noteHasNotebookText(uint clueId) const {
+ const byte *ni = noteIndex();
+ const uint16 cnt = noteIndexCount();
+ if (!ni || clueId >= cnt)
+ return false;
+
+ if (!_isFloppy)
+ return true;
+
+ // `_DrawNotes_Floppy @ 15e0:01e8` first checks `_TextSeen[idx]`,
+ // then skips the row when `*(u16 *)(notes + idx * 7) == 0`.
+ // Many floppy dialog records are spoken-only lines: they must still
+ // be marked seen so site dialog does not repeat, but they are not
+ // notebook clues and should not render fallback "note N" labels.
+ return READ_LE_UINT16(ni + clueId * 7) != 0;
+}
+
const byte *Mystery::kdTextIndex() const {
if (!isLoaded() || _kdTextOffset >= _data.size())
return nullptr;
diff --git a/engines/eem/mystery.h b/engines/eem/mystery.h
index eed403b0bdb..8ef1c584715 100644
--- a/engines/eem/mystery.h
+++ b/engines/eem/mystery.h
@@ -104,6 +104,12 @@ public:
/// Number of entries in NoteIndex.
uint16 noteIndexCount() const;
+ /// True when @p clueId has a visible notebook/accuse text entry.
+ /// Floppy dialog text indices may be spoken-only records with a
+ /// zero notebook text offset; those are marked seen but skipped by
+ /// `_DrawNotes_Floppy`.
+ bool noteHasNotebookText(uint clueId) const;
+
/// Pointer to the KDTextIndex; first u16s are TextBlock offsets for
/// host hint lines.
const byte *kdTextIndex() const;
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 61854c997bf..2493117b0a2 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -2455,7 +2455,7 @@ void EEMEngine::drawNotebookFrame(int &page) {
// original's iteration through `_CluesFound[]`.
Common::Array<uint> found;
for (uint i = 0; i < Mystery::kCluesFoundCap; i++) {
- if (_mystery._cluesFound[i])
+ if (_mystery._cluesFound[i] && _mystery.noteHasNotebookText(i))
found.push_back(i);
}
const byte *ni = _mystery.noteIndex();
@@ -3762,7 +3762,7 @@ bool EEMEngine::doAccuseNotes() {
// the same way).
Common::Array<uint> found;
for (uint i = 0; i < niCount && i < Mystery::kCluesFoundCap; i++) {
- if (_mystery._cluesFound[i])
+ if (_mystery._cluesFound[i] && _mystery.noteHasNotebookText(i))
found.push_back(i);
}
Commit: 821f9a0ba79a98e7a24744164a5e6dc3503f59d5
https://github.com/scummvm/scummvm/commit/821f9a0ba79a98e7a24744164a5e6dc3503f59d5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:04+02:00
Commit Message:
EEM: keep proper track of visited places in floppy version
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 2493117b0a2..fed7083fb99 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -55,6 +55,43 @@ const GallerySlot kGallerySlots[5] = {
{ 191, 90 } // 4
};
+byte mapVisitedMarkerColor(byte color) {
+ switch (color) {
+ case 0xf7:
+ case 0xfb:
+ case 0xfd:
+ return 0x1b;
+ case 0xf8:
+ case 0xf9:
+ case 0xfa:
+ case 0xfc:
+ case 0xfe:
+ return 0x19;
+ default:
+ return color;
+ }
+}
+
+void blitBigMapMarker(Graphics::ManagedSurface &dstSurface, const Picture &marker,
+ int x, int y, bool useVisitedColors) {
+ const byte transp = (byte)(marker.flags >> 8);
+ for (int row = 0; row < marker.surface.h; row++) {
+ const int dstY = y + row;
+ if (dstY < 0 || dstY >= dstSurface.h)
+ continue;
+ const byte *src = (const byte *)marker.surface.getBasePtr(0, row);
+ byte *dst = (byte *)dstSurface.getBasePtr(0, dstY);
+ for (int col = 0; col < marker.surface.w; col++) {
+ const int dstX = x + col;
+ if (dstX < 0 || dstX >= dstSurface.w)
+ continue;
+ if (src[col] != transp)
+ dst[dstX] = useVisitedColors ? mapVisitedMarkerColor(src[col])
+ : src[col];
+ }
+ }
+}
+
constexpr Common::Rect kEndingPrevPageRect(Common::Point(0, 0), 28, 200);
constexpr Common::Rect kEndingNextPageRect(Common::Point(292, 0), 28, 200);
constexpr uint16 kFloppyEndingBackgroundPic = 0x8b;
@@ -3499,6 +3536,9 @@ void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
// _DoneMarker = PIC 0x20d (already-searched site)
// _SiteMarker = PIC 0xc5 (default available site)
// _CrimeMarker = PIC 0xc6 (crime-scene flag set)
+ // Floppy's `PICS.DBX` has 524 entries, so PIC 0x20d is CD-only.
+ // Its pixels are the same 7x8 marker as PIC 0xc5, with the animated
+ // 0xfb/0xfc interior remapped to static 0x1b/0x19 blue.
Picture done;
Picture normal;
Picture crimeM;
@@ -3530,30 +3570,20 @@ void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
&& _mystery._visitedSite[i];
const Picture *m = nullptr;
+ bool useVisitedColors = false;
if (done_ && haveDone)
m = &done;
- else if (crime != 0 && haveCrime)
+ else if (done_ && haveNormal) {
+ m = &normal;
+ useVisitedColors = true;
+ } else if (crime != 0 && haveCrime)
m = &crimeM;
else if (haveNormal)
m = &normal;
if (m) {
- // Masked-blit the marker PIC.
- const byte transp = (byte)(m->flags >> 8);
- for (int row = 0; row < m->surface.h; row++) {
- const int dstY = (int)my + row;
- if (dstY < 0 || dstY >= 200)
- continue;
- const byte *src = (const byte *)m->surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, dstY);
- for (int col = 0; col < m->surface.w; col++) {
- const int dstX = (int)mx + col;
- if (dstX < 0 || dstX >= 320)
- continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
+ blitBigMapMarker(scratch, *m, (int)mx, (int)my,
+ useVisitedColors);
} else {
// Fallback if the markers couldn't be loaded.
const Common::Rect mark(mx - 3, my - 3, mx + 4, my + 4);
Commit: f00c88734cb88901f496fb46d4c0a281939b6d73
https://github.com/scummvm/scummvm/commit/f00c88734cb88901f496fb46d4c0a281939b6d73
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:05+02:00
Commit Message:
EEM: blit accuse partner in floppy version
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index fed7083fb99..88aa3939004 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -92,6 +92,21 @@ void blitBigMapMarker(Graphics::ManagedSurface &dstSurface, const Picture &marke
}
}
+void blitAccusePartner(Graphics::ManagedSurface &dstSurface,
+ DBDArchive &aniArchive, uint8 partner,
+ uint32 tickMs) {
+ const uint partnerAnim = (partner == 0) ? 2 : 0x10;
+ Animation partnerAni;
+ if (aniArchive.loadAnimation(partnerAnim, partnerAni) &&
+ !partnerAni.empty()) {
+ const uint frameIdx = partnerFrameAtTick(0x02,
+ (uint)partnerAni.size(),
+ tickMs);
+ blitAnimFrameAnchored(dstSurface.surfacePtr(),
+ partnerAni[frameIdx], 5, 0x50);
+ }
+}
+
constexpr Common::Rect kEndingPrevPageRect(Common::Point(0, 0), 28, 200);
constexpr Common::Rect kEndingNextPageRect(Common::Point(292, 0), 28, 200);
constexpr uint16 kFloppyEndingBackgroundPic = 0x8b;
@@ -5654,6 +5669,11 @@ void EEMEngine::doAccuseFloppy() {
_font.drawWordWrapped(&scene, balloonX + tx, balloonY + ty,
MAX<int>(8, (int)tw), alibi, 0);
}
+ // The floppy original keeps the accuse partner animation slot alive
+ // across `_DisplayAlibi_Floppy` and restores the screen with active
+ // animations between alibi phases. Stamp the same resting frame here
+ // before the KD reaction helper snapshots the current screen.
+ blitAccusePartner(scene, _aniArchive, _partner, g_system->getMillis());
g_system->copyRectToScreen(scene.getPixels(), scene.pitch, 0, 0, 320, 200);
g_system->updateScreen();
Commit: f2d1372a85de0c8056c30a4a0fb5e97f7aedb432
https://github.com/scummvm/scummvm/commit/f2d1372a85de0c8056c30a4a0fb5e97f7aedb432
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:05+02:00
Commit Message:
EEM: use the correct initial menu screens
Changed paths:
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/ui.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 12596f8fe42..da33f550739 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -68,7 +68,7 @@ const bool kDebugPopulateScrapbook1AtStartup = false;
// Fallback 11x16 mouse cursor used if the selected PIC pointer cannot be
// loaded. The original game sets the cursor visible/hidden via
// _MouseCursor; we leave it on once the screens that need it
-// (ChoosePartner, CaseSelection, sites) are reached.
+// (ChoosePartner, ActionScreen, CaseSelection, sites) are reached.
// 0 = transparent, 1 = black outline, 2 = white fill
const byte kCursorBitmap[11 * 16] = {
1,1,0,0,0,0,0,0,0,0,0,
@@ -276,10 +276,8 @@ Common::Error EEMEngine::run() {
//
// * Save HAS a mystery in progress â resume at MAP (mirrors the
// original's post-briefing state, handler 0 at 1a35:0e1d).
- // * Save has NO mystery â drop into the case-selection screen
- // (`kScreenChooseMystery`) so the player can pick which case
- // to play. This matches what the original `_ActionScreen`
- // leads to â without the redundant action menu in front.
+ // * Save has NO mystery â drop into `_ActionScreen`, same as the
+ // original after partner selection.
const int wantedSave = ConfMan.hasKey("save_slot")
? ConfMan.getInt("save_slot") : -1;
bool resumed = false;
@@ -297,8 +295,8 @@ Common::Error EEMEngine::run() {
} else {
debugC(1, kDebugGeneral,
"Resuming profile from slot %d (no mystery â "
- "â case selection)", wantedSave);
- _nextScreen = kScreenChooseMystery;
+ "â action screen)", wantedSave);
+ _nextScreen = kScreenAction;
}
resumed = true;
}
@@ -429,7 +427,8 @@ Common::Error EEMEngine::run() {
// After the title chain, the original goes Title (B) -> screen 8
// (NewPlayer / saved-record selection) -> screen 9 (ChoosePartner) ->
- // screen A (CaseSelection) -> site loop. We mirror the same order.
+ // screen C (ActionScreen). Choosing a mystery there enters screen A
+ // (CaseSelection) and then the site loop.
// Mouse stays hidden through the opening anims; show it now for
// the interactive screens (matches `_MouseCursor = 1` at the tail
// of `_NewPlayer`).
@@ -463,17 +462,9 @@ Common::Error EEMEngine::run() {
//
// `_DoChoosePartner @ 1a35:099d` sets `_NextScreen = 0xc` (= the
// original `_ActionScreen` â "Choose A Mystery / Practice Mystery /
- // See ScrapBook 1..3"). In our port that menu lives inside
- // `doCaseSelection`, which already mirrors `_ActionScreen`'s 5-entry
- // list (verified against `ActionNames @ 29be:0d6a`) and rolls the
- // individual-mystery picker (`_CaseSelection @ 1c33:0a87`) into the
- // same flow. So we route straight to `kScreenChooseMystery`; the
- // `kScreenAction` (= 0xc) state still exists for the post-win path
- // (`_DisplayCorrect @ 1df2:0895` writes 0xc) but its handler also
- // dispatches `doCaseSelection`. The previous `doActionScreen` was a
- // synthetic stub ("What now?" / "Solve a Mystery" / "Look at My
- // Books") whose strings are nowhere in the binary â confirmed by a
- // `search_strings` for "What" returning zero matches.
+ // See ScrapBook 1..3"). That screen is separate from handler 10's
+ // `_DoChooseMystery` / `_CaseSelection`, which is where the "Book N"
+ // title is drawn.
//
// Mid-mystery profile resume: if the profile picker loaded a
// save whose `hasMystery` flag was set, `_mystery.isLoaded()` is
@@ -485,7 +476,7 @@ Common::Error EEMEngine::run() {
// profile-level state via `_PlayerRecord`, not in-progress
// mysteries â so this is a ScummVM-only ergonomics improvement.
if (!shouldQuit() && !resumed)
- _nextScreen = _mystery.isLoaded() ? kScreenMap : kScreenChooseMystery;
+ _nextScreen = _mystery.isLoaded() ? kScreenMap : kScreenAction;
screen_loop:
while (!shouldQuit() && _nextScreen != kScreenInvalid) {
const ScreenId current = (ScreenId)_nextScreen;
@@ -509,28 +500,23 @@ screen_loop:
break;
case kScreenAction:
- // Post-mystery menu. The original's `_ActionScreen @
- // 1c33:195b` shows the 5-entry "Choose A Mystery /
- // Practice / ScrapBook" picker; `doCaseSelection` is
- // our port of that exact menu (plus the individual-case
- // sub-picker the original handles in `_CaseSelection @
- // 1c33:0a87`). After the player picks, fall through to
- // the same routing the `kScreenChooseMystery` case uses.
- // Reachable from `_DisplayCorrect`'s 0xc write after a
- // solve (see `ui.cpp` `_nextScreen = kScreenAction`).
+ // Top-level post-profile / post-mystery menu. `_ActionScreen
+ // @ 1c33:195b` shows the 5-entry "Choose A Mystery /
+ // Practice / ScrapBook" picker and writes screen 0xa only
+ // when the player picks "Choose A Mystery".
_nextScreen = kScreenInvalid;
- doCaseSelection();
- if (_mystery.isLoaded())
+ doActionScreen();
+ if (_nextScreen == kScreenInvalid && _mystery.isLoaded())
_nextScreen = kScreenInitClues;
break;
case kScreenChooseMystery:
// Handler 10 at 1a35:0e0e calls `_DoChooseMystery` which
// presets `_NextScreen = 0` (INIT_CLUES) before
- // `_CaseSelection`. Same dispatch as `kScreenAction`.
+ // `_CaseSelection`.
_nextScreen = kScreenInvalid;
doCaseSelection();
- if (_mystery.isLoaded())
+ if (_nextScreen == kScreenInvalid && _mystery.isLoaded())
_nextScreen = kScreenInitClues;
break;
@@ -539,7 +525,7 @@ screen_loop:
// then writes `_NextScreen = 1` (MAP).
doInitClues();
_nextScreen = _mystery.isLoaded() ? kScreenMap
- : kScreenChooseMystery;
+ : kScreenAction;
break;
case kScreenMap:
@@ -553,7 +539,7 @@ screen_loop:
// the natural next state is SITE.
doBigMap();
if (!_mystery.isLoaded())
- _nextScreen = kScreenChooseMystery;
+ _nextScreen = kScreenAction;
else if (_nextScreen == current)
_nextScreen = kScreenSite;
break;
@@ -565,7 +551,7 @@ screen_loop:
// than entering those screens as nested modals.
doSiteLoop();
if (!_mystery.isLoaded())
- _nextScreen = kScreenChooseMystery;
+ _nextScreen = kScreenAction;
else if (_nextScreen == current)
_nextScreen = kScreenInvalid; // user quit
break;
@@ -576,7 +562,7 @@ screen_loop:
// (accuse) and then returns to this dispatcher.
doNotebook();
if (!_mystery.isLoaded() && _nextScreen != kScreenAction)
- _nextScreen = kScreenChooseMystery;
+ _nextScreen = kScreenAction;
else if (_nextScreen == current)
_nextScreen = kScreenSite;
break;
@@ -587,7 +573,7 @@ screen_loop:
// 2, and the PDA button writes 4.
doGallery();
if (!_mystery.isLoaded() && _nextScreen != kScreenAction)
- _nextScreen = kScreenChooseMystery;
+ _nextScreen = kScreenAction;
else if (_nextScreen == current)
_nextScreen = kScreenSite;
break;
@@ -620,7 +606,7 @@ screen_loop:
doChoosePartner();
if (!shouldQuit())
_nextScreen = _mystery.isLoaded() ? kScreenMap
- : kScreenChooseMystery;
+ : kScreenAction;
break;
case kScreenAccuse:
@@ -629,7 +615,7 @@ screen_loop:
// 0xc (ACTION).
doAccuse();
if (!_mystery.isLoaded() && _nextScreen != kScreenAction)
- _nextScreen = kScreenChooseMystery;
+ _nextScreen = kScreenAction;
else if (_nextScreen == current)
_nextScreen = kScreenSite;
break;
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 44c5bc4a905..bc63f993db1 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -75,9 +75,9 @@ class MusicPlayer;
* for input, then writes =8. CD shows TITLE.ANM in
* `_DoOpeningAnims`, so it usually never enters this
* handler.
- * 12 (0xc) ACTION â `_ActionScreen` @ 1c33:195b â post-mystery menu
- * ("Solve a Mystery", scrapbook, more mysteries,
- * setup). Action 1 sets =10 (CHOOSE_MYSTERY).
+ * 12 (0xc) ACTION â `_ActionScreen` @ 1c33:195b â Choose A Mystery /
+ * Practice Mystery / See ScrapBook 1..3. Action 1
+ * sets =10 (CHOOSE_MYSTERY).
* 0xFFFF SENTINEL â exit loop
*
* Screen-driver state writes verified via xrefs to `_NextScreen @
@@ -451,6 +451,7 @@ private:
/// action-menu "See ScrapBook 1/2/3" entries.
void doShowScrapbook(uint stage);
+ void doActionScreen();
void doCaseSelection();
void doSiteLoop();
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 88aa3939004..d7986c80c89 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -147,6 +147,13 @@ constexpr int kNameEntryPeekY = 0xb3;
constexpr uint16 kCaseSelectionRevealPic = 0x53;
constexpr int kCaseSelectionRevealX = 0x3e;
constexpr int kCaseSelectionRevealY = 0xb2;
+constexpr uint16 kActionScreenBackgroundPic = 0x104;
+constexpr uint16 kActionScreenDecorPic = 0x0009;
+constexpr int kActionScreenDecorX = 10;
+constexpr int kActionScreenDecorY = 0x87;
+constexpr byte kChooserCycleStart = 0x6f;
+constexpr byte kChooserCycleEnd = 0x73;
+constexpr uint32 kChooserCycleMillis = 100;
bool notebookButtonAt(int x, int y) {
return kPdaHelpRect.contains(x, y) ||
@@ -244,6 +251,13 @@ void copyToScreen(Graphics::ManagedSurface &scratch) {
g_system->updateScreen();
}
+void cycleChooserPalette() {
+ // `_DoChoose` / `_DoListPicker_Floppy` rotate 0x6f..0x73 on each
+ // frame tick while waiting for input. These colors are used by the
+ // animated chooser surface baked into the menu backgrounds.
+ cyclePaletteRange(kChooserCycleStart, kChooserCycleEnd);
+}
+
void blitMaskedPicSlice(Graphics::ManagedSurface &dst, const Picture &pic,
int srcX, int srcY, int w, int h,
int dstX, int dstY) {
@@ -431,25 +445,19 @@ void drawProfilePickerFrame(const ProfilePickerView &v) {
copyToScreen(scratch);
}
-// Snapshot of `doCaseSelection`'s captured locals, used by
-// `drawCaseSelectionFrame` (which replaces the original lambda). Lives
-// on the stack inside `doCaseSelection`; never escapes.
-struct CaseSelectionView {
+// Snapshot of `doActionScreen`'s captured locals, used by
+// `drawActionMenuFrame`. Lives on the stack inside `doActionScreen`;
+// never escapes.
+struct ActionMenuView {
EEMEngine *vm;
- const Picture *caseBg;
- bool haveCaseBg;
- const Picture *revealPic;
- bool haveRevealPic;
- const Animation *kdAnim;
- bool haveKdAnim;
- uint16 kdAnimId; ///< 0x15 / 0x16 â looked up in kAnimScripts
- int kdAnimX;
- int kdAnimY;
+ const Picture *bg;
+ bool haveBg;
+ const Picture *decor;
+ bool haveDecor;
const char *separator;
const char *const *pickLabel;
const bool *pickEnabled;
uint pick;
- uint book;
};
// Mystery list shown in the "Choose A Mystery" sub-screen. Mirrors
@@ -486,6 +494,16 @@ Common::StringArray loadBookNames(uint book) {
return names;
}
+void clampCaseTopRow(uint &topRow, uint listLen, uint visibleRows) {
+ if (listLen <= visibleRows) {
+ topRow = 0;
+ return;
+ }
+ const uint maxTop = listLen - visibleRows;
+ if (topRow > maxTop)
+ topRow = maxTop;
+}
+
// Per-mystery sub-chooser ("Choose A Mystery") view.
//
// `names` are the entries from BOOK%d.NME (in display order â index 0
@@ -645,12 +663,15 @@ void drawCaseSubmenu(const CaseSubmenuView &v) {
copyToScreen(scratch);
}
-void drawCaseSelectionFrame(const CaseSelectionView &v) {
+void drawActionMenuFrame(const ActionMenuView &v) {
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
- drawCaseBase(scratch, v.vm, v.caseBg, v.haveCaseBg,
- v.revealPic, v.haveRevealPic,
- v.kdAnim, v.haveKdAnim, v.kdAnimX, v.kdAnimY, v.book);
+ scratch.clear();
+ if (v.haveBg && v.bg)
+ scratch.simpleBlitFrom(v.bg->surface);
+ if (v.haveDecor && v.decor)
+ blitMaskedPic(scratch, *v.decor,
+ kActionScreenDecorX, kActionScreenDecorY);
if (v.vm->getFont().isLoaded()) {
// `DrawList` @ 1c33:040d coordinates: `_TextBox + 3` for x
@@ -763,6 +784,7 @@ void EEMEngine::doProfilePicker() {
haveReveal ? &reveal : nullptr))
return;
drawProfilePickerFrame(view);
+ uint32 chooserLastTick = g_system->getMillis();
while (!done && !shouldQuit()) {
Common::Event ev;
@@ -875,6 +897,11 @@ void EEMEngine::doProfilePicker() {
view.start = start;
drawProfilePickerFrame(view);
}
+ const uint32 now = g_system->getMillis();
+ if (now - chooserLastTick >= kChooserCycleMillis) {
+ chooserLastTick = now;
+ cycleChooserPalette();
+ }
g_system->updateScreen();
g_system->delayMillis(15);
}
@@ -1738,21 +1765,18 @@ void EEMEngine::doSetup() {
}
}
-void EEMEngine::doCaseSelection() {
- // Mirrors `_CaseSelection` @ 1c33:0a87. The original draws PIC 0x41
- // (chooser background) plus a centred "Book %d" / "Challenge Book"
- // header at (y=12) and then calls `_DoChoose(list)` to render the
- // menu via `DrawList` @ 1c33:040d at (_TextBox+3, DAT_29be_0d02) =
- // (61, 35), 12 rows à 10 px line height. The menu list itself is
- // the static array at 29be:0d6a (verified via `push 0x0d6a` at
- // 1c33:1ab4). Strings are at 29be:0ef4 onwards. Layout:
+void EEMEngine::doActionScreen() {
+ // Mirrors `_ActionScreen` @ 1c33:195b. The original draws background
+ // PIC 0x104 plus PIC 9 at (10, 0x87), then calls `_DoChoose` with
+ // `ActionNames @ 29be:0d6a`. The "Book N" heading belongs only to
+ // `_CaseSelection`, so this top-level menu intentionally does not
+ // draw one.
+ // Layout:
// list[0] = "----------------------------------"
// list[1] = " Choose A Mystery"
// list[2..10] = alternating menu items + separators
// Five selectable items: Choose A Mystery / Practice Mystery /
// See ScrapBook 1/2/3.
- const uint kMaxMystery = 54;
-
enum MenuPick {
kPickChoose = 0,
kPickPractice,
@@ -1828,8 +1852,6 @@ void EEMEngine::doCaseSelection() {
const Common::Rect kOkRect ( 12, 63, 41, 87); // 29be:0cd8 confirm
const Common::Rect kHelpRect ( 12, 100, 41, 124); // 29be:0ce0 help
const Common::Rect kExitRect ( 12, 137, 41, 161); // 29be:0ce8 cancel
- const Common::Rect kUpArrowRect (240, 31, 250, 43); // 29be:0cf0 scroll up
- const Common::Rect kDnArrowRect (240, 148, 250, 159); // 29be:0cf8 scroll dn
const Common::Rect kListRect ( 58, 35, 238, 158); // 29be:0d00 list panel
// The original `_NewPlayer` set `_MouseCursor = 1` on exit; the
@@ -1837,7 +1859,7 @@ void EEMEngine::doCaseSelection() {
// Reassert here in case anything between hid it.
CursorMan.showMouse(true);
- // Reassert site palette 0 (the case-selection / chooser CLUT). In
+ // Reassert site palette 0 (the chooser CLUT). In
// the normal flow `doProfilePicker` (or the post-screen reset paths
// at lines 1402 / 1147 / 1121) leaves us on palette 0 already, but
// the launcher-resume path jumps straight here from `_AllBlack`
@@ -1845,65 +1867,29 @@ void EEMEngine::doCaseSelection() {
// CLUT and the player sees an empty screen.
setSitePalette(0);
- // Mirrors `_CaseSelection`: load PIC 0x41 as the chooser backdrop.
- Picture caseBg;
- const bool haveCaseBg = _picsArchive.getPicture(0x41, caseBg);
- Picture revealPic;
- const bool haveRevealPic =
- _picsArchive.getPicture(kCaseSelectionRevealPic, revealPic);
-
- // KD greeter sprite. `_CaseSelection @ 1c33:0a87` (1c33:0b7e-0ba1)
- // loads anim 0x15 (Jake-paired) or 0x16 (Jenny-paired) and registers
- // `_NewAnimation(0x112, 0x50, ..., seqnum=0x15, prior=1)` â partner-
- // dependent because the host KD changes who's "with him" on the
- // briefing intro frame. Runs continuously through the menu loop via
- // `_UpdateAnimations`. We approximate with millis-based frame cycling.
- const uint kKdAniId = (_partner == 0) ? 0x15 : 0x16;
- Animation kdAnim;
- const bool haveKdAnim = _aniArchive.loadAnimation(kKdAniId, kdAnim)
- && !kdAnim.empty();
- const int kKdAnimX = 0x112;
- const int kKdAnimY = 0x50;
- const uint caseBook = (_chainStage == 3) ? 3 :
- (_chainStage == 2) ? 2 : 1;
+ Picture bg;
+ const bool haveBg = _picsArchive.getPicture(kActionScreenBackgroundPic, bg);
+ Picture decor;
+ const bool haveDecor = _picsArchive.getPicture(kActionScreenDecorPic, decor);
- CaseSelectionView v;
+ ActionMenuView v;
v.vm = this;
- v.caseBg = &caseBg;
- v.haveCaseBg = haveCaseBg;
- v.revealPic = &revealPic;
- v.haveRevealPic = haveRevealPic;
- v.kdAnim = &kdAnim;
- v.haveKdAnim = haveKdAnim;
- v.kdAnimId = (uint16)kKdAniId;
- v.kdAnimX = kKdAnimX;
- v.kdAnimY = kKdAnimY;
+ v.bg = &bg;
+ v.haveBg = haveBg;
+ v.decor = &decor;
+ v.haveDecor = haveDecor;
v.separator = kSeparator;
v.pickLabel = kPickLabel;
v.pickEnabled = kPickEnabled;
v.pick = pick;
- v.book = caseBook;
- if (animateCaseSelectionReveal(this, &caseBg, haveCaseBg,
- &revealPic, haveRevealPic,
- &kdAnim, haveKdAnim,
- kKdAnimX, kKdAnimY, caseBook))
- return;
- drawCaseSelectionFrame(v);
- uint32 lastTick = g_system->getMillis();
+ drawActionMenuFrame(v);
+ uint32 chooserLastTick = g_system->getMillis();
bool exitChosen = false;
while (!shouldQuit()) {
Common::Event ev;
bool confirmed = false;
- // Redraw every 100 ms so the KD greeter cycles. Mirrors the
- // `_CheckFrameRate` cadence in `_CaseSelection`'s main loop.
- const uint32 now = g_system->getMillis();
- if (haveKdAnim && now - lastTick >= 100) {
- lastTick = now;
- v.pick = pick;
- drawCaseSelectionFrame(v);
- }
while (g_system->getEventManager()->pollEvent(ev)) {
if (ev.type == Common::EVENT_QUIT ||
ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
@@ -1938,7 +1924,7 @@ void EEMEngine::doCaseSelection() {
if (mp < kNumPicks && kPickEnabled[mp]) {
pick = mp;
v.pick = pick;
- drawCaseSelectionFrame(v);
+ drawActionMenuFrame(v);
continue;
}
}
@@ -1969,7 +1955,7 @@ void EEMEngine::doCaseSelection() {
break;
}
v.pick = pick;
- drawCaseSelectionFrame(v);
+ drawActionMenuFrame(v);
continue;
}
if (k == Common::KEYCODE_DOWN || k == Common::KEYCODE_RIGHT ||
@@ -1980,15 +1966,20 @@ void EEMEngine::doCaseSelection() {
break;
}
v.pick = pick;
- drawCaseSelectionFrame(v);
+ drawActionMenuFrame(v);
continue;
}
}
if (confirmed) {
v.pick = pick;
- drawCaseSelectionFrame(v);
+ drawActionMenuFrame(v);
break;
}
+ const uint32 now = g_system->getMillis();
+ if (now - chooserLastTick >= kChooserCycleMillis) {
+ chooserLastTick = now;
+ cycleChooserPalette();
+ }
g_system->updateScreen();
g_system->delayMillis(15);
}
@@ -1997,15 +1988,20 @@ void EEMEngine::doCaseSelection() {
return;
if (exitChosen) {
- _mystery.clear();
- _nextScreen = kScreenInvalid;
+ if (areYouSure()) {
+ _mystery.clear();
+ _nextScreen = kScreenInvalid;
+ } else {
+ drawActionMenuFrame(v);
+ _nextScreen = kScreenAction;
+ }
return;
}
// "Practice Mystery" is the tutorial â mystery 0.
if (pick == kPickPractice) {
if (!_mystery.load(0, &_rng)) {
- warning("doCaseSelection: failed to load practice mystery");
+ warning("doActionScreen: failed to load practice mystery");
_mystery.clear();
resetSiteArrivalState();
} else {
@@ -2021,49 +2017,72 @@ void EEMEngine::doCaseSelection() {
// call `_ShowScrapbook(0, stage)` for the matching tier
// (verified at the action-handler jumptable bytes
// `01 03 05 07 09 ff` paired with handlers at 1c33:1be1).
- // The picker here is meant to LEAVE the mystery state untouched
- // â viewing the scrapbook never starts a new case.
+ // Viewing the scrapbook never starts a new case; return to
+ // the same action menu afterwards.
const uint stage = (pick == kPickScrap1) ? 1
: (pick == kPickScrap2) ? 2 : 3;
doShowScrapbook(stage);
setSitePalette(0);
_mystery.clear();
- _nextScreen = kScreenChooseMystery;
+ _nextScreen = kScreenAction;
return;
}
- // "Choose A Mystery" sub-screen: pick a specific case from the
- // chain-stage's roster. Mirrors `_DoChooseMystery @ 1a35:02b7` +
- // `_CaseSelection @ 1c33:0a87`:
- // stage 1 (Junior, BOOK1.NME) â mysteries 1..24
- // stage 2 (Senior, BOOK2.NME) â mysteries 25..48
- // stage 3 (Master, BOOK3.NME) â mysteries 49..54
- // `_DoChooseMystery` opens BOOK<stage>.NME and reads up to 25
- // CRLF-terminated lines into a 25-entry FAR-pointer array passed
- // to `_CaseSelection`. The grey mask `_Greys = &mysteriesSolved +
- // stageLo` (1c33:0b22) makes already-solved entries unselectable.
+ _nextScreen = kScreenChooseMystery;
+}
+
+void EEMEngine::doCaseSelection() {
+ // Mirrors `_DoChooseMystery @ 1a35:02b7` + `_CaseSelection @
+ // 1c33:0a87`. `_DoChooseMystery` loads BOOK<stage>.NME and
+ // `_CaseSelection` draws PIC 0x41 plus the centered "Book N" /
+ // "Challenge Book" title. This screen is entered only after the
+ // player selects "Choose A Mystery" on `_ActionScreen`.
+ const uint kMaxMystery = 54;
+
+ CursorMan.showMouse(true);
+ setSitePalette(0);
+ _mystery.clear();
+ resetSiteArrivalState();
+
+ Picture caseBg;
+ const bool haveCaseBg = _picsArchive.getPicture(0x41, caseBg);
+ Picture revealPic;
+ const bool haveRevealPic =
+ _picsArchive.getPicture(kCaseSelectionRevealPic, revealPic);
+
+ // KD greeter sprite. `_CaseSelection @ 1c33:0a87` loads anim 0x15
+ // (Jake-paired) or 0x16 (Jenny-paired), then runs it through the
+ // chooser loop via `_UpdateAnimations`.
+ const uint kKdAniId = (_partner == 0) ? 0x15 : 0x16;
+ Animation kdAnim;
+ const bool haveKdAnim = _aniArchive.loadAnimation(kKdAniId, kdAnim)
+ && !kdAnim.empty();
+ const int kKdAnimX = 0x112;
+ const int kKdAnimY = 0x50;
+
+ // Stage roster:
+ // stage 1 (Junior, BOOK1.NME) -> mysteries 1..24
+ // stage 2 (Senior, BOOK2.NME) -> mysteries 25..48
+ // stage 3 (Master, BOOK3.NME) -> mysteries 49..54
uint stageLo = 1, stageHi = 0x18;
uint book = 1;
switch (_chainStage) {
case 2: stageLo = 0x19; stageHi = 0x30; book = 2; break;
case 3: stageLo = 0x31; stageHi = 0x36; book = 3; break;
- default: break; // stage 1 (or fallback)
+ default: break;
}
if (stageHi > kMaxMystery)
stageHi = kMaxMystery;
const Common::StringArray names = loadBookNames(book);
if (names.empty()) {
- warning("doCaseSelection: BOOK%u.NME failed to load â bailing",
- book);
- _mystery.clear();
+ warning("doCaseSelection: BOOK%u.NME failed to load", book);
return;
}
const uint listLen = MIN<uint>((uint)names.size(), stageHi - stageLo + 1);
// Per-row solved flags. `_DoChoose @ 1c33:0521` skips solved entries
- // when seeding the initial selection (`while *_Greys[select] != 0`)
- // and again per-click via the same mask check.
+ // when seeding the initial selection and ignores clicks on them.
Common::Array<bool> solvedFlags;
solvedFlags.resize(listLen);
for (uint i = 0; i < listLen; i++) {
@@ -2072,32 +2091,19 @@ void EEMEngine::doCaseSelection() {
mn < sizeof(_mysteriesSolved) && _mysteriesSolved[mn] != 0;
}
- // Seed the selection at the first unsolved entry â same as
- // `_DoChoose`'s `while (*Greys[select] != 0) select++;` loop at
- // 1c33:0524.
uint selRow = 0;
while (selRow < listLen && solvedFlags[selRow])
selRow++;
if (selRow >= listLen)
- selRow = 0; // every case solved â let player re-pick
+ selRow = 0;
+
uint topRow = 0;
const uint kVisible = 12;
if (selRow >= kVisible) {
topRow = selRow - kVisible / 2;
- if (topRow + kVisible > listLen)
- topRow = listLen > kVisible ? listLen - kVisible : 0;
+ clampCaseTopRow(topRow, listLen, kVisible);
}
- auto clampTopRow = [&](uint &t) {
- if (listLen <= kVisible) {
- t = 0;
- return;
- }
- const uint maxTop = listLen - kVisible;
- if (t > maxTop)
- t = maxTop;
- };
-
CaseSubmenuView sv;
sv.vm = this;
sv.caseBg = &caseBg;
@@ -2130,49 +2136,43 @@ void EEMEngine::doCaseSelection() {
ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
return;
if (ev.type == Common::EVENT_LBUTTONDOWN) {
- if (kOkRect.contains(ev.mouse.x, ev.mouse.y)) {
+ if (kChooserOkRect.contains(ev.mouse.x, ev.mouse.y)) {
if (selRow < listLen && !solvedFlags[selRow])
confirmed = true;
break;
}
- if (kExitRect.contains(ev.mouse.x, ev.mouse.y)) {
+ if (kChooserExitRect.contains(ev.mouse.x, ev.mouse.y)) {
_mystery.clear();
return;
}
- if (kHelpRect.contains(ev.mouse.x, ev.mouse.y)) {
- // In original `_CaseSelection`, this button is
- // PIC 0x123 and returns 0xfffe, which calls
- // `_ChooseSavedGame`. The ScummVM port stores the
- // in-progress case in the profile save instead of
- // original per-mystery files, so route to screen 8
- // (profile picker) as the load/resume path.
+ if (kChooserHelpRect.contains(ev.mouse.x, ev.mouse.y)) {
+ // Original `_CaseSelection` returns 0xfffe here,
+ // then `_ChooseSavedGame` runs. ScummVM stores
+ // in-progress cases in the profile, so route to
+ // the profile picker instead.
saveProfile(_playerName);
_mystery.clear();
_nextScreen = kScreenProfile;
return;
}
- if (kUpArrowRect.contains(ev.mouse.x, ev.mouse.y)) {
+ if (kChooserUpArrowRect.contains(ev.mouse.x, ev.mouse.y)) {
if (topRow > 0) { topRow--; dirty = true; }
continue;
}
- if (kDnArrowRect.contains(ev.mouse.x, ev.mouse.y)) {
+ if (kChooserDnArrowRect.contains(ev.mouse.x, ev.mouse.y)) {
topRow++;
- clampTopRow(topRow);
+ clampCaseTopRow(topRow, listLen, kVisible);
dirty = true;
continue;
}
- if (kListRect.contains(ev.mouse.x, ev.mouse.y)) {
- // Pick the row under the cursor â mirrors
- // 1c33:0635 `i = (MouseY - DAT_29be_0d02) / 10;`.
+ if (kChooserListRect.contains(ev.mouse.x, ev.mouse.y)) {
const int kLineH = 10;
- const int row = (ev.mouse.y - 35) / kLineH;
+ const int row = (ev.mouse.y - kChooserListRect.top) / kLineH;
if (row < 0 || row >= (int)kVisible)
continue;
const uint idx = topRow + (uint)row;
- if (idx >= listLen)
+ if (idx >= listLen || solvedFlags[idx])
continue;
- if (solvedFlags[idx])
- continue; // greyed entries ignore clicks
selRow = idx;
dirty = true;
continue;
@@ -2197,7 +2197,7 @@ void EEMEngine::doCaseSelection() {
selRow++;
if (selRow >= topRow + kVisible) {
topRow = selRow - kVisible + 1;
- clampTopRow(topRow);
+ clampCaseTopRow(topRow, listLen, kVisible);
}
dirty = true;
}
@@ -2216,7 +2216,7 @@ void EEMEngine::doCaseSelection() {
selRow = MIN<uint>(selRow + kVisible, listLen - 1);
if (selRow >= topRow + kVisible) {
topRow = selRow - kVisible + 1;
- clampTopRow(topRow);
+ clampCaseTopRow(topRow, listLen, kVisible);
}
dirty = true;
continue;
@@ -2242,10 +2242,12 @@ void EEMEngine::doCaseSelection() {
}
}
const uint32 now = g_system->getMillis();
- const bool animTick = haveKdAnim && now - submenuLastTick >= 100;
- if (animTick)
+ const bool chooserTick = now - submenuLastTick >= kChooserCycleMillis;
+ if (chooserTick) {
submenuLastTick = now;
- if (dirty || animTick) {
+ cycleChooserPalette();
+ }
+ if (dirty || (chooserTick && haveKdAnim)) {
sv.topRow = topRow;
sv.selRow = selRow;
drawCaseSubmenu(sv);
@@ -2254,6 +2256,9 @@ void EEMEngine::doCaseSelection() {
g_system->delayMillis(15);
}
+ if (shouldQuit())
+ return;
+
const uint mn = stageLo + selRow;
if (!_mystery.load(mn, &_rng)) {
warning("doCaseSelection: failed to load mystery %u", mn);
Commit: 2398f5722175044e49a6729d7a77a32c3249ed82
https://github.com/scummvm/scummvm/commit/2398f5722175044e49a6729d7a77a32c3249ed82
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:05+02:00
Commit Message:
EEM: refresh the correct part of the screen during case introductions
Changed paths:
engines/eem/clues.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 2d72d4e45b1..b6cc6b79c10 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -340,7 +340,8 @@ void EEMEngine::doInitClues() {
setSitePalette(0x22);
Picture bg;
- if (_picsArchive.getPicture(0x52, bg))
+ const bool haveBriefingBg = _picsArchive.getPicture(0x52, bg);
+ if (haveBriefingBg)
blitAt(bg, 0, 0);
const uint gameAni = _partner == 0 ? 0x17 : 0x3b;
@@ -372,7 +373,7 @@ void EEMEngine::doInitClues() {
// "thinking" pose (cell 8) for 16 ticks instead of
// flipbook-cycling, and nancy waits 18 ticks before her
// late-arrival count-up.
- if (_picsArchive.getPicture(0x52, bg))
+ if (haveBriefingBg)
blitAt(bg, 0, 0);
const uint32 t = frame * 100;
// All three briefing anims (game/book/nancy) go through
@@ -427,20 +428,38 @@ void EEMEngine::doInitClues() {
}
}
- // Freeze the completed setup animation as the base for
- // `_PlayInSequence`. The original clears the registered animations
- // before playing the short case-type overlay, so the game/book/nancy
- // cells do not keep cycling or wrap back to cell 0 underneath it.
+ // Freeze only the same setup-animation band the original bakes into
+ // its background buffers before clearing the registered animations:
+ // `_VidramRectCopy(0, 0x5a, 0x28, 0x6d, 16000, 48000/32000)`.
+ // Width is in mode-X columns, so 0x28 columns = 160 pixels. This
+ // preserves the lower-left book/Nancy area but intentionally drops the
+ // right-side game animation; `_PlayInSequence` redraws that character
+ // over a clean background next. Preserving the full screen leaves the
+ // old right-side Jake/Jenny frame underneath the sequence.
Graphics::ManagedSurface briefingBase(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
briefingBase.clear();
+ if (haveBriefingBg) {
+ briefingBase.simpleBlitFrom(bg.surface);
+ } else {
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (screen) {
+ briefingBase.simpleBlitFrom(*screen);
+ g_system->unlockScreen();
+ }
+ }
{
+ const int preserveX = 0;
+ const int preserveY = 0x5a;
+ const int preserveW = 0x28 * 4;
+ const int preserveH = 0x6d;
+ const Common::Rect preserveRect(preserveX, preserveY,
+ preserveX + preserveW,
+ preserveY + preserveH);
Graphics::Surface *screen = g_system->lockScreen();
if (screen) {
- for (int row = 0; row < 200; row++) {
- memcpy((byte *)briefingBase.getBasePtr(0, row),
- (const byte *)screen->getBasePtr(0, row), 320);
- }
+ briefingBase.simpleBlitFrom(*screen, preserveRect,
+ Common::Point(preserveX, preserveY));
g_system->unlockScreen();
}
}
Commit: d00eeb0022a1966b8e1c78cbb5e77bcd4605336c
https://github.com/scummvm/scummvm/commit/d00eeb0022a1966b8e1c78cbb5e77bcd4605336c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:06+02:00
Commit Message:
EEM: optional better fitted balloon dialog code
Changed paths:
engines/eem/clues.cpp
engines/eem/detection.cpp
engines/eem/detection.h
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/graphics.cpp
engines/eem/metaengine.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index b6cc6b79c10..b427294b306 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -808,12 +808,11 @@ void EEMEngine::displayClue(const byte *clueBlock) {
_playerName, _partner);
// Speech balloon. Mirrors `_GetBalloon` + `_AddPicBackground` in
- // `_DisplayClue`. The original looks up per-balloon text-area
- // metadata in a table at offset 0x875 (within `_DisplayClue`'s
- // segment); we don't have that table decoded yet, so we use a
- // fixed inset of 8 px from the balloon's top-left.
+ // `_DisplayClue`, with the shared balloon metadata table used for
+ // text placement and the fitted balloon variant.
+ const uint16 fittedBubNum = fitBalloonToText(bubNum, text);
Picture balloon;
- const uint16 balloonId = bubNum & 0x7F;
+ const uint16 balloonId = fittedBubNum & 0x7F;
const bool haveBalloon = bubNum != 0xFFFF &&
_balloonArchive.size() > balloonId &&
_balloonArchive.loadEntry(balloonId, balloon);
@@ -849,7 +848,7 @@ void EEMEngine::displayClue(const byte *clueBlock) {
// `_GetBalloon @ 172b:1d7d` mirrors the picture horizontally
// when `(bubNum & 0x80)` is set â used for right-side
// speakers so the tail points the other way.
- const bool flipBalloon = (bubNum & 0x80) != 0;
+ const bool flipBalloon = (fittedBubNum & 0x80) != 0;
if (bw > 0 && bh > 0) {
for (int row = 0; row < bh; row++) {
const byte *src =
@@ -1111,11 +1110,57 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
}
}
+ Common::String fitText;
+ Common::String fitPage;
+ uint16 fitXIns = 0;
+ uint16 fitYIns = 0;
+ uint16 fitWidth = 142;
+ getBalloonInsets(balByte, fitXIns, fitYIns, fitWidth);
+ uint fitTextLines = 0;
+ for (uint t = 0; t < textCount; t++) {
+ const uint8 idxByte = rec[11 + t];
+ const uint8 idx = idxByte & 0x7f;
+ const uint32 noteAbs = notesBase + (uint32)idx * 7;
+ if (noteAbs + 6 > dsz)
+ continue;
+ const uint16 textOff = (_partner == 0)
+ ? READ_LE_UINT16(notes + idx * 7 + 2)
+ : READ_LE_UINT16(notes + idx * 7 + 4);
+ if (textOff >= dsz)
+ continue;
+ const char *linePtr = (const char *)(bufBase + textOff);
+ uint32 lineLen = 0;
+ while (textOff + lineLen < dsz && linePtr[lineLen] != 0)
+ lineLen++;
+ Common::String raw(linePtr, lineLen);
+ Common::String parsed = parseString(raw, _playerName, _partner);
+ if (!parsed.empty()) {
+ if (!fitPage.empty())
+ fitPage += '\n';
+ fitPage += parsed;
+ }
+ const bool continuePage =
+ (idxByte & 0x80) != 0 && t + 1 < textCount;
+ if (!continuePage) {
+ Common::Array<Common::String> wrapped;
+ _font.wordWrapText(fitPage, MAX<int>(8, (int)fitWidth),
+ wrapped);
+ if (wrapped.size() > fitTextLines ||
+ (wrapped.size() == fitTextLines &&
+ fitPage.size() > fitText.size())) {
+ fitText = fitPage;
+ fitTextLines = wrapped.size();
+ }
+ fitPage.clear();
+ }
+ }
+
// Pre-load balloon picture + insets once per record (constant
// across all paginated text indices).
+ const uint16 fittedBalByte = fitBalloonToText(balByte, fitText);
Picture balloon;
- const uint16 balloonId = balByte & 0x7F;
- const bool flipBall = (balByte & 0x80) != 0;
+ const uint16 balloonId = fittedBalByte & 0x7F;
+ const bool flipBall = (fittedBalByte & 0x80) != 0;
const bool haveBalloon = balByte != 0xFF &&
_balloonArchive.size() > balloonId &&
_balloonArchive.loadEntry(balloonId, balloon);
diff --git a/engines/eem/detection.cpp b/engines/eem/detection.cpp
index fd620dd89f4..7fe94f56f39 100644
--- a/engines/eem/detection.cpp
+++ b/engines/eem/detection.cpp
@@ -31,7 +31,8 @@ const PlainGameDescriptor eemGames[] = {
{ nullptr, nullptr }
};
-#define GUI_OPTIONS_EEM GUIO3(GAMEOPTION_HIDE_HIGHLIGHT_BOXES, GUIO_MIDIADLIB, GUIO_MIDIMT32)
+#define GUI_OPTIONS_EEM_FLOPPY GUIO3(GAMEOPTION_HIDE_HIGHLIGHT_BOXES, GUIO_MIDIADLIB, GUIO_MIDIMT32)
+#define GUI_OPTIONS_EEM_CD GUIO4(GAMEOPTION_HIDE_HIGHLIGHT_BOXES, GAMEOPTION_FIT_DIALOG_BALLOONS, GUIO_MIDIADLIB, GUIO_MIDIMT32)
const ADGameDescription gameDescriptions[] = {
{
@@ -42,7 +43,7 @@ const ADGameDescription gameDescriptions[] = {
Common::EN_ANY,
Common::kPlatformDOS,
ADGF_NO_FLAGS,
- GUI_OPTIONS_EEM
+ GUI_OPTIONS_EEM_CD
},
{
"eem",
@@ -52,7 +53,7 @@ const ADGameDescription gameDescriptions[] = {
Common::EN_ANY,
Common::kPlatformDOS,
ADGF_NO_FLAGS,
- GUI_OPTIONS_EEM
+ GUI_OPTIONS_EEM_FLOPPY
},
{
// Spanish floppy release â same EEM.EXE binary as the English
@@ -66,7 +67,7 @@ const ADGameDescription gameDescriptions[] = {
Common::ES_ESP,
Common::kPlatformDOS,
ADGF_NO_FLAGS,
- GUI_OPTIONS_EEM
+ GUI_OPTIONS_EEM_FLOPPY
},
AD_TABLE_END_MARKER
diff --git a/engines/eem/detection.h b/engines/eem/detection.h
index 7d74db6cd7f..3ad726bb5f0 100644
--- a/engines/eem/detection.h
+++ b/engines/eem/detection.h
@@ -26,7 +26,8 @@
namespace EEM {
-#define GAMEOPTION_HIDE_HIGHLIGHT_BOXES GUIO_GAMEOPTIONS1
+#define GAMEOPTION_HIDE_HIGHLIGHT_BOXES GUIO_GAMEOPTIONS1
+#define GAMEOPTION_FIT_DIALOG_BALLOONS GUIO_GAMEOPTIONS2
enum EEMDebugChannels {
kDebugGeneral = 1 << 0,
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index da33f550739..6d4acc41d88 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -199,6 +199,7 @@ EEMEngine::EEMEngine(OSystem *syst, const ADGameDescription *gameDesc)
_playerName("Detective"),
_lastScreen(kScreenInvalid), _nextScreen(kScreenTitle), _partner(0) {
ConfMan.registerDefault("hide_highlight_boxes", false);
+ ConfMan.registerDefault("fit_dialog_balloons", false);
// `ADGameDescription::extra` is set by the matching entry in
// `gameDescriptions[]` ("CD" or "Floppy"). Keep variant detection
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index bc63f993db1..32eb70388f5 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -254,6 +254,11 @@ public:
/// ctype-bit-1 (= digit) at `29be:2be1 + char`.
uint16 getKDTextBalloon(byte firstChar) const;
+ /// Cleanup for overly tall original speech balloons. Keeps the original
+ /// bubble family and mirror bit, but picks a shorter sibling when the
+ /// wrapped text leaves enough empty lines.
+ uint16 fitBalloonToText(uint16 bubNum, const Common::String &text);
+
/// Substitute the 0x80..0x89 control bytes the engine uses inside
/// `TextBlock` strings. Mirrors `_ParseString @ 1b66:07c3`; jump
/// table at 1b66:0cbe. Used by every clue / hint / balloon caller.
diff --git a/engines/eem/graphics.cpp b/engines/eem/graphics.cpp
index 4b9eae7cbf8..ca91bcc0349 100644
--- a/engines/eem/graphics.cpp
+++ b/engines/eem/graphics.cpp
@@ -19,6 +19,7 @@
*
*/
+#include "common/config-manager.h"
#include "common/debug.h"
#include "common/events.h"
#include "common/file.h"
@@ -85,6 +86,52 @@ const BalloonInsets kBalloonInsetTable[] = {
{ 5, 8, 158, 160, 45 }, { 5, 8, 176, 176, 50 }, { 8, 7, 142, 142, 71 }
};
+struct BalloonFamily {
+ uint16 first;
+ uint16 last;
+};
+
+const BalloonFamily kBalloonFamilies[] = {
+ { 0x00, 0x06 },
+ { 0x07, 0x0d },
+ { 0x0e, 0x14 },
+ { 0x15, 0x1b },
+ { 0x1c, 0x22 },
+ { 0x23, 0x29 },
+ { 0x2a, 0x30 },
+ { 0x31, 0x31 },
+ { 0x32, 0x32 },
+ { 0x33, 0x33 },
+};
+
+bool findBalloonFamily(uint16 balloonId, uint16 &first, uint16 &last) {
+ for (uint i = 0; i < ARRAYSIZE(kBalloonFamilies); i++) {
+ if (balloonId >= kBalloonFamilies[i].first &&
+ balloonId <= kBalloonFamilies[i].last) {
+ first = kBalloonFamilies[i].first;
+ last = kBalloonFamilies[i].last;
+ return true;
+ }
+ }
+ first = last = balloonId;
+ return false;
+}
+
+// Lines that fit inside balloon @p balloonId. The metadata-table `indDY`
+// is the designed bottom of the text area, NOT the indicator-drawing
+// position alone â for families 3, 4, 6 and the singletons the bubble
+// graphic continues below `indDY` with shadow / tail decoration that
+// text must not encroach on. Image height is therefore an overestimate;
+// `indDY` is the artist-intended last text line.
+uint getBalloonLineCapacity(uint16 balloonId, int lineH) {
+ const uint idx = balloonId & 0x7F;
+ if (idx >= ARRAYSIZE(kBalloonInsetTable) || lineH <= 0)
+ return 0;
+
+ const BalloonInsets &insets = kBalloonInsetTable[idx];
+ return MAX<uint>(1, ((int)insets.indDY - (int)insets.y) / lineH + 1);
+}
+
// Floppy KDHelp hotspot-searched check. Mirrors
// `FUN_22dc_096c @ 22dc:096c`: walks the per-site dialog records at
// `site_data[+6]` to skip `hotspotIdx` hotspots, then returns the
@@ -229,6 +276,7 @@ void EEMEngine::doHelp() {
Common::String text = parseString(Common::String(txt),
_playerName, _partner);
+ balloonIdx = fitBalloonToText((uint16)balloonIdx, text) & 0x7F;
Graphics::ManagedSurface ms(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
ms.clear();
@@ -411,9 +459,10 @@ void EEMEngine::doHelp() {
// has to.
const byte firstChar =
text.empty() ? (byte)0 : (byte)text[0];
- const uint16 bubNum = getKDTextBalloon(firstChar);
+ uint16 bubNum = getKDTextBalloon(firstChar);
if (firstChar >= '0' && firstChar <= '9')
text.deleteChar(0);
+ bubNum = fitBalloonToText(bubNum, text);
Picture balloon;
const bool haveBalloon =
_balloonArchive.size() > (bubNum & 0x7F) &&
@@ -566,6 +615,72 @@ void EEMEngine::setPartnerEraseBg(const Graphics::ManagedSurface *bg) {
}
}
+uint16 EEMEngine::fitBalloonToText(uint16 bubNum,
+ const Common::String &text) {
+ // Opt-in via the "Better fit for dialog balloons" game option, and
+ // CD-only â the floppy build's balloon archive / inset table hasn't
+ // been validated for shrinking yet, so leave it on the original
+ // artist-chosen bubble.
+ if (isFloppy() || !ConfMan.getBool("fit_dialog_balloons"))
+ return bubNum;
+
+ const uint16 originalId = bubNum & 0x7F;
+ if (bubNum == 0xFFFF || text.empty() || !_font.isLoaded() ||
+ originalId >= ARRAYSIZE(kBalloonInsetTable))
+ return bubNum;
+
+ const BalloonInsets &originalInsets = kBalloonInsetTable[originalId];
+ const int lineH = _font.getFontHeight();
+ const uint originalCapacity = getBalloonLineCapacity(originalId, lineH);
+ if (originalCapacity == 0)
+ return bubNum;
+
+ Common::Array<Common::String> lines;
+ _font.wordWrapText(text, MAX<int>(8, (int)originalInsets.w), lines);
+ if (lines.empty())
+ return bubNum;
+
+ const uint usedLines = lines.size();
+ if (usedLines > originalCapacity)
+ return bubNum;
+
+ uint16 familyFirst = 0;
+ uint16 familyLast = 0;
+ if (!findBalloonFamily(originalId, familyFirst, familyLast))
+ return bubNum;
+
+ uint16 chosenId = originalId;
+ uint chosenCapacity = originalCapacity;
+ debug(
+ "fitBalloonToText: original 0x%02x, usedLines=%u, capacity=%u, family=0x%02x-0x%02x",
+ (int)originalId, usedLines, originalCapacity,
+ (int)familyFirst, (int)familyLast);
+ while (chosenId > familyFirst) {
+ if (chosenCapacity < usedLines)
+ break;
+
+ const uint16 candidateId = chosenId - 1;
+ if (candidateId >= ARRAYSIZE(kBalloonInsetTable) ||
+ kBalloonInsetTable[candidateId].w != originalInsets.w)
+ break;
+ const uint candidateCapacity =
+ getBalloonLineCapacity(candidateId, lineH);
+ if (candidateCapacity < usedLines)
+ break;
+ chosenId = candidateId;
+ chosenCapacity = candidateCapacity;
+ }
+
+ if (chosenId == originalId)
+ return bubNum;
+
+ debug(
+ "fitBalloonToText: 0x%02x -> 0x%02x (%u lines, capacity %u -> %u)",
+ (int)originalId, (int)chosenId, usedLines,
+ originalCapacity, chosenCapacity);
+ return (bubNum & 0x80) | chosenId;
+}
+
bool EEMEngine::getBalloonInsets(uint16 bubNum, uint16 &xInset,
uint16 &yInset, uint16 &textW) const {
// `kBalloonInsetTable` lives at file scope above; see comment there.
diff --git a/engines/eem/metaengine.cpp b/engines/eem/metaengine.cpp
index 52b2c5e78b5..04e778c35bc 100644
--- a/engines/eem/metaengine.cpp
+++ b/engines/eem/metaengine.cpp
@@ -43,6 +43,18 @@ static const ADExtraGuiOptionsMap optionsList[] = {
0
}
},
+ {
+ GAMEOPTION_FIT_DIALOG_BALLOONS,
+ {
+ _s("Better fit for dialog balloons"),
+ _s("Pick a smaller speech-bubble graphic when the wrapped "
+ "text doesn't need the original size."),
+ "fit_dialog_balloons",
+ false,
+ 0,
+ 0
+ }
+ },
AD_EXTRA_GUI_OPTIONS_TERMINATOR
};
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index d7986c80c89..1c3c0863398 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -4205,9 +4205,10 @@ void EEMEngine::doAccuse() {
g_system->unlockScreen();
}
const byte firstChar = (byte)entryText[0];
- const uint16 bubNum = getKDTextBalloon(firstChar);
+ uint16 bubNum = getKDTextBalloon(firstChar);
if (firstChar >= '0' && firstChar <= '9')
entryText.deleteChar(0);
+ bubNum = fitBalloonToText(bubNum, entryText);
Picture balloon;
const bool haveBalloon =
_balloonArchive.size() > (bubNum & 0x7F) &&
@@ -4329,13 +4330,14 @@ void EEMEngine::doAccuse() {
}
const byte firstChar =
hint.empty() ? (byte)0 : (byte)hint[0];
- const uint16 bubNum = getKDTextBalloon(firstChar);
+ uint16 bubNum = getKDTextBalloon(firstChar);
// Strip the digit prefix used for balloon dispatch â it's
// consumed by the original at `_DisplayAlibi @ 1df2:0163`
// (`str = pbVar7 + 1`) and shouldn't appear in the rendered
// text. `_GetKDTextBalloon` itself doesn't advance past it.
if (firstChar >= '0' && firstChar <= '9')
hint.deleteChar(0);
+ bubNum = fitBalloonToText(bubNum, hint);
Picture balloon;
const bool haveBalloon =
_balloonArchive.size() > (bubNum & 0x7F) &&
@@ -4413,7 +4415,7 @@ void EEMEngine::doAccuse() {
// the dispatch result is the same either way.
const byte firstChar =
hint.empty() ? (byte)0 : (byte)hint[0];
- const uint16 bubNum = getKDTextBalloon(firstChar);
+ uint16 bubNum = getKDTextBalloon(firstChar);
// Strip the digit prefix used for balloon dispatch.
// `_DisplayAlibi @ 1df2:0163` does `str = pbVar7 + 1`
// after using `*str` for `bindx`. Same pattern used by
@@ -4422,6 +4424,7 @@ void EEMEngine::doAccuse() {
// the intro balloon shows e.g. "1Ready to solve?".
if (firstChar >= '0' && firstChar <= '9')
hint.deleteChar(0);
+ bubNum = fitBalloonToText(bubNum, hint);
Picture balloon;
const bool haveBalloon =
_balloonArchive.size() > (bubNum & 0x7F) &&
@@ -4660,7 +4663,8 @@ void EEMEngine::doAccuse() {
}
if (bindx >= 16)
bindx = 2;
- const uint16 bubNum = kAlibiBubbles[bindx];
+ uint16 bubNum = kAlibiBubbles[bindx];
+ bubNum = fitBalloonToText(bubNum, alibi);
Picture alibiBg;
const bool haveAlibiBg = _picsArchive.getPicture(0x3e, alibiBg);
@@ -4825,9 +4829,10 @@ void EEMEngine::doAccuse() {
}
if (!react.empty()) {
const byte rChar = (byte)react[0];
- const uint16 rBub = getKDTextBalloon(rChar);
+ uint16 rBub = getKDTextBalloon(rChar);
if (rChar >= '0' && rChar <= '9')
react.deleteChar(0);
+ rBub = fitBalloonToText(rBub, react);
Picture rBalloon;
const bool haveR =
_balloonArchive.size() > (rBub & 0x7F) &&
@@ -5085,6 +5090,7 @@ void EEMEngine::doAccuseFloppy() {
}
Common::String text =
parseString(Common::String(txt), _playerName, _partner);
+ balloonIdx = fitBalloonToText((uint16)balloonIdx, text) & 0x7F;
Graphics::ManagedSurface ms(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
Graphics::Surface *cur = g_system->lockScreen();
@@ -5620,6 +5626,7 @@ void EEMEngine::doAccuseFloppy() {
balloonRaw = kFloppyAlibiBalloonByDigit[(int)(alibi[0] - '0')];
alibi.deleteChar(0);
}
+ balloonRaw = fitBalloonToText((uint16)balloonRaw, alibi);
const uint balloonIdx = balloonRaw & 0x7F;
const bool flipBalloon = (balloonRaw & 0x80) != 0;
Commit: 8ca74796331ffae0b9f49a844ed190491bb8313b
https://github.com/scummvm/scummvm/commit/8ca74796331ffae0b9f49a844ed190491bb8313b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:06+02:00
Commit Message:
EEM: only some keys will advance dialog
Changed paths:
engines/eem/clues.cpp
engines/eem/eem.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index b427294b306..68a66b1aa0d 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -926,6 +926,9 @@ void EEMEngine::displayClue(const byte *clueBlock) {
// Wait for click/key to advance â only if we drew something.
// ESC skips the entire dialogue rather than just one entry.
+ // Only Return / KP-Enter / Space advance one entry; other keys
+ // are ignored so accidental keystrokes don't blow past dialog
+ // the player hasn't finished reading.
if (hasText || (charPicId != 0 && charPicId != 0xFFFF)) {
bool advance = false;
bool skipAll = false;
@@ -950,8 +953,14 @@ void EEMEngine::displayClue(const byte *clueBlock) {
interruptAudio();
break;
}
- if (ev.type == Common::EVENT_LBUTTONDOWN ||
- ev.type == Common::EVENT_KEYDOWN) {
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ advance = true;
+ break;
+ }
+ if (ev.type == Common::EVENT_KEYDOWN &&
+ (ev.kbd.keycode == Common::KEYCODE_RETURN ||
+ ev.kbd.keycode == Common::KEYCODE_KP_ENTER ||
+ ev.kbd.keycode == Common::KEYCODE_SPACE)) {
advance = true;
break;
}
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 6d4acc41d88..85a533fee88 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -864,6 +864,10 @@ void EEMEngine::waitForInput(uint32 maxMs) {
// active audio so the theme / voice / spool don't bleed past
// the abort. Mirrors the `_CleanMysterySounds` + `_StopMIDI`
// pair around the title wait in `_DoOpeningAnims`.
+ //
+ // Only Return / KP-Enter / Space / Escape advance â letting any key
+ // dismiss balloons makes typing-while-reading (or a stuck modifier)
+ // blow past dialog the player hasn't finished reading.
const uint32 startMs = g_system->getMillis();
while (!shouldQuit() && (g_system->getMillis() - startMs < maxMs)) {
Common::Event event;
@@ -877,8 +881,13 @@ void EEMEngine::waitForInput(uint32 maxMs) {
if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
_skipIntro = true;
interruptAudio();
+ return;
+ }
+ if (event.kbd.keycode == Common::KEYCODE_RETURN ||
+ event.kbd.keycode == Common::KEYCODE_KP_ENTER ||
+ event.kbd.keycode == Common::KEYCODE_SPACE) {
+ return;
}
- return;
}
}
g_system->updateScreen();
Commit: 794eeec90c42c5472ff7aed52707120024529c6a
https://github.com/scummvm/scummvm/commit/794eeec90c42c5472ff7aed52707120024529c6a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:06+02:00
Commit Message:
EEM: strips leading spaces
Changed paths:
engines/eem/clues.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 68a66b1aa0d..27089a2696f 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -675,7 +675,25 @@ Common::String EEMEngine::parseString(const Common::String &raw,
break;
}
}
- return out;
+
+ // Strip leading spaces at the start of each emitted line. Mirrors
+ // `_DoWordWrap @ 1b66:04a7`, which advances past spaces at the
+ // start of every output line via `for (; str[last] == ' '; last++)`.
+ // ~60% of mystery-text strings carry 1-2 leading spaces in the data
+ // (verified across all CD M*.BIN files); the original WordWrap
+ // discards them, so we do the same before the text reaches
+ // `Font::wordWrapText` (which only trims at wrap-induced line
+ // boundaries, not at start-of-input or after an embedded '\n').
+ Common::String cleaned;
+ bool atLineStart = true;
+ for (uint i = 0; i < out.size(); i++) {
+ const char ch = out[i];
+ if (atLineStart && ch == ' ')
+ continue;
+ cleaned += ch;
+ atLineStart = (ch == '\n');
+ }
+ return cleaned;
}
void EEMEngine::applyClueSideEffects(const byte *c) {
Commit: 3a87e8fc489eaca75c90176ee46adc12c1dcdaab
https://github.com/scummvm/scummvm/commit/3a87e8fc489eaca75c90176ee46adc12c1dcdaab
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:07+02:00
Commit Message:
EEM: refactory doGallery into several functions
Changed paths:
engines/eem/eem.h
engines/eem/ui.cpp
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 32eb70388f5..8578d1a2d6c 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -221,6 +221,18 @@ public:
/// Show suspect gallery. Mirrors `_DrawGallery` @ 158f:0046.
void doGallery();
+ /// Suspect-detail view inside the gallery. Mirrors
+ /// `MoreInfo @ 158f:0419`: paints PIC 0x3f + suspect picture at
+ /// (0x94, 0x0f), paginates the suspect's found clues inside the
+ /// `_GalleryNoteRect`, and dispatches PDA bottom-bar buttons via
+ /// `_HandleMoreButton @ 158f:027d`. Sets `_nextScreen` and
+ /// returns true when the user picks a button that should exit
+ /// `doGallery` outright (NOTEBOOK / ACCUSE / MAP); returns false
+ /// for plain dismissal (ESC / GALLERY button) so the caller stays
+ /// on the portrait grid.
+ bool moreInfo(const byte *gd, uint suspectIdx,
+ const Picture &galBg, bool haveBg);
+
/// Show big map; click chooses next site. Mirrors `_DoBigMap` @ 20fe:09e7.
void doBigMap();
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 1c3c0863398..696a5e389d8 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -2787,235 +2787,9 @@ void EEMEngine::doGallery() {
if (slotSuspect[i] < 0)
continue;
if (slotRects[i].contains(ev.mouse.x, ev.mouse.y)) {
- // `MoreInfo(i)` â show the suspect detail page.
- // Mirrors `MoreInfo @ 158f:0419`:
- // _RefreshGalleryBackground();
- // _GetPicture(*(u16*)(gd + i*0x46));
- // _AddPicBackground(pic, 0x94, 0xf);
- // _DrawGalleryNotes(gd + i*0x46);
- // loop until ESC or button click.
- // Suspect data layout differs by variant:
- // * CD (`158f:0419`): fixed 0x46-byte stride.
- // +0..1 picId
- // +8..9 clue count (u16)
- // +0xa.. array of u16 clue IDs (max 30,
- // terminated by 0xFFFF if short).
- // * Floppy (`_MoreInfo_Floppy = 154e:042b` â
- // `FUN_154e_0201` â `FUN_15e0_01e8`):
- // variable-stride.
- // +0..1 picId
- // +2..3 alibi (0xFFFF = guilty)
- // +4 clue count (u8)
- // +5.. u8 clue IDs (per the asm at
- // 154e:020e..0282 which calls
- // `FUN_15e0_01e8(rect, entry+5,
- // entry[4], NULL)`).
- const bool floppyMI = isFloppy();
- const uint suspectIdx = (uint)slotSuspect[i];
- const byte *suspect = floppyMI
- ? _mystery.floppySuspectEntry(suspectIdx)
- : gd + suspectIdx * 0x46;
- if (!suspect)
- break;
- const uint16 detailPic =
- READ_LE_UINT16(suspect + 0);
- const uint clueCount = floppyMI
- ? (uint)suspect[4]
- : READ_LE_UINT16(suspect + 8);
-
- Graphics::ManagedSurface ms(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- ms.clear();
- if (haveBg) {
- const int bw = MIN<int>(galBg.surface.w, 320);
- const int bh = MIN<int>(galBg.surface.h, 200);
- for (int row = 0; row < bh; row++) {
- memcpy((byte *)ms.getBasePtr(0, row),
- (const byte *)galBg.surface.getBasePtr(0, row), bw);
- }
- }
- // Partner sprite at (5, 0x50). The original
- // `MoreInfo @ 158f:0419` calls
- // `_RefreshGalleryBackground` (clears the
- // portrait grid) but the partner anim slot
- // registered by `_DoGallery` keeps painting
- // at every `_UpdateAnimations` tick â the
- // suspect detail pic covers the right side
- // only (drawn at 0x94, 0xf), so the partner
- // stays visible on the left. Without this
- // blit the suspect-detail screen has no
- // partner.
- setInteractiveMouseCursor(false);
- {
- const uint partnerAnim =
- (_partner == 0) ? 2 : 0x10;
- Animation partnerAni;
- if (_aniArchive.loadAnimation(partnerAnim,
- partnerAni) &&
- !partnerAni.empty()) {
- const uint32 now = g_system->getMillis();
- const uint frameIdx =
- partnerFrameAtTick(0x02,
- (uint)partnerAni.size(), now);
- blitAnimFrameAnchored(ms.surfacePtr(),
- partnerAni[frameIdx], 5, 0x50);
- }
- }
- // Full suspect picture at (0x94, 0xf).
- Picture detail;
- if (_picsArchive.getPicture(detailPic, detail)) {
- const byte transp =
- (byte)(detail.flags >> 8);
- const int dx = 0x94, dy = 0x0f;
- const int dw = MIN<int>(detail.surface.w, 320 - dx);
- const int dh = MIN<int>(detail.surface.h, 200 - dy);
- for (int row = 0; row < dh; row++) {
- const byte *src =
- (const byte *)detail.surface.getBasePtr(0, row);
- byte *dst =
- (byte *)ms.getBasePtr(0, dy + row);
- for (int col = 0; col < dw; col++) {
- if (src[col] != transp)
- dst[dx + col] = src[col];
- }
- }
- }
- // Suspect's clue notes inside _GalleryNoteRect
- // = (78, 93, 288, 152), per 29be:0100. Cyan text
- // renders directly on the PDA's natural blue
- // screen â matches `_DrawGalleryNotes @ 158f:01f4`.
- const int rx = 78, ry = 93;
- const int rw = 288 - 78, rh = 152 - 93;
-
- const byte *ni = _mystery.noteIndex();
- const uint16 niCount = _mystery.noteIndexCount();
- int yPos = ry;
- const int lineH = _font.getFontHeight();
- bool drewAny = false;
- const uint clueMax = floppyMI ? clueCount : 30u;
- for (uint k = 0; k < clueCount && k < clueMax; k++) {
- const uint16 clueId = floppyMI
- ? (uint16)suspect[5 + k]
- : READ_LE_UINT16(suspect + 0xa + k * 2);
- if (!floppyMI && clueId == 0xFFFF)
- break;
- if (clueId >= Mystery::kCluesFoundCap ||
- !_mystery._cluesFound[clueId])
- continue;
- if (!ni || clueId >= niCount)
- continue;
- // Floppy notes are 7-byte entries:
- // +0..1 clue text (absolute offset)
- // +2..3 Jake spoken line
- // +4..5 Jenny spoken line
- // +6 score
- // `_DrawNotes_Floppy / FUN_15e0_01e8`'s
- // `*(int *)(notes + idx * 7)` shows the
- // notebook always uses +0 â the partner-
- // agnostic clue statement. The +2/+4
- // offsets are the partner spoken lines used
- // by `FUN_22dc_05c8` when rendering dialog
- // records (NOT this notebook view). CD
- // notes are 4 bytes: u16 textOff, u16
- // score.
- Common::String txt;
- if (floppyMI) {
- const uint16 textOff =
- READ_LE_UINT16(ni + clueId * 7);
- const byte *bb = _mystery.blobAt(0);
- const uint32 dsz =
- _mystery.dataSize();
- if (bb && textOff != 0 &&
- textOff < dsz) {
- const char *p =
- (const char *)(bb + textOff);
- uint32 len = 0;
- while (textOff + len < dsz &&
- p[len] != 0)
- len++;
- txt = parseString(
- Common::String(p, len),
- _playerName, _partner);
- }
- } else {
- const uint16 textOff =
- READ_LE_UINT16(ni + clueId * 4);
- txt = parseString(
- _mystery.textAt(textOff),
- _playerName, _partner);
- }
- if (txt.empty())
- continue;
- const byte color =
- _mystery._noteSelected[clueId] ? 0x3C : 0x5C;
- const int hLine = _font.drawWordWrapped(
- &ms, rx, yPos, rw, txt, color);
- yPos += hLine + 7;
- drewAny = true;
- if (yPos + lineH > ry + rh)
- break;
- }
- if (!drewAny && _font.isLoaded()) {
- _font.drawString(&ms,
- isSpanish()
- ? "Aun no hay pistas para este sospechoso."
- : "No clues yet for this suspect.",
- rx, ry, rw, 0x5C);
- }
- // Header / footer text.
- if (_font.isLoaded()) {
- _font.drawString(&ms,
- isSpanish() ? "EXPEDIENTE" : "SUSPECT FILE",
- rx, ry - 11, rw, 0x3C);
- _font.drawString(&ms,
- isSpanish() ? "(clic / ESC: volver)"
- : "(click / ESC: back)",
- rx, ry + rh + 2, rw, 0x3C);
- }
- g_system->copyRectToScreen(ms.getPixels(),
- ms.pitch, 0, 0, 320, 200);
- g_system->updateScreen();
-
- // Wait for click or ESC. Drain the queued
- // LBUTTONDOWN that triggered this MoreInfo first
- // so we don't immediately accept it as the
- // dismiss event.
- g_system->delayMillis(150);
- {
- Common::Event drain;
- while (g_system->getEventManager()->pollEvent(drain)) {
- if (drain.type == Common::EVENT_QUIT ||
- drain.type == Common::EVENT_RETURN_TO_LAUNCHER) {
- setInteractiveMouseCursor(false);
- return;
- }
- }
- }
- bool back = false;
- while (!back && !shouldQuit()) {
- Common::Event e2;
- while (g_system->getEventManager()->pollEvent(e2)) {
- if (e2.type == Common::EVENT_LBUTTONDOWN ||
- (e2.type == Common::EVENT_KEYDOWN &&
- (e2.kbd.keycode == Common::KEYCODE_ESCAPE ||
- e2.kbd.keycode == Common::KEYCODE_RETURN))) {
- back = true;
- break;
- }
- if (e2.type == Common::EVENT_QUIT ||
- e2.type == Common::EVENT_RETURN_TO_LAUNCHER) {
- setInteractiveMouseCursor(false);
- return;
- }
- }
- // Per-tick `updateScreen()` so the SDL cursor
- // follows the mouse â without it the cursor
- // freezes on entry to the MoreInfo screen
- // (we never repaint here, so the cursor never
- // gets drawn at its current position).
- g_system->updateScreen();
- g_system->delayMillis(20);
- }
+ if (moreInfo(gd, (uint)slotSuspect[i],
+ galBg, haveBg))
+ exitFlag = true;
// Force gallery redraw immediately so the
// player isn't left looking at the dismissed
// MoreInfo screen until the next 100 ms tick.
@@ -3058,6 +2832,358 @@ void EEMEngine::doGallery() {
setInteractiveMouseCursor(false);
}
+bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
+ const Picture &galBg, bool haveBg) {
+ // Suspect-detail page. Mirrors `MoreInfo @ 158f:0419`:
+ // _RefreshGalleryBackground();
+ // _GetPicture(*(u16*)(gd + i*0x46));
+ // _AddPicBackground(pic, 0x94, 0xf);
+ // _DrawGalleryNotes(gd + i*0x46);
+ // loop until ESC or button click.
+ // Suspect data layout differs by variant:
+ // * CD (`158f:0419`): fixed 0x46-byte stride.
+ // +0..1 picId
+ // +8..9 clue count (u16)
+ // +0xa.. array of u16 clue IDs (max 30,
+ // terminated by 0xFFFF if short).
+ // * Floppy (`_MoreInfo_Floppy = 154e:042b` â
+ // `FUN_154e_0201` â `FUN_15e0_01e8`):
+ // variable-stride.
+ // +0..1 picId
+ // +2..3 alibi (0xFFFF = guilty)
+ // +4 clue count (u8)
+ // +5.. u8 clue IDs (per the asm at
+ // 154e:020e..0282 which calls
+ // `FUN_15e0_01e8(rect, entry+5,
+ // entry[4], NULL)`).
+ const bool floppyMI = isFloppy();
+ const byte *suspect = floppyMI
+ ? _mystery.floppySuspectEntry(suspectIdx)
+ : gd + suspectIdx * 0x46;
+ if (!suspect)
+ return false;
+ const uint16 detailPic = READ_LE_UINT16(suspect + 0);
+ const uint clueCount = floppyMI
+ ? (uint)suspect[4]
+ : READ_LE_UINT16(suspect + 8);
+
+ setInteractiveMouseCursor(false);
+
+ // Suspect's clue notes inside _GalleryNoteRect
+ // = (78, 93, 288, 152), per 29be:0100. Cyan text
+ // renders directly on the PDA's natural blue
+ // screen â matches `_DrawGalleryNotes @ 158f:01f4`.
+ const int rx = 78, ry = 93;
+ const int rw = 288 - 78, rh = 152 - 93;
+ const int lineH = _font.getFontHeight();
+ const uint clueMax = floppyMI ? clueCount : 30u;
+ const byte *ni = _mystery.noteIndex();
+ const uint16 niCount = _mystery.noteIndexCount();
+
+ // Pagination matches `_DrawNotes @ 161e:01d0` / `MoreInfo @
+ // 158f:0419`: the original tracks `_NextClue` across redraws and
+ // only emits clues whose wrapped height fits the rect, deferring
+ // the rest to the next page. `_PageBreaks[]` stores the
+ // entry-point for each page so PREV (button 6 / case 6 at
+ // 158f:03b8 â `SUB _CurrentPage, 2; _NextClue = _PageBreaks[...]`)
+ // can step back. We mirror that with an explicit stack of
+ // page-start indices: forward push, back pop.
+ uint pageStart = 0;
+ Common::Array<uint> pageStack;
+ bool back = false;
+ bool exitGallery = false;
+ bool isFirstShow = true;
+
+ while (!back && !shouldQuit()) {
+ Graphics::ManagedSurface ms(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ ms.clear();
+ if (haveBg) {
+ const int bw = MIN<int>(galBg.surface.w, 320);
+ const int bh = MIN<int>(galBg.surface.h, 200);
+ for (int row = 0; row < bh; row++) {
+ memcpy((byte *)ms.getBasePtr(0, row),
+ (const byte *)galBg.surface.getBasePtr(0, row), bw);
+ }
+ }
+ // Partner sprite at (5, 0x50). The original `MoreInfo @
+ // 158f:0419` calls `_RefreshGalleryBackground` (clears the
+ // portrait grid) but the partner anim slot registered by
+ // `_DoGallery` keeps painting at every `_UpdateAnimations`
+ // tick â the suspect detail pic covers the right side only
+ // (drawn at 0x94, 0xf), so the partner stays visible on the
+ // left. Re-blit per page so the frame stays current.
+ {
+ const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
+ Animation partnerAni;
+ if (_aniArchive.loadAnimation(partnerAnim, partnerAni) &&
+ !partnerAni.empty()) {
+ const uint32 now = g_system->getMillis();
+ const uint frameIdx = partnerFrameAtTick(0x02,
+ (uint)partnerAni.size(), now);
+ blitAnimFrameAnchored(ms.surfacePtr(),
+ partnerAni[frameIdx], 5, 0x50);
+ }
+ }
+ // Full suspect picture at (0x94, 0xf).
+ Picture detail;
+ if (_picsArchive.getPicture(detailPic, detail)) {
+ const byte transp = (byte)(detail.flags >> 8);
+ const int dx = 0x94, dy = 0x0f;
+ const int dw = MIN<int>(detail.surface.w, 320 - dx);
+ const int dh = MIN<int>(detail.surface.h, 200 - dy);
+ for (int row = 0; row < dh; row++) {
+ const byte *src = (const byte *)
+ detail.surface.getBasePtr(0, row);
+ byte *dst = (byte *)ms.getBasePtr(0, dy + row);
+ for (int col = 0; col < dw; col++) {
+ if (src[col] != transp)
+ dst[dx + col] = src[col];
+ }
+ }
+ }
+
+ // Walk the clue list from `pageStart`. Skip clues that aren't
+ // found / lack a note entry, then measure the wrapped height
+ // before drawing. If a clue would overflow and we've already
+ // drawn at least one, defer it to the next page; if it's the
+ // FIRST clue on this page (one clue too tall to ever fit),
+ // draw it anyway so progress is guaranteed.
+ int yPos = ry;
+ bool drewAny = false;
+ uint k = pageStart;
+ bool reachedEnd = false;
+ for (; k < clueCount && k < clueMax; k++) {
+ const uint16 clueId = floppyMI
+ ? (uint16)suspect[5 + k]
+ : READ_LE_UINT16(suspect + 0xa + k * 2);
+ if (!floppyMI && clueId == 0xFFFF) {
+ reachedEnd = true;
+ break;
+ }
+ if (clueId >= Mystery::kCluesFoundCap ||
+ !_mystery._cluesFound[clueId])
+ continue;
+ if (!ni || clueId >= niCount)
+ continue;
+ // Floppy notes: 7-byte entries (+0..1 clue text, +2..3 Jake
+ // spoken, +4..5 Jenny spoken, +6 score). Notebook always
+ // uses +0 (partner-agnostic statement) per `FUN_15e0_01e8`.
+ // CD notes are 4-byte: u16 textOff + u16 score.
+ Common::String txt;
+ if (floppyMI) {
+ const uint16 textOff = READ_LE_UINT16(ni + clueId * 7);
+ const byte *bb = _mystery.blobAt(0);
+ const uint32 dsz = _mystery.dataSize();
+ if (bb && textOff != 0 && textOff < dsz) {
+ const char *p = (const char *)(bb + textOff);
+ uint32 len = 0;
+ while (textOff + len < dsz && p[len] != 0)
+ len++;
+ txt = parseString(Common::String(p, len),
+ _playerName, _partner);
+ }
+ } else {
+ const uint16 textOff = READ_LE_UINT16(ni + clueId * 4);
+ txt = parseString(_mystery.textAt(textOff),
+ _playerName, _partner);
+ }
+ if (txt.empty())
+ continue;
+
+ Common::Array<Common::String> wrapped;
+ _font.wordWrapText(txt, MAX<int>(8, rw), wrapped);
+ const int hClue = (int)wrapped.size() * lineH;
+ if (yPos + hClue > ry + rh && drewAny) {
+ // Defer to next page.
+ break;
+ }
+ const byte color = _mystery._noteSelected[clueId]
+ ? 0x3C : 0x5C;
+ for (uint l = 0; l < wrapped.size(); l++) {
+ _font.drawString(&ms, wrapped[l], rx,
+ yPos + (int)l * lineH, MAX<int>(8, rw), color);
+ }
+ yPos += hClue + 7;
+ drewAny = true;
+ }
+ if (k >= clueCount || k >= clueMax)
+ reachedEnd = true;
+ const uint pageEnd = k;
+ const bool hasMore = !reachedEnd;
+ const bool hasPrev = !pageStack.empty();
+
+ if (pageStart == 0 && !drewAny && _font.isLoaded()) {
+ _font.drawString(&ms,
+ isSpanish()
+ ? "Aun no hay pistas para este sospechoso."
+ : "No clues yet for this suspect.",
+ rx, ry, MAX<int>(8, rw), 0x5C);
+ }
+ // Header / footer text. The PDA's NEXT (case 5) and PREV
+ // (case 6) icons are visible on the bottom bar â the footer
+ // just covers dismissal here.
+ if (_font.isLoaded()) {
+ _font.drawString(&ms,
+ isSpanish() ? "EXPEDIENTE" : "SUSPECT FILE",
+ rx, ry - 11, MAX<int>(8, rw), 0x3C);
+ _font.drawString(&ms,
+ isSpanish() ? "(ESC: volver)" : "(ESC: back)",
+ rx, ry + rh + 2, MAX<int>(8, rw), 0x3C);
+ }
+ g_system->copyRectToScreen(ms.getPixels(), ms.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+
+ // Drain the queued LBUTTONDOWN that brought us into MoreInfo
+ // so it doesn't get caught by the input loop below. Subsequent
+ // pages don't need this.
+ if (isFirstShow) {
+ isFirstShow = false;
+ g_system->delayMillis(150);
+ Common::Event drain;
+ while (g_system->getEventManager()->pollEvent(drain)) {
+ if (drain.type == Common::EVENT_QUIT ||
+ drain.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ _nextScreen = kScreenInvalid;
+ return true;
+ }
+ }
+ }
+
+ // Wait for input. Each PDA button maps to a distinct action,
+ // mirroring `_HandleMoreButton @ 158f:027d`:
+ // case 0 NOTEBOOK -> NextScreen=4
+ // case 1 HELP -> _InterfaceHelp(0)
+ // case 2 GALLERY -> close MoreInfo
+ // case 3 PARTNER head-> _KDHelp + redraw
+ // case 4 ACCUSE -> NextScreen=7
+ // case 5 PAGE NEXT -> next page
+ // case 6 PAGE PREV -> prev page
+ // case 7 MAP -> NextScreen=2
+ // case 8 SITE -> no-op
+ // case 10 HELP (alt) -> _InterfaceHelp(0)
+ // Clicks outside any known button rect are no-ops, matching
+ // the original (which short-circuits on `_FindButton == -1`).
+ // ESC closes; PgDn / PgUp / Return / Backspace also paginate
+ // as a modern accessibility convenience.
+ bool advance = false;
+ bool prev = false;
+ bool redraw = false;
+ while (!back && !advance && !prev && !redraw && !shouldQuit()) {
+ Common::Event e2;
+ while (g_system->getEventManager()->pollEvent(e2)) {
+ if (e2.type == Common::EVENT_QUIT ||
+ e2.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ _nextScreen = kScreenInvalid;
+ return true;
+ }
+ if (e2.type == Common::EVENT_KEYDOWN &&
+ e2.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ back = true;
+ break;
+ }
+ if (e2.type == Common::EVENT_LBUTTONDOWN) {
+ const int mx = e2.mouse.x;
+ const int my = e2.mouse.y;
+ debugC(2, kDebugGfx,
+ "MoreInfo click (%d,%d) hasMore=%d hasPrev=%d",
+ mx, my, (int)hasMore, (int)hasPrev);
+ if (kPdaNotebookRect.contains(mx, my)) {
+ _nextScreen = kScreenNotebook;
+ exitGallery = true;
+ back = true;
+ break;
+ }
+ if (kPdaAccuseRect.contains(mx, my)) {
+ _nextScreen = kScreenAccuse;
+ exitGallery = true;
+ back = true;
+ break;
+ }
+ if (kPdaPartnerFootMapRect.contains(mx, my)) {
+ _nextScreen = kScreenMapAlt;
+ exitGallery = true;
+ back = true;
+ break;
+ }
+ if (kPdaHelpRect.contains(mx, my) ||
+ kPdaHelp2Rect.contains(mx, my)) {
+ // Help overlay; re-render the current page
+ // after it dismisses.
+ setInteractiveMouseCursor(false);
+ doInterfaceHelp(0);
+ redraw = true;
+ break;
+ }
+ if (kPdaGalleryRect.contains(mx, my)) {
+ // Case 2: close MoreInfo and return to the
+ // portrait grid.
+ back = true;
+ break;
+ }
+ if (kPdaPageNextRect.contains(mx, my)) {
+ if (hasMore)
+ advance = true;
+ break;
+ }
+ if (kPdaPagePrevRect.contains(mx, my)) {
+ if (hasPrev)
+ prev = true;
+ break;
+ }
+ if (kPdaPartnerHeadHintRect.contains(mx, my)) {
+ // Case 3: ask KD for a hint then redraw the
+ // current page over the clean BG.
+ setInteractiveMouseCursor(false);
+ doHelp();
+ redraw = true;
+ break;
+ }
+ // Case 8 SITE and click outside any known button
+ // = no-op. The original `_FindButton`
+ // short-circuits when no rect matches.
+ break;
+ }
+ if (e2.type == Common::EVENT_KEYDOWN && hasMore &&
+ (e2.kbd.keycode == Common::KEYCODE_RETURN ||
+ e2.kbd.keycode == Common::KEYCODE_KP_ENTER ||
+ e2.kbd.keycode == Common::KEYCODE_SPACE ||
+ e2.kbd.keycode == Common::KEYCODE_PAGEDOWN ||
+ e2.kbd.keycode == Common::KEYCODE_RIGHT)) {
+ advance = true;
+ break;
+ }
+ if (e2.type == Common::EVENT_KEYDOWN && hasPrev &&
+ (e2.kbd.keycode == Common::KEYCODE_BACKSPACE ||
+ e2.kbd.keycode == Common::KEYCODE_LEFT ||
+ e2.kbd.keycode == Common::KEYCODE_PAGEUP)) {
+ prev = true;
+ break;
+ }
+ }
+ // Per-tick `updateScreen()` so the SDL cursor follows the
+ // mouse â without it the cursor freezes on entry to the
+ // MoreInfo screen.
+ g_system->updateScreen();
+ g_system->delayMillis(20);
+ }
+
+ if (advance) {
+ pageStack.push_back(pageStart);
+ pageStart = pageEnd;
+ } else if (prev && !pageStack.empty()) {
+ pageStart = pageStack.back();
+ pageStack.pop_back();
+ }
+ // `redraw` falls through with the same `pageStart` and
+ // re-iterates the outer while, repainting on top of whatever
+ // the help overlay left behind.
+ }
+
+ return exitGallery;
+}
+
void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
Common::Array<Common::Rect> &slotRects,
Common::Array<int> &slotSuspect) {
Commit: b25891a6870885f6b151bcfade71f373976f243d
https://github.com/scummvm/scummvm/commit/b25891a6870885f6b151bcfade71f373976f243d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:07+02:00
Commit Message:
EEM: openMainMenuDialog when using ESC in most of the screens
Changed paths:
engines/eem/site.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index f48d44dc276..8b4c6c3fb25 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -817,20 +817,15 @@ void SiteScreen::run() {
case Common::EVENT_KEYDOWN:
notePartnerActivity();
- // `_DoSiteLoop @ 168d:07e1` only dispatches on the
- // 6-entry table at `168d:09d5` (TAB / ENTER / arrow
- // keys for hotspot cursor cycling) plus ESC handled
- // separately at 168d:07a9. We don't implement the
- // hotspot cursor cycling, so the only keyboard binding kept
- // here is ESC (matches `_ESCHit` -> "Are you sure?" -> MAP).
+ // `_DoSiteLoop @ 168d:07e1` originally routed ESC
+ // through `_ESCHit` -> "Are you sure?" -> MAP. The
+ // site has visible MAP / SETUP / etc. controls along
+ // the top bar, so ESC now opens the ScummVM in-game
+ // menu (save / load / quit) instead of the back-nav
+ // shortcut.
if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
_vm->setHotspotMouseCursor(false);
- if (_vm->areYouSure()) {
- _vm->setNextScreen(_vm->isFloppy() ? kScreenMapAlt
- : kScreenMap);
- _vm->stopMusic();
- return;
- }
+ _vm->openMainMenuDialog();
enter(cur, false);
mouse = g_system->getEventManager()->getMousePos();
updateHotspotCursor(cur, mouse.x, mouse.y);
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 696a5e389d8..fc49dbbf559 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -1934,9 +1934,8 @@ void EEMEngine::doActionScreen() {
continue;
const Common::KeyCode k = ev.kbd.keycode;
if (k == Common::KEYCODE_ESCAPE) {
- exitChosen = true;
- confirmed = true;
- break;
+ openMainMenuDialog();
+ continue;
}
if (k == Common::KEYCODE_RETURN ||
k == Common::KEYCODE_KP_ENTER) {
@@ -2173,6 +2172,13 @@ void EEMEngine::doCaseSelection() {
const uint idx = topRow + (uint)row;
if (idx >= listLen || solvedFlags[idx])
continue;
+ // Second click on the already-selected row counts as
+ // the OK button â saves the player a trip down to the
+ // bottom-bar after picking a mystery.
+ if (idx == selRow) {
+ confirmed = true;
+ break;
+ }
selRow = idx;
dirty = true;
continue;
@@ -2183,8 +2189,8 @@ void EEMEngine::doCaseSelection() {
continue;
const Common::KeyCode k = ev.kbd.keycode;
if (k == Common::KEYCODE_ESCAPE) {
- _mystery.clear();
- return;
+ openMainMenuDialog();
+ continue;
}
if (k == Common::KEYCODE_RETURN ||
k == Common::KEYCODE_KP_ENTER) {
@@ -2352,9 +2358,8 @@ void EEMEngine::doNotebook() {
}
if (ev.type == Common::EVENT_KEYDOWN) {
if (ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
- _nextScreen = kScreenSite;
- exitFlag = true;
- break;
+ openMainMenuDialog();
+ continue;
}
if (ev.kbd.keycode == Common::KEYCODE_LEFT ||
ev.kbd.keycode == Common::KEYCODE_PAGEUP) {
@@ -2724,9 +2729,8 @@ void EEMEngine::doGallery() {
}
if (ev.type == Common::EVENT_KEYDOWN) {
if (ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
- _nextScreen = kScreenSite;
- exitFlag = true;
- break;
+ openMainMenuDialog();
+ continue;
}
}
if (ev.type == Common::EVENT_LBUTTONDOWN) {
@@ -3080,7 +3084,8 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
}
if (e2.type == Common::EVENT_KEYDOWN &&
e2.kbd.keycode == Common::KEYCODE_ESCAPE) {
- back = true;
+ openMainMenuDialog();
+ redraw = true;
break;
}
if (e2.type == Common::EVENT_LBUTTONDOWN) {
@@ -3386,8 +3391,10 @@ void EEMEngine::doBigMap() {
ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
return;
if (ev.type == Common::EVENT_KEYDOWN &&
- ev.kbd.keycode == Common::KEYCODE_ESCAPE)
- return;
+ ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ openMainMenuDialog();
+ continue;
+ }
if (ev.type == Common::EVENT_LBUTTONDOWN) {
// SetupButtonRect â `_NextScreen = 6` (the original's
// settings screen, mirrors `_DoBigMap @ 20fe:0c33`
@@ -3503,8 +3510,9 @@ void EEMEngine::doBigMap() {
}
if (ev.type == Common::EVENT_KEYDOWN) {
if (ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
- setInteractiveMouseCursor(false);
- return; // exit detail back to caller (site loop / engine)
+ openMainMenuDialog();
+ dirty = true;
+ continue;
}
const int kStep = 16;
if (ev.kbd.keycode == Common::KEYCODE_LEFT) {
@@ -4144,8 +4152,9 @@ bool EEMEngine::doAccuseNotes() {
}
if (ev.type == Common::EVENT_KEYDOWN) {
if (ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
- _nextScreen = kScreenSite;
- return false;
+ openMainMenuDialog();
+ dirty = true;
+ continue;
}
if (ev.kbd.keycode == Common::KEYCODE_LEFT &&
page > 0) {
@@ -4666,7 +4675,9 @@ void EEMEngine::doAccuse() {
if (ev.type == Common::EVENT_KEYDOWN) {
switch (ev.kbd.keycode) {
case Common::KEYCODE_ESCAPE:
- return;
+ openMainMenuDialog();
+ dirty = true;
+ continue;
case Common::KEYCODE_TAB:
case Common::KEYCODE_RIGHT:
highlighted = nextLiveSlot(slotRects, highlighted, +1);
@@ -5446,8 +5457,11 @@ void EEMEngine::doAccuseFloppy() {
ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
return;
if (ev.type == Common::EVENT_KEYDOWN) {
- if (ev.kbd.keycode == Common::KEYCODE_ESCAPE)
- return;
+ if (ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ openMainMenuDialog();
+ drawGallery(highlighted, slotRects, slotSuspect);
+ continue;
+ }
if (ev.kbd.keycode == Common::KEYCODE_TAB ||
ev.kbd.keycode == Common::KEYCODE_RIGHT) {
highlighted = (highlighted + 1) % MAX<int>(1, (int)num);
Commit: a6c48ae179ca0ad35dd46f346abe8a9ddd19aced
https://github.com/scummvm/scummvm/commit/a6c48ae179ca0ad35dd46f346abe8a9ddd19aced
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:07+02:00
Commit Message:
EEM: improved animation precision with correct framerate and full script table
Changed paths:
engines/eem/site.cpp
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 8b4c6c3fb25..a6a7647c956 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -251,6 +251,41 @@ const AnimScript kAnimScripts[] = {
// 0x16 (29be:185e, alias of 0x00) â Jenny CaseSelection greeter,
// same blink script as 0x15.
{ 0x16, 10, { 0,0,0,0,0,0,0,0,0,2 } },
+ // Site / drop scripts â¤28 frames â see `_AnimationSequences @
+ // 29be:22d4`. Many of these are short count-ups used by ambient
+ // animations (people walking, vehicles passing) that the original
+ // drives one entry per `_CheckFrameRate` tick (~140 ms). Without
+ // these, our generic fallback cycles through every animation cell
+ // at one entry per tick and the ambient anims look 2-3Ã too fast.
+ // 0x1b (29be:192e, alias of 0x07) â walk-cycle 0..9.
+ { 0x1b, 10, { 0,1,2,3,4,5,6,7,8,9 } },
+ // 0x1c (29be:21a8) â short 6-frame count-up.
+ { 0x1c, 6, { 0,1,2,3,4,5 } },
+ // 0x1d (29be:21a8, alias of 0x1c).
+ { 0x1d, 6, { 0,1,2,3,4,5 } },
+ // 0x21 (29be:1b86) â paired-step idle bob.
+ { 0x21, 14, { 0,0,0,1,1,2,2,3,3,3,2,2,1,1 } },
+ // 0x25 (29be:218a) â 3-frame trigger.
+ { 0x25, 3, { 0,1,2 } },
+ // 0x26 (29be:1d3e) â count-up 0..17 (18 frames).
+ { 0x26, 18, { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17 } },
+ // 0x27 (29be:1d64) â count-up 0..11 (12 frames).
+ { 0x27, 12, { 0,1,2,3,4,5,6,7,8,9,10,11 } },
+ // 0x2a (29be:1e50) â 5-step micro-anim.
+ { 0x2a, 5, { 0,1,2,2,3 } },
+ // 0x2e (29be:21ce) â count-up 0..12 (13 frames).
+ { 0x2e, 13, { 0,1,2,3,4,5,6,7,8,9,10,11,12 } },
+ // 0x2f (29be:21ea) â count-up 0..22 (23 frames).
+ { 0x2f, 23, { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,
+ 20,21,22 } },
+ // 0x32 (29be:219c) â count-up 0..4 (5 frames).
+ { 0x32, 5, { 0,1,2,3,4 } },
+ // 0x33 (29be:2192) â count-up 0..3 (4 frames).
+ { 0x33, 4, { 0,1,2,3 } },
+ // 0x34 (29be:219c, alias of 0x32).
+ { 0x34, 5, { 0,1,2,3,4 } },
+ // 0x35 (29be:21b6) â count-up 0..10 (11 frames).
+ { 0x35, 11, { 0,1,2,3,4,5,6,7,8,9,10 } },
// Briefing animations â `_DoInitClues @ 1a35:0411` calls
// `_NewAnimation(..., (PicData *)CONCAT22(0x17, ...), 1, ...)`
// for the game animation (always anim ID 0x17 â even Jenny's
@@ -265,29 +300,228 @@ const AnimScript kAnimScripts[] = {
};
static_assert(true, "see kAnimScriptsLong below for >28-frame scripts");
-// Scripts longer than 28 frames live here so the main `kAnimScripts`
-// table can keep its tight `frames[28]` storage (the lookup in
-// `findAnimScript` checks both arrays). Used for the briefing
-// animations whose original scripts run 30 frames each.
+// Scripts longer than 28 frames live here. The lookup in
+// `findAnimScript` checks both arrays. Stored as
+// `(seqnum, len, ptr)` so each script can be any length without
+// bloating every entry â the longest (0x22) runs 115 frames.
struct AnimScriptLong {
uint16 seqnum;
- uint8 len;
- uint8 frames[36];
+ uint16 len;
+ const uint8 *frames;
+};
+
+// Briefing animations â `_DoInitClues @ 1a35:0411` calls
+// `_NewAnimation(..., (PicData *)CONCAT22(0x17, ...), 1, ...)` for the
+// game animation (always anim ID 0x17 â even Jenny's briefing reuses
+// Jake's SCRIPT, even though the loaded ANI.DBD cells come from her
+// partner-specific entry 0x3b). Same pattern for book (0x18) / nancy
+// (0x19).
+static const uint8 kScript17[] = {
+ 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,
+ 20,21,22,23,24,25,26,27,28,29
+};
+static const uint8 kScript18[] = {
+ 0,1,2,3,4,5,6,7,8,8,8,8,8,8,8,8,
+ 8,8,8,8,8,8,8,9,10,11,12,13,14,15
+};
+static const uint8 kScript19[] = {
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+ 1,2,3,4,5,6,7,8,9,10,11,12
+};
+
+// Site / NPC drop scripts (29be:22d4 entries 0x1a..0x36 minus the
+// short ones that fit in `kAnimScripts`). Many entries deliberately
+// repeat the same frame several times â that's the original's
+// "frame-hold" mechanism (the per-tick walk advances exactly one
+// entry, so K repeats hold the frame for K * `kFramePeriodMs` â
+// K * 140 ms). Without these scripts our generic fallback cycles
+// through every animation cell at one entry per tick, which is the
+// "site animations run too fast" symptom.
+
+// 0x1a (29be:19a4) â count-up 0..7, long idle hold, repeat 1..7,
+// idle, mirror 7..0, idle (77 entries).
+static const uint8 kScript1a[] = {
+ 0,1,2,3,4,5,6,7,
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+ 1,2,3,4,5,6,7,
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+ 7,6,5,4,3,2,1,
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
+};
+// 0x1e (29be:1a40) â slow walk-stutter with idle tail (76 entries).
+static const uint8 kScript1e[] = {
+ 0,1,2,3,3,3,3,4,4,3,4,4,4,4,4,3,
+ 5,5,5,5,5,5,5,5,4,4,4,4,4,4,4,4,
+ 5,5,5,5,5,5,6,5,6,5,7,7,7,7,7,7,
+ 7,8,7,7,7,7,7,8,
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
+};
+// 0x1f (29be:1ada) â 0..5, idle, 0..5, idle, 6..8 alternation,
+// idle (50 entries).
+static const uint8 kScript1f[] = {
+ 0,1,2,3,4,5,
+ 0,0,0,0,
+ 1,2,3,4,5,
+ 0,0,0,0,0,
+ 6,7,8,8,8,7,6,7,8,8,8,7,
+ 6,7,8,8,8,7,6,7,8,8,8,7,
+ 6,
+ 0,0,0,0,0
+};
+// 0x20 (29be:1b40) â count-up 0..33 (34 frames).
+static const uint8 kScript20[] = {
+ 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,
+ 20,21,22,23,24,25,26,27,28,29,30,31,32,33
+};
+// 0x22 (29be:1ba4) â long held-frame walker 0..22 with idle tail
+// (115 entries; most frames held 4-7 ticks each).
+static const uint8 kScript22[] = {
+ 0,
+ 1,1,1,1,1,
+ 2,2,2,2,2,
+ 3,3,3,3,3,
+ 4,4,4,4,
+ 5,5,5,5,
+ 6,6,6,6,
+ 7,7,7,7,
+ 8,8,8,8,
+ 9,9,9,9,
+ 10,10,10,10,10,
+ 11,11,11,11,11,
+ 12,12,12,12,12,12,
+ 13,13,13,13,13,
+ 14,14,14,14,
+ 15,15,15,15,
+ 16,16,16,16,16,16,16,
+ 17,17,17,17,
+ 18,18,18,18,
+ 19,19,19,19,
+ 20,20,20,20,
+ 21,21,21,21,
+ 22,22,22,22,
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
+};
+// 0x23 (29be:1c8c) â 29 entries: 0, 6 holds of 1, count-up 2..4,
+// down-up gesture, 5 idle frames.
+static const uint8 kScript23[] = {
+ 0,1,1,1,1,1,1,
+ 2,3,4,3,2,
+ 5,5,5,
+ 2,3,4,3,3,3,3,3,
+ 0,0,0,0,0,0
};
+// 0x24 (29be:1cc8) â bell-curve hold (58 entries): 0,0, 1,1, 2,2,
+// 3 held for 26 ticks, mirror back, idle.
+static const uint8 kScript24[] = {
+ 0,0,1,1,2,2,
+ 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,
+ 2,2,1,1,
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
+};
+// 0x28 (29be:1d7e) â gentle hold 0..3 with long hold on 3, mirror
+// back, idle (45 entries).
+static const uint8 kScript28[] = {
+ 0,1,1,2,2,
+ 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,
+ 2,2,1,1,
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
+};
+// 0x29 (29be:1dda) â paired-step count-up 0..21 plus idle
+// (58 entries).
+static const uint8 kScript29[] = {
+ 0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,
+ 11,11,12,12,13,13,14,14,15,15,16,16,17,17,18,18,19,19,
+ 20,20,21,21,
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
+};
+// 0x2b (29be:1e5c) â count-up 0..11 with each frame held 4 ticks
+// (48 entries).
+static const uint8 kScript2b[] = {
+ 0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,
+ 4,4,4,4,5,5,5,5,6,6,6,6,7,7,7,7,
+ 8,8,8,8,9,9,9,9,10,10,10,10,11,11,11,11
+};
+// 0x2c (29be:1ebe) â alternation walk 0..19 with idle tail
+// (54 entries).
+static const uint8 kScript2c[] = {
+ 0,1,2,3,4,5,
+ 0,
+ 6,7,8,9,10,10,10,10,10,10,
+ 11,11,11,
+ 12,13,14,15,
+ 0,
+ 16,17,18,19,
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
+};
+// 0x2d (29be:1f2c) â count-up 0..11 with each frame held 8 ticks
+// (96 entries).
+static const uint8 kScript2d[] = {
+ 0,0,0,0,0,0,0,0,
+ 1,1,1,1,1,1,1,1,
+ 2,2,2,2,2,2,2,2,
+ 3,3,3,3,3,3,3,3,
+ 4,4,4,4,4,4,4,4,
+ 5,5,5,5,5,5,5,5,
+ 6,6,6,6,6,6,6,6,
+ 7,7,7,7,7,7,7,7,
+ 8,8,8,8,8,8,8,8,
+ 9,9,9,9,9,9,9,9,
+ 10,10,10,10,10,10,10,10,
+ 11,11,11,11,11,11,11,11
+};
+// 0x30 (29be:1fee) â 0,0, count-up 1..19, idle, mirror down, extra
+// idle (86 entries).
+static const uint8 kScript30[] = {
+ 0,0,
+ 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,
+ 0,0,0,0,0,0,0,0,0,0,
+ 19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,
+ 5,4,4,3,3,2,2,1,1,
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
+};
+// 0x31 (29be:209c) â paired-step idle alternations (57 entries).
+static const uint8 kScript31[] = {
+ 0,0,0,1,1,1,
+ 0,0,0,1,1,1,
+ 2,2,2,3,3,3,
+ 2,2,2,3,3,3,
+ 4,4,4,5,5,5,
+ 4,4,4,5,5,5,
+ 3,3,3,2,2,2,
+ 3,3,3,2,2,2,
+ 1,1,1,
+ 0,0,0,
+ 1,1,1
+};
+// 0x36 (29be:2110) â 0..8 forward, 1..8 forward, frame 1 held 20
+// ticks, 8..0 mirror, idle tail (60 entries).
+static const uint8 kScript36[] = {
+ 0,1,2,3,4,5,6,7,8,
+ 1,2,3,4,5,6,7,8,
+ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+ 8,7,6,5,4,3,2,1,
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
+};
+
const AnimScriptLong kAnimScriptsLong[] = {
- // 0x17 (29be:221a) â briefing game count-up 0..29 (30 frames),
- // drives the per-tick frame walk of the game piece animation
- // during `_DoInitClues`.
- { 0x17, 30, { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,
- 20,21,22,23,24,25,26,27,28,29 } },
- // 0x18 (29be:2296) â briefing book: counts up to cell 8 then
- // holds for 16 ticks (the "thinking" pose) then count up 9..15.
- { 0x18, 30, { 0,1,2,3,4,5,6,7,8,8,8,8,8,8,8,8,
- 8,8,8,8,8,8,8,9,10,11,12,13,14,15 } },
- // 0x19 (29be:2258) â briefing nancy: 18 idle ticks then
- // count-up 1..12 (the late-arriving sidekick pose).
- { 0x19, 30, { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
- 1,2,3,4,5,6,7,8,9,10,11,12 } },
+ { 0x17, 30, kScript17 },
+ { 0x18, 30, kScript18 },
+ { 0x19, 30, kScript19 },
+ { 0x1a, 77, kScript1a },
+ { 0x1e, 76, kScript1e },
+ { 0x1f, 50, kScript1f },
+ { 0x20, 34, kScript20 },
+ { 0x22, 115, kScript22 },
+ { 0x23, 29, kScript23 },
+ { 0x24, 58, kScript24 },
+ { 0x28, 45, kScript28 },
+ { 0x29, 58, kScript29 },
+ { 0x2b, 48, kScript2b },
+ { 0x2c, 54, kScript2c },
+ { 0x2d, 96, kScript2d },
+ { 0x30, 86, kScript30 },
+ { 0x31, 57, kScript31 },
+ { 0x36, 60, kScript36 },
};
// `_PatientSequence` and `_ImpatientSequence` are standalone script
@@ -308,7 +542,7 @@ static const uint32 kImpatienceDelayMs = 60 * 1000;
// holds).
struct AnimScriptRef {
const uint8 *frames;
- uint8 len;
+ uint16 len;
};
static AnimScriptRef findAnimScript(uint16 seqnum) {
for (uint i = 0; i < ARRAYSIZE(kAnimScripts); i++) {
@@ -333,9 +567,17 @@ static AnimScriptRef findAnimScript(uint16 seqnum) {
return r;
}
+// Original frame period from `_InitFrameCounter @ 1a35:01ae`:
+// `LastFrame = (cs_within_hour) + 0xe`, with `cs_within_hour` =
+// `((ti_min * 60) + ti_sec) * 100 + ti_hund` (Borland C `struct time`
+// memory order is min, hour, hund, sec). The `+ 0xe` is 14
+// centiseconds â ~140 ms per frame, matching `_CheckFrameRate @
+// 1a35:0204`. Earlier 100 ms ran the partner / hotspot animations
+// roughly 1.4Ã faster than the original.
+static const uint kFramePeriodMs = 140;
+
static uint frameFromScriptAtTick(const uint8 *frames, uint len,
uint numFrames, uint32 tickMs) {
- const uint kFramePeriodMs = 100;
if (!frames || len == 0)
return numFrames > 0 ? (uint)((tickMs / kFramePeriodMs) % numFrames) : 0;
const uint scriptIdx = (uint)((tickMs / kFramePeriodMs) % len);
@@ -474,7 +716,6 @@ uint partnerFrameAtTick(uint16 seqnum, uint numFrames, uint32 tickMs) {
static uint oneShotThenLoopFrameAtTick(const uint8 *unfold, uint unfoldLen,
const uint8 *waitSeq, uint waitSeqLen,
uint numFrames, uint32 elapsedMs) {
- const uint kFramePeriodMs = 100;
const uint tick = elapsedMs / kFramePeriodMs;
const uint frame = (tick < unfoldLen)
? unfold[tick]
@@ -849,10 +1090,12 @@ void SiteScreen::run() {
// `_UpdateAnimations` at the top of `_DoSiteLoop`'s main loop).
// Restore the static BG snapshot, redraw animated NPCs +
// partner at the current frame, then re-render hotspots on
- // top. We tick at 100 ms (~10 FPS) which is in the same ball
- // park as the original.
+ // top. The original ticks at 14 cs (~140 ms, see
+ // `kFramePeriodMs` above) â we matched 100 ms before, which
+ // ran site animations ~1.4Ã too fast.
const uint32 now = g_system->getMillis();
- if (_snapshotSite == (int)cur && now - _lastTickMs >= 100) {
+ if (_snapshotSite == (int)cur &&
+ now - _lastTickMs >= kFramePeriodMs) {
if (checkImpatienceCounter()) {
_partnerWaitMood = kPartnerWaitImpatient;
debugC(1, kDebugSite, "Partner impatience: switched to impatient");
Commit: 28556038dfe1d4ec44622e0ffd332ad464b0ff5d
https://github.com/scummvm/scummvm/commit/28556038dfe1d4ec44622e0ffd332ad464b0ff5d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:08+02:00
Commit Message:
EEM: use simpleBlitFrom
Changed paths:
engines/eem/clues.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 27089a2696f..c167e3f12d3 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -744,10 +744,7 @@ void EEMEngine::displayClue(const byte *clueBlock) {
{
Graphics::Surface *screen = g_system->lockScreen();
if (screen) {
- for (int row = 0; row < 200; row++) {
- memcpy((byte *)bg.getBasePtr(0, row),
- (const byte *)screen->getBasePtr(0, row), 320);
- }
+ bg.simpleBlitFrom(*screen);
g_system->unlockScreen();
}
}
@@ -844,10 +841,7 @@ void EEMEngine::displayClue(const byte *clueBlock) {
break;
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
- for (int row = 0; row < 200; row++) {
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)screen->getBasePtr(0, row), 320);
- }
+ scratch.simpleBlitFrom(*screen);
g_system->unlockScreen();
int textX = bubX;
@@ -1035,10 +1029,7 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
{
Graphics::Surface *screen = g_system->lockScreen();
if (screen) {
- for (int row = 0; row < 200; row++) {
- memcpy((byte *)bg.getBasePtr(0, row),
- (const byte *)screen->getBasePtr(0, row), 320);
- }
+ bg.simpleBlitFrom(*screen);
g_system->unlockScreen();
}
}
@@ -1491,10 +1482,7 @@ bool EEMEngine::areYouSure() {
Graphics::ManagedSurface saved(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
if (screen) {
- for (int row = 0; row < 200; row++) {
- memcpy((byte *)saved.getBasePtr(0, row),
- (const byte *)screen->getBasePtr(0, row), 320);
- }
+ saved.simpleBlitFrom(*screen);
g_system->unlockScreen();
}
@@ -1538,9 +1526,7 @@ bool EEMEngine::areYouSure() {
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
- for (int row = 0; row < 200; row++)
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)saved.getBasePtr(0, row), 320);
+ scratch.simpleBlitFrom(saved);
scratch.fillRect(dlg, 0);
scratch.frameRect(dlg, 0xF);
_font.drawString(&scratch,
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index fc49dbbf559..004e38dda5e 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -1523,9 +1523,7 @@ void EEMEngine::doSetup() {
// transparent pixels show the setup BG underneath.
Graphics::Surface *cur = g_system->lockScreen();
if (cur) {
- for (int row = 0; row < 200; row++)
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)cur->getBasePtr(0, row), 320);
+ scratch.simpleBlitFrom(*cur);
g_system->unlockScreen();
}
const byte transp = (byte)(pic.flags >> 8);
@@ -2902,14 +2900,8 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
Graphics::ManagedSurface ms(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
ms.clear();
- if (haveBg) {
- const int bw = MIN<int>(galBg.surface.w, 320);
- const int bh = MIN<int>(galBg.surface.h, 200);
- for (int row = 0; row < bh; row++) {
- memcpy((byte *)ms.getBasePtr(0, row),
- (const byte *)galBg.surface.getBasePtr(0, row), bw);
- }
- }
+ if (haveBg)
+ ms.simpleBlitFrom(galBg.surface);
// Partner sprite at (5, 0x50). The original `MoreInfo @
// 158f:0419` calls `_RefreshGalleryBackground` (clears the
// portrait grid) but the partner anim slot registered by
@@ -3207,14 +3199,8 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
- if (haveBg) {
- const int bw = MIN<int>(galBg.surface.w, 320);
- const int bh = MIN<int>(galBg.surface.h, 200);
- for (int row = 0; row < bh; row++) {
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)galBg.surface.getBasePtr(0, row), bw);
- }
- }
+ if (haveBg)
+ scratch.simpleBlitFrom(galBg.surface);
// Partner sprite frame @ (5, 0x50). The original `_DoGallery @
// 158f:065b` registers `_NewAnimation(..., CONCAT22(2, ...), ...)`
@@ -4458,9 +4444,7 @@ void EEMEngine::doAccuse() {
ms.clear();
Graphics::Surface *cur = g_system->lockScreen();
if (cur) {
- for (int row = 0; row < 200; row++)
- memcpy((byte *)ms.getBasePtr(0, row),
- (const byte *)cur->getBasePtr(0, row), 320);
+ ms.simpleBlitFrom(*cur);
g_system->unlockScreen();
}
const byte firstChar =
@@ -4590,20 +4574,14 @@ void EEMEngine::doAccuse() {
{
Graphics::Surface *cur = g_system->lockScreen();
if (cur) {
- for (int row = 0; row < 200; row++)
- memcpy((byte *)ms.getBasePtr(0, row),
- (const byte *)cur->getBasePtr(0, row), 320);
+ ms.simpleBlitFrom(*cur);
g_system->unlockScreen();
} else if (haveAccuseBg) {
// Fallback: lockScreen failed somehow; at least
// fill from PIC 0x3f so we don't render against
- // stale memory.
- const int bw = MIN<int>(accuseBg.surface.w, 320);
- const int bh = MIN<int>(accuseBg.surface.h, 200);
- for (int row = 0; row < bh; row++) {
- memcpy((byte *)ms.getBasePtr(0, row),
- (const byte *)accuseBg.surface.getBasePtr(0, row), bw);
- }
+ // stale memory. simpleBlitFrom auto-clips, so
+ // no MIN(w, 320)/MIN(h, 200) needed.
+ ms.simpleBlitFrom(accuseBg.surface);
}
}
// Masked balloon blit â `_Rect_Move_Mask` (1000:03fc)
@@ -5892,14 +5870,8 @@ void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
- if (haveAccuseBg) {
- const int bw = MIN<int>(accuseBg.surface.w, 320);
- const int bh = MIN<int>(accuseBg.surface.h, 200);
- for (int row = 0; row < bh; row++) {
- memcpy((byte *)scratch.getBasePtr(0, row),
- (const byte *)accuseBg.surface.getBasePtr(0, row), bw);
- }
- }
+ if (haveAccuseBg)
+ scratch.simpleBlitFrom(accuseBg.surface);
// Partner sprite, drawn BEFORE portraits so the suspect grid
// covers it where they overlap (the gallery slots start at
Commit: b47044e739601668ae672ce3192961e01af7d5d2
https://github.com/scummvm/scummvm/commit/b47044e739601668ae672ce3192961e01af7d5d2
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:08+02:00
Commit Message:
EEM: more simpleBlitFrom
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 004e38dda5e..f427f7b57ac 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -2925,18 +2925,8 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
Picture detail;
if (_picsArchive.getPicture(detailPic, detail)) {
const byte transp = (byte)(detail.flags >> 8);
- const int dx = 0x94, dy = 0x0f;
- const int dw = MIN<int>(detail.surface.w, 320 - dx);
- const int dh = MIN<int>(detail.surface.h, 200 - dy);
- for (int row = 0; row < dh; row++) {
- const byte *src = (const byte *)
- detail.surface.getBasePtr(0, row);
- byte *dst = (byte *)ms.getBasePtr(0, dy + row);
- for (int col = 0; col < dw; col++) {
- if (src[col] != transp)
- dst[dx + col] = src[col];
- }
- }
+ ms.transBlitFrom(detail.surface,
+ Common::Point(0x94, 0x0f), transp);
}
// Walk the clue list from `pageStart`. Skip clues that aren't
@@ -4588,16 +4578,9 @@ void EEMEngine::doAccuse() {
// skips pixels equal to `pic[0] >> 8`.
if (haveBalloon) {
const byte transp = (byte)(balloon.flags >> 8);
- const int bw = MIN<int>(balloon.surface.w, 320 - balloonX);
- const int bh = MIN<int>(balloon.surface.h, 200 - balloonY);
- for (int row = 0; row < bh; row++) {
- const byte *src = (const byte *)balloon.surface.getBasePtr(0, row);
- byte *dst = (byte *)ms.getBasePtr(balloonX, balloonY + row);
- for (int col = 0; col < bw; col++) {
- if (src[col] != transp)
- dst[col] = src[col];
- }
- }
+ ms.transBlitFrom(balloon.surface,
+ Common::Point(balloonX, balloonY),
+ transp);
}
// Inset table @ 29be:0875 â 1df2:0acb pushes color=0.
uint16 tx = 5;
Commit: e40f51e2dc7190b652aabb5a68eb83aee2774ead
https://github.com/scummvm/scummvm/commit/e40f51e2dc7190b652aabb5a68eb83aee2774ead
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:08+02:00
Commit Message:
EEM: use transBlitFrom
Changed paths:
engines/eem/clues.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index c167e3f12d3..4de2cddd03e 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -857,24 +857,16 @@ void EEMEngine::displayClue(const byte *clueBlock) {
// the transparent colour to `_Rect_Move_Mask`. The
// on-disk u16 at file offset 0 maps to `Picture::flags`.
const byte transp = (byte)(balloon.flags >> 8);
- // `_GetBalloon @ 172b:1d7d` mirrors the picture horizontally
- // when `(bubNum & 0x80)` is set â used for right-side
- // speakers so the tail points the other way.
+ // `_GetBalloon @ 172b:1d7d` mirrors the picture
+ // horizontally when `(bubNum & 0x80)` is set â used
+ // for right-side speakers so the tail points the
+ // other way. ScummVM's `transBlitFrom` exposes the
+ // same via its `flipped` argument.
const bool flipBalloon = (fittedBubNum & 0x80) != 0;
if (bw > 0 && bh > 0) {
- for (int row = 0; row < bh; row++) {
- const byte *src =
- (const byte *)balloon.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(bubX, bubY + row);
- for (int col = 0; col < bw; col++) {
- const int srcCol = flipBalloon
- ? (balloon.surface.w - 1 - col)
- : col;
- const byte px = src[srcCol];
- if (px != transp)
- dst[col] = px;
- }
- }
+ scratch.transBlitFrom(balloon.surface,
+ Common::Point(bubX, bubY),
+ transp, flipBalloon);
}
// Per-balloon metadata from `29be:0875` (52 Ã 10 bytes,
// indexed by `bubNum & 0x7F`). The original `_DisplayClue`
@@ -1188,7 +1180,6 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
if (haveBalloon)
getBalloonInsets(balloonId, textXIns, textYIns, textWidth);
const int textX = ballX + textXIns;
- const int balloonH = haveBalloon ? balloon.surface.h : 200;
const int lineH = _font.getFontHeight();
// Pagination state â `FUN_22dc_05c8`'s text-idx loop uses
@@ -1252,23 +1243,10 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
}
}
if (haveBalloon) {
- const int bw = MIN<int>(balloon.surface.w, 320 - ballX);
- const int bh = MIN<int>(balloonH, 200 - ballY);
const byte transp = (byte)(balloon.flags >> 8);
- for (int row = 0; row < bh; row++) {
- const byte *src = (const byte *)
- balloon.surface.getBasePtr(0, row);
- byte *dst = (byte *)
- scratch.getBasePtr(ballX, ballY + row);
- for (int col = 0; col < bw; col++) {
- const int srcCol = flipBall
- ? (balloon.surface.w - 1 - col)
- : col;
- const byte px = src[srcCol];
- if (px != transp)
- dst[col] = px;
- }
- }
+ scratch.transBlitFrom(balloon.surface,
+ Common::Point(ballX, ballY),
+ transp, flipBall);
}
cursorY = ballY + textYIns;
}
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index f427f7b57ac..6e9fd72df76 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -3773,11 +3773,8 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
const int copyW = MIN<int>(mapW - scrollX, kMapWinW);
const int copyH = MIN<int>(mapH - scrollY, kMapWinH);
- for (int row = 0; row < copyH; row++) {
- memcpy((byte *)scratch.getBasePtr(kMapWinX, kMapWinY + row),
- mapPixels.data() + (scrollY + row) * mapW + scrollX,
- copyW);
- }
+ scratch.copyRectToSurface(mapPixels.data() + scrollY * mapW + scrollX,
+ mapW, kMapWinX, kMapWinY, copyW, copyH);
// Stamped site buttons. `_StampButtons @ 20fe:0d2f` (CD):
// button = _GetButton(MapData[+0])
@@ -5205,17 +5202,8 @@ void EEMEngine::doAccuseFloppy() {
if (h < 0x4e)
balloonY = (uint16)((0x50 - h) >> 1);
const byte transp = (byte)(balloon.flags >> 8);
- for (int row = 0; row < balloon.surface.h && balloonY + row < 200;
- row++) {
- const byte *src =
- (const byte *)balloon.surface.getBasePtr(0, row);
- byte *dst = (byte *)ms.getBasePtr(0x21, balloonY + row);
- for (int col = 0; col < balloon.surface.w && 0x21 + col < 320;
- col++) {
- if (src[col] != transp)
- dst[col] = src[col];
- }
- }
+ ms.transBlitFrom(balloon.surface,
+ Common::Point(0x21, balloonY), transp);
}
uint16 bx = 5;
uint16 by = 4;
@@ -5759,22 +5747,11 @@ void EEMEngine::doAccuseFloppy() {
const byte transp = (byte)(balloon.flags >> 8);
// `_GetBalloon`'s mirror flag (high bit of the table value)
// flips the balloon horizontally â the original applies it
- // inside the blit primitive. We emulate by reading the source
- // row in reverse.
- for (int row = 0; row < balloon.surface.h && balloonY + row < 200;
- row++) {
- const byte *src =
- (const byte *)balloon.surface.getBasePtr(0, row);
- byte *dst = (byte *)scene.getBasePtr(balloonX, balloonY + row);
- for (int col = 0;
- col < balloon.surface.w && balloonX + col < 320; col++) {
- const int srcCol = flipBalloon
- ? (balloon.surface.w - 1 - col) : col;
- const byte px = src[srcCol];
- if (px != transp)
- dst[col] = px;
- }
- }
+ // inside the blit primitive; ScummVM's `transBlitFrom` exposes
+ // the same via its `flipped` argument.
+ scene.transBlitFrom(balloon.surface,
+ Common::Point(balloonX, balloonY),
+ transp, flipBalloon);
}
uint16 tx = 5, ty = 4, tw = 155;
getBalloonInsets(balloonIdx, tx, ty, tw);
Commit: a2554f53bd0499a73fc939be4c6edd77ebfd1876
https://github.com/scummvm/scummvm/commit/a2554f53bd0499a73fc939be4c6edd77ebfd1876
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:09+02:00
Commit Message:
EEM: stop conversations when you skip the last phrase
Changed paths:
engines/eem/clues.cpp
engines/eem/eem.cpp
engines/eem/eem.h
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 4de2cddd03e..05fd6b64a5d 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -413,7 +413,7 @@ void EEMEngine::doInitClues() {
while (g_system->getEventManager()->pollEvent(ev)) {
if (ev.type == Common::EVENT_KEYDOWN &&
ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
- interruptAudio();
+ interruptAudio(/*stopMusicToo=*/false);
skip = true;
break;
}
@@ -540,7 +540,7 @@ void EEMEngine::doInitClues() {
while (g_system->getEventManager()->pollEvent(ev)) {
if (ev.type == Common::EVENT_KEYDOWN &&
ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
- interruptAudio();
+ interruptAudio(/*stopMusicToo=*/false);
skip = true;
break;
}
@@ -953,8 +953,9 @@ void EEMEngine::displayClue(const byte *clueBlock) {
// playing past the dialog dismissal and
// bleeds into the next screen (e.g. the
// case-briefing voice still talking on the
- // MAP after ESC).
- interruptAudio();
+ // MAP after ESC). Site / briefing MIDI
+ // stays â only voice + spool halt here.
+ interruptAudio(/*stopMusicToo=*/false);
break;
}
if (ev.type == Common::EVENT_LBUTTONDOWN) {
@@ -987,6 +988,15 @@ void EEMEngine::displayClue(const byte *clueBlock) {
applyClueSideEffects(c);
}
+
+ // `_DisplayClue @ 2404:05e6` lets the per-clue voice bleed past
+ // the last entry's dismissal â `_Wait()` returns on ANY input
+ // without touching the digital playback. We deliberately diverge
+ // (mirroring `_StopTheVoice @ 1ff1:0283`'s effect, voice only,
+ // MIDI untouched) so the briefing voice line doesn't keep talking
+ // over the next screen when the player clicks through the final
+ // entry.
+ interruptAudio(/*stopMusicToo=*/false);
}
void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
@@ -1069,7 +1079,7 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
continue;
if (ev.type == Common::EVENT_KEYDOWN &&
ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
- interruptAudio();
+ interruptAudio(/*stopMusicToo=*/false);
return true; // skip
}
if (ev.type == Common::EVENT_LBUTTONDOWN ||
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 85a533fee88..5f527ba1faf 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -718,19 +718,19 @@ bool EEMEngine::setAnmPalette(const Common::Path &anmPath) {
return true;
}
-void EEMEngine::interruptAudio() {
+void EEMEngine::interruptAudio(bool stopMusicToo) {
// Mirrors `_CleanMysterySounds @ 202f:05a5` + `_StopMIDI @
// 20a2:0512` â the original calls both whenever the player aborts
// the opening-anim chain or dismisses the title (`_DoOpeningAnims
// @ 2520:082a` writes `_LoopMIDI = 0; _StopMIDI();` after the
- // title-input loop). We expose the same combined stop on every
- // ESC handler so currently-playing music + voice + spool actually
- // halt instead of bleeding through into the next screen.
+ // title-input loop). Conversation / clue-dialog skip paths pass
+ // `stopMusicToo = false` so the site / briefing MIDI keeps going
+ // across an ESC â only the per-line voice + spool need to stop.
if (_audio) {
_audio->stopVoice();
_audio->stopSpool();
}
- if (_music)
+ if (stopMusicToo && _music)
_music->stop();
}
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 8578d1a2d6c..c942013d1cc 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -414,7 +414,12 @@ private:
/// and once before TITLE.ANM). Called from every ESC handler in
/// the intro / title chain so the theme music + voice spool
/// don't bleed past the abort.
- void interruptAudio();
+ /// Stop currently-playing voice / spooled SFX. Pass `stopMusic =
+ /// true` (the default â matches `_CleanMysterySounds + _StopMIDI`)
+ /// to also halt the MIDI track; conversation / dialog skip paths
+ /// pass `false` so the site / briefing music keeps going across an
+ /// ESC.
+ void interruptAudio(bool stopMusicToo = true);
// Screen handlers â port targets in screens/ later.
void showEAKidsLogo();
Commit: 9ca6f602c444b6ff8ea1aabca2ffa971bc17aa8f
https://github.com/scummvm/scummvm/commit/9ca6f602c444b6ff8ea1aabca2ffa971bc17aa8f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:09+02:00
Commit Message:
EEM: fixed highlighting bug when the cursor was moved during conversations
Changed paths:
engines/eem/clues.cpp
engines/eem/eem.cpp
engines/eem/site.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 05fd6b64a5d..63b384498ca 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -934,6 +934,14 @@ void EEMEngine::displayClue(const byte *clueBlock) {
// are ignored so accidental keystrokes don't blow past dialog
// the player hasn't finished reading.
if (hasText || (charPicId != 0 && charPicId != 0xFFFF)) {
+ // Click during a clue dialog only dismisses â there are no
+ // hover-interactive areas. Drop the highlighted cursor
+ // state the site loop may have left set so the player
+ // doesn't see a "clickable" cursor stuck on top of the
+ // balloon. The post-dialog `MOUSEMOVE` in the site loop
+ // will re-evaluate against hotspots and re-enable as
+ // needed.
+ setInteractiveMouseCursor(false);
bool advance = false;
bool skipAll = false;
while (!advance && !shouldQuit()) {
@@ -944,6 +952,13 @@ void EEMEngine::displayClue(const byte *clueBlock) {
advance = true;
break;
}
+ if (ev.type == Common::EVENT_MOUSEMOVE) {
+ // Keep the cursor non-interactive across moves
+ // inside the dialog (defensive â in case some
+ // other code path tries to set it).
+ setInteractiveMouseCursor(false);
+ continue;
+ }
if (ev.type == Common::EVENT_KEYDOWN &&
ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
advance = true;
@@ -1067,6 +1082,11 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
// doesn't auto-advance the new page.
Common::Event drain;
while (g_system->getEventManager()->pollEvent(drain)) {}
+ // Click during the floppy dialog only dismisses â no
+ // hover-interactive areas. Clear any stuck highlight from the
+ // site loop so the cursor stays a normal pointer over the
+ // balloon.
+ setInteractiveMouseCursor(false);
const uint32 minVisibleMs = 250;
const uint32 startedAt = g_system->getMillis();
while (!shouldQuit()) {
@@ -1075,6 +1095,10 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
if (ev.type == Common::EVENT_QUIT ||
ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
return true; // skip
+ if (ev.type == Common::EVENT_MOUSEMOVE) {
+ setInteractiveMouseCursor(false);
+ continue;
+ }
if (g_system->getMillis() - startedAt < minVisibleMs)
continue;
if (ev.type == Common::EVENT_KEYDOWN &&
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 5f527ba1faf..de24dbedb90 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -868,6 +868,14 @@ void EEMEngine::waitForInput(uint32 maxMs) {
// Only Return / KP-Enter / Space / Escape advance â letting any key
// dismiss balloons makes typing-while-reading (or a stuck modifier)
// blow past dialog the player hasn't finished reading.
+ //
+ // Drop the highlighted-cursor state any caller (site loop, gallery,
+ // notebook hover-handler) may have left on. While a balloon /
+ // intro is showing, click anywhere just dismisses â no
+ // hover-interactive areas â so a "clickable" cursor over the
+ // dialog is misleading. The caller's MOUSEMOVE handler will
+ // re-enable the highlight after we return if appropriate.
+ setInteractiveMouseCursor(false);
const uint32 startMs = g_system->getMillis();
while (!shouldQuit() && (g_system->getMillis() - startMs < maxMs)) {
Common::Event event;
@@ -877,6 +885,12 @@ void EEMEngine::waitForInput(uint32 maxMs) {
event.type == Common::EVENT_LBUTTONDOWN) {
return;
}
+ if (event.type == Common::EVENT_MOUSEMOVE) {
+ // Defensive: keep the cursor non-interactive across
+ // moves while the dialog is up.
+ setInteractiveMouseCursor(false);
+ continue;
+ }
if (event.type == Common::EVENT_KEYDOWN) {
if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
_skipIntro = true;
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index a6a7647c956..b141704a1fe 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -1039,7 +1039,12 @@ void SiteScreen::run() {
_vm->doHelp();
notePartnerActivity();
enter(cur, false);
- updateHotspotCursor(cur, event.mouse.x, event.mouse.y);
+ // Re-evaluate cursor against the CURRENT pointer
+ // position, not the click that opened the help.
+ // The player may have moved off the partner-head
+ // area during the dialog.
+ mouse = g_system->getEventManager()->getMousePos();
+ updateHotspotCursor(cur, mouse.x, mouse.y);
break;
}
const int idx = hotspotAtPoint(cur, event.mouse.x, event.mouse.y);
@@ -1049,7 +1054,12 @@ void SiteScreen::run() {
// Restore the site BG after the clue overlay.
notePartnerActivity();
enter(cur, false);
- updateHotspotCursor(cur, event.mouse.x, event.mouse.y);
+ // Use CURRENT pointer position â the click pos is
+ // still inside the hotspot rect, so reusing it
+ // would leave the "clickable" cursor stuck after
+ // the conversation even if the player moved off.
+ mouse = g_system->getEventManager()->getMousePos();
+ updateHotspotCursor(cur, mouse.x, mouse.y);
} else {
notePartnerActivity();
}
Commit: b39c08a950680c8e7a1e952e4af1756af28102c0
https://github.com/scummvm/scummvm/commit/b39c08a950680c8e7a1e952e4af1756af28102c0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:09+02:00
Commit Message:
EEM: added palette fadeout for the intro
Changed paths:
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/site.cpp
engines/eem/site.h
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index de24dbedb90..0af062785e1 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -686,17 +686,23 @@ bool EEMEngine::loadSitePalettes() {
return true;
}
-void EEMEngine::setSitePalette(uint num) {
- if (num >= kNumSitePals || _sitePals.size() < (num + 1) * kPalSize) {
- warning("setSitePalette: index %u out of range", num);
- return;
- }
+bool EEMEngine::getSitePalette(uint num, byte *out) const {
+ if (num >= kNumSitePals || _sitePals.size() < (num + 1) * kPalSize)
+ return false;
// SITEPALS stores 6-bit VGA-DAC values (0..63); ScummVM expects 8-bit
// (0..255), so left-shift by 2 like the original VGA hardware did.
const byte *src = _sitePals.data() + num * kPalSize;
- byte expanded[kPalSize];
for (uint i = 0; i < kPalSize; i++)
- expanded[i] = (byte)(src[i] << 2);
+ out[i] = (byte)(src[i] << 2);
+ return true;
+}
+
+void EEMEngine::setSitePalette(uint num) {
+ byte expanded[kPalSize];
+ if (!getSitePalette(num, expanded)) {
+ warning("setSitePalette: index %u out of range", num);
+ return;
+ }
g_system->getPaletteManager()->setPalette(expanded, 0, 256);
}
@@ -910,8 +916,18 @@ void EEMEngine::waitForInput(uint32 maxMs) {
}
void EEMEngine::showEAKidsLogo() {
- // Mirrors _ShowEAKids @ 2520:05f0 (without the color-cycle loop):
- // GetPicture(0x54), MemoryCopy to VGA, GetPalette(0x25), setmany.
+ // Mirrors `_ShowEAKids @ 2520:05f0`. The original:
+ // 1. GetPicture(0x54) + MemoryCopy to VGA + GetPalette(0x25).
+ // 2. FRAME_RATE = 25; for j in 0..1, for u in 0..0x36 (= 55):
+ // OpenColorCycle(0x01, 0x6e) // bg / outer ring shimmer
+ // OpenColorCycle(0x81, 0xee) // inner gradient shimmer
+ // every 8 ticks: OpenColorCycle(0x70, 0x80) // mid band
+ // 3. After the 110-tick loop: 5 more cycles of 0x70..0x80.
+ // 4. Wait 0x23 (= 35) more frames.
+ // 5. _OpenFadeOut.
+ // The cycling is what gives the EA Kids logo its characteristic
+ // shifting glow â it's NOT a static logo. ESC / click skips the
+ // remaining cycle.
Picture pic;
if (!_picsArchive.getPicture(kPicEAKidsLogo, pic)) {
warning("EA Kids logo (%u) load failed", kPicEAKidsLogo);
@@ -920,21 +936,95 @@ void EEMEngine::showEAKidsLogo() {
blitAt(pic, 0, 0);
setSitePalette(kPalEAKids);
g_system->updateScreen();
- waitForInput(2500);
+
+ // 25 fps â 40 ms / tick. Two outer iterations à 55 ticks each = 110
+ // ticks of palette rotation. The first inner-loop iteration of each
+ // outer pass rotates the in-memory palette once *before* applying
+ // to VGA (original gates the wait + setmany on `show != 0`); we
+ // just call the setter every tick â the visible cycle starts
+ // immediately, which is the same end result.
+ const uint kFrameMs = 40;
+ int delayCount = 8;
+ bool aborted = false;
+ for (uint outer = 0; outer < 2 && !aborted && !shouldQuit(); outer++) {
+ for (uint i = 0; i < 0x37 && !aborted && !shouldQuit(); i++) {
+ cyclePaletteRangeReverse(0x01, 0x6e);
+ cyclePaletteRangeReverse(0x81, 0xee);
+ delayCount--;
+ if (delayCount == 0) {
+ delayCount = 8;
+ cyclePaletteRangeReverse(0x70, 0x80);
+ }
+ g_system->updateScreen();
+
+ // Tick wait + skip detection.
+ const uint32 frameEnd = g_system->getMillis() + kFrameMs;
+ while (g_system->getMillis() < frameEnd && !aborted) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ _skipIntro = true;
+ return;
+ }
+ if (ev.type == Common::EVENT_KEYDOWN ||
+ ev.type == Common::EVENT_LBUTTONDOWN) {
+ if (ev.type == Common::EVENT_KEYDOWN &&
+ ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ _skipIntro = true;
+ }
+ aborted = true;
+ break;
+ }
+ }
+ g_system->delayMillis(5);
+ }
+ }
+ }
+
+ if (aborted)
+ return;
+
+ // Tail: 5 more rotations of the mid band, then 35 idle frames.
+ for (uint i = 0; i < 5 && !shouldQuit(); i++)
+ cyclePaletteRangeReverse(0x70, 0x80);
+ g_system->updateScreen();
+ waitForInput(0x23 * kFrameMs);
+
+ // `_OpenFadeOut @ 2520:0093` â 16 linear steps from current palette
+ // to black. Without this, the EA Kids logo cuts hard to the next
+ // screen.
+ fadeCurrentPaletteToBlack();
}
void EEMEngine::showHighScoreLogo() {
- // Mirrors _ShowHScoreLogo @ 2520:0799 (without the wait-loop):
- // GetPicture(0x20c), MemoryCopy to VGA, GetPalette(0x27), FadeIn.
+ // Mirrors `_ShowHScoreLogo @ 2520:0799`:
+ // GetPicture(0x20c) + MemoryCopy to VGA + GetPalette(0x27) +
+ // _OpenFadeIn + 50-tick wait at 25 fps + _OpenFadeOut.
Picture pic;
if (!_picsArchive.getPicture(kPicHighScoreLogo, pic)) {
warning("HighScore logo (%u) load failed", kPicHighScoreLogo);
return;
}
blitAt(pic, 0, 0);
- setSitePalette(kPalHighScore);
+
+ // Load target palette into a buffer, force a black palette, then
+ // fade in â without the explicit black step we'd flash the full
+ // logo briefly between blit and fade.
+ byte target[kPalSize];
+ if (!getSitePalette(kPalHighScore, target)) {
+ warning("HighScore palette (%u) load failed", kPalHighScore);
+ return;
+ }
+ byte black[kPalSize] = {};
+ g_system->getPaletteManager()->setPalette(black, 0, 256);
g_system->updateScreen();
- waitForInput(2500);
+ fadePaletteFromBlack(target);
+
+ // 50 ticks at 25 fps = ~2 s.
+ waitForInput(2000);
+
+ fadeCurrentPaletteToBlack();
}
void EEMEngine::showFloppyStormLogo() {
@@ -951,11 +1041,23 @@ void EEMEngine::showFloppyStormLogo() {
return;
}
blitAt(pic, 0, 0);
- setSitePalette(kPalStormLogo);
+
+ byte target[kPalSize];
+ if (!getSitePalette(kPalStormLogo, target)) {
+ warning("Storm palette (%u) load failed", kPalStormLogo);
+ return;
+ }
+ byte black[kPalSize] = {};
+ g_system->getPaletteManager()->setPalette(black, 0, 256);
g_system->updateScreen();
+
if (_audio)
_audio->playVoc(Common::Path("THUNDER.VOC"));
- waitForInput(2500);
+
+ fadePaletteFromBlack(target);
+ waitForInput(2000);
+ fadeCurrentPaletteToBlack();
+
if (_audio)
_audio->stopVoice();
}
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index c942013d1cc..26cb2c8f00d 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -368,6 +368,13 @@ private:
*/
void setSitePalette(uint num);
+ /// Fill @p out (256 Ã 3 bytes) with the SITEPALS palette at index
+ /// @p num, expanded from VGA's 6-bit DAC range to 8-bit. Returns
+ /// false if the index is out of range. Used when callers need the
+ /// palette for fade-in (set black first, then fade) without
+ /// flashing the target on screen.
+ bool getSitePalette(uint num, byte *out) const;
+
/**
* Upload a 6-bit VGA palette read from the head of an .ANM file (the
* first 0x300 bytes per Load_Sequence @ 2503:0006). Used until the
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index b141704a1fe..f309a2d76bc 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -137,6 +137,32 @@ void cyclePaletteRange(uint8 start, uint8 end) {
g_system->getPaletteManager()->setPalette(buf, start, count);
}
+void cyclePaletteRangeReverse(uint8 start, uint8 end) {
+ // Mirrors `_OpenColorCycle @ 2520:04f7`: save the END color, shift
+ // every entry up by one (END-1 â END, ...), wrap saved END to
+ // START. Visually colors march from start toward end â opposite
+ // direction from `cyclePaletteRange`. Used by the EA Kids /
+ // HighScore logo cycles.
+ if (end <= start)
+ return;
+ const uint count = (uint)end - (uint)start + 1;
+ byte buf[256 * 3];
+ g_system->getPaletteManager()->grabPalette(buf, start, count);
+ const uint last = count - 1;
+ const byte savedR = buf[last * 3 + 0];
+ const byte savedG = buf[last * 3 + 1];
+ const byte savedB = buf[last * 3 + 2];
+ for (uint i = last; i > 0; i--) {
+ buf[i * 3 + 0] = buf[(i - 1) * 3 + 0];
+ buf[i * 3 + 1] = buf[(i - 1) * 3 + 1];
+ buf[i * 3 + 2] = buf[(i - 1) * 3 + 2];
+ }
+ buf[0] = savedR;
+ buf[1] = savedG;
+ buf[2] = savedB;
+ g_system->getPaletteManager()->setPalette(buf, start, count);
+}
+
// Per-speaker partner-position table verified against `_WaitAnims @
// 29be:021c`. 12 bytes per entry, indexed by `siteData[+8]`. Layout:
// +0..1 anim Jake, +2..3 anim Jenny,
diff --git a/engines/eem/site.h b/engines/eem/site.h
index 110c560df05..606edf14ffe 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -81,6 +81,12 @@ void blitAnimFrameAnchored(Graphics::Surface *screen, const Picture &p,
/// BigMap marker shine.
void cyclePaletteRange(uint8 start, uint8 end);
+/// Rotate one VGA palette range by one slot in the OPPOSITE direction.
+/// Mirrors `_OpenColorCycle @ 2520:04f7` (CD) / `_ReverseColorCycle_Floppy`
+/// â used by the opening-anim logos (EA Kids, etc.) where the cycle
+/// shifts ENDâSTART rather than STARTâEND.
+void cyclePaletteRangeReverse(uint8 start, uint8 end);
+
/// One hotspot (search rectangle) within a site, 14 bytes on disk.
struct Hotspot {
int16 x1, y1, x2, y2; ///< rectangle in screen coordinates
Commit: 59304750cf0a6c8c92e2177078140834f58dcfb0
https://github.com/scummvm/scummvm/commit/59304750cf0a6c8c92e2177078140834f58dcfb0
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:10+02:00
Commit Message:
EEM: reviewed and improved comments across the engine
Changed paths:
engines/eem/animation.h
engines/eem/audio.cpp
engines/eem/audio.h
engines/eem/clues.cpp
engines/eem/detection.cpp
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/font.cpp
engines/eem/font.h
engines/eem/graphics.cpp
engines/eem/music.cpp
engines/eem/music.h
engines/eem/mystery.cpp
engines/eem/mystery.h
engines/eem/resource.cpp
engines/eem/resource.h
engines/eem/site.cpp
engines/eem/site.h
engines/eem/ui.cpp
diff --git a/engines/eem/animation.h b/engines/eem/animation.h
index 09b6e0406d1..fa5c7b8f4c2 100644
--- a/engines/eem/animation.h
+++ b/engines/eem/animation.h
@@ -30,20 +30,13 @@
namespace EEM {
/**
- * Decoder for the engine's full-screen difference animations.
- *
- * Used for `BOLT.ANM`, `TITLE.ANM`, `ANIM01.A` .. `ANIM20.A`. The format is
- * documented by Load_Sequence @ 2503:0006 / OpenDifferenceAnimation @ 2520:0337:
- *
- * - 0x300 bytes : 6-bit VGA palette
- * - u16 : frame count
- * - 12 bytes : header (height @ +2, width @ +4, rest unused)
- * - frames*u16 : packed length per frame
- * - per frame : `lengths[i]` bytes of RLE-packed delta data
- *
- * Each packed frame is unpacked by the custom `_ASM_Decompress` RLE
- * (1000:0953) into the persistent `_buffer`; skip opcodes preserve pixels
- * from the previous frame, which is how the difference encoding works.
+ * Decoder for BOLT.ANM, TITLE.ANM, ANIM01.A..ANIM20.A. Format from
+ * Load_Sequence @ 2503:0006 / OpenDifferenceAnimation @ 2520:0337:
+ * - 0x300 bytes: 6-bit VGA palette
+ * - u16: frame count
+ * - 12 bytes: header (height @ +2, width @ +4, rest unused)
+ * - frames*u16: packed length per frame
+ * - per frame: lengths[i] bytes of RLE delta data (asmDecompress).
*/
class ANMDecoder {
public:
@@ -89,11 +82,8 @@ private:
uint16 _nextFrameIdx = 0;
};
-/**
- * Decompress a single frame's RLE payload in place. Mirrors _ASM_Decompress
- * @ 1000:0953 byte-for-byte. @p dst already holds the previous frame; skip
- * opcodes leave those pixels untouched.
- */
+/// _ASM_Decompress @ 1000:0953. dst holds the previous frame; skip opcodes
+/// leave those pixels untouched (difference encoding).
void asmDecompress(const byte *src, uint srcSize, byte *dst, uint dstSize);
} // End of namespace EEM
diff --git a/engines/eem/audio.cpp b/engines/eem/audio.cpp
index 6ed89dcdf0c..3954fbb02f9 100644
--- a/engines/eem/audio.cpp
+++ b/engines/eem/audio.cpp
@@ -51,15 +51,8 @@ void AudioPlayer::stopAll() {
cleanMysterySounds();
}
-// VOC playback --------------------------------------------------------
-
void AudioPlayer::playVoc(const Common::Path &vocPath) {
- // Voice / digital audio is gated by the `DAT_2d5d_3f97` flag in
- // the original â verified at every callsite (`_DoChoosePartner @
- // 1a35:098c`, `_DisplayClue @ 2404:0845`, `_DoOpeningAnims @
- // 2520:08a8`, etc.). Setup-screen toggle and `_NewPlayer` fresh-
- // profile init both rewrite that flag. We pull it into the audio
- // player so callers don't have to duplicate the check.
+ // `_voiceEnabled` mirrors DAT_2d5d_3f97 (setup-screen voice toggle).
if (!_voiceEnabled) {
debugC(2, kDebugSound, "AudioPlayer: voice disabled, skipping %s",
vocPath.toString().c_str());
@@ -67,7 +60,6 @@ void AudioPlayer::playVoc(const Common::Path &vocPath) {
}
stopVoice();
- // Mirrors `_LoadSoundName`'s `_fopen` (1ff1:02ac).
Common::File *f = new Common::File();
if (!f->open(vocPath)) {
warning("AudioPlayer: %s missing", vocPath.toString().c_str());
@@ -83,9 +75,7 @@ void AudioPlayer::playVoc(const Common::Path &vocPath) {
return;
}
- // `_PlayVoice` (1ff1:023e) goes through `_AIL_play_VOC_file` on the
- // digital channel â we route through `kSpeechSoundType` so the
- // launcher's "Speech volume" slider applies.
+ // _PlayVoice @ 1ff1:023e â route through kSpeechSoundType for the launcher slider.
_mixer->playStream(Audio::Mixer::kSpeechSoundType, &_voiceHandle,
stream, -1, Audio::Mixer::kMaxChannelVolume,
0, DisposeAfterUse::YES);
@@ -98,9 +88,8 @@ bool AudioPlayer::isVoicePlaying() const {
}
void AudioPlayer::waitForVoiceDone(uint32 maxMs) {
- // Mirrors the wait loop at `_WaitForVoiceDone @ 1ff1:0221` â pumps
- // events (so animations + abort-on-click still work) while waiting
- // for the AIL voice channel to drain.
+ // _WaitForVoiceDone @ 1ff1:0221 â pumps events while the AIL voice channel
+ // drains, so animations + abort-on-click keep working during the wait.
const uint32 startMs = g_system->getMillis();
while (isVoicePlaying() && !_vm->shouldQuit() &&
g_system->getMillis() - startMs < maxMs) {
@@ -124,12 +113,9 @@ void AudioPlayer::stopVoice() {
_mixer->stopHandle(_voiceHandle);
}
-// Spool sound ---------------------------------------------------------
-
+// _ReadLong(&MysterySounds, size, sdx) @ 202f:0647.
+// 12-byte entries: (u32 offset, u32 compressed_size, u32 uncompressed_size).
bool AudioPlayer::readSdxIndex(const Common::Path &sdxPath) {
- // Mirrors `_ReadLong(&MysterySounds, size, sdx)` at 202f:0647 â
- // reads the entire .SDX into memory; each 12-byte entry is
- // (u32 offset, u32 compressed_size, u32 uncompressed_size).
Common::File f;
if (!f.open(sdxPath)) {
warning("AudioPlayer: %s missing", sdxPath.toString().c_str());
@@ -151,10 +137,8 @@ bool AudioPlayer::readSdxIndex(const Common::Path &sdxPath) {
return true;
}
+// _InitMysterySounds @ 202f:05cb. Strings "m%u.sdx" @ 29be:144f, "m%u.sdb" @ 29be:145b.
bool AudioPlayer::initMysterySounds(uint mysteryNum) {
- // Mirrors `_InitMysterySounds @ 202f:05cb` â calls
- // `_CleanMysterySounds` first, then sprintf-opens `m%u.sdx` (string
- // at 29be:144f) and `m%u.sdb` (29be:145b).
cleanMysterySounds();
const Common::String sdxName = Common::String::format("M%u.SDX", mysteryNum);
@@ -180,12 +164,11 @@ void AudioPlayer::cleanMysterySounds() {
_currentMystery = -1;
}
+// pcm must be allocated with malloc(): Audio::makeRawStream takes ownership
+// and frees it via free() on stream destruction (NOT delete/delete[]).
void AudioPlayer::playPcmBuffer(byte *pcm, uint32 size, uint sampleRate,
Audio::SoundHandle &handle,
Audio::Mixer::SoundType type) {
- // `Audio::makeRawStream` takes ownership and `free()`s the buffer
- // on stream destruction â so the caller must `malloc` (not `new`)
- // the PCM buffer.
Audio::SeekableAudioStream *stream =
Audio::makeRawStream(pcm, size, sampleRate, Audio::FLAG_UNSIGNED,
DisposeAfterUse::YES);
@@ -199,11 +182,10 @@ void AudioPlayer::playPcmBuffer(byte *pcm, uint32 size, uint sampleRate,
DisposeAfterUse::YES);
}
-// Floppy per-partner voice table â verified by reading the raw filename
-// pointers at `2608:0f0e` (Jake) and `2608:0f76` (Jenny) in EEM.EXE,
-// each 26 Ã FAR-ptr to a NUL-terminated `*.voc` filename. Indexed by
-// `_LoadSoundName_Floppy @ 1f4e:0305`. Slots match across partners:
-// 12 = PHONESL.VOC, 20 = partner intro, 25 = THUNDER.VOC.
+// Floppy per-partner voice tables, indexed by _LoadSoundName_Floppy @ 1f4e:0305.
+// Filename FAR-ptr arrays at 2608:0f0e (Jake) and 2608:0f76 (Jenny), each
+// 26 * FAR-ptr to a NUL-terminated `*.voc` name. Slots align across partners,
+// e.g. 12 = PHONESL.VOC, 20 = partner intro, 25 = THUNDER.VOC.
static const char *const kFloppyJakeVoiceTable[26] = {
"DING.VOC", "M-0083SL.VOC", "M-0085SL.VOC", "NEWSCAN.VOC",
"M-0089SL.VOC", "M-0091SL.VOC", "M-0092SL.VOC", "NEWSSHRT.VOC",
@@ -225,7 +207,7 @@ static const char *const kFloppyJennyVoiceTable[26] = {
void AudioPlayer::playFloppyVoiceSlot(uint slot, uint partner) {
if (slot >= 26)
- slot = 0; // mirrors `_LoadSoundName_Floppy`'s `if (0x19 < slot) slot = 0;`
+ slot = 0; // _LoadSoundName_Floppy: if (0x19 < slot) slot = 0;
const char *name = (partner == 0)
? kFloppyJakeVoiceTable[slot]
: kFloppyJennyVoiceTable[slot];
@@ -239,9 +221,7 @@ void AudioPlayer::spoolSound(uint num) {
return;
}
if (_currentMystery < 0) {
- // No SDB/SDX bundle is loaded â floppy install (no `M*.SDB`)
- // or pre-mystery state. Silently no-op; per-voice VOC playback
- // for floppy lives elsewhere (TODO).
+ // No SDB/SDX bundle loaded (floppy install or pre-mystery state).
debugC(2, kDebugSound,
"AudioPlayer: spoolSound(%u) skipped (no mystery sounds)", num);
return;
@@ -266,17 +246,14 @@ void AudioPlayer::spoolSound(uint num) {
return;
}
- // Mirrors the two `_fgetc(in)` reads at 202f:02da-e1: byte 0 =
- // Sound Blaster Time Constant, byte 1 = total AIL playback blocks.
- // Convert TC -> sample rate via the standard SB formula. The block
- // count is only used internally by the AIL DDS pipeline; ScummVM's
- // mixer doesn't need it.
+ // _UncompressedSound 2-byte header @ 202f:02da-e1:
+ // byte 0 = Sound Blaster Time Constant
+ // byte 1 = total AIL playback blocks (internal to AIL DDS; unused here)
const byte tc = sdb.readByte();
- (void)sdb.readByte(); // total blocks â unused outside AIL
+ (void)sdb.readByte(); // AIL block count, unused outside AIL
- // SB Time Constant: rate = 1000000 / (256 - tc). e.g. tc=0xD2 â
- // 22 kHz. Guard against the degenerate tc=0xFF (would divide by 1
- // â 1 MHz, well above what the mixer can resample sanely).
+ // SB Time Constant formula: rate = 1000000 / (256 - tc).
+ // e.g. tc=0xD2 -> 22 kHz. tc=0xFF would divide by 1 (1 MHz, nonsense); clamp.
const uint sampleRate = (tc < 0xFF)
? (uint)(1000000u / (256u - tc))
: 44100u;
@@ -285,11 +262,7 @@ void AudioPlayer::spoolSound(uint num) {
uint32 audioSize = 0;
if (entry.compressedSize == entry.uncompressedSize) {
- // `_UncompressedSound @ 202f:03e6` â already raw PCM. The
- // `_SpoolSound` equality check at 202f:06e6 is `comp == uncomp`;
- // in that case `len = uncompressed_size` bytes follow the
- // 2-byte header. Original reads in 16 KB chunks; we slurp the
- // lot at once.
+ // _UncompressedSound @ 202f:03e6 â raw PCM follows the 2-byte header.
audioSize = entry.uncompressedSize;
pcm = (byte *)malloc(audioSize);
if (!pcm) {
@@ -303,18 +276,15 @@ void AudioPlayer::spoolSound(uint num) {
return;
}
} else {
- // `_DeCompressSound @ 202f:02ad` â `EXPLODE @ 25c6:0d01`
- // (PKWARE DCL "Implode") with READDISKSOUND/WRITESOUND
- // callbacks. EXPLODE drives both ends via its OWN end-of-stream
- // marker (length token 519); the SDX `compressed_size` /
- // `uncompressed_size` are loose hints, NOT exact lengths â
- // 202f:0332 even computes `destSize - 2` and never reads it.
- // ScummVM's fixed-size `decompressDCL` overload errors when
- // the actual output exceeds our pre-allocated buffer (which
- // we saw on every M0 clue voice). Use the dynamic-sized
- // overload instead â it lets the DCL stream terminate at its
- // own marker. Source is bounded to the rest of the SDB so
- // the bit reader can't fall off the end.
+ // _DeCompressSound @ 202f:02ad -> EXPLODE @ 25c6:0d01 (PKWARE DCL Implode),
+ // driven by READDISKSOUND/WRITESOUND callbacks. EXPLODE terminates on its
+ // own length token 519 (end-of-stream marker), not on the SDX sizes:
+ // 202f:0332 even computes `destSize - 2` and never reads it. The SDX
+ // `compressed_size` / `uncompressed_size` fields are loose hints.
+ // ScummVM's fixed-size `decompressDCL` overload errors when actual output
+ // exceeds the pre-allocated buffer (observed on M0 clue voices), so use
+ // the dynamic-sized overload and let DCL terminate at its own marker.
+ // Source is bounded to the rest of the SDB so the bit reader stays in range.
const uint32 streamStart = (uint32)sdb.pos();
Common::SeekableSubReadStream sub(&sdb, streamStart,
(uint32)sdb.size(),
@@ -343,11 +313,7 @@ void AudioPlayer::spoolSound(uint num) {
num, tc, sampleRate, audioSize,
entry.compressedSize == entry.uncompressedSize ? "raw" : "DCL");
- // `_AIL_start_digital_playback` at 202f:040c â we route through
- // `kSFXSoundType` so the launcher's "SFX volume" slider applies
- // (this is the same slider the original would've targeted via
- // `_AIL_set_digital_master_volume`). Voice clips on the spool path
- // are gameplay SFX, not the speech-only VOC stream.
+ // _AIL_start_digital_playback @ 202f:040c. Spool clips are gameplay SFX.
playPcmBuffer(pcm, audioSize, sampleRate, _spoolHandle,
Audio::Mixer::kSFXSoundType);
}
@@ -380,17 +346,13 @@ void AudioPlayer::stopSpool() {
_mixer->stopHandle(_spoolHandle);
}
+// _SayKDDigital @ 2404:0fbc.
+// slot = kdspeak * 2 + (partner == Jake ? 1 : 0); sound = digital[slot+1] - 1
+// KDDigitalIndex = KDTextIndex + 0x12 (set by _ReadMystery @ 2404:0163-0167).
void AudioPlayer::sayKDDigital(const byte *kdTextIndex, uint kdspeak,
uint partner) {
if (!kdTextIndex || _currentMystery < 0)
return;
- // `_SayKDDigital @ 2404:0fbc`:
- // iVar1 = kdspeak * 2;
- // if (_Partner == 0) iVar1++; // Jake offset
- // sound = *(u16 *)(KDDigitalIndex + (iVar1 + 1) * 2) - 1;
- // _SpoolSound(sound);
- // KDDigitalIndex sits 18 bytes (`+ 0x12`) after KDTextIndex per
- // `_ReadMystery` 2404:0163-0167.
const byte *digital = kdTextIndex + 0x12;
const uint slot = (kdspeak * 2) + (partner == 0 ? 1u : 0u) + 1u;
const uint16 raw = READ_LE_UINT16(digital + slot * 2);
diff --git a/engines/eem/audio.h b/engines/eem/audio.h
index 12667f1179d..1d986ac47bb 100644
--- a/engines/eem/audio.h
+++ b/engines/eem/audio.h
@@ -35,119 +35,84 @@ namespace EEM {
class EEMEngine;
/**
- * Non-MIDI audio (digitised voice + sound effects). Mirrors the
- * original `SOUND.C` / `SPOOLSND.C` source files in `EEMCD.EXE` â
- * the AIL digital playback path used for both standalone .VOC files
- * and the per-mystery `M%d.SDB` spool stream.
+ * Non-MIDI audio (digitised voice + SFX). Mirrors SOUND.C / SPOOLSND.C.
*
- * Two pathways:
+ * - VOC files (THUNDER.VOC, PHONE.VOC, JEN.VOC/JAKE.VOC): _LoadSoundName +
+ * _PlayVoice @ 1ff1:0299 / 1ff1:023e via _AIL_play_VOC_file.
+ * - Mystery spool stream: _InitMysterySounds @ 202f:05cb opens M%d.SDX/.SDB.
+ * Each SDX entry is (offset, compSize, uncompSize). Equal sizes => raw PCM
+ * (_UncompressedSound @ 202f:03e6); otherwise PKWARE DCL Implode
+ * (_DeCompressSound @ 202f:02ad). Each blob starts with SB Time Constant +
+ * AIL block count, then 8-bit unsigned PCM.
*
- * 1. **VOC playback** â `_LoadSoundName @ 1ff1:0299` reads a Creative
- * Voice File into memory and `_PlayVoice @ 1ff1:023e` hands it to
- * `_AIL_play_VOC_file`. Used for THUNDER.VOC (Storm logo),
- * PHONE.VOC (briefing), JEN.VOC / JAKE.VOC (partner choose).
- *
- * 2. **Spool stream** â `_InitMysterySounds @ 202f:05cb` opens
- * `m%d.sdx` (29be:144f) and `m%d.sdb` (29be:145b) for the active
- * mystery. `_SpoolSound(num) @ 202f:068d` indexes into the SDX
- * table (12 bytes per entry: u32 file_offset, u32 compressed_size,
- * u32 uncompressed_size). If sizes match it streams via
- * `_UncompressedSound @ 202f:03e6`; otherwise it EXPLODE-decompresses
- * via `_DeCompressSound @ 202f:02ad`.
- *
- * Each entry's data starts with 2 metadata bytes â Sound Blaster
- * Time Constant (`rate = 1000000 / (256 - tc)`) and the AIL block
- * count â followed by the (optionally) PKWARE-DCL-compressed
- * 8-bit unsigned PCM stream. We use ScummVM's `Common::decompressDCL`
- * + `Audio::makeRawStream` to reach the same audio at the same
- * sample rate.
- *
- * Mystery 60 (`M60.SDB/SDX`) holds the 19 voiceovers played between
- * ANIM01..ANIM20 in `_DoOpeningAnims` (it loads `_InitMysterySounds(0x3c)`
- * before the loop and `_SpoolSound(uVar3 - 1)` between every clip).
- * Mysteries 0..55 hold each case's per-clue voice plus the partner's
- * digital lines (`_KDDigitalIndex` table within the .SD blob).
+ * M60.SDB/SDX holds the 19 voiceovers between ANIM01..ANIM20 in
+ * _DoOpeningAnims (loaded via _InitMysterySounds(0x3c)).
+ * M0..M55 hold per-mystery clue voice + partner KDDigital lines.
*/
class AudioPlayer {
public:
explicit AudioPlayer(EEMEngine *vm);
~AudioPlayer();
- /// Mirrors the gate in every original audio call site
- /// (`_DisplayClue` 2404:0845, `_DoChoosePartner` 1a35:098c,
- /// `_DoOpeningAnims` 2520:08a8, `_DisplayCorrect` 1df2:0780, ...)
- /// â every `_PlayVoice` / `_SpoolSound` is wrapped in
- /// `if ((DAT_2d5d_3f97 != 0) && (_VoiceAvailable != 0))`. The
- /// engine pulls `_voiceOn` (= `DAT_2d5d_3f97`) into here so we
- /// can early-return at the audio boundary instead of duplicating
- /// the gate at every call site.
+ /// Setup-screen voice toggle. Mirrors DAT_2d5d_3f97 â every original
+ /// `_PlayVoice` / `_SpoolSound` is wrapped in
+ /// `if ((DAT_2d5d_3f97 != 0) && (_VoiceAvailable != 0))`; we pull the
+ /// flag in here so callers don't duplicate the gate.
void setVoiceEnabled(bool enabled) { _voiceEnabled = enabled; }
bool voiceEnabled() const { return _voiceEnabled; }
- // VOC playback ----------------------------------------------------
-
- /// Mirrors `_LoadSoundName` + `_PlayVoice`. Loads the named .VOC
- /// from the game directory and hands it to the speech mixer
- /// channel. A new `playVoc` cancels any prior voice.
+ /// Loads the named .VOC from the game directory and hands it to the
+ /// speech mixer channel. Mirrors _LoadSoundName + _PlayVoice
+ /// @ 1ff1:0299 / 1ff1:023e. A new playVoc cancels any prior voice.
void playVoc(const Common::Path &vocPath);
- /// Mirrors `_VoicePlaying @ 1ff1:01f9`.
+ /// _VoicePlaying @ 1ff1:01f9.
bool isVoicePlaying() const;
- /// Mirrors `_WaitForVoiceDone @ 1ff1:0221`. Blocks (with frame /
- /// event pumping, like the rest of the engine's busy-loops) until
- /// the voice clip finishes. Returns early if the user clicks /
- /// presses a key â same abort behaviour the original
- /// `_AIL_stop_digital_playback` callback installed.
+ /// _WaitForVoiceDone @ 1ff1:0221. Blocks (with frame + event pumping)
+ /// until the voice clip finishes; returns early on click / keypress.
void waitForVoiceDone(uint32 maxMs = 60000);
- /// Mirrors `_StopTheVoice @ 1ff1:0283`.
+ /// _StopTheVoice @ 1ff1:0283.
void stopVoice();
- /// Floppy variant: play a VOC by 0..25 slot index in the per-partner
- /// voice table. Mirrors `_LoadSoundName_Floppy @ 1f4e:0305` which
- /// indexes the table at `2608:0f0e` (Jake) / `2608:0f76` (Jenny).
- /// `partner` is 0 for Jake, 1 for Jenny. Common slots: 12 =
- /// PHONESL.VOC, 20 = partner intro voice, 25 = THUNDER.VOC.
+ /// Play a floppy VOC by 0..25 slot index in the per-partner voice
+ /// table. Mirrors _LoadSoundName_Floppy @ 1f4e:0305 (tables at
+ /// 2608:0f0e Jake / 2608:0f76 Jenny). partner: 0=Jake, 1=Jenny.
+ /// Common slots: 12 = PHONESL.VOC, 20 = partner intro, 25 = THUNDER.VOC.
void playFloppyVoiceSlot(uint slot, uint partner);
- // Mystery sound spool ---------------------------------------------
-
- /// Mirrors `_InitMysterySounds @ 202f:05cb`. Loads `M%u.SDX` into
- /// memory and remembers the corresponding `M%u.SDB` path.
+ /// Loads M%u.SDX into memory and remembers the matching M%u.SDB path.
+ /// Mirrors _InitMysterySounds @ 202f:05cb.
bool initMysterySounds(uint mysteryNum);
- /// Mirrors `_CleanMysterySounds @ 202f:05a5`.
+ /// _CleanMysterySounds @ 202f:05a5.
void cleanMysterySounds();
- /// Mirrors `_SpoolSound @ 202f:068d`. Reads + decompresses entry
- /// `num` from the active SDB and queues it for SFX playback. The
- /// original blocks until playback finishes â for ScummVM we let
- /// the mixer run asynchronously and expose `waitForSpoolDone` so
- /// callers that need the original "block-then-continue" semantics
- /// can opt in.
+ /// Reads + decompresses SDX entry `num` from the active SDB and queues
+ /// it for SFX playback. Mirrors _SpoolSound @ 202f:068d. Original blocks
+ /// until playback finishes; we run async â use waitForSpoolDone to opt
+ /// in to the original block-then-continue semantics.
void spoolSound(uint num);
- /// Mirrors the abort-on-input wait loop inside `_UncompressedSound`
- /// / `_DeCompressSound`. Returns when the spool clip finishes or
- /// the user clicks / presses a key.
+ /// Wait loop inside _UncompressedSound / _DeCompressSound; aborts on
+ /// click / keypress (same abort behaviour as the original).
void waitForSpoolDone(uint32 maxMs = 60000);
- /// Mirrors the immediate `_AIL_stop_digital_playback` exit.
+ /// Immediate _AIL_stop_digital_playback exit.
void stopSpool();
bool isSpoolPlaying() const;
- /// Mirrors `_SayKDDigital(kdspeak) @ 2404:0fbc`. Each mystery
- /// embeds a `KDDigitalIndex` table immediately after the 18-byte
- /// `KDTextIndex` header (set up by `_ReadMystery @ 2404:008f`:
- /// `_KDDigitalIndex = _KDTextIndex + 0x12`). The table is two
- /// 1-based sound indices per `kdspeak` slot â Jen at +2, Jake at
- /// +4 from each entry's start (+1 word for the unused header
- /// slot). Pass the mystery's `kdTextIndex()` pointer.
+ /// Mirrors _SayKDDigital(kdspeak) @ 2404:0fbc. Each mystery embeds a
+ /// KDDigitalIndex table 18 bytes (+0x12) after its KDTextIndex header
+ /// (set up by _ReadMystery @ 2404:008f: `_KDDigitalIndex = _KDTextIndex
+ /// + 0x12`). Table is two 1-based sound indices per kdspeak slot â Jen
+ /// at +2, Jake at +4 from each entry start (+1 word skips an unused
+ /// header slot). Pass the mystery's kdTextIndex() pointer.
void sayKDDigital(const byte *kdTextIndex, uint kdspeak, uint partner);
- /// Mirrors `_QuitSounds @ 1ff1:03c5`.
+ /// _QuitSounds @ 1ff1:03c5.
void stopAll();
private:
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 63b384498ca..ebe7383137a 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -35,30 +35,25 @@
#include "eem/eem.h"
#include "eem/site.h"
-// EEM â clue / briefing pipeline (SCRIPT.C clue parts + KD.C briefing parts).
-// Everything that drives a `ClueBlock` through the displayClue / portrait /
-// balloon / notebook-side-effect flow plus the case-briefing intro that
-// preambles into the same flow.
+// Clue / briefing pipeline (SCRIPT.C + KD.C).
namespace EEM {
-// Picture / animation IDs verified against `_DoChoosePartner @ 1a35:0756`.
-// `const` at namespace scope already implies internal linkage in C++,
-// so no `static` needed.
-const uint kPicChooseBackground = 0x8c; ///< `_GetBackground(0x8c)`
-const uint kAniBoy = 8; ///< `_GetAnimation(8)` (Jake)
-const uint kAniGirl = 9; ///< `_GetAnimation(9)` (Jenny)
+// _DoChoosePartner @ 1a35:0756.
+const uint kPicChooseBackground = 0x8c; // _GetBackground(0x8c)
+const uint kAniBoy = 8; // Jake
+const uint kAniGirl = 9; // Jenny
-// `_DoHappiness @ 172b:27b5`: cursor X picks one of 4 rects; past
-// rect 3 is treated as level 4. Verbatim from `29be:030f`.
+// _DoHappiness @ 172b:27b5 â cursor X picks one of 4 rects @ 29be:030f.
+// Past rect 3 = level 4.
const Common::Rect kHappyZones[4] = {
- Common::Rect( 0, 0, 70, 200), // far left â girl very happy, boy neutral
+ Common::Rect( 0, 0, 70, 200), // far left â girl very happy, boy neutral
Common::Rect( 70, 0, 126, 200), // girl's column
Common::Rect(126, 0, 182, 200), // middle
Common::Rect(182, 0, 235, 200), // boy's column
};
-// On-screen positions verified from `_NewAnimation` calls @ 1a35:07b9 / 07d5.
+// _NewAnimation positions @ 1a35:07b9 / 07d5.
const int kBoyX = 0xe2; // 226
const int kBoyY = 0x62; // 98
const int kGirlX = 0x42; // 66
@@ -87,11 +82,9 @@ uint markClueBlockNotebookEntries(Mystery &mystery, const byte *clueBlock) {
return marked;
}
-// `_DoHappiness @ 172b:27b5`: each cursor zone swaps the partner's
-// sequence script to a more / less "happy" cycle. Boy seqs lifted
-// verbatim from `29be:0337` (5 Ã 0x14 bytes), girl seqs from
-// `29be:039b`. Both cycle through 9 frames (the boy/girl anim cells
-// contain 10 cells = pairs of "neutral, smile" at increasing intensity).
+// _DoHappiness @ 172b:27b5 â per-zone sequence scripts.
+// Boy seqs @ 29be:0337 (5 Ã 0x14 bytes), girl seqs @ 29be:039b. 9 frames each;
+// the anim cells contain 10 cells = pairs of (neutral, smile) at 5 intensities.
const uint8 kBoySeqs[5][9] = {
{ 0,0,0,0,0,0,0,1,0 }, // level 0
{ 2,2,2,2,2,2,2,3,2 }, // level 1
@@ -115,9 +108,10 @@ uint happinessLevel(int x) {
return 4; // past zone 3 â max level
}
-// Lock the framebuffer, masked-blit `p` at (x, y), unlock. The transparent
-// colour is the high byte of `p.flags`; `_AddPicBackground @ 172b:0ed4`
-// pushes `word ptr ES:[BX] >> 8` as `_Rect_Move_Mask`'s mask byte.
+// Masked blit: transparent colour = high byte of p.flags.
+// _AddPicBackground @ 172b:0ed4 pushes `word ptr ES:[BX] >> 8` as
+// _Rect_Move_Mask's mask byte (the on-disk u16 at file offset 0 maps to
+// Picture::flags).
void blitMaskedToScreen(const Picture &p, int x, int y) {
const byte transp = (byte)(p.flags >> 8);
Graphics::Surface *screen = g_system->lockScreen();
@@ -150,11 +144,10 @@ void blitRawToScreen(const Picture &p, int x, int y) {
x, y, w, h);
}
+// _DoChoosePartner @ 1a35:0756. The original places boy + girl animations
+// on a backdrop and polls four click rectangles (two per character); we
+// approximate with a single split at x=160 (left=Jenny, right=Jake).
void EEMEngine::doChoosePartner() {
- // Mirrors _DoChoosePartner @ 1a35:0756. The original places boy + girl
- // animations on a backdrop and polls four click rectangles (two per
- // character) for the player's choice. We approximate by splitting the
- // screen at x=160: left half = girl (Jenny), right half = boy (Jake).
Picture background;
if (!_picsArchive.getPicture(kPicChooseBackground, background)) {
warning("ChoosePartner background (%u) load failed", kPicChooseBackground);
@@ -174,19 +167,11 @@ void EEMEngine::doChoosePartner() {
setAnmPalette(Common::Path("TITLE.ANM"));
- // `_DoHappiness @ 172b:27b5`: the cursor's X column picks one of 4
- // rects (29be:030f, all full-height); past rect 3 â "level 4". The
- // per-zone sequence scripts (`kBoySeqs` / `kGirlSeqs`) live at file
- // scope above so the gestures match the original beat-for-beat.
-
- // `_DoChoosePartner` opens with `_SetMousePos(0xa0, 0x96)` so the
- // cursor lands centred between the two partners â start the
- // happiness level from that initial X.
+ // _DoChoosePartner opens with _SetMousePos(0xa0, 0x96).
int curMouseX = 0xa0;
uint level = happinessLevel(curMouseX);
- uint seqIdx = 0; // step within the 9-frame seq
+ uint seqIdx = 0;
- // Initial render â pose 0 of whichever zone the cursor opens in.
blitAt(background, 0, 0);
blitAt(girlAnim[kGirlSeqs[level][seqIdx % 9] % girlAnim.size()],
kGirlX, kGirlY);
@@ -201,11 +186,10 @@ void EEMEngine::doChoosePartner() {
uint32 lastTick = g_system->getMillis();
while (!shouldQuit()) {
- // Advance through the 9-frame seq at 100 ms â `_CheckFrameRate`
- // cadence. The seq is short and loops; matches the original
- // `_UpdateAnimations` which restarts at curIdx=0 on the 0x80
- // marker. Mirrors `_DoHappiness`'s rewriting of `curIdx = 0xFFFF`
- // when the cursor crosses zones (we restart `seqIdx` instead).
+ // 100ms tick: matches _CheckFrameRate's ~10 fps cadence. The seq
+ // is short and loops; _UpdateAnimations restarts at curIdx=0 on
+ // the 0x80 marker. _DoHappiness rewrites curIdx=0xFFFF on zone
+ // change (here we restart seqIdx).
if (g_system->getMillis() - lastTick > 100) {
lastTick = g_system->getMillis();
seqIdx = (seqIdx + 1) % 9;
@@ -265,16 +249,14 @@ void EEMEngine::doChoosePartner() {
g_system->delayMillis(20);
}
- // Mirrors the tail of `_DoChoosePartner @ 1a35:097f` â once the
- // player commits to a partner, load and play their intro VOC
- // (`jen.voc` for Jenny, `jake.voc` for Jake; strings at 29be:0af1 /
- // 29be:0af9) and block on `_WaitForVoiceDone`.
+ // Partner intro VOC (jake.voc / jen.voc @ 29be:0af9 / 29be:0af1),
+ // tail of _DoChoosePartner @ 1a35:097f.
if (_audio) {
if (isFloppy()) {
- // Floppy `_DoChoosePartner_Floppy @ 19bb:0a8e` calls
- // `_LoadSoundName_Floppy(0x14)` (= slot 20) which the
- // per-partner table at `2608:0f0e` / `2608:0f76` resolves
- // to `m-0113sl.voc` (Jake) or `f-0140sl.voc` (Jenny).
+ // Floppy _DoChoosePartner_Floppy @ 19bb:0a8e calls
+ // _LoadSoundName_Floppy(0x14) (slot 20); per-partner tables at
+ // 2608:0f0e / 2608:0f76 resolve to m-0113sl.voc (Jake) or
+ // f-0140sl.voc (Jenny).
_audio->playFloppyVoiceSlot(0x14, _partner);
} else {
_audio->playVoc(Common::Path(
@@ -284,24 +266,16 @@ void EEMEngine::doChoosePartner() {
}
}
+// _DoInitClues @ 1a35:0411. Sequence:
+// 1. BG 0x52, palette 0x22
+// 2. anims: game @ (0xcd, 0x6c), book @ (0, 99), nancy @ (0x68, 0x8b) on caseType=1
+// 3. cycle game anim once (click skips)
+// 4. _PlayInSequence per partner+caseType
+// 5. _DisplayClue(InitBlock + 2, 1) â briefing dialogue
+// 6. _OnSites[startSite] = 1
+// Anim IDs: gameAni = 0x17 (Jake) / 0x3b (Jenny);
+// bookAni = 0x18 / 0x3c; nancyAni = 0x19 (caseType 1 only).
void EEMEngine::doInitClues() {
- // Mirrors `_DoInitClues` @ 1a35:0411. The original does:
- // 1. _AllBlack(); _GetBackground(0x52); _GetPalette(0x22);
- // 2. _GetAnimation(gameAni); _NewAnimation(0xcd, 0x6c, ...)
- // _GetAnimation(bookAni); _NewAnimation(0, 99, ...)
- // (case type 1 also: _NewAnimation(0x68, 0x8b, nancyAni))
- // 3. _UpdateAnimations(); _FadeIn();
- // 4. while (frame != gameNum) { _CheckFrameRate(); _UpdateAnimations(); }
- // â cycles through the entire game animation once. Click skips.
- // 5. _PlayInSequence(seqId, ...) â plays a follow-up sequence based
- // on partner + case type.
- // 6. _DisplayClue(InitBlock + 2, 1) â the briefing dialogue.
- // 7. _OnSites[startSite] = 1.
- //
- // gameAni / bookAni / nancyAni values verified directly from Ghidra:
- // gameAni = 0x17 (Jake) / 0x3b (Jenny)
- // bookAni = 0x18 (Jake) / 0x3c (Jenny)
- // nancyAni = 0x19 (case type 1 only)
if (!_mystery.isLoaded())
return;
@@ -309,12 +283,9 @@ void EEMEngine::doInitClues() {
if (!ib)
return;
- // CD InitBlock starts with `u16 caseType; u16 startSite; <clue block>`.
- // Floppy InitBlock starts with `u8 caseType; u8 nSubjects; subjects[];
- // u8 nDialog; dialog_records[]` â no embedded `startSite`. Verified in
- // `FUN_19bb_042f` (floppy briefing) where `cVar1 = *(buffer +
- // initOffset)` reads caseType as a single byte and the dialog loop
- // uses `local_e = byte[initOffset + 1 + nSubjects + 1]`.
+ // CD InitBlock: u16 caseType; u16 startSite; <clue block>.
+ // Floppy InitBlock (FUN_19bb_042f): u8 caseType; u8 nSubjects;
+ // subjects[]; u8 nDialog; dialog_records[]. No startSite.
const bool floppy = isFloppy();
const uint16 caseType = floppy ? (uint16)ib[0] : READ_LE_UINT16(ib);
@@ -325,12 +296,8 @@ void EEMEngine::doInitClues() {
_mystery._siteNumber = startSite;
_mystery._lastSite = startSite;
} else {
- // Floppy InitBlock has no startSite. The floppy BigMap iterator
- // `FUN_1fed_07ed` walks every site in the SITES section
- // unconditionally (no `_OnSites` gate) â verified at the floppy
- // `_DoMapScreen @ 1fed:1060` which stamps every site marker
- // before the interaction loop. Mark every loaded site as visible
- // so our `_onSites`-gated overview stamps the same set.
+ // Floppy _DoMapScreen @ 1fed:1060 (FUN_1fed_07ed) walks every
+ // site unconditionally â mirror that by marking all visible.
const uint sites = _mystery.numSites();
for (uint s = 0; s < sites && s < Mystery::kVisitedSiteCap; s++)
_mystery._onSites[s] = 1;
@@ -354,34 +321,17 @@ void EEMEngine::doInitClues() {
&& _aniArchive.loadAnimation(0x19, nancy)
&& !nancy.empty();
- // Step 4 â cycle through the game animation once before the briefing.
- // Mirrors the `while (uVar9 != gameNum)` loop. The original calls
- // `_UpdateAnimations` per `_CheckFrameRate` tick (~10 fps). We use
- // 100 ms ticks for the same cadence. Click / key skips.
+ // Cycle game animation once (10 fps = _CheckFrameRate cadence).
+ // _DoInitClues @ 1a35:0507/0541 hard-codes the SCRIPT index to Jake's
+ // IDs (0x17/0x18/0x19) regardless of partner, so look up scripts by
+ // those IDs unconditionally.
if (haveGame || haveBook || haveNancy) {
const uint frameCount = haveGame ? game.size() : 8;
bool skip = false;
for (uint frame = 0; frame < frameCount && !shouldQuit() && !skip; frame++) {
- // Restore BG + advance frame. The original always uses
- // Jake's anim IDs (0x17/0x18/0x19) as the SCRIPT keys
- // even when Jenny's CELL data is loaded â verified at
- // `_DoInitClues @ 1a35:0507`/`0541` where
- // `_NewAnimation(..., (PicData *)CONCAT22(0x17, ...), ...)`
- // hard-codes the script index to 0x17. So we look up
- // `partnerFrameAtTick(0x17, ...)` regardless of partner.
- // This gives us the correct cadence â book holds on its
- // "thinking" pose (cell 8) for 16 ticks instead of
- // flipbook-cycling, and nancy waits 18 ticks before her
- // late-arrival count-up.
if (haveBriefingBg)
blitAt(bg, 0, 0);
const uint32 t = frame * 100;
- // All three briefing anims (game/book/nancy) go through
- // the original `_NewAnimation` path so per-frame anchors
- // apply. Use `blitAnimFrameAnchored` against a locked
- // screen surface so the briefing partner / book / nancy
- // translate cleanly between cells instead of pinning at
- // the same top-left.
Graphics::Surface *scr = g_system->lockScreen();
if (!scr) {
skip = true;
@@ -402,11 +352,7 @@ void EEMEngine::doInitClues() {
g_system->unlockScreen();
g_system->updateScreen();
- // Wait 100 ms or until input. ESC also stops any pending
- // voice / spool so audio doesn't bleed past the briefing
- // when the player skips early â without this the per-clue
- // voice line that `displayClue` would spool keeps playing
- // after we've moved on to the MAP.
+ // ESC interrupts voice/spool so audio doesn't bleed into the MAP.
const uint32 wakeup = g_system->getMillis() + 100;
while (g_system->getMillis() < wakeup && !shouldQuit() && !skip) {
Common::Event ev;
@@ -428,14 +374,11 @@ void EEMEngine::doInitClues() {
}
}
- // Freeze only the same setup-animation band the original bakes into
- // its background buffers before clearing the registered animations:
- // `_VidramRectCopy(0, 0x5a, 0x28, 0x6d, 16000, 48000/32000)`.
- // Width is in mode-X columns, so 0x28 columns = 160 pixels. This
- // preserves the lower-left book/Nancy area but intentionally drops the
- // right-side game animation; `_PlayInSequence` redraws that character
- // over a clean background next. Preserving the full screen leaves the
- // old right-side Jake/Jenny frame underneath the sequence.
+ // _VidramRectCopy(0, 0x5a, 0x28, 0x6d, 16000, 48000/32000): freeze the
+ // lower-left book/Nancy band the original bakes into BG buffers before
+ // clearing registered animations. Width is in mode-X cols (0x28 = 160px).
+ // Intentionally drops the right-side game anim; _PlayInSequence redraws
+ // that character over a clean BG next.
Graphics::ManagedSurface briefingBase(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
briefingBase.clear();
@@ -464,16 +407,12 @@ void EEMEngine::doInitClues() {
}
}
- // Step 5 â `_PlayInSequence(animSeq, 0xcd, animY)` per Ghidra:
- // Jake (partner=0):
- // caseType=1 â anim 0x38 at (0xcd, 0x6d)
- // caseType=2 â anim 0x37 at (0xcd, 0x6c)
- // caseType=3 â anim 0x39 at (0xcd, 0x6c)
- // Jenny (partner=1):
- // caseType=2 â anim 0x3a at (0xcd, 0x6c)
- // caseType=3 â anim 0x3d at (0xcd, 0x6c)
- // `_PlayInSequence @ 172b:2d03` plays each frame at (sx-w, sy-rowoff)
- // with mask blit, advancing one frame per `_CheckFrameRate` tick.
+ // _PlayInSequence @ 172b:2d03. Anim selection per partner + caseType:
+ // Jake: caseType 1 -> 0x38 @ (0xcd, 0x6d)
+ // caseType 2 -> 0x37 @ (0xcd, 0x6c)
+ // caseType 3 -> 0x39 @ (0xcd, 0x6c)
+ // Jenny: caseType 2 -> 0x3a @ (0xcd, 0x6c)
+ // caseType 3 -> 0x3d @ (0xcd, 0x6c)
uint16 seqAni = 0xFFFF;
uint16 seqY = 0x6c;
if (_partner == 0) {
@@ -514,21 +453,13 @@ void EEMEngine::doInitClues() {
for (uint frame = 0; frame < seq.size() && !shouldQuit() && !skip;
frame++) {
const Picture &fr = seq[frame];
- // Restore the frozen setup frame so the short overlay
- // does not make the setup animation wrap to frame 0.
g_system->copyRectToScreen(briefingBase.getPixels(),
briefingBase.pitch, 0, 0,
320, 200);
- // Anchor: `_PlayInSequence @ 172b:2d35-2d50` does
- // dstX = sx - cell[+0x8] ; miscflags (signed)
- // dstY = sy - cell[+0x6] ; rowoff (signed)
- // Ghidra's C decompile mis-labels `cell[+0x8]` as
- // `width`; the actual asm is `SUB AX, ES:[BX+0x8]`,
- // which is the per-frame X anchor offset stored in our
- // `Picture::miscflags` field. Using `fr.surface.w`
- // shifted every frame by the cell width and made the
- // briefing partner appear duplicated next to the BG-
- // baked figure (mystery 1 office briefing).
+ // _PlayInSequence @ 172b:2d35-2d50:
+ // dstX = sx - cell[+0x8] ; signed X anchor (miscflags)
+ // dstY = sy - cell[+0x6] ; signed Y anchor (rowoff)
+ // asm `SUB AX, ES:[BX+0x8]` â NOT the cell width.
const int dstX = (int)0xcd - (int)(int16)fr.miscflags;
const int dstY = (int)seqY - (int)(int16)fr.rowoff;
blitMaskedToScreen(fr, dstX, dstY);
@@ -556,14 +487,11 @@ void EEMEngine::doInitClues() {
}
}
- // `_DoInitClues` plays a setup-voice ONLY for caseType 2/3.
- // CD: caseType 2 â PHONE.VOC. caseType 3 â no voice.
- // Floppy `_DoInitClues_Floppy @ 19bb:042f`:
- // caseType 2 â `_LoadSoundName_Floppy(slot 0xc)` = PHONESL.VOC
- // caseType 3 â `_LoadSoundName_Floppy(slot 3)` = NEWSCAN.VOC
- // (newspaper-scanner sting for the "TV/news anchor" briefing
- // variant). Other case types open straight into the briefing
- // dialogue without it.
+ // Setup voice (caseType 2/3 only):
+ // CD: caseType 2 -> PHONE.VOC
+ // Floppy (_DoInitClues_Floppy @ 19bb:042f):
+ // caseType 2 -> slot 0xc (PHONESL.VOC)
+ // caseType 3 -> slot 3 (NEWSCAN.VOC, news-anchor variant)
if (_audio) {
if (caseType == 2) {
if (floppy)
@@ -577,23 +505,15 @@ void EEMEngine::doInitClues() {
}
}
- // Step 6 â case briefing dialogue. CD InitBlock has the clue block
- // at +4 (after `u16 caseType; u16 startSite`); floppy uses
- // `u8 caseType; u8 nSubjects; subjects[nSubjects]; u8 nDialog;
- // dialog_records[nDialog]` (each record `11 + textCount` bytes),
- // dispatched via `FUN_22dc_05c8 @ 22dc:05c8`. We render dialog
- // records ourselves on floppy.
+ // Briefing dialogue. CD: clue block @ ib+4 (after caseType,startSite).
+ // Floppy: dialog records dispatched via FUN_22dc_05c8 @ 22dc:05c8
+ // (record size = 11 + textCount bytes).
if (floppy) {
displayFloppyBriefing(ib);
} else {
const byte *briefingClues = ib + 4;
- // Ghidra confirms CD `_DoInitClues` enters the normal
- // `_DisplayClue(_InitBlock + 2, 1)` path, and `_DisplayClue`
- // calls `_AddNotebook` for each ClueEntry note list at
- // +0x30..+0x39. These starting notes are required before the
- // first PDA visit, so mark them explicitly here. The regular
- // displayClue side-effect pass is idempotent and still handles
- // gallery/site updates in the original order.
+ // _DisplayClue calls _AddNotebook for each ClueEntry note list at
+ // +0x30..+0x39. Mark starting notes before the first PDA visit.
const uint marked = markClueBlockNotebookEntries(_mystery, briefingClues);
if (marked != 0)
debugC(1, kDebugScript,
@@ -603,27 +523,20 @@ void EEMEngine::doInitClues() {
}
}
-/// Mirror `_ParseString` @ 1b66:07c3 â substitute the control bytes that
-/// the original engine uses as placeholders. Only the two we encounter most
-/// often (player name = 0x80, partner first name = 0x82) are substituted;
-/// other 0x8N opcodes are stripped. The original engine also handles
-/// hyphenation marks and a hint placeholder (0x89) we ignore for now.
+// _ParseString @ 1b66:07c3 (jump table @ 1b66:0cbe). Each handler reads
+// _Partner (u16 @ 0x7918) and indexes the name table @ 29be:0c28
+// ({Jake, Jennifer, he, she, him, her, his} as far pointers).
+// 0x80 player name (auto-cap word starts, uses _PlayerRecord)
+// 0x81 _Partner == 0 ? "Jake" : "Jennifer" (chosen detective)
+// 0x82 _Partner == 0 ? "Jennifer" : "Jake" (the OTHER one)
+// 0x83 _Partner == 0 ? "he" : "she"
+// 0x84 _Partner == 0 ? "him" : "her"
+// 0x85 _Partner == 0 ? "his" : "her"
+// 0x86..0x88 read a separate gender flag @ 0x7985 (TODO).
+// 0x89 KD hint placeholder (caller handles).
Common::String EEMEngine::parseString(const Common::String &raw,
const Common::String &playerName,
uint partner) const {
- // Substitution opcodes from `_ParseString` @ 1b66:07c3, jump-table
- // at 1b66:0cbe. Each handler reads `_Partner` (16-bit at 0x7918)
- // and indexes the name table at 29be:0c28 ({Jake, Jennifer, he,
- // she, him, her, his} as far pointers).
- // 0x80 â player's typed name (auto-cap word starts) â uses _PlayerRecord
- // 0x81 â _Partner == 0 ? "Jake" : "Jennifer" (chosen detective)
- // 0x82 â _Partner == 0 ? "Jennifer" : "Jake" (the OTHER one)
- // 0x83 â _Partner == 0 ? "he" : "she"
- // 0x84 â _Partner == 0 ? "him" : "her"
- // 0x85 â _Partner == 0 ? "his" : "her"
- // 0x86..0x88 read a different gender flag at 0x7985 â left alone
- // until that flag's source is traced.
- // 0x89 â KD hint placeholder (handled by caller).
const bool isJake = (partner == 0);
Common::String out;
for (uint i = 0; i < raw.size(); i++) {
@@ -665,7 +578,7 @@ Common::String EEMEngine::parseString(const Common::String &raw,
// through the default case and renders as a literal,
// pushing the line past the balloon's visual edge â the
// "bubbles aren't large enough" symptom. Promote it to
- // `\n` so ScummVM's `Font::wordWrapText` (which honours
+ // `\n` so `Font::wordWrapText` (which honours
// embedded newlines) picks the same break point the
// original engine did.
out += '\n';
@@ -720,24 +633,21 @@ void EEMEngine::applyClueSideEffects(const byte *c) {
}
}
+// ClueBlock layout:
+// +0..1: number (entry count; 0 = no briefing)
+// +2..3: pic ID for entry 0; entry N>0 uses (entry-1).lastWord
+// +4..: array of 62-byte entries
void EEMEngine::displayClue(const byte *clueBlock) {
if (!clueBlock || !_mystery.isLoaded())
return;
- // ClueBlock layout (verified against M0.BIN):
- // +0..1: number (entry count)
- // +2..3: pic ID for entry 0 (entry N>0 uses prev entry's last 2 bytes)
- // +4..: array of 62-byte entries
const uint16 number = READ_LE_UINT16(clueBlock);
debugC(1, kDebugScript, "displayClue: %u entries", number);
- if (number == 0 || number > 32) {
- // number==0 = no briefing (e.g. mystery 0 case-type 4); >32 is a
- // guard against bad pointers.
+ // number == 0 = no briefing (e.g. mystery 0 case-type 4); >32 = bad ptr.
+ if (number == 0 || number > 32)
return;
- }
- // Snapshot the current screen as the BG so character pics from
- // earlier entries don't stack on top of each other.
+ // Snapshot BG so per-entry character pics don't stack.
Graphics::ManagedSurface bg(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
bg.clear();
@@ -749,40 +659,29 @@ void EEMEngine::displayClue(const byte *clueBlock) {
}
}
+ // ClueEntry layout (62 bytes):
+ // +0..1, +2..3: p0 tx, ty +4..5, +6..7: p1 tx, ty
+ // +8..9, +10..11: bubText offset p0/p1 (rel. TextBlock; -1 = none)
+ // +12..13, +14..15: balloon pic ID p0/p1
+ // +0x10/+0x12: bubX, bubY (partner 0)
+ // +0x14/+0x16: bubX, bubY (partner 1)
+ // +0x18: Jenny voice (1-based)
+ // +0x1a: Jake voice (1-based)
+ // +0x30..+0x39: 5 notebook entries (-1 terminated)
+ // +0x3a..+0x3b: KD-anim number (-1 = none)
+ // _DisplayClue @ 2404:05e6: partner 1 uses its own field set only when
+ // bubText1 != -1; otherwise falls back to partner 0 fields entirely.
+ // Partner 0 always uses field 0.
for (uint i = 0; i < number && !shouldQuit(); i++) {
- // Restore BG before drawing this entry's portrait + balloon.
g_system->copyRectToScreen(bg.getPixels(), bg.pitch, 0, 0, 320, 200);
const byte *c = clueBlock + 4 + i * 62;
- // Per-partner fields:
- // +0..1, +2..3: tx, ty (partner 0)
- // +4..5, +6..7: tx, ty (partner 1)
- // +8..9, +10..11: bubText offset for partner 0/1 (rel. TextBlock)
- // +12..13, +14..15: balloon picture ID for partner 0/1
- // +16..17, +18..19: bubX, bubY
- // +0x3a..+0x3b: KD-anim number (-1 = none)
- // Per `_DisplayClue` @ 2404:05e6: partner 1 uses its own field
- // set ONLY when bubText1 is not -1; otherwise it falls back to
- // the partner 0 fields entirely. Partner 0 always uses field 0.
-
- // Per-clue partner reaction animation. `_DisplayClue` @
- // 2404:0635-064b checks `clueEntry[+0x3a]` and, when not -1,
- // calls `_DoKDAnim(num)` BEFORE drawing the speaker portrait.
- // This is what surfaces "Jenny takes a picture with a camera"
- // (and the matching Jake gestures) during NPC searches.
+
+ // _DisplayClue @ 2404:0635-064b: _DoKDAnim(num) runs before the
+ // speaker portrait.
const int16 kdAnimNum = (int16)READ_LE_UINT16(c + 0x3a);
if (kdAnimNum != -1) {
playKdAnim((uint16)kdAnimNum);
- // `playKdAnim` leaves the screen on the partner-less
- // `_partnerEraseBg`, mirroring its between-frame erase
- // source. Re-stamp `bg` so the partner sprite captured at
- // `displayClue` entry returns before we draw the speaker
- // portrait + balloon. Without this the bubble lands on a
- // partner-less screen and the partner appears invisible
- // for the entire first iteration. Mirrors the original's
- // `_UpdateAnimations @ 172b:09c1` swap on `0x80`: the
- // kdAnim slot is freed and the WaitHandle (partner) slot
- // is reactivated with frame index 0xffff, so on the next
- // tick the partner renders at script[0] of its wait anim.
+ // _UpdateAnimations @ 172b:09c1 reactivates the wait anim.
g_system->copyRectToScreen(bg.getPixels(), bg.pitch,
0, 0, 320, 200);
}
@@ -798,12 +697,8 @@ void EEMEngine::displayClue(const byte *clueBlock) {
const uint16 bubNum = READ_LE_UINT16(c + (useP1 ? 0x0E : 0x0C));
const char *raw = hasText ? _mystery.textAt(textOff) : "";
- // Speaker portrait. Mirrors `_DisplayClue`'s `pic[clues+i*62-2]`:
- // for entry 0 the pic ID is in the ClueBlock header at +2; for
- // later entries it sits in the previous entry's last 2 bytes.
- // Speaker portrait position uses partner 0 fields (+0..+3) when
- // _partner==0 or when partner 1 falls back; otherwise partner 1
- // fields (+4..+7). Same logic as the original.
+ // Speaker portrait: pic[clues + i*62 - 2]. Entry 0 ID is in
+ // ClueBlock +2; entries N>0 read (entry-1)+0x3c (last word).
const uint16 charX = READ_LE_UINT16(c + (useP1 ? 4 : 0));
const uint16 charY = READ_LE_UINT16(c + (useP1 ? 6 : 2));
const uint16 charPicId = (i == 0)
@@ -817,14 +712,10 @@ void EEMEngine::displayClue(const byte *clueBlock) {
}
}
- // Substitute control bytes (0x80..0x89) â see `parseString` for
- // the table. 0x81 = chosen detective, 0x82 = the other one.
const Common::String text = parseString(raw ? raw : "",
_playerName, _partner);
- // Speech balloon. Mirrors `_GetBalloon` + `_AddPicBackground` in
- // `_DisplayClue`, with the shared balloon metadata table used for
- // text placement and the fitted balloon variant.
+ // Speech balloon: _GetBalloon + _AddPicBackground.
const uint16 fittedBubNum = fitBalloonToText(bubNum, text);
Picture balloon;
const uint16 balloonId = fittedBubNum & 0x7F;
@@ -833,9 +724,6 @@ void EEMEngine::displayClue(const byte *clueBlock) {
_balloonArchive.loadEntry(balloonId, balloon);
if (_font.isLoaded() && !text.empty()) {
- // Snapshot the current screen, overlay balloon + text, then
- // copy the changed band back. This preserves the site BG
- // underneath unchanged regions.
Graphics::Surface *screen = g_system->lockScreen();
if (!screen)
break;
@@ -853,27 +741,21 @@ void EEMEngine::displayClue(const byte *clueBlock) {
if (haveBalloon) {
const int bw = MIN<int>(balloon.surface.w, 320 - bubX);
const int bh = MIN<int>(balloon.surface.h, 200 - bubY);
- // `_AddPicBackground` passes `pic->miscflags >> 8` as
- // the transparent colour to `_Rect_Move_Mask`. The
- // on-disk u16 at file offset 0 maps to `Picture::flags`.
+ // _AddPicBackground: transparent colour = pic->miscflags >> 8.
const byte transp = (byte)(balloon.flags >> 8);
- // `_GetBalloon @ 172b:1d7d` mirrors the picture
- // horizontally when `(bubNum & 0x80)` is set â used
- // for right-side speakers so the tail points the
- // other way. ScummVM's `transBlitFrom` exposes the
- // same via its `flipped` argument.
+ // _GetBalloon @ 172b:1d7d mirrors horizontally when
+ // (bubNum & 0x80) â for right-side speakers.
const bool flipBalloon = (fittedBubNum & 0x80) != 0;
if (bw > 0 && bh > 0) {
scratch.transBlitFrom(balloon.surface,
Common::Point(bubX, bubY),
transp, flipBalloon);
}
- // Per-balloon metadata from `29be:0875` (52 Ã 10 bytes,
- // indexed by `bubNum & 0x7F`). The original `_DisplayClue`
- // does `_WordWrap(bubX + table[bub].x, bubY + table[bub].y,
- // table[bub].w, ...)`. `getBalloonInsets` is the shared
- // accessor (defined in `graphics.cpp`); fall back to the
- // (5, 4, 155) entry-23 inset if the lookup fails.
+ // Per-balloon insets at 29be:0875 (52 x 10 bytes,
+ // indexed by bubNum & 0x7F). _DisplayClue does
+ // _WordWrap(bubX + table[bub].x, bubY + table[bub].y,
+ // table[bub].w, ...).
+ // Fallback (5, 4, 155) = entry-23 inset.
uint16 bx = 5;
uint16 by = 4;
uint16 bw_ = 155;
@@ -883,16 +765,15 @@ void EEMEngine::displayClue(const byte *clueBlock) {
textW = bw_;
copyH = bh;
} else {
- // No balloon â clear a band so old pixels don't bleed.
+ // No balloon: clear band.
const Common::Rect band(0, bubY, 320,
MIN<int>(bubY + copyH, 200));
scratch.fillRect(band, 0);
copyY = bubY;
}
- // `_DisplayClue` @ 2404:07fe passes fontColor=0 (palette
- // index 0 of the case-briefing palette 0x22) to `_WordWrap`.
- // Hard-coding 0xF here gave the wrong colour.
+ // _DisplayClue @ 2404:07fe passes fontColor=0 (palette
+ // index 0 of case-briefing palette 0x22).
_font.drawWordWrapped(&scratch, textX, textY,
MAX<int>(8, textW), text, 0);
@@ -902,22 +783,14 @@ void EEMEngine::displayClue(const byte *clueBlock) {
g_system->updateScreen();
}
- // `_DisplayClue` @ 2404:0833-085a â after the balloon is drawn,
- // spool the per-clue voice. Each ClueEntry stores two 1-based
- // sound indices: `+0x18` (Jenny voice) and `+0x1a` (Jake voice).
- //
- // Critical gate (verified at 2404:0833):
+ // _DisplayClue @ 2404:0833-085a â per-clue voice gate:
// if (clue[+0x18] != 0 && voiceOn && voiceAvail) {
- // iVar6 = clue[+0x18]; // Jenny default
- // if (Partner == 0) iVar6 = clue[+0x1a]; // Jake override
+ // iVar6 = clue[+0x18]; // Jenny default
+ // if (Partner == 0) iVar6 = clue[+0x1a]; // Jake override
// _SpoolSound(iVar6 - 1);
// }
- // The condition gates on the JENNY slot regardless of partner.
- // Some hotspot ClueBlocks define `+0x1a` (Jake voice) but leave
- // `+0x18` at 0 â for those, the original engine plays nothing
- // and the entry is text-only. Our previous code gated on the
- // partner-selected slot and ended up firing unrelated voices
- // (e.g., a "no audio" entry triggering Jake's spoolSound).
+ // Gate is on the Jenny slot regardless of partner; entries with
+ // +0x18 == 0 but +0x1a set are text-only.
if (_audio) {
const uint16 voiceJenny = READ_LE_UINT16(c + 0x18);
if (voiceJenny != 0 && voiceJenny != 0xFFFF) {
@@ -928,19 +801,10 @@ void EEMEngine::displayClue(const byte *clueBlock) {
}
}
- // Wait for click/key to advance â only if we drew something.
- // ESC skips the entire dialogue rather than just one entry.
- // Only Return / KP-Enter / Space advance one entry; other keys
- // are ignored so accidental keystrokes don't blow past dialog
- // the player hasn't finished reading.
+ // Wait for click/key. ESC skips entire dialog; Return / KP-Enter /
+ // Space advance one entry.
if (hasText || (charPicId != 0 && charPicId != 0xFFFF)) {
- // Click during a clue dialog only dismisses â there are no
- // hover-interactive areas. Drop the highlighted cursor
- // state the site loop may have left set so the player
- // doesn't see a "clickable" cursor stuck on top of the
- // balloon. The post-dialog `MOUSEMOVE` in the site loop
- // will re-evaluate against hotspots and re-enable as
- // needed.
+ // Clear any leftover highlighted-cursor state from the site loop.
setInteractiveMouseCursor(false);
bool advance = false;
bool skipAll = false;
@@ -953,9 +817,6 @@ void EEMEngine::displayClue(const byte *clueBlock) {
break;
}
if (ev.type == Common::EVENT_MOUSEMOVE) {
- // Keep the cursor non-interactive across moves
- // inside the dialog (defensive â in case some
- // other code path tries to set it).
setInteractiveMouseCursor(false);
continue;
}
@@ -963,13 +824,7 @@ void EEMEngine::displayClue(const byte *clueBlock) {
ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
advance = true;
skipAll = true;
- // Cut the per-clue voice line that was just
- // spooled â without this the voice keeps
- // playing past the dialog dismissal and
- // bleeds into the next screen (e.g. the
- // case-briefing voice still talking on the
- // MAP after ESC). Site / briefing MIDI
- // stays â only voice + spool halt here.
+ // Cut voice + spool only (keep MIDI).
interruptAudio(/*stopMusicToo=*/false);
break;
}
@@ -985,16 +840,11 @@ void EEMEngine::displayClue(const byte *clueBlock) {
break;
}
}
- // Tick the screen so the OSystem cursor follows the
- // mouse â ScummVM redraws the cursor overlay only on
- // updateScreen.
g_system->updateScreen();
g_system->delayMillis(10);
}
if (skipAll) {
- // Apply remaining side-effects without rendering. The
- // original silently runs the state updates even when the
- // player skips ahead.
+ // Apply remaining side-effects without rendering.
for (uint k = i; k < number; k++)
applyClueSideEffects(clueBlock + 4 + k * 62);
return;
@@ -1004,13 +854,8 @@ void EEMEngine::displayClue(const byte *clueBlock) {
applyClueSideEffects(c);
}
- // `_DisplayClue @ 2404:05e6` lets the per-clue voice bleed past
- // the last entry's dismissal â `_Wait()` returns on ANY input
- // without touching the digital playback. We deliberately diverge
- // (mirroring `_StopTheVoice @ 1ff1:0283`'s effect, voice only,
- // MIDI untouched) so the briefing voice line doesn't keep talking
- // over the next screen when the player clicks through the final
- // entry.
+ // _StopTheVoice @ 1ff1:0283 effect (voice only, keep MIDI). Diverges
+ // from _DisplayClue @ 2404:05e6 which lets voice bleed past dismissal.
interruptAudio(/*stopMusicToo=*/false);
}
@@ -1027,10 +872,9 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
// u8 sound @ +9 (high bit = play voice, low 7 bits = slot)
// u8 textCount@ +10
// u8 textIdx[]@ +11 (1 byte per â low 7 bits = NOTES idx)
- // Text offsets in NOTES are ABSOLUTE byte offsets into the mystery
- // buffer (verified by note 0 of M0.BIN at file offset 0xd0 holding
- // "Hello, ..., I'm ... Eagle!"), so we read text via
- // `mystery.blobAt(noteEntry[+2..3])` for Jake / `+4..5` for Jenny.
+ // NOTES text offsets are absolute byte offsets into the mystery
+ // buffer; read via mystery.blobAt(noteEntry[+2..3]) for Jake,
+ // +4..5 for Jenny.
if (!rec || !isFloppy() || !_font.isLoaded() || count == 0)
return;
@@ -1039,8 +883,7 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
if (!notes || !bufBase)
return;
- // Snapshot the current screen so each record restores the
- // briefing background between bubbles.
+ // Snapshot BG for between-bubble restores.
Graphics::ManagedSurface bg(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
{
@@ -1054,16 +897,9 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
const uint32 dsz = _mystery.dataSize();
const uint32 notesBase = (uint32)(notes - bufBase);
- // Pre-mark every text index across all records as "seen" up front.
- // `_DisplayHotspotClue_Floppy @ 22dc:05c8` writes
- // `_TextSeen_Floppy[idx] = 1` inside its render loop, but the
- // original's `_WaitForClick` blocks until input â so the loop always
- // runs to completion and every text gets marked. Our `waitForClick`
- // honours ESC as "skip all" (sets `skipAll`/break), which matches
- // player expectations for fast-forward but used to drop the seen-bit
- // for every text after the ESC point. Pre-marking restores parity:
- // ESC fast-forwards the *visual* but the notebook always reflects
- // every clue the player would have seen if they'd clicked through.
+ // _DisplayHotspotClue_Floppy @ 22dc:05c8 writes _TextSeen_Floppy[idx]=1
+ // inline per text. Pre-mark all up front so ESC-fast-forward still
+ // records every clue in the notebook.
{
const byte *r = rec;
for (uint i = 0; i < count; i++) {
@@ -1078,14 +914,9 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
}
auto waitForClick = [&]() -> bool {
- // Drain pending events first so a previous keystroke's tail
- // doesn't auto-advance the new page.
+ // Drain pending events so a stale keystroke doesn't auto-advance.
Common::Event drain;
while (g_system->getEventManager()->pollEvent(drain)) {}
- // Click during the floppy dialog only dismisses â no
- // hover-interactive areas. Clear any stuck highlight from the
- // site loop so the cursor stays a normal pointer over the
- // balloon.
setInteractiveMouseCursor(false);
const uint32 minVisibleMs = 250;
const uint32 startedAt = g_system->getMillis();
@@ -1125,25 +956,18 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
const uint8 ballY = rec[8];
const uint8 textCount= rec[10];
- // Per-record voice (byte 9 high bit) â see comment in original
- // header.
+ // byte 9: high bit = play voice slot.
const bool playedRecordVoice = (rec[9] & 0x80) != 0 && _audio;
if (playedRecordVoice) {
const uint slot = rec[9] & 0x7f;
_audio->playFloppyVoiceSlot(slot, _partner);
}
- // Suspect-found side effect for byte 9 with high bit clear.
- // Mirrors `_DisplayHotspotClue_Floppy @ 22dc:0908..0922`:
+ // _DisplayHotspotClue_Floppy @ 22dc:0908..0922 suspect-found:
// if ((rec[9] & 0x80) == 0 && rec[9] != 0)
// _InGallery_Floppy[_GalleryShuffleSeed_Floppy[rec[9]]] = 1;
- // `_GalleryShuffleSeed_Floppy` is a byte array at DS:0x2d65 that
- // overlaps `_NewOrder_Floppy` at DS:0x2d66 by one byte: i.e.
- // `_GalleryShuffleSeed[i+1] == _NewOrder[i]` (verified by
- // comparing the two writes in `_ReadMystery_Floppy @ 22dc:0178`).
- // So in our terms the mapping is `_inGallery[_newOrder[b9 - 1]]`.
- // Skipping `_newOrder` (as we did before) drew the right portrait
- // only when `_newOrder` happened to be identity â randomized
- // shuffles dropped one of the two M0 suspects.
+ // _GalleryShuffleSeed (DS:0x2d65) overlaps _NewOrder (DS:0x2d66)
+ // by one byte: GalleryShuffleSeed[i+1] == NewOrder[i]. So:
+ // _inGallery[_newOrder[b9 - 1]].
const uint8 b9 = rec[9];
if ((b9 & 0x80) == 0 && b9 != 0) {
const uint logicalIdx = (uint)b9 - 1;
@@ -1216,14 +1040,9 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
const int textX = ballX + textXIns;
const int lineH = _font.getFontHeight();
- // Pagination state â `FUN_22dc_05c8`'s text-idx loop uses
- // `local_1c` (set from the PREVIOUS text's flag bit) to decide
- // between "fresh page" (redraw balloon, restart Y at top) and
- // "continuation" (append below previous lines). We mirror that
- // state machine so multi-text records render the same way the
- // original does â without it our impl concatenates every text
- // idx into ONE balloon and the result spills out the bottom
- // (the user's "bubbles aren't large enough" screenshot).
+ // FUN_22dc_05c8 pagination via local_1c (set from previous text's
+ // flag bit): 0 = fresh page (redraw balloon, Y at top),
+ // 1 = continuation (append below previous lines).
bool firstPage = true;
int cursorY = ballY + textYIns;
bool skipAll = false;
@@ -1255,9 +1074,7 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
scratch.simpleBlitFrom(*bg.surfacePtr());
if (firstPage) {
- // Optional character portrait â only draws once per
- // fresh page (matches the original which only redraws
- // the balloon on `local_1c == 0`).
+ // Original only redraws balloon + portrait on local_1c == 0.
if (picID != 0 && picID != 0xFFFF) {
Picture pic;
if (_picsArchive.getPicture(picID, pic)) {
@@ -1285,7 +1102,6 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
cursorY = ballY + textYIns;
}
- // Wrap text into lines and draw each at cursorY.
Common::Array<Common::String> lines;
_font.wordWrapText(text, MAX<int>(8, (int)textWidth), lines);
for (uint l = 0; l < lines.size(); l++) {
@@ -1295,26 +1111,20 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
}
cursorY += (int)lines.size() * lineH;
- // Decide pagination for the NEXT text idx based on THIS
- // text's high bit.
+ // Pagination for next text idx is driven by this text's high bit.
const bool textHighBit = (idxByte & 0x80) != 0;
const bool isLastText = (t + 1 == textCount);
const bool isLastRec = (i + 1 == count);
- // Stamp the "click to continue" indicator (PIC 0xa0
- // "more" arrow / PIC 0xa1 end indicator) before
- // flipping to wait. Mirrors `_DisplayHotspotClue_Floppy
- // @ 22dc:08aa` (mid-page) and `@ 22dc:08c0`
- // (end-of-record). We skip drawing it on the very last
- // click of the very last record when the caller passes
- // `lastIndicator == 0` â that's the original `param_2 ==
- // 0` "no indicator" case.
+ // "Click to continue" indicator: PIC 0xa0 ("more" arrow) or
+ // PIC 0xa1 (end). _DisplayHotspotClue_Floppy @ 22dc:08aa
+ // (mid-page) / @ 22dc:08c0 (end-of-record). lastIndicator == 0
+ // matches the original param_2 == 0 "no indicator" case.
bool waitNeeded = false;
bool drawArrow = false;
bool useEndPic = false;
if (!isLastText) {
if (textHighBit) {
- // continuation â no wait, no indicator
firstPage = false;
} else {
waitNeeded = true;
@@ -1322,15 +1132,12 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
useEndPic = false;
}
} else {
- // Last text in this record.
waitNeeded = true;
if (!isLastRec) {
- // More records follow â original passes
- // `param_2 = 1` â PIC 0xa0.
+ // More records follow â param_2 = 1 â PIC 0xa0.
drawArrow = true;
useEndPic = false;
} else {
- // Last record. Use caller-supplied indicator.
if (lastIndicator == 1) {
drawArrow = true;
useEndPic = false;
@@ -1373,11 +1180,8 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
}
void EEMEngine::displayFloppyBriefing(const byte *initBlock) {
- // Floppy briefing â `FUN_19bb_042f @ 19bb:042f` walks
- // `nDialog = ib[2 + nSubjects]` records starting at
- // `ib + 3 + nSubjects`. Each record is rendered identically to a
- // hotspot dialog record (same `FUN_22dc_05c8` callee), so we
- // share `displayFloppyDialogRecords`.
+ // FUN_19bb_042f @ 19bb:042f: walks nDialog = ib[2 + nSubjects] records
+ // starting at ib + 3 + nSubjects, dispatching each to FUN_22dc_05c8.
if (!initBlock || !isFloppy())
return;
const uint8 nSubjects = initBlock[1];
@@ -1387,19 +1191,12 @@ void EEMEngine::displayFloppyBriefing(const byte *initBlock) {
}
void EEMEngine::displayFloppyHotspotDialog(uint siteNum, uint hotIdx) {
- // Floppy hotspot click â mirrors `FUN_22dc_0b80 @ 22dc:0b80` +
- // `FUN_1652_00e6 @ 1652:00e6` + `FUN_1652_006c @ 1652:006c`.
- // Each site stores a per-hotspot dialog list at
- // `site_data[+6..7]`. The list is laid out as:
- // for each hotspot in order:
+ // FUN_22dc_0b80 @ 22dc:0b80 + FUN_1652_00e6 @ 1652:00e6 +
+ // FUN_1652_006c @ 1652:006c. site_data[+6..7] -> per-hotspot list:
+ // for each hotspot:
// main record (11 + textCount bytes)
- // u8 contFlags (low 7 bits = continuation count, high bit
- // used by FUN_1652_00e6 to drive the partner
- // pose â irrelevant for text rendering)
- // contCount à { record (11 + textCount bytes) }
- // We walk past `hotIdx` hotspots, then dispatch the matched main
- // record + its continuation chain through the same renderer
- // `displayFloppyBriefing` uses.
+ // u8 contFlags (low 7 = cont count, high bit = partner pose)
+ // contCount x { record (11 + textCount bytes) }
if (!_mystery.isLoaded() || !isFloppy())
return;
const byte *site = _mystery.siteData(siteNum);
@@ -1411,10 +1208,8 @@ void EEMEngine::displayFloppyHotspotDialog(uint siteNum, uint hotIdx) {
return;
uint32 off = dlgListOff;
for (uint h = 0; h < hotIdx; h++) {
- // Skip main record.
const byte *rec = bufBase + off;
off += 11 + rec[10];
- // Read continuation count and skip those records.
const uint contCount = bufBase[off] & 0x7F;
off += 1;
for (uint c = 0; c < contCount; c++) {
@@ -1424,23 +1219,8 @@ void EEMEngine::displayFloppyHotspotDialog(uint siteNum, uint hotIdx) {
}
if (off >= _mystery.dataSize())
return;
- // Layout per hotspot is:
- // main record (11 + textCount bytes)
- // 1 byte: continuation count (high bit = partner-pose flag,
- // low 7 bits = number of follow-up records)
- // continuation records (each 11 + textCount bytes, tightly packed)
- // `displayFloppyDialogRecords` walks tightly so we have to call it
- // twice â once for the main record, once for the continuations â
- // otherwise the second iteration treats the cont-count byte as a
- // record header and runs off the buffer.
- //
- // Each `displayFloppyDialogRecords` call snapshots the screen to
- // know what to redraw between bubbles. If we just call it twice
- // back-to-back the second snapshot includes the first call's last
- // bubble, so the second bubble draws on top of the first one (the
- // "background isn't redrawn" glitch the user reported). Capture
- // the clean site BG here and restore it between the two calls so
- // every record snapshots a bubble-free background.
+ // Snapshot clean site BG and restore between main + continuation calls
+ // so each displayFloppyDialogRecords sees a bubble-free background.
Graphics::ManagedSurface siteBG(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
{
@@ -1458,11 +1238,10 @@ void EEMEngine::displayFloppyHotspotDialog(uint siteNum, uint hotIdx) {
contFlagsByte = bufBase[off + mainLen];
contCount = contFlagsByte & 0x7F;
}
- // `_HandleHotspotClick_Floppy @ 1652:00e6` derives the main
- // record's `param_2` from the continuation byte:
- // contFlagsByte == 0 â 0 (no indicator, only one record)
- // high bit set â 1 (PIC 0xa0, "more" arrow)
- // low 7 bits non-zero â 2 (PIC 0xa1, alternate end)
+ // _HandleHotspotClick_Floppy @ 1652:00e6 derives param_2:
+ // contFlagsByte == 0 -> 0 (no indicator)
+ // high bit set -> 1 (PIC 0xa0, "more")
+ // low 7 bits non-zero -> 2 (PIC 0xa1, alt end)
uint mainIndicator = 0;
if (contFlagsByte != 0) {
mainIndicator = (contFlagsByte & 0x80) ? 1 : 2;
@@ -1473,19 +1252,12 @@ void EEMEngine::displayFloppyHotspotDialog(uint siteNum, uint hotIdx) {
const uint32 contOff = off + mainLen + 1;
if (contOff >= _mystery.dataSize())
return;
- // Wipe the main bubble before the continuation chain snapshots the
- // screen â otherwise the first continuation bubble treats the
- // post-main-bubble image as its background and the main balloon
- // pixels persist behind every following balloon.
+ // Wipe main bubble so the continuation chain snapshots a clean BG.
g_system->copyRectToScreen(siteBG.getPixels(), siteBG.pitch,
0, 0, 320, 200);
g_system->updateScreen();
- // Continuation chain: `_DisplayDialogContinuations_Floppy @
- // 1652:006c` passes `param_2 + -1 != 0` (= 1 if more records
- // follow, 0 on the last). Our renderer maps that automatically
- // because mid-batch records always get PIC 0xa0; the last record
- // uses our `lastIndicator` argument (= 0, no indicator on the
- // final continuation).
+ // _DisplayDialogContinuations_Floppy @ 1652:006c: lastIndicator=0
+ // means no indicator on the final continuation.
displayFloppyDialogRecords(bufBase + contOff, contCount, 0);
}
@@ -1498,12 +1270,10 @@ bool EEMEngine::areYouSure() {
g_system->unlockScreen();
}
- // CD `_AreYouSure @ 1a35:0a5c` and floppy `FUN_19bb_0b43` both:
- // * load PIC 0x136 as the dialog body,
- // * load PIC 0x1fd / 0x1fe as the pressed YES / NO button images,
- // * center the dialog,
- // * hit-test YES at (x+0x0c,y+0x23)-(x+0x20,y+0x32),
- // and NO at (x+0x60,y+0x23)-(x+0x74,y+0x32).
+ // _AreYouSure @ 1a35:0a5c (CD) / FUN_19bb_0b43 (floppy):
+ // PIC 0x136 = dialog body; PIC 0x1fd/0x1fe = YES/NO pressed buttons.
+ // YES hit: (x+0x0c, y+0x23) .. (x+0x20, y+0x32)
+ // NO hit: (x+0x60, y+0x23) .. (x+0x74, y+0x32)
Picture dialogPic;
Picture yesPic;
Picture noPic;
@@ -1569,7 +1339,7 @@ bool EEMEngine::areYouSure() {
break;
}
if (ev.type == Common::EVENT_KEYDOWN) {
- // Spanish prompt is "S - Si" â accept both Y and S.
+ // Spanish prompt is "S - Si": accept Y and S.
if (ev.kbd.keycode == Common::KEYCODE_y ||
ev.kbd.keycode == Common::KEYCODE_s ||
ev.kbd.keycode == Common::KEYCODE_RETURN) {
@@ -1605,7 +1375,6 @@ bool EEMEngine::areYouSure() {
g_system->delayMillis(15);
}
- // Restore the screen so the caller's UI is intact.
g_system->copyRectToScreen(saved.getPixels(), saved.pitch, 0, 0, 320, 200);
g_system->updateScreen();
return result;
diff --git a/engines/eem/detection.cpp b/engines/eem/detection.cpp
index 7fe94f56f39..48cf5629a1d 100644
--- a/engines/eem/detection.cpp
+++ b/engines/eem/detection.cpp
@@ -56,10 +56,7 @@ const ADGameDescription gameDescriptions[] = {
GUI_OPTIONS_EEM_FLOPPY
},
{
- // Spanish floppy release â same EEM.EXE binary as the English
- // floppy (the engine code is identical), with a localised
- // PICS.DBD that swaps the embedded English image text for
- // Spanish equivalents.
+ // Spanish floppy: same EEM.EXE as English floppy, localised PICS.DBD.
"eem",
"Floppy",
AD_ENTRY2s("EEM.EXE", "692a5e6e7f4516d6e40c1f80cbc1b2cc", 109542,
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 0af062785e1..b809bf86c41 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -47,28 +47,25 @@
namespace EEM {
-const uint kPalSize = 768; ///< 256 colors * 3 bytes
-const uint kNumSitePals = 40; ///< SITEPALS holds 40 palettes (40 * 768 = 30720)
+const uint kPalSize = 768;
+const uint kNumSitePals = 40; // SITEPALS: 40 * 768 = 30720
-// Picture / palette IDs from the original code (1-based picture IDs).
-const uint kPicEAKidsLogo = 0x54; ///< _ShowEAKids: GetPicture(0x54)
-const uint kPicHighScoreLogo = 0x20c; ///< _ShowHScoreLogo: GetPicture(0x20c)
-const uint kPicStormLogo = 0x20b; ///< Floppy storm-logo still: PIC 0x20b
+// 1-based picture/palette IDs.
+const uint kPicEAKidsLogo = 0x54; // _ShowEAKids
+const uint kPicHighScoreLogo = 0x20c; // _ShowHScoreLogo
+const uint kPicStormLogo = 0x20b; // Floppy storm-logo still
const uint kPalEAKids = 0x25;
const uint kPalHighScore = 0x27;
-const uint kPalStormLogo = 0x26; ///< Floppy `FUN_23d2_0605` palette idx
-const uint kPicMousePointer = 0x50; ///< Original startup pointer; 0x51 is the wait cursor
+const uint kPalStormLogo = 0x26; // Floppy FUN_23d2_0605
+const uint kPicMousePointer = 0x50; // 0x51 is the wait cursor
const byte kSaveBodyVer = 1;
-// Internal test switch: populate ScrapBook 1 at startup without exposing a
-// game option or changing save format. Set false before release.
+// Test switch: populate ScrapBook 1 at startup without exposing a game
+// option or changing save format. Set false before release.
const bool kDebugPopulateScrapbook1AtStartup = false;
-// Fallback 11x16 mouse cursor used if the selected PIC pointer cannot be
-// loaded. The original game sets the cursor visible/hidden via
-// _MouseCursor; we leave it on once the screens that need it
-// (ChoosePartner, ActionScreen, CaseSelection, sites) are reached.
+// Fallback 11x16 cursor used if PIC pointer load fails.
// 0 = transparent, 1 = black outline, 2 = white fill
const byte kCursorBitmap[11 * 16] = {
1,1,0,0,0,0,0,0,0,0,0,
@@ -89,14 +86,14 @@ const byte kCursorBitmap[11 * 16] = {
0,0,0,0,0,0,1,2,2,1,0
};
const byte kCursorPalette[] = {
- 0x00, 0x00, 0x00, // 0 â transparent (key)
- 0x00, 0x00, 0x00, // 1 â outline
- 0xFF, 0xFF, 0xFF // 2 â fill
+ 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF
};
const byte kCursorInteractivePalette[] = {
- 0x00, 0x00, 0x00, // 0 â transparent (key)
- 0xFF, 0x00, 0x00, // 1 â red outline
- 0xFF, 0xFF, 0xFF // 2 â white fill
+ 0x00, 0x00, 0x00,
+ 0xFF, 0x00, 0x00,
+ 0xFF, 0xFF, 0xFF
};
static void fadeCurrentPaletteToBlack(uint delayMs = 8) {
@@ -201,10 +198,6 @@ EEMEngine::EEMEngine(OSystem *syst, const ADGameDescription *gameDesc)
ConfMan.registerDefault("hide_highlight_boxes", false);
ConfMan.registerDefault("fit_dialog_balloons", false);
- // `ADGameDescription::extra` is set by the matching entry in
- // `gameDescriptions[]` ("CD" or "Floppy"). Keep variant detection
- // purely string-based so a future re-release with a different
- // `extra` tag falls back to CD-style asset paths.
_variant = (gameDesc && gameDesc->extra &&
Common::String(gameDesc->extra).contains("Floppy"))
? kVariantFloppy : kVariantCD;
@@ -228,7 +221,7 @@ void EEMEngine::applyStartupTestOverrides() {
}
Common::Error EEMEngine::run() {
- // _SetMode13X @ 1000:0358 enters VGA mode 13h (320x200x256).
+ // _SetMode13X @ 1000:0358 â VGA mode 13h.
initGraphics(320, 200);
if (!openArchives())
@@ -237,48 +230,36 @@ Common::Error EEMEngine::run() {
if (!loadSitePalettes())
return Common::Error(Common::kReadingFailed, "SITEPALS load failed");
- // _LoadFont @ 1b66:023c â main 8 px bitmap font.
+ // _LoadFont @ 1b66:023c.
if (!_font.load(Common::Path("FONT.FNT")))
warning("FONT.FNT failed to load; text will not render");
- // MIDI music player. Mirrors `_InitMIDI @ 20a2:013a`. Constructed
- // here (after `initGraphics` so the OSystem's timer/mixer is up).
+ // _InitMIDI @ 20a2:013a.
_music = new MusicPlayer(isFloppy());
- // Digital audio (VOC + spool). Mirrors `_InitDrivers @ 1ff1:0368`
- // which `_AIL_register_driver`s SBDIG.ADV / PASDIG.ADV alongside
- // the MIDI driver.
+ // _InitDrivers @ 1ff1:0368 (SBDIG.ADV / PASDIG.ADV).
_audio = new AudioPlayer(this);
_audio->setVoiceEnabled(_voiceOn);
syncSoundSettings();
- // CD `_main @ 1a35:0f59` and floppy `_main_Floppy @ 19bb:1012`
- // both load `_GetPicture(0x50)` as the active mouse pointer before
- // calling `_InitMouse`. PIC 0x51 is present in both archives but has
- // no executable xrefs and appears to be the wait cursor.
+ // CD `_main @ 1a35:0f59` and floppy `_main_Floppy @ 19bb:1012` both
+ // load `_GetPicture(0x50)` as the active mouse pointer before
+ // `_InitMouse`. PIC 0x51 is present in both archives but has no
+ // executable xrefs and appears to be the wait cursor.
// CD's `_SwitchMouse` supports swapping to a hotspot cursor ID stored
// at search record +0x0c, but the shipped CD mystery data only uses
// cursor 0; floppy search records have no cursor-id field.
installMouseCursor(_picsArchive, false);
CursorMan.showMouse(false);
- // _AllBlack @ 172b:0d4b paints the screen black before the first handler.
+ // _AllBlack @ 172b:0d4b.
byte black[3 * 256] = { 0 };
g_system->getPaletteManager()->setPalette(black, 0, 256);
debugC(1, kDebugGeneral, "EEM engine starting");
- // If the user chose "Load" before pressing Play, the framework
- // invokes `loadGameState` which sets up `_playerName`, `_partner`,
- // `_mysteriesSolved`, and (optionally) `_mystery`. Honour that by
- // skipping the intros â the player has already typed their name
- // and picked a partner, so the title chain + profile picker +
- // partner picker would all be redundant.
- //
- // * Save HAS a mystery in progress â resume at MAP (mirrors the
- // original's post-briefing state, handler 0 at 1a35:0e1d).
- // * Save has NO mystery â drop into `_ActionScreen`, same as the
- // original after partner selection.
+ // Resume from save: mystery in progress â MAP (handler 0 @ 1a35:0e1d);
+ // otherwise â ACTION.
const int wantedSave = ConfMan.hasKey("save_slot")
? ConfMan.getInt("save_slot") : -1;
bool resumed = false;
@@ -303,51 +284,40 @@ Common::Error EEMEngine::run() {
}
}
- // Skip the entire intro chain (logos + anims + name entry +
- // partner pick) when resuming a saved profile â the partner is
- // already known, the player has already named themselves, and the
- // loaded mystery's site loop is what they want to see again.
if (resumed)
goto screen_loop;
- // Reproduces _DoOpeningAnims @ 2520:082a:
- // EA Kids logo (PIC) -> HighScore Productions logo (PIC) ->
- // Storm Software logo (BOLT.ANM) -> [music starts] -> 20
- // character-intro animations (ANIM01.A..ANIM20.A) -> [music
- // restarts] -> TITLE.ANM. Click / any key skips a single clip;
- // ESC skips the rest of the chain (waitForInput / playAnm raise
- // `_skipIntro` so each subsequent step bails out).
- //
- // Music timing (verified at 2520:0883 and 2520:0918):
- // - The three logos and `_InitMysterySounds(0x3c)` all run BEFORE
- // any `_MIDIPlayFile` call â those segments are voice-only.
+ // _DoOpeningAnims @ 2520:082a:
+ // EA Kids logo (PIC) -> HighScore logo (PIC) -> Storm logo
+ // (BOLT.ANM) -> [music starts] -> 20 character-intro anims
+ // (ANIM01.A..ANIM20.A) -> [music restarts] -> TITLE.ANM. Click /
+ // any key skips one clip; ESC raises _skipIntro so each step bails
+ // out.
+ // Music timing (2520:0883 + 2520:0918):
+ // - The three logos and `_InitMysterySounds(0x3c)` run BEFORE any
+ // `_MIDIPlayFile` â those segments are voice-only.
// - Theme starts with `_LoopMIDI = 0x7fff` right before the
// ANIM01..ANIM20 loop (2520:0883).
- // - After the loop the original calls `_CleanMysterySounds` and
- // then `_MIDIPlayFile("theme.xmi")` again with `_LoopMIDI =
- // 0xffff` (2520:0918) to restart the theme for TITLE.ANM.
- // - `_StopMIDI()` runs on keypress at the title screen
- // (2520:094c).
+ // - After the loop the original calls `_CleanMysterySounds` then
+ // `_MIDIPlayFile("theme.xmi")` again with `_LoopMIDI = 0xffff`
+ // (2520:0918) to restart for TITLE.ANM.
+ // - `_StopMIDI()` runs on keypress at the title screen (2520:094c).
_skipIntro = false;
if (isFloppy()) {
- // Floppy opening â driven by `FUN_23d2_039c @ 23d2:039c`:
+ // Floppy opening â `FUN_23d2_039c @ 23d2:039c`:
// FUN_23d2_0170() â clear palette
// FUN_23d2_004b() â set up timer
- // FUN_23d2_050c() â show PIC 0x54 (EA Kids logo, palette 0x25)
- // FUN_23d2_06c6() â show PIC 0x20c (High Score logo, palette 0x27)
- // FUN_23d2_0605() â show PIC 0x20b (Storm Software, palette
- // 0x26) AND play voice slot 25 (thunder.voc
- // â verified via the Jake voice table at
- // `2608:0f0e` slot 25 â `2608:11ac` =
- // "thunder.voc").
+ // FUN_23d2_050c() â PIC 0x54 (EA Kids, palette 0x25)
+ // FUN_23d2_06c6() â PIC 0x20c (High Score, palette 0x27)
+ // FUN_23d2_0605() â PIC 0x20b (Storm, palette 0x26) AND
+ // voice slot 25 = "thunder.voc" (via Jake
+ // voice table 2608:0f0e slot 25 â
+ // 2608:11ac).
// _MIDIPlayFile("theme.xmi", loop=1)
- // _PlayANM(idx=0) â CHAT.ANM (filename table at `2608:14fe`
- // index 0 â "chat.anm" at `2608:150a`).
- // _PlayANM(idx=1) â MOVIE.ANM (table index 1 â `2608:1513`).
- // `TITLE.ANM` is shown later by screen-`0xb` handler `@
- // 19bb:0ebc` once the intro driver returns. The thunder VOC
- // alongside the storm logo is the "intro voice" the user heard
- // missing â without it the lightning-bolt logo plays silently.
+ // _PlayANM(0) â CHAT.ANM (filename table 2608:14fe[0] â
+ // "chat.anm" at 2608:150a)
+ // _PlayANM(1) â MOVIE.ANM (table[1] â 2608:1513)
+ // TITLE.ANM is shown later by screen-0xb handler @ 19bb:0ebc.
if (!shouldQuit() && !_skipIntro)
showEAKidsLogo();
if (!shouldQuit() && !_skipIntro)
@@ -366,11 +336,10 @@ Common::Error EEMEngine::run() {
showEAKidsLogo();
if (!shouldQuit() && !_skipIntro)
showHighScoreLogo();
- // Storm Software logo: voice + animation. The original at
- // `_ShowStormLogo @ 2520:0707` calls `_LoadSoundName(
- // "thunder.voc")` (29be:177d) and passes the buffer to
- // `OpenDifferenceAnimation_Sound` so the thunder roar plays
- // alongside the lightning bolt.
+ // Storm Software logo: voice + animation. `_ShowStormLogo @
+ // 2520:0707` calls `_LoadSoundName("thunder.voc")` (29be:177d)
+ // and passes the buffer to `OpenDifferenceAnimation_Sound` so
+ // the thunder roar plays alongside the lightning-bolt BOLT.ANM.
if (!shouldQuit() && !_skipIntro) {
if (_audio)
_audio->playVoc(Common::Path("THUNDER.VOC"));
@@ -381,13 +350,9 @@ Common::Error EEMEngine::run() {
if (_audio)
_audio->stopVoice();
}
- // `_InitMysterySounds(0x3c)` at 2520:086a â load M60.SDX/SDB
- // so `_SpoolSound(uVar3 - 1)` between the ANIM01..ANIM20 anims
- // has data to draw from.
+ // _InitMysterySounds(0x3c) @ 2520:086a â load M60.SDX/SDB.
if (!shouldQuit() && !_skipIntro && _audio)
_audio->initMysterySounds(60);
- // Theme begins HERE â after the three silent logos, before
- // the character-intro reel.
if (!shouldQuit() && !_skipIntro && _music)
_music->playFile(Common::Path("THEME.XMI"), /*loop=*/true);
for (int i = 1; i <= 20 && !shouldQuit() && !_skipIntro; i++) {
@@ -397,21 +362,17 @@ Common::Error EEMEngine::run() {
Common::String name = Common::String::format("ANIM%02d.A", i);
playAnm(Common::Path(name), 120,
/*holdLastFrame=*/false, fadeIn);
- // `_SpoolSound(uVar3 - 1)` at 2520:08c2 â per-character
- // VO after each anim except the last (`if (uVar3 != 0x14)`
- // at 2520:08a8). Original blocks until done; we run async
- // and wait so the next anim doesn't start prematurely.
+ // _SpoolSound(uVar3 - 1) @ 2520:08c2 â per-anim VO, skipped
+ // when uVar3 == 0x14 @ 2520:08a8.
if (!shouldQuit() && !_skipIntro && i != 20 && _audio) {
_audio->spoolSound((uint)(i - 1));
_audio->waitForSpoolDone();
}
}
- // `_CleanMysterySounds` at 2520:0903 â release M60 before the
- // title.
+ // _CleanMysterySounds @ 2520:0903.
if (_audio)
_audio->cleanMysterySounds();
- // Restart the theme for TITLE.ANM â matches the second
- // `_MIDIPlayFile("theme.xmi")` call at 2520:0918.
+ // _MIDIPlayFile("theme.xmi") @ 2520:0918.
if (!shouldQuit() && !_skipIntro && _music)
_music->playFile(Common::Path("THEME.XMI"), /*loop=*/true);
if (!shouldQuit() && !_skipIntro)
@@ -426,24 +387,13 @@ Common::Error EEMEngine::run() {
goto screen_loop;
}
- // After the title chain, the original goes Title (B) -> screen 8
- // (NewPlayer / saved-record selection) -> screen 9 (ChoosePartner) ->
- // screen C (ActionScreen). Choosing a mystery there enters screen A
- // (CaseSelection) and then the site loop.
- // Mouse stays hidden through the opening anims; show it now for
- // the interactive screens (matches `_MouseCursor = 1` at the tail
- // of `_NewPlayer`).
+ // Title(B) -> screen 8 (profile) -> 9 (partner) -> C (action) ->
+ // A (case selection) -> site loop.
CursorMan.showMouse(true);
- // Stop the title music â the original `_NewPlayer / _DoChoosePartner`
- // screens have no music until the briefing's `_PlayInSequence` /
- // per-mystery `_StartTravelMusic` kicks in.
if (_music)
_music->stop();
- // Profile pick (or fresh creation) â `screen8_handler @ 1c33:1012`.
- // `doProfilePicker` lists existing profiles via `listProfiles()`
- // and falls through to `doNewPlayer` if none exist or the user
- // picks "[New Player]".
+ // screen8_handler @ 1c33:1012.
if (!shouldQuit())
doProfilePicker();
if (!shouldQuit())
@@ -451,31 +401,16 @@ Common::Error EEMEngine::run() {
if (!shouldQuit())
doChoosePartner();
- // Now drop into the screen-driver state machine â same pattern as
- // `_ScreenDriver @ 1a35:0dc1` + the per-screen handlers in the
- // table at 1a35:0e5e. The original sets `_NextScreen` either
- // directly (e.g. `_DisplayCorrect` writes 12 = ACTION) or via the
- // jumptable handlers (e.g. handler 0 calls `_DoInitClues` then
- // writes 1 = MAP). The handlers here mirror that exactly: each
- // case body runs the screen and updates `_nextScreen` for the next
- // iteration. Sentinel `kScreenInvalid` (0xFFFF) ends the loop â
- // same as the original's table-end marker.
- //
- // `_DoChoosePartner @ 1a35:099d` sets `_NextScreen = 0xc` (= the
- // original `_ActionScreen` â "Choose A Mystery / Practice Mystery /
- // See ScrapBook 1..3"). That screen is separate from handler 10's
- // `_DoChooseMystery` / `_CaseSelection`, which is where the "Book N"
- // title is drawn.
- //
- // Mid-mystery profile resume: if the profile picker loaded a
- // save whose `hasMystery` flag was set, `_mystery.isLoaded()` is
- // true here and the player just re-picked their partner. Drop
- // straight to MAP rather than the action menu so they don't have
- // to walk back through the case picker (which would
- // `_mystery.load()` fresh and discard their site / clue
- // progress). The original has no equivalent â it persists only
- // profile-level state via `_PlayerRecord`, not in-progress
- // mysteries â so this is a ScummVM-only ergonomics improvement.
+ // Drop into the screen-driver state machine â same pattern as
+ // `_ScreenDriver @ 1a35:0dc1` + the per-screen handler table at
+ // 1a35:0e5e. Sentinel `kScreenInvalid` (0xFFFF) ends the loop.
+ // `_DoChoosePartner @ 1a35:099d` writes `_NextScreen = 0xc` (the
+ // original `_ActionScreen`, which is separate from handler 10's
+ // `_DoChooseMystery` / `_CaseSelection`).
+ // Mid-mystery resume: if the loaded save had `hasMystery` set,
+ // drop straight to MAP rather than the action menu so the player
+ // doesn't walk back through the case picker (which would
+ // `_mystery.load()` fresh and discard site / clue progress).
if (!shouldQuit() && !resumed)
_nextScreen = _mystery.isLoaded() ? kScreenMap : kScreenAction;
screen_loop:
@@ -485,11 +420,9 @@ screen_loop:
switch (current) {
case kScreenTitle:
- // Floppy handler 0xb (`_HandleScreen11_Title_Floppy`) calls
- // `_DoTitle_Floppy`, whose `_PlayTitleANM_Floppy(1)` file
- // table entry is `TITLE.ANM`. The opening driver stops after
- // `MOVIE.ANM`; this live screen owns the title wait and then
- // writes `_NextScreen = 8` for the profile picker.
+ // Floppy handler 0xb _HandleScreen11_Title_Floppy ->
+ // _DoTitle_Floppy -> _PlayTitleANM_Floppy(1)=TITLE.ANM.
+ // Writes _NextScreen=8 (profile picker).
_nextScreen = kScreenProfile;
if (isFloppy()) {
CursorMan.showMouse(false);
@@ -503,8 +436,8 @@ screen_loop:
case kScreenAction:
// Top-level post-profile / post-mystery menu. `_ActionScreen
// @ 1c33:195b` shows the 5-entry "Choose A Mystery /
- // Practice / ScrapBook" picker and writes screen 0xa only
- // when the player picks "Choose A Mystery".
+ // Practice / ScrapBook 1..3" picker; writes _NextScreen=0xa
+ // only when the player picks "Choose A Mystery".
_nextScreen = kScreenInvalid;
doActionScreen();
if (_nextScreen == kScreenInvalid && _mystery.isLoaded())
@@ -512,9 +445,8 @@ screen_loop:
break;
case kScreenChooseMystery:
- // Handler 10 at 1a35:0e0e calls `_DoChooseMystery` which
- // presets `_NextScreen = 0` (INIT_CLUES) before
- // `_CaseSelection`.
+ // Handler 10 @ 1a35:0e0e -> _DoChooseMystery (presets
+ // _NextScreen=0 INIT_CLUES) -> _CaseSelection.
_nextScreen = kScreenInvalid;
doCaseSelection();
if (_nextScreen == kScreenInvalid && _mystery.isLoaded())
@@ -522,8 +454,8 @@ screen_loop:
break;
case kScreenInitClues:
- // Handler 0 at 1a35:0e14 runs `_PreLoad` + `_DoInitClues`
- // then writes `_NextScreen = 1` (MAP).
+ // Handler 0 @ 1a35:0e14 -> _PreLoad + _DoInitClues, writes
+ // _NextScreen=1 (MAP).
doInitClues();
_nextScreen = _mystery.isLoaded() ? kScreenMap
: kScreenAction;
@@ -531,13 +463,11 @@ screen_loop:
case kScreenMap:
case kScreenMapAlt:
- // Handler 1/2 at 1a35:0e25 calls `_DoMapScreen @
- // 20fe:120b` (floppy: 19bb:0ef3 -> 1fed map code),
- // which manages its own `_NextScreen` writes â 3 (a site
- // was clicked), 6 (setup), or 0xffff (quit).
- // Our `doBigMap` keeps the original's "click site, then
- // enter the site loop" behaviour inline; once it returns
- // the natural next state is SITE.
+ // Handler 1/2 @ 1a35:0e25 -> `_DoMapScreen @ 20fe:120b`
+ // (floppy 19bb:0ef3 -> 1fed map code), which manages its
+ // own _NextScreen writes: 3 (site clicked), 6 (setup), or
+ // 0xffff (quit). After `doBigMap` returns the natural
+ // next state is SITE.
doBigMap();
if (!_mystery.isLoaded())
_nextScreen = kScreenAction;
@@ -546,21 +476,19 @@ screen_loop:
break;
case kScreenSite:
- // Handler 3 at 1a35:0e2c calls `_DoSiteLoop @
- // 168d:03f4` (floppy's equivalent dispatches through
- // 1652). Site writes `_NextScreen` for PDA / map rather
- // than entering those screens as nested modals.
+ // Handler 3 @ 1a35:0e2c -> `_DoSiteLoop @ 168d:03f4`
+ // (floppy dispatches through 1652). Site writes _NextScreen
+ // for PDA / map rather than entering those as nested modals.
doSiteLoop();
if (!_mystery.isLoaded())
_nextScreen = kScreenAction;
else if (_nextScreen == current)
- _nextScreen = kScreenInvalid; // user quit
+ _nextScreen = kScreenInvalid;
break;
case kScreenNotebook:
- // Handler 4 calls the PDA notebook screen. Its button
- // handler writes 2 (map), 3 (site), 5 (gallery), or 7
- // (accuse) and then returns to this dispatcher.
+ // Handler 4 â PDA notebook screen. Button handler writes
+ // 2 (map), 3 (site), 5 (gallery), or 7 (accuse).
doNotebook();
if (!_mystery.isLoaded() && _nextScreen != kScreenAction)
_nextScreen = kScreenAction;
@@ -569,9 +497,8 @@ screen_loop:
break;
case kScreenGallery:
- // Handler 5 calls the suspect gallery. Like the original,
- // ESC and the site button write 3, the map button writes
- // 2, and the PDA button writes 4.
+ // Handler 5 â suspect gallery. ESC and the site button
+ // write 3, the map button writes 2, the PDA button writes 4.
doGallery();
if (!_mystery.isLoaded() && _nextScreen != kScreenAction)
_nextScreen = kScreenAction;
@@ -580,24 +507,15 @@ screen_loop:
break;
case kScreenSetup:
- // Handler 6 at 1a35:0e48 calls `_DoSetup @ 1f78:044e`.
- // Reachable via the BigMap setup button which writes
- // `_NextScreen = 6` (verified at 20fe:0c33). The
- // original sets `_NextScreen = _LastScreen` on entry,
- // then the toggle UI returns when ESC / Back is hit;
- // `doSetup` sets `_nextScreen` itself.
+ // Handler 6 @ 1a35:0e48 -> _DoSetup @ 1f78:044e. Entered
+ // from BigMap setup button (_NextScreen=6 @ 20fe:0c33).
doSetup();
break;
case kScreenProfile:
- // Handler 8 is the player/profile picker. CD
- // `screen8_handler @ 1c33:1012` loads an existing player
- // record or runs `_NewPlayer`; floppy
- // `_HandleScreen8_NewPlayer_Floppy @ 19bb:0ec2` then
- // writes screen 9. Mirror that route inline: after the
- // profile is selected, choose a partner, then continue to
- // the selected profile's loaded case if ScummVM save state
- // had one, otherwise to case selection.
+ // Handler 8: CD screen8_handler @ 1c33:1012 ->
+ // _NewPlayer; floppy _HandleScreen8_NewPlayer_Floppy @
+ // 19bb:0ec2 writes screen 9.
_nextScreen = kScreenInvalid;
_mystery.clear();
doProfilePicker();
@@ -611,9 +529,8 @@ screen_loop:
break;
case kScreenAccuse:
- // Handler 7 runs the accusation flow. A failed accusation
- // returns to `_LastScreen`; a correct solution writes
- // 0xc (ACTION).
+ // Handler 7 â accusation flow. Failed accusation returns to
+ // `_LastScreen`; a correct solution writes 0xc (ACTION).
doAccuse();
if (!_mystery.isLoaded() && _nextScreen != kScreenAction)
_nextScreen = kScreenAction;
@@ -646,7 +563,7 @@ void EEMEngine::setHotspotMouseCursor(bool active) {
}
bool EEMEngine::openArchives() {
- // _InitGraphicsSystem @ 172b:0145 opens these five .DBD/.DBX pairs.
+ // _InitGraphicsSystem @ 172b:0145.
if (!_picsArchive.open(Common::Path("PICS.DBD"), Common::Path("PICS.DBX"))) {
warning("PICS archive missing");
return false;
@@ -655,8 +572,6 @@ bool EEMEngine::openArchives() {
warning("ANI archive missing");
return false;
}
- // SITES + BALLOON are optional for the boot path but needed for site
- // rendering and clue display.
if (!_sitesArchive.open(Common::Path("SITES.DBD"), Common::Path("SITES.DBX")))
warning("SITES archive missing â site backgrounds disabled");
if (!_balloonArchive.open(Common::Path("BALLOON.DBD"), Common::Path("BALLOON.DBX")))
@@ -689,8 +604,8 @@ bool EEMEngine::loadSitePalettes() {
bool EEMEngine::getSitePalette(uint num, byte *out) const {
if (num >= kNumSitePals || _sitePals.size() < (num + 1) * kPalSize)
return false;
- // SITEPALS stores 6-bit VGA-DAC values (0..63); ScummVM expects 8-bit
- // (0..255), so left-shift by 2 like the original VGA hardware did.
+ // SITEPALS stores 6-bit VGA-DAC values (0..63); ScummVM expects
+ // 8-bit (0..255), so left-shift by 2 like the original VGA hardware.
const byte *src = _sitePals.data() + num * kPalSize;
for (uint i = 0; i < kPalSize; i++)
out[i] = (byte)(src[i] << 2);
@@ -726,12 +641,12 @@ bool EEMEngine::setAnmPalette(const Common::Path &anmPath) {
void EEMEngine::interruptAudio(bool stopMusicToo) {
// Mirrors `_CleanMysterySounds @ 202f:05a5` + `_StopMIDI @
- // 20a2:0512` â the original calls both whenever the player aborts
- // the opening-anim chain or dismisses the title (`_DoOpeningAnims
- // @ 2520:082a` writes `_LoopMIDI = 0; _StopMIDI();` after the
- // title-input loop). Conversation / clue-dialog skip paths pass
- // `stopMusicToo = false` so the site / briefing MIDI keeps going
- // across an ESC â only the per-line voice + spool need to stop.
+ // 20a2:0512` â both fire when the player aborts the opening-anim
+ // chain or dismisses the title (`_DoOpeningAnims` writes
+ // `_LoopMIDI = 0; _StopMIDI();` after the title-input loop).
+ // Conversation / clue-dialog skip paths pass `stopMusicToo = false`
+ // so the site / briefing MIDI keeps going across an ESC â only the
+ // per-line voice + spool need to stop.
if (_audio) {
_audio->stopVoice();
_audio->stopSpool();
@@ -778,13 +693,8 @@ void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs,
}
g_system->updateScreen();
- // Drain events and let the user skip with click/key. The original
- // uses _CheckFrameRate / _kbhit; we use a simple fixed delay until
- // the frame-rate calibration logic from _GetSpeedRating is wired up.
- // ESC additionally sets `_skipIntro` so the opening-anim chain in
- // run() bails out of the whole sequence instead of advancing to
- // the next clip â and stops every active audio channel so the
- // theme music / voice spool don't bleed past the abort.
+ // Original uses _CheckFrameRate / _kbhit; fixed delay here.
+ // ESC sets _skipIntro and interrupts audio.
const uint32 frameStart = g_system->getMillis();
bool aborted = false;
while (g_system->getMillis() - frameStart < frameDelayMs && !aborted) {
@@ -805,11 +715,10 @@ void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs,
break;
}
}
- // Refresh ScummVM's cursor overlay every tick â without
- // this the cursor only redraws when the next frame is
- // blitted (every `frameDelayMs` ms, ~8 Hz at 120 ms),
- // which the user perceives as choppy / unresponsive
- // during long animations like SCRAPBK.ANI.
+ // Refresh cursor overlay every tick â otherwise
+ // the cursor only redraws when the next frame is blitted
+ // (~8 Hz at 120 ms), perceived as choppy during long
+ // animations like SCRAPBK.ANI.
g_system->updateScreen();
g_system->delayMillis(5);
}
@@ -818,9 +727,7 @@ void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs,
}
if (holdLastFrame && !shouldQuit() && !_skipIntro) {
- // Mirror the wait-loop at the end of `_DoOpeningAnims`:
- // while (!keyDataAvailable) ;
- // We accept either a click or a key.
+ // _DoOpeningAnims tail: while (!keyDataAvailable).
while (!shouldQuit()) {
Common::Event ev;
bool clicked = false;
@@ -845,10 +752,7 @@ void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs,
g_system->updateScreen();
g_system->delayMillis(20);
}
- // `_DoOpeningAnims @ 2520:0945` writes `_LoopMIDI = 0;
- // _StopMIDI();` once the title-input loop exits â so the
- // theme stops the moment the player dismisses the title,
- // regardless of whether they used ESC or clicked.
+ // _DoOpeningAnims @ 2520:0945: _LoopMIDI=0; _StopMIDI().
if (_music)
_music->stop();
}
@@ -865,22 +769,9 @@ void EEMEngine::blitAt(const Picture &pic, int x, int y) {
}
void EEMEngine::waitForInput(uint32 maxMs) {
- // ESC additionally raises `_skipIntro` so the opening-anim chain
- // can fast-forward past the rest of the sequence, and stops any
- // active audio so the theme / voice / spool don't bleed past
- // the abort. Mirrors the `_CleanMysterySounds` + `_StopMIDI`
- // pair around the title wait in `_DoOpeningAnims`.
- //
- // Only Return / KP-Enter / Space / Escape advance â letting any key
- // dismiss balloons makes typing-while-reading (or a stuck modifier)
- // blow past dialog the player hasn't finished reading.
- //
- // Drop the highlighted-cursor state any caller (site loop, gallery,
- // notebook hover-handler) may have left on. While a balloon /
- // intro is showing, click anywhere just dismisses â no
- // hover-interactive areas â so a "clickable" cursor over the
- // dialog is misleading. The caller's MOUSEMOVE handler will
- // re-enable the highlight after we return if appropriate.
+ // ESC: _skipIntro + interruptAudio (matches _CleanMysterySounds +
+ // _StopMIDI around _DoOpeningAnims title wait).
+ // Only Return/KP-Enter/Space/Escape advance.
setInteractiveMouseCursor(false);
const uint32 startMs = g_system->getMillis();
while (!shouldQuit() && (g_system->getMillis() - startMs < maxMs)) {
@@ -892,8 +783,6 @@ void EEMEngine::waitForInput(uint32 maxMs) {
return;
}
if (event.type == Common::EVENT_MOUSEMOVE) {
- // Defensive: keep the cursor non-interactive across
- // moves while the dialog is up.
setInteractiveMouseCursor(false);
continue;
}
@@ -916,18 +805,17 @@ void EEMEngine::waitForInput(uint32 maxMs) {
}
void EEMEngine::showEAKidsLogo() {
- // Mirrors `_ShowEAKids @ 2520:05f0`. The original:
+ // _ShowEAKids @ 2520:05f0:
// 1. GetPicture(0x54) + MemoryCopy to VGA + GetPalette(0x25).
// 2. FRAME_RATE = 25; for j in 0..1, for u in 0..0x36 (= 55):
// OpenColorCycle(0x01, 0x6e) // bg / outer ring shimmer
// OpenColorCycle(0x81, 0xee) // inner gradient shimmer
// every 8 ticks: OpenColorCycle(0x70, 0x80) // mid band
- // 3. After the 110-tick loop: 5 more cycles of 0x70..0x80.
- // 4. Wait 0x23 (= 35) more frames.
+ // 3. Tail: 5 more cycles of 0x70..0x80.
+ // 4. Wait 0x23 (=35) more frames.
// 5. _OpenFadeOut.
- // The cycling is what gives the EA Kids logo its characteristic
- // shifting glow â it's NOT a static logo. ESC / click skips the
- // remaining cycle.
+ // The cycling is what gives the EA Kids logo its shifting glow â
+ // it's NOT a static logo.
Picture pic;
if (!_picsArchive.getPicture(kPicEAKidsLogo, pic)) {
warning("EA Kids logo (%u) load failed", kPicEAKidsLogo);
@@ -937,12 +825,7 @@ void EEMEngine::showEAKidsLogo() {
setSitePalette(kPalEAKids);
g_system->updateScreen();
- // 25 fps â 40 ms / tick. Two outer iterations à 55 ticks each = 110
- // ticks of palette rotation. The first inner-loop iteration of each
- // outer pass rotates the in-memory palette once *before* applying
- // to VGA (original gates the wait + setmany on `show != 0`); we
- // just call the setter every tick â the visible cycle starts
- // immediately, which is the same end result.
+ // 25 fps -> 40 ms / tick.
const uint kFrameMs = 40;
int delayCount = 8;
bool aborted = false;
@@ -957,7 +840,6 @@ void EEMEngine::showEAKidsLogo() {
}
g_system->updateScreen();
- // Tick wait + skip detection.
const uint32 frameEnd = g_system->getMillis() + kFrameMs;
while (g_system->getMillis() < frameEnd && !aborted) {
Common::Event ev;
@@ -985,22 +867,19 @@ void EEMEngine::showEAKidsLogo() {
if (aborted)
return;
- // Tail: 5 more rotations of the mid band, then 35 idle frames.
for (uint i = 0; i < 5 && !shouldQuit(); i++)
cyclePaletteRangeReverse(0x70, 0x80);
g_system->updateScreen();
waitForInput(0x23 * kFrameMs);
// `_OpenFadeOut @ 2520:0093` â 16 linear steps from current palette
- // to black. Without this, the EA Kids logo cuts hard to the next
- // screen.
+ // to black.
fadeCurrentPaletteToBlack();
}
void EEMEngine::showHighScoreLogo() {
- // Mirrors `_ShowHScoreLogo @ 2520:0799`:
- // GetPicture(0x20c) + MemoryCopy to VGA + GetPalette(0x27) +
- // _OpenFadeIn + 50-tick wait at 25 fps + _OpenFadeOut.
+ // _ShowHScoreLogo @ 2520:0799: PIC 0x20c + palette 0x27;
+ // _OpenFadeIn; 50-tick wait @ 25 fps; _OpenFadeOut.
Picture pic;
if (!_picsArchive.getPicture(kPicHighScoreLogo, pic)) {
warning("HighScore logo (%u) load failed", kPicHighScoreLogo);
@@ -1008,9 +887,7 @@ void EEMEngine::showHighScoreLogo() {
}
blitAt(pic, 0, 0);
- // Load target palette into a buffer, force a black palette, then
- // fade in â without the explicit black step we'd flash the full
- // logo briefly between blit and fade.
+ // Force black before fade-in to avoid a 1-frame full-logo flash.
byte target[kPalSize];
if (!getSitePalette(kPalHighScore, target)) {
warning("HighScore palette (%u) load failed", kPalHighScore);
@@ -1021,7 +898,7 @@ void EEMEngine::showHighScoreLogo() {
g_system->updateScreen();
fadePaletteFromBlack(target);
- // 50 ticks at 25 fps = ~2 s.
+ // 50 ticks @ 25 fps.
waitForInput(2000);
fadeCurrentPaletteToBlack();
@@ -1032,9 +909,8 @@ void EEMEngine::showFloppyStormLogo() {
// GetPicture(0x20b); BlitToVGA;
// if (sound) { LoadVOC(slot 25 = "thunder.voc"); PlayVOC(...); }
// GetPalette(0x26); FadeIn; wait 50 ticks; FadeOut.
- // CD plays `BOLT.ANM` at this slot with `THUNDER.VOC` overlaid; the
- // floppy uses a static still + the same VOC. Without the VOC the
- // lightning logo plays silently â the user noticed.
+ // CD plays `BOLT.ANM` here with `THUNDER.VOC` overlaid; floppy uses
+ // a static still + the same VOC.
Picture pic;
if (!_picsArchive.getPicture(kPicStormLogo, pic)) {
warning("Storm logo (%u) load failed", kPicStormLogo);
@@ -1063,34 +939,27 @@ void EEMEngine::showFloppyStormLogo() {
}
void EEMEngine::doSiteLoop() {
- // Mirrors the per-mystery site loop. SiteScreen::run() handles
- // hotspot clicks plus M (map), N (notebook), G (gallery), A (accuse),
- // Tab (next site), ESC (exit).
+ // Per-mystery site loop. SiteScreen::run() handles hotspot clicks
+ // plus M (map), N (notebook), G (gallery), A (accuse), Tab (next
+ // site), ESC (exit).
SiteScreen screen(this, &_mystery);
screen.run();
setHotspotMouseCursor(false);
}
void EEMEngine::startTravelMusic() {
- // Mirrors `_StartTravelMusic @ 20a2:0595`:
- //
+ // _StartTravelMusic @ 20a2:0595:
// for (num = _SiteNumber; num > 4; num -= 5) {}
// if (_MIDIAvailable && _MusicEnabled) {
// if (_IsMIDIPlaying()) _StopMIDI();
// _MIDIPlay(num);
// }
- //
- // Five travel tracks: MUS00000.XMI .. MUS00004.XMI, picked by
- // `_SiteNumber % 5`. ONE-SHOT â `_DoOpeningAnims @ 2520:0945`
- // resets `_LoopMIDI = 0` after the title-screen wait, and
- // `_StartTravelMusic` doesn't write to it; combined with
- // `_DoSiteLoop @ 168d:06c0` which waits for the track to play
- // out and then calls `_StopMIDI()` before the interactive phase
- // begins, the original effectively plays travel music ONCE
- // during the entrance animation only â the site investigation
- // itself runs without music. Our previous `loop=true` made the
- // music never end, leaving travel music droning through site
- // investigation, accuse, gallery, etc.
+ // Five travel tracks (MUS00000.XMI..MUS00004.XMI), picked by
+ // `_SiteNumber % 5`. ONE-SHOT â `_DoOpeningAnims @ 2520:0945` resets
+ // `_LoopMIDI = 0` after the title-screen wait, and the function
+ // doesn't write it; combined with `_DoSiteLoop @ 168d:06c0` calling
+ // `_StopMIDI()` before the interactive phase, travel music plays
+ // ONCE during the entrance animation only.
if (!_music || !_mystery.isLoaded() || !_voiceOn)
return;
const uint num = _mystery._siteNumber % 5;
@@ -1144,16 +1013,10 @@ bool EEMEngine::hasFeature(EngineFeature f) const {
}
bool EEMEngine::canLoadGameStateCurrently(Common::U32String *) {
- // Loading mid-mystery would replace `_mystery._data` while
- // pointers into it are alive on the stack inside `displayClue`
- // etc. Profile picking still works via `loadProfile` from the
- // menu screens before a mystery loads.
return !_mystery.isLoaded();
}
bool EEMEngine::canSaveGameStateCurrently(Common::U32String *) {
- // Profile saves (no mystery loaded) are always OK; mid-mystery
- // snapshots only after the active case has fully initialised.
return true;
}
@@ -1161,31 +1024,27 @@ Common::Error EEMEngine::saveGameStream(Common::WriteStream *stream,
bool isAutosave) {
(void)isAutosave;
- // Body header: one byte version. `Common::Serializer::setVersion`
- // alone doesn't write/read the version, so emit it explicitly and
- // require an exact match on load.
+ // Body header: explicit 1-byte version.
Common::Serializer s(nullptr, stream);
s.setVersion(kSaveBodyVer);
byte ver = kSaveBodyVer;
s.syncAsByte(ver);
- // Profile-level state â mirrors the original `_PlayerRecord` body
- // at `2d5d:3f6a` (159 bytes, written by `_SavePlayerRecord @
- // 1c33:034f`). The `_PlayerRecord` layout is:
+ // _PlayerRecord body @ 2d5d:3f6a (159 bytes, written by
+ // _SavePlayerRecord @ 1c33:034f). Layout:
// +0x00..+0x0b : player name (12 chars, null-padded)
- // +0x0c..+0x1f : random ID bytes used by `_GenerateFilename`
+ // +0x0c..+0x1f : random ID bytes for `_GenerateFilename`
// (29be:0dbf "C:\EEMCDSAV\%s.PLR") â irrelevant to
// ScummVM saves which key on slot, not filename.
// +0x20..+0x28 : derived 8-char .PLR basename â likewise unused.
- // +0x2d : voice-enable flag (`DAT_2d5d_3f97`, default 1).
- // +0x2f : chain stage (`DAT_2d5d_3f99`, 1=A, 2=B, 3=C â
- // `_DisplayCorrect` advances it once every case
- // in the current set is solved).
+ // +0x2d : voice-enable flag (`DAT_2d5d_3f97`, default 1)
+ // +0x2f : chain stage (`DAT_2d5d_3f99`, 1=A, 2=B, 3=C;
+ // `_DisplayCorrect` advances once every case in the
+ // current set is solved)
// +0x31..+0xa6 : `mysteriesSolved[55]` u16 (0=unsolved, 1=solved,
- // 2=solved on first try) â `_DisplayCorrect`
- // writes 1 always, 2 when `_FirstTry != 0`.
- //
- // We persist the gameplay-meaningful subset and skip the original
+ // 2=solved on first try â `_DisplayCorrect` writes
+ // 1 always, 2 when `_FirstTry != 0`)
+ // We persist the gameplay-meaningful subset and skip the original's
// filename-derivation bytes.
s.syncString(_playerName);
s.syncBytes(_mysteriesSolved, sizeof(_mysteriesSolved));
@@ -1193,11 +1052,8 @@ Common::Error EEMEngine::saveGameStream(Common::WriteStream *stream,
s.syncAsByte(_chainStage);
s.syncAsByte(_voiceOn);
- // ScummVM-only extension: persist the in-progress mystery so the
- // player can resume mid-case. The original engine has no such
- // notion â `_LoadGame @ 2404:0dc7` simply loads a fresh mystery,
- // it doesn't preserve site progress. The flag lets a profile save
- // stay valid even when no mystery is loaded (e.g. fresh profile).
+ // Mid-case resume: persist in-progress mystery (no equivalent in
+ // _LoadGame @ 2404:0dc7).
bool hasMystery = _mystery.isLoaded();
s.syncAsByte(hasMystery);
if (hasMystery) {
@@ -1246,13 +1102,13 @@ Common::Error EEMEngine::loadGameStream(Common::SeekableReadStream *stream) {
resetSiteArrivalState();
return Common::kReadingFailed;
}
- // `_ReadMystery @ 2404:008f` calls `_InitMysterySounds` at the
- // tail (2404:0298) so the SDB index is in place for clue and
+ // `_ReadMystery @ 2404:008f` tail-calls `_InitMysterySounds`
+ // (2404:0298) so the SDB index is in place for clue and
// partner-speech spool sounds. Floppy ships individual
- // `M-XXXX.VOC` files instead of the bundled SDB / SDX archive,
- // so skip the init there to avoid spamming "missing" warnings;
- // `spoolSound` then silently no-ops via the `_currentMystery <
- // 0` guard until the per-voice VOC mapping is wired up.
+ // `M-XXXX.VOC` files instead of the bundled SDB/SDX archive,
+ // so we skip the init there to avoid "missing" warnings;
+ // `spoolSound` then no-ops via its `_currentMystery < 0` guard
+ // until the per-voice VOC mapping is wired up.
if (_audio && !isFloppy())
_audio->initMysterySounds(mysteryNum);
_mystery.syncState(s);
@@ -1273,13 +1129,9 @@ Common::Error EEMEngine::loadGameStream(Common::SeekableReadStream *stream) {
}
SaveStateList EEMEngine::listProfiles() const {
- // Mirrors `_findfirst("*.PLR")` in `screen8_handler @ 1c33:1012`.
- // We disable autosave (`getAutosaveSlot()` returns -1) but a
- // pre-existing slot-0 file from a previous run, or one written by
- // another engine using the same target, would still show up here
- // and pollute the picker. Filter it out so the picker treats slot
- // 0 as if it didn't exist â matching the original which never
- // has an autosave concept.
+ // _findfirst("*.PLR") in screen8_handler @ 1c33:1012.
+ // Filter out slot 0 (ScummVM autosave) to match the original which
+ // has no autosave concept.
SaveStateList saves = getMetaEngine()->listSaves(_targetName.c_str());
for (uint i = 0; i < saves.size(); ) {
if (saves[i].getSaveSlot() == 0)
@@ -1296,8 +1148,7 @@ Common::Error EEMEngine::saveProfile(const Common::String &name) {
const SaveStateList saves = listProfiles();
- // Slot lookup by description: if a save with this profile name
- // already exists, overwrite it. Same as Wetlands' `saveProfile`.
+ // Overwrite on matching description.
int slot = -1;
for (auto &s : saves) {
if (s.getDescription() == name) {
@@ -1306,13 +1157,8 @@ Common::Error EEMEngine::saveProfile(const Common::String &name) {
}
}
- // New profile â pick the lowest unused visible slot. Slot 0 is
- // filtered out by `listProfiles()` because it is ScummVM's
- // conventional autosave slot, so allocating a new profile there
- // makes the profile disappear from the picker on the next refresh.
- // The MetaEngine caps us at 99 by default (`getMaximumSaveSlot`);
- // 25 was the DOS original's limit (`screen8_handler` walks `*.PLR`
- // up to 25 entries in `local_8c[0x19][2]`).
+ // New profile: pick lowest unused slot >=1 (slot 0 is autosave;
+ // DOS limit was 25 per screen8_handler local_8c[0x19][2]).
if (slot < 0) {
const int maxSlot = getMetaEngine()->getMaximumSaveSlot();
Common::Array<bool> used(maxSlot + 1);
@@ -1359,8 +1205,6 @@ bool EEMEngine::loadProfile(const Common::String &name) {
}
void EEMEngine::screenDriver() {
- // Placeholder for the eventual dispatch table (one entry per ScreenId).
- // run() currently calls handlers directly until the title path lands.
}
} // End of namespace EEM
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 26cb2c8f00d..2d3973a4d97 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -46,45 +46,36 @@ namespace EEM {
class AudioPlayer;
class MusicPlayer;
-/**
- * Screen IDs used by the original `_ScreenDriver` dispatch table at
- * 1a35:0e5e (and the fallback at 1a35:0e54). 14 entries total: each
- * one is a screen ID + a near function pointer at offset +0x1c. The
- * driver does `JMP word ptr CS:[BX + 0x1c]`, so handlers tail-call â
- * each one runs the screen and updates `_NextScreen` for the next
- * trip through the dispatcher.
- *
- * IDs and their handlers (verified from disassembly at 1a35:0dec..0e4f):
- *
- * 0 INIT_CLUES â `_PreLoad` + `_DoInitClues`, sets _NextScreen=1
- * 1 MAP â `_DoMapScreen` @ 20fe:120b (sets _NextScreen
- * inside; 3 = a site was clicked, 6 = setup, etc.)
- * 2 MAP â same handler as 1 (alternate entry, used when
- * `_LastScreen == 2` to swap the briefcase anim)
- * 3 SITE â `_DoSiteLoop` @ 168d:03f4
- * 4 NOTEBOOK â `_DoNotebook` @ 161e:0500
- * 5 GALLERY â `_DoGallery` @ 158f:065b
- * 6 SETUP â `_DoSetup` @ 1f78:044e
- * 7 ACCUSE â `_DoAccuse` @ 1df2:0bdd (win â 12, lose â last)
- * 8 PROFILE â `screen8_handler` @ 1c33:1012; tail sets =9
- * 9 PARTNER â `_DoChoosePartner` @ 1a35:0756; sets =0xc inside
- * 10 (0xa) CHOOSE_MYSTERY â `_DoChooseMystery` + `_CaseSelection`;
- * starts with _NextScreen=0 so a successful pick
- * falls through to INIT_CLUES.
- * 11 (0xb) TITLE â floppy `_DoTitle_Floppy` plays TITLE.ANM, waits
- * for input, then writes =8. CD shows TITLE.ANM in
- * `_DoOpeningAnims`, so it usually never enters this
- * handler.
- * 12 (0xc) ACTION â `_ActionScreen` @ 1c33:195b â Choose A Mystery /
- * Practice Mystery / See ScrapBook 1..3. Action 1
- * sets =10 (CHOOSE_MYSTERY).
- * 0xFFFF SENTINEL â exit loop
- *
- * Screen-driver state writes verified via xrefs to `_NextScreen @
- * 2d5d:3f26`: `_DisplayCorrect` writes 0xc (winner returns to ACTION),
- * `_DisplayAlibi` writes `_LastScreen` (loser snaps back), and
- * `_DoSiteLoop` writes 1/3/4 plus 0xffff on ESC.
- */
+/// _ScreenDriver dispatch table @ 1a35:0e5e (fallback @ 1a35:0e54).
+/// 14 entries total: each is a screen ID + near fn ptr at +0x1c; driver
+/// tail-calls via `JMP word ptr CS:[BX + 0x1c]`. Handler bodies update
+/// _NextScreen before returning. Handlers @ 1a35:0dec..0e4f:
+///
+/// 0 INIT_CLUES â `_PreLoad` + `_DoInitClues`, writes _NextScreen=1
+/// 1 MAP â `_DoMapScreen @ 20fe:120b` (writes 3=site clicked,
+/// 6=setup, 0xffff=quit)
+/// 2 MAP_ALT â same handler as 1 (used when `_LastScreen == 2` to
+/// swap the briefcase anim)
+/// 3 SITE â `_DoSiteLoop @ 168d:03f4`
+/// 4 NOTEBOOK â `_DoNotebook @ 161e:0500`
+/// 5 GALLERY â `_DoGallery @ 158f:065b`
+/// 6 SETUP â `_DoSetup @ 1f78:044e`
+/// 7 ACCUSE â `_DoAccuse @ 1df2:0bdd` (win â 0xc, lose â _LastScreen)
+/// 8 PROFILE â `screen8_handler @ 1c33:1012`; tail sets =9
+/// 9 PARTNER â `_DoChoosePartner @ 1a35:0756`; sets =0xc inside
+/// 0xa CHOOSE_MYSTERY â `_DoChooseMystery` + `_CaseSelection`; presets
+/// _NextScreen=0 so a successful pick falls through to
+/// INIT_CLUES
+/// 0xb TITLE â floppy `_DoTitle_Floppy` plays TITLE.ANM, writes =8.
+/// CD shows TITLE.ANM in `_DoOpeningAnims`, so this
+/// handler is usually unused there.
+/// 0xc ACTION â `_ActionScreen @ 1c33:195b` â Choose A Mystery /
+/// Practice / See ScrapBook 1..3. Action 1 sets =0xa.
+/// 0xFFFF SENTINEL â exit loop
+///
+/// State writes (via xrefs to `_NextScreen @ 2d5d:3f26`): `_DisplayCorrect`
+/// writes 0xc, `_DisplayAlibi` writes `_LastScreen`, `_DoSiteLoop` writes
+/// 1/3/4 plus 0xffff on ESC.
enum ScreenId {
kScreenInvalid = 0xFFFF,
kScreenInitClues = 0x00,
@@ -102,13 +93,12 @@ enum ScreenId {
kScreenAction = 0x0C
};
-/// Distribution variant. Selected at engine init from the
-/// `ADGameDescription::extra` field set by `gameDescriptions[]` in
-/// `detection.cpp`. Used to gate filename selection (TRAVEL-N.XMI
-/// vs MUS%05u.XMI, FANFARE2.XMI vs MUS00005.XMI, PHONESL.VOC vs
-/// PHONE.VOC), opening-anim flow (MOVIE.ANM vs ANIM01..20.A) and
-/// per-variant sound effects (DING.VOC / NEWSCAN.VOC only ship with
-/// the floppy release).
+/// Distribution variant from `ADGameDescription::extra` (set by
+/// `gameDescriptions[]` in `detection.cpp`). Gates filename selection
+/// (TRAVEL-N.XMI vs MUS%05u.XMI, FANFARE2.XMI vs MUS00005.XMI,
+/// PHONESL.VOC vs PHONE.VOC), opening-anim flow (MOVIE.ANM vs
+/// ANIM01..20.A), and per-variant SFX (DING.VOC / NEWSCAN.VOC ship only
+/// with floppy).
enum Variant {
kVariantCD = 0,
kVariantFloppy = 1,
@@ -130,53 +120,35 @@ public:
bool canLoadGameStateCurrently(Common::U32String *msg = nullptr) override;
bool canSaveGameStateCurrently(Common::U32String *msg = nullptr) override;
- // Disable ScummVM's periodic autosave. The original engine's
- // `screen8_handler @ 1c33:1012` builds the profile picker by
- // walking every `*.PLR` file in the save dir, and we mirror that
- // via `listProfiles() â MetaEngine::listSaves`. Letting the
- // framework write a slot-0 autosave creates a phantom profile
- // that shows up on the picker as a real save. Returning -1 tells
- // the framework to skip autosave entirely.
+ // Autosave disabled: profile picker (screen8_handler @ 1c33:1012)
+ // lists `*.PLR` and a slot-0 autosave would appear as a profile.
int getAutosaveSlot() const override { return -1; }
- // ScummVM extended-save hooks. The base `Engine::saveGameState` /
- // `loadGameState` write/read the framework header (description,
- // thumbnail, playtime, version) around our body via these
- // streams. We keep all per-profile state in the body and only
- // accept the current private body layout.
Common::Error saveGameStream(Common::WriteStream *stream,
bool isAutosave = false) override;
Common::Error loadGameStream(Common::SeekableReadStream *stream) override;
- // Per-profile save helpers. The original `_PlayerRecord` lives at
- // `2d5d:3f6a` (159 bytes) and is written by `_SavePlayerRecord @
- // 1c33:034f` to `C:\EEMCDSAV\<name>.PLR`. The DOS launcher screen
- // `screen8_handler @ 1c33:1012` walks `*.PLR`, lets the player
- // pick a profile, and calls `_LoadPlayerRecord`. We mirror the
- // pattern by mapping each ScummVM save slot to one profile (slot
- // description = player name) â same approach Wetlands uses.
-
- /// Mirrors `_SavePlayerRecord @ 1c33:034f`. Saves into the slot
- /// whose description matches @p name, or the lowest unused visible
- /// slot if no match. Returns the kNoError on success.
+ // Per-profile saves: `_PlayerRecord` @ 2d5d:3f6a (159 bytes), written
+ // by `_SavePlayerRecord @ 1c33:034f` to `C:\EEMCDSAV\<name>.PLR`.
+ // Each save slot maps to one profile (slot description = the
+ // player name) â same approach Wetlands uses. `screen8_handler @
+ // 1c33:1012` walks `*.PLR`, lets the player pick a profile, and calls
+ // `_LoadPlayerRecord`.
+
+ /// `_SavePlayerRecord @ 1c33:034f`.
Common::Error saveProfile(const Common::String &name);
- /// Mirrors `_LoadPlayerRecord @ 1c33:03a6`. Returns false if no
- /// slot has @p name as its description.
+ /// `_LoadPlayerRecord @ 1c33:03a6`.
bool loadProfile(const Common::String &name);
- /// Mirrors the `_findfirst("*.PLR")` walk inside
- /// `screen8_handler`. Sorted by slot.
+ /// `_findfirst("*.PLR")` walk inside `screen8_handler`.
SaveStateList listProfiles() const;
const ADGameDescription *_gameDescription;
Variant _variant = kVariantCD;
Common::Language _language = Common::EN_ANY;
- /// True for the Spanish floppy release. Used to swap hardcoded
- /// English UI strings (menus, name prompt, notebook labels,
- /// fallback hint copy) for their Spanish equivalents extracted
- /// from EEM.EXE in `eem-full-game/floppy-es/`.
+ /// Spanish floppy release.
bool isSpanish() const { return _language == Common::ES_ESP; }
DBDArchive &getPics() { return _picsArchive; }
@@ -188,156 +160,113 @@ public:
const EEMFont &getFont() const { return _font; }
uint8 getPartnerIndex() const { return _partner; }
- /// Switch to the red-outline cursor used to hint at interactive
- /// regions that do not have their own original highlight art.
+ /// Red-outline cursor for interactive regions without original highlight art.
void setInteractiveMouseCursor(bool active);
- /// Switch to the interactive cursor while the mouse is over a
- /// searchable hotspot.
+ /// Interactive cursor over searchable hotspots.
void setHotspotMouseCursor(bool active);
- /// Display one ClueBlock. @p clueBlock points at the u16 frame count
- /// followed by 62-byte ClueEntries. Mirrors _DisplayClue @ 2404:05e6.
+ /// `_DisplayClue @ 2404:05e6`. @p clueBlock points at the u16 frame
+ /// count followed by 62-byte ClueEntries.
void displayClue(const byte *clueBlock);
- /// Floppy hotspot click â locate the dialog records for the
- /// clicked hotspot in the site's per-hotspot dialog list at
- /// `site_data[+6]` and dispatch them through
- /// `displayFloppyDialogRecords`. Mirrors `FUN_22dc_0b80 +
- /// FUN_1652_00e6 + FUN_1652_006c`.
+ /// Floppy hotspot click. `FUN_22dc_0b80 + FUN_1652_00e6 + FUN_1652_006c`.
+ /// Locates dialog records in site_data[+6] and dispatches them.
void displayFloppyHotspotDialog(uint siteNum, uint hotIdx);
- /// Active player name (saved as the profile-save description).
+ /// Active player name (= profile-save description).
const Common::String &playerName() const { return _playerName; }
- /// Apply a single ClueEntry's side effects â notebook adds, gallery
- /// updates, site flags. Called both by `displayClue` after a normal
- /// click-through and when the player ESC-skips a multi-entry clue.
+ /// Apply a ClueEntry's side effects (notebook, gallery, site flags).
void applyClueSideEffects(const byte *entry);
- /// Show clue/notebook screen. Mirrors `_DrawNotes` @ 161e:01d0.
+ /// `_DrawNotes @ 161e:01d0`.
void doNotebook();
- /// Show suspect gallery. Mirrors `_DrawGallery` @ 158f:0046.
+ /// `_DrawGallery @ 158f:0046`.
void doGallery();
- /// Suspect-detail view inside the gallery. Mirrors
- /// `MoreInfo @ 158f:0419`: paints PIC 0x3f + suspect picture at
- /// (0x94, 0x0f), paginates the suspect's found clues inside the
- /// `_GalleryNoteRect`, and dispatches PDA bottom-bar buttons via
- /// `_HandleMoreButton @ 158f:027d`. Sets `_nextScreen` and
- /// returns true when the user picks a button that should exit
- /// `doGallery` outright (NOTEBOOK / ACCUSE / MAP); returns false
- /// for plain dismissal (ESC / GALLERY button) so the caller stays
- /// on the portrait grid.
+ /// `MoreInfo @ 158f:0419`. Suspect-detail view inside the gallery; button
+ /// dispatch via `_HandleMoreButton @ 158f:027d`. Returns true if the
+ /// caller should exit `doGallery` (NOTEBOOK / ACCUSE / MAP).
bool moreInfo(const byte *gd, uint suspectIdx,
const Picture &galBg, bool haveBg);
- /// Show big map; click chooses next site. Mirrors `_DoBigMap` @ 20fe:09e7.
+ /// `_DoBigMap @ 20fe:09e7`.
void doBigMap();
- /// Run the accuse flow (pick suspect, evaluate chains, show ending).
- /// Mirrors `_DoAccuseGallery` @ 1df2:0a31 + `_DisplayEnding` @ 1df2:0548.
+ /// Accuse flow. `_DoAccuseGallery @ 1df2:0a31` + `_DisplayEnding @ 1df2:0548`.
void doAccuse();
void doAccuseFloppy();
- /// Show the accuse-notes screen (PIC 0x1A7, the red "accuse-mode"
- /// BG with selectable clue list + "N clues" remaining counter).
- /// Mirrors the outer loop of `_DoAccuse @ 1df2:0bdd`. Returns
- /// true if the player committed (selected the chain-required
- /// number of clues and clicked SOLVE), false if they exited via
- /// ESC / back. Called from `doAccuse` before the evidence gate.
+ /// Accuse-notes screen (PIC 0x1A7). Outer loop of `_DoAccuse @ 1df2:0bdd`.
+ /// Returns true if the player committed (SOLVE clicked), false on ESC.
bool doAccuseNotes();
- /// Show a host hint from `KDTextIndex`. Mirrors `_KDHelp` @ 1560:010a +
- /// `_DisplayHint` @ 1560:0009. Cycles between the two hint slots that
- /// the original engine tracks via `_SawHelpHint`.
+ /// `_KDHelp @ 1560:010a` + `_DisplayHint @ 1560:0009`. Cycles two
+ /// hint slots tracked via `_SawHelpHint`.
void doHelp();
- /// Display the interface-help picture sequence. Mirrors `_InterfaceHelp`
- /// @ 1560:0205 â walks `HelpData @ 29be:00c8`, blits each pic fullscreen,
- /// and waits for click / key (ESC ends the cycle).
+ /// `_InterfaceHelp @ 1560:0205`. Walks `HelpData @ 29be:00c8`.
void doInterfaceHelp(uint num = 0);
- /// First-char-dispatch balloon picker. Mirrors `_GetKDTextBalloon @
- /// 1df2:0105`. For digit first chars (0..9) returns balloon from the
- /// table at `29be:1064`; for any other char returns the constant
- /// `*(u16*)29be:1068 = 0x17`. The original branch is on Borland's
- /// ctype-bit-1 (= digit) at `29be:2be1 + char`.
+ /// `_GetKDTextBalloon @ 1df2:0105`. Digits (0..9) â table @ 29be:1064;
+ /// otherwise `*(u16*)29be:1068 = 0x17`.
uint16 getKDTextBalloon(byte firstChar) const;
- /// Cleanup for overly tall original speech balloons. Keeps the original
- /// bubble family and mirror bit, but picks a shorter sibling when the
- /// wrapped text leaves enough empty lines.
+ /// Pick a shorter balloon sibling when wrapped text leaves empty lines.
uint16 fitBalloonToText(uint16 bubNum, const Common::String &text);
- /// Substitute the 0x80..0x89 control bytes the engine uses inside
- /// `TextBlock` strings. Mirrors `_ParseString @ 1b66:07c3`; jump
- /// table at 1b66:0cbe. Used by every clue / hint / balloon caller.
+ /// `_ParseString @ 1b66:07c3`. Substitutes 0x80..0x89 control bytes;
+ /// jump table @ 1b66:0cbe.
Common::String parseString(const Common::String &raw,
const Common::String &playerName,
uint partner) const;
- /// Play the partner's one-shot reaction animation slot @num. Mirrors
- /// `_DoKDAnim @ 168d:028a` + `_PlayAnimation @ 172b:1f46`. The
- /// per-partner (animId, x, y) come from `_WaitAnims[1+num] @ 29be:0228`,
- /// and the per-frame timing follows the sequence script that the
- /// original would index at `_AnimationSequences[seqnum=animId]`. Used
- /// by `displayClue` when a ClueEntry's KD-anim field (+0x3a) is set â
- /// e.g. Jenny's "take a picture" gesture when the player searches an
- /// NPC. Blocks until the script's first 0x80 marker.
+ /// `_DoKDAnim @ 168d:028a` + `_PlayAnimation @ 172b:1f46`. Per-partner
+ /// (animId, x, y) from `_WaitAnims[1+num] @ 29be:0228`. Blocks until
+ /// the script's first 0x80 marker.
void playKdAnim(uint16 num);
- /// Provide a "clean" 320x200 backdrop for the next `playKdAnim` (and
- /// any future blocking partner-anim playback) to use as the
+ /// Provide a "clean" 320x200 backdrop (site BG + static drops, no
+ /// NPCs / partner) for the next `playKdAnim` to use as the
/// background-erase source. Without this, the camera animation would
- /// composite on top of the static partner sprite from the screen and
- /// the previous resting frame would bleed through transparent pixels.
- /// `SiteScreen` calls this with its `_bgSnapshot` (site BG + static
- /// drops, no NPCs / partner) before invoking `displayClue` from a
- /// hotspot click. Pass `nullptr` to clear.
+ /// composite on top of the static partner sprite and the previous
+ /// resting frame would bleed through transparent pixels. `SiteScreen`
+ /// passes its `_bgSnapshot` before `displayClue` from a hotspot click.
+ /// Pass `nullptr` to clear.
void setPartnerEraseBg(const Graphics::ManagedSurface *bg);
- /// Look up balloon-text-inset metadata. Mirrors the 52-entry table at
- /// `29be:0875` (CD) / `2608:05f9` (floppy), indexed by
- /// `(bubNum & 0x7F)`. 10 bytes per entry; the first 3 fields
- /// (x inset, y inset, text width) are used for text wrap, the last
- /// 2 (indicator dX/dY) by `drawFloppyBubbleIndicator`. Returns
- /// false if `bubNum` is outside the table.
+ /// Balloon-text-inset metadata. 52-entry table @ 29be:0875 (CD) /
+ /// 2608:05f9 (floppy), indexed by `(bubNum & 0x7F)`. 10 bytes per
+ /// entry: the first 3 fields (x inset, y inset, text width) are used
+ /// for text wrap; the last 2 (indicator dX/dY) by
+ /// `drawFloppyBubbleIndicator`. Returns false if `bubNum` is outside
+ /// the table.
bool getBalloonInsets(uint16 bubNum, uint16 &xInset, uint16 &yInset,
uint16 &textW) const;
bool getBalloonIndicatorPos(uint16 bubNum, uint16 &dx,
uint16 &dy) const;
- /// Stamp the floppy "click to continue" indicator (PIC 0xa0 for
- /// `endIndicator==false`, PIC 0xa1 otherwise) onto @p dst at the
- /// position derived from the balloon-inset table. Mirrors
- /// `FUN_22dc_05c8 @ 22dc:08aa` (mid-page) and `@ 22dc:08c0`
- /// (end-of-record).
+ /// `FUN_22dc_05c8 @ 22dc:08aa` (mid-page) / `@ 22dc:08c0` (end).
+ /// PIC 0xa0 if !endIndicator else PIC 0xa1.
void drawFloppyBubbleIndicator(Graphics::ManagedSurface &dst,
uint16 bubNum, int ballX, int ballY,
bool endIndicator);
- /// "Are you sure?" yes/no dialog. Mirrors `_AreYouSure` @ 1a35:0a5c.
- /// Returns true if the user picked YES.
+ /// `_AreYouSure @ 1a35:0a5c`. Returns true on YES.
bool areYouSure();
private:
void applyStartupTestOverrides();
- /**
- * Central dispatch loop matching the original _ScreenDriver @ 1a35:0dc1.
- * Each iteration calls the screen handler that matches _nextScreen.
- * Handlers update _lastScreen / _nextScreen and return; the loop exits
- * when _nextScreen == kScreenInvalid.
- */
+ /// Central dispatch loop matching `_ScreenDriver @ 1a35:0dc1`. Each
+ /// iteration calls the handler that matches `_nextScreen`; handlers
+ /// update `_lastScreen` / `_nextScreen` and return. Loop exits when
+ /// `_nextScreen == kScreenInvalid`.
void screenDriver();
- /// Re-render helpers used by the corresponding `doX()` modal screens.
- /// Each replaces what would otherwise be a `[&]()` capture-everything
- /// lambda inside the `doX()` body; called from the screen's redraw
- /// triggers (input changes, frame ticks). The state these need is
- /// passed via reference parameters or read off engine members.
+ /// Re-render helpers for the corresponding `doX()` modal screens.
void drawNotebookFrame(int &page);
void drawGalleryFrame(const byte *gd, uint8 numSuspects,
Common::Array<Common::Rect> &slotRects,
@@ -352,34 +281,22 @@ private:
Common::Array<Common::Rect> &slotRects,
Common::Array<int> &slotSuspect);
- /**
- * Open the five .DBD/.DBX archive pairs the way _InitGraphicsSystem
- * @ 172b:0145 does at boot.
- */
+ /// Open the five .DBD/.DBX archive pairs. `_InitGraphicsSystem @ 172b:0145`.
bool openArchives();
- /** Slurp SITEPALS into @c _sitePals. Mirrors _ReadPalettes @ 172b:0d89. */
+ /// `_ReadPalettes @ 172b:0d89`. Slurps SITEPALS into `_sitePals`.
bool loadSitePalettes();
- /**
- * Upload palette index @p num (one of 40 stored in SITEPALS) to the
- * screen, with the VGA-DAC 6-bit-to-8-bit shift. Mirrors _GetPalette
- * @ 172b:0e80 followed by _setmany @ 1000:0930.
- */
+ /// `_GetPalette @ 172b:0e80` + `_setmany @ 1000:0930`. Uploads one of
+ /// 40 SITEPALS palettes with the VGA-DAC 6â8 bit shift.
void setSitePalette(uint num);
- /// Fill @p out (256 Ã 3 bytes) with the SITEPALS palette at index
- /// @p num, expanded from VGA's 6-bit DAC range to 8-bit. Returns
- /// false if the index is out of range. Used when callers need the
- /// palette for fade-in (set black first, then fade) without
- /// flashing the target on screen.
+ /// Fill @p out (256 Ã 3 bytes) with SITEPALS palette @p num, expanded
+ /// from 6-bit DAC to 8-bit. False if out of range.
bool getSitePalette(uint num, byte *out) const;
- /**
- * Upload a 6-bit VGA palette read from the head of an .ANM file (the
- * first 0x300 bytes per Load_Sequence @ 2503:0006). Used until the
- * full title-page animation chain is wired in.
- */
+ /// Upload the 6-bit VGA palette from the head of an .ANM file
+ /// (first 0x300 bytes per `Load_Sequence @ 2503:0006`).
bool setAnmPalette(const Common::Path &anmPath);
public:
@@ -387,256 +304,168 @@ public:
void setSitePaletteForSite(uint siteNum) { setSitePalette(siteNum + 1); }
private:
- /** Blit @p pic to @p x, @p y on screen. */
void blitAt(const Picture &pic, int x, int y);
- /** Hold the current frame for up to @p maxMs or until the user inputs. */
public:
void waitForInput(uint32 maxMs);
private:
- /**
- * Play a difference-encoded animation file (.ANM / .A) on the full
- * 320x200 screen. Mirrors the data flow of `OpenDifferenceAnimation`
- * @ 2520:0337 â `Load_Sequence` + `Play_Sequence`. Audio cues are
- * skipped for now. The default frame delay is 120 ms to match the
- * original FRAME_RATE = 0x78 used by `_DoOpeningAnims`.
- *
- * If @p holdLastFrame is true the call blocks on the final frame
- * until the user clicks or hits a key â used for the title screen.
- * If @p fadeIn is true the first decoded frame is copied while the
- * palette is black, then the animation palette is ramped in like
- * `_OpenFadeIn`.
- */
+ /// Play a difference-encoded animation file (.ANM / .A) on the full
+ /// 320x200 screen. Mirrors the data flow of `OpenDifferenceAnimation
+ /// @ 2520:0337` â `Load_Sequence` + `Play_Sequence`. Audio cues are
+ /// skipped for now. The default 120 ms frame delay matches the
+ /// original `FRAME_RATE = 0x78` used by `_DoOpeningAnims`.
+ /// If @p holdLastFrame is true the call blocks on the final frame
+ /// until the user clicks or hits a key â used for the title screen.
+ /// If @p fadeIn is true the first decoded frame is copied while the
+ /// palette is black, then ramped in like `_OpenFadeIn`.
void playAnm(const Common::Path &path, uint frameDelayMs = 120,
bool holdLastFrame = false, bool fadeIn = false);
- /// Stop every active audio channel â voice, sound spool, and
- /// MIDI. Mirrors the `_CleanMysterySounds @ 202f:05a5` +
- /// `_StopMIDI @ 20a2:0512` pair the original triggers when the
- /// player aborts the opening-anim chain or dismisses the title
- /// (`_DoOpeningAnims @ 2520:082a` writes `_LoopMIDI = 0;
- /// _StopMIDI();` after the title-input loop, and
- /// `_CleanMysterySounds` is called twice â once after the loop
- /// and once before TITLE.ANM). Called from every ESC handler in
- /// the intro / title chain so the theme music + voice spool
- /// don't bleed past the abort.
- /// Stop currently-playing voice / spooled SFX. Pass `stopMusic =
- /// true` (the default â matches `_CleanMysterySounds + _StopMIDI`)
- /// to also halt the MIDI track; conversation / dialog skip paths
- /// pass `false` so the site / briefing music keeps going across an
- /// ESC.
+ /// `_CleanMysterySounds @ 202f:05a5` + `_StopMIDI @ 20a2:0512`.
+ /// `stopMusicToo=false` keeps MIDI playing across dialog skips.
void interruptAudio(bool stopMusicToo = true);
- // Screen handlers â port targets in screens/ later.
void showEAKidsLogo();
void showHighScoreLogo();
void showFloppyStormLogo();
- /// Profile selector â mirrors `screen8_handler @ 1c33:1012`.
- /// Walks `listProfiles()`, draws the list of existing profile
- /// names plus a "[New Player]" entry, and either calls
- /// `loadProfile(name)` on a click or falls through to
- /// `doNewPlayer()` if the user picks "New". When no profiles
- /// exist, behaves identically to `doNewPlayer()` (the original
- /// also bypasses the picker when `local_20 == 0` â see
- /// 1c33:1170: `if (saves == 0) _NewPlayer();`).
+ /// `screen8_handler @ 1c33:1012`. Profile selector â walks
+ /// `listProfiles()`, falls through to `doNewPlayer()` if "New" or
+ /// no profiles exist (1c33:1170: `if (saves == 0) _NewPlayer();`).
void doProfilePicker();
- void doNewPlayer(); ///< Mirrors `_NewPlayer` @ 1c33:0dda
+ void doNewPlayer(); ///< `_NewPlayer @ 1c33:0dda`.
void doChoosePartner();
- /// Display the per-mystery ending pages from `E<num>.BIN`.
- /// Mirrors `_DisplayEnding @ 1df2:0548` + `_DisplayEndingPage
- /// @ 1df2:044c`. The file format is a 2-byte page count followed
- /// by N pages, each `{ u16 picNum, u16 x1, u16 y1, u16 x2, u16 y2,
- /// char text[] (null-terminated, ParseString placeholders) }`.
- /// Blocks until the player exits the ending view or crosses either
- /// page boundary.
+ /// Display the per-mystery ending pages from `E<num>.BIN`. Mirrors
+ /// `_DisplayEnding @ 1df2:0548` + `_DisplayEndingPage @ 1df2:044c`.
+ /// File format: u16 page count, then N pages of
+ /// `{ u16 picNum, u16 x1, u16 y1, u16 x2, u16 y2,
+ /// char text[] (null-terminated, ParseString placeholders) }`.
/// `_ShowOneScrap @ 1f78:0773` is just `_DisplayEnding(num, 1)`,
- /// so this same call covers the post-mystery scrapbook view from
- /// the action menu.
- ///
- /// `firstPage=true` opens at page 0; `false` opens at the last
- /// page (used by `doShowScrapbook` when navigating backwards
- /// between mysteries â mirrors the `local_8 = 0` write at
- /// `_ShowScrapbook @ 1f78:067e`).
- ///
- /// Returns the direction the user wants the caller to navigate:
- /// -1 â previous mystery (LEFT pressed on the first page or
- /// click in `PrevPageRect` while on first page),
- /// 0 â exit the scrapbook (ESC / quit; central clicks are ignored),
+ /// so this call also covers the post-mystery scrapbook view.
+ /// `firstPage=true` opens at page 0; `false` opens at the last page
+ /// (back-nav from `doShowScrapbook`, mirrors the `local_8 = 0` write
+ /// at `_ShowScrapbook @ 1f78:067e`).
+ /// Returns the caller's nav direction (per `[BP-0x18]` @ 1df2:0723):
+ /// -1 â previous mystery (LEFT on first page or click in PrevPageRect),
+ /// 0 â exit scrapbook (ESC / quit),
/// +1 â next mystery (RIGHT/SPACE/Enter/click on last page).
- /// Mirrors `_DisplayEnding`'s `[BP-0x18]` return at 1df2:0723.
int doShowEnding(uint num, bool firstPage = true);
- /// Browse the scrapbook tier @p stage (1=Junior, 2=Senior,
- /// 3=Master), moving between mystery endings with the original
- /// `_DisplayEnding` return direction.
- /// Mirrors `_ShowScrapbook(stage, 0) @ 1f78:0642`: the original
- /// computes the mystery range from `(stage - 1) * 0x18 + 1` to
- /// `(stage - 1) * 0x18 + 0x18`. If this is the current chain stage,
- /// unsolved entries are skipped; completed tiers are already solved.
- /// Used by both the setup-screen ScrapBook buttons and the
- /// action-menu "See ScrapBook 1/2/3" entries.
+ /// `_ShowScrapbook(stage, 0) @ 1f78:0642`. Mystery range
+ /// `(stage-1)*0x18+1 .. (stage-1)*0x18+0x18`; skips unsolved
+ /// entries in the current chain stage.
void doShowScrapbook(uint stage);
void doActionScreen();
void doCaseSelection();
void doSiteLoop();
- /// Setup / preferences screen. Mirrors `_DoSetup @ 1f78:044e` â
- /// per-profile preferences (voice on/off via `DAT_2d5d_3f97`,
- /// partner pick via SwapColors on Kid1/Kid2 rects). Reachable
- /// from BigMap's setup button (sets `_NextScreen = 6` per
- /// `_DoBigMap @ 20fe:0c33`). Returns to whatever
- /// `_lastScreen` was â typically MAP.
+ /// `_DoSetup @ 1f78:044e`. Voice on/off (`DAT_2d5d_3f97`), partner
+ /// pick via SwapColors on Kid1/Kid2 rects.
void doSetup();
- /// Render the case briefing background + game/book decorations and
- /// display the briefing ClueBlock. Mirrors `_DoInitClues` @ 1a35:0411
- /// minus the live ANI sequence playback.
+ /// `_DoInitClues @ 1a35:0411` (minus live ANI sequence playback).
void doInitClues();
- /// Floppy variant of the briefing dialog renderer. Walks the dialog
- /// record list at the tail of the floppy InitBlock (per
- /// `FUN_19bb_042f` and `FUN_22dc_05c8 @ 22dc:05c8`) and renders one
- /// speech balloon per record. Each record is `11 + textCount` bytes:
- /// +0..1 picID (character portrait, 0 = none)
- /// +2..3 picX
- /// +4 picY
- /// +5 balloonId | (mirror_flag << 7)
- /// +6..7 balloonX
- /// +8 balloonY
- /// +9 soundFlag (high bit) | sound slot (low 7 bits)
- /// +10 textCount
- /// +11.. text indices (1 byte each, low 7 bits = NOTES idx)
+ /// Floppy briefing dialog renderer. Walks the dialog record list at
+ /// the tail of the floppy InitBlock (`FUN_19bb_042f`, `FUN_22dc_05c8
+ /// @ 22dc:05c8`). Record = 11 + textCount bytes:
+ /// +0..1 picID +2..3 picX +4 picY
+ /// +5 balloonId|(mirror<<7) +6..7 balloonX +8 balloonY
+ /// +9 soundFlag|slot +10 textCount +11.. text indices
void displayFloppyBriefing(const byte *initBlock);
- /// Render `count` consecutive floppy dialog records starting at
- /// `rec`. Shared between briefing and hotspot click handlers since
- /// the original engine uses the same `FUN_22dc_05c8 @ 22dc:05c8`
- /// renderer in both contexts. `lastIndicator` is the `param_2`
- /// equivalent for the LAST record in the batch â 0 = no
- /// indicator, 1 = PIC 0xa0 (red "more" arrow), 2 = PIC 0xa1
- /// (alternate end indicator). Records before the last always get
- /// PIC 0xa0.
+ /// `FUN_22dc_05c8 @ 22dc:05c8`. Renders `count` floppy dialog records.
+ /// `lastIndicator`: 0 = none, 1 = PIC 0xa0, 2 = PIC 0xa1 (records
+ /// before the last always get PIC 0xa0).
void displayFloppyDialogRecords(const byte *rec, uint count,
uint lastIndicator = 0);
public:
- /// Mirrors `_StartTravelMusic @ 20a2:0595`. Picks `MUS%05d.XMI`
- /// based on `_mystery._siteNumber % 5` and starts it one-shot. The
- /// site loop calls this each time `enter(siteNum)` runs so the
- /// music changes as the player travels between sites.
+ /// `_StartTravelMusic @ 20a2:0595`. Picks `MUS%05d.XMI` from
+ /// `_mystery._siteNumber % 5`, one-shot.
void startTravelMusic();
- /// Block until the current MIDI cue finishes or the player skips it,
- /// then stop/unload it. Mirrors the `_IsMIDIPlaying` spin +
- /// `_StopMIDI` cleanup in `_DoSiteLoop` after the CD travel cue.
+ /// `_IsMIDIPlaying` spin + `_StopMIDI` cleanup in `_DoSiteLoop`.
void waitForMusicDone(uint32 maxMs = 60000);
- /// Stop any currently playing MIDI track. Mirrors `_StopMIDI @
- /// 20a2:0512` â used by `_DoSiteLoop @ 168d:06c0` after the
- /// one-shot travel track plays out and by `_DisplayCorrect /
- /// _DisplayAlibi` between MIDI cues.
+ /// `_StopMIDI @ 20a2:0512`.
void stopMusic();
- /// Forwarded from `Engine::syncSoundSettings`. Re-pulls the user's
- /// `music_volume` slider into the MIDI player's `_masterVolume`,
- /// otherwise the AdLib output stays at whatever the slider was at
- /// the moment `_music` was constructed (and the live launcher
- /// changes to the volume slider have no effect).
+ /// `Engine::syncSoundSettings` override. Re-pulls `music_volume`
+ /// into the MIDI player's `_masterVolume`.
void syncSoundSettings() override;
private:
- Common::String _playerName; ///< Substituted into 0x80 placeholders
+ Common::String _playerName; ///< Substituted into 0x80 placeholders.
- /// Per-mystery solved state. 0 = unsolved, 1 = solved, 2 = solved
- /// on first try. Mirrors `_PlayerRecord.SolvedMysteries[55]` in the
- /// original `_DisplayCorrect` flow.
+ /// `_PlayerRecord.SolvedMysteries[55]`. 0=unsolved, 1=solved, 2=first-try.
uint8 _mysteriesSolved[55] = {};
- /// Current chain/tier the player is at â mirrors `DAT_2d5d_3f99`
+ /// Current chain/tier the player is at â `DAT_2d5d_3f99`
/// (`_PlayerRecord +0x2f`):
- /// 1 = Junior detective (mysteries 1 .. 24, "A chain")
- /// 2 = Senior detective (mysteries 25 .. 48, "B chain")
- /// 3 = Master detective (mysteries 49 .. 54, "C chain")
- /// Initialized to 1 in `_NewPlayer @ 1c33:0fa3` and bumped by
+ /// 1 = Junior detective (mysteries 1..24, "A chain")
+ /// 2 = Senior detective (mysteries 25..48, "B chain")
+ /// 3 = Master detective (mysteries 49..54, "C chain")
+ /// Initialized to 1 in `_NewPlayer @ 1c33:0fa3`; bumped by
/// `_DisplayCorrect @ 1df2:0853` once every mystery in the current
- /// tier is solved (range checks at 1df2:080d / 0824 / 0837). The
- /// value also gates `_CaseSelection`'s book label and selection
- /// list (1c33:0a87 onwards).
+ /// tier is solved (range checks @ 1df2:080d / 0824 / 0837). Also
+ /// gates `_CaseSelection`'s book label and selection list
+ /// (1c33:0a87 onwards).
uint8 _chainStage = 1;
- /// Voice / digital-audio enable flag. Mirrors `DAT_2d5d_3f97`
- /// (`_PlayerRecord +0x2d`). Set to 1 by `_NewPlayer @ 1c33:0fa3`,
- /// toggled by the SoundOn / SoundOff hot-rects in `_DoSetup @
- /// 1f78:044e` (verified at `_SetupSettings` 1f78:0076 reading
- /// the same byte to colour the on/off labels). Gates every
- /// `_PlayVoice` and `_SpoolSound` call site (clue voices,
- /// partner speech, intro VO etc. â see `_DoChoosePartner`,
- /// `_DisplayClue`, `_SayKDDigital` xrefs).
+ /// Voice / digital-audio enable flag. `DAT_2d5d_3f97` (`_PlayerRecord
+ /// +0x2d`). Set to 1 by `_NewPlayer @ 1c33:0fa3`, toggled by the
+ /// SoundOn / SoundOff hot-rects in `_DoSetup @ 1f78:044e`. Gates
+ /// every `_PlayVoice` / `_SpoolSound` call site (clue voices,
+ /// partner speech, intro VO).
bool _voiceOn = true;
Common::RandomSource _rng;
- DBDArchive _picsArchive; ///< PICS.DBD/.DBX (sprites, buttons, frame backgrounds)
- DBDArchive _aniArchive; ///< ANI.DBD/.DBX (multi-frame character animations)
- DBDArchive _sitesArchive; ///< SITES.DBD/.DBX (one full-screen scene per site)
- DBDArchive _balloonArchive; ///< BALLOON.DBD/.DBX (speech-balloon sprites)
- DBDArchive _buttonArchive; ///< BUTTON.DBD/.DBX (per-site labeled map buttons; `_GetButton`)
- Mystery _mystery; ///< Currently-loaded case file (M<n>.BIN)
- EEMFont _font; ///< FONT.FNT - main 8 px font
+ DBDArchive _picsArchive; ///< PICS.DBD/.DBX
+ DBDArchive _aniArchive; ///< ANI.DBD/.DBX
+ DBDArchive _sitesArchive; ///< SITES.DBD/.DBX
+ DBDArchive _balloonArchive; ///< BALLOON.DBD/.DBX
+ DBDArchive _buttonArchive; ///< BUTTON.DBD/.DBX (`_GetButton`)
+ Mystery _mystery; ///< M<n>.BIN
+ EEMFont _font; ///< FONT.FNT (8 px)
- Common::Array<byte> _sitePals; ///< 40 x 768 bytes of 6-bit VGA palettes
+ Common::Array<byte> _sitePals; ///< 40 Ã 768 bytes, 6-bit VGA.
- uint16 _lastScreen; ///< Mirrors _LastScreen @ 2d5d:3f24
- uint16 _nextScreen; ///< Mirrors _NextScreen @ 2d5d:3f26
- uint8 _partner; ///< Mirrors _Partner: 0 = boy (Jake), 1 = girl (Jenny)
+ uint16 _lastScreen; ///< `_LastScreen @ 2d5d:3f24`.
+ uint16 _nextScreen; ///< `_NextScreen @ 2d5d:3f26`.
+ uint8 _partner; ///< `_Partner`: 0=Jake, 1=Jenny.
- /// Set when ESC is pressed during an intro animation or logo. Tells
- /// the opening-anim loop in run() to skip the rest of the chain
- /// instead of asking the user to click through every screen.
+ /// ESC during intro: skip remaining opening-anim chain.
bool _skipIntro = false;
- /// Per-slot rectangles + clue IDs from the most recent notebook
- /// render, populated by the `draw` lambda inside `doNotebook` and
- /// consumed by the click handler. The original walks the notes
- /// inline; we cache the layout to keep click hit-testing simple.
+ /// Cached notebook slot rects + clue IDs for click hit-testing.
Common::Array<Common::Rect> _notebookSlotRects;
Common::Array<uint> _notebookSlotClues;
- /// Optional clean BG (no partner / NPC sprites) used by `playKdAnim`
- /// to erase the partner's resting frame between camera-anim cells.
- /// Empty when no caller has provided one (PDA / case briefing /
- /// accuse contexts use their own composed backdrops). See
- /// `setPartnerEraseBg`.
+ /// Clean BG (no partner/NPC) used by `playKdAnim` between camera-anim
+ /// cells. See `setPartnerEraseBg`.
Graphics::ManagedSurface _partnerEraseBg;
bool _interactiveMouseCursor = false;
- /// Site whose entrance animation has already played in the current
- /// mystery. Kept on the engine, not SiteScreen, because PDA/gallery
- /// screens destroy and recreate SiteScreen when returning to the site.
+ /// Site whose entrance animation has already played this mystery.
+ /// Lives on the engine because PDA/gallery destroys+recreates SiteScreen.
int _lastSiteArrivalAnim = -1;
- /// XMIDI music player. Mirrors the original `MIDI.C` family
- /// (`_MIDIPlayFile`, `_MIDIPlay`, `_StopMIDI`, `_StartTravelMusic`
- /// at 20a2:00e2-05c9). Constructed lazily during `run()` once the
- /// MIDI driver / timer system is up.
+ /// `MIDI.C` family (`_MIDIPlayFile`/`_MIDIPlay`/`_StopMIDI`/
+ /// `_StartTravelMusic` @ 20a2:00e2-05c9). Constructed lazily in `run()`.
MusicPlayer *_music = nullptr;
- /// Digitised audio (voice + SFX). Mirrors `SOUND.C` / `SPOOLSND.C`
- /// â VOC playback (`_PlayVoice @ 1ff1:023e`) and the per-mystery
- /// SDB spool (`_SpoolSound @ 202f:068d` / `_InitMysterySounds @
- /// 202f:05cb`). Constructed alongside `_music` in `run()`.
+ /// `SOUND.C` / `SPOOLSND.C`. `_PlayVoice @ 1ff1:023e`,
+ /// `_SpoolSound @ 202f:068d`, `_InitMysterySounds @ 202f:05cb`.
public:
AudioPlayer *_audio = nullptr;
- /// Public setter for `_nextScreen` so site loop / inline screens
- /// can drive the screen-driver state machine without making the
- /// member itself public. Mirrors the original's direct write to
- /// `_NextScreen @ 2d5d:3f26` from anywhere in the engine.
+ /// `_NextScreen @ 2d5d:3f26` writer for site loop / inline screens.
void setNextScreen(ScreenId s) { _nextScreen = s; }
bool shouldPlaySiteArrival(uint siteNum) const {
return _lastSiteArrivalAnim != (int)siteNum;
diff --git a/engines/eem/font.cpp b/engines/eem/font.cpp
index 36d584c62b5..a5fba38d7f1 100644
--- a/engines/eem/font.cpp
+++ b/engines/eem/font.cpp
@@ -30,18 +30,13 @@
namespace EEM {
-// Character â glyph translation table.
-//
-// FONT.FNT layout (verified by dumping glyph bitmaps):
-// index 0..26 : ' ' .. ':' (punctuation, digits)
-// index 27..32 : ';' '<' '=' '>' '?' '@'
-// index 33..58 : UPPERCASE A..Z
-// index 59..84 : lowercase a..z
-//
-// The CHR2FNT segment table at 29b6:0000 in the original maps both 'A'
-// and 'a' to the lowercase glyph (so the original engine renders all
-// text in lowercase). We route uppercase ASCII letters to the uppercase
-// glyph slots (33..58) for proper mixed-case rendering.
+// CHR2FNT @ 29b6:0000. FONT.FNT layout:
+// 0..26 : ' ' .. ':'
+// 27..32 : ';' '<' '=' '>' '?' '@'
+// 33..58 : A..Z
+// 59..84 : a..z
+// Original aliases uppercase to lowercase glyphs; we route uppercase
+// ASCII to slots 33..58 for mixed-case rendering.
const byte kCharToGlyph[128] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
@@ -100,15 +95,10 @@ bool EEMFont::load(const Common::Path &path) {
_maxWidth = g.widthBits;
}
- // `_LoadFont @ 1b03:0220` sets the line stride to the FIRST
- // glyph's height (= the space character) â `DAT_28da_30ca =
- // DAT_28da_30ce` (the first byte of glyph 0 = its height). Tall
- // descender glyphs ('g', 'j', 'p', 'q', 'y') hang into the next
- // row by design, which is how the original engine keeps balloon
- // text dense enough to fit. Using `_maxHeight` (the descender-
- // inflated value) added 2â3 px per line and made every multi-
- // line bubble overflow its graphic vertically â verbatim user-
- // reported "bubbles aren't large enough" symptom.
+ // _LoadFont @ 1b03:0220 sets line stride to the first glyph's
+ // height (DAT_28da_30ca = DAT_28da_30ce, space glyph height).
+ // Descenders ('g','j','p','q','y') intentionally overhang into
+ // the next row.
_lineHeight = !_glyphs.empty() ? _glyphs[0].height : _maxHeight;
if (_lineHeight == 0)
_lineHeight = _maxHeight;
diff --git a/engines/eem/font.h b/engines/eem/font.h
index ae5a0638d7b..56275b8018e 100644
--- a/engines/eem/font.h
+++ b/engines/eem/font.h
@@ -40,18 +40,13 @@ struct FontGlyph {
};
/**
- * Loader for the engine's `.FNT` files (FONT.FNT, SYSTEM.FNT, TINY.FNT,
- * 8PNTTHIN.FNT). Mirrors `_LoadFont` @ 1b66:023c.
+ * Loader for .FNT files (FONT.FNT, SYSTEM.FNT, TINY.FNT, 8PNTTHIN.FNT).
+ * _LoadFont @ 1b66:023c.
*
- * File layout:
- * - u16 numChars
- * - per char: u8 height, u8 widthBits, u8 sizeBytes, bytes[sizeBytes] bitmap
+ * Layout: u16 numChars, then per char u8 height, u8 widthBits,
+ * u8 sizeBytes, bytes[sizeBytes] bitmap.
*
- * Subclasses `Graphics::Font` so callers can use the standard
- * `drawString` / `drawStringUnboxed` / `wordWrapText` helpers without
- * us reimplementing them. Lookups go through a 128-byte charâglyph
- * translation table extracted from CHR2FNT (segment 29b6:0000) â the
- * font is uppercase-only with lowercase aliased to uppercase glyphs.
+ * Char â glyph table extracted from CHR2FNT (29b6:0000).
*/
class EEMFont : public Graphics::Font {
public:
@@ -70,9 +65,8 @@ public:
uint32 color) const override;
using Graphics::Font::drawChar; // keep ManagedSurface overload
- /// Convenience wrap-and-draw helper that uses the inherited
- /// `wordWrapText` to break @p s into lines and then `drawString`
- /// to render each. Returns the total height drawn.
+ /// Wrap @p s to @p width via the inherited `wordWrapText` and draw
+ /// each line at (x, y) downward. Returns total height drawn.
int drawWordWrapped(Graphics::ManagedSurface *dst, int x, int y,
int width, const Common::String &s, uint32 color) const;
@@ -80,7 +74,7 @@ private:
Common::Array<FontGlyph> _glyphs;
uint16 _maxHeight = 0;
uint16 _maxWidth = 0;
- uint16 _lineHeight = 0; ///< First glyph height (= original line stride)
+ uint16 _lineHeight = 0; ///< First glyph height (original line stride)
};
} // End of namespace EEM
diff --git a/engines/eem/graphics.cpp b/engines/eem/graphics.cpp
index ca91bcc0349..c11505fa1dd 100644
--- a/engines/eem/graphics.cpp
+++ b/engines/eem/graphics.cpp
@@ -35,29 +35,25 @@
#include "eem/detection.h"
#include "eem/eem.h"
-// EEM â graphics helpers (KDGRAPH.C + KDHELP.C, the latter merged because
-// it has only three functions). Animation playback, balloon-table lookup,
-// and the help-screen primitives that share the same balloon machinery.
-
namespace EEM {
-// `_InterfaceHelp(num)` @ 1560:0205 reads `HelpData @ 29be:00c8` (5-byte
-// entries: u8 count, then up to 2 u16 picIds). Verified bytes:
+// HelpData @ 29be:00c8, read by _InterfaceHelp @ 1560:0205. 5-byte entries:
+// u8 count, then up to 2 u16 picIds. Verified bytes:
// entry 0 (PDA / gallery HELP button): count=2, picIds = 0x0063, 0x01ae
-// entry 1: count=2, picIds = 0x0192, 0x01b1
-// Only entry 0 is reachable from the PDA notebook (rect 1) and the
-// gallery (rect 1) â both call `_InterfaceHelp(0)`.
+// entry 1: count=2, picIds = 0x0192, 0x01b1
+// Only entry 0 is reachable from the PDA notebook (rect 1) and the gallery
+// (rect 1) â both call _InterfaceHelp(0).
const uint16 kHelpPics[][2] = {
{ 0x0063, 0x01ae },
{ 0x0192, 0x01b1 },
};
-// 52-entry, 10-bytes-each balloon-metadata table at `29be:0875` (CD)
-// / `2608:05f9` (floppy). Used by `_DisplayClue` to position
-// `_WordWrap` text inside the balloon AND to position the
-// "click to continue" indicator drawn by
-// `_DisplayHotspotClue_Floppy @ 22dc:05c8`. Layout per entry:
-// +0..1 = text X inset, +2..3 = text Y inset
+// kBalloonInsetTable: 52 entries x 10 bytes @ 29be:0875 (CD) / 2608:05f9
+// (floppy), indexed by (bubNum & 0x7F). Used by _DisplayClue to position
+// _WordWrap text inside the balloon AND by _DisplayHotspotClue_Floppy
+// @ 22dc:05c8 to position the "click to continue" indicator. Layout per entry:
+// +0..1 = text X inset
+// +2..3 = text Y inset
// +4..5 = wrap width
// +6..7 = "more / end" indicator X within the balloon
// +8..9 = "more / end" indicator Y within the balloon
@@ -117,12 +113,8 @@ bool findBalloonFamily(uint16 balloonId, uint16 &first, uint16 &last) {
return false;
}
-// Lines that fit inside balloon @p balloonId. The metadata-table `indDY`
-// is the designed bottom of the text area, NOT the indicator-drawing
-// position alone â for families 3, 4, 6 and the singletons the bubble
-// graphic continues below `indDY` with shadow / tail decoration that
-// text must not encroach on. Image height is therefore an overestimate;
-// `indDY` is the artist-intended last text line.
+// indDY is the artist-intended last text line, not just the indicator Y:
+// families 3, 4, 6 and singletons have shadow/tail decoration below indDY.
uint getBalloonLineCapacity(uint16 balloonId, int lineH) {
const uint idx = balloonId & 0x7F;
if (idx >= ARRAYSIZE(kBalloonInsetTable) || lineH <= 0)
@@ -132,10 +124,9 @@ uint getBalloonLineCapacity(uint16 balloonId, int lineH) {
return MAX<uint>(1, ((int)insets.indDY - (int)insets.y) / lineH + 1);
}
-// Floppy KDHelp hotspot-searched check. Mirrors
-// `FUN_22dc_096c @ 22dc:096c`: walks the per-site dialog records at
-// `site_data[+6]` to skip `hotspotIdx` hotspots, then returns the
-// `_cluesFound` flag for that hotspot's first text index.
+// FUN_22dc_096c @ 22dc:096c: walks per-site dialog records at site_data[+6]
+// to skip hotspotIdx hotspots, returns _cluesFound flag for hotspot's first
+// text index.
static bool floppyHotspotSearched(EEM::Mystery &mystery, uint siteIdx,
uint hotspotIdx) {
const byte *site = mystery.siteData(siteIdx);
@@ -169,21 +160,14 @@ static bool floppyHotspotSearched(EEM::Mystery &mystery, uint siteIdx,
}
void EEMEngine::doHelp() {
- // Floppy uses a totally different hint mechanism â per-mystery
- // `H<n>.BIN` data files (one per case). Format verified at
- // `FUN_1503_0001 @ 1503:0001` (loader, format string at
- // `2608:0154` = "h%d.bin") + `FUN_1503_01a5 @ 1503:01a5`
- // (consumer):
- // byte 0 = numChainHints
- // numChainHints à { byte siteIdx; byte hotspotIdx; }
- // byte = numExtraHints
- // numExtraHints à { byte siteIdx; byte hotspotIdx; }
- // asciiz string 1 ("[balloon-digit]Let's go to <site>...")
- // asciiz string 2 (alternate hint)
- // asciiz string 3 (post-solve hint, used when score ⥠100)
- // Selection logic: if any chain hotspot is unsearched â string 1.
- // Else if any extra hotspot is unsearched â string 2. Else if
- // `selectedPoints() ⥠100` â string 3.
+ // Floppy per-mystery H<n>.BIN hint files. Loader FUN_1503_0001 @ 1503:0001
+ // (format string "h%d.bin" @ 2608:0154), consumer FUN_1503_01a5 @ 1503:01a5.
+ // Format:
+ // byte numChainHints; numChainHints à { byte siteIdx; byte hotspotIdx; }
+ // byte numExtraHints; numExtraHints à { byte siteIdx; byte hotspotIdx; }
+ // asciiz str1, str2, str3 (post-solve, score >= 100)
+ // Selection: any chain hotspot unsearched -> str1; else any extra
+ // unsearched -> str2; else selectedPoints() >= 100 -> str3.
if (isFloppy() && _mystery.isLoaded()) {
const Common::String filename = Common::String::format("H%u.BIN",
_mystery.number());
@@ -252,18 +236,9 @@ void EEMEngine::doHelp() {
if (!chosen || *chosen == 0)
return;
- // Strip leading balloon-digit byte. `_GetKDTextBalloon @
- // 1df2:0105` (= floppy `FUN_1d40_009f`) doesn't take the
- // digit's *value* â it indexes the per-character table at
- // `2608:0c14` by the literal byte, so '0'..'9' map to a
- // non-trivial balloon-id sequence. Verified bytes at
- // `2608:0c44` (= 0xc14 + '0'):
- // '0'â0x15, '1'â0x16, '2'â0x17, '3'â0x18, '4'â0x19,
- // '5'â0x1a, '6'â0x1c, '7'â0x1d, '8'â0x1e, '9'â0x0a.
- // Without this map the previous (digit - '0') version asked
- // `getBalloonInsets` for balloon 0 (text width 142) instead of
- // the correct balloon 21 (text width 155), which is why the
- // hint bubble rendered narrower than the original.
+ // _GetKDTextBalloon @ 1df2:0105 (floppy FUN_1d40_009f) indexes the
+ // per-character table at 2608:0c14 by the literal byte. Bytes at
+ // 2608:0c44 (= 0xc14 + '0') give the '0'..'9' -> balloon-id mapping:
static const uint8 kFloppyDigitToBalloon[10] = {
0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1c, 0x1d, 0x1e, 0x0a
};
@@ -317,7 +292,6 @@ void EEMEngine::doHelp() {
g_system->copyRectToScreen(ms.getPixels(), ms.pitch, 0, 0, 320, 200);
g_system->updateScreen();
- // Wait for click â KD hint dismisses on any input.
while (!shouldQuit()) {
Common::Event ev;
bool advance = false;
@@ -338,9 +312,9 @@ void EEMEngine::doHelp() {
return;
}
- // Mirrors `_KDHelp @ 1560:010a`. The original walks the first two
- // entries of `_AChain` (the puzzle's required-clue chain â the
- // "spine" of evidence the player must collect):
+ // Mirrors _KDHelp @ 1560:010a. Walks the first two _AChain entries
+ // (the puzzle's required-clue chain â the "spine" of evidence the
+ // player must collect):
//
// for (i = 0; i < 2; i++) {
// if (_AChain[i] != -1 && _HintBlock[i] != -1 &&
@@ -351,17 +325,16 @@ void EEMEngine::doHelp() {
// if (_HintBlock[i] != -1) defined++;
// }
// if (!shown) {
- // // Fall back to the generic KD hint: KDTextIndex[+0xe]
- // // (first time) / KDTextIndex[+0x10] (second time, toggled
- // // by _SawHelpHint). If neither chain entry had a hint
- // // defined, show the global "no hints" sentinel instead.
+ // // Generic KD hint: KDTextIndex[+0xe] (first time) /
+ // // KDTextIndex[+0x10] (second time, toggled by _SawHelpHint).
+ // // If no chain hint was ever defined, render the "no hints"
+ // // sentinel instead.
// _DisplayHint(...);
// }
//
- // So this is a SMART per-puzzle hint: the partner points the
- // player at whichever chain clue they haven't yet found, only
- // falling back to the generic "let's keep looking" line when
- // every chain hint has been triggered already.
+ // SMART per-puzzle hint: partner points at whichever chain clue the
+ // player hasn't found yet, only falling back to the generic line once
+ // every chain hint has been triggered.
if (!_mystery.isLoaded() || !_font.isLoaded())
return;
@@ -393,10 +366,7 @@ void EEMEngine::doHelp() {
}
if (chosenText == 0xFFFF) {
- // No unfound chain clue had a hint to give â fall back to the
- // generic KD hint (or the "no hints defined" sentinel if the
- // chain has no hints at all). Mirrors the second arm of
- // `_KDHelp` (1560:0152-019b).
+ // Second arm of _KDHelp (1560:0152-019b): generic KD hint fallback.
if (anyHintDefined) {
const uint16 hintFirst = READ_LE_UINT16(kd + 0x0e);
const uint16 hintSecond = READ_LE_UINT16(kd + 0x10);
@@ -409,9 +379,7 @@ void EEMEngine::doHelp() {
soundNum = 8;
}
}
- // Else: keep chosenText == 0xFFFF â original would render
- // `NoHints` (a "There are no hints defined for this Mystery"
- // string at 29be:00d3); we just bail.
+ // Else: original would render NoHints string @ 29be:00d3; we bail.
}
if (chosenText == 0xFFFF) {
@@ -422,20 +390,19 @@ void EEMEngine::doHelp() {
const Common::String raw = _mystery.textAt(chosenText);
Common::String text = parseString(raw, _playerName, _partner);
- // Render as a speech-balloon overlay, exactly mirroring
- // `_DisplayHint @ 1560:0009`:
+ // Render as a speech-balloon overlay, mirroring _DisplayHint @ 1560:0009:
//
// _GetKDTextBalloon(text, &bub); // first-char dispatch
- // _GetBalloon(bub); // load balloon pic
- // y = (h < 0x4e) ? (0x50 - h) >> 1 : 1; // vertical centre
- // _AddPicBackground(balloon, 0x21, y); // overlay on screen
+ // _GetBalloon(bub); // load balloon pic
+ // y = (h < 0x4e) ? (0x50 - h) >> 1 : 1; // vertical centre
+ // _AddPicBackground(balloon, 0x21, y); // overlay on screen
// _WordWrap(0x21+tbl[bub].x, y+tbl[bub].y, // text inside balloon
// tbl[bub].w, text, -1, color=0);
- // _SayKDDigital(snd); // partner voice
+ // _SayKDDigital(snd); // partner voice
// _Wait();
//
- // The balloon BG is the caller's CURRENT screen â site / PDA /
- // gallery â not a cleared scratch.
+ // BG is the caller's CURRENT screen (site / PDA / gallery), not a cleared
+ // scratch.
Graphics::ManagedSurface ms(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
ms.clear();
@@ -447,16 +414,13 @@ void EEMEngine::doHelp() {
}
}
- // Balloon shape dispatch via `_GetKDTextBalloon @ 1df2:0105` â
- // based on the first char of the parsed text. Digits select a
- // specific balloon variant; non-digit defaults to `0x17`. The
- // digit, when present, is THEN consumed from the displayed
- // text â mirrors `_DisplayAlibi @ 1df2:0145`'s `str = pbVar7 + 1`
- // advance after using `*str` for `bindx`. Without this the hint
- // renders like "1Try checking the kitchen..." with a stray
- // leading digit. `_GetKDTextBalloon` itself doesn't strip it
- // (verified at 1df2:0105 â it just reads `*str`), so the caller
- // has to.
+ // Balloon shape dispatch via _GetKDTextBalloon @ 1df2:0105 â based on
+ // the first char of the parsed text. Digits select a specific balloon
+ // variant; non-digit defaults to 0x17. The digit, when present, is
+ // THEN consumed from the displayed text â mirrors _DisplayAlibi
+ // @ 1df2:0145's `str = pbVar7 + 1` advance after reading `*str` for
+ // bindx. _GetKDTextBalloon itself doesn't strip it (1df2:0105 just
+ // reads `*str`), so the caller has to.
const byte firstChar =
text.empty() ? (byte)0 : (byte)text[0];
uint16 bubNum = getKDTextBalloon(firstChar);
@@ -480,8 +444,6 @@ void EEMEngine::doHelp() {
(uint32)transp);
}
- // Balloon-relative text insets from the table at `29be:0875`
- // (10 bytes per entry: x, y, max-width, ...).
uint16 tx = 5, ty = 4, tw = 155;
getBalloonInsets(bubNum, tx, ty, tw);
_font.drawWordWrapped(&ms, balloonX + tx, balloonY + ty, tw, text,
@@ -491,9 +453,10 @@ void EEMEngine::doHelp() {
0, 0, 320, 200);
g_system->updateScreen();
- // `_DisplayHint @ 1560:0009` plays `_SayKDDigital(soundnum)` â
- // partner-specific voice line keyed to which hint type fired (10
- // = first chain hint, 11 = second, 7 / 8 = generic KD).
+ // _DisplayHint @ 1560:0009 plays _SayKDDigital(soundnum) â a
+ // partner-specific voice line keyed to which hint type fired:
+ // 10 = first chain hint, 11 = second chain hint,
+ // 7 = generic KD (first), 8 = generic KD (second).
if (_audio && _mystery.kdTextIndex() && soundNum > 0)
_audio->sayKDDigital(_mystery.kdTextIndex(), (uint)soundNum,
_partner);
@@ -502,29 +465,22 @@ void EEMEngine::doHelp() {
}
void EEMEngine::doInterfaceHelp(uint num) {
- // Mirrors `_InterfaceHelp(num)` @ 1560:0205. The original walks
- // `HelpData @ 29be:00c8` (5-byte entries: u8 count, then up to 2
- // u16 picIds), `_GetPicture`s each one, blits it via
- // `_Rect_Move_Mask(0, 0, ...)` (a MASKED blit on top of the
- // existing screen â transparent pixels show the caller's BG), and
- // waits for click / key. ESC at `1560:02b3` skips to end. The
- // function also hides the cursor at the top (`MOV [0x3a00], 0` at
- // 1560:0216 + `_RemoveMouse @ 1000:542f` at 1560:021c) and
- // restores it at the tail (`_DrawMouse @ 1000:5429` at 1560:02e8).
- //
- // `kHelpPics` lives at file scope above; see comment there for the
- // HelpData decoding.
+ // Mirrors _InterfaceHelp(num) @ 1560:0205. The original walks
+ // HelpData @ 29be:00c8 (5-byte entries: u8 count, then up to 2 u16
+ // picIds), _GetPictures each one, blits via _Rect_Move_Mask(0, 0, ...)
+ // (a MASKED blit on top of the existing screen â transparent pixels
+ // show the caller's BG), and waits for click / key. ESC at 1560:02b3
+ // skips to the end. The function hides the cursor at the top
+ // (MOV [0x3a00], 0 @ 1560:0216 + _RemoveMouse @ 1000:542f at
+ // 1560:021c) and restores it at the tail (_DrawMouse @ 1000:5429
+ // at 1560:02e8). See kHelpPics comment for HelpData decoding.
if (num >= ARRAYSIZE(kHelpPics))
return;
debugC(1, kDebugScript, "doInterfaceHelp(%u): showing pics 0x%x, 0x%x",
num, kHelpPics[num][0], kHelpPics[num][1]);
- // Snapshot the caller's screen ONCE so each help PIC overlays the
- // same clean BG. Without this, after the first PIC is dismissed the
- // second snapshot would include the first PIC's pixels and the two
- // would composite together â same gotcha as the setup-screen help
- // loop fix in `doSetup`.
+ // Snapshot caller's screen once: each PIC overlays the same clean BG.
Graphics::ManagedSurface bg(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
{
@@ -548,17 +504,8 @@ void EEMEngine::doInterfaceHelp(uint num) {
debugC(1, kDebugScript, "doInterfaceHelp: pic 0x%x = %dx%d flags=0x%x",
picId, pic.surface.w, pic.surface.h, pic.flags);
- // Compose a 320x200 frame from the clean BG snapshot and overlay
- // the help pic with `transBlitFrom` â `Graphics::ManagedSurface`'s
- // masked blit (transparent colour = the pic's `flags >> 8`,
- // matching `_Rect_Move_Mask`'s param_10 at 1000:03fc). Pass an
- // explicit `destPos` of (0, 0) â the no-destPos overload at
- // managed_surface.cpp:738 scales src to fill `this`'s rect,
- // stretching the help PIC to 320x200 instead of placing it at
- // native size. The original `_Rect_Move_Mask` passes destX=0,
- // destY=0 with copy-width = pic[+4] (= `pic.surface.w`) and
- // copy-height = pic[+2] (= `pic.surface.h`) â i.e. native size,
- // not stretched.
+ // transBlitFrom transp = pic.flags >> 8 matches _Rect_Move_Mask param_10
+ // @ 1000:03fc. Explicit (0,0) destPos: no-arg overload stretches to fill.
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
scratch.simpleBlitFrom(bg);
@@ -617,10 +564,7 @@ void EEMEngine::setPartnerEraseBg(const Graphics::ManagedSurface *bg) {
uint16 EEMEngine::fitBalloonToText(uint16 bubNum,
const Common::String &text) {
- // Opt-in via the "Better fit for dialog balloons" game option, and
- // CD-only â the floppy build's balloon archive / inset table hasn't
- // been validated for shrinking yet, so leave it on the original
- // artist-chosen bubble.
+ // Opt-in via "fit_dialog_balloons", CD only (floppy table unvalidated).
if (isFloppy() || !ConfMan.getBool("fit_dialog_balloons"))
return bubNum;
@@ -683,7 +627,6 @@ uint16 EEMEngine::fitBalloonToText(uint16 bubNum,
bool EEMEngine::getBalloonInsets(uint16 bubNum, uint16 &xInset,
uint16 &yInset, uint16 &textW) const {
- // `kBalloonInsetTable` lives at file scope above; see comment there.
const uint idx = bubNum & 0x7F;
if (idx >= ARRAYSIZE(kBalloonInsetTable))
return false;
@@ -706,12 +649,12 @@ bool EEMEngine::getBalloonIndicatorPos(uint16 bubNum, uint16 &dx,
void EEMEngine::drawFloppyBubbleIndicator(Graphics::ManagedSurface &dst,
uint16 bubNum, int ballX, int ballY,
bool endIndicator) {
- // Mirrors `_DisplayHotspotClue_Floppy @ 22dc:08c0` (end-of-record)
- // and `@ 22dc:08aa` (mid-pagination). Both grab a pre-loaded PIC
- // (`DAT_28da_3034 = PIC 0xa0` for "more pages",
- // `DAT_28da_3030 = PIC 0xa1` for "end indicator") and stamp it at
- // `(ballX + insetTable[bubNum].indDX,
- // ballY + insetTable[bubNum].indDY)` via `_AddPicBackground`.
+ // Mirrors _DisplayHotspotClue_Floppy @ 22dc:08c0 (end-of-record) and
+ // @ 22dc:08aa (mid-pagination). Both grab a pre-loaded PIC:
+ // DAT_28da_3034 = PIC 0xa0 "more pages" indicator
+ // DAT_28da_3030 = PIC 0xa1 "end" indicator
+ // and stamp it at (ballX + insetTable[bubNum].indDX,
+ // ballY + insetTable[bubNum].indDY) via _AddPicBackground.
uint16 dx = 0;
uint16 dy = 0;
if (!getBalloonIndicatorPos(bubNum, dx, dy))
diff --git a/engines/eem/music.cpp b/engines/eem/music.cpp
index 1bd50f83cce..c1fd4bf26b0 100644
--- a/engines/eem/music.cpp
+++ b/engines/eem/music.cpp
@@ -39,11 +39,9 @@ const int kMidiDriverFlags = MDT_MIDI | MDT_ADLIB | MDT_PREFER_MT32;
} // End of anonymous namespace
MusicPlayer::MusicPlayer(bool isFloppy) : _isFloppy(isFloppy) {
- // Mirrors `_InitMIDI @ 20a2:013a` which used `_AIL_register_driver`
- // to walk the .ADV files (ADLIB.ADV, SBFM.ADV, MT32MPU.ADV, etc.)
- // and pick a backend. We honour the launcher's "Music driver"
- // setting while preferring MT-32 when no concrete device was chosen,
- // like other ScummVM engines with native MT-32 scores.
+ // _InitMIDI @ 20a2:013a â `_AIL_register_driver` against
+ // ADLIB.ADV / SBFM.ADV / MT32MPU.ADV. We honour the launcher's
+ // "Music driver" setting and prefer MT-32 when unset.
const MidiDriver::DeviceHandle dev =
MidiDriver::detectDevice(kMidiDriverFlags);
MusicType musicType = MidiDriver::getMusicType(dev);
@@ -52,22 +50,21 @@ MusicPlayer::MusicPlayer(bool isFloppy) : _isFloppy(isFloppy) {
switch (musicType) {
case MT_ADLIB:
- // `_MIDIPlayFile` (20a2:024c) opens "SAMPLE.AD" (29be:14d6) and
- // installs every patch the sequence requests via
+ // _MIDIPlayFile @ 20a2:024c opens SAMPLE.AD (string at 29be:14d6)
+ // and installs every patch the sequence requests via
// `_AIL_install_timbre`. ScummVM's Miles AdLib driver does the
- // same: it reads SAMPLE.AD on construction and serves timbres
- // out of that bank, which is what makes the notes match the
- // 1993 release. SAMPLE.OPL would be the OPL3 variant; the game
- // only ships SAMPLE.AD, so an empty path falls back to OPL2.
+ // same on-demand install from SAMPLE.AD, which is what makes
+ // the notes match the 1993 release; the generic AdLib fallback
+ // would use ScummVM's built-in timbres instead. SAMPLE.OPL would
+ // be the OPL3 variant â the game only ships SAMPLE.AD, so the
+ // empty second path falls back to OPL2.
_milesAudioMode = true;
_driver = Audio::MidiDriver_Miles_AdLib_create(
Common::Path("SAMPLE.AD"), Common::Path());
break;
case MT_MT32:
- // `MT32MPU.ADV` was the original MT-32 driver; ScummVM has no
- // Miles MT-32 instrument bank for EEM, so we use the standard
- // MT-32 driver and let the XMIDI's own program changes drive
- // the patch selection.
+ // MT32MPU.ADV in the original. No Miles MT-32 bank ships with
+ // EEM, so use the standard MT-32 driver.
_milesAudioMode = true;
_driver = Audio::MidiDriver_Miles_MT32_create(Common::Path());
break;
@@ -84,10 +81,7 @@ MusicPlayer::MusicPlayer(bool isFloppy) : _isFloppy(isFloppy) {
delete _driver;
_driver = nullptr;
} else {
- // No GM/MT-32 reset for AdLib (Miles AdLib handles its own
- // state); for MT-32/GM the original would've sent its own
- // initialisation patches via `_AIL_install_timbre`, but we
- // don't have those banks for non-AdLib devices.
+ // Miles AdLib handles its own reset.
if (musicType != MT_ADLIB) {
if (musicType == MT_MT32 || _nativeMT32)
_driver->sendMT32Reset();
@@ -103,12 +97,12 @@ MusicPlayer::MusicPlayer(bool isFloppy) : _isFloppy(isFloppy) {
void MusicPlayer::send(uint32 b) {
// Miles drivers (both AdLib and MT-32) implement their own per-
- // source-channel mixing and timbre installation, so just forward
- // the raw event. Going through `MidiPlayer::send` would re-wrap
- // CC 7 against `_masterVolume` AND remap the source channel via
- // `sendToChannel`/`allocateChannel`, both of which the Miles driver
- // already handles internally (and break the timbre selection if
- // double-applied).
+ // source-channel mixing and timbre installation, so forward the raw
+ // event. Going through `MidiPlayer::send` would re-wrap CC 7 against
+ // `_masterVolume` AND remap the source channel via
+ // `sendToChannel` / `allocateChannel`, both of which the Miles
+ // driver already handles internally (double-applying breaks the
+ // timbre selection).
if (_milesAudioMode) {
_driver->send(b);
return;
@@ -123,7 +117,7 @@ void MusicPlayer::playFile(const Common::Path &xmiPath, bool loop) {
Common::StackLock lock(_mutex);
stop();
- // Mirrors `_MIDIPlayFile`'s `_fopen` + `_fread` (20a2:024c-029e).
+ // _MIDIPlayFile @ 20a2:024c-029e (_fopen + _fread).
Common::File f;
if (!f.open(xmiPath)) {
warning("MusicPlayer: %s missing", xmiPath.toString().c_str());
@@ -157,17 +151,15 @@ void MusicPlayer::playFile(const Common::Path &xmiPath, bool loop) {
return;
}
- // Mirrors `_LoopMIDI = 0xFFFF` (the count register the original
- // engine uses for indefinite looping in `_DoOpeningAnims`).
+ // _LoopMIDI = 0xFFFF in _DoOpeningAnims.
_isLooping = loop;
_parser->property(MidiParser::mpAutoLoop, loop ? 1 : 0);
_parser->setTrack(0);
// Pull the launcher's music_volume slider into `_masterVolume` so
// the non-Miles `Audio::MidiPlayer::send` path scales correctly.
- // (Miles drivers do their own volume handling on the Multisource
- // path, but they also honour `MidiDriver::syncSoundSettings` which
- // `Engine::syncSoundSettings` triggers.)
+ // (Miles drivers handle volume themselves but also honour
+ // `MidiDriver::syncSoundSettings` via `Engine::syncSoundSettings`.)
syncVolume();
_isPlaying = true;
debugC(1, kDebugSound, "MusicPlayer: playing %s (%u bytes, loop=%d, miles=%d)",
@@ -175,15 +167,14 @@ void MusicPlayer::playFile(const Common::Path &xmiPath, bool loop) {
}
void MusicPlayer::playMus(uint num, bool loop) {
- // CD format string verified at `29be:1525` ("mus%05d.xmi").
- // Floppy maps the same numeric slots to its own filenames:
- // 0..4 â travel music. The floppy table at 2608:1399-13cd holds
- // 5 entries (Travel-6, Travel-4, Travel-7, Travel-1,
- // Travel-8) used by `_StartTravelMusic` via
- // `siteNumber % 5`.
+ // CD format string "mus%05d.xmi" at 29be:1525. Floppy maps the same
+ // numeric slots to different filenames:
+ // 0..4 â travel music. Table at 2608:1399-13cd holds 5 entries
+ // (Travel-6, Travel-4, Travel-7, Travel-1, Travel-8) used by
+ // `_StartTravelMusic` via `siteNumber % 5`.
// 5 â FANFARE2.XMI (winner). String at 2608:0c64.
- // 6 â no equivalent in floppy install (the loser sting in
- // `_DisplayAlibi` is CD-only); skip.
+ // 6 â no equivalent on floppy (loser sting in `_DisplayAlibi`
+ // is CD-only); skip.
if (_isFloppy) {
static const char *const kTravelTracks[5] = {
"Travel-6.XMI", "Travel-4.XMI", "Travel-7.XMI",
diff --git a/engines/eem/music.h b/engines/eem/music.h
index 7e8c1fc817d..59dac57e30d 100644
--- a/engines/eem/music.h
+++ b/engines/eem/music.h
@@ -31,59 +31,37 @@
namespace EEM {
/**
- * MIDI music player. Mirrors the original `MIDI.C` source file in
- * `EEMCD.EXE` (the `_MIDIPlayFile / _MIDIPlay / _StopMIDI /
- * _IsMIDIPlaying / _StartTravelMusic` family at `20a2:00e2-05c9`).
+ * MIDI music player. Mirrors MIDI.C in EEMCD.EXE
+ * (_MIDIPlayFile / _MIDIPlay / _StopMIDI / _IsMIDIPlaying /
+ * _StartTravelMusic family at 20a2:00e2-05c9).
*
- * The original engine uses Miles Audio Interface Library (AIL):
+ * Original uses Miles AIL: _InitMIDI @ 20a2:013a registers
+ * ADLIB.ADV / SBFM.ADV / MT32MPU.ADV; _MIDIPlayFile @ 20a2:024c
+ * installs AdLib timbres from SAMPLE.AD (29be:14d6) via
+ * _AIL_install_timbre before _AIL_start_sequence. We use
+ * Audio::MidiDriver_Miles_AdLib_create (loads SAMPLE.AD) for AdLib
+ * and the Miles MT-32 driver for MT-32.
*
- * - `_InitMIDI @ 20a2:013a` calls `_AIL_register_driver` against
- * `ADLIB.ADV` / `SBFM.ADV` / `MT32MPU.ADV` and reserves a timbre
- * cache via `_AIL_define_timbre_cache`.
- * - `_MIDIPlayFile @ 20a2:024c` opens the .XMI, calls
- * `_AIL_register_sequence`, then loops over the sequence's
- * `_AIL_timbre_request` results. For every (bank, patch) pair the
- * driver asks for, it pulls the AdLib instrument definition from
- * **`SAMPLE.AD`** (string at `29be:14d6`) via `_load_global_timbre`
- * and installs it through `_AIL_install_timbre`. Only after every
- * patch is loaded does it call `_AIL_start_sequence`.
- *
- * Without those custom timbres, ScummVM's generic AdLib synth falls
- * back to its built-in default timbre table â same notes, very
- * different timbres. ScummVM ships a Miles AdLib driver
- * (`Audio::MidiDriver_Miles_AdLib_create`) that loads `SAMPLE.AD` and
- * implements the same install-on-demand workflow, so we use it for
- * AdLib output. MT-32 uses ScummVM's Miles MT-32 driver path, and any
- * other MIDI output falls back to `Audio::MidiPlayer::createDriver`.
- *
- * Available music files in the game directory:
- * - THEME.XMI â opening anims (looping) + title screen
- * - MUS00000.XMI..MUS00004.XMI â per-site travel music
- * (`_StartTravelMusic` picks one via `_SiteNumber % 5`)
- * - MUS00005.XMI â winner ending (`_DisplayCorrect` @ 1df2:0789)
- * - MUS00006.XMI â loser ending (`_DisplayAlibi` @ 1df2:018a)
+ * Music files:
+ * THEME.XMI â opening anims + title.
+ * MUS00000..MUS00004 â travel music (siteNumber % 5).
+ * MUS00005 â winner (_DisplayCorrect @ 1df2:0789).
+ * MUS00006 â loser (_DisplayAlibi @ 1df2:018a).
*/
class MusicPlayer : public Audio::MidiPlayer {
public:
explicit MusicPlayer(bool isFloppy = false);
- /// Mirrors `_MIDIPlayFile @ 20a2:024c`. Reads the .XMI from the game
- /// directory and starts playing. `loop=true` mirrors the
- /// `_LoopMIDI = 0xFFFF` writes inside `_DoOpeningAnims` (theme music).
+ /// _MIDIPlayFile @ 20a2:024c. loop=true mirrors
+ /// _LoopMIDI = 0xFFFF in _DoOpeningAnims.
void playFile(const Common::Path &xmiPath, bool loop = false);
- /// Mirrors `_MIDIPlay(num) @ 20a2:047d`. Composes the filename
- /// "MUS%05u.XMI" (CD) or maps to TRAVEL-N.XMI / FANFARE2.XMI
- /// (floppy) and plays it. Used by `_StartTravelMusic`,
- /// `_DisplayCorrect` (winner), `_DisplayAlibi` (loser).
+ /// _MIDIPlay(num) @ 20a2:047d. CD: "MUS%05u.XMI";
+ /// floppy: TRAVEL-N.XMI / FANFARE2.XMI.
void playMus(uint num, bool loop = false);
- // In Miles AdLib mode the driver allocates its own AdLib voice
- // pool and consumes the XMIDI's source-channel byte directly, so we
- // must NOT route through `Audio::MidiPlayer::sendToChannel` (which
- // remaps every source channel through `allocateChannel()` and
- // breaks the timbre selection / volume scaling the Miles driver
- // performs internally). Same workaround Toltecs / SAGA use.
+ // Miles drivers handle source-channel routing themselves; bypass
+ // Audio::MidiPlayer::sendToChannel. Same workaround as Toltecs / SAGA.
void send(uint32 b) override;
private:
diff --git a/engines/eem/mystery.cpp b/engines/eem/mystery.cpp
index 5982e5c5fc5..06892c0735a 100644
--- a/engines/eem/mystery.cpp
+++ b/engines/eem/mystery.cpp
@@ -92,63 +92,23 @@ bool Mystery::load(uint num, Common::RandomSource *rng) {
_data = staging;
_number = num;
- // Floppy `M*.BIN` uses a completely different header layout from
- // the CD release. Verified via Ghidra of `EEM.EXE` floppy:
- //
- // `_ReadMystery_Floppy @ 22dc:0178` parses M0..M54 into pointers
- // held in a global table at 28da:3c87+. The header offsets are:
- //
- // header[+0..+1] ???
- // header[+2..+3] ???
- // header[+4..+5] pointer â SUSPECTS section
- // (count byte, then 0xb-byte entries; entry[+4] =
- // pic ID, entry[+10] = recolor flag)
- // (`_FloppySuspectsPtr` @ 28da:3c8b)
- // header[+6..+7] pointer â ??? (28da:3c9f)
- // header[+8..+9] pointer â NOTES section
- // (7-byte entries indexed by clue ID; used by
- // floppy `_DrawNotes` @ 15e0:01e8)
- // (`DAT_28da_3c9b`)
- // header[+0xa..+b] pointer â GALLERY-PORTRAITS section
- // (count byte, then variable-length entries
- // `5 + name_len` bytes; entry[+0..+1] = u16
- // picID, entry[+4] = name length)
- // (`_FloppyGalleryPtr` @ 28da:3c87, count =
- // `_FloppyNumSuspects` @ 28da:004b)
- // header[+0xc..+d] pointer â TEXT block (alibi text base; used in
- // floppy `_DisplayAlibi` @ 1d40:00df)
- // header[+0x10..1] pointer â KDTextIndex (`_FloppyKDTextIndexPtr`
- // @ 28da:3c93)
- // header[+0x12..3] pointer â ??? (28da:3c8f)
- //
- // There is NO fixed-offset numSites / numSuspects / numCONSITEs
- // field â counts are stored as the FIRST byte of each section.
- // The CD release refactored this into a flat header at fixed
- // offsets; our `Mystery::load` here parses the CD layout.
- //
- // Detect the variant from the first u16: CD M0 starts with `0x003e`
- // (initOffset = 62), floppy M0 starts with `0x2286` (a section
- // pointer near end-of-file). When the first u16 is too high to be
- // a CD `_initOffset`, parse as floppy.
+ // Floppy M*.BIN uses a different header layout from CD.
+ // _ReadMystery_Floppy @ 22dc:0178. Section-pointer header:
+ // header[+0] InitBlock byte offset (caseType byte at *(buf+initOff))
+ // header[+4] SITES: count byte + 11-byte entries
+ // (entry[+4]=picID, [+6..7]=u16 X, [+8..9]=u16 Y, [+10]=recolor)
+ // header[+6] SITE INDEX (array of u16 offsets to per-site structs)
+ // header[+8] NOTES (7-byte entries / clue ID; _DrawNotes_Floppy @ 15e0:01e8)
+ // header[+0xa] GALLERY portraits: count byte + variable `5+nameLen` entries
+ // (entry[+0..1]=u16 picID, [+4]=nameLen)
+ // header[+0xc] TEXT block (alibi base; _DisplayAlibi_Floppy @ 1d40:00df)
+ // header[+0x10] KDTextIndex
+ // header[+0x12] SOLVED CLUE CHAIN
+ // Counts live in the FIRST BYTE of each section, not the header.
+ // Detect via first u16: CD M0 = 0x003e (initOffset), floppy M0 = 0x2286
+ // (section pointer near EOF).
if (readU16(0) > 0x100) {
_isFloppy = true;
- // Section-pointer header verified via Ghidra of floppy
- // `_ReadMystery_Floppy @ 22dc:0178`,
- // `_DoSiteLoop_Floppy @ 1652:03a3`,
- // and `FUN_1fed_07ed` (BigMap site iteration):
- //
- // header[+4] â SITES section
- // count byte + 0xb-byte entries; entry[+4] =
- // pic ID for BigMap marker, [+6..7] = u16 X,
- // [+8..9] = u16 Y, [+10] = recolor flag.
- // header[+6] â SITE INDEX (array of u16 offsets to per-site
- // data structs; site[+0]=picOff,
- // site[+2]=clueBlockOff, site[+8]=speakerInfo)
- // header[+8] â NOTES (7-byte entries / clue ID)
- // header[+0xa] â SUSPECTS / GALLERY portraits
- // header[+0xc] â TEXT block base
- // header[+0x10] â KDTextIndex
- // header[+0x12] â SOLVED CLUE CHAIN
_floppySuspectsOff = readU16(0x04); // SITES
_floppyHintBlockOff = readU16(0x06); // SITE INDEX
_floppyNoteIndexOff = readU16(0x08);
@@ -156,16 +116,8 @@ bool Mystery::load(uint num, Common::RandomSource *rng) {
_floppyTextOff = readU16(0x0c);
_floppyKDTextOff = readU16(0x10);
_floppySolvedOff = readU16(0x12);
-
- // header[+0] (the first u16) holds the InitBlock byte offset on
- // floppy too â verified at `FUN_19bb_042f` where `*DAT_28da_3ca5`
- // (deref'd as int *) reads the first u16 of the buffer and uses
- // it as `cVar1 = *(buffer + initOffset)` (caseType byte).
_initOffset = readU16(0x00);
- // Counts: first byte of each section. Verified at
- // `FUN_1fed_07ed` (`uVar3 = *_FloppySuspectsPtr` then iterates)
- // and `FUN_154e_0045` (`DAT_28da_004b = *DAT_28da_3c87`).
const byte *sitesSec = (_floppySuspectsOff < _data.size())
? _data.data() + _floppySuspectsOff : nullptr;
const byte *susSec = (_floppyGalleryOff < _data.size())
@@ -175,16 +127,9 @@ bool Mystery::load(uint num, Common::RandomSource *rng) {
_numCONSITEs = 0;
_numCOFFSITEs = 0;
- // Point CD-shaped accessor offsets at the floppy equivalents
- // so existing accessors return the right base for floppy:
- // siteIndexEntry() â floppy site index (header[+6])
- // noteIndex() â floppy notes (header[+8])
- // galleryData() â floppy suspects (header[+0xa])
- // textAt() â floppy text block (header[+0xc])
- // kdTextIndex() â floppy KDTextIndex (header[+0x10])
- // solvedClueBlock() â floppy solved chain (header[+0x12])
- // Per-section LAYOUTS still differ from CD, so consumers
- // walking entries need `isFloppy()` branches.
+ // Point CD-shaped accessors at the floppy equivalents.
+ // Per-section LAYOUTS still differ, so consumers walking entries
+ // need `isFloppy()` branches.
_siteIndexOffset = _floppyHintBlockOff;
_noteOffset = _floppyNoteIndexOff;
_galleryOffset = _floppyGalleryOff;
@@ -193,16 +138,9 @@ bool Mystery::load(uint num, Common::RandomSource *rng) {
_solvedOffset = _floppySolvedOff;
_hintOffset = _floppyHintBlockOff;
- // Per-mystery runtime state â mirror `_ReadMystery_Floppy @
- // 22dc:0178` zeroing `_TextSeen_Floppy` / `_InGallery_Floppy` and
- // seeding `_NewOrder_Floppy[0]` (random) + the `+1`-shift loop
- // that fills the rest. We use the identity mapping `[0..N-1]` so
- // dialog `byte9` (1-based logical idx) maps to `_inGallery[idx -
- // 1]` and the gallery render iterates the same indices. Without
- // this init the floppy load path returns with `_newOrder` left
- // at whatever `clear()` zeroed it to, so every byte9>0 dialog
- // resolves through `_newOrder[logicalIdx]==0` and the second
- // suspect overwrites the first slot.
+ // Per-mystery runtime state â mirrors _ReadMystery_Floppy @ 22dc:0178.
+ // _newOrder uses identity mapping [0..N-1] so dialog byte9 (1-based
+ // logical idx) maps to _inGallery[idx - 1].
memset(_cluesFound, 0, sizeof(_cluesFound));
memset(_noteSelected, 0, sizeof(_noteSelected));
memset(_hotSpotsSeen, 0, sizeof(_hotSpotsSeen));
@@ -245,13 +183,10 @@ bool Mystery::load(uint num, Common::RandomSource *rng) {
_numCONSITEs = (uint8)readU16(14 * 2);
_numCOFFSITEs = (uint8)readU16(15 * 2);
- // Defensive clamp. The floppy mystery file format uses a different
- // header layout (verified by comparing M0.BIN: CD has `numSites =
- // readU16(0x14) = 3`; floppy has `readU16(0x14) = 0x1925`,
- // obviously not a site count). Without a clamp, downstream loops
- // over `_onSites` / `_visitedSite` (capacity 20) blow past the
- // array end. Until the floppy format is fully supported, cap at
- // the array capacity so the engine fails gracefully.
+ // Defensive clamp: the floppy header layout differs (M0.BIN CD has
+ // numSites=readU16(0x14)=3; floppy has readU16(0x14)=0x1925, clearly
+ // not a count). Cap to `_onSites` / `_visitedSite` array capacity so
+ // downstream loops don't walk off the end on malformed/floppy files.
if (_numSites > kVisitedSiteCap)
_numSites = kVisitedSiteCap;
@@ -261,17 +196,14 @@ bool Mystery::load(uint num, Common::RandomSource *rng) {
_cChain[i] = readU16((26 + i) * 2);
}
- // Per-mystery runtime state â `_ReadMystery` zeroes these at load.
+ // Per-mystery runtime state â _ReadMystery zeroes these at load.
+ // _newOrder uses identity mapping; original randomly cycles gallery
+ // positions but requires matching changes in both clue side-effect
+ // and rendering paths.
memset(_cluesFound, 0, sizeof(_cluesFound));
memset(_noteSelected, 0, sizeof(_noteSelected));
memset(_hotSpotsSeen, 0, sizeof(_hotSpotsSeen));
memset(_inGallery, 0, sizeof(_inGallery));
- // `_NewOrder` in the original randomly cycles the gallery positions
- // per playthrough. For consistency between clue side-effects (which
- // write to `_inGallery[_newOrder[galIdx]]`) and gallery rendering
- // (which iterates logical indices), we keep the identity mapping.
- // If the original's randomized positioning is required later, both
- // the side-effect path AND the rendering path need to use it together.
(void)rng;
for (uint i = 0; i < kGalleryCap; i++)
_newOrder[i] = (uint8)i;
@@ -280,7 +212,7 @@ bool Mystery::load(uint num, Common::RandomSource *rng) {
_sawCOFFSITEs = _sawCONSITEs = _sawHelpHint = _solvedPuzzle = false;
_firstTry = true;
_searchLocationNumber = _siteNumber = 0xFFFF;
- _lastSite = 0x1B; // Sentinel matching _ReadMystery's `_LastSite = 0x1b`.
+ _lastSite = 0x1B; // _ReadMystery _LastSite sentinel.
debugC(1, kDebugMystery, "Loaded %s (%d B): %u sites, %u suspects, "
"CON=%u COFF=%u, init=0x%04x site=0x%04x text=0x%04x",
@@ -293,9 +225,8 @@ bool Mystery::load(uint num, Common::RandomSource *rng) {
const byte *Mystery::siteIndexEntry(uint siteNum) const {
if (!isLoaded() || siteNum >= _numSites)
return nullptr;
- // Floppy site index uses 2-byte (u16) entries â verified at
- // `_DoSiteLoop_Floppy @ 1652:03d2` reading `*(int *)
- // ((int)_FloppySiteIndexPtr + siteNum * 2)`. CD uses 6-byte rows.
+ // Floppy site index: 2-byte u16 entries (_DoSiteLoop_Floppy @ 1652:03d2).
+ // CD: 6-byte rows.
const uint stride = _isFloppy ? 2 : 6;
const uint off = _siteIndexOffset + siteNum * stride;
if (off + stride > _data.size())
@@ -383,13 +314,9 @@ void Mystery::loadFloppySiteAnimData() {
const byte *Mystery::hotspots(uint siteNum) const {
if (_isFloppy) {
- // Floppy: hotspot table sits inside the per-site sub-blob.
- // `site_data[+4..5]` is a u16 file offset to a header byte
- // (count) + NÃ8-byte rectangles (x1, y1, x2, y2 as u16s) â
- // verified at `FUN_22dc_0b80 @ 22dc:0b80` (the click hit-test
- // loop reads `*(byte *)(buf + site_data[+4])` for the count
- // then `FUN_14c9_0039(... buf + site_data[+4] + 1 + i*8)`
- // for each rectangle).
+ // Floppy hotspot table inside per-site sub-blob (FUN_22dc_0b80 @ 22dc:0b80).
+ // site_data[+4..5] = u16 file offset to count byte + N x 8-byte rects
+ // (x1, y1, x2, y2 as u16s).
const byte *site = siteData(siteNum);
if (!site || (size_t)(site - _data.data()) + 6 > _data.size())
return nullptr;
@@ -453,11 +380,10 @@ const byte *Mystery::noteIndex() const {
uint16 Mystery::noteIndexCount() const {
if (!isLoaded())
return 0;
- // NoteIndex runs from _noteOffset to the start of GalleryData.
- // CD entries are 4 bytes (`u16 textOff; u16 points`); floppy
- // entries are 7 bytes (`u16 ?; u16 jakeOff; u16 jennyOff; u8
- // score`) â verified at `FUN_22dc_05c8 @ 22dc:0843` reading
- // `*(int *)(notes + idx*7 + 2)` (Jake) / `+4` (Jenny).
+ // NoteIndex runs from _noteOffset to start of GalleryData.
+ // CD entries: 4 bytes (u16 textOff; u16 points).
+ // Floppy entries: 7 bytes (u16 ?; u16 jakeOff; u16 jennyOff; u8 score)
+ // per FUN_22dc_05c8 @ 22dc:0843.
if (_galleryOffset <= _noteOffset)
return 0;
const uint stride = _isFloppy ? 7 : 4;
@@ -473,11 +399,8 @@ bool Mystery::noteHasNotebookText(uint clueId) const {
if (!_isFloppy)
return true;
- // `_DrawNotes_Floppy @ 15e0:01e8` first checks `_TextSeen[idx]`,
- // then skips the row when `*(u16 *)(notes + idx * 7) == 0`.
- // Many floppy dialog records are spoken-only lines: they must still
- // be marked seen so site dialog does not repeat, but they are not
- // notebook clues and should not render fallback "note N" labels.
+ // _DrawNotes_Floppy @ 15e0:01e8 skips rows with notes[idx*7..idx*7+1] == 0.
+ // Spoken-only dialog records are marked seen but have no notebook text.
return READ_LE_UINT16(ni + clueId * 7) != 0;
}
@@ -491,12 +414,9 @@ const byte *Mystery::mapEntry(uint siteNum) const {
if (!isLoaded() || siteNum >= _numSites)
return nullptr;
if (_isFloppy) {
- // Floppy SITES section: byte[0] = count, then 11-byte entries.
- // Verified at `FUN_1fed_07ed` (BigMap site iteration) where
- // `pcVar2 = _FloppySuspectsPtr` (header[+4]) and the loop reads
- // `*(int *)(pcVar2 + i*0xb + 7)` (X) and `*(int *)(pcVar2 + i*0xb
- // + 9)` (Y) â the +7/+9 offsets are 1-based because pcVar2[0]
- // holds the count, so entry stride 11 starts at byte 1.
+ // Floppy SITES section (FUN_1fed_07ed): byte[0]=count, then 11-byte
+ // entries starting at byte 1.
+ // +4 picID, +6..7 u16 X, +8..9 u16 Y, +10 recolor.
const uint off = _floppySuspectsOff + 1 + siteNum * 11;
if (off + 11 > _data.size())
return nullptr;
@@ -509,15 +429,12 @@ const byte *Mystery::mapEntry(uint siteNum) const {
}
const byte *Mystery::floppySuspectEntry(uint suspectIdx) const {
- // Floppy gallery section: byte[0] = numSuspects, then per suspect a
- // variable-size record of `5 + nameLen` bytes (verified at
- // `_DrawGallery_Floppy @ 154e:00b6` advancing `iVar7 += pbVar4[4]
- // + 5`):
- // u16 +0 picID (gallery portrait, BUTTON.DBD entry)
- // u16 +2 alibi marker (0xFFFF = guilty; else high byte = index
- // into TEXT_BLOCK alibi-offset table at header[+0xc])
- // u8 +4 name length
- // u8 +5..+5+nameLen-1 name string
+ // Floppy gallery section (_DrawGallery_Floppy @ 154e:00b6).
+ // byte[0]=numSuspects, then per suspect a `5 + nameLen` record:
+ // u16 +0 picID (BUTTON.DBD entry)
+ // u16 +2 alibi marker (0xFFFF=guilty; else hi byte indexes TEXT_BLOCK)
+ // u8 +4 nameLen
+ // u8 +5.. name string
if (!_isFloppy || !isLoaded())
return nullptr;
const byte *gd = _data.data() + _galleryOffset;
@@ -539,11 +456,9 @@ const byte *Mystery::floppySuspectEntry(uint suspectIdx) const {
}
bool Mystery::isGuilty(uint suspectIdx) const {
- // `_WITCH @ 1df2:089f` (CD): `if (GalleryData[i*0x46 + 0x02] == -1)
- // _DisplayCorrect(); else _DisplayAlibi(...)`. Innocent suspects
- // store their alibi-text TextBlock offset at +0x02; the guilty
- // one stores the sentinel 0xFFFF. Floppy uses the same convention
- // at suspect entry +2..3 but with variable-stride entries.
+ // _WITCH @ 1df2:089f (CD): GalleryData[i*0x46 + 0x02] == 0xFFFF marks
+ // guilty; innocent suspects store their alibi TextBlock offset there.
+ // Floppy uses same convention at suspect entry +2..3 (variable stride).
if (_isFloppy) {
const byte *e = floppySuspectEntry(suspectIdx);
return e && READ_LE_UINT16(e + 2) == 0xFFFF;
@@ -557,13 +472,9 @@ bool Mystery::isGuilty(uint suspectIdx) const {
uint16 Mystery::alibiTextOffset(uint suspectIdx) const {
if (_isFloppy) {
- // Floppy alibi: u16 at suspect +2..3 carries TWO things:
- // 0xFFFF = guilty, otherwise the HIGH BYTE indexes the
- // TEXT_BLOCK table (header[+0xc]), each entry u16 = absolute
- // alibi-text offset in the buffer. Verified at
- // `_DisplayAlibi_Floppy @ 1d40:0145` reading
- // `*(int *)(textBlock + ((byte *)entry)[3] * 2)`. The result
- // is an ABSOLUTE offset; caller reads via `blobAt(off)`.
+ // Floppy alibi (_DisplayAlibi_Floppy @ 1d40:0145): u16 at suspect +2..3.
+ // 0xFFFF = guilty; else high byte indexes the TEXT_BLOCK table at
+ // header[+0xc], each entry u16 = absolute alibi-text offset.
const byte *e = floppySuspectEntry(suspectIdx);
if (!e)
return 0xFFFF;
@@ -583,10 +494,8 @@ uint16 Mystery::alibiTextOffset(uint suspectIdx) const {
}
const byte *Mystery::hintBlock() const {
- // Header word at index 9 (`_hintOffset`) â used by `_KDHelp @
- // 1560:010a`'s per-chain-clue hint table. Each pair-of-bytes is
- // a TextBlock offset for the corresponding `_AChain` entry, or
- // `0xFFFF` if no hint is defined for that chain position.
+ // Header word[9] _hintOffset (_KDHelp @ 1560:010a). Each u16 is a
+ // TextBlock offset for the corresponding _AChain entry, or 0xFFFF.
if (!isLoaded() || _hintOffset == 0 || _hintOffset >= _data.size())
return nullptr;
return _data.data() + _hintOffset;
@@ -604,10 +513,8 @@ int Mystery::selectedPoints() const {
if (!ni || cnt == 0)
return 0;
if (_isFloppy) {
- // Floppy `_GetSelectedPoints_Floppy @ 1d40:0c23`: collect the
- // per-note score (note +6 byte) for every clue with TextSeen
- // set, sort descending, sum the top 5. Mirrors
- // `_SortNotesByScore_Floppy @ 1d40:000e` + the top-5 fold.
+ // _GetSelectedPoints_Floppy @ 1d40:0c23: per-note score (note +6)
+ // for each TextSeen clue, sort descending, sum top 5.
uint8 scores[Mystery::kCluesFoundCap] = {};
uint scoreCount = 0;
const uint maxIdx = MIN<uint>(cnt, kCluesFoundCap);
@@ -616,7 +523,7 @@ int Mystery::selectedPoints() const {
continue;
scores[scoreCount++] = ni[i * 7 + 6];
}
- // Partial selection sort for top 5.
+ // Partial selection sort for the top 5 scores.
const uint topN = MIN<uint>(5u, scoreCount);
for (uint k = 0; k < topN; k++) {
uint best = k;
@@ -639,7 +546,7 @@ int Mystery::selectedPoints() const {
for (uint i = 0; i < cnt && i < kCluesFoundCap; i++) {
if (!_noteSelected[i])
continue;
- // Each NoteIndex entry is 4 bytes: u16 textOff + u16 points.
+ // CD NoteIndex entry: 4 bytes = u16 textOff + u16 points (at +2).
const uint16 pts = READ_LE_UINT16(ni + i * 4 + 2);
total += (int)(int16)pts;
}
diff --git a/engines/eem/mystery.h b/engines/eem/mystery.h
index 8ef1c584715..07545cf6e23 100644
--- a/engines/eem/mystery.h
+++ b/engines/eem/mystery.h
@@ -30,31 +30,23 @@
namespace EEM {
-/**
- * One mystery (case file) loaded from `M<n>.BIN`.
- *
- * Mirrors the layout established by `_ReadMystery` @ 2404:008f:
- *
- * word[0] = InitBlock byte offset
- * word[2] = MapData byte offset
- * word[3] = SiteIndex byte offset
- * word[4] = TextBlock byte offset
- * word[5] = NoteIndex byte offset
- * word[6] = GalleryData byte offset
- * word[7] = KDTextIndex byte offset
- * word[8] = SolvedClues byte offset
- * word[9] = HintBlock byte offset
- * word[10] = NumSites + start of MysteryStats
- * word[13] = NumSuspects (low byte)
- * word[14] = NumCONSITEs
- * word[15] = NumCOFFSITEs
- * word[16..20] = AChain (5 words)
- * word[21..25] = BChain
- * word[26..30] = CChain
- *
- * Per-mystery state is reset every time a mystery is loaded; chains and
- * indices are pointers into the in-memory `_data` blob.
- */
+/// Mystery file M<n>.BIN. CD header layout from _ReadMystery @ 2404:008f:
+/// word[0] InitBlock offset
+/// word[2] MapData offset
+/// word[3] SiteIndex offset
+/// word[4] TextBlock offset
+/// word[5] NoteIndex offset
+/// word[6] GalleryData offset
+/// word[7] KDTextIndex offset
+/// word[8] SolvedClues offset
+/// word[9] HintBlock offset
+/// word[10] NumSites
+/// word[13] NumSuspects (low byte)
+/// word[14] NumCONSITEs
+/// word[15] NumCOFFSITEs
+/// word[16..20] AChain (5 words)
+/// word[21..25] BChain
+/// word[26..30] CChain
class Mystery {
public:
static const uint kNumChains = 3;
@@ -67,14 +59,11 @@ public:
Mystery() = default;
~Mystery() = default;
- /// Load `M<num>.BIN` and reset per-mystery state. Returns false on error.
+ /// Load M<num>.BIN and reset per-mystery state. Returns false on error.
bool load(uint num, class Common::RandomSource *rng = nullptr);
- /// Drop the loaded mystery and zero per-mystery state. Safe to call
- /// at any time; `isLoaded()` returns false afterward.
void clear();
- /// True once `load()` succeeded and offsets are valid.
bool isLoaded() const { return !_data.empty(); }
uint number() const { return _number; }
@@ -83,120 +72,93 @@ public:
uint8 numCONSITEs() const { return _numCONSITEs; }
uint8 numCOFFSITEs() const { return _numCOFFSITEs; }
- /// Pointer to the InitBlock (case briefing).
- /// _InitBlock @ 2d5d:?? = mystery + word[0] in `_ReadMystery`.
+ /// InitBlock (case briefing) at mystery + word[0].
const byte *initBlock() const;
- /// Pointer to the GalleryData; one 0x46-byte entry per suspect.
- /// First u16 of each entry is the PIC picture ID for that suspect.
+ /// GalleryData: 0x46-byte entry per suspect; first u16 = PIC picture ID.
const byte *galleryData() const;
/// Floppy variable-stride suspect record. Returns nullptr on CD or
- /// when @p suspectIdx is out of range. Walks the gallery section
- /// (`5 + nameLen` bytes per suspect) to land on the requested entry.
- /// Layout: u16 picID, u16 alibiMarker (0xFFFF = guilty),
+ /// out-of-range. Layout: u16 picID, u16 alibiMarker (0xFFFF=guilty),
/// u8 nameLen, nameLen bytes of name.
const byte *floppySuspectEntry(uint suspectIdx) const;
- /// Pointer to the NoteIndex array (4 bytes per entry: u16 textOff + u16 pts).
+ /// NoteIndex array (4 bytes per entry: u16 textOff + u16 pts).
const byte *noteIndex() const;
- /// Number of entries in NoteIndex.
uint16 noteIndexCount() const;
- /// True when @p clueId has a visible notebook/accuse text entry.
- /// Floppy dialog text indices may be spoken-only records with a
- /// zero notebook text offset; those are marked seen but skipped by
- /// `_DrawNotes_Floppy`.
+ /// True when clueId has a notebook text entry. Floppy dialog records
+ /// may be spoken-only with zero notebook offset, skipped by _DrawNotes_Floppy.
bool noteHasNotebookText(uint clueId) const;
- /// Pointer to the KDTextIndex; first u16s are TextBlock offsets for
- /// host hint lines.
+ /// KDTextIndex; first u16s are TextBlock offsets for host hint lines.
const byte *kdTextIndex() const;
- /// Pointer to the HintBlock; per-clue hint TextBlock offsets indexed
- /// by `_aChain[i]` (the Nth required clue). Mirrors the
- /// `_HintBlock` global read in `_KDHelp @ 1560:010a`.
+ /// HintBlock (_KDHelp @ 1560:010a). Per-clue hint TextBlock offsets
+ /// indexed by _aChain[i].
const byte *hintBlock() const;
- /// Read entry @p i from `_aChain` (the required-clue chain). Returns
- /// 0xFFFF when no entry exists. Used by `_KDHelp` to walk unfound
- /// clues for hints.
+ /// Entry @p i of the required-clue chain. Returns 0xFFFF when out of
+ /// range. Walked by `_KDHelp` to find unfound clues for hints.
uint16 aChain(uint i) const {
return i < kChainLen ? _aChain[i] : 0xFFFF;
}
- /// Pointer to the MapData entry for site @p siteNum (14 bytes per
- /// entry; first u16 = sitepic, +4..7 = (x, y) on the big map).
+ /// MapData entry for siteNum: 14 bytes; first u16 = sitepic, +4..7 = (x, y).
const byte *mapEntry(uint siteNum) const;
- /// Pointer to the SiteIndex entry for site @p siteNum (6 bytes per site).
+ /// SiteIndex entry for siteNum (6 bytes per site on CD).
const byte *siteIndexEntry(uint siteNum) const;
- /// Pointer to the SiteData (sitepic, travel, hotspot count, ...)
- /// referenced by SiteIndex[@p siteNum].
+ /// SiteData (sitepic, travel, hotspot count, ...) per SiteIndex[siteNum].
const byte *siteData(uint siteNum) const;
- /// Floppy-only pointer to the matching `ANI.BIN` per-site animation
- /// block. Layout: u8 cycleCount, cycleCount à {u8 start, u8 end},
- /// u8 animCount, animCount à {u8 animId, u16 x, u8 y}.
+ /// Floppy ANI.BIN per-site animation block. Layout:
+ /// u8 cycleCount, cycleCount x {u8 start, u8 end},
+ /// u8 animCount, animCount x {u8 animId, u16 x, u8 y}.
const byte *floppySiteAnimData(uint siteNum) const;
- /// Pointer to the hotspot rectangle array for site @p siteNum.
- /// Each rect is 14 bytes: x1, y1, x2, y2, then 6 bytes of clue data.
+ /// Hotspot rectangle array for siteNum (14 bytes each: x1,y1,x2,y2 + clue).
const byte *hotspots(uint siteNum) const;
- /// Number of hotspots in site @p siteNum.
uint16 hotspotCount(uint siteNum) const;
- /// Pointer to a NUL-terminated string at TextBlock+ at p offset.
+ /// NUL-terminated string at TextBlock + offset.
const char *textAt(uint16 offset) const;
- /// Pointer at byte offset @p offset within the mystery blob, or null
- /// if out of range. Used to chase ClueBlock pointers stored in
- /// hotspot data.
+ /// Pointer at byte @p offset within the mystery blob, or null if out
+ /// of range. Used to chase ClueBlock pointers stored in hotspot data.
const byte *blobAt(uint32 offset) const {
return offset < _data.size() ? _data.data() + offset : nullptr;
}
- /// Total mystery blob size in bytes (for bounds checks).
uint32 dataSize() const { return (uint32)_data.size(); }
- /// Synchronize the per-mystery runtime state for save/load. The fixed
- /// arrays serialize first, then the booleans and counters.
void syncState(Common::Serializer &s);
- /// Sum of point values of every selected notebook entry. Mirrors
- /// `_GetSelectedPoints` @ 1df2:00bd.
+ /// _GetSelectedPoints @ 1df2:00bd.
int selectedPoints() const;
- /// Sum of the top five point values among found notebook entries.
- /// Mirrors `_GetFoundPoints` @ 1df2:0098.
+ /// _GetFoundPoints @ 1df2:0098 â sum of top 5 found notebook entries.
int foundPoints() const;
- /// True when `selectedPoints() > 99`. Mirrors `_SolvedCheck`.
bool solvedCheck() const { return selectedPoints() > 99; }
- /// True iff suspect @p suspectIdx is the case's guilty party. The
- /// guilty marker is `GalleryData[suspectIdx * 0x46 + 0x02] ==
- /// 0xFFFF` â innocent suspects store their alibi text offset there;
- /// the guilty suspect uses the sentinel. Verified at `_WITCH @
- /// 1df2:089f` (`if (psVar1->field_0x2 == -1) _DisplayCorrect();
- /// else _DisplayAlibi(...)`).
+ /// _WITCH @ 1df2:089f. GalleryData[i*0x46 + 0x02] == 0xFFFF marks the
+ /// guilty suspect; innocent suspects store their alibi TextBlock offset.
bool isGuilty(uint suspectIdx) const;
- /// TextBlock offset of suspect @p suspectIdx's alibi text. Returns
- /// 0xFFFF for the guilty suspect (no alibi).
+ /// TextBlock offset of suspect's alibi text. 0xFFFF for guilty suspect.
uint16 alibiTextOffset(uint suspectIdx) const;
- /// Pointer to the win-clueblock (`MysteryIndex[+0x10]` =
- /// `_solvedOffset`). Mirrors `_DisplayCorrect`'s
- /// `_DisplayClue(_Mystery + MysteryIndex[+0x10], 0)` at 1df2:0769.
+ /// Win-clueblock at `MysteryIndex[+0x10]` = `_solvedOffset`. Used by
+ /// `_DisplayCorrect` @ 1df2:0769 (`_DisplayClue(_Mystery + MysteryIndex[+0x10], 0)`).
const byte *solvedClueBlock() const;
/// Per-mystery runtime state, zeroed at load time.
uint8 _cluesFound[kCluesFoundCap] = {};
- uint8 _noteSelected[kCluesFoundCap] = {}; ///< Mirror `_NoteSelected`
+ uint8 _noteSelected[kCluesFoundCap] = {}; ///< _NoteSelected
uint16 _hotSpotsSeen[kHotSpotsCap] = {};
uint16 _inGallery[kGalleryCap] = {};
uint8 _newOrder[kGalleryCap] = {};
@@ -234,19 +196,15 @@ private:
uint16 _bChain[kChainLen] = {};
uint16 _cChain[kChainLen] = {};
- // Floppy variant uses a completely different header (see comment
- // in `Mystery::load`). When `_isFloppy` is true, the CD-shaped
- // `_initOffset / _siteIndexOffset / etc.` fields are unset and the
- // floppy section pointers below are populated from the floppy
- // header offsets verified at `_ReadMystery_Floppy @ 22dc:0178`.
+ // Floppy variant â see Mystery::load. _ReadMystery_Floppy @ 22dc:0178.
bool _isFloppy = false;
- uint16 _floppySuspectsOff = 0; ///< header[+4] â suspects
- uint16 _floppyHintBlockOff = 0; ///< header[+6] â hintâclue table
- uint16 _floppyNoteIndexOff = 0; ///< header[+8] â notes (7B/clue)
- uint16 _floppyGalleryOff = 0; ///< header[+0xa] â gallery portraits
- uint16 _floppyTextOff = 0; ///< header[+0xc] â text block
- uint16 _floppyKDTextOff = 0; ///< header[+0x10] â KDTextIndex
- uint16 _floppySolvedOff = 0; ///< header[+0x12] â solved clue chain
+ uint16 _floppySuspectsOff = 0; ///< header[+4] suspects
+ uint16 _floppyHintBlockOff = 0; ///< header[+6] hint -> clue table
+ uint16 _floppyNoteIndexOff = 0; ///< header[+8] notes (7B/clue)
+ uint16 _floppyGalleryOff = 0; ///< header[+0xa] gallery portraits
+ uint16 _floppyTextOff = 0; ///< header[+0xc] text block
+ uint16 _floppyKDTextOff = 0; ///< header[+0x10] KDTextIndex
+ uint16 _floppySolvedOff = 0; ///< header[+0x12] solved clue chain
Common::Array<byte> _floppySiteAnimData;
uint16 _floppySiteAnimSiteOff[kVisitedSiteCap] = {};
diff --git a/engines/eem/resource.cpp b/engines/eem/resource.cpp
index a08ce19f85f..deb49700d57 100644
--- a/engines/eem/resource.cpp
+++ b/engines/eem/resource.cpp
@@ -53,7 +53,7 @@ bool DBDArchive::open(const Common::Path &dbdName, const Common::Path &dbxName)
return false;
}
- // _InitGraphicsSystem @ 172b:0145 reads 10 bytes per entry until EOF.
+ // _InitGraphicsSystem @ 172b:0145: 10-byte entries until EOF.
const int32 dbxSize = dbx.size();
_index.reserve(dbxSize / 10);
while (dbx.pos() + 10 <= dbxSize) {
@@ -76,10 +76,8 @@ void DBDArchive::close() {
_index.clear();
}
-/**
- * Read one 12-byte frame header + payload at the current stream position.
- * Shared between picture and animation loaders since the layout is the same.
- */
+/// Read one 12-byte frame header + payload at the current stream position.
+/// Shared between picture and animation loaders since the layout is identical.
bool readFrame(Common::SeekableReadStream &stream, bool compressed, Picture &out) {
out.flags = stream.readUint16LE();
const uint16 height = stream.readUint16LE();
@@ -116,11 +114,10 @@ bool readFrame(Common::SeekableReadStream &stream, bool compressed, Picture &out
bool DBDArchive::loadEntry(uint num, Picture &out) {
if (num >= _index.size()) {
// Out-of-range picture IDs are non-fatal â every caller already
- // checks the return value (e.g., `haveDone`/`haveCrime` in
- // `drawBigMapOverview`). The floppy's PICS.DBD ships fewer
- // entries than the CD (e.g., the BigMap done-marker `0x20D` is
- // CD-only), so this fires routinely on floppy and shouldn't
- // be a `warning`.
+ // checks the return value (e.g. `haveDone` / `haveCrime` in
+ // `drawBigMapOverview`). Floppy PICS.DBD ships fewer entries than
+ // CD (e.g. BigMap done-marker 0x20D is CD-only), so this fires
+ // routinely on floppy and stays at debug level rather than warning.
debugC(2, kDebugGfx,
"DBDArchive::loadEntry: %u out of range (max %u)",
num, (uint)_index.size());
@@ -133,8 +130,7 @@ bool DBDArchive::loadEntry(uint num, Picture &out) {
return false;
}
- // Mirrors _GetFromDB @ 172b:105d. The 2-byte word read first matches
- // loadAnimation's frame-count read; for picture entries it is always 1.
+ // _GetFromDB @ 172b:105d. Leading u16 = frame count (always 1 for pictures).
(void)_dbd.readUint16LE();
return readFrame(_dbd, entry.compressed != 0, out);
}
@@ -151,7 +147,7 @@ bool DBDArchive::loadAnimation(uint num, Animation &out) {
return false;
}
- // Mirrors _GetAnimation @ 172b:163a: u16 frame count, then N frames.
+ // _GetAnimation @ 172b:163a: u16 frame count, then N frames.
const uint16 frameCount = _dbd.readUint16LE();
if (frameCount == 0 || frameCount > 256) {
warning("DBDArchive::loadAnimation: %u has implausible frame count %u",
diff --git a/engines/eem/resource.h b/engines/eem/resource.h
index 846bf0f1135..a984f323d35 100644
--- a/engines/eem/resource.h
+++ b/engines/eem/resource.h
@@ -31,79 +31,47 @@
namespace EEM {
-/**
- * Index entry for a .DBD/.DBX archive pair (10 bytes on disk).
- *
- * Mirrors the original `dbi` struct read by _InitGraphicsSystem @ 172b:0145
- * in 10-byte chunks until EOF. Each entry locates one resource blob in the
- * companion .DBD container.
- */
+/// dbi struct from _InitGraphicsSystem @ 172b:0145 (10 bytes per entry).
struct DBEntry {
- uint32 offset; ///< Byte offset of the entry in the .DBD file.
- uint16 compressed; ///< Non-zero if the payload is PKWARE DCL ("Implode") packed.
- uint32 size; ///< Total size of the entry on disk (including 14-byte header).
+ uint32 offset; ///< Byte offset in the .DBD file.
+ uint16 compressed; ///< Non-zero = PKWARE DCL ("Implode") packed payload.
+ uint32 size; ///< Total entry size on disk (incl. 14-byte header).
};
-/**
- * 8-bit indexed picture decoded from a .DBD entry.
- *
- * The original engine's PicData is a 16-byte struct; we keep the descriptive
- * fields here and let `Graphics::ManagedSurface` own the pixel data so the
- * rest of the engine can blit/scale/clip with the standard API.
- */
+/// 8-bit indexed picture decoded from a .DBD entry. Original PicData is
+/// 16 bytes; pixels live in Graphics::ManagedSurface.
struct Picture {
- uint16 flags = 0; ///< +0 high byte = sub-mode used by some sprites
- uint16 rowoff = 0; ///< +6 row offset (used by some clipped sprites)
- uint16 miscflags = 0; ///< +8 high byte = transparent-mask flag
+ uint16 flags = 0; ///< +0 high byte: sub-mode
+ uint16 rowoff = 0; ///< +6 row offset (clipped sprites)
+ uint16 miscflags = 0; ///< +8 high byte: transparent-mask flag
uint16 compsize = 0; ///< +10 packed payload size on disk
Graphics::ManagedSurface surface;
};
-/// Multi-frame animation as stored in ANI.DBD â a sequence of Pictures.
+/// Multi-frame animation from ANI.DBD.
typedef Common::Array<Picture> Animation;
-/**
- * Reader for a .DBD + .DBX archive pair.
- *
- * The original engine has five such pairs: PICS, SITES, ANI, BALLOON, BUTTON.
- * Each .DBX is parsed once into an in-memory `_index`; reads of individual
- * entries seek into the .DBD on demand and (when flagged) decompress with
- * `Common::decompressDCL`.
- */
+/// .DBD + .DBX archive pair (PICS, SITES, ANI, BALLOON, BUTTON).
class DBDArchive {
public:
DBDArchive();
~DBDArchive();
- /**
- * Open both halves of an archive. @p dbdName / @p dbxName are looked up
- * via SearchMan, so case is normalized for us. Returns false if either
- * file is missing or the index is malformed.
- */
+ /// Open both halves; returns false if either file is missing/malformed.
bool open(const Common::Path &dbdName, const Common::Path &dbxName);
void close();
- /** Number of entries in the index. */
uint32 size() const { return _index.size(); }
- /**
- * Load entry @p num (0-based index), decompressing if needed.
- * Returns true on success. Mirrors _GetFromDB @ 172b:105d.
- */
+ /// Load entry num (0-based), decompressing if needed.
+ /// _GetFromDB @ 172b:105d.
bool loadEntry(uint num, Picture &out);
- /**
- * Convenience wrapper that mirrors the engine's 1-based picture API:
- * `_GetPicture(num)` calls `_GetFromDB(..., num - 1)`. Use this when
- * porting code that references picture IDs by their original number.
- */
+ /// 1-based wrapper matching the engine's _GetPicture(num) -> _GetFromDB(num - 1).
bool getPicture(uint num, Picture &out) { return loadEntry(num - 1, out); }
- /**
- * Load a multi-frame animation entry. Mirrors _GetAnimation @ 172b:163a:
- * read u16 frameCount, then for each frame read a 12-byte header and
- * decompress the payload. Used for ANI.DBD entries.
- */
+ /// Multi-frame entry. _GetAnimation @ 172b:163a: u16 frameCount,
+ /// then per frame a 12-byte header + payload.
bool loadAnimation(uint num, Animation &out);
private:
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index f309a2d76bc..167f9d0d1cd 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -39,28 +39,20 @@ constexpr Common::Rect kSitePdaRect(Common::Point(35, 111), 21, 25);
constexpr Common::Rect kSitePartnerFootMapRect(Common::Point(7, 177), 50, 23);
constexpr Common::Rect kSitePartnerHeadHintRect(Common::Point(5, 80), 39, 30);
-// Masked blit a Picture into a ManagedSurface. Pixels equal to `transp`
-// (the high byte of `pic.flags`, per `_Rect_Move_Mask @ 1000:03fc`) are
-// skipped. Used by `enterSiteAnim` for both skateboard + KD slide-in
-// passes; the surface is the in-memory frame buffer that gets pushed
-// to the screen each tick.
+// Masked blit using `transp` = high byte of `pic.flags` (`_Rect_Move_Mask @ 1000:03fc`).
void blitFrame(Graphics::ManagedSurface &dst, const Picture &p,
int x, int y, byte transp) {
dst.transBlitFrom(p.surface, Common::Point(x, y), (uint32)transp);
}
-// Mask-aware blit from a Picture into a `Graphics::Surface` (the
-// locked framebuffer). Same pixel-mask semantics as `blitFrame`.
-// Used by hotspot/NPC rendering inside `SiteScreen::renderHotspots`
-// and `renderStaticDrops`.
+// Top-left masked blit. `_AddDrop @ 172b:1a77` calls
+// `_Rect_Move_Mask(..., x, y, ...)` with the raw (x, y) and IGNORES
+// per-frame anchor offsets â so this is the correct path for static
+// drops and any non-animated overlay. Animations must route through
+// `blitAnimFrameAnchored` instead so per-frame anchor offsets
+// (miscflags = X, rowoff = Y) apply correctly.
void blitMaskedSurface(Graphics::Surface *screen, const Picture &p,
int x, int y) {
- // Top-left semantics. Used for static drops (`_AddDrop @
- // 172b:1a77` which calls `_Rect_Move_Mask(..., x, y, ...)` with
- // the raw (x, y) and ignores per-frame anchors) and any other
- // non-animated overlay. Animation rendering should go through
- // `blitAnimFrameAnchored` instead so per-frame anchor offsets
- // (miscflags = X, rowoff = Y) apply correctly.
if (!screen)
return;
const byte transp = (byte)(p.flags >> 8);
@@ -82,17 +74,10 @@ void blitMaskedSurface(Graphics::Surface *screen, const Picture &p,
void blitAnimFrameAnchored(Graphics::Surface *screen, const Picture &p,
int anchorX, int anchorY) {
- // `_UpdateAnimations @ 172b:09c1` blits each animation frame at
- // `(anchor_x - puVar5[4], anchor_y - puVar5[3])` where puVar5[3]/[4]
- // are the per-frame `rowoff` / `miscflags` values from the
- // 16-byte PicData header. Both are SIGNED 16-bit anchor offsets
- // â when frames have varying anchors (anim 0x14 BigMap walk-
- // cycle has miscflags = -2 per cell, anim 0x07 has rowoff up to
- // 61), the sprite actually translates across the screen as it
- // cycles through cells. Without this, the partner "shakes in
- // place" instead of walking. (Transparency still comes from
- // `flags >> 8`, verified at the `_Rect_Move_Mask(..., *thePic >>
- // 8)` call â NOT from miscflags as an earlier comment claimed.)
+ // `_UpdateAnimations @ 172b:09c1`: blit at
+ // (anchor_x - puVar5[4], anchor_y - puVar5[3])
+ // where puVar5[3]/[4] are per-frame rowoff/miscflags (signed int16)
+ // from the 16-byte PicData header. Transparency = flags >> 8.
if (!screen)
return;
const int blitX = anchorX - (int)(int16)p.miscflags;
@@ -114,9 +99,11 @@ void blitAnimFrameAnchored(Graphics::Surface *screen, const Picture &p,
}
}
-// Rotate one VGA palette range by one slot. Mirrors `_ColorCycle @
-// 172b:2015` â used by both the per-site Loop-1 ColorCycle entries and
-// the always-on hotspot marching-ants range 0xF9..0xFE.
+// `_ColorCycle @ 172b:2015` â rotate `_fpal[start..end]` by one slot:
+// save [start], shift [start..end-1] = [start+1..end], restore saved at
+// [end], then re-upload via `_Set_Palette`. We do the same against
+// ScummVM's palette manager. Used by per-site Loop-1 ColorCycle entries
+// and the always-on hotspot marching-ants range 0xF9..0xFE.
void cyclePaletteRange(uint8 start, uint8 end) {
if (end <= start)
return;
@@ -138,11 +125,10 @@ void cyclePaletteRange(uint8 start, uint8 end) {
}
void cyclePaletteRangeReverse(uint8 start, uint8 end) {
- // Mirrors `_OpenColorCycle @ 2520:04f7`: save the END color, shift
- // every entry up by one (END-1 â END, ...), wrap saved END to
- // START. Visually colors march from start toward end â opposite
- // direction from `cyclePaletteRange`. Used by the EA Kids /
- // HighScore logo cycles.
+ // `_OpenColorCycle @ 2520:04f7`: save END, shift every entry up by
+ // one (END-1 â END, ...), wrap saved END to START. Visually colors
+ // march from start toward end â opposite direction from
+ // `cyclePaletteRange`. Used by the EA Kids / HighScore logo cycles.
if (end <= start)
return;
const uint count = (uint)end - (uint)start + 1;
@@ -163,13 +149,11 @@ void cyclePaletteRangeReverse(uint8 start, uint8 end) {
g_system->getPaletteManager()->setPalette(buf, start, count);
}
-// Per-speaker partner-position table verified against `_WaitAnims @
-// 29be:021c`. 12 bytes per entry, indexed by `siteData[+8]`. Layout:
+// `_WaitAnims @ 29be:021c`. 12 bytes per entry, indexed by `siteData[+8]`:
// +0..1 anim Jake, +2..3 anim Jenny,
// +4..5 x Jake, +6..7 x Jenny,
// +8..9 y Jake, +10..11 y Jenny.
-// Seven valid entries â anything past entry 6 in the binary is
-// `_SiteButtons` rect data that follows the table in memory.
+// 7 entries; past entry 6 is `_SiteButtons` rect data.
const uint16 kWaitAnims[7][6] = {
{ 0x00, 0x0a, 0x06, 0x06, 0x50, 0x50 }, // 0
{ 0x03, 0x0c, 0x06, 0x06, 0x50, 0x50 }, // 1
@@ -180,9 +164,8 @@ const uint16 kWaitAnims[7][6] = {
{ 0x06, 0x06, 0x06, 0x06, 0x50, 0x50 }, // 6
};
-// `_DoKDAnim` lookup table. Six valid kdAnimNum entries (0..5)
-// verified from `29be:0228`. Layout per entry: { animJake, animJenny,
-// xJake, xJenny, yJake, yJenny }. Position is (6, 80) in every entry.
+// `_DoKDAnim` table @ 29be:0228. 6 entries (kdAnimNum 0..5).
+// Layout: { animJake, animJenny, xJake, xJenny, yJake, yJenny }.
const uint16 kKdAnimTable[6][6] = {
{ 0x03, 0x0c, 6, 6, 80, 80 }, // 0 â speaker idx 1 wait anim
{ 0x01, 0x0b, 6, 6, 80, 80 }, // 1 â same as PDA idle
@@ -192,29 +175,27 @@ const uint16 kKdAnimTable[6][6] = {
{ 0x06, 0x06, 6, 6, 80, 80 }, // 5 â same anim both partners
};
-// Animation script table. Mirrors `_AnimationSequences @ 29be:22d4`
-// (a 55-entry table of far ptrs, each pointing to a u16-frame-index
-// stream terminated by 0x80; 0x81 marks a jump that we don't see in
-// the partner subset and so don't yet implement).
-//
-// `_NewAnimation @ 172b:06e1` reads the script via
+// Animation script table â mirrors `_AnimationSequences @ 29be:22d4`
+// (55-entry table of far ptrs, each pointing to a u16-frame-index
+// stream). `_NewAnimation @ 172b:06e1` reads the script via
// `_AnimationSequences[anim_id]` and stores the pointer in
-// `DAT_2d5d_3eaf[i*0xb]`. `_UpdateAnimations @ 172b:09c1` then walks
-// it one entry per `_CheckFrameRate` tick (~100 ms): the value at
-// `script[index]` is the frame to render; 0x80 resets index to 0
-// (loop). So a script like `[0,0,0,0,0,0,0,0,0,2]` renders nine ticks
-// of frame 0 then one tick of frame 2 â the natural "blink with long
-// idle hold" cadence.
+// `DAT_2d5d_3eaf[i*0xb]`; `_UpdateAnimations @ 172b:09c1` then walks it
+// one entry per `_CheckFrameRate` tick (~140 ms).
//
-// We use the same scripts for the wait anims (`renderPartner`) AND
-// the kd-clue reaction anims (`playKdAnim`), since both call
-// `_NewAnimation` in the original â only the state field differs (1
-// = looping, 4 = one-shot). seqnum == animId per `_PlayAnimation`
-// 172b:1f5d push order.
+// Script byte format:
+// 0x80 = restart (loop back to index 0; terminator for one-shots)
+// 0x81 N = jump to byte N (not used in partner subset)
+// other = frame index to render this tick
//
-// Each entry was read directly from the EXE via Ghidra; cross-checked
-// against `_NewAnimation`'s read. Frame counts include only the
-// playable frames (the trailing 0x80 is the terminator, not a frame).
+// Repeated frames are the original's "frame-hold" mechanism: per-tick
+// walk advances exactly one entry, so K repeats hold the frame for
+// K * `kFramePeriodMs` â K * 140 ms (e.g. [0,0,0,0,0,0,0,0,0,2] â
+// nine ticks of frame 0, one tick of frame 2 = "blink with long
+// idle hold"). Same scripts serve wait anims (looping) and kd-clue
+// reactions (state-4 one-shot â see `_PlayAnimation`); state field
+// in the slot differentiates them. `seqnum == animId` per
+// `_PlayAnimation @ 172b:1f5d` push order.
+// Length counts only playable frames; trailing 0x80 is not a frame.
struct AnimScript {
uint16 seqnum;
uint8 len;
@@ -225,9 +206,7 @@ const AnimScript kAnimScripts[] = {
{ 0x00, 10, { 0,0,0,0,0,0,0,0,0,2 } },
// 0x01 (29be:188a) â Jake PDA idle: alternating head bob with peak
{ 0x01, 15, { 0,1,2,0,1,0,2,1,0,1,0,1,2,1,0 } },
- // 0x02 (29be:18aa) â Jake gallery: brief wave, long hold, second
- // wave, hold (CONFIRMED 26 frames â earlier table was truncated to
- // 16 which dropped the second wave cycle).
+ // 0x02 (29be:18aa) â Jake gallery: brief wave, long hold, second wave, hold.
{ 0x02, 26, { 0,1,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,1,0,0,0,0,0,0 } },
// 0x03 (29be:18e0) â Jake "lift, hold, lower" gesture
{ 0x03, 9, { 0,1,2,3,2,2,2,1,0 } },
@@ -235,8 +214,7 @@ const AnimScript kAnimScripts[] = {
{ 0x04, 13, { 0,1,2,3,4,5,4,4,4,3,2,1,0 } },
// 0x05 (29be:1910) â Jake/Jenny shared (speaker 5): held idle, peak
{ 0x05, 13, { 0,0,0,1,2,3,2,1,0,0,0,0,0 } },
- // 0x06 (29be:192c) â speaker 6 partner: empty (immediate END,
- // renders nothing â verified at 29be:192c byte 0 = 0x80)
+ // 0x06 (29be:192c) â empty (byte 0 = 0x80, immediate END).
{ 0x06, 0, { 0 } },
// 0x07 (29be:192e) â Jake walk-cycle (10 frames: 0..9)
{ 0x07, 10, { 0,1,2,3,4,5,6,7,8,9 } },
@@ -256,33 +234,21 @@ const AnimScript kAnimScripts[] = {
{ 0x0e, 0, { 0 } },
// 0x0f â alias of 0x09
{ 0x0f, 9, { 0,0,0,1,0,0,0,0,0 } },
- // 0x10 â Jenny gallery wait (alias of 0x09 â verified at 29be:1956)
+ // 0x10 (29be:1956, alias of 0x09) â Jenny gallery wait.
{ 0x10, 9, { 0,0,0,1,0,0,0,0,0 } },
- // 0x11 (29be:1992) â Jenny entrance count-up: 0..7. Used by
- // `_DoBigMap` when `_LastScreen == 2` (BigMap entrance one-shot).
+ // 0x11 (29be:1992) â Jenny BigMap entrance count-up 0..7 (one-shot).
{ 0x11, 8, { 0,1,2,3,4,5,6,7 } },
- // 0x12 (29be:197e) â Jake entrance count-down 8..0. Used by
- // `_DoBigMap` (entrance one-shot, partner-specific exit cell).
+ // 0x12 (29be:197e) â Jake BigMap entrance count-down 8..0 (one-shot).
{ 0x12, 9, { 8,7,6,5,4,3,2,1,0 } },
- // 0x13 (29be:1992, alias of 0x11) â Jake walk-cycle 0..7,
- // looped during BigMap idle.
+ // 0x13 (29be:1992, alias of 0x11) â Jake BigMap idle walk-cycle.
{ 0x13, 8, { 0,1,2,3,4,5,6,7 } },
- // 0x14 (29be:196a) â BigMap idle walk-cycle 0..8 (9 cells),
- // partner shifts feet while you pick a site.
+ // 0x14 (29be:196a) â BigMap idle walk-cycle 0..8.
{ 0x14, 9, { 0,1,2,3,4,5,6,7,8 } },
- // 0x15 (29be:185e, alias of 0x00) â Jake CaseSelection greeter:
- // nine idle, one blink, loop. Same blink cadence as the site
- // loop's wait anim (animID 0x00).
+ // 0x15 (29be:185e, alias of 0x00) â Jake CaseSelection greeter.
{ 0x15, 10, { 0,0,0,0,0,0,0,0,0,2 } },
- // 0x16 (29be:185e, alias of 0x00) â Jenny CaseSelection greeter,
- // same blink script as 0x15.
+ // 0x16 (29be:185e, alias of 0x00) â Jenny CaseSelection greeter.
{ 0x16, 10, { 0,0,0,0,0,0,0,0,0,2 } },
- // Site / drop scripts â¤28 frames â see `_AnimationSequences @
- // 29be:22d4`. Many of these are short count-ups used by ambient
- // animations (people walking, vehicles passing) that the original
- // drives one entry per `_CheckFrameRate` tick (~140 ms). Without
- // these, our generic fallback cycles through every animation cell
- // at one entry per tick and the ambient anims look 2-3Ã too fast.
+ // Site / drop scripts â¤28 frames (`_AnimationSequences @ 29be:22d4`).
// 0x1b (29be:192e, alias of 0x07) â walk-cycle 0..9.
{ 0x1b, 10, { 0,1,2,3,4,5,6,7,8,9 } },
// 0x1c (29be:21a8) â short 6-frame count-up.
@@ -312,36 +278,20 @@ const AnimScript kAnimScripts[] = {
{ 0x34, 5, { 0,1,2,3,4 } },
// 0x35 (29be:21b6) â count-up 0..10 (11 frames).
{ 0x35, 11, { 0,1,2,3,4,5,6,7,8,9,10 } },
- // Briefing animations â `_DoInitClues @ 1a35:0411` calls
- // `_NewAnimation(..., (PicData *)CONCAT22(0x17, ...), 1, ...)`
- // for the game animation (always anim ID 0x17 â even Jenny's
- // briefing reuses Jake's SCRIPT, even though the loaded ANI.DBD
- // cells come from her partner-specific entry 0x3b). Same pattern
- // for book (0x18 always) and nancy (0x19 always).
- //
- // AnimScript len was 28 â these scripts overflow that. Bump
- // `frames[]` is fine because we just need to fit 30 frames per
- // briefing entry. We size `frames` to 36 so all five scripts fit
- // (longest is 0x18 at 30 frames).
};
-static_assert(true, "see kAnimScriptsLong below for >28-frame scripts");
-// Scripts longer than 28 frames live here. The lookup in
-// `findAnimScript` checks both arrays. Stored as
-// `(seqnum, len, ptr)` so each script can be any length without
-// bloating every entry â the longest (0x22) runs 115 frames.
+// Scripts longer than 28 frames (>fits-inline limit). `findAnimScript`
+// checks both this array and `kAnimScripts`. Longest is 0x22 (115 frames).
struct AnimScriptLong {
uint16 seqnum;
uint16 len;
const uint8 *frames;
};
-// Briefing animations â `_DoInitClues @ 1a35:0411` calls
-// `_NewAnimation(..., (PicData *)CONCAT22(0x17, ...), 1, ...)` for the
-// game animation (always anim ID 0x17 â even Jenny's briefing reuses
-// Jake's SCRIPT, even though the loaded ANI.DBD cells come from her
-// partner-specific entry 0x3b). Same pattern for book (0x18) / nancy
-// (0x19).
+// Briefing animations â `_DoInitClues @ 1a35:0411` always uses anim ID
+// 0x17 (game) / 0x18 (book) / 0x19 (nancy) regardless of partner; the
+// per-partner ANI.DBD cells come from a separate entry (e.g. 0x3b for
+// Jenny's briefing).
static const uint8 kScript17[] = {
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,
20,21,22,23,24,25,26,27,28,29
@@ -355,17 +305,10 @@ static const uint8 kScript19[] = {
1,2,3,4,5,6,7,8,9,10,11,12
};
-// Site / NPC drop scripts (29be:22d4 entries 0x1a..0x36 minus the
-// short ones that fit in `kAnimScripts`). Many entries deliberately
-// repeat the same frame several times â that's the original's
-// "frame-hold" mechanism (the per-tick walk advances exactly one
-// entry, so K repeats hold the frame for K * `kFramePeriodMs` â
-// K * 140 ms). Without these scripts our generic fallback cycles
-// through every animation cell at one entry per tick, which is the
-// "site animations run too fast" symptom.
-
-// 0x1a (29be:19a4) â count-up 0..7, long idle hold, repeat 1..7,
-// idle, mirror 7..0, idle (77 entries).
+// Site / NPC drop scripts (29be:22d4 entries 0x1a..0x36). Repeated
+// frames are the original's frame-hold mechanism (one entry per tick).
+
+// 0x1a (29be:19a4) â count-up 0..7, idle hold, repeat 1..7, idle, mirror 7..0, idle.
static const uint8 kScript1a[] = {
0,1,2,3,4,5,6,7,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
@@ -552,8 +495,9 @@ const AnimScriptLong kAnimScriptsLong[] = {
// `_PatientSequence` and `_ImpatientSequence` are standalone script
// pointers, not entries in `_AnimationSequences`. CD has the data but
-// never calls the switchers; floppy calls them from `_DoSiteLoop_Floppy`.
-// We intentionally enable the same switch for both builds.
+// never calls the switchers; floppy calls them from `_DoSiteLoop_Floppy`
+// (via `_Switch2Patient` / `_Switch2Impatient`). We intentionally enable
+// the same switch for both builds.
static const uint8 kPatientSequence[] = { 0,0,0,0,0,0,0,0,0,2 };
static const uint8 kImpatientSequence[] = { 0,1,0,1,0,1,0,1,2,1 };
@@ -562,10 +506,6 @@ static const uint8 kImpatientSequence[] = { 0,1,0,1,0,1,0,1,2,1 };
// behavior but makes the feature observable during normal testing.
static const uint32 kImpatienceDelayMs = 60 * 1000;
-// Look up the script for `seqnum`. Returns the frame array + length,
-// or `(nullptr, 0)` if no script is known â caller falls back to
-// flipbook cycling so unknown anims still animate (just without idle
-// holds).
struct AnimScriptRef {
const uint8 *frames;
uint16 len;
@@ -594,12 +534,11 @@ static AnimScriptRef findAnimScript(uint16 seqnum) {
}
// Original frame period from `_InitFrameCounter @ 1a35:01ae`:
-// `LastFrame = (cs_within_hour) + 0xe`, with `cs_within_hour` =
-// `((ti_min * 60) + ti_sec) * 100 + ti_hund` (Borland C `struct time`
-// memory order is min, hour, hund, sec). The `+ 0xe` is 14
-// centiseconds â ~140 ms per frame, matching `_CheckFrameRate @
-// 1a35:0204`. Earlier 100 ms ran the partner / hotspot animations
-// roughly 1.4Ã faster than the original.
+// LastFrame = cs_within_hour + 0xe
+// cs_within_hour = ((ti_min * 60) + ti_sec) * 100 + ti_hund
+// (Borland C `struct time` memory order is min, hour, hund, sec.)
+// `+ 0xe` is 14 centiseconds â ~140 ms per frame, matching
+// `_CheckFrameRate @ 1a35:0204`.
static const uint kFramePeriodMs = 140;
static uint frameFromScriptAtTick(const uint8 *frames, uint len,
@@ -612,17 +551,12 @@ static uint frameFromScriptAtTick(const uint8 *frames, uint len,
}
void auditPartnerAnims(EEMEngine *vm) {
- // Cross-check every registered partner-subset script against the
- // underlying ANI.DBD entry it references. If the script asks for
- // a frame past the anim's actual frame count, the visible result
- // is "missing frames" â that's the user-reported symptom we
- // want to catch and fix here, not paper over with a clamp.
+ // Cross-check every script against the ANI.DBD entry it references;
+ // warn on out-of-range frame requests.
if (!vm)
return;
DBDArchive &ani = vm->getAni();
- // Helper: audit one (id, frames, len) tuple â log a warning if
- // the script asks for a frame the ANI.DBD entry doesn't have.
struct Walker {
static void check(DBDArchive &ani, uint16 id, const uint8 *frames, uint8 len) {
if (len == 0)
@@ -639,10 +573,8 @@ void auditPartnerAnims(EEMEngine *vm) {
maxRequested = frames[j];
if (maxRequested >= a.size()) {
warning("anim 0x%02x: script wants frame %u but ANI.DBD has "
- "only %u â frames will be clamped (verify script "
- "reading from `_AnimationSequences[0x%02x]` against "
- "Ghidra)",
- id, maxRequested, (uint)a.size(), id);
+ "only %u â frames will be clamped",
+ id, maxRequested, (uint)a.size());
} else {
debugC(2, kDebugSite,
"anim 0x%02x: %u cells, script max=%u, len=%u",
@@ -658,20 +590,9 @@ void auditPartnerAnims(EEMEngine *vm) {
Walker::check(ani, kAnimScriptsLong[i].seqnum,
kAnimScriptsLong[i].frames, kAnimScriptsLong[i].len);
- // Per-frame anchor-offset audit. The original `_UpdateAnimations
- // @ 172b:09c1` blits each frame at `(anchor_x - frame.miscflags,
- // anchor_y - frame.rowoff)`. Our `blitMaskedSurface` ignores
- // those offsets â it treats the WaitAnims (anchor_x, anchor_y)
- // as the top-left. That's fine when every frame has
- // `miscflags == 0 && rowoff == 0`; if any frame has a non-zero
- // anchor, the partner sprite jumps between cells. Log the
- // offending IDs so we know whether to plumb anchors through
- // `partnerFrameAtTick` callers.
- // Audit covers every animID we register a script for â the
- // partner-subset (0x00..0x16, used by the wait anims, kd-clue
- // reactions, BigMap, Notebook, Gallery, CaseSelection greeter)
- // AND the briefing-subset (0x17..0x19, the
- // game/book/nancy anims driven by `_DoInitClues @ 1a35:0411`).
+ // Per-frame anchor-offset audit (`_UpdateAnimations @ 172b:09c1`
+ // applies (anchor_x - miscflags, anchor_y - rowoff)). Log non-zero
+ // anchors so we know which anims need `blitAnimFrameAnchored`.
const uint16 partnerIds[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c,
0x0d, 0x0f, 0x10, 0x11, 0x12, 0x13,
@@ -701,10 +622,7 @@ void auditPartnerAnims(EEMEngine *vm) {
}
}
if (anyAnchor) {
- // `_UpdateAnimations @ 172b:09c1` reads these as signed
- // 16-bit values via `puVar5[3]/[4]`, so log them with
- // the sign preserved â earlier the log printed unsigned
- // 65534 instead of -2.
+ // Signed int16 (puVar5[3]/[4] in `_UpdateAnimations`).
debugC(1, kDebugSite,
"anim 0x%02x: per-frame anchor (rowoff [%d..%d], "
"miscflags [%d..%d]) â handled by "
@@ -714,31 +632,27 @@ void auditPartnerAnims(EEMEngine *vm) {
}
}
-// Pick the frame index to render at `tickMs` for the looping
-// animation `seqnum` whose underlying ANI.DBD entry has `numFrames`
-// frames. Mirrors the looping path of `_UpdateAnimations`: walk the
-// script one entry per ~100 ms `_CheckFrameRate` tick, wrap on the
-// 0x80 terminator. Exposed (non-static) so the BigMap, CaseSelection
-// greeter, Notebook, and Gallery render paths in `ui.cpp` can use the
-// same cadence â without this, every off-site partner rendering
-// flipbook-cycles ALL cells of the ANI entry (no idle holds, no
-// timing variations) which is the user-reported "constantly looping"
-// symptom.
+// Looping path of `_UpdateAnimations`: walk the script one entry per
+// `_CheckFrameRate` tick (`kFramePeriodMs` ~= 140 ms), wrap on 0x80.
+// If `seqnum` has no registered script, falls back to flipbook
+// (`tick % numFrames`) so unknown anims still move. The script can in
+// theory request a frame past the asset's actual cell count (misencoded
+// script) â `frameFromScriptAtTick` clamps to `numFrames - 1` so the
+// caller doesn't read past `anim[]`. Exported (non-static) so BigMap,
+// CaseSelection greeter, Notebook, and Gallery render paths in
+// `ui.cpp` use the same cadence.
uint partnerFrameAtTick(uint16 seqnum, uint numFrames, uint32 tickMs) {
const AnimScriptRef s = findAnimScript(seqnum);
- // The script can in theory request a frame that's outside the
- // animation's actual frame count (a misencoded script). Clamp so
- // we don't read past `anim[]` in the caller.
return frameFromScriptAtTick(s.frames, s.len, numFrames, tickMs);
}
-// Generic "play `unfold` once, then loop `waitSeq` forever" walker.
-// Mirrors the original's slot-script-swap idiom: the entrance script
-// runs to its 0x80 terminator, then the slot's script pointer is
-// rewritten to a looping wait sequence (e.g. `_BigMapWaitSeq @
-// 29be:1574`, `_SmallMapWaitSeq @ 29be:1548`). `partnerFrameAtTick`
-// can't model that swap on its own (it always wraps on the same
-// script), hence this helper.
+// Play `unfold` once, then loop `waitSeq` forever. Mirrors the
+// original's slot-script-swap idiom: the entrance script runs to its
+// 0x80 terminator, then the slot's script pointer is rewritten to a
+// looping wait sequence (e.g. `_BigMapWaitSeq @ 29be:1574`,
+// `_SmallMapWaitSeq @ 29be:1548`). `partnerFrameAtTick` can't model
+// that swap on its own (it always wraps on the same script), hence
+// this helper.
static uint oneShotThenLoopFrameAtTick(const uint8 *unfold, uint unfoldLen,
const uint8 *waitSeq, uint waitSeqLen,
uint numFrames, uint32 elapsedMs) {
@@ -750,9 +664,10 @@ static uint oneShotThenLoopFrameAtTick(const uint8 *unfold, uint unfoldLen,
}
uint bigMapPartnerFrameAtTick(uint numFrames, uint32 elapsedMs) {
- // Script 0x14 @ 29be:196a (count-up 0..8, 0x80) â on terminator,
- // `_DoBigMap` swaps to `_BigMapWaitSeq` @ 29be:1574
- // (9,9,9,9,10,9,9,9,9, 0x80) â open-map hold with a fidget.
+ // Slot starts on script 0x14 (count-up 0..8 @ 29be:196a). On 0x80
+ // terminator, `_DoBigMap` rewrites the slot's script pointer to
+ // `_BigMapWaitSeq @ 29be:1574` = (9,9,9,9,10,9,9,9,9, 0x80) â the
+ // open-map hold with a fidget.
static const uint8 kUnfold[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8 };
static const uint8 kWaitSeq[] = { 9, 9, 9, 9, 10, 9, 9, 9, 9 };
return oneShotThenLoopFrameAtTick(kUnfold, ARRAYSIZE(kUnfold),
@@ -761,10 +676,10 @@ uint bigMapPartnerFrameAtTick(uint numFrames, uint32 elapsedMs) {
}
uint bigMapDetailPartnerFrameAtTick(uint numFrames, uint32 elapsedMs) {
- // Script 0x13 @ 29be:1992 (count-up 0..7, 0x80) â on terminator,
- // `_DoMapScreen @ 20fe:1390` swaps to `_SmallMapWaitSeq` @ 29be:1548
- // (18 entries: hold cell 7 with a single cell-10 fidget) â fidget
- // every ~1.8 s.
+ // Slot starts on script 0x13 (count-up 0..7 @ 29be:1992). On 0x80
+ // terminator, `_DoMapScreen @ 20fe:1390` rewrites the slot pointer
+ // (`MOV [BX+0x789f],0x1548`) to `_SmallMapWaitSeq @ 29be:1548` =
+ // 18 entries holding cell 7 with a single cell-10 fidget (~1.8 s).
static const uint8 kUnfold[] = { 0, 1, 2, 3, 4, 5, 6, 7 };
static const uint8 kWaitSeq[] = {
7, 7, 7, 10, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7
@@ -785,21 +700,16 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
return;
}
- // Reset the wait-anim phase so the partner starts fresh from
- // script[0] when entering. Mirrors `_DoSiteLoop @ 168d:0436`
- // where `_NewAnimation` sets the new slot's frame index to
- // 0xffff (= -1, becomes 0 on the first `_UpdateAnimations`
- // tick).
+ // `_DoSiteLoop @ 168d:0436`: `_NewAnimation` sets the new slot's
+ // frame index to 0xffff (-1) â starts at script[0].
_waitPhaseAnchor = g_system->getMillis();
if (resetPartnerMood) {
_partnerWaitMood = kPartnerWaitDefault;
initImpatienceCounter();
}
- // Capture whether this is the first time the player enters this
- // site BEFORE we mark it visited â `_DoSiteLoop @ 168d:03f4`
- // uses the same check to decide whether to play the arrival
- // dialog: `if (_VisitedSite[_SiteNumber] == 0) _DisplayClue(...)`.
+ // `_DoSiteLoop @ 168d:03f4`:
+ // if (_VisitedSite[_SiteNumber] == 0) _DisplayClue(...);
const bool firstVisit = (siteNum < Mystery::kVisitedSiteCap)
&& (_mystery->_visitedSite[siteNum] == 0);
@@ -809,22 +719,14 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
const bool playArrival = _vm->shouldPlaySiteArrival(siteNum);
- // `_DoTravel @ 168d:02da` calls `_StartTravelMusic` after the
- // destination is set. We do the same here so the music swaps as
- // the player moves between sites.
+ // `_DoTravel @ 168d:02da` calls `_StartTravelMusic`.
if (playArrival)
_vm->startTravelMusic();
- // Palette: original `_BuildBackground` calls `GetPalette(sitenum + 1)`
- // where sitenum is the global SITES.DBD index (= the per-mystery
- // `sitepic` field), not the per-mystery site index.
- //
- // Floppy site_data layout differs (per `_DoSiteLoop_Floppy @
- // 1652:03f4`): the FIRST u16 is the *offset* to a drops sub-struct
- // whose byte 0 is the small SITES.DBD picID. Without this branch
- // we'd dereference the offset directly and call setSitePalette
- // with a value in the thousands â the user reported `index 9275
- // out of range` for site 2.
+ // `_BuildBackground` calls `GetPalette(sitenum + 1)` â sitenum is the
+ // global SITES.DBD index (per-mystery `sitepic` field).
+ // Floppy (`_DoSiteLoop_Floppy @ 1652:03f4`): first u16 of site_data is
+ // an offset to a drops sub-struct whose byte 0 is the SITES.DBD picID.
const byte *sd = _mystery->siteData(siteNum);
uint16 sitepic = 0;
if (sd) {
@@ -839,18 +741,11 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
}
_vm->setSitePaletteForSite(sitepic);
- // SITEPALS ships with palette indices 0xF9..0xFE all set to a
- // uniform yellow (`3F 3E 00` = R=63 G=62 B=0 for every site palette
- // in the file), so the original `_DrawRect`'s cycling colour
- // pattern + `_ColorCycle(0xF9, 0xFE)` produced a uniformly-coloured
- // outline with no visible movement â the "marching ants" pattern
- // was a placeholder that never lit up. Override those entries with
- // a 6-step yellow ramp here so the existing per-tick rotation
- // (`applyColorCycles â cyclePaletteRange(0xF9, 0xFE)`) creates a
- // pulsing glow on unsearched hotspots. Done after `setSitePalette`
- // so the per-site palette load doesn't clobber the ramp.
+ // SITEPALS ships palette 0xF9..0xFE as uniform yellow (3F 3E 00),
+ // so original marching-ants was a placeholder. Override with a
+ // 6-step yellow ramp so `cyclePaletteRange(0xF9, 0xFE)` produces a
+ // visible pulse on unsearched hotspots.
{
- // 6-step yellow glow: dark â bright â dim â ...
static const byte kAntsGlow[6 * 3] = {
0x40, 0x40, 0x00, // F9 â dim
0x80, 0x80, 0x00, // FA
@@ -864,14 +759,11 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
renderBackground(siteNum);
- // `_DoSiteLoop @ 168d:03f4` plays `_EnterSiteAnim` whenever
- // `_LastSite != _SiteNumber`. Keep our guard on the engine rather
- // than on SiteScreen, because PDA/gallery returns recreate
- // SiteScreen and must not replay the arrival.
+ // `_DoSiteLoop @ 168d:03f4` plays `_EnterSiteAnim` when
+ // `_LastSite != _SiteNumber`. Guard lives on the engine so PDA/gallery
+ // re-entry doesn't replay arrival.
if (playArrival) {
- // `_EnterSiteAnim` snapshots the current screen, so populate
- // that temporary background with the same site layers that
- // should already be visible behind the arriving partner.
+ // `_EnterSiteAnim` snapshots the screen, so populate the BG first.
if (_vm->isFloppy())
renderFloppyDrops(siteNum);
else
@@ -885,34 +777,21 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
else
_vm->waitForMusicDone();
}
- // Re-paint the BG; the normal snapshot below should contain
- // only the static layers, while animated NPCs are redrawn per
- // tick by the frame pump.
renderBackground(siteNum);
}
- // Static drops (Loop 2 from `_DoSiteLoop`) â no animation, baked
- // into the BG snapshot the run() pump uses to restore. Floppy
- // stores them in a different shape (drops sub-struct after the
- // site_data offset), so dispatch on `isFloppy()`.
+ // Loop 2 from `_DoSiteLoop`: static drops (baked into snapshot).
if (_vm->isFloppy())
renderFloppyDrops(siteNum);
else
renderStaticDrops(siteNum);
- // Snapshot the static layers so per-tick animation re-blits don't
- // have to re-load PIC 0x43, the SITES.DBD scene, or each
- // `_AddDrop` PIC every frame.
captureBgSnapshot();
_snapshotSite = (int)siteNum;
- // Cache ColorCycle palette ranges for this site so the per-tick
- // frame pump can rotate them. Mirrors the init scan at the top of
- // `_DoSiteLoop @ 168d:03f4`.
scanColorCycles(siteNum);
- // Animated NPCs (Loop 1) and the persistent partner sit on top of
- // the snapshot. Initial frame goes at tickMs=now.
+ // Loop 1 (animated NPCs) + partner on top of the snapshot.
const uint32 now = g_system->getMillis();
renderAnimatedDrops(siteNum, now);
renderPartner(siteNum, now);
@@ -920,15 +799,12 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
renderHotspots(siteNum);
g_system->updateScreen();
- // First-visit dialog. `_DoSiteLoop @ 168d:03f4` does:
+ // First-visit dialog. `_DoSiteLoop @ 168d:03f4`:
// if (_VisitedSite[_SiteNumber] == 0) {
// _DisplayClue(_Mystery + SiteIndex[siteNum*6 + 2], 1);
// _VisitedSite[_SiteNumber] = 1;
// }
- // `SiteIndex[+2..+3]` is the byte offset (within the mystery
- // buffer) of a ClueBlock that holds the partner's arrival
- // dialogue. We've kept the +2 field undocumented up to now â
- // this confirms it's the entry-clue offset.
+ // SiteIndex[+2..+3] = byte offset of entry-clue ClueBlock.
if (firstVisit) {
const byte *idx = _mystery->siteIndexEntry(siteNum);
if (idx) {
@@ -936,10 +812,7 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
if (clueOff != 0xFFFF) {
const byte *clueBlock = _mystery->blobAt(clueOff);
if (clueBlock) {
- // See onHotspotClicked â supply a partner-less BG
- // so KD-anim playback (e.g. the partner's arrival
- // camera gesture) doesn't ghost over the resting
- // idle frame.
+ // Partner-less BG so KD-anim doesn't ghost over the idle.
_vm->setPartnerEraseBg(&_bgSnapshot);
_vm->displayClue(clueBlock);
_vm->setPartnerEraseBg(nullptr);
@@ -948,9 +821,7 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
}
if (siteNum < Mystery::kVisitedSiteCap)
_mystery->_visitedSite[siteNum] = 1;
- // The dialog overlay will have left the screen with portrait /
- // balloon residues; refresh the site so the player returns to
- // a clean state. Re-build the snapshot too.
+ // Dialog overlay leaves portrait/balloon residue; refresh & re-snapshot.
renderBackground(siteNum);
if (_vm->isFloppy())
renderFloppyDrops(siteNum);
@@ -981,10 +852,8 @@ bool SiteScreen::checkImpatienceCounter() {
}
void SiteScreen::notePartnerActivity() {
- // Mirrors `_Switch2Patient(WaitHandle)` plus
- // `_InitImpatientCounter()` in the floppy site loop, but only for
- // deliberate actions in the port: clicks and key presses. Passive
- // mouse movement should not make impatience impossible to see.
+ // `_Switch2Patient(WaitHandle)` + `_InitImpatientCounter()` from
+ // the floppy site loop. Triggered on clicks/keys only (not mouse-move).
const bool wasImpatient = _partnerWaitMood == kPartnerWaitImpatient;
_partnerWaitMood = kPartnerWaitPatient;
initImpatienceCounter();
@@ -996,11 +865,7 @@ void SiteScreen::run() {
if (!_mystery || !_mystery->isLoaded())
return;
- // The caller (run() in eem.cpp) is responsible for bringing the
- // player into a site via the map first, so `_siteNumber` is
- // already set to the destination they picked. Resuming a save
- // also restores `_siteNumber`. Start there instead of forcing
- // site 0 each time.
+ // Caller seeds `_siteNumber` (via map pick or save restore).
uint cur = _mystery->_siteNumber;
if (cur >= _mystery->numSites())
cur = 0;
@@ -1023,27 +888,14 @@ void SiteScreen::run() {
break;
case Common::EVENT_LBUTTONDOWN: {
- // On-screen UI buttons. `_DoSiteLoop @ 168d:03f4` calls
+ // `_DoSiteLoop @ 168d:03f4` calls
// _FindButton(&SiteButtons, 2, MouseX, MouseY)
- // where `SiteButtons` is two 8-byte rectangles at
- // 29be:0x274 (verified via 168d:0729-0848):
- // Button 0: (35, 111) - (56, 136) -> notebook
- // (`_NextScreen = 4`)
- // Button 1: (7, 177) - (57, 200) -> map
- // (CD `_NextScreen = 1`,
- // floppy = 2)
- // Test the buttons before falling through to hotspots so
- // a click on the PDA / partner foot doesn't accidentally
- // trigger a hotspot underneath.
- // Partner head is a port-only enhancement so the player
- // can click the host sprite for a hint, mirroring the
- // PDA's rect-3 / gallery's rect-3 behaviour. The
- // original site loop's `_FindButton(&SiteButtons, 2,
- // ...)` only checks notebook + map, but the same
- // partner-click -> `_KDHelp` shortcut is wired in
- // `_HandleNoteButton[3]` (0x0403) and
- // `_HandleGalleryButton[3]` (0x061e). Rect matches
- // the PDA / gallery `kBtnPartner` (5, 80, 44, 110).
+ // `SiteButtons` @ 29be:0274 â two 8-byte rects:
+ // Button 0: (35,111)-(56,136) notebook (_NextScreen=4)
+ // Button 1: (7,177)-(57,200) map (CD=1, floppy=2)
+ // Partner-head click is port-only: `_KDHelp` shortcut
+ // mirroring `_HandleNoteButton[3]` (0x0403) /
+ // `_HandleGalleryButton[3]` (0x061e). Rect = (5,80,44,110).
if (kSitePdaRect.contains(event.mouse.x, event.mouse.y)) {
notePartnerActivity();
_vm->setHotspotMouseCursor(false);
@@ -1054,7 +906,7 @@ void SiteScreen::run() {
if (kSitePartnerFootMapRect.contains(event.mouse.x, event.mouse.y)) {
notePartnerActivity();
_vm->setHotspotMouseCursor(false);
- // CD writes `_NextScreen = 1`; floppy writes 2.
+ // CD: _NextScreen=1, floppy=2.
_vm->setNextScreen(_vm->isFloppy() ? kScreenMapAlt
: kScreenMap);
_vm->stopMusic();
@@ -1065,10 +917,7 @@ void SiteScreen::run() {
_vm->doHelp();
notePartnerActivity();
enter(cur, false);
- // Re-evaluate cursor against the CURRENT pointer
- // position, not the click that opened the help.
- // The player may have moved off the partner-head
- // area during the dialog.
+ // Re-evaluate cursor against CURRENT pointer position.
mouse = g_system->getEventManager()->getMousePos();
updateHotspotCursor(cur, mouse.x, mouse.y);
break;
@@ -1077,13 +926,9 @@ void SiteScreen::run() {
if (idx >= 0) {
_vm->setHotspotMouseCursor(false);
onHotspotClicked(cur, (uint)idx);
- // Restore the site BG after the clue overlay.
notePartnerActivity();
enter(cur, false);
- // Use CURRENT pointer position â the click pos is
- // still inside the hotspot rect, so reusing it
- // would leave the "clickable" cursor stuck after
- // the conversation even if the player moved off.
+ // Use CURRENT pointer position (click pos still in rect).
mouse = g_system->getEventManager()->getMousePos();
updateHotspotCursor(cur, mouse.x, mouse.y);
} else {
@@ -1094,12 +939,8 @@ void SiteScreen::run() {
case Common::EVENT_KEYDOWN:
notePartnerActivity();
- // `_DoSiteLoop @ 168d:07e1` originally routed ESC
- // through `_ESCHit` -> "Are you sure?" -> MAP. The
- // site has visible MAP / SETUP / etc. controls along
- // the top bar, so ESC now opens the ScummVM in-game
- // menu (save / load / quit) instead of the back-nav
- // shortcut.
+ // `_DoSiteLoop @ 168d:07e1` routes ESC via `_ESCHit` â
+ // "Are you sure?" â MAP. Port reroutes to ScummVM menu.
if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
_vm->setHotspotMouseCursor(false);
_vm->openMainMenuDialog();
@@ -1114,21 +955,15 @@ void SiteScreen::run() {
}
}
- // Hotspot side effects can invalidate the active mystery; exit
- // immediately rather than tick another frame against stale BG
- // snapshots / hotspot tables.
+ // Hotspot side effects can invalidate the mystery; exit before
+ // ticking another frame against stale snapshots.
if (!_mystery || !_mystery->isLoaded()) {
_vm->stopMusic();
return;
}
- // Per-tick frame pump (mirrors `_CheckFrameRate` +
- // `_UpdateAnimations` at the top of `_DoSiteLoop`'s main loop).
- // Restore the static BG snapshot, redraw animated NPCs +
- // partner at the current frame, then re-render hotspots on
- // top. The original ticks at 14 cs (~140 ms, see
- // `kFramePeriodMs` above) â we matched 100 ms before, which
- // ran site animations ~1.4Ã too fast.
+ // Per-tick frame pump: `_CheckFrameRate` + `_UpdateAnimations`
+ // at the top of `_DoSiteLoop`'s main loop. 14 cs (~140 ms).
const uint32 now = g_system->getMillis();
if (_snapshotSite == (int)cur &&
now - _lastTickMs >= kFramePeriodMs) {
@@ -1140,9 +975,7 @@ void SiteScreen::run() {
renderAnimatedDrops(cur, now);
renderPartner(cur, now);
renderHotspots(cur);
- // Per-tick palette rotation for ColorCycle entries +
- // hotspot marching ants. Matches `_ColorCycle(start, end)`
- // calls inside `_DoSiteLoop @ 168d:03f4`'s main loop.
+ // `_ColorCycle(start, end)` per tick (`_DoSiteLoop @ 168d:03f4`).
applyColorCycles();
_lastTickMs = now;
}
@@ -1152,17 +985,12 @@ void SiteScreen::run() {
}
bool SiteScreen::enterSiteAnim() {
- // Mirrors `_EnterSiteAnim @ 1000:9b21`. Two phases, both partner
- // dependent:
- // Phase 1 â skateboard scroll: anim 6 (Jake) / 0xe (Jenny). Sprite
- // starts at (320 - sprite_w, 199 - sprite_h) and slides
- // left until off-screen.
- // Phase 2 â KD slide-in: anim 7 (Jake) / 0xf (Jenny). Sprite enters
- // from x = -sprite_w at y = 0x8b (Jake) / 0x8e (Jenny)
- // and slides until x = 0.
- // Original cycles frames every `_MoveSkateBoardPixels` worth of
- // motion (a runtime-calibrated speed value); we use a fixed 4 px
- // per tick which feels close to the DOS pacing.
+ // `_EnterSiteAnim @ 1000:9b21`. Two phases (partner-dependent):
+ // Phase 1 â skateboard scroll: anim 6 (Jake) / 0xe (Jenny).
+ // Slides from (320-w, 199-h) leftward off-screen.
+ // Phase 2 â KD slide-in: anim 7 (Jake) / 0xf (Jenny).
+ // Slides from x=-w at y=0x8b/0x8e until x=0.
+ // Frame rate uses `_MoveSkateBoardPixels` (runtime); we use 4 px/tick.
if (!_vm || !_mystery)
return false;
const uint8 partner = _vm->getPartnerIndex();
@@ -1170,7 +998,6 @@ bool SiteScreen::enterSiteAnim() {
const uint kKDAni = (partner == 0) ? 7 : 0xf;
const int kKDY = (partner == 0) ? 0x8b : 0x8e;
- // Snapshot the current screen so we can restore between frames.
Graphics::Surface *screen = g_system->lockScreen();
if (!screen)
return false;
@@ -1179,11 +1006,10 @@ bool SiteScreen::enterSiteAnim() {
bg.simpleBlitFrom(*screen);
g_system->unlockScreen();
- // Phase 1 â skateboard scroll. `_GetAnimation(6 | 0xe)`.
+ // Phase 1 â skateboard scroll.
Animation skate;
if (_vm->getAni().loadAnimation(kSkateAni, skate) && !skate.empty()) {
- // `iVar4 = 199 - sprite_h`, `uVar5 = 320 - sprite_w` from the
- // original; sprite_h/w come from the FIRST frame.
+ // `iVar4 = 199 - sprite_h`, `uVar5 = 320 - sprite_w` (frame 0).
const int spriteH = skate[0].surface.h;
const int spriteW = skate[0].surface.w;
int x = (320 - spriteW) & ~3; // 4-px aligned (mode-X)
@@ -1221,20 +1047,11 @@ bool SiteScreen::enterSiteAnim() {
}
}
- // Phase 2 â KD slide-in. From `_EnterSiteAnim` each frame is blitted
- // at its OWN anchor offsets (the sprite "walks in" because the
- // frame-by-frame anchors decrease as the animation progresses):
- // destX = -frame.miscflags (on-disk byte 8 = anchor X)
- // destY = kKDY - frame.rowoff (on-disk byte 6 = anchor Y)
- // Both anchors are SIGNED int16 (mirrors `blitAnimFrameAnchored` /
- // `_UpdateAnimations @ 172b:09c1`). The original's `-pPVar8->width`
- // negation wraps in 16-bit on DOS, so a frame with `miscflags = -2`
- // (0xFFFE) lands at destX = +2 in the original. Without the int16
- // re-cast our 32-bit negation produces destX = -65534 and the
- // second-to-last frame (which has a non-zero anchor in anim 7/0xf)
- // clips entirely off-screen, leaving a one-tick partner-less gap.
- // Each frame waits one `_CheckFrameRate` tick â we use 80 ms which
- // matches the original's ~12 FPS pacing.
+ // Phase 2 â KD slide-in. `_EnterSiteAnim` per-frame:
+ // destX = -frame.miscflags (signed int16; byte 8 = anchor X)
+ // destY = kKDY - frame.rowoff (signed int16; byte 6 = anchor Y)
+ // 16-bit negation: miscflags=-2 (0xFFFE) â destX=+2.
+ // ~80 ms per frame (~12 FPS, one `_CheckFrameRate` tick).
Animation kd;
if (_vm->getAni().loadAnimation(kKDAni, kd) && !kd.empty()) {
for (uint frameIdx = 0;
@@ -1268,13 +1085,9 @@ bool SiteScreen::enterSiteAnim() {
void SiteScreen::renderStaticDrops(uint siteNum) {
// Loop 2 from `_DoSiteLoop @ 168d:03f4`:
- // bound: siteData[+0x4] (verified at 168d:05c0:
- // `MOV ES:[BX+0x4], DI; CMP ES:[BX+0x4], DI`)
- // per entry at siteData[+0xc + i*6]: {picId, x, y}
- // each â `_AddDrop(picId, x, y)` (`_AddDrop @ 172b:1a77`):
- // loads PIC `picId-1` from PICS.DBD and blits with miscflags
- // high-byte as the transparent colour. These NEVER cycle, so
- // they belong to the BG snapshot.
+ // count @ siteData[+0x4], entries @ siteData[+0xc + i*6]: {picId, x, y}.
+ // `_AddDrop @ 172b:1a77` loads PIC picId-1 from PICS.DBD,
+ // blits with miscflags high-byte as transparency.
if (!_mystery)
return;
const byte *site = _mystery->siteData(siteNum);
@@ -1305,19 +1118,11 @@ void SiteScreen::renderStaticDrops(uint siteNum) {
}
void SiteScreen::renderFloppyDrops(uint siteNum) {
- // Floppy drops live inside the drops sub-struct pointed to by
- // `*site_data` (u16 offset). Verified from the call site in
- // `_DoSiteLoop_Floppy @ 1652:0418`:
- // FUN_16e2_18eb(
- // *(u16 *)(local_1a + i*5), // arg1 = u16 picID @ +0..1
- // *(u16 *)(local_1a + i*5 + 2), // arg2 = u16 X @ +2..3
- // local_1a[i*5 + 4] // arg3 = u8 Y @ +4
- // );
- // Inside `FUN_16e2_18eb @ 16e2:18eb`, `arg1 - 1` indexes the
- // PICS.DBX table at `2608:4537` (loaded from `PICS.DBX` by
- // `FUN_16e2_0149 @ 16e2:0149`); `arg2/arg3` become destX/destY.
- // drops_struct[0] = BG picID (rendered separately by
- // `renderBackground`), drops_struct[1] = drop count.
+ // Floppy drops: drops sub-struct pointed to by `*site_data` u16 offset.
+ // `_DoSiteLoop_Floppy @ 1652:0418` â `FUN_16e2_18eb @ 16e2:18eb`:
+ // entry stride 5: u16 picID @ +0, u16 X @ +2, u8 Y @ +4.
+ // picID-1 indexes PICS.DBX table @ 2608:4537.
+ // drops_struct[0] = BG picID (separate), drops_struct[1] = drop count.
if (!_mystery)
return;
const byte *site = _mystery->siteData(siteNum);
@@ -1340,9 +1145,7 @@ void SiteScreen::renderFloppyDrops(uint siteNum) {
const int16 y = (int16)e[4];
if (picID == 0)
continue;
- // `getPicture(num)` already does `loadEntry(num - 1)` (see
- // `resource.h:100`), matching the `picID - 1` index the
- // original passes to PICS.DBD.
+ // `getPicture(num)` does `loadEntry(num - 1)` (resource.h:100).
Picture pic;
if (!_vm->getPics().getPicture((uint)picID, pic))
continue;
@@ -1353,29 +1156,19 @@ void SiteScreen::renderFloppyDrops(uint siteNum) {
void SiteScreen::renderAnimatedDrops(uint siteNum, uint32 tickMs) {
// Loop 1 from `_DoSiteLoop @ 168d:03f4`:
- // bound: siteData[+0xa]
- // per entry at siteData[+0x48 + i*6]: {animId, x, y}
- // animId == -1 â `_ColorCycle(x, y)` palette range (handled
- // in the run() loop's frame pump as palette
- // rotation).
- // else â `_GetAnimation(animId)` + `_NewAnimation` then
- // `_UpdateAnimations @ 172b:09c1` walks a sequence
- // script (entries are frame indices; 0x80 = end-of-loop,
- // 0x81 = jump command). We don't have the sequence-script
- // structure decoded yet, so for now we cycle through the
- // raw animation frames in order using a global tick.
+ // count @ siteData[+0xa], entries @ siteData[+0x48 + i*6]: {animId, x, y}.
+ // animId == -1 â `_ColorCycle(x, y)` (palette rotation, separate path).
+ // else â `_GetAnimation` + `_NewAnimation` + `_UpdateAnimations @ 172b:09c1`
+ // walks a sequence script (0x80=loop, 0x81=jump).
if (!_mystery)
return;
if (_vm && _vm->isFloppy()) {
- // Floppy extra site anims live in `ANI.BIN`, not in the
- // mystery's site_data. `_ReadMystery_Floppy` asks
- // `_GetSiteAnimData_Floppy(mystery)` for the case block, then
- // walks one entry per site:
+ // Floppy site anims live in ANI.BIN (per-case block from
+ // `_GetSiteAnimData_Floppy`). Per-site:
// u8 cycleCount, cycleCount à {u8 start, u8 end},
// u8 animCount, animCount à {u8 animId, u16 x, u8 y}.
- // `_DoSiteLoop_Floppy` registers at most four of these in local
- // animation slots, so cap the draw loop the same way.
+ // `_DoSiteLoop_Floppy` caps at 4 slots.
const byte *siteAnim = _mystery->floppySiteAnimData(siteNum);
if (!siteAnim)
return;
@@ -1432,9 +1225,6 @@ void SiteScreen::renderAnimatedDrops(uint siteNum, uint32 tickMs) {
continue;
const uint frameIdx = partnerFrameAtTick((uint16)animId,
(uint)anim.size(), tickMs);
- // Animated drops go through `_NewAnimation` in the original,
- // so `_UpdateAnimations` applies per-frame anchor offsets â
- // route through the anchored blitter.
blitAnimFrameAnchored(screen, anim[frameIdx], x, y);
}
@@ -1442,14 +1232,8 @@ void SiteScreen::renderAnimatedDrops(uint siteNum, uint32 tickMs) {
}
void SiteScreen::scanColorCycles(uint siteNum) {
- // `_DoSiteLoop @ 168d:03f4` walks Loop 1 entries (siteData[+0xa]
- // count, 6-byte entries at siteData[+0x48]) and stores each entry
- // with `animId == -1` into a 5-slot table:
- // start palette idx = entry +2
- // end palette idx = entry +4
- // We mirror the layout exactly. Up to 5 entries are tracked (the
- // original's `[unaff_BP + -0x12]` and `[unaff_BP + -0x1c]` arrays
- // are 5 Ã u16 each).
+ // `_DoSiteLoop @ 168d:03f4`: Loop 1 entries with animId==-1 are
+ // ColorCycle palette ranges (start @ +2, end @ +4). Max 5 slots.
_colorCycles.clear();
if (!_mystery)
return;
@@ -1488,16 +1272,11 @@ void SiteScreen::scanColorCycles(uint siteNum) {
}
void SiteScreen::applyColorCycles() {
- // `_ColorCycle @ 172b:2015` rotates `_fpal[start..end]` by one
- // palette slot â saves [start], shifts [start..end-1] = [start+1..
- // end], restores saved at [end] â then re-uploads via `_Set_Palette`.
- // We do the same against ScummVM's palette manager. Always rotate
- // 0xf9..0xfe for hotspot marching ants (the `_ColorCycle(0xf9,
- // 0xfe)` call at the bottom of `_DoSiteLoop`'s main loop).
+ // `_ColorCycle @ 172b:2015` per range. Always rotate 0xF9..0xFE
+ // (hotspot marching ants â `_ColorCycle(0xf9, 0xfe)` in `_DoSiteLoop`).
for (uint i = 0; i < _colorCycles.size(); i++) {
cyclePaletteRange(_colorCycles[i].start, _colorCycles[i].end);
}
- // Hotspot marching ants â always cycled.
cyclePaletteRange(0xF9, 0xFE);
}
@@ -1521,18 +1300,13 @@ void SiteScreen::restoreBgSnapshot() {
void SiteScreen::renderPartner(uint siteNum, uint32 tickMs) {
// `_DoSiteLoop @ 168d:03f4` reads `siteData[+8]` as the speaker
- // table index, then for each (speaker à partner) loads
- // anim = WaitAnims[speakerIdx].anim[partner]
- // x = WaitAnims[speakerIdx].x[partner]
- // y = WaitAnims[speakerIdx].y[partner]
- // from the table at `_WaitAnims @ 29be:021c`. Each entry is
- // 12 bytes / 6 u16:
- // +0..1 anim Jake, +2..3 anim Jenny,
- // +4..5 x Jake, +6..7 x Jenny,
- // +8..9 y Jake, +10..11 y Jenny.
- // `kWaitAnims` lives at file scope above; we cap rendering at
- // `speaker < 7` since anything past entry 6 is the `_SiteButtons`
- // rect data that follows the table in the binary.
+ // table index, then for each (speaker x partner) loads:
+ // anim = WaitAnims[speakerIdx].anim[partner]
+ // x = WaitAnims[speakerIdx].x[partner]
+ // y = WaitAnims[speakerIdx].y[partner]
+ // from `_WaitAnims @ 29be:021c` (see `kWaitAnims` at file scope for
+ // 12-byte / 6-u16 entry layout). Rendering caps at speaker < 7
+ // (entries past 6 are `_SiteButtons` rect data in the binary).
const byte *site = _mystery->siteData(siteNum);
if (!site)
return;
@@ -1541,14 +1315,10 @@ void SiteScreen::renderPartner(uint siteNum, uint32 tickMs) {
int x;
int y;
if (_vm->isFloppy()) {
- // Floppy: site_data+8 is a u16 OFFSET to a 10-byte
- // speakerInfo struct (per `_DoSiteLoop_Floppy @ 1652:042b`):
- // bytes 0..1 Jake anim ID (u16)
- // bytes 2..3 Jake X (u16)
- // byte 4 Jake Y (u8)
- // bytes 5..6 Jenny anim ID (u16)
- // bytes 7..8 Jenny X (u16)
- // byte 9 Jenny Y (u8)
+ // `_DoSiteLoop_Floppy @ 1652:042b`: site_data+8 is a u16 OFFSET
+ // to a 10-byte speakerInfo struct:
+ // +0..1 Jake anim, +2..3 Jake X, +4 Jake Y,
+ // +5..6 Jenny anim, +7..8 Jenny X, +9 Jenny Y.
const uint16 spkOff = READ_LE_UINT16(site + 8);
const byte *spk = _mystery->blobAt(spkOff);
if (!spk)
@@ -1578,20 +1348,9 @@ void SiteScreen::renderPartner(uint siteNum, uint32 tickMs) {
if (!_vm->getAni().loadAnimation(animId, anim) || anim.empty())
return;
- // `_UpdateAnimations @ 172b:09c1` walks the per-anim script (from
- // `_AnimationSequences[seqnum]`) one entry per `_CheckFrameRate`
- // tick (~100 ms): render `script[index]`, advance, wrap on 0x80.
- // That's how the original gets long idle holds with brief blinks
- // â naive flipbook cycling (`tick % nFrames`) loses those pauses
- // and makes the partner constantly fidget. `partnerFrameAt` picks
- // the right frame; if no script is registered for this anim it
- // falls back to flipbook so unknown anims still move.
- // Use the relative phase anchor instead of the raw `tickMs` so
- // the wait anim resumes from script[0] after each kdAnim
- // one-shot ends â matching the original's `_PlayAnimation @
- // 172b:1f5d` resetting the resumed slot's frame index to
- // 0xffff. Without this, the wait anim snaps mid-cycle every
- // time we return from a clue display.
+ // Relative phase anchor (not raw tickMs) so wait anim resumes
+ // from script[0] after each kdAnim one-shot â matches
+ // `_PlayAnimation @ 172b:1f5d` reset to frame 0xffff.
const uint32 elapsed = (tickMs >= _waitPhaseAnchor)
? (tickMs - _waitPhaseAnchor)
: tickMs;
@@ -1611,24 +1370,17 @@ void SiteScreen::renderPartner(uint siteNum, uint32 tickMs) {
Graphics::Surface *screen = g_system->lockScreen();
if (!screen)
return;
- // Partner sprite â anchor-aware blit so per-frame `miscflags` /
- // `rowoff` apply (e.g. the BigMap walk-cycle's -2 px shift).
blitAnimFrameAnchored(screen, anim[frameIdx], x, y);
g_system->unlockScreen();
}
void SiteScreen::renderBackground(uint siteNum) {
- // Mirrors `_BuildBackground(sitepic, 0x42, 0x14)` as called from
- // `_DoSiteLoop @ 168d:03f4` and `_DisplayCorrect`. The original:
- // 1. Loads frame via `_GetFromDB(_PicIndex, 0x3d)` â that's
- // DBI entry 0x3d (0-based). Note this is NOT the same as
- // `_GetPicture(0x3d)` which would index entry 0x3c. Our
- // `loadEntry(0x3d)` matches the original directly.
- // 2. Loads SITES.DBD entry by `sitepic` (0-based, from
- // SiteData[+0..+1]).
- // 3. `_Rect_Move(... x, y, 48000, ...)` composes the scene at
- // (x, y) = (0x42, 0x14) = (66, 20) on top of the frame.
- // 4. `_GetPalette(sitepic + 1)` â per-site palette.
+ // `_BuildBackground(sitepic, 0x42, 0x14)` from `_DoSiteLoop @
+ // 168d:03f4` / `_DisplayCorrect`:
+ // 1. `_GetFromDB(_PicIndex, 0x3d)` â DBI entry 0x3d (0-based).
+ // 2. SITES.DBD entry `sitepic` (0-based, SiteData[+0..+1]).
+ // 3. `_Rect_Move(..., 0x42, 0x14, 48000, ...)` composes at (66, 20).
+ // 4. `_GetPalette(sitepic + 1)`.
Picture frame;
if (_vm->getPics().loadEntry(0x3d, frame)) {
g_system->copyRectToScreen(frame.surface.getPixels(),
@@ -1636,21 +1388,11 @@ void SiteScreen::renderBackground(uint siteNum) {
0, 0, frame.surface.w, frame.surface.h);
}
- // `_BuildBackground @ 172b:13e2` calls
- // `_GetFromDB(_siteFile, &_SiteDBIndex, sitenum)`
- // with the value at SiteData[+0] passed straight through. The
- // `_GetFromDB` callee uses that as a **0-based** index into
- // SITES.DBD (verified at 172b:14c8: `MOV BX, [BP+0x6]; IMUL BX, BX, 0xa`
- // â the dbi entry stride is 10 bytes, no -1 adjustment). Our
- // previous `loadEntry(sitepic - 1)` was off by one, which is why
- // the tutorial mystery rendered scenes from neighbouring cases.
- // Floppy site_data stores the picID one indirection deeper:
- // `*site_data` (u16) â drops sub-struct, `drops[0]` (byte) is the
- // SITES.DBD entry. CD uses the u16 directly. Verified at
- // `FUN_16e2_12fd @ 16e2:12fd` (called as
- // `FUN_16e2_12fd(*local_12, 0x42, 0x14)` from
- // `_DoSiteLoop_Floppy`, where `*local_12` is the byte at
- // drops_struct+0 via the `(undefined1 *)` cast).
+ // `_BuildBackground @ 172b:13e2` â `_GetFromDB(_siteFile,
+ // &_SiteDBIndex, sitenum)` uses SiteData[+0] as a 0-based SITES.DBD
+ // index (stride 10, asm `IMUL BX,BX,0xa` @ 172b:14c8; no -1 adjust).
+ // Floppy: site_data[0] is u16 offset to drops sub-struct; drops[0]
+ // byte is the SITES.DBD index (`FUN_16e2_12fd @ 16e2:12fd`).
const byte *site = _mystery->siteData(siteNum);
uint16 sitepic = 0;
if (site) {
@@ -1670,8 +1412,7 @@ void SiteScreen::renderBackground(uint siteNum) {
if (!haveScene)
haveScene = _vm->getPics().getPicture(sitepic + 1, scene);
if (haveScene) {
- // Hard-coded composition position from `_BuildBackground`:
- // `_Rect_Move(0, 0, h, ..., 0x42, 0x14, 48000, h, w)`.
+ // `_Rect_Move(0, 0, h, ..., 0x42, 0x14, 48000, h, w)`.
const int x = 0x42;
const int y = 0x14;
const int w = MIN<int>(scene.surface.w, 320 - x);
@@ -1683,9 +1424,7 @@ void SiteScreen::renderBackground(uint siteNum) {
}
void SiteScreen::renderHotspots(uint siteNum) {
- // Hotspot outlines (`_DrawSearchButtons`). The original always
- // draws these; the port exposes an optional game setting to hide
- // them for players who do not want location hints.
+ // `_DrawSearchButtons`. Port adds optional "hide hint" setting.
if (ConfMan.getBool("hide_highlight_boxes"))
return;
@@ -1698,31 +1437,31 @@ void SiteScreen::renderHotspots(uint siteNum) {
if (!screen)
return;
- // Mirrors `_DrawSearchButtons @ 2404:0a8f`:
+ // `_DrawSearchButtons @ 2404:0a8f`:
// for each hotspot:
- // if `_Sawit(theSite, loc) == 0` (NOT seen yet):
- // `_DrawRect(rect)` â outline in cycling colors
- // 0xF9..0xFE; `_ColorCycle(0xF9,
- // 0xFE)` rotates them every tick â
- // "marching ants" glow that draws
- // the player's eye to unsearched
- // spots.
+ // if _Sawit(theSite, loc) == 0 (NOT seen yet):
+ // _DrawRect(rect) â outline in cycling colors
+ // 0xF9..0xFE; `_ColorCycle(0xF9, 0xFE)`
+ // rotates them every tick â "marching
+ // ants" glow on unsearched spots.
// else (seen):
- // `_DrawSolidRect(rect)` â outline in solid colour 0xFF.
- // (Verified at the actual asm `2404:0af6 OR AX,AX; 2404:0af8 JZ` â
- // the C-level decompile mis-reordered the if/else branches.)
- //
- // We don't need per-pixel colour cycling here because palette
- // `0xF9..0xFE` is already rotated by `applyColorCycles` each tick;
- // drawing the outline with any single colour in that range will
- // pulse on its own. We pick a phased start so adjacent hotspots
- // don't all glow in lock-step.
+ // _DrawSolidRect(rect) â outline in solid colour 0xFF.
+ // (Branch order per asm `2404:0af6 OR AX,AX; 2404:0af8 JZ` â the
+ // C-level decompile mis-reordered the if/else.)
+ // Palette 0xF9..0xFE is already rotated by `applyColorCycles` each
+ // tick, so drawing a single colour from that range pulses on its
+ // own. Phase the start colour by hotspot index so adjacent spots
+ // don't glow in lock-step.
const uint32 tickMs = g_system->getMillis();
- // CD hotspot rows are 14 bytes each (rect + 6 bytes of clue
- // metadata). The seen key is the zero-based mystery-wide ordinal
- // at +0xa, not this site's local row index. Floppy stores plain
- // 8-byte rectangles only, so its seen key remains the row index.
+ // CD hotspot row = 14 bytes:
+ // +0..1 x1, +2..3 y1, +4..5 x2, +6..7 y2 (rect, screen coords)
+ // +8..9 clueOffset (u16 byte offset to ClueBlock in mystery)
+ // +0xa..b hotspotIndex (zero-based mystery-wide seen ordinal)
+ // +0xc..d extra (CD cursor ID for `_SwitchMouse`; shipped = 0)
+ // Seen key = the +0xa ordinal (so unrelated hotspots on later sites
+ // don't inherit the first site's seen state after travel/reload).
+ // Floppy = 8-byte plain rect only; seen key falls back to row index.
const bool floppy = _vm && _vm->isFloppy();
const uint stride = floppy ? 8 : 14;
for (uint i = 0; i < count; i++) {
@@ -1738,15 +1477,12 @@ void SiteScreen::renderHotspots(uint siteNum) {
const bool seen = seenKey < Mystery::kHotSpotsCap &&
_mystery->_hotSpotsSeen[seenKey];
if (seen) {
- // `_DrawSolidRect` (172b:0506) â outline in palette
- // index 0xFF (a fixed, non-cycling colour) so already-
- // found hotspots visually retreat into the BG.
+ // `_DrawSolidRect @ 172b:0506` â solid 0xFF (non-cycling).
screen->frameRect(rect, 0xFF);
} else {
- // `_DrawRect` (172b:03e2) â outline in palette indices
- // 0xF9..0xFE which `_ColorCycle(0xF9, 0xFE)` rotates
- // every tick. Walk all four edges incrementing the
- // colour per pixel, exactly like the original.
+ // `_DrawRect @ 172b:03e2` â walk all four edges incrementing
+ // the colour per pixel through palette indices 0xF9..0xFE,
+ // which `_ColorCycle(0xF9, 0xFE)` rotates every tick.
byte color = (byte)(0xF9 + ((i + (tickMs / 80)) & 0x07) % 6);
auto bumpColor = [&]() {
const byte next = (byte)(color + 1);
@@ -1813,21 +1549,16 @@ void SiteScreen::updateHotspotCursor(uint siteNum, int x, int y) {
void SiteScreen::onHotspotClicked(uint siteNum, uint hotIdx) {
debugC(1, kDebugSite, "Site %u: hotspot %u clicked", siteNum, hotIdx);
- // Floppy: hotspot rectangles are plain 8-byte rects (no clue
- // metadata at +0xa or +8); the dialog records live in a separate
- // per-hotspot list at `site_data[+6]`. Dispatch through the
- // floppy-specific renderer and mark the click rectangle seen by
- // array index (the only ordinal we have on floppy).
+ // Floppy: 8-byte rects only (no clue metadata @ +0xa/+8). Dialog
+ // records live in a separate list @ `site_data[+6]`. Seen key =
+ // array index (only ordinal available).
if (_vm->isFloppy()) {
if (hotIdx < Mystery::kHotSpotsCap)
_mystery->_hotSpotsSeen[hotIdx] = 1;
_mystery->_searchLocationNumber = (uint16)hotIdx;
- // Snapshot `_cluesFound` BEFORE the floppy dialog so we can
- // auto-save only when a new note (= clue text idx) is added.
- // The floppy clue-side-effect path lives in
- // `displayFloppyDialogRecords` (see clues.cpp), not in
- // `displayClue` / `applyClueSideEffects`, so the CD-side
- // autosave below would never trigger for floppy.
+ // Snapshot `_cluesFound` before dialog â autosave on new clue.
+ // Floppy side-effect path is `displayFloppyDialogRecords`
+ // (clues.cpp), not `displayClue` â autosave must be duplicated.
byte before[Mystery::kCluesFoundCap];
memcpy(before, _mystery->_cluesFound, sizeof(before));
_vm->setPartnerEraseBg(&_bgSnapshot);
@@ -1852,10 +1583,7 @@ void SiteScreen::onHotspotClicked(uint siteNum, uint hotIdx) {
// `_DoSiteLoop @ 168d:03f4` (after _DisplayClue):
// _HotSpotsSeen[hotspot[+0xa] * 2] = _HotSpotComplete;
- // The seen key is the hotspotIndex field (+0xa) â a zero-based
- // mystery-wide ordinal â NOT the site's local row index. Using the
- // local index makes unrelated hotspots on later sites inherit the
- // first site's seen state after travel or reload.
+ // Seen key = hotspotIndex @ +0xa (mystery-wide ordinal, NOT local row).
const byte *spots = _mystery->hotspots(siteNum);
uint hotOrdinal = hotIdx; // fallback to array index
if (spots) {
@@ -1865,40 +1593,24 @@ void SiteScreen::onHotspotClicked(uint siteNum, uint hotIdx) {
_mystery->_hotSpotsSeen[hotOrdinal] = 1;
_mystery->_searchLocationNumber = (uint16)hotIdx;
- // Bytes 8..9 of each 14-byte hotspot rect = byte offset within the
- // mystery blob pointing at a ClueBlock. Verified against M0.BIN:
- // site 0 hotspot 0 -> 0x0502 -> "Hi! I think somebody's playing a
- // trick on us...". `displayClue` runs the entry's side effects
- // (`_AddNotebook` for ClueEntry +0x30..+0x39, gallery +0x26..+0x2f,
- // onsite +0x1c..+0x25) so we don't need to touch `_cluesFound` here.
+ // Bytes 8..9 of each 14-byte hotspot rect = byte offset to ClueBlock.
+ // `displayClue` handles side effects (`_AddNotebook` for ClueEntry
+ // +0x30..+0x39, gallery +0x26..+0x2f, onsite +0x1c..+0x25).
if (spots) {
const uint16 clueOff = READ_LE_UINT16(spots + hotIdx * 14 + 8);
debugC(2, kDebugSite, " hotspot %u -> clue offset 0x%04x",
hotIdx, clueOff);
const byte *clueBlock = _mystery->blobAt(clueOff);
if (clueBlock) {
- // Snapshot `_cluesFound` BEFORE the clue display so we
- // can detect if any new clue was actually collected
- // (vs. a re-read of an already-found clue) â only worth
- // auto-saving when the player makes progress.
+ // Snapshot `_cluesFound` â detect new-clue 0â1 â autosave.
byte before[Mystery::kCluesFoundCap];
memcpy(before, _mystery->_cluesFound, sizeof(before));
- // Hand the engine our partner-less backdrop so that
- // `_DoKDAnim` / `playKdAnim` (the camera-style reaction
- // animation that fires when a ClueEntry has +0x3a != -1)
- // can erase the partner's resting frame between cells
- // instead of compositing over it. Cleared after the call
- // so accuse / briefing contexts fall back to their own
- // snapshots.
+ // Partner-less BG for `_DoKDAnim` / `playKdAnim` to erase
+ // the resting idle (fires when ClueEntry +0x3a != -1).
_vm->setPartnerEraseBg(&_bgSnapshot);
_vm->displayClue(clueBlock);
_vm->setPartnerEraseBg(nullptr);
- // Auto-save when a new clue is found. The original
- // engine has no autosave (saving is a manual SETUP
- // button, `_SaveGame @ 2404:0c87`); we add the autosave
- // here so the player never loses mystery progress.
- // Detected via 0â1 transition in `_cluesFound[]` (set
- // by `applyClueSideEffects` inside `displayClue`).
+ // Port-only autosave (original = manual `_SaveGame @ 2404:0c87`).
bool foundNewClue = false;
for (uint i = 0; i < Mystery::kCluesFoundCap; i++) {
if (!before[i] && _mystery->_cluesFound[i]) {
@@ -1915,26 +1627,23 @@ void SiteScreen::onHotspotClicked(uint siteNum, uint hotIdx) {
}
}
}
- // Caller (`SiteScreen::run`) re-renders the site after this returns.
}
void EEMEngine::playKdAnim(uint16 num) {
// Mirrors `_DoKDAnim(num) @ 168d:028a` + `_PlayAnimation @ 172b:1f46`:
// _SuspendAnimation(WaitHandle);
- // anim = WaitAnims[1+num].anim[partner] (table @ 29be:0228)
- // x = WaitAnims[1+num].x[partner]
- // y = WaitAnims[1+num].y[partner]
+ // anim = kKdAnimTable[num].anim[partner] (@ 29be:0228)
+ // x = kKdAnimTable[num].x[partner]
+ // y = kKdAnimTable[num].y[partner]
// _PlayAnimation(anim, x, y, WaitHandle)
- // â registers a state-4 (one-shot) animation slot and lets
- // `_UpdateAnimations` walk the sequence script until 0x80,
- // then frees this slot and re-activates `WaitHandle`.
- // Our port renders the partner's idle inline in each redraw rather
- // than via a slot system, so we play the one-shot synchronously here
+ // -> registers a state-4 (one-shot) animation slot and lets
+ // `_UpdateAnimations` walk the script until 0x80, then
+ // frees the slot and re-activates `WaitHandle`.
+ // Port renders the partner's idle inline in each redraw rather
+ // than via a slot system, so we play the one-shot synchronously
// (blocking) and resume normal idle rendering when the caller
- // returns. That matches the user-visible effect: the partner's
- // gesture (Jenny taking a picture, etc.) finishes before the
- // speaker portrait + speech balloon appear.
- //
+ // returns â matches the visible effect (partner gesture finishes
+ // before the speaker portrait + speech balloon appear).
// `kKdAnimTable` and `kAnimScripts` live at file scope above.
if (num >= ARRAYSIZE(kKdAnimTable))
return;
@@ -1950,28 +1659,17 @@ void EEMEngine::playKdAnim(uint16 num) {
return;
}
- // `_DoKDAnim` (168d:028a) calls `_PlayAnimation` with state=4 (one-
- // shot), which `_UpdateAnimations` walks until it sees the 0x80
- // terminator and then frees the slot. The same script the
- // site-loop wait anim uses (looping) is what the one-shot plays
- // through ONCE here.
+ // State-4 one-shot walks the same (looping) script through once.
const AnimScriptRef s = findAnimScript(animId);
const uint8 *frames = s.frames;
uint frameCount = s.len;
if (frameCount == 0) {
- // Fallback: linear playback through anim cells (better than
- // nothing if a future kdAnim references an unscripted anim).
+ // Fallback: linear playback through anim cells.
frameCount = (uint)anim.size();
}
- // Erase-source for between-frame redraw. Prefer the partner-less
- // backdrop the caller stashed via `setPartnerEraseBg` (e.g. the
- // site's `_bgSnapshot`, which has the static drops + frame but no
- // partner sprite). Without that, fall back to whatever's currently
- // on screen â which works for full-screen contexts (PDA / accuse /
- // briefing) where there is no separate idle partner overlay to
- // erase, but produces visible "ghosting" against the site's idle
- // partner cell at (6, 80) because it has the resting pose baked in.
+ // Erase-source: caller-stashed partner-less BG (via `setPartnerEraseBg`)
+ // or fall back to current screen (works for full-screen contexts).
Graphics::ManagedSurface bg(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
if (_partnerEraseBg.w == 320 && _partnerEraseBg.h == 200) {
@@ -1991,35 +1689,27 @@ void EEMEngine::playKdAnim(uint16 num) {
const Picture &fr = anim[frameIdx];
const byte transp = (byte)(fr.flags >> 8);
- // Restore BG, then masked-blit the next frame.
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
scratch.simpleBlitFrom(bg);
// Anchor-aware: kdAnim cells (0x03/0x04/0x0c/0x0d ...) have
- // non-zero per-frame `miscflags`/`rowoff` (anim 0x03 has
- // rowoff up to 9, anim 0x04 has miscflags = -2). Without
- // applying those, the camera-flash gesture pop-up appears
- // at a fixed pixel rather than translating across cells.
+ // non-zero per-frame `miscflags`/`rowoff` (anim 0x03 has rowoff
+ // up to 9, anim 0x04 has miscflags = -2). Routes through
+ // `blitAnimFrameAnchored` so the gesture translates across
+ // cells instead of pinning to a fixed pixel.
(void)transp; // anchored blitter recomputes from p.flags
blitAnimFrameAnchored(scratch.surfacePtr(), fr, px, py);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
g_system->updateScreen();
- // One frame per `_CheckFrameRate` tick. The original calibrates
- // this to ~10 fps; 100 ms matches what the rest of the engine
- // uses for partner / NPC frame cycling. Pump `updateScreen`
- // inside the inner wait so ScummVM's cursor overlay refreshes
- // at the same rate as the wait granularity (10 ms â 100 Hz)
- // rather than only on the per-frame redraw â without this the
- // cursor only refreshes once every 100 ms during the anim.
+ // 100 ms per frame (~10 fps). Pump updateScreen inside the wait
+ // so cursor overlay refreshes at 100 Hz.
const uint32 wakeup = g_system->getMillis() + 100;
while (g_system->getMillis() < wakeup && !shouldQuit()) {
Common::Event ev;
while (g_system->getEventManager()->pollEvent(ev)) {
- // Drain events but don't allow skipping mid-animation â
- // the speaker portrait + balloon haven't been drawn yet
- // and a click would otherwise eat the upcoming clue.
+ // Drain events; don't skip mid-anim (would eat upcoming clue).
if (ev.type == Common::EVENT_QUIT ||
ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
return;
@@ -2029,7 +1719,6 @@ void EEMEngine::playKdAnim(uint16 num) {
}
}
- // Restore BG so the next caller (speaker portrait blit) starts clean.
g_system->copyRectToScreen(bg.getPixels(), bg.pitch, 0, 0, 320, 200);
g_system->updateScreen();
}
diff --git a/engines/eem/site.h b/engines/eem/site.h
index 606edf14ffe..895d2124a9f 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -34,57 +34,46 @@ namespace EEM {
class EEMEngine;
class Mystery;
-/// Pick the frame index to render at `tickMs` for animation
-/// `seqnum`. Walks the script registered in `kAnimScripts` (mirrors
-/// `_UpdateAnimations @ 172b:09c1`'s looping path) at one entry per
-/// 100 ms tick, wrapping on the script's 0x80 terminator. Falls
-/// back to flipbook (`tick % numFrames`) when no script is
-/// registered. `numFrames` is the underlying ANI.DBD entry's cell
-/// count â used both for the fallback path and to clamp script
-/// values that point past the asset.
+/// partnerFrameAtTick: frame index for `seqnum` at `tickMs`. Walks `kAnimScripts`
+/// at `kFramePeriodMs` (~140 ms = `_CheckFrameRate` cadence) per entry; wraps
+/// on the script's 0x80 terminator. Falls back to flipbook (`tick % numFrames`)
+/// when no script is registered. `numFrames` is the ANI.DBD entry's cell count,
+/// used both for the fallback and to clamp script values past the asset.
+/// Mirrors the looping path of `_UpdateAnimations @ 172b:09c1`.
uint partnerFrameAtTick(uint16 seqnum, uint numFrames, uint32 tickMs);
-/// BigMap-overview partner frame: plays the count-up 0..8 once (the
-/// "unfold the map" pose), then loops the `_BigMapWaitSeq` hold
-/// (9,9,9,9,10,9,9,9,9). Mirrors `_DoBigMap @ 20fe:09e7`'s two-phase
-/// dispatch â the slot starts on script 0x14 (count-up @ 29be:196a)
-/// and on the 0x80 terminator the slot's script pointer is rewritten
-/// to `_BigMapWaitSeq @ 29be:1574`. `partnerFrameAtTick` can't model
-/// that swap on its own (it always loops), so without this helper the
-/// unfold cycles forever instead of resting on the open-map pose.
-/// `elapsedMs` is the time since the BigMap was opened.
+/// bigMapPartnerFrameAtTick: count-up 0..8 once, then loop `_BigMapWaitSeq`
+/// (9,9,9,9,10,9,9,9,9). Mirrors `_DoBigMap @ 20fe:09e7` two-phase swap from
+/// script 0x14 (count-up @ 29be:196a) to `_BigMapWaitSeq @ 29be:1574`.
uint bigMapPartnerFrameAtTick(uint numFrames, uint32 elapsedMs);
-/// BigMap-detail (zoomed view) partner frame. Same two-phase shape as
-/// `bigMapPartnerFrameAtTick`, but the original `_DoMapScreen @ 20fe:120b`
-/// uses script 0x13 (count-up 0..7 @ 29be:1992) for the unfold and
-/// swaps the slot pointer to `_SmallMapWaitSeq @ 29be:1548` (an
-/// 18-frame hold of cell 7 with a single cell-10 fidget) on the
-/// terminator (`MOV [BX+0x789f],0x1548` at 20fe:1390). `elapsedMs`
-/// is the time since the detail screen was opened.
+/// bigMapDetailPartnerFrameAtTick: zoomed-view partner frame. Same two-phase shape
+/// as `bigMapPartnerFrameAtTick`. `_DoMapScreen @ 20fe:120b` runs script 0x13
+/// (count-up 0..7 @ 29be:1992) then swaps to `_SmallMapWaitSeq @ 29be:1548`
+/// (`MOV [BX+0x789f],0x1548` at 20fe:1390).
uint bigMapDetailPartnerFrameAtTick(uint numFrames, uint32 elapsedMs);
-/// Anchor-aware masked blit. Mirrors the per-frame anchor offset
-/// math in `_UpdateAnimations @ 172b:09c1`:
-/// `blit_x = anchor_x - frame.miscflags`, `blit_y = anchor_y -
-/// frame.rowoff`. Use for any animation rendered through the
-/// `_NewAnimation` path in the original (partner sprites, animated
-/// drops, briefing animations) â without it, frames with non-zero
-/// per-cell anchors (e.g. anim 0x14 BigMap walk-cycle's miscflags
-/// = -2 shift) "shake in place" instead of translating across
-/// the screen as they're meant to.
+/// blitAnimFrameAnchored: mask-blit at (anchorX - frame.miscflags,
+/// anchorY - frame.rowoff). Mirrors per-frame anchor math in
+/// `_UpdateAnimations @ 172b:09c1`. Both anchors are SIGNED int16
+/// (e.g. anim 0x14 BigMap walk-cycle has miscflags = -2 per cell, so
+/// the sprite translates across the screen as it cycles). Use for any
+/// animation rendered through `_NewAnimation` in the original â partner
+/// sprites, animated drops, briefing animations. Transparency comes
+/// from `flags >> 8` (NOT miscflags).
void blitAnimFrameAnchored(Graphics::Surface *screen, const Picture &p,
int anchorX, int anchorY);
-/// Rotate one VGA palette range by one slot. Mirrors `_ColorCycle`
-/// and is used by site color cycles, hotspot marching ants, and the
-/// BigMap marker shine.
+/// Rotate one VGA palette range by one slot (STARTâEND direction).
+/// Mirrors `_ColorCycle`. Used by per-site Loop-1 ColorCycle entries,
+/// hotspot marching ants (0xF9..0xFE), and the BigMap marker shine.
void cyclePaletteRange(uint8 start, uint8 end);
-/// Rotate one VGA palette range by one slot in the OPPOSITE direction.
-/// Mirrors `_OpenColorCycle @ 2520:04f7` (CD) / `_ReverseColorCycle_Floppy`
-/// â used by the opening-anim logos (EA Kids, etc.) where the cycle
-/// shifts ENDâSTART rather than STARTâEND.
+/// Rotate one VGA palette range by one slot in the OPPOSITE direction
+/// (ENDâSTART): save END, shift every entry up by one (END-1 â END, ...),
+/// wrap saved END to START. Mirrors `_OpenColorCycle @ 2520:04f7` (CD) /
+/// `_ReverseColorCycle_Floppy`. Used by opening-anim logos (EA Kids, etc.)
+/// where the cycle shifts ENDâSTART rather than STARTâEND.
void cyclePaletteRangeReverse(uint8 start, uint8 end);
/// One hotspot (search rectangle) within a site, 14 bytes on disk.
@@ -97,14 +86,8 @@ struct Hotspot {
Common::Rect rect() const { return Common::Rect(x1, y1, x2, y2); }
};
-/**
- * Site / scene controller.
- *
- * Walks the mystery's SiteIndex to render one site at a time, polls hotspots
- * for the player's search clicks, and dispatches clue display. Mirrors the
- * site loop driven by `_DrawSearchButtons` @ 2404:0a8f and `_SearchButtons`
- * @ 2404:0bfb.
- */
+/// Site / scene controller. Mirrors `_DrawSearchButtons @ 2404:0a8f` /
+/// `_SearchButtons @ 2404:0bfb` site loop.
class SiteScreen {
public:
SiteScreen(EEMEngine *vm, Mystery *mystery)
@@ -126,40 +109,28 @@ private:
bool checkImpatienceCounter();
void notePartnerActivity();
- /// Play the partner's site-arrival sequence once `_LastSite !=
- /// _SiteNumber`. Mirrors `_EnterSiteAnim @ 1000:9b21` â animation
- /// 6 (Jake) / 14 (Jenny) skateboards in from the right edge along
- /// the bottom, then animation 7 / 15 slides KD in from the left.
- /// Returns true when the player skipped it with input.
+ /// Partner site-arrival sequence (when `_LastSite != _SiteNumber`).
+ /// Mirrors `_EnterSiteAnim @ 1000:9b21`: anim 6/14 (Jake/Jenny) skateboards
+ /// in from right, then anim 7/15 slides KD in from left. Returns true if skipped.
bool enterSiteAnim();
- /// Draw the persistent in-site partner sprite (Jake or Jenny
- /// standing/idling) at the position from `_WaitAnims` @ 29be:021c.
- /// Mirrors the `_GetAnimation` + `_NewAnimation` block at the tail
- /// of `_DoSiteLoop @ 168d:03f4`. `tickMs` selects which frame of
- /// the partner's animation to render; in the original, frames
- /// advance per `_CheckFrameRate` tick via `_UpdateAnimations`.
+ /// renderPartner: persistent in-site partner sprite at `_WaitAnims @ 29be:021c`.
+ /// Mirrors `_GetAnimation` + `_NewAnimation` tail of `_DoSiteLoop @ 168d:03f4`.
void renderPartner(uint siteNum, uint32 tickMs);
- /// Draw the per-site `_AddDrop` static decorations (Loop 2).
- /// `_DoSiteLoop` runs this loop with bound siteData[+0x4] and
- /// 6-byte entries at siteData[+0xc]: {picId, x, y}. These never
- /// animate so they go in the BG snapshot.
+ /// renderStaticDrops: `_AddDrop` static decorations (Loop 2).
+ /// siteData[+0x4] count, siteData[+0xc] entries (6 bytes: {picId, x, y}).
void renderStaticDrops(uint siteNum);
- /// Floppy variant: drops live inside the drops sub-struct
- /// (`*site_data` â drops; `drops[1]` = count; entries at
- /// `drops + 2` are 5 bytes each: {u16 X, u16 Y, byte picID}).
- /// PIC entries are loaded from PICS.DBD with `picID - 1`. Per
- /// `_DoSiteLoop_Floppy @ 1652:0418` and `FUN_16e2_18eb`.
+ /// renderFloppyDrops: floppy variant. `*site_data`âdrops; drops[1]=count;
+ /// entries at drops+2 are 5 bytes ({u16 X, u16 Y, byte picID}); PIC loaded as picID-1.
+ /// Per `_DoSiteLoop_Floppy @ 1652:0418` and `FUN_16e2_18eb`.
void renderFloppyDrops(uint siteNum);
- /// Draw the per-site animated NPCs (Loop 1) at the current tick.
- /// `_DoSiteLoop` registers each via `_NewAnimation` (siteData[+0xa]
- /// entries at siteData[+0x48]: {animId (-1 = ColorCycle), x, y})
- /// and `_UpdateAnimations @ 172b:09c1` advances frame indices each
- /// tick. We use a millis-based frame index so all NPCs cycle in
- /// step with the global clock.
+ /// renderAnimatedDrops: per-site animated NPCs (Loop 1) at current tick.
+ /// `_DoSiteLoop` registers each via `_NewAnimation` (siteData[+0xa] count,
+ /// siteData[+0x48] entries: {animId (-1=ColorCycle), x, y}); frames advance via
+ /// `_UpdateAnimations @ 172b:09c1`.
void renderAnimatedDrops(uint siteNum, uint32 tickMs);
/// Snapshot the post-BG, post-static-drops screen so the per-tick
@@ -169,16 +140,12 @@ private:
/// Restore the snapshot taken at `captureBgSnapshot` time.
void restoreBgSnapshot();
- /// Scan the site's Loop 1 entries for ColorCycle entries (animId
- /// == -1) and cache their (start, end) palette ranges. Called from
- /// `enter()`. Mirrors `_DoSiteLoop @ 168d:03f4`'s init scan.
+ /// scanColorCycles: scan Loop 1 for ColorCycle entries (animId == -1),
+ /// cache (start, end) palette ranges. Mirrors `_DoSiteLoop @ 168d:03f4` init scan.
void scanColorCycles(uint siteNum);
- /// Rotate cached ColorCycle palette ranges (and 0xf9..0xfe for
- /// hotspot marching ants) one step. Mirrors the original's per-tick
- /// `_ColorCycle(start, end)` calls inside `_DoSiteLoop`'s main
- /// loop. ScummVM's palette manager grabs current 8-bit RGB and
- /// writes back the rotated values.
+ /// applyColorCycles: rotate cached ColorCycle ranges + 0xf9..0xfe (hotspot ants)
+ /// one step. Mirrors per-tick `_ColorCycle(start, end)` calls in `_DoSiteLoop`.
void applyColorCycles();
EEMEngine *_vm;
@@ -194,24 +161,13 @@ private:
uint32 _impatientDeadlineMs = 0; ///< Test-shortened impatience deadline.
PartnerWaitMood _partnerWaitMood = kPartnerWaitDefault;
- /// Wall-clock timestamp at which the partner's wait animation
- /// "started" (or last restarted). The site loop renders the
- /// wait sprite at `partnerFrameAtTick(animId, ..., now -
- /// _waitPhaseAnchor)` so the script position is RELATIVE to
- /// this anchor, not the global clock. Bump it on:
- /// - entry to a new site (mirrors `_NewAnimation` setting
- /// the slot's frame index to 0xffff at site setup, see
- /// `_DoSiteLoop @ 168d:0436`)
- /// - return from a one-shot kdAnim (mirrors `_PlayAnimation
- /// @ 172b:1f5d` writing 0xffff to the resumed slot's frame
- /// index when state=4 chains back to WaitHandle)
- /// Without this, the wait anim "snaps" to wherever the global
- /// clock dictates each time the partner reappears, instead of
- /// resuming from script[0].
+ /// Wall-clock anchor for partner wait anim phase: rendered as
+ /// `partnerFrameAtTick(animId, ..., now - _waitPhaseAnchor)`.
+ /// Bumped on site entry (`_NewAnimation` writes 0xffff at `_DoSiteLoop @ 168d:0436`)
+ /// and on return from a one-shot kdAnim (`_PlayAnimation @ 172b:1f5d` state=4).
uint32 _waitPhaseAnchor = 0;
- /// Per-site cached ColorCycle ranges. Up to 5 (matching the
- /// original's 5-slot animation table).
+ /// Per-site cached ColorCycle ranges (up to 5, matching original 5-slot anim table).
struct ColorCycleRange { uint8 start, end; };
Common::Array<ColorCycleRange> _colorCycles;
};
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 6e9fd72df76..f8c888d1843 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -36,16 +36,10 @@
#include "eem/music.h"
#include "eem/site.h"
-// EEM â UI screens (NOTE.C, GALLERY.C, ACCUSE.C, MAP.C, CHOOSE.C combined).
-// Each function is a self-contained modal `EEMEngine::doX()` reachable from
-// the site loop. They share the same wait-for-input idiom and PIC 0x3f /
-// 0x41 / 0x42 / 0x43 backdrops.
-
namespace EEM {
-// Five fixed gallery slot positions verified at `29be:0x116`. Used by
-// both `_DrawGallery @ 158f:0046` (notebook gallery) and the accuse
-// portrait grid; the layout is identical so we share the table.
+// Gallery slot positions @ 29be:0x116. Used by `_DrawGallery @ 158f:0046`
+// and the accuse portrait grid.
struct GallerySlot { int x; int y; };
const GallerySlot kGallerySlots[5] = {
{ 83, 14 }, // 0
@@ -206,11 +200,8 @@ const byte *advanceFloppyDialogRecords(const byte *rec, uint count,
return rec;
}
-// Floppy gallery slot positions verified at `2608:0x16c` (5 Ã
-// {u16 x, u16 y}) â read by `_DrawGallery_Floppy @ 154e:0045`'s
-// `[BX + 0x16c]` (x) and `[BX + 0x16e]` (y) loads. The floppy
-// layout is shifted left ~0x30 px relative to CD: row 0 starts at
-// x=0x53 (vs 0x53â0x83 on CD) and the bottom row at x=0x77 / 0xbf.
+// Floppy gallery slot positions @ 2608:0x16c. Read by `_DrawGallery_Floppy
+// @ 154e:0045`'s [BX + 0x16c] (x) and [BX + 0x16e] (y) loads.
const GallerySlot kFloppyGallerySlots[5] = {
{ 0x53, 0x0e }, // 0
{ 0x9b, 0x0e }, // 1
@@ -219,17 +210,15 @@ const GallerySlot kFloppyGallerySlots[5] = {
{ 0xbf, 0x5a } // 4
};
-// `_GetKDTextBalloon @ 1df2:0105` digit-balloon table @ `29be:1064`:
+// `_GetKDTextBalloon @ 1df2:0105` digit-balloon table @ 29be:1064:
// '0'â0x15 '1'â0x16 '2'â0x17 '3'â0x18 '4'â0x19
// '5'â0x1a '6'â0x20 '7'â0x21 '8'â0x22 '9'â0x1e
const uint16 kDigitBalloons[10] = {
0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x20, 0x21, 0x22, 0x1e
};
-// Return the next non-empty slot in `slotRects` starting from `from`,
-// stepping by `dir` (+1 or -1) with wraparound. Used by the accuse
-// gallery's keyboard-cycle (TAB / arrow keys) â mirrors the way
-// `_PutMouseInRect` skips eliminated suspects in the original.
+// Next non-empty slot in `slotRects` from `from`, stepping by `dir` with
+// wraparound. Mirrors `_PutMouseInRect` skipping eliminated suspects.
int nextLiveSlot(const Common::Array<Common::Rect> &slotRects,
int from, int dir) {
const int n = (int)slotRects.size();
@@ -252,9 +241,7 @@ void copyToScreen(Graphics::ManagedSurface &scratch) {
}
void cycleChooserPalette() {
- // `_DoChoose` / `_DoListPicker_Floppy` rotate 0x6f..0x73 on each
- // frame tick while waiting for input. These colors are used by the
- // animated chooser surface baked into the menu backgrounds.
+ // `_DoChoose` / `_DoListPicker_Floppy` rotate 0x6f..0x73 per tick.
cyclePaletteRange(kChooserCycleStart, kChooserCycleEnd);
}
@@ -445,9 +432,6 @@ void drawProfilePickerFrame(const ProfilePickerView &v) {
copyToScreen(scratch);
}
-// Snapshot of `doActionScreen`'s captured locals, used by
-// `drawActionMenuFrame`. Lives on the stack inside `doActionScreen`;
-// never escapes.
struct ActionMenuView {
EEMEngine *vm;
const Picture *bg;
@@ -460,13 +444,9 @@ struct ActionMenuView {
uint pick;
};
-// Mystery list shown in the "Choose A Mystery" sub-screen. Mirrors
-// `_DoChooseMystery @ 1a35:02b7`: opens BOOK%u.NME (CRLF-separated
-// ASCII strings, last entry is whitespace = sentinel), reads up to 25
-// lines à 40 bytes each, hands the array to `_CaseSelection`. We
-// preserve the trailing whitespace line as the original sentinel
-// since `_DoChoose @ 1c33:0514` walks until `*piVar3 == 0 && piVar3[1]
-// == 0` â but for our renderer we just keep the names array.
+// `_DoChooseMystery @ 1a35:02b7` â opens BOOK%u.NME (CRLF strings, up
+// to 25 Ã 40 bytes, whitespace-line sentinel). `_DoChoose @ 1c33:0514`
+// walks until {0, 0}.
Common::StringArray loadBookNames(uint book) {
Common::StringArray names;
const Common::String fname = Common::String::format("BOOK%u.NME", book);
@@ -479,10 +459,7 @@ Common::StringArray loadBookNames(uint book) {
Common::String line = f.readLine();
if (f.eos() && line.empty())
break;
- // `_fgets` in the original reads CRLF terminators with the line;
- // `Common::File::readLine` strips them, so `line` here is the
- // text only. Trim trailing whitespace so the sentinel " "
- // last entry doesn't render as a blank scrollable row.
+ // Trim trailing whitespace so the sentinel line doesn't render.
while (!line.empty() &&
(line.lastChar() == ' ' || line.lastChar() == '\t' ||
line.lastChar() == '\r'))
@@ -504,14 +481,8 @@ void clampCaseTopRow(uint &topRow, uint listLen, uint visibleRows) {
topRow = maxTop;
}
-// Per-mystery sub-chooser ("Choose A Mystery") view.
-//
-// `names` are the entries from BOOK%d.NME (in display order â index 0
-// = first case in the tier, mystery number = `tierLo + index`).
-// `solvedFlags` is a parallel bool array indicating which entries are
-// already solved (greyed and unselectable in `_DoChoose`).
-// `topRow` is the scroll position; up to 12 entries are visible.
-// `selRow` is the highlighted row (0-based within the names array).
+// "Choose A Mystery" sub-chooser view. names = BOOK%d.NME entries
+// (mystery number = tierLo + index). solvedFlags marks greyed entries.
struct CaseSubmenuView {
EEMEngine *vm;
const Picture *caseBg;
@@ -535,10 +506,8 @@ void drawCaseGreeter(Graphics::ManagedSurface &scratch,
if (!haveKdAnim || !kdAnim || kdAnim->empty())
return;
- // `_CaseSelection` registers the partner-specific ANI slot, but
- // drives it with script 0x15 regardless of partner. The chooser
- // loop advances it through `_UpdateAnimations` after each
- // `_CheckFrameRate` tick.
+ // `_CaseSelection` drives the partner ANI with script 0x15 regardless
+ // of partner.
const uint32 now = g_system->getMillis();
const uint frameIdx = partnerFrameAtTick(0x15, (uint)kdAnim->size(), now);
blitAnimFrameAnchored(scratch.surfacePtr(), (*kdAnim)[frameIdx],
@@ -595,12 +564,9 @@ bool animateCaseSelectionReveal(EEMEngine *vm, const Picture *caseBg,
return false;
}
-// Mirrors `_DoChoose`'s `DrawList @ 1c33:040d`. 12 visible rows à 10 px
-// at (61, 35); colour palette: 0x13 = highlighted (selected), 0x1B =
-// greyed (already solved), 0x5C = default. We approximate with the
-// closest indices of site palette 0 â 0xF (white) / 0x7 (medium grey)
-// / 0x8 (dark grey) â since we don't decode the original CLUT byte
-// ramp.
+// `_DoChoose`'s `DrawList @ 1c33:040d`. 12 rows à 10 px at (61, 35).
+// Original colours 0x13 sel / 0x1B greyed / 0x5C default approximated
+// here as 0xF / 0x8 / 0x7 from site palette 0.
void drawCaseSubmenu(const CaseSubmenuView &v) {
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
@@ -635,18 +601,14 @@ void drawCaseSubmenu(const CaseSubmenuView &v) {
kListX, kListY0 + r * kLineH, kListW, color);
}
- // Selection arrow at the left edge of the highlighted row â the
- // original highlights via colour change but adding an arrow makes
- // the keyboard-driven path obvious.
+ // Selection arrow (ScummVM-only â original uses colour change).
if (v.selRow >= v.topRow && v.selRow < v.topRow + (uint)kVisible) {
const int r = (int)(v.selRow - v.topRow);
v.vm->getFont().drawString(&scratch, ">",
kListX - 6, kListY0 + r * kLineH, 6, 0xF);
}
- // Scrollbar thumb. `DrawThumb @ 1c33:????` renders a thumb at
- // (240, 45..146) proportional to scroll position. We draw a
- // 1-px outlined block to indicate the same range.
+ // Scrollbar thumb at (240, 45..146), proportional to scroll position.
if (count > (uint)kVisible) {
const int trackY0 = 45;
const int trackH = 146 - 45;
@@ -674,22 +636,14 @@ void drawActionMenuFrame(const ActionMenuView &v) {
kActionScreenDecorX, kActionScreenDecorY);
if (v.vm->getFont().isLoaded()) {
- // `DrawList` @ 1c33:040d coordinates: `_TextBox + 3` for x
- // and `DAT_29be_0d02` for y. `_TextBox` @ 29be:0d00 holds
- // {x=58, y=35, x2=238, y2=158}. Matches the blue panel.
+ // `DrawList @ 1c33:040d`. _TextBox @ 29be:0d00 = {58, 35, 238, 158}.
const int kListX = 58 + 3;
const int kListW = 238 - kListX;
const int kListY0 = 35;
const int kLineH = 10;
- // Render 11 list rows: separator + menu item pairs.
- // row 0 separator
- // row 1 Choose A Mystery
- // row 2 separator
- // row 3 Practice Mystery
- // ...
- // row 9 See ScrapBook 3
- // row 10 separator
+ // 11 rows: separator/item pairs (0=sep, 1=Choose A Mystery, ...,
+ // 9=See Scrapbook 3, 10=sep).
for (int r = 0; r < 11; r++) {
const int y = kListY0 + r * kLineH;
if ((r & 1) == 0) {
@@ -709,21 +663,11 @@ void drawActionMenuFrame(const ActionMenuView &v) {
}
void EEMEngine::doProfilePicker() {
- // Mirrors `screen8_handler @ 1c33:1012`. The original walks
- // `*.PLR` files in `C:\EEMCDSAV\` (max 25), reads the first 12
- // bytes of each (the player-name field of `_PlayerRecord`), and
- // hands the list to `_DoChoose`. If no profiles exist (loop hits
- // `local_20 == 0` at 1c33:1170), it falls straight into
- // `_NewPlayer`. Selecting an entry calls `_LoadPlayerRecord` and
- // returns; the 0xfffe / 0xffff chooser sentinels both enter
- // `_NewPlayer` in this screen.
-
- // Palette reset. `screen8_handler` runs `_FadeOut(); _GetPalette(0);
- // _GetBackground(0x104);` before the picker, so the BG always
- // renders against SITEPALS index 0 regardless of which intro
- // palette was active last. Without this, skipping out of an intro
- // anim (THEME / ANIM01..20 / TITLE) leaves the previous video's
- // palette in place and the picker draws with the wrong colours.
+ // `screen8_handler @ 1c33:1012` â walks *.PLR (max 25), reads 12-byte
+ // player-name field, hands list to `_DoChoose`. No profiles or
+ // 0xfffe/0xffff sentinel enters `_NewPlayer`.
+
+ // `screen8_handler` does `_FadeOut(); _GetPalette(0); _GetBackground(0x104)`.
setSitePalette(0);
const SaveStateList saves = listProfiles();
@@ -733,16 +677,12 @@ void EEMEngine::doProfilePicker() {
}
if (!_font.isLoaded()) {
- // No font means we can't render the picker â fall through.
doNewPlayer();
return;
}
- // Build the visible list: existing profile names + "[New Player]".
- // The DOS picker also has a bottom click area at 29be:0d08 that
- // returns 0xfffe and immediately enters `_NewPlayer`; keeping the
- // explicit row makes that affordance visible in ScummVM while the
- // bottom rect remains active too.
+ // Existing profiles + "[New Player]". DOS bottom click area @ 29be:0d08
+ // returns 0xfffe â `_NewPlayer`; rect remains active too.
Common::Array<ProfilePickerEntry> entries;
for (const SaveStateDescriptor &s : saves) {
ProfilePickerEntry e;
@@ -765,12 +705,8 @@ void EEMEngine::doProfilePicker() {
const bool haveReveal =
_picsArchive.getPicture(kProfilePickerRevealPic, reveal);
- // Picker geometry: `DrawList @ 1c33:040d` is called from
- // `screen8_handler @ 1c33:1012` with `(_TextBox + 3, DAT_29be_0d02)`.
- // `_TextBox @ 29be:0d00` holds {x1=58, y1=35, x2=238, y2=158} so
- // the list origin is (61, 35), 10 px per row, max 12 visible
- // rows. `screen8_handler` slides PIC 0x105 into the lower strip
- // before calling `_DoChoose`; it does not draw that caption as text.
+ // Geometry: list origin (61, 35), 10 px/row, 12 visible. PIC 0x105
+ // slides into lower strip via `screen8_handler` before `_DoChoose`.
ProfilePickerView view;
view.vm = this;
view.bg = &bg;
@@ -866,8 +802,7 @@ void EEMEngine::doProfilePicker() {
break;
}
if (kChooserHelpRect.contains(ev.mouse.x, ev.mouse.y)) {
- // `screen8_handler` sets `Chelp = 0`, so the
- // original ignores this chooser button here.
+ // `screen8_handler` sets Chelp=0 â button ignored.
break;
}
if (kChooserListRect.contains(ev.mouse.x, ev.mouse.y)) {
@@ -915,9 +850,7 @@ void EEMEngine::doProfilePicker() {
if (e.slot < 0) {
doNewPlayer();
} else {
- // Mirrors `_LoadPlayerRecord` at 1c33:1281 â slot found,
- // load it. The save body re-fills `_playerName`, partner,
- // chain stage, mysteriesSolved.
+ // `_LoadPlayerRecord @ 1c33:1281`.
if (!loadProfile(e.label)) {
warning("doProfilePicker: failed to load profile '%s' at slot %d",
e.label.c_str(), e.slot);
@@ -927,9 +860,8 @@ void EEMEngine::doProfilePicker() {
}
void EEMEngine::doNewPlayer() {
- // Mirrors `_NewPlayer` @ 1c33:0dda. The original draws background
- // 0x104 + character peek pic 0x107, then shows "Please type your
- // name" and accepts up to 12 characters until Enter.
+ // `_NewPlayer @ 1c33:0dda` â BG 0x104 + peek pic 0x107, prompt for
+ // up to 12 chars.
if (!_font.isLoaded()) {
_playerName = "Detective";
return;
@@ -938,24 +870,18 @@ void EEMEngine::doNewPlayer() {
Common::String name;
const int maxChars = 12;
- // Mirror the original: load PIC 0x104 as the name-entry backdrop.
- // The original also slides in PIC 0x107 (a peeking character).
Picture bg;
const bool haveBG = _picsArchive.getPicture(0x104, bg);
Picture peek;
const bool havePeek = _picsArchive.getPicture(kNameEntryPeekPic, peek);
- // Localized name-entry prompt. Spanish text is taken from the
- // Spanish floppy EEM.EXE ("Teclea tu nombre"). The colon suffix is
- // our own â the original DOS prompt has none.
+ // Spanish from EEM.EXE ("Teclea tu nombre"); colon added.
const char *prompt = isSpanish()
? "Teclea tu nombre:" : "Please type your name:";
if (animateNameEntryPeek(this, &bg, haveBG, havePeek ? &peek : nullptr))
return;
- // Match the original `_NewPlayer`: `_Show_String(rw=0x28, cl=0x50)`
- // for the prompt, then `_ShowChar(0x50, x, â¦)` for typed input.
- // (rw=row=y, cl=col=x.) Prompt at (y=40, x=80), input at (y=80, x=80).
+ // Prompt (y=40, x=80), input (y=80, x=80).
drawNameEntryFrame(this, &bg, haveBG, havePeek ? &peek : nullptr,
name, prompt);
@@ -977,20 +903,14 @@ void EEMEngine::doNewPlayer() {
if (k == Common::KEYCODE_RETURN) {
if (name.empty())
name = "Detective";
- // Mirrors `_NewPlayer @ 1c33:0dda` tail (1c33:0fa0+):
- // after the name is committed, try `_LoadPlayerRecord`
- // â if it returns 0 (no existing .PLR), zero out the
- // per-profile state and call `_SavePlayerRecord` to
- // create a fresh profile file. Same flow here, mapped
- // onto ScummVM save slots via name â description.
+ // `_NewPlayer @ 1c33:0fa0+`: `_LoadPlayerRecord` â if
+ // missing, zero state and `_SavePlayerRecord`.
if (!loadProfile(name)) {
_playerName = name;
memset(_mysteriesSolved, 0, sizeof(_mysteriesSolved));
_mystery.clear();
_partner = 0;
- // `_NewPlayer @ 1c33:0fa3` writes
- // `DAT_2d5d_3f99 = 1` â fresh profiles always
- // start at the Junior tier.
+ // `_NewPlayer @ 1c33:0fa3`: DAT_2d5d_3f99 = 1 (Junior).
_chainStage = 1;
saveProfile(name);
}
@@ -1027,38 +947,24 @@ void EEMEngine::doNewPlayer() {
}
int EEMEngine::doShowEnding(uint num, bool firstPage) {
- // Mirrors `_DisplayEnding @ 1df2:0548` + `_DisplayEndingPage @
- // 1df2:044c` on CD and `FUN_1d40_05b7` + `FUN_1d40_031e` on
- // floppy. CD ending file format:
+ // `_DisplayEnding @ 1df2:0548` + `_DisplayEndingPage @ 1df2:044c`
+ // (CD); `FUN_1d40_05b7` + `FUN_1d40_031e` (floppy).
+ // CD E<num>.BIN format:
// u16 pageCount
- // for each page:
- // u16 picNum
- // u16 x1, y1, x2, y2 (story rect â passed to WordWrap)
- // char text[] (null-terminated, ParseString opcodes)
- // Floppy files prepend a small title header and use a shared
- // newspaper background (PIC 0x8b) plus per-page overlay pictures.
- //
- // The original swaps the font: `_FreeFont(); _LoadFont("tiny.fnt")`
- // at 1df2:055f-1df2:0563, calls `_GetPalette(0)` (site palette 0),
- // then for each page `_GetBackground(picNum)` +
- // `_WordWrap2(x1, y1, x2-x1, text, fontColor=0, dropColor=-1)`. The
- // fontColor=0 draws in palette index 0 (the newspaper's body-text
- // colour), with no drop shadow. Verified at the call site asm
- // 1df2:04cf-1df2:04f4 (Ghidra mis-paired the two trailing args).
- //
- // Keyboard page navigation mirrors the original handlers
- // (1df2:0689 / 1df2:06a0): LEFT decrements pageIdx, RIGHT (or
- // SPACE / Enter) increments it. Hitting the boundary (LEFT on page
- // 0, RIGHT on last page) sets `[BP-0x18]` to -1 / 1 respectively
- // and exits â that return value is what `_ShowScrapbook` uses to
- // walk forward / backward through solved mysteries (see
- // 1f78:0664-1f78:069c). Mouse navigation is intentionally limited
- // to the red-highlighted edge rects so central page clicks are ignored.
- //
- // `firstPage=false` opens the ending at the LAST page (used by
- // `doShowScrapbook` after a "previous mystery" navigation â
- // matches `local_8 = 0` written before the back-step at
- // 1f78:067e).
+ // per page: u16 picNum
+ // u16 x1, y1, x2, y2 (story rect â passed to WordWrap2)
+ // char text[] (null-terminated, ParseString opcodes)
+ // Floppy: small title header + shared newspaper BG (PIC 0x8b) +
+ // per-page overlay pictures.
+ // Render: _FreeFont; _LoadFont("tiny.fnt") @ 1df2:055f; _GetPalette(0);
+ // per-page _GetBackground(picNum) + _WordWrap2(x1, y1, x2-x1, text,
+ // fontColor=0, dropColor=-1). fontColor=0 is the newspaper body-text
+ // colour (asm 1df2:04cf-04f4 â Ghidra mis-paired the trailing args).
+ // LEFT/RIGHT page nav (1df2:0689 / 06a0): boundary sets [BP-0x18] to
+ // -1 / +1 â that return value drives `_ShowScrapbook` walking forward/
+ // back through solved mysteries (1f78:0664-069c).
+ // firstPage=false opens at LAST page (used by doShowScrapbook after
+ // "previous mystery"; matches `local_8 = 0` @ 1f78:067e).
const Common::String fname = Common::String::format("E%u.BIN", num);
Common::File f;
if (!f.open(Common::Path(fname))) {
@@ -1077,19 +983,13 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
return 0;
}
- // Mirrors 1df2:0558-1df2:056a â `_FreeFont(); _LoadFont(tiny.fnt)`.
- // The newspaper layout uses TINY.FNT (smaller glyphs) so the body
- // copy fits in the columns. `_LoadFont(font.fnt)` is restored at
- // 1df2:0625 after the page loop.
+ // 1df2:0558-056a: TINY.FNT; restored at 1df2:0625.
EEMFont tinyFont;
const bool haveTinyFont = tinyFont.load(Common::Path("TINY.FNT"));
if (!haveTinyFont)
warning("doShowEnding: TINY.FNT failed to load â falling back");
- // Mirrors 1df2:055f `_GetPalette(0)` â site palette 0 is the
- // shared "newspaper" CLUT for ending pages. The newspaper body
- // text in particular is palette index 0 (= newspaper black) so we
- // MUST switch palettes before rendering.
+ // 1df2:055f `_GetPalette(0)` â newspaper CLUT (body text = idx 0).
setSitePalette(0);
CursorMan.showMouse(true);
@@ -1227,15 +1127,10 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
kFirstTryBadgePos, transp);
}
- // Story text. The bytes are a null-terminated string with
- // `_ParseString` placeholders (0x80 = player name, 0x82
- // = partner first name, etc.).
+ // `_ParseString` placeholders (0x80=name, 0x82=partner first).
const Common::String text = parseString(raw, _playerName, _partner);
- // Use TINY.FNT (`_LoadFont(@29be:10a5)` at 1df2:055f-0563)
- // and color 0 (`_WordWrap2(...,0,-1)` per asm at 1df2:04cf,
- // not 0xF as Ghidra's decompile output suggests). Falls
- // back to the main font if TINY.FNT failed to load.
+ // TINY.FNT + color 0 (asm 1df2:04cf â not 0xF as Ghidra shows).
const EEMFont &renderFont = haveTinyFont ? tinyFont : _font;
if (renderFont.isLoaded() && x2 > x1) {
const int textW = MIN<int>((int)x2 - (int)x1, 320 - (int)x1);
@@ -1330,15 +1225,9 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
}
void EEMEngine::doShowScrapbook(uint stage) {
- // Mirrors `_ShowScrapbook(stage, 0) @ 1f78:0642`. The original
- // splits the cases into 24-entry tiers, then feeds each ending
- // viewer return direction back into the tier walk:
- // -1 -> previous mystery, opened at its last page
- // 0 -> close scrapbook
- // +1 -> next mystery, opened at its first page
- // When the requested tier is the player's current chain stage, the
- // original skips unsolved entries; completed tiers are already solved,
- // so they are shown as a full range.
+ // `_ShowScrapbook(stage, 0) @ 1f78:0642`. 24-entry tiers; ending
+ // viewer returns -1/0/+1 for prev/close/next. Current tier skips
+ // unsolved entries.
if (stage < 1 || stage > 3)
return;
const int solvedCount =
@@ -1389,31 +1278,13 @@ void EEMEngine::doShowScrapbook(uint stage) {
}
void EEMEngine::doSetup() {
- // Mirrors `_DoSetup @ 1f78:044e` (CD) and `_DoSetup_Floppy @
- // 1ee2:0387` (floppy). Both variants share the same PIC 0x40 BG
- // with text labels baked in, the same 13 button rectangles at
- // `_SetupButtons @ 29be:1218` (CD) / `2608:0d8c` (floppy), and the
- // same 4 highlight rectangles at `0xe94..0xeb3` (Kid1 / Kid2 /
- // SoundOn / SoundOff). Floppy additionally pre-loads a few overlay
- // PICs (0x9b..0x9e + 0x1fa) that the CD uses on demand, but the
- // behaviour is identical â colour-key swap on `0xFE` to indicate
- // active state, click dispatch via the same 12-entry handler
- // jumptable. So a single shared handler covers both variants.
- //
- // The setup screen is BG `PIC 0x40` (loaded once on entry) with
- // every label baked in â "Setup", "Partner", "Sound", "Music",
- // the "Jake"/"Jenny"/"On"/"Off" option strings, etc. â all
- // rendered in palette key `0xFE`. The original then runs
- // `_SetupSettings @ 1f78:000d` which uses `_SwapColors @
- // 172b:1d2a` to recolour those `0xFE` pixels per label rect:
- // `0x15` for the active state, `0` for the inactive one. So
- // nothing is drawn as text; the visible state of each toggle is
- // purely a per-rect colour swap on top of `PIC 0x40`.
- //
- // Click hit-tests go through `_SetupButtons @ 29be:1218` â 13Ã
- // 8-byte rects. Each click runs `HandleSetupButton @ 1f78:0158`,
- // which dispatches via the 12-entry jumptable at `1f78:0436`.
- // Verified handler map (decompiled at each jumptable target):
+ // `_DoSetup @ 1f78:044e` (CD) / `_DoSetup_Floppy @ 1ee2:0387`.
+ // PIC 0x40 BG with labels baked in palette key 0xFE; `_SetupSettings
+ // @ 1f78:000d` runs `_SwapColors @ 172b:1d2a` per label rect
+ // (0x15 active, 0x00 inactive â nothing drawn as text). 13Ã 8-byte
+ // click rects at `_SetupButtons @ 29be:1218` (CD) / 2608:0d8c (floppy);
+ // `HandleSetupButton @ 1f78:0158` dispatches via 12-entry jumptable
+ // at 1f78:0436. Rect map (x1, y1, x2, y2 â handler):
// [0] ( 20, 44, 39, 61) Partner toggle (1f78:017a)
// [1] ( 20, 87, 39,104) Voice toggle (1f78:0196 â DAT_2d5d_3f97)
// [2] ( 20,127, 39,144) back to profile (NextScreen=8)
@@ -1427,9 +1298,8 @@ void EEMEngine::doSetup() {
// [10] (212,153,266,184) Quit (_AreYouSure â NextScreen=0xffff)
// [11] ( 81, 25,238, 37) Credits (PIC 0x208 fullscreen)
// [12] ( 11, 1, 3, 3) debug placeholder
- // Highlight rects (`Kid1 @ 29be:1320` / `Kid2 @ 29be:1328` /
- // `SoundOn @ 29be:1330` / `SoundOff @ 29be:1338`) drive
- // `_SwapColors`; they're not click targets in the original.
+ // Highlight rects (Kid1/Kid2/SoundOn/SoundOff @ 29be:1320..1338) drive
+ // `_SwapColors` only â not click targets in the original.
if (!_font.isLoaded()) {
_nextScreen = (ScreenId)_lastScreen;
return;
@@ -1454,11 +1324,9 @@ void EEMEngine::doSetup() {
const Common::Rect kSoundOnRect (106, 86, 125, 94);
const Common::Rect kSoundOffRect (106, 96, 125, 104);
- // Pixel-level color-key swap. Mirrors `_SwapColors @ 172b:1d2a`:
- // for each pixel in `r` whose value equals `from`, replace with
- // `to`. `0xFE` is the BG's text-key color; `0x15` is the active
- // (bright) palette index, `0x00` the inactive one â both verified
- // in `_SetupSettings @ 1f78:000d`.
+ // `_SwapColors @ 172b:1d2a` â replace pixels in r where value==from
+ // with to. 0xFE = BG text-key; 0x15 = active palette index, 0x00 =
+ // inactive (set by `_SetupSettings @ 1f78:000d`).
auto swapColors = [](Graphics::ManagedSurface &dst,
const Common::Rect &r, byte from, byte to) {
const int x1 = MAX<int>(0, r.left);
@@ -1500,15 +1368,9 @@ void EEMEngine::doSetup() {
};
draw();
- // Render `picId` and block until click/key. Returns the pressed
- // keycode (KEYCODE_ESCAPE for an explicit bail, KEYCODE_INVALID
- // for a click or any other key). When `transparent` is true,
- // preserve the current screen behind and overlay `picId` with
- // its transparent colour key â mirrors `_InterfaceHelp @
- // 1560:0205` calling `_Rect_Move_Mask` with the pic's
- // `miscflags >> 8` as the transp byte. When false, do a raw
- // fullscreen blit â mirrors the credits handler at 1f78:0281
- // using `_vga_fbuffvid`.
+ // Render picId and block until input. transparent=true: overlay via
+ // `_Rect_Move_Mask` (`_InterfaceHelp @ 1560:0205`). false: raw fullscreen
+ // blit via `_vga_fbuffvid` (credits @ 1f78:0281).
auto showFullscreenPic = [&](uint16 picId,
bool transparent) -> Common::KeyCode {
Picture pic;
@@ -1519,18 +1381,13 @@ void EEMEngine::doSetup() {
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
if (transparent) {
- // Preserve the current screen so the help PIC's
- // transparent pixels show the setup BG underneath.
Graphics::Surface *cur = g_system->lockScreen();
if (cur) {
scratch.simpleBlitFrom(*cur);
g_system->unlockScreen();
}
const byte transp = (byte)(pic.flags >> 8);
- // Explicit destPos â the no-destPos overload of
- // `transBlitFrom` (managed_surface.cpp:738) stretches
- // src to dst dimensions, scaling the PIC to 320x200.
- // The original `_Rect_Move_Mask` blits at native size.
+ // Explicit destPos â no-destPos overload stretches to dst.
scratch.transBlitFrom(pic.surface, Common::Point(0, 0),
(uint32)transp);
} else {
@@ -1557,10 +1414,8 @@ void EEMEngine::doSetup() {
};
auto leaveSetup = [&]() {
- // `_DoSetup`'s entry writes `_NextScreen = _LastScreen`. We
- // honor any handler that has already overridden `_nextScreen`
- // (Credits / Save don't, but New Case / Quit do). Otherwise
- // fall back to `_lastScreen`.
+ // `_DoSetup` entry: _NextScreen = _LastScreen. Fall back to it
+ // unless a handler already overrode _nextScreen.
if (_nextScreen == kScreenSetup) {
_nextScreen = (ScreenId)_lastScreen;
if (_nextScreen == kScreenSetup ||
@@ -1570,7 +1425,7 @@ void EEMEngine::doSetup() {
saveProfile(_playerName);
};
- _nextScreen = kScreenSetup; // sentinel â leaveSetup picks the real target
+ _nextScreen = kScreenSetup; // sentinel â leaveSetup picks target
while (!shouldQuit()) {
Common::Event ev;
bool dirty = false;
@@ -1592,10 +1447,8 @@ void EEMEngine::doSetup() {
const int mx = ev.mouse.x;
const int my = ev.mouse.y;
- // Partner toggle (button [0]) â original has no symmetric
- // right-side button (the [3] rect is ScrapBook 1, not a
- // partner arrow). Direct clicks on the Jake/Jenny labels
- // are accepted as a more intuitive fallback.
+ // Partner toggle [0]. Direct Jake/Jenny label clicks are a
+ // ScummVM-only fallback.
if (kPartnerBtn.contains(mx, my)) {
_partner = _partner == 0 ? 1 : 0;
dirty = true;
@@ -1610,7 +1463,7 @@ void EEMEngine::doSetup() {
continue;
}
- // Voice toggle (button [1]).
+ // Voice toggle [1].
if (kVoiceBtn.contains(mx, my)) {
_voiceOn = !_voiceOn;
if (_audio)
@@ -1637,33 +1490,26 @@ void EEMEngine::doSetup() {
continue;
}
- // New Case (button [7]). Original handler at 1f78:01ad
- // sets `_NextScreen = 0xa` (CHOOSE_MYSTERY) and exits the
- // dispatch loop with SI=1.
+ // New Case [7] @ 1f78:01ad â NextScreen=0xa (CHOOSE_MYSTERY).
if (kNewCaseBtn.contains(mx, my)) {
saveProfile(_playerName);
_nextScreen = kScreenChooseMystery;
return;
}
- // Save (button [6]). Original calls `_SaveGame @
- // 2404:0c87` and stays in the setup loop. Our save is
- // profile-scoped (one slot per player name) â same effect.
+ // Save [6] â `_SaveGame @ 2404:0c87`.
if (kSaveBtn.contains(mx, my)) {
saveProfile(_playerName);
continue;
}
- // Done (button [8]). Original handler is just `MOV SI,1;
- // JMP exit` â `_NextScreen` stays at whatever entry set it
- // to (= `_LastScreen`).
+ // Done [8] â MOV SI,1; JMP exit (NextScreen stays = LastScreen).
if (kDoneBtn.contains(mx, my)) {
leaveSetup();
return;
}
- // Quit (button [10]). Original: `_AreYouSure(0)` â
- // confirmed â `_NextScreen = 0xffff` (sentinel quit).
+ // Quit [10] â `_AreYouSure(0)` â NextScreen=0xffff.
if (kQuitBtn.contains(mx, my)) {
if (areYouSure()) {
_nextScreen = kScreenInvalid;
@@ -1673,28 +1519,17 @@ void EEMEngine::doSetup() {
continue;
}
- // Help (button [9]). Original `_InterfaceHelp(1) @
- // 1560:0205` walks the help-pic table at `29be:00c8`:
- // each `num` slot is 5 bytes â count + two u16 PIC IDs.
- // For num=1: count=2, pics = {0x0192, 0x01B1}. The
- // original blits each pic with `_Rect_Move_Mask` â a
- // MASKED blit whose transparent colour is the pic's
- // `miscflags >> 8`, so the setup BG shows through. It
- // also hides the cursor (`MOV [0x3a00], 0` + `_RemoveMouse`
- // at the top of `_InterfaceHelp`, 1560:0216-021c). ESC
- // at any point breaks out (1560:02b3 sets uVar5 = count).
+ // Help [9] â `_InterfaceHelp(1) @ 1560:0205`. Help-pic table
+ // @ 29be:00c8: each `num` slot = 5 bytes (count + two u16
+ // PIC IDs). num=1 â count=2, pics = {0x0192, 0x01B1}. Each
+ // pic blitted via `_Rect_Move_Mask` (transparent = pic
+ // `miscflags >> 8`, so setup BG shows through). Also hides
+ // cursor (1560:0216-021c). ESC breaks (1560:02b3).
if (kHelpBtn.contains(mx, my)) {
static const uint16 kHelp1Pics[] = { 0x0192, 0x01B1 };
CursorMan.showMouse(false);
for (uint i = 0; i < ARRAYSIZE(kHelp1Pics); i++) {
- // Re-render the setup BG before each help PIC so
- // each one overlays a clean canvas. Without this,
- // `showFullscreenPic`'s `lockScreen` snapshot would
- // pick up the previous PIC and the two help cards
- // would composite together. Mirrors the original's
- // `_vga_fvidvid(0)` call at the tail of every
- // `_InterfaceHelp` iteration (1560:02e5), which
- // restores the back-buffer BG between cards.
+ // Restore BG between cards (1560:02e5 `_vga_fvidvid(0)`).
draw();
const Common::KeyCode k =
showFullscreenPic(kHelp1Pics[i], /*transparent=*/true);
@@ -1706,37 +1541,25 @@ void EEMEngine::doSetup() {
continue;
}
- // Credits (button [11]). Original handler at 1f78:025a
- // loads PIC 0x208, hides the cursor (`MOV [0x3a00], 0`
- // at 1f78:0269 + `_RemoveMouse @ 1000:542f` at 1f78:026F),
- // blits it fullscreen via `_vga_fbuffvid` (raw copy, no
- // mask), then waits for any input.
+ // Credits [11] @ 1f78:025a â PIC 0x208 fullscreen.
if (kCreditsBtn.contains(mx, my)) {
CursorMan.showMouse(false);
showFullscreenPic(0x208, /*transparent=*/false);
CursorMan.showMouse(true);
- // PIC 0x208 has its own palette baked into the BG
- // dump via `_GetPicture`; the original restores via
- // `_GetPalette` on return. Reset to setup palette
- // (SITEPALS index 0) so the setup BG renders right.
+ // PIC 0x208 has its own baked palette; restore site 0.
setSitePalette(0);
dirty = true;
continue;
}
- // Profile (button [2]). Original handler writes
- // `_NextScreen = 8`, returning to the player/profile
- // picker. Save the current profile settings first, then
- // let the screen driver run profile â partner â case/map.
+ // Profile [2] â NextScreen=8.
if (kProfileBtn.contains(mx, my)) {
saveProfile(_playerName);
_nextScreen = kScreenProfile;
return;
}
- // ScrapBook 1 / 2 / 3 (buttons [3] / [4] / [5]). Original
- // handlers call `_ShowScrapbook(stage, 0)`, with stages 2
- // and 3 gated by chain progress.
+ // ScrapBook [3]/[4]/[5] â `_ShowScrapbook(stage, 0)`.
if (kScrap1Btn.contains(mx, my)) {
doShowScrapbook(1);
setSitePalette(0);
@@ -1764,17 +1587,9 @@ void EEMEngine::doSetup() {
}
void EEMEngine::doActionScreen() {
- // Mirrors `_ActionScreen` @ 1c33:195b. The original draws background
- // PIC 0x104 plus PIC 9 at (10, 0x87), then calls `_DoChoose` with
- // `ActionNames @ 29be:0d6a`. The "Book N" heading belongs only to
- // `_CaseSelection`, so this top-level menu intentionally does not
- // draw one.
- // Layout:
- // list[0] = "----------------------------------"
- // list[1] = " Choose A Mystery"
- // list[2..10] = alternating menu items + separators
- // Five selectable items: Choose A Mystery / Practice Mystery /
- // See ScrapBook 1/2/3.
+ // `_ActionScreen @ 1c33:195b` â BG PIC 0x104 + PIC 9 @ (10, 0x87),
+ // `_DoChoose` with ActionNames @ 29be:0d6a. 5 picks alternating with
+ // separators.
enum MenuPick {
kPickChoose = 0,
kPickPractice,
@@ -1783,9 +1598,7 @@ void EEMEngine::doActionScreen() {
kPickScrap3,
kNumPicks
};
- // Localized action-menu labels. Spanish text is taken verbatim
- // from the Spanish floppy EEM.EXE (`eem-full-game/floppy-es/`):
- // "Elegir Misterio" / "Caso de Practica" / "Ver Recortes 1..3".
+ // Spanish from EEM.EXE floppy-es.
const char *kPickLabelEN[kNumPicks] = {
" Choose A Mystery",
" Practice Mystery",
@@ -1801,17 +1614,11 @@ void EEMEngine::doActionScreen() {
" Ver Recortes 3"
};
const char * const *kPickLabel = isSpanish() ? kPickLabelES : kPickLabelEN;
- // Menu entry gating per `_ActionScreen @ 1c33:195b` â the asm at
- // 1c33:19d1-1a70 sets greys[] based on chain stage AND per-tier
- // solve count:
- // stage 1 â grey ScrapBook 2/3; grey ScrapBook 1 if no tier-1 solves
- // stage 2 â grey Practice + ScrapBook 3; grey ScrapBook 2 if no tier-2 solves
- // stage 3 â grey Practice; grey ScrapBook 3 if no tier-3 solves
- // stage 4 â grey Choose + Practice (post-completion read-only state)
- // In other words: each tier's ScrapBook unlocks as soon as you've
- // solved your first case in that tier. Practice Mystery is only
- // available at stage 1. Choose A Mystery is greyed once every case
- // in every tier is solved (stage 4).
+ // Gating @ 1c33:19d1-1a70 by chain stage + per-tier solves:
+ // stage 1: grey SB2/3; SB1 needs any tier-1 solve
+ // stage 2: grey Practice + SB3; SB2 needs tier-2 solve
+ // stage 3: grey Practice; SB3 needs tier-3 solve
+ // stage 4: grey Choose + Practice
bool anySolved1 = false;
for (uint i = 1; i <= 0x18 && i < sizeof(_mysteriesSolved); i++)
if (_mysteriesSolved[i]) { anySolved1 = true; break; }
@@ -1833,8 +1640,7 @@ void EEMEngine::doActionScreen() {
const bool kPickEnabled[kNumPicks] = {
chooseOn, practiceOn, scrap1On, scrap2On, scrap3On
};
- // Seed selection on the first enabled entry â at stage 4 the
- // `Choose A Mystery` default is greyed, so we land on ScrapBook 1.
+ // Seed selection on first enabled entry.
uint pick = 0;
for (uint i = 0; i < kNumPicks; i++) {
if (kPickEnabled[i]) { pick = i; break; }
@@ -1842,27 +1648,15 @@ void EEMEngine::doActionScreen() {
const char *kSeparator = "----------------------------------";
- // Click rectangles from the original `_DoChoose` @ 1c33:0514 â each
- // `_InRect(_MouseX, _MouseY, addr, 0x29be)` reads one 4Ãu16 rect at
- // the listed offset in segment 29be ({x1, y1, x2, y2}). We use
- // `Common::Rect` (left/top/right/bottom) which also gives us
- // `contains(x, y)` for hit testing.
+ // `_DoChoose @ 1c33:0514` click rects (4Ãu16 {x1,y1,x2,y2} in seg 29be):
const Common::Rect kOkRect ( 12, 63, 41, 87); // 29be:0cd8 confirm
const Common::Rect kHelpRect ( 12, 100, 41, 124); // 29be:0ce0 help
const Common::Rect kExitRect ( 12, 137, 41, 161); // 29be:0ce8 cancel
const Common::Rect kListRect ( 58, 35, 238, 158); // 29be:0d00 list panel
- // The original `_NewPlayer` set `_MouseCursor = 1` on exit; the
- // chain of screens after it expects the cursor to stay visible.
- // Reassert here in case anything between hid it.
CursorMan.showMouse(true);
- // Reassert site palette 0 (the chooser CLUT). In
- // the normal flow `doProfilePicker` (or the post-screen reset paths
- // at lines 1402 / 1147 / 1121) leaves us on palette 0 already, but
- // the launcher-resume path jumps straight here from `_AllBlack`
- // (palette = all-zero) â without this the BG renders into a black
- // CLUT and the player sees an empty screen.
+ // Launcher-resume path can enter from `_AllBlack` palette.
setSitePalette(0);
Picture bg;
@@ -1893,11 +1687,8 @@ void EEMEngine::doActionScreen() {
ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
return;
if (ev.type == Common::EVENT_LBUTTONDOWN) {
- // OK / EXIT / HELP buttons (rectangles from `_DoChoose`).
if (kOkRect.contains(ev.mouse.x, ev.mouse.y)) {
- // Greyed entries can't be confirmed (mirrors
- // `_DoChoose @ 1c33:0635` â clicks on a `_Greys[i]
- // != 0` row are ignored before `select` is set).
+ // Greyed entries ignored (`_DoChoose @ 1c33:0635`).
if (kPickEnabled[pick])
confirmed = true;
break;
@@ -1908,12 +1699,9 @@ void EEMEngine::doActionScreen() {
break;
}
if (kHelpRect.contains(ev.mouse.x, ev.mouse.y)) {
- // `_ActionScreen` sets `Chelp = 0`, so `_DoChoose`
- // ignores this middle button on the top-level menu.
+ // `_ActionScreen` sets Chelp=0.
continue;
}
- // List panel: click on a non-separator row selects the
- // menu entry under the cursor.
if (kListRect.contains(ev.mouse.x, ev.mouse.y)) {
const int kLineH = 10;
const int row = (ev.mouse.y - kListRect.top) / kLineH;
@@ -1942,10 +1730,7 @@ void EEMEngine::doActionScreen() {
break;
}
if (k == Common::KEYCODE_UP || k == Common::KEYCODE_LEFT) {
- // Cycle backwards through enabled picks (mirrors the
- // `_DoChoose` arrow handlers @ 1c33:0514). Loop is
- // bounded by `kNumPicks` so a row of all-disabled picks
- // can't spin forever.
+ // `_DoChoose` arrow handlers @ 1c33:0514, bounded loop.
for (int i = 0; i < (int)kNumPicks; i++) {
pick = (pick == 0) ? (uint)(kNumPicks - 1) : pick - 1;
if (kPickEnabled[pick])
@@ -2010,12 +1795,8 @@ void EEMEngine::doActionScreen() {
}
if (pick == kPickScrap1 || pick == kPickScrap2 || pick == kPickScrap3) {
- // `_ActionScreen` handlers at 1c33:1B13 / 1B26 / 1B40 each
- // call `_ShowScrapbook(0, stage)` for the matching tier
- // (verified at the action-handler jumptable bytes
- // `01 03 05 07 09 ff` paired with handlers at 1c33:1be1).
- // Viewing the scrapbook never starts a new case; return to
- // the same action menu afterwards.
+ // `_ActionScreen` handlers @ 1c33:1B13 / 1B26 / 1B40 â
+ // `_ShowScrapbook(0, stage)`. Returns here afterwards.
const uint stage = (pick == kPickScrap1) ? 1
: (pick == kPickScrap2) ? 2 : 3;
doShowScrapbook(stage);
@@ -2029,11 +1810,8 @@ void EEMEngine::doActionScreen() {
}
void EEMEngine::doCaseSelection() {
- // Mirrors `_DoChooseMystery @ 1a35:02b7` + `_CaseSelection @
- // 1c33:0a87`. `_DoChooseMystery` loads BOOK<stage>.NME and
- // `_CaseSelection` draws PIC 0x41 plus the centered "Book N" /
- // "Challenge Book" title. This screen is entered only after the
- // player selects "Choose A Mystery" on `_ActionScreen`.
+ // `_DoChooseMystery @ 1a35:02b7` + `_CaseSelection @ 1c33:0a87` â
+ // loads BOOK<stage>.NME, draws PIC 0x41 + centered "Book N" title.
const uint kMaxMystery = 54;
CursorMan.showMouse(true);
@@ -2047,9 +1825,7 @@ void EEMEngine::doCaseSelection() {
const bool haveRevealPic =
_picsArchive.getPicture(kCaseSelectionRevealPic, revealPic);
- // KD greeter sprite. `_CaseSelection @ 1c33:0a87` loads anim 0x15
- // (Jake-paired) or 0x16 (Jenny-paired), then runs it through the
- // chooser loop via `_UpdateAnimations`.
+ // `_CaseSelection @ 1c33:0a87` greeter ANI 0x15 (Jake) / 0x16 (Jenny).
const uint kKdAniId = (_partner == 0) ? 0x15 : 0x16;
Animation kdAnim;
const bool haveKdAnim = _aniArchive.loadAnimation(kKdAniId, kdAnim)
@@ -2057,10 +1833,8 @@ void EEMEngine::doCaseSelection() {
const int kKdAnimX = 0x112;
const int kKdAnimY = 0x50;
- // Stage roster:
- // stage 1 (Junior, BOOK1.NME) -> mysteries 1..24
- // stage 2 (Senior, BOOK2.NME) -> mysteries 25..48
- // stage 3 (Master, BOOK3.NME) -> mysteries 49..54
+ // stage 1 (Junior, BOOK1.NME) = 1..24, stage 2 (Senior, BOOK2.NME) =
+ // 25..48, stage 3 (Master, BOOK3.NME) = 49..54.
uint stageLo = 1, stageHi = 0x18;
uint book = 1;
switch (_chainStage) {
@@ -2078,8 +1852,7 @@ void EEMEngine::doCaseSelection() {
}
const uint listLen = MIN<uint>((uint)names.size(), stageHi - stageLo + 1);
- // Per-row solved flags. `_DoChoose @ 1c33:0521` skips solved entries
- // when seeding the initial selection and ignores clicks on them.
+ // Solved-row gating @ `_DoChoose @ 1c33:0521`.
Common::Array<bool> solvedFlags;
solvedFlags.resize(listLen);
for (uint i = 0; i < listLen; i++) {
@@ -2143,10 +1916,8 @@ void EEMEngine::doCaseSelection() {
return;
}
if (kChooserHelpRect.contains(ev.mouse.x, ev.mouse.y)) {
- // Original `_CaseSelection` returns 0xfffe here,
- // then `_ChooseSavedGame` runs. ScummVM stores
- // in-progress cases in the profile, so route to
- // the profile picker instead.
+ // Original returns 0xfffe â `_ChooseSavedGame`. ScummVM
+ // stores in-progress cases per profile.
saveProfile(_playerName);
_mystery.clear();
_nextScreen = kScreenProfile;
@@ -2170,9 +1941,7 @@ void EEMEngine::doCaseSelection() {
const uint idx = topRow + (uint)row;
if (idx >= listLen || solvedFlags[idx])
continue;
- // Second click on the already-selected row counts as
- // the OK button â saves the player a trip down to the
- // bottom-bar after picking a mystery.
+ // Second click on selected row = OK (ScummVM ergonomic).
if (idx == selRow) {
confirmed = true;
break;
@@ -2277,51 +2046,25 @@ void EEMEngine::doCaseSelection() {
}
void EEMEngine::doNotebook() {
- // Mirrors `_DoNotebook @ 161e:0500` + `_DrawNotes @ 161e:01d0` +
- // `_HandleNoteButton @ 161e:03cb`.
- //
- // Layout (verified from Ghidra labels in 29be:013f / 29be:0147):
- // _NotebookRect = (78, 12, 288, 152) â note display rectangle.
- // _NoteButtons (11 entries, 8 bytes each, at 29be:0147):
- // [0] (134, 174, 155, 190) decorative â `_HandleNoteButton(0)`
- // returns immediately (i-1 unsigned > 9).
- // [1] (93, 174, 115, 190) â `_InterfaceHelp(0)` (handler 0x3f9)
- // [2] (157, 174, 178, 190) â handler 0x477 (page nav)
- // [3] (5, 80, 44, 110) â `_KDHelp` (host hint, 0x403)
- // [4] (180, 174, 201, 190) â solve / accuse (0x436)
- // [5] (204, 174, 224, 190) â `_NextScreen = 5` (gallery, 0x489)
- // [6] (226, 174, 247, 190) â handler 0x4ab
- // [7] (7, 177, 57, 200) â handler 0x480 (back to map)
- // [8] (35, 111, 56, 136) â `_NextScreen = 3` (site)
- // [9] (0, 0, 0, 0) â same exit as [8]
- // [10] (66, 79, 267, 174) â `_InterfaceHelp(0)` (note area)
- // Background: PIC 0x3f.
- // Partner anim: anim 1 (Jake) / 0xb (Jenny) at (5, 80).
+ // `_DoNotebook @ 161e:0500` + `_DrawNotes @ 161e:01d0` +
+ // `_HandleNoteButton @ 161e:03cb`. _NotebookRect = (78, 12, 288, 152).
+ // _NoteButtons @ 29be:0147 â 11 rects à 8 bytes. Jumptable @ 161e:04ec
+ // dispatches handler[i-1] (rect 0's i-1 underflows = decorative slot):
+ // [0] (134,174,155,190) decorative â no handler
+ // [1] ( 93,174,115,190) HELP â `_InterfaceHelp(0)` (0x3f9)
+ // [2] (157,174,178,190) GALLERY â `_NextScreen = 5` (0x477)
+ // [3] ( 5, 80, 44,110) host hint â `_KDHelp` (0x403)
+ // [4] (180,174,201,190) SOLVE â `_SolvedCheck` â NextScreen=7 (0x436)
+ // [5] (204,174,224,190) PAGE NEXT â `_EraseNotes` + redraw (0x489)
+ // [6] (226,174,247,190) PAGE PREV â CurrentPage-- + redraw (0x4ab)
+ // [7] ( 7,177, 57,200) MAP â `_NextScreen = 2` (0x480)
+ // [8] ( 35,111, 56,136) SITE â `_NextScreen = 3` (0x3ed)
+ // [9] ( 0, 0, 0, 0) same exit as [8]
+ // [10] (267,174,288,190) â `_InterfaceHelp(0)` again (0x3f9)
+ // BG PIC 0x3f; partner ANI 1 (Jake) / 0xb (Jenny) at (5, 80).
if (!_mystery.isLoaded() || !_font.isLoaded())
return;
- // Button rects from `_NoteButtons @ 29be:0147` matched to handler
- // addresses via the jump table at `_HandleNoteButton + 0xec` (i.e.
- // 161e:04ec). Decoded handlers (i = rect_index, dispatch = handler[i-1]):
- // rect 0 (134,155) â no handler (i-1 underflows; original treats
- // this as a decorative/no-op slot)
- // rect 1 (93,115) â 0x03f9 = `_InterfaceHelp(0)` (HELP)
- // rect 2 (157,178) â 0x0477 = `_NextScreen = 5` (GALLERY)
- // rect 3 (5,80) â 0x0403 = `_KDHelp` (host hint)
- // rect 4 (180,201) â 0x0436 = `_SolvedCheck` -> NextScreen=7 (SOLVE)
- // rect 5 (204,224) â 0x0489 = `_EraseNotes` + `_DrawNotes` (PAGE NEXT)
- // rect 6 (226,247) â 0x04ab = decrement CurrentPage + redraw (PAGE PREV)
- // rect 7 (7,177) â 0x0480 = `_NextScreen = 2` (MAP)
- // rect 8 (35,111) â 0x03ed = `_NextScreen = 3` (SITE)
- // rect 9 (0,0) â 0x03ed = same as rect 8
- // rect 10 (66,79) â 0x03f9 = `_InterfaceHelp(0)` (note-area help)
- // (`_NoteButtons @ 29be:0147` actually has rect [10] at
- // (267,174,288,190) â small button on the right of the bottom
- // bar that the original handler dispatch table at 161e:04ec
- // routes to `_InterfaceHelp(0)` again. Earlier this rect was
- // mis-noted as a "note area" of (66,79,267,174) â that
- // rectangle exists nowhere in the binary's button table.)
-
CursorMan.showMouse(true);
int page = 0;
@@ -2372,10 +2115,7 @@ void EEMEngine::doNotebook() {
}
}
if (ev.type == Common::EVENT_LBUTTONDOWN) {
- // Test buttons in the order the original would â
- // button 0 / 9 are dead zones, so check the actionable
- // rects directly. Earlier rects "win" when overlapping
- // (matches `_FindButton`).
+ // Earlier rects win on overlap (matches `_FindButton`).
if (kPdaSiteRect.contains(ev.mouse.x, ev.mouse.y)) {
_nextScreen = kScreenSite;
exitFlag = true;
@@ -2403,8 +2143,7 @@ void EEMEngine::doNotebook() {
break;
}
if (kPdaHelpRect.contains(ev.mouse.x, ev.mouse.y)) {
- // rect 1 â `_InterfaceHelp(0)`: walks `HelpData[0]` and
- // blits PICs 0x63 / 0x1ae fullscreen for click-through.
+ // rect 1 â `_InterfaceHelp(0)` (PICs 0x63 / 0x1ae).
setInteractiveMouseCursor(false);
doInterfaceHelp(0);
dirty = true;
@@ -2422,22 +2161,14 @@ void EEMEngine::doNotebook() {
continue;
}
if (kPdaHelp2Rect.contains(ev.mouse.x, ev.mouse.y)) {
- // `_NoteButtons[10]` â handler 0x03f9 = same
- // `_InterfaceHelp(0)` as button [1].
+ // _NoteButtons[10] â handler 0x03f9 = same as [1].
setInteractiveMouseCursor(false);
doInterfaceHelp(0);
dirty = true;
continue;
}
- // Click on a clue's slot rect â toggle selection. The
- // original `_DoNotebook` doesn't do this â note
- // selection lives in the accuse screen there â but
- // keyboard 1..9 toggling is awkward, and the resulting
- // `_NoteSelected` state is what `_SolvedCheck` reads
- // either way. Slot rects are the per-clue rectangles
- // `drawNotebookFrame` publishes, so this just
- // reproduces the visible-text-bbox click without the
- // previous bogus outer-area gate.
+ // ScummVM-only: click on clue slot toggles _NoteSelected
+ // (original toggles only in accuse screen).
for (uint i = 0; i < _notebookSlotRects.size(); i++) {
if (_notebookSlotRects[i].contains(ev.mouse.x,
ev.mouse.y)) {
@@ -2453,7 +2184,7 @@ void EEMEngine::doNotebook() {
break;
const uint32 now = g_system->getMillis();
- // Re-render every 100 ms so the partner sprite cycles frames.
+ // Re-render every 100 ms for partner sprite cycle.
if (dirty || now - lastDraw >= 100) {
drawNotebookFrame(page);
lastDraw = now;
@@ -2469,50 +2200,31 @@ void EEMEngine::doNotebook() {
}
void EEMEngine::drawNotebookFrame(int &page) {
- // PDA notebook redraw â formerly the `draw` lambda inside `doNotebook`.
- // Mirrors `_DrawNotes @ 161e:01d0` for the per-page note layout, plus
- // the partner-sprite blit at (5, 80) (`_NewAnimation` from
- // `_DoNotebook @ 161e:0500`). Uses `_notebookSlotRects` /
- // `_notebookSlotClues` to publish the per-page slot layout to the
- // click handler in `doNotebook`.
+ // `_DrawNotes @ 161e:01d0` per-page layout + partner sprite at (5,80)
+ // from `_DoNotebook @ 161e:0500`.
const Common::Rect kNotebookRect(78, 12, 288, 152);
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
- // PIC 0x3f frame.
Picture frame;
if (_picsArchive.getPicture(0x3f, frame))
scratch.simpleBlitFrom(frame.surface);
- // Partner sprite at (5, 80). Anim 1 for Jake, 0xb (11) for Jenny
- // for CELLS, but the original `_DoNotebook @ 161e:0500` always
- // uses script 0x01 (verified by `CONCAT22(1, ...)` in its
- // `_NewAnimation` call at 161e:054c). Both 0x01 and 0x0b have
- // the SAME script in `kAnimScripts` (alias), so both lookups
- // produce identical results â but routing through 0x01
- // matches the original verbatim.
+ // Partner ANI 1/0xb (cells); script 0x01 (`_NewAnimation @ 161e:054c`).
const uint partnerAnim = (_partner == 0) ? 1 : 0xb;
Animation partnerAni;
if (_aniArchive.loadAnimation(partnerAnim, partnerAni) && !partnerAni.empty()) {
const uint32 now = g_system->getMillis();
const uint frameIdx = partnerFrameAtTick(0x01,
(uint)partnerAni.size(), now);
- // Anchor-aware blit. The PDA partner (anim 0x01/0x0b) cells
- // have miscflags = rowoff = 0 in the audit, but routing
- // through `blitAnimFrameAnchored` is harmless and keeps the
- // rendering path consistent with the BigMap partner.
blitAnimFrameAnchored(scratch.surfacePtr(),
partnerAni[frameIdx], 5, 80);
}
- // Notes â `_DrawNotes` walks `_NoteIndex` for the current page,
- // rendering each found clue's text inside `_NotebookRect` with
- // word-wrap. Selected clues are highlighted (color 0x3c in the
- // original's case-briefing palette).
- // Build a list of found-clue indices, identical ordering to the
- // original's iteration through `_CluesFound[]`.
+ // `_DrawNotes` walks `_NoteIndex` for current page; word-wraps each
+ // found clue in `_NotebookRect`. Selected = color 0x3c.
Common::Array<uint> found;
for (uint i = 0; i < Mystery::kCluesFoundCap; i++) {
if (_mystery._cluesFound[i] && _mystery.noteHasNotebookText(i))
@@ -2526,29 +2238,14 @@ void EEMEngine::drawNotebookFrame(int &page) {
const int kRectW = kNotebookRect.width();
const int kRectH = kNotebookRect.height();
- // Walk forward to the start clue of the current page.
- // Each page renders as many clues as fit in `kRectH`.
+ // Walk forward to start clue of current page; fits as many as kRectH.
int clueCursor = 0;
Common::Array<int> pageStarts;
pageStarts.push_back(0);
- // Floppy NoteIndex entries are 7 bytes:
- // +0..1 clue text offset (ABSOLUTE byte offset into the
- // mystery blob; partner-agnostic clue statement â
- // this is what the notebook displays).
- // +2..3 Jake's spoken line offset (used by
- // `_DisplayHotspotClue_Floppy` when Jake narrates a
- // dialog record â NOT for the notebook).
- // +4..5 Jenny's spoken line offset (same, for Jenny).
- // +6 score.
- // Verified at `_DrawNotes_Floppy / FUN_15e0_01e8`'s
- // `*(int *)(notes + idx * 7)` access (no `+2`/`+4` shift â
- // always reads byte +0). The earlier port used the dialog
- // spoken line for the notebook, which showed the partner's
- // "Hi, Jake! Hi, Jenny!" intro instead of the actual clue
- // statement ("We received a note that says..."). CD entries
- // are 4 bytes with offsets relative to the TextBlock at
- // header[+0xc]. Resolve the right text for the active variant
- // once per render.
+ // Floppy NoteIndex entry (7 bytes): +0 clueTextOff (absolute, what
+ // the notebook displays), +2 Jake spoken, +4 Jenny spoken, +6 score.
+ // `_DrawNotes_Floppy / FUN_15e0_01e8` reads only byte +0.
+ // CD entries are 4 bytes, offsets relative to TextBlock @ header[+0xc].
const bool floppyNb = isFloppy();
const byte *bufBase = _mystery.blobAt(0);
const uint32 mysSz = _mystery.dataSize();
@@ -2594,8 +2291,7 @@ void EEMEngine::drawNotebookFrame(int &page) {
page = 0;
}
- // Track per-slot rectangles so the click handler can map a
- // click in `kNoteArea` back to a clue index.
+ // Per-slot rects published to the click handler.
Common::Array<Common::Rect> slotRects;
Common::Array<uint> slotClues;
@@ -2611,12 +2307,7 @@ void EEMEngine::drawNotebookFrame(int &page) {
if (txt.empty())
txt = Common::String::format(
isSpanish() ? "nota %u" : "clue %u", clueId);
- // Per `_DrawNotes @ 161e:01d0`: text uses
- // `_NoteUnselectedColor` (0x5c=cyan) for unselected and 0x3c
- // (light yellow-white) for selected. Both contrast cleanly
- // with the PDA screen's natural blue, so we draw text
- // directly on PIC 0x3f without an extra fill rectangle â
- // matches the original design.
+ // `_DrawNotes @ 161e:01d0`: 0x5c unselected, 0x3c selected.
Common::Array<Common::String> wrapped;
_font.wordWrapText(txt, kRectW, wrapped);
const int lineH = _font.getFontHeight();
@@ -2632,12 +2323,7 @@ void EEMEngine::drawNotebookFrame(int &page) {
y += h + 7;
}
- // Page indicator only â the original `_DrawNotes @ 161e:01d0`
- // has no points display in the PDA notebook (the per-clue point
- // values are SPOILERS for the chain weighting that the engine
- // uses internally for `_GetSelectedPoints`). Showing the running
- // total tells the player exactly when they have enough evidence
- // to solve, which deflates the deduction step.
+ // Page indicator only (original has no points display).
_font.drawString(&scratch, Common::String::format("p%d/%d",
page + 1, (int)pageStarts.size()),
270, 4, 320, 0x5C);
@@ -2652,27 +2338,11 @@ void EEMEngine::drawNotebookFrame(int &page) {
}
void EEMEngine::doGallery() {
- // Mirrors `_DoGallery @ 158f:065b` and `_DrawGallery @ 158f:0046`.
- // Verified directly from the disassembly:
- // * Background: PIC 0x3f (same as PDA).
- // * Partner sprite at (5, 0x50): anim 2 (Jake) / 0x10 (Jenny).
- // `_NewAnimation(5, 0x50, ...)`. NOTE: gallery uses anim 2/0x10,
- // PDA uses 1/0xb â different sprites.
- // * Five fixed slot positions at `29be:0x116` (4 bytes per slot,
- // `{u16 x, u16 y}`):
- // slot 0 = ( 83, 14) slot 3 = (119, 90)
- // slot 1 = (155, 14) slot 4 = (191, 90)
- // slot 2 = (227, 14)
- // * For each logical suspect i in 0..NumSuspects-1:
- // picId = `*(u16 *)(_GalleryData + i * 0x46)` (entry +0).
- // visible = `_InGallery[_NewOrder[i]] != 0`.
- // drawX = positions[_NewOrder[i]].x
- // drawY = positions[_NewOrder[i]].y + (0x48 - pic.height)
- // So portraits are BOTTOM-aligned to baselines 0x48 + pos.y.
- // * Click on portrait via `_SearchSuspects` â `MoreInfo(i)` shows
- // the suspect detail page. ESC returns to PDA.
- // * Frame-cycled @ 100ms via `_CheckFrameRate` + `_UpdateAnimations`
- // + `_GizmoColorCycle`.
+ // `_DoGallery @ 158f:065b` + `_DrawGallery @ 158f:0046`. BG PIC 0x3f.
+ // Partner ANI 2 (Jake) / 0x10 (Jenny) at (5, 0x50). 5 slot positions
+ // @ 29be:0x116. Per suspect i: picId = *(u16*)(_GalleryData + i*0x46);
+ // visible = _InGallery[_NewOrder[i]]; drawY = pos.y + (0x48 - pic.h)
+ // (bottom-aligned to baseline). Click â `_SearchSuspects` â moreInfo.
if (!_mystery.isLoaded())
return;
@@ -2732,21 +2402,17 @@ void EEMEngine::doGallery() {
}
}
if (ev.type == Common::EVENT_LBUTTONDOWN) {
- // PDA bottom-bar buttons mirror `_NoteButtons @ 29be:0147`.
- // `_DoGallery @ 158f:065b` shares the SAME button table
- // with `_DoNotebook` (both call `_FindButton` against the
- // 11-entry table at 0x147). `_HandleGalleryButton @
- // 158f:05c0` dispatches via a different jump table
- // (158f:0645). Verified gallery button mapping:
- // rect 0 (134,155) â 0x05ef = `_NextScreen = 4` (NOTEBOOK)
- // rect 1 (93,115) â 0x0625 = `_InterfaceHelp` (HELP)
- // rect 2 (157,178) â 0x0638 = generic exit (no-op)
- // rect 3 (5,80) â 0x061e = `_KDHelp` (host hint)
- // rect 4 (180,201) â 0x05ff = `_SolvedCheck` -> SOLVE
- // rect 5 (204,224) â 0x0638 = generic exit
- // rect 6 (226,247) â 0x0638 = generic exit
- // rect 7 (7,177) â 0x05f7 = `_NextScreen = 2` (MAP)
- // rect 8 (35,111) â 0x05e4 = `_NextScreen = 3` (SITE)
+ // Shares _NoteButtons @ 29be:0147 with doNotebook;
+ // _HandleGalleryButton @ 158f:05c0 jumptable @ 158f:0645:
+ // [0] (134,155) â NOTEBOOK = NextScreen=4 (0x5ef)
+ // [1] ( 93,115) â HELP = _InterfaceHelp(0) (0x625)
+ // [2] (157,178) â generic exit (no-op) (0x638)
+ // [3] ( 5, 80) â _KDHelp (host hint) (0x61e)
+ // [4] (180,201) â _SolvedCheck â SOLVE (0x5ff)
+ // [5] (204,224) â generic exit (0x638)
+ // [6] (226,247) â generic exit (0x638)
+ // [7] ( 7,177) â MAP = NextScreen=2 (0x5f7)
+ // [8] ( 35,111) â SITE = NextScreen=3 (0x5e4)
if (kPdaSiteRect.contains(ev.mouse.x, ev.mouse.y)) {
_nextScreen = kScreenSite;
exitFlag = true;
@@ -2768,9 +2434,7 @@ void EEMEngine::doGallery() {
break;
}
if (kPdaHelpRect.contains(ev.mouse.x, ev.mouse.y)) {
- // Gallery rect 1 â `_InterfaceHelp(0)` per jmp table at
- // 158f:0625 (HandleGalleryButton). Same picture sequence
- // as the notebook HELP button.
+ // rect 1 â `_InterfaceHelp(0)` (158f:0625).
setInteractiveMouseCursor(false);
doInterfaceHelp(0);
lastDraw = 0;
@@ -2782,8 +2446,7 @@ void EEMEngine::doGallery() {
lastDraw = 0;
continue;
}
- // `_SearchSuspects` walks the per-slot rects and returns
- // the suspect index. We mirror that with cached rects.
+ // `_SearchSuspects` â per-slot rect â suspect index.
bool clicked = false;
for (uint i = 0; i < slotRects.size(); i++) {
if (slotSuspect[i] < 0)
@@ -2792,9 +2455,6 @@ void EEMEngine::doGallery() {
if (moreInfo(gd, (uint)slotSuspect[i],
galBg, haveBg))
exitFlag = true;
- // Force gallery redraw immediately so the
- // player isn't left looking at the dismissed
- // MoreInfo screen until the next 100 ms tick.
drawGalleryFrame(gd, num, slotRects, slotSuspect);
lastDraw = g_system->getMillis();
mouse = g_system->getEventManager()->getMousePos();
@@ -2823,11 +2483,6 @@ void EEMEngine::doGallery() {
gallerySlotAt(slotRects, slotSuspect,
mouse.x, mouse.y));
}
- // `g_system->updateScreen()` is what tells the framework to
- // re-render the cursor at its current mouse position; without
- // it here, the cursor only refreshes when `drawGalleryFrame`
- // runs (every 100 ms) and visibly lags the mouse. Match
- // `doNotebook`'s per-tick `updateScreen()` cadence (line 1548).
g_system->updateScreen();
g_system->delayMillis(15);
}
@@ -2836,28 +2491,13 @@ void EEMEngine::doGallery() {
bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
const Picture &galBg, bool haveBg) {
- // Suspect-detail page. Mirrors `MoreInfo @ 158f:0419`:
- // _RefreshGalleryBackground();
- // _GetPicture(*(u16*)(gd + i*0x46));
- // _AddPicBackground(pic, 0x94, 0xf);
- // _DrawGalleryNotes(gd + i*0x46);
- // loop until ESC or button click.
- // Suspect data layout differs by variant:
- // * CD (`158f:0419`): fixed 0x46-byte stride.
- // +0..1 picId
- // +8..9 clue count (u16)
- // +0xa.. array of u16 clue IDs (max 30,
- // terminated by 0xFFFF if short).
- // * Floppy (`_MoreInfo_Floppy = 154e:042b` â
- // `FUN_154e_0201` â `FUN_15e0_01e8`):
- // variable-stride.
- // +0..1 picId
- // +2..3 alibi (0xFFFF = guilty)
- // +4 clue count (u8)
- // +5.. u8 clue IDs (per the asm at
- // 154e:020e..0282 which calls
- // `FUN_15e0_01e8(rect, entry+5,
- // entry[4], NULL)`).
+ // `MoreInfo @ 158f:0419` â suspect detail. PIC = entry+0,
+ // _AddPicBackground at (0x94, 0xf), notes via _DrawGalleryNotes.
+ // CD layout (0x46-byte stride): +0 picId, +8 clueCount(u16), +0xa
+ // clue IDs(u16, 0xFFFF terminator, max 30).
+ // Floppy (`_MoreInfo_Floppy = 154e:042b` â `FUN_15e0_01e8`):
+ // variable stride. +0 picId, +2 alibi (0xFFFF = guilty), +4 count(u8),
+ // +5 clue IDs(u8).
const bool floppyMI = isFloppy();
const byte *suspect = floppyMI
? _mystery.floppySuspectEntry(suspectIdx)
@@ -2871,10 +2511,8 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
setInteractiveMouseCursor(false);
- // Suspect's clue notes inside _GalleryNoteRect
- // = (78, 93, 288, 152), per 29be:0100. Cyan text
- // renders directly on the PDA's natural blue
- // screen â matches `_DrawGalleryNotes @ 158f:01f4`.
+ // _GalleryNoteRect = (78, 93, 288, 152) @ 29be:0100.
+ // `_DrawGalleryNotes @ 158f:01f4`.
const int rx = 78, ry = 93;
const int rw = 288 - 78, rh = 152 - 93;
const int lineH = _font.getFontHeight();
@@ -2882,14 +2520,7 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
const byte *ni = _mystery.noteIndex();
const uint16 niCount = _mystery.noteIndexCount();
- // Pagination matches `_DrawNotes @ 161e:01d0` / `MoreInfo @
- // 158f:0419`: the original tracks `_NextClue` across redraws and
- // only emits clues whose wrapped height fits the rect, deferring
- // the rest to the next page. `_PageBreaks[]` stores the
- // entry-point for each page so PREV (button 6 / case 6 at
- // 158f:03b8 â `SUB _CurrentPage, 2; _NextClue = _PageBreaks[...]`)
- // can step back. We mirror that with an explicit stack of
- // page-start indices: forward push, back pop.
+ // Pagination via `_NextClue` + `_PageBreaks[]` (case 6 @ 158f:03b8).
uint pageStart = 0;
Common::Array<uint> pageStack;
bool back = false;
@@ -2902,13 +2533,7 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
ms.clear();
if (haveBg)
ms.simpleBlitFrom(galBg.surface);
- // Partner sprite at (5, 0x50). The original `MoreInfo @
- // 158f:0419` calls `_RefreshGalleryBackground` (clears the
- // portrait grid) but the partner anim slot registered by
- // `_DoGallery` keeps painting at every `_UpdateAnimations`
- // tick â the suspect detail pic covers the right side only
- // (drawn at 0x94, 0xf), so the partner stays visible on the
- // left. Re-blit per page so the frame stays current.
+ // Partner sprite at (5, 0x50). Re-blitted per page.
{
const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
Animation partnerAni;
@@ -2921,7 +2546,6 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
partnerAni[frameIdx], 5, 0x50);
}
}
- // Full suspect picture at (0x94, 0xf).
Picture detail;
if (_picsArchive.getPicture(detailPic, detail)) {
const byte transp = (byte)(detail.flags >> 8);
@@ -2929,12 +2553,8 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
Common::Point(0x94, 0x0f), transp);
}
- // Walk the clue list from `pageStart`. Skip clues that aren't
- // found / lack a note entry, then measure the wrapped height
- // before drawing. If a clue would overflow and we've already
- // drawn at least one, defer it to the next page; if it's the
- // FIRST clue on this page (one clue too tall to ever fit),
- // draw it anyway so progress is guaranteed.
+ // Walk clues from pageStart; defer overflow to next page unless
+ // first clue is too tall to ever fit.
int yPos = ry;
bool drewAny = false;
uint k = pageStart;
@@ -2952,10 +2572,8 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
continue;
if (!ni || clueId >= niCount)
continue;
- // Floppy notes: 7-byte entries (+0..1 clue text, +2..3 Jake
- // spoken, +4..5 Jenny spoken, +6 score). Notebook always
- // uses +0 (partner-agnostic statement) per `FUN_15e0_01e8`.
- // CD notes are 4-byte: u16 textOff + u16 score.
+ // Floppy: 7-byte entry (+0 text, +2 Jake, +4 Jenny, +6 score).
+ // CD: 4-byte entry (u16 textOff + u16 score).
Common::String txt;
if (floppyMI) {
const uint16 textOff = READ_LE_UINT16(ni + clueId * 7);
@@ -3006,9 +2624,7 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
: "No clues yet for this suspect.",
rx, ry, MAX<int>(8, rw), 0x5C);
}
- // Header / footer text. The PDA's NEXT (case 5) and PREV
- // (case 6) icons are visible on the bottom bar â the footer
- // just covers dismissal here.
+ // Header / footer text.
if (_font.isLoaded()) {
_font.drawString(&ms,
isSpanish() ? "EXPEDIENTE" : "SUSPECT FILE",
@@ -3021,9 +2637,7 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
0, 0, 320, 200);
g_system->updateScreen();
- // Drain the queued LBUTTONDOWN that brought us into MoreInfo
- // so it doesn't get caught by the input loop below. Subsequent
- // pages don't need this.
+ // Drain the LBUTTONDOWN that opened MoreInfo (first page only).
if (isFirstShow) {
isFirstShow = false;
g_system->delayMillis(150);
@@ -3037,22 +2651,10 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
}
}
- // Wait for input. Each PDA button maps to a distinct action,
- // mirroring `_HandleMoreButton @ 158f:027d`:
- // case 0 NOTEBOOK -> NextScreen=4
- // case 1 HELP -> _InterfaceHelp(0)
- // case 2 GALLERY -> close MoreInfo
- // case 3 PARTNER head-> _KDHelp + redraw
- // case 4 ACCUSE -> NextScreen=7
- // case 5 PAGE NEXT -> next page
- // case 6 PAGE PREV -> prev page
- // case 7 MAP -> NextScreen=2
- // case 8 SITE -> no-op
- // case 10 HELP (alt) -> _InterfaceHelp(0)
- // Clicks outside any known button rect are no-ops, matching
- // the original (which short-circuits on `_FindButton == -1`).
- // ESC closes; PgDn / PgUp / Return / Backspace also paginate
- // as a modern accessibility convenience.
+ // `_HandleMoreButton @ 158f:027d`:
+ // [0] NOTEBOOKâ4, [1] HELPâ_InterfaceHelp, [2] GALLERY (close),
+ // [3] _KDHelp, [4] ACCUSEâ7, [5] PAGE_NEXT, [6] PAGE_PREV,
+ // [7] MAPâ2, [8] SITE noop, [10] HELP alt.
bool advance = false;
bool prev = false;
bool redraw = false;
@@ -3096,16 +2698,13 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
}
if (kPdaHelpRect.contains(mx, my) ||
kPdaHelp2Rect.contains(mx, my)) {
- // Help overlay; re-render the current page
- // after it dismisses.
setInteractiveMouseCursor(false);
doInterfaceHelp(0);
redraw = true;
break;
}
if (kPdaGalleryRect.contains(mx, my)) {
- // Case 2: close MoreInfo and return to the
- // portrait grid.
+ // Case 2: close MoreInfo.
back = true;
break;
}
@@ -3120,16 +2719,13 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
break;
}
if (kPdaPartnerHeadHintRect.contains(mx, my)) {
- // Case 3: ask KD for a hint then redraw the
- // current page over the clean BG.
+ // Case 3: _KDHelp.
setInteractiveMouseCursor(false);
doHelp();
redraw = true;
break;
}
- // Case 8 SITE and click outside any known button
- // = no-op. The original `_FindButton`
- // short-circuits when no rect matches.
+ // Case 8 SITE / non-button = no-op.
break;
}
if (e2.type == Common::EVENT_KEYDOWN && hasMore &&
@@ -3149,9 +2745,6 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
break;
}
}
- // Per-tick `updateScreen()` so the SDL cursor follows the
- // mouse â without it the cursor freezes on entry to the
- // MoreInfo screen.
g_system->updateScreen();
g_system->delayMillis(20);
}
@@ -3163,9 +2756,6 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
pageStart = pageStack.back();
pageStack.pop_back();
}
- // `redraw` falls through with the same `pageStart` and
- // re-iterates the outer while, repainting on top of whatever
- // the help overlay left behind.
}
return exitGallery;
@@ -3293,32 +2883,15 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
}
void EEMEngine::doBigMap() {
- // Two-stage flow that mirrors the original screen-1 wrapper at
- // 20fe:120b and `_DoBigMap @ 20fe:09e7`:
- //
- // STAGE 1 â Overview. PIC 0x42 + site icons drawn via the
- // `_DrawBigMapButtons` algorithm at BigMap coords MapData[+4/+6].
- // The original `_DoBigMap` returns sx/sy = (mouseX*2 - 0x74,
- // mouseY*2 - 0x55) when the player clicks inside `BigMapWindow`,
- // which is the scroll position into the SmallMap.
- //
- // STAGE 2 â Detail zoom. PIC 0x43 frame + a 0xe9 Ã 0xab viewport
- // into BIGMAP.PIC at (2, 2), drawn by `DrawMap @ 20fe:1058` with
- // the (sx, sy) returned from stage 1. Site icons are stamped at
- // SmallMap coords MapData[+8/+0xa] via `_StampButtons`. Click on
- // a site icon â travel.
- //
- // MapData entry layout (14 bytes), verified directly from the
- // disassembly of `_DrawBigMapButtons @ 20fe:0877` (`PUSH ES:[BX+4]`
- // for X, `PUSH ES:[BX+6]` for Y, `CMP ES:[BX+0xc], 0` for crime)
- // and `_StampButtons @ 20fe:0d2f` (`MOV AX, ES:[BX+8]`,
- // `MOV AX, ES:[BX+0xa]`):
- // +0..3 ??? (not yet decoded)
- // +4..5 BigMap X
- // +6..7 BigMap Y
- // +8..9 SmallMap X
- // +0xa..b SmallMap Y
- // +0xc..d crime-flag
+ // `_DoBigMap @ 20fe:09e7` two stage:
+ // Stage 1 (Overview): PIC 0x42 + site icons at MapData[+4/+6]
+ // (`_DrawBigMapButtons @ 20fe:0877`). Click in BigMapWindow
+ // returns scroll (mouseX*2 - 0x74, mouseY*2 - 0x55).
+ // Stage 2 (Detail): PIC 0x43 frame + 0xe9Ã0xab BIGMAP.PIC viewport
+ // at (2,2). Icons stamped at MapData[+8/+0xa] (`_StampButtons @
+ // 20fe:0d2f`). Click icon = travel.
+ // MapData entry (14 bytes): +0..3 ???, +4 BigMapX, +6 BigMapY,
+ // +8 SmallMapX, +0xa SmallMapY, +0xc crime-flag.
if (!_mystery.isLoaded())
return;
@@ -3327,35 +2900,20 @@ void EEMEngine::doBigMap() {
while (!shouldQuit()) {
setInteractiveMouseCursor(false);
- setSitePalette(0x24); // `_GetPalette(0x24)` per `_DoBigMap @ 20fe:09e7`.
+ setSitePalette(0x24); // `_GetPalette(0x24)` @ `_DoBigMap`.
- // ------------------------------------------------------------------
- // STAGE 1 â Overview: PIC 0x42 + clickable site icons.
- // ------------------------------------------------------------------
-
- // Anchor for the partner-sprite timeline. `_DoBigMap`'s
- // `_NewAnimation` call seeds the slot's frame index to 0xffff so
- // the first `_UpdateAnimations` tick starts at script[0]; we mirror
- // that by passing elapsed-since-open (zero on the first paint) into
- // `bigMapPartnerFrameAtTick`, which plays the unfold once and then
+ // Stage 1: Overview. mapStartTick anchors partner timeline;
+ // `_NewAnimation` seeds frame to 0xffff so unfold plays once then
// loops the wait sequence.
const uint32 mapStartTick = g_system->getMillis();
drawBigMapOverview(0);
uint32 mapLastTick = mapStartTick;
- // Static rectangles read directly from the binary at the labelled
- // addresses (CD `29be:0x1596` / floppy `2608:0x13fe..0x143e`).
- // Format is {x1, y1, x2, y2}. The floppy click table at
- // `2608:1436` (verified at `_BigMapInteractionLoop_Floppy @
- // 1fed:0a3a`) puts the setup button at (251, 3, 315, 42) â 1 px
- // up and 1 px left of the CD's (252, 4, 315, 42). The floppy
- // PIC 0x42 BG paints the visible button border at the same
- // pixels, so use the variant-specific rect to match the
- // hit-test region the original uses for that variant.
- const Common::Rect kBigMapWindow(0, 0, 247, 192); // 29be:1596
+ // Rects @ CD 29be:1596 / floppy 2608:13fe (1 px diff on Setup).
+ const Common::Rect kBigMapWindow(0, 0, 247, 192);
const Common::Rect kSetupBtnRect = isFloppy()
- ? Common::Rect(251, 3, 315, 42) // 2608:1436
- : Common::Rect(252, 4, 315, 42); // 29be:15ce
+ ? Common::Rect(251, 3, 315, 42)
+ : Common::Rect(252, 4, 315, 42);
bool wantZoom = false;
int zoomX = 0;
@@ -3372,18 +2930,12 @@ void EEMEngine::doBigMap() {
continue;
}
if (ev.type == Common::EVENT_LBUTTONDOWN) {
- // SetupButtonRect â `_NextScreen = 6` (the original's
- // settings screen, mirrors `_DoBigMap @ 20fe:0c33`
- // where it pushes `_PressButton` then writes
- // `_NextScreen = 6`). Now wired to the actual
- // `doSetup` handler instead of dropping the player
- // out to the launcher.
+ // Setup â NextScreen=6 (`_DoBigMap @ 20fe:0c33`).
if (kSetupBtnRect.contains(ev.mouse.x, ev.mouse.y)) {
_nextScreen = kScreenSetup;
return;
}
- // Click in the BigMapWindow â zoom. Original formula:
- // sx = mouseX*2 - 0x74; sy = mouseY*2 - 0x55
+ // BigMapWindow click â zoom (sx=x*2-0x74, sy=y*2-0x55).
if (kBigMapWindow.contains(ev.mouse.x, ev.mouse.y)) {
int sx = ev.mouse.x * 2;
int sy = ev.mouse.y * 2;
@@ -3399,8 +2951,7 @@ void EEMEngine::doBigMap() {
if (wantZoom)
break;
- // Cycle the partner-sprite frame every 100 ms (matching the
- // original's `_CheckFrameRate` cadence inside `_DoBigMap`).
+ // `_CheckFrameRate` cadence â 100 ms.
const uint32 now = g_system->getMillis();
if (now - mapLastTick >= 100) {
mapLastTick = now;
@@ -3415,11 +2966,7 @@ void EEMEngine::doBigMap() {
if (!wantZoom)
return;
- // ------------------------------------------------------------------
- // STAGE 2 â Detail zoom: PIC 0x43 frame + scrollable BIGMAP.PIC
- // viewport at (2, 2), 0xe9 Ã 0xab. Click on a stamped icon â travel.
- // ------------------------------------------------------------------
-
+ // Stage 2: Detail. PIC 0x43 + BIGMAP.PIC viewport.
Common::File f;
if (!f.open(Common::Path("BIGMAP.PIC"))) {
warning("doBigMap: BIGMAP.PIC missing for detail view");
@@ -3443,19 +2990,13 @@ void EEMEngine::doBigMap() {
int scrollX = MAX<int>(0, MIN<int>(mapW - kMapWinW, zoomX));
int scrollY = MAX<int>(0, MIN<int>(mapH - kMapWinH, zoomY));
- // Anchor the detail-screen partner timeline (mirrors `_DoMapScreen`'s
- // `_NewAnimation` seeding the slot's frame index to 0xffff). The
- // unfold (script 0x13) plays once, then `_SmallMapWaitSeq` loops.
+ // Detail partner timeline (script 0x13 unfold, then wait seq).
const uint32 detailStartTick = g_system->getMillis();
drawBigMapDetail(scrollX, scrollY, mapPixels, mapW, mapH, 0);
uint32 detailLastTick = detailStartTick;
bool returnToOverview = false;
- // `SmallMapButtons[4]` is the large right-side panel below setup:
- // CD/floppy both store `(252, 43, 320, 200)`. Its handler at
- // CD `20fe:156c` kills the zoom animation, sets `_NextScreen = 1`
- // and calls `_DoBigMap` again, so mouse players can return to the
- // overview without leaving the map screen.
+ // `SmallMapButtons[4]` @ 20fe:156c â return to overview.
const Common::Rect kBigMapReturnRect(252, 43, 320, 200);
const Common::Rect kArrowYUp(237, 2, 247, 11);
const Common::Rect kArrowYDown(237, 163, 247, 172);
@@ -3464,8 +3005,8 @@ void EEMEngine::doBigMap() {
const Common::Rect kXSlider(15, 175, 221, 185);
const Common::Rect kYSlider(237, 14, 247, 160);
const Common::Rect kDetailSetupBtn = isFloppy()
- ? Common::Rect(251, 3, 315, 42) // 2608:1436
- : Common::Rect(252, 4, 315, 42); // 29be:15ce
+ ? Common::Rect(251, 3, 315, 42)
+ : Common::Rect(252, 4, 315, 42);
const int kArrowStep = 16;
const int kSliderRange = mapW - kMapWinW;
const int kSliderRangeY = mapH - kMapWinH;
@@ -3516,17 +3057,7 @@ void EEMEngine::doBigMap() {
kBigMapReturnRect.contains(ev.mouse.x, ev.mouse.y) ||
kDetailSetupBtn.contains(ev.mouse.x, ev.mouse.y));
if (kDetailSetupBtn.contains(ev.mouse.x, ev.mouse.y)) {
- // `_DoMapScreen @ 20fe:1560` writes `_NextScreen
- // = 6` (= kScreenSetup) and `INC [BP-8]` to bail
- // out of the detail loop â verified via the byte
- // search for `c7 06 16 79 06 00`, which finds the
- // imm at exactly this site and `_DoBigMap @
- // 20fe:0c33`. Same `SetupButtonRect @ 29be:15ce`
- // rect used by both the overview and the detail
- // (no per-screen rect duplication in the binary).
- // The detail/zoom state is lost on return because
- // the screen driver re-enters BigMap at stage 1 â
- // this matches the original behaviour.
+ // `_DoMapScreen @ 20fe:1560` â NextScreen=6.
_nextScreen = kScreenSetup;
setInteractiveMouseCursor(false);
return;
@@ -3549,8 +3080,6 @@ void EEMEngine::doBigMap() {
scrollX + kArrowStep);
dirty = true;
} else if (kXSlider.contains(ev.mouse.x, ev.mouse.y)) {
- // Click on X slider track â jump scrollX so the
- // click position maps proportionally into the map.
if (kSliderRange > 0) {
const int t = ev.mouse.x - kXSlider.left;
const int tw = kXSlider.width();
@@ -3570,9 +3099,7 @@ void EEMEngine::doBigMap() {
ev.mouse.x < kMapWinX + kMapWinW &&
ev.mouse.y >= kMapWinY &&
ev.mouse.y < kMapWinY + kMapWinH) {
- // Hit-test the per-site button at its actual bbox
- // (`_StampButtons` records the rect at SmallMap +8/+0xa
- // with the button PIC's width/height).
+ // Per-site bbox from `_StampButtons` (SmallMap +8/+0xa).
const bool fmap = _mystery.isLoaded() && isFloppy();
for (uint i = 0; i < _mystery.numSites(); i++) {
if (!_mystery._onSites[i] &&
@@ -3585,10 +3112,8 @@ void EEMEngine::doBigMap() {
uint16 my;
uint16 buttonId;
if (fmap) {
- // Floppy detail view: click rect on
- // BIGMAP.PIC at (+0, +2), labelled BUTTON.DBD
- // entry ID at entry+4 (per
- // `FUN_1fed_0c3e @ 1fed:0c3e`).
+ // Floppy: rect +0/+2; BUTTON.DBD id at +4
+ // (`FUN_1fed_0c3e @ 1fed:0c3e`).
mx = READ_LE_UINT16(entry + 0x0);
my = READ_LE_UINT16(entry + 0x2);
buttonId = (uint16)entry[0x4];
@@ -3620,8 +3145,7 @@ void EEMEngine::doBigMap() {
if (returnToOverview)
break;
- // Cycle the partner sprite at 100 ms ticks (same cadence as
- // `_DoMapScreen`'s `_CheckFrameRate` + `_UpdateAnimations` loop).
+ // 100 ms partner cycle.
const uint32 now = g_system->getMillis();
if (now - detailLastTick >= 100) {
detailLastTick = now;
@@ -3639,20 +3163,9 @@ void EEMEngine::doBigMap() {
}
void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
- // Map-overview redraw â formerly the `drawOverview` lambda inside
- // `doBigMap`. PIC 0x42 frame + per-site marker (Done / Crime / Site
- // per `_DrawBigMapButtons @ 20fe:0877`) + the partner idle sprite.
- // `_DoBigMap @ 20fe:09e7` (`_NewAnimation` block at 20fe:0a44-0a99)
- // registers the partner: when `_LastScreen == 2` it plays an
- // entrance one-shot at (0x102, 0x50) and on END swaps to the idle
- // at (0xfd, 0x50). We don't track LastScreen finely enough so we
- // always render the IDLE pose at (0xfd, 0x50). Idle anim ID:
- // Jake = 0x14 (20), Jenny = 0x12 (18).
- //
- // `elapsedMs` is the time since `doBigMap` opened â the partner-sprite
- // timeline anchor. `bigMapPartnerFrameAtTick` uses it to play the
- // unfold script (0..8) once, then loop `_BigMapWaitSeq` (the open-map
- // hold). Without that anchor the unfold would loop indefinitely.
+ // PIC 0x42 + per-site Done/Crime/Site marker (`_DrawBigMapButtons @
+ // 20fe:0877`) + partner idle at (0xfd, 0x50). Idle ANI: Jake=0x14,
+ // Jenny=0x12. elapsedMs anchors unfoldâwait timeline.
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
@@ -3661,14 +3174,9 @@ void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
if (_picsArchive.getPicture(0x42, frame))
scratch.simpleBlitFrom(frame.surface);
- // Marker PICs from `_main @ 1a35:0f59`. Three globals are filled
- // once at boot via `_GetPicture` (1-based IDs):
- // _DoneMarker = PIC 0x20d (already-searched site)
- // _SiteMarker = PIC 0xc5 (default available site)
- // _CrimeMarker = PIC 0xc6 (crime-scene flag set)
- // Floppy's `PICS.DBX` has 524 entries, so PIC 0x20d is CD-only.
- // Its pixels are the same 7x8 marker as PIC 0xc5, with the animated
- // 0xfb/0xfc interior remapped to static 0x1b/0x19 blue.
+ // Marker PICs from `_main @ 1a35:0f59`:
+ // _DoneMarker = 0x20d, _SiteMarker = 0xc5, _CrimeMarker = 0xc6.
+ // 0x20d is CD-only (floppy PICS.DBX has 524 entries).
Picture done;
Picture normal;
Picture crimeM;
@@ -3715,27 +3223,19 @@ void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
blitBigMapMarker(scratch, *m, (int)mx, (int)my,
useVisitedColors);
} else {
- // Fallback if the markers couldn't be loaded.
const Common::Rect mark(mx - 3, my - 3, mx + 4, my + 4);
scratch.fillRect(mark, 0x0F);
}
}
- // Partner idle sprite at (0xfd, 0x50). Jake = anim 0x14, Jenny = 0x12
- // for the loaded CELLS, but the original `_DoBigMap @ 20fe:0a47`
- // always passes `CONCAT22(0x14, ...)` to `_NewAnimation` so the
- // SCRIPT key is 0x14 (`[0..8]` count-up) regardless of partner.
- // Without this, Jenny was running 0x12's count-DOWN script
- // `[8..0]` over her cells â visually backwards from the original.
+ // Partner idle at (0xfd, 0x50). `_DoBigMap @ 20fe:0a47` always passes
+ // script 0x14 (count-up) to `_NewAnimation` regardless of partner.
const uint kMapAniId = (_partner == 0) ? 0x14 : 0x12;
Animation mapAnim;
if (_aniArchive.loadAnimation(kMapAniId, mapAnim) && !mapAnim.empty()) {
const uint frameIdx = bigMapPartnerFrameAtTick((uint)mapAnim.size(),
elapsedMs);
- // Anchor-aware: the BigMap walk-cycle has miscflags = -2 per
- // cell, so the partner shifts left as it cycles â without the
- // anchor adjustment the sprite "shakes in place" instead of
- // walking forward.
+ // BigMap walk-cycle miscflags = -2 (anchor shift left).
blitAnimFrameAnchored(scratch.surfacePtr(), mapAnim[frameIdx],
0xfd, 0x50);
}
@@ -3749,15 +3249,9 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
const Common::Array<byte> &mapPixels,
uint16 mapW, uint16 mapH,
uint32 elapsedMs) {
- // Map-detail redraw â formerly the `drawDetail` lambda inside
- // `doBigMap`. PIC 0x43 frame + a 0xe9 Ã 0xab BIGMAP.PIC viewport at
- // (2, 2), stamped site buttons, and the partner sprite at (0x101,
- // 0x50) â `_DoMapScreen @ 20fe:120b` (`_NewAnimation` at
- // 20fe:12cd-12f0, anim 0x13 Jake / 0x11 Jenny, seqnum 0x13).
- //
- // `elapsedMs` is the time since the detail screen was opened â
- // `bigMapDetailPartnerFrameAtTick` uses it to play the unfold once
- // and then loop `_SmallMapWaitSeq`.
+ // PIC 0x43 + 0xe9Ã0xab BIGMAP.PIC viewport at (2,2), stamped buttons,
+ // partner at (0x101, 0x50). `_DoMapScreen @ 20fe:120b` (ANI 0x13 Jake
+ // / 0x11 Jenny, seq 0x13). elapsedMs anchors unfoldâwait.
const int kMapWinW = 0xe9;
const int kMapWinH = 0xab;
const int kMapWinX = 2;
@@ -3776,17 +3270,8 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
scratch.copyRectToSurface(mapPixels.data() + scrollY * mapW + scrollX,
mapW, kMapWinX, kMapWinY, copyW, copyH);
- // Stamped site buttons. `_StampButtons @ 20fe:0d2f` (CD):
- // button = _GetButton(MapData[+0])
- // destX = MapData[+8]
- // destY = MapData[+0xa]
- // Floppy uses `FUN_1fed_0c3e @ 1fed:0c3e`: for each SITES row, the
- // byte at entry+4 is a BUTTON.DBD entry ID (loaded via
- // `FUN_16e2_1838 @ 16e2:1838`, which opens `button.dbd` â string
- // at `2608:0558`). The labelled button is stamped at
- // `(entry+0..1, entry+2..3)` on BIGMAP.PIC. These are the same
- // per-site labelled buttons the CD uses, just keyed off a
- // different field offset.
+ // `_StampButtons @ 20fe:0d2f` (CD): button=MapData[+0], dst=+8/+0xa.
+ // Floppy `FUN_1fed_0c3e @ 1fed:0c3e`: button.dbd id at +4, rect +0/+2.
const bool floppyMap = _mystery.isLoaded() && isFloppy();
for (uint i = 0; i < _mystery.numSites(); i++) {
if (!_mystery._onSites[i] && i != _mystery._siteNumber)
@@ -3830,12 +3315,7 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
}
}
- // Partner sprite on the detail map (drawn last to sit over the
- // frame and the BIGMAP.PIC viewport). The original always passes
- // `CONCAT22(0x13, ...)` to `_NewAnimation` (i.e. script ID 0x13)
- // regardless of partner â verified at `_DoBigMap @ 20fe:0a47`.
- // So we look up script 0x13 for both partners while still
- // loading the partner-specific CELLS via `kDetailAniId`.
+ // Always script 0x13 (`_NewAnimation @ _DoBigMap 20fe:0a47`).
const uint kDetailAniId = (_partner == 0) ? 0x13 : 0x11;
Animation detailAnim;
if (_aniArchive.loadAnimation(kDetailAniId, detailAnim) &&
@@ -3852,46 +3332,24 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
}
uint16 EEMEngine::getKDTextBalloon(byte firstChar) const {
- // Mirrors `_GetKDTextBalloon @ 1df2:0105`:
+ // `_GetKDTextBalloon @ 1df2:0105`:
// if ((ctype[firstChar] & 2) == 0) bub = *(u16*)29be:1068 = 0x17
// else bub = *(u16*)(29be:0fe6+0x1e+c*2)
- // `ctype` is Borland's `_ctype_` array at `29be:2be1`. Bit 1 (0x02) is
- // set only for digits '0'..'9' (verified by reading the table â '0'..'9'
- // each map to byte 0x02; everything else has bit 1 clear).
- // Lookup table at 29be:1064 (= 29be:0fe6 + 0x1e + '0'*2):
- // '0'â0x15 '1'â0x16 '2'â0x17 '3'â0x18 '4'â0x19
- // '5'â0x1a '6'â0x20 '7'â0x21 '8'â0x22 '9'â0x1e
- // Note `*(u16*)29be:1068` (= entry for '2') is the same byte the
- // non-digit fallback returns â the original encodes the constant by
- // reusing the digit-2 slot.
+ // `ctype` is Borland's `_ctype_` at 29be:2be1; bit 1 (0x02) is set
+ // only for '0'..'9'. Digit table @ 29be:1064 (see kDigitBalloons).
+ // `*(u16*)29be:1068` = entry for '2' = 0x17 â original reuses the
+ // digit-2 slot as the non-digit fallback constant.
if (firstChar < '0' || firstChar > '9')
return 0x17;
- // `kDigitBalloons` lives at file scope above.
return kDigitBalloons[firstChar - '0'];
}
bool EEMEngine::doAccuseNotes() {
- // Mirrors the accuse-notes screen at the head of `_DoAccuse @
- // 1df2:0bdd`:
- // * BG: PIC 0x1A7 (the red "accuse-mode" backdrop).
- // * `_AccuseNoteRect @ 29be:1048` = (79, 27, 304, 159) holds
- // the rendered clue list.
- // * Counter at `(0xd1, 0xb)` = `(209, 11)` shows "N clue(s)"
- // remaining (`_UpdateSelectionCount @ 1df2:08dd`,
- // `_Show_String(0xb, 0xd1, ...)`).
- // * Expected count = `6 - DAT_2d5d_3f99`:
- // chainStage 1 â 5 clues, 2 â 4 clues, 3 â 3 clues.
- // * `_NoteUnselectedColor = 1` (red) for unselected, `0x3c`
- // for selected (1df2:0c2c sets it on entry).
- // * Click on a clue toggles its selection
- // (`_SearchNoteAreas` + `_SwapColors`).
- // * Click `_NoteButtons[4]` (rect at `(180, 174, 201, 190)`,
- // the original's solve button) jumps to the evidence check;
- // `_HandleAccuseNoteButton(4)` returns 2 and the outer loop
- // forces `uStack_8 = uStack_a` to trigger `_SolvedCheck`.
- // * CD and floppy originals only cancel this screen through ESC.
- // ScummVM also wires the visible PDA navigation buttons so the
- // accusation can be abandoned without a keyboard.
+ // `_DoAccuse @ 1df2:0bdd` head. BG PIC 0x1A7. `_AccuseNoteRect @
+ // 29be:1048` = (79, 27, 304, 159). Counter @ (209, 11) shows
+ // `6 - chainStage` (stage 1=5, 2=4, 3=3 clues). Unselected color 1
+ // (red), selected 0x3c. Click toggles selection; `_NoteButtons[4]`
+ // (180,174,201,190) SOLVE â `_HandleAccuseNoteButton` returns 2.
if (!_mystery.isLoaded() || !_font.isLoaded())
return false;
const byte *ni = _mystery.noteIndex();
@@ -3902,49 +3360,31 @@ bool EEMEngine::doAccuseNotes() {
Picture accuseBg;
const bool haveBg = _picsArchive.getPicture(0x1a7, accuseBg);
- // Reset selection on entry. `FUN_1d40_0e07 @ 1d40:0e34` (floppy)
- // explicitly zeroes the 0x7f-byte `_NoteSelected_Floppy` array
- // before drawing, so the player always starts with a clean board.
- // Without this, leftover selections from a failed attempt persist
- // and the post-selection 100-point gate gets the wrong sum.
+ // `FUN_1d40_0e07 @ 1d40:0e34` zeroes _NoteSelected_Floppy on entry.
memset(_mystery._noteSelected, 0, sizeof(_mystery._noteSelected));
- // Required count for solving â `6 - chainStage`.
const uint expected = (_chainStage >= 1 && _chainStage <= 3)
? (uint)(6 - _chainStage)
: 5;
- // Build the list of FOUND clue IDs (in clue-ID order; the
- // original's `_DrawNotes(NULL, 100, ...)` walks `_CluesFound[]`
- // the same way).
+ // `_DrawNotes(NULL, 100, ...)` walks `_CluesFound[]`.
Common::Array<uint> found;
for (uint i = 0; i < niCount && i < Mystery::kCluesFoundCap; i++) {
if (_mystery._cluesFound[i] && _mystery.noteHasNotebookText(i))
found.push_back(i);
}
- // `_AccuseNoteRect` (79, 27, 304, 159) â text wrap area.
const int rectX = 79;
const int rectY = 27;
const int rectW = 304 - 79;
const int rectH = 159 - 27;
- // `_NoteButtons` rects (verified at `29be:0147`). `_DoAccuse`
- // re-uses the same table as `_DoNotebook`, but the original
- // handler only routes SOLVE / PAGE NEXT / PAGE PREV; ScummVM
- // additionally routes the visible site/map/notebook/gallery
- // buttons below for pointer-only cancellation.
- // `_HandleAccuseNoteButton @ 1df2:0990` returns `DI` (initialised
- // to 0) and only sets `DI = 2` in the `i == 4` branch (asm:
- // `1df2:09b2: MOV DI, 0x2`). The outer loop's `iVar6 == 2` test
- // at `1df2:0db2` is checking the HANDLER'S RETURN VALUE, not the
- // button INDEX â earlier comment had this backwards. So the
- // SOLVE rect is `[4]` (180, 174, 201, 190), the same icon the
- // PDA uses to trigger the accuse flow in the first place.
- const Common::Rect kBtnSolve (180, 174, 201, 190); // [4] SOLVE
- const Common::Rect kBtnPageNext(204, 174, 224, 190); // [5] PAGE NEXT
- const Common::Rect kBtnPagePrev(226, 174, 247, 190); // [6] PAGE PREV
- const Common::Rect kBtnPartner ( 5, 80, 44, 110); // [3] KD HELP
+ // `_NoteButtons` @ 29be:0147. `_HandleAccuseNoteButton @ 1df2:0990`
+ // returns DI=2 for i==4 (SOLVE).
+ const Common::Rect kBtnSolve (180, 174, 201, 190); // [4]
+ const Common::Rect kBtnPageNext(204, 174, 224, 190); // [5]
+ const Common::Rect kBtnPagePrev(226, 174, 247, 190); // [6]
+ const Common::Rect kBtnPartner ( 5, 80, 44, 110); // [3]
// Per-page slot rects + their clue IDs (for click hit-testing).
Common::Array<Common::Rect> slotRects;
@@ -3955,20 +3395,9 @@ bool EEMEngine::doAccuseNotes() {
int numPages = 1;
pageBreaks[0] = 0;
- // Variant-aware text resolver. CD note entries are 4 bytes
- // (u16 textOff RELATIVE to TextBlock, u16 score), so the offset
- // is added to `_textOffset` via `Mystery::textAt`. Floppy
- // entries are 7 bytes:
- // +0..1 clue text offset (ABSOLUTE byte offset into the
- // mystery blob â partner-agnostic clue statement;
- // this is what the notebook displays).
- // +2..3 Jake spoken-line offset (`FUN_22dc_05c8 @ 22dc:0843`).
- // +4..5 Jenny spoken-line offset.
- // +6 score (`FUN_1d40_0c48`).
- // `_DrawNotes_Floppy / FUN_15e0_01e8` reads `*(int *)(notes +
- // idx * 7)` â always byte +0 â for the notebook text.
- // Reading +2/+4 here showed the partner's "Hi, Jake! Hi,
- // Jenny!" intro instead of the actual clue statement.
+ // CD note: 4 bytes (u16 textOff rel TextBlock, u16 score).
+ // Floppy: 7 bytes (+0 abs textOff, +2 Jake, +4 Jenny, +6 score).
+ // Notebook always uses +0 (`FUN_15e0_01e8`).
const bool floppyNote = isFloppy();
const byte *bufBaseNotes = _mystery.blobAt(0);
auto noteText = [&](uint clueId) -> Common::String {
@@ -4020,14 +3449,8 @@ bool EEMEngine::doAccuseNotes() {
if (haveBg)
scratch.simpleBlitFrom(accuseBg.surface);
- // Partner sprite at (5, 0x50). The original `_DoAccuse @
- // 1df2:0c2c` does `_NewAnimation(5, 0x50, partnerCells,
- // script=2, prior=1)` â same anim cells as the gallery
- // (anim 2 for Jake / 0x10 for Jenny) with script 0x02. The
- // `_UpdateAnimations` loop in `_DoAccuse @ 1df2:0bfa` keeps
- // the slot painting through the entire selection screen;
- // without an explicit blit here the player sees a partner-
- // less accuse-mode screen.
+ // Partner at (5, 0x50). `_DoAccuse @ 1df2:0c2c`: ANI 2/0x10,
+ // script 2, prior 1.
const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
Animation partnerAni;
if (_aniArchive.loadAnimation(partnerAnim, partnerAni) &&
@@ -4039,10 +3462,7 @@ bool EEMEngine::doAccuseNotes() {
partnerAni[frameIdx], 5, 0x50);
}
- // Clue list inside `_AccuseNoteRect`. Selected = 0x3c (yellow),
- // unselected = 1 (red), per `_NoteUnselectedColor = 1` set at
- // 1df2:0c25 â the red colour is what gives the screen its
- // "accuse-mode" look together with PIC 0x1A7.
+ // Selected=0x3c, unselected=1 (`_NoteUnselectedColor` @ 1df2:0c25).
slotRects.clear();
slotClues.clear();
const int lineH = _font.getFontHeight();
@@ -4078,11 +3498,11 @@ bool EEMEngine::doAccuseNotes() {
y += h + 7;
}
- // Counter â `_UpdateSelectionCount(remaining)` at (0xd1, 0xb).
+ // `_UpdateSelectionCount(remaining)` @ (0xd1, 0xb).
const uint remaining = (selectedCount < expected)
? expected - selectedCount
: 0;
- // Spanish floppy uses "nota/notas" (verified in Spanish EEM.EXE).
+ // Spanish floppy uses "nota/notas".
const char *clueWord = isSpanish()
? (remaining == 1 ? "nota" : "notas")
: (remaining == 1 ? "clue" : "clues");
@@ -4165,12 +3585,7 @@ bool EEMEngine::doAccuseNotes() {
dirty = true;
continue;
}
- // Page navigation â `_NoteButtons[5]` / `[6]`,
- // dispatched in `_HandleAccuseNoteButton @
- // 1df2:0990`. Only effective if there's another
- // page in that direction; mirrors the
- // `1 < _CurrentPage` guard at 1df2:09a8 and the
- // `_NextClue != -1` guard at 1df2:099e.
+ // `_NoteButtons[5]/[6]` â page nav guards @ 1df2:09a8/099e.
if (kBtnPageNext.contains(mx, my)) {
if (page + 1 < numPages) {
page++;
@@ -4185,33 +3600,22 @@ bool EEMEngine::doAccuseNotes() {
}
continue;
}
- // Partner click â `_NoteButtons[3]`. Original
- // `_HandleAccuseNoteButton` doesn't dispatch this
- // (no case for i == 3), but the rect is still in
- // the table; we wire it to the puzzle hint so the
- // player can ask the partner what to look for
- // without leaving accuse mode.
+ // `_NoteButtons[3]` â ScummVM hint, original ignores.
if (kBtnPartner.contains(mx, my)) {
doHelp();
dirty = true;
continue;
}
if (kBtnSolve.contains(mx, my)) {
- // Count selected.
uint selected = 0;
for (uint i = 0; i < found.size(); i++) {
if (_mystery._noteSelected[found[i]])
selected++;
}
if (selected == expected) {
- // Commit â let the caller do the
- // `_SolvedCheck` + suspect picker dance.
+ // `_DoAccuse` gate `uStack_8 == uStack_a`.
return true;
}
- // Wrong count â `_DoAccuse` only triggers the
- // check when `uStack_8 == uStack_a`; we just
- // stay in the loop so the player can keep
- // adjusting.
continue;
}
// Toggle clue under cursor.
@@ -4237,9 +3641,7 @@ bool EEMEngine::doAccuseNotes() {
}
if (dirty)
draw();
- // Per-tick redraw so the partner sprite cycles. Same
- // 100 ms cadence as `_CheckFrameRate` + `_UpdateAnimations`
- // in the original (1df2:0bfa).
+ // 100 ms `_CheckFrameRate` cadence @ 1df2:0bfa.
static uint32 sLastTick = 0;
const uint32 now = g_system->getMillis();
if (now - sLastTick >= 100) {
@@ -4256,25 +3658,17 @@ void EEMEngine::doAccuse() {
if (!_mystery.isLoaded() || !_font.isLoaded())
return;
- // Floppy accusation flow is structurally different from the CD's:
- // the suspect gallery section uses variable-stride entries (5 +
- // nameLen bytes, NOT 0x46), the score is computed from the top-5
- // note scores in `_TextSeen` (not `_NoteSelected`), and the win
- // path renders the solved-chain dialog records at header[+0x12]
- // rather than a single ClueBlock. Dispatch to a dedicated handler
- // that mirrors `_KDHelp_Floppy` + `_HandleNoteButton_Floppy` (button
- // 4 â `FUN_1d40_11fd` "ready to solve" gate) +
- // `FUN_1d40_0c79` (gallery picker) + `_DisplayCorrect_Floppy` /
- // `_DisplayAlibi_Floppy`.
+ // Floppy: variable-stride entries (5 + nameLen, not 0x46), top-5
+ // `_TextSeen` scoring (not `_NoteSelected`), header[+0x12] win-dialog.
+ // Handled by doAccuseFloppy (`FUN_1d40_11fd`, `FUN_1d40_0c79`,
+ // `_DisplayCorrect_Floppy`, `_DisplayAlibi_Floppy`).
if (isFloppy()) {
doAccuseFloppy();
return;
}
- // `_AccuseEntry @ 1df2:0ff8` runs before the red accuse-notes
- // picker. It scores the top five FOUND clues, says how close the
- // player is, and only lets `_DoAccuse` continue when that score is
- // at least 0x65.
+ // `_AccuseEntry @ 1df2:0ff8` â gates picker on top-5 found clue
+ // score (â¥0x65 required).
const byte *entryKdIdx = _mystery.kdTextIndex();
if (!entryKdIdx)
return;
@@ -4350,36 +3744,19 @@ void EEMEngine::doAccuse() {
return;
}
- // Mirrors `_DoAccuse @ 1df2:0bdd` + `_DoAccuseGallery @ 1df2:0a31`:
- // 1. ACCUSE-NOTES SCREEN (PIC 0x1A7, the red "accuse-mode" BG):
- // `_DrawNotes(_AccuseNoteRect, NULL, 100, _NoteSelected)`
- // lists every found clue inside the rect at `29be:1048` =
- // `(79, 27, 304, 159)`. `_UpdateSelectionCount(remaining)`
- // shows "N clue(s)" at `(209, 11)` (ASM: `_Show_String(0xb,
- // 0xd1, ...)` at 1df2:0907). `_NoteUnselectedColor = 1` is
- // the dim red used for unselected entries; selected ones
- // get `0x3c`. Click toggles via `_SearchNoteAreas` +
- // `_SwapColors`. Expected count = `6 - DAT_2d5d_3f99`
- // (= 6 - chainStage):
- // stage 1 â 5 clues, stage 2 â 4, stage 3 â 3.
- // When the count matches, `_SolvedCheck` decides:
- // fail â KD "not enough evidence" balloon â return.
- // pass â `_DoAccuseGallery()` (suspect picker).
+ // `_DoAccuse @ 1df2:0bdd` + `_DoAccuseGallery @ 1df2:0a31`:
+ // 1. Accuse-notes (PIC 0x1A7) â pick `6 - chainStage` clues.
+ // Pass `_SolvedCheck` â gallery; fail â hint + return.
// 2. KD intro balloon (`KDTextIndex[+8]` + `_SayKDDigital(4)`).
- // 3. `_GetBackground(0x3f)` + `_DrawGallery()` â portraits at
- // the 5 fixed slots (`29be:0x116`).
- // 4. Click loop on portraits â `_WITCH(picked)` â guilty/alibi.
+ // 3. PIC 0x3f + `_DrawGallery` portraits at 5 slots (29be:0x116).
+ // 4. Click portrait â `_WITCH(picked)` â guilty/alibi.
const uint8 num = _mystery.numSuspects();
if (num == 0)
return;
const byte *gd = _mystery.galleryData();
- // ACCUSE-NOTES SCREEN â let the player commit which N clues they
- // believe solve the case. Mirrors the click-driven selection of
- // `_DoAccuse @ 1df2:0bdd`'s outer loop. ESC returns to the site
- // (matches `_DoAccuse @ 1df2:0c11` writing `_NextScreen = 3`);
- // pointer navigation can also leave for the PDA, gallery, or map.
+ // `_DoAccuse @ 1df2:0c11` outer loop; ESC â NextScreen=3.
if (!doAccuseNotes()) {
if (_nextScreen == kScreenAccuse) {
_nextScreen = _lastScreen != kScreenInvalid
@@ -4388,12 +3765,7 @@ void EEMEngine::doAccuse() {
return;
}
- // Evidence gate. `_DoAccuse @ 1df2:0c75` runs `_SolvedCheck`
- // before opening the suspect picker; on failure it renders a
- // partner balloon over the CURRENT screen (the PDA in the
- // original) and returns. We render the same hint over the
- // caller's screen and bail back to `_lastScreen` without ever
- // touching the gallery BG.
+ // `_DoAccuse @ 1df2:0c75` â `_SolvedCheck` gate.
if (!_mystery.solvedCheck()) {
const byte *kdIdx = _mystery.kdTextIndex();
const int16 hintOff = kdIdx
@@ -4404,11 +3776,8 @@ void EEMEngine::doAccuse() {
hint = parseString(_mystery.textAt((uint16)hintOff),
_playerName, _partner);
if (hint.empty()) {
- // Fallback if `KDTextIndex[+6]` isn't set in this mystery.
- // Spanish text from the Spanish floppy EEM.EXE â there's
- // only the single "1Necesitamos buscar pistas..." string
- // in the binary, so use it for both the zero-points and
- // some-points cases.
+ // Fallback if `KDTextIndex[+6]` missing. Spanish floppy
+ // has only one "Necesitamos buscar pistas" string.
if (isSpanish()) {
hint = "Necesitamos buscar pistas antes de resolver "
"el misterio. Investiguemos un poco mas!";
@@ -4423,9 +3792,8 @@ void EEMEngine::doAccuse() {
}
}
- // Compose balloon overlay on the current screen. Mirrors the
- // `_GetKDTextBalloon` + `_GetBalloon` + `_AddPicBackground`
- // + `_WordWrap` sequence at 1df2:0c8d-0cd1.
+ // Balloon overlay (`_GetKDTextBalloon` + `_GetBalloon` +
+ // `_AddPicBackground` + `_WordWrap` @ 1df2:0c8d-0cd1).
Graphics::ManagedSurface ms(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
ms.clear();
@@ -4437,10 +3805,7 @@ void EEMEngine::doAccuse() {
const byte firstChar =
hint.empty() ? (byte)0 : (byte)hint[0];
uint16 bubNum = getKDTextBalloon(firstChar);
- // Strip the digit prefix used for balloon dispatch â it's
- // consumed by the original at `_DisplayAlibi @ 1df2:0163`
- // (`str = pbVar7 + 1`) and shouldn't appear in the rendered
- // text. `_GetKDTextBalloon` itself doesn't advance past it.
+ // Strip digit prefix (`_DisplayAlibi @ 1df2:0163` `str=pbVar7+1`).
if (firstChar >= '0' && firstChar <= '9')
hint.deleteChar(0);
bubNum = fitBalloonToText(bubNum, hint);
@@ -4469,21 +3834,17 @@ void EEMEngine::doAccuse() {
0, 0, 320, 200);
g_system->updateScreen();
- // `_DoAccuse @ 1df2:0cd9` plays `_SayKDDigital(3)` â
- // partner-specific "not enough evidence" voice line.
+ // `_SayKDDigital(3)` @ 1df2:0cd9.
if (_audio && kdIdx)
_audio->sayKDDigital(kdIdx, 3, _partner);
waitForInput(20000);
- // `_DoAccuse @ 1df2:0ce5` writes `_NextScreen = _LastScreen`
- // so the player drops back where they came from.
+ // `_DoAccuse @ 1df2:0ce5` â NextScreen = LastScreen.
_nextScreen = _lastScreen != kScreenInvalid
? (ScreenId)_lastScreen : kScreenSite;
return;
}
- // Verbatim from 29be:0x116 â same five suspect slot positions as
- // `_DrawGallery @ 158f:0046`.
Picture accuseBg;
const bool haveAccuseBg = _picsArchive.getPicture(0x3f, accuseBg);
@@ -4496,16 +3857,9 @@ void EEMEngine::doAccuse() {
int highlighted = 0;
- // Step 1 â KD hint balloon. Mirrors `_DoAccuseGallery @ 1df2:0a31`
- // (1df2:0a4c-1df2:0afe):
- // text = TextBlock + KDTextIndex[+8] (1df2:0a4c-0a57)
- // bub = _GetKDTextBalloon(text[0]) (1df2:0a6d)
- // GetBalloon(bub) (1df2:0a7c)
- // y = (h < 0x4e) ? (0x50 - h) >> 1 : 1 (1df2:0a8b-0aa5)
- // AddPicBackground(pic, 0x21, y) (1df2:0aab)
- // WordWrap(0x21+tbl[bub].x, y+tbl[bub].y, tbl[bub].w, text, color=0)
- // tbl @ 29be:0875, 10-byte entries (1df2:0ad6-0af1)
- // _SayKDDigital(4); _Wait(); (1df2:0b09-0b11)
+ // KD hint balloon @ `_DoAccuseGallery @ 1df2:0a31` (0a4c-0afe).
+ // y = (h < 0x4e) ? (0x50-h)>>1 : 1. Inset table @ 29be:0875.
+ // `_SayKDDigital(4)`.
const byte *kdIdx = _mystery.kdTextIndex();
if (kdIdx) {
const int16 textOff = (int16)READ_LE_UINT16(kdIdx + 8);
@@ -4514,20 +3868,10 @@ void EEMEngine::doAccuse() {
Common::String hint =
parseString(raw ? raw : "", _playerName, _partner);
if (!hint.empty()) {
- // First-char dispatch via getKDTextBalloon (1df2:0105).
- // Note: we pass the *parsed* first char; the original
- // reads it BEFORE `_ParseString`, but the player-name /
- // partner-name substitutions never start with digits, so
- // the dispatch result is the same either way.
const byte firstChar =
hint.empty() ? (byte)0 : (byte)hint[0];
uint16 bubNum = getKDTextBalloon(firstChar);
- // Strip the digit prefix used for balloon dispatch.
- // `_DisplayAlibi @ 1df2:0163` does `str = pbVar7 + 1`
- // after using `*str` for `bindx`. Same pattern used by
- // `_DisplayHint`: digit picks the bubble shape AND is
- // then consumed from the rendered text. Without this
- // the intro balloon shows e.g. "1Ready to solve?".
+ // Strip digit prefix (`_DisplayAlibi @ 1df2:0163`).
if (firstChar >= '0' && firstChar <= '9')
hint.deleteChar(0);
bubNum = fitBalloonToText(bubNum, hint);
@@ -4536,22 +3880,12 @@ void EEMEngine::doAccuse() {
_balloonArchive.size() > (bubNum & 0x7F) &&
_balloonArchive.loadEntry(bubNum & 0x7F, balloon);
- // 1df2:0a8b-1df2:0aa5: y = (h < 0x4e) ? (0x50-h)>>1 : 1
const int balloonX = 0x21;
int balloonY = 1;
if (haveBalloon && balloon.surface.h < 0x4e)
balloonY = (0x50 - balloon.surface.h) / 2;
- // Render the gallery FIRST so the balloon snapshot
- // includes the partner sprite. The original
- // `_DoAccuseGallery @ 1df2:0a31` does this implicitly:
- // `_NewAnimation` registered the partner slot at
- // (5, 0x50) before reaching this point, then
- // `_GetBackground(0x3f)` + `_DrawGallery` paint the
- // portraits, and `_UpdateAnimations` keeps the partner
- // visible underneath the balloon overlay. Without this,
- // the player sees an 8-second partner-less screen
- // while reading the hint.
+ // Render gallery first so the snapshot includes partner.
drawAccuseGallery(num, gd, /*highlighted=*/-1,
slotRects, slotSuspect);
@@ -4564,22 +3898,18 @@ void EEMEngine::doAccuse() {
ms.simpleBlitFrom(*cur);
g_system->unlockScreen();
} else if (haveAccuseBg) {
- // Fallback: lockScreen failed somehow; at least
- // fill from PIC 0x3f so we don't render against
- // stale memory. simpleBlitFrom auto-clips, so
- // no MIN(w, 320)/MIN(h, 200) needed.
+ // Fallback if lockScreen failed.
ms.simpleBlitFrom(accuseBg.surface);
}
}
- // Masked balloon blit â `_Rect_Move_Mask` (1000:03fc)
- // skips pixels equal to `pic[0] >> 8`.
+ // `_Rect_Move_Mask` (1000:03fc) â transp = pic[0]>>8.
if (haveBalloon) {
const byte transp = (byte)(balloon.flags >> 8);
ms.transBlitFrom(balloon.surface,
Common::Point(balloonX, balloonY),
transp);
}
- // Inset table @ 29be:0875 â 1df2:0acb pushes color=0.
+ // Inset table @ 29be:0875.
uint16 tx = 5;
uint16 ty = 4;
uint16 tw = 155;
@@ -4599,9 +3929,7 @@ void EEMEngine::doAccuse() {
}
}
- // Helper to find the next "alive" slot (one whose `_inGallery[phys]`
- // flag is still set so a portrait was actually drawn). Mirrors the
- // way the original wraps DI past empty slots.
+ // Wrap past empty slots (matches original DI advance).
if (slotRects[highlighted].isEmpty())
highlighted = nextLiveSlot(slotRects, highlighted, +1);
@@ -4679,10 +4007,7 @@ void EEMEngine::doAccuse() {
}
}
}
- // 100 ms tick â the original calls `_UpdateAnimations` per
- // `_CheckFrameRate` (1df2:0b33). The accuse screen has no
- // animations registered, so the tick is just a redraw cadence.
- // We still re-render whenever the highlight moves (`dirty`).
+ // 100 ms `_CheckFrameRate` @ 1df2:0b33.
const uint32 now = g_system->getMillis();
if (dirty || now - lastTick >= 100) {
drawAccuseGallery(num, gd, highlighted, slotRects, slotSuspect);
@@ -4695,16 +4020,8 @@ void EEMEngine::doAccuse() {
if (picked < 0)
return;
- // Real chain evaluation. Mirrors the original two-gate accusation:
- // 1. `_AccuseEntry @ 1df2:0ff8` checked top-five found points
- // before the note picker, so by this point the player has
- // enough evidence to attempt an accusation.
- // 2. `_SolvedCheck @ 1df2:00ec` checked selectedPoints > 99
- // before opening this suspect picker, so the clue set is
- // valid if we got here.
- // 3. `_WITCH @ 1df2:089f` checks `GalleryData[picked*0x46+0x02] ==
- // 0xFFFF`. Innocent suspects store an alibi-text TextBlock
- // offset there; the guilty one uses the sentinel.
+ // `_WITCH @ 1df2:089f` â guilty when gd[picked*0x46+0x02] == 0xFFFF;
+ // innocents store an alibi TextBlock offset there.
const int points = _mystery.selectedPoints();
const bool pickedGuilty = _mystery.isGuilty((uint)picked);
const bool guessedRight = pickedGuilty;
@@ -4714,24 +4031,16 @@ void EEMEngine::doAccuse() {
pickedGuilty ? "yes" : "no",
guessedRight ? "correct" : "wrong");
- // Wrong suspect: full alibi flow. Mirrors `_DisplayAlibi @
- // 1df2:0145`:
- // 1. Plays MIDI 6 (loser sting) and waits for it to finish while
- // the gallery is still on screen (1df2:0184-1df2:0192).
- // 2. Draws PIC 0x3e + the suspect's speech balloon + their
- // portrait at (0x82, py), where the balloon shape comes from
- // `AlibiBubbles[bindx]` (table @ 29be:1050) and bindx is the
- // digit-prefix on the alibi text (else 2). bindx<8 centres the
- // balloon horizontally; bindx>=8 pins it at x=0x21.
- // 3. Plays the suspect's voice via `_SpoolSound(talk - 1)` where
- // `talk = (Partner == 0) ? gd[+0x6] : gd[+0x0]` (1df2:0258),
- // then waits for a click.
- // 4. Overlays the partner's reaction balloon (text @
- // `KDTextIndex[+10]`) at (0x21, y) and plays
- // `_SayKDDigital(5)`.
- // 5. Clears `_FirstTry` (1df2:0447) and returns to LastScreen.
+ // `_DisplayAlibi @ 1df2:0145`:
+ // 1. MIDI 6 loser sting (gallery still visible).
+ // 2. PIC 0x3e + suspect balloon + portrait at (0x82, py).
+ // bindx = digit prefix (else 2). bindx<8 centres balloon,
+ // bindx>=8 pins at x=0x21.
+ // 3. `_SpoolSound(talk-1)` where talk = partner==0 ? gd[+6] : gd[+0].
+ // 4. Partner reaction balloon @ KDTextIndex[+10], `_SayKDDigital(5)`.
+ // 5. _FirstTry = 0; NextScreen = LastScreen (1df2:043f).
if (!guessedRight) {
- // Balloon-shape table @ 29be:1050 â 16 entries à u16.
+ // Balloon-shape table @ 29be:1050.
static const uint16 kAlibiBubbles[16] = {
0x002B, 0x002C, 0x002D, 0x002E,
0x00AB, 0x00AC, 0x00AD, 0x00AE,
@@ -4746,10 +4055,7 @@ void EEMEngine::doAccuse() {
if (raw)
alibi = parseString(raw, _playerName, _partner);
}
- // Digit-prefix dispatch â `_DisplayAlibi @ 1df2:0163` reads
- // `*str` for `bindx` and advances `str = pbVar7 + 1` so the
- // digit doesn't reach the renderer. Non-digit first chars fall
- // through to the default bindx=2 (1df2:015e).
+ // `_DisplayAlibi @ 1df2:0163` â bindx = digit prefix, else 2.
uint bindx = 2;
const byte firstChar = alibi.empty() ? (byte)0 : (byte)alibi[0];
if (firstChar >= '0' && firstChar <= '9') {
@@ -4774,9 +4080,7 @@ void EEMEngine::doAccuse() {
_balloonArchive.size() > (bubNum & 0x7F) &&
_balloonArchive.loadEntry(bubNum & 0x7F, balloon);
- // Position math from 1df2:01a4-1df2:0207. py is the suspect
- // portrait's Y; defaults to 0x5a, only overridden in the
- // bindx<8 branch when the balloon is too tall to fit.
+ // Position math @ 1df2:01a4-0207.
int balloonX = 0x21;
int balloonY = 1;
int py = 0x5a;
@@ -4796,14 +4100,7 @@ void EEMEngine::doAccuse() {
balloonY = (bh < 0x4f) ? (0x50 - bh) / 2 : 1;
}
- // `base` = BG + suspect + partner sprite â the persistent layer
- // that survives across both balloon phases. The original engine
- // keeps PIC 0x3e in the master BG buffer (16000), `_AddPicBackground`
- // commits the suspect there, and the partner animation
- // registered by `_DoAccuse @ 1df2:0c30` is re-blitted by every
- // `_Repaint` via `_DrawActiveAnimations`. We don't have a
- // slot-based animation system, so we manually keep a "base"
- // surface and re-draw the partner frame for each phase.
+ // `base` = BG + suspect + partner. Survives both balloon phases.
Graphics::ManagedSurface base(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
base.clear();
@@ -4815,20 +4112,15 @@ void EEMEngine::doAccuse() {
Common::Point(0x82, py),
(uint32)transp);
}
- // Partner sprite at (5, 0x50). Anim cells: 2 (Jake) / 0x10
- // (Jenny); script key 0x02 â same indices `_DoAccuse @
- // 1df2:0c30` uses for its `_NewAnimation` call. Partner is
- // drawn AFTER the suspect so it doesn't get clipped by the
- // portrait if their bounding boxes graze.
+ // Partner at (5, 0x50). ANI 2/0x10, script 0x02 (`_DoAccuse
+ // @ 1df2:0c30`). Drawn after suspect.
const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
Animation partnerAni;
const bool havePartner =
_aniArchive.loadAnimation(partnerAnim, partnerAni) &&
!partnerAni.empty();
- // Alibi-phase scratch = base + alibi balloon + alibi text +
- // partner sprite (animation slot drawn on top per
- // `_DrawActiveAnimations`).
+ // scratch = base + alibi balloon/text + partner.
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
scratch.simpleBlitFrom(base);
@@ -4838,8 +4130,7 @@ void EEMEngine::doAccuse() {
Common::Point(balloonX, balloonY),
(uint32)transp);
}
- // Balloon-text inset table @ 29be:0875 â same dispatch as KD
- // balloons. WordWrap color is 0 inside a balloon (1df2:0240).
+ // Inset table @ 29be:0875. Color 0 inside balloon (1df2:0240).
uint16 tx = 5, ty = 4, tw = 155;
getBalloonInsets(bubNum, tx, ty, tw);
if (_font.isLoaded() && !alibi.empty()) {
@@ -4854,9 +4145,7 @@ void EEMEngine::doAccuse() {
partnerAni[frameIdx], 5, 0x50);
}
- // Step 1 â alibi music. Original blocks until MIDI 6 ends with
- // the gallery still on screen. We poll `_music->isPlaying`;
- // click/ESC aborts early.
+ // MIDI 6 â blocks until done (or click/ESC aborts).
if (_music && _voiceOn) {
_music->playMus(6, /*loop=*/false);
const uint32 musStart = g_system->getMillis();
@@ -4875,8 +4164,7 @@ void EEMEngine::doAccuse() {
break;
}
}
- // Hard cap so we never get stuck if MIDI never reports
- // finish (some sound configurations).
+ // Hard cap if MIDI never reports finish.
if (g_system->getMillis() - musStart > 10000)
break;
g_system->updateScreen();
@@ -4885,9 +4173,8 @@ void EEMEngine::doAccuse() {
_music->stop();
}
- // Step 2 â flip the alibi scene to screen + play suspect voice.
- // `talk = (Partner==0) ? gd[+0x6] : gd[+0x0]` (1df2:0252-0258);
- // indices are 1-based so subtract 1 before SpoolSound.
+ // Suspect voice. talk = partner==0 ? gd[+0x6] : gd[+0x0] (1df2:0252).
+ // 1-based, so SpoolSound(talk-1).
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, 320, 200);
g_system->updateScreen();
@@ -4903,16 +4190,8 @@ void EEMEngine::doAccuse() {
}
waitForInput(60000);
- // Step 3 â partner reaction balloon. Mirrors 1df2:026e-1df2:02b6.
- // Rebuild scratch from `base` (BG + suspect + partner sprite)
- // so the alibi balloon + text don't bleed through. The
- // original's `_Repaint` re-renders the master BG (which still
- // has the alibi balloon committed), but the engine ALSO calls
- // `_GetBackground(0x3e)` again before `_AddPicBackground` for
- // each new balloon â flushing the master back to a clean state.
- // We achieve the same end result by restoring `base` here.
- // `_SayKDDigital(5)` auto-cancels the still-playing alibi voice
- // (spoolSound calls stopSpool internally) so no explicit stop.
+ // Partner reaction @ 1df2:026e-02b6. Rebuild from `base` so alibi
+ // balloon clears. `_SayKDDigital(5)` auto-cancels alibi voice.
const byte *reactIdx = _mystery.kdTextIndex();
if (reactIdx) {
const int16 reactOff = (int16)READ_LE_UINT16(reactIdx + 10);
@@ -4937,8 +4216,6 @@ void EEMEngine::doAccuse() {
if (haveR && rBalloon.surface.h < 0x4e)
rY = (0x50 - rBalloon.surface.h) / 2;
- // Reset to a clean BG + suspect, then layer the new
- // balloon and the (refreshed) partner frame on top.
scratch.simpleBlitFrom(base);
if (haveR) {
const byte transp = (byte)(rBalloon.flags >> 8);
@@ -4971,35 +4248,22 @@ void EEMEngine::doAccuse() {
waitForInput(60000);
_mystery._firstTry = false;
- // `_DisplayAlibi @ 1df2:043f` writes `_NextScreen =
- // _LastScreen`. The original returns the player to the caller
- // (PDA / site / map) for another try.
+ // `_DisplayAlibi @ 1df2:043f` â NextScreen = LastScreen.
_nextScreen = _lastScreen != kScreenInvalid
? (ScreenId)_lastScreen : kScreenSite;
return;
}
- // Right suspect â full win flow. Mirrors `_DisplayCorrect @
- // 1df2:073c`: mark mystery solved, advance chain stage if the
- // tier is complete, swap MIDI to the win cue, run SCRAPBK.ANI,
- // show the per-mystery ending, save the profile, return to the
- // action menu (`_NextScreen = 0xc` at 1df2:0895).
+ // Win â `_DisplayCorrect @ 1df2:073c`: mark solved, advance chain,
+ // MIDI 5, SCRAPBK.ANI, ending, save, NextScreen=0xc (1df2:0895).
{
const uint mn = _mystery.number();
if (mn < sizeof(_mysteriesSolved)) {
_mysteriesSolved[mn] = _mystery._firstTry ? 2 : 1;
}
- // Mirrors the chain-advancement loop at `_DisplayCorrect @
- // 1df2:0824-0850`. Skip mystery 0 (the practice case) per the
- // `if (_MysteryNumber != 0)` guard at 1df2:080d, then check
- // every mystery in the current tier:
- // stage 1 â check 1..0x18 (24 mysteries, "A chain")
- // stage 2 â check 0x19..0x30 (24 mysteries, "B chain")
- // stage 3 â check 0x31..0x36 (6 mysteries, "C chain")
- // If every solve flag in that range is non-zero, bump the
- // stage. Original increments unconditionally (3â4 is harmless
- // since no further range covers it); we cap at 3 for clarity.
+ // Chain advance @ 1df2:0824-0850. Skip mystery 0 (practice).
+ // stage 1: 1..0x18, stage 2: 0x19..0x30, stage 3: 0x31..0x36.
if (mn != 0) {
uint lo = 0, hi = 0;
switch (_chainStage) {
@@ -5013,12 +4277,7 @@ void EEMEngine::doAccuse() {
if (i >= sizeof(_mysteriesSolved) || _mysteriesSolved[i] == 0)
allSolved = false;
}
- // `_DisplayCorrect @ 1df2:0852` increments unconditionally
- // when every case in the current tier is solved â including
- // past stage 3 (so a stage-4 endgame state exists in the
- // `.PLR` save format). `_ActionScreen @ 1c33:19d1` gates
- // the menu on `if (3 < _chainStage)` to grey
- // Choose-A-Mystery and Practice once everything's solved.
+ // 1df2:0852 increments past 3 (stage-4 endgame); cap at 4.
if (allSolved && _chainStage < 4) {
_chainStage++;
debugC(1, kDebugMystery,
@@ -5027,25 +4286,17 @@ void EEMEngine::doAccuse() {
}
}
- // `_DisplayCorrect @ 1df2:073c` order:
- // 1df2:0773 _AllBlack();
- // 1df2:0776 _BuildBackground(5, 0x42, 0x14); // conclusion BG
- // 1df2:0780 _FadeIn();
- // 1df2:0789 _MIDIPlay(5); // win music
- // 1df2:07ac _DisplayClue(MysteryIndex[+0x10]); // chain recap
- // `_BuildBackground @ 172b:13e2` loads PIC 0x3D (the standard
- // frame) and overlays SITES.DBD entry 5 at (0x42, 0x14), then
- // sets palette via `_GetPalette(sitenum + 1)` = palette 6.
- // Without this BG the chain-recap balloons render on top of
- // the accuse-gallery BG (PIC 0x3F + suspect portraits), which
- // is visually jarring â the conclusion is supposed to play
- // against the dedicated "office / desk" scene.
+ // `_DisplayCorrect @ 1df2:073c`:
+ // _AllBlack; _BuildBackground(5, 0x42, 0x14); _FadeIn;
+ // _MIDIPlay(5); _DisplayClue(MysteryIndex[+0x10]).
+ // `_BuildBackground @ 172b:13e2` = PIC 0x3D + SITES entry 5 at
+ // (0x42, 0x14), palette = sitenum+1 = 6.
Graphics::Surface *blk = g_system->lockScreen();
if (blk) {
memset(blk->getPixels(), 0, 320 * 200);
g_system->unlockScreen();
}
- setSitePalette(6); // sitenum + 1 per `_GetPalette` call
+ setSitePalette(6); // sitenum + 1 (`_GetPalette`).
Picture frame, scene;
if (_picsArchive.loadEntry(0x3d, frame)) {
g_system->copyRectToScreen(frame.surface.getPixels(),
@@ -5063,15 +4314,7 @@ void EEMEngine::doAccuse() {
sw, sh);
}
- // Partner sprite at (5, 0x50). The original `_DoAccuse @
- // 1df2:0c30` registered the partner anim BEFORE entering the
- // gallery; that slot stays active across `_DisplayCorrect`'s
- // `_BuildBackground` (which calls `_Repaint` â
- // `_DrawActiveAnimations` and re-blits the partner over the
- // fresh BG). We don't have a slot system, so manually stamp
- // the resting frame here. `displayClue` snapshots the screen
- // on entry, so the partner ends up baked into its BG and is
- // preserved across every clue iteration.
+ // Stamp partner at (5, 0x50); displayClue snapshots screen.
const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
Animation partnerAni;
if (_aniArchive.loadAnimation(partnerAnim, partnerAni) &&
@@ -5091,62 +4334,40 @@ void EEMEngine::doAccuse() {
if (_music && _voiceOn)
_music->playMus(5, /*loop=*/false);
- // Chain-by-chain RECAP. Partner enumerates every required
- // clue ("Look at this â the suspect was here at 8pm", "... and
- // remember the broken vase from the kitchen", "... so it had
- // to be X!") and arrives at the conclusion. Without rendering
- // it the player goes straight from suspect-pick to the
- // scrapbook anim and misses the deduction entirely.
+ // Chain recap â partner enumerates required clues.
const byte *solved = _mystery.solvedClueBlock();
if (solved)
displayClue(solved);
if (_music && _voiceOn)
_music->stop();
- // `_DifferenceAnimation("scrapbk.ani")` (1df2:0848) â the
- // physical scrapbook flip animation that introduces the
- // per-mystery ending pages.
+ // `_DifferenceAnimation("scrapbk.ani")` @ 1df2:0848.
playAnm(Common::Path("SCRAPBK.ANI"), 120, true);
- // `_ShowOneScrap @ 1f78:0773` is `_DisplayEnding(num, 1)` â
- // the multi-page per-mystery ending narrative. This shares the
- // same red edge cursor + edge-only mouse navigation used by the
- // scrapbook browser.
+ // `_ShowOneScrap @ 1f78:0773` = `_DisplayEnding(num, 1)`.
doShowEnding(mn);
- // Mirrors `_SavePlayerRecord` at 1df2:0857 â once the
- // `_mysteriesSolved` table is updated, the original
- // immediately persists the player record so the win sticks
- // even if the player quits before reaching the menu.
- //
- // Order matters: `_mystery.clear()` BEFORE `saveProfile` so the
- // save records `hasMystery=false`. Otherwise the next load of
- // this profile sees the just-won mystery still loaded and the
- // screen driver routes to its map (forcing the player to
- // replay the win flow). Mirrors `_DisplayCorrect @ 1df2:0851`
- // (`_DeleteSavedGame` removes the in-progress save before
- // `_SavePlayerRecord` writes the post-win profile).
+ // `_SavePlayerRecord` @ 1df2:0857. Order: clear() before save
+ // so hasMystery=false (matches `_DeleteSavedGame` @ 1df2:0851).
_mystery.clear();
const Common::Error err = saveProfile(_playerName);
if (err.getCode() != Common::kNoError)
warning("saveProfile after solve failed: %s",
err.getDesc().c_str());
- // `_DisplayCorrect @ 1df2:0895` writes `_NextScreen = 0xc` â
- // the winner returns to the post-mystery `_ActionScreen`.
+ // `_DisplayCorrect @ 1df2:0895` â NextScreen = 0xc.
_nextScreen = kScreenAction;
}
}
void EEMEngine::doAccuseFloppy() {
- // Floppy accuse flow â mirrors:
- // `_KDHelp_Floppy / FUN_1d40_11fd @ 1d40:11fd` (score gate +
- // partner "ready / not ready" balloon)
- // `FUN_1d40_0e07 @ 1d40:0e07` (clue-selection screen â skipped
- // in this MVP; selectedPoints() already takes the top-5)
- // `FUN_1d40_0c79 @ 1d40:0c79` (gallery picker)
- // `_DisplayCorrect_Floppy @ 1d40:0894`
- // `_DisplayAlibi_Floppy @ 1d40:00df`
+ // Floppy accuse:
+ // `_KDHelp_Floppy / FUN_1d40_11fd @ 1d40:11fd` â score gate.
+ // `FUN_1d40_0e07` â clue selection (skipped; selectedPoints()
+ // already takes top-5).
+ // `FUN_1d40_0c79 @ 1d40:0c79` â gallery picker.
+ // `_DisplayCorrect_Floppy @ 1d40:0894`.
+ // `_DisplayAlibi_Floppy @ 1d40:00df`.
const byte *kdIdx = _mystery.kdTextIndex();
const byte *bufBase = _mystery.blobAt(0);
const uint32 mysSize = _mystery.dataSize();
@@ -5171,9 +4392,8 @@ void EEMEngine::doAccuseFloppy() {
if (lineLen == 0)
return;
Common::String raw(p, lineLen);
- // Digit prefix â balloon variant (per `_GetKDTextBalloon_Floppy
- // @ 1d40:009f` and the `_KDBalloonByChar_Floppy` table at
- // `2608:0c14 + char`).
+ // Digit â balloon variant (`_GetKDTextBalloon_Floppy @ 1d40:009f`
+ // + table @ 2608:0c14).
static const uint8 kDigitToBalloon[10] = {
0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1c, 0x1d, 0x1e, 0x0a
};
@@ -5233,19 +4453,18 @@ void EEMEngine::doAccuseFloppy() {
}
};
- // `FUN_1d40_11fd` score gate. Picks one of three KD strings and
- // returns 1 only when score >= 100.
+ // `FUN_1d40_11fd` â score gate, ready when score >= 100.
const int score = _mystery.selectedPoints();
uint kdSlot;
bool readyToSolve;
if (score < 50) {
- kdSlot = 0; // KDTextIndex[0] â "we've barely started"
+ kdSlot = 0; // "we've barely started"
readyToSolve = false;
} else if (score < 100) {
- kdSlot = 1; // KDTextIndex[1] â "getting closer"
+ kdSlot = 1; // "getting closer"
readyToSolve = false;
} else {
- kdSlot = 2; // KDTextIndex[2] â "ready to solve"
+ kdSlot = 2; // "ready to solve"
readyToSolve = true;
}
showFloppyKDHint(kdSlot);
@@ -5255,14 +4474,8 @@ void EEMEngine::doAccuseFloppy() {
return;
}
- // Clue selection screen â `FUN_1d40_0e07 @ 1d40:0e07`. The user
- // must pick exactly `6 - chainStage` clues from the found list,
- // rendered on the red accuse-mode BG (PIC 0x1A7). Verified by
- // the asm at 1d40:0e34 reading `local_c = 6 - DAT_28da_3052`,
- // matching the CD's `_DoAccuse @ 1df2:0bdd` expected count.
- // `doAccuseNotes()` already handles this UI for both variants
- // (note text reading is variant-aware via `noteTextOff`); it
- // returns true on commit, false on ESC / pointer navigation.
+ // Clue selection â `FUN_1d40_0e07 @ 1d40:0e07`. Pick `6 - chainStage`
+ // clues (1d40:0e34: `local_c = 6 - DAT_28da_3052`).
if (!doAccuseNotes()) {
if (_nextScreen == kScreenAccuse) {
_nextScreen = _lastScreen != kScreenInvalid
@@ -5271,19 +4484,8 @@ void EEMEngine::doAccuseFloppy() {
return;
}
- // Second score gate â based on the CLUES THE USER ACTUALLY
- // PICKED. `_DoAccuse_Floppy @ 1d40:0e07` (after the commit
- // branch at 1d40:0f3a) reads `local_a = FUN_1d40_0c48()` (=
- // sum of byte +6 across selected note entries) and:
- // if (local_a < 100) â KDTextIndex[+6] hint (slot 3),
- // bail back to last screen.
- // else â call FUN_1d40_0c79 (gallery picker).
- // Note this is distinct from the entry gate on
- // `_GetSelectedPoints_Floppy @ 1d40:0c23` (which uses the
- // auto-top-5 of FOUND clues â what `Mystery::selectedPoints`
- // returns on floppy). A player can be "ready to solve" by score
- // yet still pick the wrong subset; that's the case this gate
- // catches.
+ // 2nd score gate on USER-PICKED clues (1d40:0f3a, FUN_1d40_0c48 =
+ // sum of byte +6 across selected entries). <100 â slot 3 hint.
int userSelectedScore = 0;
{
const byte *ni2 = _mystery.noteIndex();
@@ -5304,13 +4506,10 @@ void EEMEngine::doAccuseFloppy() {
return;
}
- // `FUN_1d40_0c79` â gallery picker. Show "Which suspect?" KD
- // hint at slot 4 (KDTextIndex[+8]/2 = entry index 4), then
- // render the gallery and wait for click.
+ // `FUN_1d40_0c79` â gallery picker. KDTextIndex slot 4 prompt.
showFloppyKDHint(4);
- // Build slot list. Floppy gallery section: byte0 = numSuspects,
- // then variable-stride entries.
+ // Floppy gallery: byte0 = numSuspects, then variable-stride entries.
const uint8 num = _mystery.numSuspects();
if (num == 0) {
_nextScreen = _lastScreen != kScreenInvalid
@@ -5318,8 +4517,7 @@ void EEMEngine::doAccuseFloppy() {
return;
}
- // Verbatim from `_DrawGallery_Floppy @ 154e:0050` â slot positions
- // at `2608:016c` (5 Ã {u16 x, u16 y}).
+ // `_DrawGallery_Floppy @ 154e:0050` slots @ 2608:016c.
struct GallerySlot { int x, y; };
static const GallerySlot kFloppySlots[5] = {
{ 0x53, 0x0e }, { 0x9b, 0x0e }, { 0xe3, 0x0e },
@@ -5338,7 +4536,6 @@ void EEMEngine::doAccuseFloppy() {
if (haveAccuseBg)
scratch.simpleBlitFrom(accuseBg.surface);
- // Partner sprite (anim 2 / 0x10) â same as CD doAccuse.
const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
Animation partnerAni;
if (_aniArchive.loadAnimation(partnerAnim, partnerAni) &&
@@ -5370,8 +4567,7 @@ void EEMEngine::doAccuseFloppy() {
if (!_picsArchive.getPicture(picId, portrait))
continue;
const GallerySlot &s = kFloppySlots[phys];
- // `_DrawGallery_Floppy @ 154e:00ed` bottom-aligns to baseline
- // 0x48 (= 72), same as CD: `placeY = s.y + (0x48 - h)`.
+ // Bottom-align to baseline 0x48 (`154e:00ed`).
const int placeX = s.x;
const int placeY = s.y + (0x48 - portrait.surface.h);
const byte transp = (byte)(portrait.flags >> 8);
@@ -5450,33 +4646,16 @@ void EEMEngine::doAccuseFloppy() {
const bool guilty = _mystery.isGuilty((uint)picked);
if (guilty) {
- // Win path. Mirrors `_DisplayCorrect_Floppy @ 1d40:0894`:
- // 1d40:08a0 _BuildBackground_Floppy(5, 0x42, 0x14);
- // â PIC 0x3d frame + SITES entry 5 at (0x42,
- // 0x14), palette = sitenum + 1 = 6.
- // 1d40:08b1 _FadeIn();
- // 1d40:08c0 _MIDIPlayFile("travel-2.xmi");
- // 1d40:08d0 walk solved chain via _DisplayHotspotClue_Floppy
- // + _WaitForClick per record. Mid-recap: when
- // only 3 records remain, play
- // _PlayTitleANM_Floppy(0) = SCRAPBK.ANI.
- // 1d40:0939 ((u16 *)0x3054)[mysteryNum] =
- // _firstTry ? 2 : 1;
- // 1d40:0941 tier-promotion check (advance _chainStage when
- // every mystery in the current tier is solved).
- // 1d40:0982 _SavePlayerRecord (= saveProfile)
- // 1d40:0985 _DeleteMysteryFile (= mystery cleanup)
- // 1d40:0991 MakeSolvedSound = 1; ShowOneScrap(mystery)
- // 1d40:09b0 _NextScreen = 0xc.
+ // `_DisplayCorrect_Floppy @ 1d40:0894`:
+ // _BuildBackground_Floppy(5, 0x42, 0x14); _FadeIn;
+ // _MIDIPlayFile("travel-2.xmi");
+ // Walk solved chain, SCRAPBK.ANI when 3 records left.
+ // Mark solved, tier-promote, SavePlayerRecord,
+ // ShowOneScrap, NextScreen = 0xc.
const uint mn = _mystery.number();
- // Conclusion BG composition. `_BuildBackground_Floppy @
- // 16e2:12fd` blits PIC 0x3d (the desk frame) onto a cleared
- // page, then overlays SITES.DBD entry 5 (the conclusion
- // artwork) at (param_2, param_3) = (0x42, 0x14). Palette
- // becomes `sitenum + 1` = 6. Same composition the CD `doAccuse`
- // win flow performs (see ui.cpp:4145-4166), just driven from
- // the floppy data archives.
+ // `_BuildBackground_Floppy @ 16e2:12fd` â PIC 0x3d + SITES entry 5
+ // at (0x42, 0x14), palette 6.
{
Graphics::Surface *blk = g_system->lockScreen();
if (blk) {
@@ -5501,17 +4680,8 @@ void EEMEngine::doAccuseFloppy() {
scene.surface.pitch, sx, sy, sw, sh);
}
- // Partner sprite at (5, 0x50). The original keeps its
- // `_NewAnimation` slot active across `_DisplayCorrect`'s
- // `_BuildBackground_Floppy`, so `_UpdateAnimations` keeps
- // re-stamping the partner over the fresh BG. We don't have
- // a slot system, so manually bake the resting frame into
- // the BG before the recap kicks off â `displayFloppyDialog
- // Records` snapshots the screen on entry and the partner
- // stays visible across every chain record. Without this
- // the win sequence ends up partner-less. Anim 2 (Jake) /
- // 0x10 (Jenny), script key 0x02 â same as the gallery
- // picker and the CD accuse path (ui.cpp:4177).
+ // Stamp partner at (5, 0x50); displayFloppyDialogRecords
+ // snapshots screen. ANI 2/0x10, script 0x02.
const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
Animation partnerAni;
if (_aniArchive.loadAnimation(partnerAnim, partnerAni) &&
@@ -5529,22 +4699,12 @@ void EEMEngine::doAccuseFloppy() {
g_system->updateScreen();
}
- // Win music â TRAVEL-2.XMI (`2608:0c84`, played via
- // `_MIDIPlayFile` at `_DisplayCorrect_Floppy @ 1d40:08c0`).
- // The CD path uses internal MUS index 5; the floppy plays the
- // XMI directly by filename. `_voiceOn` (= `DAT_28da_3050`) is
- // the gate the original checks first.
+ // TRAVEL-2.XMI @ 2608:0c84 (`_MIDIPlayFile` @ 1d40:08c0).
if (_music && _voiceOn)
_music->playFile(Common::Path("travel-2.xmi"), false);
- // Walk the solved-clue chain. Header[+0x12] points at a
- // `count` byte followed by `count` dialog records (same layout
- // as hotspot dialogs). When only three records remain, the
- // original clears animation slots and calls
- // `_PlayTitleANM_Floppy(0)`. The title helper's file table maps
- // index 0 to `SCRAPBK.ANI` and index 1 to `TITLE.ANM`, so this
- // is the same scrapbook flip transition used by the CD win flow,
- // just inserted before the last three floppy recap records.
+ // Walk solved chain (header[+0x12]: count byte + dialog records).
+ // SCRAPBK.ANI fires when 3 records remain (`_PlayTitleANM_Floppy(0)`).
const byte *chain = _mystery.solvedClueBlock();
if (chain) {
const uint count = chain[0];
@@ -5571,23 +4731,11 @@ void EEMEngine::doAccuseFloppy() {
if (_music && _voiceOn)
_music->stop();
- // Mark mystery solved with first-try bonus tracking.
- // `_DisplayCorrect_Floppy @ 1d40:0939`:
- // ((u16 *)0x3054)[mysteryNum] = 1;
- // if (DAT_28da_35df != 0)
- // ((u16 *)0x3054)[iVar5] = 2;
- // `DAT_28da_35df` is `_firstTry`; it starts at 1 on
- // `_ReadMystery_Floppy` and is cleared to 0 by
- // `_DisplayAlibi_Floppy` on a wrong accusation. So 2 = won on
- // first try, 1 = won after at least one alibi.
+ // 1d40:0939 â solved[mn] = _firstTry ? 2 : 1.
if (mn < sizeof(_mysteriesSolved))
_mysteriesSolved[mn] = _mystery._firstTry ? 2 : 1;
- // Tier-promotion check. `_DisplayCorrect_Floppy @
- // 1d40:0941..0978` walks the current tier's mystery range and
- // advances `DAT_28da_3052` (= `_chainStage`) when every entry
- // is non-zero. Skip mystery 0 (practice case) per the
- // `if (DAT_2608_149c != 0)` guard at 1d40:093f.
+ // Tier promotion @ 1d40:0941..0978. Skip mystery 0 (practice).
if (mn != 0) {
uint lo = 0, hi = 0;
switch (_chainStage) {
@@ -5610,10 +4758,8 @@ void EEMEngine::doAccuseFloppy() {
}
}
- // `_DisplayCorrect_Floppy` sets `MakeSolvedSound` before the
- // newly solved ending is shown. `FUN_1d40_05b7` reads byte 0 of
- // `E<num>.BIN` and maps values 0..2 through the table at
- // `2608:0c5e` to VOC slots 0x15, 0x16, 0x17.
+ // `MakeSolvedSound`. `FUN_1d40_05b7` maps E<num>.BIN byte 0 (0..2)
+ // via table @ 2608:0c5e to VOC slots 0x15/0x16/0x17.
if (_audio && _voiceOn) {
Common::File ending;
const Common::String fname =
@@ -5631,19 +4777,10 @@ void EEMEngine::doAccuseFloppy() {
}
}
- // `_DisplayCorrect_Floppy @ 1d40:0991` calls
- // `FUN_1ee2_06ac(mystery)`, whose body is just
- // `FUN_1d40_05b7(mystery, 1)` plus font cleanup. That is the
- // floppy ending/scrapbook viewer, and it receives the
- // first-try badge state from the solved table entry above.
+ // 1d40:0991 â FUN_1ee2_06ac (= FUN_1d40_05b7 + font cleanup).
doShowEnding(mn);
- // Persist progress before clearing the in-progress mystery.
- // `_DisplayCorrect_Floppy @ 1d40:0982` calls
- // `_SavePlayerRecord` then `FUN_22dc_0dbd` (which deletes the
- // per-mystery save file via DOS int 21h). Order matters here
- // for the same reason as the CD path â clear â save so the
- // profile records `hasMystery=false`.
+ // 1d40:0982 _SavePlayerRecord. clear() before save.
_mystery._solvedPuzzle = true;
_mystery.clear();
(void)saveProfile(_playerName);
@@ -5651,25 +4788,12 @@ void EEMEngine::doAccuseFloppy() {
return;
}
- // Innocent â `_DisplayAlibi_Floppy @ 1d40:00df`:
- // 1d40:00fb _GetBackground(0x3e);
- // 1d40:0103 _GetPicture(suspect.picID);
- // 1d40:010c _AddPicBackground(susp_pic, 0x82, 0x5a);
- // 1d40:0125 _MIDIPlayFile("fanfare2.xmi"); // alibi sting
- // 1d40:0145 alibi-balloon table at `2608:0c0a + first_char`
- // (separate from the KD digitâballoon table at
- // `2608:0c14` â yes, 0xc0a, NOT 0xc14, verified
- // via the `*(char *)(*local_a + 0xc0a)` access).
- // Default = `DAT_2608_0c3c` = 0x2c.
- // 1d40:01a3 balloon centred:
- // bx = (0x140 - balloon.w) / 2;
- // by = (0x5a - balloon.h) / 2;
- // 1d40:01d5 WordWrap alibi text inside balloon, _WaitForClick.
- // 1d40:01ee KD reaction at KDTextIndex[+10] = slot 5
- // (NOT slot 8 â slot 8 is the gallery prompt).
- // Balloon at (0x21, (0x50 - h) / 2).
- // 1d40:0247 _NextScreen = _LastScreen;
- // 1d40:024b _firstTry = 0;
+ // `_DisplayAlibi_Floppy @ 1d40:00df`:
+ // BG 0x3e + suspect pic at (0x82, 0x5a); MIDI fanfare2.xmi.
+ // Balloon table @ 2608:0c0a (default 0x2c). Centered:
+ // bx = (0x140-w)/2, by = (0x5a-h)/2.
+ // KD reaction @ KDTextIndex[+10] = slot 5; NextScreen = LastScreen;
+ // _firstTry = 0.
const byte *susp = _mystery.floppySuspectEntry((uint)picked);
uint16 picId = 0;
uint16 alibiOff = 0xFFFF;
@@ -5678,8 +4802,7 @@ void EEMEngine::doAccuseFloppy() {
alibiOff = _mystery.alibiTextOffset((uint)picked);
}
- // Alibi-screen MIDI sting (FANFARE2.XMI). `_MIDIPlayFile` is
- // gated on `_voiceOn` (= `DAT_28da_3050`) in the original.
+ // FANFARE2.XMI alibi sting (`_MIDIPlayFile` gated on _voiceOn).
if (_music && _voiceOn)
_music->playFile(Common::Path("fanfare2.xmi"), false);
@@ -5699,18 +4822,12 @@ void EEMEngine::doAccuseFloppy() {
_playerName, _partner);
}
- // Alibi balloon dispatch. The original reads the alibi-balloon
- // table at `2608:0c0a + first_char` â a SEPARATE table from the
- // KD-hint table at `2608:0c14`. For digits '0'..'9' the values at
- // `2608:0c3a..0c43` are { 0x2a, 0x2b, 0x2c, 0x2d, 0xaa, 0xab,
- // 0xac, 0xad, 0x09, 0x0a }; default (non-digit char) is
- // `DAT_2608_0c3c` = 0x2c. The high-bit values (0xAA..0xAD) are
- // `_GetBalloon`'s "mirrored" flag â the low 7 bits are the
- // balloon idx, top bit flips horizontally.
+ // Alibi balloon table @ 2608:0c0a (NOT 0c14). Digits @ 2608:0c3a..0c43.
+ // Default 0x2c. High bit = mirror flag.
static const uint8 kFloppyAlibiBalloonByDigit[10] = {
0x2a, 0x2b, 0x2c, 0x2d, 0xaa, 0xab, 0xac, 0xad, 0x09, 0x0a
};
- uint balloonRaw = 0x2c; // default `DAT_2608_0c3c`
+ uint balloonRaw = 0x2c; // DAT_2608_0c3c
if (!alibi.empty() && alibi[0] >= '0' && alibi[0] <= '9') {
balloonRaw = kFloppyAlibiBalloonByDigit[(int)(alibi[0] - '0')];
alibi.deleteChar(0);
@@ -5734,9 +4851,7 @@ void EEMEngine::doAccuseFloppy() {
Picture balloon;
const bool haveBalloon = _balloonArchive.size() > balloonIdx &&
_balloonArchive.loadEntry(balloonIdx, balloon);
- // Centred per `_DisplayAlibi_Floppy @ 1d40:01a0`:
- // uVar2 = (0x140 - balloon.w) / 2; // x
- // uVar3 = (0x5a - balloon.h) / 2; // y (anchor in top-half)
+ // Centered @ 1d40:01a0: x=(0x140-w)/2, y=(0x5a-h)/2.
int balloonX = 0x21;
int balloonY = 1;
if (haveBalloon) {
@@ -5745,10 +4860,7 @@ void EEMEngine::doAccuseFloppy() {
if (balloonX < 0) balloonX = 0;
if (balloonY < 0) balloonY = 0;
const byte transp = (byte)(balloon.flags >> 8);
- // `_GetBalloon`'s mirror flag (high bit of the table value)
- // flips the balloon horizontally â the original applies it
- // inside the blit primitive; ScummVM's `transBlitFrom` exposes
- // the same via its `flipped` argument.
+ // Mirror flag (high bit) flips balloon horizontally.
scene.transBlitFrom(balloon.surface,
Common::Point(balloonX, balloonY),
transp, flipBalloon);
@@ -5759,19 +4871,13 @@ void EEMEngine::doAccuseFloppy() {
_font.drawWordWrapped(&scene, balloonX + tx, balloonY + ty,
MAX<int>(8, (int)tw), alibi, 0);
}
- // The floppy original keeps the accuse partner animation slot alive
- // across `_DisplayAlibi_Floppy` and restores the screen with active
- // animations between alibi phases. Stamp the same resting frame here
- // before the KD reaction helper snapshots the current screen.
+ // Stamp partner resting frame before KD reaction snapshots screen.
blitAccusePartner(scene, _aniArchive, _partner, g_system->getMillis());
g_system->copyRectToScreen(scene.getPixels(), scene.pitch, 0, 0, 320, 200);
g_system->updateScreen();
- // `_DisplayAlibi_Floppy` does NOT play any per-suspect VOC â the
- // alibi table at `2608:0c5e` (3 bytes: 0x15, 0x16, 0x17) is
- // referenced by the post-win scrapbook (`FUN_1d40_05b7`), not the
- // alibi screen. The MIDI sting we kicked off above carries the
- // audio.
+ // No per-suspect VOC â alibi table @ 2608:0c5e is for post-win
+ // scrapbook (`FUN_1d40_05b7`), not alibi.
while (!shouldQuit()) {
Common::Event ev;
@@ -5791,12 +4897,7 @@ void EEMEngine::doAccuseFloppy() {
g_system->delayMillis(10);
}
- // Partner reaction â `KDTextIndex[+10]` is byte offset 10 = u16
- // stride entry 5 (NOT 8). Verified at
- // `_DisplayAlibi_Floppy @ 1d40:01ee`:
- // `iVar4 = MysteryOff + *(KDTextIndex + 10);`
- // Our `showFloppyKDHint(slot)` reads `kdIdx + slot * 2`, so slot
- // 5 is the right argument here.
+ // `_DisplayAlibi_Floppy @ 1d40:01ee` â KDTextIndex[+10] = slot 5.
showFloppyKDHint(5);
if (_music && _voiceOn)
_music->stop();
@@ -5810,20 +4911,8 @@ void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
int highlighted,
Common::Array<Common::Rect> &slotRects,
Common::Array<int> &slotSuspect) {
- // Accuse-gallery redraw â formerly the `drawGallery` lambda inside
- // `doAccuse`. Mirrors `_DoAccuseGallery @ 1df2:0a31` portrait grid:
- // PIC 0x3f backdrop, suspect portraits at the 5 fixed slots
- // (`kGallerySlots` in this file's anon namespace), and a 1-px
- // outline (palette index 0xFE) around the highlighted slot.
- //
- // Partner sprite at (5, 0x50): the original `_DoAccuse @ 1df2:0bdd`
- // registers `_NewAnimation(5, 0x50, partnerCells, script=2, prior=1)`
- // (1df2:0c30) BEFORE calling `_DoAccuseGallery`, then `_DrawGallery`
- // calls `_DrawActiveAnimations` (158f:00a3) which re-renders the
- // slot every frame. Without an explicit blit here, our port's
- // accuse screen comes out partner-less. Anim CELLS are 2 (Jake) /
- // 0x10 (Jenny); SCRIPT key is 0x02 for both partners (matches the
- // `CONCAT22(2, ...)` arg verified at 1df2:0c2e).
+ // `_DoAccuseGallery @ 1df2:0a31` portrait grid. PIC 0x3f + 5 slots.
+ // Partner (5, 0x50) ANI 2/0x10, script 0x02 (`_DoAccuse @ 1df2:0c30`).
Picture accuseBg;
const bool haveAccuseBg = _picsArchive.getPicture(0x3f, accuseBg);
@@ -5833,10 +4922,7 @@ void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
if (haveAccuseBg)
scratch.simpleBlitFrom(accuseBg.surface);
- // Partner sprite, drawn BEFORE portraits so the suspect grid
- // covers it where they overlap (the gallery slots start at
- // y=14 / y=90, partner is at y=0x50=80 â no overlap, so order
- // is purely defensive).
+ // Partner drawn first; defensive (no slot overlap).
const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
Animation partnerAni;
if (_aniArchive.loadAnimation(partnerAnim, partnerAni) &&
@@ -5856,8 +4942,7 @@ void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
const uint8 phys = _mystery._newOrder[i];
if (phys >= 5)
continue;
- // `_DrawGallery @ 158f:00b9` skips suspects whose
- // `_InGallery[phys]` flag is 0 â that's the original gate.
+ // `_DrawGallery @ 158f:00b9` skip on `_InGallery[phys]==0`.
if (_mystery._inGallery[phys] == 0)
continue;
const GallerySlot &s = kGallerySlots[phys];
@@ -5894,11 +4979,7 @@ void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
slotSuspect[i] = (int)i;
}
- // Highlight indicator. The original moves the mouse cursor to the
- // centre of the highlighted suspect via `_PutMouseInRect` (1df2:0b8e);
- // we draw a 1-px outline in palette index 0xFE instead, which sits
- // inside the marching-ants cycle range 0xF9..0xFE and is visible
- // under any palette without warping the player's cursor.
+ // Highlight outline (original uses `_PutMouseInRect` @ 1df2:0b8e).
if (highlighted >= 0 && highlighted < (int)slotRects.size() &&
!slotRects[highlighted].isEmpty()) {
Common::Rect r = slotRects[highlighted];
Commit: 37e6dc1ba6887c4b784d40246036e1d73cde2107
https://github.com/scummvm/scummvm/commit/37e6dc1ba6887c4b784d40246036e1d73cde2107
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:10+02:00
Commit Message:
EEM: enforced code standards, mostly removed one-lined statements
Changed paths:
engines/eem/clues.cpp
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/graphics.cpp
engines/eem/music.h
engines/eem/ui.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index ebe7383137a..cedbf6d1319 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -359,7 +359,7 @@ void EEMEngine::doInitClues() {
while (g_system->getEventManager()->pollEvent(ev)) {
if (ev.type == Common::EVENT_KEYDOWN &&
ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
- interruptAudio(/*stopMusicToo=*/false);
+ interruptAudio(/* stopMusicToo= */ false);
skip = true;
break;
}
@@ -471,7 +471,7 @@ void EEMEngine::doInitClues() {
while (g_system->getEventManager()->pollEvent(ev)) {
if (ev.type == Common::EVENT_KEYDOWN &&
ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
- interruptAudio(/*stopMusicToo=*/false);
+ interruptAudio(/* stopMusicToo= */ false);
skip = true;
break;
}
@@ -756,13 +756,13 @@ void EEMEngine::displayClue(const byte *clueBlock) {
// _WordWrap(bubX + table[bub].x, bubY + table[bub].y,
// table[bub].w, ...).
// Fallback (5, 4, 155) = entry-23 inset.
- uint16 bx = 5;
- uint16 by = 4;
- uint16 bw_ = 155;
- getBalloonInsets(balloonId, bx, by, bw_);
- textX = bubX + bx;
- textY = bubY + by;
- textW = bw_;
+ uint16 insetX = 5;
+ uint16 insetY = 4;
+ uint16 insetW = 155;
+ getBalloonInsets(balloonId, insetX, insetY, insetW);
+ textX = bubX + insetX;
+ textY = bubY + insetY;
+ textW = insetW;
copyH = bh;
} else {
// No balloon: clear band.
@@ -825,7 +825,7 @@ void EEMEngine::displayClue(const byte *clueBlock) {
advance = true;
skipAll = true;
// Cut voice + spool only (keep MIDI).
- interruptAudio(/*stopMusicToo=*/false);
+ interruptAudio(/* stopMusicToo= */ false);
break;
}
if (ev.type == Common::EVENT_LBUTTONDOWN) {
@@ -856,7 +856,7 @@ void EEMEngine::displayClue(const byte *clueBlock) {
// _StopTheVoice @ 1ff1:0283 effect (voice only, keep MIDI). Diverges
// from _DisplayClue @ 2404:05e6 which lets voice bleed past dismissal.
- interruptAudio(/*stopMusicToo=*/false);
+ interruptAudio(/* stopMusicToo= */ false);
}
void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
@@ -934,7 +934,7 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
continue;
if (ev.type == Common::EVENT_KEYDOWN &&
ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
- interruptAudio(/*stopMusicToo=*/false);
+ interruptAudio(/* stopMusicToo= */ false);
return true; // skip
}
if (ev.type == Common::EVENT_LBUTTONDOWN ||
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index b809bf86c41..0afc5f5dc9f 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -285,7 +285,7 @@ Common::Error EEMEngine::run() {
}
if (resumed)
- goto screen_loop;
+ goto screenLoop;
// _DoOpeningAnims @ 2520:082a:
// EA Kids logo (PIC) -> HighScore logo (PIC) -> Storm logo
@@ -325,13 +325,13 @@ Common::Error EEMEngine::run() {
if (!shouldQuit() && !_skipIntro)
showFloppyStormLogo();
if (!shouldQuit() && !_skipIntro && _music)
- _music->playFile(Common::Path("THEME.XMI"), /*loop=*/true);
+ _music->playFile(Common::Path("THEME.XMI"), /* loop= */ true);
if (!shouldQuit() && !_skipIntro)
playAnm(Common::Path("CHAT.ANM"), 120,
- /*holdLastFrame=*/false);
+ /* holdLastFrame= */ false);
if (!shouldQuit() && !_skipIntro)
playAnm(Common::Path("MOVIE.ANM"), 120,
- /*holdLastFrame=*/false);
+ /* holdLastFrame= */ false);
} else {
showEAKidsLogo();
if (!shouldQuit() && !_skipIntro)
@@ -344,7 +344,7 @@ Common::Error EEMEngine::run() {
if (_audio)
_audio->playVoc(Common::Path("THUNDER.VOC"));
playAnm(Common::Path("BOLT.ANM"), 120,
- /*holdLastFrame=*/false, /*fadeIn=*/true);
+ /* holdLastFrame= */ false, /* fadeIn= */ true);
waitForInput(1800);
fadeCurrentPaletteToBlack();
if (_audio)
@@ -354,14 +354,14 @@ Common::Error EEMEngine::run() {
if (!shouldQuit() && !_skipIntro && _audio)
_audio->initMysterySounds(60);
if (!shouldQuit() && !_skipIntro && _music)
- _music->playFile(Common::Path("THEME.XMI"), /*loop=*/true);
+ _music->playFile(Common::Path("THEME.XMI"), /* loop= */ true);
for (int i = 1; i <= 20 && !shouldQuit() && !_skipIntro; i++) {
const bool fadeIn = (i == 1 || i == 5);
if (i == 5)
fadeCurrentPaletteToBlack();
Common::String name = Common::String::format("ANIM%02d.A", i);
playAnm(Common::Path(name), 120,
- /*holdLastFrame=*/false, fadeIn);
+ /* holdLastFrame= */ false, fadeIn);
// _SpoolSound(uVar3 - 1) @ 2520:08c2 â per-anim VO, skipped
// when uVar3 == 0x14 @ 2520:08a8.
if (!shouldQuit() && !_skipIntro && i != 20 && _audio) {
@@ -374,17 +374,17 @@ Common::Error EEMEngine::run() {
_audio->cleanMysterySounds();
// _MIDIPlayFile("theme.xmi") @ 2520:0918.
if (!shouldQuit() && !_skipIntro && _music)
- _music->playFile(Common::Path("THEME.XMI"), /*loop=*/true);
+ _music->playFile(Common::Path("THEME.XMI"), /* loop= */ true);
if (!shouldQuit() && !_skipIntro)
playAnm(Common::Path("TITLE.ANM"), 120,
- /*holdLastFrame=*/true, /*fadeIn=*/true);
+ /* holdLastFrame= */ true, /* fadeIn= */ true);
}
skippedIntro = _skipIntro;
_skipIntro = false;
if (isFloppy() && !shouldQuit() && !skippedIntro) {
_nextScreen = kScreenTitle;
- goto screen_loop;
+ goto screenLoop;
}
// Title(B) -> screen 8 (profile) -> 9 (partner) -> C (action) ->
@@ -413,7 +413,7 @@ Common::Error EEMEngine::run() {
// `_mystery.load()` fresh and discard site / clue progress).
if (!shouldQuit() && !resumed)
_nextScreen = _mystery.isLoaded() ? kScreenMap : kScreenAction;
-screen_loop:
+screenLoop:
while (!shouldQuit() && _nextScreen != kScreenInvalid) {
const ScreenId current = (ScreenId)_nextScreen;
debugC(1, kDebugGeneral, "screenDriver: id=%d", (int)current);
@@ -427,7 +427,7 @@ screen_loop:
if (isFloppy()) {
CursorMan.showMouse(false);
playAnm(Common::Path("TITLE.ANM"), 120,
- /*holdLastFrame=*/true, /*fadeIn=*/true);
+ /* holdLastFrame= */ true, /* fadeIn= */ true);
_skipIntro = false;
CursorMan.showMouse(true);
}
@@ -963,7 +963,7 @@ void EEMEngine::startTravelMusic() {
if (!_music || !_mystery.isLoaded() || !_voiceOn)
return;
const uint num = _mystery._siteNumber % 5;
- _music->playMus(num, /*loop=*/false);
+ _music->playMus(num, /* loop= */ false);
}
void EEMEngine::waitForMusicDone(uint32 maxMs) {
@@ -1180,7 +1180,7 @@ Common::Error EEMEngine::saveProfile(const Common::String &name) {
_playerName = name;
debugC(1, kDebugGeneral, "saveProfile(%s) -> slot %d",
name.c_str(), slot);
- return saveGameState(slot, name, /*isAutosave=*/false);
+ return saveGameState(slot, name, /* isAutosave= */ false);
}
bool EEMEngine::loadProfile(const Common::String &name) {
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 2d3973a4d97..5935445df7a 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -154,7 +154,7 @@ public:
DBDArchive &getPics() { return _picsArchive; }
DBDArchive &getAni() { return _aniArchive; }
DBDArchive &getSites() { return _sitesArchive; }
- DBDArchive &getBalloons(){ return _balloonArchive; }
+ DBDArchive &getBalloons() { return _balloonArchive; }
DBDArchive &getButtons() { return _buttonArchive; }
Mystery &getMystery() { return _mystery; }
const EEMFont &getFont() const { return _font; }
diff --git a/engines/eem/graphics.cpp b/engines/eem/graphics.cpp
index c11505fa1dd..8251aee33f7 100644
--- a/engines/eem/graphics.cpp
+++ b/engines/eem/graphics.cpp
@@ -198,12 +198,16 @@ void EEMEngine::doHelp() {
const char *str2 = nullptr;
const char *str3 = nullptr;
const char *p = str1;
- while ((uint)((const byte *)p - hd) < hsz && *p != 0) p++;
- if ((uint)((const byte *)p - hd) >= hsz) return;
+ while ((uint)((const byte *)p - hd) < hsz && *p != 0)
+ p++;
+ if ((uint)((const byte *)p - hd) >= hsz)
+ return;
str2 = p + 1;
p = str2;
- while ((uint)((const byte *)p - hd) < hsz && *p != 0) p++;
- if ((uint)((const byte *)p - hd) >= hsz) return;
+ while ((uint)((const byte *)p - hd) < hsz && *p != 0)
+ p++;
+ if ((uint)((const byte *)p - hd) >= hsz)
+ return;
str3 = p + 1;
const char *chosen = nullptr;
diff --git a/engines/eem/music.h b/engines/eem/music.h
index 59dac57e30d..11001a4280a 100644
--- a/engines/eem/music.h
+++ b/engines/eem/music.h
@@ -60,8 +60,8 @@ public:
/// floppy: TRAVEL-N.XMI / FANFARE2.XMI.
void playMus(uint num, bool loop = false);
- // Miles drivers handle source-channel routing themselves; bypass
- // Audio::MidiPlayer::sendToChannel. Same workaround as Toltecs / SAGA.
+ // WORKAROUND: Miles drivers handle source-channel routing themselves;
+ // bypass Audio::MidiPlayer::sendToChannel. Same as Toltecs / SAGA.
void send(uint32 b) override;
private:
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index f8c888d1843..21a8881c7cc 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -1455,11 +1455,17 @@ void EEMEngine::doSetup() {
continue;
}
if (kKid1Rect.contains(mx, my)) {
- if (_partner != 0) { _partner = 0; dirty = true; }
+ if (_partner != 0) {
+ _partner = 0;
+ dirty = true;
+ }
continue;
}
if (kKid2Rect.contains(mx, my)) {
- if (_partner != 1) { _partner = 1; dirty = true; }
+ if (_partner != 1) {
+ _partner = 1;
+ dirty = true;
+ }
continue;
}
@@ -1532,7 +1538,7 @@ void EEMEngine::doSetup() {
// Restore BG between cards (1560:02e5 `_vga_fvidvid(0)`).
draw();
const Common::KeyCode k =
- showFullscreenPic(kHelp1Pics[i], /*transparent=*/true);
+ showFullscreenPic(kHelp1Pics[i], /* transparent= */ true);
if (k == Common::KEYCODE_ESCAPE)
break;
}
@@ -1544,7 +1550,7 @@ void EEMEngine::doSetup() {
// Credits [11] @ 1f78:025a â PIC 0x208 fullscreen.
if (kCreditsBtn.contains(mx, my)) {
CursorMan.showMouse(false);
- showFullscreenPic(0x208, /*transparent=*/false);
+ showFullscreenPic(0x208, /* transparent= */ false);
CursorMan.showMouse(true);
// PIC 0x208 has its own baked palette; restore site 0.
setSitePalette(0);
@@ -1620,14 +1626,26 @@ void EEMEngine::doActionScreen() {
// stage 3: grey Practice; SB3 needs tier-3 solve
// stage 4: grey Choose + Practice
bool anySolved1 = false;
- for (uint i = 1; i <= 0x18 && i < sizeof(_mysteriesSolved); i++)
- if (_mysteriesSolved[i]) { anySolved1 = true; break; }
+ for (uint i = 1; i <= 0x18 && i < sizeof(_mysteriesSolved); i++) {
+ if (_mysteriesSolved[i]) {
+ anySolved1 = true;
+ break;
+ }
+ }
bool anySolved2 = false;
- for (uint i = 0x19; i <= 0x30 && i < sizeof(_mysteriesSolved); i++)
- if (_mysteriesSolved[i]) { anySolved2 = true; break; }
+ for (uint i = 0x19; i <= 0x30 && i < sizeof(_mysteriesSolved); i++) {
+ if (_mysteriesSolved[i]) {
+ anySolved2 = true;
+ break;
+ }
+ }
bool anySolved3 = false;
- for (uint i = 0x31; i <= 0x36 && i < sizeof(_mysteriesSolved); i++)
- if (_mysteriesSolved[i]) { anySolved3 = true; break; }
+ for (uint i = 0x31; i <= 0x36 && i < sizeof(_mysteriesSolved); i++) {
+ if (_mysteriesSolved[i]) {
+ anySolved3 = true;
+ break;
+ }
+ }
const bool chooseOn = _chainStage < 4;
const bool practiceOn = _chainStage <= 1;
@@ -1643,7 +1661,10 @@ void EEMEngine::doActionScreen() {
// Seed selection on first enabled entry.
uint pick = 0;
for (uint i = 0; i < kNumPicks; i++) {
- if (kPickEnabled[i]) { pick = i; break; }
+ if (kPickEnabled[i]) {
+ pick = i;
+ break;
+ }
}
const char *kSeparator = "----------------------------------";
@@ -1838,9 +1859,18 @@ void EEMEngine::doCaseSelection() {
uint stageLo = 1, stageHi = 0x18;
uint book = 1;
switch (_chainStage) {
- case 2: stageLo = 0x19; stageHi = 0x30; book = 2; break;
- case 3: stageLo = 0x31; stageHi = 0x36; book = 3; break;
- default: break;
+ case 2:
+ stageLo = 0x19;
+ stageHi = 0x30;
+ book = 2;
+ break;
+ case 3:
+ stageLo = 0x31;
+ stageHi = 0x36;
+ book = 3;
+ break;
+ default:
+ break;
}
if (stageHi > kMaxMystery)
stageHi = kMaxMystery;
@@ -1924,7 +1954,10 @@ void EEMEngine::doCaseSelection() {
return;
}
if (kChooserUpArrowRect.contains(ev.mouse.x, ev.mouse.y)) {
- if (topRow > 0) { topRow--; dirty = true; }
+ if (topRow > 0) {
+ topRow--;
+ dirty = true;
+ }
continue;
}
if (kChooserDnArrowRect.contains(ev.mouse.x, ev.mouse.y)) {
@@ -3204,20 +3237,21 @@ void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
: READ_LE_UINT16(entry + 0x6);
const uint16 crime = floppy ? (uint16)entry[0xa]
: READ_LE_UINT16(entry + 0xc);
- const bool done_ = (i < Mystery::kVisitedSiteCap)
- && _mystery._visitedSite[i];
+ const bool isDone = (i < Mystery::kVisitedSiteCap)
+ && _mystery._visitedSite[i];
const Picture *m = nullptr;
bool useVisitedColors = false;
- if (done_ && haveDone)
+ if (isDone && haveDone) {
m = &done;
- else if (done_ && haveNormal) {
+ } else if (isDone && haveNormal) {
m = &normal;
useVisitedColors = true;
- } else if (crime != 0 && haveCrime)
+ } else if (crime != 0 && haveCrime) {
m = &crimeM;
- else if (haveNormal)
+ } else if (haveNormal) {
m = &normal;
+ }
if (m) {
blitBigMapMarker(scratch, *m, (int)mx, (int)my,
@@ -3886,7 +3920,7 @@ void EEMEngine::doAccuse() {
balloonY = (0x50 - balloon.surface.h) / 2;
// Render gallery first so the snapshot includes partner.
- drawAccuseGallery(num, gd, /*highlighted=*/-1,
+ drawAccuseGallery(num, gd, /* highlighted= */ -1,
slotRects, slotSuspect);
Graphics::ManagedSurface ms(320, 200,
@@ -4147,7 +4181,7 @@ void EEMEngine::doAccuse() {
// MIDI 6 â blocks until done (or click/ESC aborts).
if (_music && _voiceOn) {
- _music->playMus(6, /*loop=*/false);
+ _music->playMus(6, /* loop= */ false);
const uint32 musStart = g_system->getMillis();
bool aborted = false;
while (_music->isPlaying() && !shouldQuit() && !aborted) {
@@ -4265,12 +4299,23 @@ void EEMEngine::doAccuse() {
// Chain advance @ 1df2:0824-0850. Skip mystery 0 (practice).
// stage 1: 1..0x18, stage 2: 0x19..0x30, stage 3: 0x31..0x36.
if (mn != 0) {
- uint lo = 0, hi = 0;
+ uint lo = 0;
+ uint hi = 0;
switch (_chainStage) {
- case 1: lo = 1; hi = 0x18; break;
- case 2: lo = 0x19; hi = 0x30; break;
- case 3: lo = 0x31; hi = 0x36; break;
- default: break;
+ case 1:
+ lo = 1;
+ hi = 0x18;
+ break;
+ case 2:
+ lo = 0x19;
+ hi = 0x30;
+ break;
+ case 3:
+ lo = 0x31;
+ hi = 0x36;
+ break;
+ default:
+ break;
}
bool allSolved = (hi >= lo);
for (uint i = lo; i <= hi && allSolved; i++) {
@@ -4332,7 +4377,7 @@ void EEMEngine::doAccuse() {
g_system->updateScreen();
if (_music && _voiceOn)
- _music->playMus(5, /*loop=*/false);
+ _music->playMus(5, /* loop= */ false);
// Chain recap â partner enumerates required clues.
const byte *solved = _mystery.solvedClueBlock();
@@ -4718,7 +4763,7 @@ void EEMEngine::doAccuseFloppy() {
if (tail) {
displayFloppyDialogRecords(records, beforeScrapbook, 1);
playAnm(Common::Path("SCRAPBK.ANI"), 120,
- /*holdLastFrame=*/false, /*fadeIn=*/true);
+ /* holdLastFrame= */ false, /* fadeIn= */ true);
displayFloppyDialogRecords(tail, 3, 1);
} else {
warning("doAccuseFloppy: malformed solved chain");
@@ -4737,12 +4782,23 @@ void EEMEngine::doAccuseFloppy() {
// Tier promotion @ 1d40:0941..0978. Skip mystery 0 (practice).
if (mn != 0) {
- uint lo = 0, hi = 0;
+ uint lo = 0;
+ uint hi = 0;
switch (_chainStage) {
- case 1: lo = 1; hi = 0x18; break;
- case 2: lo = 0x19; hi = 0x30; break;
- case 3: lo = 0x31; hi = 0x36; break;
- default: break;
+ case 1:
+ lo = 1;
+ hi = 0x18;
+ break;
+ case 2:
+ lo = 0x19;
+ hi = 0x30;
+ break;
+ case 3:
+ lo = 0x31;
+ hi = 0x36;
+ break;
+ default:
+ break;
}
bool allSolved = (hi >= lo);
for (uint i = lo; i <= hi && allSolved; i++) {
@@ -4857,8 +4913,10 @@ void EEMEngine::doAccuseFloppy() {
if (haveBalloon) {
balloonX = (320 - balloon.surface.w) / 2;
balloonY = (0x5a - balloon.surface.h) / 2;
- if (balloonX < 0) balloonX = 0;
- if (balloonY < 0) balloonY = 0;
+ if (balloonX < 0)
+ balloonX = 0;
+ if (balloonY < 0)
+ balloonY = 0;
const byte transp = (byte)(balloon.flags >> 8);
// Mirror flag (high bit) flips balloon horizontally.
scene.transBlitFrom(balloon.surface,
Commit: c3c78f3e10f03d724f584c6027e75f66eed3e735
https://github.com/scummvm/scummvm/commit/c3c78f3e10f03d724f584c6027e75f66eed3e735
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:10+02:00
Commit Message:
EEM: removed lambdas in favor of explicit functions
Changed paths:
engines/eem/clues.cpp
engines/eem/eem.h
engines/eem/site.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index cedbf6d1319..39aca26fb17 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -859,6 +859,40 @@ void EEMEngine::displayClue(const byte *clueBlock) {
interruptAudio(/* stopMusicToo= */ false);
}
+bool EEMEngine::floppyDialogWaitForClick() {
+ // Drain pending events so a stale keystroke doesn't auto-advance.
+ Common::Event drain;
+ while (g_system->getEventManager()->pollEvent(drain)) {}
+ setInteractiveMouseCursor(false);
+ const uint32 minVisibleMs = 250;
+ const uint32 startedAt = g_system->getMillis();
+ while (!shouldQuit()) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return true; // skip
+ if (ev.type == Common::EVENT_MOUSEMOVE) {
+ setInteractiveMouseCursor(false);
+ continue;
+ }
+ if (g_system->getMillis() - startedAt < minVisibleMs)
+ continue;
+ if (ev.type == Common::EVENT_KEYDOWN &&
+ ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ interruptAudio(/* stopMusicToo= */ false);
+ return true; // skip
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN ||
+ ev.type == Common::EVENT_KEYDOWN)
+ return false; // advance one page
+ }
+ g_system->updateScreen();
+ g_system->delayMillis(10);
+ }
+ return true;
+}
+
void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
uint lastIndicator) {
// Render `count` consecutive floppy dialog records starting at
@@ -913,40 +947,6 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
}
}
- auto waitForClick = [&]() -> bool {
- // Drain pending events so a stale keystroke doesn't auto-advance.
- Common::Event drain;
- while (g_system->getEventManager()->pollEvent(drain)) {}
- setInteractiveMouseCursor(false);
- const uint32 minVisibleMs = 250;
- const uint32 startedAt = g_system->getMillis();
- while (!shouldQuit()) {
- Common::Event ev;
- while (g_system->getEventManager()->pollEvent(ev)) {
- if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
- return true; // skip
- if (ev.type == Common::EVENT_MOUSEMOVE) {
- setInteractiveMouseCursor(false);
- continue;
- }
- if (g_system->getMillis() - startedAt < minVisibleMs)
- continue;
- if (ev.type == Common::EVENT_KEYDOWN &&
- ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
- interruptAudio(/* stopMusicToo= */ false);
- return true; // skip
- }
- if (ev.type == Common::EVENT_LBUTTONDOWN ||
- ev.type == Common::EVENT_KEYDOWN)
- return false; // advance one page
- }
- g_system->updateScreen();
- g_system->delayMillis(10);
- }
- return true;
- };
-
for (uint i = 0; i < count && !shouldQuit(); i++) {
const uint16 picID = READ_LE_UINT16(rec + 0);
const uint16 picX = READ_LE_UINT16(rec + 2);
@@ -1158,7 +1158,7 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
g_system->updateScreen();
if (waitNeeded) {
- if (waitForClick()) {
+ if (floppyDialogWaitForClick()) {
skipAll = true;
break;
}
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 5935445df7a..6d83cdc4da9 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -25,6 +25,7 @@
#define EEM_EEM_H
#include "common/array.h"
+#include "common/keyboard.h"
#include "common/language.h"
#include "common/platform.h"
#include "common/random.h"
@@ -268,6 +269,52 @@ private:
/// Re-render helpers for the corresponding `doX()` modal screens.
void drawNotebookFrame(int &page);
+
+ /// Resolve a single NoteIndex entry to displayable notebook text.
+ /// Handles the CD (4-byte) vs floppy (7-byte) entry strides.
+ Common::String notebookNoteText(uint clueId, const byte *ni,
+ uint16 niCount, bool floppyNb,
+ const byte *bufBase,
+ uint32 mysSz) const;
+
+ /// Shared state for `doAccuseNotes` helpers (text lookup, pagination,
+ /// render). Pointer fields refer to locals owned by `doAccuseNotes`.
+ struct AccuseNotesCtx {
+ const byte *ni;
+ uint16 niCount;
+ bool floppyNote;
+ const byte *bufBaseNotes;
+ const Common::Array<uint> *found;
+ int rectX, rectY, rectW, rectH;
+ uint expected;
+ bool haveBg;
+ const Picture *accuseBg;
+ Common::Array<Common::Rect> *slotRects;
+ Common::Array<uint> *slotClues;
+ int *pageBreaks;
+ int pageBreaksCap;
+ int *numPages;
+ int *page;
+ };
+
+ /// One NoteIndex entry as displayable accuse-screen text.
+ Common::String accuseNoteText(uint clueId, const AccuseNotesCtx &ctx) const;
+
+ /// Recompute `pageBreaks`/`numPages` after `_noteSelected` or font changes.
+ void accuseRebuildPagination(const AccuseNotesCtx &ctx);
+
+ /// Render one frame of the accuse-notes screen.
+ void accuseDrawScreen(const AccuseNotesCtx &ctx);
+
+ /// Floppy accuse helpers: KD-balloon hint (`_DisplayHint_Floppy @
+ /// 1503:00ca`) and the suspect portrait grid
+ /// (`_DrawGallery_Floppy @ 154e:0050`).
+ void floppyKDHint(uint kdSlot, const byte *kdIdx,
+ const byte *bufBase, uint32 mysSize);
+ void accuseDrawGallery(int highlighted,
+ Common::Array<Common::Rect> &rects,
+ Common::Array<int> &suspects, uint8 num,
+ bool haveAccuseBg, const Picture &accuseBg);
void drawGalleryFrame(const byte *gd, uint8 numSuspects,
Common::Array<Common::Rect> &slotRects,
Common::Array<int> &slotSuspect);
@@ -366,6 +413,12 @@ private:
/// pick via SwapColors on Kid1/Kid2 rects.
void doSetup();
+ /// `doSetup` helpers: redraw BG + label highlights, render a help/
+ /// credits card with blocking input wait, and the shared exit fallback.
+ void setupDrawScreen();
+ Common::KeyCode setupShowFullscreenPic(uint16 picId, bool transparent);
+ void setupLeave();
+
/// `_DoInitClues @ 1a35:0411` (minus live ANI sequence playback).
void doInitClues();
@@ -383,6 +436,11 @@ private:
void displayFloppyDialogRecords(const byte *rec, uint count,
uint lastIndicator = 0);
+ /// Wait for a click/keypress to advance a floppy dialog page.
+ /// Returns true if the user requested to skip the rest (ESC/QUIT),
+ /// false to advance one page.
+ bool floppyDialogWaitForClick();
+
public:
/// `_StartTravelMusic @ 20a2:0595`. Picks `MUS%05d.XMI` from
/// `_mystery._siteNumber % 5`, one-shot.
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 167f9d0d1cd..47bd881a88d 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -1423,6 +1423,11 @@ void SiteScreen::renderBackground(uint siteNum) {
}
}
+void bumpHotspotEdgeColor(byte &color) {
+ const byte next = (byte)(color + 1);
+ color = (next > 0xFE) ? (byte)0xF9 : next;
+}
+
void SiteScreen::renderHotspots(uint siteNum) {
// `_DrawSearchButtons`. Port adds optional "hide hint" setting.
if (ConfMan.getBool("hide_highlight_boxes"))
@@ -1484,33 +1489,29 @@ void SiteScreen::renderHotspots(uint siteNum) {
// the colour per pixel through palette indices 0xF9..0xFE,
// which `_ColorCycle(0xF9, 0xFE)` rotates every tick.
byte color = (byte)(0xF9 + ((i + (tickMs / 80)) & 0x07) % 6);
- auto bumpColor = [&]() {
- const byte next = (byte)(color + 1);
- color = (next > 0xFE) ? (byte)0xF9 : next;
- };
// Top edge
for (int x = rect.left; x < rect.right; x++) {
if (x >= 0 && x < screen->w && rect.top >= 0 && rect.top < screen->h)
*(byte *)screen->getBasePtr(x, rect.top) = color;
- bumpColor();
+ bumpHotspotEdgeColor(color);
}
// Right edge
for (int y = rect.top; y < rect.bottom; y++) {
if (rect.right - 1 >= 0 && rect.right - 1 < screen->w && y >= 0 && y < screen->h)
*(byte *)screen->getBasePtr(rect.right - 1, y) = color;
- bumpColor();
+ bumpHotspotEdgeColor(color);
}
// Bottom edge
for (int x = rect.right - 1; x >= rect.left; x--) {
if (x >= 0 && x < screen->w && rect.bottom - 1 >= 0 && rect.bottom - 1 < screen->h)
*(byte *)screen->getBasePtr(x, rect.bottom - 1) = color;
- bumpColor();
+ bumpHotspotEdgeColor(color);
}
// Left edge
for (int y = rect.bottom - 1; y >= rect.top; y--) {
if (rect.left >= 0 && rect.left < screen->w && y >= 0 && y < screen->h)
*(byte *)screen->getBasePtr(rect.left, y) = color;
- bumpColor();
+ bumpHotspotEdgeColor(color);
}
}
}
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 21a8881c7cc..6e5c9124516 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -86,6 +86,31 @@ void blitBigMapMarker(Graphics::ManagedSurface &dstSurface, const Picture &marke
}
}
+// Setup-screen highlight rects (referenced by both `doSetup` and the
+// helper `EEMEngine::setupDrawScreen`). `_SetupHighlights @ 29be:1320`.
+constexpr Common::Rect kSetupKid1Rect (Common::Point( 99, 44), 49, 8);
+constexpr Common::Rect kSetupKid2Rect (Common::Point( 99, 54), 49, 8);
+constexpr Common::Rect kSetupSoundOnRect (Common::Point(106, 86), 19, 8);
+constexpr Common::Rect kSetupSoundOffRect(Common::Point(106, 96), 19, 8);
+
+// `_SwapColors @ 172b:1d2a` â replace pixels in r where value==from
+// with to. 0xFE = BG text-key; 0x15 = active palette index, 0x00 =
+// inactive (set by `_SetupSettings @ 1f78:000d`).
+void swapColors(Graphics::ManagedSurface &dst,
+ const Common::Rect &r, byte from, byte to) {
+ const int x1 = MAX<int>(0, r.left);
+ const int y1 = MAX<int>(0, r.top);
+ const int x2 = MIN<int>(320, r.right);
+ const int y2 = MIN<int>(200, r.bottom);
+ for (int y = y1; y < y2; y++) {
+ byte *row = (byte *)dst.getBasePtr(0, y);
+ for (int x = x1; x < x2; x++) {
+ if (row[x] == from)
+ row[x] = to;
+ }
+ }
+}
+
void blitAccusePartner(Graphics::ManagedSurface &dstSurface,
DBDArchive &aniArchive, uint8 partner,
uint32 tickMs) {
@@ -1318,114 +1343,15 @@ void EEMEngine::doSetup() {
const Common::Rect kHelpBtn (145, 163, 174, 187); // [9]
const Common::Rect kQuitBtn (212, 153, 266, 184); // [10]
const Common::Rect kCreditsBtn ( 81, 25, 238, 37); // [11]
- // Highlight / fallback-click rects.
- const Common::Rect kKid1Rect ( 99, 44, 148, 52);
- const Common::Rect kKid2Rect ( 99, 54, 148, 62);
- const Common::Rect kSoundOnRect (106, 86, 125, 94);
- const Common::Rect kSoundOffRect (106, 96, 125, 104);
-
- // `_SwapColors @ 172b:1d2a` â replace pixels in r where value==from
- // with to. 0xFE = BG text-key; 0x15 = active palette index, 0x00 =
- // inactive (set by `_SetupSettings @ 1f78:000d`).
- auto swapColors = [](Graphics::ManagedSurface &dst,
- const Common::Rect &r, byte from, byte to) {
- const int x1 = MAX<int>(0, r.left);
- const int y1 = MAX<int>(0, r.top);
- const int x2 = MIN<int>(320, r.right);
- const int y2 = MIN<int>(200, r.bottom);
- for (int y = y1; y < y2; y++) {
- byte *row = (byte *)dst.getBasePtr(0, y);
- for (int x = x1; x < x2; x++) {
- if (row[x] == from)
- row[x] = to;
- }
- }
- };
+ // Highlight / fallback-click rects (file-scope copies in `kSetup*Rect`).
+ const Common::Rect &kKid1Rect = kSetupKid1Rect;
+ const Common::Rect &kKid2Rect = kSetupKid2Rect;
+ const Common::Rect &kSoundOnRect = kSetupSoundOnRect;
+ const Common::Rect &kSoundOffRect = kSetupSoundOffRect;
- auto draw = [&]() {
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
- Picture bg;
- if (_picsArchive.getPicture(0x40, bg))
- scratch.simpleBlitFrom(bg.surface);
-
- const byte kKey = 0xFE;
- const byte kBright = 0x15;
- const byte kDim = 0x00;
- swapColors(scratch, kKid1Rect, kKey,
- _partner == 0 ? kBright : kDim);
- swapColors(scratch, kKid2Rect, kKey,
- _partner == 1 ? kBright : kDim);
- swapColors(scratch, kSoundOnRect, kKey,
- _voiceOn ? kBright : kDim);
- swapColors(scratch, kSoundOffRect, kKey,
- _voiceOn ? kDim : kBright);
+ setupDrawScreen();
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
- };
- draw();
-
- // Render picId and block until input. transparent=true: overlay via
- // `_Rect_Move_Mask` (`_InterfaceHelp @ 1560:0205`). false: raw fullscreen
- // blit via `_vga_fbuffvid` (credits @ 1f78:0281).
- auto showFullscreenPic = [&](uint16 picId,
- bool transparent) -> Common::KeyCode {
- Picture pic;
- if (!_picsArchive.getPicture(picId, pic)) {
- warning("doSetup: PIC %u missing", (uint)picId);
- return Common::KEYCODE_INVALID;
- }
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- if (transparent) {
- Graphics::Surface *cur = g_system->lockScreen();
- if (cur) {
- scratch.simpleBlitFrom(*cur);
- g_system->unlockScreen();
- }
- const byte transp = (byte)(pic.flags >> 8);
- // Explicit destPos â no-destPos overload stretches to dst.
- scratch.transBlitFrom(pic.surface, Common::Point(0, 0),
- (uint32)transp);
- } else {
- scratch.clear();
- scratch.simpleBlitFrom(pic.surface);
- }
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
- while (!shouldQuit()) {
- Common::Event ev;
- while (g_system->getEventManager()->pollEvent(ev)) {
- if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
- return Common::KEYCODE_ESCAPE;
- if (ev.type == Common::EVENT_KEYDOWN)
- return ev.kbd.keycode;
- if (ev.type == Common::EVENT_LBUTTONDOWN)
- return Common::KEYCODE_INVALID;
- }
- g_system->delayMillis(15);
- }
- return Common::KEYCODE_ESCAPE;
- };
-
- auto leaveSetup = [&]() {
- // `_DoSetup` entry: _NextScreen = _LastScreen. Fall back to it
- // unless a handler already overrode _nextScreen.
- if (_nextScreen == kScreenSetup) {
- _nextScreen = (ScreenId)_lastScreen;
- if (_nextScreen == kScreenSetup ||
- _nextScreen == kScreenInvalid)
- _nextScreen = kScreenMap;
- }
- saveProfile(_playerName);
- };
-
- _nextScreen = kScreenSetup; // sentinel â leaveSetup picks target
+ _nextScreen = kScreenSetup; // sentinel â setupLeave picks target
while (!shouldQuit()) {
Common::Event ev;
bool dirty = false;
@@ -1438,7 +1364,7 @@ void EEMEngine::doSetup() {
if (ev.type == Common::EVENT_KEYDOWN) {
if (ev.kbd.keycode == Common::KEYCODE_ESCAPE ||
ev.kbd.keycode == Common::KEYCODE_RETURN) {
- leaveSetup();
+ setupLeave();
return;
}
}
@@ -1511,7 +1437,7 @@ void EEMEngine::doSetup() {
// Done [8] â MOV SI,1; JMP exit (NextScreen stays = LastScreen).
if (kDoneBtn.contains(mx, my)) {
- leaveSetup();
+ setupLeave();
return;
}
@@ -1536,9 +1462,9 @@ void EEMEngine::doSetup() {
CursorMan.showMouse(false);
for (uint i = 0; i < ARRAYSIZE(kHelp1Pics); i++) {
// Restore BG between cards (1560:02e5 `_vga_fvidvid(0)`).
- draw();
+ setupDrawScreen();
const Common::KeyCode k =
- showFullscreenPic(kHelp1Pics[i], /* transparent= */ true);
+ setupShowFullscreenPic(kHelp1Pics[i], /* transparent= */ true);
if (k == Common::KEYCODE_ESCAPE)
break;
}
@@ -1550,7 +1476,7 @@ void EEMEngine::doSetup() {
// Credits [11] @ 1f78:025a â PIC 0x208 fullscreen.
if (kCreditsBtn.contains(mx, my)) {
CursorMan.showMouse(false);
- showFullscreenPic(0x208, /* transparent= */ false);
+ setupShowFullscreenPic(0x208, /* transparent= */ false);
CursorMan.showMouse(true);
// PIC 0x208 has its own baked palette; restore site 0.
setSitePalette(0);
@@ -1586,12 +1512,93 @@ void EEMEngine::doSetup() {
}
}
if (dirty)
- draw();
+ setupDrawScreen();
g_system->updateScreen();
g_system->delayMillis(15);
}
}
+void EEMEngine::setupDrawScreen() {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ Picture bg;
+ if (_picsArchive.getPicture(0x40, bg))
+ scratch.simpleBlitFrom(bg.surface);
+
+ const byte kKey = 0xFE;
+ const byte kBright = 0x15;
+ const byte kDim = 0x00;
+ swapColors(scratch, kSetupKid1Rect, kKey,
+ _partner == 0 ? kBright : kDim);
+ swapColors(scratch, kSetupKid2Rect, kKey,
+ _partner == 1 ? kBright : kDim);
+ swapColors(scratch, kSetupSoundOnRect, kKey,
+ _voiceOn ? kBright : kDim);
+ swapColors(scratch, kSetupSoundOffRect, kKey,
+ _voiceOn ? kDim : kBright);
+
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+}
+
+// Render picId and block until input. transparent=true: overlay via
+// `_Rect_Move_Mask` (`_InterfaceHelp @ 1560:0205`). false: raw fullscreen
+// blit via `_vga_fbuffvid` (credits @ 1f78:0281).
+Common::KeyCode EEMEngine::setupShowFullscreenPic(uint16 picId, bool transparent) {
+ Picture pic;
+ if (!_picsArchive.getPicture(picId, pic)) {
+ warning("doSetup: PIC %u missing", (uint)picId);
+ return Common::KEYCODE_INVALID;
+ }
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ if (transparent) {
+ Graphics::Surface *cur = g_system->lockScreen();
+ if (cur) {
+ scratch.simpleBlitFrom(*cur);
+ g_system->unlockScreen();
+ }
+ const byte transp = (byte)(pic.flags >> 8);
+ // Explicit destPos â no-destPos overload stretches to dst.
+ scratch.transBlitFrom(pic.surface, Common::Point(0, 0),
+ (uint32)transp);
+ } else {
+ scratch.clear();
+ scratch.simpleBlitFrom(pic.surface);
+ }
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+ while (!shouldQuit()) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return Common::KEYCODE_ESCAPE;
+ if (ev.type == Common::EVENT_KEYDOWN)
+ return ev.kbd.keycode;
+ if (ev.type == Common::EVENT_LBUTTONDOWN)
+ return Common::KEYCODE_INVALID;
+ }
+ g_system->delayMillis(15);
+ }
+ return Common::KEYCODE_ESCAPE;
+}
+
+void EEMEngine::setupLeave() {
+ // `_DoSetup` entry: _NextScreen = _LastScreen. Fall back to it
+ // unless a handler already overrode _nextScreen.
+ if (_nextScreen == kScreenSetup) {
+ _nextScreen = (ScreenId)_lastScreen;
+ if (_nextScreen == kScreenSetup ||
+ _nextScreen == kScreenInvalid)
+ _nextScreen = kScreenMap;
+ }
+ saveProfile(_playerName);
+}
+
void EEMEngine::doActionScreen() {
// `_ActionScreen @ 1c33:195b` â BG PIC 0x104 + PIC 9 @ (10, 0x87),
// `_DoChoose` with ActionNames @ 29be:0d6a. 5 picks alternating with
@@ -2232,6 +2239,28 @@ void EEMEngine::doNotebook() {
setInteractiveMouseCursor(false);
}
+Common::String EEMEngine::notebookNoteText(uint clueId, const byte *ni,
+ uint16 niCount, bool floppyNb,
+ const byte *bufBase,
+ uint32 mysSz) const {
+ if (!ni || clueId >= niCount)
+ return Common::String();
+ if (floppyNb && bufBase) {
+ const uint16 textOff = READ_LE_UINT16(ni + clueId * 7);
+ if (textOff == 0 || textOff >= mysSz)
+ return Common::String();
+ const char *p = (const char *)(bufBase + textOff);
+ uint32 len = 0;
+ while (textOff + len < mysSz && p[len] != 0)
+ len++;
+ return parseString(Common::String(p, len),
+ _playerName, _partner);
+ }
+ const uint16 textOff = READ_LE_UINT16(ni + clueId * 4);
+ return parseString(_mystery.textAt(textOff),
+ _playerName, _partner);
+}
+
void EEMEngine::drawNotebookFrame(int &page) {
// `_DrawNotes @ 161e:01d0` per-page layout + partner sprite at (5,80)
// from `_DoNotebook @ 161e:0500`.
@@ -2282,30 +2311,13 @@ void EEMEngine::drawNotebookFrame(int &page) {
const bool floppyNb = isFloppy();
const byte *bufBase = _mystery.blobAt(0);
const uint32 mysSz = _mystery.dataSize();
- auto noteText = [&](uint clueId) -> Common::String {
- if (!ni || clueId >= niCount)
- return Common::String();
- if (floppyNb && bufBase) {
- const uint16 textOff = READ_LE_UINT16(ni + clueId * 7);
- if (textOff == 0 || textOff >= mysSz)
- return Common::String();
- const char *p = (const char *)(bufBase + textOff);
- uint32 len = 0;
- while (textOff + len < mysSz && p[len] != 0)
- len++;
- return parseString(Common::String(p, len),
- _playerName, _partner);
- }
- const uint16 textOff = READ_LE_UINT16(ni + clueId * 4);
- return parseString(_mystery.textAt(textOff),
- _playerName, _partner);
- };
{
const int lineH = _font.getFontHeight();
int y = kRectY;
while (clueCursor < (int)found.size()) {
const uint clueId = found[clueCursor];
- Common::String txt = noteText(clueId);
+ Common::String txt = notebookNoteText(clueId, ni, niCount,
+ floppyNb, bufBase, mysSz);
// Measure height by wrapping the text without drawing.
Common::Array<Common::String> wrapped;
_font.wordWrapText(txt, kRectW, wrapped);
@@ -2336,7 +2348,8 @@ void EEMEngine::drawNotebookFrame(int &page) {
int y = kRectY;
for (int i = startClue; i < endClue; i++) {
const uint clueId = found[i];
- Common::String txt = noteText(clueId);
+ Common::String txt = notebookNoteText(clueId, ni, niCount,
+ floppyNb, bufBase, mysSz);
if (txt.empty())
txt = Common::String::format(
isSpanish() ? "nota %u" : "clue %u", clueId);
@@ -3378,6 +3391,132 @@ uint16 EEMEngine::getKDTextBalloon(byte firstChar) const {
return kDigitBalloons[firstChar - '0'];
}
+Common::String EEMEngine::accuseNoteText(uint clueId,
+ const AccuseNotesCtx &ctx) const {
+ if (ctx.floppyNote) {
+ const uint16 textOff = READ_LE_UINT16(ctx.ni + clueId * 7);
+ if (textOff == 0 || textOff >= _mystery.dataSize() ||
+ !ctx.bufBaseNotes)
+ return Common::String();
+ return parseString(
+ (const char *)(ctx.bufBaseNotes + textOff),
+ _playerName, _partner);
+ }
+ const uint16 textOff = READ_LE_UINT16(ctx.ni + clueId * 4);
+ return parseString(_mystery.textAt(textOff),
+ _playerName, _partner);
+}
+
+void EEMEngine::accuseRebuildPagination(const AccuseNotesCtx &ctx) {
+ *ctx.numPages = 1;
+ ctx.pageBreaks[0] = 0;
+ const int lineH = _font.getFontHeight();
+ int y = ctx.rectY;
+ const Common::Array<uint> &found = *ctx.found;
+ for (uint i = 0; i < found.size(); i++) {
+ const uint clueId = found[i];
+ Common::String txt;
+ if (clueId < ctx.niCount)
+ txt = accuseNoteText(clueId, ctx);
+ Common::Array<Common::String> wrapped;
+ _font.wordWrapText(txt, ctx.rectW, wrapped);
+ const int h = (int)wrapped.size() * lineH;
+ if (y + h + 7 > ctx.rectY + ctx.rectH) {
+ if (*ctx.numPages < ctx.pageBreaksCap) {
+ ctx.pageBreaks[(*ctx.numPages)++] = (int)i;
+ y = ctx.rectY;
+ }
+ }
+ y += h + 7;
+ }
+ if (*ctx.page >= *ctx.numPages)
+ *ctx.page = *ctx.numPages - 1;
+ if (*ctx.page < 0)
+ *ctx.page = 0;
+}
+
+void EEMEngine::accuseDrawScreen(const AccuseNotesCtx &ctx) {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ if (ctx.haveBg)
+ scratch.simpleBlitFrom(ctx.accuseBg->surface);
+
+ // Partner at (5, 0x50). `_DoAccuse @ 1df2:0c2c`: ANI 2/0x10,
+ // script 2, prior 1.
+ const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
+ Animation partnerAni;
+ if (_aniArchive.loadAnimation(partnerAnim, partnerAni) &&
+ !partnerAni.empty()) {
+ const uint32 now = g_system->getMillis();
+ const uint frameIdx = partnerFrameAtTick(0x02,
+ (uint)partnerAni.size(), now);
+ blitAnimFrameAnchored(scratch.surfacePtr(),
+ partnerAni[frameIdx], 5, 0x50);
+ }
+
+ // Selected=0x3c, unselected=1 (`_NoteUnselectedColor` @ 1df2:0c25).
+ Common::Array<Common::Rect> &slotRects = *ctx.slotRects;
+ Common::Array<uint> &slotClues = *ctx.slotClues;
+ const Common::Array<uint> &found = *ctx.found;
+ slotRects.clear();
+ slotClues.clear();
+ const int lineH = _font.getFontHeight();
+ const int startIdx = ctx.pageBreaks[*ctx.page];
+ const int endIdx = (*ctx.page + 1 < *ctx.numPages)
+ ? ctx.pageBreaks[*ctx.page + 1]
+ : (int)found.size();
+ int y = ctx.rectY;
+ uint selectedCount = 0;
+ for (uint i = 0; i < found.size(); i++) {
+ if (_mystery._noteSelected[found[i]])
+ selectedCount++;
+ }
+ for (int i = startIdx; i < endIdx; i++) {
+ const uint clueId = found[i];
+ Common::String txt;
+ if (clueId < ctx.niCount)
+ txt = accuseNoteText(clueId, ctx);
+ if (txt.empty())
+ txt = Common::String::format(
+ isSpanish() ? "nota %u" : "clue %u", clueId);
+ Common::Array<Common::String> wrapped;
+ _font.wordWrapText(txt, ctx.rectW, wrapped);
+ const int h = (int)wrapped.size() * lineH;
+ const byte color = _mystery._noteSelected[clueId] ? 0x3c : 0x01;
+ for (uint li = 0; li < wrapped.size(); li++) {
+ _font.drawString(&scratch, wrapped[li], ctx.rectX,
+ y + (int)li * lineH, ctx.rectW, color);
+ }
+ slotRects.push_back(Common::Rect(ctx.rectX, y,
+ ctx.rectX + ctx.rectW, y + h));
+ slotClues.push_back(clueId);
+ y += h + 7;
+ }
+
+ // `_UpdateSelectionCount(remaining)` @ (0xd1, 0xb).
+ const uint remaining = (selectedCount < ctx.expected)
+ ? ctx.expected - selectedCount
+ : 0;
+ // Spanish floppy uses "nota/notas".
+ const char *clueWord = isSpanish()
+ ? (remaining == 1 ? "nota" : "notas")
+ : (remaining == 1 ? "clue" : "clues");
+ const Common::String counter =
+ Common::String::format("%u %s", remaining, clueWord);
+ _font.drawString(&scratch, counter, 209, 11, 100, 0x0F);
+
+ if (*ctx.numPages > 1) {
+ _font.drawString(&scratch,
+ Common::String::format("p%d/%d", *ctx.page + 1, *ctx.numPages),
+ ctx.rectX, 11, 60, 0x0F);
+ }
+
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+}
+
bool EEMEngine::doAccuseNotes() {
// `_DoAccuse @ 1df2:0bdd` head. BG PIC 0x1A7. `_AccuseNoteRect @
// 29be:1048` = (79, 27, 304, 159). Counter @ (209, 11) shows
@@ -3434,129 +3573,29 @@ bool EEMEngine::doAccuseNotes() {
// Notebook always uses +0 (`FUN_15e0_01e8`).
const bool floppyNote = isFloppy();
const byte *bufBaseNotes = _mystery.blobAt(0);
- auto noteText = [&](uint clueId) -> Common::String {
- if (floppyNote) {
- const uint16 textOff = READ_LE_UINT16(ni + clueId * 7);
- if (textOff == 0 || textOff >= _mystery.dataSize() ||
- !bufBaseNotes)
- return Common::String();
- return parseString(
- (const char *)(bufBaseNotes + textOff),
- _playerName, _partner);
- }
- const uint16 textOff = READ_LE_UINT16(ni + clueId * 4);
- return parseString(_mystery.textAt(textOff),
- _playerName, _partner);
- };
-
- auto rebuildPagination = [&]() {
- numPages = 1;
- pageBreaks[0] = 0;
- const int lineH = _font.getFontHeight();
- int y = rectY;
- for (uint i = 0; i < found.size(); i++) {
- const uint clueId = found[i];
- Common::String txt;
- if (clueId < niCount)
- txt = noteText(clueId);
- Common::Array<Common::String> wrapped;
- _font.wordWrapText(txt, rectW, wrapped);
- const int h = (int)wrapped.size() * lineH;
- if (y + h + 7 > rectY + rectH) {
- if (numPages < (int)ARRAYSIZE(pageBreaks)) {
- pageBreaks[numPages++] = (int)i;
- y = rectY;
- }
- }
- y += h + 7;
- }
- if (page >= numPages)
- page = numPages - 1;
- if (page < 0)
- page = 0;
- };
-
- auto draw = [&]() {
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
- if (haveBg)
- scratch.simpleBlitFrom(accuseBg.surface);
-
- // Partner at (5, 0x50). `_DoAccuse @ 1df2:0c2c`: ANI 2/0x10,
- // script 2, prior 1.
- const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
- Animation partnerAni;
- if (_aniArchive.loadAnimation(partnerAnim, partnerAni) &&
- !partnerAni.empty()) {
- const uint32 now = g_system->getMillis();
- const uint frameIdx = partnerFrameAtTick(0x02,
- (uint)partnerAni.size(), now);
- blitAnimFrameAnchored(scratch.surfacePtr(),
- partnerAni[frameIdx], 5, 0x50);
- }
-
- // Selected=0x3c, unselected=1 (`_NoteUnselectedColor` @ 1df2:0c25).
- slotRects.clear();
- slotClues.clear();
- const int lineH = _font.getFontHeight();
- const int startIdx = pageBreaks[page];
- const int endIdx = (page + 1 < numPages)
- ? pageBreaks[page + 1]
- : (int)found.size();
- int y = rectY;
- uint selectedCount = 0;
- for (uint i = 0; i < found.size(); i++) {
- if (_mystery._noteSelected[found[i]])
- selectedCount++;
- }
- for (int i = startIdx; i < endIdx; i++) {
- const uint clueId = found[i];
- Common::String txt;
- if (clueId < niCount)
- txt = noteText(clueId);
- if (txt.empty())
- txt = Common::String::format(
- isSpanish() ? "nota %u" : "clue %u", clueId);
- Common::Array<Common::String> wrapped;
- _font.wordWrapText(txt, rectW, wrapped);
- const int h = (int)wrapped.size() * lineH;
- const byte color = _mystery._noteSelected[clueId] ? 0x3c : 0x01;
- for (uint li = 0; li < wrapped.size(); li++) {
- _font.drawString(&scratch, wrapped[li], rectX,
- y + (int)li * lineH, rectW, color);
- }
- slotRects.push_back(Common::Rect(rectX, y,
- rectX + rectW, y + h));
- slotClues.push_back(clueId);
- y += h + 7;
- }
-
- // `_UpdateSelectionCount(remaining)` @ (0xd1, 0xb).
- const uint remaining = (selectedCount < expected)
- ? expected - selectedCount
- : 0;
- // Spanish floppy uses "nota/notas".
- const char *clueWord = isSpanish()
- ? (remaining == 1 ? "nota" : "notas")
- : (remaining == 1 ? "clue" : "clues");
- const Common::String counter =
- Common::String::format("%u %s", remaining, clueWord);
- _font.drawString(&scratch, counter, 209, 11, 100, 0x0F);
-
- if (numPages > 1) {
- _font.drawString(&scratch,
- Common::String::format("p%d/%d", page + 1, numPages),
- rectX, 11, 60, 0x0F);
- }
-
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
- };
- rebuildPagination();
- draw();
+ AccuseNotesCtx ctx;
+ ctx.ni = ni;
+ ctx.niCount = niCount;
+ ctx.floppyNote = floppyNote;
+ ctx.bufBaseNotes = bufBaseNotes;
+ ctx.found = &found;
+ ctx.rectX = rectX;
+ ctx.rectY = rectY;
+ ctx.rectW = rectW;
+ ctx.rectH = rectH;
+ ctx.expected = expected;
+ ctx.haveBg = haveBg;
+ ctx.accuseBg = &accuseBg;
+ ctx.slotRects = &slotRects;
+ ctx.slotClues = &slotClues;
+ ctx.pageBreaks = pageBreaks;
+ ctx.pageBreaksCap = (int)ARRAYSIZE(pageBreaks);
+ ctx.numPages = &numPages;
+ ctx.page = &page;
+
+ accuseRebuildPagination(ctx);
+ accuseDrawScreen(ctx);
Common::Point mouse = g_system->getEventManager()->getMousePos();
setInteractiveMouseCursor(notebookButtonAt(mouse.x, mouse.y) ||
kPdaNotebookRect.contains(mouse.x, mouse.y) ||
@@ -3674,13 +3713,13 @@ bool EEMEngine::doAccuseNotes() {
}
}
if (dirty)
- draw();
+ accuseDrawScreen(ctx);
// 100 ms `_CheckFrameRate` cadence @ 1df2:0bfa.
static uint32 sLastTick = 0;
const uint32 now = g_system->getMillis();
if (now - sLastTick >= 100) {
sLastTick = now;
- draw();
+ accuseDrawScreen(ctx);
}
g_system->updateScreen();
g_system->delayMillis(15);
@@ -4405,6 +4444,148 @@ void EEMEngine::doAccuse() {
}
}
+// Render one KDTextIndex string as a centred KD balloon over the current
+// screen, mirroring `_DisplayHint_Floppy @ 1503:00ca` (= `FUN_1d40_11fd`'s
+// body). `kdSlot` is the index into KDTextIndex (0..N) â entries are u16
+// absolute text offsets.
+void EEMEngine::floppyKDHint(uint kdSlot, const byte *kdIdx,
+ const byte *bufBase, uint32 mysSize) {
+ if ((uint)(kdSlot * 2) + 2 > (uint)(mysSize - (kdIdx - bufBase)))
+ return;
+ const uint16 textOff = READ_LE_UINT16(kdIdx + kdSlot * 2);
+ if (textOff == 0 || textOff >= mysSize)
+ return;
+ const char *p = (const char *)(bufBase + textOff);
+ uint32 lineLen = 0;
+ while (textOff + lineLen < mysSize && p[lineLen] != 0)
+ lineLen++;
+ if (lineLen == 0)
+ return;
+ Common::String raw(p, lineLen);
+ // Digit â balloon variant (`_GetKDTextBalloon_Floppy @ 1d40:009f`
+ // + table @ 2608:0c14).
+ static const uint8 kDigitToBalloon[10] = {
+ 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1c, 0x1d, 0x1e, 0x0a
+ };
+ uint balloonIdx = 0x17;
+ const char *txt = raw.c_str();
+ if (*txt >= '0' && *txt <= '9') {
+ balloonIdx = kDigitToBalloon[(int)(*txt - '0')];
+ txt++;
+ }
+ Common::String text =
+ parseString(Common::String(txt), _playerName, _partner);
+ balloonIdx = fitBalloonToText((uint16)balloonIdx, text) & 0x7F;
+ Graphics::ManagedSurface ms(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ Graphics::Surface *cur = g_system->lockScreen();
+ if (cur) {
+ ms.simpleBlitFrom(*cur);
+ g_system->unlockScreen();
+ }
+ Picture balloon;
+ const bool haveBalloon = _balloonArchive.size() > balloonIdx &&
+ _balloonArchive.loadEntry(balloonIdx, balloon);
+ uint16 balloonY = 1;
+ if (haveBalloon) {
+ const uint h = (uint)balloon.surface.h;
+ if (h < 0x4e)
+ balloonY = (uint16)((0x50 - h) >> 1);
+ const byte transp = (byte)(balloon.flags >> 8);
+ ms.transBlitFrom(balloon.surface,
+ Common::Point(0x21, balloonY), transp);
+ }
+ uint16 bx = 5;
+ uint16 by = 4;
+ uint16 bw = 142;
+ getBalloonInsets(balloonIdx, bx, by, bw);
+ _font.drawWordWrapped(&ms, 0x21 + bx, balloonY + by,
+ MAX<int>(8, (int)bw), text, 0);
+ g_system->copyRectToScreen(ms.getPixels(), ms.pitch, 0, 0, 320, 200);
+ g_system->updateScreen();
+ // Wait for click.
+ while (!shouldQuit()) {
+ Common::Event ev;
+ bool advance = false;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER ||
+ ev.type == Common::EVENT_LBUTTONDOWN ||
+ ev.type == Common::EVENT_KEYDOWN) {
+ advance = true;
+ break;
+ }
+ }
+ if (advance)
+ break;
+ g_system->updateScreen();
+ g_system->delayMillis(10);
+ }
+}
+
+void EEMEngine::accuseDrawGallery(int highlighted,
+ Common::Array<Common::Rect> &rects,
+ Common::Array<int> &suspects, uint8 num,
+ bool haveAccuseBg,
+ const Picture &accuseBg) {
+ Graphics::ManagedSurface scratch(320, 200,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ if (haveAccuseBg)
+ scratch.simpleBlitFrom(accuseBg.surface);
+
+ const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
+ Animation partnerAni;
+ if (_aniArchive.loadAnimation(partnerAnim, partnerAni) &&
+ !partnerAni.empty()) {
+ const uint32 now = g_system->getMillis();
+ const uint frameIdx = partnerFrameAtTick(0x02,
+ (uint)partnerAni.size(), now);
+ blitAnimFrameAnchored(scratch.surfacePtr(),
+ partnerAni[frameIdx], 5, 0x50);
+ }
+
+ rects.resize(num);
+ suspects.resize(num);
+ for (uint i = 0; i < num; i++) {
+ rects[i] = Common::Rect();
+ suspects[i] = -1;
+ const uint8 phys = _mystery._newOrder[i];
+ if (phys >= 5)
+ continue;
+ if (_mystery._inGallery[phys] == 0)
+ continue;
+ const byte *e = _mystery.floppySuspectEntry(i);
+ if (!e)
+ continue;
+ const uint16 picId = READ_LE_UINT16(e + 0);
+ if (picId == 0)
+ continue;
+ Picture portrait;
+ if (!_picsArchive.getPicture(picId, portrait))
+ continue;
+ const GallerySlot &s = kFloppyGallerySlots[phys];
+ // Bottom-align to baseline 0x48 (`154e:00ed`).
+ const int placeX = s.x;
+ const int placeY = s.y + (0x48 - portrait.surface.h);
+ const byte transp = (byte)(portrait.flags >> 8);
+ scratch.transBlitFrom(portrait.surface,
+ Common::Point(placeX, placeY),
+ (uint32)transp);
+ rects[i] = Common::Rect(placeX, placeY,
+ placeX + portrait.surface.w,
+ placeY + portrait.surface.h);
+ suspects[i] = (int)i;
+ if (highlighted == (int)i) {
+ scratch.frameRect(rects[i], 0xFE);
+ }
+ }
+
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, 320, 200);
+ g_system->updateScreen();
+}
+
void EEMEngine::doAccuseFloppy() {
// Floppy accuse:
// `_KDHelp_Floppy / FUN_1d40_11fd @ 1d40:11fd` â score gate.
@@ -4419,85 +4600,6 @@ void EEMEngine::doAccuseFloppy() {
if (!kdIdx || !bufBase)
return;
- // Helper: render one KDTextIndex string as a centred KD balloon
- // over the current screen, mirroring `_DisplayHint_Floppy @
- // 1503:00ca` (= `FUN_1d40_11fd`'s body). `kdSlot` is the index
- // into KDTextIndex (0..N) â entries are u16 absolute text
- // offsets.
- auto showFloppyKDHint = [&](uint kdSlot) {
- if ((uint)(kdSlot * 2) + 2 > (uint)(mysSize - (kdIdx - bufBase)))
- return;
- const uint16 textOff = READ_LE_UINT16(kdIdx + kdSlot * 2);
- if (textOff == 0 || textOff >= mysSize)
- return;
- const char *p = (const char *)(bufBase + textOff);
- uint32 lineLen = 0;
- while (textOff + lineLen < mysSize && p[lineLen] != 0)
- lineLen++;
- if (lineLen == 0)
- return;
- Common::String raw(p, lineLen);
- // Digit â balloon variant (`_GetKDTextBalloon_Floppy @ 1d40:009f`
- // + table @ 2608:0c14).
- static const uint8 kDigitToBalloon[10] = {
- 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1c, 0x1d, 0x1e, 0x0a
- };
- uint balloonIdx = 0x17;
- const char *txt = raw.c_str();
- if (*txt >= '0' && *txt <= '9') {
- balloonIdx = kDigitToBalloon[(int)(*txt - '0')];
- txt++;
- }
- Common::String text =
- parseString(Common::String(txt), _playerName, _partner);
- balloonIdx = fitBalloonToText((uint16)balloonIdx, text) & 0x7F;
- Graphics::ManagedSurface ms(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- Graphics::Surface *cur = g_system->lockScreen();
- if (cur) {
- ms.simpleBlitFrom(*cur);
- g_system->unlockScreen();
- }
- Picture balloon;
- const bool haveBalloon = _balloonArchive.size() > balloonIdx &&
- _balloonArchive.loadEntry(balloonIdx, balloon);
- uint16 balloonY = 1;
- if (haveBalloon) {
- const uint h = (uint)balloon.surface.h;
- if (h < 0x4e)
- balloonY = (uint16)((0x50 - h) >> 1);
- const byte transp = (byte)(balloon.flags >> 8);
- ms.transBlitFrom(balloon.surface,
- Common::Point(0x21, balloonY), transp);
- }
- uint16 bx = 5;
- uint16 by = 4;
- uint16 bw = 142;
- getBalloonInsets(balloonIdx, bx, by, bw);
- _font.drawWordWrapped(&ms, 0x21 + bx, balloonY + by,
- MAX<int>(8, (int)bw), text, 0);
- g_system->copyRectToScreen(ms.getPixels(), ms.pitch, 0, 0, 320, 200);
- g_system->updateScreen();
- // Wait for click.
- while (!shouldQuit()) {
- Common::Event ev;
- bool advance = false;
- while (g_system->getEventManager()->pollEvent(ev)) {
- if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER ||
- ev.type == Common::EVENT_LBUTTONDOWN ||
- ev.type == Common::EVENT_KEYDOWN) {
- advance = true;
- break;
- }
- }
- if (advance)
- break;
- g_system->updateScreen();
- g_system->delayMillis(10);
- }
- };
-
// `FUN_1d40_11fd` â score gate, ready when score >= 100.
const int score = _mystery.selectedPoints();
uint kdSlot;
@@ -4512,7 +4614,7 @@ void EEMEngine::doAccuseFloppy() {
kdSlot = 2; // "ready to solve"
readyToSolve = true;
}
- showFloppyKDHint(kdSlot);
+ floppyKDHint(kdSlot, kdIdx, bufBase, mysSize);
if (!readyToSolve) {
_nextScreen = _lastScreen != kScreenInvalid
? (ScreenId)_lastScreen : kScreenSite;
@@ -4545,14 +4647,14 @@ void EEMEngine::doAccuseFloppy() {
}
}
if (userSelectedScore < 100) {
- showFloppyKDHint(3);
+ floppyKDHint(3, kdIdx, bufBase, mysSize);
_nextScreen = _lastScreen != kScreenInvalid
? (ScreenId)_lastScreen : kScreenSite;
return;
}
// `FUN_1d40_0c79` â gallery picker. KDTextIndex slot 4 prompt.
- showFloppyKDHint(4);
+ floppyKDHint(4, kdIdx, bufBase, mysSize);
// Floppy gallery: byte0 = numSuspects, then variable-stride entries.
const uint8 num = _mystery.numSuspects();
@@ -4562,82 +4664,15 @@ void EEMEngine::doAccuseFloppy() {
return;
}
- // `_DrawGallery_Floppy @ 154e:0050` slots @ 2608:016c.
- struct GallerySlot { int x, y; };
- static const GallerySlot kFloppySlots[5] = {
- { 0x53, 0x0e }, { 0x9b, 0x0e }, { 0xe3, 0x0e },
- { 0x77, 0x5a }, { 0xbf, 0x5a },
- };
-
Picture accuseBg;
const bool haveAccuseBg = _picsArchive.getPicture(0x3f, accuseBg);
- auto drawGallery = [&](int highlighted,
- Common::Array<Common::Rect> &rects,
- Common::Array<int> &suspects) {
- Graphics::ManagedSurface scratch(320, 200,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.clear();
- if (haveAccuseBg)
- scratch.simpleBlitFrom(accuseBg.surface);
-
- const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
- Animation partnerAni;
- if (_aniArchive.loadAnimation(partnerAnim, partnerAni) &&
- !partnerAni.empty()) {
- const uint32 now = g_system->getMillis();
- const uint frameIdx = partnerFrameAtTick(0x02,
- (uint)partnerAni.size(), now);
- blitAnimFrameAnchored(scratch.surfacePtr(),
- partnerAni[frameIdx], 5, 0x50);
- }
-
- rects.resize(num);
- suspects.resize(num);
- for (uint i = 0; i < num; i++) {
- rects[i] = Common::Rect();
- suspects[i] = -1;
- const uint8 phys = _mystery._newOrder[i];
- if (phys >= 5)
- continue;
- if (_mystery._inGallery[phys] == 0)
- continue;
- const byte *e = _mystery.floppySuspectEntry(i);
- if (!e)
- continue;
- const uint16 picId = READ_LE_UINT16(e + 0);
- if (picId == 0)
- continue;
- Picture portrait;
- if (!_picsArchive.getPicture(picId, portrait))
- continue;
- const GallerySlot &s = kFloppySlots[phys];
- // Bottom-align to baseline 0x48 (`154e:00ed`).
- const int placeX = s.x;
- const int placeY = s.y + (0x48 - portrait.surface.h);
- const byte transp = (byte)(portrait.flags >> 8);
- scratch.transBlitFrom(portrait.surface,
- Common::Point(placeX, placeY),
- (uint32)transp);
- rects[i] = Common::Rect(placeX, placeY,
- placeX + portrait.surface.w,
- placeY + portrait.surface.h);
- suspects[i] = (int)i;
- if (highlighted == (int)i) {
- scratch.frameRect(rects[i], 0xFE);
- }
- }
-
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
- g_system->updateScreen();
- };
-
Common::Array<Common::Rect> slotRects;
Common::Array<int> slotSuspect;
int highlighted = 0;
int picked = -1;
- drawGallery(highlighted, slotRects, slotSuspect);
+ accuseDrawGallery(highlighted, slotRects, slotSuspect, num,
+ haveAccuseBg, accuseBg);
uint32 lastTick = g_system->getMillis();
while (picked < 0 && !shouldQuit()) {
@@ -4649,17 +4684,20 @@ void EEMEngine::doAccuseFloppy() {
if (ev.type == Common::EVENT_KEYDOWN) {
if (ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
openMainMenuDialog();
- drawGallery(highlighted, slotRects, slotSuspect);
+ accuseDrawGallery(highlighted, slotRects, slotSuspect,
+ num, haveAccuseBg, accuseBg);
continue;
}
if (ev.kbd.keycode == Common::KEYCODE_TAB ||
ev.kbd.keycode == Common::KEYCODE_RIGHT) {
highlighted = (highlighted + 1) % MAX<int>(1, (int)num);
- drawGallery(highlighted, slotRects, slotSuspect);
+ accuseDrawGallery(highlighted, slotRects, slotSuspect,
+ num, haveAccuseBg, accuseBg);
} else if (ev.kbd.keycode == Common::KEYCODE_LEFT) {
highlighted = (highlighted + (int)num - 1) %
MAX<int>(1, (int)num);
- drawGallery(highlighted, slotRects, slotSuspect);
+ accuseDrawGallery(highlighted, slotRects, slotSuspect,
+ num, haveAccuseBg, accuseBg);
} else if ((ev.kbd.keycode == Common::KEYCODE_RETURN ||
ev.kbd.keycode == Common::KEYCODE_KP_ENTER) &&
highlighted < (int)slotRects.size() &&
@@ -4679,7 +4717,8 @@ void EEMEngine::doAccuseFloppy() {
}
const uint32 now = g_system->getMillis();
if (now - lastTick >= 100) {
- drawGallery(highlighted, slotRects, slotSuspect);
+ accuseDrawGallery(highlighted, slotRects, slotSuspect, num,
+ haveAccuseBg, accuseBg);
lastTick = now;
}
g_system->updateScreen();
@@ -4956,7 +4995,7 @@ void EEMEngine::doAccuseFloppy() {
}
// `_DisplayAlibi_Floppy @ 1d40:01ee` â KDTextIndex[+10] = slot 5.
- showFloppyKDHint(5);
+ floppyKDHint(5, kdIdx, bufBase, mysSize);
if (_music && _voiceOn)
_music->stop();
Commit: b8ebea90adecad7133d828add76452026b3249ea
https://github.com/scummvm/scummvm/commit/b8ebea90adecad7133d828add76452026b3249ea
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:11+02:00
Commit Message:
EEM: correct implementation of OpenColorCycle
Changed paths:
engines/eem/eem.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 0afc5f5dc9f..89c89a88b48 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -804,76 +804,128 @@ void EEMEngine::waitForInput(uint32 maxMs) {
}
}
+// _OpenColorCycle @ 2520:04f7. Rotate `fpal[start..end]` by one slot:
+// saved = fpal[end]
+// for u = end..start+1: fpal[u] = fpal[u-1]
+// fpal[start] = saved
+// If `show`, upload `end - start` entries (fpal[start..end-1]) â note that
+// fpal[end] is rotated in memory but intentionally not uploaded each tick.
+static void openColorCycle(byte *fpal, uint8 start, uint8 end, bool show) {
+ if (end <= start)
+ return;
+ const byte savedR = fpal[end * 3 + 0];
+ const byte savedG = fpal[end * 3 + 1];
+ const byte savedB = fpal[end * 3 + 2];
+ for (uint u = end; u > start; u--) {
+ fpal[u * 3 + 0] = fpal[(u - 1) * 3 + 0];
+ fpal[u * 3 + 1] = fpal[(u - 1) * 3 + 1];
+ fpal[u * 3 + 2] = fpal[(u - 1) * 3 + 2];
+ }
+ fpal[start * 3 + 0] = savedR;
+ fpal[start * 3 + 1] = savedG;
+ fpal[start * 3 + 2] = savedB;
+ if (show) {
+ g_system->getPaletteManager()->setPalette(fpal + start * 3, start,
+ end - start);
+ }
+}
+
void EEMEngine::showEAKidsLogo() {
// _ShowEAKids @ 2520:05f0:
- // 1. GetPicture(0x54) + MemoryCopy to VGA + GetPalette(0x25).
- // 2. FRAME_RATE = 25; for j in 0..1, for u in 0..0x36 (= 55):
- // OpenColorCycle(0x01, 0x6e) // bg / outer ring shimmer
- // OpenColorCycle(0x81, 0xee) // inner gradient shimmer
- // every 8 ticks: OpenColorCycle(0x70, 0x80) // mid band
- // 3. Tail: 5 more cycles of 0x70..0x80.
- // 4. Wait 0x23 (=35) more frames.
- // 5. _OpenFadeOut.
- // The cycling is what gives the EA Kids logo its shifting glow â
- // it's NOT a static logo.
+ // _GetPicture(0x54) + memcpy to 0xa000 (VGA).
+ // _GetPalette(0x25) loads pal 0x25 into _fpal (NOT uploaded to DAC).
+ // FRAME_RATE = 0x19 (25 fps); _InitFrameReg.
+ // for j in 0..1: show = j;
+ // for u in 0..0x37 (= 55):
+ // if (show) wait for next 25-fps tick (abort on key/click).
+ // _OpenColorCycle(0x01, 0x6e, show) // bg / outer ring shimmer
+ // _OpenColorCycle(0x81, 0xee, show) // inner gradient shimmer
+ // if (--delay == 0) {
+ // delay = 8;
+ // _OpenColorCycle(0x70, 0x80, show) // mid band
+ // }
+ // if (!abort) {
+ // for i in 0..5: _OpenColorCycle(0x70, 0x80, 1);
+ // for i in 0..0x23: wait one frame;
+ // }
+ // _OpenFadeOut().
+ //
+ // Pass 1 (j=0, show=0) pre-rolls _fpal 55 frames in memory only â no
+ // DAC upload, no frame sync. Pass 2 (j=1, show=1) uploads each shift
+ // at 25 fps. Without the pre-roll, the logo first appears at the
+ // unrotated palette-0x25 phase instead of the intended "55-shifts-in"
+ // phase.
Picture pic;
if (!_picsArchive.getPicture(kPicEAKidsLogo, pic)) {
warning("EA Kids logo (%u) load failed", kPicEAKidsLogo);
return;
}
blitAt(pic, 0, 0);
- setSitePalette(kPalEAKids);
- g_system->updateScreen();
- // 25 fps -> 40 ms / tick.
- const uint kFrameMs = 40;
- int delayCount = 8;
+ // _GetPalette(0x25) â load into our shadow buffer; do not upload.
+ // The logo bitmap is on screen but invisible until the first
+ // _OpenColorCycle(..., show=1) upload in pass 2 lights it up.
+ byte fpal[kPalSize];
+ if (!getSitePalette(kPalEAKids, fpal)) {
+ warning("EA Kids palette (%u) load failed", kPalEAKids);
+ return;
+ }
+
+ const uint kFrameMs = 40; // FRAME_RATE = 0x19 (25 fps).
bool aborted = false;
- for (uint outer = 0; outer < 2 && !aborted && !shouldQuit(); outer++) {
- for (uint i = 0; i < 0x37 && !aborted && !shouldQuit(); i++) {
- cyclePaletteRangeReverse(0x01, 0x6e);
- cyclePaletteRangeReverse(0x81, 0xee);
- delayCount--;
- if (delayCount == 0) {
- delayCount = 8;
- cyclePaletteRangeReverse(0x70, 0x80);
- }
- g_system->updateScreen();
- const uint32 frameEnd = g_system->getMillis() + kFrameMs;
- while (g_system->getMillis() < frameEnd && !aborted) {
- Common::Event ev;
- while (g_system->getEventManager()->pollEvent(ev)) {
- if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
- _skipIntro = true;
- return;
- }
- if (ev.type == Common::EVENT_KEYDOWN ||
- ev.type == Common::EVENT_LBUTTONDOWN) {
- if (ev.type == Common::EVENT_KEYDOWN &&
- ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ for (uint j = 0; j < 2 && !aborted && !shouldQuit(); j++) {
+ const bool show = (j != 0);
+ int delayCount = 8;
+
+ for (uint u = 0; u < 0x37 && !aborted && !shouldQuit(); u++) {
+ if (show) {
+ const uint32 frameEnd = g_system->getMillis() + kFrameMs;
+ while (g_system->getMillis() < frameEnd && !aborted) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
_skipIntro = true;
+ return;
+ }
+ if (ev.type == Common::EVENT_KEYDOWN ||
+ ev.type == Common::EVENT_LBUTTONDOWN) {
+ if (ev.type == Common::EVENT_KEYDOWN &&
+ ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ _skipIntro = true;
+ }
+ aborted = true;
+ break;
}
- aborted = true;
- break;
}
+ g_system->delayMillis(5);
}
- g_system->delayMillis(5);
}
+
+ openColorCycle(fpal, 0x01, 0x6e, show);
+ openColorCycle(fpal, 0x81, 0xee, show);
+ delayCount--;
+ if (delayCount == 0) {
+ delayCount = 8;
+ openColorCycle(fpal, 0x70, 0x80, show);
+ }
+ if (show)
+ g_system->updateScreen();
}
}
- if (aborted)
+ if (aborted) {
+ fadeCurrentPaletteToBlack();
return;
+ }
for (uint i = 0; i < 5 && !shouldQuit(); i++)
- cyclePaletteRangeReverse(0x70, 0x80);
+ openColorCycle(fpal, 0x70, 0x80, true);
g_system->updateScreen();
waitForInput(0x23 * kFrameMs);
- // `_OpenFadeOut @ 2520:0093` â 16 linear steps from current palette
- // to black.
+ // _OpenFadeOut @ 2520:0093 â 16 linear steps from current palette to black.
fadeCurrentPaletteToBlack();
}
Commit: ad29adf75013f363dc72fb775af4b03c68b3c3de
https://github.com/scummvm/scummvm/commit/ad29adf75013f363dc72fb775af4b03c68b3c3de
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:11+02:00
Commit Message:
EEM: fixed bug when picking up the phone earlier than expected
Changed paths:
engines/eem/clues.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 39aca26fb17..c435ab85c66 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -407,6 +407,28 @@ void EEMEngine::doInitClues() {
}
}
+ // Setup voice â _DoInitClues @ 1a35:061d runs this BEFORE _PlayInSequence
+ // (i.e. the phone rings, the player hears it ring out, and only then
+ // does Jake/Jenny reach for the receiver):
+ // CD: caseType 2 -> PHONE.VOC then _WaitForVoiceDone
+ // Floppy (_DoInitClues_Floppy @ 19bb:042f):
+ // caseType 2 -> slot 0xc (PHONESL.VOC)
+ // caseType 3 -> slot 3 (NEWSCAN.VOC, news-anchor variant)
+ // While the voice plays the screen continues showing the game anim's
+ // last frame (saved into briefingBase above).
+ if (_audio) {
+ if (caseType == 2) {
+ if (floppy)
+ _audio->playFloppyVoiceSlot(0x0c, _partner);
+ else
+ _audio->playVoc(Common::Path("PHONE.VOC"));
+ _audio->waitForVoiceDone();
+ } else if (caseType == 3 && floppy) {
+ _audio->playFloppyVoiceSlot(0x03, _partner);
+ _audio->waitForVoiceDone();
+ }
+ }
+
// _PlayInSequence @ 172b:2d03. Anim selection per partner + caseType:
// Jake: caseType 1 -> 0x38 @ (0xcd, 0x6d)
// caseType 2 -> 0x37 @ (0xcd, 0x6c)
@@ -487,24 +509,6 @@ void EEMEngine::doInitClues() {
}
}
- // Setup voice (caseType 2/3 only):
- // CD: caseType 2 -> PHONE.VOC
- // Floppy (_DoInitClues_Floppy @ 19bb:042f):
- // caseType 2 -> slot 0xc (PHONESL.VOC)
- // caseType 3 -> slot 3 (NEWSCAN.VOC, news-anchor variant)
- if (_audio) {
- if (caseType == 2) {
- if (floppy)
- _audio->playFloppyVoiceSlot(0x0c, _partner);
- else
- _audio->playVoc(Common::Path("PHONE.VOC"));
- _audio->waitForVoiceDone();
- } else if (caseType == 3 && floppy) {
- _audio->playFloppyVoiceSlot(0x03, _partner);
- _audio->waitForVoiceDone();
- }
- }
-
// Briefing dialogue. CD: clue block @ ib+4 (after caseType,startSite).
// Floppy: dialog records dispatched via FUN_22dc_05c8 @ 22dc:05c8
// (record size = 11 + textCount bytes).
Commit: b69b12c24432f19666c632e1c0312a5cc0e768a2
https://github.com/scummvm/scummvm/commit/b69b12c24432f19666c632e1c0312a5cc0e768a2
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:11+02:00
Commit Message:
EEM: fixed bug in Nancy animation
Changed paths:
engines/eem/clues.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index c435ab85c66..b68b3076ff4 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -321,17 +321,27 @@ void EEMEngine::doInitClues() {
&& _aniArchive.loadAnimation(0x19, nancy)
&& !nancy.empty();
- // Cycle game animation once (10 fps = _CheckFrameRate cadence).
- // _DoInitClues @ 1a35:0507/0541 hard-codes the SCRIPT index to Jake's
- // IDs (0x17/0x18/0x19) regardless of partner, so look up scripts by
- // those IDs unconditionally.
+ // Cycle game animation once at _CheckFrameRate cadence (~7 fps, 140 ms
+ // per _UpdateAnimations call â `LastFrame + 0xe` cs in _InitFrameCounter
+ // @ 1a35:01ae). Original loop @ 1a35:0507:
+ // uVar9 = 1;
+ // while (uVar9 != gameNum) {
+ // if (_CheckFrameRate || skipped) { _UpdateAnimations(); uVar9++; }
+ // }
+ // So `gameNum - 1` _UpdateAnimations calls; each call advances every
+ // registered slot by one script tick. _DoInitClues @ 1a35:0507/0541
+ // hard-codes the SCRIPT index to Jake's IDs (0x17/0x18/0x19) regardless
+ // of partner, so we look up scripts by those IDs unconditionally.
if (haveGame || haveBook || haveNancy) {
- const uint frameCount = haveGame ? game.size() : 8;
+ const uint kCheckFrameRateMs = 140;
+ const uint baseFrames = haveGame ? game.size() : 8;
+ // `gameNum - 1` ticks: scriptIdx 0..gameNum-2.
+ const uint frameCount = (baseFrames > 0) ? baseFrames - 1 : 0;
bool skip = false;
for (uint frame = 0; frame < frameCount && !shouldQuit() && !skip; frame++) {
if (haveBriefingBg)
blitAt(bg, 0, 0);
- const uint32 t = frame * 100;
+ const uint32 t = frame * kCheckFrameRateMs;
Graphics::Surface *scr = g_system->lockScreen();
if (!scr) {
skip = true;
@@ -353,7 +363,7 @@ void EEMEngine::doInitClues() {
g_system->updateScreen();
// ESC interrupts voice/spool so audio doesn't bleed into the MAP.
- const uint32 wakeup = g_system->getMillis() + 100;
+ const uint32 wakeup = g_system->getMillis() + kCheckFrameRateMs;
while (g_system->getMillis() < wakeup && !shouldQuit() && !skip) {
Common::Event ev;
while (g_system->getEventManager()->pollEvent(ev)) {
Commit: 24a3e5a40cd63cb210c684a5454c93567da757e1
https://github.com/scummvm/scummvm/commit/24a3e5a40cd63cb210c684a5454c93567da757e1
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:12+02:00
Commit Message:
EEM: fixed global-constructors from kHappyZones
Changed paths:
engines/eem/clues.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index b68b3076ff4..0f8a56ff977 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -45,12 +45,13 @@ const uint kAniBoy = 8; // Jake
const uint kAniGirl = 9; // Jenny
// _DoHappiness @ 172b:27b5 â cursor X picks one of 4 rects @ 29be:030f.
-// Past rect 3 = level 4.
-const Common::Rect kHappyZones[4] = {
- Common::Rect( 0, 0, 70, 200), // far left â girl very happy, boy neutral
- Common::Rect( 70, 0, 126, 200), // girl's column
- Common::Rect(126, 0, 182, 200), // middle
- Common::Rect(182, 0, 235, 200), // boy's column
+// Past rect 3 = level 4. Constexpr (Point, w, h) form to avoid a global
+// constructor (-Wglobal-constructors).
+constexpr Common::Rect kHappyZones[4] = {
+ Common::Rect(Common::Point( 0, 0), 70, 200), // far left â girl very happy, boy neutral
+ Common::Rect(Common::Point( 70, 0), 56, 200), // girl's column
+ Common::Rect(Common::Point(126, 0), 56, 200), // middle
+ Common::Rect(Common::Point(182, 0), 53, 200), // boy's column
};
// _NewAnimation positions @ 1a35:07b9 / 07d5.
Commit: 12776d655aa8e45ed88d02e26c45d7f73d60a5eb
https://github.com/scummvm/scummvm/commit/12776d655aa8e45ed88d02e26c45d7f73d60a5eb
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:12+02:00
Commit Message:
EEM: removed the 'create new' workaround
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 6e5c9124516..048e8df5efb 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -403,7 +403,7 @@ bool animateProfilePickerReveal(EEMEngine *vm, const Picture *bg,
struct ProfilePickerEntry {
Common::String label;
- int slot; ///< -1 means "create new"
+ int slot;
};
struct ProfilePickerView {
@@ -706,8 +706,11 @@ void EEMEngine::doProfilePicker() {
return;
}
- // Existing profiles + "[New Player]". DOS bottom click area @ 29be:0d08
- // returns 0xfffe â `_NewPlayer`; rect remains active too.
+ // Existing profiles only â the original `screen8_handler @ 1c33:1012`
+ // passes the bare *.PLR list to `_DoChoose` with NO synthesized "new
+ // player" entry. _DoChoose returns 0xfffe / 0xffff for the bottom click
+ // rect @ 29be:0d08 / ESC, which routes to `_NewPlayer` (handled below
+ // via kChooserNewPlayerRect / KEYCODE_ESCAPE).
Common::Array<ProfilePickerEntry> entries;
for (const SaveStateDescriptor &s : saves) {
ProfilePickerEntry e;
@@ -715,10 +718,6 @@ void EEMEngine::doProfilePicker() {
e.slot = s.getSaveSlot();
entries.push_back(e);
}
- ProfilePickerEntry newEntry;
- newEntry.label = "[New Player]";
- newEntry.slot = -1;
- entries.push_back(newEntry);
int sel = 0;
int start = 0;
@@ -871,16 +870,12 @@ void EEMEngine::doProfilePicker() {
return;
}
+ // `_LoadPlayerRecord @ 1c33:1281`.
const ProfilePickerEntry &e = entries[sel];
- if (e.slot < 0) {
+ if (!loadProfile(e.label)) {
+ warning("doProfilePicker: failed to load profile '%s' at slot %d",
+ e.label.c_str(), e.slot);
doNewPlayer();
- } else {
- // `_LoadPlayerRecord @ 1c33:1281`.
- if (!loadProfile(e.label)) {
- warning("doProfilePicker: failed to load profile '%s' at slot %d",
- e.label.c_str(), e.slot);
- doNewPlayer();
- }
}
}
Commit: 613dac165b5ddaf8a5ddc528c0caf6abcea644c8
https://github.com/scummvm/scummvm/commit/613dac165b5ddaf8a5ddc528c0caf6abcea644c8
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:12+02:00
Commit Message:
EEM: improved animation in the PDA
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 048e8df5efb..503874824e6 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -2113,6 +2113,7 @@ void EEMEngine::doNotebook() {
mouse.x, mouse.y));
uint32 lastDraw = g_system->getMillis();
+ uint32 gizmoLastTick = lastDraw;
while (!shouldQuit()) {
Common::Event ev;
@@ -2228,6 +2229,12 @@ void EEMEngine::doNotebook() {
rectListContains(_notebookSlotRects,
mouse.x, mouse.y));
}
+ // _GizmoColorCycle @ 1c33:0002 â `_DoNotebook` rotates 0x6f..0x73 each
+ // _CheckFrameRate tick (the PDA gizmo / LED indicator shimmer).
+ if (now - gizmoLastTick >= kChooserCycleMillis) {
+ gizmoLastTick = now;
+ cycleChooserPalette();
+ }
g_system->updateScreen();
g_system->delayMillis(15);
}
@@ -2417,6 +2424,7 @@ void EEMEngine::doGallery() {
gallerySlotAt(slotRects, slotSuspect,
mouse.x, mouse.y));
uint32 lastDraw = g_system->getMillis();
+ uint32 gizmoLastTick = lastDraw;
while (!shouldQuit()) {
Common::Event ev;
@@ -2524,6 +2532,11 @@ void EEMEngine::doGallery() {
gallerySlotAt(slotRects, slotSuspect,
mouse.x, mouse.y));
}
+ // _GizmoColorCycle @ 1c33:0002 â `_DoGallery` rotates 0x6f..0x73 each tick.
+ if (now - gizmoLastTick >= kChooserCycleMillis) {
+ gizmoLastTick = now;
+ cycleChooserPalette();
+ }
g_system->updateScreen();
g_system->delayMillis(15);
}
@@ -2699,6 +2712,7 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
bool advance = false;
bool prev = false;
bool redraw = false;
+ uint32 gizmoLastTick = g_system->getMillis();
while (!back && !advance && !prev && !redraw && !shouldQuit()) {
Common::Event e2;
while (g_system->getEventManager()->pollEvent(e2)) {
@@ -2786,6 +2800,13 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
break;
}
}
+ // _GizmoColorCycle @ 1c33:0002 â `MoreInfo` (158f:0480) rotates
+ // 0x6f..0x73 each tick.
+ const uint32 now = g_system->getMillis();
+ if (now - gizmoLastTick >= kChooserCycleMillis) {
+ gizmoLastTick = now;
+ cycleChooserPalette();
+ }
g_system->updateScreen();
g_system->delayMillis(20);
}
Commit: 23baebb9a800604e43f16a02f7fe5bf5935aa3c3
https://github.com/scummvm/scummvm/commit/23baebb9a800604e43f16a02f7fe5bf5935aa3c3
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:13+02:00
Commit Message:
EEM: removed incorrect attribution in the headers
Changed paths:
engines/eem/eem.h
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 6d83cdc4da9..8ba5a135353 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -17,7 +17,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
- * Based on the original engine code by EA Kids / Storm Software (1994).
*
*/
Commit: d901f38d663f85784e2f5664fe659272bbe547e2
https://github.com/scummvm/scummvm/commit/d901f38d663f85784e2f5664fe659272bbe547e2
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:13+02:00
Commit Message:
EEM: reduced code duplication using transBlitFrom
Changed paths:
engines/eem/clues.cpp
engines/eem/graphics.cpp
engines/eem/site.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 0f8a56ff977..fcb47cdb06d 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -114,23 +114,17 @@ uint happinessLevel(int x) {
// _Rect_Move_Mask's mask byte (the on-disk u16 at file offset 0 maps to
// Picture::flags).
void blitMaskedToScreen(const Picture &p, int x, int y) {
- const byte transp = (byte)(p.flags >> 8);
Graphics::Surface *screen = g_system->lockScreen();
if (!screen)
return;
- for (int row = 0; row < p.surface.h; row++) {
- const int dstY = y + row;
- if (dstY < 0 || dstY >= screen->h)
- continue;
- const byte *src = (const byte *)p.surface.getBasePtr(0, row);
- byte *dst = (byte *)screen->getBasePtr(0, dstY);
- for (int col = 0; col < p.surface.w; col++) {
- const int dstX = x + col;
- if (dstX < 0 || dstX >= screen->w)
- continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
+ const Common::Rect dst = Common::Rect(x, y, x + p.surface.w,
+ y + p.surface.h)
+ .findIntersectingRect(Common::Rect(screen->w, screen->h));
+ if (!dst.isEmpty()) {
+ const Common::Rect src(dst.left - x, dst.top - y,
+ dst.right - x, dst.bottom - y);
+ screen->copyRectToSurfaceWithKey(p.surface, dst.left, dst.top,
+ src, (uint32)(byte)(p.flags >> 8));
}
g_system->unlockScreen();
}
@@ -1093,19 +1087,9 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
if (picID != 0 && picID != 0xFFFF) {
Picture pic;
if (_picsArchive.getPicture(picID, pic)) {
- const byte transpC = (byte)(pic.flags >> 8);
- const int pw = MIN<int>(pic.surface.w, 320 - picX);
- const int ph = MIN<int>(pic.surface.h, 200 - picY);
- for (int row = 0; row < ph; row++) {
- const byte *src = (const byte *)
- pic.surface.getBasePtr(0, row);
- byte *dst = (byte *)
- scratch.getBasePtr(picX, picY + row);
- for (int col = 0; col < pw; col++) {
- if (src[col] != transpC)
- dst[col] = src[col];
- }
- }
+ scratch.transBlitFrom(pic.surface,
+ Common::Point(picX, picY),
+ (uint32)(byte)(pic.flags >> 8));
}
}
if (haveBalloon) {
diff --git a/engines/eem/graphics.cpp b/engines/eem/graphics.cpp
index 8251aee33f7..544e66c7f50 100644
--- a/engines/eem/graphics.cpp
+++ b/engines/eem/graphics.cpp
@@ -274,18 +274,9 @@ void EEMEngine::doHelp() {
const uint h = (uint)balloon.surface.h;
if (h < 0x4e)
balloonY = (uint16)((0x50 - h) >> 1);
- const byte transp = (byte)(balloon.flags >> 8);
- for (int row = 0; row < balloon.surface.h && balloonY + row < 200;
- row++) {
- const byte *src =
- (const byte *)balloon.surface.getBasePtr(0, row);
- byte *dst = (byte *)ms.getBasePtr(0x21, balloonY + row);
- for (int col = 0; col < balloon.surface.w && 0x21 + col < 320;
- col++) {
- if (src[col] != transp)
- dst[col] = src[col];
- }
- }
+ ms.transBlitFrom(balloon.surface,
+ Common::Point(0x21, balloonY),
+ (uint32)(byte)(balloon.flags >> 8));
}
uint16 bx = 5;
uint16 by = 4;
@@ -669,19 +660,8 @@ void EEMEngine::drawFloppyBubbleIndicator(Graphics::ManagedSurface &dst,
return;
const int x = ballX + (int)dx;
const int y = ballY + (int)dy;
- const byte transp = (byte)(pic.flags >> 8);
- const int pw = MIN<int>(pic.surface.w, 320 - x);
- const int ph = MIN<int>(pic.surface.h, 200 - y);
- if (x < 0 || y < 0 || pw <= 0 || ph <= 0)
- return;
- for (int row = 0; row < ph; row++) {
- const byte *src = (const byte *)pic.surface.getBasePtr(0, row);
- byte *out = (byte *)dst.getBasePtr(x, y + row);
- for (int col = 0; col < pw; col++) {
- if (src[col] != transp)
- out[col] = src[col];
- }
- }
+ dst.transBlitFrom(pic.surface, Common::Point(x, y),
+ (uint32)(byte)(pic.flags >> 8));
}
} // End of namespace EEM
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 47bd881a88d..b0b5a2e1226 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -45,6 +45,24 @@ void blitFrame(Graphics::ManagedSurface &dst, const Picture &p,
dst.transBlitFrom(p.surface, Common::Point(x, y), (uint32)transp);
}
+// Masked top-left blit onto a locked screen surface. Clips both src and
+// dst against the screen, then delegates to copyRectToSurfaceWithKey
+// (Graphics::Surface's transparent-key blit).
+static void keyBlitToScreen(Graphics::Surface *screen, const Picture &p,
+ int x, int y) {
+ if (!screen || p.surface.empty())
+ return;
+ const Common::Rect dst = Common::Rect(x, y, x + p.surface.w,
+ y + p.surface.h)
+ .findIntersectingRect(Common::Rect(screen->w, screen->h));
+ if (dst.isEmpty())
+ return;
+ const Common::Rect src(dst.left - x, dst.top - y,
+ dst.right - x, dst.bottom - y);
+ screen->copyRectToSurfaceWithKey(p.surface, dst.left, dst.top,
+ src, (uint32)(byte)(p.flags >> 8));
+}
+
// Top-left masked blit. `_AddDrop @ 172b:1a77` calls
// `_Rect_Move_Mask(..., x, y, ...)` with the raw (x, y) and IGNORES
// per-frame anchor offsets â so this is the correct path for static
@@ -53,50 +71,18 @@ void blitFrame(Graphics::ManagedSurface &dst, const Picture &p,
// (miscflags = X, rowoff = Y) apply correctly.
void blitMaskedSurface(Graphics::Surface *screen, const Picture &p,
int x, int y) {
- if (!screen)
- return;
- const byte transp = (byte)(p.flags >> 8);
- for (int row = 0; row < p.surface.h; row++) {
- const int dstY = y + row;
- if (dstY < 0 || dstY >= screen->h)
- continue;
- const byte *src = (const byte *)p.surface.getBasePtr(0, row);
- byte *dst = (byte *)screen->getBasePtr(0, dstY);
- for (int col = 0; col < p.surface.w; col++) {
- const int dstX = x + col;
- if (dstX < 0 || dstX >= screen->w)
- continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
+ keyBlitToScreen(screen, p, x, y);
}
+// `_UpdateAnimations @ 172b:09c1`: blit at
+// (anchor_x - puVar5[4], anchor_y - puVar5[3])
+// where puVar5[3]/[4] are per-frame rowoff/miscflags (signed int16) from
+// the 16-byte PicData header. Transparency = flags >> 8.
void blitAnimFrameAnchored(Graphics::Surface *screen, const Picture &p,
int anchorX, int anchorY) {
- // `_UpdateAnimations @ 172b:09c1`: blit at
- // (anchor_x - puVar5[4], anchor_y - puVar5[3])
- // where puVar5[3]/[4] are per-frame rowoff/miscflags (signed int16)
- // from the 16-byte PicData header. Transparency = flags >> 8.
- if (!screen)
- return;
- const int blitX = anchorX - (int)(int16)p.miscflags;
- const int blitY = anchorY - (int)(int16)p.rowoff;
- const byte transp = (byte)(p.flags >> 8);
- for (int row = 0; row < p.surface.h; row++) {
- const int dstY = blitY + row;
- if (dstY < 0 || dstY >= screen->h)
- continue;
- const byte *src = (const byte *)p.surface.getBasePtr(0, row);
- byte *dst = (byte *)screen->getBasePtr(0, dstY);
- for (int col = 0; col < p.surface.w; col++) {
- const int dstX = blitX + col;
- if (dstX < 0 || dstX >= screen->w)
- continue;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
+ keyBlitToScreen(screen, p,
+ anchorX - (int)(int16)p.miscflags,
+ anchorY - (int)(int16)p.rowoff);
}
// `_ColorCycle @ 172b:2015` â rotate `_fpal[start..end]` by one slot:
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 503874824e6..c1398a0fc48 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -275,23 +275,9 @@ void blitMaskedPicSlice(Graphics::ManagedSurface &dst, const Picture &pic,
int dstX, int dstY) {
if (pic.surface.empty() || w <= 0 || h <= 0)
return;
-
- const byte transp = (byte)(pic.flags >> 8);
- for (int row = 0; row < h; row++) {
- const int sy = srcY + row;
- const int dy = dstY + row;
- if (sy < 0 || sy >= pic.surface.h || dy < 0 || dy >= dst.h)
- continue;
- for (int col = 0; col < w; col++) {
- const int sx = srcX + col;
- const int dx = dstX + col;
- if (sx < 0 || sx >= pic.surface.w || dx < 0 || dx >= dst.w)
- continue;
- const byte c = *(const byte *)pic.surface.getBasePtr(sx, sy);
- if (c != transp)
- *(byte *)dst.getBasePtr(dx, dy) = c;
- }
- }
+ const Common::Rect srcRect(srcX, srcY, srcX + w, srcY + h);
+ dst.transBlitFrom(pic.surface, srcRect, Common::Point(dstX, dstY),
+ (uint32)(byte)(pic.flags >> 8));
}
void blitMaskedPic(Graphics::ManagedSurface &dst, const Picture &pic,
@@ -2898,24 +2884,13 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
const int placeX = s.x;
const int placeY = s.y + (0x48 - portrait.surface.h);
- const byte transp = (byte)(portrait.flags >> 8);
const int w = MIN<int>(portrait.surface.w, 320 - placeX);
const int h = MIN<int>(portrait.surface.h, 200 - placeY);
if (w <= 0 || h <= 0)
continue;
- for (int row = 0; row < h; row++) {
- const int dstY = placeY + row;
- if (dstY < 0)
- continue;
- const byte *src =
- (const byte *)portrait.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, dstY);
- for (int col = 0; col < w; col++) {
- const int dstX = placeX + col;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
+ scratch.transBlitFrom(portrait.surface,
+ Common::Point(placeX, placeY),
+ (uint32)(byte)(portrait.flags >> 8));
slotRects[i] = Common::Rect(placeX, placeY,
placeX + w, placeY + h);
slotSuspect[i] = (int)i;
@@ -3360,21 +3335,16 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
}
const int sx = (int)mx - scrollX + kMapWinX;
const int sy = (int)my - scrollY + kMapWinY;
- const byte transp = (byte)(button.flags >> 8);
-
- // Crop blit against the viewport.
+ // Crop the button blit against the viewport.
const int x0 = MAX<int>(sx, kMapWinX);
const int y0 = MAX<int>(sy, kMapWinY);
const int x1 = MIN<int>(sx + button.surface.w, kMapWinX + kMapWinW);
const int y1 = MIN<int>(sy + button.surface.h, kMapWinY + kMapWinH);
- for (int row = y0; row < y1; row++) {
- const byte *src = (const byte *)button.surface.getBasePtr(0, row - sy);
- byte *dst = (byte *)scratch.getBasePtr(0, row);
- for (int col = x0; col < x1; col++) {
- const byte px = src[col - sx];
- if (px != transp)
- dst[col] = px;
- }
+ if (x1 > x0 && y1 > y0) {
+ const Common::Rect srcRect(x0 - sx, y0 - sy, x1 - sx, y1 - sy);
+ scratch.transBlitFrom(button.surface, srcRect,
+ Common::Point(x0, y0),
+ (uint32)(byte)(button.flags >> 8));
}
}
@@ -5069,24 +5039,13 @@ void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
const int placeX = s.x;
const int placeY = s.y + (0x48 - portrait.surface.h);
- const byte transp = (byte)(portrait.flags >> 8);
const int w = MIN<int>(portrait.surface.w, 320 - placeX);
const int h = MIN<int>(portrait.surface.h, 200 - placeY);
if (w <= 0 || h <= 0)
continue;
- for (int row = 0; row < h; row++) {
- const int dstY = placeY + row;
- if (dstY < 0)
- continue;
- const byte *src =
- (const byte *)portrait.surface.getBasePtr(0, row);
- byte *dst = (byte *)scratch.getBasePtr(0, dstY);
- for (int col = 0; col < w; col++) {
- const int dstX = placeX + col;
- if (src[col] != transp)
- dst[dstX] = src[col];
- }
- }
+ scratch.transBlitFrom(portrait.surface,
+ Common::Point(placeX, placeY),
+ (uint32)(byte)(portrait.flags >> 8));
slotRects[i] = Common::Rect(placeX, placeY,
placeX + w, placeY + h);
slotSuspect[i] = (int)i;
Commit: e34c624ee4211dcce40b9693f53caf32364d7e5a
https://github.com/scummvm/scummvm/commit/e34c624ee4211dcce40b9693f53caf32364d7e5a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:13+02:00
Commit Message:
EEM: reduced code duplication in the PDA rendering
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index c1398a0fc48..0416fea7120 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -111,19 +111,50 @@ void swapColors(Graphics::ManagedSurface &dst,
}
}
-void blitAccusePartner(Graphics::ManagedSurface &dstSurface,
- DBDArchive &aniArchive, uint8 partner,
- uint32 tickMs) {
- const uint partnerAnim = (partner == 0) ? 2 : 0x10;
- Animation partnerAni;
- if (aniArchive.loadAnimation(partnerAnim, partnerAni) &&
- !partnerAni.empty()) {
- const uint frameIdx = partnerFrameAtTick(0x02,
- (uint)partnerAni.size(),
- tickMs);
- blitAnimFrameAnchored(dstSurface.surfacePtr(),
- partnerAni[frameIdx], 5, 0x50);
- }
+// PDA-frame partner sprite specs. `(5, 0x50)` is the partner-at-desk anchor
+// shared by _DoNotebook (script 0x01, anim 1/0xb) and _DoGallery / _DoAccuse
+// / _MoreInfo (script 0x02, anim 2/0x10).
+struct PdaPartnerSpec {
+ uint16 scriptId;
+ uint16 animJake;
+ uint16 animJenny;
+ int anchorX;
+ int anchorY;
+};
+constexpr PdaPartnerSpec kPdaGalleryPartner { 0x02, 0x02, 0x10, 5, 0x50 };
+constexpr PdaPartnerSpec kPdaNotebookPartner{ 0x01, 0x01, 0x0b, 5, 0x50 };
+
+// Lookup current frame for spec; returns nullptr if anim load fails. `outAni`
+// owns the loaded cells; the returned pointer is valid until `outAni` goes
+// out of scope.
+const Picture *partnerFrameFor(DBDArchive &aniArchive, uint8 partner,
+ const PdaPartnerSpec &spec, uint32 tickMs,
+ Animation &outAni) {
+ const uint animId = (partner == 0) ? spec.animJake : spec.animJenny;
+ if (!aniArchive.loadAnimation(animId, outAni) || outAni.empty())
+ return nullptr;
+ const uint frameIdx = partnerFrameAtTick(spec.scriptId,
+ (uint)outAni.size(), tickMs);
+ return &outAni[frameIdx];
+}
+
+void blitPdaPartner(Graphics::ManagedSurface &dst, DBDArchive &aniArchive,
+ uint8 partner, const PdaPartnerSpec &spec,
+ uint32 tickMs) {
+ Animation ani;
+ if (const Picture *fr = partnerFrameFor(aniArchive, partner, spec,
+ tickMs, ani))
+ blitAnimFrameAnchored(dst.surfacePtr(), *fr, spec.anchorX,
+ spec.anchorY);
+}
+
+void blitPdaPartner(Graphics::Surface *screen, DBDArchive &aniArchive,
+ uint8 partner, const PdaPartnerSpec &spec,
+ uint32 tickMs) {
+ Animation ani;
+ if (const Picture *fr = partnerFrameFor(aniArchive, partner, spec,
+ tickMs, ani))
+ blitAnimFrameAnchored(screen, *fr, spec.anchorX, spec.anchorY);
}
constexpr Common::Rect kEndingPrevPageRect(Common::Point(0, 0), 28, 200);
@@ -2263,15 +2294,8 @@ void EEMEngine::drawNotebookFrame(int &page) {
scratch.simpleBlitFrom(frame.surface);
// Partner ANI 1/0xb (cells); script 0x01 (`_NewAnimation @ 161e:054c`).
- const uint partnerAnim = (_partner == 0) ? 1 : 0xb;
- Animation partnerAni;
- if (_aniArchive.loadAnimation(partnerAnim, partnerAni) && !partnerAni.empty()) {
- const uint32 now = g_system->getMillis();
- const uint frameIdx = partnerFrameAtTick(0x01,
- (uint)partnerAni.size(), now);
- blitAnimFrameAnchored(scratch.surfacePtr(),
- partnerAni[frameIdx], 5, 80);
- }
+ blitPdaPartner(scratch, _aniArchive, _partner, kPdaNotebookPartner,
+ g_system->getMillis());
// `_DrawNotes` walks `_NoteIndex` for current page; word-wraps each
// found clue in `_NotebookRect`. Selected = color 0x3c.
@@ -2574,18 +2598,8 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
if (haveBg)
ms.simpleBlitFrom(galBg.surface);
// Partner sprite at (5, 0x50). Re-blitted per page.
- {
- const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
- Animation partnerAni;
- if (_aniArchive.loadAnimation(partnerAnim, partnerAni) &&
- !partnerAni.empty()) {
- const uint32 now = g_system->getMillis();
- const uint frameIdx = partnerFrameAtTick(0x02,
- (uint)partnerAni.size(), now);
- blitAnimFrameAnchored(ms.surfacePtr(),
- partnerAni[frameIdx], 5, 0x50);
- }
- }
+ blitPdaPartner(ms, _aniArchive, _partner, kPdaGalleryPartner,
+ g_system->getMillis());
Picture detail;
if (_picsArchive.getPicture(detailPic, detail)) {
const byte transp = (byte)(detail.flags >> 8);
@@ -2818,10 +2832,6 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
// positions live in `kGallerySlots` in this file's anon namespace.
Picture galBg;
const bool haveBg = _picsArchive.getPicture(0x3f, galBg);
- const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
- Animation partnerAni;
- const bool havePartner = _aniArchive.loadAnimation(partnerAnim, partnerAni)
- && !partnerAni.empty();
Graphics::ManagedSurface scratch(320, 200,
Graphics::PixelFormat::createFormatCLUT8());
@@ -2830,24 +2840,13 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
if (haveBg)
scratch.simpleBlitFrom(galBg.surface);
- // Partner sprite frame @ (5, 0x50). The original `_DoGallery @
- // 158f:065b` registers `_NewAnimation(..., CONCAT22(2, ...), ...)`
- // â script key 0x02 regardless of partner. Jake's 0x02 script
- // (26 frames, brief wave + long hold + second wave) is what
- // drives BOTH partners' cells. Earlier our port used 0x10 for
- // Jenny, which is a 9-frame short blip â so Jenny was missing
- // 17 frames of the wave-and-pause cadence that Jake has.
- if (havePartner) {
- const uint32 now = g_system->getMillis();
- const uint frameIdx = partnerFrameAtTick(0x02,
- (uint)partnerAni.size(), now);
- // Anchor-aware blit, consistent with site-loop / BigMap
- // rendering paths. Anim 0x02 has rowoff = miscflags = 0
- // per the audit but the anchored blitter is still the
- // right semantic for an `_NewAnimation`-rendered sprite.
- blitAnimFrameAnchored(scratch.surfacePtr(),
- partnerAni[frameIdx], 5, 0x50);
- }
+ // Partner sprite frame @ (5, 0x50). `_DoGallery @ 158f:065b` registers
+ // _NewAnimation(..., CONCAT22(2, ...), ...) â script key 0x02 regardless
+ // of partner. Jake's 26-frame 0x02 script (brief wave + long hold +
+ // second wave) drives BOTH partners' cells (Jenny's own 0x10 anim is
+ // only 9 frames so it wouldn't cover the full cadence).
+ blitPdaPartner(scratch, _aniArchive, _partner, kPdaGalleryPartner,
+ g_system->getMillis());
// Portraits â `_DrawGallery @ 158f:0046` (CD) /
// `_DrawGallery_Floppy @ 154e:0045` (floppy) walks suspects 0..N-1
@@ -3430,16 +3429,8 @@ void EEMEngine::accuseDrawScreen(const AccuseNotesCtx &ctx) {
// Partner at (5, 0x50). `_DoAccuse @ 1df2:0c2c`: ANI 2/0x10,
// script 2, prior 1.
- const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
- Animation partnerAni;
- if (_aniArchive.loadAnimation(partnerAnim, partnerAni) &&
- !partnerAni.empty()) {
- const uint32 now = g_system->getMillis();
- const uint frameIdx = partnerFrameAtTick(0x02,
- (uint)partnerAni.size(), now);
- blitAnimFrameAnchored(scratch.surfacePtr(),
- partnerAni[frameIdx], 5, 0x50);
- }
+ blitPdaPartner(scratch, _aniArchive, _partner, kPdaGalleryPartner,
+ g_system->getMillis());
// Selected=0x3c, unselected=1 (`_NoteUnselectedColor` @ 1df2:0c25).
Common::Array<Common::Rect> &slotRects = *ctx.slotRects;
@@ -4173,11 +4164,6 @@ void EEMEngine::doAccuse() {
}
// Partner at (5, 0x50). ANI 2/0x10, script 0x02 (`_DoAccuse
// @ 1df2:0c30`). Drawn after suspect.
- const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
- Animation partnerAni;
- const bool havePartner =
- _aniArchive.loadAnimation(partnerAnim, partnerAni) &&
- !partnerAni.empty();
// scratch = base + alibi balloon/text + partner.
Graphics::ManagedSurface scratch(320, 200,
@@ -4197,12 +4183,8 @@ void EEMEngine::doAccuse() {
balloonY + ty, tw, alibi,
haveBalloon ? 0 : 0xF);
}
- if (havePartner) {
- const uint frameIdx = partnerFrameAtTick(0x02,
- (uint)partnerAni.size(), g_system->getMillis());
- blitAnimFrameAnchored(scratch.surfacePtr(),
- partnerAni[frameIdx], 5, 0x50);
- }
+ blitPdaPartner(scratch, _aniArchive, _partner, kPdaGalleryPartner,
+ g_system->getMillis());
// MIDI 6 â blocks until done (or click/ESC aborts).
if (_music && _voiceOn) {
@@ -4289,14 +4271,8 @@ void EEMEngine::doAccuse() {
rY + rty, rtw, react,
haveR ? 0 : 0xF);
}
- if (havePartner) {
- const uint frameIdx = partnerFrameAtTick(0x02,
- (uint)partnerAni.size(),
- g_system->getMillis());
- blitAnimFrameAnchored(scratch.surfacePtr(),
- partnerAni[frameIdx],
- 5, 0x50);
- }
+ blitPdaPartner(scratch, _aniArchive, _partner,
+ kPdaGalleryPartner, g_system->getMillis());
g_system->copyRectToScreen(scratch.getPixels(),
scratch.pitch, 0, 0, 320, 200);
g_system->updateScreen();
@@ -4385,19 +4361,10 @@ void EEMEngine::doAccuse() {
}
// Stamp partner at (5, 0x50); displayClue snapshots screen.
- const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
- Animation partnerAni;
- if (_aniArchive.loadAnimation(partnerAnim, partnerAni) &&
- !partnerAni.empty()) {
- Graphics::Surface *screen = g_system->lockScreen();
- if (screen) {
- const uint frameIdx = partnerFrameAtTick(0x02,
- (uint)partnerAni.size(),
- g_system->getMillis());
- blitAnimFrameAnchored(screen, partnerAni[frameIdx],
- 5, 0x50);
- g_system->unlockScreen();
- }
+ if (Graphics::Surface *screen = g_system->lockScreen()) {
+ blitPdaPartner(screen, _aniArchive, _partner,
+ kPdaGalleryPartner, g_system->getMillis());
+ g_system->unlockScreen();
}
g_system->updateScreen();
@@ -4520,16 +4487,8 @@ void EEMEngine::accuseDrawGallery(int highlighted,
if (haveAccuseBg)
scratch.simpleBlitFrom(accuseBg.surface);
- const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
- Animation partnerAni;
- if (_aniArchive.loadAnimation(partnerAnim, partnerAni) &&
- !partnerAni.empty()) {
- const uint32 now = g_system->getMillis();
- const uint frameIdx = partnerFrameAtTick(0x02,
- (uint)partnerAni.size(), now);
- blitAnimFrameAnchored(scratch.surfacePtr(),
- partnerAni[frameIdx], 5, 0x50);
- }
+ blitPdaPartner(scratch, _aniArchive, _partner, kPdaGalleryPartner,
+ g_system->getMillis());
rects.resize(num);
suspects.resize(num);
@@ -4752,19 +4711,10 @@ void EEMEngine::doAccuseFloppy() {
// Stamp partner at (5, 0x50); displayFloppyDialogRecords
// snapshots screen. ANI 2/0x10, script 0x02.
- const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
- Animation partnerAni;
- if (_aniArchive.loadAnimation(partnerAnim, partnerAni) &&
- !partnerAni.empty()) {
- Graphics::Surface *screen = g_system->lockScreen();
- if (screen) {
- const uint frameIdx = partnerFrameAtTick(0x02,
- (uint)partnerAni.size(),
- g_system->getMillis());
- blitAnimFrameAnchored(screen, partnerAni[frameIdx],
- 5, 0x50);
- g_system->unlockScreen();
- }
+ if (Graphics::Surface *screen = g_system->lockScreen()) {
+ blitPdaPartner(screen, _aniArchive, _partner,
+ kPdaGalleryPartner, g_system->getMillis());
+ g_system->unlockScreen();
}
g_system->updateScreen();
}
@@ -4955,7 +4905,8 @@ void EEMEngine::doAccuseFloppy() {
MAX<int>(8, (int)tw), alibi, 0);
}
// Stamp partner resting frame before KD reaction snapshots screen.
- blitAccusePartner(scene, _aniArchive, _partner, g_system->getMillis());
+ blitPdaPartner(scene, _aniArchive, _partner, kPdaGalleryPartner,
+ g_system->getMillis());
g_system->copyRectToScreen(scene.getPixels(), scene.pitch, 0, 0, 320, 200);
g_system->updateScreen();
@@ -5006,16 +4957,8 @@ void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
scratch.simpleBlitFrom(accuseBg.surface);
// Partner drawn first; defensive (no slot overlap).
- const uint partnerAnim = (_partner == 0) ? 2 : 0x10;
- Animation partnerAni;
- if (_aniArchive.loadAnimation(partnerAnim, partnerAni) &&
- !partnerAni.empty()) {
- const uint32 now = g_system->getMillis();
- const uint frameIdx = partnerFrameAtTick(0x02,
- (uint)partnerAni.size(), now);
- blitAnimFrameAnchored(scratch.surfacePtr(),
- partnerAni[frameIdx], 5, 0x50);
- }
+ blitPdaPartner(scratch, _aniArchive, _partner, kPdaGalleryPartner,
+ g_system->getMillis());
for (uint i = 0; i < numSuspects && i < Mystery::kGalleryCap; i++) {
slotRects[i] = Common::Rect();
Commit: 40c2459237d7641db42ef98a4222bd5b4b8b9192
https://github.com/scummvm/scummvm/commit/40c2459237d7641db42ef98a4222bd5b4b8b9192
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:14+02:00
Commit Message:
EEM: some comment fixes and clarifications
Changed paths:
engines/eem/clues.cpp
engines/eem/eem.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index fcb47cdb06d..03233dafb3f 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -541,7 +541,13 @@ void EEMEngine::doInitClues() {
// 0x83 _Partner == 0 ? "he" : "she"
// 0x84 _Partner == 0 ? "him" : "her"
// 0x85 _Partner == 0 ? "his" : "her"
-// 0x86..0x88 read a separate gender flag @ 0x7985 (TODO).
+// 0x86..0x88 mirror 0x83..0x85 but branch on a separate gender flag
+// (DAT_29be_7985, handlers @ 1b66:0ad2/0b41/0bb0). That flag has no
+// writers anywhere in EEMCD.EXE â only the three handlers read it â
+// and no shipping mystery text (CD or floppy) contains the 0x86/0x87
+// /0x88 bytes inside parseString-formatted strings. The opcodes are
+// dead in the original engine and dead in our data, so we drop them
+// silently (which matches the always-0 flag path in DOS: he/him/his).
// 0x89 KD hint placeholder (caller handles).
Common::String EEMEngine::parseString(const Common::String &raw,
const Common::String &playerName,
@@ -572,8 +578,12 @@ Common::String EEMEngine::parseString(const Common::String &raw,
case 0x86:
case 0x87:
case 0x88:
+ // Stubbed suspect-gender pronouns; the DOS flag they branch
+ // on is never written and no mystery msg uses these bytes
+ // (see jumptable comment above).
+ break;
case 0x89:
- // Eaten silently â see comment above.
+ // KD hint placeholder (caller handles before this point).
break;
case 0:
return out;
@@ -602,7 +612,7 @@ Common::String EEMEngine::parseString(const Common::String &raw,
// `_DoWordWrap @ 1b66:04a7`, which advances past spaces at the
// start of every output line via `for (; str[last] == ' '; last++)`.
// ~60% of mystery-text strings carry 1-2 leading spaces in the data
- // (verified across all CD M*.BIN files); the original WordWrap
+ // of the original WordWrap
// discards them, so we do the same before the text reaches
// `Font::wordWrapText` (which only trims at wrap-induced line
// boundaries, not at start-of-input or after an embedded '\n').
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 89c89a88b48..088f8a8d5d4 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -1182,7 +1182,7 @@ Common::Error EEMEngine::loadGameStream(Common::SeekableReadStream *stream) {
SaveStateList EEMEngine::listProfiles() const {
// _findfirst("*.PLR") in screen8_handler @ 1c33:1012.
- // Filter out slot 0 (ScummVM autosave) to match the original which
+ // Filter out slot 0 (autosave) to match the original which
// has no autosave concept.
SaveStateList saves = getMetaEngine()->listSaves(_targetName.c_str());
for (uint i = 0; i < saves.size(); ) {
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 0416fea7120..2d36b72f204 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -1993,7 +1993,7 @@ void EEMEngine::doCaseSelection() {
const uint idx = topRow + (uint)row;
if (idx >= listLen || solvedFlags[idx])
continue;
- // Second click on selected row = OK (ScummVM ergonomic).
+ // Second click on selected row = OK (QoL fix).
if (idx == selRow) {
confirmed = true;
break;
@@ -2854,8 +2854,7 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
// Layout differs by variant:
// * CD: fixed 0x46-byte stride, slot positions at `kGallerySlots`.
// * Floppy: variable-stride entries (5 + entry[4] bytes per
- // suspect), slot positions at `kFloppyGallerySlots` (verified
- // at `2608:0x16c`).
+ // suspect), slot positions at `kFloppyGallerySlots` (`2608:0x16c`).
const bool floppy = isFloppy();
const GallerySlot * const slots =
floppy ? kFloppyGallerySlots : kGallerySlots;
@@ -3228,7 +3227,7 @@ void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
continue;
// CD entries are 14 bytes: X at +4, Y at +6, crime at +12.
// Floppy entries are 11 bytes: X at +6, Y at +8, recolor at +10.
- // Floppy layout verified at `FUN_1fed_07ed` (BigMap iteration):
+ // Floppy layout: `FUN_1fed_07ed` (BigMap iteration):
// `*(int *)(pcVar2 + i*0xb + 7)` (= entry+6, X u16)
// `*(int *)(pcVar2 + i*0xb + 9)` (= entry+8, Y u16)
// `pcVar2[i*0xb + 0xb]` (= entry+10, recolor flag â non-zero
Commit: 0a11cfeeded82cb19bf471d0a78ec6525cb4e4fb
https://github.com/scummvm/scummvm/commit/0a11cfeeded82cb19bf471d0a78ec6525cb4e4fb
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:14+02:00
Commit Message:
EEM: use named constants here and there
Changed paths:
engines/eem/clues.cpp
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/graphics.cpp
engines/eem/site.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 03233dafb3f..ee88f8a8a43 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -130,8 +130,8 @@ void blitMaskedToScreen(const Picture &p, int x, int y) {
}
void blitRawToScreen(const Picture &p, int x, int y) {
- const int w = MIN<int>(p.surface.w, 320 - x);
- const int h = MIN<int>(p.surface.h, 200 - y);
+ const int w = MIN<int>(p.surface.w, kScreenWidth - x);
+ const int h = MIN<int>(p.surface.h, kScreenHeight - y);
if (x < 0 || y < 0 || w <= 0 || h <= 0)
return;
@@ -219,18 +219,18 @@ void EEMEngine::doChoosePartner() {
}
}
if (ev.type == Common::EVENT_LBUTTONDOWN) {
- _partner = (ev.mouse.x >= 160) ? 0 : 1;
+ _partner = (ev.mouse.x >= 160) ? kPartnerJake : kPartnerJenny;
debugC(1, kDebugGeneral, "Partner picked: %s",
- _partner == 0 ? "Jake" : "Jennifer");
+ _partner == kPartnerJake ? "Jake" : "Jennifer");
done = true;
break;
}
if (ev.type == Common::EVENT_KEYDOWN) {
if (ev.kbd.keycode == Common::KEYCODE_LEFT) {
- _partner = 1; done = true; break;
+ _partner = kPartnerJenny; done = true; break;
}
if (ev.kbd.keycode == Common::KEYCODE_RIGHT) {
- _partner = 0; done = true; break;
+ _partner = kPartnerJake; done = true; break;
}
if (ev.kbd.keycode == Common::KEYCODE_RETURN ||
ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
@@ -255,7 +255,7 @@ void EEMEngine::doChoosePartner() {
_audio->playFloppyVoiceSlot(0x14, _partner);
} else {
_audio->playVoc(Common::Path(
- (_partner == 0) ? "JAKE.VOC" : "JEN.VOC"));
+ (_partner == kPartnerJake) ? "JAKE.VOC" : "JEN.VOC"));
}
_audio->waitForVoiceDone();
}
@@ -306,8 +306,8 @@ void EEMEngine::doInitClues() {
if (haveBriefingBg)
blitAt(bg, 0, 0);
- const uint gameAni = _partner == 0 ? 0x17 : 0x3b;
- const uint bookAni = _partner == 0 ? 0x18 : 0x3c;
+ const uint gameAni = _partner == kPartnerJake ? 0x17 : 0x3b;
+ const uint bookAni = _partner == kPartnerJake ? 0x18 : 0x3c;
Animation game, book, nancy;
const bool haveGame = _aniArchive.loadAnimation(gameAni, game) && !game.empty();
const bool haveBook = _aniArchive.loadAnimation(bookAni, book) && !book.empty();
@@ -384,7 +384,7 @@ void EEMEngine::doInitClues() {
// clearing registered animations. Width is in mode-X cols (0x28 = 160px).
// Intentionally drops the right-side game anim; _PlayInSequence redraws
// that character over a clean BG next.
- Graphics::ManagedSurface briefingBase(320, 200,
+ Graphics::ManagedSurface briefingBase(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
briefingBase.clear();
if (haveBriefingBg) {
@@ -442,7 +442,7 @@ void EEMEngine::doInitClues() {
// caseType 3 -> 0x3d @ (0xcd, 0x6c)
uint16 seqAni = 0xFFFF;
uint16 seqY = 0x6c;
- if (_partner == 0) {
+ if (_partner == kPartnerJake) {
switch (caseType) {
case 1:
seqAni = 0x38;
@@ -482,7 +482,7 @@ void EEMEngine::doInitClues() {
const Picture &fr = seq[frame];
g_system->copyRectToScreen(briefingBase.getPixels(),
briefingBase.pitch, 0, 0,
- 320, 200);
+ kScreenWidth, kScreenHeight);
// _PlayInSequence @ 172b:2d35-2d50:
// dstX = sx - cell[+0x8] ; signed X anchor (miscflags)
// dstY = sy - cell[+0x6] ; signed Y anchor (rowoff)
@@ -667,7 +667,7 @@ void EEMEngine::displayClue(const byte *clueBlock) {
return;
// Snapshot BG so per-entry character pics don't stack.
- Graphics::ManagedSurface bg(320, 200,
+ Graphics::ManagedSurface bg(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
bg.clear();
{
@@ -692,7 +692,7 @@ void EEMEngine::displayClue(const byte *clueBlock) {
// bubText1 != -1; otherwise falls back to partner 0 fields entirely.
// Partner 0 always uses field 0.
for (uint i = 0; i < number && !shouldQuit(); i++) {
- g_system->copyRectToScreen(bg.getPixels(), bg.pitch, 0, 0, 320, 200);
+ g_system->copyRectToScreen(bg.getPixels(), bg.pitch, 0, 0, kScreenWidth, kScreenHeight);
const byte *c = clueBlock + 4 + i * 62;
// _DisplayClue @ 2404:0635-064b: _DoKDAnim(num) runs before the
@@ -702,10 +702,10 @@ void EEMEngine::displayClue(const byte *clueBlock) {
playKdAnim((uint16)kdAnimNum);
// _UpdateAnimations @ 172b:09c1 reactivates the wait anim.
g_system->copyRectToScreen(bg.getPixels(), bg.pitch,
- 0, 0, 320, 200);
+ 0, 0, kScreenWidth, kScreenHeight);
}
- const bool useP1 = (_partner == 1) &&
+ const bool useP1 = (_partner == kPartnerJenny) &&
(READ_LE_UINT16(c + 10) != 0xFFFF);
const uint partner = useP1 ? 1 : 0;
const uint16 textOff = READ_LE_UINT16(c + 8 + partner * 2);
@@ -726,7 +726,7 @@ void EEMEngine::displayClue(const byte *clueBlock) {
if (charPicId != 0 && charPicId != 0xFFFF) {
Picture charPic;
if (_picsArchive.getPicture(charPicId, charPic) &&
- charX < 320 && charY < 200) {
+ charX < kScreenWidth && charY < kScreenHeight) {
blitMaskedToScreen(charPic, charX, charY);
}
}
@@ -746,20 +746,20 @@ void EEMEngine::displayClue(const byte *clueBlock) {
Graphics::Surface *screen = g_system->lockScreen();
if (!screen)
break;
- Graphics::ManagedSurface scratch(320, 200,
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.simpleBlitFrom(*screen);
g_system->unlockScreen();
int textX = bubX;
int textY = bubY;
- int textW = MIN<int>(320 - bubX, 200);
+ int textW = MIN<int>(kScreenWidth - bubX, 200);
int copyY = bubY;
int copyH = _font.getFontHeight() * 4 + 8;
if (haveBalloon) {
- const int bw = MIN<int>(balloon.surface.w, 320 - bubX);
- const int bh = MIN<int>(balloon.surface.h, 200 - bubY);
+ const int bw = MIN<int>(balloon.surface.w, kScreenWidth - bubX);
+ const int bh = MIN<int>(balloon.surface.h, kScreenHeight - bubY);
// _AddPicBackground: transparent colour = pic->miscflags >> 8.
const byte transp = (byte)(balloon.flags >> 8);
// _GetBalloon @ 172b:1d7d mirrors horizontally when
@@ -785,8 +785,8 @@ void EEMEngine::displayClue(const byte *clueBlock) {
copyH = bh;
} else {
// No balloon: clear band.
- const Common::Rect band(0, bubY, 320,
- MIN<int>(bubY + copyH, 200));
+ const Common::Rect band(0, bubY, kScreenWidth,
+ MIN<int>(bubY + copyH, kScreenHeight));
scratch.fillRect(band, 0);
copyY = bubY;
}
@@ -797,8 +797,8 @@ void EEMEngine::displayClue(const byte *clueBlock) {
MAX<int>(8, textW), text, 0);
g_system->copyRectToScreen(scratch.getBasePtr(0, copyY),
- scratch.pitch, 0, copyY, 320,
- MIN<int>(copyH, 200 - copyY));
+ scratch.pitch, 0, copyY, kScreenWidth,
+ MIN<int>(copyH, kScreenHeight - copyY));
g_system->updateScreen();
}
@@ -814,7 +814,7 @@ void EEMEngine::displayClue(const byte *clueBlock) {
const uint16 voiceJenny = READ_LE_UINT16(c + 0x18);
if (voiceJenny != 0 && voiceJenny != 0xFFFF) {
const uint16 voiceJake = READ_LE_UINT16(c + 0x1a);
- const uint16 voice = (_partner == 0) ? voiceJake : voiceJenny;
+ const uint16 voice = (_partner == kPartnerJake) ? voiceJake : voiceJenny;
if (voice != 0 && voice != 0xFFFF)
_audio->spoolSound((uint)(voice - 1));
}
@@ -937,7 +937,7 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
return;
// Snapshot BG for between-bubble restores.
- Graphics::ManagedSurface bg(320, 200,
+ Graphics::ManagedSurface bg(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
{
Graphics::Surface *screen = g_system->lockScreen();
@@ -1010,7 +1010,7 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
const uint32 noteAbs = notesBase + (uint32)idx * 7;
if (noteAbs + 6 > dsz)
continue;
- const uint16 textOff = (_partner == 0)
+ const uint16 textOff = (_partner == kPartnerJake)
? READ_LE_UINT16(notes + idx * 7 + 2)
: READ_LE_UINT16(notes + idx * 7 + 4);
if (textOff >= dsz)
@@ -1074,7 +1074,7 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
const uint32 noteAbs = notesBase + (uint32)idx * 7;
if (noteAbs + 6 > dsz)
break;
- const uint16 textOff = (_partner == 0)
+ const uint16 textOff = (_partner == kPartnerJake)
? READ_LE_UINT16(notes + idx * 7 + 2)
: READ_LE_UINT16(notes + idx * 7 + 4);
if (textOff >= dsz)
@@ -1088,7 +1088,7 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
parseString(raw, _playerName, _partner);
// Render this text page.
- Graphics::ManagedSurface scratch(320, 200,
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.simpleBlitFrom(*bg.surfacePtr());
@@ -1163,7 +1163,7 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
}
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
+ 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
if (waitNeeded) {
@@ -1230,7 +1230,7 @@ void EEMEngine::displayFloppyHotspotDialog(uint siteNum, uint hotIdx) {
return;
// Snapshot clean site BG and restore between main + continuation calls
// so each displayFloppyDialogRecords sees a bubble-free background.
- Graphics::ManagedSurface siteBG(320, 200,
+ Graphics::ManagedSurface siteBG(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
{
Graphics::Surface *screen = g_system->lockScreen();
@@ -1263,7 +1263,7 @@ void EEMEngine::displayFloppyHotspotDialog(uint siteNum, uint hotIdx) {
return;
// Wipe main bubble so the continuation chain snapshots a clean BG.
g_system->copyRectToScreen(siteBG.getPixels(), siteBG.pitch,
- 0, 0, 320, 200);
+ 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
// _DisplayDialogContinuations_Floppy @ 1652:006c: lastIndicator=0
// means no indicator on the final continuation.
@@ -1272,7 +1272,7 @@ void EEMEngine::displayFloppyHotspotDialog(uint siteNum, uint hotIdx) {
bool EEMEngine::areYouSure() {
Graphics::Surface *screen = g_system->lockScreen();
- Graphics::ManagedSurface saved(320, 200,
+ Graphics::ManagedSurface saved(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
if (screen) {
saved.simpleBlitFrom(*screen);
@@ -1299,8 +1299,8 @@ bool EEMEngine::areYouSure() {
int noY = 0;
if (haveOriginalDialog) {
- const int x = (320 - dialogPic.surface.w) / 2;
- const int y = (200 - dialogPic.surface.h) / 2;
+ const int x = (kScreenWidth - dialogPic.surface.w) / 2;
+ const int y = (kScreenHeight - dialogPic.surface.h) / 2;
yesX = x + 0x0c;
yesY = y + 0x23;
noX = x + 0x60;
@@ -1315,7 +1315,7 @@ bool EEMEngine::areYouSure() {
noRect = Common::Rect(dlg.left + 100, dlg.top + 34,
dlg.left + 160, dlg.top + 54);
- Graphics::ManagedSurface scratch(320, 200,
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.simpleBlitFrom(saved);
scratch.fillRect(dlg, 0);
@@ -1323,14 +1323,14 @@ bool EEMEngine::areYouSure() {
_font.drawString(&scratch,
isSpanish() ? "Estas seguro que quieres salir?"
: "Are you sure you want to quit?",
- dlg.left + 8, dlg.top + 8, 320, 0xF);
+ dlg.left + 8, dlg.top + 8, kScreenWidth, 0xF);
_font.drawString(&scratch,
isSpanish() ? "S - Si" : "Y - Yes",
- dlg.left + 16, dlg.top + 36, 320, 0xF);
+ dlg.left + 16, dlg.top + 36, kScreenWidth, 0xF);
_font.drawString(&scratch, "N - No", dlg.left + 100,
- dlg.top + 36, 320, 0xF);
+ dlg.top + 36, kScreenWidth, 0xF);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
+ 0, 0, kScreenWidth, kScreenHeight);
} else {
return true;
}
@@ -1384,7 +1384,7 @@ bool EEMEngine::areYouSure() {
g_system->delayMillis(15);
}
- g_system->copyRectToScreen(saved.getPixels(), saved.pitch, 0, 0, 320, 200);
+ g_system->copyRectToScreen(saved.getPixels(), saved.pitch, 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
return result;
}
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 088f8a8d5d4..5118851df06 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -222,7 +222,7 @@ void EEMEngine::applyStartupTestOverrides() {
Common::Error EEMEngine::run() {
// _SetMode13X @ 1000:0358 â VGA mode 13h.
- initGraphics(320, 200);
+ initGraphics(kScreenWidth, kScreenHeight);
if (!openArchives())
return Common::Error(Common::kReadingFailed, "EEM archive open failed");
@@ -760,8 +760,8 @@ void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs,
void EEMEngine::blitAt(const Picture &pic, int x, int y) {
// Clip against the 320x200 frame buffer.
- const int w = MIN<int>(pic.surface.w, 320 - x);
- const int h = MIN<int>(pic.surface.h, 200 - y);
+ const int w = MIN<int>(pic.surface.w, kScreenWidth - x);
+ const int h = MIN<int>(pic.surface.h, kScreenHeight - y);
if (w <= 0 || h <= 0)
return;
g_system->copyRectToScreen(pic.surface.getPixels(), pic.surface.pitch,
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 8ba5a135353..8c06aa64cb9 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -104,6 +104,17 @@ enum Variant {
kVariantFloppy = 1,
};
+/// `_Partner @ 29be:7918`. Selected at the partner-pick screen
+/// (_DoChoosePartner @ 1a35:0756) and persisted in the player profile.
+enum Partner {
+ kPartnerJake = 0,
+ kPartnerJenny = 1,
+};
+
+/// VGA mode 13h dimensions (initGraphics(320, 200) in `EEMEngine::run`).
+constexpr int kScreenWidth = 320;
+constexpr int kScreenHeight = 200;
+
class EEMEngine : public Engine {
public:
EEMEngine(OSystem *syst, const ADGameDescription *gameDesc);
diff --git a/engines/eem/graphics.cpp b/engines/eem/graphics.cpp
index 544e66c7f50..b80eaa85730 100644
--- a/engines/eem/graphics.cpp
+++ b/engines/eem/graphics.cpp
@@ -256,7 +256,7 @@ void EEMEngine::doHelp() {
Common::String text = parseString(Common::String(txt),
_playerName, _partner);
balloonIdx = fitBalloonToText((uint16)balloonIdx, text) & 0x7F;
- Graphics::ManagedSurface ms(320, 200,
+ Graphics::ManagedSurface ms(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
ms.clear();
{
@@ -284,7 +284,7 @@ void EEMEngine::doHelp() {
getBalloonInsets(balloonIdx, bx, by, bw);
_font.drawWordWrapped(&ms, 0x21 + bx, balloonY + by,
MAX<int>(8, (int)bw), text, 0);
- g_system->copyRectToScreen(ms.getPixels(), ms.pitch, 0, 0, 320, 200);
+ g_system->copyRectToScreen(ms.getPixels(), ms.pitch, 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
while (!shouldQuit()) {
@@ -398,7 +398,7 @@ void EEMEngine::doHelp() {
//
// BG is the caller's CURRENT screen (site / PDA / gallery), not a cleared
// scratch.
- Graphics::ManagedSurface ms(320, 200,
+ Graphics::ManagedSurface ms(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
ms.clear();
{
@@ -445,7 +445,7 @@ void EEMEngine::doHelp() {
haveBalloon ? 0 : 0xF);
g_system->copyRectToScreen(ms.getPixels(), ms.pitch,
- 0, 0, 320, 200);
+ 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
// _DisplayHint @ 1560:0009 plays _SayKDDigital(soundnum) â a
@@ -476,7 +476,7 @@ void EEMEngine::doInterfaceHelp(uint num) {
num, kHelpPics[num][0], kHelpPics[num][1]);
// Snapshot caller's screen once: each PIC overlays the same clean BG.
- Graphics::ManagedSurface bg(320, 200,
+ Graphics::ManagedSurface bg(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
{
Graphics::Surface *cur = g_system->lockScreen();
@@ -501,14 +501,14 @@ void EEMEngine::doInterfaceHelp(uint num) {
// transBlitFrom transp = pic.flags >> 8 matches _Rect_Move_Mask param_10
// @ 1000:03fc. Explicit (0,0) destPos: no-arg overload stretches to fill.
- Graphics::ManagedSurface scratch(320, 200,
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.simpleBlitFrom(bg);
const byte transp = (byte)(pic.flags >> 8);
scratch.transBlitFrom(pic.surface, Common::Point(0, 0),
(uint32)transp);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
+ 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
bool escape = false;
@@ -548,8 +548,8 @@ void EEMEngine::doInterfaceHelp(uint num) {
}
void EEMEngine::setPartnerEraseBg(const Graphics::ManagedSurface *bg) {
- if (bg && bg->w == 320 && bg->h == 200) {
- _partnerEraseBg.create(320, 200,
+ if (bg && bg->w == kScreenWidth && bg->h == kScreenHeight) {
+ _partnerEraseBg.create(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
_partnerEraseBg.simpleBlitFrom(*bg);
} else {
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index b0b5a2e1226..d523b1e3f5b 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -987,7 +987,7 @@ bool SiteScreen::enterSiteAnim() {
Graphics::Surface *screen = g_system->lockScreen();
if (!screen)
return false;
- Graphics::ManagedSurface bg(320, 200,
+ Graphics::ManagedSurface bg(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
bg.simpleBlitFrom(*screen);
g_system->unlockScreen();
@@ -995,10 +995,10 @@ bool SiteScreen::enterSiteAnim() {
// Phase 1 â skateboard scroll.
Animation skate;
if (_vm->getAni().loadAnimation(kSkateAni, skate) && !skate.empty()) {
- // `iVar4 = 199 - sprite_h`, `uVar5 = 320 - sprite_w` (frame 0).
+ // `iVar4 = 199 - sprite_h`, `uVar5 = kScreenWidth - sprite_w` (frame 0).
const int spriteH = skate[0].surface.h;
const int spriteW = skate[0].surface.w;
- int x = (320 - spriteW) & ~3; // 4-px aligned (mode-X)
+ int x = (kScreenWidth - spriteW) & ~3; // 4-px aligned (mode-X)
const int y = 199 - spriteH;
const byte transp = (byte)(skate[0].flags >> 8);
uint frameIdx = 0;
@@ -1007,12 +1007,12 @@ bool SiteScreen::enterSiteAnim() {
const int kFrameTicks = 0xc; // original switches frame at 12 px
while (x + spriteW > 0 && !_vm->shouldQuit()) {
- Graphics::ManagedSurface scratch(320, 200,
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.simpleBlitFrom(bg);
blitFrame(scratch, skate[frameIdx], x, y, transp);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
+ 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
Common::Event ev;
@@ -1048,12 +1048,12 @@ bool SiteScreen::enterSiteAnim() {
const int destX = -(int)(int16)fr.miscflags;
const int destY = kKDY - (int)(int16)fr.rowoff;
- Graphics::ManagedSurface scratch(320, 200,
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.simpleBlitFrom(bg);
blitFrame(scratch, fr, destX, destY, transp);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
+ 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
Common::Event ev;
@@ -1267,7 +1267,7 @@ void SiteScreen::applyColorCycles() {
}
void SiteScreen::captureBgSnapshot() {
- _bgSnapshot.create(320, 200, Graphics::PixelFormat::createFormatCLUT8());
+ _bgSnapshot.create(kScreenWidth, kScreenHeight, Graphics::PixelFormat::createFormatCLUT8());
Graphics::Surface *screen = g_system->lockScreen();
if (!screen) {
_snapshotSite = -1;
@@ -1278,10 +1278,10 @@ void SiteScreen::captureBgSnapshot() {
}
void SiteScreen::restoreBgSnapshot() {
- if (_bgSnapshot.w != 320 || _bgSnapshot.h != 200)
+ if (_bgSnapshot.w != kScreenWidth || _bgSnapshot.h != kScreenHeight)
return;
g_system->copyRectToScreen(_bgSnapshot.getPixels(), _bgSnapshot.pitch,
- 0, 0, 320, 200);
+ 0, 0, kScreenWidth, kScreenHeight);
}
void SiteScreen::renderPartner(uint siteNum, uint32 tickMs) {
@@ -1401,8 +1401,8 @@ void SiteScreen::renderBackground(uint siteNum) {
// `_Rect_Move(0, 0, h, ..., 0x42, 0x14, 48000, h, w)`.
const int x = 0x42;
const int y = 0x14;
- const int w = MIN<int>(scene.surface.w, 320 - x);
- const int h = MIN<int>(scene.surface.h, 200 - y);
+ const int w = MIN<int>(scene.surface.w, kScreenWidth - x);
+ const int h = MIN<int>(scene.surface.h, kScreenHeight - y);
if (w > 0 && h > 0)
g_system->copyRectToScreen(scene.surface.getPixels(),
scene.surface.pitch, x, y, w, h);
@@ -1635,7 +1635,7 @@ void EEMEngine::playKdAnim(uint16 num) {
if (num >= ARRAYSIZE(kKdAnimTable))
return;
- const uint partner = (_partner == 0) ? 0 : 1;
+ const uint partner = (_partner == kPartnerJake) ? 0 : 1;
const uint16 animId = kKdAnimTable[num][partner];
const int px = (int)kKdAnimTable[num][2 + partner];
const int py = (int)kKdAnimTable[num][4 + partner];
@@ -1657,9 +1657,9 @@ void EEMEngine::playKdAnim(uint16 num) {
// Erase-source: caller-stashed partner-less BG (via `setPartnerEraseBg`)
// or fall back to current screen (works for full-screen contexts).
- Graphics::ManagedSurface bg(320, 200,
+ Graphics::ManagedSurface bg(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
- if (_partnerEraseBg.w == 320 && _partnerEraseBg.h == 200) {
+ if (_partnerEraseBg.w == kScreenWidth && _partnerEraseBg.h == kScreenHeight) {
bg.simpleBlitFrom(_partnerEraseBg);
} else {
Graphics::Surface *screen = g_system->lockScreen();
@@ -1676,7 +1676,7 @@ void EEMEngine::playKdAnim(uint16 num) {
const Picture &fr = anim[frameIdx];
const byte transp = (byte)(fr.flags >> 8);
- Graphics::ManagedSurface scratch(320, 200,
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.simpleBlitFrom(bg);
// Anchor-aware: kdAnim cells (0x03/0x04/0x0c/0x0d ...) have
@@ -1687,7 +1687,7 @@ void EEMEngine::playKdAnim(uint16 num) {
(void)transp; // anchored blitter recomputes from p.flags
blitAnimFrameAnchored(scratch.surfacePtr(), fr, px, py);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
+ 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
// 100 ms per frame (~10 fps). Pump updateScreen inside the wait
@@ -1706,7 +1706,7 @@ void EEMEngine::playKdAnim(uint16 num) {
}
}
- g_system->copyRectToScreen(bg.getPixels(), bg.pitch, 0, 0, 320, 200);
+ g_system->copyRectToScreen(bg.getPixels(), bg.pitch, 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
}
} // End of namespace EEM
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 2d36b72f204..507d27d0506 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -100,8 +100,8 @@ void swapColors(Graphics::ManagedSurface &dst,
const Common::Rect &r, byte from, byte to) {
const int x1 = MAX<int>(0, r.left);
const int y1 = MAX<int>(0, r.top);
- const int x2 = MIN<int>(320, r.right);
- const int y2 = MIN<int>(200, r.bottom);
+ const int x2 = MIN<int>(kScreenWidth, r.right);
+ const int y2 = MIN<int>(kScreenHeight, r.bottom);
for (int y = y1; y < y2; y++) {
byte *row = (byte *)dst.getBasePtr(0, y);
for (int x = x1; x < x2; x++) {
@@ -157,8 +157,8 @@ void blitPdaPartner(Graphics::Surface *screen, DBDArchive &aniArchive,
blitAnimFrameAnchored(screen, *fr, spec.anchorX, spec.anchorY);
}
-constexpr Common::Rect kEndingPrevPageRect(Common::Point(0, 0), 28, 200);
-constexpr Common::Rect kEndingNextPageRect(Common::Point(292, 0), 28, 200);
+constexpr Common::Rect kEndingPrevPageRect(Common::Point(0, 0), 28, kScreenHeight);
+constexpr Common::Rect kEndingNextPageRect(Common::Point(292, 0), 28, kScreenHeight);
constexpr uint16 kFloppyEndingBackgroundPic = 0x8b;
constexpr uint16 kFirstTryBadgePic = 0x205;
constexpr Common::Point kFirstTryBadgePos(0x1e, 9);
@@ -292,7 +292,7 @@ int nextLiveSlot(const Common::Array<Common::Rect> &slotRects,
void copyToScreen(Graphics::ManagedSurface &scratch) {
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
+ 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
}
@@ -357,13 +357,13 @@ void drawCaseBookTitle(Graphics::ManagedSurface &scratch, const EEMEngine *vm,
: Common::String::format(spanish ? "Lib. %u" : "Book %u", book);
const int titleW = vm->getFont().getStringWidth(title);
const int titleX = (0xba - titleW) / 2 + 0x3c;
- vm->getFont().drawString(&scratch, title, titleX, 12, 320, 0xF);
+ vm->getFont().drawString(&scratch, title, titleX, 12, kScreenWidth, 0xF);
}
void drawNameEntryFrame(EEMEngine *vm, const Picture *bg, bool haveBG,
const Picture *peek, const Common::String &name,
const char *prompt) {
- Graphics::ManagedSurface scratch(320, 200,
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
if (haveBG)
@@ -383,7 +383,7 @@ bool animateNameEntryPeek(EEMEngine *vm, const Picture *bg, bool haveBG,
for (int w = 1; w <= peek->surface.w; w++) {
if (pumpQuitEvents(vm))
return true;
- Graphics::ManagedSurface scratch(320, 200,
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
if (haveBG)
@@ -404,7 +404,7 @@ bool animateProfilePickerReveal(EEMEngine *vm, const Picture *bg,
for (int h = 1; h <= reveal->surface.h; h++) {
if (pumpQuitEvents(vm))
return true;
- Graphics::ManagedSurface scratch(320, 200,
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
if (haveBG)
@@ -451,7 +451,7 @@ void clampProfileScroll(int &selected, int &start, int count) {
}
void drawProfilePickerFrame(const ProfilePickerView &v) {
- Graphics::ManagedSurface scratch(320, 200,
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
if (v.haveBG)
@@ -584,7 +584,7 @@ bool animateCaseSelectionReveal(EEMEngine *vm, const Picture *caseBg,
for (int i = 1; i <= steps; i++) {
if (pumpQuitEvents(vm))
return true;
- Graphics::ManagedSurface scratch(320, 200,
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
if (haveCaseBg && caseBg)
@@ -610,7 +610,7 @@ bool animateCaseSelectionReveal(EEMEngine *vm, const Picture *caseBg,
// Original colours 0x13 sel / 0x1B greyed / 0x5C default approximated
// here as 0xF / 0x8 / 0x7 from site palette 0.
void drawCaseSubmenu(const CaseSubmenuView &v) {
- Graphics::ManagedSurface scratch(320, 200,
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
drawCaseBase(scratch, v.vm, v.caseBg, v.haveCaseBg,
v.revealPic, v.haveRevealPic,
@@ -668,7 +668,7 @@ void drawCaseSubmenu(const CaseSubmenuView &v) {
}
void drawActionMenuFrame(const ActionMenuView &v) {
- Graphics::ManagedSurface scratch(320, 200,
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
if (v.haveBg && v.bg)
@@ -946,7 +946,7 @@ void EEMEngine::doNewPlayer() {
_playerName = name;
memset(_mysteriesSolved, 0, sizeof(_mysteriesSolved));
_mystery.clear();
- _partner = 0;
+ _partner = kPartnerJake;
// `_NewPlayer @ 1c33:0fa3`: DAT_2d5d_3f99 = 1 (Junior).
_chainStage = 1;
saveProfile(name);
@@ -1109,7 +1109,7 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
uint16 y1 = 0;
uint16 x2 = 0;
const char *raw = nullptr;
- Graphics::ManagedSurface scratch(320, 200,
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
@@ -1170,13 +1170,13 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
// TINY.FNT + color 0 (asm 1df2:04cf â not 0xF as Ghidra shows).
const EEMFont &renderFont = haveTinyFont ? tinyFont : _font;
if (renderFont.isLoaded() && x2 > x1) {
- const int textW = MIN<int>((int)x2 - (int)x1, 320 - (int)x1);
+ const int textW = MIN<int>((int)x2 - (int)x1, kScreenWidth - (int)x1);
renderFont.drawWordWrapped(&scratch, (int)x1, (int)y1,
textW, text, 0);
}
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
+ 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
dirty = false;
}
@@ -1388,20 +1388,20 @@ void EEMEngine::doSetup() {
// Partner toggle [0]. Direct Jake/Jenny label clicks are a
// ScummVM-only fallback.
if (kPartnerBtn.contains(mx, my)) {
- _partner = _partner == 0 ? 1 : 0;
+ _partner = _partner == kPartnerJake ? kPartnerJenny : kPartnerJake;
dirty = true;
continue;
}
if (kKid1Rect.contains(mx, my)) {
- if (_partner != 0) {
- _partner = 0;
+ if (_partner != kPartnerJake) {
+ _partner = kPartnerJake;
dirty = true;
}
continue;
}
if (kKid2Rect.contains(mx, my)) {
- if (_partner != 1) {
- _partner = 1;
+ if (_partner != kPartnerJenny) {
+ _partner = kPartnerJenny;
dirty = true;
}
continue;
@@ -1531,7 +1531,7 @@ void EEMEngine::doSetup() {
}
void EEMEngine::setupDrawScreen() {
- Graphics::ManagedSurface scratch(320, 200,
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
Picture bg;
@@ -1542,16 +1542,16 @@ void EEMEngine::setupDrawScreen() {
const byte kBright = 0x15;
const byte kDim = 0x00;
swapColors(scratch, kSetupKid1Rect, kKey,
- _partner == 0 ? kBright : kDim);
+ _partner == kPartnerJake ? kBright : kDim);
swapColors(scratch, kSetupKid2Rect, kKey,
- _partner == 1 ? kBright : kDim);
+ _partner == kPartnerJenny ? kBright : kDim);
swapColors(scratch, kSetupSoundOnRect, kKey,
_voiceOn ? kBright : kDim);
swapColors(scratch, kSetupSoundOffRect, kKey,
_voiceOn ? kDim : kBright);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
+ 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
}
@@ -1564,7 +1564,7 @@ Common::KeyCode EEMEngine::setupShowFullscreenPic(uint16 picId, bool transparent
warning("doSetup: PIC %u missing", (uint)picId);
return Common::KEYCODE_INVALID;
}
- Graphics::ManagedSurface scratch(320, 200,
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
if (transparent) {
Graphics::Surface *cur = g_system->lockScreen();
@@ -1581,7 +1581,7 @@ Common::KeyCode EEMEngine::setupShowFullscreenPic(uint16 picId, bool transparent
scratch.simpleBlitFrom(pic.surface);
}
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
+ 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
while (!shouldQuit()) {
Common::Event ev;
@@ -1866,7 +1866,7 @@ void EEMEngine::doCaseSelection() {
_picsArchive.getPicture(kCaseSelectionRevealPic, revealPic);
// `_CaseSelection @ 1c33:0a87` greeter ANI 0x15 (Jake) / 0x16 (Jenny).
- const uint kKdAniId = (_partner == 0) ? 0x15 : 0x16;
+ const uint kKdAniId = (_partner == kPartnerJake) ? 0x15 : 0x16;
Animation kdAnim;
const bool haveKdAnim = _aniArchive.loadAnimation(kKdAniId, kdAnim)
&& !kdAnim.empty();
@@ -2285,7 +2285,7 @@ void EEMEngine::drawNotebookFrame(int &page) {
// from `_DoNotebook @ 161e:0500`.
const Common::Rect kNotebookRect(78, 12, 288, 152);
- Graphics::ManagedSurface scratch(320, 200,
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
@@ -2384,10 +2384,10 @@ void EEMEngine::drawNotebookFrame(int &page) {
// Page indicator only (original has no points display).
_font.drawString(&scratch, Common::String::format("p%d/%d",
page + 1, (int)pageStarts.size()),
- 270, 4, 320, 0x5C);
+ 270, 4, kScreenWidth, 0x5C);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
+ 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
// Publish slot info to `doNotebook`'s click handler.
@@ -2592,7 +2592,7 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
bool isFirstShow = true;
while (!back && !shouldQuit()) {
- Graphics::ManagedSurface ms(320, 200,
+ Graphics::ManagedSurface ms(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
ms.clear();
if (haveBg)
@@ -2688,7 +2688,7 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
rx, ry + rh + 2, MAX<int>(8, rw), 0x3C);
}
g_system->copyRectToScreen(ms.getPixels(), ms.pitch,
- 0, 0, 320, 200);
+ 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
// Drain the LBUTTONDOWN that opened MoreInfo (first page only).
@@ -2833,7 +2833,7 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
Picture galBg;
const bool haveBg = _picsArchive.getPicture(0x3f, galBg);
- Graphics::ManagedSurface scratch(320, 200,
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
@@ -2882,8 +2882,8 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
const int placeX = s.x;
const int placeY = s.y + (0x48 - portrait.surface.h);
- const int w = MIN<int>(portrait.surface.w, 320 - placeX);
- const int h = MIN<int>(portrait.surface.h, 200 - placeY);
+ const int w = MIN<int>(portrait.surface.w, kScreenWidth - placeX);
+ const int h = MIN<int>(portrait.surface.h, kScreenHeight - placeY);
if (w <= 0 || h <= 0)
continue;
scratch.transBlitFrom(portrait.surface,
@@ -2898,7 +2898,7 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
const int phH = 0x48;
const int phX = s.x;
const int phY = s.y;
- if (phX + phW <= 320 && phY + phH <= 200) {
+ if (phX + phW <= kScreenWidth && phY + phH <= kScreenHeight) {
scratch.fillRect(Common::Rect(phX, phY,
phX + phW, phY + phH), 0x20);
scratch.frameRect(Common::Rect(phX, phY,
@@ -2913,7 +2913,7 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
}
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
+ 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
}
@@ -3032,7 +3032,7 @@ void EEMEngine::doBigMap() {
bool returnToOverview = false;
// `SmallMapButtons[4]` @ 20fe:156c â return to overview.
- const Common::Rect kBigMapReturnRect(252, 43, 320, 200);
+ const Common::Rect kBigMapReturnRect(252, 43, kScreenWidth, kScreenHeight);
const Common::Rect kArrowYUp(237, 2, 247, 11);
const Common::Rect kArrowYDown(237, 163, 247, 172);
const Common::Rect kArrowXLeft(2, 175, 12, 185);
@@ -3201,7 +3201,7 @@ void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
// PIC 0x42 + per-site Done/Crime/Site marker (`_DrawBigMapButtons @
// 20fe:0877`) + partner idle at (0xfd, 0x50). Idle ANI: Jake=0x14,
// Jenny=0x12. elapsedMs anchors unfoldâwait timeline.
- Graphics::ManagedSurface scratch(320, 200,
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
@@ -3266,7 +3266,7 @@ void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
// Partner idle at (0xfd, 0x50). `_DoBigMap @ 20fe:0a47` always passes
// script 0x14 (count-up) to `_NewAnimation` regardless of partner.
- const uint kMapAniId = (_partner == 0) ? 0x14 : 0x12;
+ const uint kMapAniId = (_partner == kPartnerJake) ? 0x14 : 0x12;
Animation mapAnim;
if (_aniArchive.loadAnimation(kMapAniId, mapAnim) && !mapAnim.empty()) {
const uint frameIdx = bigMapPartnerFrameAtTick((uint)mapAnim.size(),
@@ -3277,7 +3277,7 @@ void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
}
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
+ 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
}
@@ -3293,7 +3293,7 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
const int kMapWinX = 2;
const int kMapWinY = 2;
- Graphics::ManagedSurface scratch(320, 200,
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
@@ -3347,7 +3347,7 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
}
// Always script 0x13 (`_NewAnimation @ _DoBigMap 20fe:0a47`).
- const uint kDetailAniId = (_partner == 0) ? 0x13 : 0x11;
+ const uint kDetailAniId = (_partner == kPartnerJake) ? 0x13 : 0x11;
Animation detailAnim;
if (_aniArchive.loadAnimation(kDetailAniId, detailAnim) &&
!detailAnim.empty()) {
@@ -3358,7 +3358,7 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
}
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
+ 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
}
@@ -3420,7 +3420,7 @@ void EEMEngine::accuseRebuildPagination(const AccuseNotesCtx &ctx) {
}
void EEMEngine::accuseDrawScreen(const AccuseNotesCtx &ctx) {
- Graphics::ManagedSurface scratch(320, 200,
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
if (ctx.haveBg)
@@ -3489,7 +3489,7 @@ void EEMEngine::accuseDrawScreen(const AccuseNotesCtx &ctx) {
}
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
+ 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
}
@@ -3747,7 +3747,7 @@ void EEMEngine::doAccuse() {
_playerName, _partner);
}
if (!entryText.empty()) {
- Graphics::ManagedSurface ms(320, 200,
+ Graphics::ManagedSurface ms(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
ms.clear();
Graphics::Surface *cur = g_system->lockScreen();
@@ -3781,7 +3781,7 @@ void EEMEngine::doAccuse() {
tw, entryText, haveBalloon ? 0 : 0xF);
}
g_system->copyRectToScreen(ms.getPixels(), ms.pitch,
- 0, 0, 320, 200);
+ 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
if (_audio)
_audio->sayKDDigital(entryKdIdx, entryKDSpeak, _partner);
@@ -3843,7 +3843,7 @@ void EEMEngine::doAccuse() {
// Balloon overlay (`_GetKDTextBalloon` + `_GetBalloon` +
// `_AddPicBackground` + `_WordWrap` @ 1df2:0c8d-0cd1).
- Graphics::ManagedSurface ms(320, 200,
+ Graphics::ManagedSurface ms(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
ms.clear();
Graphics::Surface *cur = g_system->lockScreen();
@@ -3880,7 +3880,7 @@ void EEMEngine::doAccuse() {
haveBalloon ? 0 : 0xF);
}
g_system->copyRectToScreen(ms.getPixels(), ms.pitch,
- 0, 0, 320, 200);
+ 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
// `_SayKDDigital(3)` @ 1df2:0cd9.
@@ -3938,7 +3938,7 @@ void EEMEngine::doAccuse() {
drawAccuseGallery(num, gd, /* highlighted= */ -1,
slotRects, slotSuspect);
- Graphics::ManagedSurface ms(320, 200,
+ Graphics::ManagedSurface ms(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
ms.clear();
{
@@ -3969,7 +3969,7 @@ void EEMEngine::doAccuse() {
haveBalloon ? 0 : 0xF);
}
g_system->copyRectToScreen(ms.getPixels(), ms.pitch,
- 0, 0, 320, 200);
+ 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
if (_audio)
_audio->sayKDDigital(kdIdx, 4, _partner);
@@ -4136,7 +4136,7 @@ void EEMEngine::doAccuse() {
if (bindx < 8) {
const int bw = haveBalloon ? balloon.surface.w : 0;
const int bh = haveBalloon ? balloon.surface.h : 0;
- balloonX = (320 - bw) / 2;
+ balloonX = (kScreenWidth - bw) / 2;
if (bh < 0x5a) {
balloonY = (0x5a - bh) / 2;
} else {
@@ -4150,7 +4150,7 @@ void EEMEngine::doAccuse() {
}
// `base` = BG + suspect + partner. Survives both balloon phases.
- Graphics::ManagedSurface base(320, 200,
+ Graphics::ManagedSurface base(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
base.clear();
if (haveAlibiBg)
@@ -4165,7 +4165,7 @@ void EEMEngine::doAccuse() {
// @ 1df2:0c30`). Drawn after suspect.
// scratch = base + alibi balloon/text + partner.
- Graphics::ManagedSurface scratch(320, 200,
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.simpleBlitFrom(base);
if (haveBalloon) {
@@ -4216,7 +4216,7 @@ void EEMEngine::doAccuse() {
// Suspect voice. talk = partner==0 ? gd[+0x6] : gd[+0x0] (1df2:0252).
// 1-based, so SpoolSound(talk-1).
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
+ 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
if (_audio && gd) {
const uint16 alibiVoice =
@@ -4224,7 +4224,7 @@ void EEMEngine::doAccuse() {
const uint16 jakeVoice =
READ_LE_UINT16(gd + (uint)picked * 0x46 + 0x06);
const uint16 talk =
- (_partner == 0) ? jakeVoice : alibiVoice;
+ (_partner == kPartnerJake) ? jakeVoice : alibiVoice;
if (talk != 0)
_audio->spoolSound((uint)(talk - 1));
}
@@ -4273,7 +4273,7 @@ void EEMEngine::doAccuse() {
blitPdaPartner(scratch, _aniArchive, _partner,
kPdaGalleryPartner, g_system->getMillis());
g_system->copyRectToScreen(scratch.getPixels(),
- scratch.pitch, 0, 0, 320, 200);
+ scratch.pitch, 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
if (_audio)
_audio->sayKDDigital(reactIdx, 5, _partner);
@@ -4338,7 +4338,7 @@ void EEMEngine::doAccuse() {
// (0x42, 0x14), palette = sitenum+1 = 6.
Graphics::Surface *blk = g_system->lockScreen();
if (blk) {
- memset(blk->getPixels(), 0, 320 * 200);
+ memset(blk->getPixels(), 0, kScreenWidth * kScreenHeight);
g_system->unlockScreen();
}
setSitePalette(6); // sitenum + 1 (`_GetPalette`).
@@ -4351,8 +4351,8 @@ void EEMEngine::doAccuse() {
if (5 < _sitesArchive.size() &&
_sitesArchive.loadEntry(5, scene)) {
const int sx = 0x42, sy = 0x14;
- const int sw = MIN<int>(scene.surface.w, 320 - sx);
- const int sh = MIN<int>(scene.surface.h, 200 - sy);
+ const int sw = MIN<int>(scene.surface.w, kScreenWidth - sx);
+ const int sh = MIN<int>(scene.surface.h, kScreenHeight - sy);
if (sw > 0 && sh > 0)
g_system->copyRectToScreen(scene.surface.getPixels(),
scene.surface.pitch, sx, sy,
@@ -4428,7 +4428,7 @@ void EEMEngine::floppyKDHint(uint kdSlot, const byte *kdIdx,
Common::String text =
parseString(Common::String(txt), _playerName, _partner);
balloonIdx = fitBalloonToText((uint16)balloonIdx, text) & 0x7F;
- Graphics::ManagedSurface ms(320, 200,
+ Graphics::ManagedSurface ms(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
Graphics::Surface *cur = g_system->lockScreen();
if (cur) {
@@ -4453,7 +4453,7 @@ void EEMEngine::floppyKDHint(uint kdSlot, const byte *kdIdx,
getBalloonInsets(balloonIdx, bx, by, bw);
_font.drawWordWrapped(&ms, 0x21 + bx, balloonY + by,
MAX<int>(8, (int)bw), text, 0);
- g_system->copyRectToScreen(ms.getPixels(), ms.pitch, 0, 0, 320, 200);
+ g_system->copyRectToScreen(ms.getPixels(), ms.pitch, 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
// Wait for click.
while (!shouldQuit()) {
@@ -4480,7 +4480,7 @@ void EEMEngine::accuseDrawGallery(int highlighted,
Common::Array<int> &suspects, uint8 num,
bool haveAccuseBg,
const Picture &accuseBg) {
- Graphics::ManagedSurface scratch(320, 200,
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
if (haveAccuseBg)
@@ -4526,7 +4526,7 @@ void EEMEngine::accuseDrawGallery(int highlighted,
}
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
+ 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
}
@@ -4687,7 +4687,7 @@ void EEMEngine::doAccuseFloppy() {
{
Graphics::Surface *blk = g_system->lockScreen();
if (blk) {
- memset(blk->getPixels(), 0, 320 * 200);
+ memset(blk->getPixels(), 0, kScreenWidth * kScreenHeight);
g_system->unlockScreen();
}
setSitePalette(6);
@@ -4701,8 +4701,8 @@ void EEMEngine::doAccuseFloppy() {
if (5 < _sitesArchive.size() &&
_sitesArchive.loadEntry(5, scene)) {
const int sx = 0x42, sy = 0x14;
- const int sw = MIN<int>(scene.surface.w, 320 - sx);
- const int sh = MIN<int>(scene.surface.h, 200 - sy);
+ const int sw = MIN<int>(scene.surface.w, kScreenWidth - sx);
+ const int sh = MIN<int>(scene.surface.h, kScreenHeight - sy);
if (sw > 0 && sh > 0)
g_system->copyRectToScreen(scene.surface.getPixels(),
scene.surface.pitch, sx, sy, sw, sh);
@@ -4867,7 +4867,7 @@ void EEMEngine::doAccuseFloppy() {
const bool flipBalloon = (balloonRaw & 0x80) != 0;
// Compose alibi screen.
- Graphics::ManagedSurface scene(320, 200,
+ Graphics::ManagedSurface scene(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scene.clear();
if (haveAlibiBg)
@@ -4885,7 +4885,7 @@ void EEMEngine::doAccuseFloppy() {
int balloonX = 0x21;
int balloonY = 1;
if (haveBalloon) {
- balloonX = (320 - balloon.surface.w) / 2;
+ balloonX = (kScreenWidth - balloon.surface.w) / 2;
balloonY = (0x5a - balloon.surface.h) / 2;
if (balloonX < 0)
balloonX = 0;
@@ -4906,7 +4906,7 @@ void EEMEngine::doAccuseFloppy() {
// Stamp partner resting frame before KD reaction snapshots screen.
blitPdaPartner(scene, _aniArchive, _partner, kPdaGalleryPartner,
g_system->getMillis());
- g_system->copyRectToScreen(scene.getPixels(), scene.pitch, 0, 0, 320, 200);
+ g_system->copyRectToScreen(scene.getPixels(), scene.pitch, 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
// No per-suspect VOC â alibi table @ 2608:0c5e is for post-win
@@ -4949,7 +4949,7 @@ void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
Picture accuseBg;
const bool haveAccuseBg = _picsArchive.getPicture(0x3f, accuseBg);
- Graphics::ManagedSurface scratch(320, 200,
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
if (haveAccuseBg)
@@ -4981,8 +4981,8 @@ void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
const int placeX = s.x;
const int placeY = s.y + (0x48 - portrait.surface.h);
- const int w = MIN<int>(portrait.surface.w, 320 - placeX);
- const int h = MIN<int>(portrait.surface.h, 200 - placeY);
+ const int w = MIN<int>(portrait.surface.w, kScreenWidth - placeX);
+ const int h = MIN<int>(portrait.surface.h, kScreenHeight - placeY);
if (w <= 0 || h <= 0)
continue;
scratch.transBlitFrom(portrait.surface,
@@ -5002,7 +5002,7 @@ void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
}
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, 320, 200);
+ 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
}
Commit: 9cd879dd5cd34cd252e7551b92553f9ce05290df
https://github.com/scummvm/scummvm/commit/9cd879dd5cd34cd252e7551b92553f9ce05290df
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:14+02:00
Commit Message:
EEM: removed useless usage of static
Changed paths:
engines/eem/audio.cpp
engines/eem/eem.cpp
engines/eem/graphics.cpp
engines/eem/metaengine.cpp
engines/eem/music.cpp
engines/eem/site.cpp
diff --git a/engines/eem/audio.cpp b/engines/eem/audio.cpp
index 3954fbb02f9..10df7170c3c 100644
--- a/engines/eem/audio.cpp
+++ b/engines/eem/audio.cpp
@@ -186,7 +186,7 @@ void AudioPlayer::playPcmBuffer(byte *pcm, uint32 size, uint sampleRate,
// Filename FAR-ptr arrays at 2608:0f0e (Jake) and 2608:0f76 (Jenny), each
// 26 * FAR-ptr to a NUL-terminated `*.voc` name. Slots align across partners,
// e.g. 12 = PHONESL.VOC, 20 = partner intro, 25 = THUNDER.VOC.
-static const char *const kFloppyJakeVoiceTable[26] = {
+const char *const kFloppyJakeVoiceTable[26] = {
"DING.VOC", "M-0083SL.VOC", "M-0085SL.VOC", "NEWSCAN.VOC",
"M-0089SL.VOC", "M-0091SL.VOC", "M-0092SL.VOC", "NEWSSHRT.VOC",
"M-0096SL.VOC", "M-0102SL.VOC", "M-0104SL.VOC", "M-0107SL.VOC",
@@ -195,7 +195,7 @@ static const char *const kFloppyJakeVoiceTable[26] = {
"M-0113SL.VOC", "B-0006SL.VOC", "B-0003SL.VOC", "B-0004SL.VOC",
"M-0163SL.VOC", "THUNDER.VOC",
};
-static const char *const kFloppyJennyVoiceTable[26] = {
+const char *const kFloppyJennyVoiceTable[26] = {
"DING.VOC", "F-0194SL.VOC", "F-0191SL.VOC", "NEWSCAN.VOC",
"F-0187SL.VOC", "F-0184SL.VOC", "F-0181SL.VOC", "NEWSSHRT.VOC",
"F-0177SL.VOC", "F-0170SL.VOC", "F-0168SL.VOC", "F-0166SL.VOC",
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 5118851df06..9dfe48519f7 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -96,7 +96,7 @@ const byte kCursorInteractivePalette[] = {
0xFF, 0xFF, 0xFF
};
-static void fadeCurrentPaletteToBlack(uint delayMs = 8) {
+void fadeCurrentPaletteToBlack(uint delayMs = 8) {
byte start[kPalSize];
byte stepPal[kPalSize];
g_system->getPaletteManager()->grabPalette(start, 0, 256);
@@ -111,7 +111,7 @@ static void fadeCurrentPaletteToBlack(uint delayMs = 8) {
}
}
-static void fadePaletteFromBlack(const byte *target, uint delayMs = 8) {
+void fadePaletteFromBlack(const byte *target, uint delayMs = 8) {
byte stepPal[kPalSize];
for (uint step = 1; step <= 16; step++) {
@@ -124,7 +124,7 @@ static void fadePaletteFromBlack(const byte *target, uint delayMs = 8) {
}
}
-static void setInteractiveCursorPalette(const Picture &cursor, byte transparent) {
+void setInteractiveCursorPalette(const Picture &cursor, byte transparent) {
byte palette[kPalSize];
bool used[256];
memset(used, 0, sizeof(used));
@@ -170,7 +170,7 @@ static void setInteractiveCursorPalette(const Picture &cursor, byte transparent)
CursorMan.replaceCursorPalette(palette, 0, 256);
}
-static void installMouseCursor(DBDArchive &pics, bool interactive) {
+void installMouseCursor(DBDArchive &pics, bool interactive) {
Picture cursor;
if (pics.getPicture(kPicMousePointer, cursor) && !cursor.surface.empty()) {
const byte transparent = (byte)(cursor.flags >> 8);
@@ -810,7 +810,7 @@ void EEMEngine::waitForInput(uint32 maxMs) {
// fpal[start] = saved
// If `show`, upload `end - start` entries (fpal[start..end-1]) â note that
// fpal[end] is rotated in memory but intentionally not uploaded each tick.
-static void openColorCycle(byte *fpal, uint8 start, uint8 end, bool show) {
+void openColorCycle(byte *fpal, uint8 start, uint8 end, bool show) {
if (end <= start)
return;
const byte savedR = fpal[end * 3 + 0];
diff --git a/engines/eem/graphics.cpp b/engines/eem/graphics.cpp
index b80eaa85730..8f8441b0c41 100644
--- a/engines/eem/graphics.cpp
+++ b/engines/eem/graphics.cpp
@@ -127,7 +127,7 @@ uint getBalloonLineCapacity(uint16 balloonId, int lineH) {
// FUN_22dc_096c @ 22dc:096c: walks per-site dialog records at site_data[+6]
// to skip hotspotIdx hotspots, returns _cluesFound flag for hotspot's first
// text index.
-static bool floppyHotspotSearched(EEM::Mystery &mystery, uint siteIdx,
+bool floppyHotspotSearched(EEM::Mystery &mystery, uint siteIdx,
uint hotspotIdx) {
const byte *site = mystery.siteData(siteIdx);
if (!site)
diff --git a/engines/eem/metaengine.cpp b/engines/eem/metaengine.cpp
index 04e778c35bc..ee16e3e10d3 100644
--- a/engines/eem/metaengine.cpp
+++ b/engines/eem/metaengine.cpp
@@ -31,7 +31,7 @@
namespace EEM {
-static const ADExtraGuiOptionsMap optionsList[] = {
+const ADExtraGuiOptionsMap optionsList[] = {
{
GAMEOPTION_HIDE_HIGHLIGHT_BOXES,
{
diff --git a/engines/eem/music.cpp b/engines/eem/music.cpp
index c1fd4bf26b0..31c51f225c5 100644
--- a/engines/eem/music.cpp
+++ b/engines/eem/music.cpp
@@ -32,12 +32,8 @@
namespace EEM {
-namespace {
-
const int kMidiDriverFlags = MDT_MIDI | MDT_ADLIB | MDT_PREFER_MT32;
-} // End of anonymous namespace
-
MusicPlayer::MusicPlayer(bool isFloppy) : _isFloppy(isFloppy) {
// _InitMIDI @ 20a2:013a â `_AIL_register_driver` against
// ADLIB.ADV / SBFM.ADV / MT32MPU.ADV. We honour the launcher's
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index d523b1e3f5b..0f9fc01f99a 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -48,7 +48,7 @@ void blitFrame(Graphics::ManagedSurface &dst, const Picture &p,
// Masked top-left blit onto a locked screen surface. Clips both src and
// dst against the screen, then delegates to copyRectToSurfaceWithKey
// (Graphics::Surface's transparent-key blit).
-static void keyBlitToScreen(Graphics::Surface *screen, const Picture &p,
+void keyBlitToScreen(Graphics::Surface *screen, const Picture &p,
int x, int y) {
if (!screen || p.surface.empty())
return;
@@ -278,15 +278,15 @@ struct AnimScriptLong {
// 0x17 (game) / 0x18 (book) / 0x19 (nancy) regardless of partner; the
// per-partner ANI.DBD cells come from a separate entry (e.g. 0x3b for
// Jenny's briefing).
-static const uint8 kScript17[] = {
+const uint8 kScript17[] = {
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,
20,21,22,23,24,25,26,27,28,29
};
-static const uint8 kScript18[] = {
+const uint8 kScript18[] = {
0,1,2,3,4,5,6,7,8,8,8,8,8,8,8,8,
8,8,8,8,8,8,8,9,10,11,12,13,14,15
};
-static const uint8 kScript19[] = {
+const uint8 kScript19[] = {
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
1,2,3,4,5,6,7,8,9,10,11,12
};
@@ -295,7 +295,7 @@ static const uint8 kScript19[] = {
// frames are the original's frame-hold mechanism (one entry per tick).
// 0x1a (29be:19a4) â count-up 0..7, idle hold, repeat 1..7, idle, mirror 7..0, idle.
-static const uint8 kScript1a[] = {
+const uint8 kScript1a[] = {
0,1,2,3,4,5,6,7,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
1,2,3,4,5,6,7,
@@ -304,7 +304,7 @@ static const uint8 kScript1a[] = {
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
// 0x1e (29be:1a40) â slow walk-stutter with idle tail (76 entries).
-static const uint8 kScript1e[] = {
+const uint8 kScript1e[] = {
0,1,2,3,3,3,3,4,4,3,4,4,4,4,4,3,
5,5,5,5,5,5,5,5,4,4,4,4,4,4,4,4,
5,5,5,5,5,5,6,5,6,5,7,7,7,7,7,7,
@@ -313,7 +313,7 @@ static const uint8 kScript1e[] = {
};
// 0x1f (29be:1ada) â 0..5, idle, 0..5, idle, 6..8 alternation,
// idle (50 entries).
-static const uint8 kScript1f[] = {
+const uint8 kScript1f[] = {
0,1,2,3,4,5,
0,0,0,0,
1,2,3,4,5,
@@ -324,13 +324,13 @@ static const uint8 kScript1f[] = {
0,0,0,0,0
};
// 0x20 (29be:1b40) â count-up 0..33 (34 frames).
-static const uint8 kScript20[] = {
+const uint8 kScript20[] = {
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,
20,21,22,23,24,25,26,27,28,29,30,31,32,33
};
// 0x22 (29be:1ba4) â long held-frame walker 0..22 with idle tail
// (115 entries; most frames held 4-7 ticks each).
-static const uint8 kScript22[] = {
+const uint8 kScript22[] = {
0,
1,1,1,1,1,
2,2,2,2,2,
@@ -358,7 +358,7 @@ static const uint8 kScript22[] = {
};
// 0x23 (29be:1c8c) â 29 entries: 0, 6 holds of 1, count-up 2..4,
// down-up gesture, 5 idle frames.
-static const uint8 kScript23[] = {
+const uint8 kScript23[] = {
0,1,1,1,1,1,1,
2,3,4,3,2,
5,5,5,
@@ -367,7 +367,7 @@ static const uint8 kScript23[] = {
};
// 0x24 (29be:1cc8) â bell-curve hold (58 entries): 0,0, 1,1, 2,2,
// 3 held for 26 ticks, mirror back, idle.
-static const uint8 kScript24[] = {
+const uint8 kScript24[] = {
0,0,1,1,2,2,
3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,
2,2,1,1,
@@ -375,7 +375,7 @@ static const uint8 kScript24[] = {
};
// 0x28 (29be:1d7e) â gentle hold 0..3 with long hold on 3, mirror
// back, idle (45 entries).
-static const uint8 kScript28[] = {
+const uint8 kScript28[] = {
0,1,1,2,2,
3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,
2,2,1,1,
@@ -383,7 +383,7 @@ static const uint8 kScript28[] = {
};
// 0x29 (29be:1dda) â paired-step count-up 0..21 plus idle
// (58 entries).
-static const uint8 kScript29[] = {
+const uint8 kScript29[] = {
0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,
11,11,12,12,13,13,14,14,15,15,16,16,17,17,18,18,19,19,
20,20,21,21,
@@ -391,14 +391,14 @@ static const uint8 kScript29[] = {
};
// 0x2b (29be:1e5c) â count-up 0..11 with each frame held 4 ticks
// (48 entries).
-static const uint8 kScript2b[] = {
+const uint8 kScript2b[] = {
0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,
4,4,4,4,5,5,5,5,6,6,6,6,7,7,7,7,
8,8,8,8,9,9,9,9,10,10,10,10,11,11,11,11
};
// 0x2c (29be:1ebe) â alternation walk 0..19 with idle tail
// (54 entries).
-static const uint8 kScript2c[] = {
+const uint8 kScript2c[] = {
0,1,2,3,4,5,
0,
6,7,8,9,10,10,10,10,10,10,
@@ -410,7 +410,7 @@ static const uint8 kScript2c[] = {
};
// 0x2d (29be:1f2c) â count-up 0..11 with each frame held 8 ticks
// (96 entries).
-static const uint8 kScript2d[] = {
+const uint8 kScript2d[] = {
0,0,0,0,0,0,0,0,
1,1,1,1,1,1,1,1,
2,2,2,2,2,2,2,2,
@@ -426,7 +426,7 @@ static const uint8 kScript2d[] = {
};
// 0x30 (29be:1fee) â 0,0, count-up 1..19, idle, mirror down, extra
// idle (86 entries).
-static const uint8 kScript30[] = {
+const uint8 kScript30[] = {
0,0,
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,
0,0,0,0,0,0,0,0,0,0,
@@ -435,7 +435,7 @@ static const uint8 kScript30[] = {
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
// 0x31 (29be:209c) â paired-step idle alternations (57 entries).
-static const uint8 kScript31[] = {
+const uint8 kScript31[] = {
0,0,0,1,1,1,
0,0,0,1,1,1,
2,2,2,3,3,3,
@@ -450,7 +450,7 @@ static const uint8 kScript31[] = {
};
// 0x36 (29be:2110) â 0..8 forward, 1..8 forward, frame 1 held 20
// ticks, 8..0 mirror, idle tail (60 entries).
-static const uint8 kScript36[] = {
+const uint8 kScript36[] = {
0,1,2,3,4,5,6,7,8,
1,2,3,4,5,6,7,8,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
@@ -484,19 +484,19 @@ const AnimScriptLong kAnimScriptsLong[] = {
// never calls the switchers; floppy calls them from `_DoSiteLoop_Floppy`
// (via `_Switch2Patient` / `_Switch2Impatient`). We intentionally enable
// the same switch for both builds.
-static const uint8 kPatientSequence[] = { 0,0,0,0,0,0,0,0,0,2 };
-static const uint8 kImpatientSequence[] = { 0,1,0,1,0,1,0,1,2,1 };
+const uint8 kPatientSequence[] = { 0,0,0,0,0,0,0,0,0,2 };
+const uint8 kImpatientSequence[] = { 0,1,0,1,0,1,0,1,2,1 };
// Test-shortened impatience delay. The original stores an hour-rounded
// wall-clock value via DOS gettime; this keeps the same reset/switch
// behavior but makes the feature observable during normal testing.
-static const uint32 kImpatienceDelayMs = 60 * 1000;
+const uint32 kImpatienceDelayMs = 60 * 1000;
struct AnimScriptRef {
const uint8 *frames;
uint16 len;
};
-static AnimScriptRef findAnimScript(uint16 seqnum) {
+AnimScriptRef findAnimScript(uint16 seqnum) {
for (uint i = 0; i < ARRAYSIZE(kAnimScripts); i++) {
if (kAnimScripts[i].seqnum == seqnum) {
AnimScriptRef r;
@@ -525,9 +525,9 @@ static AnimScriptRef findAnimScript(uint16 seqnum) {
// (Borland C `struct time` memory order is min, hour, hund, sec.)
// `+ 0xe` is 14 centiseconds â ~140 ms per frame, matching
// `_CheckFrameRate @ 1a35:0204`.
-static const uint kFramePeriodMs = 140;
+const uint kFramePeriodMs = 140;
-static uint frameFromScriptAtTick(const uint8 *frames, uint len,
+uint frameFromScriptAtTick(const uint8 *frames, uint len,
uint numFrames, uint32 tickMs) {
if (!frames || len == 0)
return numFrames > 0 ? (uint)((tickMs / kFramePeriodMs) % numFrames) : 0;
@@ -639,7 +639,7 @@ uint partnerFrameAtTick(uint16 seqnum, uint numFrames, uint32 tickMs) {
// `_SmallMapWaitSeq @ 29be:1548`). `partnerFrameAtTick` can't model
// that swap on its own (it always wraps on the same script), hence
// this helper.
-static uint oneShotThenLoopFrameAtTick(const uint8 *unfold, uint unfoldLen,
+uint oneShotThenLoopFrameAtTick(const uint8 *unfold, uint unfoldLen,
const uint8 *waitSeq, uint waitSeqLen,
uint numFrames, uint32 elapsedMs) {
const uint tick = elapsedMs / kFramePeriodMs;
Commit: f7890714707e231e068bca32bc05939ee5baade7
https://github.com/scummvm/scummvm/commit/f7890714707e231e068bca32bc05939ee5baade7
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:15+02:00
Commit Message:
CI: temporary enable CI in github
Changed paths:
.github/workflows/ci.yml
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index aa19879897e..5a7badb96c9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,5 +1,12 @@
name: CI
-on: [push, pull_request]
+# Temporarily scoped to dev-eem to fix merge-blocking CI issues without
+# burning runner minutes on every push. Revert to `on: [push, pull_request]`
+# before opening upstream PRs from other branches.
+on:
+ push:
+ branches: [dev-eem]
+ pull_request:
+ branches: [dev-eem]
# schedule:
# - cron: '0 0-23/4 * * *'
permissions:
Commit: ba065aa1a9a6a4b0979c3e09aa197a4deea33c5f
https://github.com/scummvm/scummvm/commit/ba065aa1a9a6a4b0979c3e09aa197a4deea33c5f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:15+02:00
Commit Message:
CI: reverted temporary enable CI in github commit
Changed paths:
.github/workflows/ci.yml
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5a7badb96c9..aa19879897e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,12 +1,5 @@
name: CI
-# Temporarily scoped to dev-eem to fix merge-blocking CI issues without
-# burning runner minutes on every push. Revert to `on: [push, pull_request]`
-# before opening upstream PRs from other branches.
-on:
- push:
- branches: [dev-eem]
- pull_request:
- branches: [dev-eem]
+on: [push, pull_request]
# schedule:
# - cron: '0 0-23/4 * * *'
permissions:
Commit: 8bcc62ad5d61919786b859a2dadb3cb2ad5a9312
https://github.com/scummvm/scummvm/commit/8bcc62ad5d61919786b859a2dadb3cb2ad5a9312
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:15+02:00
Commit Message:
EEM: removed duplicated definitions
Changed paths:
engines/eem/eem.h
engines/eem/site.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 8c06aa64cb9..04234a5a89b 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -28,6 +28,7 @@
#include "common/language.h"
#include "common/platform.h"
#include "common/random.h"
+#include "common/rect.h"
#include "common/scummsys.h"
#include "common/serializer.h"
@@ -115,6 +116,13 @@ enum Partner {
constexpr int kScreenWidth = 320;
constexpr int kScreenHeight = 200;
+/// Shared PDA-frame navigation rects (PIC 0x3f) â reachable from Site,
+/// Notebook, Gallery, Accuse, and MoreInfo. Original `_NoteButtons` table
+/// @ 29be:0147 + the site-screen hit tests in `_DoSiteLoop @ 168d:03f4`.
+constexpr Common::Rect kPdaSiteRect (Common::Point(35, 111), 21, 25);
+constexpr Common::Rect kPdaPartnerFootMapRect (Common::Point( 7, 177), 50, 23);
+constexpr Common::Rect kPdaPartnerHeadHintRect (Common::Point( 5, 80), 39, 30);
+
class EEMEngine : public Engine {
public:
EEMEngine(OSystem *syst, const ADGameDescription *gameDesc);
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 0f9fc01f99a..8358e1d6fe2 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -35,10 +35,6 @@
namespace EEM {
-constexpr Common::Rect kSitePdaRect(Common::Point(35, 111), 21, 25);
-constexpr Common::Rect kSitePartnerFootMapRect(Common::Point(7, 177), 50, 23);
-constexpr Common::Rect kSitePartnerHeadHintRect(Common::Point(5, 80), 39, 30);
-
// Masked blit using `transp` = high byte of `pic.flags` (`_Rect_Move_Mask @ 1000:03fc`).
void blitFrame(Graphics::ManagedSurface &dst, const Picture &p,
int x, int y, byte transp) {
@@ -882,14 +878,14 @@ void SiteScreen::run() {
// Partner-head click is port-only: `_KDHelp` shortcut
// mirroring `_HandleNoteButton[3]` (0x0403) /
// `_HandleGalleryButton[3]` (0x061e). Rect = (5,80,44,110).
- if (kSitePdaRect.contains(event.mouse.x, event.mouse.y)) {
+ if (kPdaSiteRect.contains(event.mouse.x, event.mouse.y)) {
notePartnerActivity();
_vm->setHotspotMouseCursor(false);
_vm->setNextScreen(kScreenNotebook);
_vm->stopMusic();
return;
}
- if (kSitePartnerFootMapRect.contains(event.mouse.x, event.mouse.y)) {
+ if (kPdaPartnerFootMapRect.contains(event.mouse.x, event.mouse.y)) {
notePartnerActivity();
_vm->setHotspotMouseCursor(false);
// CD: _NextScreen=1, floppy=2.
@@ -898,7 +894,7 @@ void SiteScreen::run() {
_vm->stopMusic();
return;
}
- if (kSitePartnerHeadHintRect.contains(event.mouse.x, event.mouse.y)) {
+ if (kPdaPartnerHeadHintRect.contains(event.mouse.x, event.mouse.y)) {
_vm->setHotspotMouseCursor(false);
_vm->doHelp();
notePartnerActivity();
@@ -1527,9 +1523,9 @@ int SiteScreen::hotspotAtPoint(uint siteNum, int x, int y) const {
void SiteScreen::updateHotspotCursor(uint siteNum, int x, int y) {
if (!_vm)
return;
- const bool siteControl = kSitePdaRect.contains(x, y) ||
- kSitePartnerFootMapRect.contains(x, y) ||
- kSitePartnerHeadHintRect.contains(x, y);
+ const bool siteControl = kPdaSiteRect.contains(x, y) ||
+ kPdaPartnerFootMapRect.contains(x, y) ||
+ kPdaPartnerHeadHintRect.contains(x, y);
_vm->setHotspotMouseCursor(siteControl || hotspotAtPoint(siteNum, x, y) >= 0);
}
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 507d27d0506..7ef17cafaa4 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -163,16 +163,16 @@ constexpr uint16 kFloppyEndingBackgroundPic = 0x8b;
constexpr uint16 kFirstTryBadgePic = 0x205;
constexpr Common::Point kFirstTryBadgePos(0x1e, 9);
+// kPdaSiteRect / kPdaPartnerFootMapRect / kPdaPartnerHeadHintRect live in
+// eem.h (shared with site.cpp). The button-row rects below are PDA-screen-
+// only (`_NoteButtons @ 29be:0147`).
constexpr Common::Rect kPdaHelpRect(Common::Point(93, 174), 22, 16);
constexpr Common::Rect kPdaNotebookRect(Common::Point(134, 174), 21, 16);
constexpr Common::Rect kPdaGalleryRect(Common::Point(157, 174), 21, 16);
-constexpr Common::Rect kPdaPartnerHeadHintRect(Common::Point(5, 80), 39, 30);
constexpr Common::Rect kPdaAccuseRect(Common::Point(180, 174), 21, 16);
constexpr Common::Rect kPdaPageNextRect(Common::Point(204, 174), 20, 16);
constexpr Common::Rect kPdaPagePrevRect(Common::Point(226, 174), 21, 16);
constexpr Common::Rect kPdaHelp2Rect(Common::Point(267, 174), 21, 16);
-constexpr Common::Rect kPdaPartnerFootMapRect(Common::Point(7, 177), 50, 23);
-constexpr Common::Rect kPdaSiteRect(Common::Point(35, 111), 21, 25);
constexpr uint16 kProfilePickerRevealPic = 0x105;
constexpr int kProfilePickerRevealX = 0x3e;
Commit: 79188e6bd223fc5dd10476f7916af06c93b2cc84
https://github.com/scummvm/scummvm/commit/79188e6bd223fc5dd10476f7916af06c93b2cc84
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:16+02:00
Commit Message:
EEM: renamed _ASM_Decompress to avoid confusion
Changed paths:
engines/eem/animation.cpp
engines/eem/animation.h
diff --git a/engines/eem/animation.cpp b/engines/eem/animation.cpp
index 34364c6bf12..c52ee496293 100644
--- a/engines/eem/animation.cpp
+++ b/engines/eem/animation.cpp
@@ -27,7 +27,7 @@
namespace EEM {
-void asmDecompress(const byte *src, uint srcSize, byte *dst, uint dstSize) {
+void decodeAnmFrameRLE(const byte *src, uint srcSize, byte *dst, uint dstSize) {
const byte *srcEnd = src + srcSize;
byte *dstEnd = dst + dstSize;
@@ -162,7 +162,7 @@ const byte *ANMDecoder::nextFrame() {
return nullptr;
}
- asmDecompress(_packed.data(), packedSize, _buffer.data(), _buffer.size());
+ decodeAnmFrameRLE(_packed.data(), packedSize, _buffer.data(), _buffer.size());
_nextFrameIdx++;
return _buffer.data();
}
diff --git a/engines/eem/animation.h b/engines/eem/animation.h
index fa5c7b8f4c2..51c07315f79 100644
--- a/engines/eem/animation.h
+++ b/engines/eem/animation.h
@@ -36,7 +36,7 @@ namespace EEM {
* - u16: frame count
* - 12 bytes: header (height @ +2, width @ +4, rest unused)
* - frames*u16: packed length per frame
- * - per frame: lengths[i] bytes of RLE delta data (asmDecompress).
+ * - per frame: lengths[i] bytes of RLE delta data (decodeAnmFrameRLE).
*/
class ANMDecoder {
public:
@@ -82,9 +82,11 @@ private:
uint16 _nextFrameIdx = 0;
};
-/// _ASM_Decompress @ 1000:0953. dst holds the previous frame; skip opcodes
-/// leave those pixels untouched (difference encoding).
-void asmDecompress(const byte *src, uint srcSize, byte *dst, uint dstSize);
+/// Decode a single ANM frame's RLE-encoded delta payload into @p dst.
+/// `dst` already holds the previous frame; skip opcodes leave those pixels
+/// untouched (difference encoding). Original symbol: `_ASM_Decompress`
+/// @ 1000:0953.
+void decodeAnmFrameRLE(const byte *src, uint srcSize, byte *dst, uint dstSize);
} // End of namespace EEM
Commit: 2b9c008c66397e6bba2cacbd3dbac8b3040f1e51
https://github.com/scummvm/scummvm/commit/2b9c008c66397e6bba2cacbd3dbac8b3040f1e51
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:16+02:00
Commit Message:
EEM: add end of notes in the PDA rendering
Changed paths:
engines/eem/eem.h
engines/eem/ui.cpp
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 04234a5a89b..3115e45d7d4 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -518,10 +518,6 @@ private:
/// ESC during intro: skip remaining opening-anim chain.
bool _skipIntro = false;
- /// Cached notebook slot rects + clue IDs for click hit-testing.
- Common::Array<Common::Rect> _notebookSlotRects;
- Common::Array<uint> _notebookSlotClues;
-
/// Clean BG (no partner/NPC) used by `playKdAnim` between camera-anim
/// cells. See `setPartnerEraseBg`.
Graphics::ManagedSurface _partnerEraseBg;
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 7ef17cafaa4..ca54e079b9e 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -2125,9 +2125,7 @@ void EEMEngine::doNotebook() {
drawNotebookFrame(page);
Common::Point mouse = g_system->getEventManager()->getMousePos();
- setInteractiveMouseCursor(notebookButtonAt(mouse.x, mouse.y) ||
- rectListContains(_notebookSlotRects,
- mouse.x, mouse.y));
+ setInteractiveMouseCursor(notebookButtonAt(mouse.x, mouse.y));
uint32 lastDraw = g_system->getMillis();
uint32 gizmoLastTick = lastDraw;
@@ -2145,9 +2143,6 @@ void EEMEngine::doNotebook() {
}
if (ev.type == Common::EVENT_MOUSEMOVE) {
setInteractiveMouseCursor(notebookButtonAt(ev.mouse.x,
- ev.mouse.y) ||
- rectListContains(_notebookSlotRects,
- ev.mouse.x,
ev.mouse.y));
}
if (ev.type == Common::EVENT_KEYDOWN) {
@@ -2220,17 +2215,10 @@ void EEMEngine::doNotebook() {
dirty = true;
continue;
}
- // ScummVM-only: click on clue slot toggles _NoteSelected
- // (original toggles only in accuse screen).
- for (uint i = 0; i < _notebookSlotRects.size(); i++) {
- if (_notebookSlotRects[i].contains(ev.mouse.x,
- ev.mouse.y)) {
- const uint clueId = _notebookSlotClues[i];
- _mystery._noteSelected[clueId] ^= 1;
- dirty = true;
- break;
- }
- }
+ // The notebook screen is read-only in the original:
+ // `_DoNotebook @ 161e:0500` only checks clicks against
+ // `_NoteButtons` (11 rects). Clue toggling lives in the
+ // accuse screen.
}
}
if (exitFlag)
@@ -2242,9 +2230,7 @@ void EEMEngine::doNotebook() {
drawNotebookFrame(page);
lastDraw = now;
mouse = g_system->getEventManager()->getMousePos();
- setInteractiveMouseCursor(notebookButtonAt(mouse.x, mouse.y) ||
- rectListContains(_notebookSlotRects,
- mouse.x, mouse.y));
+ setInteractiveMouseCursor(notebookButtonAt(mouse.x, mouse.y));
}
// _GizmoColorCycle @ 1c33:0002 â `_DoNotebook` rotates 0x6f..0x73 each
// _CheckFrameRate tick (the PDA gizmo / LED indicator shimmer).
@@ -2348,10 +2334,10 @@ void EEMEngine::drawNotebookFrame(int &page) {
page = 0;
}
- // Per-slot rects published to the click handler.
- Common::Array<Common::Rect> slotRects;
- Common::Array<uint> slotClues;
-
+ // `_DrawNotes @ 161e:01d0` is read-only on this screen â clue selection
+ // is driven exclusively from the accuse screen. The 0x3C / 0x5C colour
+ // choice still reflects `_NoteSelected[]` so picks made in accuse remain
+ // visible when the player flips back here.
const int startClue = (page < (int)pageStarts.size())
? pageStarts[page] : 0;
const int endClue = (page + 1 < (int)pageStarts.size())
@@ -2365,7 +2351,6 @@ void EEMEngine::drawNotebookFrame(int &page) {
if (txt.empty())
txt = Common::String::format(
isSpanish() ? "nota %u" : "clue %u", clueId);
- // `_DrawNotes @ 161e:01d0`: 0x5c unselected, 0x3c selected.
Common::Array<Common::String> wrapped;
_font.wordWrapText(txt, kRectW, wrapped);
const int lineH = _font.getFontHeight();
@@ -2375,24 +2360,22 @@ void EEMEngine::drawNotebookFrame(int &page) {
_font.drawString(&scratch, wrapped[li], kRectX,
y + (int)li * lineH, kRectW, color);
}
- slotRects.push_back(Common::Rect(kRectX, y,
- kRectX + kRectW, y + h));
- slotClues.push_back(clueId);
y += h + 7;
}
- // Page indicator only (original has no points display).
- _font.drawString(&scratch, Common::String::format("p%d/%d",
- page + 1, (int)pageStarts.size()),
- 270, 4, kScreenWidth, 0x5C);
+ // `_DrawNotes @ 161e:01d0` appends a terminator line at the bottom of
+ // the last page once the clue list is exhausted â string @ 29be:01f4.
+ const bool isLastPage = (page + 1 >= (int)pageStarts.size());
+ if (isLastPage) {
+ const char *kEndMarker = isSpanish()
+ ? "-- Fin de las notas --"
+ : "-- End of notes --";
+ _font.drawString(&scratch, kEndMarker, kRectX, y, kRectW, 0x5C);
+ }
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
-
- // Publish slot info to `doNotebook`'s click handler.
- _notebookSlotRects = slotRects;
- _notebookSlotClues = slotClues;
}
void EEMEngine::doGallery() {
Commit: c9a5bbc56c706c24039210f01702ba3f6740b650
https://github.com/scummvm/scummvm/commit/c9a5bbc56c706c24039210f01702ba3f6740b650
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:16+02:00
Commit Message:
EEM: add an option to skip repeated cases
Changed paths:
engines/eem/detection.cpp
engines/eem/detection.h
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/metaengine.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/detection.cpp b/engines/eem/detection.cpp
index 48cf5629a1d..17ff91e826b 100644
--- a/engines/eem/detection.cpp
+++ b/engines/eem/detection.cpp
@@ -31,8 +31,8 @@ const PlainGameDescriptor eemGames[] = {
{ nullptr, nullptr }
};
-#define GUI_OPTIONS_EEM_FLOPPY GUIO3(GAMEOPTION_HIDE_HIGHLIGHT_BOXES, GUIO_MIDIADLIB, GUIO_MIDIMT32)
-#define GUI_OPTIONS_EEM_CD GUIO4(GAMEOPTION_HIDE_HIGHLIGHT_BOXES, GAMEOPTION_FIT_DIALOG_BALLOONS, GUIO_MIDIADLIB, GUIO_MIDIMT32)
+#define GUI_OPTIONS_EEM_FLOPPY GUIO4(GAMEOPTION_HIDE_HIGHLIGHT_BOXES, GAMEOPTION_SKIP_REPEATED_CASES, GUIO_MIDIADLIB, GUIO_MIDIMT32)
+#define GUI_OPTIONS_EEM_CD GUIO5(GAMEOPTION_HIDE_HIGHLIGHT_BOXES, GAMEOPTION_FIT_DIALOG_BALLOONS, GAMEOPTION_SKIP_REPEATED_CASES, GUIO_MIDIADLIB, GUIO_MIDIMT32)
const ADGameDescription gameDescriptions[] = {
{
diff --git a/engines/eem/detection.h b/engines/eem/detection.h
index 3ad726bb5f0..937fbe6a4e3 100644
--- a/engines/eem/detection.h
+++ b/engines/eem/detection.h
@@ -28,6 +28,7 @@ namespace EEM {
#define GAMEOPTION_HIDE_HIGHLIGHT_BOXES GUIO_GAMEOPTIONS1
#define GAMEOPTION_FIT_DIALOG_BALLOONS GUIO_GAMEOPTIONS2
+#define GAMEOPTION_SKIP_REPEATED_CASES GUIO_GAMEOPTIONS3
enum EEMDebugChannels {
kDebugGeneral = 1 << 0,
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 9dfe48519f7..2ad4b74f687 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -197,6 +197,7 @@ EEMEngine::EEMEngine(OSystem *syst, const ADGameDescription *gameDesc)
_lastScreen(kScreenInvalid), _nextScreen(kScreenTitle), _partner(0) {
ConfMan.registerDefault("hide_highlight_boxes", false);
ConfMan.registerDefault("fit_dialog_balloons", false);
+ ConfMan.registerDefault("skip_repeated_cases", false);
_variant = (gameDesc && gameDesc->extra &&
Common::String(gameDesc->extra).contains("Floppy"))
@@ -220,6 +221,69 @@ void EEMEngine::applyStartupTestOverrides() {
"startup test override: populated ScrapBook 1 mystery flags");
}
+bool EEMEngine::areMysteriesSolved(uint lo, uint hi) const {
+ if (hi < lo)
+ return false;
+ for (uint i = lo; i <= hi; i++) {
+ if (i >= sizeof(_mysteriesSolved) || _mysteriesSolved[i] == 0)
+ return false;
+ }
+ return true;
+}
+
+void EEMEngine::advanceChainStageAfterSolve(uint mysteryNum) {
+ if (mysteryNum == 0 || _chainStage >= 4)
+ return;
+
+ uint lo = 0;
+ uint hi = 0;
+ switch (_chainStage) {
+ case 1:
+ lo = 1;
+ hi = 0x18;
+ break;
+ case 2:
+ lo = 0x19;
+ hi = 0x30;
+ break;
+ case 3:
+ lo = 0x31;
+ hi = 0x36;
+ break;
+ default:
+ return;
+ }
+
+ if (!areMysteriesSolved(lo, hi))
+ return;
+
+ const uint oldStage = _chainStage;
+ // Book 2 repeats the Book 1 cases; this option keeps the original solve
+ // state but jumps the profile's active chain straight to Book 3.
+ if (_chainStage == 1 && ConfMan.getBool("skip_repeated_cases"))
+ _chainStage = 3;
+ else
+ _chainStage++;
+
+ debugC(1, kDebugMystery,
+ "chainStage advanced from %u to %u after solving mystery %u",
+ oldStage, _chainStage, mysteryNum);
+}
+
+void EEMEngine::applySkipRepeatedCasesOption() {
+ if (!ConfMan.getBool("skip_repeated_cases"))
+ return;
+ if (_mystery.isLoaded())
+ return;
+ if (_chainStage <= 2 && areMysteriesSolved(1, 0x18)) {
+ const uint oldStage = _chainStage;
+ _chainStage = 3;
+ debugC(1, kDebugMystery,
+ "skip_repeated_cases advanced chainStage from %u to %u",
+ oldStage, _chainStage);
+ }
+}
+
Common::Error EEMEngine::run() {
// _SetMode13X @ 1000:0358 â VGA mode 13h.
initGraphics(kScreenWidth, kScreenHeight);
@@ -268,6 +332,7 @@ Common::Error EEMEngine::run() {
const Common::Error err = loadGameState(wantedSave);
if (err.getCode() == Common::kNoError) {
applyStartupTestOverrides();
+ applySkipRepeatedCasesOption();
CursorMan.showMouse(true);
if (_mystery.isLoaded()) {
debugC(1, kDebugGeneral,
@@ -398,6 +463,8 @@ Common::Error EEMEngine::run() {
doProfilePicker();
if (!shouldQuit())
applyStartupTestOverrides();
+ if (!shouldQuit())
+ applySkipRepeatedCasesOption();
if (!shouldQuit())
doChoosePartner();
@@ -521,6 +588,8 @@ screenLoop:
doProfilePicker();
if (!shouldQuit())
applyStartupTestOverrides();
+ if (!shouldQuit())
+ applySkipRepeatedCasesOption();
if (!shouldQuit())
doChoosePartner();
if (!shouldQuit())
@@ -1172,6 +1241,7 @@ Common::Error EEMEngine::loadGameStream(Common::SeekableReadStream *stream) {
_mystery.clear();
resetSiteArrivalState();
}
+ applySkipRepeatedCasesOption();
debugC(1, kDebugGeneral,
"Loaded profile name=%s partner=%u mystery=%d",
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 3115e45d7d4..e2ebb1151d0 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -278,6 +278,9 @@ public:
private:
void applyStartupTestOverrides();
+ bool areMysteriesSolved(uint lo, uint hi) const;
+ void advanceChainStageAfterSolve(uint mysteryNum);
+ void applySkipRepeatedCasesOption();
/// Central dispatch loop matching `_ScreenDriver @ 1a35:0dc1`. Each
/// iteration calls the handler that matches `_nextScreen`; handlers
diff --git a/engines/eem/metaengine.cpp b/engines/eem/metaengine.cpp
index ee16e3e10d3..d3efe9c89cc 100644
--- a/engines/eem/metaengine.cpp
+++ b/engines/eem/metaengine.cpp
@@ -55,6 +55,17 @@ const ADExtraGuiOptionsMap optionsList[] = {
0
}
},
+ {
+ GAMEOPTION_SKIP_REPEATED_CASES,
+ {
+ _s("Skip repeated cases"),
+ _s("Skip all Book 2 cases and jump to Book 3 once Book 1 is complete."),
+ "skip_repeated_cases",
+ false,
+ 0,
+ 0
+ }
+ },
AD_EXTRA_GUI_OPTIONS_TERMINATOR
};
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index ca54e079b9e..3a29e5252cf 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -4281,38 +4281,7 @@ void EEMEngine::doAccuse() {
// Chain advance @ 1df2:0824-0850. Skip mystery 0 (practice).
// stage 1: 1..0x18, stage 2: 0x19..0x30, stage 3: 0x31..0x36.
- if (mn != 0) {
- uint lo = 0;
- uint hi = 0;
- switch (_chainStage) {
- case 1:
- lo = 1;
- hi = 0x18;
- break;
- case 2:
- lo = 0x19;
- hi = 0x30;
- break;
- case 3:
- lo = 0x31;
- hi = 0x36;
- break;
- default:
- break;
- }
- bool allSolved = (hi >= lo);
- for (uint i = lo; i <= hi && allSolved; i++) {
- if (i >= sizeof(_mysteriesSolved) || _mysteriesSolved[i] == 0)
- allSolved = false;
- }
- // 1df2:0852 increments past 3 (stage-4 endgame); cap at 4.
- if (allSolved && _chainStage < 4) {
- _chainStage++;
- debugC(1, kDebugMystery,
- "chainStage advanced to %u after solving mystery %u",
- _chainStage, mn);
- }
- }
+ advanceChainStageAfterSolve(mn);
// `_DisplayCorrect @ 1df2:073c`:
// _AllBlack; _BuildBackground(5, 0x42, 0x14); _FadeIn;
@@ -4738,38 +4707,7 @@ void EEMEngine::doAccuseFloppy() {
_mysteriesSolved[mn] = _mystery._firstTry ? 2 : 1;
// Tier promotion @ 1d40:0941..0978. Skip mystery 0 (practice).
- if (mn != 0) {
- uint lo = 0;
- uint hi = 0;
- switch (_chainStage) {
- case 1:
- lo = 1;
- hi = 0x18;
- break;
- case 2:
- lo = 0x19;
- hi = 0x30;
- break;
- case 3:
- lo = 0x31;
- hi = 0x36;
- break;
- default:
- break;
- }
- bool allSolved = (hi >= lo);
- for (uint i = lo; i <= hi && allSolved; i++) {
- if (i >= sizeof(_mysteriesSolved) ||
- _mysteriesSolved[i] == 0)
- allSolved = false;
- }
- if (allSolved && _chainStage < 4) {
- _chainStage++;
- debugC(1, kDebugMystery,
- "chainStage advanced to %u after solving mystery %u",
- _chainStage, mn);
- }
- }
+ advanceChainStageAfterSolve(mn);
// `MakeSolvedSound`. `FUN_1d40_05b7` maps E<num>.BIN byte 0 (0..2)
// via table @ 2608:0c5e to VOC slots 0x15/0x16/0x17.
Commit: f2c5be745a959c9ce15f92350b0ee5282c358e2a
https://github.com/scummvm/scummvm/commit/f2c5be745a959c9ce15f92350b0ee5282c358e2a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:17+02:00
Commit Message:
EEM: make sure screenshots contain all the elements from sites
Changed paths:
engines/eem/site.cpp
engines/eem/site.h
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 8358e1d6fe2..7c9112d97a4 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -35,6 +35,27 @@
namespace EEM {
+const uint kSiteBackendActionFlushPriority = 100;
+
+class SiteBackendActionObserverRegistration {
+public:
+ SiteBackendActionObserverRegistration(Common::EventObserver *observer)
+ : _dispatcher(g_system->getEventManager()->getEventDispatcher()),
+ _observer(observer) {
+ if (_dispatcher)
+ _dispatcher->registerObserver(_observer, kSiteBackendActionFlushPriority, false);
+ }
+
+ ~SiteBackendActionObserverRegistration() {
+ if (_dispatcher)
+ _dispatcher->unregisterObserver(_observer);
+ }
+
+private:
+ Common::EventDispatcher *_dispatcher;
+ Common::EventObserver *_observer;
+};
+
// Masked blit using `transp` = high byte of `pic.flags` (`_Rect_Move_Mask @ 1000:03fc`).
void blitFrame(Graphics::ManagedSurface &dst, const Picture &p,
int x, int y, byte transp) {
@@ -843,6 +864,18 @@ void SiteScreen::notePartnerActivity() {
debugC(1, kDebugSite, "Partner impatience: reset to patient");
}
+bool SiteScreen::notifyEvent(const Common::Event &event) {
+ if (event.type != Common::EVENT_CUSTOM_BACKEND_ACTION_START)
+ return false;
+
+ if (_snapshotSite < 0 || g_system->isOverlayVisible())
+ return false;
+
+ syncCompositedScreen();
+ g_system->updateScreen();
+ return false;
+}
+
void SiteScreen::run() {
if (!_mystery || !_mystery->isLoaded())
return;
@@ -851,6 +884,8 @@ void SiteScreen::run() {
uint cur = _mystery->_siteNumber;
if (cur >= _mystery->numSites())
cur = 0;
+ _snapshotSite = -1;
+ SiteBackendActionObserverRegistration backendActionRegistration(this);
enter(cur);
Common::Point mouse = g_system->getEventManager()->getMousePos();
updateHotspotCursor(cur, mouse.x, mouse.y);
@@ -1280,6 +1315,25 @@ void SiteScreen::restoreBgSnapshot() {
0, 0, kScreenWidth, kScreenHeight);
}
+void SiteScreen::syncCompositedScreen() {
+ // OpenGL screenshots read the current backbuffer. After a buffer swap,
+ // that backbuffer can contain an older site frame unless the engine keeps
+ // presenting the full composited screen, even while no animation tick
+ // fired. Copying the current game surface through copyRectToScreen keeps
+ // screenshots in sync without backend-specific changes.
+ Graphics::ManagedSurface snapshot(kScreenWidth, kScreenHeight,
+ Graphics::PixelFormat::createFormatCLUT8());
+
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (!screen)
+ return;
+ snapshot.simpleBlitFrom(*screen);
+ g_system->unlockScreen();
+
+ g_system->copyRectToScreen(snapshot.getPixels(), snapshot.pitch,
+ 0, 0, kScreenWidth, kScreenHeight);
+}
+
void SiteScreen::renderPartner(uint siteNum, uint32 tickMs) {
// `_DoSiteLoop @ 168d:03f4` reads `siteData[+8]` as the speaker
// table index, then for each (speaker x partner) loads:
diff --git a/engines/eem/site.h b/engines/eem/site.h
index 895d2124a9f..1719e68c47f 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -22,6 +22,7 @@
#ifndef EEM_SITE_H
#define EEM_SITE_H
+#include "common/events.h"
#include "common/rect.h"
#include "common/scummsys.h"
@@ -88,7 +89,7 @@ struct Hotspot {
/// Site / scene controller. Mirrors `_DrawSearchButtons @ 2404:0a8f` /
/// `_SearchButtons @ 2404:0bfb` site loop.
-class SiteScreen {
+class SiteScreen : public Common::EventObserver {
public:
SiteScreen(EEMEngine *vm, Mystery *mystery)
: _vm(vm), _mystery(mystery) {}
@@ -100,6 +101,8 @@ public:
void run();
private:
+ bool notifyEvent(const Common::Event &event) override;
+
void renderBackground(uint siteNum);
void renderHotspots(uint siteNum);
int hotspotAtPoint(uint siteNum, int x, int y) const;
@@ -140,6 +143,12 @@ private:
/// Restore the snapshot taken at `captureBgSnapshot` time.
void restoreBgSnapshot();
+ /// Push pixels written through `lockScreen()` back through the backend's
+ /// normal screen-copy path. The SDL/OpenGL screenshot code captures the
+ /// presented backend buffer, so site frames must not leave partner/NPC
+ /// sprites only in the locked software surface.
+ void syncCompositedScreen();
+
/// scanColorCycles: scan Loop 1 for ColorCycle entries (animId == -1),
/// cache (start, end) palette ranges. Mirrors `_DoSiteLoop @ 168d:03f4` init scan.
void scanColorCycles(uint siteNum);
Commit: 24fe380eedd014bab00afd035e6f0289875fb93e
https://github.com/scummvm/scummvm/commit/24fe380eedd014bab00afd035e6f0289875fb93e
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:17+02:00
Commit Message:
EEM: fixed invalid reference to a sound clip in practice mystery
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 3a29e5252cf..98960cd0b83 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -3707,6 +3707,7 @@ void EEMEngine::doAccuse() {
const int foundPoints = _mystery.foundPoints();
uint entryKDSpeak = 0;
uint16 entryTextOff = 0xFFFF;
+ uint16 entryVoiceOverride = 0xFFFF;
bool canAccuse = false;
Common::String entryText;
if (foundPoints == 0) {
@@ -3714,6 +3715,11 @@ void EEMEngine::doAccuse() {
entryText = "3We're not ready to solve this mystery yet. "
"Let's keep investigating until we have some more solid "
"evidence to make our case!";
+ // Practice mystery M0 ships the matching ZeroText takes as the
+ // final two SDB entries, but its KD digital table points kdspeak 9
+ // at earlier hint clips. Use the otherwise unreferenced pair.
+ if (_mystery.number() == 0)
+ entryVoiceOverride = (_partner == kPartnerJake) ? 105 : 104;
} else if (foundPoints < 0x32) {
entryKDSpeak = 0;
entryTextOff = READ_LE_UINT16(entryKdIdx + 0);
@@ -3766,8 +3772,12 @@ void EEMEngine::doAccuse() {
g_system->copyRectToScreen(ms.getPixels(), ms.pitch,
0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
- if (_audio)
- _audio->sayKDDigital(entryKdIdx, entryKDSpeak, _partner);
+ if (_audio) {
+ if (entryVoiceOverride != 0xFFFF)
+ _audio->spoolSound(entryVoiceOverride);
+ else
+ _audio->sayKDDigital(entryKdIdx, entryKDSpeak, _partner);
+ }
waitForInput(60000);
}
if (!canAccuse) {
@@ -4330,7 +4340,8 @@ void EEMEngine::doAccuse() {
_music->stop();
// `_DifferenceAnimation("scrapbk.ani")` @ 1df2:0848.
- playAnm(Common::Path("SCRAPBK.ANI"), 120, true);
+ playAnm(Common::Path("SCRAPBK.ANI"), 120,
+ /* holdLastFrame= */ false);
// `_ShowOneScrap @ 1f78:0773` = `_DisplayEnding(num, 1)`.
doShowEnding(mn);
Commit: 32e5d42d209c4cae47fa86047b2b73bb5c456aed
https://github.com/scummvm/scummvm/commit/32e5d42d209c4cae47fa86047b2b73bb5c456aed
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:17+02:00
Commit Message:
EEM: implemented restored content from the floppy data into the CD one
Changed paths:
A devtools/create_eem/README.md
A devtools/create_eem/create_scrapbk_extra.py
A devtools/create_eem/files/eem/SCRAPBK_EXTRA.ANI
A devtools/create_eem/files/eem/version.txt
A dists/engine-data/eem.dat
dists/engine-data/README
dists/engine-data/engine_data.mk
engines/eem/detection.cpp
engines/eem/detection.h
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/metaengine.cpp
engines/eem/ui.cpp
diff --git a/devtools/create_eem/README.md b/devtools/create_eem/README.md
new file mode 100644
index 00000000000..94234b2f217
--- /dev/null
+++ b/devtools/create_eem/README.md
@@ -0,0 +1,19 @@
+EEM engine data
+-------------------------------------------------------------------------------
+
+`SCRAPBK_EXTRA.ANI` stores the three floppy-only dialog records displayed
+after `SCRAPBK.ANI` and before the scrapbook page. The CD executable omits
+those records from its solved recap data. The restored subtitles are kept for
+all books, but voice indices are only attached for Book 1 mysteries because
+matching unused CD clips could not be identified reliably for Books 2 and 3.
+
+Generate the file from extracted English floppy and CD data:
+
+ ./create_scrapbk_extra.py \
+ --floppy-dir ../../eem-full-game/floppy/EAKIDS/EEM \
+ --cd-dir ../../eem-full-game/cd
+
+Then rebuild the distributable data archive from this directory:
+
+ cd files
+ zip -r9 ../../../dists/engine-data/eem.dat .
diff --git a/devtools/create_eem/create_scrapbk_extra.py b/devtools/create_eem/create_scrapbk_extra.py
new file mode 100755
index 00000000000..2b0b73a47f2
--- /dev/null
+++ b/devtools/create_eem/create_scrapbk_extra.py
@@ -0,0 +1,398 @@
+#!/usr/bin/env python3
+#
+# Extract the floppy-only pre-scrapbook dialog records into an EEM engine
+# data file consumed by the CD runtime.
+
+from __future__ import annotations
+
+import argparse
+import struct
+from dataclasses import dataclass
+from pathlib import Path
+
+
+MAGIC = b"EEMSBX02"
+VERSION = 2
+CASE_COUNT = 55
+NO_VOICE = 0xFFFF
+RESTORED_VOICE_FIRST_CASE = 1
+RESTORED_VOICE_LAST_CASE = 24
+
+
+ at dataclass(frozen=True)
+class ScrapbookRecord:
+ pic_id: int
+ pic_x: int
+ pic_y: int
+ balloon: int
+ balloon_x: int
+ balloon_y: int
+ jake_text: bytes
+ jenny_text: bytes
+ voice_jake: int = NO_VOICE
+ voice_jenny: int = NO_VOICE
+ voice_nancy: int = NO_VOICE
+
+
+def read_u16(data: bytes, offset: int) -> int:
+ if offset < 0 or offset + 2 > len(data):
+ return 0
+ return struct.unpack_from("<H", data, offset)[0]
+
+
+def c_string(data: bytes, offset: int) -> bytes:
+ if offset <= 0 or offset >= len(data):
+ return b""
+ end = data.find(b"\0", offset)
+ if end < 0:
+ end = len(data)
+ return data[offset:end]
+
+
+def valid_floppy_record(data: bytes, offset: int) -> bool:
+ if offset < 0 or offset + 11 > len(data):
+ return False
+ return offset + 11 + data[offset + 10] <= len(data)
+
+
+def sorted_case_numbers(data_dir: Path) -> list[int]:
+ out: list[int] = []
+ for path in data_dir.glob("M*.BIN"):
+ stem = path.stem
+ if len(stem) > 1 and stem[1:].isdigit():
+ out.append(int(stem[1:]))
+ return sorted(out)
+
+
+def extract_floppy_tail(path: Path) -> list[ScrapbookRecord]:
+ data = path.read_bytes()
+ if len(data) < 0x14 or read_u16(data, 0) <= 0x100:
+ raise ValueError(f"{path} is not an EEM floppy mystery file")
+
+ notes_off = read_u16(data, 0x08)
+ solved_off = read_u16(data, 0x12)
+ if solved_off <= 0 or solved_off >= len(data):
+ raise ValueError(f"{path} has no valid solved chain")
+
+ count = data[solved_off]
+ pos = solved_off + 1
+ record_offsets: list[int] = []
+ for _ in range(count):
+ if not valid_floppy_record(data, pos):
+ raise ValueError(f"{path} has a malformed solved-chain record at 0x{pos:04x}")
+ record_offsets.append(pos)
+ pos += 11 + data[pos + 10]
+
+ if len(record_offsets) < 3:
+ return []
+
+ records: list[ScrapbookRecord] = []
+ for offset in record_offsets[-3:]:
+ text_count = data[offset + 10]
+ if text_count != 1:
+ raise ValueError(
+ f"{path} tail record at 0x{offset:04x} has {text_count} text entries"
+ )
+
+ note_idx = data[offset + 11] & 0x7F
+ note_entry = notes_off + note_idx * 7
+ if note_entry + 6 > len(data):
+ raise ValueError(f"{path} tail record references invalid note {note_idx}")
+
+ records.append(ScrapbookRecord(
+ pic_id=read_u16(data, offset + 0),
+ pic_x=read_u16(data, offset + 2),
+ pic_y=data[offset + 4],
+ balloon=data[offset + 5],
+ balloon_x=read_u16(data, offset + 6),
+ balloon_y=data[offset + 8],
+ jake_text=c_string(data, read_u16(data, note_entry + 2)),
+ jenny_text=c_string(data, read_u16(data, note_entry + 4)),
+ ))
+
+ return records
+
+
+def sdx_entry_count(cd_dir: Path, case_num: int) -> int:
+ path = cd_dir / f"M{case_num}.SDX"
+ if not path.exists():
+ return 0
+ return path.stat().st_size // 12
+
+
+def valid_cd_clue_block(data: bytes, offset: int) -> bool:
+ if offset in (0, 0xFFFF) or offset + 4 > len(data):
+ return False
+ count = read_u16(data, offset)
+ return 0 < count <= 64 and offset + 4 + count * 62 <= len(data)
+
+
+def add_voice_ref(used: set[int], raw_voice: int, max_entries: int) -> None:
+ if raw_voice in (0, 0xFFFF):
+ return
+ if raw_voice <= max_entries:
+ used.add(raw_voice - 1)
+
+
+def collect_cd_voice_usage(
+ cd_dir: Path,
+ case_num: int,
+) -> tuple[set[int], list[int], list[int]]:
+ path = cd_dir / f"M{case_num}.BIN"
+ if not path.exists():
+ return set(), [], []
+
+ data = path.read_bytes()
+ max_entries = sdx_entry_count(cd_dir, case_num)
+ used: set[int] = set()
+ solved_jenny: list[int] = []
+ solved_jake: list[int] = []
+
+ def parse_clue_block(offset: int, solved: bool = False) -> None:
+ if not valid_cd_clue_block(data, offset):
+ return
+ count = read_u16(data, offset)
+ for entry_idx in range(count):
+ entry = offset + 4 + entry_idx * 62
+ voice_jenny = read_u16(data, entry + 0x18)
+ voice_jake = read_u16(data, entry + 0x1A)
+
+ # Original _DisplayClue gates voice playback on the Jenny/default
+ # slot; a Jake-only value with a zero Jenny slot is not played.
+ if voice_jenny in (0, 0xFFFF):
+ continue
+ add_voice_ref(used, voice_jenny, max_entries)
+ if solved and voice_jenny <= max_entries:
+ solved_jenny.append(voice_jenny - 1)
+
+ add_voice_ref(used, voice_jake, max_entries)
+ if solved and voice_jake not in (0, 0xFFFF) and voice_jake <= max_entries:
+ solved_jake.append(voice_jake - 1)
+
+ parse_clue_block(read_u16(data, 0x00) + 4)
+ parse_clue_block(read_u16(data, 0x10), solved=True)
+
+ site_index_off = read_u16(data, 0x06)
+ num_sites = read_u16(data, 0x14)
+ for site_idx in range(min(num_sites, 128)):
+ site_index = site_index_off + site_idx * 6
+ if site_index + 6 > len(data):
+ break
+ site_data_off = read_u16(data, site_index)
+ parse_clue_block(read_u16(data, site_index + 2))
+
+ if site_data_off + 8 > len(data):
+ continue
+ hotspot_count = read_u16(data, site_data_off + 6)
+ hotspot_table = read_u16(data, site_index + 4)
+ for hot_idx in range(min(hotspot_count, 300)):
+ hotspot = hotspot_table + hot_idx * 14
+ if hotspot + 14 > len(data):
+ break
+ parse_clue_block(read_u16(data, hotspot + 8))
+
+ gallery_off = read_u16(data, 0x0C)
+ num_suspects = read_u16(data, 0x1A)
+ if gallery_off and gallery_off + num_suspects * 0x46 <= len(data):
+ for suspect_idx in range(num_suspects):
+ entry = gallery_off + suspect_idx * 0x46
+ add_voice_ref(used, read_u16(data, entry + 4), max_entries)
+ add_voice_ref(used, read_u16(data, entry + 6), max_entries)
+
+ kd_text_off = read_u16(data, 0x0E)
+ if kd_text_off and kd_text_off + 0x12 < len(data):
+ digital = kd_text_off + 0x12
+ for kdspeak in (0, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11):
+ for slot in (kdspeak * 2 + 1, kdspeak * 2 + 2):
+ raw_voice = read_u16(data, digital + slot * 2)
+ add_voice_ref(used, raw_voice, max_entries)
+
+ return used, solved_jenny, solved_jake
+
+
+def contiguous_unused_after(used: set[int], sequence: list[int], max_entries: int) -> list[int]:
+ if not sequence:
+ return []
+
+ out: list[int] = []
+ entry = sequence[-1] + 1
+ while entry < max_entries and entry not in used:
+ out.append(entry)
+ entry += 1
+ return out
+
+
+def sound_duration(cd_dir: Path, case_num: int, entry: int) -> float:
+ sdx_path = cd_dir / f"M{case_num}.SDX"
+ sdb_path = cd_dir / f"M{case_num}.SDB"
+ if not sdx_path.exists() or not sdb_path.exists():
+ return 0.0
+
+ sdx_data = sdx_path.read_bytes()
+ if entry < 0 or entry * 12 + 12 > len(sdx_data):
+ return 0.0
+
+ offset = struct.unpack_from("<I", sdx_data, entry * 12)[0]
+ uncompressed_size = struct.unpack_from("<I", sdx_data, entry * 12 + 8)[0]
+ with sdb_path.open("rb") as sdb:
+ sdb.seek(offset)
+ tc_raw = sdb.read(1)
+ if not tc_raw:
+ return 0.0
+
+ tc = tc_raw[0]
+ sample_rate = 1000000 // (256 - tc) if tc < 0xFF else 44100
+ return uncompressed_size / sample_rate if sample_rate else 0.0
+
+
+def find_nancy_voice(
+ cd_dir: Path,
+ case_num: int,
+ used: set[int],
+ partner_voices: set[int],
+ first_partner_voice: int,
+) -> int:
+ max_entries = sdx_entry_count(cd_dir, case_num)
+
+ for entry in range(max(0, first_partner_voice), max_entries):
+ if entry in used or entry in partner_voices:
+ continue
+ if sound_duration(cd_dir, case_num, entry) < 1.2:
+ continue
+ return entry
+
+ return NO_VOICE
+
+
+def attach_cd_voices(cd_dir: Path, case_num: int,
+ records: list[ScrapbookRecord]) -> list[ScrapbookRecord]:
+ if len(records) < 3:
+ return records
+ if not RESTORED_VOICE_FIRST_CASE <= case_num <= RESTORED_VOICE_LAST_CASE:
+ return records
+
+ used, solved_jenny, solved_jake = collect_cd_voice_usage(cd_dir, case_num)
+ max_entries = sdx_entry_count(cd_dir, case_num)
+ jenny_candidates = contiguous_unused_after(used, solved_jenny, max_entries)
+ jake_candidates = contiguous_unused_after(used, solved_jake, max_entries)
+
+ voice_jenny = jenny_candidates[:2] if len(jenny_candidates) >= 2 else []
+ voice_jake = jake_candidates[:2] if len(jake_candidates) >= 2 else []
+ partner_voices = set(voice_jenny) | set(voice_jake)
+ voice_nancy = NO_VOICE
+ if len(voice_jenny) >= 2 and len(voice_jake) >= 2:
+ first_partner_voice = min(voice_jenny[0], voice_jake[0])
+ voice_nancy = find_nancy_voice(cd_dir, case_num, used,
+ partner_voices, first_partner_voice)
+
+ out: list[ScrapbookRecord] = []
+ for idx, record in enumerate(records):
+ extra_idx = idx - 1
+ out.append(ScrapbookRecord(
+ pic_id=record.pic_id,
+ pic_x=record.pic_x,
+ pic_y=record.pic_y,
+ balloon=record.balloon,
+ balloon_x=record.balloon_x,
+ balloon_y=record.balloon_y,
+ jake_text=record.jake_text,
+ jenny_text=record.jenny_text,
+ voice_jake=voice_jake[extra_idx] if 0 <= extra_idx < len(voice_jake) else NO_VOICE,
+ voice_jenny=voice_jenny[extra_idx] if 0 <= extra_idx < len(voice_jenny) else NO_VOICE,
+ voice_nancy=voice_nancy if idx == 0 else NO_VOICE,
+ ))
+ return out
+
+
+def encode_records(cases: list[list[ScrapbookRecord]]) -> bytes:
+ table_size = CASE_COUNT * 8
+ header = bytearray()
+ header += MAGIC
+ header += struct.pack("<HH", VERSION, CASE_COUNT)
+ table = bytearray(table_size)
+ body = bytearray()
+
+ for case_num, records in enumerate(cases):
+ if not records:
+ continue
+
+ offset = len(header) + table_size + len(body)
+ struct.pack_into("<IHH", table, case_num * 8, offset, len(records), 0)
+ for record in records:
+ if len(record.jake_text) > 0xFFFF or len(record.jenny_text) > 0xFFFF:
+ raise ValueError(f"case {case_num} contains a text string longer than 65535 bytes")
+ body += struct.pack(
+ "<HHBBHBBHHHHH",
+ record.pic_id,
+ record.pic_x,
+ record.pic_y,
+ record.balloon,
+ record.balloon_x,
+ record.balloon_y,
+ 0,
+ record.voice_jake,
+ record.voice_jenny,
+ record.voice_nancy,
+ len(record.jake_text),
+ len(record.jenny_text),
+ )
+ body += record.jake_text
+ body += record.jenny_text
+
+ return bytes(header + table + body)
+
+
+def create_scrapbook_extra(floppy_dir: Path, cd_dir: Path, out_path: Path) -> None:
+ cases: list[list[ScrapbookRecord]] = [[] for _ in range(CASE_COUNT)]
+ voiced_cases = 0
+ nancy_cases = 0
+
+ for case_num in sorted_case_numbers(floppy_dir):
+ if case_num == 0 or case_num >= CASE_COUNT:
+ continue
+ records = extract_floppy_tail(floppy_dir / f"M{case_num}.BIN")
+ records = attach_cd_voices(cd_dir, case_num, records)
+ if any(record.voice_jake != NO_VOICE or record.voice_jenny != NO_VOICE
+ for record in records):
+ voiced_cases += 1
+ if any(record.voice_nancy != NO_VOICE for record in records):
+ nancy_cases += 1
+ cases[case_num] = records
+
+ out_path.parent.mkdir(parents=True, exist_ok=True)
+ out_path.write_bytes(encode_records(cases))
+ print(f"Wrote {out_path}")
+ print(f"Cases with text: {sum(1 for records in cases if records)}")
+ print(f"Cases with mapped CD voice clips: {voiced_cases}")
+ print(f"Cases with mapped Nancy clips: {nancy_cases}")
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(
+ description="Create EEM SCRAPBK_EXTRA.ANI from floppy mystery data."
+ )
+ parser.add_argument(
+ "--floppy-dir",
+ type=Path,
+ default=Path("../../eem-full-game/floppy/EAKIDS/EEM"),
+ help="Directory containing English floppy M*.BIN data.",
+ )
+ parser.add_argument(
+ "--cd-dir",
+ type=Path,
+ default=Path("../../eem-full-game/cd"),
+ help="Directory containing CD M*.BIN/M*.SDX data for voice mapping.",
+ )
+ parser.add_argument(
+ "--out",
+ type=Path,
+ default=Path("files/eem/SCRAPBK_EXTRA.ANI"),
+ help="Output file inside devtools/create_eem/files/eem.",
+ )
+ args = parser.parse_args()
+
+ create_scrapbook_extra(args.floppy_dir, args.cd_dir, args.out)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/devtools/create_eem/files/eem/SCRAPBK_EXTRA.ANI b/devtools/create_eem/files/eem/SCRAPBK_EXTRA.ANI
new file mode 100644
index 00000000000..add4d3b906c
Binary files /dev/null and b/devtools/create_eem/files/eem/SCRAPBK_EXTRA.ANI differ
diff --git a/devtools/create_eem/files/eem/version.txt b/devtools/create_eem/files/eem/version.txt
new file mode 100644
index 00000000000..d3827e75a5c
--- /dev/null
+++ b/devtools/create_eem/files/eem/version.txt
@@ -0,0 +1 @@
+1.0
diff --git a/dists/engine-data/README b/dists/engine-data/README
index 1a68494852b..15c2dec6087 100644
--- a/dists/engine-data/README
+++ b/dists/engine-data/README
@@ -27,6 +27,10 @@ Those informations were stored in the original executables.
darkseed.dat:
This file contains essential game data used by the Darkseed engine.
+eem.dat:
+This file contains floppy-only scrapbook dialog data used by the Eagle Eye
+Mysteries engine.
+
drascula.dat:
This file contains essential game data used by the Drascula engine.
diff --git a/dists/engine-data/eem.dat b/dists/engine-data/eem.dat
new file mode 100644
index 00000000000..ae70fa2fe91
Binary files /dev/null and b/dists/engine-data/eem.dat differ
diff --git a/dists/engine-data/engine_data.mk b/dists/engine-data/engine_data.mk
index dbe5527a65d..25341375dbd 100644
--- a/dists/engine-data/engine_data.mk
+++ b/dists/engine-data/engine_data.mk
@@ -13,6 +13,9 @@ endif
ifdef ENABLE_DARKSEED
DIST_FILES_LIST += dists/engine-data/darkseed.dat
endif
+ifdef ENABLE_EEM
+DIST_FILES_LIST += dists/engine-data/eem.dat
+endif
ifdef ENABLE_DRASCULA
DIST_FILES_LIST += dists/engine-data/drascula.dat
endif
diff --git a/engines/eem/detection.cpp b/engines/eem/detection.cpp
index 17ff91e826b..fab534952a9 100644
--- a/engines/eem/detection.cpp
+++ b/engines/eem/detection.cpp
@@ -32,7 +32,7 @@ const PlainGameDescriptor eemGames[] = {
};
#define GUI_OPTIONS_EEM_FLOPPY GUIO4(GAMEOPTION_HIDE_HIGHLIGHT_BOXES, GAMEOPTION_SKIP_REPEATED_CASES, GUIO_MIDIADLIB, GUIO_MIDIMT32)
-#define GUI_OPTIONS_EEM_CD GUIO5(GAMEOPTION_HIDE_HIGHLIGHT_BOXES, GAMEOPTION_FIT_DIALOG_BALLOONS, GAMEOPTION_SKIP_REPEATED_CASES, GUIO_MIDIADLIB, GUIO_MIDIMT32)
+#define GUI_OPTIONS_EEM_CD GUIO6(GAMEOPTION_HIDE_HIGHLIGHT_BOXES, GAMEOPTION_FIT_DIALOG_BALLOONS, GAMEOPTION_SKIP_REPEATED_CASES, GAMEOPTION_RESTORED_CONTENT, GUIO_MIDIADLIB, GUIO_MIDIMT32)
const ADGameDescription gameDescriptions[] = {
{
diff --git a/engines/eem/detection.h b/engines/eem/detection.h
index 937fbe6a4e3..cac7a171f60 100644
--- a/engines/eem/detection.h
+++ b/engines/eem/detection.h
@@ -29,6 +29,7 @@ namespace EEM {
#define GAMEOPTION_HIDE_HIGHLIGHT_BOXES GUIO_GAMEOPTIONS1
#define GAMEOPTION_FIT_DIALOG_BALLOONS GUIO_GAMEOPTIONS2
#define GAMEOPTION_SKIP_REPEATED_CASES GUIO_GAMEOPTIONS3
+#define GAMEOPTION_RESTORED_CONTENT GUIO_GAMEOPTIONS4
enum EEMDebugChannels {
kDebugGeneral = 1 << 0,
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 2ad4b74f687..f6a17a87ca9 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -21,6 +21,7 @@
#include "common/debug.h"
#include "common/debug-channels.h"
+#include "common/engine_data.h"
#include "common/error.h"
#include "common/events.h"
#include "common/file.h"
@@ -198,6 +199,7 @@ EEMEngine::EEMEngine(OSystem *syst, const ADGameDescription *gameDesc)
ConfMan.registerDefault("hide_highlight_boxes", false);
ConfMan.registerDefault("fit_dialog_balloons", false);
ConfMan.registerDefault("skip_repeated_cases", false);
+ ConfMan.registerDefault("restored_content", false);
_variant = (gameDesc && gameDesc->extra &&
Common::String(gameDesc->extra).contains("Floppy"))
@@ -288,6 +290,17 @@ Common::Error EEMEngine::run() {
// _SetMode13X @ 1000:0358 â VGA mode 13h.
initGraphics(kScreenWidth, kScreenHeight);
+ if (!isFloppy() && ConfMan.getBool("restored_content")) {
+ Common::U32String engineDataError;
+ if (!Common::load_engine_data("eem.dat", "eem", 1, 0,
+ engineDataError)) {
+ warning("EEM restored content unavailable: %s",
+ Common::String(engineDataError).c_str());
+ } else {
+ _restoredContentDataLoaded = true;
+ }
+ }
+
if (!openArchives())
return Common::Error(Common::kReadingFailed, "EEM archive open failed");
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index e2ebb1151d0..8b44120f0bc 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -332,6 +332,7 @@ private:
/// (`_DrawGallery_Floppy @ 154e:0050`).
void floppyKDHint(uint kdSlot, const byte *kdIdx,
const byte *bufBase, uint32 mysSize);
+ void displayScrapbookExtra(uint mysteryNum);
void accuseDrawGallery(int highlighted,
Common::Array<Common::Rect> &rects,
Common::Array<int> &suspects, uint8 num,
@@ -531,6 +532,8 @@ private:
/// Lives on the engine because PDA/gallery destroys+recreates SiteScreen.
int _lastSiteArrivalAnim = -1;
+ bool _restoredContentDataLoaded = false;
+
/// `MIDI.C` family (`_MIDIPlayFile`/`_MIDIPlay`/`_StopMIDI`/
/// `_StartTravelMusic` @ 20a2:00e2-05c9). Constructed lazily in `run()`.
MusicPlayer *_music = nullptr;
diff --git a/engines/eem/metaengine.cpp b/engines/eem/metaengine.cpp
index d3efe9c89cc..c2f01eafc03 100644
--- a/engines/eem/metaengine.cpp
+++ b/engines/eem/metaengine.cpp
@@ -66,6 +66,17 @@ const ADExtraGuiOptionsMap optionsList[] = {
0
}
},
+ {
+ GAMEOPTION_RESTORED_CONTENT,
+ {
+ _s("Enable restored content"),
+ _s("Restore floppy release extras in the CD version, including pre-scrapbook conversations and first-try badges."),
+ "restored_content",
+ false,
+ 0,
+ 0
+ }
+ },
AD_EXTRA_GUI_OPTIONS_TERMINATOR
};
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 98960cd0b83..803cc20cf40 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -20,6 +20,7 @@
*/
#include "common/debug.h"
+#include "common/config-manager.h"
#include "common/events.h"
#include "common/file.h"
#include "common/path.h"
@@ -204,6 +205,16 @@ constexpr int kActionScreenDecorY = 0x87;
constexpr byte kChooserCycleStart = 0x6f;
constexpr byte kChooserCycleEnd = 0x73;
constexpr uint32 kChooserCycleMillis = 100;
+const char kScrapbookExtraFilename[] = "SCRAPBK_EXTRA.ANI";
+const byte kScrapbookExtraMagic[] = {
+ 'E', 'E', 'M', 'S', 'B', 'X', '0', '2'
+};
+constexpr uint16 kScrapbookExtraVersion = 2;
+constexpr uint16 kScrapbookExtraCaseCount = 55;
+constexpr uint16 kScrapbookExtraNoVoice = 0xFFFF;
+constexpr uint16 kScrapbookExtraMaxRecords = 16;
+constexpr uint kRestoredContentFirstMystery = 1;
+constexpr uint kRestoredContentLastMystery = 0x18;
bool notebookButtonAt(int x, int y) {
return kPdaHelpRect.contains(x, y) ||
@@ -234,6 +245,26 @@ bool rectListContains(const Common::Array<Common::Rect> &rects, int x, int y) {
return false;
}
+bool readScrapbookExtraText(Common::File &file, uint16 size,
+ Common::String &out) {
+ out.clear();
+ if (size == 0)
+ return true;
+
+ Common::Array<char> buf;
+ buf.resize(size);
+ if (file.read(buf.data(), size) != size)
+ return false;
+
+ out = Common::String(buf.data(), size);
+ return true;
+}
+
+bool restoredContentVoiceAppliesTo(uint mysteryNum) {
+ return mysteryNum >= kRestoredContentFirstMystery &&
+ mysteryNum <= kRestoredContentLastMystery;
+}
+
bool gallerySlotAt(const Common::Array<Common::Rect> &rects,
const Common::Array<int> &suspects, int x, int y) {
for (uint i = 0; i < rects.size() && i < suspects.size(); i++) {
@@ -1087,7 +1118,8 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
return 0;
const bool showFirstTryBadge =
- num < sizeof(_mysteriesSolved) && _mysteriesSolved[num] == 2;
+ num < sizeof(_mysteriesSolved) && _mysteriesSolved[num] == 2 &&
+ (floppyEnding || ConfMan.getBool("restored_content"));
Picture firstTryBadge;
const bool haveFirstTryBadge =
showFirstTryBadge &&
@@ -4343,6 +4375,8 @@ void EEMEngine::doAccuse() {
playAnm(Common::Path("SCRAPBK.ANI"), 120,
/* holdLastFrame= */ false);
+ displayScrapbookExtra(mn);
+
// `_ShowOneScrap @ 1f78:0773` = `_DisplayEnding(num, 1)`.
doShowEnding(mn);
@@ -4438,6 +4472,146 @@ void EEMEngine::floppyKDHint(uint kdSlot, const byte *kdIdx,
}
}
+void EEMEngine::displayScrapbookExtra(uint mysteryNum) {
+ if (isFloppy() || !ConfMan.getBool("restored_content") ||
+ !_restoredContentDataLoaded ||
+ mysteryNum >= kScrapbookExtraCaseCount || !_font.isLoaded())
+ return;
+
+ Common::File file;
+ if (!file.open(Common::Path(kScrapbookExtraFilename))) {
+ warning("EEM restored content unavailable: %s missing",
+ kScrapbookExtraFilename);
+ return;
+ }
+
+ byte magic[sizeof(kScrapbookExtraMagic)];
+ if (file.read(magic, sizeof(magic)) != sizeof(magic) ||
+ memcmp(magic, kScrapbookExtraMagic, sizeof(magic)) != 0)
+ return;
+
+ const uint16 version = file.readUint16LE();
+ const uint16 caseCount = file.readUint16LE();
+ if (version != kScrapbookExtraVersion ||
+ caseCount > kScrapbookExtraCaseCount ||
+ mysteryNum >= caseCount)
+ return;
+
+ const uint32 tableOffset = (uint32)sizeof(kScrapbookExtraMagic) + 4 +
+ mysteryNum * 8;
+ if (!file.seek(tableOffset))
+ return;
+
+ const uint32 recordsOffset = file.readUint32LE();
+ const uint16 recordCount = file.readUint16LE();
+ (void)file.readUint16LE();
+ if (recordsOffset == 0 || recordCount == 0 ||
+ recordCount > kScrapbookExtraMaxRecords ||
+ !file.seek(recordsOffset))
+ return;
+
+ Graphics::ManagedSurface bg(kScreenWidth, kScreenHeight,
+ Graphics::PixelFormat::createFormatCLUT8());
+ bg.clear();
+ if (Graphics::Surface *screen = g_system->lockScreen()) {
+ bg.simpleBlitFrom(*screen);
+ g_system->unlockScreen();
+ }
+
+ for (uint recordIdx = 0; recordIdx < recordCount && !shouldQuit();
+ recordIdx++) {
+ const uint16 picId = file.readUint16LE();
+ const uint16 picX = file.readUint16LE();
+ const uint8 picY = file.readByte();
+ const uint8 balloonRaw = file.readByte();
+ const uint16 balloonX = file.readUint16LE();
+ const uint8 balloonY = file.readByte();
+ (void)file.readByte();
+ const uint16 voiceJake = file.readUint16LE();
+ const uint16 voiceJenny = file.readUint16LE();
+ const uint16 voiceNancy = file.readUint16LE();
+ const uint16 jakeTextSize = file.readUint16LE();
+ const uint16 jennyTextSize = file.readUint16LE();
+
+ if (file.eos() || file.err())
+ return;
+
+ Common::String rawJake;
+ Common::String rawJenny;
+ if (!readScrapbookExtraText(file, jakeTextSize, rawJake) ||
+ !readScrapbookExtraText(file, jennyTextSize, rawJenny))
+ return;
+
+ Common::String text = parseString(
+ _partner == kPartnerJake ? rawJake : rawJenny,
+ _playerName, _partner);
+ if (text.empty())
+ continue;
+
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.simpleBlitFrom(*bg.surfacePtr());
+
+ if (picId != 0 && picId != 0xFFFF) {
+ Picture pic;
+ if (_picsArchive.getPicture(picId, pic) &&
+ picX < kScreenWidth && picY < kScreenHeight) {
+ const byte transp = (byte)(pic.flags >> 8);
+ scratch.transBlitFrom(pic.surface,
+ Common::Point(picX, picY),
+ (uint32)transp);
+ }
+ }
+
+ const uint16 fittedBalloon = fitBalloonToText(balloonRaw, text);
+ const uint16 balloonId = fittedBalloon & 0x7F;
+ const bool flipBalloon = (fittedBalloon & 0x80) != 0;
+ Picture balloon;
+ const bool haveBalloon = balloonRaw != 0xFF &&
+ _balloonArchive.size() > balloonId &&
+ _balloonArchive.loadEntry(balloonId, balloon);
+
+ uint16 textXInset = 5;
+ uint16 textYInset = 4;
+ uint16 textWidth = 155;
+ if (haveBalloon) {
+ const byte transp = (byte)(balloon.flags >> 8);
+ scratch.transBlitFrom(balloon.surface,
+ Common::Point(balloonX, balloonY),
+ (uint32)transp, flipBalloon);
+ getBalloonInsets(balloonId, textXInset, textYInset, textWidth);
+ }
+
+ _font.drawWordWrapped(&scratch, balloonX + textXInset,
+ balloonY + textYInset,
+ MAX<int>(8, (int)textWidth), text,
+ haveBalloon ? 0 : 0xF);
+
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, kScreenWidth, kScreenHeight);
+ g_system->updateScreen();
+
+ if (_audio && restoredContentVoiceAppliesTo(mysteryNum)) {
+ uint16 voice = kScrapbookExtraNoVoice;
+ if (recordIdx == 0 && voiceNancy != kScrapbookExtraNoVoice) {
+ voice = voiceNancy;
+ } else if (recordIdx == 1) {
+ voice = (_partner == kPartnerJake) ? voiceJake : voiceJenny;
+ } else {
+ voice = (_partner == kPartnerJake) ? voiceJenny : voiceJake;
+ }
+ if (voice != kScrapbookExtraNoVoice)
+ _audio->spoolSound(voice);
+ }
+
+ const bool skipRest = floppyDialogWaitForClick();
+ if (_audio)
+ _audio->stopSpool();
+ if (skipRest)
+ return;
+ }
+}
+
void EEMEngine::accuseDrawGallery(int highlighted,
Common::Array<Common::Rect> &rects,
Common::Array<int> &suspects, uint8 num,
Commit: 02418babc46f9de3402039152d424abedec7413e
https://github.com/scummvm/scummvm/commit/02418babc46f9de3402039152d424abedec7413e
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:18+02:00
Commit Message:
EEM: fixes and missing code for the floppy release
Changed paths:
engines/eem/eem.h
engines/eem/graphics.cpp
engines/eem/site.cpp
engines/eem/site.h
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 8b44120f0bc..3a405a74f95 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -193,6 +193,10 @@ public:
/// Locates dialog records in site_data[+6] and dispatches them.
void displayFloppyHotspotDialog(uint siteNum, uint hotIdx);
+ /// `_HotspotSearched_Floppy @ 22dc:096c`. Walks the per-hotspot dialog
+ /// records and returns whether the hotspot's searched text index was seen.
+ bool floppyHotspotSearched(uint siteNum, uint hotspotIdx) const;
+
/// Active player name (= profile-save description).
const Common::String &playerName() const { return _playerName; }
diff --git a/engines/eem/graphics.cpp b/engines/eem/graphics.cpp
index 8f8441b0c41..81c553eb35f 100644
--- a/engines/eem/graphics.cpp
+++ b/engines/eem/graphics.cpp
@@ -124,39 +124,50 @@ uint getBalloonLineCapacity(uint16 balloonId, int lineH) {
return MAX<uint>(1, ((int)insets.indDY - (int)insets.y) / lineH + 1);
}
-// FUN_22dc_096c @ 22dc:096c: walks per-site dialog records at site_data[+6]
-// to skip hotspotIdx hotspots, returns _cluesFound flag for hotspot's first
-// text index.
-bool floppyHotspotSearched(EEM::Mystery &mystery, uint siteIdx,
- uint hotspotIdx) {
- const byte *site = mystery.siteData(siteIdx);
+bool EEMEngine::floppyHotspotSearched(uint siteIdx, uint hotspotIdx) const {
+ // FUN_22dc_096c @ 22dc:096c: walks per-site dialog records at
+ // site_data[+6] to skip hotspotIdx hotspots, then returns _TextSeen for
+ // the selected hotspot's searched text index.
+ const byte *site = _mystery.siteData(siteIdx);
if (!site)
return false;
const uint16 dlgListOff = READ_LE_UINT16(site + 6);
- const byte *bufBase = mystery.blobAt(0);
- if (!bufBase || dlgListOff == 0 || dlgListOff >= mystery.dataSize())
+ const byte *bufBase = _mystery.blobAt(0);
+ const uint32 dsz = _mystery.dataSize();
+ if (!bufBase || dlgListOff == 0 || dlgListOff >= dsz)
return false;
uint32 off = dlgListOff;
for (uint h = 0; h < hotspotIdx; h++) {
- const byte *rec = bufBase + off;
- off += 11u + (uint)rec[10];
- if (off >= mystery.dataSize())
+ if (off + 10 >= dsz)
+ return false;
+ const uint32 mainLen = 11u + (uint)bufBase[off + 10];
+ off += mainLen;
+ if (off >= dsz)
return false;
const uint contCount = (uint)(bufBase[off] & 0x7F);
off += 1;
for (uint c = 0; c < contCount; c++) {
- const byte *cr = bufBase + off;
- off += 11u + (uint)cr[10];
- if (off >= mystery.dataSize())
+ if (off + 10 >= dsz)
+ return false;
+ off += 11u + (uint)bufBase[off + 10];
+ if (off >= dsz)
return false;
}
}
- if (off + 11 >= mystery.dataSize())
+ if (off + 10 >= dsz)
+ return false;
+ const uint32 mainLen = 11u + (uint)bufBase[off + 10];
+ const uint32 contFlagsOff = off + mainLen;
+ if (contFlagsOff >= dsz)
+ return false;
+ uint32 searchedRecOff = off;
+ if ((bufBase[contFlagsOff] & 0x7F) != 0)
+ searchedRecOff = contFlagsOff + 1;
+ if (searchedRecOff + 11 >= dsz || bufBase[searchedRecOff + 10] == 0)
return false;
- const byte *mainRec = bufBase + off;
- const uint8 textIdx = mainRec[11] & 0x7F;
+ const uint8 textIdx = bufBase[searchedRecOff + 11] & 0x7F;
return textIdx < EEM::Mystery::kCluesFoundCap &&
- mystery._cluesFound[textIdx] != 0;
+ _mystery._cluesFound[textIdx] != 0;
}
void EEMEngine::doHelp() {
@@ -215,7 +226,7 @@ void EEMEngine::doHelp() {
for (uint i = 0; i < chainCount; i++) {
const uint8 siteIdx = hd[off + i * 2 + 0];
const uint8 hotspotIdx = hd[off + i * 2 + 1];
- if (!floppyHotspotSearched(_mystery, siteIdx, hotspotIdx)) {
+ if (!floppyHotspotSearched(siteIdx, hotspotIdx)) {
anyChainUnseen = true;
break;
}
@@ -225,7 +236,7 @@ void EEMEngine::doHelp() {
for (uint i = 0; i < extraCount; i++) {
const uint8 siteIdx = hd[extraStart + i * 2 + 0];
const uint8 hotspotIdx = hd[extraStart + i * 2 + 1];
- if (!floppyHotspotSearched(_mystery, siteIdx, hotspotIdx)) {
+ if (!floppyHotspotSearched(siteIdx, hotspotIdx)) {
anyExtraUnseen = true;
break;
}
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 7c9112d97a4..90a2bc011de 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -1352,7 +1352,7 @@ void SiteScreen::renderPartner(uint siteNum, uint32 tickMs) {
int y;
if (_vm->isFloppy()) {
// `_DoSiteLoop_Floppy @ 1652:042b`: site_data+8 is a u16 OFFSET
- // to a 10-byte speakerInfo struct:
+ // to `_SpeakerInfo_Floppy`; the first 10 bytes are the idle pair:
// +0..1 Jake anim, +2..3 Jake X, +4 Jake Y,
// +5..6 Jenny anim, +7..8 Jenny X, +9 Jenny Y.
const uint16 spkOff = READ_LE_UINT16(site + 8);
@@ -1410,6 +1410,44 @@ void SiteScreen::renderPartner(uint siteNum, uint32 tickMs) {
g_system->unlockScreen();
}
+bool SiteScreen::renderFloppyHotspotPartnerPose(uint siteNum) {
+ if (!_vm || !_vm->isFloppy() || !_mystery)
+ return false;
+
+ const byte *site = _mystery->siteData(siteNum);
+ if (!site)
+ return false;
+
+ const uint16 spkOff = READ_LE_UINT16(site + 8);
+ // `_OnSiteHotspotClicked_Floppy @ 1652:017a`: after restoring the site
+ // it loads the active pair from `_SpeakerInfo_Floppy + 0x28/0x2d`.
+ const uint poseOff = (_vm->getPartnerIndex() == kPartnerJake)
+ ? 0x28 : 0x2d;
+ if ((uint32)spkOff + poseOff + 5 > _mystery->dataSize())
+ return false;
+
+ const byte *pose = _mystery->blobAt((uint32)spkOff + poseOff);
+ if (!pose)
+ return false;
+
+ const uint16 animId = READ_LE_UINT16(pose + 0);
+ const int x = (int)READ_LE_UINT16(pose + 2);
+ const int y = (int)pose[4];
+
+ Animation anim;
+ if (!_vm->getAni().loadAnimation(animId, anim) || anim.empty())
+ return false;
+
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (!screen)
+ return false;
+
+ const uint frameIdx = partnerFrameAtTick(animId, (uint)anim.size(), 0);
+ blitAnimFrameAnchored(screen, anim[frameIdx], x, y);
+ g_system->unlockScreen();
+ return true;
+}
+
void SiteScreen::renderBackground(uint siteNum) {
// `_BuildBackground(sitepic, 0x42, 0x14)` from `_DoSiteLoop @
// 168d:03f4` / `_DisplayCorrect`:
@@ -1464,6 +1502,17 @@ void bumpHotspotEdgeColor(byte &color) {
color = (next > 0xFE) ? (byte)0xF9 : next;
}
+byte currentWhitePaletteIndex(byte fallback) {
+ byte palette[256 * 3];
+ g_system->getPaletteManager()->grabPalette(palette, 0, 256);
+ for (uint i = 0; i < 256; i++) {
+ const byte *rgb = palette + i * 3;
+ if (rgb[0] >= 0xFC && rgb[1] >= 0xFC && rgb[2] >= 0xFC)
+ return (byte)i;
+ }
+ return fallback;
+}
+
void SiteScreen::renderHotspots(uint siteNum) {
// `_DrawSearchButtons`. Port adds optional "hide hint" setting.
if (ConfMan.getBool("hide_highlight_boxes"))
@@ -1502,9 +1551,14 @@ void SiteScreen::renderHotspots(uint siteNum) {
// +0xc..d extra (CD cursor ID for `_SwitchMouse`; shipped = 0)
// Seen key = the +0xa ordinal (so unrelated hotspots on later sites
// don't inherit the first site's seen state after travel/reload).
- // Floppy = 8-byte plain rect only; seen key falls back to row index.
+ // Floppy = 8-byte plain rect only; searched state is derived by
+ // walking the dialog record list, like `_HotspotSearched_Floppy`.
const bool floppy = _vm && _vm->isFloppy();
const uint stride = floppy ? 8 : 14;
+ // The floppy SITEPALS has at least one searchable site where 0xFF is
+ // yellow. The CD corrected that palette data; for floppy, draw with an
+ // existing white entry from the current palette instead of changing it.
+ const byte searchedColor = floppy ? currentWhitePaletteIndex(0xFF) : 0xFF;
for (uint i = 0; i < count; i++) {
const byte *r = spots + i * stride;
const int16 x1 = (int16)READ_LE_UINT16(r + 0);
@@ -1514,12 +1568,17 @@ void SiteScreen::renderHotspots(uint siteNum) {
const Common::Rect rect(MAX<int>(0, x1), MAX<int>(0, y1),
MIN<int>(screen->w, x2),
MIN<int>(screen->h, y2));
- const uint seenKey = floppy ? i : READ_LE_UINT16(r + 0xa);
- const bool seen = seenKey < Mystery::kHotSpotsCap &&
- _mystery->_hotSpotsSeen[seenKey];
+ bool seen = false;
+ if (floppy) {
+ seen = _vm->floppyHotspotSearched(siteNum, i);
+ } else {
+ const uint seenKey = READ_LE_UINT16(r + 0xa);
+ seen = seenKey < Mystery::kHotSpotsCap &&
+ _mystery->_hotSpotsSeen[seenKey];
+ }
if (seen) {
- // `_DrawSolidRect @ 172b:0506` â solid 0xFF (non-cycling).
- screen->frameRect(rect, 0xFF);
+ // `_DrawSolidRect @ 172b:0506` â solid, non-cycling outline.
+ screen->frameRect(rect, searchedColor);
} else {
// `_DrawRect @ 172b:03e2` â walk all four edges incrementing
// the colour per pixel through palette indices 0xF9..0xFE,
@@ -1587,20 +1646,29 @@ void SiteScreen::onHotspotClicked(uint siteNum, uint hotIdx) {
debugC(1, kDebugSite, "Site %u: hotspot %u clicked", siteNum, hotIdx);
// Floppy: 8-byte rects only (no clue metadata @ +0xa/+8). Dialog
- // records live in a separate list @ `site_data[+6]`. Seen key =
- // array index (only ordinal available).
+ // records live in a separate list @ `site_data[+6]`.
if (_vm->isFloppy()) {
if (hotIdx < Mystery::kHotSpotsCap)
_mystery->_hotSpotsSeen[hotIdx] = 1;
_mystery->_searchLocationNumber = (uint16)hotIdx;
+
// Snapshot `_cluesFound` before dialog â autosave on new clue.
// Floppy side-effect path is `displayFloppyDialogRecords`
// (clues.cpp), not `displayClue` â autosave must be duplicated.
byte before[Mystery::kCluesFoundCap];
memcpy(before, _mystery->_cluesFound, sizeof(before));
+
+ restoreBgSnapshot();
+ const uint32 now = g_system->getMillis();
+ renderAnimatedDrops(siteNum, now);
+ if (!renderFloppyHotspotPartnerPose(siteNum))
+ renderPartner(siteNum, now);
+ g_system->updateScreen();
+
_vm->setPartnerEraseBg(&_bgSnapshot);
_vm->displayFloppyHotspotDialog(siteNum, hotIdx);
_vm->setPartnerEraseBg(nullptr);
+
bool foundNewClue = false;
for (uint i = 0; i < Mystery::kCluesFoundCap; i++) {
if (!before[i] && _mystery->_cluesFound[i]) {
diff --git a/engines/eem/site.h b/engines/eem/site.h
index 1719e68c47f..cc1b798e453 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -121,6 +121,10 @@ private:
/// Mirrors `_GetAnimation` + `_NewAnimation` tail of `_DoSiteLoop @ 168d:03f4`.
void renderPartner(uint siteNum, uint32 tickMs);
+ /// Floppy active speaker pose shown before `_HandleHotspotClick_Floppy`.
+ /// Uses `_SpeakerInfo_Floppy + 0x28/0x2d`, replacing the idle partner.
+ bool renderFloppyHotspotPartnerPose(uint siteNum);
+
/// renderStaticDrops: `_AddDrop` static decorations (Loop 2).
/// siteData[+0x4] count, siteData[+0xc] entries (6 bytes: {picId, x, y}).
void renderStaticDrops(uint siteNum);
Commit: 558a00c546844d6a51f146cf6bc3140d11283767
https://github.com/scummvm/scummvm/commit/558a00c546844d6a51f146cf6bc3140d11283767
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:18+02:00
Commit Message:
EEM: use skip api instead of (void)
Changed paths:
engines/eem/animation.cpp
engines/eem/audio.cpp
engines/eem/resource.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/animation.cpp b/engines/eem/animation.cpp
index c52ee496293..bf534e23d5f 100644
--- a/engines/eem/animation.cpp
+++ b/engines/eem/animation.cpp
@@ -101,12 +101,10 @@ bool ANMDecoder::open(const Common::Path &path) {
return false;
}
- (void)_file.readUint16LE(); // header[+0]: ignored
+ _file.skip(2); // header[+0]: ignored
_height = _file.readUint16LE(); // header[+2]
_width = _file.readUint16LE(); // header[+4]
- (void)_file.readUint16LE(); // header[+6]
- (void)_file.readUint16LE(); // header[+8]
- (void)_file.readUint16LE(); // header[+10]
+ _file.skip(6); // header[+6..+10]: ignored
if (_width == 0 || _height == 0) {
warning("ANMDecoder: zero dimensions in %s", path.toString().c_str());
diff --git a/engines/eem/audio.cpp b/engines/eem/audio.cpp
index 10df7170c3c..b20f5553138 100644
--- a/engines/eem/audio.cpp
+++ b/engines/eem/audio.cpp
@@ -250,7 +250,7 @@ void AudioPlayer::spoolSound(uint num) {
// byte 0 = Sound Blaster Time Constant
// byte 1 = total AIL playback blocks (internal to AIL DDS; unused here)
const byte tc = sdb.readByte();
- (void)sdb.readByte(); // AIL block count, unused outside AIL
+ sdb.skip(1); // AIL block count, unused outside AIL
// SB Time Constant formula: rate = 1000000 / (256 - tc).
// e.g. tc=0xD2 -> 22 kHz. tc=0xFF would divide by 1 (1 MHz, nonsense); clamp.
diff --git a/engines/eem/resource.cpp b/engines/eem/resource.cpp
index deb49700d57..d8722ceb598 100644
--- a/engines/eem/resource.cpp
+++ b/engines/eem/resource.cpp
@@ -131,7 +131,7 @@ bool DBDArchive::loadEntry(uint num, Picture &out) {
}
// _GetFromDB @ 172b:105d. Leading u16 = frame count (always 1 for pictures).
- (void)_dbd.readUint16LE();
+ _dbd.skip(2);
return readFrame(_dbd, entry.compressed != 0, out);
}
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 803cc20cf40..420c799701c 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -4504,7 +4504,7 @@ void EEMEngine::displayScrapbookExtra(uint mysteryNum) {
const uint32 recordsOffset = file.readUint32LE();
const uint16 recordCount = file.readUint16LE();
- (void)file.readUint16LE();
+ file.skip(2); // reserved
if (recordsOffset == 0 || recordCount == 0 ||
recordCount > kScrapbookExtraMaxRecords ||
!file.seek(recordsOffset))
@@ -4526,7 +4526,7 @@ void EEMEngine::displayScrapbookExtra(uint mysteryNum) {
const uint8 balloonRaw = file.readByte();
const uint16 balloonX = file.readUint16LE();
const uint8 balloonY = file.readByte();
- (void)file.readByte();
+ file.skip(1); // reserved
const uint16 voiceJake = file.readUint16LE();
const uint16 voiceJenny = file.readUint16LE();
const uint16 voiceNancy = file.readUint16LE();
Commit: 209d590655e95cccf581ef3caafced6266617060
https://github.com/scummvm/scummvm/commit/209d590655e95cccf581ef3caafced6266617060
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:19+02:00
Commit Message:
EEM: moved cleanMysterySound after sdx/sdb declarations
Changed paths:
engines/eem/audio.cpp
diff --git a/engines/eem/audio.cpp b/engines/eem/audio.cpp
index b20f5553138..1a553b47c00 100644
--- a/engines/eem/audio.cpp
+++ b/engines/eem/audio.cpp
@@ -139,13 +139,13 @@ bool AudioPlayer::readSdxIndex(const Common::Path &sdxPath) {
// _InitMysterySounds @ 202f:05cb. Strings "m%u.sdx" @ 29be:144f, "m%u.sdb" @ 29be:145b.
bool AudioPlayer::initMysterySounds(uint mysteryNum) {
- cleanMysterySounds();
-
const Common::String sdxName = Common::String::format("M%u.SDX", mysteryNum);
const Common::String sdbName = Common::String::format("M%u.SDB", mysteryNum);
const Common::Path sdxPath(sdxName);
const Common::Path sdbPath(sdbName);
+ cleanMysterySounds();
+
if (!readSdxIndex(sdxPath)) {
_sdxIndex.clear();
return false;
Commit: a789df41d3064cf78e1bb1d8490676d6ff5ab174
https://github.com/scummvm/scummvm/commit/a789df41d3064cf78e1bb1d8490676d6ff5ab174
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:19+02:00
Commit Message:
EEM: reduced some technical comments
Changed paths:
engines/eem/clues.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index ee88f8a8a43..f4774f6fc9d 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -316,17 +316,12 @@ void EEMEngine::doInitClues() {
&& _aniArchive.loadAnimation(0x19, nancy)
&& !nancy.empty();
- // Cycle game animation once at _CheckFrameRate cadence (~7 fps, 140 ms
- // per _UpdateAnimations call â `LastFrame + 0xe` cs in _InitFrameCounter
- // @ 1a35:01ae). Original loop @ 1a35:0507:
- // uVar9 = 1;
- // while (uVar9 != gameNum) {
- // if (_CheckFrameRate || skipped) { _UpdateAnimations(); uVar9++; }
- // }
- // So `gameNum - 1` _UpdateAnimations calls; each call advances every
+ // Cycle the game animation once at _CheckFrameRate cadence (~7 fps, 140 ms
+ // per tick; _InitFrameCounter @ 1a35:01ae). The original loop @ 1a35:0507
+ // makes `gameNum - 1` _UpdateAnimations calls, each advancing every
// registered slot by one script tick. _DoInitClues @ 1a35:0507/0541
- // hard-codes the SCRIPT index to Jake's IDs (0x17/0x18/0x19) regardless
- // of partner, so we look up scripts by those IDs unconditionally.
+ // hard-codes Jake's script IDs (0x17/0x18/0x19) regardless of partner, so
+ // we look up scripts by those IDs unconditionally.
if (haveGame || haveBook || haveNancy) {
const uint kCheckFrameRateMs = 140;
const uint baseFrames = haveGame ? game.size() : 8;
@@ -802,14 +797,9 @@ void EEMEngine::displayClue(const byte *clueBlock) {
g_system->updateScreen();
}
- // _DisplayClue @ 2404:0833-085a â per-clue voice gate:
- // if (clue[+0x18] != 0 && voiceOn && voiceAvail) {
- // iVar6 = clue[+0x18]; // Jenny default
- // if (Partner == 0) iVar6 = clue[+0x1a]; // Jake override
- // _SpoolSound(iVar6 - 1);
- // }
- // Gate is on the Jenny slot regardless of partner; entries with
- // +0x18 == 0 but +0x1a set are text-only.
+ // _DisplayClue @ 2404:0833-085a â per-clue voice gate. The gate is on
+ // the Jenny slot regardless of partner; entries with +0x18 == 0 but
+ // +0x1a set are text-only.
if (_audio) {
const uint16 voiceJenny = READ_LE_UINT16(c + 0x18);
if (voiceJenny != 0 && voiceJenny != 0xFFFF) {
Commit: c63d19aea9e9f83d26c965f851b7b7b0a8c40e90
https://github.com/scummvm/scummvm/commit/c63d19aea9e9f83d26c965f851b7b7b0a8c40e90
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:19+02:00
Commit Message:
EEM: remove dead code related with fallback pointer
Changed paths:
engines/eem/eem.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index f6a17a87ca9..e531f32c460 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -66,37 +66,6 @@ const byte kSaveBodyVer = 1;
// option or changing save format. Set false before release.
const bool kDebugPopulateScrapbook1AtStartup = false;
-// Fallback 11x16 cursor used if PIC pointer load fails.
-// 0 = transparent, 1 = black outline, 2 = white fill
-const byte kCursorBitmap[11 * 16] = {
- 1,1,0,0,0,0,0,0,0,0,0,
- 1,2,1,0,0,0,0,0,0,0,0,
- 1,2,2,1,0,0,0,0,0,0,0,
- 1,2,2,2,1,0,0,0,0,0,0,
- 1,2,2,2,2,1,0,0,0,0,0,
- 1,2,2,2,2,2,1,0,0,0,0,
- 1,2,2,2,2,2,2,1,0,0,0,
- 1,2,2,2,2,2,2,2,1,0,0,
- 1,2,2,2,2,2,2,2,2,1,0,
- 1,2,2,2,2,2,2,2,2,2,1,
- 1,2,2,2,2,2,1,0,0,0,0,
- 1,2,1,0,1,2,2,1,0,0,0,
- 1,1,0,0,1,2,2,1,0,0,0,
- 0,0,0,0,0,1,2,2,1,0,0,
- 0,0,0,0,0,1,2,2,1,0,0,
- 0,0,0,0,0,0,1,2,2,1,0
-};
-const byte kCursorPalette[] = {
- 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00,
- 0xFF, 0xFF, 0xFF
-};
-const byte kCursorInteractivePalette[] = {
- 0x00, 0x00, 0x00,
- 0xFF, 0x00, 0x00,
- 0xFF, 0xFF, 0xFF
-};
-
void fadeCurrentPaletteToBlack(uint delayMs = 8) {
byte start[kPalSize];
byte stepPal[kPalSize];
@@ -173,23 +142,15 @@ void setInteractiveCursorPalette(const Picture &cursor, byte transparent) {
void installMouseCursor(DBDArchive &pics, bool interactive) {
Picture cursor;
- if (pics.getPicture(kPicMousePointer, cursor) && !cursor.surface.empty()) {
- const byte transparent = (byte)(cursor.flags >> 8);
- CursorMan.replaceCursor(cursor.surface.rawSurface(), 0, 0,
- transparent);
- if (interactive)
- setInteractiveCursorPalette(cursor, transparent);
- else
- CursorMan.replaceCursorPalette(nullptr, 0, 0);
- return;
- }
+ if (!pics.getPicture(kPicMousePointer, cursor) || cursor.surface.empty())
+ error("EEM: mouse cursor PIC 0x%x missing", kPicMousePointer);
- warning("EEM: mouse cursor PIC 0x%x missing; using fallback cursor",
- kPicMousePointer);
- CursorMan.replaceCursor(kCursorBitmap, 11, 16, 0, 0, 0);
- CursorMan.replaceCursorPalette(interactive ? kCursorInteractivePalette
- : kCursorPalette,
- 0, 3);
+ const byte transparent = (byte)(cursor.flags >> 8);
+ CursorMan.replaceCursor(cursor.surface.rawSurface(), 0, 0, transparent);
+ if (interactive)
+ setInteractiveCursorPalette(cursor, transparent);
+ else
+ CursorMan.replaceCursorPalette(nullptr, 0, 0);
}
EEMEngine::EEMEngine(OSystem *syst, const ADGameDescription *gameDesc)
Commit: 79bf202d8cb02e2f41f86b80aa0a71cc23b7ce12
https://github.com/scummvm/scummvm/commit/79bf202d8cb02e2f41f86b80aa0a71cc23b7ce12
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:20+02:00
Commit Message:
EEM: boy -> jake, girl -> jenny
Changed paths:
engines/eem/clues.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index f4774f6fc9d..3f70172723b 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -41,24 +41,24 @@ namespace EEM {
// _DoChoosePartner @ 1a35:0756.
const uint kPicChooseBackground = 0x8c; // _GetBackground(0x8c)
-const uint kAniBoy = 8; // Jake
-const uint kAniGirl = 9; // Jenny
+const uint kAniJake = 8;
+const uint kAniJenny = 9;
// _DoHappiness @ 172b:27b5 â cursor X picks one of 4 rects @ 29be:030f.
// Past rect 3 = level 4. Constexpr (Point, w, h) form to avoid a global
// constructor (-Wglobal-constructors).
constexpr Common::Rect kHappyZones[4] = {
- Common::Rect(Common::Point( 0, 0), 70, 200), // far left â girl very happy, boy neutral
- Common::Rect(Common::Point( 70, 0), 56, 200), // girl's column
+ Common::Rect(Common::Point( 0, 0), 70, 200), // far left â Jenny very happy, Jake neutral
+ Common::Rect(Common::Point( 70, 0), 56, 200), // Jenny's column
Common::Rect(Common::Point(126, 0), 56, 200), // middle
- Common::Rect(Common::Point(182, 0), 53, 200), // boy's column
+ Common::Rect(Common::Point(182, 0), 53, 200), // Jake's column
};
// _NewAnimation positions @ 1a35:07b9 / 07d5.
-const int kBoyX = 0xe2; // 226
-const int kBoyY = 0x62; // 98
-const int kGirlX = 0x42; // 66
-const int kGirlY = 0x60; // 96
+const int kJakeX = 0xe2; // 226
+const int kJakeY = 0x62; // 98
+const int kJennyX = 0x42; // 66
+const int kJennyY = 0x60; // 96
uint markClueBlockNotebookEntries(Mystery &mystery, const byte *clueBlock) {
if (!clueBlock)
@@ -84,16 +84,16 @@ uint markClueBlockNotebookEntries(Mystery &mystery, const byte *clueBlock) {
}
// _DoHappiness @ 172b:27b5 â per-zone sequence scripts.
-// Boy seqs @ 29be:0337 (5 Ã 0x14 bytes), girl seqs @ 29be:039b. 9 frames each;
+// Jake seqs @ 29be:0337 (5 Ã 0x14 bytes), Jenny seqs @ 29be:039b. 9 frames each;
// the anim cells contain 10 cells = pairs of (neutral, smile) at 5 intensities.
-const uint8 kBoySeqs[5][9] = {
+const uint8 kJakeSeqs[5][9] = {
{ 0,0,0,0,0,0,0,1,0 }, // level 0
{ 2,2,2,2,2,2,2,3,2 }, // level 1
{ 4,4,4,4,4,4,4,5,4 }, // level 2
{ 6,6,6,6,6,6,7,6,6 }, // level 3
{ 8,8,8,8,8,8,8,8,9 }, // level 4 (cursor past zone 3)
};
-const uint8 kGirlSeqs[5][9] = {
+const uint8 kJennySeqs[5][9] = {
{ 8,9,8,8,8,8,8,8,8 },
{ 6,6,6,7,6,6,6,6,6 },
{ 4,4,5,4,4,4,4,4,4 },
@@ -139,7 +139,7 @@ void blitRawToScreen(const Picture &p, int x, int y) {
x, y, w, h);
}
-// _DoChoosePartner @ 1a35:0756. The original places boy + girl animations
+// _DoChoosePartner @ 1a35:0756. The original places Jake + Jenny animations
// on a backdrop and polls four click rectangles (two per character); we
// approximate with a single split at x=160 (left=Jenny, right=Jake).
void EEMEngine::doChoosePartner() {
@@ -149,14 +149,14 @@ void EEMEngine::doChoosePartner() {
return;
}
- Animation boyAnim;
- if (!_aniArchive.loadAnimation(kAniBoy, boyAnim) || boyAnim.empty()) {
- warning("Boy animation (%u) load failed", kAniBoy);
+ Animation jakeAnim;
+ if (!_aniArchive.loadAnimation(kAniJake, jakeAnim) || jakeAnim.empty()) {
+ warning("Jake animation (%u) load failed", kAniJake);
return;
}
- Animation girlAnim;
- if (!_aniArchive.loadAnimation(kAniGirl, girlAnim) || girlAnim.empty()) {
- warning("Girl animation (%u) load failed", kAniGirl);
+ Animation jennyAnim;
+ if (!_aniArchive.loadAnimation(kAniJenny, jennyAnim) || jennyAnim.empty()) {
+ warning("Jenny animation (%u) load failed", kAniJenny);
return;
}
@@ -168,16 +168,16 @@ void EEMEngine::doChoosePartner() {
uint seqIdx = 0;
blitAt(background, 0, 0);
- blitAt(girlAnim[kGirlSeqs[level][seqIdx % 9] % girlAnim.size()],
- kGirlX, kGirlY);
- blitAt(boyAnim [kBoySeqs [level][seqIdx % 9] % boyAnim.size()],
- kBoyX, kBoyY);
+ blitAt(jennyAnim[kJennySeqs[level][seqIdx % 9] % jennyAnim.size()],
+ kJennyX, kJennyY);
+ blitAt(jakeAnim [kJakeSeqs [level][seqIdx % 9] % jakeAnim.size()],
+ kJakeX, kJakeY);
g_system->updateScreen();
- debugC(1, kDebugGeneral, "ChoosePartner: %u boy frames at (%d,%d), "
- "%u girl frames at (%d,%d)",
- (uint)boyAnim.size(), kBoyX, kBoyY,
- (uint)girlAnim.size(), kGirlX, kGirlY);
+ debugC(1, kDebugGeneral, "ChoosePartner: %u Jake frames at (%d,%d), "
+ "%u Jenny frames at (%d,%d)",
+ (uint)jakeAnim.size(), kJakeX, kJakeY,
+ (uint)jennyAnim.size(), kJennyX, kJennyY);
uint32 lastTick = g_system->getMillis();
while (!shouldQuit()) {
@@ -189,10 +189,10 @@ void EEMEngine::doChoosePartner() {
lastTick = g_system->getMillis();
seqIdx = (seqIdx + 1) % 9;
blitAt(background, 0, 0);
- blitAt(girlAnim[kGirlSeqs[level][seqIdx % 9] % girlAnim.size()],
- kGirlX, kGirlY);
- blitAt(boyAnim [kBoySeqs [level][seqIdx % 9] % boyAnim.size()],
- kBoyX, kBoyY);
+ blitAt(jennyAnim[kJennySeqs[level][seqIdx % 9] % jennyAnim.size()],
+ kJennyX, kJennyY);
+ blitAt(jakeAnim [kJakeSeqs [level][seqIdx % 9] % jakeAnim.size()],
+ kJakeX, kJakeY);
g_system->updateScreen();
}
@@ -211,10 +211,10 @@ void EEMEngine::doChoosePartner() {
level = newLevel;
seqIdx = 0; // restart cycle so the gesture pops
blitAt(background, 0, 0);
- blitAt(girlAnim[kGirlSeqs[level][seqIdx % 9] % girlAnim.size()],
- kGirlX, kGirlY);
- blitAt(boyAnim [kBoySeqs [level][seqIdx % 9] % boyAnim.size()],
- kBoyX, kBoyY);
+ blitAt(jennyAnim[kJennySeqs[level][seqIdx % 9] % jennyAnim.size()],
+ kJennyX, kJennyY);
+ blitAt(jakeAnim [kJakeSeqs [level][seqIdx % 9] % jakeAnim.size()],
+ kJakeX, kJakeY);
g_system->updateScreen();
}
}
Commit: 8f54582d7d8cfdab891a8630ee6e5c0a6c8535d3
https://github.com/scummvm/scummvm/commit/8f54582d7d8cfdab891a8630ee6e5c0a6c8535d3
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:20+02:00
Commit Message:
EEM: EEM2 proof-of-concept (intro)
Changed paths:
engines/eem/detection.cpp
engines/eem/eem.cpp
engines/eem/eem.h
diff --git a/engines/eem/detection.cpp b/engines/eem/detection.cpp
index fab534952a9..62d6c3901bd 100644
--- a/engines/eem/detection.cpp
+++ b/engines/eem/detection.cpp
@@ -28,6 +28,7 @@ namespace EEM {
const PlainGameDescriptor eemGames[] = {
{ "eem", "Eagle Eye Mysteries" },
+ { "eem2", "Eagle Eye Mysteries in London" },
{ nullptr, nullptr }
};
@@ -66,6 +67,20 @@ const ADGameDescription gameDescriptions[] = {
ADGF_NO_FLAGS,
GUI_OPTIONS_EEM_FLOPPY
},
+ {
+ // Eagle Eye Mysteries in London (EEM2CD.EXE) â proof of concept.
+ // The sequel reuses this engine's resource formats (DBD/DBX
+ // archives, SITEPALS palettes, FONT.FNT), so only the opening
+ // screens load for now; flagged unstable until properly supported.
+ "eem2",
+ "CD",
+ AD_ENTRY2s("EEM2CD.EXE", "211a376b23a1b6259d0c36cf46d26ed4", 172560,
+ "PICS.DBD", "da0b13a117bc3a207aec907c05769cd8", 2972988),
+ Common::EN_ANY,
+ Common::kPlatformDOS,
+ ADGF_UNSTABLE,
+ GUIO2(GUIO_MIDIADLIB, GUIO_MIDIMT32)
+ },
AD_TABLE_END_MARKER
};
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index e531f32c460..56e839889e2 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -165,6 +165,11 @@ EEMEngine::EEMEngine(OSystem *syst, const ADGameDescription *gameDesc)
_variant = (gameDesc && gameDesc->extra &&
Common::String(gameDesc->extra).contains("Floppy"))
? kVariantFloppy : kVariantCD;
+ // EEM2 ("...in London") ships as a separate detection entry (gameId
+ // "eem2"); it reuses this engine but only the opening screens work.
+ if (gameDesc && gameDesc->gameId &&
+ Common::String(gameDesc->gameId) == "eem2")
+ _variant = kVariantLondonCD;
_language = gameDesc ? gameDesc->language : Common::EN_ANY;
}
@@ -296,6 +301,17 @@ Common::Error EEMEngine::run() {
debugC(1, kDebugGeneral, "EEM engine starting");
+ // EEM2 ("Eagle Eye Mysteries in London") proof of concept. The archives,
+ // SITEPALS. palettes, font and mouse cursor above all load from EEM2's
+ // data using the same code paths as EEM1, which already proves the
+ // shared resource formats. Everything below here (saves, mystery/site
+ // state machine) is EEM1-specific, so the PoC just renders EEM2's
+ // opening logo screens and exits.
+ if (isLondon()) {
+ runLondonScreensPoc();
+ return Common::kNoError;
+ }
+
// Resume from save: mystery in progress â MAP (handler 0 @ 1a35:0e1d);
// otherwise â ACTION.
const int wantedSave = ConfMan.hasKey("save_slot")
@@ -630,8 +646,11 @@ bool EEMEngine::openArchives() {
bool EEMEngine::loadSitePalettes() {
Common::File f;
- if (!f.open(Common::Path("SITEPALS"))) {
- warning("SITEPALS missing");
+ // EEM1 ships "SITEPALS" (40 palettes); EEM2/London ships "SITEPALS."
+ // with a trailing dot (63 palettes). _ReadPalettes @ 17ee:0cdb.
+ const char *palFile = isLondon() ? "SITEPALS." : "SITEPALS";
+ if (!f.open(Common::Path(palFile))) {
+ warning("%s missing", palFile);
return false;
}
_sitePals.resize(f.size());
@@ -645,7 +664,9 @@ bool EEMEngine::loadSitePalettes() {
}
bool EEMEngine::getSitePalette(uint num, byte *out) const {
- if (num >= kNumSitePals || _sitePals.size() < (num + 1) * kPalSize)
+ // EEM1 SITEPALS has kNumSitePals (40) entries; EEM2/London SITEPALS.
+ // has 63. Validate against the actual loaded buffer so both work.
+ if (_sitePals.size() < (num + 1) * kPalSize)
return false;
// SITEPALS stores 6-bit VGA-DAC values (0..63); ScummVM expects
// 8-bit (0..255), so left-shift by 2 like the original VGA hardware.
@@ -999,6 +1020,65 @@ void EEMEngine::showHighScoreLogo() {
fadeCurrentPaletteToBlack();
}
+void EEMEngine::showLondonLogo(uint picId, uint palId, uint holdMs) {
+ // Parameterised twin of `showHighScoreLogo` for EEM2's opening logos
+ // (`_ShowEAKids` @ 2721:05e3 and `_ShowHScoreLogo` @ 2721:084d both do
+ // _GetPicture(pic) -> blit -> _GetPalette(pal) -> fade in -> hold ->
+ // fade out).
+ Picture pic;
+ if (!_picsArchive.getPicture(picId, pic) || pic.surface.empty()) {
+ warning("London logo PIC 0x%x load failed", picId);
+ return;
+ }
+ blitAt(pic, 0, 0);
+
+ byte target[kPalSize];
+ if (!getSitePalette(palId, target)) {
+ warning("London palette 0x%x load failed", palId);
+ return;
+ }
+ byte black[kPalSize] = {};
+ g_system->getPaletteManager()->setPalette(black, 0, 256);
+ g_system->updateScreen();
+ fadePaletteFromBlack(target);
+
+ waitForInput(holdMs);
+ fadeCurrentPaletteToBlack();
+}
+
+void EEMEngine::runLondonScreensPoc() {
+ // EEM2 opening logos â still-image portion of `_DoOpeningAnims`
+ // @ 2721:08e6. Picture/palette IDs come from RE of EEM2CD.EXE:
+ // _ShowEAKids @ 2721:05e3 â _GetPicture(0x54), _GetPalette(0x3c)
+ // _ShowStormLogo @ 2721:0729 â BOLT.ANM + THUNDER.VOC
+ // _ShowHScoreLogo @ 2721:084d â _GetPicture(0x356), _GetPalette(0x3d)
+ CursorMan.showMouse(false);
+ debugC(1, kDebugGeneral, "EEM2 (London) PoC: rendering opening screens");
+
+ // EA Kids logo.
+ if (!shouldQuit())
+ showLondonLogo(0x54, 0x3c, 2500);
+
+ // Storm Software lightning-bolt animation with thunder (same BOLT.ANM
+ // container as EEM1 CD; the palette lives in the ANM file header).
+ if (!shouldQuit()) {
+ if (_audio)
+ _audio->playVoc(Common::Path("THUNDER.VOC"));
+ playAnm(Common::Path("BOLT.ANM"), 120, /* holdLastFrame= */ false,
+ /* fadeIn= */ true);
+ waitForInput(1500);
+ fadeCurrentPaletteToBlack();
+ if (_audio)
+ _audio->stopVoice();
+ }
+
+ // High Score Productions logo.
+ if (!shouldQuit())
+ showLondonLogo(0x356, 0x3d, 2500);
+
+ debugC(1, kDebugGeneral, "EEM2 (London) PoC: opening screens done");
+}
+
void EEMEngine::showFloppyStormLogo() {
// Floppy storm-logo splash â `FUN_23d2_0605 @ 23d2:0605`:
// GetPicture(0x20b); BlitToVGA;
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 3a405a74f95..accf569bb2d 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -101,8 +101,9 @@ enum ScreenId {
/// ANIM01..20.A), and per-variant SFX (DING.VOC / NEWSCAN.VOC ship only
/// with floppy).
enum Variant {
- kVariantCD = 0,
- kVariantFloppy = 1,
+ kVariantCD = 0,
+ kVariantFloppy = 1,
+ kVariantLondonCD = 2, ///< Eagle Eye Mysteries in London (EEM2CD.EXE) â PoC.
};
/// `_Partner @ 29be:7918`. Selected at the partner-pick screen
@@ -134,6 +135,10 @@ public:
Common::Platform getPlatform() const;
Variant getVariant() const { return _variant; }
bool isFloppy() const { return _variant == kVariantFloppy; }
+ /// EEM2 ("Eagle Eye Mysteries in London"). Proof-of-concept variant:
+ /// same DBD/palette/font formats, but a 63-entry "SITEPALS." file and
+ /// different opening picture/palette IDs.
+ bool isLondon() const { return _variant == kVariantLondonCD; }
bool hasFeature(EngineFeature f) const override;
bool canLoadGameStateCurrently(Common::U32String *msg = nullptr) override;
@@ -403,6 +408,15 @@ private:
void showHighScoreLogo();
void showFloppyStormLogo();
+ // --- EEM2 ("...in London") proof of concept ---
+ /// Reproduce the opening-logo portion of EEM2's `_DoOpeningAnims`
+ /// (@ 2721:08e6): EA Kids (PIC 0x54, pal 0x3c) -> Storm (BOLT.ANM) ->
+ /// High Score (PIC 0x356, pal 0x3d), then stop.
+ void runLondonScreensPoc();
+ /// Blit a full-screen still PIC and fade it in / hold / out using the
+ /// given SITEPALS. palette index.
+ void showLondonLogo(uint picId, uint palId, uint holdMs);
+
/// `screen8_handler @ 1c33:1012`. Profile selector â walks
/// `listProfiles()`, falls through to `doNewPlayer()` if "New" or
/// no profiles exist (1c33:1170: `if (saves == 0) _NewPlayer();`).
Commit: ff0025ab9680d7d6323d66fdd7a1d79f57cfbad2
https://github.com/scummvm/scummvm/commit/ff0025ab9680d7d6323d66fdd7a1d79f57cfbad2
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:20+02:00
Commit Message:
EEM: Added a more complete intro and character selection for EEM2
Changed paths:
engines/eem/eem.cpp
engines/eem/eem.h
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 56e839889e2..32f03c7d982 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -1047,36 +1047,93 @@ void EEMEngine::showLondonLogo(uint picId, uint palId, uint holdMs) {
}
void EEMEngine::runLondonScreensPoc() {
- // EEM2 opening logos â still-image portion of `_DoOpeningAnims`
- // @ 2721:08e6. Picture/palette IDs come from RE of EEM2CD.EXE:
- // _ShowEAKids @ 2721:05e3 â _GetPicture(0x54), _GetPalette(0x3c)
- // _ShowStormLogo @ 2721:0729 â BOLT.ANM + THUNDER.VOC
- // _ShowHScoreLogo @ 2721:084d â _GetPicture(0x356), _GetPalette(0x3d)
+ // Full opening sequence â EEM2 `_DoOpeningAnims` @ 2721:08e6:
+ // _ShowEAKids @ 2721:05e3 â PIC 0x54, palette 0x3c
+ // FUN_2721_07be @ 2721:07be â PIC 0x20c, palette 0x3e (publisher logo)
+ // _ShowStormLogo@ 2721:0729 â anim 0 ("bolt.anm") + "thunder.voc"
+ // _MIDIPlay(0x65)=MUS00101.XMI; anim 1 ("movie.anm")
+ // _MIDIPlay(0x66)=MUS00102.XMI (loop); anim 2 ("wave.anm"), looped
+ // _MIDIPlay(0x67)=MUS00103.XMI; fade out
+ // Anim names come from the table @ DS:1b54 -> {bolt,movie,wave}.anm;
+ // _MIDIPlay(n) maps to MUS%05d.XMI (n).
+ const uint32 kHoldForever = 0xFFFFFFFFu;
CursorMan.showMouse(false);
- debugC(1, kDebugGeneral, "EEM2 (London) PoC: rendering opening screens");
+ _skipIntro = false;
+ debugC(1, kDebugGeneral, "EEM2 (London) PoC: opening sequence");
- // EA Kids logo.
- if (!shouldQuit())
- showLondonLogo(0x54, 0x3c, 2500);
+ // Two still logos.
+ if (!shouldQuit() && !_skipIntro)
+ showLondonLogo(0x54, 0x3c, 2500); // EA Kids
+ if (!shouldQuit() && !_skipIntro)
+ showLondonLogo(0x20c, 0x3e, 2500); // publisher logo (FUN_2721_07be)
- // Storm Software lightning-bolt animation with thunder (same BOLT.ANM
- // container as EEM1 CD; the palette lives in the ANM file header).
- if (!shouldQuit()) {
+ // Storm Software â bolt.anm with the thunder roar.
+ if (!shouldQuit() && !_skipIntro) {
if (_audio)
_audio->playVoc(Common::Path("THUNDER.VOC"));
playAnm(Common::Path("BOLT.ANM"), 120, /* holdLastFrame= */ false,
/* fadeIn= */ true);
- waitForInput(1500);
- fadeCurrentPaletteToBlack();
if (_audio)
_audio->stopVoice();
+ fadeCurrentPaletteToBlack();
}
- // High Score Productions logo.
+ // Intro movie with its theme (MUS00101.XMI).
+ if (!shouldQuit() && !_skipIntro && _music)
+ _music->playFile(Common::Path("MUS00101.XMI"), /* loop= */ false);
+ if (!shouldQuit() && !_skipIntro)
+ playAnm(Common::Path("MOVIE.ANM"), 120, /* holdLastFrame= */ false,
+ /* fadeIn= */ true);
+
+ // Animated title (wave.anm) over the looping theme (MUS00102.XMI);
+ // a click / key advances to character creation. The original loops
+ // wave.anm; the PoC plays it once, holds the last frame and waits.
+ if (!shouldQuit() && !_skipIntro && _music)
+ _music->playFile(Common::Path("MUS00102.XMI"), /* loop= */ true);
+ if (!shouldQuit() && !_skipIntro)
+ playAnm(Common::Path("WAVE.ANM"), 120, /* holdLastFrame= */ true,
+ /* fadeIn= */ true);
+ if (!shouldQuit() && !_skipIntro)
+ waitForInput(kHoldForever);
+ if (_music)
+ _music->stop();
+ _skipIntro = false;
+
+ // Character-selection screen.
if (!shouldQuit())
- showLondonLogo(0x356, 0x3d, 2500);
+ showLondonCharSelect();
- debugC(1, kDebugGeneral, "EEM2 (London) PoC: opening screens done");
+ debugC(1, kDebugGeneral, "EEM2 (London) PoC: done");
+}
+
+void EEMEngine::showLondonCharSelect() {
+ // `_NewPlayer` @ 1cd3:0f27 opens the character-creation screen with
+ // `_GetPalette(0)` + `_GetBackground(0xc)` (PICS picture 0xc), then
+ // reads a name and toggles Jake/Jenny (left/right arrow -> DAT_4c4c,
+ // Enter confirms). This PoC renders the screen; wiring up the
+ // interactive name + partner entry is the next step.
+ const uint32 kHoldForever = 0xFFFFFFFFu;
+ debugC(1, kDebugGeneral, "EEM2 (London) PoC: character-selection screen");
+
+ Picture bg;
+ if (!_picsArchive.getPicture(0xc, bg) || bg.surface.empty()) {
+ warning("London char-select background PIC 0xc load failed");
+ return;
+ }
+ blitAt(bg, 0, 0);
+
+ byte target[kPalSize];
+ byte black[kPalSize] = {};
+ g_system->getPaletteManager()->setPalette(black, 0, 256);
+ g_system->updateScreen();
+ if (getSitePalette(0, target))
+ fadePaletteFromBlack(target);
+ else
+ warning("London char-select: palette 0 unavailable");
+
+ CursorMan.showMouse(true);
+ setInteractiveMouseCursor(true);
+ waitForInput(kHoldForever);
}
void EEMEngine::showFloppyStormLogo() {
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index accf569bb2d..8fcb1abeec3 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -409,13 +409,16 @@ private:
void showFloppyStormLogo();
// --- EEM2 ("...in London") proof of concept ---
- /// Reproduce the opening-logo portion of EEM2's `_DoOpeningAnims`
- /// (@ 2721:08e6): EA Kids (PIC 0x54, pal 0x3c) -> Storm (BOLT.ANM) ->
- /// High Score (PIC 0x356, pal 0x3d), then stop.
+ /// Full opening sequence + character-selection screen â EEM2's
+ /// `_DoOpeningAnims` @ 2721:08e6 then `_NewPlayer` @ 1cd3:0f27.
void runLondonScreensPoc();
/// Blit a full-screen still PIC and fade it in / hold / out using the
/// given SITEPALS. palette index.
void showLondonLogo(uint picId, uint palId, uint holdMs);
+ /// Render EEM2's character-creation screen (`_NewPlayer`: palette 0 +
+ /// background PIC 0xc, where the player types a name and picks
+ /// Jake/Jenny). PoC display; interactive entry is still TODO.
+ void showLondonCharSelect();
/// `screen8_handler @ 1c33:1012`. Profile selector â walks
/// `listProfiles()`, falls through to `doNewPlayer()` if "New" or
Commit: 3fb2a3ea4488b5f83767141883896bf51569311a
https://github.com/scummvm/scummvm/commit/3fb2a3ea4488b5f83767141883896bf51569311a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:21+02:00
Commit Message:
EEM: load case menu is working in EEM2
Changed paths:
engines/eem/eem.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 32f03c7d982..92527a49871 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -1099,41 +1099,134 @@ void EEMEngine::runLondonScreensPoc() {
_music->stop();
_skipIntro = false;
- // Character-selection screen.
+ // Character creation (name + Jake/Jenny), then the case-selection menu.
if (!shouldQuit())
showLondonCharSelect();
+ // Reuse EEM1's `doCaseSelection()` verbatim â EEM2 shares its assets
+ // (background PIC 0x41, partner greeter ANI 0x15/0x16, BOOK*.NME, the
+ // generic chooser). The mystery load at the end is guarded out for
+ // London since EEM2's case data isn't ported yet.
+ if (!shouldQuit())
+ doCaseSelection();
debugC(1, kDebugGeneral, "EEM2 (London) PoC: done");
}
void EEMEngine::showLondonCharSelect() {
- // `_NewPlayer` @ 1cd3:0f27 opens the character-creation screen with
- // `_GetPalette(0)` + `_GetBackground(0xc)` (PICS picture 0xc), then
- // reads a name and toggles Jake/Jenny (left/right arrow -> DAT_4c4c,
- // Enter confirms). This PoC renders the screen; wiring up the
- // interactive name + partner entry is the next step.
- const uint32 kHoldForever = 0xFFFFFFFFu;
- debugC(1, kDebugGeneral, "EEM2 (London) PoC: character-selection screen");
+ // `_NewPlayer` @ 1cd3:0f27 â the character-creation screen: palette 0 +
+ // background PIC 0xc, type a name, then pick Jake/Jenny with the
+ // left/right arrows (stored at DAT_4c4c) and confirm with Enter. The
+ // original's name-field / Jake-Jenny box rects are runtime-initialised
+ // (BSS, beyond the EXE image), so the overlay positions here are
+ // approximate over EEM2's PIC 0xc artwork â tune once visible. Text
+ // uses the shared engine font.
+ debugC(1, kDebugGeneral, "EEM2 (London) PoC: character creation");
Picture bg;
- if (!_picsArchive.getPicture(0xc, bg) || bg.surface.empty()) {
- warning("London char-select background PIC 0xc load failed");
- return;
- }
- blitAt(bg, 0, 0);
+ const bool haveBg = _picsArchive.getPicture(0xc, bg) && !bg.surface.empty();
- byte target[kPalSize];
+ byte pal[kPalSize];
byte black[kPalSize] = {};
g_system->getPaletteManager()->setPalette(black, 0, 256);
g_system->updateScreen();
- if (getSitePalette(0, target))
- fadePaletteFromBlack(target);
- else
- warning("London char-select: palette 0 unavailable");
+ const bool havePal = getSitePalette(0, pal);
- CursorMan.showMouse(true);
- setInteractiveMouseCursor(true);
- waitForInput(kHoldForever);
+ CursorMan.showMouse(false);
+
+ Common::String name;
+ const uint kMaxName = 12;
+ uint8 partner = kPartnerJake;
+ bool nameDone = false, blink = true, fadedIn = false;
+ uint32 blinkMs = g_system->getMillis();
+ bool done = false, needRedraw = true;
+
+ while (!done && !shouldQuit()) {
+ if (needRedraw) {
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ if (haveBg)
+ scratch.simpleBlitFrom(bg.surface);
+ if (getFont().isLoaded()) {
+ getFont().drawString(&scratch,
+ isSpanish() ? "Escribe tu nombre:" : "Type your name:",
+ 80, 36, 240, 0xF);
+ Common::String shown = name;
+ if (!nameDone && blink)
+ shown += "_";
+ getFont().drawString(&scratch, shown, 80, 52, 240, 0xF);
+ getFont().drawString(&scratch, "Jake", 104, 116, 80,
+ partner == kPartnerJake ? 0xF : 0x8);
+ getFont().drawString(&scratch, "Jenny", 184, 116, 80,
+ partner == kPartnerJenny ? 0xF : 0x8);
+ getFont().drawString(&scratch,
+ nameDone ? (isSpanish() ? "Flechas eligen - Enter juega"
+ : "Arrows choose - Enter to play")
+ : (isSpanish() ? "Enter para continuar"
+ : "Enter to continue"),
+ 80, 150, 240, 0x7);
+ }
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, kScreenWidth, kScreenHeight);
+ if (!fadedIn) {
+ if (havePal)
+ fadePaletteFromBlack(pal);
+ fadedIn = true;
+ }
+ g_system->updateScreen();
+ needRedraw = false;
+ }
+
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return;
+ if (ev.type != Common::EVENT_KEYDOWN)
+ continue;
+ const Common::KeyCode k = ev.kbd.keycode;
+ if (!nameDone) {
+ if ((k == Common::KEYCODE_RETURN ||
+ k == Common::KEYCODE_KP_ENTER) && !name.empty())
+ nameDone = true;
+ else if (k == Common::KEYCODE_BACKSPACE && !name.empty())
+ name.deleteLastChar();
+ else if (ev.kbd.ascii >= ' ' && ev.kbd.ascii < 127 &&
+ name.size() < kMaxName)
+ name += (char)ev.kbd.ascii;
+ } else {
+ if (k == Common::KEYCODE_LEFT)
+ partner = kPartnerJake;
+ else if (k == Common::KEYCODE_RIGHT)
+ partner = kPartnerJenny;
+ else if (k == Common::KEYCODE_RETURN ||
+ k == Common::KEYCODE_KP_ENTER)
+ done = true;
+ }
+ needRedraw = true;
+ }
+
+ const uint32 now = g_system->getMillis();
+ if (!nameDone && now - blinkMs >= 400) {
+ blinkMs = now;
+ blink = !blink;
+ needRedraw = true;
+ }
+ g_system->delayMillis(15);
+ }
+ if (shouldQuit())
+ return;
+
+ // Commit the profile so the reused case-selection menu has valid state
+ // (player name shown in clue text, partner greeter ANI 0x15/0x16,
+ // Junior chain stage -> BOOK1.NME, nothing solved yet).
+ _playerName = name.empty() ? Common::String("Detective") : name;
+ _partner = partner;
+ _chainStage = 1;
+ for (uint i = 0; i < sizeof(_mysteriesSolved); i++)
+ _mysteriesSolved[i] = 0;
+ debugC(1, kDebugGeneral, "London PoC: player='%s' partner=%s",
+ _playerName.c_str(), partner == kPartnerJake ? "Jake" : "Jenny");
}
void EEMEngine::showFloppyStormLogo() {
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 420c799701c..6be8928886a 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -2117,6 +2117,14 @@ void EEMEngine::doCaseSelection() {
return;
const uint mn = stageLo + selRow;
+ if (isLondon()) {
+ // EEM2 PoC: the menu (PIC 0x41, ANI 0x15/0x16, BOOK*.NME) is shared
+ // with EEM1, but EEM2's mystery data (M*.BIN/E*.BIN) isn't ported,
+ // so stop here instead of parsing it with the EEM1 loader.
+ debugC(1, kDebugMystery,
+ "London PoC: selected mystery %u (load not implemented)", mn);
+ return;
+ }
if (!_mystery.load(mn, &_rng)) {
warning("doCaseSelection: failed to load mystery %u", mn);
_mystery.clear();
Commit: a477a58ccf0f2a5bc7e70172d8ef6c77710e2e09
https://github.com/scummvm/scummvm/commit/a477a58ccf0f2a5bc7e70172d8ef6c77710e2e09
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:21+02:00
Commit Message:
EEM: add more code in the partner selection screen
Changed paths:
engines/eem/eem.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 92527a49871..4355db818d8 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -1113,15 +1113,23 @@ void EEMEngine::runLondonScreensPoc() {
}
void EEMEngine::showLondonCharSelect() {
- // `_NewPlayer` @ 1cd3:0f27 â the character-creation screen: palette 0 +
- // background PIC 0xc, type a name, then pick Jake/Jenny with the
- // left/right arrows (stored at DAT_4c4c) and confirm with Enter. The
- // original's name-field / Jake-Jenny box rects are runtime-initialised
- // (BSS, beyond the EXE image), so the overlay positions here are
- // approximate over EEM2's PIC 0xc artwork â tune once visible. Text
- // uses the shared engine font.
+ // `_NewPlayer` @ 1cd3:0f27 â character creation over background PIC 0xc
+ // (palette 0). Two text fields then a Jake/Jenny pick (left/right arrow
+ // 0x4b/0x4d -> DAT_4c4c, Enter confirms). The field/box rects are
+ // constants in EEM2's data segment (read at 2bca:0e3a); they map to the
+ // `pr` player record's FirstName[12] / LastName[20]:
+ // first name : (54,75)-(151,85) last name : (167,75)-(266,85)
+ // Jake box : (110,116)-(120,122) Jenny box : (190,116)-(200,122)
debugC(1, kDebugGeneral, "EEM2 (London) PoC: character creation");
+ const Common::Rect kFirstRect(54, 75, 151, 85);
+ const Common::Rect kLastRect(167, 75, 266, 85);
+ const Common::Rect kJakeBox(110, 116, 120, 122);
+ const Common::Rect kJennyBox(190, 116, 200, 122);
+ const uint kMaxFirst = 12, kMaxLast = 20;
+ const uint8 kInkColor = 0x0F; // typed-name ink
+ const uint8 kHiBox = 0x0F; // selected-partner box highlight
+
Picture bg;
const bool haveBg = _picsArchive.getPicture(0xc, bg) && !bg.surface.empty();
@@ -1133,12 +1141,12 @@ void EEMEngine::showLondonCharSelect() {
CursorMan.showMouse(false);
- Common::String name;
- const uint kMaxName = 12;
+ enum { kFieldFirst = 0, kFieldLast = 1, kFieldPartner = 2 };
+ int field = kFieldFirst;
+ Common::String first, last;
uint8 partner = kPartnerJake;
- bool nameDone = false, blink = true, fadedIn = false;
+ bool blink = true, fadedIn = false, done = false, needRedraw = true;
uint32 blinkMs = g_system->getMillis();
- bool done = false, needRedraw = true;
while (!done && !shouldQuit()) {
if (needRedraw) {
@@ -1148,24 +1156,22 @@ void EEMEngine::showLondonCharSelect() {
if (haveBg)
scratch.simpleBlitFrom(bg.surface);
if (getFont().isLoaded()) {
- getFont().drawString(&scratch,
- isSpanish() ? "Escribe tu nombre:" : "Type your name:",
- 80, 36, 240, 0xF);
- Common::String shown = name;
- if (!nameDone && blink)
- shown += "_";
- getFont().drawString(&scratch, shown, 80, 52, 240, 0xF);
- getFont().drawString(&scratch, "Jake", 104, 116, 80,
- partner == kPartnerJake ? 0xF : 0x8);
- getFont().drawString(&scratch, "Jenny", 184, 116, 80,
- partner == kPartnerJenny ? 0xF : 0x8);
- getFont().drawString(&scratch,
- nameDone ? (isSpanish() ? "Flechas eligen - Enter juega"
- : "Arrows choose - Enter to play")
- : (isSpanish() ? "Enter para continuar"
- : "Enter to continue"),
- 80, 150, 240, 0x7);
+ Common::String f = first;
+ if (field == kFieldFirst && blink)
+ f += "_";
+ Common::String l = last;
+ if (field == kFieldLast && blink)
+ l += "_";
+ getFont().drawString(&scratch, f, kFirstRect.left + 2,
+ kFirstRect.top + 1, kFirstRect.width(),
+ kInkColor);
+ getFont().drawString(&scratch, l, kLastRect.left + 2,
+ kLastRect.top + 1, kLastRect.width(),
+ kInkColor);
}
+ // Highlight the selected partner's indicator box.
+ scratch.fillRect(partner == kPartnerJake ? kJakeBox : kJennyBox,
+ kHiBox);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, kScreenWidth, kScreenHeight);
if (!fadedIn) {
@@ -1185,29 +1191,34 @@ void EEMEngine::showLondonCharSelect() {
if (ev.type != Common::EVENT_KEYDOWN)
continue;
const Common::KeyCode k = ev.kbd.keycode;
- if (!nameDone) {
- if ((k == Common::KEYCODE_RETURN ||
- k == Common::KEYCODE_KP_ENTER) && !name.empty())
- nameDone = true;
- else if (k == Common::KEYCODE_BACKSPACE && !name.empty())
- name.deleteLastChar();
+ const bool enter = (k == Common::KEYCODE_RETURN ||
+ k == Common::KEYCODE_KP_ENTER);
+ if (field == kFieldFirst || field == kFieldLast) {
+ Common::String &buf = (field == kFieldFirst) ? first : last;
+ const uint cap = (field == kFieldFirst) ? kMaxFirst : kMaxLast;
+ if ((enter || k == Common::KEYCODE_TAB) &&
+ (field == kFieldLast || !first.empty()))
+ field++; // advance to last name, then to partner pick
+ else if (k == Common::KEYCODE_BACKSPACE && !buf.empty())
+ buf.deleteLastChar();
else if (ev.kbd.ascii >= ' ' && ev.kbd.ascii < 127 &&
- name.size() < kMaxName)
- name += (char)ev.kbd.ascii;
- } else {
+ buf.size() < cap)
+ buf += (char)ev.kbd.ascii;
+ } else { // partner pick
if (k == Common::KEYCODE_LEFT)
partner = kPartnerJake;
else if (k == Common::KEYCODE_RIGHT)
partner = kPartnerJenny;
- else if (k == Common::KEYCODE_RETURN ||
- k == Common::KEYCODE_KP_ENTER)
+ else if (k == Common::KEYCODE_BACKSPACE)
+ field = kFieldLast; // back to editing
+ else if (enter)
done = true;
}
needRedraw = true;
}
const uint32 now = g_system->getMillis();
- if (!nameDone && now - blinkMs >= 400) {
+ if (field != kFieldPartner && now - blinkMs >= 400) {
blinkMs = now;
blink = !blink;
needRedraw = true;
@@ -1218,9 +1229,11 @@ void EEMEngine::showLondonCharSelect() {
return;
// Commit the profile so the reused case-selection menu has valid state
- // (player name shown in clue text, partner greeter ANI 0x15/0x16,
- // Junior chain stage -> BOOK1.NME, nothing solved yet).
- _playerName = name.empty() ? Common::String("Detective") : name;
+ // (player name in clue text, partner greeter ANI 0x15/0x16, Junior
+ // chain stage -> BOOK1.NME, nothing solved yet).
+ if (first.empty())
+ first = "Detective";
+ _playerName = last.empty() ? first : (first + " " + last);
_partner = partner;
_chainStage = 1;
for (uint i = 0; i < sizeof(_mysteriesSolved); i++)
Commit: e69c835219a58d26adcac729f3b7d773f93e0d42
https://github.com/scummvm/scummvm/commit/e69c835219a58d26adcac729f3b7d773f93e0d42
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:21+02:00
Commit Message:
EEM: corrected palette
Changed paths:
engines/eem/clues.cpp
engines/eem/eem.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 3f70172723b..1c84992d616 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -300,7 +300,12 @@ void EEMEngine::doInitClues() {
_mystery._lastSite = 0;
}
- setSitePalette(0x22);
+ // Case-briefing palette. EEM1 `_DoInitClues` uses SITEPALS index 0x22;
+ // EEM2 `_DoInitClues` @ 1abf:03b3 does `_GetBackground(0x52)` then
+ // `_GetPalette(0x39)` (its 63-entry SITEPALS. shifts the UI palettes).
+ // The briefing partner animation is an ANI sprite drawn under the screen
+ // palette, so the same index fixes both the background and the anim.
+ setSitePalette(isLondon() ? 0x39 : 0x22);
Picture bg;
const bool haveBriefingBg = _picsArchive.getPicture(0x52, bg);
if (haveBriefingBg)
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 4355db818d8..66d5b11f08e 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -301,17 +301,6 @@ Common::Error EEMEngine::run() {
debugC(1, kDebugGeneral, "EEM engine starting");
- // EEM2 ("Eagle Eye Mysteries in London") proof of concept. The archives,
- // SITEPALS. palettes, font and mouse cursor above all load from EEM2's
- // data using the same code paths as EEM1, which already proves the
- // shared resource formats. Everything below here (saves, mystery/site
- // state machine) is EEM1-specific, so the PoC just renders EEM2's
- // opening logo screens and exits.
- if (isLondon()) {
- runLondonScreensPoc();
- return Common::kNoError;
- }
-
// Resume from save: mystery in progress â MAP (handler 0 @ 1a35:0e1d);
// otherwise â ACTION.
const int wantedSave = ConfMan.hasKey("save_slot")
@@ -342,6 +331,31 @@ Common::Error EEMEngine::run() {
if (resumed)
goto screenLoop;
+ // EEM2 ("Eagle Eye Mysteries in London") proof of concept: opening
+ // sequence + character creation, then start the training case (mystery
+ // 0 â what a new detective plays first) and reuse the engine's gameplay
+ // screen loop. EEM2's mystery data format matches EEM1-CD, so the shared
+ // Mystery parser + InitClues (case intro) / BigMap / Site handlers apply.
+ if (isLondon()) {
+ runLondonScreensPoc();
+ if (!shouldQuit()) {
+ CursorMan.showMouse(true);
+ if (_mystery.load(0, &_rng)) {
+ resetSiteArrivalState();
+ if (_audio)
+ _audio->initMysterySounds(0);
+ debugC(1, kDebugMystery,
+ "London: training mystery 0 loaded â %u sites, %u suspects",
+ _mystery.numSites(), _mystery.numSuspects());
+ _nextScreen = kScreenInitClues;
+ } else {
+ warning("London: training mystery 0 (M0.BIN) failed to load");
+ _nextScreen = kScreenInvalid;
+ }
+ }
+ goto screenLoop;
+ }
+
// _DoOpeningAnims @ 2520:082a:
// EA Kids logo (PIC) -> HighScore logo (PIC) -> Storm logo
// (BOLT.ANM) -> [music starts] -> 20 character-intro anims
@@ -1099,17 +1113,14 @@ void EEMEngine::runLondonScreensPoc() {
_music->stop();
_skipIntro = false;
- // Character creation (name + Jake/Jenny), then the case-selection menu.
+ // Character creation (name + Jake/Jenny). The caller then loads the
+ // training case (mystery 0) and enters the shared gameplay screen loop
+ // (case intro -> map -> site); the case-selection menu is reachable
+ // later via the action screen.
if (!shouldQuit())
showLondonCharSelect();
- // Reuse EEM1's `doCaseSelection()` verbatim â EEM2 shares its assets
- // (background PIC 0x41, partner greeter ANI 0x15/0x16, BOOK*.NME, the
- // generic chooser). The mystery load at the end is guarded out for
- // London since EEM2's case data isn't ported yet.
- if (!shouldQuit())
- doCaseSelection();
- debugC(1, kDebugGeneral, "EEM2 (London) PoC: done");
+ debugC(1, kDebugGeneral, "EEM2 (London) PoC: intro + character creation done");
}
void EEMEngine::showLondonCharSelect() {
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 6be8928886a..0d6cb294edb 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -2958,7 +2958,9 @@ void EEMEngine::doBigMap() {
while (!shouldQuit()) {
setInteractiveMouseCursor(false);
- setSitePalette(0x24); // `_GetPalette(0x24)` @ `_DoBigMap`.
+ // `_GetPalette(0x24)` @ EEM1 `_DoBigMap`; EEM2 `_DoBigMap`
+ // @ 2237:0a04 uses `_GetPalette(0x3b)` (shifted UI palettes).
+ setSitePalette(isLondon() ? 0x3b : 0x24);
// Stage 1: Overview. mapStartTick anchors partner timeline;
// `_NewAnimation` seeds frame to 0xffff so unfold plays once then
Commit: 29bef8261a7ee1b937bc269b7661f8f10100518f
https://github.com/scummvm/scummvm/commit/29bef8261a7ee1b937bc269b7661f8f10100518f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:22+02:00
Commit Message:
EEM: play correctly initial animation in London
Changed paths:
engines/eem/clues.cpp
engines/eem/detection.cpp
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/ui.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 1c84992d616..fc35a14407a 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -29,6 +29,7 @@
#include "graphics/cursorman.h"
#include "graphics/managed_surface.h"
+#include "graphics/paletteman.h"
#include "eem/audio.h"
#include "eem/detection.h"
@@ -261,56 +262,87 @@ void EEMEngine::doChoosePartner() {
}
}
-// _DoInitClues @ 1a35:0411. Sequence:
-// 1. BG 0x52, palette 0x22
-// 2. anims: game @ (0xcd, 0x6c), book @ (0, 99), nancy @ (0x68, 0x8b) on caseType=1
-// 3. cycle game anim once (click skips)
-// 4. _PlayInSequence per partner+caseType
-// 5. _DisplayClue(InitBlock + 2, 1) â briefing dialogue
-// 6. _OnSites[startSite] = 1
-// Anim IDs: gameAni = 0x17 (Jake) / 0x3b (Jenny);
-// bookAni = 0x18 / 0x3c; nancyAni = 0x19 (caseType 1 only).
-void EEMEngine::doInitClues() {
- if (!_mystery.isLoaded())
- return;
-
- const byte *ib = _mystery.initBlock();
- if (!ib)
- return;
+// EEM2 case-intro animation â `_DoInitClues` @ 1abf:03b3. Ghidra-confirmed
+// flow (decompiled prologue):
+// _AllBlack(); _GetBackground(0x52); _GetPalette(0x39);
+// anim = _GetAnimation(DAT_4bd4 ? 0x71 : 0x18); // Jake 0x18 / Jenny 0x71
+// _NewAnimation(0xd2, 0x3f, anim, ...); // registered at (210,63)
+// _UpdateAnimations(); _FadeIn(); // draw frame 0, then fade
+// while (i != _max()) { _CheckFrameRate(); _UpdateAnimations(); kbhit->skip }
+// if (caseType == 1) { _LoadSoundName("phone1.voc"); _PlayVoice; _Wait... }
+// So, unlike EEM1's game/book/nancy cycle + `_PlayInSequence`, EEM2 registers
+// ONE partner animation drawn by `_UpdateAnimations` â hence we anchor frames
+// with `blitAnimFrameAnchored` ((0xd2 - miscflags, 0x3f - rowoff), per
+// `_UpdateAnimations @ 172b:09c1`), the same helper the map/site screens use.
+void EEMEngine::playLondonInitCluesAnim(uint16 caseType, const Picture &bg,
+ bool haveBriefingBg) {
+ const uint introAni = (_partner == kPartnerJake) ? 0x18 : 0x71;
+ const int kAnchorX = 0xd2, kAnchorY = 0x3f; // _NewAnimation(0xd2, 0x3f)
+ Animation anim;
+ const bool haveAnim =
+ _aniArchive.loadAnimation(introAni, anim) && !anim.empty();
+
+ // _AllBlack(): blank the palette so the _FadeIn after frame 0 reveals the
+ // briefing (palette 0x39 â EEM2's 63-entry SITEPALS. shifts EEM1's UI set).
+ byte pal[kPalSize];
+ const bool havePal = getSitePalette(0x39, pal);
+ byte black[kPalSize] = {};
+ g_system->getPaletteManager()->setPalette(black, 0, 256);
+ g_system->updateScreen();
- // CD InitBlock: u16 caseType; u16 startSite; <clue block>.
- // Floppy InitBlock (FUN_19bb_042f): u8 caseType; u8 nSubjects;
- // subjects[]; u8 nDialog; dialog_records[]. No startSite.
- const bool floppy = isFloppy();
- const uint16 caseType = floppy ? (uint16)ib[0] : READ_LE_UINT16(ib);
+ bool skip = false;
+ const uint frames = haveAnim ? (uint)anim.size() : 1;
+ for (uint frame = 0; frame < frames && !shouldQuit() && !skip; frame++) {
+ if (haveBriefingBg)
+ blitAt(bg, 0, 0);
+ if (haveAnim) {
+ Graphics::Surface *scr = g_system->lockScreen();
+ if (scr) {
+ blitAnimFrameAnchored(scr, anim[frame], kAnchorX, kAnchorY);
+ g_system->unlockScreen();
+ }
+ }
+ if (frame == 0 && havePal)
+ fadePaletteFromBlack(pal); // _FadeIn @ 1abf:03b3
+ else
+ g_system->updateScreen();
- if (!floppy) {
- const uint16 startSite = READ_LE_UINT16(ib + 2);
- if (startSite < Mystery::kVisitedSiteCap)
- _mystery._onSites[startSite] = 1;
- _mystery._siteNumber = startSite;
- _mystery._lastSite = startSite;
- } else {
- // Floppy _DoMapScreen @ 1fed:1060 (FUN_1fed_07ed) walks every
- // site unconditionally â mirror that by marking all visible.
- const uint sites = _mystery.numSites();
- for (uint s = 0; s < sites && s < Mystery::kVisitedSiteCap; s++)
- _mystery._onSites[s] = 1;
- _mystery._siteNumber = 0;
- _mystery._lastSite = 0;
+ const uint32 wakeup = g_system->getMillis() + 140;
+ while (g_system->getMillis() < wakeup && !shouldQuit() && !skip) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_KEYDOWN &&
+ ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ interruptAudio(/* stopMusicToo= */ false);
+ skip = true;
+ break;
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN ||
+ ev.type == Common::EVENT_KEYDOWN) {
+ skip = true;
+ break;
+ }
+ }
+ g_system->delayMillis(10);
+ }
}
- // Case-briefing palette. EEM1 `_DoInitClues` uses SITEPALS index 0x22;
- // EEM2 `_DoInitClues` @ 1abf:03b3 does `_GetBackground(0x52)` then
- // `_GetPalette(0x39)` (its 63-entry SITEPALS. shifts the UI palettes).
- // The briefing partner animation is an ANI sprite drawn under the screen
- // palette, so the same index fixes both the background and the anim.
- setSitePalette(isLondon() ? 0x39 : 0x22);
- Picture bg;
- const bool haveBriefingBg = _picsArchive.getPicture(0x52, bg);
- if (haveBriefingBg)
- blitAt(bg, 0, 0);
+ // caseType 1 rings phone1.voc (EEM1's PHONE.VOC fires on caseType 2).
+ if (_audio && caseType == 1) {
+ _audio->playVoc(Common::Path("phone1.voc"));
+ _audio->waitForVoiceDone();
+ }
+}
+// EEM1 CD/floppy case-intro animation â `_DoInitClues @ 1a35:0411`:
+// anims registered: game @ (0xcd,0x6c) [0x17 Jake / 0x3b Jenny],
+// book @ (0,99) [0x18 / 0x3c], nancy @ (0x68,0x8b) [0x19, caseType 1 only];
+// cycle the game anim once (click skips), then `_PlayInSequence @ 172b:2d03`
+// plays the partner entrance per partner+caseType. A phone/news voice rings
+// first on CD caseType 2 / floppy caseType 2-3. (London: see
+// playLondonInitCluesAnim.)
+void EEMEngine::playCdFloppyInitCluesAnim(uint16 caseType, bool floppy,
+ const Picture &bg, bool haveBriefingBg) {
const uint gameAni = _partner == kPartnerJake ? 0x17 : 0x3b;
const uint bookAni = _partner == kPartnerJake ? 0x18 : 0x3c;
Animation game, book, nancy;
@@ -513,6 +545,62 @@ void EEMEngine::doInitClues() {
}
}
}
+}
+
+// _DoInitClues @ 1a35:0411 (EEM1) / 1abf:03b3 (EEM2). Sequence:
+// 1. mark the start site / visited sites from the InitBlock
+// 2. BG PIC 0x52 + briefing palette (EEM1 0x22 / EEM2 0x39)
+// 3. partner entrance animation (CD/floppy or London variant)
+// 4. _DisplayClue(InitBlock + 2) â briefing dialogue
+void EEMEngine::doInitClues() {
+ if (!_mystery.isLoaded())
+ return;
+
+ const byte *ib = _mystery.initBlock();
+ if (!ib)
+ return;
+
+ // CD InitBlock: u16 caseType; u16 startSite; <clue block>.
+ // Floppy InitBlock (FUN_19bb_042f): u8 caseType; u8 nSubjects;
+ // subjects[]; u8 nDialog; dialog_records[]. No startSite.
+ const bool floppy = isFloppy();
+ const uint16 caseType = floppy ? (uint16)ib[0] : READ_LE_UINT16(ib);
+
+ if (!floppy) {
+ const uint16 startSite = READ_LE_UINT16(ib + 2);
+ if (startSite < Mystery::kVisitedSiteCap)
+ _mystery._onSites[startSite] = 1;
+ _mystery._siteNumber = startSite;
+ _mystery._lastSite = startSite;
+ } else {
+ // Floppy _DoMapScreen @ 1fed:1060 (FUN_1fed_07ed) walks every
+ // site unconditionally â mirror that by marking all visible.
+ const uint sites = _mystery.numSites();
+ for (uint s = 0; s < sites && s < Mystery::kVisitedSiteCap; s++)
+ _mystery._onSites[s] = 1;
+ _mystery._siteNumber = 0;
+ _mystery._lastSite = 0;
+ }
+
+ // Case-briefing palette. EEM1 `_DoInitClues` uses SITEPALS index 0x22;
+ // EEM2 `_DoInitClues` @ 1abf:03b3 does `_GetBackground(0x52)` then
+ // `_GetPalette(0x39)` (its 63-entry SITEPALS. shifts the UI palettes).
+ // The briefing partner animation is an ANI sprite drawn under the screen
+ // palette, so the same index fixes both the background and the anim.
+ setSitePalette(isLondon() ? 0x39 : 0x22);
+ Picture bg;
+ const bool haveBriefingBg = _picsArchive.getPicture(0x52, bg);
+ if (haveBriefingBg)
+ blitAt(bg, 0, 0);
+
+ // Case-intro partner animation. EEM2 (`_DoInitClues` @ 1abf:03b3) plays a
+ // single partner anim (Jake 0x18 / Jenny 0x71); EEM1 CD/floppy runs a
+ // game/book/nancy cycle + `_PlayInSequence` entrance. Both then feed the
+ // shared briefing dialogue below.
+ if (isLondon())
+ playLondonInitCluesAnim(caseType, bg, haveBriefingBg);
+ else
+ playCdFloppyInitCluesAnim(caseType, floppy, bg, haveBriefingBg);
// Briefing dialogue. CD: clue block @ ib+4 (after caseType,startSite).
// Floppy: dialog records dispatched via FUN_22dc_05c8 @ 22dc:05c8
diff --git a/engines/eem/detection.cpp b/engines/eem/detection.cpp
index 62d6c3901bd..714b2d81821 100644
--- a/engines/eem/detection.cpp
+++ b/engines/eem/detection.cpp
@@ -68,10 +68,10 @@ const ADGameDescription gameDescriptions[] = {
GUI_OPTIONS_EEM_FLOPPY
},
{
- // Eagle Eye Mysteries in London (EEM2CD.EXE) â proof of concept.
- // The sequel reuses this engine's resource formats (DBD/DBX
- // archives, SITEPALS palettes, FONT.FNT), so only the opening
- // screens load for now; flagged unstable until properly supported.
+ // Eagle Eye Mysteries in London (EEM2CD.EXE), the sequel.
+ // It reuses this engine's resource formats (DBD/DBX archives,
+ // SITEPALS palettes, FONT.FNT); reimplementation is in progress,
+ // so it stays flagged unstable until fully supported.
"eem2",
"CD",
AD_ENTRY2s("EEM2CD.EXE", "211a376b23a1b6259d0c36cf46d26ed4", 172560,
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 66d5b11f08e..9b78267ec4c 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -48,7 +48,6 @@
namespace EEM {
-const uint kPalSize = 768;
const uint kNumSitePals = 40; // SITEPALS: 40 * 768 = 30720
// 1-based picture/palette IDs.
@@ -66,7 +65,7 @@ const byte kSaveBodyVer = 1;
// option or changing save format. Set false before release.
const bool kDebugPopulateScrapbook1AtStartup = false;
-void fadeCurrentPaletteToBlack(uint delayMs = 8) {
+void fadeCurrentPaletteToBlack(uint delayMs) {
byte start[kPalSize];
byte stepPal[kPalSize];
g_system->getPaletteManager()->grabPalette(start, 0, 256);
@@ -81,7 +80,7 @@ void fadeCurrentPaletteToBlack(uint delayMs = 8) {
}
}
-void fadePaletteFromBlack(const byte *target, uint delayMs = 8) {
+void fadePaletteFromBlack(const byte *target, uint delayMs) {
byte stepPal[kPalSize];
for (uint step = 1; step <= 16; step++) {
@@ -331,13 +330,13 @@ Common::Error EEMEngine::run() {
if (resumed)
goto screenLoop;
- // EEM2 ("Eagle Eye Mysteries in London") proof of concept: opening
- // sequence + character creation, then start the training case (mystery
- // 0 â what a new detective plays first) and reuse the engine's gameplay
- // screen loop. EEM2's mystery data format matches EEM1-CD, so the shared
- // Mystery parser + InitClues (case intro) / BigMap / Site handlers apply.
+ // EEM2 ("Eagle Eye Mysteries in London"): opening sequence + character
+ // creation, then start the training case (mystery 0 â what a new
+ // detective plays first) and reuse the engine's gameplay screen loop.
+ // EEM2's mystery data format matches EEM1-CD, so the shared Mystery
+ // parser + InitClues (case intro) / BigMap / Site handlers apply.
if (isLondon()) {
- runLondonScreensPoc();
+ runLondonStartup();
if (!shouldQuit()) {
CursorMan.showMouse(true);
if (_mystery.load(0, &_rng)) {
@@ -1060,7 +1059,7 @@ void EEMEngine::showLondonLogo(uint picId, uint palId, uint holdMs) {
fadeCurrentPaletteToBlack();
}
-void EEMEngine::runLondonScreensPoc() {
+void EEMEngine::runLondonStartup() {
// Full opening sequence â EEM2 `_DoOpeningAnims` @ 2721:08e6:
// _ShowEAKids @ 2721:05e3 â PIC 0x54, palette 0x3c
// FUN_2721_07be @ 2721:07be â PIC 0x20c, palette 0x3e (publisher logo)
@@ -1073,7 +1072,7 @@ void EEMEngine::runLondonScreensPoc() {
const uint32 kHoldForever = 0xFFFFFFFFu;
CursorMan.showMouse(false);
_skipIntro = false;
- debugC(1, kDebugGeneral, "EEM2 (London) PoC: opening sequence");
+ debugC(1, kDebugGeneral, "EEM2 (London): opening sequence");
// Two still logos.
if (!shouldQuit() && !_skipIntro)
@@ -1101,7 +1100,7 @@ void EEMEngine::runLondonScreensPoc() {
// Animated title (wave.anm) over the looping theme (MUS00102.XMI);
// a click / key advances to character creation. The original loops
- // wave.anm; the PoC plays it once, holds the last frame and waits.
+ // wave.anm; we play it once, hold the last frame and wait.
if (!shouldQuit() && !_skipIntro && _music)
_music->playFile(Common::Path("MUS00102.XMI"), /* loop= */ true);
if (!shouldQuit() && !_skipIntro)
@@ -1120,7 +1119,7 @@ void EEMEngine::runLondonScreensPoc() {
if (!shouldQuit())
showLondonCharSelect();
- debugC(1, kDebugGeneral, "EEM2 (London) PoC: intro + character creation done");
+ debugC(1, kDebugGeneral, "EEM2 (London): intro + character creation done");
}
void EEMEngine::showLondonCharSelect() {
@@ -1131,7 +1130,7 @@ void EEMEngine::showLondonCharSelect() {
// `pr` player record's FirstName[12] / LastName[20]:
// first name : (54,75)-(151,85) last name : (167,75)-(266,85)
// Jake box : (110,116)-(120,122) Jenny box : (190,116)-(200,122)
- debugC(1, kDebugGeneral, "EEM2 (London) PoC: character creation");
+ debugC(1, kDebugGeneral, "EEM2 (London): character creation");
const Common::Rect kFirstRect(54, 75, 151, 85);
const Common::Rect kLastRect(167, 75, 266, 85);
@@ -1249,7 +1248,7 @@ void EEMEngine::showLondonCharSelect() {
_chainStage = 1;
for (uint i = 0; i < sizeof(_mysteriesSolved); i++)
_mysteriesSolved[i] = 0;
- debugC(1, kDebugGeneral, "London PoC: player='%s' partner=%s",
+ debugC(1, kDebugGeneral, "London: player='%s' partner=%s",
_playerName.c_str(), partner == kPartnerJake ? "Jake" : "Jenny");
}
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 8fcb1abeec3..773e6a9532c 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -47,6 +47,14 @@ namespace EEM {
class AudioPlayer;
class MusicPlayer;
+/// VGA palette size in bytes (256 colours à RGB). Defined in eem.cpp.
+const uint kPalSize = 768;
+
+/// Palette fade helpers (defined in eem.cpp): ramp the VGA palette to / from
+/// black over 16 steps, matching `_FadeToBlack` / `_OpenFadeIn`.
+void fadeCurrentPaletteToBlack(uint delayMs = 8);
+void fadePaletteFromBlack(const byte *target, uint delayMs = 8);
+
/// _ScreenDriver dispatch table @ 1a35:0e5e (fallback @ 1a35:0e54).
/// 14 entries total: each is a screen ID + near fn ptr at +0x1c; driver
/// tail-calls via `JMP word ptr CS:[BX + 0x1c]`. Handler bodies update
@@ -103,7 +111,7 @@ enum ScreenId {
enum Variant {
kVariantCD = 0,
kVariantFloppy = 1,
- kVariantLondonCD = 2, ///< Eagle Eye Mysteries in London (EEM2CD.EXE) â PoC.
+ kVariantLondonCD = 2, ///< Eagle Eye Mysteries in London (EEM2CD.EXE).
};
/// `_Partner @ 29be:7918`. Selected at the partner-pick screen
@@ -135,9 +143,9 @@ public:
Common::Platform getPlatform() const;
Variant getVariant() const { return _variant; }
bool isFloppy() const { return _variant == kVariantFloppy; }
- /// EEM2 ("Eagle Eye Mysteries in London"). Proof-of-concept variant:
- /// same DBD/palette/font formats, but a 63-entry "SITEPALS." file and
- /// different opening picture/palette IDs.
+ /// EEM2 ("Eagle Eye Mysteries in London") â reimplementation in progress.
+ /// Shares the EEM1 DBD/palette/font formats, but ships a 63-entry
+ /// "SITEPALS." file and uses different picture/palette IDs per screen.
bool isLondon() const { return _variant == kVariantLondonCD; }
bool hasFeature(EngineFeature f) const override;
@@ -408,17 +416,24 @@ private:
void showHighScoreLogo();
void showFloppyStormLogo();
- // --- EEM2 ("...in London") proof of concept ---
- /// Full opening sequence + character-selection screen â EEM2's
- /// `_DoOpeningAnims` @ 2721:08e6 then `_NewPlayer` @ 1cd3:0f27.
- void runLondonScreensPoc();
+ // --- EEM2 ("Eagle Eye Mysteries in London") â reimplementation in progress ---
+ /// Opening sequence + character creation â EEM2's `_DoOpeningAnims`
+ /// @ 2721:08e6 then `_NewPlayer` @ 1cd3:0f27.
+ void runLondonStartup();
/// Blit a full-screen still PIC and fade it in / hold / out using the
/// given SITEPALS. palette index.
void showLondonLogo(uint picId, uint palId, uint holdMs);
- /// Render EEM2's character-creation screen (`_NewPlayer`: palette 0 +
- /// background PIC 0xc, where the player types a name and picks
- /// Jake/Jenny). PoC display; interactive entry is still TODO.
+ /// EEM2 character creation (`_NewPlayer`: palette 0 + background PIC 0xc):
+ /// first/last name entry + Jake/Jenny selection.
void showLondonCharSelect();
+ /// EEM2 case-intro animation (`_DoInitClues` @ 1abf:03b3): single partner
+ /// anim (Jake 0x18 / Jenny 0x71) faded in, then phone1.voc on caseType 1.
+ void playLondonInitCluesAnim(uint16 caseType, const Picture &bg,
+ bool haveBriefingBg);
+ /// EEM1 CD/floppy case-intro animation (`_DoInitClues` @ 1a35:0411):
+ /// game/book/nancy cycle + `_PlayInSequence` partner entrance.
+ void playCdFloppyInitCluesAnim(uint16 caseType, bool floppy,
+ const Picture &bg, bool haveBriefingBg);
/// `screen8_handler @ 1c33:1012`. Profile selector â walks
/// `listProfiles()`, falls through to `doNewPlayer()` if "New" or
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 0d6cb294edb..7a5d552084e 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -2118,11 +2118,11 @@ void EEMEngine::doCaseSelection() {
const uint mn = stageLo + selRow;
if (isLondon()) {
- // EEM2 PoC: the menu (PIC 0x41, ANI 0x15/0x16, BOOK*.NME) is shared
- // with EEM1, but EEM2's mystery data (M*.BIN/E*.BIN) isn't ported,
- // so stop here instead of parsing it with the EEM1 loader.
+ // EEM2: the menu (PIC 0x41, ANI 0x15/0x16, BOOK*.NME) is shared with
+ // EEM1, but loading a chosen mystery from the menu isn't wired up yet
+ // (the training case is started directly from run()), so stop here.
debugC(1, kDebugMystery,
- "London PoC: selected mystery %u (load not implemented)", mn);
+ "London: selected mystery %u (menu load not implemented yet)", mn);
return;
}
if (!_mystery.load(mn, &_rng)) {
Commit: 24d75f3cc78870ad0dc7372561fa5541e3fbccf8
https://github.com/scummvm/scummvm/commit/24d75f3cc78870ad0dc7372561fa5541e3fbccf8
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:22+02:00
Commit Message:
EEM: training case in London plays renders the first site
Changed paths:
engines/eem/clues.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index fc35a14407a..8a314cac743 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -61,7 +61,8 @@ const int kJakeY = 0x62; // 98
const int kJennyX = 0x42; // 66
const int kJennyY = 0x60; // 96
-uint markClueBlockNotebookEntries(Mystery &mystery, const byte *clueBlock) {
+uint markClueBlockNotebookEntries(Mystery &mystery, const byte *clueBlock,
+ bool isLondon) {
if (!clueBlock)
return 0;
@@ -69,11 +70,15 @@ uint markClueBlockNotebookEntries(Mystery &mystery, const byte *clueBlock) {
if (number == 0 || number > 32)
return 0;
+ // EEM2 entries are 84 bytes (0x54) with the notebook list at entry+0x40;
+ // EEM1 entries are 62 bytes with the list at entry+0x30. See displayClue.
+ const uint stride = isLondon ? 0x54 : 62;
+ const uint noteOffat = isLondon ? 0x3c : 0x30;
uint marked = 0;
for (uint i = 0; i < number; i++) {
- const byte *entry = clueBlock + 4 + i * 62;
+ const byte *entry = clueBlock + 4 + i * stride;
for (uint j = 0; j < 5; j++) {
- const uint16 note = READ_LE_UINT16(entry + 0x30 + j * 2);
+ const uint16 note = READ_LE_UINT16(entry + noteOffat + j * 2);
if (note != 0xFFFF && note < Mystery::kCluesFoundCap &&
mystery._cluesFound[note] == 0) {
mystery._cluesFound[note] = 1;
@@ -611,7 +616,8 @@ void EEMEngine::doInitClues() {
const byte *briefingClues = ib + 4;
// _DisplayClue calls _AddNotebook for each ClueEntry note list at
// +0x30..+0x39. Mark starting notes before the first PDA visit.
- const uint marked = markClueBlockNotebookEntries(_mystery, briefingClues);
+ const uint marked = markClueBlockNotebookEntries(_mystery, briefingClues,
+ isLondon());
if (marked != 0)
debugC(1, kDebugScript,
"doInitClues: marked %u CD briefing notebook entries",
@@ -717,6 +723,37 @@ Common::String EEMEngine::parseString(const Common::String &raw,
}
void EEMEngine::applyClueSideEffects(const byte *c) {
+ if (isLondon()) {
+ // EEM2 `_DisplayClue @ 2542:05bd` per-entry side effects. With the
+ // shared `c = entryBase + 4` convention the EEM2 fields land at:
+ // onsite entry+0x22 (= c+0x1e), 5 Ã u16, high bit = CONSITE flag
+ // offsite entry+0x2c (= c+0x28), 5 Ã u16, clears the site
+ // notebook entry+0x40 (= c+0x3c), 5 Ã u16 -> _AddNotebook
+ // (EEM2 has no gallery list here â that region is the onsite array.)
+ for (uint j = 0; j < 5; j++) {
+ const uint16 note = READ_LE_UINT16(c + 0x3c + j * 2);
+ if (note != 0xFFFF && note < Mystery::kCluesFoundCap)
+ _mystery._cluesFound[note] = 1;
+
+ const uint16 onIdx = READ_LE_UINT16(c + 0x1e + j * 2);
+ if (onIdx != 0xFFFF) {
+ const uint16 siteVal = onIdx & 0x7FFF;
+ if (siteVal < Mystery::kVisitedSiteCap)
+ _mystery._onSites[siteVal] = 1;
+ if (onIdx & 0x8000)
+ _mystery._sawCONSITEs = true;
+ }
+
+ const uint16 offIdx = READ_LE_UINT16(c + 0x28 + j * 2);
+ if (offIdx != 0xFFFF && (offIdx & 0x8000) == 0) {
+ const uint16 siteVal = offIdx & 0x7FFF;
+ if (siteVal < Mystery::kVisitedSiteCap)
+ _mystery._onSites[siteVal] = 0;
+ }
+ }
+ return;
+ }
+
for (uint j = 0; j < 5; j++) {
const uint16 note = READ_LE_UINT16(c + 0x30 + j * 2);
if (note != 0xFFFF && note < Mystery::kCluesFoundCap)
@@ -754,6 +791,13 @@ void EEMEngine::displayClue(const byte *clueBlock) {
if (number == 0 || number > 32)
return;
+ // ClueEntry stride. EEM1 `_DisplayClue @ 2404:05e6` packs 62-byte entries;
+ // EEM2 `_DisplayClue @ 2542:05bd` indexes `theClue + i*0x54` (84 bytes). The
+ // per-entry fields up to +0x1e are layout-identical (so entry 0 â at the
+ // shared base clueBlock+4 â reads the same either way); only the stride and
+ // the tail fields (KD anim, notebook, onsite) move. See applyClueSideEffects.
+ const uint stride = isLondon() ? 0x54 : 62;
+
// Snapshot BG so per-entry character pics don't stack.
Graphics::ManagedSurface bg(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
@@ -781,11 +825,12 @@ void EEMEngine::displayClue(const byte *clueBlock) {
// Partner 0 always uses field 0.
for (uint i = 0; i < number && !shouldQuit(); i++) {
g_system->copyRectToScreen(bg.getPixels(), bg.pitch, 0, 0, kScreenWidth, kScreenHeight);
- const byte *c = clueBlock + 4 + i * 62;
+ const byte *c = clueBlock + 4 + i * stride;
// _DisplayClue @ 2404:0635-064b: _DoKDAnim(num) runs before the
- // speaker portrait.
- const int16 kdAnimNum = (int16)READ_LE_UINT16(c + 0x3a);
+ // speaker portrait. EEM1 stores the KD-anim number at +0x3a; EEM2
+ // `_DisplayClue @ 2542:05bd` reads it at entry+0x52 (= c+0x4e).
+ const int16 kdAnimNum = (int16)READ_LE_UINT16(c + (isLondon() ? 0x4e : 0x3a));
if (kdAnimNum != -1) {
playKdAnim((uint16)kdAnimNum);
// _UpdateAnimations @ 172b:09c1 reactivates the wait anim.
@@ -884,10 +929,17 @@ void EEMEngine::displayClue(const byte *clueBlock) {
_font.drawWordWrapped(&scratch, textX, textY,
MAX<int>(8, textW), text, 0);
- g_system->copyRectToScreen(scratch.getBasePtr(0, copyY),
- scratch.pitch, 0, copyY, kScreenWidth,
- MIN<int>(copyH, kScreenHeight - copyY));
- g_system->updateScreen();
+ // Clamp to the screen: a malformed entry (e.g. a clue-format
+ // mismatch) must never hand copyRectToScreen a negative height,
+ // which would underflow to a multi-GB memcpy.
+ copyY = CLIP<int>(copyY, 0, kScreenHeight - 1);
+ const int copyRows = CLIP<int>(MIN<int>(copyH, kScreenHeight - copyY),
+ 0, kScreenHeight - copyY);
+ if (copyRows > 0) {
+ g_system->copyRectToScreen(scratch.getBasePtr(0, copyY),
+ scratch.pitch, 0, copyY, kScreenWidth, copyRows);
+ g_system->updateScreen();
+ }
}
// _DisplayClue @ 2404:0833-085a â per-clue voice gate. The gate is on
@@ -948,7 +1000,7 @@ void EEMEngine::displayClue(const byte *clueBlock) {
if (skipAll) {
// Apply remaining side-effects without rendering.
for (uint k = i; k < number; k++)
- applyClueSideEffects(clueBlock + 4 + k * 62);
+ applyClueSideEffects(clueBlock + 4 + k * stride);
return;
}
}
Commit: ea2453f9bc1643313f59c51435ace96f597d70df
https://github.com/scummvm/scummvm/commit/ea2453f9bc1643313f59c51435ace96f597d70df
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:22+02:00
Commit Message:
EEM: improved animation in London
Changed paths:
engines/eem/clues.cpp
engines/eem/eem.cpp
engines/eem/site.cpp
engines/eem/site.h
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 8a314cac743..fcfe31c2f39 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -295,15 +295,23 @@ void EEMEngine::playLondonInitCluesAnim(uint16 caseType, const Picture &bg,
g_system->getPaletteManager()->setPalette(black, 0, 256);
g_system->updateScreen();
+ // Drive the cells through the animation script (`_NewAnimation` prior
+ // 0x18 -> `_AnimationSequences[0x18]`), not a raw 0..N-1 sweep â the
+ // frame shown each tick is `script[tick]`, not `tick`. For EEM2 that
+ // script is the count-up {0..16}, but routing through `partnerFrameAtTick`
+ // keeps this faithful if the cells/script ever diverge and matches the map/
+ // site partner rendering. One pass = one script cell per ~140 ms tick.
bool skip = false;
const uint frames = haveAnim ? (uint)anim.size() : 1;
for (uint frame = 0; frame < frames && !shouldQuit() && !skip; frame++) {
if (haveBriefingBg)
blitAt(bg, 0, 0);
if (haveAnim) {
+ const uint cell =
+ partnerFrameAtTick(0x18, (uint)anim.size(), frame * 140);
Graphics::Surface *scr = g_system->lockScreen();
if (scr) {
- blitAnimFrameAnchored(scr, anim[frame], kAnchorX, kAnchorY);
+ blitAnimFrameAnchored(scr, anim[cell], kAnchorX, kAnchorY);
g_system->unlockScreen();
}
}
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 9b78267ec4c..57975e48bea 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -165,10 +165,13 @@ EEMEngine::EEMEngine(OSystem *syst, const ADGameDescription *gameDesc)
Common::String(gameDesc->extra).contains("Floppy"))
? kVariantFloppy : kVariantCD;
// EEM2 ("...in London") ships as a separate detection entry (gameId
- // "eem2"); it reuses this engine but only the opening screens work.
+ // "eem2"); it reuses this engine with London-specific data.
if (gameDesc && gameDesc->gameId &&
Common::String(gameDesc->gameId) == "eem2")
_variant = kVariantLondonCD;
+ // EEM2 ships its own `_AnimationSequences` â many partner/KD scripts
+ // differ from EEM1's, so route `findAnimScript` to the EEM2 table.
+ setLondonAnimScripts(isLondon());
_language = gameDesc ? gameDesc->language : Common::EN_ANY;
}
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 90a2bc011de..466c9e13427 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -509,11 +509,67 @@ const uint8 kImpatientSequence[] = { 0,1,0,1,0,1,0,1,2,1 };
// behavior but makes the feature observable during normal testing.
const uint32 kImpatienceDelayMs = 60 * 1000;
+// EEM2 ("Eagle Eye Mysteries in London") animation scripts. EEM2 ships its own
+// `_AnimationSequences @ 2bca:2e2e` (read from EEM2CD.EXE) and MANY partner/KD
+// scripts differ from EEM1's â e.g. 0x18 is a plain count-up 0..16 in EEM2 but
+// "0..8, hold, 9..15" in EEM1 (`kScript18`); 0x14 is 0..10 vs 0..8; the PDA /
+// gesture / gallery scripts (0x01/0x03/0x04/0x05/0x06/0x0b/0x0c/0x0d/0x0e) are
+// all different lengths. Using EEM1's scripts on EEM2's cells plays the wrong
+// frame sequence (visible corruption). These override the EEM1 tables when the
+// London variant is active; any seqnum not listed falls through to the shared
+// EEM1 scripts below. Only the scripts that actually differ are listed.
+const uint8 kScript06London[] = {
+ 0,1,2,3,4,5,6,7,8,9,10,11,11,11,5,6,7,8,9,10,11,11,11,
+ 5,6,7,8,9,10,11,11,11,5,4,3,2,1,0
+};
+const AnimScript kAnimScriptsLondon[] = {
+ { 0x01, 6, { 0,1,2,3,4,5 } }, // PDA idle
+ { 0x03, 15, { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14 } }, // gesture
+ { 0x04, 23, { 0,1,2,3,4,5,5,5,6,5,5,5,5,6,5,5,5,5,4,3,2,1,0 } },// big gesture
+ { 0x05, 11, { 0,1,2,3,4,4,4,3,2,1,0 } },
+ { 0x0b, 6, { 0,1,2,3,4,5 } }, // Jenny PDA
+ { 0x0c, 15, { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14 } }, // Jenny gesture
+ { 0x0d, 23, { 0,1,2,3,4,5,5,5,6,5,5,5,5,6,5,5,5,5,4,3,2,1,0 } },
+ { 0x0e, 22, { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,20 } },
+ { 0x14, 11, { 0,1,2,3,4,5,6,7,8,9,10 } }, // BigMap walk
+ { 0x18, 17, { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16 } }, // case-intro entrance
+ { 0x19, 2, { 0,1 } },
+ { 0x1a, 4, { 0,1,2,3 } },
+};
+const AnimScriptLong kAnimScriptsLondonLong[] = {
+ { 0x06, 38, kScript06London },
+};
+
+// Set true for the London variant so findAnimScript uses the EEM2 tables.
+bool g_londonAnimScripts = false;
+
+void setLondonAnimScripts(bool enabled) {
+ g_londonAnimScripts = enabled;
+}
+
struct AnimScriptRef {
const uint8 *frames;
uint16 len;
};
AnimScriptRef findAnimScript(uint16 seqnum) {
+ if (g_londonAnimScripts) {
+ for (uint i = 0; i < ARRAYSIZE(kAnimScriptsLondon); i++) {
+ if (kAnimScriptsLondon[i].seqnum == seqnum) {
+ AnimScriptRef r;
+ r.frames = kAnimScriptsLondon[i].frames;
+ r.len = kAnimScriptsLondon[i].len;
+ return r;
+ }
+ }
+ for (uint i = 0; i < ARRAYSIZE(kAnimScriptsLondonLong); i++) {
+ if (kAnimScriptsLondonLong[i].seqnum == seqnum) {
+ AnimScriptRef r;
+ r.frames = kAnimScriptsLondonLong[i].frames;
+ r.len = kAnimScriptsLondonLong[i].len;
+ return r;
+ }
+ }
+ }
for (uint i = 0; i < ARRAYSIZE(kAnimScripts); i++) {
if (kAnimScripts[i].seqnum == seqnum) {
AnimScriptRef r;
diff --git a/engines/eem/site.h b/engines/eem/site.h
index cc1b798e453..9f71863586e 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -43,6 +43,11 @@ class Mystery;
/// Mirrors the looping path of `_UpdateAnimations @ 172b:09c1`.
uint partnerFrameAtTick(uint16 seqnum, uint numFrames, uint32 tickMs);
+/// Select the EEM2 ("London") animation-script table inside `findAnimScript`.
+/// EEM2 ships its own `_AnimationSequences`; many partner/KD scripts differ
+/// from EEM1's, so the engine must use the EEM2 sequences for that variant.
+void setLondonAnimScripts(bool enabled);
+
/// bigMapPartnerFrameAtTick: count-up 0..8 once, then loop `_BigMapWaitSeq`
/// (9,9,9,9,10,9,9,9,9). Mirrors `_DoBigMap @ 20fe:09e7` two-phase swap from
/// script 0x14 (count-up @ 29be:196a) to `_BigMapWaitSeq @ 29be:1574`.
Commit: dd0a8761aaaf881825137ea7e1f6432d634db178
https://github.com/scummvm/scummvm/commit/dd0a8761aaaf881825137ea7e1f6432d634db178
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:23+02:00
Commit Message:
EEM: added more animation from London
Changed paths:
engines/eem/site.cpp
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 466c9e13427..0404614f1fd 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -178,6 +178,20 @@ const uint16 kKdAnimTable[6][6] = {
{ 0x06, 0x06, 6, 6, 80, 80 }, // 5 â same anim both partners
};
+// EEM2 `_DoKDAnim @ 1717:05bf` table @ 2bca:0238 (read from EEM2CD.EXE).
+// Same { animJake, animJenny, xJake, xJenny, yJake, yJenny } layout. Positions
+// differ (xâ2, yâ78 vs EEM1's 6/80) and Jenny's reactions 4/5 use distinct
+// anims 0x55/0x2d (EEM1 reused 0x05/0x06 for both partners). Selected by
+// `playKdAnim` when the London variant is active.
+const uint16 kKdAnimTableLondon[6][6] = {
+ { 0x03, 0x0c, 3, 2, 66, 65 }, // 0
+ { 0x01, 0x0b, 2, 2, 78, 78 }, // 1
+ { 0x04, 0x0d, 2, 2, 78, 78 }, // 2
+ { 0x02, 0x10, 2, 2, 78, 78 }, // 3
+ { 0x05, 0x55, 2, 2, 78, 78 }, // 4 â Jenny uses 0x55
+ { 0x06, 0x2d, 2, 2, 78, 78 }, // 5 â Jenny uses 0x2d
+};
+
// Animation script table â mirrors `_AnimationSequences @ 29be:22d4`
// (55-entry table of far ptrs, each pointing to a u16-frame-index
// stream). `_NewAnimation @ 172b:06e1` reads the script via
@@ -510,17 +524,62 @@ const uint8 kImpatientSequence[] = { 0,1,0,1,0,1,0,1,2,1 };
const uint32 kImpatienceDelayMs = 60 * 1000;
// EEM2 ("Eagle Eye Mysteries in London") animation scripts. EEM2 ships its own
-// `_AnimationSequences @ 2bca:2e2e` (read from EEM2CD.EXE) and MANY partner/KD
-// scripts differ from EEM1's â e.g. 0x18 is a plain count-up 0..16 in EEM2 but
-// "0..8, hold, 9..15" in EEM1 (`kScript18`); 0x14 is 0..10 vs 0..8; the PDA /
-// gesture / gallery scripts (0x01/0x03/0x04/0x05/0x06/0x0b/0x0c/0x0d/0x0e) are
-// all different lengths. Using EEM1's scripts on EEM2's cells plays the wrong
+// `_AnimationSequences @ 2bca:2e2e` (read from EEM2CD.EXE); essentially every
+// non-trivial partner / KD / site script differs from EEM1's â e.g. 0x18 is a
+// plain count-up 0..16 in EEM2 vs "0..8, hold, 9..15" in EEM1 (`kScript18`),
+// 0x14 is 0..10 vs 0..8. Using EEM1's scripts on EEM2's cells plays the wrong
// frame sequence (visible corruption). These override the EEM1 tables when the
// London variant is active; any seqnum not listed falls through to the shared
-// EEM1 scripts below. Only the scripts that actually differ are listed.
+// EEM1 scripts below. Only the seqnums that actually differ are listed.
+//
+// NOTE: EEM2 scripts 0x27/0x2e/0x30 end with a `0x81 N` jump (loop back to
+// entry N) rather than a 0x80 restart. `frameFromScriptAtTick` has no jump
+// support (EEM1 never used it), so the flat frame list is stored: correct for a
+// one-shot play-through, but a looped play replays the intro instead of just
+// the post-jump tail. Acceptable for these site NPC fidgets; revisit if needed.
const uint8 kScript06London[] = {
- 0,1,2,3,4,5,6,7,8,9,10,11,11,11,5,6,7,8,9,10,11,11,11,
- 5,6,7,8,9,10,11,11,11,5,4,3,2,1,0
+ 0,1,2,3,4,5,6,7,8,9,10,11,11,11,5,6,7,8,9,10,
+ 11,11,11,5,6,7,8,9,10,11,11,11,5,4,3,2,1,0,
+};
+const uint8 kScript1dLondon[] = {
+ 0,1,0,1,1,0,0,2,3,4,5,6,7,8,9,1,1,1,1,0,
+ 0,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+ 2,2,2,2,2,2,
+};
+const uint8 kScript1eLondon[] = {
+ 0,1,0,1,0,1,1,0,0,2,2,2,2,3,3,4,4,5,6,6,
+ 4,3,2,1,0,1,0,1,0,1,1,0,0,
+};
+const uint8 kScript1fLondon[] = {
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+ 0,1,1,1,1,2,1,3,3,4,5,6,5,6,4,1,1,1,
+};
+const uint8 kScript23London[] = {
+ 0,0,0,0,1,1,2,2,3,3,3,4,4,3,3,3,4,4,1,0,
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+};
+const uint8 kScript25London[] = {
+ 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,
+ 19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,
+ 19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,
+};
+const uint8 kScript27London[] = { // 0x81 jump->22
+ 0,0,0,0,0,0,0,0,0,0,1,0,1,2,3,2,4,5,6,6,
+ 7,6,8,6,7,7,7,7,8,
+};
+const uint8 kScript2bLondon[] = {
+ 0,1,0,1,1,1,0,1,2,2,2,2,3,4,3,4,3,2,1,0,
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+};
+const uint8 kScript2dLondon[] = {
+ 0,1,2,3,4,5,6,7,8,9,10,11,11,11,5,6,7,8,9,10,
+ 11,11,11,5,6,7,8,9,10,11,11,11,5,4,3,2,1,0,
+};
+const uint8 kScript31London[] = {
+ 32,32,32,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,
+ 17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,
+ 37,38,39,40,41,42,43,44,45,46,32,32,32,32,32,32,32,32,32,32,
+ 32,32,
};
const AnimScript kAnimScriptsLondon[] = {
{ 0x01, 6, { 0,1,2,3,4,5 } }, // PDA idle
@@ -535,9 +594,38 @@ const AnimScript kAnimScriptsLondon[] = {
{ 0x18, 17, { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16 } }, // case-intro entrance
{ 0x19, 2, { 0,1 } },
{ 0x1a, 4, { 0,1,2,3 } },
+ { 0x1b, 12, { 0,0,1,1,2,2,3,3,4,4,5,5 } },
+ { 0x1c, 19, { 11,11,11,0,1,2,3,4,5,6,7,8,9,10,11,11,11,11,11 } },
+ { 0x20, 19, { 0,1,1,2,3,3,0,0,1,1,1,0,3,2,0,1,4,4,4 } },
+ { 0x21, 5, { 0,0,1,1,2 } },
+ { 0x22, 10, { 0,0,0,1,0,1,0,1,1,1 } },
+ { 0x24, 18, { 0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 } },
+ { 0x26, 20, { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,2,1,3 } },
+ { 0x28, 16, { 0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7 } },
+ { 0x29, 14, { 0,1,2,3,4,4,3,4,3,4,3,2,1,0 } },
+ { 0x2a, 23, { 0,0,0,1,2,3,2,1,0,0,0,0,1,2,3,2,1,0,1,2,3,2,1 } },
+ { 0x2c, 22, { 15,15,15,15,15,15,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 } },
+ { 0x2e, 16, { 0,0,0,1,2,3,4,5,6,7,8,9,10,11,12,12 } }, // 0x81 jump->15
+ { 0x2f, 6, { 0,1,2,3,4,5 } },
+ { 0x30, 26, { 18,18,18,18,18,18,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,18 } }, // 0x81 jump->25
+ { 0x32, 27, { 0,0,0,0,1,1,0,0,0,1,0,0,0,0,0,0,0,0,1,1,0,0,0,1,0,0,1 } },
+ { 0x33, 27, { 4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,0,1,2,3,4,4,4,0,1,2,3 } },
+ { 0x34, 4, { 0,1,2,3 } },
+ { 0x35, 21, { 9,9,9,9,9,9,9,9,9,9,9,9,0,1,2,3,4,5,6,7,8 } },
+ { 0x36, 12, { 0,0,0,0,0,1,2,3,4,5,6,6 } },
+ { 0x55, 11, { 0,1,2,3,4,4,4,3,2,1,0 } }, // KD Jenny reaction-4
};
const AnimScriptLong kAnimScriptsLondonLong[] = {
{ 0x06, 38, kScript06London },
+ { 0x1d, 46, kScript1dLondon },
+ { 0x1e, 33, kScript1eLondon },
+ { 0x1f, 38, kScript1fLondon },
+ { 0x23, 39, kScript23London },
+ { 0x25, 59, kScript25London },
+ { 0x27, 29, kScript27London },
+ { 0x2b, 39, kScript2bLondon },
+ { 0x2d, 38, kScript2dLondon },
+ { 0x31, 62, kScript31London },
};
// Set true for the London variant so findAnimScript uses the EEM2 tables.
@@ -1805,14 +1893,16 @@ void EEMEngine::playKdAnim(uint16 num) {
// (blocking) and resume normal idle rendering when the caller
// returns â matches the visible effect (partner gesture finishes
// before the speaker portrait + speech balloon appear).
- // `kKdAnimTable` and `kAnimScripts` live at file scope above.
+ // `kKdAnimTable` and `kAnimScripts` live at file scope above. EEM2 ships a
+ // different table (positions + Jenny reactions) â use it for London.
if (num >= ARRAYSIZE(kKdAnimTable))
return;
+ const uint16 (*kdTable)[6] = isLondon() ? kKdAnimTableLondon : kKdAnimTable;
const uint partner = (_partner == kPartnerJake) ? 0 : 1;
- const uint16 animId = kKdAnimTable[num][partner];
- const int px = (int)kKdAnimTable[num][2 + partner];
- const int py = (int)kKdAnimTable[num][4 + partner];
+ const uint16 animId = kdTable[num][partner];
+ const int px = (int)kdTable[num][2 + partner];
+ const int py = (int)kdTable[num][4 + partner];
Animation anim;
if (!_aniArchive.loadAnimation(animId, anim) || anim.empty()) {
Commit: a5d0ea9233de74aed7ed1565873ce88de247609f
https://github.com/scummvm/scummvm/commit/a5d0ea9233de74aed7ed1565873ce88de247609f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:23+02:00
Commit Message:
EEM: mouse cursor change from London
Changed paths:
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/site.cpp
engines/eem/site.h
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 57975e48bea..5207a68524e 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -59,6 +59,15 @@ const uint kPalHighScore = 0x27;
const uint kPalStormLogo = 0x26; // Floppy FUN_23d2_0605
const uint kPicMousePointer = 0x50; // 0x51 is the wait cursor
+// EEM2 cursor table â `_main @ 1abf:0faf` loads seven cursor PICs into
+// `_AnimationObjects`-adjacent slots and `_SwitchMouse @ 17ee:2c83` activates
+// one by index. Order matches the slot order: 0 arrow, 1 wait, 2/3 examine,
+// 4 Jake hand, 5 Jenny hand, 6 approach. Indexed by the site search-record
+// cursor id (row +0xc).
+const uint16 kLondonCursorPics[7] = {
+ 0x50, 0x51, 0x206, 0xa1, 0x207, 0x20b, 0x35e
+};
+
const byte kSaveBodyVer = 1;
// Test switch: populate ScrapBook 1 at startup without exposing a game
@@ -634,9 +643,44 @@ void EEMEngine::setInteractiveMouseCursor(bool active) {
}
void EEMEngine::setHotspotMouseCursor(bool active) {
+ // EEM2 swaps the cursor SHAPE per hotspot (see setSiteHotspotCursorId), so
+ // the EEM1 red-outline recolor doesn't apply. The bool path only resets to
+ // the default arrow (cursor 0) when leaving a hotspot / the site loop.
+ if (isLondon()) {
+ if (!active)
+ setSiteHotspotCursorId(0);
+ return;
+ }
setInteractiveMouseCursor(active);
}
+void EEMEngine::setSiteHotspotCursorId(int cursorId) {
+ if (!isLondon())
+ return;
+ // `_SwitchMouse @ 17ee:2c83`: cursor 4 (Jake hand) becomes 5 for Jenny.
+ if (cursorId == 4 && _partner == kPartnerJenny)
+ cursorId = 5;
+ if (cursorId < 0 || cursorId >= (int)ARRAYSIZE(kLondonCursorPics))
+ cursorId = 0;
+ if (cursorId == _siteCursorId)
+ return;
+
+ Picture cursor;
+ if (!_picsArchive.getPicture(kLondonCursorPics[cursorId], cursor) ||
+ cursor.surface.empty()) {
+ warning("EEM2: cursor %d (PIC 0x%x) missing", cursorId,
+ kLondonCursorPics[cursorId]);
+ return;
+ }
+ // Each cursor carries its own transparent colour (flags >> 8) and uses the
+ // active screen palette (no separate cursor palette, unlike the EEM1
+ // red-outline highlight). Hotspot is the top-left (rowoff/misc are 0).
+ const byte transparent = (byte)(cursor.flags >> 8);
+ CursorMan.replaceCursor(cursor.surface.rawSurface(), 0, 0, transparent);
+ CursorMan.replaceCursorPalette(nullptr, 0, 0);
+ _siteCursorId = cursorId;
+}
+
bool EEMEngine::openArchives() {
// _InitGraphicsSystem @ 172b:0145.
if (!_picsArchive.open(Common::Path("PICS.DBD"), Common::Path("PICS.DBX"))) {
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 773e6a9532c..bb504769b3d 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -198,6 +198,12 @@ public:
/// Interactive cursor over searchable hotspots.
void setHotspotMouseCursor(bool active);
+ /// EEM2 `_SwitchMouse @ 17ee:2c83`: swap the cursor SHAPE to one of the
+ /// seven loaded cursors by ID (0 arrow, 1 wait, 2/3 examine, 4/5 partner
+ /// hand, 6 approach). London site hotspots carry a cursor ID at row +0xc;
+ /// EEM1 cursors are all 0 so this is a no-op there.
+ void setSiteHotspotCursorId(int cursorId);
+
/// `_DisplayClue @ 2404:05e6`. @p clueBlock points at the u16 frame
/// count followed by 62-byte ClueEntries.
void displayClue(const byte *clueBlock);
@@ -563,6 +569,9 @@ private:
Graphics::ManagedSurface _partnerEraseBg;
bool _interactiveMouseCursor = false;
+ /// Active EEM2 cursor shape (index into `kLondonCursorPics`). -1 forces a
+ /// reload on the next `setSiteHotspotCursorId`.
+ int _siteCursorId = 0;
/// Site whose entrance animation has already played this mystery.
/// Lives on the engine because PDA/gallery destroys+recreates SiteScreen.
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 0404614f1fd..7598fad3163 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -1777,13 +1777,31 @@ int SiteScreen::hotspotAtPoint(uint siteNum, int x, int y) const {
return -1;
}
+// CD hotspot row +0xc..d: cursor id for `_SwitchMouse` (EEM1 ships 0; EEM2
+// uses 2/3 examine, etc.). Floppy rows are 8-byte rects with no cursor field.
+int SiteScreen::hotspotCursorId(uint siteNum, int idx) const {
+ if (idx < 0 || (_vm && _vm->isFloppy()))
+ return 0;
+ const byte *spots = _mystery->hotspots(siteNum);
+ if (!spots || (uint)idx >= _mystery->hotspotCount(siteNum))
+ return 0;
+ return (int)READ_LE_UINT16(spots + idx * 14 + 0xc);
+}
+
void SiteScreen::updateHotspotCursor(uint siteNum, int x, int y) {
if (!_vm)
return;
+ const int idx = hotspotAtPoint(siteNum, x, y);
+ if (_vm->isLondon()) {
+ // EEM2 (`_DoSiteLoop` @ FUN_1717_07ab) swaps the cursor shape to the
+ // hovered hotspot's own cursor id; off any hotspot it is the arrow (0).
+ _vm->setSiteHotspotCursorId(idx >= 0 ? hotspotCursorId(siteNum, idx) : 0);
+ return;
+ }
const bool siteControl = kPdaSiteRect.contains(x, y) ||
kPdaPartnerFootMapRect.contains(x, y) ||
kPdaPartnerHeadHintRect.contains(x, y);
- _vm->setHotspotMouseCursor(siteControl || hotspotAtPoint(siteNum, x, y) >= 0);
+ _vm->setHotspotMouseCursor(siteControl || idx >= 0);
}
void SiteScreen::onHotspotClicked(uint siteNum, uint hotIdx) {
diff --git a/engines/eem/site.h b/engines/eem/site.h
index 9f71863586e..78513dd4d4c 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -111,6 +111,8 @@ private:
void renderBackground(uint siteNum);
void renderHotspots(uint siteNum);
int hotspotAtPoint(uint siteNum, int x, int y) const;
+ /// EEM2 cursor id at CD hotspot row +0xc (0 for floppy / EEM1).
+ int hotspotCursorId(uint siteNum, int idx) const;
void updateHotspotCursor(uint siteNum, int x, int y);
void onHotspotClicked(uint siteNum, uint hotIdx);
void initImpatienceCounter();
Commit: 27f48984c620e5747ff055ece33e730ebf5f57c9
https://github.com/scummvm/scummvm/commit/27f48984c620e5747ff055ece33e730ebf5f57c9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:23+02:00
Commit Message:
EEM: jump effect from applyClue in London
Changed paths:
engines/eem/clues.cpp
engines/eem/mystery.cpp
engines/eem/mystery.h
engines/eem/site.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index fcfe31c2f39..3fccb8ed644 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -737,6 +737,7 @@ void EEMEngine::applyClueSideEffects(const byte *c) {
// onsite entry+0x22 (= c+0x1e), 5 Ã u16, high bit = CONSITE flag
// offsite entry+0x2c (= c+0x28), 5 Ã u16, clears the site
// notebook entry+0x40 (= c+0x3c), 5 Ã u16 -> _AddNotebook
+ // jump entry+0x4a (= c+0x46), destination site for direct travel
// (EEM2 has no gallery list here â that region is the onsite array.)
for (uint j = 0; j < 5; j++) {
const uint16 note = READ_LE_UINT16(c + 0x3c + j * 2);
@@ -759,6 +760,9 @@ void EEMEngine::applyClueSideEffects(const byte *c) {
_mystery._onSites[siteVal] = 0;
}
}
+ const uint16 jumpSite = READ_LE_UINT16(c + 0x46);
+ if (jumpSite != 0xFFFF)
+ _mystery._pendingSiteJump = jumpSite;
return;
}
diff --git a/engines/eem/mystery.cpp b/engines/eem/mystery.cpp
index 06892c0735a..bb87dbc9939 100644
--- a/engines/eem/mystery.cpp
+++ b/engines/eem/mystery.cpp
@@ -64,6 +64,9 @@ void Mystery::clear() {
_firstTry = true;
_searchLocationNumber = _siteNumber = 0xFFFF;
_lastSite = 0x1B;
+ _pendingSiteJump = 0;
+ _siteReturnDepth = 0;
+ memset(_siteReturnStack, 0, sizeof(_siteReturnStack));
}
bool Mystery::load(uint num, Common::RandomSource *rng) {
@@ -154,6 +157,9 @@ bool Mystery::load(uint num, Common::RandomSource *rng) {
_firstTry = true;
_searchLocationNumber = _siteNumber = 0xFFFF;
_lastSite = 0x1B;
+ _pendingSiteJump = 0;
+ _siteReturnDepth = 0;
+ memset(_siteReturnStack, 0, sizeof(_siteReturnStack));
loadFloppySiteAnimData();
debugC(1, kDebugMystery,
@@ -213,6 +219,9 @@ bool Mystery::load(uint num, Common::RandomSource *rng) {
_firstTry = true;
_searchLocationNumber = _siteNumber = 0xFFFF;
_lastSite = 0x1B; // _ReadMystery _LastSite sentinel.
+ _pendingSiteJump = 0;
+ _siteReturnDepth = 0;
+ memset(_siteReturnStack, 0, sizeof(_siteReturnStack));
debugC(1, kDebugMystery, "Loaded %s (%d B): %u sites, %u suspects, "
"CON=%u COFF=%u, init=0x%04x site=0x%04x text=0x%04x",
@@ -606,6 +615,13 @@ void Mystery::syncState(Common::Serializer &s) {
s.syncAsUint16LE(_searchLocationNumber);
s.syncAsUint16LE(_siteNumber);
s.syncAsUint16LE(_lastSite);
+ if (s.isLoading())
+ _pendingSiteJump = 0;
+ s.syncAsUint16LE(_siteReturnDepth);
+ s.syncArray(_siteReturnStack, kVisitedSiteCap,
+ Common::Serializer::Uint16LE);
+ if (_siteReturnDepth > kVisitedSiteCap)
+ _siteReturnDepth = 0;
}
} // End of namespace EEM
diff --git a/engines/eem/mystery.h b/engines/eem/mystery.h
index 07545cf6e23..7f3d16589c3 100644
--- a/engines/eem/mystery.h
+++ b/engines/eem/mystery.h
@@ -172,6 +172,9 @@ public:
uint16 _searchLocationNumber = 0xFFFF;
uint16 _siteNumber = 0xFFFF;
uint16 _lastSite = 0xFFFF;
+ uint16 _pendingSiteJump = 0; ///< EEM2 _DisplayClue destination site (DAT_2bca_0282).
+ uint16 _siteReturnDepth = 0; ///< EEM2 nested site return stack depth (DAT_2bca_0280).
+ uint16 _siteReturnStack[kVisitedSiteCap] = {};
private:
Common::Array<byte> _data;
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 7598fad3163..dfe94eb3a02 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -1028,6 +1028,9 @@ void SiteScreen::run() {
uint cur = _mystery->_siteNumber;
if (cur >= _mystery->numSites())
cur = 0;
+ _mystery->_pendingSiteJump = 0;
+ if (_mystery->_siteReturnDepth > Mystery::kVisitedSiteCap)
+ _mystery->_siteReturnDepth = 0;
_snapshotSite = -1;
SiteBackendActionObserverRegistration backendActionRegistration(this);
enter(cur);
@@ -1067,6 +1070,25 @@ void SiteScreen::run() {
if (kPdaPartnerFootMapRect.contains(event.mouse.x, event.mouse.y)) {
notePartnerActivity();
_vm->setHotspotMouseCursor(false);
+ if (_vm->isLondon() && _mystery->_siteReturnDepth != 0) {
+ const uint16 depth = --_mystery->_siteReturnDepth;
+ const uint16 returnSite = _mystery->_siteReturnStack[depth];
+ _mystery->_siteReturnStack[depth] = 0;
+ if (returnSite < _mystery->numSites()) {
+ debugC(1, kDebugSite,
+ "London: returning from site %u to site %u",
+ cur, returnSite);
+ _mystery->_lastSite = (uint16)cur;
+ _mystery->_siteNumber = returnSite;
+ cur = returnSite;
+ enter(cur);
+ mouse = g_system->getEventManager()->getMousePos();
+ updateHotspotCursor(cur, mouse.x, mouse.y);
+ break;
+ }
+ warning("London site return target %u out of range",
+ returnSite);
+ }
// CD: _NextScreen=1, floppy=2.
_vm->setNextScreen(_vm->isFloppy() ? kScreenMapAlt
: kScreenMap);
@@ -1086,9 +1108,35 @@ void SiteScreen::run() {
const int idx = hotspotAtPoint(cur, event.mouse.x, event.mouse.y);
if (idx >= 0) {
_vm->setHotspotMouseCursor(false);
+ _mystery->_pendingSiteJump = 0;
onHotspotClicked(cur, (uint)idx);
notePartnerActivity();
- enter(cur, false);
+ const uint16 jumpSite = _mystery->_pendingSiteJump;
+ _mystery->_pendingSiteJump = 0;
+ if (_vm->isLondon() && jumpSite != 0) {
+ if (jumpSite < _mystery->numSites()) {
+ if (_mystery->_siteReturnDepth < Mystery::kVisitedSiteCap) {
+ _mystery->_siteReturnStack[_mystery->_siteReturnDepth++] =
+ (uint16)cur;
+ } else {
+ warning("London site return stack full; "
+ "jumping without return site");
+ }
+ debugC(1, kDebugSite,
+ "London: hotspot jump from site %u to site %u",
+ cur, jumpSite);
+ _mystery->_lastSite = (uint16)cur;
+ _mystery->_siteNumber = jumpSite;
+ cur = jumpSite;
+ enter(cur);
+ } else {
+ warning("London hotspot jump target %u out of range",
+ jumpSite);
+ enter(cur, false);
+ }
+ } else {
+ enter(cur, false);
+ }
// Use CURRENT pointer position (click pos still in rect).
mouse = g_system->getEventManager()->getMousePos();
updateHotspotCursor(cur, mouse.x, mouse.y);
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 7a5d552084e..048bc040fea 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -2954,6 +2954,12 @@ void EEMEngine::doBigMap() {
if (!_mystery.isLoaded())
return;
+ if (isLondon()) {
+ _mystery._pendingSiteJump = 0;
+ _mystery._siteReturnDepth = 0;
+ memset(_mystery._siteReturnStack, 0, sizeof(_mystery._siteReturnStack));
+ }
+
CursorMan.showMouse(true);
while (!shouldQuit()) {
Commit: 17cf0205f9b70d574feff551544faa77b30236fc
https://github.com/scummvm/scummvm/commit/17cf0205f9b70d574feff551544faa77b30236fc
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:24+02:00
Commit Message:
EEM: correct parner handling in London
Changed paths:
engines/eem/clues.cpp
engines/eem/site.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 3fccb8ed644..e75fdbda8ff 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -865,9 +865,12 @@ void EEMEngine::displayClue(const byte *clueBlock) {
// ClueBlock +2; entries N>0 read (entry-1)+0x3c (last word).
const uint16 charX = READ_LE_UINT16(c + (useP1 ? 4 : 0));
const uint16 charY = READ_LE_UINT16(c + (useP1 ? 6 : 2));
- const uint16 charPicId = (i == 0)
+ uint16 charPicId = (i == 0)
? READ_LE_UINT16(clueBlock + 2)
: READ_LE_UINT16(c - 2);
+ if (isLondon() && charPicId == 0x13e &&
+ _partner == kPartnerJake)
+ charPicId = 0x13f;
if (charPicId != 0 && charPicId != 0xFFFF) {
Picture charPic;
if (_picsArchive.getPicture(charPicId, charPic) &&
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index dfe94eb3a02..0aa84dc739b 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -192,6 +192,18 @@ const uint16 kKdAnimTableLondon[6][6] = {
{ 0x06, 0x2d, 2, 2, 78, 78 }, // 5 â Jenny uses 0x2d
};
+// EEM2 `_WaitAnims @ 2bca:022c`. Entry 0 precedes `_DoKDAnim`'s
+// table; entries 1..6 overlap `kKdAnimTableLondon`, same as EEM1.
+const uint16 kWaitAnimsLondon[7][6] = {
+ { 0x00, 0x0a, 2, 2, 78, 78 }, // 0
+ { 0x03, 0x0c, 3, 2, 66, 65 }, // 1
+ { 0x01, 0x0b, 2, 2, 78, 78 }, // 2
+ { 0x04, 0x0d, 2, 2, 78, 78 }, // 3
+ { 0x02, 0x10, 2, 2, 78, 78 }, // 4
+ { 0x05, 0x55, 2, 2, 78, 78 }, // 5
+ { 0x06, 0x2d, 2, 2, 78, 78 }, // 6
+};
+
// Animation script table â mirrors `_AnimationSequences @ 29be:22d4`
// (55-entry table of far ptrs, each pointing to a u16-frame-index
// stream). `_NewAnimation @ 172b:06e1` reads the script via
@@ -1215,6 +1227,39 @@ bool SiteScreen::enterSiteAnim() {
bg.simpleBlitFrom(*screen);
g_system->unlockScreen();
+ if (_vm->isLondon()) {
+ // EEM2 `_EnterSiteAnim @ 17ee:27a4`: no skateboard phase.
+ // It plays the partner-specific slide-in animation at anchor
+ // (0, 0x50/0x4e), using `_CheckFrameRate` between cells.
+ const uint animId = (partner == 0) ? 7 : 0xf;
+ const int anchorY = (partner == 0) ? 0x50 : 0x4e;
+ Animation anim;
+ if (!_vm->getAni().loadAnimation(animId, anim) || anim.empty())
+ return false;
+ for (uint frameIdx = 0;
+ frameIdx < anim.size() && !_vm->shouldQuit();
+ frameIdx++) {
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.simpleBlitFrom(bg);
+ blitAnimFrameAnchored(scratch.surfacePtr(), anim[frameIdx],
+ 0, anchorY);
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, kScreenWidth, kScreenHeight);
+ g_system->updateScreen();
+
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_KEYDOWN ||
+ ev.type == Common::EVENT_LBUTTONDOWN) {
+ return true;
+ }
+ }
+ g_system->delayMillis(kFramePeriodMs);
+ }
+ return false;
+ }
+
// Phase 1 â skateboard scroll.
Animation skate;
if (_vm->getAni().loadAnimation(kSkateAni, skate) && !skate.empty()) {
@@ -1312,9 +1357,12 @@ void SiteScreen::renderStaticDrops(uint siteNum) {
for (uint i = 0; i < numStatic; i++) {
const uint dropOff = 0xc + i * 6;
- const uint16 picId = READ_LE_UINT16(site + dropOff + 0);
+ uint16 picId = READ_LE_UINT16(site + dropOff + 0);
const int16 x = (int16)READ_LE_UINT16(site + dropOff + 2);
const int16 y = (int16)READ_LE_UINT16(site + dropOff + 4);
+ if (_vm->isLondon() && picId == 0x140 &&
+ _vm->getPartnerIndex() == kPartnerJake)
+ picId = 0x141;
if (picId == 0)
continue;
Picture pic;
@@ -1562,14 +1610,16 @@ void SiteScreen::renderPartner(uint siteNum, uint32 tickMs) {
}
} else {
const uint16 speaker = READ_LE_UINT16(site + 8);
+ const uint16 (*waitTable)[6] = _vm->isLondon()
+ ? kWaitAnimsLondon : kWaitAnims;
if (speaker >= ARRAYSIZE(kWaitAnims)) {
warning("renderPartner: site %u has speakerIdx=%u out of range",
siteNum, speaker);
return;
}
- animId = kWaitAnims[speaker][0 + partner];
- x = (int)(int16)kWaitAnims[speaker][2 + partner];
- y = (int)(int16)kWaitAnims[speaker][4 + partner];
+ animId = waitTable[speaker][0 + partner];
+ x = (int)(int16)waitTable[speaker][2 + partner];
+ y = (int)(int16)waitTable[speaker][4 + partner];
}
Animation anim;
Commit: b8f606891d2c512bc6b67e9d81b7856acfa76ed4
https://github.com/scummvm/scummvm/commit/b8f606891d2c512bc6b67e9d81b7856acfa76ed4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:24+02:00
Commit Message:
EEM: implemented traveling animation in London
Changed paths:
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/site.cpp
engines/eem/site.h
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 5207a68524e..36fe8d4fc8b 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -780,7 +780,8 @@ void EEMEngine::interruptAudio(bool stopMusicToo) {
}
void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs,
- bool holdLastFrame, bool fadeIn) {
+ bool holdLastFrame, bool fadeIn,
+ bool setSkipIntroOnEsc) {
ANMDecoder anm;
if (!anm.open(path)) {
warning("playAnm: %s missing", path.toString().c_str());
@@ -832,8 +833,10 @@ void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs,
}
if (event.type == Common::EVENT_KEYDOWN) {
if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
- _skipIntro = true;
- interruptAudio();
+ if (setSkipIntroOnEsc) {
+ _skipIntro = true;
+ interruptAudio();
+ }
}
aborted = true;
break;
@@ -864,8 +867,10 @@ void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs,
}
if (ev.type == Common::EVENT_KEYDOWN) {
if (ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
- _skipIntro = true;
- interruptAudio();
+ if (setSkipIntroOnEsc) {
+ _skipIntro = true;
+ interruptAudio();
+ }
}
clicked = true;
break;
@@ -1361,6 +1366,23 @@ void EEMEngine::startTravelMusic() {
_music->playMus(num, /* loop= */ false);
}
+void EEMEngine::startLondonTravelMusic(uint8 travelKind) {
+ // EEM2 `_DoTravel @ 1717:06ae`: travelKind * 6 indexes this table,
+ // then rand() picks one of three u16 MUS IDs.
+ static const uint16 kLondonTravelMusic[4][3] = {
+ { 0, 0, 0 },
+ { 3, 22, 25 },
+ { 7, 23, 17 },
+ { 10, 21, 24 },
+ };
+ if (!_music || !_voiceOn || travelKind == 0 ||
+ travelKind >= ARRAYSIZE(kLondonTravelMusic))
+ return;
+
+ const uint track = kLondonTravelMusic[travelKind][_rng.getRandomNumber(2)];
+ _music->playMus(track, /* loop= */ false);
+}
+
void EEMEngine::waitForMusicDone(uint32 maxMs) {
if (!_music)
return;
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index bb504769b3d..907dacb7d3a 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -400,7 +400,6 @@ private:
public:
void waitForInput(uint32 maxMs);
-private:
/// Play a difference-encoded animation file (.ANM / .A) on the full
/// 320x200 screen. Mirrors the data flow of `OpenDifferenceAnimation
@@ -412,8 +411,10 @@ private:
/// If @p fadeIn is true the first decoded frame is copied while the
/// palette is black, then ramped in like `_OpenFadeIn`.
void playAnm(const Common::Path &path, uint frameDelayMs = 120,
- bool holdLastFrame = false, bool fadeIn = false);
+ bool holdLastFrame = false, bool fadeIn = false,
+ bool setSkipIntroOnEsc = true);
+private:
/// `_CleanMysterySounds @ 202f:05a5` + `_StopMIDI @ 20a2:0512`.
/// `stopMusicToo=false` keeps MIDI playing across dialog skips.
void interruptAudio(bool stopMusicToo = true);
@@ -519,6 +520,10 @@ public:
/// `Engine::syncSoundSettings` override. Re-pulls `music_volume`
/// into the MIDI player's `_masterVolume`.
void syncSoundSettings() override;
+
+ /// EEM2 `_DoTravel @ 1717:0622` transition music. The matrix entry
+ /// (1..3) chooses one of three short one-shot MUS tracks at random.
+ void startLondonTravelMusic(uint8 travelKind);
private:
Common::String _playerName; ///< Substituted into 0x80 placeholders.
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 0aa84dc739b..c07ac1df532 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -22,6 +22,9 @@
#include "common/config-manager.h"
#include "common/debug.h"
#include "common/events.h"
+#include "common/file.h"
+#include "common/path.h"
+#include "common/str.h"
#include "common/system.h"
#include "common/textconsole.h"
@@ -56,6 +59,30 @@ private:
Common::EventObserver *_observer;
};
+const uint kLondonTravelMatrixDim = 60;
+const uint kLondonTravelFrameDelayMs = 120;
+const uint16 kLondonTravelLastSiteSentinel = 0x1b;
+
+bool londonTravelSitePic(const Mystery *mystery, uint siteNum, uint16 &sitePic) {
+ if (!mystery || !mystery->isLoaded())
+ return false;
+
+ if (siteNum < mystery->numSites()) {
+ const byte *sd = mystery->siteData(siteNum);
+ if (!sd)
+ return false;
+ sitePic = READ_LE_UINT16(sd);
+ return true;
+ }
+
+ if (siteNum == kLondonTravelLastSiteSentinel || siteNum == 0xffff) {
+ sitePic = kLondonTravelLastSiteSentinel;
+ return true;
+ }
+
+ return false;
+}
+
// Masked blit using `transp` = high byte of `pic.flags` (`_Rect_Move_Mask @ 1000:03fc`).
void blitFrame(Graphics::ManagedSurface &dst, const Picture &p,
int x, int y, byte transp) {
@@ -848,6 +875,61 @@ uint bigMapDetailPartnerFrameAtTick(uint numFrames, uint32 elapsedMs) {
numFrames, elapsedMs);
}
+bool SiteScreen::playLondonTravelAnimation(uint fromSite, uint toSite) {
+ if (!_vm || !_mystery || !_vm->isLondon())
+ return false;
+
+ uint16 fromPic = 0;
+ uint16 toPic = 0;
+ if (!londonTravelSitePic(_mystery, fromSite, fromPic) ||
+ !londonTravelSitePic(_mystery, toSite, toPic))
+ return false;
+ if (fromPic >= kLondonTravelMatrixDim || toPic >= kLondonTravelMatrixDim)
+ return false;
+
+ Common::File matrix;
+ if (!matrix.open(Common::Path("TRAVEL.BIN"))) {
+ warning("London travel: TRAVEL.BIN missing");
+ return false;
+ }
+
+ const uint32 matrixOff = (uint32)fromPic * kLondonTravelMatrixDim + toPic;
+ if (!matrix.seek(matrixOff)) {
+ warning("London travel: seek to matrix offset %u failed", matrixOff);
+ return false;
+ }
+
+ byte travelKind = 0;
+ if (matrix.read(&travelKind, 1) != 1) {
+ warning("London travel: short read at matrix offset %u", matrixOff);
+ return false;
+ }
+ if (travelKind == 0)
+ return false;
+ if (travelKind > 3) {
+ warning("London travel: invalid matrix value %u for pic %u -> %u",
+ travelKind, fromPic, toPic);
+ return false;
+ }
+
+ // EEM2 `_DoTravel @ 1717:06ed`: kind 3 forces partner suffix 0,
+ // so the shipped set is TRAVEL00/01/10/11/20.ANM.
+ const uint partnerSuffix = (travelKind == 3) ? 0 : _vm->getPartnerIndex();
+ const Common::String name = Common::String::format("TRAVEL%u%u.ANM",
+ (uint)travelKind - 1, partnerSuffix);
+
+ debugC(1, kDebugSite,
+ "London travel: site %u/pic %u -> site %u/pic %u, kind=%u, anim=%s",
+ fromSite, fromPic, toSite, toPic, travelKind, name.c_str());
+ _vm->startLondonTravelMusic(travelKind);
+ fadeCurrentPaletteToBlack();
+ _vm->playAnm(Common::Path(name), kLondonTravelFrameDelayMs,
+ /* holdLastFrame= */ false, /* fadeIn= */ true,
+ /* setSkipIntroOnEsc= */ false);
+ _vm->stopMusic();
+ return true;
+}
+
void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
if (!_mystery || !_mystery->isLoaded()) {
warning("SiteScreen::enter: no mystery loaded");
@@ -878,9 +960,16 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
const bool playArrival = _vm->shouldPlaySiteArrival(siteNum);
- // `_DoTravel @ 168d:02da` calls `_StartTravelMusic`.
- if (playArrival)
- _vm->startTravelMusic();
+ if (playArrival) {
+ if (_vm->isLondon()) {
+ // EEM2 `HandleLondonSiteLoop @ 1717:083a` calls `_DoTravel`
+ // before `_BuildBackground` for the destination site.
+ playLondonTravelAnimation(_mystery->_lastSite, siteNum);
+ } else {
+ // `_DoTravel @ 168d:02da` calls `_StartTravelMusic`.
+ _vm->startTravelMusic();
+ }
+ }
// `_BuildBackground` calls `GetPalette(sitenum + 1)` â sitenum is the
// global SITES.DBD index (per-mystery `sitepic` field).
@@ -930,7 +1019,7 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
renderAnimatedDrops(siteNum, g_system->getMillis());
const bool skippedArrival = enterSiteAnim();
_vm->markSiteArrivalPlayed(siteNum);
- if (!_vm->isFloppy()) {
+ if (!_vm->isFloppy() && !_vm->isLondon()) {
if (skippedArrival)
_vm->stopMusic();
else
diff --git a/engines/eem/site.h b/engines/eem/site.h
index 78513dd4d4c..c91f0cffeb8 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -118,6 +118,7 @@ private:
void initImpatienceCounter();
bool checkImpatienceCounter();
void notePartnerActivity();
+ bool playLondonTravelAnimation(uint fromSite, uint toSite);
/// Partner site-arrival sequence (when `_LastSite != _SiteNumber`).
/// Mirrors `_EnterSiteAnim @ 1000:9b21`: anim 6/14 (Jake/Jenny) skateboards
Commit: 279d74a5413c753078808263c59160fed3317fe6
https://github.com/scummvm/scummvm/commit/279d74a5413c753078808263c59160fed3317fe6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:25+02:00
Commit Message:
EEM: implemented edutaintment intro video of some locations in London
Changed paths:
engines/eem/eem.h
engines/eem/site.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 907dacb7d3a..710573ff2b9 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -517,6 +517,10 @@ public:
/// `_StopMIDI @ 20a2:0512`.
void stopMusic();
+ /// EEM2 `_DoApproach @ 1717:009b`: London-only place information
+ /// screen with a short VIDEOnn.A clip and text pages from Ann.BIN.
+ bool doLondonApproach(uint16 approachId);
+
/// `Engine::syncSoundSettings` override. Re-pulls `music_volume`
/// into the MIDI player's `_masterVolume`.
void syncSoundSettings() override;
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index c07ac1df532..5dba04a5f01 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -958,13 +958,19 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
debugC(1, kDebugSite, "Entering site %u (%u hotspots)",
siteNum, _mystery->hotspotCount(siteNum));
+ const byte *sd = _mystery->siteData(siteNum);
const bool playArrival = _vm->shouldPlaySiteArrival(siteNum);
if (playArrival) {
if (_vm->isLondon()) {
// EEM2 `HandleLondonSiteLoop @ 1717:083a` calls `_DoTravel`
// before `_BuildBackground` for the destination site.
- playLondonTravelAnimation(_mystery->_lastSite, siteNum);
+ bool showedApproach = false;
+ const uint16 approachId = sd ? READ_LE_UINT16(sd + 2) : 0xffff;
+ if (firstVisit && approachId != 0xffff)
+ showedApproach = _vm->doLondonApproach(approachId);
+ if (!showedApproach)
+ playLondonTravelAnimation(_mystery->_lastSite, siteNum);
} else {
// `_DoTravel @ 168d:02da` calls `_StartTravelMusic`.
_vm->startTravelMusic();
@@ -975,7 +981,6 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
// global SITES.DBD index (per-mystery `sitepic` field).
// Floppy (`_DoSiteLoop_Floppy @ 1652:03f4`): first u16 of site_data is
// an offset to a drops sub-struct whose byte 0 is the SITES.DBD picID.
- const byte *sd = _mystery->siteData(siteNum);
uint16 sitepic = 0;
if (sd) {
if (_vm->isFloppy()) {
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 048bc040fea..82e3ee518c3 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -19,6 +19,7 @@
*
*/
+#include "common/algorithm.h"
#include "common/debug.h"
#include "common/config-manager.h"
#include "common/events.h"
@@ -50,6 +51,33 @@ const GallerySlot kGallerySlots[5] = {
{ 191, 90 } // 4
};
+struct LondonApproachData {
+ uint16 videoId = 0;
+ uint16 videoX = 0;
+ uint16 videoY = 0;
+ Common::Rect textRect;
+ Common::Array<Common::String> pages;
+};
+
+struct LondonApproachButton {
+ int x1;
+ int y1;
+ int x2;
+ int y2;
+ int x;
+ int y;
+ uint16 picId;
+
+ Common::Rect rect() const { return Common::Rect(x1, y1, x2, y2); }
+};
+
+const LondonApproachButton kLondonApproachButtons[4] = {
+ { 10, 139, 35, 157, 11, 139, 0x361 }, // Done
+ { 10, 172, 35, 190, 11, 172, 0x362 }, // Play
+ { 287, 172, 307, 190, 287, 172, 0x35f }, // Next
+ { 287, 139, 307, 157, 287, 139, 0x360 }, // Previous
+};
+
byte mapVisitedMarkerColor(byte color) {
switch (color) {
case 0xf7:
@@ -87,6 +115,75 @@ void blitBigMapMarker(Graphics::ManagedSurface &dstSurface, const Picture &marke
}
}
+bool loadLondonApproachData(uint16 approachId, LondonApproachData &out) {
+ Common::File f;
+ const Common::String name = Common::String::format("A%u.BIN", approachId);
+ if (!f.open(Common::Path(name))) {
+ warning("London approach: cannot open %s", name.c_str());
+ return false;
+ }
+
+ const uint32 size = f.size();
+ if (size < 16) {
+ warning("London approach: %s is too short", name.c_str());
+ return false;
+ }
+
+ Common::Array<byte> data;
+ data.resize(size);
+ if (f.read(data.data(), size) != size) {
+ warning("London approach: short read on %s", name.c_str());
+ return false;
+ }
+
+ out.videoId = READ_LE_UINT16(data.data() + 0);
+ out.videoX = READ_LE_UINT16(data.data() + 2);
+ out.videoY = READ_LE_UINT16(data.data() + 4);
+ const int16 x1 = (int16)READ_LE_UINT16(data.data() + 6);
+ const int16 y1 = (int16)READ_LE_UINT16(data.data() + 8);
+ const int16 x2 = (int16)READ_LE_UINT16(data.data() + 10);
+ const int16 y2 = (int16)READ_LE_UINT16(data.data() + 12);
+ out.textRect = Common::Rect(x1, y1, x2, y2);
+
+ const uint16 pageCount = READ_LE_UINT16(data.data() + 14);
+ out.pages.clear();
+ uint32 pos = 16;
+ for (uint16 i = 0; i < pageCount && pos < size; i++) {
+ const uint32 start = pos;
+ while (pos < size && data[pos] != 0)
+ pos++;
+ out.pages.push_back(Common::String((const char *)data.data() + start,
+ pos - start));
+ if (pos < size)
+ pos++;
+ }
+ return out.videoId != 0 && !out.pages.empty();
+}
+
+bool decodeLondonApproachFirstFrame(uint16 videoId,
+ Graphics::ManagedSurface &base,
+ byte *palette) {
+ const Common::String name = Common::String::format("VIDEO%02u.A", videoId);
+ ANMDecoder anm;
+ if (!anm.open(Common::Path(name))) {
+ warning("London approach: cannot open %s", name.c_str());
+ return false;
+ }
+
+ anm.getPalette8(palette);
+ const byte *frame = anm.nextFrame();
+ if (!frame) {
+ warning("London approach: %s has no first frame", name.c_str());
+ return false;
+ }
+
+ base.clear();
+ const int w = MIN<int>(anm.width(), kScreenWidth);
+ const int h = MIN<int>(anm.height(), kScreenHeight);
+ base.copyRectToSurface(frame, anm.width(), 0, 0, w, h);
+ return true;
+}
+
// Setup-screen highlight rects (referenced by both `doSetup` and the
// helper `EEMEngine::setupDrawScreen`). `_SetupHighlights @ 29be:1320`.
constexpr Common::Rect kSetupKid1Rect (Common::Point( 99, 44), 49, 8);
@@ -2979,7 +3076,8 @@ void EEMEngine::doBigMap() {
const Common::Rect kBigMapWindow(0, 0, 247, 192);
const Common::Rect kSetupBtnRect = isFloppy()
? Common::Rect(251, 3, 315, 42)
- : Common::Rect(252, 4, 315, 42);
+ : (isLondon() ? Common::Rect(252, 1, 315, 42)
+ : Common::Rect(252, 4, 315, 42));
bool wantZoom = false;
int zoomX = 0;
@@ -3022,8 +3120,13 @@ void EEMEngine::doBigMap() {
if (now - mapLastTick >= 100) {
mapLastTick = now;
drawBigMapOverview(now - mapStartTick);
- cyclePaletteRange(0xf7, 0xfa);
- cyclePaletteRange(0xfb, 0xfe);
+ if (isLondon()) {
+ cyclePaletteRange(0xf4, 0xf9);
+ cyclePaletteRange(0xfa, 0xff);
+ } else {
+ cyclePaletteRange(0xf7, 0xfa);
+ cyclePaletteRange(0xfb, 0xfe);
+ }
}
g_system->updateScreen();
g_system->delayMillis(10);
@@ -3068,12 +3171,17 @@ void EEMEngine::doBigMap() {
const Common::Rect kArrowYDown(237, 163, 247, 172);
const Common::Rect kArrowXLeft(2, 175, 12, 185);
const Common::Rect kArrowXRight(224, 175, 234, 185);
- const Common::Rect kXSlider(15, 175, 221, 185);
- const Common::Rect kYSlider(237, 14, 247, 160);
+ const Common::Rect kXSlider = isLondon()
+ ? Common::Rect(15, 176, 220, 184)
+ : Common::Rect(15, 175, 221, 185);
+ const Common::Rect kYSlider = isLondon()
+ ? Common::Rect(238, 16, 246, 158)
+ : Common::Rect(237, 14, 247, 160);
const Common::Rect kDetailSetupBtn = isFloppy()
? Common::Rect(251, 3, 315, 42)
- : Common::Rect(252, 4, 315, 42);
- const int kArrowStep = 16;
+ : (isLondon() ? Common::Rect(251, 3, 315, 42)
+ : Common::Rect(252, 4, 315, 42));
+ const int kArrowStep = isLondon() ? 8 : 16;
const int kSliderRange = mapW - kMapWinW;
const int kSliderRangeY = mapH - kMapWinH;
const Common::Point detailMouse =
@@ -3085,6 +3193,7 @@ void EEMEngine::doBigMap() {
while (!shouldQuit() && !returnToOverview) {
Common::Event ev;
bool dirty = false;
+ bool frameTick = false;
while (g_system->getEventManager()->pollEvent(ev)) {
if (ev.type == Common::EVENT_QUIT ||
ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
@@ -3097,7 +3206,7 @@ void EEMEngine::doBigMap() {
dirty = true;
continue;
}
- const int kStep = 16;
+ const int kStep = isLondon() ? 8 : 16;
if (ev.kbd.keycode == Common::KEYCODE_LEFT) {
scrollX = MAX<int>(0, scrollX - kStep);
dirty = true;
@@ -3167,6 +3276,11 @@ void EEMEngine::doBigMap() {
ev.mouse.y < kMapWinY + kMapWinH) {
// Per-site bbox from `_StampButtons` (SmallMap +8/+0xa).
const bool fmap = _mystery.isLoaded() && isFloppy();
+ struct DetailMapHit {
+ uint site;
+ Common::Rect rect;
+ };
+ Common::Array<DetailMapHit> hits;
for (uint i = 0; i < _mystery.numSites(); i++) {
if (!_mystery._onSites[i] &&
i != _mystery._siteNumber)
@@ -3197,10 +3311,23 @@ void EEMEngine::doBigMap() {
}
const int sx = (int)mx - scrollX + kMapWinX;
const int sy = (int)my - scrollY + kMapWinY;
- if (ev.mouse.x >= sx && ev.mouse.x < sx + bw &&
- ev.mouse.y >= sy && ev.mouse.y < sy + bh) {
+ const Common::Rect r(sx, sy, sx + bw, sy + bh);
+ if (r.intersects(Common::Rect(kMapWinX, kMapWinY,
+ kMapWinX + kMapWinW, kMapWinY + kMapWinH))) {
+ DetailMapHit hit = { i, r };
+ hits.push_back(hit);
+ }
+ }
+ Common::sort(hits.begin(), hits.end(),
+ [](const DetailMapHit &a, const DetailMapHit &b) {
+ if (a.rect.top != b.rect.top)
+ return a.rect.top < b.rect.top;
+ return a.rect.left < b.rect.left;
+ });
+ for (uint i = 0; i < hits.size(); i++) {
+ if (hits[i].rect.contains(ev.mouse.x, ev.mouse.y)) {
_mystery._lastSite = _mystery._siteNumber;
- _mystery._siteNumber = (uint16)i;
+ _mystery._siteNumber = (uint16)hits[i].site;
setInteractiveMouseCursor(false);
return;
}
@@ -3216,10 +3343,15 @@ void EEMEngine::doBigMap() {
if (now - detailLastTick >= 100) {
detailLastTick = now;
dirty = true;
+ frameTick = true;
}
if (dirty)
drawBigMapDetail(scrollX, scrollY, mapPixels, mapW, mapH,
now - detailStartTick);
+ if (frameTick && isLondon()) {
+ cyclePaletteRange(0xee, 0xf2);
+ cyclePaletteRange(0xea, 0xed);
+ }
g_system->updateScreen();
g_system->delayMillis(10);
}
@@ -3228,6 +3360,215 @@ void EEMEngine::doBigMap() {
}
}
+bool EEMEngine::doLondonApproach(uint16 approachId) {
+ if (!isLondon())
+ return false;
+
+ LondonApproachData data;
+ if (!loadLondonApproachData(approachId, data))
+ return false;
+
+ Graphics::ManagedSurface base(kScreenWidth, kScreenHeight,
+ Graphics::PixelFormat::createFormatCLUT8());
+ byte palette[768] = {};
+ const bool haveVideo =
+ decodeLondonApproachFirstFrame(data.videoId, base, palette);
+ if (!haveVideo)
+ base.clear();
+
+ Picture buttonPics[ARRAYSIZE(kLondonApproachButtons)];
+ bool haveButtons[ARRAYSIZE(kLondonApproachButtons)] = {};
+ for (uint i = 0; i < ARRAYSIZE(kLondonApproachButtons); i++)
+ haveButtons[i] = _picsArchive.getPicture(
+ kLondonApproachButtons[i].picId, buttonPics[i]);
+
+ auto drawButtonFallback = [&](Graphics::ManagedSurface &dst, uint idx) {
+ static const char *const kLabels[4] = { "OK", "PLAY", ">", "<" };
+ const Common::Rect r = kLondonApproachButtons[idx].rect();
+ dst.fillRect(r, 0);
+ _font.drawString(&dst, kLabels[idx], r.left + 1, r.top + 5,
+ r.width(), 0x0f);
+ };
+
+ auto drawScreen = [&](uint page) {
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.simpleBlitFrom(base);
+
+ const Common::Rect textRect =
+ data.textRect.findIntersectingRect(Common::Rect(kScreenWidth, kScreenHeight));
+ if (!textRect.isEmpty()) {
+ scratch.fillRect(textRect, 0);
+ if (page < data.pages.size()) {
+ Common::Array<Common::String> wrapped;
+ _font.wordWrapText(data.pages[page], MAX<int>(8, textRect.width()),
+ wrapped);
+ const int lineH = _font.getFontHeight();
+ const int maxLines = MAX<int>(1, textRect.height() / lineH);
+ for (uint i = 0; i < wrapped.size() && (int)i < maxLines; i++) {
+ _font.drawString(&scratch, wrapped[i], textRect.left,
+ textRect.top + (int)i * lineH,
+ textRect.width(), 0x0f);
+ }
+ }
+ }
+
+ for (uint i = 0; i < ARRAYSIZE(kLondonApproachButtons); i++) {
+ const LondonApproachButton &b = kLondonApproachButtons[i];
+ if (haveButtons[i]) {
+ scratch.transBlitFrom(buttonPics[i].surface,
+ Common::Point(b.x, b.y),
+ (uint32)(byte)(buttonPics[i].flags >> 8));
+ } else {
+ drawButtonFallback(scratch, i);
+ }
+ }
+
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, kScreenWidth, kScreenHeight);
+ g_system->updateScreen();
+ };
+
+ auto playVideo = [&]() {
+ const Common::String name =
+ Common::String::format("VIDEO%02u.A", data.videoId);
+ ANMDecoder anm;
+ if (!anm.open(Common::Path(name))) {
+ warning("London approach: cannot open %s", name.c_str());
+ return;
+ }
+
+ // Frame 0 is already the static background. Replay frames 1..end
+ // into the upper video area, leaving text/buttons untouched.
+ (void)anm.nextFrame();
+ const int copyW = MIN<int>(anm.width(), kScreenWidth - (int)data.videoX);
+ const int copyH = MIN<int>(0x82, MIN<int>(anm.height(),
+ kScreenHeight - (int)data.videoY));
+ if (copyW <= 0 || copyH <= 0)
+ return;
+
+ while (!shouldQuit()) {
+ const byte *frame = anm.nextFrame();
+ if (!frame)
+ break;
+ g_system->copyRectToScreen(
+ frame + data.videoY * anm.width() + data.videoX,
+ anm.width(), data.videoX, data.videoY, copyW, copyH);
+ g_system->updateScreen();
+
+ const uint32 start = g_system->getMillis();
+ bool skip = false;
+ while (g_system->getMillis() - start < 120 && !skip) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ return;
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN ||
+ ev.type == Common::EVENT_KEYDOWN) {
+ skip = true;
+ break;
+ }
+ }
+ g_system->delayMillis(5);
+ }
+ if (skip)
+ break;
+ }
+ };
+
+ fadeCurrentPaletteToBlack();
+ CursorMan.showMouse(true);
+ setSiteHotspotCursorId(6);
+ if (_music && _voiceOn)
+ _music->playMus(0x27, /* loop= */ false);
+
+ uint page = 0;
+ drawScreen(page);
+ if (haveVideo)
+ fadePaletteFromBlack(palette);
+ else
+ setSitePalette(0x3b);
+ bool done = false;
+ while (!shouldQuit() && !done) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ done = true;
+ break;
+ }
+ if (ev.type == Common::EVENT_MOUSEMOVE) {
+ bool overButton = false;
+ for (uint i = 0; i < ARRAYSIZE(kLondonApproachButtons); i++) {
+ if (kLondonApproachButtons[i].rect().contains(ev.mouse.x,
+ ev.mouse.y)) {
+ overButton = true;
+ break;
+ }
+ }
+ setInteractiveMouseCursor(overButton);
+ }
+ if (ev.type == Common::EVENT_KEYDOWN) {
+ if (ev.kbd.keycode == Common::KEYCODE_ESCAPE ||
+ ev.kbd.keycode == Common::KEYCODE_RETURN ||
+ ev.kbd.keycode == Common::KEYCODE_KP_ENTER) {
+ done = true;
+ break;
+ }
+ if (ev.kbd.keycode == Common::KEYCODE_SPACE) {
+ playVideo();
+ drawScreen(page);
+ } else if (ev.kbd.keycode == Common::KEYCODE_RIGHT ||
+ ev.kbd.keycode == Common::KEYCODE_DOWN) {
+ if (page + 1 < data.pages.size()) {
+ page++;
+ drawScreen(page);
+ }
+ } else if (ev.kbd.keycode == Common::KEYCODE_LEFT ||
+ ev.kbd.keycode == Common::KEYCODE_UP) {
+ if (page > 0) {
+ page--;
+ drawScreen(page);
+ }
+ }
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ if (kLondonApproachButtons[0].rect().contains(ev.mouse.x,
+ ev.mouse.y)) {
+ done = true;
+ break;
+ }
+ if (kLondonApproachButtons[1].rect().contains(ev.mouse.x,
+ ev.mouse.y)) {
+ playVideo();
+ drawScreen(page);
+ } else if (kLondonApproachButtons[2].rect().contains(ev.mouse.x,
+ ev.mouse.y)) {
+ if (page + 1 < data.pages.size()) {
+ page++;
+ drawScreen(page);
+ }
+ } else if (kLondonApproachButtons[3].rect().contains(ev.mouse.x,
+ ev.mouse.y)) {
+ if (page > 0) {
+ page--;
+ drawScreen(page);
+ }
+ }
+ }
+ }
+ g_system->updateScreen();
+ g_system->delayMillis(10);
+ }
+
+ setInteractiveMouseCursor(false);
+ setSiteHotspotCursorId(0);
+ stopMusic();
+ return true;
+}
+
void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
// PIC 0x42 + per-site Done/Crime/Site marker (`_DrawBigMapButtons @
// 20fe:0877`) + partner idle at (0xfd, 0x50). Idle ANI: Jake=0x14,
Commit: 6ddfb2227b29c4295c5066c48fcd203902331572
https://github.com/scummvm/scummvm/commit/6ddfb2227b29c4295c5066c48fcd203902331572
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:25+02:00
Commit Message:
EEM: fix big map markers in London
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 82e3ee518c3..63bd909451f 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -3581,13 +3581,14 @@ void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
if (_picsArchive.getPicture(0x42, frame))
scratch.simpleBlitFrom(frame.surface);
- // Marker PICs from `_main @ 1a35:0f59`:
- // _DoneMarker = 0x20d, _SiteMarker = 0xc5, _CrimeMarker = 0xc6.
- // 0x20d is CD-only (floppy PICS.DBX has 524 entries).
+ // Marker PICs from `_main`:
+ // EEM1 CD @ 1a35:0f59: Done=0x20d, Site=0xc5, Crime=0xc6.
+ // EEM2 CD @ 1abf:11a6: Done=0x006, Site=0xc5, Crime=0xc6.
+ // In EEM2, 0x20d is a normal scene/character frame, not a map marker.
Picture done;
Picture normal;
Picture crimeM;
- const bool haveDone = _picsArchive.getPicture(0x20d, done);
+ const bool haveDone = _picsArchive.getPicture(isLondon() ? 0x006 : 0x20d, done);
const bool haveNormal = _picsArchive.getPicture(0xc5, normal);
const bool haveCrime = _picsArchive.getPicture(0xc6, crimeM);
Commit: a35a1dc2d51307a386a15158b9145354c21f3ac8
https://github.com/scummvm/scummvm/commit/a35a1dc2d51307a386a15158b9145354c21f3ac8
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:25+02:00
Commit Message:
EEM: implement player creation/loading in London
Changed paths:
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/ui.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 36fe8d4fc8b..80013325a49 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -74,6 +74,53 @@ const byte kSaveBodyVer = 1;
// option or changing save format. Set false before release.
const bool kDebugPopulateScrapbook1AtStartup = false;
+Common::String makeLondonProfileDisplayName(const Common::String &first,
+ const Common::String &last) {
+ Common::String cleanFirst = first;
+ Common::String cleanLast = last;
+ cleanFirst.trim();
+ cleanLast.trim();
+ if (cleanLast.empty())
+ return cleanFirst;
+ if (cleanFirst.empty())
+ return cleanLast;
+ return cleanFirst + " " + cleanLast;
+}
+
+Common::String makeLondonProfileKey(const Common::String &first,
+ const Common::String &last) {
+ // EEM2 `_GenerateFilename @ 1cd3:02fe`: copy the first 7 bytes of
+ // FirstName, pad to 8 from LastName, then replace spaces/dots with '_'.
+ Common::String cleanFirst = first;
+ Common::String cleanLast = last;
+ cleanFirst.trim();
+ cleanLast.trim();
+ Common::String key;
+ for (uint i = 0; i < cleanFirst.size() && key.size() < 7; i++)
+ key += cleanFirst[i];
+ for (uint i = 0; i < cleanLast.size() && key.size() < 8; i++)
+ key += cleanLast[i];
+ for (uint i = 0; i < key.size(); i++) {
+ if (key[i] == ' ' || key[i] == '.')
+ key.setChar('_', i);
+ }
+ key.toUppercase();
+ return key;
+}
+
+Common::String londonProfileKeyFromDisplayName(const Common::String &name) {
+ Common::String clean = name;
+ clean.trim();
+ const size_t split = clean.findFirstOf(' ');
+ if (split == Common::String::npos)
+ return makeLondonProfileKey(clean, Common::String());
+
+ Common::String first = clean.substr(0, split);
+ Common::String last = clean.substr(split + 1);
+ last.trim();
+ return makeLondonProfileKey(first, last);
+}
+
void fadeCurrentPaletteToBlack(uint delayMs) {
byte start[kPalSize];
byte stepPal[kPalSize];
@@ -342,26 +389,22 @@ Common::Error EEMEngine::run() {
if (resumed)
goto screenLoop;
- // EEM2 ("Eagle Eye Mysteries in London"): opening sequence + character
- // creation, then start the training case (mystery 0 â what a new
- // detective plays first) and reuse the engine's gameplay screen loop.
- // EEM2's mystery data format matches EEM1-CD, so the shared Mystery
- // parser + InitClues (case intro) / BigMap / Site handlers apply.
+ // EEM2 ("Eagle Eye Mysteries in London"): opening sequence, then screen
+ // 8 profile selection. A freshly-created detective starts the training
+ // case (M0); an existing profile resumes its saved menu/map state.
if (isLondon()) {
runLondonStartup();
if (!shouldQuit()) {
CursorMan.showMouse(true);
- if (_mystery.load(0, &_rng)) {
- resetSiteArrivalState();
- if (_audio)
- _audio->initMysterySounds(0);
- debugC(1, kDebugMystery,
- "London: training mystery 0 loaded â %u sites, %u suspects",
- _mystery.numSites(), _mystery.numSuspects());
- _nextScreen = kScreenInitClues;
+ applyStartupTestOverrides();
+ applySkipRepeatedCasesOption();
+ if (_mystery.isLoaded()) {
+ _nextScreen = kScreenMap;
+ } else if (_profileCreatedThisSession) {
+ _nextScreen = startLondonTrainingMystery()
+ ? kScreenInitClues : kScreenInvalid;
} else {
- warning("London: training mystery 0 (M0.BIN) failed to load");
- _nextScreen = kScreenInvalid;
+ _nextScreen = kScreenAction;
}
}
goto screenLoop;
@@ -605,11 +648,17 @@ screenLoop:
applyStartupTestOverrides();
if (!shouldQuit())
applySkipRepeatedCasesOption();
- if (!shouldQuit())
+ if (!shouldQuit() && !isLondon())
doChoosePartner();
- if (!shouldQuit())
- _nextScreen = _mystery.isLoaded() ? kScreenMap
- : kScreenAction;
+ if (!shouldQuit()) {
+ if (_mystery.isLoaded())
+ _nextScreen = kScreenMap;
+ else if (isLondon() && _profileCreatedThisSession)
+ _nextScreen = startLondonTrainingMystery()
+ ? kScreenInitClues : kScreenInvalid;
+ else
+ _nextScreen = kScreenAction;
+ }
break;
case kScreenAccuse:
@@ -1111,6 +1160,21 @@ void EEMEngine::showLondonLogo(uint picId, uint palId, uint holdMs) {
fadeCurrentPaletteToBlack();
}
+bool EEMEngine::startLondonTrainingMystery() {
+ if (_mystery.load(0, &_rng)) {
+ resetSiteArrivalState();
+ if (_audio)
+ _audio->initMysterySounds(0);
+ debugC(1, kDebugMystery,
+ "London: training mystery 0 loaded â %u sites, %u suspects",
+ _mystery.numSites(), _mystery.numSuspects());
+ return true;
+ }
+
+ warning("London: training mystery 0 (M0.BIN) failed to load");
+ return false;
+}
+
void EEMEngine::runLondonStartup() {
// Full opening sequence â EEM2 `_DoOpeningAnims` @ 2721:08e6:
// _ShowEAKids @ 2721:05e3 â PIC 0x54, palette 0x3c
@@ -1164,14 +1228,14 @@ void EEMEngine::runLondonStartup() {
_music->stop();
_skipIntro = false;
- // Character creation (name + Jake/Jenny). The caller then loads the
- // training case (mystery 0) and enters the shared gameplay screen loop
- // (case intro -> map -> site); the case-selection menu is reachable
- // later via the action screen.
- if (!shouldQuit())
- showLondonCharSelect();
+ // Screen 8 profile selection. If no profile exists or the player chooses
+ // the bottom "new player" sentinel, this opens London `_NewPlayer`.
+ if (!shouldQuit()) {
+ CursorMan.showMouse(true);
+ doProfilePicker();
+ }
- debugC(1, kDebugGeneral, "EEM2 (London): intro + character creation done");
+ debugC(1, kDebugGeneral, "EEM2 (London): intro + profile selection done");
}
void EEMEngine::showLondonCharSelect() {
@@ -1202,12 +1266,15 @@ void EEMEngine::showLondonCharSelect() {
const bool havePal = getSitePalette(0, pal);
CursorMan.showMouse(false);
+ _profileCreatedThisSession = false;
enum { kFieldFirst = 0, kFieldLast = 1, kFieldPartner = 2 };
int field = kFieldFirst;
Common::String first, last;
uint8 partner = kPartnerJake;
bool blink = true, fadedIn = false, done = false, needRedraw = true;
+ bool partnerReady = false;
+ bool partnerCanConfirm = true;
uint32 blinkMs = g_system->getMillis();
while (!done && !shouldQuit()) {
@@ -1243,6 +1310,8 @@ void EEMEngine::showLondonCharSelect() {
}
g_system->updateScreen();
needRedraw = false;
+ if (field == kFieldPartner)
+ partnerReady = true;
}
Common::Event ev;
@@ -1250,6 +1319,20 @@ void EEMEngine::showLondonCharSelect() {
if (ev.type == Common::EVENT_QUIT ||
ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
return;
+ if (field == kFieldPartner && ev.type == Common::EVENT_LBUTTONDOWN) {
+ if (!partnerReady)
+ continue;
+ partner = (ev.mouse.x < 160) ? kPartnerJake : kPartnerJenny;
+ done = true;
+ needRedraw = true;
+ break;
+ }
+ if (field == kFieldPartner && ev.type == Common::EVENT_KEYUP &&
+ (ev.kbd.keycode == Common::KEYCODE_RETURN ||
+ ev.kbd.keycode == Common::KEYCODE_KP_ENTER)) {
+ partnerCanConfirm = true;
+ continue;
+ }
if (ev.type != Common::EVENT_KEYDOWN)
continue;
const Common::KeyCode k = ev.kbd.keycode;
@@ -1258,25 +1341,39 @@ void EEMEngine::showLondonCharSelect() {
if (field == kFieldFirst || field == kFieldLast) {
Common::String &buf = (field == kFieldFirst) ? first : last;
const uint cap = (field == kFieldFirst) ? kMaxFirst : kMaxLast;
- if ((enter || k == Common::KEYCODE_TAB) &&
- (field == kFieldLast || !first.empty()))
- field++; // advance to last name, then to partner pick
- else if (k == Common::KEYCODE_BACKSPACE && !buf.empty())
+ if (enter || k == Common::KEYCODE_TAB) {
+ Common::String clean = buf;
+ clean.trim();
+ if (!clean.empty()) {
+ field++; // advance to last name, then to partner pick
+ if (field == kFieldPartner) {
+ partnerReady = false;
+ partnerCanConfirm = false;
+ }
+ }
+ } else if (k == Common::KEYCODE_BACKSPACE && !buf.empty()) {
buf.deleteLastChar();
- else if (ev.kbd.ascii >= ' ' && ev.kbd.ascii < 127 &&
- buf.size() < cap)
+ } else if (ev.kbd.ascii >= ' ' && ev.kbd.ascii < 127 &&
+ buf.size() < cap) {
buf += (char)ev.kbd.ascii;
+ }
} else { // partner pick
- if (k == Common::KEYCODE_LEFT)
+ if (k == Common::KEYCODE_LEFT) {
partner = kPartnerJake;
- else if (k == Common::KEYCODE_RIGHT)
+ partnerCanConfirm = true;
+ } else if (k == Common::KEYCODE_RIGHT) {
partner = kPartnerJenny;
- else if (k == Common::KEYCODE_BACKSPACE)
+ partnerCanConfirm = true;
+ } else if (k == Common::KEYCODE_BACKSPACE) {
field = kFieldLast; // back to editing
- else if (enter)
+ partnerCanConfirm = true;
+ } else if (enter && partnerReady && partnerCanConfirm) {
done = true;
+ }
}
needRedraw = true;
+ if (field == kFieldPartner && !partnerReady)
+ break;
}
const uint32 now = g_system->getMillis();
@@ -1290,18 +1387,54 @@ void EEMEngine::showLondonCharSelect() {
if (shouldQuit())
return;
- // Commit the profile so the reused case-selection menu has valid state
- // (player name in clue text, partner greeter ANI 0x15/0x16, Junior
- // chain stage -> BOOK1.NME, nothing solved yet).
- if (first.empty())
- first = "Detective";
- _playerName = last.empty() ? first : (first + " " + last);
+ Common::Event staleEvent;
+ while (g_system->getEventManager()->pollEvent(staleEvent)) {
+ if (staleEvent.type == Common::EVENT_QUIT ||
+ staleEvent.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return;
+ }
+
+ const Common::String displayName =
+ makeLondonProfileDisplayName(first, last);
+ const Common::String profileKey = makeLondonProfileKey(first, last);
+
+ for (const SaveStateDescriptor &s : listProfiles()) {
+ if (londonProfileKeyFromDisplayName(s.getDescription()) != profileKey)
+ continue;
+
+ const Common::Error err = loadGameState(s.getSaveSlot());
+ if (err.getCode() == Common::kNoError) {
+ _profileCreatedThisSession = false;
+ debugC(1, kDebugGeneral,
+ "London: existing player key=%s loaded from slot %d (%s)",
+ profileKey.c_str(), s.getSaveSlot(),
+ s.getDescription().c_str());
+ return;
+ }
+ warning("London: failed to load existing profile '%s' at slot %d",
+ s.getDescription().c_str(), s.getSaveSlot());
+ }
+
+ // New profile. EEM2 `_NewPlayer @ 1cd3:0f27` initializes the player
+ // record only when `_LoadPlayerRecord` failed, then immediately saves it.
+ _playerName = displayName.empty() ? "Detective" : displayName;
_partner = partner;
_chainStage = 1;
+ _voiceOn = true;
+ if (_audio)
+ _audio->setVoiceEnabled(_voiceOn);
for (uint i = 0; i < sizeof(_mysteriesSolved); i++)
_mysteriesSolved[i] = 0;
- debugC(1, kDebugGeneral, "London: player='%s' partner=%s",
- _playerName.c_str(), partner == kPartnerJake ? "Jake" : "Jenny");
+ _mystery.clear();
+ resetSiteArrivalState();
+ _profileCreatedThisSession = true;
+ const Common::Error err = saveProfile(_playerName);
+ if (err.getCode() != Common::kNoError)
+ warning("London: failed to save new profile '%s': %s",
+ _playerName.c_str(), err.getDesc().c_str());
+ debugC(1, kDebugGeneral, "London: new player='%s' key=%s partner=%s",
+ _playerName.c_str(), profileKey.c_str(),
+ partner == kPartnerJake ? "Jake" : "Jenny");
}
void EEMEngine::showFloppyStormLogo() {
@@ -1565,11 +1698,15 @@ Common::Error EEMEngine::saveProfile(const Common::String &name) {
return Common::kCreatingFileFailed;
const SaveStateList saves = listProfiles();
+ const Common::String londonKey =
+ isLondon() ? londonProfileKeyFromDisplayName(name) : Common::String();
// Overwrite on matching description.
int slot = -1;
for (auto &s : saves) {
- if (s.getDescription() == name) {
+ if (s.getDescription() == name ||
+ (isLondon() &&
+ londonProfileKeyFromDisplayName(s.getDescription()) == londonKey)) {
slot = s.getSaveSlot();
break;
}
@@ -1606,8 +1743,12 @@ bool EEMEngine::loadProfile(const Common::String &name) {
return false;
const SaveStateList saves = listProfiles();
+ const Common::String londonKey =
+ isLondon() ? londonProfileKeyFromDisplayName(name) : Common::String();
for (auto &s : saves) {
- if (s.getDescription() == name) {
+ if (s.getDescription() == name ||
+ (isLondon() &&
+ londonProfileKeyFromDisplayName(s.getDescription()) == londonKey)) {
const Common::Error err = loadGameState(s.getSaveSlot());
if (err.getCode() == Common::kNoError) {
debugC(1, kDebugGeneral, "loadProfile(%s) <- slot %d",
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 710573ff2b9..786c41a089c 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -425,8 +425,10 @@ private:
// --- EEM2 ("Eagle Eye Mysteries in London") â reimplementation in progress ---
/// Opening sequence + character creation â EEM2's `_DoOpeningAnims`
- /// @ 2721:08e6 then `_NewPlayer` @ 1cd3:0f27.
+ /// @ 2721:08e6 then screen 8 profile selection.
void runLondonStartup();
+ /// Start London mystery 0 for a freshly-created detective.
+ bool startLondonTrainingMystery();
/// Blit a full-screen still PIC and fade it in / hold / out using the
/// given SITEPALS. palette index.
void showLondonLogo(uint picId, uint palId, uint holdMs);
@@ -554,6 +556,10 @@ private:
/// partner speech, intro VO).
bool _voiceOn = true;
+ /// Set by the profile/new-player screens. London uses it to decide whether
+ /// to start the training mystery or resume the loaded profile's menu/state.
+ bool _profileCreatedThisSession = false;
+
Common::RandomSource _rng;
DBDArchive _picsArchive; ///< PICS.DBD/.DBX
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 63bd909451f..d2be151d480 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -836,18 +836,25 @@ void EEMEngine::doProfilePicker() {
// `screen8_handler @ 1c33:1012` â walks *.PLR (max 25), reads 12-byte
// player-name field, hands list to `_DoChoose`. No profiles or
// 0xfffe/0xffff sentinel enters `_NewPlayer`.
+ _profileCreatedThisSession = false;
// `screen8_handler` does `_FadeOut(); _GetPalette(0); _GetBackground(0x104)`.
setSitePalette(0);
const SaveStateList saves = listProfiles();
if (saves.empty()) {
- doNewPlayer();
+ if (isLondon())
+ showLondonCharSelect();
+ else
+ doNewPlayer();
return;
}
if (!_font.isLoaded()) {
- doNewPlayer();
+ if (isLondon())
+ showLondonCharSelect();
+ else
+ doNewPlayer();
return;
}
@@ -1011,22 +1018,31 @@ void EEMEngine::doProfilePicker() {
}
if (createNew) {
- doNewPlayer();
+ if (isLondon())
+ showLondonCharSelect();
+ else
+ doNewPlayer();
return;
}
// `_LoadPlayerRecord @ 1c33:1281`.
const ProfilePickerEntry &e = entries[sel];
- if (!loadProfile(e.label)) {
+ if (loadProfile(e.label)) {
+ _profileCreatedThisSession = false;
+ } else {
warning("doProfilePicker: failed to load profile '%s' at slot %d",
e.label.c_str(), e.slot);
- doNewPlayer();
+ if (isLondon())
+ showLondonCharSelect();
+ else
+ doNewPlayer();
}
}
void EEMEngine::doNewPlayer() {
// `_NewPlayer @ 1c33:0dda` â BG 0x104 + peek pic 0x107, prompt for
// up to 12 chars.
+ _profileCreatedThisSession = false;
if (!_font.isLoaded()) {
_playerName = "Detective";
return;
@@ -1070,7 +1086,9 @@ void EEMEngine::doNewPlayer() {
name = "Detective";
// `_NewPlayer @ 1c33:0fa0+`: `_LoadPlayerRecord` â if
// missing, zero state and `_SavePlayerRecord`.
- if (!loadProfile(name)) {
+ if (loadProfile(name)) {
+ _profileCreatedThisSession = false;
+ } else {
_playerName = name;
memset(_mysteriesSolved, 0, sizeof(_mysteriesSolved));
_mystery.clear();
@@ -1078,6 +1096,7 @@ void EEMEngine::doNewPlayer() {
// `_NewPlayer @ 1c33:0fa3`: DAT_2d5d_3f99 = 1 (Junior).
_chainStage = 1;
saveProfile(name);
+ _profileCreatedThisSession = true;
}
done = true;
break;
Commit: a252c9afa9f8f0bf3406151069e11602abdebb19
https://github.com/scummvm/scummvm/commit/a252c9afa9f8f0bf3406151069e11602abdebb19
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:26+02:00
Commit Message:
EEM: proper selection of partner in London
Changed paths:
engines/eem/clues.cpp
engines/eem/eem.cpp
engines/eem/eem.h
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index e75fdbda8ff..a6d7f734cd0 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -45,9 +45,10 @@ const uint kPicChooseBackground = 0x8c; // _GetBackground(0x8c)
const uint kAniJake = 8;
const uint kAniJenny = 9;
-// _DoHappiness @ 172b:27b5 â cursor X picks one of 4 rects @ 29be:030f.
+// EEM1 `_DoHappiness` â cursor X picks one of 4 rects @ 29be:030f.
// Past rect 3 = level 4. Constexpr (Point, w, h) form to avoid a global
-// constructor (-Wglobal-constructors).
+// constructor (-Wglobal-constructors). EEM2 uses a different 10-band table
+// at 2bca:035c, handled by `happinessLevel`.
constexpr Common::Rect kHappyZones[4] = {
Common::Rect(Common::Point( 0, 0), 70, 200), // far left â Jenny very happy, Jake neutral
Common::Rect(Common::Point( 70, 0), 56, 200), // Jenny's column
@@ -61,6 +62,13 @@ const int kJakeY = 0x62; // 98
const int kJennyX = 0x42; // 66
const int kJennyY = 0x60; // 96
+// EEM2 `_DoChoosePartner @ 1abf:0728`: Jake is on the left and Jennifer
+// on the right, unlike the first game's partner screen.
+const int kLondonJakeX = 0x05;
+const int kLondonJakeY = 0x3a;
+const int kLondonJennyX = 0xac;
+const int kLondonJennyY = 0x3a;
+
uint markClueBlockNotebookEntries(Mystery &mystery, const byte *clueBlock,
bool isLondon) {
if (!clueBlock)
@@ -107,7 +115,49 @@ const uint8 kJennySeqs[5][9] = {
{ 0,0,0,0,0,1,0,0,0 },
};
-uint happinessLevel(int x) {
+// EEM2 `FUN_17ee_26f6`: 10 rects @ 2bca:035c and 10 sequence rows for
+// Jennifer @ 2bca:03ac / Jake @ 2bca:0474. The portrait cells are 20-frame
+// ramps, so using the EEM1 5-level tables makes both characters smile in the
+// wrong places.
+const int kLondonHappyRightEdges[10] = {
+ 35, 70, 98, 126, 156, 182, 208, 235, 277, 320
+};
+
+const uint8 kLondonJennySeqs[10][9] = {
+ { 0, 0, 0, 0, 0, 0, 0, 1, 0 },
+ { 2, 2, 2, 2, 2, 2, 2, 3, 2 },
+ { 4, 4, 4, 4, 4, 4, 4, 5, 4 },
+ { 6, 6, 6, 6, 6, 6, 7, 6, 6 },
+ { 8, 8, 8, 8, 8, 8, 8, 8, 9 },
+ { 10, 10, 10, 10, 10, 10, 10, 11, 10 },
+ { 12, 12, 12, 12, 12, 12, 12, 13, 12 },
+ { 14, 14, 14, 14, 14, 14, 14, 15, 14 },
+ { 16, 16, 16, 16, 16, 16, 17, 16, 16 },
+ { 18, 18, 18, 18, 18, 18, 18, 18, 19 },
+};
+
+const uint8 kLondonJakeSeqs[10][9] = {
+ { 18, 19, 18, 18, 18, 18, 18, 18, 18 },
+ { 16, 16, 16, 17, 16, 16, 16, 16, 16 },
+ { 14, 14, 15, 14, 14, 14, 14, 14, 14 },
+ { 12, 12, 12, 12, 12, 12, 13, 12, 12 },
+ { 10, 10, 10, 10, 10, 11, 10, 10, 10 },
+ { 8, 9, 8, 8, 8, 8, 8, 8, 8 },
+ { 6, 6, 6, 7, 6, 6, 6, 6, 6 },
+ { 4, 4, 5, 4, 4, 4, 4, 4, 4 },
+ { 2, 2, 2, 2, 2, 2, 3, 2, 2 },
+ { 0, 0, 0, 0, 0, 1, 0, 0, 0 },
+};
+
+uint happinessLevel(int x, bool london) {
+ if (london) {
+ for (uint i = 0; i < ARRAYSIZE(kLondonHappyRightEdges); i++) {
+ if (x <= kLondonHappyRightEdges[i])
+ return i;
+ }
+ return ARRAYSIZE(kLondonHappyRightEdges) - 1;
+ }
+
for (uint i = 0; i < ARRAYSIZE(kHappyZones); i++) {
if (kHappyZones[i].contains(x, 100))
return i;
@@ -145,10 +195,13 @@ void blitRawToScreen(const Picture &p, int x, int y) {
x, y, w, h);
}
-// _DoChoosePartner @ 1a35:0756. The original places Jake + Jenny animations
-// on a backdrop and polls four click rectangles (two per character); we
-// approximate with a single split at x=160 (left=Jenny, right=Jake).
+// _DoChoosePartner @ 1a35:0756 / EEM2 @ 1abf:0728. The original places
+// Jake + Jenny animations on a backdrop and polls two broad character rects;
+// we approximate those with a single split at x=160. EEM1 has Jenny left /
+// Jake right; EEM2 has Jake left / Jennifer right.
void EEMEngine::doChoosePartner() {
+ _partner = kPartnerJake;
+
Picture background;
if (!_picsArchive.getPicture(kPicChooseBackground, background)) {
warning("ChoosePartner background (%u) load failed", kPicChooseBackground);
@@ -166,24 +219,31 @@ void EEMEngine::doChoosePartner() {
return;
}
- setAnmPalette(Common::Path("TITLE.ANM"));
+ setSitePalette(0);
+ CursorMan.showMouse(true);
// _DoChoosePartner opens with _SetMousePos(0xa0, 0x96).
+ const int jakeX = isLondon() ? kLondonJakeX : kJakeX;
+ const int jakeY = isLondon() ? kLondonJakeY : kJakeY;
+ const int jennyX = isLondon() ? kLondonJennyX : kJennyX;
+ const int jennyY = isLondon() ? kLondonJennyY : kJennyY;
+ const uint8 (*jakeSeqs)[9] = isLondon() ? kLondonJakeSeqs : kJakeSeqs;
+ const uint8 (*jennySeqs)[9] = isLondon() ? kLondonJennySeqs : kJennySeqs;
int curMouseX = 0xa0;
- uint level = happinessLevel(curMouseX);
+ uint level = happinessLevel(curMouseX, isLondon());
uint seqIdx = 0;
blitAt(background, 0, 0);
- blitAt(jennyAnim[kJennySeqs[level][seqIdx % 9] % jennyAnim.size()],
- kJennyX, kJennyY);
- blitAt(jakeAnim [kJakeSeqs [level][seqIdx % 9] % jakeAnim.size()],
- kJakeX, kJakeY);
+ blitAt(jennyAnim[jennySeqs[level][seqIdx % 9] % jennyAnim.size()],
+ jennyX, jennyY);
+ blitAt(jakeAnim [jakeSeqs [level][seqIdx % 9] % jakeAnim.size()],
+ jakeX, jakeY);
g_system->updateScreen();
debugC(1, kDebugGeneral, "ChoosePartner: %u Jake frames at (%d,%d), "
"%u Jenny frames at (%d,%d)",
- (uint)jakeAnim.size(), kJakeX, kJakeY,
- (uint)jennyAnim.size(), kJennyX, kJennyY);
+ (uint)jakeAnim.size(), jakeX, jakeY,
+ (uint)jennyAnim.size(), jennyX, jennyY);
uint32 lastTick = g_system->getMillis();
while (!shouldQuit()) {
@@ -195,10 +255,10 @@ void EEMEngine::doChoosePartner() {
lastTick = g_system->getMillis();
seqIdx = (seqIdx + 1) % 9;
blitAt(background, 0, 0);
- blitAt(jennyAnim[kJennySeqs[level][seqIdx % 9] % jennyAnim.size()],
- kJennyX, kJennyY);
- blitAt(jakeAnim [kJakeSeqs [level][seqIdx % 9] % jakeAnim.size()],
- kJakeX, kJakeY);
+ blitAt(jennyAnim[jennySeqs[level][seqIdx % 9] % jennyAnim.size()],
+ jennyX, jennyY);
+ blitAt(jakeAnim [jakeSeqs [level][seqIdx % 9] % jakeAnim.size()],
+ jakeX, jakeY);
g_system->updateScreen();
}
@@ -212,20 +272,23 @@ void EEMEngine::doChoosePartner() {
}
if (ev.type == Common::EVENT_MOUSEMOVE) {
curMouseX = ev.mouse.x;
- const uint newLevel = happinessLevel(curMouseX);
+ const uint newLevel = happinessLevel(curMouseX, isLondon());
if (newLevel != level) {
level = newLevel;
seqIdx = 0; // restart cycle so the gesture pops
blitAt(background, 0, 0);
- blitAt(jennyAnim[kJennySeqs[level][seqIdx % 9] % jennyAnim.size()],
- kJennyX, kJennyY);
- blitAt(jakeAnim [kJakeSeqs [level][seqIdx % 9] % jakeAnim.size()],
- kJakeX, kJakeY);
+ blitAt(jennyAnim[jennySeqs[level][seqIdx % 9] % jennyAnim.size()],
+ jennyX, jennyY);
+ blitAt(jakeAnim [jakeSeqs [level][seqIdx % 9] % jakeAnim.size()],
+ jakeX, jakeY);
g_system->updateScreen();
}
}
if (ev.type == Common::EVENT_LBUTTONDOWN) {
- _partner = (ev.mouse.x >= 160) ? kPartnerJake : kPartnerJenny;
+ const bool leftHalf = ev.mouse.x < 160;
+ _partner = isLondon()
+ ? (leftHalf ? kPartnerJake : kPartnerJenny)
+ : (leftHalf ? kPartnerJenny : kPartnerJake);
debugC(1, kDebugGeneral, "Partner picked: %s",
_partner == kPartnerJake ? "Jake" : "Jennifer");
done = true;
@@ -233,10 +296,12 @@ void EEMEngine::doChoosePartner() {
}
if (ev.type == Common::EVENT_KEYDOWN) {
if (ev.kbd.keycode == Common::KEYCODE_LEFT) {
- _partner = kPartnerJenny; done = true; break;
+ _partner = isLondon() ? kPartnerJake : kPartnerJenny;
+ done = true; break;
}
if (ev.kbd.keycode == Common::KEYCODE_RIGHT) {
- _partner = kPartnerJake; done = true; break;
+ _partner = isLondon() ? kPartnerJenny : kPartnerJake;
+ done = true; break;
}
if (ev.kbd.keycode == Common::KEYCODE_RETURN ||
ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 80013325a49..705195737c8 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -390,8 +390,9 @@ Common::Error EEMEngine::run() {
goto screenLoop;
// EEM2 ("Eagle Eye Mysteries in London"): opening sequence, then screen
- // 8 profile selection. A freshly-created detective starts the training
- // case (M0); an existing profile resumes its saved menu/map state.
+ // 8 profile selection, screen 9 partner selection. A freshly-created
+ // detective starts the training case (M0) after choosing Jake/Jennifer;
+ // an existing profile resumes its saved menu/map state.
if (isLondon()) {
runLondonStartup();
if (!shouldQuit()) {
@@ -400,11 +401,15 @@ Common::Error EEMEngine::run() {
applySkipRepeatedCasesOption();
if (_mystery.isLoaded()) {
_nextScreen = kScreenMap;
- } else if (_profileCreatedThisSession) {
- _nextScreen = startLondonTrainingMystery()
- ? kScreenInitClues : kScreenInvalid;
} else {
- _nextScreen = kScreenAction;
+ doChoosePartner();
+ (void)saveProfile(_playerName);
+ if (_profileCreatedThisSession) {
+ _nextScreen = startLondonTrainingMystery()
+ ? kScreenInitClues : kScreenInvalid;
+ } else {
+ _nextScreen = kScreenAction;
+ }
}
}
goto screenLoop;
@@ -648,8 +653,10 @@ screenLoop:
applyStartupTestOverrides();
if (!shouldQuit())
applySkipRepeatedCasesOption();
- if (!shouldQuit() && !isLondon())
+ if (!shouldQuit() && (!isLondon() || !_mystery.isLoaded()))
doChoosePartner();
+ if (!shouldQuit() && isLondon() && !_mystery.isLoaded())
+ (void)saveProfile(_playerName);
if (!shouldQuit()) {
if (_mystery.isLoaded())
_nextScreen = kScreenMap;
@@ -1240,41 +1247,59 @@ void EEMEngine::runLondonStartup() {
void EEMEngine::showLondonCharSelect() {
// `_NewPlayer` @ 1cd3:0f27 â character creation over background PIC 0xc
- // (palette 0). Two text fields then a Jake/Jenny pick (left/right arrow
- // 0x4b/0x4d -> DAT_4c4c, Enter confirms). The field/box rects are
- // constants in EEM2's data segment (read at 2bca:0e3a); they map to the
- // `pr` player record's FirstName[12] / LastName[20]:
+ // (palette 0). Two text fields then a male/female player-gender pick
+ // (left/right arrow 0x4b/0x4d -> DAT_4c4c, Enter confirms). The
+ // field/box rects are constants in EEM2's data segment (read at
+ // 2bca:0e3a); they map to the `pr` player record's FirstName[12] /
+ // LastName[20]:
// first name : (54,75)-(151,85) last name : (167,75)-(266,85)
- // Jake box : (110,116)-(120,122) Jenny box : (190,116)-(200,122)
+ // male : (110,116)-(120,122) female : (190,116)-(200,122)
debugC(1, kDebugGeneral, "EEM2 (London): character creation");
const Common::Rect kFirstRect(54, 75, 151, 85);
const Common::Rect kLastRect(167, 75, 266, 85);
- const Common::Rect kJakeBox(110, 116, 120, 122);
- const Common::Rect kJennyBox(190, 116, 200, 122);
+ const Common::Rect kMaleBox(110, 116, 120, 122);
+ const Common::Rect kFemaleBox(190, 116, 200, 122);
const uint kMaxFirst = 12, kMaxLast = 20;
const uint8 kInkColor = 0x0F; // typed-name ink
- const uint8 kHiBox = 0x0F; // selected-partner box highlight
Picture bg;
const bool haveBg = _picsArchive.getPicture(0xc, bg) && !bg.surface.empty();
+ if (!haveBg)
+ warning("London: passport background PIC 0xc failed to load");
+ const uint8 boxBlankColor = haveBg
+ ? (uint8)bg.surface.getPixel(kFirstRect.left, kFirstRect.top)
+ : 0x38;
+ const uint8 boxMarkColor = haveBg
+ ? (uint8)bg.surface.getPixel(109, 115)
+ : 0x22;
byte pal[kPalSize];
byte black[kPalSize] = {};
g_system->getPaletteManager()->setPalette(black, 0, 256);
g_system->updateScreen();
const bool havePal = getSitePalette(0, pal);
+ if (!havePal)
+ warning("London: passport palette 0 failed to load");
- CursorMan.showMouse(false);
+ Common::Event staleEvent;
+ while (g_system->getEventManager()->pollEvent(staleEvent)) {
+ if (staleEvent.type == Common::EVENT_QUIT ||
+ staleEvent.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ return;
+ }
+
+ CursorMan.showMouse(true);
+ g_system->setFeatureState(OSystem::kFeatureVirtualKeyboard, true);
_profileCreatedThisSession = false;
- enum { kFieldFirst = 0, kFieldLast = 1, kFieldPartner = 2 };
+ enum { kFieldFirst = 0, kFieldLast = 1, kFieldGender = 2 };
int field = kFieldFirst;
Common::String first, last;
- uint8 partner = kPartnerJake;
+ bool female = false;
bool blink = true, fadedIn = false, done = false, needRedraw = true;
- bool partnerReady = false;
- bool partnerCanConfirm = true;
+ bool genderReady = false;
+ bool genderCanConfirm = true;
uint32 blinkMs = g_system->getMillis();
while (!done && !shouldQuit()) {
@@ -1298,9 +1323,15 @@ void EEMEngine::showLondonCharSelect() {
kLastRect.top + 1, kLastRect.width(),
kInkColor);
}
- // Highlight the selected partner's indicator box.
- scratch.fillRect(partner == kPartnerJake ? kJakeBox : kJennyBox,
- kHiBox);
+ if (field == kFieldGender) {
+ // DOS restores both boxes, then fills the active one with the
+ // darker passport border color sampled at (109,115).
+ scratch.fillRect(kMaleBox, boxBlankColor);
+ scratch.fillRect(kFemaleBox, boxBlankColor);
+ scratch.fillRect(female ? kFemaleBox : kMaleBox,
+ boxMarkColor);
+ }
+ CursorMan.showMouse(true);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, kScreenWidth, kScreenHeight);
if (!fadedIn) {
@@ -1310,27 +1341,30 @@ void EEMEngine::showLondonCharSelect() {
}
g_system->updateScreen();
needRedraw = false;
- if (field == kFieldPartner)
- partnerReady = true;
+ if (field == kFieldGender)
+ genderReady = true;
}
Common::Event ev;
while (g_system->getEventManager()->pollEvent(ev)) {
if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ CursorMan.showMouse(true);
+ g_system->setFeatureState(OSystem::kFeatureVirtualKeyboard, false);
return;
- if (field == kFieldPartner && ev.type == Common::EVENT_LBUTTONDOWN) {
- if (!partnerReady)
+ }
+ if (field == kFieldGender && ev.type == Common::EVENT_LBUTTONDOWN) {
+ if (!genderReady)
continue;
- partner = (ev.mouse.x < 160) ? kPartnerJake : kPartnerJenny;
+ female = ev.mouse.x >= 160;
done = true;
needRedraw = true;
break;
}
- if (field == kFieldPartner && ev.type == Common::EVENT_KEYUP &&
+ if (field == kFieldGender && ev.type == Common::EVENT_KEYUP &&
(ev.kbd.keycode == Common::KEYCODE_RETURN ||
ev.kbd.keycode == Common::KEYCODE_KP_ENTER)) {
- partnerCanConfirm = true;
+ genderCanConfirm = true;
continue;
}
if (ev.type != Common::EVENT_KEYDOWN)
@@ -1345,10 +1379,10 @@ void EEMEngine::showLondonCharSelect() {
Common::String clean = buf;
clean.trim();
if (!clean.empty()) {
- field++; // advance to last name, then to partner pick
- if (field == kFieldPartner) {
- partnerReady = false;
- partnerCanConfirm = false;
+ field++; // advance to last name, then to gender pick
+ if (field == kFieldGender) {
+ genderReady = false;
+ genderCanConfirm = false;
}
}
} else if (k == Common::KEYCODE_BACKSPACE && !buf.empty()) {
@@ -1357,42 +1391,49 @@ void EEMEngine::showLondonCharSelect() {
buf.size() < cap) {
buf += (char)ev.kbd.ascii;
}
- } else { // partner pick
+ } else { // gender pick
if (k == Common::KEYCODE_LEFT) {
- partner = kPartnerJake;
- partnerCanConfirm = true;
+ female = false;
+ genderCanConfirm = true;
} else if (k == Common::KEYCODE_RIGHT) {
- partner = kPartnerJenny;
- partnerCanConfirm = true;
+ female = true;
+ genderCanConfirm = true;
} else if (k == Common::KEYCODE_BACKSPACE) {
field = kFieldLast; // back to editing
- partnerCanConfirm = true;
- } else if (enter && partnerReady && partnerCanConfirm) {
+ genderCanConfirm = true;
+ } else if (enter && genderReady && genderCanConfirm) {
done = true;
}
}
needRedraw = true;
- if (field == kFieldPartner && !partnerReady)
+ if (field == kFieldGender && !genderReady)
break;
}
const uint32 now = g_system->getMillis();
- if (field != kFieldPartner && now - blinkMs >= 400) {
+ if (field != kFieldGender && now - blinkMs >= 400) {
blinkMs = now;
blink = !blink;
needRedraw = true;
}
g_system->delayMillis(15);
}
- if (shouldQuit())
+ if (shouldQuit()) {
+ CursorMan.showMouse(true);
+ g_system->setFeatureState(OSystem::kFeatureVirtualKeyboard, false);
return;
+ }
- Common::Event staleEvent;
while (g_system->getEventManager()->pollEvent(staleEvent)) {
if (staleEvent.type == Common::EVENT_QUIT ||
- staleEvent.type == Common::EVENT_RETURN_TO_LAUNCHER)
+ staleEvent.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ CursorMan.showMouse(true);
+ g_system->setFeatureState(OSystem::kFeatureVirtualKeyboard, false);
return;
+ }
}
+ CursorMan.showMouse(true);
+ g_system->setFeatureState(OSystem::kFeatureVirtualKeyboard, false);
const Common::String displayName =
makeLondonProfileDisplayName(first, last);
@@ -1418,7 +1459,6 @@ void EEMEngine::showLondonCharSelect() {
// New profile. EEM2 `_NewPlayer @ 1cd3:0f27` initializes the player
// record only when `_LoadPlayerRecord` failed, then immediately saves it.
_playerName = displayName.empty() ? "Detective" : displayName;
- _partner = partner;
_chainStage = 1;
_voiceOn = true;
if (_audio)
@@ -1432,9 +1472,9 @@ void EEMEngine::showLondonCharSelect() {
if (err.getCode() != Common::kNoError)
warning("London: failed to save new profile '%s': %s",
_playerName.c_str(), err.getDesc().c_str());
- debugC(1, kDebugGeneral, "London: new player='%s' key=%s partner=%s",
+ debugC(1, kDebugGeneral, "London: new player='%s' key=%s gender=%s",
_playerName.c_str(), profileKey.c_str(),
- partner == kPartnerJake ? "Jake" : "Jenny");
+ female ? "female" : "male");
}
void EEMEngine::showFloppyStormLogo() {
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 786c41a089c..89d6f9634bf 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -427,13 +427,13 @@ private:
/// Opening sequence + character creation â EEM2's `_DoOpeningAnims`
/// @ 2721:08e6 then screen 8 profile selection.
void runLondonStartup();
- /// Start London mystery 0 for a freshly-created detective.
+ /// Start London mystery 0 after a freshly-created detective chooses a partner.
bool startLondonTrainingMystery();
/// Blit a full-screen still PIC and fade it in / hold / out using the
/// given SITEPALS. palette index.
void showLondonLogo(uint picId, uint palId, uint holdMs);
/// EEM2 character creation (`_NewPlayer`: palette 0 + background PIC 0xc):
- /// first/last name entry + Jake/Jenny selection.
+ /// first/last name entry + male/female player-gender selection.
void showLondonCharSelect();
/// EEM2 case-intro animation (`_DoInitClues` @ 1abf:03b3): single partner
/// anim (Jake 0x18 / Jenny 0x71) faded in, then phone1.voc on caseType 1.
Commit: ad1a9413609fb00f111f87ae9a01ef015dd9076e
https://github.com/scummvm/scummvm/commit/ad1a9413609fb00f111f87ae9a01ef015dd9076e
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:26+02:00
Commit Message:
EEM: proper case selection in London
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index d2be151d480..365edd16d8e 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -1998,9 +1998,10 @@ void EEMEngine::doActionScreen() {
}
void EEMEngine::doCaseSelection() {
- // `_DoChooseMystery @ 1a35:02b7` + `_CaseSelection @ 1c33:0a87` â
- // loads BOOK<stage>.NME, draws PIC 0x41 + centered "Book N" title.
- const uint kMaxMystery = 54;
+ // `_DoChooseMystery @ 1a35:02b7` + `_CaseSelection @ 1c33:0a87`
+ // (EEM2 @ 1abf:022a / 1cd3:0a9d): load BOOK<stage>.NME, draw PIC 0x41
+ // + centered "Book N" title, then read the chosen M*.BIN.
+ const uint kMaxMystery = isLondon() ? 50 : 54;
CursorMan.showMouse(true);
setSitePalette(0);
@@ -2021,20 +2022,30 @@ void EEMEngine::doCaseSelection() {
const int kKdAnimX = 0x112;
const int kKdAnimY = 0x50;
- // stage 1 (Junior, BOOK1.NME) = 1..24, stage 2 (Senior, BOOK2.NME) =
- // 25..48, stage 3 (Master, BOOK3.NME) = 49..54.
- uint stageLo = 1, stageHi = 0x18;
+ // EEM1: stage 1 (Junior, BOOK1.NME) = 1..24, stage 2 (Senior,
+ // BOOK2.NME) = 25..48, stage 3 (Master, BOOK3.NME) = 49..54.
+ // EEM2/London: only BOOK1.NME and BOOK2.NME ship, each with 25 cases.
+ uint stageLo = 1, stageHi = isLondon() ? 0x19 : 0x18;
uint book = 1;
switch (_chainStage) {
case 2:
- stageLo = 0x19;
- stageHi = 0x30;
+ stageLo = isLondon() ? 0x1a : 0x19;
+ stageHi = isLondon() ? 0x32 : 0x30;
book = 2;
break;
case 3:
- stageLo = 0x31;
- stageHi = 0x36;
- book = 3;
+ if (isLondon()) {
+ // London has no BOOK3.NME. Until the London progression path is
+ // fully split from EEM1, keep the picker on the second book rather
+ // than attempting to open missing data.
+ stageLo = 0x1a;
+ stageHi = 0x32;
+ book = 2;
+ } else {
+ stageLo = 0x31;
+ stageHi = 0x36;
+ book = 3;
+ }
break;
default:
break;
@@ -2233,14 +2244,6 @@ void EEMEngine::doCaseSelection() {
return;
const uint mn = stageLo + selRow;
- if (isLondon()) {
- // EEM2: the menu (PIC 0x41, ANI 0x15/0x16, BOOK*.NME) is shared with
- // EEM1, but loading a chosen mystery from the menu isn't wired up yet
- // (the training case is started directly from run()), so stop here.
- debugC(1, kDebugMystery,
- "London: selected mystery %u (menu load not implemented yet)", mn);
- return;
- }
if (!_mystery.load(mn, &_rng)) {
warning("doCaseSelection: failed to load mystery %u", mn);
_mystery.clear();
Commit: 3f0cccf90ae9b4eed827aaf1dff922b919c239dd
https://github.com/scummvm/scummvm/commit/3f0cccf90ae9b4eed827aaf1dff922b919c239dd
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:26+02:00
Commit Message:
EEM: more side effects for clues in London
Changed paths:
engines/eem/clues.cpp
engines/eem/mystery.cpp
engines/eem/mystery.h
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index a6d7f734cd0..86a627d6f18 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -97,6 +97,35 @@ uint markClueBlockNotebookEntries(Mystery &mystery, const byte *clueBlock,
return marked;
}
+void updateLondonClueSite(Mystery &mystery, uint16 rawSite, bool siteOn,
+ uint slot) {
+ if (rawSite == 0xFFFF)
+ return;
+
+ const bool conditional = (rawSite & 0x8000) != 0;
+ const uint16 siteVal = rawSite & 0x7FFF;
+ if (!conditional) {
+ if (siteVal < Mystery::kVisitedSiteCap)
+ mystery._onSites[siteVal] = siteOn ? 1 : 0;
+ return;
+ }
+
+ uint8 &seen = siteOn ? mystery._seenCONSITEs : mystery._seenCOFFSITEs;
+ const uint8 total = siteOn ? mystery.numCONSITEs() : mystery.numCOFFSITEs();
+ if (siteOn)
+ mystery._sawCONSITEs = true;
+ else
+ mystery._sawCOFFSITEs = true;
+
+ if (slot != 0)
+ return;
+
+ if (seen != 0xFF)
+ seen++;
+ if (seen == total && siteVal < Mystery::kVisitedSiteCap)
+ mystery._onSites[siteVal] = siteOn ? 1 : 0;
+}
+
// _DoHappiness @ 172b:27b5 â per-zone sequence scripts.
// Jake seqs @ 29be:0337 (5 Ã 0x14 bytes), Jenny seqs @ 29be:039b. 9 frames each;
// the anim cells contain 10 cells = pairs of (neutral, smile) at 5 intensities.
@@ -800,30 +829,26 @@ void EEMEngine::applyClueSideEffects(const byte *c) {
// EEM2 `_DisplayClue @ 2542:05bd` per-entry side effects. With the
// shared `c = entryBase + 4` convention the EEM2 fields land at:
// onsite entry+0x22 (= c+0x1e), 5 Ã u16, high bit = CONSITE flag
- // offsite entry+0x2c (= c+0x28), 5 Ã u16, clears the site
+ // offsite entry+0x2c (= c+0x28), 5 Ã u16, high bit = COFFSITE flag
+ // gallery entry+0x36 (= c+0x32), 5 Ã u16 -> _InGallery[_newOrder[idx]]
// notebook entry+0x40 (= c+0x3c), 5 Ã u16 -> _AddNotebook
// jump entry+0x4a (= c+0x46), destination site for direct travel
- // (EEM2 has no gallery list here â that region is the onsite array.)
for (uint j = 0; j < 5; j++) {
const uint16 note = READ_LE_UINT16(c + 0x3c + j * 2);
if (note != 0xFFFF && note < Mystery::kCluesFoundCap)
_mystery._cluesFound[note] = 1;
- const uint16 onIdx = READ_LE_UINT16(c + 0x1e + j * 2);
- if (onIdx != 0xFFFF) {
- const uint16 siteVal = onIdx & 0x7FFF;
- if (siteVal < Mystery::kVisitedSiteCap)
- _mystery._onSites[siteVal] = 1;
- if (onIdx & 0x8000)
- _mystery._sawCONSITEs = true;
+ const uint16 galIdx = READ_LE_UINT16(c + 0x32 + j * 2);
+ if (galIdx != 0xFFFF && galIdx < Mystery::kGalleryCap) {
+ const uint8 phys = _mystery._newOrder[galIdx];
+ if (phys < Mystery::kGalleryCap)
+ _mystery._inGallery[phys] = 1;
}
- const uint16 offIdx = READ_LE_UINT16(c + 0x28 + j * 2);
- if (offIdx != 0xFFFF && (offIdx & 0x8000) == 0) {
- const uint16 siteVal = offIdx & 0x7FFF;
- if (siteVal < Mystery::kVisitedSiteCap)
- _mystery._onSites[siteVal] = 0;
- }
+ updateLondonClueSite(_mystery, READ_LE_UINT16(c + 0x1e + j * 2),
+ true, j);
+ updateLondonClueSite(_mystery, READ_LE_UINT16(c + 0x28 + j * 2),
+ false, j);
}
const uint16 jumpSite = READ_LE_UINT16(c + 0x46);
if (jumpSite != 0xFFFF)
diff --git a/engines/eem/mystery.cpp b/engines/eem/mystery.cpp
index bb87dbc9939..3469a5d350e 100644
--- a/engines/eem/mystery.cpp
+++ b/engines/eem/mystery.cpp
@@ -61,6 +61,7 @@ void Mystery::clear() {
memset(_visitedSite, 0, sizeof(_visitedSite));
memset(_onSites, 0, sizeof(_onSites));
_sawCOFFSITEs = _sawCONSITEs = _sawHelpHint = _solvedPuzzle = false;
+ _seenCOFFSITEs = _seenCONSITEs = 0;
_firstTry = true;
_searchLocationNumber = _siteNumber = 0xFFFF;
_lastSite = 0x1B;
@@ -154,6 +155,7 @@ bool Mystery::load(uint num, Common::RandomSource *rng) {
memset(_visitedSite, 0, sizeof(_visitedSite));
memset(_onSites, 0, sizeof(_onSites));
_sawCOFFSITEs = _sawCONSITEs = _sawHelpHint = _solvedPuzzle = false;
+ _seenCOFFSITEs = _seenCONSITEs = 0;
_firstTry = true;
_searchLocationNumber = _siteNumber = 0xFFFF;
_lastSite = 0x1B;
@@ -216,6 +218,7 @@ bool Mystery::load(uint num, Common::RandomSource *rng) {
memset(_visitedSite, 0, sizeof(_visitedSite));
memset(_onSites, 0, sizeof(_onSites));
_sawCOFFSITEs = _sawCONSITEs = _sawHelpHint = _solvedPuzzle = false;
+ _seenCOFFSITEs = _seenCONSITEs = 0;
_firstTry = true;
_searchLocationNumber = _siteNumber = 0xFFFF;
_lastSite = 0x1B; // _ReadMystery _LastSite sentinel.
@@ -622,6 +625,8 @@ void Mystery::syncState(Common::Serializer &s) {
Common::Serializer::Uint16LE);
if (_siteReturnDepth > kVisitedSiteCap)
_siteReturnDepth = 0;
+ s.syncAsByte(_seenCOFFSITEs);
+ s.syncAsByte(_seenCONSITEs);
}
} // End of namespace EEM
diff --git a/engines/eem/mystery.h b/engines/eem/mystery.h
index 7f3d16589c3..32c22d7f806 100644
--- a/engines/eem/mystery.h
+++ b/engines/eem/mystery.h
@@ -166,6 +166,8 @@ public:
uint16 _onSites[kVisitedSiteCap] = {};
bool _sawCOFFSITEs = false;
bool _sawCONSITEs = false;
+ uint8 _seenCOFFSITEs = 0;
+ uint8 _seenCONSITEs = 0;
bool _sawHelpHint = false;
bool _solvedPuzzle = false;
bool _firstTry = true;
Commit: 66393f6f85a5f8a9a34eef14f51bc44cdc5b0a57
https://github.com/scummvm/scummvm/commit/66393f6f85a5f8a9a34eef14f51bc44cdc5b0a57
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:27+02:00
Commit Message:
EEM: implement game progression in London
Changed paths:
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/ui.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 705195737c8..2b4ad77c7a7 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -240,8 +240,11 @@ void EEMEngine::applyStartupTestOverrides() {
if (!kDebugPopulateScrapbook1AtStartup)
return;
- for (uint i = 1; i <= 0x18 && i < sizeof(_mysteriesSolved); i++)
- _mysteriesSolved[i] = 1;
+ uint lo = 0, hi = 0;
+ if (mysteryTierRange(1, lo, hi)) {
+ for (uint i = lo; i <= hi && i < sizeof(_mysteriesSolved); i++)
+ _mysteriesSolved[i] = 1;
+ }
debugC(1, kDebugGeneral,
"startup test override: populated ScrapBook 1 mystery flags");
@@ -257,47 +260,68 @@ bool EEMEngine::areMysteriesSolved(uint lo, uint hi) const {
return true;
}
+bool EEMEngine::anyMysterySolved(uint lo, uint hi) const {
+ for (uint i = lo; i <= hi && i < sizeof(_mysteriesSolved); i++) {
+ if (_mysteriesSolved[i] != 0)
+ return true;
+ }
+ return false;
+}
+
+bool EEMEngine::mysteryTierRange(uint stage, uint &lo, uint &hi) const {
+ if (isLondon()) {
+ // EEM2/London: two 25-case books, no BOOK3.NME
+ // (`_DisplayCorrect @ 1ea1:0619`).
+ switch (stage) {
+ case 1: lo = 0x01; hi = 0x19; return true; // 1..25
+ case 2: lo = 0x1a; hi = 0x32; return true; // 26..50
+ default: return false;
+ }
+ }
+ // EEM1: Junior 1..24, Senior 25..48, Master 49..54
+ // (`_DisplayCorrect @ 1df2:073c`).
+ switch (stage) {
+ case 1: lo = 0x01; hi = 0x18; return true; // 1..24
+ case 2: lo = 0x19; hi = 0x30; return true; // 25..48
+ case 3: lo = 0x31; hi = 0x36; return true; // 49..54
+ default: return false;
+ }
+}
+
void EEMEngine::advanceChainStageAfterSolve(uint mysteryNum) {
if (mysteryNum == 0 || _chainStage >= 4)
return;
- uint lo = 0;
- uint hi = 0;
- switch (_chainStage) {
- case 1:
- lo = 1;
- hi = 0x18;
- break;
- case 2:
- lo = 0x19;
- hi = 0x30;
- break;
- case 3:
- lo = 0x31;
- hi = 0x36;
- break;
- default:
+ uint lo = 0, hi = 0;
+ if (!mysteryTierRange(_chainStage, lo, hi))
return;
- }
-
if (!areMysteriesSolved(lo, hi))
return;
const uint oldStage = _chainStage;
- // Book 2 repeats the Book 1 cases; this option keeps the original solve
- // state but jumps the profile's active chain straight to Book 3.
- if (_chainStage == 1 && ConfMan.getBool("skip_repeated_cases"))
+ // EEM1 only: Book 2 repeats the Book 1 cases, so this option keeps the
+ // original solve state but jumps the active chain straight to Book 3.
+ // London has 50 distinct cases and no Book 3, so the option does not apply.
+ if (!isLondon() && _chainStage == 1 &&
+ ConfMan.getBool("skip_repeated_cases"))
_chainStage = 3;
else
_chainStage++;
+ // London has only two books: `_DisplayCorrect @ 1ea1:0619` collapses
+ // chainStage 3 straight to 4 (every case solved â game complete).
+ if (isLondon() && _chainStage == 3)
+ _chainStage = 4;
+
debugC(1, kDebugMystery,
"chainStage advanced from %u to %u after solving mystery %u",
oldStage, _chainStage, mysteryNum);
}
void EEMEngine::applySkipRepeatedCasesOption() {
- if (!ConfMan.getBool("skip_repeated_cases"))
+ // EEM1 only â the option jumps past the repeated Book 2 to Book 3, which
+ // London does not have (see `advanceChainStageAfterSolve`).
+ if (isLondon() || !ConfMan.getBool("skip_repeated_cases"))
return;
if (_mystery.isLoaded())
return;
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 89d6f9634bf..8bc4136c4a4 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -302,6 +302,22 @@ public:
private:
void applyStartupTestOverrides();
bool areMysteriesSolved(uint lo, uint hi) const;
+
+ /// True if *any* mystery in the inclusive 1-based range [lo, hi] is
+ /// marked solved.
+ bool anyMysterySolved(uint lo, uint hi) const;
+
+ /// Number of case "books"/tiers in this variant. EEM1 ships three
+ /// (BOOK1..3.NME â 24/24/6 cases); EEM2/London ships two (BOOK1..2.NME â
+ /// 25 cases each, no BOOK3.NME). See `mysteryTierRange`.
+ uint mysteryTierCount() const { return isLondon() ? 2 : 3; }
+
+ /// Inclusive 1-based mystery range [lo, hi] for chain `stage` (1-based).
+ /// Returns false when `stage` is not a real tier in this variant.
+ /// EEM1 `_DisplayCorrect @ 1df2:073c` (1..24 / 25..48 / 49..54);
+ /// EEM2/London `_DisplayCorrect @ 1ea1:0619` (1..25 / 26..50).
+ bool mysteryTierRange(uint stage, uint &lo, uint &hi) const;
+
void advanceChainStageAfterSolve(uint mysteryNum);
void applySkipRepeatedCasesOption();
@@ -467,9 +483,10 @@ private:
/// +1 â next mystery (RIGHT/SPACE/Enter/click on last page).
int doShowEnding(uint num, bool firstPage = true);
- /// `_ShowScrapbook(stage, 0) @ 1f78:0642`. Mystery range
- /// `(stage-1)*0x18+1 .. (stage-1)*0x18+0x18`; skips unsolved
- /// entries in the current chain stage.
+ /// EEM1 `_ShowScrapbook(stage, 0) @ 1f78:0642`; EEM2/London scrapbook
+ /// `FUN_2046_09dd`. Walks the `mysteryTierRange(stage)` cases, skipping
+ /// unsolved entries in the current chain stage. Tier sizes are
+ /// variant-specific (EEM1 24/24/6; London 25/25, no stage 3).
void doShowScrapbook(uint stage);
void doActionScreen();
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 365edd16d8e..2613a680aee 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -612,6 +612,7 @@ struct ActionMenuView {
const char *const *pickLabel;
const bool *pickEnabled;
uint pick;
+ uint numPicks; ///< Visible entries (London drops ScrapBook 3 â 4).
};
// `_DoChooseMystery @ 1a35:02b7` â opens BOOK%u.NME (CRLF strings, up
@@ -812,9 +813,10 @@ void drawActionMenuFrame(const ActionMenuView &v) {
const int kListY0 = 35;
const int kLineH = 10;
- // 11 rows: separator/item pairs (0=sep, 1=Choose A Mystery, ...,
- // 9=See Scrapbook 3, 10=sep).
- for (int r = 0; r < 11; r++) {
+ // Separator/item pairs (0=sep, 1=Choose A Mystery, ..., trailing sep):
+ // 11 rows for EEM1's five picks, 9 for London's four (no ScrapBook 3).
+ const int kRows = (int)(2 * v.numPicks + 1);
+ for (int r = 0; r < kRows; r++) {
const int y = kListY0 + r * kLineH;
if ((r & 1) == 0) {
v.vm->getFont().drawString(&scratch, v.separator,
@@ -1410,15 +1412,18 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
}
void EEMEngine::doShowScrapbook(uint stage) {
- // `_ShowScrapbook(stage, 0) @ 1f78:0642`. 24-entry tiers; ending
- // viewer returns -1/0/+1 for prev/close/next. Current tier skips
- // unsolved entries.
- if (stage < 1 || stage > 3)
+ // EEM1 `_ShowScrapbook(stage, 0) @ 1f78:0642`; EEM2/London scrapbook
+ // `FUN_2046_09dd`. Ending viewer returns -1/0/+1 for prev/close/next;
+ // current tier skips unsolved entries. Tier sizes are variant-specific
+ // (EEM1: 24/24/6 cases over three books; London: 25/25 over two books,
+ // no BOOK3.NME) â `mysteryTierRange` returns false for London stage 3.
+ uint tierLo = 0, tierHi = 0;
+ if (stage < 1 || !mysteryTierRange(stage, tierLo, tierHi))
return;
const int solvedCount =
(int)(sizeof(_mysteriesSolved) / sizeof(_mysteriesSolved[0]));
- const int lo = (int)(stage - 1) * 0x18 + 1;
- const int hi = MIN<int>(lo + 0x18, solvedCount);
+ const int lo = (int)tierLo;
+ const int hi = MIN<int>((int)tierHi + 1, solvedCount);
if (lo >= hi)
return;
const bool currentTier = (stage == _chainStage);
@@ -1664,7 +1669,9 @@ void EEMEngine::doSetup() {
dirty = true;
continue;
}
- if (kScrap3Btn.contains(mx, my) && _chainStage >= 3) {
+ // London has no third book (`mysteryTierRange` rejects stage 3).
+ if (kScrap3Btn.contains(mx, my) && !isLondon() &&
+ _chainStage >= 3) {
doShowScrapbook(3);
setSitePalette(0);
dirty = true;
@@ -1787,32 +1794,22 @@ void EEMEngine::doActionScreen() {
" Ver Recortes 3"
};
const char * const *kPickLabel = isSpanish() ? kPickLabelES : kPickLabelEN;
- // Gating @ 1c33:19d1-1a70 by chain stage + per-tier solves:
+ // London has no BOOK3.NME, so its action menu offers four entries
+ // (Choose / Practice / ScrapBook 1 / ScrapBook 2). EEM1 adds ScrapBook 3.
+ const uint numPicks = isLondon() ? (uint)kPickScrap3 : (uint)kNumPicks;
+
+ // Gating @ EEM1 1c33:19d1-1a70 by chain stage + per-tier solves:
// stage 1: grey SB2/3; SB1 needs any tier-1 solve
// stage 2: grey Practice + SB3; SB2 needs tier-2 solve
- // stage 3: grey Practice; SB3 needs tier-3 solve
+ // stage 3: grey Practice; SB3 needs tier-3 solve (EEM1 only)
// stage 4: grey Choose + Practice
- bool anySolved1 = false;
- for (uint i = 1; i <= 0x18 && i < sizeof(_mysteriesSolved); i++) {
- if (_mysteriesSolved[i]) {
- anySolved1 = true;
- break;
- }
- }
- bool anySolved2 = false;
- for (uint i = 0x19; i <= 0x30 && i < sizeof(_mysteriesSolved); i++) {
- if (_mysteriesSolved[i]) {
- anySolved2 = true;
- break;
- }
- }
- bool anySolved3 = false;
- for (uint i = 0x31; i <= 0x36 && i < sizeof(_mysteriesSolved); i++) {
- if (_mysteriesSolved[i]) {
- anySolved3 = true;
- break;
- }
- }
+ uint loT = 0, hiT = 0;
+ const bool anySolved1 = mysteryTierRange(1, loT, hiT) &&
+ anyMysterySolved(loT, hiT);
+ const bool anySolved2 = mysteryTierRange(2, loT, hiT) &&
+ anyMysterySolved(loT, hiT);
+ const bool haveTier3 = mysteryTierRange(3, loT, hiT);
+ const bool anySolved3 = haveTier3 && anyMysterySolved(loT, hiT);
const bool chooseOn = _chainStage < 4;
const bool practiceOn = _chainStage <= 1;
@@ -1821,13 +1818,13 @@ void EEMEngine::doActionScreen() {
const bool scrap2On =
_chainStage >= 3 || (_chainStage == 2 && anySolved2);
const bool scrap3On =
- _chainStage >= 4 || (_chainStage == 3 && anySolved3);
+ haveTier3 && (_chainStage >= 4 || (_chainStage == 3 && anySolved3));
const bool kPickEnabled[kNumPicks] = {
chooseOn, practiceOn, scrap1On, scrap2On, scrap3On
};
// Seed selection on first enabled entry.
uint pick = 0;
- for (uint i = 0; i < kNumPicks; i++) {
+ for (uint i = 0; i < numPicks; i++) {
if (kPickEnabled[i]) {
pick = i;
break;
@@ -1862,6 +1859,7 @@ void EEMEngine::doActionScreen() {
v.pickLabel = kPickLabel;
v.pickEnabled = kPickEnabled;
v.pick = pick;
+ v.numPicks = numPicks;
drawActionMenuFrame(v);
uint32 chooserLastTick = g_system->getMillis();
@@ -1895,7 +1893,7 @@ void EEMEngine::doActionScreen() {
const int row = (ev.mouse.y - kListRect.top) / kLineH;
if ((row & 1) == 1) {
const uint mp = (uint)(row >> 1);
- if (mp < kNumPicks && kPickEnabled[mp]) {
+ if (mp < numPicks && kPickEnabled[mp]) {
pick = mp;
v.pick = pick;
drawActionMenuFrame(v);
@@ -1919,8 +1917,8 @@ void EEMEngine::doActionScreen() {
}
if (k == Common::KEYCODE_UP || k == Common::KEYCODE_LEFT) {
// `_DoChoose` arrow handlers @ 1c33:0514, bounded loop.
- for (int i = 0; i < (int)kNumPicks; i++) {
- pick = (pick == 0) ? (uint)(kNumPicks - 1) : pick - 1;
+ for (int i = 0; i < (int)numPicks; i++) {
+ pick = (pick == 0) ? (numPicks - 1) : pick - 1;
if (kPickEnabled[pick])
break;
}
@@ -1930,8 +1928,8 @@ void EEMEngine::doActionScreen() {
}
if (k == Common::KEYCODE_DOWN || k == Common::KEYCODE_RIGHT ||
k == Common::KEYCODE_TAB) {
- for (int i = 0; i < (int)kNumPicks; i++) {
- pick = (pick + 1) % kNumPicks;
+ for (int i = 0; i < (int)numPicks; i++) {
+ pick = (pick + 1) % numPicks;
if (kPickEnabled[pick])
break;
}
@@ -2022,34 +2020,20 @@ void EEMEngine::doCaseSelection() {
const int kKdAnimX = 0x112;
const int kKdAnimY = 0x50;
- // EEM1: stage 1 (Junior, BOOK1.NME) = 1..24, stage 2 (Senior,
- // BOOK2.NME) = 25..48, stage 3 (Master, BOOK3.NME) = 49..54.
- // EEM2/London: only BOOK1.NME and BOOK2.NME ship, each with 25 cases.
- uint stageLo = 1, stageHi = isLondon() ? 0x19 : 0x18;
- uint book = 1;
+ // Tier â BOOK<n>.NME. EEM1 ships three books (Junior 1..24, Senior
+ // 25..48, Master 49..54); EEM2/London ships two (1..25 / 26..50). London
+ // has no BOOK3.NME, and its stage 3 is transient (`_DisplayCorrect @
+ // 1ea1:0619` collapses it to 4), so should it ever be reached here it
+ // reuses BOOK2.NME rather than opening a missing file. `mysteryTierRange`
+ // is the single source of truth for the per-tier mystery ranges.
+ uint book;
switch (_chainStage) {
- case 2:
- stageLo = isLondon() ? 0x1a : 0x19;
- stageHi = isLondon() ? 0x32 : 0x30;
- book = 2;
- break;
- case 3:
- if (isLondon()) {
- // London has no BOOK3.NME. Until the London progression path is
- // fully split from EEM1, keep the picker on the second book rather
- // than attempting to open missing data.
- stageLo = 0x1a;
- stageHi = 0x32;
- book = 2;
- } else {
- stageLo = 0x31;
- stageHi = 0x36;
- book = 3;
- }
- break;
- default:
- break;
+ case 2: book = 2; break;
+ case 3: book = isLondon() ? 2 : 3; break;
+ default: book = 1; break;
}
+ uint stageLo = 0, stageHi = 0;
+ mysteryTierRange(book, stageLo, stageHi);
if (stageHi > kMaxMystery)
stageHi = kMaxMystery;
@@ -3889,10 +3873,11 @@ void EEMEngine::accuseDrawScreen(const AccuseNotesCtx &ctx) {
}
bool EEMEngine::doAccuseNotes() {
- // `_DoAccuse @ 1df2:0bdd` head. BG PIC 0x1A7. `_AccuseNoteRect @
- // 29be:1048` = (79, 27, 304, 159). Counter @ (209, 11) shows
- // `6 - chainStage` (stage 1=5, 2=4, 3=3 clues). Unselected color 1
- // (red), selected 0x3c. Click toggles selection; `_NoteButtons[4]`
+ // `_DoAccuse @ 1df2:0bdd` (EEM2 `@ 1ea1:0c03`) head. BG PIC 0x1A7.
+ // `_AccuseNoteRect @ 29be:1048` = (79, 27, 304, 159). Counter @ (209, 11)
+ // shows the required clue count (EEM1 `6 - chainStage` = 5/4/3 by tier;
+ // London a flat 5 â see `expected` below). Unselected color 1 (red),
+ // selected 0x3c. Click toggles selection; `_NoteButtons[4]`
// (180,174,201,190) SOLVE â `_HandleAccuseNoteButton` returns 2.
if (!_mystery.isLoaded() || !_font.isLoaded())
return false;
@@ -3907,9 +3892,13 @@ bool EEMEngine::doAccuseNotes() {
// `FUN_1d40_0e07 @ 1d40:0e34` zeroes _NoteSelected_Floppy on entry.
memset(_mystery._noteSelected, 0, sizeof(_mystery._noteSelected));
- const uint expected = (_chainStage >= 1 && _chainStage <= 3)
- ? (uint)(6 - _chainStage)
- : 5;
+ // EEM1 `_DoAccuse @ 1df2:0bdd` scales the required clue count by tier
+ // (`6 - chainStage`: stage 1=5, 2=4, 3=3). London `_DoAccuse @ 1ea1:0c03`
+ // hardcodes 5 (`local_a = 5`) for both books.
+ const uint expected = isLondon()
+ ? 5
+ : ((_chainStage >= 1 && _chainStage <= 3) ? (uint)(6 - _chainStage)
+ : 5);
// `_DrawNotes(NULL, 100, ...)` walks `_CluesFound[]`.
Common::Array<uint> found;
@@ -4199,7 +4188,7 @@ void EEMEngine::doAccuse() {
}
// `_DoAccuse @ 1df2:0bdd` + `_DoAccuseGallery @ 1df2:0a31`:
- // 1. Accuse-notes (PIC 0x1A7) â pick `6 - chainStage` clues.
+ // 1. Accuse-notes (PIC 0x1A7) â pick `6 - chainStage` clues (London: 5).
// Pass `_SolvedCheck` â gallery; fail â hint + return.
// 2. KD intro balloon (`KDTextIndex[+8]` + `_SayKDDigital(4)`).
// 3. PIC 0x3f + `_DrawGallery` portraits at 5 slots (29be:0x116).
@@ -5083,7 +5072,8 @@ void EEMEngine::doAccuseFloppy() {
}
// Clue selection â `FUN_1d40_0e07 @ 1d40:0e07`. Pick `6 - chainStage`
- // clues (1d40:0e34: `local_c = 6 - DAT_28da_3052`).
+ // clues (1d40:0e34: `local_c = 6 - DAT_28da_3052`); London a flat 5.
+ // Count handled inside `doAccuseNotes`.
if (!doAccuseNotes()) {
if (_nextScreen == kScreenAccuse) {
_nextScreen = _lastScreen != kScreenInvalid
Commit: 7ca4b9f330a53a1f958f6322ba5b2a5355eff4c7
https://github.com/scummvm/scummvm/commit/7ca4b9f330a53a1f958f6322ba5b2a5355eff4c7
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:27+02:00
Commit Message:
EEM: added missing musical cue in London
Changed paths:
engines/eem/clues.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 86a627d6f18..2887d99ab96 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -34,6 +34,7 @@
#include "eem/audio.h"
#include "eem/detection.h"
#include "eem/eem.h"
+#include "eem/music.h"
#include "eem/site.h"
// Clue / briefing pipeline (SCRIPT.C + KD.C).
@@ -929,6 +930,18 @@ void EEMEngine::displayClue(const byte *clueBlock) {
g_system->copyRectToScreen(bg.getPixels(), bg.pitch, 0, 0, kScreenWidth, kScreenHeight);
const byte *c = clueBlock + 4 + i * stride;
+ // EEM2 `_DisplayClue @ 2542:05bd` opens each clue with a per-entry MIDI
+ // cue read at entry+0x20 (= c+0x1c, the u16 right after the two voice
+ // slots), replacing any current track (`playMus`â`playFile` calls
+ // `stop()`). 0 = no cue; EEM1 clue entries have no such field. Gated on
+ // `_voiceOn` like `startTravelMusic` â the DOS gate is the separate
+ // music-on / MIDI-available flags (DAT_3036_4cc0 && DAT_2bca_146a).
+ if (isLondon() && _music && _voiceOn) {
+ const uint16 clueMusic = READ_LE_UINT16(c + 0x1c);
+ if (clueMusic != 0)
+ _music->playMus(clueMusic, /* loop= */ false);
+ }
+
// _DisplayClue @ 2404:0635-064b: _DoKDAnim(num) runs before the
// speaker portrait. EEM1 stores the KD-anim number at +0x3a; EEM2
// `_DisplayClue @ 2542:05bd` reads it at entry+0x52 (= c+0x4e).
Commit: 53af04e55c17668bf39c9b907ad0bd0379462dc1
https://github.com/scummvm/scummvm/commit/53af04e55c17668bf39c9b907ad0bd0379462dc1
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:27+02:00
Commit Message:
EEM: added missing conversation opcodes in London
Changed paths:
engines/eem/clues.cpp
engines/eem/eem.cpp
engines/eem/eem.h
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 2887d99ab96..0abed334700 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -729,22 +729,23 @@ void EEMEngine::doInitClues() {
}
}
-// _ParseString @ 1b66:07c3 (jump table @ 1b66:0cbe). Each handler reads
-// _Partner (u16 @ 0x7918) and indexes the name table @ 29be:0c28
-// ({Jake, Jennifer, he, she, him, her, his} as far pointers).
+// _ParseString @ EEM1 1b66:07c3 / EEM2 1bff:07c4 (jump table @ EEM2
+// CS:0xd2f). Each handler indexes a name table of far pointers
+// ({Jake, Jennifer, he, she, him, her, his}).
// 0x80 player name (auto-cap word starts, uses _PlayerRecord)
// 0x81 _Partner == 0 ? "Jake" : "Jennifer" (chosen detective)
// 0x82 _Partner == 0 ? "Jennifer" : "Jake" (the OTHER one)
// 0x83 _Partner == 0 ? "he" : "she"
// 0x84 _Partner == 0 ? "him" : "her"
// 0x85 _Partner == 0 ? "his" : "her"
-// 0x86..0x88 mirror 0x83..0x85 but branch on a separate gender flag
-// (DAT_29be_7985, handlers @ 1b66:0ad2/0b41/0bb0). That flag has no
-// writers anywhere in EEMCD.EXE â only the three handlers read it â
-// and no shipping mystery text (CD or floppy) contains the 0x86/0x87
-// /0x88 bytes inside parseString-formatted strings. The opcodes are
-// dead in the original engine and dead in our data, so we drop them
-// silently (which matches the always-0 flag path in DOS: he/him/his).
+// 0x86..0x88 emit the same he/him/his vs she/her/her strings as 0x83..0x85
+// but branch on a SEPARATE gender flag, not _Partner (EEM2 handlers
+// @ 1bff:0ad3/0b42/0bb1 test [0x930c]; the partner handlers test
+// [0x9294]). They are the PLAYER's pronouns: EEM2/London sets that flag
+// from the passport gender pick (`_NewPlayer` DAT_3036_4c4c), and London
+// dialogue uses them (e.g. M13/M50.BIN "You and <86>..."). EEM1 never
+// writes the flag (DAT_29be_7985) and ships no text using these bytes, so
+// its always-male path is reproduced by `_playerFemale` defaulting false.
// 0x89 KD hint placeholder (caller handles).
Common::String EEMEngine::parseString(const Common::String &raw,
const Common::String &playerName,
@@ -773,11 +774,16 @@ Common::String EEMEngine::parseString(const Common::String &raw,
out += isJake ? "his" : "her";
break;
case 0x86:
+ // Player pronoun (passport gender), mirror of 0x83.
+ out += _playerFemale ? "she" : "he";
+ break;
case 0x87:
+ // Player pronoun (passport gender), mirror of 0x84.
+ out += _playerFemale ? "her" : "him";
+ break;
case 0x88:
- // Stubbed suspect-gender pronouns; the DOS flag they branch
- // on is never written and no mystery msg uses these bytes
- // (see jumptable comment above).
+ // Player pronoun (passport gender), mirror of 0x85.
+ out += _playerFemale ? "her" : "his";
break;
case 0x89:
// KD hint placeholder (caller handles before this point).
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 2b4ad77c7a7..660c2ffe547 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -1484,6 +1484,10 @@ void EEMEngine::showLondonCharSelect() {
// record only when `_LoadPlayerRecord` failed, then immediately saves it.
_playerName = displayName.empty() ? "Detective" : displayName;
_chainStage = 1;
+ // `_NewPlayer @ 1cd3:0f27` stores the passport gender pick (`DAT_3036_4c4c`).
+ // Drives the player pronouns 0x86-0x88 in `parseString`. (Existing profiles
+ // take the gender from their save instead, via the load path above.)
+ _playerFemale = female;
_voiceOn = true;
if (_audio)
_audio->setVoiceEnabled(_voiceOn);
@@ -1666,6 +1670,10 @@ Common::Error EEMEngine::saveGameStream(Common::WriteStream *stream,
s.syncAsByte(_chainStage);
s.syncAsByte(_voiceOn);
+ // London passport gender (player pronouns 0x86-0x88).
+ byte playerFemale = _playerFemale ? 1 : 0;
+ s.syncAsByte(playerFemale);
+
// Mid-case resume: persist in-progress mystery (no equivalent in
// _LoadGame @ 2404:0dc7).
bool hasMystery = _mystery.isLoaded();
@@ -1706,6 +1714,11 @@ Common::Error EEMEngine::loadGameStream(Common::SeekableReadStream *stream) {
if (_audio)
_audio->setVoiceEnabled(_voiceOn);
+ // London passport gender (player pronouns 0x86-0x88).
+ byte playerFemale = 0;
+ s.syncAsByte(playerFemale);
+ _playerFemale = (playerFemale != 0);
+
bool hasMystery = false;
s.syncAsByte(hasMystery);
if (hasMystery) {
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 8bc4136c4a4..162a041f1c4 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -551,6 +551,13 @@ private:
Common::String _playerName; ///< Substituted into 0x80 placeholders.
+ /// London passport gender (EEM2 `_NewPlayer @ 1cd3:0f27` gender pick,
+ /// `DAT_3036_4c4c`: left/0 = male, right/1 = female). Drives the player
+ /// pronoun opcodes 0x86/0x87/0x88 in `parseString` (he·him·his / she·her·
+ /// her). EEM1 has no passport, so it stays false (male) â matching that
+ /// engine's never-written gender flag.
+ bool _playerFemale = false;
+
/// `_PlayerRecord.SolvedMysteries[55]`. 0=unsolved, 1=solved, 2=first-try.
uint8 _mysteriesSolved[55] = {};
Commit: 076d0f0633ade0b876e9a7851fe427f08340d15b
https://github.com/scummvm/scummvm/commit/076d0f0633ade0b876e9a7851fe427f08340d15b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:28+02:00
Commit Message:
EEM: improved TRAVIS UI in London
Changed paths:
engines/eem/mystery.cpp
engines/eem/mystery.h
engines/eem/ui.cpp
diff --git a/engines/eem/mystery.cpp b/engines/eem/mystery.cpp
index 3469a5d350e..edf8261849f 100644
--- a/engines/eem/mystery.cpp
+++ b/engines/eem/mystery.cpp
@@ -402,6 +402,12 @@ uint16 Mystery::noteIndexCount() const {
return (uint16)((_galleryOffset - _noteOffset) / stride);
}
+uint Mystery::noteSectionSize() const {
+ if (!isLoaded() || _galleryOffset <= _noteOffset)
+ return 0;
+ return _galleryOffset - _noteOffset;
+}
+
bool Mystery::noteHasNotebookText(uint clueId) const {
const byte *ni = noteIndex();
const uint16 cnt = noteIndexCount();
diff --git a/engines/eem/mystery.h b/engines/eem/mystery.h
index 32c22d7f806..54830f70e50 100644
--- a/engines/eem/mystery.h
+++ b/engines/eem/mystery.h
@@ -83,11 +83,19 @@ public:
/// u8 nameLen, nameLen bytes of name.
const byte *floppySuspectEntry(uint suspectIdx) const;
- /// NoteIndex array (4 bytes per entry: u16 textOff + u16 pts).
+ /// NoteIndex array. EEM1 CD: 4 bytes/entry (u16 textOff + u16 pts).
+ /// EEM2/London CD: 2 bytes/entry (u16 textOff only â no points field;
+ /// `_DrawNotes @ 16a0:01de` reads `noteIndex[clueId*2]`). Floppy: 7
+ /// bytes/entry.
const byte *noteIndex() const;
uint16 noteIndexCount() const;
+ /// Raw byte size of the CD NoteIndex section [_noteOffset, _galleryOffset).
+ /// The notebook divides this by the variant stride (EEM1 4 / London 2) to
+ /// get the clue count, since `noteIndexCount()` assumes the 4-byte stride.
+ uint noteSectionSize() const;
+
/// True when clueId has a notebook text entry. Floppy dialog records
/// may be spoken-only with zero notebook offset, skipped by _DrawNotes_Floppy.
bool noteHasNotebookText(uint clueId) const;
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 2613a680aee..ed11bb57e91 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -271,6 +271,10 @@ constexpr Common::Rect kPdaAccuseRect(Common::Point(180, 174), 21, 16);
constexpr Common::Rect kPdaPageNextRect(Common::Point(204, 174), 20, 16);
constexpr Common::Rect kPdaPagePrevRect(Common::Point(226, 174), 21, 16);
constexpr Common::Rect kPdaHelp2Rect(Common::Point(267, 174), 21, 16);
+// London-only: `_NoteButtons[9]` = (0,0,66,79) â site (EEM1's slot 9 is a
+// dead 0-rect). A secondary "close the PDA" hotspot over the device's
+// top-left corner. EEM2 `_HandleNoteButton` slot 9 â screen 3.
+constexpr Common::Rect kPdaLondonCloseRect(Common::Point(0, 0), 66, 79);
constexpr uint16 kProfilePickerRevealPic = 0x105;
constexpr int kProfilePickerRevealX = 0x3e;
@@ -2257,6 +2261,21 @@ void EEMEngine::doNotebook() {
// [9] ( 0, 0, 0, 0) same exit as [8]
// [10] (267,174,288,190) â `_InterfaceHelp(0)` again (0x3f9)
// BG PIC 0x3f; partner ANI 1 (Jake) / 0xb (Jenny) at (5, 80).
+ //
+ // EEM2/London (`_DoNotebook @ 16a0:0517`, `_HandleNoteButton @ 16a0:03dd`,
+ // jumptable @ 16a0:0503) reuses the SAME button rect table (2bca:0151) and
+ // handler set, but reassigns two slots and revives slot 9 (verified by
+ // disassembly â `[0x9292]` is the next-screen code):
+ // [1] ( 93,174,115,190) MAP â screen 2 (EEM1: a 2nd InterfaceHelp).
+ // London's dedicated map button.
+ // [7] ( 7,177, 57,200) DOS EEM2 â SITE, but we keep it as MAP (â 2) in
+ // both variants â the EEM1 partner-foot map shortcut
+ // (a player convenience alongside button [1]).
+ // [9] ( 0, 0, 66, 79) SITE â screen 3 (EEM1: dead 0-rect)
+ // Everything else (gallery, accuse, host hint, page next/prev, help [10],
+ // site [8]) is identical. The note rendering, pagination, partner ANI
+ // (same 1/0xb), and gizmo colour-cycle are shared as-is; only button [1]
+ // (London â map) and the extra close area [9] are gated on `isLondon()`.
if (!_mystery.isLoaded() || !_font.isLoaded())
return;
@@ -2307,12 +2326,20 @@ void EEMEngine::doNotebook() {
}
if (ev.type == Common::EVENT_LBUTTONDOWN) {
// Earlier rects win on overlap (matches `_FindButton`).
- if (kPdaSiteRect.contains(ev.mouse.x, ev.mouse.y)) {
+ if (kPdaSiteRect.contains(ev.mouse.x, ev.mouse.y) ||
+ (isLondon() &&
+ kPdaLondonCloseRect.contains(ev.mouse.x, ev.mouse.y))) {
_nextScreen = kScreenSite;
exitFlag = true;
break; // back to site
}
if (kPdaPartnerFootMapRect.contains(ev.mouse.x, ev.mouse.y)) {
+ // EEM1 `_NoteButtons[7]` â map (screen 2). DOS EEM2 reassigns
+ // this slot to SITE, but we intentionally keep the partner-
+ // foot hotspot as a map shortcut in BOTH variants for parity
+ // with EEM1. London also has its own dedicated map button
+ // (`kPdaHelpRect` below) and can still close the PDA via the
+ // site button (35,111) or the top-left corner (0,0,66,79).
_nextScreen = kScreenMapAlt;
exitFlag = true;
break;
@@ -2334,7 +2361,14 @@ void EEMEngine::doNotebook() {
break;
}
if (kPdaHelpRect.contains(ev.mouse.x, ev.mouse.y)) {
- // rect 1 â `_InterfaceHelp(0)` (PICs 0x63 / 0x1ae).
+ // `_NoteButtons[1]`. EEM1: a 2nd `_InterfaceHelp(0)` button
+ // (PICs 0x63 / 0x1ae). London reassigns this slot to MAP
+ // (`_HandleNoteButton` slot 1 â screen 2).
+ if (isLondon()) {
+ _nextScreen = kScreenMapAlt;
+ exitFlag = true;
+ break;
+ }
setInteractiveMouseCursor(false);
doInterfaceHelp(0);
dirty = true;
@@ -2404,7 +2438,11 @@ Common::String EEMEngine::notebookNoteText(uint clueId, const byte *ni,
return parseString(Common::String(p, len),
_playerName, _partner);
}
- const uint16 textOff = READ_LE_UINT16(ni + clueId * 4);
+ // EEM1 CD: 4-byte entries (textOff at +0, points at +2). EEM2/London CD:
+ // 2-byte entries (textOff only) â `_DrawNotes @ 16a0:01de` reads
+ // `noteIndex[clueId*2]`.
+ const uint stride = isLondon() ? 2 : 4;
+ const uint16 textOff = READ_LE_UINT16(ni + clueId * stride);
return parseString(_mystery.textAt(textOff),
_playerName, _partner);
}
@@ -2428,13 +2466,24 @@ void EEMEngine::drawNotebookFrame(int &page) {
// `_DrawNotes` walks `_NoteIndex` for current page; word-wraps each
// found clue in `_NotebookRect`. Selected = color 0x3c.
+ // EEM2/London NoteIndex entries are 2 bytes (no points field), so its real
+ // clue count is the section size / 2. `noteIndexCount()` assumes the EEM1
+ // 4-byte stride (and stays that way for the accuse-scoring path, which is a
+ // separate London concern), so derive the London-correct count here â else
+ // high-id notes get dropped.
+ const uint16 londonCount = isLondon()
+ ? (uint16)(_mystery.noteSectionSize() / 2) : 0;
Common::Array<uint> found;
for (uint i = 0; i < Mystery::kCluesFoundCap; i++) {
- if (_mystery._cluesFound[i] && _mystery.noteHasNotebookText(i))
+ const bool hasText = isLondon()
+ ? (i < londonCount)
+ : _mystery.noteHasNotebookText(i);
+ if (_mystery._cluesFound[i] && hasText)
found.push_back(i);
}
const byte *ni = _mystery.noteIndex();
- const uint16 niCount = _mystery.noteIndexCount();
+ const uint16 niCount = isLondon()
+ ? londonCount : _mystery.noteIndexCount();
const int kRectX = kNotebookRect.left;
const int kRectY = kNotebookRect.top;
@@ -2619,7 +2668,15 @@ void EEMEngine::doGallery() {
break;
}
if (kPdaHelpRect.contains(ev.mouse.x, ev.mouse.y)) {
- // rect 1 â `_InterfaceHelp(0)` (158f:0625).
+ // EEM1 slot 1 â a 2nd `_InterfaceHelp(0)` (158f:0625).
+ // EEM2 gallery `_HandleGalleryButton @ 160e:05c7` slot 1 â
+ // MAP (`MOV [0x9292],2` @ 160e:05fe) â London's dedicated
+ // map button, same as the notebook/accuse PDA bar.
+ if (isLondon()) {
+ _nextScreen = kScreenMapAlt;
+ exitFlag = true;
+ break;
+ }
setInteractiveMouseCursor(false);
doInterfaceHelp(0);
lastDraw = 0;
@@ -2877,6 +2934,14 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
back = true;
break;
}
+ if (isLondon() && kPdaHelpRect.contains(mx, my)) {
+ // EEM2 gallery slot 1 â MAP (dedicated map button);
+ // only kPdaHelp2Rect (267,174) stays help in London.
+ _nextScreen = kScreenMapAlt;
+ exitGallery = true;
+ back = true;
+ break;
+ }
if (kPdaHelpRect.contains(mx, my) ||
kPdaHelp2Rect.contains(mx, my)) {
setInteractiveMouseCursor(false);
@@ -4011,6 +4076,13 @@ bool EEMEngine::doAccuseNotes() {
_nextScreen = kScreenGallery;
return false;
}
+ if (isLondon() && kPdaHelpRect.contains(mx, my)) {
+ // EEM2 accuse `_HandleAccuseNoteButton @ 1ea1:0873` slot 1
+ // â MAP (`MOV [0x9292],2` @ 1ea1:089b) â London's dedicated
+ // map button; only kPdaHelp2Rect (267,174) stays help.
+ _nextScreen = kScreenMapAlt;
+ return false;
+ }
if (kPdaHelpRect.contains(mx, my) ||
kPdaHelp2Rect.contains(mx, my)) {
setInteractiveMouseCursor(false);
Commit: 1f0ad1bf77d4d333c2bf3d8272a2a1f829e458db
https://github.com/scummvm/scummvm/commit/1f0ad1bf77d4d333c2bf3d8272a2a1f829e458db
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:28+02:00
Commit Message:
EEM: improved hints voices/text in London
Changed paths:
engines/eem/audio.cpp
engines/eem/audio.h
engines/eem/graphics.cpp
engines/eem/mystery.h
diff --git a/engines/eem/audio.cpp b/engines/eem/audio.cpp
index 1a553b47c00..14570379b84 100644
--- a/engines/eem/audio.cpp
+++ b/engines/eem/audio.cpp
@@ -346,19 +346,37 @@ void AudioPlayer::stopSpool() {
_mixer->stopHandle(_spoolHandle);
}
-// _SayKDDigital @ 2404:0fbc.
-// slot = kdspeak * 2 + (partner == Jake ? 1 : 0); sound = digital[slot+1] - 1
-// KDDigitalIndex = KDTextIndex + 0x12 (set by _ReadMystery @ 2404:0163-0167).
+// _SayKDDigital @ EEM1 2404:0fbc / EEM2 2542:1845. KDDigitalIndex =
+// KDTextIndex + 0x12. EEM1 reads index `kdspeak*2 + (Jake?1:0) + 1`; EEM2
+// drops the trailing +1 (`kdspeak*2 + (Jake?1:0)`) â a real per-variant
+// difference, so using EEM1's +1 for London shifts every KD line by one slot.
void AudioPlayer::sayKDDigital(const byte *kdTextIndex, uint kdspeak,
uint partner) {
if (!kdTextIndex || _currentMystery < 0)
return;
const byte *digital = kdTextIndex + 0x12;
- const uint slot = (kdspeak * 2) + (partner == 0 ? 1u : 0u) + 1u;
+ const uint slot = (kdspeak * 2) + (partner == 0 ? 1u : 0u) +
+ (_vm && _vm->isLondon() ? 0u : 1u);
const uint16 raw = READ_LE_UINT16(digital + slot * 2);
if (raw == 0 || raw == 0xFFFF)
return;
spoolSound((uint)(raw - 1));
}
+// _SayKDHintDigital @ 2542:187e â EEM2/London partner chain-hint voice.
+// Identical to EEM2 `_SayKDDigital` but indexes the table 0x3a bytes after
+// KDTextIndex (vs 0x12); `slot` is the 0..4 hint-chain slot. EEM1 voices its
+// chain hints through `sayKDDigital(slot + 10)` instead.
+void AudioPlayer::sayKDHintDigital(const byte *kdTextIndex, uint slot,
+ uint partner) {
+ if (!kdTextIndex || _currentMystery < 0)
+ return;
+ const byte *table = kdTextIndex + 0x3a;
+ const uint idx = (slot * 2) + (partner == 0 ? 1u : 0u);
+ const uint16 raw = READ_LE_UINT16(table + idx * 2);
+ if (raw == 0 || raw == 0xFFFF)
+ return;
+ spoolSound((uint)(raw - 1));
+}
+
} // End of namespace EEM
diff --git a/engines/eem/audio.h b/engines/eem/audio.h
index 1d986ac47bb..fa901b971a8 100644
--- a/engines/eem/audio.h
+++ b/engines/eem/audio.h
@@ -112,6 +112,11 @@ public:
/// header slot). Pass the mystery's kdTextIndex() pointer.
void sayKDDigital(const byte *kdTextIndex, uint kdspeak, uint partner);
+ /// `_SayKDHintDigital @ 2542:187e` â EEM2/London partner chain-hint voice.
+ /// Like `sayKDDigital` but indexes the table 0x3a after KDTextIndex (vs
+ /// 0x12) with no +1 bias. @p slot is the 0..4 hint-chain slot.
+ void sayKDHintDigital(const byte *kdTextIndex, uint slot, uint partner);
+
/// _QuitSounds @ 1ff1:03c5.
void stopAll();
diff --git a/engines/eem/graphics.cpp b/engines/eem/graphics.cpp
index 81c553eb35f..c6bd5278563 100644
--- a/engines/eem/graphics.cpp
+++ b/engines/eem/graphics.cpp
@@ -350,23 +350,38 @@ void EEMEngine::doHelp() {
return;
uint16 chosenText = 0xFFFF;
- int soundNum = 0;
+ int soundNum = 0; // _SayKDDigital line: EEM1 chain 10/11; fallback 7/8.
+ int hintVoiceSlot = -1; // London chain hint â _SayKDHintDigital(slot).
bool anyHintDefined = false;
+ // Required-clue chain walk. EEM1 `_KDHelp @ 1560:010a` checks the first two
+ // entries of chain A. EEM2 `_DoKDHelp @ 15c1:020b` walks all THREE chains
+ // (A,B,C @ header words 16-20/21-25/26-30) Ã 5 slots, indexes the 15-entry
+ // hint-text table `hintBlock()[chain*5 + slot]`, and voices a chain hint
+ // with `_SayKDHintDigital(slot)` (table kdTextIndex()+0x3a) instead of
+ // EEM1's `_SayKDDigital(slot + 10)`. Gate is the same: clue not yet found.
+ const uint kChains = isLondon() ? 3u : 1u;
+ const uint kSlots = isLondon() ? Mystery::kChainLen : 2u;
if (hb) {
- for (uint i = 0; i < 2; i++) {
- const uint16 chainClue = _mystery.aChain(i);
- if (chainClue == 0xFFFF)
- continue;
- const uint16 hintOff = READ_LE_UINT16(hb + i * 2);
- if (hintOff == 0xFFFF)
- continue;
- anyHintDefined = true;
- if (chainClue < Mystery::kCluesFoundCap &&
- _mystery._cluesFound[chainClue] == 0) {
- chosenText = hintOff;
- soundNum = (int)i + 10;
- break;
+ for (uint c = 0; c < kChains && chosenText == 0xFFFF; c++) {
+ for (uint slot = 0; slot < kSlots; slot++) {
+ const uint16 chainClue = _mystery.hintChain(c, slot);
+ if (chainClue == 0xFFFF)
+ continue;
+ const uint16 hintOff = READ_LE_UINT16(
+ hb + (c * Mystery::kChainLen + slot) * 2);
+ if (hintOff == 0xFFFF)
+ continue;
+ anyHintDefined = true;
+ if (chainClue < Mystery::kCluesFoundCap &&
+ _mystery._cluesFound[chainClue] == 0) {
+ chosenText = hintOff;
+ if (isLondon())
+ hintVoiceSlot = (int)slot;
+ else
+ soundNum = (int)slot + 10;
+ break;
+ }
}
}
}
@@ -463,9 +478,14 @@ void EEMEngine::doHelp() {
// partner-specific voice line keyed to which hint type fired:
// 10 = first chain hint, 11 = second chain hint,
// 7 = generic KD (first), 8 = generic KD (second).
- if (_audio && _mystery.kdTextIndex() && soundNum > 0)
- _audio->sayKDDigital(_mystery.kdTextIndex(), (uint)soundNum,
- _partner);
+ if (_audio && _mystery.kdTextIndex()) {
+ if (hintVoiceSlot >= 0)
+ _audio->sayKDHintDigital(_mystery.kdTextIndex(),
+ (uint)hintVoiceSlot, _partner);
+ else if (soundNum > 0)
+ _audio->sayKDDigital(_mystery.kdTextIndex(), (uint)soundNum,
+ _partner);
+ }
waitForInput(60000);
}
diff --git a/engines/eem/mystery.h b/engines/eem/mystery.h
index 54830f70e50..1536a61360f 100644
--- a/engines/eem/mystery.h
+++ b/engines/eem/mystery.h
@@ -113,6 +113,20 @@ public:
return i < kChainLen ? _aChain[i] : 0xFFFF;
}
+ /// Entry @p slot of hint chain @p chainIdx (0 = A, 1 = B, 2 = C; header
+ /// words 16-20 / 21-25 / 26-30). EEM2 `_DoKDHelp @ 15c1:020b` walks all
+ /// three chains à 5 slots; EEM1 `_KDHelp` only chain A slots 0..1.
+ uint16 hintChain(uint chainIdx, uint slot) const {
+ if (slot >= kChainLen)
+ return 0xFFFF;
+ switch (chainIdx) {
+ case 0: return _aChain[slot];
+ case 1: return _bChain[slot];
+ case 2: return _cChain[slot];
+ default: return 0xFFFF;
+ }
+ }
+
/// MapData entry for siteNum: 14 bytes; first u16 = sitepic, +4..7 = (x, y).
const byte *mapEntry(uint siteNum) const;
Commit: d7a7f626717147457245f120a62262b1fff064fc
https://github.com/scummvm/scummvm/commit/d7a7f626717147457245f120a62262b1fff064fc
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:28+02:00
Commit Message:
EEM: new doPuzzle implementation for London
Changed paths:
engines/eem/clues.cpp
engines/eem/eem.h
engines/eem/graphics.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 0abed334700..e3c589691ce 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -1130,6 +1130,26 @@ void EEMEngine::displayClue(const byte *clueBlock) {
}
applyClueSideEffects(c);
+
+ // EEM2 `_DisplayClue @ 2542:05bd`: a clue entry can gate the rest of
+ // the clue behind a "check the manual / a real map" puzzle (the id is
+ // entry+0x54 = c+0x50, a `P<id>.BIN`). Run it once per mystery
+ // (`_mystery._solvedPuzzle` = `DAT_3036_6d5c`); a wrong answer aborts
+ // the remaining entries and re-prompts on the next visit.
+ if (isLondon() && !_mystery._solvedPuzzle) {
+ const uint16 puzzleId = READ_LE_UINT16(c + 0x50);
+ if (puzzleId != 0xFFFF) {
+ // Restore the clean site BG (drop this clue's bubble) first â
+ // the DOS _Repaint before `_DoPuzzle` â so the puzzle (and its
+ // own hint) render on a clean background, not over the bubble.
+ g_system->copyRectToScreen(bg.getPixels(), bg.pitch, 0, 0,
+ kScreenWidth, kScreenHeight);
+ g_system->updateScreen();
+ _mystery._solvedPuzzle = doPuzzle(puzzleId);
+ if (!_mystery._solvedPuzzle)
+ break; // gate: block the rest of this clue
+ }
+ }
}
// _StopTheVoice @ 1ff1:0283 effect (voice only, keep MIDI). Diverges
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 162a041f1c4..524c5d1e497 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -208,6 +208,15 @@ public:
/// count followed by 62-byte ClueEntries.
void displayClue(const byte *clueBlock);
+ /// EEM2/London `_DoPuzzle @ 2542:1482`. A clue entry can gate the rest of
+ /// itself behind a "check the manual / a real map" puzzle (the id is the
+ /// entry+0x54 field). Opens `P<id>.BIN` â a header + question, then either
+ /// a typed answer (type 0) or a click-a-region multiple choice (type 1).
+ /// Returns true when answered correctly (and true if the file is missing,
+ /// so the clue is never permanently blocked); a wrong answer replays the
+ /// partner's scolding hint, and the caller re-prompts on the next visit.
+ bool doPuzzle(uint puzzleId);
+
/// Floppy hotspot click. `FUN_22dc_0b80 + FUN_1652_00e6 + FUN_1652_006c`.
/// Locates dialog records in site_data[+6] and dispatches them.
void displayFloppyHotspotDialog(uint siteNum, uint hotIdx);
diff --git a/engines/eem/graphics.cpp b/engines/eem/graphics.cpp
index c6bd5278563..02ed69a8ef7 100644
--- a/engines/eem/graphics.cpp
+++ b/engines/eem/graphics.cpp
@@ -170,6 +170,250 @@ bool EEMEngine::floppyHotspotSearched(uint siteIdx, uint hotspotIdx) const {
_mystery._cluesFound[textIdx] != 0;
}
+// fgets-style line read (until newline / NUL / EOF) from a puzzle file.
+static Common::String readPuzzleLine(Common::File &f) {
+ Common::String s;
+ while (!f.eos()) {
+ const byte c = f.readByte();
+ if (f.eos() || c == '\n' || c == 0)
+ break;
+ if (c != '\r')
+ s += (char)c;
+ }
+ return s;
+}
+
+bool EEMEngine::doPuzzle(uint puzzleId) {
+ // `_DoPuzzle @ 2542:1482`. File `P<id>.BIN` (LE u16 fields):
+ // type(0=typed,1=choice); mainPic{id,x,y}; extraCount; extra{id,x,y}*;
+ // qx; qy; qw; voiceAlt(Jenny); voiceMain(Jake); question line.
+ // type 0: answerRect{x1,y1,x2,y2}; answer line (stored UPPERCASE).
+ // type 1: choiceCount; rects{x1,y1,x2,y2}* â correct = rect 0.
+ Common::File f;
+ const Common::String fname = Common::String::format("P%u.BIN", puzzleId);
+ if (!f.open(Common::Path(fname))) {
+ // Fail open: never let a missing puzzle file permanently block a clue.
+ warning("doPuzzle: %s missing â leaving the clue ungated", fname.c_str());
+ return true;
+ }
+
+ const uint16 type = f.readUint16LE();
+
+ // The caller (displayClue) restored the clean site background, so the
+ // current screen has no clue bubbles. Keep that as `cleanBg` and build the
+ // puzzle on a copy; `cleanBg` is restored after the answer (DOS `_DoPuzzle`
+ // _Repaint) and after the wrong-answer hint (`_KDHelp` _Repaint) so neither
+ // the puzzle pics nor any bubble is left on the background.
+ Graphics::ManagedSurface cleanBg(kScreenWidth, kScreenHeight,
+ Graphics::PixelFormat::createFormatCLUT8());
+ cleanBg.clear();
+ {
+ Graphics::Surface *cur = g_system->lockScreen();
+ if (cur) {
+ cleanBg.simpleBlitFrom(*cur);
+ g_system->unlockScreen();
+ }
+ }
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.simpleBlitFrom(cleanBg);
+
+ // Main picture, then `extraCount` more â each {id, x, y}, masked blit.
+ for (int phase = 0; phase < 2; phase++) {
+ uint16 n = 1;
+ if (phase == 1)
+ n = f.readUint16LE();
+ for (uint16 i = 0; i < n; i++) {
+ const uint16 id = f.readUint16LE();
+ const int16 px = (int16)f.readUint16LE();
+ const int16 py = (int16)f.readUint16LE();
+ Picture pic;
+ if (_picsArchive.getPicture(id, pic) && !pic.surface.empty())
+ scratch.transBlitFrom(pic.surface, Common::Point(px, py),
+ (uint32)(byte)(pic.flags >> 8));
+ }
+ }
+
+ const int16 qx = (int16)f.readUint16LE();
+ const int16 qy = (int16)f.readUint16LE();
+ const int16 qw = (int16)f.readUint16LE();
+ const uint16 voiceAlt = f.readUint16LE(); // partner != Jake
+ const uint16 voiceMain = f.readUint16LE(); // partner == Jake
+ const Common::String question =
+ parseString(readPuzzleLine(f), _playerName, _partner);
+ if (_font.isLoaded() && !question.empty())
+ _font.drawWordWrapped(&scratch, qx, qy, MAX<int>(8, (int)qw),
+ question, 0);
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, kScreenWidth, kScreenHeight);
+ g_system->updateScreen();
+
+ // Partner reads the question (`_SpoolSound(id - 1)`, gated on audio).
+ if (_audio && _voiceOn) {
+ const uint16 v = (_partner == kPartnerJake) ? voiceMain : voiceAlt;
+ if (v != 0 && v != 0xFFFF)
+ _audio->spoolSound((uint)(v - 1));
+ }
+
+ bool correct = false;
+ CursorMan.showMouse(true);
+
+ if (type == 0) {
+ // Typed answer (`_CheckTypedAnswer` â `_GetNameString`, toupper+strcmp).
+ const int16 ax1 = (int16)f.readUint16LE();
+ const int16 ay1 = (int16)f.readUint16LE();
+ const int16 ax2 = (int16)f.readUint16LE();
+ const int16 ay2 = (int16)f.readUint16LE();
+ const Common::Rect rect(ax1, ay1, ax2, ay2);
+ Common::String answer = readPuzzleLine(f); // stored uppercase
+ const uint maxLen = answer.size() + 2;
+
+ Common::String input;
+ bool done = false, blink = true;
+ uint32 blinkMs = g_system->getMillis();
+ g_system->setFeatureState(OSystem::kFeatureVirtualKeyboard, true);
+ while (!done && !shouldQuit()) {
+ Graphics::ManagedSurface fld(kScreenWidth, kScreenHeight,
+ Graphics::PixelFormat::createFormatCLUT8());
+ fld.simpleBlitFrom(scratch);
+ Common::String shown = input;
+ if (blink)
+ shown += "_";
+ if (_font.isLoaded())
+ _font.drawString(&fld, shown, rect.left + 2, rect.top + 1,
+ MAX<int>(8, rect.width()), 0x0F);
+ g_system->copyRectToScreen(fld.getPixels(), fld.pitch, 0, 0,
+ kScreenWidth, kScreenHeight);
+ g_system->updateScreen();
+
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ input.clear();
+ done = true;
+ break;
+ }
+ if (ev.type != Common::EVENT_KEYDOWN)
+ continue;
+ const Common::KeyCode k = ev.kbd.keycode;
+ if (k == Common::KEYCODE_RETURN || k == Common::KEYCODE_KP_ENTER) {
+ if (!input.empty())
+ done = true;
+ } else if (k == Common::KEYCODE_ESCAPE) {
+ input.clear(); // cancel â empty â fails the compare
+ done = true;
+ } else if (k == Common::KEYCODE_BACKSPACE) {
+ if (!input.empty())
+ input.deleteLastChar();
+ } else if (ev.kbd.ascii >= ' ' && ev.kbd.ascii < 127 &&
+ input.size() < maxLen) {
+ input += (char)ev.kbd.ascii;
+ }
+ }
+ const uint32 now = g_system->getMillis();
+ if (now - blinkMs >= 400) {
+ blink = !blink;
+ blinkMs = now;
+ }
+ g_system->delayMillis(15);
+ }
+ g_system->setFeatureState(OSystem::kFeatureVirtualKeyboard, false);
+ input.toUppercase();
+ correct = input.equals(answer);
+ } else {
+ // Multiple choice: click a region; correct = the FIRST rect.
+ const uint16 count = f.readUint16LE();
+ Common::Array<Common::Rect> rects;
+ for (uint16 i = 0; i < count; i++) {
+ const int16 cx1 = (int16)f.readUint16LE();
+ const int16 cy1 = (int16)f.readUint16LE();
+ const int16 cx2 = (int16)f.readUint16LE();
+ const int16 cy2 = (int16)f.readUint16LE();
+ rects.push_back(Common::Rect(cx1, cy1, cx2, cy2));
+ }
+ int picked = -1;
+ while (picked == -1 && !shouldQuit()) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER ||
+ (ev.type == Common::EVENT_KEYDOWN &&
+ ev.kbd.keycode == Common::KEYCODE_ESCAPE)) {
+ picked = -2; // ESC / quit â wrong
+ break;
+ }
+ if (ev.type == Common::EVENT_LBUTTONDOWN) {
+ for (uint i = 0; i < rects.size(); i++) {
+ if (rects[i].contains(ev.mouse.x, ev.mouse.y)) {
+ picked = (int)i;
+ break;
+ }
+ }
+ }
+ }
+ g_system->updateScreen();
+ g_system->delayMillis(15);
+ }
+ correct = (picked == 0);
+ }
+ f.close();
+
+ // _Repaint after the answer: drop the puzzle pics back to the clean site.
+ g_system->copyRectToScreen(cleanBg.getPixels(), cleanBg.pitch, 0, 0,
+ kScreenWidth, kScreenHeight);
+ g_system->updateScreen();
+
+ if (!correct && !shouldQuit()) {
+ // Wrong: partner scolds via the KD hint (`_MIDIPlay(0x28)` +
+ // `_KDHelp(TextBlock + KDTextIndex[+0xc], voice 6)`), drawn on the
+ // clean site (the puzzle has already been _Repaint-ed away).
+ const byte *kd = _mystery.kdTextIndex();
+ const uint16 hintOff = kd ? READ_LE_UINT16(kd + 0x0c) : 0xFFFF;
+ if (hintOff != 0xFFFF) {
+ Common::String hint =
+ parseString(_mystery.textAt(hintOff), _playerName, _partner);
+ Graphics::ManagedSurface ms(kScreenWidth, kScreenHeight,
+ Graphics::PixelFormat::createFormatCLUT8());
+ ms.simpleBlitFrom(cleanBg);
+ const byte firstChar = hint.empty() ? (byte)0 : (byte)hint[0];
+ uint16 bubNum = getKDTextBalloon(firstChar);
+ if (firstChar >= '0' && firstChar <= '9')
+ hint.deleteChar(0);
+ bubNum = fitBalloonToText(bubNum, hint);
+ Picture balloon;
+ const bool haveBalloon = _balloonArchive.size() > (bubNum & 0x7F) &&
+ _balloonArchive.loadEntry(bubNum & 0x7F, balloon);
+ const int balloonX = 0x21;
+ int balloonY = 1;
+ if (haveBalloon && balloon.surface.h < 0x4e)
+ balloonY = (0x50 - balloon.surface.h) / 2;
+ if (haveBalloon)
+ ms.transBlitFrom(balloon.surface,
+ Common::Point(balloonX, balloonY),
+ (uint32)(byte)(balloon.flags >> 8));
+ uint16 tx = 5, ty = 4, tw = 155;
+ getBalloonInsets(bubNum, tx, ty, tw);
+ if (_font.isLoaded())
+ _font.drawWordWrapped(&ms, balloonX + tx, balloonY + ty, tw,
+ hint, haveBalloon ? 0 : 0xF);
+ g_system->copyRectToScreen(ms.getPixels(), ms.pitch, 0, 0,
+ kScreenWidth, kScreenHeight);
+ g_system->updateScreen();
+ if (_audio && _voiceOn && kd)
+ _audio->sayKDDigital(kd, 6, _partner);
+ waitForInput(60000);
+
+ // `_KDHelp` ends with _Repaint: clear the hint balloon too.
+ g_system->copyRectToScreen(cleanBg.getPixels(), cleanBg.pitch,
+ 0, 0, kScreenWidth, kScreenHeight);
+ g_system->updateScreen();
+ }
+ }
+ setInteractiveMouseCursor(false);
+ return correct;
+}
+
void EEMEngine::doHelp() {
// Floppy per-mystery H<n>.BIN hint files. Loader FUN_1503_0001 @ 1503:0001
// (format string "h%d.bin" @ 2608:0154), consumer FUN_1503_01a5 @ 1503:01a5.
Commit: a029ae2241c3e5b70231e488f8a5fc66452724c3
https://github.com/scummvm/scummvm/commit/a029ae2241c3e5b70231e488f8a5fc66452724c3
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:29+02:00
Commit Message:
EEM: doPuzzle highlights for London
Changed paths:
engines/eem/graphics.cpp
engines/eem/site.cpp
engines/eem/site.h
diff --git a/engines/eem/graphics.cpp b/engines/eem/graphics.cpp
index 02ed69a8ef7..ec1f5d089ce 100644
--- a/engines/eem/graphics.cpp
+++ b/engines/eem/graphics.cpp
@@ -34,6 +34,7 @@
#include "eem/audio.h"
#include "eem/detection.h"
#include "eem/eem.h"
+#include "eem/site.h"
namespace EEM {
@@ -332,8 +333,55 @@ bool EEMEngine::doPuzzle(uint puzzleId) {
const int16 cy2 = (int16)f.readUint16LE();
rects.push_back(Common::Rect(cx1, cy1, cx2, cy2));
}
+ // `_GetPuzzleChoice @ 2542:11a9`: the choice rects are the only
+ // clickable areas (no done/exit hotspot) and ESC = wrong; the Tab/arrow
+ // keys hit a global handler, not choice selection. Highlight the
+ // options two ways, both reusing the existing engine mechanisms:
+ // * Boxes: outline each option in the marching-ants ramp â palette
+ // 0xF9..0xFE, the SAME original yellow as the site hotspots, not a
+ // puzzle-specific colour. Gated like the DOS `_DrawRect`
+ // (`DAT_3036_4c4a` â port `hide_highlight_boxes`).
+ // * Cursor: recolor the default arrow over an option
+ // (`setInteractiveMouseCursor`, the EEM1 red-pixel cursor for
+ // "otherwise invisible" hotspots). These options carry no per-hotspot
+ // cursor shape, so the default cursor is what's shown â exactly the
+ // case that recolor is meant for.
+ applyHotspotGlowPalette();
+ const bool showBoxes = !ConfMan.getBool("hide_highlight_boxes");
int picked = -1;
+ uint32 lastPhase = (uint32)-1;
+ bool overOption = false;
while (picked == -1 && !shouldQuit()) {
+ if (showBoxes) {
+ const uint32 phase = g_system->getMillis() / 80;
+ if (phase != lastPhase) {
+ lastPhase = phase;
+ Graphics::ManagedSurface fr(kScreenWidth, kScreenHeight,
+ Graphics::PixelFormat::createFormatCLUT8());
+ fr.simpleBlitFrom(scratch);
+ for (uint i = 0; i < rects.size(); i++) {
+ const byte color =
+ (byte)(0xF9 + ((i + phase) & 0x07) % 6);
+ fr.frameRect(rects[i], color);
+ }
+ g_system->copyRectToScreen(fr.getPixels(), fr.pitch, 0, 0,
+ kScreenWidth, kScreenHeight);
+ }
+ }
+ // Red default-cursor while hovering a clickable option; plain arrow
+ // off all of them (mirrors `updateHotspotCursor`).
+ const Common::Point mp = g_system->getEventManager()->getMousePos();
+ bool nowOver = false;
+ for (uint i = 0; i < rects.size(); i++) {
+ if (rects[i].contains(mp.x, mp.y)) {
+ nowOver = true;
+ break;
+ }
+ }
+ if (nowOver != overOption) {
+ overOption = nowOver;
+ setInteractiveMouseCursor(nowOver);
+ }
Common::Event ev;
while (g_system->getEventManager()->pollEvent(ev)) {
if (ev.type == Common::EVENT_QUIT ||
@@ -355,6 +403,8 @@ bool EEMEngine::doPuzzle(uint puzzleId) {
g_system->updateScreen();
g_system->delayMillis(15);
}
+ // Restore the plain arrow before leaving the puzzle.
+ setInteractiveMouseCursor(false);
correct = (picked == 0);
}
f.close();
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 5dba04a5f01..7c4884ac7d6 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -179,6 +179,23 @@ void cyclePaletteRangeReverse(uint8 start, uint8 end) {
g_system->getPaletteManager()->setPalette(buf, start, count);
}
+void applyHotspotGlowPalette() {
+ // SITEPALS ships palette 0xF9..0xFE as uniform yellow (3F 3E 00), so the
+ // original marching-ants was a placeholder. Override with a 6-step yellow
+ // ramp so `cyclePaletteRange(0xF9, 0xFE)` (and the per-index draw colour)
+ // produce a visible pulse. Shared by site hotspots and the clue puzzle so
+ // both highlight clickable areas with the same (original) colours.
+ static const byte kAntsGlow[6 * 3] = {
+ 0x40, 0x40, 0x00, // F9 â dim
+ 0x80, 0x80, 0x00, // FA
+ 0xC0, 0xC0, 0x00, // FB
+ 0xFF, 0xFF, 0x40, // FC â peak
+ 0xC0, 0xC0, 0x00, // FD
+ 0x80, 0x80, 0x00, // FE
+ };
+ g_system->getPaletteManager()->setPalette(kAntsGlow, 0xF9, 6);
+}
+
// `_WaitAnims @ 29be:021c`. 12 bytes per entry, indexed by `siteData[+8]`:
// +0..1 anim Jake, +2..3 anim Jenny,
// +4..5 x Jake, +6..7 x Jenny,
@@ -994,21 +1011,9 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
}
_vm->setSitePaletteForSite(sitepic);
- // SITEPALS ships palette 0xF9..0xFE as uniform yellow (3F 3E 00),
- // so original marching-ants was a placeholder. Override with a
- // 6-step yellow ramp so `cyclePaletteRange(0xF9, 0xFE)` produces a
- // visible pulse on unsearched hotspots.
- {
- static const byte kAntsGlow[6 * 3] = {
- 0x40, 0x40, 0x00, // F9 â dim
- 0x80, 0x80, 0x00, // FA
- 0xC0, 0xC0, 0x00, // FB
- 0xFF, 0xFF, 0x40, // FC â peak
- 0xC0, 0xC0, 0x00, // FD
- 0x80, 0x80, 0x00, // FE
- };
- g_system->getPaletteManager()->setPalette(kAntsGlow, 0xF9, 6);
- }
+ // Override SITEPALS' uniform-yellow 0xF9..0xFE with the marching-ants
+ // glow ramp (shared with the clue puzzle).
+ applyHotspotGlowPalette();
renderBackground(siteNum);
diff --git a/engines/eem/site.h b/engines/eem/site.h
index c91f0cffeb8..9bdbc88aa4a 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -82,6 +82,12 @@ void cyclePaletteRange(uint8 start, uint8 end);
/// where the cycle shifts ENDâSTART rather than STARTâEND.
void cyclePaletteRangeReverse(uint8 start, uint8 end);
+/// Load the 6-step yellow marching-ants ramp into palette 0xF9..0xFE
+/// (SITEPALS ships these as uniform yellow). Shared by site hotspots and the
+/// clue puzzle so both outline clickable areas in the same original colours;
+/// `cyclePaletteRange(0xF9, 0xFE)` then pulses them.
+void applyHotspotGlowPalette();
+
/// One hotspot (search rectangle) within a site, 14 bytes on disk.
struct Hotspot {
int16 x1, y1, x2, y2; ///< rectangle in screen coordinates
Commit: c53f9e018e04683f0979fa72eb33fcc6dc825acc
https://github.com/scummvm/scummvm/commit/c53f9e018e04683f0979fa72eb33fcc6dc825acc
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:29+02:00
Commit Message:
EEM: acusation workflow for London
Changed paths:
engines/eem/mystery.cpp
engines/eem/mystery.h
engines/eem/ui.cpp
diff --git a/engines/eem/mystery.cpp b/engines/eem/mystery.cpp
index edf8261849f..715971eb77c 100644
--- a/engines/eem/mystery.cpp
+++ b/engines/eem/mystery.cpp
@@ -571,6 +571,61 @@ int Mystery::selectedPoints() const {
return total;
}
+bool Mystery::londonSolved() const {
+ // `_SolvedCheck @ 1ea1:0b1a`. Reuses the already-loaded hint chains
+ // (_aChain/_bChain/_cChain) as the three answer sets and the shared
+ // _noteSelected accuse-selection array â see londonSolved() doc in the
+ // header. EEM1/floppy keep the points model via solvedCheck().
+ for (uint chain = 0; chain < 3; chain++) {
+ int remaining = (int)kChainLen;
+ int wild = 0;
+ for (uint slot = 0; slot < kChainLen; slot++) {
+ const uint16 clue = hintChain(chain, slot);
+ if (clue == 0xFFFF) {
+ remaining--;
+ wild++;
+ } else if (clue < kCluesFoundCap && _noteSelected[clue]) {
+ remaining--;
+ }
+ }
+ if (wild == (int)kChainLen)
+ continue; // all-wildcard set is unused
+ if (remaining == 0)
+ return true; // every slot wildcard-or-selected
+ }
+ return false;
+}
+
+int Mystery::minCluesRemaining() const {
+ // `_GetMinRemaining @ 1ea1:1056`. Parallel to londonSolved() but tests the
+ // three answer sets against _cluesFound (discovered in the world). Tracks
+ // whether any non-wildcard answer clue has been found at all; if none, the
+ // player has nothing relevant yet (returns kChainLen).
+ int best = (int)kChainLen;
+ int foundReal = 0;
+ for (uint chain = 0; chain < 3; chain++) {
+ int remaining = (int)kChainLen;
+ int wild = 0;
+ for (uint slot = 0; slot < kChainLen; slot++) {
+ const uint16 clue = hintChain(chain, slot);
+ if (clue == 0xFFFF) {
+ remaining--;
+ wild++;
+ } else if (clue < kCluesFoundCap && _cluesFound[clue]) {
+ remaining--;
+ foundReal++;
+ }
+ }
+ if (wild == (int)kChainLen)
+ remaining = (int)kChainLen; // unused set must not lower the min
+ if (remaining < best)
+ best = remaining;
+ }
+ if (foundReal == 0)
+ return (int)kChainLen;
+ return best;
+}
+
int Mystery::foundPoints() const {
const byte *ni = noteIndex();
const uint16 cnt = noteIndexCount();
diff --git a/engines/eem/mystery.h b/engines/eem/mystery.h
index 1536a61360f..3793debedec 100644
--- a/engines/eem/mystery.h
+++ b/engines/eem/mystery.h
@@ -167,6 +167,23 @@ public:
bool solvedCheck() const { return selectedPoints() > 99; }
+ /// EEM2/London `_SolvedCheck @ 1ea1:0b1a`. London dropped EEM1's points
+ /// model for set matching: the three hint chains (_aChain/_bChain/_cChain,
+ /// = Ghidra g_dwSolveSet1..3, mystery header words 16/21/26) are the
+ /// accepted answer sets of up to 5 clue ids. A set is satisfied when every
+ /// slot is either 0xFFFF (wildcard / unused) or has its accuse-selected
+ /// flag (_noteSelected, = g_awSelectedClue) set; the accusation is solved
+ /// if ANY set is satisfied. An all-wildcard set is unused and never counts.
+ bool londonSolved() const;
+
+ /// EEM2/London `_GetMinRemaining @ 1ea1:1056`. Same three sets as
+ /// londonSolved() but tested against _cluesFound (clues discovered in the
+ /// world, = _CluesFound): returns the fewest clues still to find across the
+ /// usable sets â 0 means a full answer set has been discovered and the
+ /// accusation can begin. Returns 5 when no relevant clue has been found yet.
+ /// Drives the London accuse-readiness gate (`_AccuseEntry @ 1ea1:115c`).
+ int minCluesRemaining() const;
+
/// _WITCH @ 1df2:089f. GalleryData[i*0x46 + 0x02] == 0xFFFF marks the
/// guilty suspect; innocent suspects store their alibi TextBlock offset.
bool isGuilty(uint suspectIdx) const;
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index ed11bb57e91..2de366f5994 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -4177,32 +4177,58 @@ void EEMEngine::doAccuse() {
const byte *entryKdIdx = _mystery.kdTextIndex();
if (!entryKdIdx)
return;
- const int foundPoints = _mystery.foundPoints();
+ // Readiness tier (0 nothing yet .. 3 ready-to-accuse). EEM1 `_AccuseEntry
+ // @ 1df2:0ff8` tiers on the top-5 found-clue score; EEM2 `_AccuseEntry @
+ // 1ea1:115c` tiers on minCluesRemaining() (jumptable @ 1ea1:1333: 0 ->
+ // ready, 1 -> almost, 2..4 -> keep looking, 5 -> nothing yet â only the 0
+ // case sets the can-accuse flag). Both variants then pick the same four KD
+ // lines, so map onto one shared tier.
+ int readyTier;
+ if (isLondon()) {
+ const int minRem = _mystery.minCluesRemaining();
+ readyTier = (minRem >= (int)Mystery::kChainLen) ? 0
+ : (minRem >= 2) ? 1
+ : (minRem == 1) ? 2
+ : 3;
+ } else {
+ const int foundPoints = _mystery.foundPoints();
+ readyTier = (foundPoints == 0) ? 0
+ : (foundPoints < 0x32) ? 1
+ : (foundPoints < 0x65) ? 2
+ : 3;
+ }
+
uint entryKDSpeak = 0;
uint16 entryTextOff = 0xFFFF;
uint16 entryVoiceOverride = 0xFFFF;
bool canAccuse = false;
Common::String entryText;
- if (foundPoints == 0) {
+ switch (readyTier) {
+ case 0:
entryKDSpeak = 9;
entryText = "3We're not ready to solve this mystery yet. "
"Let's keep investigating until we have some more solid "
"evidence to make our case!";
// Practice mystery M0 ships the matching ZeroText takes as the
// final two SDB entries, but its KD digital table points kdspeak 9
- // at earlier hint clips. Use the otherwise unreferenced pair.
- if (_mystery.number() == 0)
+ // at earlier hint clips. Use the otherwise unreferenced pair. EEM1
+ // SDB indices, so EEM1 only.
+ if (!isLondon() && _mystery.number() == 0)
entryVoiceOverride = (_partner == kPartnerJake) ? 105 : 104;
- } else if (foundPoints < 0x32) {
+ break;
+ case 1:
entryKDSpeak = 0;
entryTextOff = READ_LE_UINT16(entryKdIdx + 0);
- } else if (foundPoints < 0x65) {
+ break;
+ case 2:
entryKDSpeak = 1;
entryTextOff = READ_LE_UINT16(entryKdIdx + 2);
- } else {
+ break;
+ default:
entryKDSpeak = 2;
entryTextOff = READ_LE_UINT16(entryKdIdx + 4);
canAccuse = true;
+ break;
}
if (entryText.empty() && entryTextOff != 0xFFFF) {
entryText = parseString(_mystery.textAt(entryTextOff),
@@ -4280,8 +4306,12 @@ void EEMEngine::doAccuse() {
return;
}
- // `_DoAccuse @ 1df2:0c75` â `_SolvedCheck` gate.
- if (!_mystery.solvedCheck()) {
+ // `_DoAccuse @ 1df2:0c75` (EEM1) / `1ea1:0c75` (EEM2) â `_SolvedCheck` gate.
+ // London matches the selected notes against the answer sets; EEM1/floppy
+ // sum clue points.
+ const bool accuseSolved =
+ isLondon() ? _mystery.londonSolved() : _mystery.solvedCheck();
+ if (!accuseSolved) {
const byte *kdIdx = _mystery.kdTextIndex();
const int16 hintOff = kdIdx
? (int16)READ_LE_UINT16(kdIdx + 6)
@@ -4297,7 +4327,12 @@ void EEMEngine::doAccuse() {
hint = "Necesitamos buscar pistas antes de resolver "
"el misterio. Investiguemos un poco mas!";
} else {
- hint = (_mystery.selectedPoints() == 0)
+ // "nothing yet" vs "almost": London has no points, so judge by
+ // whether any answer clue has been found (minCluesRemaining).
+ const bool nothingYet = isLondon()
+ ? _mystery.minCluesRemaining() >= (int)Mystery::kChainLen
+ : _mystery.selectedPoints() == 0;
+ hint = nothingYet
? "We're not ready to solve this mystery yet. "
"Let's keep investigating until we have some "
"more solid evidence."
Commit: aca47008bdbc7a8f04dc57c736f9fa6fcd21cf22
https://github.com/scummvm/scummvm/commit/aca47008bdbc7a8f04dc57c736f9fa6fcd21cf22
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:30+02:00
Commit Message:
EEM: doApproach fixes for London
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 2de366f5994..a099e78f2fd 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -148,14 +148,18 @@ bool loadLondonApproachData(uint16 approachId, LondonApproachData &out) {
const uint16 pageCount = READ_LE_UINT16(data.data() + 14);
out.pages.clear();
uint32 pos = 16;
+ // `_DoApproach @ 1717:009b` reads the pages with `_fgets` into 255-byte
+ // slots, so each page is one NEWLINE-terminated record â NOT NUL-separated.
+ // (A*.BIN carries no NULs; splitting on '\0' lumps every page into page 0,
+ // which is why the Next/Prev arrows had nothing to page through.)
for (uint16 i = 0; i < pageCount && pos < size; i++) {
const uint32 start = pos;
- while (pos < size && data[pos] != 0)
+ while (pos < size && data[pos] != '\n')
pos++;
out.pages.push_back(Common::String((const char *)data.data() + start,
pos - start));
if (pos < size)
- pos++;
+ pos++; // skip the '\n' separator
}
return out.videoId != 0 && !out.pages.empty();
}
@@ -3469,7 +3473,6 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
const Common::Rect textRect =
data.textRect.findIntersectingRect(Common::Rect(kScreenWidth, kScreenHeight));
if (!textRect.isEmpty()) {
- scratch.fillRect(textRect, 0);
if (page < data.pages.size()) {
Common::Array<Common::String> wrapped;
_font.wordWrapText(data.pages[page], MAX<int>(8, textRect.width()),
@@ -3477,9 +3480,15 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
const int lineH = _font.getFontHeight();
const int maxLines = MAX<int>(1, textRect.height() / lineH);
for (uint i = 0; i < wrapped.size() && (int)i < maxLines; i++) {
+ // `_DoApproach @ 1717:029e`:
+ // `_WordWrap(x, y, w, page, fontColor=1, dropColor=-1)`.
+ // Font colour is palette index 1 (NOT 0x0F white), drawn
+ // straight onto the restored video-frame background â the
+ // DOS `vga_fvidvid` calls just restore the clean bg, they
+ // don't paint a panel, so don't fill the rect here either.
_font.drawString(&scratch, wrapped[i], textRect.left,
textRect.top + (int)i * lineH,
- textRect.width(), 0x0f);
+ textRect.width(), 1);
}
}
}
@@ -3562,6 +3571,10 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
else
setSitePalette(0x3b);
bool done = false;
+ // `_DoApproach` idle loop (1717:02d4) shimmers palette 0xF3..0xF7 via
+ // `_ColorCycle(0xf3, 0xf7)` each frame-rate tick; mirror it on the video
+ // palette (the diff-animation ramp those entries belong to).
+ uint32 lastShimmer = g_system->getMillis();
while (!shouldQuit() && !done) {
Common::Event ev;
while (g_system->getEventManager()->pollEvent(ev)) {
@@ -3630,6 +3643,13 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
}
}
}
+ if (haveVideo) {
+ const uint32 now = g_system->getMillis();
+ if (now - lastShimmer >= 70) {
+ lastShimmer = now;
+ cyclePaletteRange(0xF3, 0xF7);
+ }
+ }
g_system->updateScreen();
g_system->delayMillis(10);
}
Commit: b749d688e8933fc49220856b57a7ebec294af6e5
https://github.com/scummvm/scummvm/commit/b749d688e8933fc49220856b57a7ebec294af6e5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:30+02:00
Commit Message:
EEM: enter the name screen fixes for London
Changed paths:
engines/eem/eem.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 660c2ffe547..7fe9458d44f 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -1285,7 +1285,10 @@ void EEMEngine::showLondonCharSelect() {
const Common::Rect kMaleBox(110, 116, 120, 122);
const Common::Rect kFemaleBox(190, 116, 200, 122);
const uint kMaxFirst = 12, kMaxLast = 20;
- const uint8 kInkColor = 0x0F; // typed-name ink
+ // `_GetNameString @ 1cd3:0ddc` draws both the typed name and the caret in
+ // colour 0x22 (the passport-field ink); the old 0x0F was a guess and the
+ // underscore caret in it was effectively invisible.
+ const uint8 kInkColor = 0x22; // typed-name ink + caret
Picture bg;
const bool haveBg = _picsArchive.getPicture(0xc, bg) && !bg.surface.empty();
@@ -1334,18 +1337,27 @@ void EEMEngine::showLondonCharSelect() {
if (haveBg)
scratch.simpleBlitFrom(bg.surface);
if (getFont().isLoaded()) {
- Common::String f = first;
- if (field == kFieldFirst && blink)
- f += "_";
- Common::String l = last;
- if (field == kFieldLast && blink)
- l += "_";
- getFont().drawString(&scratch, f, kFirstRect.left + 2,
+ getFont().drawString(&scratch, first, kFirstRect.left + 2,
kFirstRect.top + 1, kFirstRect.width(),
kInkColor);
- getFont().drawString(&scratch, l, kLastRect.left + 2,
+ getFont().drawString(&scratch, last, kLastRect.left + 2,
kLastRect.top + 1, kLastRect.width(),
kInkColor);
+ // Caret = solid block `_FillRect(x+1, y, 6, 0xb, 0x22)` at the
+ // input point (`_GetNameString @ 1cd3:0ddc`), NOT an underscore.
+ if (blink && (field == kFieldFirst || field == kFieldLast)) {
+ const Common::Rect &fr =
+ (field == kFieldFirst) ? kFirstRect : kLastRect;
+ const Common::String &buf =
+ (field == kFieldFirst) ? first : last;
+ const int caretX =
+ fr.left + 2 + getFont().getStringWidth(buf);
+ Common::Rect caret(caretX, fr.top, caretX + 6,
+ fr.top + 0xb);
+ caret.clip(Common::Rect(kScreenWidth, kScreenHeight));
+ if (!caret.isEmpty())
+ scratch.fillRect(caret, kInkColor);
+ }
}
if (field == kFieldGender) {
// DOS restores both boxes, then fills the active one with the
@@ -1440,6 +1452,11 @@ void EEMEngine::showLondonCharSelect() {
blink = !blink;
needRedraw = true;
}
+ // Flush every frame so the mouse cursor tracks smoothly. The scratch
+ // rebuild + copyRectToScreen above is gated on needRedraw (and mouse
+ // motion doesn't set it), but the cursor is only re-composited by
+ // updateScreen(), so without this it moved only on blink/keypress.
+ g_system->updateScreen();
g_system->delayMillis(15);
}
if (shouldQuit()) {
Commit: f490186308e1a088e423ab6dfd60b06cca7bd2ec
https://github.com/scummvm/scummvm/commit/f490186308e1a088e423ab6dfd60b06cca7bd2ec
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:30+02:00
Commit Message:
EEM: make sure the screen is updated during animations
Changed paths:
engines/eem/clues.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index e3c589691ce..2ed40630967 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -431,6 +431,10 @@ void EEMEngine::playLondonInitCluesAnim(uint16 caseType, const Picture &bg,
break;
}
}
+ // Re-composite the cursor every ~10 ms so the mouse stays smooth
+ // between the 140 ms animation frames (the frame-rebuild above is
+ // throttled, but the cursor is only flushed by updateScreen()).
+ g_system->updateScreen();
g_system->delayMillis(10);
}
}
@@ -514,6 +518,7 @@ void EEMEngine::playCdFloppyInitCluesAnim(uint16 caseType, bool floppy,
break;
}
}
+ g_system->updateScreen(); // keep the cursor smooth between frames
g_system->delayMillis(10);
}
}
@@ -648,6 +653,7 @@ void EEMEngine::playCdFloppyInitCluesAnim(uint16 caseType, bool floppy,
break;
}
}
+ g_system->updateScreen(); // keep the cursor smooth between frames
g_system->delayMillis(10);
}
}
Commit: e9c44065c0f45b0f9724158370b40917044c51ef
https://github.com/scummvm/scummvm/commit/e9c44065c0f45b0f9724158370b40917044c51ef
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:31+02:00
Commit Message:
EEM: added missing NPC in London cases intro
Changed paths:
engines/eem/clues.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 2ed40630967..68255504730 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -382,6 +382,27 @@ void EEMEngine::playLondonInitCluesAnim(uint16 caseType, const Picture &bg,
const bool haveAnim =
_aniArchive.loadAnimation(introAni, anim) && !anim.empty();
+ // `_DoInitClues @ 1abf:03b3` registers a SECOND, fixed briefing character
+ // (e.g. Nigel) on the LEFT, gated on caseType (jumptable @ CS:0x720):
+ // caseType 0 -> _GetAnimation(0x70) @ (0, 0x3e)
+ // caseType 2 -> _GetAnimation(0x0e) @ (0, 0x36)
+ // caseType 3 -> _GetAnimation(0x74) @ (0, 0x1a)
+ // caseType 1 and >= 4 -> partner only (no second character).
+ // Both animate during the entrance and the last frame is left on screen, so
+ // the following `displayClue` snapshot keeps them through the briefing
+ // dialogue. Without this the NPC is never drawn (Nigel invisible in M2).
+ uint npcAni = 0;
+ int npcX = 0, npcY = 0;
+ switch (caseType) {
+ case 0: npcAni = 0x70; npcY = 0x3e; break;
+ case 2: npcAni = 0x0e; npcY = 0x36; break;
+ case 3: npcAni = 0x74; npcY = 0x1a; break;
+ default: break;
+ }
+ Animation npc;
+ const bool haveNpc = npcAni != 0 &&
+ _aniArchive.loadAnimation(npcAni, npc) && !npc.empty();
+
// _AllBlack(): blank the palette so the _FadeIn after frame 0 reveals the
// briefing (palette 0x39 â EEM2's 63-entry SITEPALS. shifts EEM1's UI set).
byte pal[kPalSize];
@@ -397,18 +418,26 @@ void EEMEngine::playLondonInitCluesAnim(uint16 caseType, const Picture &bg,
// keeps this faithful if the cells/script ever diverge and matches the map/
// site partner rendering. One pass = one script cell per ~140 ms tick.
bool skip = false;
- const uint frames = haveAnim ? (uint)anim.size() : 1;
+ uint frames = haveAnim ? (uint)anim.size() : 1;
+ if (haveNpc)
+ frames = MAX<uint>(frames, (uint)npc.size());
for (uint frame = 0; frame < frames && !shouldQuit() && !skip; frame++) {
if (haveBriefingBg)
blitAt(bg, 0, 0);
- if (haveAnim) {
- const uint cell =
- partnerFrameAtTick(0x18, (uint)anim.size(), frame * 140);
- Graphics::Surface *scr = g_system->lockScreen();
- if (scr) {
+ Graphics::Surface *scr = g_system->lockScreen();
+ if (scr) {
+ if (haveAnim) {
+ const uint cell =
+ partnerFrameAtTick(0x18, (uint)anim.size(), frame * 140);
blitAnimFrameAnchored(scr, anim[cell], kAnchorX, kAnchorY);
- g_system->unlockScreen();
}
+ if (haveNpc) {
+ // NPC frame script = 0x0e (the `_NewAnimation` animId arg).
+ const uint ncell =
+ partnerFrameAtTick(0x0e, (uint)npc.size(), frame * 140);
+ blitAnimFrameAnchored(scr, npc[ncell], npcX, npcY);
+ }
+ g_system->unlockScreen();
}
if (frame == 0 && havePal)
fadePaletteFromBlack(pal); // _FadeIn @ 1abf:03b3
@@ -991,6 +1020,18 @@ void EEMEngine::displayClue(const byte *clueBlock) {
if (_picsArchive.getPicture(charPicId, charPic) &&
charX < kScreenWidth && charY < kScreenHeight) {
blitMaskedToScreen(charPic, charX, charY);
+ // `_DisplayClue` bakes the speaker portrait into the BACKGROUND
+ // via `_AddPicBackground`; the DOS never restores a clean BG
+ // mid-dialogue, so the portrait persists across the following
+ // clue entries. Bake it into the `bg` snapshot too â otherwise
+ // the per-entry full restore (top of this loop) wipes a fixed
+ // NPC portrait (e.g. London's Nigel) on every entry that carries
+ // no portrait of its own, leaving only the balloon ("rendered,
+ // then refreshed over").
+ const byte transp = (byte)(charPic.flags >> 8);
+ bg.transBlitFrom(charPic.surface,
+ Common::Point((int)charX, (int)charY),
+ (uint32)transp);
}
}
Commit: 2372aaf0180a1be5ae58f68786fd8e4cc1ad9cdb
https://github.com/scummvm/scummvm/commit/2372aaf0180a1be5ae58f68786fd8e4cc1ad9cdb
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:31+02:00
Commit Message:
EEM: implement setup screen in London
Changed paths:
engines/eem/clues.cpp
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/ui.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 68255504730..b49c45539db 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -977,7 +977,7 @@ void EEMEngine::displayClue(const byte *clueBlock) {
// `stop()`). 0 = no cue; EEM1 clue entries have no such field. Gated on
// `_voiceOn` like `startTravelMusic` â the DOS gate is the separate
// music-on / MIDI-available flags (DAT_3036_4cc0 && DAT_2bca_146a).
- if (isLondon() && _music && _voiceOn) {
+ if (isLondon() && _music && _musicOn) {
const uint16 clueMusic = READ_LE_UINT16(c + 0x1c);
if (clueMusic != 0)
_music->playMus(clueMusic, /* loop= */ false);
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 7fe9458d44f..4f455d22562 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -661,9 +661,13 @@ screenLoop:
break;
case kScreenSetup:
- // Handler 6 @ 1a35:0e48 -> _DoSetup @ 1f78:044e. Entered
- // from BigMap setup button (_NextScreen=6 @ 20fe:0c33).
- doSetup();
+ // Handler 6 @ 1a35:0e48 -> _DoSetup. EEM1 = seg-1f78 (`doSetup`);
+ // EEM2/London = seg-2046 (`doSetupLondon`, 4 toggles + own layout).
+ // Entered from BigMap setup button (_NextScreen=6 @ 20fe:0c33).
+ if (isLondon())
+ doSetupLondon();
+ else
+ doSetup();
break;
case kScreenProfile:
@@ -1578,7 +1582,8 @@ void EEMEngine::startTravelMusic() {
// doesn't write it; combined with `_DoSiteLoop @ 168d:06c0` calling
// `_StopMIDI()` before the interactive phase, travel music plays
// ONCE during the entrance animation only.
- if (!_music || !_mystery.isLoaded() || !_voiceOn)
+ if (!_music || !_mystery.isLoaded() ||
+ (isLondon() ? !_musicOn : !_voiceOn))
return;
const uint num = _mystery._siteNumber % 5;
_music->playMus(num, /* loop= */ false);
@@ -1593,7 +1598,7 @@ void EEMEngine::startLondonTravelMusic(uint8 travelKind) {
{ 7, 23, 17 },
{ 10, 21, 24 },
};
- if (!_music || !_voiceOn || travelKind == 0 ||
+ if (!_music || !_musicOn || travelKind == 0 ||
travelKind >= ARRAYSIZE(kLondonTravelMusic))
return;
@@ -1686,6 +1691,8 @@ Common::Error EEMEngine::saveGameStream(Common::WriteStream *stream,
s.syncAsByte(_partner);
s.syncAsByte(_chainStage);
s.syncAsByte(_voiceOn);
+ // EEM2/London music (MIDI) toggle â `DAT_3036_4cc0`, separate from voice.
+ s.syncAsByte(_musicOn);
// London passport gender (player pronouns 0x86-0x88).
byte playerFemale = _playerFemale ? 1 : 0;
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 524c5d1e497..9f4d98c7856 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -512,6 +512,12 @@ private:
Common::KeyCode setupShowFullscreenPic(uint16 picId, bool transparent);
void setupLeave();
+ /// EEM2/London setup screen â `_DoSetup @ 2046:067b` (distinct from EEM1's
+ /// seg-1f78 screen): 4 toggles (partner / voice / music / highlight boxes),
+ /// rearranged 13-button layout, scrapbook paging. Reuses the shared helpers.
+ void doSetupLondon();
+ void setupDrawScreenLondon();
+
/// `_DoInitClues @ 1a35:0411` (minus live ANI sequence playback).
void doInitClues();
@@ -589,6 +595,12 @@ private:
/// partner speech, intro VO).
bool _voiceOn = true;
+ /// EEM2/London music (MIDI) on/off â `DAT_3036_4cc0`, toggled by the music
+ /// button in `_DoSetup @ 2046:067b`. Separate from `_voiceOn` (EEM2 has both
+ /// a voice and a music toggle; EEM1 only had one sound toggle). Gates the
+ /// briefing / travel MIDI for London.
+ bool _musicOn = true;
+
/// Set by the profile/new-player screens. London uses it to decide whether
/// to start the training mystery or resume the loaded profile's menu/state.
bool _profileCreatedThisSession = false;
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index a099e78f2fd..6f068ed710a 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -195,6 +195,18 @@ constexpr Common::Rect kSetupKid2Rect (Common::Point( 99, 54), 49, 8);
constexpr Common::Rect kSetupSoundOnRect (Common::Point(106, 86), 19, 8);
constexpr Common::Rect kSetupSoundOffRect(Common::Point(106, 96), 19, 8);
+// EEM2/London setup highlights â `_SetupSettings @ 2046:0008` (_SwapColors,
+// key 0xFE). On/off label pairs for the 4 toggles (rects @ 2bca:13ec; drawn
+// in the DOS order ON-then-OFF). Partner reuses Jake/Jenny.
+constexpr Common::Rect kLonSetJake (Common::Point( 99, 44), 49, 8); // 0x13ec
+constexpr Common::Rect kLonSetJenny (Common::Point( 99, 54), 49, 8); // 0x13f4
+constexpr Common::Rect kLonSetVoiceOn (Common::Point(106, 68), 20, 8); // 0x13fc
+constexpr Common::Rect kLonSetVoiceOff(Common::Point(106, 68), 40, 8); // 0x1404
+constexpr Common::Rect kLonSetMusicOn (Common::Point(106, 84), 20, 8); // 0x141c
+constexpr Common::Rect kLonSetMusicOff(Common::Point(128, 85), 18, 7); // 0x1424
+constexpr Common::Rect kLonSetHiOn (Common::Point(106, 110), 29, 8); // 0x1414
+constexpr Common::Rect kLonSetHiOff (Common::Point(106, 100), 29, 8); // 0x140c
+
// `_SwapColors @ 172b:1d2a` â replace pixels in r where value==from
// with to. 0xFE = BG text-key; 0x15 = active palette index, 0x00 =
// inactive (set by `_SetupSettings @ 1f78:000d`).
@@ -1774,6 +1786,188 @@ void EEMEngine::setupLeave() {
saveProfile(_playerName);
}
+void EEMEngine::setupDrawScreenLondon() {
+ // `_SetupSettings @ 2046:0008`: BG PIC 0x40 + _SwapColors highlights for the
+ // 4 toggles (key 0xFE, 0x15 active / 0x00 dim), drawn ON-then-OFF.
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+ Graphics::PixelFormat::createFormatCLUT8());
+ scratch.clear();
+ Picture bg;
+ if (_picsArchive.getPicture(0x40, bg))
+ scratch.simpleBlitFrom(bg.surface);
+
+ const byte kKey = 0xFE, kBright = 0x15, kDim = 0x00;
+ // Partner (DAT_3036_4bd4).
+ swapColors(scratch, kLonSetJake, kKey, _partner == kPartnerJake ? kBright : kDim);
+ swapColors(scratch, kLonSetJenny, kKey, _partner == kPartnerJenny ? kBright : kDim);
+ // Voice (DAT_3036_4c4e).
+ swapColors(scratch, kLonSetVoiceOn, kKey, _voiceOn ? kBright : kDim);
+ swapColors(scratch, kLonSetVoiceOff, kKey, _voiceOn ? kDim : kBright);
+ // Music (DAT_3036_4cc0).
+ swapColors(scratch, kLonSetMusicOn, kKey, _musicOn ? kBright : kDim);
+ swapColors(scratch, kLonSetMusicOff, kKey, _musicOn ? kDim : kBright);
+ // Highlight boxes (DAT_3036_4c4a; hide_highlight_boxes is the inverse).
+ const bool hiOn = !ConfMan.getBool("hide_highlight_boxes");
+ swapColors(scratch, kLonSetHiOn, kKey, hiOn ? kBright : kDim);
+ swapColors(scratch, kLonSetHiOff, kKey, hiOn ? kDim : kBright);
+
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, kScreenWidth, kScreenHeight);
+ g_system->updateScreen();
+}
+
+void EEMEngine::doSetupLondon() {
+ // `_DoSetup @ 2046:067b`. BG PIC 0x40; 13 click rects (`_SetupButtons @
+ // 2bca:12ce`); `HandleSetupButton @ 2046:01d9` dispatch (verified from the
+ // jumptable @ 2046:0661). EEM2 has FOUR toggles vs EEM1's two and a
+ // rearranged left column:
+ // [0]( 20, 44) Partner [1]( 20, 63) Voice [2]( 20,101) Highlight boxes
+ // [3]( 20,127) Profile(8) [12](20, 82) Music [4](281, 43) ScrapBook +
+ // [5](281, 62) ScrapBook- [6](281,108) Save [7](281,127) New case(0xa)
+ // [8]( 53,153) Done [9](145,163) Quit [10](212,153) Help
+ // [11]( 81, 25) Credits (PIC 0x208). _SavePlayerRecord on exit (setupLeave).
+ if (!_font.isLoaded()) {
+ _nextScreen = (ScreenId)_lastScreen;
+ return;
+ }
+ const Common::Rect kPartnerBtn ( 20, 44, 39, 61); // [0]
+ const Common::Rect kVoiceBtn ( 20, 63, 39, 80); // [1]
+ const Common::Rect kHiBtn ( 20, 101, 39, 118); // [2]
+ const Common::Rect kProfileBtn ( 20, 127, 39, 144); // [3]
+ const Common::Rect kScrapNext (281, 43, 299, 60); // [4]
+ const Common::Rect kScrapPrev (281, 62, 299, 79); // [5]
+ const Common::Rect kSaveBtn (281, 108, 299, 125); // [6]
+ const Common::Rect kNewCaseBtn (281, 127, 299, 144); // [7]
+ const Common::Rect kDoneBtn ( 53, 153, 108, 183); // [8]
+ const Common::Rect kQuitBtn (145, 163, 174, 187); // [9]
+ const Common::Rect kHelpBtn (212, 153, 266, 184); // [10]
+ const Common::Rect kCreditsBtn ( 81, 25, 238, 37); // [11]
+ const Common::Rect kMusicBtn ( 20, 82, 38, 99); // [12]
+
+ setupDrawScreenLondon();
+ _nextScreen = kScreenSetup; // sentinel â setupLeave picks the target
+ while (!shouldQuit()) {
+ Common::Event ev;
+ bool dirty = false;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+ _nextScreen = kScreenInvalid;
+ return;
+ }
+ if (ev.type == Common::EVENT_KEYDOWN &&
+ (ev.kbd.keycode == Common::KEYCODE_ESCAPE ||
+ ev.kbd.keycode == Common::KEYCODE_RETURN)) {
+ setupLeave();
+ return;
+ }
+ if (ev.type != Common::EVENT_LBUTTONDOWN)
+ continue;
+ const int mx = ev.mouse.x, my = ev.mouse.y;
+
+ // --- toggles (stay on screen, re-render) ---
+ if (kPartnerBtn.contains(mx, my)) {
+ _partner = (_partner == kPartnerJake) ? kPartnerJenny
+ : kPartnerJake;
+ dirty = true;
+ continue;
+ }
+ if (kVoiceBtn.contains(mx, my)) {
+ _voiceOn = !_voiceOn;
+ if (_audio)
+ _audio->setVoiceEnabled(_voiceOn);
+ dirty = true;
+ continue;
+ }
+ if (kMusicBtn.contains(mx, my)) {
+ _musicOn = !_musicOn;
+ if (!_musicOn)
+ stopMusic();
+ dirty = true;
+ continue;
+ }
+ if (kHiBtn.contains(mx, my)) {
+ ConfMan.setBool("hide_highlight_boxes",
+ !ConfMan.getBool("hide_highlight_boxes"));
+ dirty = true;
+ continue;
+ }
+
+ // --- navigation (leave the screen) ---
+ if (kProfileBtn.contains(mx, my)) {
+ saveProfile(_playerName);
+ _nextScreen = kScreenProfile;
+ return;
+ }
+ if (kNewCaseBtn.contains(mx, my)) {
+ saveProfile(_playerName);
+ _nextScreen = kScreenChooseMystery;
+ return;
+ }
+ if (kDoneBtn.contains(mx, my)) {
+ setupLeave();
+ return;
+ }
+ if (kQuitBtn.contains(mx, my)) {
+ if (areYouSure()) {
+ _nextScreen = kScreenInvalid;
+ return;
+ }
+ dirty = true; // restore BG under the prompt
+ continue;
+ }
+
+ // --- actions that stay on screen ---
+ if (kSaveBtn.contains(mx, my)) {
+ // `_SaveGame` is gated on a game being active ([0x1966]).
+ if (_mystery.isLoaded())
+ saveProfile(_playerName);
+ continue;
+ }
+ if (kHelpBtn.contains(mx, my)) {
+ // `_InterfaceHelp(0)`. Reuse the EEM1 help-card mechanism.
+ static const uint16 kHelpPics[] = { 0x0192, 0x01B1 };
+ CursorMan.showMouse(false);
+ for (uint i = 0; i < ARRAYSIZE(kHelpPics); i++) {
+ setupDrawScreenLondon();
+ if (setupShowFullscreenPic(kHelpPics[i], true) ==
+ Common::KEYCODE_ESCAPE)
+ break;
+ }
+ CursorMan.showMouse(true);
+ dirty = true;
+ continue;
+ }
+ if (kCreditsBtn.contains(mx, my)) {
+ CursorMan.showMouse(false);
+ setupShowFullscreenPic(0x208, /* transparent= */ false);
+ CursorMan.showMouse(true);
+ setSitePalette(0);
+ dirty = true;
+ continue;
+ }
+ // ScrapBook +/- â London has 2 books (stage 1 / 2). The DOS pages
+ // next/prev (FUN_2046_0874); map to the two available books.
+ if (kScrapNext.contains(mx, my)) {
+ doShowScrapbook(1);
+ setSitePalette(0);
+ dirty = true;
+ continue;
+ }
+ if (kScrapPrev.contains(mx, my) && _chainStage >= 2) {
+ doShowScrapbook(2);
+ setSitePalette(0);
+ dirty = true;
+ continue;
+ }
+ }
+ if (dirty)
+ setupDrawScreenLondon();
+ g_system->updateScreen();
+ g_system->delayMillis(15);
+ }
+}
+
void EEMEngine::doActionScreen() {
// `_ActionScreen @ 1c33:195b` â BG PIC 0x104 + PIC 9 @ (10, 0x87),
// `_DoChoose` with ActionNames @ 29be:0d6a. 5 picks alternating with
Commit: 44e61cbf7f00b8da6c7f915cdd60d7aca8bef2a6
https://github.com/scummvm/scummvm/commit/44e61cbf7f00b8da6c7f915cdd60d7aca8bef2a6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:31+02:00
Commit Message:
EEM: fixed regression on clean background for London
Changed paths:
engines/eem/clues.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index b49c45539db..75d3d723f21 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -1019,19 +1019,13 @@ void EEMEngine::displayClue(const byte *clueBlock) {
Picture charPic;
if (_picsArchive.getPicture(charPicId, charPic) &&
charX < kScreenWidth && charY < kScreenHeight) {
+ // Draw over the per-entry clean BG (restored at the top of the
+ // loop), NOT into the persistent `bg` snapshot â baking it in
+ // makes successive speaker portraits stack instead of refresh
+ // (duplicate NPC portraits + labels). The briefing's fixed NPC
+ // persists via the entrance animation captured in the snapshot
+ // (playLondonInitCluesAnim), not via the per-entry portrait.
blitMaskedToScreen(charPic, charX, charY);
- // `_DisplayClue` bakes the speaker portrait into the BACKGROUND
- // via `_AddPicBackground`; the DOS never restores a clean BG
- // mid-dialogue, so the portrait persists across the following
- // clue entries. Bake it into the `bg` snapshot too â otherwise
- // the per-entry full restore (top of this loop) wipes a fixed
- // NPC portrait (e.g. London's Nigel) on every entry that carries
- // no portrait of its own, leaving only the balloon ("rendered,
- // then refreshed over").
- const byte transp = (byte)(charPic.flags >> 8);
- bg.transBlitFrom(charPic.surface,
- Common::Point((int)charX, (int)charY),
- (uint32)transp);
}
}
Commit: a79edd41f2b937a82e573bceac6e2f0a71c0a665
https://github.com/scummvm/scummvm/commit/a79edd41f2b937a82e573bceac6e2f0a71c0a665
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:32+02:00
Commit Message:
EEM: missing music cue in London scrapbook
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 6f068ed710a..6acea3e5d58 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -1448,6 +1448,14 @@ void EEMEngine::doShowScrapbook(uint stage) {
return;
const bool currentTier = (stage == _chainStage);
+ // `_ShowScrapbook @ 2046:0874` (EEM2) opens with the scrapbook/newspaper
+ // MIDI tune 0x5d (same one `_ShowOneScrap` plays after a correct accusation),
+ // gated on the voice flag + MIDI availability (DOS `DAT_3036_4c4e`/`146a` â
+ // the original gates this music on the *voice* toggle, not the music one).
+ // London only; EEM1's seg-1f78 scrapbook was not verified to play it.
+ if (isLondon() && _music && _voiceOn)
+ _music->playMus(0x5d, /* loop= */ false);
+
int mystery = lo;
if (currentTier) {
while (mystery < hi && _mysteriesSolved[mystery] == 0)
@@ -1485,6 +1493,9 @@ void EEMEngine::doShowScrapbook(uint stage) {
break;
}
}
+ // `_ShowScrapbook` / `_ShowOneScrap` stop the MIDI on the way out.
+ if (isLondon() && _music)
+ stopMusic();
}
void EEMEngine::doSetup() {
Commit: 3555a18bdbf4f1bab57d86df0e2e334bab9d31a2
https://github.com/scummvm/scummvm/commit/3555a18bdbf4f1bab57d86df0e2e334bab9d31a2
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:32+02:00
Commit Message:
EEM: reduce the verbosity of the comments accross the engine
Changed paths:
engines/eem/audio.cpp
engines/eem/clues.cpp
engines/eem/detection.cpp
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/font.cpp
engines/eem/font.h
engines/eem/graphics.cpp
engines/eem/music.cpp
engines/eem/music.h
engines/eem/mystery.cpp
engines/eem/mystery.h
engines/eem/resource.cpp
engines/eem/resource.h
engines/eem/site.cpp
engines/eem/site.h
engines/eem/ui.cpp
diff --git a/engines/eem/audio.cpp b/engines/eem/audio.cpp
index 14570379b84..d791fc1883f 100644
--- a/engines/eem/audio.cpp
+++ b/engines/eem/audio.cpp
@@ -52,7 +52,6 @@ void AudioPlayer::stopAll() {
}
void AudioPlayer::playVoc(const Common::Path &vocPath) {
- // `_voiceEnabled` mirrors DAT_2d5d_3f97 (setup-screen voice toggle).
if (!_voiceEnabled) {
debugC(2, kDebugSound, "AudioPlayer: voice disabled, skipping %s",
vocPath.toString().c_str());
@@ -75,7 +74,6 @@ void AudioPlayer::playVoc(const Common::Path &vocPath) {
return;
}
- // _PlayVoice @ 1ff1:023e â route through kSpeechSoundType for the launcher slider.
_mixer->playStream(Audio::Mixer::kSpeechSoundType, &_voiceHandle,
stream, -1, Audio::Mixer::kMaxChannelVolume,
0, DisposeAfterUse::YES);
@@ -87,9 +85,9 @@ bool AudioPlayer::isVoicePlaying() const {
return _mixer->isSoundHandleActive(_voiceHandle);
}
+// _WaitForVoiceDone @ 1ff1:0221 â pumps events while the AIL voice channel
+// drains, so animations + abort-on-click keep working during the wait.
void AudioPlayer::waitForVoiceDone(uint32 maxMs) {
- // _WaitForVoiceDone @ 1ff1:0221 â pumps events while the AIL voice channel
- // drains, so animations + abort-on-click keep working during the wait.
const uint32 startMs = g_system->getMillis();
while (isVoicePlaying() && !_vm->shouldQuit() &&
g_system->getMillis() - startMs < maxMs) {
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 75d3d723f21..9fc34c93c95 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -127,7 +127,7 @@ void updateLondonClueSite(Mystery &mystery, uint16 rawSite, bool siteOn,
mystery._onSites[siteVal] = siteOn ? 1 : 0;
}
-// _DoHappiness @ 172b:27b5 â per-zone sequence scripts.
+// _DoHappiness @ 172b:27b5 per-zone sequence scripts.
// Jake seqs @ 29be:0337 (5 Ã 0x14 bytes), Jenny seqs @ 29be:039b. 9 frames each;
// the anim cells contain 10 cells = pairs of (neutral, smile) at 5 intensities.
const uint8 kJakeSeqs[5][9] = {
@@ -146,9 +146,7 @@ const uint8 kJennySeqs[5][9] = {
};
// EEM2 `FUN_17ee_26f6`: 10 rects @ 2bca:035c and 10 sequence rows for
-// Jennifer @ 2bca:03ac / Jake @ 2bca:0474. The portrait cells are 20-frame
-// ramps, so using the EEM1 5-level tables makes both characters smile in the
-// wrong places.
+// Jennifer @ 2bca:03ac / Jake @ 2bca:0474.
const int kLondonHappyRightEdges[10] = {
35, 70, 98, 126, 156, 182, 208, 235, 277, 320
};
@@ -196,9 +194,6 @@ uint happinessLevel(int x, bool london) {
}
// Masked blit: transparent colour = high byte of p.flags.
-// _AddPicBackground @ 172b:0ed4 pushes `word ptr ES:[BX] >> 8` as
-// _Rect_Move_Mask's mask byte (the on-disk u16 at file offset 0 maps to
-// Picture::flags).
void blitMaskedToScreen(const Picture &p, int x, int y) {
Graphics::Surface *screen = g_system->lockScreen();
if (!screen)
@@ -225,10 +220,7 @@ void blitRawToScreen(const Picture &p, int x, int y) {
x, y, w, h);
}
-// _DoChoosePartner @ 1a35:0756 / EEM2 @ 1abf:0728. The original places
-// Jake + Jenny animations on a backdrop and polls two broad character rects;
-// we approximate those with a single split at x=160. EEM1 has Jenny left /
-// Jake right; EEM2 has Jake left / Jennifer right.
+// _DoChoosePartner @ 1a35:0756 / EEM2 @ 1abf:0728.
void EEMEngine::doChoosePartner() {
_partner = kPartnerJake;
@@ -277,10 +269,6 @@ void EEMEngine::doChoosePartner() {
uint32 lastTick = g_system->getMillis();
while (!shouldQuit()) {
- // 100ms tick: matches _CheckFrameRate's ~10 fps cadence. The seq
- // is short and loops; _UpdateAnimations restarts at curIdx=0 on
- // the 0x80 marker. _DoHappiness rewrites curIdx=0xFFFF on zone
- // change (here we restart seqIdx).
if (g_system->getMillis() - lastTick > 100) {
lastTick = g_system->getMillis();
seqIdx = (seqIdx + 1) % 9;
@@ -345,14 +333,9 @@ void EEMEngine::doChoosePartner() {
g_system->delayMillis(20);
}
- // Partner intro VOC (jake.voc / jen.voc @ 29be:0af9 / 29be:0af1),
- // tail of _DoChoosePartner @ 1a35:097f.
if (_audio) {
if (isFloppy()) {
- // Floppy _DoChoosePartner_Floppy @ 19bb:0a8e calls
- // _LoadSoundName_Floppy(0x14) (slot 20); per-partner tables at
- // 2608:0f0e / 2608:0f76 resolve to m-0113sl.voc (Jake) or
- // f-0140sl.voc (Jenny).
+ // Floppy _DoChoosePartner_Floppy @ 19bb:0a8e
_audio->playFloppyVoiceSlot(0x14, _partner);
} else {
_audio->playVoc(Common::Path(
@@ -362,18 +345,7 @@ void EEMEngine::doChoosePartner() {
}
}
-// EEM2 case-intro animation â `_DoInitClues` @ 1abf:03b3. Ghidra-confirmed
-// flow (decompiled prologue):
-// _AllBlack(); _GetBackground(0x52); _GetPalette(0x39);
-// anim = _GetAnimation(DAT_4bd4 ? 0x71 : 0x18); // Jake 0x18 / Jenny 0x71
-// _NewAnimation(0xd2, 0x3f, anim, ...); // registered at (210,63)
-// _UpdateAnimations(); _FadeIn(); // draw frame 0, then fade
-// while (i != _max()) { _CheckFrameRate(); _UpdateAnimations(); kbhit->skip }
-// if (caseType == 1) { _LoadSoundName("phone1.voc"); _PlayVoice; _Wait... }
-// So, unlike EEM1's game/book/nancy cycle + `_PlayInSequence`, EEM2 registers
-// ONE partner animation drawn by `_UpdateAnimations` â hence we anchor frames
-// with `blitAnimFrameAnchored` ((0xd2 - miscflags, 0x3f - rowoff), per
-// `_UpdateAnimations @ 172b:09c1`), the same helper the map/site screens use.
+// EEM2 case-intro animation â `_DoInitClues` @ 1abf:03b3.
void EEMEngine::playLondonInitCluesAnim(uint16 caseType, const Picture &bg,
bool haveBriefingBg) {
const uint introAni = (_partner == kPartnerJake) ? 0x18 : 0x71;
@@ -383,14 +355,7 @@ void EEMEngine::playLondonInitCluesAnim(uint16 caseType, const Picture &bg,
_aniArchive.loadAnimation(introAni, anim) && !anim.empty();
// `_DoInitClues @ 1abf:03b3` registers a SECOND, fixed briefing character
- // (e.g. Nigel) on the LEFT, gated on caseType (jumptable @ CS:0x720):
- // caseType 0 -> _GetAnimation(0x70) @ (0, 0x3e)
- // caseType 2 -> _GetAnimation(0x0e) @ (0, 0x36)
- // caseType 3 -> _GetAnimation(0x74) @ (0, 0x1a)
- // caseType 1 and >= 4 -> partner only (no second character).
- // Both animate during the entrance and the last frame is left on screen, so
- // the following `displayClue` snapshot keeps them through the briefing
- // dialogue. Without this the NPC is never drawn (Nigel invisible in M2).
+ // (Nigel) on the LEFT, gated on caseType (jumptable @ CS:0x720):
uint npcAni = 0;
int npcX = 0, npcY = 0;
switch (caseType) {
@@ -403,20 +368,12 @@ void EEMEngine::playLondonInitCluesAnim(uint16 caseType, const Picture &bg,
const bool haveNpc = npcAni != 0 &&
_aniArchive.loadAnimation(npcAni, npc) && !npc.empty();
- // _AllBlack(): blank the palette so the _FadeIn after frame 0 reveals the
- // briefing (palette 0x39 â EEM2's 63-entry SITEPALS. shifts EEM1's UI set).
byte pal[kPalSize];
const bool havePal = getSitePalette(0x39, pal);
byte black[kPalSize] = {};
g_system->getPaletteManager()->setPalette(black, 0, 256);
g_system->updateScreen();
- // Drive the cells through the animation script (`_NewAnimation` prior
- // 0x18 -> `_AnimationSequences[0x18]`), not a raw 0..N-1 sweep â the
- // frame shown each tick is `script[tick]`, not `tick`. For EEM2 that
- // script is the count-up {0..16}, but routing through `partnerFrameAtTick`
- // keeps this faithful if the cells/script ever diverge and matches the map/
- // site partner rendering. One pass = one script cell per ~140 ms tick.
bool skip = false;
uint frames = haveAnim ? (uint)anim.size() : 1;
if (haveNpc)
@@ -460,9 +417,6 @@ void EEMEngine::playLondonInitCluesAnim(uint16 caseType, const Picture &bg,
break;
}
}
- // Re-composite the cursor every ~10 ms so the mouse stays smooth
- // between the 140 ms animation frames (the frame-rebuild above is
- // throttled, but the cursor is only flushed by updateScreen()).
g_system->updateScreen();
g_system->delayMillis(10);
}
@@ -475,13 +429,7 @@ void EEMEngine::playLondonInitCluesAnim(uint16 caseType, const Picture &bg,
}
}
-// EEM1 CD/floppy case-intro animation â `_DoInitClues @ 1a35:0411`:
-// anims registered: game @ (0xcd,0x6c) [0x17 Jake / 0x3b Jenny],
-// book @ (0,99) [0x18 / 0x3c], nancy @ (0x68,0x8b) [0x19, caseType 1 only];
-// cycle the game anim once (click skips), then `_PlayInSequence @ 172b:2d03`
-// plays the partner entrance per partner+caseType. A phone/news voice rings
-// first on CD caseType 2 / floppy caseType 2-3. (London: see
-// playLondonInitCluesAnim.)
+// EEM1 CD/floppy case-intro animation â `_DoInitClues @ 1a35:0411`
void EEMEngine::playCdFloppyInitCluesAnim(uint16 caseType, bool floppy,
const Picture &bg, bool haveBriefingBg) {
const uint gameAni = _partner == kPartnerJake ? 0x17 : 0x3b;
@@ -494,12 +442,6 @@ void EEMEngine::playCdFloppyInitCluesAnim(uint16 caseType, bool floppy,
&& _aniArchive.loadAnimation(0x19, nancy)
&& !nancy.empty();
- // Cycle the game animation once at _CheckFrameRate cadence (~7 fps, 140 ms
- // per tick; _InitFrameCounter @ 1a35:01ae). The original loop @ 1a35:0507
- // makes `gameNum - 1` _UpdateAnimations calls, each advancing every
- // registered slot by one script tick. _DoInitClues @ 1a35:0507/0541
- // hard-codes Jake's script IDs (0x17/0x18/0x19) regardless of partner, so
- // we look up scripts by those IDs unconditionally.
if (haveGame || haveBook || haveNancy) {
const uint kCheckFrameRateMs = 140;
const uint baseFrames = haveGame ? game.size() : 8;
@@ -555,9 +497,7 @@ void EEMEngine::playCdFloppyInitCluesAnim(uint16 caseType, bool floppy,
// _VidramRectCopy(0, 0x5a, 0x28, 0x6d, 16000, 48000/32000): freeze the
// lower-left book/Nancy band the original bakes into BG buffers before
- // clearing registered animations. Width is in mode-X cols (0x28 = 160px).
- // Intentionally drops the right-side game anim; _PlayInSequence redraws
- // that character over a clean BG next.
+ // clearing registered animations.
Graphics::ManagedSurface briefingBase(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
briefingBase.clear();
@@ -586,15 +526,6 @@ void EEMEngine::playCdFloppyInitCluesAnim(uint16 caseType, bool floppy,
}
}
- // Setup voice â _DoInitClues @ 1a35:061d runs this BEFORE _PlayInSequence
- // (i.e. the phone rings, the player hears it ring out, and only then
- // does Jake/Jenny reach for the receiver):
- // CD: caseType 2 -> PHONE.VOC then _WaitForVoiceDone
- // Floppy (_DoInitClues_Floppy @ 19bb:042f):
- // caseType 2 -> slot 0xc (PHONESL.VOC)
- // caseType 3 -> slot 3 (NEWSCAN.VOC, news-anchor variant)
- // While the voice plays the screen continues showing the game anim's
- // last frame (saved into briefingBase above).
if (_audio) {
if (caseType == 2) {
if (floppy)
@@ -608,12 +539,7 @@ void EEMEngine::playCdFloppyInitCluesAnim(uint16 caseType, bool floppy,
}
}
- // _PlayInSequence @ 172b:2d03. Anim selection per partner + caseType:
- // Jake: caseType 1 -> 0x38 @ (0xcd, 0x6d)
- // caseType 2 -> 0x37 @ (0xcd, 0x6c)
- // caseType 3 -> 0x39 @ (0xcd, 0x6c)
- // Jenny: caseType 2 -> 0x3a @ (0xcd, 0x6c)
- // caseType 3 -> 0x3d @ (0xcd, 0x6c)
+ // _PlayInSequence @ 172b:2d03.
uint16 seqAni = 0xFFFF;
uint16 seqY = 0x6c;
if (_partner == kPartnerJake) {
@@ -657,10 +583,7 @@ void EEMEngine::playCdFloppyInitCluesAnim(uint16 caseType, bool floppy,
g_system->copyRectToScreen(briefingBase.getPixels(),
briefingBase.pitch, 0, 0,
kScreenWidth, kScreenHeight);
- // _PlayInSequence @ 172b:2d35-2d50:
- // dstX = sx - cell[+0x8] ; signed X anchor (miscflags)
- // dstY = sy - cell[+0x6] ; signed Y anchor (rowoff)
- // asm `SUB AX, ES:[BX+0x8]` â NOT the cell width.
+ // _PlayInSequence @ 172b:2d35-2d50
const int dstX = (int)0xcd - (int)(int16)fr.miscflags;
const int dstY = (int)seqY - (int)(int16)fr.rowoff;
blitMaskedToScreen(fr, dstX, dstY);
@@ -690,11 +613,7 @@ void EEMEngine::playCdFloppyInitCluesAnim(uint16 caseType, bool floppy,
}
}
-// _DoInitClues @ 1a35:0411 (EEM1) / 1abf:03b3 (EEM2). Sequence:
-// 1. mark the start site / visited sites from the InitBlock
-// 2. BG PIC 0x52 + briefing palette (EEM1 0x22 / EEM2 0x39)
-// 3. partner entrance animation (CD/floppy or London variant)
-// 4. _DisplayClue(InitBlock + 2) â briefing dialogue
+// _DoInitClues @ 1a35:0411 (EEM1) / 1abf:03b3 (EEM2)
void EEMEngine::doInitClues() {
if (!_mystery.isLoaded())
return;
@@ -703,9 +622,6 @@ void EEMEngine::doInitClues() {
if (!ib)
return;
- // CD InitBlock: u16 caseType; u16 startSite; <clue block>.
- // Floppy InitBlock (FUN_19bb_042f): u8 caseType; u8 nSubjects;
- // subjects[]; u8 nDialog; dialog_records[]. No startSite.
const bool floppy = isFloppy();
const uint16 caseType = floppy ? (uint16)ib[0] : READ_LE_UINT16(ib);
@@ -716,8 +632,6 @@ void EEMEngine::doInitClues() {
_mystery._siteNumber = startSite;
_mystery._lastSite = startSite;
} else {
- // Floppy _DoMapScreen @ 1fed:1060 (FUN_1fed_07ed) walks every
- // site unconditionally â mirror that by marking all visible.
const uint sites = _mystery.numSites();
for (uint s = 0; s < sites && s < Mystery::kVisitedSiteCap; s++)
_mystery._onSites[s] = 1;
@@ -725,21 +639,12 @@ void EEMEngine::doInitClues() {
_mystery._lastSite = 0;
}
- // Case-briefing palette. EEM1 `_DoInitClues` uses SITEPALS index 0x22;
- // EEM2 `_DoInitClues` @ 1abf:03b3 does `_GetBackground(0x52)` then
- // `_GetPalette(0x39)` (its 63-entry SITEPALS. shifts the UI palettes).
- // The briefing partner animation is an ANI sprite drawn under the screen
- // palette, so the same index fixes both the background and the anim.
setSitePalette(isLondon() ? 0x39 : 0x22);
Picture bg;
const bool haveBriefingBg = _picsArchive.getPicture(0x52, bg);
if (haveBriefingBg)
blitAt(bg, 0, 0);
- // Case-intro partner animation. EEM2 (`_DoInitClues` @ 1abf:03b3) plays a
- // single partner anim (Jake 0x18 / Jenny 0x71); EEM1 CD/floppy runs a
- // game/book/nancy cycle + `_PlayInSequence` entrance. Both then feed the
- // shared briefing dialogue below.
if (isLondon())
playLondonInitCluesAnim(caseType, bg, haveBriefingBg);
else
@@ -831,13 +736,7 @@ Common::String EEMEngine::parseString(const Common::String &raw,
// `_WordWrap @ 1b03:0456` treats `^` as a forced line
// break (sets `cur_width = max_width`, forcing the next
// loop turn to wrap at the previous space and skip the
- // `^` itself). Without this conversion the caret falls
- // through the default case and renders as a literal,
- // pushing the line past the balloon's visual edge â the
- // "bubbles aren't large enough" symptom. Promote it to
- // `\n` so `Font::wordWrapText` (which honours
- // embedded newlines) picks the same break point the
- // original engine did.
+ // `^` itself).
out += '\n';
break;
default:
@@ -849,11 +748,6 @@ Common::String EEMEngine::parseString(const Common::String &raw,
// Strip leading spaces at the start of each emitted line. Mirrors
// `_DoWordWrap @ 1b66:04a7`, which advances past spaces at the
// start of every output line via `for (; str[last] == ' '; last++)`.
- // ~60% of mystery-text strings carry 1-2 leading spaces in the data
- // of the original WordWrap
- // discards them, so we do the same before the text reaches
- // `Font::wordWrapText` (which only trims at wrap-induced line
- // boundaries, not at start-of-input or after an embedded '\n').
Common::String cleaned;
bool atLineStart = true;
for (uint i = 0; i < out.size(); i++) {
@@ -866,15 +760,9 @@ Common::String EEMEngine::parseString(const Common::String &raw,
return cleaned;
}
+// EEM2 `_DisplayClue @ 2542:05bd` per-entry side effects
void EEMEngine::applyClueSideEffects(const byte *c) {
if (isLondon()) {
- // EEM2 `_DisplayClue @ 2542:05bd` per-entry side effects. With the
- // shared `c = entryBase + 4` convention the EEM2 fields land at:
- // onsite entry+0x22 (= c+0x1e), 5 Ã u16, high bit = CONSITE flag
- // offsite entry+0x2c (= c+0x28), 5 Ã u16, high bit = COFFSITE flag
- // gallery entry+0x36 (= c+0x32), 5 Ã u16 -> _InGallery[_newOrder[idx]]
- // notebook entry+0x40 (= c+0x3c), 5 Ã u16 -> _AddNotebook
- // jump entry+0x4a (= c+0x46), destination site for direct travel
for (uint j = 0; j < 5; j++) {
const uint16 note = READ_LE_UINT16(c + 0x3c + j * 2);
if (note != 0xFFFF && note < Mystery::kCluesFoundCap)
@@ -935,11 +823,6 @@ void EEMEngine::displayClue(const byte *clueBlock) {
if (number == 0 || number > 32)
return;
- // ClueEntry stride. EEM1 `_DisplayClue @ 2404:05e6` packs 62-byte entries;
- // EEM2 `_DisplayClue @ 2542:05bd` indexes `theClue + i*0x54` (84 bytes). The
- // per-entry fields up to +0x1e are layout-identical (so entry 0 â at the
- // shared base clueBlock+4 â reads the same either way); only the stride and
- // the tail fields (KD anim, notebook, onsite) move. See applyClueSideEffects.
const uint stride = isLondon() ? 0x54 : 62;
// Snapshot BG so per-entry character pics don't stack.
@@ -964,32 +847,19 @@ void EEMEngine::displayClue(const byte *clueBlock) {
// +0x1a: Jake voice (1-based)
// +0x30..+0x39: 5 notebook entries (-1 terminated)
// +0x3a..+0x3b: KD-anim number (-1 = none)
- // _DisplayClue @ 2404:05e6: partner 1 uses its own field set only when
- // bubText1 != -1; otherwise falls back to partner 0 fields entirely.
- // Partner 0 always uses field 0.
for (uint i = 0; i < number && !shouldQuit(); i++) {
g_system->copyRectToScreen(bg.getPixels(), bg.pitch, 0, 0, kScreenWidth, kScreenHeight);
const byte *c = clueBlock + 4 + i * stride;
- // EEM2 `_DisplayClue @ 2542:05bd` opens each clue with a per-entry MIDI
- // cue read at entry+0x20 (= c+0x1c, the u16 right after the two voice
- // slots), replacing any current track (`playMus`â`playFile` calls
- // `stop()`). 0 = no cue; EEM1 clue entries have no such field. Gated on
- // `_voiceOn` like `startTravelMusic` â the DOS gate is the separate
- // music-on / MIDI-available flags (DAT_3036_4cc0 && DAT_2bca_146a).
if (isLondon() && _music && _musicOn) {
const uint16 clueMusic = READ_LE_UINT16(c + 0x1c);
if (clueMusic != 0)
_music->playMus(clueMusic, /* loop= */ false);
}
- // _DisplayClue @ 2404:0635-064b: _DoKDAnim(num) runs before the
- // speaker portrait. EEM1 stores the KD-anim number at +0x3a; EEM2
- // `_DisplayClue @ 2542:05bd` reads it at entry+0x52 (= c+0x4e).
const int16 kdAnimNum = (int16)READ_LE_UINT16(c + (isLondon() ? 0x4e : 0x3a));
if (kdAnimNum != -1) {
playKdAnim((uint16)kdAnimNum);
- // _UpdateAnimations @ 172b:09c1 reactivates the wait anim.
g_system->copyRectToScreen(bg.getPixels(), bg.pitch,
0, 0, kScreenWidth, kScreenHeight);
}
@@ -1022,9 +892,7 @@ void EEMEngine::displayClue(const byte *clueBlock) {
// Draw over the per-entry clean BG (restored at the top of the
// loop), NOT into the persistent `bg` snapshot â baking it in
// makes successive speaker portraits stack instead of refresh
- // (duplicate NPC portraits + labels). The briefing's fixed NPC
- // persists via the entrance animation captured in the snapshot
- // (playLondonInitCluesAnim), not via the per-entry portrait.
+ // (duplicate NPC portraits + labels).
blitMaskedToScreen(charPic, charX, charY);
}
}
@@ -1058,21 +926,13 @@ void EEMEngine::displayClue(const byte *clueBlock) {
if (haveBalloon) {
const int bw = MIN<int>(balloon.surface.w, kScreenWidth - bubX);
const int bh = MIN<int>(balloon.surface.h, kScreenHeight - bubY);
- // _AddPicBackground: transparent colour = pic->miscflags >> 8.
const byte transp = (byte)(balloon.flags >> 8);
- // _GetBalloon @ 172b:1d7d mirrors horizontally when
- // (bubNum & 0x80) â for right-side speakers.
const bool flipBalloon = (fittedBubNum & 0x80) != 0;
if (bw > 0 && bh > 0) {
scratch.transBlitFrom(balloon.surface,
Common::Point(bubX, bubY),
transp, flipBalloon);
}
- // Per-balloon insets at 29be:0875 (52 x 10 bytes,
- // indexed by bubNum & 0x7F). _DisplayClue does
- // _WordWrap(bubX + table[bub].x, bubY + table[bub].y,
- // table[bub].w, ...).
- // Fallback (5, 4, 155) = entry-23 inset.
uint16 insetX = 5;
uint16 insetY = 4;
uint16 insetW = 155;
@@ -1082,21 +942,15 @@ void EEMEngine::displayClue(const byte *clueBlock) {
textW = insetW;
copyH = bh;
} else {
- // No balloon: clear band.
const Common::Rect band(0, bubY, kScreenWidth,
MIN<int>(bubY + copyH, kScreenHeight));
scratch.fillRect(band, 0);
copyY = bubY;
}
- // _DisplayClue @ 2404:07fe passes fontColor=0 (palette
- // index 0 of case-briefing palette 0x22).
_font.drawWordWrapped(&scratch, textX, textY,
MAX<int>(8, textW), text, 0);
- // Clamp to the screen: a malformed entry (e.g. a clue-format
- // mismatch) must never hand copyRectToScreen a negative height,
- // which would underflow to a multi-GB memcpy.
copyY = CLIP<int>(copyY, 0, kScreenHeight - 1);
const int copyRows = CLIP<int>(MIN<int>(copyH, kScreenHeight - copyY),
0, kScreenHeight - copyY);
@@ -1107,9 +961,6 @@ void EEMEngine::displayClue(const byte *clueBlock) {
}
}
- // _DisplayClue @ 2404:0833-085a â per-clue voice gate. The gate is on
- // the Jenny slot regardless of partner; entries with +0x18 == 0 but
- // +0x1a set are text-only.
if (_audio) {
const uint16 voiceJenny = READ_LE_UINT16(c + 0x18);
if (voiceJenny != 0 && voiceJenny != 0xFFFF) {
@@ -1143,7 +994,6 @@ void EEMEngine::displayClue(const byte *clueBlock) {
ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
advance = true;
skipAll = true;
- // Cut voice + spool only (keep MIDI).
interruptAudio(/* stopMusicToo= */ false);
break;
}
@@ -1163,7 +1013,6 @@ void EEMEngine::displayClue(const byte *clueBlock) {
g_system->delayMillis(10);
}
if (skipAll) {
- // Apply remaining side-effects without rendering.
for (uint k = i; k < number; k++)
applyClueSideEffects(clueBlock + 4 + k * stride);
return;
@@ -1172,17 +1021,9 @@ void EEMEngine::displayClue(const byte *clueBlock) {
applyClueSideEffects(c);
- // EEM2 `_DisplayClue @ 2542:05bd`: a clue entry can gate the rest of
- // the clue behind a "check the manual / a real map" puzzle (the id is
- // entry+0x54 = c+0x50, a `P<id>.BIN`). Run it once per mystery
- // (`_mystery._solvedPuzzle` = `DAT_3036_6d5c`); a wrong answer aborts
- // the remaining entries and re-prompts on the next visit.
if (isLondon() && !_mystery._solvedPuzzle) {
const uint16 puzzleId = READ_LE_UINT16(c + 0x50);
if (puzzleId != 0xFFFF) {
- // Restore the clean site BG (drop this clue's bubble) first â
- // the DOS _Repaint before `_DoPuzzle` â so the puzzle (and its
- // own hint) render on a clean background, not over the bubble.
g_system->copyRectToScreen(bg.getPixels(), bg.pitch, 0, 0,
kScreenWidth, kScreenHeight);
g_system->updateScreen();
@@ -1193,8 +1034,6 @@ void EEMEngine::displayClue(const byte *clueBlock) {
}
}
- // _StopTheVoice @ 1ff1:0283 effect (voice only, keep MIDI). Diverges
- // from _DisplayClue @ 2404:05e6 which lets voice bleed past dismissal.
interruptAudio(/* stopMusicToo= */ false);
}
@@ -1245,9 +1084,6 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
// u8 sound @ +9 (high bit = play voice, low 7 bits = slot)
// u8 textCount@ +10
// u8 textIdx[]@ +11 (1 byte per â low 7 bits = NOTES idx)
- // NOTES text offsets are absolute byte offsets into the mystery
- // buffer; read via mystery.blobAt(noteEntry[+2..3]) for Jake,
- // +4..5 for Jenny.
if (!rec || !isFloppy() || !_font.isLoaded() || count == 0)
return;
@@ -1270,9 +1106,6 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
const uint32 dsz = _mystery.dataSize();
const uint32 notesBase = (uint32)(notes - bufBase);
- // _DisplayHotspotClue_Floppy @ 22dc:05c8 writes _TextSeen_Floppy[idx]=1
- // inline per text. Pre-mark all up front so ESC-fast-forward still
- // records every clue in the notebook.
{
const byte *r = rec;
for (uint i = 0; i < count; i++) {
@@ -1301,12 +1134,7 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
const uint slot = rec[9] & 0x7f;
_audio->playFloppyVoiceSlot(slot, _partner);
}
- // _DisplayHotspotClue_Floppy @ 22dc:0908..0922 suspect-found:
- // if ((rec[9] & 0x80) == 0 && rec[9] != 0)
- // _InGallery_Floppy[_GalleryShuffleSeed_Floppy[rec[9]]] = 1;
- // _GalleryShuffleSeed (DS:0x2d65) overlaps _NewOrder (DS:0x2d66)
- // by one byte: GalleryShuffleSeed[i+1] == NewOrder[i]. So:
- // _inGallery[_newOrder[b9 - 1]].
+
const uint8 b9 = rec[9];
if ((b9 & 0x80) == 0 && b9 != 0) {
const uint logicalIdx = (uint)b9 - 1;
@@ -1379,9 +1207,6 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
const int textX = ballX + textXIns;
const int lineH = _font.getFontHeight();
- // FUN_22dc_05c8 pagination via local_1c (set from previous text's
- // flag bit): 0 = fresh page (redraw balloon, Y at top),
- // 1 = continuation (append below previous lines).
bool firstPage = true;
int cursorY = ballY + textYIns;
bool skipAll = false;
@@ -1413,7 +1238,6 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
scratch.simpleBlitFrom(*bg.surfacePtr());
if (firstPage) {
- // Original only redraws balloon + portrait on local_1c == 0.
if (picID != 0 && picID != 0xFFFF) {
Picture pic;
if (_picsArchive.getPicture(picID, pic)) {
@@ -1445,10 +1269,6 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
const bool isLastText = (t + 1 == textCount);
const bool isLastRec = (i + 1 == count);
- // "Click to continue" indicator: PIC 0xa0 ("more" arrow) or
- // PIC 0xa1 (end). _DisplayHotspotClue_Floppy @ 22dc:08aa
- // (mid-page) / @ 22dc:08c0 (end-of-record). lastIndicator == 0
- // matches the original param_2 == 0 "no indicator" case.
bool waitNeeded = false;
bool drawArrow = false;
bool useEndPic = false;
@@ -1457,13 +1277,12 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
firstPage = false;
} else {
waitNeeded = true;
- drawArrow = true; // PIC 0xa0
+ drawArrow = true;
useEndPic = false;
}
} else {
waitNeeded = true;
if (!isLastRec) {
- // More records follow â param_2 = 1 â PIC 0xa0.
drawArrow = true;
useEndPic = false;
} else {
@@ -1472,7 +1291,7 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
useEndPic = false;
} else if (lastIndicator == 2) {
drawArrow = true;
- useEndPic = true; // PIC 0xa1
+ useEndPic = true;
}
}
}
@@ -1509,8 +1328,6 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
}
void EEMEngine::displayFloppyBriefing(const byte *initBlock) {
- // FUN_19bb_042f @ 19bb:042f: walks nDialog = ib[2 + nSubjects] records
- // starting at ib + 3 + nSubjects, dispatching each to FUN_22dc_05c8.
if (!initBlock || !isFloppy())
return;
const uint8 nSubjects = initBlock[1];
@@ -1520,12 +1337,6 @@ void EEMEngine::displayFloppyBriefing(const byte *initBlock) {
}
void EEMEngine::displayFloppyHotspotDialog(uint siteNum, uint hotIdx) {
- // FUN_22dc_0b80 @ 22dc:0b80 + FUN_1652_00e6 @ 1652:00e6 +
- // FUN_1652_006c @ 1652:006c. site_data[+6..7] -> per-hotspot list:
- // for each hotspot:
- // main record (11 + textCount bytes)
- // u8 contFlags (low 7 = cont count, high bit = partner pose)
- // contCount x { record (11 + textCount bytes) }
if (!_mystery.isLoaded() || !isFloppy())
return;
const byte *site = _mystery.siteData(siteNum);
@@ -1548,8 +1359,7 @@ void EEMEngine::displayFloppyHotspotDialog(uint siteNum, uint hotIdx) {
}
if (off >= _mystery.dataSize())
return;
- // Snapshot clean site BG and restore between main + continuation calls
- // so each displayFloppyDialogRecords sees a bubble-free background.
+
Graphics::ManagedSurface siteBG(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
{
@@ -1567,10 +1377,7 @@ void EEMEngine::displayFloppyHotspotDialog(uint siteNum, uint hotIdx) {
contFlagsByte = bufBase[off + mainLen];
contCount = contFlagsByte & 0x7F;
}
- // _HandleHotspotClick_Floppy @ 1652:00e6 derives param_2:
- // contFlagsByte == 0 -> 0 (no indicator)
- // high bit set -> 1 (PIC 0xa0, "more")
- // low 7 bits non-zero -> 2 (PIC 0xa1, alt end)
+
uint mainIndicator = 0;
if (contFlagsByte != 0) {
mainIndicator = (contFlagsByte & 0x80) ? 1 : 2;
@@ -1581,12 +1388,10 @@ void EEMEngine::displayFloppyHotspotDialog(uint siteNum, uint hotIdx) {
const uint32 contOff = off + mainLen + 1;
if (contOff >= _mystery.dataSize())
return;
- // Wipe main bubble so the continuation chain snapshots a clean BG.
+
g_system->copyRectToScreen(siteBG.getPixels(), siteBG.pitch,
0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
- // _DisplayDialogContinuations_Floppy @ 1652:006c: lastIndicator=0
- // means no indicator on the final continuation.
displayFloppyDialogRecords(bufBase + contOff, contCount, 0);
}
@@ -1599,10 +1404,6 @@ bool EEMEngine::areYouSure() {
g_system->unlockScreen();
}
- // _AreYouSure @ 1a35:0a5c (CD) / FUN_19bb_0b43 (floppy):
- // PIC 0x136 = dialog body; PIC 0x1fd/0x1fe = YES/NO pressed buttons.
- // YES hit: (x+0x0c, y+0x23) .. (x+0x20, y+0x32)
- // NO hit: (x+0x60, y+0x23) .. (x+0x74, y+0x32)
Picture dialogPic;
Picture yesPic;
Picture noPic;
diff --git a/engines/eem/detection.cpp b/engines/eem/detection.cpp
index 714b2d81821..c27f37a7bfb 100644
--- a/engines/eem/detection.cpp
+++ b/engines/eem/detection.cpp
@@ -68,10 +68,7 @@ const ADGameDescription gameDescriptions[] = {
GUI_OPTIONS_EEM_FLOPPY
},
{
- // Eagle Eye Mysteries in London (EEM2CD.EXE), the sequel.
- // It reuses this engine's resource formats (DBD/DBX archives,
- // SITEPALS palettes, FONT.FNT); reimplementation is in progress,
- // so it stays flagged unstable until fully supported.
+ // Eagle Eye Mysteries in London
"eem2",
"CD",
AD_ENTRY2s("EEM2CD.EXE", "211a376b23a1b6259d0c36cf46d26ed4", 172560,
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 4f455d22562..136a45ab278 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -48,8 +48,6 @@
namespace EEM {
-const uint kNumSitePals = 40; // SITEPALS: 40 * 768 = 30720
-
// 1-based picture/palette IDs.
const uint kPicEAKidsLogo = 0x54; // _ShowEAKids
const uint kPicHighScoreLogo = 0x20c; // _ShowHScoreLogo
@@ -89,8 +87,6 @@ Common::String makeLondonProfileDisplayName(const Common::String &first,
Common::String makeLondonProfileKey(const Common::String &first,
const Common::String &last) {
- // EEM2 `_GenerateFilename @ 1cd3:02fe`: copy the first 7 bytes of
- // FirstName, pad to 8 from LastName, then replace spaces/dots with '_'.
Common::String cleanFirst = first;
Common::String cleanLast = last;
cleanFirst.trim();
@@ -220,13 +216,9 @@ EEMEngine::EEMEngine(OSystem *syst, const ADGameDescription *gameDesc)
_variant = (gameDesc && gameDesc->extra &&
Common::String(gameDesc->extra).contains("Floppy"))
? kVariantFloppy : kVariantCD;
- // EEM2 ("...in London") ships as a separate detection entry (gameId
- // "eem2"); it reuses this engine with London-specific data.
if (gameDesc && gameDesc->gameId &&
Common::String(gameDesc->gameId) == "eem2")
_variant = kVariantLondonCD;
- // EEM2 ships its own `_AnimationSequences` â many partner/KD scripts
- // differ from EEM1's, so route `findAnimScript` to the EEM2 table.
setLondonAnimScripts(isLondon());
_language = gameDesc ? gameDesc->language : Common::EN_ANY;
}
@@ -270,7 +262,7 @@ bool EEMEngine::anyMysterySolved(uint lo, uint hi) const {
bool EEMEngine::mysteryTierRange(uint stage, uint &lo, uint &hi) const {
if (isLondon()) {
- // EEM2/London: two 25-case books, no BOOK3.NME
+ // EEM2/London: two 25-case books
// (`_DisplayCorrect @ 1ea1:0619`).
switch (stage) {
case 1: lo = 0x01; hi = 0x19; return true; // 1..25
@@ -299,17 +291,12 @@ void EEMEngine::advanceChainStageAfterSolve(uint mysteryNum) {
return;
const uint oldStage = _chainStage;
- // EEM1 only: Book 2 repeats the Book 1 cases, so this option keeps the
- // original solve state but jumps the active chain straight to Book 3.
- // London has 50 distinct cases and no Book 3, so the option does not apply.
if (!isLondon() && _chainStage == 1 &&
ConfMan.getBool("skip_repeated_cases"))
_chainStage = 3;
else
_chainStage++;
- // London has only two books: `_DisplayCorrect @ 1ea1:0619` collapses
- // chainStage 3 straight to 4 (every case solved â game complete).
if (isLondon() && _chainStage == 3)
_chainStage = 4;
@@ -319,8 +306,6 @@ void EEMEngine::advanceChainStageAfterSolve(uint mysteryNum) {
}
void EEMEngine::applySkipRepeatedCasesOption() {
- // EEM1 only â the option jumps past the repeated Book 2 to Book 3, which
- // London does not have (see `advanceChainStageAfterSolve`).
if (isLondon() || !ConfMan.getBool("skip_repeated_cases"))
return;
if (_mystery.isLoaded())
@@ -335,7 +320,6 @@ void EEMEngine::applySkipRepeatedCasesOption() {
}
Common::Error EEMEngine::run() {
- // _SetMode13X @ 1000:0358 â VGA mode 13h.
initGraphics(kScreenWidth, kScreenHeight);
if (!isFloppy() && ConfMan.getBool("restored_content")) {
@@ -367,13 +351,6 @@ Common::Error EEMEngine::run() {
_audio->setVoiceEnabled(_voiceOn);
syncSoundSettings();
- // CD `_main @ 1a35:0f59` and floppy `_main_Floppy @ 19bb:1012` both
- // load `_GetPicture(0x50)` as the active mouse pointer before
- // `_InitMouse`. PIC 0x51 is present in both archives but has no
- // executable xrefs and appears to be the wait cursor.
- // CD's `_SwitchMouse` supports swapping to a hotspot cursor ID stored
- // at search record +0x0c, but the shipped CD mystery data only uses
- // cursor 0; floppy search records have no cursor-id field.
installMouseCursor(_picsArchive, false);
CursorMan.showMouse(false);
@@ -413,10 +390,6 @@ Common::Error EEMEngine::run() {
if (resumed)
goto screenLoop;
- // EEM2 ("Eagle Eye Mysteries in London"): opening sequence, then screen
- // 8 profile selection, screen 9 partner selection. A freshly-created
- // detective starts the training case (M0) after choosing Jake/Jennifer;
- // an existing profile resumes its saved menu/map state.
if (isLondon()) {
runLondonStartup();
if (!shouldQuit()) {
@@ -439,37 +412,8 @@ Common::Error EEMEngine::run() {
goto screenLoop;
}
- // _DoOpeningAnims @ 2520:082a:
- // EA Kids logo (PIC) -> HighScore logo (PIC) -> Storm logo
- // (BOLT.ANM) -> [music starts] -> 20 character-intro anims
- // (ANIM01.A..ANIM20.A) -> [music restarts] -> TITLE.ANM. Click /
- // any key skips one clip; ESC raises _skipIntro so each step bails
- // out.
- // Music timing (2520:0883 + 2520:0918):
- // - The three logos and `_InitMysterySounds(0x3c)` run BEFORE any
- // `_MIDIPlayFile` â those segments are voice-only.
- // - Theme starts with `_LoopMIDI = 0x7fff` right before the
- // ANIM01..ANIM20 loop (2520:0883).
- // - After the loop the original calls `_CleanMysterySounds` then
- // `_MIDIPlayFile("theme.xmi")` again with `_LoopMIDI = 0xffff`
- // (2520:0918) to restart for TITLE.ANM.
- // - `_StopMIDI()` runs on keypress at the title screen (2520:094c).
_skipIntro = false;
if (isFloppy()) {
- // Floppy opening â `FUN_23d2_039c @ 23d2:039c`:
- // FUN_23d2_0170() â clear palette
- // FUN_23d2_004b() â set up timer
- // FUN_23d2_050c() â PIC 0x54 (EA Kids, palette 0x25)
- // FUN_23d2_06c6() â PIC 0x20c (High Score, palette 0x27)
- // FUN_23d2_0605() â PIC 0x20b (Storm, palette 0x26) AND
- // voice slot 25 = "thunder.voc" (via Jake
- // voice table 2608:0f0e slot 25 â
- // 2608:11ac).
- // _MIDIPlayFile("theme.xmi", loop=1)
- // _PlayANM(0) â CHAT.ANM (filename table 2608:14fe[0] â
- // "chat.anm" at 2608:150a)
- // _PlayANM(1) â MOVIE.ANM (table[1] â 2608:1513)
- // TITLE.ANM is shown later by screen-0xb handler @ 19bb:0ebc.
if (!shouldQuit() && !_skipIntro)
showEAKidsLogo();
if (!shouldQuit() && !_skipIntro)
@@ -488,10 +432,6 @@ Common::Error EEMEngine::run() {
showEAKidsLogo();
if (!shouldQuit() && !_skipIntro)
showHighScoreLogo();
- // Storm Software logo: voice + animation. `_ShowStormLogo @
- // 2520:0707` calls `_LoadSoundName("thunder.voc")` (29be:177d)
- // and passes the buffer to `OpenDifferenceAnimation_Sound` so
- // the thunder roar plays alongside the lightning-bolt BOLT.ANM.
if (!shouldQuit() && !_skipIntro) {
if (_audio)
_audio->playVoc(Common::Path("THUNDER.VOC"));
@@ -502,7 +442,7 @@ Common::Error EEMEngine::run() {
if (_audio)
_audio->stopVoice();
}
- // _InitMysterySounds(0x3c) @ 2520:086a â load M60.SDX/SDB.
+
if (!shouldQuit() && !_skipIntro && _audio)
_audio->initMysterySounds(60);
if (!shouldQuit() && !_skipIntro && _music)
@@ -514,17 +454,13 @@ Common::Error EEMEngine::run() {
Common::String name = Common::String::format("ANIM%02d.A", i);
playAnm(Common::Path(name), 120,
/* holdLastFrame= */ false, fadeIn);
- // _SpoolSound(uVar3 - 1) @ 2520:08c2 â per-anim VO, skipped
- // when uVar3 == 0x14 @ 2520:08a8.
if (!shouldQuit() && !_skipIntro && i != 20 && _audio) {
_audio->spoolSound((uint)(i - 1));
_audio->waitForSpoolDone();
}
}
- // _CleanMysterySounds @ 2520:0903.
if (_audio)
_audio->cleanMysterySounds();
- // _MIDIPlayFile("theme.xmi") @ 2520:0918.
if (!shouldQuit() && !_skipIntro && _music)
_music->playFile(Common::Path("THEME.XMI"), /* loop= */ true);
if (!shouldQuit() && !_skipIntro)
@@ -539,13 +475,11 @@ Common::Error EEMEngine::run() {
goto screenLoop;
}
- // Title(B) -> screen 8 (profile) -> 9 (partner) -> C (action) ->
- // A (case selection) -> site loop.
CursorMan.showMouse(true);
if (_music)
_music->stop();
- // screen8_handler @ 1c33:1012.
+
if (!shouldQuit())
doProfilePicker();
if (!shouldQuit())
@@ -555,16 +489,6 @@ Common::Error EEMEngine::run() {
if (!shouldQuit())
doChoosePartner();
- // Drop into the screen-driver state machine â same pattern as
- // `_ScreenDriver @ 1a35:0dc1` + the per-screen handler table at
- // 1a35:0e5e. Sentinel `kScreenInvalid` (0xFFFF) ends the loop.
- // `_DoChoosePartner @ 1a35:099d` writes `_NextScreen = 0xc` (the
- // original `_ActionScreen`, which is separate from handler 10's
- // `_DoChooseMystery` / `_CaseSelection`).
- // Mid-mystery resume: if the loaded save had `hasMystery` set,
- // drop straight to MAP rather than the action menu so the player
- // doesn't walk back through the case picker (which would
- // `_mystery.load()` fresh and discard site / clue progress).
if (!shouldQuit() && !resumed)
_nextScreen = _mystery.isLoaded() ? kScreenMap : kScreenAction;
screenLoop:
@@ -574,9 +498,6 @@ screenLoop:
switch (current) {
case kScreenTitle:
- // Floppy handler 0xb _HandleScreen11_Title_Floppy ->
- // _DoTitle_Floppy -> _PlayTitleANM_Floppy(1)=TITLE.ANM.
- // Writes _NextScreen=8 (profile picker).
_nextScreen = kScreenProfile;
if (isFloppy()) {
CursorMan.showMouse(false);
@@ -588,10 +509,6 @@ screenLoop:
break;
case kScreenAction:
- // Top-level post-profile / post-mystery menu. `_ActionScreen
- // @ 1c33:195b` shows the 5-entry "Choose A Mystery /
- // Practice / ScrapBook 1..3" picker; writes _NextScreen=0xa
- // only when the player picks "Choose A Mystery".
_nextScreen = kScreenInvalid;
doActionScreen();
if (_nextScreen == kScreenInvalid && _mystery.isLoaded())
@@ -599,8 +516,6 @@ screenLoop:
break;
case kScreenChooseMystery:
- // Handler 10 @ 1a35:0e0e -> _DoChooseMystery (presets
- // _NextScreen=0 INIT_CLUES) -> _CaseSelection.
_nextScreen = kScreenInvalid;
doCaseSelection();
if (_nextScreen == kScreenInvalid && _mystery.isLoaded())
@@ -608,8 +523,6 @@ screenLoop:
break;
case kScreenInitClues:
- // Handler 0 @ 1a35:0e14 -> _PreLoad + _DoInitClues, writes
- // _NextScreen=1 (MAP).
doInitClues();
_nextScreen = _mystery.isLoaded() ? kScreenMap
: kScreenAction;
@@ -617,11 +530,6 @@ screenLoop:
case kScreenMap:
case kScreenMapAlt:
- // Handler 1/2 @ 1a35:0e25 -> `_DoMapScreen @ 20fe:120b`
- // (floppy 19bb:0ef3 -> 1fed map code), which manages its
- // own _NextScreen writes: 3 (site clicked), 6 (setup), or
- // 0xffff (quit). After `doBigMap` returns the natural
- // next state is SITE.
doBigMap();
if (!_mystery.isLoaded())
_nextScreen = kScreenAction;
@@ -630,9 +538,6 @@ screenLoop:
break;
case kScreenSite:
- // Handler 3 @ 1a35:0e2c -> `_DoSiteLoop @ 168d:03f4`
- // (floppy dispatches through 1652). Site writes _NextScreen
- // for PDA / map rather than entering those as nested modals.
doSiteLoop();
if (!_mystery.isLoaded())
_nextScreen = kScreenAction;
@@ -641,8 +546,6 @@ screenLoop:
break;
case kScreenNotebook:
- // Handler 4 â PDA notebook screen. Button handler writes
- // 2 (map), 3 (site), 5 (gallery), or 7 (accuse).
doNotebook();
if (!_mystery.isLoaded() && _nextScreen != kScreenAction)
_nextScreen = kScreenAction;
@@ -651,8 +554,6 @@ screenLoop:
break;
case kScreenGallery:
- // Handler 5 â suspect gallery. ESC and the site button
- // write 3, the map button writes 2, the PDA button writes 4.
doGallery();
if (!_mystery.isLoaded() && _nextScreen != kScreenAction)
_nextScreen = kScreenAction;
@@ -661,9 +562,6 @@ screenLoop:
break;
case kScreenSetup:
- // Handler 6 @ 1a35:0e48 -> _DoSetup. EEM1 = seg-1f78 (`doSetup`);
- // EEM2/London = seg-2046 (`doSetupLondon`, 4 toggles + own layout).
- // Entered from BigMap setup button (_NextScreen=6 @ 20fe:0c33).
if (isLondon())
doSetupLondon();
else
@@ -671,9 +569,6 @@ screenLoop:
break;
case kScreenProfile:
- // Handler 8: CD screen8_handler @ 1c33:1012 ->
- // _NewPlayer; floppy _HandleScreen8_NewPlayer_Floppy @
- // 19bb:0ec2 writes screen 9.
_nextScreen = kScreenInvalid;
_mystery.clear();
doProfilePicker();
@@ -697,8 +592,6 @@ screenLoop:
break;
case kScreenAccuse:
- // Handler 7 â accusation flow. Failed accusation returns to
- // `_LastScreen`; a correct solution writes 0xc (ACTION).
doAccuse();
if (!_mystery.isLoaded() && _nextScreen != kScreenAction)
_nextScreen = kScreenAction;
@@ -727,9 +620,6 @@ void EEMEngine::setInteractiveMouseCursor(bool active) {
}
void EEMEngine::setHotspotMouseCursor(bool active) {
- // EEM2 swaps the cursor SHAPE per hotspot (see setSiteHotspotCursorId), so
- // the EEM1 red-outline recolor doesn't apply. The bool path only resets to
- // the default arrow (cursor 0) when leaving a hotspot / the site loop.
if (isLondon()) {
if (!active)
setSiteHotspotCursorId(0);
@@ -741,7 +631,6 @@ void EEMEngine::setHotspotMouseCursor(bool active) {
void EEMEngine::setSiteHotspotCursorId(int cursorId) {
if (!isLondon())
return;
- // `_SwitchMouse @ 17ee:2c83`: cursor 4 (Jake hand) becomes 5 for Jenny.
if (cursorId == 4 && _partner == kPartnerJenny)
cursorId = 5;
if (cursorId < 0 || cursorId >= (int)ARRAYSIZE(kLondonCursorPics))
@@ -766,7 +655,6 @@ void EEMEngine::setSiteHotspotCursorId(int cursorId) {
}
bool EEMEngine::openArchives() {
- // _InitGraphicsSystem @ 172b:0145.
if (!_picsArchive.open(Common::Path("PICS.DBD"), Common::Path("PICS.DBX"))) {
warning("PICS archive missing");
return false;
@@ -779,10 +667,6 @@ bool EEMEngine::openArchives() {
warning("SITES archive missing â site backgrounds disabled");
if (!_balloonArchive.open(Common::Path("BALLOON.DBD"), Common::Path("BALLOON.DBX")))
warning("BALLOON archive missing â clue text will lack balloons");
- // `_GetButton @ 172b:199d` reads from this archive (see strings
- // 'button.dbd' / 'Button.DBX' at 29be:06bf / 29be:04bb). Each
- // per-site map marker (used by `_StampButtons @ 20fe:0d2f` and
- // looked up via MapData[+0]) lives here.
if (!_buttonArchive.open(Common::Path("BUTTON.DBD"), Common::Path("BUTTON.DBX")))
warning("BUTTON archive missing â map markers will be unlabelled");
return true;
@@ -790,8 +674,6 @@ bool EEMEngine::openArchives() {
bool EEMEngine::loadSitePalettes() {
Common::File f;
- // EEM1 ships "SITEPALS" (40 palettes); EEM2/London ships "SITEPALS."
- // with a trailing dot (63 palettes). _ReadPalettes @ 17ee:0cdb.
const char *palFile = isLondon() ? "SITEPALS." : "SITEPALS";
if (!f.open(Common::Path(palFile))) {
warning("%s missing", palFile);
@@ -808,12 +690,8 @@ bool EEMEngine::loadSitePalettes() {
}
bool EEMEngine::getSitePalette(uint num, byte *out) const {
- // EEM1 SITEPALS has kNumSitePals (40) entries; EEM2/London SITEPALS.
- // has 63. Validate against the actual loaded buffer so both work.
if (_sitePals.size() < (num + 1) * kPalSize)
return false;
- // SITEPALS stores 6-bit VGA-DAC values (0..63); ScummVM expects
- // 8-bit (0..255), so left-shift by 2 like the original VGA hardware.
const byte *src = _sitePals.data() + num * kPalSize;
for (uint i = 0; i < kPalSize; i++)
out[i] = (byte)(src[i] << 2);
@@ -848,13 +726,6 @@ bool EEMEngine::setAnmPalette(const Common::Path &anmPath) {
}
void EEMEngine::interruptAudio(bool stopMusicToo) {
- // Mirrors `_CleanMysterySounds @ 202f:05a5` + `_StopMIDI @
- // 20a2:0512` â both fire when the player aborts the opening-anim
- // chain or dismisses the title (`_DoOpeningAnims` writes
- // `_LoopMIDI = 0; _StopMIDI();` after the title-input loop).
- // Conversation / clue-dialog skip paths pass `stopMusicToo = false`
- // so the site / briefing MIDI keeps going across an ESC â only the
- // per-line voice + spool need to stop.
if (_audio) {
_audio->stopVoice();
_audio->stopSpool();
@@ -902,8 +773,6 @@ void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs,
}
g_system->updateScreen();
- // Original uses _CheckFrameRate / _kbhit; fixed delay here.
- // ESC sets _skipIntro and interrupts audio.
const uint32 frameStart = g_system->getMillis();
bool aborted = false;
while (g_system->getMillis() - frameStart < frameDelayMs && !aborted) {
@@ -926,10 +795,6 @@ void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs,
break;
}
}
- // Refresh cursor overlay every tick â otherwise
- // the cursor only redraws when the next frame is blitted
- // (~8 Hz at 120 ms), perceived as choppy during long
- // animations like SCRAPBK.ANI.
g_system->updateScreen();
g_system->delayMillis(5);
}
@@ -938,7 +803,6 @@ void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs,
}
if (holdLastFrame && !shouldQuit() && !_skipIntro) {
- // _DoOpeningAnims tail: while (!keyDataAvailable).
while (!shouldQuit()) {
Common::Event ev;
bool clicked = false;
@@ -965,14 +829,12 @@ void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs,
g_system->updateScreen();
g_system->delayMillis(20);
}
- // _DoOpeningAnims @ 2520:0945: _LoopMIDI=0; _StopMIDI().
if (_music)
_music->stop();
}
}
void EEMEngine::blitAt(const Picture &pic, int x, int y) {
- // Clip against the 320x200 frame buffer.
const int w = MIN<int>(pic.surface.w, kScreenWidth - x);
const int h = MIN<int>(pic.surface.h, kScreenHeight - y);
if (w <= 0 || h <= 0)
@@ -982,9 +844,6 @@ void EEMEngine::blitAt(const Picture &pic, int x, int y) {
}
void EEMEngine::waitForInput(uint32 maxMs) {
- // ESC: _skipIntro + interruptAudio (matches _CleanMysterySounds +
- // _StopMIDI around _DoOpeningAnims title wait).
- // Only Return/KP-Enter/Space/Escape advance.
setInteractiveMouseCursor(false);
const uint32 startMs = g_system->getMillis();
while (!shouldQuit() && (g_system->getMillis() - startMs < maxMs)) {
@@ -1017,12 +876,7 @@ void EEMEngine::waitForInput(uint32 maxMs) {
}
}
-// _OpenColorCycle @ 2520:04f7. Rotate `fpal[start..end]` by one slot:
-// saved = fpal[end]
-// for u = end..start+1: fpal[u] = fpal[u-1]
-// fpal[start] = saved
-// If `show`, upload `end - start` entries (fpal[start..end-1]) â note that
-// fpal[end] is rotated in memory but intentionally not uploaded each tick.
+// _OpenColorCycle @ 2520:04f7. Rotate `fpal[start..end]` by one slot
void openColorCycle(byte *fpal, uint8 start, uint8 end, bool show) {
if (end <= start)
return;
@@ -1042,32 +896,8 @@ void openColorCycle(byte *fpal, uint8 start, uint8 end, bool show) {
end - start);
}
}
-
+// _ShowEAKids @ 2520:05f0:
void EEMEngine::showEAKidsLogo() {
- // _ShowEAKids @ 2520:05f0:
- // _GetPicture(0x54) + memcpy to 0xa000 (VGA).
- // _GetPalette(0x25) loads pal 0x25 into _fpal (NOT uploaded to DAC).
- // FRAME_RATE = 0x19 (25 fps); _InitFrameReg.
- // for j in 0..1: show = j;
- // for u in 0..0x37 (= 55):
- // if (show) wait for next 25-fps tick (abort on key/click).
- // _OpenColorCycle(0x01, 0x6e, show) // bg / outer ring shimmer
- // _OpenColorCycle(0x81, 0xee, show) // inner gradient shimmer
- // if (--delay == 0) {
- // delay = 8;
- // _OpenColorCycle(0x70, 0x80, show) // mid band
- // }
- // if (!abort) {
- // for i in 0..5: _OpenColorCycle(0x70, 0x80, 1);
- // for i in 0..0x23: wait one frame;
- // }
- // _OpenFadeOut().
- //
- // Pass 1 (j=0, show=0) pre-rolls _fpal 55 frames in memory only â no
- // DAC upload, no frame sync. Pass 2 (j=1, show=1) uploads each shift
- // at 25 fps. Without the pre-roll, the logo first appears at the
- // unrotated palette-0x25 phase instead of the intended "55-shifts-in"
- // phase.
Picture pic;
if (!_picsArchive.getPicture(kPicEAKidsLogo, pic)) {
warning("EA Kids logo (%u) load failed", kPicEAKidsLogo);
@@ -1075,16 +905,13 @@ void EEMEngine::showEAKidsLogo() {
}
blitAt(pic, 0, 0);
- // _GetPalette(0x25) â load into our shadow buffer; do not upload.
- // The logo bitmap is on screen but invisible until the first
- // _OpenColorCycle(..., show=1) upload in pass 2 lights it up.
byte fpal[kPalSize];
if (!getSitePalette(kPalEAKids, fpal)) {
warning("EA Kids palette (%u) load failed", kPalEAKids);
return;
}
- const uint kFrameMs = 40; // FRAME_RATE = 0x19 (25 fps).
+ const uint kFrameMs = 40;
bool aborted = false;
for (uint j = 0; j < 2 && !aborted && !shouldQuit(); j++) {
@@ -1138,13 +965,11 @@ void EEMEngine::showEAKidsLogo() {
g_system->updateScreen();
waitForInput(0x23 * kFrameMs);
- // _OpenFadeOut @ 2520:0093 â 16 linear steps from current palette to black.
fadeCurrentPaletteToBlack();
}
+// _ShowHScoreLogo @ 2520:0799: PIC 0x20c + palette 0x27;
void EEMEngine::showHighScoreLogo() {
- // _ShowHScoreLogo @ 2520:0799: PIC 0x20c + palette 0x27;
- // _OpenFadeIn; 50-tick wait @ 25 fps; _OpenFadeOut.
Picture pic;
if (!_picsArchive.getPicture(kPicHighScoreLogo, pic)) {
warning("HighScore logo (%u) load failed", kPicHighScoreLogo);
@@ -1152,7 +977,6 @@ void EEMEngine::showHighScoreLogo() {
}
blitAt(pic, 0, 0);
- // Force black before fade-in to avoid a 1-frame full-logo flash.
byte target[kPalSize];
if (!getSitePalette(kPalHighScore, target)) {
warning("HighScore palette (%u) load failed", kPalHighScore);
@@ -1163,17 +987,12 @@ void EEMEngine::showHighScoreLogo() {
g_system->updateScreen();
fadePaletteFromBlack(target);
- // 50 ticks @ 25 fps.
waitForInput(2000);
fadeCurrentPaletteToBlack();
}
void EEMEngine::showLondonLogo(uint picId, uint palId, uint holdMs) {
- // Parameterised twin of `showHighScoreLogo` for EEM2's opening logos
- // (`_ShowEAKids` @ 2721:05e3 and `_ShowHScoreLogo` @ 2721:084d both do
- // _GetPicture(pic) -> blit -> _GetPalette(pal) -> fade in -> hold ->
- // fade out).
Picture pic;
if (!_picsArchive.getPicture(picId, pic) || pic.surface.empty()) {
warning("London logo PIC 0x%x load failed", picId);
@@ -1211,15 +1030,6 @@ bool EEMEngine::startLondonTrainingMystery() {
}
void EEMEngine::runLondonStartup() {
- // Full opening sequence â EEM2 `_DoOpeningAnims` @ 2721:08e6:
- // _ShowEAKids @ 2721:05e3 â PIC 0x54, palette 0x3c
- // FUN_2721_07be @ 2721:07be â PIC 0x20c, palette 0x3e (publisher logo)
- // _ShowStormLogo@ 2721:0729 â anim 0 ("bolt.anm") + "thunder.voc"
- // _MIDIPlay(0x65)=MUS00101.XMI; anim 1 ("movie.anm")
- // _MIDIPlay(0x66)=MUS00102.XMI (loop); anim 2 ("wave.anm"), looped
- // _MIDIPlay(0x67)=MUS00103.XMI; fade out
- // Anim names come from the table @ DS:1b54 -> {bolt,movie,wave}.anm;
- // _MIDIPlay(n) maps to MUS%05d.XMI (n).
const uint32 kHoldForever = 0xFFFFFFFFu;
CursorMan.showMouse(false);
_skipIntro = false;
@@ -1273,15 +1083,8 @@ void EEMEngine::runLondonStartup() {
debugC(1, kDebugGeneral, "EEM2 (London): intro + profile selection done");
}
+// `_NewPlayer` @ 1cd3:0f27 â character creation over background PIC 0xc
void EEMEngine::showLondonCharSelect() {
- // `_NewPlayer` @ 1cd3:0f27 â character creation over background PIC 0xc
- // (palette 0). Two text fields then a male/female player-gender pick
- // (left/right arrow 0x4b/0x4d -> DAT_4c4c, Enter confirms). The
- // field/box rects are constants in EEM2's data segment (read at
- // 2bca:0e3a); they map to the `pr` player record's FirstName[12] /
- // LastName[20]:
- // first name : (54,75)-(151,85) last name : (167,75)-(266,85)
- // male : (110,116)-(120,122) female : (190,116)-(200,122)
debugC(1, kDebugGeneral, "EEM2 (London): character creation");
const Common::Rect kFirstRect(54, 75, 151, 85);
@@ -1289,10 +1092,7 @@ void EEMEngine::showLondonCharSelect() {
const Common::Rect kMaleBox(110, 116, 120, 122);
const Common::Rect kFemaleBox(190, 116, 200, 122);
const uint kMaxFirst = 12, kMaxLast = 20;
- // `_GetNameString @ 1cd3:0ddc` draws both the typed name and the caret in
- // colour 0x22 (the passport-field ink); the old 0x0F was a guess and the
- // underscore caret in it was effectively invisible.
- const uint8 kInkColor = 0x22; // typed-name ink + caret
+ const uint8 kInkColor = 0x22;
Picture bg;
const bool haveBg = _picsArchive.getPicture(0xc, bg) && !bg.surface.empty();
@@ -1347,8 +1147,6 @@ void EEMEngine::showLondonCharSelect() {
getFont().drawString(&scratch, last, kLastRect.left + 2,
kLastRect.top + 1, kLastRect.width(),
kInkColor);
- // Caret = solid block `_FillRect(x+1, y, 6, 0xb, 0x22)` at the
- // input point (`_GetNameString @ 1cd3:0ddc`), NOT an underscore.
if (blink && (field == kFieldFirst || field == kFieldLast)) {
const Common::Rect &fr =
(field == kFieldFirst) ? kFirstRect : kLastRect;
@@ -1364,8 +1162,6 @@ void EEMEngine::showLondonCharSelect() {
}
}
if (field == kFieldGender) {
- // DOS restores both boxes, then fills the active one with the
- // darker passport border color sampled at (109,115).
scratch.fillRect(kMaleBox, boxBlankColor);
scratch.fillRect(kFemaleBox, boxBlankColor);
scratch.fillRect(female ? kFemaleBox : kMaleBox,
@@ -1456,10 +1252,6 @@ void EEMEngine::showLondonCharSelect() {
blink = !blink;
needRedraw = true;
}
- // Flush every frame so the mouse cursor tracks smoothly. The scratch
- // rebuild + copyRectToScreen above is gated on needRedraw (and mouse
- // motion doesn't set it), but the cursor is only re-composited by
- // updateScreen(), so without this it moved only on blink/keypress.
g_system->updateScreen();
g_system->delayMillis(15);
}
@@ -1501,13 +1293,8 @@ void EEMEngine::showLondonCharSelect() {
s.getDescription().c_str(), s.getSaveSlot());
}
- // New profile. EEM2 `_NewPlayer @ 1cd3:0f27` initializes the player
- // record only when `_LoadPlayerRecord` failed, then immediately saves it.
_playerName = displayName.empty() ? "Detective" : displayName;
_chainStage = 1;
- // `_NewPlayer @ 1cd3:0f27` stores the passport gender pick (`DAT_3036_4c4c`).
- // Drives the player pronouns 0x86-0x88 in `parseString`. (Existing profiles
- // take the gender from their save instead, via the load path above.)
_playerFemale = female;
_voiceOn = true;
if (_audio)
@@ -1525,14 +1312,8 @@ void EEMEngine::showLondonCharSelect() {
_playerName.c_str(), profileKey.c_str(),
female ? "female" : "male");
}
-
+// Floppy storm-logo splash â `23d2:0605`:
void EEMEngine::showFloppyStormLogo() {
- // Floppy storm-logo splash â `FUN_23d2_0605 @ 23d2:0605`:
- // GetPicture(0x20b); BlitToVGA;
- // if (sound) { LoadVOC(slot 25 = "thunder.voc"); PlayVOC(...); }
- // GetPalette(0x26); FadeIn; wait 50 ticks; FadeOut.
- // CD plays `BOLT.ANM` here with `THUNDER.VOC` overlaid; floppy uses
- // a static still + the same VOC.
Picture pic;
if (!_picsArchive.getPicture(kPicStormLogo, pic)) {
warning("Storm logo (%u) load failed", kPicStormLogo);
@@ -1561,27 +1342,13 @@ void EEMEngine::showFloppyStormLogo() {
}
void EEMEngine::doSiteLoop() {
- // Per-mystery site loop. SiteScreen::run() handles hotspot clicks
- // plus M (map), N (notebook), G (gallery), A (accuse), Tab (next
- // site), ESC (exit).
SiteScreen screen(this, &_mystery);
screen.run();
setHotspotMouseCursor(false);
}
+// _StartTravelMusic @ 20a2:0595:
void EEMEngine::startTravelMusic() {
- // _StartTravelMusic @ 20a2:0595:
- // for (num = _SiteNumber; num > 4; num -= 5) {}
- // if (_MIDIAvailable && _MusicEnabled) {
- // if (_IsMIDIPlaying()) _StopMIDI();
- // _MIDIPlay(num);
- // }
- // Five travel tracks (MUS00000.XMI..MUS00004.XMI), picked by
- // `_SiteNumber % 5`. ONE-SHOT â `_DoOpeningAnims @ 2520:0945` resets
- // `_LoopMIDI = 0` after the title-screen wait, and the function
- // doesn't write it; combined with `_DoSiteLoop @ 168d:06c0` calling
- // `_StopMIDI()` before the interactive phase, travel music plays
- // ONCE during the entrance animation only.
if (!_music || !_mystery.isLoaded() ||
(isLondon() ? !_musicOn : !_voiceOn))
return;
@@ -1590,8 +1357,6 @@ void EEMEngine::startTravelMusic() {
}
void EEMEngine::startLondonTravelMusic(uint8 travelKind) {
- // EEM2 `_DoTravel @ 1717:06ae`: travelKind * 6 indexes this table,
- // then rand() picks one of three u16 MUS IDs.
static const uint16 kLondonTravelMusic[4][3] = {
{ 0, 0, 0 },
{ 3, 22, 25 },
@@ -1644,10 +1409,6 @@ void EEMEngine::syncSoundSettings() {
}
bool EEMEngine::hasFeature(EngineFeature f) const {
- // We support saving any time but loading only at startup (via the
- // `--save-slot=N` resume path or a slot picked from the launcher).
- // Runtime loads would replace `_mystery._data` while pointers into
- // it are alive on the stack inside `displayClue` etc.
return f == kSupportsSavingDuringRuntime ||
f == kSupportsReturnToLauncher;
}
@@ -1675,7 +1436,7 @@ Common::Error EEMEngine::saveGameStream(Common::WriteStream *stream,
// +0x00..+0x0b : player name (12 chars, null-padded)
// +0x0c..+0x1f : random ID bytes for `_GenerateFilename`
// (29be:0dbf "C:\EEMCDSAV\%s.PLR") â irrelevant to
- // ScummVM saves which key on slot, not filename.
+ // us since saves which key on slot, not filename.
// +0x20..+0x28 : derived 8-char .PLR basename â likewise unused.
// +0x2d : voice-enable flag (`DAT_2d5d_3f97`, default 1)
// +0x2f : chain stage (`DAT_2d5d_3f99`, 1=A, 2=B, 3=C;
@@ -1691,15 +1452,11 @@ Common::Error EEMEngine::saveGameStream(Common::WriteStream *stream,
s.syncAsByte(_partner);
s.syncAsByte(_chainStage);
s.syncAsByte(_voiceOn);
- // EEM2/London music (MIDI) toggle â `DAT_3036_4cc0`, separate from voice.
s.syncAsByte(_musicOn);
- // London passport gender (player pronouns 0x86-0x88).
byte playerFemale = _playerFemale ? 1 : 0;
s.syncAsByte(playerFemale);
- // Mid-case resume: persist in-progress mystery (no equivalent in
- // _LoadGame @ 2404:0dc7).
bool hasMystery = _mystery.isLoaded();
s.syncAsByte(hasMystery);
if (hasMystery) {
@@ -1738,7 +1495,6 @@ Common::Error EEMEngine::loadGameStream(Common::SeekableReadStream *stream) {
if (_audio)
_audio->setVoiceEnabled(_voiceOn);
- // London passport gender (player pronouns 0x86-0x88).
byte playerFemale = 0;
s.syncAsByte(playerFemale);
_playerFemale = (playerFemale != 0);
@@ -1753,13 +1509,6 @@ Common::Error EEMEngine::loadGameStream(Common::SeekableReadStream *stream) {
resetSiteArrivalState();
return Common::kReadingFailed;
}
- // `_ReadMystery @ 2404:008f` tail-calls `_InitMysterySounds`
- // (2404:0298) so the SDB index is in place for clue and
- // partner-speech spool sounds. Floppy ships individual
- // `M-XXXX.VOC` files instead of the bundled SDB/SDX archive,
- // so we skip the init there to avoid "missing" warnings;
- // `spoolSound` then no-ops via its `_currentMystery < 0` guard
- // until the per-voice VOC mapping is wired up.
if (_audio && !isFloppy())
_audio->initMysterySounds(mysteryNum);
_mystery.syncState(s);
@@ -1781,9 +1530,6 @@ Common::Error EEMEngine::loadGameStream(Common::SeekableReadStream *stream) {
}
SaveStateList EEMEngine::listProfiles() const {
- // _findfirst("*.PLR") in screen8_handler @ 1c33:1012.
- // Filter out slot 0 (autosave) to match the original which
- // has no autosave concept.
SaveStateList saves = getMetaEngine()->listSaves(_targetName.c_str());
for (uint i = 0; i < saves.size(); ) {
if (saves[i].getSaveSlot() == 0)
@@ -1813,8 +1559,7 @@ Common::Error EEMEngine::saveProfile(const Common::String &name) {
}
}
- // New profile: pick lowest unused slot >=1 (slot 0 is autosave;
- // DOS limit was 25 per screen8_handler local_8c[0x19][2]).
+ // New profile: pick lowest unused slot >=1 (slot 0 is autosave);
if (slot < 0) {
const int maxSlot = getMetaEngine()->getMaximumSaveSlot();
Common::Array<bool> used(maxSlot + 1);
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 9f4d98c7856..4ea98b5aa65 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -47,18 +47,14 @@ namespace EEM {
class AudioPlayer;
class MusicPlayer;
-/// VGA palette size in bytes (256 colours à RGB). Defined in eem.cpp.
+/// VGA palette size in bytes (256 colours à RGB)
const uint kPalSize = 768;
-/// Palette fade helpers (defined in eem.cpp): ramp the VGA palette to / from
-/// black over 16 steps, matching `_FadeToBlack` / `_OpenFadeIn`.
void fadeCurrentPaletteToBlack(uint delayMs = 8);
void fadePaletteFromBlack(const byte *target, uint delayMs = 8);
/// _ScreenDriver dispatch table @ 1a35:0e5e (fallback @ 1a35:0e54).
-/// 14 entries total: each is a screen ID + near fn ptr at +0x1c; driver
-/// tail-calls via `JMP word ptr CS:[BX + 0x1c]`. Handler bodies update
-/// _NextScreen before returning. Handlers @ 1a35:0dec..0e4f:
+/// 14 entries total:
///
/// 0 INIT_CLUES â `_PreLoad` + `_DoInitClues`, writes _NextScreen=1
/// 1 MAP â `_DoMapScreen @ 20fe:120b` (writes 3=site clicked,
@@ -81,10 +77,6 @@ void fadePaletteFromBlack(const byte *target, uint delayMs = 8);
/// 0xc ACTION â `_ActionScreen @ 1c33:195b` â Choose A Mystery /
/// Practice / See ScrapBook 1..3. Action 1 sets =0xa.
/// 0xFFFF SENTINEL â exit loop
-///
-/// State writes (via xrefs to `_NextScreen @ 2d5d:3f26`): `_DisplayCorrect`
-/// writes 0xc, `_DisplayAlibi` writes `_LastScreen`, `_DoSiteLoop` writes
-/// 1/3/4 plus 0xffff on ESC.
enum ScreenId {
kScreenInvalid = 0xFFFF,
kScreenInitClues = 0x00,
@@ -103,15 +95,11 @@ enum ScreenId {
};
/// Distribution variant from `ADGameDescription::extra` (set by
-/// `gameDescriptions[]` in `detection.cpp`). Gates filename selection
-/// (TRAVEL-N.XMI vs MUS%05u.XMI, FANFARE2.XMI vs MUS00005.XMI,
-/// PHONESL.VOC vs PHONE.VOC), opening-anim flow (MOVIE.ANM vs
-/// ANIM01..20.A), and per-variant SFX (DING.VOC / NEWSCAN.VOC ship only
-/// with floppy).
+/// `gameDescriptions[]` in `detection.cpp`).
enum Variant {
kVariantCD = 0,
kVariantFloppy = 1,
- kVariantLondonCD = 2, ///< Eagle Eye Mysteries in London (EEM2CD.EXE).
+ kVariantLondonCD = 2,
};
/// `_Partner @ 29be:7918`. Selected at the partner-pick screen
@@ -121,13 +109,9 @@ enum Partner {
kPartnerJenny = 1,
};
-/// VGA mode 13h dimensions (initGraphics(320, 200) in `EEMEngine::run`).
constexpr int kScreenWidth = 320;
constexpr int kScreenHeight = 200;
-/// Shared PDA-frame navigation rects (PIC 0x3f) â reachable from Site,
-/// Notebook, Gallery, Accuse, and MoreInfo. Original `_NoteButtons` table
-/// @ 29be:0147 + the site-screen hit tests in `_DoSiteLoop @ 168d:03f4`.
constexpr Common::Rect kPdaSiteRect (Common::Point(35, 111), 21, 25);
constexpr Common::Rect kPdaPartnerFootMapRect (Common::Point( 7, 177), 50, 23);
constexpr Common::Rect kPdaPartnerHeadHintRect (Common::Point( 5, 80), 39, 30);
@@ -143,37 +127,22 @@ public:
Common::Platform getPlatform() const;
Variant getVariant() const { return _variant; }
bool isFloppy() const { return _variant == kVariantFloppy; }
- /// EEM2 ("Eagle Eye Mysteries in London") â reimplementation in progress.
- /// Shares the EEM1 DBD/palette/font formats, but ships a 63-entry
- /// "SITEPALS." file and uses different picture/palette IDs per screen.
bool isLondon() const { return _variant == kVariantLondonCD; }
bool hasFeature(EngineFeature f) const override;
bool canLoadGameStateCurrently(Common::U32String *msg = nullptr) override;
bool canSaveGameStateCurrently(Common::U32String *msg = nullptr) override;
- // Autosave disabled: profile picker (screen8_handler @ 1c33:1012)
- // lists `*.PLR` and a slot-0 autosave would appear as a profile.
int getAutosaveSlot() const override { return -1; }
Common::Error saveGameStream(Common::WriteStream *stream,
bool isAutosave = false) override;
Common::Error loadGameStream(Common::SeekableReadStream *stream) override;
- // Per-profile saves: `_PlayerRecord` @ 2d5d:3f6a (159 bytes), written
- // by `_SavePlayerRecord @ 1c33:034f` to `C:\EEMCDSAV\<name>.PLR`.
- // Each save slot maps to one profile (slot description = the
- // player name) â same approach Wetlands uses. `screen8_handler @
- // 1c33:1012` walks `*.PLR`, lets the player pick a profile, and calls
- // `_LoadPlayerRecord`.
-
- /// `_SavePlayerRecord @ 1c33:034f`.
Common::Error saveProfile(const Common::String &name);
- /// `_LoadPlayerRecord @ 1c33:03a6`.
bool loadProfile(const Common::String &name);
- /// `_findfirst("*.PLR")` walk inside `screen8_handler`.
SaveStateList listProfiles() const;
const ADGameDescription *_gameDescription;
@@ -204,25 +173,15 @@ public:
/// EEM1 cursors are all 0 so this is a no-op there.
void setSiteHotspotCursorId(int cursorId);
- /// `_DisplayClue @ 2404:05e6`. @p clueBlock points at the u16 frame
- /// count followed by 62-byte ClueEntries.
+ /// `_DisplayClue @ 2404:05e6`.
void displayClue(const byte *clueBlock);
/// EEM2/London `_DoPuzzle @ 2542:1482`. A clue entry can gate the rest of
- /// itself behind a "check the manual / a real map" puzzle (the id is the
- /// entry+0x54 field). Opens `P<id>.BIN` â a header + question, then either
- /// a typed answer (type 0) or a click-a-region multiple choice (type 1).
- /// Returns true when answered correctly (and true if the file is missing,
- /// so the clue is never permanently blocked); a wrong answer replays the
- /// partner's scolding hint, and the caller re-prompts on the next visit.
+ /// itself behind a "check the manual / a real map" puzzle
bool doPuzzle(uint puzzleId);
- /// Floppy hotspot click. `FUN_22dc_0b80 + FUN_1652_00e6 + FUN_1652_006c`.
- /// Locates dialog records in site_data[+6] and dispatches them.
void displayFloppyHotspotDialog(uint siteNum, uint hotIdx);
- /// `_HotspotSearched_Floppy @ 22dc:096c`. Walks the per-hotspot dialog
- /// records and returns whether the hotspot's searched text index was seen.
bool floppyHotspotSearched(uint siteNum, uint hotspotIdx) const;
/// Active player name (= profile-save description).
@@ -231,19 +190,10 @@ public:
/// Apply a ClueEntry's side effects (notebook, gallery, site flags).
void applyClueSideEffects(const byte *entry);
- /// `_DrawNotes @ 161e:01d0`.
void doNotebook();
-
- /// `_DrawGallery @ 158f:0046`.
void doGallery();
-
- /// `MoreInfo @ 158f:0419`. Suspect-detail view inside the gallery; button
- /// dispatch via `_HandleMoreButton @ 158f:027d`. Returns true if the
- /// caller should exit `doGallery` (NOTEBOOK / ACCUSE / MAP).
bool moreInfo(const byte *gd, uint suspectIdx,
const Picture &galBg, bool haveBg);
-
- /// `_DoBigMap @ 20fe:09e7`.
void doBigMap();
/// Accuse flow. `_DoAccuseGallery @ 1df2:0a31` + `_DisplayEnding @ 1df2:0548`.
@@ -254,38 +204,26 @@ public:
/// Returns true if the player committed (SOLVE clicked), false on ESC.
bool doAccuseNotes();
- /// `_KDHelp @ 1560:010a` + `_DisplayHint @ 1560:0009`. Cycles two
- /// hint slots tracked via `_SawHelpHint`.
+ /// `_KDHelp @ 1560:010a` + `_DisplayHint @ 1560:0009`.
void doHelp();
/// `_InterfaceHelp @ 1560:0205`. Walks `HelpData @ 29be:00c8`.
void doInterfaceHelp(uint num = 0);
- /// `_GetKDTextBalloon @ 1df2:0105`. Digits (0..9) â table @ 29be:1064;
- /// otherwise `*(u16*)29be:1068 = 0x17`.
+ /// `_GetKDTextBalloon @ 1df2:0105`.
uint16 getKDTextBalloon(byte firstChar) const;
/// Pick a shorter balloon sibling when wrapped text leaves empty lines.
uint16 fitBalloonToText(uint16 bubNum, const Common::String &text);
- /// `_ParseString @ 1b66:07c3`. Substitutes 0x80..0x89 control bytes;
- /// jump table @ 1b66:0cbe.
+ /// `_ParseString @ 1b66:07c3`.
Common::String parseString(const Common::String &raw,
const Common::String &playerName,
uint partner) const;
- /// `_DoKDAnim @ 168d:028a` + `_PlayAnimation @ 172b:1f46`. Per-partner
- /// (animId, x, y) from `_WaitAnims[1+num] @ 29be:0228`. Blocks until
- /// the script's first 0x80 marker.
+ /// `_DoKDAnim @ 168d:028a` + `_PlayAnimation @ 172b:1f46`.
void playKdAnim(uint16 num);
- /// Provide a "clean" 320x200 backdrop (site BG + static drops, no
- /// NPCs / partner) for the next `playKdAnim` to use as the
- /// background-erase source. Without this, the camera animation would
- /// composite on top of the static partner sprite and the previous
- /// resting frame would bleed through transparent pixels. `SiteScreen`
- /// passes its `_bgSnapshot` before `displayClue` from a hotspot click.
- /// Pass `nullptr` to clear.
void setPartnerEraseBg(const Graphics::ManagedSurface *bg);
/// Balloon-text-inset metadata. 52-entry table @ 29be:0875 (CD) /
@@ -299,13 +237,10 @@ public:
bool getBalloonIndicatorPos(uint16 bubNum, uint16 &dx,
uint16 &dy) const;
- /// `FUN_22dc_05c8 @ 22dc:08aa` (mid-page) / `@ 22dc:08c0` (end).
- /// PIC 0xa0 if !endIndicator else PIC 0xa1.
void drawFloppyBubbleIndicator(Graphics::ManagedSurface &dst,
uint16 bubNum, int ballX, int ballY,
bool endIndicator);
- /// `_AreYouSure @ 1a35:0a5c`. Returns true on YES.
bool areYouSure();
private:
@@ -323,17 +258,11 @@ private:
/// Inclusive 1-based mystery range [lo, hi] for chain `stage` (1-based).
/// Returns false when `stage` is not a real tier in this variant.
- /// EEM1 `_DisplayCorrect @ 1df2:073c` (1..24 / 25..48 / 49..54);
- /// EEM2/London `_DisplayCorrect @ 1ea1:0619` (1..25 / 26..50).
bool mysteryTierRange(uint stage, uint &lo, uint &hi) const;
void advanceChainStageAfterSolve(uint mysteryNum);
void applySkipRepeatedCasesOption();
- /// Central dispatch loop matching `_ScreenDriver @ 1a35:0dc1`. Each
- /// iteration calls the handler that matches `_nextScreen`; handlers
- /// update `_lastScreen` / `_nextScreen` and return. Loop exits when
- /// `_nextScreen == kScreenInvalid`.
void screenDriver();
/// Re-render helpers for the corresponding `doX()` modal screens.
@@ -427,10 +356,7 @@ public:
void waitForInput(uint32 maxMs);
/// Play a difference-encoded animation file (.ANM / .A) on the full
- /// 320x200 screen. Mirrors the data flow of `OpenDifferenceAnimation
- /// @ 2520:0337` â `Load_Sequence` + `Play_Sequence`. Audio cues are
- /// skipped for now. The default 120 ms frame delay matches the
- /// original `FRAME_RATE = 0x78` used by `_DoOpeningAnims`.
+ /// 320x200 screen.
/// If @p holdLastFrame is true the call blocks on the final frame
/// until the user clicks or hits a key â used for the title screen.
/// If @p fadeIn is true the first decoded frame is copied while the
@@ -440,40 +366,24 @@ public:
bool setSkipIntroOnEsc = true);
private:
- /// `_CleanMysterySounds @ 202f:05a5` + `_StopMIDI @ 20a2:0512`.
- /// `stopMusicToo=false` keeps MIDI playing across dialog skips.
void interruptAudio(bool stopMusicToo = true);
void showEAKidsLogo();
void showHighScoreLogo();
void showFloppyStormLogo();
- // --- EEM2 ("Eagle Eye Mysteries in London") â reimplementation in progress ---
- /// Opening sequence + character creation â EEM2's `_DoOpeningAnims`
- /// @ 2721:08e6 then screen 8 profile selection.
void runLondonStartup();
/// Start London mystery 0 after a freshly-created detective chooses a partner.
bool startLondonTrainingMystery();
- /// Blit a full-screen still PIC and fade it in / hold / out using the
- /// given SITEPALS. palette index.
void showLondonLogo(uint picId, uint palId, uint holdMs);
- /// EEM2 character creation (`_NewPlayer`: palette 0 + background PIC 0xc):
- /// first/last name entry + male/female player-gender selection.
void showLondonCharSelect();
- /// EEM2 case-intro animation (`_DoInitClues` @ 1abf:03b3): single partner
- /// anim (Jake 0x18 / Jenny 0x71) faded in, then phone1.voc on caseType 1.
void playLondonInitCluesAnim(uint16 caseType, const Picture &bg,
bool haveBriefingBg);
- /// EEM1 CD/floppy case-intro animation (`_DoInitClues` @ 1a35:0411):
- /// game/book/nancy cycle + `_PlayInSequence` partner entrance.
void playCdFloppyInitCluesAnim(uint16 caseType, bool floppy,
const Picture &bg, bool haveBriefingBg);
- /// `screen8_handler @ 1c33:1012`. Profile selector â walks
- /// `listProfiles()`, falls through to `doNewPlayer()` if "New" or
- /// no profiles exist (1c33:1170: `if (saves == 0) _NewPlayer();`).
void doProfilePicker();
- void doNewPlayer(); ///< `_NewPlayer @ 1c33:0dda`.
+ void doNewPlayer();
void doChoosePartner();
/// Display the per-mystery ending pages from `E<num>.BIN`. Mirrors
@@ -481,11 +391,6 @@ private:
/// File format: u16 page count, then N pages of
/// `{ u16 picNum, u16 x1, u16 y1, u16 x2, u16 y2,
/// char text[] (null-terminated, ParseString placeholders) }`.
- /// `_ShowOneScrap @ 1f78:0773` is just `_DisplayEnding(num, 1)`,
- /// so this call also covers the post-mystery scrapbook view.
- /// `firstPage=true` opens at page 0; `false` opens at the last page
- /// (back-nav from `doShowScrapbook`, mirrors the `local_8 = 0` write
- /// at `_ShowScrapbook @ 1f78:067e`).
/// Returns the caller's nav direction (per `[BP-0x18]` @ 1df2:0723):
/// -1 â previous mystery (LEFT on first page or click in PrevPageRect),
/// 0 â exit scrapbook (ESC / quit),
@@ -493,28 +398,20 @@ private:
int doShowEnding(uint num, bool firstPage = true);
/// EEM1 `_ShowScrapbook(stage, 0) @ 1f78:0642`; EEM2/London scrapbook
- /// `FUN_2046_09dd`. Walks the `mysteryTierRange(stage)` cases, skipping
- /// unsolved entries in the current chain stage. Tier sizes are
- /// variant-specific (EEM1 24/24/6; London 25/25, no stage 3).
+ /// `2046:09dd`. Walks the `mysteryTierRange(stage)` cases, skipping
+ /// unsolved entries in the current chain stage.
void doShowScrapbook(uint stage);
void doActionScreen();
void doCaseSelection();
void doSiteLoop();
- /// `_DoSetup @ 1f78:044e`. Voice on/off (`DAT_2d5d_3f97`), partner
- /// pick via SwapColors on Kid1/Kid2 rects.
void doSetup();
- /// `doSetup` helpers: redraw BG + label highlights, render a help/
- /// credits card with blocking input wait, and the shared exit fallback.
void setupDrawScreen();
Common::KeyCode setupShowFullscreenPic(uint16 picId, bool transparent);
void setupLeave();
- /// EEM2/London setup screen â `_DoSetup @ 2046:067b` (distinct from EEM1's
- /// seg-1f78 screen): 4 toggles (partner / voice / music / highlight boxes),
- /// rearranged 13-button layout, scrapbook paging. Reuses the shared helpers.
void doSetupLondon();
void setupDrawScreenLondon();
@@ -569,36 +466,14 @@ private:
/// London passport gender (EEM2 `_NewPlayer @ 1cd3:0f27` gender pick,
/// `DAT_3036_4c4c`: left/0 = male, right/1 = female). Drives the player
/// pronoun opcodes 0x86/0x87/0x88 in `parseString` (he·him·his / she·her·
- /// her). EEM1 has no passport, so it stays false (male) â matching that
- /// engine's never-written gender flag.
+ /// her). EEM1 has no gender selection, so it stays false (male)
bool _playerFemale = false;
/// `_PlayerRecord.SolvedMysteries[55]`. 0=unsolved, 1=solved, 2=first-try.
uint8 _mysteriesSolved[55] = {};
-
- /// Current chain/tier the player is at â `DAT_2d5d_3f99`
- /// (`_PlayerRecord +0x2f`):
- /// 1 = Junior detective (mysteries 1..24, "A chain")
- /// 2 = Senior detective (mysteries 25..48, "B chain")
- /// 3 = Master detective (mysteries 49..54, "C chain")
- /// Initialized to 1 in `_NewPlayer @ 1c33:0fa3`; bumped by
- /// `_DisplayCorrect @ 1df2:0853` once every mystery in the current
- /// tier is solved (range checks @ 1df2:080d / 0824 / 0837). Also
- /// gates `_CaseSelection`'s book label and selection list
- /// (1c33:0a87 onwards).
uint8 _chainStage = 1;
- /// Voice / digital-audio enable flag. `DAT_2d5d_3f97` (`_PlayerRecord
- /// +0x2d`). Set to 1 by `_NewPlayer @ 1c33:0fa3`, toggled by the
- /// SoundOn / SoundOff hot-rects in `_DoSetup @ 1f78:044e`. Gates
- /// every `_PlayVoice` / `_SpoolSound` call site (clue voices,
- /// partner speech, intro VO).
bool _voiceOn = true;
-
- /// EEM2/London music (MIDI) on/off â `DAT_3036_4cc0`, toggled by the music
- /// button in `_DoSetup @ 2046:067b`. Separate from `_voiceOn` (EEM2 has both
- /// a voice and a music toggle; EEM1 only had one sound toggle). Gates the
- /// briefing / travel MIDI for London.
bool _musicOn = true;
/// Set by the profile/new-player screens. London uses it to decide whether
@@ -625,7 +500,7 @@ private:
bool _skipIntro = false;
/// Clean BG (no partner/NPC) used by `playKdAnim` between camera-anim
- /// cells. See `setPartnerEraseBg`.
+ /// cells.
Graphics::ManagedSurface _partnerEraseBg;
bool _interactiveMouseCursor = false;
@@ -643,8 +518,6 @@ private:
/// `_StartTravelMusic` @ 20a2:00e2-05c9). Constructed lazily in `run()`.
MusicPlayer *_music = nullptr;
- /// `SOUND.C` / `SPOOLSND.C`. `_PlayVoice @ 1ff1:023e`,
- /// `_SpoolSound @ 202f:068d`, `_InitMysterySounds @ 202f:05cb`.
public:
AudioPlayer *_audio = nullptr;
diff --git a/engines/eem/font.cpp b/engines/eem/font.cpp
index a5fba38d7f1..7a6fb6006e3 100644
--- a/engines/eem/font.cpp
+++ b/engines/eem/font.cpp
@@ -95,8 +95,7 @@ bool EEMFont::load(const Common::Path &path) {
_maxWidth = g.widthBits;
}
- // _LoadFont @ 1b03:0220 sets line stride to the first glyph's
- // height (DAT_28da_30ca = DAT_28da_30ce, space glyph height).
+ // _LoadFont @ 1b03:0220 sets line stride to the first glyph's height
// Descenders ('g','j','p','q','y') intentionally overhang into
// the next row.
_lineHeight = !_glyphs.empty() ? _glyphs[0].height : _maxHeight;
diff --git a/engines/eem/font.h b/engines/eem/font.h
index 56275b8018e..8c77e97c1ea 100644
--- a/engines/eem/font.h
+++ b/engines/eem/font.h
@@ -55,7 +55,6 @@ public:
bool load(const Common::Path &path);
bool isLoaded() const { return !_glyphs.empty(); }
- // --- Graphics::Font overrides ---
int getFontHeight() const override {
return _lineHeight ? _lineHeight : _maxHeight;
}
diff --git a/engines/eem/graphics.cpp b/engines/eem/graphics.cpp
index ec1f5d089ce..da96f26668c 100644
--- a/engines/eem/graphics.cpp
+++ b/engines/eem/graphics.cpp
@@ -38,12 +38,6 @@
namespace EEM {
-// HelpData @ 29be:00c8, read by _InterfaceHelp @ 1560:0205. 5-byte entries:
-// u8 count, then up to 2 u16 picIds. Verified bytes:
-// entry 0 (PDA / gallery HELP button): count=2, picIds = 0x0063, 0x01ae
-// entry 1: count=2, picIds = 0x0192, 0x01b1
-// Only entry 0 is reachable from the PDA notebook (rect 1) and the gallery
-// (rect 1) â both call _InterfaceHelp(0).
const uint16 kHelpPics[][2] = {
{ 0x0063, 0x01ae },
{ 0x0192, 0x01b1 },
@@ -114,8 +108,6 @@ bool findBalloonFamily(uint16 balloonId, uint16 &first, uint16 &last) {
return false;
}
-// indDY is the artist-intended last text line, not just the indicator Y:
-// families 3, 4, 6 and singletons have shadow/tail decoration below indDY.
uint getBalloonLineCapacity(uint16 balloonId, int lineH) {
const uint idx = balloonId & 0x7F;
if (idx >= ARRAYSIZE(kBalloonInsetTable) || lineH <= 0)
@@ -126,9 +118,6 @@ uint getBalloonLineCapacity(uint16 balloonId, int lineH) {
}
bool EEMEngine::floppyHotspotSearched(uint siteIdx, uint hotspotIdx) const {
- // FUN_22dc_096c @ 22dc:096c: walks per-site dialog records at
- // site_data[+6] to skip hotspotIdx hotspots, then returns _TextSeen for
- // the selected hotspot's searched text index.
const byte *site = _mystery.siteData(siteIdx);
if (!site)
return false;
@@ -183,13 +172,8 @@ static Common::String readPuzzleLine(Common::File &f) {
}
return s;
}
-
+// `_DoPuzzle @ 2542:1482`.
bool EEMEngine::doPuzzle(uint puzzleId) {
- // `_DoPuzzle @ 2542:1482`. File `P<id>.BIN` (LE u16 fields):
- // type(0=typed,1=choice); mainPic{id,x,y}; extraCount; extra{id,x,y}*;
- // qx; qy; qw; voiceAlt(Jenny); voiceMain(Jake); question line.
- // type 0: answerRect{x1,y1,x2,y2}; answer line (stored UPPERCASE).
- // type 1: choiceCount; rects{x1,y1,x2,y2}* â correct = rect 0.
Common::File f;
const Common::String fname = Common::String::format("P%u.BIN", puzzleId);
if (!f.open(Common::Path(fname))) {
@@ -200,10 +184,6 @@ bool EEMEngine::doPuzzle(uint puzzleId) {
const uint16 type = f.readUint16LE();
- // The caller (displayClue) restored the clean site background, so the
- // current screen has no clue bubbles. Keep that as `cleanBg` and build the
- // puzzle on a copy; `cleanBg` is restored after the answer (DOS `_DoPuzzle`
- // _Repaint) and after the wrong-answer hint (`_KDHelp` _Repaint) so neither
// the puzzle pics nor any bubble is left on the background.
Graphics::ManagedSurface cleanBg(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
@@ -219,7 +199,6 @@ bool EEMEngine::doPuzzle(uint puzzleId) {
Graphics::PixelFormat::createFormatCLUT8());
scratch.simpleBlitFrom(cleanBg);
- // Main picture, then `extraCount` more â each {id, x, y}, masked blit.
for (int phase = 0; phase < 2; phase++) {
uint16 n = 1;
if (phase == 1)
@@ -249,7 +228,6 @@ bool EEMEngine::doPuzzle(uint puzzleId) {
0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
- // Partner reads the question (`_SpoolSound(id - 1)`, gated on audio).
if (_audio && _voiceOn) {
const uint16 v = (_partner == kPartnerJake) ? voiceMain : voiceAlt;
if (v != 0 && v != 0xFFFF)
@@ -260,13 +238,12 @@ bool EEMEngine::doPuzzle(uint puzzleId) {
CursorMan.showMouse(true);
if (type == 0) {
- // Typed answer (`_CheckTypedAnswer` â `_GetNameString`, toupper+strcmp).
const int16 ax1 = (int16)f.readUint16LE();
const int16 ay1 = (int16)f.readUint16LE();
const int16 ax2 = (int16)f.readUint16LE();
const int16 ay2 = (int16)f.readUint16LE();
const Common::Rect rect(ax1, ay1, ax2, ay2);
- Common::String answer = readPuzzleLine(f); // stored uppercase
+ Common::String answer = readPuzzleLine(f);
const uint maxLen = answer.size() + 2;
Common::String input;
@@ -302,7 +279,7 @@ bool EEMEngine::doPuzzle(uint puzzleId) {
if (!input.empty())
done = true;
} else if (k == Common::KEYCODE_ESCAPE) {
- input.clear(); // cancel â empty â fails the compare
+ input.clear();
done = true;
} else if (k == Common::KEYCODE_BACKSPACE) {
if (!input.empty())
@@ -333,19 +310,6 @@ bool EEMEngine::doPuzzle(uint puzzleId) {
const int16 cy2 = (int16)f.readUint16LE();
rects.push_back(Common::Rect(cx1, cy1, cx2, cy2));
}
- // `_GetPuzzleChoice @ 2542:11a9`: the choice rects are the only
- // clickable areas (no done/exit hotspot) and ESC = wrong; the Tab/arrow
- // keys hit a global handler, not choice selection. Highlight the
- // options two ways, both reusing the existing engine mechanisms:
- // * Boxes: outline each option in the marching-ants ramp â palette
- // 0xF9..0xFE, the SAME original yellow as the site hotspots, not a
- // puzzle-specific colour. Gated like the DOS `_DrawRect`
- // (`DAT_3036_4c4a` â port `hide_highlight_boxes`).
- // * Cursor: recolor the default arrow over an option
- // (`setInteractiveMouseCursor`, the EEM1 red-pixel cursor for
- // "otherwise invisible" hotspots). These options carry no per-hotspot
- // cursor shape, so the default cursor is what's shown â exactly the
- // case that recolor is meant for.
applyHotspotGlowPalette();
const bool showBoxes = !ConfMan.getBool("hide_highlight_boxes");
int picked = -1;
@@ -368,8 +332,6 @@ bool EEMEngine::doPuzzle(uint puzzleId) {
kScreenWidth, kScreenHeight);
}
}
- // Red default-cursor while hovering a clickable option; plain arrow
- // off all of them (mirrors `updateHotspotCursor`).
const Common::Point mp = g_system->getEventManager()->getMousePos();
bool nowOver = false;
for (uint i = 0; i < rects.size(); i++) {
@@ -409,15 +371,11 @@ bool EEMEngine::doPuzzle(uint puzzleId) {
}
f.close();
- // _Repaint after the answer: drop the puzzle pics back to the clean site.
g_system->copyRectToScreen(cleanBg.getPixels(), cleanBg.pitch, 0, 0,
kScreenWidth, kScreenHeight);
g_system->updateScreen();
if (!correct && !shouldQuit()) {
- // Wrong: partner scolds via the KD hint (`_MIDIPlay(0x28)` +
- // `_KDHelp(TextBlock + KDTextIndex[+0xc], voice 6)`), drawn on the
- // clean site (the puzzle has already been _Repaint-ed away).
const byte *kd = _mystery.kdTextIndex();
const uint16 hintOff = kd ? READ_LE_UINT16(kd + 0x0c) : 0xFFFF;
if (hintOff != 0xFFFF) {
@@ -454,7 +412,6 @@ bool EEMEngine::doPuzzle(uint puzzleId) {
_audio->sayKDDigital(kd, 6, _partner);
waitForInput(60000);
- // `_KDHelp` ends with _Repaint: clear the hint balloon too.
g_system->copyRectToScreen(cleanBg.getPixels(), cleanBg.pitch,
0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
@@ -465,14 +422,6 @@ bool EEMEngine::doPuzzle(uint puzzleId) {
}
void EEMEngine::doHelp() {
- // Floppy per-mystery H<n>.BIN hint files. Loader FUN_1503_0001 @ 1503:0001
- // (format string "h%d.bin" @ 2608:0154), consumer FUN_1503_01a5 @ 1503:01a5.
- // Format:
- // byte numChainHints; numChainHints à { byte siteIdx; byte hotspotIdx; }
- // byte numExtraHints; numExtraHints à { byte siteIdx; byte hotspotIdx; }
- // asciiz str1, str2, str3 (post-solve, score >= 100)
- // Selection: any chain hotspot unsearched -> str1; else any extra
- // unsearched -> str2; else selectedPoints() >= 100 -> str3.
if (isFloppy() && _mystery.isLoaded()) {
const Common::String filename = Common::String::format("H%u.BIN",
_mystery.number());
@@ -545,9 +494,6 @@ void EEMEngine::doHelp() {
if (!chosen || *chosen == 0)
return;
- // _GetKDTextBalloon @ 1df2:0105 (floppy FUN_1d40_009f) indexes the
- // per-character table at 2608:0c14 by the literal byte. Bytes at
- // 2608:0c44 (= 0xc14 + '0') give the '0'..'9' -> balloon-id mapping:
static const uint8 kFloppyDigitToBalloon[10] = {
0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1c, 0x1d, 0x1e, 0x0a
};
@@ -612,27 +558,7 @@ void EEMEngine::doHelp() {
return;
}
- // Mirrors _KDHelp @ 1560:010a. Walks the first two _AChain entries
- // (the puzzle's required-clue chain â the "spine" of evidence the
- // player must collect):
- //
- // for (i = 0; i < 2; i++) {
- // if (_AChain[i] != -1 && _HintBlock[i] != -1 &&
- // _CluesFound[_AChain[i]] == 0) {
- // _DisplayHint(TextBlock + _HintBlock[i], i + 10);
- // shown++; break;
- // }
- // if (_HintBlock[i] != -1) defined++;
- // }
- // if (!shown) {
- // // Generic KD hint: KDTextIndex[+0xe] (first time) /
- // // KDTextIndex[+0x10] (second time, toggled by _SawHelpHint).
- // // If no chain hint was ever defined, render the "no hints"
- // // sentinel instead.
- // _DisplayHint(...);
- // }
- //
- // SMART per-puzzle hint: partner points at whichever chain clue the
+ // per-puzzle hint: partner points at whichever chain clue the
// player hasn't found yet, only falling back to the generic line once
// every chain hint has been triggered.
if (!_mystery.isLoaded() || !_font.isLoaded())
@@ -644,16 +570,10 @@ void EEMEngine::doHelp() {
return;
uint16 chosenText = 0xFFFF;
- int soundNum = 0; // _SayKDDigital line: EEM1 chain 10/11; fallback 7/8.
- int hintVoiceSlot = -1; // London chain hint â _SayKDHintDigital(slot).
+ int soundNum = 0;
+ int hintVoiceSlot = -1;
bool anyHintDefined = false;
- // Required-clue chain walk. EEM1 `_KDHelp @ 1560:010a` checks the first two
- // entries of chain A. EEM2 `_DoKDHelp @ 15c1:020b` walks all THREE chains
- // (A,B,C @ header words 16-20/21-25/26-30) Ã 5 slots, indexes the 15-entry
- // hint-text table `hintBlock()[chain*5 + slot]`, and voices a chain hint
- // with `_SayKDHintDigital(slot)` (table kdTextIndex()+0x3a) instead of
- // EEM1's `_SayKDDigital(slot + 10)`. Gate is the same: clue not yet found.
const uint kChains = isLondon() ? 3u : 1u;
const uint kSlots = isLondon() ? Mystery::kChainLen : 2u;
if (hb) {
@@ -681,7 +601,6 @@ void EEMEngine::doHelp() {
}
if (chosenText == 0xFFFF) {
- // Second arm of _KDHelp (1560:0152-019b): generic KD hint fallback.
if (anyHintDefined) {
const uint16 hintFirst = READ_LE_UINT16(kd + 0x0e);
const uint16 hintSecond = READ_LE_UINT16(kd + 0x10);
@@ -704,20 +623,6 @@ void EEMEngine::doHelp() {
const Common::String raw = _mystery.textAt(chosenText);
Common::String text = parseString(raw, _playerName, _partner);
-
- // Render as a speech-balloon overlay, mirroring _DisplayHint @ 1560:0009:
- //
- // _GetKDTextBalloon(text, &bub); // first-char dispatch
- // _GetBalloon(bub); // load balloon pic
- // y = (h < 0x4e) ? (0x50 - h) >> 1 : 1; // vertical centre
- // _AddPicBackground(balloon, 0x21, y); // overlay on screen
- // _WordWrap(0x21+tbl[bub].x, y+tbl[bub].y, // text inside balloon
- // tbl[bub].w, text, -1, color=0);
- // _SayKDDigital(snd); // partner voice
- // _Wait();
- //
- // BG is the caller's CURRENT screen (site / PDA / gallery), not a cleared
- // scratch.
Graphics::ManagedSurface ms(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
ms.clear();
@@ -729,13 +634,6 @@ void EEMEngine::doHelp() {
}
}
- // Balloon shape dispatch via _GetKDTextBalloon @ 1df2:0105 â based on
- // the first char of the parsed text. Digits select a specific balloon
- // variant; non-digit defaults to 0x17. The digit, when present, is
- // THEN consumed from the displayed text â mirrors _DisplayAlibi
- // @ 1df2:0145's `str = pbVar7 + 1` advance after reading `*str` for
- // bindx. _GetKDTextBalloon itself doesn't strip it (1df2:0105 just
- // reads `*str`), so the caller has to.
const byte firstChar =
text.empty() ? (byte)0 : (byte)text[0];
uint16 bubNum = getKDTextBalloon(firstChar);
@@ -768,10 +666,6 @@ void EEMEngine::doHelp() {
0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
- // _DisplayHint @ 1560:0009 plays _SayKDDigital(soundnum) â a
- // partner-specific voice line keyed to which hint type fired:
- // 10 = first chain hint, 11 = second chain hint,
- // 7 = generic KD (first), 8 = generic KD (second).
if (_audio && _mystery.kdTextIndex()) {
if (hintVoiceSlot >= 0)
_audio->sayKDHintDigital(_mystery.kdTextIndex(),
@@ -783,17 +677,8 @@ void EEMEngine::doHelp() {
waitForInput(60000);
}
-
+// _InterfaceHelp(num) @ 1560:0205
void EEMEngine::doInterfaceHelp(uint num) {
- // Mirrors _InterfaceHelp(num) @ 1560:0205. The original walks
- // HelpData @ 29be:00c8 (5-byte entries: u8 count, then up to 2 u16
- // picIds), _GetPictures each one, blits via _Rect_Move_Mask(0, 0, ...)
- // (a MASKED blit on top of the existing screen â transparent pixels
- // show the caller's BG), and waits for click / key. ESC at 1560:02b3
- // skips to the end. The function hides the cursor at the top
- // (MOV [0x3a00], 0 @ 1560:0216 + _RemoveMouse @ 1000:542f at
- // 1560:021c) and restores it at the tail (_DrawMouse @ 1000:5429
- // at 1560:02e8). See kHelpPics comment for HelpData decoding.
if (num >= ARRAYSIZE(kHelpPics))
return;
@@ -824,8 +709,6 @@ void EEMEngine::doInterfaceHelp(uint num) {
debugC(1, kDebugScript, "doInterfaceHelp: pic 0x%x = %dx%d flags=0x%x",
picId, pic.surface.w, pic.surface.h, pic.flags);
- // transBlitFrom transp = pic.flags >> 8 matches _Rect_Move_Mask param_10
- // @ 1000:03fc. Explicit (0,0) destPos: no-arg overload stretches to fill.
Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.simpleBlitFrom(bg);
@@ -884,7 +767,7 @@ void EEMEngine::setPartnerEraseBg(const Graphics::ManagedSurface *bg) {
uint16 EEMEngine::fitBalloonToText(uint16 bubNum,
const Common::String &text) {
- // Opt-in via "fit_dialog_balloons", CD only (floppy table unvalidated).
+ // Opt-in via "fit_dialog_balloons", CD only
if (isFloppy() || !ConfMan.getBool("fit_dialog_balloons"))
return bubNum;
@@ -969,12 +852,6 @@ bool EEMEngine::getBalloonIndicatorPos(uint16 bubNum, uint16 &dx,
void EEMEngine::drawFloppyBubbleIndicator(Graphics::ManagedSurface &dst,
uint16 bubNum, int ballX, int ballY,
bool endIndicator) {
- // Mirrors _DisplayHotspotClue_Floppy @ 22dc:08c0 (end-of-record) and
- // @ 22dc:08aa (mid-pagination). Both grab a pre-loaded PIC:
- // DAT_28da_3034 = PIC 0xa0 "more pages" indicator
- // DAT_28da_3030 = PIC 0xa1 "end" indicator
- // and stamp it at (ballX + insetTable[bubNum].indDX,
- // ballY + insetTable[bubNum].indDY) via _AddPicBackground.
uint16 dx = 0;
uint16 dy = 0;
if (!getBalloonIndicatorPos(bubNum, dx, dy))
diff --git a/engines/eem/music.cpp b/engines/eem/music.cpp
index 31c51f225c5..80f20aa9bda 100644
--- a/engines/eem/music.cpp
+++ b/engines/eem/music.cpp
@@ -48,12 +48,7 @@ MusicPlayer::MusicPlayer(bool isFloppy) : _isFloppy(isFloppy) {
case MT_ADLIB:
// _MIDIPlayFile @ 20a2:024c opens SAMPLE.AD (string at 29be:14d6)
// and installs every patch the sequence requests via
- // `_AIL_install_timbre`. ScummVM's Miles AdLib driver does the
- // same on-demand install from SAMPLE.AD, which is what makes
- // the notes match the 1993 release; the generic AdLib fallback
- // would use ScummVM's built-in timbres instead. SAMPLE.OPL would
- // be the OPL3 variant â the game only ships SAMPLE.AD, so the
- // empty second path falls back to OPL2.
+ // `_AIL_install_timbre`.
_milesAudioMode = true;
_driver = Audio::MidiDriver_Miles_AdLib_create(
Common::Path("SAMPLE.AD"), Common::Path());
@@ -94,11 +89,7 @@ MusicPlayer::MusicPlayer(bool isFloppy) : _isFloppy(isFloppy) {
void MusicPlayer::send(uint32 b) {
// Miles drivers (both AdLib and MT-32) implement their own per-
// source-channel mixing and timbre installation, so forward the raw
- // event. Going through `MidiPlayer::send` would re-wrap CC 7 against
- // `_masterVolume` AND remap the source channel via
- // `sendToChannel` / `allocateChannel`, both of which the Miles
- // driver already handles internally (double-applying breaks the
- // timbre selection).
+ // event.
if (_milesAudioMode) {
_driver->send(b);
return;
@@ -113,7 +104,7 @@ void MusicPlayer::playFile(const Common::Path &xmiPath, bool loop) {
Common::StackLock lock(_mutex);
stop();
- // _MIDIPlayFile @ 20a2:024c-029e (_fopen + _fread).
+
Common::File f;
if (!f.open(xmiPath)) {
warning("MusicPlayer: %s missing", xmiPath.toString().c_str());
@@ -147,15 +138,10 @@ void MusicPlayer::playFile(const Common::Path &xmiPath, bool loop) {
return;
}
- // _LoopMIDI = 0xFFFF in _DoOpeningAnims.
_isLooping = loop;
_parser->property(MidiParser::mpAutoLoop, loop ? 1 : 0);
_parser->setTrack(0);
- // Pull the launcher's music_volume slider into `_masterVolume` so
- // the non-Miles `Audio::MidiPlayer::send` path scales correctly.
- // (Miles drivers handle volume themselves but also honour
- // `MidiDriver::syncSoundSettings` via `Engine::syncSoundSettings`.)
syncVolume();
_isPlaying = true;
debugC(1, kDebugSound, "MusicPlayer: playing %s (%u bytes, loop=%d, miles=%d)",
@@ -168,7 +154,7 @@ void MusicPlayer::playMus(uint num, bool loop) {
// 0..4 â travel music. Table at 2608:1399-13cd holds 5 entries
// (Travel-6, Travel-4, Travel-7, Travel-1, Travel-8) used by
// `_StartTravelMusic` via `siteNumber % 5`.
- // 5 â FANFARE2.XMI (winner). String at 2608:0c64.
+ // 5 â FANFARE2.XMI (winner).
// 6 â no equivalent on floppy (loser sting in `_DisplayAlibi`
// is CD-only); skip.
if (_isFloppy) {
diff --git a/engines/eem/music.h b/engines/eem/music.h
index 11001a4280a..8209df47d89 100644
--- a/engines/eem/music.h
+++ b/engines/eem/music.h
@@ -53,7 +53,6 @@ public:
explicit MusicPlayer(bool isFloppy = false);
/// _MIDIPlayFile @ 20a2:024c. loop=true mirrors
- /// _LoopMIDI = 0xFFFF in _DoOpeningAnims.
void playFile(const Common::Path &xmiPath, bool loop = false);
/// _MIDIPlay(num) @ 20a2:047d. CD: "MUS%05u.XMI";
diff --git a/engines/eem/mystery.cpp b/engines/eem/mystery.cpp
index 715971eb77c..1734c804175 100644
--- a/engines/eem/mystery.cpp
+++ b/engines/eem/mystery.cpp
@@ -175,7 +175,6 @@ bool Mystery::load(uint num, Common::RandomSource *rng) {
return true;
}
- // Header is 16-bit-word indexed (matches `int *piVar1 = __Mystery; piVar1[N]`).
_initOffset = readU16(0 * 2);
_mapOffset = readU16(2 * 2);
_siteIndexOffset = readU16(3 * 2);
@@ -191,10 +190,6 @@ bool Mystery::load(uint num, Common::RandomSource *rng) {
_numCONSITEs = (uint8)readU16(14 * 2);
_numCOFFSITEs = (uint8)readU16(15 * 2);
- // Defensive clamp: the floppy header layout differs (M0.BIN CD has
- // numSites=readU16(0x14)=3; floppy has readU16(0x14)=0x1925, clearly
- // not a count). Cap to `_onSites` / `_visitedSite` array capacity so
- // downstream loops don't walk off the end on malformed/floppy files.
if (_numSites > kVisitedSiteCap)
_numSites = kVisitedSiteCap;
@@ -204,10 +199,6 @@ bool Mystery::load(uint num, Common::RandomSource *rng) {
_cChain[i] = readU16((26 + i) * 2);
}
- // Per-mystery runtime state â _ReadMystery zeroes these at load.
- // _newOrder uses identity mapping; original randomly cycles gallery
- // positions but requires matching changes in both clue side-effect
- // and rendering paths.
memset(_cluesFound, 0, sizeof(_cluesFound));
memset(_noteSelected, 0, sizeof(_noteSelected));
memset(_hotSpotsSeen, 0, sizeof(_hotSpotsSeen));
@@ -326,9 +317,6 @@ void Mystery::loadFloppySiteAnimData() {
const byte *Mystery::hotspots(uint siteNum) const {
if (_isFloppy) {
- // Floppy hotspot table inside per-site sub-blob (FUN_22dc_0b80 @ 22dc:0b80).
- // site_data[+4..5] = u16 file offset to count byte + N x 8-byte rects
- // (x1, y1, x2, y2 as u16s).
const byte *site = siteData(siteNum);
if (!site || (size_t)(site - _data.data()) + 6 > _data.size())
return nullptr;
@@ -395,7 +383,6 @@ uint16 Mystery::noteIndexCount() const {
// NoteIndex runs from _noteOffset to start of GalleryData.
// CD entries: 4 bytes (u16 textOff; u16 points).
// Floppy entries: 7 bytes (u16 ?; u16 jakeOff; u16 jennyOff; u8 score)
- // per FUN_22dc_05c8 @ 22dc:0843.
if (_galleryOffset <= _noteOffset)
return 0;
const uint stride = _isFloppy ? 7 : 4;
@@ -474,9 +461,6 @@ const byte *Mystery::floppySuspectEntry(uint suspectIdx) const {
}
bool Mystery::isGuilty(uint suspectIdx) const {
- // _WITCH @ 1df2:089f (CD): GalleryData[i*0x46 + 0x02] == 0xFFFF marks
- // guilty; innocent suspects store their alibi TextBlock offset there.
- // Floppy uses same convention at suspect entry +2..3 (variable stride).
if (_isFloppy) {
const byte *e = floppySuspectEntry(suspectIdx);
return e && READ_LE_UINT16(e + 2) == 0xFFFF;
@@ -490,9 +474,6 @@ bool Mystery::isGuilty(uint suspectIdx) const {
uint16 Mystery::alibiTextOffset(uint suspectIdx) const {
if (_isFloppy) {
- // Floppy alibi (_DisplayAlibi_Floppy @ 1d40:0145): u16 at suspect +2..3.
- // 0xFFFF = guilty; else high byte indexes the TEXT_BLOCK table at
- // header[+0xc], each entry u16 = absolute alibi-text offset.
const byte *e = floppySuspectEntry(suspectIdx);
if (!e)
return 0xFFFF;
@@ -570,12 +551,11 @@ int Mystery::selectedPoints() const {
}
return total;
}
-
+// `_SolvedCheck @ 1ea1:0b1a`. Reuses the already-loaded hint chains
+// (_aChain/_bChain/_cChain) as the three answer sets and the shared
+// _noteSelected accuse-selection array â see londonSolved() doc in the
+// header. EEM1/floppy keep the points model via solvedCheck().
bool Mystery::londonSolved() const {
- // `_SolvedCheck @ 1ea1:0b1a`. Reuses the already-loaded hint chains
- // (_aChain/_bChain/_cChain) as the three answer sets and the shared
- // _noteSelected accuse-selection array â see londonSolved() doc in the
- // header. EEM1/floppy keep the points model via solvedCheck().
for (uint chain = 0; chain < 3; chain++) {
int remaining = (int)kChainLen;
int wild = 0;
@@ -595,12 +575,11 @@ bool Mystery::londonSolved() const {
}
return false;
}
-
+// `_GetMinRemaining @ 1ea1:1056`. Parallel to londonSolved() but tests the
+// three answer sets against _cluesFound (discovered in the world). Tracks
+// whether any non-wildcard answer clue has been found at all; if none, the
+// player has nothing relevant yet (returns kChainLen).
int Mystery::minCluesRemaining() const {
- // `_GetMinRemaining @ 1ea1:1056`. Parallel to londonSolved() but tests the
- // three answer sets against _cluesFound (discovered in the world). Tracks
- // whether any non-wildcard answer clue has been found at all; if none, the
- // player has nothing relevant yet (returns kChainLen).
int best = (int)kChainLen;
int foundReal = 0;
for (uint chain = 0; chain < 3; chain++) {
diff --git a/engines/eem/mystery.h b/engines/eem/mystery.h
index 3793debedec..86f13dc7aeb 100644
--- a/engines/eem/mystery.h
+++ b/engines/eem/mystery.h
@@ -84,9 +84,7 @@ public:
const byte *floppySuspectEntry(uint suspectIdx) const;
/// NoteIndex array. EEM1 CD: 4 bytes/entry (u16 textOff + u16 pts).
- /// EEM2/London CD: 2 bytes/entry (u16 textOff only â no points field;
- /// `_DrawNotes @ 16a0:01de` reads `noteIndex[clueId*2]`). Floppy: 7
- /// bytes/entry.
+ /// Floppy: 7 bytes/entry. EEM2/London CD: 2 bytes/entry.
const byte *noteIndex() const;
uint16 noteIndexCount() const;
@@ -181,7 +179,6 @@ public:
/// world, = _CluesFound): returns the fewest clues still to find across the
/// usable sets â 0 means a full answer set has been discovered and the
/// accusation can begin. Returns 5 when no relevant clue has been found yet.
- /// Drives the London accuse-readiness gate (`_AccuseEntry @ 1ea1:115c`).
int minCluesRemaining() const;
/// _WITCH @ 1df2:089f. GalleryData[i*0x46 + 0x02] == 0xFFFF marks the
@@ -197,7 +194,7 @@ public:
/// Per-mystery runtime state, zeroed at load time.
uint8 _cluesFound[kCluesFoundCap] = {};
- uint8 _noteSelected[kCluesFoundCap] = {}; ///< _NoteSelected
+ uint8 _noteSelected[kCluesFoundCap] = {};
uint16 _hotSpotsSeen[kHotSpotsCap] = {};
uint16 _inGallery[kGalleryCap] = {};
uint8 _newOrder[kGalleryCap] = {};
@@ -213,8 +210,8 @@ public:
uint16 _searchLocationNumber = 0xFFFF;
uint16 _siteNumber = 0xFFFF;
uint16 _lastSite = 0xFFFF;
- uint16 _pendingSiteJump = 0; ///< EEM2 _DisplayClue destination site (DAT_2bca_0282).
- uint16 _siteReturnDepth = 0; ///< EEM2 nested site return stack depth (DAT_2bca_0280).
+ uint16 _pendingSiteJump = 0;
+ uint16 _siteReturnDepth = 0;
uint16 _siteReturnStack[kVisitedSiteCap] = {};
private:
@@ -240,7 +237,6 @@ private:
uint16 _bChain[kChainLen] = {};
uint16 _cChain[kChainLen] = {};
- // Floppy variant â see Mystery::load. _ReadMystery_Floppy @ 22dc:0178.
bool _isFloppy = false;
uint16 _floppySuspectsOff = 0; ///< header[+4] suspects
uint16 _floppyHintBlockOff = 0; ///< header[+6] hint -> clue table
diff --git a/engines/eem/resource.cpp b/engines/eem/resource.cpp
index d8722ceb598..1371cd177e7 100644
--- a/engines/eem/resource.cpp
+++ b/engines/eem/resource.cpp
@@ -53,7 +53,6 @@ bool DBDArchive::open(const Common::Path &dbdName, const Common::Path &dbxName)
return false;
}
- // _InitGraphicsSystem @ 172b:0145: 10-byte entries until EOF.
const int32 dbxSize = dbx.size();
_index.reserve(dbxSize / 10);
while (dbx.pos() + 10 <= dbxSize) {
@@ -113,11 +112,7 @@ bool readFrame(Common::SeekableReadStream &stream, bool compressed, Picture &out
bool DBDArchive::loadEntry(uint num, Picture &out) {
if (num >= _index.size()) {
- // Out-of-range picture IDs are non-fatal â every caller already
- // checks the return value (e.g. `haveDone` / `haveCrime` in
- // `drawBigMapOverview`). Floppy PICS.DBD ships fewer entries than
- // CD (e.g. BigMap done-marker 0x20D is CD-only), so this fires
- // routinely on floppy and stays at debug level rather than warning.
+ // Out-of-range picture IDs are non-fatal
debugC(2, kDebugGfx,
"DBDArchive::loadEntry: %u out of range (max %u)",
num, (uint)_index.size());
@@ -130,7 +125,7 @@ bool DBDArchive::loadEntry(uint num, Picture &out) {
return false;
}
- // _GetFromDB @ 172b:105d. Leading u16 = frame count (always 1 for pictures).
+ // Leading u16 = frame count (always 1 for pictures).
_dbd.skip(2);
return readFrame(_dbd, entry.compressed != 0, out);
}
@@ -147,7 +142,6 @@ bool DBDArchive::loadAnimation(uint num, Animation &out) {
return false;
}
- // _GetAnimation @ 172b:163a: u16 frame count, then N frames.
const uint16 frameCount = _dbd.readUint16LE();
if (frameCount == 0 || frameCount > 256) {
warning("DBDArchive::loadAnimation: %u has implausible frame count %u",
diff --git a/engines/eem/resource.h b/engines/eem/resource.h
index a984f323d35..2e19340ff4a 100644
--- a/engines/eem/resource.h
+++ b/engines/eem/resource.h
@@ -31,7 +31,6 @@
namespace EEM {
-/// dbi struct from _InitGraphicsSystem @ 172b:0145 (10 bytes per entry).
struct DBEntry {
uint32 offset; ///< Byte offset in the .DBD file.
uint16 compressed; ///< Non-zero = PKWARE DCL ("Implode") packed payload.
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 7c4884ac7d6..ce2b9d89ab4 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -91,7 +91,6 @@ void blitFrame(Graphics::ManagedSurface &dst, const Picture &p,
// Masked top-left blit onto a locked screen surface. Clips both src and
// dst against the screen, then delegates to copyRectToSurfaceWithKey
-// (Graphics::Surface's transparent-key blit).
void keyBlitToScreen(Graphics::Surface *screen, const Picture &p,
int x, int y) {
if (!screen || p.surface.empty())
@@ -107,21 +106,12 @@ void keyBlitToScreen(Graphics::Surface *screen, const Picture &p,
src, (uint32)(byte)(p.flags >> 8));
}
-// Top-left masked blit. `_AddDrop @ 172b:1a77` calls
-// `_Rect_Move_Mask(..., x, y, ...)` with the raw (x, y) and IGNORES
-// per-frame anchor offsets â so this is the correct path for static
-// drops and any non-animated overlay. Animations must route through
-// `blitAnimFrameAnchored` instead so per-frame anchor offsets
-// (miscflags = X, rowoff = Y) apply correctly.
void blitMaskedSurface(Graphics::Surface *screen, const Picture &p,
int x, int y) {
keyBlitToScreen(screen, p, x, y);
}
-// `_UpdateAnimations @ 172b:09c1`: blit at
-// (anchor_x - puVar5[4], anchor_y - puVar5[3])
-// where puVar5[3]/[4] are per-frame rowoff/miscflags (signed int16) from
-// the 16-byte PicData header. Transparency = flags >> 8.
+// `_UpdateAnimations @ 172b:09c1`
void blitAnimFrameAnchored(Graphics::Surface *screen, const Picture &p,
int anchorX, int anchorY) {
keyBlitToScreen(screen, p,
@@ -131,9 +121,7 @@ void blitAnimFrameAnchored(Graphics::Surface *screen, const Picture &p,
// `_ColorCycle @ 172b:2015` â rotate `_fpal[start..end]` by one slot:
// save [start], shift [start..end-1] = [start+1..end], restore saved at
-// [end], then re-upload via `_Set_Palette`. We do the same against
-// ScummVM's palette manager. Used by per-site Loop-1 ColorCycle entries
-// and the always-on hotspot marching-ants range 0xF9..0xFE.
+// [end], then re-upload via `_Set_Palette`.
void cyclePaletteRange(uint8 start, uint8 end) {
if (end <= start)
return;
@@ -153,12 +141,8 @@ void cyclePaletteRange(uint8 start, uint8 end) {
buf[(count - 1) * 3 + 2] = savedB;
g_system->getPaletteManager()->setPalette(buf, start, count);
}
-
+// `_OpenColorCycle @ 2520:04f7`
void cyclePaletteRangeReverse(uint8 start, uint8 end) {
- // `_OpenColorCycle @ 2520:04f7`: save END, shift every entry up by
- // one (END-1 â END, ...), wrap saved END to START. Visually colors
- // march from start toward end â opposite direction from
- // `cyclePaletteRange`. Used by the EA Kids / HighScore logo cycles.
if (end <= start)
return;
const uint count = (uint)end - (uint)start + 1;
@@ -180,11 +164,6 @@ void cyclePaletteRangeReverse(uint8 start, uint8 end) {
}
void applyHotspotGlowPalette() {
- // SITEPALS ships palette 0xF9..0xFE as uniform yellow (3F 3E 00), so the
- // original marching-ants was a placeholder. Override with a 6-step yellow
- // ramp so `cyclePaletteRange(0xF9, 0xFE)` (and the per-index draw colour)
- // produce a visible pulse. Shared by site hotspots and the clue puzzle so
- // both highlight clickable areas with the same (original) colours.
static const byte kAntsGlow[6 * 3] = {
0x40, 0x40, 0x00, // F9 â dim
0x80, 0x80, 0x00, // FA
@@ -211,8 +190,6 @@ const uint16 kWaitAnims[7][6] = {
{ 0x06, 0x06, 0x06, 0x06, 0x50, 0x50 }, // 6
};
-// `_DoKDAnim` table @ 29be:0228. 6 entries (kdAnimNum 0..5).
-// Layout: { animJake, animJenny, xJake, xJenny, yJake, yJenny }.
const uint16 kKdAnimTable[6][6] = {
{ 0x03, 0x0c, 6, 6, 80, 80 }, // 0 â speaker idx 1 wait anim
{ 0x01, 0x0b, 6, 6, 80, 80 }, // 1 â same as PDA idle
@@ -222,11 +199,7 @@ const uint16 kKdAnimTable[6][6] = {
{ 0x06, 0x06, 6, 6, 80, 80 }, // 5 â same anim both partners
};
-// EEM2 `_DoKDAnim @ 1717:05bf` table @ 2bca:0238 (read from EEM2CD.EXE).
-// Same { animJake, animJenny, xJake, xJenny, yJake, yJenny } layout. Positions
-// differ (xâ2, yâ78 vs EEM1's 6/80) and Jenny's reactions 4/5 use distinct
-// anims 0x55/0x2d (EEM1 reused 0x05/0x06 for both partners). Selected by
-// `playKdAnim` when the London variant is active.
+// EEM2 `_DoKDAnim @ 1717:05bf` table @ 2bca:0238
const uint16 kKdAnimTableLondon[6][6] = {
{ 0x03, 0x0c, 3, 2, 66, 65 }, // 0
{ 0x01, 0x0b, 2, 2, 78, 78 }, // 1
@@ -236,8 +209,7 @@ const uint16 kKdAnimTableLondon[6][6] = {
{ 0x06, 0x2d, 2, 2, 78, 78 }, // 5 â Jenny uses 0x2d
};
-// EEM2 `_WaitAnims @ 2bca:022c`. Entry 0 precedes `_DoKDAnim`'s
-// table; entries 1..6 overlap `kKdAnimTableLondon`, same as EEM1.
+// EEM2 `_WaitAnims @ 2bca:022c`.
const uint16 kWaitAnimsLondon[7][6] = {
{ 0x00, 0x0a, 2, 2, 78, 78 }, // 0
{ 0x03, 0x0c, 3, 2, 66, 65 }, // 1
@@ -248,13 +220,7 @@ const uint16 kWaitAnimsLondon[7][6] = {
{ 0x06, 0x2d, 2, 2, 78, 78 }, // 6
};
-// Animation script table â mirrors `_AnimationSequences @ 29be:22d4`
-// (55-entry table of far ptrs, each pointing to a u16-frame-index
-// stream). `_NewAnimation @ 172b:06e1` reads the script via
-// `_AnimationSequences[anim_id]` and stores the pointer in
-// `DAT_2d5d_3eaf[i*0xb]`; `_UpdateAnimations @ 172b:09c1` then walks it
-// one entry per `_CheckFrameRate` tick (~140 ms).
-//
+// Animation script table (`_AnimationSequences @ 29be:22d4`)
// Script byte format:
// 0x80 = restart (loop back to index 0; terminator for one-shots)
// 0x81 N = jump to byte N (not used in partner subset)
@@ -353,8 +319,6 @@ const AnimScript kAnimScripts[] = {
{ 0x35, 11, { 0,1,2,3,4,5,6,7,8,9,10 } },
};
-// Scripts longer than 28 frames (>fits-inline limit). `findAnimScript`
-// checks both this array and `kAnimScripts`. Longest is 0x22 (115 frames).
struct AnimScriptLong {
uint16 seqnum;
uint16 len;
@@ -362,9 +326,7 @@ struct AnimScriptLong {
};
// Briefing animations â `_DoInitClues @ 1a35:0411` always uses anim ID
-// 0x17 (game) / 0x18 (book) / 0x19 (nancy) regardless of partner; the
-// per-partner ANI.DBD cells come from a separate entry (e.g. 0x3b for
-// Jenny's briefing).
+// 0x17 (game) / 0x18 (book) / 0x19 (nancy) regardless of partner
const uint8 kScript17[] = {
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,
20,21,22,23,24,25,26,27,28,29
@@ -378,10 +340,7 @@ const uint8 kScript19[] = {
1,2,3,4,5,6,7,8,9,10,11,12
};
-// Site / NPC drop scripts (29be:22d4 entries 0x1a..0x36). Repeated
-// frames are the original's frame-hold mechanism (one entry per tick).
-
-// 0x1a (29be:19a4) â count-up 0..7, idle hold, repeat 1..7, idle, mirror 7..0, idle.
+// Site / NPC drop scripts
const uint8 kScript1a[] = {
0,1,2,3,4,5,6,7,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
@@ -390,7 +349,7 @@ const uint8 kScript1a[] = {
7,6,5,4,3,2,1,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
-// 0x1e (29be:1a40) â slow walk-stutter with idle tail (76 entries).
+
const uint8 kScript1e[] = {
0,1,2,3,3,3,3,4,4,3,4,4,4,4,4,3,
5,5,5,5,5,5,5,5,4,4,4,4,4,4,4,4,
@@ -398,8 +357,7 @@ const uint8 kScript1e[] = {
7,8,7,7,7,7,7,8,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
-// 0x1f (29be:1ada) â 0..5, idle, 0..5, idle, 6..8 alternation,
-// idle (50 entries).
+
const uint8 kScript1f[] = {
0,1,2,3,4,5,
0,0,0,0,
@@ -410,13 +368,12 @@ const uint8 kScript1f[] = {
6,
0,0,0,0,0
};
-// 0x20 (29be:1b40) â count-up 0..33 (34 frames).
+
const uint8 kScript20[] = {
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,
20,21,22,23,24,25,26,27,28,29,30,31,32,33
};
-// 0x22 (29be:1ba4) â long held-frame walker 0..22 with idle tail
-// (115 entries; most frames held 4-7 ticks each).
+
const uint8 kScript22[] = {
0,
1,1,1,1,1,
@@ -443,8 +400,7 @@ const uint8 kScript22[] = {
22,22,22,22,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
-// 0x23 (29be:1c8c) â 29 entries: 0, 6 holds of 1, count-up 2..4,
-// down-up gesture, 5 idle frames.
+
const uint8 kScript23[] = {
0,1,1,1,1,1,1,
2,3,4,3,2,
@@ -452,39 +408,34 @@ const uint8 kScript23[] = {
2,3,4,3,3,3,3,3,
0,0,0,0,0,0
};
-// 0x24 (29be:1cc8) â bell-curve hold (58 entries): 0,0, 1,1, 2,2,
-// 3 held for 26 ticks, mirror back, idle.
+
const uint8 kScript24[] = {
0,0,1,1,2,2,
3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,
2,2,1,1,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
-// 0x28 (29be:1d7e) â gentle hold 0..3 with long hold on 3, mirror
-// back, idle (45 entries).
+
const uint8 kScript28[] = {
0,1,1,2,2,
3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,
2,2,1,1,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
-// 0x29 (29be:1dda) â paired-step count-up 0..21 plus idle
-// (58 entries).
+
const uint8 kScript29[] = {
0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,
11,11,12,12,13,13,14,14,15,15,16,16,17,17,18,18,19,19,
20,20,21,21,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
-// 0x2b (29be:1e5c) â count-up 0..11 with each frame held 4 ticks
-// (48 entries).
+
const uint8 kScript2b[] = {
0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,
4,4,4,4,5,5,5,5,6,6,6,6,7,7,7,7,
8,8,8,8,9,9,9,9,10,10,10,10,11,11,11,11
};
-// 0x2c (29be:1ebe) â alternation walk 0..19 with idle tail
-// (54 entries).
+
const uint8 kScript2c[] = {
0,1,2,3,4,5,
0,
@@ -495,8 +446,7 @@ const uint8 kScript2c[] = {
16,17,18,19,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
-// 0x2d (29be:1f2c) â count-up 0..11 with each frame held 8 ticks
-// (96 entries).
+
const uint8 kScript2d[] = {
0,0,0,0,0,0,0,0,
1,1,1,1,1,1,1,1,
@@ -511,8 +461,7 @@ const uint8 kScript2d[] = {
10,10,10,10,10,10,10,10,
11,11,11,11,11,11,11,11
};
-// 0x30 (29be:1fee) â 0,0, count-up 1..19, idle, mirror down, extra
-// idle (86 entries).
+
const uint8 kScript30[] = {
0,0,
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,
@@ -521,7 +470,7 @@ const uint8 kScript30[] = {
5,4,4,3,3,2,2,1,1,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
-// 0x31 (29be:209c) â paired-step idle alternations (57 entries).
+
const uint8 kScript31[] = {
0,0,0,1,1,1,
0,0,0,1,1,1,
@@ -535,8 +484,7 @@ const uint8 kScript31[] = {
0,0,0,
1,1,1
};
-// 0x36 (29be:2110) â 0..8 forward, 1..8 forward, frame 1 held 20
-// ticks, 8..0 mirror, idle tail (60 entries).
+
const uint8 kScript36[] = {
0,1,2,3,4,5,6,7,8,
1,2,3,4,5,6,7,8,
@@ -567,10 +515,9 @@ const AnimScriptLong kAnimScriptsLong[] = {
};
// `_PatientSequence` and `_ImpatientSequence` are standalone script
-// pointers, not entries in `_AnimationSequences`. CD has the data but
-// never calls the switchers; floppy calls them from `_DoSiteLoop_Floppy`
-// (via `_Switch2Patient` / `_Switch2Impatient`). We intentionally enable
-// the same switch for both builds.
+// pointers. CD has the data but never calls the switchers; floppy calls
+// them from `_DoSiteLoop_Floppy` (via `_Switch2Patient` / `_Switch2Impatient`).
+// We intentionally enable the same switch for both builds.
const uint8 kPatientSequence[] = { 0,0,0,0,0,0,0,0,0,2 };
const uint8 kImpatientSequence[] = { 0,1,0,1,0,1,0,1,2,1 };
@@ -737,11 +684,6 @@ AnimScriptRef findAnimScript(uint16 seqnum) {
}
// Original frame period from `_InitFrameCounter @ 1a35:01ae`:
-// LastFrame = cs_within_hour + 0xe
-// cs_within_hour = ((ti_min * 60) + ti_sec) * 100 + ti_hund
-// (Borland C `struct time` memory order is min, hour, hund, sec.)
-// `+ 0xe` is 14 centiseconds â ~140 ms per frame, matching
-// `_CheckFrameRate @ 1a35:0204`.
const uint kFramePeriodMs = 140;
uint frameFromScriptAtTick(const uint8 *frames, uint len,
@@ -753,109 +695,15 @@ uint frameFromScriptAtTick(const uint8 *frames, uint len,
return (numFrames > 0) ? MIN<uint>(frame, numFrames - 1) : 0;
}
-void auditPartnerAnims(EEMEngine *vm) {
- // Cross-check every script against the ANI.DBD entry it references;
- // warn on out-of-range frame requests.
- if (!vm)
- return;
- DBDArchive &ani = vm->getAni();
-
- struct Walker {
- static void check(DBDArchive &ani, uint16 id, const uint8 *frames, uint8 len) {
- if (len == 0)
- return;
- Animation a;
- if (!ani.loadAnimation(id, a) || a.empty()) {
- debugC(1, kDebugSite,
- "auditPartnerAnims: anim 0x%02x failed to load", id);
- return;
- }
- uint maxRequested = 0;
- for (uint j = 0; j < len; j++)
- if (frames[j] > maxRequested)
- maxRequested = frames[j];
- if (maxRequested >= a.size()) {
- warning("anim 0x%02x: script wants frame %u but ANI.DBD has "
- "only %u â frames will be clamped",
- id, maxRequested, (uint)a.size());
- } else {
- debugC(2, kDebugSite,
- "anim 0x%02x: %u cells, script max=%u, len=%u",
- id, (uint)a.size(), maxRequested, len);
- }
- }
- };
-
- for (uint i = 0; i < ARRAYSIZE(kAnimScripts); i++)
- Walker::check(ani, kAnimScripts[i].seqnum,
- kAnimScripts[i].frames, kAnimScripts[i].len);
- for (uint i = 0; i < ARRAYSIZE(kAnimScriptsLong); i++)
- Walker::check(ani, kAnimScriptsLong[i].seqnum,
- kAnimScriptsLong[i].frames, kAnimScriptsLong[i].len);
-
- // Per-frame anchor-offset audit (`_UpdateAnimations @ 172b:09c1`
- // applies (anchor_x - miscflags, anchor_y - rowoff)). Log non-zero
- // anchors so we know which anims need `blitAnimFrameAnchored`.
- const uint16 partnerIds[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
- 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c,
- 0x0d, 0x0f, 0x10, 0x11, 0x12, 0x13,
- 0x14, 0x15, 0x16, 0x17, 0x18, 0x19 };
- for (uint i = 0; i < ARRAYSIZE(partnerIds); i++) {
- const uint16 id = partnerIds[i];
- Animation a;
- if (!ani.loadAnimation(id, a) || a.empty())
- continue;
- bool anyAnchor = false;
- int rowMin = 0, rowMax = 0, miscMin = 0, miscMax = 0;
- for (uint f = 0; f < a.size(); f++) {
- const Picture &fr = a[f];
- const int sRow = (int)(int16)fr.rowoff;
- const int sMisc = (int)(int16)fr.miscflags;
- if (sRow != 0 || sMisc != 0) {
- if (!anyAnchor) {
- rowMin = rowMax = sRow;
- miscMin = miscMax = sMisc;
- anyAnchor = true;
- } else {
- rowMin = MIN(rowMin, sRow);
- rowMax = MAX(rowMax, sRow);
- miscMin = MIN(miscMin, sMisc);
- miscMax = MAX(miscMax, sMisc);
- }
- }
- }
- if (anyAnchor) {
- // Signed int16 (puVar5[3]/[4] in `_UpdateAnimations`).
- debugC(1, kDebugSite,
- "anim 0x%02x: per-frame anchor (rowoff [%d..%d], "
- "miscflags [%d..%d]) â handled by "
- "`blitAnimFrameAnchored`",
- id, rowMin, rowMax, miscMin, miscMax);
- }
- }
-}
-
// Looping path of `_UpdateAnimations`: walk the script one entry per
// `_CheckFrameRate` tick (`kFramePeriodMs` ~= 140 ms), wrap on 0x80.
-// If `seqnum` has no registered script, falls back to flipbook
-// (`tick % numFrames`) so unknown anims still move. The script can in
-// theory request a frame past the asset's actual cell count (misencoded
-// script) â `frameFromScriptAtTick` clamps to `numFrames - 1` so the
-// caller doesn't read past `anim[]`. Exported (non-static) so BigMap,
-// CaseSelection greeter, Notebook, and Gallery render paths in
-// `ui.cpp` use the same cadence.
uint partnerFrameAtTick(uint16 seqnum, uint numFrames, uint32 tickMs) {
const AnimScriptRef s = findAnimScript(seqnum);
return frameFromScriptAtTick(s.frames, s.len, numFrames, tickMs);
}
// Play `unfold` once, then loop `waitSeq` forever. Mirrors the
-// original's slot-script-swap idiom: the entrance script runs to its
-// 0x80 terminator, then the slot's script pointer is rewritten to a
-// looping wait sequence (e.g. `_BigMapWaitSeq @ 29be:1574`,
-// `_SmallMapWaitSeq @ 29be:1548`). `partnerFrameAtTick` can't model
-// that swap on its own (it always wraps on the same script), hence
-// this helper.
+// original's slot-script-swap idiom
uint oneShotThenLoopFrameAtTick(const uint8 *unfold, uint unfoldLen,
const uint8 *waitSeq, uint waitSeqLen,
uint numFrames, uint32 elapsedMs) {
@@ -867,10 +715,6 @@ uint oneShotThenLoopFrameAtTick(const uint8 *unfold, uint unfoldLen,
}
uint bigMapPartnerFrameAtTick(uint numFrames, uint32 elapsedMs) {
- // Slot starts on script 0x14 (count-up 0..8 @ 29be:196a). On 0x80
- // terminator, `_DoBigMap` rewrites the slot's script pointer to
- // `_BigMapWaitSeq @ 29be:1574` = (9,9,9,9,10,9,9,9,9, 0x80) â the
- // open-map hold with a fidget.
static const uint8 kUnfold[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8 };
static const uint8 kWaitSeq[] = { 9, 9, 9, 9, 10, 9, 9, 9, 9 };
return oneShotThenLoopFrameAtTick(kUnfold, ARRAYSIZE(kUnfold),
@@ -879,10 +723,6 @@ uint bigMapPartnerFrameAtTick(uint numFrames, uint32 elapsedMs) {
}
uint bigMapDetailPartnerFrameAtTick(uint numFrames, uint32 elapsedMs) {
- // Slot starts on script 0x13 (count-up 0..7 @ 29be:1992). On 0x80
- // terminator, `_DoMapScreen @ 20fe:1390` rewrites the slot pointer
- // (`MOV [BX+0x789f],0x1548`) to `_SmallMapWaitSeq @ 29be:1548` =
- // 18 entries holding cell 7 with a single cell-10 fidget (~1.8 s).
static const uint8 kUnfold[] = { 0, 1, 2, 3, 4, 5, 6, 7 };
static const uint8 kWaitSeq[] = {
7, 7, 7, 10, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7
@@ -929,8 +769,6 @@ bool SiteScreen::playLondonTravelAnimation(uint fromSite, uint toSite) {
return false;
}
- // EEM2 `_DoTravel @ 1717:06ed`: kind 3 forces partner suffix 0,
- // so the shipped set is TRAVEL00/01/10/11/20.ANM.
const uint partnerSuffix = (travelKind == 3) ? 0 : _vm->getPartnerIndex();
const Common::String name = Common::String::format("TRAVEL%u%u.ANM",
(uint)travelKind - 1, partnerSuffix);
@@ -958,16 +796,12 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
return;
}
- // `_DoSiteLoop @ 168d:0436`: `_NewAnimation` sets the new slot's
- // frame index to 0xffff (-1) â starts at script[0].
_waitPhaseAnchor = g_system->getMillis();
if (resetPartnerMood) {
_partnerWaitMood = kPartnerWaitDefault;
initImpatienceCounter();
}
- // `_DoSiteLoop @ 168d:03f4`:
- // if (_VisitedSite[_SiteNumber] == 0) _DisplayClue(...);
const bool firstVisit = (siteNum < Mystery::kVisitedSiteCap)
&& (_mystery->_visitedSite[siteNum] == 0);
@@ -980,8 +814,6 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
if (playArrival) {
if (_vm->isLondon()) {
- // EEM2 `HandleLondonSiteLoop @ 1717:083a` calls `_DoTravel`
- // before `_BuildBackground` for the destination site.
bool showedApproach = false;
const uint16 approachId = sd ? READ_LE_UINT16(sd + 2) : 0xffff;
if (firstVisit && approachId != 0xffff)
@@ -989,15 +821,10 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
if (!showedApproach)
playLondonTravelAnimation(_mystery->_lastSite, siteNum);
} else {
- // `_DoTravel @ 168d:02da` calls `_StartTravelMusic`.
_vm->startTravelMusic();
}
}
- // `_BuildBackground` calls `GetPalette(sitenum + 1)` â sitenum is the
- // global SITES.DBD index (per-mystery `sitepic` field).
- // Floppy (`_DoSiteLoop_Floppy @ 1652:03f4`): first u16 of site_data is
- // an offset to a drops sub-struct whose byte 0 is the SITES.DBD picID.
uint16 sitepic = 0;
if (sd) {
if (_vm->isFloppy()) {
@@ -1011,17 +838,11 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
}
_vm->setSitePaletteForSite(sitepic);
- // Override SITEPALS' uniform-yellow 0xF9..0xFE with the marching-ants
- // glow ramp (shared with the clue puzzle).
applyHotspotGlowPalette();
renderBackground(siteNum);
- // `_DoSiteLoop @ 168d:03f4` plays `_EnterSiteAnim` when
- // `_LastSite != _SiteNumber`. Guard lives on the engine so PDA/gallery
- // re-entry doesn't replay arrival.
if (playArrival) {
- // `_EnterSiteAnim` snapshots the screen, so populate the BG first.
if (_vm->isFloppy())
renderFloppyDrops(siteNum);
else
@@ -1038,7 +859,6 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
renderBackground(siteNum);
}
- // Loop 2 from `_DoSiteLoop`: static drops (baked into snapshot).
if (_vm->isFloppy())
renderFloppyDrops(siteNum);
else
@@ -1049,7 +869,6 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
scanColorCycles(siteNum);
- // Loop 1 (animated NPCs) + partner on top of the snapshot.
const uint32 now = g_system->getMillis();
renderAnimatedDrops(siteNum, now);
renderPartner(siteNum, now);
@@ -1110,8 +929,6 @@ bool SiteScreen::checkImpatienceCounter() {
}
void SiteScreen::notePartnerActivity() {
- // `_Switch2Patient(WaitHandle)` + `_InitImpatientCounter()` from
- // the floppy site loop. Triggered on clicks/keys only (not mouse-move).
const bool wasImpatient = _partnerWaitMood == kPartnerWaitImpatient;
_partnerWaitMood = kPartnerWaitPatient;
initImpatienceCounter();
@@ -1135,7 +952,6 @@ void SiteScreen::run() {
if (!_mystery || !_mystery->isLoaded())
return;
- // Caller seeds `_siteNumber` (via map pick or save restore).
uint cur = _mystery->_siteNumber;
if (cur >= _mystery->numSites())
cur = 0;
@@ -1163,14 +979,6 @@ void SiteScreen::run() {
break;
case Common::EVENT_LBUTTONDOWN: {
- // `_DoSiteLoop @ 168d:03f4` calls
- // _FindButton(&SiteButtons, 2, MouseX, MouseY)
- // `SiteButtons` @ 29be:0274 â two 8-byte rects:
- // Button 0: (35,111)-(56,136) notebook (_NextScreen=4)
- // Button 1: (7,177)-(57,200) map (CD=1, floppy=2)
- // Partner-head click is port-only: `_KDHelp` shortcut
- // mirroring `_HandleNoteButton[3]` (0x0403) /
- // `_HandleGalleryButton[3]` (0x061e). Rect = (5,80,44,110).
if (kPdaSiteRect.contains(event.mouse.x, event.mouse.y)) {
notePartnerActivity();
_vm->setHotspotMouseCursor(false);
@@ -1259,8 +1067,6 @@ void SiteScreen::run() {
case Common::EVENT_KEYDOWN:
notePartnerActivity();
- // `_DoSiteLoop @ 168d:07e1` routes ESC via `_ESCHit` â
- // "Are you sure?" â MAP. Port reroutes to ScummVM menu.
if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
_vm->setHotspotMouseCursor(false);
_vm->openMainMenuDialog();
@@ -1275,15 +1081,11 @@ void SiteScreen::run() {
}
}
- // Hotspot side effects can invalidate the mystery; exit before
- // ticking another frame against stale snapshots.
if (!_mystery || !_mystery->isLoaded()) {
_vm->stopMusic();
return;
}
- // Per-tick frame pump: `_CheckFrameRate` + `_UpdateAnimations`
- // at the top of `_DoSiteLoop`'s main loop. 14 cs (~140 ms).
const uint32 now = g_system->getMillis();
if (_snapshotSite == (int)cur &&
now - _lastTickMs >= kFramePeriodMs) {
@@ -1295,7 +1097,6 @@ void SiteScreen::run() {
renderAnimatedDrops(cur, now);
renderPartner(cur, now);
renderHotspots(cur);
- // `_ColorCycle(start, end)` per tick (`_DoSiteLoop @ 168d:03f4`).
applyColorCycles();
_lastTickMs = now;
}
@@ -1303,14 +1104,12 @@ void SiteScreen::run() {
g_system->delayMillis(10);
}
}
-
+// `_EnterSiteAnim @ 1000:9b21`. Two phases (partner-dependent):
+// Phase 1 â skateboard scroll: anim 6 (Jake) / 0xe (Jenny).
+// Slides from (320-w, 199-h) leftward off-screen.
+// Phase 2 â KD slide-in: anim 7 (Jake) / 0xf (Jenny).
+// Slides from x=-w at y=0x8b/0x8e until x=0.
bool SiteScreen::enterSiteAnim() {
- // `_EnterSiteAnim @ 1000:9b21`. Two phases (partner-dependent):
- // Phase 1 â skateboard scroll: anim 6 (Jake) / 0xe (Jenny).
- // Slides from (320-w, 199-h) leftward off-screen.
- // Phase 2 â KD slide-in: anim 7 (Jake) / 0xf (Jenny).
- // Slides from x=-w at y=0x8b/0x8e until x=0.
- // Frame rate uses `_MoveSkateBoardPixels` (runtime); we use 4 px/tick.
if (!_vm || !_mystery)
return false;
const uint8 partner = _vm->getPartnerIndex();
@@ -1327,9 +1126,6 @@ bool SiteScreen::enterSiteAnim() {
g_system->unlockScreen();
if (_vm->isLondon()) {
- // EEM2 `_EnterSiteAnim @ 17ee:27a4`: no skateboard phase.
- // It plays the partner-specific slide-in animation at anchor
- // (0, 0x50/0x4e), using `_CheckFrameRate` between cells.
const uint animId = (partner == 0) ? 7 : 0xf;
const int anchorY = (partner == 0) ? 0x50 : 0x4e;
Animation anim;
@@ -1359,10 +1155,8 @@ bool SiteScreen::enterSiteAnim() {
return false;
}
- // Phase 1 â skateboard scroll.
Animation skate;
if (_vm->getAni().loadAnimation(kSkateAni, skate) && !skate.empty()) {
- // `iVar4 = 199 - sprite_h`, `uVar5 = kScreenWidth - sprite_w` (frame 0).
const int spriteH = skate[0].surface.h;
const int spriteW = skate[0].surface.w;
int x = (kScreenWidth - spriteW) & ~3; // 4-px aligned (mode-X)
@@ -1400,11 +1194,6 @@ bool SiteScreen::enterSiteAnim() {
}
}
- // Phase 2 â KD slide-in. `_EnterSiteAnim` per-frame:
- // destX = -frame.miscflags (signed int16; byte 8 = anchor X)
- // destY = kKDY - frame.rowoff (signed int16; byte 6 = anchor Y)
- // 16-bit negation: miscflags=-2 (0xFFFE) â destX=+2.
- // ~80 ms per frame (~12 FPS, one `_CheckFrameRate` tick).
Animation kd;
if (_vm->getAni().loadAnimation(kKDAni, kd) && !kd.empty()) {
for (uint frameIdx = 0;
@@ -1437,10 +1226,6 @@ bool SiteScreen::enterSiteAnim() {
}
void SiteScreen::renderStaticDrops(uint siteNum) {
- // Loop 2 from `_DoSiteLoop @ 168d:03f4`:
- // count @ siteData[+0x4], entries @ siteData[+0xc + i*6]: {picId, x, y}.
- // `_AddDrop @ 172b:1a77` loads PIC picId-1 from PICS.DBD,
- // blits with miscflags high-byte as transparency.
if (!_mystery)
return;
const byte *site = _mystery->siteData(siteNum);
@@ -1474,11 +1259,6 @@ void SiteScreen::renderStaticDrops(uint siteNum) {
}
void SiteScreen::renderFloppyDrops(uint siteNum) {
- // Floppy drops: drops sub-struct pointed to by `*site_data` u16 offset.
- // `_DoSiteLoop_Floppy @ 1652:0418` â `FUN_16e2_18eb @ 16e2:18eb`:
- // entry stride 5: u16 picID @ +0, u16 X @ +2, u8 Y @ +4.
- // picID-1 indexes PICS.DBX table @ 2608:4537.
- // drops_struct[0] = BG picID (separate), drops_struct[1] = drop count.
if (!_mystery)
return;
const byte *site = _mystery->siteData(siteNum);
@@ -1501,7 +1281,6 @@ void SiteScreen::renderFloppyDrops(uint siteNum) {
const int16 y = (int16)e[4];
if (picID == 0)
continue;
- // `getPicture(num)` does `loadEntry(num - 1)` (resource.h:100).
Picture pic;
if (!_vm->getPics().getPicture((uint)picID, pic))
continue;
@@ -1511,20 +1290,10 @@ void SiteScreen::renderFloppyDrops(uint siteNum) {
}
void SiteScreen::renderAnimatedDrops(uint siteNum, uint32 tickMs) {
- // Loop 1 from `_DoSiteLoop @ 168d:03f4`:
- // count @ siteData[+0xa], entries @ siteData[+0x48 + i*6]: {animId, x, y}.
- // animId == -1 â `_ColorCycle(x, y)` (palette rotation, separate path).
- // else â `_GetAnimation` + `_NewAnimation` + `_UpdateAnimations @ 172b:09c1`
- // walks a sequence script (0x80=loop, 0x81=jump).
if (!_mystery)
return;
if (_vm && _vm->isFloppy()) {
- // Floppy site anims live in ANI.BIN (per-case block from
- // `_GetSiteAnimData_Floppy`). Per-site:
- // u8 cycleCount, cycleCount à {u8 start, u8 end},
- // u8 animCount, animCount à {u8 animId, u16 x, u8 y}.
- // `_DoSiteLoop_Floppy` caps at 4 slots.
const byte *siteAnim = _mystery->floppySiteAnimData(siteNum);
if (!siteAnim)
return;
@@ -1588,8 +1357,6 @@ void SiteScreen::renderAnimatedDrops(uint siteNum, uint32 tickMs) {
}
void SiteScreen::scanColorCycles(uint siteNum) {
- // `_DoSiteLoop @ 168d:03f4`: Loop 1 entries with animId==-1 are
- // ColorCycle palette ranges (start @ +2, end @ +4). Max 5 slots.
_colorCycles.clear();
if (!_mystery)
return;
@@ -1628,8 +1395,6 @@ void SiteScreen::scanColorCycles(uint siteNum) {
}
void SiteScreen::applyColorCycles() {
- // `_ColorCycle @ 172b:2015` per range. Always rotate 0xF9..0xFE
- // (hotspot marching ants â `_ColorCycle(0xf9, 0xfe)` in `_DoSiteLoop`).
for (uint i = 0; i < _colorCycles.size(); i++) {
cyclePaletteRange(_colorCycles[i].start, _colorCycles[i].end);
}
@@ -1655,11 +1420,6 @@ void SiteScreen::restoreBgSnapshot() {
}
void SiteScreen::syncCompositedScreen() {
- // OpenGL screenshots read the current backbuffer. After a buffer swap,
- // that backbuffer can contain an older site frame unless the engine keeps
- // presenting the full composited screen, even while no animation tick
- // fired. Copying the current game surface through copyRectToScreen keeps
- // screenshots in sync without backend-specific changes.
Graphics::ManagedSurface snapshot(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
@@ -1674,14 +1434,6 @@ void SiteScreen::syncCompositedScreen() {
}
void SiteScreen::renderPartner(uint siteNum, uint32 tickMs) {
- // `_DoSiteLoop @ 168d:03f4` reads `siteData[+8]` as the speaker
- // table index, then for each (speaker x partner) loads:
- // anim = WaitAnims[speakerIdx].anim[partner]
- // x = WaitAnims[speakerIdx].x[partner]
- // y = WaitAnims[speakerIdx].y[partner]
- // from `_WaitAnims @ 29be:021c` (see `kWaitAnims` at file scope for
- // 12-byte / 6-u16 entry layout). Rendering caps at speaker < 7
- // (entries past 6 are `_SiteButtons` rect data in the binary).
const byte *site = _mystery->siteData(siteNum);
if (!site)
return;
@@ -1690,10 +1442,6 @@ void SiteScreen::renderPartner(uint siteNum, uint32 tickMs) {
int x;
int y;
if (_vm->isFloppy()) {
- // `_DoSiteLoop_Floppy @ 1652:042b`: site_data+8 is a u16 OFFSET
- // to `_SpeakerInfo_Floppy`; the first 10 bytes are the idle pair:
- // +0..1 Jake anim, +2..3 Jake X, +4 Jake Y,
- // +5..6 Jenny anim, +7..8 Jenny X, +9 Jenny Y.
const uint16 spkOff = READ_LE_UINT16(site + 8);
const byte *spk = _mystery->blobAt(spkOff);
if (!spk)
@@ -1725,9 +1473,6 @@ void SiteScreen::renderPartner(uint siteNum, uint32 tickMs) {
if (!_vm->getAni().loadAnimation(animId, anim) || anim.empty())
return;
- // Relative phase anchor (not raw tickMs) so wait anim resumes
- // from script[0] after each kdAnim one-shot â matches
- // `_PlayAnimation @ 172b:1f5d` reset to frame 0xffff.
const uint32 elapsed = (tickMs >= _waitPhaseAnchor)
? (tickMs - _waitPhaseAnchor)
: tickMs;
@@ -1760,8 +1505,6 @@ bool SiteScreen::renderFloppyHotspotPartnerPose(uint siteNum) {
return false;
const uint16 spkOff = READ_LE_UINT16(site + 8);
- // `_OnSiteHotspotClicked_Floppy @ 1652:017a`: after restoring the site
- // it loads the active pair from `_SpeakerInfo_Floppy + 0x28/0x2d`.
const uint poseOff = (_vm->getPartnerIndex() == kPartnerJake)
? 0x28 : 0x2d;
if ((uint32)spkOff + poseOff + 5 > _mystery->dataSize())
@@ -1790,12 +1533,6 @@ bool SiteScreen::renderFloppyHotspotPartnerPose(uint siteNum) {
}
void SiteScreen::renderBackground(uint siteNum) {
- // `_BuildBackground(sitepic, 0x42, 0x14)` from `_DoSiteLoop @
- // 168d:03f4` / `_DisplayCorrect`:
- // 1. `_GetFromDB(_PicIndex, 0x3d)` â DBI entry 0x3d (0-based).
- // 2. SITES.DBD entry `sitepic` (0-based, SiteData[+0..+1]).
- // 3. `_Rect_Move(..., 0x42, 0x14, 48000, ...)` composes at (66, 20).
- // 4. `_GetPalette(sitepic + 1)`.
Picture frame;
if (_vm->getPics().loadEntry(0x3d, frame)) {
g_system->copyRectToScreen(frame.surface.getPixels(),
@@ -1803,11 +1540,6 @@ void SiteScreen::renderBackground(uint siteNum) {
0, 0, frame.surface.w, frame.surface.h);
}
- // `_BuildBackground @ 172b:13e2` â `_GetFromDB(_siteFile,
- // &_SiteDBIndex, sitenum)` uses SiteData[+0] as a 0-based SITES.DBD
- // index (stride 10, asm `IMUL BX,BX,0xa` @ 172b:14c8; no -1 adjust).
- // Floppy: site_data[0] is u16 offset to drops sub-struct; drops[0]
- // byte is the SITES.DBD index (`FUN_16e2_12fd @ 16e2:12fd`).
const byte *site = _mystery->siteData(siteNum);
uint16 sitepic = 0;
if (site) {
@@ -1855,7 +1587,6 @@ byte currentWhitePaletteIndex(byte fallback) {
}
void SiteScreen::renderHotspots(uint siteNum) {
- // `_DrawSearchButtons`. Port adds optional "hide hint" setting.
if (ConfMan.getBool("hide_highlight_boxes"))
return;
@@ -1868,21 +1599,6 @@ void SiteScreen::renderHotspots(uint siteNum) {
if (!screen)
return;
- // `_DrawSearchButtons @ 2404:0a8f`:
- // for each hotspot:
- // if _Sawit(theSite, loc) == 0 (NOT seen yet):
- // _DrawRect(rect) â outline in cycling colors
- // 0xF9..0xFE; `_ColorCycle(0xF9, 0xFE)`
- // rotates them every tick â "marching
- // ants" glow on unsearched spots.
- // else (seen):
- // _DrawSolidRect(rect) â outline in solid colour 0xFF.
- // (Branch order per asm `2404:0af6 OR AX,AX; 2404:0af8 JZ` â the
- // C-level decompile mis-reordered the if/else.)
- // Palette 0xF9..0xFE is already rotated by `applyColorCycles` each
- // tick, so drawing a single colour from that range pulses on its
- // own. Phase the start colour by hotspot index so adjacent spots
- // don't glow in lock-step.
const uint32 tickMs = g_system->getMillis();
// CD hotspot row = 14 bytes:
@@ -1918,12 +1634,8 @@ void SiteScreen::renderHotspots(uint siteNum) {
_mystery->_hotSpotsSeen[seenKey];
}
if (seen) {
- // `_DrawSolidRect @ 172b:0506` â solid, non-cycling outline.
screen->frameRect(rect, searchedColor);
} else {
- // `_DrawRect @ 172b:03e2` â walk all four edges incrementing
- // the colour per pixel through palette indices 0xF9..0xFE,
- // which `_ColorCycle(0xF9, 0xFE)` rotates every tick.
byte color = (byte)(0xF9 + ((i + (tickMs / 80)) & 0x07) % 6);
// Top edge
for (int x = rect.left; x < rect.right; x++) {
@@ -2069,12 +1781,10 @@ void SiteScreen::onHotspotClicked(uint siteNum, uint hotIdx) {
// Snapshot `_cluesFound` â detect new-clue 0â1 â autosave.
byte before[Mystery::kCluesFoundCap];
memcpy(before, _mystery->_cluesFound, sizeof(before));
- // Partner-less BG for `_DoKDAnim` / `playKdAnim` to erase
- // the resting idle (fires when ClueEntry +0x3a != -1).
_vm->setPartnerEraseBg(&_bgSnapshot);
_vm->displayClue(clueBlock);
_vm->setPartnerEraseBg(nullptr);
- // Port-only autosave (original = manual `_SaveGame @ 2404:0c87`).
+ // New feature: autosave on new clue.
bool foundNewClue = false;
for (uint i = 0; i < Mystery::kCluesFoundCap; i++) {
if (!before[i] && _mystery->_cluesFound[i]) {
@@ -2092,24 +1802,16 @@ void SiteScreen::onHotspotClicked(uint siteNum, uint hotIdx) {
}
}
}
-
+// `_DoKDAnim(num) @ 168d:028a` + `_PlayAnimation @ 172b:1f46`:
+// _SuspendAnimation(WaitHandle);
+// anim = kKdAnimTable[num].anim[partner] (@ 29be:0228)
+// x = kKdAnimTable[num].x[partner]
+// y = kKdAnimTable[num].y[partner]
+// _PlayAnimation(anim, x, y, WaitHandle)
+// -> registers a state-4 (one-shot) animation slot and lets
+// `_UpdateAnimations` walk the script until 0x80, then
+// frees the slot and re-activates `WaitHandle`.
void EEMEngine::playKdAnim(uint16 num) {
- // Mirrors `_DoKDAnim(num) @ 168d:028a` + `_PlayAnimation @ 172b:1f46`:
- // _SuspendAnimation(WaitHandle);
- // anim = kKdAnimTable[num].anim[partner] (@ 29be:0228)
- // x = kKdAnimTable[num].x[partner]
- // y = kKdAnimTable[num].y[partner]
- // _PlayAnimation(anim, x, y, WaitHandle)
- // -> registers a state-4 (one-shot) animation slot and lets
- // `_UpdateAnimations` walk the script until 0x80, then
- // frees the slot and re-activates `WaitHandle`.
- // Port renders the partner's idle inline in each redraw rather
- // than via a slot system, so we play the one-shot synchronously
- // (blocking) and resume normal idle rendering when the caller
- // returns â matches the visible effect (partner gesture finishes
- // before the speaker portrait + speech balloon appear).
- // `kKdAnimTable` and `kAnimScripts` live at file scope above. EEM2 ships a
- // different table (positions + Jenny reactions) â use it for London.
if (num >= ARRAYSIZE(kKdAnimTable))
return;
@@ -2158,19 +1860,12 @@ void EEMEngine::playKdAnim(uint16 num) {
Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.simpleBlitFrom(bg);
- // Anchor-aware: kdAnim cells (0x03/0x04/0x0c/0x0d ...) have
- // non-zero per-frame `miscflags`/`rowoff` (anim 0x03 has rowoff
- // up to 9, anim 0x04 has miscflags = -2). Routes through
- // `blitAnimFrameAnchored` so the gesture translates across
- // cells instead of pinning to a fixed pixel.
(void)transp; // anchored blitter recomputes from p.flags
blitAnimFrameAnchored(scratch.surfacePtr(), fr, px, py);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
- // 100 ms per frame (~10 fps). Pump updateScreen inside the wait
- // so cursor overlay refreshes at 100 Hz.
const uint32 wakeup = g_system->getMillis() + 100;
while (g_system->getMillis() < wakeup && !shouldQuit()) {
Common::Event ev;
diff --git a/engines/eem/site.h b/engines/eem/site.h
index 9bdbc88aa4a..30567b2339d 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -36,11 +36,10 @@ class EEMEngine;
class Mystery;
/// partnerFrameAtTick: frame index for `seqnum` at `tickMs`. Walks `kAnimScripts`
-/// at `kFramePeriodMs` (~140 ms = `_CheckFrameRate` cadence) per entry; wraps
+/// at `kFramePeriodMs` per entry; wraps
/// on the script's 0x80 terminator. Falls back to flipbook (`tick % numFrames`)
/// when no script is registered. `numFrames` is the ANI.DBD entry's cell count,
/// used both for the fallback and to clamp script values past the asset.
-/// Mirrors the looping path of `_UpdateAnimations @ 172b:09c1`.
uint partnerFrameAtTick(uint16 seqnum, uint numFrames, uint32 tickMs);
/// Select the EEM2 ("London") animation-script table inside `findAnimScript`.
@@ -49,43 +48,25 @@ uint partnerFrameAtTick(uint16 seqnum, uint numFrames, uint32 tickMs);
void setLondonAnimScripts(bool enabled);
/// bigMapPartnerFrameAtTick: count-up 0..8 once, then loop `_BigMapWaitSeq`
-/// (9,9,9,9,10,9,9,9,9). Mirrors `_DoBigMap @ 20fe:09e7` two-phase swap from
-/// script 0x14 (count-up @ 29be:196a) to `_BigMapWaitSeq @ 29be:1574`.
uint bigMapPartnerFrameAtTick(uint numFrames, uint32 elapsedMs);
/// bigMapDetailPartnerFrameAtTick: zoomed-view partner frame. Same two-phase shape
-/// as `bigMapPartnerFrameAtTick`. `_DoMapScreen @ 20fe:120b` runs script 0x13
-/// (count-up 0..7 @ 29be:1992) then swaps to `_SmallMapWaitSeq @ 29be:1548`
-/// (`MOV [BX+0x789f],0x1548` at 20fe:1390).
+/// as `bigMapPartnerFrameAtTick`.
uint bigMapDetailPartnerFrameAtTick(uint numFrames, uint32 elapsedMs);
/// blitAnimFrameAnchored: mask-blit at (anchorX - frame.miscflags,
-/// anchorY - frame.rowoff). Mirrors per-frame anchor math in
-/// `_UpdateAnimations @ 172b:09c1`. Both anchors are SIGNED int16
-/// (e.g. anim 0x14 BigMap walk-cycle has miscflags = -2 per cell, so
-/// the sprite translates across the screen as it cycles). Use for any
-/// animation rendered through `_NewAnimation` in the original â partner
-/// sprites, animated drops, briefing animations. Transparency comes
-/// from `flags >> 8` (NOT miscflags).
+/// anchorY - frame.rowoff).
void blitAnimFrameAnchored(Graphics::Surface *screen, const Picture &p,
int anchorX, int anchorY);
/// Rotate one VGA palette range by one slot (STARTâEND direction).
-/// Mirrors `_ColorCycle`. Used by per-site Loop-1 ColorCycle entries,
-/// hotspot marching ants (0xF9..0xFE), and the BigMap marker shine.
void cyclePaletteRange(uint8 start, uint8 end);
/// Rotate one VGA palette range by one slot in the OPPOSITE direction
-/// (ENDâSTART): save END, shift every entry up by one (END-1 â END, ...),
-/// wrap saved END to START. Mirrors `_OpenColorCycle @ 2520:04f7` (CD) /
-/// `_ReverseColorCycle_Floppy`. Used by opening-anim logos (EA Kids, etc.)
-/// where the cycle shifts ENDâSTART rather than STARTâEND.
+/// (ENDâSTART).
void cyclePaletteRangeReverse(uint8 start, uint8 end);
/// Load the 6-step yellow marching-ants ramp into palette 0xF9..0xFE
-/// (SITEPALS ships these as uniform yellow). Shared by site hotspots and the
-/// clue puzzle so both outline clickable areas in the same original colours;
-/// `cyclePaletteRange(0xF9, 0xFE)` then pulses them.
void applyHotspotGlowPalette();
/// One hotspot (search rectangle) within a site, 14 bytes on disk.
@@ -98,8 +79,7 @@ struct Hotspot {
Common::Rect rect() const { return Common::Rect(x1, y1, x2, y2); }
};
-/// Site / scene controller. Mirrors `_DrawSearchButtons @ 2404:0a8f` /
-/// `_SearchButtons @ 2404:0bfb` site loop.
+/// Site / scene controller.
class SiteScreen : public Common::EventObserver {
public:
SiteScreen(EEMEngine *vm, Mystery *mystery)
@@ -126,32 +106,17 @@ private:
void notePartnerActivity();
bool playLondonTravelAnimation(uint fromSite, uint toSite);
- /// Partner site-arrival sequence (when `_LastSite != _SiteNumber`).
- /// Mirrors `_EnterSiteAnim @ 1000:9b21`: anim 6/14 (Jake/Jenny) skateboards
- /// in from right, then anim 7/15 slides KD in from left. Returns true if skipped.
+ /// Partner site-arrival sequence
bool enterSiteAnim();
- /// renderPartner: persistent in-site partner sprite at `_WaitAnims @ 29be:021c`.
- /// Mirrors `_GetAnimation` + `_NewAnimation` tail of `_DoSiteLoop @ 168d:03f4`.
+ /// renderPartner: persistent in-site partner sprite
void renderPartner(uint siteNum, uint32 tickMs);
- /// Floppy active speaker pose shown before `_HandleHotspotClick_Floppy`.
- /// Uses `_SpeakerInfo_Floppy + 0x28/0x2d`, replacing the idle partner.
+ /// Floppy active speaker pose
bool renderFloppyHotspotPartnerPose(uint siteNum);
- /// renderStaticDrops: `_AddDrop` static decorations (Loop 2).
- /// siteData[+0x4] count, siteData[+0xc] entries (6 bytes: {picId, x, y}).
void renderStaticDrops(uint siteNum);
-
- /// renderFloppyDrops: floppy variant. `*site_data`âdrops; drops[1]=count;
- /// entries at drops+2 are 5 bytes ({u16 X, u16 Y, byte picID}); PIC loaded as picID-1.
- /// Per `_DoSiteLoop_Floppy @ 1652:0418` and `FUN_16e2_18eb`.
void renderFloppyDrops(uint siteNum);
-
- /// renderAnimatedDrops: per-site animated NPCs (Loop 1) at current tick.
- /// `_DoSiteLoop` registers each via `_NewAnimation` (siteData[+0xa] count,
- /// siteData[+0x48] entries: {animId (-1=ColorCycle), x, y}); frames advance via
- /// `_UpdateAnimations @ 172b:09c1`.
void renderAnimatedDrops(uint siteNum, uint32 tickMs);
/// Snapshot the post-BG, post-static-drops screen so the per-tick
@@ -168,11 +133,10 @@ private:
void syncCompositedScreen();
/// scanColorCycles: scan Loop 1 for ColorCycle entries (animId == -1),
- /// cache (start, end) palette ranges. Mirrors `_DoSiteLoop @ 168d:03f4` init scan.
+ /// cache (start, end) palette ranges.
void scanColorCycles(uint siteNum);
- /// applyColorCycles: rotate cached ColorCycle ranges + 0xf9..0xfe (hotspot ants)
- /// one step. Mirrors per-tick `_ColorCycle(start, end)` calls in `_DoSiteLoop`.
+ /// applyColorCycles: rotate cached ColorCycle ranges
void applyColorCycles();
EEMEngine *_vm;
@@ -188,10 +152,7 @@ private:
uint32 _impatientDeadlineMs = 0; ///< Test-shortened impatience deadline.
PartnerWaitMood _partnerWaitMood = kPartnerWaitDefault;
- /// Wall-clock anchor for partner wait anim phase: rendered as
- /// `partnerFrameAtTick(animId, ..., now - _waitPhaseAnchor)`.
- /// Bumped on site entry (`_NewAnimation` writes 0xffff at `_DoSiteLoop @ 168d:0436`)
- /// and on return from a one-shot kdAnim (`_PlayAnimation @ 172b:1f5d` state=4).
+ /// Wall-clock anchor for partner wait anim phase
uint32 _waitPhaseAnchor = 0;
/// Per-site cached ColorCycle ranges (up to 5, matching original 5-slot anim table).
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 6acea3e5d58..dc7c47c74a4 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -40,8 +40,6 @@
namespace EEM {
-// Gallery slot positions @ 29be:0x116. Used by `_DrawGallery @ 158f:0046`
-// and the accuse portrait grid.
struct GallerySlot { int x; int y; };
const GallerySlot kGallerySlots[5] = {
{ 83, 14 }, // 0
@@ -149,9 +147,7 @@ bool loadLondonApproachData(uint16 approachId, LondonApproachData &out) {
out.pages.clear();
uint32 pos = 16;
// `_DoApproach @ 1717:009b` reads the pages with `_fgets` into 255-byte
- // slots, so each page is one NEWLINE-terminated record â NOT NUL-separated.
- // (A*.BIN carries no NULs; splitting on '\0' lumps every page into page 0,
- // which is why the Next/Prev arrows had nothing to page through.)
+ // slots, so each page is one NEWLINE-terminated record, NOT NUL-separated.
for (uint16 i = 0; i < pageCount && pos < size; i++) {
const uint32 start = pos;
while (pos < size && data[pos] != '\n')
@@ -188,16 +184,13 @@ bool decodeLondonApproachFirstFrame(uint16 videoId,
return true;
}
-// Setup-screen highlight rects (referenced by both `doSetup` and the
-// helper `EEMEngine::setupDrawScreen`). `_SetupHighlights @ 29be:1320`.
+// Setup-screen highlight rects
constexpr Common::Rect kSetupKid1Rect (Common::Point( 99, 44), 49, 8);
constexpr Common::Rect kSetupKid2Rect (Common::Point( 99, 54), 49, 8);
constexpr Common::Rect kSetupSoundOnRect (Common::Point(106, 86), 19, 8);
constexpr Common::Rect kSetupSoundOffRect(Common::Point(106, 96), 19, 8);
-// EEM2/London setup highlights â `_SetupSettings @ 2046:0008` (_SwapColors,
-// key 0xFE). On/off label pairs for the 4 toggles (rects @ 2bca:13ec; drawn
-// in the DOS order ON-then-OFF). Partner reuses Jake/Jenny.
+// EEM2/London setup highlights
constexpr Common::Rect kLonSetJake (Common::Point( 99, 44), 49, 8); // 0x13ec
constexpr Common::Rect kLonSetJenny (Common::Point( 99, 54), 49, 8); // 0x13f4
constexpr Common::Rect kLonSetVoiceOn (Common::Point(106, 68), 20, 8); // 0x13fc
@@ -207,9 +200,6 @@ constexpr Common::Rect kLonSetMusicOff(Common::Point(128, 85), 18, 7); // 0x142
constexpr Common::Rect kLonSetHiOn (Common::Point(106, 110), 29, 8); // 0x1414
constexpr Common::Rect kLonSetHiOff (Common::Point(106, 100), 29, 8); // 0x140c
-// `_SwapColors @ 172b:1d2a` â replace pixels in r where value==from
-// with to. 0xFE = BG text-key; 0x15 = active palette index, 0x00 =
-// inactive (set by `_SetupSettings @ 1f78:000d`).
void swapColors(Graphics::ManagedSurface &dst,
const Common::Rect &r, byte from, byte to) {
const int x1 = MAX<int>(0, r.left);
@@ -225,9 +215,7 @@ void swapColors(Graphics::ManagedSurface &dst,
}
}
-// PDA-frame partner sprite specs. `(5, 0x50)` is the partner-at-desk anchor
-// shared by _DoNotebook (script 0x01, anim 1/0xb) and _DoGallery / _DoAccuse
-// / _MoreInfo (script 0x02, anim 2/0x10).
+// TODO: change Pda naming to TRAVIS
struct PdaPartnerSpec {
uint16 scriptId;
uint16 animJake;
@@ -277,9 +265,6 @@ constexpr uint16 kFloppyEndingBackgroundPic = 0x8b;
constexpr uint16 kFirstTryBadgePic = 0x205;
constexpr Common::Point kFirstTryBadgePos(0x1e, 9);
-// kPdaSiteRect / kPdaPartnerFootMapRect / kPdaPartnerHeadHintRect live in
-// eem.h (shared with site.cpp). The button-row rects below are PDA-screen-
-// only (`_NoteButtons @ 29be:0147`).
constexpr Common::Rect kPdaHelpRect(Common::Point(93, 174), 22, 16);
constexpr Common::Rect kPdaNotebookRect(Common::Point(134, 174), 21, 16);
constexpr Common::Rect kPdaGalleryRect(Common::Point(157, 174), 21, 16);
@@ -287,9 +272,6 @@ constexpr Common::Rect kPdaAccuseRect(Common::Point(180, 174), 21, 16);
constexpr Common::Rect kPdaPageNextRect(Common::Point(204, 174), 20, 16);
constexpr Common::Rect kPdaPagePrevRect(Common::Point(226, 174), 21, 16);
constexpr Common::Rect kPdaHelp2Rect(Common::Point(267, 174), 21, 16);
-// London-only: `_NoteButtons[9]` = (0,0,66,79) â site (EEM1's slot 9 is a
-// dead 0-rect). A secondary "close the PDA" hotspot over the device's
-// top-left corner. EEM2 `_HandleNoteButton` slot 9 â screen 3.
constexpr Common::Rect kPdaLondonCloseRect(Common::Point(0, 0), 66, 79);
constexpr uint16 kProfilePickerRevealPic = 0x105;
@@ -404,8 +386,7 @@ const byte *advanceFloppyDialogRecords(const byte *rec, uint count,
return rec;
}
-// Floppy gallery slot positions @ 2608:0x16c. Read by `_DrawGallery_Floppy
-// @ 154e:0045`'s [BX + 0x16c] (x) and [BX + 0x16e] (y) loads.
+// Floppy gallery slot positions @ 2608:0x16c.
const GallerySlot kFloppyGallerySlots[5] = {
{ 0x53, 0x0e }, // 0
{ 0x9b, 0x0e }, // 1
@@ -414,9 +395,7 @@ const GallerySlot kFloppyGallerySlots[5] = {
{ 0xbf, 0x5a } // 4
};
-// `_GetKDTextBalloon @ 1df2:0105` digit-balloon table @ 29be:1064:
-// '0'â0x15 '1'â0x16 '2'â0x17 '3'â0x18 '4'â0x19
-// '5'â0x1a '6'â0x20 '7'â0x21 '8'â0x22 '9'â0x1e
+// `_GetKDTextBalloon @ 1df2:0105` digit-balloon table @ 29be:1064
const uint16 kDigitBalloons[10] = {
0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x20, 0x21, 0x22, 0x1e
};
@@ -445,7 +424,6 @@ void copyToScreen(Graphics::ManagedSurface &scratch) {
}
void cycleChooserPalette() {
- // `_DoChoose` / `_DoListPicker_Floppy` rotate 0x6f..0x73 per tick.
cyclePaletteRange(kChooserCycleStart, kChooserCycleEnd);
}
@@ -632,12 +610,10 @@ struct ActionMenuView {
const char *const *pickLabel;
const bool *pickEnabled;
uint pick;
- uint numPicks; ///< Visible entries (London drops ScrapBook 3 â 4).
+ uint numPicks;
};
-// `_DoChooseMystery @ 1a35:02b7` â opens BOOK%u.NME (CRLF strings, up
-// to 25 Ã 40 bytes, whitespace-line sentinel). `_DoChoose @ 1c33:0514`
-// walks until {0, 0}.
+// `_DoChooseMystery @ 1a35:02b7`
Common::StringArray loadBookNames(uint book) {
Common::StringArray names;
const Common::String fname = Common::String::format("BOOK%u.NME", book);
@@ -672,8 +648,6 @@ void clampCaseTopRow(uint &topRow, uint listLen, uint visibleRows) {
topRow = maxTop;
}
-// "Choose A Mystery" sub-chooser view. names = BOOK%d.NME entries
-// (mystery number = tierLo + index). solvedFlags marks greyed entries.
struct CaseSubmenuView {
EEMEngine *vm;
const Picture *caseBg;
@@ -688,7 +662,7 @@ struct CaseSubmenuView {
const Common::Array<bool> *solvedFlags;
uint topRow;
uint selRow;
- uint book; ///< 1..3 â for the "Book N" / "Challenge Book" title
+ uint book;
};
void drawCaseGreeter(Graphics::ManagedSurface &scratch,
@@ -697,8 +671,6 @@ void drawCaseGreeter(Graphics::ManagedSurface &scratch,
if (!haveKdAnim || !kdAnim || kdAnim->empty())
return;
- // `_CaseSelection` drives the partner ANI with script 0x15 regardless
- // of partner.
const uint32 now = g_system->getMillis();
const uint frameIdx = partnerFrameAtTick(0x15, (uint)kdAnim->size(), now);
blitAnimFrameAnchored(scratch.surfacePtr(), (*kdAnim)[frameIdx],
@@ -755,9 +727,7 @@ bool animateCaseSelectionReveal(EEMEngine *vm, const Picture *caseBg,
return false;
}
-// `_DoChoose`'s `DrawList @ 1c33:040d`. 12 rows à 10 px at (61, 35).
-// Original colours 0x13 sel / 0x1B greyed / 0x5C default approximated
-// here as 0xF / 0x8 / 0x7 from site palette 0.
+// `DrawList @ 1c33:040d`
void drawCaseSubmenu(const CaseSubmenuView &v) {
Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
@@ -792,7 +762,7 @@ void drawCaseSubmenu(const CaseSubmenuView &v) {
kListX, kListY0 + r * kLineH, kListW, color);
}
- // Selection arrow (ScummVM-only â original uses colour change).
+ // Selection arrow.
if (v.selRow >= v.topRow && v.selRow < v.topRow + (uint)kVisible) {
const int r = (int)(v.selRow - v.topRow);
v.vm->getFont().drawString(&scratch, ">",
@@ -855,12 +825,8 @@ void drawActionMenuFrame(const ActionMenuView &v) {
}
void EEMEngine::doProfilePicker() {
- // `screen8_handler @ 1c33:1012` â walks *.PLR (max 25), reads 12-byte
- // player-name field, hands list to `_DoChoose`. No profiles or
- // 0xfffe/0xffff sentinel enters `_NewPlayer`.
_profileCreatedThisSession = false;
- // `screen8_handler` does `_FadeOut(); _GetPalette(0); _GetBackground(0x104)`.
setSitePalette(0);
const SaveStateList saves = listProfiles();
@@ -880,11 +846,6 @@ void EEMEngine::doProfilePicker() {
return;
}
- // Existing profiles only â the original `screen8_handler @ 1c33:1012`
- // passes the bare *.PLR list to `_DoChoose` with NO synthesized "new
- // player" entry. _DoChoose returns 0xfffe / 0xffff for the bottom click
- // rect @ 29be:0d08 / ESC, which routes to `_NewPlayer` (handled below
- // via kChooserNewPlayerRect / KEYCODE_ESCAPE).
Common::Array<ProfilePickerEntry> entries;
for (const SaveStateDescriptor &s : saves) {
ProfilePickerEntry e;
@@ -903,8 +864,6 @@ void EEMEngine::doProfilePicker() {
const bool haveReveal =
_picsArchive.getPicture(kProfilePickerRevealPic, reveal);
- // Geometry: list origin (61, 35), 10 px/row, 12 visible. PIC 0x105
- // slides into lower strip via `screen8_handler` before `_DoChoose`.
ProfilePickerView view;
view.vm = this;
view.bg = &bg;
@@ -1000,7 +959,6 @@ void EEMEngine::doProfilePicker() {
break;
}
if (kChooserHelpRect.contains(ev.mouse.x, ev.mouse.y)) {
- // `screen8_handler` sets Chelp=0 â button ignored.
break;
}
if (kChooserListRect.contains(ev.mouse.x, ev.mouse.y)) {
@@ -1047,7 +1005,6 @@ void EEMEngine::doProfilePicker() {
return;
}
- // `_LoadPlayerRecord @ 1c33:1281`.
const ProfilePickerEntry &e = entries[sel];
if (loadProfile(e.label)) {
_profileCreatedThisSession = false;
@@ -1060,10 +1017,8 @@ void EEMEngine::doProfilePicker() {
doNewPlayer();
}
}
-
+// `_NewPlayer @ 1c33:0dda`
void EEMEngine::doNewPlayer() {
- // `_NewPlayer @ 1c33:0dda` â BG 0x104 + peek pic 0x107, prompt for
- // up to 12 chars.
_profileCreatedThisSession = false;
if (!_font.isLoaded()) {
_playerName = "Detective";
@@ -1106,8 +1061,6 @@ void EEMEngine::doNewPlayer() {
if (k == Common::KEYCODE_RETURN) {
if (name.empty())
name = "Detective";
- // `_NewPlayer @ 1c33:0fa0+`: `_LoadPlayerRecord` â if
- // missing, zero state and `_SavePlayerRecord`.
if (loadProfile(name)) {
_profileCreatedThisSession = false;
} else {
@@ -1115,7 +1068,6 @@ void EEMEngine::doNewPlayer() {
memset(_mysteriesSolved, 0, sizeof(_mysteriesSolved));
_mystery.clear();
_partner = kPartnerJake;
- // `_NewPlayer @ 1c33:0fa3`: DAT_2d5d_3f99 = 1 (Junior).
_chainStage = 1;
saveProfile(name);
_profileCreatedThisSession = true;
@@ -1151,26 +1103,8 @@ void EEMEngine::doNewPlayer() {
g_system->setFeatureState(OSystem::kFeatureVirtualKeyboard, false);
}
-
+// `_DisplayEnding @ 1df2:0548` + `_DisplayEndingPage @ 1df2:044c`
int EEMEngine::doShowEnding(uint num, bool firstPage) {
- // `_DisplayEnding @ 1df2:0548` + `_DisplayEndingPage @ 1df2:044c`
- // (CD); `FUN_1d40_05b7` + `FUN_1d40_031e` (floppy).
- // CD E<num>.BIN format:
- // u16 pageCount
- // per page: u16 picNum
- // u16 x1, y1, x2, y2 (story rect â passed to WordWrap2)
- // char text[] (null-terminated, ParseString opcodes)
- // Floppy: small title header + shared newspaper BG (PIC 0x8b) +
- // per-page overlay pictures.
- // Render: _FreeFont; _LoadFont("tiny.fnt") @ 1df2:055f; _GetPalette(0);
- // per-page _GetBackground(picNum) + _WordWrap2(x1, y1, x2-x1, text,
- // fontColor=0, dropColor=-1). fontColor=0 is the newspaper body-text
- // colour (asm 1df2:04cf-04f4 â Ghidra mis-paired the trailing args).
- // LEFT/RIGHT page nav (1df2:0689 / 06a0): boundary sets [BP-0x18] to
- // -1 / +1 â that return value drives `_ShowScrapbook` walking forward/
- // back through solved mysteries (1f78:0664-069c).
- // firstPage=false opens at LAST page (used by doShowScrapbook after
- // "previous mystery"; matches `local_8 = 0` @ 1f78:067e).
const Common::String fname = Common::String::format("E%u.BIN", num);
Common::File f;
if (!f.open(Common::Path(fname))) {
@@ -1189,18 +1123,16 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
return 0;
}
- // 1df2:0558-056a: TINY.FNT; restored at 1df2:0625.
EEMFont tinyFont;
const bool haveTinyFont = tinyFont.load(Common::Path("TINY.FNT"));
if (!haveTinyFont)
warning("doShowEnding: TINY.FNT failed to load â falling back");
- // 1df2:055f `_GetPalette(0)` â newspaper CLUT (body text = idx 0).
setSitePalette(0);
CursorMan.showMouse(true);
const bool floppyEnding = isFloppy();
- uint pageOffsets[8]; // ENDING_RANGE_MAX from `_DisplayEnding`
+ uint pageOffsets[8];
const uint pageOffsetCap =
(uint)(sizeof(pageOffsets) / sizeof(pageOffsets[0]));
uint validPages = 0;
@@ -1264,7 +1196,7 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
_picsArchive.getPicture(kFirstTryBadgePic, firstTryBadge);
uint pageIdx = firstPage ? 0 : (validPages - 1);
- int direction = 0; // -1 / 0 / +1, see header doc.
+ int direction = 0;
bool exitLoop = false;
bool dirty = true;
const Common::Point mousePos = g_system->getEventManager()->getMousePos();
@@ -1334,10 +1266,8 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
kFirstTryBadgePos, transp);
}
- // `_ParseString` placeholders (0x80=name, 0x82=partner first).
const Common::String text = parseString(raw, _playerName, _partner);
- // TINY.FNT + color 0 (asm 1df2:04cf â not 0xF as Ghidra shows).
const EEMFont &renderFont = haveTinyFont ? tinyFont : _font;
if (renderFont.isLoaded() && x2 > x1) {
const int textW = MIN<int>((int)x2 - (int)x1, kScreenWidth - (int)x1);
@@ -1430,13 +1360,9 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
setInteractiveMouseCursor(false);
return direction;
}
-
+// EEM1 `_ShowScrapbook(stage, 0) @ 1f78:0642`; EEM2/London scrapbook `2046::09dd`.
void EEMEngine::doShowScrapbook(uint stage) {
- // EEM1 `_ShowScrapbook(stage, 0) @ 1f78:0642`; EEM2/London scrapbook
- // `FUN_2046_09dd`. Ending viewer returns -1/0/+1 for prev/close/next;
- // current tier skips unsolved entries. Tier sizes are variant-specific
- // (EEM1: 24/24/6 cases over three books; London: 25/25 over two books,
- // no BOOK3.NME) â `mysteryTierRange` returns false for London stage 3.
+
uint tierLo = 0, tierHi = 0;
if (stage < 1 || !mysteryTierRange(stage, tierLo, tierHi))
return;
@@ -1448,11 +1374,6 @@ void EEMEngine::doShowScrapbook(uint stage) {
return;
const bool currentTier = (stage == _chainStage);
- // `_ShowScrapbook @ 2046:0874` (EEM2) opens with the scrapbook/newspaper
- // MIDI tune 0x5d (same one `_ShowOneScrap` plays after a correct accusation),
- // gated on the voice flag + MIDI availability (DOS `DAT_3036_4c4e`/`146a` â
- // the original gates this music on the *voice* toggle, not the music one).
- // London only; EEM1's seg-1f78 scrapbook was not verified to play it.
if (isLondon() && _music && _voiceOn)
_music->playMus(0x5d, /* loop= */ false);
@@ -1493,40 +1414,16 @@ void EEMEngine::doShowScrapbook(uint stage) {
break;
}
}
- // `_ShowScrapbook` / `_ShowOneScrap` stop the MIDI on the way out.
if (isLondon() && _music)
stopMusic();
}
-
+// `_DoSetup @ 1f78:044e` (CD) / `_DoSetup_Floppy @ 1ee2:0387`.
void EEMEngine::doSetup() {
- // `_DoSetup @ 1f78:044e` (CD) / `_DoSetup_Floppy @ 1ee2:0387`.
- // PIC 0x40 BG with labels baked in palette key 0xFE; `_SetupSettings
- // @ 1f78:000d` runs `_SwapColors @ 172b:1d2a` per label rect
- // (0x15 active, 0x00 inactive â nothing drawn as text). 13Ã 8-byte
- // click rects at `_SetupButtons @ 29be:1218` (CD) / 2608:0d8c (floppy);
- // `HandleSetupButton @ 1f78:0158` dispatches via 12-entry jumptable
- // at 1f78:0436. Rect map (x1, y1, x2, y2 â handler):
- // [0] ( 20, 44, 39, 61) Partner toggle (1f78:017a)
- // [1] ( 20, 87, 39,104) Voice toggle (1f78:0196 â DAT_2d5d_3f97)
- // [2] ( 20,127, 39,144) back to profile (NextScreen=8)
- // [3] (281, 43,299, 60) ScrapBook 1 (_ShowScrapbook(0,1))
- // [4] (281, 62,299, 79) ScrapBook 2 gated chainStage>=2
- // [5] (281, 81,299, 98) ScrapBook 3 gated chainStage>=3
- // [6] (281,108,299,125) Save game (_SaveGame @ 2404:0c87)
- // [7] (281,127,299,144) New Case (NextScreen=0xa)
- // [8] ( 53,153,108,183) Done (SI=1, exit)
- // [9] (145,163,174,187) Help (_InterfaceHelp(1))
- // [10] (212,153,266,184) Quit (_AreYouSure â NextScreen=0xffff)
- // [11] ( 81, 25,238, 37) Credits (PIC 0x208 fullscreen)
- // [12] ( 11, 1, 3, 3) debug placeholder
- // Highlight rects (Kid1/Kid2/SoundOn/SoundOff @ 29be:1320..1338) drive
- // `_SwapColors` only â not click targets in the original.
if (!_font.isLoaded()) {
_nextScreen = (ScreenId)_lastScreen;
return;
}
- // Original button rects (`_SetupButtons` indices wired here).
const Common::Rect kPartnerBtn ( 20, 44, 39, 61); // [0]
const Common::Rect kVoiceBtn ( 20, 87, 39, 104); // [1]
const Common::Rect kProfileBtn ( 20, 127, 39, 144); // [2]
@@ -1539,7 +1436,7 @@ void EEMEngine::doSetup() {
const Common::Rect kHelpBtn (145, 163, 174, 187); // [9]
const Common::Rect kQuitBtn (212, 153, 266, 184); // [10]
const Common::Rect kCreditsBtn ( 81, 25, 238, 37); // [11]
- // Highlight / fallback-click rects (file-scope copies in `kSetup*Rect`).
+
const Common::Rect &kKid1Rect = kSetupKid1Rect;
const Common::Rect &kKid2Rect = kSetupKid2Rect;
const Common::Rect &kSoundOnRect = kSetupSoundOnRect;
@@ -1547,7 +1444,7 @@ void EEMEngine::doSetup() {
setupDrawScreen();
- _nextScreen = kScreenSetup; // sentinel â setupLeave picks target
+ _nextScreen = kScreenSetup;
while (!shouldQuit()) {
Common::Event ev;
bool dirty = false;
@@ -1569,8 +1466,6 @@ void EEMEngine::doSetup() {
const int mx = ev.mouse.x;
const int my = ev.mouse.y;
- // Partner toggle [0]. Direct Jake/Jenny label clicks are a
- // ScummVM-only fallback.
if (kPartnerBtn.contains(mx, my)) {
_partner = _partner == kPartnerJake ? kPartnerJenny : kPartnerJake;
dirty = true;
@@ -1591,7 +1486,6 @@ void EEMEngine::doSetup() {
continue;
}
- // Voice toggle [1].
if (kVoiceBtn.contains(mx, my)) {
_voiceOn = !_voiceOn;
if (_audio)
@@ -1618,46 +1512,35 @@ void EEMEngine::doSetup() {
continue;
}
- // New Case [7] @ 1f78:01ad â NextScreen=0xa (CHOOSE_MYSTERY).
if (kNewCaseBtn.contains(mx, my)) {
saveProfile(_playerName);
_nextScreen = kScreenChooseMystery;
return;
}
- // Save [6] â `_SaveGame @ 2404:0c87`.
if (kSaveBtn.contains(mx, my)) {
saveProfile(_playerName);
continue;
}
- // Done [8] â MOV SI,1; JMP exit (NextScreen stays = LastScreen).
if (kDoneBtn.contains(mx, my)) {
setupLeave();
return;
}
- // Quit [10] â `_AreYouSure(0)` â NextScreen=0xffff.
if (kQuitBtn.contains(mx, my)) {
if (areYouSure()) {
_nextScreen = kScreenInvalid;
return;
}
- dirty = true; // restore the BG after the prompt
+ dirty = true;
continue;
}
- // Help [9] â `_InterfaceHelp(1) @ 1560:0205`. Help-pic table
- // @ 29be:00c8: each `num` slot = 5 bytes (count + two u16
- // PIC IDs). num=1 â count=2, pics = {0x0192, 0x01B1}. Each
- // pic blitted via `_Rect_Move_Mask` (transparent = pic
- // `miscflags >> 8`, so setup BG shows through). Also hides
- // cursor (1560:0216-021c). ESC breaks (1560:02b3).
if (kHelpBtn.contains(mx, my)) {
static const uint16 kHelp1Pics[] = { 0x0192, 0x01B1 };
CursorMan.showMouse(false);
for (uint i = 0; i < ARRAYSIZE(kHelp1Pics); i++) {
- // Restore BG between cards (1560:02e5 `_vga_fvidvid(0)`).
setupDrawScreen();
const Common::KeyCode k =
setupShowFullscreenPic(kHelp1Pics[i], /* transparent= */ true);
@@ -1669,7 +1552,6 @@ void EEMEngine::doSetup() {
continue;
}
- // Credits [11] @ 1f78:025a â PIC 0x208 fullscreen.
if (kCreditsBtn.contains(mx, my)) {
CursorMan.showMouse(false);
setupShowFullscreenPic(0x208, /* transparent= */ false);
@@ -1680,14 +1562,12 @@ void EEMEngine::doSetup() {
continue;
}
- // Profile [2] â NextScreen=8.
if (kProfileBtn.contains(mx, my)) {
saveProfile(_playerName);
_nextScreen = kScreenProfile;
return;
}
- // ScrapBook [3]/[4]/[5] â `_ShowScrapbook(stage, 0)`.
if (kScrap1Btn.contains(mx, my)) {
doShowScrapbook(1);
setSitePalette(0);
@@ -1700,7 +1580,7 @@ void EEMEngine::doSetup() {
dirty = true;
continue;
}
- // London has no third book (`mysteryTierRange` rejects stage 3).
+
if (kScrap3Btn.contains(mx, my) && !isLondon() &&
_chainStage >= 3) {
doShowScrapbook(3);
@@ -1741,9 +1621,6 @@ void EEMEngine::setupDrawScreen() {
g_system->updateScreen();
}
-// Render picId and block until input. transparent=true: overlay via
-// `_Rect_Move_Mask` (`_InterfaceHelp @ 1560:0205`). false: raw fullscreen
-// blit via `_vga_fbuffvid` (credits @ 1f78:0281).
Common::KeyCode EEMEngine::setupShowFullscreenPic(uint16 picId, bool transparent) {
Picture pic;
if (!_picsArchive.getPicture(picId, pic)) {
@@ -1759,7 +1636,6 @@ Common::KeyCode EEMEngine::setupShowFullscreenPic(uint16 picId, bool transparent
g_system->unlockScreen();
}
const byte transp = (byte)(pic.flags >> 8);
- // Explicit destPos â no-destPos overload stretches to dst.
scratch.transBlitFrom(pic.surface, Common::Point(0, 0),
(uint32)transp);
} else {
@@ -1786,8 +1662,6 @@ Common::KeyCode EEMEngine::setupShowFullscreenPic(uint16 picId, bool transparent
}
void EEMEngine::setupLeave() {
- // `_DoSetup` entry: _NextScreen = _LastScreen. Fall back to it
- // unless a handler already overrode _nextScreen.
if (_nextScreen == kScreenSetup) {
_nextScreen = (ScreenId)_lastScreen;
if (_nextScreen == kScreenSetup ||
@@ -1796,10 +1670,8 @@ void EEMEngine::setupLeave() {
}
saveProfile(_playerName);
}
-
+// `_SetupSettings @ 2046:0008`
void EEMEngine::setupDrawScreenLondon() {
- // `_SetupSettings @ 2046:0008`: BG PIC 0x40 + _SwapColors highlights for the
- // 4 toggles (key 0xFE, 0x15 active / 0x00 dim), drawn ON-then-OFF.
Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
@@ -1808,16 +1680,16 @@ void EEMEngine::setupDrawScreenLondon() {
scratch.simpleBlitFrom(bg.surface);
const byte kKey = 0xFE, kBright = 0x15, kDim = 0x00;
- // Partner (DAT_3036_4bd4).
+
swapColors(scratch, kLonSetJake, kKey, _partner == kPartnerJake ? kBright : kDim);
swapColors(scratch, kLonSetJenny, kKey, _partner == kPartnerJenny ? kBright : kDim);
- // Voice (DAT_3036_4c4e).
+
swapColors(scratch, kLonSetVoiceOn, kKey, _voiceOn ? kBright : kDim);
swapColors(scratch, kLonSetVoiceOff, kKey, _voiceOn ? kDim : kBright);
- // Music (DAT_3036_4cc0).
+
swapColors(scratch, kLonSetMusicOn, kKey, _musicOn ? kBright : kDim);
swapColors(scratch, kLonSetMusicOff, kKey, _musicOn ? kDim : kBright);
- // Highlight boxes (DAT_3036_4c4a; hide_highlight_boxes is the inverse).
+
const bool hiOn = !ConfMan.getBool("hide_highlight_boxes");
swapColors(scratch, kLonSetHiOn, kKey, hiOn ? kBright : kDim);
swapColors(scratch, kLonSetHiOff, kKey, hiOn ? kDim : kBright);
@@ -1827,16 +1699,8 @@ void EEMEngine::setupDrawScreenLondon() {
g_system->updateScreen();
}
+// `_DoSetup @ 2046:067b`
void EEMEngine::doSetupLondon() {
- // `_DoSetup @ 2046:067b`. BG PIC 0x40; 13 click rects (`_SetupButtons @
- // 2bca:12ce`); `HandleSetupButton @ 2046:01d9` dispatch (verified from the
- // jumptable @ 2046:0661). EEM2 has FOUR toggles vs EEM1's two and a
- // rearranged left column:
- // [0]( 20, 44) Partner [1]( 20, 63) Voice [2]( 20,101) Highlight boxes
- // [3]( 20,127) Profile(8) [12](20, 82) Music [4](281, 43) ScrapBook +
- // [5](281, 62) ScrapBook- [6](281,108) Save [7](281,127) New case(0xa)
- // [8]( 53,153) Done [9](145,163) Quit [10](212,153) Help
- // [11]( 81, 25) Credits (PIC 0x208). _SavePlayerRecord on exit (setupLeave).
if (!_font.isLoaded()) {
_nextScreen = (ScreenId)_lastScreen;
return;
@@ -1856,7 +1720,7 @@ void EEMEngine::doSetupLondon() {
const Common::Rect kMusicBtn ( 20, 82, 38, 99); // [12]
setupDrawScreenLondon();
- _nextScreen = kScreenSetup; // sentinel â setupLeave picks the target
+ _nextScreen = kScreenSetup;
while (!shouldQuit()) {
Common::Event ev;
bool dirty = false;
@@ -1876,7 +1740,6 @@ void EEMEngine::doSetupLondon() {
continue;
const int mx = ev.mouse.x, my = ev.mouse.y;
- // --- toggles (stay on screen, re-render) ---
if (kPartnerBtn.contains(mx, my)) {
_partner = (_partner == kPartnerJake) ? kPartnerJenny
: kPartnerJake;
@@ -1904,7 +1767,6 @@ void EEMEngine::doSetupLondon() {
continue;
}
- // --- navigation (leave the screen) ---
if (kProfileBtn.contains(mx, my)) {
saveProfile(_playerName);
_nextScreen = kScreenProfile;
@@ -1924,19 +1786,16 @@ void EEMEngine::doSetupLondon() {
_nextScreen = kScreenInvalid;
return;
}
- dirty = true; // restore BG under the prompt
+ dirty = true;
continue;
}
- // --- actions that stay on screen ---
if (kSaveBtn.contains(mx, my)) {
- // `_SaveGame` is gated on a game being active ([0x1966]).
if (_mystery.isLoaded())
saveProfile(_playerName);
continue;
}
if (kHelpBtn.contains(mx, my)) {
- // `_InterfaceHelp(0)`. Reuse the EEM1 help-card mechanism.
static const uint16 kHelpPics[] = { 0x0192, 0x01B1 };
CursorMan.showMouse(false);
for (uint i = 0; i < ARRAYSIZE(kHelpPics); i++) {
@@ -1957,8 +1816,7 @@ void EEMEngine::doSetupLondon() {
dirty = true;
continue;
}
- // ScrapBook +/- â London has 2 books (stage 1 / 2). The DOS pages
- // next/prev (FUN_2046_0874); map to the two available books.
+
if (kScrapNext.contains(mx, my)) {
doShowScrapbook(1);
setSitePalette(0);
@@ -1978,11 +1836,8 @@ void EEMEngine::doSetupLondon() {
g_system->delayMillis(15);
}
}
-
+// `_ActionScreen @ 1c33:195b`
void EEMEngine::doActionScreen() {
- // `_ActionScreen @ 1c33:195b` â BG PIC 0x104 + PIC 9 @ (10, 0x87),
- // `_DoChoose` with ActionNames @ 29be:0d6a. 5 picks alternating with
- // separators.
enum MenuPick {
kPickChoose = 0,
kPickPractice,
@@ -2008,14 +1863,8 @@ void EEMEngine::doActionScreen() {
};
const char * const *kPickLabel = isSpanish() ? kPickLabelES : kPickLabelEN;
// London has no BOOK3.NME, so its action menu offers four entries
- // (Choose / Practice / ScrapBook 1 / ScrapBook 2). EEM1 adds ScrapBook 3.
const uint numPicks = isLondon() ? (uint)kPickScrap3 : (uint)kNumPicks;
- // Gating @ EEM1 1c33:19d1-1a70 by chain stage + per-tier solves:
- // stage 1: grey SB2/3; SB1 needs any tier-1 solve
- // stage 2: grey Practice + SB3; SB2 needs tier-2 solve
- // stage 3: grey Practice; SB3 needs tier-3 solve (EEM1 only)
- // stage 4: grey Choose + Practice
uint loT = 0, hiT = 0;
const bool anySolved1 = mysteryTierRange(1, loT, hiT) &&
anyMysterySolved(loT, hiT);
@@ -2035,7 +1884,7 @@ void EEMEngine::doActionScreen() {
const bool kPickEnabled[kNumPicks] = {
chooseOn, practiceOn, scrap1On, scrap2On, scrap3On
};
- // Seed selection on first enabled entry.
+
uint pick = 0;
for (uint i = 0; i < numPicks; i++) {
if (kPickEnabled[i]) {
@@ -2046,7 +1895,6 @@ void EEMEngine::doActionScreen() {
const char *kSeparator = "----------------------------------";
- // `_DoChoose @ 1c33:0514` click rects (4Ãu16 {x1,y1,x2,y2} in seg 29be):
const Common::Rect kOkRect ( 12, 63, 41, 87); // 29be:0cd8 confirm
const Common::Rect kHelpRect ( 12, 100, 41, 124); // 29be:0ce0 help
const Common::Rect kExitRect ( 12, 137, 41, 161); // 29be:0ce8 cancel
@@ -2054,7 +1902,6 @@ void EEMEngine::doActionScreen() {
CursorMan.showMouse(true);
- // Launcher-resume path can enter from `_AllBlack` palette.
setSitePalette(0);
Picture bg;
@@ -2087,7 +1934,6 @@ void EEMEngine::doActionScreen() {
return;
if (ev.type == Common::EVENT_LBUTTONDOWN) {
if (kOkRect.contains(ev.mouse.x, ev.mouse.y)) {
- // Greyed entries ignored (`_DoChoose @ 1c33:0635`).
if (kPickEnabled[pick])
confirmed = true;
break;
@@ -2098,7 +1944,6 @@ void EEMEngine::doActionScreen() {
break;
}
if (kHelpRect.contains(ev.mouse.x, ev.mouse.y)) {
- // `_ActionScreen` sets Chelp=0.
continue;
}
if (kListRect.contains(ev.mouse.x, ev.mouse.y)) {
@@ -2129,7 +1974,6 @@ void EEMEngine::doActionScreen() {
break;
}
if (k == Common::KEYCODE_UP || k == Common::KEYCODE_LEFT) {
- // `_DoChoose` arrow handlers @ 1c33:0514, bounded loop.
for (int i = 0; i < (int)numPicks; i++) {
pick = (pick == 0) ? (numPicks - 1) : pick - 1;
if (kPickEnabled[pick])
@@ -2179,7 +2023,6 @@ void EEMEngine::doActionScreen() {
return;
}
- // "Practice Mystery" is the tutorial â mystery 0.
if (pick == kPickPractice) {
if (!_mystery.load(0, &_rng)) {
warning("doActionScreen: failed to load practice mystery");
@@ -2194,8 +2037,6 @@ void EEMEngine::doActionScreen() {
}
if (pick == kPickScrap1 || pick == kPickScrap2 || pick == kPickScrap3) {
- // `_ActionScreen` handlers @ 1c33:1B13 / 1B26 / 1B40 â
- // `_ShowScrapbook(0, stage)`. Returns here afterwards.
const uint stage = (pick == kPickScrap1) ? 1
: (pick == kPickScrap2) ? 2 : 3;
doShowScrapbook(stage);
@@ -2207,11 +2048,9 @@ void EEMEngine::doActionScreen() {
_nextScreen = kScreenChooseMystery;
}
-
+// EEM1: `_DoChooseMystery @ 1a35:02b7` + `_CaseSelection @ 1c33:0a87`
+// EEM2: 1abf:022a + 1cd3:0a9d)
void EEMEngine::doCaseSelection() {
- // `_DoChooseMystery @ 1a35:02b7` + `_CaseSelection @ 1c33:0a87`
- // (EEM2 @ 1abf:022a / 1cd3:0a9d): load BOOK<stage>.NME, draw PIC 0x41
- // + centered "Book N" title, then read the chosen M*.BIN.
const uint kMaxMystery = isLondon() ? 50 : 54;
CursorMan.showMouse(true);
@@ -2225,7 +2064,6 @@ void EEMEngine::doCaseSelection() {
const bool haveRevealPic =
_picsArchive.getPicture(kCaseSelectionRevealPic, revealPic);
- // `_CaseSelection @ 1c33:0a87` greeter ANI 0x15 (Jake) / 0x16 (Jenny).
const uint kKdAniId = (_partner == kPartnerJake) ? 0x15 : 0x16;
Animation kdAnim;
const bool haveKdAnim = _aniArchive.loadAnimation(kKdAniId, kdAnim)
@@ -2233,12 +2071,6 @@ void EEMEngine::doCaseSelection() {
const int kKdAnimX = 0x112;
const int kKdAnimY = 0x50;
- // Tier â BOOK<n>.NME. EEM1 ships three books (Junior 1..24, Senior
- // 25..48, Master 49..54); EEM2/London ships two (1..25 / 26..50). London
- // has no BOOK3.NME, and its stage 3 is transient (`_DisplayCorrect @
- // 1ea1:0619` collapses it to 4), so should it ever be reached here it
- // reuses BOOK2.NME rather than opening a missing file. `mysteryTierRange`
- // is the single source of truth for the per-tier mystery ranges.
uint book;
switch (_chainStage) {
case 2: book = 2; break;
@@ -2257,7 +2089,6 @@ void EEMEngine::doCaseSelection() {
}
const uint listLen = MIN<uint>((uint)names.size(), stageHi - stageLo + 1);
- // Solved-row gating @ `_DoChoose @ 1c33:0521`.
Common::Array<bool> solvedFlags;
solvedFlags.resize(listLen);
for (uint i = 0; i < listLen; i++) {
@@ -2321,8 +2152,6 @@ void EEMEngine::doCaseSelection() {
return;
}
if (kChooserHelpRect.contains(ev.mouse.x, ev.mouse.y)) {
- // Original returns 0xfffe â `_ChooseSavedGame`. ScummVM
- // stores in-progress cases per profile.
saveProfile(_playerName);
_mystery.clear();
_nextScreen = kScreenProfile;
@@ -2452,39 +2281,38 @@ void EEMEngine::doCaseSelection() {
debugC(1, kDebugMystery, "Mystery %u loaded; %u sites, %u suspects",
mn, _mystery.numSites(), _mystery.numSuspects());
}
+// `_DoNotebook @ 161e:0500` + `_DrawNotes @ 161e:01d0` +
+// `_HandleNoteButton @ 161e:03cb`. _NotebookRect = (78, 12, 288, 152).
+// _NoteButtons @ 29be:0147 â 11 rects à 8 bytes. Jumptable @ 161e:04ec
+// dispatches handler[i-1] (rect 0's i-1 underflows = decorative slot):
+// [0] (134,174,155,190) decorative â no handler
+// [1] ( 93,174,115,190) HELP â `_InterfaceHelp(0)` (0x3f9)
+// [2] (157,174,178,190) GALLERY â `_NextScreen = 5` (0x477)
+// [3] ( 5, 80, 44,110) host hint â `_KDHelp` (0x403)
+// [4] (180,174,201,190) SOLVE â `_SolvedCheck` â NextScreen=7 (0x436)
+// [5] (204,174,224,190) PAGE NEXT â `_EraseNotes` + redraw (0x489)
+// [6] (226,174,247,190) PAGE PREV â CurrentPage-- + redraw (0x4ab)
+// [7] ( 7,177, 57,200) MAP â `_NextScreen = 2` (0x480)
+// [8] ( 35,111, 56,136) SITE â `_NextScreen = 3` (0x3ed)
+// [9] ( 0, 0, 0, 0) same exit as [8]
+// [10] (267,174,288,190) â `_InterfaceHelp(0)` again (0x3f9)
+//
+// EEM2/London (`_DoNotebook @ 16a0:0517`, `_HandleNoteButton @ 16a0:03dd`,
+// jumptable @ 16a0:0503) reuses the SAME button rect table (2bca:0151) and
+// handler set, but reassigns two slots and revives slot 9 (verified by
+// disassembly â `[0x9292]` is the next-screen code):
+// [1] ( 93,174,115,190) MAP â screen 2 (EEM1: a 2nd InterfaceHelp).
+// London's dedicated map button.
+// [7] ( 7,177, 57,200) DOS EEM2 â SITE, but we keep it as MAP (â 2) in
+// both variants â the EEM1 partner-foot map shortcut
+// (a player convenience alongside button [1]).
+// [9] ( 0, 0, 66, 79) SITE â screen 3 (EEM1: dead 0-rect)
+// Everything else (gallery, accuse, host hint, page next/prev, help [10],
+// site [8]) is identical. The note rendering, pagination, partner ANI
+// (same 1/0xb), and gizmo colour-cycle are shared as-is; only button [1]
+// (London â map) and the extra close area [9] are gated on `isLondon()`.
void EEMEngine::doNotebook() {
- // `_DoNotebook @ 161e:0500` + `_DrawNotes @ 161e:01d0` +
- // `_HandleNoteButton @ 161e:03cb`. _NotebookRect = (78, 12, 288, 152).
- // _NoteButtons @ 29be:0147 â 11 rects à 8 bytes. Jumptable @ 161e:04ec
- // dispatches handler[i-1] (rect 0's i-1 underflows = decorative slot):
- // [0] (134,174,155,190) decorative â no handler
- // [1] ( 93,174,115,190) HELP â `_InterfaceHelp(0)` (0x3f9)
- // [2] (157,174,178,190) GALLERY â `_NextScreen = 5` (0x477)
- // [3] ( 5, 80, 44,110) host hint â `_KDHelp` (0x403)
- // [4] (180,174,201,190) SOLVE â `_SolvedCheck` â NextScreen=7 (0x436)
- // [5] (204,174,224,190) PAGE NEXT â `_EraseNotes` + redraw (0x489)
- // [6] (226,174,247,190) PAGE PREV â CurrentPage-- + redraw (0x4ab)
- // [7] ( 7,177, 57,200) MAP â `_NextScreen = 2` (0x480)
- // [8] ( 35,111, 56,136) SITE â `_NextScreen = 3` (0x3ed)
- // [9] ( 0, 0, 0, 0) same exit as [8]
- // [10] (267,174,288,190) â `_InterfaceHelp(0)` again (0x3f9)
- // BG PIC 0x3f; partner ANI 1 (Jake) / 0xb (Jenny) at (5, 80).
- //
- // EEM2/London (`_DoNotebook @ 16a0:0517`, `_HandleNoteButton @ 16a0:03dd`,
- // jumptable @ 16a0:0503) reuses the SAME button rect table (2bca:0151) and
- // handler set, but reassigns two slots and revives slot 9 (verified by
- // disassembly â `[0x9292]` is the next-screen code):
- // [1] ( 93,174,115,190) MAP â screen 2 (EEM1: a 2nd InterfaceHelp).
- // London's dedicated map button.
- // [7] ( 7,177, 57,200) DOS EEM2 â SITE, but we keep it as MAP (â 2) in
- // both variants â the EEM1 partner-foot map shortcut
- // (a player convenience alongside button [1]).
- // [9] ( 0, 0, 66, 79) SITE â screen 3 (EEM1: dead 0-rect)
- // Everything else (gallery, accuse, host hint, page next/prev, help [10],
- // site [8]) is identical. The note rendering, pagination, partner ANI
- // (same 1/0xb), and gizmo colour-cycle are shared as-is; only button [1]
- // (London â map) and the extra close area [9] are gated on `isLondon()`.
if (!_mystery.isLoaded() || !_font.isLoaded())
return;
@@ -2534,7 +2362,6 @@ void EEMEngine::doNotebook() {
}
}
if (ev.type == Common::EVENT_LBUTTONDOWN) {
- // Earlier rects win on overlap (matches `_FindButton`).
if (kPdaSiteRect.contains(ev.mouse.x, ev.mouse.y) ||
(isLondon() &&
kPdaLondonCloseRect.contains(ev.mouse.x, ev.mouse.y))) {
@@ -2543,19 +2370,13 @@ void EEMEngine::doNotebook() {
break; // back to site
}
if (kPdaPartnerFootMapRect.contains(ev.mouse.x, ev.mouse.y)) {
- // EEM1 `_NoteButtons[7]` â map (screen 2). DOS EEM2 reassigns
- // this slot to SITE, but we intentionally keep the partner-
- // foot hotspot as a map shortcut in BOTH variants for parity
- // with EEM1. London also has its own dedicated map button
- // (`kPdaHelpRect` below) and can still close the PDA via the
- // site button (35,111) or the top-left corner (0,0,66,79).
_nextScreen = kScreenMapAlt;
exitFlag = true;
break;
}
if (kPdaPartnerHeadHintRect.contains(ev.mouse.x, ev.mouse.y)) {
setInteractiveMouseCursor(false);
- doHelp(); // _KDHelp = host hint
+ doHelp();
dirty = true;
continue;
}
@@ -2570,9 +2391,6 @@ void EEMEngine::doNotebook() {
break;
}
if (kPdaHelpRect.contains(ev.mouse.x, ev.mouse.y)) {
- // `_NoteButtons[1]`. EEM1: a 2nd `_InterfaceHelp(0)` button
- // (PICs 0x63 / 0x1ae). London reassigns this slot to MAP
- // (`_HandleNoteButton` slot 1 â screen 2).
if (isLondon()) {
_nextScreen = kScreenMapAlt;
exitFlag = true;
@@ -2595,31 +2413,23 @@ void EEMEngine::doNotebook() {
continue;
}
if (kPdaHelp2Rect.contains(ev.mouse.x, ev.mouse.y)) {
- // _NoteButtons[10] â handler 0x03f9 = same as [1].
setInteractiveMouseCursor(false);
doInterfaceHelp(0);
dirty = true;
continue;
}
- // The notebook screen is read-only in the original:
- // `_DoNotebook @ 161e:0500` only checks clicks against
- // `_NoteButtons` (11 rects). Clue toggling lives in the
- // accuse screen.
}
}
if (exitFlag)
break;
const uint32 now = g_system->getMillis();
- // Re-render every 100 ms for partner sprite cycle.
if (dirty || now - lastDraw >= 100) {
drawNotebookFrame(page);
lastDraw = now;
mouse = g_system->getEventManager()->getMousePos();
setInteractiveMouseCursor(notebookButtonAt(mouse.x, mouse.y));
}
- // _GizmoColorCycle @ 1c33:0002 â `_DoNotebook` rotates 0x6f..0x73 each
- // _CheckFrameRate tick (the PDA gizmo / LED indicator shimmer).
if (now - gizmoLastTick >= kChooserCycleMillis) {
gizmoLastTick = now;
cycleChooserPalette();
@@ -2647,9 +2457,6 @@ Common::String EEMEngine::notebookNoteText(uint clueId, const byte *ni,
return parseString(Common::String(p, len),
_playerName, _partner);
}
- // EEM1 CD: 4-byte entries (textOff at +0, points at +2). EEM2/London CD:
- // 2-byte entries (textOff only) â `_DrawNotes @ 16a0:01de` reads
- // `noteIndex[clueId*2]`.
const uint stride = isLondon() ? 2 : 4;
const uint16 textOff = READ_LE_UINT16(ni + clueId * stride);
return parseString(_mystery.textAt(textOff),
@@ -2657,8 +2464,6 @@ Common::String EEMEngine::notebookNoteText(uint clueId, const byte *ni,
}
void EEMEngine::drawNotebookFrame(int &page) {
- // `_DrawNotes @ 161e:01d0` per-page layout + partner sprite at (5,80)
- // from `_DoNotebook @ 161e:0500`.
const Common::Rect kNotebookRect(78, 12, 288, 152);
Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
@@ -2669,7 +2474,6 @@ void EEMEngine::drawNotebookFrame(int &page) {
if (_picsArchive.getPicture(0x3f, frame))
scratch.simpleBlitFrom(frame.surface);
- // Partner ANI 1/0xb (cells); script 0x01 (`_NewAnimation @ 161e:054c`).
blitPdaPartner(scratch, _aniArchive, _partner, kPdaNotebookPartner,
g_system->getMillis());
@@ -2678,8 +2482,7 @@ void EEMEngine::drawNotebookFrame(int &page) {
// EEM2/London NoteIndex entries are 2 bytes (no points field), so its real
// clue count is the section size / 2. `noteIndexCount()` assumes the EEM1
// 4-byte stride (and stays that way for the accuse-scoring path, which is a
- // separate London concern), so derive the London-correct count here â else
- // high-id notes get dropped.
+ // separate London concern)
const uint16 londonCount = isLondon()
? (uint16)(_mystery.noteSectionSize() / 2) : 0;
Common::Array<uint> found;
@@ -2699,14 +2502,10 @@ void EEMEngine::drawNotebookFrame(int &page) {
const int kRectW = kNotebookRect.width();
const int kRectH = kNotebookRect.height();
- // Walk forward to start clue of current page; fits as many as kRectH.
int clueCursor = 0;
Common::Array<int> pageStarts;
pageStarts.push_back(0);
- // Floppy NoteIndex entry (7 bytes): +0 clueTextOff (absolute, what
- // the notebook displays), +2 Jake spoken, +4 Jenny spoken, +6 score.
- // `_DrawNotes_Floppy / FUN_15e0_01e8` reads only byte +0.
- // CD entries are 4 bytes, offsets relative to TextBlock @ header[+0xc].
+
const bool floppyNb = isFloppy();
const byte *bufBase = _mystery.blobAt(0);
const uint32 mysSz = _mystery.dataSize();
@@ -2735,10 +2534,6 @@ void EEMEngine::drawNotebookFrame(int &page) {
page = 0;
}
- // `_DrawNotes @ 161e:01d0` is read-only on this screen â clue selection
- // is driven exclusively from the accuse screen. The 0x3C / 0x5C colour
- // choice still reflects `_NoteSelected[]` so picks made in accuse remain
- // visible when the player flips back here.
const int startClue = (page < (int)pageStarts.size())
? pageStarts[page] : 0;
const int endClue = (page + 1 < (int)pageStarts.size())
@@ -2764,8 +2559,6 @@ void EEMEngine::drawNotebookFrame(int &page) {
y += h + 7;
}
- // `_DrawNotes @ 161e:01d0` appends a terminator line at the bottom of
- // the last page once the clue list is exhausted â string @ 29be:01f4.
const bool isLastPage = (page + 1 >= (int)pageStarts.size());
if (isLastPage) {
const char *kEndMarker = isSpanish()
@@ -2780,11 +2573,6 @@ void EEMEngine::drawNotebookFrame(int &page) {
}
void EEMEngine::doGallery() {
- // `_DoGallery @ 158f:065b` + `_DrawGallery @ 158f:0046`. BG PIC 0x3f.
- // Partner ANI 2 (Jake) / 0x10 (Jenny) at (5, 0x50). 5 slot positions
- // @ 29be:0x116. Per suspect i: picId = *(u16*)(_GalleryData + i*0x46);
- // visible = _InGallery[_NewOrder[i]]; drawY = pos.y + (0x48 - pic.h)
- // (bottom-aligned to baseline). Click â `_SearchSuspects` â moreInfo.
if (!_mystery.isLoaded())
return;
@@ -2796,16 +2584,13 @@ void EEMEngine::doGallery() {
CursorMan.showMouse(true);
- // Pre-load PIC 0x3f for the MoreInfo backdrop blit further down.
- // (`drawGalleryFrame` reloads it on its own per-call too.)
Picture galBg;
const bool haveBg = _picsArchive.getPicture(0x3f, galBg);
const uint8 num = _mystery.numSuspects();
- // Cache slot rects for click hit-testing.
Common::Array<Common::Rect> slotRects;
- Common::Array<int> slotSuspect; // logical suspect index in [0, num)
+ Common::Array<int> slotSuspect;
slotRects.resize(num);
slotSuspect.resize(num);
for (uint i = 0; i < num; i++) {
@@ -2877,10 +2662,6 @@ void EEMEngine::doGallery() {
break;
}
if (kPdaHelpRect.contains(ev.mouse.x, ev.mouse.y)) {
- // EEM1 slot 1 â a 2nd `_InterfaceHelp(0)` (158f:0625).
- // EEM2 gallery `_HandleGalleryButton @ 160e:05c7` slot 1 â
- // MAP (`MOV [0x9292],2` @ 160e:05fe) â London's dedicated
- // map button, same as the notebook/accuse PDA bar.
if (isLondon()) {
_nextScreen = kScreenMapAlt;
exitFlag = true;
@@ -2897,7 +2678,6 @@ void EEMEngine::doGallery() {
lastDraw = 0;
continue;
}
- // `_SearchSuspects` â per-slot rect â suspect index.
bool clicked = false;
for (uint i = 0; i < slotRects.size(); i++) {
if (slotSuspect[i] < 0)
@@ -2934,7 +2714,6 @@ void EEMEngine::doGallery() {
gallerySlotAt(slotRects, slotSuspect,
mouse.x, mouse.y));
}
- // _GizmoColorCycle @ 1c33:0002 â `_DoGallery` rotates 0x6f..0x73 each tick.
if (now - gizmoLastTick >= kChooserCycleMillis) {
gizmoLastTick = now;
cycleChooserPalette();
@@ -2944,16 +2723,9 @@ void EEMEngine::doGallery() {
}
setInteractiveMouseCursor(false);
}
-
+// `MoreInfo @ 158f:0419`
bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
const Picture &galBg, bool haveBg) {
- // `MoreInfo @ 158f:0419` â suspect detail. PIC = entry+0,
- // _AddPicBackground at (0x94, 0xf), notes via _DrawGalleryNotes.
- // CD layout (0x46-byte stride): +0 picId, +8 clueCount(u16), +0xa
- // clue IDs(u16, 0xFFFF terminator, max 30).
- // Floppy (`_MoreInfo_Floppy = 154e:042b` â `FUN_15e0_01e8`):
- // variable stride. +0 picId, +2 alibi (0xFFFF = guilty), +4 count(u8),
- // +5 clue IDs(u8).
const bool floppyMI = isFloppy();
const byte *suspect = floppyMI
? _mystery.floppySuspectEntry(suspectIdx)
@@ -2967,8 +2739,6 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
setInteractiveMouseCursor(false);
- // _GalleryNoteRect = (78, 93, 288, 152) @ 29be:0100.
- // `_DrawGalleryNotes @ 158f:01f4`.
const int rx = 78, ry = 93;
const int rw = 288 - 78, rh = 152 - 93;
const int lineH = _font.getFontHeight();
@@ -2976,7 +2746,6 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
const byte *ni = _mystery.noteIndex();
const uint16 niCount = _mystery.noteIndexCount();
- // Pagination via `_NextClue` + `_PageBreaks[]` (case 6 @ 158f:03b8).
uint pageStart = 0;
Common::Array<uint> pageStack;
bool back = false;
@@ -2989,7 +2758,7 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
ms.clear();
if (haveBg)
ms.simpleBlitFrom(galBg.surface);
- // Partner sprite at (5, 0x50). Re-blitted per page.
+
blitPdaPartner(ms, _aniArchive, _partner, kPdaGalleryPartner,
g_system->getMillis());
Picture detail;
@@ -3097,10 +2866,6 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
}
}
- // `_HandleMoreButton @ 158f:027d`:
- // [0] NOTEBOOKâ4, [1] HELPâ_InterfaceHelp, [2] GALLERY (close),
- // [3] _KDHelp, [4] ACCUSEâ7, [5] PAGE_NEXT, [6] PAGE_PREV,
- // [7] MAPâ2, [8] SITE noop, [10] HELP alt.
bool advance = false;
bool prev = false;
bool redraw = false;
@@ -3144,8 +2909,6 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
break;
}
if (isLondon() && kPdaHelpRect.contains(mx, my)) {
- // EEM2 gallery slot 1 â MAP (dedicated map button);
- // only kPdaHelp2Rect (267,174) stays help in London.
_nextScreen = kScreenMapAlt;
exitGallery = true;
back = true;
@@ -3200,8 +2963,7 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
break;
}
}
- // _GizmoColorCycle @ 1c33:0002 â `MoreInfo` (158f:0480) rotates
- // 0x6f..0x73 each tick.
+
const uint32 now = g_system->getMillis();
if (now - gizmoLastTick >= kChooserCycleMillis) {
gizmoLastTick = now;
@@ -3226,10 +2988,6 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
Common::Array<Common::Rect> &slotRects,
Common::Array<int> &slotSuspect) {
- // Gallery redraw â formerly the `drawFrame` lambda inside `doGallery`.
- // Mirrors `_DrawGallery @ 158f:0046`: PIC 0x3f frame + partner sprite
- // at (5, 0x50) + suspect portraits in their `_NewOrder` slots. Slot
- // positions live in `kGallerySlots` in this file's anon namespace.
Picture galBg;
const bool haveBg = _picsArchive.getPicture(0x3f, galBg);
@@ -3240,21 +2998,9 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
if (haveBg)
scratch.simpleBlitFrom(galBg.surface);
- // Partner sprite frame @ (5, 0x50). `_DoGallery @ 158f:065b` registers
- // _NewAnimation(..., CONCAT22(2, ...), ...) â script key 0x02 regardless
- // of partner. Jake's 26-frame 0x02 script (brief wave + long hold +
- // second wave) drives BOTH partners' cells (Jenny's own 0x10 anim is
- // only 9 frames so it wouldn't cover the full cadence).
blitPdaPartner(scratch, _aniArchive, _partner, kPdaGalleryPartner,
g_system->getMillis());
- // Portraits â `_DrawGallery @ 158f:0046` (CD) /
- // `_DrawGallery_Floppy @ 154e:0045` (floppy) walks suspects 0..N-1
- // and only renders those flagged in `_InGallery[NewOrder[i]]`.
- // Layout differs by variant:
- // * CD: fixed 0x46-byte stride, slot positions at `kGallerySlots`.
- // * Floppy: variable-stride entries (5 + entry[4] bytes per
- // suspect), slot positions at `kFloppyGallerySlots` (`2608:0x16c`).
const bool floppy = isFloppy();
const GallerySlot * const slots =
floppy ? kFloppyGallerySlots : kGallerySlots;
@@ -3317,16 +3063,16 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
g_system->updateScreen();
}
+// `_DoBigMap @ 20fe:09e7` two stage:
+// Stage 1 (Overview): PIC 0x42 + site icons at MapData[+4/+6]
+// (`_DrawBigMapButtons @ 20fe:0877`). Click in BigMapWindow
+// returns scroll (mouseX*2 - 0x74, mouseY*2 - 0x55).
+// Stage 2 (Detail): PIC 0x43 frame + 0xe9Ã0xab BIGMAP.PIC viewport
+// at (2,2). Icons stamped at MapData[+8/+0xa] (`_StampButtons @
+// 20fe:0d2f`). Click icon = travel.
+// MapData entry (14 bytes): +0..3 ???, +4 BigMapX, +6 BigMapY,
+// +8 SmallMapX, +0xa SmallMapY, +0xc crime-flag.
void EEMEngine::doBigMap() {
- // `_DoBigMap @ 20fe:09e7` two stage:
- // Stage 1 (Overview): PIC 0x42 + site icons at MapData[+4/+6]
- // (`_DrawBigMapButtons @ 20fe:0877`). Click in BigMapWindow
- // returns scroll (mouseX*2 - 0x74, mouseY*2 - 0x55).
- // Stage 2 (Detail): PIC 0x43 frame + 0xe9Ã0xab BIGMAP.PIC viewport
- // at (2,2). Icons stamped at MapData[+8/+0xa] (`_StampButtons @
- // 20fe:0d2f`). Click icon = travel.
- // MapData entry (14 bytes): +0..3 ???, +4 BigMapX, +6 BigMapY,
- // +8 SmallMapX, +0xa SmallMapY, +0xc crime-flag.
if (!_mystery.isLoaded())
return;
@@ -3341,18 +3087,12 @@ void EEMEngine::doBigMap() {
while (!shouldQuit()) {
setInteractiveMouseCursor(false);
- // `_GetPalette(0x24)` @ EEM1 `_DoBigMap`; EEM2 `_DoBigMap`
- // @ 2237:0a04 uses `_GetPalette(0x3b)` (shifted UI palettes).
setSitePalette(isLondon() ? 0x3b : 0x24);
- // Stage 1: Overview. mapStartTick anchors partner timeline;
- // `_NewAnimation` seeds frame to 0xffff so unfold plays once then
- // loops the wait sequence.
const uint32 mapStartTick = g_system->getMillis();
drawBigMapOverview(0);
uint32 mapLastTick = mapStartTick;
- // Rects @ CD 29be:1596 / floppy 2608:13fe (1 px diff on Setup).
const Common::Rect kBigMapWindow(0, 0, 247, 192);
const Common::Rect kSetupBtnRect = isFloppy()
? Common::Rect(251, 3, 315, 42)
@@ -3374,12 +3114,11 @@ void EEMEngine::doBigMap() {
continue;
}
if (ev.type == Common::EVENT_LBUTTONDOWN) {
- // Setup â NextScreen=6 (`_DoBigMap @ 20fe:0c33`).
if (kSetupBtnRect.contains(ev.mouse.x, ev.mouse.y)) {
_nextScreen = kScreenSetup;
return;
}
- // BigMapWindow click â zoom (sx=x*2-0x74, sy=y*2-0x55).
+
if (kBigMapWindow.contains(ev.mouse.x, ev.mouse.y)) {
int sx = ev.mouse.x * 2;
int sy = ev.mouse.y * 2;
@@ -3395,7 +3134,6 @@ void EEMEngine::doBigMap() {
if (wantZoom)
break;
- // `_CheckFrameRate` cadence â 100 ms.
const uint32 now = g_system->getMillis();
if (now - mapLastTick >= 100) {
mapLastTick = now;
@@ -3415,7 +3153,6 @@ void EEMEngine::doBigMap() {
if (!wantZoom)
return;
- // Stage 2: Detail. PIC 0x43 + BIGMAP.PIC viewport.
Common::File f;
if (!f.open(Common::Path("BIGMAP.PIC"))) {
warning("doBigMap: BIGMAP.PIC missing for detail view");
@@ -3439,13 +3176,11 @@ void EEMEngine::doBigMap() {
int scrollX = MAX<int>(0, MIN<int>(mapW - kMapWinW, zoomX));
int scrollY = MAX<int>(0, MIN<int>(mapH - kMapWinH, zoomY));
- // Detail partner timeline (script 0x13 unfold, then wait seq).
const uint32 detailStartTick = g_system->getMillis();
drawBigMapDetail(scrollX, scrollY, mapPixels, mapW, mapH, 0);
uint32 detailLastTick = detailStartTick;
bool returnToOverview = false;
- // `SmallMapButtons[4]` @ 20fe:156c â return to overview.
const Common::Rect kBigMapReturnRect(252, 43, kScreenWidth, kScreenHeight);
const Common::Rect kArrowYUp(237, 2, 247, 11);
const Common::Rect kArrowYDown(237, 163, 247, 172);
@@ -3512,7 +3247,6 @@ void EEMEngine::doBigMap() {
kBigMapReturnRect.contains(ev.mouse.x, ev.mouse.y) ||
kDetailSetupBtn.contains(ev.mouse.x, ev.mouse.y));
if (kDetailSetupBtn.contains(ev.mouse.x, ev.mouse.y)) {
- // `_DoMapScreen @ 20fe:1560` â NextScreen=6.
_nextScreen = kScreenSetup;
setInteractiveMouseCursor(false);
return;
@@ -3572,8 +3306,6 @@ void EEMEngine::doBigMap() {
uint16 my;
uint16 buttonId;
if (fmap) {
- // Floppy: rect +0/+2; BUTTON.DBD id at +4
- // (`FUN_1fed_0c3e @ 1fed:0c3e`).
mx = READ_LE_UINT16(entry + 0x0);
my = READ_LE_UINT16(entry + 0x2);
buttonId = (uint16)entry[0x4];
@@ -3618,7 +3350,6 @@ void EEMEngine::doBigMap() {
if (returnToOverview)
break;
- // 100 ms partner cycle.
const uint32 now = g_system->getMillis();
if (now - detailLastTick >= 100) {
detailLastTick = now;
@@ -3685,12 +3416,6 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
const int lineH = _font.getFontHeight();
const int maxLines = MAX<int>(1, textRect.height() / lineH);
for (uint i = 0; i < wrapped.size() && (int)i < maxLines; i++) {
- // `_DoApproach @ 1717:029e`:
- // `_WordWrap(x, y, w, page, fontColor=1, dropColor=-1)`.
- // Font colour is palette index 1 (NOT 0x0F white), drawn
- // straight onto the restored video-frame background â the
- // DOS `vga_fvidvid` calls just restore the clean bg, they
- // don't paint a panel, so don't fill the rect here either.
_font.drawString(&scratch, wrapped[i], textRect.left,
textRect.top + (int)i * lineH,
textRect.width(), 1);
@@ -3776,9 +3501,6 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
else
setSitePalette(0x3b);
bool done = false;
- // `_DoApproach` idle loop (1717:02d4) shimmers palette 0xF3..0xF7 via
- // `_ColorCycle(0xf3, 0xf7)` each frame-rate tick; mirror it on the video
- // palette (the diff-animation ramp those entries belong to).
uint32 lastShimmer = g_system->getMillis();
while (!shouldQuit() && !done) {
Common::Event ev;
@@ -3866,9 +3588,6 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
}
void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
- // PIC 0x42 + per-site Done/Crime/Site marker (`_DrawBigMapButtons @
- // 20fe:0877`) + partner idle at (0xfd, 0x50). Idle ANI: Jake=0x14,
- // Jenny=0x12. elapsedMs anchors unfoldâwait timeline.
Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
@@ -3880,7 +3599,6 @@ void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
// Marker PICs from `_main`:
// EEM1 CD @ 1a35:0f59: Done=0x20d, Site=0xc5, Crime=0xc6.
// EEM2 CD @ 1abf:11a6: Done=0x006, Site=0xc5, Crime=0xc6.
- // In EEM2, 0x20d is a normal scene/character frame, not a map marker.
Picture done;
Picture normal;
Picture crimeM;
@@ -3894,13 +3612,7 @@ void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
const byte *entry = _mystery.mapEntry(i);
if (!entry)
continue;
- // CD entries are 14 bytes: X at +4, Y at +6, crime at +12.
- // Floppy entries are 11 bytes: X at +6, Y at +8, recolor at +10.
- // Floppy layout: `FUN_1fed_07ed` (BigMap iteration):
- // `*(int *)(pcVar2 + i*0xb + 7)` (= entry+6, X u16)
- // `*(int *)(pcVar2 + i*0xb + 9)` (= entry+8, Y u16)
- // `pcVar2[i*0xb + 0xb]` (= entry+10, recolor flag â non-zero
- // selects the crime-marker PIC over the regular site marker).
+
const bool floppy = _mystery.isLoaded() && isFloppy();
const uint16 mx = floppy ? READ_LE_UINT16(entry + 0x6)
: READ_LE_UINT16(entry + 0x4);
@@ -3933,14 +3645,12 @@ void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
}
}
- // Partner idle at (0xfd, 0x50). `_DoBigMap @ 20fe:0a47` always passes
- // script 0x14 (count-up) to `_NewAnimation` regardless of partner.
const uint kMapAniId = (_partner == kPartnerJake) ? 0x14 : 0x12;
Animation mapAnim;
if (_aniArchive.loadAnimation(kMapAniId, mapAnim) && !mapAnim.empty()) {
const uint frameIdx = bigMapPartnerFrameAtTick((uint)mapAnim.size(),
elapsedMs);
- // BigMap walk-cycle miscflags = -2 (anchor shift left).
+
blitAnimFrameAnchored(scratch.surfacePtr(), mapAnim[frameIdx],
0xfd, 0x50);
}
@@ -3954,9 +3664,7 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
const Common::Array<byte> &mapPixels,
uint16 mapW, uint16 mapH,
uint32 elapsedMs) {
- // PIC 0x43 + 0xe9Ã0xab BIGMAP.PIC viewport at (2,2), stamped buttons,
- // partner at (0x101, 0x50). `_DoMapScreen @ 20fe:120b` (ANI 0x13 Jake
- // / 0x11 Jenny, seq 0x13). elapsedMs anchors unfoldâwait.
+
const int kMapWinW = 0xe9;
const int kMapWinH = 0xab;
const int kMapWinX = 2;
@@ -3975,8 +3683,6 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
scratch.copyRectToSurface(mapPixels.data() + scrollY * mapW + scrollX,
mapW, kMapWinX, kMapWinY, copyW, copyH);
- // `_StampButtons @ 20fe:0d2f` (CD): button=MapData[+0], dst=+8/+0xa.
- // Floppy `FUN_1fed_0c3e @ 1fed:0c3e`: button.dbd id at +4, rect +0/+2.
const bool floppyMap = _mystery.isLoaded() && isFloppy();
for (uint i = 0; i < _mystery.numSites(); i++) {
if (!_mystery._onSites[i] && i != _mystery._siteNumber)
@@ -4015,7 +3721,6 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
}
}
- // Always script 0x13 (`_NewAnimation @ _DoBigMap 20fe:0a47`).
const uint kDetailAniId = (_partner == kPartnerJake) ? 0x13 : 0x11;
Animation detailAnim;
if (_aniArchive.loadAnimation(kDetailAniId, detailAnim) &&
@@ -4031,14 +3736,8 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
g_system->updateScreen();
}
+// `_GetKDTextBalloon @ 1df2:0105`
uint16 EEMEngine::getKDTextBalloon(byte firstChar) const {
- // `_GetKDTextBalloon @ 1df2:0105`:
- // if ((ctype[firstChar] & 2) == 0) bub = *(u16*)29be:1068 = 0x17
- // else bub = *(u16*)(29be:0fe6+0x1e+c*2)
- // `ctype` is Borland's `_ctype_` at 29be:2be1; bit 1 (0x02) is set
- // only for '0'..'9'. Digit table @ 29be:1064 (see kDigitBalloons).
- // `*(u16*)29be:1068` = entry for '2' = 0x17 â original reuses the
- // digit-2 slot as the non-digit fallback constant.
if (firstChar < '0' || firstChar > '9')
return 0x17;
return kDigitBalloons[firstChar - '0'];
@@ -4095,12 +3794,9 @@ void EEMEngine::accuseDrawScreen(const AccuseNotesCtx &ctx) {
if (ctx.haveBg)
scratch.simpleBlitFrom(ctx.accuseBg->surface);
- // Partner at (5, 0x50). `_DoAccuse @ 1df2:0c2c`: ANI 2/0x10,
- // script 2, prior 1.
blitPdaPartner(scratch, _aniArchive, _partner, kPdaGalleryPartner,
g_system->getMillis());
- // Selected=0x3c, unselected=1 (`_NoteUnselectedColor` @ 1df2:0c25).
Common::Array<Common::Rect> &slotRects = *ctx.slotRects;
Common::Array<uint> &slotClues = *ctx.slotClues;
const Common::Array<uint> &found = *ctx.found;
@@ -4139,7 +3835,6 @@ void EEMEngine::accuseDrawScreen(const AccuseNotesCtx &ctx) {
y += h + 7;
}
- // `_UpdateSelectionCount(remaining)` @ (0xd1, 0xb).
const uint remaining = (selectedCount < ctx.expected)
? ctx.expected - selectedCount
: 0;
@@ -4163,12 +3858,6 @@ void EEMEngine::accuseDrawScreen(const AccuseNotesCtx &ctx) {
}
bool EEMEngine::doAccuseNotes() {
- // `_DoAccuse @ 1df2:0bdd` (EEM2 `@ 1ea1:0c03`) head. BG PIC 0x1A7.
- // `_AccuseNoteRect @ 29be:1048` = (79, 27, 304, 159). Counter @ (209, 11)
- // shows the required clue count (EEM1 `6 - chainStage` = 5/4/3 by tier;
- // London a flat 5 â see `expected` below). Unselected color 1 (red),
- // selected 0x3c. Click toggles selection; `_NoteButtons[4]`
- // (180,174,201,190) SOLVE â `_HandleAccuseNoteButton` returns 2.
if (!_mystery.isLoaded() || !_font.isLoaded())
return false;
const byte *ni = _mystery.noteIndex();
@@ -4179,18 +3868,13 @@ bool EEMEngine::doAccuseNotes() {
Picture accuseBg;
const bool haveBg = _picsArchive.getPicture(0x1a7, accuseBg);
- // `FUN_1d40_0e07 @ 1d40:0e34` zeroes _NoteSelected_Floppy on entry.
memset(_mystery._noteSelected, 0, sizeof(_mystery._noteSelected));
- // EEM1 `_DoAccuse @ 1df2:0bdd` scales the required clue count by tier
- // (`6 - chainStage`: stage 1=5, 2=4, 3=3). London `_DoAccuse @ 1ea1:0c03`
- // hardcodes 5 (`local_a = 5`) for both books.
const uint expected = isLondon()
? 5
: ((_chainStage >= 1 && _chainStage <= 3) ? (uint)(6 - _chainStage)
: 5);
- // `_DrawNotes(NULL, 100, ...)` walks `_CluesFound[]`.
Common::Array<uint> found;
for (uint i = 0; i < niCount && i < Mystery::kCluesFoundCap; i++) {
if (_mystery._cluesFound[i] && _mystery.noteHasNotebookText(i))
@@ -4202,8 +3886,6 @@ bool EEMEngine::doAccuseNotes() {
const int rectW = 304 - 79;
const int rectH = 159 - 27;
- // `_NoteButtons` @ 29be:0147. `_HandleAccuseNoteButton @ 1df2:0990`
- // returns DI=2 for i==4 (SOLVE).
const Common::Rect kBtnSolve (180, 174, 201, 190); // [4]
const Common::Rect kBtnPageNext(204, 174, 224, 190); // [5]
const Common::Rect kBtnPagePrev(226, 174, 247, 190); // [6]
@@ -4218,9 +3900,6 @@ bool EEMEngine::doAccuseNotes() {
int numPages = 1;
pageBreaks[0] = 0;
- // CD note: 4 bytes (u16 textOff rel TextBlock, u16 score).
- // Floppy: 7 bytes (+0 abs textOff, +2 Jake, +4 Jenny, +6 score).
- // Notebook always uses +0 (`FUN_15e0_01e8`).
const bool floppyNote = isFloppy();
const byte *bufBaseNotes = _mystery.blobAt(0);
@@ -4302,9 +3981,6 @@ bool EEMEngine::doAccuseNotes() {
return false;
}
if (isLondon() && kPdaHelpRect.contains(mx, my)) {
- // EEM2 accuse `_HandleAccuseNoteButton @ 1ea1:0873` slot 1
- // â MAP (`MOV [0x9292],2` @ 1ea1:089b) â London's dedicated
- // map button; only kPdaHelp2Rect (267,174) stays help.
_nextScreen = kScreenMapAlt;
return false;
}
@@ -4315,7 +3991,6 @@ bool EEMEngine::doAccuseNotes() {
dirty = true;
continue;
}
- // `_NoteButtons[5]/[6]` â page nav guards @ 1df2:09a8/099e.
if (kBtnPageNext.contains(mx, my)) {
if (page + 1 < numPages) {
page++;
@@ -4330,7 +4005,6 @@ bool EEMEngine::doAccuseNotes() {
}
continue;
}
- // `_NoteButtons[3]` â ScummVM hint, original ignores.
if (kBtnPartner.contains(mx, my)) {
doHelp();
dirty = true;
@@ -4343,7 +4017,6 @@ bool EEMEngine::doAccuseNotes() {
selected++;
}
if (selected == expected) {
- // `_DoAccuse` gate `uStack_8 == uStack_a`.
return true;
}
continue;
@@ -4371,7 +4044,6 @@ bool EEMEngine::doAccuseNotes() {
}
if (dirty)
accuseDrawScreen(ctx);
- // 100 ms `_CheckFrameRate` cadence @ 1df2:0bfa.
static uint32 sLastTick = 0;
const uint32 now = g_system->getMillis();
if (now - sLastTick >= 100) {
@@ -4388,17 +4060,11 @@ void EEMEngine::doAccuse() {
if (!_mystery.isLoaded() || !_font.isLoaded())
return;
- // Floppy: variable-stride entries (5 + nameLen, not 0x46), top-5
- // `_TextSeen` scoring (not `_NoteSelected`), header[+0x12] win-dialog.
- // Handled by doAccuseFloppy (`FUN_1d40_11fd`, `FUN_1d40_0c79`,
- // `_DisplayCorrect_Floppy`, `_DisplayAlibi_Floppy`).
if (isFloppy()) {
doAccuseFloppy();
return;
}
- // `_AccuseEntry @ 1df2:0ff8` â gates picker on top-5 found clue
- // score (â¥0x65 required).
const byte *entryKdIdx = _mystery.kdTextIndex();
if (!entryKdIdx)
return;
@@ -4567,8 +4233,6 @@ void EEMEngine::doAccuse() {
}
}
- // Balloon overlay (`_GetKDTextBalloon` + `_GetBalloon` +
- // `_AddPicBackground` + `_WordWrap` @ 1df2:0c8d-0cd1).
Graphics::ManagedSurface ms(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
ms.clear();
@@ -4580,7 +4244,7 @@ void EEMEngine::doAccuse() {
const byte firstChar =
hint.empty() ? (byte)0 : (byte)hint[0];
uint16 bubNum = getKDTextBalloon(firstChar);
- // Strip digit prefix (`_DisplayAlibi @ 1df2:0163` `str=pbVar7+1`).
+
if (firstChar >= '0' && firstChar <= '9')
hint.deleteChar(0);
bubNum = fitBalloonToText(bubNum, hint);
@@ -4609,12 +4273,11 @@ void EEMEngine::doAccuse() {
0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
- // `_SayKDDigital(3)` @ 1df2:0cd9.
+
if (_audio && kdIdx)
_audio->sayKDDigital(kdIdx, 3, _partner);
waitForInput(20000);
- // `_DoAccuse @ 1df2:0ce5` â NextScreen = LastScreen.
_nextScreen = _lastScreen != kScreenInvalid
? (ScreenId)_lastScreen : kScreenSite;
return;
@@ -4632,9 +4295,6 @@ void EEMEngine::doAccuse() {
int highlighted = 0;
- // KD hint balloon @ `_DoAccuseGallery @ 1df2:0a31` (0a4c-0afe).
- // y = (h < 0x4e) ? (0x50-h)>>1 : 1. Inset table @ 29be:0875.
- // `_SayKDDigital(4)`.
const byte *kdIdx = _mystery.kdTextIndex();
if (kdIdx) {
const int16 textOff = (int16)READ_LE_UINT16(kdIdx + 8);
@@ -4677,14 +4337,12 @@ void EEMEngine::doAccuse() {
ms.simpleBlitFrom(accuseBg.surface);
}
}
- // `_Rect_Move_Mask` (1000:03fc) â transp = pic[0]>>8.
if (haveBalloon) {
const byte transp = (byte)(balloon.flags >> 8);
ms.transBlitFrom(balloon.surface,
Common::Point(balloonX, balloonY),
transp);
}
- // Inset table @ 29be:0875.
uint16 tx = 5;
uint16 ty = 4;
uint16 tw = 155;
@@ -4710,20 +4368,6 @@ void EEMEngine::doAccuse() {
drawAccuseGallery(num, gd, highlighted, slotRects, slotSuspect);
- // Wait-for-pick loop. Mirrors `_DoAccuseGallery` 1df2:0b26-1df2:0bc8:
- // * `_CheckFrameRate` + `_UpdateAnimations` per tick (1df2:0b2a-0b33)
- // * 5-entry input dispatch table @ 1df2:0bc9:
- // 0x09 (TAB) â handler 0x0b94 (cycle highlight)
- // 0x0d (Enter) â handler 0x0b72 (pick = _SearchSuspects)
- // 0x4b (LEFT) â handler 0x0b94
- // 0x4d (RIGHT) â handler 0x0b94
- // 0xFFFF (mb) â handler 0x0b72
- // * 0x0b94: `INC DI` + wraparound + `_PutMouseInRect(&Guys[DI])`,
- // i.e. advance highlight and warp cursor (1df2:0b94-0bb1).
- // * 0x0b72: `_SearchSuspects` (158f:0584) â mouse-rect hit-test;
- // if non-0xFFFF, pick that suspect.
- // We don't warp the cursor (unfriendly under SDL); instead the
- // highlight is drawn as a 1px outline and Enter picks it.
int picked = -1;
uint32 lastTick = g_system->getMillis();
bool dirty = false;
@@ -4745,9 +4389,6 @@ void EEMEngine::doAccuse() {
dirty = true;
break;
case Common::KEYCODE_LEFT:
- // 1df2:0b94 increments DI for LEFT too â but a
- // keyboard-driven UX is friendlier with separate
- // directions, so we mirror Right=+1 / Left=-1.
highlighted = nextLiveSlot(slotRects, highlighted, -1);
dirty = true;
break;
@@ -4782,7 +4423,6 @@ void EEMEngine::doAccuse() {
}
}
}
- // 100 ms `_CheckFrameRate` @ 1df2:0b33.
const uint32 now = g_system->getMillis();
if (dirty || now - lastTick >= 100) {
drawAccuseGallery(num, gd, highlighted, slotRects, slotSuspect);
@@ -4795,8 +4435,6 @@ void EEMEngine::doAccuse() {
if (picked < 0)
return;
- // `_WITCH @ 1df2:089f` â guilty when gd[picked*0x46+0x02] == 0xFFFF;
- // innocents store an alibi TextBlock offset there.
const int points = _mystery.selectedPoints();
const bool pickedGuilty = _mystery.isGuilty((uint)picked);
const bool guessedRight = pickedGuilty;
@@ -4815,7 +4453,6 @@ void EEMEngine::doAccuse() {
// 4. Partner reaction balloon @ KDTextIndex[+10], `_SayKDDigital(5)`.
// 5. _FirstTry = 0; NextScreen = LastScreen (1df2:043f).
if (!guessedRight) {
- // Balloon-shape table @ 29be:1050.
static const uint16 kAlibiBubbles[16] = {
0x002B, 0x002C, 0x002D, 0x002E,
0x00AB, 0x00AC, 0x00AD, 0x00AE,
@@ -4830,7 +4467,6 @@ void EEMEngine::doAccuse() {
if (raw)
alibi = parseString(raw, _playerName, _partner);
}
- // `_DisplayAlibi @ 1df2:0163` â bindx = digit prefix, else 2.
uint bindx = 2;
const byte firstChar = alibi.empty() ? (byte)0 : (byte)alibi[0];
if (firstChar >= '0' && firstChar <= '9') {
@@ -4855,7 +4491,6 @@ void EEMEngine::doAccuse() {
_balloonArchive.size() > (bubNum & 0x7F) &&
_balloonArchive.loadEntry(bubNum & 0x7F, balloon);
- // Position math @ 1df2:01a4-0207.
int balloonX = 0x21;
int balloonY = 1;
int py = 0x5a;
@@ -4875,7 +4510,6 @@ void EEMEngine::doAccuse() {
balloonY = (bh < 0x4f) ? (0x50 - bh) / 2 : 1;
}
- // `base` = BG + suspect + partner. Survives both balloon phases.
Graphics::ManagedSurface base(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
base.clear();
@@ -4887,10 +4521,7 @@ void EEMEngine::doAccuse() {
Common::Point(0x82, py),
(uint32)transp);
}
- // Partner at (5, 0x50). ANI 2/0x10, script 0x02 (`_DoAccuse
- // @ 1df2:0c30`). Drawn after suspect.
- // scratch = base + alibi balloon/text + partner.
Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
Graphics::PixelFormat::createFormatCLUT8());
scratch.simpleBlitFrom(base);
@@ -4900,7 +4531,7 @@ void EEMEngine::doAccuse() {
Common::Point(balloonX, balloonY),
(uint32)transp);
}
- // Inset table @ 29be:0875. Color 0 inside balloon (1df2:0240).
+
uint16 tx = 5, ty = 4, tw = 155;
getBalloonInsets(bubNum, tx, ty, tw);
if (_font.isLoaded() && !alibi.empty()) {
@@ -4911,7 +4542,6 @@ void EEMEngine::doAccuse() {
blitPdaPartner(scratch, _aniArchive, _partner, kPdaGalleryPartner,
g_system->getMillis());
- // MIDI 6 â blocks until done (or click/ESC aborts).
if (_music && _voiceOn) {
_music->playMus(6, /* loop= */ false);
const uint32 musStart = g_system->getMillis();
@@ -4930,7 +4560,6 @@ void EEMEngine::doAccuse() {
break;
}
}
- // Hard cap if MIDI never reports finish.
if (g_system->getMillis() - musStart > 10000)
break;
g_system->updateScreen();
@@ -4939,8 +4568,6 @@ void EEMEngine::doAccuse() {
_music->stop();
}
- // Suspect voice. talk = partner==0 ? gd[+0x6] : gd[+0x0] (1df2:0252).
- // 1-based, so SpoolSound(talk-1).
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
@@ -4956,8 +4583,6 @@ void EEMEngine::doAccuse() {
}
waitForInput(60000);
- // Partner reaction @ 1df2:026e-02b6. Rebuild from `base` so alibi
- // balloon clears. `_SayKDDigital(5)` auto-cancels alibi voice.
const byte *reactIdx = _mystery.kdTextIndex();
if (reactIdx) {
const int16 reactOff = (int16)READ_LE_UINT16(reactIdx + 10);
@@ -5008,35 +4633,25 @@ void EEMEngine::doAccuse() {
waitForInput(60000);
_mystery._firstTry = false;
- // `_DisplayAlibi @ 1df2:043f` â NextScreen = LastScreen.
_nextScreen = _lastScreen != kScreenInvalid
? (ScreenId)_lastScreen : kScreenSite;
return;
}
- // Win â `_DisplayCorrect @ 1df2:073c`: mark solved, advance chain,
- // MIDI 5, SCRAPBK.ANI, ending, save, NextScreen=0xc (1df2:0895).
{
const uint mn = _mystery.number();
if (mn < sizeof(_mysteriesSolved)) {
_mysteriesSolved[mn] = _mystery._firstTry ? 2 : 1;
}
- // Chain advance @ 1df2:0824-0850. Skip mystery 0 (practice).
- // stage 1: 1..0x18, stage 2: 0x19..0x30, stage 3: 0x31..0x36.
advanceChainStageAfterSolve(mn);
- // `_DisplayCorrect @ 1df2:073c`:
- // _AllBlack; _BuildBackground(5, 0x42, 0x14); _FadeIn;
- // _MIDIPlay(5); _DisplayClue(MysteryIndex[+0x10]).
- // `_BuildBackground @ 172b:13e2` = PIC 0x3D + SITES entry 5 at
- // (0x42, 0x14), palette = sitenum+1 = 6.
Graphics::Surface *blk = g_system->lockScreen();
if (blk) {
memset(blk->getPixels(), 0, kScreenWidth * kScreenHeight);
g_system->unlockScreen();
}
- setSitePalette(6); // sitenum + 1 (`_GetPalette`).
+ setSitePalette(6);
Picture frame, scene;
if (_picsArchive.loadEntry(0x3d, frame)) {
g_system->copyRectToScreen(frame.surface.getPixels(),
@@ -5054,7 +4669,6 @@ void EEMEngine::doAccuse() {
sw, sh);
}
- // Stamp partner at (5, 0x50); displayClue snapshots screen.
if (Graphics::Surface *screen = g_system->lockScreen()) {
blitPdaPartner(screen, _aniArchive, _partner,
kPdaGalleryPartner, g_system->getMillis());
@@ -5065,39 +4679,29 @@ void EEMEngine::doAccuse() {
if (_music && _voiceOn)
_music->playMus(5, /* loop= */ false);
- // Chain recap â partner enumerates required clues.
const byte *solved = _mystery.solvedClueBlock();
if (solved)
displayClue(solved);
if (_music && _voiceOn)
_music->stop();
- // `_DifferenceAnimation("scrapbk.ani")` @ 1df2:0848.
playAnm(Common::Path("SCRAPBK.ANI"), 120,
/* holdLastFrame= */ false);
displayScrapbookExtra(mn);
- // `_ShowOneScrap @ 1f78:0773` = `_DisplayEnding(num, 1)`.
doShowEnding(mn);
- // `_SavePlayerRecord` @ 1df2:0857. Order: clear() before save
- // so hasMystery=false (matches `_DeleteSavedGame` @ 1df2:0851).
_mystery.clear();
const Common::Error err = saveProfile(_playerName);
if (err.getCode() != Common::kNoError)
warning("saveProfile after solve failed: %s",
err.getDesc().c_str());
- // `_DisplayCorrect @ 1df2:0895` â NextScreen = 0xc.
_nextScreen = kScreenAction;
}
}
-// Render one KDTextIndex string as a centred KD balloon over the current
-// screen, mirroring `_DisplayHint_Floppy @ 1503:00ca` (= `FUN_1d40_11fd`'s
-// body). `kdSlot` is the index into KDTextIndex (0..N) â entries are u16
-// absolute text offsets.
void EEMEngine::floppyKDHint(uint kdSlot, const byte *kdIdx,
const byte *bufBase, uint32 mysSize) {
if ((uint)(kdSlot * 2) + 2 > (uint)(mysSize - (kdIdx - bufBase)))
@@ -5112,8 +4716,6 @@ void EEMEngine::floppyKDHint(uint kdSlot, const byte *kdIdx,
if (lineLen == 0)
return;
Common::String raw(p, lineLen);
- // Digit â balloon variant (`_GetKDTextBalloon_Floppy @ 1d40:009f`
- // + table @ 2608:0c14).
static const uint8 kDigitToBalloon[10] = {
0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1c, 0x1d, 0x1e, 0x0a
};
@@ -5153,7 +4755,7 @@ void EEMEngine::floppyKDHint(uint kdSlot, const byte *kdIdx,
MAX<int>(8, (int)bw), text, 0);
g_system->copyRectToScreen(ms.getPixels(), ms.pitch, 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
- // Wait for click.
+
while (!shouldQuit()) {
Common::Event ev;
bool advance = false;
@@ -5347,7 +4949,6 @@ void EEMEngine::accuseDrawGallery(int highlighted,
if (!_picsArchive.getPicture(picId, portrait))
continue;
const GallerySlot &s = kFloppyGallerySlots[phys];
- // Bottom-align to baseline 0x48 (`154e:00ed`).
const int placeX = s.x;
const int placeY = s.y + (0x48 - portrait.surface.h);
const byte transp = (byte)(portrait.flags >> 8);
@@ -5369,20 +4970,12 @@ void EEMEngine::accuseDrawGallery(int highlighted,
}
void EEMEngine::doAccuseFloppy() {
- // Floppy accuse:
- // `_KDHelp_Floppy / FUN_1d40_11fd @ 1d40:11fd` â score gate.
- // `FUN_1d40_0e07` â clue selection (skipped; selectedPoints()
- // already takes top-5).
- // `FUN_1d40_0c79 @ 1d40:0c79` â gallery picker.
- // `_DisplayCorrect_Floppy @ 1d40:0894`.
- // `_DisplayAlibi_Floppy @ 1d40:00df`.
const byte *kdIdx = _mystery.kdTextIndex();
const byte *bufBase = _mystery.blobAt(0);
const uint32 mysSize = _mystery.dataSize();
if (!kdIdx || !bufBase)
return;
- // `FUN_1d40_11fd` â score gate, ready when score >= 100.
const int score = _mystery.selectedPoints();
uint kdSlot;
bool readyToSolve;
@@ -5403,9 +4996,6 @@ void EEMEngine::doAccuseFloppy() {
return;
}
- // Clue selection â `FUN_1d40_0e07 @ 1d40:0e07`. Pick `6 - chainStage`
- // clues (1d40:0e34: `local_c = 6 - DAT_28da_3052`); London a flat 5.
- // Count handled inside `doAccuseNotes`.
if (!doAccuseNotes()) {
if (_nextScreen == kScreenAccuse) {
_nextScreen = _lastScreen != kScreenInvalid
@@ -5414,8 +5004,6 @@ void EEMEngine::doAccuseFloppy() {
return;
}
- // 2nd score gate on USER-PICKED clues (1d40:0f3a, FUN_1d40_0c48 =
- // sum of byte +6 across selected entries). <100 â slot 3 hint.
int userSelectedScore = 0;
{
const byte *ni2 = _mystery.noteIndex();
@@ -5436,10 +5024,7 @@ void EEMEngine::doAccuseFloppy() {
return;
}
- // `FUN_1d40_0c79` â gallery picker. KDTextIndex slot 4 prompt.
floppyKDHint(4, kdIdx, bufBase, mysSize);
-
- // Floppy gallery: byte0 = numSuspects, then variable-stride entries.
const uint8 num = _mystery.numSuspects();
if (num == 0) {
_nextScreen = _lastScreen != kScreenInvalid
@@ -5513,16 +5098,8 @@ void EEMEngine::doAccuseFloppy() {
const bool guilty = _mystery.isGuilty((uint)picked);
if (guilty) {
- // `_DisplayCorrect_Floppy @ 1d40:0894`:
- // _BuildBackground_Floppy(5, 0x42, 0x14); _FadeIn;
- // _MIDIPlayFile("travel-2.xmi");
- // Walk solved chain, SCRAPBK.ANI when 3 records left.
- // Mark solved, tier-promote, SavePlayerRecord,
- // ShowOneScrap, NextScreen = 0xc.
const uint mn = _mystery.number();
- // `_BuildBackground_Floppy @ 16e2:12fd` â PIC 0x3d + SITES entry 5
- // at (0x42, 0x14), palette 6.
{
Graphics::Surface *blk = g_system->lockScreen();
if (blk) {
@@ -5547,8 +5124,6 @@ void EEMEngine::doAccuseFloppy() {
scene.surface.pitch, sx, sy, sw, sh);
}
- // Stamp partner at (5, 0x50); displayFloppyDialogRecords
- // snapshots screen. ANI 2/0x10, script 0x02.
if (Graphics::Surface *screen = g_system->lockScreen()) {
blitPdaPartner(screen, _aniArchive, _partner,
kPdaGalleryPartner, g_system->getMillis());
@@ -5557,12 +5132,9 @@ void EEMEngine::doAccuseFloppy() {
g_system->updateScreen();
}
- // TRAVEL-2.XMI @ 2608:0c84 (`_MIDIPlayFile` @ 1d40:08c0).
if (_music && _voiceOn)
_music->playFile(Common::Path("travel-2.xmi"), false);
- // Walk solved chain (header[+0x12]: count byte + dialog records).
- // SCRAPBK.ANI fires when 3 records remain (`_PlayTitleANM_Floppy(0)`).
const byte *chain = _mystery.solvedClueBlock();
if (chain) {
const uint count = chain[0];
@@ -5589,15 +5161,11 @@ void EEMEngine::doAccuseFloppy() {
if (_music && _voiceOn)
_music->stop();
- // 1d40:0939 â solved[mn] = _firstTry ? 2 : 1.
if (mn < sizeof(_mysteriesSolved))
_mysteriesSolved[mn] = _mystery._firstTry ? 2 : 1;
- // Tier promotion @ 1d40:0941..0978. Skip mystery 0 (practice).
advanceChainStageAfterSolve(mn);
- // `MakeSolvedSound`. `FUN_1d40_05b7` maps E<num>.BIN byte 0 (0..2)
- // via table @ 2608:0c5e to VOC slots 0x15/0x16/0x17.
if (_audio && _voiceOn) {
Common::File ending;
const Common::String fname =
@@ -5615,10 +5183,8 @@ void EEMEngine::doAccuseFloppy() {
}
}
- // 1d40:0991 â FUN_1ee2_06ac (= FUN_1d40_05b7 + font cleanup).
doShowEnding(mn);
- // 1d40:0982 _SavePlayerRecord. clear() before save.
_mystery._solvedPuzzle = true;
_mystery.clear();
(void)saveProfile(_playerName);
@@ -5626,12 +5192,6 @@ void EEMEngine::doAccuseFloppy() {
return;
}
- // `_DisplayAlibi_Floppy @ 1d40:00df`:
- // BG 0x3e + suspect pic at (0x82, 0x5a); MIDI fanfare2.xmi.
- // Balloon table @ 2608:0c0a (default 0x2c). Centered:
- // bx = (0x140-w)/2, by = (0x5a-h)/2.
- // KD reaction @ KDTextIndex[+10] = slot 5; NextScreen = LastScreen;
- // _firstTry = 0.
const byte *susp = _mystery.floppySuspectEntry((uint)picked);
uint16 picId = 0;
uint16 alibiOff = 0xFFFF;
@@ -5640,7 +5200,6 @@ void EEMEngine::doAccuseFloppy() {
alibiOff = _mystery.alibiTextOffset((uint)picked);
}
- // FANFARE2.XMI alibi sting (`_MIDIPlayFile` gated on _voiceOn).
if (_music && _voiceOn)
_music->playFile(Common::Path("fanfare2.xmi"), false);
@@ -5660,8 +5219,6 @@ void EEMEngine::doAccuseFloppy() {
_playerName, _partner);
}
- // Alibi balloon table @ 2608:0c0a (NOT 0c14). Digits @ 2608:0c3a..0c43.
- // Default 0x2c. High bit = mirror flag.
static const uint8 kFloppyAlibiBalloonByDigit[10] = {
0x2a, 0x2b, 0x2c, 0x2d, 0xaa, 0xab, 0xac, 0xad, 0x09, 0x0a
};
@@ -5689,7 +5246,6 @@ void EEMEngine::doAccuseFloppy() {
Picture balloon;
const bool haveBalloon = _balloonArchive.size() > balloonIdx &&
_balloonArchive.loadEntry(balloonIdx, balloon);
- // Centered @ 1d40:01a0: x=(0x140-w)/2, y=(0x5a-h)/2.
int balloonX = 0x21;
int balloonY = 1;
if (haveBalloon) {
@@ -5717,9 +5273,6 @@ void EEMEngine::doAccuseFloppy() {
g_system->copyRectToScreen(scene.getPixels(), scene.pitch, 0, 0, kScreenWidth, kScreenHeight);
g_system->updateScreen();
- // No per-suspect VOC â alibi table @ 2608:0c5e is for post-win
- // scrapbook (`FUN_1d40_05b7`), not alibi.
-
while (!shouldQuit()) {
Common::Event ev;
bool advance = false;
@@ -5738,7 +5291,6 @@ void EEMEngine::doAccuseFloppy() {
g_system->delayMillis(10);
}
- // `_DisplayAlibi_Floppy @ 1d40:01ee` â KDTextIndex[+10] = slot 5.
floppyKDHint(5, kdIdx, bufBase, mysSize);
if (_music && _voiceOn)
_music->stop();
@@ -5747,13 +5299,11 @@ void EEMEngine::doAccuseFloppy() {
_nextScreen = _lastScreen != kScreenInvalid
? (ScreenId)_lastScreen : kScreenSite;
}
-
+// _DoAccuseGallery @ 1df2:0a31.
void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
int highlighted,
Common::Array<Common::Rect> &slotRects,
Common::Array<int> &slotSuspect) {
- // `_DoAccuseGallery @ 1df2:0a31` portrait grid. PIC 0x3f + 5 slots.
- // Partner (5, 0x50) ANI 2/0x10, script 0x02 (`_DoAccuse @ 1df2:0c30`).
Picture accuseBg;
const bool haveAccuseBg = _picsArchive.getPicture(0x3f, accuseBg);
@@ -5775,7 +5325,6 @@ void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
const uint8 phys = _mystery._newOrder[i];
if (phys >= 5)
continue;
- // `_DrawGallery @ 158f:00b9` skip on `_InGallery[phys]==0`.
if (_mystery._inGallery[phys] == 0)
continue;
const GallerySlot &s = kGallerySlots[phys];
Commit: 215d297ebd4887cc744870fa79e21345c27f063b
https://github.com/scummvm/scummvm/commit/215d297ebd4887cc744870fa79e21345c27f063b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:32+02:00
Commit Message:
EEM: enable fit dialog ballons for London
Changed paths:
engines/eem/detection.cpp
diff --git a/engines/eem/detection.cpp b/engines/eem/detection.cpp
index c27f37a7bfb..056915c71f6 100644
--- a/engines/eem/detection.cpp
+++ b/engines/eem/detection.cpp
@@ -76,7 +76,7 @@ const ADGameDescription gameDescriptions[] = {
Common::EN_ANY,
Common::kPlatformDOS,
ADGF_UNSTABLE,
- GUIO2(GUIO_MIDIADLIB, GUIO_MIDIMT32)
+ GUIO3(GAMEOPTION_FIT_DIALOG_BALLOONS, GUIO_MIDIADLIB, GUIO_MIDIMT32)
},
AD_TABLE_END_MARKER
Commit: 670bb5b82079201e1871d6d46e22dbe2c6342c25
https://github.com/scummvm/scummvm/commit/670bb5b82079201e1871d6d46e22dbe2c6342c25
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:33+02:00
Commit Message:
EEM: fixed partner animation that was one frame longer in London
Changed paths:
engines/eem/site.cpp
engines/eem/site.h
engines/eem/ui.cpp
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index ce2b9d89ab4..2c9d1561b43 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -714,7 +714,15 @@ uint oneShotThenLoopFrameAtTick(const uint8 *unfold, uint unfoldLen,
return (numFrames > 0) ? MIN<uint>(frame, numFrames - 1) : 0;
}
-uint bigMapPartnerFrameAtTick(uint numFrames, uint32 elapsedMs) {
+uint bigMapPartnerFrameAtTick(uint numFrames, uint32 elapsedMs, bool london) {
+ if (london) {
+ static const uint8 kUnfoldL[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
+ static const uint8 kWaitSeqL[] = { 10, 11, 10, 10, 10, 10, 10, 10, 10 };
+ return oneShotThenLoopFrameAtTick(kUnfoldL, ARRAYSIZE(kUnfoldL),
+ kWaitSeqL, ARRAYSIZE(kWaitSeqL),
+ numFrames, elapsedMs);
+ }
+ // EEM1 (11-frame anim): entrance {0..8}, idle `_BigMapWaitSeq` {9,..,10,..}.
static const uint8 kUnfold[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8 };
static const uint8 kWaitSeq[] = { 9, 9, 9, 9, 10, 9, 9, 9, 9 };
return oneShotThenLoopFrameAtTick(kUnfold, ARRAYSIZE(kUnfold),
diff --git a/engines/eem/site.h b/engines/eem/site.h
index 30567b2339d..ec7d80f370f 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -47,8 +47,10 @@ uint partnerFrameAtTick(uint16 seqnum, uint numFrames, uint32 tickMs);
/// from EEM1's, so the engine must use the EEM2 sequences for that variant.
void setLondonAnimScripts(bool enabled);
-/// bigMapPartnerFrameAtTick: count-up 0..8 once, then loop `_BigMapWaitSeq`
-uint bigMapPartnerFrameAtTick(uint numFrames, uint32 elapsedMs);
+/// bigMapPartnerFrameAtTick: overview-map partner walk. EEM1 (11-frame anim):
+/// count-up 0..8 once, then idle `_BigMapWaitSeq`. London's anim 0x14/0x12 has
+/// 12 frames, so it uses a longer entrance and an idle that reaches frame 11.
+uint bigMapPartnerFrameAtTick(uint numFrames, uint32 elapsedMs, bool london);
/// bigMapDetailPartnerFrameAtTick: zoomed-view partner frame. Same two-phase shape
/// as `bigMapPartnerFrameAtTick`.
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index dc7c47c74a4..c87536e9cb2 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -3649,7 +3649,7 @@ void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
Animation mapAnim;
if (_aniArchive.loadAnimation(kMapAniId, mapAnim) && !mapAnim.empty()) {
const uint frameIdx = bigMapPartnerFrameAtTick((uint)mapAnim.size(),
- elapsedMs);
+ elapsedMs, isLondon());
blitAnimFrameAnchored(scratch.surfacePtr(), mapAnim[frameIdx],
0xfd, 0x50);
Commit: 3b86f3eb9aaf7babc29aaf19bd54c8f1de3dfe88
https://github.com/scummvm/scummvm/commit/3b86f3eb9aaf7babc29aaf19bd54c8f1de3dfe88
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:33+02:00
Commit Message:
EEM: do not render sublocations in London map
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index c87536e9cb2..7979152d299 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -3296,8 +3296,8 @@ void EEMEngine::doBigMap() {
};
Common::Array<DetailMapHit> hits;
for (uint i = 0; i < _mystery.numSites(); i++) {
- if (!_mystery._onSites[i] &&
- i != _mystery._siteNumber)
+ // On-map flag alone, matching `_SearchMapButtons`.
+ if (!_mystery._onSites[i])
continue;
const byte *entry = _mystery.mapEntry(i);
if (!entry)
@@ -3607,7 +3607,9 @@ void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
const bool haveCrime = _picsArchive.getPicture(0xc6, crimeM);
for (uint i = 0; i < _mystery.numSites(); i++) {
- if (!_mystery._onSites[i] && i != _mystery._siteNumber)
+ // `_DrawBigMapButtons` gates markers on the on-map flag alone, never the
+ // current site: a sublocation (in-site jump, never flagged) must not draw.
+ if (!_mystery._onSites[i])
continue;
const byte *entry = _mystery.mapEntry(i);
if (!entry)
@@ -3685,7 +3687,9 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
const bool floppyMap = _mystery.isLoaded() && isFloppy();
for (uint i = 0; i < _mystery.numSites(); i++) {
- if (!_mystery._onSites[i] && i != _mystery._siteNumber)
+ // `_DrawBigMapButtons` gates markers on the on-map flag alone, never the
+ // current site: a sublocation (in-site jump, never flagged) must not draw.
+ if (!_mystery._onSites[i])
continue;
const byte *entry = _mystery.mapEntry(i);
if (!entry)
Commit: c021df15c99e73cdbde2906909e3e2e45e451433
https://github.com/scummvm/scummvm/commit/c021df15c99e73cdbde2906909e3e2e45e451433
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:33+02:00
Commit Message:
EEM: added missing music in London
Changed paths:
engines/eem/eem.cpp
engines/eem/graphics.cpp
engines/eem/ui.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 136a45ab278..f8d7db79897 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -518,6 +518,8 @@ screenLoop:
case kScreenChooseMystery:
_nextScreen = kScreenInvalid;
doCaseSelection();
+ if (isLondon())
+ stopMusic();
if (_nextScreen == kScreenInvalid && _mystery.isLoaded())
_nextScreen = kScreenInitClues;
break;
@@ -547,6 +549,8 @@ screenLoop:
case kScreenNotebook:
doNotebook();
+ if (isLondon())
+ stopMusic();
if (!_mystery.isLoaded() && _nextScreen != kScreenAction)
_nextScreen = kScreenAction;
else if (_nextScreen == current)
@@ -555,6 +559,8 @@ screenLoop:
case kScreenGallery:
doGallery();
+ if (isLondon())
+ stopMusic();
if (!_mystery.isLoaded() && _nextScreen != kScreenAction)
_nextScreen = kScreenAction;
else if (_nextScreen == current)
@@ -593,6 +599,8 @@ screenLoop:
case kScreenAccuse:
doAccuse();
+ if (isLondon())
+ stopMusic();
if (!_mystery.isLoaded() && _nextScreen != kScreenAction)
_nextScreen = kScreenAction;
else if (_nextScreen == current)
diff --git a/engines/eem/graphics.cpp b/engines/eem/graphics.cpp
index da96f26668c..c6c5dd08d43 100644
--- a/engines/eem/graphics.cpp
+++ b/engines/eem/graphics.cpp
@@ -34,6 +34,7 @@
#include "eem/audio.h"
#include "eem/detection.h"
#include "eem/eem.h"
+#include "eem/music.h"
#include "eem/site.h"
namespace EEM {
@@ -379,6 +380,8 @@ bool EEMEngine::doPuzzle(uint puzzleId) {
const byte *kd = _mystery.kdTextIndex();
const uint16 hintOff = kd ? READ_LE_UINT16(kd + 0x0c) : 0xFFFF;
if (hintOff != 0xFFFF) {
+ if (_music && _voiceOn)
+ _music->playMus(40, /* loop= */ false);
Common::String hint =
parseString(_mystery.textAt(hintOff), _playerName, _partner);
Graphics::ManagedSurface ms(kScreenWidth, kScreenHeight,
@@ -411,6 +414,7 @@ bool EEMEngine::doPuzzle(uint puzzleId) {
if (_audio && _voiceOn && kd)
_audio->sayKDDigital(kd, 6, _partner);
waitForInput(60000);
+ stopMusic();
g_system->copyRectToScreen(cleanBg.getPixels(), cleanBg.pitch,
0, 0, kScreenWidth, kScreenHeight);
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 7979152d299..6ffa2d61c46 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -1375,7 +1375,7 @@ void EEMEngine::doShowScrapbook(uint stage) {
const bool currentTier = (stage == _chainStage);
if (isLondon() && _music && _voiceOn)
- _music->playMus(0x5d, /* loop= */ false);
+ _music->playMus(0x5d, /* loop= */ true);
int mystery = lo;
if (currentTier) {
@@ -2087,6 +2087,10 @@ void EEMEngine::doCaseSelection() {
warning("doCaseSelection: BOOK%u.NME failed to load", book);
return;
}
+
+ if (isLondon() && _music && _voiceOn)
+ _music->playMus(2, /* loop= */ true);
+
const uint listLen = MIN<uint>((uint)names.size(), stageHi - stageLo + 1);
Common::Array<bool> solvedFlags;
@@ -2322,10 +2326,24 @@ void EEMEngine::doNotebook() {
int hoveredNoteSlot = -1;
(void)hoveredNoteSlot;
+ const bool notebookFromSite = isLondon() && _lastScreen == kScreenSite;
+ if (_music && _voiceOn && notebookFromSite)
+ _music->playMus(30, /* loop= */ false);
+
drawNotebookFrame(page);
Common::Point mouse = g_system->getEventManager()->getMousePos();
setInteractiveMouseCursor(notebookButtonAt(mouse.x, mouse.y));
+ if (isLondon() && _music && _voiceOn) {
+ while (notebookFromSite && _music->isPlaying() && !shouldQuit()) {
+ Common::Event drain;
+ while (g_system->getEventManager()->pollEvent(drain)) {}
+ g_system->updateScreen();
+ g_system->delayMillis(10);
+ }
+ _music->playMus(5, /* loop= */ true);
+ }
+
uint32 lastDraw = g_system->getMillis();
uint32 gizmoLastTick = lastDraw;
@@ -2587,6 +2605,9 @@ void EEMEngine::doGallery() {
Picture galBg;
const bool haveBg = _picsArchive.getPicture(0x3f, galBg);
+ if (isLondon() && _music && _voiceOn)
+ _music->playMus(5, /* loop= */ true);
+
const uint8 num = _mystery.numSuspects();
Common::Array<Common::Rect> slotRects;
@@ -3492,7 +3513,7 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
CursorMan.showMouse(true);
setSiteHotspotCursorId(6);
if (_music && _voiceOn)
- _music->playMus(0x27, /* loop= */ false);
+ _music->playMus(0x27, /* loop= */ true);
uint page = 0;
drawScreen(page);
@@ -4192,6 +4213,9 @@ void EEMEngine::doAccuse() {
const byte *gd = _mystery.galleryData();
+ if (isLondon() && _music && _voiceOn)
+ _music->playMus(4, /* loop= */ true);
+
// `_DoAccuse @ 1df2:0c11` outer loop; ESC â NextScreen=3.
if (!doAccuseNotes()) {
if (_nextScreen == kScreenAccuse) {
@@ -4366,6 +4390,9 @@ void EEMEngine::doAccuse() {
}
}
+ if (isLondon() && _music && _voiceOn)
+ _music->playMus(33, /* loop= */ true);
+
// Wrap past empty slots (matches original DI advance).
if (slotRects[highlighted].isEmpty())
highlighted = nextLiveSlot(slotRects, highlighted, +1);
Commit: 1234998c14ff024a18149b0922821c9c436e7062
https://github.com/scummvm/scummvm/commit/1234998c14ff024a18149b0922821c9c436e7062
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:34+02:00
Commit Message:
EEM: corrected palette for the water in London map
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 6ffa2d61c46..d4c06d6a981 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -3160,11 +3160,11 @@ void EEMEngine::doBigMap() {
mapLastTick = now;
drawBigMapOverview(now - mapStartTick);
if (isLondon()) {
- cyclePaletteRange(0xf4, 0xf9);
- cyclePaletteRange(0xfa, 0xff);
+ cyclePaletteRangeReverse(0xf4, 0xf9);
+ cyclePaletteRangeReverse(0xfa, 0xff);
} else {
- cyclePaletteRange(0xf7, 0xfa);
- cyclePaletteRange(0xfb, 0xfe);
+ cyclePaletteRangeReverse(0xf7, 0xfa);
+ cyclePaletteRangeReverse(0xfb, 0xfe);
}
}
g_system->updateScreen();
@@ -3197,6 +3197,8 @@ void EEMEngine::doBigMap() {
int scrollX = MAX<int>(0, MIN<int>(mapW - kMapWinW, zoomX));
int scrollY = MAX<int>(0, MIN<int>(mapH - kMapWinH, zoomY));
+ setSitePalette(isLondon() ? 0x3a : 0x23);
+
const uint32 detailStartTick = g_system->getMillis();
drawBigMapDetail(scrollX, scrollY, mapPixels, mapW, mapH, 0);
uint32 detailLastTick = detailStartTick;
@@ -3381,8 +3383,8 @@ void EEMEngine::doBigMap() {
drawBigMapDetail(scrollX, scrollY, mapPixels, mapW, mapH,
now - detailStartTick);
if (frameTick && isLondon()) {
- cyclePaletteRange(0xee, 0xf2);
- cyclePaletteRange(0xea, 0xed);
+ cyclePaletteRangeReverse(0xee, 0xf2);
+ cyclePaletteRangeReverse(0xea, 0xed);
}
g_system->updateScreen();
g_system->delayMillis(10);
Commit: 0e72f804415c161bdcb52223a72fb28bee52fa30
https://github.com/scummvm/scummvm/commit/0e72f804415c161bdcb52223a72fb28bee52fa30
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:34+02:00
Commit Message:
EEM: autosave every time we talk or get a new clue in London
Changed paths:
engines/eem/eem.cpp
engines/eem/site.cpp
engines/eem/site.h
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index f8d7db79897..5c245a8793f 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -1502,6 +1502,7 @@ Common::Error EEMEngine::loadGameStream(Common::SeekableReadStream *stream) {
s.syncAsByte(_voiceOn);
if (_audio)
_audio->setVoiceEnabled(_voiceOn);
+ s.syncAsByte(_musicOn);
byte playerFemale = 0;
s.syncAsByte(playerFemale);
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 2c9d1561b43..45d286e1012 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -896,12 +896,8 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
const uint16 clueOff = READ_LE_UINT16(idx + 2);
if (clueOff != 0xFFFF) {
const byte *clueBlock = _mystery->blobAt(clueOff);
- if (clueBlock) {
- // Partner-less BG so KD-anim doesn't ghost over the idle.
- _vm->setPartnerEraseBg(&_bgSnapshot);
- _vm->displayClue(clueBlock);
- _vm->setPartnerEraseBg(nullptr);
- }
+ if (clueBlock)
+ displayClueAndAutosave(clueBlock);
}
}
if (siteNum < Mystery::kVisitedSiteCap)
@@ -1721,6 +1717,26 @@ void SiteScreen::updateHotspotCursor(uint siteNum, int x, int y) {
_vm->setHotspotMouseCursor(siteControl || idx >= 0);
}
+void SiteScreen::displayClueAndAutosave(const byte *clueBlock, bool forceSave) {
+ byte before[Mystery::kCluesFoundCap];
+ memcpy(before, _mystery->_cluesFound, sizeof(before));
+
+ _vm->setPartnerEraseBg(&_bgSnapshot);
+ _vm->displayClue(clueBlock);
+ _vm->setPartnerEraseBg(nullptr);
+
+ bool save = forceSave;
+ for (uint i = 0; !save && i < Mystery::kCluesFoundCap; i++)
+ save = !before[i] && _mystery->_cluesFound[i];
+
+ if (save) {
+ const Common::Error err = _vm->saveProfile(_vm->playerName());
+ if (err.getCode() != Common::kNoError)
+ warning("auto-save after clue failed: %s",
+ err.getDesc().c_str());
+ }
+}
+
void SiteScreen::onHotspotClicked(uint siteNum, uint hotIdx) {
debugC(1, kDebugSite, "Site %u: hotspot %u clicked", siteNum, hotIdx);
@@ -1773,8 +1789,11 @@ void SiteScreen::onHotspotClicked(uint siteNum, uint hotIdx) {
if (spots) {
hotOrdinal = READ_LE_UINT16(spots + hotIdx * 14 + 0xa);
}
- if (hotOrdinal < Mystery::kHotSpotsCap)
+ bool newlySeen = false;
+ if (hotOrdinal < Mystery::kHotSpotsCap) {
+ newlySeen = _mystery->_hotSpotsSeen[hotOrdinal] == 0;
_mystery->_hotSpotsSeen[hotOrdinal] = 1;
+ }
_mystery->_searchLocationNumber = (uint16)hotIdx;
// Bytes 8..9 of each 14-byte hotspot rect = byte offset to ClueBlock.
@@ -1785,29 +1804,8 @@ void SiteScreen::onHotspotClicked(uint siteNum, uint hotIdx) {
debugC(2, kDebugSite, " hotspot %u -> clue offset 0x%04x",
hotIdx, clueOff);
const byte *clueBlock = _mystery->blobAt(clueOff);
- if (clueBlock) {
- // Snapshot `_cluesFound` â detect new-clue 0â1 â autosave.
- byte before[Mystery::kCluesFoundCap];
- memcpy(before, _mystery->_cluesFound, sizeof(before));
- _vm->setPartnerEraseBg(&_bgSnapshot);
- _vm->displayClue(clueBlock);
- _vm->setPartnerEraseBg(nullptr);
- // New feature: autosave on new clue.
- bool foundNewClue = false;
- for (uint i = 0; i < Mystery::kCluesFoundCap; i++) {
- if (!before[i] && _mystery->_cluesFound[i]) {
- foundNewClue = true;
- break;
- }
- }
- if (foundNewClue) {
- const Common::Error err =
- _vm->saveProfile(_vm->playerName());
- if (err.getCode() != Common::kNoError)
- warning("auto-save after clue failed: %s",
- err.getDesc().c_str());
- }
- }
+ if (clueBlock)
+ displayClueAndAutosave(clueBlock, /* forceSave= */ newlySeen);
}
}
// `_DoKDAnim(num) @ 168d:028a` + `_PlayAnimation @ 172b:1f46`:
diff --git a/engines/eem/site.h b/engines/eem/site.h
index ec7d80f370f..5301ab68eaa 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -103,6 +103,10 @@ private:
int hotspotCursorId(uint siteNum, int idx) const;
void updateHotspotCursor(uint siteNum, int x, int y);
void onHotspotClicked(uint siteNum, uint hotIdx);
+ /// Show a site clue, then autosave the profile if @p forceSave is set (e.g.
+ /// a hotspot was newly marked seen) or it revealed a new notebook clue
+ /// (`_cluesFound` 0->1). Matches EEM1's per-clue persistence.
+ void displayClueAndAutosave(const byte *clueBlock, bool forceSave = false);
void initImpatienceCounter();
bool checkImpatienceCounter();
void notePartnerActivity();
Commit: 846a1b54522db9b23ac75c50986a457fd7497239
https://github.com/scummvm/scummvm/commit/846a1b54522db9b23ac75c50986a457fd7497239
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:35+02:00
Commit Message:
EEM: show notes for each suspect in the gallery correctly in London
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index d4c06d6a981..acb2e08f421 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -2765,7 +2765,9 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
const int lineH = _font.getFontHeight();
const uint clueMax = floppyMI ? clueCount : 30u;
const byte *ni = _mystery.noteIndex();
- const uint16 niCount = _mystery.noteIndexCount();
+ const uint16 niCount = isLondon()
+ ? (uint16)(_mystery.noteSectionSize() / 2)
+ : _mystery.noteIndexCount();
uint pageStart = 0;
Common::Array<uint> pageStack;
@@ -2808,26 +2810,11 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
continue;
if (!ni || clueId >= niCount)
continue;
- // Floppy: 7-byte entry (+0 text, +2 Jake, +4 Jenny, +6 score).
- // CD: 4-byte entry (u16 textOff + u16 score).
- Common::String txt;
- if (floppyMI) {
- const uint16 textOff = READ_LE_UINT16(ni + clueId * 7);
- const byte *bb = _mystery.blobAt(0);
- const uint32 dsz = _mystery.dataSize();
- if (bb && textOff != 0 && textOff < dsz) {
- const char *p = (const char *)(bb + textOff);
- uint32 len = 0;
- while (textOff + len < dsz && p[len] != 0)
- len++;
- txt = parseString(Common::String(p, len),
- _playerName, _partner);
- }
- } else {
- const uint16 textOff = READ_LE_UINT16(ni + clueId * 4);
- txt = parseString(_mystery.textAt(textOff),
- _playerName, _partner);
- }
+ // Shared notebook lookup: honours London's 2-byte NoteIndex stride
+ // (this path previously hardcoded the EEM1 4-byte stride).
+ Common::String txt = notebookNoteText(clueId, ni, niCount, floppyMI,
+ _mystery.blobAt(0),
+ _mystery.dataSize());
if (txt.empty())
continue;
Commit: 8d9720ef7419530b8321e854fd2f29798fd5c4c3
https://github.com/scummvm/scummvm/commit/8d9720ef7419530b8321e854fd2f29798fd5c4c3
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:35+02:00
Commit Message:
EEM: deduction flow fixes for London
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index acb2e08f421..de374c2bc52 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -2498,9 +2498,9 @@ void EEMEngine::drawNotebookFrame(int &page) {
// `_DrawNotes` walks `_NoteIndex` for current page; word-wraps each
// found clue in `_NotebookRect`. Selected = color 0x3c.
// EEM2/London NoteIndex entries are 2 bytes (no points field), so its real
- // clue count is the section size / 2. `noteIndexCount()` assumes the EEM1
- // 4-byte stride (and stays that way for the accuse-scoring path, which is a
- // separate London concern)
+ // clue count is the section size / 2; `noteIndexCount()` assumes the EEM1
+ // 4-byte stride and undercounts by half. The notebook, gallery, and accuse
+ // note lists all use the London count so no found clue is dropped.
const uint16 londonCount = isLondon()
? (uint16)(_mystery.noteSectionSize() / 2) : 0;
Common::Array<uint> found;
@@ -3768,7 +3768,8 @@ Common::String EEMEngine::accuseNoteText(uint clueId,
(const char *)(ctx.bufBaseNotes + textOff),
_playerName, _partner);
}
- const uint16 textOff = READ_LE_UINT16(ctx.ni + clueId * 4);
+ const uint stride = isLondon() ? 2 : 4;
+ const uint16 textOff = READ_LE_UINT16(ctx.ni + clueId * stride);
return parseString(_mystery.textAt(textOff),
_playerName, _partner);
}
@@ -3875,7 +3876,13 @@ bool EEMEngine::doAccuseNotes() {
if (!_mystery.isLoaded() || !_font.isLoaded())
return false;
const byte *ni = _mystery.noteIndex();
- const uint16 niCount = _mystery.noteIndexCount();
+ // London's NoteIndex is 2-byte entries, so its real clue count is
+ // section size / 2; noteIndexCount() assumes EEM1's 4-byte stride and
+ // undercounts by half, which would drop the upper clues (e.g. answer
+ // clues 13/16 in the training case) from the selectable accuse list.
+ const uint16 niCount = isLondon()
+ ? (uint16)(_mystery.noteSectionSize() / 2)
+ : _mystery.noteIndexCount();
if (!ni)
return false;
@@ -3891,7 +3898,10 @@ bool EEMEngine::doAccuseNotes() {
Common::Array<uint> found;
for (uint i = 0; i < niCount && i < Mystery::kCluesFoundCap; i++) {
- if (_mystery._cluesFound[i] && _mystery.noteHasNotebookText(i))
+ const bool hasText = isLondon()
+ ? (i < niCount)
+ : _mystery.noteHasNotebookText(i);
+ if (_mystery._cluesFound[i] && hasText)
found.push_back(i);
}
@@ -4671,15 +4681,19 @@ void EEMEngine::doAccuse() {
memset(blk->getPixels(), 0, kScreenWidth * kScreenHeight);
g_system->unlockScreen();
}
- setSitePalette(6);
+ // `_DisplayCorrect` win background = `_BuildBackground(scene, 0x42, 0x14)`
+ // (frame PIC 0x3d + scene at 0x42,0x14, palette scene+1). EEM1 CD uses
+ // scene 5; EEM2/London uses scene 0x1b.
+ const uint winScene = isLondon() ? 0x1b : 5;
+ setSitePalette(winScene + 1);
Picture frame, scene;
if (_picsArchive.loadEntry(0x3d, frame)) {
g_system->copyRectToScreen(frame.surface.getPixels(),
frame.surface.pitch, 0, 0,
frame.surface.w, frame.surface.h);
}
- if (5 < _sitesArchive.size() &&
- _sitesArchive.loadEntry(5, scene)) {
+ if (winScene < _sitesArchive.size() &&
+ _sitesArchive.loadEntry(winScene, scene)) {
const int sx = 0x42, sy = 0x14;
const int sw = MIN<int>(scene.surface.w, kScreenWidth - sx);
const int sh = MIN<int>(scene.surface.h, kScreenHeight - sy);
Commit: cfed4b9586b93e7271fde79aa32d16c541ffe590
https://github.com/scummvm/scummvm/commit/cfed4b9586b93e7271fde79aa32d16c541ffe590
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:35+02:00
Commit Message:
EEM: play the correct animation before showing the scrapbook in London
Changed paths:
engines/eem/ui.cpp
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index de374c2bc52..27797d24f8f 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -4719,7 +4719,7 @@ void EEMEngine::doAccuse() {
if (_music && _voiceOn)
_music->stop();
- playAnm(Common::Path("SCRAPBK.ANI"), 120,
+ playAnm(Common::Path(isLondon() ? "SCRAP.ANM" : "SCRAPBK.ANI"), 120,
/* holdLastFrame= */ false);
displayScrapbookExtra(mn);
Commit: 1161071df317d6d5776aee13ed5f8788440d9a92
https://github.com/scummvm/scummvm/commit/1161071df317d6d5776aee13ed5f8788440d9a92
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:36+02:00
Commit Message:
EEM: concurrent partner animations during conversations
Changed paths:
engines/eem/clues.cpp
engines/eem/eem.h
engines/eem/site.cpp
engines/eem/site.h
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 9fc34c93c95..6e312202ca0 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -858,10 +858,21 @@ void EEMEngine::displayClue(const byte *clueBlock) {
}
const int16 kdAnimNum = (int16)READ_LE_UINT16(c + (isLondon() ? 0x4e : 0x3a));
- if (kdAnimNum != -1) {
- playKdAnim((uint16)kdAnimNum);
- g_system->copyRectToScreen(bg.getPixels(), bg.pitch,
- 0, 0, kScreenWidth, kScreenHeight);
+ // Load the partner gesture; it animates concurrently with the balloon
+ // and voice in the wait loop below.
+ Animation kdAnim;
+ int kdPx = 0;
+ int kdPy = 0;
+ uint16 kdAnimId = 0;
+ const bool haveKd = kdAnimNum != -1 &&
+ loadKdAnim((uint16)kdAnimNum, kdAnim, kdPx, kdPy, kdAnimId);
+
+ // Animate the gesture over the partner-less scene so it doesn't ghost
+ // the static partner.
+ if (haveKd && _partnerEraseBg.w == kScreenWidth &&
+ _partnerEraseBg.h == kScreenHeight) {
+ g_system->copyRectToScreen(_partnerEraseBg.getPixels(),
+ _partnerEraseBg.pitch, 0, 0, kScreenWidth, kScreenHeight);
}
const bool useP1 = (_partner == kPartnerJenny) &&
@@ -978,6 +989,19 @@ void EEMEngine::displayClue(const byte *clueBlock) {
setInteractiveMouseCursor(false);
bool advance = false;
bool skipAll = false;
+ Graphics::ManagedSurface kdBase(kScreenWidth, kScreenHeight,
+ Graphics::PixelFormat::createFormatCLUT8());
+ bool haveKdBase = false;
+ uint kdLastFrame = (uint)-1;
+ const uint32 kdStartMs = g_system->getMillis();
+ if (haveKd) {
+ Graphics::Surface *kdScr = g_system->lockScreen();
+ if (kdScr) {
+ kdBase.simpleBlitFrom(*kdScr);
+ g_system->unlockScreen();
+ haveKdBase = true;
+ }
+ }
while (!advance && !shouldQuit()) {
Common::Event ev;
while (g_system->getEventManager()->pollEvent(ev)) {
@@ -1009,6 +1033,20 @@ void EEMEngine::displayClue(const byte *clueBlock) {
break;
}
}
+ if (haveKdBase) {
+ const uint kdFrame = oneShotFrameAtTick(kdAnimId,
+ (uint)kdAnim.size(), g_system->getMillis() - kdStartMs);
+ if (kdFrame != kdLastFrame && kdFrame < kdAnim.size()) {
+ kdLastFrame = kdFrame;
+ Graphics::ManagedSurface comp(kScreenWidth, kScreenHeight,
+ Graphics::PixelFormat::createFormatCLUT8());
+ comp.simpleBlitFrom(kdBase);
+ blitAnimFrameAnchored(comp.surfacePtr(),
+ kdAnim[kdFrame], kdPx, kdPy);
+ g_system->copyRectToScreen(comp.getPixels(), comp.pitch,
+ 0, 0, kScreenWidth, kScreenHeight);
+ }
+ }
g_system->updateScreen();
g_system->delayMillis(10);
}
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 4ea98b5aa65..c06781e49e0 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -221,8 +221,10 @@ public:
const Common::String &playerName,
uint partner) const;
- /// `_DoKDAnim @ 168d:028a` + `_PlayAnimation @ 172b:1f46`.
- void playKdAnim(uint16 num);
+ /// Load the partner gesture (animId + anchor) for concurrent playback with
+ /// the clue balloon/voice. false if out of range.
+ bool loadKdAnim(uint16 num, Animation &anim, int &px, int &py,
+ uint16 &animId);
void setPartnerEraseBg(const Graphics::ManagedSurface *bg);
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 45d286e1012..0627cdeeb65 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -702,6 +702,16 @@ uint partnerFrameAtTick(uint16 seqnum, uint numFrames, uint32 tickMs) {
return frameFromScriptAtTick(s.frames, s.len, numFrames, tickMs);
}
+uint oneShotFrameAtTick(uint16 seqnum, uint numFrames, uint32 tickMs) {
+ const AnimScriptRef s = findAnimScript(seqnum);
+ const uint tick = (uint)(tickMs / kFramePeriodMs);
+ if (!s.frames || s.len == 0)
+ return numFrames > 0 ? MIN<uint>(tick, numFrames - 1) : 0;
+ const uint scriptIdx = MIN<uint>(tick, (uint)s.len - 1);
+ const uint frame = s.frames[scriptIdx];
+ return numFrames > 0 ? MIN<uint>(frame, numFrames - 1) : 0;
+}
+
// Play `unfold` once, then loop `waitSeq` forever. Mirrors the
// original's slot-script-swap idiom
uint oneShotThenLoopFrameAtTick(const uint8 *unfold, uint unfoldLen,
@@ -1817,76 +1827,21 @@ void SiteScreen::onHotspotClicked(uint siteNum, uint hotIdx) {
// -> registers a state-4 (one-shot) animation slot and lets
// `_UpdateAnimations` walk the script until 0x80, then
// frees the slot and re-activates `WaitHandle`.
-void EEMEngine::playKdAnim(uint16 num) {
+bool EEMEngine::loadKdAnim(uint16 num, Animation &anim, int &px, int &py,
+ uint16 &animId) {
if (num >= ARRAYSIZE(kKdAnimTable))
- return;
+ return false;
const uint16 (*kdTable)[6] = isLondon() ? kKdAnimTableLondon : kKdAnimTable;
const uint partner = (_partner == kPartnerJake) ? 0 : 1;
- const uint16 animId = kdTable[num][partner];
- const int px = (int)kdTable[num][2 + partner];
- const int py = (int)kdTable[num][4 + partner];
+ animId = kdTable[num][partner];
+ px = (int)kdTable[num][2 + partner];
+ py = (int)kdTable[num][4 + partner];
- Animation anim;
if (!_aniArchive.loadAnimation(animId, anim) || anim.empty()) {
- warning("playKdAnim(%u): anim %u failed to load", num, animId);
- return;
- }
-
- // State-4 one-shot walks the same (looping) script through once.
- const AnimScriptRef s = findAnimScript(animId);
- const uint8 *frames = s.frames;
- uint frameCount = s.len;
- if (frameCount == 0) {
- // Fallback: linear playback through anim cells.
- frameCount = (uint)anim.size();
- }
-
- // Erase-source: caller-stashed partner-less BG (via `setPartnerEraseBg`)
- // or fall back to current screen (works for full-screen contexts).
- Graphics::ManagedSurface bg(kScreenWidth, kScreenHeight,
- Graphics::PixelFormat::createFormatCLUT8());
- if (_partnerEraseBg.w == kScreenWidth && _partnerEraseBg.h == kScreenHeight) {
- bg.simpleBlitFrom(_partnerEraseBg);
- } else {
- Graphics::Surface *screen = g_system->lockScreen();
- if (!screen)
- return;
- bg.simpleBlitFrom(*screen);
- g_system->unlockScreen();
- }
-
- for (uint i = 0; i < frameCount && !shouldQuit(); i++) {
- const uint frameIdx = frames ? (uint)frames[i] : i;
- if (frameIdx >= anim.size())
- continue;
- const Picture &fr = anim[frameIdx];
- const byte transp = (byte)(fr.flags >> 8);
-
- Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
- Graphics::PixelFormat::createFormatCLUT8());
- scratch.simpleBlitFrom(bg);
- (void)transp; // anchored blitter recomputes from p.flags
- blitAnimFrameAnchored(scratch.surfacePtr(), fr, px, py);
- g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, kScreenWidth, kScreenHeight);
- g_system->updateScreen();
-
- const uint32 wakeup = g_system->getMillis() + 100;
- while (g_system->getMillis() < wakeup && !shouldQuit()) {
- Common::Event ev;
- while (g_system->getEventManager()->pollEvent(ev)) {
- // Drain events; don't skip mid-anim (would eat upcoming clue).
- if (ev.type == Common::EVENT_QUIT ||
- ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
- return;
- }
- g_system->updateScreen();
- g_system->delayMillis(10);
- }
+ warning("loadKdAnim(%u): anim %u failed to load", num, animId);
+ return false;
}
-
- g_system->copyRectToScreen(bg.getPixels(), bg.pitch, 0, 0, kScreenWidth, kScreenHeight);
- g_system->updateScreen();
+ return true;
}
} // End of namespace EEM
diff --git a/engines/eem/site.h b/engines/eem/site.h
index 5301ab68eaa..106bb556a35 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -42,6 +42,9 @@ class Mystery;
/// used both for the fallback and to clamp script values past the asset.
uint partnerFrameAtTick(uint16 seqnum, uint numFrames, uint32 tickMs);
+/// Like partnerFrameAtTick but plays the script ONCE and holds the final frame.
+uint oneShotFrameAtTick(uint16 seqnum, uint numFrames, uint32 tickMs);
+
/// Select the EEM2 ("London") animation-script table inside `findAnimScript`.
/// EEM2 ships its own `_AnimationSequences`; many partner/KD scripts differ
/// from EEM1's, so the engine must use the EEM2 sequences for that variant.
Commit: ce99471d1431b4b5689278677587c1da538a9b40
https://github.com/scummvm/scummvm/commit/ce99471d1431b4b5689278677587c1da538a9b40
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:36+02:00
Commit Message:
EEM: improved partner animations reducing flickering
Changed paths:
engines/eem/clues.cpp
engines/eem/eem.h
engines/eem/site.cpp
engines/eem/site.h
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 6e312202ca0..e4f3957e725 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -968,7 +968,10 @@ void EEMEngine::displayClue(const byte *clueBlock) {
if (copyRows > 0) {
g_system->copyRectToScreen(scratch.getBasePtr(0, copyY),
scratch.pitch, 0, copyY, kScreenWidth, copyRows);
- g_system->updateScreen();
+ // Gesture entry: let the wait loop present, so the partner-less
+ // base isn't flashed before the gesture's first frame.
+ if (!haveKd)
+ g_system->updateScreen();
}
}
@@ -1002,6 +1005,23 @@ void EEMEngine::displayClue(const byte *clueBlock) {
haveKdBase = true;
}
}
+ // Play the gesture one-shot, then loop the partner idle over the
+ // same partner-less base (the original resumes idle when it ends).
+ const uint32 kdDurationMs = haveKd
+ ? oneShotDurationMs(kdAnimId, (uint)kdAnim.size()) : 0;
+ Animation idleAnim;
+ int idleX = 0;
+ int idleY = 0;
+ bool haveIdle = false;
+ if (haveKdBase && _hasPartnerIdle &&
+ getAni().loadAnimation(_partnerIdleAnimId, idleAnim) &&
+ !idleAnim.empty()) {
+ haveIdle = true;
+ idleX = _partnerIdleX;
+ idleY = _partnerIdleY;
+ }
+ bool kdInIdle = false;
+ uint kdLastIdleFrame = (uint)-1;
while (!advance && !shouldQuit()) {
Common::Event ev;
while (g_system->getEventManager()->pollEvent(ev)) {
@@ -1033,18 +1053,40 @@ void EEMEngine::displayClue(const byte *clueBlock) {
break;
}
}
+ if (skipAll)
+ break;
if (haveKdBase) {
- const uint kdFrame = oneShotFrameAtTick(kdAnimId,
- (uint)kdAnim.size(), g_system->getMillis() - kdStartMs);
- if (kdFrame != kdLastFrame && kdFrame < kdAnim.size()) {
- kdLastFrame = kdFrame;
- Graphics::ManagedSurface comp(kScreenWidth, kScreenHeight,
- Graphics::PixelFormat::createFormatCLUT8());
- comp.simpleBlitFrom(kdBase);
- blitAnimFrameAnchored(comp.surfacePtr(),
- kdAnim[kdFrame], kdPx, kdPy);
- g_system->copyRectToScreen(comp.getPixels(), comp.pitch,
- 0, 0, kScreenWidth, kScreenHeight);
+ const uint32 kdElapsed = g_system->getMillis() - kdStartMs;
+ if (haveIdle && kdElapsed >= kdDurationMs) {
+ // Resume the looping idle wait-anim.
+ const uint f = partnerFrameAtTick(_partnerIdleAnimId,
+ (uint)idleAnim.size(), kdElapsed - kdDurationMs);
+ if ((!kdInIdle || f != kdLastIdleFrame) &&
+ f < idleAnim.size()) {
+ kdInIdle = true;
+ kdLastIdleFrame = f;
+ Graphics::ManagedSurface comp(kScreenWidth, kScreenHeight,
+ Graphics::PixelFormat::createFormatCLUT8());
+ comp.simpleBlitFrom(kdBase);
+ blitAnimFrameAnchored(comp.surfacePtr(),
+ idleAnim[f], idleX, idleY);
+ g_system->copyRectToScreen(comp.getPixels(), comp.pitch,
+ 0, 0, kScreenWidth, kScreenHeight);
+ }
+ } else {
+ // Gesture one-shot.
+ const uint f = oneShotFrameAtTick(kdAnimId,
+ (uint)kdAnim.size(), kdElapsed);
+ if (f != kdLastFrame && f < kdAnim.size()) {
+ kdLastFrame = f;
+ Graphics::ManagedSurface comp(kScreenWidth, kScreenHeight,
+ Graphics::PixelFormat::createFormatCLUT8());
+ comp.simpleBlitFrom(kdBase);
+ blitAnimFrameAnchored(comp.surfacePtr(),
+ kdAnim[f], kdPx, kdPy);
+ g_system->copyRectToScreen(comp.getPixels(), comp.pitch,
+ 0, 0, kScreenWidth, kScreenHeight);
+ }
}
}
g_system->updateScreen();
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index c06781e49e0..4fd29ea5183 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -228,6 +228,15 @@ public:
void setPartnerEraseBg(const Graphics::ManagedSurface *bg);
+ /// Partner idle wait-anim that `displayClue` loops once a clue gesture's
+ /// one-shot ends. Set around `displayClue` by the site loop.
+ void setPartnerIdleAnim(bool has, uint16 animId, int x, int y) {
+ _hasPartnerIdle = has;
+ _partnerIdleAnimId = animId;
+ _partnerIdleX = x;
+ _partnerIdleY = y;
+ }
+
/// Balloon-text-inset metadata. 52-entry table @ 29be:0875 (CD) /
/// 2608:05f9 (floppy), indexed by `(bubNum & 0x7F)`. 10 bytes per
/// entry: the first 3 fields (x inset, y inset, text width) are used
@@ -501,10 +510,15 @@ private:
/// ESC during intro: skip remaining opening-anim chain.
bool _skipIntro = false;
- /// Clean BG (no partner/NPC) used by `playKdAnim` between camera-anim
- /// cells.
+ /// Partner-less scene that `displayClue` composites the gesture/idle over.
Graphics::ManagedSurface _partnerEraseBg;
+ /// Idle wait-anim to resume after a clue gesture (see setPartnerIdleAnim).
+ bool _hasPartnerIdle = false;
+ uint16 _partnerIdleAnimId = 0;
+ int _partnerIdleX = 0;
+ int _partnerIdleY = 0;
+
bool _interactiveMouseCursor = false;
/// Active EEM2 cursor shape (index into `kLondonCursorPics`). -1 forces a
/// reload on the next `setSiteHotspotCursorId`.
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 0627cdeeb65..c4d7b9f9949 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -712,6 +712,12 @@ uint oneShotFrameAtTick(uint16 seqnum, uint numFrames, uint32 tickMs) {
return numFrames > 0 ? MIN<uint>(frame, numFrames - 1) : 0;
}
+uint32 oneShotDurationMs(uint16 seqnum, uint numFrames) {
+ const AnimScriptRef s = findAnimScript(seqnum);
+ const uint count = (s.frames && s.len) ? (uint)s.len : numFrames;
+ return (uint32)count * kFramePeriodMs;
+}
+
// Play `unfold` once, then loop `waitSeq` forever. Mirrors the
// original's slot-script-swap idiom
uint oneShotThenLoopFrameAtTick(const uint8 *unfold, uint unfoldLen,
@@ -1447,19 +1453,17 @@ void SiteScreen::syncCompositedScreen() {
0, 0, kScreenWidth, kScreenHeight);
}
-void SiteScreen::renderPartner(uint siteNum, uint32 tickMs) {
+bool SiteScreen::partnerIdleAnimParams(uint siteNum, uint16 &animId,
+ int &x, int &y) {
const byte *site = _mystery->siteData(siteNum);
if (!site)
- return;
+ return false;
const uint8 partner = _vm->getPartnerIndex();
- uint animId;
- int x;
- int y;
if (_vm->isFloppy()) {
const uint16 spkOff = READ_LE_UINT16(site + 8);
const byte *spk = _mystery->blobAt(spkOff);
if (!spk)
- return;
+ return false;
if (partner == 0) {
animId = READ_LE_UINT16(spk + 0);
x = (int)READ_LE_UINT16(spk + 2);
@@ -1473,15 +1477,21 @@ void SiteScreen::renderPartner(uint siteNum, uint32 tickMs) {
const uint16 speaker = READ_LE_UINT16(site + 8);
const uint16 (*waitTable)[6] = _vm->isLondon()
? kWaitAnimsLondon : kWaitAnims;
- if (speaker >= ARRAYSIZE(kWaitAnims)) {
- warning("renderPartner: site %u has speakerIdx=%u out of range",
- siteNum, speaker);
- return;
- }
+ if (speaker >= ARRAYSIZE(kWaitAnims))
+ return false;
animId = waitTable[speaker][0 + partner];
x = (int)(int16)waitTable[speaker][2 + partner];
y = (int)(int16)waitTable[speaker][4 + partner];
}
+ return true;
+}
+
+void SiteScreen::renderPartner(uint siteNum, uint32 tickMs) {
+ uint16 animId;
+ int x;
+ int y;
+ if (!partnerIdleAnimParams(siteNum, animId, x, y))
+ return;
Animation anim;
if (!_vm->getAni().loadAnimation(animId, anim) || anim.empty())
@@ -1731,8 +1741,16 @@ void SiteScreen::displayClueAndAutosave(const byte *clueBlock, bool forceSave) {
byte before[Mystery::kCluesFoundCap];
memcpy(before, _mystery->_cluesFound, sizeof(before));
+ // Idle wait-anim for `displayClue` to resume after a gesture's one-shot.
+ uint16 idleId = 0;
+ int idleX = 0, idleY = 0;
+ const bool hasIdle =
+ partnerIdleAnimParams(_mystery->_siteNumber, idleId, idleX, idleY);
+
_vm->setPartnerEraseBg(&_bgSnapshot);
+ _vm->setPartnerIdleAnim(hasIdle, idleId, idleX, idleY);
_vm->displayClue(clueBlock);
+ _vm->setPartnerIdleAnim(false, 0, 0, 0);
_vm->setPartnerEraseBg(nullptr);
bool save = forceSave;
diff --git a/engines/eem/site.h b/engines/eem/site.h
index 106bb556a35..f17f8ae4e20 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -45,6 +45,9 @@ uint partnerFrameAtTick(uint16 seqnum, uint numFrames, uint32 tickMs);
/// Like partnerFrameAtTick but plays the script ONCE and holds the final frame.
uint oneShotFrameAtTick(uint16 seqnum, uint numFrames, uint32 tickMs);
+/// Total time for one full play of a one-shot gesture (frame count * period).
+uint32 oneShotDurationMs(uint16 seqnum, uint numFrames);
+
/// Select the EEM2 ("London") animation-script table inside `findAnimScript`.
/// EEM2 ships its own `_AnimationSequences`; many partner/KD scripts differ
/// from EEM1's, so the engine must use the EEM2 sequences for that variant.
@@ -118,9 +121,14 @@ private:
/// Partner site-arrival sequence
bool enterSiteAnim();
- /// renderPartner: persistent in-site partner sprite
+ /// renderPartner: persistent in-site partner sprite
void renderPartner(uint siteNum, uint32 tickMs);
+ /// Resolve the partner idle wait-anim (animId + screen anchor) for a site.
+ /// Shared by renderPartner and the clue gesture's idle-resume. Returns
+ /// false when the site has no usable speaker/partner entry.
+ bool partnerIdleAnimParams(uint siteNum, uint16 &animId, int &x, int &y);
+
/// Floppy active speaker pose
bool renderFloppyHotspotPartnerPose(uint siteNum);
Commit: 24173e32113517a95c4ce900e35eba441f7af73b
https://github.com/scummvm/scummvm/commit/24173e32113517a95c4ce900e35eba441f7af73b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:36+02:00
Commit Message:
EEM: properly mark interactive zones in London
Changed paths:
engines/eem/eem.cpp
engines/eem/site.cpp
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 5c245a8793f..82613a39f66 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -625,6 +625,9 @@ void EEMEngine::setInteractiveMouseCursor(bool active) {
_interactiveMouseCursor = active;
installMouseCursor(_picsArchive, active);
+ // The red-outline highlight replaced any London cursor shape; force the
+ // next setSiteHotspotCursorId to reinstall.
+ _siteCursorId = -1;
}
void EEMEngine::setHotspotMouseCursor(bool active) {
@@ -660,6 +663,8 @@ void EEMEngine::setSiteHotspotCursorId(int cursorId) {
CursorMan.replaceCursor(cursor.surface.rawSurface(), 0, 0, transparent);
CursorMan.replaceCursorPalette(nullptr, 0, 0);
_siteCursorId = cursorId;
+ // This London cursor replaced the red-outline highlight, if it was active.
+ _interactiveMouseCursor = false;
}
bool EEMEngine::openArchives() {
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index c4d7b9f9949..88e6c6e919e 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -1725,15 +1725,22 @@ void SiteScreen::updateHotspotCursor(uint siteNum, int x, int y) {
if (!_vm)
return;
const int idx = hotspotAtPoint(siteNum, x, y);
- if (_vm->isLondon()) {
- // EEM2 (`_DoSiteLoop` @ FUN_1717_07ab) swaps the cursor shape to the
- // hovered hotspot's own cursor id; off any hotspot it is the arrow (0).
- _vm->setSiteHotspotCursorId(idx >= 0 ? hotspotCursorId(siteNum, idx) : 0);
- return;
- }
const bool siteControl = kPdaSiteRect.contains(x, y) ||
kPdaPartnerFootMapRect.contains(x, y) ||
kPdaPartnerHeadHintRect.contains(x, y);
+ if (_vm->isLondon()) {
+ // EEM2 gives some hotspots their own cursor shape; every other
+ // interactive zone (the site/foot/head controls, or a hotspot with no
+ // custom cursor) falls back to the red-outline highlight.
+ const int cursorId = idx >= 0 ? hotspotCursorId(siteNum, idx) : 0;
+ if (cursorId > 0)
+ _vm->setSiteHotspotCursorId(cursorId);
+ else if (idx >= 0 || siteControl)
+ _vm->setInteractiveMouseCursor(true);
+ else
+ _vm->setSiteHotspotCursorId(0);
+ return;
+ }
_vm->setHotspotMouseCursor(siteControl || idx >= 0);
}
Commit: 111cc673fc7edb49681f4eddd0b2c1cd26b6f3e8
https://github.com/scummvm/scummvm/commit/111cc673fc7edb49681f4eddd0b2c1cd26b6f3e8
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-22T00:21:37+02:00
Commit Message:
EEM: completed implementation for setup screen in London
Changed paths:
engines/eem/eem.h
engines/eem/ui.cpp
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 4fd29ea5183..a91adfc73e8 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -425,6 +425,9 @@ private:
void doSetupLondon();
void setupDrawScreenLondon();
+ /// London "profile saved" confirmation: PIC 0x203 centred over the setup
+ /// screen, dismissed by any click/key (caller redraws the setup screen).
+ void setupShowSavedConfirm();
/// `_DoInitClues @ 1a35:0411` (minus live ANI sequence playback).
void doInitClues();
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 27797d24f8f..086fd98647f 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -1699,6 +1699,37 @@ void EEMEngine::setupDrawScreenLondon() {
g_system->updateScreen();
}
+void EEMEngine::setupShowSavedConfirm() {
+ Picture pic;
+ if (!_picsArchive.getPicture(0x203, pic) || pic.surface.empty())
+ return;
+ Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+ Graphics::PixelFormat::createFormatCLUT8());
+ Graphics::Surface *cur = g_system->lockScreen();
+ if (cur) {
+ scratch.simpleBlitFrom(*cur);
+ g_system->unlockScreen();
+ }
+ const int sx = MAX<int>(0, (kScreenWidth - pic.surface.w) / 2);
+ const int sy = MAX<int>(0, (kScreenHeight - pic.surface.h) / 2);
+ scratch.transBlitFrom(pic.surface, Common::Point(sx, sy),
+ (uint32)(byte)(pic.flags >> 8));
+ g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+ 0, 0, kScreenWidth, kScreenHeight);
+ g_system->updateScreen();
+ while (!shouldQuit()) {
+ Common::Event ev;
+ while (g_system->getEventManager()->pollEvent(ev)) {
+ if (ev.type == Common::EVENT_QUIT ||
+ ev.type == Common::EVENT_RETURN_TO_LAUNCHER ||
+ ev.type == Common::EVENT_KEYDOWN ||
+ ev.type == Common::EVENT_LBUTTONDOWN)
+ return;
+ }
+ g_system->delayMillis(15);
+ }
+}
+
// `_DoSetup @ 2046:067b`
void EEMEngine::doSetupLondon() {
if (!_font.isLoaded()) {
@@ -1714,8 +1745,8 @@ void EEMEngine::doSetupLondon() {
const Common::Rect kSaveBtn (281, 108, 299, 125); // [6]
const Common::Rect kNewCaseBtn (281, 127, 299, 144); // [7]
const Common::Rect kDoneBtn ( 53, 153, 108, 183); // [8]
- const Common::Rect kQuitBtn (145, 163, 174, 187); // [9]
- const Common::Rect kHelpBtn (212, 153, 266, 184); // [10]
+ const Common::Rect kHelpBtn (145, 163, 174, 187); // [9]
+ const Common::Rect kQuitBtn (212, 153, 266, 184); // [10]
const Common::Rect kCreditsBtn ( 81, 25, 238, 37); // [11]
const Common::Rect kMusicBtn ( 20, 82, 38, 99); // [12]
@@ -1791,8 +1822,11 @@ void EEMEngine::doSetupLondon() {
}
if (kSaveBtn.contains(mx, my)) {
- if (_mystery.isLoaded())
+ if (_mystery.isLoaded()) {
saveProfile(_playerName);
+ setupShowSavedConfirm();
+ dirty = true;
+ }
continue;
}
if (kHelpBtn.contains(mx, my)) {
More information about the Scummvm-git-logs
mailing list