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

mgerhardy noreply at scummvm.org
Mon Aug 31 19:43:19 UTC 2026


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

Summary:
1b90e5aae2 MACS2: reduced code duplication in amiga resource loading
db20d5ede4 MACS2: support translating the ui labels for the action bar
ada2aa0d38 MACS2: reset version back to 1 - the game was never officially supported
59e2ff7b3c MACS2: several ux enhancement fixes for the custom action bar
57873094c1 MACS2: use the dialogue font for german special chars
9f6feb29d7 MACS2: convert to enums
54fb2284b9 MACS2: renamed members
95f33ac6be MACS2: cleanup amiga resource loading
0377e6b5b0 MACS2: fixed CID 1685788
eef25e7b2c MACS2: replaced magic number
2b6c76b41a MACS2: fixed portrait palette
a40f1cdd83 MACS2: amiga palette fixes
ca4601c2ec MACS2: reduced code duplication
a307f3d6cb MACS2: amiga cleanup


Commit: 1b90e5aae2727a0b83cb7f938fb3001fc6ca74e3
    https://github.com/scummvm/scummvm/commit/1b90e5aae2727a0b83cb7f938fb3001fc6ca74e3
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-31T21:14:09+02:00

Commit Message:
MACS2: reduced code duplication in amiga resource loading

Changed paths:
    engines/macs2/amiga_archive.h
    engines/macs2/amiga_decode.cpp
    engines/macs2/amiga_decode.h
    engines/macs2/amiga_resources.cpp


diff --git a/engines/macs2/amiga_archive.h b/engines/macs2/amiga_archive.h
index 07995fcb39a..5054ebc5a03 100644
--- a/engines/macs2/amiga_archive.h
+++ b/engines/macs2/amiga_archive.h
@@ -58,15 +58,15 @@ struct AmigaInfoData {
 	uint16 sceneCount = 0;
 	uint16 volumeCount = 0;
 	uint16 mdirSize = 0;
-	/** 16x 12-bit 0x0RGB UI colors from MXIN (Ghidra DAT_002379dc). */
+	/** 16x 12-bit 0x0RGB UI colors from MXIN. */
 	uint16 uiPaletteAmiga[16] = {0};
 	/**
-	 * Panel border line/corner color indices from MXIN (Ghidra g_awPanelBorderColorIndices).
+	 * Panel border line/corner color indices from MXIN.
 	 * Demo: 18,19,20,21,22.
 	 */
 	uint16 panelBorderColorIndices[5] = {0};
 	/**
-	 * Panel darken luminance->color table (Ghidra g_awPanelDarkenColorIndices).
+	 * Panel darken luminance->color table.
 	 * Demo: 28,29,29,60,61,31,62,23. Indexed by (7 - (R4+G4+B4)/0x18).
 	 */
 	uint16 panelDarkenColorIndices[8] = {0};
diff --git a/engines/macs2/amiga_decode.cpp b/engines/macs2/amiga_decode.cpp
index 48bf8984821..0b85b7f1ae5 100644
--- a/engines/macs2/amiga_decode.cpp
+++ b/engines/macs2/amiga_decode.cpp
@@ -50,7 +50,7 @@ bool parseAmigaMxoo(const byte *mxoo, uint32 mxooSize, AmigaMxooInfo &out) {
 	if (READ_BE_UINT16(body + 0x0C) != 0x0101)
 		return false;
 
-	for (uint i = 0; i < 21; i++)
+	for (uint i = 0; i < ARRAYSIZE(out.slotOffsets); i++)
 		out.slotOffsets[i] = READ_BE_UINT32(body + 0x0E + i * 4);
 	out.extraOffset = READ_BE_UINT32(body + 0x62);
 	return true;
@@ -311,8 +311,6 @@ bool convertAmigaPortraitAtlasToDosBlob(const byte *mxoo, uint32 mxooSize, uint3
 		return false;
 
 	const uint32 absExtra = 12 + bodyRelativeExtraOffset;
-	// animateDialoguePortrait @ 0022f79c: copy 14400 bytes; blit D3=D4=D6=0x50,
-	// D5=0xF0. drawSprite plane loop uses 6 separated planes -> 30*80*6=14400.
 	// Atlas is 240x80 (3x80x80 frames). Color from planes 0..4.
 	const uint16 atlasW = 240;
 	const uint16 atlasH = 80;
@@ -374,12 +372,6 @@ bool convertAmigaPortraitAtlasToDosBlob(const byte *mxoo, uint32 mxooSize, uint3
 				   atlas.data() + (uint32)y * atlasW + (uint32)f * frameW,
 				   frameW);
 		}
-		// Keep Amiga COLOR indices 0..31. Demo portraits (OO_*) use only the
-		// copper high bank COLOR17..31 (chrome / skin / navy cap) - the same
-		// registers animateDialoguePortrait blits into on hardware. Remapping
-		// into a private 0xD0 bank drifted from live copper and caused the
-		// Freunde-dialogue palette mismatch vs FS-UAE.
-		// Color 0 stays transparent (drawSprite skips 0).
 		off += frameBytes;
 	}
 	return true;
@@ -402,6 +394,24 @@ static bool decompressPp20ToBuffer(const byte *src, uint32 srcLen, Common::Array
 	return true;
 }
 
+void amiga12ToVga6(uint16 rgb, byte &r6, byte &g6, byte &b6) {
+	const byte r4 = (rgb >> 8) & 0xF;
+	const byte g4 = (rgb >> 4) & 0xF;
+	const byte b4 = rgb & 0xF;
+	r6 = (byte)((r4 * 63) / 15);
+	g6 = (byte)((g4 * 63) / 15);
+	b6 = (byte)((b4 * 63) / 15);
+}
+
+void amiga12ToRgb8(uint16 rgb, byte &r, byte &g, byte &b) {
+	const byte r4 = (rgb >> 8) & 0xF;
+	const byte g4 = (rgb >> 4) & 0xF;
+	const byte b4 = rgb & 0xF;
+	r = (byte)(r4 * 17);
+	g = (byte)(g4 * 17);
+	b = (byte)(b4 * 17);
+}
+
 bool decodeAmigaMxmmSceneBackground(const byte *mxmm, uint32 mxmmSize,
 									Common::Array<byte> &outPixels,
 									Graphics::Palette &outPalette,
@@ -453,19 +463,9 @@ bool decodeAmigaMxmmSceneBackground(const byte *mxmm, uint32 mxmmSize,
 
 	const byte *copper = screen.data() + kAmigaSceneCopperOffset;
 	uint16 base16[16];
-	for (uint i = 0; i < 16; i++)
+	for (uint i = 0; i < ARRAYSIZE(base16); i++)
 		base16[i] = READ_BE_UINT16(copper + i * 2);
 	const byte *lineColors = copper + 0x20; // 200 x 16 x u16BE
-
-	auto amiga12ToRgb8 = [](uint16 rgb, byte &r, byte &g, byte &b) {
-		const byte r4 = (rgb >> 8) & 0xF;
-		const byte g4 = (rgb >> 4) & 0xF;
-		const byte b4 = rgb & 0xF;
-		r = (byte)(r4 * 17);
-		g = (byte)(g4 * 17);
-		b = (byte)(b4 * 17);
-	};
-
 	auto buildPal32 = [&](uint16 y, byte pal32[32][3]) {
 		amiga12ToRgb8(base16[0], pal32[0][0], pal32[0][1], pal32[0][2]);
 		for (uint i = 0; i < 16; i++) {
diff --git a/engines/macs2/amiga_decode.h b/engines/macs2/amiga_decode.h
index bc21397725b..a4de2cc5571 100644
--- a/engines/macs2/amiga_decode.h
+++ b/engines/macs2/amiga_decode.h
@@ -70,6 +70,7 @@ struct AmigaAnimSlotInfo {
 
 bool parseAmigaMxoo(const byte *mxoo, uint32 mxooSize, AmigaMxooInfo &out);
 bool inspectAmigaAnimSlot(const byte *mxoo, uint32 mxooSize, uint32 bodyRelativeOffset, AmigaAnimSlotInfo &out);
+void amiga12ToVga6(uint16 rgb, byte &r6, byte &g6, byte &b6);
 
 /** Decode one frame of planar Amiga anim data to chunky 8bpp (color planes 0..4). */
 bool decodeAmigaPlanarFrame(const byte *planar, uint16 width, uint16 height, uint16 frameIndex,
diff --git a/engines/macs2/amiga_resources.cpp b/engines/macs2/amiga_resources.cpp
index 1c4afeab99d..9923ab88972 100644
--- a/engines/macs2/amiga_resources.cpp
+++ b/engines/macs2/amiga_resources.cpp
@@ -37,7 +37,6 @@
 
 namespace Macs2 {
 
-
 bool Macs2Engine::loadAmigaSceneBackground(uint32 sceneResourceId) {
 	if (!_amigaArchive || sceneResourceId == 0 || sceneResourceId > 0xFFFF)
 		return false;
@@ -81,20 +80,15 @@ bool Macs2Engine::loadAmigaSceneBackground(uint32 sceneResourceId) {
 	// Keep Info UI chrome colors in the high VGA indices used by panel drawing.
 	if (_amigaArchive->getInfo().loaded) {
 		const AmigaInfoData &info = _amigaArchive->getInfo();
-		auto amiga12ToVga6 = [](uint16 rgb, byte &r6, byte &g6, byte &b6) {
-			const byte r4 = (rgb >> 8) & 0xF;
-			const byte g4 = (rgb >> 4) & 0xF;
-			const byte b4 = rgb & 0xF;
-			r6 = (byte)((r4 * 63) / 15);
-			g6 = (byte)((g4 * 63) / 15);
-			b6 = (byte)((b4 * 63) / 15);
-		};
-		for (uint i = 0; i < 16; i++) {
-			byte r6, g6, b6;
-			amiga12ToVga6(info.uiPaletteAmiga[i], r6, g6, b6);
+		for (uint i = 0; i < ARRAYSIZE(info.uiPaletteAmiga); i++) {
 			const uint idx = 0xF0 + i;
 			if (idx >= 256)
 				break;
+			uint16 rgb = info.uiPaletteAmiga[i];
+			byte r6;
+			byte g6;
+			byte b6;
+			amiga12ToVga6(rgb, r6, g6, b6);
 			_palVanilla.set(idx, r6, g6, b6);
 		}
 	}
@@ -105,10 +99,11 @@ bool Macs2Engine::loadAmigaSceneBackground(uint32 sceneResourceId) {
 	buildAmigaPanelRemapTable();
 	applyPaletteDarkening();
 
-	_depthMap.fillRect(Common::Rect(0, 0, kScreenWidth, kGameHeight), 0);
-	_pathfindingMap.fillRect(Common::Rect(0, 0, kScreenWidth, kGameHeight), 0);
-	_shadowMap.fillRect(Common::Rect(0, 0, kScreenWidth, kGameHeight), 0);
-	_hotspotMap.fillRect(Common::Rect(0, 0, kScreenWidth, kGameHeight), 0);
+	Common::Rect screenRect(0, 0, kScreenWidth, kGameHeight);
+	_depthMap.fillRect(screenRect, 0);
+	_pathfindingMap.fillRect(screenRect, 0);
+	_shadowMap.fillRect(screenRect, 0);
+	_hotspotMap.fillRect(screenRect, 0);
 
 	Common::Array<byte> pathMap, depthMap, shadowMap;
 	if (extractAmigaMxmmSceneMaps(mxmm.data(), size, pathMap, depthMap, shadowMap)) {
@@ -216,7 +211,6 @@ bool Macs2Engine::loadAmigaSceneBackground(uint32 sceneResourceId) {
 	return true;
 }
 
-// --- loadAmigaMxffFont (macs2.cpp:2807-2866) ---
 bool Macs2Engine::loadAmigaMxffFont() {
 	if (!_amigaArchive || !_amigaArchive->hasResource(kAmigaResFF, 0))
 		return false;
@@ -340,7 +334,6 @@ bool Macs2Engine::loadAmigaOverlayFont(uint8 resourceIndex) {
 	return true;
 }
 
-// --- loadAmigaCursorResource (macs2.cpp:2868-2894) ---
 bool Macs2Engine::loadAmigaCursorResource(uint16 resourceId, AnimFrame &out) {
 	out = AnimFrame();
 	if (!_amigaArchive || resourceId == 0)
@@ -369,7 +362,6 @@ bool Macs2Engine::loadAmigaCursorResource(uint16 resourceId, AnimFrame &out) {
 	return true;
 }
 
-// --- installAmigaPortraitPalette (macs2.cpp:2896-2937) ---
 void Macs2Engine::installAmigaPortraitPalette(bool copyFromPlayfield) {
 	// Portraits now keep Amiga COLOR indices and share the playfield copper
 	// high bank (COLOR17..31), matching animateDialoguePortrait on hardware.
@@ -384,15 +376,6 @@ void Macs2Engine::installAmigaPortraitPalette(bool copyFromPlayfield) {
 	if (_amigaNativePlayfieldPalette)
 		return;
 
-	auto amiga12ToVga6 = [](uint16 rgb, byte &r6, byte &g6, byte &b6) {
-		const byte r4 = (rgb >> 8) & 0xF;
-		const byte g4 = (rgb >> 4) & 0xF;
-		const byte b4 = rgb & 0xF;
-		r6 = (byte)((r4 * 63) / 15);
-		g6 = (byte)((g4 * 63) / 15);
-		b6 = (byte)((b4 * 63) / 15);
-	};
-
 	static const uint16 kHighBankFallback[15] = {
 		0x0BBA, 0x0EB8, 0x0C96, 0x0A74, 0x0963, 0x0741, 0x0000, 0x049E,
 		0x0C00, 0x0DDC, 0x0EEE, 0x0887, 0x0776, 0x0006, 0x0520
@@ -403,7 +386,7 @@ void Macs2Engine::installAmigaPortraitPalette(bool copyFromPlayfield) {
 	if (_amigaArchive && _amigaArchive->getInfo().loaded)
 		highSrc = &_amigaArchive->getInfo().uiPaletteAmiga[1];
 
-	for (uint i = 0; i < 15; i++) {
+	for (uint i = 0; i < ARRAYSIZE(kHighBankFallback); i++) {
 		byte r6, g6, b6;
 		amiga12ToVga6(highSrc[i], r6, g6, b6);
 		const uint idx = 17 + i;
@@ -411,22 +394,12 @@ void Macs2Engine::installAmigaPortraitPalette(bool copyFromPlayfield) {
 	}
 }
 
-// --- applyAmigaUiPalette (macs2.cpp:2939-2990) ---
 void Macs2Engine::applyAmigaUiPalette() {
 	if (!_amigaArchive || !_amigaArchive->getInfo().loaded)
 		return;
 
 	const AmigaInfoData &info = _amigaArchive->getInfo();
 
-	auto amiga12ToVga6 = [](uint16 rgb, byte &r6, byte &g6, byte &b6) {
-		const byte r4 = (rgb >> 8) & 0xF;
-		const byte g4 = (rgb >> 4) & 0xF;
-		const byte b4 = rgb & 0xF;
-		r6 = (byte)((r4 * 63) / 15);
-		g6 = (byte)((g4 * 63) / 15);
-		b6 = (byte)((b4 * 63) / 15);
-	};
-
 	// Provisional playfield until an MM_* copper list is loaded.
 	// Copper base16 layout: COLOR00 + COLOR17..31 = MXIN ui[0..15]. COLOR01..16
 	// stay a visible ramp (overwritten per-line by scene copper).
@@ -443,7 +416,7 @@ void Macs2Engine::applyAmigaUiPalette() {
 	}
 
 	// Amiga UI colors also live in the high indices used by panel/chrome drawing.
-	for (uint i = 0; i < 16; i++) {
+	for (uint i = 0; i < ARRAYSIZE(info.uiPaletteAmiga); i++) {
 		amiga12ToVga6(info.uiPaletteAmiga[i], r6, g6, b6);
 		const uint idx = 0xF0 + i;
 		if (idx >= Graphics::PALETTE_COUNT)
@@ -456,19 +429,13 @@ void Macs2Engine::applyAmigaUiPalette() {
 	applyPaletteDarkening();
 }
 
-// --- buildAmigaPanelRemapTable (macs2.cpp:2992-3018) ---
 void Macs2Engine::buildAmigaPanelRemapTable() {
-	// Ghidra fill_ui_panel_darken_remap @ 002221fe:
-	// bucket = 7 - (R4+G4+B4)/0x18, then index g_awPanelDarkenColorIndices[bucket].
-	// Those indices are playfield copper slots on Amiga. ScummVM must not paint wood
-	// RGB into those slots (breaks intro/scene art). Instead map buckets onto the
-	// private MXIN UI bank already installed at 0xF0..0xFF (opaque brown ramp).
 	if (_panelRemapTable.size() != 0x100)
 		_panelRemapTable.resize(0x100);
 
 	// MXIN UI[0..5] = wood ramp (BBA..741). UI[6] is 0x000 - never use it for fill.
 	static const byte kUiWood[8] = {0, 1, 2, 3, 4, 5, 2, 3};
-	for (uint i = 0; i < 0x100; i++) {
+	for (uint i = 0; i < 256; i++) {
 		byte r6, g6, b6;
 		_palVanilla.get(i, r6, g6, b6);
 		const uint r4 = (r6 * 15) / 63;
@@ -562,7 +529,6 @@ void Macs2Engine::readAmigaResources() {
 
 	applyAmigaUiPalette();
 
-	// Native MXFF dialogue font (Ghidra drawText @ 00224492). Prefer over DOS MCS glyphs.
 	const bool loadedMxff = loadAmigaMxffFont();
 
 	// Load every OO resource as a GameObject. Object index = resource id + 1.
@@ -700,8 +666,6 @@ void Macs2Engine::readAmigaResources() {
 		loadedObjects++;
 	}
 
-	// Protagonist object slot (OO_0000 -> object 1). Do NOT place him in the room here -
-	// the intro/scene MXMM scripts call moveObject on scene init (same as DOS).
 	GameObject *protagonist = GameObjects::instance()._objects[0];
 	if (protagonist == nullptr) {
 		protagonist = new GameObject();
@@ -715,8 +679,6 @@ void Macs2Engine::readAmigaResources() {
 
 	Scenes::instance()._currentActorIndex = 1;
 
-	// Info MXIN u32 @ offset 8 = starting MM resource id (demo: 40 -> intro MM_0040).
-	// Script-visible scene id = MM id + 1 (Ghidra load_scene_mxmm / FUN_002215fa).
 	uint16 startResourceId = _amigaArchive->getInfo().startSceneResourceId;
 	if (startResourceId == 0 || !_amigaArchive->hasResource(kAmigaResMM, startResourceId)) {
 		if (_amigaArchive->hasResource(kAmigaResMM, 40))
@@ -744,7 +706,6 @@ void Macs2Engine::readAmigaResources() {
 	_scenePaletteMode = 1;
 	_paletteDarkenPercent = 0;
 
-	// Fonts: MXFF dialogue font from DataA (standalone Amiga demo - no DOS MCS).
 	if (!loadedMxff)
 		warning("Amiga: no MXFF font FF_0000 in DataA - text may be missing");
 


Commit: db20d5ede42f442281b64a9b04540e72a0219a19
    https://github.com/scummvm/scummvm/commit/db20d5ede42f442281b64a9b04540e72a0219a19
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-31T21:14:09+02:00

Commit Message:
MACS2: support translating the ui labels for the action bar

Changed paths:
    engines/macs2/actionbar.cpp
    engines/macs2/actionbar.h
    engines/macs2/gameobjects.h
    engines/macs2/macs2.cpp
    engines/macs2/macs2.h
    engines/macs2/view1.cpp


diff --git a/engines/macs2/actionbar.cpp b/engines/macs2/actionbar.cpp
index 6870c42b0c1..d362b19f0fa 100644
--- a/engines/macs2/actionbar.cpp
+++ b/engines/macs2/actionbar.cpp
@@ -23,6 +23,7 @@
 
 #include "common/debug.h"
 #include "common/system.h"
+#include "common/translation.h"
 #include "engines/savestate.h"
 #include "gui/message.h"
 #include "macs2/detection.h"
@@ -33,22 +34,22 @@
 
 namespace Macs2 {
 
-static Common::String getObjectDisplayName(const GameObject *obj) {
-	if (!obj)
-		return Common::String();
-
-	const GameObjects &objects = GameObjects::instance();
-	if (obj->_index < objects._objectNames.size() && !objects._objectNames[obj->_index].empty())
-		return objects._objectNames[obj->_index];
+namespace {
 
-	return Common::String();
+// German source keys for macs2_translation.dat msgctxt "uilabel".
+Common::String uiText(const char *source) {
+	if (g_engine)
+		return g_engine->translateUiLabel(source);
+	return source;
 }
 
+} // namespace
+
 const ActionBar::VerbDef ActionBar::kVerbs[4] = {
-	{"Walk", Script::MouseMode::Walk},
-	{"Look", Script::MouseMode::Look},
-	{"Use", Script::MouseMode::Use},
-	{"Talk", Script::MouseMode::Talk}
+	{"Gehen", Script::MouseMode::Walk},
+	{"Schauen", Script::MouseMode::Look},
+	{"Benutzen", Script::MouseMode::Use},
+	{"Reden", Script::MouseMode::Talk}
 };
 
 ActionBar::ActionBar(View1 *view)
@@ -166,27 +167,7 @@ void ActionBar::drawUIButton(const Common::Rect &rect, bool pressed, Graphics::M
 }
 
 void ActionBar::drawSentenceLine(Graphics::ManagedSurface &s) {
-	Common::String sentence;
-	const Script::MouseMode mode = g_engine->_scriptExecutor->_cursorMode;
-
-	if (mode == Script::MouseMode::UseInventory && _view->_activeInventoryItem) {
-		sentence = "Use";
-		const Common::String itemName = getObjectDisplayName(_view->_activeInventoryItem);
-		if (!itemName.empty())
-			sentence += " " + itemName;
-	} else if (_activeVerbIndex >= 0 && _activeVerbIndex < 4) {
-		sentence = kVerbs[_activeVerbIndex].label;
-	}
-	if (!_sentenceObject.empty()) {
-		if (!sentence.empty()) {
-			if (mode == Script::MouseMode::UseInventory)
-				sentence += " with ";
-			else
-				sentence += " ";
-		}
-		sentence += _sentenceObject;
-	}
-
+	Common::String sentence = buildSentenceLine();
 	if (sentence.empty())
 		return;
 
@@ -209,9 +190,10 @@ void ActionBar::drawVerbBar(Graphics::ManagedSurface &s) {
 
 		drawUIButton(r, isActive || isHovered, s);
 
-		const int textX = r.left + (r.width() - (int)strlen(kVerbs[i].label) * 6) / 2;
+		const Common::String label = uiText(kVerbs[i].label);
+		const int textX = r.left + (r.width() - (int)label.size() * 6) / 2;
 		const int textY = r.top + (r.height() - (int)g_engine->maxGlyphHeight) / 2;
-		_view->renderStringTo(textX, textY, kVerbs[i].label, s);
+		_view->renderStringTo(textX, textY, label, s);
 	}
 }
 
@@ -431,6 +413,7 @@ void ActionBar::handleMouseMoveScumm(const Common::Point &pos) {
 					break;
 				if (getInvItemRect(i).contains(pos)) {
 					_hoveredItemIndex = i;
+					updateSentenceLine(getObjectHotspotName(items[itemIdx]->_index));
 					break;
 				}
 			}
@@ -497,53 +480,68 @@ void ActionBar::refreshSaveSlotNames() {
 		if (desc.getSaveSlot() != -1 && !desc.getDescription().empty())
 			g_engine->_saveSlotNames.push_back(desc.getDescription());
 		else
-			g_engine->_saveSlotNames.push_back(Common::String::format("--- Slot %d ---", slot + 1));
+			g_engine->_saveSlotNames.push_back(Common::String::format(uiText("--- Platz %d ---").c_str(), slot + 1));
+	}
+}
+
+Common::String ActionBar::translatedVerbLabel(Script::MouseMode mode) const {
+	if (mode == Script::MouseMode::UseInventory) {
+		mode = Script::MouseMode::Use;
+	}
+	for (int i = 0; i < ARRAYSIZE(kVerbs); i++) {
+		if (kVerbs[i].mode == mode) {
+			return uiText(kVerbs[i].label);
+		}
 	}
+	return uiText("Gehen");
+}
+
+Common::String ActionBar::currentTargetDisplayName() const {
+	if (!_sentenceObject.empty())
+		return _sentenceObject;
+
+	const Common::Point mouse = g_system->getEventManager()->getMousePos();
+	if (isPointInUI(mouse))
+		return Common::String();
+
+	uint16 hoverId = _view->getHitObjectID(mouse);
+	if (hoverId == 0)
+		hoverId = g_engine->getHotspotAtPoint(mouse);
+	return lookupInteractionDisplayName(hoverId);
 }
 
-Common::String ActionBar::buildNativeSentenceLine() const {
+Common::String ActionBar::buildSentenceLine() const {
 	const Script::MouseMode mode = g_engine->_scriptExecutor->_cursorMode;
-	const char *verb = "Walk";
-	if (mode == Script::MouseMode::Look)
-		verb = "Look";
-	else if (mode == Script::MouseMode::Talk)
-		verb = "Talk";
-	else if (mode == Script::MouseMode::Use || mode == Script::MouseMode::UseInventory)
-		verb = "Use";
-	else if (mode == Script::MouseMode::PanelCursor || mode == Script::MouseMode::Disabled)
+	if (mode == Script::MouseMode::PanelCursor || mode == Script::MouseMode::Disabled) {
 		return Common::String();
+	}
 
-	Common::String line(verb);
+	const Common::String targetName = currentTargetDisplayName();
+	Common::String itemName;
 	if (_view->_activeInventoryItem != nullptr) {
-		const Common::String itemName = getObjectDisplayName(_view->_activeInventoryItem);
-		if (!itemName.empty()) {
-			line += " ";
-			line += itemName;
-			line += " with";
-		}
+		itemName = getObjectHotspotName(_view->_activeInventoryItem->_index);
 	}
 
-	uint16 hoverId = 0;
-	const Common::Point mouse = g_system->getEventManager()->getMousePos();
-	if (!isPointInUI(mouse)) {
-		hoverId = _view->getHitObjectID(mouse);
-		if (hoverId == 0)
-			hoverId = g_engine->getHotspotAtPoint(mouse);
-	}
-	if (hoverId >= 0x400) {
-		const uint16 objIndex = hoverId - 0x400;
-		if (objIndex < GameObjects::instance()._objectNames.size() &&
-			!GameObjects::instance()._objectNames[objIndex].empty()) {
-			line += " ";
-			line += GameObjects::instance()._objectNames[objIndex];
+	if (mode == Script::MouseMode::UseInventory && !itemName.empty()) {
+		if (!targetName.empty())
+			return Common::String::format(uiText("Benutze %s mit %s").c_str(), itemName.c_str(), targetName.c_str());
+		return Common::String::format(uiText("Benutze %s").c_str(), itemName.c_str());
+	}
+
+	if (!targetName.empty()) {
+		switch (mode) {
+		case Script::MouseMode::Look:
+			return Common::String::format(uiText("Schaue an %s").c_str(), targetName.c_str());
+		case Script::MouseMode::Talk:
+			return Common::String::format(uiText("Rede mit %s").c_str(), targetName.c_str());
+		case Script::MouseMode::Use:
+			return Common::String::format(uiText("Benutze %s").c_str(), targetName.c_str());
+		default:
+			return Common::String::format(uiText("Gehe zu %s").c_str(), targetName.c_str());
 		}
-	} else if (_view->_hoverHotspotId != 0 &&
-			   _view->_hoverHotspotId < GameObjects::instance()._objectNames.size() &&
-			   !GameObjects::instance()._objectNames[_view->_hoverHotspotId].empty()) {
-		line += " ";
-		line += GameObjects::instance()._objectNames[_view->_hoverHotspotId];
 	}
-	return line;
+
+	return translatedVerbLabel(mode);
 }
 
 const HudButton *ActionBar::findHudButtonAt(const Common::Point &pos, int *outIndex) const {
@@ -664,7 +662,7 @@ void ActionBar::drawNative(Graphics::ManagedSurface &s) {
 		const GlyphData *font = g_engine->numGlyphs ? g_engine->_glyphs : panelFont;
 		const uint16 fontCount = g_engine->numGlyphs ? g_engine->numGlyphs : panelFontCount;
 		if (fontCount != 0) {
-			Common::String sentence = buildNativeSentenceLine();
+			Common::String sentence = buildSentenceLine();
 			if (!sentence.empty()) {
 				const uint16 maxW = (uint16)(g_engine->screenWidth() - 16);
 				while (sentence.size() > 1) {
@@ -775,7 +773,7 @@ bool ActionBar::handleClickNative(const Common::Point &pos) {
 			if (g_engine->_optionsSubMode == 2) {
 				g_engine->loadGameState(slot);
 			} else if (g_engine->_optionsSubMode == 1) {
-				Common::String name = Common::String::format("Save %d", slot + 1);
+				Common::String name = Common::String::format(uiText("Spielstand %d").c_str(), slot + 1);
 				if (row < g_engine->_saveSlotNames.size() &&
 					!g_engine->_saveSlotNames[row].empty() &&
 					!g_engine->_saveSlotNames[row].hasPrefix("---"))
@@ -848,9 +846,7 @@ bool ActionBar::handleClickNative(const Common::Point &pos) {
 		g_engine->softRestart();
 		return true;
 	} else if (id == 0x21) {
-		::GUI::MessageDialog quitDialog(
-			Common::U32String("Quit the game?"),
-			Common::U32String("Quit"), Common::U32String("Cancel"));
+		::GUI::MessageDialog quitDialog(_("Quit the game?"), _("Quit"), _("Cancel"));
 		if (quitDialog.runModal() == ::GUI::kMessageOK)
 			Engine::quitGame();
 	} else if (id == 0x14 || id == 0x16) {
diff --git a/engines/macs2/actionbar.h b/engines/macs2/actionbar.h
index 9b6a52577be..242b6995d36 100644
--- a/engines/macs2/actionbar.h
+++ b/engines/macs2/actionbar.h
@@ -108,7 +108,9 @@ private:
 	bool handleClickNative(const Common::Point &pos);
 	void handleMouseMoveNative(const Common::Point &pos);
 	void refreshSaveSlotNames();
-	Common::String buildNativeSentenceLine() const;
+	Common::String translatedVerbLabel(Script::MouseMode mode) const;
+	Common::String currentTargetDisplayName() const;
+	Common::String buildSentenceLine() const;
 	const HudButton *findHudButtonAt(const Common::Point &pos, int *outIndex = nullptr) const;
 
 	View1 *_view;
diff --git a/engines/macs2/gameobjects.h b/engines/macs2/gameobjects.h
index 92f91956833..0a6f9ac3666 100644
--- a/engines/macs2/gameobjects.h
+++ b/engines/macs2/gameobjects.h
@@ -225,11 +225,6 @@ public:
 
 class GameObjects : public Common::Singleton<GameObjects> {
 public:
-	// Maximum of 200h objects
-	// How to address them in the original code:
-	// mov	di,[bp+6h]
-	//	shl di, 2h;
-	// les di, [di + 77Ch]
 	Common::Array<GameObject *> _objects;
 
 	Common::Array<Common::String> _objectNames;
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index ef068af1524..0341b8d59a2 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -54,6 +54,34 @@ namespace Macs2 {
 
 static constexpr const char *kGameSpeedModeConfigKey = "macs2_game_speed_mode";
 
+namespace {
+
+static constexpr uint16 kMaxSceneObjects = 0x200;
+
+Common::U32String hotspotLabelToU32(const Common::String &name) {
+	if (name.empty())
+		return Common::U32String();
+	return Common::U32String(name.c_str(), Common::kDos850);
+}
+
+bool isMapModeActive() {
+	if (g_events == nullptr)
+		return false;
+	View1 *view = (View1 *)g_events->findView("View1");
+	return view != nullptr && view->_currentMode == ViewMode::VM_HELP;
+}
+
+Common::Point getSceneObjectHotspotPosition(View1 *view, GameObject *obj) {
+	if (view != nullptr) {
+		Character *character = view->getCharacterByIndex(obj->_index);
+		if (character != nullptr && !character->_markedForDeletion)
+			return character->getPosition();
+	}
+	return obj->_position;
+}
+
+} // namespace
+
 void resetCharacterWalkPath(Character *character) {
 	if (character == nullptr || character->_gameObject == nullptr)
 		return;
@@ -2399,10 +2427,6 @@ uint16 Macs2Engine::getHotspotAtPoint(const Common::Point &p) {
 	return 0;
 }
 
-namespace {
-
-static constexpr uint16 kMaxSceneObjects = 0x200;
-
 Common::String getObjectHotspotName(uint16 objectIndex) {
 	const GameObjects &objects = GameObjects::instance();
 	if (objectIndex > 0 && objectIndex < objects._objectNames.size() && !objects._objectNames[objectIndex].empty()) {
@@ -2413,30 +2437,14 @@ Common::String getObjectHotspotName(uint16 objectIndex) {
 	return Common::String();
 }
 
-Common::U32String hotspotLabelToU32(const Common::String &name) {
-	if (name.empty())
-		return Common::U32String();
-	return Common::U32String(name.c_str(), Common::kDos850);
-}
-
-bool isMapModeActive() {
-	if (g_events == nullptr)
-		return false;
-	View1 *view = (View1 *)g_events->findView("View1");
-	return view != nullptr && view->_currentMode == ViewMode::VM_HELP;
-}
-
-Common::Point getSceneObjectHotspotPosition(View1 *view, GameObject *obj) {
-	if (view != nullptr) {
-		Character *character = view->getCharacterByIndex(obj->_index);
-		if (character != nullptr && !character->_markedForDeletion)
-			return character->getPosition();
-	}
-	return obj->_position;
+Common::String lookupInteractionDisplayName(uint16 interactionId) {
+	if (interactionId >= 0x800)
+		return lookupSceneHotspotName((uint16)Scenes::instance()._currentSceneIndex, (uint16)(interactionId - 0x800));
+	if (interactionId >= 0x400)
+		return getObjectHotspotName((uint16)(interactionId - 0x400));
+	return Common::String();
 }
 
-} // namespace
-
 void Macs2Engine::rebuildHotspotSnapshot() const {
 	_hotspotSnapshot.currentSceneIndex = Scenes::instance()._currentSceneIndex;
 	_hotspotSnapshot.numHotspots = _numHotspots;
@@ -2579,7 +2587,7 @@ void Macs2Engine::getHotspotPositions(Common::Array<Graphics::HotspotInfo> &hots
 		if (isCharacter && GameObjects::isNpcIndex(entry.index))
 			hotspotType = Graphics::kHotspotNPC;
 
-		const Common::String name = getObjectHotspotName(entry.index);
+		const Common::String &name = getObjectHotspotName(entry.index);
 		hotspots.push_back(Graphics::HotspotInfo(screenPos, hotspotLabelToU32(name), hotspotType));
 	}
 }
@@ -2732,7 +2740,7 @@ void Macs2Engine::loadTranslation() {
 	}
 
 	uint16 version = f->readUint16LE();
-	if (version != 1 && version != 2) {
+	if (version < 1 || version > 3) {
 		warning("Unsupported macs2_translation.dat version %u", version);
 		delete f;
 		return;
@@ -2741,8 +2749,11 @@ void Macs2Engine::loadTranslation() {
 	uint16 numScenes = f->readUint16LE();
 	uint16 numObjects = f->readUint16LE();
 	uint16 numHotspotLabels = 0;
+	uint16 numUiLabels = 0;
 	if (version >= 2)
 		numHotspotLabels = f->readUint16LE();
+	if (version >= 3)
+		numUiLabels = f->readUint16LE();
 
 	// Read index tables
 	struct IndexEntry {
@@ -2797,25 +2808,31 @@ void Macs2Engine::loadTranslation() {
 		_objectTranslations[objectIndex[i].id] = entry;
 	}
 
+	auto readLabelMap = [f](uint16 count, Common::HashMap<Common::String, Common::String> &out) {
+		for (uint16 i = 0; i < count; i++) {
+			uint16 keyLen = f->readUint16LE();
+			Common::String key;
+			for (uint16 k = 0; k < keyLen; k++)
+				key += (char)f->readByte();
+			uint16 valLen = f->readUint16LE();
+			Common::String val;
+			for (uint16 k = 0; k < valLen; k++)
+				val += (char)f->readByte();
+			if (!key.empty() && !val.empty())
+				out[key] = val;
+		}
+	};
+
 	_hotspotLabelTranslations.clear();
-	if (numHotspotLabels > 0)
+	_uiLabelTranslations.clear();
+	if (numHotspotLabels > 0 || numUiLabels > 0)
 		f->seek(stringDataEnd);
-	for (uint16 i = 0; i < numHotspotLabels; i++) {
-		uint16 keyLen = f->readUint16LE();
-		Common::String key;
-		for (uint16 k = 0; k < keyLen; k++)
-			key += (char)f->readByte();
-		uint16 valLen = f->readUint16LE();
-		Common::String val;
-		for (uint16 k = 0; k < valLen; k++)
-			val += (char)f->readByte();
-		if (!key.empty() && !val.empty())
-			_hotspotLabelTranslations[key] = val;
-	}
+	readLabelMap(numHotspotLabels, _hotspotLabelTranslations);
+	readLabelMap(numUiLabels, _uiLabelTranslations);
 
 	delete f;
-	debug("Loaded macs2_translation.dat: %u scenes, %u objects, %u overlay labels",
-		  numScenes, numObjects, (uint)_hotspotLabelTranslations.size());
+	debug("Loaded macs2_translation.dat: %u scenes, %u objects, %u overlay labels, %u UI labels",
+		  numScenes, numObjects, (uint)_hotspotLabelTranslations.size(), (uint)_uiLabelTranslations.size());
 }
 
 Common::String Macs2Engine::translateHotspotLabel(const Common::String &cp850Name) const {
@@ -2827,6 +2844,15 @@ Common::String Macs2Engine::translateHotspotLabel(const Common::String &cp850Nam
 	return cp850Name;
 }
 
+Common::String Macs2Engine::translateUiLabel(const Common::String &source) const {
+	if (source.empty() || !(getFeatures() & GF_TRANSLATED))
+		return source;
+	auto it = _uiLabelTranslations.find(source);
+	if (it != _uiLabelTranslations.end())
+		return it->_value;
+	return source;
+}
+
 Common::StringArray Macs2Engine::decodeStrings(Common::MemoryReadStream *stream, int offset, int numStrings, int sceneId, int objectId) {
 	Common::StringArray result(numStrings);
 	stream->seek(offset);
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index 2cf882fe2d8..d4dfc5c1fac 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -733,8 +733,11 @@ public:
 	Common::HashMap<uint32, TranslationEntry> _sceneTranslations;
 	Common::HashMap<uint32, TranslationEntry> _objectTranslations;
 	Common::HashMap<Common::String, Common::String> _hotspotLabelTranslations;
+	Common::HashMap<Common::String, Common::String> _uiLabelTranslations;
 	void loadTranslation();
 	Common::String translateHotspotLabel(const Common::String &cp850Name) const;
+	/** Action-bar / HUD chrome; German source key, same lookup rules as hotspot labels. */
+	Common::String translateUiLabel(const Common::String &source) const;
 	// Compute the sequential string index at the given byte offset in a string blob
 	int computeStringIndex(Common::MemoryReadStream *stream, int targetOffset);
 
@@ -926,6 +929,9 @@ public:
 
 extern Macs2Engine *g_engine;
 #define SHOULD_QUIT ::Macs2::g_engine->shouldQuit()
+Common::String getObjectHotspotName(uint16 objectIndex);
+/** Display name for a hit id: 0x400+object or 0x800+scene hotspot. */
+Common::String lookupInteractionDisplayName(uint16 interactionId);
 
 } // End of namespace Macs2
 
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index 38a0607f30e..896e5000798 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -2074,14 +2074,9 @@ bool View1::msgMouseMove(const MouseMoveMessage &msg) {
 			uint16 index = getHitObjectID(msg._pos);
 			if (index == 0)
 				index = g_engine->getHotspotAtPoint(msg._pos);
-			if (index != 0 && index >= 0x400) {
-				const uint16 objIndex = index - 0x400;
-				if (objIndex < GameObjects::instance()._objectNames.size()) {
-					const Common::String &name = GameObjects::instance()._objectNames[objIndex];
-					if (!name.empty())
-						_actionBar->updateSentenceLine(name);
-				}
-			}
+			const Common::String name = lookupInteractionDisplayName(index);
+			if (!name.empty())
+				_actionBar->updateSentenceLine(name);
 		}
 	}
 


Commit: ada2aa0d38394775d27d08519358ac7ee2132142
    https://github.com/scummvm/scummvm/commit/ada2aa0d38394775d27d08519358ac7ee2132142
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-31T21:14:09+02:00

Commit Message:
MACS2: reset version back to 1 - the game was never officially supported

Changed paths:
    engines/macs2/macs2.cpp


diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 0341b8d59a2..203c4a89d08 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -2740,7 +2740,7 @@ void Macs2Engine::loadTranslation() {
 	}
 
 	uint16 version = f->readUint16LE();
-	if (version < 1 || version > 3) {
+	if (version != 1) {
 		warning("Unsupported macs2_translation.dat version %u", version);
 		delete f;
 		return;
@@ -2748,12 +2748,8 @@ void Macs2Engine::loadTranslation() {
 
 	uint16 numScenes = f->readUint16LE();
 	uint16 numObjects = f->readUint16LE();
-	uint16 numHotspotLabels = 0;
-	uint16 numUiLabels = 0;
-	if (version >= 2)
-		numHotspotLabels = f->readUint16LE();
-	if (version >= 3)
-		numUiLabels = f->readUint16LE();
+	uint16 numHotspotLabels = f->readUint16LE();
+	uint16 numUiLabels = f->readUint16LE();
 
 	// Read index tables
 	struct IndexEntry {


Commit: 59e2ff7b3ca403e8a5776d2511df00b12c5ebbee
    https://github.com/scummvm/scummvm/commit/59e2ff7b3ca403e8a5776d2511df00b12c5ebbee
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-31T21:14:09+02:00

Commit Message:
MACS2: several ux enhancement fixes for the custom action bar

Changed paths:
    engines/macs2/actionbar.cpp
    engines/macs2/actionbar.h
    engines/macs2/macs2.cpp
    engines/macs2/macs2.h
    engines/macs2/scriptexecutor.cpp
    engines/macs2/view1.cpp
    engines/macs2/view1.h


diff --git a/engines/macs2/actionbar.cpp b/engines/macs2/actionbar.cpp
index d362b19f0fa..63158f7cc90 100644
--- a/engines/macs2/actionbar.cpp
+++ b/engines/macs2/actionbar.cpp
@@ -46,10 +46,10 @@ Common::String uiText(const char *source) {
 } // namespace
 
 const ActionBar::VerbDef ActionBar::kVerbs[4] = {
-	{"Gehen", Script::MouseMode::Walk},
-	{"Schauen", Script::MouseMode::Look},
-	{"Benutzen", Script::MouseMode::Use},
-	{"Reden", Script::MouseMode::Talk}
+	{"Gehe", Script::MouseMode::Walk},
+	{"Schaue", Script::MouseMode::Look},
+	{"Benutze", Script::MouseMode::Use},
+	{"Rede", Script::MouseMode::Talk}
 };
 
 ActionBar::ActionBar(View1 *view)
@@ -77,15 +77,25 @@ void ActionBar::syncInventory() {
 
 void ActionBar::rebuildProtagonistItems() {
 	_protagonistItems.clear();
+	const uint16 invScene = Scenes::instance()._currentActorIndex + 0x400;
 
-	if (_view->isInventorySourceProtagonist()) {
-		_protagonistItems = _view->_inventoryItems;
-		return;
+	// Keep the engine list order, then append any inventory objects it missed
+	// (pickup / moveObject can update sceneIndex before _inventoryItems).
+	for (GameObject *obj : _view->_inventoryItems) {
+		if (obj && obj->_sceneIndex == invScene)
+			_protagonistItems.push_back(obj);
 	}
-
-	const uint16 invScene = Scenes::instance()._currentActorIndex + 0x400;
 	for (GameObject *obj : GameObjects::instance()._objects) {
-		if (obj && obj->_sceneIndex == invScene)
+		if (!obj || obj->_sceneIndex != invScene)
+			continue;
+		bool listed = false;
+		for (GameObject *listedObj : _protagonistItems) {
+			if (listedObj == obj) {
+				listed = true;
+				break;
+			}
+		}
+		if (!listed)
 			_protagonistItems.push_back(obj);
 	}
 }
@@ -166,16 +176,23 @@ void ActionBar::drawUIButton(const Common::Rect &rect, bool pressed, Graphics::M
 							   style, false, false, s);
 }
 
+void ActionBar::actionBarFont(const GlyphData *&font, uint16 &fontCount, int &glyphH) const {
+	const bool usePanelFont = g_engine->numPanelGlyphs > 0;
+	font = usePanelFont ? g_engine->_panelGlyphs : g_engine->_glyphs;
+	fontCount = usePanelFont ? g_engine->numPanelGlyphs : g_engine->numGlyphs;
+	glyphH = usePanelFont ? (int)g_engine->maxPanelGlyphHeight : (int)g_engine->maxGlyphHeight;
+}
+
 void ActionBar::drawSentenceLine(Graphics::ManagedSurface &s) {
 	Common::String sentence = buildSentenceLine();
 	if (sentence.empty())
 		return;
 
-	const bool usePanelFont = g_engine->numPanelGlyphs > 0;
-	const GlyphData *font = usePanelFont ? g_engine->_panelGlyphs : g_engine->_glyphs;
-	const uint16 fontCount = usePanelFont ? g_engine->numPanelGlyphs : g_engine->numGlyphs;
-	const int glyphH = usePanelFont ? g_engine->maxPanelGlyphHeight : g_engine->maxGlyphHeight;
-	if (usePanelFont)
+	const GlyphData *font = nullptr;
+	uint16 fontCount = 0;
+	int glyphH = 0;
+	actionBarFont(font, fontCount, glyphH);
+	if (g_engine->numPanelGlyphs > 0)
 		sentence.toUppercase();
 	const int textY = kUITop + MAX(0, (kSentenceH - glyphH) / 2);
 	const int textX = MAX(0, (kScreenWidth - _view->measureStringWithFont(sentence, font, fontCount)) / 2);
@@ -183,6 +200,11 @@ void ActionBar::drawSentenceLine(Graphics::ManagedSurface &s) {
 }
 
 void ActionBar::drawVerbBar(Graphics::ManagedSurface &s) {
+	const GlyphData *font = nullptr;
+	uint16 fontCount = 0;
+	int glyphH = 0;
+	actionBarFont(font, fontCount, glyphH);
+
 	for (int i = 0; i < ARRAYSIZE(kVerbs); i++) {
 		const Common::Rect r = getVerbRect(i);
 		const bool isActive = (i == _activeVerbIndex);
@@ -190,10 +212,13 @@ void ActionBar::drawVerbBar(Graphics::ManagedSurface &s) {
 
 		drawUIButton(r, isActive || isHovered, s);
 
-		const Common::String label = uiText(kVerbs[i].label);
-		const int textX = r.left + (r.width() - (int)label.size() * 6) / 2;
-		const int textY = r.top + (r.height() - (int)g_engine->maxGlyphHeight) / 2;
-		_view->renderStringTo(textX, textY, label, s);
+		Common::String label = uiText(kVerbs[i].label);
+		if (g_engine->numPanelGlyphs > 0)
+			label.toUppercase();
+		const int textW = _view->measureStringWithFont(label, font, fontCount);
+		const int textX = r.left + (r.width() - textW) / 2;
+		const int textY = r.top + (r.height() - glyphH) / 2;
+		_view->renderStringWithFontTo(textX, textY, label, font, fontCount, s);
 	}
 }
 
@@ -209,7 +234,13 @@ int ActionBar::getScrollButtonWidth() const {
 }
 
 int ActionBar::getInvArrowX() const {
-	return kInvX;
+	return kBarPadX + kVerbCols * kVerbW + kVerbInvGap;
+}
+
+int ActionBar::getInvItemWidth() const {
+	const int scrollW = getScrollButtonWidth();
+	const int available = kScreenWidth - getInvArrowX() - kBarPadX - 2 * scrollW;
+	return MAX(24, available / kInvCols);
 }
 
 void ActionBar::drawScrollButton(Graphics::ManagedSurface &s, const Common::Rect &rect,
@@ -437,7 +468,7 @@ void ActionBar::clearSentenceObject() {
 Common::Rect ActionBar::getVerbRect(int index) const {
 	const int col = index % kVerbCols;
 	const int row = index / kVerbCols;
-	const int x = col * kVerbW;
+	const int x = kBarPadX + col * kVerbW;
 	const int y = kVerbY + row * kVerbH;
 	return Common::Rect(x, y, x + kVerbW, y + kVerbH);
 }
@@ -446,9 +477,10 @@ Common::Rect ActionBar::getInvItemRect(int index) const {
 	const int col = index % kInvCols;
 	const int row = index / kInvCols;
 	const int scrollW = getScrollButtonWidth();
-	const int x = getInvArrowX() + scrollW + col * kInvItemW;
+	const int itemW = getInvItemWidth();
+	const int x = getInvArrowX() + scrollW + col * itemW;
 	const int y = kVerbY + row * kInvItemH;
-	return Common::Rect(x, y, x + kInvItemW, y + kInvItemH);
+	return Common::Rect(x, y, x + itemW, y + kInvItemH);
 }
 
 Common::Rect ActionBar::getInvScrollLeftRect() const {
@@ -465,7 +497,7 @@ bool ActionBar::isPointInInventoryStrip(const Common::Point &pos) const {
 
 Common::Rect ActionBar::getInvScrollRightRect() const {
 	const int scrollW = getScrollButtonWidth();
-	const int x = getInvArrowX() + scrollW + kInvCols * kInvItemW;
+	const int x = getInvArrowX() + scrollW + kInvCols * getInvItemWidth();
 	return Common::Rect(x, kVerbY, x + scrollW, kVerbY + kVerbH * kVerbRows);
 }
 
@@ -493,7 +525,7 @@ Common::String ActionBar::translatedVerbLabel(Script::MouseMode mode) const {
 			return uiText(kVerbs[i].label);
 		}
 	}
-	return uiText("Gehen");
+	return uiText("Gehe");
 }
 
 Common::String ActionBar::currentTargetDisplayName() const {
@@ -504,6 +536,14 @@ Common::String ActionBar::currentTargetDisplayName() const {
 	if (isPointInUI(mouse))
 		return Common::String();
 
+	if (_view->_uiPanelState == View1::kUiPanelContainerInventory ||
+		_view->_uiPanelState == View1::kUiPanelInventory) {
+		GameObject *hovered = _view->getClickedInventoryItem(mouse);
+		if (hovered != nullptr)
+			return getObjectHotspotName(hovered->_index);
+		return Common::String();
+	}
+
 	uint16 hoverId = _view->getHitObjectID(mouse);
 	if (hoverId == 0)
 		hoverId = g_engine->getHotspotAtPoint(mouse);
diff --git a/engines/macs2/actionbar.h b/engines/macs2/actionbar.h
index 242b6995d36..600df652fee 100644
--- a/engines/macs2/actionbar.h
+++ b/engines/macs2/actionbar.h
@@ -33,6 +33,7 @@ namespace Macs2 {
 class View1;
 class GameObject;
 struct AnimFrame;
+struct GlyphData;
 struct HudButton;
 
 /**
@@ -63,12 +64,12 @@ private:
 	static constexpr int kUITop = kGameHeight;
 	static constexpr int kSentenceY = kGameHeight;
 	static constexpr int kVerbY = kGameHeight + kSentenceH;
-	static constexpr int kVerbW = 64;
+	static constexpr int kBarPadX = 6;
+	static constexpr int kVerbW = 78;
 	static constexpr int kVerbH = 25;
 	static constexpr int kVerbCols = 2;
 	static constexpr int kVerbRows = 2;
-	static constexpr int kInvX = 128;
-	static constexpr int kInvItemW = 34;
+	static constexpr int kVerbInvGap = 4;
 	static constexpr int kInvItemH = 25;
 	static constexpr int kInvIconInset = 1;
 	static constexpr int kInvCols = 4;
@@ -84,6 +85,7 @@ private:
 	void drawScumm(Graphics::ManagedSurface &s);
 	bool handleClickScumm(const Common::Point &pos, bool scriptsRunning);
 	void handleMouseMoveScumm(const Common::Point &pos);
+	void actionBarFont(const GlyphData *&font, uint16 &fontCount, int &glyphH) const;
 	void drawSentenceLine(Graphics::ManagedSurface &s);
 	void drawVerbBar(Graphics::ManagedSurface &s);
 	void drawInventoryStrip(Graphics::ManagedSurface &s);
@@ -93,6 +95,7 @@ private:
 
 	int getScrollButtonWidth() const;
 	int getInvArrowX() const;
+	int getInvItemWidth() const;
 
 	Common::Array<GameObject *> getProtagonistItems() const;
 
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 203c4a89d08..11df6a165bd 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -1277,6 +1277,10 @@ void Macs2Engine::changeScene(uint32 newSceneIndex, bool executeScript) {
 		if (!loadSceneGraphics(newSceneIndex))
 			error("changeScene(): Failed to load scene graphics for scene %u", newSceneIndex);
 
+		_menuMode = 1;
+		_optionsSubMode = 0;
+		_bottomHudVisible = true;
+
 		View1 *currentView = (View1 *)findView("View1");
 		if (currentView != nullptr) {
 			// Do not push _pal here: scriptChangeScene fades the previous
@@ -1399,13 +1403,12 @@ void Macs2Engine::changeScene(uint32 newSceneIndex, bool executeScript) {
 	if (!loadSceneGraphics(newSceneIndex))
 		error("changeScene(): Failed to load scene graphics for scene %u", newSceneIndex);
 
-	// V2 starts with the main DisplayMenu bar visible; scene
-	// scripts (e.g. world map / overview map) may hideActionBar during isSceneInit
-	if (isV2()) {
-		_menuMode = 1;
-		_optionsSubMode = 0;
-		_bottomHudVisible = true;
-	}
+	// Scene change starts with the main HUD shown. v2 scripts may hide it
+	// during init (overview map). v1 has no hide/show opcodes; kEnhUIUX uses
+	// the same flag so a scene change restores the strip.
+	_menuMode = 1;
+	_optionsSubMode = 0;
+	_bottomHudVisible = true;
 
 	// Refresh characters
 	View1 *currentView = (View1 *)findView("View1");
@@ -2306,6 +2309,7 @@ void Macs2Engine::nextCursorMode() {
 		setCursorMode(Script::MouseMode::Use);
 		break;
 	case Script::MouseMode::Use:
+	case Script::MouseMode::UseInventory:
 		setCursorMode(Script::MouseMode::Walk);
 		break;
 	default:
@@ -2315,15 +2319,22 @@ void Macs2Engine::nextCursorMode() {
 }
 
 void Macs2Engine::setBottomHudVisible(bool visible) {
-	_bottomHudVisible = visible;
+	// hide/show opcodes toggle menuMode (0 vs 1). Cursor mode alone never hides
+	// the HUD. Native skin restores the cursor saved on hide.
 	if (hasNativeHudAssets()) {
 		if (visible) {
-			if (_menuMode == 0)
+			if (_menuMode == 0) {
 				_menuMode = 1;
+				if (_scriptExecutor)
+					setCursorMode(_savedMenuCursorMode);
+			}
 		} else {
+			if (_menuMode == 1 && _scriptExecutor)
+				_savedMenuCursorMode = _scriptExecutor->_cursorMode;
 			_menuMode = 0;
 		}
 	}
+	_bottomHudVisible = visible;
 }
 
 void Macs2Engine::setCursorMode(Script::MouseMode newMode) {
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index d4dfc5c1fac..0d98ebe4d04 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -776,8 +776,9 @@ public:
 
 	/**
 	 * Bottom HUD / action-bar visibility (dialect-neutral).
-	 * Driven by showActionBar / hideActionBar; Scumm verb strip and native
-	 * HUDs both respect this flag. Native skin also maps visible <-> menuMode.
+	 * hide/show opcodes toggle menuMode (0 vs 1) and restore the cursor saved
+	 * on hide. The Scumm kEnhUIUX strip uses the same flag; cursor mode alone
+	 * never hides the bar.
 	 */
 	bool isBottomHudVisible() const { return _bottomHudVisible; }
 	void setBottomHudVisible(bool visible);
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index 741b9afb92b..f80068e24fc 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -1115,6 +1115,8 @@ OpcodeResult Script::ScriptExecutor::scriptMoveObject() {
 			}
 		}
 	}
+	if (currentView->_inventorySource != nullptr)
+		currentView->setInventorySource(currentView->_inventorySource);
 
 	// Check from sortObjectsByDepth (1008:0da6): if the moved object is the active
 	// inventory item cursor, clear it and reset cursor mode from UseInventory to Use.
@@ -3830,19 +3832,11 @@ OpcodeResult ScriptExecutor::scriptShowActionBar() {
 	if (currentView == nullptr)
 		return OpcodeResult::Continue;
 
-	if (_engine->hasNativeHudAssets()) {
-		// Dialect-v2: if MenuMode==0 -> MenuMode=1, restore saved cursor, redraw.
-		if (_engine->_menuMode == 0) {
-			_engine->setBottomHudVisible(true);
-			_engine->setCursorMode(_engine->_savedMenuCursorMode);
-			currentView->updateCursor();
-			currentView->redraw();
-		}
-		return OpcodeResult::Continue;
-	}
-
-	if (currentView->hasPersistentActionBar()) {
+	// v2 opcode 0x65. v1 tables end at waitForAdlib; kEnhUIUX uses the same
+	// setBottomHudVisible switch (menuMode 0 -> 1 + cursor restore).
+	if (_engine->hasNativeHudAssets() || currentView->hasPersistentActionBar()) {
 		_engine->setBottomHudVisible(true);
+		currentView->updateCursor();
 		currentView->redraw();
 		return OpcodeResult::Continue;
 	}
@@ -3858,16 +3852,8 @@ OpcodeResult ScriptExecutor::scriptHideActionBar() {
 	if (currentView == nullptr)
 		return OpcodeResult::Continue;
 
-	if (_engine->hasNativeHudAssets()) {
-		// Dialect-v2: save cursor when leaving main HUD, set MenuMode=0.
-		if (_engine->_menuMode == 1)
-			_engine->_savedMenuCursorMode = _cursorMode;
-		_engine->setBottomHudVisible(false);
-		currentView->redraw();
-		return OpcodeResult::Continue;
-	}
-
-	if (currentView->hasPersistentActionBar()) {
+	// v2 opcode 0x66. Same flag as show; classic v1 popup is a separate path.
+	if (_engine->hasNativeHudAssets() || currentView->hasPersistentActionBar()) {
 		_engine->setBottomHudVisible(false);
 		currentView->redraw();
 		return OpcodeResult::Continue;
@@ -4307,7 +4293,10 @@ void ScriptExecutor::run(bool firstRun) {
 	}
 	_state = ExecutorState::Executing;
 	step();
-	syncScriptIsExecutingFlag();
+	// step() rewinds the scene script to 0 when it goes Idle. A pos<end check
+	// would then mark g_wScriptIsExecuting again and steal every scene click.
+	if (_state != ExecutorState::Idle)
+		syncScriptIsExecutingFlag();
 }
 
 uint32 ScriptExecutor::effectiveScriptEnd() const {
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index 896e5000798..19a2b3c4761 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -171,7 +171,6 @@ void View1::ensureActionBar() {
 	if (_innerBounds.width() != sw || _innerBounds.height() != sh) {
 		_bounds = Common::Rect(0, 0, sw, sh);
 		_innerBounds = _bounds;
-		::initGraphics(sw, sh);
 	}
 }
 
@@ -194,13 +193,12 @@ bool View1::shouldShowActionBar() const {
 		return false;
 	if (_currentMode == ViewMode::VM_HELP)
 		return false;
-	if (_isShowingTextBox || _isShowingDialoguePanel)
-		return false;
-	// Keep the strip visible during container inventory so items can be taken
-	// into the protagonist inventory (Drop/Take is suppressed on that panel).
 	if (_uiPanelState == kUiPanelSaveLoad)
 		return false;
-	if (g_engine->_scriptExecutor->_cursorMode == Script::MouseMode::Disabled)
+	// Scumm strip only: native HUD stays up during speech/choices (DisplayMenu
+	// is independent of AddText / TalkTo; mode 4 draws choices in the panel).
+	if (!g_engine->hasNativeHudAssets() &&
+		(_isShowingDialoguePanel || _isDialogueChoiceInputActive || _isShowingTextBox))
 		return false;
 
 	// Use the actor object table directly; Character lookup can lag behind scene changes.
@@ -259,6 +257,7 @@ void View1::openInventory(GameObject *newInventorySource) {
 	if (hasPersistentActionBar() && newInventorySource->_index == Scenes::instance()._currentActorIndex) {
 		if (_actionBar)
 			_actionBar->syncInventory();
+		redraw();
 		return;
 	}
 
@@ -363,7 +362,8 @@ void View1::refreshProtagonistInventoryAfterLoad(uint16 actorIndex) {
 }
 
 bool View1::isInventorySourceProtagonist() const {
-	return _inventorySource->_index == 1;
+	return _inventorySource &&
+		   _inventorySource->_index == Scenes::instance()._currentActorIndex;
 }
 
 void View1::transferInventoryItem(GameObject *item, GameObject *targetContainer) {
@@ -1124,7 +1124,7 @@ void View1::transferPickupTarget(GameObject *targetObject) {
 		}
 	}
 
-	if (_inventorySource != nullptr && _inventorySource->_index == actorIndex) {
+	if (_inventorySource == nullptr || _inventorySource->_index == actorIndex) {
 		bool alreadyListed = false;
 		for (const GameObject *item : _inventoryItems) {
 			if (item->_index == targetObject->_index) {
@@ -1144,6 +1144,7 @@ void View1::transferPickupTarget(GameObject *targetObject) {
 		}
 	}
 
+
 	if (_activeInventoryItem != nullptr && _activeInventoryItem->_index == targetObject->_index) {
 		_activeInventoryItem = nullptr;
 		if (executor->_cursorMode == Script::MouseMode::UseInventory) {
@@ -1701,6 +1702,41 @@ bool View1::handleHelpClick(const MouseDownMessage &msg) {
 	return true;
 }
 
+void View1::walkToScreenPosition(const Common::Point &pos) {
+	Character *protagonist = getCharacterByIndex(Scenes::instance()._currentActorIndex);
+	if (protagonist == nullptr) {
+		debugC(kDebugScript, "Ignoring walk click without active actor character in the scene");
+		return;
+	}
+
+	Common::Point target = pos;
+	Common::Point charPos = protagonist->getPosition();
+
+	int16 targetY = target.y;
+	int16 targetX = target.x;
+	g_engine->snapToWalkablePosition(&targetY, &targetX, charPos.y, charPos.x);
+	target.x = targetX;
+	target.y = targetY;
+
+	protagonist->_pathFinalDestination = target;
+	protagonist->_currentPathIndex = 0;
+	protagonist->_path.clear();
+
+	const bool directPath = g_engine->isPathWalkable(target.y, target.x, charPos.y, charPos.x);
+	if (directPath || Macs2Engine::isWalkabilityBlocking(g_engine->getWalkabilityAt(target.y, target.x))) {
+		protagonist->_targetPosition = target;
+	} else {
+		const bool found = protagonist->calculatePath(target);
+		if (!found)
+			protagonist->_targetPosition = target;
+	}
+	protagonist->_stepDeltaX = abs(protagonist->_targetPosition.x - charPos.x);
+	protagonist->_stepDeltaY = abs(protagonist->_targetPosition.y - charPos.y);
+	protagonist->_stepError = 0;
+	protagonist->_stepDirectionSet = false;
+	g_engine->_scriptExecutor->saveWalkRuntime(protagonist, protagonist->_gameObject);
+}
+
 bool View1::handleInput(const MouseDownMessage &msg) {
 	if (msg._button == MouseMessage::MB_LEFT) {
 		// Help mode (depth-based scene preview) from handleInput (1008:e8bf).
@@ -1791,89 +1827,46 @@ bool View1::handleInput(const MouseDownMessage &msg) {
 			return true;
 		}
 
-		if (g_engine->_scriptExecutor->_cursorMode == Script::MouseMode::Walk) {
-			if (shouldShowActionBar() && msg._pos.y >= actionBarTopY())
-				return true;
-
-			Character *protagonist = getCharacterByIndex(Scenes::instance()._currentActorIndex);
-			if (protagonist == nullptr) {
-				debugC(kDebugScript, "Ignoring walk click without active actor character in the scene");
-				return true;
-			}
-
-			Common::Point target = msg._pos;
-			Common::Point charPos = protagonist->getPosition();
-
-			// Snap target to nearest walkable position (1008:9be2)
-			int16 targetY = target.y;
-			int16 targetX = target.x;
-			g_engine->snapToWalkablePosition(&targetY, &targetX, charPos.y, charPos.x);
-			target.x = targetX;
-			target.y = targetY;
-
-			// handleInput (1008:e8bf): isPathWalkable(targetY, targetX, charY, charX).
-			// calculatePath only when direct line fails AND target tile is walkable (< 0xC8).
-			protagonist->_pathFinalDestination = target;
-			protagonist->_currentPathIndex = 0;
-			protagonist->_path.clear();
-
-			const bool directPath = g_engine->isPathWalkable(target.y, target.x, charPos.y, charPos.x);
-			if (directPath || Macs2Engine::isWalkabilityBlocking(g_engine->getWalkabilityAt(target.y, target.x))) {
-				protagonist->_targetPosition = target;
-			} else {
-				const bool found = protagonist->calculatePath(target);
-				if (!found) {
-					protagonist->_targetPosition = target;
-				}
-			}
-			protagonist->_stepDeltaX = abs(protagonist->_targetPosition.x - charPos.x);
-			protagonist->_stepDeltaY = abs(protagonist->_targetPosition.y - charPos.y);
-			protagonist->_stepError = 0;
-			protagonist->_stepDirectionSet = false;
-			g_engine->_scriptExecutor->saveWalkRuntime(protagonist, protagonist->_gameObject);
-			return true;
-		}
-
-		// Check if we hit something
 		if (shouldShowActionBar() && msg._pos.y >= actionBarTopY())
 			return true;
 
-		// Original order: getHotspotAtPoint first, then drawCharactersAndHitTest overrides.
-		// Our order (objects first, fallback to background) produces the same result.
-		uint16 index = getHitObjectID(Common::Point(msg._pos.x, msg._pos.y));
-		if (index == 0) {
-			index = g_engine->getHotspotAtPoint(msg._pos);
-		}
-		if (index != 0) {
-			debugC(kDebugScript, "*** New interaction started");
-
-			// Binary (handleInput 1008:ef2d): stop character movement before interaction.
-			// Sets runtime target/finalDest to current position, clears path state.
-			Character *protagonist = getCharacterByIndex(Scenes::instance()._currentActorIndex);
-			if (protagonist != nullptr) {
-				Common::Point pos = protagonist->getPosition();
-				protagonist->_targetPosition = pos;
-				protagonist->_pathFinalDestination = pos;
-				protagonist->_path.clear();
-				protagonist->_currentPathIndex = 0;
-			}
+		const Script::MouseMode mode = g_engine->_scriptExecutor->_cursorMode;
 
-			// Binary (handleInput 1008:ef8f): if mode != 0x17, clear inventory item ID.
-			// Note: the binary does NOT touch g_wInventoryActionFlag here.
-			if (g_engine->_scriptExecutor->_cursorMode != Script::MouseMode::UseInventory) {
-				g_engine->_scriptExecutor->_interactedInventoryItemId = 0;
-				_activeInventoryItem = nullptr;
-			}
+		// Walk never hit-tests; other verbs interact when a target is under the cursor.
+		// Empty-ground clicks walk so the persistent verb bar does not trap the player
+		// in Look/Use/Talk/UseInventory with no way to move.
+		if (mode != Script::MouseMode::Walk) {
+			uint16 index = getHitObjectID(Common::Point(msg._pos.x, msg._pos.y));
+			if (index == 0)
+				index = g_engine->getHotspotAtPoint(msg._pos);
+			if (index != 0) {
+				debugC(kDebugScript, "*** New interaction started");
+
+				Character *protagonist = getCharacterByIndex(Scenes::instance()._currentActorIndex);
+				if (protagonist != nullptr) {
+					Common::Point pos = protagonist->getPosition();
+					protagonist->_targetPosition = pos;
+					protagonist->_pathFinalDestination = pos;
+					protagonist->_path.clear();
+					protagonist->_currentPathIndex = 0;
+				}
 
-			g_engine->_scriptExecutor->_interactedObjectID = index;
+				if (mode != Script::MouseMode::UseInventory) {
+					g_engine->_scriptExecutor->_interactedInventoryItemId = 0;
+					_activeInventoryItem = nullptr;
+				}
 
-			// Binary: runScriptExecutor() - internally rewinds scene script when
-			// g_wScriptIsExecuting==0 (which it is here, since we're in the
-			// "not executing" branch of handleInput).
-			g_engine->runScriptExecutor(false);
+				g_engine->_scriptExecutor->_interactedObjectID = index;
+				g_engine->runScriptExecutor(false);
+				g_engine->_scriptExecutor->_interactedObjectID = 0;
+				return true;
+			}
+		}
 
-			// Binary: only g_wInteractedObjectId is cleared after runScriptExecutor.
-			g_engine->_scriptExecutor->_interactedObjectID = 0;
+		if (mode == Script::MouseMode::Walk || mode == Script::MouseMode::Look ||
+			mode == Script::MouseMode::Use || mode == Script::MouseMode::Talk ||
+			mode == Script::MouseMode::UseInventory) {
+			walkToScreenPosition(msg._pos);
 		}
 		return true;
 	} else if (msg._button == MouseMessage::MB_RIGHT) {
@@ -2069,6 +2062,16 @@ bool View1::msgMouseMove(const MouseMoveMessage &msg) {
 	if (shouldShowActionBar() && _actionBar) {
 		if (_actionBar->isPointInUI(msg._pos)) {
 			_actionBar->handleMouseMove(msg._pos);
+		} else if (_uiPanelState == kUiPanelContainerInventory || _uiPanelState == kUiPanelInventory) {
+			// Inventory popup is modal: do not punch through to scene objects
+			// or areas behind the dialog.
+			_actionBar->clearSentenceObject();
+			GameObject *hovered = getClickedInventoryItem(msg._pos);
+			if (hovered != nullptr) {
+				const Common::String name = getObjectHotspotName(hovered->_index);
+				if (!name.empty())
+					_actionBar->updateSentenceLine(name);
+			}
 		} else if (msg._pos.y < actionBarTopY()) {
 			_actionBar->clearSentenceObject();
 			uint16 index = getHitObjectID(msg._pos);
@@ -2317,19 +2320,23 @@ void View1::draw() {
 
 	if (hasPersistentActionBar()) {
 		ensureActionBar();
-		if (_actionBar && !_isShowingTextBox && !_isShowingDialoguePanel) {
+		if (_actionBar) {
 			const int sw = g_engine->screenWidth();
 			const int sh = g_engine->screenHeight();
 			Graphics::ManagedSurface fullScreen(*g_events->getScreen(), Common::Rect(0, 0, sw, sh));
 			if (shouldShowActionBar()) {
 				_actionBar->draw(fullScreen);
+				// Glyphs use setPixel and do not dirty the Screen.
+				g_events->getScreen()->addDirtyRect(Common::Rect(0, actionBarTopY(), sw, sh));
 			} else if (g_engine->hasNativeHudAssets() && g_engine->_menuMode == 0) {
-				// hideActionBar / overview map: leave playfield pixels alone so
-				// scene art (and hotspots) remain visible in the former panel band.
+				// hideActionBar / overview map: leave playfield pixels in the
+				// former panel band so scene art and hotspots stay visible.
 			} else {
 				const int top = actionBarTopY();
-				if (top >= 0 && top < sh)
+				if (top >= 0 && top < sh) {
 					fullScreen.fillRect(Common::Rect(0, top, sw, sh), 0);
+					g_events->getScreen()->addDirtyRect(Common::Rect(0, top, sw, sh));
+				}
 			}
 		}
 	}
@@ -3062,13 +3069,13 @@ void View1::drawSprite(int16 x, int16 y, uint16 width, uint16 height, byte *data
 				if (finalX >= 0 && finalX < s.w && finalY >= 0 && finalY < s.h) {
 					if (clipToGameArea && finalY >= actionBarTopY())
 						continue;
-					// Check for depth
-					uint8 bgDepth = g_engine->_depthMap.getPixel(finalX, finalY);
-					// Depth test: draw pixel only if depth map value < character depth
-					// (verified: drawSpriteTransparent at 1010:0ed1 uses *depthMap < param_4)
-					if (!useDepth || bgDepth < depth) {
-						s.setPixel(x + actualX, y + currentY, val);
+					if (useDepth) {
+						if (finalX >= g_engine->_depthMap.w || finalY >= g_engine->_depthMap.h)
+							continue;
+						if (g_engine->_depthMap.getPixel(finalX, finalY) >= depth)
+							continue;
 					}
+					s.setPixel(x + actualX, y + currentY, val);
 				}
 			}
 		}
@@ -3400,6 +3407,8 @@ void View1::drawBorderSide(const Common::Point &pos, const Common::Point &size,
 	uint16 currentX = clippingRect.left;
 	uint16 currentY = clippingRect.top;
 	const AnimFrame &sprite = g_engine->_imageResources[31];
+	if (sprite._width == 0 || sprite._height == 0 || sprite._data.empty())
+		return;
 
 	while (currentY < clippingRect.bottom) {
 		while (currentX < clippingRect.right) {
diff --git a/engines/macs2/view1.h b/engines/macs2/view1.h
index b2aa1f49f68..f86ac6f35d0 100644
--- a/engines/macs2/view1.h
+++ b/engines/macs2/view1.h
@@ -269,6 +269,7 @@ private:
 	// if pending != 0 -> uiBackgroundRestorePending=1; flip; runScriptExecutor; pending=0.
 	void runInventoryPanelScriptIfPending(bool excludeCloseButton);
 	bool handleActionBarClick(const MouseDownMessage &msg);
+	void walkToScreenPosition(const Common::Point &pos);
 
 	// Input state machine from handleInput (1008:e8bf).
 	// The original game's input handler has two major branches:


Commit: 57873094c10e283e28eda5a3def059cc10496ccf
    https://github.com/scummvm/scummvm/commit/57873094c10e283e28eda5a3def059cc10496ccf
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-31T21:14:09+02:00

Commit Message:
MACS2: use the dialogue font for german special chars

Changed paths:
    engines/macs2/actionbar.cpp


diff --git a/engines/macs2/actionbar.cpp b/engines/macs2/actionbar.cpp
index 63158f7cc90..b69a877f1c8 100644
--- a/engines/macs2/actionbar.cpp
+++ b/engines/macs2/actionbar.cpp
@@ -188,12 +188,13 @@ void ActionBar::drawSentenceLine(Graphics::ManagedSurface &s) {
 	if (sentence.empty())
 		return;
 
-	const GlyphData *font = nullptr;
-	uint16 fontCount = 0;
-	int glyphH = 0;
-	actionBarFont(font, fontCount, glyphH);
-	if (g_engine->numPanelGlyphs > 0)
-		sentence.toUppercase();
+	// Dialogue Font1 has German glyphs; the panel/save-load font does not.
+	const GlyphData *font = g_engine->_glyphs;
+	uint16 fontCount = g_engine->numGlyphs;
+	int glyphH = (int)g_engine->maxGlyphHeight;
+	if (fontCount == 0)
+		actionBarFont(font, fontCount, glyphH);
+
 	const int textY = kUITop + MAX(0, (kSentenceH - glyphH) / 2);
 	const int textX = MAX(0, (kScreenWidth - _view->measureStringWithFont(sentence, font, fontCount)) / 2);
 	_view->renderStringWithFontTo(textX, textY, sentence, font, fontCount, s);


Commit: 9f6feb29d74268703b595eebdfa8373da301bdfe
    https://github.com/scummvm/scummvm/commit/9f6feb29d74268703b595eebdfa8373da301bdfe
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-31T21:14:09+02:00

Commit Message:
MACS2: convert to enums

Changed paths:
    engines/macs2/actionbar.cpp
    engines/macs2/macs2.cpp
    engines/macs2/macs2.h
    engines/macs2/scriptexecutor.cpp
    engines/macs2/view1.cpp


diff --git a/engines/macs2/actionbar.cpp b/engines/macs2/actionbar.cpp
index b69a877f1c8..d2e4bba351c 100644
--- a/engines/macs2/actionbar.cpp
+++ b/engines/macs2/actionbar.cpp
@@ -59,7 +59,7 @@ ActionBar::ActionBar(View1 *view)
 
 bool ActionBar::isPointInUI(const Common::Point &pos) const {
 	if (useNativeSkin()) {
-		if (g_engine->_menuMode == 0)
+		if (g_engine->_menuMode == MenuMode::Hidden)
 			return false;
 		return pos.y >= (int16)g_engine->_panelTopY;
 	}
@@ -591,10 +591,10 @@ const HudButton *ActionBar::findHudButtonAt(const Common::Point &pos, int *outIn
 	if (!isPointInUI(pos))
 		return nullptr;
 	const uint16 panelTop = g_engine->_panelTopY;
-	const uint16 menuMode = g_engine->_menuMode;
+	const MenuMode menuMode = g_engine->_menuMode;
 	for (uint i = 0; i < g_engine->_hudButtons.size(); i++) {
 		const HudButton &btn = g_engine->_hudButtons[i];
-		if (btn.menuId != menuMode || btn.frame._data.empty())
+		if (btn.menuId != (uint16)menuMode || btn.frame._data.empty())
 			continue;
 		const AnimFrame &hitFrame = btn.frame;
 		const Common::Point local(pos.x - btn.x, pos.y - (int)panelTop - btn.y);
@@ -614,11 +614,11 @@ const HudButton *ActionBar::findHudButtonAt(const Common::Point &pos, int *outIn
 void ActionBar::drawNative(Graphics::ManagedSurface &s) {
 	if (!g_engine->hasNativeHudAssets())
 		return;
-	if (g_engine->_menuMode == 0)
+	if (g_engine->_menuMode == MenuMode::Hidden)
 		return;
 
 	const uint16 panelTop = g_engine->_panelTopY;
-	const uint16 menuMode = g_engine->_menuMode;
+	const MenuMode menuMode = g_engine->_menuMode;
 	const int megaIndex = (int)menuMode - 1;
 	if (megaIndex >= 0 && megaIndex < 6 && g_engine->_hudMegapicLoaded[megaIndex]) {
 		const Graphics::ManagedSurface &mega = g_engine->_hudMegapics[megaIndex];
@@ -628,20 +628,20 @@ void ActionBar::drawNative(Graphics::ManagedSurface &s) {
 	}
 
 	for (const HudButton &btn : g_engine->_hudButtons) {
-		if (btn.menuId != menuMode || btn.frame._data.empty())
+		if (btn.menuId != (uint16)menuMode || btn.frame._data.empty())
 			continue;
 		const AnimFrame *frame = &btn.frame;
 		const Script::MouseMode mode = g_engine->_scriptExecutor->_cursorMode;
 		const bool selected =
-			(menuMode == 1 &&
+			(menuMode == MenuMode::Main &&
 			 ((btn.buttonId == 1 && mode == Script::MouseMode::Walk) ||
 			  (btn.buttonId == 2 && mode == Script::MouseMode::Look) ||
 			  (btn.buttonId == 3 && mode == Script::MouseMode::Talk) ||
 			  (btn.buttonId == 4 && (mode == Script::MouseMode::Use ||
 									mode == Script::MouseMode::UseInventory)))) ||
-			(menuMode == 2 &&
-			 ((btn.buttonId == 0x1e && g_engine->_optionsSubMode == 1) ||
-			  (btn.buttonId == 0x1f && g_engine->_optionsSubMode == 2)));
+			(menuMode == MenuMode::Options &&
+			 ((btn.buttonId == 0x1e && g_engine->_optionsSubMode == OptionsSubMode::Save) ||
+			  (btn.buttonId == 0x1f && g_engine->_optionsSubMode == OptionsSubMode::Load)));
 		const bool pressed = (_pressedButtonId != 0 && btn.buttonId == _pressedButtonId);
 		const bool hovered = (_hoveredButtonId != 0 && btn.buttonId == _hoveredButtonId);
 
@@ -663,7 +663,7 @@ void ActionBar::drawNative(Graphics::ManagedSurface &s) {
 	const GlyphData *panelFont = g_engine->numPanelGlyphs ? g_engine->_panelGlyphs : g_engine->_glyphs;
 	const uint16 panelFontCount = g_engine->numPanelGlyphs ? g_engine->numPanelGlyphs : g_engine->numGlyphs;
 
-	if (menuMode == 1) {
+	if (menuMode == MenuMode::Main) {
 		if (_view->_inventorySource == nullptr ||
 			_view->_inventorySource->_index != Scenes::instance()._currentActorIndex)
 			_view->setInventorySource(GameObjects::instance().getProtagonistObject());
@@ -718,7 +718,7 @@ void ActionBar::drawNative(Graphics::ManagedSurface &s) {
 				_view->renderStringWithFontTo((uint16)textX, (uint16)textY, sentence, font, fontCount, s);
 			}
 		}
-	} else if (menuMode == 2 && panelFontCount != 0) {
+	} else if (menuMode == MenuMode::Options && panelFontCount != 0) {
 		if (g_engine->_saveSlotNames.empty())
 			refreshSaveSlotNames();
 		for (uint i = 0; i < g_engine->_saveSlotNames.size() && i < lineCount; i++) {
@@ -731,8 +731,8 @@ void ActionBar::drawNative(Graphics::ManagedSurface &s) {
 			_view->renderStringWithFontTo(optTextX, panelTop + optTextY + (int)i * linePitch,
 										  name, panelFont, panelFontCount, s);
 		}
-	} else if (menuMode == 4 && panelFontCount != 0 && _view->_isDialogueChoiceInputActive) {
-		// Dialogue choice list at layout[5..6]; wired when assets set menuMode 4.
+	} else if (menuMode == MenuMode::DialogueList && panelFontCount != 0 && _view->_isDialogueChoiceInputActive) {
+		// Dialogue choice list at layout[5..6]; wired when assets set DialogueList.
 		const uint16 dlgX = g_engine->_hudTextLayout[5];
 		const uint16 dlgY = g_engine->_hudTextLayout[6];
 		const uint16 pitch = g_engine->_hudTextLayout[4] ? g_engine->_hudTextLayout[4] : 10;
@@ -754,9 +754,9 @@ bool ActionBar::handleClickNative(const Common::Point &pos) {
 
 	const uint16 panelTop = g_engine->_panelTopY;
 	const int localY = pos.y - (int)panelTop;
-	const uint16 menuMode = g_engine->_menuMode;
+	const MenuMode menuMode = g_engine->_menuMode;
 
-	if (menuMode == 1) {
+	if (menuMode == MenuMode::Main) {
 		const uint16 cols = g_engine->_inventCols;
 		const uint16 rows = g_engine->_inventRows;
 		const uint16 slotW = g_engine->_inventSlotW;
@@ -800,7 +800,7 @@ bool ActionBar::handleClickNative(const Common::Point &pos) {
 		}
 	}
 
-	if (menuMode == 2 && g_engine->_optionsSubMode != 0) {
+	if (menuMode == MenuMode::Options && g_engine->_optionsSubMode != OptionsSubMode::None) {
 		const uint16 textX = g_engine->_hudTextLayout[0];
 		const uint16 textY = g_engine->_hudTextLayout[1];
 		const uint16 textMaxW = g_engine->_hudTextLayout[2] ? g_engine->_hudTextLayout[2] : 212;
@@ -811,9 +811,9 @@ bool ActionBar::handleClickNative(const Common::Point &pos) {
 			const uint16 row = (uint16)((localY - textY) / linePitch);
 			const uint16 start = g_engine->_saveListScroll == 0 ? 1 : g_engine->_saveListScroll;
 			const int slot = (int)start - 1 + (int)row;
-			if (g_engine->_optionsSubMode == 2) {
+			if (g_engine->_optionsSubMode == OptionsSubMode::Load) {
 				g_engine->loadGameState(slot);
-			} else if (g_engine->_optionsSubMode == 1) {
+			} else if (g_engine->_optionsSubMode == OptionsSubMode::Save) {
 				Common::String name = Common::String::format(uiText("Spielstand %d").c_str(), slot + 1);
 				if (row < g_engine->_saveSlotNames.size() &&
 					!g_engine->_saveSlotNames[row].empty() &&
@@ -827,7 +827,7 @@ bool ActionBar::handleClickNative(const Common::Point &pos) {
 		}
 	}
 
-	if (menuMode == 4 && _view->_isDialogueChoiceInputActive) {
+	if (menuMode == MenuMode::DialogueList && _view->_isDialogueChoiceInputActive) {
 		const uint16 dlgX = g_engine->_hudTextLayout[5];
 		const uint16 dlgY = g_engine->_hudTextLayout[6];
 		const uint16 pitch = g_engine->_hudTextLayout[4] ? g_engine->_hudTextLayout[4] : 10;
@@ -868,20 +868,20 @@ bool ActionBar::handleClickNative(const Common::Point &pos) {
 		g_engine->setCursorMode(Script::MouseMode::Use);
 	} else if (id == 0x33) {
 		g_engine->_savedMenuCursorMode = g_engine->_scriptExecutor->_cursorMode;
-		g_engine->_menuMode = 2;
-		g_engine->_optionsSubMode = 0;
+		g_engine->_menuMode = MenuMode::Options;
+		g_engine->_optionsSubMode = OptionsSubMode::None;
 		g_engine->_saveListScroll = 1;
 		refreshSaveSlotNames();
 		g_engine->setCursorMode(Script::MouseMode::PanelCursor);
 	} else if (id == 0x32) {
-		g_engine->_menuMode = 1;
-		g_engine->_optionsSubMode = 0;
+		g_engine->_menuMode = MenuMode::Main;
+		g_engine->_optionsSubMode = OptionsSubMode::None;
 		g_engine->setCursorMode(g_engine->_savedMenuCursorMode);
 	} else if (id == 0x1e) {
-		g_engine->_optionsSubMode = 1;
+		g_engine->_optionsSubMode = OptionsSubMode::Save;
 		refreshSaveSlotNames();
 	} else if (id == 0x1f) {
-		g_engine->_optionsSubMode = 2;
+		g_engine->_optionsSubMode = OptionsSubMode::Load;
 		refreshSaveSlotNames();
 	} else if (id == 0x20) {
 		g_engine->softRestart();
@@ -942,7 +942,7 @@ bool ActionBar::handleClickNative(const Common::Point &pos) {
 	} else if (id == 0x41) {
 		g_engine->_scriptExecutor->_textEnabled = false;
 	} else {
-		debugC(1, kDebugScript, "ActionBar: unhandled button id=0x%x menu=%u", id, menuMode);
+		debugC(1, kDebugScript, "ActionBar: unhandled button id=0x%x menu=%u", id, (uint)menuMode);
 	}
 	_view->updateCursor();
 	_view->redraw();
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 11df6a165bd..a276dea557d 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -290,8 +290,8 @@ void Macs2Engine::loadResourceFileV2() {
 	}
 	_panelTopY = 0;
 	_panelHeight = 0;
-	_menuMode = 1;
-	_optionsSubMode = 0;
+	_menuMode = MenuMode::Main;
+	_optionsSubMode = OptionsSubMode::None;
 	_savedMenuCursorMode = Script::MouseMode::Walk;
 	_inventScroll = 1;
 	memset(_hudTextLayout, 0, sizeof(_hudTextLayout));
@@ -673,8 +673,8 @@ void Macs2Engine::softRestart() {
 	stopSpeech();
 	clearDeltaAnim();
 	_skipSpeed = 1;
-	_menuMode = 1;
-	_optionsSubMode = 0;
+	_menuMode = MenuMode::Main;
+	_optionsSubMode = OptionsSubMode::None;
 	_inventScroll = 1;
 	_saveListScroll = 1;
 
@@ -1277,8 +1277,8 @@ void Macs2Engine::changeScene(uint32 newSceneIndex, bool executeScript) {
 		if (!loadSceneGraphics(newSceneIndex))
 			error("changeScene(): Failed to load scene graphics for scene %u", newSceneIndex);
 
-		_menuMode = 1;
-		_optionsSubMode = 0;
+		_menuMode = MenuMode::Main;
+		_optionsSubMode = OptionsSubMode::None;
 		_bottomHudVisible = true;
 
 		View1 *currentView = (View1 *)findView("View1");
@@ -1406,8 +1406,8 @@ void Macs2Engine::changeScene(uint32 newSceneIndex, bool executeScript) {
 	// Scene change starts with the main HUD shown. v2 scripts may hide it
 	// during init (overview map). v1 has no hide/show opcodes; kEnhUIUX uses
 	// the same flag so a scene change restores the strip.
-	_menuMode = 1;
-	_optionsSubMode = 0;
+	_menuMode = MenuMode::Main;
+	_optionsSubMode = OptionsSubMode::None;
 	_bottomHudVisible = true;
 
 	// Refresh characters
@@ -2319,19 +2319,19 @@ void Macs2Engine::nextCursorMode() {
 }
 
 void Macs2Engine::setBottomHudVisible(bool visible) {
-	// hide/show opcodes toggle menuMode (0 vs 1). Cursor mode alone never hides
+	// hide/show opcodes toggle Hidden vs Main. Cursor mode alone never hides
 	// the HUD. Native skin restores the cursor saved on hide.
 	if (hasNativeHudAssets()) {
 		if (visible) {
-			if (_menuMode == 0) {
-				_menuMode = 1;
+			if (_menuMode == MenuMode::Hidden) {
+				_menuMode = MenuMode::Main;
 				if (_scriptExecutor)
 					setCursorMode(_savedMenuCursorMode);
 			}
 		} else {
-			if (_menuMode == 1 && _scriptExecutor)
+			if (_menuMode == MenuMode::Main && _scriptExecutor)
 				_savedMenuCursorMode = _scriptExecutor->_cursorMode;
-			_menuMode = 0;
+			_menuMode = MenuMode::Hidden;
 		}
 	}
 	_bottomHudVisible = visible;
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index 0d98ebe4d04..0732cb399ad 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -123,6 +123,19 @@ struct AnimFrame : public Sprite {
 	Common::Point getBottomMiddleOffset(uint16 scale = 100) const;
 };
 
+enum class MenuMode : uint16 {
+	Hidden = 0,
+	Main = 1,
+	Options = 2,
+	DialogueList = 4
+};
+
+enum class OptionsSubMode : uint16 {
+	None = 0,
+	Save = 1,
+	Load = 2
+};
+
 /** Persistent native HUD button (megapic panel skin). */
 struct HudButton {
 	int16 x = 0;
@@ -131,7 +144,7 @@ struct HudButton {
 	uint16 activeStep = 0;
 	uint16 hoverStep = 0;
 	uint16 buttonId = 0; // 1=Walk, 2=Look, 3=Talk, 4=Use, 0x33=Options, ...
-	uint16 menuId = 0;   // 1=main bar, 2=options, ...
+	uint16 menuId = 0;   // MenuMode value from assets; 7 = cursor-map entry
 	AnimFrame frame;
 	AnimFrame activeFrame;
 	AnimFrame hoverFrame;
@@ -554,10 +567,8 @@ public:
 	/** Y where the bottom HUD starts; scene above this is interactive. */
 	uint16 _panelTopY = 0;
 	uint16 _panelHeight = 0;
-	/** 0=hidden, 1=main verbs/invent, 2=options, 4=dialogue list. */
-	uint16 _menuMode = 1;
-	/** 0=none, 1=save, 2=load (options submenu). */
-	uint16 _optionsSubMode = 0;
+	MenuMode _menuMode = MenuMode::Main;
+	OptionsSubMode _optionsSubMode = OptionsSubMode::None;
 	Script::MouseMode _savedMenuCursorMode = Script::MouseMode::Walk;
 	uint16 _inventScroll = 1;
 	uint16 _inventOriginX = 0;
@@ -776,7 +787,7 @@ public:
 
 	/**
 	 * Bottom HUD / action-bar visibility (dialect-neutral).
-	 * hide/show opcodes toggle menuMode (0 vs 1) and restore the cursor saved
+	 * hide/show opcodes toggle MenuMode Hidden vs Main and restore the cursor saved
 	 * on hide. The Scumm kEnhUIUX strip uses the same flag; cursor mode alone
 	 * never hides the bar.
 	 */
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index f80068e24fc..ef32d5c2e36 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -3833,7 +3833,7 @@ OpcodeResult ScriptExecutor::scriptShowActionBar() {
 		return OpcodeResult::Continue;
 
 	// v2 opcode 0x65. v1 tables end at waitForAdlib; kEnhUIUX uses the same
-	// setBottomHudVisible switch (menuMode 0 -> 1 + cursor restore).
+	// setBottomHudVisible switch (MenuMode Hidden -> Main + cursor restore).
 	if (_engine->hasNativeHudAssets() || currentView->hasPersistentActionBar()) {
 		_engine->setBottomHudVisible(true);
 		currentView->updateCursor();
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index 19a2b3c4761..7017558bca3 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -181,7 +181,7 @@ bool View1::hasPersistentActionBar() const {
 int View1::actionBarTopY() const {
 	if (_actionBar && shouldShowActionBar())
 		return _actionBar->gameAreaBottomY();
-	if (g_engine->hasNativeHudAssets() && g_engine->isBottomHudVisible() && g_engine->_menuMode != 0)
+	if (g_engine->hasNativeHudAssets() && g_engine->isBottomHudVisible() && g_engine->_menuMode != MenuMode::Hidden)
 		return (int)g_engine->_panelTopY;
 	return g_engine->gameHeight();
 }
@@ -2328,7 +2328,7 @@ void View1::draw() {
 				_actionBar->draw(fullScreen);
 				// Glyphs use setPixel and do not dirty the Screen.
 				g_events->getScreen()->addDirtyRect(Common::Rect(0, actionBarTopY(), sw, sh));
-			} else if (g_engine->hasNativeHudAssets() && g_engine->_menuMode == 0) {
+			} else if (g_engine->hasNativeHudAssets() && g_engine->_menuMode == MenuMode::Hidden) {
 				// hideActionBar / overview map: leave playfield pixels in the
 				// former panel band so scene art and hotspots stay visible.
 			} else {


Commit: 54fb2284b9a725a084e9fd7c89559ee17b39bd16
    https://github.com/scummvm/scummvm/commit/54fb2284b9a725a084e9fd7c89559ee17b39bd16
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-31T21:14:09+02:00

Commit Message:
MACS2: renamed members

Changed paths:
    engines/macs2/actionbar.cpp
    engines/macs2/amiga_decode.cpp
    engines/macs2/amiga_resources.cpp
    engines/macs2/debugtools.cpp
    engines/macs2/macs2.cpp
    engines/macs2/macs2.h
    engines/macs2/view1.cpp


diff --git a/engines/macs2/actionbar.cpp b/engines/macs2/actionbar.cpp
index d2e4bba351c..311eff50830 100644
--- a/engines/macs2/actionbar.cpp
+++ b/engines/macs2/actionbar.cpp
@@ -179,8 +179,8 @@ void ActionBar::drawUIButton(const Common::Rect &rect, bool pressed, Graphics::M
 void ActionBar::actionBarFont(const GlyphData *&font, uint16 &fontCount, int &glyphH) const {
 	const bool usePanelFont = g_engine->numPanelGlyphs > 0;
 	font = usePanelFont ? g_engine->_panelGlyphs : g_engine->_glyphs;
-	fontCount = usePanelFont ? g_engine->numPanelGlyphs : g_engine->numGlyphs;
-	glyphH = usePanelFont ? (int)g_engine->maxPanelGlyphHeight : (int)g_engine->maxGlyphHeight;
+	fontCount = usePanelFont ? g_engine->numPanelGlyphs : g_engine->_numGlyphs;
+	glyphH = usePanelFont ? (int)g_engine->maxPanelGlyphHeight : (int)g_engine->_maxGlyphHeight;
 }
 
 void ActionBar::drawSentenceLine(Graphics::ManagedSurface &s) {
@@ -190,8 +190,8 @@ void ActionBar::drawSentenceLine(Graphics::ManagedSurface &s) {
 
 	// Dialogue Font1 has German glyphs; the panel/save-load font does not.
 	const GlyphData *font = g_engine->_glyphs;
-	uint16 fontCount = g_engine->numGlyphs;
-	int glyphH = (int)g_engine->maxGlyphHeight;
+	uint16 fontCount = g_engine->_numGlyphs;
+	int glyphH = (int)g_engine->_maxGlyphHeight;
 	if (fontCount == 0)
 		actionBarFont(font, fontCount, glyphH);
 
@@ -661,7 +661,7 @@ void ActionBar::drawNative(Graphics::ManagedSurface &s) {
 	const uint16 lineCount = g_engine->_hudTextLayout[3] ? g_engine->_hudTextLayout[3] : 9;
 	const uint16 linePitch = g_engine->_hudTextLayout[4] ? g_engine->_hudTextLayout[4] : 10;
 	const GlyphData *panelFont = g_engine->numPanelGlyphs ? g_engine->_panelGlyphs : g_engine->_glyphs;
-	const uint16 panelFontCount = g_engine->numPanelGlyphs ? g_engine->numPanelGlyphs : g_engine->numGlyphs;
+	const uint16 panelFontCount = g_engine->numPanelGlyphs ? g_engine->numPanelGlyphs : g_engine->_numGlyphs;
 
 	if (menuMode == MenuMode::Main) {
 		if (_view->_inventorySource == nullptr ||
@@ -700,8 +700,8 @@ void ActionBar::drawNative(Graphics::ManagedSurface &s) {
 			delete icon;
 		}
 
-		const GlyphData *font = g_engine->numGlyphs ? g_engine->_glyphs : panelFont;
-		const uint16 fontCount = g_engine->numGlyphs ? g_engine->numGlyphs : panelFontCount;
+		const GlyphData *font = g_engine->_numGlyphs ? g_engine->_glyphs : panelFont;
+		const uint16 fontCount = g_engine->_numGlyphs ? g_engine->_numGlyphs : panelFontCount;
 		if (fontCount != 0) {
 			Common::String sentence = buildSentenceLine();
 			if (!sentence.empty()) {
@@ -713,7 +713,7 @@ void ActionBar::drawNative(Graphics::ManagedSurface &s) {
 				}
 				const int textW = _view->measureStringWithFont(sentence, font, fontCount);
 				const int textX = MAX(0, (g_engine->screenWidth() - textW) / 2);
-				const int glyphH = g_engine->maxGlyphHeight ? (int)g_engine->maxGlyphHeight : 12;
+				const int glyphH = g_engine->_maxGlyphHeight ? (int)g_engine->_maxGlyphHeight : 12;
 				const int textY = MAX(0, (int)panelTop - glyphH - 2);
 				_view->renderStringWithFontTo((uint16)textX, (uint16)textY, sentence, font, fontCount, s);
 			}
diff --git a/engines/macs2/amiga_decode.cpp b/engines/macs2/amiga_decode.cpp
index 0b85b7f1ae5..2b93a4826b8 100644
--- a/engines/macs2/amiga_decode.cpp
+++ b/engines/macs2/amiga_decode.cpp
@@ -379,7 +379,7 @@ bool convertAmigaPortraitAtlasToDosBlob(const byte *mxoo, uint32 mxooSize, uint3
 
 static bool decompressPp20ToBuffer(const byte *src, uint32 srcLen, Common::Array<byte> &out) {
 	out.clear();
-	if (!src || srcLen < 12 || READ_BE_UINT32(src) != MKTAG('P', 'P', '2', '0'))
+	if (!src)
 		return false;
 
 	uint32 outLen = 0;
diff --git a/engines/macs2/amiga_resources.cpp b/engines/macs2/amiga_resources.cpp
index 9923ab88972..62a0eb6175b 100644
--- a/engines/macs2/amiga_resources.cpp
+++ b/engines/macs2/amiga_resources.cpp
@@ -230,8 +230,8 @@ bool Macs2Engine::loadAmigaMxffFont() {
 	if (!decodeAmigaMxffFont(mxff.data(), size, glyphs) || glyphs.empty())
 		return false;
 
-	numGlyphs = 0;
-	maxGlyphHeight = 0;
+	_numGlyphs = 0;
+	_maxGlyphHeight = 0;
 	amigaTextLinePitch = 0;
 	if (size >= 0x0A) {
 		const uint16 atlasRows = READ_BE_UINT16(mxff.data() + 8);
@@ -256,20 +256,20 @@ bool Macs2Engine::loadAmigaMxffFont() {
 			else if (c < 16)
 				_glyphs[i]._data[p] = (byte)(0xF0 + c);
 		}
-		maxGlyphHeight = MAX(maxGlyphHeight, _glyphs[i]._height);
-		numGlyphs++;
+		_maxGlyphHeight = MAX(_maxGlyphHeight, _glyphs[i]._height);
+		_numGlyphs++;
 	}
-	if (amigaTextLinePitch == 0 && maxGlyphHeight > 1)
-		amigaTextLinePitch = (uint16)(maxGlyphHeight - 1);
+	if (amigaTextLinePitch == 0 && _maxGlyphHeight > 1)
+		amigaTextLinePitch = (uint16)(_maxGlyphHeight - 1);
 	// Reuse dialogue font for panel/save UI until a second MXFF exists.
-	numPanelGlyphs = numGlyphs;
-	maxPanelGlyphHeight = maxGlyphHeight;
-	for (uint i = 0; i < numGlyphs; i++)
+	numPanelGlyphs = _numGlyphs;
+	maxPanelGlyphHeight = _maxGlyphHeight;
+	for (uint i = 0; i < _numGlyphs; i++)
 		_panelGlyphs[i] = _glyphs[i];
 
 	debugC(1, kDebugFilePath, "Amiga: loaded MXFF font FF_0000 (%u glyphs, height %u, linePitch %u)",
-		   numGlyphs, maxGlyphHeight, amigaTextLinePitch);
-	return numGlyphs > 0;
+		   _numGlyphs, _maxGlyphHeight, amigaTextLinePitch);
+	return _numGlyphs > 0;
 }
 
 bool Macs2Engine::loadAmigaOverlayFontResource(uint16 ffId) {
@@ -324,12 +324,12 @@ bool Macs2Engine::loadAmigaOverlayFont(uint8 resourceIndex) {
 	}
 
 	// Fall back to the already-loaded main MXFF dialogue font.
-	if (numGlyphs == 0)
+	if (_numGlyphs == 0)
 		return false;
 
-	numOverlayGlyphs = numGlyphs;
-	maxOverlayGlyphHeight = maxGlyphHeight;
-	for (uint i = 0; i < numGlyphs; i++)
+	numOverlayGlyphs = _numGlyphs;
+	maxOverlayGlyphHeight = _maxGlyphHeight;
+	for (uint i = 0; i < _numGlyphs; i++)
 		_overlayGlyphs[i] = _glyphs[i];
 	return true;
 }
diff --git a/engines/macs2/debugtools.cpp b/engines/macs2/debugtools.cpp
index e000b31cd04..6a3335373a6 100644
--- a/engines/macs2/debugtools.cpp
+++ b/engines/macs2/debugtools.cpp
@@ -791,7 +791,7 @@ static void showVariablesWindow() {
 				ImGui::Text("Showing: Y | Count: %u", view->_dialogueChoiceCount);
 				ImGui::Text("BoxPos: (%d,%d)", view->_stringBoxPosition.x, view->_stringBoxPosition.y);
 				Common::Point mousePos = g_system->getEventManager()->getMousePos();
-				int lineHeight = g_engine->maxGlyphHeight + 2;
+				int lineHeight = g_engine->_maxGlyphHeight + 2;
 				int firstLineY = view->_stringBoxPosition.y + 9;
 				int relY = mousePos.y - firstLineY;
 				int hoveredChoice = -1;
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index a276dea557d..3e4f3dfc18d 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -235,12 +235,12 @@ void Macs2Engine::loadResourceFileV1() {
 	uint32 font1SizeField = _fileStream->readUint32LE(); // skip size field
 	(void)font1SizeField;
 	uint16 font1GlyphCount = _fileStream->readUint16LE();
-	maxGlyphHeight = 0;
+	_maxGlyphHeight = 0;
 	for (uint i = 0; i < font1GlyphCount; i++) {
 		_glyphs[i].readFromMemory(_fileStream);
-		maxGlyphHeight = MAX(_glyphs[i]._height, maxGlyphHeight);
+		_maxGlyphHeight = MAX(_glyphs[i]._height, _maxGlyphHeight);
 	}
-	numGlyphs = font1GlyphCount;
+	_numGlyphs = font1GlyphCount;
 
 	// Font 2: clean sans-serif font used by save/load panel (scene data offset 0x1044)
 	uint32 font2SizeField = _fileStream->readUint32LE();
@@ -273,7 +273,7 @@ void Macs2Engine::loadResourceFileV2() {
 	//   TalkVol + Font1 + SysFont + 0x400 map offsets
 	_shadingTable.clear();
 	_shadingTable.resize(0x800, 0);
-	numGlyphs = 0;
+	_numGlyphs = 0;
 	numPanelGlyphs = 0;
 	memset(_mapSceneOffsets, 0, sizeof(_mapSceneOffsets));
 	_imageResources.clear();
@@ -478,7 +478,7 @@ void Macs2Engine::loadResourceFileV2() {
 		_fileStream->seek(fontStart + (int64)fontSize, SEEK_SET);
 		return true;
 	};
-	if (!loadSizedFont(_glyphs, numGlyphs, maxGlyphHeight))
+	if (!loadSizedFont(_glyphs, _numGlyphs, _maxGlyphHeight))
 		warning("readGlobalAssetsV2: failed loading Font1");
 	if (!loadSizedFont(_panelGlyphs, numPanelGlyphs, maxPanelGlyphHeight))
 		warning("readGlobalAssetsV2: failed loading SysFont");
@@ -503,7 +503,7 @@ void Macs2Engine::loadResourceFileV2() {
 		   "readGlobalAssetsV2: panel=%u+%u megapics=%u buttons=%u cursors=%u invent=%ux%u @(%u,%u) fonts=%u/%u",
 		   _panelTopY, _panelHeight, megas, (uint)_hudButtons.size(), installed,
 		   _inventCols, _inventRows, _inventOriginX, _inventOriginY,
-		   numGlyphs, numPanelGlyphs);
+		   _numGlyphs, numPanelGlyphs);
 	_fileStream->seek(kMcsV2ActorIndexOffset, SEEK_SET);
 	bootstrapMcsActorsObjectsAndScene();
 }
@@ -1883,7 +1883,7 @@ bool Macs2Engine::loadOverlayFont(uint8 resourceIndex, uint16 executingObjectID)
 }
 
 bool Macs2Engine::findGlyph(char c, GlyphData &out) const {
-	for (int i = 0; i < numGlyphs; i++) {
+	for (int i = 0; i < _numGlyphs; i++) {
 		if (_glyphs[i]._ascii == c) {
 			out = _glyphs[i];
 			return true;
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index 0732cb399ad..6c471e31fac 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -144,7 +144,7 @@ struct HudButton {
 	uint16 activeStep = 0;
 	uint16 hoverStep = 0;
 	uint16 buttonId = 0; // 1=Walk, 2=Look, 3=Talk, 4=Use, 0x33=Options, ...
-	uint16 menuId = 0;   // MenuMode value from assets; 7 = cursor-map entry
+	uint16 menuId = 0;
 	AnimFrame frame;
 	AnimFrame activeFrame;
 	AnimFrame hoverFrame;
@@ -518,8 +518,8 @@ public:
 	void applyDeltaFrameToBackground(const DeltaFrame &frame);
 	void playDeltaFrameSfx(uint16 displayFrame);
 	// Font glyph count (79 glyphs in the resource file's font data)
-	uint16 numGlyphs = 79;
-	uint16 maxGlyphHeight;
+	uint16 _numGlyphs = 79;
+	uint16 _maxGlyphHeight;
 
 	AnimFrame _animFrames[6];
 	// 6 flag/decoration animation frames at fixed file offset 0x6A5941, each followed by 6 padding bytes
@@ -843,8 +843,8 @@ public:
 	 */
 	int dialogLineHeight() const {
 		if (isAmiga())
-			return amigaTextLinePitch ? (int)amigaTextLinePitch : (int)maxGlyphHeight;
-		return (int)maxGlyphHeight + dialogLineGap();
+			return amigaTextLinePitch ? (int)amigaTextLinePitch : (int)_maxGlyphHeight;
+		return (int)_maxGlyphHeight + dialogLineGap();
 	}
 
 	/** Depth-map compare Y for sprite occlusion (halved on v2 full-res depth). */
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index 7017558bca3..30ac21bf633 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -770,7 +770,7 @@ void View1::drawOverlayTextEntries() {
 		Common::String text = entry.text;
 		// Use overlay font if loaded, otherwise fall back to main font
 		const GlyphData *font = g_engine->numOverlayGlyphs > 0 ? g_engine->_overlayGlyphs : g_engine->_glyphs;
-		uint16 fontCount = g_engine->numOverlayGlyphs > 0 ? g_engine->numOverlayGlyphs : g_engine->numGlyphs;
+		uint16 fontCount = g_engine->numOverlayGlyphs > 0 ? g_engine->numOverlayGlyphs : g_engine->_numGlyphs;
 
 		if (entry.alignment == 1) {
 			x -= measureStringWithFont(text, font, fontCount);
@@ -4474,7 +4474,7 @@ void View1::drawOriginalSaveLoadPanel(Graphics::ManagedSurface &s) {
 			label = "NONE";
 		}
 		const GlyphData *font = g_engine->numPanelGlyphs > 0 ? g_engine->_panelGlyphs : g_engine->_glyphs;
-		uint16 fontCount = g_engine->numPanelGlyphs > 0 ? g_engine->numPanelGlyphs : g_engine->numGlyphs;
+		uint16 fontCount = g_engine->numPanelGlyphs > 0 ? g_engine->numPanelGlyphs : g_engine->_numGlyphs;
 		label.toUppercase();
 		renderStringWithFont(panelX + 6, panelY + 6 + slot * 0xc, label, font, fontCount);
 	}


Commit: 95f33ac6be681b5543d63da725fc0624566051bc
    https://github.com/scummvm/scummvm/commit/95f33ac6be681b5543d63da725fc0624566051bc
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-31T21:14:09+02:00

Commit Message:
MACS2: cleanup amiga resource loading

Changed paths:
    engines/macs2/amiga_decode.cpp
    engines/macs2/amiga_decode.h
    engines/macs2/amiga_resources.cpp


diff --git a/engines/macs2/amiga_decode.cpp b/engines/macs2/amiga_decode.cpp
index 2b93a4826b8..9582047efe6 100644
--- a/engines/macs2/amiga_decode.cpp
+++ b/engines/macs2/amiga_decode.cpp
@@ -71,8 +71,6 @@ bool inspectAmigaAnimSlot(const byte *mxoo, uint32 mxooSize, uint32 bodyRelative
 	out.repeatCounter = READ_BE_UINT16(p + 4);
 	out.loopStart = READ_BE_UINT16(p + 6);
 
-	// Sequence length follows the same pattern as the DOS blob: headerHint is often
-	// the frame count; sequence payload is max(hint - 2, 0) bytes, padded to even.
 	const uint16 seqBytes = (out.headerHint >= 2) ? (uint16)(out.headerHint - 2) : 0;
 	const uint16 seqPadded = (seqBytes + 1) & ~1;
 	const uint32 metaOff = absOff + 8 + seqPadded;
@@ -114,10 +112,6 @@ bool decodeAmigaPlanarFrame(const byte *planar, uint16 width, uint16 height, uin
 	outPixels.resize((uint32)width * height);
 	Common::fill(outPixels.begin(), outPixels.end(), 0);
 
-	// Anim slots store 6 bitplanes; colors use planes 0..4 (indices 0..31).
-	// Plane 5 is not applied as a sprite mask here - doing so clears almost all
-	// opaque pixels in the demo's character anims. Color 0 remains transparent
-	// for the existing DOS draw path.
 	for (uint16 y = 0; y < height; y++) {
 		for (uint16 x = 0; x < width; x++) {
 			byte color = 0;
@@ -143,11 +137,7 @@ bool convertAmigaAnimSlotToDosBlob(const byte *mxoo, uint32 mxooSize, uint32 bod
 
 	const byte *planar = mxoo + info.pixelOffset;
 
-	// DOS AnimBlobView layout (LE):
-	//   +0x00 unknown, +0x02 seqPos, +0x04 repeat, +0x06 loopStart, +0x08 delay,
-	//   +0x0A seqLenMinusOne; sequence at +0x0C (N bytes);
-	//   frames at +0x0B + (N+1) = +0x0C + N: uint16 frameCount, then per-frame records
-	// Frame: ox,oy,unk,w,h (LE) + chunky pixels
+	// DOS AnimBlobView layout
 	Common::Array<byte> sequence;
 	for (uint16 i = 0; i < info.frameCount; i++)
 		sequence.push_back((byte)(10 + i));
@@ -209,8 +199,6 @@ bool extractAmigaStringBlock(const byte *mxoo, uint32 mxooSize, Common::Array<by
 	AmigaMxooInfo info;
 	if (!parseAmigaMxoo(mxoo, mxooSize, info))
 		return false;
-	// String section: u16BE pad, u16BE size, then length-prefixed plaintext entries.
-	// Script string offsets are relative to the entries (after the 4-byte header).
 	if (info.stringOffset + 4 > mxooSize)
 		return false;
 	const uint16 size = READ_BE_UINT16(mxoo + info.stringOffset + 2);
@@ -283,8 +271,6 @@ bool convertAmigaSimpleSpriteToDosBlob(const byte *mxoo, uint32 mxooSize, Common
 		}
 	}
 
-	// seqLenMinusOne=0 -> sequenceLength=1 -> frame data at 0x0C:
-	// uint16 frameCount, then ox,oy,unk,w,h + pixels.
 	const uint32 total = 0x0C + 2 + 10 + (uint32)width * height;
 	outBlob.resize(total);
 	byte *dst = outBlob.data();
@@ -419,16 +405,16 @@ bool decodeAmigaMxmmSceneBackground(const byte *mxmm, uint32 mxmmSize,
 	outPixels.clear();
 	outColorCount = 0;
 	outPalette = Graphics::Palette(Graphics::PALETTE_COUNT);
-	if (!mxmm || mxmmSize < 14)
+	if (!mxmm || mxmmSize < kAmigaMxmmMinSize)
 		return false;
 	if (READ_BE_UINT32(mxmm) != MKTAG('M', 'X', 'M', 'M'))
 		return false;
 
-	const uint32 chunk0Size = READ_BE_UINT32(mxmm + 10);
-	if (chunk0Size == 0 || 14 + chunk0Size > mxmmSize)
+	const uint32 chunk0Size = READ_BE_UINT32(mxmm + kAmigaMxmmHeaderSize);
+	if (chunk0Size == 0 || kAmigaMxmmMinSize + chunk0Size > mxmmSize)
 		return false;
 
-	const byte *chunk0 = mxmm + 14;
+	const byte *chunk0 = mxmm + kAmigaMxmmMinSize;
 	Common::Array<byte> screen;
 	if (READ_BE_UINT32(chunk0) == MKTAG('P', 'P', '2', '0')) {
 		if (!decompressPp20ToBuffer(chunk0, chunk0Size, screen))
@@ -462,31 +448,31 @@ bool decodeAmigaMxmmSceneBackground(const byte *mxmm, uint32 mxmmSize,
 	}
 
 	const byte *copper = screen.data() + kAmigaSceneCopperOffset;
-	uint16 base16[16];
+	uint16 base16[kAmigaSceneCopperColorCount];
 	for (uint i = 0; i < ARRAYSIZE(base16); i++)
 		base16[i] = READ_BE_UINT16(copper + i * 2);
-	const byte *lineColors = copper + 0x20; // 200 x 16 x u16BE
-	auto buildPal32 = [&](uint16 y, byte pal32[32][3]) {
+	const byte *lineColors = copper + kAmigaSceneCopperBaseBytes;
+	auto buildPal32 = [&](uint16 y, byte pal32[kAmigaColorRegisterCount][3]) {
 		amiga12ToRgb8(base16[0], pal32[0][0], pal32[0][1], pal32[0][2]);
-		for (uint i = 0; i < 16; i++) {
-			const uint16 c = READ_BE_UINT16(lineColors + (uint32)y * 32 + i * 2);
+		for (uint i = 0; i < kAmigaSceneCopperColorCount; i++) {
+			const uint16 c = READ_BE_UINT16(lineColors + (uint32)y * kAmigaSceneCopperLineBytes + i * 2);
 			amiga12ToRgb8(c, pal32[1 + i][0], pal32[1 + i][1], pal32[1 + i][2]);
 		}
-		for (uint i = 1; i < 16; i++)
-			amiga12ToRgb8(base16[i], pal32[16 + i][0], pal32[16 + i][1], pal32[16 + i][2]);
+		for (uint i = 1; i < kAmigaSceneCopperColorCount; i++)
+			amiga12ToRgb8(base16[i], pal32[kAmigaSceneCopperColorCount + i][0], pal32[kAmigaSceneCopperColorCount + i][1], pal32[kAmigaSceneCopperColorCount + i][2]);
 	};
 
 	// Reserve 0..31 for Amiga COLOR registers (sprites) and 32..63 for EHB.
-	byte staticPal[32][3];
+	byte staticPal[kAmigaColorRegisterCount][3];
 	buildPal32(0, staticPal);
-	for (uint i = 0; i < 32; i++)
+	for (uint i = 0; i < kAmigaColorRegisterCount; i++)
 		outPalette.set(i, staticPal[i][0], staticPal[i][1], staticPal[i][2]);
-	for (uint i = 0; i < 32; i++)
-		outPalette.set(32 + i, (byte)(staticPal[i][0] / 2), (byte)(staticPal[i][1] / 2), (byte)(staticPal[i][2] / 2));
-	outColorCount = 64;
+	for (uint i = 0; i < kAmigaColorRegisterCount; i++)
+		outPalette.set(kAmigaColorRegisterCount + i, (byte)(staticPal[i][0] / 2), (byte)(staticPal[i][1] / 2), (byte)(staticPal[i][2] / 2));
+	outColorCount = kAmigaEhbPaletteCount;
 
 	Common::HashMap<uint32, byte> colorToIndex;
-	for (uint i = 0; i < 64; i++) {
+	for (uint i = 0; i < kAmigaEhbPaletteCount; i++) {
 		byte r, g, b;
 		outPalette.get(i, r, g, b);
 		const uint32 key = ((uint32)r << 16) | ((uint32)g << 8) | b;
@@ -496,14 +482,14 @@ bool decodeAmigaMxmmSceneBackground(const byte *mxmm, uint32 mxmmSize,
 
 	outPixels.resize((uint)kAmigaSceneWidth * kAmigaSceneHeight);
 	for (uint16 y = 0; y < kAmigaSceneHeight; y++) {
-		byte pal32[32][3];
+		byte pal32[kAmigaColorRegisterCount][3];
 		buildPal32(y, pal32);
 
 		for (uint16 x = 0; x < kAmigaSceneWidth; x++) {
 			byte idx = planarIndex[(uint32)y * kAmigaSceneWidth + x];
 			byte r, g, b;
-			if (idx >= 32) {
-				const byte base = (byte)(idx - 32);
+			if (idx >= kAmigaColorRegisterCount) {
+				const byte base = (byte)(idx - kAmigaColorRegisterCount);
 				r = (byte)(pal32[base][0] / 2);
 				g = (byte)(pal32[base][1] / 2);
 				b = (byte)(pal32[base][2] / 2);
@@ -523,7 +509,7 @@ bool decodeAmigaMxmmSceneBackground(const byte *mxmm, uint32 mxmmSize,
 				outPalette.set(outIdx, r, g, b);
 				outColorCount++;
 			} else {
-				outIdx = idx < 64 ? idx : (byte)(idx & 31);
+				outIdx = idx < kAmigaEhbPaletteCount ? idx : (byte)(idx & (kAmigaColorRegisterCount - 1));
 			}
 			outPixels[(uint32)y * kAmigaSceneWidth + x] = outIdx;
 		}
@@ -532,27 +518,59 @@ bool decodeAmigaMxmmSceneBackground(const byte *mxmm, uint32 mxmmSize,
 	return outColorCount > 0;
 }
 
-static bool amigaMxmmIterChunks(const byte *mxmm, uint32 mxmmSize, uint32 &outTrailerOff,
-								Common::Array<const byte *> *outChunkPtrs,
-								Common::Array<uint32> *outChunkSizes) {
-	outTrailerOff = 0;
-	if (!mxmm || mxmmSize < 14 || READ_BE_UINT32(mxmm) != MKTAG('M', 'X', 'M', 'M'))
-		return false;
-
-	uint32 pos = 10;
-	while (pos + 4 <= mxmmSize) {
-		const uint32 chunkSize = READ_BE_UINT32(mxmm + pos);
-		if (chunkSize == 0)
-			break;
-		if (pos + 4 + chunkSize > mxmmSize)
-			break;
-		if (outChunkPtrs && outChunkSizes) {
-			outChunkPtrs->push_back(mxmm + pos + 4);
-			outChunkSizes->push_back(chunkSize);
-		}
-		pos += 4 + chunkSize;
+// load_scene_mxmm sequential slots after the 10-byte MXMM header.
+enum AmigaMxmmSlot {
+	kAmigaMxmmSlotBg = 0,
+	kAmigaMxmmSlotMap0 = 1,
+	kAmigaMxmmSlotMap1 = 2,
+	kAmigaMxmmSlotMap2 = 3,
+	kAmigaMxmmSlotMxaa = 4,
+	kAmigaMxmmSlotCount = 5
+};
+
+struct AmigaMxmmLayout {
+	const byte *slot[kAmigaMxmmSlotCount];
+	uint32 slotSize[kAmigaMxmmSlotCount];
+	const byte *script;
+	uint32 scriptSize;
+	const byte *extra;
+	uint32 extraSize;
+	uint32 tableOff;
+};
+
+static bool amigaMxmmReadSizedBlob(const byte *mxmm, uint32 mxmmSize, uint32 &pos,
+								   const byte *&outPtr, uint32 &outSize) {
+	outPtr = nullptr;
+	outSize = 0;
+	if (pos + 4 > mxmmSize)
+		return false;
+	const int32 sz = (int32)READ_BE_UINT32(mxmm + pos);
+	pos += 4;
+	if (sz < 1)
+		return true;
+	if (pos + (uint32)sz > mxmmSize)
+		return false;
+	outPtr = mxmm + pos;
+	outSize = (uint32)sz;
+	pos += (uint32)sz;
+	return true;
+}
+
+static bool parseAmigaMxmmLayout(const byte *mxmm, uint32 mxmmSize, AmigaMxmmLayout &out) {
+	out = AmigaMxmmLayout();
+	if (!mxmm || mxmmSize < kAmigaMxmmMinSize || READ_BE_UINT32(mxmm) != MKTAG('M', 'X', 'M', 'M'))
+		return false;
+
+	uint32 pos = kAmigaMxmmHeaderSize;
+	for (int i = 0; i < kAmigaMxmmSlotCount; i++) {
+		if (!amigaMxmmReadSizedBlob(mxmm, mxmmSize, pos, out.slot[i], out.slotSize[i]))
+			return false;
 	}
-	outTrailerOff = pos;
+	if (!amigaMxmmReadSizedBlob(mxmm, mxmmSize, pos, out.script, out.scriptSize))
+		return false;
+	if (!amigaMxmmReadSizedBlob(mxmm, mxmmSize, pos, out.extra, out.extraSize))
+		return false;
+	out.tableOff = pos;
 	return true;
 }
 
@@ -567,40 +585,39 @@ static bool decompressAmigaChunkToBuffer(const byte *chunk, uint32 chunkSize, Co
 	return true;
 }
 
-/**
- * Advance past MXMM size-prefixed chunks and the trailer script / optional extra
- * blob so pos sits at the pathfinding table preamble (native load_scene_mxmm
- * after bindLoadedScriptBlobToSlot @ 00221d90).
- */
-static bool amigaMxmmSeekSceneTables(const byte *mxmm, uint32 mxmmSize, uint32 &outPos) {
-	outPos = 0;
-	uint32 pos = 0;
-	if (!amigaMxmmIterChunks(mxmm, mxmmSize, pos, nullptr, nullptr))
-		return false;
+static bool amigaMxmmDecodeLengthPrefixedBlob(const byte *payload, uint32 payloadSize,
+											 Common::Array<byte> &out) {
+	out.clear();
+	if (!payload || payloadSize == 0)
+		return true;
 
-	// Empty MXAA (and any other leading zero size words) before the script payload.
-	while (pos + 4 <= mxmmSize && READ_BE_UINT32(mxmm + pos) == 0)
-		pos += 4;
-	if (pos + 4 > mxmmSize)
+	Common::Array<byte> blob;
+	if (payloadSize >= 4 && READ_BE_UINT32(payload) == MKTAG('P', 'P', '2', '0')) {
+		if (!decompressPp20ToBuffer(payload, payloadSize, blob))
+			return false;
+	} else {
+		blob.resize(payloadSize);
+		memcpy(blob.data(), payload, payloadSize);
+	}
+	if (blob.size() < 4)
 		return false;
 
-	const uint32 scriptPayloadSize = READ_BE_UINT32(mxmm + pos);
-	pos += 4;
-	if (scriptPayloadSize == 0 || pos + scriptPayloadSize > mxmmSize)
+	const uint32 innerLen = READ_BE_UINT32(blob.data());
+	if (innerLen == 0)
+		return true;
+	if (4 + innerLen > blob.size())
 		return false;
-	pos += scriptPayloadSize;
+	out.resize(innerLen);
+	memcpy(out.data(), blob.data() + 4, innerLen);
+	return true;
+}
 
-	if (pos + 4 > mxmmSize)
+static bool amigaMxmmSeekSceneTables(const byte *mxmm, uint32 mxmmSize, uint32 &outPos) {
+	outPos = 0;
+	AmigaMxmmLayout layout;
+	if (!parseAmigaMxmmLayout(mxmm, mxmmSize, layout))
 		return false;
-	const uint32 extraSize = READ_BE_UINT32(mxmm + pos);
-	pos += 4;
-	if (extraSize > 0) {
-		if (pos + extraSize > mxmmSize)
-			return false;
-		pos += extraSize;
-	}
-
-	outPos = pos;
+	outPos = layout.tableOff;
 	return true;
 }
 
@@ -610,77 +627,14 @@ bool extractAmigaMxmmSceneScript(const byte *mxmm, uint32 mxmmSize,
 	outScript.clear();
 	outStrings.clear();
 
-	uint32 trailerOff = 0;
-	if (!amigaMxmmIterChunks(mxmm, mxmmSize, trailerOff, nullptr, nullptr))
+	AmigaMxmmLayout layout;
+	if (!parseAmigaMxmmLayout(mxmm, mxmmSize, layout))
 		return false;
-	if (trailerOff >= mxmmSize)
+	if (!amigaMxmmDecodeLengthPrefixedBlob(layout.script, layout.scriptSize, outScript))
 		return false;
-
-	const byte *trailer = mxmm + trailerOff;
-	uint32 trailerSize = mxmmSize - trailerOff;
-	uint32 pos = 0;
-
-	// Skip leading empty u32 markers (optional trailing chunk sizes of 0).
-	while (pos + 8 <= trailerSize && READ_BE_UINT32(trailer + pos) == 0)
-		pos += 4;
-
-	if (pos + 8 > trailerSize)
+	if (layout.extraSize != 0 &&
+		!amigaMxmmDecodeLengthPrefixedBlob(layout.extra, layout.extraSize, outStrings))
 		return false;
-
-	// Native reads one size-prefixed payload; the first u32 inside is often an
-	// inner bytecode length (e.g. 690) with the remaining bytes as script body.
-	const uint32 scriptPayloadSize = READ_BE_UINT32(trailer + pos);
-	pos += 4;
-	if (scriptPayloadSize == 0 || pos + scriptPayloadSize > trailerSize)
-		return false;
-
-	const byte *payload = trailer + pos;
-	pos += scriptPayloadSize;
-
-	uint32 scriptSize = scriptPayloadSize;
-	const byte *script = payload;
-	if (scriptPayloadSize >= 6) {
-		const uint32 innerLen = READ_BE_UINT32(payload);
-		if (innerLen >= 2 && innerLen + 4 <= scriptPayloadSize &&
-			(innerLen + 4 == scriptPayloadSize || innerLen + 4 + 1 == scriptPayloadSize)) {
-			script = payload + 4;
-			scriptSize = innerLen;
-		}
-	}
-
-	// Accept empty/tiny stubs (e.g. single 0x18) and real scene scripts.
-	if (scriptSize >= 2) {
-		const byte op = script[0];
-		const byte len = script[1];
-		if (len > 32 || (uint32)2 + len > scriptSize)
-			return false;
-		if (op != 0x04 && op != 0x05 && op != 0x01 && op != 0x0F && op != 0x0C && op != 0x18 && op != 0x07)
-			return false;
-	}
-
-	outScript.resize(scriptSize);
-	memcpy(outScript.data(), script, scriptSize);
-
-	// Optional extra blob: outer u32 size, payload often starts with an inner
-	// u32 string-bytes length (Ghidra load_scene_mxmm stage 0x19).
-	if (pos + 4 <= trailerSize) {
-		const uint32 extraSize = READ_BE_UINT32(trailer + pos);
-		if (extraSize > 0 && pos + 4 + extraSize <= trailerSize) {
-			const byte *extra = trailer + pos + 4;
-			uint32 stringBytes = extraSize;
-			const byte *stringData = extra;
-			if (extraSize >= 4) {
-				const uint32 inner = READ_BE_UINT32(extra);
-				if (inner > 0 && inner + 4 <= extraSize) {
-					stringData = extra + 4;
-					stringBytes = inner;
-				}
-			}
-			outStrings.resize(stringBytes);
-			memcpy(outStrings.data(), stringData, stringBytes);
-		}
-	}
-
 	return !outScript.empty();
 }
 
@@ -688,11 +642,9 @@ static bool amigaMxmmGetTrailerTableBase(const byte *mxmm, uint32 mxmmSize, uint
 	outPos = 0;
 	if (!amigaMxmmSeekSceneTables(mxmm, mxmmSize, outPos))
 		return false;
-	// Native @ 00221d90: skip u16, then 0xA2 pathfinding, skip u16, 0x22 hotspot, 0xA walk.
-	const uint32 kTableBytes = 2 + 0xA2 + 2 + 0x22 + 10;
-	if (outPos + kTableBytes > mxmmSize)
+	if (outPos + kAmigaMxmmTrailerTablesSize > mxmmSize)
 		return false;
-	outPos += 2; // preamble u16
+	outPos += kAmigaMxmmTrailerPreambleSize;
 	return true;
 }
 
@@ -708,23 +660,20 @@ bool extractAmigaMxmmScenePathfinding(const byte *mxmm, uint32 mxmmSize,
 
 	const byte *table = mxmm + pos;
 	outNumPoints = READ_BE_UINT16(table);
-	if (outNumPoints > 16)
-		outNumPoints = 16;
+	if (outNumPoints > kAmigaMxmmPathfindingNodeCount)
+		outNumPoints = kAmigaMxmmPathfindingNodeCount;
 
-	outNodes.resize(16);
-	for (uint i = 0; i < 16; i++) {
-		const byte *node = table + 2 + i * 10;
+	outNodes.resize(kAmigaMxmmPathfindingNodeCount);
+	for (uint i = 0; i < kAmigaMxmmPathfindingNodeCount; i++) {
+		const byte *node = table + kAmigaMxmmWordSize + i * kAmigaMxmmPathfindingNodeSize;
 		AmigaPathfindingNode &dst = outNodes[i];
-		dst.x = READ_BE_UINT16(node + 0);
-		dst.y = READ_BE_UINT16(node + 2);
-		dst.adjacent[0] = node[4];
-		dst.adjacent[1] = node[5];
-		dst.adjacent[2] = node[6];
-		dst.adjacent[3] = node[7];
-		// DOS/Amiga: last word is connection count (not padding). BE on Amiga.
-		dst.numConnections = READ_BE_UINT16(node + 8);
-		if (dst.numConnections > 4)
-			dst.numConnections = 4;
+		dst.x = READ_BE_UINT16(node + kAmigaMxmmPathfindingNodeXOffset);
+		dst.y = READ_BE_UINT16(node + kAmigaMxmmPathfindingNodeYOffset);
+		for (uint a = 0; a < kAmigaMxmmPathfindingMaxAdj; a++)
+			dst.adjacent[a] = node[kAmigaMxmmPathfindingNodeAdjOffset + a];
+		dst.numConnections = READ_BE_UINT16(node + kAmigaMxmmPathfindingNodeConnOffset);
+		if (dst.numConnections > kAmigaMxmmPathfindingMaxAdj)
+			dst.numConnections = kAmigaMxmmPathfindingMaxAdj;
 	}
 	return true;
 }
@@ -745,14 +694,13 @@ bool extractAmigaMxmmSceneWalkParams(const byte *mxmm, uint32 mxmmSize,
 	if (!amigaMxmmGetTrailerTableBase(mxmm, mxmmSize, pos))
 		return false;
 
-	// Skip pathfinding (0xA2) + preamble u16 + hotspot (0x22).
-	pos += 0xA2 + 2 + 0x22;
+	pos += kAmigaMxmmPathfindingToWalkOffset;
 
-	outWalkDepthThresholdY = READ_BE_UINT16(mxmm + pos + 0);
-	outWalkDepthScaleFactor = READ_BE_UINT16(mxmm + pos + 2);
-	outWalkBaseSpeedPct = READ_BE_UINT16(mxmm + pos + 4);
-	outScenePaletteMode = READ_BE_UINT16(mxmm + pos + 6);
-	outPaletteDarkenPercent = READ_BE_UINT16(mxmm + pos + 8);
+	outWalkDepthThresholdY = READ_BE_UINT16(mxmm + pos + kAmigaMxmmWalkDepthThresholdYOffset);
+	outWalkDepthScaleFactor = READ_BE_UINT16(mxmm + pos + kAmigaMxmmWalkDepthScaleFactorOffset);
+	outWalkBaseSpeedPct = READ_BE_UINT16(mxmm + pos + kAmigaMxmmWalkBaseSpeedPctOffset);
+	outScenePaletteMode = READ_BE_UINT16(mxmm + pos + kAmigaMxmmWalkScenePaletteModeOffset);
+	outPaletteDarkenPercent = READ_BE_UINT16(mxmm + pos + kAmigaMxmmWalkPaletteDarkenPercentOffset);
 	return true;
 }
 
@@ -764,46 +712,27 @@ bool extractAmigaMxmmSceneMaps(const byte *mxmm, uint32 mxmmSize,
 	outDepth.clear();
 	outShadow.clear();
 
-	uint32 trailerOff = 0;
-	Common::Array<const byte *> chunkPtrs;
-	Common::Array<uint32> chunkSizes;
-	if (!amigaMxmmIterChunks(mxmm, mxmmSize, trailerOff, &chunkPtrs, &chunkSizes))
-		return false;
-	if (chunkPtrs.size() < 2)
+	AmigaMxmmLayout layout;
+	if (!parseAmigaMxmmLayout(mxmm, mxmmSize, layout))
 		return false;
 
 	const uint32 kMapBytes = (uint32)kAmigaSceneWidth * kAmigaSceneHeight; // 64000
-	Common::Array<byte> maps[3];
-	uint mapCount = 0;
-
-	// Chunk0 is the planar screen; subsequent 64000-byte buffers are maps.
-	for (uint i = 1; i < chunkPtrs.size() && mapCount < 3; i++) {
+	auto decodeMap = [&](int slot, Common::Array<byte> &dest) -> bool {
+		if (layout.slotSize[slot] == 0)
+			return false;
 		Common::Array<byte> decoded;
-		if (!decompressAmigaChunkToBuffer(chunkPtrs[i], chunkSizes[i], decoded))
-			continue;
+		if (!decompressAmigaChunkToBuffer(layout.slot[slot], layout.slotSize[slot], decoded))
+			return false;
 		if (decoded.size() != kMapBytes)
-			continue;
-		maps[mapCount++] = Common::move(decoded);
-	}
+			return false;
+		dest = Common::move(decoded);
+		return true;
+	};
 
-	if (mapCount == 0)
-		return false;
-
-	// Native load_scene_mxmm order (NOT DOS MCS order):
-	//   Map0 -> depth / draw mask (g_pSceneMap0Buffer; buildDepthMaskFromMap0)
-	//   Map1 -> walkability (g_pSceneMap1Buffer; getWalkabilityAt)
-	//   Map2 -> MXCC hotspot RLE (not a 64000 surface; see extractAmigaMxmmMxccHotspotMap)
-	// DOS MCS is depth, pathfinding, shadow, hotspot. Swapping Map0/Map1 into
-	// ScummVM's pathfinding slot made MM_0004 treat the depth mask as walk
-	// blockers (255 walls), so off-screen walkTo cancelled and waitForWalk
-	// completed early. Map1 on that demo is all zeros (= open, like DOS scene 5).
-	if (mapCount >= 1)
-		outDepth = Common::move(maps[0]);
-	if (mapCount >= 2)
-		outPathfinding = Common::move(maps[1]);
-	if (mapCount >= 3)
-		outShadow = Common::move(maps[2]);
-	return true;
+	const bool gotDepth = decodeMap(kAmigaMxmmSlotMap0, outDepth);
+	const bool gotPath = decodeMap(kAmigaMxmmSlotMap1, outPathfinding);
+	// Map2 is MXCC, not a 64000 shadow surface.
+	return gotDepth || gotPath;
 }
 
 bool extractAmigaMxmmSceneHotspotColors(const byte *mxmm, uint32 mxmmSize,
@@ -816,19 +745,16 @@ bool extractAmigaMxmmSceneHotspotColors(const byte *mxmm, uint32 mxmmSize,
 	if (!amigaMxmmGetTrailerTableBase(mxmm, mxmmSize, pos))
 		return false;
 
-	// Skip pathfinding (0xA2) + preamble u16 before hotspot table.
-	pos += 0xA2 + 2;
-	if (pos + 0x22 > mxmmSize)
+	pos += kAmigaMxmmPathfindingToHotspotOffset;
+	if (pos + kAmigaMxmmHotspotTableSize > mxmmSize)
 		return false;
 
 	outNumHotspots = READ_BE_UINT16(mxmm + pos);
-	if (outNumHotspots > 16)
-		outNumHotspots = 16;
+	if (outNumHotspots > kAmigaMxmmPathfindingNodeCount)
+		outNumHotspots = kAmigaMxmmPathfindingNodeCount;
 
-	// Same 0x20 byte layout as DOS (f9 00 fe 00 ...). On LE hosts, memcpy into
-	// uint16 yields color in the low byte - matches getHotspotAtPoint.
-	outColorTable.resize(0x20 / sizeof(uint16));
-	memcpy(outColorTable.data(), mxmm + pos + 2, 0x20);
+	outColorTable.resize(kAmigaMxmmHotspotColorBytes / sizeof(uint16));
+	memcpy(outColorTable.data(), mxmm + pos + kAmigaMxmmWordSize, kAmigaMxmmHotspotColorBytes);
 	return true;
 }
 
@@ -836,59 +762,44 @@ bool extractAmigaMxmmMxccHotspotMap(const byte *mxmm, uint32 mxmmSize,
 									Common::Array<byte> &outHotspotMap) {
 	outHotspotMap.clear();
 
-	uint32 trailerOff = 0;
-	Common::Array<const byte *> chunkPtrs;
-	Common::Array<uint32> chunkSizes;
-	if (!amigaMxmmIterChunks(mxmm, mxmmSize, trailerOff, &chunkPtrs, &chunkSizes))
+	AmigaMxmmLayout layout;
+	if (!parseAmigaMxmmLayout(mxmm, mxmmSize, layout))
 		return false;
 
-	const byte *mxcc = nullptr;
-	uint32 mxccSize = 0;
-	for (uint i = 0; i < chunkPtrs.size(); i++) {
-		if (chunkSizes[i] >= 0x19A + 320 &&
-			READ_BE_UINT32(chunkPtrs[i]) == MKTAG('M', 'X', 'C', 'C')) {
-			mxcc = chunkPtrs[i];
-			mxccSize = chunkSizes[i];
-			break;
-		}
-	}
-	if (!mxcc)
+	const byte *mxcc = layout.slot[kAmigaMxmmSlotMap2];
+	const uint32 mxccSize = layout.slotSize[kAmigaMxmmSlotMap2];
+	if (!mxcc || mxccSize < kAmigaMxccRowDataOffset + kAmigaSceneWidth ||
+		READ_BE_UINT32(mxcc) != MKTAG('M', 'X', 'C', 'C'))
 		return false;
 
-	// Header: MXCC + ver u16BE + pad u16BE + payloadSize u16BE (Ghidra Map2).
-	if (READ_BE_UINT16(mxcc + 4) != 1)
+	if (READ_BE_UINT16(mxcc + kAmigaMxccVersionOffset) != 1)
 		return false;
 
-	const byte marker = mxcc[5];
-	const uint32 kRowTableOff = 0x0A;
-	const uint32 kRowDataBase = 0x19A;
-	const uint kRows = 200;
-	const uint kWidth = 320;
-	if (kRowTableOff + kRows * 2 > mxccSize || kRowDataBase > mxccSize)
+	const byte marker = mxcc[kAmigaMxccMarkerOffset];
+	if (kAmigaMxccRowDataOffset > mxccSize)
 		return false;
 
-	outHotspotMap.resize(kWidth * kRows);
+	outHotspotMap.resize((uint)kAmigaSceneWidth * kAmigaSceneHeight);
 	Common::fill(outHotspotMap.begin(), outHotspotMap.end(), 0);
 
-	for (uint y = 0; y < kRows; y++) {
-		// decodeMxccRunLengthAt: y==0 uses offset 0; else BE word at +0x0A+(y-1)*2.
+	for (uint y = 0; y < kAmigaSceneHeight; y++) {
 		uint32 rowOff = 0;
 		if (y > 0)
-			rowOff = READ_BE_UINT16(mxcc + kRowTableOff + (y - 1) * 2);
+			rowOff = READ_BE_UINT16(mxcc + kAmigaMxccRowTableOffset + (y - 1) * 2);
 
-		uint32 p = kRowDataBase + rowOff;
+		uint32 p = kAmigaMxccRowDataOffset + rowOff;
 		uint x = 0;
-		while (x < kWidth && p < mxccSize) {
+		while (x < kAmigaSceneWidth && p < mxccSize) {
 			const byte b = mxcc[p++];
 			if (b == marker) {
 				if (p + 1 >= mxccSize)
 					break;
 				const byte color = mxcc[p++];
 				const byte run = mxcc[p++];
-				for (uint r = 0; r < run && x < kWidth; r++)
-					outHotspotMap[y * kWidth + x++] = color;
+				for (uint r = 0; r < run && x < kAmigaSceneWidth; r++)
+					outHotspotMap[y * kAmigaSceneWidth + x++] = color;
 			} else {
-				outHotspotMap[y * kWidth + x++] = b;
+				outHotspotMap[y * kAmigaSceneWidth + x++] = b;
 			}
 		}
 	}
@@ -896,16 +807,13 @@ bool extractAmigaMxmmMxccHotspotMap(const byte *mxmm, uint32 mxmmSize,
 }
 
 bool amigaMxmmHasMxaaOverlay(const byte *mxmm, uint32 mxmmSize) {
-	uint32 pos = 0;
-	if (!amigaMxmmIterChunks(mxmm, mxmmSize, pos, nullptr, nullptr))
-		return false;
-	// Native: immediately after Map2 comes the MXAA size word (0 = absent).
-	if (pos + 4 > mxmmSize)
+	AmigaMxmmLayout layout;
+	if (!parseAmigaMxmmLayout(mxmm, mxmmSize, layout))
 		return false;
-	const uint32 mxaaSize = READ_BE_UINT32(mxmm + pos);
-	if (mxaaSize == 0 || pos + 4 + mxaaSize > mxmmSize)
+	if (layout.slotSize[kAmigaMxmmSlotMxaa] == 0)
 		return false;
-	const byte *blob = mxmm + pos + 4;
+	const byte *blob = layout.slot[kAmigaMxmmSlotMxaa];
+	const uint32 mxaaSize = layout.slotSize[kAmigaMxmmSlotMxaa];
 	if (mxaaSize >= 4 && READ_BE_UINT32(blob) == MKTAG('M', 'X', 'A', 'A'))
 		return true;
 	if (mxaaSize >= 4 && READ_BE_UINT32(blob) == MKTAG('P', 'P', '2', '0')) {
@@ -930,14 +838,14 @@ bool decodeAmigaMxffFont(const byte *mxff, uint32 mxffSize, Common::Array<AmigaM
 	const uint16 atlasRows = READ_BE_UINT16(mxff + 8);
 	const uint16 atlasWidthPixels = READ_BE_UINT16(mxff + 0xA);
 	const uint16 planes = READ_BE_UINT16(mxff + 0xC);
-	if (blitHeight == 0 || atlasRows == 0 || atlasWidthPixels < 8 || planes != 6)
+	if (blitHeight == 0 || atlasRows == 0 || atlasWidthPixels < 8 || planes != kAmigaScenePlanes)
 		return false;
 
 	const uint32 rowBytes = (uint32)(atlasWidthPixels >> 3);
 	if (rowBytes == 0)
 		return false;
 	const uint32 planeBytes = rowBytes * atlasRows;
-	const uint32 atlasBytes = planeBytes * 6;
+	const uint32 atlasBytes = planeBytes * kAmigaScenePlanes;
 	const uint32 atlasOff = 0x10A;
 	if (atlasOff + atlasBytes > mxffSize)
 		return false;
@@ -946,14 +854,13 @@ bool decodeAmigaMxffFont(const byte *mxff, uint32 mxffSize, Common::Array<AmigaM
 	const byte *widths = mxff + 0x8C;
 	const byte *atlas = mxff + atlasOff;
 	const uint16 glyphHeight = MIN(blitHeight, atlasRows);
-	const uint16 cellWidth = 16; // drawText: glyphIndex * (height>>3) byte offset -> 16px cells
+	const uint16 cellWidth = 16;
 
-	// chars 0x20..0x20+0x7D (0x7E entries)
 	for (uint i = 0; i < 0x7E; i++) {
 		const byte ascii = (byte)(0x20 + i);
 		const byte glyphIndex = charmap[i];
 		const byte advance = widths[i];
-		// 0x4E in the map is unused/empty (Ghidra charmap filler).
+		// 0x4E in the map is unused/empty
 		if (glyphIndex == 0x4E || advance == 0)
 			continue;
 
@@ -974,7 +881,7 @@ bool decodeAmigaMxffFont(const byte *mxff, uint32 mxffSize, Common::Array<AmigaM
 				byte color = 0;
 				const uint32 bit = absX & 7;
 				const uint32 byteInRow = absX >> 3;
-				for (uint16 plane = 0; plane < 6; plane++) {
+				for (uint16 plane = 0; plane < kAmigaScenePlanes; plane++) {
 					const byte *planeRow = atlas + plane * planeBytes + (uint32)y * rowBytes;
 					if (planeRow[byteInRow] & (0x80 >> bit))
 						color |= (byte)(1 << plane);
@@ -996,13 +903,10 @@ bool extractAmigaMxosPcm(const byte *mxos, uint32 mxosSize, Common::Array<byte>
 	if (READ_BE_UINT32(mxos) != MKTAG('M', 'X', 'O', 'S'))
 		return false;
 
-	// Demo MXOS: word at +0x10 is the byte offset of the first sample block.
 	const uint16 sampleOff = READ_BE_UINT16(mxos + 0x10);
 	if (sampleOff < 0x14 || sampleOff >= mxosSize)
 		return false;
 
-	// Optional Paula period near the sample header (word after offset on several demos).
-	// period 0 -> keep 8000 Hz. NTSC color clock / period ~= Paula playback rate.
 	if ((uint32)sampleOff + 4 <= mxosSize) {
 		const uint16 period = READ_BE_UINT16(mxos + 0x12);
 		if (period >= 0x50 && period <= 0x400) {
@@ -1017,7 +921,6 @@ bool extractAmigaMxosPcm(const byte *mxos, uint32 mxosSize, Common::Array<byte>
 		return false;
 
 	outPcm.resize(pcmBytes);
-	// Paula samples are signed 8-bit; MacsAudioStream expects unsigned (value-128)*256.
 	for (uint32 i = 0; i < pcmBytes; i++)
 		outPcm[i] = (byte)((int8)mxos[sampleOff + i] + 128);
 
diff --git a/engines/macs2/amiga_decode.h b/engines/macs2/amiga_decode.h
index a4de2cc5571..1f3d6da4943 100644
--- a/engines/macs2/amiga_decode.h
+++ b/engines/macs2/amiga_decode.h
@@ -28,19 +28,6 @@
 
 namespace Macs2 {
 
-/**
- * Amiga MXOO object body layout (after the 12-byte MXOO header):
- *   +0x00..0x0B  padding
- *   +0x0C        signature 0x0101
- *   +0x0E        21 x uint32BE slot offsets (0 / 0xFFFFFFFF = empty)
- *   +0x62        uint32BE offset of extra/portrait section (often end of anims)
- *
- * Each anim slot is planar 6-plane frame data with a short BE header.
- * Script bytecode is identical to DOS (LE operands). Strings are plaintext
- * with uint16BE length prefixes (no XOR cipher).
- *
- * Game object index = Amiga OO resource id + 1 (OO_0000 -> object 1).
- */
 struct AmigaMxooInfo {
 	uint32 scriptOffset = 0;
 	uint32 stringOffset = 0;
@@ -71,103 +58,74 @@ struct AmigaAnimSlotInfo {
 bool parseAmigaMxoo(const byte *mxoo, uint32 mxooSize, AmigaMxooInfo &out);
 bool inspectAmigaAnimSlot(const byte *mxoo, uint32 mxooSize, uint32 bodyRelativeOffset, AmigaAnimSlotInfo &out);
 void amiga12ToVga6(uint16 rgb, byte &r6, byte &g6, byte &b6);
-
-/** Decode one frame of planar Amiga anim data to chunky 8bpp (color planes 0..4). */
 bool decodeAmigaPlanarFrame(const byte *planar, uint16 width, uint16 height, uint16 frameIndex,
 							uint16 frameCount, Common::Array<byte> &outPixels);
-
-/**
- * Convert an Amiga anim slot into a DOS-compatible animation blob so the
- * existing AnimBlobView / renderer path can consume it.
- */
 bool convertAmigaAnimSlotToDosBlob(const byte *mxoo, uint32 mxooSize, uint32 bodyRelativeOffset,
 								   Common::Array<byte> &outBlob);
-
-/** Extract script bytecode (without the MXOO script section header). */
 bool extractAmigaScript(const byte *mxoo, uint32 mxooSize, Common::Array<byte> &outScript);
-
-/**
- * Extract Amiga string entries (u16BE length + plaintext), skipping the
- * MXOO string-section header. Offsets used by scripts are relative to this block.
- */
 bool extractAmigaStringBlock(const byte *mxoo, uint32 mxooSize, Common::Array<byte> &outStrings);
-
-/** Convert a simple planar MXOO sprite (cursor/icon) into a 1-frame DOS anim blob. */
 bool convertAmigaSimpleSpriteToDosBlob(const byte *mxoo, uint32 mxooSize, Common::Array<byte> &outBlob);
-
-/**
- * Convert Amiga dialogue portrait atlas (body+0x62) into a DOS anim blob.
- * Layout: 240x80x6 separated planar = three 80x80 faces (D5=0xF0, D3/D4/D6=0x50,
- * 6 plane blit in animateDialoguePortrait @ 0022f79c). Color from planes 0..4.
- * Pixels keep Amiga COLOR indices 0..31 (playfield copper); demo portraits use
- * only COLOR17..31 from the copper high bank / MXIN chrome ramp.
- */
 bool convertAmigaPortraitAtlasToDosBlob(const byte *mxoo, uint32 mxooSize, uint32 bodyRelativeExtraOffset,
 										Common::Array<byte> &outBlob);
 
-/** Decompressed MXMM chunk0 screen buffer size (6x8000 planes + copper block). */
 enum : uint32 {
-	kAmigaSceneScreenSize = 54432, // 0xD4A0
-	kAmigaSceneCopperOffset = 0xBB80,
-	kAmigaSceneCopperSize = 0x1920, // 16 base colors + 200x16 line colors
-	kAmigaScenePlaneBytes = 8000 // 40x200
-};
-enum : uint16 {
 	kAmigaSceneWidth = 320,
 	kAmigaSceneHeight = 200,
-	kAmigaScenePlanes = 6 // BPLCON0 = 0x6200 -> EHB
+	kAmigaScenePlanes = 6,
+	kAmigaSceneScreenSize = 54432,
+	kAmigaSceneCopperOffset = 0xBB80,
+	kAmigaSceneCopperSize = 0x1920, // 16 base colors + 200x16 line colors
+	kAmigaSceneCopperColorCount = 16,
+	kAmigaSceneCopperLineBytes = kAmigaSceneCopperColorCount * 2, // 16 x u16BE
+	kAmigaSceneCopperBaseBytes = kAmigaSceneCopperLineBytes, // static COLOR0..15 before per-line colors
+	kAmigaScenePlaneBytes = 8000, // 40x200
+	kAmigaColorRegisterCount = 32,
+	kAmigaEhbPaletteCount = 64,
+
+	/** MXMM + version u16 + pad u16 + scene id u16. */
+	kAmigaMxmmHeaderSize = 10,
+	kAmigaMxmmMinSize = kAmigaMxmmHeaderSize + 4,
+
+	kAmigaMxmmWordSize = 2, // u16BE count / preamble
+	kAmigaMxmmTrailerPreambleSize = kAmigaMxmmWordSize,
+	kAmigaMxmmPathfindingNodeCount = 16,
+	kAmigaMxmmPathfindingNodeSize = 10,
+	kAmigaMxmmPathfindingMaxAdj = 4,
+	kAmigaMxmmPathfindingNodeXOffset = 0,
+	kAmigaMxmmPathfindingNodeYOffset = 2,
+	kAmigaMxmmPathfindingNodeAdjOffset = 4,
+	kAmigaMxmmPathfindingNodeConnOffset = 8,
+	kAmigaMxmmPathfindingTableSize = kAmigaMxmmWordSize +
+									 kAmigaMxmmPathfindingNodeCount * kAmigaMxmmPathfindingNodeSize, // 0xA2
+	kAmigaMxmmHotspotColorBytes = 0x20,
+	kAmigaMxmmHotspotTableSize = kAmigaMxmmWordSize + kAmigaMxmmHotspotColorBytes, // 0x22
+	kAmigaMxmmWalkPaletteSize = 10, // 5 x u16BE
+	kAmigaMxmmWalkDepthThresholdYOffset = 0,
+	kAmigaMxmmWalkDepthScaleFactorOffset = 2,
+	kAmigaMxmmWalkBaseSpeedPctOffset = 4,
+	kAmigaMxmmWalkScenePaletteModeOffset = 6,
+	kAmigaMxmmWalkPaletteDarkenPercentOffset = 8,
+	kAmigaMxmmPathfindingToHotspotOffset = kAmigaMxmmPathfindingTableSize + kAmigaMxmmTrailerPreambleSize,
+	kAmigaMxmmPathfindingToWalkOffset = kAmigaMxmmPathfindingToHotspotOffset + kAmigaMxmmHotspotTableSize,
+	kAmigaMxmmTrailerTablesSize = kAmigaMxmmTrailerPreambleSize + kAmigaMxmmPathfindingTableSize +
+								  kAmigaMxmmTrailerPreambleSize + kAmigaMxmmHotspotTableSize +
+								  kAmigaMxmmWalkPaletteSize,
+
+	kAmigaMxccVersionOffset = 4,
+	kAmigaMxccMarkerOffset = 5,
+	kAmigaMxccRowTableOffset = 0x0A,
+	kAmigaMxccRowDataOffset = kAmigaMxccRowTableOffset + kAmigaSceneHeight * 2 // 0x19A
 };
 
-/**
- * Decode MXMM scene package chunk0 into chunky 8bpp 320x200 pixels and an RGB8
- * palette (up to 256 entries).
- *
- * Palette layout (matches Amiga copper / sprite drawing):
- * - indices 0..31: COLOR00..31 from the copper base block + first scanline
- * - indices 32..63: Extra HalfBrite of 0..31 (BPLCON0 0x6200)
- * - indices 64..*: extra colors needed for per-scanline copper differences
- *
- * Character/OO sprites use planes 0..4 against COLOR00..31, so 0..31 must stay
- * stable Amiga hardware colors.
- */
 bool decodeAmigaMxmmSceneBackground(const byte *mxmm, uint32 mxmmSize,
 									Common::Array<byte> &outPixels,
 									Graphics::Palette &outPalette,
 									uint &outColorCount);
 
-/**
- * Extract scene script + string block from an MXMM package trailer.
- *
- * After the size-prefixed chunks (BG / maps / MXCC / ...), the trailer is:
- *   [optional u32 zero markers]
- *   u32BE scriptSize
- *   u32BE unknown (ignored)
- *   script[scriptSize]          - DOS-identical LE bytecode
- *   [optional] u32BE stringBytes + u16BE-length plaintext entries
- *
- * Ghidra: load_scene_mxmm reads this after planar BG / map chunks.
- * Script-visible scene ids are MM_resource_id + 1 (changeScene subtracts 1
- * before MM lookup; curScene is set to mmId+1 after load).
- */
 bool extractAmigaMxmmSceneScript(const byte *mxmm, uint32 mxmmSize,
 								 Common::Array<byte> &outScript,
 								 Common::Array<byte> &outStrings);
 
-/**
- * Extract scene walk / palette scalars from the MXMM trailer tables.
- *
- * Ghidra load_scene_mxmm @ 00221de8 reads 10 bytes after pathfinding (0xA2) and
- * hotspot (0x22) tables into DAT_002376da..e2 (five BE u16s), matching DOS
- * scene+0x51FD..0x5205:
- *   [0] walk depth threshold Y
- *   [1] walk depth scale factor
- *   [2] walk base speed percent
- *   [3] scene palette mode
- *   [4] palette darken percent
- *
- * Without these, ScummVM's DOS walkAlongPath formula collapses to 1 px/frame
- * when the Amiga native path never loads MCS scene metadata.
- */
 bool extractAmigaMxmmSceneWalkParams(const byte *mxmm, uint32 mxmmSize,
 									 uint16 &outWalkDepthThresholdY,
 									 uint16 &outWalkDepthScaleFactor,
@@ -175,73 +133,31 @@ bool extractAmigaMxmmSceneWalkParams(const byte *mxmm, uint32 mxmmSize,
 									 uint16 &outScenePaletteMode,
 									 uint16 &outPaletteDarkenPercent);
 
-/**
- * One pathfinding graph node from the MXMM trailer 0xA2 table
- * (Ghidra load_scene_mxmm @ 00221d9a -> 00248794).
- *
- * Layout (BE), matching DOS MCS 16x10-byte nodes:
- *   u16 x, u16 y, u8 adj[4], u16 unused
- * Prefixed by u16BE active node count (DOS scene+0x51F7).
- */
 struct AmigaPathfindingNode {
 	uint16 x = 0;
 	uint16 y = 0;
-	byte adjacent[4] = {0, 0, 0, 0};
+	byte adjacent[kAmigaMxmmPathfindingMaxAdj] = {};
 	uint16 numConnections = 0;
 };
 
-/**
- * Extract pathfinding node graph from the MXMM trailer (0xA2 bytes after a
- * skipped u16). Always yields 16 node slots; outNumPoints is the active count.
- * Without this, Amiga scenes keep _numPathfindingPoints==0, calculatePath fails,
- * walks cancel early, and waitForWalk completes at the stuck position.
- */
 bool extractAmigaMxmmScenePathfinding(const byte *mxmm, uint32 mxmmSize,
 									  uint16 &outNumPoints,
 									  Common::Array<AmigaPathfindingNode> &outNodes);
 
-/**
- * Decode MXMM map chunks (320x200) after chunk0.
- * Native order: Map0=depth (g_pSceneMap0Buffer), Map1=walkability
- * (g_pSceneMap1Buffer). Map2 holds MXCC (hotspot RLE), not a 64000 shadow -
- * use extractAmigaMxmmMxccHotspotMap for that. A third 64000 PP20 (rare) is
- * returned as outShadow when present.
- */
 bool extractAmigaMxmmSceneMaps(const byte *mxmm, uint32 mxmmSize,
 							   Common::Array<byte> &outPathfinding,
 							   Common::Array<byte> &outDepth,
 							   Common::Array<byte> &outShadow);
 
-/**
- * Extract hotspot color table from the MXMM trailer (0x22 bytes after pathfinding).
- * Layout matches DOS scene+0x50C3: u16BE numHotspots + 0x20 bytes color words
- * (color in the first byte of each pair / low byte once stored as uint16 on LE).
- * MM_0004: num=7, colors 249,254,250,251,253,252,248 - same as DOS scene 5.
- */
 bool extractAmigaMxmmSceneHotspotColors(const byte *mxmm, uint32 mxmmSize,
 										uint16 &outNumHotspots,
 										Common::Array<uint16> &outColorTable);
 
-/**
- * Decode MXCC chunk (Map2 / g_pSceneMap2Buffer) to a 320x200 hotspot pixel map.
- * Ghidra decodeMxccRunLengthAt @ 00233590: per-row RLE with marker at MXCC+5,
- * row offsets at +0x0A (200xu16BE), row data at +0x19A + offset.
- * MM_0004 decodes pixel-identical to DOS scene 5 hotspot RLE.
- * No separate 4th 64000 map chunk exists in the Amiga demo format.
- */
 bool extractAmigaMxmmMxccHotspotMap(const byte *mxmm, uint32 mxmmSize,
 									Common::Array<byte> &outHotspotMap);
 
-/**
- * True if MXMM has a non-empty MXAA overlay-anim blob (native tickMxaaOverlayAnims).
- * Demo MM_0004/MM_0040 have size 0 - no consumer wired yet.
- */
 bool amigaMxmmHasMxaaOverlay(const byte *mxmm, uint32 mxmmSize);
 
-/**
- * One decoded MXFF glyph (chunky 8bpp, color 0 = transparent).
- * Ghidra drawText @ 00224492: charmap at +0x0E, widths at +0x8C, atlas at +0x10A.
- */
 struct AmigaMxffGlyph {
 	byte ascii = 0;
 	uint16 width = 0;
@@ -249,22 +165,8 @@ struct AmigaMxffGlyph {
 	Common::Array<byte> pixels; // width * height
 };
 
-/**
- * Decode native Amiga MXFF font (FF_0000) into chunky glyphs for GlyphData.
- *
- * Header (BE): magic MXFF, ver=1 at +4, blit height at +6, atlas rows at +8,
- * atlas width-pixels at +0xA (row bytes = value>>3), planes=6 at +0xC.
- * Glyph index map: +0x0E (0x7E bytes for chars 0x20..).
- * Advance widths: +0x8C (0x7E bytes).
- * Planar atlas: +0x10A, rowBytes * atlasRows * 6 planes; cell X = glyphIndex * 16.
- */
 bool decodeAmigaMxffFont(const byte *mxff, uint32 mxffSize, Common::Array<AmigaMxffGlyph> &outGlyphs);
 
-/**
- * Extract unsigned 8-bit PCM (+ optional rate) from an MXOS container for MacsAudioStream.
- * Returns false if magic/version/layout is not recognized.
- * outRateHz is set from Paula period when present (NTSC clock / period), else 8000.
- */
 bool extractAmigaMxosPcm(const byte *mxos, uint32 mxosSize, Common::Array<byte> &outPcm, uint16 &outRateHz);
 
 } // End of namespace Macs2
diff --git a/engines/macs2/amiga_resources.cpp b/engines/macs2/amiga_resources.cpp
index 62a0eb6175b..565d5b68802 100644
--- a/engines/macs2/amiga_resources.cpp
+++ b/engines/macs2/amiga_resources.cpp
@@ -120,8 +120,6 @@ bool Macs2Engine::loadAmigaSceneBackground(uint32 sceneResourceId) {
 		blitMap(_shadowMap, shadowMap);
 	}
 
-	// Hotspot colors (trailer 0x22) + MXCC RLE map (Map2). No 4th 64000 hotspot
-	// chunk on Amiga - decodeMxccRunLengthAt expands MXCC to the DOS hotspot map.
 	_numHotspots = 0;
 	_hotspotColorTable.clear();
 	uint16 numHotspots = 0;
@@ -143,20 +141,11 @@ bool Macs2Engine::loadAmigaSceneBackground(uint32 sceneResourceId) {
 	_backgroundAnimationsBlobs.clear();
 	_mapImageFileOffset = 0;
 	_mapSubSceneTableFilePos = 0;
-	// Trailer 0x40+0x80 after walk params: native copies to 0024888c/00248954.
-	// Demo scenes are all zeros; no other xrefs in this binary - leave
-	// _sceneResourceOffsets alone (Amiga loads by archive type/id).
-	// MXAA: size word after Map2 is 0 on MM_0004/MM_0040; tickMxaaOverlayAnims
-	// has nothing to run until a scene ships a real MXAA blob.
 	if (amigaMxmmHasMxaaOverlay(mxmm.data(), size)) {
 		debugC(1, kDebugFilePath, "Amiga: MM_%04u has MXAA overlay data (not loaded yet)",
 			   (uint)sceneResourceId);
 	}
 
-	// Pathfinding graph + walk depth/speed live in the MXMM trailer (Ghidra
-	// load_scene_mxmm @ 00221d90). Without nodes, calculatePath always fails and
-	// walkAlongPath cancels with finalDest=current - waitForWalk then completes
-	// immediately while the actor is still short of the script target.
 	_pathfindingPoints.clear();
 	_numPathfindingPoints = 0;
 	uint16 numPfPoints = 0;
@@ -178,8 +167,6 @@ bool Macs2Engine::loadAmigaSceneBackground(uint32 sceneResourceId) {
 		}
 	}
 
-	// Walk depth/speed percent (Ghidra g_abSceneWalkPaletteParams / DOS 0x51FD..).
-	// Leaving base at 0 makes walkAlongPath clamp to 1 px/frame and cancel early.
 	uint16 walkThreshY = 0, walkScale = 0, walkBasePct = 0, palMode = 0, darken = 0;
 	if (extractAmigaMxmmSceneWalkParams(mxmm.data(), size, walkThreshY, walkScale, walkBasePct, palMode,
 										darken)) {
@@ -189,7 +176,6 @@ bool Macs2Engine::loadAmigaSceneBackground(uint32 sceneResourceId) {
 		_scenePaletteMode = palMode != 0 ? palMode : 1;
 		_paletteDarkenPercent = darken;
 	} else {
-		// Empty stubs (e.g. MM_0040) and DOS flat rooms use 100/100/100.
 		_walkDepthThresholdY = 100;
 		_walkDepthScaleFactor = 100;
 		_walkBaseSpeedPct = 100;
@@ -197,7 +183,6 @@ bool Macs2Engine::loadAmigaSceneBackground(uint32 sceneResourceId) {
 		_paletteDarkenPercent = 0;
 	}
 
-	// Scene script/strings live in the MXMM trailer (not the global scene_table stub).
 	_amigaPendingSceneScript.clear();
 	_amigaPendingSceneStrings.clear();
 	extractAmigaMxmmSceneScript(mxmm.data(), size, _amigaPendingSceneScript, _amigaPendingSceneStrings);


Commit: 0377e6b5b03b4971905ef03b8ffb2b1ed1b9af37
    https://github.com/scummvm/scummvm/commit/0377e6b5b03b4971905ef03b8ffb2b1ed1b9af37
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-31T21:14:09+02:00

Commit Message:
MACS2: fixed CID 1685788

Changed paths:
    engines/macs2/scriptexecutor.cpp


diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index ef32d5c2e36..67f9c2bd469 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -616,11 +616,11 @@ void ScriptExecutor::step() {
 			// Continue execution
 
 			// Check if the currently executing script is at the end
-			if (_stream && _stream->pos() >= effectiveScriptEnd()) {
+			if (!_stream || _stream->pos() >= effectiveScriptEnd()) {
 				syncScriptIsExecutingFlag();
 				// Binary (runScriptExecutor 1008:e3e7): if script finishes while
 				// g_wScriptSkippable is still set, treat as error 0x11 and abort.
-				if (_scriptSkippable) {
+				if (_stream && _scriptSkippable) {
 					setScriptError(0x11);
 					_scriptSkippable = false;
 					shouldContinue = false;
@@ -648,7 +648,7 @@ void ScriptExecutor::step() {
 					return;
 				}
 				syncScriptIsExecutingFlag();
-				if (_stream && _stream->pos() >= effectiveScriptEnd()) {
+				if (_stream->pos() >= effectiveScriptEnd()) {
 					if (_scriptSkippable) {
 						setScriptError(0x11);
 						_scriptSkippable = false;


Commit: eef25e7b2c9b5e56b9595105ff2d6e8c0e6fb21f
    https://github.com/scummvm/scummvm/commit/eef25e7b2c9b5e56b9595105ff2d6e8c0e6fb21f
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-31T21:14:09+02:00

Commit Message:
MACS2: replaced magic number

Changed paths:
    engines/macs2/amiga_resources.cpp


diff --git a/engines/macs2/amiga_resources.cpp b/engines/macs2/amiga_resources.cpp
index 565d5b68802..8893c5e9355 100644
--- a/engines/macs2/amiga_resources.cpp
+++ b/engines/macs2/amiga_resources.cpp
@@ -37,6 +37,15 @@
 
 namespace Macs2 {
 
+static void blitMap(Graphics::ManagedSurface &dest, const Common::Array<byte> &src) {
+	if (src.size() != (uint)kScreenWidth * kGameHeight)
+		return;
+	for (int y = 0; y < kGameHeight; y++) {
+		for (int x = 0; x < kScreenWidth; x++)
+			dest.setPixel(x, y, src[(uint)y * kScreenWidth + x]);
+	}
+}
+
 bool Macs2Engine::loadAmigaSceneBackground(uint32 sceneResourceId) {
 	if (!_amigaArchive || sceneResourceId == 0 || sceneResourceId > 0xFFFF)
 		return false;
@@ -107,14 +116,6 @@ bool Macs2Engine::loadAmigaSceneBackground(uint32 sceneResourceId) {
 
 	Common::Array<byte> pathMap, depthMap, shadowMap;
 	if (extractAmigaMxmmSceneMaps(mxmm.data(), size, pathMap, depthMap, shadowMap)) {
-		auto blitMap = [](Graphics::ManagedSurface &dest, const Common::Array<byte> &src) {
-			if (src.size() != (uint)kScreenWidth * kGameHeight)
-				return;
-			for (int y = 0; y < kGameHeight; y++) {
-				for (int x = 0; x < kScreenWidth; x++)
-					dest.setPixel(x, y, src[(uint)y * kScreenWidth + x]);
-			}
-		};
 		blitMap(_pathfindingMap, pathMap);
 		blitMap(_depthMap, depthMap);
 		blitMap(_shadowMap, shadowMap);
@@ -555,8 +556,6 @@ void Macs2Engine::readAmigaResources() {
 		gameObject->_sceneIndex = 0;
 		gameObject->_orientation = 11;
 		gameObject->_verticalOffsetScale = 0;
-		// MXOO has no DOS +0x185/+0x186 flag bytes. Defaults stay false until we
-		// infer character-style rendering below (or a script opcode sets them).
 
 		while (gameObject->_blobs.size() < 0x15)
 			gameObject->_blobs.push_back(Common::Array<uint8>());
@@ -589,7 +588,7 @@ void Macs2Engine::readAmigaResources() {
 			static const uint16 kAmigaWalkSpeeds[8] = {2, 4, 6, 4, 2, 4, 6, 4};
 			static const uint8 kMirrorOrientToSource[6][2] = {
 				{6, 4}, {7, 3}, {8, 2}, {14, 12}, {15, 11}, {16, 10}};
-			for (uint i = 0; i < 8; i++) {
+			for (uint i = 0; i < ARRAYSIZE(kAmigaWalkSpeeds); i++) {
 				if (i < gameObject->_blobWalkSpeeds.size())
 					gameObject->_blobWalkSpeeds[i] = kAmigaWalkSpeeds[i];
 			}
@@ -694,8 +693,6 @@ void Macs2Engine::readAmigaResources() {
 	if (!loadedMxff)
 		warning("Amiga: no MXFF font FF_0000 in DataA - text may be missing");
 
-	// Border chrome uses fixed slots 30/31/32. Empty/zero-size tiles make
-	// drawBorderSide spin forever (no events, frameWait appears stuck).
 	auto ensureBorderTile = [this](uint index, byte color, uint16 w, uint16 h) {
 		if (index >= _imageResources.size())
 			return;


Commit: 2b6c76b41a05efbe6b929d9c6c3a0aeba0968d73
    https://github.com/scummvm/scummvm/commit/2b6c76b41a05efbe6b929d9c6c3a0aeba0968d73
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-31T21:14:09+02:00

Commit Message:
MACS2: fixed portrait palette

Changed paths:
    engines/macs2/amiga_decode.cpp
    engines/macs2/amiga_decode.h


diff --git a/engines/macs2/amiga_decode.cpp b/engines/macs2/amiga_decode.cpp
index 9582047efe6..f524f9e9dc2 100644
--- a/engines/macs2/amiga_decode.cpp
+++ b/engines/macs2/amiga_decode.cpp
@@ -318,12 +318,12 @@ bool convertAmigaPortraitAtlasToDosBlob(const byte *mxoo, uint32 mxooSize, uint3
 			byte color = 0;
 			const uint32 bitIndex = (uint32)x & 7;
 			const uint32 byteInRow = (uint32)x >> 3;
-			for (uint16 plane = 0; plane < 5; plane++) {
+			for (uint16 plane = 0; plane < kPlanes; plane++) {
 				const byte *planeRow = src + plane * planeBytes + (uint32)y * rowBytes;
 				if (planeRow[byteInRow] & (0x80 >> bitIndex))
 					color |= (byte)(1 << plane);
 			}
-			atlas[(uint32)y * atlasW + x] = color;
+			atlas[(uint32)y * atlasW + x] = remapAmigaCopperIndexToStableUi(color);
 		}
 	}
 
@@ -380,6 +380,27 @@ static bool decompressPp20ToBuffer(const byte *src, uint32 srcLen, Common::Array
 	return true;
 }
 
+byte remapAmigaCopperIndexToStableUi(byte color) {
+	if (color == 0)
+		return 0;
+
+	const bool ehb = color >= kAmigaColorRegisterCount;
+	const byte base = ehb ? (byte)(color - kAmigaColorRegisterCount) : color;
+	byte ui;
+	if (base >= 17 && base <= 31)
+		ui = (byte)(0xF0 + (base - 16));
+	else if (base < 16)
+		ui = (byte)(0xF0 + base);
+	else
+		ui = base;
+
+	if (!ehb)
+		return ui;
+	if (ui < 0xF0)
+		return (byte)(kAmigaColorRegisterCount + base);
+	return (byte)(0xE0 + (ui - 0xF0));
+}
+
 void amiga12ToVga6(uint16 rgb, byte &r6, byte &g6, byte &b6) {
 	const byte r4 = (rgb >> 8) & 0xF;
 	const byte g4 = (rgb >> 4) & 0xF;
diff --git a/engines/macs2/amiga_decode.h b/engines/macs2/amiga_decode.h
index 1f3d6da4943..4c977459457 100644
--- a/engines/macs2/amiga_decode.h
+++ b/engines/macs2/amiga_decode.h
@@ -58,6 +58,7 @@ struct AmigaAnimSlotInfo {
 bool parseAmigaMxoo(const byte *mxoo, uint32 mxooSize, AmigaMxooInfo &out);
 bool inspectAmigaAnimSlot(const byte *mxoo, uint32 mxooSize, uint32 bodyRelativeOffset, AmigaAnimSlotInfo &out);
 void amiga12ToVga6(uint16 rgb, byte &r6, byte &g6, byte &b6);
+byte remapAmigaCopperIndexToStableUi(byte color);
 bool decodeAmigaPlanarFrame(const byte *planar, uint16 width, uint16 height, uint16 frameIndex,
 							uint16 frameCount, Common::Array<byte> &outPixels);
 bool convertAmigaAnimSlotToDosBlob(const byte *mxoo, uint32 mxooSize, uint32 bodyRelativeOffset,


Commit: a40f1cdd83f0b3f4f2296d34e2b3fc8a3498c8f9
    https://github.com/scummvm/scummvm/commit/a40f1cdd83f0b3f4f2296d34e2b3fc8a3498c8f9
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-31T21:14:25+02:00

Commit Message:
MACS2: amiga palette fixes

Changed paths:
    engines/macs2/amiga_decode.cpp
    engines/macs2/amiga_decode.h
    engines/macs2/amiga_resources.cpp
    engines/macs2/macs2.cpp
    engines/macs2/macs2.h
    engines/macs2/view1.cpp


diff --git a/engines/macs2/amiga_decode.cpp b/engines/macs2/amiga_decode.cpp
index f524f9e9dc2..632da989470 100644
--- a/engines/macs2/amiga_decode.cpp
+++ b/engines/macs2/amiga_decode.cpp
@@ -117,7 +117,7 @@ bool decodeAmigaPlanarFrame(const byte *planar, uint16 width, uint16 height, uin
 			byte color = 0;
 			const uint32 bitIndex = (uint32)x & 7;
 			const uint32 byteInRow = (uint32)x >> 3;
-			for (uint16 plane = 0; plane < 5; plane++) {
+			for (uint16 plane = 0; plane < kPlanes; plane++) {
 				const byte *planeRow = frameBase + plane * planeBytes + (uint32)y * rowBytes;
 				if (planeRow[byteInRow] & (0x80 >> bitIndex))
 					color |= (byte)(1 << plane);
@@ -422,8 +422,10 @@ void amiga12ToRgb8(uint16 rgb, byte &r, byte &g, byte &b) {
 bool decodeAmigaMxmmSceneBackground(const byte *mxmm, uint32 mxmmSize,
 									Common::Array<byte> &outPixels,
 									Graphics::Palette &outPalette,
-									uint &outColorCount) {
+									uint &outColorCount,
+									Common::Array<byte> &outLineCopperPal) {
 	outPixels.clear();
+	outLineCopperPal.clear();
 	outColorCount = 0;
 	outPalette = Graphics::Palette(Graphics::PALETTE_COUNT);
 	if (!mxmm || mxmmSize < kAmigaMxmmMinSize)
@@ -524,7 +526,7 @@ bool decodeAmigaMxmmSceneBackground(const byte *mxmm, uint32 mxmmSize,
 			byte outIdx;
 			if (colorToIndex.contains(key)) {
 				outIdx = colorToIndex[key];
-			} else if (outColorCount < 256) {
+			} else if (outColorCount < 0xE0) {
 				outIdx = (byte)outColorCount;
 				colorToIndex[key] = outIdx;
 				outPalette.set(outIdx, r, g, b);
@@ -536,6 +538,33 @@ bool decodeAmigaMxmmSceneBackground(const byte *mxmm, uint32 mxmmSize,
 		}
 	}
 
+	auto internRgb = [&](byte r, byte g, byte b, byte fallback) -> byte {
+		const uint32 key = ((uint32)r << 16) | ((uint32)g << 8) | b;
+		if (colorToIndex.contains(key))
+			return colorToIndex[key];
+		if (outColorCount < 0xE0) {
+			const byte outIdx = (byte)outColorCount;
+			colorToIndex[key] = outIdx;
+			outPalette.set(outIdx, r, g, b);
+			outColorCount++;
+			return outIdx;
+		}
+		return fallback;
+	};
+
+	outLineCopperPal.resize((uint)kAmigaSceneHeight * kAmigaEhbPaletteCount);
+	for (uint16 y = 0; y < kAmigaSceneHeight; y++) {
+		byte pal32[kAmigaColorRegisterCount][3];
+		buildPal32(y, pal32);
+		for (uint c = 0; c < kAmigaColorRegisterCount; c++) {
+			outLineCopperPal[(uint32)y * kAmigaEhbPaletteCount + c] =
+				internRgb(pal32[c][0], pal32[c][1], pal32[c][2], (byte)c);
+			outLineCopperPal[(uint32)y * kAmigaEhbPaletteCount + kAmigaColorRegisterCount + c] =
+				internRgb((byte)(pal32[c][0] / 2), (byte)(pal32[c][1] / 2), (byte)(pal32[c][2] / 2),
+						  (byte)(kAmigaColorRegisterCount + c));
+		}
+	}
+
 	return outColorCount > 0;
 }
 
diff --git a/engines/macs2/amiga_decode.h b/engines/macs2/amiga_decode.h
index 4c977459457..df2cd582cd8 100644
--- a/engines/macs2/amiga_decode.h
+++ b/engines/macs2/amiga_decode.h
@@ -121,7 +121,8 @@ enum : uint32 {
 bool decodeAmigaMxmmSceneBackground(const byte *mxmm, uint32 mxmmSize,
 									Common::Array<byte> &outPixels,
 									Graphics::Palette &outPalette,
-									uint &outColorCount);
+									uint &outColorCount,
+									Common::Array<byte> &outLineCopperPal);
 
 bool extractAmigaMxmmSceneScript(const byte *mxmm, uint32 mxmmSize,
 								 Common::Array<byte> &outScript,
diff --git a/engines/macs2/amiga_resources.cpp b/engines/macs2/amiga_resources.cpp
index 8893c5e9355..6521eb8da1e 100644
--- a/engines/macs2/amiga_resources.cpp
+++ b/engines/macs2/amiga_resources.cpp
@@ -66,7 +66,8 @@ bool Macs2Engine::loadAmigaSceneBackground(uint32 sceneResourceId) {
 	Common::Array<byte> pixels;
 	Graphics::Palette paletteRgb(Graphics::PALETTE_COUNT);
 	uint colorCount = 0;
-	if (!decodeAmigaMxmmSceneBackground(mxmm.data(), size, pixels, paletteRgb, colorCount))
+	Common::Array<byte> lineCopperPal;
+	if (!decodeAmigaMxmmSceneBackground(mxmm.data(), size, pixels, paletteRgb, colorCount, lineCopperPal))
 		return false;
 	if (pixels.size() != (uint)kScreenWidth * kGameHeight || colorCount == 0)
 		return false;
@@ -102,6 +103,7 @@ bool Macs2Engine::loadAmigaSceneBackground(uint32 sceneResourceId) {
 		}
 	}
 	_amigaNativePlayfieldPalette = true;
+	_amigaLineCopperPal = Common::move(lineCopperPal);
 	// Portraits share playfield COLOR17..31 (copper high bank). Native copper
 	// already filled those slots; installAmigaPortraitPalette is a no-op here.
 	installAmigaPortraitPalette(true);
@@ -349,34 +351,28 @@ bool Macs2Engine::loadAmigaCursorResource(uint16 resourceId, AnimFrame &out) {
 }
 
 void Macs2Engine::installAmigaPortraitPalette(bool copyFromPlayfield) {
-	// Portraits now keep Amiga COLOR indices and share the playfield copper
-	// high bank (COLOR17..31), matching animateDialoguePortrait on hardware.
-	// Demo OO_* atlases never touch COLOR01..16.
-	//
-	// When native copper is not resident (pre-scene), seed
-	// COLOR17..31 from MXIN chrome so portraits are not left on the provisional
-	// ramp from applyAmigaUiPalette. copyFromPlayfield is ignored - live copper
-	// already owns those slots after loadAmigaSceneBackground.
-	(void)copyFromPlayfield;
-
-	if (_amigaNativePlayfieldPalette)
-		return;
-
 	static const uint16 kHighBankFallback[15] = {
 		0x0BBA, 0x0EB8, 0x0C96, 0x0A74, 0x0963, 0x0741, 0x0000, 0x049E,
 		0x0C00, 0x0DDC, 0x0EEE, 0x0887, 0x0776, 0x0006, 0x0520
 	};
 
-	// Copper base16: COLOR00 + COLOR17..31 == MXIN ui[0..15].
 	const uint16 *highSrc = kHighBankFallback;
 	if (_amigaArchive && _amigaArchive->getInfo().loaded)
 		highSrc = &_amigaArchive->getInfo().uiPaletteAmiga[1];
 
-	for (uint i = 0; i < ARRAYSIZE(kHighBankFallback); i++) {
+	if (!_amigaNativePlayfieldPalette) {
+		// Copper base16: COLOR00 + COLOR17..31 == MXIN ui[0..15].
+		for (uint i = 0; i < ARRAYSIZE(kHighBankFallback); i++) {
+			byte r6, g6, b6;
+			amiga12ToVga6(highSrc[i], r6, g6, b6);
+			_palVanilla.set(17 + i, r6, g6, b6);
+		}
+	}
+
+	for (uint i = 0; i < 16; i++) {
 		byte r6, g6, b6;
-		amiga12ToVga6(highSrc[i], r6, g6, b6);
-		const uint idx = 17 + i;
-		_palVanilla.set(idx, r6, g6, b6);
+		_palVanilla.get(0xF0 + i, r6, g6, b6);
+		_palVanilla.set(0xE0 + i, (byte)(r6 / 2), (byte)(g6 / 2), (byte)(b6 / 2));
 	}
 }
 
@@ -386,9 +382,6 @@ void Macs2Engine::applyAmigaUiPalette() {
 
 	const AmigaInfoData &info = _amigaArchive->getInfo();
 
-	// Provisional playfield until an MM_* copper list is loaded.
-	// Copper base16 layout: COLOR00 + COLOR17..31 = MXIN ui[0..15]. COLOR01..16
-	// stay a visible ramp (overwritten per-line by scene copper).
 	byte r6, g6, b6;
 	amiga12ToVga6(info.uiPaletteAmiga[0], r6, g6, b6);
 	_palVanilla.set(0, 0, 0, 0);
@@ -410,6 +403,7 @@ void Macs2Engine::applyAmigaUiPalette() {
 		_palVanilla.set(idx, r6, g6, b6);
 	}
 	_amigaNativePlayfieldPalette = false;
+	_amigaLineCopperPal.clear();
 	installAmigaPortraitPalette(false);
 	buildAmigaPanelRemapTable();
 	applyPaletteDarkening();
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 3e4f3dfc18d..d018cb6e924 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -3697,9 +3697,15 @@ void Macs2Engine::applyPaletteDarkening() {
 	for (uint i = 0; i < Graphics::PALETTE_COUNT; i++) {
 		byte r, g, b;
 		_palVanilla.get(i, r, g, b);
-		_pal.set(i, (byte)((r * brightnessFactor / 100 * 259 + 33) >> 6),
-				 (byte)((g * brightnessFactor / 100 * 259 + 33) >> 6),
-				 (byte)((b * brightnessFactor / 100 * 259 + 33) >> 6));
+		if (isAmiga()) {
+			_pal.set(i, (byte)((r * brightnessFactor / 100 * 255) / 63),
+					 (byte)((g * brightnessFactor / 100 * 255) / 63),
+					 (byte)((b * brightnessFactor / 100 * 255) / 63));
+		} else {
+			_pal.set(i, (byte)((r * brightnessFactor / 100 * 259 + 33) >> 6),
+					 (byte)((g * brightnessFactor / 100 * 259 + 33) >> 6),
+					 (byte)((b * brightnessFactor / 100 * 259 + 33) >> 6));
+		}
 	}
 }
 
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index 6c471e31fac..80366d91055 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -548,6 +548,7 @@ public:
 	uint16 amigaTextLinePitch = 0;
 	/** True after loadAmigaSceneBackground installed copper colors in 0..31. */
 	bool _amigaNativePlayfieldPalette = false;
+	Common::Array<byte> _amigaLineCopperPal;
 	/** Amiga DataA/Mdir archive (owned). Null on DOS. */
 	Macs2AmigaArchive *_amigaArchive = nullptr;
 	/** Filled by loadAmigaSceneBackground; consumed by Amiga changeScene. */
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index 30ac21bf633..57a2691fdc4 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -33,6 +33,7 @@
 #include "macs2/debugtools.h"
 #include "macs2/detection.h"
 #include "macs2/gameobjects.h"
+#include "macs2/amiga_decode.h"
 #include "macs2/macs2.h"
 #include "macs2/music.h"
 #include "macs2/actionbar.h"
@@ -66,6 +67,110 @@ void resetObjectDrawBounds(GameObject *obj) {
 		obj->resetDrawBounds();
 }
 
+void plotAmigaUiPixel(Graphics::ManagedSurface &s, int x, int y, byte color) {
+	if (x < 0 || y < 0 || x >= s.w || y >= s.h)
+		return;
+	s.setPixel(x, y, color);
+}
+
+void drawAmigaUiLine(Graphics::ManagedSurface &s, int x0, int y0, int x1, int y1, byte color) {
+	int dx = ABS(x1 - x0);
+	int sx = x0 < x1 ? 1 : -1;
+	int dy = -ABS(y1 - y0);
+	int sy = y0 < y1 ? 1 : -1;
+	int err = dx + dy;
+	for (;;) {
+		plotAmigaUiPixel(s, x0, y0, color);
+		if (x0 == x1 && y0 == y1)
+			break;
+		const int e2 = 2 * err;
+		if (e2 >= dy) {
+			err += dy;
+			x0 += sx;
+		}
+		if (e2 <= dx) {
+			err += dx;
+			y0 += sy;
+		}
+	}
+}
+
+byte amigaPanelBorderColor(int tableIndex) {
+	uint16 copper = (uint16)(18 + tableIndex);
+	if (g_engine->_amigaArchive && g_engine->_amigaArchive->getInfo().loaded) {
+		const uint16 *idx = g_engine->_amigaArchive->getInfo().panelBorderColorIndices;
+		if (tableIndex >= 0 && tableIndex < 5 && idx[tableIndex] != 0)
+			copper = idx[tableIndex];
+	}
+	return remapAmigaCopperIndexToStableUi((byte)copper);
+}
+
+void fillAmigaPanelInterior(Graphics::ManagedSurface &s, int x, int y, int w, int h) {
+	// UAE SOLID panels are opaque dark wood, not scene-through SHADE (that
+	// would be sky-blue at the top of the screen). Pattern sheet BSS is zero;
+	// COLOR21/22 (963/741) EHB matches the emulator interior (~78,51,21).
+	const byte a = remapAmigaCopperIndexToStableUi(21);
+	const byte bCol = remapAmigaCopperIndexToStableUi(22);
+	byte ehbA = a;
+	byte ehbB = bCol;
+	if (a >= 0xF0)
+		ehbA = (byte)(0xE0 + (a - 0xF0));
+	if (bCol >= 0xF0)
+		ehbB = (byte)(0xE0 + (bCol - 0xF0));
+	for (int oy = 0; oy < h; oy++) {
+		for (int ox = 0; ox < w; ox++)
+			plotAmigaUiPixel(s, x + ox, y + oy, ((ox ^ oy) & 1) ? ehbA : ehbB);
+	}
+}
+
+void drawAmigaPanelBevel(Graphics::ManagedSurface &s, int x, int y, int w, int h) {
+	const byte c0 = amigaPanelBorderColor(0);
+	const byte c1 = amigaPanelBorderColor(1);
+	const byte c2 = amigaPanelBorderColor(2);
+	const byte c3 = amigaPanelBorderColor(3);
+	const byte c4 = amigaPanelBorderColor(4);
+	const int midX = x + (w >> 1);
+	const int midY = y + (h >> 1);
+
+	drawAmigaUiLine(s, x, y, midX, y, c0);
+	drawAmigaUiLine(s, x, y + h, midX, y + h, c3);
+	drawAmigaUiLine(s, midX, y, x + w, y, c1);
+	drawAmigaUiLine(s, midX, y + h, x + w, y + h, c4);
+	drawAmigaUiLine(s, x, y, x, midY, c0);
+	drawAmigaUiLine(s, x + w, y, x + w, midY, c3);
+	drawAmigaUiLine(s, x, midY, x, y + h, c1);
+	drawAmigaUiLine(s, x + w, midY, x + w, y + h + 1, c4);
+
+	plotAmigaUiPixel(s, x + w, y, c2);
+	plotAmigaUiPixel(s, x, y + h, c2);
+	plotAmigaUiPixel(s, midX + 2, y, c0);
+	plotAmigaUiPixel(s, midX + 2, y + h, c3);
+	plotAmigaUiPixel(s, x, midY + 2, c0);
+	plotAmigaUiPixel(s, x + w, midY + 2, c3);
+}
+
+void drawAmigaUiPanel(const Common::Point &pos, const Common::Point &size, Graphics::ManagedSurface &s) {
+	const int x = pos.x;
+	const int y = pos.y;
+	const int w = size.x - 1;
+	const int h = size.y - 1;
+	if (w < 1 || h < 1)
+		return;
+	fillAmigaPanelInterior(s, x, y, w, h);
+	drawAmigaPanelBevel(s, x, y, w, h);
+}
+
+byte remapAmigaPlayfieldIndex(byte val, int y) {
+	if (val >= kAmigaEhbPaletteCount)
+		return val;
+	const Common::Array<byte> &map = g_engine->_amigaLineCopperPal;
+	if (map.size() < (uint)kAmigaSceneHeight * kAmigaEhbPaletteCount)
+		return val;
+	if (y < 0 || y >= (int)kAmigaSceneHeight)
+		return val;
+	return map[(uint)y * kAmigaEhbPaletteCount + val];
+}
+
 // Build a screen-clipped erase rect from the previous frame's sprite bounds.
 // Returns false when there is nothing on-screen to erase.
 bool buildClippedEraseRect(int32 left, int32 top, uint16 width, uint16 height,
@@ -628,17 +733,20 @@ void View1::drawCurrentSpeaker(Graphics::ManagedSurface &s) {
 	AnimFrame *leftPortrait = currentSpeechActData.speaker->getCurrentPortrait(false, 0);
 	AnimFrame *rightPortrait = currentSpeechActData.speaker->getCurrentPortrait(true, 0);
 
-	// See l0037_B462: for the calculations below
-	// Draw the border
-	const int portraitWidth = MAX<int>(leftPortrait ? leftPortrait->_width : 0, rightPortrait ? rightPortrait->_width : 0);
-	const int portraitHeight = MAX<int>(leftPortrait ? leftPortrait->_height : 0, rightPortrait ? rightPortrait->_height : 0);
-	const int borderPad = g_engine->portraitBorderPad();
-	const int contentInset = g_engine->portraitContentInset();
-	const Common::Point borderSize(portraitWidth + borderPad, portraitHeight + borderPad);
-	drawBorder(currentSpeechActData.position, borderSize, s);
-
-	// Draw the portrait over the border
-	Common::Point pos = currentSpeechActData.position + Common::Point(contentInset, contentInset);
+	Common::Point pos = currentSpeechActData.position;
+	if (g_engine->isAmiga()) {
+		drawAmigaUiPanel(currentSpeechActData.position,
+						 Common::Point(frame->_width + 2, frame->_height + 2), s);
+		pos += Common::Point(1, 1);
+	} else {
+		const int portraitWidth = MAX<int>(leftPortrait ? leftPortrait->_width : 0, rightPortrait ? rightPortrait->_width : 0);
+		const int portraitHeight = MAX<int>(leftPortrait ? leftPortrait->_height : 0, rightPortrait ? rightPortrait->_height : 0);
+		const int borderPad = g_engine->portraitBorderPad();
+		const int contentInset = g_engine->portraitContentInset();
+		const Common::Point borderSize(portraitWidth + borderPad, portraitHeight + borderPad);
+		drawBorder(currentSpeechActData.position, borderSize, s);
+		pos += Common::Point(contentInset, contentInset);
+	}
 	drawSprite(pos, frame->_width, frame->_height, frame->_data.data(), s, false);
 	delete frame;
 	delete leftPortrait;
@@ -3075,7 +3183,9 @@ void View1::drawSprite(int16 x, int16 y, uint16 width, uint16 height, byte *data
 						if (g_engine->_depthMap.getPixel(finalX, finalY) >= depth)
 							continue;
 					}
-					s.setPixel(x + actualX, y + currentY, val);
+					if (g_engine->isAmiga())
+						val = remapAmigaPlayfieldIndex(val, finalY);
+					s.setPixel(finalX, finalY, val);
 				}
 			}
 		}
@@ -3097,8 +3207,11 @@ void View1::drawSpriteClipped(uint16 x, uint16 y, Common::Rect &clippingRect, ui
 			if (val != 0) {
 				const int px = x + currentX;
 				const int py = y + currentY;
-				if (clippingRect.contains(px, py) && px < s.w && py < s.h)
+				if (clippingRect.contains(px, py) && px < s.w && py < s.h) {
+					if (g_engine->isAmiga())
+						val = remapAmigaPlayfieldIndex(val, py);
 					s.setPixel(px, py, val);
+				}
 			}
 		}
 	}
@@ -3146,7 +3259,7 @@ void View1::drawSpriteFitted(const Common::Rect &bounds, const Sprite &sprite, G
 			if (px < inner.left || px >= inner.right || px < 0 || px >= s.w || py < 0 || py >= s.h)
 				continue;
 
-			s.setPixel(px, py, val);
+			s.setPixel(px, py, g_engine->isAmiga() ? remapAmigaPlayfieldIndex(val, py) : val);
 		}
 	}
 }
@@ -3199,8 +3312,10 @@ void View1::drawSpriteScaled(int shadingTableOffset, uint8 depthThreshold, int16
 						const uint8 color = srcPixels[srcRow + srcX];
 						if (color != 0) {
 							const byte bg = s.getPixel(screenX, screenY);
-							s.setPixel(screenX, screenY,
-									   applyShadingTable(color, shadingTableOffset, bg, useMaskedShading));
+							byte drawn = applyShadingTable(color, shadingTableOffset, bg, useMaskedShading);
+							if (g_engine->isAmiga())
+								drawn = remapAmigaPlayfieldIndex(drawn, screenY);
+							s.setPixel(screenX, screenY, drawn);
 						}
 					}
 				}
@@ -3235,8 +3350,10 @@ void View1::drawSpriteTransparent(int shadingTableOffset, uint8 depthThreshold,
 				if (color != 0 && screenX >= 0 && screenX < s.w &&
 					g_engine->_depthMap.getPixel(screenX, screenY) < depthThreshold) {
 					const byte bg = s.getPixel(screenX, screenY);
-					s.setPixel(screenX, screenY,
-							   applyShadingTable(color, shadingTableOffset, bg, useMaskedShading));
+					byte drawn = applyShadingTable(color, shadingTableOffset, bg, useMaskedShading);
+					if (g_engine->isAmiga())
+						drawn = remapAmigaPlayfieldIndex(drawn, screenY);
+					s.setPixel(screenX, screenY, drawn);
 				}
 
 				screenX++;
@@ -3375,9 +3492,14 @@ void View1::drawNinePatchBorder(const Common::Point &pos, const Common::Point &s
 
 void View1::drawBorder(const Common::Point &pos, const Common::Point &size, Graphics::ManagedSurface &s) {
 	// fn0037_A65D proc
-	constexpr uint16 border = 6;
 	debugC(kDebugScript, "Render border: pos=(%d,%d) size=(%d,%d)", pos.x, pos.y, size.x, size.y);
 
+	if (g_engine->isAmiga()) {
+		drawAmigaUiPanel(pos, size, s);
+		return;
+	}
+
+	constexpr uint16 border = 6;
 	drawDarkRectangle(pos.x + 1, pos.y + 1, size.x - 1, size.y - 1);
 
 	// Four textured border sides


Commit: ca4601c2ec2160e7df74d2228cdf884ae84ab1d6
    https://github.com/scummvm/scummvm/commit/ca4601c2ec2160e7df74d2228cdf884ae84ab1d6
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-31T21:21:05+02:00

Commit Message:
MACS2: reduced code duplication

Changed paths:
    engines/macs2/view1.cpp


diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index 57a2691fdc4..f247e503480 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -160,15 +160,14 @@ void drawAmigaUiPanel(const Common::Point &pos, const Common::Point &size, Graph
 	drawAmigaPanelBevel(s, x, y, w, h);
 }
 
-byte remapAmigaPlayfieldIndex(byte val, int y) {
-	if (val >= kAmigaEhbPaletteCount)
-		return val;
-	const Common::Array<byte> &map = g_engine->_amigaLineCopperPal;
-	if (map.size() < (uint)kAmigaSceneHeight * kAmigaEhbPaletteCount)
-		return val;
-	if (y < 0 || y >= (int)kAmigaSceneHeight)
-		return val;
-	return map[(uint)y * kAmigaEhbPaletteCount + val];
+void setPixel(Graphics::ManagedSurface &s, int x, int y, byte color) {
+	if (g_engine->isAmiga() && color < kAmigaEhbPaletteCount &&
+		y >= 0 && y < (int)kAmigaSceneHeight) {
+		const Common::Array<byte> &map = g_engine->_amigaLineCopperPal;
+		if (map.size() >= (uint)kAmigaSceneHeight * kAmigaEhbPaletteCount)
+			color = map[(uint)y * kAmigaEhbPaletteCount + color];
+	}
+	s.setPixel(x, y, color);
 }
 
 // Build a screen-clipped erase rect from the previous frame's sprite bounds.
@@ -3183,9 +3182,7 @@ void View1::drawSprite(int16 x, int16 y, uint16 width, uint16 height, byte *data
 						if (g_engine->_depthMap.getPixel(finalX, finalY) >= depth)
 							continue;
 					}
-					if (g_engine->isAmiga())
-						val = remapAmigaPlayfieldIndex(val, finalY);
-					s.setPixel(finalX, finalY, val);
+					setPixel(s, finalX, finalY, val);
 				}
 			}
 		}
@@ -3207,11 +3204,8 @@ void View1::drawSpriteClipped(uint16 x, uint16 y, Common::Rect &clippingRect, ui
 			if (val != 0) {
 				const int px = x + currentX;
 				const int py = y + currentY;
-				if (clippingRect.contains(px, py) && px < s.w && py < s.h) {
-					if (g_engine->isAmiga())
-						val = remapAmigaPlayfieldIndex(val, py);
-					s.setPixel(px, py, val);
-				}
+				if (clippingRect.contains(px, py) && px < s.w && py < s.h)
+					setPixel(s, px, py, val);
 			}
 		}
 	}
@@ -3259,7 +3253,7 @@ void View1::drawSpriteFitted(const Common::Rect &bounds, const Sprite &sprite, G
 			if (px < inner.left || px >= inner.right || px < 0 || px >= s.w || py < 0 || py >= s.h)
 				continue;
 
-			s.setPixel(px, py, g_engine->isAmiga() ? remapAmigaPlayfieldIndex(val, py) : val);
+			setPixel(s, px, py, val);
 		}
 	}
 }
@@ -3312,10 +3306,7 @@ void View1::drawSpriteScaled(int shadingTableOffset, uint8 depthThreshold, int16
 						const uint8 color = srcPixels[srcRow + srcX];
 						if (color != 0) {
 							const byte bg = s.getPixel(screenX, screenY);
-							byte drawn = applyShadingTable(color, shadingTableOffset, bg, useMaskedShading);
-							if (g_engine->isAmiga())
-								drawn = remapAmigaPlayfieldIndex(drawn, screenY);
-							s.setPixel(screenX, screenY, drawn);
+							setPixel(s, screenX, screenY, applyShadingTable(color, shadingTableOffset, bg, useMaskedShading));
 						}
 					}
 				}
@@ -3350,10 +3341,7 @@ void View1::drawSpriteTransparent(int shadingTableOffset, uint8 depthThreshold,
 				if (color != 0 && screenX >= 0 && screenX < s.w &&
 					g_engine->_depthMap.getPixel(screenX, screenY) < depthThreshold) {
 					const byte bg = s.getPixel(screenX, screenY);
-					byte drawn = applyShadingTable(color, shadingTableOffset, bg, useMaskedShading);
-					if (g_engine->isAmiga())
-						drawn = remapAmigaPlayfieldIndex(drawn, screenY);
-					s.setPixel(screenX, screenY, drawn);
+					setPixel(s, screenX, screenY, applyShadingTable(color, shadingTableOffset, bg, useMaskedShading));
 				}
 
 				screenX++;


Commit: a307f3d6cb00f4d6ea454efa6cb2f17225ca9028
    https://github.com/scummvm/scummvm/commit/a307f3d6cb00f4d6ea454efa6cb2f17225ca9028
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-08-31T21:42:47+02:00

Commit Message:
MACS2: amiga cleanup

Changed paths:
    engines/macs2/view1.cpp


diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index f247e503480..64e8f09be54 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -67,20 +67,20 @@ void resetObjectDrawBounds(GameObject *obj) {
 		obj->resetDrawBounds();
 }
 
-void plotAmigaUiPixel(Graphics::ManagedSurface &s, int x, int y, byte color) {
+void setPixelClipped(Graphics::ManagedSurface &s, int x, int y, byte color) {
 	if (x < 0 || y < 0 || x >= s.w || y >= s.h)
 		return;
 	s.setPixel(x, y, color);
 }
 
-void drawAmigaUiLine(Graphics::ManagedSurface &s, int x0, int y0, int x1, int y1, byte color) {
+void drawLine(Graphics::ManagedSurface &s, int x0, int y0, int x1, int y1, byte color) {
 	int dx = ABS(x1 - x0);
 	int sx = x0 < x1 ? 1 : -1;
 	int dy = -ABS(y1 - y0);
 	int sy = y0 < y1 ? 1 : -1;
 	int err = dx + dy;
 	for (;;) {
-		plotAmigaUiPixel(s, x0, y0, color);
+		setPixelClipped(s, x0, y0, color);
 		if (x0 == x1 && y0 == y1)
 			break;
 		const int e2 = 2 * err;
@@ -105,10 +105,17 @@ byte amigaPanelBorderColor(int tableIndex) {
 	return remapAmigaCopperIndexToStableUi((byte)copper);
 }
 
-void fillAmigaPanelInterior(Graphics::ManagedSurface &s, int x, int y, int w, int h) {
-	// UAE SOLID panels are opaque dark wood, not scene-through SHADE (that
-	// would be sky-blue at the top of the screen). Pattern sheet BSS is zero;
-	// COLOR21/22 (963/741) EHB matches the emulator interior (~78,51,21).
+void drawAmigaUiPanel(const Common::Point &pos, const Common::Point &size, Graphics::ManagedSurface &s) {
+	const int x = pos.x;
+	const int y = pos.y;
+	const int w = size.x - 1;
+	const int h = size.y - 1;
+	if (w < 1 || h < 1) {
+		return;
+	}
+
+	// background
+	// TODO: the pattern is wrong
 	const byte a = remapAmigaCopperIndexToStableUi(21);
 	const byte bCol = remapAmigaCopperIndexToStableUi(22);
 	byte ehbA = a;
@@ -118,46 +125,27 @@ void fillAmigaPanelInterior(Graphics::ManagedSurface &s, int x, int y, int w, in
 	if (bCol >= 0xF0)
 		ehbB = (byte)(0xE0 + (bCol - 0xF0));
 	for (int oy = 0; oy < h; oy++) {
-		for (int ox = 0; ox < w; ox++)
-			plotAmigaUiPixel(s, x + ox, y + oy, ((ox ^ oy) & 1) ? ehbA : ehbB);
+		for (int ox = 0; ox < w; ox++) {
+			setPixelClipped(s, x + ox, y + oy, ((ox ^ oy) & 1) ? ehbA : ehbB);
+		}
 	}
-}
 
-void drawAmigaPanelBevel(Graphics::ManagedSurface &s, int x, int y, int w, int h) {
+	// borders
 	const byte c0 = amigaPanelBorderColor(0);
 	const byte c1 = amigaPanelBorderColor(1);
-	const byte c2 = amigaPanelBorderColor(2);
-	const byte c3 = amigaPanelBorderColor(3);
-	const byte c4 = amigaPanelBorderColor(4);
+	const byte c2 = amigaPanelBorderColor(3);
+	const byte c3 = amigaPanelBorderColor(4);
 	const int midX = x + (w >> 1);
 	const int midY = y + (h >> 1);
 
-	drawAmigaUiLine(s, x, y, midX, y, c0);
-	drawAmigaUiLine(s, x, y + h, midX, y + h, c3);
-	drawAmigaUiLine(s, midX, y, x + w, y, c1);
-	drawAmigaUiLine(s, midX, y + h, x + w, y + h, c4);
-	drawAmigaUiLine(s, x, y, x, midY, c0);
-	drawAmigaUiLine(s, x + w, y, x + w, midY, c3);
-	drawAmigaUiLine(s, x, midY, x, y + h, c1);
-	drawAmigaUiLine(s, x + w, midY, x + w, y + h + 1, c4);
-
-	plotAmigaUiPixel(s, x + w, y, c2);
-	plotAmigaUiPixel(s, x, y + h, c2);
-	plotAmigaUiPixel(s, midX + 2, y, c0);
-	plotAmigaUiPixel(s, midX + 2, y + h, c3);
-	plotAmigaUiPixel(s, x, midY + 2, c0);
-	plotAmigaUiPixel(s, x + w, midY + 2, c3);
-}
-
-void drawAmigaUiPanel(const Common::Point &pos, const Common::Point &size, Graphics::ManagedSurface &s) {
-	const int x = pos.x;
-	const int y = pos.y;
-	const int w = size.x - 1;
-	const int h = size.y - 1;
-	if (w < 1 || h < 1)
-		return;
-	fillAmigaPanelInterior(s, x, y, w, h);
-	drawAmigaPanelBevel(s, x, y, w, h);
+	drawLine(s, x, y, midX, y, c0);
+	drawLine(s, x, y + h, midX, y + h, c2);
+	drawLine(s, midX, y, x + w, y, c1);
+	drawLine(s, midX, y + h, x + w, y + h, c3);
+	drawLine(s, x, y, x, midY, c0);
+	drawLine(s, x + w, y, x + w, midY, c2);
+	drawLine(s, x, midY, x, y + h, c1);
+	drawLine(s, x + w, midY, x + w, y + h, c3);
 }
 
 void setPixel(Graphics::ManagedSurface &s, int x, int y, byte color) {
@@ -177,8 +165,6 @@ bool buildClippedEraseRect(int32 left, int32 top, uint16 width, uint16 height,
 	if (width == 0 && height == 0)
 		return false;
 
-	// drawAllCharacters @ 1008:90a2: dirty right/bottom are inclusive (+1 padding),
-	// Common::Rect uses exclusive right/bottom (+1 more).
 	const int32 exclRight = left + (int32)width + 2;
 	const int32 exclBottom = top + (int32)height + 2;
 	if (exclRight <= 0 || exclBottom <= 0 || left >= screenW || top >= screenH)
@@ -194,7 +180,6 @@ bool buildClippedEraseRect(int32 left, int32 top, uint16 width, uint16 height,
 	if (clipLeft < -32768 || clipTop < -32768 || clipRight > 32767 || clipBottom > 32767)
 		return false;
 
-	// Avoid Common::Rect(x1,y1,x2,y2) constructor (asserts on invalid input).
 	out.left = (int16)clipLeft;
 	out.top = (int16)clipTop;
 	out.right = (int16)clipRight;
@@ -355,7 +340,7 @@ void View1::openInventory(GameObject *newInventorySource) {
 	}
 
 	setInventorySource(newInventorySource);
-	_pendingPanelRequest = kPanelRequestNone; // Binary: g_wPendingPanelRequest = 0
+	_pendingPanelRequest = kPanelRequestNone;
 
 	// SCUMM verb UI: protagonist inventory is always visible in the strip.
 	if (hasPersistentActionBar() && newInventorySource->_index == Scenes::instance()._currentActorIndex) {
@@ -373,7 +358,6 @@ void View1::openInventory(GameObject *newInventorySource) {
 	_activeInventoryItem = nullptr;
 	g_engine->_scriptExecutor->_inventoryActionFlag = false;
 	g_engine->_scriptExecutor->_inventoryCombineFlag = false;
-	// Binary drawProtagonistInventoryPanel (1008:45aa): unconditionally calls setCursorMode(0x15)
 	g_engine->setCursorMode(Script::MouseMode::Use);
 	updateCursor();
 	redraw();
@@ -651,7 +635,6 @@ AnimFrame *View1::getInventoryIcon(GameObject *gameObject) {
 }
 
 void View1::drawDarkRectangle(uint16 x, uint16 y, uint16 width, uint16 height) {
-	// drawAnimFrameScaled @ 1010:1399: remap each background pixel through per-scene 256-byte table
 	Graphics::ManagedSurface s = getSurface();
 	for (uint16 xOffset = 0; xOffset < width; xOffset++) {
 		for (uint16 yOffset = 0; yOffset < height; yOffset++) {
@@ -733,11 +716,7 @@ void View1::drawCurrentSpeaker(Graphics::ManagedSurface &s) {
 	AnimFrame *rightPortrait = currentSpeechActData.speaker->getCurrentPortrait(true, 0);
 
 	Common::Point pos = currentSpeechActData.position;
-	if (g_engine->isAmiga()) {
-		drawAmigaUiPanel(currentSpeechActData.position,
-						 Common::Point(frame->_width + 2, frame->_height + 2), s);
-		pos += Common::Point(1, 1);
-	} else {
+	if (!g_engine->isAmiga()) {
 		const int portraitWidth = MAX<int>(leftPortrait ? leftPortrait->_width : 0, rightPortrait ? rightPortrait->_width : 0);
 		const int portraitHeight = MAX<int>(leftPortrait ? leftPortrait->_height : 0, rightPortrait ? rightPortrait->_height : 0);
 		const int borderPad = g_engine->portraitBorderPad();
@@ -894,9 +873,6 @@ void View1::drawOverlayTextEntries() {
 }
 
 void View1::showStringBox(const Common::StringArray &sa) {
-	// This calculation can be found at l0037_B368:
-	// int borderWidth = 10;
-	// int padding = 3;
 	const int padW = g_engine->dialogPadW();
 	const int padH = g_engine->dialogPadH();
 	const int textInset = g_engine->dialogTextInset();
@@ -910,7 +886,6 @@ void View1::showStringBox(const Common::StringArray &sa) {
 
 	Graphics::ManagedSurface s = getSurface();
 	drawBorder(_stringBoxPosition, Common::Point(totalWidth, totalHeight), s);
-	// TODO range based
 	int lineOffset = _stringBoxPosition.y + textInset;
 	for (auto iter = sa.begin(); iter < sa.end(); iter++) {
 		logRenderedText("TextBox", _stringBoxPosition.x + textInset, lineOffset, *iter);




More information about the Scummvm-git-logs mailing list