[Scummvm-git-logs] scummvm master -> 17fd586fb5424a00e084c5d631a5e6a06e3bcec7
neuromancer
noreply at scummvm.org
Thu Jun 25 10:55:19 UTC 2026
This automated email contains information about 6 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
ead8c9991e EEM: first step to support EEM2 Mac
a678ff5982 EEM: intro animation in EEM2 Mac
178a9f45d9 EEM: added missing intro playback animation in EEM2 Mac
e3047778c2 EEM: fixed parner selection in EEM2 Mac
b36e3a1eef EEM: start rendering case introduction in EEM2 Mac
17fd586fb5 EEM: render text in EEM2 Mac
Commit: ead8c9991ed87d3c7064ac4eaceb330b9dc8cdd3
https://github.com/scummvm/scummvm/commit/ead8c9991ed87d3c7064ac4eaceb330b9dc8cdd3
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-25T12:55:09+02:00
Commit Message:
EEM: first step to support EEM2 Mac
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 22cbe01d787..725c386fa3a 100644
--- a/engines/eem/detection.cpp
+++ b/engines/eem/detection.cpp
@@ -32,6 +32,17 @@ const PlainGameDescriptor eemGames[] = {
{ nullptr, nullptr }
};
+// The EEM2 (London) Macintosh CD keeps its data in subfolders ("EEM2 CD" with
+// "Data Files"/"Mac Scripts", and the app in "EEM London CD"). Let the detector
+// descend into them; the DOS releases have no such folders and are unaffected.
+static const char *const directoryGlobs[] = {
+ "EEM2 CD",
+ "Data Files",
+ "Mac Scripts",
+ "EEM London CD",
+ nullptr
+};
+
#define GUI_OPTIONS_EEM_FLOPPY GUIO4(GAMEOPTION_HIDE_HIGHLIGHT_BOXES, 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)
#define GUI_OPTIONS_EEM_DEMO GUIO2(GAMEOPTION_HIDE_HIGHLIGHT_BOXES, GUIO_NOMIDI)
@@ -104,7 +115,7 @@ const ADGameDescription gameDescriptions[] = {
GUI_OPTIONS_EEM_MAC
},
{
- // Eagle Eye Mysteries in London
+ // Eagle Eye Mysteries in London
"eem2",
"CD",
AD_ENTRY2s("EEM2CD.EXE", "211a376b23a1b6259d0c36cf46d26ed4", 172560,
@@ -114,6 +125,20 @@ const ADGameDescription gameDescriptions[] = {
ADGF_UNSTABLE,
GUIO3(GAMEOPTION_FIT_DIALOG_BALLOONS, GUIO_MIDIADLIB, GUIO_MIDIMT32)
},
+ {
+ // Eagle Eye Mysteries in London, Macintosh CD. Played straight from the
+ // disc: shared data in "EEM2 CD/Data Files", mystery scripts (loose
+ // m<n>.bin) in "EEM2 CD/Mac Scripts", Mac assets (PICT/FONT/snd/Midi) in
+ // the "EEM London CD" app resource fork. Big-endian like EEM1 Mac.
+ "eem2",
+ "CD",
+ AD_ENTRY2s("pics.dbd", "6ba1085cb6f8fedad7d8ee5ef5f461cd", 10250928,
+ "m0.bin", "a0b943bb520c349259fa13308d2d368e", 37971),
+ Common::EN_ANY,
+ Common::kPlatformMacintosh,
+ ADGF_UNSTABLE,
+ GUI_OPTIONS_EEM_MAC
+ },
AD_TABLE_END_MARKER
};
@@ -133,6 +158,9 @@ const DebugChannelDef debugFlagList[] = {
class EEMMetaEngineDetection : public AdvancedMetaEngineDetection<ADGameDescription> {
public:
EEMMetaEngineDetection() : AdvancedMetaEngineDetection(EEM::gameDescriptions, EEM::eemGames) {
+ // Reach the EEM2 Mac data nested under "EEM2 CD/Data Files" etc.
+ _maxScanDepth = 3;
+ _directoryGlobs = EEM::directoryGlobs;
}
const char *getName() const override {
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index cd59898e5b7..817fe3ef609 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -787,11 +787,30 @@ void EEMEngine::setSiteHotspotCursorId(int cursorId) {
bool EEMEngine::openArchives() {
const bool mac = isMacintosh();
- // The Mac release can be played straight from its floppy installer. When
- // those files are present, mount a virtual archive that decompresses the
- // game data (PICS.DBD, MysteryData, fonts, ...) on demand, so the opens
- // below and the Mac resource-fork lookups resolve transparently.
- if (mac && !SearchMan.hasArchive("eem-installer")) {
+ // EEM2 (London) Mac is played straight from the CD, whose data lives in
+ // subfolders ("Data Files", "Mac Scripts") with the Mac app in "EEM London
+ // CD". Register them so the bare-name opens below -- and the later script,
+ // mystery and palette loaders -- resolve whether the user points ScummVM at
+ // the disc root or at the "EEM2 CD" folder.
+ if (mac && isLondon()) {
+ const Common::FSNode gameDir(ConfMan.getPath("path"));
+ // Disc-root layout (recommended -- this also reaches the "EEM London
+ // CD" app that holds the Mac fonts/sound). The "/"-separated names
+ // descend two levels (see Common::addSubDirectoryMatching).
+ SearchMan.addSubDirectoryMatching(gameDir, "EEM2 CD/Data Files");
+ SearchMan.addSubDirectoryMatching(gameDir, "EEM2 CD/Mac Scripts");
+ SearchMan.addSubDirectoryMatching(gameDir, "EEM London CD");
+ // ...or the user pointed ScummVM straight at the "EEM2 CD" folder.
+ SearchMan.addSubDirectoryMatching(gameDir, "Data Files");
+ SearchMan.addSubDirectoryMatching(gameDir, "Mac Scripts");
+ }
+
+ // The EEM1 Mac release can be played straight from its floppy installer.
+ // When those files are present, mount a virtual archive that decompresses
+ // the game data (PICS.DBD, MysteryData, fonts, ...) on demand, so the opens
+ // below and the Mac resource-fork lookups resolve transparently. (EEM2 Mac
+ // ships loose on the CD and has no such installer.)
+ if (mac && !isLondon() && !SearchMan.hasArchive("eem-installer")) {
const Common::FSNode gameDir(ConfMan.getPath("path"));
if (Common::Archive *installer = createInstallerArchive(gameDir)) {
SearchMan.add("eem-installer", installer);
@@ -820,7 +839,8 @@ bool EEMEngine::openArchives() {
bool EEMEngine::loadSitePalettes() {
Common::File f;
- const char *palFile = isLondon() ? "SITEPALS." : "SITEPALS";
+ // EEM2 DOS uses "SITEPALS." (8.3); EEM1 and both Mac releases use "SITEPALS".
+ const char *palFile = (isLondon() && !isMacintosh()) ? "SITEPALS." : "SITEPALS";
if (!f.open(Common::Path(palFile))) {
warning("%s missing", palFile);
return false;
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index a00e5cfa847..bf7a3fa83d7 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -131,7 +131,11 @@ public:
Variant getVariant() const { return _variant; }
bool isFloppy() const { return _variant == kVariantFloppy || isDemo(); }
bool isLondon() const { return _variant == kVariantLondonCD; }
- bool isMacintosh() const { return _variant == kVariantMac; }
+ // London (game) and Macintosh (platform) are orthogonal -- the London CD
+ // shipped for both DOS and Mac -- so derive Mac-ness from the platform
+ // rather than the single-valued `_variant` (which can only hold one of
+ // them at a time). This lets EEM2 Mac be both London and Macintosh.
+ bool isMacintosh() const { return getPlatform() == Common::kPlatformMacintosh; }
bool isDemo() const {
return _gameDescription && (_gameDescription->flags & ADGF_DEMO);
}
Commit: a678ff5982f92c2ccc61bfa8f7d1a1935f820ce4
https://github.com/scummvm/scummvm/commit/a678ff5982f92c2ccc61bfa8f7d1a1935f820ce4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-25T12:55:09+02:00
Commit Message:
EEM: intro animation in EEM2 Mac
Changed paths:
engines/eem/eem.cpp
engines/eem/eem.h
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 817fe3ef609..35882ad6ea2 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -36,6 +36,8 @@
#include "graphics/cursorman.h"
#include "graphics/paletteman.h"
+#include "video/flic_decoder.h"
+
#include "eem/audio.h"
#include "eem/detection.h"
#include "eem/eem.h"
@@ -374,13 +376,18 @@ static void addMacResourceSearchPaths() {
static bool loadMacFontResource(EEMFont &font, uint16 resourceId, int size) {
addMacResourceSearchPaths();
- const Common::Path appResourceFork("Eagle Eye Mysteries");
- if (font.loadMacResource(appResourceFork, resourceId, size))
- return true;
-
- const Common::Path nestedAppResourceFork("rsrc/Eagle Eye Mysteries");
- if (font.loadMacResource(nestedAppResourceFork, resourceId, size))
- return true;
+ // The Eagle Eye fonts live in the game application's resource fork. EEM1 Mac
+ // ships it as "Eagle Eye Mysteries"; EEM2 (London) Mac ships it as
+ // "EEM London CD" but reuses the same FONT resource ids (3214/3209).
+ static const char *const kAppForks[] = {
+ "Eagle Eye Mysteries",
+ "rsrc/Eagle Eye Mysteries",
+ "EEM London CD",
+ };
+ for (uint i = 0; i < ARRAYSIZE(kAppForks); i++) {
+ if (font.loadMacResource(Common::Path(kAppForks[i]), resourceId, size))
+ return true;
+ }
return false;
}
@@ -799,10 +806,12 @@ bool EEMEngine::openArchives() {
// descend two levels (see Common::addSubDirectoryMatching).
SearchMan.addSubDirectoryMatching(gameDir, "EEM2 CD/Data Files");
SearchMan.addSubDirectoryMatching(gameDir, "EEM2 CD/Mac Scripts");
+ SearchMan.addSubDirectoryMatching(gameDir, "EEM2 CD/Anim Files");
SearchMan.addSubDirectoryMatching(gameDir, "EEM London CD");
// ...or the user pointed ScummVM straight at the "EEM2 CD" folder.
SearchMan.addSubDirectoryMatching(gameDir, "Data Files");
SearchMan.addSubDirectoryMatching(gameDir, "Mac Scripts");
+ SearchMan.addSubDirectoryMatching(gameDir, "Anim Files");
}
// The EEM1 Mac release can be played straight from its floppy installer.
@@ -919,6 +928,64 @@ void EEMEngine::interruptAudio(bool stopMusicToo) {
_music->stop();
}
+void EEMEngine::playFlc(const Common::Path &path, bool fadeIn) {
+ Video::FlicDecoder flic;
+ if (!flic.loadFile(path)) {
+ warning("playFlc: %s missing", path.toString().c_str());
+ return;
+ }
+
+ const int fw = flic.getWidth();
+ const int fh = flic.getHeight();
+ // Centre the movie on the (larger) Mac screen with a black letterbox.
+ const int ox = MAX(0, (screenWidth() - fw) / 2);
+ const int oy = MAX(0, (screenHeight() - fh) / 2);
+ const int w = MIN(fw, screenWidth() - ox);
+ const int h = MIN(fh, screenHeight() - oy);
+
+ byte black[3 * 256] = { 0 };
+ g_system->getPaletteManager()->setPalette(black, 0, 256);
+ if (Graphics::Surface *screen = g_system->lockScreen()) {
+ screen->fillRect(Common::Rect(screen->w, screen->h), 0);
+ g_system->unlockScreen();
+ }
+
+ flic.start();
+ bool paletteApplied = false;
+ while (!flic.endOfVideo() && !shouldQuit() && !_skipIntro) {
+ if (flic.needsUpdate()) {
+ const Graphics::Surface *frame = flic.decodeNextFrame();
+ if (frame) {
+ g_system->copyRectToScreen(frame->getPixels(), frame->pitch,
+ ox, oy, w, h);
+ if (flic.hasDirtyPalette()) {
+ if (fadeIn && !paletteApplied)
+ fadePaletteFromBlack(flic.getPalette());
+ else
+ g_system->getPaletteManager()->setPalette(
+ flic.getPalette(), 0, 256);
+ paletteApplied = true;
+ }
+ }
+ g_system->updateScreen();
+ }
+
+ 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)
+ return;
+ if (event.type == Common::EVENT_KEYDOWN) {
+ if (event.kbd.keycode == Common::KEYCODE_ESCAPE)
+ _skipIntro = true;
+ return;
+ }
+ }
+ g_system->delayMillis(10);
+ }
+}
+
void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs,
bool holdLastFrame, bool fadeIn,
bool setSkipIntroOnEsc) {
@@ -1491,34 +1558,41 @@ void EEMEngine::runLondonStartup() {
if (!shouldQuit() && !_skipIntro)
showLondonLogo(0x20c, 0x3e, 2500); // publisher logo (FUN_2721_07be)
- // 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);
- if (_audio)
- _audio->stopVoice();
- fadeCurrentPaletteToBlack();
- }
+ if (isMacintosh()) {
+ // The Mac CD ships the post-logo intro as a single Flic movie
+ // (KDCDINTR.FLC) instead of the DOS bolt/movie/wave .ANM sequence.
+ if (!shouldQuit() && !_skipIntro)
+ playFlc(Common::Path("KDCDINTR.FLC"), /* fadeIn= */ true);
+ } else {
+ // 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);
+ if (_audio)
+ _audio->stopVoice();
+ fadeCurrentPaletteToBlack();
+ }
- // 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);
+ // 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; 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)
- playAnm(Common::Path("WAVE.ANM"), 120, /* holdLastFrame= */ true,
- /* fadeIn= */ true);
- if (!shouldQuit() && !_skipIntro)
- waitForInput(kHoldForever);
+ // Animated title (wave.anm) over the looping theme (MUS00102.XMI);
+ // a click / key advances to character creation. The original loops
+ // 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)
+ playAnm(Common::Path("WAVE.ANM"), 120, /* holdLastFrame= */ true,
+ /* fadeIn= */ true);
+ if (!shouldQuit() && !_skipIntro)
+ waitForInput(kHoldForever);
+ }
if (_music)
_music->stop();
_skipIntro = false;
@@ -1537,6 +1611,10 @@ void EEMEngine::runLondonStartup() {
void EEMEngine::showLondonCharSelect() {
debugC(1, kDebugGeneral, "EEM2 (London): character creation");
+ // Coordinates are DOS-logical (320x200); on the Mac the screen is 512x384,
+ // so the rects scale up (scaleRect/scaleX/scaleY are identity on DOS). The
+ // passport background (PIC 0xc) is authored at each platform's resolution
+ // -- 512x384 on the Mac -- so the scaled field/box coords line up with it.
const Common::Rect kFirstRect(54, 75, 151, 85);
const Common::Rect kLastRect(167, 75, 266, 85);
const Common::Rect kMaleBox(110, 116, 120, 122);
@@ -1544,15 +1622,21 @@ void EEMEngine::showLondonCharSelect() {
const uint kMaxFirst = 12, kMaxLast = 20;
const uint8 kInkColor = 0x22;
+ const Common::Rect firstRect = scaleRect(kFirstRect);
+ const Common::Rect lastRect = scaleRect(kLastRect);
+ const Common::Rect maleBox = scaleRect(kMaleBox);
+ const Common::Rect femaleBox = scaleRect(kFemaleBox);
+ const int sw = screenWidth(), sh = screenHeight();
+
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)
+ ? (uint8)bg.surface.getPixel(firstRect.left, firstRect.top)
: 0x38;
const uint8 boxMarkColor = haveBg
- ? (uint8)bg.surface.getPixel(109, 115)
+ ? (uint8)bg.surface.getPixel(scaleX(109), scaleY(115))
: 0x22;
byte pal[kPalSize];
@@ -1585,41 +1669,41 @@ void EEMEngine::showLondonCharSelect() {
while (!done && !shouldQuit()) {
if (needRedraw) {
- Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+ Graphics::ManagedSurface scratch(sw, sh,
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
if (haveBg)
scratch.simpleBlitFrom(bg.surface);
if (getFont().isLoaded()) {
- getFont().drawString(&scratch, first, kFirstRect.left + 2,
- kFirstRect.top + 1, kFirstRect.width(),
+ getFont().drawString(&scratch, first, firstRect.left + 2,
+ firstRect.top + 1, firstRect.width(),
kInkColor);
- getFont().drawString(&scratch, last, kLastRect.left + 2,
- kLastRect.top + 1, kLastRect.width(),
+ getFont().drawString(&scratch, last, lastRect.left + 2,
+ lastRect.top + 1, lastRect.width(),
kInkColor);
if (blink && (field == kFieldFirst || field == kFieldLast)) {
const Common::Rect &fr =
- (field == kFieldFirst) ? kFirstRect : kLastRect;
+ (field == kFieldFirst) ? firstRect : lastRect;
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));
+ Common::Rect caret(caretX, fr.top, caretX + scaleX(6),
+ fr.bottom);
+ caret.clip(Common::Rect(sw, sh));
if (!caret.isEmpty())
scratch.fillRect(caret, kInkColor);
}
}
if (field == kFieldGender) {
- scratch.fillRect(kMaleBox, boxBlankColor);
- scratch.fillRect(kFemaleBox, boxBlankColor);
- scratch.fillRect(female ? kFemaleBox : kMaleBox,
+ scratch.fillRect(maleBox, boxBlankColor);
+ scratch.fillRect(femaleBox, boxBlankColor);
+ scratch.fillRect(female ? femaleBox : maleBox,
boxMarkColor);
}
CursorMan.showMouse(true);
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, kScreenWidth, kScreenHeight);
+ 0, 0, sw, sh);
if (!fadedIn) {
if (havePal)
fadePaletteFromBlack(pal);
@@ -1642,7 +1726,7 @@ void EEMEngine::showLondonCharSelect() {
if (field == kFieldGender && ev.type == Common::EVENT_LBUTTONDOWN) {
if (!genderReady)
continue;
- female = ev.mouse.x >= 160;
+ female = ev.mouse.x >= sw / 2;
done = true;
needRedraw = true;
break;
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index bf7a3fa83d7..2adb61beab7 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -414,6 +414,12 @@ public:
bool holdLastFrame = false, bool fadeIn = false,
bool setSkipIntroOnEsc = true);
+ /// Play a Flic (.FLC) movie, centred on the screen with a black
+ /// letterbox. The Mac CD ships its intro as a Flic (KDCDINTR.FLC) where
+ /// the DOS release uses .ANM movies. A click/key/Esc ends it (Esc also
+ /// sets `_skipIntro`).
+ void playFlc(const Common::Path &path, bool fadeIn = false);
+
private:
void interruptAudio(bool stopMusicToo = true);
Commit: 178a9f45d9a438a7e4fa4813784892ff8ab8ef53
https://github.com/scummvm/scummvm/commit/178a9f45d9a438a7e4fa4813784892ff8ab8ef53
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-25T12:55:09+02:00
Commit Message:
EEM: added missing intro playback animation in EEM2 Mac
Changed paths:
engines/eem/clues.cpp
engines/eem/detection.cpp
engines/eem/eem.cpp
engines/eem/eem.h
engines/eem/music.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index cfefa7a4a0a..f6b6bd48a19 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -254,12 +254,15 @@ void EEMEngine::doChoosePartner() {
int curMouseX = 0xa0;
uint level = happinessLevel(curMouseX, isLondon());
uint seqIdx = 0;
+ // EEM2 (London) Mac stores the partner heads at half resolution and
+ // pixel-doubles them to fill the hi-res passport-photo boxes.
+ const int faceScale = (isMacintosh() && isLondon()) ? 2 : 1;
blitAt(background, 0, 0);
- blitAt(jennyAnim[jennySeqs[level][seqIdx % 9] % jennyAnim.size()],
- jennyX, jennyY);
- blitAt(jakeAnim [jakeSeqs [level][seqIdx % 9] % jakeAnim.size()],
- jakeX, jakeY);
+ blitAtScaled(jennyAnim[jennySeqs[level][seqIdx % 9] % jennyAnim.size()],
+ jennyX, jennyY, faceScale);
+ blitAtScaled(jakeAnim [jakeSeqs [level][seqIdx % 9] % jakeAnim.size()],
+ jakeX, jakeY, faceScale);
g_system->updateScreen();
debugC(1, kDebugGeneral, "ChoosePartner: %u Jake frames at (%d,%d), "
@@ -273,10 +276,10 @@ void EEMEngine::doChoosePartner() {
lastTick = g_system->getMillis();
seqIdx = (seqIdx + 1) % 9;
blitAt(background, 0, 0);
- blitAt(jennyAnim[jennySeqs[level][seqIdx % 9] % jennyAnim.size()],
- jennyX, jennyY);
- blitAt(jakeAnim [jakeSeqs [level][seqIdx % 9] % jakeAnim.size()],
- jakeX, jakeY);
+ blitAtScaled(jennyAnim[jennySeqs[level][seqIdx % 9] % jennyAnim.size()],
+ jennyX, jennyY, faceScale);
+ blitAtScaled(jakeAnim [jakeSeqs [level][seqIdx % 9] % jakeAnim.size()],
+ jakeX, jakeY, faceScale);
g_system->updateScreen();
}
@@ -295,10 +298,10 @@ void EEMEngine::doChoosePartner() {
level = newLevel;
seqIdx = 0; // restart cycle so the gesture pops
blitAt(background, 0, 0);
- blitAt(jennyAnim[jennySeqs[level][seqIdx % 9] % jennyAnim.size()],
- jennyX, jennyY);
- blitAt(jakeAnim [jakeSeqs [level][seqIdx % 9] % jakeAnim.size()],
- jakeX, jakeY);
+ blitAtScaled(jennyAnim[jennySeqs[level][seqIdx % 9] % jennyAnim.size()],
+ jennyX, jennyY, faceScale);
+ blitAtScaled(jakeAnim [jakeSeqs [level][seqIdx % 9] % jakeAnim.size()],
+ jakeX, jakeY, faceScale);
g_system->updateScreen();
}
}
diff --git a/engines/eem/detection.cpp b/engines/eem/detection.cpp
index 725c386fa3a..b9d20900159 100644
--- a/engines/eem/detection.cpp
+++ b/engines/eem/detection.cpp
@@ -139,6 +139,17 @@ const ADGameDescription gameDescriptions[] = {
ADGF_UNSTABLE,
GUI_OPTIONS_EEM_MAC
},
+ {
+ // Alternate Macintosh CD mastering found in the local EEM_London image.
+ "eem2",
+ "CD",
+ AD_ENTRY2s("pics.dbd", "4ace2ae4fbe5877678c973066d025b4f", 10250928,
+ "m0.bin", "d38210ee60c478b14dc59a0a8ed7bb39", 37971),
+ Common::EN_ANY,
+ Common::kPlatformMacintosh,
+ ADGF_UNSTABLE,
+ GUI_OPTIONS_EEM_MAC
+ },
AD_TABLE_END_MARKER
};
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 35882ad6ea2..3a6432d2281 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -34,6 +34,7 @@
#include "engines/util.h"
#include "graphics/cursorman.h"
+#include "graphics/maccursor.h"
#include "graphics/paletteman.h"
#include "video/flic_decoder.h"
@@ -214,6 +215,22 @@ void setInteractiveCursorPalette(const Picture &cursor, byte transparent) {
}
void installMouseCursor(DBDArchive &pics, bool interactive, bool mac) {
+ if (mac && !interactive) {
+ // EEM2 (London) Mac keeps its pointers as 'crsr' colour cursors in the
+ // "EEM London CD" application fork; PIC 0x50 is only a 1x1 stub there.
+ // EEM1 Mac has no such file, so it falls through to the PIC path below.
+ Common::ScopedPtr<Common::SeekableReadStream> crsrStream(openMacResource(
+ Common::Path("EEM London CD"), MKTAG('c', 'r', 's', 'r'), 128));
+ if (crsrStream) {
+ Graphics::MacCursor macCursor;
+ if (macCursor.readFromStream(*crsrStream)) {
+ CursorMan.replaceCursor(&macCursor);
+ CursorMan.replaceCursorPalette(macCursor.getPalette(), 0, 256);
+ return;
+ }
+ }
+ }
+
Picture cursor;
if (!pics.getPicture(kPicMousePointer, cursor) || cursor.surface.empty())
error("EEM: mouse cursor PIC 0x%x missing", kPicMousePointer);
@@ -928,7 +945,8 @@ void EEMEngine::interruptAudio(bool stopMusicToo) {
_music->stop();
}
-void EEMEngine::playFlc(const Common::Path &path, bool fadeIn) {
+void EEMEngine::playFlc(const Common::Path &path, bool fadeIn,
+ bool holdLastFrame) {
Video::FlicDecoder flic;
if (!flic.loadFile(path)) {
warning("playFlc: %s missing", path.toString().c_str());
@@ -952,6 +970,7 @@ void EEMEngine::playFlc(const Common::Path &path, bool fadeIn) {
flic.start();
bool paletteApplied = false;
+ bool aborted = false;
while (!flic.endOfVideo() && !shouldQuit() && !_skipIntro) {
if (flic.needsUpdate()) {
const Graphics::Surface *frame = flic.decodeNextFrame();
@@ -974,16 +993,22 @@ void EEMEngine::playFlc(const Common::Path &path, bool fadeIn) {
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_LBUTTONDOWN) {
+ aborted = true;
return;
+ }
if (event.type == Common::EVENT_KEYDOWN) {
if (event.kbd.keycode == Common::KEYCODE_ESCAPE)
_skipIntro = true;
+ aborted = true;
return;
}
}
g_system->delayMillis(10);
}
+
+ if (holdLastFrame && !aborted && !shouldQuit() && !_skipIntro)
+ waitForInput(0xFFFFFFFFu);
}
void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs,
@@ -1097,6 +1122,35 @@ void EEMEngine::blitAt(const Picture &pic, int x, int y) {
x, y, w, h);
}
+void EEMEngine::blitAtScaled(const Picture &pic, int x, int y, int scale) {
+ x = scaleX(x);
+ y = scaleY(y);
+ const int dstW = pic.surface.w * scale;
+ const int dstH = pic.surface.h * scale;
+ const int w = MIN<int>(dstW, screenWidth() - x);
+ const int h = MIN<int>(dstH, screenHeight() - y);
+ if (w <= 0 || h <= 0)
+ return;
+ if (scale <= 1) {
+ g_system->copyRectToScreen(pic.surface.getPixels(), pic.surface.pitch,
+ x, y, w, h);
+ return;
+ }
+ // Nearest-neighbour pixel-double into a scratch, then present (opaque, to
+ // match blitAt: the sprite's surround index already matches the backdrop).
+ Graphics::ManagedSurface scaled(dstW, dstH,
+ Graphics::PixelFormat::createFormatCLUT8());
+ const byte *src = (const byte *)pic.surface.getPixels();
+ const int srcPitch = pic.surface.pitch;
+ for (int yy = 0; yy < dstH; yy++) {
+ byte *dst = (byte *)scaled.getBasePtr(0, yy);
+ const byte *srow = src + (yy / scale) * srcPitch;
+ for (int xx = 0; xx < dstW; xx++)
+ dst[xx] = srow[xx / scale];
+ }
+ g_system->copyRectToScreen(scaled.getPixels(), scaled.pitch, x, y, w, h);
+}
+
void EEMEngine::waitForInput(uint32 maxMs) {
setInteractiveMouseCursor(false);
const uint32 startMs = g_system->getMillis();
@@ -1509,7 +1563,55 @@ void EEMEngine::runMacStartup() {
_skipIntro = false;
}
-void EEMEngine::showLondonLogo(uint picId, uint palId, uint holdMs) {
+void EEMEngine::showLondonEAKidsLogo() {
+ Picture pic;
+ if (!_picsArchive.getPicture(0x54, pic)) {
+ warning("London EA Kids logo PIC 0x54 load failed");
+ return;
+ }
+ blitAt(pic, 0, 0);
+
+ byte fpal[kPalSize];
+ if (!getSitePalette(0x3c, fpal)) {
+ warning("London EA Kids palette 0x3c load failed");
+ return;
+ }
+
+ bool aborted = false;
+ int delayCount = 9;
+
+ for (uint pass = 0; pass < 2 && !aborted && !shouldQuit(); pass++) {
+ const bool show = (pass != 0);
+ for (uint frame = 0; frame < 0x37 && !aborted && !shouldQuit();
+ frame++) {
+ if (show && waitIntroDelay(40)) {
+ aborted = true;
+ break;
+ }
+
+ openColorCycle(fpal, 0x01, 0x6e, show);
+ openColorCycle(fpal, 0x81, 0xee, show);
+ if (--delayCount == 0) {
+ delayCount = 9;
+ openColorCycle(fpal, 0x70, 0x80, show);
+ }
+ if (show)
+ g_system->updateScreen();
+ }
+ }
+
+ if (!aborted && !shouldQuit()) {
+ for (uint i = 0; i < 5; i++)
+ openColorCycle(fpal, 0x70, 0x80, true);
+ g_system->updateScreen();
+ waitIntroDelay(0x5a * 40);
+ }
+
+ fadeCurrentPaletteToBlack();
+}
+
+void EEMEngine::showLondonLogo(uint picId, uint palId, uint holdMs,
+ bool playThunder) {
Picture pic;
if (!_picsArchive.getPicture(picId, pic) || pic.surface.empty()) {
warning("London logo PIC 0x%x load failed", picId);
@@ -1527,7 +1629,12 @@ void EEMEngine::showLondonLogo(uint picId, uint palId, uint holdMs) {
g_system->updateScreen();
fadePaletteFromBlack(target);
+ if (playThunder && _audio && !isMacintosh())
+ _audio->playVoc(Common::Path("THUNDER.VOC"));
+
waitForInput(holdMs);
+ if (_audio)
+ _audio->stopVoice();
fadeCurrentPaletteToBlack();
}
@@ -1552,17 +1659,26 @@ void EEMEngine::runLondonStartup() {
_skipIntro = false;
debugC(1, kDebugGeneral, "EEM2 (London): opening sequence");
- // Two still logos.
+ // Opening logos. Mac London mirrors FUN_00009256: EA Kids colour-cycle,
+ // publisher still, Storm still, then the FLC intro/title pair.
if (!shouldQuit() && !_skipIntro)
- showLondonLogo(0x54, 0x3c, 2500); // EA Kids
+ showLondonEAKidsLogo();
if (!shouldQuit() && !_skipIntro)
- showLondonLogo(0x20c, 0x3e, 2500); // publisher logo (FUN_2721_07be)
+ showLondonLogo(0x20c, 0x3e, 3000); // publisher logo (FUN_00009074)
+ if (!shouldQuit() && !_skipIntro)
+ showLondonLogo(0x20b, 0x3d, 3000, /* playThunder= */ true);
if (isMacintosh()) {
- // The Mac CD ships the post-logo intro as a single Flic movie
- // (KDCDINTR.FLC) instead of the DOS bolt/movie/wave .ANM sequence.
+ // The Mac CD ships the post-logo intro as Flic movies where DOS uses
+ // bolt/movie/wave .ANM. KDCDINTR is the centered intro; BOOK54 is the
+ // full-screen animated title, held for the profile click/key.
if (!shouldQuit() && !_skipIntro)
playFlc(Common::Path("KDCDINTR.FLC"), /* fadeIn= */ true);
+ if (!shouldQuit() && !_skipIntro && _music)
+ _music->playFile(Common::Path("MUS00102.XMI"), /* loop= */ true);
+ if (!shouldQuit() && !_skipIntro)
+ playFlc(Common::Path("BOOK54.FLC"), /* fadeIn= */ true,
+ /* holdLastFrame= */ true);
} else {
// Storm Software â bolt.anm with the thunder roar.
if (!shouldQuit() && !_skipIntro) {
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 2adb61beab7..90a600c58c1 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -401,6 +401,12 @@ private:
void blitAt(const Picture &pic, int x, int y);
+ /// Like blitAt, but pixel-doubles the sprite by an integer @p scale. The
+ /// Mac CD authors some sprites (e.g. the choose-partner heads) at half
+ /// resolution and draws them scaled to fill the hi-res art. @p scale == 1
+ /// is identical to blitAt.
+ void blitAtScaled(const Picture &pic, int x, int y, int scale);
+
public:
void waitForInput(uint32 maxMs);
@@ -418,7 +424,8 @@ public:
/// letterbox. The Mac CD ships its intro as a Flic (KDCDINTR.FLC) where
/// the DOS release uses .ANM movies. A click/key/Esc ends it (Esc also
/// sets `_skipIntro`).
- void playFlc(const Common::Path &path, bool fadeIn = false);
+ void playFlc(const Common::Path &path, bool fadeIn = false,
+ bool holdLastFrame = false);
private:
void interruptAudio(bool stopMusicToo = true);
@@ -437,7 +444,9 @@ private:
void runLondonStartup();
/// Start London mystery 0 after a freshly-created detective chooses a partner.
bool startLondonTrainingMystery();
- void showLondonLogo(uint picId, uint palId, uint holdMs);
+ void showLondonEAKidsLogo();
+ void showLondonLogo(uint picId, uint palId, uint holdMs,
+ bool playThunder = false);
void showLondonCharSelect();
void playLondonInitCluesAnim(uint16 caseType, const Picture &bg,
bool haveBriefingBg);
diff --git a/engines/eem/music.cpp b/engines/eem/music.cpp
index 9d41fc64d8b..61bff51bc2d 100644
--- a/engines/eem/music.cpp
+++ b/engines/eem/music.cpp
@@ -51,19 +51,29 @@ Common::String musicNameFromPath(const Common::Path &path) {
}
Common::SeekableReadStream *openMacMidiResource(uint16 resourceId) {
- Common::SeekableReadStream *stream =
- openMacResource(Common::Path("EEM Sound&Music"),
- MKTAG('c', 'm', 'i', 'd'), resourceId);
- if (stream)
- return stream;
-
- stream = openMacResource(Common::Path("EEM Sound&Music"),
- MKTAG('M', 'I', 'D', 'I'), resourceId);
- if (stream)
- return stream;
-
- return openMacResource(Common::Path("EEM Sound&Music"),
- MKTAG('M', 'i', 'd', 'i'), resourceId);
+ static const char *const kMacMusicForks[] = {
+ "EEM Sound&Music",
+ "rsrc/EEM Sound&Music",
+ "EEM London CD",
+ "rsrc/EEM London CD",
+ };
+ static const uint32 kMacMidiTypes[] = {
+ MKTAG('c', 'm', 'i', 'd'),
+ MKTAG('M', 'I', 'D', 'I'),
+ MKTAG('M', 'i', 'd', 'i'),
+ };
+
+ for (uint i = 0; i < ARRAYSIZE(kMacMusicForks); i++) {
+ for (uint j = 0; j < ARRAYSIZE(kMacMidiTypes); j++) {
+ Common::SeekableReadStream *stream =
+ openMacResource(Common::Path(kMacMusicForks[i]),
+ kMacMidiTypes[j], resourceId);
+ if (stream)
+ return stream;
+ }
+ }
+
+ return nullptr;
}
uint16 macSongResourceIdForFile(const Common::Path &path) {
@@ -84,6 +94,12 @@ uint16 macSongResourceIdForFile(const Common::Path &path) {
return 1005;
if (name == "WRONG2")
return 1007;
+ if (name == "MUS00101")
+ return 1101;
+ if (name == "MUS00102")
+ return 1102;
+ if (name == "MUS00103")
+ return 1103;
return kInvalidMacSongResource;
}
@@ -249,9 +265,18 @@ bool MusicPlayer::loadMacSong(uint16 resourceId, uint16 &midiId) {
midiId = kInvalidMacMidiResource;
clearMacInstrumentMap();
- Common::SeekableReadStream *stream =
- openMacResource(Common::Path("EEM Sound&Music"),
- MKTAG('S', 'O', 'N', 'G'), resourceId);
+ static const char *const kMacMusicForks[] = {
+ "EEM Sound&Music",
+ "rsrc/EEM Sound&Music",
+ "EEM London CD",
+ "rsrc/EEM London CD",
+ };
+
+ Common::SeekableReadStream *stream = nullptr;
+ for (uint i = 0; i < ARRAYSIZE(kMacMusicForks) && !stream; i++) {
+ stream = openMacResource(Common::Path(kMacMusicForks[i]),
+ MKTAG('S', 'O', 'N', 'G'), resourceId);
+ }
if (!stream) {
warning("MusicPlayer: Mac SONG resource %u missing", resourceId);
return false;
Commit: e3047778c2ac6be52fceb3537042476106c64c60
https://github.com/scummvm/scummvm/commit/e3047778c2ac6be52fceb3537042476106c64c60
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-25T12:55:09+02:00
Commit Message:
EEM: fixed parner selection in EEM2 Mac
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 f6b6bd48a19..9651e619dda 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -70,6 +70,13 @@ const int kLondonJakeY = 0x3a;
const int kLondonJennyX = 0xac;
const int kLondonJennyY = 0x3a;
+// Mac EEM2 `_DoChoosePartner` creates the animation objects at native
+// 512x384 coordinates: ANI 8 at 0x21,0xa5 and ANI 9 at 0x12d,0xa4.
+const int kMacLondonJakeX = 0x21;
+const int kMacLondonJakeY = 0xa5;
+const int kMacLondonJennyX = 0x12d;
+const int kMacLondonJennyY = 0xa4;
+
uint markClueBlockNotebookEntries(Mystery &mystery, const byte *clueBlock,
bool isLondon) {
if (!clueBlock)
@@ -220,6 +227,15 @@ void blitRawToScreen(const Picture &p, int x, int y) {
x, y, w, h);
}
+void blitMacMaskedToScreen(const Picture &pic, int x, int y) {
+ Graphics::Surface *screen = g_system->lockScreen();
+ if (!screen)
+ return;
+
+ blitMacMaskedSurface(screen, pic, x, y);
+ g_system->unlockScreen();
+}
+
// _DoChoosePartner @ 1a35:0756 / EEM2 @ 1abf:0728.
void EEMEngine::doChoosePartner() {
_partner = kPartnerJake;
@@ -245,24 +261,32 @@ void EEMEngine::doChoosePartner() {
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 bool macLondon = isMacintosh() && isLondon();
+ const int jakeX = macLondon ? kMacLondonJakeX
+ : (isLondon() ? kLondonJakeX : kJakeX);
+ const int jakeY = macLondon ? kMacLondonJakeY
+ : (isLondon() ? kLondonJakeY : kJakeY);
+ const int jennyX = macLondon ? kMacLondonJennyX
+ : (isLondon() ? kLondonJennyX : kJennyX);
+ const int jennyY = macLondon ? kMacLondonJennyY
+ : (isLondon() ? kLondonJennyY : kJennyY);
const uint8 (*jakeSeqs)[9] = isLondon() ? kLondonJakeSeqs : kJakeSeqs;
const uint8 (*jennySeqs)[9] = isLondon() ? kLondonJennySeqs : kJennySeqs;
int curMouseX = 0xa0;
uint level = happinessLevel(curMouseX, isLondon());
uint seqIdx = 0;
- // EEM2 (London) Mac stores the partner heads at half resolution and
- // pixel-doubles them to fill the hi-res passport-photo boxes.
- const int faceScale = (isMacintosh() && isLondon()) ? 2 : 1;
+ auto blitPartnerFrame = [&](const Picture &pic, int x, int y) {
+ if (macLondon)
+ blitMacMaskedToScreen(pic, x, y);
+ else
+ blitAtScaled(pic, x, y, 1);
+ };
blitAt(background, 0, 0);
- blitAtScaled(jennyAnim[jennySeqs[level][seqIdx % 9] % jennyAnim.size()],
- jennyX, jennyY, faceScale);
- blitAtScaled(jakeAnim [jakeSeqs [level][seqIdx % 9] % jakeAnim.size()],
- jakeX, jakeY, faceScale);
+ blitPartnerFrame(jennyAnim[jennySeqs[level][seqIdx % 9] % jennyAnim.size()],
+ jennyX, jennyY);
+ blitPartnerFrame(jakeAnim [jakeSeqs [level][seqIdx % 9] % jakeAnim.size()],
+ jakeX, jakeY);
g_system->updateScreen();
debugC(1, kDebugGeneral, "ChoosePartner: %u Jake frames at (%d,%d), "
@@ -276,10 +300,10 @@ void EEMEngine::doChoosePartner() {
lastTick = g_system->getMillis();
seqIdx = (seqIdx + 1) % 9;
blitAt(background, 0, 0);
- blitAtScaled(jennyAnim[jennySeqs[level][seqIdx % 9] % jennyAnim.size()],
- jennyX, jennyY, faceScale);
- blitAtScaled(jakeAnim [jakeSeqs [level][seqIdx % 9] % jakeAnim.size()],
- jakeX, jakeY, faceScale);
+ blitPartnerFrame(jennyAnim[jennySeqs[level][seqIdx % 9] % jennyAnim.size()],
+ jennyX, jennyY);
+ blitPartnerFrame(jakeAnim [jakeSeqs [level][seqIdx % 9] % jakeAnim.size()],
+ jakeX, jakeY);
g_system->updateScreen();
}
@@ -298,10 +322,10 @@ void EEMEngine::doChoosePartner() {
level = newLevel;
seqIdx = 0; // restart cycle so the gesture pops
blitAt(background, 0, 0);
- blitAtScaled(jennyAnim[jennySeqs[level][seqIdx % 9] % jennyAnim.size()],
- jennyX, jennyY, faceScale);
- blitAtScaled(jakeAnim [jakeSeqs [level][seqIdx % 9] % jakeAnim.size()],
- jakeX, jakeY, faceScale);
+ blitPartnerFrame(jennyAnim[jennySeqs[level][seqIdx % 9] % jennyAnim.size()],
+ jennyX, jennyY);
+ blitPartnerFrame(jakeAnim [jakeSeqs [level][seqIdx % 9] % jakeAnim.size()],
+ jakeX, jakeY);
g_system->updateScreen();
}
}
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 3a6432d2281..3c75db5cb99 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -89,6 +89,13 @@ const uint16 kLondonCursorPics[7] = {
0x50, 0x51, 0x206, 0xa1, 0x207, 0x20b, 0x35e
};
+// Mac EEM2 CODE resource 6 loads five Color QuickDraw cursors with
+// GetCCursor: 128, 138, 139, 129 and 132. Its SwitchMouse branch table maps
+// index 0/1/default to 132, 2 to 128, 3 to 129, and 4 to the active partner.
+const uint16 kLondonMacCursorCrsrs[7] = {
+ 132, 132, 128, 129, 138, 139, 132
+};
+
const byte kSaveBodyVer = 1;
// Test switch: populate ScrapBook 1 at startup without exposing a game
@@ -214,23 +221,39 @@ void setInteractiveCursorPalette(const Picture &cursor, byte transparent) {
CursorMan.replaceCursorPalette(palette, 0, 256);
}
-void installMouseCursor(DBDArchive &pics, bool interactive, bool mac) {
- if (mac && !interactive) {
- // EEM2 (London) Mac keeps its pointers as 'crsr' colour cursors in the
- // "EEM London CD" application fork; PIC 0x50 is only a 1x1 stub there.
- // EEM1 Mac has no such file, so it falls through to the PIC path below.
- Common::ScopedPtr<Common::SeekableReadStream> crsrStream(openMacResource(
- Common::Path("EEM London CD"), MKTAG('c', 'r', 's', 'r'), 128));
+bool installMacLondonCursor(uint16 resourceId) {
+ static const char *const kAppForks[] = {
+ "EEM London CD",
+ "rsrc/EEM London CD"
+ };
+
+ for (uint i = 0; i < ARRAYSIZE(kAppForks); i++) {
+ Common::ScopedPtr<Common::SeekableReadStream> crsrStream(
+ openMacResource(Common::Path(kAppForks[i]),
+ MKTAG('c', 'r', 's', 'r'), resourceId));
if (crsrStream) {
Graphics::MacCursor macCursor;
if (macCursor.readFromStream(*crsrStream)) {
CursorMan.replaceCursor(&macCursor);
CursorMan.replaceCursorPalette(macCursor.getPalette(), 0, 256);
- return;
+ return true;
}
}
}
+ return false;
+}
+
+void installMouseCursor(DBDArchive &pics, bool interactive, bool mac,
+ bool london) {
+ if (mac && london) {
+ // EEM2 (London) Mac keeps pointers as 'crsr' colour cursors in the
+ // application resource fork; the DOS cursor PIC ids are mostly 1x1
+ // stubs or unrelated full-screen pictures in this release.
+ if (installMacLondonCursor(kLondonMacCursorCrsrs[0]))
+ return;
+ }
+
Picture cursor;
if (!pics.getPicture(kPicMousePointer, cursor) || cursor.surface.empty())
error("EEM: mouse cursor PIC 0x%x missing", kPicMousePointer);
@@ -463,7 +486,7 @@ Common::Error EEMEngine::run() {
_audio->setVoiceEnabled(_voiceOn);
syncSoundSettings();
- installMouseCursor(_picsArchive, false, isMacintosh());
+ installMouseCursor(_picsArchive, false, isMacintosh(), isLondon());
CursorMan.showMouse(false);
// _AllBlack @ 172b:0d4b.
@@ -765,7 +788,7 @@ void EEMEngine::setInteractiveMouseCursor(bool active) {
return;
_interactiveMouseCursor = active;
- installMouseCursor(_picsArchive, active, isMacintosh());
+ installMouseCursor(_picsArchive, active, isMacintosh(), isLondon());
// The red-outline highlight replaced any London cursor shape; force the
// next setSiteHotspotCursorId to reinstall.
_siteCursorId = -1;
@@ -783,13 +806,27 @@ void EEMEngine::setHotspotMouseCursor(bool active) {
void EEMEngine::setSiteHotspotCursorId(int cursorId) {
if (!isLondon())
return;
- if (cursorId == 4 && _partner == kPartnerJenny)
- cursorId = 5;
if (cursorId < 0 || cursorId >= (int)ARRAYSIZE(kLondonCursorPics))
cursorId = 0;
if (cursorId == _siteCursorId)
return;
+ if (isMacintosh()) {
+ uint16 resourceId = kLondonMacCursorCrsrs[cursorId];
+ if (cursorId == 4 || cursorId == 5)
+ resourceId = (_partner == kPartnerJenny) ? 139 : 138;
+ if (!installMacLondonCursor(resourceId)) {
+ warning("EEM2 Mac: cursor %d ('crsr' %u) missing",
+ cursorId, resourceId);
+ return;
+ }
+ _siteCursorId = cursorId;
+ _interactiveMouseCursor = false;
+ return;
+ }
+
+ if (cursorId == 4 && _partner == kPartnerJenny)
+ cursorId = 5;
Picture cursor;
if (!_picsArchive.getPicture(kLondonCursorPics[cursorId], cursor) ||
cursor.surface.empty()) {
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 90a600c58c1..24e1ab8cfdf 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -193,16 +193,16 @@ public:
const EEMFont &getFont() const { return _font; }
uint8 getPartnerIndex() const { return _partner; }
- /// Red-outline cursor for interactive regions without original highlight art.
+ /// Interactive-region cursor. DOS/EEM1 uses a red-outline pointer; Mac
+ /// London uses the original Color QuickDraw arrow.
void setInteractiveMouseCursor(bool active);
/// 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.
+ /// EEM2 `_SwitchMouse`: swap the cursor shape by ID. DOS uses the seven
+ /// PIC-backed cursor slots; Mac London maps the same IDs through the
+ /// original `crsr` resources loaded by the application.
void setSiteHotspotCursorId(int cursorId);
/// `_DisplayClue @ 2404:05e6`.
Commit: b36e3a1eefff1e68acd3aaa571623b04305606f1
https://github.com/scummvm/scummvm/commit/b36e3a1eefff1e68acd3aaa571623b04305606f1
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-25T12:55:09+02:00
Commit Message:
EEM: start rendering case introduction in EEM2 Mac
Changed paths:
engines/eem/clues.cpp
engines/eem/graphics.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 9651e619dda..fa3d05d5e82 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -240,9 +240,10 @@ void blitMacMaskedToScreen(const Picture &pic, int x, int y) {
void EEMEngine::doChoosePartner() {
_partner = kPartnerJake;
+ const uint chooseBgId = kPicChooseBackground;
Picture background;
- if (!_picsArchive.getPicture(kPicChooseBackground, background)) {
- warning("ChoosePartner background (%u) load failed", kPicChooseBackground);
+ if (!_picsArchive.getPicture(chooseBgId, background)) {
+ warning("ChoosePartner background (%u) load failed", chooseBgId);
return;
}
@@ -272,7 +273,7 @@ void EEMEngine::doChoosePartner() {
: (isLondon() ? kLondonJennyY : kJennyY);
const uint8 (*jakeSeqs)[9] = isLondon() ? kLondonJakeSeqs : kJakeSeqs;
const uint8 (*jennySeqs)[9] = isLondon() ? kLondonJennySeqs : kJennySeqs;
- int curMouseX = 0xa0;
+ int curMouseX = macLondon ? unscaleX(0xa0) : 0xa0;
uint level = happinessLevel(curMouseX, isLondon());
uint seqIdx = 0;
auto blitPartnerFrame = [&](const Picture &pic, int x, int y) {
@@ -375,11 +376,20 @@ void EEMEngine::doChoosePartner() {
// EEM2 case-intro animation â `_DoInitClues` @ 1abf:03b3.
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)
+ const bool mac = isMacintosh();
+ const uint introAni = mac ? 0x17
+ : (_partner == kPartnerJake ? 0x18 : 0x71);
+ const uint introScript = mac ? (_partner == kPartnerJake ? 0x18 : 0x1c)
+ : 0x18;
+ const int kAnchorX = mac ? 0x14c : 0xd2;
+ const int kAnchorY = mac ? (_partner == kPartnerJake ? 0x78 : 0x76)
+ : 0x3f;
Animation anim;
const bool haveAnim =
_aniArchive.loadAnimation(introAni, anim) && !anim.empty();
+ MacSpritePaletteMap macPaletteMap = {0x00, 0xFF};
+ if (mac)
+ macPaletteMap = getMacSpritePaletteMap();
// `_DoInitClues @ 1abf:03b3` registers a SECOND, fixed briefing character
// (Nigel) on the LEFT, gated on caseType (jumptable @ CS:0x720):
@@ -412,14 +422,24 @@ void EEMEngine::playLondonInitCluesAnim(uint16 caseType, const Picture &bg,
if (scr) {
if (haveAnim) {
const uint cell =
- partnerFrameAtTick(0x18, (uint)anim.size(), frame * 140);
- blitAnimFrameAnchored(scr, anim[cell], kAnchorX, kAnchorY);
+ partnerFrameAtTick((uint16)introScript,
+ (uint)anim.size(), frame * 140);
+ if (mac)
+ blitMacAnimFrameAnchored(scr, anim[cell],
+ kAnchorX, kAnchorY,
+ macPaletteMap);
+ else
+ blitAnimFrameAnchored(scr, anim[cell], kAnchorX, kAnchorY);
}
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);
+ if (mac)
+ blitMacAnimFrameAnchored(scr, npc[ncell], npcX, npcY,
+ macPaletteMap);
+ else
+ blitAnimFrameAnchored(scr, npc[ncell], npcX, npcY);
}
g_system->unlockScreen();
}
@@ -666,13 +686,14 @@ void EEMEngine::doInitClues() {
return;
const bool floppy = isFloppy();
- const bool compactInit = floppy || isMacintosh();
+ const bool compactMac = isMacintosh() && _mystery.usesCompactMacData();
+ const bool compactInit = floppy || compactMac;
const uint16 caseType = compactInit ? (uint16)ib[0] : READ_LE_UINT16(ib);
if (!floppy) {
- const uint16 startSite = isMacintosh() ? (uint16)ib[1]
- : READ_LE_UINT16(ib + 2);
- if (isMacintosh()) {
+ const uint16 startSite = compactMac ? (uint16)ib[1]
+ : READ_LE_UINT16(ib + 2);
+ if (compactMac) {
const uint sites = _mystery.numSites();
for (uint s = 0; s < sites && s < Mystery::kVisitedSiteCap; s++)
_mystery._onSites[s] = 1;
@@ -716,7 +737,7 @@ void EEMEngine::doInitClues() {
// 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 || isMacintosh()) {
+ if (floppy || compactMac) {
displayFloppyBriefing(ib);
} else {
const byte *briefingClues = ib + 4;
@@ -887,9 +908,18 @@ void EEMEngine::displayClue(const byte *clueBlock) {
return;
const uint stride = isLondon() ? 0x54 : 62;
+ const bool mac = isMacintosh();
+ const int sw = screenWidth();
+ const int sh = screenHeight();
+ MacSpritePaletteMap macPaletteMap = {0x00, 0xFF};
+ if (mac)
+ macPaletteMap = getMacSpritePaletteMap();
+ const EEMFont &dialogFont =
+ (mac && _dialogFont.isLoaded()) ? _dialogFont : _font;
+ const byte textColor = mac ? macPaletteMap.black : 0;
// Snapshot BG so per-entry character pics don't stack.
- Graphics::ManagedSurface bg(kScreenWidth, kScreenHeight,
+ Graphics::ManagedSurface bg(sw, sh,
Graphics::PixelFormat::createFormatCLUT8());
bg.clear();
{
@@ -911,7 +941,7 @@ void EEMEngine::displayClue(const byte *clueBlock) {
// +0x30..+0x39: 5 notebook entries (-1 terminated)
// +0x3a..+0x3b: KD-anim number (-1 = none)
for (uint i = 0; i < number && !shouldQuit(); i++) {
- g_system->copyRectToScreen(bg.getPixels(), bg.pitch, 0, 0, kScreenWidth, kScreenHeight);
+ g_system->copyRectToScreen(bg.getPixels(), bg.pitch, 0, 0, sw, sh);
const byte *c = clueBlock + 4 + i * stride;
if (isLondon() && _music && _musicOn) {
@@ -932,10 +962,10 @@ void EEMEngine::displayClue(const byte *clueBlock) {
// Animate the gesture over the partner-less scene so it doesn't ghost
// the static partner.
- if (haveKd && _partnerEraseBg.w == kScreenWidth &&
- _partnerEraseBg.h == kScreenHeight) {
+ if (haveKd && _partnerEraseBg.w == sw &&
+ _partnerEraseBg.h == sh) {
g_system->copyRectToScreen(_partnerEraseBg.getPixels(),
- _partnerEraseBg.pitch, 0, 0, kScreenWidth, kScreenHeight);
+ _partnerEraseBg.pitch, 0, 0, sw, sh);
}
const bool useP1 = (_partner == kPartnerJenny) &&
@@ -962,12 +992,15 @@ void EEMEngine::displayClue(const byte *clueBlock) {
if (charPicId != 0 && charPicId != 0xFFFF) {
Picture charPic;
if (_picsArchive.getPicture(charPicId, charPic) &&
- charX < kScreenWidth && charY < kScreenHeight) {
+ charX < sw && charY < sh) {
// 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).
- blitMaskedToScreen(charPic, charX, charY);
+ if (mac)
+ blitMacMaskedToScreen(charPic, charX, charY);
+ else
+ blitMaskedToScreen(charPic, charX, charY);
}
}
@@ -982,30 +1015,35 @@ void EEMEngine::displayClue(const byte *clueBlock) {
_balloonArchive.size() > balloonId &&
_balloonArchive.loadEntry(balloonId, balloon);
- if (_font.isLoaded() && !text.empty()) {
+ if (dialogFont.isLoaded() && !text.empty()) {
Graphics::Surface *screen = g_system->lockScreen();
if (!screen)
break;
- Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+ Graphics::ManagedSurface scratch(sw, sh,
Graphics::PixelFormat::createFormatCLUT8());
scratch.simpleBlitFrom(*screen);
g_system->unlockScreen();
int textX = bubX;
int textY = bubY;
- int textW = MIN<int>(kScreenWidth - bubX, 200);
+ int textW = MIN<int>(sw - bubX, 200);
int copyY = bubY;
- int copyH = _font.getFontHeight() * 4 + 8;
+ int copyH = dialogFont.getFontHeight() * 4 + 8;
if (haveBalloon) {
- const int bw = MIN<int>(balloon.surface.w, kScreenWidth - bubX);
- const int bh = MIN<int>(balloon.surface.h, kScreenHeight - bubY);
+ const int bw = MIN<int>(balloon.surface.w, sw - bubX);
+ const int bh = MIN<int>(balloon.surface.h, sh - bubY);
const byte transp = (byte)(balloon.flags >> 8);
const bool flipBalloon = (fittedBubNum & 0x80) != 0;
if (bw > 0 && bh > 0) {
- scratch.transBlitFrom(balloon.surface,
- Common::Point(bubX, bubY),
- transp, flipBalloon);
+ if (mac)
+ blitMacMaskedSurface(scratch.surfacePtr(), balloon,
+ bubX, bubY, flipBalloon,
+ macPaletteMap);
+ else
+ scratch.transBlitFrom(balloon.surface,
+ Common::Point(bubX, bubY),
+ transp, flipBalloon);
}
uint16 insetX = 5;
uint16 insetY = 4;
@@ -1016,21 +1054,25 @@ void EEMEngine::displayClue(const byte *clueBlock) {
textW = insetW;
copyH = bh;
} else {
- const Common::Rect band(0, bubY, kScreenWidth,
- MIN<int>(bubY + copyH, kScreenHeight));
+ const Common::Rect band(0, bubY, sw,
+ MIN<int>(bubY + copyH, sh));
scratch.fillRect(band, 0);
copyY = bubY;
}
- _font.drawWordWrapped(&scratch, textX, textY,
- MAX<int>(8, textW), text, 0);
+ if (mac && textColor != 0xFF) {
+ dialogFont.drawWordWrapped(&scratch, textX, textY,
+ MAX<int>(8, textW), text, 0xFF);
+ }
+ dialogFont.drawWordWrapped(&scratch, textX, textY,
+ MAX<int>(8, textW), text, textColor);
- copyY = CLIP<int>(copyY, 0, kScreenHeight - 1);
- const int copyRows = CLIP<int>(MIN<int>(copyH, kScreenHeight - copyY),
- 0, kScreenHeight - copyY);
+ copyY = CLIP<int>(copyY, 0, sh - 1);
+ const int copyRows = CLIP<int>(MIN<int>(copyH, sh - copyY),
+ 0, sh - copyY);
if (copyRows > 0) {
g_system->copyRectToScreen(scratch.getBasePtr(0, copyY),
- scratch.pitch, 0, copyY, kScreenWidth, copyRows);
+ scratch.pitch, 0, copyY, sw, copyRows);
// Gesture entry: let the wait loop present, so the partner-less
// base isn't flashed before the gesture's first frame.
if (!haveKd)
@@ -1055,7 +1097,7 @@ void EEMEngine::displayClue(const byte *clueBlock) {
setInteractiveMouseCursor(false);
bool advance = false;
bool skipAll = false;
- Graphics::ManagedSurface kdBase(kScreenWidth, kScreenHeight,
+ Graphics::ManagedSurface kdBase(sw, sh,
Graphics::PixelFormat::createFormatCLUT8());
bool haveKdBase = false;
uint kdLastFrame = (uint)-1;
@@ -1128,13 +1170,17 @@ void EEMEngine::displayClue(const byte *clueBlock) {
f < idleAnim.size()) {
kdInIdle = true;
kdLastIdleFrame = f;
- Graphics::ManagedSurface comp(kScreenWidth, kScreenHeight,
+ Graphics::ManagedSurface comp(sw, sh,
Graphics::PixelFormat::createFormatCLUT8());
comp.simpleBlitFrom(kdBase);
- blitAnimFrameAnchored(comp.surfacePtr(),
- idleAnim[f], idleX, idleY);
+ if (mac)
+ blitMacAnimFrameAnchored(comp.surfacePtr(),
+ idleAnim[f], idleX, idleY, macPaletteMap);
+ else
+ blitAnimFrameAnchored(comp.surfacePtr(),
+ idleAnim[f], idleX, idleY);
g_system->copyRectToScreen(comp.getPixels(), comp.pitch,
- 0, 0, kScreenWidth, kScreenHeight);
+ 0, 0, sw, sh);
}
} else {
// Gesture one-shot.
@@ -1142,13 +1188,17 @@ void EEMEngine::displayClue(const byte *clueBlock) {
(uint)kdAnim.size(), kdElapsed);
if (f != kdLastFrame && f < kdAnim.size()) {
kdLastFrame = f;
- Graphics::ManagedSurface comp(kScreenWidth, kScreenHeight,
+ Graphics::ManagedSurface comp(sw, sh,
Graphics::PixelFormat::createFormatCLUT8());
comp.simpleBlitFrom(kdBase);
- blitAnimFrameAnchored(comp.surfacePtr(),
- kdAnim[f], kdPx, kdPy);
+ if (mac)
+ blitMacAnimFrameAnchored(comp.surfacePtr(),
+ kdAnim[f], kdPx, kdPy, macPaletteMap);
+ else
+ blitAnimFrameAnchored(comp.surfacePtr(),
+ kdAnim[f], kdPx, kdPy);
g_system->copyRectToScreen(comp.getPixels(), comp.pitch,
- 0, 0, kScreenWidth, kScreenHeight);
+ 0, 0, sw, sh);
}
}
}
@@ -1168,7 +1218,7 @@ void EEMEngine::displayClue(const byte *clueBlock) {
const uint16 puzzleId = READ_LE_UINT16(c + 0x50);
if (puzzleId != 0xFFFF) {
g_system->copyRectToScreen(bg.getPixels(), bg.pitch, 0, 0,
- kScreenWidth, kScreenHeight);
+ sw, sh);
g_system->updateScreen();
_mystery._solvedPuzzle = doPuzzle(puzzleId);
if (!_mystery._solvedPuzzle)
diff --git a/engines/eem/graphics.cpp b/engines/eem/graphics.cpp
index 860f3fdf40d..6f220e2e26a 100644
--- a/engines/eem/graphics.cpp
+++ b/engines/eem/graphics.cpp
@@ -187,20 +187,61 @@ static Common::String readPuzzleLine(Common::File &f) {
}
return s;
}
+
+struct PuzzleFilePattern {
+ const char *pattern;
+ bool bigEndian;
+};
+
+static bool openPuzzleFile(uint puzzleId, bool macintosh, Common::File &f,
+ Common::String &fname, bool &bigEndian) {
+ static const PuzzleFilePattern kDosPatterns[] = {
+ { "P%u.BIN", false }
+ };
+ static const PuzzleFilePattern kMacPatterns[] = {
+ { "Puzzles/p%u.bin", true },
+ { "Puzzles/P%u.BIN", true },
+ { "p%u.bin", true },
+ { "P%u.BIN", false }
+ };
+
+ const PuzzleFilePattern *patterns = macintosh ? kMacPatterns
+ : kDosPatterns;
+ const uint patternCount = macintosh ? ARRAYSIZE(kMacPatterns)
+ : ARRAYSIZE(kDosPatterns);
+ for (uint i = 0; i < patternCount; i++) {
+ fname = Common::String::format(patterns[i].pattern, puzzleId);
+ if (f.open(Common::Path(fname))) {
+ bigEndian = patterns[i].bigEndian;
+ return true;
+ }
+ }
+ bigEndian = false;
+ return false;
+}
+
+static uint16 readPuzzleU16(Common::File &f, bool bigEndian) {
+ return bigEndian ? f.readUint16BE() : f.readUint16LE();
+}
+
// `_DoPuzzle @ 2542:1482`.
bool EEMEngine::doPuzzle(uint puzzleId) {
Common::File f;
- const Common::String fname = Common::String::format("P%u.BIN", puzzleId);
- if (!f.open(Common::Path(fname))) {
+ Common::String fname;
+ bool bigEndian = false;
+ if (!openPuzzleFile(puzzleId, isMacintosh(), f, fname, bigEndian)) {
// Fail open: never let a missing puzzle file permanently block a clue.
- warning("doPuzzle: %s missing â leaving the clue ungated", fname.c_str());
+ warning("doPuzzle: puzzle %u missing â leaving the clue ungated",
+ puzzleId);
return true;
}
- const uint16 type = f.readUint16LE();
+ const uint16 type = readPuzzleU16(f, bigEndian);
+ const int sw = screenWidth();
+ const int sh = screenHeight();
// the puzzle pics nor any bubble is left on the background.
- Graphics::ManagedSurface cleanBg(kScreenWidth, kScreenHeight,
+ Graphics::ManagedSurface cleanBg(sw, sh,
Graphics::PixelFormat::createFormatCLUT8());
cleanBg.clear();
{
@@ -210,18 +251,18 @@ bool EEMEngine::doPuzzle(uint puzzleId) {
g_system->unlockScreen();
}
}
- Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+ Graphics::ManagedSurface scratch(sw, sh,
Graphics::PixelFormat::createFormatCLUT8());
scratch.simpleBlitFrom(cleanBg);
for (int phase = 0; phase < 2; phase++) {
uint16 n = 1;
if (phase == 1)
- n = f.readUint16LE();
+ n = readPuzzleU16(f, bigEndian);
for (uint16 i = 0; i < n; i++) {
- const uint16 id = f.readUint16LE();
- const int16 px = (int16)f.readUint16LE();
- const int16 py = (int16)f.readUint16LE();
+ const uint16 id = readPuzzleU16(f, bigEndian);
+ const int16 px = (int16)readPuzzleU16(f, bigEndian);
+ const int16 py = (int16)readPuzzleU16(f, bigEndian);
Picture pic;
if (_picsArchive.getPicture(id, pic) && !pic.surface.empty())
scratch.transBlitFrom(pic.surface, Common::Point(px, py),
@@ -229,18 +270,18 @@ bool EEMEngine::doPuzzle(uint puzzleId) {
}
}
- 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 int16 qx = (int16)readPuzzleU16(f, bigEndian);
+ const int16 qy = (int16)readPuzzleU16(f, bigEndian);
+ const int16 qw = (int16)readPuzzleU16(f, bigEndian);
+ const uint16 voiceAlt = readPuzzleU16(f, bigEndian); // partner != Jake
+ const uint16 voiceMain = readPuzzleU16(f, bigEndian); // 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);
+ 0, 0, sw, sh);
g_system->updateScreen();
if (_audio && _voiceOn) {
@@ -253,10 +294,10 @@ bool EEMEngine::doPuzzle(uint puzzleId) {
CursorMan.showMouse(true);
if (type == 0) {
- 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 int16 ax1 = (int16)readPuzzleU16(f, bigEndian);
+ const int16 ay1 = (int16)readPuzzleU16(f, bigEndian);
+ const int16 ax2 = (int16)readPuzzleU16(f, bigEndian);
+ const int16 ay2 = (int16)readPuzzleU16(f, bigEndian);
const Common::Rect rect(ax1, ay1, ax2, ay2);
Common::String answer = readPuzzleLine(f);
const uint maxLen = answer.size() + 2;
@@ -266,7 +307,7 @@ bool EEMEngine::doPuzzle(uint puzzleId) {
uint32 blinkMs = g_system->getMillis();
g_system->setFeatureState(OSystem::kFeatureVirtualKeyboard, true);
while (!done && !shouldQuit()) {
- Graphics::ManagedSurface fld(kScreenWidth, kScreenHeight,
+ Graphics::ManagedSurface fld(sw, sh,
Graphics::PixelFormat::createFormatCLUT8());
fld.simpleBlitFrom(scratch);
Common::String shown = input;
@@ -276,7 +317,7 @@ bool EEMEngine::doPuzzle(uint puzzleId) {
_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);
+ sw, sh);
g_system->updateScreen();
Common::Event ev;
@@ -316,13 +357,13 @@ bool EEMEngine::doPuzzle(uint puzzleId) {
correct = input.equals(answer);
} else {
// Multiple choice: click a region; correct = the FIRST rect.
- const uint16 count = f.readUint16LE();
+ const uint16 count = readPuzzleU16(f, bigEndian);
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();
+ const int16 cx1 = (int16)readPuzzleU16(f, bigEndian);
+ const int16 cy1 = (int16)readPuzzleU16(f, bigEndian);
+ const int16 cx2 = (int16)readPuzzleU16(f, bigEndian);
+ const int16 cy2 = (int16)readPuzzleU16(f, bigEndian);
rects.push_back(Common::Rect(cx1, cy1, cx2, cy2));
}
applyHotspotGlowPalette();
@@ -335,7 +376,7 @@ bool EEMEngine::doPuzzle(uint puzzleId) {
const uint32 phase = g_system->getMillis() / 80;
if (phase != lastPhase) {
lastPhase = phase;
- Graphics::ManagedSurface fr(kScreenWidth, kScreenHeight,
+ Graphics::ManagedSurface fr(sw, sh,
Graphics::PixelFormat::createFormatCLUT8());
fr.simpleBlitFrom(scratch);
for (uint i = 0; i < rects.size(); i++) {
@@ -344,7 +385,7 @@ bool EEMEngine::doPuzzle(uint puzzleId) {
fr.frameRect(rects[i], color);
}
g_system->copyRectToScreen(fr.getPixels(), fr.pitch, 0, 0,
- kScreenWidth, kScreenHeight);
+ sw, sh);
}
}
const Common::Point mp = g_system->getEventManager()->getMousePos();
@@ -387,7 +428,7 @@ bool EEMEngine::doPuzzle(uint puzzleId) {
f.close();
g_system->copyRectToScreen(cleanBg.getPixels(), cleanBg.pitch, 0, 0,
- kScreenWidth, kScreenHeight);
+ sw, sh);
g_system->updateScreen();
if (!correct && !shouldQuit()) {
@@ -398,7 +439,7 @@ bool EEMEngine::doPuzzle(uint puzzleId) {
_music->playMus(40, /* loop= */ false);
Common::String hint =
parseString(_mystery.textAt(hintOff), _playerName, _partner);
- Graphics::ManagedSurface ms(kScreenWidth, kScreenHeight,
+ Graphics::ManagedSurface ms(sw, sh,
Graphics::PixelFormat::createFormatCLUT8());
ms.simpleBlitFrom(cleanBg);
const byte firstChar = hint.empty() ? (byte)0 : (byte)hint[0];
@@ -423,7 +464,7 @@ bool EEMEngine::doPuzzle(uint puzzleId) {
_font.drawWordWrapped(&ms, balloonX + tx, balloonY + ty, tw,
hint, haveBalloon ? 0 : 0xF);
g_system->copyRectToScreen(ms.getPixels(), ms.pitch, 0, 0,
- kScreenWidth, kScreenHeight);
+ sw, sh);
g_system->updateScreen();
if (_audio && _voiceOn && kd)
_audio->sayKDDigital(kd, 6, _partner);
@@ -431,7 +472,7 @@ bool EEMEngine::doPuzzle(uint puzzleId) {
stopMusic();
g_system->copyRectToScreen(cleanBg.getPixels(), cleanBg.pitch,
- 0, 0, kScreenWidth, kScreenHeight);
+ 0, 0, sw, sh);
g_system->updateScreen();
}
}
@@ -733,8 +774,10 @@ void EEMEngine::doInterfaceHelp(uint num) {
}
void EEMEngine::setPartnerEraseBg(const Graphics::ManagedSurface *bg) {
- if (bg && bg->w == kScreenWidth && bg->h == kScreenHeight) {
- _partnerEraseBg.create(kScreenWidth, kScreenHeight,
+ const int sw = screenWidth();
+ const int sh = screenHeight();
+ if (bg && bg->w == sw && bg->h == sh) {
+ _partnerEraseBg.create(sw, sh,
Graphics::PixelFormat::createFormatCLUT8());
_partnerEraseBg.simpleBlitFrom(*bg);
} else {
diff --git a/engines/eem/mystery.cpp b/engines/eem/mystery.cpp
index b6e5994613c..3aaad6f7f2a 100644
--- a/engines/eem/mystery.cpp
+++ b/engines/eem/mystery.cpp
@@ -81,6 +81,40 @@ static bool loadMacMysteryBlob(uint num, Common::Array<byte> &out) {
return true;
}
+static bool loadLooseMacMysteryBlob(uint num, Common::Array<byte> &out,
+ Common::String &fname) {
+ static const char *const kPatterns[] = {
+ "m%u.bin",
+ "M%u.BIN"
+ };
+
+ for (uint i = 0; i < ARRAYSIZE(kPatterns); i++) {
+ Common::String candidate = Common::String::format(kPatterns[i], num);
+ Common::File f;
+ if (!f.open(Common::Path(candidate)))
+ continue;
+
+ const int32 size = f.size();
+ if (size <= 20) {
+ warning("Mystery::load: %s too small (%d bytes)",
+ candidate.c_str(), size);
+ return false;
+ }
+
+ out.resize((uint)size);
+ if (f.read(out.data(), (uint32)size) != (uint32)size) {
+ warning("Mystery::load: short read on %s", candidate.c_str());
+ out.clear();
+ return false;
+ }
+
+ fname = candidate;
+ return true;
+ }
+
+ return false;
+}
+
static uint16 readBE16(const Common::Array<byte> &data, uint offset) {
if (offset + 2 > data.size())
return 0;
@@ -195,6 +229,27 @@ static void normalizeMacMystery(Common::Array<byte> &data) {
// as big-endian for Mac.
}
+static void normalizeMacLondonMystery(Common::Array<byte> &data) {
+ if (data.size() <= 0x3e)
+ return;
+
+ uint16 section[10];
+ for (uint i = 0; i < ARRAYSIZE(section); i++)
+ section[i] = readBE16(data, i * 2);
+
+ // Loose EEM2 Mac scripts use the DOS London section/count header with
+ // big-endian words. The structured regions before the text block are
+ // CD-style clue/map/site records; strings at section[4] stay byte-exact.
+ swapU16Range(data, 0, section[0]);
+ swapU16Range(data, section[0], section[4]);
+
+ swapU16Range(data, section[5], section[6]); // 2-byte London NoteIndex
+ swapU16Range(data, section[6], section[7]); // CD-style GalleryData
+ swapU16Range(data, section[7], section[8]); // KDTextIndex
+ swapU16Range(data, section[8], section[9]); // CD-style solved clue block
+ swapU16Range(data, section[9], data.size()); // Hint text index
+}
+
uint16 Mystery::readU16(uint offset) const {
if (offset + 2 > _data.size())
return 0;
@@ -211,6 +266,7 @@ void Mystery::clear() {
_numSuspects = _numCONSITEs = _numCOFFSITEs = 0;
_isFloppy = false;
_isMacintosh = false;
+ _isMacintoshLooseScripts = false;
_floppySuspectsOff = _floppyHintBlockOff = _floppyNoteIndexOff = 0;
_floppyGalleryOff = _floppyTextOff = _floppyKDTextOff = 0;
_floppySolvedOff = 0;
@@ -239,12 +295,18 @@ void Mystery::clear() {
bool Mystery::load(uint num, Common::RandomSource *rng, bool macintosh) {
Common::String fname = Common::String::format("M%u.BIN", num);
Common::Array<byte> staging;
+ bool macLooseScripts = false;
if (macintosh) {
- if (!loadMacMysteryBlob(num, staging))
- return false;
- normalizeMacMystery(staging);
- fname = Common::String::format("MysteryData[%u]", num);
+ if (loadLooseMacMysteryBlob(num, staging, fname)) {
+ macLooseScripts = true;
+ normalizeMacLondonMystery(staging);
+ } else {
+ if (!loadMacMysteryBlob(num, staging))
+ return false;
+ normalizeMacMystery(staging);
+ fname = Common::String::format("MysteryData[%u]", num);
+ }
} else {
Common::File f;
if (!f.open(Common::Path(fname))) {
@@ -270,24 +332,52 @@ bool Mystery::load(uint num, Common::RandomSource *rng, bool macintosh) {
_data = staging;
_number = num;
+ _isFloppy = false;
+ _isMacintosh = false;
+ _isMacintoshLooseScripts = false;
if (macintosh) {
_isMacintosh = true;
-
- _initOffset = readU16(0 * 2);
- _mapOffset = readU16(2 * 2);
- _siteIndexOffset = readU16(3 * 2);
- _noteOffset = readU16(4 * 2);
- _galleryOffset = readU16(5 * 2);
- _textOffset = readU16(7 * 2);
- _kdTextOffset = readU16(8 * 2);
- _solvedOffset = readU16(9 * 2);
- _hintOffset = _kdTextOffset;
-
- _numSites = (_mapOffset < _data.size()) ? _data[_mapOffset] : 0;
- _numSuspects = (_galleryOffset < _data.size()) ? _data[_galleryOffset] : 0;
- _numCONSITEs = 0;
- _numCOFFSITEs = 0;
+ _isMacintoshLooseScripts = macLooseScripts;
+
+ if (_isMacintoshLooseScripts) {
+ _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);
+ }
+ } else {
+ _initOffset = readU16(0 * 2);
+ _mapOffset = readU16(2 * 2);
+ _siteIndexOffset = readU16(3 * 2);
+ _noteOffset = readU16(4 * 2);
+ _galleryOffset = readU16(5 * 2);
+ _textOffset = readU16(7 * 2);
+ _kdTextOffset = readU16(8 * 2);
+ _solvedOffset = readU16(9 * 2);
+ _hintOffset = _kdTextOffset;
+
+ _numSites = (_mapOffset < _data.size()) ? _data[_mapOffset] : 0;
+ _numSuspects = (_galleryOffset < _data.size())
+ ? _data[_galleryOffset] : 0;
+ _numCONSITEs = 0;
+ _numCOFFSITEs = 0;
+ }
if (_numSites > kVisitedSiteCap)
_numSites = kVisitedSiteCap;
@@ -452,7 +542,7 @@ const byte *Mystery::siteIndexEntry(uint siteNum) const {
return nullptr;
// Floppy site index: 2-byte u16 entries (_DoSiteLoop_Floppy @ 1652:03d2).
// CD: 6-byte rows.
- const uint stride = (_isFloppy || _isMacintosh) ? 2 : 6;
+ const uint stride = (_isFloppy || usesCompactMacData()) ? 2 : 6;
const uint off = _siteIndexOffset + siteNum * stride;
if (off + stride > _data.size())
return nullptr;
@@ -538,7 +628,7 @@ void Mystery::loadFloppySiteAnimData() {
}
const byte *Mystery::hotspots(uint siteNum) const {
- if (_isFloppy || _isMacintosh) {
+ if (_isFloppy || usesCompactMacData()) {
const byte *site = siteData(siteNum);
if (!site || (size_t)(site - _data.data()) + 6 > _data.size())
return nullptr;
@@ -557,7 +647,7 @@ const byte *Mystery::hotspots(uint siteNum) const {
}
uint16 Mystery::hotspotCount(uint siteNum) const {
- if (_isFloppy || _isMacintosh) {
+ if (_isFloppy || usesCompactMacData()) {
const byte *site = siteData(siteNum);
if (!site || (size_t)(site - _data.data()) + 6 > _data.size())
return 0;
@@ -602,6 +692,11 @@ const byte *Mystery::noteIndex() const {
uint16 Mystery::noteIndexCount() const {
if (!isLoaded())
return 0;
+ if (_isMacintoshLooseScripts) {
+ if (_galleryOffset <= _noteOffset)
+ return 0;
+ return (uint16)((_galleryOffset - _noteOffset) / 2);
+ }
// 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)
@@ -613,6 +708,11 @@ uint16 Mystery::noteIndexCount() const {
}
uint Mystery::noteSectionSize() const {
+ if (_isMacintoshLooseScripts) {
+ if (!isLoaded() || _galleryOffset <= _noteOffset)
+ return 0;
+ return _galleryOffset - _noteOffset;
+ }
const uint endOffset = _isMacintosh ? _textOffset : _galleryOffset;
if (!isLoaded() || endOffset <= _noteOffset)
return 0;
@@ -625,6 +725,9 @@ bool Mystery::noteHasNotebookText(uint clueId) const {
if (!ni || clueId >= cnt)
return false;
+ if (_isMacintoshLooseScripts)
+ return READ_LE_UINT16(ni + clueId * 2) != 0;
+
if (_isMacintosh)
return READ_LE_UINT16(ni + clueId * 8) != 0;
@@ -645,7 +748,7 @@ const byte *Mystery::kdTextIndex() const {
const byte *Mystery::mapEntry(uint siteNum) const {
if (!isLoaded() || siteNum >= _numSites)
return nullptr;
- if (_isMacintosh) {
+ if (usesCompactMacData()) {
// Mac SITES section: byte[0]=count, then 12-byte entries.
const uint off = _mapOffset + 1 + siteNum * 12;
if (off + 12 > _data.size())
@@ -674,7 +777,7 @@ const byte *Mystery::floppySuspectEntry(uint suspectIdx) const {
// u16 +2 alibi marker/text offset (0xFFFF = guilty)
// u8 +4 clue/name count
// u8 +5.. clue ids (Mac) or name bytes (floppy)
- if ((!_isFloppy && !_isMacintosh) || !isLoaded())
+ if ((!_isFloppy && !usesCompactMacData()) || !isLoaded())
return nullptr;
const byte *gd = _data.data() + _galleryOffset;
if (gd + 1 > _data.data() + _data.size())
@@ -695,7 +798,7 @@ const byte *Mystery::floppySuspectEntry(uint suspectIdx) const {
}
bool Mystery::isGuilty(uint suspectIdx) const {
- if (_isFloppy || _isMacintosh) {
+ if (_isFloppy || usesCompactMacData()) {
const byte *e = floppySuspectEntry(suspectIdx);
return e && READ_LE_UINT16(e + 2) == 0xFFFF;
}
@@ -707,7 +810,7 @@ bool Mystery::isGuilty(uint suspectIdx) const {
}
uint16 Mystery::alibiTextOffset(uint suspectIdx) const {
- if (_isMacintosh) {
+ if (usesCompactMacData()) {
const byte *e = floppySuspectEntry(suspectIdx);
if (!e)
return 0xFFFF;
@@ -747,6 +850,9 @@ const byte *Mystery::solvedClueBlock() const {
}
int Mystery::selectedPoints() const {
+ if (_isMacintoshLooseScripts)
+ return 0;
+
const byte *ni = noteIndex();
const uint16 cnt = noteIndexCount();
if (!ni || cnt == 0)
@@ -848,6 +954,9 @@ int Mystery::minCluesRemaining() const {
}
int Mystery::foundPoints() const {
+ if (_isMacintoshLooseScripts)
+ return 0;
+
const byte *ni = noteIndex();
const uint16 cnt = noteIndexCount();
if (!ni || cnt == 0)
diff --git a/engines/eem/mystery.h b/engines/eem/mystery.h
index 99ee5cbf7ff..883f9edbcd6 100644
--- a/engines/eem/mystery.h
+++ b/engines/eem/mystery.h
@@ -67,6 +67,9 @@ public:
void clear();
bool isLoaded() const { return !_data.empty(); }
+ bool usesCompactMacData() const {
+ return _isMacintosh && !_isMacintoshLooseScripts;
+ }
uint number() const { return _number; }
uint16 numSites() const { return _numSites; }
@@ -242,6 +245,7 @@ private:
bool _isFloppy = false;
bool _isMacintosh = false;
+ bool _isMacintoshLooseScripts = false;
uint16 _floppySuspectsOff = 0; ///< header[+4] suspects
uint16 _floppyHintBlockOff = 0; ///< header[+6] hint -> clue table
uint16 _floppyNoteIndexOff = 0; ///< header[+8] notes (7B/clue)
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 117eb842cc6..57afddc5199 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -974,7 +974,8 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
}
}
- const bool compactSite = _vm->isFloppy() || _vm->isMacintosh();
+ const bool compactSite = _vm->isFloppy() ||
+ (_mystery && _mystery->usesCompactMacData());
uint16 sitepic = 0;
if (sd) {
if (compactSite) {
@@ -1032,7 +1033,7 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
// _VisitedSite[_SiteNumber] = 1;
// }
// SiteIndex[+2..+3] = byte offset of entry-clue ClueBlock.
- if (firstVisit && !_vm->isMacintosh()) {
+ if (firstVisit && !(_mystery && _mystery->usesCompactMacData())) {
const byte *idx = _mystery->siteIndexEntry(siteNum);
if (idx) {
const uint16 clueOff = READ_LE_UINT16(idx + 2);
@@ -1628,7 +1629,7 @@ bool SiteScreen::partnerIdleAnimParams(uint siteNum, uint16 &animId,
if (!site)
return false;
const uint8 partner = _vm->getPartnerIndex();
- if (_vm->isMacintosh()) {
+ if (_mystery->usesCompactMacData()) {
const uint16 spkOff = READ_LE_UINT16(site + 8);
const byte *spk = _mystery->blobAt(spkOff);
if (!spk)
@@ -1702,7 +1703,8 @@ void SiteScreen::renderPartner(uint siteNum, uint32 tickMs) {
}
bool SiteScreen::renderFloppyHotspotPartnerPose(uint siteNum) {
- if (!_vm || (!_vm->isFloppy() && !_vm->isMacintosh()) || !_mystery)
+ if (!_vm || !_mystery ||
+ (!_vm->isFloppy() && !_mystery->usesCompactMacData()))
return false;
const byte *site = _mystery->siteData(siteNum);
@@ -1763,7 +1765,7 @@ void SiteScreen::renderBackground(uint siteNum) {
const byte *site = _mystery->siteData(siteNum);
uint16 sitepic = 0;
if (site) {
- if (_vm->isFloppy() || _vm->isMacintosh()) {
+ if (_vm->isFloppy() || _mystery->usesCompactMacData()) {
const uint16 dropsOff = READ_LE_UINT16(site);
const byte *drops = _mystery->blobAt(dropsOff);
if (drops)
@@ -1833,7 +1835,8 @@ void SiteScreen::renderHotspots(uint siteNum) {
// don't inherit the first site's seen state after travel/reload).
// Floppy = 8-byte plain rect only; searched state is derived by
// walking the dialog record list, like `_HotspotSearched_Floppy`.
- const bool compact = _vm && (_vm->isFloppy() || _vm->isMacintosh());
+ const bool compact = _vm &&
+ (_vm->isFloppy() || (_mystery && _mystery->usesCompactMacData()));
const bool floppy = _vm && _vm->isFloppy();
const bool mac = _vm && _vm->isMacintosh();
const uint stride = compact ? 8 : 14;
@@ -1897,8 +1900,10 @@ int SiteScreen::hotspotAtPoint(uint siteNum, int x, int y) const {
if (!spots)
return -1;
- const uint stride = _vm && (_vm->isFloppy() || _vm->isMacintosh()) ? 8 : 14;
- const bool mac = _vm && _vm->isMacintosh();
+ const bool compact = _vm &&
+ (_vm->isFloppy() || (_mystery && _mystery->usesCompactMacData()));
+ const uint stride = compact ? 8 : 14;
+ const bool mac = _vm && _mystery && _mystery->usesCompactMacData();
for (uint i = 0; i < count; i++) {
const byte *r = spots + i * stride;
Common::Rect rect;
@@ -1911,7 +1916,8 @@ int SiteScreen::hotspotAtPoint(uint siteNum, int x, int y) const {
// 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() || _vm->isMacintosh())))
+ if (idx < 0 || (_vm && (_vm->isFloppy() ||
+ (_mystery && _mystery->usesCompactMacData()))))
return 0;
const byte *spots = _mystery->hotspots(siteNum);
if (!spots || (uint)idx >= _mystery->hotspotCount(siteNum))
@@ -1975,7 +1981,7 @@ void SiteScreen::onHotspotClicked(uint siteNum, uint hotIdx) {
// Floppy: 8-byte rects only (no clue metadata @ +0xa/+8). Dialog
// records live in a separate list @ `site_data[+6]`.
- if (_vm->isFloppy() || _vm->isMacintosh()) {
+ if (_vm->isFloppy() || _mystery->usesCompactMacData()) {
if (hotIdx < Mystery::kHotSpotsCap)
_mystery->_hotSpotsSeen[hotIdx] = 1;
_mystery->_searchLocationNumber = (uint16)hotIdx;
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index cb5b4739084..89f88c1431e 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -31,6 +31,9 @@
#include "graphics/cursorman.h"
#include "graphics/managed_surface.h"
+#include "graphics/paletteman.h"
+
+#include "video/flic_decoder.h"
#include "eem/audio.h"
#include "eem/detection.h"
@@ -227,11 +230,43 @@ bool loadMacBigMapPixels(Common::Array<byte> &mapPixels,
return true;
}
-bool loadLondonApproachData(uint16 approachId, LondonApproachData &out) {
+static bool openNumberedScriptFile(Common::File &f, Common::String &name,
+ uint num, const char *const *patterns,
+ uint patternCount) {
+ for (uint i = 0; i < patternCount; i++) {
+ name = Common::String::format(patterns[i], num);
+ if (f.open(Common::Path(name)))
+ return true;
+ }
+ name.clear();
+ return false;
+}
+
+static uint16 readScriptU16(const byte *p, bool bigEndian) {
+ return bigEndian ? READ_BE_UINT16(p) : READ_LE_UINT16(p);
+}
+
+bool loadLondonApproachData(uint16 approachId, LondonApproachData &out,
+ bool macintosh) {
+ static const char *const kDosPatterns[] = {
+ "A%u.BIN"
+ };
+ static const char *const kMacPatterns[] = {
+ "Approaches/a%u.bin",
+ "Approaches/A%u.BIN",
+ "a%u.bin",
+ "A%u.BIN"
+ };
+
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());
+ Common::String name;
+ const bool opened = macintosh
+ ? openNumberedScriptFile(f, name, approachId, kMacPatterns,
+ ARRAYSIZE(kMacPatterns))
+ : openNumberedScriptFile(f, name, approachId, kDosPatterns,
+ ARRAYSIZE(kDosPatterns));
+ if (!opened) {
+ warning("London approach: cannot open approach %u", approachId);
return false;
}
@@ -248,16 +283,16 @@ bool loadLondonApproachData(uint16 approachId, LondonApproachData &out) {
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.videoId = readScriptU16(data.data() + 0, macintosh);
+ out.videoX = readScriptU16(data.data() + 2, macintosh);
+ out.videoY = readScriptU16(data.data() + 4, macintosh);
+ const int16 x1 = (int16)readScriptU16(data.data() + 6, macintosh);
+ const int16 y1 = (int16)readScriptU16(data.data() + 8, macintosh);
+ const int16 x2 = (int16)readScriptU16(data.data() + 10, macintosh);
+ const int16 y2 = (int16)readScriptU16(data.data() + 12, macintosh);
out.textRect = Common::Rect(x1, y1, x2, y2);
- const uint16 pageCount = READ_LE_UINT16(data.data() + 14);
+ const uint16 pageCount = readScriptU16(data.data() + 14, macintosh);
out.pages.clear();
uint32 pos = 16;
// `_DoApproach @ 1717:009b` reads the pages with `_fgets` into 255-byte
@@ -274,9 +309,50 @@ bool loadLondonApproachData(uint16 approachId, LondonApproachData &out) {
return out.videoId != 0 && !out.pages.empty();
}
+bool openLondonApproachFlic(uint16 videoId, Video::FlicDecoder &flic,
+ Common::String &name) {
+ static const char *const kPatterns[] = {
+ "VIDEO%02u.FLC",
+ "video%02u.FLC"
+ };
+
+ for (uint i = 0; i < ARRAYSIZE(kPatterns); i++) {
+ name = Common::String::format(kPatterns[i], videoId);
+ if (flic.loadFile(Common::Path(name)))
+ return true;
+ }
+ name.clear();
+ return false;
+}
+
bool decodeLondonApproachFirstFrame(uint16 videoId,
Graphics::ManagedSurface &base,
- byte *palette) {
+ byte *palette, bool macintosh) {
+ if (macintosh) {
+ Video::FlicDecoder flic;
+ Common::String name;
+ if (!openLondonApproachFlic(videoId, flic, name)) {
+ warning("London approach: cannot open FLC video %u", videoId);
+ return false;
+ }
+
+ flic.start();
+ const Graphics::Surface *frame = flic.decodeNextFrame();
+ if (!frame) {
+ warning("London approach: %s has no first frame", name.c_str());
+ return false;
+ }
+
+ const byte *fpal = flic.getPalette();
+ if (fpal)
+ memcpy(palette, fpal, 768);
+ base.clear();
+ const int w = MIN<int>(frame->w, base.w);
+ const int h = MIN<int>(frame->h, base.h);
+ base.copyRectToSurface(frame->getPixels(), frame->pitch, 0, 0, w, h);
+ return true;
+ }
+
const Common::String name = Common::String::format("VIDEO%02u.A", videoId);
ANMDecoder anm;
if (!anm.open(Common::Path(name))) {
@@ -292,8 +368,8 @@ bool decodeLondonApproachFirstFrame(uint16 videoId,
}
base.clear();
- const int w = MIN<int>(anm.width(), kScreenWidth);
- const int h = MIN<int>(anm.height(), kScreenHeight);
+ const int w = MIN<int>(anm.width(), base.w);
+ const int h = MIN<int>(anm.height(), base.h);
base.copyRectToSurface(frame, anm.width(), 0, 0, w, h);
return true;
}
@@ -683,7 +759,35 @@ void copyToScreen(Graphics::ManagedSurface &scratch) {
g_system->updateScreen();
}
-bool loadMacEndingBlob(uint num, Common::Array<byte> &out) {
+bool loadMacEndingBlob(uint num, Common::Array<byte> &out, bool &looseScript) {
+ static const char *const kLoosePatterns[] = {
+ "Endings/e%u.bin",
+ "Endings/E%u.BIN",
+ "e%u.bin",
+ "E%u.BIN"
+ };
+
+ looseScript = false;
+ Common::File loose;
+ Common::String name;
+ if (openNumberedScriptFile(loose, name, num, kLoosePatterns,
+ ARRAYSIZE(kLoosePatterns))) {
+ const uint32 size = loose.size();
+ if (size < 12) {
+ warning("doShowEnding: %s too small (%u bytes)",
+ name.c_str(), size);
+ return false;
+ }
+ out.resize(size);
+ if (loose.read(out.data(), size) != size) {
+ warning("doShowEnding: %s short read", name.c_str());
+ out.clear();
+ return false;
+ }
+ looseScript = true;
+ return true;
+ }
+
if (num >= kMacMysteryDataEndingCount)
return false;
@@ -1515,9 +1619,10 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
Common::Array<byte> buf;
uint32 size = 0;
const bool macEnding = isMacintosh();
+ bool macLooseEnding = false;
if (macEnding) {
- if (!loadMacEndingBlob(num, buf))
+ if (!loadMacEndingBlob(num, buf, macLooseEnding))
return 0;
size = (uint32)buf.size();
} else {
@@ -1560,9 +1665,11 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
const uint pageOffsetCap =
(uint)(sizeof(pageOffsets) / sizeof(pageOffsets[0]));
uint validPages = 0;
+ const bool compactEnding = floppyEnding || (macEnding && !macLooseEnding);
+ const bool cdEnding = !compactEnding;
- if (floppyEnding || macEnding) {
- // Floppy `E<num>.BIN` and Mac `MysteryData[55 + num]` start with:
+ if (compactEnding) {
+ // Floppy `E<num>.BIN` and Mac EEM1 `MysteryData[55 + num]` start with:
// u8 type, 3 bytes of title metadata, char title[], u8 pageCount
// followed by pages. Mac stores overlay pic/x words big-endian and
// native-coordinate text rects in little-endian order.
@@ -1591,7 +1698,7 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
cursor++; // past the null
}
} else {
- const uint16 pageCount = READ_LE_UINT16(buf.data());
+ const uint16 pageCount = readScriptU16(buf.data(), macLooseEnding);
if (pageCount == 0)
return 0;
const uint maxPages = MIN<uint>(pageCount, pageOffsetCap);
@@ -1638,7 +1745,7 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
Graphics::PixelFormat::createFormatCLUT8());
scratch.clear();
- if (floppyEnding || macEnding) {
+ if (compactEnding) {
Picture bg;
if (floppyEnding &&
_picsArchive.getPicture(kFloppyEndingBackgroundPic, bg))
@@ -1679,14 +1786,15 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
raw = (const char *)buf.data() + cursor + 8;
if (macEnding && (byte)*raw == 0xd9)
raw++;
- } else {
+ } else if (cdEnding) {
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)
+ const uint16 picNum =
+ readScriptU16(buf.data() + off, macLooseEnding);
+ x1 = readScriptU16(buf.data() + off + 2, macLooseEnding);
+ y1 = readScriptU16(buf.data() + off + 4, macLooseEnding);
+ x2 = readScriptU16(buf.data() + off + 6, macLooseEnding);
+ (void)readScriptU16(buf.data() + off + 8, macLooseEnding); // y2 (unused â WordWrap2 takes width only)
Picture bg;
if (_picsArchive.getPicture(picNum, bg))
@@ -2990,7 +3098,7 @@ Common::String EEMEngine::notebookNoteText(uint clueId, const byte *ni,
return parseString(Common::String(p, len),
_playerName, _partner);
}
- if (isMacintosh() && bufBase) {
+ if (isMacintosh() && !isLondon() && bufBase) {
const uint16 textOff = READ_LE_UINT16(ni + clueId * 8);
if (textOff == 0 || textOff >= mysSz)
return Common::String();
@@ -3291,7 +3399,7 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
const Picture &galBg, bool haveBg) {
const bool floppyMI = isFloppy();
const bool mac = isMacintosh();
- const bool compactMI = floppyMI || mac;
+ const bool compactMI = floppyMI || _mystery.usesCompactMacData();
const byte *suspect = compactMI
? _mystery.floppySuspectEntry(suspectIdx)
: gd + suspectIdx * 0x46;
@@ -3573,7 +3681,7 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
g_system->getMillis(), mac);
const bool floppy = isFloppy();
- const bool compactGallery = floppy || isMacintosh();
+ const bool compactGallery = floppy || _mystery.usesCompactMacData();
const GallerySlot * const slots =
mac ? kMacGallerySlots : (floppy ? kFloppyGallerySlots : kGallerySlots);
for (uint i = 0; i < numSuspects && i < Mystery::kGalleryCap; i++) {
@@ -3933,7 +4041,9 @@ void EEMEngine::doBigMap() {
if (!entry)
continue;
BigMapEntryInfo info;
- if (!readBigMapEntryInfo(entry, fmap, mac, info))
+ if (!readBigMapEntryInfo(entry, fmap,
+ mac && _mystery.usesCompactMacData(),
+ info))
continue;
Picture button;
int bw = 16;
@@ -3997,14 +4107,21 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
return false;
LondonApproachData data;
- if (!loadLondonApproachData(approachId, data))
+ const bool mac = isMacintosh();
+ if (!loadLondonApproachData(approachId, data, mac))
return false;
- Graphics::ManagedSurface base(kScreenWidth, kScreenHeight,
+ const int sw = screenWidth();
+ const int sh = screenHeight();
+ MacSpritePaletteMap macPaletteMap = {0x00, 0xFF};
+ if (mac)
+ macPaletteMap = getMacSpritePaletteMap();
+
+ Graphics::ManagedSurface base(sw, sh,
Graphics::PixelFormat::createFormatCLUT8());
byte palette[768] = {};
const bool haveVideo =
- decodeLondonApproachFirstFrame(data.videoId, base, palette);
+ decodeLondonApproachFirstFrame(data.videoId, base, palette, mac);
if (!haveVideo)
base.clear();
@@ -4014,21 +4131,31 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
haveButtons[i] = _picsArchive.getPicture(
kLondonApproachButtons[i].picId, buttonPics[i]);
+ auto buttonRect = [&](uint idx) {
+ const Common::Rect r = kLondonApproachButtons[idx].rect();
+ return mac ? scaleRect(r) : r;
+ };
+ auto buttonPoint = [&](uint idx) {
+ const LondonApproachButton &b = kLondonApproachButtons[idx];
+ return Common::Point(mac ? scaleX(b.x) : b.x,
+ mac ? scaleY(b.y) : b.y);
+ };
+
auto drawButtonFallback = [&](Graphics::ManagedSurface &dst, uint idx) {
static const char *const kLabels[4] = { "OK", "PLAY", ">", "<" };
- const Common::Rect r = kLondonApproachButtons[idx].rect();
+ const Common::Rect r = buttonRect(idx);
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::ManagedSurface scratch(sw, sh,
Graphics::PixelFormat::createFormatCLUT8());
scratch.simpleBlitFrom(base);
const Common::Rect textRect =
- data.textRect.findIntersectingRect(Common::Rect(kScreenWidth, kScreenHeight));
+ data.textRect.findIntersectingRect(Common::Rect(sw, sh));
if (!textRect.isEmpty()) {
if (page < data.pages.size()) {
Common::Array<Common::String> wrapped;
@@ -4039,28 +4166,88 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
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(), 1);
+ textRect.width(),
+ mac ? macPaletteMap.black : 1);
}
}
}
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));
+ const Common::Point p = buttonPoint(i);
+ if (mac)
+ blitMacMaskedSurface(scratch.surfacePtr(),
+ buttonPics[i], p.x, p.y, false,
+ macPaletteMap);
+ else
+ scratch.transBlitFrom(buttonPics[i].surface, p,
+ (uint32)(byte)(buttonPics[i].flags >> 8));
} else {
drawButtonFallback(scratch, i);
}
}
g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
- 0, 0, kScreenWidth, kScreenHeight);
+ 0, 0, sw, sh);
g_system->updateScreen();
};
auto playVideo = [&]() {
+ if (mac) {
+ Video::FlicDecoder flic;
+ Common::String name;
+ if (!openLondonApproachFlic(data.videoId, flic, name)) {
+ warning("London approach: cannot open FLC video %u",
+ data.videoId);
+ return;
+ }
+
+ flic.start();
+ (void)flic.decodeNextFrame(); // first frame is already the background
+ const int videoBandH = scaleY(0x82);
+ while (!shouldQuit() && !flic.endOfVideo()) {
+ const Graphics::Surface *frame = flic.decodeNextFrame();
+ if (!frame)
+ break;
+ const int copyW = MIN<int>(frame->w - (int)data.videoX,
+ sw - (int)data.videoX);
+ const int copyH = MIN<int>(videoBandH,
+ MIN<int>(frame->h - (int)data.videoY,
+ sh - (int)data.videoY));
+ if (copyW <= 0 || copyH <= 0)
+ break;
+ g_system->copyRectToScreen(
+ (const byte *)frame->getBasePtr(data.videoX, data.videoY),
+ frame->pitch, data.videoX, data.videoY, copyW, copyH);
+ if (flic.hasDirtyPalette()) {
+ const byte *fpal = flic.getPalette();
+ if (fpal)
+ g_system->getPaletteManager()->setPalette(fpal, 0, 256);
+ }
+ 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;
+ }
+ return;
+ }
+
const Common::String name =
Common::String::format("VIDEO%02u.A", data.videoId);
ANMDecoder anm;
@@ -4072,9 +4259,9 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
// 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 copyW = MIN<int>(anm.width(), sw - (int)data.videoX);
const int copyH = MIN<int>(0x82, MIN<int>(anm.height(),
- kScreenHeight - (int)data.videoY));
+ sh - (int)data.videoY));
if (copyW <= 0 || copyH <= 0)
return;
@@ -4134,8 +4321,7 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
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)) {
+ if (buttonRect(i).contains(ev.mouse.x, ev.mouse.y)) {
overButton = true;
break;
}
@@ -4167,23 +4353,19 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
}
}
if (ev.type == Common::EVENT_LBUTTONDOWN) {
- if (kLondonApproachButtons[0].rect().contains(ev.mouse.x,
- ev.mouse.y)) {
+ if (buttonRect(0).contains(ev.mouse.x, ev.mouse.y)) {
done = true;
break;
}
- if (kLondonApproachButtons[1].rect().contains(ev.mouse.x,
- ev.mouse.y)) {
+ if (buttonRect(1).contains(ev.mouse.x, ev.mouse.y)) {
playVideo();
drawScreen(page);
- } else if (kLondonApproachButtons[2].rect().contains(ev.mouse.x,
- ev.mouse.y)) {
+ } else if (buttonRect(2).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)) {
+ } else if (buttonRect(3).contains(ev.mouse.x, ev.mouse.y)) {
if (page > 0) {
page--;
drawScreen(page);
@@ -4244,7 +4426,8 @@ void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
const bool floppy = _mystery.isLoaded() && isFloppy();
BigMapEntryInfo info;
- if (!readBigMapEntryInfo(entry, floppy, mac, info))
+ if (!readBigMapEntryInfo(entry, floppy,
+ mac && _mystery.usesCompactMacData(), info))
continue;
const bool isDone = (i < Mystery::kVisitedSiteCap)
&& _mystery._visitedSite[i];
@@ -4332,7 +4515,8 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
if (!entry)
continue;
BigMapEntryInfo info;
- if (!readBigMapEntryInfo(entry, floppyMap, mac, info))
+ if (!readBigMapEntryInfo(entry, floppyMap,
+ mac && _mystery.usesCompactMacData(), info))
continue;
Picture button;
if (!_buttonArchive.loadEntry(info.buttonId, button))
@@ -5099,7 +5283,7 @@ void EEMEngine::doAccuse() {
Picture alibiBg;
const bool haveAlibiBg = _picsArchive.getPicture(0x3e, alibiBg);
Picture suspect;
- const byte *pickedSuspect = isMacintosh()
+ const byte *pickedSuspect = _mystery.usesCompactMacData()
? _mystery.floppySuspectEntry((uint)picked)
: (gd ? gd + (uint)picked * 0x46 : nullptr);
const uint16 picId = pickedSuspect ? READ_LE_UINT16(pickedSuspect) : 0;
@@ -5300,7 +5484,7 @@ void EEMEngine::doAccuse() {
_music->playMus(5, /* loop= */ false);
const byte *solved = _mystery.solvedClueBlock();
- if (isMacintosh() && solved) {
+ if (isMacintosh() && _mystery.usesCompactMacData() && solved) {
const byte *bufBase = _mystery.blobAt(0);
const byte *end = bufBase ? bufBase + _mystery.dataSize()
: nullptr;
@@ -5575,6 +5759,7 @@ void EEMEngine::accuseDrawGallery(int highlighted,
blitPdaPartner(scratch, _aniArchive, _partner, kPdaGalleryPartner,
g_system->getMillis());
+ const byte *gd = _mystery.galleryData();
rects.resize(num);
suspects.resize(num);
for (uint i = 0; i < num; i++) {
@@ -5585,7 +5770,9 @@ void EEMEngine::accuseDrawGallery(int highlighted,
continue;
if (_mystery._inGallery[phys] == 0)
continue;
- const byte *e = _mystery.floppySuspectEntry(i);
+ const byte *e = _mystery.usesCompactMacData()
+ ? _mystery.floppySuspectEntry(i)
+ : (gd ? gd + i * 0x46 : nullptr);
if (!e)
continue;
const uint16 picId = READ_LE_UINT16(e + 0);
@@ -5741,6 +5928,7 @@ void EEMEngine::doAccuseFloppy() {
if (picked < 0)
return;
+ const byte *gd = _mystery.galleryData();
const bool guilty = _mystery.isGuilty((uint)picked);
if (guilty) {
@@ -5838,7 +6026,9 @@ void EEMEngine::doAccuseFloppy() {
return;
}
- const byte *susp = _mystery.floppySuspectEntry((uint)picked);
+ const byte *susp = _mystery.usesCompactMacData()
+ ? _mystery.floppySuspectEntry((uint)picked)
+ : (gd ? gd + (uint)picked * 0x46 : nullptr);
uint16 picId = 0;
uint16 alibiOff = 0xFFFF;
if (susp) {
@@ -5979,7 +6169,7 @@ void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
continue;
const GallerySlot &s = slots[phys];
- const byte *entry = isMacintosh()
+ const byte *entry = _mystery.usesCompactMacData()
? _mystery.floppySuspectEntry(i)
: gd + i * 0x46;
if (!entry)
Commit: 17fd586fb5424a00e084c5d631a5e6a06e3bcec7
https://github.com/scummvm/scummvm/commit/17fd586fb5424a00e084c5d631a5e6a06e3bcec7
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-25T12:55:09+02:00
Commit Message:
EEM: render text in EEM2 Mac
Changed paths:
engines/eem/clues.cpp
engines/eem/mystery.cpp
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index fa3d05d5e82..6eaa7bc151f 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -753,10 +753,38 @@ void EEMEngine::doInitClues() {
}
}
+void splitLondonPlayerName(const Common::String &displayName,
+ Common::String &firstName,
+ Common::String &lastName) {
+ Common::String clean = displayName;
+ clean.trim();
+ const size_t split = clean.findFirstOf(' ');
+ if (split == Common::String::npos) {
+ firstName = clean;
+ lastName.clear();
+ return;
+ }
+
+ firstName = clean.substr(0, split);
+ lastName = clean.substr(split + 1);
+ lastName.trim();
+}
+
+void appendCapitalizedName(Common::String &out, const Common::String &name) {
+ bool capitalize = true;
+ for (uint i = 0; i < name.size(); i++) {
+ char c = name[i];
+ if (capitalize && c >= 'a' && c <= 'z')
+ c = (char)(c - 'a' + 'A');
+ out += c;
+ capitalize = (c == ' ');
+ }
+}
+
// _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)
+// 0x80 player first 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"
@@ -770,17 +798,23 @@ void EEMEngine::doInitClues() {
// 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).
+// 0x89 player last name in Mac London loose scripts (same capitalization
+// path as 0x80 in FUN_00002758).
Common::String EEMEngine::parseString(const Common::String &raw,
const Common::String &playerName,
uint partner) const {
const bool isJake = (partner == 0);
+ Common::String firstName = playerName;
+ Common::String lastName;
+ if (isLondon())
+ splitLondonPlayerName(playerName, firstName, lastName);
+
Common::String out;
for (uint i = 0; i < raw.size(); i++) {
const byte c = (byte)raw[i];
switch (c) {
case 0x80:
- out += playerName;
+ appendCapitalizedName(out, firstName);
break;
case 0x81:
out += isJake ? "Jake" : "Jennifer";
@@ -810,7 +844,8 @@ Common::String EEMEngine::parseString(const Common::String &raw,
out += _playerFemale ? "her" : "his";
break;
case 0x89:
- // KD hint placeholder (caller handles before this point).
+ if (isLondon())
+ appendCapitalizedName(out, lastName);
break;
case 0:
return out;
diff --git a/engines/eem/mystery.cpp b/engines/eem/mystery.cpp
index 3aaad6f7f2a..2d54b907286 100644
--- a/engines/eem/mystery.cpp
+++ b/engines/eem/mystery.cpp
@@ -665,7 +665,8 @@ uint16 Mystery::hotspotCount(uint siteNum) const {
const char *Mystery::textAt(uint16 offset) const {
if (!isLoaded())
return "";
- const uint pos = _isMacintosh ? offset : _textOffset + offset;
+ const uint pos = (_isMacintosh && !_isMacintoshLooseScripts)
+ ? offset : _textOffset + offset;
if (pos >= _data.size())
return "";
return (const char *)(_data.data() + pos);
More information about the Scummvm-git-logs
mailing list