[Scummvm-git-logs] scummvm master -> 805f259a19d71eb12db1e3b0b9b24c27ee18e8b6
dreammaster
noreply at scummvm.org
Wed Aug 5 02:02:23 UTC 2026
This automated email contains information about 8 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
32e9672fa7 AUDIO: Clamp Macintosh samples to resource size
31a1cc3b18 MADS: NEBULAR: Add Mac resource provider hooks
a387683ccb MADS: NEBULAR: Add Macintosh resource provider
e80c0a09e8 MADS: NEBULAR: Add Macintosh sampled sound
2802bdee00 MADS: Add Mac port presentation hooks
27c40832c7 MADS: NEBULAR: Add initial Macintosh release support
849a761db7 MADS: NEBULAR: Separate Macintosh menu adapters
805f259a19 MADS: NEBULAR: Separate Macintosh presentation code
Commit: 32e9672fa7e438121f752b0f2ae19d2c386e05be
https://github.com/scummvm/scummvm/commit/32e9672fa7e438121f752b0f2ae19d2c386e05be
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-05T12:02:15+10:00
Commit Message:
AUDIO: Clamp Macintosh samples to resource size
Assisted-by: Codex:GPT-5.4
Changed paths:
audio/decoders/mac_snd.cpp
diff --git a/audio/decoders/mac_snd.cpp b/audio/decoders/mac_snd.cpp
index 1345eb6c9dd..376fb537ee4 100644
--- a/audio/decoders/mac_snd.cpp
+++ b/audio/decoders/mac_snd.cpp
@@ -97,6 +97,11 @@ SeekableAudioStream *makeMacSndStream(Common::SeekableReadStream *stream,
}
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);
Commit: 31a1cc3b18cab633f9c17c122c56f967412ea024
https://github.com/scummvm/scummvm/commit/31a1cc3b18cab633f9c17c122c56f967412ea024
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-05T12:02:15+10:00
Commit Message:
MADS: NEBULAR: Add Mac resource provider hooks
Add Macintosh-specific hooks for file, text, and cursor resources.
Preserve the existing resource-loading paths when no provider is
installed.
Assisted-by: Codex:GPT-5.4
Changed paths:
engines/mads/core/env.cpp
engines/mads/core/env.h
engines/mads/core/kernel.cpp
engines/mads/core/mouse.cpp
engines/mads/core/text.cpp
diff --git a/engines/mads/core/env.cpp b/engines/mads/core/env.cpp
index fb36f42651e..9e800cfcf8f 100644
--- a/engines/mads/core/env.cpp
+++ b/engines/mads/core/env.cpp
@@ -60,6 +60,29 @@ static const char env_slash_string[3] = "\\";
static const char env_speech1_string[8] = "SPEECH1";
static const char env_speech2_string[8] = "SPEECH2";
+static EnvResourceProvider *env_resource_provider;
+
+void env_set_resource_provider(EnvResourceProvider *provider) {
+ env_resource_provider = provider;
+}
+
+Common::SeekableReadStream *env_open_text(int32 id, uint16 &unpacked_size) {
+ return env_resource_provider ? env_resource_provider->openText(id, unpacked_size) : nullptr;
+}
+
+int env_get_cursor_count() {
+ return env_resource_provider ? env_resource_provider->getCursorCount() : 0;
+}
+
+bool env_set_cursor(int id) {
+ return env_resource_provider ? env_resource_provider->setCursor(id) : false;
+}
+
+void env_update_cursor() {
+ if (env_resource_provider)
+ env_resource_provider->updateCursor();
+}
+
int env_verify() {
Common::String str = ConfMan.get(MADS_PRIV_ENV);
@@ -313,6 +336,16 @@ Common::SeekableReadStream *env_open(const char *filename, const char *options)
Concat concat;
concat.file_offset = concat.file_size = 0;
+ if (env_resource_provider) {
+ handle = env_resource_provider->open(filename);
+ if (handle) {
+ env_concat_file_size = handle->size();
+ return handle;
+ }
+ if (!env_resource_provider->allowsFallback(filename))
+ return nullptr;
+ }
+
Common::strcpy_s(file_path, filename);
if (env_get_path(load_file, file_path) == NULL) goto done;
@@ -485,6 +518,13 @@ int env_exist(const char *filename) {
Common::strcpy_s(file_name, filename);
mads_strupr(file_name);
+ if (env_resource_provider) {
+ if (env_resource_provider->exists(filename))
+ return true;
+ if (!env_resource_provider->allowsFallback(filename))
+ return false;
+ }
+
// If CD version and checking RAW or RAC file
if (env_search_cd && (strstr(file_name, ".RAW") || strstr(file_name, ".RAC"))) {
diff --git a/engines/mads/core/env.h b/engines/mads/core/env.h
index da5e1cbc156..34ea577f1de 100644
--- a/engines/mads/core/env.h
+++ b/engines/mads/core/env.h
@@ -27,6 +27,21 @@
namespace MADS {
+class EnvResourceProvider {
+public:
+ virtual ~EnvResourceProvider() {}
+
+ virtual Common::SeekableReadStream *open(const char *filename) = 0;
+ virtual bool exists(const char *filename) = 0;
+ virtual bool allowsFallback(const char *) const { return true; }
+ virtual Common::SeekableReadStream *openText(int32, uint16 &) {
+ return nullptr;
+ }
+ virtual int getCursorCount() const { return 0; }
+ virtual bool setCursor(int) { return false; }
+ virtual void updateCursor() {}
+};
+
#define MADS_ENV "MADS"
#define MADS_PRIV_ENV "MADSPRIV"
#define MADS_SOUND_ENV "MADSOUND"
@@ -72,6 +87,11 @@ extern int env_sound_override;
extern long env_concat_file_size; /* Size of last concat file opened */
extern char env_null[7];
+extern void env_set_resource_provider(EnvResourceProvider *provider);
+extern Common::SeekableReadStream *env_open_text(int32 id, uint16 &unpacked_size);
+extern int env_get_cursor_count();
+extern bool env_set_cursor(int id);
+extern void env_update_cursor();
extern int env_verify();
extern Common::SeekableReadStream *env_open(const char *file_path, const char *options = "rb");
diff --git a/engines/mads/core/kernel.cpp b/engines/mads/core/kernel.cpp
index 9828411d939..fa0fefe5a2d 100644
--- a/engines/mads/core/kernel.cpp
+++ b/engines/mads/core/kernel.cpp
@@ -423,8 +423,19 @@ int kernel_game_startup(int game_video_mode, int load_flag,
mcga_setpal_range(&master_palette, 0, 4);
}
- // Load cursor sprite series
- cursor = sprite_series_load("*CURSOR.SS", PAL_MAP_RESERVED);
+ // The Macintosh release exposes native system cursors without a MADS
+ // sprite series. Keep a lightweight header so the shared cursor-selection
+ // logic can retain its existing range checks.
+ const int nativeCursorCount = env_get_cursor_count();
+ if (nativeCursorCount > 0) {
+ cursor = (SeriesPtr)mem_get_name(sizeof(Series), "CURSOR");
+ if (cursor) {
+ memset(cursor, 0, sizeof(Series));
+ cursor->num_sprites = nativeCursorCount;
+ }
+ } else {
+ cursor = sprite_series_load("*CURSOR.SS", PAL_MAP_RESERVED);
+ }
if (cursor == NULL) {
goto done;
}
diff --git a/engines/mads/core/mouse.cpp b/engines/mads/core/mouse.cpp
index cb7764cfcc0..ed93421741b 100644
--- a/engines/mads/core/mouse.cpp
+++ b/engines/mads/core/mouse.cpp
@@ -23,6 +23,7 @@
#include "graphics/cursorman.h"
#include "mads/core/general.h"
#include "mads/core/buffer.h"
+#include "mads/core/env.h"
#include "mads/core/matte.h"
#include "mads/core/mouse.h"
#include "mads/core/timer.h"
@@ -86,6 +87,8 @@ void mouse_init_cycle() {
}
void mouse_begin_cycle(int double_flag) {
+ env_update_cursor();
+
if (double_flag) mouse_check_double();
mouse_old_x = mouse_x;
@@ -124,6 +127,9 @@ void mouse_end_cycle(int double_flag, int timing_flag) {
}
void mouse_cursor_sprite(SeriesPtr series, int id) {
+ if (env_set_cursor(id))
+ return;
+
byte work_area[17][17];
Buffer load_buffer = { 17, 17, &work_area[0][0] };
int hot_x = 0, hot_y = 0, count;
@@ -251,6 +257,3 @@ void mouse_hard_cursor_mode(int mode, Palette mypal) {
}
} // namespace MADS
-
-
-
diff --git a/engines/mads/core/text.cpp b/engines/mads/core/text.cpp
index e9419a74da3..5ec240bb0f9 100644
--- a/engines/mads/core/text.cpp
+++ b/engines/mads/core/text.cpp
@@ -95,6 +95,28 @@ char text_command_demonstrative[14] = TEXT_COMMAND_DEMONSTRATIVE;
extern int16 global[GLOBAL_LIST_SIZE];
+static TextPtr unpackText(Common::SeekableReadStream *handle, uint16 length, bool dcl) {
+ TextPtr result = (TextPtr)mem_get_name(length + sizeof(word), "$text$");
+ if (!result)
+ return nullptr;
+
+ result->length = length;
+ if (dcl) {
+ if (!pack_dcl_explode(handle, (byte *)&result->text[0], length)) {
+ mem_free(result);
+ return nullptr;
+ }
+ } else {
+ pack_strategy = PACK_PFAB;
+ if (pack_data(PACK_EXPLODE, length, FROM_DISK, handle,
+ TO_MEMORY, &result->text[0]) != length) {
+ mem_free(result);
+ return nullptr;
+ }
+ }
+
+ return result;
+}
TextPtr text_load(long id) {
TextPtr result = NULL;
@@ -110,6 +132,12 @@ TextPtr text_load(long id) {
dir.id = dir.file_offset = dir.length = 0;
mem_last_alloc_loader = MODULE_TEXT_LOADER;
+ handle = env_open_text(id, dir.length);
+ if (handle) {
+ result = unpackText(handle, dir.length, false);
+ goto done;
+ }
+
Common::strcpy_s(temp_buf, text_filename);
Common::strcat_s(temp_buf, TEXT_COMPILED);
@@ -128,11 +156,6 @@ TextPtr text_load(long id) {
target_offset = file_offset + dir.file_offset;
- result = (TextPtr)mem_get_name(dir.length + sizeof(word), "$text$");
- if (result == NULL) goto done;
-
- result->length = dir.length;
-
fileio_setpos(handle, target_offset);
// The Rex Nebular demo's messages.dat predates MADSPACK 2.0's switch to
@@ -140,18 +163,9 @@ TextPtr text_load(long id) {
// (MADSPACK 1.0). Route it to a dedicated decoder rather than risking
// the shared pack_data() path used by every other game.
if (g_engine->getGameID() == GType_RexNebular && g_engine->isDemo()) {
- if (!pack_dcl_explode(handle, (byte *)&result->text[0], dir.length)) {
- mem_free(result);
- result = NULL;
- }
+ result = unpackText(handle, dir.length, true);
} else {
- pack_strategy = PACK_PFAB;
- if (pack_data(PACK_EXPLODE, dir.length,
- FROM_DISK, handle,
- TO_MEMORY, &result->text[0]) != (int)dir.length) {
- mem_free(result);
- result = NULL;
- }
+ result = unpackText(handle, dir.length, false);
}
done:
Commit: a387683ccbbed3e913fd9d0fce641e22997ce872
https://github.com/scummvm/scummvm/commit/a387683ccbbed3e913fd9d0fce641e22997ce872
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-05T12:02:15+10:00
Commit Message:
MADS: NEBULAR: Add Macintosh resource provider
Read Rex data, messages, artwork, fonts, and cursors from the Macintosh
resource forks.
Assisted-by: Codex:GPT-5.4
Changed paths:
A engines/mads/nebular/mac_resources.cpp
A engines/mads/nebular/mac_resources.h
engines/mads/module.mk
diff --git a/engines/mads/module.mk b/engines/mads/module.mk
index b5c1f97d97a..63d5209256a 100644
--- a/engines/mads/module.mk
+++ b/engines/mads/module.mk
@@ -73,6 +73,7 @@ MODULE_OBJS := \
nebular/global.o \
nebular/main.o \
nebular/main_menu.o \
+ nebular/mac_resources.o \
nebular/menus.o \
nebular/popup.o \
nebular/mads/mads.o \
diff --git a/engines/mads/nebular/mac_resources.cpp b/engines/mads/nebular/mac_resources.cpp
new file mode 100644
index 00000000000..b688682236e
--- /dev/null
+++ b/engines/mads/nebular/mac_resources.cpp
@@ -0,0 +1,641 @@
+/* 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/debug.h"
+#include "common/endian.h"
+#include "common/macresman.h"
+#include "common/memstream.h"
+#include "common/system.h"
+#include "common/textconsole.h"
+#include "graphics/cursorman.h"
+#include "graphics/font.h"
+#include "graphics/macgui/macfontmanager.h"
+#include "graphics/macgui/macwindowmanager.h"
+#include "graphics/surface.h"
+#include "image/iff.h"
+#include "mads/core/color.h"
+#include "mads/core/inter.h"
+#include "mads/core/pack.h"
+#include "mads/nebular/mac_resources.h"
+
+namespace MADS {
+namespace RexNebular {
+
+static const char *const kContainerNames[] = {
+ nullptr,
+ "Rex Nebular",
+ "Rex Global Data",
+ "Rex Sound Data",
+ "Rex Section I Data",
+ "Rex Section II Data",
+ "Rex Section III Data",
+ "Rex Section IV Data",
+ "Rex Section V Data",
+ "Rex Section VI Data",
+ "Rex Section VII Data",
+ "Rex Section VIII Data",
+ "Rex Section IX Data"
+};
+
+static uint32 makeRoomType(char prefix, int room) {
+ return MKTAG(prefix, '0' + room / 100, '0' + (room / 10) % 10, '0' + room % 10);
+}
+
+static bool parseDecimal(const Common::String &text, int &value) {
+ if (text.empty())
+ return false;
+
+ value = 0;
+ for (uint i = 0; i < text.size(); ++i) {
+ if (text[i] < '0' || text[i] > '9')
+ return false;
+ value = value * 10 + text[i] - '0';
+ }
+ return true;
+}
+
+static uint16 suffixToID(const Common::String &suffix) {
+ if (suffix.size() == 1)
+ return (byte)suffix[0];
+ if (suffix.size() == 2)
+ return ((byte)suffix[0] << 8) | (byte)suffix[1];
+ return 0;
+}
+
+// Synthetic platform resources still pass through the standard MADS loader,
+// so expose them as ordinary one-record, uncompressed packet files.
+static byte *createUncompressedPack(uint32 payloadSize, byte priority) {
+ byte *data = (byte *)calloc(PackList::SIZE + payloadSize, 1);
+ if (!data)
+ return nullptr;
+
+ memcpy(data, PACK_ID_STRING, PACK_ID_LENGTH);
+ WRITE_LE_UINT16(data + PACK_ID_LENGTH, 1);
+ data[PACK_HEADER] = PACK_NONE;
+ data[PACK_HEADER + 1] = priority;
+ WRITE_LE_UINT32(data + PACK_HEADER + 2, payloadSize);
+ WRITE_LE_UINT32(data + PACK_HEADER + 6, payloadSize);
+ return data;
+}
+
+struct GlobalSeriesFamily {
+ const char *prefix;
+ uint32 type;
+};
+
+struct RoomSeriesAlias {
+ const char *filename;
+ int room;
+ char type;
+ uint16 id;
+};
+
+static const GlobalSeriesFamily kGlobalSeriesFamilies[] = {
+ { "RXMRD_", MKTAG('R', 'M', 'R', 'D') },
+ { "RXSW_", MKTAG('R', 'S', 'W', '_') },
+ { "RXCD_", MKTAG('R', 'X', 'C', 'D') },
+ { "RXCL_", MKTAG('R', 'X', 'C', 'L') },
+ { "RXKIC_", MKTAG('R', 'X', 'K', 'I') },
+ { "RXMBD_", MKTAG('R', 'X', 'M', 'B') },
+ { "RXMRC_", MKTAG('R', 'M', 'R', 'C') },
+ { "RXMSU_", MKTAG('R', 'X', 'M', 'S') },
+ { "RXMTP_", MKTAG('R', 'X', 'M', 'T') },
+ { "RXM_", MKTAG('R', 'X', 'M', '_') },
+ { "RXRC_", MKTAG('R', 'X', 'R', 'C') },
+ { "RXRD_", MKTAG('R', 'X', 'R', 'D') },
+ { "RXSWRC_", MKTAG('R', 'S', 'W', 'R') },
+ { "RXBOT_", MKTAG('R', 'X', 'B', 'O') },
+ { "RXTH_", MKTAG('R', 'X', 'T', 'H') },
+ { "RXMRO_", MKTAG('R', 'M', 'R', 'O') },
+ { "RXTY_", MKTAG('R', 'X', 'T', 'Y') },
+ { "ROXBD_", MKTAG('R', 'O', 'X', 'B') },
+ { "ROXCD_", MKTAG('R', 'O', 'C', 'D') },
+ { "ROXCL_", MKTAG('R', 'O', 'C', 'L') },
+ { "ROXHAN", MKTAG('R', 'O', 'X', 'H') },
+ { "ROXRC_", MKTAG('R', 'O', 'R', 'C') },
+ { "ROXRD_", MKTAG('R', 'O', 'R', 'D') },
+ { "ROXTP_", MKTAG('R', 'O', 'X', 'T') },
+ { "ROX_", MKTAG('R', 'O', 'X', '_') },
+ { "GRD1_", MKTAG('G', 'R', 'D', '1') },
+ { "GRD2_", MKTAG('G', 'R', 'D', '2') },
+ { "GRDRC2_", MKTAG('G', 'R', 'D', 'R') }
+};
+
+// These names cannot be derived from the resource catalog alone: the Mac
+// resource IDs retain shortened or renamed DOS identifiers used by the
+// shared scripts.
+static const RoomSeriesAlias kRoomSeriesAliases[] = {
+ { "RM101S.AA", 101, 'A', 'A' },
+ { "RM313A_2.SS", 313, 'S', MKTAG16('A', '2') },
+ { "RM313A_3.SS", 313, 'S', MKTAG16('A', '3') },
+ { "RM313A_6.SS", 313, 'S', MKTAG16('A', '6') },
+ { "RM313A_8.SS", 313, 'S', MKTAG16('A', '8') },
+ { "RM313A_9.SS", 313, 'S', MKTAG16('A', '9') },
+ { "RM604R1.AA", 604, 'A', MKTAG16('A', '1') },
+ { "RM971K0A.SS", 971, 'S', MKTAG16('K', '0') },
+ { "RM971K3A.SS", 971, 'S', MKTAG16('K', '3') },
+ { "RM971K4A.SS", 971, 'S', MKTAG16('K', '4') },
+ { "RM979.SS", 979, 'S', MKTAG16('.', 'S') },
+ { "RM980.SS", 980, 'S', MKTAG16('.', 'S') }
+};
+
+MacResourceProvider::MacResourceProvider() {
+ for (uint i = kApplicationContainer; i <= kSection9Container; ++i)
+ _containers[i] = new Common::MacResManager();
+}
+
+MacResourceProvider::~MacResourceProvider() {
+ for (uint i = kApplicationContainer; i <= kSection9Container; ++i)
+ delete _containers[i];
+ delete _fontManager;
+}
+
+bool MacResourceProvider::load() {
+ for (uint i = kApplicationContainer; i <= kSection9Container; ++i) {
+ if (!_containers[i]->open(Common::Path(kContainerNames[i]))) {
+ warning("Could not open Macintosh resource container '%s'", kContainerNames[i]);
+ return false;
+ }
+ }
+
+ delete _fontManager;
+ _fontManager = new Graphics::MacFontManager(
+ Graphics::MacGUIConstants::kWMModeForceMacFonts, Common::EN_ANY);
+ return true;
+}
+
+Common::MacResManager *MacResourceProvider::getContainer(Container container) const {
+ if (container < kApplicationContainer || container > kSection9Container)
+ return nullptr;
+ return _containers[container];
+}
+
+const Graphics::Font *MacResourceProvider::getDialogFont() {
+ if (!_fontManager)
+ return nullptr;
+
+ // The native popup code selects regular Geneva at 9 points. This is
+ // distinct from the bold 10-point font adapted for the shared MADS scene
+ // renderer by createFontResource().
+ return _fontManager->getFont(Graphics::MacFont(
+ Graphics::kMacFontGeneva, 9, Graphics::kMacFontRegular));
+}
+
+const Graphics::Font *MacResourceProvider::getInterfaceFont() {
+ if (!_fontManager)
+ return nullptr;
+
+ // CODE 7 selects bold Geneva at 10 points for the native interface.
+ // Use it directly instead of enlarging text from the compatibility surface.
+ return _fontManager->getFont(Graphics::MacFont(
+ Graphics::kMacFontGeneva, 10, Graphics::kMacFontBold));
+}
+
+MacResourceProvider::ResourceID MacResourceProvider::mapResource(const Common::String &filename) {
+ Common::String name(filename);
+ name.toUppercase();
+ if (name.hasPrefix("*"))
+ name.deleteChar(0);
+
+ ResourceID result;
+ if (name == "VOCAB.DAT")
+ return { kGlobalContainer, MKTAG('V', 'o', 'c', 'a'), 1000 };
+ if (name == "OBJECTS.DAT")
+ return { kGlobalContainer, MKTAG('O', 'b', 'j', 'e'), 1000 };
+ if (name == "QUOTES.DAT")
+ return { kGlobalContainer, MKTAG('Q', 'u', 'o', 't'), 1000 };
+ if (name == "HOGANUS.DAT")
+ return { kGlobalContainer, MKTAG('H', 'o', 'g', 'a'), 1000 };
+ if (name == "ERRORS.DB")
+ return { kGlobalContainer, MKTAG('D', 'a', 'b', 'a'), 0x4552 };
+ if (name == "MODULES.DB")
+ return { kGlobalContainer, MKTAG('D', 'a', 'b', 'a'), 0x4d4f };
+ if (name == "REXHAND.SS")
+ return { kGlobalContainer, MKTAG('R', 'e', 'x', 'h'), 1000 };
+ if (name == "METHAND.SS")
+ return { kGlobalContainer, MKTAG('M', 'e', 't', 'h'), 1000 };
+ if (name == "BTSPIN.SS")
+ return { kGlobalContainer, MKTAG('B', 't', 's', 'p'), 1000 };
+
+ if (name.hasPrefix("ENDING") && name.hasSuffix(".TXR") && name.size() == 11 &&
+ name[6] >= '0' && name[6] <= '9') {
+ return { kGlobalContainer, MKTAG('E', 'n', 'd', 'i'), (uint16)name[6] };
+ }
+ if (name.hasPrefix("WARN") && name.hasSuffix(".DAT") && name.size() == 9 &&
+ name[4] >= '0' && name[4] <= '9') {
+ return { kGlobalContainer, MKTAG('W', 'a', 'r', 'n'), (uint16)name[4] };
+ }
+
+ if (name.hasPrefix("OB") && name.hasSuffix(".SS")) {
+ Common::String number = name.substr(2, 3);
+ int object = 0;
+ if (parseDecimal(number, object) && object <= 999) {
+ const bool inventory = name.size() == 9 && name[5] == 'I';
+ if ((inventory && name == Common::String::format("OB%03dI.SS", object)) ||
+ (!inventory && name == Common::String::format("OB%03d.SS", object))) {
+ return { kGlobalContainer,
+ inventory ? MKTAG('O', 'b', 's', 'i') : MKTAG('O', 'b', 's', 's'),
+ (uint16)object };
+ }
+ }
+ }
+
+ if (name.size() == 5 && name[0] == 'I' && name.hasSuffix(".AA") &&
+ name[1] >= '0' && name[1] <= '9') {
+ return { kGlobalContainer, MKTAG('A', 'i', ' ', ' '),
+ (uint16)(1000 + name[1]) };
+ }
+
+ if (name.hasSuffix(".SS")) {
+ const Common::String base = name.substr(0, name.size() - 3);
+ for (uint i = 0; i < ARRAYSIZE(kGlobalSeriesFamilies); ++i) {
+ const Common::String prefix(kGlobalSeriesFamilies[i].prefix);
+ if (!base.hasPrefix(prefix))
+ continue;
+ const Common::String suffix = base.substr(prefix.size());
+ if (suffix.size() >= 1 && suffix.size() <= 2) {
+ return { kGlobalContainer, kGlobalSeriesFamilies[i].type,
+ suffixToID(suffix) };
+ }
+ }
+ }
+
+ for (uint i = 0; i < ARRAYSIZE(kRoomSeriesAliases); ++i) {
+ const RoomSeriesAlias &alias = kRoomSeriesAliases[i];
+ if (name == alias.filename) {
+ return { (Container)(kSection1Container + alias.room / 100 - 1),
+ makeRoomType(alias.type, alias.room), alias.id };
+ }
+ }
+
+ if (name.hasPrefix("SC") && name.hasSuffix(".SS")) {
+ int section = 0;
+ const Common::String suffix = name.substr(5, name.size() - 8);
+ if (parseDecimal(name.substr(2, 3), section) && section >= 1 && section <= 9 &&
+ suffix.size() >= 1 && suffix.size() <= 2) {
+ return { (Container)(kSection1Container + section - 1),
+ makeRoomType('S', section), suffixToID(suffix) };
+ }
+ }
+
+ if (!name.hasPrefix("RM") || name.size() < 8)
+ return result;
+
+ int room = 0;
+ if (!parseDecimal(name.substr(2, 3), room) || room < 100 || room > 999)
+ return result;
+
+ const int section = room / 100;
+ if (section < 1 || section > 9)
+ return result;
+ result.container = (Container)(kSection1Container + section - 1);
+
+ const int extensionAt = name.findLastOf('.');
+ if (extensionAt < 5)
+ return ResourceID();
+ const Common::String suffix = name.substr(5, extensionAt - 5);
+ const Common::String extension = name.substr(extensionAt);
+
+ if (suffix.empty() && extension == ".ART") {
+ result.type = MKTAG('R', 'a', 'r', 't');
+ result.id = room;
+ } else if (suffix.empty() && extension == ".DAT") {
+ result.type = MKTAG('R', 'd', 'a', 't');
+ result.id = room;
+ } else if (suffix.empty() && extension == ".HH") {
+ result.type = MKTAG('R', 'h', 'o', 't');
+ result.id = room;
+ } else if (!suffix.empty() && suffix.size() <= 2 && extension == ".SS") {
+ result.type = makeRoomType('S', room);
+ result.id = suffixToID(suffix);
+ } else if (!suffix.empty() && suffix.size() <= 2 && extension == ".AA") {
+ result.type = makeRoomType('A', room);
+ result.id = suffixToID(suffix);
+ } else {
+ return ResourceID();
+ }
+
+ return result;
+}
+
+Common::SeekableReadStream *MacResourceProvider::open(const char *filename) {
+ Common::String name(filename);
+ name.toUppercase();
+ if (name.hasPrefix("*"))
+ name.deleteChar(0);
+ if (name == "FONTMAIN.FF" || name == "FONTINTR.FF" ||
+ name == "FONTCONV.FF" || name == "FONTMISC.FF" ||
+ name == "FONTTELE.FF") {
+ return createFontResource();
+ }
+ if (name.size() >= 6 && name[0] == 'I' &&
+ name[1] >= '0' && name[1] <= '9' &&
+ (name.substr(2) == ".INT" || name.substr(2) == "A.INT")) {
+ return createInterfaceResource(name[1] - '0');
+ }
+
+ const ResourceID resource = mapResource(filename);
+ if (!resource.isValid()) {
+ debug(2, "Macintosh Rex resource is not mapped: '%s'", filename);
+ return nullptr;
+ }
+
+ Common::MacResManager *container = getContainer(resource.container);
+ Common::SeekableReadStream *stream =
+ container ? container->getResource(resource.type, resource.id) : nullptr;
+ if (!stream) {
+ debug(2, "Macintosh Rex resource is missing: '%s' (container %d, type %08x, ID %u)",
+ filename, resource.container, resource.type, resource.id);
+ }
+ return stream;
+}
+
+Common::SeekableReadStream *MacResourceProvider::createFontResource() {
+ if (!_fontManager)
+ return nullptr;
+
+ // 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));
+ if (!font)
+ return nullptr;
+
+ enum {
+ kGlyphCount = 128,
+ kHeaderSize = 2 + kGlyphCount + kGlyphCount * 2
+ };
+
+ const int height = MIN<int>(font->getFontHeight(), 200);
+ byte widths[kGlyphCount];
+ uint16 offsets[kGlyphCount];
+ Common::Array<byte> pixels;
+ byte maxWidth = 0;
+
+ for (int index = 0; index < kGlyphCount; ++index) {
+ const uint32 character = index + 1;
+ const int width = CLIP<int>(font->getCharWidth(character), 0, 255);
+ widths[index] = width;
+ maxWidth = MAX<byte>(maxWidth, width);
+ offsets[index] = kHeaderSize + pixels.size();
+ if (!width)
+ continue;
+
+ Graphics::Surface glyph;
+ glyph.create(width, height, Graphics::PixelFormat::createFormatCLUT8());
+ memset(glyph.getPixels(), 0, glyph.pitch * glyph.h);
+ font->drawChar(&glyph, character, 0, 0, 1);
+
+ const int rowBytes = (width + 3) / 4;
+ for (int y = 0; y < height; ++y) {
+ for (int byteIndex = 0; byteIndex < rowBytes; ++byteIndex) {
+ byte packed = 0;
+ for (int pixel = 0; pixel < 4; ++pixel) {
+ const int x = byteIndex * 4 + pixel;
+ if (x < width && *(const byte *)glyph.getBasePtr(x, y))
+ packed |= 1 << (6 - pixel * 2);
+ }
+ pixels.push_back(packed);
+ }
+ }
+ glyph.free();
+ }
+
+ const uint32 fontSize = kHeaderSize + pixels.size();
+ const uint32 size = PackList::SIZE + fontSize;
+ byte *data = createUncompressedPack(fontSize, PACK_PRIORITY_FONTS);
+ if (!data)
+ return nullptr;
+
+ byte *fontData = data + PackList::SIZE;
+ fontData[0] = height;
+ fontData[1] = maxWidth;
+ memcpy(fontData + 2, widths, sizeof(widths));
+ for (int index = 0; index < kGlyphCount; ++index)
+ WRITE_LE_UINT16(fontData + 2 + kGlyphCount + index * 2, offsets[index]);
+ memcpy(fontData + kHeaderSize, pixels.data(), pixels.size());
+
+ return new Common::MemoryReadStream(data, size, DisposeAfterUse::YES);
+}
+
+Common::SeekableReadStream *MacResourceProvider::createInterfaceResource(int interfaceID) {
+ // The Macintosh port replaces each DOS I#.INT bitmap with an InBx ILBM.
+ // Adapt its 512x88 native interface to the shared 320x44 MADS renderer;
+ // the images use only the first eight palette indices.
+ Common::SeekableReadStream *source = _containers[kGlobalContainer]->getResource(
+ MKTAG('I', 'n', 'B', 'x'), 1000 + interfaceID);
+ if (!source)
+ return nullptr;
+
+ ::Image::IFFDecoder decoder;
+ const bool loaded = decoder.loadStream(*source);
+ delete source;
+ const Graphics::Surface *surface = decoder.getSurface();
+ if (!loaded || !surface || surface->format.bytesPerPixel != 1 ||
+ surface->w <= 0 || surface->h <= 0 || decoder.getPalette().size() < 16) {
+ warning("Invalid Macintosh Rex interface resource I%d", interfaceID);
+ return nullptr;
+ }
+
+ // Keep the unscaled native panel for the Macintosh presentation path.
+ // The synthetic pack below exists only so the shared interface state
+ // machine can continue operating in its original 320x44 coordinate space.
+ _nativeInterface.copyFrom(*surface);
+
+ const uint32 payloadSize = sizeof(Color) * 16 + video_x * inter_size_y;
+ const uint32 size = PackList::SIZE + payloadSize;
+ byte *data = createUncompressedPack(payloadSize, PACK_PRIORITY_INTERFACES);
+ if (!data)
+ return nullptr;
+
+ Color *colors = (Color *)(data + PackList::SIZE);
+ const Graphics::Palette &palette = decoder.getPalette();
+ for (uint i = 0; i < 16; ++i) {
+ byte r, g, b;
+ palette.get(i, r, g, b);
+ _nativeInterfacePalette[i * 3 + 0] = r;
+ _nativeInterfacePalette[i * 3 + 1] = g;
+ _nativeInterfacePalette[i * 3 + 2] = b;
+ colors[i].r = PALETTE_8BIT_TO_6BIT(r);
+ colors[i].g = PALETTE_8BIT_TO_6BIT(g);
+ colors[i].b = PALETTE_8BIT_TO_6BIT(b);
+ }
+
+ // InBx pixels use only colors 0 through 7. The native interface code
+ // selects 13, 14, and 15 for its two selection states and normal text,
+ // respectively; supply those QuickDraw foreground colors independently
+ // of the bitmap's otherwise-black unused CMAP entries.
+ colors[8].r = colors[8].g = colors[8].b = 0;
+ colors[13] = colors[1];
+ colors[14] = colors[2];
+ colors[15].r = colors[15].g = colors[15].b = 63;
+
+ byte *target = data + PackList::SIZE + sizeof(Color) * 16;
+ for (int y = 0; y < inter_size_y; ++y) {
+ const int sourceY = ((2 * y + 1) * surface->h) / (2 * inter_size_y);
+ const byte *sourceRow = (const byte *)surface->getBasePtr(0, sourceY);
+ for (int x = 0; x < video_x; ++x) {
+ const int sourceX = ((2 * x + 1) * surface->w) / (2 * video_x);
+ const byte color = sourceRow[sourceX];
+ if (color >= 16) {
+ warning("Macintosh Rex interface I%d uses unsupported color %u", interfaceID, color);
+ free(data);
+ return nullptr;
+ }
+ target[y * video_x + x] = color;
+ }
+ }
+
+ // Retain the exact baseline supplied to the shared interface renderer.
+ // The presentation layer compares its live 320x44 buffer against this
+ // image, then composites only the changed pixels over the native panel.
+ _logicalInterface.create(video_x, inter_size_y,
+ Graphics::PixelFormat::createFormatCLUT8());
+ for (int y = 0; y < inter_size_y; ++y)
+ memcpy(_logicalInterface.getBasePtr(0, y), target + y * video_x, video_x);
+
+ debug(2, "Adapted Macintosh Rex interface I%d from %dx%d to %dx%d",
+ interfaceID, surface->w, surface->h, video_x, inter_size_y);
+ return new Common::MemoryReadStream(data, size, DisposeAfterUse::YES);
+}
+
+bool MacResourceProvider::installCursorResource(uint16 id) {
+ Common::SeekableReadStream *stream =
+ _containers[kApplicationContainer]->getResource(MKTAG('C', 'U', 'R', 'S'), id);
+ if (!stream || stream->size() != 68) {
+ delete stream;
+ return false;
+ }
+
+ byte data[68];
+ stream->read(data, sizeof(data));
+ delete stream;
+
+ Graphics::Surface surface;
+ surface.create(16, 16, Graphics::PixelFormat::createFormatCLUT8());
+ for (int y = 0; y < 16; ++y) {
+ for (int x = 0; x < 16; ++x) {
+ const int bit = y * 16 + x;
+ const byte mask = data[32 + bit / 8] & (0x80 >> (bit & 7));
+ const byte image = data[bit / 8] & (0x80 >> (bit & 7));
+ *(byte *)surface.getBasePtr(x, y) = !mask ? 2 : (image ? 0 : 1);
+ }
+ }
+
+ static const byte kCursorPalette[] = { 0, 0, 0, 255, 255, 255 };
+ CursorMan.replaceCursor(surface, READ_BE_UINT16(data + 66),
+ READ_BE_UINT16(data + 64), 2);
+ CursorMan.replaceCursorPalette(kCursorPalette, 0, 2);
+ surface.free();
+ return true;
+}
+
+bool MacResourceProvider::setCursor(int id) {
+ _cursorID = id;
+ if (id <= 1) {
+ const byte *data, *palette, *mask;
+ int width, height, hotspotX, hotspotY, transparentColor;
+ if (Graphics::MacWindowManager::getBuiltInCursorData(Graphics::kMacCursorArrow,
+ data, palette, mask, width, height, hotspotX, hotspotY, transparentColor)) {
+ CursorMan.replaceCursor(data, width, height, hotspotX, hotspotY,
+ transparentColor, nullptr, mask);
+ CursorMan.replaceCursorPalette(palette, 0, 2);
+ } else {
+ CursorMan.setDefaultArrowCursor();
+ }
+ return true;
+ }
+ if (id == 2) {
+ _waitCursorFrame = 0;
+ _nextCursorTime = g_system->getMillis() + 15;
+ return installCursorResource(1000);
+ }
+ if (id >= 3 && id <= 6)
+ return installCursorResource(3000 + id);
+ return false;
+}
+
+void MacResourceProvider::updateCursor() {
+ if (_cursorID != 2)
+ return;
+
+ const uint32 now = g_system->getMillis();
+ if (now < _nextCursorTime)
+ return;
+
+ const uint32 frames = 1 + (now - _nextCursorTime) / 15;
+ _waitCursorFrame = (_waitCursorFrame + frames) % 34;
+ _nextCursorTime += frames * 15;
+ installCursorResource(1000 + _waitCursorFrame);
+}
+
+bool MacResourceProvider::exists(const char *filename) {
+ Common::SeekableReadStream *stream = open(filename);
+ const bool result = stream != nullptr;
+ delete stream;
+ return result;
+}
+
+bool MacResourceProvider::allowsFallback(const char *filename) const {
+ // Star-prefixed names address the MADS virtual resource hierarchy. The
+ // Macintosh release stores that hierarchy entirely in resource forks and
+ // has no DOS HAG archives to search after a native lookup fails. Ordinary
+ // paths remain eligible for the established filesystem fallback.
+ return !filename || filename[0] != '*';
+}
+
+Common::SeekableReadStream *MacResourceProvider::openText(int32 id, uint16 &unpackedSize) {
+ uint32 type;
+ uint16 resourceID;
+ if (id >= 10000) {
+ type = makeRoomType('M', id / 100);
+ resourceID = 1000 + id % 100;
+ } else {
+ type = MKTAG('M', '0', '0', '0');
+ resourceID = 1000 + id;
+ }
+
+ Common::SeekableReadStream *stream = _containers[kGlobalContainer]->getResource(type, resourceID);
+ if (!stream)
+ return nullptr;
+
+ const int32 storedID = stream->readSint32BE();
+ stream->skip(4); // Offset in the original aggregate message file.
+ unpackedSize = stream->readUint16BE();
+ if (storedID != id || stream->err()) {
+ warning("Invalid Macintosh message resource for ID %d", id);
+ delete stream;
+ return nullptr;
+ }
+
+ return stream;
+}
+
+Common::SeekableReadStream *MacResourceProvider::openSound(int section, int command) {
+ if (section < 1 || section > 9 || command < 0 || command >= 1000)
+ return nullptr;
+ return _containers[kSoundContainer]->getResource(MKTAG('s', 'n', 'd', ' '),
+ section * 1000 + command);
+}
+
+} // namespace RexNebular
+} // namespace MADS
diff --git a/engines/mads/nebular/mac_resources.h b/engines/mads/nebular/mac_resources.h
new file mode 100644
index 00000000000..6ef0b66521d
--- /dev/null
+++ b/engines/mads/nebular/mac_resources.h
@@ -0,0 +1,107 @@
+/* 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_RESOURCES_H
+#define MADS_NEBULAR_MAC_RESOURCES_H
+
+#include "common/scummsys.h"
+#include "common/str.h"
+#include "graphics/managed_surface.h"
+#include "mads/core/env.h"
+
+namespace Common {
+class MacResManager;
+}
+
+namespace Graphics {
+class Font;
+class MacFontManager;
+}
+
+namespace MADS {
+namespace RexNebular {
+
+class MacResourceProvider : public EnvResourceProvider {
+public:
+ enum Container {
+ kInvalidContainer,
+ kApplicationContainer,
+ kGlobalContainer,
+ kSoundContainer,
+ kSection1Container,
+ kSection9Container = kSection1Container + 8
+ };
+
+ struct ResourceID {
+ Container container;
+ uint32 type;
+ uint16 id;
+
+ ResourceID(Container container_ = kInvalidContainer, uint32 type_ = 0, uint16 id_ = 0) :
+ container(container_), type(type_), id(id_) {}
+
+ bool isValid() const { return container != kInvalidContainer; }
+ };
+
+ MacResourceProvider();
+ ~MacResourceProvider() override;
+
+ bool load();
+ Common::SeekableReadStream *open(const char *filename) override;
+ bool exists(const char *filename) override;
+ bool allowsFallback(const char *filename) const override;
+ Common::SeekableReadStream *openText(int32 id, uint16 &unpackedSize) override;
+ Common::SeekableReadStream *openSound(int section, int command);
+ int getCursorCount() const override { return 6; }
+ bool setCursor(int id) override;
+ void updateCursor() override;
+ const Graphics::Surface *getNativeInterface() const {
+ return _nativeInterface.empty() ? nullptr : &_nativeInterface.rawSurface();
+ }
+ const Graphics::Surface *getLogicalInterface() const {
+ return _logicalInterface.empty() ? nullptr : &_logicalInterface.rawSurface();
+ }
+ const Graphics::Font *getDialogFont();
+ const Graphics::Font *getInterfaceFont();
+ const byte *getNativeInterfacePalette() const { return _nativeInterfacePalette; }
+
+ static ResourceID mapResource(const Common::String &filename);
+
+private:
+ Common::MacResManager *_containers[kSection9Container + 1] = {};
+ Graphics::MacFontManager *_fontManager = nullptr;
+ Graphics::ManagedSurface _nativeInterface;
+ Graphics::ManagedSurface _logicalInterface;
+ byte _nativeInterfacePalette[16 * 3] = {};
+ int _cursorID = 0;
+ int _waitCursorFrame = 0;
+ uint32 _nextCursorTime = 0;
+
+ Common::MacResManager *getContainer(Container container) const;
+ Common::SeekableReadStream *createFontResource();
+ Common::SeekableReadStream *createInterfaceResource(int interfaceID);
+ bool installCursorResource(uint16 id);
+};
+
+} // namespace RexNebular
+} // namespace MADS
+
+#endif
Commit: e80c0a09e8b6b65623c604621040b132744efbe1
https://github.com/scummvm/scummvm/commit/e80c0a09e8b6b65623c604621040b132744efbe1
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-05T12:02:15+10:00
Commit Message:
MADS: NEBULAR: Add Macintosh sampled sound
Assisted-by: Codex:GPT-5.4
Changed paths:
A engines/mads/nebular/sound/mac_sound.cpp
A engines/mads/nebular/sound/mac_sound.h
engines/mads/core/sound_manager.h
engines/mads/module.mk
diff --git a/engines/mads/core/sound_manager.h b/engines/mads/core/sound_manager.h
index b592f489a1c..22696f6cf90 100644
--- a/engines/mads/core/sound_manager.h
+++ b/engines/mads/core/sound_manager.h
@@ -48,6 +48,8 @@ protected:
return Common::MemoryReadStream(&_soundData[offset], _soundData.size() - offset);
}
+ explicit SoundDriver(Audio::Mixer *mixer) : _mixer(mixer) {}
+
public:
SoundDriver(Audio::Mixer *mixer, const Common::Path &filename,
int dataOffset, int dataSize);
diff --git a/engines/mads/module.mk b/engines/mads/module.mk
index 63d5209256a..a196e6eda19 100644
--- a/engines/mads/module.mk
+++ b/engines/mads/module.mk
@@ -204,6 +204,7 @@ MODULE_OBJS := \
nebular/sound/asound_nebular.o \
nebular/sound/isound.o \
nebular/sound/isound_nebular.o \
+ nebular/sound/mac_sound.o \
nebular/sound/rsound.o \
nebular/sound/rsound_nebular.o \
nebular/sound/sound.o \
diff --git a/engines/mads/nebular/sound/mac_sound.cpp b/engines/mads/nebular/sound/mac_sound.cpp
new file mode 100644
index 00000000000..c1a9ae4a703
--- /dev/null
+++ b/engines/mads/nebular/sound/mac_sound.cpp
@@ -0,0 +1,123 @@
+/* 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 "audio/audiostream.h"
+#include "audio/decoders/mac_snd.h"
+#include "common/textconsole.h"
+#include "mads/nebular/mac_resources.h"
+#include "mads/nebular/sound/mac_sound.h"
+
+namespace MADS {
+namespace RexNebular {
+namespace Sound {
+
+MacSoundDriver::MacSoundDriver(Audio::Mixer *mixer, MacResourceProvider *resources, int section) :
+ SoundDriver(mixer), _resources(resources), _section(section) {
+}
+
+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);
+ }
+}
+
+void MacSoundDriver::discardFinishedSounds() {
+ for (uint i = _handles.size(); i > 0; --i) {
+ if (!_mixer->isSoundHandleActive(_handles[i - 1]))
+ _handles.remove_at(i - 1);
+ }
+}
+
+int MacSoundDriver::command(int commandId, int param) {
+ switch (commandId) {
+ case 0:
+ return stop();
+ case 6:
+ setPaused(true);
+ return 0;
+ case 7:
+ setPaused(false);
+ return 0;
+ case 8:
+ for (uint i = 0; i < _handles.size(); ++i) {
+ if (_mixer->isSoundHandleActive(_handles[i]))
+ return 1;
+ }
+ return 0;
+ default:
+ 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;
+ }
+
+ 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);
+ return 0;
+}
+
+int MacSoundDriver::stop() {
+ for (uint i = 0; i < _handles.size(); ++i) {
+ if (_mixer->isSoundHandleActive(_handles[i]))
+ _mixer->stopHandle(_handles[i]);
+ }
+ _handles.clear();
+ _paused = false;
+ 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);
+ }
+}
+
+void MacSoundManager::loadDriver(int sectionNum) {
+ removeDriver();
+ _driver = new MacSoundDriver(_mixer, _resources, sectionNum);
+}
+
+} // namespace Sound
+} // namespace RexNebular
+} // namespace MADS
diff --git a/engines/mads/nebular/sound/mac_sound.h b/engines/mads/nebular/sound/mac_sound.h
new file mode 100644
index 00000000000..a3db7bbe8f9
--- /dev/null
+++ b/engines/mads/nebular/sound/mac_sound.h
@@ -0,0 +1,74 @@
+/* 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_SOUND_MAC_SOUND_H
+#define MADS_NEBULAR_SOUND_MAC_SOUND_H
+
+#include "audio/mixer.h"
+#include "mads/core/sound_manager.h"
+
+namespace MADS {
+namespace RexNebular {
+
+class MacResourceProvider;
+
+namespace Sound {
+
+class MacSoundDriver : public SoundDriver {
+private:
+ MacResourceProvider *_resources;
+ int _section;
+ Common::Array<Audio::SoundHandle> _handles;
+ int _volume = Audio::Mixer::kMaxChannelVolume;
+ bool _paused = false;
+
+ void setPaused(bool paused);
+ void discardFinishedSounds();
+
+public:
+ MacSoundDriver(Audio::Mixer *mixer, MacResourceProvider *resources, int section);
+
+ int command(int commandId, int param) override;
+ int stop() override;
+ int poll() override { return 0; }
+ void noise() override {}
+ void setVolume(int volume) override;
+};
+
+class MacSoundManager : public SoundManager {
+private:
+ MacResourceProvider *_resources;
+
+protected:
+ void loadDriver(int sectionNum) override;
+
+public:
+ MacSoundManager(Audio::Mixer *mixer, bool &soundFlag, MacResourceProvider *resources) :
+ SoundManager(mixer, soundFlag), _resources(resources) {}
+
+ void validate() override {}
+};
+
+} // namespace Sound
+} // namespace RexNebular
+} // namespace MADS
+
+#endif
Commit: 2802bdee00b13174fd7ddfb8193837c81ce5bdad
https://github.com/scummvm/scummvm/commit/2802bdee00b13174fd7ddfb8193837c81ce5bdad
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-05T12:02:15+10:00
Commit Message:
MADS: Add Mac port presentation hooks
Add Macintosh-specific coordinate transforms, native framebuffer
presentation, and menu shortcut routing while preserving the existing
DOS behavior.
Assisted-by: Codex:GPT-5.4
Changed paths:
engines/mads/core/game.cpp
engines/mads/core/mcga.cpp
engines/mads/core/mouse.cpp
engines/mads/core/video.cpp
engines/mads/mads.cpp
engines/mads/mads.h
diff --git a/engines/mads/core/game.cpp b/engines/mads/core/game.cpp
index 01a3d81fb32..c23ca85c27d 100644
--- a/engines/mads/core/game.cpp
+++ b/engines/mads/core/game.cpp
@@ -947,6 +947,30 @@ int game_parse_keystroke(int mykey) {
mykey = main_normal_key(mykey);
+ // FIXME: The Macintosh release uses the Mac platform's menu instead of
+ // MADS' DOS menu rooms. Until a native Macintosh menu bar is exposed
+ // through ScummVM, route its menu shortcuts to the Global Menu and avoid
+ // loading game-specific rooms 921-924.
+ if (g_engine->usesScummVMMenu()) {
+ switch (mykey) {
+ case esc_key:
+ case f1_key:
+ case f2_key:
+ case alt_s_key:
+ case f3_key:
+ case alt_r_key:
+ case f4_key:
+ case f5_key:
+ case f6_key:
+ case f7_key:
+ g_engine->openMainMenuDialog();
+ mykey = 0;
+ break;
+ default:
+ break;
+ }
+ }
+
switch (mykey) {
case space_key:
global[player_hyperwalked] = true;
diff --git a/engines/mads/core/mcga.cpp b/engines/mads/core/mcga.cpp
index 9fda820c726..2f98f06cfeb 100644
--- a/engines/mads/core/mcga.cpp
+++ b/engines/mads/core/mcga.cpp
@@ -133,7 +133,7 @@ void mcga_cls(byte inp) {
}
void mcga_retrace() {
- g_engine->getScreen()->update();
+ g_engine->updateDisplay();
}
void mcga_compute_retrace_parameters() {
diff --git a/engines/mads/core/mouse.cpp b/engines/mads/core/mouse.cpp
index ed93421741b..3f3c89bec66 100644
--- a/engines/mads/core/mouse.cpp
+++ b/engines/mads/core/mouse.cpp
@@ -69,7 +69,7 @@ void mouse_hide() {
}
void mouse_force(int x, int y) {
- g_system->warpMouse(x, y);
+ g_engine->warpMouse(x, y);
}
int mouse_in_box(int ul_x, int ul_y, int lr_x, int lr_y) {
diff --git a/engines/mads/core/video.cpp b/engines/mads/core/video.cpp
index b42b02f3486..cec74ad9a0d 100644
--- a/engines/mads/core/video.cpp
+++ b/engines/mads/core/video.cpp
@@ -29,8 +29,7 @@ void video_init(int mode, int set_mode) {
}
void video_update() {
- auto &screen = *g_engine->getScreen();
- screen.update();
+ g_engine->updateDisplay();
}
void video_update(Buffer *from, int from_x, int from_y,
diff --git a/engines/mads/mads.cpp b/engines/mads/mads.cpp
index b1d433b0730..2681a38d499 100644
--- a/engines/mads/mads.cpp
+++ b/engines/mads/mads.cpp
@@ -295,7 +295,7 @@ void MADSEngine::pollEvents() {
}
if (isMouse)
- _mousePos = e.mouse;
+ _mousePos = screenToGame(e.mouse);
if (e.type == Common::EVENT_KEYDOWN && !isSpecialKey(e.kbd.keycode))
_keyEvents.push(e.kbd);
@@ -306,24 +306,37 @@ void MADSEngine::pollEvents() {
}
void MADSEngine::updateScreen() {
+ int offset = 0;
+
// Handle any screen shaking
if (mcga_shakes) {
_shakeRandom = _shakeRandom * 5 + 1;
- int offset = (_shakeRandom >> 8) & 3;
+ offset = (_shakeRandom >> 8) & 3;
if (--mcga_shakes == 0)
offset = 0;
+ }
- // Manually copy the screen with the left hand hide side of the screen of a given offset width shown
- // at the very right. The offset changes to give an effect of shaking the screen
- offset *= 4;
- const byte *buf = (const byte *)_screen->getBasePtr(offset, 0);
- g_system->copyRectToScreen(buf, 320, 0, 0, 320 - offset, 200);
- if (offset > 0)
- g_system->copyRectToScreen(_screen->getPixels(), 320, 320 - offset, 0, offset, 200);
- g_system->updateScreen();
+ presentScreen(offset * 4);
+}
+Common::Point MADSEngine::screenToGame(const Common::Point &point) const {
+ return point;
+}
+
+Common::Point MADSEngine::gameToScreen(const Common::Point &point) const {
+ return point;
+}
+
+void MADSEngine::presentScreen(int shakeOffset) {
+ if (shakeOffset > 0) {
+ // Copy the hidden strip at the left back to the right edge.
+ const byte *buf = (const byte *)_screen->getBasePtr(shakeOffset, 0);
+ g_system->copyRectToScreen(buf, 320, 0, 0, 320 - shakeOffset, 200);
+ g_system->copyRectToScreen(_screen->getPixels(), 320,
+ 320 - shakeOffset, 0, shakeOffset, 200);
+ g_system->updateScreen();
} else {
- // Because the screen is accessed directly via Buffer objects, we need to do a full screen update each frame
+ // Buffer objects access the surface directly, so every frame is dirty.
_screen->markAllDirty();
_screen->update();
}
@@ -391,6 +404,15 @@ int MADSEngine::getMouseState(int &x, int &y) {
return _mouseButtons;
}
+void MADSEngine::warpMouse(int x, int y) {
+ const Common::Point point = gameToScreen(Common::Point(x, y));
+ g_system->warpMouse(point.x, point.y);
+}
+
+void MADSEngine::updateDisplay() {
+ presentScreen(0);
+}
+
uint32 MADSEngine::getMillis() {
pollEvents();
return g_system->getMillis();
diff --git a/engines/mads/mads.h b/engines/mads/mads.h
index 24539d4e439..f17bd9e9dcb 100644
--- a/engines/mads/mads.h
+++ b/engines/mads/mads.h
@@ -74,7 +74,7 @@ private:
void syncGame(Common::Serializer &s);
bool isSpecialKey(Common::KeyCode key) const;
void updateScreen();
-
+
protected:
const MADSGameDescription *_gameDescription;
Common::RandomSource _randomSource;
@@ -87,6 +87,10 @@ protected:
TimerFunction _timerFunction = nullptr;
uint32 _nextTimerTime = 0;
+ virtual Common::Point screenToGame(const Common::Point &point) const;
+ virtual Common::Point gameToScreen(const Common::Point &point) const;
+ virtual void presentScreen(int shakeOffset);
+
bool hasFeature(EngineFeature f) const override;
void pollEvents();
@@ -111,6 +115,7 @@ public:
uint32 getGameFeatures() const;
bool isDemo() const;
bool isCDROM() const;
+ virtual bool usesScummVMMenu() const { return false; }
void readConfigFile();
int getRandomNumber(int maxNumber);
@@ -125,6 +130,8 @@ public:
void flushKeys();
int getMouseState(int &x, int &y);
+ void warpMouse(int x, int y);
+ void updateDisplay();
/**
* Get the elapsed time in milliseconds
Commit: 27c40832c761b57db57abde667252a26035da3e6
https://github.com/scummvm/scummvm/commit/27c40832c761b57db57abde667252a26035da3e6
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-05T12:02:15+10:00
Commit Message:
MADS: NEBULAR: Add initial Macintosh release support
Load the native Rex resources, start the game without DOS menu assets,
and present its scene, interface, dialogs, cursors, and menu behavior at
Macintosh geometry.
Assisted-by: Codex:GPT-5.6-sol
Changed paths:
engines/mads/core/inter.cpp
engines/mads/core/popup.cpp
engines/mads/detection_tables.h
engines/mads/nebular/mac_resources.cpp
engines/mads/nebular/main.cpp
engines/mads/nebular/nebular.cpp
engines/mads/nebular/nebular.h
engines/mads/nebular/popup.cpp
engines/mads/nebular/popup.h
diff --git a/engines/mads/core/inter.cpp b/engines/mads/core/inter.cpp
index 0c2ebbdfaae..f8cf4873c4e 100644
--- a/engines/mads/core/inter.cpp
+++ b/engines/mads/core/inter.cpp
@@ -137,8 +137,14 @@ int picked_word = 0; /* Word most recently picked. */
char inter_sentence[64]; /* Sentence building buffer */
int inter_sentence_handle = -1; /* Sentence message handle (for matte) */
+static int inter_sentence_shadow_handle = -1;
int inter_sentence_changed = false; /* Mark if sentence contents changed */
+enum {
+ kMacSentenceColor = 15,
+ kMacSentenceShadowColor = 8
+};
+
int inter_look_around; /* "Look around" command */
int inter_command; /* Vocab # of sentence's verb */
@@ -244,6 +250,7 @@ void init_inter() {
picked_word = 0;
memset(inter_sentence, 0, sizeof(inter_sentence));
inter_sentence_handle = -1;
+ inter_sentence_shadow_handle = -1;
inter_sentence_changed = false;
inter_look_around = 0;
inter_command = 0;
@@ -2140,6 +2147,10 @@ void inter_main_loop(int allow_input) {
matte_clear_message(inter_sentence_handle);
inter_sentence_handle = -1;
}
+ if (inter_sentence_shadow_handle >= 0) {
+ matte_clear_message(inter_sentence_shadow_handle);
+ inter_sentence_shadow_handle = -1;
+ }
if (g_engine->getGameID() != GType_Forest && (strlen(inter_sentence) > 0) &&
((inter_input_mode == INTER_BUILDING_SENTENCES) || (inter_input_mode == INTER_LIMITED_SENTENCES))) {
use_font = font_main;
@@ -2155,8 +2166,18 @@ void inter_main_loop(int allow_input) {
x = (video_x >> 1) - (width >> 1);
y = (viewing_at_y + scr_work.y - 1) - 12;
- inter_sentence_handle = matte_add_message (use_font, inter_sentence, x, y,
- g_engine->getGameID() == GType_RexNebular ? INTER_MESSAGE_COLOR_REX : INTER_MESSAGE_COLOR,
+ const bool isMacRex = g_engine->getGameID() == GType_RexNebular &&
+ g_engine->getPlatform() == Common::kPlatformMacintosh;
+ if (isMacRex) {
+ // Native large-window captions use a light face with a dark
+ // one-pixel offset, rather than the DOS interface cyan.
+ inter_sentence_shadow_handle = matte_add_message(use_font,
+ inter_sentence, x + 1, y + 1, kMacSentenceShadowColor,
+ use_spacing);
+ }
+ inter_sentence_handle = matte_add_message(use_font, inter_sentence, x, y,
+ isMacRex ? kMacSentenceColor :
+ (g_engine->getGameID() == GType_RexNebular ? INTER_MESSAGE_COLOR_REX : INTER_MESSAGE_COLOR),
use_spacing);
}
inter_sentence_changed = false;
diff --git a/engines/mads/core/popup.cpp b/engines/mads/core/popup.cpp
index c1d8f937738..4faf3044d99 100644
--- a/engines/mads/core/popup.cpp
+++ b/engines/mads/core/popup.cpp
@@ -780,6 +780,10 @@ void popup_destroy() {
int x, y;
int xs, ys;
+ if (g_engine->getGameID() == GType_RexNebular &&
+ g_engine->getPlatform() == Common::kPlatformMacintosh)
+ RexNebular::popup_close();
+
if (box->active && box->screen_saved) {
// Always restore the screen from scr_main â it spans both the game
// area and the interface strip, so no buffer split is required.
diff --git a/engines/mads/detection_tables.h b/engines/mads/detection_tables.h
index ad1173192bb..cf19467145e 100644
--- a/engines/mads/detection_tables.h
+++ b/engines/mads/detection_tables.h
@@ -146,6 +146,29 @@ static const MADSGameDescription gameDescriptions[] = {
GType_RexNebular,
0
},
+
+ {
+ // Rex Nebular and the Cosmic Gender Bender Macintosh English
+ {
+ "nebular",
+ 0,
+ AD_ENTRY4s("Rex Nebular", "r:e70957f9448af272e27990524b8a3f1a", 509575,
+ "Rex Global Data", "r:ff13f7f2e6d6bb85a990015f44c19f2e", 1297502,
+ "Rex Section I Data", "r:63cad9da79cc235a4021128bfb9f2063", 924486,
+ "Rex Sound Data", "r:36dcb243224b489c38f1289fa61ca874", 2064349),
+ Common::EN_ANY,
+ Common::kPlatformMacintosh,
+ ADGF_UNSTABLE,
+#ifdef USE_TTS
+ GUIO9(GUIO_NOMIDI, GUIO_NOSPEECH, GUIO_NOASPECT, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ANIMATED_INVENTORY, GAMEOPTION_ANIMATED_INTERFACE, GAMEOPTION_NAUGHTY_MODE, GAMEOPTION_TTS_NARRATOR, GAMEOPTION_COPY_PROTECTION)
+#else
+ GUIO8(GUIO_NOMIDI, GUIO_NOSPEECH, GUIO_NOASPECT, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ANIMATED_INVENTORY, GAMEOPTION_ANIMATED_INTERFACE, GAMEOPTION_NAUGHTY_MODE, GAMEOPTION_COPY_PROTECTION)
+#endif
+ },
+ GType_RexNebular,
+ 0
+ },
+
{
// Return of the Phantom DOS English
{
diff --git a/engines/mads/nebular/mac_resources.cpp b/engines/mads/nebular/mac_resources.cpp
index b688682236e..fc9ccd1fd6d 100644
--- a/engines/mads/nebular/mac_resources.cpp
+++ b/engines/mads/nebular/mac_resources.cpp
@@ -203,10 +203,10 @@ const Graphics::Font *MacResourceProvider::getInterfaceFont() {
if (!_fontManager)
return nullptr;
- // CODE 7 selects bold Geneva at 10 points for the native interface.
+ // CODE 7 selects plain Geneva at 10 points for the native interface.
// Use it directly instead of enlarging text from the compatibility surface.
return _fontManager->getFont(Graphics::MacFont(
- Graphics::kMacFontGeneva, 10, Graphics::kMacFontBold));
+ Graphics::kMacFontGeneva, 10, Graphics::kMacFontRegular));
}
MacResourceProvider::ResourceID MacResourceProvider::mapResource(const Common::String &filename) {
diff --git a/engines/mads/nebular/main.cpp b/engines/mads/nebular/main.cpp
index 2ac9836e650..54a911d3daf 100644
--- a/engines/mads/nebular/main.cpp
+++ b/engines/mads/nebular/main.cpp
@@ -20,10 +20,13 @@
*/
#include "common/config-manager.h"
+#include "common/translation.h"
+#include "gui/chooser.h"
#include "gui/saveload.h"
#include "mads/nebular/main.h"
#include "mads/animview/animview.h"
#include "mads/textview/textview.h"
+#include "mads/core/config.h"
#include "mads/core/env.h"
#include "mads/core/error.h"
#include "mads/core/fileio.h"
@@ -237,6 +240,38 @@ static void main_cold_data_init() {
kernel.cheating = gDebugLevel == 9 ? kernel_cheating_allowed : 0;
}
+static void selectMacintoshDifficulty() {
+ const int configuredDifficulty = ConfMan.getInt("difficulty");
+ if (configuredDifficulty >= DIFFICULTY_HARD && configuredDifficulty <= DIFFICULTY_EASY) {
+ game.difficulty = configuredDifficulty;
+ return;
+ }
+
+ Common::U32StringArray choices;
+ choices.push_back(_("Novice - Easy"));
+ choices.push_back(_("Advanced - Difficult"));
+ choices.push_back(_("Expert - Very Difficult"));
+ // The generic browser layout is tied to the launcher's game-list widget.
+ // Use its screen-based counterpart because this chooser runs in-engine.
+ GUI::ChooserDialog dialog(_("Select a Difficulty Level:"), "FileBrowser");
+ dialog.setList(choices);
+
+ switch (dialog.runModal()) {
+ case 0:
+ game.difficulty = DIFFICULTY_EASY;
+ break;
+ case 1:
+ game.difficulty = DIFFICULTY_MEDIUM;
+ break;
+ case 2:
+ game.difficulty = DIFFICULTY_HARD;
+ break;
+ default:
+ game.difficulty = DIFFICULTY_MEDIUM;
+ break;
+ }
+}
+
static void game_main(int argc, const char **argv) {
int count;
int mads_mode;
@@ -255,6 +290,9 @@ static void game_main(int argc, const char **argv) {
game_cold_data_init();
main_cold_data_init();
+ if (g_engine->getPlatform() == Common::kPlatformMacintosh &&
+ !ConfMan.hasKey("save_slot"))
+ selectMacintoshDifficulty();
global_load_config_parameters();
if (argc >= 2) {
@@ -318,7 +356,12 @@ void nebular_main() {
g_engine->readConfigFile();
- if (ConfMan.getBool("start_game") || ConfMan.hasKey("save_slot"))
+ if (g_engine->getPlatform() == Common::kPlatformMacintosh)
+ // FIXME: The Macintosh application has a native resource-based
+ // outer menu, not the DOS MADS menu and playlist files used below.
+ // The ScummVM launcher provides that outer-menu boundary.
+ selected_item = 0;
+ else if (ConfMan.getBool("start_game") || ConfMan.hasKey("save_slot"))
selected_item = 0;
else if (g_engine->isDemo())
selected_item = 9;
diff --git a/engines/mads/nebular/nebular.cpp b/engines/mads/nebular/nebular.cpp
index 807e64f8338..997e6d5f010 100644
--- a/engines/mads/nebular/nebular.cpp
+++ b/engines/mads/nebular/nebular.cpp
@@ -20,6 +20,10 @@
*/
#include "engines/util.h"
+#include "common/debug.h"
+#include "common/system.h"
+#include "graphics/font.h"
+#include "graphics/managed_surface.h"
#include "mads/core/mps_installer.h"
#include "mads/core/attr.h"
#include "mads/core/config.h"
@@ -30,6 +34,7 @@
#include "mads/core/inter.h"
#include "mads/core/kernel.h"
#include "mads/core/matte.h"
+#include "mads/core/mcga.h"
#include "mads/core/object.h"
#include "mads/core/pal.h"
#include "mads/core/rail.h"
@@ -40,11 +45,13 @@
#include "mads/nebular/console.h"
#include "mads/nebular/copy.h"
#include "mads/nebular/global.h"
+#include "mads/nebular/mac_resources.h"
#include "mads/nebular/main.h"
#include "mads/nebular/popup.h"
#include "mads/nebular/mads/inventory.h"
#include "mads/nebular/mads/words.h"
#include "mads/nebular/sound/sound.h"
+#include "mads/nebular/sound/mac_sound.h"
#include "mads/nebular/rooms/section1.h"
#include "mads/nebular/rooms/section2.h"
#include "mads/nebular/rooms/section3.h"
@@ -57,15 +64,397 @@
namespace MADS {
namespace RexNebular {
+enum {
+ kMacScreenWidth = 640,
+ kMacSceneHeight = 312,
+ kMacInterfaceWidth = 512,
+ kMacInterfaceHeight = 88,
+ kMacInterfaceX = (kMacScreenWidth - kMacInterfaceWidth) / 2,
+ kMacScreenHeight = kMacSceneHeight + kMacInterfaceHeight,
+ kMacBlackColor = 8,
+ kMacNormalTextColor = 15,
+ kMacLeftSelectColor = 13,
+ kMacRightSelectColor = 14,
+ kMacPopupColor = REX_DIALOG_FE_COLOR,
+ kMacPanelTintR = 22,
+ kMacPanelTintG = 39,
+ kMacPanelTintB = 42
+};
+
+struct MacPopupLine {
+ Common::String text;
+ word tab;
+
+ MacPopupLine(const Common::String &lineText, word lineTab) :
+ text(lineText), tab(lineTab) {}
+};
+
+static void appendWrappedMacPopupText(const Graphics::Font &font,
+ const Common::String &text, word tab, int width,
+ Common::Array<MacPopupLine> &lines) {
+ Common::Array<Common::String> wrapped;
+ font.wordWrapText(text, width, wrapped);
+ for (uint i = 0; i < wrapped.size(); ++i)
+ lines.push_back(MacPopupLine(wrapped[i], tab));
+}
+
+static int macPanelColorDistance(int r1, int g1, int b1,
+ int r2, int g2, int b2) {
+ const int dr = r1 - r2;
+ const int dg = g1 - g2;
+ const int db = b1 - b2;
+ return dr * dr + dg * dg + db * db;
+}
+
+static void buildMacPanelWashLUT(const byte *nativePalette, byte washLUT[256]) {
+ // The panel is still presented as CLUT8. Replace its first eight display
+ // colors with a uniform 25-percent source / 75-percent cyan blend, then
+ // map the completed panel to those colors. This models a translucent wash
+ // without the checkerboard pattern used by the earlier approximation.
+ Palette displayPalette;
+ for (int color = 0; color < 8; ++color) {
+ const int r = (nativePalette[color * 3 + 0] * 63 + 127) / 255;
+ const int g = (nativePalette[color * 3 + 1] * 63 + 127) / 255;
+ const int b = (nativePalette[color * 3 + 2] * 63 + 127) / 255;
+ displayPalette[color].r = (r + 3 * kMacPanelTintR + 2) / 4;
+ displayPalette[color].g = (g + 3 * kMacPanelTintG + 2) / 4;
+ displayPalette[color].b = (b + 3 * kMacPanelTintB + 2) / 4;
+ }
+
+ // Index 8 backs the side gutters, popup text, and caption shadow. The
+ // normal MADS interface palette makes it dark gray, not Macintosh black.
+ displayPalette[kMacBlackColor].r = 0;
+ displayPalette[kMacBlackColor].g = 0;
+ displayPalette[kMacBlackColor].b = 0;
+ mcga_setpal_range(&displayPalette, 0, kMacBlackColor + 1);
+
+ // Keep the action caption face independent of room palette changes.
+ displayPalette[kMacNormalTextColor].r = 63;
+ displayPalette[kMacNormalTextColor].g = 63;
+ displayPalette[kMacNormalTextColor].b = 63;
+ mcga_setpal_range(&displayPalette, kMacNormalTextColor, 1);
+
+ for (int sourceColor = 0; sourceColor < 256; ++sourceColor) {
+ int sourceR, sourceG, sourceB;
+ if (sourceColor < 8 || sourceColor == kMacLeftSelectColor ||
+ sourceColor == kMacRightSelectColor) {
+ const int nativeColor = sourceColor == kMacLeftSelectColor ? 1 :
+ (sourceColor == kMacRightSelectColor ? 2 : sourceColor);
+ sourceR = (nativePalette[nativeColor * 3 + 0] * 63 + 127) / 255;
+ sourceG = (nativePalette[nativeColor * 3 + 1] * 63 + 127) / 255;
+ sourceB = (nativePalette[nativeColor * 3 + 2] * 63 + 127) / 255;
+ } else if (sourceColor == kMacNormalTextColor) {
+ sourceR = sourceG = sourceB = 63;
+ } else {
+ sourceR = master_palette[sourceColor].r;
+ sourceG = master_palette[sourceColor].g;
+ sourceB = master_palette[sourceColor].b;
+ }
+
+ const int washedR = (sourceR + 3 * kMacPanelTintR + 2) / 4;
+ const int washedG = (sourceG + 3 * kMacPanelTintG + 2) / 4;
+ const int washedB = (sourceB + 3 * kMacPanelTintB + 2) / 4;
+ int bestColor = 0;
+ int bestDistance = macPanelColorDistance(washedR, washedG, washedB,
+ displayPalette[0].r, displayPalette[0].g, displayPalette[0].b);
+ for (int candidate = 1; candidate < 8; ++candidate) {
+ const int distance = macPanelColorDistance(washedR, washedG, washedB,
+ displayPalette[candidate].r, displayPalette[candidate].g,
+ displayPalette[candidate].b);
+ if (distance < bestDistance) {
+ bestDistance = distance;
+ bestColor = candidate;
+ }
+ }
+ washLUT[sourceColor] = bestColor;
+ }
+}
+
+static bool getMacInterfaceSpot(int class_, int id, Common::Rect &rect) {
+ int row = 0;
+ int col = 0;
+ int baseX = 0;
+ int baseY = 0;
+ int deltaX = 0;
+
+ switch (class_) {
+ case STROKE_COMMAND:
+ row = id % inter_columns;
+ col = id / inter_columns;
+ baseX = command_base_x;
+ baseY = command_base_y;
+ deltaX = command_delta_x;
+ break;
+ case STROKE_INVEN:
+ if (id < first_inven || id >= first_inven + inter_columns)
+ return false;
+ row = id - first_inven;
+ baseX = inven_base_x;
+ baseY = inven_base_y;
+ deltaX = inven_delta_x;
+ break;
+ case STROKE_DIALOG:
+ row = id;
+ baseX = command_base_x;
+ baseY = command_base_y;
+ deltaX = dialog_delta_x;
+ break;
+ case STROKE_SCROLL:
+ baseX = inter_scroll_x1 + (id == SCROLL_THUMB ? 2 : 0);
+ deltaX = inter_scroll_x2 - inter_scroll_x1 + 1;
+ switch (id) {
+ case SCROLL_UP:
+ baseY = inter_up_y1;
+ row = 0;
+ break;
+ case SCROLL_DOWN:
+ baseY = inter_down_y1;
+ row = 0;
+ break;
+ case SCROLL_ELEVATOR:
+ baseY = inter_elevator_y1;
+ row = 0;
+ break;
+ case SCROLL_THUMB:
+ baseY = inter_thumb_y1 + scrollbar_elevator;
+ row = 0;
+ break;
+ default:
+ return false;
+ }
+ break;
+ case STROKE_ACTION:
+ row = id;
+ baseX = action_base_x;
+ baseY = action_base_y;
+ deltaX = action_delta_x;
+ break;
+ default:
+ return false;
+ }
+
+ int height = inter_delta_y;
+ if (class_ == STROKE_SCROLL) {
+ switch (id) {
+ case SCROLL_UP:
+ height = inter_up_y2 - inter_up_y1 + 1;
+ break;
+ case SCROLL_DOWN:
+ height = inter_down_y2 - inter_down_y1 + 1;
+ break;
+ case SCROLL_ELEVATOR:
+ height = inter_elevator_y2 - inter_elevator_y1 + 1;
+ break;
+ case SCROLL_THUMB:
+ height = 1;
+ break;
+ }
+ }
+
+ const int x = baseX + col * deltaX;
+ const int y = baseY + row * inter_delta_y;
+ rect = Common::Rect(x, y, x + deltaX, y + height);
+ return true;
+}
+
+static int macInterfaceX(int logicalX) {
+ return logicalX * kMacInterfaceWidth / 320;
+}
+
+static int macInterfaceY(int logicalY) {
+ return logicalY * 2;
+}
+
+static Common::Rect scaleMacInterfaceRect(const Common::Rect &logical) {
+ return Common::Rect(
+ CLIP<int>(macInterfaceX(logical.left), 0, kMacInterfaceWidth),
+ CLIP<int>(macInterfaceY(logical.top), 0, kMacInterfaceHeight),
+ CLIP<int>(macInterfaceX(logical.right), 0, kMacInterfaceWidth),
+ CLIP<int>(macInterfaceY(logical.bottom), 0, kMacInterfaceHeight));
+}
+
+static byte getMacInterfaceTextColor(int class_, int id) {
+ if ((class_ == STROKE_COMMAND && id == left_command) ||
+ (class_ == STROKE_INVEN && id == left_inven) ||
+ (class_ == STROKE_ACTION && id == left_action) ||
+ (class_ == STROKE_DIALOG && id == left_command))
+ return kMacLeftSelectColor;
+ if ((class_ == STROKE_COMMAND && id == right_command) ||
+ (class_ == STROKE_INVEN && id == active_inven) ||
+ (class_ == STROKE_ACTION && id == right_action))
+ return kMacRightSelectColor;
+ return kMacNormalTextColor;
+}
+
+static void drawMacInterfaceText(Graphics::ManagedSurface &panel,
+ const Graphics::Font &font, int class_, int id,
+ const Common::String &text) {
+ Common::Rect logical;
+ if (text.empty() || !getMacInterfaceSpot(class_, id, logical))
+ return;
+
+ // CODE 7 scales the logical x coordinate from 320 to 512 pixels and
+ // converts the logical y coordinate into a QuickDraw baseline with
+ // y * 2 + 11. Reproduce that transform instead of centering the font in
+ // the compatibility rectangle.
+ const int left = macInterfaceX(logical.left);
+ const int right = macInterfaceX(logical.right);
+ const int width = MAX(0, right - left);
+ const int y = macInterfaceY(logical.top) + 11 - font.getFontAscent();
+ if (width > 0 && y < kMacInterfaceHeight)
+ font.drawString(&panel, text, left, y, width,
+ getMacInterfaceTextColor(class_, id));
+}
+
+static Common::String getMacInterfaceWord(int wordId) {
+ char text[80] = { 0 };
+ Common::strcpy_s(text, vocab_string(wordId));
+ if (text[0])
+ text[0] = (char)toupper((byte)text[0]);
+ return Common::String(text);
+}
+
+static void setMacInterfacePixel(Graphics::ManagedSurface &panel,
+ int x, int y, byte color) {
+ if (x >= 0 && x < panel.w && y >= 0 && y < panel.h)
+ *(byte *)panel.getBasePtr(x, y) = color;
+}
+
+static void drawMacInterfaceArrow(Graphics::ManagedSurface &panel,
+ const Common::Rect &rect, bool up, byte color) {
+ if (rect.isEmpty())
+ return;
+
+ const int centerX = (rect.left + rect.right - 1) / 2;
+ const int centerY = (rect.top + rect.bottom - 1) / 2;
+ const int radius = MAX(1, MIN((rect.width() - 1) / 2,
+ (rect.height() - 1) / 2));
+ for (int row = 0; row <= radius; ++row) {
+ const int y = up ? centerY - radius / 2 + row :
+ centerY + radius / 2 - row;
+ for (int x = centerX - row; x <= centerX + row; ++x)
+ setMacInterfacePixel(panel, x, y, color);
+ }
+}
+
+static Common::Rect getScaledMacInterfaceSpot(int class_, int id) {
+ Common::Rect logical;
+ return getMacInterfaceSpot(class_, id, logical) ?
+ scaleMacInterfaceRect(logical) : Common::Rect();
+}
+
+static void drawMacInterfaceScrollbar(Graphics::ManagedSurface &panel) {
+ const Common::Rect up = getScaledMacInterfaceSpot(STROKE_SCROLL, SCROLL_UP);
+ const Common::Rect down = getScaledMacInterfaceSpot(STROKE_SCROLL, SCROLL_DOWN);
+ const Common::Rect elevator =
+ getScaledMacInterfaceSpot(STROKE_SCROLL, SCROLL_ELEVATOR);
+ Common::Rect thumb = getScaledMacInterfaceSpot(STROKE_SCROLL, SCROLL_THUMB);
+
+ if (!elevator.isEmpty())
+ panel.frameRect(elevator, scrollbar_active == SCROLL_ELEVATOR ?
+ kMacLeftSelectColor : kMacNormalTextColor);
+ drawMacInterfaceArrow(panel, up, true,
+ scrollbar_active == SCROLL_UP ? kMacLeftSelectColor : kMacNormalTextColor);
+ drawMacInterfaceArrow(panel, down, false,
+ scrollbar_active == SCROLL_DOWN ? kMacLeftSelectColor : kMacNormalTextColor);
+
+ if (!thumb.isEmpty() && !elevator.isEmpty()) {
+ thumb.left = MAX<int>(thumb.left, elevator.left + 2);
+ thumb.right = MIN<int>(thumb.right, elevator.right - 2);
+ if (!thumb.isEmpty())
+ panel.fillRect(thumb, scrollbar_active == SCROLL_ELEVATOR ?
+ kMacLeftSelectColor : kMacNormalTextColor);
+ }
+}
+
+static void drawMacInterfaceState(Graphics::ManagedSurface &panel,
+ const Graphics::Font &font) {
+ if (inter_input_mode == INTER_BUILDING_SENTENCES ||
+ inter_input_mode == INTER_LIMITED_SENTENCES) {
+ for (int id = 0; id < INTER_COMMANDS; ++id)
+ drawMacInterfaceText(panel, font, STROKE_COMMAND, id,
+ getMacInterfaceWord(command[id].id));
+ for (int id = first_inven; id < first_inven + inter_columns &&
+ id < inven_num_objects; ++id) {
+ drawMacInterfaceText(panel, font, STROKE_INVEN, id,
+ getMacInterfaceWord(object[inven[id]].vocab_id));
+ }
+ if (active_inven >= 0) {
+ for (int id = 0; id < object[inven[active_inven]].num_verbs; ++id) {
+ drawMacInterfaceText(panel, font, STROKE_ACTION, id,
+ getMacInterfaceWord(object[inven[active_inven]].verb[id].id));
+ }
+ }
+ drawMacInterfaceScrollbar(panel);
+ } else if (inter_input_mode == INTER_CONVERSATION) {
+ for (int id = 0; id < inter_columns; ++id) {
+ if (inter_dialog_strings[id])
+ drawMacInterfaceText(panel, font, STROKE_DIALOG, id,
+ Common::String(inter_dialog_strings[id]));
+ }
+ }
+}
+
+static bool isMacInterfaceSemanticPixel(int x, int y) {
+ Common::Rect rect;
+ if (inter_input_mode == INTER_CONVERSATION) {
+ for (int id = 0; id < inter_columns; ++id) {
+ if (getMacInterfaceSpot(STROKE_DIALOG, id, rect) && rect.contains(x, y))
+ return true;
+ }
+ return false;
+ }
+
+ if (inter_input_mode != INTER_BUILDING_SENTENCES &&
+ inter_input_mode != INTER_LIMITED_SENTENCES)
+ return false;
+
+ for (int id = 0; id < INTER_COMMANDS; ++id) {
+ if (getMacInterfaceSpot(STROKE_COMMAND, id, rect) && rect.contains(x, y))
+ return true;
+ }
+ for (int id = first_inven; id < first_inven + inter_columns; ++id) {
+ if (getMacInterfaceSpot(STROKE_INVEN, id, rect) && rect.contains(x, y))
+ return true;
+ }
+ for (int id = 0; id < OBJECT_MAX_VERBS; ++id) {
+ if (getMacInterfaceSpot(STROKE_ACTION, id, rect) && rect.contains(x, y))
+ return true;
+ }
+ for (int id = SCROLL_UP; id <= SCROLL_THUMB; ++id) {
+ if (getMacInterfaceSpot(STROKE_SCROLL, id, rect) && rect.contains(x, y))
+ return true;
+ }
+ return false;
+}
+
RexNebularEngine::RexNebularEngine(OSystem *syst, const MADSGameDescription *gameDesc) :
MADSEngine(syst, gameDesc) {
// Initialize globals
RexNebular::popup_init();
}
+RexNebularEngine::~RexNebularEngine() {
+ if (_macResources) {
+ delete _soundManager;
+ _soundManager = nullptr;
+ env_set_resource_provider(nullptr);
+ delete _macResources;
+ }
+}
+
Common::Error RexNebularEngine::run() {
- initGraphics(320, 200);
- _screen = new Graphics::Screen();
+ const bool isMacintosh = getPlatform() == Common::kPlatformMacintosh;
+ if (isMacintosh)
+ initGraphics(kMacScreenWidth, kMacScreenHeight);
+ else
+ initGraphics(320, 200);
+ applyGameSettings();
+
+ // The shared engine always renders into its original 320x200 work screen.
+ // Macintosh presentation expands the scene and supplies its independent
+ // native interface panel when the frame is sent to the backend.
+ _screen = new Graphics::Screen(320, 200);
scr_live.data = (byte *)_screen->getPixels();
// Create a debugger console
@@ -78,8 +467,17 @@ Common::Error RexNebularEngine::run() {
SearchMan.add("mpslabs", arch);
}
- // Set up sound manager
- _soundManager = new Sound::RexSoundManager(_mixer, _soundFlag, isDemo());
+ // Set up the platform resource and sound providers
+ if (isMacintosh) {
+ _macResources = new MacResourceProvider();
+ if (!_macResources->load())
+ return Common::Error(Common::kNoGameDataFoundError,
+ "Could not open the Macintosh Rex resource files");
+ env_set_resource_provider(_macResources);
+ _soundManager = new Sound::MacSoundManager(_mixer, _soundFlag, _macResources);
+ } else {
+ _soundManager = new Sound::RexSoundManager(_mixer, _soundFlag, isDemo());
+ }
_soundManager->validate();
// Run the game
@@ -88,6 +486,269 @@ Common::Error RexNebularEngine::run() {
return Common::kNoError;
}
+void RexNebularEngine::applyGameSettings() {
+ Engine::applyGameSettings();
+
+ // The Macintosh port's 640x400 large-window mode uses square pixels.
+ // DOS-style 320x200 aspect correction would stretch its scene and native
+ // interface vertically.
+ if (getPlatform() == Common::kPlatformMacintosh &&
+ g_system->hasFeature(OSystem::kFeatureAspectRatioCorrection) &&
+ g_system->getFeatureState(OSystem::kFeatureAspectRatioCorrection)) {
+ g_system->beginGFXTransaction();
+ g_system->setFeatureState(OSystem::kFeatureAspectRatioCorrection, false);
+ g_system->endGFXTransaction();
+ }
+}
+
+Common::Point RexNebularEngine::screenToGame(const Common::Point &point) const {
+ if (getPlatform() != Common::kPlatformMacintosh)
+ return MADSEngine::screenToGame(point);
+
+ if (point.y >= 0 && point.y < kMacSceneHeight)
+ return Common::Point(CLIP<int>(point.x / 2, 0, 319), point.y / 2);
+
+ if (point.y >= kMacSceneHeight && point.y < kMacScreenHeight &&
+ point.x >= kMacInterfaceX &&
+ point.x < kMacInterfaceX + kMacInterfaceWidth) {
+ return Common::Point(
+ (point.x - kMacInterfaceX) * 320 / kMacInterfaceWidth,
+ 156 + (point.y - kMacSceneHeight) / 2);
+ }
+
+ // The native interface is narrower than the scene. Its side gutters are
+ // intentionally inactive rather than being stretched into hotspots.
+ return Common::Point(-1, -1);
+}
+
+Common::Point RexNebularEngine::gameToScreen(const Common::Point &point) const {
+ if (getPlatform() != Common::kPlatformMacintosh)
+ return MADSEngine::gameToScreen(point);
+
+ if (point.y < 156)
+ return Common::Point(point.x * 2, point.y * 2);
+
+ return Common::Point(kMacInterfaceX + point.x * kMacInterfaceWidth / 320,
+ kMacSceneHeight + (point.y - 156) * 2);
+}
+
+void RexNebularEngine::presentScreen(int shakeOffset) {
+ if (getPlatform() != Common::kPlatformMacintosh) {
+ MADSEngine::presentScreen(shakeOffset);
+ return;
+ }
+
+ _macOutput.resize(kMacScreenWidth * kMacScreenHeight);
+ memset(_macOutput.data(), kMacBlackColor, _macOutput.size());
+
+ // Native large-window mode doubles the 320x156 scene in both axes.
+ for (int y = 0; y < 156; ++y) {
+ const byte *source = (const byte *)_screen->getBasePtr(0, y);
+ byte *line1 = _macOutput.data() + (y * 2) * kMacScreenWidth;
+ byte *line2 = line1 + kMacScreenWidth;
+ for (int x = 0; x < 320; ++x) {
+ const byte color = source[(x + shakeOffset) % 320];
+ line1[x * 2] = color;
+ line1[x * 2 + 1] = color;
+ }
+ memcpy(line2, line1, kMacScreenWidth);
+ }
+
+ const Graphics::Surface *nativeInterface =
+ _macResources ? _macResources->getNativeInterface() : nullptr;
+ const Graphics::Surface *logicalInterface =
+ _macResources ? _macResources->getLogicalInterface() : nullptr;
+ if (nativeInterface && nativeInterface->w == kMacInterfaceWidth &&
+ nativeInterface->h == kMacInterfaceHeight &&
+ nativeInterface->format.bytesPerPixel == 1) {
+ Graphics::ManagedSurface panel;
+ panel.create(kMacInterfaceWidth, kMacInterfaceHeight,
+ Graphics::PixelFormat::createFormatCLUT8());
+ for (int y = 0; y < kMacInterfaceHeight; ++y)
+ memcpy(panel.getBasePtr(0, y), nativeInterface->getBasePtr(0, y),
+ kMacInterfaceWidth);
+
+ const Graphics::Font *interfaceFont =
+ _macResources ? _macResources->getInterfaceFont() : nullptr;
+
+ // Keep live inventory artwork and other non-text changes produced by
+ // the shared interface state machine. Semantic regions are redrawn
+ // below, avoiding enlarged compatibility-font glyphs and FONTMISC's
+ // private scrollbar character convention.
+ if (logicalInterface && logicalInterface->w == 320 &&
+ logicalInterface->h == 44 &&
+ logicalInterface->format.bytesPerPixel == 1) {
+ for (int y = 0; y < kMacInterfaceHeight; ++y) {
+ byte *target = (byte *)panel.getBasePtr(0, y);
+ const int logicalY = y / 2;
+ const byte *current =
+ (const byte *)_screen->getBasePtr(0, 156 + logicalY);
+ const byte *baseline =
+ (const byte *)logicalInterface->getBasePtr(0, logicalY);
+ for (int x = 0; x < kMacInterfaceWidth; ++x) {
+ const int logicalX = x * 320 / kMacInterfaceWidth;
+ if (current[logicalX] != baseline[logicalX] &&
+ (!interfaceFont ||
+ !isMacInterfaceSemanticPixel(logicalX, logicalY)))
+ target[x] = current[logicalX];
+ }
+ }
+ }
+
+ if (interfaceFont)
+ drawMacInterfaceState(panel, *interfaceFont);
+
+ // Apply the blue layer after composing artwork, live state, controls,
+ // and text so the whole native panel receives one uniform treatment.
+ byte washLUT[256];
+ buildMacPanelWashLUT(_macResources->getNativeInterfacePalette(), washLUT);
+ for (int y = 0; y < kMacInterfaceHeight; ++y) {
+ byte *target = (byte *)panel.getBasePtr(0, y);
+ for (int x = 0; x < kMacInterfaceWidth; ++x)
+ target[x] = washLUT[target[x]];
+ }
+
+ for (int y = 0; y < kMacInterfaceHeight; ++y) {
+ memcpy(_macOutput.data() +
+ (kMacSceneHeight + y) * kMacScreenWidth + kMacInterfaceX,
+ panel.getBasePtr(0, y), kMacInterfaceWidth);
+ }
+ } else {
+ // 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) {
+ const byte *source = (const byte *)_screen->getBasePtr(0, 156 + y / 2);
+ byte *target = _macOutput.data() +
+ (kMacSceneHeight + y) * kMacScreenWidth + kMacInterfaceX;
+ for (int x = 0; x < kMacInterfaceWidth; ++x)
+ target[x] = source[x * 320 / kMacInterfaceWidth];
+ }
+ }
+
+ if (_macPopupActive && !_macPopup.empty()) {
+ for (int y = 0; y < _macPopup.h; ++y) {
+ const int targetY = _macPopupRect.top + y;
+ if (targetY < 0 || targetY >= kMacScreenHeight)
+ continue;
+
+ const int targetX = MAX<int>(0, _macPopupRect.left);
+ const int sourceX = targetX - _macPopupRect.left;
+ const int width = MIN<int>(_macPopup.w - sourceX,
+ kMacScreenWidth - targetX);
+ if (width > 0)
+ memcpy(_macOutput.data() + targetY * kMacScreenWidth + targetX,
+ _macPopup.getBasePtr(sourceX, y), width);
+ }
+ }
+
+ if (!_macLayoutLogged) {
+ debug(2, "Presenting Macintosh Rex as 640x312 scene plus centered 512x88 interface");
+ _macLayoutLogged = true;
+ }
+
+ g_system->copyRectToScreen(_macOutput.data(), kMacScreenWidth,
+ 0, 0, kMacScreenWidth, kMacScreenHeight);
+ g_system->updateScreen();
+ _screen->clearDirtyRects();
+}
+
+void RexNebularEngine::showMacPopup() {
+ if (getPlatform() != Common::kPlatformMacintosh || !_macResources || !box)
+ return;
+
+ const Graphics::Font *font = _macResources->getDialogFont();
+ if (!font)
+ return;
+
+ const int width = CLIP<int>(((box->horiz_pieces * 11 + 20) * 15) / 16, 40,
+ kMacScreenWidth - 2);
+
+ // The shared parser has already wrapped text with the compatibility
+ // packed font. Join ordinary runs and wrap them again with native Geneva
+ // metrics; structural bars, underlines, blank lines, and tabs remain
+ // separate records.
+ Common::Array<MacPopupLine> lines;
+ Common::String paragraph;
+ word paragraphTab = 0;
+ for (int line = 0; line <= box->text_y; ++line) {
+ const word tab = box->tab[line];
+ const word plainTab = tab & ~(POPUP_UNDERLINE | POPUP_DOWNPIXEL);
+ const bool structural = tab == POPUP_BAR ||
+ (tab & (POPUP_UNDERLINE | POPUP_DOWNPIXEL)) || !box->text[line][0];
+ if (structural || (!paragraph.empty() && plainTab != paragraphTab)) {
+ if (!paragraph.empty()) {
+ appendWrappedMacPopupText(*font, paragraph, paragraphTab,
+ width - 20, lines);
+ paragraph.clear();
+ }
+ }
+
+ if (structural) {
+ lines.push_back(MacPopupLine(box->text[line], tab));
+ } else {
+ if (paragraph.empty())
+ paragraphTab = plainTab;
+ else
+ paragraph += ' ';
+ paragraph += box->text[line];
+ }
+ }
+ if (!paragraph.empty())
+ appendWrappedMacPopupText(*font, paragraph, paragraphTab,
+ width - 20, lines);
+
+ const int height = CLIP<int>((int)lines.size() * 12 + 20, 20,
+ kMacScreenHeight - 2);
+ _macPopup.create(width, height, Graphics::PixelFormat::createFormatCLUT8());
+ _macPopup.fillRect(Common::Rect(width, height), kMacPopupColor);
+ _macPopup.frameRect(Common::Rect(width, height), kMacBlackColor);
+
+ const int ascent = font->getFontAscent() >= 0 ?
+ font->getFontAscent() : font->getFontHeight();
+ for (uint line = 0; line < lines.size(); ++line) {
+ const int baseline = 22 + line * 12 +
+ ((lines[line].tab & POPUP_DOWNPIXEL) ? 1 : 0);
+ if (baseline - ascent >= height)
+ break;
+
+ if (lines[line].tab == POPUP_BAR) {
+ _macPopup.fillRect(Common::Rect(2, baseline - 5,
+ width - 2, baseline - 4), kMacBlackColor);
+ continue;
+ }
+
+ const int textWidth = font->getStringWidth(lines[line].text);
+ const int x = (lines[line].tab & POPUP_UNDERLINE) ?
+ (width - textWidth) / 2 :
+ 10 + (lines[line].tab &
+ ~(POPUP_UNDERLINE | POPUP_DOWNPIXEL)) * 3 / 4;
+ font->drawString(&_macPopup, lines[line].text, x, baseline - ascent,
+ MAX(0, width - x - 2), kMacBlackColor);
+ if (lines[line].tab & POPUP_UNDERLINE) {
+ _macPopup.fillRect(Common::Rect(x, baseline + 1,
+ MIN(width - 2, x + textWidth), baseline + 2),
+ kMacBlackColor);
+ }
+ }
+
+ _macPopupRect = Common::Rect(
+ (kMacScreenWidth - width) / 2,
+ (kMacScreenHeight - height) / 2,
+ (kMacScreenWidth + width) / 2,
+ (kMacScreenHeight + height) / 2);
+ _macPopupActive = true;
+ presentScreen(0);
+}
+
+void RexNebularEngine::hideMacPopup() {
+ if (!_macPopupActive)
+ return;
+
+ _macPopupActive = false;
+ _macPopup.free();
+ presentScreen(0);
+}
+
int RexNebularEngine::main_copy_verify() {
return global_copy_verify();
}
diff --git a/engines/mads/nebular/nebular.h b/engines/mads/nebular/nebular.h
index 423e69236a9..4be7752276c 100644
--- a/engines/mads/nebular/nebular.h
+++ b/engines/mads/nebular/nebular.h
@@ -22,11 +22,16 @@
#ifndef MADS_NEBULAR_H
#define MADS_NEBULAR_H
+#include "common/array.h"
+#include "common/rect.h"
+#include "graphics/managed_surface.h"
#include "mads/mads.h"
namespace MADS {
namespace RexNebular {
+class MacResourceProvider;
+
struct MADSSavegameHeader {
uint8 _version;
Common::String _saveName;
@@ -41,14 +46,32 @@ struct MADSSavegameHeader {
class RexNebularEngine : public MADSEngine {
private:
+ MacResourceProvider *_macResources = nullptr;
+ Common::Array<byte> _macOutput;
+ Graphics::ManagedSurface _macPopup;
+ Common::Rect _macPopupRect;
+ bool _macPopupActive = false;
+ bool _macLayoutLogged = false;
+
void showRecipe();
+protected:
+ void applyGameSettings() override;
+ Common::Point screenToGame(const Common::Point &point) const override;
+ Common::Point gameToScreen(const Common::Point &point) const override;
+ void presentScreen(int shakeOffset) override;
+
public:
RexNebularEngine(OSystem *syst, const MADSGameDescription *gameDesc);
- ~RexNebularEngine() override {}
+ ~RexNebularEngine() override;
Common::Error run() override;
+ bool usesScummVMMenu() const override {
+ return getPlatform() == Common::kPlatformMacintosh;
+ }
void syncRoom(Common::Serializer &s) override;
+ void showMacPopup();
+ void hideMacPopup();
int main_copy_verify() override;
void global_init_code() override;
diff --git a/engines/mads/nebular/popup.cpp b/engines/mads/nebular/popup.cpp
index 10f0f383bd3..5a60947042b 100644
--- a/engines/mads/nebular/popup.cpp
+++ b/engines/mads/nebular/popup.cpp
@@ -30,6 +30,7 @@
#include "mads/core/mouse.h"
#include "mads/core/pal.h"
#include "mads/core/video.h"
+#include "mads/nebular/nebular.h"
#include "mads/nebular/popup.h"
namespace MADS {
@@ -93,6 +94,11 @@ static int popup_draw_content(int x, int y, int xs, int ys, int unknown, byte co
}
void popup_draw() {
+ if (g_engine->getPlatform() == Common::kPlatformMacintosh) {
+ static_cast<RexNebularEngine *>(g_engine)->showMacPopup();
+ return;
+ }
+
int askY;
int y2 = box->y + box->ys;
@@ -150,6 +156,11 @@ void popup_draw() {
mouse_show();
}
+void popup_close() {
+ if (g_engine->getPlatform() == Common::kPlatformMacintosh)
+ static_cast<RexNebularEngine *>(g_engine)->hideMacPopup();
+}
+
void popup_setup_cycle() {
font_set_colors(-1, DIALOG_BLACK_COLOR, DIALOG_BLACK_COLOR, DIALOG_BLACK_COLOR);
diff --git a/engines/mads/nebular/popup.h b/engines/mads/nebular/popup.h
index 5d35612617c..a1a04bd58a4 100644
--- a/engines/mads/nebular/popup.h
+++ b/engines/mads/nebular/popup.h
@@ -30,6 +30,7 @@ namespace RexNebular {
extern void popup_init();
extern void popup_draw();
+extern void popup_close();
extern void popup_setup_cycle();
extern void popup_update_ask(const char *string, int maxlen);
Commit: 849a761db701a08ccd96e7ea898d4de557554224
https://github.com/scummvm/scummvm/commit/849a761db701a08ccd96e7ea898d4de557554224
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-05T12:02:15+10:00
Commit Message:
MADS: NEBULAR: Separate Macintosh menu adapters
Keep the Mac-specific GUI difficulty chooser out of the shared Rex
startup implementation.
Changed paths:
A engines/mads/nebular/mac_menus.cpp
A engines/mads/nebular/mac_menus.h
engines/mads/module.mk
engines/mads/nebular/main.cpp
diff --git a/engines/mads/module.mk b/engines/mads/module.mk
index a196e6eda19..ae0aa63857e 100644
--- a/engines/mads/module.mk
+++ b/engines/mads/module.mk
@@ -73,6 +73,7 @@ MODULE_OBJS := \
nebular/global.o \
nebular/main.o \
nebular/main_menu.o \
+ nebular/mac_menus.o \
nebular/mac_resources.o \
nebular/menus.o \
nebular/popup.o \
diff --git a/engines/mads/nebular/mac_menus.cpp b/engines/mads/nebular/mac_menus.cpp
new file mode 100644
index 00000000000..e6a0e072e93
--- /dev/null
+++ b/engines/mads/nebular/mac_menus.cpp
@@ -0,0 +1,65 @@
+/* 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/config-manager.h"
+#include "common/translation.h"
+#include "gui/chooser.h"
+#include "mads/core/config.h"
+#include "mads/core/game.h"
+#include "mads/nebular/mac_menus.h"
+
+namespace MADS {
+namespace RexNebular {
+
+void selectMacintoshDifficulty() {
+ const int configuredDifficulty = ConfMan.getInt("difficulty");
+ if (configuredDifficulty >= DIFFICULTY_HARD && configuredDifficulty <= DIFFICULTY_EASY) {
+ game.difficulty = configuredDifficulty;
+ return;
+ }
+
+ Common::U32StringArray choices;
+ choices.push_back(_("Novice - Easy"));
+ choices.push_back(_("Advanced - Difficult"));
+ choices.push_back(_("Expert - Very Difficult"));
+ // The generic browser layout is tied to the launcher's game-list widget.
+ // Use its screen-based counterpart because this chooser runs in-engine.
+ GUI::ChooserDialog dialog(_("Select a Difficulty Level:"), "FileBrowser");
+ dialog.setList(choices);
+
+ switch (dialog.runModal()) {
+ case 0:
+ game.difficulty = DIFFICULTY_EASY;
+ break;
+ case 1:
+ game.difficulty = DIFFICULTY_MEDIUM;
+ break;
+ case 2:
+ game.difficulty = DIFFICULTY_HARD;
+ break;
+ default:
+ game.difficulty = DIFFICULTY_MEDIUM;
+ break;
+ }
+}
+
+} // namespace RexNebular
+} // namespace MADS
diff --git a/engines/mads/nebular/mac_menus.h b/engines/mads/nebular/mac_menus.h
new file mode 100644
index 00000000000..9d4e045f975
--- /dev/null
+++ b/engines/mads/nebular/mac_menus.h
@@ -0,0 +1,33 @@
+/* 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_MENUS_H
+#define MADS_NEBULAR_MAC_MENUS_H
+
+namespace MADS {
+namespace RexNebular {
+
+void selectMacintoshDifficulty();
+
+} // namespace RexNebular
+} // namespace MADS
+
+#endif
diff --git a/engines/mads/nebular/main.cpp b/engines/mads/nebular/main.cpp
index 54a911d3daf..3272d013c2b 100644
--- a/engines/mads/nebular/main.cpp
+++ b/engines/mads/nebular/main.cpp
@@ -20,9 +20,6 @@
*/
#include "common/config-manager.h"
-#include "common/translation.h"
-#include "gui/chooser.h"
-#include "gui/saveload.h"
#include "mads/nebular/main.h"
#include "mads/animview/animview.h"
#include "mads/textview/textview.h"
@@ -48,6 +45,7 @@
#include "mads/core/timer.h"
#include "mads/core/video.h"
#include "mads/nebular/main_menu.h"
+#include "mads/nebular/mac_menus.h"
#include "mads/nebular/menus.h"
#include "mads/mads.h"
@@ -240,38 +238,6 @@ static void main_cold_data_init() {
kernel.cheating = gDebugLevel == 9 ? kernel_cheating_allowed : 0;
}
-static void selectMacintoshDifficulty() {
- const int configuredDifficulty = ConfMan.getInt("difficulty");
- if (configuredDifficulty >= DIFFICULTY_HARD && configuredDifficulty <= DIFFICULTY_EASY) {
- game.difficulty = configuredDifficulty;
- return;
- }
-
- Common::U32StringArray choices;
- choices.push_back(_("Novice - Easy"));
- choices.push_back(_("Advanced - Difficult"));
- choices.push_back(_("Expert - Very Difficult"));
- // The generic browser layout is tied to the launcher's game-list widget.
- // Use its screen-based counterpart because this chooser runs in-engine.
- GUI::ChooserDialog dialog(_("Select a Difficulty Level:"), "FileBrowser");
- dialog.setList(choices);
-
- switch (dialog.runModal()) {
- case 0:
- game.difficulty = DIFFICULTY_EASY;
- break;
- case 1:
- game.difficulty = DIFFICULTY_MEDIUM;
- break;
- case 2:
- game.difficulty = DIFFICULTY_HARD;
- break;
- default:
- game.difficulty = DIFFICULTY_MEDIUM;
- break;
- }
-}
-
static void game_main(int argc, const char **argv) {
int count;
int mads_mode;
Commit: 805f259a19d71eb12db1e3b0b9b24c27ee18e8b6
https://github.com/scummvm/scummvm/commit/805f259a19d71eb12db1e3b0b9b24c27ee18e8b6
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-05T12:02:15+10:00
Commit Message:
MADS: NEBULAR: Separate Macintosh presentation code
Centralize most Macintosh-specific presentation code: resource
management, graphics initialization, coordinate conversion, panel
composition, frame presentation, and native popup rendering.
Assisted-by: Codex:GPT-5.4
Changed paths:
A engines/mads/nebular/mac_nebular.cpp
engines/mads/module.mk
engines/mads/nebular/nebular.cpp
engines/mads/nebular/nebular.h
diff --git a/engines/mads/module.mk b/engines/mads/module.mk
index ae0aa63857e..7ae486eae4a 100644
--- a/engines/mads/module.mk
+++ b/engines/mads/module.mk
@@ -74,6 +74,7 @@ MODULE_OBJS := \
nebular/main.o \
nebular/main_menu.o \
nebular/mac_menus.o \
+ nebular/mac_nebular.o \
nebular/mac_resources.o \
nebular/menus.o \
nebular/popup.o \
diff --git a/engines/mads/nebular/mac_nebular.cpp b/engines/mads/nebular/mac_nebular.cpp
new file mode 100644
index 00000000000..7fdcf958abe
--- /dev/null
+++ b/engines/mads/nebular/mac_nebular.cpp
@@ -0,0 +1,695 @@
+/* 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 "engines/util.h"
+#include "common/debug.h"
+#include "common/system.h"
+#include "graphics/font.h"
+#include "graphics/managed_surface.h"
+#include "mads/core/env.h"
+#include "mads/core/inter.h"
+#include "mads/core/mcga.h"
+#include "mads/core/object.h"
+#include "mads/core/pal.h"
+#include "mads/core/screen.h"
+#include "mads/nebular/mac_resources.h"
+#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 {
+
+enum {
+ kMacScreenWidth = 640,
+ kMacSceneHeight = 312,
+ kMacInterfaceWidth = 512,
+ kMacInterfaceHeight = 88,
+ kMacInterfaceX = (kMacScreenWidth - kMacInterfaceWidth) / 2,
+ kMacScreenHeight = kMacSceneHeight + kMacInterfaceHeight,
+ kMacBlackColor = 8,
+ kMacNormalTextColor = 15,
+ kMacLeftSelectColor = 13,
+ kMacRightSelectColor = 14,
+ kMacPopupColor = REX_DIALOG_FE_COLOR,
+ kMacPanelTintR = 22,
+ kMacPanelTintG = 39,
+ kMacPanelTintB = 42
+};
+
+struct MacPopupLine {
+ Common::String text;
+ word tab;
+
+ MacPopupLine(const Common::String &lineText, word lineTab) :
+ text(lineText), tab(lineTab) {}
+};
+
+static void appendWrappedMacPopupText(const Graphics::Font &font,
+ const Common::String &text, word tab, int width,
+ Common::Array<MacPopupLine> &lines) {
+ Common::Array<Common::String> wrapped;
+ font.wordWrapText(text, width, wrapped);
+ for (uint i = 0; i < wrapped.size(); ++i)
+ lines.push_back(MacPopupLine(wrapped[i], tab));
+}
+
+static int macPanelColorDistance(int r1, int g1, int b1,
+ int r2, int g2, int b2) {
+ const int dr = r1 - r2;
+ const int dg = g1 - g2;
+ const int db = b1 - b2;
+ return dr * dr + dg * dg + db * db;
+}
+
+static void buildMacPanelWashLUT(const byte *nativePalette, byte washLUT[256]) {
+ // The panel is still presented as CLUT8. Replace its first eight display
+ // colors with a uniform 25-percent source / 75-percent cyan blend, then
+ // map the completed panel to those colors. This models a translucent wash
+ // without the checkerboard pattern used by an earlier approximation.
+ Palette displayPalette;
+ for (int color = 0; color < 8; ++color) {
+ const int r = (nativePalette[color * 3 + 0] * 63 + 127) / 255;
+ const int g = (nativePalette[color * 3 + 1] * 63 + 127) / 255;
+ const int b = (nativePalette[color * 3 + 2] * 63 + 127) / 255;
+ displayPalette[color].r = (r + 3 * kMacPanelTintR + 2) / 4;
+ displayPalette[color].g = (g + 3 * kMacPanelTintG + 2) / 4;
+ displayPalette[color].b = (b + 3 * kMacPanelTintB + 2) / 4;
+ }
+
+ // Index 8 backs the side gutters, popup text, and caption shadow. The
+ // normal MADS interface palette makes it dark gray, not Macintosh black.
+ displayPalette[kMacBlackColor].r = 0;
+ displayPalette[kMacBlackColor].g = 0;
+ displayPalette[kMacBlackColor].b = 0;
+ mcga_setpal_range(&displayPalette, 0, kMacBlackColor + 1);
+
+ // Keep the action caption face independent of room palette changes.
+ displayPalette[kMacNormalTextColor].r = 63;
+ displayPalette[kMacNormalTextColor].g = 63;
+ displayPalette[kMacNormalTextColor].b = 63;
+ mcga_setpal_range(&displayPalette, kMacNormalTextColor, 1);
+
+ for (int sourceColor = 0; sourceColor < 256; ++sourceColor) {
+ int sourceR, sourceG, sourceB;
+ if (sourceColor < 8 || sourceColor == kMacLeftSelectColor ||
+ sourceColor == kMacRightSelectColor) {
+ const int nativeColor = sourceColor == kMacLeftSelectColor ? 1 :
+ (sourceColor == kMacRightSelectColor ? 2 : sourceColor);
+ sourceR = (nativePalette[nativeColor * 3 + 0] * 63 + 127) / 255;
+ sourceG = (nativePalette[nativeColor * 3 + 1] * 63 + 127) / 255;
+ sourceB = (nativePalette[nativeColor * 3 + 2] * 63 + 127) / 255;
+ } else if (sourceColor == kMacNormalTextColor) {
+ sourceR = sourceG = sourceB = 63;
+ } else {
+ sourceR = master_palette[sourceColor].r;
+ sourceG = master_palette[sourceColor].g;
+ sourceB = master_palette[sourceColor].b;
+ }
+
+ const int washedR = (sourceR + 3 * kMacPanelTintR + 2) / 4;
+ const int washedG = (sourceG + 3 * kMacPanelTintG + 2) / 4;
+ const int washedB = (sourceB + 3 * kMacPanelTintB + 2) / 4;
+ int bestColor = 0;
+ int bestDistance = macPanelColorDistance(washedR, washedG, washedB,
+ displayPalette[0].r, displayPalette[0].g, displayPalette[0].b);
+ for (int candidate = 1; candidate < 8; ++candidate) {
+ const int distance = macPanelColorDistance(washedR, washedG, washedB,
+ displayPalette[candidate].r, displayPalette[candidate].g,
+ displayPalette[candidate].b);
+ if (distance < bestDistance) {
+ bestDistance = distance;
+ bestColor = candidate;
+ }
+ }
+ washLUT[sourceColor] = bestColor;
+ }
+}
+
+static bool getMacInterfaceSpot(int class_, int id, Common::Rect &rect) {
+ int row = 0;
+ int col = 0;
+ int baseX = 0;
+ int baseY = 0;
+ int deltaX = 0;
+
+ switch (class_) {
+ case STROKE_COMMAND:
+ row = id % inter_columns;
+ col = id / inter_columns;
+ baseX = command_base_x;
+ baseY = command_base_y;
+ deltaX = command_delta_x;
+ break;
+ case STROKE_INVEN:
+ if (id < first_inven || id >= first_inven + inter_columns)
+ return false;
+ row = id - first_inven;
+ baseX = inven_base_x;
+ baseY = inven_base_y;
+ deltaX = inven_delta_x;
+ break;
+ case STROKE_DIALOG:
+ row = id;
+ baseX = command_base_x;
+ baseY = command_base_y;
+ deltaX = dialog_delta_x;
+ break;
+ case STROKE_SCROLL:
+ baseX = inter_scroll_x1 + (id == SCROLL_THUMB ? 2 : 0);
+ deltaX = inter_scroll_x2 - inter_scroll_x1 + 1;
+ switch (id) {
+ case SCROLL_UP:
+ baseY = inter_up_y1;
+ row = 0;
+ break;
+ case SCROLL_DOWN:
+ baseY = inter_down_y1;
+ row = 0;
+ break;
+ case SCROLL_ELEVATOR:
+ baseY = inter_elevator_y1;
+ row = 0;
+ break;
+ case SCROLL_THUMB:
+ baseY = inter_thumb_y1 + scrollbar_elevator;
+ row = 0;
+ break;
+ default:
+ return false;
+ }
+ break;
+ case STROKE_ACTION:
+ row = id;
+ baseX = action_base_x;
+ baseY = action_base_y;
+ deltaX = action_delta_x;
+ break;
+ default:
+ return false;
+ }
+
+ int height = inter_delta_y;
+ if (class_ == STROKE_SCROLL) {
+ switch (id) {
+ case SCROLL_UP:
+ height = inter_up_y2 - inter_up_y1 + 1;
+ break;
+ case SCROLL_DOWN:
+ height = inter_down_y2 - inter_down_y1 + 1;
+ break;
+ case SCROLL_ELEVATOR:
+ height = inter_elevator_y2 - inter_elevator_y1 + 1;
+ break;
+ case SCROLL_THUMB:
+ height = 1;
+ break;
+ }
+ }
+
+ const int x = baseX + col * deltaX;
+ const int y = baseY + row * inter_delta_y;
+ rect = Common::Rect(x, y, x + deltaX, y + height);
+ return true;
+}
+
+static int macInterfaceX(int logicalX) {
+ return logicalX * kMacInterfaceWidth / 320;
+}
+
+static int macInterfaceY(int logicalY) {
+ return logicalY * 2;
+}
+
+static Common::Rect scaleMacInterfaceRect(const Common::Rect &logical) {
+ return Common::Rect(
+ CLIP<int>(macInterfaceX(logical.left), 0, kMacInterfaceWidth),
+ CLIP<int>(macInterfaceY(logical.top), 0, kMacInterfaceHeight),
+ CLIP<int>(macInterfaceX(logical.right), 0, kMacInterfaceWidth),
+ CLIP<int>(macInterfaceY(logical.bottom), 0, kMacInterfaceHeight));
+}
+
+static byte getMacInterfaceTextColor(int class_, int id) {
+ if ((class_ == STROKE_COMMAND && id == left_command) ||
+ (class_ == STROKE_INVEN && id == left_inven) ||
+ (class_ == STROKE_ACTION && id == left_action) ||
+ (class_ == STROKE_DIALOG && id == left_command))
+ return kMacLeftSelectColor;
+ if ((class_ == STROKE_COMMAND && id == right_command) ||
+ (class_ == STROKE_INVEN && id == active_inven) ||
+ (class_ == STROKE_ACTION && id == right_action))
+ return kMacRightSelectColor;
+ return kMacNormalTextColor;
+}
+
+static void drawMacInterfaceText(Graphics::ManagedSurface &panel,
+ const Graphics::Font &font, int class_, int id,
+ const Common::String &text) {
+ Common::Rect logical;
+ if (text.empty() || !getMacInterfaceSpot(class_, id, logical))
+ return;
+
+ // CODE 7 scales the logical x coordinate from 320 to 512 pixels and
+ // converts the logical y coordinate into a QuickDraw baseline with
+ // y * 2 + 11. Reproduce that transform instead of centering the font in
+ // the compatibility rectangle.
+ const int left = macInterfaceX(logical.left);
+ const int right = macInterfaceX(logical.right);
+ const int width = MAX(0, right - left);
+ const int y = macInterfaceY(logical.top) + 11 - font.getFontAscent();
+ if (width > 0 && y < kMacInterfaceHeight)
+ font.drawString(&panel, text, left, y, width,
+ getMacInterfaceTextColor(class_, id));
+}
+
+static Common::String getMacInterfaceWord(int wordId) {
+ char text[80] = { 0 };
+ Common::strcpy_s(text, vocab_string(wordId));
+ if (text[0])
+ text[0] = (char)toupper((byte)text[0]);
+ return Common::String(text);
+}
+
+static void setMacInterfacePixel(Graphics::ManagedSurface &panel,
+ int x, int y, byte color) {
+ if (x >= 0 && x < panel.w && y >= 0 && y < panel.h)
+ *(byte *)panel.getBasePtr(x, y) = color;
+}
+
+static void drawMacInterfaceArrow(Graphics::ManagedSurface &panel,
+ const Common::Rect &rect, bool up, byte color) {
+ if (rect.isEmpty())
+ return;
+
+ const int centerX = (rect.left + rect.right - 1) / 2;
+ const int centerY = (rect.top + rect.bottom - 1) / 2;
+ const int radius = MAX(1, MIN((rect.width() - 1) / 2,
+ (rect.height() - 1) / 2));
+ for (int row = 0; row <= radius; ++row) {
+ const int y = up ? centerY - radius / 2 + row :
+ centerY + radius / 2 - row;
+ for (int x = centerX - row; x <= centerX + row; ++x)
+ setMacInterfacePixel(panel, x, y, color);
+ }
+}
+
+static Common::Rect getScaledMacInterfaceSpot(int class_, int id) {
+ Common::Rect logical;
+ return getMacInterfaceSpot(class_, id, logical) ?
+ scaleMacInterfaceRect(logical) : Common::Rect();
+}
+
+static void drawMacInterfaceScrollbar(Graphics::ManagedSurface &panel) {
+ const Common::Rect up = getScaledMacInterfaceSpot(STROKE_SCROLL, SCROLL_UP);
+ const Common::Rect down = getScaledMacInterfaceSpot(STROKE_SCROLL, SCROLL_DOWN);
+ const Common::Rect elevator =
+ getScaledMacInterfaceSpot(STROKE_SCROLL, SCROLL_ELEVATOR);
+ Common::Rect thumb = getScaledMacInterfaceSpot(STROKE_SCROLL, SCROLL_THUMB);
+
+ if (!elevator.isEmpty())
+ panel.frameRect(elevator, scrollbar_active == SCROLL_ELEVATOR ?
+ kMacLeftSelectColor : kMacNormalTextColor);
+ drawMacInterfaceArrow(panel, up, true,
+ scrollbar_active == SCROLL_UP ? kMacLeftSelectColor : kMacNormalTextColor);
+ drawMacInterfaceArrow(panel, down, false,
+ scrollbar_active == SCROLL_DOWN ? kMacLeftSelectColor : kMacNormalTextColor);
+
+ if (!thumb.isEmpty() && !elevator.isEmpty()) {
+ thumb.left = MAX<int>(thumb.left, elevator.left + 2);
+ thumb.right = MIN<int>(thumb.right, elevator.right - 2);
+ if (!thumb.isEmpty())
+ panel.fillRect(thumb, scrollbar_active == SCROLL_ELEVATOR ?
+ kMacLeftSelectColor : kMacNormalTextColor);
+ }
+}
+
+static void drawMacInterfaceState(Graphics::ManagedSurface &panel,
+ const Graphics::Font &font) {
+ if (inter_input_mode == INTER_BUILDING_SENTENCES ||
+ inter_input_mode == INTER_LIMITED_SENTENCES) {
+ for (int id = 0; id < INTER_COMMANDS; ++id)
+ drawMacInterfaceText(panel, font, STROKE_COMMAND, id,
+ getMacInterfaceWord(command[id].id));
+ for (int id = first_inven; id < first_inven + inter_columns &&
+ id < inven_num_objects; ++id) {
+ drawMacInterfaceText(panel, font, STROKE_INVEN, id,
+ getMacInterfaceWord(object[inven[id]].vocab_id));
+ }
+ if (active_inven >= 0) {
+ for (int id = 0; id < object[inven[active_inven]].num_verbs; ++id) {
+ drawMacInterfaceText(panel, font, STROKE_ACTION, id,
+ getMacInterfaceWord(object[inven[active_inven]].verb[id].id));
+ }
+ }
+ drawMacInterfaceScrollbar(panel);
+ } else if (inter_input_mode == INTER_CONVERSATION) {
+ for (int id = 0; id < inter_columns; ++id) {
+ if (inter_dialog_strings[id])
+ drawMacInterfaceText(panel, font, STROKE_DIALOG, id,
+ Common::String(inter_dialog_strings[id]));
+ }
+ }
+}
+
+static bool isMacInterfaceSemanticPixel(int x, int y) {
+ Common::Rect rect;
+ if (inter_input_mode == INTER_CONVERSATION) {
+ for (int id = 0; id < inter_columns; ++id) {
+ if (getMacInterfaceSpot(STROKE_DIALOG, id, rect) && rect.contains(x, y))
+ return true;
+ }
+ return false;
+ }
+
+ if (inter_input_mode != INTER_BUILDING_SENTENCES &&
+ inter_input_mode != INTER_LIMITED_SENTENCES)
+ return false;
+
+ for (int id = 0; id < INTER_COMMANDS; ++id) {
+ if (getMacInterfaceSpot(STROKE_COMMAND, id, rect) && rect.contains(x, y))
+ return true;
+ }
+ for (int id = first_inven; id < first_inven + inter_columns; ++id) {
+ if (getMacInterfaceSpot(STROKE_INVEN, id, rect) && rect.contains(x, y))
+ return true;
+ }
+ for (int id = 0; id < OBJECT_MAX_VERBS; ++id) {
+ if (getMacInterfaceSpot(STROKE_ACTION, id, rect) && rect.contains(x, y))
+ return true;
+ }
+ for (int id = SCROLL_UP; id <= SCROLL_THUMB; ++id) {
+ if (getMacInterfaceSpot(STROKE_SCROLL, id, rect) && rect.contains(x, y))
+ return true;
+ }
+ return false;
+}
+
+void RexNebularEngine::initMacintoshGraphics() {
+ initGraphics(kMacScreenWidth, kMacScreenHeight);
+}
+
+bool RexNebularEngine::initMacintoshResources() {
+ _macResources = new MacResourceProvider();
+ if (!_macResources->load())
+ return false;
+
+ env_set_resource_provider(_macResources);
+ _soundManager = new Sound::MacSoundManager(_mixer, _soundFlag, _macResources);
+ return true;
+}
+
+void RexNebularEngine::shutdownMacintoshResources() {
+ if (!_macResources)
+ return;
+
+ delete _soundManager;
+ _soundManager = nullptr;
+ env_set_resource_provider(nullptr);
+ delete _macResources;
+ _macResources = nullptr;
+}
+
+void RexNebularEngine::applyGameSettings() {
+ Engine::applyGameSettings();
+
+ // The Macintosh port's 640x400 large-window mode uses square pixels.
+ // DOS-style 320x200 aspect correction would stretch its scene and native
+ // interface vertically.
+ if (getPlatform() == Common::kPlatformMacintosh &&
+ g_system->hasFeature(OSystem::kFeatureAspectRatioCorrection) &&
+ g_system->getFeatureState(OSystem::kFeatureAspectRatioCorrection)) {
+ g_system->beginGFXTransaction();
+ g_system->setFeatureState(OSystem::kFeatureAspectRatioCorrection, false);
+ g_system->endGFXTransaction();
+ }
+}
+
+Common::Point RexNebularEngine::screenToGame(const Common::Point &point) const {
+ if (getPlatform() != Common::kPlatformMacintosh)
+ return MADSEngine::screenToGame(point);
+
+ if (point.y >= 0 && point.y < kMacSceneHeight)
+ return Common::Point(CLIP<int>(point.x / 2, 0, 319), point.y / 2);
+
+ if (point.y >= kMacSceneHeight && point.y < kMacScreenHeight &&
+ point.x >= kMacInterfaceX &&
+ point.x < kMacInterfaceX + kMacInterfaceWidth) {
+ return Common::Point(
+ (point.x - kMacInterfaceX) * 320 / kMacInterfaceWidth,
+ 156 + (point.y - kMacSceneHeight) / 2);
+ }
+
+ // The native interface is narrower than the scene. Its side gutters are
+ // intentionally inactive rather than being stretched into hotspots.
+ return Common::Point(-1, -1);
+}
+
+Common::Point RexNebularEngine::gameToScreen(const Common::Point &point) const {
+ if (getPlatform() != Common::kPlatformMacintosh)
+ return MADSEngine::gameToScreen(point);
+
+ if (point.y < 156)
+ return Common::Point(point.x * 2, point.y * 2);
+
+ return Common::Point(kMacInterfaceX + point.x * kMacInterfaceWidth / 320,
+ kMacSceneHeight + (point.y - 156) * 2);
+}
+
+void RexNebularEngine::presentScreen(int shakeOffset) {
+ if (getPlatform() != Common::kPlatformMacintosh) {
+ MADSEngine::presentScreen(shakeOffset);
+ return;
+ }
+
+ _macOutput.resize(kMacScreenWidth * kMacScreenHeight);
+ memset(_macOutput.data(), kMacBlackColor, _macOutput.size());
+
+ // Native large-window mode doubles the 320x156 scene in both axes.
+ for (int y = 0; y < 156; ++y) {
+ const byte *source = (const byte *)_screen->getBasePtr(0, y);
+ byte *line1 = _macOutput.data() + (y * 2) * kMacScreenWidth;
+ byte *line2 = line1 + kMacScreenWidth;
+ for (int x = 0; x < 320; ++x) {
+ const byte color = source[(x + shakeOffset) % 320];
+ line1[x * 2] = color;
+ line1[x * 2 + 1] = color;
+ }
+ memcpy(line2, line1, kMacScreenWidth);
+ }
+
+ const Graphics::Surface *nativeInterface =
+ _macResources ? _macResources->getNativeInterface() : nullptr;
+ const Graphics::Surface *logicalInterface =
+ _macResources ? _macResources->getLogicalInterface() : nullptr;
+ if (nativeInterface && nativeInterface->w == kMacInterfaceWidth &&
+ nativeInterface->h == kMacInterfaceHeight &&
+ nativeInterface->format.bytesPerPixel == 1) {
+ Graphics::ManagedSurface panel;
+ panel.create(kMacInterfaceWidth, kMacInterfaceHeight,
+ Graphics::PixelFormat::createFormatCLUT8());
+ for (int y = 0; y < kMacInterfaceHeight; ++y)
+ memcpy(panel.getBasePtr(0, y), nativeInterface->getBasePtr(0, y),
+ kMacInterfaceWidth);
+
+ const Graphics::Font *interfaceFont =
+ _macResources ? _macResources->getInterfaceFont() : nullptr;
+
+ // Keep live inventory artwork and other non-text changes produced by
+ // the shared interface state machine. Semantic regions are redrawn
+ // below, avoiding enlarged compatibility-font glyphs and FONTMISC's
+ // private scrollbar character convention.
+ if (logicalInterface && logicalInterface->w == 320 &&
+ logicalInterface->h == 44 &&
+ logicalInterface->format.bytesPerPixel == 1) {
+ for (int y = 0; y < kMacInterfaceHeight; ++y) {
+ byte *target = (byte *)panel.getBasePtr(0, y);
+ const int logicalY = y / 2;
+ const byte *current =
+ (const byte *)_screen->getBasePtr(0, 156 + logicalY);
+ const byte *baseline =
+ (const byte *)logicalInterface->getBasePtr(0, logicalY);
+ for (int x = 0; x < kMacInterfaceWidth; ++x) {
+ const int logicalX = x * 320 / kMacInterfaceWidth;
+ if (current[logicalX] != baseline[logicalX] &&
+ (!interfaceFont ||
+ !isMacInterfaceSemanticPixel(logicalX, logicalY)))
+ target[x] = current[logicalX];
+ }
+ }
+ }
+
+ if (interfaceFont)
+ drawMacInterfaceState(panel, *interfaceFont);
+
+ // Apply the blue layer after composing artwork, live state, controls,
+ // and text so the whole native panel receives one uniform treatment.
+ byte washLUT[256];
+ buildMacPanelWashLUT(_macResources->getNativeInterfacePalette(), washLUT);
+ for (int y = 0; y < kMacInterfaceHeight; ++y) {
+ byte *target = (byte *)panel.getBasePtr(0, y);
+ for (int x = 0; x < kMacInterfaceWidth; ++x)
+ target[x] = washLUT[target[x]];
+ }
+
+ for (int y = 0; y < kMacInterfaceHeight; ++y) {
+ memcpy(_macOutput.data() +
+ (kMacSceneHeight + y) * kMacScreenWidth + kMacInterfaceX,
+ panel.getBasePtr(0, y), kMacInterfaceWidth);
+ }
+ } else {
+ // 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) {
+ const byte *source = (const byte *)_screen->getBasePtr(0, 156 + y / 2);
+ byte *target = _macOutput.data() +
+ (kMacSceneHeight + y) * kMacScreenWidth + kMacInterfaceX;
+ for (int x = 0; x < kMacInterfaceWidth; ++x)
+ target[x] = source[x * 320 / kMacInterfaceWidth];
+ }
+ }
+
+ if (_macPopupActive && !_macPopup.empty()) {
+ for (int y = 0; y < _macPopup.h; ++y) {
+ const int targetY = _macPopupRect.top + y;
+ if (targetY < 0 || targetY >= kMacScreenHeight)
+ continue;
+
+ const int targetX = MAX<int>(0, _macPopupRect.left);
+ const int sourceX = targetX - _macPopupRect.left;
+ const int width = MIN<int>(_macPopup.w - sourceX,
+ kMacScreenWidth - targetX);
+ if (width > 0)
+ memcpy(_macOutput.data() + targetY * kMacScreenWidth + targetX,
+ _macPopup.getBasePtr(sourceX, y), width);
+ }
+ }
+
+ if (!_macLayoutLogged) {
+ debug(2, "Presenting Macintosh Rex as 640x312 scene plus centered 512x88 interface");
+ _macLayoutLogged = true;
+ }
+
+ g_system->copyRectToScreen(_macOutput.data(), kMacScreenWidth,
+ 0, 0, kMacScreenWidth, kMacScreenHeight);
+ g_system->updateScreen();
+ _screen->clearDirtyRects();
+}
+
+void RexNebularEngine::showMacPopup() {
+ if (getPlatform() != Common::kPlatformMacintosh || !_macResources || !box)
+ return;
+
+ const Graphics::Font *font = _macResources->getDialogFont();
+ if (!font)
+ return;
+
+ const int width = CLIP<int>(((box->horiz_pieces * 11 + 20) * 15) / 16, 40,
+ kMacScreenWidth - 2);
+
+ // The shared parser has already wrapped text with the compatibility
+ // packed font. Join ordinary runs and wrap them again with native Geneva
+ // metrics; structural bars, underlines, blank lines, and tabs remain
+ // separate records.
+ Common::Array<MacPopupLine> lines;
+ Common::String paragraph;
+ word paragraphTab = 0;
+ for (int line = 0; line <= box->text_y; ++line) {
+ const word tab = box->tab[line];
+ const word plainTab = tab & ~(POPUP_UNDERLINE | POPUP_DOWNPIXEL);
+ const bool structural = tab == POPUP_BAR ||
+ (tab & (POPUP_UNDERLINE | POPUP_DOWNPIXEL)) || !box->text[line][0];
+ if (structural || (!paragraph.empty() && plainTab != paragraphTab)) {
+ if (!paragraph.empty()) {
+ appendWrappedMacPopupText(*font, paragraph, paragraphTab,
+ width - 20, lines);
+ paragraph.clear();
+ }
+ }
+
+ if (structural) {
+ lines.push_back(MacPopupLine(box->text[line], tab));
+ } else {
+ if (paragraph.empty())
+ paragraphTab = plainTab;
+ else
+ paragraph += ' ';
+ paragraph += box->text[line];
+ }
+ }
+ if (!paragraph.empty())
+ appendWrappedMacPopupText(*font, paragraph, paragraphTab,
+ width - 20, lines);
+
+ const int height = CLIP<int>((int)lines.size() * 12 + 20, 20,
+ kMacScreenHeight - 2);
+ _macPopup.create(width, height, Graphics::PixelFormat::createFormatCLUT8());
+ _macPopup.fillRect(Common::Rect(width, height), kMacPopupColor);
+ _macPopup.frameRect(Common::Rect(width, height), kMacBlackColor);
+
+ const int ascent = font->getFontAscent() >= 0 ?
+ font->getFontAscent() : font->getFontHeight();
+ for (uint line = 0; line < lines.size(); ++line) {
+ const int baseline = 22 + line * 12 +
+ ((lines[line].tab & POPUP_DOWNPIXEL) ? 1 : 0);
+ if (baseline - ascent >= height)
+ break;
+
+ if (lines[line].tab == POPUP_BAR) {
+ _macPopup.fillRect(Common::Rect(2, baseline - 5,
+ width - 2, baseline - 4), kMacBlackColor);
+ continue;
+ }
+
+ const int textWidth = font->getStringWidth(lines[line].text);
+ const int x = (lines[line].tab & POPUP_UNDERLINE) ?
+ (width - textWidth) / 2 :
+ 10 + (lines[line].tab &
+ ~(POPUP_UNDERLINE | POPUP_DOWNPIXEL)) * 3 / 4;
+ font->drawString(&_macPopup, lines[line].text, x, baseline - ascent,
+ MAX(0, width - x - 2), kMacBlackColor);
+ if (lines[line].tab & POPUP_UNDERLINE) {
+ _macPopup.fillRect(Common::Rect(x, baseline + 1,
+ MIN(width - 2, x + textWidth), baseline + 2),
+ kMacBlackColor);
+ }
+ }
+
+ _macPopupRect = Common::Rect(
+ (kMacScreenWidth - width) / 2,
+ (kMacScreenHeight - height) / 2,
+ (kMacScreenWidth + width) / 2,
+ (kMacScreenHeight + height) / 2);
+ _macPopupActive = true;
+ presentScreen(0);
+}
+
+void RexNebularEngine::hideMacPopup() {
+ if (!_macPopupActive)
+ return;
+
+ _macPopupActive = false;
+ _macPopup.free();
+ presentScreen(0);
+}
+
+} // namespace RexNebular
+} // namespace MADS
diff --git a/engines/mads/nebular/nebular.cpp b/engines/mads/nebular/nebular.cpp
index 997e6d5f010..93ec941e59b 100644
--- a/engines/mads/nebular/nebular.cpp
+++ b/engines/mads/nebular/nebular.cpp
@@ -20,10 +20,6 @@
*/
#include "engines/util.h"
-#include "common/debug.h"
-#include "common/system.h"
-#include "graphics/font.h"
-#include "graphics/managed_surface.h"
#include "mads/core/mps_installer.h"
#include "mads/core/attr.h"
#include "mads/core/config.h"
@@ -34,9 +30,7 @@
#include "mads/core/inter.h"
#include "mads/core/kernel.h"
#include "mads/core/matte.h"
-#include "mads/core/mcga.h"
#include "mads/core/object.h"
-#include "mads/core/pal.h"
#include "mads/core/rail.h"
#include "mads/core/screen.h"
#include "mads/core/sound.h"
@@ -45,13 +39,11 @@
#include "mads/nebular/console.h"
#include "mads/nebular/copy.h"
#include "mads/nebular/global.h"
-#include "mads/nebular/mac_resources.h"
#include "mads/nebular/main.h"
#include "mads/nebular/popup.h"
#include "mads/nebular/mads/inventory.h"
#include "mads/nebular/mads/words.h"
#include "mads/nebular/sound/sound.h"
-#include "mads/nebular/sound/mac_sound.h"
#include "mads/nebular/rooms/section1.h"
#include "mads/nebular/rooms/section2.h"
#include "mads/nebular/rooms/section3.h"
@@ -64,370 +56,6 @@
namespace MADS {
namespace RexNebular {
-enum {
- kMacScreenWidth = 640,
- kMacSceneHeight = 312,
- kMacInterfaceWidth = 512,
- kMacInterfaceHeight = 88,
- kMacInterfaceX = (kMacScreenWidth - kMacInterfaceWidth) / 2,
- kMacScreenHeight = kMacSceneHeight + kMacInterfaceHeight,
- kMacBlackColor = 8,
- kMacNormalTextColor = 15,
- kMacLeftSelectColor = 13,
- kMacRightSelectColor = 14,
- kMacPopupColor = REX_DIALOG_FE_COLOR,
- kMacPanelTintR = 22,
- kMacPanelTintG = 39,
- kMacPanelTintB = 42
-};
-
-struct MacPopupLine {
- Common::String text;
- word tab;
-
- MacPopupLine(const Common::String &lineText, word lineTab) :
- text(lineText), tab(lineTab) {}
-};
-
-static void appendWrappedMacPopupText(const Graphics::Font &font,
- const Common::String &text, word tab, int width,
- Common::Array<MacPopupLine> &lines) {
- Common::Array<Common::String> wrapped;
- font.wordWrapText(text, width, wrapped);
- for (uint i = 0; i < wrapped.size(); ++i)
- lines.push_back(MacPopupLine(wrapped[i], tab));
-}
-
-static int macPanelColorDistance(int r1, int g1, int b1,
- int r2, int g2, int b2) {
- const int dr = r1 - r2;
- const int dg = g1 - g2;
- const int db = b1 - b2;
- return dr * dr + dg * dg + db * db;
-}
-
-static void buildMacPanelWashLUT(const byte *nativePalette, byte washLUT[256]) {
- // The panel is still presented as CLUT8. Replace its first eight display
- // colors with a uniform 25-percent source / 75-percent cyan blend, then
- // map the completed panel to those colors. This models a translucent wash
- // without the checkerboard pattern used by the earlier approximation.
- Palette displayPalette;
- for (int color = 0; color < 8; ++color) {
- const int r = (nativePalette[color * 3 + 0] * 63 + 127) / 255;
- const int g = (nativePalette[color * 3 + 1] * 63 + 127) / 255;
- const int b = (nativePalette[color * 3 + 2] * 63 + 127) / 255;
- displayPalette[color].r = (r + 3 * kMacPanelTintR + 2) / 4;
- displayPalette[color].g = (g + 3 * kMacPanelTintG + 2) / 4;
- displayPalette[color].b = (b + 3 * kMacPanelTintB + 2) / 4;
- }
-
- // Index 8 backs the side gutters, popup text, and caption shadow. The
- // normal MADS interface palette makes it dark gray, not Macintosh black.
- displayPalette[kMacBlackColor].r = 0;
- displayPalette[kMacBlackColor].g = 0;
- displayPalette[kMacBlackColor].b = 0;
- mcga_setpal_range(&displayPalette, 0, kMacBlackColor + 1);
-
- // Keep the action caption face independent of room palette changes.
- displayPalette[kMacNormalTextColor].r = 63;
- displayPalette[kMacNormalTextColor].g = 63;
- displayPalette[kMacNormalTextColor].b = 63;
- mcga_setpal_range(&displayPalette, kMacNormalTextColor, 1);
-
- for (int sourceColor = 0; sourceColor < 256; ++sourceColor) {
- int sourceR, sourceG, sourceB;
- if (sourceColor < 8 || sourceColor == kMacLeftSelectColor ||
- sourceColor == kMacRightSelectColor) {
- const int nativeColor = sourceColor == kMacLeftSelectColor ? 1 :
- (sourceColor == kMacRightSelectColor ? 2 : sourceColor);
- sourceR = (nativePalette[nativeColor * 3 + 0] * 63 + 127) / 255;
- sourceG = (nativePalette[nativeColor * 3 + 1] * 63 + 127) / 255;
- sourceB = (nativePalette[nativeColor * 3 + 2] * 63 + 127) / 255;
- } else if (sourceColor == kMacNormalTextColor) {
- sourceR = sourceG = sourceB = 63;
- } else {
- sourceR = master_palette[sourceColor].r;
- sourceG = master_palette[sourceColor].g;
- sourceB = master_palette[sourceColor].b;
- }
-
- const int washedR = (sourceR + 3 * kMacPanelTintR + 2) / 4;
- const int washedG = (sourceG + 3 * kMacPanelTintG + 2) / 4;
- const int washedB = (sourceB + 3 * kMacPanelTintB + 2) / 4;
- int bestColor = 0;
- int bestDistance = macPanelColorDistance(washedR, washedG, washedB,
- displayPalette[0].r, displayPalette[0].g, displayPalette[0].b);
- for (int candidate = 1; candidate < 8; ++candidate) {
- const int distance = macPanelColorDistance(washedR, washedG, washedB,
- displayPalette[candidate].r, displayPalette[candidate].g,
- displayPalette[candidate].b);
- if (distance < bestDistance) {
- bestDistance = distance;
- bestColor = candidate;
- }
- }
- washLUT[sourceColor] = bestColor;
- }
-}
-
-static bool getMacInterfaceSpot(int class_, int id, Common::Rect &rect) {
- int row = 0;
- int col = 0;
- int baseX = 0;
- int baseY = 0;
- int deltaX = 0;
-
- switch (class_) {
- case STROKE_COMMAND:
- row = id % inter_columns;
- col = id / inter_columns;
- baseX = command_base_x;
- baseY = command_base_y;
- deltaX = command_delta_x;
- break;
- case STROKE_INVEN:
- if (id < first_inven || id >= first_inven + inter_columns)
- return false;
- row = id - first_inven;
- baseX = inven_base_x;
- baseY = inven_base_y;
- deltaX = inven_delta_x;
- break;
- case STROKE_DIALOG:
- row = id;
- baseX = command_base_x;
- baseY = command_base_y;
- deltaX = dialog_delta_x;
- break;
- case STROKE_SCROLL:
- baseX = inter_scroll_x1 + (id == SCROLL_THUMB ? 2 : 0);
- deltaX = inter_scroll_x2 - inter_scroll_x1 + 1;
- switch (id) {
- case SCROLL_UP:
- baseY = inter_up_y1;
- row = 0;
- break;
- case SCROLL_DOWN:
- baseY = inter_down_y1;
- row = 0;
- break;
- case SCROLL_ELEVATOR:
- baseY = inter_elevator_y1;
- row = 0;
- break;
- case SCROLL_THUMB:
- baseY = inter_thumb_y1 + scrollbar_elevator;
- row = 0;
- break;
- default:
- return false;
- }
- break;
- case STROKE_ACTION:
- row = id;
- baseX = action_base_x;
- baseY = action_base_y;
- deltaX = action_delta_x;
- break;
- default:
- return false;
- }
-
- int height = inter_delta_y;
- if (class_ == STROKE_SCROLL) {
- switch (id) {
- case SCROLL_UP:
- height = inter_up_y2 - inter_up_y1 + 1;
- break;
- case SCROLL_DOWN:
- height = inter_down_y2 - inter_down_y1 + 1;
- break;
- case SCROLL_ELEVATOR:
- height = inter_elevator_y2 - inter_elevator_y1 + 1;
- break;
- case SCROLL_THUMB:
- height = 1;
- break;
- }
- }
-
- const int x = baseX + col * deltaX;
- const int y = baseY + row * inter_delta_y;
- rect = Common::Rect(x, y, x + deltaX, y + height);
- return true;
-}
-
-static int macInterfaceX(int logicalX) {
- return logicalX * kMacInterfaceWidth / 320;
-}
-
-static int macInterfaceY(int logicalY) {
- return logicalY * 2;
-}
-
-static Common::Rect scaleMacInterfaceRect(const Common::Rect &logical) {
- return Common::Rect(
- CLIP<int>(macInterfaceX(logical.left), 0, kMacInterfaceWidth),
- CLIP<int>(macInterfaceY(logical.top), 0, kMacInterfaceHeight),
- CLIP<int>(macInterfaceX(logical.right), 0, kMacInterfaceWidth),
- CLIP<int>(macInterfaceY(logical.bottom), 0, kMacInterfaceHeight));
-}
-
-static byte getMacInterfaceTextColor(int class_, int id) {
- if ((class_ == STROKE_COMMAND && id == left_command) ||
- (class_ == STROKE_INVEN && id == left_inven) ||
- (class_ == STROKE_ACTION && id == left_action) ||
- (class_ == STROKE_DIALOG && id == left_command))
- return kMacLeftSelectColor;
- if ((class_ == STROKE_COMMAND && id == right_command) ||
- (class_ == STROKE_INVEN && id == active_inven) ||
- (class_ == STROKE_ACTION && id == right_action))
- return kMacRightSelectColor;
- return kMacNormalTextColor;
-}
-
-static void drawMacInterfaceText(Graphics::ManagedSurface &panel,
- const Graphics::Font &font, int class_, int id,
- const Common::String &text) {
- Common::Rect logical;
- if (text.empty() || !getMacInterfaceSpot(class_, id, logical))
- return;
-
- // CODE 7 scales the logical x coordinate from 320 to 512 pixels and
- // converts the logical y coordinate into a QuickDraw baseline with
- // y * 2 + 11. Reproduce that transform instead of centering the font in
- // the compatibility rectangle.
- const int left = macInterfaceX(logical.left);
- const int right = macInterfaceX(logical.right);
- const int width = MAX(0, right - left);
- const int y = macInterfaceY(logical.top) + 11 - font.getFontAscent();
- if (width > 0 && y < kMacInterfaceHeight)
- font.drawString(&panel, text, left, y, width,
- getMacInterfaceTextColor(class_, id));
-}
-
-static Common::String getMacInterfaceWord(int wordId) {
- char text[80] = { 0 };
- Common::strcpy_s(text, vocab_string(wordId));
- if (text[0])
- text[0] = (char)toupper((byte)text[0]);
- return Common::String(text);
-}
-
-static void setMacInterfacePixel(Graphics::ManagedSurface &panel,
- int x, int y, byte color) {
- if (x >= 0 && x < panel.w && y >= 0 && y < panel.h)
- *(byte *)panel.getBasePtr(x, y) = color;
-}
-
-static void drawMacInterfaceArrow(Graphics::ManagedSurface &panel,
- const Common::Rect &rect, bool up, byte color) {
- if (rect.isEmpty())
- return;
-
- const int centerX = (rect.left + rect.right - 1) / 2;
- const int centerY = (rect.top + rect.bottom - 1) / 2;
- const int radius = MAX(1, MIN((rect.width() - 1) / 2,
- (rect.height() - 1) / 2));
- for (int row = 0; row <= radius; ++row) {
- const int y = up ? centerY - radius / 2 + row :
- centerY + radius / 2 - row;
- for (int x = centerX - row; x <= centerX + row; ++x)
- setMacInterfacePixel(panel, x, y, color);
- }
-}
-
-static Common::Rect getScaledMacInterfaceSpot(int class_, int id) {
- Common::Rect logical;
- return getMacInterfaceSpot(class_, id, logical) ?
- scaleMacInterfaceRect(logical) : Common::Rect();
-}
-
-static void drawMacInterfaceScrollbar(Graphics::ManagedSurface &panel) {
- const Common::Rect up = getScaledMacInterfaceSpot(STROKE_SCROLL, SCROLL_UP);
- const Common::Rect down = getScaledMacInterfaceSpot(STROKE_SCROLL, SCROLL_DOWN);
- const Common::Rect elevator =
- getScaledMacInterfaceSpot(STROKE_SCROLL, SCROLL_ELEVATOR);
- Common::Rect thumb = getScaledMacInterfaceSpot(STROKE_SCROLL, SCROLL_THUMB);
-
- if (!elevator.isEmpty())
- panel.frameRect(elevator, scrollbar_active == SCROLL_ELEVATOR ?
- kMacLeftSelectColor : kMacNormalTextColor);
- drawMacInterfaceArrow(panel, up, true,
- scrollbar_active == SCROLL_UP ? kMacLeftSelectColor : kMacNormalTextColor);
- drawMacInterfaceArrow(panel, down, false,
- scrollbar_active == SCROLL_DOWN ? kMacLeftSelectColor : kMacNormalTextColor);
-
- if (!thumb.isEmpty() && !elevator.isEmpty()) {
- thumb.left = MAX<int>(thumb.left, elevator.left + 2);
- thumb.right = MIN<int>(thumb.right, elevator.right - 2);
- if (!thumb.isEmpty())
- panel.fillRect(thumb, scrollbar_active == SCROLL_ELEVATOR ?
- kMacLeftSelectColor : kMacNormalTextColor);
- }
-}
-
-static void drawMacInterfaceState(Graphics::ManagedSurface &panel,
- const Graphics::Font &font) {
- if (inter_input_mode == INTER_BUILDING_SENTENCES ||
- inter_input_mode == INTER_LIMITED_SENTENCES) {
- for (int id = 0; id < INTER_COMMANDS; ++id)
- drawMacInterfaceText(panel, font, STROKE_COMMAND, id,
- getMacInterfaceWord(command[id].id));
- for (int id = first_inven; id < first_inven + inter_columns &&
- id < inven_num_objects; ++id) {
- drawMacInterfaceText(panel, font, STROKE_INVEN, id,
- getMacInterfaceWord(object[inven[id]].vocab_id));
- }
- if (active_inven >= 0) {
- for (int id = 0; id < object[inven[active_inven]].num_verbs; ++id) {
- drawMacInterfaceText(panel, font, STROKE_ACTION, id,
- getMacInterfaceWord(object[inven[active_inven]].verb[id].id));
- }
- }
- drawMacInterfaceScrollbar(panel);
- } else if (inter_input_mode == INTER_CONVERSATION) {
- for (int id = 0; id < inter_columns; ++id) {
- if (inter_dialog_strings[id])
- drawMacInterfaceText(panel, font, STROKE_DIALOG, id,
- Common::String(inter_dialog_strings[id]));
- }
- }
-}
-
-static bool isMacInterfaceSemanticPixel(int x, int y) {
- Common::Rect rect;
- if (inter_input_mode == INTER_CONVERSATION) {
- for (int id = 0; id < inter_columns; ++id) {
- if (getMacInterfaceSpot(STROKE_DIALOG, id, rect) && rect.contains(x, y))
- return true;
- }
- return false;
- }
-
- if (inter_input_mode != INTER_BUILDING_SENTENCES &&
- inter_input_mode != INTER_LIMITED_SENTENCES)
- return false;
-
- for (int id = 0; id < INTER_COMMANDS; ++id) {
- if (getMacInterfaceSpot(STROKE_COMMAND, id, rect) && rect.contains(x, y))
- return true;
- }
- for (int id = first_inven; id < first_inven + inter_columns; ++id) {
- if (getMacInterfaceSpot(STROKE_INVEN, id, rect) && rect.contains(x, y))
- return true;
- }
- for (int id = 0; id < OBJECT_MAX_VERBS; ++id) {
- if (getMacInterfaceSpot(STROKE_ACTION, id, rect) && rect.contains(x, y))
- return true;
- }
- for (int id = SCROLL_UP; id <= SCROLL_THUMB; ++id) {
- if (getMacInterfaceSpot(STROKE_SCROLL, id, rect) && rect.contains(x, y))
- return true;
- }
- return false;
-}
-
RexNebularEngine::RexNebularEngine(OSystem *syst, const MADSGameDescription *gameDesc) :
MADSEngine(syst, gameDesc) {
// Initialize globals
@@ -435,18 +63,13 @@ RexNebularEngine::RexNebularEngine(OSystem *syst, const MADSGameDescription *gam
}
RexNebularEngine::~RexNebularEngine() {
- if (_macResources) {
- delete _soundManager;
- _soundManager = nullptr;
- env_set_resource_provider(nullptr);
- delete _macResources;
- }
+ shutdownMacintoshResources();
}
Common::Error RexNebularEngine::run() {
const bool isMacintosh = getPlatform() == Common::kPlatformMacintosh;
if (isMacintosh)
- initGraphics(kMacScreenWidth, kMacScreenHeight);
+ initMacintoshGraphics();
else
initGraphics(320, 200);
applyGameSettings();
@@ -469,12 +92,9 @@ Common::Error RexNebularEngine::run() {
// Set up the platform resource and sound providers
if (isMacintosh) {
- _macResources = new MacResourceProvider();
- if (!_macResources->load())
+ if (!initMacintoshResources())
return Common::Error(Common::kNoGameDataFoundError,
"Could not open the Macintosh Rex resource files");
- env_set_resource_provider(_macResources);
- _soundManager = new Sound::MacSoundManager(_mixer, _soundFlag, _macResources);
} else {
_soundManager = new Sound::RexSoundManager(_mixer, _soundFlag, isDemo());
}
@@ -486,269 +106,6 @@ Common::Error RexNebularEngine::run() {
return Common::kNoError;
}
-void RexNebularEngine::applyGameSettings() {
- Engine::applyGameSettings();
-
- // The Macintosh port's 640x400 large-window mode uses square pixels.
- // DOS-style 320x200 aspect correction would stretch its scene and native
- // interface vertically.
- if (getPlatform() == Common::kPlatformMacintosh &&
- g_system->hasFeature(OSystem::kFeatureAspectRatioCorrection) &&
- g_system->getFeatureState(OSystem::kFeatureAspectRatioCorrection)) {
- g_system->beginGFXTransaction();
- g_system->setFeatureState(OSystem::kFeatureAspectRatioCorrection, false);
- g_system->endGFXTransaction();
- }
-}
-
-Common::Point RexNebularEngine::screenToGame(const Common::Point &point) const {
- if (getPlatform() != Common::kPlatformMacintosh)
- return MADSEngine::screenToGame(point);
-
- if (point.y >= 0 && point.y < kMacSceneHeight)
- return Common::Point(CLIP<int>(point.x / 2, 0, 319), point.y / 2);
-
- if (point.y >= kMacSceneHeight && point.y < kMacScreenHeight &&
- point.x >= kMacInterfaceX &&
- point.x < kMacInterfaceX + kMacInterfaceWidth) {
- return Common::Point(
- (point.x - kMacInterfaceX) * 320 / kMacInterfaceWidth,
- 156 + (point.y - kMacSceneHeight) / 2);
- }
-
- // The native interface is narrower than the scene. Its side gutters are
- // intentionally inactive rather than being stretched into hotspots.
- return Common::Point(-1, -1);
-}
-
-Common::Point RexNebularEngine::gameToScreen(const Common::Point &point) const {
- if (getPlatform() != Common::kPlatformMacintosh)
- return MADSEngine::gameToScreen(point);
-
- if (point.y < 156)
- return Common::Point(point.x * 2, point.y * 2);
-
- return Common::Point(kMacInterfaceX + point.x * kMacInterfaceWidth / 320,
- kMacSceneHeight + (point.y - 156) * 2);
-}
-
-void RexNebularEngine::presentScreen(int shakeOffset) {
- if (getPlatform() != Common::kPlatformMacintosh) {
- MADSEngine::presentScreen(shakeOffset);
- return;
- }
-
- _macOutput.resize(kMacScreenWidth * kMacScreenHeight);
- memset(_macOutput.data(), kMacBlackColor, _macOutput.size());
-
- // Native large-window mode doubles the 320x156 scene in both axes.
- for (int y = 0; y < 156; ++y) {
- const byte *source = (const byte *)_screen->getBasePtr(0, y);
- byte *line1 = _macOutput.data() + (y * 2) * kMacScreenWidth;
- byte *line2 = line1 + kMacScreenWidth;
- for (int x = 0; x < 320; ++x) {
- const byte color = source[(x + shakeOffset) % 320];
- line1[x * 2] = color;
- line1[x * 2 + 1] = color;
- }
- memcpy(line2, line1, kMacScreenWidth);
- }
-
- const Graphics::Surface *nativeInterface =
- _macResources ? _macResources->getNativeInterface() : nullptr;
- const Graphics::Surface *logicalInterface =
- _macResources ? _macResources->getLogicalInterface() : nullptr;
- if (nativeInterface && nativeInterface->w == kMacInterfaceWidth &&
- nativeInterface->h == kMacInterfaceHeight &&
- nativeInterface->format.bytesPerPixel == 1) {
- Graphics::ManagedSurface panel;
- panel.create(kMacInterfaceWidth, kMacInterfaceHeight,
- Graphics::PixelFormat::createFormatCLUT8());
- for (int y = 0; y < kMacInterfaceHeight; ++y)
- memcpy(panel.getBasePtr(0, y), nativeInterface->getBasePtr(0, y),
- kMacInterfaceWidth);
-
- const Graphics::Font *interfaceFont =
- _macResources ? _macResources->getInterfaceFont() : nullptr;
-
- // Keep live inventory artwork and other non-text changes produced by
- // the shared interface state machine. Semantic regions are redrawn
- // below, avoiding enlarged compatibility-font glyphs and FONTMISC's
- // private scrollbar character convention.
- if (logicalInterface && logicalInterface->w == 320 &&
- logicalInterface->h == 44 &&
- logicalInterface->format.bytesPerPixel == 1) {
- for (int y = 0; y < kMacInterfaceHeight; ++y) {
- byte *target = (byte *)panel.getBasePtr(0, y);
- const int logicalY = y / 2;
- const byte *current =
- (const byte *)_screen->getBasePtr(0, 156 + logicalY);
- const byte *baseline =
- (const byte *)logicalInterface->getBasePtr(0, logicalY);
- for (int x = 0; x < kMacInterfaceWidth; ++x) {
- const int logicalX = x * 320 / kMacInterfaceWidth;
- if (current[logicalX] != baseline[logicalX] &&
- (!interfaceFont ||
- !isMacInterfaceSemanticPixel(logicalX, logicalY)))
- target[x] = current[logicalX];
- }
- }
- }
-
- if (interfaceFont)
- drawMacInterfaceState(panel, *interfaceFont);
-
- // Apply the blue layer after composing artwork, live state, controls,
- // and text so the whole native panel receives one uniform treatment.
- byte washLUT[256];
- buildMacPanelWashLUT(_macResources->getNativeInterfacePalette(), washLUT);
- for (int y = 0; y < kMacInterfaceHeight; ++y) {
- byte *target = (byte *)panel.getBasePtr(0, y);
- for (int x = 0; x < kMacInterfaceWidth; ++x)
- target[x] = washLUT[target[x]];
- }
-
- for (int y = 0; y < kMacInterfaceHeight; ++y) {
- memcpy(_macOutput.data() +
- (kMacSceneHeight + y) * kMacScreenWidth + kMacInterfaceX,
- panel.getBasePtr(0, y), kMacInterfaceWidth);
- }
- } else {
- // 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) {
- const byte *source = (const byte *)_screen->getBasePtr(0, 156 + y / 2);
- byte *target = _macOutput.data() +
- (kMacSceneHeight + y) * kMacScreenWidth + kMacInterfaceX;
- for (int x = 0; x < kMacInterfaceWidth; ++x)
- target[x] = source[x * 320 / kMacInterfaceWidth];
- }
- }
-
- if (_macPopupActive && !_macPopup.empty()) {
- for (int y = 0; y < _macPopup.h; ++y) {
- const int targetY = _macPopupRect.top + y;
- if (targetY < 0 || targetY >= kMacScreenHeight)
- continue;
-
- const int targetX = MAX<int>(0, _macPopupRect.left);
- const int sourceX = targetX - _macPopupRect.left;
- const int width = MIN<int>(_macPopup.w - sourceX,
- kMacScreenWidth - targetX);
- if (width > 0)
- memcpy(_macOutput.data() + targetY * kMacScreenWidth + targetX,
- _macPopup.getBasePtr(sourceX, y), width);
- }
- }
-
- if (!_macLayoutLogged) {
- debug(2, "Presenting Macintosh Rex as 640x312 scene plus centered 512x88 interface");
- _macLayoutLogged = true;
- }
-
- g_system->copyRectToScreen(_macOutput.data(), kMacScreenWidth,
- 0, 0, kMacScreenWidth, kMacScreenHeight);
- g_system->updateScreen();
- _screen->clearDirtyRects();
-}
-
-void RexNebularEngine::showMacPopup() {
- if (getPlatform() != Common::kPlatformMacintosh || !_macResources || !box)
- return;
-
- const Graphics::Font *font = _macResources->getDialogFont();
- if (!font)
- return;
-
- const int width = CLIP<int>(((box->horiz_pieces * 11 + 20) * 15) / 16, 40,
- kMacScreenWidth - 2);
-
- // The shared parser has already wrapped text with the compatibility
- // packed font. Join ordinary runs and wrap them again with native Geneva
- // metrics; structural bars, underlines, blank lines, and tabs remain
- // separate records.
- Common::Array<MacPopupLine> lines;
- Common::String paragraph;
- word paragraphTab = 0;
- for (int line = 0; line <= box->text_y; ++line) {
- const word tab = box->tab[line];
- const word plainTab = tab & ~(POPUP_UNDERLINE | POPUP_DOWNPIXEL);
- const bool structural = tab == POPUP_BAR ||
- (tab & (POPUP_UNDERLINE | POPUP_DOWNPIXEL)) || !box->text[line][0];
- if (structural || (!paragraph.empty() && plainTab != paragraphTab)) {
- if (!paragraph.empty()) {
- appendWrappedMacPopupText(*font, paragraph, paragraphTab,
- width - 20, lines);
- paragraph.clear();
- }
- }
-
- if (structural) {
- lines.push_back(MacPopupLine(box->text[line], tab));
- } else {
- if (paragraph.empty())
- paragraphTab = plainTab;
- else
- paragraph += ' ';
- paragraph += box->text[line];
- }
- }
- if (!paragraph.empty())
- appendWrappedMacPopupText(*font, paragraph, paragraphTab,
- width - 20, lines);
-
- const int height = CLIP<int>((int)lines.size() * 12 + 20, 20,
- kMacScreenHeight - 2);
- _macPopup.create(width, height, Graphics::PixelFormat::createFormatCLUT8());
- _macPopup.fillRect(Common::Rect(width, height), kMacPopupColor);
- _macPopup.frameRect(Common::Rect(width, height), kMacBlackColor);
-
- const int ascent = font->getFontAscent() >= 0 ?
- font->getFontAscent() : font->getFontHeight();
- for (uint line = 0; line < lines.size(); ++line) {
- const int baseline = 22 + line * 12 +
- ((lines[line].tab & POPUP_DOWNPIXEL) ? 1 : 0);
- if (baseline - ascent >= height)
- break;
-
- if (lines[line].tab == POPUP_BAR) {
- _macPopup.fillRect(Common::Rect(2, baseline - 5,
- width - 2, baseline - 4), kMacBlackColor);
- continue;
- }
-
- const int textWidth = font->getStringWidth(lines[line].text);
- const int x = (lines[line].tab & POPUP_UNDERLINE) ?
- (width - textWidth) / 2 :
- 10 + (lines[line].tab &
- ~(POPUP_UNDERLINE | POPUP_DOWNPIXEL)) * 3 / 4;
- font->drawString(&_macPopup, lines[line].text, x, baseline - ascent,
- MAX(0, width - x - 2), kMacBlackColor);
- if (lines[line].tab & POPUP_UNDERLINE) {
- _macPopup.fillRect(Common::Rect(x, baseline + 1,
- MIN(width - 2, x + textWidth), baseline + 2),
- kMacBlackColor);
- }
- }
-
- _macPopupRect = Common::Rect(
- (kMacScreenWidth - width) / 2,
- (kMacScreenHeight - height) / 2,
- (kMacScreenWidth + width) / 2,
- (kMacScreenHeight + height) / 2);
- _macPopupActive = true;
- presentScreen(0);
-}
-
-void RexNebularEngine::hideMacPopup() {
- if (!_macPopupActive)
- return;
-
- _macPopupActive = false;
- _macPopup.free();
- presentScreen(0);
-}
-
int RexNebularEngine::main_copy_verify() {
return global_copy_verify();
}
diff --git a/engines/mads/nebular/nebular.h b/engines/mads/nebular/nebular.h
index 4be7752276c..f24553dcb87 100644
--- a/engines/mads/nebular/nebular.h
+++ b/engines/mads/nebular/nebular.h
@@ -53,6 +53,9 @@ private:
bool _macPopupActive = false;
bool _macLayoutLogged = false;
+ void initMacintoshGraphics();
+ bool initMacintoshResources();
+ void shutdownMacintoshResources();
void showRecipe();
protected:
More information about the Scummvm-git-logs
mailing list