[Scummvm-git-logs] scummvm master -> d9a29123e1abf2617289c16f2f25f4355cc79b4d

neuromancer noreply at scummvm.org
Tue Jun 23 13:48:22 UTC 2026


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

Summary:
35cc52ad15 EEM: added support for EEM1 DOS demo
964ffc5b79 EEM: initial code for supporting EEM1 Mac release
7b230f43c4 EEM: font support for EEM1 Mac
2685dc2d34 EEM: sound support for EEM1 Mac
95e34f0a24 EEM: start loading the practice mystery in EEM1 Mac
de9f508c07 EEM: practice mystery starts to work in EEM1 Mac
5b53d52b05 EEM: correctly parse hotspots in EEM1 Mac
7bf463b483 EEM: load all the cases from EEM1 Mac
c2f7d05571 EEM: correctly load animations from EEM1 Mac
c17db422a8 EEM: improved playback of music from EEM1 Mac
d9a29123e1 EEM: implemented intro from EEM1 Mac


Commit: 35cc52ad15a7b97a733f65d5d1274e399236c643
    https://github.com/scummvm/scummvm/commit/35cc52ad15a7b97a733f65d5d1274e399236c643
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-23T15:47:52+02:00

Commit Message:
EEM: added support for EEM1 DOS demo

Changed paths:
    engines/eem/clues.cpp
    engines/eem/detection.cpp
    engines/eem/eem.cpp
    engines/eem/eem.h
    engines/eem/ui.cpp


diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index e4f3957e725..ebaf437f0d5 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -333,7 +333,7 @@ void EEMEngine::doChoosePartner() {
 		g_system->delayMillis(20);
 	}
 
-	if (_audio) {
+	if (_audio && !isDemo()) {
 		if (isFloppy()) {
 			// Floppy _DoChoosePartner_Floppy @ 19bb:0a8e 
 			_audio->playFloppyVoiceSlot(0x14, _partner);
@@ -639,15 +639,28 @@ void EEMEngine::doInitClues() {
 		_mystery._lastSite = 0;
 	}
 
-	setSitePalette(isLondon() ? 0x39 : 0x22);
+	const bool demo = isDemo();
+	byte demoPalette[kPalSize];
+	const bool haveDemoPalette = demo && getSitePalette(0x22, demoPalette);
+	if (demo && haveDemoPalette) {
+		byte black[kPalSize] = {};
+		g_system->getPaletteManager()->setPalette(black, 0, 256);
+	} else {
+		setSitePalette(isLondon() ? 0x39 : 0x22);
+	}
+
 	Picture bg;
-	const bool haveBriefingBg = _picsArchive.getPicture(0x52, bg);
+	const uint16 bgPic = demo ? (_partner == kPartnerJake ? 3 : 2) : 0x52;
+	const bool haveBriefingBg = _picsArchive.getPicture(bgPic, bg);
 	if (haveBriefingBg)
 		blitAt(bg, 0, 0);
 
+	if (demo && haveDemoPalette)
+		fadePaletteFromBlack(demoPalette);
+
 	if (isLondon())
 		playLondonInitCluesAnim(caseType, bg, haveBriefingBg);
-	else
+	else if (!demo)
 		playCdFloppyInitCluesAnim(caseType, floppy, bg, haveBriefingBg);
 
 	// Briefing dialogue. CD: clue block @ ib+4 (after caseType,startSite).
diff --git a/engines/eem/detection.cpp b/engines/eem/detection.cpp
index 056915c71f6..7ffa1a8527a 100644
--- a/engines/eem/detection.cpp
+++ b/engines/eem/detection.cpp
@@ -34,6 +34,7 @@ const PlainGameDescriptor eemGames[] = {
 
 #define GUI_OPTIONS_EEM_FLOPPY GUIO4(GAMEOPTION_HIDE_HIGHLIGHT_BOXES, GAMEOPTION_SKIP_REPEATED_CASES, GUIO_MIDIADLIB, GUIO_MIDIMT32)
 #define GUI_OPTIONS_EEM_CD     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)
 
 const ADGameDescription gameDescriptions[] = {
 	{
@@ -67,6 +68,16 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_NO_FLAGS,
 		GUI_OPTIONS_EEM_FLOPPY
 	},
+	{
+		"eem",
+		"Demo",
+		AD_ENTRY2s("EEMDEMO.EXE", "723eb5e1744f5b88562068d732990c7b", 107300,
+				   "PICS.DBD",   "fc6be43a0391a79263535760a8bfaecd", 322194),
+		Common::EN_ANY,
+		Common::kPlatformDOS,
+		ADGF_DEMO,
+		GUI_OPTIONS_EEM_DEMO
+	},
 	{
 		// Eagle Eye Mysteries in London 
 		"eem2",
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 82613a39f66..7a3870881b8 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -306,7 +306,7 @@ void EEMEngine::advanceChainStageAfterSolve(uint mysteryNum) {
 }
 
 void EEMEngine::applySkipRepeatedCasesOption() {
-	if (isLondon() || !ConfMan.getBool("skip_repeated_cases"))
+	if (isLondon() || isDemo() || !ConfMan.getBool("skip_repeated_cases"))
 		return;
 	if (_mystery.isLoaded())
 		return;
@@ -343,8 +343,9 @@ Common::Error EEMEngine::run() {
 	if (!_font.load(Common::Path("FONT.FNT")))
 		warning("FONT.FNT failed to load; text will not render");
 
-	// _InitMIDI @ 20a2:013a.
-	_music = new MusicPlayer(isFloppy());
+	// _InitMIDI @ 20a2:013a. The demo does not ship SAMPLE.AD or XMIDI data.
+	if (!isDemo())
+		_music = new MusicPlayer(isFloppy());
 
 	// _InitDrivers @ 1ff1:0368 (SBDIG.ADV / PASDIG.ADV).
 	_audio = new AudioPlayer(this);
@@ -420,12 +421,12 @@ Common::Error EEMEngine::run() {
 			showHighScoreLogo();
 		if (!shouldQuit() && !_skipIntro)
 			showFloppyStormLogo();
-		if (!shouldQuit() && !_skipIntro && _music)
+		if (!shouldQuit() && !_skipIntro && !isDemo() && _music)
 			_music->playFile(Common::Path("THEME.XMI"), /* loop= */ true);
-		if (!shouldQuit() && !_skipIntro)
+		if (!shouldQuit() && !_skipIntro && !isDemo())
 			playAnm(Common::Path("CHAT.ANM"), 120,
 					/* holdLastFrame= */ false);
-		if (!shouldQuit() && !_skipIntro)
+		if (!shouldQuit() && !_skipIntro && !isDemo())
 			playAnm(Common::Path("MOVIE.ANM"), 120,
 					/* holdLastFrame= */ false);
 	} else {
@@ -499,7 +500,20 @@ screenLoop:
 		switch (current) {
 		case kScreenTitle:
 			_nextScreen = kScreenProfile;
-			if (isFloppy()) {
+			if (isDemo()) {
+				Picture title;
+				CursorMan.showMouse(false);
+				setSitePalette(0);
+				if (_picsArchive.getPicture(1, title) && !title.surface.empty()) {
+					blitAt(title, 0, 0);
+					g_system->updateScreen();
+					waitForInput(0xFFFFFFFFu);
+				} else {
+					warning("EEM demo title PIC 1 failed to load");
+				}
+				_skipIntro = false;
+				CursorMan.showMouse(true);
+			} else if (isFloppy()) {
 				CursorMan.showMouse(false);
 				playAnm(Common::Path("TITLE.ANM"), 120,
 						/* holdLastFrame= */ true, /* fadeIn= */ true);
@@ -1343,14 +1357,14 @@ void EEMEngine::showFloppyStormLogo() {
 	g_system->getPaletteManager()->setPalette(black, 0, 256);
 	g_system->updateScreen();
 
-	if (_audio)
+	if (_audio && !isDemo())
 		_audio->playVoc(Common::Path("THUNDER.VOC"));
 
 	fadePaletteFromBlack(target);
 	waitForInput(2000);
 	fadeCurrentPaletteToBlack();
 
-	if (_audio)
+	if (_audio && !isDemo())
 		_audio->stopVoice();
 }
 
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index a91adfc73e8..529fb549922 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -126,8 +126,11 @@ public:
 	const char *getGameId() const;
 	Common::Platform getPlatform() const;
 	Variant getVariant() const { return _variant; }
-	bool isFloppy() const { return _variant == kVariantFloppy; }
+	bool isFloppy() const { return _variant == kVariantFloppy || isDemo(); }
 	bool isLondon() const { return _variant == kVariantLondonCD; }
+	bool isDemo() const {
+		return _gameDescription && (_gameDescription->flags & ADGF_DEMO);
+	}
 
 	bool hasFeature(EngineFeature f) const override;
 	bool canLoadGameStateCurrently(Common::U32String *msg = nullptr) override;
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 086fd98647f..b0e9bccebef 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -1514,7 +1514,18 @@ void EEMEngine::doSetup() {
 
 			if (kNewCaseBtn.contains(mx, my)) {
 				saveProfile(_playerName);
-				_nextScreen = kScreenChooseMystery;
+				if (isDemo()) {
+					if (_mystery.load(0, &_rng)) {
+						resetSiteArrivalState();
+						_nextScreen = kScreenInitClues;
+					} else {
+						warning("doSetup: failed to restart demo practice mystery");
+						_mystery.clear();
+						_nextScreen = kScreenAction;
+					}
+				} else {
+					_nextScreen = kScreenChooseMystery;
+				}
 				return;
 			}
 
@@ -1896,8 +1907,9 @@ void EEMEngine::doActionScreen() {
 		"         Ver Recortes  3"
 	};
 	const char * const *kPickLabel = isSpanish() ? kPickLabelES : kPickLabelEN;
-	// London has no BOOK3.NME, so its action menu offers four entries
-	const uint numPicks = isLondon() ? (uint)kPickScrap3 : (uint)kNumPicks;
+	// London has no BOOK3.NME, and the demo ships only the practice case.
+	const uint numPicks = isDemo() ? (uint)kPickScrap1
+						: isLondon() ? (uint)kPickScrap3 : (uint)kNumPicks;
 
 	uint loT = 0, hiT = 0;
 	const bool anySolved1 = mysteryTierRange(1, loT, hiT) &&
@@ -1907,8 +1919,8 @@ void EEMEngine::doActionScreen() {
 	const bool haveTier3 = mysteryTierRange(3, loT, hiT);
 	const bool anySolved3 = haveTier3 && anyMysterySolved(loT, hiT);
 
-	const bool chooseOn   = _chainStage < 4;
-	const bool practiceOn = _chainStage <= 1;
+	const bool chooseOn   = !isDemo() && _chainStage < 4;
+	const bool practiceOn = isDemo() || _chainStage <= 1;
 	const bool scrap1On =
 		_chainStage >= 2 || (_chainStage == 1 && anySolved1);
 	const bool scrap2On =


Commit: 964ffc5b79afca2961fefc361b3ecc09f3b7643d
    https://github.com/scummvm/scummvm/commit/964ffc5b79afca2961fefc361b3ecc09f3b7643d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-23T15:47:52+02:00

Commit Message:
EEM: initial code for supporting EEM1 Mac release

Changed paths:
    engines/eem/clues.cpp
    engines/eem/detection.cpp
    engines/eem/eem.cpp
    engines/eem/eem.h
    engines/eem/resource.cpp
    engines/eem/resource.h
    engines/eem/site.cpp
    engines/eem/ui.cpp


diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index ebaf437f0d5..6f79aeb5f81 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -289,7 +289,7 @@ void EEMEngine::doChoosePartner() {
 				break;
 			}
 			if (ev.type == Common::EVENT_MOUSEMOVE) {
-				curMouseX = ev.mouse.x;
+				curMouseX = unscaleX(ev.mouse.x);
 				const uint newLevel = happinessLevel(curMouseX, isLondon());
 				if (newLevel != level) {
 					level = newLevel;
@@ -303,7 +303,7 @@ void EEMEngine::doChoosePartner() {
 				}
 			}
 			if (ev.type == Common::EVENT_LBUTTONDOWN) {
-				const bool leftHalf = ev.mouse.x < 160;
+				const bool leftHalf = unscaleX(ev.mouse.x) < 160;
 				_partner = isLondon()
 					? (leftHalf ? kPartnerJake : kPartnerJenny)
 					: (leftHalf ? kPartnerJenny : kPartnerJake);
@@ -333,7 +333,7 @@ void EEMEngine::doChoosePartner() {
 		g_system->delayMillis(20);
 	}
 
-	if (_audio && !isDemo()) {
+	if (_audio && !isDemo() && !isMacintosh()) {
 		if (isFloppy()) {
 			// Floppy _DoChoosePartner_Floppy @ 19bb:0a8e 
 			_audio->playFloppyVoiceSlot(0x14, _partner);
diff --git a/engines/eem/detection.cpp b/engines/eem/detection.cpp
index 7ffa1a8527a..cae6ac41901 100644
--- a/engines/eem/detection.cpp
+++ b/engines/eem/detection.cpp
@@ -35,6 +35,7 @@ const PlainGameDescriptor eemGames[] = {
 #define GUI_OPTIONS_EEM_FLOPPY GUIO4(GAMEOPTION_HIDE_HIGHLIGHT_BOXES, GAMEOPTION_SKIP_REPEATED_CASES, GUIO_MIDIADLIB, GUIO_MIDIMT32)
 #define GUI_OPTIONS_EEM_CD     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)
+#define GUI_OPTIONS_EEM_MAC    GUIO2(GAMEOPTION_HIDE_HIGHLIGHT_BOXES, GUIO_NOMIDI)
 
 const ADGameDescription gameDescriptions[] = {
 	{
@@ -78,6 +79,16 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_DEMO,
 		GUI_OPTIONS_EEM_DEMO
 	},
+	{
+		"eem",
+		"Mac",
+		AD_ENTRY2s("MysteryData", "d94c087c27e68cc299d7c5e737e458f9", 941029,
+				   "PICS.DBD",    "8905041070ff1352666d98cd78d5501c", 3800445),
+		Common::EN_ANY,
+		Common::kPlatformMacintosh,
+		ADGF_UNSTABLE,
+		GUI_OPTIONS_EEM_MAC
+	},
 	{
 		// Eagle Eye Mysteries in London 
 		"eem2",
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 7a3870881b8..ce40cb5851b 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -213,9 +213,12 @@ EEMEngine::EEMEngine(OSystem *syst, const ADGameDescription *gameDesc)
 	ConfMan.registerDefault("skip_repeated_cases", false);
 	ConfMan.registerDefault("restored_content", false);
 
-	_variant = (gameDesc && gameDesc->extra &&
-				Common::String(gameDesc->extra).contains("Floppy"))
-				 ? kVariantFloppy : kVariantCD;
+	_variant = kVariantCD;
+	if (gameDesc && gameDesc->extra &&
+		Common::String(gameDesc->extra).contains("Floppy"))
+		_variant = kVariantFloppy;
+	if (gameDesc && gameDesc->platform == Common::kPlatformMacintosh)
+		_variant = kVariantMac;
 	if (gameDesc && gameDesc->gameId &&
 		Common::String(gameDesc->gameId) == "eem2")
 		_variant = kVariantLondonCD;
@@ -320,7 +323,7 @@ void EEMEngine::applySkipRepeatedCasesOption() {
 }
 
 Common::Error EEMEngine::run() {
-	initGraphics(kScreenWidth, kScreenHeight);
+	initGraphics(screenWidth(), screenHeight());
 
 	if (!isFloppy() && ConfMan.getBool("restored_content")) {
 		Common::U32String engineDataError;
@@ -343,8 +346,8 @@ Common::Error EEMEngine::run() {
 	if (!_font.load(Common::Path("FONT.FNT")))
 		warning("FONT.FNT failed to load; text will not render");
 
-	// _InitMIDI @ 20a2:013a. The demo does not ship SAMPLE.AD or XMIDI data.
-	if (!isDemo())
+	// _InitMIDI @ 20a2:013a. The demo and Mac release do not ship DOS XMIDI data.
+	if (!isDemo() && !isMacintosh())
 		_music = new MusicPlayer(isFloppy());
 
 	// _InitDrivers @ 1ff1:0368 (SBDIG.ADV / PASDIG.ADV).
@@ -413,6 +416,19 @@ Common::Error EEMEngine::run() {
 		goto screenLoop;
 	}
 
+	if (isMacintosh()) {
+		CursorMan.showMouse(true);
+		if (!shouldQuit())
+			doProfilePicker();
+		if (!shouldQuit())
+			applyStartupTestOverrides();
+		if (!shouldQuit())
+			doChoosePartner();
+		if (!shouldQuit())
+			_nextScreen = _mystery.isLoaded() ? kScreenMap : kScreenAction;
+		goto screenLoop;
+	}
+
 	_skipIntro = false;
 	if (isFloppy()) {
 		if (!shouldQuit() && !_skipIntro)
@@ -682,19 +698,21 @@ void EEMEngine::setSiteHotspotCursorId(int cursorId) {
 }
 
 bool EEMEngine::openArchives() {
-	if (!_picsArchive.open(Common::Path("PICS.DBD"), Common::Path("PICS.DBX"))) {
+	const bool mac = isMacintosh();
+
+	if (!_picsArchive.open(Common::Path("PICS.DBD"), Common::Path("PICS.DBX"), mac)) {
 		warning("PICS archive missing");
 		return false;
 	}
-	if (!_aniArchive.open(Common::Path("ANI.DBD"), Common::Path("ANI.DBX"))) {
+	if (!_aniArchive.open(Common::Path("ANI.DBD"), Common::Path("ANI.DBX"), mac)) {
 		warning("ANI archive missing");
 		return false;
 	}
-	if (!_sitesArchive.open(Common::Path("SITES.DBD"), Common::Path("SITES.DBX")))
+	if (!_sitesArchive.open(Common::Path("SITES.DBD"), Common::Path("SITES.DBX"), mac))
 		warning("SITES archive missing — site backgrounds disabled");
-	if (!_balloonArchive.open(Common::Path("BALLOON.DBD"), Common::Path("BALLOON.DBX")))
+	if (!_balloonArchive.open(Common::Path("BALLOON.DBD"), Common::Path("BALLOON.DBX"), mac))
 		warning("BALLOON archive missing — clue text will lack balloons");
-	if (!_buttonArchive.open(Common::Path("BUTTON.DBD"), Common::Path("BUTTON.DBX")))
+	if (!_buttonArchive.open(Common::Path("BUTTON.DBD"), Common::Path("BUTTON.DBX"), mac))
 		warning("BUTTON archive missing — map markers will be unlabelled");
 	return true;
 }
@@ -711,12 +729,31 @@ bool EEMEngine::loadSitePalettes() {
 		warning("SITEPALS short read");
 		return false;
 	}
+	if (isMacintosh()) {
+		debugC(1, kDebugGfx, "Loaded %u Mac SITEPALS ColorTables",
+			   (uint)(_sitePals.size() / (8 + 256 * 8)));
+		return true;
+	}
 	debugC(1, kDebugGfx, "Loaded %u SITEPALS palettes",
 		   (uint)(_sitePals.size() / kPalSize));
 	return true;
 }
 
 bool EEMEngine::getSitePalette(uint num, byte *out) const {
+	if (isMacintosh()) {
+		const uint kMacColorTableSize = 8 + 256 * 8;
+		if (_sitePals.size() < (num + 1) * kMacColorTableSize)
+			return false;
+		const byte *src = _sitePals.data() + num * kMacColorTableSize + 8;
+		for (uint i = 0; i < 256; i++) {
+			const byte *entry = src + i * 8;
+			out[i * 3 + 0] = entry[2];
+			out[i * 3 + 1] = entry[4];
+			out[i * 3 + 2] = entry[6];
+		}
+		return true;
+	}
+
 	if (_sitePals.size() < (num + 1) * kPalSize)
 		return false;
 	const byte *src = _sitePals.data() + num * kPalSize;
@@ -862,8 +899,10 @@ void EEMEngine::playAnm(const Common::Path &path, uint frameDelayMs,
 }
 
 void EEMEngine::blitAt(const Picture &pic, int x, int y) {
-	const int w = MIN<int>(pic.surface.w, kScreenWidth - x);
-	const int h = MIN<int>(pic.surface.h, kScreenHeight - y);
+	x = scaleX(x);
+	y = scaleY(y);
+	const int w = MIN<int>(pic.surface.w, screenWidth() - x);
+	const int h = MIN<int>(pic.surface.h, screenHeight() - y);
 	if (w <= 0 || h <= 0)
 		return;
 	g_system->copyRectToScreen(pic.surface.getPixels(), pic.surface.pitch,
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 529fb549922..00ba2fc9367 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -100,6 +100,7 @@ enum Variant {
 	kVariantCD       = 0,
 	kVariantFloppy   = 1,
 	kVariantLondonCD = 2,
+	kVariantMac      = 3,
 };
 
 /// `_Partner @ 29be:7918`. Selected at the partner-pick screen
@@ -111,6 +112,8 @@ enum Partner {
 
 constexpr int kScreenWidth  = 320;
 constexpr int kScreenHeight = 200;
+constexpr int kMacScreenWidth  = 512;
+constexpr int kMacScreenHeight = 384;
 
 constexpr Common::Rect kPdaSiteRect             (Common::Point(35, 111), 21, 25);
 constexpr Common::Rect kPdaPartnerFootMapRect   (Common::Point( 7, 177), 50, 23);
@@ -128,9 +131,31 @@ 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; }
 	bool isDemo() const {
 		return _gameDescription && (_gameDescription->flags & ADGF_DEMO);
 	}
+	int screenWidth() const { return isMacintosh() ? kMacScreenWidth : kScreenWidth; }
+	int screenHeight() const { return isMacintosh() ? kMacScreenHeight : kScreenHeight; }
+	int scaleX(int x) const {
+		return isMacintosh() ? scaleCoord(x, kMacScreenWidth, kScreenWidth) : x;
+	}
+	int scaleY(int y) const {
+		return isMacintosh() ? scaleCoord(y, kMacScreenHeight, kScreenHeight) : y;
+	}
+	int unscaleX(int x) const {
+		return isMacintosh() ? scaleCoord(x, kScreenWidth, kMacScreenWidth) : x;
+	}
+	int unscaleY(int y) const {
+		return isMacintosh() ? scaleCoord(y, kScreenHeight, kMacScreenHeight) : y;
+	}
+	Common::Point scalePoint(int x, int y) const {
+		return Common::Point(scaleX(x), scaleY(y));
+	}
+	Common::Rect scaleRect(const Common::Rect &r) const {
+		return Common::Rect(scaleX(r.left), scaleY(r.top),
+							scaleX(r.right), scaleY(r.bottom));
+	}
 
 	bool hasFeature(EngineFeature f) const override;
 	bool canLoadGameStateCurrently(Common::U32String *msg = nullptr) override;
@@ -477,6 +502,12 @@ public:
 	/// (1..3) chooses one of three short one-shot MUS tracks at random.
 	void startLondonTravelMusic(uint8 travelKind);
 private:
+	static int scaleCoord(int value, int target, int source) {
+		const bool negative = value < 0;
+		const int magnitude = negative ? -value : value;
+		const int scaled = (magnitude * target + source / 2) / source;
+		return negative ? -scaled : scaled;
+	}
 
 	Common::String _playerName;  ///< Substituted into 0x80 placeholders.
 
diff --git a/engines/eem/resource.cpp b/engines/eem/resource.cpp
index 1371cd177e7..d9a441aef3f 100644
--- a/engines/eem/resource.cpp
+++ b/engines/eem/resource.cpp
@@ -20,6 +20,7 @@
  */
 
 #include "common/compression/dcl.h"
+#include "common/array.h"
 #include "common/debug.h"
 #include "common/file.h"
 #include "common/textconsole.h"
@@ -38,8 +39,9 @@ DBDArchive::~DBDArchive() {
 	close();
 }
 
-bool DBDArchive::open(const Common::Path &dbdName, const Common::Path &dbxName) {
+bool DBDArchive::open(const Common::Path &dbdName, const Common::Path &dbxName, bool bigEndian) {
 	close();
+	_bigEndian = bigEndian;
 
 	if (!_dbd.open(dbdName)) {
 		warning("DBDArchive: cannot open %s", dbdName.toString().c_str());
@@ -57,9 +59,15 @@ bool DBDArchive::open(const Common::Path &dbdName, const Common::Path &dbxName)
 	_index.reserve(dbxSize / 10);
 	while (dbx.pos() + 10 <= dbxSize) {
 		DBEntry entry;
-		entry.offset     = dbx.readUint32LE();
-		entry.compressed = dbx.readUint16LE();
-		entry.size       = dbx.readUint32LE();
+		if (_bigEndian) {
+			entry.offset     = dbx.readUint32BE();
+			entry.compressed = dbx.readUint16BE();
+			entry.size       = dbx.readUint32BE();
+		} else {
+			entry.offset     = dbx.readUint32LE();
+			entry.compressed = dbx.readUint16LE();
+			entry.size       = dbx.readUint32LE();
+		}
 		_index.push_back(entry);
 	}
 	dbx.close();
@@ -73,17 +81,89 @@ void DBDArchive::close() {
 	if (_dbd.isOpen())
 		_dbd.close();
 	_index.clear();
+	_bigEndian = false;
 }
 
-/// Read one 12-byte frame header + payload at the current stream position.
+bool decompressMacLZSS(Common::SeekableReadStream &stream, byte *dest,
+					   uint32 packedSize, uint32 unpackedSize) {
+	Common::Array<byte> packed;
+	packed.resize(packedSize);
+	if (stream.read(packed.data(), packedSize) != packedSize) {
+		warning("decompressMacLZSS: short packed read (%u bytes)", packedSize);
+		return false;
+	}
+
+	byte dict[4096];
+	memset(dict, 0x20, sizeof(dict));
+
+	uint32 src = 0;
+	uint32 dst = 0;
+	uint16 dictPos = 0x0fee;
+	byte flags = 0;
+	byte mask = 0;
+
+	while (dst < unpackedSize && src < packedSize) {
+		if (mask == 0) {
+			flags = packed[src++];
+			mask = 1;
+			if (src > packedSize)
+				break;
+		}
+
+		if (flags & mask) {
+			if (src >= packedSize)
+				break;
+			const byte b = packed[src++];
+			dest[dst++] = b;
+			dict[dictPos] = b;
+			dictPos = (dictPos + 1) & 0x0fff;
+		} else {
+			if (src + 1 >= packedSize)
+				break;
+			const byte lo = packed[src++];
+			const byte hi = packed[src++];
+			uint16 offset = (uint16)lo | (uint16)((hi & 0xf0) << 4);
+			uint16 count = (hi & 0x0f) + 3;
+			while (count-- && dst < unpackedSize) {
+				const byte b = dict[offset];
+				offset = (offset + 1) & 0x0fff;
+				dest[dst++] = b;
+				dict[dictPos] = b;
+				dictPos = (dictPos + 1) & 0x0fff;
+			}
+		}
+
+		mask <<= 1;
+	}
+
+	if (dst != unpackedSize) {
+		warning("decompressMacLZSS: decoded %u of %u bytes", dst, unpackedSize);
+		return false;
+	}
+	return true;
+}
+
+/// Read one frame header + payload at the current stream position.
 /// Shared between picture and animation loaders since the layout is identical.
-bool readFrame(Common::SeekableReadStream &stream, bool compressed, Picture &out) {
-	out.flags             = stream.readUint16LE();
-	const uint16 height   = stream.readUint16LE();
-	const uint16 width    = stream.readUint16LE();
-	out.rowoff            = stream.readUint16LE();
-	out.miscflags         = stream.readUint16LE();
-	out.compsize          = stream.readUint16LE();
+bool readFrame(Common::SeekableReadStream &stream, bool compressed, bool bigEndian, Picture &out) {
+	uint16 height;
+	uint16 width;
+
+	if (bigEndian) {
+		out.flags     = stream.readUint16BE();
+		height        = stream.readUint16BE();
+		width         = stream.readUint16BE();
+		out.rowoff    = stream.readUint16BE();
+		out.miscflags = stream.readUint16BE();
+		out.compsize  = stream.readUint32BE();
+	} else {
+		out.flags     = stream.readUint16LE();
+		height        = stream.readUint16LE();
+		width         = stream.readUint16LE();
+		out.rowoff    = stream.readUint16LE();
+		out.miscflags = stream.readUint16LE();
+		out.compsize  = stream.readUint16LE();
+	}
 
 	if (width == 0 || height == 0) {
 		warning("readFrame: zero dimensions (%ux%u)", width, height);
@@ -94,10 +174,49 @@ bool readFrame(Common::SeekableReadStream &stream, bool compressed, Picture &out
 	const uint32 unpacked = (uint32)width * (uint32)height;
 
 	if (!compressed) {
-		if (stream.read(out.surface.getPixels(), unpacked) != unpacked) {
+		if (!bigEndian && stream.read(out.surface.getPixels(), unpacked) != unpacked) {
 			warning("readFrame: short raw read (%u bytes)", unpacked);
 			return false;
 		}
+		if (bigEndian) {
+			const uint32 rowBytes = (width + 1) & ~1U;
+			Common::Array<byte> row;
+			row.resize(rowBytes);
+			for (uint16 y = 0; y < height; y++) {
+				if (stream.read(row.data(), rowBytes) != rowBytes) {
+					warning("readFrame: short Mac raw row read (%u bytes)", rowBytes);
+					return false;
+				}
+				memcpy(out.surface.getBasePtr(0, y), row.data(), width);
+			}
+		}
+		return true;
+	}
+
+	if (bigEndian) {
+		if (out.compsize < 4) {
+			warning("readFrame: invalid Mac compressed size %u", out.compsize);
+			return false;
+		}
+		const uint32 decodedSize = stream.readUint32BE();
+		const uint32 packedSize = out.compsize - 4;
+
+		Common::Array<byte> decoded;
+		decoded.resize(decodedSize);
+		if (!decompressMacLZSS(stream, decoded.data(), packedSize, decodedSize)) {
+			warning("readFrame: Mac LZSS decompression failed (%u packed -> %u bytes)",
+					packedSize, decodedSize);
+			return false;
+		}
+
+		const uint32 rowBytes = (width + 1) & ~1U;
+		if (decodedSize < rowBytes * height) {
+			warning("readFrame: Mac decoded frame too short (%u < %u)",
+					decodedSize, rowBytes * height);
+			return false;
+		}
+		for (uint16 y = 0; y < height; y++)
+			memcpy(out.surface.getBasePtr(0, y), decoded.data() + y * rowBytes, width);
 		return true;
 	}
 
@@ -126,8 +245,8 @@ bool DBDArchive::loadEntry(uint num, Picture &out) {
 	}
 
 	// Leading u16 = frame count (always 1 for pictures).
-	_dbd.skip(2);
-	return readFrame(_dbd, entry.compressed != 0, out);
+	_bigEndian ? _dbd.readUint16BE() : _dbd.readUint16LE();
+	return readFrame(_dbd, entry.compressed != 0, _bigEndian, out);
 }
 
 bool DBDArchive::loadAnimation(uint num, Animation &out) {
@@ -142,7 +261,7 @@ bool DBDArchive::loadAnimation(uint num, Animation &out) {
 		return false;
 	}
 
-	const uint16 frameCount = _dbd.readUint16LE();
+	const uint16 frameCount = _bigEndian ? _dbd.readUint16BE() : _dbd.readUint16LE();
 	if (frameCount == 0 || frameCount > 256) {
 		warning("DBDArchive::loadAnimation: %u has implausible frame count %u",
 				num, frameCount);
@@ -151,7 +270,7 @@ bool DBDArchive::loadAnimation(uint num, Animation &out) {
 
 	out.resize(frameCount);
 	for (uint16 i = 0; i < frameCount; i++) {
-		if (!readFrame(_dbd, entry.compressed != 0, out[i])) {
+		if (!readFrame(_dbd, entry.compressed != 0, _bigEndian, out[i])) {
 			warning("DBDArchive::loadAnimation: frame %u/%u failed in entry %u",
 					i, frameCount, num);
 			return false;
diff --git a/engines/eem/resource.h b/engines/eem/resource.h
index 2e19340ff4a..735ed30797b 100644
--- a/engines/eem/resource.h
+++ b/engines/eem/resource.h
@@ -43,7 +43,7 @@ struct Picture {
 	uint16 flags     = 0; ///< +0  high byte: sub-mode
 	uint16 rowoff    = 0; ///< +6  row offset (clipped sprites)
 	uint16 miscflags = 0; ///< +8  high byte: transparent-mask flag
-	uint16 compsize  = 0; ///< +10 packed payload size on disk
+	uint32 compsize  = 0; ///< Packed payload size on disk.
 	Graphics::ManagedSurface surface;
 };
 
@@ -57,7 +57,7 @@ public:
 	~DBDArchive();
 
 	/// Open both halves; returns false if either file is missing/malformed.
-	bool open(const Common::Path &dbdName, const Common::Path &dbxName);
+	bool open(const Common::Path &dbdName, const Common::Path &dbxName, bool bigEndian = false);
 	void close();
 
 	uint32 size() const { return _index.size(); }
@@ -76,6 +76,7 @@ public:
 private:
 	Common::File _dbd;
 	Common::Array<DBEntry> _index;
+	bool _bigEndian = false;
 };
 
 } // End of namespace EEM
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 88e6c6e919e..b06d208d0fe 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -1584,10 +1584,10 @@ void SiteScreen::renderBackground(uint siteNum) {
 		haveScene = _vm->getPics().getPicture(sitepic + 1, scene);
 	if (haveScene) {
 		// `_Rect_Move(0, 0, h, ..., 0x42, 0x14, 48000, h, w)`.
-		const int x = 0x42;
-		const int y = 0x14;
-		const int w = MIN<int>(scene.surface.w, kScreenWidth - x);
-		const int h = MIN<int>(scene.surface.h, kScreenHeight - y);
+		const int x = _vm->scaleX(0x42);
+		const int y = _vm->scaleY(0x14);
+		const int w = MIN<int>(scene.surface.w, _vm->screenWidth() - x);
+		const int h = MIN<int>(scene.surface.h, _vm->screenHeight() - y);
 		if (w > 0 && h > 0)
 			g_system->copyRectToScreen(scene.surface.getPixels(),
 									   scene.surface.pitch, x, y, w, h);
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index b0e9bccebef..d0bca0e0e6a 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -204,8 +204,8 @@ void swapColors(Graphics::ManagedSurface &dst,
 					   const Common::Rect &r, byte from, byte to) {
 	const int x1 = MAX<int>(0, r.left);
 	const int y1 = MAX<int>(0, r.top);
-	const int x2 = MIN<int>(kScreenWidth, r.right);
-	const int y2 = MIN<int>(kScreenHeight, r.bottom);
+	const int x2 = MIN<int>(dst.w, r.right);
+	const int y2 = MIN<int>(dst.h, r.bottom);
 	for (int y = y1; y < y2; y++) {
 		byte *row = (byte *)dst.getBasePtr(0, y);
 		for (int x = x1; x < x2; x++) {
@@ -419,7 +419,7 @@ int nextLiveSlot(const Common::Array<Common::Rect> &slotRects,
 
 void copyToScreen(Graphics::ManagedSurface &scratch) {
 	g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
-							   0, 0, kScreenWidth, kScreenHeight);
+							   0, 0, scratch.w, scratch.h);
 	g_system->updateScreen();
 }
 
@@ -483,21 +483,25 @@ void drawCaseBookTitle(Graphics::ManagedSurface &scratch, const EEMEngine *vm,
 		: Common::String::format(spanish ? "Lib. %u" : "Book %u", book);
 	const int titleW = vm->getFont().getStringWidth(title);
 	const int titleX = (0xba - titleW) / 2 + 0x3c;
-	vm->getFont().drawString(&scratch, title, titleX, 12, kScreenWidth, 0xF);
+	vm->getFont().drawString(&scratch, title, vm->scaleX(titleX),
+							 vm->scaleY(12), vm->scaleX(kScreenWidth), 0xF);
 }
 
 void drawNameEntryFrame(EEMEngine *vm, const Picture *bg, bool haveBG,
 						const Picture *peek, const Common::String &name,
 						const char *prompt) {
-	Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+	Graphics::ManagedSurface scratch(vm->screenWidth(), vm->screenHeight(),
 		Graphics::PixelFormat::createFormatCLUT8());
 	scratch.clear();
 	if (haveBG)
 		scratch.simpleBlitFrom(bg->surface);
 	if (peek)
-		blitMaskedPic(scratch, *peek, kNameEntryPeekX, kNameEntryPeekY);
-	vm->getFont().drawString(&scratch, prompt, 80, 40, 240, 0xF);
-	vm->getFont().drawString(&scratch, name + "_", 80, 80, 240, 0xF);
+		blitMaskedPic(scratch, *peek, vm->scaleX(kNameEntryPeekX),
+					   vm->scaleY(kNameEntryPeekY));
+	vm->getFont().drawString(&scratch, prompt, vm->scaleX(80),
+							 vm->scaleY(40), vm->scaleX(240), 0xF);
+	vm->getFont().drawString(&scratch, name + "_", vm->scaleX(80),
+							 vm->scaleY(80), vm->scaleX(240), 0xF);
 	copyToScreen(scratch);
 }
 
@@ -509,13 +513,14 @@ bool animateNameEntryPeek(EEMEngine *vm, const Picture *bg, bool haveBG,
 	for (int w = 1; w <= peek->surface.w; w++) {
 		if (pumpQuitEvents(vm))
 			return true;
-		Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+		Graphics::ManagedSurface scratch(vm->screenWidth(), vm->screenHeight(),
 			Graphics::PixelFormat::createFormatCLUT8());
 		scratch.clear();
 		if (haveBG)
 			scratch.simpleBlitFrom(bg->surface);
 		blitMaskedPicRightReveal(scratch, *peek,
-								  kNameEntryPeekX, kNameEntryPeekY, w);
+								  vm->scaleX(kNameEntryPeekX),
+								  vm->scaleY(kNameEntryPeekY), w);
 		copyToScreen(scratch);
 		g_system->delayMillis(10);
 	}
@@ -530,14 +535,14 @@ bool animateProfilePickerReveal(EEMEngine *vm, const Picture *bg,
 	for (int h = 1; h <= reveal->surface.h; h++) {
 		if (pumpQuitEvents(vm))
 			return true;
-		Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+		Graphics::ManagedSurface scratch(vm->screenWidth(), vm->screenHeight(),
 			Graphics::PixelFormat::createFormatCLUT8());
 		scratch.clear();
 		if (haveBG)
 			scratch.simpleBlitFrom(bg->surface);
 		blitMaskedPicBottomReveal(scratch, *reveal,
-								   kProfilePickerRevealX,
-								   kProfilePickerRevealY, h);
+								   vm->scaleX(kProfilePickerRevealX),
+								   vm->scaleY(kProfilePickerRevealY), h);
 		copyToScreen(scratch);
 		g_system->delayMillis(10);
 	}
@@ -577,14 +582,15 @@ void clampProfileScroll(int &selected, int &start, int count) {
 }
 
 void drawProfilePickerFrame(const ProfilePickerView &v) {
-	Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+	Graphics::ManagedSurface scratch(v.vm->screenWidth(), v.vm->screenHeight(),
 		Graphics::PixelFormat::createFormatCLUT8());
 	scratch.clear();
 	if (v.haveBG)
 		scratch.simpleBlitFrom(v.bg->surface);
 	if (v.haveReveal)
 		blitMaskedPic(scratch, *v.reveal,
-					   kProfilePickerRevealX, kProfilePickerRevealY);
+					   v.vm->scaleX(kProfilePickerRevealX),
+					   v.vm->scaleY(kProfilePickerRevealY));
 
 	const int count = v.entries ? (int)v.entries->size() : 0;
 	for (int row = 0; row < kProfileVisibleRows; row++) {
@@ -593,9 +599,9 @@ void drawProfilePickerFrame(const ProfilePickerView &v) {
 			break;
 		const byte color = idx == v.selected ? 0xF : 0x8;
 		v.vm->getFont().drawString(&scratch, (*v.entries)[idx].label,
-									kProfileListX,
-									kProfileListY + row * kProfileLineH,
-									kProfileListW, color);
+									v.vm->scaleX(kProfileListX),
+									v.vm->scaleY(kProfileListY + row * kProfileLineH),
+									v.vm->scaleX(kProfileListW), color);
 	}
 	copyToScreen(scratch);
 }
@@ -687,9 +693,11 @@ void drawCaseBase(Graphics::ManagedSurface &scratch, EEMEngine *vm,
 		scratch.simpleBlitFrom(caseBg->surface);
 	if (haveRevealPic && revealPic)
 		blitMaskedPic(scratch, *revealPic,
-					   kCaseSelectionRevealX, kCaseSelectionRevealY);
+					   vm->scaleX(kCaseSelectionRevealX),
+					   vm->scaleY(kCaseSelectionRevealY));
 	drawCaseBookTitle(scratch, vm, book);
-	drawCaseGreeter(scratch, kdAnim, haveKdAnim, kdAnimX, kdAnimY);
+	drawCaseGreeter(scratch, kdAnim, haveKdAnim,
+					vm->scaleX(kdAnimX), vm->scaleY(kdAnimY));
 }
 
 bool animateCaseSelectionReveal(EEMEngine *vm, const Picture *caseBg,
@@ -705,22 +713,23 @@ bool animateCaseSelectionReveal(EEMEngine *vm, const Picture *caseBg,
 	for (int i = 1; i <= steps; i++) {
 		if (pumpQuitEvents(vm))
 			return true;
-		Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+		Graphics::ManagedSurface scratch(vm->screenWidth(), vm->screenHeight(),
 			Graphics::PixelFormat::createFormatCLUT8());
 		scratch.clear();
 		if (haveCaseBg && caseBg)
 			scratch.simpleBlitFrom(caseBg->surface);
 		if (vm && vm->isFloppy()) {
 			blitMaskedPicRightReveal(scratch, *revealPic,
-									  kCaseSelectionRevealX,
-									  kCaseSelectionRevealY, i);
+									  vm->scaleX(kCaseSelectionRevealX),
+									  vm->scaleY(kCaseSelectionRevealY), i);
 		} else {
 			blitMaskedPicBottomReveal(scratch, *revealPic,
-									   kCaseSelectionRevealX,
-									   kCaseSelectionRevealY, i);
+									   vm->scaleX(kCaseSelectionRevealX),
+									   vm->scaleY(kCaseSelectionRevealY), i);
 		}
 		drawCaseBookTitle(scratch, vm, book);
-		drawCaseGreeter(scratch, kdAnim, haveKdAnim, kdAnimX, kdAnimY);
+		drawCaseGreeter(scratch, kdAnim, haveKdAnim,
+						vm->scaleX(kdAnimX), vm->scaleY(kdAnimY));
 		copyToScreen(scratch);
 		g_system->delayMillis(8);
 	}
@@ -729,7 +738,7 @@ bool animateCaseSelectionReveal(EEMEngine *vm, const Picture *caseBg,
 
 // `DrawList @ 1c33:040d`
 void drawCaseSubmenu(const CaseSubmenuView &v) {
-	Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+	Graphics::ManagedSurface scratch(v.vm->screenWidth(), v.vm->screenHeight(),
 		Graphics::PixelFormat::createFormatCLUT8());
 	drawCaseBase(scratch, v.vm, v.caseBg, v.haveCaseBg,
 				 v.revealPic, v.haveRevealPic,
@@ -759,14 +768,16 @@ void drawCaseSubmenu(const CaseSubmenuView &v) {
 			color = 0x7;   // normal
 		}
 		v.vm->getFont().drawString(&scratch, name,
-			kListX, kListY0 + r * kLineH, kListW, color);
+			v.vm->scaleX(kListX), v.vm->scaleY(kListY0 + r * kLineH),
+			v.vm->scaleX(kListW), color);
 	}
 
 	// Selection arrow.
 	if (v.selRow >= v.topRow && v.selRow < v.topRow + (uint)kVisible) {
 		const int r = (int)(v.selRow - v.topRow);
 		v.vm->getFont().drawString(&scratch, ">",
-			kListX - 6, kListY0 + r * kLineH, 6, 0xF);
+			v.vm->scaleX(kListX - 6), v.vm->scaleY(kListY0 + r * kLineH),
+			v.vm->scaleX(6), 0xF);
 	}
 
 	// Scrollbar thumb at (240, 45..146), proportional to scroll position.
@@ -779,22 +790,23 @@ void drawCaseSubmenu(const CaseSubmenuView &v) {
 						MAX<int>(1, (int)count - kVisible);
 		const Common::Rect thumb(240, trackY0 + pos,
 								  250, trackY0 + pos + thumbH);
-		scratch.fillRect(thumb, 0x8);
-		scratch.frameRect(thumb, 0xF);
+		scratch.fillRect(v.vm->scaleRect(thumb), 0x8);
+		scratch.frameRect(v.vm->scaleRect(thumb), 0xF);
 	}
 
 	copyToScreen(scratch);
 }
 
 void drawActionMenuFrame(const ActionMenuView &v) {
-	Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+	Graphics::ManagedSurface scratch(v.vm->screenWidth(), v.vm->screenHeight(),
 		Graphics::PixelFormat::createFormatCLUT8());
 	scratch.clear();
 	if (v.haveBg && v.bg)
 		scratch.simpleBlitFrom(v.bg->surface);
 	if (v.haveDecor && v.decor)
 		blitMaskedPic(scratch, *v.decor,
-					   kActionScreenDecorX, kActionScreenDecorY);
+					   v.vm->scaleX(kActionScreenDecorX),
+					   v.vm->scaleY(kActionScreenDecorY));
 
 	if (v.vm->getFont().isLoaded()) {
 		// `DrawList @ 1c33:040d`. _TextBox @ 29be:0d00 = {58, 35, 238, 158}.
@@ -810,7 +822,8 @@ void drawActionMenuFrame(const ActionMenuView &v) {
 			const int y = kListY0 + r * kLineH;
 			if ((r & 1) == 0) {
 				v.vm->getFont().drawString(&scratch, v.separator,
-										   kListX, y, kListW, 0x7);
+										   v.vm->scaleX(kListX), v.vm->scaleY(y),
+										   v.vm->scaleX(kListW), 0x7);
 				continue;
 			}
 			const uint mp = (uint)(r >> 1);
@@ -818,7 +831,8 @@ void drawActionMenuFrame(const ActionMenuView &v) {
 			const byte color  = isSel             ? 0xF :
 								v.pickEnabled[mp] ? 0x7 : 0x8;
 			v.vm->getFont().drawString(&scratch, v.pickLabel[mp],
-									   kListX, y, kListW, color);
+									   v.vm->scaleX(kListX), v.vm->scaleY(y),
+									   v.vm->scaleX(kListW), color);
 		}
 	}
 	copyToScreen(scratch);
@@ -926,17 +940,19 @@ void EEMEngine::doProfilePicker() {
 				}
 			}
 			if (ev.type == Common::EVENT_LBUTTONDOWN) {
-				if (kChooserOkRect.contains(ev.mouse.x, ev.mouse.y)) {
+				const Common::Point mouse(unscaleX(ev.mouse.x),
+										  unscaleY(ev.mouse.y));
+				if (kChooserOkRect.contains(mouse.x, mouse.y)) {
 					committed = true;
 					break;
 				}
-				if (kChooserExitRect.contains(ev.mouse.x, ev.mouse.y) ||
-					kChooserNewPlayerRect.contains(ev.mouse.x, ev.mouse.y)) {
+				if (kChooserExitRect.contains(mouse.x, mouse.y) ||
+					kChooserNewPlayerRect.contains(mouse.x, mouse.y)) {
 					createNew = true;
 					committed = true;
 					break;
 				}
-				if (kChooserUpArrowRect.contains(ev.mouse.x, ev.mouse.y)) {
+				if (kChooserUpArrowRect.contains(mouse.x, mouse.y)) {
 					if (start > 0) {
 						start--;
 						if (sel >= start + kProfileVisibleRows)
@@ -946,7 +962,7 @@ void EEMEngine::doProfilePicker() {
 					}
 					break;
 				}
-				if (kChooserDnArrowRect.contains(ev.mouse.x, ev.mouse.y)) {
+				if (kChooserDnArrowRect.contains(mouse.x, mouse.y)) {
 					const int maxStart = MAX<int>(0,
 						(int)entries.size() - kProfileVisibleRows);
 					if (start < maxStart) {
@@ -958,11 +974,11 @@ void EEMEngine::doProfilePicker() {
 					}
 					break;
 				}
-				if (kChooserHelpRect.contains(ev.mouse.x, ev.mouse.y)) {
+				if (kChooserHelpRect.contains(mouse.x, mouse.y)) {
 					break;
 				}
-				if (kChooserListRect.contains(ev.mouse.x, ev.mouse.y)) {
-					const int hit = (ev.mouse.y - kProfileListY) /
+				if (kChooserListRect.contains(mouse.x, mouse.y)) {
+					const int hit = (mouse.y - kProfileListY) /
 									kProfileLineH;
 					const int idx = start + hit;
 					if (hit >= 0 && hit < kProfileVisibleRows &&
@@ -1463,8 +1479,10 @@ void EEMEngine::doSetup() {
 			}
 			if (ev.type != Common::EVENT_LBUTTONDOWN)
 				continue;
-			const int mx = ev.mouse.x;
-			const int my = ev.mouse.y;
+			const Common::Point mouse(unscaleX(ev.mouse.x),
+									  unscaleY(ev.mouse.y));
+			const int mx = mouse.x;
+			const int my = mouse.y;
 
 			if (kPartnerBtn.contains(mx, my)) {
 				_partner = _partner == kPartnerJake ? kPartnerJenny : kPartnerJake;
@@ -1608,7 +1626,7 @@ void EEMEngine::doSetup() {
 }
 
 void EEMEngine::setupDrawScreen() {
-	Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+	Graphics::ManagedSurface scratch(screenWidth(), screenHeight(),
 		Graphics::PixelFormat::createFormatCLUT8());
 	scratch.clear();
 	Picture bg;
@@ -1618,17 +1636,17 @@ void EEMEngine::setupDrawScreen() {
 	const byte kKey    = 0xFE;
 	const byte kBright = 0x15;
 	const byte kDim    = 0x00;
-	swapColors(scratch, kSetupKid1Rect, kKey,
+	swapColors(scratch, scaleRect(kSetupKid1Rect), kKey,
 			   _partner == kPartnerJake ? kBright : kDim);
-	swapColors(scratch, kSetupKid2Rect, kKey,
+	swapColors(scratch, scaleRect(kSetupKid2Rect), kKey,
 			   _partner == kPartnerJenny ? kBright : kDim);
-	swapColors(scratch, kSetupSoundOnRect,  kKey,
+	swapColors(scratch, scaleRect(kSetupSoundOnRect),  kKey,
 			   _voiceOn ? kBright : kDim);
-	swapColors(scratch, kSetupSoundOffRect, kKey,
+	swapColors(scratch, scaleRect(kSetupSoundOffRect), kKey,
 			   _voiceOn ? kDim : kBright);
 
 	g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
-							   0, 0, kScreenWidth, kScreenHeight);
+							   0, 0, scratch.w, scratch.h);
 	g_system->updateScreen();
 }
 
@@ -1638,7 +1656,7 @@ Common::KeyCode EEMEngine::setupShowFullscreenPic(uint16 picId, bool transparent
 		warning("doSetup: PIC %u missing", (uint)picId);
 		return Common::KEYCODE_INVALID;
 	}
-	Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+	Graphics::ManagedSurface scratch(screenWidth(), screenHeight(),
 		Graphics::PixelFormat::createFormatCLUT8());
 	if (transparent) {
 		Graphics::Surface *cur = g_system->lockScreen();
@@ -1654,7 +1672,7 @@ Common::KeyCode EEMEngine::setupShowFullscreenPic(uint16 picId, bool transparent
 		scratch.simpleBlitFrom(pic.surface);
 	}
 	g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
-							   0, 0, kScreenWidth, kScreenHeight);
+							   0, 0, scratch.w, scratch.h);
 	g_system->updateScreen();
 	while (!shouldQuit()) {
 		Common::Event ev;
@@ -1979,22 +1997,24 @@ void EEMEngine::doActionScreen() {
 				ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
 				return;
 			if (ev.type == Common::EVENT_LBUTTONDOWN) {
-				if (kOkRect.contains(ev.mouse.x, ev.mouse.y)) {
+				const Common::Point mouse(unscaleX(ev.mouse.x),
+										  unscaleY(ev.mouse.y));
+				if (kOkRect.contains(mouse.x, mouse.y)) {
 					if (kPickEnabled[pick])
 						confirmed = true;
 					break;
 				}
-				if (kExitRect.contains(ev.mouse.x, ev.mouse.y)) {
+				if (kExitRect.contains(mouse.x, mouse.y)) {
 					exitChosen = true;
 					confirmed = true;
 					break;
 				}
-				if (kHelpRect.contains(ev.mouse.x, ev.mouse.y)) {
+				if (kHelpRect.contains(mouse.x, mouse.y)) {
 					continue;
 				}
-				if (kListRect.contains(ev.mouse.x, ev.mouse.y)) {
+				if (kListRect.contains(mouse.x, mouse.y)) {
 					const int kLineH = 10;
-					const int row = (ev.mouse.y - kListRect.top) / kLineH;
+					const int row = (mouse.y - kListRect.top) / kLineH;
 					if ((row & 1) == 1) {
 						const uint mp = (uint)(row >> 1);
 						if (mp < numPicks && kPickEnabled[mp]) {
@@ -2192,37 +2212,39 @@ void EEMEngine::doCaseSelection() {
 				ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
 				return;
 			if (ev.type == Common::EVENT_LBUTTONDOWN) {
-				if (kChooserOkRect.contains(ev.mouse.x, ev.mouse.y)) {
+				const Common::Point mouse(unscaleX(ev.mouse.x),
+										  unscaleY(ev.mouse.y));
+				if (kChooserOkRect.contains(mouse.x, mouse.y)) {
 					if (selRow < listLen && !solvedFlags[selRow])
 						confirmed = true;
 					break;
 				}
-				if (kChooserExitRect.contains(ev.mouse.x, ev.mouse.y)) {
+				if (kChooserExitRect.contains(mouse.x, mouse.y)) {
 					_mystery.clear();
 					return;
 				}
-				if (kChooserHelpRect.contains(ev.mouse.x, ev.mouse.y)) {
+				if (kChooserHelpRect.contains(mouse.x, mouse.y)) {
 					saveProfile(_playerName);
 					_mystery.clear();
 					_nextScreen = kScreenProfile;
 					return;
 				}
-				if (kChooserUpArrowRect.contains(ev.mouse.x, ev.mouse.y)) {
+				if (kChooserUpArrowRect.contains(mouse.x, mouse.y)) {
 					if (topRow > 0) {
 						topRow--;
 						dirty = true;
 					}
 					continue;
 				}
-				if (kChooserDnArrowRect.contains(ev.mouse.x, ev.mouse.y)) {
+				if (kChooserDnArrowRect.contains(mouse.x, mouse.y)) {
 					topRow++;
 					clampCaseTopRow(topRow, listLen, kVisible);
 					dirty = true;
 					continue;
 				}
-				if (kChooserListRect.contains(ev.mouse.x, ev.mouse.y)) {
+				if (kChooserListRect.contains(mouse.x, mouse.y)) {
 					const int kLineH = 10;
-					const int row = (ev.mouse.y - kChooserListRect.top) / kLineH;
+					const int row = (mouse.y - kChooserListRect.top) / kLineH;
 					if (row < 0 || row >= (int)kVisible)
 						continue;
 					const uint idx = topRow + (uint)row;


Commit: 7b230f43c4e5fcc23cea4d4d65e694afb2b6da75
    https://github.com/scummvm/scummvm/commit/7b230f43c4e5fcc23cea4d4d65e694afb2b6da75
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-23T15:47:52+02:00

Commit Message:
EEM: font support for EEM1 Mac

Changed paths:
    engines/eem/eem.cpp
    engines/eem/font.cpp
    engines/eem/font.h


diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index ce40cb5851b..5bdb49d22d1 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -24,7 +24,9 @@
 #include "common/engine_data.h"
 #include "common/error.h"
 #include "common/events.h"
+#include "common/archive.h"
 #include "common/file.h"
+#include "common/fs.h"
 #include "common/path.h"
 #include "common/system.h"
 #include "common/textconsole.h"
@@ -56,6 +58,8 @@ const uint kPalEAKids          = 0x25;
 const uint kPalHighScore       = 0x27;
 const uint kPalStormLogo       = 0x26;  // Floppy FUN_23d2_0605
 const uint kPicMousePointer    = 0x50;  // 0x51 is the wait cursor
+const uint16 kMacFontResource  = 3214;  // 'Eagle Eye 14' FONT resource
+const uint16 kMacSmallFontResource = 3209; // 'Eagle Eye 9' fallback
 
 // EEM2 cursor table — `_main @ 1abf:0faf` loads seven cursor PICs into
 // `_AnimationObjects`-adjacent slots and `_SwitchMouse @ 17ee:2c83` activates
@@ -322,6 +326,39 @@ void EEMEngine::applySkipRepeatedCasesOption() {
 	}
 }
 
+static void addMacResourceSearchPaths() {
+	const Common::FSNode gameDataDir(ConfMan.getPath("path"));
+	if (!gameDataDir.exists() || !gameDataDir.isDirectory())
+		return;
+
+	const Common::FSNode childRsrcDir = gameDataDir.getChild("rsrc");
+	if (childRsrcDir.exists() && childRsrcDir.isDirectory())
+		SearchMan.addDirectory("eem-mac-rsrc-child", childRsrcDir);
+
+	const Common::FSNode siblingRsrcDir =
+		gameDataDir.getParent().getChild("rsrc");
+	if (siblingRsrcDir.exists() && siblingRsrcDir.isDirectory())
+		SearchMan.addDirectory("eem-mac-rsrc-sibling", siblingRsrcDir);
+}
+
+static bool loadMacFont(EEMFont &font) {
+	addMacResourceSearchPaths();
+
+	const Common::Path appResourceFork("Eagle Eye Mysteries");
+	if (font.loadMacResource(appResourceFork, kMacFontResource, 14))
+		return true;
+	if (font.loadMacResource(appResourceFork, kMacSmallFontResource, 9))
+		return true;
+
+	const Common::Path nestedAppResourceFork("rsrc/Eagle Eye Mysteries");
+	if (font.loadMacResource(nestedAppResourceFork, kMacFontResource, 14))
+		return true;
+	if (font.loadMacResource(nestedAppResourceFork, kMacSmallFontResource, 9))
+		return true;
+
+	return false;
+}
+
 Common::Error EEMEngine::run() {
 	initGraphics(screenWidth(), screenHeight());
 
@@ -342,8 +379,11 @@ Common::Error EEMEngine::run() {
 	if (!loadSitePalettes())
 		return Common::Error(Common::kReadingFailed, "SITEPALS load failed");
 
-	// _LoadFont @ 1b66:023c.
-	if (!_font.load(Common::Path("FONT.FNT")))
+	// _LoadFont @ 1b66:023c. The Mac release stores its Eagle Eye fonts in
+	// the application resource fork instead of a DOS FONT.FNT file.
+	if (isMacintosh() && !loadMacFont(_font))
+		warning("Mac FONT resource failed to load; text will not render");
+	else if (!isMacintosh() && !_font.load(Common::Path("FONT.FNT")))
 		warning("FONT.FNT failed to load; text will not render");
 
 	// _InitMIDI @ 20a2:013a. The demo and Mac release do not ship DOS XMIDI data.
diff --git a/engines/eem/font.cpp b/engines/eem/font.cpp
index 7a6fb6006e3..3dc862ede65 100644
--- a/engines/eem/font.cpp
+++ b/engines/eem/font.cpp
@@ -20,9 +20,13 @@
  */
 
 #include "common/debug.h"
+#include "common/endian.h"
 #include "common/file.h"
+#include "common/macresman.h"
+#include "common/memstream.h"
 #include "common/textconsole.h"
 
+#include "graphics/fonts/macfont.h"
 #include "graphics/surface.h"
 
 #include "eem/detection.h"
@@ -60,7 +64,111 @@ inline byte mapChar(uint32 c) {
 	return c < 128 ? kCharToGlyph[c] : 0;
 }
 
+uint32 readUint24BE(Common::SeekableReadStream &stream) {
+	const uint32 hi = stream.readByte();
+	const uint32 mid = stream.readByte();
+	const uint32 lo = stream.readByte();
+	return (hi << 16) | (mid << 8) | lo;
+}
+
+Common::SeekableReadStream *getRawMacResource(const Common::Path &path,
+											  uint32 typeId, uint16 resourceId) {
+	Common::File f;
+	if (!f.open(path))
+		return nullptr;
+
+	const uint32 fileSize = (uint32)f.size();
+	if (fileSize < 16)
+		return nullptr;
+
+	const uint32 dataOffset = f.readUint32BE();
+	const uint32 mapOffset = f.readUint32BE();
+	const uint32 dataLength = f.readUint32BE();
+	const uint32 mapLength = f.readUint32BE();
+	if (dataOffset > fileSize || mapOffset > fileSize ||
+		dataLength > fileSize - dataOffset ||
+		mapLength > fileSize - mapOffset || mapLength < 28)
+		return nullptr;
+
+	f.seek(mapOffset + 24);
+	const uint16 typeListOffset = f.readUint16BE();
+	const uint32 typeListBase = mapOffset + typeListOffset;
+	const uint32 mapEnd = mapOffset + mapLength;
+	if (typeListBase + 2 > mapEnd)
+		return nullptr;
+
+	f.seek(typeListBase);
+	const uint16 typeCount = f.readUint16BE() + 1;
+	for (uint typeIndex = 0; typeIndex < typeCount; typeIndex++) {
+		const uint32 typeEntry = typeListBase + 2 + typeIndex * 8;
+		if (typeEntry + 8 > mapEnd)
+			return nullptr;
+
+		f.seek(typeEntry);
+		const uint32 curType = f.readUint32BE();
+		const uint16 resourceCount = f.readUint16BE() + 1;
+		const uint16 refListOffset = f.readUint16BE();
+		if (curType != typeId)
+			continue;
+
+		const uint32 refListBase = typeListBase + refListOffset;
+		if (refListBase > mapEnd)
+			return nullptr;
+
+		for (uint resIndex = 0; resIndex < resourceCount; resIndex++) {
+			const uint32 refEntry = refListBase + resIndex * 12;
+			if (refEntry + 12 > mapEnd)
+				return nullptr;
+
+			f.seek(refEntry);
+			const uint16 curId = f.readUint16BE();
+			f.skip(2); // resource name offset
+			f.skip(1); // attributes
+			const uint32 resourceDataOffset = readUint24BE(f);
+			f.skip(4); // handle
+
+			if (curId != resourceId)
+				continue;
+
+			const uint32 resourceData = dataOffset + resourceDataOffset;
+			if (resourceData + 4 > dataOffset + dataLength)
+				return nullptr;
+
+			f.seek(resourceData);
+			const uint32 resourceLength = f.readUint32BE();
+			if (resourceLength > dataOffset + dataLength - resourceData - 4)
+				return nullptr;
+
+			byte *data = (byte *)malloc(resourceLength);
+			if (!data)
+				return nullptr;
+			if (f.read(data, resourceLength) != resourceLength) {
+				free(data);
+				return nullptr;
+			}
+
+			return new Common::MemoryReadStream(data, resourceLength,
+											   DisposeAfterUse::YES);
+		}
+	}
+
+	return nullptr;
+}
+
+EEMFont::~EEMFont() {
+	clear();
+}
+
+void EEMFont::clear() {
+	_glyphs.clear();
+	delete _macFont;
+	_macFont = nullptr;
+	_maxHeight = _maxWidth = _lineHeight = 0;
+}
+
 bool EEMFont::load(const Common::Path &path) {
+	clear();
+
 	Common::File f;
 	if (!f.open(path)) {
 		warning("EEMFont::load: cannot open %s", path.toString().c_str());
@@ -107,7 +215,50 @@ bool EEMFont::load(const Common::Path &path) {
 	return true;
 }
 
+bool EEMFont::loadMacResource(const Common::Path &path, uint16 resourceId,
+							  int size) {
+	clear();
+
+	Common::MacResManager res;
+	Common::SeekableReadStream *stream = nullptr;
+	if (res.open(path) && res.hasResFork())
+		stream = res.getResource(MKTAG('F', 'O', 'N', 'T'), resourceId);
+	if (!stream)
+		stream = getRawMacResource(path, MKTAG('F', 'O', 'N', 'T'), resourceId);
+	if (!stream)
+		return false;
+
+	Graphics::MacFONTFont *font = new Graphics::MacFONTFont();
+	if (!font->loadFont(*stream, nullptr, size, 0)) {
+		delete stream;
+		delete font;
+		return false;
+	}
+	delete stream;
+
+	_macFont = font;
+	debugC(1, kDebugGfx, "Mac FONT %u loaded from %s: %dx%d",
+		   resourceId, path.toString().c_str(), _macFont->getMaxCharWidth(),
+		   _macFont->getFontHeight());
+	return true;
+}
+
+int EEMFont::getFontHeight() const {
+	if (_macFont)
+		return _macFont->getFontHeight();
+	return _lineHeight ? _lineHeight : _maxHeight;
+}
+
+int EEMFont::getMaxCharWidth() const {
+	if (_macFont)
+		return _macFont->getMaxCharWidth();
+	return _maxWidth;
+}
+
 int EEMFont::getCharWidth(uint32 chr) const {
+	if (_macFont)
+		return _macFont->getCharWidth(chr);
+
 	const byte gi = mapChar(chr);
 	if (gi >= _glyphs.size())
 		return 0;
@@ -129,6 +280,12 @@ void EEMFont::drawChar(Graphics::Surface *dst, uint32 chr, int x, int y,
 					   uint32 color) const {
 	if (!dst)
 		return;
+
+	if (_macFont) {
+		_macFont->drawChar(dst, chr, x, y, color);
+		return;
+	}
+
 	const byte gi = mapChar(chr);
 	if (gi >= _glyphs.size())
 		return;
diff --git a/engines/eem/font.h b/engines/eem/font.h
index 8c77e97c1ea..3d82be00bab 100644
--- a/engines/eem/font.h
+++ b/engines/eem/font.h
@@ -29,6 +29,10 @@
 
 #include "graphics/font.h"
 
+namespace Graphics {
+class MacFONTFont;
+}
+
 namespace EEM {
 
 /// One bitmap glyph, 1 bit per pixel, MSB first.
@@ -51,14 +55,14 @@ struct FontGlyph {
 class EEMFont : public Graphics::Font {
 public:
 	EEMFont() = default;
+	~EEMFont() override;
 
 	bool load(const Common::Path &path);
-	bool isLoaded() const { return !_glyphs.empty(); }
+	bool loadMacResource(const Common::Path &path, uint16 resourceId, int size);
+	bool isLoaded() const { return _macFont || !_glyphs.empty(); }
 
-	int getFontHeight() const override {
-		return _lineHeight ? _lineHeight : _maxHeight;
-	}
-	int getMaxCharWidth() const override { return _maxWidth; }
+	int getFontHeight() const override;
+	int getMaxCharWidth() const override;
 	int getCharWidth(uint32 chr) const override;
 	void drawChar(Graphics::Surface *dst, uint32 chr, int x, int y,
 				  uint32 color) const override;
@@ -70,7 +74,10 @@ public:
 						int width, const Common::String &s, uint32 color) const;
 
 private:
+	void clear();
+
 	Common::Array<FontGlyph> _glyphs;
+	Graphics::MacFONTFont *_macFont = nullptr;
 	uint16 _maxHeight  = 0;
 	uint16 _maxWidth   = 0;
 	uint16 _lineHeight = 0;  ///< First glyph height (original line stride)


Commit: 2685dc2d3444404d53bc2b2c46efc1a1c7a80676
    https://github.com/scummvm/scummvm/commit/2685dc2d3444404d53bc2b2c46efc1a1c7a80676
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-23T15:47:52+02:00

Commit Message:
EEM: sound support for EEM1 Mac

Changed paths:
    engines/eem/audio.cpp
    engines/eem/audio.h
    engines/eem/clues.cpp
    engines/eem/eem.cpp
    engines/eem/font.cpp
    engines/eem/music.cpp
    engines/eem/music.h
    engines/eem/resource.cpp
    engines/eem/resource.h


diff --git a/engines/eem/audio.cpp b/engines/eem/audio.cpp
index d791fc1883f..b84844d9712 100644
--- a/engines/eem/audio.cpp
+++ b/engines/eem/audio.cpp
@@ -20,6 +20,7 @@
  */
 
 #include "audio/decoders/raw.h"
+#include "audio/decoders/mac_snd.h"
 #include "audio/decoders/voc.h"
 
 #include "common/compression/dcl.h"
@@ -34,11 +35,57 @@
 #include "eem/audio.h"
 #include "eem/detection.h"
 #include "eem/eem.h"
+#include "eem/resource.h"
 
 namespace EEM {
 
 AudioPlayer::AudioPlayer(EEMEngine *vm) :
-	_vm(vm), _mixer(g_system->getMixer()) {
+	_vm(vm), _mixer(g_system->getMixer()),
+	_isMacintosh(vm && vm->isMacintosh()) {
+}
+
+struct MacSndResource {
+	const char *name;
+	uint16 id;
+};
+
+const MacSndResource kMacSndResources[] = {
+	{ "B-0003SL", 7022 }, { "B-0004SL", 7023 }, { "B-0006SL", 7021 },
+	{ "DING",     7000 }, { "F-0013SL", 8018 }, { "F-0016SL", 8017 },
+	{ "F-0061SL", 8015 }, { "F-0067SL", 8016 }, { "F-0140SL", 8020 },
+	{ "F-0159SL", 8014 }, { "F-0161SL", 8013 }, { "F-0165SL", 8024 },
+	{ "F-0166SL", 8011 }, { "F-0168SL", 8010 }, { "F-0170SL", 8009 },
+	{ "F-0177SL", 8008 }, { "F-0181SL", 8006 }, { "F-0184SL", 8005 },
+	{ "F-0187SL", 8004 }, { "F-0191SL", 8002 }, { "F-0194SL", 8001 },
+	{ "M-0012SL", 7018 }, { "M-0014SL", 7017 }, { "M-0054SL", 7016 },
+	{ "M-0075SL", 7015 }, { "M-0083SL", 7001 }, { "M-0085SL", 7002 },
+	{ "M-0089SL", 7004 }, { "M-0091SL", 7005 }, { "M-0092SL", 7006 },
+	{ "M-0096SL", 7008 }, { "M-0102SL", 7009 }, { "M-0104SL", 7010 },
+	{ "M-0107SL", 7011 }, { "M-0113SL", 7013 }, { "M-0115SL", 7014 },
+	{ "M-0163SL", 7024 }, { "NEWSCAN",  7003 }, { "NEWSSHRT", 7007 },
+	{ "PHONESL",  7012 }, { "SQUAK2SL", 7019 }, { "THUNDER",  7025 },
+};
+
+Common::String macSndNameFromPath(const Common::Path &path) {
+	Common::String name = path.baseName();
+	const size_t dot = name.findLastOf('.');
+	if (dot != Common::String::npos)
+		name = name.substr(0, dot);
+	name.toUppercase();
+
+	if (name == "PHONE")
+		name = "PHONESL";
+
+	return name;
+}
+
+uint16 macSndResourceIdForPath(const Common::Path &path) {
+	const Common::String name = macSndNameFromPath(path);
+	for (uint i = 0; i < ARRAYSIZE(kMacSndResources); i++) {
+		if (name.equalsIgnoreCase(kMacSndResources[i].name))
+			return kMacSndResources[i].id;
+	}
+	return 0;
 }
 
 AudioPlayer::~AudioPlayer() {
@@ -59,6 +106,17 @@ void AudioPlayer::playVoc(const Common::Path &vocPath) {
 	}
 	stopVoice();
 
+	if (_isMacintosh) {
+		const uint16 resourceId = macSndResourceIdForPath(vocPath);
+		if (resourceId == 0) {
+			warning("AudioPlayer: Mac snd resource for %s missing",
+					vocPath.toString().c_str());
+			return;
+		}
+		playMacSnd(resourceId, _voiceHandle, Audio::Mixer::kSpeechSoundType);
+		return;
+	}
+
 	Common::File *f = new Common::File();
 	if (!f->open(vocPath)) {
 		warning("AudioPlayer: %s missing", vocPath.toString().c_str());
@@ -144,6 +202,13 @@ bool AudioPlayer::initMysterySounds(uint mysteryNum) {
 
 	cleanMysterySounds();
 
+	if (_isMacintosh) {
+		debugC(2, kDebugSound,
+			   "AudioPlayer: Mac release has no SDB/SDX bundle for mystery %u",
+			   mysteryNum);
+		return true;
+	}
+
 	if (!readSdxIndex(sdxPath)) {
 		_sdxIndex.clear();
 		return false;
@@ -162,6 +227,30 @@ void AudioPlayer::cleanMysterySounds() {
 	_currentMystery = -1;
 }
 
+void AudioPlayer::playMacSnd(uint16 resourceId, Audio::SoundHandle &handle,
+							 Audio::Mixer::SoundType type) {
+	Common::SeekableReadStream *stream =
+		openMacResource(Common::Path("EEM Sound&Music"),
+						MKTAG('s', 'n', 'd', ' '), resourceId);
+	if (!stream) {
+		warning("AudioPlayer: Mac snd resource %u missing", resourceId);
+		return;
+	}
+
+	Audio::SeekableAudioStream *audioStream =
+		Audio::makeMacSndStream(stream, DisposeAfterUse::YES);
+	if (!audioStream) {
+		delete stream;
+		warning("AudioPlayer: Mac snd resource %u is not playable", resourceId);
+		return;
+	}
+
+	_mixer->playStream(type, &handle, audioStream, -1,
+					   Audio::Mixer::kMaxChannelVolume, 0,
+					   DisposeAfterUse::YES);
+	debugC(1, kDebugSound, "AudioPlayer: playMacSnd(%u)", resourceId);
+}
+
 // pcm must be allocated with malloc(): Audio::makeRawStream takes ownership
 // and frees it via free() on stream destruction (NOT delete/delete[]).
 void AudioPlayer::playPcmBuffer(byte *pcm, uint32 size, uint sampleRate,
diff --git a/engines/eem/audio.h b/engines/eem/audio.h
index fa901b971a8..dc6298190c8 100644
--- a/engines/eem/audio.h
+++ b/engines/eem/audio.h
@@ -128,6 +128,8 @@ private:
 	};
 
 	bool readSdxIndex(const Common::Path &sdxPath);
+	void playMacSnd(uint16 resourceId, Audio::SoundHandle &handle,
+					Audio::Mixer::SoundType type);
 	void playPcmBuffer(byte *pcm, uint32 size, uint sampleRate,
 					   Audio::SoundHandle &handle,
 					   Audio::Mixer::SoundType type);
@@ -141,6 +143,7 @@ private:
 	Common::Path _sdbPath;
 	int _currentMystery = -1;
 	bool _voiceEnabled = true;
+	bool _isMacintosh = false;
 };
 
 } // End of namespace EEM
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 6f79aeb5f81..81dd1e2a326 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -333,8 +333,8 @@ void EEMEngine::doChoosePartner() {
 		g_system->delayMillis(20);
 	}
 
-	if (_audio && !isDemo() && !isMacintosh()) {
-		if (isFloppy()) {
+	if (_audio && !isDemo()) {
+		if (isFloppy() || isMacintosh()) {
 			// Floppy _DoChoosePartner_Floppy @ 19bb:0a8e 
 			_audio->playFloppyVoiceSlot(0x14, _partner);
 		} else {
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 5bdb49d22d1..cfe99d67bec 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -386,9 +386,10 @@ Common::Error EEMEngine::run() {
 	else if (!isMacintosh() && !_font.load(Common::Path("FONT.FNT")))
 		warning("FONT.FNT failed to load; text will not render");
 
-	// _InitMIDI @ 20a2:013a. The demo and Mac release do not ship DOS XMIDI data.
-	if (!isDemo() && !isMacintosh())
-		_music = new MusicPlayer(isFloppy());
+	// _InitMIDI @ 20a2:013a. The demo ships no music. The Mac release stores
+	// SMF MIDI resources in EEM Sound&Music instead of loose DOS XMIDI files.
+	if (!isDemo())
+		_music = new MusicPlayer(isFloppy(), isMacintosh());
 
 	// _InitDrivers @ 1ff1:0368 (SBDIG.ADV / PASDIG.ADV).
 	_audio = new AudioPlayer(this);
diff --git a/engines/eem/font.cpp b/engines/eem/font.cpp
index 3dc862ede65..a426c4961ae 100644
--- a/engines/eem/font.cpp
+++ b/engines/eem/font.cpp
@@ -22,8 +22,6 @@
 #include "common/debug.h"
 #include "common/endian.h"
 #include "common/file.h"
-#include "common/macresman.h"
-#include "common/memstream.h"
 #include "common/textconsole.h"
 
 #include "graphics/fonts/macfont.h"
@@ -31,6 +29,7 @@
 
 #include "eem/detection.h"
 #include "eem/font.h"
+#include "eem/resource.h"
 
 namespace EEM {
 
@@ -64,97 +63,6 @@ inline byte mapChar(uint32 c) {
 	return c < 128 ? kCharToGlyph[c] : 0;
 }
 
-uint32 readUint24BE(Common::SeekableReadStream &stream) {
-	const uint32 hi = stream.readByte();
-	const uint32 mid = stream.readByte();
-	const uint32 lo = stream.readByte();
-	return (hi << 16) | (mid << 8) | lo;
-}
-
-Common::SeekableReadStream *getRawMacResource(const Common::Path &path,
-											  uint32 typeId, uint16 resourceId) {
-	Common::File f;
-	if (!f.open(path))
-		return nullptr;
-
-	const uint32 fileSize = (uint32)f.size();
-	if (fileSize < 16)
-		return nullptr;
-
-	const uint32 dataOffset = f.readUint32BE();
-	const uint32 mapOffset = f.readUint32BE();
-	const uint32 dataLength = f.readUint32BE();
-	const uint32 mapLength = f.readUint32BE();
-	if (dataOffset > fileSize || mapOffset > fileSize ||
-		dataLength > fileSize - dataOffset ||
-		mapLength > fileSize - mapOffset || mapLength < 28)
-		return nullptr;
-
-	f.seek(mapOffset + 24);
-	const uint16 typeListOffset = f.readUint16BE();
-	const uint32 typeListBase = mapOffset + typeListOffset;
-	const uint32 mapEnd = mapOffset + mapLength;
-	if (typeListBase + 2 > mapEnd)
-		return nullptr;
-
-	f.seek(typeListBase);
-	const uint16 typeCount = f.readUint16BE() + 1;
-	for (uint typeIndex = 0; typeIndex < typeCount; typeIndex++) {
-		const uint32 typeEntry = typeListBase + 2 + typeIndex * 8;
-		if (typeEntry + 8 > mapEnd)
-			return nullptr;
-
-		f.seek(typeEntry);
-		const uint32 curType = f.readUint32BE();
-		const uint16 resourceCount = f.readUint16BE() + 1;
-		const uint16 refListOffset = f.readUint16BE();
-		if (curType != typeId)
-			continue;
-
-		const uint32 refListBase = typeListBase + refListOffset;
-		if (refListBase > mapEnd)
-			return nullptr;
-
-		for (uint resIndex = 0; resIndex < resourceCount; resIndex++) {
-			const uint32 refEntry = refListBase + resIndex * 12;
-			if (refEntry + 12 > mapEnd)
-				return nullptr;
-
-			f.seek(refEntry);
-			const uint16 curId = f.readUint16BE();
-			f.skip(2); // resource name offset
-			f.skip(1); // attributes
-			const uint32 resourceDataOffset = readUint24BE(f);
-			f.skip(4); // handle
-
-			if (curId != resourceId)
-				continue;
-
-			const uint32 resourceData = dataOffset + resourceDataOffset;
-			if (resourceData + 4 > dataOffset + dataLength)
-				return nullptr;
-
-			f.seek(resourceData);
-			const uint32 resourceLength = f.readUint32BE();
-			if (resourceLength > dataOffset + dataLength - resourceData - 4)
-				return nullptr;
-
-			byte *data = (byte *)malloc(resourceLength);
-			if (!data)
-				return nullptr;
-			if (f.read(data, resourceLength) != resourceLength) {
-				free(data);
-				return nullptr;
-			}
-
-			return new Common::MemoryReadStream(data, resourceLength,
-											   DisposeAfterUse::YES);
-		}
-	}
-
-	return nullptr;
-}
-
 EEMFont::~EEMFont() {
 	clear();
 }
@@ -219,12 +127,8 @@ bool EEMFont::loadMacResource(const Common::Path &path, uint16 resourceId,
 							  int size) {
 	clear();
 
-	Common::MacResManager res;
-	Common::SeekableReadStream *stream = nullptr;
-	if (res.open(path) && res.hasResFork())
-		stream = res.getResource(MKTAG('F', 'O', 'N', 'T'), resourceId);
-	if (!stream)
-		stream = getRawMacResource(path, MKTAG('F', 'O', 'N', 'T'), resourceId);
+	Common::SeekableReadStream *stream =
+		openMacResource(path, MKTAG('F', 'O', 'N', 'T'), resourceId);
 	if (!stream)
 		return false;
 
diff --git a/engines/eem/music.cpp b/engines/eem/music.cpp
index 80f20aa9bda..fe5a5e5e0b0 100644
--- a/engines/eem/music.cpp
+++ b/engines/eem/music.cpp
@@ -24,17 +24,65 @@
 
 #include "common/config-manager.h"
 #include "common/debug.h"
+#include "common/endian.h"
 #include "common/file.h"
+#include "common/stream.h"
 #include "common/textconsole.h"
 
 #include "eem/detection.h"
 #include "eem/music.h"
+#include "eem/resource.h"
 
 namespace EEM {
 
 const int kMidiDriverFlags = MDT_MIDI | MDT_ADLIB | MDT_PREFER_MT32;
+const uint16 kInvalidMacMidiResource = 0xffff;
 
-MusicPlayer::MusicPlayer(bool isFloppy) : _isFloppy(isFloppy) {
+Common::String musicNameFromPath(const Common::Path &path) {
+	Common::String name = path.baseName();
+	const size_t dot = name.findLastOf('.');
+	if (dot != Common::String::npos)
+		name = name.substr(0, dot);
+	name.toUppercase();
+	return name;
+}
+
+uint16 macMidiResourceIdForFile(const Common::Path &path) {
+	const Common::String name = musicNameFromPath(path);
+	if (name == "THEME" || name == "THEME SONG")
+		return 0;
+	if (name == "WIN1" || name == "FANFARE2")
+		return 1;
+	if (name == "TRAVEL-1")
+		return 2;
+	if (name == "TRAVEL-4")
+		return 3;
+	if (name == "TRAVEL-6")
+		return 4;
+	if (name == "TRAVEL-7")
+		return 5;
+	if (name == "TRAVEL-8")
+		return 6;
+	if (name == "WRONG2")
+		return 7;
+	return kInvalidMacMidiResource;
+}
+
+uint16 macMidiResourceIdForMus(uint num) {
+	static const uint16 kTravelTracks[5] = {
+		4, 3, 5, 2, 6  // Travel-6, Travel-4, Travel-7, Travel-1, Travel-8
+	};
+	if (num < ARRAYSIZE(kTravelTracks))
+		return kTravelTracks[num];
+	if (num == 5)
+		return 1; // WIN1.MID
+	if (num == 6)
+		return 7; // WRONG2.MID
+	return kInvalidMacMidiResource;
+}
+
+MusicPlayer::MusicPlayer(bool isFloppy, bool isMacintosh) :
+	_isFloppy(isFloppy), _isMacintosh(isMacintosh) {
 	// _InitMIDI @ 20a2:013a — `_AIL_register_driver` against
 	// ADLIB.ADV / SBFM.ADV / MT32MPU.ADV. We honour the launcher's
 	// "Music driver" setting and prefer MT-32 when unset.
@@ -44,25 +92,30 @@ MusicPlayer::MusicPlayer(bool isFloppy) : _isFloppy(isFloppy) {
 	if (musicType == MT_GM && ConfMan.getBool("native_mt32"))
 		musicType = MT_MT32;
 
-	switch (musicType) {
-	case MT_ADLIB:
-		// _MIDIPlayFile @ 20a2:024c opens SAMPLE.AD (string at 29be:14d6)
-		// and installs every patch the sequence requests via
-		// `_AIL_install_timbre`.
-		_milesAudioMode = true;
-		_driver = Audio::MidiDriver_Miles_AdLib_create(
-			Common::Path("SAMPLE.AD"), Common::Path());
-		break;
-	case MT_MT32:
-		// MT32MPU.ADV in the original. No Miles MT-32 bank ships with
-		// EEM, so use the standard MT-32 driver.
-		_milesAudioMode = true;
-		_driver = Audio::MidiDriver_Miles_MT32_create(Common::Path());
-		break;
-	default:
+	if (_isMacintosh) {
 		_milesAudioMode = false;
 		createDriver(kMidiDriverFlags);
-		break;
+	} else {
+		switch (musicType) {
+		case MT_ADLIB:
+			// _MIDIPlayFile @ 20a2:024c opens SAMPLE.AD (string at 29be:14d6)
+			// and installs every patch the sequence requests via
+			// `_AIL_install_timbre`.
+			_milesAudioMode = true;
+			_driver = Audio::MidiDriver_Miles_AdLib_create(
+				Common::Path("SAMPLE.AD"), Common::Path());
+			break;
+		case MT_MT32:
+			// MT32MPU.ADV in the original. No Miles MT-32 bank ships with
+			// EEM, so use the standard MT-32 driver.
+			_milesAudioMode = true;
+			_driver = Audio::MidiDriver_Miles_MT32_create(Common::Path());
+			break;
+		default:
+			_milesAudioMode = false;
+			createDriver(kMidiDriverFlags);
+			break;
+		}
 	}
 
 	if (_driver) {
@@ -73,7 +126,7 @@ MusicPlayer::MusicPlayer(bool isFloppy) : _isFloppy(isFloppy) {
 			_driver = nullptr;
 		} else {
 			// Miles AdLib handles its own reset.
-			if (musicType != MT_ADLIB) {
+			if (_isMacintosh || musicType != MT_ADLIB) {
 				if (musicType == MT_MT32 || _nativeMT32)
 					_driver->sendMT32Reset();
 				else
@@ -97,6 +150,66 @@ void MusicPlayer::send(uint32 b) {
 	Audio::MidiPlayer::send(b);
 }
 
+void MusicPlayer::startLoadedMusic(const Common::String &name, bool loop,
+								   bool smf) {
+	_parser = smf ? MidiParser::createParser_SMF()
+				  : MidiParser::createParser_XMIDI(nullptr, nullptr, 0);
+	_parser->setMidiDriver(this);
+	_parser->setTimerRate(_driver->getBaseTempo());
+	_parser->property(MidiParser::mpCenterPitchWheelOnUnload, 1);
+	_parser->property(MidiParser::mpSendSustainOffOnNotesOff, 1);
+
+	if (!_parser->loadMusic(_xmiData.data(), _xmiData.size())) {
+		warning("MusicPlayer: %s parser rejected %s",
+				smf ? "SMF" : "XMIDI", name.c_str());
+		delete _parser;
+		_parser = nullptr;
+		_xmiData.clear();
+		return;
+	}
+
+	_isLooping = loop;
+	_parser->property(MidiParser::mpAutoLoop, loop ? 1 : 0);
+	_parser->setTrack(0);
+
+	syncVolume();
+	_isPlaying = true;
+	debugC(1, kDebugSound,
+		   "MusicPlayer: playing %s (%u bytes, loop=%d, miles=%d, smf=%d)",
+		   name.c_str(), _xmiData.size(), loop, _milesAudioMode, smf);
+}
+
+void MusicPlayer::playMacMidiResource(uint16 resourceId, bool loop) {
+	if (resourceId == kInvalidMacMidiResource)
+		return;
+
+	Common::SeekableReadStream *stream =
+		openMacResource(Common::Path("EEM Sound&Music"),
+						MKTAG('M', 'i', 'd', 'i'), resourceId);
+	if (!stream) {
+		warning("MusicPlayer: Mac Midi resource %u missing", resourceId);
+		return;
+	}
+
+	const uint32 size = (uint32)stream->size();
+	if (size == 0) {
+		delete stream;
+		warning("MusicPlayer: Mac Midi resource %u is empty", resourceId);
+		return;
+	}
+	_xmiData.resize(size);
+	if (stream->read(_xmiData.data(), size) != size) {
+		delete stream;
+		_xmiData.clear();
+		warning("MusicPlayer: short read on Mac Midi resource %u", resourceId);
+		return;
+	}
+	delete stream;
+
+	startLoadedMusic(Common::String::format("Mac Midi %u", resourceId), loop,
+					 /* smf= */ true);
+}
+
 void MusicPlayer::playFile(const Common::Path &xmiPath, bool loop) {
 	if (!_driver)
 		return;
@@ -104,6 +217,16 @@ void MusicPlayer::playFile(const Common::Path &xmiPath, bool loop) {
 	Common::StackLock lock(_mutex);
 	stop();
 
+	if (_isMacintosh) {
+		const uint16 resourceId = macMidiResourceIdForFile(xmiPath);
+		if (resourceId == kInvalidMacMidiResource) {
+			warning("MusicPlayer: no Mac Midi mapping for %s",
+					xmiPath.toString().c_str());
+			return;
+		}
+		playMacMidiResource(resourceId, loop);
+		return;
+	}
 
 	Common::File f;
 	if (!f.open(xmiPath)) {
@@ -123,32 +246,17 @@ void MusicPlayer::playFile(const Common::Path &xmiPath, bool loop) {
 		return;
 	}
 
-	_parser = MidiParser::createParser_XMIDI(nullptr, nullptr, 0);
-	_parser->setMidiDriver(this);
-	_parser->setTimerRate(_driver->getBaseTempo());
-	_parser->property(MidiParser::mpCenterPitchWheelOnUnload, 1);
-	_parser->property(MidiParser::mpSendSustainOffOnNotesOff, 1);
+	startLoadedMusic(xmiPath.toString(), loop, /* smf= */ false);
+}
 
-	if (!_parser->loadMusic(_xmiData.data(), _xmiData.size())) {
-		warning("MusicPlayer: XMIDI parser rejected %s",
-				xmiPath.toString().c_str());
-		delete _parser;
-		_parser = nullptr;
-		_xmiData.clear();
+void MusicPlayer::playMus(uint num, bool loop) {
+	if (_isMacintosh) {
+		Common::StackLock lock(_mutex);
+		stop();
+		playMacMidiResource(macMidiResourceIdForMus(num), loop);
 		return;
 	}
 
-	_isLooping = loop;
-	_parser->property(MidiParser::mpAutoLoop, loop ? 1 : 0);
-	_parser->setTrack(0);
-
-	syncVolume();
-	_isPlaying = true;
-	debugC(1, kDebugSound, "MusicPlayer: playing %s (%u bytes, loop=%d, miles=%d)",
-		   xmiPath.toString().c_str(), size, loop, _milesAudioMode);
-}
-
-void MusicPlayer::playMus(uint num, bool loop) {
 	// CD format string "mus%05d.xmi" at 29be:1525. Floppy maps the same
 	// numeric slots to different filenames:
 	//   0..4 → travel music. Table at 2608:1399-13cd holds 5 entries
diff --git a/engines/eem/music.h b/engines/eem/music.h
index 8209df47d89..35b9ddd8e80 100644
--- a/engines/eem/music.h
+++ b/engines/eem/music.h
@@ -50,7 +50,7 @@ namespace EEM {
  */
 class MusicPlayer : public Audio::MidiPlayer {
 public:
-	explicit MusicPlayer(bool isFloppy = false);
+	explicit MusicPlayer(bool isFloppy = false, bool isMacintosh = false);
 
 	/// _MIDIPlayFile @ 20a2:024c. loop=true mirrors
 	void playFile(const Common::Path &xmiPath, bool loop = false);
@@ -64,8 +64,12 @@ public:
 	void send(uint32 b) override;
 
 private:
+	void playMacMidiResource(uint16 resourceId, bool loop);
+	void startLoadedMusic(const Common::String &name, bool loop, bool smf);
+
 	bool _milesAudioMode = false;
 	const bool _isFloppy;
+	const bool _isMacintosh;
 	Common::Array<byte> _xmiData;
 };
 
diff --git a/engines/eem/resource.cpp b/engines/eem/resource.cpp
index d9a441aef3f..fb8161d95aa 100644
--- a/engines/eem/resource.cpp
+++ b/engines/eem/resource.cpp
@@ -22,7 +22,10 @@
 #include "common/compression/dcl.h"
 #include "common/array.h"
 #include "common/debug.h"
+#include "common/endian.h"
 #include "common/file.h"
+#include "common/macresman.h"
+#include "common/memstream.h"
 #include "common/textconsole.h"
 
 #include "graphics/pixelformat.h"
@@ -39,6 +42,111 @@ DBDArchive::~DBDArchive() {
 	close();
 }
 
+static uint32 readUint24BE(Common::SeekableReadStream &stream) {
+	const uint32 hi = stream.readByte();
+	const uint32 mid = stream.readByte();
+	const uint32 lo = stream.readByte();
+	return (hi << 16) | (mid << 8) | lo;
+}
+
+static Common::SeekableReadStream *openRawMacResource(const Common::Path &path,
+													  uint32 typeId,
+													  uint16 resourceId) {
+	Common::File f;
+	if (!f.open(path))
+		return nullptr;
+
+	const uint32 fileSize = (uint32)f.size();
+	if (fileSize < 16)
+		return nullptr;
+
+	const uint32 dataOffset = f.readUint32BE();
+	const uint32 mapOffset = f.readUint32BE();
+	const uint32 dataLength = f.readUint32BE();
+	const uint32 mapLength = f.readUint32BE();
+	if (dataOffset > fileSize || mapOffset > fileSize ||
+		dataLength > fileSize - dataOffset ||
+		mapLength > fileSize - mapOffset || mapLength < 28)
+		return nullptr;
+
+	f.seek(mapOffset + 24);
+	const uint16 typeListOffset = f.readUint16BE();
+	const uint32 typeListBase = mapOffset + typeListOffset;
+	const uint32 mapEnd = mapOffset + mapLength;
+	if (typeListBase + 2 > mapEnd)
+		return nullptr;
+
+	f.seek(typeListBase);
+	const uint16 typeCount = f.readUint16BE() + 1;
+	for (uint typeIndex = 0; typeIndex < typeCount; typeIndex++) {
+		const uint32 typeEntry = typeListBase + 2 + typeIndex * 8;
+		if (typeEntry + 8 > mapEnd)
+			return nullptr;
+
+		f.seek(typeEntry);
+		const uint32 curType = f.readUint32BE();
+		const uint16 resourceCount = f.readUint16BE() + 1;
+		const uint16 refListOffset = f.readUint16BE();
+		if (curType != typeId)
+			continue;
+
+		const uint32 refListBase = typeListBase + refListOffset;
+		if (refListBase > mapEnd)
+			return nullptr;
+
+		for (uint resIndex = 0; resIndex < resourceCount; resIndex++) {
+			const uint32 refEntry = refListBase + resIndex * 12;
+			if (refEntry + 12 > mapEnd)
+				return nullptr;
+
+			f.seek(refEntry);
+			const uint16 curId = f.readUint16BE();
+			f.skip(2); // resource name offset
+			f.skip(1); // attributes
+			const uint32 resourceDataOffset = readUint24BE(f);
+			f.skip(4); // handle
+
+			if (curId != resourceId)
+				continue;
+
+			const uint32 resourceData = dataOffset + resourceDataOffset;
+			if (resourceData + 4 > dataOffset + dataLength)
+				return nullptr;
+
+			f.seek(resourceData);
+			const uint32 resourceLength = f.readUint32BE();
+			if (resourceLength > dataOffset + dataLength - resourceData - 4)
+				return nullptr;
+
+			byte *data = (byte *)malloc(resourceLength);
+			if (!data)
+				return nullptr;
+			if (f.read(data, resourceLength) != resourceLength) {
+				free(data);
+				return nullptr;
+			}
+
+			return new Common::MemoryReadStream(data, resourceLength,
+											   DisposeAfterUse::YES);
+		}
+	}
+
+	return nullptr;
+}
+
+Common::SeekableReadStream *openMacResource(const Common::Path &path,
+											uint32 typeId,
+											uint16 resourceId) {
+	Common::MacResManager res;
+	Common::SeekableReadStream *stream = nullptr;
+	if (res.open(path) && res.hasResFork())
+		stream = res.getResource(typeId, resourceId);
+	if (stream)
+		return stream;
+
+	return openRawMacResource(path, typeId, resourceId);
+}
+
 bool DBDArchive::open(const Common::Path &dbdName, const Common::Path &dbxName, bool bigEndian) {
 	close();
 	_bigEndian = bigEndian;
diff --git a/engines/eem/resource.h b/engines/eem/resource.h
index 735ed30797b..229bf707c9d 100644
--- a/engines/eem/resource.h
+++ b/engines/eem/resource.h
@@ -29,6 +29,10 @@
 
 #include "graphics/managed_surface.h"
 
+namespace Common {
+class SeekableReadStream;
+}
+
 namespace EEM {
 
 struct DBEntry {
@@ -79,6 +83,10 @@ private:
 	bool _bigEndian = false;
 };
 
+Common::SeekableReadStream *openMacResource(const Common::Path &path,
+											uint32 typeId,
+											uint16 resourceId);
+
 } // End of namespace EEM
 
 #endif


Commit: 95e34f0a24e6ced2700ed7b33f90176d80cb7413
    https://github.com/scummvm/scummvm/commit/95e34f0a24e6ced2700ed7b33f90176d80cb7413
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-23T15:47:52+02:00

Commit Message:
EEM: start loading the practice mystery in EEM1 Mac

Changed paths:
    engines/eem/clues.cpp
    engines/eem/eem.cpp
    engines/eem/eem.h
    engines/eem/graphics.cpp
    engines/eem/mystery.cpp
    engines/eem/mystery.h
    engines/eem/ui.cpp


diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 81dd1e2a326..b977f53f211 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -432,8 +432,18 @@ void EEMEngine::playLondonInitCluesAnim(uint16 caseType, const Picture &bg,
 // EEM1 CD/floppy case-intro animation — `_DoInitClues @ 1a35:0411`
 void EEMEngine::playCdFloppyInitCluesAnim(uint16 caseType, bool floppy,
 										  const Picture &bg, bool haveBriefingBg) {
+	const bool mac = isMacintosh();
 	const uint gameAni = _partner == kPartnerJake ? 0x17 : 0x3b;
 	const uint bookAni = _partner == kPartnerJake ? 0x18 : 0x3c;
+	const int gameX = mac
+		? (_partner == kPartnerJake ? 0x144 : 0x146) : 0xcd;
+	const int gameY = mac
+		? (_partner == kPartnerJake ? 0x0d8 : 0x0d6) : 0x6c;
+	const int bookX = 0;
+	const int bookY = mac
+		? (_partner == kPartnerJake ? 0x0ca : 0x0c2) : 99;
+	const int nancyX = mac ? 0x0aa : 0x68;
+	const int nancyY = mac ? 0x114 : 0x8b;
 	Animation game, book, nancy;
 	const bool haveGame  = _aniArchive.loadAnimation(gameAni, game) && !game.empty();
 	const bool haveBook  = _aniArchive.loadAnimation(bookAni, book) && !book.empty();
@@ -459,15 +469,15 @@ void EEMEngine::playCdFloppyInitCluesAnim(uint16 caseType, bool floppy,
 			}
 			if (haveGame) {
 				const uint f = partnerFrameAtTick(0x17, (uint)game.size(), t);
-				blitAnimFrameAnchored(scr, game[f], 0xcd, 0x6c);
+				blitAnimFrameAnchored(scr, game[f], gameX, gameY);
 			}
 			if (haveBook) {
 				const uint f = partnerFrameAtTick(0x18, (uint)book.size(), t);
-				blitAnimFrameAnchored(scr, book[f], 0, 99);
+				blitAnimFrameAnchored(scr, book[f], bookX, bookY);
 			}
 			if (haveNancy) {
 				const uint f = partnerFrameAtTick(0x19, (uint)nancy.size(), t);
-				blitAnimFrameAnchored(scr, nancy[f], 0x68, 0x8b);
+				blitAnimFrameAnchored(scr, nancy[f], nancyX, nancyY);
 			}
 			g_system->unlockScreen();
 			g_system->updateScreen();
@@ -498,7 +508,9 @@ void EEMEngine::playCdFloppyInitCluesAnim(uint16 caseType, bool floppy,
 	// _VidramRectCopy(0, 0x5a, 0x28, 0x6d, 16000, 48000/32000): freeze the
 	// lower-left book/Nancy band the original bakes into BG buffers before
 	// clearing registered animations.
-	Graphics::ManagedSurface briefingBase(kScreenWidth, kScreenHeight,
+	const int sw = screenWidth();
+	const int sh = screenHeight();
+	Graphics::ManagedSurface briefingBase(sw, sh,
 		Graphics::PixelFormat::createFormatCLUT8());
 	briefingBase.clear();
 	if (haveBriefingBg) {
@@ -512,9 +524,9 @@ void EEMEngine::playCdFloppyInitCluesAnim(uint16 caseType, bool floppy,
 	}
 	{
 		const int preserveX = 0;
-		const int preserveY = 0x5a;
-		const int preserveW = 0x28 * 4;
-		const int preserveH = 0x6d;
+		const int preserveY = mac ? scaleY(0x5a) : 0x5a;
+		const int preserveW = mac ? scaleX(0x28 * 4) : 0x28 * 4;
+		const int preserveH = mac ? scaleY(0x6d) : 0x6d;
 		const Common::Rect preserveRect(preserveX, preserveY,
 										preserveX + preserveW,
 										preserveY + preserveH);
@@ -541,23 +553,27 @@ void EEMEngine::playCdFloppyInitCluesAnim(uint16 caseType, bool floppy,
 
 	// _PlayInSequence @ 172b:2d03.
 	uint16 seqAni = 0xFFFF;
-	uint16 seqY   = 0x6c;
-	if (_partner == kPartnerJake) {
+	int seqX = 0xcd;
+	int seqY = 0x6c;
+	if (mac && _partner == kPartnerJake) {
 		switch (caseType) {
-		case 1:
-			seqAni = 0x38;
-			seqY = 0x6d;
-			break;
-		case 2:
-			seqAni = 0x37;
-			seqY = 0x6c;
-			break;
-		case 3:
-			seqAni = 0x39;
-			seqY = 0x6c;
-			break;
-		default:
-			break;
+		case 1: seqAni = 0x38; seqX = 0x175; seqY = 0x0ce; break;
+		case 2: seqAni = 0x37; seqX = 0x156; seqY = 0x0d8; break;
+		case 3: seqAni = 0x39; seqX = 0x155; seqY = 0x0da; break;
+		default: break;
+		}
+	} else if (mac) {
+		switch (caseType) {
+		case 2: seqAni = 0x3a; seqX = 0x157; seqY = 0x0dd; break;
+		case 3: seqAni = 0x3d; seqX = 0x168; seqY = 0x0d4; break;
+		default: break;
+		}
+	} else if (_partner == kPartnerJake) {
+		switch (caseType) {
+		case 1: seqAni = 0x38; seqY = 0x6d; break;
+		case 2: seqAni = 0x37; seqY = 0x6c; break;
+		case 3: seqAni = 0x39; seqY = 0x6c; break;
+		default: break;
 		}
 	} else {
 		switch (caseType) {
@@ -582,10 +598,10 @@ void EEMEngine::playCdFloppyInitCluesAnim(uint16 caseType, bool floppy,
 				const Picture &fr = seq[frame];
 				g_system->copyRectToScreen(briefingBase.getPixels(),
 										   briefingBase.pitch, 0, 0,
-										   kScreenWidth, kScreenHeight);
+										   sw, sh);
 				// _PlayInSequence @ 172b:2d35-2d50
-				const int dstX = (int)0xcd - (int)(int16)fr.miscflags;
-				const int dstY = (int)seqY - (int)(int16)fr.rowoff;
+				const int dstX = seqX - (int)(int16)fr.miscflags;
+				const int dstY = seqY - (int)(int16)fr.rowoff;
 				blitMaskedToScreen(fr, dstX, dstY);
 				g_system->updateScreen();
 				const uint32 wakeup = g_system->getMillis() + 100;
@@ -623,10 +639,12 @@ void EEMEngine::doInitClues() {
 		return;
 
 	const bool floppy = isFloppy();
-	const uint16 caseType = floppy ? (uint16)ib[0] : READ_LE_UINT16(ib);
+	const bool compactInit = floppy || isMacintosh();
+	const uint16 caseType = compactInit ? (uint16)ib[0] : READ_LE_UINT16(ib);
 
 	if (!floppy) {
-		const uint16 startSite = READ_LE_UINT16(ib + 2);
+		const uint16 startSite = isMacintosh() ? (uint16)ib[1]
+											   : READ_LE_UINT16(ib + 2);
 		if (startSite < Mystery::kVisitedSiteCap)
 			_mystery._onSites[startSite] = 1;
 		_mystery._siteNumber = startSite;
@@ -666,7 +684,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) {
+	if (floppy || isMacintosh()) {
 		displayFloppyBriefing(ib);
 	} else {
 		const byte *briefingClues = ib + 4;
@@ -1177,7 +1195,13 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
 	//   u8  sound    @ +9     (high bit = play voice, low 7 bits = slot)
 	//   u8  textCount@ +10
 	//   u8  textIdx[]@ +11    (1 byte per — low 7 bits = NOTES idx)
-	if (!rec || !isFloppy() || !_font.isLoaded() || count == 0)
+	// Mac uses the same logical fields, but widens picY/balloon/ballY to
+	// big-endian u16: 6 words, then u8 sound, u8 textCount, textIdx[].
+	const bool mac = isMacintosh();
+	const EEMFont &dialogFont =
+		(mac && _dialogFont.isLoaded()) ? _dialogFont : _font;
+	if (!rec || (!isFloppy() && !mac) || !dialogFont.isLoaded() ||
+		count == 0)
 		return;
 
 	const byte *notes   = _mystery.noteIndex();
@@ -1185,8 +1209,11 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
 	if (!notes || !bufBase)
 		return;
 
+	const int sw = screenWidth();
+	const int sh = screenHeight();
+
 	// Snapshot BG for between-bubble restores.
-	Graphics::ManagedSurface bg(kScreenWidth, kScreenHeight,
+	Graphics::ManagedSurface bg(sw, sh,
 		Graphics::PixelFormat::createFormatCLUT8());
 	{
 		Graphics::Surface *screen = g_system->lockScreen();
@@ -1198,37 +1225,44 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
 
 	const uint32 dsz       = _mystery.dataSize();
 	const uint32 notesBase = (uint32)(notes - bufBase);
+	const uint noteStride  = isMacintosh() ? 8 : 7;
 
 	{
 		const byte *r = rec;
 		for (uint i = 0; i < count; i++) {
-			const uint8 tc = r[10];
+			const uint8 tc = mac ? r[13] : r[10];
+			const byte *textIdx = mac ? r + 14 : r + 11;
 			for (uint t = 0; t < tc; t++) {
-				const uint8 idx = r[11 + t] & 0x7f;
+				const uint8 idx = textIdx[t] & 0x7f;
 				if (idx < Mystery::kCluesFoundCap)
 					_mystery._cluesFound[idx] = 1;
 			}
-			r += 11 + tc;
+			r += (mac ? 14 : 11) + tc;
 		}
 	}
 
 	for (uint i = 0; i < count && !shouldQuit(); i++) {
-		const uint16 picID    = READ_LE_UINT16(rec + 0);
-		const uint16 picX     = READ_LE_UINT16(rec + 2);
-		const uint8  picY     = rec[4];
-		const uint8  balByte  = rec[5];
-		const uint16 ballX    = READ_LE_UINT16(rec + 6);
-		const uint8  ballY    = rec[8];
-		const uint8  textCount= rec[10];
+		const uint16 picID = mac ? READ_BE_UINT16(rec + 0)
+								 : READ_LE_UINT16(rec + 0);
+		const uint16 picX = mac ? READ_BE_UINT16(rec + 2)
+								: READ_LE_UINT16(rec + 2);
+		const uint16 picY = mac ? READ_BE_UINT16(rec + 4) : rec[4];
+		const uint16 balByte = mac ? READ_BE_UINT16(rec + 6) : rec[5];
+		const uint16 ballX = mac ? READ_BE_UINT16(rec + 8)
+								 : READ_LE_UINT16(rec + 6);
+		const uint16 ballY = mac ? READ_BE_UINT16(rec + 10) : rec[8];
+		const uint8 soundByte = mac ? rec[12] : rec[9];
+		const uint8 textCount = mac ? rec[13] : rec[10];
+		const byte *textIdx = mac ? rec + 14 : rec + 11;
 
 		// byte 9: high bit = play voice slot.
-		const bool playedRecordVoice = (rec[9] & 0x80) != 0 && _audio;
+		const bool playedRecordVoice = (soundByte & 0x80) != 0 && _audio;
 		if (playedRecordVoice) {
-			const uint slot = rec[9] & 0x7f;
+			const uint slot = soundByte & 0x7f;
 			_audio->playFloppyVoiceSlot(slot, _partner);
 		}
 
-		const uint8 b9 = rec[9];
+		const uint8 b9 = soundByte;
 		if ((b9 & 0x80) == 0 && b9 != 0) {
 			const uint logicalIdx = (uint)b9 - 1;
 			if (logicalIdx < Mystery::kGalleryCap) {
@@ -1242,18 +1276,18 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
 		Common::String fitPage;
 		uint16 fitXIns = 0;
 		uint16 fitYIns = 0;
-		uint16 fitWidth = 142;
+		uint16 fitWidth = mac ? (uint16)scaleX(142) : 142;
 		getBalloonInsets(balByte, fitXIns, fitYIns, fitWidth);
 		uint fitTextLines = 0;
 		for (uint t = 0; t < textCount; t++) {
-			const uint8 idxByte = rec[11 + t];
+			const uint8 idxByte = textIdx[t];
 			const uint8 idx = idxByte & 0x7f;
-			const uint32 noteAbs = notesBase + (uint32)idx * 7;
+			const uint32 noteAbs = notesBase + (uint32)idx * noteStride;
 			if (noteAbs + 6 > dsz)
 				continue;
 			const uint16 textOff = (_partner == kPartnerJake)
-				? READ_LE_UINT16(notes + idx * 7 + 2)
-				: READ_LE_UINT16(notes + idx * 7 + 4);
+				? READ_LE_UINT16(notes + idx * noteStride + 2)
+				: READ_LE_UINT16(notes + idx * noteStride + 4);
 			if (textOff >= dsz)
 				continue;
 			const char *linePtr = (const char *)(bufBase + textOff);
@@ -1271,8 +1305,8 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
 				(idxByte & 0x80) != 0 && t + 1 < textCount;
 			if (!continuePage) {
 				Common::Array<Common::String> wrapped;
-				_font.wordWrapText(fitPage, MAX<int>(8, (int)fitWidth),
-					wrapped);
+				dialogFont.wordWrapText(fitPage, MAX<int>(8, (int)fitWidth),
+										wrapped);
 				if (wrapped.size() > fitTextLines ||
 					(wrapped.size() == fitTextLines &&
 					 fitPage.size() > fitText.size())) {
@@ -1292,29 +1326,30 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
 		const bool   haveBalloon = balByte != 0xFF &&
 			_balloonArchive.size() > balloonId &&
 			_balloonArchive.loadEntry(balloonId, balloon);
-		uint16 textWidth = 142;
-		uint16 textXIns  = 6;
-		uint16 textYIns  = 4;
+		uint16 textWidth = mac ? (uint16)scaleX(142) : 142;
+		uint16 textXIns  = mac ? (uint16)scaleX(6) : 6;
+		uint16 textYIns  = mac ? (uint16)scaleY(4) : 4;
 		if (haveBalloon)
 			getBalloonInsets(balloonId, textXIns, textYIns, textWidth);
 		const int textX = ballX + textXIns;
-		const int lineH    = _font.getFontHeight();
+		const int lineH = dialogFont.getFontHeight();
+		const byte textColor = mac ? 0xff : 0;
 
 		bool firstPage  = true;
 		int  cursorY    = ballY + textYIns;
 		bool skipAll    = false;
 
 		for (uint t = 0; t < textCount && !shouldQuit() && !skipAll; t++) {
-			const uint8 idxByte = rec[11 + t];
+			const uint8 idxByte = textIdx[t];
 			const uint8 idx     = idxByte & 0x7f;
 			if (idx < Mystery::kCluesFoundCap)
 				_mystery._cluesFound[idx] = 1;
-			const uint32 noteAbs = notesBase + (uint32)idx * 7;
+			const uint32 noteAbs = notesBase + (uint32)idx * noteStride;
 			if (noteAbs + 6 > dsz)
 				break;
 			const uint16 textOff = (_partner == kPartnerJake)
-				? READ_LE_UINT16(notes + idx * 7 + 2)
-				: READ_LE_UINT16(notes + idx * 7 + 4);
+				? READ_LE_UINT16(notes + idx * noteStride + 2)
+				: READ_LE_UINT16(notes + idx * noteStride + 4);
 			if (textOff >= dsz)
 				break;
 			const char *linePtr = (const char *)(bufBase + textOff);
@@ -1326,7 +1361,7 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
 				parseString(raw, _playerName, _partner);
 
 			// Render this text page.
-			Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+			Graphics::ManagedSurface scratch(sw, sh,
 				Graphics::PixelFormat::createFormatCLUT8());
 			scratch.simpleBlitFrom(*bg.surfacePtr());
 
@@ -1349,11 +1384,11 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
 			}
 
 			Common::Array<Common::String> lines;
-			_font.wordWrapText(text, MAX<int>(8, (int)textWidth), lines);
+			dialogFont.wordWrapText(text, MAX<int>(8, (int)textWidth), lines);
 			for (uint l = 0; l < lines.size(); l++) {
-				_font.drawString(&scratch, lines[l], textX,
-								  cursorY + (int)l * lineH,
-								  MAX<int>(8, (int)textWidth), 0);
+				dialogFont.drawString(&scratch, lines[l], textX,
+									  cursorY + (int)l * lineH,
+									  MAX<int>(8, (int)textWidth), textColor);
 			}
 			cursorY += (int)lines.size() * lineH;
 
@@ -1395,7 +1430,7 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
 			}
 
 			g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
-									   0, 0, kScreenWidth, kScreenHeight);
+									   0, 0, sw, sh);
 			g_system->updateScreen();
 
 			if (waitNeeded) {
@@ -1416,13 +1451,19 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
 		if (playedRecordVoice)
 			_audio->stopVoice();
 
-		rec += 11 + textCount;
+		rec += (mac ? 14 : 11) + textCount;
 	}
 }
 
 void EEMEngine::displayFloppyBriefing(const byte *initBlock) {
-	if (!initBlock || !isFloppy())
+	if (!initBlock || (!isFloppy() && !isMacintosh()))
 		return;
+	if (isMacintosh()) {
+		const uint16 nDialog = READ_LE_UINT16(initBlock + 2);
+		const byte *rec = initBlock + 4;
+		displayFloppyDialogRecords(rec, nDialog);
+		return;
+	}
 	const uint8 nSubjects = initBlock[1];
 	const uint8 nDialog   = initBlock[2 + nSubjects];
 	const byte *rec       = initBlock + 3 + nSubjects;
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index cfe99d67bec..939158f95b1 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -59,7 +59,7 @@ const uint kPalHighScore       = 0x27;
 const uint kPalStormLogo       = 0x26;  // Floppy FUN_23d2_0605
 const uint kPicMousePointer    = 0x50;  // 0x51 is the wait cursor
 const uint16 kMacFontResource  = 3214;  // 'Eagle Eye 14' FONT resource
-const uint16 kMacSmallFontResource = 3209; // 'Eagle Eye 9' fallback
+const uint16 kMacSmallFontResource = 3209; // Smaller speech/dialog FONT.
 
 // EEM2 cursor table — `_main @ 1abf:0faf` loads seven cursor PICs into
 // `_AnimationObjects`-adjacent slots and `_SwitchMouse @ 17ee:2c83` activates
@@ -341,24 +341,30 @@ static void addMacResourceSearchPaths() {
 		SearchMan.addDirectory("eem-mac-rsrc-sibling", siblingRsrcDir);
 }
 
-static bool loadMacFont(EEMFont &font) {
+static bool loadMacFontResource(EEMFont &font, uint16 resourceId, int size) {
 	addMacResourceSearchPaths();
 
 	const Common::Path appResourceFork("Eagle Eye Mysteries");
-	if (font.loadMacResource(appResourceFork, kMacFontResource, 14))
-		return true;
-	if (font.loadMacResource(appResourceFork, kMacSmallFontResource, 9))
+	if (font.loadMacResource(appResourceFork, resourceId, size))
 		return true;
 
 	const Common::Path nestedAppResourceFork("rsrc/Eagle Eye Mysteries");
-	if (font.loadMacResource(nestedAppResourceFork, kMacFontResource, 14))
-		return true;
-	if (font.loadMacResource(nestedAppResourceFork, kMacSmallFontResource, 9))
+	if (font.loadMacResource(nestedAppResourceFork, resourceId, size))
 		return true;
 
 	return false;
 }
 
+static bool loadMacFont(EEMFont &font) {
+	return loadMacFontResource(font, kMacFontResource, 14) ||
+		   loadMacFontResource(font, kMacSmallFontResource, 9);
+}
+
+static bool loadMacDialogFont(EEMFont &font) {
+	return loadMacFontResource(font, kMacSmallFontResource, 9) ||
+		   loadMacFontResource(font, kMacFontResource, 14);
+}
+
 Common::Error EEMEngine::run() {
 	initGraphics(screenWidth(), screenHeight());
 
@@ -381,10 +387,14 @@ Common::Error EEMEngine::run() {
 
 	// _LoadFont @ 1b66:023c. The Mac release stores its Eagle Eye fonts in
 	// the application resource fork instead of a DOS FONT.FNT file.
-	if (isMacintosh() && !loadMacFont(_font))
-		warning("Mac FONT resource failed to load; text will not render");
-	else if (!isMacintosh() && !_font.load(Common::Path("FONT.FNT")))
+	if (isMacintosh()) {
+		if (!loadMacFont(_font))
+			warning("Mac FONT resource failed to load; text will not render");
+		if (!loadMacDialogFont(_dialogFont))
+			warning("Mac dialog FONT resource failed to load");
+	} else if (!_font.load(Common::Path("FONT.FNT"))) {
 		warning("FONT.FNT failed to load; text will not render");
+	}
 
 	// _InitMIDI @ 20a2:013a. The demo ships no music. The Mac release stores
 	// SMF MIDI resources in EEM Sound&Music instead of loose DOS XMIDI files.
@@ -1122,7 +1132,7 @@ void EEMEngine::showLondonLogo(uint picId, uint palId, uint holdMs) {
 }
 
 bool EEMEngine::startLondonTrainingMystery() {
-	if (_mystery.load(0, &_rng)) {
+	if (_mystery.load(0, &_rng, isMacintosh())) {
 		resetSiteArrivalState();
 		if (_audio)
 			_audio->initMysterySounds(0);
@@ -1612,7 +1622,7 @@ Common::Error EEMEngine::loadGameStream(Common::SeekableReadStream *stream) {
 	if (hasMystery) {
 		uint16 mysteryNum = 0;
 		s.syncAsUint16LE(mysteryNum);
-		if (!_mystery.load(mysteryNum, &_rng)) {
+		if (!_mystery.load(mysteryNum, &_rng, isMacintosh())) {
 			_mystery.clear();
 			resetSiteArrivalState();
 			return Common::kReadingFailed;
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 00ba2fc9367..f99c4086b79 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -537,6 +537,7 @@ private:
 	DBDArchive _buttonArchive;   ///< BUTTON.DBD/.DBX (`_GetButton`)
 	Mystery    _mystery;         ///< M<n>.BIN
 	EEMFont    _font;            ///< FONT.FNT (8 px)
+	EEMFont    _dialogFont;      ///< Mac small FONT used inside speech balloons.
 
 	Common::Array<byte> _sitePals; ///< 40 × 768 bytes, 6-bit VGA.
 
diff --git a/engines/eem/graphics.cpp b/engines/eem/graphics.cpp
index c6c5dd08d43..744121049be 100644
--- a/engines/eem/graphics.cpp
+++ b/engines/eem/graphics.cpp
@@ -772,7 +772,7 @@ void EEMEngine::setPartnerEraseBg(const Graphics::ManagedSurface *bg) {
 uint16 EEMEngine::fitBalloonToText(uint16 bubNum,
 								   const Common::String &text) {
 	// Opt-in via "fit_dialog_balloons", CD only
-	if (isFloppy() || !ConfMan.getBool("fit_dialog_balloons"))
+	if (isFloppy() || isMacintosh() || !ConfMan.getBool("fit_dialog_balloons"))
 		return bubNum;
 
 	const uint16 originalId = bubNum & 0x7F;
@@ -837,9 +837,15 @@ bool EEMEngine::getBalloonInsets(uint16 bubNum, uint16 &xInset,
 	const uint idx = bubNum & 0x7F;
 	if (idx >= ARRAYSIZE(kBalloonInsetTable))
 		return false;
-	xInset = kBalloonInsetTable[idx].x;
-	yInset = kBalloonInsetTable[idx].y;
-	textW  = kBalloonInsetTable[idx].w;
+	if (isMacintosh()) {
+		xInset = (uint16)scaleX(kBalloonInsetTable[idx].x);
+		yInset = (uint16)scaleY(kBalloonInsetTable[idx].y);
+		textW  = (uint16)scaleX(kBalloonInsetTable[idx].w);
+	} else {
+		xInset = kBalloonInsetTable[idx].x;
+		yInset = kBalloonInsetTable[idx].y;
+		textW  = kBalloonInsetTable[idx].w;
+	}
 	return true;
 }
 
@@ -848,8 +854,13 @@ bool EEMEngine::getBalloonIndicatorPos(uint16 bubNum, uint16 &dx,
 	const uint idx = bubNum & 0x7F;
 	if (idx >= ARRAYSIZE(kBalloonInsetTable))
 		return false;
-	dx = kBalloonInsetTable[idx].indDX;
-	dy = kBalloonInsetTable[idx].indDY;
+	if (isMacintosh()) {
+		dx = (uint16)scaleX(kBalloonInsetTable[idx].indDX);
+		dy = (uint16)scaleY(kBalloonInsetTable[idx].indDY);
+	} else {
+		dx = kBalloonInsetTable[idx].indDX;
+		dy = kBalloonInsetTable[idx].indDY;
+	}
 	return true;
 }
 
diff --git a/engines/eem/mystery.cpp b/engines/eem/mystery.cpp
index 1734c804175..65e2a87c3fc 100644
--- a/engines/eem/mystery.cpp
+++ b/engines/eem/mystery.cpp
@@ -30,6 +30,91 @@
 
 namespace EEM {
 
+const uint kMacMysteryCount = 55;
+const uint kMacMysteryDataTableOffset = 0x08cd;
+
+static void swapU16Range(Common::Array<byte> &data, uint start, uint end) {
+	end = MIN<uint>(end, data.size());
+	if (start >= end)
+		return;
+	for (uint pos = start; pos + 1 < end; pos += 2) {
+		const byte tmp = data[pos];
+		data[pos] = data[pos + 1];
+		data[pos + 1] = tmp;
+	}
+}
+
+static bool loadMacMysteryBlob(uint num, Common::Array<byte> &out) {
+	if (num >= kMacMysteryCount)
+		return false;
+
+	Common::File f;
+	if (!f.open(Common::Path("MysteryData"))) {
+		warning("Mystery::load: cannot open MysteryData");
+		return false;
+	}
+
+	const uint32 tableOff = kMacMysteryDataTableOffset + num * 8;
+	if (tableOff + 8 > (uint32)f.size()) {
+		warning("Mystery::load: MysteryData index %u out of range", num);
+		return false;
+	}
+
+	f.seek(tableOff);
+	const uint32 offset = f.readUint32BE();
+	const uint32 size = f.readUint32BE();
+	if (size <= 20 || offset > (uint32)f.size() ||
+		size > (uint32)f.size() - offset) {
+		warning("Mystery::load: MysteryData entry %u is invalid "
+				"(off=0x%08x size=0x%08x)", num, offset, size);
+		return false;
+	}
+
+	out.resize(size);
+	f.seek(offset);
+	if (f.read(out.data(), size) != size) {
+		warning("Mystery::load: short read on MysteryData entry %u", num);
+		out.clear();
+		return false;
+	}
+
+	return true;
+}
+
+static uint16 readBE16(const Common::Array<byte> &data, uint offset) {
+	if (offset + 2 > data.size())
+		return 0;
+	return READ_BE_UINT16(data.data() + offset);
+}
+
+static void normalizeMacMystery(Common::Array<byte> &data) {
+	if (data.size() <= 20)
+		return;
+
+	uint16 section[10];
+	for (uint i = 0; i < ARRAYSIZE(section); i++)
+		section[i] = readBE16(data, i * 2);
+
+	swapU16Range(data, 0, 20); // offset header
+
+	// Compact init block: u8 caseType, u8 startSite, u16 dialog count,
+	// followed by Mac-native big-endian dialog records. Keep the records
+	// byte-exact; their widened coordinate fields are parsed by the Mac
+	// briefing path.
+	if (section[0] + 4 <= data.size())
+		swapU16Range(data, section[0] + 2, section[0] + 4);
+
+	// Remaining structured regions. The text regions at section[6] and
+	// section[7] are raw strings and must stay byte-exact.
+	swapU16Range(data, section[1], section[2]); // clue/site chain table
+	swapU16Range(data, section[2] + 1, section[3]); // counted map entries
+	swapU16Range(data, section[3], section[4]); // site index + site data
+	swapU16Range(data, section[4], section[7]); // notebook index
+	swapU16Range(data, section[5] + 1, section[6]); // compact gallery metadata
+	swapU16Range(data, section[8], MIN<uint>(section[8] + 12, section[9]));
+	swapU16Range(data, section[9], data.size()); // solved clue block
+}
+
 uint16 Mystery::readU16(uint offset) const {
 	if (offset + 2 > _data.size())
 		return 0;
@@ -45,6 +130,7 @@ void Mystery::clear() {
 	_numSites = 0;
 	_numSuspects = _numCONSITEs = _numCOFFSITEs = 0;
 	_isFloppy = false;
+	_isMacintosh = false;
 	_floppySuspectsOff = _floppyHintBlockOff = _floppyNoteIndexOff = 0;
 	_floppyGalleryOff = _floppyTextOff = _floppyKDTextOff = 0;
 	_floppySolvedOff = 0;
@@ -70,32 +156,88 @@ void Mystery::clear() {
 	memset(_siteReturnStack, 0, sizeof(_siteReturnStack));
 }
 
-bool Mystery::load(uint num, Common::RandomSource *rng) {
-	const Common::String fname = Common::String::format("M%u.BIN", num);
-	Common::File f;
-	if (!f.open(Common::Path(fname))) {
-		warning("Mystery::load: cannot open %s", fname.c_str());
-		return false;
-	}
+bool Mystery::load(uint num, Common::RandomSource *rng, bool macintosh) {
+	Common::String fname = Common::String::format("M%u.BIN", num);
+	Common::Array<byte> staging;
+
+	if (macintosh) {
+		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))) {
+			warning("Mystery::load: cannot open %s", fname.c_str());
+			return false;
+		}
 
-	const int32 size = f.size();
-	if (size <= 64) {
-		warning("Mystery::load: %s too small (%d bytes)", fname.c_str(), size);
-		return false;
-	}
+		const int32 size = f.size();
+		if (size <= 64) {
+			warning("Mystery::load: %s too small (%d bytes)", fname.c_str(), size);
+			return false;
+		}
 
-	// Stage to a temporary buffer so a short read leaves the previous
-	// mystery state intact instead of half-clobbering `_data`.
-	Common::Array<byte> staging(size);
-	if (f.read(staging.data(), size) != (uint32)size) {
-		warning("Mystery::load: short read on %s", fname.c_str());
-		return false;
+		// Stage to a temporary buffer so a short read leaves the previous
+		// mystery state intact instead of half-clobbering `_data`.
+		staging.resize(size);
+		if (f.read(staging.data(), size) != (uint32)size) {
+			warning("Mystery::load: short read on %s", fname.c_str());
+			return false;
+		}
+		f.close();
 	}
-	f.close();
 
 	_data = staging;
 	_number = num;
 
+	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;
+		if (_numSites > kVisitedSiteCap)
+			_numSites = kVisitedSiteCap;
+
+		memset(_cluesFound, 0, sizeof(_cluesFound));
+		memset(_noteSelected, 0, sizeof(_noteSelected));
+		memset(_hotSpotsSeen, 0, sizeof(_hotSpotsSeen));
+		memset(_inGallery, 0, sizeof(_inGallery));
+		(void)rng;
+		for (uint i = 0; i < kGalleryCap; i++)
+			_newOrder[i] = (uint8)i;
+		memset(_visitedSite, 0, sizeof(_visitedSite));
+		memset(_onSites, 0, sizeof(_onSites));
+		_sawCOFFSITEs = _sawCONSITEs = _sawHelpHint = _solvedPuzzle = false;
+		_seenCOFFSITEs = _seenCONSITEs = 0;
+		_firstTry = true;
+		_searchLocationNumber = _siteNumber = 0xFFFF;
+		_lastSite = 0x1B;
+		_pendingSiteJump = 0;
+		_siteReturnDepth = 0;
+		memset(_siteReturnStack, 0, sizeof(_siteReturnStack));
+
+		debugC(1, kDebugMystery,
+			   "Loaded %s (%u B): %u sites, %u suspects, init=0x%04x "
+			   "map=0x%04x site=0x%04x text=0x%04x notes=0x%04x",
+			   fname.c_str(), (uint)_data.size(), _numSites, _numSuspects,
+			   _initOffset, _mapOffset, _siteIndexOffset, _textOffset,
+			   _noteOffset);
+		return true;
+	}
+
 	// Floppy M*.BIN uses a different header layout from CD.
 	// _ReadMystery_Floppy @ 22dc:0178. Section-pointer header:
 	//   header[+0]    InitBlock byte offset (caseType byte at *(buf+initOff))
@@ -219,7 +361,7 @@ bool Mystery::load(uint num, Common::RandomSource *rng) {
 
 	debugC(1, kDebugMystery, "Loaded %s (%d B): %u sites, %u suspects, "
 		   "CON=%u COFF=%u, init=0x%04x site=0x%04x text=0x%04x",
-		   fname.c_str(), size, _numSites, _numSuspects,
+		   fname.c_str(), (int)_data.size(), _numSites, _numSuspects,
 		   _numCONSITEs, _numCOFFSITEs,
 		   _initOffset, _siteIndexOffset, _textOffset);
 	return true;
@@ -230,7 +372,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 ? 2 : 6;
+	const uint stride = (_isFloppy || _isMacintosh) ? 2 : 6;
 	const uint off = _siteIndexOffset + siteNum * stride;
 	if (off + stride > _data.size())
 		return nullptr;
@@ -316,7 +458,7 @@ void Mystery::loadFloppySiteAnimData() {
 }
 
 const byte *Mystery::hotspots(uint siteNum) const {
-	if (_isFloppy) {
+	if (_isFloppy || _isMacintosh) {
 		const byte *site = siteData(siteNum);
 		if (!site || (size_t)(site - _data.data()) + 6 > _data.size())
 			return nullptr;
@@ -335,7 +477,7 @@ const byte *Mystery::hotspots(uint siteNum) const {
 }
 
 uint16 Mystery::hotspotCount(uint siteNum) const {
-	if (_isFloppy) {
+	if (_isFloppy || _isMacintosh) {
 		const byte *site = siteData(siteNum);
 		if (!site || (size_t)(site - _data.data()) + 6 > _data.size())
 			return 0;
@@ -353,7 +495,7 @@ uint16 Mystery::hotspotCount(uint siteNum) const {
 const char *Mystery::textAt(uint16 offset) const {
 	if (!isLoaded())
 		return "";
-	const uint pos = _textOffset + offset;
+	const uint pos = _isMacintosh ? offset : _textOffset + offset;
 	if (pos >= _data.size())
 		return "";
 	return (const char *)(_data.data() + pos);
@@ -383,16 +525,18 @@ uint16 Mystery::noteIndexCount() const {
 	// NoteIndex runs from _noteOffset to start of GalleryData.
 	// CD entries: 4 bytes (u16 textOff; u16 points).
 	// Floppy entries: 7 bytes (u16 ?; u16 jakeOff; u16 jennyOff; u8 score)
-	if (_galleryOffset <= _noteOffset)
+	const uint endOffset = _isMacintosh ? _textOffset : _galleryOffset;
+	if (endOffset <= _noteOffset)
 		return 0;
-	const uint stride = _isFloppy ? 7 : 4;
-	return (uint16)((_galleryOffset - _noteOffset) / stride);
+	const uint stride = _isMacintosh ? 8 : (_isFloppy ? 7 : 4);
+	return (uint16)((endOffset - _noteOffset) / stride);
 }
 
 uint Mystery::noteSectionSize() const {
-	if (!isLoaded() || _galleryOffset <= _noteOffset)
+	const uint endOffset = _isMacintosh ? _textOffset : _galleryOffset;
+	if (!isLoaded() || endOffset <= _noteOffset)
 		return 0;
-	return _galleryOffset - _noteOffset;
+	return endOffset - _noteOffset;
 }
 
 bool Mystery::noteHasNotebookText(uint clueId) const {
@@ -418,6 +562,13 @@ const byte *Mystery::kdTextIndex() const {
 const byte *Mystery::mapEntry(uint siteNum) const {
 	if (!isLoaded() || siteNum >= _numSites)
 		return nullptr;
+	if (_isMacintosh) {
+		// Mac SITES section: byte[0]=count, then 12-byte entries.
+		const uint off = _mapOffset + 1 + siteNum * 12;
+		if (off + 12 > _data.size())
+			return nullptr;
+		return _data.data() + off;
+	}
 	if (_isFloppy) {
 		// Floppy SITES section (FUN_1fed_07ed): byte[0]=count, then 11-byte
 		// entries starting at byte 1.
diff --git a/engines/eem/mystery.h b/engines/eem/mystery.h
index 86f13dc7aeb..ca10d929e1b 100644
--- a/engines/eem/mystery.h
+++ b/engines/eem/mystery.h
@@ -59,8 +59,10 @@ public:
 	Mystery() = default;
 	~Mystery() = default;
 
-	/// Load M<num>.BIN and reset per-mystery state. Returns false on error.
-	bool load(uint num, class Common::RandomSource *rng = nullptr);
+	/// Load M<num>.BIN and reset per-mystery state. The Mac release stores
+	/// cases in the indexed MysteryData container instead of loose files.
+	bool load(uint num, class Common::RandomSource *rng = nullptr,
+			  bool macintosh = false);
 
 	void clear();
 
@@ -238,6 +240,7 @@ private:
 	uint16 _cChain[kChainLen] = {};
 
 	bool   _isFloppy = false;
+	bool   _isMacintosh = 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/ui.cpp b/engines/eem/ui.cpp
index d0bca0e0e6a..af55ce31d62 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -113,6 +113,76 @@ void blitBigMapMarker(Graphics::ManagedSurface &dstSurface, const Picture &marke
 	}
 }
 
+struct BigMapEntryInfo {
+	uint16 overviewX = 0;
+	uint16 overviewY = 0;
+	uint16 detailX = 0;
+	uint16 detailY = 0;
+	uint16 buttonId = 0;
+	uint16 crime = 0;
+};
+
+bool readBigMapEntryInfo(const byte *entry, bool floppy, bool macintosh,
+						 BigMapEntryInfo &out) {
+	if (!entry)
+		return false;
+
+	if (macintosh) {
+		out.detailX   = READ_LE_UINT16(entry + 0x0);
+		out.detailY   = READ_LE_UINT16(entry + 0x2);
+		out.buttonId  = entry[0x4];
+		out.overviewX = READ_LE_UINT16(entry + 0x6);
+		out.overviewY = READ_LE_UINT16(entry + 0x8);
+		out.crime     = READ_LE_UINT16(entry + 0xa);
+		return true;
+	}
+
+	if (floppy) {
+		out.detailX   = READ_LE_UINT16(entry + 0x0);
+		out.detailY   = READ_LE_UINT16(entry + 0x2);
+		out.buttonId  = entry[0x4];
+		out.overviewX = READ_LE_UINT16(entry + 0x6);
+		out.overviewY = READ_LE_UINT16(entry + 0x8);
+		out.crime     = entry[0xa];
+		return true;
+	}
+
+	out.buttonId  = READ_LE_UINT16(entry + 0x0);
+	out.overviewX = READ_LE_UINT16(entry + 0x4);
+	out.overviewY = READ_LE_UINT16(entry + 0x6);
+	out.detailX   = READ_LE_UINT16(entry + 0x8);
+	out.detailY   = READ_LE_UINT16(entry + 0xa);
+	out.crime     = READ_LE_UINT16(entry + 0xc);
+	return true;
+}
+
+bool loadMacBigMapPixels(Common::Array<byte> &mapPixels,
+						 uint16 &mapW, uint16 &mapH) {
+	DBDArchive bigMapArchive;
+	if (!bigMapArchive.open(Common::Path("BIGMAP.DBD"),
+							Common::Path("BIGMAP.DBX"), true)) {
+		warning("doBigMap: BIGMAP archive missing");
+		return false;
+	}
+
+	Picture mapPic;
+	if (!bigMapArchive.loadEntry(0, mapPic) || mapPic.surface.empty()) {
+		warning("doBigMap: BIGMAP.DBD entry 0 failed to load");
+		return false;
+	}
+	if (mapPic.surface.w <= 0 || mapPic.surface.h <= 0)
+		return false;
+
+	mapW = (uint16)mapPic.surface.w;
+	mapH = (uint16)mapPic.surface.h;
+	mapPixels.resize((uint32)mapW * mapH);
+	for (uint y = 0; y < mapH; y++) {
+		const byte *src = (const byte *)mapPic.surface.getBasePtr(0, y);
+		memcpy(mapPixels.data() + y * mapW, src, mapW);
+	}
+	return true;
+}
+
 bool loadLondonApproachData(uint16 approachId, LondonApproachData &out) {
 	Common::File f;
 	const Common::String name = Common::String::format("A%u.BIN", approachId);
@@ -1533,7 +1603,7 @@ void EEMEngine::doSetup() {
 			if (kNewCaseBtn.contains(mx, my)) {
 				saveProfile(_playerName);
 				if (isDemo()) {
-					if (_mystery.load(0, &_rng)) {
+					if (_mystery.load(0, &_rng, isMacintosh())) {
 						resetSiteArrivalState();
 						_nextScreen = kScreenInitClues;
 					} else {
@@ -2090,7 +2160,7 @@ void EEMEngine::doActionScreen() {
 	}
 
 	if (pick == kPickPractice) {
-		if (!_mystery.load(0, &_rng)) {
+		if (!_mystery.load(0, &_rng, isMacintosh())) {
 			warning("doActionScreen: failed to load practice mystery");
 			_mystery.clear();
 			resetSiteArrivalState();
@@ -2342,7 +2412,7 @@ void EEMEngine::doCaseSelection() {
 		return;
 
 	const uint mn = stageLo + selRow;
-	if (!_mystery.load(mn, &_rng)) {
+	if (!_mystery.load(mn, &_rng, isMacintosh())) {
 		warning("doCaseSelection: failed to load mystery %u", mn);
 		_mystery.clear();
 		return;
@@ -3169,11 +3239,16 @@ void EEMEngine::doBigMap() {
 		drawBigMapOverview(0);
 		uint32 mapLastTick = mapStartTick;
 
-		const Common::Rect kBigMapWindow(0, 0, 247, 192);
-		const Common::Rect kSetupBtnRect = isFloppy()
+		const bool mac = isMacintosh();
+		const Common::Rect bigMapWindowBase(0, 0, 247, 192);
+		const Common::Rect kBigMapWindow =
+			mac ? scaleRect(bigMapWindowBase) : bigMapWindowBase;
+		const Common::Rect setupBtnBase = isFloppy()
 			? Common::Rect(251, 3, 315, 42)
 			: (isLondon() ? Common::Rect(252, 1, 315, 42)
 						  : Common::Rect(252, 4, 315, 42));
+		const Common::Rect kSetupBtnRect =
+			mac ? scaleRect(setupBtnBase) : setupBtnBase;
 
 		bool wantZoom = false;
 		int zoomX = 0;
@@ -3196,12 +3271,17 @@ void EEMEngine::doBigMap() {
 					}
 
 					if (kBigMapWindow.contains(ev.mouse.x, ev.mouse.y)) {
-						int sx = ev.mouse.x * 2;
-						int sy = ev.mouse.y * 2;
-						sx = (sx < 0x75) ? 0 : sx - 0x74;
-						sy = (sy < 0x56) ? 0 : sy - 0x55;
-						zoomX = sx;
-						zoomY = sy;
+						if (mac) {
+							zoomX = ev.mouse.x - kBigMapWindow.left;
+							zoomY = ev.mouse.y - kBigMapWindow.top;
+						} else {
+							int sx = ev.mouse.x * 2;
+							int sy = ev.mouse.y * 2;
+							sx = (sx < 0x75) ? 0 : sx - 0x74;
+							sy = (sy < 0x56) ? 0 : sy - 0x55;
+							zoomX = sx;
+							zoomY = sy;
+						}
 						wantZoom = true;
 						break;
 					}
@@ -3229,28 +3309,49 @@ void EEMEngine::doBigMap() {
 		if (!wantZoom)
 			return;
 
-		Common::File f;
-		if (!f.open(Common::Path("BIGMAP.PIC"))) {
-			warning("doBigMap: BIGMAP.PIC missing for detail view");
-			return;
-		}
-		const uint16 mapH = f.readUint16LE();
-		const uint16 mapW = f.readUint16LE();
-		if (mapW == 0 || mapH == 0)
-			return;
-		Common::Array<byte> mapPixels((uint32)mapW * mapH);
-		if (f.read(mapPixels.data(), mapPixels.size()) != mapPixels.size()) {
-			warning("doBigMap: short read on BIGMAP.PIC for detail view");
-			return;
+		uint16 mapW = 0;
+		uint16 mapH = 0;
+		Common::Array<byte> mapPixels;
+		if (mac) {
+			if (!loadMacBigMapPixels(mapPixels, mapW, mapH))
+				return;
+		} else {
+			Common::File f;
+			if (!f.open(Common::Path("BIGMAP.PIC"))) {
+				warning("doBigMap: BIGMAP.PIC missing for detail view");
+				return;
+			}
+			mapH = f.readUint16LE();
+			mapW = f.readUint16LE();
+			if (mapW == 0 || mapH == 0)
+				return;
+			mapPixels.resize((uint32)mapW * mapH);
+			if (f.read(mapPixels.data(), mapPixels.size()) != mapPixels.size()) {
+				warning("doBigMap: short read on BIGMAP.PIC for detail view");
+				return;
+			}
 		}
 
-		const int kMapWinW = 0xe9; // 233
-		const int kMapWinH = 0xab; // 171
-		const int kMapWinX = 2;
-		const int kMapWinY = 2;
-
-		int scrollX = MAX<int>(0, MIN<int>(mapW - kMapWinW, zoomX));
-		int scrollY = MAX<int>(0, MIN<int>(mapH - kMapWinH, zoomY));
+		const int kMapWinW = mac ? scaleX(0xe9) : 0xe9; // 233
+		const int kMapWinH = mac ? scaleY(0xab) : 0xab; // 171
+		const int kMapWinX = mac ? scaleX(2) : 2;
+		const int kMapWinY = mac ? scaleY(2) : 2;
+
+		const int maxScrollX = MAX<int>(0, (int)mapW - kMapWinW);
+		const int maxScrollY = MAX<int>(0, (int)mapH - kMapWinH);
+		int scrollX;
+		int scrollY;
+		if (mac) {
+			scrollX = zoomX * (int)mapW /
+				MAX<int>(1, kBigMapWindow.width()) - kMapWinW / 2;
+			scrollY = zoomY * (int)mapH /
+				MAX<int>(1, kBigMapWindow.height()) - kMapWinH / 2;
+		} else {
+			scrollX = zoomX;
+			scrollY = zoomY;
+		}
+		scrollX = MAX<int>(0, MIN<int>(maxScrollX, scrollX));
+		scrollY = MAX<int>(0, MIN<int>(maxScrollY, scrollY));
 
 		setSitePalette(isLondon() ? 0x3a : 0x23);
 
@@ -3259,24 +3360,42 @@ void EEMEngine::doBigMap() {
 		uint32 detailLastTick = detailStartTick;
 		bool returnToOverview = false;
 
-		const Common::Rect kBigMapReturnRect(252, 43, kScreenWidth, kScreenHeight);
-		const Common::Rect kArrowYUp(237, 2, 247, 11);
-		const Common::Rect kArrowYDown(237, 163, 247, 172);
-		const Common::Rect kArrowXLeft(2, 175, 12, 185);
-		const Common::Rect kArrowXRight(224, 175, 234, 185);
-		const Common::Rect kXSlider = isLondon()
+		const Common::Rect returnBase(252, 43, kScreenWidth, kScreenHeight);
+		const Common::Rect kBigMapReturnRect =
+			mac ? scaleRect(returnBase) : returnBase;
+		const Common::Rect kArrowYUp =
+			mac ? scaleRect(Common::Rect(237, 2, 247, 11))
+				: Common::Rect(237, 2, 247, 11);
+		const Common::Rect kArrowYDown =
+			mac ? scaleRect(Common::Rect(237, 163, 247, 172))
+				: Common::Rect(237, 163, 247, 172);
+		const Common::Rect kArrowXLeft =
+			mac ? scaleRect(Common::Rect(2, 175, 12, 185))
+				: Common::Rect(2, 175, 12, 185);
+		const Common::Rect kArrowXRight =
+			mac ? scaleRect(Common::Rect(224, 175, 234, 185))
+				: Common::Rect(224, 175, 234, 185);
+		const Common::Rect xSliderBase = isLondon()
 			? Common::Rect(15, 176, 220, 184)
 			: Common::Rect(15, 175, 221, 185);
-		const Common::Rect kYSlider = isLondon()
+		const Common::Rect ySliderBase = isLondon()
 			? Common::Rect(238, 16, 246, 158)
 			: Common::Rect(237, 14, 247, 160);
-		const Common::Rect kDetailSetupBtn = isFloppy()
+		const Common::Rect kXSlider =
+			mac ? scaleRect(xSliderBase) : xSliderBase;
+		const Common::Rect kYSlider =
+			mac ? scaleRect(ySliderBase) : ySliderBase;
+		const Common::Rect detailSetupBase = isFloppy()
 			? Common::Rect(251, 3, 315, 42)
 			: (isLondon() ? Common::Rect(251, 3, 315, 42)
 						  : Common::Rect(252, 4, 315, 42));
-		const int kArrowStep = isLondon() ? 8 : 16;
-		const int kSliderRange = mapW - kMapWinW;
-		const int kSliderRangeY = mapH - kMapWinH;
+		const Common::Rect kDetailSetupBtn =
+			mac ? scaleRect(detailSetupBase) : detailSetupBase;
+		const int baseArrowStep = isLondon() ? 8 : 16;
+		const int kArrowStepX = mac ? scaleX(baseArrowStep) : baseArrowStep;
+		const int kArrowStepY = mac ? scaleY(baseArrowStep) : baseArrowStep;
+		const int kSliderRange = maxScrollX;
+		const int kSliderRangeY = maxScrollY;
 		const Common::Point detailMouse =
 			g_system->getEventManager()->getMousePos();
 		setInteractiveMouseCursor(
@@ -3299,20 +3418,17 @@ void EEMEngine::doBigMap() {
 						dirty = true;
 						continue;
 					}
-					const int kStep = isLondon() ? 8 : 16;
 					if (ev.kbd.keycode == Common::KEYCODE_LEFT) {
-						scrollX = MAX<int>(0, scrollX - kStep);
+						scrollX = MAX<int>(0, scrollX - kArrowStepX);
 						dirty = true;
 					} else if (ev.kbd.keycode == Common::KEYCODE_RIGHT) {
-						scrollX = MIN<int>(MAX<int>(0, mapW - kMapWinW),
-							scrollX + kStep);
+						scrollX = MIN<int>(maxScrollX, scrollX + kArrowStepX);
 						dirty = true;
 					} else if (ev.kbd.keycode == Common::KEYCODE_UP) {
-						scrollY = MAX<int>(0, scrollY - kStep);
+						scrollY = MAX<int>(0, scrollY - kArrowStepY);
 						dirty = true;
 					} else if (ev.kbd.keycode == Common::KEYCODE_DOWN) {
-						scrollY = MIN<int>(MAX<int>(0, mapH - kMapWinH),
-							scrollY + kStep);
+						scrollY = MIN<int>(maxScrollY, scrollY + kArrowStepY);
 						dirty = true;
 					}
 				}
@@ -3333,18 +3449,18 @@ void EEMEngine::doBigMap() {
 						returnToOverview = true;
 						break;
 					} else if (kArrowYUp.contains(ev.mouse.x, ev.mouse.y)) {
-						scrollY = MAX<int>(0, scrollY - kArrowStep);
+						scrollY = MAX<int>(0, scrollY - kArrowStepY);
 						dirty = true;
 					} else if (kArrowYDown.contains(ev.mouse.x, ev.mouse.y)) {
 						scrollY = MIN<int>(MAX<int>(0, kSliderRangeY),
-							scrollY + kArrowStep);
+							scrollY + kArrowStepY);
 						dirty = true;
 					} else if (kArrowXLeft.contains(ev.mouse.x, ev.mouse.y)) {
-						scrollX = MAX<int>(0, scrollX - kArrowStep);
+						scrollX = MAX<int>(0, scrollX - kArrowStepX);
 						dirty = true;
 					} else if (kArrowXRight.contains(ev.mouse.x, ev.mouse.y)) {
 						scrollX = MIN<int>(MAX<int>(0, kSliderRange),
-							scrollX + kArrowStep);
+							scrollX + kArrowStepX);
 						dirty = true;
 					} else if (kXSlider.contains(ev.mouse.x, ev.mouse.y)) {
 						if (kSliderRange > 0) {
@@ -3380,27 +3496,18 @@ void EEMEngine::doBigMap() {
 							const byte *entry = _mystery.mapEntry(i);
 							if (!entry)
 								continue;
-							uint16 mx;
-							uint16 my;
-							uint16 buttonId;
-							if (fmap) {
-								mx = READ_LE_UINT16(entry + 0x0);
-								my = READ_LE_UINT16(entry + 0x2);
-								buttonId = (uint16)entry[0x4];
-							} else {
-								buttonId = READ_LE_UINT16(entry + 0x0);
-								mx       = READ_LE_UINT16(entry + 0x8);
-								my       = READ_LE_UINT16(entry + 0xa);
-							}
+							BigMapEntryInfo info;
+							if (!readBigMapEntryInfo(entry, fmap, mac, info))
+								continue;
 							Picture button;
 							int bw = 16;
 							int bh = 16;
-							if (_buttonArchive.loadEntry(buttonId, button)) {
+							if (_buttonArchive.loadEntry(info.buttonId, button)) {
 								bw = button.surface.w;
 								bh = button.surface.h;
 							}
-							const int sx = (int)mx - scrollX + kMapWinX;
-							const int sy = (int)my - scrollY + kMapWinY;
+							const int sx = (int)info.detailX - scrollX + kMapWinX;
+							const int sy = (int)info.detailY - scrollY + kMapWinY;
 							const Common::Rect r(sx, sy, sx + bw, sy + bh);
 							if (r.intersects(Common::Rect(kMapWinX, kMapWinY,
 									kMapWinX + kMapWinW, kMapWinY + kMapWinH))) {
@@ -3666,7 +3773,10 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
 }
 
 void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
-	Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+	const bool mac = isMacintosh();
+	const int sw = screenWidth();
+	const int sh = screenHeight();
+	Graphics::ManagedSurface scratch(sw, sh,
 		Graphics::PixelFormat::createFormatCLUT8());
 	scratch.clear();
 
@@ -3680,9 +3790,12 @@ void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
 	Picture done;
 	Picture normal;
 	Picture crimeM;
-	const bool haveDone   = _picsArchive.getPicture(isLondon() ? 0x006 : 0x20d, done);
-	const bool haveNormal = _picsArchive.getPicture(0xc5,  normal);
-	const bool haveCrime  = _picsArchive.getPicture(0xc6,  crimeM);
+	const bool haveDone = _picsArchive.getPicture(isLondon() ? 0x006 : 0x20d, done) &&
+		done.surface.w < 64 && done.surface.h < 64;
+	const bool haveNormal = _picsArchive.getPicture(0xc5, normal) &&
+		normal.surface.w < 64 && normal.surface.h < 64;
+	const bool haveCrime = _picsArchive.getPicture(0xc6, crimeM) &&
+		crimeM.surface.w < 64 && crimeM.surface.h < 64;
 
 	for (uint i = 0; i < _mystery.numSites(); i++) {
 		// `_DrawBigMapButtons` gates markers on the on-map flag alone, never the
@@ -3694,12 +3807,9 @@ void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
 			continue;
 
 		const bool floppy  = _mystery.isLoaded() && isFloppy();
-		const uint16 mx    = floppy ? READ_LE_UINT16(entry + 0x6)
-									: READ_LE_UINT16(entry + 0x4);
-		const uint16 my    = floppy ? READ_LE_UINT16(entry + 0x8)
-									: READ_LE_UINT16(entry + 0x6);
-		const uint16 crime = floppy ? (uint16)entry[0xa]
-									: READ_LE_UINT16(entry + 0xc);
+		BigMapEntryInfo info;
+		if (!readBigMapEntryInfo(entry, floppy, mac, info))
+			continue;
 		const bool isDone = (i < Mystery::kVisitedSiteCap)
 							 && _mystery._visitedSite[i];
 
@@ -3710,16 +3820,19 @@ void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
 		} else if (isDone && haveNormal) {
 			m = &normal;
 			useVisitedColors = true;
-		} else if (crime != 0 && haveCrime) {
+		} else if (info.crime != 0 && haveCrime) {
 			m = &crimeM;
 		} else if (haveNormal) {
 			m = &normal;
 		}
 
 		if (m) {
-			blitBigMapMarker(scratch, *m, (int)mx, (int)my,
+			blitBigMapMarker(scratch, *m, (int)info.overviewX,
+							  (int)info.overviewY,
 							  useVisitedColors);
 		} else {
+			const int mx = (int)info.overviewX;
+			const int my = (int)info.overviewY;
 			const Common::Rect mark(mx - 3, my - 3, mx + 4, my + 4);
 			scratch.fillRect(mark, 0x0F);
 		}
@@ -3732,11 +3845,12 @@ void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
 													   elapsedMs, isLondon());
 
 		blitAnimFrameAnchored(scratch.surfacePtr(), mapAnim[frameIdx],
-							  0xfd, 0x50);
+							  mac ? scaleX(0xfd) : 0xfd,
+							  mac ? scaleY(0x50) : 0x50);
 	}
 
 	g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
-							   0, 0, kScreenWidth, kScreenHeight);
+							   0, 0, sw, sh);
 	g_system->updateScreen();
 }
 
@@ -3745,12 +3859,15 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
 								 uint16 mapW, uint16 mapH,
 								 uint32 elapsedMs) {
 
-	const int kMapWinW = 0xe9;
-	const int kMapWinH = 0xab;
-	const int kMapWinX = 2;
-	const int kMapWinY = 2;
+	const bool mac = isMacintosh();
+	const int sw = screenWidth();
+	const int sh = screenHeight();
+	const int kMapWinW = mac ? scaleX(0xe9) : 0xe9;
+	const int kMapWinH = mac ? scaleY(0xab) : 0xab;
+	const int kMapWinX = mac ? scaleX(2) : 2;
+	const int kMapWinY = mac ? scaleY(2) : 2;
 
-	Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+	Graphics::ManagedSurface scratch(sw, sh,
 		Graphics::PixelFormat::createFormatCLUT8());
 	scratch.clear();
 
@@ -3772,24 +3889,14 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
 		const byte *entry = _mystery.mapEntry(i);
 		if (!entry)
 			continue;
-		uint16 mx;
-		uint16 my;
+		BigMapEntryInfo info;
+		if (!readBigMapEntryInfo(entry, floppyMap, mac, info))
+			continue;
 		Picture button;
-		if (floppyMap) {
-			mx = READ_LE_UINT16(entry + 0x0);
-			my = READ_LE_UINT16(entry + 0x2);
-			const uint16 buttonId = (uint16)entry[0x4];
-			if (!_buttonArchive.loadEntry(buttonId, button))
-				continue;
-		} else {
-			const uint16 buttonId = READ_LE_UINT16(entry + 0x0);
-			mx                    = READ_LE_UINT16(entry + 0x8);
-			my                    = READ_LE_UINT16(entry + 0xa);
-			if (!_buttonArchive.loadEntry(buttonId, button))
-				continue;
-		}
-		const int sx = (int)mx - scrollX + kMapWinX;
-		const int sy = (int)my - scrollY + kMapWinY;
+		if (!_buttonArchive.loadEntry(info.buttonId, button))
+			continue;
+		const int sx = (int)info.detailX - scrollX + kMapWinX;
+		const int sy = (int)info.detailY - scrollY + kMapWinY;
 		// Crop the button blit against the viewport.
 		const int x0 = MAX<int>(sx, kMapWinX);
 		const int y0 = MAX<int>(sy, kMapWinY);
@@ -3810,11 +3917,13 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
 		const uint frameIdx = bigMapDetailPartnerFrameAtTick(
 				(uint)detailAnim.size(), elapsedMs);
 		blitAnimFrameAnchored(scratch.surfacePtr(),
-							  detailAnim[frameIdx], 0x101, 0x50);
+							  detailAnim[frameIdx],
+							  mac ? scaleX(0x101) : 0x101,
+							  mac ? scaleY(0x50) : 0x50);
 	}
 
 	g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
-							   0, 0, kScreenWidth, kScreenHeight);
+							   0, 0, sw, sh);
 	g_system->updateScreen();
 }
 


Commit: de9f508c07ac6cfd2d9453271adcf85683964821
    https://github.com/scummvm/scummvm/commit/de9f508c07ac6cfd2d9453271adcf85683964821
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-23T15:47:52+02:00

Commit Message:
EEM: practice mystery starts to work in EEM1 Mac

Changed paths:
    engines/eem/clues.cpp
    engines/eem/graphics.cpp
    engines/eem/mystery.cpp
    engines/eem/site.cpp
    engines/eem/ui.cpp


diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index b977f53f211..4052bf8d31c 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -645,6 +645,11 @@ void EEMEngine::doInitClues() {
 	if (!floppy) {
 		const uint16 startSite = isMacintosh() ? (uint16)ib[1]
 											   : READ_LE_UINT16(ib + 2);
+		if (isMacintosh()) {
+			const uint sites = _mystery.numSites();
+			for (uint s = 0; s < sites && s < Mystery::kVisitedSiteCap; s++)
+				_mystery._onSites[s] = 1;
+		}
 		if (startSite < Mystery::kVisitedSiteCap)
 			_mystery._onSites[startSite] = 1;
 		_mystery._siteNumber = startSite;
@@ -1471,7 +1476,8 @@ void EEMEngine::displayFloppyBriefing(const byte *initBlock) {
 }
 
 void EEMEngine::displayFloppyHotspotDialog(uint siteNum, uint hotIdx) {
-	if (!_mystery.isLoaded() || !isFloppy())
+	const bool mac = isMacintosh();
+	if (!_mystery.isLoaded() || (!isFloppy() && !mac))
 		return;
 	const byte *site = _mystery.siteData(siteNum);
 	if (!site)
@@ -1483,18 +1489,20 @@ void EEMEngine::displayFloppyHotspotDialog(uint siteNum, uint hotIdx) {
 	uint32 off = dlgListOff;
 	for (uint h = 0; h < hotIdx; h++) {
 		const byte *rec = bufBase + off;
-		off += 11 + rec[10];
+		off += (mac ? 14 : 11) + rec[mac ? 13 : 10];
 		const uint contCount = bufBase[off] & 0x7F;
 		off += 1;
 		for (uint c = 0; c < contCount; c++) {
 			const byte *cr = bufBase + off;
-			off += 11 + cr[10];
+			off += (mac ? 14 : 11) + cr[mac ? 13 : 10];
 		}
 	}
 	if (off >= _mystery.dataSize())
 		return;
 
-	Graphics::ManagedSurface siteBG(kScreenWidth, kScreenHeight,
+	const int sw = screenWidth();
+	const int sh = screenHeight();
+	Graphics::ManagedSurface siteBG(sw, sh,
 		Graphics::PixelFormat::createFormatCLUT8());
 	{
 		Graphics::Surface *screen = g_system->lockScreen();
@@ -1504,7 +1512,7 @@ void EEMEngine::displayFloppyHotspotDialog(uint siteNum, uint hotIdx) {
 		}
 	}
 	const byte *mainRec = bufBase + off;
-	const uint mainLen = 11u + (uint)mainRec[10];
+	const uint mainLen = (mac ? 14u : 11u) + (uint)mainRec[mac ? 13 : 10];
 	uint contCount = 0;
 	uint contFlagsByte = 0;
 	if (off + mainLen < _mystery.dataSize()) {
@@ -1524,7 +1532,7 @@ void EEMEngine::displayFloppyHotspotDialog(uint siteNum, uint hotIdx) {
 		return;
 
 	g_system->copyRectToScreen(siteBG.getPixels(), siteBG.pitch,
-							   0, 0, kScreenWidth, kScreenHeight);
+							   0, 0, sw, sh);
 	g_system->updateScreen();
 	displayFloppyDialogRecords(bufBase + contOff, contCount, 0);
 }
diff --git a/engines/eem/graphics.cpp b/engines/eem/graphics.cpp
index 744121049be..6ded96af338 100644
--- a/engines/eem/graphics.cpp
+++ b/engines/eem/graphics.cpp
@@ -122,6 +122,7 @@ bool EEMEngine::floppyHotspotSearched(uint siteIdx, uint hotspotIdx) const {
 	const byte *site = _mystery.siteData(siteIdx);
 	if (!site)
 		return false;
+	const bool mac = isMacintosh();
 	const uint16 dlgListOff = READ_LE_UINT16(site + 6);
 	const byte *bufBase = _mystery.blobAt(0);
 	const uint32 dsz = _mystery.dataSize();
@@ -129,34 +130,44 @@ bool EEMEngine::floppyHotspotSearched(uint siteIdx, uint hotspotIdx) const {
 		return false;
 	uint32 off = dlgListOff;
 	for (uint h = 0; h < hotspotIdx; h++) {
-		if (off + 10 >= dsz)
+		const uint textCountOff = mac ? off + 13 : off + 10;
+		if (textCountOff >= dsz)
 			return false;
-		const uint32 mainLen = 11u + (uint)bufBase[off + 10];
+		const uint32 mainLen = (mac ? 14u : 11u) +
+			(uint)bufBase[textCountOff];
 		off += mainLen;
 		if (off >= dsz)
 			return false;
 		const uint contCount = (uint)(bufBase[off] & 0x7F);
 		off += 1;
 		for (uint c = 0; c < contCount; c++) {
-			if (off + 10 >= dsz)
+			const uint contTextCountOff = mac ? off + 13 : off + 10;
+			if (contTextCountOff >= dsz)
 				return false;
-			off += 11u + (uint)bufBase[off + 10];
+			off += (mac ? 14u : 11u) +
+				(uint)bufBase[contTextCountOff];
 			if (off >= dsz)
 				return false;
 		}
 	}
-	if (off + 10 >= dsz)
+	const uint textCountOff = mac ? off + 13 : off + 10;
+	if (textCountOff >= dsz)
 		return false;
-	const uint32 mainLen = 11u + (uint)bufBase[off + 10];
+	const uint32 mainLen = (mac ? 14u : 11u) + (uint)bufBase[textCountOff];
 	const uint32 contFlagsOff = off + mainLen;
 	if (contFlagsOff >= dsz)
 		return false;
 	uint32 searchedRecOff = off;
 	if ((bufBase[contFlagsOff] & 0x7F) != 0)
 		searchedRecOff = contFlagsOff + 1;
-	if (searchedRecOff + 11 >= dsz || bufBase[searchedRecOff + 10] == 0)
+	const uint searchedTextCountOff = mac ? searchedRecOff + 13
+										  : searchedRecOff + 10;
+	const uint searchedTextIdxOff = mac ? searchedRecOff + 14
+										: searchedRecOff + 11;
+	if (searchedTextIdxOff >= dsz || searchedTextCountOff >= dsz ||
+		bufBase[searchedTextCountOff] == 0)
 		return false;
-	const uint8 textIdx = bufBase[searchedRecOff + 11] & 0x7F;
+	const uint8 textIdx = bufBase[searchedTextIdxOff] & 0x7F;
 	return textIdx < EEM::Mystery::kCluesFoundCap &&
 		   _mystery._cluesFound[textIdx] != 0;
 }
diff --git a/engines/eem/mystery.cpp b/engines/eem/mystery.cpp
index 65e2a87c3fc..b9937e4cee4 100644
--- a/engines/eem/mystery.cpp
+++ b/engines/eem/mystery.cpp
@@ -87,6 +87,58 @@ static uint16 readBE16(const Common::Array<byte> &data, uint offset) {
 	return READ_BE_UINT16(data.data() + offset);
 }
 
+static void normalizeMacSiteData(Common::Array<byte> &data,
+								 const uint16 *section) {
+	if (section[2] >= data.size() || section[3] >= data.size())
+		return;
+
+	const uint numSites = MIN<uint>((uint)data[section[2]],
+									Mystery::kVisitedSiteCap);
+	Common::Array<uint16> siteOffsets;
+	siteOffsets.resize(numSites);
+	for (uint i = 0; i < numSites; i++)
+		siteOffsets[i] = readBE16(data, section[3] + i * 2);
+
+	// The site index itself is a table of big-endian offsets.
+	swapU16Range(data, section[3], section[3] + numSites * 2);
+
+	for (uint i = 0; i < numSites; i++) {
+		const uint16 siteOff = siteOffsets[i];
+		if (siteOff + 12 > data.size())
+			continue;
+
+		const uint16 dropsOff = readBE16(data, siteOff + 0);
+		const uint16 hotspotOff = readBE16(data, siteOff + 4);
+		const uint16 speakerOff = readBE16(data, siteOff + 8);
+		const uint16 nextSiteOff = readBE16(data, siteOff + 10);
+
+		// Compact site records are six offset words.
+		swapU16Range(data, siteOff, siteOff + 12);
+
+		// Drop blocks keep byte[0]=sitepic and byte[1]=count, then Mac uses
+		// count x {u16 pic, u16 x, u16 y}.
+		if (dropsOff + 2 <= data.size()) {
+			const uint count = data[dropsOff + 1];
+			swapU16Range(data, dropsOff + 2,
+						 dropsOff + 2 + count * 6);
+		}
+
+		// Hotspot blocks keep byte[0]=count, then count x {u16 x1, y1, x2, y2}.
+		if (hotspotOff + 1 <= data.size()) {
+			const uint count = data[hotspotOff];
+			swapU16Range(data, hotspotOff + 1,
+						 hotspotOff + 1 + count * 8);
+		}
+
+		// Speaker/pose blocks are word triples. They run until the next site
+		// data record (or the next major section for the final site).
+		uint speakerEnd = nextSiteOff;
+		if (speakerEnd <= speakerOff || speakerEnd > data.size())
+			speakerEnd = section[4];
+		swapU16Range(data, speakerOff, speakerEnd);
+	}
+}
+
 static void normalizeMacMystery(Common::Array<byte> &data) {
 	if (data.size() <= 20)
 		return;
@@ -108,7 +160,7 @@ static void normalizeMacMystery(Common::Array<byte> &data) {
 	// section[7] are raw strings and must stay byte-exact.
 	swapU16Range(data, section[1], section[2]); // clue/site chain table
 	swapU16Range(data, section[2] + 1, section[3]); // counted map entries
-	swapU16Range(data, section[3], section[4]); // site index + site data
+	normalizeMacSiteData(data, section);
 	swapU16Range(data, section[4], section[7]); // notebook index
 	swapU16Range(data, section[5] + 1, section[6]); // compact gallery metadata
 	swapU16Range(data, section[8], MIN<uint>(section[8] + 12, section[9]));
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index b06d208d0fe..9c58ed8a58d 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -849,9 +849,10 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
 		}
 	}
 
+	const bool compactSite = _vm->isFloppy() || _vm->isMacintosh();
 	uint16 sitepic = 0;
 	if (sd) {
-		if (_vm->isFloppy()) {
+		if (compactSite) {
 			const uint16 dropsOff = READ_LE_UINT16(sd);
 			const byte *drops = _mystery->blobAt(dropsOff);
 			if (drops)
@@ -867,7 +868,7 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
 	renderBackground(siteNum);
 
 	if (playArrival) {
-		if (_vm->isFloppy())
+		if (compactSite)
 			renderFloppyDrops(siteNum);
 		else
 			renderStaticDrops(siteNum);
@@ -883,7 +884,7 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
 		renderBackground(siteNum);
 	}
 
-	if (_vm->isFloppy())
+	if (compactSite)
 		renderFloppyDrops(siteNum);
 	else
 		renderStaticDrops(siteNum);
@@ -906,7 +907,7 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
 	//       _VisitedSite[_SiteNumber] = 1;
 	//   }
 	// SiteIndex[+2..+3] = byte offset of entry-clue ClueBlock.
-	if (firstVisit) {
+	if (firstVisit && !_vm->isMacintosh()) {
 		const byte *idx = _mystery->siteIndexEntry(siteNum);
 		if (idx) {
 			const uint16 clueOff = READ_LE_UINT16(idx + 2);
@@ -920,7 +921,7 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
 			_mystery->_visitedSite[siteNum] = 1;
 		// Dialog overlay leaves portrait/balloon residue; refresh & re-snapshot.
 		renderBackground(siteNum);
-		if (_vm->isFloppy())
+		if (compactSite)
 			renderFloppyDrops(siteNum);
 		else
 			renderStaticDrops(siteNum);
@@ -1284,6 +1285,7 @@ void SiteScreen::renderFloppyDrops(uint siteNum) {
 	const byte *site = _mystery->siteData(siteNum);
 	if (!site)
 		return;
+	const bool mac = _vm && _vm->isMacintosh();
 	const uint16 dropsOff = READ_LE_UINT16(site);
 	const byte *drops = _mystery->blobAt(dropsOff);
 	if (!drops)
@@ -1295,10 +1297,11 @@ void SiteScreen::renderFloppyDrops(uint siteNum) {
 		return;
 
 	for (uint i = 0; i < count; i++) {
-		const byte *e = drops + 2 + i * 5;
+		const byte *e = drops + 2 + i * (mac ? 6 : 5);
 		const uint16 picID = READ_LE_UINT16(e + 0);
 		const int16  x     = (int16)READ_LE_UINT16(e + 2);
-		const int16  y     = (int16)e[4];
+		const int16  y     = mac ? (int16)READ_LE_UINT16(e + 4)
+								  : (int16)e[4];
 		if (picID == 0)
 			continue;
 		Picture pic;
@@ -1313,6 +1316,9 @@ void SiteScreen::renderAnimatedDrops(uint siteNum, uint32 tickMs) {
 	if (!_mystery)
 		return;
 
+	if (_vm && _vm->isMacintosh())
+		return;
+
 	if (_vm && _vm->isFloppy()) {
 		const byte *siteAnim = _mystery->floppySiteAnimData(siteNum);
 		if (!siteAnim)
@@ -1381,6 +1387,9 @@ void SiteScreen::scanColorCycles(uint siteNum) {
 	if (!_mystery)
 		return;
 
+	if (_vm && _vm->isMacintosh())
+		return;
+
 	if (_vm && _vm->isFloppy()) {
 		const byte *siteAnim = _mystery->floppySiteAnimData(siteNum);
 		if (!siteAnim)
@@ -1422,7 +1431,9 @@ void SiteScreen::applyColorCycles() {
 }
 
 void SiteScreen::captureBgSnapshot() {
-	_bgSnapshot.create(kScreenWidth, kScreenHeight, Graphics::PixelFormat::createFormatCLUT8());
+	const int sw = _vm ? _vm->screenWidth() : kScreenWidth;
+	const int sh = _vm ? _vm->screenHeight() : kScreenHeight;
+	_bgSnapshot.create(sw, sh, Graphics::PixelFormat::createFormatCLUT8());
 	Graphics::Surface *screen = g_system->lockScreen();
 	if (!screen) {
 		_snapshotSite = -1;
@@ -1433,14 +1444,18 @@ void SiteScreen::captureBgSnapshot() {
 }
 
 void SiteScreen::restoreBgSnapshot() {
-	if (_bgSnapshot.w != kScreenWidth || _bgSnapshot.h != kScreenHeight)
+	const int sw = _vm ? _vm->screenWidth() : kScreenWidth;
+	const int sh = _vm ? _vm->screenHeight() : kScreenHeight;
+	if (_bgSnapshot.w != sw || _bgSnapshot.h != sh)
 		return;
 	g_system->copyRectToScreen(_bgSnapshot.getPixels(), _bgSnapshot.pitch,
-							   0, 0, kScreenWidth, kScreenHeight);
+							   0, 0, sw, sh);
 }
 
 void SiteScreen::syncCompositedScreen() {
-	Graphics::ManagedSurface snapshot(kScreenWidth, kScreenHeight,
+	const int sw = _vm ? _vm->screenWidth() : kScreenWidth;
+	const int sh = _vm ? _vm->screenHeight() : kScreenHeight;
+	Graphics::ManagedSurface snapshot(sw, sh,
 		Graphics::PixelFormat::createFormatCLUT8());
 
 	Graphics::Surface *screen = g_system->lockScreen();
@@ -1450,7 +1465,7 @@ void SiteScreen::syncCompositedScreen() {
 	g_system->unlockScreen();
 
 	g_system->copyRectToScreen(snapshot.getPixels(), snapshot.pitch,
-							   0, 0, kScreenWidth, kScreenHeight);
+							   0, 0, sw, sh);
 }
 
 bool SiteScreen::partnerIdleAnimParams(uint siteNum, uint16 &animId,
@@ -1459,7 +1474,16 @@ bool SiteScreen::partnerIdleAnimParams(uint siteNum, uint16 &animId,
 	if (!site)
 		return false;
 	const uint8 partner = _vm->getPartnerIndex();
-	if (_vm->isFloppy()) {
+	if (_vm->isMacintosh()) {
+		const uint16 spkOff = READ_LE_UINT16(site + 8);
+		const byte *spk = _mystery->blobAt(spkOff);
+		if (!spk)
+			return false;
+		const uint poseOff = partner == 0 ? 0 : 6;
+		animId = READ_LE_UINT16(spk + poseOff + 0);
+		x      = (int)READ_LE_UINT16(spk + poseOff + 2);
+		y      = (int)READ_LE_UINT16(spk + poseOff + 4);
+	} else if (_vm->isFloppy()) {
 		const uint16 spkOff = READ_LE_UINT16(site + 8);
 		const byte *spk = _mystery->blobAt(spkOff);
 		if (!spk)
@@ -1521,7 +1545,7 @@ void SiteScreen::renderPartner(uint siteNum, uint32 tickMs) {
 }
 
 bool SiteScreen::renderFloppyHotspotPartnerPose(uint siteNum) {
-	if (!_vm || !_vm->isFloppy() || !_mystery)
+	if (!_vm || (!_vm->isFloppy() && !_vm->isMacintosh()) || !_mystery)
 		return false;
 
 	const byte *site = _mystery->siteData(siteNum);
@@ -1529,9 +1553,11 @@ bool SiteScreen::renderFloppyHotspotPartnerPose(uint siteNum) {
 		return false;
 
 	const uint16 spkOff = READ_LE_UINT16(site + 8);
+	const bool mac = _vm->isMacintosh();
 	const uint poseOff = (_vm->getPartnerIndex() == kPartnerJake)
-		? 0x28 : 0x2d;
-	if ((uint32)spkOff + poseOff + 5 > _mystery->dataSize())
+		? (mac ? 0x30 : 0x28) : (mac ? 0x36 : 0x2d);
+	const uint poseSize = mac ? 6 : 5;
+	if ((uint32)spkOff + poseOff + poseSize > _mystery->dataSize())
 		return false;
 
 	const byte *pose = _mystery->blobAt((uint32)spkOff + poseOff);
@@ -1540,7 +1566,7 @@ bool SiteScreen::renderFloppyHotspotPartnerPose(uint siteNum) {
 
 	const uint16 animId = READ_LE_UINT16(pose + 0);
 	const int x = (int)READ_LE_UINT16(pose + 2);
-	const int y = (int)pose[4];
+	const int y = mac ? (int)READ_LE_UINT16(pose + 4) : (int)pose[4];
 
 	Animation anim;
 	if (!_vm->getAni().loadAnimation(animId, anim) || anim.empty())
@@ -1567,7 +1593,7 @@ void SiteScreen::renderBackground(uint siteNum) {
 	const byte *site = _mystery->siteData(siteNum);
 	uint16 sitepic = 0;
 	if (site) {
-		if (_vm->isFloppy()) {
+		if (_vm->isFloppy() || _vm->isMacintosh()) {
 			const uint16 dropsOff = READ_LE_UINT16(site);
 			const byte *drops = _mystery->blobAt(dropsOff);
 			if (drops)
@@ -1634,8 +1660,9 @@ 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 floppy = _vm && _vm->isFloppy();
-	const uint stride = floppy ? 8 : 14;
+	const uint stride = compact ? 8 : 14;
 	// The floppy SITEPALS has at least one searchable site where 0xFF is
 	// yellow. The CD corrected that palette data; for floppy, draw with an
 	// existing white entry from the current palette instead of changing it.
@@ -1646,11 +1673,15 @@ void SiteScreen::renderHotspots(uint siteNum) {
 		const int16 y1 = (int16)READ_LE_UINT16(r + 2);
 		const int16 x2 = (int16)READ_LE_UINT16(r + 4);
 		const int16 y2 = (int16)READ_LE_UINT16(r + 6);
-		const Common::Rect rect(MAX<int>(0, x1), MAX<int>(0, y1),
-								MIN<int>(screen->w, x2),
-								MIN<int>(screen->h, y2));
+		const int left = MAX<int>(0, x1);
+		const int top = MAX<int>(0, y1);
+		const int right = MIN<int>(screen->w, x2);
+		const int bottom = MIN<int>(screen->h, y2);
+		if (right <= left || bottom <= top)
+			continue;
+		const Common::Rect rect(left, top, right, bottom);
 		bool seen = false;
-		if (floppy) {
+		if (compact) {
 			seen = _vm->floppyHotspotSearched(siteNum, i);
 		} else {
 			const uint seenKey = READ_LE_UINT16(r + 0xa);
@@ -1697,7 +1728,7 @@ int SiteScreen::hotspotAtPoint(uint siteNum, int x, int y) const {
 	if (!spots)
 		return -1;
 
-	const uint stride = _vm && _vm->isFloppy() ? 8 : 14;
+	const uint stride = _vm && (_vm->isFloppy() || _vm->isMacintosh()) ? 8 : 14;
 	for (uint i = 0; i < count; i++) {
 		const byte *r = spots + i * stride;
 		const int16 x1 = (int16)READ_LE_UINT16(r + 0);
@@ -1713,7 +1744,7 @@ 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()))
+	if (idx < 0 || (_vm && (_vm->isFloppy() || _vm->isMacintosh())))
 		return 0;
 	const byte *spots = _mystery->hotspots(siteNum);
 	if (!spots || (uint)idx >= _mystery->hotspotCount(siteNum))
@@ -1777,7 +1808,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()) {
+	if (_vm->isFloppy() || _vm->isMacintosh()) {
 		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 af55ce31d62..0c437773cb8 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -130,7 +130,7 @@ bool readBigMapEntryInfo(const byte *entry, bool floppy, bool macintosh,
 	if (macintosh) {
 		out.detailX   = READ_LE_UINT16(entry + 0x0);
 		out.detailY   = READ_LE_UINT16(entry + 0x2);
-		out.buttonId  = entry[0x4];
+		out.buttonId  = entry[0x5];
 		out.overviewX = READ_LE_UINT16(entry + 0x6);
 		out.overviewY = READ_LE_UINT16(entry + 0x8);
 		out.crime     = READ_LE_UINT16(entry + 0xa);
@@ -178,7 +178,11 @@ bool loadMacBigMapPixels(Common::Array<byte> &mapPixels,
 	mapPixels.resize((uint32)mapW * mapH);
 	for (uint y = 0; y < mapH; y++) {
 		const byte *src = (const byte *)mapPic.surface.getBasePtr(0, y);
-		memcpy(mapPixels.data() + y * mapW, src, mapW);
+		byte *dst = mapPixels.data() + y * mapW;
+		// BIGMAP.DBD uses 0xff for dark coast/detail pixels, but the Mac
+		// detail-map palette keeps 0xff white for the UI frame/buttons.
+		for (uint x = 0; x < mapW; x++)
+			dst[x] = src[x] == 0xff ? 0x00 : src[x];
 	}
 	return true;
 }
@@ -3223,6 +3227,12 @@ void EEMEngine::doBigMap() {
 	if (!_mystery.isLoaded())
 		return;
 
+	if (isMacintosh()) {
+		for (uint i = 0; i < _mystery.numSites() &&
+						 i < Mystery::kVisitedSiteCap; i++)
+			_mystery._onSites[i] = 1;
+	}
+
 	if (isLondon()) {
 		_mystery._pendingSiteJump = 0;
 		_mystery._siteReturnDepth = 0;


Commit: 5b53d52b056ad25361c9586911065d4de62694d5
    https://github.com/scummvm/scummvm/commit/5b53d52b056ad25361c9586911065d4de62694d5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-23T15:47:52+02:00

Commit Message:
EEM: correctly parse hotspots in EEM1 Mac

Changed paths:
    engines/eem/music.cpp
    engines/eem/site.cpp


diff --git a/engines/eem/music.cpp b/engines/eem/music.cpp
index fe5a5e5e0b0..988dd7ad949 100644
--- a/engines/eem/music.cpp
+++ b/engines/eem/music.cpp
@@ -36,6 +36,7 @@
 namespace EEM {
 
 const int kMidiDriverFlags = MDT_MIDI | MDT_ADLIB | MDT_PREFER_MT32;
+const int kMacMidiDriverFlags = MDT_MIDI | MDT_PREFER_GM;
 const uint16 kInvalidMacMidiResource = 0xffff;
 
 Common::String musicNameFromPath(const Common::Path &path) {
@@ -87,14 +88,16 @@ MusicPlayer::MusicPlayer(bool isFloppy, bool isMacintosh) :
 	// ADLIB.ADV / SBFM.ADV / MT32MPU.ADV. We honour the launcher's
 	// "Music driver" setting and prefer MT-32 when unset.
 	const MidiDriver::DeviceHandle dev =
-		MidiDriver::detectDevice(kMidiDriverFlags);
+		MidiDriver::detectDevice(_isMacintosh ? kMacMidiDriverFlags
+											  : kMidiDriverFlags);
 	MusicType musicType = MidiDriver::getMusicType(dev);
-	if (musicType == MT_GM && ConfMan.getBool("native_mt32"))
+	if (!_isMacintosh && musicType == MT_GM &&
+		ConfMan.getBool("native_mt32"))
 		musicType = MT_MT32;
 
 	if (_isMacintosh) {
 		_milesAudioMode = false;
-		createDriver(kMidiDriverFlags);
+		createDriver(kMacMidiDriverFlags);
 	} else {
 		switch (musicType) {
 		case MT_ADLIB:
@@ -126,7 +129,9 @@ MusicPlayer::MusicPlayer(bool isFloppy, bool isMacintosh) :
 			_driver = nullptr;
 		} else {
 			// Miles AdLib handles its own reset.
-			if (_isMacintosh || musicType != MT_ADLIB) {
+			if (_isMacintosh) {
+				_driver->sendGMReset();
+			} else if (musicType != MT_ADLIB) {
 				if (musicType == MT_MT32 || _nativeMT32)
 					_driver->sendMT32Reset();
 				else
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 9c58ed8a58d..b0bac98637d 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -119,6 +119,32 @@ void blitAnimFrameAnchored(Graphics::Surface *screen, const Picture &p,
 					anchorY - (int)(int16)p.rowoff);
 }
 
+bool readHotspotRect(const byte *r, bool mac, Common::Rect &rect) {
+	if (mac) {
+		const int16 top = (int16)READ_LE_UINT16(r + 0);
+		const int16 left = (int16)READ_LE_UINT16(r + 2);
+		const int16 bottom = (int16)READ_LE_UINT16(r + 4);
+		const int16 right = (int16)READ_LE_UINT16(r + 6);
+		if (right <= left || bottom <= top)
+			return false;
+		rect = Common::Rect(left, top, right, bottom);
+		return true;
+	}
+
+	const int16 x1 = (int16)READ_LE_UINT16(r + 0);
+	const int16 y1 = (int16)READ_LE_UINT16(r + 2);
+	const int16 x2 = (int16)READ_LE_UINT16(r + 4);
+	const int16 y2 = (int16)READ_LE_UINT16(r + 6);
+	if (x2 <= x1 || y2 <= y1)
+		return false;
+	rect = Common::Rect(x1, y1, x2, y2);
+	return true;
+}
+
+Common::Rect siteControlRect(const EEMEngine *vm, const Common::Rect &rect) {
+	return (vm && vm->isMacintosh()) ? vm->scaleRect(rect) : rect;
+}
+
 // `_ColorCycle @ 172b:2015` — rotate `_fpal[start..end]` by one slot:
 // save [start], shift [start..end-1] = [start+1..end], restore saved at
 // [end], then re-upload via `_Set_Palette`. 
@@ -980,6 +1006,12 @@ void SiteScreen::run() {
 	if (_mystery->_siteReturnDepth > Mystery::kVisitedSiteCap)
 		_mystery->_siteReturnDepth = 0;
 	_snapshotSite = -1;
+	const Common::Rect pdaSiteRect =
+		siteControlRect(_vm, kPdaSiteRect);
+	const Common::Rect pdaPartnerFootMapRect =
+		siteControlRect(_vm, kPdaPartnerFootMapRect);
+	const Common::Rect pdaPartnerHeadHintRect =
+		siteControlRect(_vm, kPdaPartnerHeadHintRect);
 	SiteBackendActionObserverRegistration backendActionRegistration(this);
 	enter(cur);
 	Common::Point mouse = g_system->getEventManager()->getMousePos();
@@ -1000,14 +1032,15 @@ void SiteScreen::run() {
 				break;
 
 			case Common::EVENT_LBUTTONDOWN: {
-				if (kPdaSiteRect.contains(event.mouse.x, event.mouse.y)) {
+				if (pdaSiteRect.contains(event.mouse.x, event.mouse.y)) {
 					notePartnerActivity();
 					_vm->setHotspotMouseCursor(false);
 					_vm->setNextScreen(kScreenNotebook);
 					_vm->stopMusic();
 					return;
 				}
-				if (kPdaPartnerFootMapRect.contains(event.mouse.x, event.mouse.y)) {
+				if (pdaPartnerFootMapRect.contains(event.mouse.x,
+												   event.mouse.y)) {
 					notePartnerActivity();
 					_vm->setHotspotMouseCursor(false);
 					if (_vm->isLondon() && _mystery->_siteReturnDepth != 0) {
@@ -1035,7 +1068,8 @@ void SiteScreen::run() {
 					_vm->stopMusic();
 					return;
 				}
-				if (kPdaPartnerHeadHintRect.contains(event.mouse.x, event.mouse.y)) {
+				if (pdaPartnerHeadHintRect.contains(event.mouse.x,
+													event.mouse.y)) {
 					_vm->setHotspotMouseCursor(false);
 					_vm->doHelp();
 					notePartnerActivity();
@@ -1136,12 +1170,15 @@ bool SiteScreen::enterSiteAnim() {
 	const uint8 partner = _vm->getPartnerIndex();
 	const uint kSkateAni = (partner == 0) ? 6  : 0xe;
 	const uint kKDAni    = (partner == 0) ? 7  : 0xf;
-	const int  kKDY      = (partner == 0) ? 0x8b : 0x8e;
+	const int baseKDY = (partner == 0) ? 0x8b : 0x8e;
+	const int kKDY = _vm->isMacintosh() ? _vm->scaleY(baseKDY) : baseKDY;
+	const int sw = _vm->screenWidth();
+	const int sh = _vm->screenHeight();
 
 	Graphics::Surface *screen = g_system->lockScreen();
 	if (!screen)
 		return false;
-	Graphics::ManagedSurface bg(kScreenWidth, kScreenHeight,
+	Graphics::ManagedSurface bg(sw, sh,
 		Graphics::PixelFormat::createFormatCLUT8());
 	bg.simpleBlitFrom(*screen);
 	g_system->unlockScreen();
@@ -1155,13 +1192,13 @@ bool SiteScreen::enterSiteAnim() {
 		for (uint frameIdx = 0;
 			 frameIdx < anim.size() && !_vm->shouldQuit();
 			 frameIdx++) {
-			Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+			Graphics::ManagedSurface scratch(sw, sh,
 				Graphics::PixelFormat::createFormatCLUT8());
 			scratch.simpleBlitFrom(bg);
 			blitAnimFrameAnchored(scratch.surfacePtr(), anim[frameIdx],
 								  0, anchorY);
 			g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
-									   0, 0, kScreenWidth, kScreenHeight);
+									   0, 0, sw, sh);
 			g_system->updateScreen();
 
 			Common::Event ev;
@@ -1180,21 +1217,21 @@ bool SiteScreen::enterSiteAnim() {
 	if (_vm->getAni().loadAnimation(kSkateAni, skate) && !skate.empty()) {
 		const int spriteH = skate[0].surface.h;
 		const int spriteW = skate[0].surface.w;
-		int x = (kScreenWidth - spriteW) & ~3;            // 4-px aligned (mode-X)
-		const int y = 199 - spriteH;
+		int x = (sw - spriteW) & ~3;            // 4-px aligned (mode-X)
+		const int y = (_vm->isMacintosh() ? sh : 199) - spriteH;
 		const byte transp = (byte)(skate[0].flags >> 8);
 		uint frameIdx = 0;
 		int distSinceTick = 0;
-		const int kStep = 4;            // _MoveSkateBoardPixels analogue
-		const int kFrameTicks = 0xc;    // original switches frame at 12 px
+		const int kStep = _vm->isMacintosh() ? _vm->scaleX(4) : 4;
+		const int kFrameTicks = _vm->isMacintosh() ? _vm->scaleX(0xc) : 0xc;
 
 		while (x + spriteW > 0 && !_vm->shouldQuit()) {
-			Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+			Graphics::ManagedSurface scratch(sw, sh,
 				Graphics::PixelFormat::createFormatCLUT8());
 			scratch.simpleBlitFrom(bg);
 			blitFrame(scratch, skate[frameIdx], x, y, transp);
 			g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
-									   0, 0, kScreenWidth, kScreenHeight);
+									   0, 0, sw, sh);
 			g_system->updateScreen();
 
 			Common::Event ev;
@@ -1225,12 +1262,12 @@ bool SiteScreen::enterSiteAnim() {
 			const int destX = -(int)(int16)fr.miscflags;
 			const int destY = kKDY - (int)(int16)fr.rowoff;
 
-			Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+			Graphics::ManagedSurface scratch(sw, sh,
 				Graphics::PixelFormat::createFormatCLUT8());
 			scratch.simpleBlitFrom(bg);
 			blitFrame(scratch, fr, destX, destY, transp);
 			g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
-									   0, 0, kScreenWidth, kScreenHeight);
+									   0, 0, sw, sh);
 			g_system->updateScreen();
 
 			Common::Event ev;
@@ -1662,6 +1699,7 @@ void SiteScreen::renderHotspots(uint siteNum) {
 	// walking the dialog record list, like `_HotspotSearched_Floppy`.
 	const bool compact = _vm && (_vm->isFloppy() || _vm->isMacintosh());
 	const bool floppy = _vm && _vm->isFloppy();
+	const bool mac = _vm && _vm->isMacintosh();
 	const uint stride = compact ? 8 : 14;
 	// The floppy SITEPALS has at least one searchable site where 0xFF is
 	// yellow. The CD corrected that palette data; for floppy, draw with an
@@ -1669,17 +1707,12 @@ void SiteScreen::renderHotspots(uint siteNum) {
 	const byte searchedColor = floppy ? currentWhitePaletteIndex(0xFF) : 0xFF;
 	for (uint i = 0; i < count; i++) {
 		const byte *r = spots + i * stride;
-		const int16 x1 = (int16)READ_LE_UINT16(r + 0);
-		const int16 y1 = (int16)READ_LE_UINT16(r + 2);
-		const int16 x2 = (int16)READ_LE_UINT16(r + 4);
-		const int16 y2 = (int16)READ_LE_UINT16(r + 6);
-		const int left = MAX<int>(0, x1);
-		const int top = MAX<int>(0, y1);
-		const int right = MIN<int>(screen->w, x2);
-		const int bottom = MIN<int>(screen->h, y2);
-		if (right <= left || bottom <= top)
+		Common::Rect rect;
+		if (!readHotspotRect(r, mac, rect))
+			continue;
+		rect = rect.findIntersectingRect(Common::Rect(screen->w, screen->h));
+		if (rect.isEmpty())
 			continue;
-		const Common::Rect rect(left, top, right, bottom);
 		bool seen = false;
 		if (compact) {
 			seen = _vm->floppyHotspotSearched(siteNum, i);
@@ -1729,13 +1762,11 @@ int SiteScreen::hotspotAtPoint(uint siteNum, int x, int y) const {
 		return -1;
 
 	const uint stride = _vm && (_vm->isFloppy() || _vm->isMacintosh()) ? 8 : 14;
+	const bool mac = _vm && _vm->isMacintosh();
 	for (uint i = 0; i < count; i++) {
 		const byte *r = spots + i * stride;
-		const int16 x1 = (int16)READ_LE_UINT16(r + 0);
-		const int16 y1 = (int16)READ_LE_UINT16(r + 2);
-		const int16 x2 = (int16)READ_LE_UINT16(r + 4);
-		const int16 y2 = (int16)READ_LE_UINT16(r + 6);
-		if (x >= x1 && x < x2 && y >= y1 && y < y2)
+		Common::Rect rect;
+		if (readHotspotRect(r, mac, rect) && rect.contains(x, y))
 			return (int)i;
 	}
 	return -1;
@@ -1756,9 +1787,9 @@ void SiteScreen::updateHotspotCursor(uint siteNum, int x, int y) {
 	if (!_vm)
 		return;
 	const int idx = hotspotAtPoint(siteNum, x, y);
-	const bool siteControl = kPdaSiteRect.contains(x, y) ||
-							 kPdaPartnerFootMapRect.contains(x, y) ||
-							 kPdaPartnerHeadHintRect.contains(x, y);
+	const bool siteControl = siteControlRect(_vm, kPdaSiteRect).contains(x, y) ||
+							 siteControlRect(_vm, kPdaPartnerFootMapRect).contains(x, y) ||
+							 siteControlRect(_vm, kPdaPartnerHeadHintRect).contains(x, y);
 	if (_vm->isLondon()) {
 		// EEM2 gives some hotspots their own cursor shape; every other
 		// interactive zone (the site/foot/head controls, or a hotspot with no


Commit: 7bf463b4830c377b8f09627938da13f2a006f359
    https://github.com/scummvm/scummvm/commit/7bf463b4830c377b8f09627938da13f2a006f359
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-23T15:47:52+02:00

Commit Message:
EEM: load all the cases from EEM1 Mac

Changed paths:
    engines/eem/eem.cpp
    engines/eem/mystery.cpp
    engines/eem/mystery.h
    engines/eem/ui.cpp


diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 939158f95b1..a3a762b7c2a 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -332,12 +332,14 @@ static void addMacResourceSearchPaths() {
 		return;
 
 	const Common::FSNode childRsrcDir = gameDataDir.getChild("rsrc");
-	if (childRsrcDir.exists() && childRsrcDir.isDirectory())
+	if (childRsrcDir.exists() && childRsrcDir.isDirectory() &&
+		!SearchMan.hasArchive("eem-mac-rsrc-child"))
 		SearchMan.addDirectory("eem-mac-rsrc-child", childRsrcDir);
 
 	const Common::FSNode siblingRsrcDir =
 		gameDataDir.getParent().getChild("rsrc");
-	if (siblingRsrcDir.exists() && siblingRsrcDir.isDirectory())
+	if (siblingRsrcDir.exists() && siblingRsrcDir.isDirectory() &&
+		!SearchMan.hasArchive("eem-mac-rsrc-sibling"))
 		SearchMan.addDirectory("eem-mac-rsrc-sibling", siblingRsrcDir);
 }
 
diff --git a/engines/eem/mystery.cpp b/engines/eem/mystery.cpp
index b9937e4cee4..d8dbd96a2eb 100644
--- a/engines/eem/mystery.cpp
+++ b/engines/eem/mystery.cpp
@@ -139,6 +139,31 @@ static void normalizeMacSiteData(Common::Array<byte> &data,
 	}
 }
 
+static void normalizeMacGalleryData(Common::Array<byte> &data,
+									uint16 start, uint16 end) {
+	if (start >= data.size() || end > data.size() || start >= end)
+		return;
+
+	const byte *bufEnd = data.data() + end;
+	byte *p = data.data() + start + 1;
+	const uint count = data[start];
+	for (uint i = 0; i < count; i++) {
+		if (p + 5 > bufEnd)
+			return;
+		byte tmp = p[0];
+		p[0] = p[1];
+		p[1] = tmp; // pic id
+		tmp = p[2];
+		p[2] = p[3];
+		p[3] = tmp; // alibi text offset
+		const uint clueCount = p[4];
+		p += 5;
+		if (p + clueCount > bufEnd)
+			return;
+		p += clueCount;
+	}
+}
+
 static void normalizeMacMystery(Common::Array<byte> &data) {
 	if (data.size() <= 20)
 		return;
@@ -162,7 +187,7 @@ static void normalizeMacMystery(Common::Array<byte> &data) {
 	swapU16Range(data, section[2] + 1, section[3]); // counted map entries
 	normalizeMacSiteData(data, section);
 	swapU16Range(data, section[4], section[7]); // notebook index
-	swapU16Range(data, section[5] + 1, section[6]); // compact gallery metadata
+	normalizeMacGalleryData(data, section[5], section[6]);
 	swapU16Range(data, section[8], MIN<uint>(section[8] + 12, section[9]));
 	swapU16Range(data, section[9], data.size()); // solved clue block
 }
@@ -597,6 +622,9 @@ bool Mystery::noteHasNotebookText(uint clueId) const {
 	if (!ni || clueId >= cnt)
 		return false;
 
+	if (_isMacintosh)
+		return READ_LE_UINT16(ni + clueId * 8) != 0;
+
 	if (!_isFloppy)
 		return true;
 
@@ -637,13 +665,13 @@ const byte *Mystery::mapEntry(uint siteNum) const {
 }
 
 const byte *Mystery::floppySuspectEntry(uint suspectIdx) const {
-	// Floppy gallery section (_DrawGallery_Floppy @ 154e:00b6).
-	// byte[0]=numSuspects, then per suspect a `5 + nameLen` record:
-	//   u16 +0 picID (BUTTON.DBD entry)
-	//   u16 +2 alibi marker (0xFFFF=guilty; else hi byte indexes TEXT_BLOCK)
-	//   u8  +4 nameLen
-	//   u8  +5.. name string
-	if (!_isFloppy || !isLoaded())
+	// Floppy gallery section (_DrawGallery_Floppy @ 154e:00b6) and Mac
+	// compact gallery section:
+	//   u16 +0 picID
+	//   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())
 		return nullptr;
 	const byte *gd = _data.data() + _galleryOffset;
 	if (gd + 1 > _data.data() + _data.size())
@@ -664,7 +692,7 @@ const byte *Mystery::floppySuspectEntry(uint suspectIdx) const {
 }
 
 bool Mystery::isGuilty(uint suspectIdx) const {
-	if (_isFloppy) {
+	if (_isFloppy || _isMacintosh) {
 		const byte *e = floppySuspectEntry(suspectIdx);
 		return e && READ_LE_UINT16(e + 2) == 0xFFFF;
 	}
@@ -676,6 +704,12 @@ bool Mystery::isGuilty(uint suspectIdx) const {
 }
 
 uint16 Mystery::alibiTextOffset(uint suspectIdx) const {
+	if (_isMacintosh) {
+		const byte *e = floppySuspectEntry(suspectIdx);
+		if (!e)
+			return 0xFFFF;
+		return READ_LE_UINT16(e + 2);
+	}
 	if (_isFloppy) {
 		const byte *e = floppySuspectEntry(suspectIdx);
 		if (!e)
@@ -749,7 +783,9 @@ int Mystery::selectedPoints() const {
 		if (!_noteSelected[i])
 			continue;
 		// CD NoteIndex entry: 4 bytes = u16 textOff + u16 points (at +2).
-		const uint16 pts = READ_LE_UINT16(ni + i * 4 + 2);
+		// Mac rows are 8 bytes: notebook text, Jake text, Jenny text, points.
+		const uint16 pts = _isMacintosh ? READ_LE_UINT16(ni + i * 8 + 6)
+										: READ_LE_UINT16(ni + i * 4 + 2);
 		total += (int)(int16)pts;
 	}
 	return total;
@@ -820,8 +856,10 @@ int Mystery::foundPoints() const {
 	for (uint i = 0; i < maxIdx; i++) {
 		if (_cluesFound[i] == 0)
 			continue;
-		const uint16 pts = _isFloppy ? ni[i * 7 + 6]
-									 : READ_LE_UINT16(ni + i * 4 + 2);
+		const uint16 pts = _isFloppy
+			? ni[i * 7 + 6]
+			: (_isMacintosh ? READ_LE_UINT16(ni + i * 8 + 6)
+							: READ_LE_UINT16(ni + i * 4 + 2));
 		scores[scoreCount++] = pts;
 	}
 
diff --git a/engines/eem/mystery.h b/engines/eem/mystery.h
index ca10d929e1b..99ee5cbf7ff 100644
--- a/engines/eem/mystery.h
+++ b/engines/eem/mystery.h
@@ -77,12 +77,13 @@ public:
 	/// InitBlock (case briefing) at mystery + word[0].
 	const byte *initBlock() const;
 
-	/// GalleryData: 0x46-byte entry per suspect; first u16 = PIC picture ID.
+	/// GalleryData. DOS CD uses 0x46-byte entries; floppy/Mac use compact
+	/// variable-stride entries accessed through floppySuspectEntry().
 	const byte *galleryData() const;
 
-	/// Floppy variable-stride suspect record. Returns nullptr on CD or
-	/// out-of-range. Layout: u16 picID, u16 alibiMarker (0xFFFF=guilty),
-	/// u8 nameLen, nameLen bytes of name.
+	/// Compact variable-stride suspect record for floppy/Mac. Returns nullptr
+	/// on CD or out-of-range. Layout: u16 picID, u16 alibiMarker
+	/// (0xFFFF=guilty), u8 count, count bytes of name/clue data.
 	const byte *floppySuspectEntry(uint suspectIdx) const;
 
 	/// NoteIndex array. EEM1 CD: 4 bytes/entry (u16 textOff + u16 pts).
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 0c437773cb8..2dd491ef025 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -113,6 +113,38 @@ void blitBigMapMarker(Graphics::ManagedSurface &dstSurface, const Picture &marke
 	}
 }
 
+void blitMacBigMapPartnerFrame(Graphics::ManagedSurface &dstSurface,
+							   const Picture &frame, int anchorX,
+							   int anchorY) {
+	const byte transp = (byte)(frame.flags >> 8);
+	const int x = anchorX - (int)(int16)frame.miscflags;
+	const int y = anchorY - (int)(int16)frame.rowoff;
+	for (int row = 0; row < frame.surface.h; row++) {
+		const int dstY = y + row;
+		if (dstY < 0 || dstY >= dstSurface.h)
+			continue;
+		const byte *src = (const byte *)frame.surface.getBasePtr(0, row);
+		byte *dst = (byte *)dstSurface.getBasePtr(0, dstY);
+		for (int col = 0; col < frame.surface.w; col++) {
+			const int dstX = x + col;
+			if (dstX < 0 || dstX >= dstSurface.w)
+				continue;
+			const byte color = src[col];
+			if (color != transp) {
+				// The map partner frames are authored against the overview
+				// ColorTable: 0 is white and 0xff is black. The detail-map
+				// ColorTable swaps those endpoints.
+				if (color == 0x00)
+					dst[dstX] = 0xff;
+				else if (color == 0xff)
+					dst[dstX] = 0x00;
+				else
+					dst[dstX] = color;
+			}
+		}
+	}
+}
+
 struct BigMapEntryInfo {
 	uint16 overviewX = 0;
 	uint16 overviewY = 0;
@@ -693,8 +725,100 @@ struct ActionMenuView {
 	uint numPicks;
 };
 
+Common::StringArray loadMacBookNames(uint book) {
+	// The Mac release embeds the chooser strings in the application resource
+	// data instead of shipping BOOK*.NME files.
+	static const char *const kMacBook1Names[] = {
+		"Case of the Stolen Skateboard",
+		"Case of the Roundabout Robber",
+		"Case of the Runaway Reptile",
+		"Case of the Mysterious Monster",
+		"Case of the Rock Ripoff",
+		"Case of the Pilfered Pop",
+		"Case of the Creepy Cinema",
+		"Case of the Angry Arsonist",
+		"Case of the Crazy Compass",
+		"Case of the Midnight Masquerade",
+		"Case of the Missing Mink",
+		"Case of the Basketball Blooper",
+		"Case of the Reappearing Recipe",
+		"Case of the Attacking Aliens",
+		"Case of the Dangling Diamond",
+		"Case of the Buried Booty",
+		"Case of the Questionable Quiz",
+		"Case of the Cryptic Cavern",
+		"Case of the International Idol",
+		"Case of the Counterfeit Card",
+		"Case of the Puzzling Pooches",
+		"Case of the Baffling Bones",
+		"Case of the Vanishing Violin",
+		"Case of the Antique Autograph",
+	};
+	static const char *const kMacBook2Names[] = {
+		"Case of the Stolen Skateboard",
+		"Case of the Angry Arsonist",
+		"Case of the Roundabout Robber",
+		"Case of the Cryptic Cavern",
+		"Case of the Missing Mink",
+		"Case of the International Idol",
+		"Case of the Runaway Reptile",
+		"Case of the Buried Booty",
+		"Case of the Midnight Masquerade",
+		"Case of the Pilfered Pop",
+		"Case of the Rock Ripoff",
+		"Case of the Basketball Blooper",
+		"Case of the Questionable Quiz",
+		"Case of the Baffling Bones",
+		"Case of the Vanishing Violin",
+		"Case of the Dangling Diamond",
+		"Case of the Puzzling Pooches",
+		"Case of the Mysterious Monster",
+		"Case of the Attacking Aliens",
+		"Case of the Antique Autograph",
+		"Case of the Reappearing Recipe",
+		"Case of the Creepy Cinema",
+		"Case of the Crazy Compass",
+		"Case of the Counterfeit Card",
+	};
+	static const char *const kMacBook3Names[] = {
+		"Case of the Midnight Masquerade",
+		"Case of the Puzzled Pooches",
+		"Case of the Buried Booty",
+		"Case of the International Idol",
+		"Case of the Antique Autograph",
+		"Case of the Attacking Aliens",
+	};
+
+	const char *const *src = nullptr;
+	uint count = 0;
+	switch (book) {
+	case 1:
+		src = kMacBook1Names;
+		count = ARRAYSIZE(kMacBook1Names);
+		break;
+	case 2:
+		src = kMacBook2Names;
+		count = ARRAYSIZE(kMacBook2Names);
+		break;
+	case 3:
+		src = kMacBook3Names;
+		count = ARRAYSIZE(kMacBook3Names);
+		break;
+	default:
+		break;
+	}
+
+	Common::StringArray names;
+	for (uint i = 0; i < count; i++)
+		names.push_back(src[i]);
+	return names;
+}
+
 // `_DoChooseMystery @ 1a35:02b7`
-Common::StringArray loadBookNames(uint book) {
+Common::StringArray loadBookNames(uint book, bool macintosh) {
+	if (macintosh)
+		return loadMacBookNames(book);
+
 	Common::StringArray names;
 	const Common::String fname = Common::String::format("BOOK%u.NME", book);
 	Common::File f;
@@ -2222,7 +2346,7 @@ void EEMEngine::doCaseSelection() {
 	if (stageHi > kMaxMystery)
 		stageHi = kMaxMystery;
 
-	const Common::StringArray names = loadBookNames(book);
+	const Common::StringArray names = loadBookNames(book, isMacintosh());
 	if (names.empty()) {
 		warning("doCaseSelection: BOOK%u.NME failed to load", book);
 		return;
@@ -2617,6 +2741,17 @@ Common::String EEMEngine::notebookNoteText(uint clueId, const byte *ni,
 		return parseString(Common::String(p, len),
 						   _playerName, _partner);
 	}
+	if (isMacintosh() && bufBase) {
+		const uint16 textOff = READ_LE_UINT16(ni + clueId * 8);
+		if (textOff == 0 || textOff >= mysSz)
+			return Common::String();
+		const char *p = (const char *)(bufBase + textOff);
+		uint32 len = 0;
+		while (textOff + len < mysSz && p[len] != 0)
+			len++;
+		return parseString(Common::String(p, len),
+						   _playerName, _partner);
+	}
 	const uint stride = isLondon() ? 2 : 4;
 	const uint16 textOff = READ_LE_UINT16(ni + clueId * stride);
 	return parseString(_mystery.textAt(textOff),
@@ -2890,13 +3025,14 @@ void EEMEngine::doGallery() {
 bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 						  const Picture &galBg, bool haveBg) {
 	const bool floppyMI = isFloppy();
-	const byte *suspect = floppyMI
+	const bool compactMI = floppyMI || isMacintosh();
+	const byte *suspect = compactMI
 							  ? _mystery.floppySuspectEntry(suspectIdx)
 							  : gd + suspectIdx * 0x46;
 	if (!suspect)
 		return false;
 	const uint16 detailPic = READ_LE_UINT16(suspect + 0);
-	const uint clueCount = floppyMI
+	const uint clueCount = compactMI
 							   ? (uint)suspect[4]
 							   : READ_LE_UINT16(suspect + 8);
 
@@ -2905,7 +3041,7 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 	const int rx = 78, ry = 93;
 	const int rw = 288 - 78, rh = 152 - 93;
 	const int lineH = _font.getFontHeight();
-	const uint clueMax = floppyMI ? clueCount : 30u;
+	const uint clueMax = compactMI ? clueCount : 30u;
 	const byte *ni = _mystery.noteIndex();
 	const uint16 niCount = isLondon()
 		? (uint16)(_mystery.noteSectionSize() / 2)
@@ -2940,10 +3076,10 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 		uint k = pageStart;
 		bool reachedEnd = false;
 		for (; k < clueCount && k < clueMax; k++) {
-			const uint16 clueId = floppyMI
+			const uint16 clueId = compactMI
 				? (uint16)suspect[5 + k]
 				: READ_LE_UINT16(suspect + 0xa + k * 2);
-			if (!floppyMI && clueId == 0xFFFF) {
+			if (!compactMI && clueId == 0xFFFF) {
 				reachedEnd = true;
 				break;
 			}
@@ -3152,6 +3288,7 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
 				   g_system->getMillis());
 
 	const bool floppy = isFloppy();
+	const bool compactGallery = floppy || isMacintosh();
 	const GallerySlot * const slots =
 		floppy ? kFloppyGallerySlots : kGallerySlots;
 	for (uint i = 0; i < numSuspects && i < Mystery::kGalleryCap; i++) {
@@ -3165,7 +3302,7 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
 
 		const bool discovered = _mystery._inGallery[phys] != 0;
 		if (discovered) {
-			const byte *entry = floppy
+			const byte *entry = compactGallery
 				? _mystery.floppySuspectEntry(i)
 				: gd + i * 0x46;
 			if (!entry)
@@ -3926,10 +4063,15 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
 		!detailAnim.empty()) {
 		const uint frameIdx = bigMapDetailPartnerFrameAtTick(
 				(uint)detailAnim.size(), elapsedMs);
-		blitAnimFrameAnchored(scratch.surfacePtr(),
-							  detailAnim[frameIdx],
-							  mac ? scaleX(0x101) : 0x101,
-							  mac ? scaleY(0x50) : 0x50);
+		const int anchorX = mac ? scaleX(0x101) : 0x101;
+		const int anchorY = mac ? scaleY(0x50) : 0x50;
+		if (mac)
+			blitMacBigMapPartnerFrame(scratch, detailAnim[frameIdx],
+									  anchorX, anchorY);
+		else
+			blitAnimFrameAnchored(scratch.surfacePtr(),
+								  detailAnim[frameIdx],
+								  anchorX, anchorY);
 	}
 
 	g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
@@ -3955,6 +4097,14 @@ Common::String EEMEngine::accuseNoteText(uint clueId,
 			(const char *)(ctx.bufBaseNotes + textOff),
 			_playerName, _partner);
 	}
+	if (isMacintosh() && ctx.bufBaseNotes) {
+		const uint16 textOff = READ_LE_UINT16(ctx.ni + clueId * 8);
+		if (textOff == 0 || textOff >= _mystery.dataSize())
+			return Common::String();
+		return parseString(
+			(const char *)(ctx.bufBaseNotes + textOff),
+			_playerName, _partner);
+	}
 	const uint stride = isLondon() ? 2 : 4;
 	const uint16 textOff = READ_LE_UINT16(ctx.ni + clueId * stride);
 	return parseString(_mystery.textAt(textOff),
@@ -4698,9 +4848,10 @@ void EEMEngine::doAccuse() {
 		Picture alibiBg;
 		const bool haveAlibiBg = _picsArchive.getPicture(0x3e, alibiBg);
 		Picture suspect;
-		const uint16 picId = gd
-			? READ_LE_UINT16(gd + (uint)picked * 0x46)
-			: 0;
+		const byte *pickedSuspect = isMacintosh()
+			? _mystery.floppySuspectEntry((uint)picked)
+			: (gd ? gd + (uint)picked * 0x46 : nullptr);
+		const uint16 picId = pickedSuspect ? READ_LE_UINT16(pickedSuspect) : 0;
 		const bool haveSuspect = picId != 0 &&
 			_picsArchive.getPicture(picId, suspect);
 		Picture balloon;
@@ -4788,7 +4939,7 @@ void EEMEngine::doAccuse() {
 		g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
 								   0, 0, kScreenWidth, kScreenHeight);
 		g_system->updateScreen();
-		if (_audio && gd) {
+		if (_audio && gd && !isMacintosh()) {
 			const uint16 alibiVoice =
 				READ_LE_UINT16(gd + (uint)picked * 0x46 + 0x00);
 			const uint16 jakeVoice =
@@ -5550,7 +5701,12 @@ void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
 			continue;
 		const GallerySlot &s = kGallerySlots[phys];
 
-		const uint16 picId = READ_LE_UINT16(gd + i * 0x46);
+		const byte *entry = isMacintosh()
+			? _mystery.floppySuspectEntry(i)
+			: gd + i * 0x46;
+		if (!entry)
+			continue;
+		const uint16 picId = READ_LE_UINT16(entry);
 		if (picId == 0)
 			continue;
 		Picture portrait;


Commit: c2f7d05571509f8b7f0849c9fbf4143a2862c07a
    https://github.com/scummvm/scummvm/commit/c2f7d05571509f8b7f0849c9fbf4143a2862c07a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-23T15:47:52+02:00

Commit Message:
EEM: correctly load animations from EEM1 Mac

Changed paths:
    engines/eem/clues.cpp
    engines/eem/graphics.cpp
    engines/eem/site.cpp
    engines/eem/site.h


diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 4052bf8d31c..cfefa7a4a0a 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -1338,7 +1338,10 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
 			getBalloonInsets(balloonId, textXIns, textYIns, textWidth);
 		const int textX = ballX + textXIns;
 		const int lineH = dialogFont.getFontHeight();
-		const byte textColor = mac ? 0xff : 0;
+		MacSpritePaletteMap macPaletteMap = {0x00, 0xFF};
+		if (mac)
+			macPaletteMap = getMacSpritePaletteMap();
+		const byte textColor = mac ? macPaletteMap.black : 0;
 
 		bool firstPage  = true;
 		int  cursorY    = ballY + textYIns;
@@ -1374,16 +1377,26 @@ void EEMEngine::displayFloppyDialogRecords(const byte *rec, uint count,
 				if (picID != 0 && picID != 0xFFFF) {
 					Picture pic;
 					if (_picsArchive.getPicture(picID, pic)) {
-						scratch.transBlitFrom(pic.surface,
-											  Common::Point(picX, picY),
-											  (uint32)(byte)(pic.flags >> 8));
+						if (mac)
+							blitMacMaskedSurface(scratch.surfacePtr(), pic,
+												 picX, picY, false,
+												 macPaletteMap);
+						else
+							scratch.transBlitFrom(pic.surface,
+												  Common::Point(picX, picY),
+												  (uint32)(byte)(pic.flags >> 8));
 					}
 				}
 				if (haveBalloon) {
 					const byte transp = (byte)(balloon.flags >> 8);
-					scratch.transBlitFrom(balloon.surface,
-										  Common::Point(ballX, ballY),
-										  transp, flipBall);
+					if (mac)
+						blitMacMaskedSurface(scratch.surfacePtr(), balloon,
+											 ballX, ballY, flipBall,
+											 macPaletteMap);
+					else
+						scratch.transBlitFrom(balloon.surface,
+											  Common::Point(ballX, ballY),
+											  transp, flipBall);
 				}
 				cursorY = ballY + textYIns;
 			}
diff --git a/engines/eem/graphics.cpp b/engines/eem/graphics.cpp
index 6ded96af338..3f616e3c70a 100644
--- a/engines/eem/graphics.cpp
+++ b/engines/eem/graphics.cpp
@@ -888,8 +888,11 @@ void EEMEngine::drawFloppyBubbleIndicator(Graphics::ManagedSurface &dst,
 		return;
 	const int x = ballX + (int)dx;
 	const int y = ballY + (int)dy;
-	dst.transBlitFrom(pic.surface, Common::Point(x, y),
-					  (uint32)(byte)(pic.flags >> 8));
+	if (isMacintosh())
+		blitMacMaskedSurface(dst.surfacePtr(), pic, x, y);
+	else
+		dst.transBlitFrom(pic.surface, Common::Point(x, y),
+						  (uint32)(byte)(pic.flags >> 8));
 }
 
 } // End of namespace EEM
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index b0bac98637d..5a9d6e95c87 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -83,6 +83,48 @@ bool londonTravelSitePic(const Mystery *mystery, uint siteNum, uint16 &sitePic)
 	return false;
 }
 
+static bool paletteEntryIsWhite(const byte *rgb) {
+	return rgb[0] >= 0xFC && rgb[1] >= 0xFC && rgb[2] >= 0xFC;
+}
+
+static bool paletteEntryIsBlack(const byte *rgb) {
+	return rgb[0] <= 0x03 && rgb[1] <= 0x03 && rgb[2] <= 0x03;
+}
+
+static byte findPaletteEndpoint(const byte *palette, bool wantWhite,
+								byte preferred, byte alternate,
+								byte fallback) {
+	bool (*matches)(const byte *) = wantWhite ? paletteEntryIsWhite
+											  : paletteEntryIsBlack;
+	if (matches(palette + preferred * 3))
+		return preferred;
+	if (matches(palette + alternate * 3))
+		return alternate;
+	for (uint i = 0; i < 256; i++) {
+		if (matches(palette + i * 3))
+			return (byte)i;
+	}
+	return fallback;
+}
+
+MacSpritePaletteMap getMacSpritePaletteMap() {
+	byte palette[256 * 3];
+	g_system->getPaletteManager()->grabPalette(palette, 0, 256);
+
+	MacSpritePaletteMap map;
+	map.white = findPaletteEndpoint(palette, true, 0x00, 0xFF, 0x00);
+	map.black = findPaletteEndpoint(palette, false, 0xFF, 0x00, 0xFF);
+	return map;
+}
+
+byte mapMacSpriteColor(byte color, const MacSpritePaletteMap &paletteMap) {
+	if (color == 0x00)
+		return paletteMap.white;
+	if (color == 0xFF)
+		return paletteMap.black;
+	return color;
+}
+
 // Masked blit using `transp` = high byte of `pic.flags` (`_Rect_Move_Mask @ 1000:03fc`).
 void blitFrame(Graphics::ManagedSurface &dst, const Picture &p,
 			   int x, int y, byte transp) {
@@ -111,6 +153,39 @@ void blitMaskedSurface(Graphics::Surface *screen, const Picture &p,
 	keyBlitToScreen(screen, p, x, y);
 }
 
+void blitMacMaskedSurface(Graphics::Surface *dst, const Picture &p,
+						  int x, int y, bool flipX,
+						  const MacSpritePaletteMap &paletteMap) {
+	if (!dst || p.surface.empty())
+		return;
+
+	const Common::Rect dstRect =
+		Common::Rect(x, y, x + p.surface.w, y + p.surface.h)
+			.findIntersectingRect(Common::Rect(dst->w, dst->h));
+	if (dstRect.isEmpty())
+		return;
+
+	const byte transp = (byte)(p.flags >> 8);
+	for (int row = dstRect.top; row < dstRect.bottom; row++) {
+		const int srcY = row - y;
+		const byte *src = (const byte *)p.surface.getBasePtr(0, srcY);
+		byte *out = (byte *)dst->getBasePtr(dstRect.left, row);
+		for (int col = dstRect.left; col < dstRect.right; col++, out++) {
+			const int relX = col - x;
+			const int srcX = flipX ? (p.surface.w - 1 - relX) : relX;
+			const byte color = src[srcX];
+			if (color != transp)
+				*out = mapMacSpriteColor(color, paletteMap);
+		}
+	}
+}
+
+void blitMacMaskedSurface(Graphics::Surface *dst, const Picture &p,
+						  int x, int y, bool flipX) {
+	const MacSpritePaletteMap paletteMap = getMacSpritePaletteMap();
+	blitMacMaskedSurface(dst, p, x, y, flipX, paletteMap);
+}
+
 // `_UpdateAnimations @ 172b:09c1`
 void blitAnimFrameAnchored(Graphics::Surface *screen, const Picture &p,
 						   int anchorX, int anchorY) {
@@ -119,6 +194,21 @@ void blitAnimFrameAnchored(Graphics::Surface *screen, const Picture &p,
 					anchorY - (int)(int16)p.rowoff);
 }
 
+void blitMacAnimFrameAnchored(Graphics::Surface *dst, const Picture &p,
+							  int anchorX, int anchorY,
+							  const MacSpritePaletteMap &paletteMap) {
+	blitMacMaskedSurface(dst, p,
+						 anchorX - (int)(int16)p.miscflags,
+						 anchorY - (int)(int16)p.rowoff,
+						 false, paletteMap);
+}
+
+void blitMacAnimFrameAnchored(Graphics::Surface *dst, const Picture &p,
+							  int anchorX, int anchorY) {
+	const MacSpritePaletteMap paletteMap = getMacSpritePaletteMap();
+	blitMacAnimFrameAnchored(dst, p, anchorX, anchorY, paletteMap);
+}
+
 bool readHotspotRect(const byte *r, bool mac, Common::Rect &rect) {
 	if (mac) {
 		const int16 top = (int16)READ_LE_UINT16(r + 0);
@@ -1182,6 +1272,10 @@ bool SiteScreen::enterSiteAnim() {
 		Graphics::PixelFormat::createFormatCLUT8());
 	bg.simpleBlitFrom(*screen);
 	g_system->unlockScreen();
+	const bool mac = _vm->isMacintosh();
+	MacSpritePaletteMap macPaletteMap = {0x00, 0xFF};
+	if (mac)
+		macPaletteMap = getMacSpritePaletteMap();
 
 	if (_vm->isLondon()) {
 		const uint animId = (partner == 0) ? 7 : 0xf;
@@ -1229,7 +1323,11 @@ bool SiteScreen::enterSiteAnim() {
 			Graphics::ManagedSurface scratch(sw, sh,
 				Graphics::PixelFormat::createFormatCLUT8());
 			scratch.simpleBlitFrom(bg);
-			blitFrame(scratch, skate[frameIdx], x, y, transp);
+			if (mac)
+				blitMacMaskedSurface(scratch.surfacePtr(), skate[frameIdx],
+									 x, y, false, macPaletteMap);
+			else
+				blitFrame(scratch, skate[frameIdx], x, y, transp);
 			g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
 									   0, 0, sw, sh);
 			g_system->updateScreen();
@@ -1265,7 +1363,11 @@ bool SiteScreen::enterSiteAnim() {
 			Graphics::ManagedSurface scratch(sw, sh,
 				Graphics::PixelFormat::createFormatCLUT8());
 			scratch.simpleBlitFrom(bg);
-			blitFrame(scratch, fr, destX, destY, transp);
+			if (mac)
+				blitMacMaskedSurface(scratch.surfacePtr(), fr, destX, destY,
+									 false, macPaletteMap);
+			else
+				blitFrame(scratch, fr, destX, destY, transp);
 			g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
 									   0, 0, sw, sh);
 			g_system->updateScreen();
@@ -1333,6 +1435,9 @@ void SiteScreen::renderFloppyDrops(uint siteNum) {
 	if (!screen)
 		return;
 
+	MacSpritePaletteMap macPaletteMap = {0x00, 0xFF};
+	if (mac)
+		macPaletteMap = getMacSpritePaletteMap();
 	for (uint i = 0; i < count; i++) {
 		const byte *e = drops + 2 + i * (mac ? 6 : 5);
 		const uint16 picID = READ_LE_UINT16(e + 0);
@@ -1344,7 +1449,10 @@ void SiteScreen::renderFloppyDrops(uint siteNum) {
 		Picture pic;
 		if (!_vm->getPics().getPicture((uint)picID, pic))
 			continue;
-		blitMaskedSurface(screen, pic, x, y);
+		if (mac)
+			blitMacMaskedSurface(screen, pic, x, y, false, macPaletteMap);
+		else
+			blitMaskedSurface(screen, pic, x, y);
 	}
 	g_system->unlockScreen();
 }
@@ -1577,7 +1685,10 @@ void SiteScreen::renderPartner(uint siteNum, uint32 tickMs) {
 	Graphics::Surface *screen = g_system->lockScreen();
 	if (!screen)
 		return;
-	blitAnimFrameAnchored(screen, anim[frameIdx], x, y);
+	if (_vm->isMacintosh())
+		blitMacAnimFrameAnchored(screen, anim[frameIdx], x, y);
+	else
+		blitAnimFrameAnchored(screen, anim[frameIdx], x, y);
 	g_system->unlockScreen();
 }
 
@@ -1614,7 +1725,10 @@ bool SiteScreen::renderFloppyHotspotPartnerPose(uint siteNum) {
 		return false;
 
 	const uint frameIdx = partnerFrameAtTick(animId, (uint)anim.size(), 0);
-	blitAnimFrameAnchored(screen, anim[frameIdx], x, y);
+	if (mac)
+		blitMacAnimFrameAnchored(screen, anim[frameIdx], x, y);
+	else
+		blitAnimFrameAnchored(screen, anim[frameIdx], x, y);
 	g_system->unlockScreen();
 	return true;
 }
diff --git a/engines/eem/site.h b/engines/eem/site.h
index f17f8ae4e20..8c65d9c0eee 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -67,6 +67,26 @@ uint bigMapDetailPartnerFrameAtTick(uint numFrames, uint32 elapsedMs);
 void blitAnimFrameAnchored(Graphics::Surface *screen, const Picture &p,
 						   int anchorX, int anchorY);
 
+struct MacSpritePaletteMap {
+	byte white;
+	byte black;
+};
+
+/// Mac UI/sprite art is authored with color 0 = white and 0xff = black,
+/// but several site ColorTables swap those endpoint slots.
+MacSpritePaletteMap getMacSpritePaletteMap();
+byte mapMacSpriteColor(byte color, const MacSpritePaletteMap &paletteMap);
+void blitMacMaskedSurface(Graphics::Surface *dst, const Picture &p,
+						  int x, int y, bool flipX = false);
+void blitMacMaskedSurface(Graphics::Surface *dst, const Picture &p,
+						  int x, int y, bool flipX,
+						  const MacSpritePaletteMap &paletteMap);
+void blitMacAnimFrameAnchored(Graphics::Surface *dst, const Picture &p,
+							  int anchorX, int anchorY);
+void blitMacAnimFrameAnchored(Graphics::Surface *dst, const Picture &p,
+							  int anchorX, int anchorY,
+							  const MacSpritePaletteMap &paletteMap);
+
 /// Rotate one VGA palette range by one slot (START→END direction).
 void cyclePaletteRange(uint8 start, uint8 end);
 


Commit: c17db422a871b20eebb33232bf410cf6ecff5caf
    https://github.com/scummvm/scummvm/commit/c17db422a871b20eebb33232bf410cf6ecff5caf
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-23T15:47:52+02:00

Commit Message:
EEM: improved playback of music from EEM1 Mac

Changed paths:
    engines/eem/music.cpp
    engines/eem/music.h


diff --git a/engines/eem/music.cpp b/engines/eem/music.cpp
index 988dd7ad949..9d41fc64d8b 100644
--- a/engines/eem/music.cpp
+++ b/engines/eem/music.cpp
@@ -38,6 +38,8 @@ namespace EEM {
 const int kMidiDriverFlags = MDT_MIDI | MDT_ADLIB | MDT_PREFER_MT32;
 const int kMacMidiDriverFlags = MDT_MIDI | MDT_PREFER_GM;
 const uint16 kInvalidMacMidiResource = 0xffff;
+const uint16 kInvalidMacSongResource = 0xffff;
+const byte kNoMacInstrument = 0xff;
 
 Common::String musicNameFromPath(const Common::Path &path) {
 	Common::String name = path.baseName();
@@ -48,42 +50,60 @@ Common::String musicNameFromPath(const Common::Path &path) {
 	return name;
 }
 
-uint16 macMidiResourceIdForFile(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);
+}
+
+uint16 macSongResourceIdForFile(const Common::Path &path) {
 	const Common::String name = musicNameFromPath(path);
 	if (name == "THEME" || name == "THEME SONG")
-		return 0;
+		return 1000;
 	if (name == "WIN1" || name == "FANFARE2")
-		return 1;
+		return 1001;
 	if (name == "TRAVEL-1")
-		return 2;
+		return 1006;
 	if (name == "TRAVEL-4")
-		return 3;
+		return 1002;
 	if (name == "TRAVEL-6")
-		return 4;
+		return 1004;
 	if (name == "TRAVEL-7")
-		return 5;
+		return 1003;
 	if (name == "TRAVEL-8")
-		return 6;
+		return 1005;
 	if (name == "WRONG2")
-		return 7;
-	return kInvalidMacMidiResource;
+		return 1007;
+	return kInvalidMacSongResource;
 }
 
-uint16 macMidiResourceIdForMus(uint num) {
+uint16 macSongResourceIdForMus(uint num) {
 	static const uint16 kTravelTracks[5] = {
-		4, 3, 5, 2, 6  // Travel-6, Travel-4, Travel-7, Travel-1, Travel-8
+		1004, 1002, 1003, 1006, 1005 // Travel-6/4/7/1/8
 	};
 	if (num < ARRAYSIZE(kTravelTracks))
 		return kTravelTracks[num];
 	if (num == 5)
-		return 1; // WIN1.MID
+		return 1001; // Win1
 	if (num == 6)
-		return 7; // WRONG2.MID
-	return kInvalidMacMidiResource;
+		return 1007; // Wrong2
+	return kInvalidMacSongResource;
 }
 
 MusicPlayer::MusicPlayer(bool isFloppy, bool isMacintosh) :
 	_isFloppy(isFloppy), _isMacintosh(isMacintosh) {
+	clearMacInstrumentMap();
+
 	// _InitMIDI @ 20a2:013a — `_AIL_register_driver` against
 	// ADLIB.ADV / SBFM.ADV / MT32MPU.ADV. We honour the launcher's
 	// "Music driver" setting and prefer MT-32 when unset.
@@ -145,6 +165,15 @@ MusicPlayer::MusicPlayer(bool isFloppy, bool isMacintosh) :
 }
 
 void MusicPlayer::send(uint32 b) {
+	if (_isMacintosh && (b & 0xF0) == 0xC0) {
+		const byte channel = (byte)(b & 0x0F);
+		const byte rawProgram = (byte)((b >> 8) & 0x7F);
+		const byte inst = _macChannelInstrument[channel] != kNoMacInstrument
+			? _macChannelInstrument[channel] : rawProgram;
+		const byte gmProgram = mapMacInstrumentToGM(inst, channel);
+		b = (b & 0xFFFF00FF) | ((uint32)gmProgram << 8);
+	}
+
 	// Miles drivers (both AdLib and MT-32) implement their own per-
 	// source-channel mixing and timbre installation, so forward the raw
 	// event.
@@ -184,13 +213,95 @@ void MusicPlayer::startLoadedMusic(const Common::String &name, bool loop,
 		   name.c_str(), _xmiData.size(), loop, _milesAudioMode, smf);
 }
 
+void MusicPlayer::clearMacInstrumentMap() {
+	memset(_macChannelInstrument, kNoMacInstrument,
+		   sizeof(_macChannelInstrument));
+}
+
+byte MusicPlayer::mapMacInstrumentToGM(byte inst, byte channel) const {
+	if (channel == 9)
+		return 0;
+
+	switch (inst) {
+	case 2:
+	case 3:
+		return 0;   // Piano
+	case 11:
+		return 16;  // Organ
+	case 60:
+		return 24;  // Guitar
+	case 64:
+	case 65:
+		return 33;  // Bass
+	case 73:
+	case 74:
+	case 75:
+		return 73;  // Flute
+	case 83:
+	case 84:
+		return 71;  // Clarinet
+	default:
+		return inst < 128 ? inst : 0;
+	}
+}
+
+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);
+	if (!stream) {
+		warning("MusicPlayer: Mac SONG resource %u missing", resourceId);
+		return false;
+	}
+
+	const uint32 size = (uint32)stream->size();
+	if (size < 18) {
+		delete stream;
+		warning("MusicPlayer: Mac SONG resource %u too short", resourceId);
+		return false;
+	}
+
+	Common::Array<byte> song;
+	song.resize(size);
+	if (stream->read(song.data(), size) != size) {
+		delete stream;
+		warning("MusicPlayer: short read on Mac SONG resource %u", resourceId);
+		return false;
+	}
+	delete stream;
+
+	midiId = READ_BE_UINT16(song.data());
+	const uint16 instCount = READ_BE_UINT16(song.data() + 16);
+	uint32 pos = 18;
+	for (uint i = 0; i < instCount && pos + 4 <= size; i++, pos += 4) {
+		const uint16 channel = READ_BE_UINT16(song.data() + pos);
+		const uint16 inst = READ_BE_UINT16(song.data() + pos + 2);
+		if (channel < ARRAYSIZE(_macChannelInstrument) && inst < 128)
+			_macChannelInstrument[channel] = (byte)inst;
+	}
+
+	return true;
+}
+
+void MusicPlayer::playMacSongResource(uint16 resourceId, bool loop) {
+	if (resourceId == kInvalidMacSongResource)
+		return;
+
+	uint16 midiId = kInvalidMacMidiResource;
+	if (!loadMacSong(resourceId, midiId))
+		return;
+
+	playMacMidiResource(midiId, loop);
+}
+
 void MusicPlayer::playMacMidiResource(uint16 resourceId, bool loop) {
 	if (resourceId == kInvalidMacMidiResource)
 		return;
 
-	Common::SeekableReadStream *stream =
-		openMacResource(Common::Path("EEM Sound&Music"),
-						MKTAG('M', 'i', 'd', 'i'), resourceId);
+	Common::SeekableReadStream *stream = openMacMidiResource(resourceId);
 	if (!stream) {
 		warning("MusicPlayer: Mac Midi resource %u missing", resourceId);
 		return;
@@ -223,13 +334,13 @@ void MusicPlayer::playFile(const Common::Path &xmiPath, bool loop) {
 	stop();
 
 	if (_isMacintosh) {
-		const uint16 resourceId = macMidiResourceIdForFile(xmiPath);
-		if (resourceId == kInvalidMacMidiResource) {
-			warning("MusicPlayer: no Mac Midi mapping for %s",
+		const uint16 resourceId = macSongResourceIdForFile(xmiPath);
+		if (resourceId == kInvalidMacSongResource) {
+			warning("MusicPlayer: no Mac SONG mapping for %s",
 					xmiPath.toString().c_str());
 			return;
 		}
-		playMacMidiResource(resourceId, loop);
+		playMacSongResource(resourceId, loop);
 		return;
 	}
 
@@ -258,7 +369,7 @@ void MusicPlayer::playMus(uint num, bool loop) {
 	if (_isMacintosh) {
 		Common::StackLock lock(_mutex);
 		stop();
-		playMacMidiResource(macMidiResourceIdForMus(num), loop);
+		playMacSongResource(macSongResourceIdForMus(num), loop);
 		return;
 	}
 
diff --git a/engines/eem/music.h b/engines/eem/music.h
index 35b9ddd8e80..0aec7735c32 100644
--- a/engines/eem/music.h
+++ b/engines/eem/music.h
@@ -65,12 +65,17 @@ public:
 
 private:
 	void playMacMidiResource(uint16 resourceId, bool loop);
+	void playMacSongResource(uint16 resourceId, bool loop);
 	void startLoadedMusic(const Common::String &name, bool loop, bool smf);
+	bool loadMacSong(uint16 resourceId, uint16 &midiId);
+	void clearMacInstrumentMap();
+	byte mapMacInstrumentToGM(byte inst, byte channel) const;
 
 	bool _milesAudioMode = false;
 	const bool _isFloppy;
 	const bool _isMacintosh;
 	Common::Array<byte> _xmiData;
+	byte _macChannelInstrument[16] = {};
 };
 
 } // End of namespace EEM


Commit: d9a29123e1abf2617289c16f2f25f4355cc79b4d
    https://github.com/scummvm/scummvm/commit/d9a29123e1abf2617289c16f2f25f4355cc79b4d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-23T15:47:52+02:00

Commit Message:
EEM: implemented intro from EEM1 Mac

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


diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index a3a762b7c2a..c9cab6db0a4 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -57,6 +57,21 @@ const uint kPicStormLogo       = 0x20b; // Floppy storm-logo still
 const uint kPalEAKids          = 0x25;
 const uint kPalHighScore       = 0x27;
 const uint kPalStormLogo       = 0x26;  // Floppy FUN_23d2_0605
+const uint kMacPicEAKidsLogo   = 0x213; // FUN_000092be
+const uint kMacPicTitleRoom    = 0x20d; // FUN_000096fa title room
+const uint kMacPicTitleDark    = 0x20e;
+const uint kMacPicTitleFinal   = 0x20f;
+const uint kMacPicTitleIn0     = 0x210;
+const uint kMacPicTitleIn1     = 0x211;
+const uint kMacPicTitleIn2     = 0x212;
+const uint kMacPicTitleLeft0   = 0x214;
+const uint kMacPicTitleLeft1   = 0x215;
+const uint kMacPicTitleLeft2   = 0x216;
+const uint kMacPicTitleRight0  = 0x217;
+const uint kMacPicTitleRight1  = 0x218;
+const uint kMacPicTitleRight2  = 0x219;
+const uint kMacPalEAKids       = 0x28;
+const uint kMacPalTitle        = 0x29;
 const uint kPicMousePointer    = 0x50;  // 0x51 is the wait cursor
 const uint16 kMacFontResource  = 3214;  // 'Eagle Eye 14' FONT resource
 const uint16 kMacSmallFontResource = 3209; // Smaller speech/dialog FONT.
@@ -470,7 +485,10 @@ Common::Error EEMEngine::run() {
 	}
 
 	if (isMacintosh()) {
+		runMacStartup();
 		CursorMan.showMouse(true);
+		if (_music)
+			_music->stop();
 		if (!shouldQuit())
 			doProfilePicker();
 		if (!shouldQuit())
@@ -1111,6 +1129,268 @@ void EEMEngine::showHighScoreLogo() {
 	fadeCurrentPaletteToBlack();
 }
 
+static void copyNativePictureToScreen(const Picture &pic) {
+	const int w = MIN<int>(pic.surface.w, kMacScreenWidth);
+	const int h = MIN<int>(pic.surface.h, kMacScreenHeight);
+	if (w <= 0 || h <= 0)
+		return;
+	g_system->copyRectToScreen(pic.surface.getPixels(), pic.surface.pitch,
+							   0, 0, w, h);
+}
+
+static void copyNativeSurfaceToScreen(const Graphics::ManagedSurface &surface) {
+	const int w = MIN<int>(surface.w, kMacScreenWidth);
+	const int h = MIN<int>(surface.h, kMacScreenHeight);
+	if (w <= 0 || h <= 0)
+		return;
+	g_system->copyRectToScreen(surface.getPixels(), surface.pitch,
+							   0, 0, w, h);
+}
+
+static void blitNativeTransparent(Graphics::ManagedSurface &dst,
+								  const Picture &pic, int x, int y) {
+	if (pic.surface.empty())
+		return;
+	const byte transparent = (byte)(pic.flags >> 8);
+	dst.transBlitFrom(pic.surface, Common::Point(x, y), transparent);
+}
+
+bool EEMEngine::waitIntroDelay(uint32 maxMs) {
+	const uint32 startMs = g_system->getMillis();
+	while (!shouldQuit() && (g_system->getMillis() - startMs < maxMs)) {
+		Common::Event event;
+		while (g_system->getEventManager()->pollEvent(event)) {
+			if (event.type == Common::EVENT_QUIT ||
+				event.type == Common::EVENT_RETURN_TO_LAUNCHER)
+				return true;
+			if (event.type == Common::EVENT_LBUTTONDOWN)
+				return true;
+			if (event.type == Common::EVENT_KEYDOWN) {
+				if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
+					_skipIntro = true;
+					interruptAudio();
+				}
+				return true;
+			}
+		}
+		g_system->updateScreen();
+		g_system->delayMillis(5);
+	}
+	return false;
+}
+
+void EEMEngine::showMacEAKidsLogo() {
+	Picture pic;
+	if (!_picsArchive.getPicture(kMacPicEAKidsLogo, pic)) {
+		warning("Mac EA Kids logo (%u) load failed", kMacPicEAKidsLogo);
+		return;
+	}
+	copyNativePictureToScreen(pic);
+
+	byte fpal[kPalSize];
+	if (!getSitePalette(kMacPalEAKids, fpal)) {
+		warning("Mac EA Kids palette (%u) load failed", kMacPalEAKids);
+		return;
+	}
+
+	bool aborted = false;
+	for (uint pass = 0; pass < 2 && !aborted && !shouldQuit(); pass++) {
+		const bool show = (pass != 0);
+		int delayCount = 9;
+
+		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(0x23 * 40);
+	}
+
+	fadeCurrentPaletteToBlack();
+}
+
+void EEMEngine::showMacStillLogo(uint picId, uint palId, uint holdMs,
+								 bool playThunder) {
+	Picture pic;
+	if (!_picsArchive.getPicture(picId, pic)) {
+		warning("Mac logo PIC 0x%x load failed", picId);
+		return;
+	}
+	copyNativePictureToScreen(pic);
+
+	byte target[kPalSize];
+	if (!getSitePalette(palId, target)) {
+		warning("Mac logo palette 0x%x load failed", palId);
+		return;
+	}
+
+	byte black[kPalSize] = {};
+	g_system->getPaletteManager()->setPalette(black, 0, 256);
+	g_system->updateScreen();
+	fadePaletteFromBlack(target);
+
+	if (playThunder && _audio)
+		_audio->playVoc(Common::Path("THUNDER.VOC"));
+
+	waitIntroDelay(holdMs);
+	if (_audio)
+		_audio->stopVoice();
+	fadeCurrentPaletteToBlack();
+}
+
+void EEMEngine::showMacTitleIntro() {
+	byte target[kPalSize];
+	if (!getSitePalette(kMacPalTitle, target)) {
+		warning("Mac title palette (%u) load failed", kMacPalTitle);
+		return;
+	}
+
+	Picture room, titleDark, titleFinal;
+	if (!_picsArchive.getPicture(kMacPicTitleRoom, room) ||
+		!_picsArchive.getPicture(kMacPicTitleDark, titleDark) ||
+		!_picsArchive.getPicture(kMacPicTitleFinal, titleFinal)) {
+		warning("Mac title base pictures failed to load");
+		return;
+	}
+
+	Picture in[3], left[3], right[3];
+	const uint inIds[3] = {
+		kMacPicTitleIn0, kMacPicTitleIn1, kMacPicTitleIn2
+	};
+	const uint leftIds[3] = {
+		kMacPicTitleLeft0, kMacPicTitleLeft1, kMacPicTitleLeft2
+	};
+	const uint rightIds[3] = {
+		kMacPicTitleRight0, kMacPicTitleRight1, kMacPicTitleRight2
+	};
+	for (uint i = 0; i < 3; i++) {
+		if (!_picsArchive.getPicture(inIds[i], in[i]) ||
+			!_picsArchive.getPicture(leftIds[i], left[i]) ||
+			!_picsArchive.getPicture(rightIds[i], right[i])) {
+			warning("Mac title overlay PIC load failed");
+			return;
+		}
+	}
+
+	Graphics::ManagedSurface frame(kMacScreenWidth, kMacScreenHeight,
+								   Graphics::PixelFormat::createFormatCLUT8());
+	frame.blitFrom(room.surface, Common::Point(0, 0));
+	copyNativeSurfaceToScreen(frame);
+
+	byte black[kPalSize] = {};
+	g_system->getPaletteManager()->setPalette(black, 0, 256);
+	g_system->updateScreen();
+	fadePaletteFromBlack(target);
+
+	for (int i = 0; i < 0x2c && !shouldQuit() && !_skipIntro; i++) {
+		frame.blitFrom(room.surface, Common::Point(0, 0));
+		const int revealW = (i + 1) * kMacScreenWidth / 0x2c;
+		if (revealW > 0) {
+			const Common::Rect src(0, 0, revealW, kMacScreenHeight);
+			frame.blitFrom(titleDark.surface, src, Common::Point(0, 0));
+		}
+
+		switch (i) {
+		case 15:
+		case 19:
+			blitNativeTransparent(frame, in[0], 0xb9, 0x29);
+			break;
+		case 16:
+		case 18:
+			blitNativeTransparent(frame, in[1], 0xb9, 0x29);
+			break;
+		case 17:
+			blitNativeTransparent(frame, in[2], 0xb9, 0x29);
+			break;
+		default:
+			break;
+		}
+
+		copyNativeSurfaceToScreen(frame);
+		if (waitIntroDelay(40))
+			return;
+	}
+
+	for (int i = 0x2a; i >= 0 && !shouldQuit() && !_skipIntro; i--) {
+		frame.blitFrom(titleFinal.surface, Common::Point(0, 0));
+
+		switch (i) {
+		case 7:
+		case 11:
+			blitNativeTransparent(frame, left[0], 0x39, 0xb1);
+			break;
+		case 8:
+		case 10:
+			blitNativeTransparent(frame, left[1], 0x39, 0xb1);
+			break;
+		case 9:
+			blitNativeTransparent(frame, left[2], 0x39, 0xb1);
+			break;
+		case 24:
+		case 28:
+			blitNativeTransparent(frame, right[0], 0x131, 0xb1);
+			break;
+		case 25:
+		case 27:
+			blitNativeTransparent(frame, right[1], 0x131, 0xb1);
+			break;
+		case 26:
+			blitNativeTransparent(frame, right[2], 0x131, 0xb1);
+			break;
+		default:
+			break;
+		}
+
+		copyNativeSurfaceToScreen(frame);
+		if (waitIntroDelay(40))
+			return;
+	}
+
+	frame.blitFrom(titleFinal.surface, Common::Point(0, 0));
+	copyNativeSurfaceToScreen(frame);
+	waitIntroDelay(0xFFFFFFFFu);
+}
+
+void EEMEngine::runMacStartup() {
+	CursorMan.showMouse(false);
+	_skipIntro = false;
+	debugC(1, kDebugGeneral, "EEM1 Mac: opening sequence");
+
+	if (!shouldQuit() && !_skipIntro)
+		showMacEAKidsLogo();
+	if (!shouldQuit() && !_skipIntro)
+		showMacStillLogo(kPicHighScoreLogo, kPalHighScore, 3000,
+						 /* playThunder= */ false);
+	if (!shouldQuit() && !_skipIntro)
+		showMacStillLogo(kPicStormLogo, kPalStormLogo, 3000,
+						 /* playThunder= */ true);
+
+	if (!shouldQuit() && !_skipIntro && _music)
+		_music->playFile(Common::Path("THEME.XMI"), /* loop= */ true);
+	if (!shouldQuit() && !_skipIntro)
+		showMacTitleIntro();
+
+	if (_music)
+		_music->stop();
+	_skipIntro = false;
+}
+
 void EEMEngine::showLondonLogo(uint picId, uint palId, uint holdMs) {
 	Picture pic;
 	if (!_picsArchive.getPicture(picId, pic) || pic.surface.empty()) {
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index f99c4086b79..7c0ae46d08f 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -411,6 +411,13 @@ private:
 	void showHighScoreLogo();
 	void showFloppyStormLogo();
 
+	bool waitIntroDelay(uint32 maxMs);
+	void runMacStartup();
+	void showMacEAKidsLogo();
+	void showMacStillLogo(uint picId, uint palId, uint holdMs,
+						  bool playThunder);
+	void showMacTitleIntro();
+
 	void runLondonStartup();
 	/// Start London mystery 0 after a freshly-created detective chooses a partner.
 	bool startLondonTrainingMystery();




More information about the Scummvm-git-logs mailing list