[Scummvm-git-logs] scummvm master -> 4b8f06beb6df5420921fed23aed9b7d27436af54

dreammaster noreply at scummvm.org
Wed Aug 12 06:48:51 UTC 2026


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

Summary:
c2f9670e97 MADS: Preserve buffers when EMS is unavailable
d8ed36b26c MADS: NEBULAR: Restore Macintosh interface palettes
4ea430c191 MADS: NEBULAR: Restore Macintosh interface controls
83d353a539 MADS: NEBULAR: Preserve Macintosh palette transitions
e7bebb0821 MADS: NEBULAR: Fix Macintosh object examination
7da7ec62db MADS: Handle AnimView resource terminators
92ce40af41 MADS: NEBULAR: Restore Macintosh difficulty dialog
066be0fb31 MADS: NEBULAR: Restore Macintosh outer menu
7b67d7f3f1 MADS: Add configurable frontend viewer presentation
212e8daff5 MADS: NEBULAR: Restore Macintosh TextView
fe155831d2 MADS: NEBULAR: Restore Macintosh AnimView
47019b36e7 MADS: NEBULAR: Match native frontend transitions
9d3fbe767c MADS: NEBULAR: Clear Macintosh viewer padding
c3442d1e4b MADS: NEBULAR: Preserve Macintosh menu colors
a0b7b82a90 MADS: NEBULAR: Avoid duplicate Macintosh menu fade
45a28596d5 MADS: Center initial TextView line
b9834f0872 MADS: NEBULAR: Refresh Macintosh difficulty cursor
4f1d749d52 MADS: NEBULAR: Hide incomplete Macintosh room frames
4b8f06beb6 MADS: NEBULAR: Map Macintosh frontend resources


Commit: c2f9670e9729a7b675d7e3a8159f412168831e92
    https://github.com/scummvm/scummvm/commit/c2f9670e9729a7b675d7e3a8159f412168831e92
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:48:38+10:00

Commit Message:
MADS: Preserve buffers when EMS is unavailable

Return the actual EMS page handle instead of converting it to a Boolean.
This lets buffer preservation fall back to disk when EMS allocation
fails.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/core/buffer.cpp
    engines/mads/core/buffer.h


diff --git a/engines/mads/core/buffer.cpp b/engines/mads/core/buffer.cpp
index f7c0b1c50f1..59e08e36879 100644
--- a/engines/mads/core/buffer.cpp
+++ b/engines/mads/core/buffer.cpp
@@ -810,7 +810,7 @@ done:
 	;
 }
 
-bool buffer_to_ems(Buffer *source, int page_handle, int source_ems_handle,
+int buffer_to_ems(Buffer *source, int page_handle, int source_ems_handle,
 		int x, int y, int xs, int ys) {
 	int special_page_handle = 0;
 	Buffer ems_buffer = { video_y, video_x, NULL };
@@ -840,7 +840,7 @@ done:
 	return page_handle;
 }
 
-bool buffer_from_ems(Buffer *source, int page_handle, int target_ems_handle,
+int buffer_from_ems(Buffer *source, int page_handle, int target_ems_handle,
 		int x, int y, int xs, int ys) {
 	Buffer ems_buffer = { video_y, video_x, NULL };
 	int special_page_handle;
diff --git a/engines/mads/core/buffer.h b/engines/mads/core/buffer.h
index dca2363e532..f78e0006b7b 100644
--- a/engines/mads/core/buffer.h
+++ b/engines/mads/core/buffer.h
@@ -243,9 +243,9 @@ extern void buffer_peel_vert(Buffer *target, int peel,
 extern int buffer_to_disk(Buffer *source, int x, int y, int xs, int ys);
 extern void buffer_from_disk(Buffer *source, int buffer_id, int x, int y, int xs, int ys);
 
-extern bool buffer_to_ems(Buffer *source, int page_handle,
+extern int buffer_to_ems(Buffer *source, int page_handle,
 	int source_ems_handle, int x, int y, int xs, int ys);
-extern bool buffer_from_ems(Buffer *source, int page_handle,
+extern int buffer_from_ems(Buffer *source, int page_handle,
 	int target_ems_handle, int x, int y, int xs, int ys);
 
 extern int buffer_preserve(Buffer *source, int flags,


Commit: d8ed36b26c4d256389554a18040068e2583ca289
    https://github.com/scummvm/scummvm/commit/d8ed36b26c4d256389554a18040068e2583ca289
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:48:38+10:00

Commit Message:
MADS: NEBULAR: Restore Macintosh interface palettes

Use the complete InBx artwork and its gamma-corrected ten-color palette
instead of applying the provisional cyan wash. Restore the
section-specific semantic colors used by the native interface.

Match CODE 7 selection behavior: left selections are white, active
inventory and right actions are black, and command words retain the
normal foreground color.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/nebular/mac_nebular.cpp
    engines/mads/nebular/mac_resources.cpp
    engines/mads/nebular/mac_resources.h


diff --git a/engines/mads/nebular/mac_nebular.cpp b/engines/mads/nebular/mac_nebular.cpp
index 55a89c982e8..922bba4ba4c 100644
--- a/engines/mads/nebular/mac_nebular.cpp
+++ b/engines/mads/nebular/mac_nebular.cpp
@@ -25,10 +25,12 @@
 #include "common/events.h"
 #include "common/system.h"
 #include "graphics/font.h"
+#include "graphics/macgamma.h"
 #include "graphics/managed_surface.h"
+#include "graphics/paletteman.h"
 #include "mads/core/env.h"
 #include "mads/core/inter.h"
-#include "mads/core/mcga.h"
+#include "mads/core/kernel.h"
 #include "mads/core/object.h"
 #include "mads/core/pal.h"
 #include "mads/core/screen.h"
@@ -55,14 +57,12 @@ enum {
 	kMacDesktopSceneY = kMacMenuBarHeight + 20,
 	kMacDesktopSeparatorY = kMacDesktopSceneY + kMacSceneHeight,
 	kMacDesktopInterfaceY = kMacDesktopSeparatorY + 1,
-	kMacBlackColor = 8,
+	kMacPanelPaletteColors = 10,
 	kMacNormalTextColor = 15,
 	kMacLeftSelectColor = 13,
 	kMacRightSelectColor = 14,
-	kMacPopupColor = REX_DIALOG_FE_COLOR,
-	kMacPanelTintR = 22,
-	kMacPanelTintG = 39,
-	kMacPanelTintB = 42
+	kMacBlackColor = kMacRightSelectColor,
+	kMacPopupColor = REX_DIALOG_FE_COLOR
 };
 
 struct MacPopupLine {
@@ -82,90 +82,74 @@ static void appendWrappedMacPopupText(const Graphics::Font &font,
 		lines.push_back(MacPopupLine(wrapped[i], tab));
 }
 
-static int macPanelColorDistance(int r1, int g1, int b1,
-		int r2, int g2, int b2) {
-	const int dr = r1 - r2;
-	const int dg = g1 - g2;
-	const int db = b1 - b2;
-	return dr * dr + dg * dg + db * db;
-}
-
-static void buildMacPanelWashLUT(const byte *nativePalette, byte washLUT[256]) {
-	// The panel is still presented as CLUT8. Replace its first eight display
-	// colors with a uniform 25-percent source / 75-percent cyan blend, then
-	// map the completed panel to those colors. This models a translucent wash
-	// without the checkerboard pattern used by an earlier approximation.
-	Palette displayPalette;
-	for (int color = 0; color < 8; ++color) {
-		const int r = (nativePalette[color * 3 + 0] * 63 + 127) / 255;
-		const int g = (nativePalette[color * 3 + 1] * 63 + 127) / 255;
-		const int b = (nativePalette[color * 3 + 2] * 63 + 127) / 255;
-		displayPalette[color].r = (r + 3 * kMacPanelTintR + 2) / 4;
-		displayPalette[color].g = (g + 3 * kMacPanelTintG + 2) / 4;
-		displayPalette[color].b = (b + 3 * kMacPanelTintB + 2) / 4;
-	}
+static byte macGammaCorrect(byte color) {
+	return Graphics::macGammaCorrectionLookUp[color << 2];
+}
 
-	// Index 8 backs the side gutters, popup text, and caption shadow. The
-	// normal MADS interface palette makes it dark gray, not Macintosh black.
-	displayPalette[kMacBlackColor].r = 0;
-	displayPalette[kMacBlackColor].g = 0;
-	displayPalette[kMacBlackColor].b = 0;
-	mcga_setpal_range(&displayPalette, 0, kMacBlackColor + 1);
-
-	// CODE 7 draws normal and selected interface words with palette indexes
-	// 15, 13, and 14 respectively. Keep those roles independent of room
-	// palette changes and outside the panel wash below.
-	displayPalette[kMacLeftSelectColor].r =
-		(nativePalette[1 * 3 + 0] * 63 + 127) / 255;
-	displayPalette[kMacLeftSelectColor].g =
-		(nativePalette[1 * 3 + 1] * 63 + 127) / 255;
-	displayPalette[kMacLeftSelectColor].b =
-		(nativePalette[1 * 3 + 2] * 63 + 127) / 255;
-	displayPalette[kMacRightSelectColor].r =
-		(nativePalette[2 * 3 + 0] * 63 + 127) / 255;
-	displayPalette[kMacRightSelectColor].g =
-		(nativePalette[2 * 3 + 1] * 63 + 127) / 255;
-	displayPalette[kMacRightSelectColor].b =
-		(nativePalette[2 * 3 + 2] * 63 + 127) / 255;
-	displayPalette[kMacNormalTextColor].r = 63;
-	displayPalette[kMacNormalTextColor].g = 63;
-	displayPalette[kMacNormalTextColor].b = 63;
-	mcga_setpal_range(&displayPalette, kMacLeftSelectColor, 3);
-
-	for (int sourceColor = 0; sourceColor < 256; ++sourceColor) {
-		int sourceR, sourceG, sourceB;
-		if (sourceColor < 8 || sourceColor == kMacLeftSelectColor ||
-				sourceColor == kMacRightSelectColor) {
-			const int nativeColor = sourceColor == kMacLeftSelectColor ? 1 :
-				(sourceColor == kMacRightSelectColor ? 2 : sourceColor);
-			sourceR = (nativePalette[nativeColor * 3 + 0] * 63 + 127) / 255;
-			sourceG = (nativePalette[nativeColor * 3 + 1] * 63 + 127) / 255;
-			sourceB = (nativePalette[nativeColor * 3 + 2] * 63 + 127) / 255;
-		} else if (sourceColor == kMacNormalTextColor) {
-			sourceR = sourceG = sourceB = 63;
-		} else {
-			sourceR = master_palette[sourceColor].r;
-			sourceG = master_palette[sourceColor].g;
-			sourceB = master_palette[sourceColor].b;
-		}
+static byte macPaletteComponentToSixBit(byte color) {
+	return (color * 63 + 127) / 255;
+}
 
-		const int washedR = (sourceR + 3 * kMacPanelTintR + 2) / 4;
-		const int washedG = (sourceG + 3 * kMacPanelTintG + 2) / 4;
-		const int washedB = (sourceB + 3 * kMacPanelTintB + 2) / 4;
-		int bestColor = 0;
-		int bestDistance = macPanelColorDistance(washedR, washedG, washedB,
-			displayPalette[0].r, displayPalette[0].g, displayPalette[0].b);
-		for (int candidate = 1; candidate < 8; ++candidate) {
-			const int distance = macPanelColorDistance(washedR, washedG, washedB,
-				displayPalette[candidate].r, displayPalette[candidate].g,
-				displayPalette[candidate].b);
-			if (distance < bestDistance) {
-				bestDistance = distance;
-				bestColor = candidate;
-			}
+static void setMacInterfacePalette(const MacResourceProvider *resources) {
+	if (resources && resources->getNativeInterface()) {
+		const byte *panelPalette = resources->getNativeInterfacePalette();
+		// Keep MADS' six-bit palette state synchronized, but install the exact
+		// gamma-corrected colors in the backend.
+		for (int color = 0; color < kMacPanelPaletteColors; ++color) {
+			master_palette[color + 2].r =
+				macPaletteComponentToSixBit(panelPalette[color * 3]);
+			master_palette[color + 2].g =
+				macPaletteComponentToSixBit(panelPalette[color * 3 + 1]);
+			master_palette[color + 2].b =
+				macPaletteComponentToSixBit(panelPalette[color * 3 + 2]);
 		}
-		washLUT[sourceColor] = bestColor;
+		g_system->getPaletteManager()->setPalette(panelPalette, 2,
+			kMacPanelPaletteColors);
 	}
+
+	byte normalR = 25;
+	byte normalG = 48;
+	byte normalB = 51;
+	switch (section_id) {
+	case 2:
+	case 3:
+		normalR = 10;
+		normalG = 43;
+		normalB = 10;
+		break;
+	case 5:
+		normalR = 41;
+		normalG = 45;
+		normalB = 53;
+		break;
+	case 6:
+	case 7:
+		// Section 8 shares section 7's interface behavior in the port.
+	case 8:
+		normalR = 42;
+		normalG = 47;
+		normalB = 54;
+		break;
+	default:
+		break;
+	}
+
+	byte semanticPalette[3 * 3] = {
+		macGammaCorrect(63), macGammaCorrect(63), macGammaCorrect(63),
+		macGammaCorrect(0), macGammaCorrect(0), macGammaCorrect(0),
+		macGammaCorrect(normalR), macGammaCorrect(normalG),
+		macGammaCorrect(normalB)
+	};
+	for (int color = 0; color < 3; ++color) {
+		master_palette[color + kMacLeftSelectColor].r =
+			macPaletteComponentToSixBit(semanticPalette[color * 3]);
+		master_palette[color + kMacLeftSelectColor].g =
+			macPaletteComponentToSixBit(semanticPalette[color * 3 + 1]);
+		master_palette[color + kMacLeftSelectColor].b =
+			macPaletteComponentToSixBit(semanticPalette[color * 3 + 2]);
+	}
+	g_system->getPaletteManager()->setPalette(semanticPalette,
+		kMacLeftSelectColor, 3);
 }
 
 static bool getMacInterfaceSpot(int class_, int id, Common::Rect &rect) {
@@ -277,8 +261,7 @@ static byte getMacInterfaceTextColor(int class_, int id) {
 			(class_ == STROKE_ACTION && id == left_action) ||
 			(class_ == STROKE_DIALOG && id == left_command))
 		return kMacLeftSelectColor;
-	if ((class_ == STROKE_COMMAND && id == right_command) ||
-			(class_ == STROKE_INVEN && id == active_inven) ||
+	if ((class_ == STROKE_INVEN && id == active_inven) ||
 			(class_ == STROKE_ACTION && id == right_action))
 		return kMacRightSelectColor;
 	return kMacNormalTextColor;
@@ -519,6 +502,7 @@ void MacNebular::presentScreen(int shakeOffset) {
 	const int sceneY = _useOriginalMenus ? kMacDesktopSceneY : 0;
 	const int interfaceY = _useOriginalMenus ?
 		kMacDesktopInterfaceY : kMacSceneHeight;
+	setMacInterfacePalette(_resources);
 	_output.fillRect(_output.getBounds(), kMacBlackColor);
 
 	// Native large-window mode doubles the 320x156 scene in both axes.
@@ -568,25 +552,12 @@ void MacNebular::presentScreen(int shakeOffset) {
 				for (int x = 0; x < kMacInterfaceWidth; ++x) {
 					const int logicalX = x * 320 / kMacInterfaceWidth;
 					if (current[logicalX] != baseline[logicalX] &&
-							(!interfaceFont ||
-							!isMacInterfaceSemanticPixel(logicalX, logicalY)))
+							!isMacInterfaceSemanticPixel(logicalX, logicalY))
 						target[x] = current[logicalX];
 				}
 			}
 		}
 
-		// Apply the native blue layer to the panel artwork and non-semantic live
-		// state. CODE 7 draws the interface words afterward with distinct
-		// normal and selection colors, so they must not be quantized into the
-		// eight washed background colors.
-		byte washLUT[256];
-		buildMacPanelWashLUT(_resources->getNativeInterfacePalette(), washLUT);
-		for (int y = 0; y < kMacInterfaceHeight; ++y) {
-			byte *target = (byte *)panel.getBasePtr(0, y);
-			for (int x = 0; x < kMacInterfaceWidth; ++x)
-				target[x] = washLUT[target[x]];
-		}
-
 		if (interfaceFont)
 			drawMacInterfaceState(panel, *interfaceFont);
 
diff --git a/engines/mads/nebular/mac_resources.cpp b/engines/mads/nebular/mac_resources.cpp
index 35b0e47a2d1..58c580b2a84 100644
--- a/engines/mads/nebular/mac_resources.cpp
+++ b/engines/mads/nebular/mac_resources.cpp
@@ -27,6 +27,7 @@
 #include "common/textconsole.h"
 #include "graphics/cursorman.h"
 #include "graphics/font.h"
+#include "graphics/macgamma.h"
 #include "graphics/macgui/macfontmanager.h"
 #include "graphics/macgui/macwindowmanager.h"
 #include "graphics/surface.h"
@@ -34,6 +35,7 @@
 #include "mads/core/color.h"
 #include "mads/core/inter.h"
 #include "mads/core/pack.h"
+#include "mads/core/pal.h"
 #include "mads/nebular/mac_resources.h"
 
 namespace MADS {
@@ -96,6 +98,14 @@ static byte *createUncompressedPack(uint32 payloadSize, byte priority) {
 	return data;
 }
 
+static byte macGammaCorrectPaletteComponent(byte color) {
+	return Graphics::macGammaCorrectionLookUp[color & 0xfc];
+}
+
+static byte macPaletteComponentToSixBit(byte color) {
+	return (color * 63 + 127) / 255;
+}
+
 struct GlobalSeriesFamily {
 	const char *prefix;
 	uint32 type;
@@ -446,7 +456,7 @@ Common::SeekableReadStream *MacResourceProvider::createFontResource() {
 Common::SeekableReadStream *MacResourceProvider::createInterfaceResource(int interfaceID) {
 	// The Macintosh port replaces each DOS I#.INT bitmap with an InBx ILBM.
 	// Adapt its 512x88 native interface to the shared 320x44 MADS renderer;
-	// the images use only the first eight palette indices.
+	// the native loader maps its first ten palette indices to slots 2 through 11.
 	Common::SeekableReadStream *source = _containers[kGlobalContainer]->getResource(
 		MKTAG('I', 'n', 'B', 'x'), 1000 + interfaceID);
 	if (!source)
@@ -457,15 +467,31 @@ Common::SeekableReadStream *MacResourceProvider::createInterfaceResource(int int
 	delete source;
 	const Graphics::Surface *surface = decoder.getSurface();
 	if (!loaded || !surface || surface->format.bytesPerPixel != 1 ||
-			surface->w <= 0 || surface->h <= 0 || decoder.getPalette().size() < 16) {
+			surface->w <= 0 || surface->h <= 0 || decoder.getPalette().size() < 10) {
 		warning("Invalid Macintosh Rex interface resource I%d", interfaceID);
 		return nullptr;
 	}
 
+	for (int y = 0; y < surface->h; ++y) {
+		const byte *sourceRow = (const byte *)surface->getBasePtr(0, y);
+		for (int x = 0; x < surface->w; ++x) {
+			if (sourceRow[x] >= 10) {
+				warning("Macintosh Rex interface I%d uses unsupported color %u",
+					interfaceID, sourceRow[x]);
+				return nullptr;
+			}
+		}
+	}
+
 	// Keep the unscaled native panel for the Macintosh presentation path.
 	// The synthetic pack below exists only so the shared interface state
 	// machine can continue operating in its original 320x44 coordinate space.
 	_nativeInterface.copyFrom(*surface);
+	for (int y = 0; y < _nativeInterface.h; ++y) {
+		byte *row = (byte *)_nativeInterface.getBasePtr(0, y);
+		for (int x = 0; x < _nativeInterface.w; ++x)
+			row[x] += 2;
+	}
 
 	const uint32 payloadSize = sizeof(Color) * 16 + video_x * inter_size_y;
 	const uint32 size = PackList::SIZE + payloadSize;
@@ -474,40 +500,36 @@ Common::SeekableReadStream *MacResourceProvider::createInterfaceResource(int int
 		return nullptr;
 
 	Color *colors = (Color *)(data + PackList::SIZE);
+	for (int i = 0; i < 16; ++i) {
+		colors[i].r = master_palette[i].r;
+		colors[i].g = master_palette[i].g;
+		colors[i].b = master_palette[i].b;
+	}
+
 	const Graphics::Palette &palette = decoder.getPalette();
-	for (uint i = 0; i < 16; ++i) {
+	for (uint i = 0; i < 10; ++i) {
 		byte r, g, b;
 		palette.get(i, r, g, b);
-		_nativeInterfacePalette[i * 3 + 0] = r;
-		_nativeInterfacePalette[i * 3 + 1] = g;
-		_nativeInterfacePalette[i * 3 + 2] = b;
-		colors[i].r = PALETTE_8BIT_TO_6BIT(r);
-		colors[i].g = PALETTE_8BIT_TO_6BIT(g);
-		colors[i].b = PALETTE_8BIT_TO_6BIT(b);
+		// The native loader truncates CMAP components to six bits before
+		// Macintosh display gamma is applied.
+		const byte correctedR = macGammaCorrectPaletteComponent(r);
+		const byte correctedG = macGammaCorrectPaletteComponent(g);
+		const byte correctedB = macGammaCorrectPaletteComponent(b);
+		_nativeInterfacePalette[i * 3 + 0] = correctedR;
+		_nativeInterfacePalette[i * 3 + 1] = correctedG;
+		_nativeInterfacePalette[i * 3 + 2] = correctedB;
+		colors[i + 2].r = macPaletteComponentToSixBit(correctedR);
+		colors[i + 2].g = macPaletteComponentToSixBit(correctedG);
+		colors[i + 2].b = macPaletteComponentToSixBit(correctedB);
 	}
 
-	// InBx pixels use only colors 0 through 7. The native interface code
-	// selects 13, 14, and 15 for its two selection states and normal text,
-	// respectively; supply those QuickDraw foreground colors independently
-	// of the bitmap's otherwise-black unused CMAP entries.
-	colors[8].r = colors[8].g = colors[8].b = 0;
-	colors[13] = colors[1];
-	colors[14] = colors[2];
-	colors[15].r = colors[15].g = colors[15].b = 63;
-
 	byte *target = data + PackList::SIZE + sizeof(Color) * 16;
 	for (int y = 0; y < inter_size_y; ++y) {
 		const int sourceY = ((2 * y + 1) * surface->h) / (2 * inter_size_y);
 		const byte *sourceRow = (const byte *)surface->getBasePtr(0, sourceY);
 		for (int x = 0; x < video_x; ++x) {
 			const int sourceX = ((2 * x + 1) * surface->w) / (2 * video_x);
-			const byte color = sourceRow[sourceX];
-			if (color >= 16) {
-				warning("Macintosh Rex interface I%d uses unsupported color %u", interfaceID, color);
-				free(data);
-				return nullptr;
-			}
-			target[y * video_x + x] = color;
+			target[y * video_x + x] = sourceRow[sourceX] + 2;
 		}
 	}
 
diff --git a/engines/mads/nebular/mac_resources.h b/engines/mads/nebular/mac_resources.h
index e2fc58930de..bd3b4815949 100644
--- a/engines/mads/nebular/mac_resources.h
+++ b/engines/mads/nebular/mac_resources.h
@@ -93,7 +93,7 @@ private:
 	Graphics::MacFontManager *_fontManager = nullptr;
 	Graphics::ManagedSurface _nativeInterface;
 	Graphics::ManagedSurface _logicalInterface;
-	byte _nativeInterfacePalette[16 * 3] = {};
+	byte _nativeInterfacePalette[10 * 3] = {};
 	int _cursorID = 0;
 	int _waitCursorFrame = 0;
 	uint32 _nextCursorTime = 0;


Commit: 4ea430c19184a9d205e3abb5bae6f8652ec1ed6f
    https://github.com/scummvm/scummvm/commit/4ea430c19184a9d205e3abb5bae6f8652ec1ed6f
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:48:38+10:00

Commit Message:
MADS: NEBULAR: Restore Macintosh interface controls

Keep the compatibility interface state machine, but leave text and
scrollbar rendering to the Macintosh presenter. Mask the DOS glyph
regions before drawing the native controls so FONTMISC characters cannot
leak into the panel.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/core/inter.cpp
    engines/mads/mads.h
    engines/mads/nebular/mac_nebular.cpp
    engines/mads/nebular/nebular.h


diff --git a/engines/mads/core/inter.cpp b/engines/mads/core/inter.cpp
index 87aa08159f8..5fa4dea397f 100644
--- a/engines/mads/core/inter.cpp
+++ b/engines/mads/core/inter.cpp
@@ -424,6 +424,8 @@ static void inter_show_word(int class_, int id) {
 	if (!inter_get_spot(class_, id, &x, &y, &junk, &junk)) {
 		goto done;
 	}
+	if (g_engine->hasMacintoshInterface())
+		goto done;
 
 	switch (class_) {
 	case STROKE_COMMAND:
diff --git a/engines/mads/mads.h b/engines/mads/mads.h
index e98aee825c6..4cd2df1f2a1 100644
--- a/engines/mads/mads.h
+++ b/engines/mads/mads.h
@@ -186,6 +186,7 @@ public:
 	virtual bool getInterfaceSentenceColors(byte &, byte &) const {
 		return false;
 	}
+	virtual bool hasMacintoshInterface() const { return false; }
 
 	virtual void player_keep_walking();
 
diff --git a/engines/mads/nebular/mac_nebular.cpp b/engines/mads/nebular/mac_nebular.cpp
index 922bba4ba4c..3e4ceec5aa4 100644
--- a/engines/mads/nebular/mac_nebular.cpp
+++ b/engines/mads/nebular/mac_nebular.cpp
@@ -247,14 +247,6 @@ static int macInterfaceY(int logicalY) {
 	return logicalY * 2;
 }
 
-static Common::Rect scaleMacInterfaceRect(const Common::Rect &logical) {
-	return Common::Rect(
-		CLIP<int>(macInterfaceX(logical.left), 0, kMacInterfaceWidth),
-		CLIP<int>(macInterfaceY(logical.top), 0, kMacInterfaceHeight),
-		CLIP<int>(macInterfaceX(logical.right), 0, kMacInterfaceWidth),
-		CLIP<int>(macInterfaceY(logical.bottom), 0, kMacInterfaceHeight));
-}
-
 static byte getMacInterfaceTextColor(int class_, int id) {
 	if ((class_ == STROKE_COMMAND && id == left_command) ||
 			(class_ == STROKE_INVEN && id == left_inven) ||
@@ -295,56 +287,23 @@ static Common::String getMacInterfaceWord(int wordId) {
 	return Common::String(text);
 }
 
-static void setMacInterfacePixel(Graphics::ManagedSurface &panel,
-		int x, int y, byte color) {
-	if (x >= 0 && x < panel.w && y >= 0 && y < panel.h)
-		*(byte *)panel.getBasePtr(x, y) = color;
-}
-
-static void drawMacInterfaceArrow(Graphics::ManagedSurface &panel,
-		const Common::Rect &rect, bool up, byte color) {
-	if (rect.isEmpty())
-		return;
-
-	const int centerX = (rect.left + rect.right - 1) / 2;
-	const int centerY = (rect.top + rect.bottom - 1) / 2;
-	const int radius = MAX(1, MIN((rect.width() - 1) / 2,
-		(rect.height() - 1) / 2));
-	for (int row = 0; row <= radius; ++row) {
-		const int y = up ? centerY - radius / 2 + row :
-			centerY + radius / 2 - row;
-		for (int x = centerX - row; x <= centerX + row; ++x)
-			setMacInterfacePixel(panel, x, y, color);
-	}
-}
-
-static Common::Rect getScaledMacInterfaceSpot(int class_, int id) {
-	Common::Rect logical;
-	return getMacInterfaceSpot(class_, id, logical) ?
-		scaleMacInterfaceRect(logical) : Common::Rect();
-}
-
 static void drawMacInterfaceScrollbar(Graphics::ManagedSurface &panel) {
-	const Common::Rect up = getScaledMacInterfaceSpot(STROKE_SCROLL, SCROLL_UP);
-	const Common::Rect down = getScaledMacInterfaceSpot(STROKE_SCROLL, SCROLL_DOWN);
-	const Common::Rect elevator =
-		getScaledMacInterfaceSpot(STROKE_SCROLL, SCROLL_ELEVATOR);
-	Common::Rect thumb = getScaledMacInterfaceSpot(STROKE_SCROLL, SCROLL_THUMB);
-
-	if (!elevator.isEmpty())
-		panel.frameRect(elevator, scrollbar_active == SCROLL_ELEVATOR ?
-			kMacLeftSelectColor : kMacNormalTextColor);
-	drawMacInterfaceArrow(panel, up, true,
-		scrollbar_active == SCROLL_UP ? kMacLeftSelectColor : kMacNormalTextColor);
-	drawMacInterfaceArrow(panel, down, false,
-		scrollbar_active == SCROLL_DOWN ? kMacLeftSelectColor : kMacNormalTextColor);
-
-	if (!thumb.isEmpty() && !elevator.isEmpty()) {
-		thumb.left = MAX<int>(thumb.left, elevator.left + 2);
-		thumb.right = MIN<int>(thumb.right, elevator.right - 2);
-		if (!thumb.isEmpty())
-			panel.fillRect(thumb, scrollbar_active == SCROLL_ELEVATOR ?
-				kMacLeftSelectColor : kMacNormalTextColor);
+	// CODE 7 draws this control directly in native coordinates. The arrow
+	// line pattern is invisible in the normal state; the three frames are
+	// the complete control until the inventory exceeds five items.
+	const Common::Rect scrollbar(120, 2, 136, 86);
+	const Common::Rect up(120, 2, 136, 18);
+	const Common::Rect down(120, 70, 136, 86);
+	panel.frameRect(scrollbar, kMacNormalTextColor);
+	panel.frameRect(up, kMacNormalTextColor);
+	panel.frameRect(down, kMacNormalTextColor);
+
+	if (inven_num_objects > 5) {
+		panel.fillRect(Common::Rect(121, 18, 135, 70), 4);
+		const int thumbTop = 18 +
+			(first_inven * 48) / (inven_num_objects - 5);
+		panel.frameRect(Common::Rect(121, thumbTop, 135, thumbTop + 4),
+			kMacNormalTextColor);
 	}
 }
 
@@ -409,6 +368,13 @@ static bool isMacInterfaceSemanticPixel(int x, int y) {
 	return false;
 }
 
+static bool isMacInterfaceScrollbarPixel(int x, int y) {
+	// Mask the union of the scaled compatibility glyph area and CODE 7's
+	// native control so FONTMISC's private a/b/c/d glyphs cannot leak out.
+	return Common::Rect(macInterfaceX(inter_scroll_x1), 2, 136, 86)
+		.contains(x, y);
+}
+
 MacNebular::MacNebular(RexNebularEngine &engine) :
 		_engine(engine), _useOriginalMenus(ConfMan.getBool("original_mac_menus")) {
 }
@@ -552,7 +518,8 @@ void MacNebular::presentScreen(int shakeOffset) {
 				for (int x = 0; x < kMacInterfaceWidth; ++x) {
 					const int logicalX = x * 320 / kMacInterfaceWidth;
 					if (current[logicalX] != baseline[logicalX] &&
-							!isMacInterfaceSemanticPixel(logicalX, logicalY))
+							!isMacInterfaceSemanticPixel(logicalX, logicalY) &&
+							!isMacInterfaceScrollbarPixel(x, y))
 						target[x] = current[logicalX];
 				}
 			}
@@ -768,6 +735,10 @@ bool RexNebularEngine::getInterfaceSentenceColors(byte &foreground, byte &shadow
 	return true;
 }
 
+bool RexNebularEngine::hasMacintoshInterface() const {
+	return _macNebular != nullptr;
+}
+
 bool RexNebularEngine::hasInterfaceAnimations() const {
 	// Macintosh CODE 7 stops loading an AA_INTERFACE controller before its
 	// DOS sprite-series list. The Mac resource set accordingly contains the
diff --git a/engines/mads/nebular/nebular.h b/engines/mads/nebular/nebular.h
index f3232243075..fb3a91d0ad2 100644
--- a/engines/mads/nebular/nebular.h
+++ b/engines/mads/nebular/nebular.h
@@ -77,6 +77,7 @@ public:
 	bool drawPopup() override;
 	void onPopupDestroyed() override;
 	bool getInterfaceSentenceColors(byte &foreground, byte &shadow) const override;
+	bool hasMacintoshInterface() const override;
 };
 
 } // namespace RexNebular


Commit: 83d353a539f2d5a0f2e8b8ae0e21fceb956719f2
    https://github.com/scummvm/scummvm/commit/83d353a539f2d5a0f2e8b8ae0e21fceb956719f2
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:48:38+10:00

Commit Message:
MADS: NEBULAR: Preserve Macintosh palette transitions

Let Macintosh Rex retain the ungamma-corrected MADS palette while
installing corrected scene colors in the backend. Route palette reads
and writes through neutral engine hooks. Present each native fade step.

The default hooks leave DOS releases and other MADS games unchanged.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/core/magic.cpp
    engines/mads/core/mcga.cpp
    engines/mads/mads.h
    engines/mads/nebular/mac_nebular.cpp
    engines/mads/nebular/mac_nebular.h
    engines/mads/nebular/nebular.h


diff --git a/engines/mads/core/magic.cpp b/engines/mads/core/magic.cpp
index 7d3e63757f7..2ac5602de2a 100644
--- a/engines/mads/core/magic.cpp
+++ b/engines/mads/core/magic.cpp
@@ -19,6 +19,7 @@
  *
  */
 
+#include "common/system.h"
 #include "mads/core/magic.h"
 #include "mads/core/buffer.h"
 #include "mads/core/error.h"
@@ -271,6 +272,8 @@ void magic_fade_to_grey(Palette &pal, byte *map_pointer,
 		}
 
 		mcga_setpal((Palette *)pal);
+		if (g_engine->hasMacintoshInterface())
+			g_system->updateScreen();
 
 		do {
 			now_timing = timer_read_600();
@@ -369,6 +372,8 @@ void magic_fade_from_grey(RGBcolor *pal, Palette target,
 		}
 
 		mcga_setpal((Palette *)pal);
+		if (g_engine->hasMacintoshInterface())
+			g_system->updateScreen();
 
 		do {
 			now_timing = timer_read_600();
diff --git a/engines/mads/core/mcga.cpp b/engines/mads/core/mcga.cpp
index 2f98f06cfeb..d3dd6923dae 100644
--- a/engines/mads/core/mcga.cpp
+++ b/engines/mads/core/mcga.cpp
@@ -105,6 +105,10 @@ void mcga_close_window(byte *inp) {
 }
 
 void mcga_getpal(Palette *pal) {
+	if (g_engine->getMacintoshPalette(&(*pal)[0], 0,
+			Graphics::PALETTE_COUNT))
+		return;
+
 	byte tmp[Graphics::PALETTE_COUNT * 3];
 	g_system->getPaletteManager()->grabPalette(tmp, 0, Graphics::PALETTE_COUNT);
 	for (int i = 0; i < Graphics::PALETTE_COUNT; i++) {
@@ -115,6 +119,9 @@ void mcga_getpal(Palette *pal) {
 }
 
 void mcga_setpal_range(Palette *pal, int first_color, int num_colors) {
+	if (g_engine->setMacintoshPalette(&(*pal)[0], first_color, num_colors))
+		return;
+
 	byte tmp[Graphics::PALETTE_COUNT * 3];
 	for (int i = 0; i < num_colors; i++) {
 		tmp[i * 3 + 0] = (*pal)[first_color + i].r * 255 / 63;
diff --git a/engines/mads/mads.h b/engines/mads/mads.h
index 4cd2df1f2a1..303ff92c9b6 100644
--- a/engines/mads/mads.h
+++ b/engines/mads/mads.h
@@ -187,6 +187,12 @@ public:
 		return false;
 	}
 	virtual bool hasMacintoshInterface() const { return false; }
+	virtual bool setMacintoshPalette(const RGBcolor *, int, int) {
+		return false;
+	}
+	virtual bool getMacintoshPalette(RGBcolor *, int, int) const {
+		return false;
+	}
 
 	virtual void player_keep_walking();
 
diff --git a/engines/mads/nebular/mac_nebular.cpp b/engines/mads/nebular/mac_nebular.cpp
index 3e4ceec5aa4..14401ab1f39 100644
--- a/engines/mads/nebular/mac_nebular.cpp
+++ b/engines/mads/nebular/mac_nebular.cpp
@@ -58,6 +58,7 @@ enum {
 	kMacDesktopSeparatorY = kMacDesktopSceneY + kMacSceneHeight,
 	kMacDesktopInterfaceY = kMacDesktopSeparatorY + 1,
 	kMacPanelPaletteColors = 10,
+	kMacScenePaletteStart = 16,
 	kMacNormalTextColor = 15,
 	kMacLeftSelectColor = 13,
 	kMacRightSelectColor = 14,
@@ -377,6 +378,7 @@ static bool isMacInterfaceScrollbarPixel(int x, int y) {
 
 MacNebular::MacNebular(RexNebularEngine &engine) :
 		_engine(engine), _useOriginalMenus(ConfMan.getBool("original_mac_menus")) {
+	memset(_palette, 0, sizeof(_palette));
 }
 
 MacNebular::~MacNebular() {
@@ -464,6 +466,36 @@ Common::Point MacNebular::gameToScreen(const Common::Point &point) const {
 		interfaceY + (point.y - 156) * 2);
 }
 
+void MacNebular::setPalette(const RGBcolor *palette, int firstColor,
+		int numColors) {
+	const int lastColor = MIN(firstColor + numColors,
+		Graphics::PALETTE_COUNT);
+	for (int color = firstColor; color < lastColor; ++color)
+		_palette[color] = palette[color];
+
+	const int sceneFirst = MAX<int>(firstColor, kMacScenePaletteStart);
+	if (sceneFirst >= lastColor)
+		return;
+
+	byte corrected[Graphics::PALETTE_SIZE];
+	for (int color = sceneFirst; color < lastColor; ++color) {
+		const int offset = (color - sceneFirst) * 3;
+		corrected[offset] = macGammaCorrect(palette[color].r);
+		corrected[offset + 1] = macGammaCorrect(palette[color].g);
+		corrected[offset + 2] = macGammaCorrect(palette[color].b);
+	}
+	g_system->getPaletteManager()->setPalette(corrected, sceneFirst,
+		lastColor - sceneFirst);
+}
+
+void MacNebular::getPalette(RGBcolor *palette, int firstColor,
+		int numColors) const {
+	const int lastColor = MIN(firstColor + numColors,
+		Graphics::PALETTE_COUNT);
+	for (int color = firstColor; color < lastColor; ++color)
+		palette[color] = _palette[color];
+}
+
 void MacNebular::presentScreen(int shakeOffset) {
 	const int sceneY = _useOriginalMenus ? kMacDesktopSceneY : 0;
 	const int interfaceY = _useOriginalMenus ?
@@ -739,6 +771,24 @@ bool RexNebularEngine::hasMacintoshInterface() const {
 	return _macNebular != nullptr;
 }
 
+bool RexNebularEngine::setMacintoshPalette(const RGBcolor *palette,
+		int firstColor, int numColors) {
+	if (!_macNebular)
+		return false;
+
+	_macNebular->setPalette(palette, firstColor, numColors);
+	return true;
+}
+
+bool RexNebularEngine::getMacintoshPalette(RGBcolor *palette,
+		int firstColor, int numColors) const {
+	if (!_macNebular)
+		return false;
+
+	_macNebular->getPalette(palette, firstColor, numColors);
+	return true;
+}
+
 bool RexNebularEngine::hasInterfaceAnimations() const {
 	// Macintosh CODE 7 stops loading an AA_INTERFACE controller before its
 	// DOS sprite-series list. The Mac resource set accordingly contains the
diff --git a/engines/mads/nebular/mac_nebular.h b/engines/mads/nebular/mac_nebular.h
index 79efd0ce051..10fdb830797 100644
--- a/engines/mads/nebular/mac_nebular.h
+++ b/engines/mads/nebular/mac_nebular.h
@@ -24,6 +24,7 @@
 
 #include "common/rect.h"
 #include "graphics/managed_surface.h"
+#include "mads/core/general.h"
 
 namespace Common {
 struct Event;
@@ -44,6 +45,7 @@ private:
 	Graphics::ManagedSurface _output;
 	Graphics::ManagedSurface _popup;
 	Common::Rect _popupRect;
+	RGBcolor _palette[256];
 	bool _useOriginalMenus;
 	bool _popupActive = false;
 	bool _layoutLogged = false;
@@ -58,6 +60,8 @@ public:
 	Common::Point screenToGame(const Common::Point &point) const;
 	Common::Point gameToScreen(const Common::Point &point) const;
 	bool handleMacEvent(Common::Event &event);
+	void setPalette(const RGBcolor *palette, int firstColor, int numColors);
+	void getPalette(RGBcolor *palette, int firstColor, int numColors) const;
 	void presentScreen(int shakeOffset);
 	void showPopup();
 	void hidePopup();
diff --git a/engines/mads/nebular/nebular.h b/engines/mads/nebular/nebular.h
index fb3a91d0ad2..a0b8049c805 100644
--- a/engines/mads/nebular/nebular.h
+++ b/engines/mads/nebular/nebular.h
@@ -78,6 +78,10 @@ public:
 	void onPopupDestroyed() override;
 	bool getInterfaceSentenceColors(byte &foreground, byte &shadow) const override;
 	bool hasMacintoshInterface() const override;
+	bool setMacintoshPalette(const RGBcolor *palette, int firstColor,
+		int numColors) override;
+	bool getMacintoshPalette(RGBcolor *palette, int firstColor,
+		int numColors) const override;
 };
 
 } // namespace RexNebular


Commit: e7bebb08216129bf8126196f16aca9842be6215e
    https://github.com/scummvm/scummvm/commit/e7bebb08216129bf8126196f16aca9842be6215e
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:48:38+10:00

Commit Message:
MADS: NEBULAR: Fix Macintosh object examination

Keep the Macintosh action sentence out of object close-ups and
preserve the scene grayscale ramp. Present restored room pixels before
fading back to color. Center native popups using the recovered CODE 9
rules.

Defer presentation when a popup is destroyed so the temporary object
examination buffer is never exposed before the room is restored.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/core/inter.cpp
    engines/mads/core/inter.h
    engines/mads/core/object.cpp
    engines/mads/core/popup.cpp
    engines/mads/nebular/mac_nebular.cpp


diff --git a/engines/mads/core/inter.cpp b/engines/mads/core/inter.cpp
index 5fa4dea397f..cee8e77a43c 100644
--- a/engines/mads/core/inter.cpp
+++ b/engines/mads/core/inter.cpp
@@ -138,6 +138,7 @@ int  picked_word = 0;           /* Word most recently picked.         */
 char inter_sentence[64];                /* Sentence building buffer            */
 int  inter_sentence_handle = -1;       /* Sentence message handle (for matte) */
 static int inter_sentence_shadow_handle = -1;
+static bool inter_macintosh_sentence_hidden = false;
 int  inter_sentence_changed = false;    /* Mark if sentence contents changed   */
 
 int  inter_look_around;                 /* "Look around" command            */
@@ -246,6 +247,7 @@ void init_inter() {
 	memset(inter_sentence, 0, sizeof(inter_sentence));
 	inter_sentence_handle = -1;
 	inter_sentence_shadow_handle = -1;
+	inter_macintosh_sentence_hidden = false;
 	inter_sentence_changed = false;
 	inter_look_around = 0;
 	inter_command = 0;
@@ -1956,6 +1958,29 @@ void inter_screen_update() {
 	}
 }
 
+void inter_hide_macintosh_sentence() {
+	bool changed = false;
+	inter_macintosh_sentence_hidden = true;
+	if (inter_sentence_handle >= 0) {
+		matte_clear_message(inter_sentence_handle);
+		inter_sentence_handle = -1;
+		changed = true;
+	}
+	if (inter_sentence_shadow_handle >= 0) {
+		matte_clear_message(inter_sentence_shadow_handle);
+		inter_sentence_shadow_handle = -1;
+		changed = true;
+	}
+	if (changed)
+		matte_frame(false, false);
+	inter_sentence_changed = true;
+}
+
+void inter_restore_macintosh_sentence() {
+	inter_macintosh_sentence_hidden = false;
+	inter_sentence_changed = true;
+}
+
 static void inter_exec_function(void (*(target))()) {
 	target();
 }
@@ -2130,7 +2155,7 @@ void inter_main_loop(int allow_input) {
 		}
 	}
 
-	if (inter_sentence_changed) {
+	if (inter_sentence_changed && !inter_macintosh_sentence_hidden) {
 		if (inter_sentence_handle >= 0) {
 			matte_clear_message(inter_sentence_handle);
 			inter_sentence_handle = -1;
diff --git a/engines/mads/core/inter.h b/engines/mads/core/inter.h
index e0c5e5cbd06..26cc2cb09b3 100644
--- a/engines/mads/core/inter.h
+++ b/engines/mads/core/inter.h
@@ -326,6 +326,9 @@ extern void inter_main_loop(int allow_input);
 
 extern void inter_screen_update();
 
+extern void inter_hide_macintosh_sentence();
+extern void inter_restore_macintosh_sentence();
+
 extern void inter_turn_off_object();
 extern void inter_spin_object(int object_id);
 extern int  inter_allocate_objects();
diff --git a/engines/mads/core/object.cpp b/engines/mads/core/object.cpp
index ba49f7d339d..cff3083b70a 100644
--- a/engines/mads/core/object.cpp
+++ b/engines/mads/core/object.cpp
@@ -215,6 +215,8 @@ int object_examine(int number, long message, int speech) {
 	RGBcolor top_eight[8];
 	SeriesPtr object_series = nullptr;
 	bool isRex = g_engine->getGameID() == GType_RexNebular;
+	bool isMacRex = isRex &&
+		g_engine->getPlatform() == Common::kPlatformMacintosh;
 
 	// Wait cursor
 	cursor_id = 2;
@@ -225,6 +227,8 @@ int object_examine(int number, long message, int speech) {
 
 	inter_turn_off_object();
 	inter_screen_update();
+	if (isMacRex)
+		inter_hide_macintosh_sentence();
 
 	memcpy(top_eight, &master_palette[248].r, 8 * sizeof(RGBcolor));
 
@@ -411,6 +415,12 @@ int object_examine(int number, long message, int speech) {
 		if (refresh_flag) mouse_refresh_done();
 		mouse_thaw();
 
+		// Macintosh Rex composes the game screen into a separate output
+		// surface. Present the restored pixels before the palette-only fade;
+		// otherwise that surface still contains the object close-up.
+		if (isMacRex)
+			g_engine->updateDisplay();
+
 		// Finally, we can fade back to our original palette.
 		magic_fade_from_grey((RGBcolor *)greyed_master_palette, master_palette,
 			KERNEL_RESERVED_LOW_COLORS, num_colors,
@@ -437,6 +447,8 @@ int object_examine(int number, long message, int speech) {
 
 	tile_pan(&depth_map, picture_view_x, picture_view_y);
 	kernel_force_refresh();
+	if (isMacRex)
+		inter_restore_macintosh_sentence();
 	inter_spin_object(inven[active_inven]);
 
 	return restored_screen;
diff --git a/engines/mads/core/popup.cpp b/engines/mads/core/popup.cpp
index f60e3694704..86a6993dad3 100644
--- a/engines/mads/core/popup.cpp
+++ b/engines/mads/core/popup.cpp
@@ -133,7 +133,10 @@ int popup_create(int horiz_pieces, int x, int y) {
 		box->text_xs = middle_width;
 		box->text_width = horiz_pieces * 2;
 
-		RexNebular::popup_setup_cycle();
+		// Macintosh Rex draws popups in a separate native-style surface. Its
+		// colors must not replace the grayscale ramp used by the game scene.
+		if (g_engine->getPlatform() != Common::kPlatformMacintosh)
+			RexNebular::popup_setup_cycle();
 
 	} else {
 		middle_width = pop_xs(POPUP_UPPER_CENTER) + ((pop_xs(POPUP_TOP) << 1) * horiz_pieces);
diff --git a/engines/mads/nebular/mac_nebular.cpp b/engines/mads/nebular/mac_nebular.cpp
index 14401ab1f39..7e1eab1bde0 100644
--- a/engines/mads/nebular/mac_nebular.cpp
+++ b/engines/mads/nebular/mac_nebular.cpp
@@ -59,11 +59,12 @@ enum {
 	kMacDesktopInterfaceY = kMacDesktopSeparatorY + 1,
 	kMacPanelPaletteColors = 10,
 	kMacScenePaletteStart = 16,
+	kMacPopupGray = 39,
+	kMacPopupColor = 12,
 	kMacNormalTextColor = 15,
 	kMacLeftSelectColor = 13,
 	kMacRightSelectColor = 14,
-	kMacBlackColor = kMacRightSelectColor,
-	kMacPopupColor = REX_DIALOG_FE_COLOR
+	kMacBlackColor = kMacRightSelectColor
 };
 
 struct MacPopupLine {
@@ -108,6 +109,17 @@ static void setMacInterfacePalette(const MacResourceProvider *resources) {
 			kMacPanelPaletteColors);
 	}
 
+	const byte popupGray = macGammaCorrect(kMacPopupGray);
+	byte popupPalette[3] = { popupGray, popupGray, popupGray };
+	master_palette[kMacPopupColor].r =
+		macPaletteComponentToSixBit(popupGray);
+	master_palette[kMacPopupColor].g =
+		macPaletteComponentToSixBit(popupGray);
+	master_palette[kMacPopupColor].b =
+		macPaletteComponentToSixBit(popupGray);
+	g_system->getPaletteManager()->setPalette(popupPalette,
+		kMacPopupColor, 1);
+
 	byte normalR = 25;
 	byte normalG = 48;
 	byte normalB = 51;
@@ -660,8 +672,7 @@ void MacNebular::showPopup() {
 			width - 20, lines);
 
 	const int popupAreaY = _useOriginalMenus ? kMacDesktopSceneY : 0;
-	const int popupAreaHeight = _useOriginalMenus ?
-		kMacSceneHeight : kMacLegacyScreenHeight;
+	const int popupAreaHeight = kMacLegacyScreenHeight;
 	const int height = CLIP<int>((int)lines.size() * 12 + 20, 20,
 		popupAreaHeight - 2);
 	_popup.create(width, height, Graphics::PixelFormat::createFormatCLUT8());
@@ -696,11 +707,17 @@ void MacNebular::showPopup() {
 		}
 	}
 
+	// CODE 9 centers a requested centered popup in the 640x400 content
+	// area. A popup with an explicit vertical position, as used by object
+	// examination, starts three pixels below the content midpoint.
+	const int logicalCenteredY = (video_y - box->ys) / 2;
+	const int popupY = box->y == logicalCenteredY ?
+		(popupAreaHeight - height) / 2 : popupAreaHeight / 2 + 3;
 	_popupRect = Common::Rect(
 		(kMacScreenWidth - width) / 2,
-		popupAreaY + (popupAreaHeight - height) / 2,
+		popupAreaY + popupY,
 		(kMacScreenWidth + width) / 2,
-		popupAreaY + (popupAreaHeight + height) / 2);
+		popupAreaY + popupY + height);
 	_popupActive = true;
 	presentScreen(0);
 }
@@ -711,7 +728,10 @@ void MacNebular::hidePopup() {
 
 	_popupActive = false;
 	_popup.free();
-	presentScreen(0);
+	// The popup owner resumes drawing after destruction. In particular,
+	// object examination still has a temporary work screen here and restores
+	// the room immediately afterwards, so presenting now would expose that
+	// intermediate buffer.
 }
 
 // -------------------------------------------------------------------------


Commit: 7da7ec62dba232f90e9fd1534be87bdb66c66096
    https://github.com/scummvm/scummvm/commit/7da7ec62dba232f90e9fd1534be87bdb66c66096
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:48:38+10:00

Commit Message:
MADS: Handle AnimView resource terminators

Macintosh animation playlists use a line containing three asterisks to
terminate the list. Stop parsing at that marker instead of treating it as
an animation filename.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/animview/functions.cpp


diff --git a/engines/mads/animview/functions.cpp b/engines/mads/animview/functions.cpp
index 9fc97f36dc0..eb1d89cb3e6 100644
--- a/engines/mads/animview/functions.cpp
+++ b/engines/mads/animview/functions.cpp
@@ -159,6 +159,8 @@ void read_resource(Common::SeekableReadStream *src) {
 	while (!src->eos()) {
 		Common::String line = src->readLine();
 		line.trim();
+		if (line == "***")
+			break;
 		if (line.empty() || src->eos())
 			continue;
 


Commit: 92ce40af41c9efdbacacd0591237e8b681cd0057
    https://github.com/scummvm/scummvm/commit/92ce40af41c9efdbacacd0591237e8b681cd0057
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:48:38+10:00

Commit Message:
MADS: NEBULAR: Restore Macintosh difficulty dialog

Read DLOG and DITL 2500 for the native three-choice dialog. Center the
dialog in both Macintosh compositions without routing through a generic
ScummVM chooser.

Make the mouse cursor visible while the modal dialog is active and restore
its prior state afterward. Do not ask for difficulty before restoring a
save, because the savegame already contains that choice.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/nebular/mac_menus.cpp
    engines/mads/nebular/mac_menus.h
    engines/mads/nebular/mac_nebular.cpp
    engines/mads/nebular/mac_nebular.h
    engines/mads/nebular/main.cpp
    engines/mads/nebular/nebular.h


diff --git a/engines/mads/nebular/mac_menus.cpp b/engines/mads/nebular/mac_menus.cpp
index 1623185ccb6..cbaf0c54282 100644
--- a/engines/mads/nebular/mac_menus.cpp
+++ b/engines/mads/nebular/mac_menus.cpp
@@ -24,12 +24,13 @@
 #include "common/events.h"
 #include "common/stream.h"
 #include "common/system.h"
-#include "common/translation.h"
+#include "graphics/cursorman.h"
+#include "graphics/font.h"
 #include "graphics/macgui/macmenu.h"
 #include "graphics/macgui/macwindowmanager.h"
 #include "graphics/managed_surface.h"
 #include "graphics/paletteman.h"
-#include "gui/chooser.h"
+#include "graphics/primitives.h"
 #include "mads/core/config.h"
 #include "mads/core/game.h"
 #include "mads/core/kernel.h"
@@ -45,6 +46,11 @@ namespace RexNebular {
 namespace {
 
 enum {
+	kDifficultyDialog = 2500,
+	kDialogButton = 4,
+	kDialogRadioButton = 6,
+	kDialogStaticText = 8,
+
 	kFileMenu = 1,
 	kOptionsMenu = 3,
 	kWindowMenu = 4,
@@ -62,6 +68,19 @@ enum {
 	kStoryNice = (102 << 16) | 1
 };
 
+struct DialogResourceItem {
+	Common::Rect bounds;
+	Common::String text;
+	byte type = 0;
+	bool enabled = false;
+};
+
+struct DialogResource {
+	Common::Rect bounds;
+	uint16 itemList = 0;
+	Common::Array<DialogResourceItem> items;
+};
+
 struct MenuResourceItem {
 	Common::String text;
 	byte key = 0;
@@ -105,6 +124,138 @@ bool readMenuResource(Common::SeekableReadStream &stream, MenuResource &resource
 	return false;
 }
 
+bool readDialogResource(Common::SeekableReadStream &stream,
+		DialogResource &resource) {
+	if (stream.size() < 21)
+		return false;
+
+	resource.bounds.top = stream.readUint16BE();
+	resource.bounds.left = stream.readUint16BE();
+	resource.bounds.bottom = stream.readUint16BE();
+	resource.bounds.right = stream.readUint16BE();
+	stream.skip(10); // Procedure, visibility, go-away flag, and refCon.
+	resource.itemList = stream.readUint16BE();
+	/* Common::String title = */ stream.readPascalString();
+	return !stream.err() && resource.bounds.isValidRect();
+}
+
+bool readDialogItems(Common::SeekableReadStream &stream,
+		DialogResource &resource) {
+	if (stream.size() < 2)
+		return false;
+
+	const uint itemCount = stream.readUint16BE() + 1;
+	for (uint itemNumber = 0; itemNumber < itemCount; ++itemNumber) {
+		if (stream.pos() + 14 > stream.size())
+			return false;
+
+		DialogResourceItem item;
+		stream.skip(4); // Placeholder for a handle or procedure pointer.
+		item.bounds.top = stream.readUint16BE();
+		item.bounds.left = stream.readUint16BE();
+		item.bounds.bottom = stream.readUint16BE();
+		item.bounds.right = stream.readUint16BE();
+		const byte rawType = stream.readByte();
+		const uint length = stream.readByte();
+		if (stream.pos() + length > stream.size())
+			return false;
+
+		item.type = rawType & 0x7f;
+		item.enabled = (rawType & 0x80) == 0;
+		if (item.type == kDialogButton ||
+				item.type == kDialogRadioButton ||
+				item.type == kDialogStaticText) {
+			for (uint i = 0; i < length; ++i)
+				item.text += (char)stream.readByte();
+			resource.items.push_back(item);
+		} else {
+			stream.skip(length);
+		}
+
+		if (length & 1)
+			stream.skip(1);
+	}
+
+	return !stream.err();
+}
+
+Common::Rect getDialogItemBounds(const DialogResource &dialog,
+		const DialogResourceItem &item) {
+	Common::Rect bounds(item.bounds);
+	bounds.translate(dialog.bounds.left, dialog.bounds.top);
+	return bounds;
+}
+
+void drawDifficultyDialog(Graphics::ManagedSurface &screen,
+		Graphics::MacWindowManager &windowManager,
+		const Graphics::Font &font, const DialogResource &dialog,
+		int selectedRadio, bool pressedButton) {
+	Graphics::Primitives &primitives = windowManager.getDrawPrimitives();
+	Graphics::MacPlotData plot(&screen, nullptr,
+		&windowManager.getBuiltinPatterns(), 1, 0, 0,
+		Common::Point(1, 1), windowManager._colorWhite, false);
+	primitives.drawFilledRect1(dialog.bounds, windowManager._colorWhite, &plot);
+	primitives.drawRect1(dialog.bounds, windowManager._colorBlack, &plot);
+
+	int radio = 0;
+	for (uint i = 0; i < dialog.items.size(); ++i) {
+		const DialogResourceItem &item = dialog.items[i];
+		const Common::Rect bounds = getDialogItemBounds(dialog, item);
+		const int textY = bounds.top +
+			(bounds.height() - font.getFontHeight()) / 2;
+
+		switch (item.type) {
+		case kDialogButton: {
+			primitives.drawRect1(bounds, windowManager._colorBlack, &plot);
+			if (pressedButton) {
+				Common::Rect inside(bounds);
+				inside.grow(-2);
+				primitives.drawFilledRect1(inside,
+					windowManager._colorBlack, &plot);
+			}
+			const uint32 color = pressedButton ? windowManager._colorWhite :
+				windowManager._colorBlack;
+			const int textX = bounds.left +
+				(bounds.width() - font.getStringWidth(item.text)) / 2;
+			font.drawString(&screen, item.text, textX, textY,
+				bounds.width(), color);
+			break;
+		}
+		case kDialogRadioButton: {
+			const int circleTop = bounds.top + (bounds.height() - 12) / 2;
+			primitives.drawEllipse(bounds.left, circleTop,
+				bounds.left + 11, circleTop + 11,
+				windowManager._colorBlack, false, &plot);
+			if (radio == selectedRadio)
+				primitives.drawEllipse(bounds.left + 4, circleTop + 4,
+					bounds.left + 7, circleTop + 7,
+					windowManager._colorBlack, true, &plot);
+			font.drawString(&screen, item.text, bounds.left + 17, textY,
+				bounds.width() - 17, windowManager._colorBlack);
+			++radio;
+			break;
+		}
+		case kDialogStaticText:
+			font.drawString(&screen, item.text, bounds.left, textY,
+				bounds.width(), windowManager._colorBlack);
+			break;
+		default:
+			break;
+		}
+	}
+}
+
+int difficultyForRadio(int radio) {
+	switch (radio) {
+	case 0:
+		return DIFFICULTY_EASY;
+	case 2:
+		return DIFFICULTY_HARD;
+	default:
+		return DIFFICULTY_MEDIUM;
+	}
+}
+
 } // namespace
 
 MacNebularMenu::MacNebularMenu(RexNebularEngine &engine,
@@ -116,7 +267,10 @@ MacNebularMenu::~MacNebularMenu() {
 	delete _windowManager;
 }
 
-bool MacNebularMenu::initialize() {
+bool MacNebularMenu::initializeWindowManager() {
+	if (_windowManager)
+		return true;
+
 	// MacWindowManager installs its default palette during construction. Save
 	// and restore the active MADS palette before it can reach the backend.
 	byte palette[256 * 3];
@@ -133,6 +287,15 @@ bool MacNebularMenu::initialize() {
 	_windowManager->passPalette(palette, 256);
 	memcpy(_palette, palette, sizeof(_palette));
 	_paletteValid = true;
+	return true;
+}
+
+bool MacNebularMenu::initialize() {
+	if (!initializeWindowManager())
+		return false;
+	if (_menu)
+		return true;
+
 	_menu = _windowManager->addMenu();
 	_menu->setCommandsCallback(menuCallback, this);
 
@@ -352,36 +515,155 @@ void MacNebularMenu::menuCallback(int commandId, Common::String &, void *data) {
 	menus->_menu->closeMenu();
 }
 
-void selectMacintoshDifficulty() {
+int MacNebularMenu::runDifficultyDialog() {
+	if (!initializeWindowManager())
+		return -1;
+
+	Common::SeekableReadStream *dialogStream = _resources.openResource(
+		MacResourceProvider::kApplicationContainer,
+		MKTAG('D', 'L', 'O', 'G'), kDifficultyDialog);
+	if (!dialogStream)
+		return -1;
+
+	DialogResource dialog;
+	const bool validDialog = readDialogResource(*dialogStream, dialog);
+	delete dialogStream;
+	if (!validDialog || dialog.itemList != kDifficultyDialog)
+		return -1;
+	dialog.bounds.moveTo((_screen.w - dialog.bounds.width()) / 2,
+		(_screen.h - dialog.bounds.height()) / 2);
+
+	Common::SeekableReadStream *itemsStream = _resources.openResource(
+		MacResourceProvider::kApplicationContainer,
+		MKTAG('D', 'I', 'T', 'L'), dialog.itemList);
+	if (!itemsStream)
+		return -1;
+
+	const bool validItems = readDialogItems(*itemsStream, dialog);
+	delete itemsStream;
+	const Graphics::Font *font = _resources.getDialogFont();
+	if (!validItems || !font)
+		return -1;
+
+	Graphics::ManagedSurface saved;
+	saved.create(dialog.bounds.width(), dialog.bounds.height(), _screen.format);
+	saved.copyRectToSurface(_screen.getBasePtr(dialog.bounds.left,
+		dialog.bounds.top), _screen.pitch, 0, 0,
+		dialog.bounds.width(), dialog.bounds.height());
+
+	int selectedRadio = 1;
+	bool pressedButton = false;
+	bool done = false;
+	bool quit = false;
+	bool redraw = true;
+	const bool cursorWasVisible = CursorMan.isVisible();
+	_windowManager->clearHandlingWidgets();
+	_windowManager->pushCursor(Graphics::kMacCursorArrow);
+	CursorMan.showMouse(true);
+
+	while (!done) {
+		if (redraw) {
+			drawDifficultyDialog(_screen, *_windowManager, *font, dialog,
+				selectedRadio, pressedButton);
+			g_system->copyRectToScreen(_screen.getBasePtr(dialog.bounds.left,
+				dialog.bounds.top), _screen.pitch, dialog.bounds.left,
+				dialog.bounds.top, dialog.bounds.width(), dialog.bounds.height());
+			g_system->updateScreen();
+			redraw = false;
+		}
+
+		Common::Event event;
+		while (!done && g_system->getEventManager()->pollEvent(event)) {
+			switch (event.type) {
+			case Common::EVENT_QUIT:
+				quit = true;
+				done = true;
+				break;
+			case Common::EVENT_KEYDOWN:
+				if (event.kbd.keycode == Common::KEYCODE_RETURN ||
+						event.kbd.keycode == Common::KEYCODE_KP_ENTER) {
+					done = true;
+				} else if (event.kbd.keycode == Common::KEYCODE_UP) {
+					selectedRadio = MAX(0, selectedRadio - 1);
+					redraw = true;
+				} else if (event.kbd.keycode == Common::KEYCODE_DOWN) {
+					selectedRadio = MIN(2, selectedRadio + 1);
+					redraw = true;
+				}
+				break;
+			case Common::EVENT_LBUTTONDOWN: {
+				int radio = 0;
+				for (uint i = 0; i < dialog.items.size(); ++i) {
+					const DialogResourceItem &item = dialog.items[i];
+					const Common::Rect bounds = getDialogItemBounds(dialog, item);
+					if (item.type == kDialogRadioButton) {
+						if (item.enabled && bounds.contains(event.mouse.x,
+								event.mouse.y)) {
+							selectedRadio = radio;
+							redraw = true;
+						}
+						++radio;
+					} else if (item.type == kDialogButton && item.enabled &&
+							bounds.contains(event.mouse.x, event.mouse.y)) {
+						pressedButton = true;
+						redraw = true;
+					}
+				}
+				break;
+			}
+			case Common::EVENT_LBUTTONUP:
+				if (pressedButton) {
+					for (uint i = 0; i < dialog.items.size(); ++i) {
+						const DialogResourceItem &item = dialog.items[i];
+						if (item.type == kDialogButton && item.enabled &&
+								getDialogItemBounds(dialog, item).contains(
+									event.mouse.x, event.mouse.y))
+							done = true;
+					}
+				}
+				pressedButton = false;
+				redraw = true;
+				break;
+			default:
+				break;
+			}
+		}
+
+		if (!done)
+			g_system->delayMillis(10);
+	}
+
+	_screen.copyRectToSurface(saved.getPixels(), saved.pitch,
+		dialog.bounds.left, dialog.bounds.top,
+		dialog.bounds.width(), dialog.bounds.height());
+	g_system->copyRectToScreen(_screen.getBasePtr(dialog.bounds.left,
+		dialog.bounds.top), _screen.pitch, dialog.bounds.left,
+		dialog.bounds.top, dialog.bounds.width(), dialog.bounds.height());
+	g_system->updateScreen();
+	_windowManager->popCursor();
+	CursorMan.showMouse(cursorWasVisible);
+	if (quit)
+		_engine.quitGame();
+
+	return difficultyForRadio(selectedRadio);
+}
+
+void selectMacintoshDifficulty(MacNebularMenu *menus) {
 	const int configuredDifficulty = ConfMan.getInt("difficulty");
 	if (configuredDifficulty >= DIFFICULTY_HARD && configuredDifficulty <= DIFFICULTY_EASY) {
 		game.difficulty = configuredDifficulty;
 		return;
 	}
 
-	Common::U32StringArray choices;
-	choices.push_back(_("Novice - Easy"));
-	choices.push_back(_("Advanced - Difficult"));
-	choices.push_back(_("Expert - Very Difficult"));
-	// The generic browser layout is tied to the launcher's game-list widget.
-	// Use its screen-based counterpart because this chooser runs in-engine.
-	GUI::ChooserDialog dialog(_("Select a Difficulty Level:"), "FileBrowser");
-	dialog.setList(choices);
-
-	switch (dialog.runModal()) {
-	case 0:
-		game.difficulty = DIFFICULTY_EASY;
-		break;
-	case 1:
-		game.difficulty = DIFFICULTY_MEDIUM;
-		break;
-	case 2:
-		game.difficulty = DIFFICULTY_HARD;
-		break;
-	default:
-		game.difficulty = DIFFICULTY_MEDIUM;
-		break;
+	const int nativeDifficulty = menus ? menus->runDifficultyDialog() :
+		DIFFICULTY_MEDIUM;
+	if (nativeDifficulty >= DIFFICULTY_HARD &&
+			nativeDifficulty <= DIFFICULTY_EASY) {
+		game.difficulty = nativeDifficulty;
+		return;
 	}
+
+	game.difficulty = DIFFICULTY_MEDIUM;
 }
 
 void macintoshGameMenu() {
@@ -390,7 +672,7 @@ void macintoshGameMenu() {
 	kernel.activate_menu = GAME_NO_MENU;
 
 	if (requestedMenu == GAME_DIFFICULTY_MENU)
-		selectMacintoshDifficulty();
+		((RexNebularEngine *)g_engine)->selectMacintoshDifficulty();
 	else if (requestedMenu != GAME_NO_MENU)
 		g_engine->openMainMenuDialog();
 }
diff --git a/engines/mads/nebular/mac_menus.h b/engines/mads/nebular/mac_menus.h
index 62ff1902f28..08c4148a105 100644
--- a/engines/mads/nebular/mac_menus.h
+++ b/engines/mads/nebular/mac_menus.h
@@ -55,6 +55,7 @@ private:
 	int _pendingCommand = -1;
 
 	static void menuCallback(int commandId, Common::String &name, void *data);
+	bool initializeWindowManager();
 	bool loadMenuResource(uint16 resourceID,
 		Graphics::MacMenuSubMenu *parent = nullptr, int parentItem = -1);
 	Graphics::MacMenuItem *getMenuItem(int menu, int item) const;
@@ -72,9 +73,10 @@ public:
 	bool initialize();
 	bool processEvent(Common::Event &event);
 	void draw();
+	int runDifficultyDialog();
 };
 
-void selectMacintoshDifficulty();
+void selectMacintoshDifficulty(MacNebularMenu *menus);
 void macintoshGameMenu();
 
 } // namespace RexNebular
diff --git a/engines/mads/nebular/mac_nebular.cpp b/engines/mads/nebular/mac_nebular.cpp
index 7e1eab1bde0..7bd106fd16d 100644
--- a/engines/mads/nebular/mac_nebular.cpp
+++ b/engines/mads/nebular/mac_nebular.cpp
@@ -424,11 +424,11 @@ bool MacNebular::initResources() {
 	_engine._soundManager = new Sound::MacSoundManager(
 		_engine._mixer, _engine._soundFlag, _resources);
 
+	_menus = new MacNebularMenu(_engine, *_resources, _output);
 	if (_useOriginalMenus) {
-		_menus = new MacNebularMenu(_engine, *_resources, _output);
 		if (!_menus->initialize()) {
 			delete _menus;
-			_menus = nullptr;
+			_menus = new MacNebularMenu(_engine, *_resources, _output);
 		}
 	}
 	return true;
@@ -446,6 +446,10 @@ void MacNebular::applyGameSettings() {
 	}
 }
 
+void MacNebular::selectDifficulty() {
+	selectMacintoshDifficulty(_menus);
+}
+
 Common::Point MacNebular::screenToGame(const Common::Point &point) const {
 	const int sceneY = _useOriginalMenus ? kMacDesktopSceneY : 0;
 	const int interfaceY = _useOriginalMenus ?
@@ -613,7 +617,7 @@ void MacNebular::presentScreen(int shakeOffset) {
 		_layoutLogged = true;
 	}
 
-	if (_menus)
+	if (_useOriginalMenus && _menus)
 		_menus->draw();
 
 	g_system->copyRectToScreen(_output.getPixels(), _output.pitch,
@@ -623,7 +627,7 @@ void MacNebular::presentScreen(int shakeOffset) {
 }
 
 bool MacNebular::handleMacEvent(Common::Event &event) {
-	return _menus && _menus->processEvent(event);
+	return _useOriginalMenus && _menus && _menus->processEvent(event);
 }
 
 void MacNebular::showPopup() {
@@ -765,6 +769,11 @@ bool RexNebularEngine::handleMacEvent(Common::Event &event) {
 	return _macNebular && _macNebular->handleMacEvent(event);
 }
 
+void RexNebularEngine::selectMacintoshDifficulty() {
+	if (_macNebular)
+		_macNebular->selectDifficulty();
+}
+
 bool RexNebularEngine::drawPopup() {
 	if (!_macNebular)
 		return false;
diff --git a/engines/mads/nebular/mac_nebular.h b/engines/mads/nebular/mac_nebular.h
index 10fdb830797..f7fa0650527 100644
--- a/engines/mads/nebular/mac_nebular.h
+++ b/engines/mads/nebular/mac_nebular.h
@@ -57,6 +57,7 @@ public:
 	void initGraphics();
 	bool initResources();
 	void applyGameSettings();
+	void selectDifficulty();
 	Common::Point screenToGame(const Common::Point &point) const;
 	Common::Point gameToScreen(const Common::Point &point) const;
 	bool handleMacEvent(Common::Event &event);
diff --git a/engines/mads/nebular/main.cpp b/engines/mads/nebular/main.cpp
index 0a41bde62e7..65c62d4e951 100644
--- a/engines/mads/nebular/main.cpp
+++ b/engines/mads/nebular/main.cpp
@@ -47,7 +47,7 @@
 #include "mads/nebular/main_menu.h"
 #include "mads/nebular/mac_menus.h"
 #include "mads/nebular/menus.h"
-#include "mads/mads.h"
+#include "mads/nebular/nebular.h"
 
 namespace MADS {
 namespace RexNebular {
@@ -257,8 +257,9 @@ static void game_main(int argc, const char **argv) {
 	game_cold_data_init();
 	main_cold_data_init();
 	if (g_engine->getPlatform() == Common::kPlatformMacintosh &&
+			savegame_slot == -1 &&
 			!ConfMan.hasKey("save_slot"))
-		selectMacintoshDifficulty();
+		((RexNebularEngine *)g_engine)->selectMacintoshDifficulty();
 	global_load_config_parameters();
 
 	if (argc >= 2) {
diff --git a/engines/mads/nebular/nebular.h b/engines/mads/nebular/nebular.h
index a0b8049c805..2a21c41f39a 100644
--- a/engines/mads/nebular/nebular.h
+++ b/engines/mads/nebular/nebular.h
@@ -61,6 +61,7 @@ public:
 
 	Common::Error run() override;
 	void syncRoom(Common::Serializer &s) override;
+	void selectMacintoshDifficulty();
 
 	int main_copy_verify() override;
 	void global_init_code() override;


Commit: 066be0fb3152319f040aa1cd85af61c28d1b58f1
    https://github.com/scummvm/scummvm/commit/066be0fb3152319f040aa1cd85af61c28d1b58f1
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:48:38+10:00

Commit Message:
MADS: NEBULAR: Restore Macintosh outer menu

Reuse the room-990 controller matched by Macintosh CODE 133 and present
its full 320x200 surface in the native desktop composition. Translate
input through that window and apply the recovered temporary menu gating.

Use the Macintosh cursor provider instead of the DOS menu bitmap. Present
top-level AnimView and TextView sequences as complete 320x200 frames and
let them install their complete palettes.

Map the native intro and credit scripts, resume the newest save directly,
and offer the ScummVM load chooser when none is available. Leave the game
directly on Exit instead of trying to play the unavailable DOS advert.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/nebular/mac_menus.cpp
    engines/mads/nebular/mac_menus.h
    engines/mads/nebular/mac_nebular.cpp
    engines/mads/nebular/mac_nebular.h
    engines/mads/nebular/mac_resources.cpp
    engines/mads/nebular/main.cpp
    engines/mads/nebular/main_menu.cpp
    engines/mads/nebular/nebular.h


diff --git a/engines/mads/nebular/mac_menus.cpp b/engines/mads/nebular/mac_menus.cpp
index cbaf0c54282..e36b765c680 100644
--- a/engines/mads/nebular/mac_menus.cpp
+++ b/engines/mads/nebular/mac_menus.cpp
@@ -31,6 +31,7 @@
 #include "graphics/managed_surface.h"
 #include "graphics/paletteman.h"
 #include "graphics/primitives.h"
+#include "gui/saveload.h"
 #include "mads/core/config.h"
 #include "mads/core/game.h"
 #include "mads/core/kernel.h"
@@ -418,6 +419,16 @@ void MacNebularMenu::updateState() {
 	setItemState(getMenuItem(kWindowMenu, 0), false, false);
 	setItemState(getMenuItem(kWindowMenu, 1), false, false);
 	setItemState(getMenuItem(kWindowMenu, 2), true, true);
+
+	if (_outerMenuActive) {
+		// CODE 133 disables New, Resume, Open, Save, and Save As while
+		// the native room-990 controller owns the application window.
+		setItemState(getMenuItem(kFileMenu, 0), false, false);
+		setItemState(getMenuItem(kFileMenu, 1), false, false);
+		setItemState(getMenuItem(kFileMenu, 2), false, false);
+		setItemState(getMenuItem(kFileMenu, 4), false, false);
+		setItemState(getMenuItem(kFileMenu, 5), false, false);
+	}
 }
 
 void MacNebularMenu::dispatchCommand(int commandId) {
@@ -509,6 +520,13 @@ void MacNebularMenu::draw() {
 	_menu->draw(&_screen, true);
 }
 
+byte MacNebularMenu::getBlackColor() {
+	if (!initializeWindowManager())
+		return 0;
+	syncPalette();
+	return (byte)_windowManager->_colorBlack;
+}
+
 void MacNebularMenu::menuCallback(int commandId, Common::String &, void *data) {
 	MacNebularMenu *menus = (MacNebularMenu *)data;
 	menus->_pendingCommand = commandId;
@@ -648,6 +666,11 @@ int MacNebularMenu::runDifficultyDialog() {
 	return difficultyForRadio(selectedRadio);
 }
 
+int MacNebularMenu::selectResumeSlot() {
+	GUI::SaveLoadChooser dialog(false);
+	return dialog.runModalWithCurrentTarget();
+}
+
 void selectMacintoshDifficulty(MacNebularMenu *menus) {
 	const int configuredDifficulty = ConfMan.getInt("difficulty");
 	if (configuredDifficulty >= DIFFICULTY_HARD && configuredDifficulty <= DIFFICULTY_EASY) {
diff --git a/engines/mads/nebular/mac_menus.h b/engines/mads/nebular/mac_menus.h
index 08c4148a105..7be6a6a7470 100644
--- a/engines/mads/nebular/mac_menus.h
+++ b/engines/mads/nebular/mac_menus.h
@@ -52,6 +52,7 @@ private:
 	Graphics::MacMenu *_menu = nullptr;
 	byte _palette[256 * 3] = {};
 	bool _paletteValid = false;
+	bool _outerMenuActive = false;
 	int _pendingCommand = -1;
 
 	static void menuCallback(int commandId, Common::String &name, void *data);
@@ -73,7 +74,10 @@ public:
 	bool initialize();
 	bool processEvent(Common::Event &event);
 	void draw();
+	byte getBlackColor();
+	void setOuterMenuActive(bool active) { _outerMenuActive = active; }
 	int runDifficultyDialog();
+	int selectResumeSlot();
 };
 
 void selectMacintoshDifficulty(MacNebularMenu *menus);
diff --git a/engines/mads/nebular/mac_nebular.cpp b/engines/mads/nebular/mac_nebular.cpp
index 7bd106fd16d..8c7655bf634 100644
--- a/engines/mads/nebular/mac_nebular.cpp
+++ b/engines/mads/nebular/mac_nebular.cpp
@@ -55,6 +55,7 @@ enum {
 	kMacDesktopHeight = 480,
 	kMacMenuBarHeight = 20,
 	kMacDesktopSceneY = kMacMenuBarHeight + 20,
+	kMacFullFrameHeight = 400,
 	kMacDesktopSeparatorY = kMacDesktopSceneY + kMacSceneHeight,
 	kMacDesktopInterfaceY = kMacDesktopSeparatorY + 1,
 	kMacPanelPaletteColors = 10,
@@ -450,7 +451,25 @@ void MacNebular::selectDifficulty() {
 	selectMacintoshDifficulty(_menus);
 }
 
+int MacNebular::selectResumeSlot() {
+	return _menus ? _menus->selectResumeSlot() : -1;
+}
+
+void MacNebular::setOuterMenuActive(bool active) {
+	setFullFrameActive(active);
+	if (_menus)
+		_menus->setOuterMenuActive(active);
+}
+
 Common::Point MacNebular::screenToGame(const Common::Point &point) const {
+	if (_fullFrameActive) {
+		const int frameY = _useOriginalMenus ? kMacDesktopSceneY : 0;
+		if (point.y >= frameY && point.y < frameY + kMacFullFrameHeight)
+			return Common::Point(CLIP<int>(point.x / 2, 0, 319),
+				(point.y - frameY) / 2);
+		return Common::Point(-1, -1);
+	}
+
 	const int sceneY = _useOriginalMenus ? kMacDesktopSceneY : 0;
 	const int interfaceY = _useOriginalMenus ?
 		kMacDesktopInterfaceY : kMacSceneHeight;
@@ -472,6 +491,12 @@ Common::Point MacNebular::screenToGame(const Common::Point &point) const {
 }
 
 Common::Point MacNebular::gameToScreen(const Common::Point &point) const {
+	if (_fullFrameActive) {
+		const int frameY = _useOriginalMenus ? kMacDesktopSceneY : 0;
+		return Common::Point(point.x * 2,
+			frameY + point.y * 2);
+	}
+
 	const int sceneY = _useOriginalMenus ? kMacDesktopSceneY : 0;
 	const int interfaceY = _useOriginalMenus ?
 		kMacDesktopInterfaceY : kMacSceneHeight;
@@ -489,19 +514,22 @@ void MacNebular::setPalette(const RGBcolor *palette, int firstColor,
 	for (int color = firstColor; color < lastColor; ++color)
 		_palette[color] = palette[color];
 
-	const int sceneFirst = MAX<int>(firstColor, kMacScenePaletteStart);
-	if (sceneFirst >= lastColor)
+	// Full-frame viewers own the complete palette. Gameplay keeps the low
+	// colors available for the native interface.
+	const int outputFirst = _fullFrameActive ? firstColor :
+		MAX<int>(firstColor, kMacScenePaletteStart);
+	if (outputFirst >= lastColor)
 		return;
 
 	byte corrected[Graphics::PALETTE_SIZE];
-	for (int color = sceneFirst; color < lastColor; ++color) {
-		const int offset = (color - sceneFirst) * 3;
+	for (int color = outputFirst; color < lastColor; ++color) {
+		const int offset = (color - outputFirst) * 3;
 		corrected[offset] = macGammaCorrect(palette[color].r);
 		corrected[offset + 1] = macGammaCorrect(palette[color].g);
 		corrected[offset + 2] = macGammaCorrect(palette[color].b);
 	}
-	g_system->getPaletteManager()->setPalette(corrected, sceneFirst,
-		lastColor - sceneFirst);
+	g_system->getPaletteManager()->setPalette(corrected, outputFirst,
+		lastColor - outputFirst);
 }
 
 void MacNebular::getPalette(RGBcolor *palette, int firstColor,
@@ -513,6 +541,35 @@ void MacNebular::getPalette(RGBcolor *palette, int firstColor,
 }
 
 void MacNebular::presentScreen(int shakeOffset) {
+	if (_fullFrameActive) {
+		const int frameY = _useOriginalMenus ? kMacDesktopSceneY : 0;
+		const byte frameBlack = _useOriginalMenus && _menus ?
+			_menus->getBlackColor() : 0;
+		_output.fillRect(_output.getBounds(), frameBlack);
+		for (int y = 0; y < 200; ++y) {
+			const byte *source =
+				(const byte *)_engine._screen->getBasePtr(0, y);
+			byte *line1 = (byte *)_output.getBasePtr(
+				0, frameY + y * 2);
+			byte *line2 = (byte *)_output.getBasePtr(
+				0, frameY + y * 2 + 1);
+			for (int x = 0; x < 320; ++x) {
+				const byte color = source[(x + shakeOffset) % 320];
+				line1[x * 2] = color;
+				line1[x * 2 + 1] = color;
+			}
+			memcpy(line2, line1, kMacScreenWidth);
+		}
+
+		if (_useOriginalMenus && _menus)
+			_menus->draw();
+		g_system->copyRectToScreen(_output.getPixels(), _output.pitch,
+			0, 0, kMacScreenWidth, _output.h);
+		g_system->updateScreen();
+		_engine._screen->clearDirtyRects();
+		return;
+	}
+
 	const int sceneY = _useOriginalMenus ? kMacDesktopSceneY : 0;
 	const int interfaceY = _useOriginalMenus ?
 		kMacDesktopInterfaceY : kMacSceneHeight;
@@ -774,6 +831,24 @@ void RexNebularEngine::selectMacintoshDifficulty() {
 		_macNebular->selectDifficulty();
 }
 
+int RexNebularEngine::selectMacintoshResumeSlot() {
+	return _macNebular ? _macNebular->selectResumeSlot() : -1;
+}
+
+bool RexNebularEngine::usesOriginalMacintoshMenus() const {
+	return _macNebular && _macNebular->usesOriginalMenus();
+}
+
+void RexNebularEngine::setMacintoshOuterMenuActive(bool active) {
+	if (_macNebular)
+		_macNebular->setOuterMenuActive(active);
+}
+
+void RexNebularEngine::setMacintoshFullFrameActive(bool active) {
+	if (_macNebular)
+		_macNebular->setFullFrameActive(active);
+}
+
 bool RexNebularEngine::drawPopup() {
 	if (!_macNebular)
 		return false;
diff --git a/engines/mads/nebular/mac_nebular.h b/engines/mads/nebular/mac_nebular.h
index f7fa0650527..7ce36f15b85 100644
--- a/engines/mads/nebular/mac_nebular.h
+++ b/engines/mads/nebular/mac_nebular.h
@@ -47,6 +47,7 @@ private:
 	Common::Rect _popupRect;
 	RGBcolor _palette[256];
 	bool _useOriginalMenus;
+	bool _fullFrameActive = false;
 	bool _popupActive = false;
 	bool _layoutLogged = false;
 
@@ -58,6 +59,10 @@ public:
 	bool initResources();
 	void applyGameSettings();
 	void selectDifficulty();
+	int selectResumeSlot();
+	bool usesOriginalMenus() const { return _useOriginalMenus; }
+	void setFullFrameActive(bool active) { _fullFrameActive = active; }
+	void setOuterMenuActive(bool active);
 	Common::Point screenToGame(const Common::Point &point) const;
 	Common::Point gameToScreen(const Common::Point &point) const;
 	bool handleMacEvent(Common::Event &event);
diff --git a/engines/mads/nebular/mac_resources.cpp b/engines/mads/nebular/mac_resources.cpp
index 58c580b2a84..8f460fc82d2 100644
--- a/engines/mads/nebular/mac_resources.cpp
+++ b/engines/mads/nebular/mac_resources.cpp
@@ -250,6 +250,10 @@ MacResourceProvider::ResourceID MacResourceProvider::mapResource(const Common::S
 		return { kGlobalContainer, MKTAG('M', 'e', 't', 'h'), 1000 };
 	if (name == "BTSPIN.SS")
 		return { kGlobalContainer, MKTAG('B', 't', 's', 'p'), 1000 };
+	if (name == "@REXOPEN")
+		return { kSection9Container, MKTAG('A', 'R', 'e', 's'), 1078 };
+	if (name == "CREDITS.TXR")
+		return { kGlobalContainer, MKTAG('C', 'R', 'E', 'D'), 1000 };
 
 	if (name.hasPrefix("ENDING") && name.hasSuffix(".TXR") && name.size() == 11 &&
 			name[6] >= '0' && name[6] <= '9') {
diff --git a/engines/mads/nebular/main.cpp b/engines/mads/nebular/main.cpp
index 65c62d4e951..c337b496b4e 100644
--- a/engines/mads/nebular/main.cpp
+++ b/engines/mads/nebular/main.cpp
@@ -195,7 +195,8 @@ static void main_menu_main() {
 
 	kernel_unload_sound_driver();
 
-	if (selected_item == 5)
+	if (selected_item == 5 &&
+			g_engine->getPlatform() != Common::kPlatformMacintosh)
 		show_exit_advert(palette);
 
 	keys_remove();
@@ -307,6 +308,20 @@ done:
 	global_write_config_file();
 }
 
+static void run_full_frame_animview(RexNebularEngine *engine,
+		const char *resource) {
+	engine->setMacintoshFullFrameActive(true);
+	AnimView::animview_main(resource);
+	engine->setMacintoshFullFrameActive(false);
+}
+
+static void run_full_frame_textview(RexNebularEngine *engine,
+		const char *resource) {
+	engine->setMacintoshFullFrameActive(true);
+	TextView::textview_main(resource);
+	engine->setMacintoshFullFrameActive(false);
+}
+
 void nebular_main() {
 	static const char *CMD_LINE[] = { nullptr, "-p" };
 	Palette palette;
@@ -316,11 +331,10 @@ void nebular_main() {
 		env_search_mode = ENV_SEARCH_CONCAT_FILES;
 
 	g_engine->readConfigFile();
+	RexNebularEngine *const engine = (RexNebularEngine *)g_engine;
 
-	if (g_engine->getPlatform() == Common::kPlatformMacintosh)
-		// FIXME: The Macintosh application has a native resource-based
-		// outer menu, not the DOS MADS menu and playlist files used below.
-		// The ScummVM launcher provides that outer-menu boundary.
+	if (g_engine->getPlatform() == Common::kPlatformMacintosh &&
+			!engine->usesOriginalMacintoshMenus())
 		selected_item = 0;
 	else if (ConfMan.getBool("start_game") || ConfMan.hasKey("save_slot"))
 		selected_item = 0;
@@ -336,7 +350,13 @@ void nebular_main() {
 
 		switch (selected_item) {
 		case -1:
+			// Macintosh CODE 133 uses the same room-990 series and six
+			// selections as the existing MADS controller. Only its native
+			// window composition differs.
+			engine->setMacintoshOuterMenuActive(
+				engine->usesOriginalMacintoshMenus());
 			main_menu_main();
+			engine->setMacintoshOuterMenuActive(false);
 
 			if (selected_item >= 0) {
 				Common::fill(magic_color_values, magic_color_values + 3, 0);
@@ -360,9 +380,16 @@ void nebular_main() {
 		case 1: {
 			// Resume savegame
 			// Get a list of saves and choose the last one
-			auto saves = g_engine->listSaves();
+			SaveStateList saves = g_engine->listSaves();
 			if (!saves.empty())
 				savegame_slot = saves.back().getSaveSlot();
+			else if (engine->usesOriginalMacintoshMenus()) {
+				savegame_slot = engine->selectMacintoshResumeSlot();
+				if (savegame_slot < 0) {
+					selected_item = -1;
+					break;
+				}
+			}
 
 			// Start the game, which will also load the selected savegame
 			game_main(2, CMD_LINE);
@@ -371,46 +398,46 @@ void nebular_main() {
 
 		case 2:
 			// Intro
-			AnimView::animview_main("@rexopen");
+			run_full_frame_animview(engine, "@rexopen");
 			selected_item = -1;
 			break;
 
 		case 3:
 			// Credits
-			TextView::textview_main("credits");
+			run_full_frame_textview(engine, "credits");
 			selected_item = -1;
 			break;
 
 		case 4:
 			// Quotes
-			TextView::textview_main("quotes");
+			run_full_frame_textview(engine, "quotes");
 			selected_item = -1;
 			break;
 
 		case 9:
 			// The DOS demo batch file plays this ANIMVIEW playlist before
 			// starting its three-room playable section.
-			AnimView::animview_main("@demodisk");
+			run_full_frame_animview(engine, "@demodisk");
 			selected_item = 0;
 			break;
 
 		case WIN_QUICK_DEATH + 16:
-			AnimView::animview_main("@rexend1");
-			TextView::textview_main("ending1");
+			run_full_frame_animview(engine, "@rexend1");
+			run_full_frame_textview(engine, "ending1");
 			return;
 
 		case WIN_SLOW_DEATH + 16:
-			AnimView::animview_main("@rexend2");
-			TextView::textview_main("ending2");
+			run_full_frame_animview(engine, "@rexend2");
+			run_full_frame_textview(engine, "ending2");
 			return;
 
 		case WIN_ALL_THE_MONEY + 16:
-			AnimView::animview_main("@rexend3");
-			TextView::textview_main("credits");
+			run_full_frame_animview(engine, "@rexend3");
+			run_full_frame_textview(engine, "credits");
 			return;
 
 		case WIN_A_HEAD_POW + 16:
-			TextView::textview_main("ending4");
+			run_full_frame_textview(engine, "ending4");
 			return;
 
 		case 5:
diff --git a/engines/mads/nebular/main_menu.cpp b/engines/mads/nebular/main_menu.cpp
index 3d3f801e6d9..96da34242de 100644
--- a/engines/mads/nebular/main_menu.cpp
+++ b/engines/mads/nebular/main_menu.cpp
@@ -21,6 +21,7 @@
 
 #include "mads/core/general.h"
 #include "mads/core/config.h"
+#include "mads/core/env.h"
 #include "mads/core/error.h"
 #include "mads/core/font.h"
 #include "mads/core/hspot.h"
@@ -149,11 +150,13 @@ static void load_title_screen() {
 	pal_white(master_palette);
 
 	// Set up mouse cursor
-	Graphics::Surface surf;
-	surf.format = Graphics::PixelFormat::createFormatCLUT8();
-	surf.w = surf.pitch = surf.h = 16;
-	surf.setPixels(const_cast<byte *>(&ARROW_CURSOR[0][0]));
-	mouse_cursor_surface(surf, 1, 1);
+	if (!env_set_cursor(1)) {
+		Graphics::Surface surf;
+		surf.format = Graphics::PixelFormat::createFormatCLUT8();
+		surf.w = surf.pitch = surf.h = 16;
+		surf.setPixels(const_cast<byte *>(&ARROW_CURSOR[0][0]));
+		mouse_cursor_surface(surf, 1, 1);
+	}
 	mouse_show();
 
 	// Load the title screen room
diff --git a/engines/mads/nebular/nebular.h b/engines/mads/nebular/nebular.h
index 2a21c41f39a..6902f44552f 100644
--- a/engines/mads/nebular/nebular.h
+++ b/engines/mads/nebular/nebular.h
@@ -62,6 +62,10 @@ public:
 	Common::Error run() override;
 	void syncRoom(Common::Serializer &s) override;
 	void selectMacintoshDifficulty();
+	int selectMacintoshResumeSlot();
+	bool usesOriginalMacintoshMenus() const;
+	void setMacintoshOuterMenuActive(bool active);
+	void setMacintoshFullFrameActive(bool active);
 
 	int main_copy_verify() override;
 	void global_init_code() override;


Commit: 7b67d7f3f14c580035f70a64bebf5f9ab7d9b66b
    https://github.com/scummvm/scummvm/commit/7b67d7f3f14c580035f70a64bebf5f9ab7d9b66b
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:48:38+10:00

Commit Message:
MADS: Add configurable frontend viewer presentation

Add constructor-free presentation settings for AnimView and
TextView. Existing entry points retain the DOS-derived geometry and
boundary-line behavior.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/animview/animview.cpp
    engines/mads/animview/animview.h
    engines/mads/textview/textview.cpp
    engines/mads/textview/textview.h


diff --git a/engines/mads/animview/animview.cpp b/engines/mads/animview/animview.cpp
index 9e6576f260f..8ced8a8ae2e 100644
--- a/engines/mads/animview/animview.cpp
+++ b/engines/mads/animview/animview.cpp
@@ -91,6 +91,8 @@ static bool hasAnimInited;
 static int runVal1, runVal2, runVal3;
 static int runVal12;
 static int error_code;
+static int presentationBufferHeight;
+static bool presentationDrawBoundaryLines;
 
 /**
  * Initializes animview global variables
@@ -188,10 +190,11 @@ static void run_animation(int animIndex) {
 	}
 
 	auto &screen = *g_engine->getScreen();
-	if (viewing_at_y && anim_list[animIndex].show_bars) {
+	if (presentationDrawBoundaryLines && viewing_at_y &&
+			anim_list[animIndex].show_bars) {
 		screen.hLine(0, viewing_at_y - 2, 319, 253);
 		screen.hLine(0, viewing_at_y + scr_work.y + 1, 319, 253);
-	} else if (viewing_at_y) {
+	} else if (presentationDrawBoundaryLines && viewing_at_y) {
 		screen.hLine(0, viewing_at_y - 2, 319, 0);
 		screen.hLine(0, viewing_at_y + scr_work.y + 1, 319, 0);
 	}
@@ -538,7 +541,8 @@ static void animate() {
 		has_cycles = anim_cycle_list.num_cycles > 0;
 		current_anim_inter = (AnimInterPtr)current_anim;
 
-		int height = (scr_orig.y == 200) ? 200 : 156;
+		int height = presentationBufferHeight ? presentationBufferHeight :
+			((scr_orig.y == 200) ? 200 : 156);
 		buffer_init(&scr_work, 320, height);
 		scr_inter = scr_work;
 		assert(scr_work.data);
@@ -653,7 +657,16 @@ done:
 }
 
 void animview_main(const char *resName) {
+	Presentation presentation;
+	presentation.bufferHeight = 0;
+	presentation.drawBoundaryLines = true;
+	animview_main(resName, presentation);
+}
+
+void animview_main(const char *resName, const Presentation &presentation) {
 	char name[16];
+	presentationBufferHeight = presentation.bufferHeight;
+	presentationDrawBoundaryLines = presentation.drawBoundaryLines;
 
 	init_globals();
 
diff --git a/engines/mads/animview/animview.h b/engines/mads/animview/animview.h
index 1b8b5f3fb99..7676655ad7a 100644
--- a/engines/mads/animview/animview.h
+++ b/engines/mads/animview/animview.h
@@ -29,6 +29,11 @@
 namespace MADS {
 namespace AnimView {
 
+struct Presentation {
+	int bufferHeight;
+	bool drawBoundaryLines;
+};
+
 // Variables shared with other AnimView namespace files
 extern int speechFlags;
 extern int current_error_code;
@@ -58,6 +63,7 @@ extern int speechResourceId;
 
 // Main animview function
 extern void animview_main(const char *resName);
+extern void animview_main(const char *resName, const Presentation &presentation);
 
 } // namespace AnimView
 } // namespace MADS
diff --git a/engines/mads/textview/textview.cpp b/engines/mads/textview/textview.cpp
index ac14145feb8..60536426bda 100644
--- a/engines/mads/textview/textview.cpp
+++ b/engines/mads/textview/textview.cpp
@@ -62,6 +62,9 @@ static Buffer room_picture[3];
 static Buffer *background_ptr;
 static int16 xPos;
 static byte line_slice[156];
+static int bufferHeight;
+static int visibleHeight;
+static bool drawBoundaryLines;
 
 static void read_line_slice(int xp) {
 	const byte *src = buffer_pointer(background_ptr, xp, 0);
@@ -445,6 +448,20 @@ static void animate() {
 }
 
 void textview_main(const char *resName) {
+	Presentation presentation;
+	presentation.bufferHeight = 156;
+	presentation.visibleHeight = 156;
+	presentation.drawBoundaryLines = true;
+	textview_main(resName, presentation);
+}
+
+void textview_main(const char *resName, const Presentation &presentation) {
+	bufferHeight = presentation.bufferHeight;
+	visibleHeight = presentation.visibleHeight;
+	drawBoundaryLines = presentation.drawBoundaryLines;
+	assert(bufferHeight >= 156 && visibleHeight > 0 &&
+		visibleHeight <= bufferHeight && visibleHeight <= 200);
+
 	active = false;
 	isGoing = flag2 = true;
 	isEnd = flag3 = false;
@@ -482,12 +499,12 @@ void textview_main(const char *resName) {
 	// Initialize surfaces
 	pal_init(8, 8);
 	pal_white(master_palette);
-	buffer_init(&scr_work, 320, 156);
-	buffer_init(&scr_depth, 320, 156);
-	buffer_init(&scr_orig, 320, 156);
+	buffer_init(&scr_work, 320, bufferHeight);
+	buffer_init(&scr_depth, 320, bufferHeight);
+	buffer_init(&scr_orig, 320, bufferHeight);
 	assert(scr_work.data && scr_depth.data && scr_orig.data);
 
-	viewing_at_y = (200 - scr_work.y) / 2;
+	viewing_at_y = (200 - visibleHeight) / 2;
 	room = nullptr;
 
 	// Clear buffers and palette
@@ -497,16 +514,17 @@ void textview_main(const char *resName) {
 	mcga_setpal(&master_palette);
 
 	mouse_set_work_buffer(scr_work.data, scr_work.x);
-	mouse_set_view_port_loc(0, viewing_at_y, scr_work.x, scr_work.y + viewing_at_y - 1);
+	mouse_set_view_port_loc(0, viewing_at_y, scr_work.x,
+		visibleHeight + viewing_at_y - 1);
 	mouse_set_view_port(0, 0);
 	timer_install();
 	matte_init(-1);
 	timer_activate_low_priority(cycle_colors);
 
 	// Draw boundary horizontal lines at top and bottom of the screen
-	if (viewing_at_y) {
+	if (drawBoundaryLines && viewing_at_y) {
 		screen.hLine(0, viewing_at_y - 2, 319, 2);
-		screen.hLine(0, viewing_at_y + scr_work.y + 1, 319, 2);
+		screen.hLine(0, viewing_at_y + visibleHeight + 1, 319, 2);
 	}
 
 	// Clear the lines
diff --git a/engines/mads/textview/textview.h b/engines/mads/textview/textview.h
index 787c6dc40f8..63936c01f84 100644
--- a/engines/mads/textview/textview.h
+++ b/engines/mads/textview/textview.h
@@ -27,8 +27,15 @@
 namespace MADS {
 namespace TextView {
 
+struct Presentation {
+	int bufferHeight;
+	int visibleHeight;
+	bool drawBoundaryLines;
+};
+
 // Main textview function
 extern void textview_main(const char *resName);
+extern void textview_main(const char *resName, const Presentation &presentation);
 
 } // namespace TextView
 } // namespace MADS


Commit: 212e8daff54ff7dd66b494e97e2dfef5039dc8a9
    https://github.com/scummvm/scummvm/commit/212e8daff54ff7dd66b494e97e2dfef5039dc8a9
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:48:38+10:00

Commit Message:
MADS: NEBULAR: Restore Macintosh TextView

Present TextView through the native 320x210 backing and 320x200
visible geometry recovered from CODE 133. Keep the DOS viewer
dimensions and boundary lines unchanged.

Assisted-by: Codex:GPT-5.4

Changed paths:
  A engines/mads/nebular/mac_frontend.cpp
  A engines/mads/nebular/mac_frontend.h
    engines/mads/module.mk
    engines/mads/nebular/main.cpp


diff --git a/engines/mads/module.mk b/engines/mads/module.mk
index 5db16de117e..51e0dffca1a 100644
--- a/engines/mads/module.mk
+++ b/engines/mads/module.mk
@@ -78,6 +78,7 @@ MODULE_OBJS := \
 	nebular/global.o \
 	nebular/main.o \
 	nebular/main_menu.o \
+	nebular/mac_frontend.o \
 	nebular/mac_menus.o \
 	nebular/mac_nebular.o \
 	nebular/mac_resources.o \
diff --git a/engines/mads/nebular/mac_frontend.cpp b/engines/mads/nebular/mac_frontend.cpp
new file mode 100644
index 00000000000..fc7301656b1
--- /dev/null
+++ b/engines/mads/nebular/mac_frontend.cpp
@@ -0,0 +1,44 @@
+/* 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 "mads/nebular/mac_frontend.h"
+
+#include "mads/nebular/nebular.h"
+#include "mads/textview/textview.h"
+
+namespace MADS {
+namespace RexNebular {
+namespace MacFrontend {
+
+void runTextView(RexNebularEngine &engine, const char *resource) {
+	TextView::Presentation presentation;
+	presentation.bufferHeight = 210;
+	presentation.visibleHeight = 200;
+	presentation.drawBoundaryLines = false;
+
+	engine.setMacintoshFullFrameActive(true);
+	TextView::textview_main(resource, presentation);
+	engine.setMacintoshFullFrameActive(false);
+}
+
+} // namespace MacFrontend
+} // namespace RexNebular
+} // namespace MADS
diff --git a/engines/mads/nebular/mac_frontend.h b/engines/mads/nebular/mac_frontend.h
new file mode 100644
index 00000000000..3a97e854ca1
--- /dev/null
+++ b/engines/mads/nebular/mac_frontend.h
@@ -0,0 +1,38 @@
+/* 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 MADS_NEBULAR_MAC_FRONTEND_H
+#define MADS_NEBULAR_MAC_FRONTEND_H
+
+namespace MADS {
+namespace RexNebular {
+
+class RexNebularEngine;
+
+namespace MacFrontend {
+
+void runTextView(RexNebularEngine &engine, const char *resource);
+
+} // namespace MacFrontend
+} // namespace RexNebular
+} // namespace MADS
+
+#endif
diff --git a/engines/mads/nebular/main.cpp b/engines/mads/nebular/main.cpp
index c337b496b4e..6efb2030b1b 100644
--- a/engines/mads/nebular/main.cpp
+++ b/engines/mads/nebular/main.cpp
@@ -44,6 +44,7 @@
 #include "mads/core/speech.h"
 #include "mads/core/timer.h"
 #include "mads/core/video.h"
+#include "mads/nebular/mac_frontend.h"
 #include "mads/nebular/main_menu.h"
 #include "mads/nebular/mac_menus.h"
 #include "mads/nebular/menus.h"
@@ -317,9 +318,10 @@ static void run_full_frame_animview(RexNebularEngine *engine,
 
 static void run_full_frame_textview(RexNebularEngine *engine,
 		const char *resource) {
-	engine->setMacintoshFullFrameActive(true);
-	TextView::textview_main(resource);
-	engine->setMacintoshFullFrameActive(false);
+	if (g_engine->getPlatform() == Common::kPlatformMacintosh)
+		MacFrontend::runTextView(*engine, resource);
+	else
+		TextView::textview_main(resource);
 }
 
 void nebular_main() {


Commit: fe155831d2f4688569dd55153e7c177c9250a108
    https://github.com/scummvm/scummvm/commit/fe155831d2f4688569dd55153e7c177c9250a108
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:48:38+10:00

Commit Message:
MADS: NEBULAR: Restore Macintosh AnimView

Use the native 320x200 presentation and service animation frames
from the viewer loop as recovered from CODE 133. Retain the
callback-driven DOS path as the default.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/animview/animview.cpp
    engines/mads/animview/animview.h
    engines/mads/nebular/mac_frontend.cpp
    engines/mads/nebular/mac_frontend.h
    engines/mads/nebular/main.cpp


diff --git a/engines/mads/animview/animview.cpp b/engines/mads/animview/animview.cpp
index 8ced8a8ae2e..6e52f5ce4aa 100644
--- a/engines/mads/animview/animview.cpp
+++ b/engines/mads/animview/animview.cpp
@@ -93,6 +93,7 @@ static int runVal12;
 static int error_code;
 static int presentationBufferHeight;
 static bool presentationDrawBoundaryLines;
+static bool presentationServiceFramesInline;
 
 /**
  * Initializes animview global variables
@@ -245,11 +246,16 @@ static void run_animation(int animIndex) {
 		runCtr1 = 0;
 		peelFlag = current_anim->misc_peel_x || current_anim->misc_peel_y;
 		timer2 = timer1;
-		timer_activate_low_priority(anim_timer);
+		timer_activate_low_priority(presentationServiceFramesInline ?
+			nullptr : anim_timer);
 	}
 
 	// Main animation loop
 	while (currentFrame < maxFrame && !current_error_code) {
+		if (presentationServiceFramesInline &&
+				current_anim->background_type != AA_INTERFACE)
+			anim_timer();
+
 		if (speechResourceId != -1) {
 			if (current_anim->load_flags & AA_LOAD_SPEECH) {
 				char buf[80];
@@ -660,6 +666,7 @@ void animview_main(const char *resName) {
 	Presentation presentation;
 	presentation.bufferHeight = 0;
 	presentation.drawBoundaryLines = true;
+	presentation.serviceFramesInline = false;
 	animview_main(resName, presentation);
 }
 
@@ -667,6 +674,7 @@ void animview_main(const char *resName, const Presentation &presentation) {
 	char name[16];
 	presentationBufferHeight = presentation.bufferHeight;
 	presentationDrawBoundaryLines = presentation.drawBoundaryLines;
+	presentationServiceFramesInline = presentation.serviceFramesInline;
 
 	init_globals();
 
diff --git a/engines/mads/animview/animview.h b/engines/mads/animview/animview.h
index 7676655ad7a..8e0e071fc6e 100644
--- a/engines/mads/animview/animview.h
+++ b/engines/mads/animview/animview.h
@@ -32,6 +32,7 @@ namespace AnimView {
 struct Presentation {
 	int bufferHeight;
 	bool drawBoundaryLines;
+	bool serviceFramesInline;
 };
 
 // Variables shared with other AnimView namespace files
diff --git a/engines/mads/nebular/mac_frontend.cpp b/engines/mads/nebular/mac_frontend.cpp
index fc7301656b1..d218abbb3ff 100644
--- a/engines/mads/nebular/mac_frontend.cpp
+++ b/engines/mads/nebular/mac_frontend.cpp
@@ -21,6 +21,7 @@
 
 #include "mads/nebular/mac_frontend.h"
 
+#include "mads/animview/animview.h"
 #include "mads/nebular/nebular.h"
 #include "mads/textview/textview.h"
 
@@ -28,6 +29,17 @@ namespace MADS {
 namespace RexNebular {
 namespace MacFrontend {
 
+void runAnimView(RexNebularEngine &engine, const char *resource) {
+	AnimView::Presentation presentation;
+	presentation.bufferHeight = 200;
+	presentation.drawBoundaryLines = false;
+	presentation.serviceFramesInline = true;
+
+	engine.setMacintoshFullFrameActive(true);
+	AnimView::animview_main(resource, presentation);
+	engine.setMacintoshFullFrameActive(false);
+}
+
 void runTextView(RexNebularEngine &engine, const char *resource) {
 	TextView::Presentation presentation;
 	presentation.bufferHeight = 210;
diff --git a/engines/mads/nebular/mac_frontend.h b/engines/mads/nebular/mac_frontend.h
index 3a97e854ca1..dd7b0a26a26 100644
--- a/engines/mads/nebular/mac_frontend.h
+++ b/engines/mads/nebular/mac_frontend.h
@@ -29,6 +29,7 @@ class RexNebularEngine;
 
 namespace MacFrontend {
 
+void runAnimView(RexNebularEngine &engine, const char *resource);
 void runTextView(RexNebularEngine &engine, const char *resource);
 
 } // namespace MacFrontend
diff --git a/engines/mads/nebular/main.cpp b/engines/mads/nebular/main.cpp
index 6efb2030b1b..e3364092fdb 100644
--- a/engines/mads/nebular/main.cpp
+++ b/engines/mads/nebular/main.cpp
@@ -311,9 +311,10 @@ done:
 
 static void run_full_frame_animview(RexNebularEngine *engine,
 		const char *resource) {
-	engine->setMacintoshFullFrameActive(true);
-	AnimView::animview_main(resource);
-	engine->setMacintoshFullFrameActive(false);
+	if (g_engine->getPlatform() == Common::kPlatformMacintosh)
+		MacFrontend::runAnimView(*engine, resource);
+	else
+		AnimView::animview_main(resource);
 }
 
 static void run_full_frame_textview(RexNebularEngine *engine,


Commit: 47019b36e78b981ede9c8d6aff917443b7fbe7e4
    https://github.com/scummvm/scummvm/commit/47019b36e78b981ede9c8d6aff917443b7fbe7e4
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:48:38+10:00

Commit Message:
MADS: NEBULAR: Match native frontend transitions

Follow the recovered Macintosh death-ending text with CRED 1000.
The all-money path already reaches the same credits resource, while DOS
keeps its existing transitions.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/nebular/mac_frontend.cpp
    engines/mads/nebular/mac_frontend.h
    engines/mads/nebular/main.cpp


diff --git a/engines/mads/nebular/mac_frontend.cpp b/engines/mads/nebular/mac_frontend.cpp
index d218abbb3ff..b36a2c1e635 100644
--- a/engines/mads/nebular/mac_frontend.cpp
+++ b/engines/mads/nebular/mac_frontend.cpp
@@ -51,6 +51,11 @@ void runTextView(RexNebularEngine &engine, const char *resource) {
 	engine.setMacintoshFullFrameActive(false);
 }
 
+void showCreditsAfterEnding(RexNebularEngine &engine) {
+	// CODE 133 follows Endi 49, 50 and 52 with CRED 1000.
+	runTextView(engine, "credits");
+}
+
 } // namespace MacFrontend
 } // namespace RexNebular
 } // namespace MADS
diff --git a/engines/mads/nebular/mac_frontend.h b/engines/mads/nebular/mac_frontend.h
index dd7b0a26a26..20ce16a5c14 100644
--- a/engines/mads/nebular/mac_frontend.h
+++ b/engines/mads/nebular/mac_frontend.h
@@ -31,6 +31,7 @@ namespace MacFrontend {
 
 void runAnimView(RexNebularEngine &engine, const char *resource);
 void runTextView(RexNebularEngine &engine, const char *resource);
+void showCreditsAfterEnding(RexNebularEngine &engine);
 
 } // namespace MacFrontend
 } // namespace RexNebular
diff --git a/engines/mads/nebular/main.cpp b/engines/mads/nebular/main.cpp
index e3364092fdb..7b8bcdf2eed 100644
--- a/engines/mads/nebular/main.cpp
+++ b/engines/mads/nebular/main.cpp
@@ -427,11 +427,15 @@ void nebular_main() {
 		case WIN_QUICK_DEATH + 16:
 			run_full_frame_animview(engine, "@rexend1");
 			run_full_frame_textview(engine, "ending1");
+			if (g_engine->getPlatform() == Common::kPlatformMacintosh)
+				MacFrontend::showCreditsAfterEnding(*engine);
 			return;
 
 		case WIN_SLOW_DEATH + 16:
 			run_full_frame_animview(engine, "@rexend2");
 			run_full_frame_textview(engine, "ending2");
+			if (g_engine->getPlatform() == Common::kPlatformMacintosh)
+				MacFrontend::showCreditsAfterEnding(*engine);
 			return;
 
 		case WIN_ALL_THE_MONEY + 16:
@@ -441,6 +445,8 @@ void nebular_main() {
 
 		case WIN_A_HEAD_POW + 16:
 			run_full_frame_textview(engine, "ending4");
+			if (g_engine->getPlatform() == Common::kPlatformMacintosh)
+				MacFrontend::showCreditsAfterEnding(*engine);
 			return;
 
 		case 5:


Commit: 9d3fbe767c13ddc507bc07e2b2ac388ef3e5ef7e
    https://github.com/scummvm/scummvm/commit/9d3fbe767c13ddc507bc07e2b2ac388ef3e5ef7e
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:48:38+10:00

Commit Message:
MADS: NEBULAR: Clear Macintosh viewer padding

The Macintosh frontend presents 320x156 MADS content in a 320x200
port. Pad TextView backgrounds with owned black rows and clear the
logical screen before AnimView playback so stale pixels cannot leak
below line 155. Gate the behavior behind macintoshFullFrame; other
TextView callers retain their existing path.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/nebular/mac_frontend.cpp
    engines/mads/textview/textview.cpp
    engines/mads/textview/textview.h


diff --git a/engines/mads/nebular/mac_frontend.cpp b/engines/mads/nebular/mac_frontend.cpp
index b36a2c1e635..47c40266258 100644
--- a/engines/mads/nebular/mac_frontend.cpp
+++ b/engines/mads/nebular/mac_frontend.cpp
@@ -36,6 +36,7 @@ void runAnimView(RexNebularEngine &engine, const char *resource) {
 	presentation.serviceFramesInline = true;
 
 	engine.setMacintoshFullFrameActive(true);
+	engine.getScreen()->clear();
 	AnimView::animview_main(resource, presentation);
 	engine.setMacintoshFullFrameActive(false);
 }
@@ -45,6 +46,7 @@ void runTextView(RexNebularEngine &engine, const char *resource) {
 	presentation.bufferHeight = 210;
 	presentation.visibleHeight = 200;
 	presentation.drawBoundaryLines = false;
+	presentation.macintoshFullFrame = true;
 
 	engine.setMacintoshFullFrameActive(true);
 	TextView::textview_main(resource, presentation);
diff --git a/engines/mads/textview/textview.cpp b/engines/mads/textview/textview.cpp
index 60536426bda..a38ee5b8a3a 100644
--- a/engines/mads/textview/textview.cpp
+++ b/engines/mads/textview/textview.cpp
@@ -65,6 +65,19 @@ static byte line_slice[156];
 static int bufferHeight;
 static int visibleHeight;
 static bool drawBoundaryLines;
+static bool macintoshFullFrame;
+
+static void present_matte_frame(int specialEffect, int fullScreen) {
+	if (!macintoshFullFrame) {
+		matte_frame(specialEffect, fullScreen);
+		return;
+	}
+
+	const int workHeight = scr_work.y;
+	scr_work.y = visibleHeight;
+	matte_frame(specialEffect, fullScreen);
+	scr_work.y = workHeight;
+}
 
 static void read_line_slice(int xp) {
 	const byte *src = buffer_pointer(background_ptr, xp, 0);
@@ -165,6 +178,24 @@ static void load_background(const char *value) {
 	if (!room)
 		error("Could not load room %d", room_id);
 
+	if (macintoshFullFrame && scr_orig.y < visibleHeight) {
+		Buffer paddedBackground;
+		memset(&paddedBackground, 0, sizeof(paddedBackground));
+		buffer_init(&paddedBackground, scr_orig.x, visibleHeight);
+		assert(paddedBackground.data);
+		buffer_fill(paddedBackground, 0);
+		buffer_rect_copy_2(scr_orig, paddedBackground, 0, 0, 0, 0,
+			scr_orig.x, scr_orig.y);
+		buffer_free(&scr_orig);
+		scr_orig = paddedBackground;
+	}
+
+	if (macintoshFullFrame) {
+		buffer_rect_copy_2(scr_orig, scr_work, 0, 0, 0, 0,
+			MIN(scr_orig.x, scr_work.x),
+			MIN(scr_orig.y, visibleHeight));
+	}
+
 	image_marker = 1;
 	image_list[0].flags = IMAGE_REFRESH;
 	image_list[0].segment_id = 0xff;
@@ -411,7 +442,7 @@ static void animate() {
 			curr_time = timer_read();
 
 			if (curr_time < timer2) {
-				matte_frame(0, 0);
+				present_matte_frame(0, 0);
 				mouse_hide();
 			}
 		}
@@ -436,7 +467,7 @@ static void animate() {
 			if (isEnd && total == 0)
 				isGoing = false;
 
-			matte_frame(has_background ? 2 : 0, 0);
+			present_matte_frame(has_background ? 2 : 0, 0);
 			mouse_hide();
 
 			flag3 = has_background = false;
@@ -452,6 +483,7 @@ void textview_main(const char *resName) {
 	presentation.bufferHeight = 156;
 	presentation.visibleHeight = 156;
 	presentation.drawBoundaryLines = true;
+	presentation.macintoshFullFrame = false;
 	textview_main(resName, presentation);
 }
 
@@ -459,6 +491,7 @@ void textview_main(const char *resName, const Presentation &presentation) {
 	bufferHeight = presentation.bufferHeight;
 	visibleHeight = presentation.visibleHeight;
 	drawBoundaryLines = presentation.drawBoundaryLines;
+	macintoshFullFrame = presentation.macintoshFullFrame;
 	assert(bufferHeight >= 156 && visibleHeight > 0 &&
 		visibleHeight <= bufferHeight && visibleHeight <= 200);
 
diff --git a/engines/mads/textview/textview.h b/engines/mads/textview/textview.h
index 63936c01f84..85f31c8429b 100644
--- a/engines/mads/textview/textview.h
+++ b/engines/mads/textview/textview.h
@@ -31,6 +31,7 @@ struct Presentation {
 	int bufferHeight;
 	int visibleHeight;
 	bool drawBoundaryLines;
+	bool macintoshFullFrame;
 };
 
 // Main textview function


Commit: c3442d1e4bc38f8ea7b33913aa4497f85c97f67b
    https://github.com/scummvm/scummvm/commit/c3442d1e4bc38f8ea7b33913aa4497f85c97f67b
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:48:38+10:00

Commit Message:
MADS: NEBULAR: Preserve Macintosh menu colors

Keep the Macintosh menu bar's black and white system colors outside
full-frame viewer palette fades. The game frame continues to own and
transition every other palette entry.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/nebular/mac_menus.cpp
    engines/mads/nebular/mac_menus.h
    engines/mads/nebular/mac_nebular.cpp


diff --git a/engines/mads/nebular/mac_menus.cpp b/engines/mads/nebular/mac_menus.cpp
index e36b765c680..43512ce2267 100644
--- a/engines/mads/nebular/mac_menus.cpp
+++ b/engines/mads/nebular/mac_menus.cpp
@@ -521,10 +521,19 @@ void MacNebularMenu::draw() {
 }
 
 byte MacNebularMenu::getBlackColor() {
+	byte menuBlack, menuWhite;
+	getMenuColors(menuBlack, menuWhite);
+	return menuBlack;
+}
+
+void MacNebularMenu::getMenuColors(byte &menuBlack, byte &menuWhite) {
+	menuBlack = 0;
+	menuWhite = 0;
 	if (!initializeWindowManager())
-		return 0;
+		return;
 	syncPalette();
-	return (byte)_windowManager->_colorBlack;
+	menuBlack = (byte)_windowManager->_colorBlack;
+	menuWhite = (byte)_windowManager->_colorWhite;
 }
 
 void MacNebularMenu::menuCallback(int commandId, Common::String &, void *data) {
diff --git a/engines/mads/nebular/mac_menus.h b/engines/mads/nebular/mac_menus.h
index 7be6a6a7470..c4790f3292b 100644
--- a/engines/mads/nebular/mac_menus.h
+++ b/engines/mads/nebular/mac_menus.h
@@ -75,6 +75,7 @@ public:
 	bool processEvent(Common::Event &event);
 	void draw();
 	byte getBlackColor();
+	void getMenuColors(byte &menuBlack, byte &menuWhite);
 	void setOuterMenuActive(bool active) { _outerMenuActive = active; }
 	int runDifficultyDialog();
 	int selectResumeSlot();
diff --git a/engines/mads/nebular/mac_nebular.cpp b/engines/mads/nebular/mac_nebular.cpp
index 8c7655bf634..7d2e3cd5d86 100644
--- a/engines/mads/nebular/mac_nebular.cpp
+++ b/engines/mads/nebular/mac_nebular.cpp
@@ -528,8 +528,31 @@ void MacNebular::setPalette(const RGBcolor *palette, int firstColor,
 		corrected[offset + 1] = macGammaCorrect(palette[color].g);
 		corrected[offset + 2] = macGammaCorrect(palette[color].b);
 	}
-	g_system->getPaletteManager()->setPalette(corrected, outputFirst,
-		lastColor - outputFirst);
+
+	if (!_fullFrameActive || !_useOriginalMenus || !_menus) {
+		g_system->getPaletteManager()->setPalette(corrected, outputFirst,
+			lastColor - outputFirst);
+		return;
+	}
+
+	// The native menu bar belongs to the Macintosh desktop, not to the
+	// game's indexed frame. Keep its system colors out of viewer fades.
+	byte menuBlack = 0;
+	byte menuWhite = 0;
+	_menus->getMenuColors(menuBlack, menuWhite);
+	int rangeFirst = outputFirst;
+	for (int color = outputFirst; color <= lastColor; ++color) {
+		if (color != lastColor && color != menuBlack &&
+			color != menuWhite)
+			continue;
+
+		if (rangeFirst < color) {
+			g_system->getPaletteManager()->setPalette(
+				corrected + (rangeFirst - outputFirst) * 3,
+				rangeFirst, color - rangeFirst);
+		}
+		rangeFirst = color + 1;
+	}
 }
 
 void MacNebular::getPalette(RGBcolor *palette, int firstColor,


Commit: a0b7b82a905dfd38c2a48557f9c481997cab7a7d
    https://github.com/scummvm/scummvm/commit/a0b7b82a905dfd38c2a48557f9c481997cab7a7d
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:48:38+10:00

Commit Message:
MADS: NEBULAR: Avoid duplicate Macintosh menu fade

The native CODE 133 menu controller performs its palette
transition before dispatching the selected action. Avoid repeating
that transition after the Macintosh menu has already been torn
down, while preserving the existing DOS path.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/nebular/main.cpp


diff --git a/engines/mads/nebular/main.cpp b/engines/mads/nebular/main.cpp
index 7b8bcdf2eed..04e957b28e6 100644
--- a/engines/mads/nebular/main.cpp
+++ b/engines/mads/nebular/main.cpp
@@ -361,7 +361,10 @@ void nebular_main() {
 			main_menu_main();
 			engine->setMacintoshOuterMenuActive(false);
 
-			if (selected_item >= 0) {
+			// Native CODE 133 performs this transition inside
+			// main_menu_main(), before dispatching the selected action.
+			if (selected_item >= 0 &&
+					!engine->usesOriginalMacintoshMenus()) {
 				Common::fill(magic_color_values, magic_color_values + 3, 0);
 				Common::fill(magic_color_flags, magic_color_flags + 3, 0);
 				mcga_getpal(&palette);


Commit: 45a28596d59ba7d15de987314cd9ad16cacbf25a
    https://github.com/scummvm/scummvm/commit/45a28596d59ba7d15de987314cd9ad16cacbf25a
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:48:38+10:00

Commit Message:
MADS: Center initial TextView line

TextView centers each line after the first unless a resource command
supplies an explicit horizontal position. Initialize the first line
the same way so DOS and Macintosh presentations behave consistently.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/textview/textview.cpp


diff --git a/engines/mads/textview/textview.cpp b/engines/mads/textview/textview.cpp
index a38ee5b8a3a..f56bbd8a629 100644
--- a/engines/mads/textview/textview.cpp
+++ b/engines/mads/textview/textview.cpp
@@ -502,7 +502,8 @@ void textview_main(const char *resName, const Presentation &presentation) {
 	timer1 = timer2 = timer3 = 0;
 	has_background = pan_flag = false;
 	flag5 = 0;
-	text_x = text_y = 0;
+	text_x = -1;
+	text_y = 0;
 	font_auto_spacing = -1;
 	room_id = 0;
 	memset(spare, 0, sizeof(spare));


Commit: b9834f08722be59366e2c4d51b01b614c5922062
    https://github.com/scummvm/scummvm/commit/b9834f08722be59366e2c4d51b01b614c5922062
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:48:38+10:00

Commit Message:
MADS: NEBULAR: Refresh Macintosh difficulty cursor

Refresh the backend during every iteration of the native
Macintosh difficulty dialog, matching the Macintosh menu event
loop. This keeps composited cursor movement responsive without
redrawing the dialog for mouse motion.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/nebular/mac_menus.cpp


diff --git a/engines/mads/nebular/mac_menus.cpp b/engines/mads/nebular/mac_menus.cpp
index 43512ce2267..bb27472b619 100644
--- a/engines/mads/nebular/mac_menus.cpp
+++ b/engines/mads/nebular/mac_menus.cpp
@@ -595,7 +595,6 @@ int MacNebularMenu::runDifficultyDialog() {
 			g_system->copyRectToScreen(_screen.getBasePtr(dialog.bounds.left,
 				dialog.bounds.top), _screen.pitch, dialog.bounds.left,
 				dialog.bounds.top, dialog.bounds.width(), dialog.bounds.height());
-			g_system->updateScreen();
 			redraw = false;
 		}
 
@@ -656,8 +655,10 @@ int MacNebularMenu::runDifficultyDialog() {
 			}
 		}
 
-		if (!done)
+		if (!done) {
+			g_system->updateScreen();
 			g_system->delayMillis(10);
+		}
 	}
 
 	_screen.copyRectToSurface(saved.getPixels(), saved.pitch,


Commit: 4f1d749d5236c89eb1de063e1c5390ed44abd180
    https://github.com/scummvm/scummvm/commit/4f1d749d5236c89eb1de063e1c5390ed44abd180
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:48:38+10:00

Commit Message:
MADS: NEBULAR: Hide incomplete Macintosh room frames

Keep the original Macintosh composition on its last complete
frame while MADS initializes a room and its interface. Resume
presentation when the kernel enters active gameplay, avoiding brief
partial-panel frames during transitions.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/nebular/mac_nebular.cpp


diff --git a/engines/mads/nebular/mac_nebular.cpp b/engines/mads/nebular/mac_nebular.cpp
index 7d2e3cd5d86..659a66e0e8b 100644
--- a/engines/mads/nebular/mac_nebular.cpp
+++ b/engines/mads/nebular/mac_nebular.cpp
@@ -564,6 +564,12 @@ void MacNebular::getPalette(RGBcolor *palette, int firstColor,
 }
 
 void MacNebular::presentScreen(int shakeOffset) {
+	// Keep the original Macintosh composition on its previous frame while
+	// the shared engine initializes the next room and interface.
+	if (_useOriginalMenus && !_fullFrameActive &&
+			kernel_mode != KERNEL_ACTIVE_CODE)
+		return;
+
 	if (_fullFrameActive) {
 		const int frameY = _useOriginalMenus ? kMacDesktopSceneY : 0;
 		const byte frameBlack = _useOriginalMenus && _menus ?


Commit: 4b8f06beb6df5420921fed23aed9b7d27436af54
    https://github.com/scummvm/scummvm/commit/4b8f06beb6df5420921fed23aed9b7d27436af54
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:48:38+10:00

Commit Message:
MADS: NEBULAR: Map Macintosh frontend resources

Map the three native ending animation playlists to their Section IX ARes
resources. Also expose the quotes TextView name as an alias of the
existing QUOT resource.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/nebular/mac_resources.cpp


diff --git a/engines/mads/nebular/mac_resources.cpp b/engines/mads/nebular/mac_resources.cpp
index 8f460fc82d2..0a17ff71578 100644
--- a/engines/mads/nebular/mac_resources.cpp
+++ b/engines/mads/nebular/mac_resources.cpp
@@ -236,7 +236,7 @@ MacResourceProvider::ResourceID MacResourceProvider::mapResource(const Common::S
 		return { kGlobalContainer, MKTAG('V', 'o', 'c', 'a'), 1000 };
 	if (name == "OBJECTS.DAT")
 		return { kGlobalContainer, MKTAG('O', 'b', 'j', 'e'), 1000 };
-	if (name == "QUOTES.DAT")
+	if (name == "QUOTES.DAT" || name == "QUOTES.TXR")
 		return { kGlobalContainer, MKTAG('Q', 'u', 'o', 't'), 1000 };
 	if (name == "HOGANUS.DAT")
 		return { kGlobalContainer, MKTAG('H', 'o', 'g', 'a'), 1000 };
@@ -252,6 +252,11 @@ MacResourceProvider::ResourceID MacResourceProvider::mapResource(const Common::S
 		return { kGlobalContainer, MKTAG('B', 't', 's', 'p'), 1000 };
 	if (name == "@REXOPEN")
 		return { kSection9Container, MKTAG('A', 'R', 'e', 's'), 1078 };
+	if (name.hasPrefix("@REXEND") && name.size() == 8 &&
+			name[7] >= '1' && name[7] <= '3') {
+		return { kSection9Container, MKTAG('A', 'R', 'e', 's'),
+			(uint16)(1048 + name[7] - '0') };
+	}
 	if (name == "CREDITS.TXR")
 		return { kGlobalContainer, MKTAG('C', 'R', 'E', 'D'), 1000 };
 




More information about the Scummvm-git-logs mailing list