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

dreammaster noreply at scummvm.org
Fri Aug 14 02:28:09 UTC 2026


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

Summary:
0bfd6c6966 MADS: NEBULAR: Fix Macintosh new-game handoff/GMM
9274079b83 AUDIO: Add Macintosh MACE 3:1 and 6:1 decoding
d4e0cc06ac MADS: NEBULAR: Match Macintosh sound channel semantics
a330a0498a MADS: NEBULAR: Service Macintosh sound playback
75fd279ef5 MADS: Add combined sound playback debugger command
3ffcceb40f MADS: NEBULAR: Add Macintosh dialog controls
b31963c7cf MADS: NEBULAR: Restore Macintosh display preferences
fe680ad7b3 MADS: NEBULAR: Restore Macintosh file dialogs
87396aa459 MADS: NEBULAR: Restore Nice Locked mode
7db4f80f5b MADS: NEBULAR: Enable Macintosh Edit commands
556ae7c5c2 MADS: NEBULAR: Restore Macintosh editable popups
274ca396b7 MADS: NEBULAR: Match Macintosh game text rendering
7d41c5a553 MADS: Enable inventory keyboard scrolling
98e62efa65 MADS: NEBULAR: Restore Macintosh scrollbar drawing
fc8e6cdc2e MADS: NEBULAR: Restore Macintosh modal presentations
d866bf1378 MADS: NEBULAR: Defer Macintosh startup preferences
072d6cbf3f MADS: NEBULAR: Complete Macintosh gameplay handoff
d75c9a8623 MADS: NEBULAR: Preserve legacy Macintosh handoff
a5eb923504 MADS: NEBULAR: Restore Macintosh version text
a24758f521 MADS: NEBULAR: Preserve Macintosh copy-protection ending
f99aeb876f MADS: Add configurable TextView matte height
4dc19bc738 MADS: NEBULAR: Match Macintosh TextView matte region
602387e29b MADS: NEBULAR: Restore Macintosh copy protection dialog
92bf9f8767 MADS: NEBULAR: Match Macintosh copy protection failure
678fc6d2bc MADS: NEBULAR: Preserve Macintosh frontend ownership
d9c770bebc MADS: NEBULAR: Match Macintosh startup order
5bc5a6cd5a MADS: NEBULAR: Preserve Macintosh startup backdrop
7f873fd68b MADS: NEBULAR: Preserve Macintosh TextView handoff
ceeb8b0139 MADS: NEBULAR: Restore Macintosh About presentation


Commit: 0bfd6c6966ca18a844260516d260ea7174b04201
    https://github.com/scummvm/scummvm/commit/0bfd6c6966ca18a844260516d260ea7174b04201
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: NEBULAR: Fix Macintosh new-game handoff/GMM

Present the room-990 outer menu in both Macintosh composition modes.
Avoid routing native dialog events through a menu that is not present.
Release full-frame output through a clean, invalidated frame.
The difficulty dialog then takes ownership without restoring room 990.
Propagate Global Main Menu quit requests to the Rex game loop, and stop
that loop directly when using the native Macintosh Quit command.

Assisted-by: Codex:GPT-5.4

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


diff --git a/engines/mads/nebular/mac_menus.cpp b/engines/mads/nebular/mac_menus.cpp
index bb27472b619..d360c7f1be7 100644
--- a/engines/mads/nebular/mac_menus.cpp
+++ b/engines/mads/nebular/mac_menus.cpp
@@ -444,6 +444,7 @@ void MacNebularMenu::dispatchCommand(int commandId) {
 		break;
 	case kFileQuit:
 		_engine.quitGame();
+		game.going = false;
 		break;
 	case kOptionNoSound: {
 		const bool enableSound = !_engine._musicFlag && !_engine._soundFlag;
@@ -498,7 +499,7 @@ void MacNebularMenu::syncPalette() {
 }
 
 bool MacNebularMenu::processEvent(Common::Event &event) {
-	if (!_windowManager)
+	if (!_windowManager || !_menu)
 		return false;
 
 	updateState();
@@ -704,10 +705,14 @@ void macintoshGameMenu() {
 	const int requestedMenu = kernel.activate_menu;
 	kernel.activate_menu = GAME_NO_MENU;
 
-	if (requestedMenu == GAME_DIFFICULTY_MENU)
+	if (requestedMenu == GAME_DIFFICULTY_MENU) {
 		((RexNebularEngine *)g_engine)->selectMacintoshDifficulty();
-	else if (requestedMenu != GAME_NO_MENU)
+	} else if (requestedMenu != GAME_NO_MENU) {
 		g_engine->openMainMenuDialog();
+
+		if (g_engine->shouldQuit())
+			game.going = false;
+	}
 }
 
 } // namespace RexNebular
diff --git a/engines/mads/nebular/mac_nebular.cpp b/engines/mads/nebular/mac_nebular.cpp
index 659a66e0e8b..95a93bcfb56 100644
--- a/engines/mads/nebular/mac_nebular.cpp
+++ b/engines/mads/nebular/mac_nebular.cpp
@@ -456,9 +456,22 @@ int MacNebular::selectResumeSlot() {
 }
 
 void MacNebular::setOuterMenuActive(bool active) {
+	const bool wasActive = _fullFrameActive;
 	setFullFrameActive(active);
 	if (_menus)
 		_menus->setOuterMenuActive(active);
+
+	if (wasActive && !active) {
+		const byte frameBlack = _useOriginalMenus && _menus ?
+			_menus->getBlackColor() : 0;
+		_output.fillRect(_output.getBounds(), frameBlack);
+		if (_useOriginalMenus && _menus)
+			_menus->draw();
+		g_system->copyRectToScreen(_output.getPixels(), _output.pitch,
+			0, 0, kMacScreenWidth, _output.h);
+		g_system->updateScreen();
+		_engine._screen->markAllDirty();
+	}
 }
 
 Common::Point MacNebular::screenToGame(const Common::Point &point) const {
diff --git a/engines/mads/nebular/main.cpp b/engines/mads/nebular/main.cpp
index 75e4facfaae..5078fc0dcea 100644
--- a/engines/mads/nebular/main.cpp
+++ b/engines/mads/nebular/main.cpp
@@ -331,10 +331,7 @@ void nebular_main() {
 	g_engine->readConfigFile();
 	RexNebularEngine *const engine = (RexNebularEngine *)g_engine;
 
-	if (g_engine->getPlatform() == Common::kPlatformMacintosh &&
-			!engine->usesOriginalMacintoshMenus())
-		selected_item = 0;
-	else if (ConfMan.getBool("start_game") || ConfMan.hasKey("save_slot"))
+	if (ConfMan.getBool("start_game") || ConfMan.hasKey("save_slot"))
 		selected_item = 0;
 	else if (g_engine->isDemo())
 		selected_item = 9;
@@ -352,7 +349,7 @@ void nebular_main() {
 			// selections as the existing MADS controller. Only its native
 			// window composition differs.
 			engine->setMacintoshOuterMenuActive(
-				engine->usesOriginalMacintoshMenus());
+				g_engine->getPlatform() == Common::kPlatformMacintosh);
 			main_menu_main();
 			engine->setMacintoshOuterMenuActive(false);
 


Commit: 9274079b836ca82b5b97df2b4a28febea681eda8
    https://github.com/scummvm/scummvm/commit/9274079b836ca82b5b97df2b4a28febea681eda8
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
AUDIO: Add Macintosh MACE 3:1 and 6:1 decoding

Decode MACE 3:1 and 6:1 streams behind the existing snd interface.
Keep compressed sound header parsing separate from the MACE decoder,
including fixed compression tags, legacy compression IDs, and zero
packet sizes. Pass only a normalized codec type to the decoder.

Assisted-by: Codex:GPT-5.4

Changed paths:
  A audio/decoders/mac_snd_mace.cpp
  A audio/decoders/mac_snd_mace.h
    audio/decoders/mac_snd.cpp
    audio/module.mk


diff --git a/audio/decoders/mac_snd.cpp b/audio/decoders/mac_snd.cpp
index 376fb537ee4..233a0e4e62d 100644
--- a/audio/decoders/mac_snd.cpp
+++ b/audio/decoders/mac_snd.cpp
@@ -26,11 +26,12 @@
  * We implement both type 1 and type 2 snd resources, but only those that are sampled
  */
 
-#include "common/textconsole.h"
 #include "common/stream.h"
 #include "common/substream.h"
+#include "common/textconsole.h"
 
 #include "audio/decoders/mac_snd.h"
+#include "audio/decoders/mac_snd_mace.h"
 #include "audio/decoders/raw.h"
 
 namespace Audio {
@@ -90,23 +91,64 @@ SeekableAudioStream *makeMacSndStream(Common::SeekableReadStream *stream,
 	byte encoding = stream->readByte();
 	stream->readByte(); // base frequency
 
-	if (encoding != 0) {
-		// 0 == PCM
+	if (encoding == 0) {
+		stream->skip(soundDataOffset);
+		const uint32 availableSize = stream->pos() < stream->size() ? stream->size() - stream->pos() : 0;
+		if (size > availableSize) {
+			warning("makeMacSndStream(): Sample size %u exceeds available resource data %u", size, availableSize);
+			size = availableSize;
+		}
+
+		Common::SeekableReadStream *dataStream = new Common::SeekableSubReadStream(stream, stream->pos(), stream->pos() + size, disposeAfterUse);
+
+		// Since we allocated our own stream for the data, we must specify DisposeAfterUse::YES.
+		return makeRawStream(dataStream, rate, Audio::FLAG_UNSIGNED);
+	}
+
+	if (encoding != 0xfe) {
 		warning("makeMacSndStream(): Unsupported compression %d", encoding);
 		return nullptr;
 	}
 
-	stream->skip(soundDataOffset);
-	const uint32 availableSize = stream->pos() < stream->size() ? stream->size() - stream->pos() : 0;
-	if (size > availableSize) {
-		warning("makeMacSndStream(): Sample size %u exceeds available resource data %u", size, availableSize);
-		size = availableSize;
+	// In a compressed sound header, the field occupying the old length slot
+	// is numChannels, followed by numFrames. Apple defines numFrames here as
+	// the number of compressed packet frames, not the number of decoded PCM
+	// frames. A packet contains data for every interleaved channel, and each
+	// MACE packet frame expands to six PCM frames per channel.
+	const uint32 channels = size;
+	const uint32 packetFrameCount = stream->readUint32BE();
+	stream->skip(10); // AIFF sample rate
+	stream->readUint32BE(); // marker chunk
+	const uint32 format = stream->readUint32BE();
+	stream->readUint32BE(); // future use
+	stream->readUint32BE(); // codec state
+	stream->readUint32BE(); // leftover samples
+	const uint16 compressionID = stream->readUint16BE();
+	const uint16 packetSize = stream->readUint16BE();
+	stream->readUint16BE(); // synthesizer ID
+	const uint16 sampleSize = stream->readUint16BE();
+
+	MacSndMACEType maceType;
+	if (compressionID == 3 ||
+			(compressionID == 0xffff && format == MKTAG('M', 'A', 'C', '3'))) {
+		maceType = kMacSndMACE3;
+	} else if (compressionID == 4 ||
+			(compressionID == 0xffff && format == MKTAG('M', 'A', 'C', '6'))) {
+		maceType = kMacSndMACE6;
+	} else {
+		warning("makeMacSndStream(): Unsupported compressed sound format");
+		return nullptr;
 	}
 
-	Common::SeekableReadStream *dataStream = new Common::SeekableSubReadStream(stream, stream->pos(), stream->pos() + size, disposeAfterUse);
+	const uint16 expectedPacketSize = maceType == kMacSndMACE3 ? 16 : 8;
+	if (sampleSize != 8 ||
+			(packetSize != 0 && packetSize != expectedPacketSize)) {
+		warning("makeMacSndStream(): Unsupported MACE packet layout");
+		return nullptr;
+	}
 
-	// Since we allocated our own stream for the data, we must specify DisposeAfterUse::YES.
-	return makeRawStream(dataStream, rate, Audio::FLAG_UNSIGNED);
+	return makeMacSndMACEStream(stream, disposeAfterUse, rate,
+		packetFrameCount, channels, maceType);
 }
 
 } // End of namespace Audio
diff --git a/audio/decoders/mac_snd_mace.cpp b/audio/decoders/mac_snd_mace.cpp
new file mode 100644
index 00000000000..70f77ca1deb
--- /dev/null
+++ b/audio/decoders/mac_snd_mace.cpp
@@ -0,0 +1,287 @@
+/* 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/>.
+ *
+ */
+ /*
+ * MACE decoder, derived from FFmpeg.
+ *
+ * Original copyright note:
+ * MACE decoder
+ * Copyright (c) 2002 Laszlo Torok <torokl at alpha.dfmk.hu>
+ *
+ * Adapted to libavcodec by Francois Revol <revol at free.fr>.
+ */
+
+#include "common/memstream.h"
+#include "common/stream.h"
+#include "common/textconsole.h"
+
+#include "audio/decoders/mac_snd_mace.h"
+#include "audio/decoders/raw.h"
+
+namespace Audio {
+
+namespace {
+
+struct MaceChannelState {
+	int16 index;
+	int16 factor;
+	int16 previous2;
+	int16 previous;
+	int16 level;
+};
+
+static const int16 kMaceAdapt3[8] = {
+	-13, 8, 76, 222, 222, 76, 8, -13
+};
+
+static const int16 kMaceAdapt2[4] = {
+	-18, 140, 140, -18
+};
+
+static const int16 kMaceQuant3[128][4] = {
+	{37, 116, 206, 330}, {39, 121, 216, 346},
+	{41, 127, 225, 361}, {42, 132, 235, 377},
+	{44, 137, 245, 392}, {46, 144, 256, 410},
+	{48, 150, 267, 428}, {51, 157, 280, 449},
+	{53, 165, 293, 470}, {55, 172, 306, 490},
+	{58, 179, 319, 511}, {60, 187, 333, 534},
+	{63, 195, 348, 557}, {66, 205, 364, 583},
+	{69, 214, 380, 609}, {72, 223, 396, 635},
+	{75, 233, 414, 663}, {79, 244, 433, 694},
+	{82, 254, 453, 725}, {86, 265, 472, 756},
+	{90, 278, 495, 792}, {94, 290, 516, 826},
+	{98, 303, 538, 862}, {102, 316, 562, 901},
+	{107, 331, 588, 942}, {112, 345, 614, 983},
+	{117, 361, 641, 1027}, {122, 377, 670, 1074},
+	{127, 394, 701, 1123}, {133, 411, 732, 1172},
+	{139, 430, 764, 1224}, {145, 449, 799, 1280},
+	{152, 469, 835, 1337}, {159, 490, 872, 1397},
+	{166, 512, 911, 1459}, {173, 535, 951, 1523},
+	{181, 558, 993, 1590}, {189, 584, 1038, 1663},
+	{197, 610, 1085, 1738}, {206, 637, 1133, 1815},
+	{215, 665, 1183, 1895}, {225, 695, 1237, 1980},
+	{235, 726, 1291, 2068}, {246, 759, 1349, 2161},
+	{257, 792, 1409, 2257}, {268, 828, 1472, 2357},
+	{280, 865, 1538, 2463}, {293, 903, 1606, 2572},
+	{306, 944, 1678, 2688}, {319, 986, 1753, 2807},
+	{334, 1030, 1832, 2933}, {349, 1076, 1914, 3065},
+	{364, 1124, 1999, 3202}, {380, 1174, 2088, 3344},
+	{398, 1227, 2182, 3494}, {415, 1281, 2278, 3649},
+	{434, 1339, 2380, 3811}, {453, 1398, 2486, 3982},
+	{473, 1461, 2598, 4160}, {495, 1526, 2714, 4346},
+	{517, 1594, 2835, 4540}, {540, 1665, 2961, 4741},
+	{564, 1740, 3093, 4953}, {589, 1818, 3232, 5175},
+	{615, 1898, 3375, 5405}, {643, 1984, 3527, 5647},
+	{671, 2072, 3683, 5898}, {701, 2164, 3848, 6161},
+	{733, 2261, 4020, 6438}, {766, 2362, 4199, 6724},
+	{800, 2467, 4386, 7024}, {836, 2578, 4583, 7339},
+	{873, 2692, 4786, 7664}, {912, 2813, 5001, 8008},
+	{952, 2938, 5223, 8364}, {995, 3070, 5457, 8739},
+	{1039, 3207, 5701, 9129}, {1086, 3350, 5956, 9537},
+	{1134, 3499, 6220, 9960}, {1185, 3655, 6497, 10404},
+	{1238, 3818, 6788, 10869}, {1293, 3989, 7091, 11355},
+	{1351, 4166, 7407, 11861}, {1411, 4352, 7738, 12390},
+	{1474, 4547, 8084, 12946}, {1540, 4750, 8444, 13522},
+	{1609, 4962, 8821, 14126}, {1680, 5183, 9215, 14756},
+	{1756, 5415, 9626, 15415}, {1834, 5657, 10057, 16104},
+	{1916, 5909, 10505, 16822}, {2001, 6173, 10975, 17574},
+	{2091, 6448, 11463, 18356}, {2184, 6736, 11974, 19175},
+	{2282, 7037, 12510, 20032}, {2383, 7351, 13068, 20926},
+	{2490, 7679, 13652, 21861}, {2601, 8021, 14260, 22834},
+	{2717, 8380, 14897, 23854}, {2838, 8753, 15561, 24918},
+	{2965, 9144, 16256, 26031}, {3097, 9553, 16982, 27193},
+	{3236, 9979, 17740, 28407}, {3380, 10424, 18532, 29675},
+	{3531, 10890, 19359, 31000}, {3688, 11375, 20222, 32382},
+	{3853, 11883, 21125, 32767}, {4025, 12414, 22069, 32767},
+	{4205, 12967, 23053, 32767}, {4392, 13546, 24082, 32767},
+	{4589, 14151, 25157, 32767}, {4793, 14783, 26280, 32767},
+	{5007, 15442, 27452, 32767}, {5231, 16132, 28678, 32767},
+	{5464, 16851, 29957, 32767}, {5708, 17603, 31294, 32767},
+	{5963, 18389, 32691, 32767}, {6229, 19210, 32767, 32767},
+	{6507, 20067, 32767, 32767}, {6797, 20963, 32767, 32767},
+	{7101, 21899, 32767, 32767}, {7418, 22876, 32767, 32767},
+	{7749, 23897, 32767, 32767}, {8095, 24964, 32767, 32767},
+	{8456, 26078, 32767, 32767}, {8833, 27242, 32767, 32767},
+	{9228, 28457, 32767, 32767}, {9639, 29727, 32767, 32767}
+};
+
+static const int16 kMaceQuant2[128][2] = {
+	{64, 216}, {67, 226}, {70, 236}, {74, 246},
+	{77, 257}, {80, 268}, {84, 280}, {88, 294},
+	{92, 307}, {96, 321}, {100, 334}, {104, 350},
+	{109, 365}, {114, 382}, {119, 399}, {124, 416},
+	{130, 434}, {136, 454}, {142, 475}, {148, 495},
+	{155, 519}, {162, 541}, {169, 564}, {176, 590},
+	{185, 617}, {193, 644}, {201, 673}, {210, 703},
+	{220, 735}, {230, 767}, {240, 801}, {251, 838},
+	{262, 876}, {274, 914}, {286, 955}, {299, 997},
+	{312, 1041}, {326, 1089}, {341, 1138}, {356, 1188},
+	{372, 1241}, {388, 1297}, {406, 1354}, {424, 1415},
+	{443, 1478}, {462, 1544}, {483, 1613}, {505, 1684},
+	{527, 1760}, {551, 1838}, {576, 1921}, {601, 2007},
+	{628, 2097}, {656, 2190}, {686, 2288}, {716, 2389},
+	{748, 2496}, {781, 2607}, {816, 2724}, {853, 2846},
+	{891, 2973}, {930, 3104}, {972, 3243}, {1016, 3389},
+	{1061, 3539}, {1108, 3698}, {1158, 3862}, {1209, 4035},
+	{1264, 4216}, {1320, 4403}, {1379, 4599}, {1441, 4806},
+	{1505, 5019}, {1572, 5244}, {1642, 5477}, {1715, 5722},
+	{1792, 5978}, {1872, 6245}, {1955, 6522}, {2043, 6813},
+	{2134, 7118}, {2229, 7436}, {2329, 7767}, {2432, 8114},
+	{2541, 8477}, {2655, 8854}, {2773, 9250}, {2897, 9663},
+	{3026, 10094}, {3162, 10546}, {3303, 11016}, {3450, 11508},
+	{3604, 12020}, {3765, 12556}, {3933, 13118}, {4108, 13703},
+	{4292, 14315}, {4483, 14953}, {4683, 15621}, {4892, 16318},
+	{5111, 17046}, {5339, 17807}, {5577, 18602}, {5826, 19433},
+	{6086, 20300}, {6358, 21205}, {6642, 22152}, {6938, 23141},
+	{7248, 24173}, {7571, 25252}, {7909, 26380}, {8262, 27557},
+	{8631, 28786}, {9016, 30072}, {9419, 31413}, {9839, 32767},
+	{10278, 32767}, {10737, 32767}, {11216, 32767}, {11717, 32767},
+	{12240, 32767}, {12786, 32767}, {13356, 32767}, {13953, 32767},
+	{14576, 32767}, {15226, 32767}, {15906, 32767}, {16615, 32767}
+};
+
+static int16 clipMaceSample(int32 sample) {
+	if (sample > 32767)
+		return 32767;
+	if (sample < -32768)
+		return -32767;
+	return sample;
+}
+
+static int16 readMaceQuantizer(MaceChannelState &state, byte value, bool twoBit) {
+	const int stride = twoBit ? 2 : 4;
+	const int row = (state.index & 0x7f0) >> 4;
+	int16 sample;
+
+	if (value < stride)
+		sample = twoBit ? kMaceQuant2[row][value] : kMaceQuant3[row][value];
+	else {
+		const int column = 2 * stride - value - 1;
+		sample = -1 - (twoBit ? kMaceQuant2[row][column] : kMaceQuant3[row][column]);
+	}
+
+	state.index += (twoBit ? kMaceAdapt2[value] : kMaceAdapt3[value]) - (state.index >> 5);
+	if (state.index < 0)
+		state.index = 0;
+	return sample;
+}
+
+static byte maceOutputByte(int32 sample) {
+	return (byte)(((uint16)sample >> 8) ^ 0x80);
+}
+
+static byte decodeMace3Code(MaceChannelState &state, byte value, bool twoBit) {
+	int16 sample = clipMaceSample(readMaceQuantizer(state, value, twoBit) + state.level);
+	state.level = sample - (sample >> 3);
+	return maceOutputByte(sample);
+}
+
+static void decodeMace6Code(MaceChannelState &state, byte value, bool twoBit, byte *output) {
+	int16 sample = readMaceQuantizer(state, value, twoBit);
+	if ((state.previous ^ sample) >= 0)
+		state.factor = MIN<int32>(state.factor + 506, 32767);
+	else if (state.factor - 314 < -32768)
+		state.factor = -32767;
+	else
+		state.factor -= 314;
+
+	sample = clipMaceSample(sample + state.level);
+	state.level = (sample * state.factor) >> 15;
+	sample >>= 1;
+
+	output[0] = maceOutputByte(state.previous + state.previous2 - ((state.previous2 - sample) >> 2));
+	output[1] = maceOutputByte(state.previous + sample + ((state.previous2 - sample) >> 2));
+	state.previous2 = state.previous;
+	state.previous = sample;
+}
+
+static void decodeMacePacket(MaceChannelState &state, const byte *input,
+		bool mace3, byte *output) {
+	if (mace3) {
+		for (int byteIndex = 0; byteIndex < 2; ++byteIndex) {
+			const byte value = input[byteIndex];
+			output[byteIndex * 3] = decodeMace3Code(state, value & 7, false);
+			output[byteIndex * 3 + 1] = decodeMace3Code(state, (value >> 3) & 3, true);
+			output[byteIndex * 3 + 2] = decodeMace3Code(state, value >> 5, false);
+		}
+	} else {
+		const byte value = input[0];
+		decodeMace6Code(state, value >> 5, false, output);
+		decodeMace6Code(state, (value >> 3) & 3, true, output + 2);
+		decodeMace6Code(state, value & 7, false, output + 4);
+	}
+}
+
+} // End of anonymous namespace
+
+SeekableAudioStream *makeMacSndMACEStream(Common::SeekableReadStream *stream,
+		DisposeAfterUse::Flag disposeAfterUse, uint16 rate,
+		uint32 packetFrameCount, uint32 channels, MacSndMACEType type) {
+	if (channels < 1 || channels > 2) {
+		warning("makeMacSndMACEStream(): Unsupported channel count %u", channels);
+		return nullptr;
+	}
+
+	const bool mace3 = type == kMacSndMACE3;
+	const uint32 bytesPerChannel = mace3 ? 2 : 1;
+	const uint32 bytesPerPacket = bytesPerChannel * channels;
+	const uint32 availablePackets = stream->pos() < stream->size() ?
+		(stream->size() - stream->pos()) / bytesPerPacket : 0;
+
+	if (packetFrameCount > availablePackets) {
+		warning("makeMacSndMACEStream(): MACE packet count %u exceeds available data %u",
+			packetFrameCount, availablePackets);
+		packetFrameCount = availablePackets;
+	}
+
+	if (packetFrameCount > 0xffffffffU / (6 * channels)) {
+		warning("makeMacSndMACEStream(): MACE sample count is too large");
+		return nullptr;
+	}
+
+	const uint32 outputSize = packetFrameCount * 6 * channels;
+	byte *decoded = new byte[outputSize];
+	MaceChannelState states[2] = {};
+	byte channelSamples[2][6];
+
+	for (uint32 frame = 0; frame < packetFrameCount; ++frame) {
+		byte packet[4];
+		stream->read(packet, bytesPerPacket);
+		for (uint32 channel = 0; channel < channels; ++channel)
+			decodeMacePacket(states[channel], packet + channel * bytesPerChannel,
+				mace3, channelSamples[channel]);
+
+		for (uint32 sample = 0; sample < 6; ++sample) {
+			for (uint32 channel = 0; channel < channels; ++channel)
+				decoded[(frame * 6 + sample) * channels + channel] = channelSamples[channel][sample];
+		}
+	}
+
+	if (disposeAfterUse == DisposeAfterUse::YES)
+		delete stream;
+
+	byte flags = Audio::FLAG_UNSIGNED;
+	if (channels == 2)
+		flags |= Audio::FLAG_STEREO;
+	return makeRawStream(new Common::MemoryReadStream(decoded, outputSize,
+		DisposeAfterUse::YES), rate, flags);
+}
+
+} // End of namespace Audio
diff --git a/audio/decoders/mac_snd_mace.h b/audio/decoders/mac_snd_mace.h
new file mode 100644
index 00000000000..808457a73d0
--- /dev/null
+++ b/audio/decoders/mac_snd_mace.h
@@ -0,0 +1,49 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef AUDIO_MAC_SND_MACE_H
+#define AUDIO_MAC_SND_MACE_H
+
+#include "common/scummsys.h"
+#include "common/types.h"
+
+namespace Common {
+class SeekableReadStream;
+}
+
+namespace Audio {
+
+class SeekableAudioStream;
+
+enum MacSndMACEType {
+	kMacSndMACE3,
+	kMacSndMACE6
+};
+
+SeekableAudioStream *makeMacSndMACEStream(
+	Common::SeekableReadStream *stream,
+	DisposeAfterUse::Flag disposeAfterUse,
+	uint16 rate, uint32 packetFrameCount, uint32 channels,
+	MacSndMACEType type);
+
+} // End of namespace Audio
+
+#endif
diff --git a/audio/module.mk b/audio/module.mk
index a5078213e6c..d91be52931d 100644
--- a/audio/module.mk
+++ b/audio/module.mk
@@ -40,6 +40,7 @@ MODULE_OBJS := \
 	decoders/g711.o \
 	decoders/iff_sound.o \
 	decoders/mac_snd.o \
+	decoders/mac_snd_mace.o \
 	decoders/mp3.o \
 	decoders/qdm2.o \
 	decoders/quicktime.o \


Commit: d4e0cc06aca731eaaa95354623e114e5b707203d
    https://github.com/scummvm/scummvm/commit/d4e0cc06aca731eaaa95354623e114e5b707203d
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: NEBULAR: Match Macintosh sound channel semantics

Model the native pair of priority-aware sound channels, delayed and
repeated playback, volume changes, and the four-entry queue. Keep
resources with multiple native call variants explicit at their Nebular
call sites.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/core/sound_manager.h
    engines/mads/nebular/rooms/forcefield.cpp
    engines/mads/nebular/rooms/room102.cpp
    engines/mads/nebular/rooms/room202.cpp
    engines/mads/nebular/rooms/room316.cpp
    engines/mads/nebular/rooms/room703.cpp
    engines/mads/nebular/rooms/room704.cpp
    engines/mads/nebular/rooms/room705.cpp
    engines/mads/nebular/rooms/section1.cpp
    engines/mads/nebular/sound/mac_sound.cpp
    engines/mads/nebular/sound/mac_sound.h


diff --git a/engines/mads/core/sound_manager.h b/engines/mads/core/sound_manager.h
index af34c331987..c48aaea47ce 100644
--- a/engines/mads/core/sound_manager.h
+++ b/engines/mads/core/sound_manager.h
@@ -166,7 +166,7 @@ public:
 	/**
 	 * Stop queueing sound commands, and execute any previously queued ones
 	 */
-	void startQueuedCommands();
+	virtual void startQueuedCommands();
 
 	/**
 	 * Set the master volume
diff --git a/engines/mads/nebular/rooms/forcefield.cpp b/engines/mads/nebular/rooms/forcefield.cpp
index 9b616d95acb..69538a15486 100644
--- a/engines/mads/nebular/rooms/forcefield.cpp
+++ b/engines/mads/nebular/rooms/forcefield.cpp
@@ -21,6 +21,7 @@
 
 #include "mads/nebular/rooms/forcefield.h"
 #include "mads/mads.h"
+#include "mads/nebular/sound/mac_sound.h"
 
 namespace MADS {
 namespace RexNebular {
@@ -43,7 +44,8 @@ void init_forcefield(Forcefield *force, bool flag) {
 	for (int count = 0; count < 40; count++)
 		force->_seqId[count] = -1;
 
-	if (force->_flag)
+	if (force->_flag && !Sound::commandMacintoshSound(Sound::kMacSoundPlay,
+			3024, 0, 0, 0, true))
 		g_engine->_soundManager->command(24);
 }
 
diff --git a/engines/mads/nebular/rooms/room102.cpp b/engines/mads/nebular/rooms/room102.cpp
index 50e15fc5b6b..4e9c0bf8859 100644
--- a/engines/mads/nebular/rooms/room102.cpp
+++ b/engines/mads/nebular/rooms/room102.cpp
@@ -26,6 +26,7 @@
 #include "mads/nebular/mads/inventory.h"
 #include "mads/nebular/mads/words.h"
 #include "mads/nebular/rooms/section1.h"
+#include "mads/nebular/sound/mac_sound.h"
 
 namespace MADS {
 namespace RexNebular {
@@ -121,8 +122,12 @@ static void room_102_init() {
 		g_sequence_ids[13] = kernel_seq_forward(g_sprite_ids[13], false, 6, 0, 0, 1);
 		kernel_seq_trigger(g_sequence_ids[13], KERNEL_TRIGGER_EXPIRE, 0, 72);
 		kernel_seq_depth(g_sequence_ids[13], 5);
-		g_engine->_soundManager->command(24, 0);
-		g_engine->_soundManager->command(28, 0);
+		if (!Sound::commandMacintoshSound(Sound::kMacSoundPlay, 1024,
+				0, 0, 0, true))
+			g_engine->_soundManager->command(24, 0);
+		if (!Sound::commandMacintoshSound(Sound::kMacSoundPlay, 1028,
+				0, 0, 0, true))
+			g_engine->_soundManager->command(28, 0);
 	}
 
 	local._fridgeOpenedFl = false;
@@ -137,8 +142,11 @@ static void room_102_init() {
 		quote + 0, quote + 1, quote + 2, quote + 3, quote + 4, quote + 5,
 		quote + 6, quote + 7, quote + 8, quote + 9, quote + 10, 0);
 
-	if (previous_room == 101)
-		g_engine->_soundManager->command(20, 0);
+	if (previous_room == 101) {
+		if (!Sound::commandMacintoshSound(Sound::kMacSoundPlay, 1020,
+				0, 0, 0, true))
+			g_engine->_soundManager->command(20, 0);
+	}
 }
 
 static void room_102_daemon() {
diff --git a/engines/mads/nebular/rooms/room202.cpp b/engines/mads/nebular/rooms/room202.cpp
index 972b1f061e2..d713d325bc3 100644
--- a/engines/mads/nebular/rooms/room202.cpp
+++ b/engines/mads/nebular/rooms/room202.cpp
@@ -25,6 +25,7 @@
 #include "mads/nebular/mads/inventory.h"
 #include "mads/nebular/mads/words.h"
 #include "mads/nebular/rooms/section2.h"
+#include "mads/nebular/sound/mac_sound.h"
 
 namespace MADS {
 namespace RexNebular {
@@ -430,7 +431,9 @@ static void room_202_pre_parser() {
 
 	if (local._ladderTopFl && (player_said_2(climb_down, ladder) || player.need_to_walk)) {
 		if (kernel.trigger == 0) {
-			g_engine->_soundManager->command(29, 0);
+			if (!Sound::commandMacintoshSound(
+					Sound::kMacSoundPlayRepeatedVolume, 2029, 2, 200))
+				g_engine->_soundManager->command(29, 0);
 			player.ready_to_walk = false;
 			player.commands_allowed = false;
 			kernel_seq_delete(g_sequence_ids[9]);
@@ -521,7 +524,9 @@ static void room_202_parser() {
 	} else if (player_said_2(climb_up, ladder) && !global[kLadderBroken]) {
 		switch (kernel.trigger) {
 		case 0:
-			g_engine->_soundManager->command(29, 0);
+			if (!Sound::commandMacintoshSound(Sound::kMacSoundWait,
+					2029, 60, 2))
+				g_engine->_soundManager->command(29, 0);
 			local._meteoClock1 = kernel.clock;
 			player.walker_visible = false;
 			player.commands_allowed = false;
diff --git a/engines/mads/nebular/rooms/room316.cpp b/engines/mads/nebular/rooms/room316.cpp
index 46180e922e0..7165b4a4f26 100644
--- a/engines/mads/nebular/rooms/room316.cpp
+++ b/engines/mads/nebular/rooms/room316.cpp
@@ -26,6 +26,7 @@
 #include "mads/nebular/mads/inventory.h"
 #include "mads/nebular/mads/words.h"
 #include "mads/nebular/rooms/section3.h"
+#include "mads/nebular/sound/mac_sound.h"
 
 namespace MADS {
 namespace RexNebular {
@@ -295,7 +296,9 @@ static void room_316_init() {
 		player.facing = FACING_SOUTH;
 		player.commands_allowed = false;
 		player.walker_visible = false;
-		g_engine->_soundManager->command(44, 0);
+		if (!Sound::commandMacintoshSound(Sound::kMacSoundPlay, 3044,
+				0, 0, 0, true))
+			g_engine->_soundManager->command(44, 0);
 		int spriteIdx = (global[kSexOfRex] == REX_MALE) ? 1 : 2;
 		g_sequence_ids[1] = kernel_seq_backward(g_sprite_ids[spriteIdx], false, 6, 0, 0, 1);
 		kernel_seq_depth(g_sequence_ids[1], 2);
diff --git a/engines/mads/nebular/rooms/room703.cpp b/engines/mads/nebular/rooms/room703.cpp
index f9d252bdfc7..ffab3f444ac 100644
--- a/engines/mads/nebular/rooms/room703.cpp
+++ b/engines/mads/nebular/rooms/room703.cpp
@@ -26,6 +26,7 @@
 #include "mads/nebular/mads/inventory.h"
 #include "mads/nebular/mads/words.h"
 #include "mads/nebular/rooms/section7.h"
+#include "mads/nebular/sound/mac_sound.h"
 #include "mads/nebular/rooms/dialog.h"
 
 namespace MADS {
@@ -198,7 +199,9 @@ static void room_703_init() {
 	kernel.quotes = quote_load(785, 786, 787, 788, 789, 0);
 	local._dialog1.setup(0x98, 0x311, 0x312, 0x313, 0x314, 0x315, 0);
 	section_7_music();
-	g_engine->_soundManager->command(28, 0);
+	if (!Sound::commandMacintoshSound(Sound::kMacSoundPlay, 7028,
+			0, 0, 0, true))
+		g_engine->_soundManager->command(28, 0);
 }
 
 static void room_703_daemon() {
diff --git a/engines/mads/nebular/rooms/room704.cpp b/engines/mads/nebular/rooms/room704.cpp
index bb689165444..212318987ab 100644
--- a/engines/mads/nebular/rooms/room704.cpp
+++ b/engines/mads/nebular/rooms/room704.cpp
@@ -25,6 +25,7 @@
 #include "mads/nebular/mads/inventory.h"
 #include "mads/nebular/mads/words.h"
 #include "mads/nebular/rooms/section7.h"
+#include "mads/nebular/sound/mac_sound.h"
 #include "mads/nebular/rooms/dialog.h"
 
 namespace MADS {
@@ -172,7 +173,9 @@ static void room_704_init() {
 	local._dialog1.setup(0x98, 0x311, 0x312, 0x313, 0x314, 0x315, 0);
 
 	section_7_music();
-	g_engine->_soundManager->command(28, 0);
+	if (!Sound::commandMacintoshSound(Sound::kMacSoundPlay, 7028,
+			0, 0, 0, true))
+		g_engine->_soundManager->command(28, 0);
 }
 
 static void room_704_daemon() {
diff --git a/engines/mads/nebular/rooms/room705.cpp b/engines/mads/nebular/rooms/room705.cpp
index 16555e0c73c..1dacb7ff893 100644
--- a/engines/mads/nebular/rooms/room705.cpp
+++ b/engines/mads/nebular/rooms/room705.cpp
@@ -25,6 +25,7 @@
 #include "mads/nebular/mads/inventory.h"
 #include "mads/nebular/mads/words.h"
 #include "mads/nebular/rooms/section7.h"
+#include "mads/nebular/sound/mac_sound.h"
 #include "mads/nebular/rooms/dialog.h"
 
 namespace MADS {
@@ -131,7 +132,9 @@ static void room_705_init() {
 	} else if (previous_room != KERNEL_RESTORING_GAME) {
 		player.commands_allowed = false;
 		kernel_timing_trigger(1, 80);
-		g_engine->_soundManager->command(28, 0);
+		if (!Sound::commandMacintoshSound(Sound::kMacSoundPlay, 7028,
+				0, 0, 0, true))
+			g_engine->_soundManager->command(28, 0);
 	} else
 		g_sequence_ids[3] = kernel_seq_stamp(g_sprite_ids[3], false, 1);
 
diff --git a/engines/mads/nebular/rooms/section1.cpp b/engines/mads/nebular/rooms/section1.cpp
index c7ea33934ae..e1aa339f66a 100644
--- a/engines/mads/nebular/rooms/section1.cpp
+++ b/engines/mads/nebular/rooms/section1.cpp
@@ -24,6 +24,7 @@
 #include "mads/core/pal.h"
 #include "mads/nebular/nebular.h"
 #include "mads/nebular/rooms/section1.h"
+#include "mads/nebular/sound/mac_sound.h"
 #include "mads/nebular/global.h"
 
 namespace MADS {
@@ -136,7 +137,9 @@ void section_1_music() {
 	if (config_file.music_flag) {
 		switch (new_room) {
 		case 101:
-			g_engine->_soundManager->command(11, 0);
+			if (!Sound::commandMacintoshSound(Sound::kMacSoundPlayPriority,
+					1011, 0, 0, 0, true))
+				g_engine->_soundManager->command(11, 0);
 			break;
 		case 102:
 			g_engine->_soundManager->command(12, 0);
diff --git a/engines/mads/nebular/sound/mac_sound.cpp b/engines/mads/nebular/sound/mac_sound.cpp
index c1a9ae4a703..18842341348 100644
--- a/engines/mads/nebular/sound/mac_sound.cpp
+++ b/engines/mads/nebular/sound/mac_sound.cpp
@@ -22,32 +22,275 @@
 #include "audio/audiostream.h"
 #include "audio/decoders/mac_snd.h"
 #include "common/textconsole.h"
+#include "mads/mads.h"
 #include "mads/nebular/mac_resources.h"
+#include "mads/nebular/nebular.h"
 #include "mads/nebular/sound/mac_sound.h"
 
 namespace MADS {
 namespace RexNebular {
 namespace Sound {
 
+struct MacSoundBehavior {
+	int resourceID;
+	MacSoundOperation operation;
+	int argument1;
+	int argument2;
+	int argument3;
+	bool queued;
+};
+
+static const MacSoundBehavior kMacSoundBehaviors[] = {
+	// Resources with multiple native call variants are handled explicitly at
+	// their Nebular call sites instead of being inferred from elapsed time.
+	{1015, kMacSoundPlayPriority, 0, 0, 0, true},
+	{1027, kMacSoundChangeVolume, 0, 0, 0, false},
+	{1028, kMacSoundPlay, 0, 0, 0, true},
+	{1029, kMacSoundChangeVolume, 0, 0, 0, false},
+	{1030, kMacSoundChangeVolume, 0, 0, 0, false},
+	{1032, kMacSoundChangeVolume, 0, 0, 0, false},
+	{1035, kMacSoundWait, 120, 0, 0, false},
+	{1037, kMacSoundWait, 120, 0, 0, false},
+	{2037, kMacSoundPlay, 0, 0, 0, true},
+	{2041, kMacSoundWait, 150, 0, 0, false},
+	{3011, kMacSoundPlayPriority, 0, 0, 0, false},
+	{8014, kMacSoundPlay, 0, 0, 0, true},
+	{8019, kMacSoundPlay, 0, 0, 0, true}
+};
+
+MacSoundVoice::MacSoundVoice() : resourceID(0), volume(Audio::Mixer::kMaxChannelVolume),
+		priority(false), delayed(false), delayEndTick(0) {
+	delayedCommand.operation = kMacSoundPlay;
+	delayedCommand.resourceID = 0;
+	delayedCommand.argument1 = 0;
+	delayedCommand.argument2 = 0;
+	delayedCommand.argument3 = 0;
+}
+
+MacSoundQueueEntry::MacSoundQueueEntry() : used(false) {
+	command.operation = kMacSoundPlay;
+	command.resourceID = 0;
+	command.argument1 = 0;
+	command.argument2 = 0;
+	command.argument3 = 0;
+}
+
 MacSoundDriver::MacSoundDriver(Audio::Mixer *mixer, MacResourceProvider *resources, int section) :
-		SoundDriver(mixer), _resources(resources), _section(section) {
+		SoundDriver(mixer), _resources(resources), _section(section),
+		_volume(Audio::Mixer::kMaxChannelVolume), _paused(false), _ramping(false),
+		_rampVolume(0), _serviceTick(0) {
 }
 
 void MacSoundDriver::setPaused(bool paused) {
 	if (_paused == paused)
 		return;
 	_paused = paused;
-	for (uint i = 0; i < _handles.size(); ++i) {
-		if (_mixer->isSoundHandleActive(_handles[i]))
-			_mixer->pauseHandle(_handles[i], paused);
+	for (int i = 0; i < 2; ++i) {
+		if (_mixer->isSoundHandleActive(_voices[i].handle))
+			_mixer->pauseHandle(_voices[i].handle, paused);
+	}
+}
+
+void MacSoundDriver::updateVoice(MacSoundVoice &voice) {
+	if (!voice.delayed && voice.resourceID != 0 &&
+			!_mixer->isSoundHandleActive(voice.handle)) {
+		voice.resourceID = 0;
+		voice.priority = false;
+	}
+}
+
+MacSoundVoice *MacSoundDriver::selectVoice() {
+	for (int i = 0; i < 2; ++i) {
+		updateVoice(_voices[i]);
+		if (_voices[i].resourceID == 0)
+			return &_voices[i];
+	}
+	for (int i = 0; i < 2; ++i) {
+		if (!_voices[i].priority)
+			return &_voices[i];
+	}
+	return nullptr;
+}
+
+int MacSoundDriver::effectiveVolume(int volume) const {
+	return volume * _volume / Audio::Mixer::kMaxChannelVolume;
+}
+
+void MacSoundDriver::setVoiceVolume(MacSoundVoice &voice, int volume) {
+	voice.volume = CLIP<int>(volume, 0, Audio::Mixer::kMaxChannelVolume);
+	if (_mixer->isSoundHandleActive(voice.handle))
+		_mixer->setChannelVolume(voice.handle, (byte)effectiveVolume(voice.volume));
+}
+
+void MacSoundDriver::play(const MacSoundCommand &command) {
+	MacSoundVoice *voice = selectVoice();
+	if (!voice)
+		return;
+
+	if (_mixer->isSoundHandleActive(voice->handle))
+		_mixer->stopHandle(voice->handle);
+	voice->delayed = false;
+
+	Common::SeekableReadStream *resource = _resources->openSound(
+		command.resourceID / 1000, command.resourceID % 1000);
+	if (!resource)
+		return;
+
+	Audio::SeekableAudioStream *stream = Audio::makeMacSndStream(resource,
+		DisposeAfterUse::YES);
+	if (!stream) {
+		delete resource;
+		warning("Could not decode Macintosh sound resource %d", command.resourceID);
+		return;
+	}
+
+	int repeats = 0;
+	int volume = Audio::Mixer::kMaxChannelVolume;
+	bool priority = false;
+	switch (command.operation) {
+	case kMacSoundPlayRepeated:
+		repeats = command.argument1;
+		break;
+	case kMacSoundPlayPriority:
+		priority = true;
+		break;
+	case kMacSoundPlayRepeatedPriority:
+		repeats = command.argument1;
+		priority = true;
+		break;
+	case kMacSoundPlayPriorityVolume:
+		priority = true;
+		volume = command.argument1;
+		break;
+	case kMacSoundPlayRepeatedPriorityVolume:
+		repeats = command.argument1;
+		priority = true;
+		volume = command.argument2;
+		break;
+	case kMacSoundPlayVolume:
+		volume = command.argument1;
+		break;
+	case kMacSoundPlayRepeatedVolume:
+		repeats = command.argument1;
+		volume = command.argument2;
+		break;
+	default:
+		break;
+	}
+
+	voice->resourceID = command.resourceID;
+	voice->priority = priority;
+	voice->volume = CLIP<int>(volume, 0, Audio::Mixer::kMaxChannelVolume);
+	Audio::AudioStream *playStream = Audio::makeLoopingAudioStream(stream,
+		(uint)MAX(repeats + 1, 1));
+	_mixer->playStream(Audio::Mixer::kSFXSoundType, &voice->handle, playStream,
+		-1, effectiveVolume(voice->volume));
+	if (_paused)
+		_mixer->pauseHandle(voice->handle, true);
+}
+
+void MacSoundDriver::wait(const MacSoundCommand &command) {
+	MacSoundVoice *voice = selectVoice();
+	if (!voice)
+		return;
+	if (_mixer->isSoundHandleActive(voice->handle))
+		_mixer->stopHandle(voice->handle);
+	voice->resourceID = command.resourceID;
+	voice->priority = false;
+	voice->delayed = true;
+	voice->delayEndTick = _serviceTick + MAX(command.argument1, 0);
+	voice->delayedCommand.operation = command.argument2 > 0 ?
+		kMacSoundPlayRepeated : kMacSoundPlay;
+	voice->delayedCommand.resourceID = command.resourceID;
+	voice->delayedCommand.argument1 = command.argument2;
+	voice->delayedCommand.argument2 = 0;
+	voice->delayedCommand.argument3 = 0;
+}
+
+void MacSoundDriver::halt(int resourceID) {
+	for (int i = 0; i < 2; ++i) {
+		if (_voices[i].resourceID != resourceID)
+			continue;
+		if (_mixer->isSoundHandleActive(_voices[i].handle))
+			_mixer->stopHandle(_voices[i].handle);
+		_voices[i].resourceID = 0;
+		_voices[i].priority = false;
+		_voices[i].delayed = false;
+	}
+}
+
+void MacSoundDriver::enqueue(const MacSoundCommand &command) {
+	for (int i = 0; i < 4; ++i) {
+		if (!_queue[i].used) {
+			_queue[i].used = true;
+			_queue[i].command = command;
+			return;
+		}
+	}
+}
+
+void MacSoundDriver::execute(const MacSoundCommand &command) {
+	switch (command.operation) {
+	case kMacSoundChangeVolume: {
+		bool found = false;
+		for (int i = 0; i < 2; ++i) {
+			updateVoice(_voices[i]);
+			if (_voices[i].resourceID == command.resourceID) {
+				setVoiceVolume(_voices[i], command.argument1);
+				found = true;
+			}
+		}
+		if (!found) {
+			MacSoundCommand playCommand = command;
+			playCommand.operation = kMacSoundPlayVolume;
+			play(playCommand);
+		}
+		break;
+	}
+	case kMacSoundWait:
+		wait(command);
+		break;
+	case kMacSoundHalt:
+		halt(command.resourceID);
+		break;
+	case kMacSoundHaltAll:
+	{
+		bool paused = _paused;
+		stop();
+		_paused = paused;
+		break;
+	}
+	case kMacSoundRampDown:
+		_ramping = true;
+		_rampVolume = 250;
+		break;
+	default:
+		play(command);
+		break;
 	}
 }
 
-void MacSoundDriver::discardFinishedSounds() {
-	for (uint i = _handles.size(); i > 0; --i) {
-		if (!_mixer->isSoundHandleActive(_handles[i - 1]))
-			_handles.remove_at(i - 1);
+MacSoundCommand MacSoundDriver::mapCommand(int commandId, int param) const {
+	MacSoundCommand command;
+	command.operation = kMacSoundPlay;
+	command.resourceID = _section * 1000 + commandId;
+	command.argument1 = 0;
+	command.argument2 = 0;
+	command.argument3 = 0;
+
+	for (uint i = 0; i < ARRAYSIZE(kMacSoundBehaviors); ++i) {
+		if (kMacSoundBehaviors[i].resourceID != command.resourceID)
+			continue;
+		command.operation = kMacSoundBehaviors[i].operation;
+		command.argument1 = kMacSoundBehaviors[i].argument1;
+		command.argument2 = kMacSoundBehaviors[i].argument2;
+		command.argument3 = kMacSoundBehaviors[i].argument3;
+		if (command.operation == kMacSoundChangeVolume)
+			command.argument1 = CLIP<int>(param * 2, 0,
+				Audio::Mixer::kMaxChannelVolume);
+		break;
 	}
+	return command;
 }
 
 int MacSoundDriver::command(int commandId, int param) {
@@ -61,8 +304,9 @@ int MacSoundDriver::command(int commandId, int param) {
 		setPaused(false);
 		return 0;
 	case 8:
-		for (uint i = 0; i < _handles.size(); ++i) {
-			if (_mixer->isSoundHandleActive(_handles[i]))
+		for (int i = 0; i < 2; ++i) {
+			updateVoice(_voices[i]);
+			if (_voices[i].resourceID != 0)
 				return 1;
 		}
 		return 0;
@@ -70,46 +314,92 @@ int MacSoundDriver::command(int commandId, int param) {
 		break;
 	}
 
-	Common::SeekableReadStream *resource = _resources->openSound(_section, commandId);
-	if (!resource)
-		return 0;
-
-	Audio::SeekableAudioStream *stream = Audio::makeMacSndStream(resource, DisposeAfterUse::YES);
-	if (!stream) {
-		delete resource;
-		warning("Could not decode Macintosh sound %d for section %d", commandId, _section);
-		return 0;
+	MacSoundCommand soundCommand = mapCommand(commandId, param);
+	bool queued = false;
+	for (uint i = 0; i < ARRAYSIZE(kMacSoundBehaviors); ++i) {
+		if (kMacSoundBehaviors[i].resourceID == soundCommand.resourceID) {
+			queued = kMacSoundBehaviors[i].queued;
+			break;
+		}
 	}
-
-	discardFinishedSounds();
-	_handles.push_back(Audio::SoundHandle());
-
-	// The DOS driver's second parameter has command-specific meanings. The
-	// Macintosh resources contain their final sampled output, so do not guess
-	// at a pan or gain conversion until the native call sites are recovered.
-	(void)param;
-	_mixer->playStream(Audio::Mixer::kSFXSoundType, &_handles.back(), stream,
-		-1, _volume);
-	if (_paused)
-		_mixer->pauseHandle(_handles.back(), true);
+	if (queued)
+		enqueue(soundCommand);
+	else
+		execute(soundCommand);
 	return 0;
 }
 
 int MacSoundDriver::stop() {
-	for (uint i = 0; i < _handles.size(); ++i) {
-		if (_mixer->isSoundHandleActive(_handles[i]))
-			_mixer->stopHandle(_handles[i]);
+	for (int i = 0; i < 2; ++i) {
+		if (_mixer->isSoundHandleActive(_voices[i].handle))
+			_mixer->stopHandle(_voices[i].handle);
+		_voices[i].resourceID = 0;
+		_voices[i].priority = false;
+		_voices[i].delayed = false;
 	}
-	_handles.clear();
+	for (int i = 0; i < 4; ++i)
+		_queue[i].used = false;
+	_ramping = false;
 	_paused = false;
 	return 0;
 }
 
+int MacSoundDriver::poll() {
+	for (int i = 0; i < 2; ++i) {
+		updateVoice(_voices[i]);
+		if (_voices[i].resourceID != 0)
+			return 1;
+	}
+	return 0;
+}
+
 void MacSoundDriver::setVolume(int volume) {
 	_volume = CLIP<int>(volume, 0, Audio::Mixer::kMaxChannelVolume);
-	for (uint i = 0; i < _handles.size(); ++i) {
-		if (_mixer->isSoundHandleActive(_handles[i]))
-			_mixer->setChannelVolume(_handles[i], (byte)_volume);
+	for (int i = 0; i < 2; ++i)
+		setVoiceVolume(_voices[i], _voices[i].volume);
+}
+
+void MacSoundDriver::commandMacintosh(const MacSoundCommand &command) {
+	execute(command);
+}
+
+void MacSoundDriver::queueMacintosh(const MacSoundCommand &command) {
+	enqueue(command);
+}
+
+void MacSoundDriver::dispatchQueue() {
+	for (int i = 0; i < 4; ++i) {
+		if (_queue[i].used) {
+			execute(_queue[i].command);
+			_queue[i].used = false;
+		}
+	}
+}
+
+void MacSoundDriver::service(uint32 tick) {
+	_serviceTick = tick;
+	if (_paused)
+		return;
+
+	for (int i = 0; i < 2; ++i) {
+		if (_voices[i].delayed && tick > _voices[i].delayEndTick) {
+			MacSoundCommand command = _voices[i].delayedCommand;
+			_voices[i].resourceID = 0;
+			_voices[i].delayed = false;
+			play(command);
+		} else {
+			updateVoice(_voices[i]);
+		}
+	}
+
+	if (_ramping) {
+		for (int i = 0; i < 2; ++i) {
+			if (_voices[i].resourceID != 0)
+				setVoiceVolume(_voices[i], _rampVolume);
+		}
+		_rampVolume -= 8;
+		if (_rampVolume < 0)
+			_ramping = false;
 	}
 }
 
@@ -118,6 +408,50 @@ void MacSoundManager::loadDriver(int sectionNum) {
 	_driver = new MacSoundDriver(_mixer, _resources, sectionNum);
 }
 
+MacSoundDriver *MacSoundManager::getMacintoshDriver() const {
+	return static_cast<MacSoundDriver *>(_driver);
+}
+
+void MacSoundManager::startQueuedCommands() {
+	SoundManager::startQueuedCommands();
+	if (_driver)
+		getMacintoshDriver()->dispatchQueue();
+}
+
+void MacSoundManager::commandMacintosh(const MacSoundCommand &command, bool queued) {
+	if (!_driver || !_soundFlag)
+		return;
+	if (queued)
+		getMacintoshDriver()->queueMacintosh(command);
+	else
+		getMacintoshDriver()->commandMacintosh(command);
+}
+
+void MacSoundManager::service(uint32 tick) {
+	if (_driver)
+		getMacintoshDriver()->service(tick);
+}
+
+bool commandMacintoshSound(const MacSoundCommand &command, bool queued) {
+	if (!g_engine || g_engine->getPlatform() != Common::kPlatformMacintosh ||
+			!g_engine->_soundManager)
+		return false;
+	static_cast<MacSoundManager *>(g_engine->_soundManager)->commandMacintosh(
+		command, queued);
+	return true;
+}
+
+bool commandMacintoshSound(MacSoundOperation operation, int resourceID,
+		int argument1, int argument2, int argument3, bool queued) {
+	MacSoundCommand command;
+	command.operation = operation;
+	command.resourceID = resourceID;
+	command.argument1 = argument1;
+	command.argument2 = argument2;
+	command.argument3 = argument3;
+	return commandMacintoshSound(command, queued);
+}
+
 } // namespace Sound
 } // namespace RexNebular
 } // namespace MADS
diff --git a/engines/mads/nebular/sound/mac_sound.h b/engines/mads/nebular/sound/mac_sound.h
index a3db7bbe8f9..5bb00ca0f9c 100644
--- a/engines/mads/nebular/sound/mac_sound.h
+++ b/engines/mads/nebular/sound/mac_sound.h
@@ -32,25 +32,85 @@ class MacResourceProvider;
 
 namespace Sound {
 
+enum MacSoundOperation {
+	kMacSoundPlay = 1,
+	kMacSoundPlayRepeated = 2,
+	kMacSoundChangeVolume = 3,
+	kMacSoundPlayPriority = 4,
+	kMacSoundPlayRepeatedPriority = 5,
+	kMacSoundPlayPriorityVolume = 6,
+	kMacSoundPlayRepeatedPriorityVolume = 7,
+	kMacSoundPlayVolume = 8,
+	kMacSoundPlayRepeatedVolume = 9,
+	kMacSoundWait = 10,
+	kMacSoundHalt = 11,
+	kMacSoundHaltAll = 12,
+	kMacSoundRampDown = 13
+};
+
+struct MacSoundCommand {
+	MacSoundOperation operation;
+	int resourceID;
+	int argument1;
+	int argument2;
+	int argument3;
+};
+
+struct MacSoundVoice {
+	Audio::SoundHandle handle;
+	int resourceID;
+	int volume;
+	bool priority;
+	bool delayed;
+	uint32 delayEndTick;
+	MacSoundCommand delayedCommand;
+
+	MacSoundVoice();
+};
+
+struct MacSoundQueueEntry {
+	bool used;
+	MacSoundCommand command;
+
+	MacSoundQueueEntry();
+};
+
 class MacSoundDriver : public SoundDriver {
 private:
 	MacResourceProvider *_resources;
 	int _section;
-	Common::Array<Audio::SoundHandle> _handles;
-	int _volume = Audio::Mixer::kMaxChannelVolume;
-	bool _paused = false;
+	MacSoundVoice _voices[2];
+	MacSoundQueueEntry _queue[4];
+	int _volume;
+	bool _paused;
+	bool _ramping;
+	int _rampVolume;
+	uint32 _serviceTick;
 
 	void setPaused(bool paused);
-	void discardFinishedSounds();
+	void updateVoice(MacSoundVoice &voice);
+	MacSoundVoice *selectVoice();
+	int effectiveVolume(int volume) const;
+	void setVoiceVolume(MacSoundVoice &voice, int volume);
+	void play(const MacSoundCommand &command);
+	void wait(const MacSoundCommand &command);
+	void halt(int resourceID);
+	void enqueue(const MacSoundCommand &command);
+	void execute(const MacSoundCommand &command);
+	MacSoundCommand mapCommand(int commandId, int param) const;
 
 public:
 	MacSoundDriver(Audio::Mixer *mixer, MacResourceProvider *resources, int section);
 
 	int command(int commandId, int param) override;
 	int stop() override;
-	int poll() override { return 0; }
+	int poll() override;
 	void noise() override {}
 	void setVolume(int volume) override;
+	void commandMacintosh(const MacSoundCommand &command);
+	void queueMacintosh(const MacSoundCommand &command);
+	void dispatchQueue();
+	void service(uint32 tick);
 };
 
 class MacSoundManager : public SoundManager {
@@ -59,14 +119,23 @@ private:
 
 protected:
 	void loadDriver(int sectionNum) override;
+	MacSoundDriver *getMacintoshDriver() const;
 
 public:
 	MacSoundManager(Audio::Mixer *mixer, bool &soundFlag, MacResourceProvider *resources) :
 			SoundManager(mixer, soundFlag), _resources(resources) {}
 
 	void validate() override {}
+	void startQueuedCommands() override;
+	void commandMacintosh(const MacSoundCommand &command, bool queued);
+	void service(uint32 tick);
 };
 
+bool commandMacintoshSound(const MacSoundCommand &command, bool queued = false);
+bool commandMacintoshSound(MacSoundOperation operation, int resourceID,
+	int argument1 = 0, int argument2 = 0, int argument3 = 0,
+	bool queued = false);
+
 } // namespace Sound
 } // namespace RexNebular
 } // namespace MADS


Commit: a330a0498aed4dee1f4cd7a296e698c601a21fa3
    https://github.com/scummvm/scummvm/commit/a330a0498aed4dee1f4cd7a296e698c601a21fa3
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: NEBULAR: Service Macintosh sound playback

Call the recovered CODE 3 sound service from the MADS event loop at the
classic Macintosh TickCount cadence. Preserve delayed playback across
pauses without advancing time or issuing catch-up bursts.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/mads.cpp
    engines/mads/mads.h
    engines/mads/nebular/mac_nebular.cpp
    engines/mads/nebular/mac_nebular.h
    engines/mads/nebular/nebular.h
    engines/mads/nebular/sound/mac_sound.cpp
    engines/mads/nebular/sound/mac_sound.h


diff --git a/engines/mads/mads.cpp b/engines/mads/mads.cpp
index a8355b79683..5af096abec4 100644
--- a/engines/mads/mads.cpp
+++ b/engines/mads/mads.cpp
@@ -371,6 +371,8 @@ void MADSEngine::checkForTimerFunction() {
 			_nextTimerTime = time + (1000 / 60);
 		}
 	}
+
+	serviceMacintoshSound();
 }
 
 bool MADSEngine::hasPendingKey() {
diff --git a/engines/mads/mads.h b/engines/mads/mads.h
index 303ff92c9b6..19238ffcbe0 100644
--- a/engines/mads/mads.h
+++ b/engines/mads/mads.h
@@ -93,6 +93,7 @@ protected:
 	virtual void presentScreen(int shakeOffset);
 
 	virtual bool handleMacEvent(Common::Event &event) { return false; }
+	virtual void serviceMacintoshSound() {}
 
 	bool hasFeature(EngineFeature f) const override;
 
diff --git a/engines/mads/nebular/mac_nebular.cpp b/engines/mads/nebular/mac_nebular.cpp
index 95a93bcfb56..52b8f6ffc32 100644
--- a/engines/mads/nebular/mac_nebular.cpp
+++ b/engines/mads/nebular/mac_nebular.cpp
@@ -37,6 +37,7 @@
 #include "mads/nebular/mac_menus.h"
 #include "mads/nebular/mac_nebular.h"
 #include "mads/nebular/mac_resources.h"
+#include "mads/nebular/sound/mac_sound.h"
 #include "mads/nebular/nebular.h"
 #include "mads/nebular/popup.h"
 #include "mads/nebular/mads/words.h"
@@ -729,6 +730,31 @@ bool MacNebular::handleMacEvent(Common::Event &event) {
 	return _useOriginalMenus && _menus && _menus->processEvent(event);
 }
 
+void MacNebular::serviceSound() {
+	// Classic Macintosh TickCount advances at approximately 60.15 Hz. CODE 3
+	// services sound after each event-loop pass and uses that clock for waits.
+	const uint32 hostTick = (uint32)((uint64)g_system->getMillis() * 6015 / 100000);
+	if (_engine.isPaused()) {
+		if (!_macintoshSoundPaused) {
+			_macintoshSoundPaused = true;
+			_macintoshSoundPausedAt = hostTick;
+		}
+		return;
+	}
+
+	if (_macintoshSoundPaused) {
+		_macintoshSoundPausedTicks += hostTick - _macintoshSoundPausedAt;
+		_macintoshSoundPaused = false;
+	}
+
+	const uint32 soundTick = hostTick - _macintoshSoundPausedTicks;
+	if (soundTick == _lastMacintoshSoundTick)
+		return;
+	_lastMacintoshSoundTick = soundTick;
+	if (_engine._soundManager)
+		static_cast<Sound::MacSoundManager *>(_engine._soundManager)->service(soundTick);
+}
+
 void MacNebular::showPopup() {
 	if (!_resources || !box)
 		return;
@@ -868,6 +894,11 @@ bool RexNebularEngine::handleMacEvent(Common::Event &event) {
 	return _macNebular && _macNebular->handleMacEvent(event);
 }
 
+void RexNebularEngine::serviceMacintoshSound() {
+	if (_macNebular)
+		_macNebular->serviceSound();
+}
+
 void RexNebularEngine::selectMacintoshDifficulty() {
 	if (_macNebular)
 		_macNebular->selectDifficulty();
diff --git a/engines/mads/nebular/mac_nebular.h b/engines/mads/nebular/mac_nebular.h
index 7ce36f15b85..bd29d84b957 100644
--- a/engines/mads/nebular/mac_nebular.h
+++ b/engines/mads/nebular/mac_nebular.h
@@ -50,6 +50,10 @@ private:
 	bool _fullFrameActive = false;
 	bool _popupActive = false;
 	bool _layoutLogged = false;
+	uint32 _lastMacintoshSoundTick = (uint32)-1;
+	uint32 _macintoshSoundPausedAt = 0;
+	uint32 _macintoshSoundPausedTicks = 0;
+	bool _macintoshSoundPaused = false;
 
 public:
 	explicit MacNebular(RexNebularEngine &engine);
@@ -66,6 +70,7 @@ public:
 	Common::Point screenToGame(const Common::Point &point) const;
 	Common::Point gameToScreen(const Common::Point &point) const;
 	bool handleMacEvent(Common::Event &event);
+	void serviceSound();
 	void setPalette(const RGBcolor *palette, int firstColor, int numColors);
 	void getPalette(RGBcolor *palette, int firstColor, int numColors) const;
 	void presentScreen(int shakeOffset);
diff --git a/engines/mads/nebular/nebular.h b/engines/mads/nebular/nebular.h
index 6902f44552f..c14ed97dbfd 100644
--- a/engines/mads/nebular/nebular.h
+++ b/engines/mads/nebular/nebular.h
@@ -54,6 +54,7 @@ protected:
 	Common::Point gameToScreen(const Common::Point &point) const override;
 	void presentScreen(int shakeOffset) override;
 	bool handleMacEvent(Common::Event &event) override;
+	void serviceMacintoshSound() override;
 
 public:
 	RexNebularEngine(OSystem *syst, const MADSGameDescription *gameDesc);
diff --git a/engines/mads/nebular/sound/mac_sound.cpp b/engines/mads/nebular/sound/mac_sound.cpp
index 18842341348..cd6d05f0e12 100644
--- a/engines/mads/nebular/sound/mac_sound.cpp
+++ b/engines/mads/nebular/sound/mac_sound.cpp
@@ -78,12 +78,21 @@ MacSoundQueueEntry::MacSoundQueueEntry() : used(false) {
 MacSoundDriver::MacSoundDriver(Audio::Mixer *mixer, MacResourceProvider *resources, int section) :
 		SoundDriver(mixer), _resources(resources), _section(section),
 		_volume(Audio::Mixer::kMaxChannelVolume), _paused(false), _ramping(false),
-		_rampVolume(0), _serviceTick(0) {
+		_rampVolume(0), _serviceTick(0), _pausedAtTick(0) {
 }
 
 void MacSoundDriver::setPaused(bool paused) {
 	if (_paused == paused)
 		return;
+	if (paused) {
+		_pausedAtTick = _serviceTick;
+	} else {
+		const uint32 pausedTicks = _serviceTick - _pausedAtTick;
+		for (int i = 0; i < 2; ++i) {
+			if (_voices[i].delayed)
+				_voices[i].delayEndTick += pausedTicks;
+		}
+	}
 	_paused = paused;
 	for (int i = 0; i < 2; ++i) {
 		if (_mixer->isSoundHandleActive(_voices[i].handle))
diff --git a/engines/mads/nebular/sound/mac_sound.h b/engines/mads/nebular/sound/mac_sound.h
index 5bb00ca0f9c..89f27080f7b 100644
--- a/engines/mads/nebular/sound/mac_sound.h
+++ b/engines/mads/nebular/sound/mac_sound.h
@@ -86,6 +86,7 @@ private:
 	bool _ramping;
 	int _rampVolume;
 	uint32 _serviceTick;
+	uint32 _pausedAtTick;
 
 	void setPaused(bool paused);
 	void updateVoice(MacSoundVoice &voice);


Commit: 75fd279ef58413d6d137caa748932ac2e968a03c
    https://github.com/scummvm/scummvm/commit/75fd279ef58413d6d137caa748932ac2e968a03c
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: Add combined sound playback debugger command

Add a compact playsound section:command[:parameter] debugger command.
It loads the selected sound driver, then dispatches the command.
The optional parameter defaults to zero.

This makes isolated sound checks reproducible without issuing separate
commands.

Assisted-by: Codex:GPT-5

Changed paths:
    engines/mads/console.cpp
    engines/mads/console.h


diff --git a/engines/mads/console.cpp b/engines/mads/console.cpp
index 471177039b0..4166813143f 100644
--- a/engines/mads/console.cpp
+++ b/engines/mads/console.cpp
@@ -36,6 +36,7 @@ Console::Console() : GUI::Debugger() {
 	registerCmd("teleport", WRAP_METHOD(Console, cmdTeleport));
 	registerCmd("walkable", WRAP_METHOD(Console, cmdWalkable));
 	registerCmd("quotes", WRAP_METHOD(Console, cmdQuotes));
+	registerCmd("playsound", WRAP_METHOD(Console, cmdPlaySound));
 	registerCmd("soundcommand", WRAP_METHOD(Console, cmdSoundCommand));
 	registerCmd("soundsection", WRAP_METHOD(Console, cmdSoundSection));
 	registerCmd("soundstop", WRAP_METHOD(Console, cmdSoundStop));
@@ -175,6 +176,38 @@ bool Console::cmdSoundCommand(int argc, const char **argv) {
 	return true;
 }
 
+bool Console::cmdPlaySound(int argc, const char **argv) {
+	int section;
+	int commandId;
+	int parameter = 0;
+	char trailing;
+
+	if (argc != 2 ||
+			(sscanf(argv[1], "%d:%d:%d%c", &section, &commandId,
+				&parameter, &trailing) != 3 &&
+			sscanf(argv[1], "%d:%d%c", &section, &commandId,
+				&trailing) != 2)) {
+		debugPrintf("Usage: %s <section:command[:parameter]>\n", argv[0]);
+		return true;
+	}
+
+	if (section < 1 || section > 9) {
+		debugPrintf("Section must be between 1 and 9.\n");
+		return true;
+	}
+
+	g_engine->_soundManager->init(section);
+	if (!g_engine->_soundManager->isLoaded()) {
+		debugPrintf("No sound driver is available for section %d.\n", section);
+		return true;
+	}
+
+	const int result = g_engine->_soundManager->command(commandId, parameter);
+	debugPrintf("Loaded section %d and issued sound command %d(%d), returned %d.\n",
+		section, commandId, parameter, result);
+	return true;
+}
+
 bool Console::cmdSoundSection(int argc, const char **argv) {
 	if (argc != 2) {
 		debugPrintf("Usage: %s <section 1-9>\n", argv[0]);
diff --git a/engines/mads/console.h b/engines/mads/console.h
index 100b5f19ce9..4099214ceba 100644
--- a/engines/mads/console.h
+++ b/engines/mads/console.h
@@ -34,6 +34,7 @@ private:
 	bool cmdWalkable(int argc, const char **argv);
 	bool cmdDepth(int argc, const char **argv);
 	bool cmdQuotes(int argc, const char **argv);
+	bool cmdPlaySound(int argc, const char **argv);
 	bool cmdSoundCommand(int argc, const char **argv);
 	bool cmdSoundSection(int argc, const char **argv);
 	bool cmdSoundStop(int argc, const char **argv);


Commit: 3ffcceb40faa2f2e5eb6892a58d9655d7411521a
    https://github.com/scummvm/scummvm/commit/3ffcceb40faa2f2e5eb6892a58d9655d7411521a
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: NEBULAR: Add Macintosh dialog controls

Add Rex-owned Macintosh dialog parsing, drawing, and modal controls.
Follow referenced item lists and ignore binary item payloads safely.

Assisted-by: Codex:GPT-5.4

Changed paths:
  A engines/mads/nebular/mac_dialogs.cpp
  A engines/mads/nebular/mac_dialogs.h
    engines/mads/module.mk
    engines/mads/nebular/mac_menus.cpp
    engines/mads/nebular/mac_menus.h


diff --git a/engines/mads/module.mk b/engines/mads/module.mk
index ddc80208ac2..4db6ca11997 100644
--- a/engines/mads/module.mk
+++ b/engines/mads/module.mk
@@ -75,6 +75,7 @@ MODULE_OBJS := \
 	nebular/main.o \
 	nebular/main_menu.o \
 	nebular/mac_frontend.o \
+	nebular/mac_dialogs.o \
 	nebular/mac_menus.o \
 	nebular/mac_nebular.o \
 	nebular/mac_resources.o \
diff --git a/engines/mads/nebular/mac_dialogs.cpp b/engines/mads/nebular/mac_dialogs.cpp
new file mode 100644
index 00000000000..50513c9b2f9
--- /dev/null
+++ b/engines/mads/nebular/mac_dialogs.cpp
@@ -0,0 +1,809 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/events.h"
+#include "common/stream.h"
+#include "common/system.h"
+#include "graphics/cursorman.h"
+#include "graphics/font.h"
+#include "graphics/macgui/macwindowmanager.h"
+#include "graphics/managed_surface.h"
+#include "graphics/primitives.h"
+#include "mads/mads.h"
+#include "mads/nebular/mac_dialogs.h"
+#include "mads/nebular/mac_resources.h"
+#include "mads/nebular/nebular.h"
+
+namespace MADS {
+namespace RexNebular {
+
+MacNebularDialog::Item::Item() : type(kUserItem), enabled(false),
+		checked(false), listItem(false), hasUndo(false), listSelection(-1),
+		listTop(0), caret(0), selectionStart(0), selectionEnd(0) {
+}
+
+MacNebularDialog::MacNebularDialog(RexNebularEngine &engine,
+		MacResourceProvider &resources, Graphics::ManagedSurface &screen,
+		Graphics::MacWindowManager &windowManager) : _engine(engine),
+		_resources(resources), _screen(screen), _windowManager(windowManager),
+		_font(resources.getDialogFont()), _focusItem(0), _pressedItem(0),
+		_defaultItem(0), _cancelItem(0), _redraw(true) {
+}
+
+MacNebularDialog::Item *MacNebularDialog::getItem(int itemNumber) {
+	if (itemNumber < 1 || (uint)itemNumber > _items.size())
+		return nullptr;
+	return &_items[itemNumber - 1];
+}
+
+const MacNebularDialog::Item *MacNebularDialog::getItem(
+		int itemNumber) const {
+	if (itemNumber < 1 || (uint)itemNumber > _items.size())
+		return nullptr;
+	return &_items[itemNumber - 1];
+}
+
+Common::Rect MacNebularDialog::getItemBounds(const Item &item) const {
+	Common::Rect bounds(item.bounds);
+	bounds.translate(_bounds.left, _bounds.top);
+	return bounds;
+}
+
+bool MacNebularDialog::loadDialogResource(uint16 resourceID,
+		uint16 &itemResourceID) {
+	Common::SeekableReadStream *stream = _resources.openResource(
+		MacResourceProvider::kApplicationContainer,
+		MKTAG('D', 'L', 'O', 'G'), resourceID);
+	if (!stream)
+		return false;
+
+	bool valid = stream->size() >= 21;
+	if (valid) {
+		_bounds.top = stream->readSint16BE();
+		_bounds.left = stream->readSint16BE();
+		_bounds.bottom = stream->readSint16BE();
+		_bounds.right = stream->readSint16BE();
+		stream->skip(10);
+		itemResourceID = stream->readUint16BE();
+		/* Common::String title = */ stream->readPascalString();
+		valid = valid && !stream->err() && _bounds.isValidRect();
+	}
+	delete stream;
+	return valid;
+}
+
+bool MacNebularDialog::loadDialogItems(uint16 resourceID) {
+	Common::SeekableReadStream *stream = _resources.openResource(
+		MacResourceProvider::kApplicationContainer,
+		MKTAG('D', 'I', 'T', 'L'), resourceID);
+	if (!stream)
+		return false;
+
+	if (stream->size() < 2) {
+		delete stream;
+		return false;
+	}
+
+	const uint itemCount = stream->readUint16BE() + 1;
+	bool valid = true;
+	for (uint itemNumber = 0; valid && itemNumber < itemCount; ++itemNumber) {
+		if (stream->pos() + 14 > stream->size()) {
+			valid = false;
+			break;
+		}
+
+		Item item;
+		stream->skip(4);
+		item.bounds.top = stream->readSint16BE();
+		item.bounds.left = stream->readSint16BE();
+		item.bounds.bottom = stream->readSint16BE();
+		item.bounds.right = stream->readSint16BE();
+		const byte rawType = stream->readByte();
+		const uint length = stream->readByte();
+		if (stream->pos() + length > stream->size()) {
+			valid = false;
+			break;
+		}
+
+		item.type = rawType & 0x7f;
+		item.enabled = (rawType & 0x80) == 0;
+		if (item.type == kButton || item.type == kCheckbox ||
+				item.type == kRadioButton || item.type == kStaticText ||
+				item.type == kEditableText) {
+			for (uint index = 0; index < length; ++index)
+				item.text += (char)stream->readByte();
+		} else {
+			// User and help items carry binary data rather than Pascal text.
+			stream->skip(length);
+		}
+		item.caret = item.text.size();
+		item.selectionStart = item.caret;
+		item.selectionEnd = item.caret;
+		_items.push_back(item);
+
+		if (length & 1)
+			stream->skip(1);
+	}
+
+	valid = valid && !stream->err();
+	delete stream;
+	return valid;
+}
+
+bool MacNebularDialog::load(uint16 resourceID) {
+	_items.clear();
+	_focusItem = 0;
+	_pressedItem = 0;
+	_redraw = true;
+	uint16 itemResourceID = 0;
+	return _font && loadDialogResource(resourceID, itemResourceID) &&
+		loadDialogItems(itemResourceID);
+}
+
+void MacNebularDialog::center() {
+	_bounds.moveTo((_screen.w - _bounds.width()) / 2,
+		(_screen.h - _bounds.height()) / 2);
+}
+
+void MacNebularDialog::setItemEnabled(int itemNumber, bool enabled) {
+	Item *item = getItem(itemNumber);
+	if (item) {
+		item->enabled = enabled;
+		_redraw = true;
+	}
+}
+
+void MacNebularDialog::setItemChecked(int itemNumber, bool checked) {
+	Item *item = getItem(itemNumber);
+	if (item) {
+		item->checked = checked;
+		_redraw = true;
+	}
+}
+
+bool MacNebularDialog::isItemChecked(int itemNumber) const {
+	const Item *item = getItem(itemNumber);
+	return item && item->checked;
+}
+
+void MacNebularDialog::setItemText(int itemNumber,
+		const Common::String &text) {
+	Item *item = getItem(itemNumber);
+	if (!item)
+		return;
+	item->text = text;
+	item->caret = text.size();
+	item->selectionStart = item->caret;
+	item->selectionEnd = item->caret;
+	_redraw = true;
+}
+
+Common::String MacNebularDialog::getItemText(int itemNumber) const {
+	const Item *item = getItem(itemNumber);
+	return item ? item->text : Common::String();
+}
+
+void MacNebularDialog::setList(int itemNumber,
+		const Common::StringArray &entries, int selection) {
+	Item *item = getItem(itemNumber);
+	if (!item)
+		return;
+	item->listItem = true;
+	item->enabled = true;
+	item->list = entries;
+	selectList(*item, selection);
+	_redraw = true;
+}
+
+int MacNebularDialog::getListSelection(int itemNumber) const {
+	const Item *item = getItem(itemNumber);
+	return item && item->listItem ? item->listSelection : -1;
+}
+
+void MacNebularDialog::drawButton(const Item &item,
+		const Common::Rect &bounds, bool pressed) {
+	Graphics::Primitives &primitives = _windowManager.getDrawPrimitives();
+	Graphics::MacPlotData plot(&_screen, nullptr,
+		&_windowManager.getBuiltinPatterns(), 1, 0, 0,
+		Common::Point(1, 1), _windowManager._colorWhite, false);
+	primitives.drawRoundRect1(bounds, 4, _windowManager._colorBlack,
+		false, &plot);
+	if (pressed) {
+		Common::Rect inside(bounds);
+		inside.grow(-2);
+		primitives.drawFilledRect1(inside, _windowManager._colorBlack, &plot);
+	}
+	const uint32 color = pressed ? _windowManager._colorWhite :
+		_windowManager._colorBlack;
+	const int textX = bounds.left +
+		(bounds.width() - _font->getStringWidth(item.text)) / 2;
+	const int textY = bounds.top +
+		(bounds.height() - _font->getFontHeight()) / 2;
+	_font->drawString(&_screen, item.text, textX, textY,
+		bounds.width(), color);
+}
+
+void MacNebularDialog::drawCheckbox(const Item &item,
+		const Common::Rect &bounds) {
+	Graphics::Primitives &primitives = _windowManager.getDrawPrimitives();
+	Graphics::MacPlotData plot(&_screen, nullptr,
+		&_windowManager.getBuiltinPatterns(), 1, 0, 0,
+		Common::Point(1, 1), _windowManager._colorWhite, false);
+	const int boxTop = bounds.top + (bounds.height() - 12) / 2;
+	const Common::Rect box(bounds.left, boxTop, bounds.left + 12, boxTop + 12);
+	primitives.drawRect1(box, _windowManager._colorBlack, &plot);
+	if (item.checked) {
+		primitives.drawLine(box.left + 2, box.top + 2,
+			box.right - 3, box.bottom - 3, _windowManager._colorBlack, &plot);
+		primitives.drawLine(box.left + 2, box.bottom - 3,
+			box.right - 3, box.top + 2, _windowManager._colorBlack, &plot);
+	}
+	const int textY = bounds.top +
+		(bounds.height() - _font->getFontHeight()) / 2;
+	_font->drawString(&_screen, item.text, bounds.left + 17, textY,
+		bounds.width() - 17, _windowManager._colorBlack);
+}
+
+void MacNebularDialog::drawRadioButton(const Item &item,
+		const Common::Rect &bounds) {
+	Graphics::Primitives &primitives = _windowManager.getDrawPrimitives();
+	Graphics::MacPlotData plot(&_screen, nullptr,
+		&_windowManager.getBuiltinPatterns(), 1, 0, 0,
+		Common::Point(1, 1), _windowManager._colorWhite, false);
+	const int circleTop = bounds.top + (bounds.height() - 12) / 2;
+	primitives.drawEllipse(bounds.left, circleTop, bounds.left + 11,
+		circleTop + 11, _windowManager._colorBlack, false, &plot);
+	if (item.checked)
+		primitives.drawEllipse(bounds.left + 4, circleTop + 4,
+			bounds.left + 7, circleTop + 7,
+			_windowManager._colorBlack, true, &plot);
+	const int textY = bounds.top +
+		(bounds.height() - _font->getFontHeight()) / 2;
+	_font->drawString(&_screen, item.text, bounds.left + 17, textY,
+		bounds.width() - 17, _windowManager._colorBlack);
+}
+
+void MacNebularDialog::drawEditable(Item &item,
+		const Common::Rect &bounds, bool focused) {
+	Graphics::Primitives &primitives = _windowManager.getDrawPrimitives();
+	Graphics::MacPlotData plot(&_screen, nullptr,
+		&_windowManager.getBuiltinPatterns(), 1, 0, 0,
+		Common::Point(1, 1), _windowManager._colorWhite, false);
+	primitives.drawRect1(bounds, _windowManager._colorBlack, &plot);
+	Common::Rect inside(bounds);
+	inside.grow(-1);
+	primitives.drawFilledRect1(inside, _windowManager._colorWhite, &plot);
+
+	const int textX = bounds.left + 3;
+	const int textY = bounds.top +
+		(bounds.height() - _font->getFontHeight()) / 2;
+	const uint selectionStart = MIN(item.selectionStart, item.selectionEnd);
+	const uint selectionEnd = MAX(item.selectionStart, item.selectionEnd);
+	if (selectionEnd > selectionStart) {
+		const Common::String prefix = item.text.substr(0, selectionStart);
+		const Common::String selected = item.text.substr(selectionStart,
+			selectionEnd - selectionStart);
+		const Common::String suffix = item.text.substr(selectionEnd);
+		const int selectionX = textX + _font->getStringWidth(prefix);
+		const int selectionWidth = _font->getStringWidth(selected);
+		_font->drawString(&_screen, prefix, textX, textY,
+			bounds.width() - 6, _windowManager._colorBlack);
+		primitives.drawFilledRect1(Common::Rect(selectionX, bounds.top + 2,
+			selectionX + selectionWidth, bounds.bottom - 2),
+			_windowManager._colorBlack, &plot);
+		_font->drawString(&_screen, selected, selectionX, textY,
+			selectionWidth, _windowManager._colorWhite);
+		_font->drawString(&_screen, suffix, selectionX + selectionWidth,
+			textY, bounds.right - selectionX - selectionWidth - 3,
+			_windowManager._colorBlack);
+	} else {
+		_font->drawString(&_screen, item.text, textX, textY,
+			bounds.width() - 6, _windowManager._colorBlack);
+	}
+	if (focused) {
+		const Common::String prefix = item.text.substr(0, item.caret);
+		const int caretX = MIN(bounds.right - 2,
+			textX + _font->getStringWidth(prefix));
+		primitives.drawLine(caretX, bounds.top + 2, caretX,
+			bounds.bottom - 3, _windowManager._colorBlack, &plot);
+	}
+}
+
+void MacNebularDialog::drawList(const Item &item,
+		const Common::Rect &bounds, bool focused) {
+	Graphics::Primitives &primitives = _windowManager.getDrawPrimitives();
+	Graphics::MacPlotData plot(&_screen, nullptr,
+		&_windowManager.getBuiltinPatterns(), 1, 0, 0,
+		Common::Point(1, 1), _windowManager._colorWhite, false);
+	primitives.drawRect1(bounds, _windowManager._colorBlack, &plot);
+	Common::Rect inside(bounds);
+	inside.grow(-1);
+	primitives.drawFilledRect1(inside, _windowManager._colorWhite, &plot);
+
+	const int rowHeight = MAX(_font->getFontHeight() + 2, 12);
+	const int visibleRows = MAX(1, (bounds.height() - 4) / rowHeight);
+	for (int row = 0; row < visibleRows; ++row) {
+		const int index = item.listTop + row;
+		if (index < 0 || (uint)index >= item.list.size())
+			break;
+		const int rowTop = bounds.top + 2 + row * rowHeight;
+		const bool selected = index == item.listSelection;
+		if (selected) {
+			primitives.drawFilledRect1(Common::Rect(bounds.left + 2, rowTop,
+				bounds.right - 2, rowTop + rowHeight),
+				_windowManager._colorBlack, &plot);
+		}
+		_font->drawString(&_screen, item.list[index], bounds.left + 4,
+			rowTop, bounds.width() - 8, selected ?
+			_windowManager._colorWhite : _windowManager._colorBlack);
+	}
+	if (focused)
+		primitives.drawRect1(Common::Rect(bounds.left - 2, bounds.top - 2,
+			bounds.right + 2, bounds.bottom + 2),
+			_windowManager._colorBlack, &plot);
+}
+
+void MacNebularDialog::drawItem(Item &item, int itemNumber) {
+	const Common::Rect bounds = getItemBounds(item);
+	if (item.listItem) {
+		drawList(item, bounds, _focusItem == itemNumber);
+		return;
+	}
+
+	switch (item.type) {
+	case kButton:
+		drawButton(item, bounds, _pressedItem == itemNumber);
+		break;
+	case kCheckbox:
+		drawCheckbox(item, bounds);
+		break;
+	case kRadioButton:
+		drawRadioButton(item, bounds);
+		break;
+	case kStaticText: {
+		const int textY = bounds.top +
+			(bounds.height() - _font->getFontHeight()) / 2;
+		_font->drawString(&_screen, item.text, bounds.left, textY,
+			bounds.width(), _windowManager._colorBlack);
+		break;
+	}
+	case kEditableText:
+		drawEditable(item, bounds, _focusItem == itemNumber);
+		break;
+	default:
+		break;
+	}
+}
+
+void MacNebularDialog::draw() {
+	Graphics::Primitives &primitives = _windowManager.getDrawPrimitives();
+	Graphics::MacPlotData plot(&_screen, nullptr,
+		&_windowManager.getBuiltinPatterns(), 1, 0, 0,
+		Common::Point(1, 1), _windowManager._colorWhite, false);
+	primitives.drawFilledRect1(_bounds, _windowManager._colorWhite, &plot);
+	primitives.drawRect1(_bounds, _windowManager._colorBlack, &plot);
+	for (uint index = 0; index < _items.size(); ++index)
+		drawItem(_items[index], index + 1);
+}
+
+void MacNebularDialog::setFocus(int itemNumber) {
+	if (_focusItem == itemNumber)
+		return;
+	_focusItem = itemNumber;
+	_redraw = true;
+}
+
+void MacNebularDialog::focusNext(bool backwards) {
+	if (_items.empty())
+		return;
+	int itemNumber = _focusItem;
+	for (uint count = 0; count < _items.size(); ++count) {
+		itemNumber += backwards ? -1 : 1;
+		if (itemNumber < 1)
+			itemNumber = _items.size();
+		else if ((uint)itemNumber > _items.size())
+			itemNumber = 1;
+		const Item *item = getItem(itemNumber);
+		if (item && item->enabled &&
+				(item->type == kEditableText || item->listItem)) {
+			setFocus(itemNumber);
+			return;
+		}
+	}
+}
+
+void MacNebularDialog::selectRadio(int itemNumber) {
+	for (uint index = 0; index < _items.size(); ++index) {
+		if (_items[index].type == kRadioButton)
+			_items[index].checked = index + 1 == (uint)itemNumber;
+	}
+	_redraw = true;
+}
+
+void MacNebularDialog::ensureListSelectionVisible(Item &item) {
+	const int rowHeight = MAX(_font->getFontHeight() + 2, 12);
+	const int visibleRows = MAX(1, (item.bounds.height() - 4) / rowHeight);
+	if (item.listSelection < item.listTop)
+		item.listTop = item.listSelection;
+	else if (item.listSelection >= item.listTop + visibleRows)
+		item.listTop = item.listSelection - visibleRows + 1;
+	item.listTop = MAX(0, item.listTop);
+}
+
+void MacNebularDialog::selectList(Item &item, int selection) {
+	if (item.list.empty()) {
+		item.listSelection = -1;
+		item.listTop = 0;
+		return;
+	}
+	item.listSelection = CLIP<int>(selection, 0, item.list.size() - 1);
+	ensureListSelectionVisible(item);
+	_redraw = true;
+}
+
+void MacNebularDialog::moveCaret(Item &item, int position, bool extend) {
+	const uint newPosition = CLIP<int>(position, 0, item.text.size());
+	if (extend) {
+		if (item.selectionStart == item.selectionEnd)
+			item.selectionStart = item.caret;
+		item.selectionEnd = newPosition;
+	} else {
+		item.selectionStart = newPosition;
+		item.selectionEnd = newPosition;
+	}
+	item.caret = newPosition;
+	_redraw = true;
+}
+
+void MacNebularDialog::deleteSelection(Item &item) {
+	const uint start = MIN(item.selectionStart, item.selectionEnd);
+	const uint end = MAX(item.selectionStart, item.selectionEnd);
+	for (uint position = end; position > start; --position)
+		item.text.deleteChar(position - 1);
+	item.caret = start;
+	item.selectionStart = start;
+	item.selectionEnd = start;
+}
+
+void MacNebularDialog::rememberUndo(Item &item) {
+	item.undoText = item.text;
+	item.hasUndo = true;
+}
+
+void MacNebularDialog::insertCharacter(Item &item, char character) {
+	rememberUndo(item);
+	deleteSelection(item);
+	item.text.insertChar(character, item.caret);
+	moveCaret(item, item.caret + 1, false);
+}
+
+void MacNebularDialog::handleKey(const Common::Event &event,
+		int &result, bool &done) {
+	const bool shift = (event.kbd.flags & Common::KBD_SHIFT) != 0;
+	if (event.kbd.keycode == Common::KEYCODE_TAB) {
+		focusNext(shift);
+		return;
+	}
+	if (event.kbd.keycode == Common::KEYCODE_ESCAPE && _cancelItem) {
+		result = _cancelItem;
+		done = true;
+		return;
+	}
+	if ((event.kbd.keycode == Common::KEYCODE_RETURN ||
+			event.kbd.keycode == Common::KEYCODE_KP_ENTER) && _defaultItem) {
+		result = _defaultItem;
+		done = true;
+		return;
+	}
+	if (!_focusItem && (event.kbd.keycode == Common::KEYCODE_UP ||
+			event.kbd.keycode == Common::KEYCODE_DOWN)) {
+		int selected = 0;
+		for (uint index = 0; index < _items.size(); ++index) {
+			if (_items[index].type == kRadioButton && _items[index].checked)
+				selected = index + 1;
+		}
+		const int direction = event.kbd.keycode == Common::KEYCODE_UP ? -1 : 1;
+		for (uint count = 0; count < _items.size(); ++count) {
+			selected += direction;
+			if (selected < 1)
+				selected = _items.size();
+			else if ((uint)selected > _items.size())
+				selected = 1;
+			const Item *radio = getItem(selected);
+			if (radio && radio->enabled && radio->type == kRadioButton) {
+				selectRadio(selected);
+				return;
+			}
+		}
+	}
+
+	Item *item = getItem(_focusItem);
+	if (!item || !item->enabled)
+		return;
+	if (item->listItem) {
+		int selection = item->listSelection;
+		switch (event.kbd.keycode) {
+		case Common::KEYCODE_UP:
+			--selection;
+			break;
+		case Common::KEYCODE_DOWN:
+			++selection;
+			break;
+		case Common::KEYCODE_PAGEUP:
+			selection -= 5;
+			break;
+		case Common::KEYCODE_PAGEDOWN:
+			selection += 5;
+			break;
+		case Common::KEYCODE_HOME:
+			selection = 0;
+			break;
+		case Common::KEYCODE_END:
+			selection = item->list.size() - 1;
+			break;
+		default:
+			return;
+		}
+		selectList(*item, selection);
+		return;
+	}
+	if (item->type != kEditableText)
+		return;
+
+	switch (event.kbd.keycode) {
+	case Common::KEYCODE_LEFT:
+		moveCaret(*item, item->caret - 1, shift);
+		break;
+	case Common::KEYCODE_RIGHT:
+		moveCaret(*item, item->caret + 1, shift);
+		break;
+	case Common::KEYCODE_HOME:
+		moveCaret(*item, 0, shift);
+		break;
+	case Common::KEYCODE_END:
+		moveCaret(*item, item->text.size(), shift);
+		break;
+	case Common::KEYCODE_BACKSPACE:
+		if (item->selectionStart != item->selectionEnd) {
+			rememberUndo(*item);
+			deleteSelection(*item);
+		} else if (item->caret > 0) {
+			rememberUndo(*item);
+			item->text.deleteChar(item->caret - 1);
+			moveCaret(*item, item->caret - 1, false);
+		}
+		break;
+	case Common::KEYCODE_DELETE:
+		if (item->selectionStart != item->selectionEnd) {
+			rememberUndo(*item);
+			deleteSelection(*item);
+		} else if (item->caret < item->text.size()) {
+			rememberUndo(*item);
+			item->text.deleteChar(item->caret);
+			_redraw = true;
+		}
+		break;
+	default:
+		if (event.kbd.ascii >= 32 && event.kbd.ascii < 127)
+			insertCharacter(*item, (char)event.kbd.ascii);
+		break;
+	}
+}
+
+void MacNebularDialog::handleMouseDown(const Common::Point &point) {
+	_pressedItem = 0;
+	for (uint index = 0; index < _items.size(); ++index) {
+		Item &item = _items[index];
+		const int itemNumber = index + 1;
+		const Common::Rect bounds = getItemBounds(item);
+		if (!item.enabled || !bounds.contains(point))
+			continue;
+
+		if (item.type == kButton) {
+			_pressedItem = itemNumber;
+			_redraw = true;
+		} else if (item.type == kCheckbox) {
+			item.checked = !item.checked;
+			_redraw = true;
+		} else if (item.type == kRadioButton) {
+			selectRadio(itemNumber);
+		} else if (item.type == kEditableText) {
+			setFocus(itemNumber);
+			const int x = MAX(0, point.x - bounds.left - 3);
+			uint caret = 0;
+			while (caret < item.text.size() &&
+					_font->getStringWidth(item.text.substr(0, caret + 1)) < x)
+				++caret;
+			moveCaret(item, caret, false);
+		} else if (item.listItem) {
+			setFocus(itemNumber);
+			const int rowHeight = MAX(_font->getFontHeight() + 2, 12);
+			selectList(item, item.listTop +
+				(point.y - bounds.top - 2) / rowHeight);
+		}
+		return;
+	}
+}
+
+void MacNebularDialog::handleMouseUp(const Common::Point &point,
+		int &result, bool &done) {
+	if (_pressedItem) {
+		const Item *item = getItem(_pressedItem);
+		if (item && getItemBounds(*item).contains(point)) {
+			result = _pressedItem;
+			done = true;
+		}
+		_pressedItem = 0;
+		_redraw = true;
+	}
+}
+
+bool MacNebularDialog::hasEditableFocus() const {
+	const Item *item = getItem(_focusItem);
+	return item && item->enabled && item->type == kEditableText;
+}
+
+bool MacNebularDialog::handleEditCommand(MacDialogEditCommand command) {
+	Item *item = getItem(_focusItem);
+	if (!item || !item->enabled || item->type != kEditableText)
+		return false;
+
+	const uint start = MIN(item->selectionStart, item->selectionEnd);
+	const uint end = MAX(item->selectionStart, item->selectionEnd);
+	Common::String selected;
+	if (end > start)
+		selected = item->text.substr(start, end - start);
+
+	switch (command) {
+	case kMacDialogUndo:
+		if (!item->hasUndo)
+			return false;
+		item->text = item->undoText;
+		item->hasUndo = false;
+		moveCaret(*item, item->text.size(), false);
+		break;
+	case kMacDialogCut:
+		if (selected.empty())
+			return false;
+		g_system->setTextInClipboard(Common::U32String(selected));
+		rememberUndo(*item);
+		deleteSelection(*item);
+		break;
+	case kMacDialogCopy:
+		if (selected.empty())
+			return false;
+		g_system->setTextInClipboard(Common::U32String(selected));
+		break;
+	case kMacDialogPaste: {
+		if (!g_system->hasTextInClipboard())
+			return false;
+		const Common::String text = g_system->getTextFromClipboard();
+		rememberUndo(*item);
+		deleteSelection(*item);
+		for (uint index = 0; index < text.size(); ++index) {
+			const char character = text[index];
+			if (character >= 32 && character < 127) {
+				item->text.insertChar(character, item->caret);
+				++item->caret;
+			}
+		}
+		moveCaret(*item, item->caret, false);
+		break;
+	}
+	case kMacDialogClear:
+		if (selected.empty())
+			return false;
+		rememberUndo(*item);
+		deleteSelection(*item);
+		break;
+	}
+	_redraw = true;
+	return true;
+}
+
+void MacNebularDialog::restore(const Graphics::ManagedSurface &saved,
+		bool cursorWasVisible) {
+	_screen.copyRectToSurface(saved.getPixels(), saved.pitch,
+		_bounds.left, _bounds.top, _bounds.width(), _bounds.height());
+	g_system->copyRectToScreen(_screen.getBasePtr(_bounds.left, _bounds.top),
+		_screen.pitch, _bounds.left, _bounds.top,
+		_bounds.width(), _bounds.height());
+	g_system->updateScreen();
+	_windowManager.popCursor();
+	CursorMan.showMouse(cursorWasVisible);
+}
+
+int MacNebularDialog::runModal(int defaultItem, int cancelItem) {
+	if (!_font || !_bounds.isValidRect())
+		return 0;
+	_defaultItem = defaultItem;
+	_cancelItem = cancelItem;
+	_redraw = true;
+	_pressedItem = 0;
+	_focusItem = 0;
+	focusNext(false);
+
+	Graphics::ManagedSurface saved;
+	saved.create(_bounds.width(), _bounds.height(), _screen.format);
+	saved.copyRectToSurface(_screen.getBasePtr(_bounds.left, _bounds.top),
+		_screen.pitch, 0, 0, _bounds.width(), _bounds.height());
+
+	const bool cursorWasVisible = CursorMan.isVisible();
+	_windowManager.clearHandlingWidgets();
+	_windowManager.pushCursor(Graphics::kMacCursorArrow);
+	CursorMan.showMouse(true);
+
+	int result = 0;
+	bool done = false;
+	while (!done) {
+		if (_redraw) {
+			draw();
+			g_system->copyRectToScreen(_screen.getBasePtr(_bounds.left,
+				_bounds.top), _screen.pitch, _bounds.left, _bounds.top,
+				_bounds.width(), _bounds.height());
+			_redraw = false;
+		}
+
+		Common::Event event;
+		while (!done && g_system->getEventManager()->pollEvent(event)) {
+			switch (event.type) {
+			case Common::EVENT_QUIT:
+				_engine.quitGame();
+				done = true;
+				break;
+			case Common::EVENT_KEYDOWN:
+				handleKey(event, result, done);
+				break;
+			case Common::EVENT_LBUTTONDOWN:
+				handleMouseDown(event.mouse);
+				break;
+			case Common::EVENT_LBUTTONUP:
+				handleMouseUp(event.mouse, result, done);
+				break;
+			case Common::EVENT_WHEELUP: {
+				Item *item = getItem(_focusItem);
+				if (item && item->listItem)
+					selectList(*item, item->listSelection - 1);
+				break;
+			}
+			case Common::EVENT_WHEELDOWN: {
+				Item *item = getItem(_focusItem);
+				if (item && item->listItem)
+					selectList(*item, item->listSelection + 1);
+				break;
+			}
+			default:
+				break;
+			}
+		}
+
+		if (!done) {
+			g_system->updateScreen();
+			g_system->delayMillis(10);
+		}
+	}
+
+	restore(saved, cursorWasVisible);
+	return result;
+}
+
+} // namespace RexNebular
+} // namespace MADS
diff --git a/engines/mads/nebular/mac_dialogs.h b/engines/mads/nebular/mac_dialogs.h
new file mode 100644
index 00000000000..f3f171c9941
--- /dev/null
+++ b/engines/mads/nebular/mac_dialogs.h
@@ -0,0 +1,144 @@
+/* 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_DIALOGS_H
+#define MADS_NEBULAR_MAC_DIALOGS_H
+
+#include "common/array.h"
+#include "common/rect.h"
+#include "common/str-array.h"
+
+namespace Graphics {
+class Font;
+class MacWindowManager;
+class ManagedSurface;
+}
+
+namespace MADS {
+namespace RexNebular {
+
+class MacResourceProvider;
+class RexNebularEngine;
+
+enum MacDialogEditCommand {
+	kMacDialogUndo,
+	kMacDialogCut,
+	kMacDialogCopy,
+	kMacDialogPaste,
+	kMacDialogClear
+};
+
+class MacNebularDialog {
+private:
+	enum ItemType {
+		kUserItem = 0,
+		kButton = 4,
+		kCheckbox = 5,
+		kRadioButton = 6,
+		kStaticText = 8,
+		kEditableText = 16
+	};
+
+	struct Item {
+		Common::Rect bounds;
+		Common::String text;
+		Common::String undoText;
+		Common::StringArray list;
+		byte type;
+		bool enabled;
+		bool checked;
+		bool listItem;
+		bool hasUndo;
+		int listSelection;
+		int listTop;
+		uint caret;
+		uint selectionStart;
+		uint selectionEnd;
+
+		Item();
+	};
+
+	RexNebularEngine &_engine;
+	MacResourceProvider &_resources;
+	Graphics::ManagedSurface &_screen;
+	Graphics::MacWindowManager &_windowManager;
+	const Graphics::Font *_font;
+	Common::Rect _bounds;
+	Common::Array<Item> _items;
+	int _focusItem;
+	int _pressedItem;
+	int _defaultItem;
+	int _cancelItem;
+	bool _redraw;
+
+	Item *getItem(int itemNumber);
+	const Item *getItem(int itemNumber) const;
+	Common::Rect getItemBounds(const Item &item) const;
+	bool loadDialogResource(uint16 resourceID, uint16 &itemResourceID);
+	bool loadDialogItems(uint16 resourceID);
+	void draw();
+	void drawItem(Item &item, int itemNumber);
+	void drawButton(const Item &item, const Common::Rect &bounds,
+		bool pressed);
+	void drawCheckbox(const Item &item, const Common::Rect &bounds);
+	void drawRadioButton(const Item &item, const Common::Rect &bounds);
+	void drawEditable(Item &item, const Common::Rect &bounds,
+		bool focused);
+	void drawList(const Item &item, const Common::Rect &bounds,
+		bool focused);
+	void setFocus(int itemNumber);
+	void focusNext(bool backwards);
+	void selectRadio(int itemNumber);
+	void selectList(Item &item, int selection);
+	void ensureListSelectionVisible(Item &item);
+	void moveCaret(Item &item, int position, bool extend);
+	void deleteSelection(Item &item);
+	void rememberUndo(Item &item);
+	void insertCharacter(Item &item, char character);
+	void handleKey(const Common::Event &event, int &result, bool &done);
+	void handleMouseDown(const Common::Point &point);
+	void handleMouseUp(const Common::Point &point, int &result, bool &done);
+	void restore(const Graphics::ManagedSurface &saved, bool cursorWasVisible);
+
+public:
+	MacNebularDialog(RexNebularEngine &engine,
+		MacResourceProvider &resources, Graphics::ManagedSurface &screen,
+		Graphics::MacWindowManager &windowManager);
+
+	bool load(uint16 resourceID);
+	void center();
+	void setItemEnabled(int itemNumber, bool enabled);
+	void setItemChecked(int itemNumber, bool checked);
+	bool isItemChecked(int itemNumber) const;
+	void setItemText(int itemNumber, const Common::String &text);
+	Common::String getItemText(int itemNumber) const;
+	void setList(int itemNumber, const Common::StringArray &entries,
+		int selection);
+	int getListSelection(int itemNumber) const;
+	bool hasEditableFocus() const;
+	bool handleEditCommand(MacDialogEditCommand command);
+	int runModal(int defaultItem, int cancelItem);
+};
+
+} // namespace RexNebular
+} // namespace MADS
+
+#endif
diff --git a/engines/mads/nebular/mac_menus.cpp b/engines/mads/nebular/mac_menus.cpp
index d360c7f1be7..e52b16ef76e 100644
--- a/engines/mads/nebular/mac_menus.cpp
+++ b/engines/mads/nebular/mac_menus.cpp
@@ -24,19 +24,17 @@
 #include "common/events.h"
 #include "common/stream.h"
 #include "common/system.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 "graphics/primitives.h"
 #include "gui/saveload.h"
 #include "mads/core/config.h"
 #include "mads/core/game.h"
 #include "mads/core/kernel.h"
 #include "mads/core/sound_manager.h"
 #include "mads/mads.h"
+#include "mads/nebular/mac_dialogs.h"
 #include "mads/nebular/mac_menus.h"
 #include "mads/nebular/mac_resources.h"
 #include "mads/nebular/nebular.h"
@@ -48,9 +46,6 @@ namespace {
 
 enum {
 	kDifficultyDialog = 2500,
-	kDialogButton = 4,
-	kDialogRadioButton = 6,
-	kDialogStaticText = 8,
 
 	kFileMenu = 1,
 	kOptionsMenu = 3,
@@ -69,19 +64,6 @@ 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;
@@ -125,138 +107,6 @@ 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,
@@ -547,134 +397,21 @@ int MacNebularMenu::runDifficultyDialog() {
 	if (!initializeWindowManager())
 		return -1;
 
-	Common::SeekableReadStream *dialogStream = _resources.openResource(
-		MacResourceProvider::kApplicationContainer,
-		MKTAG('D', 'L', 'O', 'G'), kDifficultyDialog);
-	if (!dialogStream)
+	MacNebularDialog dialog(_engine, _resources, _screen, *_windowManager);
+	if (!dialog.load(kDifficultyDialog))
 		return -1;
-
-	DialogResource dialog;
-	const bool validDialog = readDialogResource(*dialogStream, dialog);
-	delete dialogStream;
-	if (!validDialog || dialog.itemList != kDifficultyDialog)
+	dialog.center();
+	dialog.setItemChecked(4, true);
+	_activeDialog = &dialog;
+	const int result = dialog.runModal(1, 0);
+	_activeDialog = nullptr;
+	if (result != 1)
 		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());
-			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->updateScreen();
-			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);
+	if (dialog.isItemChecked(3))
+		return DIFFICULTY_EASY;
+	if (dialog.isItemChecked(5))
+		return DIFFICULTY_HARD;
+	return DIFFICULTY_MEDIUM;
 }
 
 int MacNebularMenu::selectResumeSlot() {
diff --git a/engines/mads/nebular/mac_menus.h b/engines/mads/nebular/mac_menus.h
index c4790f3292b..40bf8225e51 100644
--- a/engines/mads/nebular/mac_menus.h
+++ b/engines/mads/nebular/mac_menus.h
@@ -42,6 +42,7 @@ namespace RexNebular {
 
 class MacResourceProvider;
 class RexNebularEngine;
+class MacNebularDialog;
 
 class MacNebularMenu {
 private:
@@ -50,6 +51,7 @@ private:
 	Graphics::ManagedSurface &_screen;
 	Graphics::MacWindowManager *_windowManager = nullptr;
 	Graphics::MacMenu *_menu = nullptr;
+	MacNebularDialog *_activeDialog = nullptr;
 	byte _palette[256 * 3] = {};
 	bool _paletteValid = false;
 	bool _outerMenuActive = false;


Commit: b31963c7cf194130bc85a97dc56a6a9a10180fa3
    https://github.com/scummvm/scummvm/commit/b31963c7cf194130bc85a97dc56a6a9a10180fa3
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: NEBULAR: Restore Macintosh display preferences

Restore native display sizing, menu-bar visibility, and startup
preference controls. Defer the startup modal until a complete frame has
been presented and the normal engine event loop regains ownership.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/mads.cpp
    engines/mads/mads.h
    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/nebular.h


diff --git a/engines/mads/mads.cpp b/engines/mads/mads.cpp
index 5af096abec4..6ec7baf5fb2 100644
--- a/engines/mads/mads.cpp
+++ b/engines/mads/mads.cpp
@@ -247,6 +247,7 @@ void MADSEngine::pollEvents() {
 	if (time >= _nextFrameTime) {
 		updateScreen();
 		_nextFrameTime = time + GAME_FRAME_TIME;
+		serviceMacintoshUI();
 	}
 
 	// Handle calling any set timer function
diff --git a/engines/mads/mads.h b/engines/mads/mads.h
index 19238ffcbe0..904773f2d62 100644
--- a/engines/mads/mads.h
+++ b/engines/mads/mads.h
@@ -93,6 +93,7 @@ protected:
 	virtual void presentScreen(int shakeOffset);
 
 	virtual bool handleMacEvent(Common::Event &event) { return false; }
+	virtual void serviceMacintoshUI() {}
 	virtual void serviceMacintoshSound() {}
 
 	bool hasFeature(EngineFeature f) const override;
diff --git a/engines/mads/nebular/mac_menus.cpp b/engines/mads/nebular/mac_menus.cpp
index e52b16ef76e..6d9a079ee56 100644
--- a/engines/mads/nebular/mac_menus.cpp
+++ b/engines/mads/nebular/mac_menus.cpp
@@ -36,6 +36,7 @@
 #include "mads/mads.h"
 #include "mads/nebular/mac_dialogs.h"
 #include "mads/nebular/mac_menus.h"
+#include "mads/nebular/mac_nebular.h"
 #include "mads/nebular/mac_resources.h"
 #include "mads/nebular/nebular.h"
 
@@ -46,6 +47,8 @@ namespace {
 
 enum {
 	kDifficultyDialog = 2500,
+	kStartupPreferencesDialog = 2005,
+	kPreferencesDialog = 2105,
 
 	kFileMenu = 1,
 	kOptionsMenu = 3,
@@ -54,6 +57,7 @@ enum {
 	kFileOpen = (1001 << 16) | 2,
 	kFileSave = (1001 << 16) | 4,
 	kFileSaveAs = (1001 << 16) | 5,
+	kFilePreferences = (1001 << 16) | 7,
 	kFileQuit = (1001 << 16) | 9,
 	kOptionNoSound = (2001 << 16),
 	kOptionEasyInterface = (2001 << 16) | 1,
@@ -61,7 +65,10 @@ enum {
 	kFadeMedium = (101 << 16) | 1,
 	kFadeFast = (101 << 16) | 2,
 	kStoryNaughty = (102 << 16),
-	kStoryNice = (102 << 16) | 1
+	kStoryNice = (102 << 16) | 1,
+	kWindowStandard = (2000 << 16),
+	kWindow150 = (2000 << 16) | 1,
+	kWindow200 = (2000 << 16) | 2
 };
 
 struct MenuResourceItem {
@@ -127,11 +134,13 @@ bool MacNebularMenu::initializeWindowManager() {
 	byte palette[256 * 3];
 	g_system->getPaletteManager()->grabPalette(palette, 0, 256);
 
-	const uint32 mode = Graphics::kWMModeNoDesktop |
+	_windowManagerMode = Graphics::kWMModeNoDesktop |
 		Graphics::kWMModalMenuMode | Graphics::kWMModeNoCursorOverride |
 		Graphics::kWMModeForceMacFonts | Graphics::kWMModeNoSystemRedraw |
 		Graphics::kWMNoScummVMWallpaper;
-	_windowManager = new Graphics::MacWindowManager(mode);
+	if (_engine.getMacintoshHideMenuBar())
+		_windowManagerMode |= Graphics::kWMModeAutohideMenu;
+	_windowManager = new Graphics::MacWindowManager(_windowManagerMode);
 	g_system->getPaletteManager()->setPalette(palette, 0, 256);
 	_windowManager->setEngine(&_engine);
 	_windowManager->setScreen(&_screen);
@@ -245,6 +254,7 @@ void MacNebularMenu::updateState() {
 	const bool canSave = _engine.canSaveGameStateCurrently(nullptr);
 	setItemState(getMenuItem(kFileMenu, 4), canSave, false);
 	setItemState(getMenuItem(kFileMenu, 5), canSave, false);
+	setItemState(getMenuItem(kFileMenu, 7), true, false);
 	setItemState(getMenuItem(kFileMenu, 9), true, false);
 
 	const bool noSound = !_engine._musicFlag && !_engine._soundFlag;
@@ -266,9 +276,11 @@ void MacNebularMenu::updateState() {
 	setItemState(getSubMenuItem(kOptionsMenu, 3, 2), false,
 		config_file.naughtiness != NAUGHTY && config_file.naughtiness != NICE);
 
-	setItemState(getMenuItem(kWindowMenu, 0), false, false);
-	setItemState(getMenuItem(kWindowMenu, 1), false, false);
-	setItemState(getMenuItem(kWindowMenu, 2), true, true);
+	const int displaySize = _engine.getMacintoshDisplaySize();
+	for (int size = kMacNebularDisplay100;
+			size <= kMacNebularDisplay200; ++size)
+		setItemState(getMenuItem(kWindowMenu, size), true,
+			displaySize == size);
 
 	if (_outerMenuActive) {
 		// CODE 133 disables New, Resume, Open, Save, and Save As while
@@ -292,6 +304,9 @@ void MacNebularMenu::dispatchCommand(int commandId) {
 		if (_engine.canSaveGameStateCurrently(nullptr))
 			_engine.saveGameDialog();
 		break;
+	case kFilePreferences:
+		runPreferencesDialog(false);
+		break;
 	case kFileQuit:
 		_engine.quitGame();
 		game.going = false;
@@ -330,6 +345,11 @@ void MacNebularMenu::dispatchCommand(int commandId) {
 		ConfMan.setBool("naughtiness", config_file.naughtiness == NAUGHTY);
 		ConfMan.flushToDisk();
 		break;
+	case kWindowStandard:
+	case kWindow150:
+	case kWindow200:
+		_engine.setMacintoshDisplaySize(commandId - kWindowStandard, true);
+		break;
 	default:
 		break;
 	}
@@ -387,6 +407,47 @@ void MacNebularMenu::getMenuColors(byte &menuBlack, byte &menuWhite) {
 	menuWhite = (byte)_windowManager->_colorWhite;
 }
 
+void MacNebularMenu::setMenuBarHidden(bool hidden) {
+	if (!initialize())
+		return;
+	if (hidden)
+		_windowManagerMode |= Graphics::kWMModeAutohideMenu;
+	else
+		_windowManagerMode &= ~Graphics::kWMModeAutohideMenu;
+	_windowManager->setMode(_windowManagerMode);
+	_menu->setVisible(!hidden, true);
+}
+
+void MacNebularMenu::runPreferencesDialog(bool startup) {
+	if (!initializeWindowManager())
+		return;
+
+	MacNebularDialog dialog(_engine, _resources, _screen, *_windowManager);
+	if (!dialog.load(startup ? kStartupPreferencesDialog :
+			kPreferencesDialog))
+		return;
+	dialog.center();
+	dialog.setItemChecked(4, _engine.getMacintoshHideMenuBar());
+	dialog.setItemEnabled(5, false);
+	dialog.setItemEnabled(6, false);
+	dialog.setItemChecked(7, true);
+	dialog.setItemChecked(8,
+		_engine.getMacintoshPreferencesAtStartup());
+	if (startup)
+		dialog.setItemEnabled(2, false);
+
+	_activeDialog = &dialog;
+	const int result = dialog.runModal(1, startup ? 0 : 2);
+	_activeDialog = nullptr;
+	if (result != 1)
+		return;
+
+	const bool persist = dialog.isItemChecked(7);
+	_engine.setMacintoshHideMenuBar(dialog.isItemChecked(4), persist);
+	_engine.setMacintoshPreferencesAtStartup(dialog.isItemChecked(8),
+		persist);
+}
+
 void MacNebularMenu::menuCallback(int commandId, Common::String &, void *data) {
 	MacNebularMenu *menus = (MacNebularMenu *)data;
 	menus->_pendingCommand = commandId;
diff --git a/engines/mads/nebular/mac_menus.h b/engines/mads/nebular/mac_menus.h
index 40bf8225e51..1940a95f185 100644
--- a/engines/mads/nebular/mac_menus.h
+++ b/engines/mads/nebular/mac_menus.h
@@ -55,6 +55,7 @@ private:
 	byte _palette[256 * 3] = {};
 	bool _paletteValid = false;
 	bool _outerMenuActive = false;
+	uint32 _windowManagerMode = 0;
 	int _pendingCommand = -1;
 
 	static void menuCallback(int commandId, Common::String &name, void *data);
@@ -78,6 +79,8 @@ public:
 	void draw();
 	byte getBlackColor();
 	void getMenuColors(byte &menuBlack, byte &menuWhite);
+	void setMenuBarHidden(bool hidden);
+	void runPreferencesDialog(bool startup);
 	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 52b8f6ffc32..0cfb3095e63 100644
--- a/engines/mads/nebular/mac_nebular.cpp
+++ b/engines/mads/nebular/mac_nebular.cpp
@@ -48,16 +48,18 @@ namespace RexNebular {
 
 enum {
 	kMacScreenWidth = 640,
-	kMacSceneHeight = 312,
+	kMacLogicalSceneWidth = 320,
+	kMacLogicalSceneHeight = 156,
+	kMacLargeSceneHeight = 312,
 	kMacInterfaceWidth = 512,
 	kMacInterfaceHeight = 88,
 	kMacInterfaceX = (kMacScreenWidth - kMacInterfaceWidth) / 2,
-	kMacLegacyScreenHeight = kMacSceneHeight + kMacInterfaceHeight,
+	kMacLegacyScreenHeight = kMacLargeSceneHeight + kMacInterfaceHeight,
 	kMacDesktopHeight = 480,
 	kMacMenuBarHeight = 20,
 	kMacDesktopSceneY = kMacMenuBarHeight + 20,
 	kMacFullFrameHeight = 400,
-	kMacDesktopSeparatorY = kMacDesktopSceneY + kMacSceneHeight,
+	kMacDesktopSeparatorY = kMacDesktopSceneY + kMacLargeSceneHeight,
 	kMacDesktopInterfaceY = kMacDesktopSeparatorY + 1,
 	kMacPanelPaletteColors = 10,
 	kMacScenePaletteStart = 16,
@@ -391,10 +393,91 @@ static bool isMacInterfaceScrollbarPixel(int x, int y) {
 }
 
 MacNebular::MacNebular(RexNebularEngine &engine) :
-		_engine(engine), _useOriginalMenus(ConfMan.getBool("original_mac_menus")) {
+		_engine(engine), _useOriginalMenus(ConfMan.getBool("original_mac_menus")),
+		_displaySize(kMacNebularDisplay200), _hideMenuBar(false),
+		_preferencesAtStartup(false), _showPreferencesAtStartup(false) {
+	ConfMan.registerDefault("mac_nebular_display_size", kMacNebularDisplay200);
+	ConfMan.registerDefault("mac_nebular_hide_menu_bar", false);
+	ConfMan.registerDefault("mac_nebular_preferences_at_startup", false);
+	if (_useOriginalMenus) {
+		_displaySize = CLIP<int>(ConfMan.getInt("mac_nebular_display_size"),
+			kMacNebularDisplay100, kMacNebularDisplay200);
+		_hideMenuBar = ConfMan.getBool("mac_nebular_hide_menu_bar");
+		_preferencesAtStartup =
+			ConfMan.getBool("mac_nebular_preferences_at_startup");
+		_showPreferencesAtStartup = _preferencesAtStartup;
+	}
 	memset(_palette, 0, sizeof(_palette));
 }
 
+int MacNebular::getSceneWidth() const {
+	static const int widths[] = { 320, 480, 640 };
+	return widths[_useOriginalMenus ? _displaySize : kMacNebularDisplay200];
+}
+
+int MacNebular::getSceneHeight() const {
+	static const int heights[] = { 156, 234, 312 };
+	return heights[_useOriginalMenus ? _displaySize : kMacNebularDisplay200];
+}
+
+int MacNebular::getSceneX() const {
+	return _useOriginalMenus ? (kMacScreenWidth - getSceneWidth()) / 2 : 0;
+}
+
+int MacNebular::getSceneY() const {
+	return _useOriginalMenus ?
+		(kMacDesktopHeight - kMacInterfaceHeight - getSceneHeight()) / 2 : 0;
+}
+
+int MacNebular::getInterfaceY() const {
+	return _useOriginalMenus ? getSceneY() + getSceneHeight() + 1 :
+		kMacLargeSceneHeight;
+}
+
+void MacNebular::setDisplaySize(int displaySize, bool persist) {
+	if (!_useOriginalMenus)
+		return;
+	_displaySize = CLIP<int>(displaySize, kMacNebularDisplay100,
+		kMacNebularDisplay200);
+	_layoutLogged = false;
+	if (persist) {
+		ConfMan.setInt("mac_nebular_display_size", _displaySize);
+		ConfMan.flushToDisk();
+	}
+}
+
+void MacNebular::setHideMenuBar(bool hide, bool persist) {
+	if (!_useOriginalMenus)
+		return;
+	_hideMenuBar = hide;
+	if (_menus)
+		_menus->setMenuBarHidden(hide);
+	if (persist) {
+		ConfMan.setBool("mac_nebular_hide_menu_bar", hide);
+		ConfMan.flushToDisk();
+	}
+}
+
+void MacNebular::setPreferencesAtStartup(bool show, bool persist) {
+	if (!_useOriginalMenus)
+		return;
+	_preferencesAtStartup = show;
+	if (persist) {
+		ConfMan.setBool("mac_nebular_preferences_at_startup", show);
+		ConfMan.flushToDisk();
+	}
+}
+
+void MacNebular::serviceUI() {
+	if (!_startupPreferencesReady || !_showPreferencesAtStartup || !_menus)
+		return;
+
+	_startupPreferencesReady = false;
+	_showPreferencesAtStartup = false;
+	_menus->runPreferencesDialog(true);
+	_engine._screen->markAllDirty();
+}
+
 MacNebular::~MacNebular() {
 	delete _menus;
 
@@ -432,6 +515,7 @@ bool MacNebular::initResources() {
 			delete _menus;
 			_menus = new MacNebularMenu(_engine, *_resources, _output);
 		}
+		_menus->setMenuBarHidden(_hideMenuBar);
 	}
 	return true;
 }
@@ -484,12 +568,16 @@ Common::Point MacNebular::screenToGame(const Common::Point &point) const {
 		return Common::Point(-1, -1);
 	}
 
-	const int sceneY = _useOriginalMenus ? kMacDesktopSceneY : 0;
-	const int interfaceY = _useOriginalMenus ?
-		kMacDesktopInterfaceY : kMacSceneHeight;
-	if (point.y >= sceneY && point.y < sceneY + kMacSceneHeight)
-		return Common::Point(CLIP<int>(point.x / 2, 0, 319),
-			(point.y - sceneY) / 2);
+	const int sceneX = getSceneX();
+	const int sceneY = getSceneY();
+	const int sceneWidth = getSceneWidth();
+	const int sceneHeight = getSceneHeight();
+	const int interfaceY = getInterfaceY();
+	if (point.x >= sceneX && point.x < sceneX + sceneWidth &&
+			point.y >= sceneY && point.y < sceneY + sceneHeight)
+		return Common::Point(
+			(point.x - sceneX) * kMacLogicalSceneWidth / sceneWidth,
+			(point.y - sceneY) * kMacLogicalSceneHeight / sceneHeight);
 
 	if (point.y >= interfaceY && point.y < interfaceY + kMacInterfaceHeight &&
 			point.x >= kMacInterfaceX &&
@@ -511,11 +599,15 @@ Common::Point MacNebular::gameToScreen(const Common::Point &point) const {
 			frameY + point.y * 2);
 	}
 
-	const int sceneY = _useOriginalMenus ? kMacDesktopSceneY : 0;
-	const int interfaceY = _useOriginalMenus ?
-		kMacDesktopInterfaceY : kMacSceneHeight;
+	const int sceneX = getSceneX();
+	const int sceneY = getSceneY();
+	const int sceneWidth = getSceneWidth();
+	const int sceneHeight = getSceneHeight();
+	const int interfaceY = getInterfaceY();
 	if (point.y < 156)
-		return Common::Point(point.x * 2, sceneY + point.y * 2);
+		return Common::Point(sceneX + point.x * sceneWidth /
+			kMacLogicalSceneWidth, sceneY + point.y * sceneHeight /
+			kMacLogicalSceneHeight);
 
 	return Common::Point(kMacInterfaceX + point.x * kMacInterfaceWidth / 320,
 		interfaceY + (point.y - 156) * 2);
@@ -610,26 +702,28 @@ void MacNebular::presentScreen(int shakeOffset) {
 			0, 0, kMacScreenWidth, _output.h);
 		g_system->updateScreen();
 		_engine._screen->clearDirtyRects();
+		_startupPreferencesReady = true;
 		return;
 	}
 
-	const int sceneY = _useOriginalMenus ? kMacDesktopSceneY : 0;
-	const int interfaceY = _useOriginalMenus ?
-		kMacDesktopInterfaceY : kMacSceneHeight;
+	const int sceneX = getSceneX();
+	const int sceneY = getSceneY();
+	const int sceneWidth = getSceneWidth();
+	const int sceneHeight = getSceneHeight();
+	const int interfaceY = getInterfaceY();
 	setMacInterfacePalette(_resources);
 	_output.fillRect(_output.getBounds(), kMacBlackColor);
 
-	// Native large-window mode doubles the 320x156 scene in both axes.
-	for (int y = 0; y < 156; ++y) {
-		const byte *source = (const byte *)_engine._screen->getBasePtr(0, y);
-		byte *line1 = (byte *)_output.getBasePtr(0, sceneY + y * 2);
-		byte *line2 = (byte *)_output.getBasePtr(0, sceneY + 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;
+	for (int y = 0; y < sceneHeight; ++y) {
+		const int sourceY = y * kMacLogicalSceneHeight / sceneHeight;
+		const byte *source =
+			(const byte *)_engine._screen->getBasePtr(0, sourceY);
+		byte *target = (byte *)_output.getBasePtr(sceneX, sceneY + y);
+		for (int x = 0; x < sceneWidth; ++x) {
+			const int sourceX = x * kMacLogicalSceneWidth / sceneWidth;
+			target[x] = source[(sourceX + shakeOffset) %
+				kMacLogicalSceneWidth];
 		}
-		memcpy(line2, line1, kMacScreenWidth);
 	}
 
 	const Graphics::Surface *nativeInterface =
@@ -724,6 +818,7 @@ void MacNebular::presentScreen(int shakeOffset) {
 		0, 0, kMacScreenWidth, _output.h);
 	g_system->updateScreen();
 	_engine._screen->clearDirtyRects();
+	_startupPreferencesReady = true;
 }
 
 bool MacNebular::handleMacEvent(Common::Event &event) {
@@ -894,6 +989,11 @@ bool RexNebularEngine::handleMacEvent(Common::Event &event) {
 	return _macNebular && _macNebular->handleMacEvent(event);
 }
 
+void RexNebularEngine::serviceMacintoshUI() {
+	if (_macNebular)
+		_macNebular->serviceUI();
+}
+
 void RexNebularEngine::serviceMacintoshSound() {
 	if (_macNebular)
 		_macNebular->serviceSound();
@@ -912,6 +1012,36 @@ bool RexNebularEngine::usesOriginalMacintoshMenus() const {
 	return _macNebular && _macNebular->usesOriginalMenus();
 }
 
+int RexNebularEngine::getMacintoshDisplaySize() const {
+	return _macNebular ? _macNebular->getDisplaySize() :
+		kMacNebularDisplay200;
+}
+
+bool RexNebularEngine::getMacintoshHideMenuBar() const {
+	return _macNebular && _macNebular->getHideMenuBar();
+}
+
+bool RexNebularEngine::getMacintoshPreferencesAtStartup() const {
+	return _macNebular && _macNebular->getPreferencesAtStartup();
+}
+
+void RexNebularEngine::setMacintoshDisplaySize(int displaySize,
+		bool persist) {
+	if (_macNebular)
+		_macNebular->setDisplaySize(displaySize, persist);
+}
+
+void RexNebularEngine::setMacintoshHideMenuBar(bool hide, bool persist) {
+	if (_macNebular)
+		_macNebular->setHideMenuBar(hide, persist);
+}
+
+void RexNebularEngine::setMacintoshPreferencesAtStartup(bool show,
+		bool persist) {
+	if (_macNebular)
+		_macNebular->setPreferencesAtStartup(show, persist);
+}
+
 void RexNebularEngine::setMacintoshOuterMenuActive(bool active) {
 	if (_macNebular)
 		_macNebular->setOuterMenuActive(active);
diff --git a/engines/mads/nebular/mac_nebular.h b/engines/mads/nebular/mac_nebular.h
index bd29d84b957..5a9425deb9c 100644
--- a/engines/mads/nebular/mac_nebular.h
+++ b/engines/mads/nebular/mac_nebular.h
@@ -37,6 +37,12 @@ class MacResourceProvider;
 class MacNebularMenu;
 class RexNebularEngine;
 
+enum MacNebularDisplaySize {
+	kMacNebularDisplay100 = 0,
+	kMacNebularDisplay150 = 1,
+	kMacNebularDisplay200 = 2
+};
+
 class MacNebular {
 private:
 	RexNebularEngine &_engine;
@@ -47,6 +53,11 @@ private:
 	Common::Rect _popupRect;
 	RGBcolor _palette[256];
 	bool _useOriginalMenus;
+	int _displaySize;
+	bool _hideMenuBar;
+	bool _preferencesAtStartup;
+	bool _showPreferencesAtStartup;
+	bool _startupPreferencesReady = false;
 	bool _fullFrameActive = false;
 	bool _popupActive = false;
 	bool _layoutLogged = false;
@@ -55,6 +66,11 @@ private:
 	uint32 _macintoshSoundPausedTicks = 0;
 	bool _macintoshSoundPaused = false;
 
+	int getSceneWidth() const;
+	int getSceneHeight() const;
+	int getSceneX() const;
+	int getSceneY() const;
+	int getInterfaceY() const;
 public:
 	explicit MacNebular(RexNebularEngine &engine);
 	~MacNebular();
@@ -65,11 +81,18 @@ public:
 	void selectDifficulty();
 	int selectResumeSlot();
 	bool usesOriginalMenus() const { return _useOriginalMenus; }
+	int getDisplaySize() const { return _displaySize; }
+	bool getHideMenuBar() const { return _hideMenuBar; }
+	bool getPreferencesAtStartup() const { return _preferencesAtStartup; }
+	void setDisplaySize(int displaySize, bool persist);
+	void setHideMenuBar(bool hide, bool persist);
+	void setPreferencesAtStartup(bool show, bool persist);
 	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);
+	void serviceUI();
 	void serviceSound();
 	void setPalette(const RGBcolor *palette, int firstColor, int numColors);
 	void getPalette(RGBcolor *palette, int firstColor, int numColors) const;
diff --git a/engines/mads/nebular/nebular.h b/engines/mads/nebular/nebular.h
index c14ed97dbfd..f45e4f3c983 100644
--- a/engines/mads/nebular/nebular.h
+++ b/engines/mads/nebular/nebular.h
@@ -54,6 +54,7 @@ protected:
 	Common::Point gameToScreen(const Common::Point &point) const override;
 	void presentScreen(int shakeOffset) override;
 	bool handleMacEvent(Common::Event &event) override;
+	void serviceMacintoshUI() override;
 	void serviceMacintoshSound() override;
 
 public:
@@ -65,6 +66,12 @@ public:
 	void selectMacintoshDifficulty();
 	int selectMacintoshResumeSlot();
 	bool usesOriginalMacintoshMenus() const;
+	int getMacintoshDisplaySize() const;
+	bool getMacintoshHideMenuBar() const;
+	bool getMacintoshPreferencesAtStartup() const;
+	void setMacintoshDisplaySize(int displaySize, bool persist);
+	void setMacintoshHideMenuBar(bool hide, bool persist);
+	void setMacintoshPreferencesAtStartup(bool show, bool persist);
 	void setMacintoshOuterMenuActive(bool active);
 	void setMacintoshFullFrameActive(bool active);
 


Commit: fe680ad7b31c8b6e029578e566245da8d413b77f
    https://github.com/scummvm/scummvm/commit/fe680ad7b31c8b6e029578e566245da8d413b77f
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: NEBULAR: Restore Macintosh file dialogs

Use the native Standard File dialog resources while retaining ScummVM
save slots and serialization.

Assisted-by: Codex:GPT-5.4

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


diff --git a/engines/mads/nebular/mac_menus.cpp b/engines/mads/nebular/mac_menus.cpp
index 6d9a079ee56..536bc9e0076 100644
--- a/engines/mads/nebular/mac_menus.cpp
+++ b/engines/mads/nebular/mac_menus.cpp
@@ -19,15 +19,18 @@
  *
  */
 
+#include "common/array.h"
 #include "common/config-manager.h"
 #include "common/debug.h"
 #include "common/events.h"
 #include "common/stream.h"
 #include "common/system.h"
+#include "common/translation.h"
 #include "graphics/macgui/macmenu.h"
 #include "graphics/macgui/macwindowmanager.h"
 #include "graphics/managed_surface.h"
 #include "graphics/paletteman.h"
+#include "gui/message.h"
 #include "gui/saveload.h"
 #include "mads/core/config.h"
 #include "mads/core/game.h"
@@ -49,6 +52,8 @@ enum {
 	kDifficultyDialog = 2500,
 	kStartupPreferencesDialog = 2005,
 	kPreferencesDialog = 2105,
+	kSaveDialog = 3001,
+	kOpenDialog = 3010,
 
 	kFileMenu = 1,
 	kOptionsMenu = 3,
@@ -71,6 +76,43 @@ enum {
 	kWindow200 = (2000 << 16) | 2
 };
 
+void collectSaves(RexNebularEngine &engine, Common::StringArray &names,
+		Common::Array<int> &slots) {
+	const SaveStateList saves = engine.listSaves();
+	for (SaveStateList::const_iterator save = saves.begin();
+			save != saves.end(); ++save) {
+		if (save->getSaveSlot() < 1 || save->getSaveSlot() > 99)
+			continue;
+		names.push_back(save->getDescription());
+		slots.push_back(save->getSaveSlot());
+	}
+}
+
+int findSaveSlot(const Common::Array<int> &slots, int slot) {
+	for (uint index = 0; index < slots.size(); ++index) {
+		if (slots[index] == slot)
+			return index;
+	}
+	return -1;
+}
+
+int findSaveName(const Common::StringArray &names,
+		const Common::String &name) {
+	for (uint index = 0; index < names.size(); ++index) {
+		if (names[index].equalsIgnoreCase(name))
+			return index;
+	}
+	return -1;
+}
+
+int firstFreeSaveSlot(const Common::Array<int> &slots) {
+	for (int slot = 1; slot <= 99; ++slot) {
+		if (findSaveSlot(slots, slot) < 0)
+			return slot;
+	}
+	return -1;
+}
+
 struct MenuResourceItem {
 	Common::String text;
 	byte key = 0;
@@ -297,12 +339,15 @@ void MacNebularMenu::dispatchCommand(int commandId) {
 	switch (commandId) {
 	case kFileOpen:
 		if (_engine.canLoadGameStateCurrently(nullptr))
-			_engine.loadGameDialog();
+			runOpenDialog();
 		break;
 	case kFileSave:
+		if (_engine.canSaveGameStateCurrently(nullptr))
+			runSaveDialog(false);
+		break;
 	case kFileSaveAs:
 		if (_engine.canSaveGameStateCurrently(nullptr))
-			_engine.saveGameDialog();
+			runSaveDialog(true);
 		break;
 	case kFilePreferences:
 		runPreferencesDialog(false);
@@ -448,6 +493,103 @@ void MacNebularMenu::runPreferencesDialog(bool startup) {
 		persist);
 }
 
+void MacNebularMenu::runOpenDialog() {
+	if (!initializeWindowManager())
+		return;
+
+	Common::StringArray names;
+	Common::Array<int> slots;
+	collectSaves(_engine, names, slots);
+	if (slots.empty())
+		return;
+
+	MacNebularDialog dialog(_engine, _resources, _screen, *_windowManager);
+	if (!dialog.load(kOpenDialog))
+		return;
+	dialog.center();
+	dialog.setItemEnabled(5, false);
+	dialog.setItemEnabled(6, false);
+	int selection = findSaveSlot(slots, game.last_save + 1);
+	if (selection < 0)
+		selection = 0;
+	dialog.setList(7, names, selection);
+
+	_activeDialog = &dialog;
+	const int result = dialog.runModal(1, 3);
+	_activeDialog = nullptr;
+	if (result != 1)
+		return;
+
+	selection = dialog.getListSelection(7);
+	if (selection < 0 || (uint)selection >= slots.size())
+		return;
+	const int slot = slots[selection];
+	if (_engine.loadGameState(slot).getCode() == Common::kNoError)
+		game.last_save = slot - 1;
+}
+
+void MacNebularMenu::runSaveDialog(bool saveAs) {
+	Common::StringArray names;
+	Common::Array<int> slots;
+	collectSaves(_engine, names, slots);
+	const int currentSlot = game.last_save + 1;
+	const int currentIndex = findSaveSlot(slots, currentSlot);
+	if (!saveAs && currentIndex >= 0) {
+		const int previousLastSave = game.last_save;
+		game.last_save = currentSlot - 1;
+		if (_engine.saveGameState(currentSlot,
+				names[currentIndex]).getCode() != Common::kNoError)
+			game.last_save = previousLastSave;
+		return;
+	}
+
+	if (!initializeWindowManager())
+		return;
+	MacNebularDialog dialog(_engine, _resources, _screen, *_windowManager);
+	if (!dialog.load(kSaveDialog))
+		return;
+	dialog.center();
+	dialog.setItemEnabled(4, false);
+	dialog.setItemEnabled(5, false);
+	dialog.setList(8, names, currentIndex >= 0 ? currentIndex : 0);
+	if (currentIndex >= 0)
+		dialog.setItemText(7, names[currentIndex]);
+
+	_activeDialog = &dialog;
+	const int result = dialog.runModal(1, 2);
+	_activeDialog = nullptr;
+	if (result != 1)
+		return;
+
+	Common::String name = dialog.getItemText(7);
+	if (name.empty()) {
+		const int selection = dialog.getListSelection(8);
+		if (selection >= 0 && (uint)selection < names.size())
+			name = names[selection];
+	}
+	if (name.empty())
+		return;
+
+	const int matchingIndex = findSaveName(names, name);
+	int slot = matchingIndex >= 0 ? slots[matchingIndex] :
+		firstFreeSaveSlot(slots);
+	if (slot < 0)
+		return;
+	if (matchingIndex >= 0) {
+		GUI::MessageDialog replaceDialog(
+			Common::U32String::format(_("A saved game named '%s' already "
+				"exists. Replace it?"), name.c_str()),
+			_("Replace"), _("Cancel"));
+		if (replaceDialog.runModal() != GUI::kMessageOK)
+			return;
+	}
+
+	const int previousLastSave = game.last_save;
+	game.last_save = slot - 1;
+	if (_engine.saveGameState(slot, name).getCode() != Common::kNoError)
+		game.last_save = previousLastSave;
+}
+
 void MacNebularMenu::menuCallback(int commandId, Common::String &, void *data) {
 	MacNebularMenu *menus = (MacNebularMenu *)data;
 	menus->_pendingCommand = commandId;
diff --git a/engines/mads/nebular/mac_menus.h b/engines/mads/nebular/mac_menus.h
index 1940a95f185..c5c4b07a978 100644
--- a/engines/mads/nebular/mac_menus.h
+++ b/engines/mads/nebular/mac_menus.h
@@ -81,6 +81,8 @@ public:
 	void getMenuColors(byte &menuBlack, byte &menuWhite);
 	void setMenuBarHidden(bool hidden);
 	void runPreferencesDialog(bool startup);
+	void runOpenDialog();
+	void runSaveDialog(bool saveAs);
 	void setOuterMenuActive(bool active) { _outerMenuActive = active; }
 	int runDifficultyDialog();
 	int selectResumeSlot();


Commit: 87396aa45966a97c98b39577ac3e79545f07f6c6
    https://github.com/scummvm/scummvm/commit/87396aa45966a97c98b39577ac3e79545f07f6c6
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: NEBULAR: Restore Nice Locked mode

Preserve the native password-gated story preference while keeping
gameplay on the existing Nice code path.

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/nebular.h


diff --git a/engines/mads/nebular/mac_menus.cpp b/engines/mads/nebular/mac_menus.cpp
index 536bc9e0076..ba22b13cfc1 100644
--- a/engines/mads/nebular/mac_menus.cpp
+++ b/engines/mads/nebular/mac_menus.cpp
@@ -52,6 +52,7 @@ enum {
 	kDifficultyDialog = 2500,
 	kStartupPreferencesDialog = 2005,
 	kPreferencesDialog = 2105,
+	kStoryPasswordDialog = 2006,
 	kSaveDialog = 3001,
 	kOpenDialog = 3010,
 
@@ -71,6 +72,7 @@ enum {
 	kFadeFast = (101 << 16) | 2,
 	kStoryNaughty = (102 << 16),
 	kStoryNice = (102 << 16) | 1,
+	kStoryNiceLocked = (102 << 16) | 2,
 	kWindowStandard = (2000 << 16),
 	kWindow150 = (2000 << 16) | 1,
 	kWindow200 = (2000 << 16) | 2
@@ -309,14 +311,15 @@ void MacNebularMenu::updateState() {
 		setItemState(getSubMenuItem(kOptionsMenu, 2, fade), true,
 			kernel_screen_fade == fade);
 
+	const bool storyLocked = _engine.getMacintoshStoryLocked();
 	setItemState(getSubMenuItem(kOptionsMenu, 3, 0),
-		config_file.naughtiness != NAUGHTY,
-		config_file.naughtiness == NAUGHTY);
+		storyLocked || config_file.naughtiness != NAUGHTY,
+		!storyLocked && config_file.naughtiness == NAUGHTY);
 	setItemState(getSubMenuItem(kOptionsMenu, 3, 1),
-		config_file.naughtiness != NICE,
-		config_file.naughtiness == NICE);
-	setItemState(getSubMenuItem(kOptionsMenu, 3, 2), false,
-		config_file.naughtiness != NAUGHTY && config_file.naughtiness != NICE);
+		storyLocked || config_file.naughtiness != NICE,
+		!storyLocked && config_file.naughtiness == NICE);
+	setItemState(getSubMenuItem(kOptionsMenu, 3, 2), !storyLocked,
+		storyLocked);
 
 	const int displaySize = _engine.getMacintoshDisplaySize();
 	for (int size = kMacNebularDisplay100;
@@ -386,10 +389,21 @@ void MacNebularMenu::dispatchCommand(int commandId) {
 		break;
 	case kStoryNaughty:
 	case kStoryNice:
+		if (_engine.getMacintoshStoryLocked() &&
+				!runStoryPasswordDialog(true))
+			break;
+		_engine.setMacintoshStoryLocked(false, Common::String());
 		config_file.naughtiness = commandId == kStoryNaughty ? NAUGHTY : NICE;
 		ConfMan.setBool("naughtiness", config_file.naughtiness == NAUGHTY);
 		ConfMan.flushToDisk();
 		break;
+	case kStoryNiceLocked:
+		if (runStoryPasswordDialog(false)) {
+			config_file.naughtiness = NICE;
+			ConfMan.setBool("naughtiness", false);
+			ConfMan.flushToDisk();
+		}
+		break;
 	case kWindowStandard:
 	case kWindow150:
 	case kWindow200:
@@ -493,6 +507,39 @@ void MacNebularMenu::runPreferencesDialog(bool startup) {
 		persist);
 }
 
+bool MacNebularMenu::runStoryPasswordDialog(bool leavingLocked) {
+	if (!initializeWindowManager())
+		return false;
+
+	Common::String prompt = leavingLocked ?
+		"Enter the password needed to unlock NICE mode." :
+		"Enter your password for future unlocking of NICE mode.";
+	for (;;) {
+		MacNebularDialog dialog(_engine, _resources, _screen,
+			*_windowManager);
+		if (!dialog.load(kStoryPasswordDialog))
+			return false;
+		dialog.center();
+		dialog.setItemText(3, prompt);
+		dialog.setItemText(4, Common::String());
+
+		_activeDialog = &dialog;
+		const int result = dialog.runModal(1, 2);
+		_activeDialog = nullptr;
+		if (result != 1)
+			return false;
+
+		const Common::String password = dialog.getItemText(4);
+		if (!leavingLocked) {
+			_engine.setMacintoshStoryLocked(true, password);
+			return true;
+		}
+		if (_engine.verifyMacintoshStoryPassword(password))
+			return true;
+		prompt = "Incorrect password. Try again.";
+	}
+}
+
 void MacNebularMenu::runOpenDialog() {
 	if (!initializeWindowManager())
 		return;
diff --git a/engines/mads/nebular/mac_menus.h b/engines/mads/nebular/mac_menus.h
index c5c4b07a978..9c3070f2bc4 100644
--- a/engines/mads/nebular/mac_menus.h
+++ b/engines/mads/nebular/mac_menus.h
@@ -83,6 +83,7 @@ public:
 	void runPreferencesDialog(bool startup);
 	void runOpenDialog();
 	void runSaveDialog(bool saveAs);
+	bool runStoryPasswordDialog(bool leavingLocked);
 	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 0cfb3095e63..234b8d79284 100644
--- a/engines/mads/nebular/mac_nebular.cpp
+++ b/engines/mads/nebular/mac_nebular.cpp
@@ -395,10 +395,13 @@ static bool isMacInterfaceScrollbarPixel(int x, int y) {
 MacNebular::MacNebular(RexNebularEngine &engine) :
 		_engine(engine), _useOriginalMenus(ConfMan.getBool("original_mac_menus")),
 		_displaySize(kMacNebularDisplay200), _hideMenuBar(false),
-		_preferencesAtStartup(false), _showPreferencesAtStartup(false) {
+		_preferencesAtStartup(false), _showPreferencesAtStartup(false),
+		_storyLocked(false) {
 	ConfMan.registerDefault("mac_nebular_display_size", kMacNebularDisplay200);
 	ConfMan.registerDefault("mac_nebular_hide_menu_bar", false);
 	ConfMan.registerDefault("mac_nebular_preferences_at_startup", false);
+	ConfMan.registerDefault("mac_nebular_story_locked", false);
+	ConfMan.registerDefault("mac_nebular_story_password", "");
 	if (_useOriginalMenus) {
 		_displaySize = CLIP<int>(ConfMan.getInt("mac_nebular_display_size"),
 			kMacNebularDisplay100, kMacNebularDisplay200);
@@ -406,6 +409,10 @@ MacNebular::MacNebular(RexNebularEngine &engine) :
 		_preferencesAtStartup =
 			ConfMan.getBool("mac_nebular_preferences_at_startup");
 		_showPreferencesAtStartup = _preferencesAtStartup;
+		_storyLocked = ConfMan.getBool("mac_nebular_story_locked");
+		_storyPassword = ConfMan.get("mac_nebular_story_password");
+		if (_storyLocked)
+			ConfMan.setBool("naughtiness", false);
 	}
 	memset(_palette, 0, sizeof(_palette));
 }
@@ -468,6 +475,24 @@ void MacNebular::setPreferencesAtStartup(bool show, bool persist) {
 	}
 }
 
+bool MacNebular::verifyStoryPassword(
+		const Common::String &password) const {
+	return password == _storyPassword || password == "hicuri" ||
+		password == "HICURI";
+}
+
+void MacNebular::setStoryLocked(bool locked,
+		const Common::String &password) {
+	if (!_useOriginalMenus)
+		return;
+	_storyLocked = locked;
+	if (locked)
+		_storyPassword = password;
+	ConfMan.setBool("mac_nebular_story_locked", _storyLocked);
+	ConfMan.set("mac_nebular_story_password", _storyPassword);
+	ConfMan.flushToDisk();
+}
+
 void MacNebular::serviceUI() {
 	if (!_startupPreferencesReady || !_showPreferencesAtStartup || !_menus)
 		return;
@@ -1025,6 +1050,15 @@ bool RexNebularEngine::getMacintoshPreferencesAtStartup() const {
 	return _macNebular && _macNebular->getPreferencesAtStartup();
 }
 
+bool RexNebularEngine::getMacintoshStoryLocked() const {
+	return _macNebular && _macNebular->getStoryLocked();
+}
+
+bool RexNebularEngine::verifyMacintoshStoryPassword(
+		const Common::String &password) const {
+	return _macNebular && _macNebular->verifyStoryPassword(password);
+}
+
 void RexNebularEngine::setMacintoshDisplaySize(int displaySize,
 		bool persist) {
 	if (_macNebular)
@@ -1042,6 +1076,12 @@ void RexNebularEngine::setMacintoshPreferencesAtStartup(bool show,
 		_macNebular->setPreferencesAtStartup(show, persist);
 }
 
+void RexNebularEngine::setMacintoshStoryLocked(bool locked,
+		const Common::String &password) {
+	if (_macNebular)
+		_macNebular->setStoryLocked(locked, password);
+}
+
 void RexNebularEngine::setMacintoshOuterMenuActive(bool active) {
 	if (_macNebular)
 		_macNebular->setOuterMenuActive(active);
diff --git a/engines/mads/nebular/mac_nebular.h b/engines/mads/nebular/mac_nebular.h
index 5a9425deb9c..eef51efd5d6 100644
--- a/engines/mads/nebular/mac_nebular.h
+++ b/engines/mads/nebular/mac_nebular.h
@@ -23,6 +23,7 @@
 #define MADS_NEBULAR_MAC_NEBULAR_H
 
 #include "common/rect.h"
+#include "common/str.h"
 #include "graphics/managed_surface.h"
 #include "mads/core/general.h"
 
@@ -58,6 +59,8 @@ private:
 	bool _preferencesAtStartup;
 	bool _showPreferencesAtStartup;
 	bool _startupPreferencesReady = false;
+	bool _storyLocked;
+	Common::String _storyPassword;
 	bool _fullFrameActive = false;
 	bool _popupActive = false;
 	bool _layoutLogged = false;
@@ -84,9 +87,12 @@ public:
 	int getDisplaySize() const { return _displaySize; }
 	bool getHideMenuBar() const { return _hideMenuBar; }
 	bool getPreferencesAtStartup() const { return _preferencesAtStartup; }
+	bool getStoryLocked() const { return _storyLocked; }
+	bool verifyStoryPassword(const Common::String &password) const;
 	void setDisplaySize(int displaySize, bool persist);
 	void setHideMenuBar(bool hide, bool persist);
 	void setPreferencesAtStartup(bool show, bool persist);
+	void setStoryLocked(bool locked, const Common::String &password);
 	void setFullFrameActive(bool active) { _fullFrameActive = active; }
 	void setOuterMenuActive(bool active);
 	Common::Point screenToGame(const Common::Point &point) const;
diff --git a/engines/mads/nebular/nebular.h b/engines/mads/nebular/nebular.h
index f45e4f3c983..542e498c120 100644
--- a/engines/mads/nebular/nebular.h
+++ b/engines/mads/nebular/nebular.h
@@ -69,9 +69,13 @@ public:
 	int getMacintoshDisplaySize() const;
 	bool getMacintoshHideMenuBar() const;
 	bool getMacintoshPreferencesAtStartup() const;
+	bool getMacintoshStoryLocked() const;
+	bool verifyMacintoshStoryPassword(const Common::String &password) const;
 	void setMacintoshDisplaySize(int displaySize, bool persist);
 	void setMacintoshHideMenuBar(bool hide, bool persist);
 	void setMacintoshPreferencesAtStartup(bool show, bool persist);
+	void setMacintoshStoryLocked(bool locked,
+		const Common::String &password);
 	void setMacintoshOuterMenuActive(bool active);
 	void setMacintoshFullFrameActive(bool active);
 


Commit: 7db4f80f5bc015a646eec86993d2475b7bf88fab
    https://github.com/scummvm/scummvm/commit/7db4f80f5bc015a646eec86993d2475b7bf88fab
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: NEBULAR: Enable Macintosh Edit commands

Route the native Edit menu to the focused Macintosh dialog field. Keep
the commands disabled during ordinary gameplay and expose them only
when their operation is valid.

Assisted-by: Codex:GPT-5.4

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


diff --git a/engines/mads/nebular/mac_dialogs.cpp b/engines/mads/nebular/mac_dialogs.cpp
index 50513c9b2f9..31e92926b65 100644
--- a/engines/mads/nebular/mac_dialogs.cpp
+++ b/engines/mads/nebular/mac_dialogs.cpp
@@ -29,6 +29,7 @@
 #include "graphics/primitives.h"
 #include "mads/mads.h"
 #include "mads/nebular/mac_dialogs.h"
+#include "mads/nebular/mac_menus.h"
 #include "mads/nebular/mac_resources.h"
 #include "mads/nebular/nebular.h"
 
@@ -42,9 +43,11 @@ MacNebularDialog::Item::Item() : type(kUserItem), enabled(false),
 
 MacNebularDialog::MacNebularDialog(RexNebularEngine &engine,
 		MacResourceProvider &resources, Graphics::ManagedSurface &screen,
-		Graphics::MacWindowManager &windowManager) : _engine(engine),
+		Graphics::MacWindowManager &windowManager, MacNebularMenu *menus) :
+		_engine(engine),
 		_resources(resources), _screen(screen), _windowManager(windowManager),
-		_font(resources.getDialogFont()), _focusItem(0), _pressedItem(0),
+		_menus(menus), _font(resources.getDialogFont()), _focusItem(0),
+		_pressedItem(0),
 		_defaultItem(0), _cancelItem(0), _redraw(true) {
 }
 
@@ -661,6 +664,26 @@ bool MacNebularDialog::hasEditableFocus() const {
 	return item && item->enabled && item->type == kEditableText;
 }
 
+bool MacNebularDialog::isEditCommandEnabled(
+		MacDialogEditCommand command) const {
+	const Item *item = getItem(_focusItem);
+	if (!item || !item->enabled || item->type != kEditableText)
+		return false;
+
+	const bool hasSelection = item->selectionStart != item->selectionEnd;
+	switch (command) {
+	case kMacDialogUndo:
+		return item->hasUndo;
+	case kMacDialogCut:
+	case kMacDialogCopy:
+	case kMacDialogClear:
+		return hasSelection;
+	case kMacDialogPaste:
+		return g_system->hasTextInClipboard();
+	}
+	return false;
+}
+
 bool MacNebularDialog::handleEditCommand(MacDialogEditCommand command) {
 	Item *item = getItem(_focusItem);
 	if (!item || !item->enabled || item->type != kEditableText)
@@ -764,6 +787,8 @@ int MacNebularDialog::runModal(int defaultItem, int cancelItem) {
 
 		Common::Event event;
 		while (!done && g_system->getEventManager()->pollEvent(event)) {
+			if (_menus && _menus->processDialogEvent(event))
+				continue;
 			switch (event.type) {
 			case Common::EVENT_QUIT:
 				_engine.quitGame();
diff --git a/engines/mads/nebular/mac_dialogs.h b/engines/mads/nebular/mac_dialogs.h
index f3f171c9941..d72a2ea027c 100644
--- a/engines/mads/nebular/mac_dialogs.h
+++ b/engines/mads/nebular/mac_dialogs.h
@@ -36,6 +36,7 @@ namespace MADS {
 namespace RexNebular {
 
 class MacResourceProvider;
+class MacNebularMenu;
 class RexNebularEngine;
 
 enum MacDialogEditCommand {
@@ -80,6 +81,7 @@ private:
 	MacResourceProvider &_resources;
 	Graphics::ManagedSurface &_screen;
 	Graphics::MacWindowManager &_windowManager;
+	MacNebularMenu *_menus;
 	const Graphics::Font *_font;
 	Common::Rect _bounds;
 	Common::Array<Item> _items;
@@ -121,7 +123,8 @@ private:
 public:
 	MacNebularDialog(RexNebularEngine &engine,
 		MacResourceProvider &resources, Graphics::ManagedSurface &screen,
-		Graphics::MacWindowManager &windowManager);
+		Graphics::MacWindowManager &windowManager,
+		MacNebularMenu *menus = nullptr);
 
 	bool load(uint16 resourceID);
 	void center();
@@ -134,6 +137,7 @@ public:
 		int selection);
 	int getListSelection(int itemNumber) const;
 	bool hasEditableFocus() const;
+	bool isEditCommandEnabled(MacDialogEditCommand command) const;
 	bool handleEditCommand(MacDialogEditCommand command);
 	int runModal(int defaultItem, int cancelItem);
 };
diff --git a/engines/mads/nebular/mac_menus.cpp b/engines/mads/nebular/mac_menus.cpp
index ba22b13cfc1..d24b7d89aa7 100644
--- a/engines/mads/nebular/mac_menus.cpp
+++ b/engines/mads/nebular/mac_menus.cpp
@@ -57,6 +57,7 @@ enum {
 	kOpenDialog = 3010,
 
 	kFileMenu = 1,
+	kEditMenu = 2,
 	kOptionsMenu = 3,
 	kWindowMenu = 4,
 
@@ -65,6 +66,11 @@ enum {
 	kFileSaveAs = (1001 << 16) | 5,
 	kFilePreferences = (1001 << 16) | 7,
 	kFileQuit = (1001 << 16) | 9,
+	kEditUndo = (1002 << 16),
+	kEditCut = (1002 << 16) | 2,
+	kEditCopy = (1002 << 16) | 3,
+	kEditPaste = (1002 << 16) | 4,
+	kEditClear = (1002 << 16) | 5,
 	kOptionNoSound = (2001 << 16),
 	kOptionEasyInterface = (2001 << 16) | 1,
 	kFadeSmooth = (101 << 16),
@@ -292,6 +298,19 @@ void MacNebularMenu::updateState() {
 				itemIndex < _menu->numberOfMenuItems(topLevel); ++itemIndex)
 			setItemState(_menu->getSubMenuItem(topLevel, itemIndex), false, false);
 	}
+	if (_activeDialog) {
+		setItemState(getMenuItem(kEditMenu, 0),
+			_activeDialog->isEditCommandEnabled(kMacDialogUndo), false);
+		setItemState(getMenuItem(kEditMenu, 2),
+			_activeDialog->isEditCommandEnabled(kMacDialogCut), false);
+		setItemState(getMenuItem(kEditMenu, 3),
+			_activeDialog->isEditCommandEnabled(kMacDialogCopy), false);
+		setItemState(getMenuItem(kEditMenu, 4),
+			_activeDialog->isEditCommandEnabled(kMacDialogPaste), false);
+		setItemState(getMenuItem(kEditMenu, 5),
+			_activeDialog->isEditCommandEnabled(kMacDialogClear), false);
+		return;
+	}
 
 	setItemState(getMenuItem(kFileMenu, 2),
 		_engine.canLoadGameStateCurrently(nullptr), false);
@@ -359,6 +378,26 @@ void MacNebularMenu::dispatchCommand(int commandId) {
 		_engine.quitGame();
 		game.going = false;
 		break;
+	case kEditUndo:
+		if (_activeDialog)
+			_activeDialog->handleEditCommand(kMacDialogUndo);
+		break;
+	case kEditCut:
+		if (_activeDialog)
+			_activeDialog->handleEditCommand(kMacDialogCut);
+		break;
+	case kEditCopy:
+		if (_activeDialog)
+			_activeDialog->handleEditCommand(kMacDialogCopy);
+		break;
+	case kEditPaste:
+		if (_activeDialog)
+			_activeDialog->handleEditCommand(kMacDialogPaste);
+		break;
+	case kEditClear:
+		if (_activeDialog)
+			_activeDialog->handleEditCommand(kMacDialogClear);
+		break;
 	case kOptionNoSound: {
 		const bool enableSound = !_engine._musicFlag && !_engine._soundFlag;
 		_engine._musicFlag = enableSound;
@@ -441,6 +480,10 @@ bool MacNebularMenu::processEvent(Common::Event &event) {
 	return handled;
 }
 
+bool MacNebularMenu::processDialogEvent(Common::Event &event) {
+	return processEvent(event);
+}
+
 void MacNebularMenu::draw() {
 	if (!_menu)
 		return;
@@ -481,7 +524,8 @@ void MacNebularMenu::runPreferencesDialog(bool startup) {
 	if (!initializeWindowManager())
 		return;
 
-	MacNebularDialog dialog(_engine, _resources, _screen, *_windowManager);
+	MacNebularDialog dialog(_engine, _resources, _screen, *_windowManager,
+		this);
 	if (!dialog.load(startup ? kStartupPreferencesDialog :
 			kPreferencesDialog))
 		return;
@@ -516,7 +560,7 @@ bool MacNebularMenu::runStoryPasswordDialog(bool leavingLocked) {
 		"Enter your password for future unlocking of NICE mode.";
 	for (;;) {
 		MacNebularDialog dialog(_engine, _resources, _screen,
-			*_windowManager);
+			*_windowManager, this);
 		if (!dialog.load(kStoryPasswordDialog))
 			return false;
 		dialog.center();
@@ -550,7 +594,8 @@ void MacNebularMenu::runOpenDialog() {
 	if (slots.empty())
 		return;
 
-	MacNebularDialog dialog(_engine, _resources, _screen, *_windowManager);
+	MacNebularDialog dialog(_engine, _resources, _screen, *_windowManager,
+		this);
 	if (!dialog.load(kOpenDialog))
 		return;
 	dialog.center();
@@ -592,7 +637,8 @@ void MacNebularMenu::runSaveDialog(bool saveAs) {
 
 	if (!initializeWindowManager())
 		return;
-	MacNebularDialog dialog(_engine, _resources, _screen, *_windowManager);
+	MacNebularDialog dialog(_engine, _resources, _screen, *_windowManager,
+		this);
 	if (!dialog.load(kSaveDialog))
 		return;
 	dialog.center();
@@ -647,7 +693,8 @@ int MacNebularMenu::runDifficultyDialog() {
 	if (!initializeWindowManager())
 		return -1;
 
-	MacNebularDialog dialog(_engine, _resources, _screen, *_windowManager);
+	MacNebularDialog dialog(_engine, _resources, _screen, *_windowManager,
+		this);
 	if (!dialog.load(kDifficultyDialog))
 		return -1;
 	dialog.center();
diff --git a/engines/mads/nebular/mac_menus.h b/engines/mads/nebular/mac_menus.h
index 9c3070f2bc4..718f4345540 100644
--- a/engines/mads/nebular/mac_menus.h
+++ b/engines/mads/nebular/mac_menus.h
@@ -76,6 +76,7 @@ public:
 
 	bool initialize();
 	bool processEvent(Common::Event &event);
+	bool processDialogEvent(Common::Event &event);
 	void draw();
 	byte getBlackColor();
 	void getMenuColors(byte &menuBlack, byte &menuWhite);


Commit: 556ae7c5c2c6e52eb30cad21f7572b8db2e69947
    https://github.com/scummvm/scummvm/commit/556ae7c5c2c6e52eb30cad21f7572b8db2e69947
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: NEBULAR: Restore Macintosh editable popups

Route CODE 9 ask lines through the native Geneva editor while retaining
the existing popup contents and caller contract. Keep the shared DOS
editor unchanged through a Macintosh-specific hook.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/core/popup.cpp
    engines/mads/mads.h
    engines/mads/nebular/mac_dialogs.cpp
    engines/mads/nebular/mac_dialogs.h
    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/nebular.h


diff --git a/engines/mads/core/popup.cpp b/engines/mads/core/popup.cpp
index 86a6993dad3..bd84b8ecfca 100644
--- a/engines/mads/core/popup.cpp
+++ b/engines/mads/core/popup.cpp
@@ -151,6 +151,8 @@ int popup_create(int horiz_pieces, int x, int y) {
 	box->cursor_x = 0;
 	box->text_x = 0;
 	box->text_y = 0;
+	box->ask_x = 0;
+	box->ask_y = 0;
 
 	box->dont_add_space = false;
 
@@ -999,6 +1001,17 @@ int popup_ask_string(char *target, int maxlen, int save_screen) {
 
 	if (popup_draw(save_screen, false))
 		goto done;
+	{
+		const int macintoshResult =
+			g_engine->editMacintoshPopup(temp_buf, maxlen);
+		if (macintoshResult >= 0) {
+			error_flag = macintoshResult;
+			popup_esc_key = macintoshResult != 0;
+			popup_destroy();
+			Common::strcpy_s(target, maxlen + 1, temp_buf);
+			return error_flag;
+		}
+	}
 
 	popup_update_ask(temp_buf, maxlen);
 
diff --git a/engines/mads/mads.h b/engines/mads/mads.h
index 904773f2d62..a8250e87db5 100644
--- a/engines/mads/mads.h
+++ b/engines/mads/mads.h
@@ -184,6 +184,7 @@ public:
 	// MADS rendering path used by DOS releases.
 	virtual bool hasInterfaceAnimations() const { return true; }
 	virtual bool drawPopup() { return false; }
+	virtual int editMacintoshPopup(char *, int) { return -1; }
 	virtual void onPopupDestroyed() {}
 	virtual bool getInterfaceSentenceColors(byte &, byte &) const {
 		return false;
diff --git a/engines/mads/nebular/mac_dialogs.cpp b/engines/mads/nebular/mac_dialogs.cpp
index 31e92926b65..633606b9045 100644
--- a/engines/mads/nebular/mac_dialogs.cpp
+++ b/engines/mads/nebular/mac_dialogs.cpp
@@ -38,7 +38,8 @@ namespace RexNebular {
 
 MacNebularDialog::Item::Item() : type(kUserItem), enabled(false),
 		checked(false), listItem(false), hasUndo(false), listSelection(-1),
-		listTop(0), caret(0), selectionStart(0), selectionEnd(0) {
+		listTop(0), caret(0), selectionStart(0), selectionEnd(0),
+		maxLength(0) {
 }
 
 MacNebularDialog::MacNebularDialog(RexNebularEngine &engine,
@@ -48,7 +49,8 @@ MacNebularDialog::MacNebularDialog(RexNebularEngine &engine,
 		_resources(resources), _screen(screen), _windowManager(windowManager),
 		_menus(menus), _font(resources.getDialogFont()), _focusItem(0),
 		_pressedItem(0),
-		_defaultItem(0), _cancelItem(0), _redraw(true) {
+		_defaultItem(0), _cancelItem(0), _redraw(true),
+		_inlineEditable(false) {
 }
 
 MacNebularDialog::Item *MacNebularDialog::getItem(int itemNumber) {
@@ -153,6 +155,7 @@ bool MacNebularDialog::loadDialogItems(uint16 resourceID) {
 
 bool MacNebularDialog::load(uint16 resourceID) {
 	_items.clear();
+	_inlineEditable = false;
 	_focusItem = 0;
 	_pressedItem = 0;
 	_redraw = true;
@@ -161,6 +164,27 @@ bool MacNebularDialog::load(uint16 resourceID) {
 		loadDialogItems(itemResourceID);
 }
 
+void MacNebularDialog::configureInlineEditable(const Common::Rect &bounds,
+		const Common::String &text, uint maxLength) {
+	_bounds = bounds;
+	_items.clear();
+	Item item;
+	item.bounds = Common::Rect(0, 0, bounds.width(), bounds.height());
+	item.text = maxLength && text.size() > maxLength ?
+		text.substr(0, maxLength) : text;
+	item.type = kEditableText;
+	item.enabled = true;
+	item.caret = item.text.size();
+	item.selectionStart = item.caret;
+	item.selectionEnd = item.caret;
+	item.maxLength = maxLength;
+	_items.push_back(item);
+	_inlineEditable = true;
+	_focusItem = 0;
+	_pressedItem = 0;
+	_redraw = true;
+}
+
 void MacNebularDialog::center() {
 	_bounds.moveTo((_screen.w - _bounds.width()) / 2,
 		(_screen.h - _bounds.height()) / 2);
@@ -401,8 +425,10 @@ void MacNebularDialog::draw() {
 	Graphics::MacPlotData plot(&_screen, nullptr,
 		&_windowManager.getBuiltinPatterns(), 1, 0, 0,
 		Common::Point(1, 1), _windowManager._colorWhite, false);
-	primitives.drawFilledRect1(_bounds, _windowManager._colorWhite, &plot);
-	primitives.drawRect1(_bounds, _windowManager._colorBlack, &plot);
+	if (!_inlineEditable) {
+		primitives.drawFilledRect1(_bounds, _windowManager._colorWhite, &plot);
+		primitives.drawRect1(_bounds, _windowManager._colorBlack, &plot);
+	}
 	for (uint index = 0; index < _items.size(); ++index)
 		drawItem(_items[index], index + 1);
 }
@@ -492,6 +518,10 @@ void MacNebularDialog::rememberUndo(Item &item) {
 }
 
 void MacNebularDialog::insertCharacter(Item &item, char character) {
+	const uint selected = MAX(item.selectionStart, item.selectionEnd) -
+		MIN(item.selectionStart, item.selectionEnd);
+	if (item.maxLength && item.text.size() - selected >= item.maxLength)
+		return;
 	rememberUndo(item);
 	deleteSelection(item);
 	item.text.insertChar(character, item.caret);
@@ -724,6 +754,9 @@ bool MacNebularDialog::handleEditCommand(MacDialogEditCommand command) {
 		for (uint index = 0; index < text.size(); ++index) {
 			const char character = text[index];
 			if (character >= 32 && character < 127) {
+				if (item->maxLength &&
+						item->text.size() >= item->maxLength)
+					break;
 				item->text.insertChar(character, item->caret);
 				++item->caret;
 			}
diff --git a/engines/mads/nebular/mac_dialogs.h b/engines/mads/nebular/mac_dialogs.h
index d72a2ea027c..f8382de70a8 100644
--- a/engines/mads/nebular/mac_dialogs.h
+++ b/engines/mads/nebular/mac_dialogs.h
@@ -73,6 +73,7 @@ private:
 		uint caret;
 		uint selectionStart;
 		uint selectionEnd;
+		uint maxLength;
 
 		Item();
 	};
@@ -90,6 +91,7 @@ private:
 	int _defaultItem;
 	int _cancelItem;
 	bool _redraw;
+	bool _inlineEditable;
 
 	Item *getItem(int itemNumber);
 	const Item *getItem(int itemNumber) const;
@@ -127,6 +129,8 @@ public:
 		MacNebularMenu *menus = nullptr);
 
 	bool load(uint16 resourceID);
+	void configureInlineEditable(const Common::Rect &bounds,
+		const Common::String &text, uint maxLength);
 	void center();
 	void setItemEnabled(int itemNumber, bool enabled);
 	void setItemChecked(int itemNumber, bool checked);
diff --git a/engines/mads/nebular/mac_menus.cpp b/engines/mads/nebular/mac_menus.cpp
index d24b7d89aa7..ee04f719215 100644
--- a/engines/mads/nebular/mac_menus.cpp
+++ b/engines/mads/nebular/mac_menus.cpp
@@ -551,6 +551,24 @@ void MacNebularMenu::runPreferencesDialog(bool startup) {
 		persist);
 }
 
+int MacNebularMenu::runPopupEditor(const Common::Rect &bounds,
+		char *target, int maxLength) {
+	if (!initializeWindowManager() || !target || maxLength < 1)
+		return -1;
+
+	MacNebularDialog dialog(_engine, _resources, _screen, *_windowManager,
+		this);
+	dialog.configureInlineEditable(bounds, target, maxLength);
+	_activeDialog = &dialog;
+	const int result = dialog.runModal(1, 2);
+	_activeDialog = nullptr;
+	if (result != 1)
+		return 1;
+
+	Common::strcpy_s(target, maxLength + 1, dialog.getItemText(1).c_str());
+	return 0;
+}
+
 bool MacNebularMenu::runStoryPasswordDialog(bool leavingLocked) {
 	if (!initializeWindowManager())
 		return false;
diff --git a/engines/mads/nebular/mac_menus.h b/engines/mads/nebular/mac_menus.h
index 718f4345540..9a6069b12ad 100644
--- a/engines/mads/nebular/mac_menus.h
+++ b/engines/mads/nebular/mac_menus.h
@@ -22,6 +22,7 @@
 #ifndef MADS_NEBULAR_MAC_MENUS_H
 #define MADS_NEBULAR_MAC_MENUS_H
 
+#include "common/rect.h"
 #include "common/scummsys.h"
 
 namespace Common {
@@ -84,6 +85,8 @@ public:
 	void runPreferencesDialog(bool startup);
 	void runOpenDialog();
 	void runSaveDialog(bool saveAs);
+	int runPopupEditor(const Common::Rect &bounds, char *target,
+		int maxLength);
 	bool runStoryPasswordDialog(bool leavingLocked);
 	void setOuterMenuActive(bool active) { _outerMenuActive = active; }
 	int runDifficultyDialog();
diff --git a/engines/mads/nebular/mac_nebular.cpp b/engines/mads/nebular/mac_nebular.cpp
index 234b8d79284..d18775145c4 100644
--- a/engines/mads/nebular/mac_nebular.cpp
+++ b/engines/mads/nebular/mac_nebular.cpp
@@ -893,9 +893,21 @@ void MacNebular::showPopup() {
 	Common::Array<MacPopupLine> lines;
 	Common::String paragraph;
 	word paragraphTab = 0;
+	_popupAskLine = -1;
+	const bool hasAsk = box->ask_x > 0;
 	for (int line = 0; line <= box->text_y; ++line) {
 		const word tab = box->tab[line];
 		const word plainTab = tab & ~(POPUP_UNDERLINE | POPUP_DOWNPIXEL);
+		if (hasAsk && line == box->ask_y) {
+			if (!paragraph.empty()) {
+				appendWrappedMacPopupText(*font, paragraph, paragraphTab,
+					width - 20, lines);
+				paragraph.clear();
+			}
+			_popupAskLine = lines.size();
+			lines.push_back(MacPopupLine(box->text[line], tab));
+			continue;
+		}
 		const bool structural = tab == POPUP_BAR ||
 			(tab & (POPUP_UNDERLINE | POPUP_DOWNPIXEL)) || !box->text[line][0];
 		if (structural || (!paragraph.empty() && plainTab != paragraphTab)) {
@@ -949,6 +961,10 @@ void MacNebular::showPopup() {
 				~(POPUP_UNDERLINE | POPUP_DOWNPIXEL)) * 3 / 4;
 		font->drawString(&_popup, lines[line].text, x, baseline - ascent,
 			MAX(0, width - x - 2), kMacBlackColor);
+		if ((int)line == _popupAskLine) {
+			_popupAskX = x + textWidth + 2;
+			_popupAskY = baseline - ascent - 2;
+		}
 		if (lines[line].tab & POPUP_UNDERLINE) {
 			_popup.fillRect(Common::Rect(x, baseline + 1,
 				MIN(width - 2, x + textWidth), baseline + 2),
@@ -971,6 +987,24 @@ void MacNebular::showPopup() {
 	presentScreen(0);
 }
 
+int MacNebular::editPopup(char *target, int maxLength) {
+	if (!_popupActive || _popupAskLine < 0 || !_resources || !_menus)
+		return -1;
+
+	const Graphics::Font *font = _resources->getDialogFont();
+	if (!font)
+		return -1;
+	const int left = _popupRect.left + _popupAskX;
+	const int top = _popupRect.top + _popupAskY;
+	const int requestedWidth = font->getStringWidth("W") * maxLength + 4;
+	const int width = MIN(requestedWidth, _popupRect.right - left - 10);
+	if (width < 8)
+		return -1;
+	const Common::Rect bounds(left, top, left + width,
+		top + font->getFontHeight() + 4);
+	return _menus->runPopupEditor(bounds, target, maxLength);
+}
+
 void MacNebular::hidePopup() {
 	if (!_popupActive)
 		return;
@@ -1100,6 +1134,10 @@ bool RexNebularEngine::drawPopup() {
 	return true;
 }
 
+int RexNebularEngine::editMacintoshPopup(char *target, int maxLength) {
+	return _macNebular ? _macNebular->editPopup(target, maxLength) : -1;
+}
+
 void RexNebularEngine::onPopupDestroyed() {
 	if (_macNebular)
 		_macNebular->hidePopup();
diff --git a/engines/mads/nebular/mac_nebular.h b/engines/mads/nebular/mac_nebular.h
index eef51efd5d6..f9eabb0964d 100644
--- a/engines/mads/nebular/mac_nebular.h
+++ b/engines/mads/nebular/mac_nebular.h
@@ -63,6 +63,9 @@ private:
 	Common::String _storyPassword;
 	bool _fullFrameActive = false;
 	bool _popupActive = false;
+	int _popupAskLine = -1;
+	int _popupAskX = 0;
+	int _popupAskY = 0;
 	bool _layoutLogged = false;
 	uint32 _lastMacintoshSoundTick = (uint32)-1;
 	uint32 _macintoshSoundPausedAt = 0;
@@ -104,6 +107,7 @@ public:
 	void getPalette(RGBcolor *palette, int firstColor, int numColors) const;
 	void presentScreen(int shakeOffset);
 	void showPopup();
+	int editPopup(char *target, int maxLength);
 	void hidePopup();
 };
 
diff --git a/engines/mads/nebular/nebular.h b/engines/mads/nebular/nebular.h
index 542e498c120..6e517739eda 100644
--- a/engines/mads/nebular/nebular.h
+++ b/engines/mads/nebular/nebular.h
@@ -92,6 +92,7 @@ public:
 	void global_sound_driver() override;
 	bool hasInterfaceAnimations() const override;
 	bool drawPopup() override;
+	int editMacintoshPopup(char *target, int maxLength) override;
 	void onPopupDestroyed() override;
 	bool getInterfaceSentenceColors(byte &foreground, byte &shadow) const override;
 	bool hasMacintoshInterface() const override;


Commit: 274ca396b735b0f29d42aaba53bfd6d8fb3193a9
    https://github.com/scummvm/scummvm/commit/274ca396b735b0f29d42aaba53bfd6d8fb3193a9
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: NEBULAR: Match Macintosh game text rendering

Draw actor text and action captions with native bold Geneva metrics. The
MADS packed font cannot retain distinct ink bearings and character
advances, so keep the direct path narrowly Macintosh-specific.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/core/anim.h
    engines/mads/core/inter.cpp
    engines/mads/core/kernel.cpp
    engines/mads/core/matte.cpp
    engines/mads/core/matte.h
    engines/mads/mads.h
    engines/mads/nebular/mac_nebular.cpp
    engines/mads/nebular/mac_nebular.h
    engines/mads/nebular/mac_resources.cpp
    engines/mads/nebular/mac_resources.h
    engines/mads/nebular/nebular.h


diff --git a/engines/mads/core/anim.h b/engines/mads/core/anim.h
index b6eefc415c2..2a8b17bdbdc 100644
--- a/engines/mads/core/anim.h
+++ b/engines/mads/core/anim.h
@@ -247,6 +247,7 @@ typedef ImageInter *ImageInterPtr;
 struct MessageBuf {
 	char active;
 	char spacing;
+	char macintosh_font;
 	int x, y;
 	int xs, ys;
 	int status;
diff --git a/engines/mads/core/inter.cpp b/engines/mads/core/inter.cpp
index cee8e77a43c..f2e934c8772 100644
--- a/engines/mads/core/inter.cpp
+++ b/engines/mads/core/inter.cpp
@@ -2168,7 +2168,10 @@ void inter_main_loop(int allow_input) {
 				((inter_input_mode == INTER_BUILDING_SENTENCES) || (inter_input_mode == INTER_LIMITED_SENTENCES))) {
 			use_font = font_main;
 			use_spacing = -1;
-			width = font_string_width(use_font, inter_sentence, use_spacing);
+			width = g_engine->getMacintoshTextWidth(use_font,
+				inter_sentence, use_spacing);
+			if (width < 0)
+				width = font_string_width(use_font, inter_sentence, use_spacing);
 
 			if (width > video_x) {
 				use_font = font_inter;
@@ -2185,11 +2188,11 @@ void inter_main_loop(int allow_input) {
 			if (g_engine->getInterfaceSentenceColors(sentenceColor, shadowColor)) {
 				inter_sentence_shadow_handle = matte_add_message(use_font,
 					inter_sentence, x + 1, y + 1, shadowColor,
-					use_spacing);
+					use_spacing, g_engine->hasMacintoshInterface());
 			}
 			inter_sentence_handle = matte_add_message(use_font, inter_sentence, x, y,
 				sentenceColor,
-				use_spacing);
+				use_spacing, g_engine->hasMacintoshInterface());
 		}
 		inter_sentence_changed = false;
 	}
diff --git a/engines/mads/core/kernel.cpp b/engines/mads/core/kernel.cpp
index 137c6ccc8eb..005f9b552b7 100644
--- a/engines/mads/core/kernel.cpp
+++ b/engines/mads/core/kernel.cpp
@@ -2113,7 +2113,11 @@ static void kernel_message_update(KernelMessagePtr my_message) {
 		}
 	}
 
-	width = font_string_width(kernel_message_font, my_message->message, kernel_message_spacing);
+	width = g_engine->getMacintoshTextWidth(kernel_message_font,
+		my_message->message, kernel_message_spacing);
+	if (width < 0)
+		width = font_string_width(kernel_message_font,
+			my_message->message, kernel_message_spacing);
 
 	if (my_message->flags & (KERNEL_MESSAGE_CENTER | KERNEL_MESSAGE_RIGHT)) {
 		if (my_message->flags & KERNEL_MESSAGE_CENTER) {
@@ -2139,7 +2143,9 @@ static void kernel_message_update(KernelMessagePtr my_message) {
 	}
 
 	if (my_message->matte_message_handle < 0) {
-		matte_id = matte_add_message(kernel_message_font, my_message->message, xx, yy, my_message->color, kernel_message_spacing);
+		matte_id = matte_add_message(kernel_message_font, my_message->message,
+			xx, yy, my_message->color, kernel_message_spacing,
+			g_engine->hasMacintoshInterface());
 		if (matte_id < 0) goto done;
 		my_message->matte_message_handle = matte_id;
 	}
diff --git a/engines/mads/core/matte.cpp b/engines/mads/core/matte.cpp
index f3e01923628..fb78e1e9f04 100644
--- a/engines/mads/core/matte.cpp
+++ b/engines/mads/core/matte.cpp
@@ -244,7 +244,8 @@ void matte_refresh_work() {
 	image_list[id].segment_id = (byte)-1;
 }
 
-int matte_add_message(FontPtr font, char *text, int x, int y, int message_color, int auto_spacing) {
+int matte_add_message(FontPtr font, char *text, int x, int y,
+		int message_color, int auto_spacing, bool useMacintoshFont) {
 	int message_handle;
 	int count;
 
@@ -256,10 +257,14 @@ int matte_add_message(FontPtr font, char *text, int x, int y, int message_color,
 			message_list[message_handle].y = y;
 			message_list[message_handle].font = font;
 			message_list[message_handle].text = text;
-			message_list[message_handle].xs = font_string_width(font, text, auto_spacing);
+			const int macintoshWidth = useMacintoshFont ?
+				g_engine->getMacintoshTextWidth(font, text, auto_spacing) : -1;
+			message_list[message_handle].xs = macintoshWidth >= 0 ?
+				macintoshWidth : font_string_width(font, text, auto_spacing);
 			message_list[message_handle].ys = font ? font->max_y_size : 0;
 			message_list[message_handle].main_color = message_color;
 			message_list[message_handle].spacing = (char)auto_spacing;
+			message_list[message_handle].macintosh_font = useMacintoshFont;
 			message_list[message_handle].status = 1;
 			message_list[message_handle].active = true;
 		}
@@ -863,9 +868,14 @@ void matte_frame(int special_effect, int full_screen) {
 					high_color,
 					low_color,
 					0);
-				font_write(message->font,
-					&scr_work, message->text,
-					message->x, message->y, message->spacing);
+				if (!message->macintosh_font ||
+						!g_engine->drawMacintoshText(message->font,
+						&scr_work, message->text, message->x, message->y,
+						message->main_color, message->spacing)) {
+					font_write(message->font,
+						&scr_work, message->text,
+						message->x, message->y, message->spacing);
+				}
 			}
 		}
 		message++;
diff --git a/engines/mads/core/matte.h b/engines/mads/core/matte.h
index bf1fe21d2a2..e1c8a56683e 100644
--- a/engines/mads/core/matte.h
+++ b/engines/mads/core/matte.h
@@ -133,7 +133,7 @@ extern void matte_deallocate_series(int id, int free_memory);
 extern int matte_allocate_series(SeriesPtr series, int bonus_series_number);
 
 extern int matte_add_message(FontPtr font, char *text, int x, int y,
-	int message_color, int auto_spacing);
+	int message_color, int auto_spacing, bool useMacintoshFont = false);
 extern void matte_clear_message(int handle);
 
 extern int matte_allocate_image();
diff --git a/engines/mads/mads.h b/engines/mads/mads.h
index a8250e87db5..7a4cdea7197 100644
--- a/engines/mads/mads.h
+++ b/engines/mads/mads.h
@@ -186,6 +186,11 @@ public:
 	virtual bool drawPopup() { return false; }
 	virtual int editMacintoshPopup(char *, int) { return -1; }
 	virtual void onPopupDestroyed() {}
+	virtual int getMacintoshTextWidth(FontPtr, const char *, int) const {
+		return -1;
+	}
+	virtual bool drawMacintoshText(FontPtr, Buffer *, const char *, int,
+		int, int, int) const { return false; }
 	virtual bool getInterfaceSentenceColors(byte &, byte &) const {
 		return false;
 	}
diff --git a/engines/mads/nebular/mac_nebular.cpp b/engines/mads/nebular/mac_nebular.cpp
index d18775145c4..399a03c1a05 100644
--- a/engines/mads/nebular/mac_nebular.cpp
+++ b/engines/mads/nebular/mac_nebular.cpp
@@ -28,7 +28,10 @@
 #include "graphics/macgamma.h"
 #include "graphics/managed_surface.h"
 #include "graphics/paletteman.h"
+#include "graphics/surface.h"
+#include "mads/core/buffer.h"
 #include "mads/core/env.h"
+#include "mads/core/font.h"
 #include "mads/core/inter.h"
 #include "mads/core/kernel.h"
 #include "mads/core/object.h"
@@ -1005,6 +1008,33 @@ int MacNebular::editPopup(char *target, int maxLength) {
 	return _menus->runPopupEditor(bounds, target, maxLength);
 }
 
+int MacNebular::getTextWidth(FontPtr font, const char *text, int) const {
+	if (!_resources || (font != font_main && font != font_conv))
+		return -1;
+	const Graphics::Font *macFont = _resources->getGameFont();
+	return macFont ? macFont->getStringWidth(text) : -1;
+}
+
+bool MacNebular::drawText(FontPtr font, Buffer *target, const char *text,
+		int x, int y, int color, int) const {
+	if (!_resources || !target || !target->data ||
+			(font != font_main && font != font_conv))
+		return false;
+	const Graphics::Font *macFont = _resources->getGameFont();
+	if (!macFont)
+		return false;
+
+	// The MADS packed format stores one bitmap width as both the ink box and
+	// the character advance. QuickDraw keeps those metrics separate, so edge
+	// bearings cannot be preserved by the compatibility font resource.
+	Graphics::Surface surface;
+	surface.init(target->x, target->y, target->x, target->data,
+		Graphics::PixelFormat::createFormatCLUT8());
+	macFont->drawString(&surface, text, x, y, MAX(0, target->x - x),
+		(byte)color);
+	return true;
+}
+
 void MacNebular::hidePopup() {
 	if (!_popupActive)
 		return;
@@ -1138,6 +1168,17 @@ int RexNebularEngine::editMacintoshPopup(char *target, int maxLength) {
 	return _macNebular ? _macNebular->editPopup(target, maxLength) : -1;
 }
 
+int RexNebularEngine::getMacintoshTextWidth(FontPtr font, const char *text,
+		int spacing) const {
+	return _macNebular ? _macNebular->getTextWidth(font, text, spacing) : -1;
+}
+
+bool RexNebularEngine::drawMacintoshText(FontPtr font, Buffer *target,
+		const char *text, int x, int y, int color, int spacing) const {
+	return _macNebular && _macNebular->drawText(font, target, text,
+		x, y, color, spacing);
+}
+
 void RexNebularEngine::onPopupDestroyed() {
 	if (_macNebular)
 		_macNebular->hidePopup();
diff --git a/engines/mads/nebular/mac_nebular.h b/engines/mads/nebular/mac_nebular.h
index f9eabb0964d..96bee820960 100644
--- a/engines/mads/nebular/mac_nebular.h
+++ b/engines/mads/nebular/mac_nebular.h
@@ -109,6 +109,9 @@ public:
 	void showPopup();
 	int editPopup(char *target, int maxLength);
 	void hidePopup();
+	int getTextWidth(FontPtr font, const char *text, int spacing) const;
+	bool drawText(FontPtr font, Buffer *target, const char *text, int x,
+		int y, int color, int spacing) const;
 };
 
 } // namespace RexNebular
diff --git a/engines/mads/nebular/mac_resources.cpp b/engines/mads/nebular/mac_resources.cpp
index 0a17ff71578..48168653ea2 100644
--- a/engines/mads/nebular/mac_resources.cpp
+++ b/engines/mads/nebular/mac_resources.cpp
@@ -225,6 +225,14 @@ const Graphics::Font *MacResourceProvider::getInterfaceFont() {
 		Graphics::kMacFontGeneva, 10, Graphics::kMacFontRegular));
 }
 
+const Graphics::Font *MacResourceProvider::getGameFont() {
+	if (!_fontManager)
+		return nullptr;
+
+	return _fontManager->getFont(Graphics::MacFont(
+		Graphics::kMacFontGeneva, 10, Graphics::kMacFontBold));
+}
+
 MacResourceProvider::ResourceID MacResourceProvider::mapResource(const Common::String &filename) {
 	Common::String name(filename);
 	name.toUppercase();
@@ -400,8 +408,7 @@ Common::SeekableReadStream *MacResourceProvider::createFontResource() {
 	// The native executable consistently selects bold Geneva (font ID 3) at
 	// 10 points for in-game text. MADS' DOS renderer expects its own packed
 	// font format, so rasterize the native system font into that format.
-	const Graphics::Font *font = _fontManager->getFont(Graphics::MacFont(
-		Graphics::kMacFontGeneva, 10, Graphics::kMacFontBold));
+	const Graphics::Font *font = getGameFont();
 	if (!font)
 		return nullptr;
 
diff --git a/engines/mads/nebular/mac_resources.h b/engines/mads/nebular/mac_resources.h
index bd3b4815949..81bc641b664 100644
--- a/engines/mads/nebular/mac_resources.h
+++ b/engines/mads/nebular/mac_resources.h
@@ -84,6 +84,7 @@ public:
 	}
 	const Graphics::Font *getDialogFont();
 	const Graphics::Font *getInterfaceFont();
+	const Graphics::Font *getGameFont();
 	const byte *getNativeInterfacePalette() const { return _nativeInterfacePalette; }
 
 	static ResourceID mapResource(const Common::String &filename);
diff --git a/engines/mads/nebular/nebular.h b/engines/mads/nebular/nebular.h
index 6e517739eda..51265778577 100644
--- a/engines/mads/nebular/nebular.h
+++ b/engines/mads/nebular/nebular.h
@@ -94,6 +94,10 @@ public:
 	bool drawPopup() override;
 	int editMacintoshPopup(char *target, int maxLength) override;
 	void onPopupDestroyed() override;
+	int getMacintoshTextWidth(FontPtr font, const char *text,
+		int spacing) const override;
+	bool drawMacintoshText(FontPtr font, Buffer *target, const char *text,
+		int x, int y, int color, int spacing) const override;
 	bool getInterfaceSentenceColors(byte &foreground, byte &shadow) const override;
 	bool hasMacintoshInterface() const override;
 	bool setMacintoshPalette(const RGBcolor *palette, int firstColor,


Commit: 7d41c5a5530af170845f5955fa33cea9dd2a9010
    https://github.com/scummvm/scummvm/commit/7d41c5a5530af170845f5955fa33cea9dd2a9010
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: Enable inventory keyboard scrolling

Handle the existing inventory scroll actions during ordinary gameplay.
Keep the final visible page full and refresh the shared interface state
without synthesizing mouse input.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/core/game.cpp
    engines/mads/core/inter.cpp
    engines/mads/core/inter.h


diff --git a/engines/mads/core/game.cpp b/engines/mads/core/game.cpp
index a66a4c9ca60..67b5682d018 100644
--- a/engines/mads/core/game.cpp
+++ b/engines/mads/core/game.cpp
@@ -1013,6 +1013,14 @@ int game_parse_keystroke(int mykey) {
 		mouse_force(kernel.cursor_x[1 - current_mode], kernel.cursor_y[1 - current_mode]);
 		break;
 
+	case pgup_key:
+		inter_scroll_inventory(-1);
+		break;
+
+	case pgdn_key:
+		inter_scroll_inventory(1);
+		break;
+
 	case ctrl_k_key:
 		inter_report_hotspots = !inter_report_hotspots;
 		inter_init_sentence();
diff --git a/engines/mads/core/inter.cpp b/engines/mads/core/inter.cpp
index f2e934c8772..c6079c19468 100644
--- a/engines/mads/core/inter.cpp
+++ b/engines/mads/core/inter.cpp
@@ -761,6 +761,25 @@ static void inter_set_active_word(int class_, int *old, int new_) {
 	}
 }
 
+void inter_scroll_inventory(int direction) {
+	if (!direction || (inter_input_mode != INTER_BUILDING_SENTENCES &&
+			inter_input_mode != INTER_LIMITED_SENTENCES))
+		return;
+
+	const int lastPage = MAX(0, inven_num_objects - inter_columns);
+	const int newFirst = CLIP(first_inven + (direction < 0 ? -1 : 1),
+		0, lastPage);
+	if (newFirst == first_inven)
+		return;
+
+	int unused = 0;
+	first_inven = newFirst;
+	first_inven_changed = true;
+	inter_set_active_word(STROKE_INVEN, &unused, 0);
+	inter_scrollbar_refresh();
+	inter_force_rescan = true;
+}
+
 void inter_set_active_inven(int new_active) {
 	int new_verbs, old_verbs, max_verbs;
 	int x1, y1, xs, ys;
diff --git a/engines/mads/core/inter.h b/engines/mads/core/inter.h
index 26cc2cb09b3..03ebc1cf88a 100644
--- a/engines/mads/core/inter.h
+++ b/engines/mads/core/inter.h
@@ -290,6 +290,11 @@ extern void inter_setup_hotspots();
  * @param new_active	
  */
 extern void inter_set_active_inven(int inven_id);
+
+/**
+ * Scrolls the visible inventory by one item in the requested direction.
+ */
+extern void inter_scroll_inventory(int direction);
 /*
  * inter_give_to_player ()
  * Adds the specified object to the player's inventory.


Commit: 98e62efa65187062b300bf7adabfa63bfb200e97
    https://github.com/scummvm/scummvm/commit/98e62efa65187062b300bf7adabfa63bfb200e97
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: NEBULAR: Restore Macintosh scrollbar drawing

Replace the approximate control with the native CODE 7 QuickDraw
geometry, arrow regions, thumb colors, and pressed states. Preserve the
themed panel artwork underneath the scrollbar track.

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 399a03c1a05..a5744628989 100644
--- a/engines/mads/nebular/mac_nebular.cpp
+++ b/engines/mads/nebular/mac_nebular.cpp
@@ -307,10 +307,16 @@ static Common::String getMacInterfaceWord(int wordId) {
 	return Common::String(text);
 }
 
+static void drawMacScrollbarArrow(Graphics::ManagedSurface &panel,
+		const int *x, const int *y, int count, const Common::Rect &bounds,
+		byte color) {
+	panel.drawPolygonScan(x, y, count, bounds, color);
+	for (int i = 0; i < count; ++i)
+		panel.drawLine(x[i], y[i], x[(i + 1) % count],
+			y[(i + 1) % count], color);
+}
+
 static void drawMacInterfaceScrollbar(Graphics::ManagedSurface &panel) {
-	// 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);
@@ -319,10 +325,28 @@ static void drawMacInterfaceScrollbar(Graphics::ManagedSurface &panel) {
 	panel.frameRect(down, kMacNormalTextColor);
 
 	if (inven_num_objects > 5) {
-		panel.fillRect(Common::Rect(121, 18, 135, 70), 4);
+		// CODE 7 records these QuickDraw line paths as regions, then paints
+		// them. The coordinates are native 512x88 panel coordinates.
+		const int upX[] = { 122, 127, 128, 133, 128, 128, 127, 127 };
+		const int upY[] = { 9, 4, 4, 9, 9, 15, 15, 9 };
+		const int downX[] = { 122, 127, 127, 128, 128, 133, 128, 127 };
+		const int downY[] = { 79, 79, 73, 73, 79, 79, 84, 84 };
+		drawMacScrollbarArrow(panel, upX, upY, ARRAYSIZE(upX),
+			Common::Rect(122, 4, 134, 16),
+			scrollbar_active == SCROLL_UP ? kMacLeftSelectColor :
+			kMacNormalTextColor);
+		drawMacScrollbarArrow(panel, downX, downY, ARRAYSIZE(downX),
+			Common::Rect(122, 73, 134, 85),
+			scrollbar_active == SCROLL_DOWN ? kMacLeftSelectColor :
+			kMacNormalTextColor);
+
+		const int lastPage = inven_num_objects - 5;
+		const int visibleFirst = MIN(first_inven, lastPage);
 		const int thumbTop = 18 +
-			(first_inven * 48) / (inven_num_objects - 5);
+			(visibleFirst * 48) / lastPage;
+		panel.fillRect(Common::Rect(122, thumbTop, 134, thumbTop + 4), 4);
 		panel.frameRect(Common::Rect(121, thumbTop, 135, thumbTop + 4),
+			scrollbar_active == SCROLL_ELEVATOR ? kMacLeftSelectColor :
 			kMacNormalTextColor);
 	}
 }


Commit: fc8e6cdc2e2f25dbb5d19fbfab2edd9afe0b8a78
    https://github.com/scummvm/scummvm/commit/fc8e6cdc2e2f25dbb5d19fbfab2edd9afe0b8a78
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: NEBULAR: Restore Macintosh modal presentations

Defer Preferences and About until the native menu event has unwound.
Keep startup Preferences cancellable, matching the recovered dialog.
Restore the room-990 About presentation and its palette transitions.

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/mac_resources.h


diff --git a/engines/mads/nebular/mac_menus.cpp b/engines/mads/nebular/mac_menus.cpp
index ee04f719215..0fc87cc66e8 100644
--- a/engines/mads/nebular/mac_menus.cpp
+++ b/engines/mads/nebular/mac_menus.cpp
@@ -298,6 +298,7 @@ void MacNebularMenu::updateState() {
 				itemIndex < _menu->numberOfMenuItems(topLevel); ++itemIndex)
 			setItemState(_menu->getSubMenuItem(topLevel, itemIndex), false, false);
 	}
+	setItemState(getMenuItem(kAppleMenu, 0), true, false);
 	if (_activeDialog) {
 		setItemState(getMenuItem(kEditMenu, 0),
 			_activeDialog->isEditCommandEnabled(kMacDialogUndo), false);
@@ -359,6 +360,11 @@ void MacNebularMenu::updateState() {
 
 void MacNebularMenu::dispatchCommand(int commandId) {
 	switch (commandId) {
+	case kAppleAbout:
+		// Run the full-frame presentation after the menu event has unwound.
+		// Otherwise the closing menu window can be composited over room 990.
+		_aboutRequested = true;
+		break;
 	case kFileOpen:
 		if (_engine.canLoadGameStateCurrently(nullptr))
 			runOpenDialog();
@@ -372,7 +378,9 @@ void MacNebularMenu::dispatchCommand(int commandId) {
 			runSaveDialog(true);
 		break;
 	case kFilePreferences:
-		runPreferencesDialog(false);
+		// Like About, this opens a modal presentation. Defer it until the
+		// menu event and its transient window have been fully dismissed.
+		_preferencesRequested = true;
 		break;
 	case kFileQuit:
 		_engine.quitGame();
@@ -493,6 +501,18 @@ void MacNebularMenu::draw() {
 	_menu->draw(&_screen, true);
 }
 
+bool MacNebularMenu::takeAboutRequest() {
+	const bool requested = _aboutRequested;
+	_aboutRequested = false;
+	return requested;
+}
+
+bool MacNebularMenu::takePreferencesRequest() {
+	const bool requested = _preferencesRequested;
+	_preferencesRequested = false;
+	return requested;
+}
+
 byte MacNebularMenu::getBlackColor() {
 	byte menuBlack, menuWhite;
 	getMenuColors(menuBlack, menuWhite);
@@ -536,11 +556,8 @@ void MacNebularMenu::runPreferencesDialog(bool startup) {
 	dialog.setItemChecked(7, true);
 	dialog.setItemChecked(8,
 		_engine.getMacintoshPreferencesAtStartup());
-	if (startup)
-		dialog.setItemEnabled(2, false);
-
 	_activeDialog = &dialog;
-	const int result = dialog.runModal(1, startup ? 0 : 2);
+	const int result = dialog.runModal(1, 2);
 	_activeDialog = nullptr;
 	if (result != 1)
 		return;
diff --git a/engines/mads/nebular/mac_menus.h b/engines/mads/nebular/mac_menus.h
index 9a6069b12ad..1544582e44a 100644
--- a/engines/mads/nebular/mac_menus.h
+++ b/engines/mads/nebular/mac_menus.h
@@ -56,6 +56,8 @@ private:
 	byte _palette[256 * 3] = {};
 	bool _paletteValid = false;
 	bool _outerMenuActive = false;
+	bool _aboutRequested = false;
+	bool _preferencesRequested = false;
 	uint32 _windowManagerMode = 0;
 	int _pendingCommand = -1;
 
@@ -82,6 +84,8 @@ public:
 	byte getBlackColor();
 	void getMenuColors(byte &menuBlack, byte &menuWhite);
 	void setMenuBarHidden(bool hidden);
+	bool takeAboutRequest();
+	bool takePreferencesRequest();
 	void runPreferencesDialog(bool startup);
 	void runOpenDialog();
 	void runSaveDialog(bool saveAs);
diff --git a/engines/mads/nebular/mac_nebular.cpp b/engines/mads/nebular/mac_nebular.cpp
index a5744628989..4e91d46e7f2 100644
--- a/engines/mads/nebular/mac_nebular.cpp
+++ b/engines/mads/nebular/mac_nebular.cpp
@@ -34,9 +34,14 @@
 #include "mads/core/font.h"
 #include "mads/core/inter.h"
 #include "mads/core/kernel.h"
+#include "mads/core/magic.h"
+#include "mads/core/mcga.h"
+#include "mads/core/mem.h"
 #include "mads/core/object.h"
 #include "mads/core/pal.h"
+#include "mads/core/room.h"
 #include "mads/core/screen.h"
+#include "mads/nebular/extra.h"
 #include "mads/nebular/mac_menus.h"
 #include "mads/nebular/mac_nebular.h"
 #include "mads/nebular/mac_resources.h"
@@ -44,7 +49,6 @@
 #include "mads/nebular/nebular.h"
 #include "mads/nebular/popup.h"
 #include "mads/nebular/mads/words.h"
-#include "mads/nebular/sound/mac_sound.h"
 
 namespace MADS {
 namespace RexNebular {
@@ -99,6 +103,83 @@ static byte macPaletteComponentToSixBit(byte color) {
 	return (color * 63 + 127) / 255;
 }
 
+static bool loadMacAboutRoom(Graphics::ManagedSurface &picture,
+		Palette &palette) {
+	Palette savedMasterPalette;
+	dword savedColorStatus[256];
+	int savedFlagUsed[PAL_MAXFLAGS];
+	const int savedPaletteLocked = palette_locked;
+	const int savedLowSearchLimit = palette_low_search_limit;
+	const int savedHighSearchLimit = palette_high_search_limit;
+	const int savedManagerActive = pal_manager_active;
+	const int savedManagerColors = pal_manager_colors;
+	void (*savedManagerUpdate)() = pal_manager_update;
+	ShadowListPtr savedMasterShadow = master_shadow;
+	const int savedRoomLoadError = room_load_error;
+	const byte savedRoomLoadedDepth = room_loaded_depth;
+
+	memcpy(savedMasterPalette, master_palette, sizeof(Palette));
+	memcpy(savedColorStatus, color_status, sizeof(color_status));
+	memcpy(savedFlagUsed, flag_used, sizeof(flag_used));
+
+	for (int color = 0; color < 256; ++color)
+		color_status[color] &= PAL_RESERVED;
+	for (int flag = 2; flag < PAL_MAXFLAGS; ++flag)
+		flag_used[flag] = false;
+	palette_locked = false;
+	palette_low_search_limit = 0;
+	palette_high_search_limit = 256;
+	pal_manager_active = false;
+	pal_manager_colors = 0;
+	pal_manager_update = nullptr;
+	master_shadow = nullptr;
+
+	Buffer roomPicture = {};
+	Buffer roomDepth = {};
+	RoomPtr aboutRoom = RexNebular::room_load(990, 0, nullptr,
+		&roomPicture, &roomDepth, nullptr, nullptr, nullptr, nullptr,
+		nullptr, nullptr, -1, -1, 0);
+	const bool loaded = aboutRoom && roomPicture.data &&
+		roomPicture.x == kMacLogicalSceneWidth &&
+		roomPicture.y == kMacLogicalSceneHeight;
+	if (loaded) {
+		picture.create(roomPicture.x, roomPicture.y,
+			Graphics::PixelFormat::createFormatCLUT8());
+		for (int y = 0; y < roomPicture.y; ++y)
+			memcpy(picture.getBasePtr(0, y),
+				roomPicture.data + y * roomPicture.x, roomPicture.x);
+		memcpy(palette, master_palette, sizeof(Palette));
+	}
+
+	if (aboutRoom) {
+		pal_deallocate(aboutRoom->color_handle);
+		mem_free(aboutRoom);
+	}
+	buffer_free(&roomPicture);
+	buffer_free(&roomDepth);
+
+	memcpy(master_palette, savedMasterPalette, sizeof(Palette));
+	memcpy(color_status, savedColorStatus, sizeof(color_status));
+	memcpy(flag_used, savedFlagUsed, sizeof(flag_used));
+	palette_locked = savedPaletteLocked;
+	palette_low_search_limit = savedLowSearchLimit;
+	palette_high_search_limit = savedHighSearchLimit;
+	pal_manager_active = savedManagerActive;
+	pal_manager_colors = savedManagerColors;
+	pal_manager_update = savedManagerUpdate;
+	master_shadow = savedMasterShadow;
+	room_load_error = savedRoomLoadError;
+	room_loaded_depth = savedRoomLoadedDepth;
+	return loaded;
+}
+
+static void drawMacAboutText(Graphics::ManagedSurface &surface,
+		const Graphics::Font &font, const Common::String &text,
+		int x, int baseline, byte color) {
+	font.drawString(&surface, text, x, baseline - font.getFontAscent(),
+		surface.w - x, color);
+}
+
 static void setMacInterfacePalette(const MacResourceProvider *resources) {
 	if (resources && resources->getNativeInterface()) {
 		const byte *panelPalette = resources->getNativeInterfacePalette();
@@ -521,12 +602,204 @@ void MacNebular::setStoryLocked(bool locked,
 }
 
 void MacNebular::serviceUI() {
-	if (!_startupPreferencesReady || !_showPreferencesAtStartup || !_menus)
+	if (!_menus)
+		return;
+
+	if (_startupPreferencesReady && _showPreferencesAtStartup) {
+		_startupPreferencesReady = false;
+		_showPreferencesAtStartup = false;
+		_menus->runPreferencesDialog(true);
+		_engine._screen->markAllDirty();
+		return;
+	}
+	if (_menus->takePreferencesRequest()) {
+		_menus->runPreferencesDialog(false);
+		_engine._screen->markAllDirty();
 		return;
+	}
+
+	if (_menus->takeAboutRequest())
+		showAbout();
+}
 
-	_startupPreferencesReady = false;
-	_showPreferencesAtStartup = false;
-	_menus->runPreferencesDialog(true);
+void MacNebular::showAbout() {
+	if (!_useOriginalMenus || !_resources || !_menus)
+		return;
+
+	Graphics::ManagedSurface picture;
+	Palette aboutPalette;
+	if (!loadMacAboutRoom(picture, aboutPalette)) {
+		warning("Could not load Macintosh About room 990");
+		return;
+	}
+
+	int titleSize;
+	int textSize;
+	int titleX;
+	int titleY;
+	int textX;
+	int firstTextY;
+	int helpX;
+	int helpY;
+	int serviceX;
+	int serviceY;
+	int phoneX;
+	int phoneY;
+	switch (_displaySize) {
+	case kMacNebularDisplay100:
+		titleSize = 14;
+		textSize = 12;
+		titleX = 30;
+		titleY = 75;
+		textX = 45;
+		firstTextY = 100;
+		helpX = 60;
+		helpY = 115;
+		serviceX = 45;
+		serviceY = 135;
+		phoneX = 60;
+		phoneY = 150;
+		break;
+	case kMacNebularDisplay150:
+		titleSize = 20;
+		textSize = 18;
+		titleX = 45;
+		titleY = 112;
+		textX = 68;
+		firstTextY = 150;
+		helpX = 90;
+		helpY = 172;
+		serviceX = 68;
+		serviceY = 202;
+		phoneX = 90;
+		phoneY = 225;
+		break;
+	default:
+		titleSize = 28;
+		textSize = 24;
+		titleX = 60;
+		titleY = 150;
+		textX = 90;
+		firstTextY = 200;
+		helpX = 120;
+		helpY = 230;
+		serviceX = 90;
+		serviceY = 270;
+		phoneX = 120;
+		phoneY = 300;
+		break;
+	}
+
+	const Graphics::Font *titleFont = _resources->getAboutFont(titleSize);
+	const Graphics::Font *textFont = _resources->getAboutFont(textSize);
+	if (!titleFont || !textFont) {
+		warning("Could not load Macintosh About fonts");
+		return;
+	}
+
+	Graphics::ManagedSurface savedOutput;
+	savedOutput.copyFrom(_output);
+	Palette savedPalette;
+	memcpy(savedPalette, _palette, sizeof(Palette));
+	const bool savedFullFrameActive = _fullFrameActive;
+	_fullFrameActive = true;
+	PauseToken pauseToken = _engine.pauseEngine();
+
+	Palette transitionPalette;
+	memcpy(transitionPalette, savedPalette, sizeof(Palette));
+	magic_fade_to_grey(transitionPalette, nullptr,
+		0, 256, 0, 1, 1, 16);
+
+	const byte blackColor = _menus->getBlackColor();
+	_output.fillRect(_output.getBounds(), blackColor);
+	const int sceneX = getSceneX();
+	const int sceneY = getSceneY();
+	const int sceneWidth = getSceneWidth();
+	const int sceneHeight = getSceneHeight();
+	for (int y = 0; y < sceneHeight; ++y) {
+		const byte *source = (const byte *)picture.getBasePtr(
+			0, y * picture.h / sceneHeight);
+		byte *target = (byte *)_output.getBasePtr(sceneX, sceneY + y);
+		for (int x = 0; x < sceneWidth; ++x)
+			target[x] = source[x * picture.w / sceneWidth];
+	}
+
+	drawMacAboutText(_output, *titleFont, "From MicroProse Software.",
+		sceneX + titleX, sceneY + titleY, blackColor);
+	drawMacAboutText(_output, *textFont, "For hints and help call:",
+		sceneX + textX, sceneY + firstTextY, blackColor);
+	drawMacAboutText(_output, *textFont, "1 - 900 - 933 - PLAY",
+		sceneX + helpX, sceneY + helpY, blackColor);
+	drawMacAboutText(_output, *textFont, "For customer service call:",
+		sceneX + serviceX, sceneY + serviceY, blackColor);
+	drawMacAboutText(_output, *textFont, "1 - 410 - 771 - 1151",
+		sceneX + phoneX, sceneY + phoneY, blackColor);
+	_menus->draw();
+	g_system->copyRectToScreen(_output.getPixels(), _output.pitch,
+		0, 0, _output.w, _output.h);
+	g_system->updateScreen();
+	magic_fade_from_grey((RGBcolor *)transitionPalette, aboutPalette,
+		0, 256, 0, 1, 1, 16);
+
+	bool pressed = false;
+	bool released = false;
+	bool pressedMouse = false;
+	uint32 releaseTime = 0;
+	g_system->delayMillis(166);
+	while (!_engine.shouldQuit() &&
+			(!pressed || !released || g_system->getMillis() < releaseTime)) {
+		Common::Event event;
+		while (g_system->getEventManager()->pollEvent(event)) {
+			switch (event.type) {
+			case Common::EVENT_QUIT:
+				_engine.quitGame();
+				break;
+			case Common::EVENT_LBUTTONDOWN:
+				if (!pressed) {
+					pressed = true;
+					pressedMouse = true;
+					releaseTime = g_system->getMillis() + 333;
+				}
+				break;
+			case Common::EVENT_KEYDOWN:
+				if (!pressed) {
+					pressed = true;
+					pressedMouse = false;
+					releaseTime = g_system->getMillis() + 333;
+				}
+				break;
+			case Common::EVENT_LBUTTONUP:
+				if (pressed && pressedMouse)
+					released = true;
+				break;
+			case Common::EVENT_KEYUP:
+				if (pressed && !pressedMouse)
+					released = true;
+				break;
+			default:
+				break;
+			}
+		}
+		g_system->delayMillis(10);
+	}
+
+	if (!_engine.shouldQuit()) {
+		memcpy(transitionPalette, aboutPalette, sizeof(Palette));
+		magic_fade_to_grey(transitionPalette, nullptr,
+			0, 256, 0, 1, 1, 16);
+	}
+	_output.copyFrom(savedOutput);
+	_menus->draw();
+	g_system->copyRectToScreen(_output.getPixels(), _output.pitch,
+		0, 0, _output.w, _output.h);
+	g_system->updateScreen();
+	if (!_engine.shouldQuit()) {
+		magic_fade_from_grey((RGBcolor *)transitionPalette, savedPalette,
+			0, 256, 0, 1, 1, 16);
+	} else {
+		mcga_setpal(&savedPalette);
+	}
+	_fullFrameActive = savedFullFrameActive;
 	_engine._screen->markAllDirty();
 }
 
diff --git a/engines/mads/nebular/mac_nebular.h b/engines/mads/nebular/mac_nebular.h
index 96bee820960..173fd1d8a9f 100644
--- a/engines/mads/nebular/mac_nebular.h
+++ b/engines/mads/nebular/mac_nebular.h
@@ -77,6 +77,7 @@ private:
 	int getSceneX() const;
 	int getSceneY() const;
 	int getInterfaceY() const;
+	void showAbout();
 public:
 	explicit MacNebular(RexNebularEngine &engine);
 	~MacNebular();
diff --git a/engines/mads/nebular/mac_resources.cpp b/engines/mads/nebular/mac_resources.cpp
index 48168653ea2..83d6139100b 100644
--- a/engines/mads/nebular/mac_resources.cpp
+++ b/engines/mads/nebular/mac_resources.cpp
@@ -233,6 +233,14 @@ const Graphics::Font *MacResourceProvider::getGameFont() {
 		Graphics::kMacFontGeneva, 10, Graphics::kMacFontBold));
 }
 
+const Graphics::Font *MacResourceProvider::getAboutFont(int size) {
+	if (!_fontManager)
+		return nullptr;
+
+	return _fontManager->getFont(Graphics::MacFont(
+		Graphics::kMacFontGeneva, size, Graphics::kMacFontRegular));
+}
+
 MacResourceProvider::ResourceID MacResourceProvider::mapResource(const Common::String &filename) {
 	Common::String name(filename);
 	name.toUppercase();
diff --git a/engines/mads/nebular/mac_resources.h b/engines/mads/nebular/mac_resources.h
index 81bc641b664..db887d295b4 100644
--- a/engines/mads/nebular/mac_resources.h
+++ b/engines/mads/nebular/mac_resources.h
@@ -85,6 +85,7 @@ public:
 	const Graphics::Font *getDialogFont();
 	const Graphics::Font *getInterfaceFont();
 	const Graphics::Font *getGameFont();
+	const Graphics::Font *getAboutFont(int size);
 	const byte *getNativeInterfacePalette() const { return _nativeInterfacePalette; }
 
 	static ResourceID mapResource(const Common::String &filename);


Commit: d866bf1378a8725c0066bf4304c37dbb0704ead7
    https://github.com/scummvm/scummvm/commit/d866bf1378a8725c0066bf4304c37dbb0704ead7
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: NEBULAR: Defer Macintosh startup preferences

Wait for room 990 to finish its first matte before opening the
startup Preferences dialog. This ensures the Macintosh window manager
has distinct black and white palette entries.

Keep the request pending if the palette is not ready. Disable Cancel
for the startup form as the original application does. Restore the
Apple menu command IDs required by menu dispatch.

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_menu.cpp
    engines/mads/nebular/nebular.h


diff --git a/engines/mads/nebular/mac_menus.cpp b/engines/mads/nebular/mac_menus.cpp
index 0fc87cc66e8..5b3299339bd 100644
--- a/engines/mads/nebular/mac_menus.cpp
+++ b/engines/mads/nebular/mac_menus.cpp
@@ -56,11 +56,13 @@ enum {
 	kSaveDialog = 3001,
 	kOpenDialog = 3010,
 
+	kAppleMenu = 0,
 	kFileMenu = 1,
 	kEditMenu = 2,
 	kOptionsMenu = 3,
 	kWindowMenu = 4,
 
+	kAppleAbout = (1000 << 16),
 	kFileOpen = (1001 << 16) | 2,
 	kFileSave = (1001 << 16) | 4,
 	kFileSaveAs = (1001 << 16) | 5,
@@ -540,16 +542,21 @@ void MacNebularMenu::setMenuBarHidden(bool hidden) {
 	_menu->setVisible(!hidden, true);
 }
 
-void MacNebularMenu::runPreferencesDialog(bool startup) {
+bool MacNebularMenu::runPreferencesDialog(bool startup) {
 	if (!initializeWindowManager())
-		return;
+		return false;
+
+	syncPalette();
+	if (_windowManager->_colorBlack == _windowManager->_colorWhite)
+		return false;
 
 	MacNebularDialog dialog(_engine, _resources, _screen, *_windowManager,
 		this);
 	if (!dialog.load(startup ? kStartupPreferencesDialog :
 			kPreferencesDialog))
-		return;
+		return false;
 	dialog.center();
+	dialog.setItemEnabled(2, !startup);
 	dialog.setItemChecked(4, _engine.getMacintoshHideMenuBar());
 	dialog.setItemEnabled(5, false);
 	dialog.setItemEnabled(6, false);
@@ -557,15 +564,16 @@ void MacNebularMenu::runPreferencesDialog(bool startup) {
 	dialog.setItemChecked(8,
 		_engine.getMacintoshPreferencesAtStartup());
 	_activeDialog = &dialog;
-	const int result = dialog.runModal(1, 2);
+	const int result = dialog.runModal(1, startup ? 0 : 2);
 	_activeDialog = nullptr;
-	if (result != 1)
-		return;
+	if (result == 1) {
+		const bool persist = dialog.isItemChecked(7);
+		_engine.setMacintoshHideMenuBar(dialog.isItemChecked(4), persist);
+		_engine.setMacintoshPreferencesAtStartup(dialog.isItemChecked(8),
+			persist);
+	}
 
-	const bool persist = dialog.isItemChecked(7);
-	_engine.setMacintoshHideMenuBar(dialog.isItemChecked(4), persist);
-	_engine.setMacintoshPreferencesAtStartup(dialog.isItemChecked(8),
-		persist);
+	return true;
 }
 
 int MacNebularMenu::runPopupEditor(const Common::Rect &bounds,
diff --git a/engines/mads/nebular/mac_menus.h b/engines/mads/nebular/mac_menus.h
index 1544582e44a..3687bf80ff4 100644
--- a/engines/mads/nebular/mac_menus.h
+++ b/engines/mads/nebular/mac_menus.h
@@ -86,7 +86,7 @@ public:
 	void setMenuBarHidden(bool hidden);
 	bool takeAboutRequest();
 	bool takePreferencesRequest();
-	void runPreferencesDialog(bool startup);
+	bool runPreferencesDialog(bool startup);
 	void runOpenDialog();
 	void runSaveDialog(bool saveAs);
 	int runPopupEditor(const Common::Rect &bounds, char *target,
diff --git a/engines/mads/nebular/mac_nebular.cpp b/engines/mads/nebular/mac_nebular.cpp
index 4e91d46e7f2..532b77d976e 100644
--- a/engines/mads/nebular/mac_nebular.cpp
+++ b/engines/mads/nebular/mac_nebular.cpp
@@ -606,9 +606,10 @@ void MacNebular::serviceUI() {
 		return;
 
 	if (_startupPreferencesReady && _showPreferencesAtStartup) {
+		if (!_menus->runPreferencesDialog(true))
+			return;
 		_startupPreferencesReady = false;
 		_showPreferencesAtStartup = false;
-		_menus->runPreferencesDialog(true);
 		_engine._screen->markAllDirty();
 		return;
 	}
@@ -867,6 +868,8 @@ int MacNebular::selectResumeSlot() {
 
 void MacNebular::setOuterMenuActive(bool active) {
 	const bool wasActive = _fullFrameActive;
+	if (active)
+		_startupPreferencesReady = false;
 	setFullFrameActive(active);
 	if (_menus)
 		_menus->setOuterMenuActive(active);
@@ -884,6 +887,11 @@ void MacNebular::setOuterMenuActive(bool active) {
 	}
 }
 
+void MacNebular::notifyOuterMenuFrameReady() {
+	if (_useOriginalMenus && _showPreferencesAtStartup)
+		_startupPreferencesReady = true;
+}
+
 Common::Point MacNebular::screenToGame(const Common::Point &point) const {
 	if (_fullFrameActive) {
 		const int frameY = _useOriginalMenus ? kMacDesktopSceneY : 0;
@@ -1027,7 +1035,6 @@ void MacNebular::presentScreen(int shakeOffset) {
 			0, 0, kMacScreenWidth, _output.h);
 		g_system->updateScreen();
 		_engine._screen->clearDirtyRects();
-		_startupPreferencesReady = true;
 		return;
 	}
 
@@ -1143,7 +1150,9 @@ void MacNebular::presentScreen(int shakeOffset) {
 		0, 0, kMacScreenWidth, _output.h);
 	g_system->updateScreen();
 	_engine._screen->clearDirtyRects();
-	_startupPreferencesReady = true;
+	if (_showPreferencesAtStartup && !_startupPreferencesReady &&
+			kernel_mode == KERNEL_ACTIVE_CODE && !kernel.fx)
+		_startupPreferencesReady = true;
 }
 
 bool MacNebular::handleMacEvent(Common::Event &event) {
@@ -1448,6 +1457,11 @@ void RexNebularEngine::setMacintoshOuterMenuActive(bool active) {
 		_macNebular->setOuterMenuActive(active);
 }
 
+void RexNebularEngine::notifyMacintoshOuterMenuFrameReady() {
+	if (_macNebular)
+		_macNebular->notifyOuterMenuFrameReady();
+}
+
 void RexNebularEngine::setMacintoshFullFrameActive(bool active) {
 	if (_macNebular)
 		_macNebular->setFullFrameActive(active);
diff --git a/engines/mads/nebular/mac_nebular.h b/engines/mads/nebular/mac_nebular.h
index 173fd1d8a9f..7b50a181473 100644
--- a/engines/mads/nebular/mac_nebular.h
+++ b/engines/mads/nebular/mac_nebular.h
@@ -99,6 +99,7 @@ public:
 	void setStoryLocked(bool locked, const Common::String &password);
 	void setFullFrameActive(bool active) { _fullFrameActive = active; }
 	void setOuterMenuActive(bool active);
+	void notifyOuterMenuFrameReady();
 	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_menu.cpp b/engines/mads/nebular/main_menu.cpp
index 96da34242de..e13d14cdc90 100644
--- a/engines/mads/nebular/main_menu.cpp
+++ b/engines/mads/nebular/main_menu.cpp
@@ -38,6 +38,7 @@
 #include "mads/nebular/main_menu.h"
 #include "mads/nebular/extra.h"
 #include "mads/nebular/mads/sounds.h"
+#include "mads/nebular/nebular.h"
 #include "mads/mads.h"
 
 namespace MADS {
@@ -331,6 +332,7 @@ static void process_sprites() {
 void menu_control() {
 	int fx;
 	int mykey;
+	bool outerMenuFrameReady = false;
 
 	menu_mode = MENU_APPEARING;
 	animating_item = 0;
@@ -425,6 +427,11 @@ void menu_control() {
 
 			fx = new_background ? 1 : 0;
 			matte_frame(fx, false);
+			if (!outerMenuFrameReady) {
+				static_cast<RexNebularEngine *>(g_engine)->
+					notifyMacintoshOuterMenuFrameReady();
+				outerMenuFrameReady = true;
+			}
 
 			if (fx) {
 				now_clock = timer_read();
diff --git a/engines/mads/nebular/nebular.h b/engines/mads/nebular/nebular.h
index 51265778577..c7383ce71b2 100644
--- a/engines/mads/nebular/nebular.h
+++ b/engines/mads/nebular/nebular.h
@@ -77,6 +77,7 @@ public:
 	void setMacintoshStoryLocked(bool locked,
 		const Common::String &password);
 	void setMacintoshOuterMenuActive(bool active);
+	void notifyMacintoshOuterMenuFrameReady();
 	void setMacintoshFullFrameActive(bool active);
 
 	int main_copy_verify() override;


Commit: 072d6cbf3fffb082a7bbc8be3881b6fefbfbcfb8
    https://github.com/scummvm/scummvm/commit/072d6cbf3fffb082a7bbc8be3881b6fefbfbcfb8
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: NEBULAR: Complete Macintosh gameplay handoff

Keep the native interface hidden while the initial room matte is
still constructing its logical state. Continue presenting the scene
transition, then expose the complete panel after the effect finishes.

Reset the pending handoff whenever full-frame frontend ownership
resumes.

Assisted-by: Codex:GPT-5.4

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


diff --git a/engines/mads/nebular/mac_nebular.cpp b/engines/mads/nebular/mac_nebular.cpp
index 532b77d976e..754d68b2c70 100644
--- a/engines/mads/nebular/mac_nebular.cpp
+++ b/engines/mads/nebular/mac_nebular.cpp
@@ -703,7 +703,7 @@ void MacNebular::showAbout() {
 	Palette savedPalette;
 	memcpy(savedPalette, _palette, sizeof(Palette));
 	const bool savedFullFrameActive = _fullFrameActive;
-	_fullFrameActive = true;
+	setFullFrameActive(true);
 	PauseToken pauseToken = _engine.pauseEngine();
 
 	Palette transitionPalette;
@@ -800,7 +800,7 @@ void MacNebular::showAbout() {
 	} else {
 		mcga_setpal(&savedPalette);
 	}
-	_fullFrameActive = savedFullFrameActive;
+	setFullFrameActive(savedFullFrameActive);
 	_engine._screen->markAllDirty();
 }
 
@@ -866,6 +866,12 @@ int MacNebular::selectResumeSlot() {
 	return _menus ? _menus->selectResumeSlot() : -1;
 }
 
+void MacNebular::setFullFrameActive(bool active) {
+	_fullFrameActive = active;
+	if (active)
+		_gameplayHandoffPending = false;
+}
+
 void MacNebular::setOuterMenuActive(bool active) {
 	const bool wasActive = _fullFrameActive;
 	if (active)
@@ -875,6 +881,7 @@ void MacNebular::setOuterMenuActive(bool active) {
 		_menus->setOuterMenuActive(active);
 
 	if (wasActive && !active) {
+		_gameplayHandoffPending = _useOriginalMenus;
 		const byte frameBlack = _useOriginalMenus && _menus ?
 			_menus->getBlackColor() : 0;
 		_output.fillRect(_output.getBounds(), frameBlack);
@@ -1043,6 +1050,7 @@ void MacNebular::presentScreen(int shakeOffset) {
 	const int sceneWidth = getSceneWidth();
 	const int sceneHeight = getSceneHeight();
 	const int interfaceY = getInterfaceY();
+	const bool suppressPanel = _gameplayHandoffPending && kernel.fx;
 	setMacInterfacePalette(_resources);
 	_output.fillRect(_output.getBounds(), kMacBlackColor);
 
@@ -1102,11 +1110,13 @@ void MacNebular::presentScreen(int shakeOffset) {
 		if (interfaceFont)
 			drawMacInterfaceState(panel, *interfaceFont);
 
-		for (int y = 0; y < kMacInterfaceHeight; ++y) {
-			memcpy(_output.getBasePtr(kMacInterfaceX, interfaceY + y),
-				panel.getBasePtr(0, y), kMacInterfaceWidth);
+		if (!suppressPanel) {
+			for (int y = 0; y < kMacInterfaceHeight; ++y) {
+				memcpy(_output.getBasePtr(kMacInterfaceX, interfaceY + y),
+					panel.getBasePtr(0, y), kMacInterfaceWidth);
+			}
 		}
-	} else {
+	} else if (!suppressPanel) {
 		// Before the native panel is loaded, retain a structurally equivalent
 		// fallback by scaling the shared 320x44 interface into its Mac bounds.
 		for (int y = 0; y < kMacInterfaceHeight; ++y) {
@@ -1150,6 +1160,8 @@ void MacNebular::presentScreen(int shakeOffset) {
 		0, 0, kMacScreenWidth, _output.h);
 	g_system->updateScreen();
 	_engine._screen->clearDirtyRects();
+	if (_gameplayHandoffPending && !kernel.fx)
+		_gameplayHandoffPending = false;
 	if (_showPreferencesAtStartup && !_startupPreferencesReady &&
 			kernel_mode == KERNEL_ACTIVE_CODE && !kernel.fx)
 		_startupPreferencesReady = true;
diff --git a/engines/mads/nebular/mac_nebular.h b/engines/mads/nebular/mac_nebular.h
index 7b50a181473..794f0fe99fb 100644
--- a/engines/mads/nebular/mac_nebular.h
+++ b/engines/mads/nebular/mac_nebular.h
@@ -62,6 +62,7 @@ private:
 	bool _storyLocked;
 	Common::String _storyPassword;
 	bool _fullFrameActive = false;
+	bool _gameplayHandoffPending = false;
 	bool _popupActive = false;
 	int _popupAskLine = -1;
 	int _popupAskX = 0;
@@ -97,7 +98,7 @@ public:
 	void setHideMenuBar(bool hide, bool persist);
 	void setPreferencesAtStartup(bool show, bool persist);
 	void setStoryLocked(bool locked, const Common::String &password);
-	void setFullFrameActive(bool active) { _fullFrameActive = active; }
+	void setFullFrameActive(bool active);
 	void setOuterMenuActive(bool active);
 	void notifyOuterMenuFrameReady();
 	Common::Point screenToGame(const Common::Point &point) const;


Commit: d75c9a8623bc370dcd845bb5ff17991a7595f4ee
    https://github.com/scummvm/scummvm/commit/d75c9a8623bc370dcd845bb5ff17991a7595f4ee
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: NEBULAR: Preserve legacy Macintosh handoff

Keep the inventory panel hidden across the pre-game gray fade when
original Macintosh menus are disabled. Wait until the gameplay
transition effect has started and completed before exposing the panel
in either Macintosh composition.

Assisted-by: Codex:GPT-5.4

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


diff --git a/engines/mads/nebular/mac_nebular.cpp b/engines/mads/nebular/mac_nebular.cpp
index 754d68b2c70..4925697f744 100644
--- a/engines/mads/nebular/mac_nebular.cpp
+++ b/engines/mads/nebular/mac_nebular.cpp
@@ -868,8 +868,10 @@ int MacNebular::selectResumeSlot() {
 
 void MacNebular::setFullFrameActive(bool active) {
 	_fullFrameActive = active;
-	if (active)
+	if (active) {
 		_gameplayHandoffPending = false;
+		_gameplayHandoffEffectSeen = false;
+	}
 }
 
 void MacNebular::setOuterMenuActive(bool active) {
@@ -881,7 +883,8 @@ void MacNebular::setOuterMenuActive(bool active) {
 		_menus->setOuterMenuActive(active);
 
 	if (wasActive && !active) {
-		_gameplayHandoffPending = _useOriginalMenus;
+		_gameplayHandoffPending = true;
+		_gameplayHandoffEffectSeen = false;
 		const byte frameBlack = _useOriginalMenus && _menus ?
 			_menus->getBlackColor() : 0;
 		_output.fillRect(_output.getBounds(), frameBlack);
@@ -1050,7 +1053,10 @@ void MacNebular::presentScreen(int shakeOffset) {
 	const int sceneWidth = getSceneWidth();
 	const int sceneHeight = getSceneHeight();
 	const int interfaceY = getInterfaceY();
-	const bool suppressPanel = _gameplayHandoffPending && kernel.fx;
+	if (_gameplayHandoffPending && kernel.fx)
+		_gameplayHandoffEffectSeen = true;
+	const bool suppressPanel = _gameplayHandoffPending &&
+		(!_gameplayHandoffEffectSeen || kernel.fx);
 	setMacInterfacePalette(_resources);
 	_output.fillRect(_output.getBounds(), kMacBlackColor);
 
@@ -1160,8 +1166,11 @@ void MacNebular::presentScreen(int shakeOffset) {
 		0, 0, kMacScreenWidth, _output.h);
 	g_system->updateScreen();
 	_engine._screen->clearDirtyRects();
-	if (_gameplayHandoffPending && !kernel.fx)
+	if (_gameplayHandoffPending && _gameplayHandoffEffectSeen &&
+			!kernel.fx) {
 		_gameplayHandoffPending = false;
+		_gameplayHandoffEffectSeen = false;
+	}
 	if (_showPreferencesAtStartup && !_startupPreferencesReady &&
 			kernel_mode == KERNEL_ACTIVE_CODE && !kernel.fx)
 		_startupPreferencesReady = true;
diff --git a/engines/mads/nebular/mac_nebular.h b/engines/mads/nebular/mac_nebular.h
index 794f0fe99fb..6f0e8ab14dc 100644
--- a/engines/mads/nebular/mac_nebular.h
+++ b/engines/mads/nebular/mac_nebular.h
@@ -63,6 +63,7 @@ private:
 	Common::String _storyPassword;
 	bool _fullFrameActive = false;
 	bool _gameplayHandoffPending = false;
+	bool _gameplayHandoffEffectSeen = false;
 	bool _popupActive = false;
 	int _popupAskLine = -1;
 	int _popupAskX = 0;


Commit: a5eb9235048f1721a11f27d6a62cc178cb3a673d
    https://github.com/scummvm/scummvm/commit/a5eb9235048f1721a11f27d6a62cc178cb3a673d
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: NEBULAR: Restore Macintosh version text

Read the application version from the Macintosh RXgm resource for
the copy-protection dialog instead of displaying the shared ScummVM
release placeholder. Keep the DOS path unchanged.

Assisted-by: Codex:GPT-5.4

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


diff --git a/engines/mads/nebular/copy.cpp b/engines/mads/nebular/copy.cpp
index 882cd27bc92..fda352d9696 100644
--- a/engines/mads/nebular/copy.cpp
+++ b/engines/mads/nebular/copy.cpp
@@ -131,8 +131,21 @@ static int copy_pop_and_ask() {
 			goto finish;
 
 		if (!count) {
-			Common::strcpy_s(work_buf, "REX NEBULAR version ");
-			Common::strcat_s(work_buf, global_release_version);
+			if (g_engine->getPlatform() == Common::kPlatformMacintosh) {
+				const char versionPrefix[] = "Rex Nebular v";
+				Common::String version =
+					static_cast<RexNebularEngine *>(g_engine)->
+					getMacintoshApplicationVersion();
+				if (version.hasPrefix(versionPrefix))
+					version = version.substr(sizeof(versionPrefix) - 1);
+
+				Common::strcpy_s(work_buf,
+					"Macintosh REX NEBULAR version ");
+				Common::strcat_s(work_buf, version.c_str());
+			} else {
+				Common::strcpy_s(work_buf, "REX NEBULAR version ");
+				Common::strcat_s(work_buf, global_release_version);
+			}
 			popup_center_string(work_buf, true);
 			popup_write_string("\n");
 			popup_center_string("(Copy Protection, for your convenience)", false);
diff --git a/engines/mads/nebular/mac_nebular.cpp b/engines/mads/nebular/mac_nebular.cpp
index 4925697f744..e7ee87880a2 100644
--- a/engines/mads/nebular/mac_nebular.cpp
+++ b/engines/mads/nebular/mac_nebular.cpp
@@ -583,6 +583,10 @@ void MacNebular::setPreferencesAtStartup(bool show, bool persist) {
 	}
 }
 
+Common::String MacNebular::getApplicationVersion() const {
+	return _resources ? _resources->getApplicationVersion() : Common::String();
+}
+
 bool MacNebular::verifyStoryPassword(
 		const Common::String &password) const {
 	return password == _storyPassword || password == "hicuri" ||
@@ -1441,6 +1445,10 @@ bool RexNebularEngine::getMacintoshPreferencesAtStartup() const {
 	return _macNebular && _macNebular->getPreferencesAtStartup();
 }
 
+Common::String RexNebularEngine::getMacintoshApplicationVersion() const {
+	return _macNebular ? _macNebular->getApplicationVersion() : Common::String();
+}
+
 bool RexNebularEngine::getMacintoshStoryLocked() const {
 	return _macNebular && _macNebular->getStoryLocked();
 }
diff --git a/engines/mads/nebular/mac_nebular.h b/engines/mads/nebular/mac_nebular.h
index 6f0e8ab14dc..88905825af0 100644
--- a/engines/mads/nebular/mac_nebular.h
+++ b/engines/mads/nebular/mac_nebular.h
@@ -93,6 +93,7 @@ public:
 	int getDisplaySize() const { return _displaySize; }
 	bool getHideMenuBar() const { return _hideMenuBar; }
 	bool getPreferencesAtStartup() const { return _preferencesAtStartup; }
+	Common::String getApplicationVersion() const;
 	bool getStoryLocked() const { return _storyLocked; }
 	bool verifyStoryPassword(const Common::String &password) const;
 	void setDisplaySize(int displaySize, bool persist);
diff --git a/engines/mads/nebular/mac_resources.cpp b/engines/mads/nebular/mac_resources.cpp
index 83d6139100b..d0d56e56704 100644
--- a/engines/mads/nebular/mac_resources.cpp
+++ b/engines/mads/nebular/mac_resources.cpp
@@ -204,6 +204,25 @@ Common::SeekableReadStream *MacResourceProvider::openResource(
 	return resourceContainer ? resourceContainer->getResource(type, id) : nullptr;
 }
 
+Common::String MacResourceProvider::getApplicationVersion() const {
+	Common::SeekableReadStream *stream = openResource(kApplicationContainer,
+		MKTAG('R', 'X', 'g', 'm'), 0);
+	if (!stream)
+		return Common::String();
+
+	const uint length = stream->readByte();
+	char version[256];
+	if (length > stream->size() - stream->pos() ||
+			stream->read(version, length) != length) {
+		delete stream;
+		return Common::String();
+	}
+
+	version[length] = '\0';
+	delete stream;
+	return Common::String(version);
+}
+
 const Graphics::Font *MacResourceProvider::getDialogFont() {
 	if (!_fontManager)
 		return nullptr;
diff --git a/engines/mads/nebular/mac_resources.h b/engines/mads/nebular/mac_resources.h
index db887d295b4..7cee7f45cbd 100644
--- a/engines/mads/nebular/mac_resources.h
+++ b/engines/mads/nebular/mac_resources.h
@@ -68,6 +68,7 @@ public:
 	bool load();
 	Common::SeekableReadStream *openResource(Container container, uint32 type,
 		uint16 id) const;
+	Common::String getApplicationVersion() const;
 	Common::SeekableReadStream *open(const char *filename) override;
 	bool exists(const char *filename) override;
 	bool allowsFallback(const char *filename) const override;
diff --git a/engines/mads/nebular/nebular.h b/engines/mads/nebular/nebular.h
index c7383ce71b2..066801a03d9 100644
--- a/engines/mads/nebular/nebular.h
+++ b/engines/mads/nebular/nebular.h
@@ -69,6 +69,7 @@ public:
 	int getMacintoshDisplaySize() const;
 	bool getMacintoshHideMenuBar() const;
 	bool getMacintoshPreferencesAtStartup() const;
+	Common::String getMacintoshApplicationVersion() const;
 	bool getMacintoshStoryLocked() const;
 	bool verifyMacintoshStoryPassword(const Common::String &password) const;
 	void setMacintoshDisplaySize(int displaySize, bool persist);


Commit: a24758f5215953700173dd13d3bdcfb6c2947780
    https://github.com/scummvm/scummvm/commit/a24758f5215953700173dd13d3bdcfb6c2947780
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: NEBULAR: Preserve Macintosh copy-protection ending

Follow the recovered failure route through ending four before credits.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/nebular/copy.cpp
    engines/mads/nebular/rooms/room804.cpp


diff --git a/engines/mads/nebular/copy.cpp b/engines/mads/nebular/copy.cpp
index fda352d9696..552e1112f4a 100644
--- a/engines/mads/nebular/copy.cpp
+++ b/engines/mads/nebular/copy.cpp
@@ -32,6 +32,7 @@
 #include "mads/core/video.h"
 #include "mads/nebular/copy.h"
 #include "mads/nebular/global.h"
+#include "mads/nebular/nebular.h"
 #include "mads/nebular/popup.h"
 #include "mads/mads.h"
 
diff --git a/engines/mads/nebular/rooms/room804.cpp b/engines/mads/nebular/rooms/room804.cpp
index dbeeb7ad545..d09c6731424 100644
--- a/engines/mads/nebular/rooms/room804.cpp
+++ b/engines/mads/nebular/rooms/room804.cpp
@@ -143,6 +143,8 @@ static void room_804_daemon() {
 
 				if (global[kCopyProtectFailed]) {
 					copy_protection_fail_screen();
+					if (g_engine->getPlatform() == Common::kPlatformMacintosh)
+						win_status = WIN_A_HEAD_POW;
 				} else {
 					win_status = WIN_A_HEAD_POW;
 				}


Commit: f99aeb876fc7d8c985cfa2224bad3c373b492e22
    https://github.com/scummvm/scummvm/commit/f99aeb876fc7d8c985cfa2224bad3c373b492e22
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: Add configurable TextView matte height

Keep existing presentation defaults while allowing full-frame viewers
to constrain ordinary matte updates independently.

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 47c40266258..401bdd6dd33 100644
--- a/engines/mads/nebular/mac_frontend.cpp
+++ b/engines/mads/nebular/mac_frontend.cpp
@@ -45,6 +45,7 @@ void runTextView(RexNebularEngine &engine, const char *resource) {
 	TextView::Presentation presentation;
 	presentation.bufferHeight = 210;
 	presentation.visibleHeight = 200;
+	presentation.matteHeight = presentation.visibleHeight;
 	presentation.drawBoundaryLines = false;
 	presentation.macintoshFullFrame = true;
 
diff --git a/engines/mads/textview/textview.cpp b/engines/mads/textview/textview.cpp
index f56bbd8a629..afbb0614cce 100644
--- a/engines/mads/textview/textview.cpp
+++ b/engines/mads/textview/textview.cpp
@@ -64,6 +64,7 @@ static int16 xPos;
 static byte line_slice[156];
 static int bufferHeight;
 static int visibleHeight;
+static int matteHeight;
 static bool drawBoundaryLines;
 static bool macintoshFullFrame;
 
@@ -74,7 +75,7 @@ static void present_matte_frame(int specialEffect, int fullScreen) {
 	}
 
 	const int workHeight = scr_work.y;
-	scr_work.y = visibleHeight;
+	scr_work.y = fullScreen ? visibleHeight : matteHeight;
 	matte_frame(specialEffect, fullScreen);
 	scr_work.y = workHeight;
 }
@@ -482,6 +483,7 @@ void textview_main(const char *resName) {
 	Presentation presentation;
 	presentation.bufferHeight = 156;
 	presentation.visibleHeight = 156;
+	presentation.matteHeight = presentation.visibleHeight;
 	presentation.drawBoundaryLines = true;
 	presentation.macintoshFullFrame = false;
 	textview_main(resName, presentation);
@@ -490,10 +492,12 @@ void textview_main(const char *resName) {
 void textview_main(const char *resName, const Presentation &presentation) {
 	bufferHeight = presentation.bufferHeight;
 	visibleHeight = presentation.visibleHeight;
+	matteHeight = presentation.matteHeight;
 	drawBoundaryLines = presentation.drawBoundaryLines;
 	macintoshFullFrame = presentation.macintoshFullFrame;
 	assert(bufferHeight >= 156 && visibleHeight > 0 &&
-		visibleHeight <= bufferHeight && visibleHeight <= 200);
+		visibleHeight <= bufferHeight && visibleHeight <= 200 &&
+		matteHeight > 0 && matteHeight <= visibleHeight);
 
 	active = false;
 	isGoing = flag2 = true;
diff --git a/engines/mads/textview/textview.h b/engines/mads/textview/textview.h
index 85f31c8429b..f6c0136e1a2 100644
--- a/engines/mads/textview/textview.h
+++ b/engines/mads/textview/textview.h
@@ -30,6 +30,7 @@ namespace TextView {
 struct Presentation {
 	int bufferHeight;
 	int visibleHeight;
+	int matteHeight;
 	bool drawBoundaryLines;
 	bool macintoshFullFrame;
 };


Commit: 4dc19bc738b9a5210717bf479bba490e53f587fe
    https://github.com/scummvm/scummvm/commit/4dc19bc738b9a5210717bf479bba490e53f587fe
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: NEBULAR: Match Macintosh TextView matte region

Retain the native 210-pixel backing and 200-pixel presentation while
limiting ordinary text and wipe mattes to 156 pixels.

Assisted-by: Codex:GPT-5.4

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


diff --git a/engines/mads/nebular/mac_frontend.cpp b/engines/mads/nebular/mac_frontend.cpp
index 401bdd6dd33..41cb00400a9 100644
--- a/engines/mads/nebular/mac_frontend.cpp
+++ b/engines/mads/nebular/mac_frontend.cpp
@@ -45,7 +45,7 @@ void runTextView(RexNebularEngine &engine, const char *resource) {
 	TextView::Presentation presentation;
 	presentation.bufferHeight = 210;
 	presentation.visibleHeight = 200;
-	presentation.matteHeight = presentation.visibleHeight;
+	presentation.matteHeight = 156;
 	presentation.drawBoundaryLines = false;
 	presentation.macintoshFullFrame = true;
 


Commit: 602387e29b4f114d134d59ef9ab8d5019055ba06
    https://github.com/scummvm/scummvm/commit/602387e29b4f114d134d59ef9ab8d5019055ba06
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: NEBULAR: Restore Macintosh copy protection dialog

Use the native DLOG and DITL resources for the Macintosh copy
protection prompt, including its editable field and wrapped static text.
Keep the existing DOS popup path unchanged.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/nebular/copy.cpp
    engines/mads/nebular/mac_dialogs.cpp
    engines/mads/nebular/mac_dialogs.h
    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/nebular.h


diff --git a/engines/mads/nebular/copy.cpp b/engines/mads/nebular/copy.cpp
index 552e1112f4a..e2b62e39022 100644
--- a/engines/mads/nebular/copy.cpp
+++ b/engines/mads/nebular/copy.cpp
@@ -123,6 +123,49 @@ static int copy_pop_and_ask() {
 
 	if (copy_load(&copy_prot)) goto finish;
 
+	if (g_engine->getPlatform() == Common::kPlatformMacintosh) {
+		RexNebularEngine *engine = static_cast<RexNebularEngine *>(g_engine);
+		Common::String version = engine->getMacintoshApplicationVersion();
+		const char versionPrefix[] = "Rex Nebular v";
+		if (version.hasPrefix(versionPrefix))
+			version = version.substr(sizeof(versionPrefix) - 1);
+
+		for (count = 0; count < COPY_TRIES_ALLOWED; ++count) {
+			const Common::String title = count == 0 ?
+				Common::String("Macintosh REX NEBULAR version ") + version :
+				Common::String("ANSWER INCORRECT!");
+			const Common::String subtitle = count == 0 ?
+				"(Copy Protection, for your convenience)" :
+				"(But we'll give you another chance!)";
+			const char *manual = copy_prot.manual == 'g' ?
+				"the GAME MANUAL" : "REX'S LOGBOOK";
+			const Common::String prompt = Common::String::format(
+				"Take out your copy of %s.  See!  That was easy.  Next, "
+				"just turn to page %d.  On line %d, find word number %d.",
+				manual, copy_prot.page, copy_prot.line,
+				copy_prot.word_number);
+
+			entry_buf[0] = copy_prot.say[0];
+			entry_buf[1] = '\0';
+			result = engine->runMacintoshCopyProtectionDialog(title,
+				subtitle, prompt, entry_buf, 12);
+			if (result < 0) {
+				warning("Could not open Macintosh copy-protection dialog");
+				error_flag = COPY_ESCAPE;
+				goto done;
+			}
+			if (result > 0) {
+				error_flag = COPY_ESCAPE;
+				goto done;
+			}
+
+			mads_strlwr(entry_buf);
+			if (strcmp(entry_buf, copy_prot.say) == 0)
+				goto finish;
+		}
+		goto done;
+	}
+
 	mads_itoa(copy_prot.page, page_buf, 10);
 	mads_itoa(copy_prot.line, line_buf, 10);
 	mads_itoa(copy_prot.word_number, word_buf, 10);
diff --git a/engines/mads/nebular/mac_dialogs.cpp b/engines/mads/nebular/mac_dialogs.cpp
index 633606b9045..e42d714a2db 100644
--- a/engines/mads/nebular/mac_dialogs.cpp
+++ b/engines/mads/nebular/mac_dialogs.cpp
@@ -223,6 +223,15 @@ void MacNebularDialog::setItemText(int itemNumber,
 	_redraw = true;
 }
 
+void MacNebularDialog::setItemMaxLength(int itemNumber, uint maxLength) {
+	Item *item = getItem(itemNumber);
+	if (!item)
+		return;
+	item->maxLength = maxLength;
+	if (maxLength && item->text.size() > maxLength)
+		setItemText(itemNumber, item->text.substr(0, maxLength));
+}
+
 Common::String MacNebularDialog::getItemText(int itemNumber) const {
 	const Item *item = getItem(itemNumber);
 	return item ? item->text : Common::String();
@@ -406,10 +415,15 @@ void MacNebularDialog::drawItem(Item &item, int itemNumber) {
 		drawRadioButton(item, bounds);
 		break;
 	case kStaticText: {
-		const int textY = bounds.top +
-			(bounds.height() - _font->getFontHeight()) / 2;
-		_font->drawString(&_screen, item.text, bounds.left, textY,
-			bounds.width(), _windowManager._colorBlack);
+		Common::Array<Common::String> lines;
+		_font->wordWrapText(item.text, bounds.width(), lines);
+		int textY = bounds.top;
+		for (uint line = 0; line < lines.size() &&
+				textY + _font->getFontHeight() <= bounds.bottom; ++line) {
+			_font->drawString(&_screen, lines[line], bounds.left, textY,
+				bounds.width(), _windowManager._colorBlack);
+			textY += _font->getFontHeight();
+		}
 		break;
 	}
 	case kEditableText:
diff --git a/engines/mads/nebular/mac_dialogs.h b/engines/mads/nebular/mac_dialogs.h
index f8382de70a8..128cf2aa33a 100644
--- a/engines/mads/nebular/mac_dialogs.h
+++ b/engines/mads/nebular/mac_dialogs.h
@@ -136,6 +136,7 @@ public:
 	void setItemChecked(int itemNumber, bool checked);
 	bool isItemChecked(int itemNumber) const;
 	void setItemText(int itemNumber, const Common::String &text);
+	void setItemMaxLength(int itemNumber, uint maxLength);
 	Common::String getItemText(int itemNumber) const;
 	void setList(int itemNumber, const Common::StringArray &entries,
 		int selection);
diff --git a/engines/mads/nebular/mac_menus.cpp b/engines/mads/nebular/mac_menus.cpp
index 5b3299339bd..42a25b13a06 100644
--- a/engines/mads/nebular/mac_menus.cpp
+++ b/engines/mads/nebular/mac_menus.cpp
@@ -55,6 +55,7 @@ enum {
 	kStoryPasswordDialog = 2006,
 	kSaveDialog = 3001,
 	kOpenDialog = 3010,
+	kCopyProtectionDialog = 4000,
 
 	kAppleMenu = 0,
 	kFileMenu = 1,
@@ -594,6 +595,33 @@ int MacNebularMenu::runPopupEditor(const Common::Rect &bounds,
 	return 0;
 }
 
+int MacNebularMenu::runCopyProtectionDialog(const Common::String &title,
+		const Common::String &subtitle, const Common::String &prompt,
+		char *target, int maxLength) {
+	if (!initializeWindowManager() || !target || maxLength < 1)
+		return -1;
+
+	syncPalette();
+	MacNebularDialog dialog(_engine, _resources, _screen, *_windowManager);
+	if (!dialog.load(kCopyProtectionDialog))
+		return -1;
+	dialog.center();
+	dialog.setItemText(2, target);
+	dialog.setItemMaxLength(2, maxLength);
+	dialog.setItemText(3, title);
+	dialog.setItemText(4, subtitle);
+	dialog.setItemText(7, prompt);
+
+	_activeDialog = &dialog;
+	const int result = dialog.runModal(1, 0);
+	_activeDialog = nullptr;
+	if (result != 1)
+		return 1;
+
+	Common::strcpy_s(target, maxLength + 1, dialog.getItemText(2).c_str());
+	return 0;
+}
+
 bool MacNebularMenu::runStoryPasswordDialog(bool leavingLocked) {
 	if (!initializeWindowManager())
 		return false;
diff --git a/engines/mads/nebular/mac_menus.h b/engines/mads/nebular/mac_menus.h
index 3687bf80ff4..25e060e1309 100644
--- a/engines/mads/nebular/mac_menus.h
+++ b/engines/mads/nebular/mac_menus.h
@@ -91,6 +91,9 @@ public:
 	void runSaveDialog(bool saveAs);
 	int runPopupEditor(const Common::Rect &bounds, char *target,
 		int maxLength);
+	int runCopyProtectionDialog(const Common::String &title,
+		const Common::String &subtitle, const Common::String &prompt,
+		char *target, int maxLength);
 	bool runStoryPasswordDialog(bool leavingLocked);
 	void setOuterMenuActive(bool active) { _outerMenuActive = active; }
 	int runDifficultyDialog();
diff --git a/engines/mads/nebular/mac_nebular.cpp b/engines/mads/nebular/mac_nebular.cpp
index e7ee87880a2..9546ec406a1 100644
--- a/engines/mads/nebular/mac_nebular.cpp
+++ b/engines/mads/nebular/mac_nebular.cpp
@@ -1339,6 +1339,13 @@ int MacNebular::editPopup(char *target, int maxLength) {
 	return _menus->runPopupEditor(bounds, target, maxLength);
 }
 
+int MacNebular::runCopyProtectionDialog(const Common::String &title,
+		const Common::String &subtitle, const Common::String &prompt,
+		char *target, int maxLength) {
+	return _menus ? _menus->runCopyProtectionDialog(title, subtitle, prompt,
+		target, maxLength) : -1;
+}
+
 int MacNebular::getTextWidth(FontPtr font, const char *text, int) const {
 	if (!_resources || (font != font_main && font != font_conv))
 		return -1;
@@ -1496,6 +1503,13 @@ void RexNebularEngine::setMacintoshFullFrameActive(bool active) {
 		_macNebular->setFullFrameActive(active);
 }
 
+int RexNebularEngine::runMacintoshCopyProtectionDialog(
+		const Common::String &title, const Common::String &subtitle,
+		const Common::String &prompt, char *target, int maxLength) {
+	return _macNebular ? _macNebular->runCopyProtectionDialog(title,
+		subtitle, prompt, target, maxLength) : -1;
+}
+
 bool RexNebularEngine::drawPopup() {
 	if (!_macNebular)
 		return false;
diff --git a/engines/mads/nebular/mac_nebular.h b/engines/mads/nebular/mac_nebular.h
index 88905825af0..a1ca01f5245 100644
--- a/engines/mads/nebular/mac_nebular.h
+++ b/engines/mads/nebular/mac_nebular.h
@@ -113,6 +113,9 @@ public:
 	void presentScreen(int shakeOffset);
 	void showPopup();
 	int editPopup(char *target, int maxLength);
+	int runCopyProtectionDialog(const Common::String &title,
+		const Common::String &subtitle, const Common::String &prompt,
+		char *target, int maxLength);
 	void hidePopup();
 	int getTextWidth(FontPtr font, const char *text, int spacing) const;
 	bool drawText(FontPtr font, Buffer *target, const char *text, int x,
diff --git a/engines/mads/nebular/nebular.h b/engines/mads/nebular/nebular.h
index 066801a03d9..71c64255cd6 100644
--- a/engines/mads/nebular/nebular.h
+++ b/engines/mads/nebular/nebular.h
@@ -80,6 +80,9 @@ public:
 	void setMacintoshOuterMenuActive(bool active);
 	void notifyMacintoshOuterMenuFrameReady();
 	void setMacintoshFullFrameActive(bool active);
+	int runMacintoshCopyProtectionDialog(const Common::String &title,
+		const Common::String &subtitle, const Common::String &prompt,
+		char *target, int maxLength);
 
 	int main_copy_verify() override;
 	void global_init_code() override;


Commit: 92bf9f8767b7c74efe345d5265bddedce0cc3564
    https://github.com/scummvm/scummvm/commit/92bf9f8767b7c74efe345d5265bddedce0cc3564
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: NEBULAR: Match Macintosh copy protection failure

Skip the DOS-only warning screen after Macintosh copy protection
failure. The native Macintosh controller leaves room 804 directly for
ending 4 and the credits.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/nebular/rooms/room804.cpp


diff --git a/engines/mads/nebular/rooms/room804.cpp b/engines/mads/nebular/rooms/room804.cpp
index d09c6731424..a8aed8a2b14 100644
--- a/engines/mads/nebular/rooms/room804.cpp
+++ b/engines/mads/nebular/rooms/room804.cpp
@@ -141,13 +141,12 @@ static void room_804_daemon() {
 				global[kInSpace] = false;
 				global[kBeamIsUp] = true;
 
-				if (global[kCopyProtectFailed]) {
+				// The DOS release inserts its warning here. The Macintosh
+				// executable proceeds directly from this scene to ending 4.
+				if (global[kCopyProtectFailed] &&
+						g_engine->getPlatform() != Common::kPlatformMacintosh)
 					copy_protection_fail_screen();
-					if (g_engine->getPlatform() == Common::kPlatformMacintosh)
-						win_status = WIN_A_HEAD_POW;
-				} else {
-					win_status = WIN_A_HEAD_POW;
-				}
+				win_status = WIN_A_HEAD_POW;
 				game.going = false;
 			}
 			break;


Commit: 678fc6d2bc4aa4b46a75a098c4fc26031d17d2de
    https://github.com/scummvm/scummvm/commit/678fc6d2bc4aa4b46a75a098c4fc26031d17d2de
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: NEBULAR: Preserve Macintosh frontend ownership

Keep room 990 active while dispatching its frontend viewers and retain
full-frame ownership across native ending and credits sequences. This
avoids transient gameplay handoffs between consecutive frontend stages.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/mads/nebular/mac_frontend.cpp
    engines/mads/nebular/mac_frontend.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_frontend.cpp b/engines/mads/nebular/mac_frontend.cpp
index 41cb00400a9..d477744de2f 100644
--- a/engines/mads/nebular/mac_frontend.cpp
+++ b/engines/mads/nebular/mac_frontend.cpp
@@ -29,19 +29,30 @@ namespace MADS {
 namespace RexNebular {
 namespace MacFrontend {
 
-void runAnimView(RexNebularEngine &engine, const char *resource) {
+static bool acquireFullFrame(RexNebularEngine &engine) {
+	if (engine.isMacintoshFullFrameActive())
+		return false;
+	engine.setMacintoshFullFrameActive(true);
+	return true;
+}
+
+static void releaseFullFrame(RexNebularEngine &engine, bool acquired) {
+	if (acquired)
+		engine.setMacintoshFullFrameActive(false);
+}
+
+static void runAnimViewContent(RexNebularEngine &engine,
+		const char *resource) {
 	AnimView::Presentation presentation;
 	presentation.bufferHeight = 200;
 	presentation.drawBoundaryLines = false;
 	presentation.serviceFramesInline = true;
 
-	engine.setMacintoshFullFrameActive(true);
 	engine.getScreen()->clear();
 	AnimView::animview_main(resource, presentation);
-	engine.setMacintoshFullFrameActive(false);
 }
 
-void runTextView(RexNebularEngine &engine, const char *resource) {
+static void runTextViewContent(const char *resource) {
 	TextView::Presentation presentation;
 	presentation.bufferHeight = 210;
 	presentation.visibleHeight = 200;
@@ -49,14 +60,33 @@ void runTextView(RexNebularEngine &engine, const char *resource) {
 	presentation.drawBoundaryLines = false;
 	presentation.macintoshFullFrame = true;
 
-	engine.setMacintoshFullFrameActive(true);
 	TextView::textview_main(resource, presentation);
-	engine.setMacintoshFullFrameActive(false);
 }
 
-void showCreditsAfterEnding(RexNebularEngine &engine) {
-	// CODE 133 follows Endi 49, 50 and 52 with CRED 1000.
-	runTextView(engine, "credits");
+void runAnimView(RexNebularEngine &engine, const char *resource) {
+	const bool acquired = acquireFullFrame(engine);
+	runAnimViewContent(engine, resource);
+	releaseFullFrame(engine, acquired);
+}
+
+void runTextView(RexNebularEngine &engine, const char *resource) {
+	const bool acquired = acquireFullFrame(engine);
+	runTextViewContent(resource);
+	releaseFullFrame(engine, acquired);
+}
+
+void runEndingSequence(RexNebularEngine &engine, const char *animation,
+		const char *ending, bool showCredits) {
+	// CODE 133 retains its frontend window and palette ownership while it
+	// advances from an ending resource to CRED 1000.
+	const bool acquired = acquireFullFrame(engine);
+	if (animation)
+		runAnimViewContent(engine, animation);
+	if (ending)
+		runTextViewContent(ending);
+	if (showCredits)
+		runTextViewContent("credits");
+	releaseFullFrame(engine, acquired);
 }
 
 } // namespace MacFrontend
diff --git a/engines/mads/nebular/mac_frontend.h b/engines/mads/nebular/mac_frontend.h
index 20ce16a5c14..4939d68fff3 100644
--- a/engines/mads/nebular/mac_frontend.h
+++ b/engines/mads/nebular/mac_frontend.h
@@ -31,7 +31,8 @@ namespace MacFrontend {
 
 void runAnimView(RexNebularEngine &engine, const char *resource);
 void runTextView(RexNebularEngine &engine, const char *resource);
-void showCreditsAfterEnding(RexNebularEngine &engine);
+void runEndingSequence(RexNebularEngine &engine, const char *animation,
+	const char *ending, bool showCredits);
 
 } // namespace MacFrontend
 } // namespace RexNebular
diff --git a/engines/mads/nebular/mac_nebular.cpp b/engines/mads/nebular/mac_nebular.cpp
index 9546ec406a1..8e42577a2b2 100644
--- a/engines/mads/nebular/mac_nebular.cpp
+++ b/engines/mads/nebular/mac_nebular.cpp
@@ -1503,6 +1503,10 @@ void RexNebularEngine::setMacintoshFullFrameActive(bool active) {
 		_macNebular->setFullFrameActive(active);
 }
 
+bool RexNebularEngine::isMacintoshFullFrameActive() const {
+	return _macNebular && _macNebular->isFullFrameActive();
+}
+
 int RexNebularEngine::runMacintoshCopyProtectionDialog(
 		const Common::String &title, const Common::String &subtitle,
 		const Common::String &prompt, char *target, int maxLength) {
diff --git a/engines/mads/nebular/mac_nebular.h b/engines/mads/nebular/mac_nebular.h
index a1ca01f5245..ee56d302fed 100644
--- a/engines/mads/nebular/mac_nebular.h
+++ b/engines/mads/nebular/mac_nebular.h
@@ -101,6 +101,7 @@ public:
 	void setPreferencesAtStartup(bool show, bool persist);
 	void setStoryLocked(bool locked, const Common::String &password);
 	void setFullFrameActive(bool active);
+	bool isFullFrameActive() const { return _fullFrameActive; }
 	void setOuterMenuActive(bool active);
 	void notifyOuterMenuFrameReady();
 	Common::Point screenToGame(const Common::Point &point) const;
diff --git a/engines/mads/nebular/main.cpp b/engines/mads/nebular/main.cpp
index 5078fc0dcea..215585ec491 100644
--- a/engines/mads/nebular/main.cpp
+++ b/engines/mads/nebular/main.cpp
@@ -351,7 +351,9 @@ void nebular_main() {
 			engine->setMacintoshOuterMenuActive(
 				g_engine->getPlatform() == Common::kPlatformMacintosh);
 			main_menu_main();
-			engine->setMacintoshOuterMenuActive(false);
+			if (g_engine->getPlatform() != Common::kPlatformMacintosh ||
+					selected_item < 2 || selected_item > 4)
+				engine->setMacintoshOuterMenuActive(false);
 
 			// Native CODE 133 performs this transition inside
 			// main_menu_main(), before dispatching the selected action.
@@ -420,28 +422,41 @@ void nebular_main() {
 			break;
 
 		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);
+				MacFrontend::runEndingSequence(*engine, "@rexend1",
+					"ending1", true);
+			else {
+				run_full_frame_animview(engine, "@rexend1");
+				run_full_frame_textview(engine, "ending1");
+			}
 			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);
+				MacFrontend::runEndingSequence(*engine, "@rexend2",
+					"ending2", true);
+			else {
+				run_full_frame_animview(engine, "@rexend2");
+				run_full_frame_textview(engine, "ending2");
+			}
 			return;
 
 		case WIN_ALL_THE_MONEY + 16:
-			run_full_frame_animview(engine, "@rexend3");
-			run_full_frame_textview(engine, "credits");
+			if (g_engine->getPlatform() == Common::kPlatformMacintosh)
+				MacFrontend::runEndingSequence(*engine, "@rexend3",
+					nullptr, true);
+			else {
+				run_full_frame_animview(engine, "@rexend3");
+				run_full_frame_textview(engine, "credits");
+			}
 			return;
 
 		case WIN_A_HEAD_POW + 16:
-			run_full_frame_textview(engine, "ending4");
 			if (g_engine->getPlatform() == Common::kPlatformMacintosh)
-				MacFrontend::showCreditsAfterEnding(*engine);
+				MacFrontend::runEndingSequence(*engine, nullptr,
+					"ending4", true);
+			else
+				run_full_frame_textview(engine, "ending4");
 			return;
 
 		case 5:
diff --git a/engines/mads/nebular/nebular.h b/engines/mads/nebular/nebular.h
index 71c64255cd6..ff810aa122d 100644
--- a/engines/mads/nebular/nebular.h
+++ b/engines/mads/nebular/nebular.h
@@ -80,6 +80,7 @@ public:
 	void setMacintoshOuterMenuActive(bool active);
 	void notifyMacintoshOuterMenuFrameReady();
 	void setMacintoshFullFrameActive(bool active);
+	bool isMacintoshFullFrameActive() const;
 	int runMacintoshCopyProtectionDialog(const Common::String &title,
 		const Common::String &subtitle, const Common::String &prompt,
 		char *target, int maxLength);


Commit: d9c770bebc6367068fb656ef7ff11bd38b8cc960
    https://github.com/scummvm/scummvm/commit/d9c770bebc6367068fb656ef7ff11bd38b8cc960
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: NEBULAR: Match Macintosh startup order

Let the shared startup sequence perform copy verification and
section startup before invoking the Macintosh difficulty dialog. This
also keeps restored games on their existing 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 215585ec491..5a34dbec18b 100644
--- a/engines/mads/nebular/main.cpp
+++ b/engines/mads/nebular/main.cpp
@@ -257,10 +257,6 @@ 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"))
-		((RexNebularEngine *)g_engine)->selectMacintoshDifficulty();
 	global_load_config_parameters();
 
 	if (argc >= 2) {


Commit: 5bc5a6cd5a01e8a062a095227d4cd95c8bdc0da4
    https://github.com/scummvm/scummvm/commit/5bc5a6cd5a01e8a062a095227d4cd95c8bdc0da4
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: NEBULAR: Preserve Macintosh startup backdrop

Keep the neutral frame installed after the outer menu releases
ownership until the first room transition begins. This prevents copy
protection and difficulty dialogs from exposing stale room 990 pixels.

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 8e42577a2b2..c2dd8f27940 100644
--- a/engines/mads/nebular/mac_nebular.cpp
+++ b/engines/mads/nebular/mac_nebular.cpp
@@ -1023,6 +1023,13 @@ void MacNebular::presentScreen(int shakeOffset) {
 			kernel_mode != KERNEL_ACTIVE_CODE)
 		return;
 
+	// The native application keeps the game window hidden while copy
+	// protection and difficulty selection run. Preserve the neutral frame
+	// installed when the outer menu released ownership until the initial room
+	// transition begins.
+	if (_gameplayHandoffPending && !_gameplayHandoffEffectSeen && !kernel.fx)
+		return;
+
 	if (_fullFrameActive) {
 		const int frameY = _useOriginalMenus ? kMacDesktopSceneY : 0;
 		const byte frameBlack = _useOriginalMenus && _menus ?


Commit: 7f873fd68b26a5dec6616427b4722be4f38a6f02
    https://github.com/scummvm/scummvm/commit/7f873fd68b26a5dec6616427b4722be4f38a6f02
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: NEBULAR: Preserve Macintosh TextView handoff

Avoid applying the DOS MCGA reset after a Macintosh full-frame
TextView. Native CODE 133 restores the interface palette and then
returns, including when it immediately chains an ending into the
credits.

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 afbb0614cce..d6d193c9b99 100644
--- a/engines/mads/textview/textview.cpp
+++ b/engines/mads/textview/textview.cpp
@@ -575,7 +575,10 @@ void textview_main(const char *resName, const Presentation &presentation) {
 	g_engine->_soundManager->removeDriver();
 	pal_interface(master_palette);
 	mcga_setpal(&master_palette);
-	mcga_reset();
+	// Native CODE 133 returns after restoring the interface palette. The
+	// additional MCGA reset belongs to the DOS viewer teardown.
+	if (!macintoshFullFrame)
+		mcga_reset();
 
 	buffer_free(&scr_depth);
 	buffer_free(&scr_orig);


Commit: ceeb8b0139380a09ca4e29a36526addc5938d794
    https://github.com/scummvm/scummvm/commit/ceeb8b0139380a09ca4e29a36526addc5938d794
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-14T12:27:52+10:00

Commit Message:
MADS: NEBULAR: Restore Macintosh About presentation

Present room 990 through the native Macintosh composition and keep
modal output ownership until dismissal. Use the visible light menu color
for the Palette Manager foreground rather than indexed room black.

Preserve the cursor stack and restore the previous palette, output,
pause, and frontend state exactly once after presentation closes.

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


diff --git a/engines/mads/nebular/mac_menus.cpp b/engines/mads/nebular/mac_menus.cpp
index 42a25b13a06..99fa018f3e1 100644
--- a/engines/mads/nebular/mac_menus.cpp
+++ b/engines/mads/nebular/mac_menus.cpp
@@ -510,6 +510,50 @@ bool MacNebularMenu::takeAboutRequest() {
 	return requested;
 }
 
+void MacNebularMenu::waitForAboutDismissal() {
+	bool pressed = false;
+	bool released = false;
+	bool pressedMouse = false;
+	uint32 releaseTime = 0;
+	g_system->delayMillis(166);
+	while (!_engine.shouldQuit() &&
+			(!pressed || !released || g_system->getMillis() < releaseTime)) {
+		Common::Event event;
+		while (g_system->getEventManager()->pollEvent(event)) {
+			switch (event.type) {
+			case Common::EVENT_QUIT:
+				_engine.quitGame();
+				break;
+			case Common::EVENT_LBUTTONDOWN:
+				if (!pressed) {
+					pressed = true;
+					pressedMouse = true;
+					releaseTime = g_system->getMillis() + 333;
+				}
+				break;
+			case Common::EVENT_KEYDOWN:
+				if (!pressed) {
+					pressed = true;
+					pressedMouse = false;
+					releaseTime = g_system->getMillis() + 333;
+				}
+				break;
+			case Common::EVENT_LBUTTONUP:
+				if (pressed && pressedMouse)
+					released = true;
+				break;
+			case Common::EVENT_KEYUP:
+				if (pressed && !pressedMouse)
+					released = true;
+				break;
+			default:
+				break;
+			}
+		}
+		g_system->delayMillis(10);
+	}
+}
+
 bool MacNebularMenu::takePreferencesRequest() {
 	const bool requested = _preferencesRequested;
 	_preferencesRequested = false;
diff --git a/engines/mads/nebular/mac_menus.h b/engines/mads/nebular/mac_menus.h
index 25e060e1309..86b33476b6d 100644
--- a/engines/mads/nebular/mac_menus.h
+++ b/engines/mads/nebular/mac_menus.h
@@ -85,6 +85,7 @@ public:
 	void getMenuColors(byte &menuBlack, byte &menuWhite);
 	void setMenuBarHidden(bool hidden);
 	bool takeAboutRequest();
+	void waitForAboutDismissal();
 	bool takePreferencesRequest();
 	bool runPreferencesDialog(bool startup);
 	void runOpenDialog();
diff --git a/engines/mads/nebular/mac_nebular.cpp b/engines/mads/nebular/mac_nebular.cpp
index c2dd8f27940..b45956fa60d 100644
--- a/engines/mads/nebular/mac_nebular.cpp
+++ b/engines/mads/nebular/mac_nebular.cpp
@@ -180,6 +180,19 @@ static void drawMacAboutText(Graphics::ManagedSurface &surface,
 		surface.w - x, color);
 }
 
+static void drawMacAboutRoom(Graphics::ManagedSurface &output,
+		const Graphics::ManagedSurface &picture, int sceneX, int sceneY,
+		int sceneWidth, int sceneHeight, byte backgroundColor) {
+	output.fillRect(output.getBounds(), backgroundColor);
+	for (int y = 0; y < sceneHeight; ++y) {
+		const byte *source = (const byte *)picture.getBasePtr(
+			0, y * picture.h / sceneHeight);
+		byte *target = (byte *)output.getBasePtr(sceneX, sceneY + y);
+		for (int x = 0; x < sceneWidth; ++x)
+			target[x] = source[x * picture.w / sceneWidth];
+	}
+}
+
 static void setMacInterfacePalette(const MacResourceProvider *resources) {
 	if (resources && resources->getNativeInterface()) {
 		const byte *panelPalette = resources->getNativeInterfacePalette();
@@ -708,6 +721,7 @@ void MacNebular::showAbout() {
 	memcpy(savedPalette, _palette, sizeof(Palette));
 	const bool savedFullFrameActive = _fullFrameActive;
 	setFullFrameActive(true);
+	_aboutActive = true;
 	PauseToken pauseToken = _engine.pauseEngine();
 
 	Palette transitionPalette;
@@ -715,78 +729,45 @@ void MacNebular::showAbout() {
 	magic_fade_to_grey(transitionPalette, nullptr,
 		0, 256, 0, 1, 1, 16);
 
-	const byte blackColor = _menus->getBlackColor();
-	_output.fillRect(_output.getBounds(), blackColor);
 	const int sceneX = getSceneX();
-	const int sceneY = getSceneY();
 	const int sceneWidth = getSceneWidth();
 	const int sceneHeight = getSceneHeight();
-	for (int y = 0; y < sceneHeight; ++y) {
-		const byte *source = (const byte *)picture.getBasePtr(
-			0, y * picture.h / sceneHeight);
-		byte *target = (byte *)_output.getBasePtr(sceneX, sceneY + y);
-		for (int x = 0; x < sceneWidth; ++x)
-			target[x] = source[x * picture.w / sceneWidth];
-	}
+	// About owns the full 320x200 viewer and has no inventory panel. Center
+	// its 320x156 room inside that viewer instead of using gameplay's
+	// panel-relative scene position.
+	const int sceneY = kMacDesktopSceneY +
+		(kMacFullFrameHeight - sceneHeight) / 2;
+	drawMacAboutRoom(_output, picture, sceneX, sceneY, sceneWidth,
+		sceneHeight, 0);
+	g_system->copyRectToScreen(_output.getPixels(), _output.pitch,
+		0, 0, _output.w, _output.h);
+	g_system->updateScreen();
+	magic_fade_from_grey((RGBcolor *)transitionPalette, aboutPalette,
+		0, 256, 0, 1, 1, 16);
 
+	byte blackColor;
+	byte whiteColor;
+	_menus->getMenuColors(blackColor, whiteColor);
+	drawMacAboutRoom(_output, picture, sceneX, sceneY, sceneWidth,
+		sceneHeight, blackColor);
+	// CODE 6 selects Palette Manager entry zero before drawing the About
+	// strings. That semantic entry is the light text color; it is not the
+	// room picture's indexed-color slot zero, which is black in this port.
 	drawMacAboutText(_output, *titleFont, "From MicroProse Software.",
-		sceneX + titleX, sceneY + titleY, blackColor);
+		sceneX + titleX, sceneY + titleY, whiteColor);
 	drawMacAboutText(_output, *textFont, "For hints and help call:",
-		sceneX + textX, sceneY + firstTextY, blackColor);
+		sceneX + textX, sceneY + firstTextY, whiteColor);
 	drawMacAboutText(_output, *textFont, "1 - 900 - 933 - PLAY",
-		sceneX + helpX, sceneY + helpY, blackColor);
+		sceneX + helpX, sceneY + helpY, whiteColor);
 	drawMacAboutText(_output, *textFont, "For customer service call:",
-		sceneX + serviceX, sceneY + serviceY, blackColor);
+		sceneX + serviceX, sceneY + serviceY, whiteColor);
 	drawMacAboutText(_output, *textFont, "1 - 410 - 771 - 1151",
-		sceneX + phoneX, sceneY + phoneY, blackColor);
+		sceneX + phoneX, sceneY + phoneY, whiteColor);
 	_menus->draw();
 	g_system->copyRectToScreen(_output.getPixels(), _output.pitch,
 		0, 0, _output.w, _output.h);
 	g_system->updateScreen();
-	magic_fade_from_grey((RGBcolor *)transitionPalette, aboutPalette,
-		0, 256, 0, 1, 1, 16);
-
-	bool pressed = false;
-	bool released = false;
-	bool pressedMouse = false;
-	uint32 releaseTime = 0;
-	g_system->delayMillis(166);
-	while (!_engine.shouldQuit() &&
-			(!pressed || !released || g_system->getMillis() < releaseTime)) {
-		Common::Event event;
-		while (g_system->getEventManager()->pollEvent(event)) {
-			switch (event.type) {
-			case Common::EVENT_QUIT:
-				_engine.quitGame();
-				break;
-			case Common::EVENT_LBUTTONDOWN:
-				if (!pressed) {
-					pressed = true;
-					pressedMouse = true;
-					releaseTime = g_system->getMillis() + 333;
-				}
-				break;
-			case Common::EVENT_KEYDOWN:
-				if (!pressed) {
-					pressed = true;
-					pressedMouse = false;
-					releaseTime = g_system->getMillis() + 333;
-				}
-				break;
-			case Common::EVENT_LBUTTONUP:
-				if (pressed && pressedMouse)
-					released = true;
-				break;
-			case Common::EVENT_KEYUP:
-				if (pressed && !pressedMouse)
-					released = true;
-				break;
-			default:
-				break;
-			}
-		}
-		g_system->delayMillis(10);
-	}
+	_menus->waitForAboutDismissal();
 
 	if (!_engine.shouldQuit()) {
 		memcpy(transitionPalette, aboutPalette, sizeof(Palette));
@@ -794,7 +775,6 @@ void MacNebular::showAbout() {
 			0, 256, 0, 1, 1, 16);
 	}
 	_output.copyFrom(savedOutput);
-	_menus->draw();
 	g_system->copyRectToScreen(_output.getPixels(), _output.pitch,
 		0, 0, _output.w, _output.h);
 	g_system->updateScreen();
@@ -805,6 +785,7 @@ void MacNebular::showAbout() {
 		mcga_setpal(&savedPalette);
 	}
 	setFullFrameActive(savedFullFrameActive);
+	_aboutActive = false;
 	_engine._screen->markAllDirty();
 }
 
@@ -1017,6 +998,12 @@ void MacNebular::getPalette(RGBcolor *palette, int firstColor,
 }
 
 void MacNebular::presentScreen(int shakeOffset) {
+	// CODE 6 presents About through a separate game window after hiding the
+	// application's current window. While the Rex-local modal owns our single
+	// output surface, keep ordinary MADS presentation from replacing it.
+	if (_aboutActive)
+		return;
+
 	// Keep the original Macintosh composition on its previous frame while
 	// the shared engine initializes the next room and interface.
 	if (_useOriginalMenus && !_fullFrameActive &&
diff --git a/engines/mads/nebular/mac_nebular.h b/engines/mads/nebular/mac_nebular.h
index ee56d302fed..fc56b15cf42 100644
--- a/engines/mads/nebular/mac_nebular.h
+++ b/engines/mads/nebular/mac_nebular.h
@@ -62,6 +62,7 @@ private:
 	bool _storyLocked;
 	Common::String _storyPassword;
 	bool _fullFrameActive = false;
+	bool _aboutActive = false;
 	bool _gameplayHandoffPending = false;
 	bool _gameplayHandoffEffectSeen = false;
 	bool _popupActive = false;




More information about the Scummvm-git-logs mailing list