[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