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

neuromancer noreply at scummvm.org
Fri Jun 26 18:38:01 UTC 2026


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

Summary:
d0ed0e9810 EEM: proper selection of clues in EEM2 Mac
ee8eee5869 EEM: improved setup screen in EEM2 Mac
4ec3f34aec EEM: implemented approach screens in EEM2 Mac
4762d3458d EEM: refactored ui.cpp into two files
2e596e7adb EEM: refactored map related code into several functions
4db53b24af EEM: refactored coordinate handling
e502b122c9 EEM: fixed regression in EEM2 DOS intro


Commit: d0ed0e98100f2075a5b00145b84f53930e9e1ca2
    https://github.com/scummvm/scummvm/commit/d0ed0e98100f2075a5b00145b84f53930e9e1ca2
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-26T20:37:27+02:00

Commit Message:
EEM: proper selection of clues in EEM2 Mac

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


diff --git a/engines/eem/clues.cpp b/engines/eem/clues.cpp
index 398e1064b15..bf7b09e54a2 100644
--- a/engines/eem/clues.cpp
+++ b/engines/eem/clues.cpp
@@ -86,7 +86,7 @@ uint markClueBlockNotebookEntries(Mystery &mystery, const byte *clueBlock,
 	if (number == 0 || number > 32)
 		return 0;
 
-	// EEM2 entries are 84 bytes (0x54) with the notebook list at entry+0x40;
+	// EEM2 entries are 84 bytes (0x54) with the notebook list at entry+0x3c;
 	// EEM1 entries are 62 bytes with the list at entry+0x30. See displayClue.
 	const uint stride    = isLondon ? 0x54 : 62;
 	const uint noteOffat = isLondon ? 0x3c : 0x30;
@@ -740,8 +740,9 @@ void EEMEngine::doInitClues() {
 		displayFloppyBriefing(ib);
 	} else {
 		const byte *briefingClues = ib + 4;
-		// _DisplayClue calls _AddNotebook for each ClueEntry note list at
-		// +0x30..+0x39. Mark starting notes before the first PDA visit.
+		// _DisplayClue calls _AddNotebook for each ClueEntry note list
+		// (EEM1 +0x30..+0x39, EEM2 +0x3c..+0x45). Mark starting notes
+		// before the first PDA visit.
 		const uint marked = markClueBlockNotebookEntries(_mystery, briefingClues,
 														 isLondon());
 		if (marked != 0)
@@ -964,7 +965,8 @@ void EEMEngine::displayClue(const byte *clueBlock) {
 		}
 	}
 
-	// ClueEntry layout (62 bytes):
+	// ClueEntry layout. EEM1 entries are 62 bytes; EEM2/London entries
+	// extend this to 0x54 bytes and move the side-effect lists below.
 	//   +0..1, +2..3:   p0 tx, ty       +4..5, +6..7:   p1 tx, ty
 	//   +8..9, +10..11: bubText offset p0/p1 (rel. TextBlock; -1 = none)
 	//   +12..13, +14..15: balloon pic ID p0/p1
@@ -972,8 +974,9 @@ void EEMEngine::displayClue(const byte *clueBlock) {
 	//   +0x14/+0x16: bubX, bubY (partner 1)
 	//   +0x18: Jenny voice (1-based)
 	//   +0x1a: Jake  voice (1-based)
-	//   +0x30..+0x39: 5 notebook entries (-1 terminated)
-	//   +0x3a..+0x3b: KD-anim number (-1 = none)
+	//   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++) {
 		g_system->copyRectToScreen(bg.getPixels(), bg.pitch, 0, 0, sw, sh);
 		const byte *c = clueBlock + 4 + i * stride;
diff --git a/engines/eem/graphics.cpp b/engines/eem/graphics.cpp
index 6f220e2e26a..343653921a1 100644
--- a/engines/eem/graphics.cpp
+++ b/engines/eem/graphics.cpp
@@ -703,8 +703,12 @@ void EEMEngine::doInterfaceHelp(uint num) {
 	debugC(1, kDebugScript, "doInterfaceHelp(%u): showing pics 0x%x, 0x%x",
 		   num, kHelpPics[num][0], kHelpPics[num][1]);
 
+	const bool mac = isMacintosh();
+	const int sw = screenWidth();
+	const int sh = screenHeight();
+
 	// Snapshot caller's screen once: each PIC overlays the same clean BG.
-	Graphics::ManagedSurface bg(kScreenWidth, kScreenHeight,
+	Graphics::ManagedSurface bg(sw, sh,
 		Graphics::PixelFormat::createFormatCLUT8());
 	{
 		Graphics::Surface *cur = g_system->lockScreen();
@@ -727,14 +731,18 @@ void EEMEngine::doInterfaceHelp(uint num) {
 		debugC(1, kDebugScript, "doInterfaceHelp: pic 0x%x = %dx%d flags=0x%x",
 			   picId, pic.surface.w, pic.surface.h, pic.flags);
 
-		Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+		Graphics::ManagedSurface scratch(sw, sh,
 			Graphics::PixelFormat::createFormatCLUT8());
 		scratch.simpleBlitFrom(bg);
 		const byte transp = (byte)(pic.flags >> 8);
-		scratch.transBlitFrom(pic.surface, Common::Point(0, 0),
-							  (uint32)transp);
+		if (mac) {
+			blitMacMaskedSurface(scratch.surfacePtr(), pic, 0, 0);
+		} else {
+			scratch.transBlitFrom(pic.surface, Common::Point(0, 0),
+								  (uint32)transp);
+		}
 		g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
-								   0, 0, kScreenWidth, kScreenHeight);
+								   0, 0, sw, sh);
 		g_system->updateScreen();
 
 		bool escape = false;
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index fff43073d73..ab8cc8d99dd 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -554,20 +554,21 @@ constexpr Common::Rect kPdaPagePrevRect(Common::Point(226, 174), 21, 16);
 constexpr Common::Rect kPdaHelp2Rect(Common::Point(267, 174), 21, 16);
 constexpr Common::Rect kPdaLondonCloseRect(Common::Point(0, 0), 66, 79);
 
-// Mac _NotebookRect and _NoteButtons table at 0x177e. The coordinates are
-// native QuickDraw rects; keep them exact instead of deriving them from the
-// rounded DOS scaler.
+// 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 kMacPdaNotebookRect(Common::Point(214, 334), 34, 30);
-constexpr Common::Rect kMacPdaHelpRect(Common::Point(149, 334), 35, 30);
-constexpr Common::Rect kMacPdaGalleryRect(Common::Point(251, 334), 34, 30);
+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);
 constexpr Common::Rect kMacPdaPartnerHeadHintRect(Common::Point(13, 154), 57, 57);
 constexpr Common::Rect kMacPdaAccuseRect(Common::Point(288, 334), 33, 30);
-constexpr Common::Rect kMacPdaPageNextRect(Common::Point(325, 334), 33, 30);
-constexpr Common::Rect kMacPdaPagePrevRect(Common::Point(362, 334), 33, 30);
+constexpr Common::Rect kMacPdaPagePrevRect(Common::Point(112, 336), 48, 28);
+constexpr Common::Rect kMacPdaPageNextRect(Common::Point(452, 336), 42, 28);
+constexpr Common::Rect kMacPdaScrollBarRect(Common::Point(98, 336), 396, 48);
+constexpr int kMacPdaScrollBarMidX = 296;
 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(427, 334), 33, 30);
+constexpr Common::Rect kMacPdaHelp2Rect(Common::Point(413, 336), 34, 28);
 
 constexpr uint16 kProfilePickerRevealPic = 0x105;
 constexpr int kProfilePickerRevealX = 0x3e;
@@ -636,6 +637,28 @@ Common::Rect pdaControlRect(const EEMEngine *vm, const Common::Rect &rect) {
 	return vm->scaleRect(rect);
 }
 
+int macPdaScrollBarDelta(const EEMEngine *vm, int x, int y) {
+	if (!vm || !vm->isMacintosh() || !kMacPdaScrollBarRect.contains(x, y))
+		return 0;
+
+	static const Common::Rect kMacPdaScrollBarButtons[] = {
+		kMacPdaHelpRect,
+		kMacPdaNotebookRect,
+		kMacPdaGalleryRect,
+		kMacPdaAccuseRect,
+		kMacPdaHelp2Rect
+	};
+	for (uint i = 0; i < ARRAYSIZE(kMacPdaScrollBarButtons); i++) {
+		if (kMacPdaScrollBarButtons[i].contains(x, y))
+			return 0;
+	}
+	return x < kMacPdaScrollBarMidX ? -1 : 1;
+}
+
+bool macPdaScrollBarAt(const EEMEngine *vm, int x, int y) {
+	return macPdaScrollBarDelta(vm, x, y) != 0;
+}
+
 bool notebookButtonAt(const EEMEngine *vm, int x, int y) {
 	return pdaControlRect(vm, kPdaHelpRect).contains(x, y) ||
 		   pdaControlRect(vm, kPdaGalleryRect).contains(x, y) ||
@@ -645,7 +668,8 @@ 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);
+		   pdaControlRect(vm, kPdaSiteRect).contains(x, y) ||
+		   macPdaScrollBarAt(vm, x, y);
 }
 
 bool notebookButtonAt(int x, int y) {
@@ -667,6 +691,7 @@ bool galleryButtonAt(const EEMEngine *vm, int x, int y) {
 		   pdaControlRect(vm, kPdaAccuseRect).contains(x, y) ||
 		   pdaControlRect(vm, kPdaNotebookRect).contains(x, y) ||
 		   pdaControlRect(vm, kPdaHelpRect).contains(x, y) ||
+		   pdaControlRect(vm, kPdaHelp2Rect).contains(x, y) ||
 		   pdaControlRect(vm, kPdaPartnerHeadHintRect).contains(x, y);
 }
 
@@ -707,6 +732,18 @@ bool gallerySlotAt(const Common::Array<Common::Rect> &rects,
 	return false;
 }
 
+void blitTravisBackground(Graphics::ManagedSurface &dst, const Picture &pic,
+						  bool mac) {
+	if (mac) {
+		Graphics::ManagedSurface bg;
+		bg.copyFrom(pic.surface);
+		remapMacSurfaceEndpoints(bg, getMacSpritePaletteMap());
+		dst.simpleBlitFrom(bg);
+	} else {
+		dst.simpleBlitFrom(pic.surface);
+	}
+}
+
 const byte *advanceFloppyDialogRecords(const byte *rec, uint count,
 									   const byte *end, bool mac = false) {
 	if (!rec || (!end && count != 0))
@@ -3060,6 +3097,19 @@ void EEMEngine::doNotebook() {
 					dirty = true;
 					continue;
 				}
+				const int scrollDelta =
+					macPdaScrollBarDelta(this, ev.mouse.x, ev.mouse.y);
+				if (scrollDelta < 0) {
+					if (page > 0)
+						page--;
+					dirty = true;
+					continue;
+				}
+				if (scrollDelta > 0) {
+					page++;
+					dirty = true;
+					continue;
+				}
 			}
 		}
 		if (exitFlag)
@@ -3130,7 +3180,7 @@ void EEMEngine::drawNotebookFrame(int &page) {
 
 	Picture frame;
 	if (_picsArchive.getPicture(0x3f, frame))
-		scratch.simpleBlitFrom(frame.surface);
+		blitTravisBackground(scratch, frame, isMacintosh());
 
 	blitPdaPartner(scratch, _aniArchive, _partner, kPdaNotebookPartner,
 				   g_system->getMillis(), isMacintosh());
@@ -3342,6 +3392,15 @@ void EEMEngine::doGallery() {
 					lastDraw = 0;
 					continue;
 				}
+				if (pdaControlRect(this, kPdaHelp2Rect).contains(ev.mouse.x,
+																 ev.mouse.y)) {
+					setInteractiveMouseCursor(false);
+					doInterfaceHelp(0);
+					if (isMacintosh())
+						setSitePalette(0);
+					lastDraw = 0;
+					continue;
+				}
 				if (pdaControlRect(this, kPdaPartnerHeadHintRect)
 						.contains(ev.mouse.x, ev.mouse.y)) {
 					setInteractiveMouseCursor(false);
@@ -3440,7 +3499,7 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 			Graphics::PixelFormat::createFormatCLUT8());
 		ms.clear();
 		if (haveBg)
-			ms.simpleBlitFrom(galBg.surface);
+			blitTravisBackground(ms, galBg, mac);
 
 		blitPdaPartner(ms, _aniArchive, _partner, kPdaGalleryPartner,
 					   g_system->getMillis(), mac);
@@ -3490,8 +3549,7 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 				// Defer to next page.
 				break;
 			}
-			const byte color = _mystery._noteSelected[clueId]
-								   ? 0x3C : 0x5C;
+			const byte color = _mystery._noteSelected[clueId] ? 0x3C : 0x5C;
 			for (uint l = 0; l < wrapped.size(); l++) {
 				_font.drawString(&ms, wrapped[l], rx,
 					yPos + (int)l * lineH, MAX<int>(8, rw), color);
@@ -3557,6 +3615,14 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 					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) ||
+						(scrollDelta < 0 && hasPrev) ||
+						(scrollDelta > 0 && hasMore));
+				}
 				if (e2.type == Common::EVENT_LBUTTONDOWN) {
 					const int mx = e2.mouse.x;
 					const int my = e2.mouse.y;
@@ -3612,6 +3678,18 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 							prev = true;
 						break;
 					}
+					const int scrollDelta =
+						macPdaScrollBarDelta(this, mx, my);
+					if (scrollDelta < 0) {
+						if (hasPrev)
+							prev = true;
+						break;
+					}
+					if (scrollDelta > 0) {
+						if (hasMore)
+							advance = true;
+						break;
+					}
 					if (pdaControlRect(this, kPdaPartnerHeadHintRect).contains(mx, my)) {
 						// Case 3: _KDHelp.
 						setInteractiveMouseCursor(false);
@@ -3677,7 +3755,7 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
 	scratch.clear();
 
 	if (haveBg)
-		scratch.simpleBlitFrom(galBg.surface);
+		blitTravisBackground(scratch, galBg, mac);
 
 	blitPdaPartner(scratch, _aniArchive, _partner, kPdaGalleryPartner,
 				   g_system->getMillis(), mac);
@@ -4607,7 +4685,9 @@ Common::String EEMEngine::accuseNoteText(uint clueId,
 			(const char *)(ctx.bufBaseNotes + textOff),
 			_playerName, _partner);
 	}
-	if (isMacintosh() && ctx.bufBaseNotes) {
+	// 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) {
 		const uint16 textOff = READ_LE_UINT16(ctx.ni + clueId * 8);
 		if (textOff == 0 || textOff >= _mystery.dataSize())
 			return Common::String();
@@ -4658,7 +4738,7 @@ void EEMEngine::accuseDrawScreen(const AccuseNotesCtx &ctx) {
 		Graphics::PixelFormat::createFormatCLUT8());
 	scratch.clear();
 	if (ctx.haveBg)
-		scratch.simpleBlitFrom(ctx.accuseBg->surface);
+		blitTravisBackground(scratch, *ctx.accuseBg, mac);
 
 	blitPdaPartner(scratch, _aniArchive, _partner, kPdaGalleryPartner,
 				   g_system->getMillis(), mac);
@@ -4738,6 +4818,9 @@ bool EEMEngine::doAccuseNotes() {
 	if (!ni)
 		return false;
 
+	if (isMacintosh())
+		setSitePalette(0);
+
 	Picture accuseBg;
 	const bool haveBg = _picsArchive.getPicture(0x1a7, accuseBg);
 
@@ -4894,6 +4977,21 @@ bool EEMEngine::doAccuseNotes() {
 					}
 					continue;
 				}
+				const int scrollDelta = macPdaScrollBarDelta(this, mx, my);
+				if (scrollDelta < 0) {
+					if (page > 0) {
+						page--;
+						dirty = true;
+					}
+					continue;
+				}
+				if (scrollDelta > 0) {
+					if (page + 1 < numPages) {
+						page++;
+						dirty = true;
+					}
+					continue;
+				}
 				if (btnPartner.contains(mx, my)) {
 					doHelp();
 					if (isMacintosh())
@@ -4907,9 +5005,8 @@ bool EEMEngine::doAccuseNotes() {
 						if (_mystery._noteSelected[found[i]])
 							selected++;
 					}
-					if (selected == expected) {
+					if (selected == expected)
 						return true;
-					}
 					continue;
 				}
 				// Toggle clue under cursor.


Commit: ee8eee5869c1bb08372f02d27eb69279129a3c5a
    https://github.com/scummvm/scummvm/commit/ee8eee5869c1bb08372f02d27eb69279129a3c5a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-26T20:37:27+02:00

Commit Message:
EEM: improved setup screen in EEM2 Mac

Changed paths:
    engines/eem/ui.cpp


diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index ab8cc8d99dd..178fb2f8a9e 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -392,8 +392,25 @@ constexpr Common::Rect kLonSetMusicOff(Common::Point(128,  85), 18, 7); // 0x142
 constexpr Common::Rect kLonSetHiOn    (Common::Point(106, 110), 29, 8); // 0x1414
 constexpr Common::Rect kLonSetHiOff   (Common::Point(106, 100), 29, 8); // 0x140c
 
+// Mac setup PIC 0x40 is native 512x384 art. The selected-option labels are
+// encoded as 0xfe mask components in the picture itself, not DOS-scaled rects.
+constexpr Common::Rect kMacLonSetJake    (Common::Point(172,  87), 75, 14);
+constexpr Common::Rect kMacLonSetJenny   (Common::Point(172, 106), 75, 14);
+constexpr Common::Rect kMacLonSetVoiceOn (Common::Point(172, 139), 28, 14);
+constexpr Common::Rect kMacLonSetVoiceOff(Common::Point(205, 139), 28, 14);
+constexpr Common::Rect kMacLonSetMusicOn (Common::Point(172, 166), 28, 14);
+constexpr Common::Rect kMacLonSetMusicOff(Common::Point(205, 166), 28, 14);
+constexpr Common::Rect kMacLonSetHiOn    (Common::Point(172, 222), 44, 14);
+constexpr Common::Rect kMacLonSetHiOff   (Common::Point(172, 203), 44, 14);
+
+Common::Rect londonSetupTextRect(const EEMEngine *vm,
+								 const Common::Rect &dosRect,
+								 const Common::Rect &macRect) {
+	return vm && vm->isMacintosh() ? macRect : dosRect;
+}
+
 void swapColors(Graphics::ManagedSurface &dst,
-					   const Common::Rect &r, byte from, byte to) {
+						   const Common::Rect &r, byte from, byte to) {
 	const int x1 = MAX<int>(0, r.left);
 	const int y1 = MAX<int>(0, r.top);
 	const int x2 = MIN<int>(dst.w, r.right);
@@ -2277,7 +2294,10 @@ void EEMEngine::setupLeave() {
 }
 // `_SetupSettings @ 2046:0008`
 void EEMEngine::setupDrawScreenLondon() {
-	Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+	if (isMacintosh())
+		setSitePalette(0);
+
+	Graphics::ManagedSurface scratch(screenWidth(), screenHeight(),
 		Graphics::PixelFormat::createFormatCLUT8());
 	scratch.clear();
 	Picture bg;
@@ -2286,21 +2306,29 @@ void EEMEngine::setupDrawScreenLondon() {
 
 	const byte kKey = 0xFE, kBright = 0x15, kDim = 0x00;
 
-	swapColors(scratch, kLonSetJake,  kKey, _partner == kPartnerJake  ? kBright : kDim);
-	swapColors(scratch, kLonSetJenny, kKey, _partner == kPartnerJenny ? kBright : kDim);
+	swapColors(scratch, londonSetupTextRect(this, kLonSetJake, kMacLonSetJake),
+			   kKey, _partner == kPartnerJake ? kBright : kDim);
+	swapColors(scratch, londonSetupTextRect(this, kLonSetJenny, kMacLonSetJenny),
+			   kKey, _partner == kPartnerJenny ? kBright : kDim);
 
-	swapColors(scratch, kLonSetVoiceOn,  kKey, _voiceOn ? kBright : kDim);
-	swapColors(scratch, kLonSetVoiceOff, kKey, _voiceOn ? kDim : kBright);
+	swapColors(scratch, londonSetupTextRect(this, kLonSetVoiceOn, kMacLonSetVoiceOn),
+			   kKey, _voiceOn ? kBright : kDim);
+	swapColors(scratch, londonSetupTextRect(this, kLonSetVoiceOff, kMacLonSetVoiceOff),
+			   kKey, _voiceOn ? kDim : kBright);
 
-	swapColors(scratch, kLonSetMusicOn,  kKey, _musicOn ? kBright : kDim);
-	swapColors(scratch, kLonSetMusicOff, kKey, _musicOn ? kDim : kBright);
+	swapColors(scratch, londonSetupTextRect(this, kLonSetMusicOn, kMacLonSetMusicOn),
+			   kKey, _musicOn ? kBright : kDim);
+	swapColors(scratch, londonSetupTextRect(this, kLonSetMusicOff, kMacLonSetMusicOff),
+			   kKey, _musicOn ? kDim : kBright);
 
 	const bool hiOn = !ConfMan.getBool("hide_highlight_boxes");
-	swapColors(scratch, kLonSetHiOn,  kKey, hiOn ? kBright : kDim);
-	swapColors(scratch, kLonSetHiOff, kKey, hiOn ? kDim : kBright);
+	swapColors(scratch, londonSetupTextRect(this, kLonSetHiOn, kMacLonSetHiOn),
+			   kKey, hiOn ? kBright : kDim);
+	swapColors(scratch, londonSetupTextRect(this, kLonSetHiOff, kMacLonSetHiOff),
+			   kKey, hiOn ? kDim : kBright);
 
 	g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
-							   0, 0, kScreenWidth, kScreenHeight);
+							   0, 0, scratch.w, scratch.h);
 	g_system->updateScreen();
 }
 
@@ -2308,19 +2336,19 @@ void EEMEngine::setupShowSavedConfirm() {
 	Picture pic;
 	if (!_picsArchive.getPicture(0x203, pic) || pic.surface.empty())
 		return;
-	Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+	Graphics::ManagedSurface scratch(screenWidth(), screenHeight(),
 		Graphics::PixelFormat::createFormatCLUT8());
 	Graphics::Surface *cur = g_system->lockScreen();
 	if (cur) {
 		scratch.simpleBlitFrom(*cur);
 		g_system->unlockScreen();
 	}
-	const int sx = MAX<int>(0, (kScreenWidth  - pic.surface.w) / 2);
-	const int sy = MAX<int>(0, (kScreenHeight - pic.surface.h) / 2);
+	const int sx = MAX<int>(0, (scratch.w - pic.surface.w) / 2);
+	const int sy = MAX<int>(0, (scratch.h - pic.surface.h) / 2);
 	scratch.transBlitFrom(pic.surface, Common::Point(sx, sy),
 						  (uint32)(byte)(pic.flags >> 8));
 	g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
-							   0, 0, kScreenWidth, kScreenHeight);
+							   0, 0, scratch.w, scratch.h);
 	g_system->updateScreen();
 	while (!shouldQuit()) {
 		Common::Event ev;
@@ -2374,7 +2402,10 @@ void EEMEngine::doSetupLondon() {
 			}
 			if (ev.type != Common::EVENT_LBUTTONDOWN)
 				continue;
-			const int mx = ev.mouse.x, my = ev.mouse.y;
+			const Common::Point mouse(unscaleX(ev.mouse.x),
+									  unscaleY(ev.mouse.y));
+			const int mx = mouse.x;
+			const int my = mouse.y;
 
 			if (kPartnerBtn.contains(mx, my)) {
 				_partner = (_partner == kPartnerJake) ? kPartnerJenny


Commit: 4ec3f34aec0ae977906925c0d7a02f05f7e8a1c8
    https://github.com/scummvm/scummvm/commit/4ec3f34aec0ae977906925c0d7a02f05f7e8a1c8
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-26T20:37:27+02:00

Commit Message:
EEM: implemented approach screens in EEM2 Mac

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


diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 3c75db5cb99..66c9ea28a74 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -413,6 +413,83 @@ static void addMacResourceSearchPaths() {
 		SearchMan.addDirectory("eem-mac-rsrc-sibling", siblingRsrcDir);
 }
 
+Common::FSNode findChildDirectoryIgnoreCase(const Common::FSNode &dir,
+											const char *name) {
+	Common::FSList children;
+	if (!dir.exists() || !dir.isDirectory() ||
+		!dir.getChildren(children, Common::FSNode::kListDirectoriesOnly))
+		return Common::FSNode();
+
+	for (Common::FSList::const_iterator it = children.begin();
+		 it != children.end(); ++it) {
+		if (it->getName().equalsIgnoreCase(name))
+			return *it;
+	}
+
+	return Common::FSNode();
+}
+
+void addMacLondonDirectoryIfPresent(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) {
+	if (!base.exists() || !base.isDirectory())
+		return;
+
+	Common::FSNode cd = findChildDirectoryIgnoreCase(base, "EEM2 CD");
+	if (!cd.exists()) {
+		Common::FSNode wrapper = findChildDirectoryIgnoreCase(base, "EEM_London");
+		if (wrapper.exists())
+			cd = findChildDirectoryIgnoreCase(wrapper, "EEM2 CD");
+	}
+	if (!cd.exists() && base.getName().equalsIgnoreCase("EEM2 CD"))
+		cd = base;
+	if (!cd.exists()) {
+		const Common::String baseName = base.getName();
+		if (baseName.equalsIgnoreCase("Data Files") ||
+			baseName.equalsIgnoreCase("Mac Scripts") ||
+			baseName.equalsIgnoreCase("Anim Files"))
+			cd = base.getParent();
+	}
+	if (!cd.exists())
+		return;
+
+	Common::String dataArchive = Common::String::format(
+		"%s-data-files", archivePrefix);
+	Common::String scriptsArchive = Common::String::format(
+		"%s-mac-scripts", archivePrefix);
+	Common::String animArchive = Common::String::format(
+		"%s-anim-files", archivePrefix);
+	Common::String appArchive = Common::String::format(
+		"%s-app", archivePrefix);
+
+	const Common::FSNode dataDir = findChildDirectoryIgnoreCase(cd, "Data Files");
+	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);
+
+	if (scriptsDir.exists()) {
+		Common::String approachesArchive = Common::String::format(
+			"%s-approaches", archivePrefix);
+		addMacLondonDirectoryIfPresent(
+			findChildDirectoryIgnoreCase(scriptsDir, "Approaches"),
+			approachesArchive.c_str());
+	}
+
+	const Common::FSNode app =
+		findChildDirectoryIgnoreCase(cd.getParent(), "EEM London CD");
+	addMacLondonDirectoryIfPresent(app, appArchive.c_str(), 2);
+}
+
 static bool loadMacFontResource(EEMFont &font, uint16 resourceId, int size) {
 	addMacResourceSearchPaths();
 
@@ -855,17 +932,19 @@ bool EEMEngine::openArchives() {
 	// the disc root or at the "EEM2 CD" folder.
 	if (mac && isLondon()) {
 		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");
-		SearchMan.addSubDirectoryMatching(gameDir, "EEM2 CD/Mac Scripts");
-		SearchMan.addSubDirectoryMatching(gameDir, "EEM2 CD/Anim Files");
-		SearchMan.addSubDirectoryMatching(gameDir, "EEM London CD");
+		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");
-		SearchMan.addSubDirectoryMatching(gameDir, "Mac Scripts");
-		SearchMan.addSubDirectoryMatching(gameDir, "Anim Files");
+		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.
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index d9f888732ce..ec49e304f40 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -973,18 +973,20 @@ void SiteScreen::enter(uint siteNum, bool resetPartnerMood) {
 
 	const byte *sd = _mystery->siteData(siteNum);
 	const bool playArrival = _vm->shouldPlaySiteArrival(siteNum);
+	const bool london = _vm->isLondon();
+	const uint16 approachId = (london && sd) ? READ_LE_UINT16(sd + 2) : 0xffff;
 
-	if (playArrival) {
-		if (_vm->isLondon()) {
-			bool showedApproach = false;
-			const uint16 approachId = sd ? READ_LE_UINT16(sd + 2) : 0xffff;
-			if (firstVisit && approachId != 0xffff)
-				showedApproach = _vm->doLondonApproach(approachId);
-			if (!showedApproach)
-				playLondonTravelAnimation(_mystery->_lastSite, siteNum);
-		} else {
-			_vm->startTravelMusic();
+	if (london) {
+		if (playArrival)
+			playLondonTravelAnimation(_mystery->_lastSite, siteNum);
+		if (firstVisit && approachId != 0xffff) {
+			debugC(1, kDebugSite,
+				   "London approach: site %u first visit, approach %u",
+				   siteNum, approachId);
+			_vm->doLondonApproach(approachId);
 		}
+	} else if (playArrival) {
+		_vm->startTravelMusic();
 	}
 
 	const bool compactSite = _vm->isFloppy() ||
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 178fb2f8a9e..1eb8624e259 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -87,6 +87,18 @@ const LondonApproachButton kLondonApproachButtons[4] = {
 	{ 287, 139, 307, 157, 287, 139, 0x360 }, // Previous
 };
 
+const uint16 kMacLondonApproachBackgroundBasePic = 0x38c;
+const int kMacLondonApproachMovieX = 114;
+const int kMacLondonApproachMovieY = 23;
+const int kMacLondonApproachPlayLeft = 9;
+const int kMacLondonApproachPlayTop = 277;
+const int kMacLondonApproachPlayRight = 60;
+const int kMacLondonApproachPlayBottom = 332;
+const int kMacLondonApproachDoneLeft = 452;
+const int kMacLondonApproachDoneTop = 277;
+const int kMacLondonApproachDoneRight = 504;
+const int kMacLondonApproachDoneBottom = 332;
+
 byte mapVisitedMarkerColor(byte color) {
 	switch (color) {
 	case 0xf7:
@@ -248,8 +260,17 @@ static uint16 readScriptU16(const byte *p, bool bigEndian) {
 	return bigEndian ? READ_BE_UINT16(p) : READ_LE_UINT16(p);
 }
 
+Common::String cleanLondonApproachPage(const byte *start, uint32 len) {
+	Common::String page((const char *)start, len);
+	while (!page.empty() &&
+		   (page.lastChar() == '\r' || page.lastChar() == '\n' ||
+			(byte)page.lastChar() == 0xff))
+		page.deleteLastChar();
+	return page;
+}
+
 bool loadLondonApproachData(uint16 approachId, LondonApproachData &out,
-							bool macintosh) {
+							 bool macintosh) {
 	static const char *const kDosPatterns[] = {
 		"A%u.BIN"
 	};
@@ -288,11 +309,19 @@ bool loadLondonApproachData(uint16 approachId, LondonApproachData &out,
 	out.videoId = readScriptU16(data.data() + 0, macintosh);
 	out.videoX = readScriptU16(data.data() + 2, macintosh);
 	out.videoY = readScriptU16(data.data() + 4, macintosh);
-	const int16 x1 = (int16)readScriptU16(data.data() + 6, macintosh);
-	const int16 y1 = (int16)readScriptU16(data.data() + 8, macintosh);
-	const int16 x2 = (int16)readScriptU16(data.data() + 10, macintosh);
-	const int16 y2 = (int16)readScriptU16(data.data() + 12, macintosh);
-	out.textRect = Common::Rect(x1, y1, x2, y2);
+	if (macintosh) {
+		const int16 top = (int16)readScriptU16(data.data() + 6, true);
+		const int16 left = (int16)readScriptU16(data.data() + 8, true);
+		const int16 bottom = (int16)readScriptU16(data.data() + 10, true);
+		const int16 right = (int16)readScriptU16(data.data() + 12, true);
+		out.textRect = Common::Rect(left, top, right, bottom);
+	} else {
+		const int16 x1 = (int16)readScriptU16(data.data() + 6, false);
+		const int16 y1 = (int16)readScriptU16(data.data() + 8, false);
+		const int16 x2 = (int16)readScriptU16(data.data() + 10, false);
+		const int16 y2 = (int16)readScriptU16(data.data() + 12, false);
+		out.textRect = Common::Rect(x1, y1, x2, y2);
+	}
 
 	const uint16 pageCount = readScriptU16(data.data() + 14, macintosh);
 	out.pages.clear();
@@ -303,8 +332,8 @@ bool loadLondonApproachData(uint16 approachId, LondonApproachData &out,
 		const uint32 start = pos;
 		while (pos < size && data[pos] != '\n')
 			pos++;
-		out.pages.push_back(Common::String((const char *)data.data() + start,
-										   pos - start));
+		out.pages.push_back(cleanLondonApproachPage(data.data() + start,
+													pos - start));
 		if (pos < size)
 			pos++;  // skip the '\n' separator
 	}
@@ -376,6 +405,39 @@ bool decodeLondonApproachFirstFrame(uint16 videoId,
 	return true;
 }
 
+byte closestPaletteIndex(const byte *palette, byte r, byte g, byte b,
+						 byte fallback) {
+	if (!palette)
+		return fallback;
+
+	uint bestDist = 0xffffffff;
+	byte best = fallback;
+	for (uint i = 0; i < 256; i++) {
+		const int dr = (int)palette[i * 3 + 0] - r;
+		const int dg = (int)palette[i * 3 + 1] - g;
+		const int db = (int)palette[i * 3 + 2] - b;
+		const uint dist = (uint)(dr * dr + dg * dg + db * db);
+		if (dist < bestDist) {
+			bestDist = dist;
+			best = (byte)i;
+		}
+	}
+	return best;
+}
+
+void remapSurfaceColor(Graphics::ManagedSurface &surface, byte from, byte to) {
+	if (surface.empty() || from == to)
+		return;
+
+	for (int y = 0; y < surface.h; y++) {
+		byte *row = (byte *)surface.getBasePtr(0, y);
+		for (int x = 0; x < surface.w; x++) {
+			if (row[x] == from)
+				row[x] = to;
+		}
+	}
+}
+
 // Setup-screen highlight rects
 constexpr Common::Rect kSetupKid1Rect    (Common::Point( 99,  44),  49,  8);
 constexpr Common::Rect kSetupKid2Rect    (Common::Point( 99,  54),  49,  8);
@@ -4245,9 +4307,6 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
 
 	const int sw = screenWidth();
 	const int sh = screenHeight();
-	MacSpritePaletteMap macPaletteMap = {0x00, 0xFF};
-	if (mac)
-		macPaletteMap = getMacSpritePaletteMap();
 
 	Graphics::ManagedSurface base(sw, sh,
 		Graphics::PixelFormat::createFormatCLUT8());
@@ -4256,38 +4315,67 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
 		decodeLondonApproachFirstFrame(data.videoId, base, palette, mac);
 	if (!haveVideo)
 		base.clear();
+	if (mac) {
+		Picture background;
+		const uint16 backgroundPic =
+			kMacLondonApproachBackgroundBasePic + data.videoId;
+		if (_picsArchive.getPicture(backgroundPic, background) &&
+			!background.surface.empty()) {
+			if (haveVideo) {
+				const byte black =
+					closestPaletteIndex(palette, 0x00, 0x00, 0x00, 0xfe);
+				remapSurfaceColor(background.surface, 0xff, black);
+			}
+			base.clear();
+			base.simpleBlitFrom(background.surface);
+		}
+	}
 
 	Picture buttonPics[ARRAYSIZE(kLondonApproachButtons)];
 	bool haveButtons[ARRAYSIZE(kLondonApproachButtons)] = {};
-	for (uint i = 0; i < ARRAYSIZE(kLondonApproachButtons); i++)
-		haveButtons[i] = _picsArchive.getPicture(
+	for (uint i = 0; i < ARRAYSIZE(kLondonApproachButtons); i++) {
+		// DOS `_DoApproach` loads PICS 0x361/0x362/0x35f/0x360 as the
+		// four control sprites. In the Mac London data those IDs are
+		// full-screen scrapbook pages, not buttons.
+		haveButtons[i] = !mac && _picsArchive.getPicture(
 			kLondonApproachButtons[i].picId, buttonPics[i]);
+	}
 
 	auto buttonRect = [&](uint idx) {
-		const Common::Rect r = kLondonApproachButtons[idx].rect();
-		return mac ? scaleRect(r) : r;
+		return kLondonApproachButtons[idx].rect();
 	};
 	auto buttonPoint = [&](uint idx) {
 		const LondonApproachButton &b = kLondonApproachButtons[idx];
-		return Common::Point(mac ? scaleX(b.x) : b.x,
-							 mac ? scaleY(b.y) : b.y);
+		return Common::Point(b.x, b.y);
 	};
-
-	auto drawButtonFallback = [&](Graphics::ManagedSurface &dst, uint idx) {
-		static const char *const kLabels[4] = { "OK", "PLAY", ">", "<" };
-		const Common::Rect r = buttonRect(idx);
-		dst.fillRect(r, 0);
-		_font.drawString(&dst, kLabels[idx], r.left + 1, r.top + 5,
-						 r.width(), 0x0f);
+	const int movieX = mac ? kMacLondonApproachMovieX : (int)data.videoX;
+	const int movieY = mac ? kMacLondonApproachMovieY : (int)data.videoY;
+	auto approachTextRect = [&]() {
+		Common::Rect textRect = data.textRect;
+		if (mac)
+			textRect = scaleRect(textRect);
+		return textRect.findIntersectingRect(Common::Rect(sw, sh));
 	};
+	auto macApproachButton = [&](const Common::Point &mouse) {
+		if (mouse.x >= kMacLondonApproachDoneLeft &&
+			mouse.x < kMacLondonApproachDoneRight &&
+			mouse.y >= kMacLondonApproachDoneTop &&
+			mouse.y < kMacLondonApproachDoneBottom)
+			return 0;
+		if (mouse.x >= kMacLondonApproachPlayLeft &&
+			mouse.x < kMacLondonApproachPlayRight &&
+			mouse.y >= kMacLondonApproachPlayTop &&
+			mouse.y < kMacLondonApproachPlayBottom)
+			return 1;
+		return -1;
+	};
+	const byte approachTextColor =
+		closestPaletteIndex(haveVideo ? palette : nullptr, 0, 0, 0,
+							mac ? (byte)0xfe : (byte)1);
 
-	auto drawScreen = [&](uint page) {
-		Graphics::ManagedSurface scratch(sw, sh,
-			Graphics::PixelFormat::createFormatCLUT8());
-		scratch.simpleBlitFrom(base);
-
-		const Common::Rect textRect =
-			data.textRect.findIntersectingRect(Common::Rect(sw, sh));
+	auto drawApproachOverlay = [&](Graphics::ManagedSurface &scratch,
+								   uint page) {
+		const Common::Rect textRect = approachTextRect();
 		if (!textRect.isEmpty()) {
 			if (page < data.pages.size()) {
 				Common::Array<Common::String> wrapped;
@@ -4299,7 +4387,7 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
 					_font.drawString(&scratch, wrapped[i], textRect.left,
 									 textRect.top + (int)i * lineH,
 									 textRect.width(),
-									 mac ? macPaletteMap.black : 1);
+									 approachTextColor);
 				}
 			}
 		}
@@ -4307,24 +4395,25 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
 		for (uint i = 0; i < ARRAYSIZE(kLondonApproachButtons); i++) {
 			if (haveButtons[i]) {
 				const Common::Point p = buttonPoint(i);
-				if (mac)
-					blitMacMaskedSurface(scratch.surfacePtr(),
-										 buttonPics[i], p.x, p.y, false,
-										 macPaletteMap);
-				else
-					scratch.transBlitFrom(buttonPics[i].surface, p,
-										  (uint32)(byte)(buttonPics[i].flags >> 8));
-			} else {
-				drawButtonFallback(scratch, i);
+				scratch.transBlitFrom(buttonPics[i].surface, p,
+									  (uint32)(byte)(buttonPics[i].flags >> 8));
 			}
 		}
+	};
+
+	auto drawScreen = [&](uint page) {
+		Graphics::ManagedSurface scratch(sw, sh,
+			Graphics::PixelFormat::createFormatCLUT8());
+		scratch.clear();
+		scratch.simpleBlitFrom(base);
+		drawApproachOverlay(scratch, page);
 
 		g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
 								   0, 0, sw, sh);
 		g_system->updateScreen();
 	};
 
-	auto playVideo = [&]() {
+	auto playVideo = [&](uint page) {
 		if (mac) {
 			Video::FlicDecoder flic;
 			Common::String name;
@@ -4335,22 +4424,24 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
 			}
 
 			flic.start();
-			(void)flic.decodeNextFrame(); // first frame is already the background
-			const int videoBandH = scaleY(0x82);
 			while (!shouldQuit() && !flic.endOfVideo()) {
 				const Graphics::Surface *frame = flic.decodeNextFrame();
 				if (!frame)
 					break;
-				const int copyW = MIN<int>(frame->w - (int)data.videoX,
-										   sw - (int)data.videoX);
-				const int copyH = MIN<int>(videoBandH,
-					MIN<int>(frame->h - (int)data.videoY,
-							 sh - (int)data.videoY));
+				Graphics::ManagedSurface scratch(sw, sh,
+					Graphics::PixelFormat::createFormatCLUT8());
+				scratch.clear();
+				scratch.simpleBlitFrom(base);
+				const int copyW = MIN<int>(frame->w, sw - movieX);
+				const int copyH = MIN<int>(frame->h, sh - movieY);
 				if (copyW <= 0 || copyH <= 0)
 					break;
-				g_system->copyRectToScreen(
-					(const byte *)frame->getBasePtr(data.videoX, data.videoY),
-					frame->pitch, data.videoX, data.videoY, copyW, copyH);
+				scratch.copyRectToSurface(frame->getPixels(), frame->pitch,
+										  movieX, movieY,
+										  copyW, copyH);
+				drawApproachOverlay(scratch, page);
+				g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+										   0, 0, sw, sh);
 				if (flic.hasDirtyPalette()) {
 					const byte *fpal = flic.getPalette();
 					if (fpal)
@@ -4359,8 +4450,9 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
 				g_system->updateScreen();
 
 				const uint32 start = g_system->getMillis();
+				const uint32 delay = MAX<uint32>(10, flic.getTimeToNextFrame());
 				bool skip = false;
-				while (g_system->getMillis() - start < 120 && !skip) {
+				while (g_system->getMillis() - start < delay && !skip) {
 					Common::Event ev;
 					while (g_system->getEventManager()->pollEvent(ev)) {
 						if (ev.type == Common::EVENT_QUIT ||
@@ -4389,11 +4481,10 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
 		}
 
 		// Frame 0 is already the static background. Replay frames 1..end
-		// into the upper video area, leaving text/buttons untouched.
+		// with the approach text/buttons composited above it.
 		(void)anm.nextFrame();
 		const int copyW = MIN<int>(anm.width(), sw - (int)data.videoX);
-		const int copyH = MIN<int>(0x82, MIN<int>(anm.height(),
-			sh - (int)data.videoY));
+		const int copyH = MIN<int>(anm.height(), sh - (int)data.videoY);
 		if (copyW <= 0 || copyH <= 0)
 			return;
 
@@ -4401,9 +4492,15 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
 			const byte *frame = anm.nextFrame();
 			if (!frame)
 				break;
-			g_system->copyRectToScreen(
-				frame + data.videoY * anm.width() + data.videoX,
-				anm.width(), data.videoX, data.videoY, copyW, copyH);
+			Graphics::ManagedSurface scratch(sw, sh,
+				Graphics::PixelFormat::createFormatCLUT8());
+			scratch.clear();
+			scratch.copyRectToSurface(frame, anm.width(),
+									  data.videoX, data.videoY,
+									  copyW, copyH);
+			drawApproachOverlay(scratch, page);
+			g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+									   0, 0, sw, sh);
 			g_system->updateScreen();
 
 			const uint32 start = g_system->getMillis();
@@ -4440,6 +4537,10 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
 		fadePaletteFromBlack(palette);
 	else
 		setSitePalette(0x3b);
+	if (haveVideo) {
+		playVideo(page);
+		drawScreen(page);
+	}
 	bool done = false;
 	uint32 lastShimmer = g_system->getMillis();
 	while (!shouldQuit() && !done) {
@@ -4452,10 +4553,15 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
 			}
 			if (ev.type == Common::EVENT_MOUSEMOVE) {
 				bool overButton = false;
-				for (uint i = 0; i < ARRAYSIZE(kLondonApproachButtons); i++) {
-					if (buttonRect(i).contains(ev.mouse.x, ev.mouse.y)) {
-						overButton = true;
-						break;
+				if (mac) {
+					overButton = macApproachButton(
+						Common::Point(ev.mouse.x, ev.mouse.y)) >= 0;
+				} else {
+					for (uint i = 0; i < ARRAYSIZE(kLondonApproachButtons); i++) {
+						if (buttonRect(i).contains(ev.mouse.x, ev.mouse.y)) {
+							overButton = true;
+							break;
+						}
 					}
 				}
 				setInteractiveMouseCursor(overButton);
@@ -4468,7 +4574,7 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
 					break;
 				}
 				if (ev.kbd.keycode == Common::KEYCODE_SPACE) {
-					playVideo();
+					playVideo(page);
 					drawScreen(page);
 				} else if (ev.kbd.keycode == Common::KEYCODE_RIGHT ||
 						   ev.kbd.keycode == Common::KEYCODE_DOWN) {
@@ -4485,12 +4591,25 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
 				}
 			}
 			if (ev.type == Common::EVENT_LBUTTONDOWN) {
+				if (mac) {
+					const int button = macApproachButton(
+						Common::Point(ev.mouse.x, ev.mouse.y));
+					if (button == 0) {
+						done = true;
+						break;
+					}
+					if (button == 1) {
+						playVideo(page);
+						drawScreen(page);
+					}
+					continue;
+				}
 				if (buttonRect(0).contains(ev.mouse.x, ev.mouse.y)) {
 					done = true;
 					break;
 				}
 				if (buttonRect(1).contains(ev.mouse.x, ev.mouse.y)) {
-					playVideo();
+					playVideo(page);
 					drawScreen(page);
 				} else if (buttonRect(2).contains(ev.mouse.x, ev.mouse.y)) {
 					if (page + 1 < data.pages.size()) {
@@ -4505,7 +4624,7 @@ bool EEMEngine::doLondonApproach(uint16 approachId) {
 				}
 			}
 		}
-		if (haveVideo) {
+		if (haveVideo && !mac) {
 			const uint32 now = g_system->getMillis();
 			if (now - lastShimmer >= 70) {
 				lastShimmer = now;


Commit: 4762d3458d0625272035b430b82dea1b70223487
    https://github.com/scummvm/scummvm/commit/4762d3458d0625272035b430b82dea1b70223487
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-26T20:37:27+02:00

Commit Message:
EEM: refactored ui.cpp into two files

Changed paths:
  A engines/eem/map_ui.cpp
    engines/eem/module.mk
    engines/eem/ui.cpp


diff --git a/engines/eem/map_ui.cpp b/engines/eem/map_ui.cpp
new file mode 100644
index 00000000000..36eec5c649b
--- /dev/null
+++ b/engines/eem/map_ui.cpp
@@ -0,0 +1,1319 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/algorithm.h"
+#include "common/debug.h"
+#include "common/events.h"
+#include "common/file.h"
+#include "common/path.h"
+#include "common/system.h"
+#include "common/textconsole.h"
+
+#include "graphics/cursorman.h"
+#include "graphics/managed_surface.h"
+#include "graphics/paletteman.h"
+
+#include "video/flic_decoder.h"
+
+#include "eem/animation.h"
+#include "eem/eem.h"
+#include "eem/music.h"
+#include "eem/site.h"
+
+namespace EEM {
+
+struct LondonApproachData {
+	uint16 videoId = 0;
+	uint16 videoX = 0;
+	uint16 videoY = 0;
+	Common::Rect textRect;
+	Common::Array<Common::String> pages;
+};
+
+struct LondonApproachButton {
+	int x1;
+	int y1;
+	int x2;
+	int y2;
+	int x;
+	int y;
+	uint16 picId;
+
+	Common::Rect rect() const { return Common::Rect(x1, y1, x2, y2); }
+};
+
+const LondonApproachButton kLondonApproachButtons[4] = {
+	{  10, 139,  35, 157,  11, 139, 0x361 }, // Done
+	{  10, 172,  35, 190,  11, 172, 0x362 }, // Play
+	{ 287, 172, 307, 190, 287, 172, 0x35f }, // Next
+	{ 287, 139, 307, 157, 287, 139, 0x360 }, // Previous
+};
+
+const uint16 kMacLondonApproachBackgroundBasePic = 0x38c;
+const int kMacLondonApproachMovieX = 114;
+const int kMacLondonApproachMovieY = 23;
+const int kMacLondonApproachPlayLeft = 9;
+const int kMacLondonApproachPlayTop = 277;
+const int kMacLondonApproachPlayRight = 60;
+const int kMacLondonApproachPlayBottom = 332;
+const int kMacLondonApproachDoneLeft = 452;
+const int kMacLondonApproachDoneTop = 277;
+const int kMacLondonApproachDoneRight = 504;
+const int kMacLondonApproachDoneBottom = 332;
+
+byte mapVisitedMarkerColor(byte color) {
+	switch (color) {
+	case 0xf7:
+	case 0xfb:
+	case 0xfd:
+		return 0x1b;
+	case 0xf8:
+	case 0xf9:
+	case 0xfa:
+	case 0xfc:
+	case 0xfe:
+		return 0x19;
+	default:
+		return color;
+	}
+}
+
+void blitBigMapMarker(Graphics::ManagedSurface &dstSurface, const Picture &marker,
+					  int x, int y, bool useVisitedColors) {
+	const byte transp = (byte)(marker.flags >> 8);
+	for (int row = 0; row < marker.surface.h; row++) {
+		const int dstY = y + row;
+		if (dstY < 0 || dstY >= dstSurface.h)
+			continue;
+		const byte *src = (const byte *)marker.surface.getBasePtr(0, row);
+		byte *dst = (byte *)dstSurface.getBasePtr(0, dstY);
+		for (int col = 0; col < marker.surface.w; col++) {
+			const int dstX = x + col;
+			if (dstX < 0 || dstX >= dstSurface.w)
+				continue;
+			if (src[col] != transp)
+				dst[dstX] = useVisitedColors ? mapVisitedMarkerColor(src[col])
+											  : src[col];
+		}
+	}
+}
+
+void blitMacBigMapPartnerFrame(Graphics::ManagedSurface &dstSurface,
+							   const Picture &frame, int anchorX,
+							   int anchorY) {
+	const byte transp = (byte)(frame.flags >> 8);
+	const int x = anchorX - (int)(int16)frame.miscflags;
+	const int y = anchorY - (int)(int16)frame.rowoff;
+	for (int row = 0; row < frame.surface.h; row++) {
+		const int dstY = y + row;
+		if (dstY < 0 || dstY >= dstSurface.h)
+			continue;
+		const byte *src = (const byte *)frame.surface.getBasePtr(0, row);
+		byte *dst = (byte *)dstSurface.getBasePtr(0, dstY);
+		for (int col = 0; col < frame.surface.w; col++) {
+			const int dstX = x + col;
+			if (dstX < 0 || dstX >= dstSurface.w)
+				continue;
+			const byte color = src[col];
+			if (color != transp) {
+				// The map partner frames are authored against the overview
+				// ColorTable: 0 is white and 0xff is black. The detail-map
+				// ColorTable swaps those endpoints.
+				if (color == 0x00)
+					dst[dstX] = 0xff;
+				else if (color == 0xff)
+					dst[dstX] = 0x00;
+				else
+					dst[dstX] = color;
+			}
+		}
+	}
+}
+
+struct BigMapEntryInfo {
+	uint16 overviewX = 0;
+	uint16 overviewY = 0;
+	uint16 detailX = 0;
+	uint16 detailY = 0;
+	uint16 buttonId = 0;
+	uint16 crime = 0;
+};
+
+bool readBigMapEntryInfo(const byte *entry, bool floppy, bool macintosh,
+						 BigMapEntryInfo &out) {
+	if (!entry)
+		return false;
+
+	if (macintosh) {
+		out.detailX   = READ_LE_UINT16(entry + 0x0);
+		out.detailY   = READ_LE_UINT16(entry + 0x2);
+		out.buttonId  = entry[0x5];
+		out.overviewX = READ_LE_UINT16(entry + 0x6);
+		out.overviewY = READ_LE_UINT16(entry + 0x8);
+		out.crime     = READ_LE_UINT16(entry + 0xa);
+		return true;
+	}
+
+	if (floppy) {
+		out.detailX   = READ_LE_UINT16(entry + 0x0);
+		out.detailY   = READ_LE_UINT16(entry + 0x2);
+		out.buttonId  = entry[0x4];
+		out.overviewX = READ_LE_UINT16(entry + 0x6);
+		out.overviewY = READ_LE_UINT16(entry + 0x8);
+		out.crime     = entry[0xa];
+		return true;
+	}
+
+	out.buttonId  = READ_LE_UINT16(entry + 0x0);
+	out.overviewX = READ_LE_UINT16(entry + 0x4);
+	out.overviewY = READ_LE_UINT16(entry + 0x6);
+	out.detailX   = READ_LE_UINT16(entry + 0x8);
+	out.detailY   = READ_LE_UINT16(entry + 0xa);
+	out.crime     = READ_LE_UINT16(entry + 0xc);
+	return true;
+}
+
+const uint32 kMacMapColorCycleDelayMs = 25 * 1000 / 60;
+
+bool loadMacBigMapPixels(Common::Array<byte> &mapPixels,
+						 uint16 &mapW, uint16 &mapH) {
+	DBDArchive bigMapArchive;
+	if (!bigMapArchive.open(Common::Path("BIGMAP.DBD"),
+							Common::Path("BIGMAP.DBX"), true)) {
+		warning("doBigMap: BIGMAP archive missing");
+		return false;
+	}
+
+	Picture mapPic;
+	if (!bigMapArchive.loadEntry(0, mapPic) || mapPic.surface.empty()) {
+		warning("doBigMap: BIGMAP.DBD entry 0 failed to load");
+		return false;
+	}
+	if (mapPic.surface.w <= 0 || mapPic.surface.h <= 0)
+		return false;
+
+	mapW = (uint16)mapPic.surface.w;
+	mapH = (uint16)mapPic.surface.h;
+	mapPixels.resize((uint32)mapW * mapH);
+	for (uint y = 0; y < mapH; y++) {
+		const byte *src = (const byte *)mapPic.surface.getBasePtr(0, y);
+		byte *dst = mapPixels.data() + y * mapW;
+		// BIGMAP.DBD uses 0xff for dark coast/detail pixels, but the Mac
+		// detail-map palette keeps 0xff white for the UI frame/buttons.
+		for (uint x = 0; x < mapW; x++)
+			dst[x] = src[x] == 0xff ? 0x00 : src[x];
+	}
+	return true;
+}
+
+static bool openNumberedScriptFile(Common::File &f, Common::String &name,
+								   uint num, const char *const *patterns,
+								   uint patternCount) {
+	for (uint i = 0; i < patternCount; i++) {
+		name = Common::String::format(patterns[i], num);
+		if (f.open(Common::Path(name)))
+			return true;
+	}
+	name.clear();
+	return false;
+}
+
+static uint16 readScriptU16(const byte *p, bool bigEndian) {
+	return bigEndian ? READ_BE_UINT16(p) : READ_LE_UINT16(p);
+}
+
+Common::String cleanLondonApproachPage(const byte *start, uint32 len) {
+	Common::String page((const char *)start, len);
+	while (!page.empty() &&
+		   (page.lastChar() == '\r' || page.lastChar() == '\n' ||
+			(byte)page.lastChar() == 0xff))
+		page.deleteLastChar();
+	return page;
+}
+
+bool loadLondonApproachData(uint16 approachId, LondonApproachData &out,
+							 bool macintosh) {
+	static const char *const kDosPatterns[] = {
+		"A%u.BIN"
+	};
+	static const char *const kMacPatterns[] = {
+		"Approaches/a%u.bin",
+		"Approaches/A%u.BIN",
+		"a%u.bin",
+		"A%u.BIN"
+	};
+
+	Common::File f;
+	Common::String name;
+	const bool opened = macintosh
+		? openNumberedScriptFile(f, name, approachId, kMacPatterns,
+								 ARRAYSIZE(kMacPatterns))
+		: openNumberedScriptFile(f, name, approachId, kDosPatterns,
+								 ARRAYSIZE(kDosPatterns));
+	if (!opened) {
+		warning("London approach: cannot open approach %u", approachId);
+		return false;
+	}
+
+	const uint32 size = f.size();
+	if (size < 16) {
+		warning("London approach: %s is too short", name.c_str());
+		return false;
+	}
+
+	Common::Array<byte> data;
+	data.resize(size);
+	if (f.read(data.data(), size) != size) {
+		warning("London approach: short read on %s", name.c_str());
+		return false;
+	}
+
+	out.videoId = readScriptU16(data.data() + 0, macintosh);
+	out.videoX = readScriptU16(data.data() + 2, macintosh);
+	out.videoY = readScriptU16(data.data() + 4, macintosh);
+	if (macintosh) {
+		const int16 top = (int16)readScriptU16(data.data() + 6, true);
+		const int16 left = (int16)readScriptU16(data.data() + 8, true);
+		const int16 bottom = (int16)readScriptU16(data.data() + 10, true);
+		const int16 right = (int16)readScriptU16(data.data() + 12, true);
+		out.textRect = Common::Rect(left, top, right, bottom);
+	} else {
+		const int16 x1 = (int16)readScriptU16(data.data() + 6, false);
+		const int16 y1 = (int16)readScriptU16(data.data() + 8, false);
+		const int16 x2 = (int16)readScriptU16(data.data() + 10, false);
+		const int16 y2 = (int16)readScriptU16(data.data() + 12, false);
+		out.textRect = Common::Rect(x1, y1, x2, y2);
+	}
+
+	const uint16 pageCount = readScriptU16(data.data() + 14, macintosh);
+	out.pages.clear();
+	uint32 pos = 16;
+	// `_DoApproach @ 1717:009b` reads the pages with `_fgets` into 255-byte
+	// slots, so each page is one NEWLINE-terminated record, NOT NUL-separated.
+	for (uint16 i = 0; i < pageCount && pos < size; i++) {
+		const uint32 start = pos;
+		while (pos < size && data[pos] != '\n')
+			pos++;
+		out.pages.push_back(cleanLondonApproachPage(data.data() + start,
+													pos - start));
+		if (pos < size)
+			pos++;  // skip the '\n' separator
+	}
+	return out.videoId != 0 && !out.pages.empty();
+}
+
+bool openLondonApproachFlic(uint16 videoId, Video::FlicDecoder &flic,
+							Common::String &name) {
+	static const char *const kPatterns[] = {
+		"VIDEO%02u.FLC",
+		"video%02u.FLC"
+	};
+
+	for (uint i = 0; i < ARRAYSIZE(kPatterns); i++) {
+		name = Common::String::format(kPatterns[i], videoId);
+		if (flic.loadFile(Common::Path(name)))
+			return true;
+	}
+	name.clear();
+	return false;
+}
+
+bool decodeLondonApproachFirstFrame(uint16 videoId,
+									Graphics::ManagedSurface &base,
+									byte *palette, bool macintosh) {
+	if (macintosh) {
+		Video::FlicDecoder flic;
+		Common::String name;
+		if (!openLondonApproachFlic(videoId, flic, name)) {
+			warning("London approach: cannot open FLC video %u", videoId);
+			return false;
+		}
+
+		flic.start();
+		const Graphics::Surface *frame = flic.decodeNextFrame();
+		if (!frame) {
+			warning("London approach: %s has no first frame", name.c_str());
+			return false;
+		}
+
+		const byte *fpal = flic.getPalette();
+		if (fpal)
+			memcpy(palette, fpal, 768);
+		base.clear();
+		const int w = MIN<int>(frame->w, base.w);
+		const int h = MIN<int>(frame->h, base.h);
+		base.copyRectToSurface(frame->getPixels(), frame->pitch, 0, 0, w, h);
+		return true;
+	}
+
+	const Common::String name = Common::String::format("VIDEO%02u.A", videoId);
+	ANMDecoder anm;
+	if (!anm.open(Common::Path(name))) {
+		warning("London approach: cannot open %s", name.c_str());
+		return false;
+	}
+
+	anm.getPalette8(palette);
+	const byte *frame = anm.nextFrame();
+	if (!frame) {
+		warning("London approach: %s has no first frame", name.c_str());
+		return false;
+	}
+
+	base.clear();
+	const int w = MIN<int>(anm.width(), base.w);
+	const int h = MIN<int>(anm.height(), base.h);
+	base.copyRectToSurface(frame, anm.width(), 0, 0, w, h);
+	return true;
+}
+
+byte closestPaletteIndex(const byte *palette, byte r, byte g, byte b,
+						 byte fallback) {
+	if (!palette)
+		return fallback;
+
+	uint bestDist = 0xffffffff;
+	byte best = fallback;
+	for (uint i = 0; i < 256; i++) {
+		const int dr = (int)palette[i * 3 + 0] - r;
+		const int dg = (int)palette[i * 3 + 1] - g;
+		const int db = (int)palette[i * 3 + 2] - b;
+		const uint dist = (uint)(dr * dr + dg * dg + db * db);
+		if (dist < bestDist) {
+			bestDist = dist;
+			best = (byte)i;
+		}
+	}
+	return best;
+}
+
+void remapSurfaceColor(Graphics::ManagedSurface &surface, byte from, byte to) {
+	if (surface.empty() || from == to)
+		return;
+
+	for (int y = 0; y < surface.h; y++) {
+		byte *row = (byte *)surface.getBasePtr(0, y);
+		for (int x = 0; x < surface.w; x++) {
+			if (row[x] == from)
+				row[x] = to;
+		}
+	}
+}
+
+// `_DoBigMap @ 20fe:09e7` two stage:
+//   Stage 1 (Overview): PIC 0x42 + site icons at MapData[+4/+6]
+//     (`_DrawBigMapButtons @ 20fe:0877`). Click in BigMapWindow
+//     returns scroll (mouseX*2 - 0x74, mouseY*2 - 0x55).
+//   Stage 2 (Detail): PIC 0x43 frame + 0xe9×0xab BIGMAP.PIC viewport
+//     at (2,2). Icons stamped at MapData[+8/+0xa] (`_StampButtons @
+//     20fe:0d2f`). Click icon = travel.
+// MapData entry (14 bytes): +0..3 ???, +4 BigMapX, +6 BigMapY,
+//   +8 SmallMapX, +0xa SmallMapY, +0xc crime-flag.
+void EEMEngine::doBigMap() {
+
+	if (!_mystery.isLoaded())
+		return;
+
+	if (isLondon()) {
+		_mystery._pendingSiteJump = 0;
+		_mystery._siteReturnDepth = 0;
+		memset(_mystery._siteReturnStack, 0, sizeof(_mystery._siteReturnStack));
+	}
+
+	CursorMan.showMouse(true);
+
+	while (!shouldQuit()) {
+		setInteractiveMouseCursor(false);
+		setSitePalette(isLondon() ? 0x3b : 0x24);
+
+		const uint32 mapStartTick = g_system->getMillis();
+		drawBigMapOverview(0);
+		uint32 mapLastTick = mapStartTick;
+		uint32 mapLastCycleTick = mapStartTick;
+
+		const bool mac = isMacintosh();
+		const Common::Rect bigMapWindowBase(0, 0, 247, 192);
+		const Common::Rect kBigMapWindow =
+			mac ? scaleRect(bigMapWindowBase) : bigMapWindowBase;
+		const Common::Rect setupBtnBase = isFloppy()
+			? Common::Rect(251, 3, 315, 42)
+			: (isLondon() ? Common::Rect(252, 1, 315, 42)
+						  : Common::Rect(252, 4, 315, 42));
+		const Common::Rect kSetupBtnRect =
+			mac ? scaleRect(setupBtnBase) : setupBtnBase;
+
+		bool wantZoom = false;
+		int zoomX = 0;
+		int zoomY = 0;
+		while (!shouldQuit()) {
+			Common::Event ev;
+			while (g_system->getEventManager()->pollEvent(ev)) {
+				if (ev.type == Common::EVENT_QUIT ||
+					ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+					return;
+				if (ev.type == Common::EVENT_KEYDOWN &&
+					ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+					openMainMenuDialog();
+					continue;
+				}
+				if (ev.type == Common::EVENT_LBUTTONDOWN) {
+					if (kSetupBtnRect.contains(ev.mouse.x, ev.mouse.y)) {
+						_nextScreen = kScreenSetup;
+						return;
+					}
+
+					if (kBigMapWindow.contains(ev.mouse.x, ev.mouse.y)) {
+						if (mac) {
+							zoomX = ev.mouse.x - kBigMapWindow.left;
+							zoomY = ev.mouse.y - kBigMapWindow.top;
+						} else {
+							int sx = ev.mouse.x * 2;
+							int sy = ev.mouse.y * 2;
+							sx = (sx < 0x75) ? 0 : sx - 0x74;
+							sy = (sy < 0x56) ? 0 : sy - 0x55;
+							zoomX = sx;
+							zoomY = sy;
+						}
+						wantZoom = true;
+						break;
+					}
+				}
+			}
+			if (wantZoom)
+				break;
+
+			const uint32 now = g_system->getMillis();
+			if (mac && isLondon() &&
+				now - mapLastCycleTick >= kMacMapColorCycleDelayMs) {
+				mapLastCycleTick = now;
+				// Mac `_UpdateBigMap` rotates ColorTable entries 0xef..0xf2
+				// and 0xfc..0xff. Redraw immediately so Mac endpoint art maps
+				// its black pixels to the current black slot after the cycle.
+				cyclePaletteRangeReverse(0xef, 0xf2);
+				cyclePaletteRangeReverse(0xfc, 0xff);
+				drawBigMapOverview(now - mapStartTick);
+				mapLastTick = now;
+			} else if (now - mapLastTick >= 100) {
+				mapLastTick = now;
+				drawBigMapOverview(now - mapStartTick);
+				if (isLondon()) {
+					if (!mac) {
+						cyclePaletteRangeReverse(0xf4, 0xf9);
+						cyclePaletteRangeReverse(0xfa, 0xff);
+					}
+				} else {
+					cyclePaletteRangeReverse(0xf7, 0xfa);
+					cyclePaletteRangeReverse(0xfb, 0xfe);
+				}
+			}
+			g_system->updateScreen();
+			g_system->delayMillis(10);
+		}
+
+		if (!wantZoom)
+			return;
+
+		uint16 mapW = 0;
+		uint16 mapH = 0;
+		Common::Array<byte> mapPixels;
+		if (mac) {
+			if (!loadMacBigMapPixels(mapPixels, mapW, mapH))
+				return;
+		} else {
+			Common::File f;
+			if (!f.open(Common::Path("BIGMAP.PIC"))) {
+				warning("doBigMap: BIGMAP.PIC missing for detail view");
+				return;
+			}
+			mapH = f.readUint16LE();
+			mapW = f.readUint16LE();
+			if (mapW == 0 || mapH == 0)
+				return;
+			mapPixels.resize((uint32)mapW * mapH);
+			if (f.read(mapPixels.data(), mapPixels.size()) != mapPixels.size()) {
+				warning("doBigMap: short read on BIGMAP.PIC for detail view");
+				return;
+			}
+		}
+
+		const int kMapWinW = mac ? scaleX(0xe9) : 0xe9; // 233
+		const int kMapWinH = mac ? scaleY(0xab) : 0xab; // 171
+		const int kMapWinX = mac ? scaleX(2) : 2;
+		const int kMapWinY = mac ? scaleY(2) : 2;
+
+		const int maxScrollX = MAX<int>(0, (int)mapW - kMapWinW);
+		const int maxScrollY = MAX<int>(0, (int)mapH - kMapWinH);
+		int scrollX;
+		int scrollY;
+		if (mac) {
+			scrollX = zoomX * (int)mapW /
+				MAX<int>(1, kBigMapWindow.width()) - kMapWinW / 2;
+			scrollY = zoomY * (int)mapH /
+				MAX<int>(1, kBigMapWindow.height()) - kMapWinH / 2;
+		} else {
+			scrollX = zoomX;
+			scrollY = zoomY;
+		}
+		scrollX = MAX<int>(0, MIN<int>(maxScrollX, scrollX));
+		scrollY = MAX<int>(0, MIN<int>(maxScrollY, scrollY));
+
+		setSitePalette(isLondon() ? 0x3a : 0x23);
+
+		const uint32 detailStartTick = g_system->getMillis();
+		drawBigMapDetail(scrollX, scrollY, mapPixels, mapW, mapH, 0);
+		uint32 detailLastTick = detailStartTick;
+		uint32 detailLastCycleTick = detailStartTick;
+		bool returnToOverview = false;
+
+		const Common::Rect returnBase(252, 43, kScreenWidth, kScreenHeight);
+		const Common::Rect kBigMapReturnRect =
+			mac ? scaleRect(returnBase) : returnBase;
+		const Common::Rect kArrowYUp =
+			mac ? scaleRect(Common::Rect(237, 2, 247, 11))
+				: Common::Rect(237, 2, 247, 11);
+		const Common::Rect kArrowYDown =
+			mac ? scaleRect(Common::Rect(237, 163, 247, 172))
+				: Common::Rect(237, 163, 247, 172);
+		const Common::Rect kArrowXLeft =
+			mac ? scaleRect(Common::Rect(2, 175, 12, 185))
+				: Common::Rect(2, 175, 12, 185);
+		const Common::Rect kArrowXRight =
+			mac ? scaleRect(Common::Rect(224, 175, 234, 185))
+				: Common::Rect(224, 175, 234, 185);
+		const Common::Rect xSliderBase = isLondon()
+			? Common::Rect(15, 176, 220, 184)
+			: Common::Rect(15, 175, 221, 185);
+		const Common::Rect ySliderBase = isLondon()
+			? Common::Rect(238, 16, 246, 158)
+			: Common::Rect(237, 14, 247, 160);
+		const Common::Rect kXSlider =
+			mac ? scaleRect(xSliderBase) : xSliderBase;
+		const Common::Rect kYSlider =
+			mac ? scaleRect(ySliderBase) : ySliderBase;
+		const Common::Rect detailSetupBase = isFloppy()
+			? Common::Rect(251, 3, 315, 42)
+			: (isLondon() ? Common::Rect(251, 3, 315, 42)
+						  : Common::Rect(252, 4, 315, 42));
+		const Common::Rect kDetailSetupBtn =
+			mac ? scaleRect(detailSetupBase) : detailSetupBase;
+		const int baseArrowStep = isLondon() ? 8 : 16;
+		const int kArrowStepX = mac ? scaleX(baseArrowStep) : baseArrowStep;
+		const int kArrowStepY = mac ? scaleY(baseArrowStep) : baseArrowStep;
+		const int kSliderRange = maxScrollX;
+		const int kSliderRangeY = maxScrollY;
+		const Common::Point detailMouse =
+			g_system->getEventManager()->getMousePos();
+		setInteractiveMouseCursor(
+			kBigMapReturnRect.contains(detailMouse.x, detailMouse.y) ||
+			kDetailSetupBtn.contains(detailMouse.x, detailMouse.y));
+
+		while (!shouldQuit() && !returnToOverview) {
+			Common::Event ev;
+			bool dirty = false;
+			bool cycleTick = false;
+			while (g_system->getEventManager()->pollEvent(ev)) {
+				if (ev.type == Common::EVENT_QUIT ||
+					ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+					setInteractiveMouseCursor(false);
+					return;
+				}
+				if (ev.type == Common::EVENT_KEYDOWN) {
+					if (ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+						openMainMenuDialog();
+						dirty = true;
+						continue;
+					}
+					if (ev.kbd.keycode == Common::KEYCODE_LEFT) {
+						scrollX = MAX<int>(0, scrollX - kArrowStepX);
+						dirty = true;
+					} else if (ev.kbd.keycode == Common::KEYCODE_RIGHT) {
+						scrollX = MIN<int>(maxScrollX, scrollX + kArrowStepX);
+						dirty = true;
+					} else if (ev.kbd.keycode == Common::KEYCODE_UP) {
+						scrollY = MAX<int>(0, scrollY - kArrowStepY);
+						dirty = true;
+					} else if (ev.kbd.keycode == Common::KEYCODE_DOWN) {
+						scrollY = MIN<int>(maxScrollY, scrollY + kArrowStepY);
+						dirty = true;
+					}
+				}
+				if (ev.type == Common::EVENT_MOUSEMOVE)
+					setInteractiveMouseCursor(
+						kBigMapReturnRect.contains(ev.mouse.x, ev.mouse.y) ||
+						kDetailSetupBtn.contains(ev.mouse.x, ev.mouse.y));
+				if (ev.type == Common::EVENT_LBUTTONDOWN) {
+					setInteractiveMouseCursor(
+						kBigMapReturnRect.contains(ev.mouse.x, ev.mouse.y) ||
+						kDetailSetupBtn.contains(ev.mouse.x, ev.mouse.y));
+					if (kDetailSetupBtn.contains(ev.mouse.x, ev.mouse.y)) {
+						_nextScreen = kScreenSetup;
+						setInteractiveMouseCursor(false);
+						return;
+					}
+					if (kBigMapReturnRect.contains(ev.mouse.x, ev.mouse.y)) {
+						returnToOverview = true;
+						break;
+					} else if (kArrowYUp.contains(ev.mouse.x, ev.mouse.y)) {
+						scrollY = MAX<int>(0, scrollY - kArrowStepY);
+						dirty = true;
+					} else if (kArrowYDown.contains(ev.mouse.x, ev.mouse.y)) {
+						scrollY = MIN<int>(MAX<int>(0, kSliderRangeY),
+							scrollY + kArrowStepY);
+						dirty = true;
+					} else if (kArrowXLeft.contains(ev.mouse.x, ev.mouse.y)) {
+						scrollX = MAX<int>(0, scrollX - kArrowStepX);
+						dirty = true;
+					} else if (kArrowXRight.contains(ev.mouse.x, ev.mouse.y)) {
+						scrollX = MIN<int>(MAX<int>(0, kSliderRange),
+							scrollX + kArrowStepX);
+						dirty = true;
+					} else if (kXSlider.contains(ev.mouse.x, ev.mouse.y)) {
+						if (kSliderRange > 0) {
+							const int t = ev.mouse.x - kXSlider.left;
+							const int tw = kXSlider.width();
+							scrollX = MAX<int>(0, MIN<int>(kSliderRange,
+								t * kSliderRange / MAX<int>(1, tw)));
+							dirty = true;
+						}
+					} else if (kYSlider.contains(ev.mouse.x, ev.mouse.y)) {
+						if (kSliderRangeY > 0) {
+							const int t = ev.mouse.y - kYSlider.top;
+							const int th = kYSlider.height();
+							scrollY = MAX<int>(0, MIN<int>(kSliderRangeY,
+								t * kSliderRangeY / MAX<int>(1, th)));
+							dirty = true;
+						}
+					} else if (ev.mouse.x >= kMapWinX &&
+							   ev.mouse.x < kMapWinX + kMapWinW &&
+							   ev.mouse.y >= kMapWinY &&
+							   ev.mouse.y < kMapWinY + kMapWinH) {
+						// Per-site bbox from `_StampButtons` (SmallMap +8/+0xa).
+						const bool fmap = _mystery.isLoaded() && isFloppy();
+						struct DetailMapHit {
+							uint site;
+							Common::Rect rect;
+						};
+						Common::Array<DetailMapHit> hits;
+						for (uint i = 0; i < _mystery.numSites(); i++) {
+							// On-map flag alone, matching `_SearchMapButtons`.
+							if (!_mystery._onSites[i])
+								continue;
+							const byte *entry = _mystery.mapEntry(i);
+							if (!entry)
+								continue;
+							BigMapEntryInfo info;
+							if (!readBigMapEntryInfo(entry, fmap,
+													  mac && _mystery.usesCompactMacData(),
+													  info))
+								continue;
+							Picture button;
+							int bw = 16;
+							int bh = 16;
+							if (_buttonArchive.loadEntry(info.buttonId, button)) {
+								bw = button.surface.w;
+								bh = button.surface.h;
+							}
+							const int sx = (int)info.detailX - scrollX + kMapWinX;
+							const int sy = (int)info.detailY - scrollY + kMapWinY;
+							const Common::Rect r(sx, sy, sx + bw, sy + bh);
+							if (r.intersects(Common::Rect(kMapWinX, kMapWinY,
+									kMapWinX + kMapWinW, kMapWinY + kMapWinH))) {
+								DetailMapHit hit = { i, r };
+								hits.push_back(hit);
+							}
+						}
+						Common::sort(hits.begin(), hits.end(),
+							[](const DetailMapHit &a, const DetailMapHit &b) {
+								if (a.rect.top != b.rect.top)
+									return a.rect.top < b.rect.top;
+								return a.rect.left < b.rect.left;
+							});
+						for (uint i = 0; i < hits.size(); i++) {
+							if (hits[i].rect.contains(ev.mouse.x, ev.mouse.y)) {
+								_mystery._lastSite = _mystery._siteNumber;
+								_mystery._siteNumber = (uint16)hits[i].site;
+								setInteractiveMouseCursor(false);
+								return;
+							}
+						}
+					}
+				}
+			}
+			if (returnToOverview)
+				break;
+
+			const uint32 now = g_system->getMillis();
+			if (now - detailLastTick >= 100) {
+				detailLastTick = now;
+				dirty = true;
+			}
+			if (isLondon()) {
+				const uint32 cycleDelay = mac ? kMacMapColorCycleDelayMs : 100;
+				if (now - detailLastCycleTick >= cycleDelay) {
+					detailLastCycleTick = now;
+					cycleTick = true;
+					dirty = true;
+				}
+			}
+			if (cycleTick && isLondon()) {
+				if (mac) {
+					cyclePaletteRangeReverse(0xe9, 0xeb);
+					cyclePaletteRangeReverse(0xec, 0xef);
+					cyclePaletteRangeReverse(0xf0, 0xf2);
+				} else {
+					cyclePaletteRangeReverse(0xee, 0xf2);
+					cyclePaletteRangeReverse(0xea, 0xed);
+				}
+			}
+			if (dirty)
+				drawBigMapDetail(scrollX, scrollY, mapPixels, mapW, mapH,
+					now - detailStartTick);
+			g_system->updateScreen();
+			g_system->delayMillis(10);
+		}
+		if (!returnToOverview)
+			return;
+	}
+}
+
+bool EEMEngine::doLondonApproach(uint16 approachId) {
+	if (!isLondon())
+		return false;
+
+	LondonApproachData data;
+	const bool mac = isMacintosh();
+	if (!loadLondonApproachData(approachId, data, mac))
+		return false;
+
+	const int sw = screenWidth();
+	const int sh = screenHeight();
+
+	Graphics::ManagedSurface base(sw, sh,
+		Graphics::PixelFormat::createFormatCLUT8());
+	byte palette[768] = {};
+	const bool haveVideo =
+		decodeLondonApproachFirstFrame(data.videoId, base, palette, mac);
+	if (!haveVideo)
+		base.clear();
+	if (mac) {
+		Picture background;
+		const uint16 backgroundPic =
+			kMacLondonApproachBackgroundBasePic + data.videoId;
+		if (_picsArchive.getPicture(backgroundPic, background) &&
+			!background.surface.empty()) {
+			if (haveVideo) {
+				const byte black =
+					closestPaletteIndex(palette, 0x00, 0x00, 0x00, 0xfe);
+				remapSurfaceColor(background.surface, 0xff, black);
+			}
+			base.clear();
+			base.simpleBlitFrom(background.surface);
+		}
+	}
+
+	Picture buttonPics[ARRAYSIZE(kLondonApproachButtons)];
+	bool haveButtons[ARRAYSIZE(kLondonApproachButtons)] = {};
+	for (uint i = 0; i < ARRAYSIZE(kLondonApproachButtons); i++) {
+		// DOS `_DoApproach` loads PICS 0x361/0x362/0x35f/0x360 as the
+		// four control sprites. In the Mac London data those IDs are
+		// full-screen scrapbook pages, not buttons.
+		haveButtons[i] = !mac && _picsArchive.getPicture(
+			kLondonApproachButtons[i].picId, buttonPics[i]);
+	}
+
+	auto buttonRect = [&](uint idx) {
+		return kLondonApproachButtons[idx].rect();
+	};
+	auto buttonPoint = [&](uint idx) {
+		const LondonApproachButton &b = kLondonApproachButtons[idx];
+		return Common::Point(b.x, b.y);
+	};
+	const int movieX = mac ? kMacLondonApproachMovieX : (int)data.videoX;
+	const int movieY = mac ? kMacLondonApproachMovieY : (int)data.videoY;
+	auto approachTextRect = [&]() {
+		Common::Rect textRect = data.textRect;
+		if (mac)
+			textRect = scaleRect(textRect);
+		return textRect.findIntersectingRect(Common::Rect(sw, sh));
+	};
+	auto macApproachButton = [&](const Common::Point &mouse) {
+		if (mouse.x >= kMacLondonApproachDoneLeft &&
+			mouse.x < kMacLondonApproachDoneRight &&
+			mouse.y >= kMacLondonApproachDoneTop &&
+			mouse.y < kMacLondonApproachDoneBottom)
+			return 0;
+		if (mouse.x >= kMacLondonApproachPlayLeft &&
+			mouse.x < kMacLondonApproachPlayRight &&
+			mouse.y >= kMacLondonApproachPlayTop &&
+			mouse.y < kMacLondonApproachPlayBottom)
+			return 1;
+		return -1;
+	};
+	const byte approachTextColor =
+		closestPaletteIndex(haveVideo ? palette : nullptr, 0, 0, 0,
+							mac ? (byte)0xfe : (byte)1);
+
+	auto drawApproachOverlay = [&](Graphics::ManagedSurface &scratch,
+								   uint page) {
+		const Common::Rect textRect = approachTextRect();
+		if (!textRect.isEmpty()) {
+			if (page < data.pages.size()) {
+				Common::Array<Common::String> wrapped;
+				_font.wordWrapText(data.pages[page], MAX<int>(8, textRect.width()),
+								   wrapped);
+				const int lineH = _font.getFontHeight();
+				const int maxLines = MAX<int>(1, textRect.height() / lineH);
+				for (uint i = 0; i < wrapped.size() && (int)i < maxLines; i++) {
+					_font.drawString(&scratch, wrapped[i], textRect.left,
+									 textRect.top + (int)i * lineH,
+									 textRect.width(),
+									 approachTextColor);
+				}
+			}
+		}
+
+		for (uint i = 0; i < ARRAYSIZE(kLondonApproachButtons); i++) {
+			if (haveButtons[i]) {
+				const Common::Point p = buttonPoint(i);
+				scratch.transBlitFrom(buttonPics[i].surface, p,
+									  (uint32)(byte)(buttonPics[i].flags >> 8));
+			}
+		}
+	};
+
+	auto drawScreen = [&](uint page) {
+		Graphics::ManagedSurface scratch(sw, sh,
+			Graphics::PixelFormat::createFormatCLUT8());
+		scratch.clear();
+		scratch.simpleBlitFrom(base);
+		drawApproachOverlay(scratch, page);
+
+		g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+								   0, 0, sw, sh);
+		g_system->updateScreen();
+	};
+
+	auto playVideo = [&](uint page) {
+		if (mac) {
+			Video::FlicDecoder flic;
+			Common::String name;
+			if (!openLondonApproachFlic(data.videoId, flic, name)) {
+				warning("London approach: cannot open FLC video %u",
+						data.videoId);
+				return;
+			}
+
+			flic.start();
+			while (!shouldQuit() && !flic.endOfVideo()) {
+				const Graphics::Surface *frame = flic.decodeNextFrame();
+				if (!frame)
+					break;
+				Graphics::ManagedSurface scratch(sw, sh,
+					Graphics::PixelFormat::createFormatCLUT8());
+				scratch.clear();
+				scratch.simpleBlitFrom(base);
+				const int copyW = MIN<int>(frame->w, sw - movieX);
+				const int copyH = MIN<int>(frame->h, sh - movieY);
+				if (copyW <= 0 || copyH <= 0)
+					break;
+				scratch.copyRectToSurface(frame->getPixels(), frame->pitch,
+										  movieX, movieY,
+										  copyW, copyH);
+				drawApproachOverlay(scratch, page);
+				g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+										   0, 0, sw, sh);
+				if (flic.hasDirtyPalette()) {
+					const byte *fpal = flic.getPalette();
+					if (fpal)
+						g_system->getPaletteManager()->setPalette(fpal, 0, 256);
+				}
+				g_system->updateScreen();
+
+				const uint32 start = g_system->getMillis();
+				const uint32 delay = MAX<uint32>(10, flic.getTimeToNextFrame());
+				bool skip = false;
+				while (g_system->getMillis() - start < delay && !skip) {
+					Common::Event ev;
+					while (g_system->getEventManager()->pollEvent(ev)) {
+						if (ev.type == Common::EVENT_QUIT ||
+							ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+							return;
+						if (ev.type == Common::EVENT_LBUTTONDOWN ||
+							ev.type == Common::EVENT_KEYDOWN) {
+							skip = true;
+							break;
+						}
+					}
+					g_system->delayMillis(5);
+				}
+				if (skip)
+					break;
+			}
+			return;
+		}
+
+		const Common::String name =
+			Common::String::format("VIDEO%02u.A", data.videoId);
+		ANMDecoder anm;
+		if (!anm.open(Common::Path(name))) {
+			warning("London approach: cannot open %s", name.c_str());
+			return;
+		}
+
+		// Frame 0 is already the static background. Replay frames 1..end
+		// with the approach text/buttons composited above it.
+		(void)anm.nextFrame();
+		const int copyW = MIN<int>(anm.width(), sw - (int)data.videoX);
+		const int copyH = MIN<int>(anm.height(), sh - (int)data.videoY);
+		if (copyW <= 0 || copyH <= 0)
+			return;
+
+		while (!shouldQuit()) {
+			const byte *frame = anm.nextFrame();
+			if (!frame)
+				break;
+			Graphics::ManagedSurface scratch(sw, sh,
+				Graphics::PixelFormat::createFormatCLUT8());
+			scratch.clear();
+			scratch.copyRectToSurface(frame, anm.width(),
+									  data.videoX, data.videoY,
+									  copyW, copyH);
+			drawApproachOverlay(scratch, page);
+			g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+									   0, 0, sw, sh);
+			g_system->updateScreen();
+
+			const uint32 start = g_system->getMillis();
+			bool skip = false;
+			while (g_system->getMillis() - start < 120 && !skip) {
+				Common::Event ev;
+				while (g_system->getEventManager()->pollEvent(ev)) {
+					if (ev.type == Common::EVENT_QUIT ||
+						ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+						return;
+					}
+					if (ev.type == Common::EVENT_LBUTTONDOWN ||
+						ev.type == Common::EVENT_KEYDOWN) {
+						skip = true;
+						break;
+					}
+				}
+				g_system->delayMillis(5);
+			}
+			if (skip)
+				break;
+		}
+	};
+
+	fadeCurrentPaletteToBlack();
+	CursorMan.showMouse(true);
+	setSiteHotspotCursorId(6);
+	if (_music && _voiceOn)
+		_music->playMus(0x27, /* loop= */ true);
+
+	uint page = 0;
+	drawScreen(page);
+	if (haveVideo)
+		fadePaletteFromBlack(palette);
+	else
+		setSitePalette(0x3b);
+	if (haveVideo) {
+		playVideo(page);
+		drawScreen(page);
+	}
+	bool done = false;
+	uint32 lastShimmer = g_system->getMillis();
+	while (!shouldQuit() && !done) {
+		Common::Event ev;
+		while (g_system->getEventManager()->pollEvent(ev)) {
+			if (ev.type == Common::EVENT_QUIT ||
+				ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+				done = true;
+				break;
+			}
+			if (ev.type == Common::EVENT_MOUSEMOVE) {
+				bool overButton = false;
+				if (mac) {
+					overButton = macApproachButton(
+						Common::Point(ev.mouse.x, ev.mouse.y)) >= 0;
+				} else {
+					for (uint i = 0; i < ARRAYSIZE(kLondonApproachButtons); i++) {
+						if (buttonRect(i).contains(ev.mouse.x, ev.mouse.y)) {
+							overButton = true;
+							break;
+						}
+					}
+				}
+				setInteractiveMouseCursor(overButton);
+			}
+			if (ev.type == Common::EVENT_KEYDOWN) {
+				if (ev.kbd.keycode == Common::KEYCODE_ESCAPE ||
+					ev.kbd.keycode == Common::KEYCODE_RETURN ||
+					ev.kbd.keycode == Common::KEYCODE_KP_ENTER) {
+					done = true;
+					break;
+				}
+				if (ev.kbd.keycode == Common::KEYCODE_SPACE) {
+					playVideo(page);
+					drawScreen(page);
+				} else if (ev.kbd.keycode == Common::KEYCODE_RIGHT ||
+						   ev.kbd.keycode == Common::KEYCODE_DOWN) {
+					if (page + 1 < data.pages.size()) {
+						page++;
+						drawScreen(page);
+					}
+				} else if (ev.kbd.keycode == Common::KEYCODE_LEFT ||
+						   ev.kbd.keycode == Common::KEYCODE_UP) {
+					if (page > 0) {
+						page--;
+						drawScreen(page);
+					}
+				}
+			}
+			if (ev.type == Common::EVENT_LBUTTONDOWN) {
+				if (mac) {
+					const int button = macApproachButton(
+						Common::Point(ev.mouse.x, ev.mouse.y));
+					if (button == 0) {
+						done = true;
+						break;
+					}
+					if (button == 1) {
+						playVideo(page);
+						drawScreen(page);
+					}
+					continue;
+				}
+				if (buttonRect(0).contains(ev.mouse.x, ev.mouse.y)) {
+					done = true;
+					break;
+				}
+				if (buttonRect(1).contains(ev.mouse.x, ev.mouse.y)) {
+					playVideo(page);
+					drawScreen(page);
+				} else if (buttonRect(2).contains(ev.mouse.x, ev.mouse.y)) {
+					if (page + 1 < data.pages.size()) {
+						page++;
+						drawScreen(page);
+					}
+				} else if (buttonRect(3).contains(ev.mouse.x, ev.mouse.y)) {
+					if (page > 0) {
+						page--;
+						drawScreen(page);
+					}
+				}
+			}
+		}
+		if (haveVideo && !mac) {
+			const uint32 now = g_system->getMillis();
+			if (now - lastShimmer >= 70) {
+				lastShimmer = now;
+				cyclePaletteRange(0xF3, 0xF7);
+			}
+		}
+		g_system->updateScreen();
+		g_system->delayMillis(10);
+	}
+
+	setInteractiveMouseCursor(false);
+	setSiteHotspotCursorId(0);
+	stopMusic();
+	return true;
+}
+
+void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
+	const bool mac = isMacintosh();
+	const int sw = screenWidth();
+	const int sh = screenHeight();
+	Graphics::ManagedSurface scratch(sw, sh,
+		Graphics::PixelFormat::createFormatCLUT8());
+	scratch.clear();
+
+	Picture frame;
+	if (_picsArchive.getPicture(0x42, frame)) {
+		if (mac)
+			remapMacSurfaceEndpoints(frame.surface, getMacSpritePaletteMap());
+		scratch.simpleBlitFrom(frame.surface);
+	}
+
+	// Marker PICs from `_main`:
+	//   EEM1 CD @ 1a35:0f59: Done=0x20d, Site=0xc5, Crime=0xc6.
+	//   EEM2 CD @ 1abf:11a6: Done=0x006, Site=0xc5, Crime=0xc6.
+	Picture done;
+	Picture normal;
+	Picture crimeM;
+	const bool haveDone = _picsArchive.getPicture(isLondon() ? 0x006 : 0x20d, done) &&
+		done.surface.w < 64 && done.surface.h < 64;
+	const bool haveNormal = _picsArchive.getPicture(0xc5, normal) &&
+		normal.surface.w < 64 && normal.surface.h < 64;
+	const bool haveCrime = _picsArchive.getPicture(0xc6, crimeM) &&
+		crimeM.surface.w < 64 && crimeM.surface.h < 64;
+
+	for (uint i = 0; i < _mystery.numSites(); i++) {
+		// `_DrawBigMapButtons` gates markers on the on-map flag alone, never the
+		// current site: a sublocation (in-site jump, never flagged) must not draw.
+		if (!_mystery._onSites[i])
+			continue;
+		const byte *entry = _mystery.mapEntry(i);
+		if (!entry)
+			continue;
+
+		const bool floppy  = _mystery.isLoaded() && isFloppy();
+		BigMapEntryInfo info;
+		if (!readBigMapEntryInfo(entry, floppy,
+								 mac && _mystery.usesCompactMacData(), info))
+			continue;
+		const bool isDone = (i < Mystery::kVisitedSiteCap)
+							 && _mystery._visitedSite[i];
+
+		const Picture *m = nullptr;
+		bool useVisitedColors = false;
+		if (isDone && haveDone) {
+			m = &done;
+		} else if (isDone && haveNormal) {
+			m = &normal;
+			useVisitedColors = true;
+		} else if (info.crime != 0 && haveCrime) {
+			m = &crimeM;
+		} else if (haveNormal) {
+			m = &normal;
+		}
+
+		if (m) {
+			blitBigMapMarker(scratch, *m, (int)info.overviewX,
+							  (int)info.overviewY,
+							  useVisitedColors);
+		} else {
+			const int mx = (int)info.overviewX;
+			const int my = (int)info.overviewY;
+			const Common::Rect mark(mx - 3, my - 3, mx + 4, my + 4);
+			scratch.fillRect(mark, 0x0F);
+		}
+	}
+
+	const uint kMapAniId = (_partner == kPartnerJake) ? 0x14 : 0x12;
+	Animation mapAnim;
+	if (_aniArchive.loadAnimation(kMapAniId, mapAnim) && !mapAnim.empty()) {
+		const uint frameIdx = bigMapPartnerFrameAtTick((uint)mapAnim.size(),
+													   elapsedMs, isLondon());
+
+		const int anchorX = mac ? scaleX(0xfd) : 0xfd;
+		const int anchorY = mac ? scaleY(0x50) : 0x50;
+		if (mac)
+			blitMacAnimFrameAnchored(scratch.surfacePtr(), mapAnim[frameIdx],
+									 anchorX, anchorY);
+		else
+			blitAnimFrameAnchored(scratch.surfacePtr(), mapAnim[frameIdx],
+								  anchorX, anchorY);
+	}
+
+	g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+							   0, 0, sw, sh);
+	g_system->updateScreen();
+}
+
+void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
+								 const Common::Array<byte> &mapPixels,
+								 uint16 mapW, uint16 mapH,
+								 uint32 elapsedMs) {
+
+	const bool mac = isMacintosh();
+	const int sw = screenWidth();
+	const int sh = screenHeight();
+	const int kMapWinW = mac ? scaleX(0xe9) : 0xe9;
+	const int kMapWinH = mac ? scaleY(0xab) : 0xab;
+	const int kMapWinX = mac ? scaleX(2) : 2;
+	const int kMapWinY = mac ? scaleY(2) : 2;
+
+	Graphics::ManagedSurface scratch(sw, sh,
+		Graphics::PixelFormat::createFormatCLUT8());
+	scratch.clear();
+
+	Picture frame;
+	if (_picsArchive.getPicture(0x43, frame)) {
+		// The frame and its buttons use the normal 0x00=white / 0xFF=black
+		// convention, but the detail-map ColorTable (0x23) swaps those endpoints,
+		// so normalize them or the SETUP button's black border renders white.
+		if (mac)
+			remapMacSurfaceEndpoints(frame.surface, getMacSpritePaletteMap());
+		scratch.simpleBlitFrom(frame.surface);
+	}
+
+	const int copyW = MIN<int>(mapW - scrollX, kMapWinW);
+	const int copyH = MIN<int>(mapH - scrollY, kMapWinH);
+	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.
+		if (!_mystery._onSites[i])
+			continue;
+		const byte *entry = _mystery.mapEntry(i);
+		if (!entry)
+			continue;
+		BigMapEntryInfo info;
+		if (!readBigMapEntryInfo(entry, floppyMap,
+								 mac && _mystery.usesCompactMacData(), info))
+			continue;
+		Picture button;
+		if (!_buttonArchive.loadEntry(info.buttonId, button))
+			continue;
+		const int sx = (int)info.detailX - scrollX + kMapWinX;
+		const int sy = (int)info.detailY - scrollY + kMapWinY;
+		// Crop the button blit against the viewport.
+		const int x0 = MAX<int>(sx, kMapWinX);
+		const int y0 = MAX<int>(sy, kMapWinY);
+		const int x1 = MIN<int>(sx + button.surface.w, kMapWinX + kMapWinW);
+		const int y1 = MIN<int>(sy + button.surface.h, kMapWinY + kMapWinH);
+		if (x1 > x0 && y1 > y0) {
+			const Common::Rect srcRect(x0 - sx, y0 - sy, x1 - sx, y1 - sy);
+			scratch.transBlitFrom(button.surface, srcRect,
+								  Common::Point(x0, y0),
+								  (uint32)(byte)(button.flags >> 8));
+		}
+	}
+
+	const uint kDetailAniId = (_partner == kPartnerJake) ? 0x13 : 0x11;
+	Animation detailAnim;
+	if (_aniArchive.loadAnimation(kDetailAniId, detailAnim) &&
+		!detailAnim.empty()) {
+		const uint frameIdx = bigMapDetailPartnerFrameAtTick(
+				(uint)detailAnim.size(), elapsedMs);
+		const int anchorX = mac ? scaleX(0x101) : 0x101;
+		const int anchorY = mac ? scaleY(0x50) : 0x50;
+		if (mac)
+			blitMacBigMapPartnerFrame(scratch, detailAnim[frameIdx],
+									  anchorX, anchorY);
+		else
+			blitAnimFrameAnchored(scratch.surfacePtr(),
+								  detailAnim[frameIdx],
+								  anchorX, anchorY);
+	}
+
+	g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+							   0, 0, sw, sh);
+	g_system->updateScreen();
+}
+
+
+} // End of namespace EEM
diff --git a/engines/eem/module.mk b/engines/eem/module.mk
index 917289b1522..4b708614703 100644
--- a/engines/eem/module.mk
+++ b/engines/eem/module.mk
@@ -8,6 +8,7 @@ MODULE_OBJS = \
 	font.o \
 	graphics.o \
 	installer.o \
+	map_ui.o \
 	metaengine.o \
 	music.o \
 	mystery.o \
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 1eb8624e259..cf4de4ea63c 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -31,9 +31,6 @@
 
 #include "graphics/cursorman.h"
 #include "graphics/managed_surface.h"
-#include "graphics/paletteman.h"
-
-#include "video/flic_decoder.h"
 
 #include "eem/audio.h"
 #include "eem/detection.h"
@@ -60,190 +57,6 @@ const GallerySlot kMacGallerySlots[5] = {
 	{ 306, 173 }  // 4
 };
 
-struct LondonApproachData {
-	uint16 videoId = 0;
-	uint16 videoX = 0;
-	uint16 videoY = 0;
-	Common::Rect textRect;
-	Common::Array<Common::String> pages;
-};
-
-struct LondonApproachButton {
-	int x1;
-	int y1;
-	int x2;
-	int y2;
-	int x;
-	int y;
-	uint16 picId;
-
-	Common::Rect rect() const { return Common::Rect(x1, y1, x2, y2); }
-};
-
-const LondonApproachButton kLondonApproachButtons[4] = {
-	{  10, 139,  35, 157,  11, 139, 0x361 }, // Done
-	{  10, 172,  35, 190,  11, 172, 0x362 }, // Play
-	{ 287, 172, 307, 190, 287, 172, 0x35f }, // Next
-	{ 287, 139, 307, 157, 287, 139, 0x360 }, // Previous
-};
-
-const uint16 kMacLondonApproachBackgroundBasePic = 0x38c;
-const int kMacLondonApproachMovieX = 114;
-const int kMacLondonApproachMovieY = 23;
-const int kMacLondonApproachPlayLeft = 9;
-const int kMacLondonApproachPlayTop = 277;
-const int kMacLondonApproachPlayRight = 60;
-const int kMacLondonApproachPlayBottom = 332;
-const int kMacLondonApproachDoneLeft = 452;
-const int kMacLondonApproachDoneTop = 277;
-const int kMacLondonApproachDoneRight = 504;
-const int kMacLondonApproachDoneBottom = 332;
-
-byte mapVisitedMarkerColor(byte color) {
-	switch (color) {
-	case 0xf7:
-	case 0xfb:
-	case 0xfd:
-		return 0x1b;
-	case 0xf8:
-	case 0xf9:
-	case 0xfa:
-	case 0xfc:
-	case 0xfe:
-		return 0x19;
-	default:
-		return color;
-	}
-}
-
-void blitBigMapMarker(Graphics::ManagedSurface &dstSurface, const Picture &marker,
-					  int x, int y, bool useVisitedColors) {
-	const byte transp = (byte)(marker.flags >> 8);
-	for (int row = 0; row < marker.surface.h; row++) {
-		const int dstY = y + row;
-		if (dstY < 0 || dstY >= dstSurface.h)
-			continue;
-		const byte *src = (const byte *)marker.surface.getBasePtr(0, row);
-		byte *dst = (byte *)dstSurface.getBasePtr(0, dstY);
-		for (int col = 0; col < marker.surface.w; col++) {
-			const int dstX = x + col;
-			if (dstX < 0 || dstX >= dstSurface.w)
-				continue;
-			if (src[col] != transp)
-				dst[dstX] = useVisitedColors ? mapVisitedMarkerColor(src[col])
-											  : src[col];
-		}
-	}
-}
-
-void blitMacBigMapPartnerFrame(Graphics::ManagedSurface &dstSurface,
-							   const Picture &frame, int anchorX,
-							   int anchorY) {
-	const byte transp = (byte)(frame.flags >> 8);
-	const int x = anchorX - (int)(int16)frame.miscflags;
-	const int y = anchorY - (int)(int16)frame.rowoff;
-	for (int row = 0; row < frame.surface.h; row++) {
-		const int dstY = y + row;
-		if (dstY < 0 || dstY >= dstSurface.h)
-			continue;
-		const byte *src = (const byte *)frame.surface.getBasePtr(0, row);
-		byte *dst = (byte *)dstSurface.getBasePtr(0, dstY);
-		for (int col = 0; col < frame.surface.w; col++) {
-			const int dstX = x + col;
-			if (dstX < 0 || dstX >= dstSurface.w)
-				continue;
-			const byte color = src[col];
-			if (color != transp) {
-				// The map partner frames are authored against the overview
-				// ColorTable: 0 is white and 0xff is black. The detail-map
-				// ColorTable swaps those endpoints.
-				if (color == 0x00)
-					dst[dstX] = 0xff;
-				else if (color == 0xff)
-					dst[dstX] = 0x00;
-				else
-					dst[dstX] = color;
-			}
-		}
-	}
-}
-
-struct BigMapEntryInfo {
-	uint16 overviewX = 0;
-	uint16 overviewY = 0;
-	uint16 detailX = 0;
-	uint16 detailY = 0;
-	uint16 buttonId = 0;
-	uint16 crime = 0;
-};
-
-bool readBigMapEntryInfo(const byte *entry, bool floppy, bool macintosh,
-						 BigMapEntryInfo &out) {
-	if (!entry)
-		return false;
-
-	if (macintosh) {
-		out.detailX   = READ_LE_UINT16(entry + 0x0);
-		out.detailY   = READ_LE_UINT16(entry + 0x2);
-		out.buttonId  = entry[0x5];
-		out.overviewX = READ_LE_UINT16(entry + 0x6);
-		out.overviewY = READ_LE_UINT16(entry + 0x8);
-		out.crime     = READ_LE_UINT16(entry + 0xa);
-		return true;
-	}
-
-	if (floppy) {
-		out.detailX   = READ_LE_UINT16(entry + 0x0);
-		out.detailY   = READ_LE_UINT16(entry + 0x2);
-		out.buttonId  = entry[0x4];
-		out.overviewX = READ_LE_UINT16(entry + 0x6);
-		out.overviewY = READ_LE_UINT16(entry + 0x8);
-		out.crime     = entry[0xa];
-		return true;
-	}
-
-	out.buttonId  = READ_LE_UINT16(entry + 0x0);
-	out.overviewX = READ_LE_UINT16(entry + 0x4);
-	out.overviewY = READ_LE_UINT16(entry + 0x6);
-	out.detailX   = READ_LE_UINT16(entry + 0x8);
-	out.detailY   = READ_LE_UINT16(entry + 0xa);
-	out.crime     = READ_LE_UINT16(entry + 0xc);
-	return true;
-}
-
-const uint32 kMacMapColorCycleDelayMs = 25 * 1000 / 60;
-
-bool loadMacBigMapPixels(Common::Array<byte> &mapPixels,
-						 uint16 &mapW, uint16 &mapH) {
-	DBDArchive bigMapArchive;
-	if (!bigMapArchive.open(Common::Path("BIGMAP.DBD"),
-							Common::Path("BIGMAP.DBX"), true)) {
-		warning("doBigMap: BIGMAP archive missing");
-		return false;
-	}
-
-	Picture mapPic;
-	if (!bigMapArchive.loadEntry(0, mapPic) || mapPic.surface.empty()) {
-		warning("doBigMap: BIGMAP.DBD entry 0 failed to load");
-		return false;
-	}
-	if (mapPic.surface.w <= 0 || mapPic.surface.h <= 0)
-		return false;
-
-	mapW = (uint16)mapPic.surface.w;
-	mapH = (uint16)mapPic.surface.h;
-	mapPixels.resize((uint32)mapW * mapH);
-	for (uint y = 0; y < mapH; y++) {
-		const byte *src = (const byte *)mapPic.surface.getBasePtr(0, y);
-		byte *dst = mapPixels.data() + y * mapW;
-		// BIGMAP.DBD uses 0xff for dark coast/detail pixels, but the Mac
-		// detail-map palette keeps 0xff white for the UI frame/buttons.
-		for (uint x = 0; x < mapW; x++)
-			dst[x] = src[x] == 0xff ? 0x00 : src[x];
-	}
-	return true;
-}
-
 static bool openNumberedScriptFile(Common::File &f, Common::String &name,
 								   uint num, const char *const *patterns,
 								   uint patternCount) {
@@ -260,184 +73,6 @@ static uint16 readScriptU16(const byte *p, bool bigEndian) {
 	return bigEndian ? READ_BE_UINT16(p) : READ_LE_UINT16(p);
 }
 
-Common::String cleanLondonApproachPage(const byte *start, uint32 len) {
-	Common::String page((const char *)start, len);
-	while (!page.empty() &&
-		   (page.lastChar() == '\r' || page.lastChar() == '\n' ||
-			(byte)page.lastChar() == 0xff))
-		page.deleteLastChar();
-	return page;
-}
-
-bool loadLondonApproachData(uint16 approachId, LondonApproachData &out,
-							 bool macintosh) {
-	static const char *const kDosPatterns[] = {
-		"A%u.BIN"
-	};
-	static const char *const kMacPatterns[] = {
-		"Approaches/a%u.bin",
-		"Approaches/A%u.BIN",
-		"a%u.bin",
-		"A%u.BIN"
-	};
-
-	Common::File f;
-	Common::String name;
-	const bool opened = macintosh
-		? openNumberedScriptFile(f, name, approachId, kMacPatterns,
-								 ARRAYSIZE(kMacPatterns))
-		: openNumberedScriptFile(f, name, approachId, kDosPatterns,
-								 ARRAYSIZE(kDosPatterns));
-	if (!opened) {
-		warning("London approach: cannot open approach %u", approachId);
-		return false;
-	}
-
-	const uint32 size = f.size();
-	if (size < 16) {
-		warning("London approach: %s is too short", name.c_str());
-		return false;
-	}
-
-	Common::Array<byte> data;
-	data.resize(size);
-	if (f.read(data.data(), size) != size) {
-		warning("London approach: short read on %s", name.c_str());
-		return false;
-	}
-
-	out.videoId = readScriptU16(data.data() + 0, macintosh);
-	out.videoX = readScriptU16(data.data() + 2, macintosh);
-	out.videoY = readScriptU16(data.data() + 4, macintosh);
-	if (macintosh) {
-		const int16 top = (int16)readScriptU16(data.data() + 6, true);
-		const int16 left = (int16)readScriptU16(data.data() + 8, true);
-		const int16 bottom = (int16)readScriptU16(data.data() + 10, true);
-		const int16 right = (int16)readScriptU16(data.data() + 12, true);
-		out.textRect = Common::Rect(left, top, right, bottom);
-	} else {
-		const int16 x1 = (int16)readScriptU16(data.data() + 6, false);
-		const int16 y1 = (int16)readScriptU16(data.data() + 8, false);
-		const int16 x2 = (int16)readScriptU16(data.data() + 10, false);
-		const int16 y2 = (int16)readScriptU16(data.data() + 12, false);
-		out.textRect = Common::Rect(x1, y1, x2, y2);
-	}
-
-	const uint16 pageCount = readScriptU16(data.data() + 14, macintosh);
-	out.pages.clear();
-	uint32 pos = 16;
-	// `_DoApproach @ 1717:009b` reads the pages with `_fgets` into 255-byte
-	// slots, so each page is one NEWLINE-terminated record, NOT NUL-separated.
-	for (uint16 i = 0; i < pageCount && pos < size; i++) {
-		const uint32 start = pos;
-		while (pos < size && data[pos] != '\n')
-			pos++;
-		out.pages.push_back(cleanLondonApproachPage(data.data() + start,
-													pos - start));
-		if (pos < size)
-			pos++;  // skip the '\n' separator
-	}
-	return out.videoId != 0 && !out.pages.empty();
-}
-
-bool openLondonApproachFlic(uint16 videoId, Video::FlicDecoder &flic,
-							Common::String &name) {
-	static const char *const kPatterns[] = {
-		"VIDEO%02u.FLC",
-		"video%02u.FLC"
-	};
-
-	for (uint i = 0; i < ARRAYSIZE(kPatterns); i++) {
-		name = Common::String::format(kPatterns[i], videoId);
-		if (flic.loadFile(Common::Path(name)))
-			return true;
-	}
-	name.clear();
-	return false;
-}
-
-bool decodeLondonApproachFirstFrame(uint16 videoId,
-									Graphics::ManagedSurface &base,
-									byte *palette, bool macintosh) {
-	if (macintosh) {
-		Video::FlicDecoder flic;
-		Common::String name;
-		if (!openLondonApproachFlic(videoId, flic, name)) {
-			warning("London approach: cannot open FLC video %u", videoId);
-			return false;
-		}
-
-		flic.start();
-		const Graphics::Surface *frame = flic.decodeNextFrame();
-		if (!frame) {
-			warning("London approach: %s has no first frame", name.c_str());
-			return false;
-		}
-
-		const byte *fpal = flic.getPalette();
-		if (fpal)
-			memcpy(palette, fpal, 768);
-		base.clear();
-		const int w = MIN<int>(frame->w, base.w);
-		const int h = MIN<int>(frame->h, base.h);
-		base.copyRectToSurface(frame->getPixels(), frame->pitch, 0, 0, w, h);
-		return true;
-	}
-
-	const Common::String name = Common::String::format("VIDEO%02u.A", videoId);
-	ANMDecoder anm;
-	if (!anm.open(Common::Path(name))) {
-		warning("London approach: cannot open %s", name.c_str());
-		return false;
-	}
-
-	anm.getPalette8(palette);
-	const byte *frame = anm.nextFrame();
-	if (!frame) {
-		warning("London approach: %s has no first frame", name.c_str());
-		return false;
-	}
-
-	base.clear();
-	const int w = MIN<int>(anm.width(), base.w);
-	const int h = MIN<int>(anm.height(), base.h);
-	base.copyRectToSurface(frame, anm.width(), 0, 0, w, h);
-	return true;
-}
-
-byte closestPaletteIndex(const byte *palette, byte r, byte g, byte b,
-						 byte fallback) {
-	if (!palette)
-		return fallback;
-
-	uint bestDist = 0xffffffff;
-	byte best = fallback;
-	for (uint i = 0; i < 256; i++) {
-		const int dr = (int)palette[i * 3 + 0] - r;
-		const int dg = (int)palette[i * 3 + 1] - g;
-		const int db = (int)palette[i * 3 + 2] - b;
-		const uint dist = (uint)(dr * dr + dg * dg + db * db);
-		if (dist < bestDist) {
-			bestDist = dist;
-			best = (byte)i;
-		}
-	}
-	return best;
-}
-
-void remapSurfaceColor(Graphics::ManagedSurface &surface, byte from, byte to) {
-	if (surface.empty() || from == to)
-		return;
-
-	for (int y = 0; y < surface.h; y++) {
-		byte *row = (byte *)surface.getBasePtr(0, y);
-		for (int x = 0; x < surface.w; x++) {
-			if (row[x] == from)
-				row[x] = to;
-		}
-	}
-}
-
 // Setup-screen highlight rects
 constexpr Common::Rect kSetupKid1Rect    (Common::Point( 99,  44),  49,  8);
 constexpr Common::Rect kSetupKid2Rect    (Common::Point( 99,  54),  49,  8);
@@ -3920,903 +3555,6 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
 	g_system->updateScreen();
 }
 
-// `_DoBigMap @ 20fe:09e7` two stage:
-//   Stage 1 (Overview): PIC 0x42 + site icons at MapData[+4/+6]
-//     (`_DrawBigMapButtons @ 20fe:0877`). Click in BigMapWindow
-//     returns scroll (mouseX*2 - 0x74, mouseY*2 - 0x55).
-//   Stage 2 (Detail): PIC 0x43 frame + 0xe9×0xab BIGMAP.PIC viewport
-//     at (2,2). Icons stamped at MapData[+8/+0xa] (`_StampButtons @
-//     20fe:0d2f`). Click icon = travel.
-// MapData entry (14 bytes): +0..3 ???, +4 BigMapX, +6 BigMapY,
-//   +8 SmallMapX, +0xa SmallMapY, +0xc crime-flag.
-void EEMEngine::doBigMap() {
-
-	if (!_mystery.isLoaded())
-		return;
-
-	if (isLondon()) {
-		_mystery._pendingSiteJump = 0;
-		_mystery._siteReturnDepth = 0;
-		memset(_mystery._siteReturnStack, 0, sizeof(_mystery._siteReturnStack));
-	}
-
-	CursorMan.showMouse(true);
-
-	while (!shouldQuit()) {
-		setInteractiveMouseCursor(false);
-		setSitePalette(isLondon() ? 0x3b : 0x24);
-
-		const uint32 mapStartTick = g_system->getMillis();
-		drawBigMapOverview(0);
-		uint32 mapLastTick = mapStartTick;
-		uint32 mapLastCycleTick = mapStartTick;
-
-		const bool mac = isMacintosh();
-		const Common::Rect bigMapWindowBase(0, 0, 247, 192);
-		const Common::Rect kBigMapWindow =
-			mac ? scaleRect(bigMapWindowBase) : bigMapWindowBase;
-		const Common::Rect setupBtnBase = isFloppy()
-			? Common::Rect(251, 3, 315, 42)
-			: (isLondon() ? Common::Rect(252, 1, 315, 42)
-						  : Common::Rect(252, 4, 315, 42));
-		const Common::Rect kSetupBtnRect =
-			mac ? scaleRect(setupBtnBase) : setupBtnBase;
-
-		bool wantZoom = false;
-		int zoomX = 0;
-		int zoomY = 0;
-		while (!shouldQuit()) {
-			Common::Event ev;
-			while (g_system->getEventManager()->pollEvent(ev)) {
-				if (ev.type == Common::EVENT_QUIT ||
-					ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
-					return;
-				if (ev.type == Common::EVENT_KEYDOWN &&
-					ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
-					openMainMenuDialog();
-					continue;
-				}
-				if (ev.type == Common::EVENT_LBUTTONDOWN) {
-					if (kSetupBtnRect.contains(ev.mouse.x, ev.mouse.y)) {
-						_nextScreen = kScreenSetup;
-						return;
-					}
-
-					if (kBigMapWindow.contains(ev.mouse.x, ev.mouse.y)) {
-						if (mac) {
-							zoomX = ev.mouse.x - kBigMapWindow.left;
-							zoomY = ev.mouse.y - kBigMapWindow.top;
-						} else {
-							int sx = ev.mouse.x * 2;
-							int sy = ev.mouse.y * 2;
-							sx = (sx < 0x75) ? 0 : sx - 0x74;
-							sy = (sy < 0x56) ? 0 : sy - 0x55;
-							zoomX = sx;
-							zoomY = sy;
-						}
-						wantZoom = true;
-						break;
-					}
-				}
-			}
-			if (wantZoom)
-				break;
-
-			const uint32 now = g_system->getMillis();
-			if (mac && isLondon() &&
-				now - mapLastCycleTick >= kMacMapColorCycleDelayMs) {
-				mapLastCycleTick = now;
-				// Mac `_UpdateBigMap` rotates ColorTable entries 0xef..0xf2
-				// and 0xfc..0xff. Redraw immediately so Mac endpoint art maps
-				// its black pixels to the current black slot after the cycle.
-				cyclePaletteRangeReverse(0xef, 0xf2);
-				cyclePaletteRangeReverse(0xfc, 0xff);
-				drawBigMapOverview(now - mapStartTick);
-				mapLastTick = now;
-			} else if (now - mapLastTick >= 100) {
-				mapLastTick = now;
-				drawBigMapOverview(now - mapStartTick);
-				if (isLondon()) {
-					if (!mac) {
-						cyclePaletteRangeReverse(0xf4, 0xf9);
-						cyclePaletteRangeReverse(0xfa, 0xff);
-					}
-				} else {
-					cyclePaletteRangeReverse(0xf7, 0xfa);
-					cyclePaletteRangeReverse(0xfb, 0xfe);
-				}
-			}
-			g_system->updateScreen();
-			g_system->delayMillis(10);
-		}
-
-		if (!wantZoom)
-			return;
-
-		uint16 mapW = 0;
-		uint16 mapH = 0;
-		Common::Array<byte> mapPixels;
-		if (mac) {
-			if (!loadMacBigMapPixels(mapPixels, mapW, mapH))
-				return;
-		} else {
-			Common::File f;
-			if (!f.open(Common::Path("BIGMAP.PIC"))) {
-				warning("doBigMap: BIGMAP.PIC missing for detail view");
-				return;
-			}
-			mapH = f.readUint16LE();
-			mapW = f.readUint16LE();
-			if (mapW == 0 || mapH == 0)
-				return;
-			mapPixels.resize((uint32)mapW * mapH);
-			if (f.read(mapPixels.data(), mapPixels.size()) != mapPixels.size()) {
-				warning("doBigMap: short read on BIGMAP.PIC for detail view");
-				return;
-			}
-		}
-
-		const int kMapWinW = mac ? scaleX(0xe9) : 0xe9; // 233
-		const int kMapWinH = mac ? scaleY(0xab) : 0xab; // 171
-		const int kMapWinX = mac ? scaleX(2) : 2;
-		const int kMapWinY = mac ? scaleY(2) : 2;
-
-		const int maxScrollX = MAX<int>(0, (int)mapW - kMapWinW);
-		const int maxScrollY = MAX<int>(0, (int)mapH - kMapWinH);
-		int scrollX;
-		int scrollY;
-		if (mac) {
-			scrollX = zoomX * (int)mapW /
-				MAX<int>(1, kBigMapWindow.width()) - kMapWinW / 2;
-			scrollY = zoomY * (int)mapH /
-				MAX<int>(1, kBigMapWindow.height()) - kMapWinH / 2;
-		} else {
-			scrollX = zoomX;
-			scrollY = zoomY;
-		}
-		scrollX = MAX<int>(0, MIN<int>(maxScrollX, scrollX));
-		scrollY = MAX<int>(0, MIN<int>(maxScrollY, scrollY));
-
-		setSitePalette(isLondon() ? 0x3a : 0x23);
-
-		const uint32 detailStartTick = g_system->getMillis();
-		drawBigMapDetail(scrollX, scrollY, mapPixels, mapW, mapH, 0);
-		uint32 detailLastTick = detailStartTick;
-		uint32 detailLastCycleTick = detailStartTick;
-		bool returnToOverview = false;
-
-		const Common::Rect returnBase(252, 43, kScreenWidth, kScreenHeight);
-		const Common::Rect kBigMapReturnRect =
-			mac ? scaleRect(returnBase) : returnBase;
-		const Common::Rect kArrowYUp =
-			mac ? scaleRect(Common::Rect(237, 2, 247, 11))
-				: Common::Rect(237, 2, 247, 11);
-		const Common::Rect kArrowYDown =
-			mac ? scaleRect(Common::Rect(237, 163, 247, 172))
-				: Common::Rect(237, 163, 247, 172);
-		const Common::Rect kArrowXLeft =
-			mac ? scaleRect(Common::Rect(2, 175, 12, 185))
-				: Common::Rect(2, 175, 12, 185);
-		const Common::Rect kArrowXRight =
-			mac ? scaleRect(Common::Rect(224, 175, 234, 185))
-				: Common::Rect(224, 175, 234, 185);
-		const Common::Rect xSliderBase = isLondon()
-			? Common::Rect(15, 176, 220, 184)
-			: Common::Rect(15, 175, 221, 185);
-		const Common::Rect ySliderBase = isLondon()
-			? Common::Rect(238, 16, 246, 158)
-			: Common::Rect(237, 14, 247, 160);
-		const Common::Rect kXSlider =
-			mac ? scaleRect(xSliderBase) : xSliderBase;
-		const Common::Rect kYSlider =
-			mac ? scaleRect(ySliderBase) : ySliderBase;
-		const Common::Rect detailSetupBase = isFloppy()
-			? Common::Rect(251, 3, 315, 42)
-			: (isLondon() ? Common::Rect(251, 3, 315, 42)
-						  : Common::Rect(252, 4, 315, 42));
-		const Common::Rect kDetailSetupBtn =
-			mac ? scaleRect(detailSetupBase) : detailSetupBase;
-		const int baseArrowStep = isLondon() ? 8 : 16;
-		const int kArrowStepX = mac ? scaleX(baseArrowStep) : baseArrowStep;
-		const int kArrowStepY = mac ? scaleY(baseArrowStep) : baseArrowStep;
-		const int kSliderRange = maxScrollX;
-		const int kSliderRangeY = maxScrollY;
-		const Common::Point detailMouse =
-			g_system->getEventManager()->getMousePos();
-		setInteractiveMouseCursor(
-			kBigMapReturnRect.contains(detailMouse.x, detailMouse.y) ||
-			kDetailSetupBtn.contains(detailMouse.x, detailMouse.y));
-
-		while (!shouldQuit() && !returnToOverview) {
-			Common::Event ev;
-			bool dirty = false;
-			bool cycleTick = false;
-			while (g_system->getEventManager()->pollEvent(ev)) {
-				if (ev.type == Common::EVENT_QUIT ||
-					ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
-					setInteractiveMouseCursor(false);
-					return;
-				}
-				if (ev.type == Common::EVENT_KEYDOWN) {
-					if (ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
-						openMainMenuDialog();
-						dirty = true;
-						continue;
-					}
-					if (ev.kbd.keycode == Common::KEYCODE_LEFT) {
-						scrollX = MAX<int>(0, scrollX - kArrowStepX);
-						dirty = true;
-					} else if (ev.kbd.keycode == Common::KEYCODE_RIGHT) {
-						scrollX = MIN<int>(maxScrollX, scrollX + kArrowStepX);
-						dirty = true;
-					} else if (ev.kbd.keycode == Common::KEYCODE_UP) {
-						scrollY = MAX<int>(0, scrollY - kArrowStepY);
-						dirty = true;
-					} else if (ev.kbd.keycode == Common::KEYCODE_DOWN) {
-						scrollY = MIN<int>(maxScrollY, scrollY + kArrowStepY);
-						dirty = true;
-					}
-				}
-				if (ev.type == Common::EVENT_MOUSEMOVE)
-					setInteractiveMouseCursor(
-						kBigMapReturnRect.contains(ev.mouse.x, ev.mouse.y) ||
-						kDetailSetupBtn.contains(ev.mouse.x, ev.mouse.y));
-				if (ev.type == Common::EVENT_LBUTTONDOWN) {
-					setInteractiveMouseCursor(
-						kBigMapReturnRect.contains(ev.mouse.x, ev.mouse.y) ||
-						kDetailSetupBtn.contains(ev.mouse.x, ev.mouse.y));
-					if (kDetailSetupBtn.contains(ev.mouse.x, ev.mouse.y)) {
-						_nextScreen = kScreenSetup;
-						setInteractiveMouseCursor(false);
-						return;
-					}
-					if (kBigMapReturnRect.contains(ev.mouse.x, ev.mouse.y)) {
-						returnToOverview = true;
-						break;
-					} else if (kArrowYUp.contains(ev.mouse.x, ev.mouse.y)) {
-						scrollY = MAX<int>(0, scrollY - kArrowStepY);
-						dirty = true;
-					} else if (kArrowYDown.contains(ev.mouse.x, ev.mouse.y)) {
-						scrollY = MIN<int>(MAX<int>(0, kSliderRangeY),
-							scrollY + kArrowStepY);
-						dirty = true;
-					} else if (kArrowXLeft.contains(ev.mouse.x, ev.mouse.y)) {
-						scrollX = MAX<int>(0, scrollX - kArrowStepX);
-						dirty = true;
-					} else if (kArrowXRight.contains(ev.mouse.x, ev.mouse.y)) {
-						scrollX = MIN<int>(MAX<int>(0, kSliderRange),
-							scrollX + kArrowStepX);
-						dirty = true;
-					} else if (kXSlider.contains(ev.mouse.x, ev.mouse.y)) {
-						if (kSliderRange > 0) {
-							const int t = ev.mouse.x - kXSlider.left;
-							const int tw = kXSlider.width();
-							scrollX = MAX<int>(0, MIN<int>(kSliderRange,
-								t * kSliderRange / MAX<int>(1, tw)));
-							dirty = true;
-						}
-					} else if (kYSlider.contains(ev.mouse.x, ev.mouse.y)) {
-						if (kSliderRangeY > 0) {
-							const int t = ev.mouse.y - kYSlider.top;
-							const int th = kYSlider.height();
-							scrollY = MAX<int>(0, MIN<int>(kSliderRangeY,
-								t * kSliderRangeY / MAX<int>(1, th)));
-							dirty = true;
-						}
-					} else if (ev.mouse.x >= kMapWinX &&
-							   ev.mouse.x < kMapWinX + kMapWinW &&
-							   ev.mouse.y >= kMapWinY &&
-							   ev.mouse.y < kMapWinY + kMapWinH) {
-						// Per-site bbox from `_StampButtons` (SmallMap +8/+0xa).
-						const bool fmap = _mystery.isLoaded() && isFloppy();
-						struct DetailMapHit {
-							uint site;
-							Common::Rect rect;
-						};
-						Common::Array<DetailMapHit> hits;
-						for (uint i = 0; i < _mystery.numSites(); i++) {
-							// On-map flag alone, matching `_SearchMapButtons`.
-							if (!_mystery._onSites[i])
-								continue;
-							const byte *entry = _mystery.mapEntry(i);
-							if (!entry)
-								continue;
-							BigMapEntryInfo info;
-							if (!readBigMapEntryInfo(entry, fmap,
-													  mac && _mystery.usesCompactMacData(),
-													  info))
-								continue;
-							Picture button;
-							int bw = 16;
-							int bh = 16;
-							if (_buttonArchive.loadEntry(info.buttonId, button)) {
-								bw = button.surface.w;
-								bh = button.surface.h;
-							}
-							const int sx = (int)info.detailX - scrollX + kMapWinX;
-							const int sy = (int)info.detailY - scrollY + kMapWinY;
-							const Common::Rect r(sx, sy, sx + bw, sy + bh);
-							if (r.intersects(Common::Rect(kMapWinX, kMapWinY,
-									kMapWinX + kMapWinW, kMapWinY + kMapWinH))) {
-								DetailMapHit hit = { i, r };
-								hits.push_back(hit);
-							}
-						}
-						Common::sort(hits.begin(), hits.end(),
-							[](const DetailMapHit &a, const DetailMapHit &b) {
-								if (a.rect.top != b.rect.top)
-									return a.rect.top < b.rect.top;
-								return a.rect.left < b.rect.left;
-							});
-						for (uint i = 0; i < hits.size(); i++) {
-							if (hits[i].rect.contains(ev.mouse.x, ev.mouse.y)) {
-								_mystery._lastSite = _mystery._siteNumber;
-								_mystery._siteNumber = (uint16)hits[i].site;
-								setInteractiveMouseCursor(false);
-								return;
-							}
-						}
-					}
-				}
-			}
-			if (returnToOverview)
-				break;
-
-			const uint32 now = g_system->getMillis();
-			if (now - detailLastTick >= 100) {
-				detailLastTick = now;
-				dirty = true;
-			}
-			if (isLondon()) {
-				const uint32 cycleDelay = mac ? kMacMapColorCycleDelayMs : 100;
-				if (now - detailLastCycleTick >= cycleDelay) {
-					detailLastCycleTick = now;
-					cycleTick = true;
-					dirty = true;
-				}
-			}
-			if (cycleTick && isLondon()) {
-				if (mac) {
-					cyclePaletteRangeReverse(0xe9, 0xeb);
-					cyclePaletteRangeReverse(0xec, 0xef);
-					cyclePaletteRangeReverse(0xf0, 0xf2);
-				} else {
-					cyclePaletteRangeReverse(0xee, 0xf2);
-					cyclePaletteRangeReverse(0xea, 0xed);
-				}
-			}
-			if (dirty)
-				drawBigMapDetail(scrollX, scrollY, mapPixels, mapW, mapH,
-					now - detailStartTick);
-			g_system->updateScreen();
-			g_system->delayMillis(10);
-		}
-		if (!returnToOverview)
-			return;
-	}
-}
-
-bool EEMEngine::doLondonApproach(uint16 approachId) {
-	if (!isLondon())
-		return false;
-
-	LondonApproachData data;
-	const bool mac = isMacintosh();
-	if (!loadLondonApproachData(approachId, data, mac))
-		return false;
-
-	const int sw = screenWidth();
-	const int sh = screenHeight();
-
-	Graphics::ManagedSurface base(sw, sh,
-		Graphics::PixelFormat::createFormatCLUT8());
-	byte palette[768] = {};
-	const bool haveVideo =
-		decodeLondonApproachFirstFrame(data.videoId, base, palette, mac);
-	if (!haveVideo)
-		base.clear();
-	if (mac) {
-		Picture background;
-		const uint16 backgroundPic =
-			kMacLondonApproachBackgroundBasePic + data.videoId;
-		if (_picsArchive.getPicture(backgroundPic, background) &&
-			!background.surface.empty()) {
-			if (haveVideo) {
-				const byte black =
-					closestPaletteIndex(palette, 0x00, 0x00, 0x00, 0xfe);
-				remapSurfaceColor(background.surface, 0xff, black);
-			}
-			base.clear();
-			base.simpleBlitFrom(background.surface);
-		}
-	}
-
-	Picture buttonPics[ARRAYSIZE(kLondonApproachButtons)];
-	bool haveButtons[ARRAYSIZE(kLondonApproachButtons)] = {};
-	for (uint i = 0; i < ARRAYSIZE(kLondonApproachButtons); i++) {
-		// DOS `_DoApproach` loads PICS 0x361/0x362/0x35f/0x360 as the
-		// four control sprites. In the Mac London data those IDs are
-		// full-screen scrapbook pages, not buttons.
-		haveButtons[i] = !mac && _picsArchive.getPicture(
-			kLondonApproachButtons[i].picId, buttonPics[i]);
-	}
-
-	auto buttonRect = [&](uint idx) {
-		return kLondonApproachButtons[idx].rect();
-	};
-	auto buttonPoint = [&](uint idx) {
-		const LondonApproachButton &b = kLondonApproachButtons[idx];
-		return Common::Point(b.x, b.y);
-	};
-	const int movieX = mac ? kMacLondonApproachMovieX : (int)data.videoX;
-	const int movieY = mac ? kMacLondonApproachMovieY : (int)data.videoY;
-	auto approachTextRect = [&]() {
-		Common::Rect textRect = data.textRect;
-		if (mac)
-			textRect = scaleRect(textRect);
-		return textRect.findIntersectingRect(Common::Rect(sw, sh));
-	};
-	auto macApproachButton = [&](const Common::Point &mouse) {
-		if (mouse.x >= kMacLondonApproachDoneLeft &&
-			mouse.x < kMacLondonApproachDoneRight &&
-			mouse.y >= kMacLondonApproachDoneTop &&
-			mouse.y < kMacLondonApproachDoneBottom)
-			return 0;
-		if (mouse.x >= kMacLondonApproachPlayLeft &&
-			mouse.x < kMacLondonApproachPlayRight &&
-			mouse.y >= kMacLondonApproachPlayTop &&
-			mouse.y < kMacLondonApproachPlayBottom)
-			return 1;
-		return -1;
-	};
-	const byte approachTextColor =
-		closestPaletteIndex(haveVideo ? palette : nullptr, 0, 0, 0,
-							mac ? (byte)0xfe : (byte)1);
-
-	auto drawApproachOverlay = [&](Graphics::ManagedSurface &scratch,
-								   uint page) {
-		const Common::Rect textRect = approachTextRect();
-		if (!textRect.isEmpty()) {
-			if (page < data.pages.size()) {
-				Common::Array<Common::String> wrapped;
-				_font.wordWrapText(data.pages[page], MAX<int>(8, textRect.width()),
-								   wrapped);
-				const int lineH = _font.getFontHeight();
-				const int maxLines = MAX<int>(1, textRect.height() / lineH);
-				for (uint i = 0; i < wrapped.size() && (int)i < maxLines; i++) {
-					_font.drawString(&scratch, wrapped[i], textRect.left,
-									 textRect.top + (int)i * lineH,
-									 textRect.width(),
-									 approachTextColor);
-				}
-			}
-		}
-
-		for (uint i = 0; i < ARRAYSIZE(kLondonApproachButtons); i++) {
-			if (haveButtons[i]) {
-				const Common::Point p = buttonPoint(i);
-				scratch.transBlitFrom(buttonPics[i].surface, p,
-									  (uint32)(byte)(buttonPics[i].flags >> 8));
-			}
-		}
-	};
-
-	auto drawScreen = [&](uint page) {
-		Graphics::ManagedSurface scratch(sw, sh,
-			Graphics::PixelFormat::createFormatCLUT8());
-		scratch.clear();
-		scratch.simpleBlitFrom(base);
-		drawApproachOverlay(scratch, page);
-
-		g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
-								   0, 0, sw, sh);
-		g_system->updateScreen();
-	};
-
-	auto playVideo = [&](uint page) {
-		if (mac) {
-			Video::FlicDecoder flic;
-			Common::String name;
-			if (!openLondonApproachFlic(data.videoId, flic, name)) {
-				warning("London approach: cannot open FLC video %u",
-						data.videoId);
-				return;
-			}
-
-			flic.start();
-			while (!shouldQuit() && !flic.endOfVideo()) {
-				const Graphics::Surface *frame = flic.decodeNextFrame();
-				if (!frame)
-					break;
-				Graphics::ManagedSurface scratch(sw, sh,
-					Graphics::PixelFormat::createFormatCLUT8());
-				scratch.clear();
-				scratch.simpleBlitFrom(base);
-				const int copyW = MIN<int>(frame->w, sw - movieX);
-				const int copyH = MIN<int>(frame->h, sh - movieY);
-				if (copyW <= 0 || copyH <= 0)
-					break;
-				scratch.copyRectToSurface(frame->getPixels(), frame->pitch,
-										  movieX, movieY,
-										  copyW, copyH);
-				drawApproachOverlay(scratch, page);
-				g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
-										   0, 0, sw, sh);
-				if (flic.hasDirtyPalette()) {
-					const byte *fpal = flic.getPalette();
-					if (fpal)
-						g_system->getPaletteManager()->setPalette(fpal, 0, 256);
-				}
-				g_system->updateScreen();
-
-				const uint32 start = g_system->getMillis();
-				const uint32 delay = MAX<uint32>(10, flic.getTimeToNextFrame());
-				bool skip = false;
-				while (g_system->getMillis() - start < delay && !skip) {
-					Common::Event ev;
-					while (g_system->getEventManager()->pollEvent(ev)) {
-						if (ev.type == Common::EVENT_QUIT ||
-							ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
-							return;
-						if (ev.type == Common::EVENT_LBUTTONDOWN ||
-							ev.type == Common::EVENT_KEYDOWN) {
-							skip = true;
-							break;
-						}
-					}
-					g_system->delayMillis(5);
-				}
-				if (skip)
-					break;
-			}
-			return;
-		}
-
-		const Common::String name =
-			Common::String::format("VIDEO%02u.A", data.videoId);
-		ANMDecoder anm;
-		if (!anm.open(Common::Path(name))) {
-			warning("London approach: cannot open %s", name.c_str());
-			return;
-		}
-
-		// Frame 0 is already the static background. Replay frames 1..end
-		// with the approach text/buttons composited above it.
-		(void)anm.nextFrame();
-		const int copyW = MIN<int>(anm.width(), sw - (int)data.videoX);
-		const int copyH = MIN<int>(anm.height(), sh - (int)data.videoY);
-		if (copyW <= 0 || copyH <= 0)
-			return;
-
-		while (!shouldQuit()) {
-			const byte *frame = anm.nextFrame();
-			if (!frame)
-				break;
-			Graphics::ManagedSurface scratch(sw, sh,
-				Graphics::PixelFormat::createFormatCLUT8());
-			scratch.clear();
-			scratch.copyRectToSurface(frame, anm.width(),
-									  data.videoX, data.videoY,
-									  copyW, copyH);
-			drawApproachOverlay(scratch, page);
-			g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
-									   0, 0, sw, sh);
-			g_system->updateScreen();
-
-			const uint32 start = g_system->getMillis();
-			bool skip = false;
-			while (g_system->getMillis() - start < 120 && !skip) {
-				Common::Event ev;
-				while (g_system->getEventManager()->pollEvent(ev)) {
-					if (ev.type == Common::EVENT_QUIT ||
-						ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
-						return;
-					}
-					if (ev.type == Common::EVENT_LBUTTONDOWN ||
-						ev.type == Common::EVENT_KEYDOWN) {
-						skip = true;
-						break;
-					}
-				}
-				g_system->delayMillis(5);
-			}
-			if (skip)
-				break;
-		}
-	};
-
-	fadeCurrentPaletteToBlack();
-	CursorMan.showMouse(true);
-	setSiteHotspotCursorId(6);
-	if (_music && _voiceOn)
-		_music->playMus(0x27, /* loop= */ true);
-
-	uint page = 0;
-	drawScreen(page);
-	if (haveVideo)
-		fadePaletteFromBlack(palette);
-	else
-		setSitePalette(0x3b);
-	if (haveVideo) {
-		playVideo(page);
-		drawScreen(page);
-	}
-	bool done = false;
-	uint32 lastShimmer = g_system->getMillis();
-	while (!shouldQuit() && !done) {
-		Common::Event ev;
-		while (g_system->getEventManager()->pollEvent(ev)) {
-			if (ev.type == Common::EVENT_QUIT ||
-				ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
-				done = true;
-				break;
-			}
-			if (ev.type == Common::EVENT_MOUSEMOVE) {
-				bool overButton = false;
-				if (mac) {
-					overButton = macApproachButton(
-						Common::Point(ev.mouse.x, ev.mouse.y)) >= 0;
-				} else {
-					for (uint i = 0; i < ARRAYSIZE(kLondonApproachButtons); i++) {
-						if (buttonRect(i).contains(ev.mouse.x, ev.mouse.y)) {
-							overButton = true;
-							break;
-						}
-					}
-				}
-				setInteractiveMouseCursor(overButton);
-			}
-			if (ev.type == Common::EVENT_KEYDOWN) {
-				if (ev.kbd.keycode == Common::KEYCODE_ESCAPE ||
-					ev.kbd.keycode == Common::KEYCODE_RETURN ||
-					ev.kbd.keycode == Common::KEYCODE_KP_ENTER) {
-					done = true;
-					break;
-				}
-				if (ev.kbd.keycode == Common::KEYCODE_SPACE) {
-					playVideo(page);
-					drawScreen(page);
-				} else if (ev.kbd.keycode == Common::KEYCODE_RIGHT ||
-						   ev.kbd.keycode == Common::KEYCODE_DOWN) {
-					if (page + 1 < data.pages.size()) {
-						page++;
-						drawScreen(page);
-					}
-				} else if (ev.kbd.keycode == Common::KEYCODE_LEFT ||
-						   ev.kbd.keycode == Common::KEYCODE_UP) {
-					if (page > 0) {
-						page--;
-						drawScreen(page);
-					}
-				}
-			}
-			if (ev.type == Common::EVENT_LBUTTONDOWN) {
-				if (mac) {
-					const int button = macApproachButton(
-						Common::Point(ev.mouse.x, ev.mouse.y));
-					if (button == 0) {
-						done = true;
-						break;
-					}
-					if (button == 1) {
-						playVideo(page);
-						drawScreen(page);
-					}
-					continue;
-				}
-				if (buttonRect(0).contains(ev.mouse.x, ev.mouse.y)) {
-					done = true;
-					break;
-				}
-				if (buttonRect(1).contains(ev.mouse.x, ev.mouse.y)) {
-					playVideo(page);
-					drawScreen(page);
-				} else if (buttonRect(2).contains(ev.mouse.x, ev.mouse.y)) {
-					if (page + 1 < data.pages.size()) {
-						page++;
-						drawScreen(page);
-					}
-				} else if (buttonRect(3).contains(ev.mouse.x, ev.mouse.y)) {
-					if (page > 0) {
-						page--;
-						drawScreen(page);
-					}
-				}
-			}
-		}
-		if (haveVideo && !mac) {
-			const uint32 now = g_system->getMillis();
-			if (now - lastShimmer >= 70) {
-				lastShimmer = now;
-				cyclePaletteRange(0xF3, 0xF7);
-			}
-		}
-		g_system->updateScreen();
-		g_system->delayMillis(10);
-	}
-
-	setInteractiveMouseCursor(false);
-	setSiteHotspotCursorId(0);
-	stopMusic();
-	return true;
-}
-
-void EEMEngine::drawBigMapOverview(uint32 elapsedMs) {
-	const bool mac = isMacintosh();
-	const int sw = screenWidth();
-	const int sh = screenHeight();
-	Graphics::ManagedSurface scratch(sw, sh,
-		Graphics::PixelFormat::createFormatCLUT8());
-	scratch.clear();
-
-	Picture frame;
-	if (_picsArchive.getPicture(0x42, frame)) {
-		if (mac)
-			remapMacSurfaceEndpoints(frame.surface, getMacSpritePaletteMap());
-		scratch.simpleBlitFrom(frame.surface);
-	}
-
-	// Marker PICs from `_main`:
-	//   EEM1 CD @ 1a35:0f59: Done=0x20d, Site=0xc5, Crime=0xc6.
-	//   EEM2 CD @ 1abf:11a6: Done=0x006, Site=0xc5, Crime=0xc6.
-	Picture done;
-	Picture normal;
-	Picture crimeM;
-	const bool haveDone = _picsArchive.getPicture(isLondon() ? 0x006 : 0x20d, done) &&
-		done.surface.w < 64 && done.surface.h < 64;
-	const bool haveNormal = _picsArchive.getPicture(0xc5, normal) &&
-		normal.surface.w < 64 && normal.surface.h < 64;
-	const bool haveCrime = _picsArchive.getPicture(0xc6, crimeM) &&
-		crimeM.surface.w < 64 && crimeM.surface.h < 64;
-
-	for (uint i = 0; i < _mystery.numSites(); i++) {
-		// `_DrawBigMapButtons` gates markers on the on-map flag alone, never the
-		// current site: a sublocation (in-site jump, never flagged) must not draw.
-		if (!_mystery._onSites[i])
-			continue;
-		const byte *entry = _mystery.mapEntry(i);
-		if (!entry)
-			continue;
-
-		const bool floppy  = _mystery.isLoaded() && isFloppy();
-		BigMapEntryInfo info;
-		if (!readBigMapEntryInfo(entry, floppy,
-								 mac && _mystery.usesCompactMacData(), info))
-			continue;
-		const bool isDone = (i < Mystery::kVisitedSiteCap)
-							 && _mystery._visitedSite[i];
-
-		const Picture *m = nullptr;
-		bool useVisitedColors = false;
-		if (isDone && haveDone) {
-			m = &done;
-		} else if (isDone && haveNormal) {
-			m = &normal;
-			useVisitedColors = true;
-		} else if (info.crime != 0 && haveCrime) {
-			m = &crimeM;
-		} else if (haveNormal) {
-			m = &normal;
-		}
-
-		if (m) {
-			blitBigMapMarker(scratch, *m, (int)info.overviewX,
-							  (int)info.overviewY,
-							  useVisitedColors);
-		} else {
-			const int mx = (int)info.overviewX;
-			const int my = (int)info.overviewY;
-			const Common::Rect mark(mx - 3, my - 3, mx + 4, my + 4);
-			scratch.fillRect(mark, 0x0F);
-		}
-	}
-
-	const uint kMapAniId = (_partner == kPartnerJake) ? 0x14 : 0x12;
-	Animation mapAnim;
-	if (_aniArchive.loadAnimation(kMapAniId, mapAnim) && !mapAnim.empty()) {
-		const uint frameIdx = bigMapPartnerFrameAtTick((uint)mapAnim.size(),
-													   elapsedMs, isLondon());
-
-		const int anchorX = mac ? scaleX(0xfd) : 0xfd;
-		const int anchorY = mac ? scaleY(0x50) : 0x50;
-		if (mac)
-			blitMacAnimFrameAnchored(scratch.surfacePtr(), mapAnim[frameIdx],
-									 anchorX, anchorY);
-		else
-			blitAnimFrameAnchored(scratch.surfacePtr(), mapAnim[frameIdx],
-								  anchorX, anchorY);
-	}
-
-	g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
-							   0, 0, sw, sh);
-	g_system->updateScreen();
-}
-
-void EEMEngine::drawBigMapDetail(int scrollX, int scrollY,
-								 const Common::Array<byte> &mapPixels,
-								 uint16 mapW, uint16 mapH,
-								 uint32 elapsedMs) {
-
-	const bool mac = isMacintosh();
-	const int sw = screenWidth();
-	const int sh = screenHeight();
-	const int kMapWinW = mac ? scaleX(0xe9) : 0xe9;
-	const int kMapWinH = mac ? scaleY(0xab) : 0xab;
-	const int kMapWinX = mac ? scaleX(2) : 2;
-	const int kMapWinY = mac ? scaleY(2) : 2;
-
-	Graphics::ManagedSurface scratch(sw, sh,
-		Graphics::PixelFormat::createFormatCLUT8());
-	scratch.clear();
-
-	Picture frame;
-	if (_picsArchive.getPicture(0x43, frame)) {
-		// The frame and its buttons use the normal 0x00=white / 0xFF=black
-		// convention, but the detail-map ColorTable (0x23) swaps those endpoints,
-		// so normalize them or the SETUP button's black border renders white.
-		if (mac)
-			remapMacSurfaceEndpoints(frame.surface, getMacSpritePaletteMap());
-		scratch.simpleBlitFrom(frame.surface);
-	}
-
-	const int copyW = MIN<int>(mapW - scrollX, kMapWinW);
-	const int copyH = MIN<int>(mapH - scrollY, kMapWinH);
-	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.
-		if (!_mystery._onSites[i])
-			continue;
-		const byte *entry = _mystery.mapEntry(i);
-		if (!entry)
-			continue;
-		BigMapEntryInfo info;
-		if (!readBigMapEntryInfo(entry, floppyMap,
-								 mac && _mystery.usesCompactMacData(), info))
-			continue;
-		Picture button;
-		if (!_buttonArchive.loadEntry(info.buttonId, button))
-			continue;
-		const int sx = (int)info.detailX - scrollX + kMapWinX;
-		const int sy = (int)info.detailY - scrollY + kMapWinY;
-		// Crop the button blit against the viewport.
-		const int x0 = MAX<int>(sx, kMapWinX);
-		const int y0 = MAX<int>(sy, kMapWinY);
-		const int x1 = MIN<int>(sx + button.surface.w, kMapWinX + kMapWinW);
-		const int y1 = MIN<int>(sy + button.surface.h, kMapWinY + kMapWinH);
-		if (x1 > x0 && y1 > y0) {
-			const Common::Rect srcRect(x0 - sx, y0 - sy, x1 - sx, y1 - sy);
-			scratch.transBlitFrom(button.surface, srcRect,
-								  Common::Point(x0, y0),
-								  (uint32)(byte)(button.flags >> 8));
-		}
-	}
-
-	const uint kDetailAniId = (_partner == kPartnerJake) ? 0x13 : 0x11;
-	Animation detailAnim;
-	if (_aniArchive.loadAnimation(kDetailAniId, detailAnim) &&
-		!detailAnim.empty()) {
-		const uint frameIdx = bigMapDetailPartnerFrameAtTick(
-				(uint)detailAnim.size(), elapsedMs);
-		const int anchorX = mac ? scaleX(0x101) : 0x101;
-		const int anchorY = mac ? scaleY(0x50) : 0x50;
-		if (mac)
-			blitMacBigMapPartnerFrame(scratch, detailAnim[frameIdx],
-									  anchorX, anchorY);
-		else
-			blitAnimFrameAnchored(scratch.surfacePtr(),
-								  detailAnim[frameIdx],
-								  anchorX, anchorY);
-	}
-
-	g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
-							   0, 0, sw, sh);
-	g_system->updateScreen();
-}
-
 // `_GetKDTextBalloon @ 1df2:0105`
 uint16 EEMEngine::getKDTextBalloon(byte firstChar) const {
 	if (firstChar < '0' || firstChar > '9')


Commit: 2e596e7adb5578aa99416928938f82c89fd0e609
    https://github.com/scummvm/scummvm/commit/2e596e7adb5578aa99416928938f82c89fd0e609
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-26T20:37:27+02:00

Commit Message:
EEM: refactored map related code into several functions

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


diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 24e1ab8cfdf..12b2b1eea2f 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -24,6 +24,7 @@
 #define EEM_EEM_H
 
 #include "common/array.h"
+#include "common/events.h"
 #include "common/keyboard.h"
 #include "common/language.h"
 #include "common/platform.h"
@@ -366,6 +367,66 @@ private:
 	void drawGalleryFrame(const byte *gd, uint8 numSuspects,
 						  Common::Array<Common::Rect> &slotRects,
 						  Common::Array<int> &slotSuspect);
+
+	struct BigMapOverviewState {
+		Common::Rect window;
+		int zoomX = 0;
+		int zoomY = 0;
+	};
+
+	struct BigMapDetailState {
+		const Common::Array<byte> *mapPixels = nullptr;
+		uint16 mapW = 0;
+		uint16 mapH = 0;
+
+		int scrollX = 0;
+		int scrollY = 0;
+		int maxScrollX = 0;
+		int maxScrollY = 0;
+
+		int mapWinX = 0;
+		int mapWinY = 0;
+		int mapWinW = 0;
+		int mapWinH = 0;
+
+		Common::Rect returnRect;
+		Common::Rect setupRect;
+		Common::Rect arrowYUp;
+		Common::Rect arrowYDown;
+		Common::Rect arrowXLeft;
+		Common::Rect arrowXRight;
+		Common::Rect xSlider;
+		Common::Rect ySlider;
+
+		int arrowStepX = 0;
+		int arrowStepY = 0;
+
+		uint32 startTick = 0;
+		uint32 lastDrawTick = 0;
+		uint32 lastCycleTick = 0;
+	};
+
+	Common::Rect bigMapScaledRect(const Common::Rect &rect) const;
+	bool bigMapRunOverview(BigMapOverviewState &state);
+	bool bigMapLoadDetailPixels(Common::Array<byte> &mapPixels,
+								uint16 &mapW, uint16 &mapH);
+	void bigMapInitDetailState(BigMapDetailState &state,
+							   const Common::Array<byte> &mapPixels,
+							   uint16 mapW, uint16 mapH,
+							   const BigMapOverviewState &overview);
+	bool bigMapRunDetail(BigMapDetailState &state);
+	bool bigMapHandleDetailEvent(const Common::Event &ev,
+								 BigMapDetailState &state,
+								 bool &returnToOverview, bool &dirty);
+	void bigMapHandleDetailKey(const Common::Event &ev,
+							   BigMapDetailState &state, bool &dirty);
+	bool bigMapHandleDetailMouseDown(const Common::Event &ev,
+									 BigMapDetailState &state,
+									 bool &returnToOverview, bool &dirty);
+	bool bigMapTrySelectDetailSite(int mouseX, int mouseY,
+								   const BigMapDetailState &state);
+	void bigMapCycleOverviewPalette(bool mac);
+	void bigMapCycleDetailPalette(bool mac);
 	void drawBigMapOverview(uint32 elapsedMs);
 	void drawBigMapDetail(int scrollX, int scrollY,
 						  const Common::Array<byte> &mapPixels,
diff --git a/engines/eem/map_ui.cpp b/engines/eem/map_ui.cpp
index 36eec5c649b..dd9bd6cf11c 100644
--- a/engines/eem/map_ui.cpp
+++ b/engines/eem/map_ui.cpp
@@ -427,369 +427,462 @@ void remapSurfaceColor(Graphics::ManagedSurface &surface, byte from, byte to) {
 //     20fe:0d2f`). Click icon = travel.
 // MapData entry (14 bytes): +0..3 ???, +4 BigMapX, +6 BigMapY,
 //   +8 SmallMapX, +0xa SmallMapY, +0xc crime-flag.
-void EEMEngine::doBigMap() {
+Common::Rect EEMEngine::bigMapScaledRect(const Common::Rect &rect) const {
+	return isMacintosh() ? scaleRect(rect) : rect;
+}
 
-	if (!_mystery.isLoaded())
+void EEMEngine::bigMapCycleOverviewPalette(bool mac) {
+	if (isLondon()) {
+		if (mac) {
+			// Mac `_UpdateBigMap` rotates ColorTable entries 0xef..0xf2
+			// and 0xfc..0xff. Redraw happens after the cycle so Mac endpoint
+			// art maps its black pixels to the current black slot.
+			cyclePaletteRangeReverse(0xef, 0xf2);
+			cyclePaletteRangeReverse(0xfc, 0xff);
+		} else {
+			cyclePaletteRangeReverse(0xf4, 0xf9);
+			cyclePaletteRangeReverse(0xfa, 0xff);
+		}
+	} else {
+		cyclePaletteRangeReverse(0xf7, 0xfa);
+		cyclePaletteRangeReverse(0xfb, 0xfe);
+	}
+}
+
+void EEMEngine::bigMapCycleDetailPalette(bool mac) {
+	if (!isLondon())
 		return;
 
-	if (isLondon()) {
-		_mystery._pendingSiteJump = 0;
-		_mystery._siteReturnDepth = 0;
-		memset(_mystery._siteReturnStack, 0, sizeof(_mystery._siteReturnStack));
+	if (mac) {
+		cyclePaletteRangeReverse(0xe9, 0xeb);
+		cyclePaletteRangeReverse(0xec, 0xef);
+		cyclePaletteRangeReverse(0xf0, 0xf2);
+	} else {
+		cyclePaletteRangeReverse(0xee, 0xf2);
+		cyclePaletteRangeReverse(0xea, 0xed);
 	}
+}
 
-	CursorMan.showMouse(true);
+bool EEMEngine::bigMapRunOverview(BigMapOverviewState &state) {
+	setInteractiveMouseCursor(false);
+	setSitePalette(isLondon() ? 0x3b : 0x24);
+
+	const bool mac = isMacintosh();
+	const Common::Rect setupBtnBase = isFloppy()
+		? Common::Rect(251, 3, 315, 42)
+		: (isLondon() ? Common::Rect(252, 1, 315, 42)
+					  : Common::Rect(252, 4, 315, 42));
+	const Common::Rect setupBtnRect = bigMapScaledRect(setupBtnBase);
+	state.window = bigMapScaledRect(Common::Rect(0, 0, 247, 192));
+	state.zoomX = 0;
+	state.zoomY = 0;
+
+	const uint32 mapStartTick = g_system->getMillis();
+	drawBigMapOverview(0);
+	uint32 mapLastTick = mapStartTick;
+	uint32 mapLastCycleTick = mapStartTick;
 
 	while (!shouldQuit()) {
-		setInteractiveMouseCursor(false);
-		setSitePalette(isLondon() ? 0x3b : 0x24);
-
-		const uint32 mapStartTick = g_system->getMillis();
-		drawBigMapOverview(0);
-		uint32 mapLastTick = mapStartTick;
-		uint32 mapLastCycleTick = mapStartTick;
-
-		const bool mac = isMacintosh();
-		const Common::Rect bigMapWindowBase(0, 0, 247, 192);
-		const Common::Rect kBigMapWindow =
-			mac ? scaleRect(bigMapWindowBase) : bigMapWindowBase;
-		const Common::Rect setupBtnBase = isFloppy()
-			? Common::Rect(251, 3, 315, 42)
-			: (isLondon() ? Common::Rect(252, 1, 315, 42)
-						  : Common::Rect(252, 4, 315, 42));
-		const Common::Rect kSetupBtnRect =
-			mac ? scaleRect(setupBtnBase) : setupBtnBase;
-
-		bool wantZoom = false;
-		int zoomX = 0;
-		int zoomY = 0;
-		while (!shouldQuit()) {
-			Common::Event ev;
-			while (g_system->getEventManager()->pollEvent(ev)) {
-				if (ev.type == Common::EVENT_QUIT ||
-					ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
-					return;
-				if (ev.type == Common::EVENT_KEYDOWN &&
-					ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
-					openMainMenuDialog();
-					continue;
-				}
-				if (ev.type == Common::EVENT_LBUTTONDOWN) {
-					if (kSetupBtnRect.contains(ev.mouse.x, ev.mouse.y)) {
-						_nextScreen = kScreenSetup;
-						return;
-					}
+		Common::Event ev;
+		while (g_system->getEventManager()->pollEvent(ev)) {
+			if (ev.type == Common::EVENT_QUIT ||
+				ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+				return false;
+			if (ev.type == Common::EVENT_KEYDOWN &&
+				ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
+				openMainMenuDialog();
+				continue;
+			}
+			if (ev.type != Common::EVENT_LBUTTONDOWN)
+				continue;
 
-					if (kBigMapWindow.contains(ev.mouse.x, ev.mouse.y)) {
-						if (mac) {
-							zoomX = ev.mouse.x - kBigMapWindow.left;
-							zoomY = ev.mouse.y - kBigMapWindow.top;
-						} else {
-							int sx = ev.mouse.x * 2;
-							int sy = ev.mouse.y * 2;
-							sx = (sx < 0x75) ? 0 : sx - 0x74;
-							sy = (sy < 0x56) ? 0 : sy - 0x55;
-							zoomX = sx;
-							zoomY = sy;
-						}
-						wantZoom = true;
-						break;
-					}
-				}
+			if (setupBtnRect.contains(ev.mouse.x, ev.mouse.y)) {
+				_nextScreen = kScreenSetup;
+				return false;
 			}
-			if (wantZoom)
-				break;
 
-			const uint32 now = g_system->getMillis();
-			if (mac && isLondon() &&
-				now - mapLastCycleTick >= kMacMapColorCycleDelayMs) {
-				mapLastCycleTick = now;
-				// Mac `_UpdateBigMap` rotates ColorTable entries 0xef..0xf2
-				// and 0xfc..0xff. Redraw immediately so Mac endpoint art maps
-				// its black pixels to the current black slot after the cycle.
-				cyclePaletteRangeReverse(0xef, 0xf2);
-				cyclePaletteRangeReverse(0xfc, 0xff);
-				drawBigMapOverview(now - mapStartTick);
-				mapLastTick = now;
-			} else if (now - mapLastTick >= 100) {
-				mapLastTick = now;
-				drawBigMapOverview(now - mapStartTick);
-				if (isLondon()) {
-					if (!mac) {
-						cyclePaletteRangeReverse(0xf4, 0xf9);
-						cyclePaletteRangeReverse(0xfa, 0xff);
-					}
+			if (state.window.contains(ev.mouse.x, ev.mouse.y)) {
+				if (mac) {
+					state.zoomX = ev.mouse.x - state.window.left;
+					state.zoomY = ev.mouse.y - state.window.top;
 				} else {
-					cyclePaletteRangeReverse(0xf7, 0xfa);
-					cyclePaletteRangeReverse(0xfb, 0xfe);
+					int sx = ev.mouse.x * 2;
+					int sy = ev.mouse.y * 2;
+					sx = (sx < 0x75) ? 0 : sx - 0x74;
+					sy = (sy < 0x56) ? 0 : sy - 0x55;
+					state.zoomX = sx;
+					state.zoomY = sy;
 				}
+				return true;
 			}
-			g_system->updateScreen();
-			g_system->delayMillis(10);
 		}
 
-		if (!wantZoom)
-			return;
+		const uint32 now = g_system->getMillis();
+		if (mac && isLondon() &&
+			now - mapLastCycleTick >= kMacMapColorCycleDelayMs) {
+			mapLastCycleTick = now;
+			bigMapCycleOverviewPalette(mac);
+			drawBigMapOverview(now - mapStartTick);
+			mapLastTick = now;
+		} else if (now - mapLastTick >= 100) {
+			mapLastTick = now;
+			drawBigMapOverview(now - mapStartTick);
+			if (!mac || !isLondon())
+				bigMapCycleOverviewPalette(mac);
+		}
+		g_system->updateScreen();
+		g_system->delayMillis(10);
+	}
 
-		uint16 mapW = 0;
-		uint16 mapH = 0;
-		Common::Array<byte> mapPixels;
-		if (mac) {
-			if (!loadMacBigMapPixels(mapPixels, mapW, mapH))
-				return;
-		} else {
-			Common::File f;
-			if (!f.open(Common::Path("BIGMAP.PIC"))) {
-				warning("doBigMap: BIGMAP.PIC missing for detail view");
-				return;
-			}
-			mapH = f.readUint16LE();
-			mapW = f.readUint16LE();
-			if (mapW == 0 || mapH == 0)
-				return;
-			mapPixels.resize((uint32)mapW * mapH);
-			if (f.read(mapPixels.data(), mapPixels.size()) != mapPixels.size()) {
-				warning("doBigMap: short read on BIGMAP.PIC for detail view");
-				return;
-			}
+	return false;
+}
+
+bool EEMEngine::bigMapLoadDetailPixels(Common::Array<byte> &mapPixels,
+									   uint16 &mapW, uint16 &mapH) {
+	mapW = 0;
+	mapH = 0;
+	mapPixels.clear();
+
+	if (isMacintosh())
+		return loadMacBigMapPixels(mapPixels, mapW, mapH);
+
+	Common::File f;
+	if (!f.open(Common::Path("BIGMAP.PIC"))) {
+		warning("doBigMap: BIGMAP.PIC missing for detail view");
+		return false;
+	}
+	mapH = f.readUint16LE();
+	mapW = f.readUint16LE();
+	if (mapW == 0 || mapH == 0)
+		return false;
+	mapPixels.resize((uint32)mapW * mapH);
+	if (f.read(mapPixels.data(), mapPixels.size()) != mapPixels.size()) {
+		warning("doBigMap: short read on BIGMAP.PIC for detail view");
+		return false;
+	}
+	return true;
+}
+
+void EEMEngine::bigMapInitDetailState(BigMapDetailState &state,
+									  const Common::Array<byte> &mapPixels,
+									  uint16 mapW, uint16 mapH,
+									  const BigMapOverviewState &overview) {
+	const bool mac = isMacintosh();
+
+	state.mapPixels = &mapPixels;
+	state.mapW = mapW;
+	state.mapH = mapH;
+	state.mapWinW = mac ? scaleX(0xe9) : 0xe9; // 233
+	state.mapWinH = mac ? scaleY(0xab) : 0xab; // 171
+	state.mapWinX = mac ? scaleX(2) : 2;
+	state.mapWinY = mac ? scaleY(2) : 2;
+	state.maxScrollX = MAX<int>(0, (int)mapW - state.mapWinW);
+	state.maxScrollY = MAX<int>(0, (int)mapH - state.mapWinH);
+
+	if (mac) {
+		state.scrollX = overview.zoomX * (int)mapW /
+			MAX<int>(1, overview.window.width()) - state.mapWinW / 2;
+		state.scrollY = overview.zoomY * (int)mapH /
+			MAX<int>(1, overview.window.height()) - state.mapWinH / 2;
+	} else {
+		state.scrollX = overview.zoomX;
+		state.scrollY = overview.zoomY;
+	}
+	state.scrollX = MAX<int>(0, MIN<int>(state.maxScrollX, state.scrollX));
+	state.scrollY = MAX<int>(0, MIN<int>(state.maxScrollY, state.scrollY));
+
+	state.returnRect = bigMapScaledRect(
+		Common::Rect(252, 43, kScreenWidth, kScreenHeight));
+	state.arrowYUp = bigMapScaledRect(Common::Rect(237, 2, 247, 11));
+	state.arrowYDown = bigMapScaledRect(Common::Rect(237, 163, 247, 172));
+	state.arrowXLeft = bigMapScaledRect(Common::Rect(2, 175, 12, 185));
+	state.arrowXRight = bigMapScaledRect(Common::Rect(224, 175, 234, 185));
+	state.xSlider = bigMapScaledRect(isLondon()
+		? Common::Rect(15, 176, 220, 184)
+		: Common::Rect(15, 175, 221, 185));
+	state.ySlider = bigMapScaledRect(isLondon()
+		? Common::Rect(238, 16, 246, 158)
+		: Common::Rect(237, 14, 247, 160));
+	state.setupRect = bigMapScaledRect(isFloppy()
+		? Common::Rect(251, 3, 315, 42)
+		: (isLondon() ? Common::Rect(251, 3, 315, 42)
+					  : Common::Rect(252, 4, 315, 42)));
+
+	const int baseArrowStep = isLondon() ? 8 : 16;
+	state.arrowStepX = mac ? scaleX(baseArrowStep) : baseArrowStep;
+	state.arrowStepY = mac ? scaleY(baseArrowStep) : baseArrowStep;
+}
+
+bool EEMEngine::bigMapTrySelectDetailSite(int mouseX, int mouseY,
+										  const BigMapDetailState &state) {
+	if (mouseX < state.mapWinX || mouseX >= state.mapWinX + state.mapWinW ||
+		mouseY < state.mapWinY || mouseY >= state.mapWinY + state.mapWinH)
+		return false;
+
+	// Per-site bbox from `_StampButtons` (SmallMap +8/+0xa).
+	struct DetailMapHit {
+		uint site;
+		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])
+			continue;
+		const byte *entry = _mystery.mapEntry(i);
+		if (!entry)
+			continue;
+		BigMapEntryInfo info;
+		if (!readBigMapEntryInfo(entry, floppyMap, macMap, info))
+			continue;
+
+		Picture button;
+		int bw = 16;
+		int bh = 16;
+		if (_buttonArchive.loadEntry(info.buttonId, button)) {
+			bw = button.surface.w;
+			bh = button.surface.h;
+		}
+		const int sx = (int)info.detailX - state.scrollX + state.mapWinX;
+		const int sy = (int)info.detailY - state.scrollY + state.mapWinY;
+		const Common::Rect r(sx, sy, sx + bw, sy + bh);
+		if (r.intersects(Common::Rect(state.mapWinX, state.mapWinY,
+				state.mapWinX + state.mapWinW,
+				state.mapWinY + state.mapWinH))) {
+			DetailMapHit hit = { i, r };
+			hits.push_back(hit);
+		}
+	}
+	Common::sort(hits.begin(), hits.end(),
+		[](const DetailMapHit &a, const DetailMapHit &b) {
+			if (a.rect.top != b.rect.top)
+				return a.rect.top < b.rect.top;
+			return a.rect.left < b.rect.left;
+		});
+	for (uint i = 0; i < hits.size(); i++) {
+		if (hits[i].rect.contains(mouseX, mouseY)) {
+			_mystery._lastSite = _mystery._siteNumber;
+			_mystery._siteNumber = (uint16)hits[i].site;
+			return true;
 		}
+	}
+	return false;
+}
 
-		const int kMapWinW = mac ? scaleX(0xe9) : 0xe9; // 233
-		const int kMapWinH = mac ? scaleY(0xab) : 0xab; // 171
-		const int kMapWinX = mac ? scaleX(2) : 2;
-		const int kMapWinY = mac ? scaleY(2) : 2;
+void EEMEngine::bigMapHandleDetailKey(const Common::Event &ev,
+									  BigMapDetailState &state,
+									  bool &dirty) {
+	switch (ev.kbd.keycode) {
+	case Common::KEYCODE_ESCAPE:
+		openMainMenuDialog();
+		dirty = true;
+		break;
+	case Common::KEYCODE_LEFT:
+		state.scrollX = MAX<int>(0, state.scrollX - state.arrowStepX);
+		dirty = true;
+		break;
+	case Common::KEYCODE_RIGHT:
+		state.scrollX = MIN<int>(state.maxScrollX,
+								 state.scrollX + state.arrowStepX);
+		dirty = true;
+		break;
+	case Common::KEYCODE_UP:
+		state.scrollY = MAX<int>(0, state.scrollY - state.arrowStepY);
+		dirty = true;
+		break;
+	case Common::KEYCODE_DOWN:
+		state.scrollY = MIN<int>(state.maxScrollY,
+								 state.scrollY + state.arrowStepY);
+		dirty = true;
+		break;
+	default:
+		break;
+	}
+}
 
-		const int maxScrollX = MAX<int>(0, (int)mapW - kMapWinW);
-		const int maxScrollY = MAX<int>(0, (int)mapH - kMapWinH);
-		int scrollX;
-		int scrollY;
-		if (mac) {
-			scrollX = zoomX * (int)mapW /
-				MAX<int>(1, kBigMapWindow.width()) - kMapWinW / 2;
-			scrollY = zoomY * (int)mapH /
-				MAX<int>(1, kBigMapWindow.height()) - kMapWinH / 2;
-		} else {
-			scrollX = zoomX;
-			scrollY = zoomY;
+bool EEMEngine::bigMapHandleDetailMouseDown(const Common::Event &ev,
+											BigMapDetailState &state,
+											bool &returnToOverview,
+											bool &dirty) {
+	setInteractiveMouseCursor(
+		state.returnRect.contains(ev.mouse.x, ev.mouse.y) ||
+		state.setupRect.contains(ev.mouse.x, ev.mouse.y));
+
+	if (state.setupRect.contains(ev.mouse.x, ev.mouse.y)) {
+		_nextScreen = kScreenSetup;
+		setInteractiveMouseCursor(false);
+		return false;
+	}
+	if (state.returnRect.contains(ev.mouse.x, ev.mouse.y)) {
+		returnToOverview = true;
+		return true;
+	}
+	if (state.arrowYUp.contains(ev.mouse.x, ev.mouse.y)) {
+		state.scrollY = MAX<int>(0, state.scrollY - state.arrowStepY);
+		dirty = true;
+		return true;
+	}
+	if (state.arrowYDown.contains(ev.mouse.x, ev.mouse.y)) {
+		state.scrollY = MIN<int>(state.maxScrollY,
+								 state.scrollY + state.arrowStepY);
+		dirty = true;
+		return true;
+	}
+	if (state.arrowXLeft.contains(ev.mouse.x, ev.mouse.y)) {
+		state.scrollX = MAX<int>(0, state.scrollX - state.arrowStepX);
+		dirty = true;
+		return true;
+	}
+	if (state.arrowXRight.contains(ev.mouse.x, ev.mouse.y)) {
+		state.scrollX = MIN<int>(state.maxScrollX,
+								 state.scrollX + state.arrowStepX);
+		dirty = true;
+		return true;
+	}
+	if (state.xSlider.contains(ev.mouse.x, ev.mouse.y)) {
+		if (state.maxScrollX > 0) {
+			const int t = ev.mouse.x - state.xSlider.left;
+			const int tw = state.xSlider.width();
+			state.scrollX = MAX<int>(0, MIN<int>(state.maxScrollX,
+				t * state.maxScrollX / MAX<int>(1, tw)));
+			dirty = true;
+		}
+		return true;
+	}
+	if (state.ySlider.contains(ev.mouse.x, ev.mouse.y)) {
+		if (state.maxScrollY > 0) {
+			const int t = ev.mouse.y - state.ySlider.top;
+			const int th = state.ySlider.height();
+			state.scrollY = MAX<int>(0, MIN<int>(state.maxScrollY,
+				t * state.maxScrollY / MAX<int>(1, th)));
+			dirty = true;
 		}
-		scrollX = MAX<int>(0, MIN<int>(maxScrollX, scrollX));
-		scrollY = MAX<int>(0, MIN<int>(maxScrollY, scrollY));
-
-		setSitePalette(isLondon() ? 0x3a : 0x23);
-
-		const uint32 detailStartTick = g_system->getMillis();
-		drawBigMapDetail(scrollX, scrollY, mapPixels, mapW, mapH, 0);
-		uint32 detailLastTick = detailStartTick;
-		uint32 detailLastCycleTick = detailStartTick;
-		bool returnToOverview = false;
-
-		const Common::Rect returnBase(252, 43, kScreenWidth, kScreenHeight);
-		const Common::Rect kBigMapReturnRect =
-			mac ? scaleRect(returnBase) : returnBase;
-		const Common::Rect kArrowYUp =
-			mac ? scaleRect(Common::Rect(237, 2, 247, 11))
-				: Common::Rect(237, 2, 247, 11);
-		const Common::Rect kArrowYDown =
-			mac ? scaleRect(Common::Rect(237, 163, 247, 172))
-				: Common::Rect(237, 163, 247, 172);
-		const Common::Rect kArrowXLeft =
-			mac ? scaleRect(Common::Rect(2, 175, 12, 185))
-				: Common::Rect(2, 175, 12, 185);
-		const Common::Rect kArrowXRight =
-			mac ? scaleRect(Common::Rect(224, 175, 234, 185))
-				: Common::Rect(224, 175, 234, 185);
-		const Common::Rect xSliderBase = isLondon()
-			? Common::Rect(15, 176, 220, 184)
-			: Common::Rect(15, 175, 221, 185);
-		const Common::Rect ySliderBase = isLondon()
-			? Common::Rect(238, 16, 246, 158)
-			: Common::Rect(237, 14, 247, 160);
-		const Common::Rect kXSlider =
-			mac ? scaleRect(xSliderBase) : xSliderBase;
-		const Common::Rect kYSlider =
-			mac ? scaleRect(ySliderBase) : ySliderBase;
-		const Common::Rect detailSetupBase = isFloppy()
-			? Common::Rect(251, 3, 315, 42)
-			: (isLondon() ? Common::Rect(251, 3, 315, 42)
-						  : Common::Rect(252, 4, 315, 42));
-		const Common::Rect kDetailSetupBtn =
-			mac ? scaleRect(detailSetupBase) : detailSetupBase;
-		const int baseArrowStep = isLondon() ? 8 : 16;
-		const int kArrowStepX = mac ? scaleX(baseArrowStep) : baseArrowStep;
-		const int kArrowStepY = mac ? scaleY(baseArrowStep) : baseArrowStep;
-		const int kSliderRange = maxScrollX;
-		const int kSliderRangeY = maxScrollY;
-		const Common::Point detailMouse =
-			g_system->getEventManager()->getMousePos();
+		return true;
+	}
+	if (bigMapTrySelectDetailSite(ev.mouse.x, ev.mouse.y, state)) {
+		setInteractiveMouseCursor(false);
+		return false;
+	}
+
+	return true;
+}
+
+bool EEMEngine::bigMapHandleDetailEvent(const Common::Event &ev,
+										BigMapDetailState &state,
+										bool &returnToOverview,
+										bool &dirty) {
+	if (ev.type == Common::EVENT_QUIT ||
+		ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
+		setInteractiveMouseCursor(false);
+		return false;
+	}
+
+	if (ev.type == Common::EVENT_KEYDOWN) {
+		bigMapHandleDetailKey(ev, state, dirty);
+		return true;
+	}
+
+	if (ev.type == Common::EVENT_MOUSEMOVE) {
 		setInteractiveMouseCursor(
-			kBigMapReturnRect.contains(detailMouse.x, detailMouse.y) ||
-			kDetailSetupBtn.contains(detailMouse.x, detailMouse.y));
-
-		while (!shouldQuit() && !returnToOverview) {
-			Common::Event ev;
-			bool dirty = false;
-			bool cycleTick = false;
-			while (g_system->getEventManager()->pollEvent(ev)) {
-				if (ev.type == Common::EVENT_QUIT ||
-					ev.type == Common::EVENT_RETURN_TO_LAUNCHER) {
-					setInteractiveMouseCursor(false);
-					return;
-				}
-				if (ev.type == Common::EVENT_KEYDOWN) {
-					if (ev.kbd.keycode == Common::KEYCODE_ESCAPE) {
-						openMainMenuDialog();
-						dirty = true;
-						continue;
-					}
-					if (ev.kbd.keycode == Common::KEYCODE_LEFT) {
-						scrollX = MAX<int>(0, scrollX - kArrowStepX);
-						dirty = true;
-					} else if (ev.kbd.keycode == Common::KEYCODE_RIGHT) {
-						scrollX = MIN<int>(maxScrollX, scrollX + kArrowStepX);
-						dirty = true;
-					} else if (ev.kbd.keycode == Common::KEYCODE_UP) {
-						scrollY = MAX<int>(0, scrollY - kArrowStepY);
-						dirty = true;
-					} else if (ev.kbd.keycode == Common::KEYCODE_DOWN) {
-						scrollY = MIN<int>(maxScrollY, scrollY + kArrowStepY);
-						dirty = true;
-					}
-				}
-				if (ev.type == Common::EVENT_MOUSEMOVE)
-					setInteractiveMouseCursor(
-						kBigMapReturnRect.contains(ev.mouse.x, ev.mouse.y) ||
-						kDetailSetupBtn.contains(ev.mouse.x, ev.mouse.y));
-				if (ev.type == Common::EVENT_LBUTTONDOWN) {
-					setInteractiveMouseCursor(
-						kBigMapReturnRect.contains(ev.mouse.x, ev.mouse.y) ||
-						kDetailSetupBtn.contains(ev.mouse.x, ev.mouse.y));
-					if (kDetailSetupBtn.contains(ev.mouse.x, ev.mouse.y)) {
-						_nextScreen = kScreenSetup;
-						setInteractiveMouseCursor(false);
-						return;
-					}
-					if (kBigMapReturnRect.contains(ev.mouse.x, ev.mouse.y)) {
-						returnToOverview = true;
-						break;
-					} else if (kArrowYUp.contains(ev.mouse.x, ev.mouse.y)) {
-						scrollY = MAX<int>(0, scrollY - kArrowStepY);
-						dirty = true;
-					} else if (kArrowYDown.contains(ev.mouse.x, ev.mouse.y)) {
-						scrollY = MIN<int>(MAX<int>(0, kSliderRangeY),
-							scrollY + kArrowStepY);
-						dirty = true;
-					} else if (kArrowXLeft.contains(ev.mouse.x, ev.mouse.y)) {
-						scrollX = MAX<int>(0, scrollX - kArrowStepX);
-						dirty = true;
-					} else if (kArrowXRight.contains(ev.mouse.x, ev.mouse.y)) {
-						scrollX = MIN<int>(MAX<int>(0, kSliderRange),
-							scrollX + kArrowStepX);
-						dirty = true;
-					} else if (kXSlider.contains(ev.mouse.x, ev.mouse.y)) {
-						if (kSliderRange > 0) {
-							const int t = ev.mouse.x - kXSlider.left;
-							const int tw = kXSlider.width();
-							scrollX = MAX<int>(0, MIN<int>(kSliderRange,
-								t * kSliderRange / MAX<int>(1, tw)));
-							dirty = true;
-						}
-					} else if (kYSlider.contains(ev.mouse.x, ev.mouse.y)) {
-						if (kSliderRangeY > 0) {
-							const int t = ev.mouse.y - kYSlider.top;
-							const int th = kYSlider.height();
-							scrollY = MAX<int>(0, MIN<int>(kSliderRangeY,
-								t * kSliderRangeY / MAX<int>(1, th)));
-							dirty = true;
-						}
-					} else if (ev.mouse.x >= kMapWinX &&
-							   ev.mouse.x < kMapWinX + kMapWinW &&
-							   ev.mouse.y >= kMapWinY &&
-							   ev.mouse.y < kMapWinY + kMapWinH) {
-						// Per-site bbox from `_StampButtons` (SmallMap +8/+0xa).
-						const bool fmap = _mystery.isLoaded() && isFloppy();
-						struct DetailMapHit {
-							uint site;
-							Common::Rect rect;
-						};
-						Common::Array<DetailMapHit> hits;
-						for (uint i = 0; i < _mystery.numSites(); i++) {
-							// On-map flag alone, matching `_SearchMapButtons`.
-							if (!_mystery._onSites[i])
-								continue;
-							const byte *entry = _mystery.mapEntry(i);
-							if (!entry)
-								continue;
-							BigMapEntryInfo info;
-							if (!readBigMapEntryInfo(entry, fmap,
-													  mac && _mystery.usesCompactMacData(),
-													  info))
-								continue;
-							Picture button;
-							int bw = 16;
-							int bh = 16;
-							if (_buttonArchive.loadEntry(info.buttonId, button)) {
-								bw = button.surface.w;
-								bh = button.surface.h;
-							}
-							const int sx = (int)info.detailX - scrollX + kMapWinX;
-							const int sy = (int)info.detailY - scrollY + kMapWinY;
-							const Common::Rect r(sx, sy, sx + bw, sy + bh);
-							if (r.intersects(Common::Rect(kMapWinX, kMapWinY,
-									kMapWinX + kMapWinW, kMapWinY + kMapWinH))) {
-								DetailMapHit hit = { i, r };
-								hits.push_back(hit);
-							}
-						}
-						Common::sort(hits.begin(), hits.end(),
-							[](const DetailMapHit &a, const DetailMapHit &b) {
-								if (a.rect.top != b.rect.top)
-									return a.rect.top < b.rect.top;
-								return a.rect.left < b.rect.left;
-							});
-						for (uint i = 0; i < hits.size(); i++) {
-							if (hits[i].rect.contains(ev.mouse.x, ev.mouse.y)) {
-								_mystery._lastSite = _mystery._siteNumber;
-								_mystery._siteNumber = (uint16)hits[i].site;
-								setInteractiveMouseCursor(false);
-								return;
-							}
-						}
-					}
-				}
-			}
+			state.returnRect.contains(ev.mouse.x, ev.mouse.y) ||
+			state.setupRect.contains(ev.mouse.x, ev.mouse.y));
+		return true;
+	}
+
+	if (ev.type == Common::EVENT_LBUTTONDOWN)
+		return bigMapHandleDetailMouseDown(ev, state, returnToOverview,
+										   dirty);
+
+	return true;
+}
+
+bool EEMEngine::bigMapRunDetail(BigMapDetailState &state) {
+	if (!state.mapPixels)
+		return false;
+
+	const bool mac = isMacintosh();
+	setSitePalette(isLondon() ? 0x3a : 0x23);
+
+	state.startTick = g_system->getMillis();
+	state.lastDrawTick = state.startTick;
+	state.lastCycleTick = state.startTick;
+	drawBigMapDetail(state.scrollX, state.scrollY, *state.mapPixels,
+					 state.mapW, state.mapH, 0);
+
+	const Common::Point detailMouse =
+		g_system->getEventManager()->getMousePos();
+	setInteractiveMouseCursor(
+		state.returnRect.contains(detailMouse.x, detailMouse.y) ||
+		state.setupRect.contains(detailMouse.x, detailMouse.y));
+
+	bool returnToOverview = false;
+	while (!shouldQuit() && !returnToOverview) {
+		Common::Event ev;
+		bool dirty = false;
+		bool cycleTick = false;
+		while (g_system->getEventManager()->pollEvent(ev)) {
+			if (!bigMapHandleDetailEvent(ev, state, returnToOverview, dirty))
+				return false;
 			if (returnToOverview)
 				break;
+		}
+		if (returnToOverview)
+			break;
 
-			const uint32 now = g_system->getMillis();
-			if (now - detailLastTick >= 100) {
-				detailLastTick = now;
+		const uint32 now = g_system->getMillis();
+		if (now - state.lastDrawTick >= 100) {
+			state.lastDrawTick = now;
+			dirty = true;
+		}
+		if (isLondon()) {
+			const uint32 cycleDelay = mac ? kMacMapColorCycleDelayMs : 100;
+			if (now - state.lastCycleTick >= cycleDelay) {
+				state.lastCycleTick = now;
+				cycleTick = true;
 				dirty = true;
 			}
-			if (isLondon()) {
-				const uint32 cycleDelay = mac ? kMacMapColorCycleDelayMs : 100;
-				if (now - detailLastCycleTick >= cycleDelay) {
-					detailLastCycleTick = now;
-					cycleTick = true;
-					dirty = true;
-				}
-			}
-			if (cycleTick && isLondon()) {
-				if (mac) {
-					cyclePaletteRangeReverse(0xe9, 0xeb);
-					cyclePaletteRangeReverse(0xec, 0xef);
-					cyclePaletteRangeReverse(0xf0, 0xf2);
-				} else {
-					cyclePaletteRangeReverse(0xee, 0xf2);
-					cyclePaletteRangeReverse(0xea, 0xed);
-				}
-			}
-			if (dirty)
-				drawBigMapDetail(scrollX, scrollY, mapPixels, mapW, mapH,
-					now - detailStartTick);
-			g_system->updateScreen();
-			g_system->delayMillis(10);
 		}
-		if (!returnToOverview)
+		if (cycleTick)
+			bigMapCycleDetailPalette(mac);
+		if (dirty)
+			drawBigMapDetail(state.scrollX, state.scrollY, *state.mapPixels,
+							 state.mapW, state.mapH,
+							 now - state.startTick);
+		g_system->updateScreen();
+		g_system->delayMillis(10);
+	}
+
+	return returnToOverview;
+}
+
+void EEMEngine::doBigMap() {
+	if (!_mystery.isLoaded())
+		return;
+
+	if (isLondon()) {
+		_mystery._pendingSiteJump = 0;
+		_mystery._siteReturnDepth = 0;
+		memset(_mystery._siteReturnStack, 0, sizeof(_mystery._siteReturnStack));
+	}
+
+	CursorMan.showMouse(true);
+
+	while (!shouldQuit()) {
+		BigMapOverviewState overview;
+		if (!bigMapRunOverview(overview))
+			return;
+
+		Common::Array<byte> mapPixels;
+		uint16 mapW = 0;
+		uint16 mapH = 0;
+		if (!bigMapLoadDetailPixels(mapPixels, mapW, mapH))
+			return;
+
+		BigMapDetailState detail;
+		bigMapInitDetailState(detail, mapPixels, mapW, mapH, overview);
+		if (!bigMapRunDetail(detail))
 			return;
 	}
 }


Commit: 4db53b24afaead2456813e9faad3f2ee37cb8fe8
    https://github.com/scummvm/scummvm/commit/4db53b24afaead2456813e9faad3f2ee37cb8fe8
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-26T20:37:27+02:00

Commit Message:
EEM: refactored coordinate handling

Changed paths:
  A engines/eem/coords.cpp
  A engines/eem/coords.h
    engines/eem/eem.h
    engines/eem/map_ui.cpp
    engines/eem/module.mk
    engines/eem/site.cpp
    engines/eem/ui.cpp


diff --git a/engines/eem/coords.cpp b/engines/eem/coords.cpp
new file mode 100644
index 00000000000..38cdccafbbb
--- /dev/null
+++ b/engines/eem/coords.cpp
@@ -0,0 +1,70 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "eem/coords.h"
+#include "eem/eem.h"
+
+namespace EEM {
+
+uint16 readScriptU16(const byte *p, bool bigEndian) {
+	return bigEndian ? READ_BE_UINT16(p) : READ_LE_UINT16(p);
+}
+
+int16 readScriptS16(const byte *p, bool bigEndian) {
+	return (int16)readScriptU16(p, bigEndian);
+}
+
+Common::Rect readRectXYXY(const byte *p, bool bigEndian) {
+	const int16 x1 = readScriptS16(p, bigEndian);
+	const int16 y1 = readScriptS16(p + 2, bigEndian);
+	const int16 x2 = readScriptS16(p + 4, bigEndian);
+	const int16 y2 = readScriptS16(p + 6, bigEndian);
+
+	return Common::Rect(x1, y1, x2, y2);
+}
+
+Common::Rect readDosRectLE(const byte *p) {
+	return readRectXYXY(p, false);
+}
+
+Common::Rect readMacQuickDrawRectBE(const byte *p) {
+	const int16 top = readScriptS16(p, true);
+	const int16 left = readScriptS16(p + 2, true);
+	const int16 bottom = readScriptS16(p + 4, true);
+	const int16 right = readScriptS16(p + 6, true);
+
+	return Common::Rect(left, top, right, bottom);
+}
+
+Common::Rect readMacQuickDrawRectLE(const byte *p) {
+	const int16 top = readScriptS16(p, false);
+	const int16 left = readScriptS16(p + 2, false);
+	const int16 bottom = readScriptS16(p + 4, false);
+	const int16 right = readScriptS16(p + 6, false);
+
+	return Common::Rect(left, top, right, bottom);
+}
+
+Common::Rect scaleDosRectIfMac(const EEMEngine &vm, const Common::Rect &rect) {
+	return vm.scaleRect(rect);
+}
+
+} // End of namespace EEM
diff --git a/engines/eem/coords.h b/engines/eem/coords.h
new file mode 100644
index 00000000000..fb5289c7076
--- /dev/null
+++ b/engines/eem/coords.h
@@ -0,0 +1,49 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef EEM_COORDS_H
+#define EEM_COORDS_H
+
+#include "common/rect.h"
+#include "common/scummsys.h"
+
+namespace EEM {
+
+class EEMEngine;
+
+uint16 readScriptU16(const byte *p, bool bigEndian);
+int16 readScriptS16(const byte *p, bool bigEndian);
+
+/// x1, y1, x2, y2, with the requested word endian.
+Common::Rect readRectXYXY(const byte *p, bool bigEndian);
+
+/// DOS script/site rectangle: x1, y1, x2, y2, little-endian.
+Common::Rect readDosRectLE(const byte *p);
+
+/// Mac QuickDraw rectangle: top, left, bottom, right.
+Common::Rect readMacQuickDrawRectBE(const byte *p);
+Common::Rect readMacQuickDrawRectLE(const byte *p);
+
+Common::Rect scaleDosRectIfMac(const EEMEngine &vm, const Common::Rect &rect);
+
+} // End of namespace EEM
+
+#endif
diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 12b2b1eea2f..1c827f3af39 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -406,7 +406,6 @@ private:
 		uint32 lastCycleTick = 0;
 	};
 
-	Common::Rect bigMapScaledRect(const Common::Rect &rect) const;
 	bool bigMapRunOverview(BigMapOverviewState &state);
 	bool bigMapLoadDetailPixels(Common::Array<byte> &mapPixels,
 								uint16 &mapW, uint16 &mapH);
@@ -684,6 +683,8 @@ public:
 	}
 };
 
+Common::Rect pdaControlRect(const EEMEngine *vm, const Common::Rect &rect);
+
 } // End of namespace EEM
 
 #endif
diff --git a/engines/eem/map_ui.cpp b/engines/eem/map_ui.cpp
index dd9bd6cf11c..b91d437f385 100644
--- a/engines/eem/map_ui.cpp
+++ b/engines/eem/map_ui.cpp
@@ -34,6 +34,7 @@
 #include "video/flic_decoder.h"
 
 #include "eem/animation.h"
+#include "eem/coords.h"
 #include "eem/eem.h"
 #include "eem/music.h"
 #include "eem/site.h"
@@ -78,6 +79,10 @@ const int kMacLondonApproachDoneLeft = 452;
 const int kMacLondonApproachDoneTop = 277;
 const int kMacLondonApproachDoneRight = 504;
 const int kMacLondonApproachDoneBottom = 332;
+// Mac FUN_0000849e maps overview clicks to detail scroll with
+// mouse * 2 - center, clamped to zero before subtracting.
+const int kMacBigMapScrollCenterX = 0xbf;
+const int kMacBigMapScrollCenterY = 0xab;
 
 byte mapVisitedMarkerColor(byte color) {
 	switch (color) {
@@ -236,10 +241,6 @@ static bool openNumberedScriptFile(Common::File &f, Common::String &name,
 	return false;
 }
 
-static uint16 readScriptU16(const byte *p, bool bigEndian) {
-	return bigEndian ? READ_BE_UINT16(p) : READ_LE_UINT16(p);
-}
-
 Common::String cleanLondonApproachPage(const byte *start, uint32 len) {
 	Common::String page((const char *)start, len);
 	while (!page.empty() &&
@@ -289,19 +290,8 @@ bool loadLondonApproachData(uint16 approachId, LondonApproachData &out,
 	out.videoId = readScriptU16(data.data() + 0, macintosh);
 	out.videoX = readScriptU16(data.data() + 2, macintosh);
 	out.videoY = readScriptU16(data.data() + 4, macintosh);
-	if (macintosh) {
-		const int16 top = (int16)readScriptU16(data.data() + 6, true);
-		const int16 left = (int16)readScriptU16(data.data() + 8, true);
-		const int16 bottom = (int16)readScriptU16(data.data() + 10, true);
-		const int16 right = (int16)readScriptU16(data.data() + 12, true);
-		out.textRect = Common::Rect(left, top, right, bottom);
-	} else {
-		const int16 x1 = (int16)readScriptU16(data.data() + 6, false);
-		const int16 y1 = (int16)readScriptU16(data.data() + 8, false);
-		const int16 x2 = (int16)readScriptU16(data.data() + 10, false);
-		const int16 y2 = (int16)readScriptU16(data.data() + 12, false);
-		out.textRect = Common::Rect(x1, y1, x2, y2);
-	}
+	out.textRect = macintosh ? readMacQuickDrawRectBE(data.data() + 6)
+							 : readDosRectLE(data.data() + 6);
 
 	const uint16 pageCount = readScriptU16(data.data() + 14, macintosh);
 	out.pages.clear();
@@ -427,10 +417,6 @@ void remapSurfaceColor(Graphics::ManagedSurface &surface, byte from, byte to) {
 //     20fe:0d2f`). Click icon = travel.
 // MapData entry (14 bytes): +0..3 ???, +4 BigMapX, +6 BigMapY,
 //   +8 SmallMapX, +0xa SmallMapY, +0xc crime-flag.
-Common::Rect EEMEngine::bigMapScaledRect(const Common::Rect &rect) const {
-	return isMacintosh() ? scaleRect(rect) : rect;
-}
-
 void EEMEngine::bigMapCycleOverviewPalette(bool mac) {
 	if (isLondon()) {
 		if (mac) {
@@ -472,8 +458,8 @@ bool EEMEngine::bigMapRunOverview(BigMapOverviewState &state) {
 		? Common::Rect(251, 3, 315, 42)
 		: (isLondon() ? Common::Rect(252, 1, 315, 42)
 					  : Common::Rect(252, 4, 315, 42));
-	const Common::Rect setupBtnRect = bigMapScaledRect(setupBtnBase);
-	state.window = bigMapScaledRect(Common::Rect(0, 0, 247, 192));
+	const Common::Rect setupBtnRect = scaleDosRectIfMac(*this, setupBtnBase);
+	state.window = scaleDosRectIfMac(*this, Common::Rect(0, 0, 247, 192));
 	state.zoomX = 0;
 	state.zoomY = 0;
 
@@ -580,10 +566,12 @@ void EEMEngine::bigMapInitDetailState(BigMapDetailState &state,
 	state.maxScrollY = MAX<int>(0, (int)mapH - state.mapWinH);
 
 	if (mac) {
-		state.scrollX = overview.zoomX * (int)mapW /
-			MAX<int>(1, overview.window.width()) - state.mapWinW / 2;
-		state.scrollY = overview.zoomY * (int)mapH /
-			MAX<int>(1, overview.window.height()) - state.mapWinH / 2;
+		state.scrollX = overview.zoomX * 2;
+		state.scrollY = overview.zoomY * 2;
+		state.scrollX = state.scrollX < kMacBigMapScrollCenterX + 1
+			? 0 : state.scrollX - kMacBigMapScrollCenterX;
+		state.scrollY = state.scrollY < kMacBigMapScrollCenterY + 1
+			? 0 : state.scrollY - kMacBigMapScrollCenterY;
 	} else {
 		state.scrollX = overview.zoomX;
 		state.scrollY = overview.zoomY;
@@ -591,19 +579,19 @@ void EEMEngine::bigMapInitDetailState(BigMapDetailState &state,
 	state.scrollX = MAX<int>(0, MIN<int>(state.maxScrollX, state.scrollX));
 	state.scrollY = MAX<int>(0, MIN<int>(state.maxScrollY, state.scrollY));
 
-	state.returnRect = bigMapScaledRect(
+	state.returnRect = scaleDosRectIfMac(*this,
 		Common::Rect(252, 43, kScreenWidth, kScreenHeight));
-	state.arrowYUp = bigMapScaledRect(Common::Rect(237, 2, 247, 11));
-	state.arrowYDown = bigMapScaledRect(Common::Rect(237, 163, 247, 172));
-	state.arrowXLeft = bigMapScaledRect(Common::Rect(2, 175, 12, 185));
-	state.arrowXRight = bigMapScaledRect(Common::Rect(224, 175, 234, 185));
-	state.xSlider = bigMapScaledRect(isLondon()
+	state.arrowYUp = scaleDosRectIfMac(*this, Common::Rect(237, 2, 247, 11));
+	state.arrowYDown = scaleDosRectIfMac(*this, Common::Rect(237, 163, 247, 172));
+	state.arrowXLeft = scaleDosRectIfMac(*this, Common::Rect(2, 175, 12, 185));
+	state.arrowXRight = scaleDosRectIfMac(*this, Common::Rect(224, 175, 234, 185));
+	state.xSlider = scaleDosRectIfMac(*this, isLondon()
 		? Common::Rect(15, 176, 220, 184)
 		: Common::Rect(15, 175, 221, 185));
-	state.ySlider = bigMapScaledRect(isLondon()
+	state.ySlider = scaleDosRectIfMac(*this, isLondon()
 		? Common::Rect(238, 16, 246, 158)
 		: Common::Rect(237, 14, 247, 160));
-	state.setupRect = bigMapScaledRect(isFloppy()
+	state.setupRect = scaleDosRectIfMac(*this, isFloppy()
 		? Common::Rect(251, 3, 315, 42)
 		: (isLondon() ? Common::Rect(251, 3, 315, 42)
 					  : Common::Rect(252, 4, 315, 42)));
diff --git a/engines/eem/module.mk b/engines/eem/module.mk
index 4b708614703..0ea400f8c61 100644
--- a/engines/eem/module.mk
+++ b/engines/eem/module.mk
@@ -4,6 +4,7 @@ MODULE_OBJS = \
 	animation.o \
 	audio.o \
 	clues.o \
+	coords.o \
 	eem.o \
 	font.o \
 	graphics.o \
diff --git a/engines/eem/site.cpp b/engines/eem/site.cpp
index ec49e304f40..f5db58d401a 100644
--- a/engines/eem/site.cpp
+++ b/engines/eem/site.cpp
@@ -31,6 +31,7 @@
 #include "graphics/paletteman.h"
 
 #include "eem/audio.h"
+#include "eem/coords.h"
 #include "eem/detection.h"
 #include "eem/eem.h"
 #include "eem/mystery.h"
@@ -219,29 +220,14 @@ void blitMacAnimFrameAnchored(Graphics::Surface *dst, const Picture &p,
 }
 
 bool readHotspotRect(const byte *r, bool mac, Common::Rect &rect) {
-	if (mac) {
-		const int16 top = (int16)READ_LE_UINT16(r + 0);
-		const int16 left = (int16)READ_LE_UINT16(r + 2);
-		const int16 bottom = (int16)READ_LE_UINT16(r + 4);
-		const int16 right = (int16)READ_LE_UINT16(r + 6);
-		if (right <= left || bottom <= top)
-			return false;
-		rect = Common::Rect(left, top, right, bottom);
-		return true;
-	}
-
-	const int16 x1 = (int16)READ_LE_UINT16(r + 0);
-	const int16 y1 = (int16)READ_LE_UINT16(r + 2);
-	const int16 x2 = (int16)READ_LE_UINT16(r + 4);
-	const int16 y2 = (int16)READ_LE_UINT16(r + 6);
-	if (x2 <= x1 || y2 <= y1)
+	rect = mac ? readMacQuickDrawRectLE(r) : readDosRectLE(r);
+	if (rect.right <= rect.left || rect.bottom <= rect.top)
 		return false;
-	rect = Common::Rect(x1, y1, x2, y2);
 	return true;
 }
 
 Common::Rect siteControlRect(const EEMEngine *vm, const Common::Rect &rect) {
-	return (vm && vm->isMacintosh()) ? vm->scaleRect(rect) : rect;
+	return pdaControlRect(vm, rect);
 }
 
 // `_ColorCycle @ 172b:2015` — rotate `_fpal[start..end]` by one slot:
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index cf4de4ea63c..37adc6b7d7a 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -33,6 +33,7 @@
 #include "graphics/managed_surface.h"
 
 #include "eem/audio.h"
+#include "eem/coords.h"
 #include "eem/detection.h"
 #include "eem/eem.h"
 #include "eem/music.h"
@@ -69,10 +70,6 @@ static bool openNumberedScriptFile(Common::File &f, Common::String &name,
 	return false;
 }
 
-static uint16 readScriptU16(const byte *p, bool bigEndian) {
-	return bigEndian ? READ_BE_UINT16(p) : READ_LE_UINT16(p);
-}
-
 // Setup-screen highlight rects
 constexpr Common::Rect kSetupKid1Rect    (Common::Point( 99,  44),  49,  8);
 constexpr Common::Rect kSetupKid2Rect    (Common::Point( 99,  54),  49,  8);
@@ -1411,9 +1408,9 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
 	const int sw = screenWidth();
 	const int sh = screenHeight();
 	const Common::Rect endingPrevRect =
-		macEnding ? scaleRect(kEndingPrevPageRect) : kEndingPrevPageRect;
+		scaleDosRectIfMac(*this, kEndingPrevPageRect);
 	const Common::Rect endingNextRect =
-		macEnding ? scaleRect(kEndingNextPageRect) : kEndingNextPageRect;
+		scaleDosRectIfMac(*this, kEndingNextPageRect);
 	uint pageOffsets[8];
 	const uint pageOffsetCap =
 		(uint)(sizeof(pageOffsets) / sizeof(pageOffsets[0]));
@@ -1544,10 +1541,11 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
 					break;
 				const uint16 picNum =
 					readScriptU16(buf.data() + off, macLooseEnding);
-				x1 = readScriptU16(buf.data() + off + 2, macLooseEnding);
-				y1 = readScriptU16(buf.data() + off + 4, macLooseEnding);
-				x2 = readScriptU16(buf.data() + off + 6, macLooseEnding);
-				(void)readScriptU16(buf.data() + off + 8, macLooseEnding);  // y2 (unused — WordWrap2 takes width only)
+				const Common::Rect textRect =
+					readRectXYXY(buf.data() + off + 2, macLooseEnding);
+				x1 = textRect.left;
+				y1 = textRect.top;
+				x2 = textRect.right;
 
 				Picture bg;
 				if (_picsArchive.getPicture(picNum, bg))


Commit: e502b122c9064d2811274316972dcd94cb439ef4
    https://github.com/scummvm/scummvm/commit/e502b122c9064d2811274316972dcd94cb439ef4
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-26T20:37:27+02:00

Commit Message:
EEM: fixed regression in EEM2 DOS intro

Changed paths:
    engines/eem/eem.cpp


diff --git a/engines/eem/eem.cpp b/engines/eem/eem.cpp
index 66c9ea28a74..480ebbc889b 100644
--- a/engines/eem/eem.cpp
+++ b/engines/eem/eem.cpp
@@ -74,6 +74,8 @@ const uint kMacPicTitleLeft2   = 0x216;
 const uint kMacPicTitleRight0  = 0x217;
 const uint kMacPicTitleRight1  = 0x218;
 const uint kMacPicTitleRight2  = 0x219;
+const uint kDosLondonPicHighScoreLogo = 0x356; // FUN_2721_084d
+const uint kDosLondonPalHighScoreLogo = 0x3d;
 const uint kMacPalEAKids       = 0x28;
 const uint kMacPalTitle        = 0x29;
 const uint kPicMousePointer    = 0x50;  // 0x51 is the wait cursor
@@ -1776,15 +1778,17 @@ void EEMEngine::runLondonStartup() {
 	debugC(1, kDebugGeneral, "EEM2 (London): opening sequence");
 
 	// Opening logos. Mac London mirrors FUN_00009256: EA Kids colour-cycle,
-	// publisher still, Storm still, then the FLC intro/title pair.
+	// publisher still, Storm still, then the FLC intro/title pair. DOS London
+	// uses the original ANM sequence after EA Kids instead.
 	if (!shouldQuit() && !_skipIntro)
 		showLondonEAKidsLogo();
-	if (!shouldQuit() && !_skipIntro)
-		showLondonLogo(0x20c, 0x3e, 3000);  // publisher logo (FUN_00009074)
-	if (!shouldQuit() && !_skipIntro)
-		showLondonLogo(0x20b, 0x3d, 3000, /* playThunder= */ true);
 
 	if (isMacintosh()) {
+		if (!shouldQuit() && !_skipIntro)
+			showLondonLogo(0x20c, 0x3e, 3000);  // publisher logo (FUN_00009074)
+		if (!shouldQuit() && !_skipIntro)
+			showLondonLogo(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
 		// full-screen animated title, held for the profile click/key.
@@ -1806,6 +1810,9 @@ void EEMEngine::runLondonStartup() {
 				_audio->stopVoice();
 			fadeCurrentPaletteToBlack();
 		}
+		if (!shouldQuit() && !_skipIntro)
+			showLondonLogo(kDosLondonPicHighScoreLogo,
+						   kDosLondonPalHighScoreLogo, 3000);
 
 		// Intro movie with its theme (MUS00101.XMI).
 		if (!shouldQuit() && !_skipIntro && _music)




More information about the Scummvm-git-logs mailing list