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

neuromancer noreply at scummvm.org
Tue Jun 23 19:52:13 UTC 2026


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

Summary:
7c2094af1f EEM: correct TRAVIS rendering for EEM1 Mac
904afc0b5e EEM: clue resolution in EEM1 Mac
267abe0a5e FREESCAPE: make sound timing more accurate in DOS sounds
c0ecf7cccd FREESCAPE: fix infinite loop in DOS/ZX sound handling


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

Commit Message:
EEM: correct TRAVIS rendering for EEM1 Mac

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


diff --git a/engines/eem/eem.h b/engines/eem/eem.h
index 7c0ae46d08f..c90c52279f0 100644
--- a/engines/eem/eem.h
+++ b/engines/eem/eem.h
@@ -232,6 +232,8 @@ public:
 	/// Returns true if the player committed (SOLVE clicked), false on ESC.
 	bool doAccuseNotes();
 
+	void drawKDBalloonOverCurrentScreen(Common::String text);
+
 	/// `_KDHelp @ 1560:010a` + `_DisplayHint @ 1560:0009`. 
 	void doHelp();
 
diff --git a/engines/eem/graphics.cpp b/engines/eem/graphics.cpp
index 3f616e3c70a..1832c6fcd95 100644
--- a/engines/eem/graphics.cpp
+++ b/engines/eem/graphics.cpp
@@ -638,48 +638,7 @@ void EEMEngine::doHelp() {
 
 	const Common::String raw  = _mystery.textAt(chosenText);
 	Common::String text = parseString(raw, _playerName, _partner);
-	Graphics::ManagedSurface ms(kScreenWidth, kScreenHeight,
-		Graphics::PixelFormat::createFormatCLUT8());
-	ms.clear();
-	{
-		Graphics::Surface *cur = g_system->lockScreen();
-		if (cur) {
-			ms.simpleBlitFrom(*cur);
-			g_system->unlockScreen();
-		}
-	}
-
-	const byte firstChar =
-		text.empty() ? (byte)0 : (byte)text[0];
-	uint16 bubNum = getKDTextBalloon(firstChar);
-	if (firstChar >= '0' && firstChar <= '9')
-		text.deleteChar(0);
-	bubNum = fitBalloonToText(bubNum, text);
-	Picture balloon;
-	const bool haveBalloon =
-		_balloonArchive.size() > (bubNum & 0x7F) &&
-		_balloonArchive.loadEntry(bubNum & 0x7F, balloon);
-
-	const int balloonX = 0x21;
-	int balloonY = 1;
-	if (haveBalloon && balloon.surface.h < 0x4e)
-		balloonY = (0x50 - balloon.surface.h) / 2;
-
-	if (haveBalloon) {
-		const byte transp = (byte)(balloon.flags >> 8);
-		ms.transBlitFrom(balloon.surface,
-						 Common::Point(balloonX, balloonY),
-						 (uint32)transp);
-	}
-
-	uint16 tx = 5, ty = 4, tw = 155;
-	getBalloonInsets(bubNum, tx, ty, tw);
-	_font.drawWordWrapped(&ms, balloonX + tx, balloonY + ty, tw, text,
-						  haveBalloon ? 0 : 0xF);
-
-	g_system->copyRectToScreen(ms.getPixels(), ms.pitch,
-							   0, 0, kScreenWidth, kScreenHeight);
-	g_system->updateScreen();
+	drawKDBalloonOverCurrentScreen(text);
 
 	if (_audio && _mystery.kdTextIndex()) {
 		if (hintVoiceSlot >= 0)
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 2dd491ef025..300adf74dc3 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -49,6 +49,14 @@ const GallerySlot kGallerySlots[5] = {
 	{ 191,  90 }  // 4
 };
 
+const GallerySlot kMacGallerySlots[5] = {
+	{ 133,  27 }, // 0
+	{ 248,  27 }, // 1
+	{ 363,  27 }, // 2
+	{ 190, 173 }, // 3
+	{ 306, 173 }  // 4
+};
+
 struct LondonApproachData {
 	uint16 videoId = 0;
 	uint16 videoX = 0;
@@ -346,23 +354,49 @@ const Picture *partnerFrameFor(DBDArchive &aniArchive, uint8 partner,
 	return &outAni[frameIdx];
 }
 
+int scalePdaAnchor(int value, int target, int source) {
+	const bool negative = value < 0;
+	const int magnitude = negative ? -value : value;
+	const int scaled = (magnitude * target + source / 2) / source;
+	return negative ? -scaled : scaled;
+}
+
 void blitPdaPartner(Graphics::ManagedSurface &dst, DBDArchive &aniArchive,
 					uint8 partner, const PdaPartnerSpec &spec,
-					uint32 tickMs) {
+					uint32 tickMs, bool mac = false) {
 	Animation ani;
 	if (const Picture *fr = partnerFrameFor(aniArchive, partner, spec,
-											tickMs, ani))
-		blitAnimFrameAnchored(dst.surfacePtr(), *fr, spec.anchorX,
-							  spec.anchorY);
+											tickMs, ani)) {
+		if (mac) {
+			const int anchorX =
+				scalePdaAnchor(spec.anchorX, kMacScreenWidth, kScreenWidth);
+			const int anchorY =
+				scalePdaAnchor(spec.anchorY, kMacScreenHeight, kScreenHeight);
+			blitMacAnimFrameAnchored(dst.surfacePtr(), *fr, anchorX,
+									 anchorY);
+		} else {
+			blitAnimFrameAnchored(dst.surfacePtr(), *fr, spec.anchorX,
+								  spec.anchorY);
+		}
+	}
 }
 
 void blitPdaPartner(Graphics::Surface *screen, DBDArchive &aniArchive,
 					uint8 partner, const PdaPartnerSpec &spec,
-					uint32 tickMs) {
+					uint32 tickMs, bool mac = false) {
 	Animation ani;
 	if (const Picture *fr = partnerFrameFor(aniArchive, partner, spec,
-											tickMs, ani))
-		blitAnimFrameAnchored(screen, *fr, spec.anchorX, spec.anchorY);
+											tickMs, ani)) {
+		if (mac) {
+			const int anchorX =
+				scalePdaAnchor(spec.anchorX, kMacScreenWidth, kScreenWidth);
+			const int anchorY =
+				scalePdaAnchor(spec.anchorY, kMacScreenHeight, kScreenHeight);
+			blitMacAnimFrameAnchored(screen, *fr, anchorX, anchorY);
+		} else {
+			blitAnimFrameAnchored(screen, *fr, spec.anchorX, spec.anchorY);
+		}
+	}
 }
 
 constexpr Common::Rect kEndingPrevPageRect(Common::Point(0, 0), 28, kScreenHeight);
@@ -380,6 +414,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.
+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 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 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 uint16 kProfilePickerRevealPic = 0x105;
 constexpr int kProfilePickerRevealX = 0x3e;
 constexpr int kProfilePickerRevealY = 0xb3;
@@ -421,16 +470,46 @@ constexpr uint16 kScrapbookExtraMaxRecords = 16;
 constexpr uint kRestoredContentFirstMystery = 1;
 constexpr uint kRestoredContentLastMystery = 0x18;
 
+Common::Rect pdaControlRect(const EEMEngine *vm, const Common::Rect &rect) {
+	if (!vm || !vm->isMacintosh())
+		return rect;
+	if (rect == kPdaNotebookRect)
+		return kMacPdaNotebookRect;
+	if (rect == kPdaHelpRect)
+		return kMacPdaHelpRect;
+	if (rect == kPdaGalleryRect)
+		return kMacPdaGalleryRect;
+	if (rect == kPdaPartnerHeadHintRect)
+		return kMacPdaPartnerHeadHintRect;
+	if (rect == kPdaAccuseRect)
+		return kMacPdaAccuseRect;
+	if (rect == kPdaPageNextRect)
+		return kMacPdaPageNextRect;
+	if (rect == kPdaPagePrevRect)
+		return kMacPdaPagePrevRect;
+	if (rect == kPdaPartnerFootMapRect)
+		return kMacPdaPartnerFootMapRect;
+	if (rect == kPdaSiteRect)
+		return kMacPdaSiteRect;
+	if (rect == kPdaHelp2Rect)
+		return kMacPdaHelp2Rect;
+	return vm->scaleRect(rect);
+}
+
+bool notebookButtonAt(const EEMEngine *vm, int x, int y) {
+	return pdaControlRect(vm, kPdaHelpRect).contains(x, y) ||
+		   pdaControlRect(vm, kPdaGalleryRect).contains(x, y) ||
+		   pdaControlRect(vm, kPdaPartnerHeadHintRect).contains(x, y) ||
+		   pdaControlRect(vm, kPdaAccuseRect).contains(x, y) ||
+		   pdaControlRect(vm, kPdaPageNextRect).contains(x, 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);
+}
+
 bool notebookButtonAt(int x, int y) {
-	return kPdaHelpRect.contains(x, y) ||
-		   kPdaGalleryRect.contains(x, y) ||
-		   kPdaPartnerHeadHintRect.contains(x, y) ||
-		   kPdaAccuseRect.contains(x, y) ||
-		   kPdaPageNextRect.contains(x, y) ||
-		   kPdaPagePrevRect.contains(x, y) ||
-		   kPdaHelp2Rect.contains(x, y) ||
-		   kPdaPartnerFootMapRect.contains(x, y) ||
-		   kPdaSiteRect.contains(x, y);
+	return notebookButtonAt(nullptr, x, y);
 }
 
 bool galleryButtonAt(int x, int y) {
@@ -442,6 +521,15 @@ bool galleryButtonAt(int x, int y) {
 		   kPdaPartnerHeadHintRect.contains(x, y);
 }
 
+bool galleryButtonAt(const EEMEngine *vm, int x, int y) {
+	return pdaControlRect(vm, kPdaSiteRect).contains(x, y) ||
+		   pdaControlRect(vm, kPdaPartnerFootMapRect).contains(x, y) ||
+		   pdaControlRect(vm, kPdaAccuseRect).contains(x, y) ||
+		   pdaControlRect(vm, kPdaNotebookRect).contains(x, y) ||
+		   pdaControlRect(vm, kPdaHelpRect).contains(x, y) ||
+		   pdaControlRect(vm, kPdaPartnerHeadHintRect).contains(x, y);
+}
+
 bool rectListContains(const Common::Array<Common::Rect> &rects, int x, int y) {
 	for (uint i = 0; i < rects.size(); i++) {
 		if (rects[i].contains(x, y))
@@ -2596,9 +2684,11 @@ void EEMEngine::doNotebook() {
 	if (_music && _voiceOn && notebookFromSite)
 		_music->playMus(30, /* loop= */ false);
 
+	if (isMacintosh())
+		setSitePalette(0);
 	drawNotebookFrame(page);
 	Common::Point mouse = g_system->getEventManager()->getMousePos();
-	setInteractiveMouseCursor(notebookButtonAt(mouse.x, mouse.y));
+	setInteractiveMouseCursor(notebookButtonAt(this, mouse.x, mouse.y));
 
 	if (isLondon() && _music && _voiceOn) {
 		while (notebookFromSite && _music->isPlaying() && !shouldQuit()) {
@@ -2625,7 +2715,7 @@ void EEMEngine::doNotebook() {
 				break;
 			}
 			if (ev.type == Common::EVENT_MOUSEMOVE) {
-				setInteractiveMouseCursor(notebookButtonAt(ev.mouse.x,
+				setInteractiveMouseCursor(notebookButtonAt(this, ev.mouse.x,
 														   ev.mouse.y));
 			}
 			if (ev.type == Common::EVENT_KEYDOWN) {
@@ -2646,35 +2736,43 @@ void EEMEngine::doNotebook() {
 				}
 			}
 			if (ev.type == Common::EVENT_LBUTTONDOWN) {
-				if (kPdaSiteRect.contains(ev.mouse.x, ev.mouse.y) ||
+				if (pdaControlRect(this, kPdaSiteRect).contains(ev.mouse.x,
+																ev.mouse.y) ||
 					(isLondon() &&
 					 kPdaLondonCloseRect.contains(ev.mouse.x, ev.mouse.y))) {
 					_nextScreen = kScreenSite;
 					exitFlag = true;
 					break;  // back to site
 				}
-				if (kPdaPartnerFootMapRect.contains(ev.mouse.x, ev.mouse.y)) {
+				if (pdaControlRect(this, kPdaPartnerFootMapRect)
+						.contains(ev.mouse.x, ev.mouse.y)) {
 					_nextScreen = kScreenMapAlt;
 					exitFlag = true;
 					break;
 				}
-				if (kPdaPartnerHeadHintRect.contains(ev.mouse.x, ev.mouse.y)) {
+				if (pdaControlRect(this, kPdaPartnerHeadHintRect)
+						.contains(ev.mouse.x, ev.mouse.y)) {
 					setInteractiveMouseCursor(false);
 					doHelp();
+					if (isMacintosh())
+						setSitePalette(0);
 					dirty = true;
 					continue;
 				}
-				if (kPdaAccuseRect.contains(ev.mouse.x, ev.mouse.y)) {
+				if (pdaControlRect(this, kPdaAccuseRect).contains(ev.mouse.x,
+																  ev.mouse.y)) {
 					_nextScreen = kScreenAccuse;
 					exitFlag = true;
 					break;
 				}
-				if (kPdaGalleryRect.contains(ev.mouse.x, ev.mouse.y)) {
+				if (pdaControlRect(this, kPdaGalleryRect).contains(ev.mouse.x,
+																   ev.mouse.y)) {
 					_nextScreen = kScreenGallery;
 					exitFlag = true;
 					break;
 				}
-				if (kPdaHelpRect.contains(ev.mouse.x, ev.mouse.y)) {
+				if (pdaControlRect(this, kPdaHelpRect).contains(ev.mouse.x,
+																ev.mouse.y)) {
 					if (isLondon()) {
 						_nextScreen = kScreenMapAlt;
 						exitFlag = true;
@@ -2682,23 +2780,30 @@ void EEMEngine::doNotebook() {
 					}
 					setInteractiveMouseCursor(false);
 					doInterfaceHelp(0);
+					if (isMacintosh())
+						setSitePalette(0);
 					dirty = true;
 					continue;
 				}
-				if (kPdaPagePrevRect.contains(ev.mouse.x, ev.mouse.y)) {
+				if (pdaControlRect(this, kPdaPagePrevRect)
+						.contains(ev.mouse.x, ev.mouse.y)) {
 					if (page > 0)
 						page--;
 					dirty = true;
 					continue;
 				}
-				if (kPdaPageNextRect.contains(ev.mouse.x, ev.mouse.y)) {
+				if (pdaControlRect(this, kPdaPageNextRect)
+						.contains(ev.mouse.x, ev.mouse.y)) {
 					page++;
 					dirty = true;
 					continue;
 				}
-				if (kPdaHelp2Rect.contains(ev.mouse.x, ev.mouse.y)) {
+				if (pdaControlRect(this, kPdaHelp2Rect).contains(ev.mouse.x,
+																 ev.mouse.y)) {
 					setInteractiveMouseCursor(false);
 					doInterfaceHelp(0);
+					if (isMacintosh())
+						setSitePalette(0);
 					dirty = true;
 					continue;
 				}
@@ -2712,7 +2817,8 @@ void EEMEngine::doNotebook() {
 			drawNotebookFrame(page);
 			lastDraw = now;
 			mouse = g_system->getEventManager()->getMousePos();
-			setInteractiveMouseCursor(notebookButtonAt(mouse.x, mouse.y));
+			setInteractiveMouseCursor(notebookButtonAt(this, mouse.x,
+													   mouse.y));
 		}
 		if (now - gizmoLastTick >= kChooserCycleMillis) {
 			gizmoLastTick = now;
@@ -2760,8 +2866,12 @@ Common::String EEMEngine::notebookNoteText(uint clueId, const byte *ni,
 
 void EEMEngine::drawNotebookFrame(int &page) {
 	const Common::Rect kNotebookRect(78, 12, 288, 152);
+	const Common::Rect notebookRect =
+		isMacintosh() ? kMacNotebookTextRect : kNotebookRect;
+	const int sw = screenWidth();
+	const int sh = screenHeight();
 
-	Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+	Graphics::ManagedSurface scratch(sw, sh,
 		Graphics::PixelFormat::createFormatCLUT8());
 	scratch.clear();
 
@@ -2770,7 +2880,7 @@ void EEMEngine::drawNotebookFrame(int &page) {
 		scratch.simpleBlitFrom(frame.surface);
 
 	blitPdaPartner(scratch, _aniArchive, _partner, kPdaNotebookPartner,
-				   g_system->getMillis());
+				   g_system->getMillis(), isMacintosh());
 
 	// `_DrawNotes` walks `_NoteIndex` for current page; word-wraps each
 	// found clue in `_NotebookRect`. Selected = color 0x3c.
@@ -2792,10 +2902,10 @@ void EEMEngine::drawNotebookFrame(int &page) {
 	const uint16 niCount = isLondon()
 		? londonCount : _mystery.noteIndexCount();
 
-	const int kRectX = kNotebookRect.left;
-	const int kRectY = kNotebookRect.top;
-	const int kRectW = kNotebookRect.width();
-	const int kRectH = kNotebookRect.height();
+	const int kRectX = notebookRect.left;
+	const int kRectY = notebookRect.top;
+	const int kRectW = notebookRect.width();
+	const int kRectH = notebookRect.height();
 
 	int clueCursor = 0;
 	Common::Array<int> pageStarts;
@@ -2863,7 +2973,7 @@ void EEMEngine::drawNotebookFrame(int &page) {
 	}
 
 	g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
-							   0, 0, kScreenWidth, kScreenHeight);
+							   0, 0, sw, sh);
 	g_system->updateScreen();
 }
 
@@ -2878,6 +2988,8 @@ void EEMEngine::doGallery() {
 	}
 
 	CursorMan.showMouse(true);
+	if (isMacintosh())
+		setSitePalette(0);
 
 	Picture galBg;
 	const bool haveBg = _picsArchive.getPicture(0x3f, galBg);
@@ -2897,7 +3009,7 @@ void EEMEngine::doGallery() {
 
 	drawGalleryFrame(gd, num, slotRects, slotSuspect);
 	Common::Point mouse = g_system->getEventManager()->getMousePos();
-	setInteractiveMouseCursor(galleryButtonAt(mouse.x, mouse.y) ||
+	setInteractiveMouseCursor(galleryButtonAt(this, mouse.x, mouse.y) ||
 							  gallerySlotAt(slotRects, slotSuspect,
 											mouse.x, mouse.y));
 	uint32 lastDraw = g_system->getMillis();
@@ -2914,7 +3026,7 @@ void EEMEngine::doGallery() {
 				return;
 			}
 			if (ev.type == Common::EVENT_MOUSEMOVE) {
-				setInteractiveMouseCursor(galleryButtonAt(ev.mouse.x,
+				setInteractiveMouseCursor(galleryButtonAt(this, ev.mouse.x,
 														  ev.mouse.y) ||
 										  gallerySlotAt(slotRects,
 														slotSuspect,
@@ -2939,27 +3051,32 @@ void EEMEngine::doGallery() {
 				//   [6] (226,247) → generic exit              (0x638)
 				//   [7] (  7,177) → MAP = NextScreen=2        (0x5f7)
 				//   [8] ( 35,111) → SITE = NextScreen=3       (0x5e4)
-				if (kPdaSiteRect.contains(ev.mouse.x, ev.mouse.y)) {
+				if (pdaControlRect(this, kPdaSiteRect).contains(ev.mouse.x,
+																ev.mouse.y)) {
 					_nextScreen = kScreenSite;
 					exitFlag = true;
 					break;
 				}
-				if (kPdaPartnerFootMapRect.contains(ev.mouse.x, ev.mouse.y)) {
+				if (pdaControlRect(this, kPdaPartnerFootMapRect)
+						.contains(ev.mouse.x, ev.mouse.y)) {
 					_nextScreen = kScreenMapAlt;
 					exitFlag = true;
 					break;
 				}
-				if (kPdaAccuseRect.contains(ev.mouse.x, ev.mouse.y)) {
+				if (pdaControlRect(this, kPdaAccuseRect).contains(ev.mouse.x,
+																  ev.mouse.y)) {
 					_nextScreen = kScreenAccuse;
 					exitFlag = true;
 					break;
 				}
-				if (kPdaNotebookRect.contains(ev.mouse.x, ev.mouse.y)) {
+				if (pdaControlRect(this, kPdaNotebookRect).contains(ev.mouse.x,
+																	ev.mouse.y)) {
 					_nextScreen = kScreenNotebook;
 					exitFlag = true;
 					break;
 				}
-				if (kPdaHelpRect.contains(ev.mouse.x, ev.mouse.y)) {
+				if (pdaControlRect(this, kPdaHelpRect).contains(ev.mouse.x,
+																ev.mouse.y)) {
 					if (isLondon()) {
 						_nextScreen = kScreenMapAlt;
 						exitFlag = true;
@@ -2967,12 +3084,17 @@ void EEMEngine::doGallery() {
 					}
 					setInteractiveMouseCursor(false);
 					doInterfaceHelp(0);
+					if (isMacintosh())
+						setSitePalette(0);
 					lastDraw = 0;
 					continue;
 				}
-				if (kPdaPartnerHeadHintRect.contains(ev.mouse.x, ev.mouse.y)) {
+				if (pdaControlRect(this, kPdaPartnerHeadHintRect)
+						.contains(ev.mouse.x, ev.mouse.y)) {
 					setInteractiveMouseCursor(false);
 					doHelp();
+					if (isMacintosh())
+						setSitePalette(0);
 					lastDraw = 0;
 					continue;
 				}
@@ -2987,7 +3109,7 @@ void EEMEngine::doGallery() {
 						drawGalleryFrame(gd, num, slotRects, slotSuspect);
 						lastDraw = g_system->getMillis();
 						mouse = g_system->getEventManager()->getMousePos();
-						setInteractiveMouseCursor(galleryButtonAt(mouse.x,
+						setInteractiveMouseCursor(galleryButtonAt(this, mouse.x,
 																  mouse.y) ||
 												  gallerySlotAt(slotRects,
 																slotSuspect,
@@ -3008,7 +3130,7 @@ void EEMEngine::doGallery() {
 			drawGalleryFrame(gd, num, slotRects, slotSuspect);
 			lastDraw = now;
 			mouse = g_system->getEventManager()->getMousePos();
-			setInteractiveMouseCursor(galleryButtonAt(mouse.x, mouse.y) ||
+			setInteractiveMouseCursor(galleryButtonAt(this, mouse.x, mouse.y) ||
 									  gallerySlotAt(slotRects, slotSuspect,
 													mouse.x, mouse.y));
 		}
@@ -3025,7 +3147,8 @@ void EEMEngine::doGallery() {
 bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 						  const Picture &galBg, bool haveBg) {
 	const bool floppyMI = isFloppy();
-	const bool compactMI = floppyMI || isMacintosh();
+	const bool mac = isMacintosh();
+	const bool compactMI = floppyMI || mac;
 	const byte *suspect = compactMI
 							  ? _mystery.floppySuspectEntry(suspectIdx)
 							  : gd + suspectIdx * 0x46;
@@ -3038,8 +3161,14 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 
 	setInteractiveMouseCursor(false);
 
-	const int rx = 78, ry = 93;
-	const int rw = 288 - 78, rh = 152 - 93;
+	const Common::Rect noteRectBase(78, 93, 288, 152);
+	const Common::Rect noteRect = mac ? scaleRect(noteRectBase) : noteRectBase;
+	const int rx = noteRect.left;
+	const int ry = noteRect.top;
+	const int rw = noteRect.width();
+	const int rh = noteRect.height();
+	const int sw = screenWidth();
+	const int sh = screenHeight();
 	const int lineH = _font.getFontHeight();
 	const uint clueMax = compactMI ? clueCount : 30u;
 	const byte *ni = _mystery.noteIndex();
@@ -3054,19 +3183,24 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 	bool isFirstShow = true;
 
 	while (!back && !shouldQuit()) {
-		Graphics::ManagedSurface ms(kScreenWidth, kScreenHeight,
+		Graphics::ManagedSurface ms(sw, sh,
 			Graphics::PixelFormat::createFormatCLUT8());
 		ms.clear();
 		if (haveBg)
 			ms.simpleBlitFrom(galBg.surface);
 
 		blitPdaPartner(ms, _aniArchive, _partner, kPdaGalleryPartner,
-					   g_system->getMillis());
+					   g_system->getMillis(), mac);
 		Picture detail;
 		if (_picsArchive.getPicture(detailPic, detail)) {
-			const byte transp = (byte)(detail.flags >> 8);
-			ms.transBlitFrom(detail.surface,
-							 Common::Point(0x94, 0x0f), transp);
+			const Common::Point detailPos =
+				mac ? scalePoint(0x94, 0x0f) : Common::Point(0x94, 0x0f);
+			if (mac)
+				blitMacMaskedSurface(ms.surfacePtr(), detail,
+									 detailPos.x, detailPos.y);
+			else
+				ms.transBlitFrom(detail.surface, detailPos,
+								 (uint32)(byte)(detail.flags >> 8));
 		}
 
 		// Walk clues from pageStart; defer overflow to next page unless
@@ -3135,7 +3269,7 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 				rx, ry + rh + 2, MAX<int>(8, rw), 0x3C);
 		}
 		g_system->copyRectToScreen(ms.getPixels(), ms.pitch,
-			0, 0, kScreenWidth, kScreenHeight);
+			0, 0, sw, sh);
 		g_system->updateScreen();
 
 		// Drain the LBUTTONDOWN that opened MoreInfo (first page only).
@@ -3176,56 +3310,61 @@ bool EEMEngine::moreInfo(const byte *gd, uint suspectIdx,
 					debugC(2, kDebugGfx,
 						"MoreInfo click (%d,%d) hasMore=%d hasPrev=%d",
 						mx, my, (int)hasMore, (int)hasPrev);
-					if (kPdaNotebookRect.contains(mx, my)) {
+					if (pdaControlRect(this, kPdaNotebookRect).contains(mx, my)) {
 						_nextScreen = kScreenNotebook;
 						exitGallery = true;
 						back = true;
 						break;
 					}
-					if (kPdaAccuseRect.contains(mx, my)) {
+					if (pdaControlRect(this, kPdaAccuseRect).contains(mx, my)) {
 						_nextScreen = kScreenAccuse;
 						exitGallery = true;
 						back = true;
 						break;
 					}
-					if (kPdaPartnerFootMapRect.contains(mx, my)) {
+					if (pdaControlRect(this, kPdaPartnerFootMapRect).contains(mx, my)) {
 						_nextScreen = kScreenMapAlt;
 						exitGallery = true;
 						back = true;
 						break;
 					}
-					if (isLondon() && kPdaHelpRect.contains(mx, my)) {
+					if (isLondon() &&
+						pdaControlRect(this, kPdaHelpRect).contains(mx, my)) {
 						_nextScreen = kScreenMapAlt;
 						exitGallery = true;
 						back = true;
 						break;
 					}
-					if (kPdaHelpRect.contains(mx, my) ||
-						kPdaHelp2Rect.contains(mx, my)) {
+					if (pdaControlRect(this, kPdaHelpRect).contains(mx, my) ||
+						pdaControlRect(this, kPdaHelp2Rect).contains(mx, my)) {
 						setInteractiveMouseCursor(false);
 						doInterfaceHelp(0);
+						if (mac)
+							setSitePalette(0);
 						redraw = true;
 						break;
 					}
-					if (kPdaGalleryRect.contains(mx, my)) {
+					if (pdaControlRect(this, kPdaGalleryRect).contains(mx, my)) {
 						// Case 2: close MoreInfo.
 						back = true;
 						break;
 					}
-					if (kPdaPageNextRect.contains(mx, my)) {
+					if (pdaControlRect(this, kPdaPageNextRect).contains(mx, my)) {
 						if (hasMore)
 							advance = true;
 						break;
 					}
-					if (kPdaPagePrevRect.contains(mx, my)) {
+					if (pdaControlRect(this, kPdaPagePrevRect).contains(mx, my)) {
 						if (hasPrev)
 							prev = true;
 						break;
 					}
-					if (kPdaPartnerHeadHintRect.contains(mx, my)) {
+					if (pdaControlRect(this, kPdaPartnerHeadHintRect).contains(mx, my)) {
 						// Case 3: _KDHelp.
 						setInteractiveMouseCursor(false);
 						doHelp();
+						if (mac)
+							setSitePalette(0);
 						redraw = true;
 						break;
 					}
@@ -3276,8 +3415,11 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
 								  Common::Array<int> &slotSuspect) {
 	Picture galBg;
 	const bool haveBg = _picsArchive.getPicture(0x3f, galBg);
+	const bool mac = isMacintosh();
+	const int sw = screenWidth();
+	const int sh = screenHeight();
 
-	Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+	Graphics::ManagedSurface scratch(sw, sh,
 		Graphics::PixelFormat::createFormatCLUT8());
 	scratch.clear();
 
@@ -3285,12 +3427,12 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
 		scratch.simpleBlitFrom(galBg.surface);
 
 	blitPdaPartner(scratch, _aniArchive, _partner, kPdaGalleryPartner,
-				   g_system->getMillis());
+				   g_system->getMillis(), mac);
 
 	const bool floppy = isFloppy();
 	const bool compactGallery = floppy || isMacintosh();
 	const GallerySlot * const slots =
-		floppy ? kFloppyGallerySlots : kGallerySlots;
+		mac ? kMacGallerySlots : (floppy ? kFloppyGallerySlots : kGallerySlots);
 	for (uint i = 0; i < numSuspects && i < Mystery::kGalleryCap; i++) {
 		slotRects[i] = Common::Rect();
 		slotSuspect[i] = -1;
@@ -3314,24 +3456,28 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
 				continue;
 
 			const int placeX = s.x;
-			const int placeY = s.y + (0x48 - portrait.surface.h);
-			const int w = MIN<int>(portrait.surface.w, kScreenWidth - placeX);
-			const int h = MIN<int>(portrait.surface.h, kScreenHeight - placeY);
+			const int placeY = mac ? s.y : s.y + (0x48 - portrait.surface.h);
+			const int w = MIN<int>(portrait.surface.w, sw - placeX);
+			const int h = MIN<int>(portrait.surface.h, sh - placeY);
 			if (w <= 0 || h <= 0)
 				continue;
-			scratch.transBlitFrom(portrait.surface,
-								  Common::Point(placeX, placeY),
-								  (uint32)(byte)(portrait.flags >> 8));
+			if (mac)
+				blitMacMaskedSurface(scratch.surfacePtr(), portrait,
+									 placeX, placeY);
+			else
+				scratch.transBlitFrom(portrait.surface,
+									  Common::Point(placeX, placeY),
+									  (uint32)(byte)(portrait.flags >> 8));
 			slotRects[i] = Common::Rect(placeX, placeY,
 										 placeX + w, placeY + h);
 			slotSuspect[i] = (int)i;
 		} else {
 			// Undiscovered placeholder — small framed "?" box.
-			const int phW = 0x40;
-			const int phH = 0x48;
+			const int phW = mac ? 0x72 : 0x40;
+			const int phH = mac ? 0x90 : 0x48;
 			const int phX = s.x;
 			const int phY = s.y;
-			if (phX + phW <= kScreenWidth && phY + phH <= kScreenHeight) {
+			if (phX + phW <= sw && phY + phH <= sh) {
 				scratch.fillRect(Common::Rect(phX, phY,
 					phX + phW, phY + phH), 0x20);
 				scratch.frameRect(Common::Rect(phX, phY,
@@ -3346,7 +3492,7 @@ void EEMEngine::drawGalleryFrame(const byte *gd, uint8 numSuspects,
 	}
 
 	g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
-							   0, 0, kScreenWidth, kScreenHeight);
+							   0, 0, sw, sh);
 	g_system->updateScreen();
 }
 
@@ -4140,14 +4286,18 @@ void EEMEngine::accuseRebuildPagination(const AccuseNotesCtx &ctx) {
 }
 
 void EEMEngine::accuseDrawScreen(const AccuseNotesCtx &ctx) {
-	Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+	const bool mac = isMacintosh();
+	const int sw = screenWidth();
+	const int sh = screenHeight();
+
+	Graphics::ManagedSurface scratch(sw, sh,
 		Graphics::PixelFormat::createFormatCLUT8());
 	scratch.clear();
 	if (ctx.haveBg)
 		scratch.simpleBlitFrom(ctx.accuseBg->surface);
 
 	blitPdaPartner(scratch, _aniArchive, _partner, kPdaGalleryPartner,
-				   g_system->getMillis());
+				   g_system->getMillis(), mac);
 
 	Common::Array<Common::Rect> &slotRects = *ctx.slotRects;
 	Common::Array<uint> &slotClues = *ctx.slotClues;
@@ -4196,16 +4346,17 @@ void EEMEngine::accuseDrawScreen(const AccuseNotesCtx &ctx) {
 		: (remaining == 1 ? "clue" : "clues");
 	const Common::String counter =
 		Common::String::format("%u %s", remaining, clueWord);
-	_font.drawString(&scratch, counter, 209, 11, 100, 0x0F);
+	_font.drawString(&scratch, counter, scaleX(209), scaleY(11),
+					 scaleX(100), 0x0F);
 
 	if (*ctx.numPages > 1) {
 		_font.drawString(&scratch,
 			Common::String::format("p%d/%d", *ctx.page + 1, *ctx.numPages),
-			ctx.rectX, 11, 60, 0x0F);
+			ctx.rectX, scaleY(11), scaleX(60), 0x0F);
 	}
 
 	g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
-							   0, 0, kScreenWidth, kScreenHeight);
+							   0, 0, sw, sh);
 	g_system->updateScreen();
 }
 
@@ -4242,15 +4393,22 @@ bool EEMEngine::doAccuseNotes() {
 			found.push_back(i);
 	}
 
-	const int rectX = 79;
-	const int rectY = 27;
-	const int rectW = 304 - 79;
-	const int rectH = 159 - 27;
+	const Common::Rect noteRectBase(79, 27, 304, 159);
+	const Common::Rect noteRect =
+		isMacintosh() ? scaleRect(noteRectBase) : noteRectBase;
+	const int rectX = noteRect.left;
+	const int rectY = noteRect.top;
+	const int rectW = noteRect.width();
+	const int rectH = noteRect.height();
 
-	const Common::Rect kBtnSolve   (180, 174, 201, 190); // [4]
-	const Common::Rect kBtnPageNext(204, 174, 224, 190); // [5]
-	const Common::Rect kBtnPagePrev(226, 174, 247, 190); // [6]
-	const Common::Rect kBtnPartner (  5,  80,  44, 110); // [3]
+	const Common::Rect btnSolve =
+		pdaControlRect(this, kPdaAccuseRect); // [4]
+	const Common::Rect btnPageNext =
+		pdaControlRect(this, kPdaPageNextRect); // [5]
+	const Common::Rect btnPagePrev =
+		pdaControlRect(this, kPdaPagePrevRect); // [6]
+	const Common::Rect btnPartner =
+		pdaControlRect(this, kPdaPartnerHeadHintRect); // [3]
 
 	// Per-page slot rects + their clue IDs (for click hit-testing).
 	Common::Array<Common::Rect> slotRects;
@@ -4287,8 +4445,9 @@ bool EEMEngine::doAccuseNotes() {
 	accuseRebuildPagination(ctx);
 	accuseDrawScreen(ctx);
 	Common::Point mouse = g_system->getEventManager()->getMousePos();
-	setInteractiveMouseCursor(notebookButtonAt(mouse.x, mouse.y) ||
-							  kPdaNotebookRect.contains(mouse.x, mouse.y) ||
+	setInteractiveMouseCursor(notebookButtonAt(this, mouse.x, mouse.y) ||
+							  pdaControlRect(this, kPdaNotebookRect)
+								  .contains(mouse.x, mouse.y) ||
 							  rectListContains(slotRects, mouse.x, mouse.y));
 
 	while (!shouldQuit()) {
@@ -4302,8 +4461,9 @@ bool EEMEngine::doAccuseNotes() {
 			}
 			if (ev.type == Common::EVENT_MOUSEMOVE) {
 				setInteractiveMouseCursor(
-					notebookButtonAt(ev.mouse.x, ev.mouse.y) ||
-					kPdaNotebookRect.contains(ev.mouse.x, ev.mouse.y) ||
+					notebookButtonAt(this, ev.mouse.x, ev.mouse.y) ||
+					pdaControlRect(this, kPdaNotebookRect)
+						.contains(ev.mouse.x, ev.mouse.y) ||
 					rectListContains(slotRects, ev.mouse.x, ev.mouse.y));
 			}
 			if (ev.type == Common::EVENT_KEYDOWN) {
@@ -4325,53 +4485,59 @@ bool EEMEngine::doAccuseNotes() {
 			if (ev.type == Common::EVENT_LBUTTONDOWN) {
 				const int mx = ev.mouse.x;
 				const int my = ev.mouse.y;
-				if (kPdaSiteRect.contains(mx, my)) {
+				if (pdaControlRect(this, kPdaSiteRect).contains(mx, my)) {
 					_nextScreen = kScreenSite;
 					return false;
 				}
-				if (kPdaPartnerFootMapRect.contains(mx, my)) {
+				if (pdaControlRect(this, kPdaPartnerFootMapRect)
+						.contains(mx, my)) {
 					_nextScreen = kScreenMapAlt;
 					return false;
 				}
-				if (kPdaNotebookRect.contains(mx, my)) {
+				if (pdaControlRect(this, kPdaNotebookRect).contains(mx, my)) {
 					_nextScreen = kScreenNotebook;
 					return false;
 				}
-				if (kPdaGalleryRect.contains(mx, my)) {
+				if (pdaControlRect(this, kPdaGalleryRect).contains(mx, my)) {
 					_nextScreen = kScreenGallery;
 					return false;
 				}
-				if (isLondon() && kPdaHelpRect.contains(mx, my)) {
+				if (isLondon() &&
+					pdaControlRect(this, kPdaHelpRect).contains(mx, my)) {
 					_nextScreen = kScreenMapAlt;
 					return false;
 				}
-				if (kPdaHelpRect.contains(mx, my) ||
-					kPdaHelp2Rect.contains(mx, my)) {
+				if (pdaControlRect(this, kPdaHelpRect).contains(mx, my) ||
+					pdaControlRect(this, kPdaHelp2Rect).contains(mx, my)) {
 					setInteractiveMouseCursor(false);
 					doInterfaceHelp(0);
+					if (isMacintosh())
+						setSitePalette(0);
 					dirty = true;
 					continue;
 				}
-				if (kBtnPageNext.contains(mx, my)) {
+				if (btnPageNext.contains(mx, my)) {
 					if (page + 1 < numPages) {
 						page++;
 						dirty = true;
 					}
 					continue;
 				}
-				if (kBtnPagePrev.contains(mx, my)) {
+				if (btnPagePrev.contains(mx, my)) {
 					if (page > 0) {
 						page--;
 						dirty = true;
 					}
 					continue;
 				}
-				if (kBtnPartner.contains(mx, my)) {
+				if (btnPartner.contains(mx, my)) {
 					doHelp();
+					if (isMacintosh())
+						setSitePalette(0);
 					dirty = true;
 					continue;
 				}
-				if (kBtnSolve.contains(mx, my)) {
+				if (btnSolve.contains(mx, my)) {
 					uint selected = 0;
 					for (uint i = 0; i < found.size(); i++) {
 						if (_mystery._noteSelected[found[i]])
@@ -4417,6 +4583,67 @@ bool EEMEngine::doAccuseNotes() {
 	return false;
 }
 
+void EEMEngine::drawKDBalloonOverCurrentScreen(Common::String text) {
+	const bool mac = isMacintosh();
+	const int sw = screenWidth();
+	const int sh = screenHeight();
+
+	Graphics::ManagedSurface ms(sw, sh,
+		Graphics::PixelFormat::createFormatCLUT8());
+	ms.clear();
+	Graphics::Surface *cur = g_system->lockScreen();
+	if (cur) {
+		ms.simpleBlitFrom(*cur);
+		g_system->unlockScreen();
+	}
+
+	const byte firstChar = text.empty() ? (byte)0 : (byte)text[0];
+	uint16 bubNum = getKDTextBalloon(firstChar);
+	if (firstChar >= '0' && firstChar <= '9')
+		text.deleteChar(0);
+	bubNum = fitBalloonToText(bubNum, text);
+
+	Picture balloon;
+	const bool haveBalloon =
+		_balloonArchive.size() > (bubNum & 0x7F) &&
+		_balloonArchive.loadEntry(bubNum & 0x7F, balloon);
+
+	const int balloonX = mac ? scaleX(0x21) : 0x21;
+	const int topBandH = mac ? scaleY(0x50) : 0x50;
+	const int centeredMaxH = mac ? scaleY(0x4e) : 0x4e;
+	int balloonY = mac ? scaleY(1) : 1;
+	if (haveBalloon && balloon.surface.h < centeredMaxH)
+		balloonY = (topBandH - balloon.surface.h) / 2;
+
+	MacSpritePaletteMap macPaletteMap = {0x00, 0xFF};
+	if (mac)
+		macPaletteMap = getMacSpritePaletteMap();
+
+	if (haveBalloon) {
+		if (mac) {
+			blitMacMaskedSurface(ms.surfacePtr(), balloon, balloonX,
+								 balloonY, false, macPaletteMap);
+		} else {
+			ms.transBlitFrom(balloon.surface,
+							 Common::Point(balloonX, balloonY),
+							 (uint32)(byte)(balloon.flags >> 8));
+		}
+	}
+
+	uint16 tx = 5, ty = 4, tw = 155;
+	getBalloonInsets(bubNum, tx, ty, tw);
+	const EEMFont &dialogFont =
+		(mac && _dialogFont.isLoaded()) ? _dialogFont : _font;
+	if (dialogFont.isLoaded()) {
+		const byte textColor =
+			mac ? macPaletteMap.black : (haveBalloon ? 0 : 0xF);
+		dialogFont.drawWordWrapped(&ms, balloonX + tx, balloonY + ty,
+								   tw, text, textColor);
+	}
+
+	copyToScreen(ms);
+}
+
 void EEMEngine::doAccuse() {
 	if (!_mystery.isLoaded() || !_font.isLoaded())
 		return;
@@ -4487,42 +4714,7 @@ void EEMEngine::doAccuse() {
 								_playerName, _partner);
 	}
 	if (!entryText.empty()) {
-		Graphics::ManagedSurface ms(kScreenWidth, kScreenHeight,
-			Graphics::PixelFormat::createFormatCLUT8());
-		ms.clear();
-		Graphics::Surface *cur = g_system->lockScreen();
-		if (cur) {
-			ms.simpleBlitFrom(*cur);
-			g_system->unlockScreen();
-		}
-		const byte firstChar = (byte)entryText[0];
-		uint16 bubNum = getKDTextBalloon(firstChar);
-		if (firstChar >= '0' && firstChar <= '9')
-			entryText.deleteChar(0);
-		bubNum = fitBalloonToText(bubNum, entryText);
-		Picture balloon;
-		const bool haveBalloon =
-			_balloonArchive.size() > (bubNum & 0x7F) &&
-			_balloonArchive.loadEntry(bubNum & 0x7F, balloon);
-		const int balloonX = 0x21;
-		int balloonY = 1;
-		if (haveBalloon && balloon.surface.h < 0x4e)
-			balloonY = (0x50 - balloon.surface.h) / 2;
-		if (haveBalloon) {
-			const byte transp = (byte)(balloon.flags >> 8);
-			ms.transBlitFrom(balloon.surface,
-							 Common::Point(balloonX, balloonY),
-							 (uint32)transp);
-		}
-		uint16 tx = 5, ty = 4, tw = 155;
-		getBalloonInsets(bubNum, tx, ty, tw);
-		if (_font.isLoaded()) {
-			_font.drawWordWrapped(&ms, balloonX + tx, balloonY + ty,
-								  tw, entryText, haveBalloon ? 0 : 0xF);
-		}
-		g_system->copyRectToScreen(ms.getPixels(), ms.pitch,
-								   0, 0, kScreenWidth, kScreenHeight);
-		g_system->updateScreen();
+		drawKDBalloonOverCurrentScreen(entryText);
 		if (_audio) {
 			if (entryVoiceOverride != 0xFFFF)
 				_audio->spoolSound(entryVoiceOverride);
@@ -4597,46 +4789,7 @@ void EEMEngine::doAccuse() {
 			}
 		}
 
-		Graphics::ManagedSurface ms(kScreenWidth, kScreenHeight,
-			Graphics::PixelFormat::createFormatCLUT8());
-		ms.clear();
-		Graphics::Surface *cur = g_system->lockScreen();
-		if (cur) {
-			ms.simpleBlitFrom(*cur);
-			g_system->unlockScreen();
-		}
-		const byte firstChar =
-			hint.empty() ? (byte)0 : (byte)hint[0];
-		uint16 bubNum = getKDTextBalloon(firstChar);
-
-		if (firstChar >= '0' && firstChar <= '9')
-			hint.deleteChar(0);
-		bubNum = fitBalloonToText(bubNum, hint);
-		Picture balloon;
-		const bool haveBalloon =
-			_balloonArchive.size() > (bubNum & 0x7F) &&
-			_balloonArchive.loadEntry(bubNum & 0x7F, balloon);
-		const int balloonX = 0x21;
-		int balloonY = 1;
-		if (haveBalloon && balloon.surface.h < 0x4e)
-			balloonY = (0x50 - balloon.surface.h) / 2;
-		if (haveBalloon) {
-			const byte transp = (byte)(balloon.flags >> 8);
-			ms.transBlitFrom(balloon.surface,
-							 Common::Point(balloonX, balloonY),
-							 (uint32)transp);
-		}
-		uint16 tx = 5, ty = 4, tw = 155;
-		getBalloonInsets(bubNum, tx, ty, tw);
-		if (_font.isLoaded()) {
-			_font.drawWordWrapped(&ms, balloonX + tx,
-								  balloonY + ty, tw, hint,
-								  haveBalloon ? 0 : 0xF);
-		}
-		g_system->copyRectToScreen(ms.getPixels(), ms.pitch,
-								   0, 0, kScreenWidth, kScreenHeight);
-		g_system->updateScreen();
-
+		drawKDBalloonOverCurrentScreen(hint);
 
 		if (_audio && kdIdx)
 			_audio->sayKDDigital(kdIdx, 3, _partner);
@@ -4647,9 +4800,6 @@ void EEMEngine::doAccuse() {
 		return;
 	}
 
-	Picture accuseBg;
-	const bool haveAccuseBg = _picsArchive.getPicture(0x3f, accuseBg);
-
 	Common::Array<Common::Rect> slotRects;
 	Common::Array<int> slotSuspect;
 	slotRects.resize(num);
@@ -4667,58 +4817,10 @@ void EEMEngine::doAccuse() {
 			Common::String hint =
 				parseString(raw ? raw : "", _playerName, _partner);
 			if (!hint.empty()) {
-				const byte firstChar =
-					hint.empty() ? (byte)0 : (byte)hint[0];
-				uint16 bubNum = getKDTextBalloon(firstChar);
-				// Strip digit prefix (`_DisplayAlibi @ 1df2:0163`).
-				if (firstChar >= '0' && firstChar <= '9')
-					hint.deleteChar(0);
-				bubNum = fitBalloonToText(bubNum, hint);
-				Picture balloon;
-				const bool haveBalloon =
-					_balloonArchive.size() > (bubNum & 0x7F) &&
-					_balloonArchive.loadEntry(bubNum & 0x7F, balloon);
-
-				const int balloonX = 0x21;
-				int balloonY = 1;
-				if (haveBalloon && balloon.surface.h < 0x4e)
-					balloonY = (0x50 - balloon.surface.h) / 2;
-
 				// Render gallery first so the snapshot includes partner.
 				drawAccuseGallery(num, gd, /* highlighted= */ -1,
 								  slotRects, slotSuspect);
-
-				Graphics::ManagedSurface ms(kScreenWidth, kScreenHeight,
-					Graphics::PixelFormat::createFormatCLUT8());
-				ms.clear();
-				{
-					Graphics::Surface *cur = g_system->lockScreen();
-					if (cur) {
-						ms.simpleBlitFrom(*cur);
-						g_system->unlockScreen();
-					} else if (haveAccuseBg) {
-						// Fallback if lockScreen failed.
-						ms.simpleBlitFrom(accuseBg.surface);
-					}
-				}
-				if (haveBalloon) {
-					const byte transp = (byte)(balloon.flags >> 8);
-					ms.transBlitFrom(balloon.surface,
-									 Common::Point(balloonX, balloonY),
-									 transp);
-				}
-				uint16 tx = 5;
-				uint16 ty = 4;
-				uint16 tw = 155;
-				getBalloonInsets(bubNum, tx, ty, tw);
-				if (_font.isLoaded()) {
-					_font.drawWordWrapped(&ms, balloonX + tx,
-										  balloonY + ty, tw, hint,
-										  haveBalloon ? 0 : 0xF);
-				}
-				g_system->copyRectToScreen(ms.getPixels(), ms.pitch,
-					0, 0, kScreenWidth, kScreenHeight);
-				g_system->updateScreen();
+				drawKDBalloonOverCurrentScreen(hint);
 				if (_audio)
 					_audio->sayKDDigital(kdIdx, 4, _partner);
 				waitForInput(8000);
@@ -5678,8 +5780,11 @@ void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
 								   Common::Array<int> &slotSuspect) {
 	Picture accuseBg;
 	const bool haveAccuseBg = _picsArchive.getPicture(0x3f, accuseBg);
+	const bool mac = isMacintosh();
+	const int sw = screenWidth();
+	const int sh = screenHeight();
 
-	Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+	Graphics::ManagedSurface scratch(sw, sh,
 		Graphics::PixelFormat::createFormatCLUT8());
 	scratch.clear();
 	if (haveAccuseBg)
@@ -5687,7 +5792,8 @@ void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
 
 	// Partner drawn first; defensive (no slot overlap).
 	blitPdaPartner(scratch, _aniArchive, _partner, kPdaGalleryPartner,
-				   g_system->getMillis());
+				   g_system->getMillis(), mac);
+	const GallerySlot * const slots = mac ? kMacGallerySlots : kGallerySlots;
 
 	for (uint i = 0; i < numSuspects && i < Mystery::kGalleryCap; i++) {
 		slotRects[i] = Common::Rect();
@@ -5699,7 +5805,7 @@ void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
 			continue;
 		if (_mystery._inGallery[phys] == 0)
 			continue;
-		const GallerySlot &s = kGallerySlots[phys];
+		const GallerySlot &s = slots[phys];
 
 		const byte *entry = isMacintosh()
 			? _mystery.floppySuspectEntry(i)
@@ -5714,14 +5820,18 @@ void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
 			continue;
 
 		const int placeX = s.x;
-		const int placeY = s.y + (0x48 - portrait.surface.h);
-		const int w = MIN<int>(portrait.surface.w, kScreenWidth - placeX);
-		const int h = MIN<int>(portrait.surface.h, kScreenHeight - placeY);
+		const int placeY = mac ? s.y : s.y + (0x48 - portrait.surface.h);
+		const int w = MIN<int>(portrait.surface.w, sw - placeX);
+		const int h = MIN<int>(portrait.surface.h, sh - placeY);
 		if (w <= 0 || h <= 0)
 			continue;
-		scratch.transBlitFrom(portrait.surface,
-							  Common::Point(placeX, placeY),
-							  (uint32)(byte)(portrait.flags >> 8));
+		if (mac)
+			blitMacMaskedSurface(scratch.surfacePtr(), portrait,
+								 placeX, placeY);
+		else
+			scratch.transBlitFrom(portrait.surface,
+								  Common::Point(placeX, placeY),
+								  (uint32)(byte)(portrait.flags >> 8));
 		slotRects[i] = Common::Rect(placeX, placeY,
 									 placeX + w, placeY + h);
 		slotSuspect[i] = (int)i;
@@ -5736,7 +5846,7 @@ void EEMEngine::drawAccuseGallery(uint8 numSuspects, const byte *gd,
 	}
 
 	g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
-							   0, 0, kScreenWidth, kScreenHeight);
+							   0, 0, sw, sh);
 	g_system->updateScreen();
 }
 


Commit: 904afc0b5e28dfe4b65a8d2dda652aaedbf5738b
    https://github.com/scummvm/scummvm/commit/904afc0b5e28dfe4b65a8d2dda652aaedbf5738b
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-23T21:52:01+02:00

Commit Message:
EEM: clue resolution in EEM1 Mac

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


diff --git a/engines/eem/mystery.cpp b/engines/eem/mystery.cpp
index d8dbd96a2eb..b6e5994613c 100644
--- a/engines/eem/mystery.cpp
+++ b/engines/eem/mystery.cpp
@@ -189,7 +189,10 @@ static void normalizeMacMystery(Common::Array<byte> &data) {
 	swapU16Range(data, section[4], section[7]); // notebook index
 	normalizeMacGalleryData(data, section[5], section[6]);
 	swapU16Range(data, section[8], MIN<uint>(section[8] + 12, section[9]));
-	swapU16Range(data, section[9], data.size()); // solved clue block
+
+	// Solved outro block: u8 count followed by Mac-native dialog records.
+	// Keep it byte-exact; displayFloppyDialogRecords() parses widened fields
+	// as big-endian for Mac.
 }
 
 uint16 Mystery::readU16(uint offset) const {
diff --git a/engines/eem/ui.cpp b/engines/eem/ui.cpp
index 300adf74dc3..27a88aa3691 100644
--- a/engines/eem/ui.cpp
+++ b/engines/eem/ui.cpp
@@ -399,11 +399,73 @@ void blitPdaPartner(Graphics::Surface *screen, DBDArchive &aniArchive,
 	}
 }
 
+bool playMacScrapbookPartnerAnimation(EEMEngine *vm,
+									  Graphics::ManagedSurface &base) {
+	if (!vm || !vm->isMacintosh())
+		return false;
+
+	g_system->copyRectToScreen(base.getPixels(), base.pitch, 0, 0,
+							   base.w, base.h);
+	g_system->updateScreen();
+
+	const uint animId =
+		(vm->getPartnerIndex() == kPartnerJake) ? 0x17 : 0x3b;
+	const int anchorX =
+		(vm->getPartnerIndex() == kPartnerJake) ? 0x144 : 0x146;
+	const int anchorY =
+		(vm->getPartnerIndex() == kPartnerJake) ? 0x0d8 : 0x0d6;
+	Animation anim;
+	if (!vm->getAni().loadAnimation(animId, anim) || anim.empty())
+		return false;
+
+	const uint kFrameDelayMs = 140;
+	bool skip = false;
+	for (uint frame = 0; frame < anim.size() && !vm->shouldQuit() && !skip;
+		 frame++) {
+		Graphics::ManagedSurface scratch(base.w, base.h,
+			Graphics::PixelFormat::createFormatCLUT8());
+		scratch.simpleBlitFrom(*base.surfacePtr());
+
+		// The Mac executable drives these endgame partner cells with the
+		// same script used by the case-intro desk animation.
+		const uint cell = partnerFrameAtTick(0x17, (uint)anim.size(),
+											 frame * kFrameDelayMs);
+		blitMacAnimFrameAnchored(scratch.surfacePtr(), anim[cell],
+								  anchorX, anchorY);
+		g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+								   0, 0, scratch.w, scratch.h);
+		g_system->updateScreen();
+
+		const uint32 wakeup = g_system->getMillis() + kFrameDelayMs;
+		while (g_system->getMillis() < wakeup && !vm->shouldQuit() &&
+			   !skip) {
+			Common::Event ev;
+			while (g_system->getEventManager()->pollEvent(ev)) {
+				if (ev.type == Common::EVENT_QUIT ||
+					ev.type == Common::EVENT_RETURN_TO_LAUNCHER)
+					return true;
+				if (ev.type == Common::EVENT_KEYDOWN ||
+					ev.type == Common::EVENT_LBUTTONDOWN) {
+					skip = true;
+					break;
+				}
+			}
+			g_system->updateScreen();
+			g_system->delayMillis(10);
+		}
+	}
+
+	return true;
+}
+
 constexpr Common::Rect kEndingPrevPageRect(Common::Point(0, 0), 28, kScreenHeight);
 constexpr Common::Rect kEndingNextPageRect(Common::Point(292, 0), 28, kScreenHeight);
 constexpr uint16 kFloppyEndingBackgroundPic = 0x8b;
 constexpr uint16 kFirstTryBadgePic = 0x205;
 constexpr Common::Point kFirstTryBadgePos(0x1e, 9);
+constexpr uint kMacMysteryDataTableOffset = 0x08cd;
+constexpr uint kMacMysteryDataMysteryCount = 55;
+constexpr uint kMacMysteryDataEndingCount = 55;
 
 constexpr Common::Rect kPdaHelpRect(Common::Point(93, 174), 22, 16);
 constexpr Common::Rect kPdaNotebookRect(Common::Point(134, 174), 21, 16);
@@ -568,11 +630,15 @@ bool gallerySlotAt(const Common::Array<Common::Rect> &rects,
 }
 
 const byte *advanceFloppyDialogRecords(const byte *rec, uint count,
-									   const byte *end) {
+									   const byte *end, bool mac = false) {
+	if (!rec || (!end && count != 0))
+		return nullptr;
+	const uint headerLen = mac ? 14 : 11;
+	const uint textCountOffset = mac ? 13 : 10;
 	for (uint i = 0; i < count; i++) {
-		if (!rec || rec + 11 > end)
+		if (rec + headerLen > end)
 			return nullptr;
-		const uint len = 11u + rec[10];
+		const uint len = headerLen + rec[textCountOffset];
 		if (rec + len > end)
 			return nullptr;
 		rec += len;
@@ -617,6 +683,45 @@ void copyToScreen(Graphics::ManagedSurface &scratch) {
 	g_system->updateScreen();
 }
 
+bool loadMacEndingBlob(uint num, Common::Array<byte> &out) {
+	if (num >= kMacMysteryDataEndingCount)
+		return false;
+
+	Common::File f;
+	if (!f.open(Common::Path("MysteryData"))) {
+		warning("doShowEnding: cannot open MysteryData");
+		return false;
+	}
+
+	const uint entry = kMacMysteryDataMysteryCount + num;
+	const uint32 tableOff = kMacMysteryDataTableOffset + entry * 8;
+	if (tableOff + 8 > (uint32)f.size()) {
+		warning("doShowEnding: MysteryData ending index %u out of range",
+				num);
+		return false;
+	}
+
+	f.seek(tableOff);
+	const uint32 offset = f.readUint32BE();
+	const uint32 size = f.readUint32BE();
+	if (size < 16 || offset > (uint32)f.size() ||
+		size > (uint32)f.size() - offset) {
+		warning("doShowEnding: MysteryData ending %u is invalid "
+				"(off=0x%08x size=0x%08x)", num, offset, size);
+		return false;
+	}
+
+	out.resize(size);
+	f.seek(offset);
+	if (f.read(out.data(), size) != size) {
+		warning("doShowEnding: short read on MysteryData ending %u", num);
+		out.clear();
+		return false;
+	}
+
+	return true;
+}
+
 void cycleChooserPalette() {
 	cyclePaletteRange(kChooserCycleStart, kChooserCycleEnd);
 }
@@ -1407,44 +1512,60 @@ void EEMEngine::doNewPlayer() {
 }
 // `_DisplayEnding @ 1df2:0548` + `_DisplayEndingPage @ 1df2:044c`
 int EEMEngine::doShowEnding(uint num, bool firstPage) {
-	const Common::String fname = Common::String::format("E%u.BIN", num);
-	Common::File f;
-	if (!f.open(Common::Path(fname))) {
-		warning("doShowEnding: %s missing", fname.c_str());
-		return 0;
-	}
-	const uint32 size = f.size();
-	if (size < 2) {
-		warning("doShowEnding: %s too small (%u bytes)",
-				fname.c_str(), size);
-		return 0;
-	}
-	Common::Array<byte> buf(size);
-	if (f.read(buf.data(), size) != size) {
-		warning("doShowEnding: %s short read", fname.c_str());
-		return 0;
+	Common::Array<byte> buf;
+	uint32 size = 0;
+	const bool macEnding = isMacintosh();
+
+	if (macEnding) {
+		if (!loadMacEndingBlob(num, buf))
+			return 0;
+		size = (uint32)buf.size();
+	} else {
+		const Common::String fname = Common::String::format("E%u.BIN", num);
+		Common::File f;
+		if (!f.open(Common::Path(fname))) {
+			warning("doShowEnding: %s missing", fname.c_str());
+			return 0;
+		}
+		size = f.size();
+		if (size < 2) {
+			warning("doShowEnding: %s too small (%u bytes)",
+					fname.c_str(), size);
+			return 0;
+		}
+		buf.resize(size);
+		if (f.read(buf.data(), size) != size) {
+			warning("doShowEnding: %s short read", fname.c_str());
+			return 0;
+		}
 	}
 
 	EEMFont tinyFont;
-	const bool haveTinyFont = tinyFont.load(Common::Path("TINY.FNT"));
-	if (!haveTinyFont)
+	const bool haveTinyFont =
+		!macEnding && tinyFont.load(Common::Path("TINY.FNT"));
+	if (!macEnding && !haveTinyFont)
 		warning("doShowEnding: TINY.FNT failed to load — falling back");
 
 	setSitePalette(0);
 	CursorMan.showMouse(true);
 
 	const bool floppyEnding = isFloppy();
+	const int sw = screenWidth();
+	const int sh = screenHeight();
+	const Common::Rect endingPrevRect =
+		macEnding ? scaleRect(kEndingPrevPageRect) : kEndingPrevPageRect;
+	const Common::Rect endingNextRect =
+		macEnding ? scaleRect(kEndingNextPageRect) : kEndingNextPageRect;
 	uint pageOffsets[8];
 	const uint pageOffsetCap =
 		(uint)(sizeof(pageOffsets) / sizeof(pageOffsets[0]));
 	uint validPages = 0;
 
-	if (floppyEnding) {
-		// Floppy `E<num>.BIN` starts with:
+	if (floppyEnding || macEnding) {
+		// Floppy `E<num>.BIN` and Mac `MysteryData[55 + num]` start with:
 		//   u8 type, 3 bytes of title metadata, char title[], u8 pageCount
-		// followed by pages:
-		//   u8 overlayCount, N * { u16 picNum, u16 x, u8 y },
-		//   u16 x1, y1, x2, y2, char text[].
+		// followed by pages. Mac stores overlay pic/x words big-endian and
+		// native-coordinate text rects in little-endian order.
 		uint titleEnd = 4;
 		while (titleEnd < size && buf[titleEnd] != 0)
 			titleEnd++;
@@ -1502,10 +1623,10 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
 	bool exitLoop = false;
 	bool dirty = true;
 	const Common::Point mousePos = g_system->getEventManager()->getMousePos();
-	setInteractiveMouseCursor(kEndingPrevPageRect.contains(mousePos.x,
-														   mousePos.y) ||
-							  kEndingNextPageRect.contains(mousePos.x,
-														   mousePos.y));
+	setInteractiveMouseCursor(endingPrevRect.contains(mousePos.x,
+													  mousePos.y) ||
+							  endingNextRect.contains(mousePos.x,
+													  mousePos.y));
 	while (!shouldQuit() && !exitLoop) {
 		if (dirty) {
 			const uint off = pageOffsets[pageIdx];
@@ -1513,13 +1634,14 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
 			uint16 y1 = 0;
 			uint16 x2 = 0;
 			const char *raw = nullptr;
-			Graphics::ManagedSurface scratch(kScreenWidth, kScreenHeight,
+			Graphics::ManagedSurface scratch(sw, sh,
 				Graphics::PixelFormat::createFormatCLUT8());
 			scratch.clear();
 
-			if (floppyEnding) {
+			if (floppyEnding || macEnding) {
 				Picture bg;
-				if (_picsArchive.getPicture(kFloppyEndingBackgroundPic, bg))
+				if (floppyEnding &&
+					_picsArchive.getPicture(kFloppyEndingBackgroundPic, bg))
 					scratch.simpleBlitFrom(bg.surface);
 
 				uint cursor = off;
@@ -1529,14 +1651,22 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
 				for (uint i = 0; i < overlayCount; i++) {
 					if (cursor + 5 > size)
 						break;
-					const uint16 picNum = READ_LE_UINT16(buf.data() + cursor);
-					const uint16 px = READ_LE_UINT16(buf.data() + cursor + 2);
+					const uint16 picNum = macEnding
+						? READ_BE_UINT16(buf.data() + cursor)
+						: READ_LE_UINT16(buf.data() + cursor);
+					const uint16 px = macEnding
+						? READ_BE_UINT16(buf.data() + cursor + 2)
+						: READ_LE_UINT16(buf.data() + cursor + 2);
 					const byte py = buf[cursor + 4];
 					Picture overlay;
 					if (_picsArchive.getPicture(picNum, overlay)) {
-						const byte transp = (byte)(overlay.flags >> 8);
-						scratch.transBlitFrom(overlay.surface,
-											  Common::Point(px, py), transp);
+						if (macEnding)
+							blitMacMaskedSurface(scratch.surfacePtr(),
+												 overlay, px, py);
+						else
+							scratch.transBlitFrom(overlay.surface,
+								Common::Point(px, py),
+								(uint32)(byte)(overlay.flags >> 8));
 					}
 					cursor += 5;
 				}
@@ -1547,6 +1677,8 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
 				x2 = READ_LE_UINT16(buf.data() + cursor + 4);
 				(void)READ_LE_UINT16(buf.data() + cursor + 6);
 				raw = (const char *)buf.data() + cursor + 8;
+				if (macEnding && (byte)*raw == 0xd9)
+					raw++;
 			} else {
 				if (off + 10 >= size)
 					break;
@@ -1572,13 +1704,18 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
 
 			const EEMFont &renderFont = haveTinyFont ? tinyFont : _font;
 			if (renderFont.isLoaded() && x2 > x1) {
-				const int textW = MIN<int>((int)x2 - (int)x1, kScreenWidth - (int)x1);
+				const int textW = MIN<int>((int)x2 - (int)x1,
+										   sw - (int)x1);
+				MacSpritePaletteMap macPaletteMap = {0x00, 0xFF};
+				if (macEnding)
+					macPaletteMap = getMacSpritePaletteMap();
 				renderFont.drawWordWrapped(&scratch, (int)x1, (int)y1,
-										   textW, text, 0);
+										   textW, text,
+										   macEnding ? macPaletteMap.black : 0);
 			}
 
 			g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
-									   0, 0, kScreenWidth, kScreenHeight);
+									   0, 0, sw, sh);
 			g_system->updateScreen();
 			dirty = false;
 		}
@@ -1593,8 +1730,8 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
 			}
 			if (ev.type == Common::EVENT_MOUSEMOVE)
 				setInteractiveMouseCursor(
-					kEndingPrevPageRect.contains(ev.mouse.x, ev.mouse.y) ||
-					kEndingNextPageRect.contains(ev.mouse.x, ev.mouse.y));
+					endingPrevRect.contains(ev.mouse.x, ev.mouse.y) ||
+					endingNextRect.contains(ev.mouse.x, ev.mouse.y));
 			if (ev.type == Common::EVENT_KEYDOWN) {
 				switch (ev.kbd.keycode) {
 				case Common::KEYCODE_ESCAPE:
@@ -1633,9 +1770,9 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
 			}
 			if (ev.type == Common::EVENT_LBUTTONDOWN) {
 				setInteractiveMouseCursor(
-					kEndingPrevPageRect.contains(ev.mouse.x, ev.mouse.y) ||
-					kEndingNextPageRect.contains(ev.mouse.x, ev.mouse.y));
-				if (kEndingPrevPageRect.contains(ev.mouse.x, ev.mouse.y)) {
+					endingPrevRect.contains(ev.mouse.x, ev.mouse.y) ||
+					endingNextRect.contains(ev.mouse.x, ev.mouse.y));
+				if (endingPrevRect.contains(ev.mouse.x, ev.mouse.y)) {
 					if (pageIdx > 0) {
 						pageIdx--;
 						dirty = true;
@@ -1643,7 +1780,7 @@ int EEMEngine::doShowEnding(uint num, bool firstPage) {
 						direction = -1;
 						exitLoop = true;
 					}
-				} else if (kEndingNextPageRect.contains(ev.mouse.x, ev.mouse.y)) {
+				} else if (endingNextRect.contains(ev.mouse.x, ev.mouse.y)) {
 					if (pageIdx + 1 < validPages) {
 						pageIdx++;
 						dirty = true;
@@ -5116,53 +5253,76 @@ void EEMEngine::doAccuse() {
 
 		advanceChainStageAfterSolve(mn);
 
-		Graphics::Surface *blk = g_system->lockScreen();
-		if (blk) {
-			memset(blk->getPixels(), 0, kScreenWidth * kScreenHeight);
-			g_system->unlockScreen();
-		}
 		// `_DisplayCorrect` win background = `_BuildBackground(scene, 0x42, 0x14)`
 		// (frame PIC 0x3d + scene at 0x42,0x14, palette scene+1). EEM1 CD uses
 		// scene 5; EEM2/London uses scene 0x1b.
 		const uint winScene = isLondon() ? 0x1b : 5;
 		setSitePalette(winScene + 1);
+		const int screenW = screenWidth();
+		const int screenH = screenHeight();
+		Graphics::ManagedSurface scratch(screenW, screenH,
+			Graphics::PixelFormat::createFormatCLUT8());
+		scratch.clear();
 		Picture frame, scene;
-		if (_picsArchive.loadEntry(0x3d, frame)) {
-			g_system->copyRectToScreen(frame.surface.getPixels(),
-									   frame.surface.pitch, 0, 0,
-									   frame.surface.w, frame.surface.h);
-		}
+		if (_picsArchive.loadEntry(0x3d, frame))
+			scratch.simpleBlitFrom(frame.surface);
 		if (winScene < _sitesArchive.size() &&
 			_sitesArchive.loadEntry(winScene, scene)) {
-			const int sx = 0x42, sy = 0x14;
-			const int sw = MIN<int>(scene.surface.w, kScreenWidth - sx);
-			const int sh = MIN<int>(scene.surface.h, kScreenHeight - sy);
+			const int sx = scaleX(0x42);
+			const int sy = scaleY(0x14);
+			const int sw = MIN<int>(scene.surface.w, screenW - sx);
+			const int sh = MIN<int>(scene.surface.h, screenH - sy);
 			if (sw > 0 && sh > 0)
-				g_system->copyRectToScreen(scene.surface.getPixels(),
-										   scene.surface.pitch, sx, sy,
-										   sw, sh);
-		}
-
-		if (Graphics::Surface *screen = g_system->lockScreen()) {
-			blitPdaPartner(screen, _aniArchive, _partner,
-						   kPdaGalleryPartner, g_system->getMillis());
-			g_system->unlockScreen();
+				scratch.copyRectToSurface(scene.surface.getPixels(),
+										  scene.surface.pitch, sx, sy,
+										  sw, sh);
 		}
+		blitPdaPartner(scratch, _aniArchive, _partner,
+					   kPdaGalleryPartner, g_system->getMillis(),
+					   isMacintosh());
+		g_system->copyRectToScreen(scratch.getPixels(), scratch.pitch,
+								   0, 0, screenW, screenH);
 		g_system->updateScreen();
 
 		if (_music && _voiceOn)
 			_music->playMus(5, /* loop= */ false);
 
 		const byte *solved = _mystery.solvedClueBlock();
-		if (solved)
+		if (isMacintosh() && solved) {
+			const byte *bufBase = _mystery.blobAt(0);
+			const byte *end = bufBase ? bufBase + _mystery.dataSize()
+									  : nullptr;
+			const uint count = solved[0];
+			const byte *records = solved + 1;
+			const bool validChain =
+				advanceFloppyDialogRecords(records, count, end,
+										   /* mac= */ true) != nullptr;
+			if (!validChain) {
+				warning("doAccuse: malformed Mac solved chain");
+			} else if (count > 3) {
+				const uint beforeScrapbook = count - 3;
+				const byte *tail =
+					advanceFloppyDialogRecords(records, beforeScrapbook,
+											   end, /* mac= */ true);
+				if (tail) {
+					displayFloppyDialogRecords(records, beforeScrapbook, 1);
+					playMacScrapbookPartnerAnimation(this, scratch);
+					displayFloppyDialogRecords(tail, 3, 1);
+				}
+			} else {
+				displayFloppyDialogRecords(records, count, 1);
+			}
+		} else if (solved) {
 			displayClue(solved);
+		}
 		if (_music && _voiceOn)
 			_music->stop();
 
-		playAnm(Common::Path(isLondon() ? "SCRAP.ANM" : "SCRAPBK.ANI"), 120,
-				/* holdLastFrame= */ false);
-
-		displayScrapbookExtra(mn);
+		if (!isMacintosh()) {
+			playAnm(Common::Path(isLondon() ? "SCRAP.ANM" : "SCRAPBK.ANI"),
+					120, /* holdLastFrame= */ false);
+			displayScrapbookExtra(mn);
+		}
 
 		doShowEnding(mn);
 


Commit: 267abe0a5e90b7cbcce6799b2f2f0a754fcfa35c
    https://github.com/scummvm/scummvm/commit/267abe0a5e90b7cbcce6799b2f2f0a754fcfa35c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-06-23T21:52:01+02:00

Commit Message:
FREESCAPE: make sound timing more accurate in DOS sounds

Changed paths:
    engines/freescape/sound/dos.cpp


diff --git a/engines/freescape/sound/dos.cpp b/engines/freescape/sound/dos.cpp
index ddd0fba5bca..ab3a84e4ddc 100644
--- a/engines/freescape/sound/dos.cpp
+++ b/engines/freescape/sound/dos.cpp
@@ -148,8 +148,13 @@ uint16 SoundDOS::playSoundDOSSpeaker(uint16 frequencyStart, soundSpeakerFx *spea
 	uint8 frequencyDuration = speakerFxInfo->frequencyDuration;
 
 	int16 freq = frequencyStart;
-	int waveDurationMultipler = 1800;
-	int waveDuration = waveDurationMultipler * (frequencyDuration + 1);
+	// The DOS build advances the speaker sequencer from its timer IRQ, which
+	// the executable reprograms to ~300 Hz (PIT channel 0 reload = 3977, i.e.
+	// 1193182/3977). Each step holds its frequency for frequencyDuration timer
+	// ticks; the original 8-bit counter makes a duration of 0 wrap to 256 ticks.
+	const int kTimerHz = 300;
+	int durationTicks = frequencyDuration ? frequencyDuration : 256;
+	int waveDuration = (1000000 / kTimerHz) * durationTicks; // microseconds
 
 	while (true) {
 		if (freq > 0) {


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

Commit Message:
FREESCAPE: fix infinite loop in DOS/ZX sound handling

Changed paths:
    engines/freescape/sound/dos.cpp
    engines/freescape/sound/zx.cpp


diff --git a/engines/freescape/sound/dos.cpp b/engines/freescape/sound/dos.cpp
index ab3a84e4ddc..cb9dbdf83e0 100644
--- a/engines/freescape/sound/dos.cpp
+++ b/engines/freescape/sound/dos.cpp
@@ -56,6 +56,7 @@ public:
 	}
 
 	void stopSound(Type type) override {
+		_speaker->stop(); // flush the queue too, or isPlayingSound() hangs waitForSounds()
 		_mixer->stopHandle(_soundFxHandle);
 	}
 
diff --git a/engines/freescape/sound/zx.cpp b/engines/freescape/sound/zx.cpp
index bf57552f5f2..a6ccc887461 100644
--- a/engines/freescape/sound/zx.cpp
+++ b/engines/freescape/sound/zx.cpp
@@ -50,6 +50,7 @@ public:
 	}
 
 	void stopSound(Type type) override {
+		_speaker->stop(); // flush the queue too, or isPlayingSound() hangs waitForSounds()
 		_mixer->stopHandle(_soundFxHandle);
 	}
 




More information about the Scummvm-git-logs mailing list