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

mgerhardy noreply at scummvm.org
Thu Sep 3 17:52:53 UTC 2026


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

Summary:
746cb6513f MACS2: extract pathfinding component
8a1680afbe MACS2: extract text code
c7ca3456d2 MACS2: don't read opl values from exe, but embed them in the engine code


Commit: 746cb6513f171b76afc2c2e2003708d74f57a5ea
    https://github.com/scummvm/scummvm/commit/746cb6513f171b76afc2c2e2003708d74f57a5ea
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-09-03T19:50:40+02:00

Commit Message:
MACS2: extract pathfinding component

Changed paths:
  A engines/macs2/pathfinding.cpp
  A engines/macs2/pathfinding.h
    engines/macs2/amiga_resources.cpp
    engines/macs2/character.cpp
    engines/macs2/character.h
    engines/macs2/debugtools.cpp
    engines/macs2/macs2.cpp
    engines/macs2/macs2.h
    engines/macs2/module.mk
    engines/macs2/saveload.cpp
    engines/macs2/scriptexecutor.cpp
    engines/macs2/view1.cpp


diff --git a/engines/macs2/amiga_resources.cpp b/engines/macs2/amiga_resources.cpp
index a566d8a4860..200d8639ae3 100644
--- a/engines/macs2/amiga_resources.cpp
+++ b/engines/macs2/amiga_resources.cpp
@@ -112,13 +112,13 @@ bool Macs2Engine::loadAmigaSceneBackground(uint32 sceneResourceId) {
 
 	Common::Rect screenRect(0, 0, kScreenWidth, kGameHeight);
 	_depthMap.fillRect(screenRect, 0);
-	_pathfindingMap.fillRect(screenRect, 0);
+	_pathfinding._map.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)) {
-		blitMap(_pathfindingMap, pathMap);
+		blitMap(_pathfinding._map, pathMap);
 		blitMap(_depthMap, depthMap);
 		blitMap(_shadowMap, shadowMap);
 	}
@@ -149,12 +149,12 @@ bool Macs2Engine::loadAmigaSceneBackground(uint32 sceneResourceId) {
 			   (uint)sceneResourceId);
 	}
 
-	_pathfindingPoints.clear();
-	_numPathfindingPoints = 0;
+	_pathfinding._points.clear();
+	_pathfinding._numPoints = 0;
 	uint16 numPfPoints = 0;
 	Common::Array<AmigaPathfindingNode> pfNodes;
 	if (extractAmigaMxmmScenePathfinding(mxmm.data(), size, numPfPoints, pfNodes)) {
-		_numPathfindingPoints = numPfPoints;
+		_pathfinding._numPoints = numPfPoints;
 		for (uint i = 0; i < pfNodes.size(); i++) {
 			PathfindingPoint current;
 			current._index = (uint16)i;
@@ -166,7 +166,7 @@ bool Macs2Engine::loadAmigaSceneBackground(uint32 sceneResourceId) {
 				if (pfNodes[i].adjacent[j] != 0)
 					current._adjacentPoints.push_back(pfNodes[i].adjacent[j]);
 			}
-			_pathfindingPoints.push_back(current);
+			_pathfinding._points.push_back(current);
 		}
 	}
 
@@ -194,7 +194,7 @@ bool Macs2Engine::loadAmigaSceneBackground(uint32 sceneResourceId) {
 		   "Amiga: loaded native MM_%04u (script %u bytes, strings %u bytes, pfNodes %u, "
 		   "hotspots %u, walk %u/%u/%u)",
 		   (uint)sceneResourceId, (uint)_amigaPendingSceneScript.size(),
-		   (uint)_amigaPendingSceneStrings.size(), (uint)_numPathfindingPoints, (uint)_numHotspots,
+		   (uint)_amigaPendingSceneStrings.size(), (uint)_pathfinding._numPoints, (uint)_numHotspots,
 		   (uint)_walkDepthThresholdY, (uint)_walkDepthScaleFactor, (uint)_walkBaseSpeedPct);
 	return true;
 }
@@ -671,7 +671,7 @@ void Macs2Engine::readAmigaResources() {
 
 	_sceneBackground.create(kScreenWidth, kGameHeight, Graphics::PixelFormat::createFormatCLUT8());
 	_depthMap.create(kScreenWidth, kGameHeight, Graphics::PixelFormat::createFormatCLUT8());
-	_pathfindingMap.create(kScreenWidth, kGameHeight, Graphics::PixelFormat::createFormatCLUT8());
+	_pathfinding.createMap(kScreenWidth, kGameHeight);
 	_shadowMap.create(kScreenWidth, kGameHeight, Graphics::PixelFormat::createFormatCLUT8());
 	_hotspotMap.create(kScreenWidth, kGameHeight, Graphics::PixelFormat::createFormatCLUT8());
 	_shadingTable.resize(0x800);
@@ -680,7 +680,7 @@ void Macs2Engine::readAmigaResources() {
 	buildAmigaPanelRemapTable();
 
 	_numHotspots = 0;
-	_numPathfindingPoints = 0;
+	_pathfinding._numPoints = 0;
 	_scenePaletteMode = 1;
 	_paletteDarkenPercent = 0;
 
diff --git a/engines/macs2/character.cpp b/engines/macs2/character.cpp
index b3b92f41fdb..5d88f38e15a 100644
--- a/engines/macs2/character.cpp
+++ b/engines/macs2/character.cpp
@@ -27,6 +27,7 @@
 #include "macs2/events.h"
 #include "macs2/macs2.h"
 #include "macs2/macs2_constants.h"
+#include "macs2/pathfinding.h"
 #include "macs2/scriptexecutor.h"
 #include "macs2/view1.h"
 
@@ -153,146 +154,41 @@ Character::Character() : _pathfindingOverlay(g_engine->screenWidth() * g_engine-
 }
 
 bool Character::calculatePath(Common::Point target) {
-	// Binary calculatePath (1008:1966). Params: charY, charX, finalDestY, finalDestX, actorIndex.
-	// The binary operates on the runtime struct directly; we store equivalent state in _path etc.
-	const Common::Point &charPos = _gameObject->_position;
-	const int nodeCount = g_engine->getPathfindingNodeCount();
-
-	// Step 1: Mark reachability anchored on FINAL DESTINATION (not character)
-	// scene[i + 0x50C2] = isPathWalkable(finalDest, node[i])
-	bool reachable[kPathNodeSlots + 1] = {};
-	for (int i = 1; i <= nodeCount; i++) {
-		const Common::Point &nodePos = g_engine->_pathfindingPoints[i - 1]._position;
-		reachable[i] = g_engine->isPathWalkable(target.y, target.x, nodePos.y, nodePos.x);
-	}
-
-	// Step 2: Find best entry node (lowest combined distance to both source and dest)
-	int bestCost = 0x7777;
-	int bestNode = 0;
-	for (int i = 1; i <= nodeCount; i++) {
-		const Common::Point &nodePos = g_engine->_pathfindingPoints[i - 1]._position;
-		int costToDest = g_engine->euclideanDistance(nodePos, target);
-		int costToChar = g_engine->euclideanDistance(nodePos, charPos);
-		if (costToDest + costToChar < bestCost) {
-			// Verify this node can connect source to target
-			// Binary calls canNodeConnectSourceToTarget(destY, destX, charY, charX, i)
-			// due to calculatePath being invoked with swapped source/dest params.
-			// This means the gate check is "can node see CHARACTER" and the flood-fill
-			// checks "any node reachable from DEST" AND "any node visible from CHARACTER".
-			// TODO: validate this with a playthought:
-			//   PVS-Studio V764: Possible incorrect order of arguments passed to
-			//   'canNodeConnectSourceToTarget' function: 'target' and 'charPos'.
-			// I didn't had any issues in previous runs
-			if (canNodeConnectSourceToTarget(i, target, charPos, reachable, nodeCount)) {
-				// Recompute cost (binary does this twice)
-				costToDest = g_engine->euclideanDistance(nodePos, target);
-				costToChar = g_engine->euclideanDistance(nodePos, charPos);
-				bestCost = costToDest + costToChar;
-				bestNode = i;
-			}
-		}
-	}
-
-	if (bestNode == 0) {
-		// No path found - go directly to target
-		// Binary: pathNodeCount=0, pathIndex=1
-		_path.clear();
-		_currentPathIndex = 1;
-		_targetPosition = target;
-		return false;
-	}
-
-	// Step 3: smoothPath - build path from bestNode toward a reachable node
-	_path.clear();
-	_path.push_back(bestNode);
-	int currentNode = bestNode;
-	while (!reachable[currentNode]) {
-		const PathfindingPoint &curPt = g_engine->_pathfindingPoints[currentNode - 1];
-		int localBestCost = 0x7777;
-		int nextNode = currentNode;
-		for (uint a = 0; a < curPt._adjacentPoints.size(); a++) {
-			const int adjIdx = curPt._adjacentPoints[a];
-			const int cost = g_engine->computeMinCostToReachable(adjIdx, 0x7fff, _gameObject->_index, reachable, nodeCount, target);
-			const int edgeCost = g_engine->walkableDistance(adjIdx, currentNode);
-			if (cost + edgeCost < localBestCost) {
-				nextNode = adjIdx;
-				localBestCost = cost + edgeCost;
-			}
-		}
-		currentNode = nextNode;
-		_path.push_back(currentNode);
-		if (_path.size() > kPathNodeSlots)
-			break; // safety
-	}
+	const PathRoute route = g_engine->_pathfinding.calculateRoute(_gameObject->_position, target);
+	_path = route.nodes;
+	_currentPathIndex = route.startIndex;
+	_targetPosition = route.found ? route.firstWaypoint : target;
+	return route.found;
+}
 
-	// Step 4: Validate path - consecutive nodes must be walkable to each other
-	for (uint i = 0; i + 1 < _path.size(); i++) {
-		const Common::Point &p1 = g_engine->_pathfindingPoints[_path[i + 1] - 1]._position;
-		const Common::Point &p2 = g_engine->_pathfindingPoints[_path[i] - 1]._position;
-		if (!g_engine->isPathWalkable(p1.y, p1.x, p2.y, p2.x)) {
-			// Path invalid - abort, go directly to target
-			_path.clear();
-			_targetPosition = target;
-			return false;
-		}
-	}
+void Character::setWalkTarget(const Common::Point &target, bool snap) {
+	Common::Point dest = target;
+	const Common::Point current = getPosition();
+	Pathfinding &pf = g_engine->_pathfinding;
+	if (snap)
+		pf.snapToWalkable(&dest.y, &dest.x, current.y, current.x);
 
-	// Step 5: Skip-forward optimization - skip nodes the character can already reach directly.
-	// Binary: checks isPathWalkable(nextNode, charPos) - "can character see the next node?"
-	// Note: binary's calculatePath is called with swapped params, so its 'finalDest' param
-	// is actually the character position.
+	_pathFinalDestination = dest;
 	_currentPathIndex = 0;
-	while (_currentPathIndex + 1 < (int16)_path.size()) {
-		const Common::Point &nextNodePos = g_engine->_pathfindingPoints[_path[_currentPathIndex + 1] - 1]._position;
-		if (!g_engine->isPathWalkable(nextNodePos.y, nextNodePos.x, charPos.y, charPos.x))
-			break;
-		_currentPathIndex++;
-	}
-
-	// Set immediate target to the current path node
-	const Common::Point &firstTarget = g_engine->_pathfindingPoints[_path[_currentPathIndex] - 1]._position;
-	_targetPosition = firstTarget;
-	return true;
-}
-
-bool Character::canNodeConnectSourceToTarget(uint16 nodeIndex, const Common::Point &charPos, const Common::Point &target, const bool *reachable, int nodeCount) {
-	// Checks if node can connect source (charPos) to target:
-	// 1. Node must be able to see the target
-	// 2. Flood-fill connected component from node
-	// 3. Some node in component must see target AND some node must be seen from source
-	const Common::Point &nodePos = g_engine->_pathfindingPoints[nodeIndex - 1]._position;
-	if (!g_engine->isPathWalkable(nodePos.y, nodePos.x, target.y, target.x))
-		return false;
+	_path.clear();
 
-	// Flood-fill connected nodes
-	bool visited[kPathNodeSlots + 1] = {};
-	floodFillConnectedNodes(nodeIndex, visited, nodeCount);
-
-	// Check both conditions
-	bool anySeesTarget = false;
-	bool anySeenFromSource = false;
-	for (int i = 1; i <= nodeCount; i++) {
-		if (!visited[i])
-			continue;
-		const Common::Point &p = g_engine->_pathfindingPoints[i - 1]._position;
-		if (g_engine->isPathWalkable(p.y, p.x, target.y, target.x))
-			anySeesTarget = true;
-		if (g_engine->isPathWalkable(charPos.y, charPos.x, p.y, p.x))
-			anySeenFromSource = true;
+	const bool direct = pf.isLineWalkable(dest.y, dest.x, current.y, current.x);
+	if (snap) {
+		if (direct || Pathfinding::isWalkabilityBlocking(pf.walkabilityAt(dest.y, dest.x)))
+			_targetPosition = dest;
+		else if (!calculatePath(dest))
+			_targetPosition = dest;
+	} else {
+		if (!direct && Pathfinding::isWalkabilityWalkable(pf.walkabilityAt(dest.y, dest.x)))
+			calculatePath(dest);
+		if (_path.empty())
+			_targetPosition = dest;
 	}
-	return anySeesTarget && anySeenFromSource;
-}
 
-void Character::floodFillConnectedNodes(int nodeIndex, bool *visited, int nodeCount) {
-	if (nodeIndex < 1 || nodeIndex > nodeCount)
-		return;
-	if (visited[nodeIndex])
-		return;
-	visited[nodeIndex] = true;
-	const PathfindingPoint &pt = g_engine->_pathfindingPoints[nodeIndex - 1];
-	for (uint i = 0; i < pt._adjacentPoints.size(); i++) {
-		floodFillConnectedNodes(pt._adjacentPoints[i], visited, nodeCount);
-	}
+	_stepDeltaX = (int16)ABS(_targetPosition.x - current.x);
+	_stepDeltaY = (int16)ABS(_targetPosition.y - current.y);
+	_stepError = 0;
+	_stepDirectionSet = false;
 }
 
 const Common::Point &Character::getPosition() const {
@@ -323,7 +219,7 @@ uint16 Character::getVerticalOffset() const {
 bool Character::walkAlongPath() {
 	if (_currentPathIndex >= 0 && _currentPathIndex < (int16)_path.size()) {
 		const uint16 snapIdx = _path[_currentPathIndex];
-		const Common::Point &snapPos = g_engine->_pathfindingPoints[snapIdx - 1]._position;
+		const Common::Point &snapPos = g_engine->_pathfinding._points[snapIdx - 1]._position;
 		_gameObject->_position = snapPos;
 	}
 	_currentPathIndex++;
@@ -337,7 +233,7 @@ bool Character::walkAlongPath() {
 		return false; // No more path segments after this
 	}
 	const uint16 nodeIdx = _path[_currentPathIndex];
-	const Common::Point &nodePos = g_engine->_pathfindingPoints[nodeIdx - 1]._position;
+	const Common::Point &nodePos = g_engine->_pathfinding._points[nodeIdx - 1]._position;
 	_targetPosition = nodePos;
 	_stepDeltaX = (int16)ABS(_targetPosition.x - _gameObject->_position.x);
 	_stepDeltaY = (int16)ABS(_targetPosition.y - _gameObject->_position.y);
@@ -431,30 +327,9 @@ void Character::startLerpTo(const Common::Point &target, uint32 duration, bool i
 
 void Character::startPickup(Macs2::GameObject *object) {
 	_pickedUpObject = object;
-	_pathFinalDestination = getObjectEffectivePosition(object);
 	_pickupFrameCounter = 0;
 	_pickupItemTransferred = false;
-
-	const Common::Point &current = getPosition();
-	const int16 destX = _pathFinalDestination.x;
-	const int16 destY = _pathFinalDestination.y;
-
-	_currentPathIndex = 0;
-	_path.clear();
-
-	const bool directPath = g_engine->isPathWalkable(destY, destX, current.y, current.x);
-	if (!directPath && Macs2Engine::isWalkabilityWalkable(g_engine->getWalkabilityAt(destY, destX))) {
-		calculatePath(Common::Point(destX, destY));
-	}
-
-	if (_path.empty()) {
-		_targetPosition = _pathFinalDestination;
-	}
-
-	_stepDeltaX = (int16)ABS(_targetPosition.x - current.x);
-	_stepDeltaY = (int16)ABS(_targetPosition.y - current.y);
-	_stepError = 0;
-	_stepDirectionSet = false;
+	setWalkTarget(getObjectEffectivePosition(object), false);
 }
 
 bool Character::hasPendingVerticalMotion() const {
diff --git a/engines/macs2/character.h b/engines/macs2/character.h
index a32b1a56236..5dad7d68258 100644
--- a/engines/macs2/character.h
+++ b/engines/macs2/character.h
@@ -47,12 +47,10 @@ private:
 	uint16 lookupWalkability(const Common::Point &p) const;
 	bool shouldStepVerticalMotion() const;
 	bool isAnimationMirrored() const;
-	void floodFillConnectedNodes(int nodeIndex, bool *visited, int nodeCount);
 	// Returns false if we are at the end of the path already or the path is not valid
 	bool walkAlongPath();
 	void startLerpTo(const Common::Point &target, uint32 duration, bool ignoreObstacles = false);
 	bool isWalkable(const Common::Point &p) const;
-	bool canNodeConnectSourceToTarget(uint16 nodeIndex, const Common::Point &charPos, const Common::Point &target, const bool *reachable, int nodeCount);
 
 public:
 	Character();
@@ -87,6 +85,7 @@ public:
 	uint16 _motionStartVerticalOffset = 0;
 
 	bool calculatePath(Common::Point target);
+	void setWalkTarget(const Common::Point &target, bool snap);
 	void startPickup(Macs2::GameObject *object);
 
 	const Common::Point &getPosition() const;
diff --git a/engines/macs2/debugtools.cpp b/engines/macs2/debugtools.cpp
index 599b5dd6af0..3a1b57c61fc 100644
--- a/engines/macs2/debugtools.cpp
+++ b/engines/macs2/debugtools.cpp
@@ -1309,17 +1309,17 @@ static void showSceneMapsWindow() {
 		static Graphics::ManagedSurface overlayComposite;
 
 		if (selectedTab == 0) {
-			surface = &g_engine->_pathfindingMap;
+			surface = &g_engine->_pathfinding._map;
 		} else if (selectedTab == 1) {
 			surface = &g_engine->_depthMap;
 		} else if (selectedTab == 2) {
 			// Composite: pathfinding map + character path overlay + pathfinding points
-			overlayComposite.copyFrom(g_engine->_pathfindingMap);
+			overlayComposite.copyFrom(g_engine->_pathfinding._map);
 			View1 *view = (View1 *)g_engine->findView("View1");
 			if (view) {
 				// Draw pathfinding point nodes and connections
 				for (int i = 0; i < 16; i++) {
-					PathfindingPoint &pt = g_engine->_pathfindingPoints[i];
+					PathfindingPoint &pt = g_engine->_pathfinding._points[i];
 					if (pt._position.x >= 0 && pt._position.x < kScreenWidth && pt._position.y >= 0 && pt._position.y < kGameHeight) {
 						// Draw cross at node
 						for (int d = -2; d <= 2; d++) {
@@ -1335,7 +1335,7 @@ static void showSceneMapsWindow() {
 						for (uint8 adj : pt._adjacentPoints) {
 							if (adj == 0 || adj > 16)
 								continue;
-							PathfindingPoint &other = g_engine->_pathfindingPoints[adj - 1];
+							PathfindingPoint &other = g_engine->_pathfinding._points[adj - 1];
 							overlayComposite.drawLine(pt._position.x, pt._position.y, other._position.x, other._position.y, 0xFE);
 						}
 					}
@@ -1403,7 +1403,7 @@ static void showSceneMapsWindow() {
 					ImDrawList *dl = ImGui::GetWindowDrawList();
 					ImVec2 imgOrigin = ImGui::GetItemRectMin();
 					for (int i = 0; i < 16; i++) {
-						PathfindingPoint &pt = g_engine->_pathfindingPoints[i];
+						PathfindingPoint &pt = g_engine->_pathfinding._points[i];
 						if (pt._position.x >= 0 && pt._position.x < kScreenWidth && pt._position.y >= 0 && pt._position.y < kGameHeight) {
 							char buf[4];
 							snprintf(buf, sizeof(buf), "%d", i);
@@ -1528,7 +1528,7 @@ static void showSceneMapsWindow() {
 		ImGui::Text("_walkDepthThresholdY=%u  _walkDepthScaleFactor=%u  _walkBaseSpeedPct=%u",
 					g_engine->_walkDepthThresholdY, g_engine->_walkDepthScaleFactor, g_engine->_walkBaseSpeedPct);
 		ImGui::Text("Pathfinding points: %u  Path nodes: %u",
-					(uint)g_engine->_pathfindingPoints.size(), (uint)g_engine->_path.size());
+					(uint)g_engine->_pathfinding._points.size(), (uint)g_engine->_path.size());
 
 		// Node detail table
 		if (ImGui::CollapsingHeader("Node Graph", ImGuiTreeNodeFlags_DefaultOpen)) {
@@ -1536,8 +1536,8 @@ static void showSceneMapsWindow() {
 			Character *protagonist = view ? view->getCharacterByIndex(Scenes::instance()._currentActorIndex) : nullptr;
 			Common::Point charPos = protagonist ? protagonist->getPosition() : Common::Point(0, 0);
 
-			for (int i = 0; i < (int)g_engine->_pathfindingPoints.size(); i++) {
-				const PathfindingPoint &pt = g_engine->_pathfindingPoints[i];
+			for (int i = 0; i < (int)g_engine->_pathfinding._points.size(); i++) {
+				const PathfindingPoint &pt = g_engine->_pathfinding._points[i];
 				// Check reachability from character
 				bool reachable = protagonist && g_engine->isPathWalkable(charPos.y, charPos.x, pt._position.y, pt._position.x);
 				// Check if node is in current path
diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 343e6da93ed..754b73fa498 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -199,7 +199,7 @@ void Macs2Engine::readResourceFile() {
 		const int gh = gameHeight();
 		_sceneBackground.create(sw, gh, Graphics::PixelFormat::createFormatCLUT8());
 		_depthMap.create(sw, gh, Graphics::PixelFormat::createFormatCLUT8());
-		_pathfindingMap.create(sw, gh, Graphics::PixelFormat::createFormatCLUT8());
+		_pathfinding.createMap(sw, gh);
 		_shadowMap.create(sw, gh, Graphics::PixelFormat::createFormatCLUT8());
 		_hotspotMap.create(sw, gh, Graphics::PixelFormat::createFormatCLUT8());
 		changeScene(Scenes::instance()._currentSceneIndex);
@@ -1004,11 +1004,11 @@ bool Macs2Engine::loadSceneGraphics(uint32 sceneIndex) {
 
 		_sceneBackground.fillRect(Common::Rect(0, 0, kScreenWidth, kGameHeight), 0);
 		_depthMap.fillRect(Common::Rect(0, 0, kScreenWidth, kGameHeight), 0);
-		_pathfindingMap.fillRect(Common::Rect(0, 0, kScreenWidth, kGameHeight), 0);
+		_pathfinding._map.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);
 		_numHotspots = 0;
-		_numPathfindingPoints = 0;
+		_pathfinding._numPoints = 0;
 		_walkDepthThresholdY = 100;
 		_walkDepthScaleFactor = 100;
 		_walkBaseSpeedPct = 100;
@@ -1106,7 +1106,7 @@ bool Macs2Engine::loadSceneGraphicsV1(uint32 sceneIndex) {
 
 	Graphics::ManagedSurface pathfindingRLE = readRLEImage(_fileStream->pos(), _fileStream);
 	// Walkability/pathfinding map at scene offset 0x2017
-	_pathfindingMap.blitFrom(pathfindingRLE);
+	_pathfinding._map.blitFrom(pathfindingRLE);
 
 	// Shadow/shading intensity map for character rendering
 	Graphics::ManagedSurface shadowRLE = readRLEImage(_fileStream->pos(), _fileStream);
@@ -1118,7 +1118,7 @@ bool Macs2Engine::loadSceneGraphicsV1(uint32 sceneIndex) {
 
 	// Pretty sure that this is the pathfinding points. We address them starting
 	// Load pathfinding nodes (16 entries x 10 bytes at scene+0x5023)
-	_pathfindingPoints.clear();
+	_pathfinding._points.clear();
 	for (int i = 0; i < 16; i++) {
 		PathfindingPoint current;
 		current._index = i;
@@ -1130,7 +1130,7 @@ bool Macs2Engine::loadSceneGraphicsV1(uint32 sceneIndex) {
 		current._adjacentPoints.clear();
 		for (uint16 j = 0; j < numConnections && j < 4; j++)
 			current._adjacentPoints.push_back(adj[j]);
-		_pathfindingPoints.push_back(current);
+		_pathfinding._points.push_back(current);
 	}
 
 	_numHotspots = _fileStream->readUint16LE();
@@ -1143,7 +1143,7 @@ bool Macs2Engine::loadSceneGraphicsV1(uint32 sceneIndex) {
 	readBackgroundAnimations(_fileStream);
 	updateAllBackgroundAnimationDepthMaps();
 
-	_numPathfindingPoints = _fileStream->readUint16LE();
+	_pathfinding._numPoints = _fileStream->readUint16LE();
 
 	// Offset 51F9h
 	_fileStream->readUint16LE();
@@ -1237,7 +1237,7 @@ bool Macs2Engine::loadSceneGraphicsV2(uint32 sceneIndex) {
 	Graphics::ManagedSurface half;
 	if (!readMegaPicImage(stream, kScreenWidth, kGameHeight, half))
 		return false;
-	upscaleHalfRes(half, _pathfindingMap);
+	upscaleHalfRes(half, _pathfinding._map);
 
 	if (!readMegaPicImage(stream, kScreenWidth, kGameHeight, half))
 		return false;
@@ -1247,7 +1247,7 @@ bool Macs2Engine::loadSceneGraphicsV2(uint32 sceneIndex) {
 		return false;
 	upscaleHalfRes(half, _hotspotMap);
 
-	_pathfindingPoints.clear();
+	_pathfinding._points.clear();
 	for (int i = 0; i < 16; i++) {
 		PathfindingPoint current;
 		current._index = i;
@@ -1260,7 +1260,7 @@ bool Macs2Engine::loadSceneGraphicsV2(uint32 sceneIndex) {
 		current._adjacentPoints.clear();
 		for (uint16 j = 0; j < numConnections && j < 4; j++)
 			current._adjacentPoints.push_back(adj[j]);
-		_pathfindingPoints.push_back(current);
+		_pathfinding._points.push_back(current);
 	}
 	stream->skip(352);
 
@@ -1320,9 +1320,9 @@ bool Macs2Engine::loadSceneGraphicsV2(uint32 sceneIndex) {
 		}
 	}
 
-	_numPathfindingPoints = stream->readUint16LE();
-	if (_numPathfindingPoints == 0 || _numPathfindingPoints > 16)
-		_numPathfindingPoints = 16;
+	_pathfinding._numPoints = stream->readUint16LE();
+	if (_pathfinding._numPoints == 0 || _pathfinding._numPoints > 16)
+		_pathfinding._numPoints = 16;
 	(void)stream->readUint16LE();
 	(void)stream->readUint16LE();
 	_walkDepthThresholdY = (uint16)(stream->readUint16LE() << 1);
@@ -1356,7 +1356,7 @@ void Macs2Engine::changeScene(uint32 newSceneIndex, bool executeScript) {
 	// Release old scene resources
 	_backgroundAnimations.clear();
 	_backgroundAnimationsBlobs.clear();
-	memset(_areaOverrides, 0, sizeof(_areaOverrides));
+	_pathfinding.clearAreaOverrides();
 
 	if (isAmiga()) {
 		// Amiga scripts use scene ids = MM_resource_id + 1 (Ghidra FUN_002215fa
@@ -1477,7 +1477,7 @@ void Macs2Engine::changeScene(uint32 newSceneIndex, bool executeScript) {
 		Scenes::instance()._currentSceneSpecialAnimOffsets.clear();
 		_scriptExecutor->setScript(Scenes::instance()._currentSceneScript);
 
-		_pathfindingOverrides.clear();
+		_pathfinding.clearWalkOverrides();
 		for (uint i = 0; i < _hotspotOverrides.size(); i++) {
 			_hotspotOverrides[i] = 0xFFFF;
 		}
@@ -1584,7 +1584,7 @@ void Macs2Engine::changeScene(uint32 newSceneIndex, bool executeScript) {
 	// Reset overrides before running the new scene's script (original placement:
 	// memsetBytes(0, 200, sceneData+0x528D) and memsetBytes(0xffff, 0x20, sceneData+0x5BD3)
 	// happen after all scene data is loaded, before script execution)
-	_pathfindingOverrides.clear();
+	_pathfinding.clearWalkOverrides();
 	for (uint i = 0; i < _hotspotOverrides.size(); i++) {
 		_hotspotOverrides[i] = 0xFFFF;
 	}
@@ -2032,28 +2032,6 @@ bool Macs2Engine::findGlyph(char c, GlyphData &out) const {
 	return false;
 }
 
-// getWalkabilityAt (1008:0e8c)
-// Params: (param_1=y, param_2=x)
-// Bounds: x<0 || x>=screenWidth || y<0 || y>=gameHeight -> return 0
-// Lookup: scene[y*4 + 0x2017] -> row pointer, then byte at [rowPtr + x]
-// Values 0xC8..0xEF: override range - checks scene[value*5 + 0x4EA5]:
-//   If override disabled (flag==0): returns 0xFF
-//   If override enabled (flag!=0): returns scene[value*5 + 0x4EA6]
-uint16 Macs2Engine::getWalkabilityAt(int16 y, int16 x) {
-	if (x < 0 || x >= screenWidth() || y < 0 || y >= gameHeight() || _pathfindingMap.w == 0) {
-		return 0;
-	}
-	uint16 value = _pathfindingMap.getPixel(x, y);
-	if (value >= 0xC8 && value <= 0xEF) {
-		uint16 overrideResult;
-		if (getPathfindingOverride(value, overrideResult)) {
-			return overrideResult;
-		}
-		return 0xFF;
-	}
-	return value;
-}
-
 void Macs2Engine::updateBackgroundAnimationDepthMap(size_t animIndex) {
 	if (isV2() || _sceneDepthMap.w == 0 || animIndex >= _backgroundAnimations.size()) {
 		return;
@@ -2126,346 +2104,6 @@ void Macs2Engine::updateAllBackgroundAnimationDepthMaps() {
 	}
 }
 
-// Params: (pTargetY, pTargetX, charY, charX)
-// Modifies *pTargetY and *pTargetX in place.
-void Macs2Engine::snapToWalkablePosition(int16 *pTargetY, int16 *pTargetX, int16 charY, int16 charX) {
-	int16 savedX = *pTargetX;
-	int16 savedY = *pTargetY;
-	const int16 maxY = (int16)gameHeightLast();
-	const int16 maxX = (int16)screenWidthLast();
-
-	// Phase 1: Scan downward with depth constraint
-	// Condition: walkability >= 200 OR (targetY - walkability) < savedY
-	while (true) {
-		uint16 w = getWalkabilityAt(*pTargetY, savedX);
-		if (isWalkabilityWalkable(w) && (*pTargetY - (int16)w >= savedY)) {
-			break;
-		}
-		if (*pTargetY >= maxY) {
-			break;
-		}
-		*pTargetY = *pTargetY + 1;
-	}
-
-	// Phase 2: Continue scanning to bottom for best depth match
-	int16 scanY = *pTargetY;
-	while (scanY <= maxY) {
-		uint16 w = getWalkabilityAt(scanY, *pTargetX);
-		if (scanY - (int16)w == savedY) {
-			*pTargetY = scanY;
-		}
-		if (scanY == maxY) {
-			break;
-		}
-		scanY++;
-	}
-
-	// Phase 3: If at screen bottom and still non-walkable, scan upward
-	if (*pTargetY == maxY) {
-		uint16 w = getWalkabilityAt(*pTargetY, *pTargetX);
-		if (isWalkabilityBlocking(w)) {
-			while (isWalkabilityBlocking(w) && *pTargetY > 0) {
-				*pTargetY = *pTargetY - 1;
-				w = getWalkabilityAt(*pTargetY, *pTargetX);
-			}
-		}
-	}
-
-	// Phase 4: If still non-walkable, scan X toward character
-	uint16 w = getWalkabilityAt(*pTargetY, *pTargetX);
-	if (isWalkabilityBlocking(w)) {
-		*pTargetY = savedY;
-		if (charX < *pTargetX) {
-			while (true) {
-				uint16 w2 = getWalkabilityAt(*pTargetY, *pTargetX);
-				if (isWalkabilityWalkable(w2)) {
-					break;
-				}
-				if (*pTargetX <= 0) {
-					break;
-				}
-				*pTargetX = *pTargetX - 1;
-			}
-		} else {
-			while (true) {
-				uint16 w2 = getWalkabilityAt(*pTargetY, *pTargetX);
-				if (isWalkabilityWalkable(w2)) {
-					break;
-				}
-				if (*pTargetX >= maxX) {
-					break;
-				}
-				*pTargetX = *pTargetX + 1;
-			}
-		}
-		// Phase 5: If all failed, fall back to character position
-		uint16 w2 = getWalkabilityAt(*pTargetY, *pTargetX);
-		if (isWalkabilityBlocking(w2)) {
-			*pTargetX = charX;
-			*pTargetY = charY;
-		}
-	}
-
-	// Phase 6: Gradient-based wall push
-	int16 pushX = 0;
-	int16 pushY = 0;
-	if (isWalkabilityBlocking(getWalkabilityAt(*pTargetY, *pTargetX + 1))) {
-		pushX--;
-	}
-	if (isWalkabilityBlocking(getWalkabilityAt(*pTargetY, *pTargetX - 1))) {
-		pushX++;
-	}
-	if (isWalkabilityBlocking(getWalkabilityAt(*pTargetY + 1, *pTargetX))) {
-		pushY--;
-	}
-	if (isWalkabilityBlocking(getWalkabilityAt(*pTargetY - 1, *pTargetX))) {
-		pushY++;
-	}
-	if (isWalkabilityBlocking(getWalkabilityAt(*pTargetY, *pTargetX + 2))) {
-		pushX--;
-	}
-	if (isWalkabilityBlocking(getWalkabilityAt(*pTargetY, *pTargetX - 2))) {
-		pushX++;
-	}
-	if (isWalkabilityBlocking(getWalkabilityAt(*pTargetY + 2, *pTargetX))) {
-		pushY--;
-	}
-	if (isWalkabilityBlocking(getWalkabilityAt(*pTargetY - 2, *pTargetX))) {
-		pushY++;
-	}
-
-	while (pushX != 0 || pushY != 0) {
-		if (pushX < 0) {
-			if (isWalkabilityWalkable(getWalkabilityAt(*pTargetY, *pTargetX - 1))) {
-				*pTargetX = *pTargetX - 1;
-			}
-			pushX++;
-		}
-		if (pushX > 0) {
-			if (isWalkabilityWalkable(getWalkabilityAt(*pTargetY, *pTargetX + 1))) {
-				*pTargetX = *pTargetX + 1;
-			}
-			pushX--;
-		}
-		if (pushY < 0) {
-			if (isWalkabilityWalkable(getWalkabilityAt(*pTargetY - 1, *pTargetX))) {
-				*pTargetY = *pTargetY - 1;
-			}
-			pushY++;
-		}
-		if (pushY > 0) {
-			if (isWalkabilityWalkable(getWalkabilityAt(*pTargetY + 1, *pTargetX))) {
-				*pTargetY = *pTargetY + 1;
-			}
-			pushY--;
-		}
-	}
-}
-
-bool Macs2Engine::getPathfindingOverride(uint16 index, uint16 &result) const {
-	for (const PathfindingAreaOverride &current : _pathfindingOverrides) {
-		if (current._index == index && current._active) {
-			result = current._overrideValue;
-			return true;
-		}
-	}
-	return false;
-}
-void Macs2Engine::setPathfindingOverride(uint16 index, uint16 overrideValue) {
-	removePathfindingOverride(index);
-	PathfindingAreaOverride override;
-	override._active = true;
-	override._index = index;
-	override._overrideValue = overrideValue;
-	_pathfindingOverrides.push_back(override);
-}
-
-uint16 Macs2Engine::getPathfindingOverride2(uint16 index) const {
-	if (index < AREA_OVERRIDE_MIN || index > AREA_OVERRIDE_MAX) {
-		return 0;
-	}
-	return _areaOverrides[index - AREA_OVERRIDE_MIN];
-}
-
-void Macs2Engine::removePathfindingOverride(uint16 index) {
-	for (uint i = 0; i < _pathfindingOverrides.size(); i++) {
-		PathfindingAreaOverride &current = _pathfindingOverrides[i];
-		if (current._index == index) {
-			_pathfindingOverrides.remove_at(i);
-			return;
-		}
-	}
-};
-
-// Params: (param_1=y1, param_2=x1, param_3=y2, param_4=x2)
-// Traces from (x2,y2) toward (x1,y1). Checks walkability only on major-axis steps.
-// Uses unsigned 16-bit error accumulator with wrapping arithmetic.
-// Returns true if entire line is walkable (all sampled pixels < 0xC8).
-bool Macs2Engine::isPathWalkable(int16 y1, int16 x1, int16 y2, int16 x2) {
-	uint16 error = 0;
-	int16 curX = x2;
-	int16 curY = y2;
-	uint16 absDx = (uint16)ABS((int)(x2 - x1));
-	uint16 absDy = (uint16)ABS((int)(y2 - y1));
-	bool result = true;
-
-	do {
-		bool steppedX;
-		if (error >= absDx) {
-			if (y1 < y2) {
-				curY--;
-			}
-			if (y2 < y1) {
-				curY++;
-			}
-			error -= absDx;
-			steppedX = false;
-		} else {
-			if (x1 < x2) {
-				curX--;
-			}
-			if (x2 < x1) {
-				curX++;
-			}
-			error += absDy;
-			steppedX = true;
-		}
-
-		if (absDx > absDy && steppedX) {
-			if (isWalkabilityBlocking(getWalkabilityAt(curY, curX))) {
-				result = false;
-			}
-		}
-		if (absDx <= absDy && !steppedX) {
-			if (isWalkabilityBlocking(getWalkabilityAt(curY, curX))) {
-				result = false;
-			}
-		}
-	} while (curX != x1 || curY != y1);
-
-	return result;
-}
-
-// integer Euclidean distance approximation.
-// Iterates i from 0 until i^2 >= dx^2 + dy^2. Capped at 0x500.
-int Macs2Engine::euclideanDistance(const Common::Point &a, const Common::Point &b) {
-	int32 dx = ABS((int)(b.x - a.x));
-	int32 dy = ABS((int)(b.y - a.y));
-	int32 distSq = dx * dx + dy * dy;
-	int i = 0;
-	while (i < 0x500 && (int32)i * i < distSq) {
-		i++;
-	}
-	return i;
-}
-
-// distance between two nodes IF walkable, else 0x500.
-// Uses binary search on precomputed squared-distance table (scene+0x61DC) for O(log n) sqrt.
-int Macs2Engine::walkableDistance(int nodeA, int nodeB) {
-	const Common::Point &a = _pathfindingPoints[nodeA - 1]._position;
-	const Common::Point &b = _pathfindingPoints[nodeB - 1]._position;
-	if (!isPathWalkable(a.y, a.x, b.y, b.x)) {
-		return 0x500;
-	}
-	// Binary search for integer sqrt(dx^2 + dy^2), matching binary at 1008:1293
-	int32 dx = ABS((int)(b.x - a.x));
-	int32 dy = ABS((int)(b.y - a.y));
-	int32 distSq = dx * dx + dy * dy;
-	int result = 0x280;
-	int step = 0x280;
-	do {
-		step = step >> 1;
-		if ((int32)result * result >= distSq) {
-			result -= step;
-		} else {
-			result += step;
-		}
-	} while (step > 1);
-	return result;
-}
-
-// recursive DFS cost to reach a reachable node.
-// Full recursive DFS with visited-stack cycle detection matching binary exactly.
-// Terminal: returns walkableDistance(node, finalDest) when node is reachable.
-// Recursive: min(computeMinCostToReachable(adj)) + walkableDistance(bestAdj, current).
-int Macs2Engine::computeMinCostToReachable(int nodeIndex, int prevNode, uint16 actorIndex, const bool *reachable, int nodeCount, const Common::Point &finalDest) {
-	// Push current node to visited stack
-	_visitedCount++;
-	_visitedStack[_visitedCount] = nodeIndex;
-
-	int result;
-	const Common::Point &nodePos = _pathfindingPoints[nodeIndex - 1]._position;
-
-	if (reachable[nodeIndex]) {
-		// Terminal: return walkable distance from this node to finalDest
-		// Binary calls walkableDistance(nodePos, finalDest) = findPathNode(1008:1293)
-		if (!isPathWalkable(nodePos.y, nodePos.x, finalDest.y, finalDest.x)) {
-			result = 0x500;
-		} else {
-			int32 dx = ABS((int)(finalDest.x - nodePos.x));
-			int32 dy = ABS((int)(finalDest.y - nodePos.y));
-			int32 distSq = dx * dx + dy * dy;
-			int dist = 640;
-			int step = 320;
-			do {
-				step = step >> 1;
-				if ((int32)dist * dist >= distSq) {
-					dist -= step;
-				} else {
-					dist += step;
-				}
-			} while (step > 1);
-			result = dist;
-		}
-		_visitedCount--;
-		return result;
-	}
-
-	int bestCost = 0x7777;
-	int bestAdj = 0;
-	const PathfindingPoint &pt = _pathfindingPoints[nodeIndex - 1];
-	int adjCount = (int)pt._adjacentPoints.size();
-
-	if (adjCount > 0) {
-		for (int i = 0; i < adjCount; i++) {
-			const int adj = pt._adjacentPoints[i];
-			if (adj == prevNode) {
-				continue;
-			}
-
-			// Check visited stack
-			bool alreadyVisited = false;
-			for (int j = 1; j < _visitedCount; j++) {
-				if (_visitedStack[j] == adj) {
-					alreadyVisited = true;
-					break;
-				}
-			}
-			if (alreadyVisited) {
-				continue;
-			}
-
-			// Recursive call
-			const int cost = computeMinCostToReachable(adj, nodeIndex, actorIndex, reachable, nodeCount, finalDest);
-			if (cost < bestCost) {
-				bestAdj = adj;
-				bestCost = cost;
-			}
-		}
-	}
-
-	if (bestCost < 0x7777) {
-		// Add edge cost: walkable distance from bestAdj to current node
-		result = bestCost + walkableDistance(bestAdj, nodeIndex);
-	} else {
-		result = 0x7777;
-	}
-
-	// Pop visited stack
-	_visitedCount--;
-	return result;
-}
-
 void Macs2Engine::nextCursorMode() {
 	switch (_scriptExecutor->_cursorMode) {
 	case Script::MouseMode::Talk:
@@ -2860,10 +2498,6 @@ bool Macs2Engine::readInputFrame(uint16 &mouseX, uint16 &mouseY, uint16 &buttons
 	return !_inputPlaybackStream->eos();
 }
 
-uint16 Macs2Engine::getWalkabilityAt(const Common::Point &p) {
-	return getWalkabilityAt((int16)p.y, (int16)p.x);
-}
-
 int Macs2Engine::measureString(const Common::String &s) {
 	int sum = 0;
 	GlyphData currentGlyph;
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index 595338aa796..082f4d0b1b7 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -42,6 +42,7 @@
 #include "macs2/amiga_archive.h"
 #include "macs2/events.h"
 #include "macs2/macs2_constants.h"
+#include "macs2/pathfinding.h"
 #include "macs2/scriptexecutor.h"
 
 namespace Macs2 {
@@ -279,24 +280,6 @@ struct AnimBlobView {
 	}
 };
 
-struct PathfindingPoint {
-	uint8 _index;
-	Common::Point _position;
-	Common::Array<uint8> _adjacentPoints;
-};
-
-struct PathfindingAreaOverride {
-	bool _active;
-	uint16 _index;
-	uint16 _overrideValue;
-};
-
-// Area override table at scene+0x4EA8 (indexed by pathfinding value 0xC8..0xEF)
-// Set by opcode 0x4D, read by getAreaAtPoint (1008:101d)
-#define AREA_OVERRIDE_MIN 200
-#define AREA_OVERRIDE_MAX 239
-#define AREA_OVERRIDE_COUNT (AREA_OVERRIDE_MAX - AREA_OVERRIDE_MIN + 1)
-
 class Macs2Engine : public Engine, public Events {
 private:
 	const ADGameDescription *_gameDescription;
@@ -367,10 +350,6 @@ public:
 	// Assumes that the stream is at the start of the right section
 	void readImageResources(Common::SeekableReadStream *stream);
 
-	// visited stack (matches binary's stack-frame approach, max 16 nodes)
-	int _visitedStack[17] {};
-	int _visitedCount = 0;
-
 public:
 	Macs2Engine(OSystem *osystem, const ADGameDescription *gameDesc);
 	~Macs2Engine() override;
@@ -406,41 +385,46 @@ public:
 	Common::Array<Common::String> _debugOutput;
 	Common::Array<Common::String> _textLog;
 
-	// Note: This is used both for pathfinding as well as for area IDs
-	Graphics::ManagedSurface _pathfindingMap;
-
-	Common::Array<PathfindingAreaOverride> _pathfindingOverrides;
-	// Area override table at scene+value*5+0x4EA8 (for getAreaAtPoint)
-	uint16 _areaOverrides[AREA_OVERRIDE_COUNT] = {0};
-	Common::Array<PathfindingPoint> _pathfindingPoints;
+	Pathfinding _pathfinding;
 	Common::Array<Common::Point> _path;
 
-	bool getPathfindingOverride(uint16 index, uint16 &result) const;
-	void setPathfindingOverride(uint16 index, uint16 overrideValue);
+	bool getPathfindingOverride(uint16 index, uint16 &result) const {
+		return _pathfinding.getWalkOverride(index, result);
+	}
+	void setPathfindingOverride(uint16 index, uint16 overrideValue) {
+		_pathfinding.setWalkOverride(index, overrideValue);
+	}
 
-	// Walkability threshold 0xC8 uses signed 16-bit comparison in the binary (JL/JGE).
-	// Values with (int16)value < 0xC8 are walkable heights; e.g. -2 (0xFFFE) is walkable.
 	static inline bool isWalkabilityBlocking(uint16 value) {
-		return (int16)value >= 0xC8;
+		return Pathfinding::isWalkabilityBlocking(value);
 	}
 	static inline bool isWalkabilityWalkable(uint16 value) {
-		return (int16)value < 0xC8;
+		return Pathfinding::isWalkabilityWalkable(value);
 	}
 
-	// This one implements the lookup relative to es:[di+4EA8h] vs. the other one at es:[di+4EA5h] and es:[di+4EA6h]
-	uint16 getPathfindingOverride2(uint16 index) const;
-	void removePathfindingOverride(uint16 index);
+	uint16 getPathfindingOverride2(uint16 index) const {
+		return _pathfinding.areaOverrideAt(index);
+	}
+	void removePathfindingOverride(uint16 index) {
+		_pathfinding.removeWalkOverride(index);
+	}
 
-	uint16 getWalkabilityAt(int16 y, int16 x);
+	uint16 getWalkabilityAt(int16 y, int16 x) {
+		return _pathfinding.walkabilityAt(y, x);
+	}
+	uint16 getWalkabilityAt(const Common::Point &p) {
+		return _pathfinding.walkabilityAt(p);
+	}
 	/** Sync depth map with the current background animation frame (v1 gate fix). */
 	void updateBackgroundAnimationDepthMap(size_t animIndex);
 	void updateAllBackgroundAnimationDepthMaps();
-	bool isPathWalkable(int16 y1, int16 x1, int16 y2, int16 x2);
-	void snapToWalkablePosition(int16 *pTargetY, int16 *pTargetX, int16 charY, int16 charX);
-	int getPathfindingNodeCount() const { return (int)_numPathfindingPoints; }
-	int euclideanDistance(const Common::Point &a, const Common::Point &b);
-	int walkableDistance(int nodeA, int nodeB);
-	int computeMinCostToReachable(int nodeIndex, int prevNode, uint16 actorIndex, const bool *reachable, int nodeCount, const Common::Point &finalDest);
+	bool isPathWalkable(int16 y1, int16 x1, int16 y2, int16 x2) {
+		return _pathfinding.isLineWalkable(y1, x1, y2, x2);
+	}
+	void snapToWalkablePosition(int16 *pTargetY, int16 *pTargetX, int16 charY, int16 charX) {
+		_pathfinding.snapToWalkable(pTargetY, pTargetX, charY, charX);
+	}
+	int getPathfindingNodeCount() const { return _pathfinding.nodeCount(); }
 
 	// This is the override list living at [5BD1]
 	// Savegames sync 16 words into indices 1..16 (array size 0x11 during sync).
@@ -613,7 +597,6 @@ public:
 
 	Common::Array<uint16> _hotspotColorTable;
 
-	uint16 _numPathfindingPoints;
 	uint16 _walkDepthThresholdY;
 	uint16 _walkDepthScaleFactor;
 	uint16 _walkBaseSpeedPct;
@@ -746,8 +729,6 @@ public:
 	// Schedules a run of the script the next time the executor is ticked
 	void scheduleRun(bool initScene = false);
 
-	uint16 getWalkabilityAt(const Common::Point &p);
-
 	int measureString(const Common::String &s);
 
 	int measureStrings(const Common::StringArray &sa);
diff --git a/engines/macs2/module.mk b/engines/macs2/module.mk
index 4054b8fbd3b..f64c37fdf61 100644
--- a/engines/macs2/module.mk
+++ b/engines/macs2/module.mk
@@ -5,6 +5,7 @@ MODULE_OBJS = \
 	amiga_decode.o \
 	amiga_resources.o \
 	character.o \
+	pathfinding.o \
 	midiparser_macs2.o \
 	music.o \
 	dialogs.o \
diff --git a/engines/macs2/pathfinding.cpp b/engines/macs2/pathfinding.cpp
new file mode 100644
index 00000000000..7a7b4c27b18
--- /dev/null
+++ b/engines/macs2/pathfinding.cpp
@@ -0,0 +1,509 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "macs2/pathfinding.h"
+#include "common/scummsys.h"
+#include "common/util.h"
+#include "graphics/pixelformat.h"
+
+namespace Macs2 {
+
+void Pathfinding::createMap(int width, int height) {
+	_map.create(width, height, Graphics::PixelFormat::createFormatCLUT8());
+}
+
+void Pathfinding::clearWalkOverrides() {
+	_walkOverrides.clear();
+}
+
+void Pathfinding::clearAreaOverrides() {
+	memset(_areaOverrides, 0, sizeof(_areaOverrides));
+}
+
+uint16 Pathfinding::walkabilityAt(int16 y, int16 x) const {
+	if (x < 0 || x >= _map.w || y < 0 || y >= _map.h) {
+		return 0;
+	}
+	uint16 value = _map.getPixel(x, y);
+	if (value >= 0xC8 && value <= 0xEF) {
+		uint16 overrideResult;
+		if (getWalkOverride(value, overrideResult)) {
+			return overrideResult;
+		}
+		return 0xFF;
+	}
+	return value;
+}
+
+uint16 Pathfinding::walkabilityAt(const Common::Point &p) const {
+	return walkabilityAt((int16)p.y, (int16)p.x);
+}
+
+uint16 Pathfinding::areaAt(uint16 x, uint16 y) const {
+	if (x >= (uint16)_map.w || y >= (uint16)_map.h) {
+		return 0;
+	}
+	uint16 result = _map.getPixel(x, y);
+	if (result >= AREA_OVERRIDE_MIN && result < 250) {
+		const uint16 overrideValue = areaOverrideAt(result);
+		if (overrideValue >= AREA_OVERRIDE_MIN) {
+			result = overrideValue;
+		}
+	}
+	return result;
+}
+
+bool Pathfinding::getWalkOverride(uint16 index, uint16 &result) const {
+	for (const PathfindingAreaOverride &current : _walkOverrides) {
+		if (current._index == index && current._active) {
+			result = current._overrideValue;
+			return true;
+		}
+	}
+	return false;
+}
+
+void Pathfinding::setWalkOverride(uint16 index, uint16 overrideValue) {
+	removeWalkOverride(index);
+	PathfindingAreaOverride override;
+	override._active = true;
+	override._index = index;
+	override._overrideValue = overrideValue;
+	_walkOverrides.push_back(override);
+}
+
+uint16 Pathfinding::areaOverrideAt(uint16 index) const {
+	if (index < AREA_OVERRIDE_MIN || index > AREA_OVERRIDE_MAX) {
+		return 0;
+	}
+	return _areaOverrides[index - AREA_OVERRIDE_MIN];
+}
+
+void Pathfinding::removeWalkOverride(uint16 index) {
+	for (uint i = 0; i < _walkOverrides.size(); i++) {
+		PathfindingAreaOverride &current = _walkOverrides[i];
+		if (current._index == index) {
+			_walkOverrides.remove_at(i);
+			return;
+		}
+	}
+}
+
+void Pathfinding::snapToWalkable(int16 *pTargetY, int16 *pTargetX, int16 charY, int16 charX) const {
+	if (_map.w == 0 || _map.h == 0) {
+		return;
+	}
+
+	int16 savedX = *pTargetX;
+	int16 savedY = *pTargetY;
+	const int16 maxY = (int16)(_map.h - 1);
+	const int16 maxX = (int16)(_map.w - 1);
+
+	// Phase 1: Scan downward with depth constraint
+	// Condition: walkability >= 200 OR (targetY - walkability) < savedY
+	while (true) {
+		uint16 w = walkabilityAt(*pTargetY, savedX);
+		if (isWalkabilityWalkable(w) && (*pTargetY - (int16)w >= savedY)) {
+			break;
+		}
+		if (*pTargetY >= maxY) {
+			break;
+		}
+		*pTargetY = *pTargetY + 1;
+	}
+
+	// Phase 2: Continue scanning to bottom for best depth match
+	int16 scanY = *pTargetY;
+	while (scanY <= maxY) {
+		uint16 w = walkabilityAt(scanY, *pTargetX);
+		if (scanY - (int16)w == savedY) {
+			*pTargetY = scanY;
+		}
+		if (scanY == maxY) {
+			break;
+		}
+		scanY++;
+	}
+
+	// Phase 3: If at screen bottom and still non-walkable, scan upward
+	if (*pTargetY == maxY) {
+		uint16 w = walkabilityAt(*pTargetY, *pTargetX);
+		if (isWalkabilityBlocking(w)) {
+			while (isWalkabilityBlocking(w) && *pTargetY > 0) {
+				*pTargetY = *pTargetY - 1;
+				w = walkabilityAt(*pTargetY, *pTargetX);
+			}
+		}
+	}
+
+	// Phase 4: If still non-walkable, scan X toward character
+	uint16 w = walkabilityAt(*pTargetY, *pTargetX);
+	if (isWalkabilityBlocking(w)) {
+		*pTargetY = savedY;
+		if (charX < *pTargetX) {
+			while (true) {
+				uint16 w2 = walkabilityAt(*pTargetY, *pTargetX);
+				if (isWalkabilityWalkable(w2)) {
+					break;
+				}
+				if (*pTargetX <= 0) {
+					break;
+				}
+				*pTargetX = *pTargetX - 1;
+			}
+		} else {
+			while (true) {
+				uint16 w2 = walkabilityAt(*pTargetY, *pTargetX);
+				if (isWalkabilityWalkable(w2)) {
+					break;
+				}
+				if (*pTargetX >= maxX) {
+					break;
+				}
+				*pTargetX = *pTargetX + 1;
+			}
+		}
+		// Phase 5: If all failed, fall back to character position
+		uint16 w2 = walkabilityAt(*pTargetY, *pTargetX);
+		if (isWalkabilityBlocking(w2)) {
+			*pTargetX = charX;
+			*pTargetY = charY;
+		}
+	}
+
+	// Phase 6: Gradient-based wall push
+	int16 pushX = 0;
+	int16 pushY = 0;
+	if (isWalkabilityBlocking(walkabilityAt(*pTargetY, *pTargetX + 1))) {
+		pushX--;
+	}
+	if (isWalkabilityBlocking(walkabilityAt(*pTargetY, *pTargetX - 1))) {
+		pushX++;
+	}
+	if (isWalkabilityBlocking(walkabilityAt(*pTargetY + 1, *pTargetX))) {
+		pushY--;
+	}
+	if (isWalkabilityBlocking(walkabilityAt(*pTargetY - 1, *pTargetX))) {
+		pushY++;
+	}
+	if (isWalkabilityBlocking(walkabilityAt(*pTargetY, *pTargetX + 2))) {
+		pushX--;
+	}
+	if (isWalkabilityBlocking(walkabilityAt(*pTargetY, *pTargetX - 2))) {
+		pushX++;
+	}
+	if (isWalkabilityBlocking(walkabilityAt(*pTargetY + 2, *pTargetX))) {
+		pushY--;
+	}
+	if (isWalkabilityBlocking(walkabilityAt(*pTargetY - 2, *pTargetX))) {
+		pushY++;
+	}
+
+	while (pushX != 0 || pushY != 0) {
+		if (pushX < 0) {
+			if (isWalkabilityWalkable(walkabilityAt(*pTargetY, *pTargetX - 1))) {
+				*pTargetX = *pTargetX - 1;
+			}
+			pushX++;
+		}
+		if (pushX > 0) {
+			if (isWalkabilityWalkable(walkabilityAt(*pTargetY, *pTargetX + 1))) {
+				*pTargetX = *pTargetX + 1;
+			}
+			pushX--;
+		}
+		if (pushY < 0) {
+			if (isWalkabilityWalkable(walkabilityAt(*pTargetY - 1, *pTargetX))) {
+				*pTargetY = *pTargetY - 1;
+			}
+			pushY++;
+		}
+		if (pushY > 0) {
+			if (isWalkabilityWalkable(walkabilityAt(*pTargetY + 1, *pTargetX))) {
+				*pTargetY = *pTargetY + 1;
+			}
+			pushY--;
+		}
+	}
+}
+
+bool Pathfinding::isLineWalkable(int16 y1, int16 x1, int16 y2, int16 x2) const {
+	uint16 error = 0;
+	int16 curX = x2;
+	int16 curY = y2;
+	uint16 absDx = (uint16)ABS((int)(x2 - x1));
+	uint16 absDy = (uint16)ABS((int)(y2 - y1));
+	bool result = true;
+
+	do {
+		bool steppedX;
+		if (error >= absDx) {
+			if (y1 < y2) {
+				curY--;
+			}
+			if (y2 < y1) {
+				curY++;
+			}
+			error -= absDx;
+			steppedX = false;
+		} else {
+			if (x1 < x2) {
+				curX--;
+			}
+			if (x2 < x1) {
+				curX++;
+			}
+			error += absDy;
+			steppedX = true;
+		}
+
+		if (absDx > absDy && steppedX) {
+			if (isWalkabilityBlocking(walkabilityAt(curY, curX))) {
+				result = false;
+			}
+		}
+		if (absDx <= absDy && !steppedX) {
+			if (isWalkabilityBlocking(walkabilityAt(curY, curX))) {
+				result = false;
+			}
+		}
+	} while (curX != x1 || curY != y1);
+
+	return result;
+}
+
+int Pathfinding::euclideanDistance(const Common::Point &a, const Common::Point &b) const {
+	int32 dx = ABS((int)(b.x - a.x));
+	int32 dy = ABS((int)(b.y - a.y));
+	int32 distSq = dx * dx + dy * dy;
+	int i = 0;
+	while (i < 0x500 && (int32)i * i < distSq) {
+		i++;
+	}
+	return i;
+}
+
+int Pathfinding::walkableDistance(int nodeA, int nodeB) const {
+	const Common::Point &a = _points[nodeA - 1]._position;
+	const Common::Point &b = _points[nodeB - 1]._position;
+	if (!isLineWalkable(a.y, a.x, b.y, b.x)) {
+		return 0x500;
+	}
+	int32 dx = ABS((int)(b.x - a.x));
+	int32 dy = ABS((int)(b.y - a.y));
+	int32 distSq = dx * dx + dy * dy;
+	int result = 0x280;
+	int step = 0x280;
+	do {
+		step = step >> 1;
+		if ((int32)result * result >= distSq) {
+			result -= step;
+		} else {
+			result += step;
+		}
+	} while (step > 1);
+	return result;
+}
+
+int Pathfinding::computeMinCostToReachable(int nodeIndex, int prevNode, const bool *reachable, int nodeCount, const Common::Point &finalDest) {
+	_visitedCount++;
+	_visitedStack[_visitedCount] = nodeIndex;
+
+	int result;
+	const Common::Point &nodePos = _points[nodeIndex - 1]._position;
+
+	if (reachable[nodeIndex]) {
+		if (!isLineWalkable(nodePos.y, nodePos.x, finalDest.y, finalDest.x)) {
+			result = 0x500;
+		} else {
+			int32 dx = ABS((int)(finalDest.x - nodePos.x));
+			int32 dy = ABS((int)(finalDest.y - nodePos.y));
+			int32 distSq = dx * dx + dy * dy;
+			int dist = 640;
+			int step = 320;
+			do {
+				step = step >> 1;
+				if ((int32)dist * dist >= distSq) {
+					dist -= step;
+				} else {
+					dist += step;
+				}
+			} while (step > 1);
+			result = dist;
+		}
+		_visitedCount--;
+		return result;
+	}
+
+	int bestCost = 0x7777;
+	int bestAdj = 0;
+	const PathfindingPoint &pt = _points[nodeIndex - 1];
+	int adjCount = (int)pt._adjacentPoints.size();
+
+	if (adjCount > 0) {
+		for (int i = 0; i < adjCount; i++) {
+			const int adj = pt._adjacentPoints[i];
+			if (adj == prevNode) {
+				continue;
+			}
+
+			bool alreadyVisited = false;
+			for (int j = 1; j < _visitedCount; j++) {
+				if (_visitedStack[j] == adj) {
+					alreadyVisited = true;
+					break;
+				}
+			}
+			if (alreadyVisited) {
+				continue;
+			}
+
+			const int cost = computeMinCostToReachable(adj, nodeIndex, reachable, nodeCount, finalDest);
+			if (cost < bestCost) {
+				bestAdj = adj;
+				bestCost = cost;
+			}
+		}
+	}
+
+	if (bestCost < 0x7777) {
+		result = bestCost + walkableDistance(bestAdj, nodeIndex);
+	} else {
+		result = 0x7777;
+	}
+
+	_visitedCount--;
+	return result;
+}
+
+bool Pathfinding::canNodeConnectSourceToTarget(uint16 nodeIndex, const Common::Point &charPos, const Common::Point &target, const bool *reachable, int nodeCount) const {
+	const Common::Point &nodePos = _points[nodeIndex - 1]._position;
+	if (!isLineWalkable(nodePos.y, nodePos.x, target.y, target.x))
+		return false;
+
+	bool visited[kPathNodeSlots + 1] = {};
+	floodFillConnectedNodes(nodeIndex, visited, nodeCount);
+
+	bool anySeesTarget = false;
+	bool anySeenFromSource = false;
+	for (int i = 1; i <= nodeCount; i++) {
+		if (!visited[i])
+			continue;
+		const Common::Point &p = _points[i - 1]._position;
+		if (isLineWalkable(p.y, p.x, target.y, target.x))
+			anySeesTarget = true;
+		if (isLineWalkable(charPos.y, charPos.x, p.y, p.x))
+			anySeenFromSource = true;
+	}
+	return anySeesTarget && anySeenFromSource;
+}
+
+void Pathfinding::floodFillConnectedNodes(int nodeIndex, bool *visited, int nodeCount) const {
+	if (nodeIndex < 1 || nodeIndex > nodeCount)
+		return;
+	if (visited[nodeIndex])
+		return;
+	visited[nodeIndex] = true;
+	const PathfindingPoint &pt = _points[nodeIndex - 1];
+	for (uint i = 0; i < pt._adjacentPoints.size(); i++) {
+		floodFillConnectedNodes(pt._adjacentPoints[i], visited, nodeCount);
+	}
+}
+
+PathRoute Pathfinding::calculateRoute(const Common::Point &from, const Common::Point &to) {
+	// Binary calculatePath (1008:1966). Params: charY, charX, finalDestY, finalDestX.
+	PathRoute route;
+	route.firstWaypoint = to;
+	const int count = nodeCount();
+
+	bool reachable[kPathNodeSlots + 1] = {};
+	for (int i = 1; i <= count; i++) {
+		const Common::Point &nodePos = _points[i - 1]._position;
+		reachable[i] = isLineWalkable(to.y, to.x, nodePos.y, nodePos.x);
+	}
+
+	int bestCost = 0x7777;
+	int bestNode = 0;
+	for (int i = 1; i <= count; i++) {
+		const Common::Point &nodePos = _points[i - 1]._position;
+		int costToDest = euclideanDistance(nodePos, to);
+		int costToChar = euclideanDistance(nodePos, from);
+		if (costToDest + costToChar < bestCost) {
+			// Binary calls canNodeConnectSourceToTarget(destY, destX, charY, charX, i)
+			// due to calculatePath being invoked with swapped source/dest params.
+			if (canNodeConnectSourceToTarget(i, to, from, reachable, count)) {
+				costToDest = euclideanDistance(nodePos, to);
+				costToChar = euclideanDistance(nodePos, from);
+				bestCost = costToDest + costToChar;
+				bestNode = i;
+			}
+		}
+	}
+
+	if (bestNode == 0) {
+		route.startIndex = 1;
+		return route;
+	}
+
+	route.nodes.push_back(bestNode);
+	int currentNode = bestNode;
+	while (!reachable[currentNode]) {
+		const PathfindingPoint &curPt = _points[currentNode - 1];
+		int localBestCost = 0x7777;
+		int nextNode = currentNode;
+		for (uint a = 0; a < curPt._adjacentPoints.size(); a++) {
+			const int adjIdx = curPt._adjacentPoints[a];
+			const int cost = computeMinCostToReachable(adjIdx, 0x7fff, reachable, count, to);
+			const int edgeCost = walkableDistance(adjIdx, currentNode);
+			if (cost + edgeCost < localBestCost) {
+				nextNode = adjIdx;
+				localBestCost = cost + edgeCost;
+			}
+		}
+		currentNode = nextNode;
+		route.nodes.push_back(currentNode);
+		if (route.nodes.size() > kPathNodeSlots)
+			break;
+	}
+
+	for (uint i = 0; i + 1 < route.nodes.size(); i++) {
+		const Common::Point &p1 = _points[route.nodes[i + 1] - 1]._position;
+		const Common::Point &p2 = _points[route.nodes[i] - 1]._position;
+		if (!isLineWalkable(p1.y, p1.x, p2.y, p2.x)) {
+			route.nodes.clear();
+			return route;
+		}
+	}
+
+	route.startIndex = 0;
+	while (route.startIndex + 1 < (int16)route.nodes.size()) {
+		const Common::Point &nextNodePos = _points[route.nodes[route.startIndex + 1] - 1]._position;
+		if (!isLineWalkable(nextNodePos.y, nextNodePos.x, from.y, from.x))
+			break;
+		route.startIndex++;
+	}
+
+	route.firstWaypoint = _points[route.nodes[route.startIndex] - 1]._position;
+	route.found = true;
+	return route;
+}
+
+} // namespace Macs2
diff --git a/engines/macs2/pathfinding.h b/engines/macs2/pathfinding.h
new file mode 100644
index 00000000000..30aa667e906
--- /dev/null
+++ b/engines/macs2/pathfinding.h
@@ -0,0 +1,107 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MACS2_PATHFINDING_H
+#define MACS2_PATHFINDING_H
+
+#include "common/array.h"
+#include "common/rect.h"
+#include "graphics/managed_surface.h"
+#include "macs2/macs2_constants.h"
+
+namespace Macs2 {
+
+// Area override table at scene+0x4EA8 (indexed by pathfinding value 0xC8..0xEF)
+// Set by opcode 0x4D, read by getAreaAtPoint (1008:101d)
+static constexpr uint16 AREA_OVERRIDE_MIN = 200;
+static constexpr uint16 AREA_OVERRIDE_MAX = 239;
+static constexpr uint16 AREA_OVERRIDE_COUNT = AREA_OVERRIDE_MAX - AREA_OVERRIDE_MIN + 1;
+
+struct PathfindingPoint {
+	uint8 _index = 0;
+	Common::Point _position;
+	Common::Array<uint8> _adjacentPoints;
+};
+
+struct PathfindingAreaOverride {
+	bool _active = false;
+	uint16 _index = 0;
+	uint16 _overrideValue = 0;
+};
+
+struct PathRoute {
+	Common::Array<uint16> nodes;
+	int16 startIndex = 0;
+	Common::Point firstWaypoint;
+	bool found = false;
+};
+
+class Pathfinding {
+public:
+	Graphics::ManagedSurface _map;
+	Common::Array<PathfindingPoint> _points;
+	Common::Array<PathfindingAreaOverride> _walkOverrides;
+	uint16 _areaOverrides[AREA_OVERRIDE_COUNT] = {0};
+	uint16 _numPoints = 0;
+
+	void createMap(int width, int height);
+	void clearWalkOverrides();
+	void clearAreaOverrides();
+
+	// Walkability threshold 0xC8 uses signed 16-bit comparison in the binary (JL/JGE).
+	// Values with (int16)value < 0xC8 are walkable heights; e.g. -2 (0xFFFE) is walkable.
+	static inline bool isWalkabilityBlocking(uint16 value) {
+		return (int16)value >= 0xC8;
+	}
+	static inline bool isWalkabilityWalkable(uint16 value) {
+		return (int16)value < 0xC8;
+	}
+
+	uint16 walkabilityAt(int16 y, int16 x) const;
+	uint16 walkabilityAt(const Common::Point &p) const;
+	uint16 areaAt(uint16 x, uint16 y) const;
+
+	bool getWalkOverride(uint16 index, uint16 &result) const;
+	void setWalkOverride(uint16 index, uint16 overrideValue);
+	void removeWalkOverride(uint16 index);
+	uint16 areaOverrideAt(uint16 index) const;
+
+	bool isLineWalkable(int16 y1, int16 x1, int16 y2, int16 x2) const;
+	void snapToWalkable(int16 *pTargetY, int16 *pTargetX, int16 charY, int16 charX) const;
+
+	int nodeCount() const { return (int)_numPoints; }
+	int euclideanDistance(const Common::Point &a, const Common::Point &b) const;
+	int walkableDistance(int nodeA, int nodeB) const;
+
+	PathRoute calculateRoute(const Common::Point &from, const Common::Point &to);
+
+private:
+	int _visitedStack[17] {};
+	int _visitedCount = 0;
+
+	int computeMinCostToReachable(int nodeIndex, int prevNode, const bool *reachable, int nodeCount, const Common::Point &finalDest);
+	bool canNodeConnectSourceToTarget(uint16 nodeIndex, const Common::Point &charPos, const Common::Point &target, const bool *reachable, int nodeCount) const;
+	void floodFillConnectedNodes(int nodeIndex, bool *visited, int nodeCount) const;
+};
+
+} // namespace Macs2
+
+#endif
diff --git a/engines/macs2/saveload.cpp b/engines/macs2/saveload.cpp
index feabfd36bd0..fc3c40703e5 100644
--- a/engines/macs2/saveload.cpp
+++ b/engines/macs2/saveload.cpp
@@ -304,21 +304,21 @@ Common::Error Macs2Engine::syncGameV1(Common::Serializer &s) {
 
 	// Scene data: pathfinding overrides [+0x528D]: 200 bytes ---
 	if (s.isLoading())
-		_pathfindingOverrides.clear();
-	for (int i = 0; i < ARRAYSIZE(_areaOverrides); i++) {
+		_pathfinding._walkOverrides.clear();
+	for (int i = 0; i < ARRAYSIZE(_pathfinding._areaOverrides); i++) {
 		uint8 active = 0;
 		uint16 overrideValue = 0;
 		uint16 remap = 0;
 		if (s.isSaving()) {
 			uint16 idx = AREA_OVERRIDE_MIN + i;
-			for (const auto &ov : _pathfindingOverrides) {
+			for (const auto &ov : _pathfinding._walkOverrides) {
 				if (ov._index == idx && ov._active) {
 					active = 1;
 					overrideValue = ov._overrideValue;
 					break;
 				}
 			}
-			remap = _areaOverrides[i];
+			remap = _pathfinding._areaOverrides[i];
 		}
 		s.syncAsByte(active);
 		s.syncAsUint16LE(overrideValue);
@@ -329,9 +329,9 @@ Common::Error Macs2Engine::syncGameV1(Common::Serializer &s) {
 				ov._active = true;
 				ov._index = AREA_OVERRIDE_MIN + i;
 				ov._overrideValue = overrideValue;
-				_pathfindingOverrides.push_back(ov);
+				_pathfinding._walkOverrides.push_back(ov);
 			}
-			_areaOverrides[i] = remap;
+			_pathfinding._areaOverrides[i] = remap;
 		}
 	}
 
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index d1dfe05e985..0933e7f0aef 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -315,18 +315,7 @@ OpcodeResult ScriptExecutor::scriptChangeAnimation() {
 uint16 ScriptExecutor::getAreaAtPoint(uint16 x, uint16 y) {
 	// getAreaAtPoint (1008:101d). Reads the pathfinding map pixel and applies
 	// the area override table at sceneData + value*5 + 0x4EA8.
-	if (x >= (uint16)_engine->screenWidth() || y >= (uint16)_engine->gameHeight() ||
-		_engine->_pathfindingMap.w == 0) {
-		return 0;
-	}
-	uint16 result = _engine->_pathfindingMap.getPixel(x, y);
-	if (result >= AREA_OVERRIDE_MIN && result < 250) {
-		uint16 overrideValue = _engine->getPathfindingOverride2(result);
-		if (overrideValue >= AREA_OVERRIDE_MIN) {
-			result = overrideValue;
-		}
-	}
-	return result;
+	return _engine->_pathfinding.areaAt(x, y);
 }
 
 bool ScriptExecutor::loadIndexedResource(Common::Array<uint8> &outData, uint8 resourceIndex, uint16 /*objectTableOffset*/) {
@@ -1425,25 +1414,7 @@ OpcodeResult Script::ScriptExecutor::scriptWalkToPosition() {
 		c = &stackCharacter;
 	}
 
-	const Common::Point current = c->getPosition();
-	const Common::Point target(x, y);
-
-	c->_currentPathIndex = 0;
-	c->_path.clear();
-	c->_pathFinalDestination = target;
-
-	bool directPath = _engine->isPathWalkable(y, x, current.y, current.x);
-	if (!directPath && Macs2Engine::isWalkabilityWalkable(_engine->getWalkabilityAt(y, x))) {
-		c->calculatePath(target);
-	}
-	if (c->_path.empty()) {
-		c->_targetPosition = c->_pathFinalDestination;
-	}
-
-	c->_stepDeltaX = (int16)ABS((int32)c->_targetPosition.x - current.x);
-	c->_stepDeltaY = (int16)ABS((int32)c->_targetPosition.y - current.y);
-	c->_stepError = 0;
-	c->_stepDirectionSet = false;
+	c->setWalkTarget(Common::Point(x, y), false);
 
 	// Binary loadObjectData seeds runtime+0x21D from the object table vertical offset;
 	// scriptWalkToPosition does not change it. Match that so waitForWalk can complete
@@ -3127,7 +3098,7 @@ OpcodeResult Script::ScriptExecutor::scriptSetPathfindingRemap() {
 		setScriptError(0x0D);
 		return OpcodeResult::Continue;
 	}
-	g_engine->_areaOverrides[sourceValue - AREA_OVERRIDE_MIN] = targetValue;
+	g_engine->_pathfinding._areaOverrides[sourceValue - AREA_OVERRIDE_MIN] = targetValue;
 	return OpcodeResult::Continue;
 }
 
@@ -3955,7 +3926,7 @@ OpcodeResult ScriptExecutor::scriptLoadWalkMask() {
 	const bool halfRes = _engine->isV2();
 	const int w = halfRes ? kScreenWidth : _engine->screenWidth();
 	const int h = halfRes ? kGameHeight : _engine->gameHeight();
-	if (!_engine->loadMaskFromResource(resourceIndex, _executingScriptObjectId, _engine->_pathfindingMap,
+	if (!_engine->loadMaskFromResource(resourceIndex, _executingScriptObjectId, _engine->_pathfinding._map,
 									   w, h, halfRes))
 		warning("loadWalkMask: failed resource %u", resourceIndex);
 	return OpcodeResult::Continue;
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index 6fcb9e6c9f7..b9515c09446 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -926,17 +926,17 @@ void View1::drawPathfindingPoints(Graphics::ManagedSurface &s) {
 		yOffset = xData._height / 2;
 	}
 	for (int i = 0; i < 16; i++) {
-		const PathfindingPoint &current = g_engine->_pathfindingPoints[i];
+		const PathfindingPoint &current = g_engine->_pathfinding._points[i];
 		renderString(current._position.x - xOffset, current._position.y - yOffset, "x");
 
 		const Common::String &number = Common::String::format("%u", i);
 		renderString(current._position.x - xOffset + 10, current._position.y - yOffset + 10, number.c_str());
 
 		for (uint8 adjacentIndex : current._adjacentPoints) {
-			if (adjacentIndex >= g_engine->_pathfindingPoints.size()) {
+			if (adjacentIndex >= g_engine->_pathfinding._points.size()) {
 				continue;
 			}
-			PathfindingPoint &other = g_engine->_pathfindingPoints[adjacentIndex - 1];
+			PathfindingPoint &other = g_engine->_pathfinding._points[adjacentIndex - 1];
 			s.drawLine(current._position.x, current._position.y, other._position.x, other._position.y, 0xFFFFFFFF);
 		}
 	}
@@ -1794,28 +1794,7 @@ void View1::walkToScreenPosition(const Common::Point &pos) {
 		return;
 	}
 
-	const Common::Point &charPos = protagonist->getPosition();
-
-	Common::Point target = pos;
-	g_engine->snapToWalkablePosition(&target.y, &target.x, charPos.y, charPos.x);
-
-	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 = (int16)ABS(protagonist->_targetPosition.x - charPos.x);
-	protagonist->_stepDeltaY = (int16)ABS(protagonist->_targetPosition.y - charPos.y);
-	protagonist->_stepError = 0;
-	protagonist->_stepDirectionSet = false;
+	protagonist->setWalkTarget(pos, true);
 	g_engine->_scriptExecutor->saveWalkRuntime(protagonist, protagonist->_gameObject);
 }
 
@@ -2833,7 +2812,7 @@ void View1::drawAllCharacters(Graphics::ManagedSurface *surface, bool fullUpdate
 			}
 
 			int16 walkabilityOffset = 0;
-			if (g_engine->_pathfindingMap.w > 0) {
+			if (g_engine->_pathfinding._map.w > 0) {
 				walkabilityOffset = g_engine->getWalkabilityAt(charY, charX);
 				if (Macs2Engine::isWalkabilityBlocking((uint16)walkabilityOffset))
 					walkabilityOffset = 0;


Commit: 8a1680afbe2d0d3d1bf406e286379088e51aadcd
    https://github.com/scummvm/scummvm/commit/8a1680afbe2d0d3d1bf406e286379088e51aadcd
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-09-03T19:50:40+02:00

Commit Message:
MACS2: extract text code

Changed paths:
  A engines/macs2/sprite.h
  A engines/macs2/text.cpp
  A engines/macs2/text.h
    engines/macs2/actionbar.cpp
    engines/macs2/actionbar.h
    engines/macs2/amiga_resources.cpp
    engines/macs2/debugtools.cpp
    engines/macs2/macs2.cpp
    engines/macs2/macs2.h
    engines/macs2/module.mk
    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 409ef3e0904..03071d1ccac 100644
--- a/engines/macs2/actionbar.cpp
+++ b/engines/macs2/actionbar.cpp
@@ -177,11 +177,11 @@ 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::actionBarFont(const GlyphData *&font, uint16 &glyphCount, int &glyphH) const {
+	const bool usePanelFont = g_engine->_text.numPanelGlyphs > 0;
+	font = usePanelFont ? g_engine->_text._panelGlyphs : g_engine->_text._glyphs;
+	glyphCount = usePanelFont ? g_engine->_text.numPanelGlyphs : g_engine->_text._numGlyphs;
+	glyphH = usePanelFont ? (int)g_engine->_text.maxPanelGlyphHeight : (int)g_engine->_text._maxGlyphHeight;
 }
 
 void ActionBar::drawSentenceLine(Graphics::ManagedSurface &s) {
@@ -190,15 +190,15 @@ void ActionBar::drawSentenceLine(Graphics::ManagedSurface &s) {
 		return;
 
 	// 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 GlyphData *font = g_engine->_text._glyphs;
+	uint16 glyphCount = g_engine->_text._numGlyphs;
+	int glyphH = (int)g_engine->_text._maxGlyphHeight;
+	if (glyphCount == 0)
+		actionBarFont(font, glyphCount, 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);
+	const int textX = MAX(0, (kScreenWidth - _view->measureStringWithFont(sentence, font, glyphCount)) / 2);
+	_view->renderStringWithFontTo(textX, textY, sentence, font, glyphCount, s);
 }
 
 void ActionBar::drawVerbBar(Graphics::ManagedSurface &s) {
@@ -215,7 +215,7 @@ void ActionBar::drawVerbBar(Graphics::ManagedSurface &s) {
 		drawUIButton(r, isActive || isHovered, s);
 
 		Common::String label = uiText(kVerbs[i].label);
-		if (g_engine->numPanelGlyphs > 0)
+		if (g_engine->_text.numPanelGlyphs > 0)
 			label.toUppercase();
 		const int textW = _view->measureStringWithFont(label, font, fontCount);
 		const int textX = r.left + (r.width() - textW) / 2;
@@ -661,8 +661,8 @@ void ActionBar::drawNative(Graphics::ManagedSurface &s) {
 	const uint16 optTextMaxW = g_engine->_hudTextLayout[2] ? g_engine->_hudTextLayout[2] : 212;
 	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 GlyphData *panelGlyph = g_engine->_text.numPanelGlyphs ? g_engine->_text._panelGlyphs : g_engine->_text._glyphs;
+	const uint16 panelGlyphCount = g_engine->_text.numPanelGlyphs ? g_engine->_text.numPanelGlyphs : g_engine->_text._numGlyphs;
 
 	if (menuMode == MenuMode::Main) {
 		if (_view->_inventorySource == nullptr ||
@@ -701,38 +701,38 @@ 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;
-		if (fontCount != 0) {
+		const GlyphData *font = g_engine->_text._numGlyphs ? g_engine->_text._glyphs : panelGlyph;
+		const uint16 glyphCount = g_engine->_text._numGlyphs ? g_engine->_text._numGlyphs : panelGlyphCount;
+		if (glyphCount != 0) {
 			Common::String sentence = buildSentenceLine();
 			if (!sentence.empty()) {
 				const uint16 maxW = (uint16)(g_engine->screenWidth() - 16);
 				while (sentence.size() > 1) {
-					if ((uint16)_view->measureStringWithFont(sentence, font, fontCount) <= maxW)
+					if ((uint16)_view->measureStringWithFont(sentence, font, glyphCount) <= maxW)
 						break;
 					sentence.deleteLastChar();
 				}
-				const int textW = _view->measureStringWithFont(sentence, font, fontCount);
+				const int textW = _view->measureStringWithFont(sentence, font, glyphCount);
 				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->_text._maxGlyphHeight ? (int)g_engine->_text._maxGlyphHeight : 12;
 				const int textY = MAX(0, (int)panelTop - glyphH - 2);
-				_view->renderStringWithFontTo((uint16)textX, (uint16)textY, sentence, font, fontCount, s);
+				_view->renderStringWithFontTo((uint16)textX, (uint16)textY, sentence, font, glyphCount, s);
 			}
 		}
-	} else if (menuMode == MenuMode::Options && panelFontCount != 0) {
+	} else if (menuMode == MenuMode::Options && panelGlyphCount != 0) {
 		if (g_engine->_saveSlotNames.empty())
 			refreshSaveSlotNames();
 		for (uint i = 0; i < g_engine->_saveSlotNames.size() && i < lineCount; i++) {
 			Common::String name = g_engine->_saveSlotNames[i];
 			while (name.size() > 1) {
-				if ((uint16)_view->measureStringWithFont(name, panelFont, panelFontCount) <= optTextMaxW)
+				if ((uint16)_view->measureStringWithFont(name, panelGlyph, panelGlyphCount) <= optTextMaxW)
 					break;
 				name.deleteLastChar();
 			}
 			_view->renderStringWithFontTo(optTextX, panelTop + optTextY + (int)i * linePitch,
-										  name, panelFont, panelFontCount, s);
+										  name, panelGlyph, panelGlyphCount, s);
 		}
-	} else if (menuMode == MenuMode::DialogueList && panelFontCount != 0 && _view->_isDialogueChoiceInputActive) {
+	} else if (menuMode == MenuMode::DialogueList && panelGlyphCount != 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];
@@ -743,7 +743,7 @@ void ActionBar::drawNative(Graphics::ManagedSurface &s) {
 							  line < _view->_drawnStringBox.size();
 				 li++, line++) {
 				_view->renderStringWithFontTo(dlgX, panelTop + dlgY + (int)line * pitch,
-											  _view->_drawnStringBox[line], panelFont, panelFontCount, s);
+											  _view->_drawnStringBox[line], panelGlyph, panelGlyphCount, s);
 			}
 		}
 	}
diff --git a/engines/macs2/actionbar.h b/engines/macs2/actionbar.h
index 600df652fee..da77d97a984 100644
--- a/engines/macs2/actionbar.h
+++ b/engines/macs2/actionbar.h
@@ -85,7 +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 actionBarFont(const GlyphData *&font, uint16 &glyphCount, int &glyphH) const;
 	void drawSentenceLine(Graphics::ManagedSurface &s);
 	void drawVerbBar(Graphics::ManagedSurface &s);
 	void drawInventoryStrip(Graphics::ManagedSurface &s);
diff --git a/engines/macs2/amiga_resources.cpp b/engines/macs2/amiga_resources.cpp
index 200d8639ae3..ba48b7a4671 100644
--- a/engines/macs2/amiga_resources.cpp
+++ b/engines/macs2/amiga_resources.cpp
@@ -218,46 +218,46 @@ bool Macs2Engine::loadAmigaMxffFont() {
 	if (!decodeAmigaMxffFont(mxff.data(), size, glyphs) || glyphs.empty())
 		return false;
 
-	_numGlyphs = 0;
-	_maxGlyphHeight = 0;
-	amigaTextLinePitch = 0;
+	_text._numGlyphs = 0;
+	_text._maxGlyphHeight = 0;
+	_text.amigaTextLinePitch = 0;
 	if (size >= 0x0A) {
 		const uint16 atlasRows = READ_BE_UINT16(mxff.data() + 8);
 		if (atlasRows > 1)
-			amigaTextLinePitch = (uint16)(atlasRows - 1);
+			_text.amigaTextLinePitch = (uint16)(atlasRows - 1);
 	}
 	for (uint i = 0; i < glyphs.size() && i < 256; i++) {
-		_glyphs[i]._ascii = glyphs[i].ascii;
-		_glyphs[i]._width = glyphs[i].width;
-		_glyphs[i]._height = glyphs[i].height;
-		_glyphs[i]._data = Common::move(glyphs[i].pixels);
+		_text._glyphs[i]._ascii = glyphs[i].ascii;
+		_text._glyphs[i]._width = glyphs[i].width;
+		_text._glyphs[i]._height = glyphs[i].height;
+		_text._glyphs[i]._data = Common::move(glyphs[i].pixels);
 		// MXFF dialogue glyphs use copper COLOR23 (black) + COLOR27 (near-white
 		// outline). Remap into private UI bank 0xF0.. so outdoor copper cannot
 		// recolor text. COLOR17+i -> 0xF1+i (MXIN ui[1+i]); COLOR23->0xF7=ui[7]=0,
 		// COLOR27->0xFB=ui[11]=EEE. drawText @ 00224492 blits via drawSprite.
-		for (uint p = 0; p < _glyphs[i]._data.size(); p++) {
-			const byte c = _glyphs[i]._data[p];
+		for (uint p = 0; p < _text._glyphs[i]._data.size(); p++) {
+			const byte c = _text._glyphs[i]._data[p];
 			if (c == 0)
 				continue;
 			if (c >= 17 && c <= 31)
-				_glyphs[i]._data[p] = (byte)(0xF0 + (c - 16));
+				_text._glyphs[i]._data[p] = (byte)(0xF0 + (c - 16));
 			else if (c < 16)
-				_glyphs[i]._data[p] = (byte)(0xF0 + c);
+				_text._glyphs[i]._data[p] = (byte)(0xF0 + c);
 		}
-		_maxGlyphHeight = MAX(_maxGlyphHeight, _glyphs[i]._height);
-		_numGlyphs++;
+		_text._maxGlyphHeight = MAX(_text._maxGlyphHeight, _text._glyphs[i]._height);
+		_text._numGlyphs++;
 	}
-	if (amigaTextLinePitch == 0 && _maxGlyphHeight > 1)
-		amigaTextLinePitch = (uint16)(_maxGlyphHeight - 1);
+	if (_text.amigaTextLinePitch == 0 && _text._maxGlyphHeight > 1)
+		_text.amigaTextLinePitch = (uint16)(_text._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++)
-		_panelGlyphs[i] = _glyphs[i];
+	_text.numPanelGlyphs = _text._numGlyphs;
+	_text.maxPanelGlyphHeight = _text._maxGlyphHeight;
+	for (uint i = 0; i < _text._numGlyphs; i++)
+		_text._panelGlyphs[i] = _text._glyphs[i];
 
 	debugC(1, kDebugFilePath, "Amiga: loaded MXFF font FF_0000 (%u glyphs, height %u, linePitch %u)",
-		   _numGlyphs, _maxGlyphHeight, amigaTextLinePitch);
-	return _numGlyphs > 0;
+		   _text._numGlyphs, _text._maxGlyphHeight, _text.amigaTextLinePitch);
+	return _text._numGlyphs > 0;
 }
 
 bool Macs2Engine::loadAmigaOverlayFontResource(uint16 ffId) {
@@ -279,27 +279,27 @@ bool Macs2Engine::loadAmigaOverlayFontResource(uint16 ffId) {
 	if (!decodeAmigaMxffFont(mxff.data(), size, glyphs) || glyphs.empty())
 		return false;
 
-	numOverlayGlyphs = 0;
-	maxOverlayGlyphHeight = 0;
+	_text.numOverlayGlyphs = 0;
+	_text.maxOverlayGlyphHeight = 0;
 	for (uint i = 0; i < glyphs.size() && i < 256; i++) {
-		_overlayGlyphs[i]._ascii = glyphs[i].ascii;
-		_overlayGlyphs[i]._width = glyphs[i].width;
-		_overlayGlyphs[i]._height = glyphs[i].height;
-		_overlayGlyphs[i]._data = Common::move(glyphs[i].pixels);
+		_text._overlayGlyphs[i]._ascii = glyphs[i].ascii;
+		_text._overlayGlyphs[i]._width = glyphs[i].width;
+		_text._overlayGlyphs[i]._height = glyphs[i].height;
+		_text._overlayGlyphs[i]._data = Common::move(glyphs[i].pixels);
 		// Same copper->UI-bank remap as loadAmigaMxffFont.
-		for (uint p = 0; p < _overlayGlyphs[i]._data.size(); p++) {
-			const byte c = _overlayGlyphs[i]._data[p];
+		for (uint p = 0; p < _text._overlayGlyphs[i]._data.size(); p++) {
+			const byte c = _text._overlayGlyphs[i]._data[p];
 			if (c == 0)
 				continue;
 			if (c >= 17 && c <= 31)
-				_overlayGlyphs[i]._data[p] = (byte)(0xF0 + (c - 16));
+				_text._overlayGlyphs[i]._data[p] = (byte)(0xF0 + (c - 16));
 			else if (c < 16)
-				_overlayGlyphs[i]._data[p] = (byte)(0xF0 + c);
+				_text._overlayGlyphs[i]._data[p] = (byte)(0xF0 + c);
 		}
-		maxOverlayGlyphHeight = MAX(maxOverlayGlyphHeight, _overlayGlyphs[i]._height);
-		numOverlayGlyphs++;
+		_text.maxOverlayGlyphHeight = MAX(_text.maxOverlayGlyphHeight, _text._overlayGlyphs[i]._height);
+		_text.numOverlayGlyphs++;
 	}
-	return numOverlayGlyphs > 0;
+	return _text.numOverlayGlyphs > 0;
 }
 
 bool Macs2Engine::loadAmigaOverlayFont(uint8 resourceIndex) {
@@ -312,13 +312,13 @@ bool Macs2Engine::loadAmigaOverlayFont(uint8 resourceIndex) {
 	}
 
 	// Fall back to the already-loaded main MXFF dialogue font.
-	if (_numGlyphs == 0)
+	if (_text._numGlyphs == 0)
 		return false;
 
-	numOverlayGlyphs = _numGlyphs;
-	maxOverlayGlyphHeight = _maxGlyphHeight;
-	for (uint i = 0; i < _numGlyphs; i++)
-		_overlayGlyphs[i] = _glyphs[i];
+	_text.numOverlayGlyphs = _text._numGlyphs;
+	_text.maxOverlayGlyphHeight = _text._maxGlyphHeight;
+	for (uint i = 0; i < _text._numGlyphs; i++)
+		_text._overlayGlyphs[i] = _text._glyphs[i];
 	return true;
 }
 
diff --git a/engines/macs2/debugtools.cpp b/engines/macs2/debugtools.cpp
index 3a1b57c61fc..a6e3c8d215e 100644
--- a/engines/macs2/debugtools.cpp
+++ b/engines/macs2/debugtools.cpp
@@ -792,7 +792,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->_text._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 754b73fa498..1dd55630ec2 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -224,23 +224,23 @@ void Macs2Engine::loadResourceFileV1() {
 	uint32 font1SizeField = _fileStream->readUint32LE(); // skip size field
 	(void)font1SizeField;
 	uint16 font1GlyphCount = _fileStream->readUint16LE();
-	_maxGlyphHeight = 0;
+	_text._maxGlyphHeight = 0;
 	for (uint i = 0; i < font1GlyphCount; i++) {
-		_glyphs[i].readFromMemory(_fileStream);
-		_maxGlyphHeight = MAX(_glyphs[i]._height, _maxGlyphHeight);
+		_text._glyphs[i].readFromMemory(_fileStream);
+		_text._maxGlyphHeight = MAX(_text._glyphs[i]._height, _text._maxGlyphHeight);
 	}
-	_numGlyphs = font1GlyphCount;
+	_text._numGlyphs = font1GlyphCount;
 
 	// Font 2: clean sans-serif font used by save/load panel (scene data offset 0x1044)
 	uint32 font2SizeField = _fileStream->readUint32LE();
 	(void)font2SizeField;
 	uint16 font2GlyphCount = _fileStream->readUint16LE();
-	maxPanelGlyphHeight = 0;
+	_text.maxPanelGlyphHeight = 0;
 	for (uint i = 0; i < font2GlyphCount && i < 256; i++) {
-		_panelGlyphs[i].readFromMemory(_fileStream);
-		maxPanelGlyphHeight = MAX(maxPanelGlyphHeight, _panelGlyphs[i]._height);
+		_text._panelGlyphs[i].readFromMemory(_fileStream);
+		_text.maxPanelGlyphHeight = MAX(_text.maxPanelGlyphHeight, _text._panelGlyphs[i]._height);
 	}
-	numPanelGlyphs = font2GlyphCount;
+	_text.numPanelGlyphs = font2GlyphCount;
 
 	// Map scene offsets -> scene+0x5DDB. First entry is the help screen image offset.
 	for (uint i = 0; i < kMcsV1MapSceneOffsetCount; i++) {
@@ -262,8 +262,8 @@ void Macs2Engine::loadResourceFileV2() {
 	//   TalkVol + Font1 + SysFont + 0x400 map offsets
 	_shadingTable.clear();
 	_shadingTable.resize(0x800, 0);
-	_numGlyphs = 0;
-	numPanelGlyphs = 0;
+	_text._numGlyphs = 0;
+	_text.numPanelGlyphs = 0;
 	memset(_mapSceneOffsets, 0, sizeof(_mapSceneOffsets));
 	_imageResources.clear();
 	_imageResources.resize(33);
@@ -467,9 +467,9 @@ void Macs2Engine::loadResourceFileV2() {
 		_fileStream->seek(fontStart + (int64)fontSize, SEEK_SET);
 		return true;
 	};
-	if (!loadSizedFont(_glyphs, _numGlyphs, _maxGlyphHeight))
+	if (!loadSizedFont(_text._glyphs, _text._numGlyphs, _text._maxGlyphHeight))
 		warning("readGlobalAssetsV2: failed loading Font1");
-	if (!loadSizedFont(_panelGlyphs, numPanelGlyphs, maxPanelGlyphHeight))
+	if (!loadSizedFont(_text._panelGlyphs, _text.numPanelGlyphs, _text.maxPanelGlyphHeight))
 		warning("readGlobalAssetsV2: failed loading SysFont");
 
 	for (int i = 0; i < ARRAYSIZE(_mapSceneOffsets); i++)
@@ -492,7 +492,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);
+		   _text._numGlyphs, _text.numPanelGlyphs);
 	_fileStream->seek(kMcsV2ActorIndexOffset, SEEK_SET);
 	bootstrapMcsActorsObjectsAndScene();
 }
@@ -2012,26 +2012,16 @@ bool Macs2Engine::loadOverlayFont(uint8 resourceIndex, uint16 executingObjectID)
 		return false;
 	}
 
-	numOverlayGlyphs = glyphCount;
-	maxOverlayGlyphHeight = 0;
+	_text.numOverlayGlyphs = glyphCount;
+	_text.maxOverlayGlyphHeight = 0;
 	for (uint i = 0; i < glyphCount; i++) {
-		_overlayGlyphs[i].readFromMemory(_fileStream);
-		maxOverlayGlyphHeight = MAX(maxOverlayGlyphHeight, _overlayGlyphs[i]._height);
+		_text._overlayGlyphs[i].readFromMemory(_fileStream);
+		_text.maxOverlayGlyphHeight = MAX(_text.maxOverlayGlyphHeight, _text._overlayGlyphs[i]._height);
 	}
 	_fileStream->seek(oldPos, SEEK_SET);
 	return true;
 }
 
-bool Macs2Engine::findGlyph(char c, GlyphData &out) const {
-	for (int i = 0; i < _numGlyphs; i++) {
-		if (_glyphs[i]._ascii == c) {
-			out = _glyphs[i];
-			return true;
-		}
-	}
-	return false;
-}
-
 void Macs2Engine::updateBackgroundAnimationDepthMap(size_t animIndex) {
 	if (isV2() || _sceneDepthMap.w == 0 || animIndex >= _backgroundAnimations.size()) {
 		return;
@@ -2498,40 +2488,6 @@ bool Macs2Engine::readInputFrame(uint16 &mouseX, uint16 &mouseY, uint16 &buttons
 	return !_inputPlaybackStream->eos();
 }
 
-int Macs2Engine::measureString(const Common::String &s) {
-	int sum = 0;
-	GlyphData currentGlyph;
-	uint16 widestGlyph = 0;
-	for (auto current = s.begin(); current != s.end(); current++) {
-		bool found = findGlyph(*current, currentGlyph);
-		if (found) {
-			widestGlyph = MAX(widestGlyph, currentGlyph._width);
-		}
-	}
-
-	for (auto current = s.begin(); current != s.end(); current++) {
-		bool found = findGlyph(*current, currentGlyph);
-		if (!found) {
-			sum += widestGlyph;
-		} else {
-			sum += currentGlyph._width + 1;
-		}
-	}
-	return sum;
-}
-
-int Macs2Engine::measureStringsVertically(const Common::StringArray &sa) {
-	return (int)sa.size() * dialogLineHeight();
-}
-
-int Macs2Engine::measureStrings(const Common::StringArray &sa) {
-	int max = -1;
-	for (auto iter = sa.begin(); iter != sa.end(); iter++) {
-		max = MAX(measureString(*iter), max);
-	}
-	return max;
-}
-
 int Macs2Engine::computeStringIndex(Common::MemoryReadStream *stream, int targetOffset) {
 	stream->seek(0);
 	int index = 0;
@@ -3198,22 +3154,6 @@ bool Macs2Engine::tick() {
 	return result;
 }
 
-void GlyphData::readFromeFile(Common::File &file) {
-	_ascii = file.readByte();
-	_width = file.readUint16LE();
-	_height = file.readUint16LE();
-	_data.resize(_width * _height);
-	file.read(_data.data(), _width * _height);
-}
-
-void GlyphData::readFromMemory(Common::SeekableReadStream *stream) {
-	_ascii = stream->readByte();
-	_width = stream->readUint16LE();
-	_height = stream->readUint16LE();
-	_data.resize(_width * _height);
-	stream->read(_data.data(), _width * _height);
-}
-
 void AnimFrame::readFromeFile(Common::File &file) {
 	_width = file.readUint16LE();
 	_height = file.readUint16LE();
diff --git a/engines/macs2/macs2.h b/engines/macs2/macs2.h
index 082f4d0b1b7..b94ae4fe37f 100644
--- a/engines/macs2/macs2.h
+++ b/engines/macs2/macs2.h
@@ -44,6 +44,8 @@
 #include "macs2/macs2_constants.h"
 #include "macs2/pathfinding.h"
 #include "macs2/scriptexecutor.h"
+#include "macs2/sprite.h"
+#include "macs2/text.h"
 
 namespace Macs2 {
 
@@ -99,19 +101,6 @@ public:
 struct Macs2GameDescription;
 class Music;
 
-struct Sprite {
-	uint16 _width = 0;
-	uint16 _height = 0;
-	Common::Array<uint8> _data;
-};
-
-struct GlyphData : public Sprite {
-	char _ascii = 0;
-
-	void readFromeFile(Common::File &file);
-	void readFromMemory(Common::SeekableReadStream *stream);
-};
-
 struct AnimFrame : public Sprite {
 	int16 _offsetX = 0;
 	int16 _offsetY = 0;
@@ -435,13 +424,7 @@ public:
 	/** Per-cursor hotspot from native HUD button metadata (v2); (0,0) = use center. */
 	Common::Point _cursorHotspots[33];
 
-	GlyphData _glyphs[256];
-	GlyphData _panelGlyphs[256]; // Font 2: clean sans-serif font used by save/load panel
-	GlyphData _overlayGlyphs[256];
-	uint16 numOverlayGlyphs = 0;
-	uint16 maxOverlayGlyphHeight = 0;
-	uint16 numPanelGlyphs = 0;
-	uint16 maxPanelGlyphHeight = 0;
+	Text _text;
 	bool loadOverlayFont(uint8 resourceIndex, uint16 executingObjectID);
 	/**
 	 * Resolve scene/object resource table entry to an absolute MCS file offset.
@@ -515,15 +498,9 @@ public:
 	bool tickDeltaPlayback();
 	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;
-
 	AnimFrame _animFrames[6];
 	// 6 flag/decoration animation frames at fixed file offset 0x6A5941, each followed by 6 padding bytes
 
-	bool findGlyph(char c, GlyphData &out) const;
-
 	// Character shading remap (loadResourceFile @ 1008:2e8d -> scene+0x53D3).
 	// Indexed as (color - 0xC0) * 0x20 + shadowIntensity (drawSpriteTransparent @ 1010:0ed1).
 	Common::Array<byte> _shadingTable;
@@ -542,8 +519,6 @@ public:
 	/** Absolute file offset of the 0x3000-byte scene/object directory. */
 	uint32 _mcsDirectoryOffset = kMcsV1DirectoryOffset;
 
-	/** Amiga MXFF line pitch: measureTextWidth @ 00224420 uses (font[+8] - 1). */
-	uint16 amigaTextLinePitch = 0;
 	/** True after loadAmigaSceneBackground installed copper colors in 0..31. */
 	bool _amigaNativePlayfieldPalette = false;
 	Common::Array<byte> _amigaLineCopperPal;
@@ -729,11 +704,6 @@ public:
 	// Schedules a run of the script the next time the executor is ticked
 	void scheduleRun(bool initScene = false);
 
-	int measureString(const Common::String &s);
-
-	int measureStrings(const Common::StringArray &sa);
-	int measureStringsVertically(const Common::StringArray &sa);
-
 	Common::StringArray decodeStrings(Common::MemoryReadStream *stream, int offset, int numStrings, int sceneId = 0, int objectId = 0);
 
 	// --- Translation support ---
@@ -828,13 +798,13 @@ public:
 	int16 scaleScriptCoord(int16 coord) const { return isV2() ? (int16)(coord * 2) : coord; }
 
 	/** Dialogue / text-box chrome (DOS l0037_B368 / B462). */
-	int dialogPadW() const { return isAmiga() ? 0x08 : 0x12; }
-	int dialogPadH() const { return isAmiga() ? 0x08 : 0x10; }
-	int dialogTextInset() const { return isAmiga() ? 0x04 : 0x09; }
+	int dialogPadW() const { return isAmiga() ? 8 : 18; }
+	int dialogPadH() const { return isAmiga() ? 8 : 16; }
+	int dialogTextInset() const { return isAmiga() ? 4 : 9; }
 	int dialogLineGap() const { return 2; }
-	int portraitBorderPad() const { return isAmiga() ? 2 : 0x0D; }
+	int portraitBorderPad() const { return isAmiga() ? 2 : 13; }
 	int portraitContentInset() const { return isAmiga() ? 1 : 7; }
-	int portraitTextGap() const { return isAmiga() ? 0x0A : 0x12; }
+	int portraitTextGap() const { return isAmiga() ? 10 : 18; }
 	/**
 	 * Per-line step for dialogue layout.
 	 * DOS: maxGlyphHeight + dialogLineGap(). Amiga: absolute MXFF pitch
@@ -842,8 +812,8 @@ public:
 	 */
 	int dialogLineHeight() const {
 		if (isAmiga())
-			return amigaTextLinePitch ? (int)amigaTextLinePitch : (int)_maxGlyphHeight;
-		return (int)_maxGlyphHeight + dialogLineGap();
+			return _text.amigaTextLinePitch ? (int)_text.amigaTextLinePitch : (int)_text._maxGlyphHeight;
+		return (int)_text._maxGlyphHeight + dialogLineGap();
 	}
 
 	/** Depth-map compare Y for sprite occlusion (halved on v2 full-res depth). */
diff --git a/engines/macs2/module.mk b/engines/macs2/module.mk
index f64c37fdf61..5caf65419a5 100644
--- a/engines/macs2/module.mk
+++ b/engines/macs2/module.mk
@@ -17,6 +17,7 @@ MODULE_OBJS = \
 	metaengine.o \
 	saveload.o \
 	scriptexecutor.o \
+	text.o \
 	actionbar.o \
 	view1.o
 
diff --git a/engines/macs2/scriptexecutor.cpp b/engines/macs2/scriptexecutor.cpp
index 0933e7f0aef..7b5a9835fa3 100644
--- a/engines/macs2/scriptexecutor.cpp
+++ b/engines/macs2/scriptexecutor.cpp
@@ -413,7 +413,8 @@ void ScriptExecutor::scriptPrintString(bool alignRight) {
 	int stringBoxX = x;
 	const int stringBoxY = y;
 	if (alignRight) {
-		const int totalWidth = g_engine->measureStrings(strings) + g_engine->dialogPadW();
+		Text *text = &g_engine->_text;
+		const int totalWidth = text->measureStrings(strings) + g_engine->dialogPadW();
 		stringBoxX -= totalWidth;
 	}
 
diff --git a/engines/macs2/sprite.h b/engines/macs2/sprite.h
new file mode 100644
index 00000000000..819562ce615
--- /dev/null
+++ b/engines/macs2/sprite.h
@@ -0,0 +1,38 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MACS2_SPRITE_H
+#define MACS2_SPRITE_H
+
+#include "common/array.h"
+#include "common/scummsys.h"
+
+namespace Macs2 {
+
+struct Sprite {
+	uint16 _width = 0;
+	uint16 _height = 0;
+	Common::Array<uint8> _data;
+};
+
+}
+
+#endif
diff --git a/engines/macs2/text.cpp b/engines/macs2/text.cpp
new file mode 100644
index 00000000000..24fae48ba48
--- /dev/null
+++ b/engines/macs2/text.cpp
@@ -0,0 +1,106 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "macs2/text.h"
+#include "common/util.h"
+
+namespace Macs2 {
+
+void GlyphData::readFromeFile(Common::File &file) {
+	_ascii = file.readByte();
+	_width = file.readUint16LE();
+	_height = file.readUint16LE();
+	_data.resize(_width * _height);
+	file.read(_data.data(), _width * _height);
+}
+
+void GlyphData::readFromMemory(Common::SeekableReadStream *stream) {
+	_ascii = stream->readByte();
+	_width = stream->readUint16LE();
+	_height = stream->readUint16LE();
+	_data.resize(_width * _height);
+	stream->read(_data.data(), _width * _height);
+}
+
+bool Text::findGlyph(char c, GlyphData &out, const GlyphData *glyphs, uint16 numGlyphs) const {
+	for (uint16 i = 0; i < numGlyphs; i++) {
+		if (glyphs[i]._ascii == c) {
+			out = glyphs[i];
+			return true;
+		}
+	}
+	return false;
+}
+
+bool Text::findGlyph(char c, GlyphData &out) const {
+	return findGlyph(c, out, _glyphs, _numGlyphs);
+}
+
+int Text::measureString(const Common::String &s) const {
+	int sum = 0;
+	GlyphData currentGlyph;
+	uint16 widestGlyph = 0;
+	for (auto current = s.begin(); current != s.end(); current++) {
+		if (findGlyph(*current, currentGlyph)) {
+			widestGlyph = MAX(widestGlyph, currentGlyph._width);
+		}
+	}
+
+	for (auto current = s.begin(); current != s.end(); current++) {
+		if (!findGlyph(*current, currentGlyph)) {
+			sum += widestGlyph;
+		} else {
+			sum += currentGlyph._width + 1;
+		}
+	}
+	return sum;
+}
+
+int Text::measureString(const Common::String &s, const GlyphData *glyphs, uint16 numGlyphs) const {
+	int width = 0;
+	uint16 widestGlyph = 1;
+	for (uint i = 0; i < numGlyphs; i++) {
+		widestGlyph = MAX(widestGlyph, glyphs[i]._width);
+	}
+	for (auto iter = s.begin(); iter != s.end(); iter++) {
+		GlyphData glyph;
+		if (findGlyph(*iter, glyph, glyphs, numGlyphs)) {
+			width += glyph._width + 1;
+		} else {
+			width += widestGlyph;
+		}
+	}
+	return width;
+}
+
+int Text::measureStrings(const Common::StringArray &sa) const {
+	int max = -1;
+	for (auto iter = sa.begin(); iter != sa.end(); iter++) {
+		max = MAX(measureString(*iter), max);
+	}
+	return max;
+}
+
+int Text::measureStringsVertically(const Common::StringArray &sa, int lineHeight) const {
+	return (int)sa.size() * lineHeight;
+}
+
+} // namespace Macs2
diff --git a/engines/macs2/text.h b/engines/macs2/text.h
new file mode 100644
index 00000000000..ffa76d64c90
--- /dev/null
+++ b/engines/macs2/text.h
@@ -0,0 +1,66 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MACS2_TEXT_H
+#define MACS2_TEXT_H
+
+#include "common/file.h"
+#include "common/str.h"
+#include "common/str-array.h"
+#include "common/stream.h"
+#include "macs2/sprite.h"
+
+namespace Macs2 {
+
+struct GlyphData : public Sprite {
+	char _ascii = 0;
+
+	void readFromeFile(Common::File &file);
+	void readFromMemory(Common::SeekableReadStream *stream);
+};
+
+class Text {
+public:
+	static constexpr uint kMaxGlyphs = 256;
+
+	GlyphData _glyphs[kMaxGlyphs];
+	GlyphData _panelGlyphs[kMaxGlyphs];
+	GlyphData _overlayGlyphs[kMaxGlyphs];
+	uint16 _numGlyphs = 79;
+	uint16 _maxGlyphHeight = 0;
+	uint16 numOverlayGlyphs = 0;
+	uint16 maxOverlayGlyphHeight = 0;
+	uint16 numPanelGlyphs = 0;
+	uint16 maxPanelGlyphHeight = 0;
+	uint16 amigaTextLinePitch = 0;
+
+	bool findGlyph(char c, GlyphData &out) const;
+	bool findGlyph(char c, GlyphData &out, const GlyphData *glyphs, uint16 numGlyphs) const;
+
+	int measureString(const Common::String &s) const;
+	int measureString(const Common::String &s, const GlyphData *glyphs, uint16 numGlyphs) const;
+	int measureStrings(const Common::StringArray &sa) const;
+	int measureStringsVertically(const Common::StringArray &sa, int lineHeight) const;
+};
+
+} // namespace Macs2
+
+#endif
diff --git a/engines/macs2/view1.cpp b/engines/macs2/view1.cpp
index b9515c09446..88f8f4f7d98 100644
--- a/engines/macs2/view1.cpp
+++ b/engines/macs2/view1.cpp
@@ -753,6 +753,7 @@ void View1::drawCurrentSpeaker(Graphics::ManagedSurface &s) {
 
 void View1::renderString(uint16 x, uint16 y, const Common::String &s) {
 	Graphics::ManagedSurface surf = getSurface();
+	Text *text = &g_engine->_text;
 	uint16 currentX = x;
 	uint16 currentY = y;
 
@@ -760,7 +761,7 @@ void View1::renderString(uint16 x, uint16 y, const Common::String &s) {
 	uint16 widestGlyph = 1;
 	for (auto iter = s.begin(); iter != s.end(); iter++) {
 		GlyphData data;
-		if (g_engine->findGlyph(*iter, data)) {
+		if (text->findGlyph(*iter, data)) {
 			widestGlyph = MAX(widestGlyph, data._width);
 		}
 	}
@@ -768,7 +769,7 @@ void View1::renderString(uint16 x, uint16 y, const Common::String &s) {
 	// Second pass: render with correct spacing
 	for (auto iter = s.begin(); iter != s.end(); iter++) {
 		GlyphData data;
-		const bool found = g_engine->findGlyph(*iter, data);
+		const bool found = text->findGlyph(*iter, data);
 		if (found) {
 			drawSprite(currentX, currentY, data, surf, false);
 			currentX += data._width + 1;
@@ -786,20 +787,21 @@ void View1::renderString(const Common::Point &pos, const Common::String &s) {
 }
 
 void View1::renderStringTo(uint16 x, uint16 y, const Common::String &s, Graphics::ManagedSurface &surf) {
+	Text *text = &g_engine->_text;
 	uint16 currentX = x;
 	uint16 currentY = y;
 
 	uint16 widestGlyph = 1;
 	for (auto iter = s.begin(); iter != s.end(); iter++) {
 		GlyphData data;
-		if (g_engine->findGlyph(*iter, data)) {
+		if (text->findGlyph(*iter, data)) {
 			widestGlyph = MAX(widestGlyph, data._width);
 		}
 	}
 
 	for (auto iter = s.begin(); iter != s.end(); iter++) {
 		GlyphData data;
-		const bool found = g_engine->findGlyph(*iter, data);
+		const bool found = text->findGlyph(*iter, data);
 		if (found) {
 			drawSprite(currentX, currentY, data, surf, false);
 			currentX += data._width + 1;
@@ -810,25 +812,8 @@ void View1::renderStringTo(uint16 x, uint16 y, const Common::String &s, Graphics
 }
 
 int View1::measureStringWithFont(const Common::String &s, const GlyphData *glyphs, uint16 numGlyphs) {
-	int width = 0;
-	uint16 widestGlyph = 1;
-	for (uint i = 0; i < numGlyphs; i++) {
-		widestGlyph = MAX(widestGlyph, glyphs[i]._width);
-	}
-	for (auto iter = s.begin(); iter != s.end(); iter++) {
-		bool found = false;
-		for (uint i = 0; i < numGlyphs; i++) {
-			if (glyphs[i]._ascii == *iter) {
-				width += glyphs[i]._width + 1;
-				found = true;
-				break;
-			}
-		}
-		if (!found) {
-			width += widestGlyph;
-		}
-	}
-	return width;
+	Text *text = &g_engine->_text;
+	return text->measureString(s, glyphs, numGlyphs);
 }
 
 void View1::renderStringWithFont(uint16 x, uint16 y, const Common::String &s, const GlyphData *glyphs, uint16 numGlyphs) {
@@ -877,8 +862,8 @@ void View1::drawOverlayTextEntries() {
 		int x = entry.position.x;
 		const 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;
-		const uint16 fontCount = g_engine->numOverlayGlyphs > 0 ? g_engine->numOverlayGlyphs : g_engine->_numGlyphs;
+		const GlyphData *font = g_engine->_text.numOverlayGlyphs > 0 ? g_engine->_text._overlayGlyphs : g_engine->_text._glyphs;
+		const uint16 fontCount = g_engine->_text.numOverlayGlyphs > 0 ? g_engine->_text.numOverlayGlyphs : g_engine->_text._numGlyphs;
 
 		if (entry.alignment == 1) {
 			x -= measureStringWithFont(text, font, fontCount);
@@ -896,12 +881,13 @@ void View1::drawOverlayTextEntries() {
 }
 
 void View1::showStringBox(const Common::StringArray &sa) {
+	Text *text = &g_engine->_text;
 	const int padW = g_engine->dialogPadW();
 	const int padH = g_engine->dialogPadH();
 	const int textInset = g_engine->dialogTextInset();
 	const int lineHeight = g_engine->dialogLineHeight();
-	const int totalWidth = g_engine->measureStrings(sa) + padW;
-	const int totalHeight = g_engine->measureStringsVertically(sa) + padH;
+	const int totalWidth = text->measureStrings(sa) + padW;
+	const int totalHeight = text->measureStringsVertically(sa, lineHeight) + padH;
 	g_engine->_textLog.push_back(Common::String::format(
 									 "Render text box: lines=%u pos=(%d,%d) size=(%d,%d) text=\"", sa.size(),
 									 _stringBoxPosition.x, _stringBoxPosition.y, totalWidth, totalHeight) +
@@ -918,10 +904,11 @@ void View1::showStringBox(const Common::StringArray &sa) {
 }
 
 void View1::drawPathfindingPoints(Graphics::ManagedSurface &s) {
+	Text *text = &g_engine->_text;
 	GlyphData xData;
 	int xOffset = 0;
 	int yOffset = 0;
-	if (g_engine->findGlyph('x', xData)) {
+	if (text->findGlyph('x', xData)) {
 		xOffset = xData._width / 2;
 		yOffset = xData._height / 2;
 	}
@@ -1137,17 +1124,18 @@ bool View1::handleDialogueChoiceClick(int clickY, int clickX) {
 	// Checks if click is within text box bounds (X+9..X+W-9, Y+9..Y+H-9).
 	// Iterates choice entries to find which line was clicked.
 	// Stores script index at scene+0x53B7 and clears scene+0x53B9.
+	Text *text = &g_engine->_text;
 	const int padW = g_engine->dialogPadW();
 	const int padH = g_engine->dialogPadH();
 	const int textInset = g_engine->dialogTextInset();
-	const int boxW = g_engine->measureStrings(_drawnStringBox) + padW;
-	const int boxH = g_engine->measureStringsVertically(_drawnStringBox) + padH;
+	const int lineHeight = g_engine->dialogLineHeight();
+	const int boxW = text->measureStrings(_drawnStringBox) + padW;
+	const int boxH = text->measureStringsVertically(_drawnStringBox, lineHeight) + padH;
 	if (clickX < _stringBoxPosition.x + textInset || clickY < _stringBoxPosition.y + textInset ||
 		clickX > _stringBoxPosition.x + boxW - textInset || clickY > _stringBoxPosition.y + boxH - textInset) {
 		return false;
 	}
 
-	const int lineHeight = g_engine->dialogLineHeight();
 	const int firstLineY = _stringBoxPosition.y + textInset;
 	const int relY = clickY - firstLineY;
 	debug("handleDialogueChoiceClick: clickY=%d firstLineY=%d relY=%d lineHeight=%d clickedLine=%d",
@@ -3117,6 +3105,10 @@ void View1::drawSprite(int16 x, int16 y, const Sprite &sprite, Graphics::Managed
 	drawSprite(x, y, sprite._width, sprite._height, const_cast<byte *>(sprite._data.data()), s, mirrored, useDepth, depth, clipToGameArea);
 }
 
+void View1::drawSprite(int16 x, int16 y, const GlyphData &glyph, Graphics::ManagedSurface &s, bool mirrored, bool useDepth, uint8 depth, bool clipToGameArea) {
+	drawSprite(x, y, glyph._width, glyph._height, const_cast<byte *>(glyph._data.data()), s, mirrored, useDepth, depth, clipToGameArea);
+}
+
 void View1::drawSpriteClipped(uint16 x, uint16 y, const Common::Rect &clippingRect, uint16 width, uint16 height, const byte *const data, Graphics::ManagedSurface &s) {
 	for (int currentX = 0; currentX < width; currentX++) {
 		for (int currentY = 0; currentY < height; currentY++) {
@@ -3314,11 +3306,12 @@ void View1::showSpeechAct(uint16 characterIndex, const Common::Array<Common::Str
 	_currentSpeechActData.position = position;
 	_currentSpeechActData.onRightSide = onRightSide;
 
+	Text *text = &g_engine->_text;
 	const int padW = g_engine->dialogPadW();
 	const int padH = g_engine->dialogPadH();
 	const int portraitGap = g_engine->portraitTextGap();
-	const int totalWidth = g_engine->measureStrings(strings) + padW;
-	const int totalHeight = g_engine->measureStringsVertically(strings) + padH;
+	const int totalWidth = text->measureStrings(strings) + padW;
+	const int totalHeight = text->measureStringsVertically(strings, g_engine->dialogLineHeight()) + padH;
 	int stringBoxX = position.x;
 	int stringBoxY = position.y;
 	Common::Point portraitBoxPosition = position;
@@ -3753,8 +3746,8 @@ void View1::drawOriginalSaveLoadPanel(Graphics::ManagedSurface &s) {
 		} else {
 			label = "NONE";
 		}
-		const GlyphData *font = g_engine->numPanelGlyphs > 0 ? g_engine->_panelGlyphs : g_engine->_glyphs;
-		const uint16 fontCount = g_engine->numPanelGlyphs > 0 ? g_engine->numPanelGlyphs : g_engine->_numGlyphs;
+		const GlyphData *font = g_engine->_text.numPanelGlyphs > 0 ? g_engine->_text._panelGlyphs : g_engine->_text._glyphs;
+		const uint16 fontCount = g_engine->_text.numPanelGlyphs > 0 ? g_engine->_text.numPanelGlyphs : g_engine->_text._numGlyphs;
 		renderStringWithFont(panelX + 6, panelY + 6 + slot * slotH, label, font, fontCount);
 	}
 }
diff --git a/engines/macs2/view1.h b/engines/macs2/view1.h
index ae8541dd33a..0106c066122 100644
--- a/engines/macs2/view1.h
+++ b/engines/macs2/view1.h
@@ -200,6 +200,7 @@ private:
 
 	void drawSprite(int16 x, int16 y, uint16 width, uint16 height, byte *data, Graphics::ManagedSurface &s, bool mirrored, bool useDepth = false, uint8 depth = 0, bool clipToGameArea = false);
 	void drawSprite(int16 x, int16 y, const Sprite &sprite, Graphics::ManagedSurface &s, bool mirrored, bool useDepth = false, uint8 depth = 0, bool clipToGameArea = false);
+	void drawSprite(int16 x, int16 y, const GlyphData &glyph, Graphics::ManagedSurface &s, bool mirrored, bool useDepth = false, uint8 depth = 0, bool clipToGameArea = false);
 	void drawSprite(const Common::Point &pos, uint16 width, uint16 height, byte *data, Graphics::ManagedSurface &s, bool mirrored, bool useDepth = false, uint8 depth = 0, bool clipToGameArea = false);
 
 	void drawSpriteClipped(uint16 x, uint16 y, const Common::Rect &clippingRect, uint16 width, uint16 height, const byte *const data, Graphics::ManagedSurface &s);


Commit: c7ca3456d20b9fc6f979fe54b297a6ed5de382e5
    https://github.com/scummvm/scummvm/commit/c7ca3456d20b9fc6f979fe54b297a6ed5de382e5
Author: Martin Gerhardy (martin.gerhardy at gmail.com)
Date: 2026-09-03T19:50:40+02:00

Commit Message:
MACS2: don't read opl values from exe, but embed them in the engine code

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


diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp
index 1dd55630ec2..fe306dc2641 100644
--- a/engines/macs2/macs2.cpp
+++ b/engines/macs2/macs2.cpp
@@ -641,8 +641,6 @@ void Macs2Engine::readExecutable() {
 	// but the whole Data5 segment is identical (1020:0000...1020:3787)
 	// TODO: if there are ever other games using different versions of MCSEXEC.EXE, we should check the checksum here
 
-	_music->readDataFromExecutable(exeFileStream.get());
-
 	exeFileStream->seek(0x0001B610, SEEK_SET);
 	exeFileStream->read(inventoryIconIndices.data(), 12);
 
diff --git a/engines/macs2/music.cpp b/engines/macs2/music.cpp
index 3d9a54b224f..0d74323a3be 100644
--- a/engines/macs2/music.cpp
+++ b/engines/macs2/music.cpp
@@ -25,7 +25,6 @@
 #include "common/config-manager.h"
 #include "common/endian.h"
 #include "common/file.h"
-#include "common/memstream.h"
 #include "common/util.h"
 #include "engines/macs2/midiparser_macs2.h"
 
@@ -33,6 +32,58 @@
 
 namespace Macs2 {
 
+// AdLib lookup tables from MCSEXEC.EXE Data5. Full and demo binaries match at these offsets.
+static const uint8 kOpSlotTable[18] = {
+	0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x08, 0x09,
+	0x0A, 0x0B, 0x0C, 0x0D, 0x10, 0x11, 0x12, 0x13,
+	0x14, 0x15
+};
+
+// Voice -> modulator operator (MCSEXEC 0x1B68D)
+static const uint8 kOpMap1[9] = {
+	0x00, 0x01, 0x02, 0x08, 0x09, 0x0A, 0x10, 0x11, 0x12
+};
+
+// Voice -> carrier operator (MCSEXEC 0x1B696)
+static const uint8 kOpMap2[9] = {
+	0x03, 0x04, 0x05, 0x0B, 0x0C, 0x0D, 0x13, 0x14, 0x15
+};
+
+// MIDI note -> OPL F-number low byte (MCSEXEC 0x1B69F)
+static const uint8 kFreqTableLo[128] = {
+	0xAB, 0xB5, 0xC0, 0xCC, 0xD8, 0xE5, 0xF2, 0x01, 0x10, 0x20, 0x31, 0x43, 0x57, 0x6B, 0x81, 0x98,
+	0xB0, 0xCA, 0xE5, 0x02, 0x20, 0x41, 0x63, 0x87, 0x57, 0x6B, 0x81, 0x98, 0xB0, 0xCA, 0xE5, 0x02,
+	0x20, 0x41, 0x63, 0x87, 0x57, 0x6B, 0x81, 0x98, 0xB0, 0xCA, 0xE5, 0x02, 0x20, 0x41, 0x63, 0x87,
+	0x57, 0x6B, 0x81, 0x98, 0xB0, 0xCA, 0xE5, 0x02, 0x20, 0x41, 0x63, 0x87, 0x57, 0x6B, 0x81, 0x98,
+	0xB0, 0xCA, 0xE5, 0x02, 0x20, 0x41, 0x63, 0x87, 0x57, 0x6B, 0x81, 0x98, 0xB0, 0xCA, 0xE5, 0x02,
+	0x20, 0x41, 0x63, 0x87, 0x57, 0x6B, 0x81, 0x98, 0xB0, 0xCA, 0xE5, 0x02, 0x20, 0x41, 0x63, 0x87,
+	0x57, 0x6B, 0x81, 0x98, 0xB0, 0xCA, 0xE5, 0x02, 0x20, 0x41, 0x63, 0x87, 0xAE, 0xD6, 0x02, 0x30,
+	0x60, 0x94, 0xCA, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
+};
+
+// MIDI note -> OPL block / F-number high byte (MCSEXEC 0x1B71F)
+static const uint8 kFreqTableHi[128] = {
+	0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21,
+	0x21, 0x21, 0x21, 0x22, 0x22, 0x22, 0x22, 0x22, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x25, 0x26,
+	0x26, 0x26, 0x26, 0x26, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x29, 0x2A, 0x2A, 0x2A, 0x2A, 0x2A,
+	0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x2E, 0x2E, 0x2E, 0x2E, 0x2E, 0x31, 0x31, 0x31, 0x31,
+	0x31, 0x31, 0x31, 0x32, 0x32, 0x32, 0x32, 0x32, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x36,
+	0x36, 0x36, 0x36, 0x36, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x3A, 0x3A, 0x3A, 0x3A, 0x3A,
+	0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3D, 0x3E, 0x3E, 0x3E, 0x3E, 0x3E, 0x3E, 0x3E, 0x3F, 0x3F,
+	0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F
+};
+
+static const uint8 kPercVolTable[32] = {
+	28, 25, 23, 18, 14, 11, 8, 2,
+	50, 42, 37, 35, 34, 32, 30, 2,
+	55, 50, 49, 48, 45, 43, 40, 2,
+	60, 60, 58, 56, 54, 52, 50, 2
+};
+
+static const uint8 kPercOpMap[5] = {19, 20, 18, 21, 17};
+static const uint8 kPercFreqChannel[5] = {6, 7, 8, 8, 7};
+
+
 Music::Music() : _opl(nullptr), _parser(nullptr), _playing(false),
 				 _masterVolume(0), _numOplChannels(9), _instrumentDataOffset(0),
 				 _smf(nullptr), _smfDucked(false), _smfVolumeBeforeDuck(192) {
@@ -91,8 +142,8 @@ void Music::silenceAll() {
 		writeReg(i + 0xB0, readReg(i + 0xB0) & 0xDF);
 	}
 	// Silence all operator volumes
-	for (int i = 0; i < (int)_opSlotTable.size() && i <= 0x11; i++) {
-		writeReg(_opSlotTable[i] + 0x40, 0xFF);
+	for (int i = 0; i < ARRAYSIZE(kOpSlotTable); i++) {
+		writeReg(kOpSlotTable[i] + 0x40, 0xFF);
 	}
 }
 
@@ -325,10 +376,10 @@ void Music::noteOn(byte channel, byte note, byte velocity) {
 
 		// Key off, set volumes, then key on
 		writeReg(voice + 0xB0, 0);
-		const byte reg2 = readReg(_opMap2[voice] + 0x40);
-		writeReg(_opMap2[voice] + 0x40, (reg2 & 0xC0) + vol1);
-		const byte reg1 = readReg(_opMap1[voice] + 0x40);
-		writeReg(_opMap1[voice] + 0x40, (reg1 & 0xC0) + vol2);
+		const byte reg2 = readReg(kOpMap2[voice] + 0x40);
+		writeReg(kOpMap2[voice] + 0x40, (reg2 & 0xC0) + vol1);
+		const byte reg1 = readReg(kOpMap1[voice] + 0x40);
+		writeReg(kOpMap1[voice] + 0x40, (reg1 & 0xC0) + vol2);
 
 		_channelPitchBend[channel] = 0;
 		setFrequency(voice, note, 0);
@@ -337,14 +388,14 @@ void Music::noteOn(byte channel, byte note, byte velocity) {
 		const uint16 instBase = (uint16)_channelPrograms[channel] << 4;
 		const uint8 percIdx = channel - 0x0B;
 
-		if (percIdx >= _percOpMap.size())
+		if (percIdx >= ARRAYSIZE(kPercOpMap))
 			return;
 
-		const uint8 opIdx = _percOpMap[percIdx];
+		const uint8 opIdx = kPercOpMap[percIdx];
 
 		if (channel == 0x0B) {
 			// Bass drum: load full instrument
-			loadInstrument(_percFreqChannel[percIdx], _channelPrograms[0x0B]);
+			loadInstrument(kPercFreqChannel[percIdx], _channelPrograms[0x0B]);
 		} else if ((uint32)_instrumentDataOffset + instBase + 9 <= _songData.size()) {
 			const byte *instPtr = _songData.data() + _instrumentDataOffset + instBase;
 			writeReg(opIdx + 0x20, instPtr[0]);
@@ -359,13 +410,13 @@ void Music::noteOn(byte channel, byte note, byte velocity) {
 		if ((uint32)(_instrumentDataOffset + instBase + 3) < _songData.size()) {
 			const byte volByte = _songData[_instrumentDataOffset + instBase + 3];
 			const uint8 volIdx = ((volByte & 0x3F) >> 4) * 8 + (velocity >> 4);
-			if (volIdx < _percVolTable.size())
-				vol = _percVolTable[volIdx] + _masterVolume;
+			if (volIdx < ARRAYSIZE(kPercVolTable))
+				vol = kPercVolTable[volIdx] + _masterVolume;
 		}
 		if (vol > 0x3F)
 			vol = 0x3F;
 
-		const uint8 freqChan = _percFreqChannel[percIdx];
+		const uint8 freqChan = kPercFreqChannel[percIdx];
 		writeReg(freqChan + 0xB0, 0);
 		const byte regVal = readReg(opIdx + 0x40);
 		writeReg(opIdx + 0x40, vol + (regVal & 0xC0));
@@ -387,9 +438,11 @@ void Music::noteOff(byte channel, byte note) {
 		for (uint8 v = 0; v < _numOplChannels; v++) {
 			if (_voiceAge[v] == 0 && _voiceMidiChannel[v] == channel && _voiceNote[v] == note) {
 				// Write frequency without key-on
-				const uint16 freq = ((uint16)_freqTableHi[note] << 8) | _freqTableLo[note];
-				writeReg(v + 0xA0, freq & 0xFF);
-				writeReg(v + 0xB0, (freq >> 8) & 0xDF); // clear key-on bit
+				if (note < ARRAYSIZE(kFreqTableLo)) {
+					const uint16 freq = ((uint16)kFreqTableHi[note] << 8) | kFreqTableLo[note];
+					writeReg(v + 0xA0, freq & 0xFF);
+					writeReg(v + 0xB0, (freq >> 8) & 0xDF); // clear key-on bit
+				}
 				_voiceAge[v] = 1;
 				break;
 			}
@@ -444,8 +497,8 @@ void Music::loadInstrument(uint8 voice, uint8 program) {
 		return;
 
 	const byte *inst = _songData.data() + _instrumentDataOffset + instBase;
-	const uint8 op1 = _opMap1[voice];
-	const uint8 op2 = _opMap2[voice];
+	const uint8 op1 = kOpMap1[voice];
+	const uint8 op2 = kOpMap2[voice];
 
 	writeReg(op1 + 0x20, inst[0]);
 	writeReg(op2 + 0x20, inst[1]);
@@ -461,19 +514,19 @@ void Music::loadInstrument(uint8 voice, uint8 program) {
 }
 
 void Music::setFrequency(uint8 voice, uint8 note, uint8 pitchBend) {
-	if (note == 0xFF || note >= _freqTableLo.size())
+	if (note == 0xFF || note >= ARRAYSIZE(kFreqTableLo))
 		return;
 
-	uint16 freq = ((uint16)_freqTableHi[note] << 8) | _freqTableLo[note];
+	uint16 freq = ((uint16)kFreqTableHi[note] << 8) | kFreqTableLo[note];
 
 	if (pitchBend != 0) {
 		if (pitchBend < 0x80) {
 			const uint8 nextNote = (note < 0x7F) ? note + 1 : 0x7F;
-			const uint16 nextFreq = ((uint16)_freqTableHi[nextNote] << 8) | _freqTableLo[nextNote];
+			const uint16 nextFreq = ((uint16)kFreqTableHi[nextNote] << 8) | kFreqTableLo[nextNote];
 			freq += (uint16)((uint64)pitchBend * (nextFreq - freq) >> 7);
 		} else {
 			const uint8 prevNote = (note > 0) ? note - 1 : 0;
-			const uint16 prevFreq = ((uint16)_freqTableHi[prevNote] << 8) | _freqTableLo[prevNote];
+			const uint16 prevFreq = ((uint16)kFreqTableHi[prevNote] << 8) | kFreqTableLo[prevNote];
 			freq -= (uint16)((uint64)pitchBend * (freq - prevFreq) >> 7);
 		}
 	}
@@ -490,40 +543,12 @@ void Music::updateDebugState() {
 		_debug.voices[i].note = _voiceNote[i];
 		_debug.voices[i].channel = _voiceMidiChannel[i];
 		_debug.voices[i].active = (_voiceAge[i] == 0);
-		_debug.voices[i].volume = _regShadow[_opMap2.size() > (uint)i ? _opMap2[i] + 0x40 : 0] & 0x3F;
+		_debug.voices[i].volume = _regShadow[kOpMap2[i] + 0x40] & 0x3F;
 		_debug.regHistory[i][_debug.ringPos] = (_regShadow[i + 0xB0] & 0x20) ? 1.0f - (_debug.voices[i].volume / 63.0f) : 0.0f;
 	}
 	_debug.ringPos = (_debug.ringPos + 1) % kDebugRingSize;
 }
 
-void Music::readDataFromExecutable(Common::MemoryReadStream *fileStream) {
-	constexpr uint32 size = 255;
-
-	_opSlotTable.resize(size);
-	loadData(fileStream, 0x0001B669, size, _opSlotTable.data());
-
-	_opMap1.resize(size);
-	loadData(fileStream, 0x0001B68D, size, _opMap1.data());
-
-	_opMap2.resize(size);
-	loadData(fileStream, 0x0001B696, size, _opMap2.data());
-
-	_freqTableLo.resize(size);
-	loadData(fileStream, 0x0001B69F, size, _freqTableLo.data());
-
-	_freqTableHi.resize(size);
-	loadData(fileStream, 0x0001B71F, size, _freqTableHi.data());
-
-	_percVolTable = {28, 25, 23, 18, 14, 11, 8, 2, 50, 42, 37, 35, 34, 32, 30, 2, 55, 50, 49, 48, 45, 43, 40, 2, 60, 60, 58, 56, 54, 52, 50, 2};
-	_percOpMap = {19, 20, 18, 21, 17};
-	_percFreqChannel = {6, 7, 8, 8, 7};
-}
-
-void Music::loadData(Common::MemoryReadStream *fileStream, int64 pos, uint16 size, void *target) {
-	fileStream->seek(pos, SEEK_SET);
-	fileStream->read(target, size);
-}
-
 SmfMidiPlayer::SmfMidiPlayer() {
 	createDriver();
 	if (_driver == nullptr)
diff --git a/engines/macs2/music.h b/engines/macs2/music.h
index 5bf25ed2853..04d195ff725 100644
--- a/engines/macs2/music.h
+++ b/engines/macs2/music.h
@@ -30,10 +30,6 @@
 
 class MidiParser;
 
-namespace Common {
-class MemoryReadStream;
-}
-
 namespace OPL {
 class OPL;
 }
@@ -94,8 +90,6 @@ public:
 	bool isMidiFilePlaying() const;
 	bool hasAdlibBackend() const { return _opl != nullptr; }
 
-	void readDataFromExecutable(Common::MemoryReadStream *fileStream);
-
 	// MidiDriver_BASE interface
 	void send(uint32 b) override;
 	void metaEvent(byte type, const byte *data, uint16 length) override;
@@ -170,18 +164,6 @@ private:
 	void stopAdlibPlayback();
 	void stopSmfPlayback();
 	bool ensureSmfPlayer();
-
-	// Lookup tables from EXE
-	Common::Array<uint8> _opSlotTable;
-	Common::Array<uint8> _opMap1;
-	Common::Array<uint8> _opMap2;
-	Common::Array<uint8> _freqTableLo;
-	Common::Array<uint8> _freqTableHi;
-	Common::Array<uint8> _percVolTable;
-	Common::Array<uint8> _percOpMap;
-	Common::Array<uint8> _percFreqChannel;
-
-	void loadData(Common::MemoryReadStream *stream, int64 pos, uint16 size, void *target);
 };
 
 } // End of namespace Macs2




More information about the Scummvm-git-logs mailing list