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

neuromancer noreply at scummvm.org
Thu Sep 10 15:45:45 UTC 2026


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

Summary:
6b343086b8 EEM: initial support for talkie release of eem1 (mac)
60dcce8a22 EEM: show intro for eem1 talkie (mac)
96d27fdf90 EEM: better animations and precise map locations in EEM1 talkie (mac)
67c90d9a0b EEM: implement TRAVIS scroll bar in EEM1 talkie (mac)
be891235d7 EEM: extend TRAVIS scroll bar code for EEM2 (mac)
b141102aaf EEM: review and improved case workflow for EEM1 (mac)
1710907c2d EEM: allow to come back to the map from sites in EEM1 (mac)
cee1993fd4 EEM: avoid busy waiting when playing music in EEM1 (mac)
a67c973f3b EEM: improved mouse pointer rendering for both release of EEM1 (mac)
b5d5892fb4 EEM: enabled restored content for EEM1 (mac cd)


Commit: 6b343086b888019d45d998021a57ac6f590a357c
    https://github.com/scummvm/scummvm/commit/6b343086b888019d45d998021a57ac6f590a357c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-10T17:45:23+02:00

Commit Message:
EEM: initial support for talkie release of eem1 (mac)

Changed paths:
    engines/eem/audio.cpp
    engines/eem/audio.h
    engines/eem/detection.cpp
    engines/eem/eem.cpp
    engines/eem/installer.cpp
    engines/eem/installer.h
    engines/eem/music.cpp
    engines/eem/mystery.cpp
    engines/eem/mystery.h
    engines/eem/resource.cpp
    engines/eem/resource.h
    engines/eem/ui.cpp


diff --git a/engines/eem/audio.cpp b/engines/eem/audio.cpp
index 59fb5369aa2..aedff299acc 100644
--- a/engines/eem/audio.cpp
+++ b/engines/eem/audio.cpp
@@ -225,8 +225,10 @@ void AudioPlayer::cleanMysterySounds() {
 
 bool AudioPlayer::initMacMysterySounds(uint mysteryNum) {
 	const uint16 firstResourceId = 1001;
-	const Common::Path candidates[2] = {
+	const Common::Path candidates[] = {
+		Common::Path(Common::String::format("M%u.DBD", mysteryNum)),
 		Common::Path(Common::String::format("M%02u.DBD", mysteryNum)),
+		Common::Path(Common::String::format("M%u.CPD", mysteryNum)),
 		Common::Path(Common::String::format("M%02u.CPD", mysteryNum))
 	};
 
@@ -259,6 +261,9 @@ void AudioPlayer::playMacSnd(uint16 resourceId, Audio::SoundHandle &handle,
 	Common::SeekableReadStream *stream =
 		openMacResource(Common::Path("EEM Sound&Music"),
 						MKTAG('s', 'n', 'd', ' '), resourceId);
+	if (!stream)
+		stream = openMacResource(Common::Path("Eagle Eye Mysteries CD"),
+								 MKTAG('s', 'n', 'd', ' '), resourceId);
 	if (!stream) {
 		warning("AudioPlayer: Mac snd resource %u missing", resourceId);
 		return;
diff --git a/engines/eem/audio.h b/engines/eem/audio.h
index e2f8778c97e..b6c64ecb594 100644
--- a/engines/eem/audio.h
+++ b/engines/eem/audio.h
@@ -44,7 +44,8 @@ class EEMEngine;
  *   (_UncompressedSound @ 202f:03e6); otherwise PKWARE DCL Implode
  *   (_DeCompressSound @ 202f:02ad). Each blob starts with SB Time Constant +
  *   AIL block count, then 8-bit unsigned PCM.
- * - Mac mystery spool streams live in M%02d.DBD/M%02d.CPD resource forks.
+ * - Mac mystery spool streams live in M%d.DBD/M%d.CPD resource forks
+ *   (some releases use two-digit numbers).
  *   The executable tries compressed 'csnd' by resource id, then falls back to
  *   plain 'snd '; ids are 1001 + the zero-based mystery voice slot.
  *
@@ -86,7 +87,7 @@ public:
 	void playFloppyVoiceSlot(uint slot, uint partner);
 
 	/// Loads the current mystery voice bundle. DOS uses M%u.SDX/M%u.SDB;
-	/// Mac uses M%02u.DBD/M%02u.CPD 'csnd'/'snd ' resources.
+	/// Mac uses M%u.DBD/M%u.CPD 'csnd'/'snd ' resources.
 	bool initMysterySounds(uint mysteryNum);
 
 	/// _CleanMysterySounds @ 202f:05a5.
diff --git a/engines/eem/detection.cpp b/engines/eem/detection.cpp
index cc8c06edacb..4b36092e8ed 100644
--- a/engines/eem/detection.cpp
+++ b/engines/eem/detection.cpp
@@ -32,13 +32,13 @@ const PlainGameDescriptor eemGames[] = {
 	{ nullptr, nullptr }
 };
 
-// The EEM2 (London) Macintosh CD keeps its data in subfolders ("EEM2 CD" with
-// "Data Files"/"Mac Scripts", and the app in "EEM London CD"). Let the detector
-// descend into them; the DOS releases have no such folders and are unaffected.
+// Macintosh CDs keep data and scripts in separate subfolders.
 static const char *const directoryGlobs[] = {
+	"EEM CD",
 	"EEM2 CD",
 	"Data Files",
 	"Mac Scripts",
+	"Eagle Eye Mysteries CD",
 	"EEM London CD",
 	nullptr
 };
@@ -114,6 +114,16 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_TESTING,
 		GUI_OPTIONS_EEM_MAC
 	},
+	{
+		"eem",
+		"CD",
+		AD_ENTRY2s("pics.dbd", "d:abbef80edd830e0a85d7fdc42e6a419c", 3840029,
+				   "m0.bin",   "d:acd867b61ef36dd98f94bbf00cc5adb0", 12193),
+		Common::EN_ANY,
+		Common::kPlatformMacintosh,
+		ADGF_UNSTABLE,
+		GUI_OPTIONS_EEM_MAC
+	},
 	{
 		// Eagle Eye Mysteries in London
 		"eem2",
@@ -169,7 +179,7 @@ const DebugChannelDef debugFlagList[] = {
 class EEMMetaEngineDetection : public AdvancedMetaEngineDetection<ADGameDescription> {
 public:
 	EEMMetaEngineDetection() : AdvancedMetaEngineDetection(EEM::gameDescriptions, EEM::eemGames) {
-		// Reach the EEM2 Mac data nested under "EEM2 CD/Data Files" etc.
+		// Reach the Mac CD data and script folders.
 		_maxScanDepth = 3;
 		_directoryGlobs = EEM::directoryGlobs;
 	}
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 84b6edc5853..4ac0289d266 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -30,6 +30,7 @@
 #include "common/path.h"
 #include "common/system.h"
 #include "common/textconsole.h"
+#include "common/macresman.h"
 
 #include "engines/util.h"
 
@@ -431,26 +432,26 @@ Common::FSNode findChildDirectoryIgnoreCase(const Common::FSNode &dir,
 	return Common::FSNode();
 }
 
-void addMacLondonDirectoryIfPresent(const Common::FSNode &dir,
-									const char *archiveName,
-									int depth = 1) {
+void addMacDirectoryIfPresent(const Common::FSNode &dir,
+							 const char *archiveName, int depth = 1) {
 	if (dir.exists() && dir.isDirectory() &&
 		!SearchMan.hasArchive(archiveName))
 		SearchMan.addDirectory(archiveName, dir, 0, depth);
 }
 
-void addMacLondonSearchPathsFrom(const Common::FSNode &base,
-								 const char *archivePrefix) {
+void addMacCDSearchPathsFrom(const Common::FSNode &base,
+							 const char *archivePrefix, bool london) {
 	if (!base.exists() || !base.isDirectory())
 		return;
 
-	Common::FSNode cd = findChildDirectoryIgnoreCase(base, "EEM2 CD");
-	if (!cd.exists()) {
+	const char *cdName = london ? "EEM2 CD" : "EEM CD";
+	Common::FSNode cd = findChildDirectoryIgnoreCase(base, cdName);
+	if (!cd.exists() && london) {
 		Common::FSNode wrapper = findChildDirectoryIgnoreCase(base, "EEM_London");
 		if (wrapper.exists())
 			cd = findChildDirectoryIgnoreCase(wrapper, "EEM2 CD");
 	}
-	if (!cd.exists() && base.getName().equalsIgnoreCase("EEM2 CD"))
+	if (!cd.exists() && base.getName().equalsIgnoreCase(cdName))
 		cd = base;
 	if (!cd.exists()) {
 		const Common::String baseName = base.getName();
@@ -475,32 +476,33 @@ void addMacLondonSearchPathsFrom(const Common::FSNode &base,
 	const Common::FSNode scriptsDir = findChildDirectoryIgnoreCase(cd, "Mac Scripts");
 	const Common::FSNode animDir = findChildDirectoryIgnoreCase(cd, "Anim Files");
 
-	addMacLondonDirectoryIfPresent(dataDir, dataArchive.c_str(), 2);
-	addMacLondonDirectoryIfPresent(scriptsDir, scriptsArchive.c_str(), 3);
-	addMacLondonDirectoryIfPresent(animDir, animArchive.c_str(), 2);
+	addMacDirectoryIfPresent(dataDir, dataArchive.c_str(), 2);
+	addMacDirectoryIfPresent(scriptsDir, scriptsArchive.c_str(), 3);
+	addMacDirectoryIfPresent(animDir, animArchive.c_str(), 2);
 
 	if (scriptsDir.exists()) {
 		Common::String approachesArchive = Common::String::format(
 			"%s-approaches", archivePrefix);
-		addMacLondonDirectoryIfPresent(
+		addMacDirectoryIfPresent(
 			findChildDirectoryIgnoreCase(scriptsDir, "Approaches"),
 			approachesArchive.c_str());
 	}
 
 	const Common::FSNode app =
-		findChildDirectoryIgnoreCase(cd.getParent(), "EEM London CD");
-	addMacLondonDirectoryIfPresent(app, appArchive.c_str(), 2);
+		findChildDirectoryIgnoreCase(cd.getParent(),
+			london ? "EEM London CD" : "Eagle Eye Mysteries CD");
+	addMacDirectoryIfPresent(app, appArchive.c_str(), 2);
 }
 
 static bool loadMacFontResource(EEMFont &font, uint16 resourceId, int size) {
 	addMacResourceSearchPaths();
 
-	// The Eagle Eye fonts live in the game application's resource fork. EEM1 Mac
-	// ships it as "Eagle Eye Mysteries"; EEM2 (London) Mac ships it as
-	// "EEM London CD" but reuses the same FONT resource ids (3214/3209).
+	// The Mac applications share FONT resource IDs 3214/3209.
 	static const char *const kAppForks[] = {
 		"Eagle Eye Mysteries",
 		"rsrc/Eagle Eye Mysteries",
+		"Eagle Eye Mysteries CD",
+		"rsrc/Eagle Eye Mysteries CD",
 		"EEM London CD",
 	};
 	for (uint i = 0; i < ARRAYSIZE(kAppForks); i++) {
@@ -927,40 +929,22 @@ void EEMEngine::setSiteHotspotCursorId(int cursorId) {
 bool EEMEngine::openArchives() {
 	const bool mac = isMacintosh();
 
-	// EEM2 (London) Mac is played straight from the CD, whose data lives in
-	// subfolders ("Data Files", "Mac Scripts") with the Mac app in "EEM London
-	// CD". Register them so the bare-name opens below -- and the later script,
-	// mystery and palette loaders -- resolve whether the user points ScummVM at
-	// the disc root or at the "EEM2 CD" folder.
-	if (mac && isLondon()) {
+	// Accept either the disc root or its data folder.
+	if (mac) {
 		const Common::FSNode gameDir(ConfMan.getPath("path"));
-		addMacLondonSearchPathsFrom(gameDir, "eem-london-game");
-		addMacLondonSearchPathsFrom(gameDir.getParent(), "eem-london-parent");
-		// Disc-root layout (recommended -- this also reaches the "EEM London
-		// CD" app that holds the Mac fonts/sound). The "/"-separated names
-		// descend two levels (see Common::addSubDirectoryMatching).
-		SearchMan.addSubDirectoryMatching(gameDir, "EEM2 CD/Data Files", 0, 2);
-		SearchMan.addSubDirectoryMatching(gameDir, "EEM2 CD/Mac Scripts", 0, 3);
-		SearchMan.addSubDirectoryMatching(gameDir, "EEM2 CD/Anim Files", 0, 2);
-		SearchMan.addSubDirectoryMatching(gameDir, "EEM London CD", 0, 2);
-		// ...or the user pointed ScummVM straight at the "EEM2 CD" folder.
-		SearchMan.addSubDirectoryMatching(gameDir, "Data Files", 0, 2);
-		SearchMan.addSubDirectoryMatching(gameDir, "Mac Scripts", 0, 3);
-		SearchMan.addSubDirectoryMatching(gameDir, "Anim Files", 0, 2);
-	}
-
-	// The EEM1 Mac release can be played straight from its floppy installer.
-	// When those files are present, mount a virtual archive that decompresses
-	// the game data (PICS.DBD, MysteryData, fonts, ...) on demand, so the opens
-	// below and the Mac resource-fork lookups resolve transparently. (EEM2 Mac
-	// ships loose on the CD and has no such installer.)
+		addMacCDSearchPathsFrom(gameDir, "eem-mac-game", isLondon());
+		addMacCDSearchPathsFrom(gameDir.getParent(), "eem-mac-parent", isLondon());
+	}
+
+	// EEM1 installers provide the application resources, and floppy game data.
 	if (mac && !isLondon() && !SearchMan.hasArchive("eem-installer")) {
 		const Common::FSNode gameDir(ConfMan.getPath("path"));
-		if (Common::Archive *installer = createInstallerArchive(gameDir)) {
+		Common::Archive *installer = createInstallerArchive(gameDir);
+		if (!installer)
+			installer = createInstallerArchive(gameDir.getParent());
+		if (installer) {
 			SearchMan.add("eem-installer", installer);
 			debugC(1, kDebugGeneral, "Mounted Eagle Eye Mysteries Mac installer archive");
-		} else {
-			warning("EEMTEST: createInstallerArchive returned null");
 		}
 	}
 
@@ -985,7 +969,7 @@ bool EEMEngine::loadSitePalettes() {
 	Common::File f;
 	// EEM2 DOS uses "SITEPALS." (8.3); EEM1 and both Mac releases use "SITEPALS".
 	const char *palFile = (isLondon() && !isMacintosh()) ? "SITEPALS." : "SITEPALS";
-	if (!f.open(Common::Path(palFile))) {
+	if (!openDataFile(f, Common::Path(palFile))) {
 		warning("%s missing", palFile);
 		return false;
 	}
@@ -1066,10 +1050,13 @@ void EEMEngine::interruptAudio(bool stopMusicToo) {
 void EEMEngine::playFlc(const Common::Path &path, bool fadeIn,
 						bool holdLastFrame) {
 	Video::FlicDecoder flic;
-	if (!flic.loadFile(path)) {
+	Common::ScopedPtr<Common::SeekableReadStream> stream(
+		Common::MacResManager::openFileOrDataFork(path));
+	if (!stream || !flic.loadStream(stream.get())) {
 		warning("playFlc: %s missing", path.toString().c_str());
 		return;
 	}
+	stream.release();
 
 	const int fw = flic.getWidth();
 	const int fh = flic.getHeight();
@@ -1757,7 +1744,7 @@ void EEMEngine::showLondonLogo(uint picId, uint palId, uint holdMs,
 }
 
 bool EEMEngine::startLondonTrainingMystery() {
-	if (_mystery.load(0, &_rng, isMacintosh())) {
+	if (_mystery.load(0, &_rng, isMacintosh(), isLondon())) {
 		resetSiteArrivalState();
 		if (_audio)
 			_audio->initMysterySounds(0);
@@ -2278,7 +2265,7 @@ Common::Error EEMEngine::loadGameStream(Common::SeekableReadStream *stream) {
 	if (hasMystery) {
 		uint16 mysteryNum = 0;
 		s.syncAsUint16LE(mysteryNum);
-		if (!_mystery.load(mysteryNum, &_rng, isMacintosh())) {
+		if (!_mystery.load(mysteryNum, &_rng, isMacintosh(), isLondon())) {
 			_mystery.clear();
 			resetSiteArrivalState();
 			return Common::kReadingFailed;
diff --git a/engines/eem/installer.cpp b/engines/eem/installer.cpp
index a0eeea28b00..3d34a20c233 100644
--- a/engines/eem/installer.cpp
+++ b/engines/eem/installer.cpp
@@ -381,12 +381,14 @@ private:
 };
 
 bool InstallerArchive::load(const Common::FSNode &dir) {
-	// disk 1 first, then the five continuation segments, concatenated into one
-	// logical stream (payload spans cross floppy boundaries).
+	// The CD installer contains only the application; floppy data spans six disks.
+	const bool cd = dir.getChild("Eagle Eye CD Installer").exists();
 	Common::Array<Common::String> names;
-	names.push_back("Eagle Eye Installer");
-	for (int i = 2; i <= 6; i++)
-		names.push_back(Common::String::format("EEM Install Data %d", i));
+	names.push_back(cd ? "Eagle Eye CD Installer" : "Eagle Eye Installer");
+	if (!cd) {
+		for (int i = 2; i <= 6; i++)
+			names.push_back(Common::String::format("EEM Install Data %d", i));
+	}
 
 	_diskBase.resize(names.size() + 1); // 1-based; index 0 unused
 	for (uint i = 0; i < _diskBase.size(); i++)
diff --git a/engines/eem/installer.h b/engines/eem/installer.h
index 69a983f6c50..4a2b6472e19 100644
--- a/engines/eem/installer.h
+++ b/engines/eem/installer.h
@@ -47,6 +47,9 @@ namespace EEM {
 // members; the application and "EEM Sound&Music" resource forks are exposed as
 // Mac resource-fork alt streams so Common::MacResManager can read them.
 //
+// "Eagle Eye CD Installer" uses the same format for the CD application;
+// its game data and mystery scripts are stored separately on the disc.
+//
 // Returns nullptr when the installer files are absent or cannot be parsed.
 Common::Archive *createInstallerArchive(const Common::FSNode &dir);
 
diff --git a/engines/eem/music.cpp b/engines/eem/music.cpp
index 3db172c6ece..aa4674aadc0 100644
--- a/engines/eem/music.cpp
+++ b/engines/eem/music.cpp
@@ -82,7 +82,7 @@ static Common::SeekableReadStream *expandMacMidiRunningStatus(Common::SeekableRe
 		output.writeUint32BE(size);
 
 		if (tag != MKTAG('M', 'T', 'r', 'k')) {
-			if (output.writeStream(&stream, size) != size)
+			if (size && output.writeStream(&stream, size) != size)
 				return nullptr;
 			continue;
 		}
@@ -120,7 +120,8 @@ static Common::SeekableReadStream *expandMacMidiRunningStatus(Common::SeekableRe
 				return nullptr;
 			}
 
-			if (dataSize > end - stream.pos() || output.writeStream(&stream, dataSize) != dataSize)
+			if (dataSize > end - stream.pos() ||
+				(dataSize && output.writeStream(&stream, dataSize) != dataSize))
 				return nullptr;
 		}
 		WRITE_BE_UINT32(output.getData() + sizeOffset, output.size() - sizeOffset - 4);
@@ -136,8 +137,14 @@ Common::SeekableReadStream *MusicPlayer::getResource(uint16 id, uint32 type) {
 	static const char *const kMacMusicForks[] = {
 		"EEM Sound&Music",
 		"rsrc/EEM Sound&Music",
+		"Eagle Eye Mysteries CD",
+		"rsrc/Eagle Eye Mysteries CD",
+		nullptr
+	};
+	static const char *const kMacLondonMusicForks[] = {
 		"EEM London CD",
 		"rsrc/EEM London CD",
+		nullptr
 	};
 	static const uint32 kMacMidiTypes[] = {
 		MKTAG('c', 'm', 'i', 'd'),
@@ -145,9 +152,9 @@ Common::SeekableReadStream *MusicPlayer::getResource(uint16 id, uint32 type) {
 		MKTAG('M', 'i', 'd', 'i'),
 	};
 
-	const uint firstFork = _isLondon ? 2 : 0;
-	for (uint i = firstFork; i < firstFork + 2; i++) {
-		const Common::Path path(kMacMusicForks[i]);
+	const char *const *forks = _isLondon ? kMacLondonMusicForks : kMacMusicForks;
+	for (uint i = 0; forks[i]; i++) {
+		const Common::Path path(forks[i]);
 		if (type == MKTAG('M', 'I', 'D', 'I') || type == MKTAG('M', 'i', 'd', 'i')) {
 			for (uint j = 0; j < ARRAYSIZE(kMacMidiTypes); j++) {
 				Common::SeekableReadStream *stream = openMacResource(path, kMacMidiTypes[j], id);
diff --git a/engines/eem/mystery.cpp b/engines/eem/mystery.cpp
index a2cc7a3a996..45a77fb3ffc 100644
--- a/engines/eem/mystery.cpp
+++ b/engines/eem/mystery.cpp
@@ -27,6 +27,7 @@
 
 #include "eem/detection.h"
 #include "eem/mystery.h"
+#include "eem/resource.h"
 
 namespace EEM {
 
@@ -49,7 +50,7 @@ static bool loadMacMysteryBlob(uint num, Common::Array<byte> &out) {
 		return false;
 
 	Common::File f;
-	if (!f.open(Common::Path("MysteryData"))) {
+	if (!openDataFile(f, Common::Path("MysteryData"))) {
 		warning("Mystery::load: cannot open MysteryData");
 		return false;
 	}
@@ -91,7 +92,7 @@ static bool loadLooseMacMysteryBlob(uint num, Common::Array<byte> &out,
 	for (uint i = 0; i < ARRAYSIZE(kPatterns); i++) {
 		Common::String candidate = Common::String::format(kPatterns[i], num);
 		Common::File f;
-		if (!f.open(Common::Path(candidate)))
+		if (!openDataFile(f, Common::Path(candidate)))
 			continue;
 
 		const int32 size = f.size();
@@ -229,7 +230,7 @@ static void normalizeMacMystery(Common::Array<byte> &data) {
 	// as big-endian for Mac.
 }
 
-static void normalizeMacLondonMystery(Common::Array<byte> &data) {
+static void normalizeMacCDMystery(Common::Array<byte> &data) {
 	if (data.size() <= 0x3e)
 		return;
 
@@ -237,13 +238,12 @@ static void normalizeMacLondonMystery(Common::Array<byte> &data) {
 	for (uint i = 0; i < ARRAYSIZE(section); i++)
 		section[i] = readBE16(data, i * 2);
 
-	// Loose EEM2 Mac scripts use the DOS London section/count header with
-	// big-endian words. The structured regions before the text block are
-	// CD-style clue/map/site records; strings at section[4] stay byte-exact.
+	// Mac CD scripts use the DOS CD layout with big-endian words.
+	// Strings at section[4] stay byte-exact.
 	swapU16Range(data, 0, section[0]);
 	swapU16Range(data, section[0], section[4]);
 
-	swapU16Range(data, section[5], section[6]); // 2-byte London NoteIndex
+	swapU16Range(data, section[5], section[6]); // NoteIndex
 	swapU16Range(data, section[6], section[7]); // CD-style GalleryData
 	swapU16Range(data, section[7], section[8]); // KDTextIndex
 	swapU16Range(data, section[8], section[9]); // CD-style solved clue block
@@ -267,6 +267,7 @@ void Mystery::clear() {
 	_isFloppy = false;
 	_isMacintosh = false;
 	_isMacintoshLooseScripts = false;
+	_isLondon = false;
 	_floppySuspectsOff = _floppyHintBlockOff = _floppyNoteIndexOff = 0;
 	_floppyGalleryOff = _floppyTextOff = _floppyKDTextOff = 0;
 	_floppySolvedOff = 0;
@@ -292,7 +293,7 @@ void Mystery::clear() {
 	memset(_siteReturnStack, 0, sizeof(_siteReturnStack));
 }
 
-bool Mystery::load(uint num, Common::RandomSource *rng, bool macintosh) {
+bool Mystery::load(uint num, Common::RandomSource *rng, bool macintosh, bool london) {
 	Common::String fname = Common::String::format("M%u.BIN", num);
 	Common::Array<byte> staging;
 	bool macLooseScripts = false;
@@ -300,7 +301,7 @@ bool Mystery::load(uint num, Common::RandomSource *rng, bool macintosh) {
 	if (macintosh) {
 		if (loadLooseMacMysteryBlob(num, staging, fname)) {
 			macLooseScripts = true;
-			normalizeMacLondonMystery(staging);
+			normalizeMacCDMystery(staging);
 		} else {
 			if (!loadMacMysteryBlob(num, staging))
 				return false;
@@ -335,6 +336,7 @@ bool Mystery::load(uint num, Common::RandomSource *rng, bool macintosh) {
 	_isFloppy = false;
 	_isMacintosh = false;
 	_isMacintoshLooseScripts = false;
+	_isLondon = london;
 
 	if (macintosh) {
 		_isMacintosh = true;
@@ -695,7 +697,7 @@ const byte *Mystery::noteIndex() const {
 uint16 Mystery::noteIndexCount() const {
 	if (!isLoaded())
 		return 0;
-	if (_isMacintoshLooseScripts) {
+	if (_isLondon) {
 		if (_galleryOffset <= _noteOffset)
 			return 0;
 		return (uint16)((_galleryOffset - _noteOffset) / 2);
@@ -703,10 +705,10 @@ 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)
-	const uint endOffset = _isMacintosh ? _textOffset : _galleryOffset;
+	const uint endOffset = usesCompactMacData() ? _textOffset : _galleryOffset;
 	if (endOffset <= _noteOffset)
 		return 0;
-	const uint stride = _isMacintosh ? 8 : (_isFloppy ? 7 : 4);
+	const uint stride = usesCompactMacData() ? 8 : (_isFloppy ? 7 : 4);
 	return (uint16)((endOffset - _noteOffset) / stride);
 }
 
@@ -728,10 +730,10 @@ bool Mystery::noteHasNotebookText(uint clueId) const {
 	if (!ni || clueId >= cnt)
 		return false;
 
-	if (_isMacintoshLooseScripts)
+	if (_isLondon)
 		return READ_LE_UINT16(ni + clueId * 2) != 0;
 
-	if (_isMacintosh)
+	if (usesCompactMacData())
 		return READ_LE_UINT16(ni + clueId * 8) != 0;
 
 	if (!_isFloppy)
@@ -853,7 +855,7 @@ const byte *Mystery::solvedClueBlock() const {
 }
 
 int Mystery::selectedPoints() const {
-	if (_isMacintoshLooseScripts)
+	if (_isLondon)
 		return 0;
 
 	const byte *ni = noteIndex();
@@ -896,7 +898,7 @@ int Mystery::selectedPoints() const {
 			continue;
 		// CD NoteIndex entry: 4 bytes = u16 textOff + u16 points (at +2).
 		// Mac rows are 8 bytes: notebook text, Jake text, Jenny text, points.
-		const uint16 pts = _isMacintosh ? READ_LE_UINT16(ni + i * 8 + 6)
+		const uint16 pts = usesCompactMacData() ? READ_LE_UINT16(ni + i * 8 + 6)
 										: READ_LE_UINT16(ni + i * 4 + 2);
 		total += (int)(int16)pts;
 	}
@@ -957,7 +959,7 @@ int Mystery::minCluesRemaining() const {
 }
 
 int Mystery::foundPoints() const {
-	if (_isMacintoshLooseScripts)
+	if (_isLondon)
 		return 0;
 
 	const byte *ni = noteIndex();
@@ -973,7 +975,7 @@ int Mystery::foundPoints() const {
 			continue;
 		const uint16 pts = _isFloppy
 			? ni[i * 7 + 6]
-			: (_isMacintosh ? READ_LE_UINT16(ni + i * 8 + 6)
+			: (usesCompactMacData() ? 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 883f9edbcd6..e9401fb525a 100644
--- a/engines/eem/mystery.h
+++ b/engines/eem/mystery.h
@@ -59,10 +59,9 @@ public:
 	Mystery() = default;
 	~Mystery() = default;
 
-	/// Load M<num>.BIN and reset per-mystery state. The Mac release stores
-	/// cases in the indexed MysteryData container instead of loose files.
+	/// Load M<num>.BIN, or MysteryData for Mac floppy, and reset mystery state.
 	bool load(uint num, class Common::RandomSource *rng = nullptr,
-			  bool macintosh = false);
+			  bool macintosh = false, bool london = false);
 
 	void clear();
 
@@ -246,6 +245,7 @@ private:
 	bool   _isFloppy = false;
 	bool   _isMacintosh = false;
 	bool   _isMacintoshLooseScripts = false;
+	bool   _isLondon = 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/resource.cpp b/engines/eem/resource.cpp
index f61492637af..e33f0353283 100644
--- a/engines/eem/resource.cpp
+++ b/engines/eem/resource.cpp
@@ -35,6 +35,10 @@
 
 namespace EEM {
 
+bool openDataFile(Common::File &file, const Common::Path &path) {
+	return file.open(Common::MacResManager::openFileOrDataFork(path), path.toString());
+}
+
 DBDArchive::DBDArchive() {
 }
 
@@ -221,13 +225,13 @@ bool DBDArchive::open(const Common::Path &dbdName, const Common::Path &dbxName,
 	close();
 	_bigEndian = bigEndian;
 
-	if (!_dbd.open(dbdName)) {
+	if (!openDataFile(_dbd, dbdName)) {
 		warning("DBDArchive: cannot open %s", dbdName.toString().c_str());
 		return false;
 	}
 
 	Common::File dbx;
-	if (!dbx.open(dbxName)) {
+	if (!openDataFile(dbx, dbxName)) {
 		warning("DBDArchive: cannot open %s", dbxName.toString().c_str());
 		_dbd.close();
 		return false;
diff --git a/engines/eem/resource.h b/engines/eem/resource.h
index 0e4567926fa..e5c7c5e2a8a 100644
--- a/engines/eem/resource.h
+++ b/engines/eem/resource.h
@@ -35,6 +35,8 @@ class SeekableReadStream;
 
 namespace EEM {
 
+bool openDataFile(Common::File &file, const Common::Path &path);
+
 struct DBEntry {
 	uint32 offset;     ///< Byte offset in the .DBD file.
 	uint16 compressed; ///< Non-zero = PKWARE DCL ("Implode") packed payload.
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 97e460009d6..dcef8c18198 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -63,7 +63,7 @@ static bool openNumberedScriptFile(Common::File &f, Common::String &name,
 								   uint patternCount) {
 	for (uint i = 0; i < patternCount; i++) {
 		name = Common::String::format(patterns[i], num);
-		if (f.open(Common::Path(name)))
+		if (openDataFile(f, Common::Path(name)))
 			return true;
 	}
 	name.clear();
@@ -542,7 +542,7 @@ bool loadMacEndingBlob(uint num, Common::Array<byte> &out, bool &looseScript) {
 		return false;
 
 	Common::File f;
-	if (!f.open(Common::Path("MysteryData"))) {
+	if (!openDataFile(f, Common::Path("MysteryData"))) {
 		warning("doShowEnding: cannot open MysteryData");
 		return false;
 	}
@@ -1814,7 +1814,7 @@ void EEMEngine::doSetup() {
 			if (kNewCaseBtn.contains(mx, my)) {
 				saveProfile(_playerName);
 				if (isDemo()) {
-					if (_mystery.load(0, &_rng, isMacintosh())) {
+					if (_mystery.load(0, &_rng, isMacintosh(), isLondon())) {
 						resetSiteArrivalState();
 						_nextScreen = kScreenInitClues;
 					} else {
@@ -2391,7 +2391,7 @@ void EEMEngine::doActionScreen() {
 	}
 
 	if (pick == kPickPractice) {
-		if (!_mystery.load(0, &_rng, isMacintosh())) {
+		if (!_mystery.load(0, &_rng, isMacintosh(), isLondon())) {
 			warning("doActionScreen: failed to load practice mystery");
 			_mystery.clear();
 			resetSiteArrivalState();
@@ -2643,7 +2643,7 @@ void EEMEngine::doCaseSelection() {
 		return;
 
 	const uint mn = stageLo + selRow;
-	if (!_mystery.load(mn, &_rng, isMacintosh())) {
+	if (!_mystery.load(mn, &_rng, isMacintosh(), isLondon())) {
 		warning("doCaseSelection: failed to load mystery %u", mn);
 		_mystery.clear();
 		return;
@@ -2875,7 +2875,7 @@ Common::String EEMEngine::notebookNoteText(uint clueId, const byte *ni,
 		return parseString(Common::String(p, len),
 						   _playerName, _partner);
 	}
-	if (isMacintosh() && !isLondon() && bufBase) {
+	if (_mystery.usesCompactMacData() && bufBase) {
 		const uint16 textOff = READ_LE_UINT16(ni + clueId * 8);
 		if (textOff == 0 || textOff >= mysSz)
 			return Common::String();
@@ -3570,9 +3570,7 @@ Common::String EEMEngine::accuseNoteText(uint clueId,
 			(const char *)(ctx.bufBaseNotes + textOff),
 			_playerName, _partner);
 	}
-	// Only the compact EEM1 Mac mystery data uses 8-byte NoteIndex records.
-	// EEM2 London Mac loose scripts keep the London 2-byte table.
-	if (isMacintosh() && !isLondon() && ctx.bufBaseNotes) {
+	if (_mystery.usesCompactMacData() && ctx.bufBaseNotes) {
 		const uint16 textOff = READ_LE_UINT16(ctx.ni + clueId * 8);
 		if (textOff == 0 || textOff >= _mystery.dataSize())
 			return Common::String();


Commit: 60dcce8a22a0be7d8585a770436a8a5e5200845c
    https://github.com/scummvm/scummvm/commit/60dcce8a22a0be7d8585a770436a8a5e5200845c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-10T17:45:23+02:00

Commit Message:
EEM: show intro for eem1 talkie (mac)

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


diff --git a/engines/eem/audio.cpp b/engines/eem/audio.cpp
index aedff299acc..961bc84c0f4 100644
--- a/engines/eem/audio.cpp
+++ b/engines/eem/audio.cpp
@@ -49,6 +49,7 @@ struct MacSndResource {
 };
 
 const MacSndResource kMacSndResources[] = {
+	{ "JAKE",     8001 }, { "JEN",      8002 },
 	{ "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 },
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index bf7b09e54a2..78da5855fd7 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -362,7 +362,7 @@ void EEMEngine::doChoosePartner() {
 	}
 
 	if (_audio && !isDemo()) {
-		if (isFloppy() || isMacintosh()) {
+		if (isFloppy() || (isMacintosh() && !isMacCD())) {
 			// Floppy _DoChoosePartner_Floppy @ 19bb:0a8e 
 			_audio->playFloppyVoiceSlot(0x14, _partner);
 		} else {
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 4ac0289d266..e3141083f3a 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -295,7 +295,8 @@ EEMEngine::EEMEngine(OSystem *syst, const ADGameDescription *gameDesc)
 		Common::String(gameDesc->extra).contains("Floppy"))
 		_variant = kVariantFloppy;
 	if (gameDesc && gameDesc->platform == Common::kPlatformMacintosh)
-		_variant = kVariantMac;
+		_variant = gameDesc->extra && Common::String(gameDesc->extra).contains("CD")
+			? kVariantMacCD : kVariantMac;
 	if (gameDesc && gameDesc->gameId &&
 		Common::String(gameDesc->gameId) == "eem2")
 		_variant = kVariantLondonCD;
@@ -1644,6 +1645,64 @@ void EEMEngine::showMacTitleIntro() {
 	waitIntroDelay(0xFFFFFFFFu);
 }
 
+void EEMEngine::playMacCDIntro() {
+	// CODE 2:5884 uses one-based frame numbers and pauses for each line.
+	static const uint16 kVoiceFrames[] = {
+		11, 15, 17, 19, 63, 64, 97, 98, 103, 104,
+		105, 125, 126, 127, 219, 220, 311, 312, 313
+	};
+	const uint32 kFrameDelayMs = 9 * 1000 / 60;
+	Video::FlicDecoder flic;
+	Common::ScopedPtr<Common::SeekableReadStream> stream(
+		Common::MacResManager::openFileOrDataFork(Common::Path("FIN07.FLC")));
+	if (!stream || !flic.loadStream(stream.get())) {
+		warning("Mac CD intro FIN07.FLC failed to load");
+		return;
+	}
+	stream.release();
+
+	if (_audio)
+		_audio->initMysterySounds(60);
+	if (_music)
+		_music->playFile(Common::Path("THEME.XMI"), false);
+
+	uint cue = 0;
+	bool aborted = false;
+	uint32 lastFrameMs = g_system->getMillis();
+	flic.start();
+	while (!flic.endOfVideo() && !shouldQuit() && !aborted && !_skipIntro) {
+		const Graphics::Surface *frame = flic.decodeNextFrame();
+		if (!frame)
+			break;
+		// The Mac player ignores the FLC header's frame delay.
+		const uint32 elapsed = g_system->getMillis() - lastFrameMs;
+		if (waitIntroDelay(elapsed < kFrameDelayMs ? kFrameDelayMs - elapsed : 1))
+			break;
+		g_system->copyRectToScreen(frame->getPixels(), frame->pitch, 0, 0,
+			MIN<int>(frame->w, screenWidth()), MIN<int>(frame->h, screenHeight()));
+		if (flic.hasDirtyPalette())
+			g_system->getPaletteManager()->setPalette(flic.getPalette(), 0, 256);
+		g_system->updateScreen();
+		lastFrameMs = g_system->getMillis();
+
+		const uint frameNumber = flic.getCurFrame() + 1;
+		if (frameNumber == 219 && _music)
+			_music->playFile(Common::Path("THEME.XMI"), false);
+		if (cue < ARRAYSIZE(kVoiceFrames) && frameNumber == kVoiceFrames[cue]) {
+			if (_audio) {
+				_audio->spoolSound(cue);
+				while (_audio->isSpoolPlaying() && !shouldQuit() && !aborted)
+					aborted = waitIntroDelay(10);
+				_audio->stopSpool();
+			}
+			cue++;
+		}
+	}
+	if (_audio)
+		_audio->cleanMysterySounds();
+	fadeCurrentPaletteToBlack();
+}
+
 void EEMEngine::runMacStartup() {
 	CursorMan.showMouse(false);
 	_skipIntro = false;
@@ -1658,6 +1717,8 @@ void EEMEngine::runMacStartup() {
 		showMacStillLogo(kPicStormLogo, kPalStormLogo, 3000,
 						 /* playThunder= */ true);
 
+	if (!shouldQuit() && !_skipIntro && isMacCD())
+		playMacCDIntro();
 	if (!shouldQuit() && !_skipIntro && _music)
 		_music->playFile(Common::Path("THEME.XMI"), /* loop= */ true);
 	if (!shouldQuit() && !_skipIntro)
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 1c827f3af39..5fa869d62a1 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -102,6 +102,7 @@ enum Variant {
 	kVariantFloppy   = 1,
 	kVariantLondonCD = 2,
 	kVariantMac      = 3,
+	kVariantMacCD    = 4,
 };
 
 /// `_Partner @ 29be:7918`. Selected at the partner-pick screen
@@ -132,6 +133,7 @@ public:
 	Variant getVariant() const { return _variant; }
 	bool isFloppy() const { return _variant == kVariantFloppy || isDemo(); }
 	bool isLondon() const { return _variant == kVariantLondonCD; }
+	bool isMacCD() const { return _variant == kVariantMacCD; }
 	// London (game) and Macintosh (platform) are orthogonal -- the London CD
 	// shipped for both DOS and Mac -- so derive Mac-ness from the platform
 	// rather than the single-valued `_variant` (which can only hold one of
@@ -496,6 +498,7 @@ private:
 
 	bool waitIntroDelay(uint32 maxMs);
 	void runMacStartup();
+	void playMacCDIntro();
 	void showMacEAKidsLogo();
 	void showMacStillLogo(uint picId, uint palId, uint holdMs,
 						  bool playThunder);
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 8bf6a1aa982..b686d9b8b66 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -301,6 +301,15 @@ const uint16 kWaitAnims[7][6] = {
 	{ 0x06, 0x06, 0x06, 0x06, 0x50, 0x50 }, // 6
 };
 
+// EEM1 Mac CD, CODE 7:5318, A5-0x30b2.
+const uint16 kMacWaitAnims[5][6] = {
+	{ 0x00, 0x0a, 7, 7, 153, 153 },
+	{ 0x03, 0x0c, 7, 7, 153, 153 },
+	{ 0x01, 0x0b, 7, 7, 153, 153 },
+	{ 0x04, 0x0d, 7, 7, 153, 153 },
+	{ 0x02, 0x10, 7, 7, 153, 153 },
+};
+
 const uint16 kKdAnimTable[6][6] = {
 	{ 0x03, 0x0c, 6, 6, 80, 80 }, // 0 — speaker idx 1 wait anim
 	{ 0x01, 0x0b, 6, 6, 80, 80 }, // 1 — same as PDA idle
@@ -1689,10 +1698,15 @@ bool SiteScreen::partnerIdleAnimParams(uint siteNum, uint16 &animId,
 	} else {
 		const uint16 speaker = READ_LE_UINT16(site + 8);
 		const uint16 (*waitTable)[6] = kWaitAnims;
-		if (_vm->isLondon())
+		uint tableSize = ARRAYSIZE(kWaitAnims);
+		if (_vm->isMacCD()) {
+			waitTable = kMacWaitAnims;
+			tableSize = ARRAYSIZE(kMacWaitAnims);
+		} else if (_vm->isLondon()) {
 			waitTable = _vm->isMacintosh()
 				? kMacWaitAnimsLondon : kWaitAnimsLondon;
-		if (speaker >= ARRAYSIZE(kWaitAnims))
+		}
+		if (speaker >= tableSize)
 			return false;
 		animId = waitTable[speaker][0 + partner];
 		x      = (int)(int16)waitTable[speaker][2 + partner];
@@ -2097,11 +2111,13 @@ void SiteScreen::onHotspotClicked(uint siteNum, uint hotIdx) {
 //        frees the slot and re-activates `WaitHandle`.
 bool EEMEngine::loadKdAnim(uint16 num, Animation &anim, int &px, int &py,
 						   uint16 &animId) {
-	if (num >= ARRAYSIZE(kKdAnimTable))
+	if (num >= (isMacCD() ? ARRAYSIZE(kMacWaitAnims) - 1 : ARRAYSIZE(kKdAnimTable)))
 		return false;
 
 	const uint16 (*kdTable)[6] = kKdAnimTable;
-	if (isLondon()) {
+	if (isMacCD()) {
+		kdTable = kMacWaitAnims + 1;
+	} else if (isLondon()) {
 		// EEM2 Mac FUN_0000ce6e indexes `_WaitAnims + 1`
 		// (`lea (-0x2034,A5)`), so KD gestures share the Mac idle anchors
 		// for speaker rows 1..6.


Commit: 96d27fdf90e4cf4aa381d09215e00ef1b62a4a2a
    https://github.com/scummvm/scummvm/commit/96d27fdf90e4cf4aa381d09215e00ef1b62a4a2a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-10T17:45:23+02:00

Commit Message:
EEM: better animations and precise map locations in EEM1 talkie (mac)

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


diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index e3141083f3a..ff2ba64a91e 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -300,7 +300,7 @@ EEMEngine::EEMEngine(OSystem *syst, const ADGameDescription *gameDesc)
 	if (gameDesc && gameDesc->gameId &&
 		Common::String(gameDesc->gameId) == "eem2")
 		_variant = kVariantLondonCD;
-	setLondonAnimScripts(isLondon());
+	setAnimScripts(isLondon(), isMacCD());
 	_language = gameDesc ? gameDesc->language : Common::EN_ANY;
 }
 
@@ -561,7 +561,7 @@ Common::Error EEMEngine::run() {
 	// _InitMIDI @ 20a2:013a. The demo ships no music. Mac releases use
 	// Halestorm song and instrument resources instead of loose DOS XMIDI files.
 	if (!isDemo())
-		_music = new MusicPlayer(isFloppy(), isMacintosh(), isLondon());
+		_music = new MusicPlayer(isFloppy(), isMacintosh(), isLondon(), isMacCD());
 
 	// _InitDrivers @ 1ff1:0368 (SBDIG.ADV / PASDIG.ADV).
 	_audio = new AudioPlayer(this);
@@ -2192,6 +2192,35 @@ void EEMEngine::startLondonTravelMusic(uint8 travelKind) {
 	_music->playMus(track, /* loop= */ false);
 }
 
+void EEMEngine::finishTravelMusic(bool skipped) {
+	// Mac CD fades after the entrance; DOS lets the travel tune finish.
+	if (isMacCD()) {
+		if (_music && _music->isPlaying() && !shouldQuit()) {
+			_music->fadeOut();
+			const uint32 startMs = g_system->getMillis();
+			while (_music->isFading() && !shouldQuit() &&
+				   g_system->getMillis() - startMs < 5000) {
+				Common::Event event;
+				while (g_system->getEventManager()->pollEvent(event)) {
+					if (event.type == Common::EVENT_QUIT ||
+						event.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+						stopMusic();
+						return;
+					}
+				}
+				g_system->updateScreen();
+				g_system->delayMillis(10);
+			}
+		}
+		stopMusic();
+	} else if (!isFloppy() && !isLondon()) {
+		if (skipped || shouldQuit())
+			stopMusic();
+		else
+			waitForMusicDone();
+	}
+}
+
 void EEMEngine::waitForMusicDone(uint32 maxMs) {
 	if (!_music)
 		return;
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 5fa869d62a1..741d6dae022 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -578,6 +578,7 @@ public:
 	/// `_StartTravelMusic @ 20a2:0595`. Picks `MUS%05d.XMI` from
 	/// `_mystery._siteNumber % 5`, one-shot.
 	void startTravelMusic();
+	void finishTravelMusic(bool skipped);
 
 	/// `_IsMIDIPlaying` spin + `_StopMIDI` cleanup in `_DoSiteLoop`.
 	void waitForMusicDone(uint32 maxMs = 60000);
diff --git a/engines/eem/map_ui.cpp b/engines/eem/map_ui.cpp
index b91d437f385..3a9c9a71a51 100644
--- a/engines/eem/map_ui.cpp
+++ b/engines/eem/map_ui.cpp
@@ -162,12 +162,12 @@ struct BigMapEntryInfo {
 	uint16 crime = 0;
 };
 
-bool readBigMapEntryInfo(const byte *entry, bool floppy, bool macintosh,
+bool readBigMapEntryInfo(const byte *entry, const EEMEngine &vm, bool compactMac,
 						 BigMapEntryInfo &out) {
 	if (!entry)
 		return false;
 
-	if (macintosh) {
+	if (compactMac) {
 		out.detailX   = READ_LE_UINT16(entry + 0x0);
 		out.detailY   = READ_LE_UINT16(entry + 0x2);
 		out.buttonId  = entry[0x5];
@@ -177,7 +177,7 @@ bool readBigMapEntryInfo(const byte *entry, bool floppy, bool macintosh,
 		return true;
 	}
 
-	if (floppy) {
+	if (vm.isFloppy()) {
 		out.detailX   = READ_LE_UINT16(entry + 0x0);
 		out.detailY   = READ_LE_UINT16(entry + 0x2);
 		out.buttonId  = entry[0x4];
@@ -193,6 +193,13 @@ bool readBigMapEntryInfo(const byte *entry, bool floppy, bool macintosh,
 	out.detailX   = READ_LE_UINT16(entry + 0x8);
 	out.detailY   = READ_LE_UINT16(entry + 0xa);
 	out.crime     = READ_LE_UINT16(entry + 0xc);
+	if (vm.isMacCD()) {
+		// Mac CD scales DOS map coordinates and truncates the result (CODE 6).
+		out.overviewX = out.overviewX * kMacScreenWidth / kScreenWidth;
+		out.overviewY = out.overviewY * kMacScreenHeight / kScreenHeight;
+		out.detailX = out.detailX * kMacScreenWidth / kScreenWidth;
+		out.detailY = out.detailY * kMacScreenHeight / kScreenHeight;
+	}
 	return true;
 }
 
@@ -613,8 +620,6 @@ bool EEMEngine::bigMapTrySelectDetailSite(int mouseX, int mouseY,
 		Common::Rect rect;
 	};
 	Common::Array<DetailMapHit> hits;
-	const bool floppyMap = _mystery.isLoaded() && isFloppy();
-	const bool macMap = isMacintosh() && _mystery.usesCompactMacData();
 	for (uint i = 0; i < _mystery.numSites(); i++) {
 		// On-map flag alone, matching `_SearchMapButtons`.
 		if (!_mystery._onSites[i])
@@ -623,7 +628,7 @@ bool EEMEngine::bigMapTrySelectDetailSite(int mouseX, int mouseY,
 		if (!entry)
 			continue;
 		BigMapEntryInfo info;
-		if (!readBigMapEntryInfo(entry, floppyMap, macMap, info))
+		if (!readBigMapEntryInfo(entry, *this, _mystery.usesCompactMacData(), info))
 			continue;
 
 		Picture button;
@@ -1257,10 +1262,8 @@ void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
 		if (!entry)
 			continue;
 
-		const bool floppy  = _mystery.isLoaded() && isFloppy();
 		BigMapEntryInfo info;
-		if (!readBigMapEntryInfo(entry, floppy,
-								 mac && _mystery.usesCompactMacData(), info))
+		if (!readBigMapEntryInfo(entry, *this, _mystery.usesCompactMacData(), info))
 			continue;
 		const bool isDone = (i < Mystery::kVisitedSiteCap)
 							 && _mystery._visitedSite[i];
@@ -1343,7 +1346,6 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
 	scratch.copyRectToSurface(mapPixels.data() + scrollY * mapW + scrollX,
 							  mapW, kMapWinX, kMapWinY, copyW, copyH);
 
-	const bool floppyMap = _mystery.isLoaded() && isFloppy();
 	for (uint i = 0; i < _mystery.numSites(); i++) {
 		// `_DrawBigMapButtons` gates markers on the on-map flag alone, never the
 		// current site: a sublocation (in-site jump, never flagged) must not draw.
@@ -1353,8 +1355,7 @@ void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
 		if (!entry)
 			continue;
 		BigMapEntryInfo info;
-		if (!readBigMapEntryInfo(entry, floppyMap,
-								 mac && _mystery.usesCompactMacData(), info))
+		if (!readBigMapEntryInfo(entry, *this, _mystery.usesCompactMacData(), info))
 			continue;
 		Picture button;
 		if (!_buttonArchive.loadEntry(info.buttonId, button))
diff --git a/engines/eem/music.cpp b/engines/eem/music.cpp
index aa4674aadc0..3413e4ea309 100644
--- a/engines/eem/music.cpp
+++ b/engines/eem/music.cpp
@@ -212,7 +212,9 @@ uint16 macSongResourceIdForFile(const Common::Path &path) {
 	return kInvalidMacSongResource;
 }
 
-uint16 macSongResourceIdForMus(uint num) {
+uint16 macSongResourceIdForMus(uint num, bool macCD) {
+	if (macCD && num < 5)
+		return 1002 + num;
 	static const uint16 kTravelTracks[5] = {
 		1004, 1002, 1003, 1006, 1005 // Travel-6/4/7/1/8
 	};
@@ -225,8 +227,9 @@ uint16 macSongResourceIdForMus(uint num) {
 	return kInvalidMacSongResource;
 }
 
-MusicPlayer::MusicPlayer(bool isFloppy, bool isMacintosh, bool isLondon) :
-	_isFloppy(isFloppy), _isMacintosh(isMacintosh), _isLondon(isLondon) {
+MusicPlayer::MusicPlayer(bool isFloppy, bool isMacintosh, bool isLondon, bool isMacCD) :
+	_isFloppy(isFloppy), _isMacintosh(isMacintosh), _isLondon(isLondon),
+	_isMacCD(isMacCD) {
 	if (_isMacintosh)
 		return;
 
@@ -301,6 +304,17 @@ bool MusicPlayer::isPlaying() const {
 	return Audio::MidiPlayer::isPlaying();
 }
 
+void MusicPlayer::fadeOut() {
+	// Mac CD CODE 6:2358 uses Halestorm fade speed 30.
+	if (_macDriver)
+		_macDriver->doCommand(Audio::HalestormDriver::kSongFadeOut, 30);
+}
+
+bool MusicPlayer::isFading() const {
+	return _macDriver && isPlaying() &&
+		_macDriver->doCommand(Audio::HalestormDriver::kSongFadeGetState) > 2;
+}
+
 void MusicPlayer::setVolume(int volume) {
 	if (_isMacintosh) {
 		_masterVolume = CLIP<int>(volume, 0, Audio::Mixer::kMaxMixerVolume);
@@ -414,10 +428,9 @@ void MusicPlayer::playFile(const Common::Path &xmiPath, bool loop) {
 
 void MusicPlayer::playMus(uint num, bool loop) {
 	if (_isMacintosh) {
-		// London SONG ids are 1000 + the MUS number; EEM1 uses named tracks.
 		const uint16 resourceId = _isLondon
 			? (num < kInvalidMacSongResource - 1000 ? 1000 + num : kInvalidMacSongResource)
-			: macSongResourceIdForMus(num);
+			: macSongResourceIdForMus(num, _isMacCD);
 		playMacSongResource(resourceId, loop);
 		return;
 	}
diff --git a/engines/eem/music.h b/engines/eem/music.h
index 8aea1e8486c..47edef8d6a1 100644
--- a/engines/eem/music.h
+++ b/engines/eem/music.h
@@ -52,7 +52,8 @@ namespace EEM {
  */
 class MusicPlayer : public Audio::MidiPlayer, private Audio::HalestormLoader {
 public:
-	explicit MusicPlayer(bool isFloppy = false, bool isMacintosh = false, bool isLondon = false);
+	explicit MusicPlayer(bool isFloppy = false, bool isMacintosh = false,
+						 bool isLondon = false, bool isMacCD = false);
 	~MusicPlayer() override;
 
 	/// _MIDIPlayFile @ 20a2:024c. loop=true mirrors
@@ -64,6 +65,8 @@ public:
 
 	void stop() override;
 	bool isPlaying() const;
+	void fadeOut();
+	bool isFading() const;
 	void setVolume(int volume) override;
 
 	// WORKAROUND: Miles drivers handle source-channel routing themselves;
@@ -79,6 +82,7 @@ private:
 	const bool _isFloppy;
 	const bool _isMacintosh;
 	const bool _isLondon;
+	const bool _isMacCD;
 	Common::Array<byte> _xmiData;
 	Audio::HalestormDriver *_macDriver = nullptr;
 };
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index b686d9b8b66..16e4f19bee9 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -28,6 +28,7 @@
 #include "common/system.h"
 #include "common/textconsole.h"
 
+#include "graphics/cursorman.h"
 #include "graphics/paletteman.h"
 
 #include "eem/audio.h"
@@ -274,7 +275,17 @@ void cyclePaletteRangeReverse(uint8 start, uint8 end) {
 	g_system->getPaletteManager()->setPalette(buf, start, count);
 }
 
-void applyHotspotGlowPalette() {
+void applyHotspotGlowPalette(bool macCD) {
+	if (macCD) {
+		// Mac CD CODE 2:3d5e, including the searched-hotspot color.
+		static const byte kMacGlow[7 * 3] = {
+			0x54, 0xFC, 0xFC, 0x38, 0xDC, 0xE4, 0x20, 0xBC, 0xD0,
+			0x0C, 0x9C, 0xBC, 0x20, 0xBC, 0xD0, 0x38, 0xDC, 0xE4,
+			0x0C, 0x9C, 0xBC
+		};
+		g_system->getPaletteManager()->setPalette(kMacGlow, 0xF8, 7);
+		return;
+	}
 	static const byte kAntsGlow[6 * 3] = {
 		0x40, 0x40, 0x00, // F9 — dim
 		0x80, 0x80, 0x00, // FA
@@ -361,7 +372,7 @@ const uint16 kMacWaitAnimsLondon[7][6] = {
 //
 // Repeated frames are the original's "frame-hold" mechanism: per-tick
 // walk advances exactly one entry, so K repeats hold the frame for
-// K * `kFramePeriodMs` ≈ K * 140 ms (e.g. [0,0,0,0,0,0,0,0,0,2] →
+// K * the frame period (e.g. [0,0,0,0,0,0,0,0,0,2] →
 // nine ticks of frame 0, one tick of frame 2 = "blink with long
 // idle hold"). Same scripts serve wait anims (looping) and kd-clue
 // reactions (state-4 one-shot — see `_PlayAnimation`); state field
@@ -647,6 +658,85 @@ const AnimScriptLong kAnimScriptsLong[] = {
 	{ 0x36, 60,  kScript36 },
 };
 
+// Mac CD animation scripts that differ from DOS (A5-0x4c26).
+const uint8 kMacCDScript1a[] = {
+	0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 9,
+	8, 7, 6, 5, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0
+};
+
+const uint8 kMacCDScript1c[] = {
+	0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
+	20
+};
+
+const uint8 kMacCDScript22[] = {
+	0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4,
+	5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 9,
+	10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13,
+	13, 14, 14, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17,
+	18, 18, 18, 18, 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22,
+	23, 23, 24, 24, 25, 25, 26, 26, 27, 27, 28, 28, 29, 29, 30, 30, 31, 31, 32, 32,
+	33, 33, 34, 34, 35, 35, 36, 36, 37, 37, 38, 38, 39, 39, 40, 40, 41, 41, 42, 42,
+	43, 43, 44, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+};
+
+const uint8 kMacCDScript25[] = {
+	0, 1, 2, 3, 4
+};
+
+const uint8 kMacCDScript2a[] = {
+	0, 1, 2, 2, 3, 4
+};
+
+const uint8 kMacCDScript2c[] = {
+	0, 1, 2, 3, 4, 5, 0, 6, 7, 8, 9, 10, 11, 12, 13, 13, 13, 13, 13, 13,
+	13, 13, 14, 14, 14, 14, 14, 14, 14, 13, 13, 13, 13, 13, 13, 15, 16, 17, 18, 19,
+	20, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0
+};
+
+const uint8 kMacCDScript30[] = {
+	0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 16, 16, 16,
+	16, 16, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33,
+	34, 16, 16, 16, 16, 16, 16, 16, 16, 16
+};
+
+const uint8 kMacCDScript33[] = {
+	0, 1, 2, 3, 4, 5, 6, 7
+};
+
+const uint8 kMacCDScript34[] = {
+	0, 1, 2, 3, 4, 5
+};
+
+const uint8 kMacCDScript35[] = {
+	0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16
+};
+
+const uint8 kMacCDScript36[] = {
+	0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 1, 2, 3, 4, 5,
+	6, 7, 8, 9, 10, 11, 12, 13, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4,
+	3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+};
+
+const AnimScriptLong kAnimScriptsMacCD[] = {
+	{ 0x1a, ARRAYSIZE(kMacCDScript1a), kMacCDScript1a },
+	{ 0x1c, ARRAYSIZE(kMacCDScript1c), kMacCDScript1c },
+	{ 0x22, ARRAYSIZE(kMacCDScript22), kMacCDScript22 },
+	{ 0x25, ARRAYSIZE(kMacCDScript25), kMacCDScript25 },
+	{ 0x2a, ARRAYSIZE(kMacCDScript2a), kMacCDScript2a },
+	{ 0x2c, ARRAYSIZE(kMacCDScript2c), kMacCDScript2c },
+	{ 0x30, ARRAYSIZE(kMacCDScript30), kMacCDScript30 },
+	{ 0x33, ARRAYSIZE(kMacCDScript33), kMacCDScript33 },
+	{ 0x34, ARRAYSIZE(kMacCDScript34), kMacCDScript34 },
+	{ 0x35, ARRAYSIZE(kMacCDScript35), kMacCDScript35 },
+	{ 0x36, ARRAYSIZE(kMacCDScript36), kMacCDScript36 },
+};
+
 // `_PatientSequence` and `_ImpatientSequence` are standalone script
 // pointers. CD has the data but never calls the switchers; floppy calls 
 // them from `_DoSiteLoop_Floppy` (via `_Switch2Patient` / `_Switch2Impatient`). 
@@ -764,11 +854,13 @@ const AnimScriptLong kAnimScriptsLondonLong[] = {
 	{ 0x31, 62, kScript31London },
 };
 
-// Set true for the London variant so findAnimScript uses the EEM2 tables.
+// Select the scripts and timing of the original release.
 bool g_londonAnimScripts = false;
+bool g_macCDAnimScripts = false;
 
-void setLondonAnimScripts(bool enabled) {
-	g_londonAnimScripts = enabled;
+void setAnimScripts(bool london, bool macCD) {
+	g_londonAnimScripts = london;
+	g_macCDAnimScripts = macCD;
 }
 
 struct AnimScriptRef {
@@ -776,6 +868,16 @@ struct AnimScriptRef {
 	uint16 len;
 };
 AnimScriptRef findAnimScript(uint16 seqnum) {
+	if (g_macCDAnimScripts) {
+		for (uint i = 0; i < ARRAYSIZE(kAnimScriptsMacCD); i++) {
+			if (kAnimScriptsMacCD[i].seqnum == seqnum) {
+				AnimScriptRef r;
+				r.frames = kAnimScriptsMacCD[i].frames;
+				r.len = kAnimScriptsMacCD[i].len;
+				return r;
+			}
+		}
+	}
 	if (g_londonAnimScripts) {
 		for (uint i = 0; i < ARRAYSIZE(kAnimScriptsLondon); i++) {
 			if (kAnimScriptsLondon[i].seqnum == seqnum) {
@@ -816,20 +918,22 @@ AnimScriptRef findAnimScript(uint16 seqnum) {
 	return r;
 }
 
-// Original frame period from `_InitFrameCounter @ 1a35:01ae`:
-const uint kFramePeriodMs = 140;
+uint animationFramePeriodMs() {
+	// Mac CD CODE 2:21c4 advances after nine 60 Hz ticks.
+	return g_macCDAnimScripts ? 150 : 140;
+}
 
 uint frameFromScriptAtTick(const uint8 *frames, uint len,
 								  uint numFrames, uint32 tickMs) {
 	if (!frames || len == 0)
-		return numFrames > 0 ? (uint)((tickMs / kFramePeriodMs) % numFrames) : 0;
-	const uint scriptIdx = (uint)((tickMs / kFramePeriodMs) % len);
+		return numFrames > 0 ? (uint)((tickMs / animationFramePeriodMs()) % numFrames) : 0;
+	const uint scriptIdx = (uint)((tickMs / animationFramePeriodMs()) % len);
 	const uint frame     = frames[scriptIdx];
 	return (numFrames > 0) ? MIN<uint>(frame, numFrames - 1) : 0;
 }
 
 // Looping path of `_UpdateAnimations`: walk the script one entry per
-// `_CheckFrameRate` tick (`kFramePeriodMs` ~= 140 ms), wrap on 0x80.
+// `_CheckFrameRate` tick, wrap on 0x80.
 uint partnerFrameAtTick(uint16 seqnum, uint numFrames, uint32 tickMs) {
 	const AnimScriptRef s = findAnimScript(seqnum);
 	return frameFromScriptAtTick(s.frames, s.len, numFrames, tickMs);
@@ -837,7 +941,7 @@ uint partnerFrameAtTick(uint16 seqnum, uint numFrames, uint32 tickMs) {
 
 uint oneShotFrameAtTick(uint16 seqnum, uint numFrames, uint32 tickMs) {
 	const AnimScriptRef s = findAnimScript(seqnum);
-	const uint tick = (uint)(tickMs / kFramePeriodMs);
+	const uint tick = (uint)(tickMs / animationFramePeriodMs());
 	if (!s.frames || s.len == 0)
 		return numFrames > 0 ? MIN<uint>(tick, numFrames - 1) : 0;
 	const uint scriptIdx = MIN<uint>(tick, (uint)s.len - 1);
@@ -848,7 +952,7 @@ uint oneShotFrameAtTick(uint16 seqnum, uint numFrames, uint32 tickMs) {
 uint32 oneShotDurationMs(uint16 seqnum, uint numFrames) {
 	const AnimScriptRef s = findAnimScript(seqnum);
 	const uint count = (s.frames && s.len) ? (uint)s.len : numFrames;
-	return (uint32)count * kFramePeriodMs;
+	return (uint32)count * animationFramePeriodMs();
 }
 
 // Play `unfold` once, then loop `waitSeq` forever. Mirrors the
@@ -856,7 +960,7 @@ uint32 oneShotDurationMs(uint16 seqnum, uint numFrames) {
 uint oneShotThenLoopFrameAtTick(const uint8 *unfold, uint unfoldLen,
 									   const uint8 *waitSeq, uint waitSeqLen,
 									   uint numFrames, uint32 elapsedMs) {
-	const uint tick = elapsedMs / kFramePeriodMs;
+	const uint tick = elapsedMs / animationFramePeriodMs();
 	const uint frame = (tick < unfoldLen)
 		? unfold[tick]
 		: waitSeq[(tick - unfoldLen) % waitSeqLen];
@@ -954,10 +1058,8 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
 	}
 
 	_waitPhaseAnchor = g_system->getMillis();
-	if (resetPartnerMood) {
+	if (resetPartnerMood)
 		_partnerWaitMood = kPartnerWaitDefault;
-		initImpatienceCounter();
-	}
 
 	const bool firstVisit = (siteNum < Mystery::kVisitedSiteCap)
 							 && (_mystery->_visitedSite[siteNum] == 0);
@@ -980,8 +1082,6 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
 				   siteNum, approachId);
 			_vm->doLondonApproach(approachId);
 		}
-	} else if (playArrival) {
-		_vm->startTravelMusic();
 	}
 
 	const bool compactSite = _vm->isFloppy() ||
@@ -999,7 +1099,7 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
 	}
 	_vm->setSitePaletteForSite(sitepic);
 
-	applyHotspotGlowPalette();
+	applyHotspotGlowPalette(_vm->isMacCD());
 
 	renderBackground(siteNum);
 
@@ -1008,17 +1108,24 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
 			renderFloppyDrops(siteNum);
 		else
 			renderStaticDrops(siteNum);
-		renderAnimatedDrops(siteNum, g_system->getMillis());
+		renderAnimatedDrops(siteNum, _vm->isMacCD() ? _waitPhaseAnchor : g_system->getMillis());
+		if (!london && !_vm->isMacCD())
+			_vm->startTravelMusic();
+		const bool cursorVisible = CursorMan.isVisible();
+		if (_vm->isMacCD())
+			CursorMan.showMouse(false);
 		const bool skippedArrival = enterSiteAnim();
 		_vm->markSiteArrivalPlayed(siteNum);
-		if (!_vm->isFloppy() && !_vm->isLondon()) {
-			if (skippedArrival)
-				_vm->stopMusic();
-			else
-				_vm->waitForMusicDone();
-		}
+		_vm->finishTravelMusic(skippedArrival);
+		if (_vm->isMacCD())
+			CursorMan.showMouse(cursorVisible);
+		if (_vm->shouldQuit())
+			return;
 		renderBackground(siteNum);
 	}
+	_waitPhaseAnchor = g_system->getMillis();
+	if (resetPartnerMood)
+		initImpatienceCounter();
 
 	if (compactSite)
 		renderFloppyDrops(siteNum);
@@ -1074,7 +1181,8 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
 }
 
 void SiteScreen::initImpatienceCounter() {
-	_impatientDeadlineMs = g_system->getMillis() + kImpatienceDelayMs;
+	_impatientDeadlineMs = g_system->getMillis() +
+		(_vm->isMacCD() ? 30000 : kImpatienceDelayMs);
 }
 
 bool SiteScreen::checkImpatienceCounter() {
@@ -1253,7 +1361,7 @@ void SiteScreen::run() {
 
 		const uint32 now = g_system->getMillis();
 		if (_snapshotSite == (int)cur &&
-			now - _lastTickMs >= kFramePeriodMs) {
+			now - _lastTickMs >= animationFramePeriodMs()) {
 			if (checkImpatienceCounter()) {
 				_partnerWaitMood = kPartnerWaitImpatient;
 				debugC(1, kDebugSite, "Partner impatience: switched to impatient");
@@ -1269,11 +1377,89 @@ void SiteScreen::run() {
 		g_system->delayMillis(10);
 	}
 }
-// `_EnterSiteAnim @ 1000:9b21`. Two phases (partner-dependent):
-//   Phase 1 — skateboard scroll: anim 6 (Jake) / 0xe (Jenny).
-//             Slides from (320-w, 199-h) leftward off-screen.
-//   Phase 2 — KD slide-in: anim 7 (Jake) / 0xf (Jenny).
-//             Slides from x=-w at y=0x8b/0x8e until x=0.
+
+static bool waitForArrivalFrame(EEMEngine *vm, uint32 deadline, bool allowSkip) {
+	while (!vm->shouldQuit()) {
+		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 (allowSkip && (event.type == Common::EVENT_KEYDOWN ||
+							  event.type == Common::EVENT_LBUTTONDOWN))
+				return true;
+		}
+		const int32 remaining = deadline - g_system->getMillis();
+		if (remaining <= 0)
+			return false;
+		g_system->delayMillis(MIN<int32>(remaining, 10));
+	}
+	return true;
+}
+
+// Mac CD CODE 2:4e22. Only the skate across is skippable.
+bool SiteScreen::enterMacSiteAnim(const Graphics::ManagedSurface &bg) {
+	const bool jake = _vm->getPartnerIndex() == kPartnerJake;
+	Animation skate, entry;
+	if (!_vm->getAni().loadAnimation(jake ? 6 : 14, skate) || skate.empty() ||
+		!_vm->getAni().loadAnimation(jake ? 7 : 15, entry) || entry.empty())
+		return false;
+
+	const MacSpritePaletteMap paletteMap = getMacSpritePaletteMap();
+	Graphics::ManagedSurface scratch(bg.w, bg.h, bg.format);
+	auto drawFrame = [&](const Picture &frame, int x, int y) {
+		scratch.simpleBlitFrom(bg);
+		blitMacAnimFrameAnchored(scratch.surfacePtr(), frame, x, y, paletteMap);
+		g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch, 0, 0, bg.w, bg.h);
+		g_system->updateScreen();
+	};
+
+	byte palette[256 * 3];
+		g_system->getPaletteManager()->grabPalette(palette, 0, 256);
+	_vm->startTravelMusic();
+	fadePaletteFromBlack(palette);
+
+	int x = 512;
+	const int y = 383 - skate[0].surface.h;
+	uint frame = 0;
+	uint distance = 0;
+	uint tick = 0;
+	bool skipped = false;
+	const uint32 startMs = g_system->getMillis();
+	// The original's fastest-machine path moves four pixels per Mac tick.
+	do {
+		if (waitForArrivalFrame(_vm, startMs + ++tick * 1000 / 60, true)) {
+			skipped = true;
+			break;
+		}
+		x -= 4;
+		drawFrame(skate[frame], x, y);
+		distance += 4;
+		if (distance > 21) {
+			frame = (frame + 1) % skate.size();
+			distance = 0;
+		}
+	} while (x >= 0 || -x <= skate[frame].surface.w);
+
+	g_system->copyRectToScreen(bg.getPixels(), bg.pitch, 0, 0, bg.w, bg.h);
+	g_system->updateScreen();
+	if (waitForArrivalFrame(_vm, g_system->getMillis() + 500, false))
+		return true;
+
+	const int entryY = jake ? 267 : 273;
+	uint32 nextFrameMs = g_system->getMillis();
+	drawFrame(entry[0], 0, entryY);
+	// Frame zero is drawn again on the first advancing tick.
+	for (uint i = 0; i < entry.size(); i++) {
+		nextFrameMs += animationFramePeriodMs();
+		if (waitForArrivalFrame(_vm, nextFrameMs, false))
+			return true;
+		drawFrame(entry[i], 0, entryY);
+	}
+	return skipped;
+}
+
+// DOS _EnterSiteAnim at 172b:2871.
 bool SiteScreen::enterSiteAnim() {
 	if (!_vm || !_mystery)
 		return false;
@@ -1292,6 +1478,8 @@ bool SiteScreen::enterSiteAnim() {
 		Graphics::PixelFormat::createFormatCLUT8());
 	bg.simpleBlitFrom(*screen);
 	g_system->unlockScreen();
+	if (_vm->isMacCD())
+		return enterMacSiteAnim(bg);
 	const bool mac = _vm->isMacintosh();
 	MacSpritePaletteMap macPaletteMap = {0x00, 0xFF};
 	if (mac)
@@ -1355,7 +1543,7 @@ bool SiteScreen::enterSiteAnim() {
 					return true;
 				}
 			}
-			g_system->delayMillis(kFramePeriodMs);
+			g_system->delayMillis(animationFramePeriodMs());
 		}
 		return false;
 	}
@@ -1465,7 +1653,10 @@ void SiteScreen::renderStaticDrops(uint siteNum) {
 		Picture pic;
 		if (!_vm->getPics().getPicture(picId, pic))
 			continue;
-		blitMaskedSurface(screen, pic, x, y);
+		if (_vm->isMacCD())
+			blitMacMaskedSurface(screen, pic, x, y);
+		else
+			blitMaskedSurface(screen, pic, x, y);
 	}
 
 	g_system->unlockScreen();
@@ -1514,7 +1705,7 @@ void SiteScreen::renderAnimatedDrops(uint siteNum, uint32 tickMs) {
 	if (!_mystery || !_vm)
 		return;
 
-	if (_vm->isMacintosh())
+	if (_vm->isMacintosh() && !_vm->isMacCD())
 		return;
 
 	if (_vm->isFloppy()) {
@@ -1572,9 +1763,13 @@ void SiteScreen::renderAnimatedDrops(uint siteNum, uint32 tickMs) {
 		Animation anim;
 		if (!_vm->getAni().loadAnimation((uint)animId, anim) || anim.empty())
 			continue;
+		const uint32 elapsed = _vm->isMacCD() ? tickMs - _waitPhaseAnchor : tickMs;
 		const uint frameIdx = partnerFrameAtTick((uint16)animId,
-												  (uint)anim.size(), tickMs);
-		blitAnimFrameAnchored(screen, anim[frameIdx], x, y);
+												  (uint)anim.size(), elapsed);
+		if (_vm->isMacCD())
+			blitMacAnimFrameAnchored(screen, anim[frameIdx], x, y);
+		else
+			blitAnimFrameAnchored(screen, anim[frameIdx], x, y);
 	}
 
 	g_system->unlockScreen();
@@ -1585,7 +1780,7 @@ void SiteScreen::scanColorCycles(uint siteNum) {
 	if (!_mystery)
 		return;
 
-	if (_vm && _vm->isMacintosh())
+	if (_vm && _vm->isMacintosh() && !_vm->isMacCD())
 		return;
 
 	if (_vm && _vm->isFloppy()) {
@@ -1614,8 +1809,9 @@ void SiteScreen::scanColorCycles(uint siteNum) {
 		const uint16 startPal = READ_LE_UINT16(site + 0x48 + i * 6 + 2);
 		const uint16 endPal   = READ_LE_UINT16(site + 0x48 + i * 6 + 4);
 		ColorCycleRange r;
-		r.start = (uint8)startPal;
-		r.end   = (uint8)endPal;
+		// Mac CODE 2:3dd2 takes one-based palette indices.
+		r.start = (uint8)(startPal - (_vm->isMacCD() ? 1 : 0));
+		r.end   = (uint8)(endPal - (_vm->isMacCD() ? 1 : 0));
 		if (r.end > r.start)
 			_colorCycles.push_back(r);
 	}
@@ -1625,7 +1821,10 @@ void SiteScreen::applyColorCycles() {
 	for (uint i = 0; i < _colorCycles.size(); i++) {
 		cyclePaletteRange(_colorCycles[i].start, _colorCycles[i].end);
 	}
-	cyclePaletteRange(0xF9, 0xFE);
+	if (_vm->isMacCD())
+		cyclePaletteRange(0xF8, 0xFD);
+	else
+		cyclePaletteRange(0xF9, 0xFE);
 }
 
 void SiteScreen::captureBgSnapshot() {
@@ -1861,6 +2060,28 @@ byte currentWhitePaletteIndex(byte fallback) {
 	return fallback;
 }
 
+static void drawMacSiteHotspot(Graphics::Surface *screen, const Common::Rect &rect, bool seen) {
+	byte color = seen ? 0xFE : 0xF8;
+	auto plot = [&](int x, int y) {
+		Common::Rect pixel(x, y, x + 2, y + 2);
+		pixel.clip(Common::Rect(screen->w, screen->h));
+		if (!pixel.isEmpty())
+			screen->fillRect(pixel, color);
+		if (!seen && ++color > 0xFD)
+			color = 0xF8;
+	};
+	int x = rect.left;
+	int y = rect.top + 1;
+	for (; y <= rect.bottom - 3; ++y)
+		plot(x, y);
+	for (; x <= rect.right - 3; ++x)
+		plot(x, y);
+	for (; y >= rect.top + 1; --y)
+		plot(x, y);
+	for (; x >= rect.left; --x)
+		plot(x, y);
+}
+
 void SiteScreen::renderHotspots(uint siteNum) {
 	if (ConfMan.getBool("hide_highlight_boxes"))
 		return;
@@ -1911,6 +2132,10 @@ void SiteScreen::renderHotspots(uint siteNum) {
 			seen = seenKey < Mystery::kHotSpotsCap &&
 				   _mystery->_hotSpotsSeen[seenKey];
 		}
+		if (_vm->isMacCD()) {
+			drawMacSiteHotspot(screen, rect, seen);
+			continue;
+		}
 		if (seen) {
 			screen->frameRect(rect, searchedColor);
 		} else {
@@ -2011,7 +2236,22 @@ void SiteScreen::displayClueAndAutosave(const byte *clueBlock, bool forceSave) {
 	const bool hasIdle =
 		partnerIdleAnimParams(_mystery->_siteNumber, idleId, idleX, idleY);
 
-	_vm->setPartnerEraseBg(&_bgSnapshot);
+	Graphics::ManagedSurface clueBg;
+	if (_vm->isMacCD()) {
+		// Keep the site's animated objects behind partner gestures.
+		const uint32 now = g_system->getMillis();
+		restoreBgSnapshot();
+		renderAnimatedDrops(_mystery->_siteNumber, now);
+		renderHotspots(_mystery->_siteNumber);
+		Graphics::Surface *screen = g_system->lockScreen();
+		if (screen) {
+			clueBg.create(screen->w, screen->h, screen->format);
+			clueBg.simpleBlitFrom(*screen);
+			g_system->unlockScreen();
+		}
+		renderPartner(_mystery->_siteNumber, now);
+	}
+	_vm->setPartnerEraseBg(clueBg.empty() ? &_bgSnapshot : &clueBg);
 	_vm->setPartnerIdleAnim(hasIdle, idleId, idleX, idleY);
 	_vm->displayClue(clueBlock);
 	_vm->setPartnerIdleAnim(false, 0, 0, 0);
diff --git a/engines/eem/site.h b/engines/eem/site.h
index 17576110e69..3b7b214f770 100644
--- a/engines/eem/site.h
+++ b/engines/eem/site.h
@@ -36,7 +36,7 @@ class EEMEngine;
 class Mystery;
 
 /// partnerFrameAtTick: frame index for `seqnum` at `tickMs`. Walks `kAnimScripts`
-/// at `kFramePeriodMs` per entry; wraps
+/// at the release's frame period per entry; wraps
 /// on the script's 0x80 terminator. Falls back to flipbook (`tick % numFrames`)
 /// when no script is registered. `numFrames` is the ANI.DBD entry's cell count,
 /// used both for the fallback and to clamp script values past the asset.
@@ -48,10 +48,8 @@ uint oneShotFrameAtTick(uint16 seqnum, uint numFrames, uint32 tickMs);
 /// Total time for one full play of a one-shot gesture (frame count * period).
 uint32 oneShotDurationMs(uint16 seqnum, uint numFrames);
 
-/// Select the EEM2 ("London") animation-script table inside `findAnimScript`.
-/// EEM2 ships its own `_AnimationSequences`; many partner/KD scripts differ
-/// from EEM1's, so the engine must use the EEM2 sequences for that variant.
-void setLondonAnimScripts(bool enabled);
+void setAnimScripts(bool london, bool macCD);
+uint animationFramePeriodMs();
 
 /// bigMapPartnerFrameAtTick: overview-map partner walk. EEM1 (11-frame anim):
 /// count-up 0..8 once, then idle `_BigMapWaitSeq`. London's anim 0x14/0x12 has
@@ -101,8 +99,8 @@ void cyclePaletteRange(uint8 start, uint8 end);
 /// (END→START).
 void cyclePaletteRangeReverse(uint8 start, uint8 end);
 
-/// Load the 6-step yellow marching-ants ramp into palette 0xF9..0xFE
-void applyHotspotGlowPalette();
+/// Load the hotspot palette used by the release.
+void applyHotspotGlowPalette(bool macCD = false);
 
 /// One hotspot (search rectangle) within a site, 14 bytes on disk.
 struct Hotspot {
@@ -147,6 +145,7 @@ private:
 
 	/// Partner site-arrival sequence
 	bool enterSiteAnim();
+	bool enterMacSiteAnim(const Graphics::ManagedSurface &bg);
 
 	/// renderPartner: persistent in-site partner sprite
 	void renderPartner(uint siteNum, uint32 tickMs);


Commit: 67c90d9a0b27a9e7375a72326d4d035195e62248
    https://github.com/scummvm/scummvm/commit/67c90d9a0b27a9e7375a72326d4d035195e62248
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-10T17:45:23+02:00

Commit Message:
EEM: implement TRAVIS scroll bar in EEM1 talkie (mac)

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


diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 741d6dae022..0c6ca8cd8b2 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -45,6 +45,8 @@
 
 namespace EEM {
 
+class TravisScrollBar;
+
 class AudioPlayer;
 class MusicPlayer;
 
@@ -318,7 +320,7 @@ private:
 	void screenDriver();
 
 	/// Re-render helpers for the corresponding `doX()` modal screens.
-	void drawNotebookFrame(int &page);
+	void drawNotebookFrame(int &page, TravisScrollBar *scrollBar = nullptr);
 
 	/// Resolve a single NoteIndex entry to displayable notebook text.
 	/// Handles the CD (4-byte) vs floppy (7-byte) entry strides.
@@ -345,6 +347,7 @@ private:
 		int pageBreaksCap;
 		int *numPages;
 		int *page;
+		TravisScrollBar *scrollBar = nullptr;
 	};
 
 	/// One NoteIndex entry as displayable accuse-screen text.
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index dcef8c18198..a70c01a7a9d 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -152,14 +152,17 @@ int scalePdaAnchor(int value, int target, int source) {
 
 void blitPdaPartner(Graphics::ManagedSurface &dst, DBDArchive &aniArchive,
 					uint8 partner, const PdaPartnerSpec &spec,
-					uint32 tickMs, bool mac = false) {
+					uint32 tickMs, bool mac = false, bool macCD = false) {
+	const PdaPartnerSpec nativeSpec { 0x02, 0x02, 0x10,
+		(spec.scriptId == 0x02 && partner == kPartnerJenny) ? 4 : 7, 152 };
+	const PdaPartnerSpec &activeSpec = macCD ? nativeSpec : spec;
 	Animation ani;
-	if (const Picture *fr = partnerFrameFor(aniArchive, partner, spec,
+	if (const Picture *fr = partnerFrameFor(aniArchive, partner, activeSpec,
 											tickMs, ani)) {
 		if (mac) {
-			const int anchorX =
+			const int anchorX = macCD ? activeSpec.anchorX :
 				scalePdaAnchor(spec.anchorX, kMacScreenWidth, kScreenWidth);
-			const int anchorY =
+			const int anchorY = macCD ? activeSpec.anchorY :
 				scalePdaAnchor(spec.anchorY, kMacScreenHeight, kScreenHeight);
 			blitMacAnimFrameAnchored(dst.surfacePtr(), *fr, anchorX,
 									 anchorY);
@@ -281,6 +284,202 @@ constexpr Common::Rect kMacPdaPartnerFootMapRect(Common::Point(11, 340), 80, 44)
 constexpr Common::Rect kMacPdaSiteRect(Common::Point(56, 213), 34, 48);
 constexpr Common::Rect kMacPdaHelp2Rect(Common::Point(413, 336), 34, 28);
 
+// Mac CD CODE 7: TRAVIS buttons and Control Manager scrollbars.
+constexpr Common::Rect kMacCDPdaMapRect(Common::Point(149, 334), 35, 30);
+constexpr Common::Rect kMacCDPdaNotebookRect(Common::Point(249, 334), 36, 30);
+constexpr Common::Rect kMacCDPdaGalleryRect(Common::Point(327, 334), 35, 30);
+constexpr Common::Rect kMacCDPdaHelpRect(Common::Point(426, 334), 36, 30);
+constexpr Common::Rect kMacCDPdaScrollRect(Common::Point(476, 19), 16, 294);
+constexpr Common::Rect kMacCDSuspectScrollRect(Common::Point(476, 179), 16, 113);
+constexpr Common::Rect kMacCDSuspectTextRect(Common::Point(125, 179), 326, 113);
+constexpr Common::Rect kMacCDAccuseTextRect(Common::Point(126, 52), 346, 259);
+constexpr int kMacCDPdaLineHeight = 16;
+constexpr int kMacCDPdaTextInsetX = 5;
+constexpr int kMacCDPdaTextInsetY = 4;
+
+class TravisScrollBar {
+public:
+	explicit TravisScrollBar(const Common::Rect &rect) : _rect(rect) {}
+
+	void layout(const Common::Array<int> &heights, int viewHeight, int &first) {
+		_tops.clear();
+		_tops.push_back(0);
+		for (uint i = 0; i < heights.size(); ++i)
+			_tops.push_back(_tops.back() + heights[i] + 7);
+		_viewHeight = viewHeight;
+		_last = MAX<int>(0, (int)heights.size() - 1);
+		while (_last > 0 && _tops.back() - _tops[_last - 1] - 7 < _viewHeight)
+			--_last;
+		first = CLIP(first, 0, _last);
+	}
+
+	int end(int first) const {
+		int last = first;
+		while (last + 1 < (int)_tops.size() &&
+			   (last == first || _tops[last + 1] - _tops[first] - 7 < _viewHeight))
+			++last;
+		return last;
+	}
+
+	bool contains(const Common::Point &point) const {
+		return _rect.contains(point);
+	}
+
+	bool handleEvent(const Common::Event &event, int &first) {
+		if (event.type == Common::EVENT_KEYDOWN) {
+			switch (event.kbd.keycode) {
+			case Common::KEYCODE_UP:
+				move(kUp, first);
+				return true;
+			case Common::KEYCODE_DOWN:
+				move(kDown, first);
+				return true;
+			case Common::KEYCODE_LEFT:
+			case Common::KEYCODE_PAGEUP:
+			case Common::KEYCODE_BACKSPACE:
+				move(kPageUp, first);
+				return true;
+			case Common::KEYCODE_RIGHT:
+			case Common::KEYCODE_PAGEDOWN:
+			case Common::KEYCODE_SPACE:
+				move(kPageDown, first);
+				return true;
+			case Common::KEYCODE_HOME:
+				first = 0;
+				return true;
+			case Common::KEYCODE_END:
+				first = _last;
+				return true;
+			default:
+				return false;
+			}
+		}
+		if (event.type == Common::EVENT_WHEELUP || event.type == Common::EVENT_WHEELDOWN) {
+			move(event.type == Common::EVENT_WHEELUP ? kUp : kDown, first);
+			return true;
+		}
+		if (event.type == Common::EVENT_LBUTTONUP && _pressed != kNone) {
+			_pressed = kNone;
+			return true;
+		}
+		if (event.type == Common::EVENT_MOUSEMOVE && _pressed == kThumb) {
+			const int travel = _rect.height() - 3 * _rect.width();
+			const int y = CLIP<int>(event.mouse.y - _dragOffset - _rect.top - _rect.width(), 0, travel);
+			const int target = travel > 0 ? y * _tops[_last] / travel : 0;
+			first = 0;
+			while (first < _last && _tops[first] < target)
+				++first;
+			return true;
+		}
+		if (event.type != Common::EVENT_LBUTTONDOWN || !contains(event.mouse))
+			return false;
+		if (_last == 0)
+			return true;
+		_pressed = partAt(event.mouse, first);
+		if (_pressed == kThumb)
+			_dragOffset = event.mouse.y - thumbRect(first).top;
+		else
+			move(_pressed, first);
+		_repeatAt = g_system->getMillis() + 400;
+		return true;
+	}
+
+	bool update(int &first) {
+		if (_pressed != kNone && !(g_system->getEventManager()->getButtonState() & Common::EventManager::LBUTTON)) {
+			_pressed = kNone;
+			return true;
+		}
+		if (_pressed == kNone || _pressed == kThumb || g_system->getMillis() < _repeatAt)
+			return false;
+		_repeatAt = g_system->getMillis() + 100;
+		if (partAt(g_system->getEventManager()->getMousePos(), first) != _pressed)
+			return false;
+		const int previous = first;
+		move(_pressed, first);
+		return first != previous;
+	}
+
+	void draw(Graphics::ManagedSurface &surface, int first) const {
+		const MacSpritePaletteMap colors = getMacSpritePaletteMap();
+		surface.fillRect(_rect, colors.white);
+		surface.frameRect(_rect, colors.black);
+		const int size = _rect.width();
+		if (_last > 0) {
+			for (int y = _rect.top + size; y < _rect.bottom - size; ++y) {
+				for (int x = _rect.left + 1; x < _rect.right - 1; ++x) {
+					if ((x + y) & 1)
+						*(byte *)surface.getBasePtr(x, y) = colors.black;
+				}
+			}
+			const Common::Rect thumb = thumbRect(first);
+			surface.fillRect(thumb, colors.white);
+			surface.frameRect(thumb, colors.black);
+		}
+		const byte arrow[] = { 0x08, 0x1c, 0x3e, 0x7f, 0x1c, 0x1c, 0x1c };
+		for (int direction = 0; direction < 2; ++direction) {
+			Common::Rect button(_rect.left, direction ? _rect.bottom - size : _rect.top,
+							_rect.right, direction ? _rect.bottom : _rect.top + size);
+			const bool pressed = _pressed == (direction ? kDown : kUp);
+			surface.fillRect(button, pressed ? colors.black : colors.white);
+			surface.frameRect(button, colors.black);
+			for (int y = 0; y < 7; ++y) {
+				for (int x = 0; x < 7; ++x) {
+					if ((arrow[direction ? 6 - y : y] & (1 << x)) && (_last > 0 || ((x + y) & 1)))
+						*(byte *)surface.getBasePtr(button.left + 4 + x, button.top + 4 + y) =
+							pressed ? colors.white : colors.black;
+				}
+			}
+		}
+	}
+
+private:
+	enum Part { kNone, kUp, kDown, kPageUp, kPageDown, kThumb };
+	Common::Rect _rect;
+	Common::Array<int> _tops;
+	int _viewHeight = 0;
+	int _last = 0;
+	Part _pressed = kNone;
+	int _dragOffset = 0;
+	uint32 _repeatAt = 0;
+
+	Common::Rect thumbRect(int first) const {
+		const int size = _rect.width();
+		const int y = _rect.top + size + (_last > 0 ?
+			(_rect.height() - 3 * size) * _tops[first] / _tops[_last] : 0);
+		return Common::Rect(_rect.left, y, _rect.right, y + size);
+	}
+
+	Part partAt(const Common::Point &point, int first) const {
+		if (!contains(point))
+			return kNone;
+		if (point.y < _rect.top + _rect.width())
+			return kUp;
+		if (point.y >= _rect.bottom - _rect.width())
+			return kDown;
+		const Common::Rect thumb = thumbRect(first);
+		if (point.y < thumb.top)
+			return kPageUp;
+		return point.y >= thumb.bottom ? kPageDown : kThumb;
+	}
+
+	void move(Part part, int &first) const {
+		if (part == kUp)
+			--first;
+		else if (part == kDown)
+			++first;
+		else if (part == kPageDown)
+			first = end(first);
+		else if (part == kPageUp) {
+			const int bottom = _tops[first];
+			if (first > 0)
+				--first;
+			while (first > 0 && bottom - _tops[first - 1] < _viewHeight)
+				--first;
+		}
+		first = CLIP(first, 0, _last);
+	}
+};
+
 constexpr uint16 kProfilePickerRevealPic = 0x105;
 constexpr int kProfilePickerRevealX = 0x3e;
 constexpr int kProfilePickerRevealY = 0xb3;
@@ -325,6 +524,18 @@ constexpr uint kRestoredContentLastMystery = 0x18;
 Common::Rect pdaControlRect(const EEMEngine *vm, const Common::Rect &rect) {
 	if (!vm || !vm->isMacintosh())
 		return rect;
+	if (vm->isMacCD()) {
+		if (rect == kPdaNotebookRect)
+			return kMacCDPdaNotebookRect;
+		if (rect == kPdaHelpRect)
+			return kMacCDPdaMapRect;
+		if (rect == kPdaGalleryRect)
+			return kMacCDPdaGalleryRect;
+		if (rect == kPdaHelp2Rect)
+			return kMacCDPdaHelpRect;
+		if (rect == kPdaPageNextRect || rect == kPdaPagePrevRect || rect == kPdaPartnerFootMapRect)
+			return Common::Rect();
+	}
 	if (rect == kPdaNotebookRect)
 		return kMacPdaNotebookRect;
 	if (rect == kPdaHelpRect)
@@ -349,7 +560,7 @@ Common::Rect pdaControlRect(const EEMEngine *vm, const Common::Rect &rect) {
 }
 
 int macPdaScrollBarDelta(const EEMEngine *vm, int x, int y) {
-	if (!vm || !vm->isMacintosh() || !kMacPdaScrollBarRect.contains(x, y))
+	if (!vm || !vm->isMacintosh() || vm->isMacCD() || !kMacPdaScrollBarRect.contains(x, y))
 		return 0;
 
 	static const Common::Rect kMacPdaScrollBarButtons[] = {
@@ -367,9 +578,124 @@ int macPdaScrollBarDelta(const EEMEngine *vm, int x, int y) {
 }
 
 bool macPdaScrollBarAt(const EEMEngine *vm, int x, int y) {
+	if (vm && vm->isMacCD())
+		return kMacCDPdaScrollRect.contains(x, y);
 	return macPdaScrollBarDelta(vm, x, y) != 0;
 }
 
+bool pdaSiteButtonAt(const EEMEngine *vm, int x, int y) {
+	return pdaControlRect(vm, kPdaSiteRect).contains(x, y) ||
+		(vm && vm->isMacCD() && Common::Rect(0, 0, 106, 384).contains(x, y) &&
+		 !kMacPdaPartnerHeadHintRect.contains(x, y));
+}
+
+bool handleMacCDPdaKey(Common::Event &event, const Common::Array<Common::Rect> *items = nullptr,
+					  bool includeButtons = true) {
+	if (event.type != Common::EVENT_KEYDOWN)
+		return false;
+	const Common::Point mouse = g_system->getEventManager()->getMousePos();
+	if (event.kbd.keycode == Common::KEYCODE_TAB) {
+		const Common::Rect buttons[] = {
+			kMacCDPdaNotebookRect, kMacCDPdaGalleryRect, kMacPdaAccuseRect,
+			kMacCDPdaHelpRect, kMacCDPdaMapRect, kMacPdaSiteRect,
+			kMacPdaPartnerHeadHintRect, Common::Rect(0, 0, 106, 152)
+		};
+		Common::Array<Common::Rect> targets;
+		if (includeButtons) {
+			for (uint i = 0; i < ARRAYSIZE(buttons); ++i)
+				targets.push_back(buttons[i]);
+		}
+		if (items) {
+			for (uint i = 0; i < items->size(); ++i) {
+				if (!(*items)[i].isEmpty())
+					targets.push_back((*items)[i]);
+			}
+		}
+		if (targets.empty())
+			return true;
+		int current = -1;
+		for (uint i = 0; i < targets.size(); ++i) {
+			if (targets[i].contains(mouse)) {
+				current = i;
+				break;
+			}
+		}
+		const int step = (event.kbd.flags & Common::KBD_SHIFT) ? -1 : 1;
+		const int count = targets.size();
+		const int nextIndex = current < 0 ? (step > 0 ? 0 : count - 1) : (current + step + count) % count;
+		const Common::Rect &next = targets[nextIndex];
+		event.type = Common::EVENT_MOUSEMOVE;
+		event.mouse = Common::Point((next.left + next.right) / 2, (next.top + next.bottom) / 2);
+		g_system->warpMouse(event.mouse.x, event.mouse.y);
+		return false;
+	}
+	if (event.kbd.keycode == Common::KEYCODE_RETURN || event.kbd.keycode == Common::KEYCODE_KP_ENTER) {
+		if (kMacCDPdaScrollRect.contains(mouse))
+			return true;
+		event.type = Common::EVENT_LBUTTONDOWN;
+		event.mouse = mouse;
+	}
+	return false;
+}
+
+bool trackMacCDPdaButton(EEMEngine &vm, Common::Event &event) {
+	if (event.type != Common::EVENT_LBUTTONDOWN)
+		return false;
+	const struct {
+		Common::Rect rect;
+		uint16 picture;
+		Common::Point position;
+	} buttons[] = {
+		{ kMacCDPdaMapRect,      0x108, Common::Point(148, 334) },
+		{ kMacCDPdaNotebookRect, 0x10c, Common::Point(249, 335) },
+		{ kMacCDPdaGalleryRect,  0x10d, Common::Point(325, 335) },
+		{ kMacPdaAccuseRect,    0x10a, Common::Point(287, 335) },
+		{ kMacCDPdaHelpRect,    0x10b, Common::Point(426, 335) }
+	};
+	for (uint i = 0; i < ARRAYSIZE(buttons); ++i) {
+		if (!buttons[i].rect.contains(event.mouse))
+			continue;
+		Picture picture;
+		if (!vm.getPics().getPicture(buttons[i].picture, picture))
+			return false;
+		Graphics::ManagedSurface background;
+		Graphics::Surface *screen = g_system->lockScreen();
+		background.copyFrom(*screen);
+		g_system->unlockScreen();
+		Graphics::ManagedSurface pressed;
+		pressed.copyFrom(*background.surfacePtr());
+		blitMacMaskedSurface(pressed.surfacePtr(), picture, buttons[i].position.x, buttons[i].position.y);
+		bool inside = true;
+		g_system->copyRectToScreen(pressed.getPixels(), pressed.pitch, 0, 0, pressed.w, pressed.h);
+		g_system->updateScreen();
+		while (!vm.shouldQuit() && (g_system->getEventManager()->getButtonState() & Common::EventManager::LBUTTON)) {
+			Common::Event tracking;
+			while (g_system->getEventManager()->pollEvent(tracking)) {
+				if (tracking.type == Common::EVENT_QUIT || tracking.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+					event = tracking;
+					break;
+				}
+				if (tracking.type == Common::EVENT_LBUTTONUP)
+					break;
+			}
+			if (event.type == Common::EVENT_QUIT || event.type == Common::EVENT_RETURN_TO_LAUNCHER)
+				break;
+			const bool over = buttons[i].rect.contains(g_system->getEventManager()->getMousePos());
+			if (over != inside) {
+				inside = over;
+				const Graphics::ManagedSurface &image = inside ? pressed : background;
+				g_system->copyRectToScreen(image.getPixels(), image.pitch, 0, 0, image.w, image.h);
+				g_system->updateScreen();
+			}
+			g_system->delayMillis(10);
+		}
+		g_system->copyRectToScreen(background.getPixels(), background.pitch, 0, 0, background.w, background.h);
+		g_system->updateScreen();
+		return !inside && event.type == Common::EVENT_LBUTTONDOWN;
+	}
+	return false;
+}
+
 bool notebookButtonAt(const EEMEngine *vm, int x, int y) {
 	return pdaControlRect(vm, kPdaHelpRect).contains(x, y) ||
 		   pdaControlRect(vm, kPdaGalleryRect).contains(x, y) ||
@@ -379,7 +705,7 @@ bool notebookButtonAt(const EEMEngine *vm, int x, int y) {
 		   pdaControlRect(vm, kPdaPagePrevRect).contains(x, y) ||
 		   pdaControlRect(vm, kPdaHelp2Rect).contains(x, y) ||
 		   pdaControlRect(vm, kPdaPartnerFootMapRect).contains(x, y) ||
-		   pdaControlRect(vm, kPdaSiteRect).contains(x, y) ||
+		   pdaSiteButtonAt(vm, x, y) ||
 		   macPdaScrollBarAt(vm, x, y);
 }
 
@@ -397,7 +723,7 @@ bool galleryButtonAt(int x, int y) {
 }
 
 bool galleryButtonAt(const EEMEngine *vm, int x, int y) {
-	return pdaControlRect(vm, kPdaSiteRect).contains(x, y) ||
+	return pdaSiteButtonAt(vm, x, y) ||
 		   pdaControlRect(vm, kPdaPartnerFootMapRect).contains(x, y) ||
 		   pdaControlRect(vm, kPdaAccuseRect).contains(x, y) ||
 		   pdaControlRect(vm, kPdaNotebookRect).contains(x, y) ||
@@ -580,6 +906,13 @@ void cycleChooserPalette() {
 	cyclePaletteRange(kChooserCycleStart, kChooserCycleEnd);
 }
 
+void cycleTravisPalette(bool macCD) {
+	if (macCD)
+		cyclePaletteRange(0xef, 0xf3);
+	else
+		cycleChooserPalette();
+}
+
 void blitMaskedPicSlice(Graphics::ManagedSurface &dst, const Picture &pic,
 						int srcX, int srcY, int w, int h,
 						int dstX, int dstY) {
@@ -2692,8 +3025,8 @@ void EEMEngine::doNotebook() {
 	CursorMan.showMouse(true);
 
 	int page = 0;
-	int hoveredNoteSlot = -1;
-	(void)hoveredNoteSlot;
+	TravisScrollBar macScroll(kMacCDPdaScrollRect);
+	TravisScrollBar *scrollBar = isMacCD() ? &macScroll : nullptr;
 
 	const bool notebookFromSite = isLondon() && _lastScreen == kScreenSite;
 	if (_music && _voiceOn && notebookFromSite)
@@ -2701,7 +3034,7 @@ void EEMEngine::doNotebook() {
 
 	if (isMacintosh())
 		setSitePalette(0);
-	drawNotebookFrame(page);
+	drawNotebookFrame(page, scrollBar);
 	Common::Point mouse = g_system->getEventManager()->getMousePos();
 	setInteractiveMouseCursor(notebookButtonAt(this, mouse.x, mouse.y));
 
@@ -2723,12 +3056,18 @@ void EEMEngine::doNotebook() {
 		bool dirty = false;
 		bool exitFlag = false;
 		while (g_system->getEventManager()->pollEvent(ev)) {
+			if (isMacCD() && (handleMacCDPdaKey(ev) || trackMacCDPdaButton(*this, ev)))
+				continue;
 			if (ev.type == Common::EVENT_QUIT ||
 				ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
 				_nextScreen = kScreenInvalid;
 				exitFlag = true;
 				break;
 			}
+			if (scrollBar && scrollBar->handleEvent(ev, page)) {
+				dirty = true;
+				break;
+			}
 			if (ev.type == Common::EVENT_MOUSEMOVE) {
 				setInteractiveMouseCursor(notebookButtonAt(this, ev.mouse.x,
 														   ev.mouse.y));
@@ -2751,8 +3090,7 @@ void EEMEngine::doNotebook() {
 				}
 			}
 			if (ev.type == Common::EVENT_LBUTTONDOWN) {
-				if (pdaControlRect(this, kPdaSiteRect).contains(ev.mouse.x,
-																ev.mouse.y) ||
+				if (pdaSiteButtonAt(this, ev.mouse.x, ev.mouse.y) ||
 					(isLondon() &&
 					 kPdaLondonCloseRect.contains(ev.mouse.x, ev.mouse.y))) {
 					_nextScreen = kScreenSite;
@@ -2788,7 +3126,7 @@ void EEMEngine::doNotebook() {
 				}
 				if (pdaControlRect(this, kPdaHelpRect).contains(ev.mouse.x,
 																ev.mouse.y)) {
-					if (isLondon()) {
+					if (isLondon() || isMacCD()) {
 						_nextScreen = kScreenMapAlt;
 						exitFlag = true;
 						break;
@@ -2841,16 +3179,18 @@ void EEMEngine::doNotebook() {
 			break;
 
 		const uint32 now = g_system->getMillis();
+		if (scrollBar && scrollBar->update(page))
+			dirty = true;
 		if (dirty || now - lastDraw >= 100) {
-			drawNotebookFrame(page);
+			drawNotebookFrame(page, scrollBar);
 			lastDraw = now;
 			mouse = g_system->getEventManager()->getMousePos();
 			setInteractiveMouseCursor(notebookButtonAt(this, mouse.x,
 													   mouse.y));
 		}
-		if (now - gizmoLastTick >= kChooserCycleMillis) {
+		if (now - gizmoLastTick >= (isMacCD() ? 150 : kChooserCycleMillis)) {
 			gizmoLastTick = now;
-			cycleChooserPalette();
+			cycleTravisPalette(isMacCD());
 		}
 		g_system->updateScreen();
 		g_system->delayMillis(15);
@@ -2892,7 +3232,7 @@ Common::String EEMEngine::notebookNoteText(uint clueId, const byte *ni,
 					   _playerName, _partner);
 }
 
-void EEMEngine::drawNotebookFrame(int &page) {
+void EEMEngine::drawNotebookFrame(int &page, TravisScrollBar *scrollBar) {
 	const Common::Rect kNotebookRect(78, 12, 288, 152);
 	const Common::Rect notebookRect =
 		isMacintosh() ? kMacNotebookTextRect : kNotebookRect;
@@ -2908,7 +3248,7 @@ void EEMEngine::drawNotebookFrame(int &page) {
 		blitTravisBackground(scratch, frame, isMacintosh());
 
 	blitPdaPartner(scratch, _aniArchive, _partner, kPdaNotebookPartner,
-				   g_system->getMillis(), isMacintosh());
+				   g_system->getMillis(), isMacintosh(), isMacCD());
 
 	// `_DrawNotes` walks `_NoteIndex` for current page; word-wraps each
 	// found clue in `_NotebookRect`. Selected = color 0x3c.
@@ -2934,16 +3274,20 @@ void EEMEngine::drawNotebookFrame(int &page) {
 	const int kRectY = notebookRect.top;
 	const int kRectW = notebookRect.width();
 	const int kRectH = notebookRect.height();
+	const int insetX = isMacCD() ? kMacCDPdaTextInsetX : 0;
+	const int insetY = isMacCD() ? kMacCDPdaTextInsetY : 0;
+	const int textWidth = kRectW - insetX;
+	const int lineH = isMacCD() ? kMacCDPdaLineHeight : _font.getFontHeight();
 
 	int clueCursor = 0;
 	Common::Array<int> pageStarts;
+	Common::Array<int> noteHeights;
 	pageStarts.push_back(0);
 
 	const bool floppyNb = isFloppy();
 	const byte *bufBase = _mystery.blobAt(0);
 	const uint32 mysSz  = _mystery.dataSize();
 	{
-		const int lineH = _font.getFontHeight();
 		int y = kRectY;
 		while (clueCursor < (int)found.size()) {
 			const uint clueId = found[clueCursor];
@@ -2951,8 +3295,9 @@ void EEMEngine::drawNotebookFrame(int &page) {
 												  floppyNb, bufBase, mysSz);
 			// Measure height by wrapping the text without drawing.
 			Common::Array<Common::String> wrapped;
-			_font.wordWrapText(txt, kRectW, wrapped);
+			_font.wordWrapText(txt, textWidth, wrapped);
 			const int h = (int)wrapped.size() * lineH;
+			noteHeights.push_back(h);
 			if (y + h + 7 > kRectY + kRectH) {
 				// Page break before this clue.
 				y = kRectY;
@@ -2961,17 +3306,20 @@ void EEMEngine::drawNotebookFrame(int &page) {
 			y += h + 7;
 			clueCursor++;
 		}
-		if (page >= (int)pageStarts.size())
+		if (scrollBar)
+			scrollBar->layout(noteHeights, kRectH, page);
+		else if (page >= (int)pageStarts.size())
 			page = (int)pageStarts.size() - 1;
 		if (page < 0)
 			page = 0;
 	}
 
-	const int startClue = (page < (int)pageStarts.size())
+	const int startClue = scrollBar ? page : (page < (int)pageStarts.size())
 							? pageStarts[page] : 0;
-	const int endClue   = (page + 1 < (int)pageStarts.size())
+	const int endClue   = scrollBar ? scrollBar->end(page) : (page + 1 < (int)pageStarts.size())
 							? pageStarts[page + 1] : (int)found.size();
 
+	Graphics::ManagedSurface textSurface(scratch, notebookRect);
 	int y = kRectY;
 	for (int i = startClue; i < endClue; i++) {
 		const uint clueId = found[i];
@@ -2981,24 +3329,25 @@ void EEMEngine::drawNotebookFrame(int &page) {
 			txt = Common::String::format(
 				isSpanish() ? "nota %u" : "clue %u", clueId);
 		Common::Array<Common::String> wrapped;
-		_font.wordWrapText(txt, kRectW, wrapped);
-		const int lineH = _font.getFontHeight();
+		_font.wordWrapText(txt, textWidth, wrapped);
 		const int h = (int)wrapped.size() * lineH;
-		const byte color = _mystery._noteSelected[clueId] ? 0x3C : 0x5C;
+		const byte color = !isMacCD() && _mystery._noteSelected[clueId] ? 0x3C : 0x5C;
 		for (uint li = 0; li < wrapped.size(); li++) {
-			_font.drawString(&scratch, wrapped[li], kRectX,
-							 y + (int)li * lineH, kRectW, color);
+			_font.drawString(&textSurface, wrapped[li], insetX,
+							 y - kRectY + insetY + (int)li * lineH, textWidth, color);
 		}
 		y += h + 7;
 	}
 
 	const bool isLastPage = (page + 1 >= (int)pageStarts.size());
-	if (isLastPage) {
+	if (isLastPage && !isMacCD()) {
 		const char *kEndMarker = isSpanish()
 			? "-- Fin de las notas --"
 			: "-- End of notes --";
 		_font.drawString(&scratch, kEndMarker, kRectX, y, kRectW, 0x5C);
 	}
+	if (scrollBar)
+		scrollBar->draw(scratch, page);
 
 	g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
 							   0, 0, sw, sh);
@@ -3047,6 +3396,8 @@ void EEMEngine::doGallery() {
 		Common::Event ev;
 		bool exitFlag = false;
 		while (g_system->getEventManager()->pollEvent(ev)) {
+			if (isMacCD() && (handleMacCDPdaKey(ev, &slotRects) || trackMacCDPdaButton(*this, ev)))
+				continue;
 			if (ev.type == Common::EVENT_QUIT ||
 				ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
 				_nextScreen = kScreenInvalid;
@@ -3079,8 +3430,7 @@ void EEMEngine::doGallery() {
 				//   [6] (226,247) → generic exit              (0x638)
 				//   [7] (  7,177) → MAP = NextScreen=2        (0x5f7)
 				//   [8] ( 35,111) → SITE = NextScreen=3       (0x5e4)
-				if (pdaControlRect(this, kPdaSiteRect).contains(ev.mouse.x,
-																ev.mouse.y)) {
+				if (pdaSiteButtonAt(this, ev.mouse.x, ev.mouse.y)) {
 					_nextScreen = kScreenSite;
 					exitFlag = true;
 					break;
@@ -3105,7 +3455,7 @@ void EEMEngine::doGallery() {
 				}
 				if (pdaControlRect(this, kPdaHelpRect).contains(ev.mouse.x,
 																ev.mouse.y)) {
-					if (isLondon()) {
+					if (isLondon() || isMacCD()) {
 						_nextScreen = kScreenMapAlt;
 						exitFlag = true;
 						break;
@@ -3171,9 +3521,9 @@ void EEMEngine::doGallery() {
 									  gallerySlotAt(slotRects, slotSuspect,
 													mouse.x, mouse.y));
 		}
-		if (now - gizmoLastTick >= kChooserCycleMillis) {
+		if (now - gizmoLastTick >= (isMacCD() ? 150 : kChooserCycleMillis)) {
 			gizmoLastTick = now;
-			cycleChooserPalette();
+			cycleTravisPalette(isMacCD());
 		}
 		g_system->updateScreen();
 		g_system->delayMillis(15);
@@ -3199,25 +3549,50 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 	setInteractiveMouseCursor(false);
 
 	const Common::Rect noteRectBase(78, 93, 288, 152);
-	const Common::Rect noteRect = mac ? scaleRect(noteRectBase) : noteRectBase;
+	const Common::Rect noteRect = isMacCD() ? kMacCDSuspectTextRect :
+		(mac ? scaleRect(noteRectBase) : noteRectBase);
 	const int rx = noteRect.left;
 	const int ry = noteRect.top;
 	const int rw = noteRect.width();
 	const int rh = noteRect.height();
 	const int sw = screenWidth();
 	const int sh = screenHeight();
-	const int lineH = _font.getFontHeight();
+	const int lineH = isMacCD() ? kMacCDPdaLineHeight : _font.getFontHeight();
+	const int insetX = isMacCD() ? kMacCDPdaTextInsetX : 0;
+	const int insetY = isMacCD() ? kMacCDPdaTextInsetY : 0;
+	const int textWidth = MAX<int>(8, rw - insetX);
 	const uint clueMax = compactMI ? clueCount : 30u;
 	const byte *ni = _mystery.noteIndex();
 	const uint16 niCount = isLondon()
 		? (uint16)(_mystery.noteSectionSize() / 2)
 		: _mystery.noteIndexCount();
 
-	uint pageStart = 0;
-	Common::Array<uint> pageStack;
+	int pageStart = 0;
+	Common::Array<int> pageStack;
+	TravisScrollBar macScroll(kMacCDSuspectScrollRect);
+	Common::Array<uint> macClues;
+	if (isMacCD()) {
+		Common::Array<int> heights;
+		for (uint i = 0; i < clueCount && i < clueMax; ++i) {
+			const uint clueId = READ_LE_UINT16(suspect + 0xa + i * 2);
+			if (clueId >= Mystery::kCluesFoundCap || clueId >= niCount || !_mystery._cluesFound[clueId])
+				continue;
+			const Common::String text = notebookNoteText(clueId, ni, niCount, false,
+				_mystery.blobAt(0), _mystery.dataSize());
+			if (text.empty())
+				continue;
+			Common::Array<Common::String> lines;
+			_font.wordWrapText(text, textWidth, lines);
+			heights.push_back(lines.size() * lineH);
+			macClues.push_back(clueId);
+		}
+		macScroll.layout(heights, rh, pageStart);
+	}
+	const uint entryCount = isMacCD() ? macClues.size() : MIN(clueCount, clueMax);
 	bool back = false;
 	bool exitGallery = false;
 	bool isFirstShow = true;
+	uint32 gizmoLastTick = g_system->getMillis();
 
 	while (!back && !shouldQuit()) {
 		Graphics::ManagedSurface ms(sw, sh,
@@ -3227,7 +3602,7 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 			blitTravisBackground(ms, galBg, mac);
 
 		blitPdaPartner(ms, _aniArchive, _partner, kPdaGalleryPartner,
-					   g_system->getMillis(), mac);
+					   g_system->getMillis(), mac, isMacCD());
 		Picture detail;
 		if (_picsArchive.getPicture(detailPic, detail)) {
 			const Common::Point detailPos =
@@ -3243,11 +3618,13 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 		// Walk clues from pageStart; defer overflow to next page unless
 		// first clue is too tall to ever fit.
 		int yPos = ry;
+		Graphics::ManagedSurface textSurface(ms, noteRect);
 		bool drewAny = false;
 		uint k = pageStart;
 		bool reachedEnd = false;
-		for (; k < clueCount && k < clueMax; k++) {
-			const uint16 clueId = compactMI
+		const uint end = isMacCD() ? macScroll.end(pageStart) : entryCount;
+		for (; k < end; k++) {
+			const uint16 clueId = isMacCD() ? macClues[k] : compactMI
 				? (uint16)suspect[5 + k]
 				: READ_LE_UINT16(suspect + 0xa + k * 2);
 			if (!compactMI && clueId == 0xFFFF) {
@@ -3268,27 +3645,27 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 				continue;
 
 			Common::Array<Common::String> wrapped;
-			_font.wordWrapText(txt, MAX<int>(8, rw), wrapped);
+			_font.wordWrapText(txt, textWidth, wrapped);
 			const int hClue = (int)wrapped.size() * lineH;
 			if (yPos + hClue > ry + rh && drewAny) {
 				// Defer to next page.
 				break;
 			}
-			const byte color = _mystery._noteSelected[clueId] ? 0x3C : 0x5C;
+			const byte color = !isMacCD() && _mystery._noteSelected[clueId] ? 0x3C : 0x5C;
 			for (uint l = 0; l < wrapped.size(); l++) {
-				_font.drawString(&ms, wrapped[l], rx,
-					yPos + (int)l * lineH, MAX<int>(8, rw), color);
+				_font.drawString(&textSurface, wrapped[l], insetX,
+					yPos - ry + insetY + (int)l * lineH, textWidth, color);
 			}
 			yPos += hClue + 7;
 			drewAny = true;
 		}
-		if (k >= clueCount || k >= clueMax)
+		if (k >= entryCount)
 			reachedEnd = true;
 		const uint pageEnd = k;
 		const bool hasMore = !reachedEnd;
 		const bool hasPrev = !pageStack.empty();
 
-		if (pageStart == 0 && !drewAny && _font.isLoaded()) {
+		if (!isMacCD() && pageStart == 0 && !drewAny && _font.isLoaded()) {
 			_font.drawString(&ms,
 				isSpanish()
 					? "Aun no hay pistas para este sospechoso."
@@ -3296,7 +3673,7 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 				rx, ry, MAX<int>(8, rw), 0x5C);
 		}
 		// Header / footer text.
-		if (_font.isLoaded()) {
+		if (!isMacCD() && _font.isLoaded()) {
 			_font.drawString(&ms,
 				isSpanish() ? "EXPEDIENTE" : "SUSPECT FILE",
 				rx, ry - 11, MAX<int>(8, rw), 0x3C);
@@ -3304,6 +3681,8 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 				isSpanish() ? "(ESC: volver)" : "(ESC: back)",
 				rx, ry + rh + 2, MAX<int>(8, rw), 0x3C);
 		}
+		if (isMacCD())
+			macScroll.draw(ms, pageStart);
 		g_system->copyRectToScreen(ms.getPixels(), ms.pitch,
 			0, 0, sw, sh);
 		g_system->updateScreen();
@@ -3325,10 +3704,12 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 		bool advance = false;
 		bool prev = false;
 		bool redraw = false;
-		uint32 gizmoLastTick = g_system->getMillis();
+		const uint32 lastDraw = g_system->getMillis();
 		while (!back && !advance && !prev && !redraw && !shouldQuit()) {
 			Common::Event e2;
 			while (g_system->getEventManager()->pollEvent(e2)) {
+				if (isMacCD() && (handleMacCDPdaKey(e2) || trackMacCDPdaButton(*this, e2)))
+					continue;
 				if (e2.type == Common::EVENT_QUIT ||
 					e2.type == Common::EVENT_RETURN_TO_LAUNCHER) {
 					_nextScreen = kScreenInvalid;
@@ -3340,17 +3721,26 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 					redraw = true;
 					break;
 				}
+				if (isMacCD() && macScroll.handleEvent(e2, pageStart)) {
+					redraw = true;
+					break;
+				}
 				if (e2.type == Common::EVENT_MOUSEMOVE) {
 					const int scrollDelta =
 						macPdaScrollBarDelta(this, e2.mouse.x, e2.mouse.y);
 					setInteractiveMouseCursor(
 						galleryButtonAt(this, e2.mouse.x, e2.mouse.y) ||
+						(isMacCD() && macScroll.contains(e2.mouse)) ||
 						(scrollDelta < 0 && hasPrev) ||
 						(scrollDelta > 0 && hasMore));
 				}
 				if (e2.type == Common::EVENT_LBUTTONDOWN) {
 					const int mx = e2.mouse.x;
 					const int my = e2.mouse.y;
+					if (isMacCD() && pdaSiteButtonAt(this, mx, my)) {
+						_nextScreen = kScreenSite;
+						return true;
+					}
 					debugC(2, kDebugGfx,
 						"MoreInfo click (%d,%d) hasMore=%d hasPrev=%d",
 						mx, my, (int)hasMore, (int)hasPrev);
@@ -3372,7 +3762,7 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 						back = true;
 						break;
 					}
-					if (isLondon() &&
+					if ((isLondon() || isMacCD()) &&
 						pdaControlRect(this, kPdaHelpRect).contains(mx, my)) {
 						_nextScreen = kScreenMapAlt;
 						exitGallery = true;
@@ -3446,10 +3836,12 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 			}
 
 			const uint32 now = g_system->getMillis();
-			if (now - gizmoLastTick >= kChooserCycleMillis) {
+			if (now - gizmoLastTick >= (isMacCD() ? 150 : kChooserCycleMillis)) {
 				gizmoLastTick = now;
-				cycleChooserPalette();
+				cycleTravisPalette(isMacCD());
 			}
+			if (isMacCD() && (macScroll.update(pageStart) || now - lastDraw >= 150))
+				redraw = true;
 			g_system->updateScreen();
 			g_system->delayMillis(20);
 		}
@@ -3483,7 +3875,7 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
 		blitTravisBackground(scratch, galBg, mac);
 
 	blitPdaPartner(scratch, _aniArchive, _partner, kPdaGalleryPartner,
-				   g_system->getMillis(), mac);
+				   g_system->getMillis(), mac, isMacCD());
 
 	const bool floppy = isFloppy();
 	const bool compactGallery = floppy || _mystery.usesCompactMacData();
@@ -3512,7 +3904,8 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
 				continue;
 
 			const int placeX = s.x;
-			const int placeY = mac ? s.y : s.y + (0x48 - portrait.surface.h);
+			const int placeY = isMacCD() ? s.y + 138 - portrait.surface.h :
+				(mac ? s.y : s.y + (0x48 - portrait.surface.h));
 			const int w = MIN<int>(portrait.surface.w, sw - placeX);
 			const int h = MIN<int>(portrait.surface.h, sh - placeY);
 			if (w <= 0 || h <= 0)
@@ -3527,7 +3920,7 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
 			slotRects[i] = Common::Rect(placeX, placeY,
 										 placeX + w, placeY + h);
 			slotSuspect[i] = (int)i;
-		} else {
+		} else if (!isMacCD()) {
 			// Undiscovered placeholder — small framed "?" box.
 			const int phW = mac ? 0x72 : 0x40;
 			const int phH = mac ? 0x90 : 0x48;
@@ -3585,6 +3978,16 @@ Common::String EEMEngine::accuseNoteText(uint clueId,
 }
 
 void EEMEngine::accuseRebuildPagination(const AccuseNotesCtx &ctx) {
+	if (ctx.scrollBar) {
+		Common::Array<int> heights;
+		for (uint i = 0; i < ctx.found->size(); ++i) {
+			Common::Array<Common::String> lines;
+			_font.wordWrapText(accuseNoteText((*ctx.found)[i], ctx), ctx.rectW - kMacCDPdaTextInsetX, lines);
+			heights.push_back(lines.size() * kMacCDPdaLineHeight);
+		}
+		ctx.scrollBar->layout(heights, ctx.rectH, *ctx.page);
+		return;
+	}
 	*ctx.numPages = 1;
 	ctx.pageBreaks[0] = 0;
 	const int lineH = _font.getFontHeight();
@@ -3623,19 +4026,25 @@ void EEMEngine::accuseDrawScreen(const AccuseNotesCtx &ctx) {
 	if (ctx.haveBg)
 		blitTravisBackground(scratch, *ctx.accuseBg, mac);
 
-	blitPdaPartner(scratch, _aniArchive, _partner, kPdaGalleryPartner,
-				   g_system->getMillis(), mac);
+	blitPdaPartner(scratch, _aniArchive, _partner,
+				   isMacCD() ? kPdaNotebookPartner : kPdaGalleryPartner,
+				   g_system->getMillis(), mac, isMacCD());
 
 	Common::Array<Common::Rect> &slotRects = *ctx.slotRects;
 	Common::Array<uint> &slotClues = *ctx.slotClues;
 	const Common::Array<uint> &found = *ctx.found;
 	slotRects.clear();
 	slotClues.clear();
-	const int lineH = _font.getFontHeight();
-	const int startIdx = ctx.pageBreaks[*ctx.page];
-	const int endIdx   = (*ctx.page + 1 < *ctx.numPages)
+	const int lineH = isMacCD() ? kMacCDPdaLineHeight : _font.getFontHeight();
+	const int insetX = isMacCD() ? kMacCDPdaTextInsetX : 0;
+	const int insetY = isMacCD() ? kMacCDPdaTextInsetY : 0;
+	const int textWidth = ctx.rectW - insetX;
+	const int startIdx = ctx.scrollBar ? *ctx.page : ctx.pageBreaks[*ctx.page];
+	const int endIdx   = ctx.scrollBar ? ctx.scrollBar->end(*ctx.page) : (*ctx.page + 1 < *ctx.numPages)
 		? ctx.pageBreaks[*ctx.page + 1]
 		: (int)found.size();
+	Graphics::ManagedSurface textSurface(scratch, Common::Rect(ctx.rectX, ctx.rectY,
+		ctx.rectX + ctx.rectW, ctx.rectY + ctx.rectH));
 	int y = ctx.rectY;
 	uint selectedCount = 0;
 	for (uint i = 0; i < found.size(); i++) {
@@ -3651,15 +4060,16 @@ void EEMEngine::accuseDrawScreen(const AccuseNotesCtx &ctx) {
 			txt = Common::String::format(
 				isSpanish() ? "nota %u" : "clue %u", clueId);
 		Common::Array<Common::String> wrapped;
-		_font.wordWrapText(txt, ctx.rectW, wrapped);
+		_font.wordWrapText(txt, textWidth, wrapped);
 		const int h = (int)wrapped.size() * lineH;
 		const byte color = _mystery._noteSelected[clueId] ? 0x3c : 0x01;
 		for (uint li = 0; li < wrapped.size(); li++) {
-			_font.drawString(&scratch, wrapped[li], ctx.rectX,
-							 y + (int)li * lineH, ctx.rectW, color);
+			_font.drawString(&textSurface, wrapped[li], insetX,
+							 y - ctx.rectY + insetY + (int)li * lineH, textWidth, color);
 		}
 		slotRects.push_back(Common::Rect(ctx.rectX, y,
-										  ctx.rectX + ctx.rectW, y + h));
+			ctx.rectX + ctx.rectW - (isMacCD() ? 12 : 0),
+			MIN(y + h + (isMacCD() ? 6 : 0), ctx.rectY + ctx.rectH)));
 		slotClues.push_back(clueId);
 		y += h + 7;
 	}
@@ -3673,14 +4083,20 @@ void EEMEngine::accuseDrawScreen(const AccuseNotesCtx &ctx) {
 		: (remaining == 1 ? "clue" : "clues");
 	const Common::String counter =
 		Common::String::format("%u %s", remaining, clueWord);
-	_font.drawString(&scratch, counter, scaleX(209), scaleY(11),
-					 scaleX(100), 0x0F);
+	if (isMacCD()) {
+		_font.drawString(&scratch, Common::String::format("%u", remaining), 334, 23, 16, 0x23);
+		_font.drawString(&scratch, clueWord, 350, 23, 120, 0x0f);
+	} else {
+		_font.drawString(&scratch, counter, scaleX(209), scaleY(11), scaleX(100), 0x0f);
+	}
 
-	if (*ctx.numPages > 1) {
+	if (!isMacCD() && *ctx.numPages > 1) {
 		_font.drawString(&scratch,
 			Common::String::format("p%d/%d", *ctx.page + 1, *ctx.numPages),
 			ctx.rectX, scaleY(11), scaleX(60), 0x0F);
 	}
+	if (ctx.scrollBar)
+		ctx.scrollBar->draw(scratch, *ctx.page);
 
 	g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
 							   0, 0, sw, sh);
@@ -3725,7 +4141,7 @@ bool EEMEngine::doAccuseNotes() {
 
 	const Common::Rect noteRectBase(79, 27, 304, 159);
 	const Common::Rect noteRect =
-		isMacintosh() ? scaleRect(noteRectBase) : noteRectBase;
+		isMacCD() ? kMacCDAccuseTextRect : (isMacintosh() ? scaleRect(noteRectBase) : noteRectBase);
 	const int rectX = noteRect.left;
 	const int rectY = noteRect.top;
 	const int rectW = noteRect.width();
@@ -3745,6 +4161,7 @@ bool EEMEngine::doAccuseNotes() {
 	Common::Array<uint> slotClues;
 
 	int page = 0;
+	TravisScrollBar macScroll(kMacCDPdaScrollRect);
 	int pageBreaks[16];
 	int numPages = 1;
 	pageBreaks[0] = 0;
@@ -3771,6 +4188,7 @@ bool EEMEngine::doAccuseNotes() {
 	ctx.pageBreaksCap = (int)ARRAYSIZE(pageBreaks);
 	ctx.numPages      = &numPages;
 	ctx.page          = &page;
+	ctx.scrollBar     = isMacCD() ? &macScroll : nullptr;
 
 	accuseRebuildPagination(ctx);
 	accuseDrawScreen(ctx);
@@ -3779,16 +4197,24 @@ bool EEMEngine::doAccuseNotes() {
 							  pdaControlRect(this, kPdaNotebookRect)
 								  .contains(mouse.x, mouse.y) ||
 							  rectListContains(slotRects, mouse.x, mouse.y));
+	uint32 lastDraw = g_system->getMillis();
+	uint32 gizmoLastTick = lastDraw;
 
 	while (!shouldQuit()) {
 		Common::Event ev;
 		bool dirty = false;
 		while (g_system->getEventManager()->pollEvent(ev)) {
+			if (isMacCD() && (handleMacCDPdaKey(ev, &slotRects, false) || trackMacCDPdaButton(*this, ev)))
+				continue;
 			if (ev.type == Common::EVENT_QUIT ||
 				ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
 				_nextScreen = kScreenInvalid;
 				return false;
 			}
+			if (ctx.scrollBar && ctx.scrollBar->handleEvent(ev, page)) {
+				dirty = true;
+				break;
+			}
 			if (ev.type == Common::EVENT_MOUSEMOVE) {
 				setInteractiveMouseCursor(
 					notebookButtonAt(this, ev.mouse.x, ev.mouse.y) ||
@@ -3815,7 +4241,7 @@ bool EEMEngine::doAccuseNotes() {
 			if (ev.type == Common::EVENT_LBUTTONDOWN) {
 				const int mx = ev.mouse.x;
 				const int my = ev.mouse.y;
-				if (pdaControlRect(this, kPdaSiteRect).contains(mx, my)) {
+				if (pdaSiteButtonAt(this, mx, my)) {
 					_nextScreen = kScreenSite;
 					return false;
 				}
@@ -3832,7 +4258,7 @@ bool EEMEngine::doAccuseNotes() {
 					_nextScreen = kScreenGallery;
 					return false;
 				}
-				if (isLondon() &&
+				if ((isLondon() || isMacCD()) &&
 					pdaControlRect(this, kPdaHelpRect).contains(mx, my)) {
 					_nextScreen = kScreenMapAlt;
 					return false;
@@ -3888,7 +4314,7 @@ bool EEMEngine::doAccuseNotes() {
 						if (_mystery._noteSelected[found[i]])
 							selected++;
 					}
-					if (selected == expected)
+					if (isMacCD() || selected == expected)
 						return true;
 					continue;
 				}
@@ -3896,31 +4322,36 @@ bool EEMEngine::doAccuseNotes() {
 				for (uint i = 0; i < slotRects.size(); i++) {
 					if (slotRects[i].contains(mx, my)) {
 						const uint clueId = slotClues[i];
-						if (!_mystery._noteSelected[clueId]) {
-							uint selected = 0;
-							for (uint j = 0; j < found.size(); j++) {
-								if (_mystery._noteSelected[found[j]])
-									selected++;
-							}
-							if (selected >= expected)
-								break;
+						uint selected = 0;
+						for (uint j = 0; j < found.size(); j++) {
+							if (_mystery._noteSelected[found[j]])
+								selected++;
 						}
+						if (!_mystery._noteSelected[clueId] && selected >= expected)
+							break;
 						_mystery._noteSelected[clueId] =
 							_mystery._noteSelected[clueId] ? 0 : 1;
+						if (isMacCD() && _mystery._noteSelected[clueId] && selected + 1 == expected) {
+							accuseDrawScreen(ctx);
+							return true;
+						}
 						dirty = true;
 						break;
 					}
 				}
 			}
 		}
-		if (dirty)
-			accuseDrawScreen(ctx);
-		static uint32 sLastTick = 0;
+		if (ctx.scrollBar && ctx.scrollBar->update(page))
+			dirty = true;
 		const uint32 now = g_system->getMillis();
-		if (now - sLastTick >= 100) {
-			sLastTick = now;
+		if (dirty || now - lastDraw >= 100) {
+			lastDraw = now;
 			accuseDrawScreen(ctx);
 		}
+		if (isMacCD() && now - gizmoLastTick >= 150) {
+			gizmoLastTick = now;
+			cycleTravisPalette(true);
+		}
 		g_system->updateScreen();
 		g_system->delayMillis(15);
 	}
@@ -4183,10 +4614,22 @@ void EEMEngine::doAccuse() {
 
 	int picked = -1;
 	uint32 lastTick = g_system->getMillis();
+	uint32 gizmoLastTick = lastTick;
 	bool dirty = false;
 	while (picked < 0 && !shouldQuit()) {
 		Common::Event ev;
 		while (g_system->getEventManager()->pollEvent(ev)) {
+			if (isMacCD()) {
+				if (ev.type == Common::EVENT_KEYDOWN &&
+					(ev.kbd.keycode == Common::KEYCODE_LEFT || ev.kbd.keycode == Common::KEYCODE_RIGHT)) {
+					ev.kbd.flags = ev.kbd.keycode == Common::KEYCODE_LEFT ? Common::KBD_SHIFT : 0;
+					ev.kbd.keycode = Common::KEYCODE_TAB;
+				}
+				if (handleMacCDPdaKey(ev, &slotRects, false))
+					continue;
+				if (ev.type == Common::EVENT_MOUSEMOVE)
+					setInteractiveMouseCursor(rectListContains(slotRects, ev.mouse.x, ev.mouse.y));
+			}
 			if (ev.type == Common::EVENT_QUIT ||
 				ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
 				return;
@@ -4242,6 +4685,10 @@ void EEMEngine::doAccuse() {
 			lastTick = now;
 			dirty = false;
 		}
+		if (isMacCD() && now - gizmoLastTick >= 150) {
+			gizmoLastTick = now;
+			cycleTravisPalette(true);
+		}
 		g_system->updateScreen();
 		g_system->delayMillis(10);
 	}
@@ -5161,11 +5608,12 @@ void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
 		Graphics::PixelFormat::createFormatCLUT8());
 	scratch.clear();
 	if (haveAccuseBg)
-		scratch.simpleBlitFrom(accuseBg.surface);
+		blitTravisBackground(scratch, accuseBg, mac);
 
 	// Partner drawn first; defensive (no slot overlap).
-	blitPdaPartner(scratch, _aniArchive, _partner, kPdaGalleryPartner,
-				   g_system->getMillis(), mac);
+	blitPdaPartner(scratch, _aniArchive, _partner,
+				   isMacCD() ? kPdaNotebookPartner : kPdaGalleryPartner,
+				   g_system->getMillis(), mac, isMacCD());
 	const GallerySlot * const slots = mac ? kMacGallerySlots : kGallerySlots;
 
 	for (uint i = 0; i < numSuspects && i < Mystery::kGalleryCap; i++) {
@@ -5193,7 +5641,8 @@ void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
 			continue;
 
 		const int placeX = s.x;
-		const int placeY = mac ? s.y : s.y + (0x48 - portrait.surface.h);
+		const int placeY = isMacCD() ? s.y + 138 - portrait.surface.h :
+			(mac ? s.y : s.y + (0x48 - portrait.surface.h));
 		const int w = MIN<int>(portrait.surface.w, sw - placeX);
 		const int h = MIN<int>(portrait.surface.h, sh - placeY);
 		if (w <= 0 || h <= 0)
@@ -5211,7 +5660,7 @@ void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
 	}
 
 	// Highlight outline (original uses `_PutMouseInRect` @ 1df2:0b8e).
-	if (highlighted >= 0 && highlighted < (int)slotRects.size() &&
+	if (!isMacCD() && highlighted >= 0 && highlighted < (int)slotRects.size() &&
 		!slotRects[highlighted].isEmpty()) {
 		Common::Rect r = slotRects[highlighted];
 		r.grow(1);


Commit: be891235d79da1794d582555aef45853886bf08f
    https://github.com/scummvm/scummvm/commit/be891235d79da1794d582555aef45853886bf08f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-10T17:45:23+02:00

Commit Message:
EEM: extend TRAVIS scroll bar code for EEM2 (mac)

Changed paths:
    engines/eem/ui.cpp


diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index a70c01a7a9d..99375d64ace 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -271,6 +271,7 @@ constexpr Common::Rect kPdaLondonCloseRect(Common::Point(0, 0), 66, 79);
 // Mac _NotebookRect and the TRAVIS button table use native QuickDraw rects;
 // keep them exact instead of deriving them from the rounded DOS scaler.
 constexpr Common::Rect kMacNotebookTextRect(Common::Point(125, 23), 336, 269);
+constexpr Common::Rect kMacLondonNotebookTextRect(Common::Point(125, 23), 331, 274);
 constexpr Common::Rect kMacPdaHelpRect(Common::Point(164, 336), 33, 28);
 constexpr Common::Rect kMacPdaNotebookRect(Common::Point(247, 336), 34, 28);
 constexpr Common::Rect kMacPdaGalleryRect(Common::Point(330, 336), 34, 28);
@@ -289,13 +290,17 @@ constexpr Common::Rect kMacCDPdaMapRect(Common::Point(149, 334), 35, 30);
 constexpr Common::Rect kMacCDPdaNotebookRect(Common::Point(249, 334), 36, 30);
 constexpr Common::Rect kMacCDPdaGalleryRect(Common::Point(327, 334), 35, 30);
 constexpr Common::Rect kMacCDPdaHelpRect(Common::Point(426, 334), 36, 30);
-constexpr Common::Rect kMacCDPdaScrollRect(Common::Point(476, 19), 16, 294);
-constexpr Common::Rect kMacCDSuspectScrollRect(Common::Point(476, 179), 16, 113);
-constexpr Common::Rect kMacCDSuspectTextRect(Common::Point(125, 179), 326, 113);
-constexpr Common::Rect kMacCDAccuseTextRect(Common::Point(126, 52), 346, 259);
-constexpr int kMacCDPdaLineHeight = 16;
-constexpr int kMacCDPdaTextInsetX = 5;
-constexpr int kMacCDPdaTextInsetY = 4;
+constexpr Common::Rect kMacTravisScrollRect(Common::Point(476, 19), 16, 294);
+constexpr Common::Rect kMacTravisSuspectScrollRect(Common::Point(476, 179), 16, 113);
+constexpr Common::Rect kMacTravisSuspectTextRect(Common::Point(125, 179), 326, 113);
+constexpr Common::Rect kMacTravisAccuseTextRect(Common::Point(126, 52), 346, 259);
+constexpr int kMacTravisLineHeight = 16;
+constexpr int kMacTravisTextInsetX = 5;
+constexpr int kMacTravisTextInsetY = 4;
+
+bool usesTravisScrollBar(const EEMEngine *vm) {
+	return vm && (vm->isMacCD() || (vm->isMacintosh() && vm->isLondon()));
+}
 
 class TravisScrollBar {
 public:
@@ -524,6 +529,8 @@ constexpr uint kRestoredContentLastMystery = 0x18;
 Common::Rect pdaControlRect(const EEMEngine *vm, const Common::Rect &rect) {
 	if (!vm || !vm->isMacintosh())
 		return rect;
+	if (usesTravisScrollBar(vm) && (rect == kPdaPageNextRect || rect == kPdaPagePrevRect))
+		return Common::Rect();
 	if (vm->isMacCD()) {
 		if (rect == kPdaNotebookRect)
 			return kMacCDPdaNotebookRect;
@@ -533,7 +540,7 @@ Common::Rect pdaControlRect(const EEMEngine *vm, const Common::Rect &rect) {
 			return kMacCDPdaGalleryRect;
 		if (rect == kPdaHelp2Rect)
 			return kMacCDPdaHelpRect;
-		if (rect == kPdaPageNextRect || rect == kPdaPagePrevRect || rect == kPdaPartnerFootMapRect)
+		if (rect == kPdaPartnerFootMapRect)
 			return Common::Rect();
 	}
 	if (rect == kPdaNotebookRect)
@@ -560,7 +567,7 @@ Common::Rect pdaControlRect(const EEMEngine *vm, const Common::Rect &rect) {
 }
 
 int macPdaScrollBarDelta(const EEMEngine *vm, int x, int y) {
-	if (!vm || !vm->isMacintosh() || vm->isMacCD() || !kMacPdaScrollBarRect.contains(x, y))
+	if (!vm || !vm->isMacintosh() || usesTravisScrollBar(vm) || !kMacPdaScrollBarRect.contains(x, y))
 		return 0;
 
 	static const Common::Rect kMacPdaScrollBarButtons[] = {
@@ -578,8 +585,8 @@ int macPdaScrollBarDelta(const EEMEngine *vm, int x, int y) {
 }
 
 bool macPdaScrollBarAt(const EEMEngine *vm, int x, int y) {
-	if (vm && vm->isMacCD())
-		return kMacCDPdaScrollRect.contains(x, y);
+	if (usesTravisScrollBar(vm))
+		return kMacTravisScrollRect.contains(x, y);
 	return macPdaScrollBarDelta(vm, x, y) != 0;
 }
 
@@ -589,16 +596,18 @@ bool pdaSiteButtonAt(const EEMEngine *vm, int x, int y) {
 		 !kMacPdaPartnerHeadHintRect.contains(x, y));
 }
 
-bool handleMacCDPdaKey(Common::Event &event, const Common::Array<Common::Rect> *items = nullptr,
-					  bool includeButtons = true) {
+bool handleMacTravisKey(const EEMEngine &vm, Common::Event &event,
+					   const Common::Array<Common::Rect> *items = nullptr, bool includeButtons = true) {
 	if (event.type != Common::EVENT_KEYDOWN)
 		return false;
 	const Common::Point mouse = g_system->getEventManager()->getMousePos();
 	if (event.kbd.keycode == Common::KEYCODE_TAB) {
 		const Common::Rect buttons[] = {
-			kMacCDPdaNotebookRect, kMacCDPdaGalleryRect, kMacPdaAccuseRect,
-			kMacCDPdaHelpRect, kMacCDPdaMapRect, kMacPdaSiteRect,
-			kMacPdaPartnerHeadHintRect, Common::Rect(0, 0, 106, 152)
+			pdaControlRect(&vm, kPdaNotebookRect), pdaControlRect(&vm, kPdaGalleryRect),
+			pdaControlRect(&vm, kPdaAccuseRect), pdaControlRect(&vm, kPdaHelp2Rect),
+			pdaControlRect(&vm, kPdaHelpRect), pdaControlRect(&vm, kPdaSiteRect),
+			pdaControlRect(&vm, kPdaPartnerHeadHintRect),
+			vm.isMacCD() ? Common::Rect(0, 0, 106, 152) : kMacPdaPartnerFootMapRect
 		};
 		Common::Array<Common::Rect> targets;
 		if (includeButtons) {
@@ -630,7 +639,7 @@ bool handleMacCDPdaKey(Common::Event &event, const Common::Array<Common::Rect> *
 		return false;
 	}
 	if (event.kbd.keycode == Common::KEYCODE_RETURN || event.kbd.keycode == Common::KEYCODE_KP_ENTER) {
-		if (kMacCDPdaScrollRect.contains(mouse))
+		if (kMacTravisScrollRect.contains(mouse))
 			return true;
 		event.type = Common::EVENT_LBUTTONDOWN;
 		event.mouse = mouse;
@@ -3025,8 +3034,8 @@ void EEMEngine::doNotebook() {
 	CursorMan.showMouse(true);
 
 	int page = 0;
-	TravisScrollBar macScroll(kMacCDPdaScrollRect);
-	TravisScrollBar *scrollBar = isMacCD() ? &macScroll : nullptr;
+	TravisScrollBar macScroll(kMacTravisScrollRect);
+	TravisScrollBar *scrollBar = usesTravisScrollBar(this) ? &macScroll : nullptr;
 
 	const bool notebookFromSite = isLondon() && _lastScreen == kScreenSite;
 	if (_music && _voiceOn && notebookFromSite)
@@ -3056,7 +3065,8 @@ void EEMEngine::doNotebook() {
 		bool dirty = false;
 		bool exitFlag = false;
 		while (g_system->getEventManager()->pollEvent(ev)) {
-			if (isMacCD() && (handleMacCDPdaKey(ev) || trackMacCDPdaButton(*this, ev)))
+			if ((scrollBar && handleMacTravisKey(*this, ev)) ||
+				(isMacCD() && trackMacCDPdaButton(*this, ev)))
 				continue;
 			if (ev.type == Common::EVENT_QUIT ||
 				ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
@@ -3235,7 +3245,7 @@ Common::String EEMEngine::notebookNoteText(uint clueId, const byte *ni,
 void EEMEngine::drawNotebookFrame(int &page, TravisScrollBar *scrollBar) {
 	const Common::Rect kNotebookRect(78, 12, 288, 152);
 	const Common::Rect notebookRect =
-		isMacintosh() ? kMacNotebookTextRect : kNotebookRect;
+		isMacintosh() ? (isLondon() ? kMacLondonNotebookTextRect : kMacNotebookTextRect) : kNotebookRect;
 	const int sw = screenWidth();
 	const int sh = screenHeight();
 
@@ -3274,10 +3284,10 @@ void EEMEngine::drawNotebookFrame(int &page, TravisScrollBar *scrollBar) {
 	const int kRectY = notebookRect.top;
 	const int kRectW = notebookRect.width();
 	const int kRectH = notebookRect.height();
-	const int insetX = isMacCD() ? kMacCDPdaTextInsetX : 0;
-	const int insetY = isMacCD() ? kMacCDPdaTextInsetY : 0;
+	const int insetX = scrollBar ? kMacTravisTextInsetX : 0;
+	const int insetY = scrollBar ? kMacTravisTextInsetY : 0;
 	const int textWidth = kRectW - insetX;
-	const int lineH = isMacCD() ? kMacCDPdaLineHeight : _font.getFontHeight();
+	const int lineH = scrollBar ? kMacTravisLineHeight : _font.getFontHeight();
 
 	int clueCursor = 0;
 	Common::Array<int> pageStarts;
@@ -3331,7 +3341,7 @@ void EEMEngine::drawNotebookFrame(int &page, TravisScrollBar *scrollBar) {
 		Common::Array<Common::String> wrapped;
 		_font.wordWrapText(txt, textWidth, wrapped);
 		const int h = (int)wrapped.size() * lineH;
-		const byte color = !isMacCD() && _mystery._noteSelected[clueId] ? 0x3C : 0x5C;
+		const byte color = !scrollBar && _mystery._noteSelected[clueId] ? 0x3C : 0x5C;
 		for (uint li = 0; li < wrapped.size(); li++) {
 			_font.drawString(&textSurface, wrapped[li], insetX,
 							 y - kRectY + insetY + (int)li * lineH, textWidth, color);
@@ -3340,7 +3350,7 @@ void EEMEngine::drawNotebookFrame(int &page, TravisScrollBar *scrollBar) {
 	}
 
 	const bool isLastPage = (page + 1 >= (int)pageStarts.size());
-	if (isLastPage && !isMacCD()) {
+	if (isLastPage && !scrollBar) {
 		const char *kEndMarker = isSpanish()
 			? "-- Fin de las notas --"
 			: "-- End of notes --";
@@ -3396,7 +3406,7 @@ void EEMEngine::doGallery() {
 		Common::Event ev;
 		bool exitFlag = false;
 		while (g_system->getEventManager()->pollEvent(ev)) {
-			if (isMacCD() && (handleMacCDPdaKey(ev, &slotRects) || trackMacCDPdaButton(*this, ev)))
+			if (isMacCD() && (handleMacTravisKey(*this, ev, &slotRects) || trackMacCDPdaButton(*this, ev)))
 				continue;
 			if (ev.type == Common::EVENT_QUIT ||
 				ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
@@ -3535,6 +3545,7 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 						  const Picture &galBg, bool haveBg) {
 	const bool floppyMI = isFloppy();
 	const bool mac = isMacintosh();
+	const bool useScrollBar = usesTravisScrollBar(this);
 	const bool compactMI = floppyMI || _mystery.usesCompactMacData();
 	const byte *suspect = compactMI
 							  ? _mystery.floppySuspectEntry(suspectIdx)
@@ -3549,7 +3560,7 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 	setInteractiveMouseCursor(false);
 
 	const Common::Rect noteRectBase(78, 93, 288, 152);
-	const Common::Rect noteRect = isMacCD() ? kMacCDSuspectTextRect :
+	const Common::Rect noteRect = useScrollBar ? kMacTravisSuspectTextRect :
 		(mac ? scaleRect(noteRectBase) : noteRectBase);
 	const int rx = noteRect.left;
 	const int ry = noteRect.top;
@@ -3557,9 +3568,9 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 	const int rh = noteRect.height();
 	const int sw = screenWidth();
 	const int sh = screenHeight();
-	const int lineH = isMacCD() ? kMacCDPdaLineHeight : _font.getFontHeight();
-	const int insetX = isMacCD() ? kMacCDPdaTextInsetX : 0;
-	const int insetY = isMacCD() ? kMacCDPdaTextInsetY : 0;
+	const int lineH = useScrollBar ? kMacTravisLineHeight : _font.getFontHeight();
+	const int insetX = useScrollBar ? kMacTravisTextInsetX : 0;
+	const int insetY = useScrollBar ? kMacTravisTextInsetY : 0;
 	const int textWidth = MAX<int>(8, rw - insetX);
 	const uint clueMax = compactMI ? clueCount : 30u;
 	const byte *ni = _mystery.noteIndex();
@@ -3569,12 +3580,12 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 
 	int pageStart = 0;
 	Common::Array<int> pageStack;
-	TravisScrollBar macScroll(kMacCDSuspectScrollRect);
+	TravisScrollBar macScroll(kMacTravisSuspectScrollRect);
 	Common::Array<uint> macClues;
-	if (isMacCD()) {
+	if (useScrollBar) {
 		Common::Array<int> heights;
 		for (uint i = 0; i < clueCount && i < clueMax; ++i) {
-			const uint clueId = READ_LE_UINT16(suspect + 0xa + i * 2);
+			const uint clueId = compactMI ? suspect[5 + i] : READ_LE_UINT16(suspect + 0xa + i * 2);
 			if (clueId >= Mystery::kCluesFoundCap || clueId >= niCount || !_mystery._cluesFound[clueId])
 				continue;
 			const Common::String text = notebookNoteText(clueId, ni, niCount, false,
@@ -3588,7 +3599,7 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 		}
 		macScroll.layout(heights, rh, pageStart);
 	}
-	const uint entryCount = isMacCD() ? macClues.size() : MIN(clueCount, clueMax);
+	const uint entryCount = useScrollBar ? macClues.size() : MIN(clueCount, clueMax);
 	bool back = false;
 	bool exitGallery = false;
 	bool isFirstShow = true;
@@ -3622,9 +3633,9 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 		bool drewAny = false;
 		uint k = pageStart;
 		bool reachedEnd = false;
-		const uint end = isMacCD() ? macScroll.end(pageStart) : entryCount;
+		const uint end = useScrollBar ? macScroll.end(pageStart) : entryCount;
 		for (; k < end; k++) {
-			const uint16 clueId = isMacCD() ? macClues[k] : compactMI
+			const uint16 clueId = useScrollBar ? macClues[k] : compactMI
 				? (uint16)suspect[5 + k]
 				: READ_LE_UINT16(suspect + 0xa + k * 2);
 			if (!compactMI && clueId == 0xFFFF) {
@@ -3651,7 +3662,7 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 				// Defer to next page.
 				break;
 			}
-			const byte color = !isMacCD() && _mystery._noteSelected[clueId] ? 0x3C : 0x5C;
+			const byte color = !useScrollBar && _mystery._noteSelected[clueId] ? 0x3C : 0x5C;
 			for (uint l = 0; l < wrapped.size(); l++) {
 				_font.drawString(&textSurface, wrapped[l], insetX,
 					yPos - ry + insetY + (int)l * lineH, textWidth, color);
@@ -3665,7 +3676,7 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 		const bool hasMore = !reachedEnd;
 		const bool hasPrev = !pageStack.empty();
 
-		if (!isMacCD() && pageStart == 0 && !drewAny && _font.isLoaded()) {
+		if (!useScrollBar && pageStart == 0 && !drewAny && _font.isLoaded()) {
 			_font.drawString(&ms,
 				isSpanish()
 					? "Aun no hay pistas para este sospechoso."
@@ -3673,7 +3684,7 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 				rx, ry, MAX<int>(8, rw), 0x5C);
 		}
 		// Header / footer text.
-		if (!isMacCD() && _font.isLoaded()) {
+		if (!useScrollBar && _font.isLoaded()) {
 			_font.drawString(&ms,
 				isSpanish() ? "EXPEDIENTE" : "SUSPECT FILE",
 				rx, ry - 11, MAX<int>(8, rw), 0x3C);
@@ -3681,7 +3692,7 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 				isSpanish() ? "(ESC: volver)" : "(ESC: back)",
 				rx, ry + rh + 2, MAX<int>(8, rw), 0x3C);
 		}
-		if (isMacCD())
+		if (useScrollBar)
 			macScroll.draw(ms, pageStart);
 		g_system->copyRectToScreen(ms.getPixels(), ms.pitch,
 			0, 0, sw, sh);
@@ -3708,7 +3719,8 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 		while (!back && !advance && !prev && !redraw && !shouldQuit()) {
 			Common::Event e2;
 			while (g_system->getEventManager()->pollEvent(e2)) {
-				if (isMacCD() && (handleMacCDPdaKey(e2) || trackMacCDPdaButton(*this, e2)))
+				if ((useScrollBar && handleMacTravisKey(*this, e2)) ||
+					(isMacCD() && trackMacCDPdaButton(*this, e2)))
 					continue;
 				if (e2.type == Common::EVENT_QUIT ||
 					e2.type == Common::EVENT_RETURN_TO_LAUNCHER) {
@@ -3721,7 +3733,7 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 					redraw = true;
 					break;
 				}
-				if (isMacCD() && macScroll.handleEvent(e2, pageStart)) {
+				if (useScrollBar && macScroll.handleEvent(e2, pageStart)) {
 					redraw = true;
 					break;
 				}
@@ -3730,7 +3742,7 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 						macPdaScrollBarDelta(this, e2.mouse.x, e2.mouse.y);
 					setInteractiveMouseCursor(
 						galleryButtonAt(this, e2.mouse.x, e2.mouse.y) ||
-						(isMacCD() && macScroll.contains(e2.mouse)) ||
+						(useScrollBar && macScroll.contains(e2.mouse)) ||
 						(scrollDelta < 0 && hasPrev) ||
 						(scrollDelta > 0 && hasMore));
 				}
@@ -3840,7 +3852,7 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 				gizmoLastTick = now;
 				cycleTravisPalette(isMacCD());
 			}
-			if (isMacCD() && (macScroll.update(pageStart) || now - lastDraw >= 150))
+			if (useScrollBar && (macScroll.update(pageStart) || now - lastDraw >= 150))
 				redraw = true;
 			g_system->updateScreen();
 			g_system->delayMillis(20);
@@ -3982,8 +3994,8 @@ void EEMEngine::accuseRebuildPagination(const AccuseNotesCtx &ctx) {
 		Common::Array<int> heights;
 		for (uint i = 0; i < ctx.found->size(); ++i) {
 			Common::Array<Common::String> lines;
-			_font.wordWrapText(accuseNoteText((*ctx.found)[i], ctx), ctx.rectW - kMacCDPdaTextInsetX, lines);
-			heights.push_back(lines.size() * kMacCDPdaLineHeight);
+			_font.wordWrapText(accuseNoteText((*ctx.found)[i], ctx), ctx.rectW - kMacTravisTextInsetX, lines);
+			heights.push_back(lines.size() * kMacTravisLineHeight);
 		}
 		ctx.scrollBar->layout(heights, ctx.rectH, *ctx.page);
 		return;
@@ -4035,9 +4047,9 @@ void EEMEngine::accuseDrawScreen(const AccuseNotesCtx &ctx) {
 	const Common::Array<uint> &found = *ctx.found;
 	slotRects.clear();
 	slotClues.clear();
-	const int lineH = isMacCD() ? kMacCDPdaLineHeight : _font.getFontHeight();
-	const int insetX = isMacCD() ? kMacCDPdaTextInsetX : 0;
-	const int insetY = isMacCD() ? kMacCDPdaTextInsetY : 0;
+	const int lineH = ctx.scrollBar ? kMacTravisLineHeight : _font.getFontHeight();
+	const int insetX = ctx.scrollBar ? kMacTravisTextInsetX : 0;
+	const int insetY = ctx.scrollBar ? kMacTravisTextInsetY : 0;
 	const int textWidth = ctx.rectW - insetX;
 	const int startIdx = ctx.scrollBar ? *ctx.page : ctx.pageBreaks[*ctx.page];
 	const int endIdx   = ctx.scrollBar ? ctx.scrollBar->end(*ctx.page) : (*ctx.page + 1 < *ctx.numPages)
@@ -4068,8 +4080,8 @@ void EEMEngine::accuseDrawScreen(const AccuseNotesCtx &ctx) {
 							 y - ctx.rectY + insetY + (int)li * lineH, textWidth, color);
 		}
 		slotRects.push_back(Common::Rect(ctx.rectX, y,
-			ctx.rectX + ctx.rectW - (isMacCD() ? 12 : 0),
-			MIN(y + h + (isMacCD() ? 6 : 0), ctx.rectY + ctx.rectH)));
+			ctx.rectX + ctx.rectW - (ctx.scrollBar ? 12 : 0),
+			MIN(y + h + (ctx.scrollBar ? 6 : 0), ctx.rectY + ctx.rectH)));
 		slotClues.push_back(clueId);
 		y += h + 7;
 	}
@@ -4090,7 +4102,7 @@ void EEMEngine::accuseDrawScreen(const AccuseNotesCtx &ctx) {
 		_font.drawString(&scratch, counter, scaleX(209), scaleY(11), scaleX(100), 0x0f);
 	}
 
-	if (!isMacCD() && *ctx.numPages > 1) {
+	if (!ctx.scrollBar && *ctx.numPages > 1) {
 		_font.drawString(&scratch,
 			Common::String::format("p%d/%d", *ctx.page + 1, *ctx.numPages),
 			ctx.rectX, scaleY(11), scaleX(60), 0x0F);
@@ -4141,7 +4153,7 @@ bool EEMEngine::doAccuseNotes() {
 
 	const Common::Rect noteRectBase(79, 27, 304, 159);
 	const Common::Rect noteRect =
-		isMacCD() ? kMacCDAccuseTextRect : (isMacintosh() ? scaleRect(noteRectBase) : noteRectBase);
+		usesTravisScrollBar(this) ? kMacTravisAccuseTextRect : (isMacintosh() ? scaleRect(noteRectBase) : noteRectBase);
 	const int rectX = noteRect.left;
 	const int rectY = noteRect.top;
 	const int rectW = noteRect.width();
@@ -4161,7 +4173,7 @@ bool EEMEngine::doAccuseNotes() {
 	Common::Array<uint> slotClues;
 
 	int page = 0;
-	TravisScrollBar macScroll(kMacCDPdaScrollRect);
+	TravisScrollBar macScroll(kMacTravisScrollRect);
 	int pageBreaks[16];
 	int numPages = 1;
 	pageBreaks[0] = 0;
@@ -4188,7 +4200,7 @@ bool EEMEngine::doAccuseNotes() {
 	ctx.pageBreaksCap = (int)ARRAYSIZE(pageBreaks);
 	ctx.numPages      = &numPages;
 	ctx.page          = &page;
-	ctx.scrollBar     = isMacCD() ? &macScroll : nullptr;
+	ctx.scrollBar     = usesTravisScrollBar(this) ? &macScroll : nullptr;
 
 	accuseRebuildPagination(ctx);
 	accuseDrawScreen(ctx);
@@ -4204,7 +4216,8 @@ bool EEMEngine::doAccuseNotes() {
 		Common::Event ev;
 		bool dirty = false;
 		while (g_system->getEventManager()->pollEvent(ev)) {
-			if (isMacCD() && (handleMacCDPdaKey(ev, &slotRects, false) || trackMacCDPdaButton(*this, ev)))
+			if ((ctx.scrollBar && handleMacTravisKey(*this, ev, &slotRects, false)) ||
+				(isMacCD() && trackMacCDPdaButton(*this, ev)))
 				continue;
 			if (ev.type == Common::EVENT_QUIT ||
 				ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
@@ -4625,7 +4638,7 @@ void EEMEngine::doAccuse() {
 					ev.kbd.flags = ev.kbd.keycode == Common::KEYCODE_LEFT ? Common::KBD_SHIFT : 0;
 					ev.kbd.keycode = Common::KEYCODE_TAB;
 				}
-				if (handleMacCDPdaKey(ev, &slotRects, false))
+				if (handleMacTravisKey(*this, ev, &slotRects, false))
 					continue;
 				if (ev.type == Common::EVENT_MOUSEMOVE)
 					setInteractiveMouseCursor(rectListContains(slotRects, ev.mouse.x, ev.mouse.y));


Commit: b141102aaf93fdb1e2f771c4ee94e4a3f1c78822
    https://github.com/scummvm/scummvm/commit/b141102aaf93fdb1e2f771c4ee94e4a3f1c78822
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-10T17:45:23+02:00

Commit Message:
EEM: review and improved case workflow for EEM1 (mac)

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


diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 78da5855fd7..da80872b1b4 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -944,6 +944,7 @@ void EEMEngine::displayClue(const byte *clueBlock) {
 
 	const uint stride = isLondon() ? 0x54 : 62;
 	const bool mac = isMacintosh();
+	const bool macSolved = isMacCD() && clueBlock == _mystery.solvedClueBlock();
 	const int sw = screenWidth();
 	const int sh = screenHeight();
 	MacSpritePaletteMap macPaletteMap = {0x00, 0xFF};
@@ -964,6 +965,8 @@ void EEMEngine::displayClue(const byte *clueBlock) {
 			g_system->unlockScreen();
 		}
 	}
+	if (macSolved && !_partnerEraseBg.empty())
+		bg.simpleBlitFrom(_partnerEraseBg);
 
 	// ClueEntry layout. EEM1 entries are 62 bytes; EEM2/London entries
 	// extend this to 0x54 bytes and move the side-effect lists below.
@@ -978,6 +981,8 @@ void EEMEngine::displayClue(const byte *clueBlock) {
 	//       5 notebook entries (-1 terminated)
 	//   EEM1 +0x3a / EEM2 +0x4e: KD-anim number (-1 = none)
 	for (uint i = 0; i < number && !shouldQuit(); i++) {
+		if (isMacCD() && _audio)
+			_audio->stopSpool();
 		g_system->copyRectToScreen(bg.getPixels(), bg.pitch, 0, 0, sw, sh);
 		const byte *c = clueBlock + 4 + i * stride;
 
@@ -996,10 +1001,11 @@ void EEMEngine::displayClue(const byte *clueBlock) {
 		uint16 kdAnimId = 0;
 		const bool haveKd = kdAnimNum != -1 &&
 			loadKdAnim((uint16)kdAnimNum, kdAnim, kdPx, kdPy, kdAnimId);
+		const bool animatePartner = haveKd || (macSolved && _hasPartnerIdle);
 
 		// Animate the gesture over the partner-less scene so it doesn't ghost
 		// the static partner.
-		if (haveKd && _partnerEraseBg.w == sw &&
+		if (animatePartner && _partnerEraseBg.w == sw &&
 			_partnerEraseBg.h == sh) {
 			g_system->copyRectToScreen(_partnerEraseBg.getPixels(),
 				_partnerEraseBg.pitch, 0, 0, sw, sh);
@@ -1019,11 +1025,12 @@ void EEMEngine::displayClue(const byte *clueBlock) {
 		// Speaker portrait: pic[clues + i*62 - 2]. Entry 0 ID is in
 		// ClueBlock +2; entries N>0 read (entry-1)+0x3c (last word).
 		const uint16 charX  = READ_LE_UINT16(c + (useP1 ? 4 : 0));
-		const uint16 charY  = READ_LE_UINT16(c + (useP1 ? 6 : 2));
+		// Mac CD adds 7.68 to the portrait Y and rounds to an integer.
+		const int charY = READ_LE_UINT16(c + (useP1 ? 6 : 2)) + (isMacCD() ? 8 : 0);
 		uint16 charPicId = (i == 0)
 			? READ_LE_UINT16(clueBlock + 2)
 			: READ_LE_UINT16(c - 2);
-		if (isLondon() && charPicId == 0x13e &&
+		if ((isLondon() || isMacCD()) && charPicId == 0x13e &&
 			_partner == kPartnerJake)
 			charPicId = 0x13f;
 		if (charPicId != 0 && charPicId != 0xFFFF) {
@@ -1097,12 +1104,16 @@ void EEMEngine::displayClue(const byte *clueBlock) {
 				copyY = bubY;
 			}
 
-			if (mac && textColor != 0xFF) {
+			if (isMacCD()) {
+				dialogFont.drawMacWordWrapped(&scratch, textX, textY,
+					MAX<int>(8, textW), text, textColor);
+			} else {
+				if (mac && textColor != 0xFF)
+					dialogFont.drawWordWrapped(&scratch, textX, textY,
+						MAX<int>(8, textW), text, 0xFF);
 				dialogFont.drawWordWrapped(&scratch, textX, textY,
-					MAX<int>(8, textW), text, 0xFF);
+					MAX<int>(8, textW), text, textColor);
 			}
-			dialogFont.drawWordWrapped(&scratch, textX, textY,
-				MAX<int>(8, textW), text, textColor);
 
 			copyY = CLIP<int>(copyY, 0, sh - 1);
 			const int copyRows = CLIP<int>(MIN<int>(copyH, sh - copyY),
@@ -1112,7 +1123,7 @@ void EEMEngine::displayClue(const byte *clueBlock) {
 					scratch.pitch, 0, copyY, sw, copyRows);
 				// Gesture entry: let the wait loop present, so the partner-less
 				// base isn't flashed before the gesture's first frame.
-				if (!haveKd)
+				if (!animatePartner)
 					g_system->updateScreen();
 			}
 		}
@@ -1139,7 +1150,7 @@ void EEMEngine::displayClue(const byte *clueBlock) {
 			bool haveKdBase = false;
 			uint kdLastFrame = (uint)-1;
 			const uint32 kdStartMs = g_system->getMillis();
-			if (haveKd) {
+			if (animatePartner) {
 				Graphics::Surface *kdScr = g_system->lockScreen();
 				if (kdScr) {
 					kdBase.simpleBlitFrom(*kdScr);
@@ -1201,7 +1212,7 @@ void EEMEngine::displayClue(const byte *clueBlock) {
 					const uint32 kdElapsed = g_system->getMillis() - kdStartMs;
 					if (haveIdle && kdElapsed >= kdDurationMs) {
 						// Resume the looping idle wait-anim.
-						const uint f = partnerFrameAtTick(_partnerIdleAnimId,
+						const uint f = partnerFrameAtTick(macSolved ? 0x02 : _partnerIdleAnimId,
 							(uint)idleAnim.size(), kdElapsed - kdDurationMs);
 						if ((!kdInIdle || f != kdLastIdleFrame) &&
 							f < idleAnim.size()) {
@@ -1219,7 +1230,7 @@ void EEMEngine::displayClue(const byte *clueBlock) {
 							g_system->copyRectToScreen(comp.getPixels(), comp.pitch,
 								0, 0, sw, sh);
 						}
-					} else {
+					} else if (haveKd) {
 						// Gesture one-shot.
 						const uint f = oneShotFrameAtTick(kdAnimId,
 							(uint)kdAnim.size(), kdElapsed);
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index ff2ba64a91e..eef0c2a640a 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -554,6 +554,8 @@ Common::Error EEMEngine::run() {
 			warning("Mac FONT resource failed to load; text will not render");
 		if (!loadMacDialogFont(_dialogFont))
 			warning("Mac dialog FONT resource failed to load");
+		if (isMacCD() && !loadMacFontResource(_newspaperFont, kMacSmallFontResource, 9))
+			warning("Mac newspaper FONT resource failed to load");
 	} else if (!_font.load(Common::Path("FONT.FNT"))) {
 		warning("FONT.FNT failed to load; text will not render");
 	}
@@ -1776,18 +1778,18 @@ void EEMEngine::showLondonEAKidsLogo() {
 	fadeCurrentPaletteToBlack();
 }
 
-void EEMEngine::showLondonLogo(uint picId, uint palId, uint holdMs,
+void EEMEngine::showStillPicture(uint picId, uint palId, uint holdMs,
 							   bool playThunder) {
 	Picture pic;
 	if (!_picsArchive.getPicture(picId, pic) || pic.surface.empty()) {
-		warning("London logo PIC 0x%x load failed", picId);
+		warning("PIC 0x%x load failed", picId);
 		return;
 	}
 	blitAt(pic, 0, 0);
 
 	byte target[kPalSize];
 	if (!getSitePalette(palId, target)) {
-		warning("London palette 0x%x load failed", palId);
+		warning("Palette 0x%x load failed", palId);
 		return;
 	}
 	byte black[kPalSize] = {};
@@ -1833,9 +1835,9 @@ void EEMEngine::runLondonStartup() {
 
 	if (isMacintosh()) {
 		if (!shouldQuit() && !_skipIntro)
-			showLondonLogo(0x20c, 0x3e, 3000);  // publisher logo (FUN_00009074)
+			showStillPicture(0x20c, 0x3e, 3000);  // publisher logo (FUN_00009074)
 		if (!shouldQuit() && !_skipIntro)
-			showLondonLogo(0x20b, 0x3d, 3000, /* playThunder= */ true);
+			showStillPicture(0x20b, 0x3d, 3000, /* playThunder= */ true);
 
 		// The Mac CD ships the post-logo intro as Flic movies where DOS uses
 		// bolt/movie/wave .ANM. KDCDINTR is the centered intro; BOOK54 is the
@@ -1859,7 +1861,7 @@ void EEMEngine::runLondonStartup() {
 			fadeCurrentPaletteToBlack();
 		}
 		if (!shouldQuit() && !_skipIntro)
-			showLondonLogo(kDosLondonPicHighScoreLogo,
+			showStillPicture(kDosLondonPicHighScoreLogo,
 						   kDosLondonPalHighScoreLogo, 3000);
 
 		// Intro movie with its theme (MUS00101.XMI).
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 0c6ca8cd8b2..7603a01fd8b 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -511,7 +511,7 @@ private:
 	/// Start London mystery 0 after a freshly-created detective chooses a partner.
 	bool startLondonTrainingMystery();
 	void showLondonEAKidsLogo();
-	void showLondonLogo(uint picId, uint palId, uint holdMs,
+	void showStillPicture(uint picId, uint palId, uint holdMs,
 						bool playThunder = false);
 	void showLondonCharSelect();
 	void playLondonInitCluesAnim(uint16 caseType, const Picture &bg,
@@ -637,6 +637,7 @@ private:
 	Mystery    _mystery;         ///< M<n>.BIN
 	EEMFont    _font;            ///< FONT.FNT (8 px)
 	EEMFont    _dialogFont;      ///< Mac 14pt FONT used inside speech balloons.
+	EEMFont    _newspaperFont;
 
 	Common::Array<byte> _sitePals; ///< 40 × 768 bytes, 6-bit VGA.
 
diff --git a/engines/eem/font.cpp b/engines/eem/font.cpp
index a426c4961ae..4b2b474c5c7 100644
--- a/engines/eem/font.cpp
+++ b/engines/eem/font.cpp
@@ -180,6 +180,21 @@ int EEMFont::drawWordWrapped(Graphics::ManagedSurface *dst, int x, int y,
 	return (int)lines.size() * lineH;
 }
 
+int EEMFont::drawMacWordWrapped(Graphics::ManagedSurface *dst, int x, int y,
+								int width, const Common::String &s, uint32 color,
+								int lineHeight) const {
+	if (!_macFont || width <= 5)
+		return 0;
+
+	// Mac WriteText adds a left margin and advances the baseline first.
+	Common::Array<Common::String> lines;
+	wordWrapText(s, width - 5, lines);
+	y += lineHeight - _macFont->getFontAscent();
+	for (uint i = 0; i < lines.size(); i++)
+		drawString(dst, lines[i], x + 5, y + (int)i * lineHeight, width - 5, color);
+	return (int)lines.size() * lineHeight;
+}
+
 void EEMFont::drawChar(Graphics::Surface *dst, uint32 chr, int x, int y,
 					   uint32 color) const {
 	if (!dst)
diff --git a/engines/eem/font.h b/engines/eem/font.h
index 3d82be00bab..2a1432d15f2 100644
--- a/engines/eem/font.h
+++ b/engines/eem/font.h
@@ -72,6 +72,9 @@ public:
 	/// each line at (x, y) downward. Returns total height drawn.
 	int drawWordWrapped(Graphics::ManagedSurface *dst, int x, int y,
 						int width, const Common::String &s, uint32 color) const;
+	int drawMacWordWrapped(Graphics::ManagedSurface *dst, int x, int y,
+						   int width, const Common::String &s, uint32 color,
+						   int lineHeight = 16) const;
 
 private:
 	void clear();
diff --git a/engines/eem/graphics.cpp b/engines/eem/graphics.cpp
index f283765f3cd..2eb36fbd15d 100644
--- a/engines/eem/graphics.cpp
+++ b/engines/eem/graphics.cpp
@@ -116,7 +116,10 @@ uint EEMEngine::getBalloonLineCapacity(uint16 balloonId, int lineH) const {
 
 	const BalloonInsets &insets = kBalloonInsetTable[idx];
 	int textHeight = (int)insets.indDY - (int)insets.y;
-	if (isMacintosh())
+	if (isMacCD())
+		textHeight = insets.indDY * kMacScreenHeight / kScreenHeight -
+			insets.y * kMacScreenHeight / kScreenHeight;
+	else if (isMacintosh())
 		textHeight = scaleY(textHeight); // table is in DOS coords; Mac renders scaled
 	return MAX<uint>(1, textHeight / lineH + 1);
 }
@@ -810,14 +813,15 @@ uint16 EEMEngine::fitBalloonToText(uint16 bubNum,
 		return bubNum;
 
 	const BalloonInsets &originalInsets = kBalloonInsetTable[originalId];
-	const int lineH = _font.getFontHeight();
+	const int lineH = isMacCD() ? 16 : _font.getFontHeight();
 	const uint originalCapacity = getBalloonLineCapacity(originalId, lineH);
 	if (originalCapacity == 0)
 		return bubNum;
 
 	Common::Array<Common::String> lines;
-	const int wrapW = isMacintosh() ? scaleX((int)originalInsets.w)
-									: (int)originalInsets.w;
+	uint16 insetX, insetY, wrapWidth;
+	getBalloonInsets(originalId, insetX, insetY, wrapWidth);
+	const int wrapW = wrapWidth - (isMacCD() ? 5 : 0);
 	_font.wordWrapText(text, MAX<int>(8, wrapW), lines);
 	if (lines.empty())
 		return bubNum;
@@ -868,7 +872,12 @@ bool EEMEngine::getBalloonInsets(uint16 bubNum, uint16 &xInset,
 	const uint idx = bubNum & 0x7F;
 	if (idx >= ARRAYSIZE(kBalloonInsetTable))
 		return false;
-	if (isMacintosh()) {
+	if (isMacCD()) {
+		// CODE 2:1f74 truncates the fixed-point products.
+		xInset = kBalloonInsetTable[idx].x * kMacScreenWidth / kScreenWidth;
+		yInset = kBalloonInsetTable[idx].y * kMacScreenHeight / kScreenHeight;
+		textW = kBalloonInsetTable[idx].w * kMacScreenWidth / kScreenWidth;
+	} else if (isMacintosh()) {
 		xInset = (uint16)scaleX(kBalloonInsetTable[idx].x);
 		yInset = (uint16)scaleY(kBalloonInsetTable[idx].y);
 		textW  = (uint16)scaleX(kBalloonInsetTable[idx].w);
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 99375d64ace..4b9e002bbd4 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -1747,12 +1747,15 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
 	CursorMan.showMouse(true);
 
 	const bool floppyEnding = isFloppy();
+	const bool macCDEnding = isMacCD() && macLooseEnding;
 	const int sw = screenWidth();
 	const int sh = screenHeight();
-	const Common::Rect endingPrevRect =
+	const Common::Rect endingPrevRect = macCDEnding ? Common::Rect(0, 20, 43, 384) :
 		scaleDosRectIfMac(*this, kEndingPrevPageRect);
-	const Common::Rect endingNextRect =
+	const Common::Rect endingNextRect = macCDEnding ? Common::Rect(45, 20, 506, 376) :
 		scaleDosRectIfMac(*this, kEndingNextPageRect);
+	const Common::Rect endingExitRect = macCDEnding ? Common::Rect(0, 0, 506, 20) :
+		Common::Rect();
 	uint pageOffsets[8];
 	const uint pageOffsetCap =
 		(uint)ARRAYSIZE(pageOffsets);
@@ -1798,11 +1801,14 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
 		for (uint p = 0; p < maxPages; p++) {
 			if (cursor + 10 >= size)
 				break;
-			pageOffsets[validPages++] = cursor;
+			const uint pageStart = cursor;
 			// Skip the 10-byte header and find the null terminator.
 			cursor += 10;
 			while (cursor < size && buf[cursor] != 0)
 				cursor++;
+			if (cursor >= size)
+				break;
+			pageOffsets[validPages++] = pageStart;
 			cursor++;  // past the null
 		}
 	}
@@ -1811,7 +1817,7 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
 
 	const bool showFirstTryBadge =
 		num < sizeof(_mysteriesSolved) && _mysteriesSolved[num] == 2 &&
-		(floppyEnding || ConfMan.getBool("restored_content"));
+		!macCDEnding && (floppyEnding || ConfMan.getBool("restored_content"));
 	Picture firstTryBadge;
 	const bool haveFirstTryBadge =
 		showFirstTryBadge &&
@@ -1821,11 +1827,12 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
 	int direction = 0;
 	bool exitLoop = false;
 	bool dirty = true;
+	auto updateCursor = [&](const Common::Point &pos) {
+		setInteractiveMouseCursor(endingPrevRect.contains(pos) ||
+			endingNextRect.contains(pos) || endingExitRect.contains(pos));
+	};
 	const Common::Point mousePos = g_system->getEventManager()->getMousePos();
-	setInteractiveMouseCursor(endingPrevRect.contains(mousePos.x,
-													  mousePos.y) ||
-							  endingNextRect.contains(mousePos.x,
-													  mousePos.y));
+	updateCursor(mousePos);
 	while (!shouldQuit() && !exitLoop) {
 		if (dirty) {
 			const uint off = pageOffsets[pageIdx];
@@ -1883,15 +1890,22 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
 					break;
 				const uint16 picNum =
 					readScriptU16(buf.data() + off, macLooseEnding);
-				const Common::Rect textRect =
-					readRectXYXY(buf.data() + off + 2, macLooseEnding);
+				const Common::Rect textRect = macCDEnding
+					? readMacQuickDrawRectBE(buf.data() + off + 2)
+					: readRectXYXY(buf.data() + off + 2, macLooseEnding);
 				x1 = textRect.left;
 				y1 = textRect.top;
 				x2 = textRect.right;
 
 				Picture bg;
-				if (_picsArchive.getPicture(picNum, bg))
+				if (macCDEnding) {
+					if (_picsArchive.getPicture(kFloppyEndingBackgroundPic, bg))
+						blitTravisBackground(scratch, bg, true);
+					if (_picsArchive.getPicture(picNum, bg))
+						blitMacMaskedSurface(scratch.surfacePtr(), bg, 92, 34);
+				} else if (_picsArchive.getPicture(picNum, bg)) {
 					scratch.simpleBlitFrom(bg.surface);
+				}
 				raw = (const char *)buf.data() + off + 10;
 			}
 
@@ -1903,16 +1917,20 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
 
 			const Common::String text = parseString(raw, _playerName, _partner);
 
-			const EEMFont &renderFont = haveTinyFont ? tinyFont : _font;
+			const EEMFont &renderFont = macCDEnding ? _newspaperFont :
+				(haveTinyFont ? tinyFont : _font);
 			if (renderFont.isLoaded() && x2 > x1) {
 				const int textW = MIN<int>((int)x2 - (int)x1,
 										   sw - (int)x1);
 				MacSpritePaletteMap macPaletteMap = {0x00, 0xFF};
 				if (macEnding)
 					macPaletteMap = getMacSpritePaletteMap();
-				renderFont.drawWordWrapped(&scratch, (int)x1, (int)y1,
-										   textW, text,
-										   macEnding ? macPaletteMap.black : 0);
+				if (macCDEnding)
+					renderFont.drawMacWordWrapped(&scratch, x1, y1, textW, text, 0x2b, 13);
+				else
+					renderFont.drawWordWrapped(&scratch, (int)x1, (int)y1,
+											   textW, text,
+											   macEnding ? macPaletteMap.black : 0);
 			}
 
 			g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
@@ -1930,9 +1948,7 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
 				break;
 			}
 			if (ev.type == Common::EVENT_MOUSEMOVE)
-				setInteractiveMouseCursor(
-					endingPrevRect.contains(ev.mouse.x, ev.mouse.y) ||
-					endingNextRect.contains(ev.mouse.x, ev.mouse.y));
+				updateCursor(ev.mouse);
 			if (ev.type == Common::EVENT_KEYDOWN) {
 				switch (ev.kbd.keycode) {
 				case Common::KEYCODE_ESCAPE:
@@ -1970,10 +1986,11 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
 					break;
 			}
 			if (ev.type == Common::EVENT_LBUTTONDOWN) {
-				setInteractiveMouseCursor(
-					endingPrevRect.contains(ev.mouse.x, ev.mouse.y) ||
-					endingNextRect.contains(ev.mouse.x, ev.mouse.y));
-				if (endingPrevRect.contains(ev.mouse.x, ev.mouse.y)) {
+				updateCursor(ev.mouse);
+				if (endingExitRect.contains(ev.mouse)) {
+					direction = 0;
+					exitLoop = true;
+				} else if (endingPrevRect.contains(ev.mouse.x, ev.mouse.y)) {
 					if (pageIdx > 0) {
 						pageIdx--;
 						dirty = true;
@@ -4396,10 +4413,10 @@ void EEMEngine::drawKDBalloonOverCurrentScreen(Common::String text) {
 		_balloonArchive.size() > (bubNum & 0x7F) &&
 		_balloonArchive.loadEntry(bubNum & 0x7F, balloon);
 
-	const int balloonX = mac ? scaleX(0x21) : 0x21;
-	const int topBandH = mac ? scaleY(0x50) : 0x50;
-	const int centeredMaxH = mac ? scaleY(0x4e) : 0x4e;
-	int balloonY = mac ? scaleY(1) : 1;
+	const int balloonX = isMacCD() ? 100 : scaleX(0x21);
+	const int topBandH = isMacCD() ? 155 : scaleY(0x50);
+	const int centeredMaxH = isMacCD() ? 155 : scaleY(0x4e);
+	int balloonY = isMacCD() ? 1 : scaleY(1);
 	if (haveBalloon && balloon.surface.h < centeredMaxH)
 		balloonY = (topBandH - balloon.surface.h) / 2;
 
@@ -4425,8 +4442,12 @@ void EEMEngine::drawKDBalloonOverCurrentScreen(Common::String text) {
 	if (dialogFont.isLoaded()) {
 		const byte textColor =
 			mac ? macPaletteMap.black : (haveBalloon ? 0 : 0xF);
-		dialogFont.drawWordWrapped(&ms, balloonX + tx, balloonY + ty,
-								   tw, text, textColor);
+		if (isMacCD())
+			dialogFont.drawMacWordWrapped(&ms, balloonX + tx, balloonY + ty,
+									  tw, text, textColor);
+		else
+			dialogFont.drawWordWrapped(&ms, balloonX + tx, balloonY + ty,
+									   tw, text, textColor);
 	}
 
 	copyToScreen(ms);
@@ -4461,7 +4482,7 @@ void EEMEngine::doAccuse() {
 		const int foundPoints = _mystery.foundPoints();
 		readyTier = (foundPoints == 0)    ? 0
 				  : (foundPoints < 0x32)  ? 1
-				  : (foundPoints < 0x65)  ? 2
+				  : (foundPoints < (isMacCD() ? 100 : 101)) ? 2
 											: 3;
 	}
 
@@ -4480,7 +4501,7 @@ void EEMEngine::doAccuse() {
 		// final two SDB entries, but its KD digital table points kdspeak 9
 		// at earlier hint clips. Use the otherwise unreferenced pair. EEM1
 		// SDB indices, so EEM1 only.
-		if (!isLondon() && _mystery.number() == 0)
+		if (!isLondon() && !isMacCD() && _mystery.number() == 0)
 			entryVoiceOverride = (_partner == kPartnerJake) ? 105 : 104;
 		break;
 	case 1:
@@ -4509,8 +4530,12 @@ void EEMEngine::doAccuse() {
 			else
 				_audio->sayKDDigital(entryKdIdx, entryKDSpeak, _partner);
 		}
-		waitForInput(60000);
+		waitForInput(isMacCD() ? 0xFFFFFFFFu : 60000);
+		if (isMacCD())
+			interruptAudio();
 	}
+	if (shouldQuit())
+		return;
 	if (!canAccuse) {
 		_nextScreen = _lastScreen != kScreenInvalid
 						? (ScreenId)_lastScreen : kScreenSite;
@@ -4545,7 +4570,8 @@ void EEMEngine::doAccuse() {
 	// London matches the selected notes against the answer sets; EEM1/floppy
 	// sum clue points.
 	const bool accuseSolved =
-		isLondon() ? _mystery.londonSolved() : _mystery.solvedCheck();
+		isLondon() ? _mystery.londonSolved() :
+		(isMacCD() ? _mystery.selectedPoints() >= 99 : _mystery.solvedCheck());
 	if (!accuseSolved) {
 		const byte *kdIdx = _mystery.kdTextIndex();
 		const int16 hintOff = kdIdx
@@ -4582,7 +4608,9 @@ void EEMEngine::doAccuse() {
 		if (_audio && kdIdx)
 			_audio->sayKDDigital(kdIdx, 3, _partner);
 
-		waitForInput(20000);
+		waitForInput(isMacCD() ? 0xFFFFFFFFu : 20000);
+		if (isMacCD() && _audio)
+			_audio->stopSpool();
 		_nextScreen = _lastScreen != kScreenInvalid
 						? (ScreenId)_lastScreen : kScreenSite;
 		return;
@@ -4605,25 +4633,41 @@ void EEMEngine::doAccuse() {
 			Common::String hint =
 				parseString(raw ? raw : "", _playerName, _partner);
 			if (!hint.empty()) {
-				// Render gallery first so the snapshot includes partner.
-				drawAccuseGallery(num, gd, /* highlighted= */ -1,
-								  slotRects, slotSuspect);
+				if (isMacCD()) {
+					Picture background;
+					Graphics::ManagedSurface scratch(screenWidth(), screenHeight(),
+						Graphics::PixelFormat::createFormatCLUT8());
+					scratch.clear();
+					if (_picsArchive.getPicture(0x1a7, background))
+						blitTravisBackground(scratch, background, true);
+					blitPdaPartner(scratch, _aniArchive, _partner, kPdaNotebookPartner,
+						g_system->getMillis(), true, true);
+					copyToScreen(scratch);
+				} else {
+					drawAccuseGallery(num, gd, /* highlighted= */ -1,
+									  slotRects, slotSuspect);
+				}
 				drawKDBalloonOverCurrentScreen(hint);
 				if (_audio)
 					_audio->sayKDDigital(kdIdx, 4, _partner);
-				waitForInput(8000);
+				waitForInput(isMacCD() ? 0xFFFFFFFFu : 8000);
+				if (isMacCD() && _audio)
+					_audio->stopSpool();
 			}
 		}
 	}
+	if (shouldQuit())
+		return;
 
 	if (isLondon() && _music && _voiceOn)
 		_music->playMus(33, /* loop= */ true);
 
+	drawAccuseGallery(num, gd, highlighted, slotRects, slotSuspect);
 	// Wrap past empty slots (matches original DI advance).
-	if (slotRects[highlighted].isEmpty())
+	if (slotRects[highlighted].isEmpty()) {
 		highlighted = nextLiveSlot(slotRects, highlighted, +1);
-
-	drawAccuseGallery(num, gd, highlighted, slotRects, slotSuspect);
+		drawAccuseGallery(num, gd, highlighted, slotRects, slotSuspect);
+	}
 
 	int picked = -1;
 	uint32 lastTick = g_system->getMillis();
@@ -4717,15 +4761,13 @@ void EEMEngine::doAccuse() {
 		   pickedGuilty ? "yes" : "no",
 		   guessedRight ? "correct" : "wrong");
 
-	// `_DisplayAlibi @ 1df2:0145`:
-	//   1. MIDI 6 loser sting (gallery still visible).
-	//   2. PIC 0x3e + suspect balloon + portrait at (0x82, py).
-	//      bindx = digit prefix (else 2). bindx<8 centres balloon,
-	//      bindx>=8 pins at x=0x21.
-	//   3. `_SpoolSound(talk-1)` where talk = partner==0 ? gd[+6] : gd[+0].
-	//   4. Partner reaction balloon @ KDTextIndex[+10], `_SayKDDigital(5)`.
-	//   5. _FirstTry = 0; NextScreen = LastScreen (1df2:043f).
+	// DisplayAlibi: DOS 1df2:0145, Mac CD CODE 7:043e.
 	if (!guessedRight) {
+		const bool macCD = isMacCD();
+		const int sw = macCD ? screenWidth() : kScreenWidth;
+		const int sh = macCD ? screenHeight() : kScreenHeight;
+		const MacSpritePaletteMap palette = macCD ? getMacSpritePaletteMap() :
+			MacSpritePaletteMap{0x00, 0xFF};
 		static const uint16 kAlibiBubbles[16] = {
 			0x002B, 0x002C, 0x002D, 0x002E,
 			0x00AB, 0x00AC, 0x00AD, 0x00AE,
@@ -4765,58 +4807,67 @@ void EEMEngine::doAccuse() {
 			_balloonArchive.size() > (bubNum & 0x7F) &&
 			_balloonArchive.loadEntry(bubNum & 0x7F, balloon);
 
-		int balloonX = 0x21;
+		int balloonX = macCD ? 100 : 0x21;
 		int balloonY = 1;
-		int py = 0x5a;
+		int py = macCD ? 173 : 0x5a;
 		if (bindx < 8) {
 			const int bw = haveBalloon ? balloon.surface.w : 0;
 			const int bh = haveBalloon ? balloon.surface.h : 0;
-			balloonX = (kScreenWidth - bw) / 2;
-			if (bh < 0x5a) {
-				balloonY = (0x5a - bh) / 2;
+			balloonX = (sw - bw) / 2;
+			if (bh < py) {
+				balloonY = (py - bh) / 2;
 			} else {
 				balloonY = 1;
-				py = bh;
+				if (!macCD)
+					py = bh;
 			}
 		} else {
 			const int bh = haveBalloon ? balloon.surface.h : 0;
-			balloonX = 0x21;
-			balloonY = (bh < 0x4f) ? (0x50 - bh) / 2 : 1;
+			balloonY = macCD ? MAX(1, (155 - bh) / 2) :
+				((bh < 0x4f) ? (0x50 - bh) / 2 : 1);
 		}
 
-		Graphics::ManagedSurface base(kScreenWidth, kScreenHeight,
+		Graphics::ManagedSurface base(sw, sh,
 			Graphics::PixelFormat::createFormatCLUT8());
 		base.clear();
 		if (haveAlibiBg)
-			base.simpleBlitFrom(alibiBg.surface);
+			blitTravisBackground(base, alibiBg, macCD);
 		if (haveSuspect) {
-			const byte transp = (byte)(suspect.flags >> 8);
-			base.transBlitFrom(suspect.surface,
-							   Common::Point(0x82, py),
-							   (uint32)transp);
+			if (macCD)
+				blitMacMaskedSurface(base.surfacePtr(), suspect, 208, py);
+			else
+				base.transBlitFrom(suspect.surface, Common::Point(0x82, py),
+					(uint32)(byte)(suspect.flags >> 8));
 		}
 
-		Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+		Graphics::ManagedSurface scratch(sw, sh,
 			Graphics::PixelFormat::createFormatCLUT8());
 		scratch.simpleBlitFrom(base);
 		if (haveBalloon) {
-			const byte transp = (byte)(balloon.flags >> 8);
-			scratch.transBlitFrom(balloon.surface,
-								  Common::Point(balloonX, balloonY),
-								  (uint32)transp);
+			if (macCD)
+				blitMacMaskedSurface(scratch.surfacePtr(), balloon, balloonX, balloonY,
+					(bubNum & 0x80) != 0, palette);
+			else
+				scratch.transBlitFrom(balloon.surface, Common::Point(balloonX, balloonY),
+					(uint32)(byte)(balloon.flags >> 8));
 		}
 
 		uint16 tx = 5, ty = 4, tw = 155;
 		getBalloonInsets(bubNum, tx, ty, tw);
 		if (_font.isLoaded() && !alibi.empty()) {
-			_font.drawWordWrapped(&scratch, balloonX + tx,
-								  balloonY + ty, tw, alibi,
-								  haveBalloon ? 0 : 0xF);
+			if (macCD)
+				_font.drawMacWordWrapped(&scratch, balloonX + tx, balloonY + ty,
+					tw, alibi, palette.black);
+			else
+				_font.drawWordWrapped(&scratch, balloonX + tx,
+									  balloonY + ty, tw, alibi,
+									  haveBalloon ? 0 : 0xF);
 		}
-		blitPdaPartner(scratch, _aniArchive, _partner, kPdaGalleryPartner,
-					   g_system->getMillis());
+		blitPdaPartner(scratch, _aniArchive, _partner,
+					   macCD ? kPdaNotebookPartner : kPdaGalleryPartner,
+					   g_system->getMillis(), macCD, macCD);
 
-		if (_music && _voiceOn) {
+		if (!macCD && _music && _voiceOn) {
 			_music->playMus(6, /* loop= */ false);
 			const uint32 musStart = g_system->getMillis();
 			bool aborted = false;
@@ -4842,20 +4893,22 @@ void EEMEngine::doAccuse() {
 			_music->stop();
 		}
 
-		g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
-								   0, 0, kScreenWidth, kScreenHeight);
-		g_system->updateScreen();
-		if (_audio && gd && !isMacintosh()) {
+		copyToScreen(scratch);
+		if (_audio && gd && (!isMacintosh() || macCD)) {
 			const uint16 alibiVoice =
-				READ_LE_UINT16(gd + (uint)picked * 0x46 + 0x00);
+				READ_LE_UINT16(gd + (uint)picked * 0x46 + (macCD ? 4 : 0));
 			const uint16 jakeVoice =
 				READ_LE_UINT16(gd + (uint)picked * 0x46 + 0x06);
 			const uint16 talk =
 				(_partner == kPartnerJake) ? jakeVoice : alibiVoice;
-			if (talk != 0)
+			if (talk != 0 && talk != 0xFFFF)
 				_audio->spoolSound((uint)(talk - 1));
 		}
-		waitForInput(60000);
+		waitForInput(macCD ? 0xFFFFFFFFu : 60000);
+		if (macCD && _audio)
+			_audio->stopSpool();
+		if (shouldQuit())
+			return;
 
 		const byte *reactIdx = _mystery.kdTextIndex();
 		if (reactIdx) {
@@ -4876,35 +4929,41 @@ void EEMEngine::doAccuse() {
 				const bool haveR =
 					_balloonArchive.size() > (rBub & 0x7F) &&
 					_balloonArchive.loadEntry(rBub & 0x7F, rBalloon);
-				const int rX = 0x21;
+				const int rX = macCD ? 100 : 0x21;
 				int rY = 1;
-				if (haveR && rBalloon.surface.h < 0x4e)
-					rY = (0x50 - rBalloon.surface.h) / 2;
+				if (haveR && rBalloon.surface.h < (macCD ? 155 : 0x4e))
+					rY = ((macCD ? 155 : 0x50) - rBalloon.surface.h) / 2;
 
 				scratch.simpleBlitFrom(base);
 				if (haveR) {
-					const byte transp = (byte)(rBalloon.flags >> 8);
-					scratch.transBlitFrom(rBalloon.surface,
-										   Common::Point(rX, rY),
-										   (uint32)transp);
+					if (macCD)
+						blitMacMaskedSurface(scratch.surfacePtr(), rBalloon, rX, rY);
+					else
+						scratch.transBlitFrom(rBalloon.surface, Common::Point(rX, rY),
+							(uint32)(byte)(rBalloon.flags >> 8));
 				}
 				uint16 rtx = 5, rty = 4, rtw = 155;
 				getBalloonInsets(rBub, rtx, rty, rtw);
 				if (_font.isLoaded()) {
-					_font.drawWordWrapped(&scratch, rX + rtx,
-										  rY + rty, rtw, react,
-										  haveR ? 0 : 0xF);
+					if (macCD)
+						_font.drawMacWordWrapped(&scratch, rX + rtx, rY + rty,
+							rtw, react, palette.black);
+					else
+						_font.drawWordWrapped(&scratch, rX + rtx,
+											  rY + rty, rtw, react,
+											  haveR ? 0 : 0xF);
 				}
 				blitPdaPartner(scratch, _aniArchive, _partner,
-							   kPdaGalleryPartner, g_system->getMillis());
-				g_system->copyRectToScreen(scratch.getPixels(),
-					scratch.pitch, 0, 0, kScreenWidth, kScreenHeight);
-				g_system->updateScreen();
+							   macCD ? kPdaNotebookPartner : kPdaGalleryPartner,
+							   g_system->getMillis(), macCD, macCD);
+				copyToScreen(scratch);
 				if (_audio)
 					_audio->sayKDDigital(reactIdx, 5, _partner);
 			}
 		}
-		waitForInput(60000);
+		waitForInput(macCD ? 0xFFFFFFFFu : 60000);
+		if (macCD && _audio)
+			_audio->stopSpool();
 
 		_mystery._firstTry = false;
 		_nextScreen = _lastScreen != kScreenInvalid
@@ -4914,12 +4973,12 @@ void EEMEngine::doAccuse() {
 
 	{
 		const uint mn = _mystery.number();
-		if (mn < sizeof(_mysteriesSolved)) {
-			_mysteriesSolved[mn] = _mystery._firstTry ? 2 : 1;
+		if (!isMacCD()) {
+			if (mn < sizeof(_mysteriesSolved))
+				_mysteriesSolved[mn] = _mystery._firstTry ? 2 : 1;
+			advanceChainStageAfterSolve(mn);
 		}
 
-		advanceChainStageAfterSolve(mn);
-
 		// `_DisplayCorrect` win background = `_BuildBackground(scene, 0x42, 0x14)`
 		// (frame PIC 0x3d + scene at 0x42,0x14, palette scene+1). EEM1 CD uses
 		// scene 5; EEM2/London uses scene 0x1b.
@@ -4935,8 +4994,8 @@ void EEMEngine::doAccuse() {
 			scratch.simpleBlitFrom(frame.surface);
 		if (winScene < _sitesArchive.size() &&
 			_sitesArchive.loadEntry(winScene, scene)) {
-			const int sx = scaleX(0x42);
-			const int sy = scaleY(0x14);
+			const int sx = isMacCD() ? 106 : scaleX(0x42);
+			const int sy = isMacCD() ? 38 : scaleY(0x14);
 			const int sw = MIN<int>(scene.surface.w, screenW - sx);
 			const int sh = MIN<int>(scene.surface.h, screenH - sy);
 			if (sw > 0 && sh > 0)
@@ -4944,15 +5003,22 @@ void EEMEngine::doAccuse() {
 										  scene.surface.pitch, sx, sy,
 										  sw, sh);
 		}
+		if (isMacCD()) {
+			remapMacSurfaceEndpoints(scratch, getMacSpritePaletteMap());
+			setPartnerEraseBg(&scratch);
+			setPartnerIdleAnim(true, _partner == kPartnerJake ? 0x02 : 0x10, 7, 152);
+		}
 		blitPdaPartner(scratch, _aniArchive, _partner,
-					   kPdaGalleryPartner, g_system->getMillis(),
-					   isMacintosh());
+					   isMacCD() ? kPdaNotebookPartner : kPdaGalleryPartner,
+					   g_system->getMillis(), isMacintosh(), isMacCD());
 		g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
 								   0, 0, screenW, screenH);
 		g_system->updateScreen();
 
-		if (_music && _voiceOn)
+		if (_music && (isMacCD() ? _musicOn : _voiceOn))
 			_music->playMus(5, /* loop= */ false);
+		if (isMacCD())
+			waitForMusicDone();
 
 		const byte *solved = _mystery.solvedClueBlock();
 		if (isMacintosh() && _mystery.usesCompactMacData() && solved) {
@@ -4982,6 +5048,15 @@ void EEMEngine::doAccuse() {
 		} else if (solved) {
 			displayClue(solved);
 		}
+		if (isMacCD()) {
+			setPartnerIdleAnim(false, 0, 0, 0);
+			setPartnerEraseBg(nullptr);
+			if (shouldQuit())
+				return;
+			if (mn < sizeof(_mysteriesSolved))
+				_mysteriesSolved[mn] = _mystery._firstTry ? 2 : 1;
+			advanceChainStageAfterSolve(mn);
+		}
 		if (_music && _voiceOn)
 			_music->stop();
 
@@ -4989,9 +5064,13 @@ void EEMEngine::doAccuse() {
 			playAnm(Common::Path(isLondon() ? "SCRAP.ANM" : "SCRAPBK.ANI"),
 					120, /* holdLastFrame= */ false);
 			displayScrapbookExtra(mn);
+		} else if (isMacCD()) {
+			fadeCurrentPaletteToBlack();
+			showStillPicture(0x20d, 0x25, 4000);
 		}
 
-		doShowEnding(mn);
+		if (!shouldQuit())
+			doShowEnding(mn);
 
 		_mystery.clear();
 		const Common::Error err = saveProfile(_playerName);


Commit: 1710907c2d907e3208de599e88dcc2e6215cc500
    https://github.com/scummvm/scummvm/commit/1710907c2d907e3208de599e88dcc2e6215cc500
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-10T17:45:23+02:00

Commit Message:
EEM: allow to come back to the map from sites in EEM1 (mac)

Changed paths:
    engines/eem/site.cpp


diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index 16e4f19bee9..93550c31ceb 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -228,6 +228,9 @@ bool readHotspotRect(const byte *r, bool mac, Common::Rect &rect) {
 }
 
 Common::Rect siteControlRect(const EEMEngine *vm, const Common::Rect &rect) {
+	// Mac CD SiteButtons retain the feet shortcut outside TRAVIS.
+	if (vm && vm->isMacCD() && rect == kPdaPartnerFootMapRect)
+		return Common::Rect(11, 340, 91, 384);
 	return pdaControlRect(vm, rect);
 }
 


Commit: cee1993fd4909a43f69b8915d3f5b4c2c6fde5d8
    https://github.com/scummvm/scummvm/commit/cee1993fd4909a43f69b8915d3f5b4c2c6fde5d8
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-10T17:45:24+02:00

Commit Message:
EEM: avoid busy waiting when playing music in 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 3413e4ea309..4944db5c8cc 100644
--- a/engines/eem/music.cpp
+++ b/engines/eem/music.cpp
@@ -27,6 +27,7 @@
 #include "common/debug.h"
 #include "common/endian.h"
 #include "common/file.h"
+#include "common/macresman.h"
 #include "common/memstream.h"
 #include "common/stream.h"
 #include "common/system.h"
@@ -133,7 +134,7 @@ static Common::SeekableReadStream *expandMacMidiRunningStatus(Common::SeekableRe
 	return expanded.readStream(expanded.size());
 }
 
-Common::SeekableReadStream *MusicPlayer::getResource(uint16 id, uint32 type) {
+void MusicPlayer::openMacMusicResources() {
 	static const char *const kMacMusicForks[] = {
 		"EEM Sound&Music",
 		"rsrc/EEM Sound&Music",
@@ -146,18 +147,33 @@ Common::SeekableReadStream *MusicPlayer::getResource(uint16 id, uint32 type) {
 		"rsrc/EEM London CD",
 		nullptr
 	};
+	const char *const *forks = _isLondon ? kMacLondonMusicForks : kMacMusicForks;
+	for (uint i = 0; forks[i]; i++)
+		_macResourcePaths.push_back(Common::Path(forks[i]));
+
+	// Keep installer forks open; reopening them decompresses the whole application.
+	_macResources = new Common::MacResManager[_macResourcePaths.size()];
+	for (uint i = 0; i < _macResourcePaths.size(); i++)
+		_macResources[i].open(_macResourcePaths[i]);
+}
+
+Common::SeekableReadStream *MusicPlayer::getMacMusicResource(uint file, uint16 id, uint32 type) {
+	if (_macResources[file].hasResFork())
+		return _macResources[file].getResource(type, id);
+	return openMacResource(_macResourcePaths[file], type, id);
+}
+
+Common::SeekableReadStream *MusicPlayer::getResource(uint16 id, uint32 type) {
 	static const uint32 kMacMidiTypes[] = {
 		MKTAG('c', 'm', 'i', 'd'),
 		MKTAG('M', 'I', 'D', 'I'),
 		MKTAG('M', 'i', 'd', 'i'),
 	};
 
-	const char *const *forks = _isLondon ? kMacLondonMusicForks : kMacMusicForks;
-	for (uint i = 0; forks[i]; i++) {
-		const Common::Path path(forks[i]);
+	for (uint i = 0; i < _macResourcePaths.size(); i++) {
 		if (type == MKTAG('M', 'I', 'D', 'I') || type == MKTAG('M', 'i', 'd', 'i')) {
 			for (uint j = 0; j < ARRAYSIZE(kMacMidiTypes); j++) {
-				Common::SeekableReadStream *stream = openMacResource(path, kMacMidiTypes[j], id);
+				Common::SeekableReadStream *stream = getMacMusicResource(i, id, kMacMidiTypes[j]);
 				if (stream) {
 					Common::SeekableReadStream *expanded = expandMacMidiRunningStatus(*stream);
 					delete stream;
@@ -169,7 +185,7 @@ Common::SeekableReadStream *MusicPlayer::getResource(uint16 id, uint32 type) {
 			continue;
 		} else if (type == MKTAG('s', 'n', 'd', ' ')) {
 			// London stores most instrument samples as delta-compressed csnd.
-			Common::SeekableReadStream *packed = openMacResource(path, MKTAG('c', 's', 'n', 'd'), id);
+			Common::SeekableReadStream *packed = getMacMusicResource(i, id, MKTAG('c', 's', 'n', 'd'));
 			if (packed) {
 				Common::SeekableReadStream *stream = decompressMacSound(*packed);
 				delete packed;
@@ -177,7 +193,7 @@ Common::SeekableReadStream *MusicPlayer::getResource(uint16 id, uint32 type) {
 			}
 		}
 
-		Common::SeekableReadStream *stream = openMacResource(path, type, id);
+		Common::SeekableReadStream *stream = getMacMusicResource(i, id, type);
 		if (stream)
 			return stream;
 	}
@@ -230,8 +246,10 @@ uint16 macSongResourceIdForMus(uint num, bool macCD) {
 MusicPlayer::MusicPlayer(bool isFloppy, bool isMacintosh, bool isLondon, bool isMacCD) :
 	_isFloppy(isFloppy), _isMacintosh(isMacintosh), _isLondon(isLondon),
 	_isMacCD(isMacCD) {
-	if (_isMacintosh)
+	if (_isMacintosh) {
+		openMacMusicResources();
 		return;
+	}
 
 	// _InitMIDI @ 20a2:013a — `_AIL_register_driver` against
 	// ADLIB.ADV / SBFM.ADV / MT32MPU.ADV. We honour the launcher's
@@ -285,6 +303,7 @@ MusicPlayer::MusicPlayer(bool isFloppy, bool isMacintosh, bool isLondon, bool is
 
 MusicPlayer::~MusicPlayer() {
 	stop();
+	delete[] _macResources;
 }
 
 void MusicPlayer::stop() {
diff --git a/engines/eem/music.h b/engines/eem/music.h
index 47edef8d6a1..5628ba8999c 100644
--- a/engines/eem/music.h
+++ b/engines/eem/music.h
@@ -29,6 +29,10 @@
 #include "common/path.h"
 #include "common/scummsys.h"
 
+namespace Common {
+class MacResManager;
+}
+
 namespace EEM {
 
 /**
@@ -74,6 +78,8 @@ public:
 	void send(uint32 b) override;
 
 private:
+	void openMacMusicResources();
+	Common::SeekableReadStream *getMacMusicResource(uint file, uint16 id, uint32 type);
 	void playMacSongResource(uint16 resourceId, bool loop);
 	void startLoadedMusic(const Common::String &name, bool loop);
 	Common::SeekableReadStream *getResource(uint16 id, uint32 type) override;
@@ -84,6 +90,8 @@ private:
 	const bool _isLondon;
 	const bool _isMacCD;
 	Common::Array<byte> _xmiData;
+	Common::Array<Common::Path> _macResourcePaths;
+	Common::MacResManager *_macResources = nullptr;
 	Audio::HalestormDriver *_macDriver = nullptr;
 };
 


Commit: a67c973f3b32454ffb3ae870c90760207f54301c
    https://github.com/scummvm/scummvm/commit/a67c973f3b32454ffb3ae870c90760207f54301c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-10T17:45:24+02:00

Commit Message:
EEM: improved mouse pointer rendering for both release of EEM1 (mac)

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


diff --git a/engines/eem/detection.cpp b/engines/eem/detection.cpp
index 4b36092e8ed..00fab8b8bac 100644
--- a/engines/eem/detection.cpp
+++ b/engines/eem/detection.cpp
@@ -92,7 +92,7 @@ const ADGameDescription gameDescriptions[] = {
 	},
 	{
 		"eem",
-		"",
+		"Floppy",
 		AD_ENTRY2s("MysteryData", "d94c087c27e68cc299d7c5e737e458f9", 941029,
 				   "PICS.DBD",    "8905041070ff1352666d98cd78d5501c", 3800445),
 		Common::EN_ANY,
@@ -106,7 +106,7 @@ const ADGameDescription gameDescriptions[] = {
 		// the fly (see installer.cpp). "d:" hashes the data fork so this matches
 		// both the raw-fork (CD/floppy) and MacBinary-wrapped captures.
 		"eem",
-		"",
+		"Floppy",
 		AD_ENTRY2s("Eagle Eye Installer", "d:08440dbf0cb47fb57e522f050159ffaa", 1391577,
 				   "EEM Install Data 2",  "aebccc677e149b37285f291f6ac72f57", 1446428),
 		Common::EN_ANY,
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index eef0c2a640a..82b2413ae0a 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -36,6 +36,7 @@
 
 #include "graphics/cursorman.h"
 #include "graphics/maccursor.h"
+#include "graphics/macgui/macwindowmanager.h"
 #include "graphics/paletteman.h"
 
 #include "video/flic_decoder.h"
@@ -224,15 +225,22 @@ void setInteractiveCursorPalette(const Picture &cursor, byte transparent) {
 	CursorMan.replaceCursorPalette(palette, 0, 256);
 }
 
-bool installMacLondonCursor(uint16 resourceId) {
-	static const char *const kAppForks[] = {
+bool installMacCursor(uint16 resourceId, bool london) {
+	static const char *const kMacCDAppForks[] = {
+		"Eagle Eye Mysteries CD",
+		"rsrc/Eagle Eye Mysteries CD",
+		nullptr
+	};
+	static const char *const kLondonAppForks[] = {
 		"EEM London CD",
-		"rsrc/EEM London CD"
+		"rsrc/EEM London CD",
+		nullptr
 	};
 
-	for (uint i = 0; i < ARRAYSIZE(kAppForks); i++) {
+	const char *const *appForks = london ? kLondonAppForks : kMacCDAppForks;
+	for (uint i = 0; appForks[i]; i++) {
 		Common::ScopedPtr<Common::SeekableReadStream> crsrStream(
-			openMacResource(Common::Path(kAppForks[i]),
+			openMacResource(Common::Path(appForks[i]),
 							MKTAG('c', 'r', 's', 'r'), resourceId));
 		if (crsrStream) {
 			Graphics::MacCursor macCursor;
@@ -248,12 +256,26 @@ bool installMacLondonCursor(uint16 resourceId) {
 }
 
 void installMouseCursor(DBDArchive &pics, bool interactive, bool mac,
-						bool london) {
-	if (mac && london) {
-		// EEM2 (London) Mac keeps pointers as 'crsr' colour cursors in the
-		// application resource fork; the DOS cursor PIC ids are mostly 1x1
-		// stubs or unrelated full-screen pictures in this release.
-		if (installMacLondonCursor(kLondonMacCursorCrsrs[0]))
+						bool london, bool macCD) {
+	if (mac && !london && !macCD) {
+		// Mac floppy CODE 2:484c selects QuickDraw's standard arrow.
+		const byte *data, *palette, *mask;
+		int width, height, hotspotX, hotspotY, transparent;
+		if (Graphics::MacWindowManager::getBuiltInCursorData(Graphics::kMacCursorArrow,
+				data, palette, mask, width, height, hotspotX, hotspotY, transparent)) {
+			CursorMan.replaceCursor(data, width, height, hotspotX, hotspotY,
+				transparent, nullptr, mask);
+			byte colors[6];
+			memcpy(colors, palette, sizeof(colors));
+			if (interactive)
+				colors[0] = 0xff;
+			CursorMan.replaceCursorPalette(colors, 0, 2);
+			return;
+		}
+	}
+	if (mac && (london || macCD)) {
+		// Mac CD CODE 2:3aea selects the same native arrow as Mac London.
+		if (installMacCursor(kLondonMacCursorCrsrs[0], london))
 			return;
 	}
 
@@ -296,7 +318,7 @@ EEMEngine::EEMEngine(OSystem *syst, const ADGameDescription *gameDesc)
 		_variant = kVariantFloppy;
 	if (gameDesc && gameDesc->platform == Common::kPlatformMacintosh)
 		_variant = gameDesc->extra && Common::String(gameDesc->extra).contains("CD")
-			? kVariantMacCD : kVariantMac;
+			? kVariantMacCD : kVariantMacFloppy;
 	if (gameDesc && gameDesc->gameId &&
 		Common::String(gameDesc->gameId) == "eem2")
 		_variant = kVariantLondonCD;
@@ -570,7 +592,7 @@ Common::Error EEMEngine::run() {
 	_audio->setVoiceEnabled(_voiceOn);
 	syncSoundSettings();
 
-	installMouseCursor(_picsArchive, false, isMacintosh(), isLondon());
+	installMouseCursor(_picsArchive, false, isMacintosh(), isLondon(), isMacCD());
 	CursorMan.showMouse(false);
 
 	// _AllBlack @ 172b:0d4b.
@@ -872,7 +894,13 @@ void EEMEngine::setInteractiveMouseCursor(bool active) {
 		return;
 
 	_interactiveMouseCursor = active;
-	installMouseCursor(_picsArchive, active, isMacintosh(), isLondon());
+	if (isMacCD()) {
+		// Native arrow 132 uses white (0) and black (1); only recolour its fill.
+		const byte palette[] = { 0xff, 0xff, 0xff, (byte)(active ? 0xff : 0), 0, 0 };
+		CursorMan.replaceCursorPalette(palette, 0, 2);
+		return;
+	}
+	installMouseCursor(_picsArchive, active, isMacintosh(), isLondon(), isMacCD());
 	// The red-outline highlight replaced any London cursor shape; force the
 	// next setSiteHotspotCursorId to reinstall.
 	_siteCursorId = -1;
@@ -899,7 +927,7 @@ void EEMEngine::setSiteHotspotCursorId(int cursorId) {
 		uint16 resourceId = kLondonMacCursorCrsrs[cursorId];
 		if (cursorId == 4 || cursorId == 5)
 			resourceId = (_partner == kPartnerJenny) ? 139 : 138;
-		if (!installMacLondonCursor(resourceId)) {
+		if (!installMacCursor(resourceId, true)) {
 			warning("EEM2 Mac: cursor %d ('crsr' %u) missing",
 					cursorId, resourceId);
 			return;
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 7603a01fd8b..76c74a18ffe 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -100,11 +100,11 @@ enum ScreenId {
 /// Distribution variant from `ADGameDescription::extra` (set by
 /// `gameDescriptions[]` in `detection.cpp`).
 enum Variant {
-	kVariantCD       = 0,
-	kVariantFloppy   = 1,
-	kVariantLondonCD = 2,
-	kVariantMac      = 3,
-	kVariantMacCD    = 4,
+	kVariantCD        = 0,
+	kVariantFloppy    = 1,
+	kVariantLondonCD  = 2,
+	kVariantMacFloppy = 3,
+	kVariantMacCD     = 4,
 };
 
 /// `_Partner @ 29be:7918`. Selected at the partner-pick screen
@@ -198,8 +198,7 @@ public:
 	const EEMFont &getFont() const { return _font; }
 	uint8       getPartnerIndex() const { return _partner; }
 
-	/// Interactive-region cursor. DOS/EEM1 uses a red-outline pointer; Mac
-	/// London uses the original Color QuickDraw arrow.
+	/// Highlight interactive regions; Mac London retains its native colours.
 	void setInteractiveMouseCursor(bool active);
 
 	/// Interactive cursor over searchable hotspots.


Commit: b5d5892fb4f30ec01a4ac85a70688cdec8a86d9b
    https://github.com/scummvm/scummvm/commit/b5d5892fb4f30ec01a4ac85a70688cdec8a86d9b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-09-10T17:45:24+02:00

Commit Message:
EEM: enabled restored content for EEM1 (mac cd)

Changed paths:
    engines/eem/audio.cpp
    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/audio.cpp b/engines/eem/audio.cpp
index 961bc84c0f4..29c0b6ece2e 100644
--- a/engines/eem/audio.cpp
+++ b/engines/eem/audio.cpp
@@ -478,6 +478,11 @@ bool AudioPlayer::playMacMysterySound(uint num) {
 				resourceId);
 		return false;
 	}
+	// Some unused speech resources contain only a two-sample placeholder.
+	if (audioStream->getLength().msecs() == 0) {
+		delete audioStream;
+		return false;
+	}
 
 	_mixer->playStream(Audio::Mixer::kSFXSoundType, &_spoolHandle,
 					   audioStream, -1, Audio::Mixer::kMaxChannelVolume,
diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index da80872b1b4..3d88c85b522 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -932,15 +932,16 @@ void EEMEngine::applyClueSideEffects(const byte *c) {
 //   +0..1: number (entry count; 0 = no briefing)
 //   +2..3: pic ID for entry 0; entry N>0 uses (entry-1).lastWord
 //   +4..:  array of 62-byte entries
-void EEMEngine::displayClue(const byte *clueBlock) {
+void EEMEngine::displayClue(const byte *clueBlock, uint maxEntries) {
 	if (!clueBlock || !_mystery.isLoaded())
 		return;
 
 	const uint16 number = READ_LE_UINT16(clueBlock);
-	debugC(1, kDebugScript, "displayClue: %u entries", number);
 	// number == 0 = no briefing (e.g. mystery 0 case-type 4); >32 = bad ptr.
 	if (number == 0 || number > 32)
 		return;
+	const uint count = MIN<uint>(number, maxEntries);
+	debugC(1, kDebugScript, "displayClue: %u entries", count);
 
 	const uint stride = isLondon() ? 0x54 : 62;
 	const bool mac = isMacintosh();
@@ -980,7 +981,7 @@ void EEMEngine::displayClue(const byte *clueBlock) {
 	//   EEM1 +0x30..+0x39 / EEM2 +0x3c..+0x45:
 	//       5 notebook entries (-1 terminated)
 	//   EEM1 +0x3a / EEM2 +0x4e: KD-anim number (-1 = none)
-	for (uint i = 0; i < number && !shouldQuit(); i++) {
+	for (uint i = 0; i < count && !shouldQuit(); i++) {
 		if (isMacCD() && _audio)
 			_audio->stopSpool();
 		g_system->copyRectToScreen(bg.getPixels(), bg.pitch, 0, 0, sw, sh);
@@ -1254,7 +1255,7 @@ void EEMEngine::displayClue(const byte *clueBlock) {
 				g_system->delayMillis(10);
 			}
 			if (skipAll) {
-				for (uint k = i; k < number; k++)
+				for (uint k = i; k < count; k++)
 					applyClueSideEffects(clueBlock + 4 + k * stride);
 				return;
 			}
diff --git a/engines/eem/detection.cpp b/engines/eem/detection.cpp
index 00fab8b8bac..29e1033a925 100644
--- a/engines/eem/detection.cpp
+++ b/engines/eem/detection.cpp
@@ -47,6 +47,7 @@ static const char *const directoryGlobs[] = {
 #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    GUIO3(GAMEOPTION_HIDE_HIGHLIGHT_BOXES, GAMEOPTION_FIT_DIALOG_BALLOONS, GUIO_NOMIDI)
+#define GUI_OPTIONS_EEM_MAC_CD GUIO4(GAMEOPTION_HIDE_HIGHLIGHT_BOXES, GAMEOPTION_FIT_DIALOG_BALLOONS, GAMEOPTION_RESTORED_CONTENT, GUIO_NOMIDI)
 
 const ADGameDescription gameDescriptions[] = {
 	{
@@ -122,7 +123,7 @@ const ADGameDescription gameDescriptions[] = {
 		Common::EN_ANY,
 		Common::kPlatformMacintosh,
 		ADGF_UNSTABLE,
-		GUI_OPTIONS_EEM_MAC
+		GUI_OPTIONS_EEM_MAC_CD
 	},
 	{
 		// Eagle Eye Mysteries in London
diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 82b2413ae0a..2ad0385e342 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -1807,21 +1807,25 @@ void EEMEngine::showLondonEAKidsLogo() {
 }
 
 void EEMEngine::showStillPicture(uint picId, uint palId, uint holdMs,
-							   bool playThunder) {
+							   bool playThunder, bool holdLastFrame) {
 	Picture pic;
 	if (!_picsArchive.getPicture(picId, pic) || pic.surface.empty()) {
 		warning("PIC 0x%x load failed", picId);
 		return;
 	}
-	blitAt(pic, 0, 0);
-
 	byte target[kPalSize];
 	if (!getSitePalette(palId, target)) {
 		warning("Palette 0x%x load failed", palId);
 		return;
 	}
+	// Mac _LoadPalette puts white at 0 and black at 255.
+	if (isMacintosh() && target[0] == 0 && target[1] == 0 && target[2] == 0) {
+		for (uint i = 0; i < 3; i++)
+			SWAP(target[i], target[255 * 3 + i]);
+	}
 	byte black[kPalSize] = {};
 	g_system->getPaletteManager()->setPalette(black, 0, 256);
+	blitAt(pic, 0, 0);
 	g_system->updateScreen();
 	fadePaletteFromBlack(target);
 
@@ -1831,7 +1835,8 @@ void EEMEngine::showStillPicture(uint picId, uint palId, uint holdMs,
 	waitForInput(holdMs);
 	if (_audio)
 		_audio->stopVoice();
-	fadeCurrentPaletteToBlack();
+	if (!holdLastFrame)
+		fadeCurrentPaletteToBlack();
 }
 
 bool EEMEngine::startLondonTrainingMystery() {
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 76c74a18ffe..d293ed3e3af 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -210,7 +210,7 @@ public:
 	void setSiteHotspotCursorId(int cursorId);
 
 	/// `_DisplayClue @ 2404:05e6`. 
-	void displayClue(const byte *clueBlock);
+	void displayClue(const byte *clueBlock, uint maxEntries = 32);
 
 	/// EEM2/London `_DoPuzzle @ 2542:1482`. A clue entry can gate the rest of
 	/// itself behind a "check the manual / a real map" puzzle
@@ -364,6 +364,7 @@ private:
 	void floppyKDHint(uint kdSlot, const byte *kdIdx,
 					  const byte *bufBase, uint32 mysSize);
 	void displayScrapbookExtra(uint mysteryNum);
+	void displayMacPracticeScrapbook();
 	void accuseDrawGallery(int highlighted,
 						   Common::Array<Common::Rect> &rects,
 						   Common::Array<int> &suspects, uint8 num,
@@ -511,7 +512,7 @@ private:
 	bool startLondonTrainingMystery();
 	void showLondonEAKidsLogo();
 	void showStillPicture(uint picId, uint palId, uint holdMs,
-						bool playThunder = false);
+						  bool playThunder = false, bool holdLastFrame = false);
 	void showLondonCharSelect();
 	void playLondonInitCluesAnim(uint16 caseType, const Picture &bg,
 								 bool haveBriefingBg);
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 4b9e002bbd4..de7851710a6 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -255,6 +255,7 @@ constexpr Common::Rect kEndingNextPageRect(Common::Point(292, 0), 28, kScreenHei
 constexpr uint16 kFloppyEndingBackgroundPic = 0x8b;
 constexpr uint16 kFirstTryBadgePic = 0x205;
 constexpr Common::Point kFirstTryBadgePos(0x1e, 9);
+constexpr Common::Point kMacFirstTryBadgePos(43, 13);
 constexpr uint kMacMysteryDataTableOffset = 0x08cd;
 constexpr uint kMacMysteryDataMysteryCount = 55;
 constexpr uint kMacMysteryDataEndingCount = 55;
@@ -769,6 +770,15 @@ bool restoredContentVoiceAppliesTo(uint mysteryNum) {
 		   mysteryNum <= kRestoredContentLastMystery;
 }
 
+Common::Point scrapbookExtraPosition(uint16 x, uint16 y, bool mac) {
+	if (!mac)
+		return Common::Point(x, y);
+	// Match the fixed-point conversion used by the Mac floppy scripts.
+	const uint32 xScale = (kMacScreenWidth << 16) / kScreenWidth;
+	const uint32 yScale = (kMacScreenHeight << 16) / kScreenHeight;
+	return Common::Point((x * xScale) >> 16, (y * yScale) >> 16);
+}
+
 bool gallerySlotAt(const Common::Array<Common::Rect> &rects,
 				   const Common::Array<int> &suspects, int x, int y) {
 	for (uint i = 0; i < rects.size() && i < suspects.size(); i++) {
@@ -1817,7 +1827,7 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
 
 	const bool showFirstTryBadge =
 		num < sizeof(_mysteriesSolved) && _mysteriesSolved[num] == 2 &&
-		!macCDEnding && (floppyEnding || ConfMan.getBool("restored_content"));
+		(floppyEnding || ConfMan.getBool("restored_content"));
 	Picture firstTryBadge;
 	const bool haveFirstTryBadge =
 		showFirstTryBadge &&
@@ -1910,9 +1920,14 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
 			}
 
 			if (pageIdx == 0 && haveFirstTryBadge) {
-				const byte transp = (byte)(firstTryBadge.flags >> 8);
-				scratch.transBlitFrom(firstTryBadge.surface,
-									  kFirstTryBadgePos, transp);
+				if (macEnding) {
+					blitMacMaskedSurface(scratch.surfacePtr(), firstTryBadge,
+						kMacFirstTryBadgePos.x, kMacFirstTryBadgePos.y);
+				} else {
+					const byte transp = (byte)(firstTryBadge.flags >> 8);
+					scratch.transBlitFrom(firstTryBadge.surface,
+						kFirstTryBadgePos, transp);
+				}
 			}
 
 			const Common::String text = parseString(raw, _playerName, _partner);
@@ -4973,6 +4988,8 @@ void EEMEngine::doAccuse() {
 
 	{
 		const uint mn = _mystery.number();
+		const bool macRestored = isMacCD() && ConfMan.getBool("restored_content") &&
+			(mn == 0 || _restoredContentDataLoaded);
 		if (!isMacCD()) {
 			if (mn < sizeof(_mysteriesSolved))
 				_mysteriesSolved[mn] = _mystery._firstTry ? 2 : 1;
@@ -5046,7 +5063,9 @@ void EEMEngine::doAccuse() {
 				displayFloppyDialogRecords(records, count, 1);
 			}
 		} else if (solved) {
-			displayClue(solved);
+			const uint count = READ_LE_UINT16(solved);
+			// The practice CD script already includes the three scrapbook lines.
+			displayClue(solved, macRestored && mn == 0 && count > 3 ? count - 3 : count);
 		}
 		if (isMacCD()) {
 			setPartnerIdleAnim(false, 0, 0, 0);
@@ -5066,7 +5085,11 @@ void EEMEngine::doAccuse() {
 			displayScrapbookExtra(mn);
 		} else if (isMacCD()) {
 			fadeCurrentPaletteToBlack();
-			showStillPicture(0x20d, 0x25, 4000);
+			showStillPicture(0x20d, 0x25, 4000, false, macRestored);
+			if (!shouldQuit() && macRestored) {
+				displayScrapbookExtra(mn);
+				fadeCurrentPaletteToBlack();
+			}
 		}
 
 		if (!shouldQuit())
@@ -5155,12 +5178,50 @@ void EEMEngine::floppyKDHint(uint kdSlot, const byte *kdIdx,
 	}
 }
 
+void EEMEngine::displayMacPracticeScrapbook() {
+	const byte *solved = _mystery.solvedClueBlock();
+	const uint count = solved ? READ_LE_UINT16(solved) : 0;
+	if (count <= 3 || count > 32)
+		return;
+
+	byte dialogue[4 + 3 * 62] = {};
+	WRITE_LE_UINT16(dialogue, 3);
+	memcpy(dialogue + 4, solved + 4 + (count - 3) * 62, 3 * 62);
+
+	// Keep the CD partner's text and speech, using the seated balloon positions.
+	const Common::Point position = _partner == kPartnerJake
+		? Common::Point(31, 32) : Common::Point(135, 28);
+	for (uint i = 0; i < 3; i++) {
+		byte *entry = dialogue + 4 + i * 62;
+		for (uint partner = 0; partner < 2; partner++) {
+			WRITE_LE_UINT16(entry + 12 + partner * 2, 0x98);
+			WRITE_LE_UINT16(entry + 16 + partner * 4, position.x);
+			WRITE_LE_UINT16(entry + 18 + partner * 4, position.y);
+		}
+		WRITE_LE_UINT16(entry + 58, 0xFFFF);
+		WRITE_LE_UINT16(entry + 60, 0);
+	}
+	displayClue(dialogue);
+}
+
 void EEMEngine::displayScrapbookExtra(uint mysteryNum) {
 	if (isFloppy() || !ConfMan.getBool("restored_content") ||
-		!_restoredContentDataLoaded ||
 		mysteryNum >= kScrapbookExtraCaseCount || !_font.isLoaded())
 		return;
 
+	if (isMacCD() && mysteryNum == 0) {
+		displayMacPracticeScrapbook();
+		return;
+	}
+	if (!_restoredContentDataLoaded)
+		return;
+
+	const bool mac = isMacCD();
+	const int sw = screenWidth();
+	const int sh = screenHeight();
+	const EEMFont &dialogFont = mac && _dialogFont.isLoaded() ? _dialogFont : _font;
+	const MacSpritePaletteMap paletteMap = mac ? getMacSpritePaletteMap() : MacSpritePaletteMap();
+
 	Common::File file;
 	if (!file.open(Common::Path(kScrapbookExtraFilename))) {
 		warning("EEM restored content unavailable: %s missing",
@@ -5193,7 +5254,7 @@ void EEMEngine::displayScrapbookExtra(uint mysteryNum) {
 		!file.seek(recordsOffset))
 		return;
 
-	Graphics::ManagedSurface bg(kScreenWidth, kScreenHeight,
+	Graphics::ManagedSurface bg(sw, sh,
 		Graphics::PixelFormat::createFormatCLUT8());
 	bg.clear();
 	if (Graphics::Surface *screen = g_system->lockScreen()) {
@@ -5215,6 +5276,8 @@ void EEMEngine::displayScrapbookExtra(uint mysteryNum) {
 		const uint16 voiceNancy = file.readUint16LE();
 		const uint16 jakeTextSize = file.readUint16LE();
 		const uint16 jennyTextSize = file.readUint16LE();
+		const Common::Point picPos = scrapbookExtraPosition(picX, picY, mac);
+		const Common::Point balloonPos = scrapbookExtraPosition(balloonX, balloonY, mac);
 
 		if (file.eos() || file.err())
 			return;
@@ -5231,18 +5294,21 @@ void EEMEngine::displayScrapbookExtra(uint mysteryNum) {
 		if (text.empty())
 			continue;
 
-		Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+		Graphics::ManagedSurface scratch(sw, sh,
 			Graphics::PixelFormat::createFormatCLUT8());
 		scratch.simpleBlitFrom(*bg.surfacePtr());
 
 		if (picId != 0 && picId != 0xFFFF) {
 			Picture pic;
 			if (_picsArchive.getPicture(picId, pic) &&
-				picX < kScreenWidth && picY < kScreenHeight) {
-				const byte transp = (byte)(pic.flags >> 8);
-				scratch.transBlitFrom(pic.surface,
-									  Common::Point(picX, picY),
-									  (uint32)transp);
+				picPos.x < sw && picPos.y < sh) {
+				if (mac) {
+					blitMacMaskedSurface(scratch.surfacePtr(), pic,
+						picPos.x, picPos.y, false, paletteMap);
+				} else {
+					const byte transp = (byte)(pic.flags >> 8);
+					scratch.transBlitFrom(pic.surface, picPos, (uint32)transp);
+				}
 			}
 		}
 
@@ -5258,20 +5324,27 @@ void EEMEngine::displayScrapbookExtra(uint mysteryNum) {
 		uint16 textYInset = 4;
 		uint16 textWidth = 155;
 		if (haveBalloon) {
-			const byte transp = (byte)(balloon.flags >> 8);
-			scratch.transBlitFrom(balloon.surface,
-								  Common::Point(balloonX, balloonY),
-								  (uint32)transp, flipBalloon);
+			if (mac) {
+				blitMacMaskedSurface(scratch.surfacePtr(), balloon,
+					balloonPos.x, balloonPos.y, flipBalloon, paletteMap);
+			} else {
+				const byte transp = (byte)(balloon.flags >> 8);
+				scratch.transBlitFrom(balloon.surface, balloonPos,
+					(uint32)transp, flipBalloon);
+			}
 			getBalloonInsets(balloonId, textXInset, textYInset, textWidth);
 		}
 
-		_font.drawWordWrapped(&scratch, balloonX + textXInset,
-							  balloonY + textYInset,
-							  MAX<int>(8, (int)textWidth), text,
-							  haveBalloon ? 0 : 0xF);
+		if (mac) {
+			dialogFont.drawMacWordWrapped(&scratch, balloonPos.x + textXInset,
+				balloonPos.y + textYInset, MAX<int>(8, textWidth), text, paletteMap.black);
+		} else {
+			dialogFont.drawWordWrapped(&scratch, balloonPos.x + textXInset,
+				balloonPos.y + textYInset, MAX<int>(8, textWidth), text, haveBalloon ? 0 : 0xF);
+		}
 
 		g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
-								   0, 0, kScreenWidth, kScreenHeight);
+								   0, 0, sw, sh);
 		g_system->updateScreen();
 
 		if (_audio && restoredContentVoiceAppliesTo(mysteryNum)) {




More information about the Scummvm-git-logs mailing list