[Scummvm-git-logs] scummvm master -> 1ca640151e36825c99c4813b5558f5d9c1ac6269

npjg noreply at scummvm.org
Wed Feb 25 02:12:08 UTC 2026


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

Summary:
11a48f04bf MEDIASTATION: Fix incorrect comparison when triggering timer events
f60361fd33 MEDIASTATION: Improve debug printing
f14ca14830 MEDIASTATION: Make sprite clips work with newer engine versions
f6b3fd2ab8 MEDIASTATION: Add helper function for getting an actor of a specific type
11b97406c5 MEDIASTATION: Issue warnings, not asserts, when script argument conditions are not met
90cbd70cd3 MEDIASTATION: Rename BitmapHeader to ImageInfo
a86401a695 MEDIASTATION: Add initial support for stream movie proxies
98528cadc1 MEDIASTATION: Add support for camera images
b9e38dd7e7 MEDIASTATION: Add Cursor actor
8cd32db3f4 MEDIASTATION: Make Path actor more accurate
987b07ddde MEDIASTATION: Implement more builtin script functions/methods
62184a96dd MEDIASTATION: Fix numerous inconsistencies with image data reading
64fb8b2037 MEDIASTATION: Add initial support for Canvas and Text actors
34af4483c4 MEDIASTATION: Enforce consistent switch breaks, rather than returns
9d426237f6 MEDIASTATION: Enforce correct copy semantics for script values/variables
2505aa1acb MEDIASTATION: Properly pass script return values back
1ca640151e MEDIASTATION: Make caching-related parameters consistently read


Commit: 11a48f04bfd8289d77ea701815e581871cd04020
    https://github.com/scummvm/scummvm/commit/11a48f04bfd8289d77ea701815e581871cd04020
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-02-24T20:25:58-05:00

Commit Message:
MEDIASTATION: Fix incorrect comparison when triggering timer events

Changed paths:
    engines/mediastation/actor.cpp


diff --git a/engines/mediastation/actor.cpp b/engines/mediastation/actor.cpp
index d97523ad135..2dd457a4b07 100644
--- a/engines/mediastation/actor.cpp
+++ b/engines/mediastation/actor.cpp
@@ -96,9 +96,10 @@ void Actor::processTimeEventHandlers() {
 		double timeEventInFractionalSeconds = timeEvent->_argumentValue.asFloat();
 		uint timeEventInMilliseconds = timeEventInFractionalSeconds * 1000;
 		bool timeEventAlreadyProcessed = timeEventInMilliseconds < _lastProcessedTime;
-		bool timeEventNeedsToBeProcessed = timeEventInMilliseconds <= currentTime - _startTime;
+		bool timeEventNeedsToBeProcessed = timeEventInMilliseconds < currentTime - _startTime;
 		if (!timeEventAlreadyProcessed && timeEventNeedsToBeProcessed) {
-			debugC(5, kDebugScript, "Actor::processTimeEventHandlers(): Running On Time handler for time %d ms", timeEventInMilliseconds);
+			debugC(5, kDebugScript, "%s: Running On Time handler for time %d ms (lastProcessedTime: %d, currentTime: %d)",
+				__func__, timeEventInMilliseconds, _lastProcessedTime, currentTime);
 			timeEvent->execute(_id);
 		}
 	}


Commit: f60361fd3323283856cda0baddebefbbfa826d66
    https://github.com/scummvm/scummvm/commit/f60361fd3323283856cda0baddebefbbfa826d66
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-02-24T20:49:55-05:00

Commit Message:
MEDIASTATION: Improve debug printing

Get entity names from PROFILE._ST when available.
This is only included in some titles, but when it is included it is immensely helpful.

Debug-print script variable values.

Improve debug output on mismatched script value comparison.

Adjust some debug levels and make debug formatting more consistent in places.

Changed paths:
  A engines/mediastation/profile.cpp
  A engines/mediastation/profile.h
    engines/mediastation/actor.cpp
    engines/mediastation/actor.h
    engines/mediastation/actors/camera.cpp
    engines/mediastation/actors/document.cpp
    engines/mediastation/actors/hotspot.cpp
    engines/mediastation/actors/movie.cpp
    engines/mediastation/actors/sound.cpp
    engines/mediastation/actors/sprite.cpp
    engines/mediastation/actors/stage.cpp
    engines/mediastation/actors/timer.cpp
    engines/mediastation/audio.cpp
    engines/mediastation/context.cpp
    engines/mediastation/datafile.cpp
    engines/mediastation/graphics.cpp
    engines/mediastation/mediascript/codechunk.cpp
    engines/mediastation/mediascript/collection.cpp
    engines/mediastation/mediascript/eventhandler.cpp
    engines/mediastation/mediascript/function.cpp
    engines/mediastation/mediascript/scriptconstants.cpp
    engines/mediastation/mediascript/scriptvalue.cpp
    engines/mediastation/mediascript/scriptvalue.h
    engines/mediastation/mediastation.cpp
    engines/mediastation/mediastation.h
    engines/mediastation/module.mk


diff --git a/engines/mediastation/actor.cpp b/engines/mediastation/actor.cpp
index 2dd457a4b07..99ccb72e361 100644
--- a/engines/mediastation/actor.cpp
+++ b/engines/mediastation/actor.cpp
@@ -30,6 +30,76 @@
 
 namespace MediaStation {
 
+const char *actorTypeToStr(ActorType type) {
+	switch (type) {
+	case kActorTypeEmpty:
+		return "Empty";
+	case kActorTypeScreen:
+		return "Screen";
+	case kActorTypeStage:
+		return "Stage";
+	case kActorTypePath:
+		return "Path";
+	case kActorTypeSound:
+		return "Sound";
+	case kActorTypeTimer:
+		return "Timer";
+	case kActorTypeImage:
+		return "Image";
+	case kActorTypeHotspot:
+		return "Hotspot";
+	case kActorTypeCursor:
+		return "Cursor";
+	case kActorTypeSprite:
+		return "Sprite";
+	case kActorTypeLKZazu:
+		return "LKZazu";
+	case kActorTypeLKConstellations:
+		return "LKConstellations";
+	case kActorTypeDocument:
+		return "Document";
+	case kActorTypeImageSet:
+		return "ImageSet";
+	case kActorTypeMovie:
+		return "Movie";
+	case kActorTypePalette:
+		return "Palette";
+	case kActorTypePrinter:
+		return "Printer";
+	case kActorTypeText:
+		return "Text";
+	case kActorTypeFont:
+		return "Font";
+	case kActorTypeCamera:
+		return "Camera";
+	case kActorTypeCanvas:
+		return "Canvas";
+	case kActorTypeXsnd:
+		return "Xsnd";
+	case kActorTypeXsndMidi:
+		return "XsndMidi";
+	case kActorTypeRecorder:
+		return "Recorder";
+	case kActorTypeFunction:
+		return "Function";
+	default:
+		return "UNKNOWN";
+	}
+}
+
+void Actor::setId(uint id) {
+	_id = id;
+	updateDebugName();
+}
+
+void Actor::updateDebugName() {
+	_debugName = g_engine->formatActorName(this);
+}
+
+const char *Actor::debugName() const {
+	return _debugName.c_str();
+}
+
 Actor::~Actor() {
 	for (auto it = _eventHandlers.begin(); it != _eventHandlers.end(); ++it) {
 		Common::Array<EventHandler *> &handlersForType = it->_value;
@@ -62,8 +132,8 @@ void Actor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 		// This is not a hashmap because we don't want to have to hash ScriptValues.
 		for (EventHandler *existingEventHandler : eventHandlersForType) {
 			if (existingEventHandler->_argumentValue == eventHandler->_argumentValue) {
-				error("%s: Event handler for %s (%s) already exists", __func__,
-					  eventTypeToStr(eventHandler->_type), eventHandler->_argumentValue.getDebugString().c_str());
+				error("[%s] %s: Event handler for %s (%s) already exists", debugName(), __func__,
+					eventTypeToStr(eventHandler->_type), eventHandler->_argumentValue.getDebugString().c_str());
 			}
 		}
 		eventHandlersForType.push_back(eventHandler);
@@ -71,19 +141,20 @@ void Actor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 	}
 
 	default:
-		error("Got unimplemented actor parameter 0x%x", static_cast<uint>(paramType));
+		error("[%s] %s: Got unimplemented actor parameter 0x%x", debugName(), __func__, static_cast<uint>(paramType));
 	}
 }
 
 void Actor::loadIsComplete() {
 	if (_loadIsComplete) {
-		warning("%s: Called more than once for actor %d", __func__, _id);
+		warning("[%s] %s: Already loaded", debugName(), __func__);
 	}
 	_loadIsComplete = true;
 }
 
 ScriptValue Actor::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
-	warning("%s: Got unimplemented method call 0x%x (%s)", __func__, static_cast<uint>(methodId), builtInMethodToStr(methodId));
+	warning("[%s] %s: Got unimplemented method call 0x%x (%s)",
+		debugName(), __func__, static_cast<uint>(methodId), builtInMethodToStr(methodId));
 	return ScriptValue();
 }
 
@@ -112,18 +183,19 @@ void Actor::runEventHandlerIfExists(EventType eventType, const ScriptValue &arg)
 		const ScriptValue &argToCheck = eventHandler->_argumentValue;
 
 		if (arg.getType() != argToCheck.getType()) {
-			warning("Got event handler arg type %s, expected %s",
-					scriptValueTypeToStr(arg.getType()), scriptValueTypeToStr(argToCheck.getType()));
+			warning("[%s] %s: Got event handler arg type %s, expected %s", debugName(), __func__,
+				scriptValueTypeToStr(arg.getType()), scriptValueTypeToStr(argToCheck.getType()));
 			continue;
 		}
 
 		if (arg == argToCheck) {
-			debugC(5, kDebugScript, "Executing handler for event type %s on actor %d", eventTypeToStr(eventType), _id);
+			debugC(5, kDebugScript, "[%s] %s: Executing handler for event type %s", debugName(), __func__, eventTypeToStr(eventType));
 			eventHandler->execute(_id);
 			return;
 		}
 	}
-	debugC(5, kDebugScript, "No event handler for event type %s on actor %d", eventTypeToStr(eventType), _id);
+
+	debugC(5, kDebugScript, "[%s] %s: No event handler for event type %s", debugName(), __func__, eventTypeToStr(eventType));
 }
 
 void Actor::runEventHandlerIfExists(EventType eventType) {
@@ -335,6 +407,9 @@ void SpatialEntity::invalidateMouse() {
 
 void SpatialEntity::moveTo(int16 x, int16 y) {
 	Common::Point dest(x, y);
+	debugC(3, kDebugGraphics, "[%s] %s: (%d, %d) -> (%d, %d)", debugName(), __func__,
+		_originalBoundingBox.origin().x, _originalBoundingBox.origin().y, x, y);
+
 	if (dest == _boundingBox.origin()) {
 		// We aren't actually moving anywhere.
 		return;
@@ -356,6 +431,7 @@ void SpatialEntity::moveTo(int16 x, int16 y) {
 void SpatialEntity::moveToCentered(int16 x, int16 y) {
 	int16 targetX = x - (_boundingBox.width() / 2);
 	int16 targetY = y - (_boundingBox.height() / 2);
+	debugC(3, kDebugGraphics, "[%s] %s: (%d, %d)", debugName(), __func__, targetX, targetY);
 	moveTo(targetX, targetY);
 }
 
@@ -410,12 +486,12 @@ void SpatialEntity::invalidateLocalBounds() {
 		_parentStage->setAdjustedBounds(kWrapNone);
 		_parentStage->invalidateRect(getBbox());
 	} else {
-		error("%s: No parent stage for entity %d", __func__, _id);
+		warning("[%s] %s: No parent stage", debugName(), __func__);
 	}
 }
 
 void SpatialEntity::invalidateLocalZIndex() {
-	warning("STUB: %s", __func__);
+	warning("[%s] %s: STUB", debugName(), __func__);
 }
 
 void SpatialEntity::setAdjustedBounds(CylindricalWrapMode alignmentMode) {
@@ -483,12 +559,13 @@ void SpatialEntity::setAdjustedBounds(CylindricalWrapMode alignmentMode) {
 
 	if (alignmentMode != kWrapNone) {
 		// TODO: Implement this once we have a title that actually uses it.
-		warning("%s: Actor %d: Wrapping mode %d not handled yet: (%d, %d, %d, %d) -= (%d, %d)", __func__, _id, static_cast<uint>(alignmentMode), PRINT_RECT(_boundingBox), offset.x, offset.y);
+		warning("[%s] %s: Wrapping mode %d not handled yet: (%d, %d, %d, %d) -= (%d, %d)", debugName(),  __func__,
+			static_cast<uint>(alignmentMode), PRINT_RECT(_boundingBox), offset.x, offset.y);
 	}
 
 	if (_scaleX != 0.0 || _scaleY != 0.0) {
 		// TODO: Implement this once we have a title that actually uses it.
-		warning("%s: Scale not handled yet (scaleX: %f, scaleY: %f)", __func__, _scaleX, _scaleY);
+		warning("[%s] %s: Scale not handled yet (scaleX: %f, scaleY: %f)", debugName(), __func__, _scaleX, _scaleY);
 	}
 }
 
diff --git a/engines/mediastation/actor.h b/engines/mediastation/actor.h
index 9898ae076e7..78a63c6e79e 100644
--- a/engines/mediastation/actor.h
+++ b/engines/mediastation/actor.h
@@ -64,6 +64,7 @@ enum ActorType {
 	kActorTypeRecorder = 0x0021,
 	kActorTypeFunction = 0x0069 // FUN
 };
+const char *actorTypeToStr(ActorType type);
 
 enum ActorHeaderSectionType {
 	kActorHeaderEmptySection = 0x0000,
@@ -186,14 +187,17 @@ public:
 	ActorType type() const { return _type; }
 	uint id() const { return _id; }
 	uint contextId() const { return _contextId; }
-	void setId(uint id) { _id = id; }
+	void setId(uint id);
 	void setContextId(uint id) { _contextId = id; }
+	const char *debugName() const;
+	void updateDebugName();
 
 protected:
 	ActorType _type = kActorTypeEmpty;
 	bool _loadIsComplete = false;
 	uint _id = 0;
 	uint _contextId = 0;
+	Common::String _debugName;
 
 	uint _startTime = 0;
 	uint _lastProcessedTime = 0;
diff --git a/engines/mediastation/actors/camera.cpp b/engines/mediastation/actors/camera.cpp
index ab61816ec6b..8870604a16b 100644
--- a/engines/mediastation/actors/camera.cpp
+++ b/engines/mediastation/actors/camera.cpp
@@ -336,7 +336,7 @@ void CameraActor::drawUsingCamera(DisplayContext &displayContext, const Common::
 
 void CameraActor::drawObject(DisplayContext &sourceContext, DisplayContext &destContext, SpatialEntity *objectToDraw) {
 	if (_parentStage == nullptr) {
-		warning("%s: No parent stage", __func__);
+		warning("[%s] %s: No parent stage", debugName(), __func__);
 		return;
 	}
 
@@ -347,11 +347,11 @@ void CameraActor::drawObject(DisplayContext &sourceContext, DisplayContext &dest
 	}
 
 	if (_parentStage->cylindricalX()) {
-		warning("%s: CylindricalX not handled yet", __func__);
+		warning("[%s] %s: CylindricalX not handled yet", debugName(), __func__);
 	}
 
 	if (_parentStage->cylindricalY()) {
-		warning("%s: CylindricalY not handled yet", __func__);
+		warning("[%s] %s: CylindricalY not handled yet", debugName(), __func__);
 	}
 	objectToDraw->setAdjustedBounds(kWrapNone);
 }
@@ -359,7 +359,7 @@ void CameraActor::drawObject(DisplayContext &sourceContext, DisplayContext &dest
 void CameraActor::setXYDelta(uint xDelta, uint yDelta) {
 	_panDelta.x = xDelta;
 	_panDelta.y = yDelta;
-	debugC(6, kDebugCamera, "%s: (%d, %d)", __func__, _panDelta.x, _panDelta.y);
+	debugC(6, kDebugCamera, "[%s] %s: (%d, %d)", debugName(), __func__, _panDelta.x, _panDelta.y);
 }
 
 void CameraActor::setXYDelta() {
@@ -376,7 +376,7 @@ void CameraActor::setXYDelta() {
 	} else if (_panDest.y < _panStart.y) {
 		_panDelta.y = -1;
 	}
-	debugC(6, kDebugCamera, "%s: (%d, %d)", __func__, _panDelta.x, _panDelta.y);
+	debugC(6, kDebugCamera, "[%s] %s: (%d, %d)", debugName(), __func__, _panDelta.x, _panDelta.y);
 }
 
 void CameraActor::panToByTime(int16 x, int16 y, double duration) {
@@ -387,8 +387,8 @@ void CameraActor::panToByTime(int16 x, int16 y, double duration) {
 	_currentPanStep = 1;
 	_startTime = g_system->getMillis();
 	_nextPanStepTime = 0;
-	debugC(6, kDebugCamera, "%s: panStart: (%d, %d); panDest: (%d, %d); panDuration: %f",
-		__func__, _panStart.x, _panStart.y, _panDest.x, _panDest.y, _panDuration);
+	debugC(6, kDebugCamera, "[%s] %s: panStart: (%d, %d); panDest: (%d, %d); panDuration: %f",
+		debugName(), __func__, _panStart.x, _panStart.y, _panDest.x, _panDest.y, _panDuration);
 	setXYDelta();
 	calcNewViewportOrigin();
 }
@@ -402,8 +402,8 @@ void CameraActor::panToByStepCount(int16 x, int16 y, uint panSteps, double durat
 	_maxPanStep = panSteps;
 	_startTime = g_system->getMillis();
 	_nextPanStepTime = 0;
-	debugC(6, kDebugCamera, "%s: panStart: (%d, %d); panDest: (%d, %d); panDuration: %f; maxPanStep: %d",
-		__func__, _panStart.x, _panStart.y, _panDest.x, _panDest.y, _panDuration, _maxPanStep);
+	debugC(6, kDebugCamera, "[%s] %s: panStart: (%d, %d); panDest: (%d, %d); panDuration: %f; maxPanStep: %d",
+		debugName(), __func__, _panStart.x, _panStart.y, _panDest.x, _panDest.y, _panDuration, _maxPanStep);
 	setXYDelta();
 	calcNewViewportOrigin();
 }
@@ -416,7 +416,7 @@ void CameraActor::startPan(uint xOffset, uint yOffset, double duration) {
 	_currentPanStep = 0;
 	_maxPanStep = 0;
 	setXYDelta(xOffset, yOffset);
-	debugC(6, kDebugCamera, "%s: xOffset: %u, yOffset: %u, duration: %f", __func__, xOffset, yOffset, duration);
+	debugC(6, kDebugCamera, "[%s] %s: xOffset: %u, yOffset: %u, duration: %f", debugName(), __func__, xOffset, yOffset, duration);
 }
 
 void CameraActor::stopPan() {
@@ -426,8 +426,8 @@ void CameraActor::stopPan() {
 	_nextPanStepTime = 0;
 	_currentPanStep = 0;
 	_maxPanStep = 0;
-	debugC(6, kDebugCamera, "%s: nextViewportOrigin: (%d, %d); actualViewportOrigin: (%d, %d)",
-		__func__,  _nextViewportOrigin.x, _nextViewportOrigin.y, _currentViewportOrigin.x, _currentViewportOrigin.y);
+	debugC(6, kDebugCamera, "[%s] %s: nextViewportOrigin: (%d, %d); actualViewportOrigin: (%d, %d)",
+		debugName(), __func__,  _nextViewportOrigin.x, _nextViewportOrigin.y, _currentViewportOrigin.x, _currentViewportOrigin.y);
 }
 
 bool CameraActor::continuePan() {
@@ -441,7 +441,7 @@ bool CameraActor::continuePan() {
 			panShouldContinue = false;
 		}
 	}
-	debugC(6, kDebugCamera, "%s: %s", __func__, panShouldContinue ? "true": "false");
+	debugC(6, kDebugCamera, "[%s] %s: %s", debugName(), __func__, panShouldContinue ? "true": "false");
 	return panShouldContinue;
 }
 
@@ -536,24 +536,24 @@ void CameraActor::processNextPanStep() {
 
 void CameraActor::adjustCameraViewport(Common::Point &viewportToAdjust) {
 	if (_parentStage == nullptr) {
-		warning("%s: No parent stage", __func__);
+		warning("[%s] %s: No parent stage", debugName(), __func__);
 		return;
 	}
 
 	if (_parentStage->cylindricalX()) {
-		warning("%s: CylindricalX not handled yet", __func__);
+		warning("[%s] %s: CylindricalX not handled yet", debugName(), __func__);
 	}
 
 	if (_parentStage->cylindricalY()) {
-		warning("%s: CylindricalY not handled yet", __func__);
+		warning("[%s] %s: CylindricalY not handled yet", debugName(), __func__);
 	}
 }
 
 void CameraActor::calcNewViewportOrigin() {
 	if (_panState == kCameraPanningStarted) {
 		_nextViewportOrigin = _currentViewportOrigin + _panDelta;
-		debugC(6, kDebugCamera, "%s: (%d, %d) [panDelta: (%d, %d)]",
-			__func__, _nextViewportOrigin.x, _nextViewportOrigin.y, _panDelta.x, _panDelta.y);
+		debugC(6, kDebugCamera, "[%s] %s: (%d, %d) [panDelta: (%d, %d)]",
+			debugName(), __func__, _nextViewportOrigin.x, _nextViewportOrigin.y, _panDelta.x, _panDelta.y);
 	} else {
 		// Interpolate from the start to the dest based on percent complete.
 		double progress = percentComplete();
@@ -566,8 +566,8 @@ void CameraActor::calcNewViewportOrigin() {
 		double endY = static_cast<double>(_panDest.y);
 		double interpolatedY = startY + (endY - startY) * progress + 0.5;
 		_nextViewportOrigin.y = static_cast<int16>(interpolatedY);
-		debugC(6, kDebugCamera, "%s: (%d, %d) [panStart: (%d, %d); panDest: (%d, %d); percentComplete: %f]",
-			__func__, _nextViewportOrigin.x, _nextViewportOrigin.y, _panStart.x, _panStart.y, _panDest.x, _panDest.y, progress);
+		debugC(6, kDebugCamera, "[%s] %s: (%d, %d) [panStart: (%d, %d); panDest: (%d, %d); percentComplete: %f]",
+			debugName(), __func__, _nextViewportOrigin.x, _nextViewportOrigin.y, _panStart.x, _panStart.y, _panDest.x, _panDest.y, progress);
 	}
 }
 
@@ -588,7 +588,7 @@ bool CameraActor::cameraWithinStage(const Common::Point &candidate) {
 		} else if (candidate.x < 0) {
 			result = false;
 		}
-		debugC(6, kDebugCamera, "%s: %s [rightBoundary: %d, extent: %d]", __func__, result ? "true" : "false", candidateRightBoundary, _parentStage->extent().x);
+		debugC(6, kDebugCamera, "[%s] %s: %s [rightBoundary: %d, extent: %d]", debugName(), __func__, result ? "true" : "false", candidateRightBoundary, _parentStage->extent().x);
 	}
 
 	// We can only be out of vertical bounds if we have a requested delta and
@@ -602,7 +602,7 @@ bool CameraActor::cameraWithinStage(const Common::Point &candidate) {
 		} else if (candidate.y < 0) {
 			result = false;
 		}
-		debugC(6, kDebugCamera, "%s: %s [bottomBoundary: %d, extent: %d]", __func__, result ? "true" : "false", candidateBottomBoundary, _parentStage->extent().y);
+		debugC(6, kDebugCamera, "[%s] %s: %s [bottomBoundary: %d, extent: %d]", debugName(), __func__, result ? "true" : "false", candidateBottomBoundary, _parentStage->extent().y);
 	}
 	return result;
 }
diff --git a/engines/mediastation/actors/document.cpp b/engines/mediastation/actors/document.cpp
index 9b930d28cd6..ec58e48934f 100644
--- a/engines/mediastation/actors/document.cpp
+++ b/engines/mediastation/actors/document.cpp
@@ -49,7 +49,7 @@ ScriptValue DocumentActor::callMethod(BuiltInMethod methodId, Common::Array<Scri
 	case kDocumentSetMultipleSoundsMethod: {
 		assert(args.size() == 1);
 		bool value = args[0].asBool();
-		warning("%s: STUB: %s: %d", __func__, builtInMethodToStr(methodId), value);
+		warning("[%s] %s: STUB: %s: %d", debugName(), __func__, builtInMethodToStr(methodId), value);
 		break;
 	}
 
@@ -91,7 +91,7 @@ void DocumentActor::processBranch(Common::Array<ScriptValue> &args) {
 	if (args.size() > 1) {
 		bool disableUpdates = static_cast<bool>(args[1].asParamToken());
 		if (disableUpdates)
-			warning("%s: disableUpdates parameter not handled yet", __func__);
+			warning("[%s] %s: disableUpdates parameter not handled yet", debugName(), __func__);
 	}
 
 	g_engine->getDocument()->scheduleScreenBranch(contextId);
diff --git a/engines/mediastation/actors/hotspot.cpp b/engines/mediastation/actors/hotspot.cpp
index e2d960537c3..808f186ef1c 100644
--- a/engines/mediastation/actors/hotspot.cpp
+++ b/engines/mediastation/actors/hotspot.cpp
@@ -165,7 +165,7 @@ uint16 HotspotActor::findActorToAcceptMouseEvents(
 			result |= kMouseUpFlag;
 		}
 	} else {
-		debugC(5, kDebugEvents, "%s: %d: Inactive", __func__, id());
+		debugC(6, kDebugEvents, "[%s] %s: Inactive", debugName(),  __func__);
 	}
 
 	return result;
@@ -194,7 +194,7 @@ void HotspotActor::deactivate() {
 
 void HotspotActor::mouseDownEvent(const Common::Event &event) {
 	if (!_isActive) {
-		warning("%s: Called on inactive hotspot", __func__);
+		warning("[%s] %s: Inactive", debugName(), __func__);
 		return;
 	}
 
@@ -204,7 +204,7 @@ void HotspotActor::mouseDownEvent(const Common::Event &event) {
 
 void HotspotActor::mouseUpEvent(const Common::Event &event) {
 	if (!_isActive) {
-		warning("%s: Called on inactive hotspot", __func__);
+		warning("[%s] %s: Inactive", debugName(), __func__);
 		return;
 	}
 
@@ -214,16 +214,16 @@ void HotspotActor::mouseUpEvent(const Common::Event &event) {
 
 void HotspotActor::mouseEnteredEvent(const Common::Event &event) {
 	if (!_isActive) {
-		warning("%s: Called on inactive hotspot", __func__);
+		warning("[%s] %s: Inactive", debugName(), __func__);
 		return;
 	}
 
 	g_engine->setMouseInsideHotspot(this);
 	if (_cursorResourceId != 0) {
-		debugC(5, kDebugEvents, "%s: Setting cursor %d for asset %d", __func__, _cursorResourceId, id());
+		debugC(5, kDebugEvents, "[%s] %s: Setting cursor %d", debugName(), __func__, _cursorResourceId);
 		g_engine->getCursorManager()->setAsTemporary(_cursorResourceId);
 	} else {
-		debugC(5, kDebugEvents, "%s: Unsetting cursor for asset %d", __func__, id());
+		debugC(5, kDebugEvents, "[%s] %s: Unsetting cursor", debugName(), __func__);
 		g_engine->getCursorManager()->unsetTemporary();
 	}
 
@@ -232,7 +232,7 @@ void HotspotActor::mouseEnteredEvent(const Common::Event &event) {
 
 void HotspotActor::mouseMovedEvent(const Common::Event &event) {
 	if (!_isActive) {
-		warning("%s: Called on inactive hotspot", __func__);
+		warning("[%s] %s: Inactive", debugName(), __func__);
 		return;
 	}
 
@@ -241,7 +241,7 @@ void HotspotActor::mouseMovedEvent(const Common::Event &event) {
 
 void HotspotActor::mouseExitedEvent(const Common::Event &event) {
 	if (!_isActive) {
-		warning("%s: Called on inactive hotspot", __func__);
+		warning("[%s] %s: Inactive", debugName(), __func__);
 		return;
 	}
 
diff --git a/engines/mediastation/actors/movie.cpp b/engines/mediastation/actors/movie.cpp
index b29b3918cdd..3514702a552 100644
--- a/engines/mediastation/actors/movie.cpp
+++ b/engines/mediastation/actors/movie.cpp
@@ -262,7 +262,7 @@ void StreamMovieActor::updateFrameState() {
 		uint currentTime = g_system->getMillis();
 		movieTime = currentTime - _startTime;
 	}
-	debugC(5, kDebugGraphics, "StreamMovieActor::updateFrameState (%d): Starting update (movie time: %d)", _id, movieTime);
+	debugC(7, kDebugGraphics, "[%s] %s: Starting update (movie time: %d)", debugName(), __func__, movieTime);
 
 	// This complexity is necessary becuase movies can have more than one frame
 	// showing at the same time - for instance, a movie background and an
@@ -314,14 +314,16 @@ void StreamMovieActor::updateFrameState() {
 
 	// Show the frames that are currently active, for debugging purposes.
 	for (MovieFrame *frame : _framesOnScreen) {
-		debugC(5, kDebugGraphics, "   (time: %d ms) Frame %d (%d x %d) @ (%d, %d); start: %d ms, end: %d ms, zIndex = %d", \
-			movieTime, frame->index, frame->image->width(), frame->image->height(), frame->leftTop.x, frame->leftTop.y, frame->startInMilliseconds, frame->endInMilliseconds, frame->zIndex);
+		debugC(8, kDebugGraphics, "[%s] %s: (time: %d ms) Frame %d (%d x %d) @ (%d, %d); start: %d ms, end: %d ms, zIndex = %d",
+			debugName(), __func__, movieTime, frame->index, frame->image->width(), frame->image->height(), frame->leftTop.x, frame->leftTop.y, frame->startInMilliseconds, frame->endInMilliseconds, frame->zIndex);
 	}
 }
 
 void StreamMovieActor::draw(DisplayContext &displayContext) {
 	for (MovieFrame *frame : _framesOnScreen) {
 		Common::Rect bbox = getFrameBoundingBox(frame);
+		debugC(8, kDebugGraphics, "%s: %s: frame %d (%d, %d, %d, %d)",
+			__func__, debugName(), frame->index, PRINT_RECT(bbox));
 
 		switch (frame->blitType) {
 		case kUncompressedMovieBlit:
@@ -427,12 +429,11 @@ void StreamMovieActor::readChunk(Chunk &chunk) {
 void StreamMovieActor::parseMovieHeader(Chunk &chunk) {
 	_chunkCount = chunk.readTypedUint16();
 	_frameRate = chunk.readTypedDouble();
-	debugC(5, kDebugLoading, "%s: chunkCount = 0x%x, frameRate = %f (@0x%llx)", __func__, _chunkCount, _frameRate, static_cast<long long int>(chunk.pos()));
+	debugC(5, kDebugLoading, "[%s] %s: chunkCount: 0x%x, frameRate: %f", debugName(), __func__, _chunkCount, _frameRate);
 
 	Common::Array<uint> chunkLengths;
 	for (uint i = 0; i < _chunkCount; i++) {
 		uint chunkLength = chunk.readTypedUint32();
-		debugC(5, kDebugLoading, "StreamMovieActor::readSubfile(): chunkLength = 0x%x (@0x%llx)", chunkLength, static_cast<long long int>(chunk.pos()));
 		chunkLengths.push_back(chunkLength);
 	}
 }
diff --git a/engines/mediastation/actors/sound.cpp b/engines/mediastation/actors/sound.cpp
index ce7e2a426e5..5b2505c967e 100644
--- a/engines/mediastation/actors/sound.cpp
+++ b/engines/mediastation/actors/sound.cpp
@@ -41,7 +41,7 @@ void SoundActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 		// as the ID we have already read.
 		uint32 duplicateActorId = chunk.readTypedUint16();
 		if (duplicateActorId != _id) {
-			warning("Duplicate actor ID %d does not match original ID %d", duplicateActorId, _id);
+			warning("[%s] %s: Duplicate actor ID %s does not match", debugName(), __func__, g_engine->formatActorName(duplicateActorId).c_str());
 		}
 		break;
 	}
diff --git a/engines/mediastation/actors/sprite.cpp b/engines/mediastation/actors/sprite.cpp
index 19dfa5f02cd..641ea76a423 100644
--- a/engines/mediastation/actors/sprite.cpp
+++ b/engines/mediastation/actors/sprite.cpp
@@ -276,7 +276,7 @@ void SpriteMovieActor::setCurrentClip(uint clipId) {
 			_activeClip = _clips.getVal(clipId);
 		} else {
 			_activeClip.id = clipId;
-			warning("%s: Sprite clip %d not found in sprite %d", __func__, clipId, _id);
+			warning("%s: Sprite clip %d not found in sprite %s", __func__, clipId, debugName());
 		}
 	}
 
@@ -304,7 +304,6 @@ void SpriteMovieActor::process() {
 
 void SpriteMovieActor::readChunk(Chunk &chunk) {
 	// Reads one frame from the sprite.
-	debugC(5, kDebugLoading, "Sprite::readFrame(): Reading sprite frame (@0x%llx)", static_cast<long long int>(chunk.pos()));
 	SpriteFrameHeader *header = new SpriteFrameHeader(chunk);
 	SpriteFrame *frame = new SpriteFrame(chunk, header);
 	_asset->frames.push_back(frame);
diff --git a/engines/mediastation/actors/stage.cpp b/engines/mediastation/actors/stage.cpp
index cf8687ca357..bb3df7c7fee 100644
--- a/engines/mediastation/actors/stage.cpp
+++ b/engines/mediastation/actors/stage.cpp
@@ -162,7 +162,8 @@ void StageActor::drawUsingStage(DisplayContext &displayContext) {
 		entity->setAdjustedBounds(kWrapNone);
 		if (entity->isVisible()) {
 			if (displayContext.rectIsInClip(entity->getBbox())) {
-				debugC(5, kDebugGraphics, "%s: Redrawing actor %d", __func__, entity->id());
+				debugC(8, kDebugGraphics, "[%s] %s: Redrawing actor %s (%d, %d, %d, %d)", debugName(), __func__,
+					entity->debugName(), PRINT_RECT(entity->getBbox()));
 				entity->draw(displayContext);
 			}
 		}
@@ -181,7 +182,7 @@ void StageActor::invalidateRect(const Common::Rect &rect) {
 			invalidateUsingCameras(rectRelativeToParent);
 		}
 	} else {
-		error("%s: Attempt to invalidate rect without a parent stage", __func__);
+		warning("[%s] %s: Attempt to invalidate rect without a parent stage", debugName(), __func__);
 	}
 }
 
@@ -365,7 +366,7 @@ ScriptValue StageActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptV
 
 void StageActor::addChildSpatialEntity(SpatialEntity *entity) {
 	if (!assertHasNoParent(entity)) {
-		error("%s: Attempt to add entity that already has a parent", __func__);
+		error("[%s] %s: Attempt to add entity that already has a parent", debugName(), __func__);
 	}
 
 	entity->setParentStage(this);
@@ -378,7 +379,7 @@ void StageActor::addChildSpatialEntity(SpatialEntity *entity) {
 
 void StageActor::removeChildSpatialEntity(SpatialEntity *entity) {
 	if (!assertIsMyChild(entity)) {
-		error("%s: Attempt to remove entity that is not a child", __func__);
+		warning("[%s] %s: Attempt to remove entity that is not a child", debugName(), __func__);
 	}
 
 	if (isVisible()) {
@@ -401,14 +402,17 @@ uint16 StageActor::queryChildrenAboutMouseEvents(
 	CylindricalWrapMode wrapMode) {
 
 	uint16 result = 0;
-	Common::Point adjustedPoint = point - _boundingBox.origin();
+	Common::Point mousePosRelativeToStageOrigin = point - _boundingBox.origin();
 	for (auto childIterator = _children.end(); childIterator != _children.begin();) {
 		--childIterator; // Decrement first, then dereference
 		SpatialEntity *child = *childIterator;
-		debugC(7, kDebugEvents, " %s: Checking actor %d (z-index: %d) (eventMask: 0x%02x) (result: 0x%02x) (wrapMode: %d)", __func__, child->id(), child->zIndex(), eventMask, result, wrapMode);
+		debugC(6, kDebugEvents, "  [%s] %s: Checking %s (mousePos: %d, %d -> %d, %d) (bounds: %d, %d, %d, %d) (z-index: %d) (eventMask: 0x%02x) (result: 0x%02x) (wrapMode: %d)",
+			debugName(), __func__, child->debugName(),
+			point.x, point.y, mousePosRelativeToStageOrigin.x, mousePosRelativeToStageOrigin.y,
+			PRINT_RECT(child->getBbox()), child->zIndex(), eventMask, result, wrapMode);
 
 		child->setAdjustedBounds(wrapMode);
-		uint16 handledEvents = child->findActorToAcceptMouseEvents(adjustedPoint, eventMask, state, true);
+		uint16 handledEvents = child->findActorToAcceptMouseEvents(mousePosRelativeToStageOrigin, eventMask, state, true);
 		child->setAdjustedBounds(kWrapNone);
 
 		eventMask &= ~handledEvents;
@@ -515,11 +519,11 @@ uint16 StageActor::findActorToAcceptMouseEvents(
 	MouseActorState &state,
 	bool inBounds) {
 
-	debugC(6, kDebugEvents, " --- %s ---", __func__);
-
-	Common::Point mousePosAdjustedByStageOrigin = point;
-	mousePosAdjustedByStageOrigin.x -= _boundingBox.left;
-	mousePosAdjustedByStageOrigin.y -= _boundingBox.top;
+	Common::Point mousePosRelativeToStageOrigin = point;
+	mousePosRelativeToStageOrigin.x -= _boundingBox.left;
+	mousePosRelativeToStageOrigin.y -= _boundingBox.top;
+	debugC(4, kDebugEvents, "[%s] %s: mousePos: (%d, %d), relativeToStage: (%d, %d)", debugName(), __func__,
+		point.x, point.y, mousePosRelativeToStageOrigin.x, mousePosRelativeToStageOrigin.y);
 
 	uint16 result;
 	if (_cameras.empty()) {
@@ -528,12 +532,11 @@ uint16 StageActor::findActorToAcceptMouseEvents(
 				inBounds = true;
 			}
 		}
-		result = queryChildrenAboutMouseEvents(mousePosAdjustedByStageOrigin, eventMask, state, kWrapNone);
+		result = queryChildrenAboutMouseEvents(point, eventMask, state, kWrapNone);
 	} else {
-		result = findActorToAcceptMouseEventsCamera(mousePosAdjustedByStageOrigin, eventMask, state, inBounds);
+		result = findActorToAcceptMouseEventsCamera(mousePosRelativeToStageOrigin, eventMask, state, inBounds);
 	}
 
-	debugC(6, kDebugEvents, " --- END %s ---", __func__);
 	return result;
 }
 
@@ -601,7 +604,7 @@ void StageActor::invalidateLocalZIndex() {
 
 void StageActor::invalidateZIndexOf(const SpatialEntity *entity) {
 	if (!assertIsMyChild(entity)) {
-		error("%s: Attempt to invalidate local z-index of non-child", __func__);
+		error("[%s] %s: Attempt to invalidate local z-index of non-child", debugName(), __func__);
 	}
 
 	// Remove the entity from the sorted array and re-insert it at the correct position.
@@ -707,7 +710,7 @@ void RootStage::deleteChildrenFromContextId(uint contextId) {
 void RootStage::setMousePosition(int16 x, int16 y) {
 	x += _boundingBox.left;
 	y += _boundingBox.top;
-	warning("%s: STUB: (%d, %d)", __func__, x, y);
+	warning("[%s] %s: STUB: (%d, %d)", debugName(), __func__, x, y);
 }
 
 StageDirector::StageDirector() {
@@ -739,7 +742,7 @@ void StageDirector::handleKeyboardEvent(const Common::Event &event) {
 	MouseActorState state;
 	uint16 flags = _rootStage->findActorToAcceptKeyboardEvents(event.kbd.ascii, kKeyDownFlag, state);
 	if (flags & kKeyDownFlag) {
-		debugC(5, kDebugEvents, "%s: Dispatching to actor %d", __func__, state.keyDown->id());
+		debugC(5, kDebugEvents, "%s: Dispatching to %s from root stage", __func__, state.keyDown->debugName());
 		state.keyDown->keyboardEvent(event);
 	}
 }
@@ -748,7 +751,8 @@ void StageDirector::handleMouseDownEvent(const Common::Event &event) {
 	MouseActorState state;
 	uint16 flags = _rootStage->findActorToAcceptMouseEvents(event.mouse, kMouseDownFlag, state, false);
 	if (flags & kMouseDownFlag) {
-		debugC(5, kDebugEvents, "%s: Dispatching to actor %d", __func__, state.mouseDown->id());
+		debugC(5, kDebugEvents, "%s: Dispatching to %s from root stage (mousePos: %d, %d) (bounds: %d, %d, %d, %d)",
+			__func__, state.mouseDown->debugName(), event.mouse.x, event.mouse.y, PRINT_RECT(state.mouseDown->getBbox()));
 		state.mouseDown->mouseDownEvent(event);
 	}
 }
@@ -757,7 +761,8 @@ void StageDirector::handleMouseUpEvent(const Common::Event &event) {
 	MouseActorState state;
 	uint16 flags = _rootStage->findActorToAcceptMouseEvents(event.mouse, kMouseUpFlag, state, false);
 	if (flags & kMouseUpFlag) {
-		debugC(5, kDebugEvents, "%s: Dispatching to actor %d", __func__, state.mouseUp->id());
+		debugC(5, kDebugEvents, "%s: Dispatching to %s from root stage (mousePos: %d, %d) (bounds: %d, %d, %d, %d)",
+			__func__, state.mouseUp->debugName(), event.mouse.x, event.mouse.y, PRINT_RECT(state.mouseUp->getBbox()));
 		state.mouseUp->mouseUpEvent(event);
 	}
 }
@@ -768,11 +773,11 @@ void StageDirector::handleMouseMovedEvent(const Common::Event &event) {
 		event.mouse,
 		kMouseEnterFlag | kMouseExitFlag | kMouseMovedFlag,
 		state, false);
-	debugC(5, kDebugEvents, "%s: Calling sendMouseEnterExitEvent", __func__);
 
 	sendMouseEnterExitEvent(flags, state, event);
 	if (flags & kMouseMovedFlag) {
-		debugC(5, kDebugEvents, "%s: Dispatching mouse moved to actor %d", __func__, state.mouseMoved->id());
+		debugC(5, kDebugEvents, "%s: Dispatching to %s (mousePos: %d, %d) (bounds: %d, %d, %d, %d)",
+			__func__, state.mouseMoved->debugName(), event.mouse.x, event.mouse.y, PRINT_RECT(state.mouseMoved->getBbox()));
 		state.mouseMoved->mouseMovedEvent(event);
 	}
 }
@@ -788,12 +793,12 @@ void StageDirector::handleMouseOutOfFocusEvent(const Common::Event &event) {
 	uint16 flags = _rootStage->findActorToAcceptMouseEvents(event.mouse, kMouseExitFlag | kMouseOutOfFocusFlag, state, false);
 
 	if (flags & kMouseExitFlag) {
-		debugC(5, kDebugEvents, "%s: Dispatching mouse enter to actor %d", __func__, state.mouseExit->id());
+		debugC(5, kDebugEvents, "%s: Dispatching mouse enter to %s", __func__, state.mouseExit->debugName());
 		state.mouseExit->mouseExitedEvent(event);
 	}
 
 	if (flags & kMouseOutOfFocusFlag) {
-		debugC(5, kDebugEvents, "%s: Dispatching mouse out of focus to actor %d", __func__, state.mouseOutOfFocus->id());
+		debugC(5, kDebugEvents, "%s: Dispatching mouse out of focus to %s", __func__, state.mouseOutOfFocus->debugName());
 		state.mouseOutOfFocus->mouseOutOfFocusEvent(event);
 	}
 }
@@ -801,16 +806,16 @@ void StageDirector::handleMouseOutOfFocusEvent(const Common::Event &event) {
 void StageDirector::sendMouseEnterExitEvent(uint16 flags, MouseActorState &state, const Common::Event &event) {
 	if (state.mouseMoved != state.mouseEnter || state.mouseMoved != state.mouseExit) {
 		if (flags & kMouseEnterFlag) {
-			debugC(5, kDebugEvents, "%s: Dispatching mouse enter to actor %d", __func__, state.mouseEnter->id());
+			debugC(5, kDebugEvents, "%s: Dispatching mouse enter to %s", __func__, state.mouseEnter->debugName());
 			state.mouseEnter->mouseEnteredEvent(event);
 		}
 
 		if (flags & kMouseExitFlag) {
-			debugC(5, kDebugEvents, "%s: Dispatching mouse exit to actor %d", __func__, state.mouseExit->id());
+			debugC(5, kDebugEvents, "%s: Dispatching mouse exit to %s", __func__, state.mouseExit->debugName());
 			state.mouseExit->mouseExitedEvent(event);
 		}
 	} else {
-		debugC(5, kDebugEvents, "%s: No hotspot to dispatch to", __func__);
+		debugC(5, kDebugEvents, "%s: No actor to accept event", __func__);
 	}
 }
 
diff --git a/engines/mediastation/actors/timer.cpp b/engines/mediastation/actors/timer.cpp
index 44361c39592..8119a458c22 100644
--- a/engines/mediastation/actors/timer.cpp
+++ b/engines/mediastation/actors/timer.cpp
@@ -72,7 +72,7 @@ void TimerActor::timePlay() {
 		}
 	}
 
-	debugC(5, kDebugScript, "Timer::timePlay(): Now playing for %d ms", _duration);
+	debugC(5, kDebugScript, "[%s] %s: Now playing for %d ms", debugName(), __func__, _duration);
 }
 
 void TimerActor::timeStop() {
diff --git a/engines/mediastation/audio.cpp b/engines/mediastation/audio.cpp
index ba76640e03e..97d3f662815 100644
--- a/engines/mediastation/audio.cpp
+++ b/engines/mediastation/audio.cpp
@@ -78,7 +78,6 @@ void AudioSequence::readChunk(Chunk &chunk) {
 		error("%s: Unknown audio encoding 0x%x", __func__, static_cast<uint>(_bitsPerSample));
 	}
 	_streams.push_back(stream);
-	debugC(5, kDebugLoading, "Finished reading audio chunk (@0x%llx)", static_cast<long long int>(chunk.pos()));
 }
 
 bool AudioSequence::isActive() {
diff --git a/engines/mediastation/context.cpp b/engines/mediastation/context.cpp
index 95c39c55d3c..b93ee4ba7cf 100644
--- a/engines/mediastation/context.cpp
+++ b/engines/mediastation/context.cpp
@@ -54,7 +54,7 @@ void MediaStationEngine::readControlCommands(Chunk &chunk) {
 	ContextSectionType sectionType = kContextEndOfSection;
 	do {
 		sectionType = static_cast<ContextSectionType>(chunk.readTypedUint16());
-		debugC(5, kDebugLoading, "%s: sectionType = 0x%x (@0x%llx)", __func__, static_cast<uint>(sectionType), static_cast<long long int>(chunk.pos()));
+		debugC(5, kDebugLoading, "%s: command 0x%x", __func__, static_cast<uint>(sectionType));
 		if (sectionType != kContextEndOfSection) {
 			readCommandFromStream(chunk, sectionType);
 		}
@@ -80,14 +80,14 @@ void MediaStationEngine::readDestroyContextData(Chunk &chunk) {
 
 void MediaStationEngine::readDestroyActorData(Chunk &chunk) {
 	uint actorId = chunk.readTypedUint16();
-	debugC(5, kDebugLoading, "%s: Actor %d", __func__, actorId);
+	debugC(5, kDebugLoading, "[%s] %s", g_engine->formatActorName(actorId).c_str(), __func__);
 	destroyActor(actorId);
 }
 
 void MediaStationEngine::readActorLoadComplete(Chunk &chunk) {
 	uint actorId = chunk.readTypedUint16();
-	debugC(5, kDebugLoading, "%s: Actor %d", __func__, actorId);
 	Actor *actor = g_engine->getActorById(actorId);
+	debugC(5, kDebugLoading, "[%s] %s", actor->debugName(), __func__);
 	actor->loadIsComplete();
 }
 
@@ -95,7 +95,7 @@ void MediaStationEngine::readCreateActorData(Chunk &chunk) {
 	uint contextId = chunk.readTypedUint16();
 	ActorType type = static_cast<ActorType>(chunk.readTypedUint16());
 	uint id = chunk.readTypedUint16();
-	debugC(5, kDebugLoading, "%s: Actor %d, type 0x%x", __func__, id, static_cast<uint>(type));
+	debugC(5, kDebugLoading, "[%s] %s: type 0x%x", g_engine->formatActorName(id).c_str(), __func__, static_cast<uint>(type));
 
 	Actor *actor = nullptr;
 	switch (type) {
@@ -156,7 +156,7 @@ void MediaStationEngine::readCreateActorData(Chunk &chunk) {
 		break;
 
 	default:
-		error("%s: No class for actor type 0x%x (@0x%llx)", __func__, static_cast<uint>(type), static_cast<long long int>(chunk.pos()));
+		error("%s: No class for actor type 0x%x", __func__, static_cast<uint>(type));
 	}
 	actor->setId(id);
 	actor->setContextId(contextId);
@@ -168,7 +168,7 @@ void MediaStationEngine::readCreateVariableData(Chunk &chunk) {
 	uint contextId = chunk.readTypedUint16();
 	uint id = chunk.readTypedUint16();
 	if (g_engine->getVariable(id) != nullptr) {
-		error("%s: Global variable %d already exists", __func__, id);
+		error("[%s] %s: Global variable already exists", g_engine->formatVariableName(id).c_str(), __func__);
 	}
 
 	ScriptValue *value = new ScriptValue(&chunk);
@@ -178,19 +178,21 @@ void MediaStationEngine::readCreateVariableData(Chunk &chunk) {
 	}
 
 	context->_variables.setVal(id, value);
-	debugC(5, kDebugScript, "%s: %d (type: %s)", __func__, id, scriptValueTypeToStr(value->getType()));
+	debugC(5, kDebugLoading, "[%s] %s", g_engine->formatVariableName(id).c_str(), __func__);
 }
 
 void MediaStationEngine::readHeaderSections(Subfile &subfile, Chunk &chunk) {
 	do {
+		debugC(5, kDebugLoading, "[%s] %s", g_engine->formatAssetNameForChannelIdent(chunk._id).c_str(), __func__);
 		ChannelClient *actor = g_engine->getChannelClientByChannelIdent(chunk._id);
 		if (actor == nullptr) {
-			error("%s: Client \"%s\" (0x%x) does not exist or has not been read yet in this title. (@0x%llx)", __func__, tag2str(chunk._id), chunk._id, static_cast<long long int>(chunk.pos()));
+			error("%s: Client %s does not exist or has not been read yet in this title",
+				__func__, g_engine->formatAssetNameForChannelIdent(chunk._id).c_str());
 		}
+
 		if (chunk.bytesRemaining() > 0) {
 			actor->readChunk(chunk);
 		}
-
 		if (chunk.bytesRemaining() != 0) {
 			warning("%s: %d bytes remaining at end of chunk", __func__, chunk.bytesRemaining());
 		}
diff --git a/engines/mediastation/datafile.cpp b/engines/mediastation/datafile.cpp
index 0d71ae00863..6a365d17da4 100644
--- a/engines/mediastation/datafile.cpp
+++ b/engines/mediastation/datafile.cpp
@@ -168,10 +168,10 @@ bool Chunk::seek(int64 offset, int whence) {
 
 	if (pos() < _dataStartOffset) {
 		uint overrun = _dataStartOffset - offset;
-		error("Attempted to seek 0x%x bytes before start of chunk (@0x%llx)", overrun, static_cast<long long int>(pos()));
+		error("%s: Attempted to seek 0x%x bytes before start of chunk (@0x%llx)", __func__, overrun, static_cast<long long int>(pos()));
 	} else if (pos() > _dataEndOffset) {
 		uint overrun = offset - _dataEndOffset;
-		error("Attempted to seek 0x%x bytes past end of chunk (@0x%llx)", overrun, static_cast<long long int>(pos()));
+		error("%s: Attempted to seek 0x%x bytes past end of chunk (@0x%llx)", __func__, overrun, static_cast<long long int>(pos()));
 	}
 	return true;
 }
@@ -227,7 +227,7 @@ void CdRomStream::openStream(uint streamId) {
 
 	const FileInfo &fileInfo = g_engine->fileInfoForIdent(streamInfo._fileId);
 	if (fileInfo._id == 0) {
-		error("%s: File %d for stream %d not found in current title", __func__, streamInfo._fileId, streamId);
+		error("%s: File %s for stream %d not found in current title", __func__, g_engine->formatFileName(streamInfo._fileId).c_str(), streamId);
 	}
 
 	bool requestedStreamAlreadyOpen = isOpen() && _fileId == streamInfo._fileId;
@@ -312,7 +312,7 @@ void StreamFeedManager::closeStreamFeed(StreamFeed *streamFeed) {
 
 void StreamFeedManager::registerChannelClient(ChannelClient *client) {
 	if (_channelClients.getValOrDefault(client->channelIdent()) != nullptr) {
-		error("%s: Channel ident %d already has a client", __func__, client->channelIdent());
+		error("%s: Channel %s already has a client", __func__, g_engine->formatAssetNameForChannelIdent(client->channelIdent()).c_str());
 	}
 	_channelClients.setVal(client->channelIdent(), client);
 }
diff --git a/engines/mediastation/graphics.cpp b/engines/mediastation/graphics.cpp
index bea28202896..fb55ddb9cd7 100644
--- a/engines/mediastation/graphics.cpp
+++ b/engines/mediastation/graphics.cpp
@@ -228,6 +228,7 @@ bool VideoDisplayManager::attemptToReadFromStream(Chunk &chunk, uint sectionType
 		break;
 
 	case kVideoDisplayManagerSetTime:
+		debugC(5, kDebugGraphics, "%s", __func__);
 		_defaultTransitionTime = chunk.readTypedTime();
 		break;
 
@@ -253,6 +254,7 @@ void VideoDisplayManager::readAndEffectTransition(Chunk &chunk) {
 }
 
 void VideoDisplayManager::readAndRegisterPalette(Chunk &chunk) {
+	debugC(5, kDebugGraphics, "%s", __func__);
 	byte *buffer = new byte[Graphics::PALETTE_SIZE];
 	chunk.read(buffer, Graphics::PALETTE_SIZE);
 	if (_registeredPalette != nullptr) {
@@ -330,10 +332,12 @@ void VideoDisplayManager::doTransitionOnSync() {
 }
 
 void VideoDisplayManager::performUpdateDirty() {
+	debugC(5, kDebugGraphics, "%s", __func__);
 	g_engine->draw();
 }
 
 void VideoDisplayManager::performUpdateAll() {
+	debugC(5, kDebugGraphics, "%s", __func__);
 	g_engine->draw(false);
 }
 
@@ -350,6 +354,7 @@ void VideoDisplayManager::fadeToBlack(Common::Array<ScriptValue> &args) {
 		colorCount = static_cast<uint>(args[3].asFloat());
 	}
 
+	debugC(5, kDebugGraphics, "%s: fadeTime: %f, paletteRange: [%d, %d]", __func__, fadeTime, startIndex, startIndex + colorCount);
 	_fadeToColor(0, 0, 0, fadeTime, startIndex, colorCount);
 }
 
@@ -366,6 +371,7 @@ void VideoDisplayManager::fadeToRegisteredPalette(Common::Array<ScriptValue> &ar
 		colorCount = static_cast<uint>(args[3].asFloat());
 	}
 
+	debugC(5, kDebugGraphics, "%s: fadeTime: %f, paletteRange: [%d, %d]", __func__, fadeTime, startIndex, startIndex + colorCount);
 	_fadeToRegisteredPalette(fadeTime, startIndex, colorCount);
 }
 
@@ -378,6 +384,7 @@ void VideoDisplayManager::setToRegisteredPalette(Common::Array<ScriptValue> &arg
 		colorCount = static_cast<uint>(args[2].asFloat());
 	}
 
+	debugC(5, kDebugGraphics, "%s: colors: [%d, %d]", __func__, startIndex, startIndex + colorCount);
 	_setToRegisteredPalette(startIndex, colorCount);
 }
 
@@ -390,6 +397,7 @@ void VideoDisplayManager::setToBlack(Common::Array<ScriptValue> &args) {
 		colorCount = static_cast<uint>(args[2].asFloat());
 	}
 
+	debugC(5, kDebugGraphics, "%s: colors: [%d, %d]", __func__, startIndex, startIndex + colorCount);
 	_setToColor(0, 0, 0, startIndex, colorCount);
 }
 
@@ -411,6 +419,8 @@ void VideoDisplayManager::fadeToColor(Common::Array<ScriptValue> &args) {
 		colorCount = static_cast<uint>(args[7].asFloat());
 	}
 
+	debugC(5, kDebugGraphics, "%s: (%d, %d, %d), fadeTime: %f, paletteRange: [%d, %d]",
+		__func__, r, g, b, fadeTime, startIndex, startIndex + colorCount);
 	_fadeToColor(r, g, b, fadeTime, startIndex, colorCount);
 }
 
@@ -425,6 +435,8 @@ void VideoDisplayManager::setToColor(Common::Array<ScriptValue> &args) {
 	uint startIndex = static_cast<uint>(args[4].asFloat());
 	uint colorCount = static_cast<uint>(args[5].asFloat());
 
+	debugC(5, kDebugGraphics, "%s: (%d, %d, %d), paletteRange: [%d, %d]",
+		__func__, r, g, b, startIndex, startIndex + colorCount);
 	_setToColor(r, g, b, startIndex, colorCount);
 }
 
@@ -440,6 +452,8 @@ void VideoDisplayManager::setToPercentOfPalette(Common::Array<ScriptValue> &args
 	uint startIndex = static_cast<uint>(args[5].asFloat());
 	uint colorCount = static_cast<uint>(args[6].asFloat());
 
+	debugC(5, kDebugGraphics, "%s: %f of (%d, %d, %d), paletteRange: [%d, %d]",
+		__func__, percent, r, g, b, startIndex, startIndex + colorCount);
 	_setPercentToColor(percent, r, g, b, startIndex, colorCount);
 }
 
@@ -463,6 +477,8 @@ void VideoDisplayManager::fadeToPaletteObject(Common::Array<ScriptValue> &args)
 		colorCount = static_cast<uint>(args[4].asFloat());
 	}
 
+	debugC(5, kDebugGraphics, "%s: %d, fadeTime: %f, paletteRange: [%d, %d]",
+		__func__, paletteId, fadeTime, startIndex, startIndex + colorCount);
 	_fadeToPaletteObject(paletteId, fadeTime, startIndex, colorCount);
 }
 
@@ -482,6 +498,8 @@ void VideoDisplayManager::setToPaletteObject(Common::Array<ScriptValue> &args) {
 		colorCount = static_cast<uint>(args[3].asFloat());
 	}
 
+	debugC(5, kDebugGraphics, "%s: %d, paletteRange: [%d, %d]",
+		__func__, paletteId, startIndex, startIndex + colorCount);
 	_setToPaletteObject(paletteId, startIndex, colorCount);
 }
 
@@ -503,6 +521,8 @@ void VideoDisplayManager::setToPercentOfPaletteObject(Common::Array<ScriptValue>
 		colorCount = static_cast<uint>(args[4].asFloat());
 	}
 
+	debugC(5, kDebugGraphics, "%s: %f of %d, paletteRange: [%d, %d]",
+		__func__, percent, paletteId, startIndex, startIndex + colorCount);
 	_setPercentToPaletteObject(percent, paletteId, startIndex, colorCount);
 }
 
@@ -516,6 +536,8 @@ void VideoDisplayManager::colorShiftCurrentPalette(Common::Array<ScriptValue> &a
 	uint startIndex = static_cast<uint>(args[2].asFloat());
 	uint colorCount = static_cast<uint>(args[3].asFloat());
 
+	debugC(5, kDebugGraphics, "%s: shift: %d, paletteRange: [%d, %d]",
+		__func__, shift, startIndex, startIndex + colorCount);
 	_colorShiftCurrentPalette(startIndex, shift, colorCount);
 }
 
diff --git a/engines/mediastation/mediascript/codechunk.cpp b/engines/mediastation/mediascript/codechunk.cpp
index f0db162afd1..b2e619c7a03 100644
--- a/engines/mediastation/mediascript/codechunk.cpp
+++ b/engines/mediastation/mediascript/codechunk.cpp
@@ -237,14 +237,16 @@ ScriptValue CodeChunk::evaluateValue() {
 
 	case kOperandTypeParamToken: {
 		uint literal = _bytecode->readTypedUint16();
-		debugC(5, kDebugScript, "%d ", literal);
+		Common::String tokenName = g_engine->formatParamTokenName(literal);
+		debugC(5, kDebugScript, "%s ", tokenName.c_str());
 		returnValue.setToParamToken(literal);
 		return returnValue;
 	}
 
 	case kOperandTypeActorId: {
 		uint actorId = _bytecode->readTypedUint16();
-		debugC(5, kDebugScript, "%d ", actorId);
+		Common::String actorName = g_engine->formatActorName(actorId, true);
+		debugC(5, kDebugScript, "%s ", actorName.c_str());
 		returnValue.setToActorId(actorId);
 		return returnValue;
 	}
@@ -263,7 +265,9 @@ ScriptValue CodeChunk::evaluateValue() {
 
 	case kOperandTypeFunctionId: {
 		uint functionId = _bytecode->readTypedUint16();
-		debugC(5, kDebugScript, "%d ", functionId);
+		// Function IDs are included in this same listing that also includes actors.
+		Common::String functionName = g_engine->formatFunctionName(functionId);
+		debugC(5, kDebugScript, "%s ", functionName.c_str());
 		returnValue.setToFunctionId(functionId);
 		return returnValue;
 	}
@@ -288,27 +292,29 @@ ScriptValue CodeChunk::evaluateVariable() {
 ScriptValue *CodeChunk::readAndReturnVariable() {
 	uint id = _bytecode->readTypedUint16();
 	VariableScope scope = static_cast<VariableScope>(_bytecode->readTypedUint16());
-	debugC(5, kDebugScript, "%d (%s)", id, variableScopeToStr(scope));
+	Common::String name = g_engine->formatVariableName(id);
 
-	ScriptValue returnValue;
+	ScriptValue *variable = nullptr;
 	switch (scope) {
 	case kVariableScopeGlobal: {
-		ScriptValue *variable = g_engine->getVariable(id);
+		variable = g_engine->getVariable(id);
 		if (variable == nullptr) {
-			error("%s: Global variable %d doesn't exist", __func__, id);
+			error("%s: Global variable %s doesn't exist", __func__, g_engine->formatVariableName(id).c_str());
 		}
-		return variable;
+		break;
 	}
 
 	case kVariableScopeLocal: {
 		uint index = id - 1;
-		return &_locals.operator[](index);
+		variable = &_locals.operator[](index);
+		break;
 	}
 
 	case kVariableScopeIndirectParameter: {
 		ScriptValue indexValue = evaluateExpression();
 		uint index = static_cast<uint>(indexValue.asFloat() + id);
-		return &_args->operator[](index);
+		variable = &_args->operator[](index);
+		break;
 	}
 
 	case kVariableScopeParameter: {
@@ -316,12 +322,16 @@ ScriptValue *CodeChunk::readAndReturnVariable() {
 		if (_args == nullptr) {
 			error("%s: Requested a parameter in a code chunk that has no parameters", __func__);
 		}
-		return &_args->operator[](index);
+		variable = &_args->operator[](index);
+		break;
 	}
 
 	default:
 		error("%s: Got unknown variable scope %s (%d)", __func__, variableScopeToStr(scope), static_cast<uint>(scope));
 	}
+
+	debugC(5, kDebugScript, "%s (%s) [value: %s]", name.c_str(), variableScopeToStr(scope), variable->getDebugString().c_str());
+	return variable;
 }
 
 void CodeChunk::evaluateIf() {
@@ -465,7 +475,8 @@ ScriptValue CodeChunk::evaluateFunctionCall(bool isIndirect) {
 }
 
 ScriptValue CodeChunk::evaluateFunctionCall(uint functionId, uint paramCount) {
-	debugC(5, kDebugScript, "%d (%d params)", functionId, paramCount);
+	Common::String functionName = g_engine->formatFunctionName(functionId);
+	debugC(5, kDebugScript, "%s (%d params)", functionName.c_str(), paramCount);
 
 	Common::Array<ScriptValue> args;
 	for (uint i = 0; i < paramCount; i++) {
@@ -515,14 +526,14 @@ ScriptValue CodeChunk::evaluateMethodCall(BuiltInMethod method, uint paramCount)
 		if (target.asActorId() == 0) {
 			// It seems to be valid to call a method on a null actor ID, in
 			// which case nothing happens. Still issue warning for traceability.
-			warning("%s: Attempt to call method on a null actor ID", __func__);
+			warning("%s: Attempt to call method %s (%d) on null actor ID", __func__, builtInMethodToStr(method), static_cast<uint>(method));
 			return returnValue;
 		} else {
 			// This is a regular actor that we can process directly.
 			uint actorId = target.asActorId();
 			Actor *targetActor = g_engine->getActorById(actorId);
 			if (targetActor == nullptr) {
-				error("%s: Attempt to call method on actor ID %d, which isn't loaded", __func__, target.asActorId());
+				error("[%s] %s: Actor not loaded", g_engine->formatActorName(target.asActorId()).c_str(), __func__);
 			}
 			returnValue = targetActor->callMethod(method, args);
 			return returnValue;
@@ -536,7 +547,8 @@ ScriptValue CodeChunk::evaluateMethodCall(BuiltInMethod method, uint paramCount)
 	}
 
 	default:
-		error("Attempt to call method on unimplemented value type %s (%d)",
+		error("%s: Attempt to call method %s (%d) on unimplemented value type %s (%d)", __func__,
+			builtInMethodToStr(method), static_cast<uint>(method),
 			scriptValueTypeToStr(target.getType()), static_cast<uint>(target.getType()));
 	}
 }
@@ -544,7 +556,7 @@ ScriptValue CodeChunk::evaluateMethodCall(BuiltInMethod method, uint paramCount)
 void CodeChunk::evaluateDeclareLocals() {
 	uint localVariableCount = _bytecode->readTypedUint16();
 	if (localVariableCount <= 0) {
-		error("Got non-positive local variable count");
+		error("%s: Got non-positive local variable count", __func__);
 	}
 	debugC(5, kDebugScript, "%d", localVariableCount);
 	_locals = Common::Array<ScriptValue>(localVariableCount);
@@ -569,11 +581,11 @@ void CodeChunk::evaluateWhileLoop() {
 		_bytecode->seek(loopStartPosition);
 		ScriptValue condition = evaluateExpression();
 		if (condition.getType() != kScriptValueTypeBool) {
-			error("Expected loop condition to be bool, not %s", scriptValueTypeToStr(condition.getType()));
+			error("%s: Expected loop condition to be bool, not %s", __func__, scriptValueTypeToStr(condition.getType()));
 		}
 
 		if (++iterationCount >= MAX_LOOP_ITERATION_COUNT) {
-			error("Exceeded max loop iteration count");
+			error("%s: Exceeded max loop iteration count", __func__);
 		}
 
 		if (condition.asBool()) {
diff --git a/engines/mediastation/mediascript/collection.cpp b/engines/mediastation/mediascript/collection.cpp
index 52b64037bb7..195a84f68f5 100644
--- a/engines/mediastation/mediascript/collection.cpp
+++ b/engines/mediastation/mediascript/collection.cpp
@@ -47,12 +47,22 @@ ScriptValue Collection::callMethod(BuiltInMethod method, Common::Array<ScriptVal
 
 	case kDeleteFirstMethod:
 		assert(args.empty());
-		returnValue = remove_at(0);
+		if (size() > 0) {
+			returnValue = remove_at(0);
+			debugC(7, kDebugScript, "%s: %s", __func__, returnValue.getDebugString().c_str());
+		} else {
+			warning("%s: Array is empty", __func__);
+		}
 		break;
 
 	case kDeleteLastMethod:
 		assert(args.empty());
-		returnValue = remove_at(size() - 1);
+		if (size() > 0) {
+			returnValue = remove_at(size() - 1);
+			debugC(7, kDebugScript, "%s: %s", __func__, returnValue.getDebugString().c_str());
+		} else {
+			warning("%s: Array is empty", __func__);
+		}
 		break;
 
 	case kEmptyMethod:
@@ -63,7 +73,11 @@ ScriptValue Collection::callMethod(BuiltInMethod method, Common::Array<ScriptVal
 	case kGetAtMethod: {
 		assert(args.size() == 1);
 		uint index = static_cast<uint>(args[0].asFloat());
-		returnValue = operator[](index);
+		if (index < size()) {
+			returnValue = operator[](index);
+		} else {
+			warning("%s: Index %d out of bounds %d", __func__, index, size());
+		}
 		break;
 	}
 
@@ -91,21 +105,34 @@ ScriptValue Collection::callMethod(BuiltInMethod method, Common::Array<ScriptVal
 	case kDeleteAtMethod: {
 		assert(args.size() == 1);
 		uint index = static_cast<uint>(args[0].asFloat());
-		returnValue = remove_at(index);
+		if (index < size()) {
+			returnValue = remove_at(index);
+			debugC(7, kDebugScript, "%s: %s", __func__, returnValue.getDebugString().c_str());
+		} else {
+			warning("%s: Index %d out of bounds %d", __func__, index, size());
+		}
 		break;
 	}
 
 	case kInsertAtMethod: {
 		assert(args.size() == 2);
 		uint index = static_cast<uint>(args[1].asFloat());
-		insert_at(index, args[0]);
+		if (index <= size()) {
+			insert_at(index, args[0]);
+		} else {
+			warning("%s: Index %d out of bounds %d", __func__, index, size());
+		}
 		break;
 	}
 
 	case kReplaceAtMethod: {
 		assert(args.size() == 2);
 		uint index = static_cast<uint>(args[1].asFloat());
-		operator[](index) = args[0];
+		if (index < size()) {
+			operator[](index) = args[0];
+		} else {
+			warning("%s: Index %d out of bounds %d", __func__, index, size());
+		}
 		break;
 	}
 
@@ -130,6 +157,7 @@ void Collection::apply(const Common::Array<ScriptValue> &args) {
 	uint functionId = args[0].asFunctionId();
 	for (const ScriptValue &item : *this) {
 		argsToApply[0] = item;
+		debugC(7, kDebugScript, "%s: %s: %s", __func__, g_engine->formatFunctionName(functionId).c_str(), item.getDebugString().c_str());
 		g_engine->getFunctionManager()->call(functionId, argsToApply);
 	}
 }
@@ -148,15 +176,19 @@ void Collection::send(const Common::Array<ScriptValue> &args) {
 		uint actorId = item.asActorId();
 		Actor *targetActor = g_engine->getActorById(actorId);
 		if (targetActor != nullptr) {
+			debugC(7, kDebugScript, "%s: %s: %d", __func__, builtInMethodToStr(methodToSend), actorId);
 			targetActor->callMethod(methodToSend, argsToSend);
 		}
 	}
 }
 
-int Collection::seek(const ScriptValue &item) {
+int Collection::seek(const ScriptValue &lhs) {
 	// Search from back to front.
 	for (int i = size() - 1; i >= 0; i--) {
-		if (item == operator[](i)) {
+		const ScriptValue &rhs = operator[](i);
+		debugC(7, kDebugScript, "%s: %d of %d: Checking (%s) == (%s)",
+			__func__, i, size(), lhs.getDebugString().c_str(), rhs.getDebugString().c_str());
+		if (lhs == rhs) {
 			return i;
 		}
 	}
diff --git a/engines/mediastation/mediascript/eventhandler.cpp b/engines/mediastation/mediascript/eventhandler.cpp
index d97321475bf..ae597207d10 100644
--- a/engines/mediastation/mediascript/eventhandler.cpp
+++ b/engines/mediastation/mediascript/eventhandler.cpp
@@ -21,13 +21,13 @@
 
 #include "mediastation/mediascript/eventhandler.h"
 #include "mediastation/debugchannels.h"
+#include "mediastation/mediastation.h"
 
 namespace MediaStation {
 
 EventHandler::EventHandler(Chunk &chunk) {
 	_type = static_cast<EventType>(chunk.readTypedUint16());
-	debugC(5, kDebugLoading, "EventHandler::EventHandler(): Type %s (%d) (@0x%llx)",
-		eventTypeToStr(_type), static_cast<uint>(_type), static_cast<long long int>(chunk.pos()));
+	debugC(5, kDebugLoading, "%s: %s (%d)", __func__, eventTypeToStr(_type), static_cast<uint>(_type));
 
 	_argumentValue = ScriptValue(&chunk);
 	_code = new CodeChunk(chunk);
@@ -36,7 +36,8 @@ EventHandler::EventHandler(Chunk &chunk) {
 ScriptValue EventHandler::execute(uint actorId) {
 	// TODO: The actorId is only passed in for debug visibility, there should be
 	// a better way to handle that.
-	Common::String actorAndType = Common::String::format("(actor %d) (type = %s)", actorId, eventTypeToStr(_type));
+	Common::String actorName = g_engine->formatActorName(actorId, true);
+	Common::String actorAndType = Common::String::format("%s (%s)", actorName.c_str(), eventTypeToStr(_type));
 	Common::String argValue = Common::String::format("(%s)", _argumentValue.getDebugString().c_str());
 	debugC(5, kDebugScript, "\n********** EVENT HANDLER %s %s **********", actorAndType.c_str(), argValue.c_str());
 
diff --git a/engines/mediastation/mediascript/function.cpp b/engines/mediastation/mediascript/function.cpp
index 5075cbc4b21..490b968e978 100644
--- a/engines/mediastation/mediascript/function.cpp
+++ b/engines/mediastation/mediascript/function.cpp
@@ -26,9 +26,6 @@
 namespace MediaStation {
 ScriptFunction::ScriptFunction(Chunk &chunk) {
 	_contextId = chunk.readTypedUint16();
-	// In PROFILE._ST (only present in some titles), the function ID is reported
-	// with 19900 added, so function 100 would be reported as 20000. But in
-	// bytecode, the zero-based ID is used, so that's what we'll store here.
 	_id = chunk.readTypedUint16();
 	_code = new CodeChunk(chunk);
 }
@@ -39,7 +36,8 @@ ScriptFunction::~ScriptFunction() {
 }
 
 ScriptValue ScriptFunction::execute(Common::Array<ScriptValue> &args) {
-	debugC(5, kDebugScript, "\n********** SCRIPT FUNCTION %d **********", _id);
+	Common::String name = g_engine->formatFunctionName(_id);
+	debugC(5, kDebugScript, "\n********** SCRIPT FUNCTION %s **********", name.c_str());
 	ScriptValue returnValue = _code->execute(&args);
 	debugC(5, kDebugScript, "********** END SCRIPT FUNCTION **********");
 	return returnValue;
diff --git a/engines/mediastation/mediascript/scriptconstants.cpp b/engines/mediastation/mediascript/scriptconstants.cpp
index 75af8bcb854..833dc63158c 100644
--- a/engines/mediastation/mediascript/scriptconstants.cpp
+++ b/engines/mediastation/mediascript/scriptconstants.cpp
@@ -443,7 +443,7 @@ const char *operandTypeToStr(OperandType type) {
 	case kOperandTypeString:
 		return "String";
 	case kOperandTypeParamToken:
-		return "DollarSignVariable";
+		return "ParamToken";
 	case kOperandTypeActorId:
 		return "ActorId";
 	case kOperandTypeTime:
diff --git a/engines/mediastation/mediascript/scriptvalue.cpp b/engines/mediastation/mediascript/scriptvalue.cpp
index b6b67e0dcc5..283814e5eaf 100644
--- a/engines/mediastation/mediascript/scriptvalue.cpp
+++ b/engines/mediastation/mediascript/scriptvalue.cpp
@@ -21,6 +21,7 @@
 
 #include "mediastation/mediascript/scriptvalue.h"
 #include "mediastation/mediascript/function.h"
+#include "mediastation/mediastation.h"
 
 namespace MediaStation {
 
@@ -232,7 +233,7 @@ BuiltInMethod ScriptValue::asMethodId() const {
 	}
 }
 
-Common::String ScriptValue::getDebugString() {
+Common::String ScriptValue::getDebugString() const {
 	switch (getType()) {
 	case kScriptValueTypeEmpty:
 		return "empty";
@@ -240,14 +241,21 @@ Common::String ScriptValue::getDebugString() {
 	case kScriptValueTypeFloat:
 		return Common::String::format("float: %f", asFloat());
 
-	case kScriptValueTypeActorId:
-		return Common::String::format("actor: %d", asActorId());
+	case kScriptValueTypeActorId: {
+		Common::String actorName = g_engine->formatActorName(asActorId(), true);
+		return Common::String::format("actor: %s", actorName.c_str());
+	}
 
 	case kScriptValueTypeTime:
 		return Common::String::format("time: %f", asTime());
 
-	case kScriptValueTypeParamToken:
-		return Common::String::format("token: %d", asParamToken());
+	case kScriptValueTypeParamToken: {
+		Common::String tokenName = g_engine->formatParamTokenName(asParamToken());
+		return Common::String::format("token: %s", tokenName.c_str());
+	}
+
+	case kScriptValueTypeString:
+		return Common::String::format("string: \"%s\"", asString().c_str());
 
 	default:
 		return Common::String::format("arg type %s", scriptValueTypeToStr(getType()));
@@ -256,52 +264,56 @@ Common::String ScriptValue::getDebugString() {
 
 bool ScriptValue::compare(Opcode op, const ScriptValue &lhs, const ScriptValue &rhs) {
 	if (lhs.getType() != rhs.getType()) {
-		warning("%s: Attempt to compare mismatched types %s and %s", __func__, scriptValueTypeToStr(lhs.getType()), scriptValueTypeToStr(rhs.getType()));
+		warning("%s: Attempt to compare mismatched values: %s; %s",
+			__func__, lhs.getDebugString().c_str(), rhs.getDebugString().c_str());
 	}
 
+	bool result = false;
 	switch (lhs.getType()) {
 	case kScriptValueTypeEmpty:
-		return compareEmptyValues(op);
+		result = compareEmptyValues(op);
+		break;
 
 	case kScriptValueTypeFloat:
-		return compare(op, lhs.asFloat(), rhs.asFloat());
+		result = compare(op, lhs.asFloat(), rhs.asFloat());
 		break;
 
 	case kScriptValueTypeBool:
-		return compare(op, lhs.asBool(), rhs.asBool());
+		result = compare(op, lhs.asBool(), rhs.asBool());
 		break;
 
 	case kScriptValueTypeTime:
-		return compare(op, lhs.asTime(), rhs.asTime());
+		result = compare(op, lhs.asTime(), rhs.asTime());
 		break;
 
 	case kScriptValueTypeParamToken:
-		return compare(op, lhs.asParamToken(), rhs.asParamToken());
+		result = compare(op, lhs.asParamToken(), rhs.asParamToken());
 		break;
 
 	case kScriptValueTypeActorId:
-		return compare(op, lhs.asActorId(), rhs.asActorId());
+		result = compare(op, lhs.asActorId(), rhs.asActorId());
 		break;
 
 	case kScriptValueTypeString:
-		return compareStrings(op, lhs.asString(), rhs.asString());
+		result = compareStrings(op, lhs.asString(), rhs.asString());
 		break;
 
 	case kScriptValueTypeCollection:
-		return compare(op, lhs.asCollection(), rhs.asCollection());
+		result = compare(op, lhs.asCollection(), rhs.asCollection());
 		break;
 
 	case kScriptValueTypeFunctionId:
-		return compare(op, lhs.asFunctionId(), rhs.asFunctionId());
+		result = compare(op, lhs.asFunctionId(), rhs.asFunctionId());
 		break;
 
 	case kScriptValueTypeMethodId:
-		return compare(op, static_cast<uint>(lhs.asMethodId()), static_cast<uint>(rhs.asMethodId()));
+		result = compare(op, static_cast<uint>(lhs.asMethodId()), static_cast<uint>(rhs.asMethodId()));
 		break;
 
 	default:
 		error("%s: Got unknown script value type %d", __func__, lhs.getType());
 	}
+	return result;
 }
 
 bool ScriptValue::compareEmptyValues(Opcode op) {
diff --git a/engines/mediastation/mediascript/scriptvalue.h b/engines/mediastation/mediascript/scriptvalue.h
index eb738b77751..6b8fc130f24 100644
--- a/engines/mediastation/mediascript/scriptvalue.h
+++ b/engines/mediastation/mediascript/scriptvalue.h
@@ -70,7 +70,7 @@ public:
 	void setToMethodId(BuiltInMethod methodId);
 	BuiltInMethod asMethodId() const;
 
-	Common::String getDebugString();
+	Common::String getDebugString() const;
 
 	bool operator==(const ScriptValue &other) const;
 	bool operator!=(const ScriptValue &other) const;
diff --git a/engines/mediastation/mediastation.cpp b/engines/mediastation/mediastation.cpp
index f187ed526d7..c99212b81fd 100644
--- a/engines/mediastation/mediastation.cpp
+++ b/engines/mediastation/mediastation.cpp
@@ -87,6 +87,9 @@ MediaStationEngine::~MediaStationEngine() {
 	delete _streamFeedManager;
 	_streamFeedManager = nullptr;
 
+	delete _profile;
+	_profile = nullptr;
+
 	_contextReferences.clear();
 	_streamMap.clear();
 	_engineResourceDeclarations.clear();
@@ -103,7 +106,7 @@ SpatialEntity *MediaStationEngine::getSpatialEntityById(uint spatialEntityId) {
 	Actor *actor = getActorById(spatialEntityId);
 	if (actor != nullptr) {
 		if (!actor->isSpatialActor()) {
-			error("%s: Actor %d is not a spatial actor", __func__, spatialEntityId);
+			error("[%s] %s: Not a spatial actor", formatActorName(spatialEntityId).c_str(), __func__);
 		}
 		return static_cast<SpatialEntity *>(actor);
 	}
@@ -152,6 +155,7 @@ Common::Error MediaStationEngine::run() {
 	initDeviceOwner();
 	initStageDirector();
 	initStreamFeedManager();
+	initProfile();
 	setupInitialStreamMap();
 
 	if (ConfMan.hasKey("entry_context")) {
@@ -176,12 +180,12 @@ void MediaStationEngine::runEventLoop() {
 		}
 		_document->process();
 
-		debugC(5, kDebugGraphics, "***** START SCREEN UPDATE ***");
+		debugC(9, kDebugGraphics, "***** START SCREEN UPDATE ***");
 		for (auto it = _actors.begin(); it != _actors.end(); ++it) {
 			it->_value->process();
 		}
 		draw();
-		debugC(5, kDebugGraphics, "***** END SCREEN UPDATE ***");
+		debugC(9, kDebugGraphics, "***** END SCREEN UPDATE ***");
 
 		g_system->delayMillis(10);
 	}
@@ -231,6 +235,11 @@ void MediaStationEngine::initStreamFeedManager() {
 	registerWithStreamManager();
 }
 
+void MediaStationEngine::initProfile() {
+	_profile = new Profile();
+	_profile->load("PROFILE._ST");
+}
+
 void MediaStationEngine::setupInitialStreamMap() {
 	StreamInfo streamInfo;
 	streamInfo._actorId = 0;
@@ -299,7 +308,7 @@ void MediaStationEngine::draw(bool dirtyOnly) {
 
 void MediaStationEngine::registerActor(Actor *actorToAdd) {
 	if (getActorById(actorToAdd->id())) {
-		error("%s: Actor with ID 0x%d was already defined in this title", __func__, actorToAdd->id());
+		error("[%s] %s: Already defined in this title", formatActorName(actorToAdd).c_str(), __func__);
 	}
 	_actors.setVal(actorToAdd->id(), actorToAdd);
 }
@@ -310,15 +319,16 @@ void MediaStationEngine::destroyActor(uint actorId) {
 		delete _actors[actorId];
 		_actors.erase(actorId);
 	} else {
-		warning("%s: Actor %d is not currently loaded", __func__, actorId);
+		warning("[%s] %s: Not currently loaded", formatActorName(actorId).c_str(), __func__);
 	}
 }
 
 void MediaStationEngine::destroyContext(uint contextId, bool eraseFromLoadedContexts) {
-	debugC(5, kDebugScript, "%s: Destroying context %d", __func__, contextId);
+	debugC(5, kDebugScript, "%s: Context %d", __func__, contextId);
 	Context *context = _loadedContexts.getValOrDefault(contextId);
 	if (context == nullptr) {
-		error("%s: Attempted to unload context %d that is not currently loaded", __func__, contextId);
+		warning("%s: Attempted to unload context %d that is not currently loaded", __func__, contextId);
+		return;
 	}
 
 	getRootStage()->deleteChildrenFromContextId(contextId);
@@ -374,7 +384,7 @@ void MediaStationEngine::readUnrecognizedFromStream(Chunk &chunk, uint sectionTy
 	}
 
 	if (!paramHandled) {
-		warning("%s: Parameter %d not handled", __func__, sectionType);
+		warning("%s: Parameter %d not handled (0x%llx)", __func__, sectionType, static_cast<long long int>(chunk.pos()));
 	}
 }
 
@@ -390,7 +400,7 @@ void MediaStationEngine::readChunk(Chunk &chunk) {
 		break;
 
 	default:
-		error("%s: Unhandled section type 0x%x", __func__, static_cast<uint>(streamType));
+		error("%s: Unhandled section type 0x%x (0x%llx)", __func__, static_cast<uint>(streamType), static_cast<long long int>(chunk.pos()));
 	}
 }
 
diff --git a/engines/mediastation/mediastation.h b/engines/mediastation/mediastation.h
index d5d20411980..f57c6c27c10 100644
--- a/engines/mediastation/mediastation.h
+++ b/engines/mediastation/mediastation.h
@@ -43,6 +43,7 @@
 #include "mediastation/actor.h"
 #include "mediastation/cursors.h"
 #include "mediastation/graphics.h"
+#include "mediastation/profile.h"
 #include "mediastation/mediascript/function.h"
 #include "mediastation/actors/stage.h"
 
@@ -107,6 +108,14 @@ public:
 	StreamFeedManager *getStreamFeedManager() { return _streamFeedManager; }
 	Document *getDocument() { return _document; }
 
+	Common::String formatActorName(uint actorId, bool attemptToGetType = false) { return _profile->formatActorName(actorId, attemptToGetType); }
+	Common::String formatActorName(const Actor *actor) { return _profile->formatActorName(actor); }
+	Common::String formatFunctionName(uint functionId) { return _profile->formatFunctionName(functionId); }
+	Common::String formatFileName(uint fileId) { return _profile->formatFileName(fileId); }
+	Common::String formatVariableName(uint variableId) { return _profile->formatVariableName(variableId); }
+	Common::String formatParamTokenName(uint paramToken) { return _profile->formatParamTokenName(paramToken); }
+	Common::String formatAssetNameForChannelIdent(uint channelIdent) { return _profile->formatAssetNameForChannelIdent(channelIdent); }
+
 	const FileInfo &fileInfoForIdent(uint fileId) { return _fileMap.getValOrDefault(fileId); }
 	const StreamInfo &streamInfoForIdent(uint streamId) { return _streamMap.getValOrDefault(streamId); }
 	const ScreenReference &screenRefWithId(uint screenActorId) { return _screenReferences.getValOrDefault(screenActorId); }
@@ -144,6 +153,7 @@ private:
 	DeviceOwner *_deviceOwner = nullptr;
 	StageDirector *_stageDirector = nullptr;
 	StreamFeedManager *_streamFeedManager = nullptr;
+	Profile *_profile = nullptr;
 
 	Common::HashMap<uint, Actor *> _actors;
 	SpatialEntity *_mouseInsideHotspot = nullptr;
@@ -169,6 +179,7 @@ private:
 	void initDeviceOwner();
 	void initStageDirector();
 	void initStreamFeedManager();
+	void initProfile();
 	void setupInitialStreamMap();
 
 	void runEventLoop();
diff --git a/engines/mediastation/module.mk b/engines/mediastation/module.mk
index 475e70fea9d..8f1d9a51e75 100644
--- a/engines/mediastation/module.mk
+++ b/engines/mediastation/module.mk
@@ -32,7 +32,8 @@ MODULE_OBJS = \
 	mediascript/scriptconstants.o \
 	mediascript/scriptvalue.o \
 	mediastation.o \
-	metaengine.o
+	metaengine.o \
+	profile.o
 
 # This module can be built as a plugin
 ifeq ($(ENABLE_MEDIASTATION), DYNAMIC_PLUGIN)
diff --git a/engines/mediastation/profile.cpp b/engines/mediastation/profile.cpp
new file mode 100644
index 00000000000..0c9e20abad0
--- /dev/null
+++ b/engines/mediastation/profile.cpp
@@ -0,0 +1,315 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/tokenizer.h"
+
+#include "mediastation/actor.h"
+#include "mediastation/debugchannels.h"
+#include "mediastation/profile.h"
+#include "mediastation/mediastation.h"
+#include "mediastation/mediascript/scriptconstants.h"
+
+namespace MediaStation {
+
+void Profile::readSection(Common::File &file, void (Profile::*sectionParserMethod)(const Common::String &)) {
+	const Common::String IMAGE_SET_LINE_DELIMITER = Common::String('#');
+	const Common::String SUMMARY_LINE_DELIMITER = Common::String('*');
+	const Common::String SECTION_DELIMITER = Common::String('!');
+
+	Common::String line;
+	bool shouldKeepReading = !file.err() && !file.eos();
+	while (shouldKeepReading) {
+		line = file.readLine();
+		if (file.err() || file.eos()) {
+			break;
+		}
+
+		line.trim();
+		if (line.empty()) {
+			// There should only be a single empty line at the end of the file.
+			if (!file.eos()) {
+				warning("%s: Encountered unexpected empty line", __func__);
+			}
+
+		} else if (line == SECTION_DELIMITER) {
+			debugC(7, kDebugLoading, "%s: End of section", __func__);
+			break;
+
+		} else if (line.hasPrefix(SUMMARY_LINE_DELIMITER)) {
+			// Example: "* 3987 20432 3881 15000"
+			// Don't bother parsing the summary line, because it doesn't contain information
+			// that uniquely identifies entities for pretty printing while debugging.
+			debugC(7, kDebugLoading, "%s: summary: %s", __func__, line.c_str());
+
+		} else if (line.hasPrefix(IMAGE_SET_LINE_DELIMITER)) {
+			// Example: "# image_7d12g_Background 15000 15001 15002 15003 15004 15005"
+			// This seems to only occur once in Hercules (even though there are other titles that have
+			// image set actors).  For now, just print it raw but don't process it.
+			debugC(5, kDebugLoading, "%s: image set: %s", __func__, line.c_str());
+
+		} else {
+			// Try to actually parse this line.
+			(this->*sectionParserMethod)(line);
+		}
+
+		shouldKeepReading = !file.err() && !file.eos();
+	}
+}
+
+void Profile::load(const Common::Path &filename) {
+	Common::File file;
+	if (!file.open(filename)) {
+		debugC(5, kDebugLoading, "%s: Could not open profile %s. Entity names will not be available.",
+			__func__, filename.toString().c_str());
+		return;
+	}
+	parseVersionInfo(file.readLine());
+
+	readSection(file, &Profile::parseContextInfo);
+	debugC(5, kDebugLoading, "%s: Read %d context infos", __func__, _contexts.size());
+
+	readSection(file, &Profile::parseAssetInfo);
+	debugC(5, kDebugLoading, "%s: Read %d asset infos", __func__, _assets.size());
+
+	readSection(file, &Profile::parseFileInfo);
+	debugC(5, kDebugLoading, "%s: Read %d file infos", __func__, _files.size());
+
+	readSection(file, &Profile::parseVariableInfo);
+	debugC(5, kDebugLoading, "%s: Read %d variable infos", __func__, _variables.size());
+
+	readSection(file, &Profile::parseParamTokenInfo);
+	debugC(5, kDebugLoading, "%s: Read %d param token infos", __func__, _paramTokens.size());
+
+	readSection(file, &Profile::parseScriptConstantInfo);
+	debugC(5, kDebugLoading, "%s: Read %d script constant infos", __func__, _constants.size());
+
+	file.close();
+}
+
+void Profile::parseVersionInfo(const Common::String &line) {
+	// Example: "_Version3.3_ _MAC_"
+	Common::StringTokenizer tokenizer(line);
+	_versionNumber = tokenizer.nextToken();
+	_platform = tokenizer.nextToken();
+	debugC(5, kDebugLoading, "%s: version: %s; platform: %s", __func__,  _versionNumber.c_str(), _platform.c_str());
+}
+
+void Profile::parseContextInfo(const Common::String &line) {
+	// Example: "Context Root_6c00 2929877932"
+	ProfileContextInfo contextInfo;
+	Common::StringTokenizer tokenizer(line);
+	contextInfo.type = tokenizer.nextToken();
+	contextInfo.name = tokenizer.nextToken();
+	Common::String unk1Str = tokenizer.nextToken();
+	contextInfo.unk1 = atol(unk1Str.c_str());
+	_contexts.push_back(contextInfo);
+
+	debugC(5, kDebugLoading, "%s: \"%s\" -> type: %s; name: %s; unk1: %d",
+		__func__, line.c_str(), contextInfo.type.c_str(), contextInfo.name.c_str(), contextInfo.unk1);
+}
+
+void Profile::parseAssetInfo(const Common::String &line) {
+	// Examples:
+	//  - No channel: "Puppy_Transition 113 0"
+	//  - One channel: "snd_6c16_NoteLevel1 1436 74"
+	//  - Multiple channels: "movie_6cb1_LayeredBumpers 154 111 112 113"
+	//		(Currently only stream movies seem to have multiple channels.)
+	Common::StringTokenizer tokenizer(line);
+	ProfileAssetInfo assetInfo;
+	assetInfo.name = tokenizer.nextToken();
+	Common::String idAsString = tokenizer.nextToken();
+	assetInfo.id = atoi(idAsString.c_str());
+	debugCN(5, kDebugLoading, "%s: \"%s\" -> name %s; id: %d; channelIdents: [ ",
+		__func__, line.c_str(), assetInfo.name.c_str(), assetInfo.id);
+
+	// The channel IDs presented here must be transformed into the actual channel IDs
+	// (FourCCs) seen when reading chunks by converting them to hex and adding an offset.
+	// So, for example, "snd_6c16_NoteLevel1 1436 74" has actual channel ID "a04a", since
+	// 74 in hex is 0x4A, and then we add 0xA000.
+	const uint CHANNEL_ID_OFFSET_TO_OBTAIN_REAL_CHUNK_FOURCC = 0xA000;
+	while (!tokenizer.empty()) {
+		Common::String channelIdentAsString = tokenizer.nextToken();
+		uint channelIdentAsUnoffsetInt = atoi(channelIdentAsString.c_str());
+		if (channelIdentAsUnoffsetInt != 0) {
+			uint channelIdentAsInt = atoi(channelIdentAsString.c_str()) + CHANNEL_ID_OFFSET_TO_OBTAIN_REAL_CHUNK_FOURCC;
+			assetInfo.channelIdents.push_back(channelIdentAsInt);
+			_channelIdentsAsIntToAssetId.setVal(channelIdentAsInt, assetInfo.id);
+			debugCN(5, kDebugLoading, "%x ", channelIdentAsInt);
+		}
+	}
+	debugC(5, kDebugLoading, "]"); // Close out the printed channel idents list.
+	_assets.setVal(assetInfo.id, assetInfo);
+}
+
+void Profile::parseFileInfo(const Common::String &line) {
+	Common::StringTokenizer tokenizer(line);
+	ProfileFileInfo fileInfo;
+	fileInfo.name = tokenizer.nextToken();
+	Common::String idStr = tokenizer.nextToken();
+	fileInfo.id = atoi(idStr.c_str());
+	_files.setVal(fileInfo.id, fileInfo);
+
+	debugC(5, kDebugLoading, "%s: \"%s\" -> filename: %s fileId: %d",
+		__func__, line.c_str(), fileInfo.name.c_str(), fileInfo.id);
+}
+
+void Profile::parseVariableInfo(const Common::String &line) {
+	Common::StringTokenizer tokenizer(line);
+	ProfileVariableInfo variableInfo;
+	variableInfo.name = tokenizer.nextToken();
+	Common::String idStr = tokenizer.nextToken();
+	variableInfo.id = atoi(idStr.c_str());
+	_variables.setVal(variableInfo.id, variableInfo);
+
+	debugC(5, kDebugLoading, "%s: \"%s\" -> name: %s id: %d",
+		__func__, line.c_str(), variableInfo.name.c_str(), variableInfo.id);
+}
+
+void Profile::parseParamTokenInfo(const Common::String &line) {
+	Common::StringTokenizer tokenizer(line);
+	ProfileParamTokenInfo paramTokenInfo;
+	paramTokenInfo.name = tokenizer.nextToken();
+	Common::String idStr = tokenizer.nextToken();
+	paramTokenInfo.id = (uint16)atoi(idStr.c_str());
+	_paramTokens.setVal(paramTokenInfo.id, paramTokenInfo);
+
+	debugC(5, kDebugLoading, "%s: \"%s\" -> name: %s id: %d",
+		__func__, line.c_str(), paramTokenInfo.name.c_str(), paramTokenInfo.id);
+}
+
+void Profile::parseScriptConstantInfo(const Common::String &line) {
+	Common::StringTokenizer tokenizer(line);
+	ProfileScriptConstantInfo constantInfo;
+	constantInfo.name = tokenizer.nextToken();
+	if (!tokenizer.empty()) {
+		// There is no type information stored here, so we will just store the
+		// textual value of the constant as is.
+		constantInfo.value = tokenizer.nextToken();
+	}
+	_constants.push_back(constantInfo);
+
+	debugC(5, kDebugLoading, "%s: \"%s\" -> name: %s value: %s",
+		__func__, line.c_str(), constantInfo.name.c_str(), constantInfo.value.c_str());
+}
+
+Common::String Profile::formatActorName(uint actorId, bool attemptToGetType) {
+	// If requested, try to get the actor type by looking up the loaded actor
+	if (attemptToGetType) {
+		Actor *actor = g_engine->getActorById(actorId);
+		if (actor != nullptr) {
+			return formatActorName(actor);
+		}
+	}
+
+	Common::String formattedName;
+	const Common::String &actorName = _assets.getValOrDefault(actorId).name;
+	if (!actorName.empty()) {
+		formattedName = Common::String::format("%s (%d)", actorName.c_str(), actorId);
+	} else {
+		formattedName = Common::String::format("%d", actorId);
+	}
+	return formattedName;
+}
+
+Common::String Profile::formatActorName(const Actor *actor) {
+	if (actor == nullptr) {
+		return "<null>";
+	}
+
+	Common::String formattedName;
+	const Common::String &actorName = _assets.getValOrDefault(actor->id()).name;
+	if (!actorName.empty()) {
+		formattedName = Common::String::format("%s [%s %d]", actorName.c_str(), actorTypeToStr(actor->type()), actor->id());
+	} else {
+		// Even if we don't have a name, try to give at least some visibility by including the type.
+		formattedName = Common::String::format("%s %d", actorTypeToStr(actor->type()), actor->id());
+	}
+	return formattedName;
+}
+
+Common::String Profile::formatFunctionName(uint functionId) {
+	// Only in PROFILE._ST, the function ID is reported with 19900 added,
+	// so function 100 would be reported as 20000. But in bytecode, the
+	// zero-based ID is used.
+	Common::String formattedName;
+	uint offsetFunctionId = functionId + 19900;
+	const Common::String &functionName = _assets.getValOrDefault(offsetFunctionId).name;
+	if (!functionName.empty()) {
+		// Report the function ID as it appears in bytecode, so without the odd offset added.
+		formattedName = Common::String::format("%s (%d)", functionName.c_str(), functionId);
+	} else {
+		// This might be a built-in function, in which case we can try to get the built-in name.
+		formattedName = Common::String::format("%s (%d)", builtInFunctionToStr(static_cast<BuiltInFunction>(functionId)), functionId);
+	}
+	return formattedName;
+}
+
+Common::String Profile::formatFileName(uint fileId) {
+	Common::String formattedName;
+	const Common::String &fileName = _files.getValOrDefault(fileId).name;
+	if (!fileName.empty()) {
+		formattedName = Common::String::format("%s (%d)", fileName.c_str(), fileId);
+	} else {
+		formattedName = Common::String::format("%d", fileId);
+	}
+	return formattedName;
+}
+
+Common::String Profile::formatVariableName(uint variableId) {
+	Common::String formattedName;
+	const Common::String &variableName = _variables.getValOrDefault(variableId).name;
+	if (!variableName.empty()) {
+		formattedName = Common::String::format("%s (%d)", variableName.c_str(), variableId);
+	} else {
+		formattedName = Common::String::format("%d", variableId);
+	}
+	return formattedName;
+}
+
+Common::String Profile::formatParamTokenName(uint paramToken) {
+	Common::String formattedName;
+	const Common::String &paramTokenName = _paramTokens.getValOrDefault(paramToken).name;
+	if (!paramTokenName.empty()) {
+		formattedName = Common::String::format("%s (%d)", paramTokenName.c_str(), paramToken);
+	} else {
+		formattedName = Common::String::format("%d", paramToken);
+	}
+	return formattedName;
+}
+
+Common::String Profile::formatAssetNameForChannelIdent(uint channelIdentAsTag) {
+	Common::String formattedName;
+	if (channelIdentAsTag == MKTAG('i', 'g', 'o', 'd')) {
+		formattedName = "ImtGod";
+	} else {
+		Common::String channelIdentAsString = Common::tag2string(channelIdentAsTag);
+		uint channelIdentAsInt = strtol(channelIdentAsString.c_str(), nullptr, 16);
+		if (_channelIdentsAsIntToAssetId.contains(channelIdentAsInt)) {
+			uint assetId = _channelIdentsAsIntToAssetId.getVal(channelIdentAsInt);
+			formattedName = Common::String::format("%s [%s]", channelIdentAsString.c_str(), formatActorName(assetId).c_str());
+		} else {
+			formattedName = Common::String::format("%s", tag2str(channelIdentAsTag));
+		}
+	}
+	return formattedName;
+}
+
+} // End of namespace MediaStation
diff --git a/engines/mediastation/profile.h b/engines/mediastation/profile.h
new file mode 100644
index 00000000000..b1af55856e4
--- /dev/null
+++ b/engines/mediastation/profile.h
@@ -0,0 +1,112 @@
+/* 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 MEDIASTATION_PROFILE_H
+#define MEDIASTATION_PROFILE_H
+
+#include "common/str.h"
+#include "common/array.h"
+#include "common/hashmap.h"
+#include "common/file.h"
+
+namespace MediaStation {
+
+class Actor;
+
+struct ProfileContextInfo {
+	// This is usually "Document", "Context", or "Screen".
+	Common::String type;
+	Common::String name;
+	uint unk1 = 0;
+};
+
+// These can be actors or functions, hence using the generic term "asset" here.
+struct ProfileAssetInfo {
+	Common::String name;
+	uint id = 0;
+	Common::Array<uint16> channelIdents;
+};
+
+struct ProfileFileInfo {
+	Common::String name;
+	uint id = 0;
+};
+
+struct ProfileVariableInfo {
+	Common::String name;
+	uint id = 0;
+};
+
+struct ProfileParamTokenInfo {
+	Common::String name;
+	uint id = 0;
+};
+
+struct ProfileScriptConstantInfo {
+	Common::String name;
+	Common::String value;
+};
+
+// Profiles (PROFILE._ST) contain mappings between names and IDs for assets,
+// functions, variables, and other entities. Some titles do not have this mapping,
+// but when it exists it is very helpful for debugging. It is not required for actually
+// running any games.
+class Profile {
+public:
+	void load(const Common::Path &filename);
+
+	Common::String formatActorName(uint actorId, bool attemptToGetType = false);
+	Common::String formatActorName(const Actor *actor);
+
+	Common::String formatFunctionName(uint assetId);
+	Common::String formatFileName(uint fileId);
+	Common::String formatVariableName(uint variableId);
+	Common::String formatParamTokenName(uint paramToken);
+	Common::String formatAssetNameForChannelIdent(uint channelIdent);
+
+	const Common::String &getFileName(uint16 fileId) const { return _files.getValOrDefault(fileId).name; }
+	const Common::String &getResourceName(uint16 resourceId) const { return _paramTokens.getValOrDefault(resourceId).name; }
+
+private:
+	Common::String _versionNumber;
+	Common::String _platform;
+	Common::Array<ProfileContextInfo> _contexts; // It isn't clear what the key would be.
+	Common::HashMap<uint, ProfileAssetInfo> _assets;
+	Common::HashMap<uint, uint> _channelIdentsAsIntToAssetId;
+	Common::HashMap<uint, ProfileFileInfo> _files;
+	Common::HashMap<uint, ProfileVariableInfo> _variables;
+	Common::HashMap<uint, ProfileParamTokenInfo> _paramTokens;
+	Common::Array<ProfileScriptConstantInfo> _constants;
+
+	void parseVersionInfo(const Common::String &line);
+	void parseContextInfo(const Common::String &line);
+	void parseAssetInfo(const Common::String &line);
+	void parseFileInfo(const Common::String &line);
+	void parseVariableInfo(const Common::String &line);
+	void parseParamTokenInfo(const Common::String &line);
+	void parseScriptConstantInfo(const Common::String &line);
+
+	void readSection(Common::File &file, void (Profile::*parser)(const Common::String &));
+};
+
+} // End of namespace MediaStation
+
+#endif


Commit: f14ca1483037f344758c7ab2cb4e3b0a734f5db2
    https://github.com/scummvm/scummvm/commit/f14ca1483037f344758c7ab2cb4e3b0a734f5db2
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-02-24T20:49:55-05:00

Commit Message:
MEDIASTATION: Make sprite clips work with newer engine versions

Changed paths:
    engines/mediastation/actor.h
    engines/mediastation/actors/sprite.cpp
    engines/mediastation/actors/sprite.h
    engines/mediastation/debugchannels.h
    engines/mediastation/detection.cpp
    engines/mediastation/mediastation.cpp


diff --git a/engines/mediastation/actor.h b/engines/mediastation/actor.h
index 78a63c6e79e..736ca22987b 100644
--- a/engines/mediastation/actor.h
+++ b/engines/mediastation/actor.h
@@ -134,7 +134,7 @@ enum ActorHeaderSectionType {
 
 	// SPRITE FIELDS.
 	kActorHeaderSpriteClip = 0x03e9,
-	kActorHeaderCurrentSpriteClip = 0x03ea
+	kActorHeaderDefaultSpriteClip = 0x03ea
 };
 
 enum CylindricalWrapMode : int;
diff --git a/engines/mediastation/actors/sprite.cpp b/engines/mediastation/actors/sprite.cpp
index 641ea76a423..8b1c2fb0ebc 100644
--- a/engines/mediastation/actors/sprite.cpp
+++ b/engines/mediastation/actors/sprite.cpp
@@ -30,6 +30,10 @@ SpriteFrameHeader::SpriteFrameHeader(Chunk &chunk) : BitmapHeader(chunk) {
 	_offset = chunk.readTypedPoint();
 }
 
+Common::String SpriteMovieClip::getDebugString() const {
+	return Common::String::format("%s: [%d, %d]", g_engine->formatParamTokenName(id).c_str(), firstFrameIndex, lastFrameIndex);
+}
+
 SpriteFrame::SpriteFrame(Chunk &chunk, SpriteFrameHeader *header) : Bitmap(chunk, header) {
 	_bitmapHeader = header;
 }
@@ -88,33 +92,22 @@ void SpriteMovieActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramT
 		_isVisible = static_cast<bool>(chunk.readTypedByte());
 		break;
 
-	case kActorHeaderSpriteChunkCount: {
-		_frameCount = chunk.readTypedUint16();
-
-		// Set the default clip.
-		SpriteClip clip;
-		clip.id = DEFAULT_CLIP_ID;
-		clip.firstFrameIndex = 0;
-		clip.lastFrameIndex = _frameCount - 1;
-		_clips.setVal(clip.id, clip);
-		setCurrentClip(clip.id);
+	case kActorHeaderSpriteChunkCount:
+		_asset->_frameCount = chunk.readTypedUint16();
 		break;
-	}
 
 	case kActorHeaderSpriteClip: {
-		SpriteClip spriteClip;
-		spriteClip.id = chunk.readTypedUint16();
-		spriteClip.firstFrameIndex = chunk.readTypedUint16();
-		spriteClip.lastFrameIndex = chunk.readTypedUint16();
-		_clips.setVal(spriteClip.id, spriteClip);
+		SpriteMovieClip clip;
+		clip.id = chunk.readTypedUint16();
+		clip.firstFrameIndex = chunk.readTypedUint16();
+		clip.lastFrameIndex = chunk.readTypedUint16();
+		_clips.setVal(clip.id, clip);
 		break;
 	}
 
-	case kActorHeaderCurrentSpriteClip: {
-		uint clipId = chunk.readTypedUint16();
-		setCurrentClip(clipId);
+	case kActorHeaderDefaultSpriteClip:
+		_defaultClipId = chunk.readTypedUint16();
 		break;
-	}
 
 	case kActorHeaderActorReference: {
 		_actorReference = chunk.readTypedUint16();
@@ -135,6 +128,23 @@ void SpriteMovieActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramT
 	}
 }
 
+void SpriteMovieActor::loadIsComplete() {
+	// This clip goes forward through all the sprite's frames.
+	SpriteMovieClip forwardClip(DEFAULT_FORWARD_CLIP_ID, 0, _asset->_frameCount - 1);
+	if (!_clips.contains(DEFAULT_FORWARD_CLIP_ID)) {
+		_clips.setVal(forwardClip.id, forwardClip);
+	}
+
+	// This clip goes backward through all the sprite's frames.
+	SpriteMovieClip backwardClip(DEFAULT_BACKWARD_CLIP_ID, _asset->_frameCount - 1, 0);
+	if (!_clips.contains(DEFAULT_BACKWARD_CLIP_ID)) {
+		_clips.setVal(backwardClip.id, backwardClip);
+	}
+
+	SpatialEntity::loadIsComplete();
+	setCurrentClip(_defaultClipId);
+}
+
 ScriptValue SpriteMovieActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
 	ScriptValue returnValue;
 
@@ -171,7 +181,7 @@ ScriptValue SpriteMovieActor::callMethod(BuiltInMethod methodId, Common::Array<S
 
 	case kSetCurrentClipMethod: {
 		assert(args.size() <= 1);
-		uint clipId = DEFAULT_CLIP_ID;
+		uint clipId = DEFAULT_FORWARD_CLIP_ID;
 		if (args.size() == 1) {
 			clipId = args[0].asParamToken();
 		}
@@ -226,19 +236,53 @@ ScriptValue SpriteMovieActor::callMethod(BuiltInMethod methodId, Common::Array<S
 }
 
 bool SpriteMovieActor::activateNextFrame() {
-	if (_currentFrameIndex < _activeClip.lastFrameIndex) {
-		_currentFrameIndex++;
-		dirtyIfVisible();
-		return true;
+	bool clipMovesForward = _activeClip.firstFrameIndex <= _activeClip.lastFrameIndex;
+	if (clipMovesForward) {
+		debugC(3, kDebugSpriteMovie, "[%s] %s: FORWARD: currentFrameIndex: %d; activeClip.lastFrameIndex: %d",
+			debugName(), __func__, _currentFrameIndex, _activeClip.lastFrameIndex);
+
+		bool canMoveForward = _currentFrameIndex < _activeClip.lastFrameIndex;
+		if (canMoveForward) {
+			dirtyIfVisible();
+			_currentFrameIndex++;
+			dirtyIfVisible();
+			return true;
+		}
+
+	} else {
+		debugC(3, kDebugSpriteMovie, "[%s] %s: BACKWARD: currentFrameIndex: %d; activeClip.lastFrameIndex: %d",
+			debugName(), __func__, _currentFrameIndex, _activeClip.lastFrameIndex);
+
+		bool canMoveBackward = _currentFrameIndex > _activeClip.lastFrameIndex;
+		if (canMoveBackward) {
+			dirtyIfVisible();
+			_currentFrameIndex--;
+			dirtyIfVisible();
+			return true;
+		}
 	}
 	return false;
 }
 
 bool SpriteMovieActor::activatePreviousFrame() {
-	if (_currentFrameIndex > _activeClip.firstFrameIndex) {
-		_currentFrameIndex--;
-		dirtyIfVisible();
-		return true;
+	bool clipMovesBackward = _activeClip.lastFrameIndex < _activeClip.firstFrameIndex;
+	if (clipMovesBackward) {
+		bool canMoveTowardFirst = _currentFrameIndex < _activeClip.firstFrameIndex;
+		if (canMoveTowardFirst) {
+			dirtyIfVisible();
+			_currentFrameIndex++;
+			dirtyIfVisible();
+			return true;
+		}
+
+	} else {
+		bool canMoveTowardFirst = _activeClip.firstFrameIndex < _currentFrameIndex;
+		if (canMoveTowardFirst) {
+			dirtyIfVisible();
+			_currentFrameIndex--;
+			dirtyIfVisible();
+			return true;
+		}
 	}
 	return false;
 }
@@ -263,35 +307,46 @@ void SpriteMovieActor::play() {
 	_nextFrameTime = 0;
 
 	scheduleNextFrame();
+	debugC(3, kDebugSpriteMovie, "[%s] %s", debugName(), __func__);
 }
 
 void SpriteMovieActor::stop() {
 	_nextFrameTime = 0;
 	_isPlaying = false;
+	debugC(3, kDebugSpriteMovie, "[%s] %s", debugName(), __func__);
 }
 
 void SpriteMovieActor::setCurrentClip(uint clipId) {
 	if (_activeClip.id != clipId) {
 		if (_clips.contains(clipId)) {
+			SpriteMovieClip newClip = _clips.getVal(clipId);
+			debugC(3, kDebugSpriteMovie, "[%s] %s: (frameCount: %d) activeClip: %s; newClip: %s",
+				debugName(), __func__, _asset->_frameCount, _activeClip.getDebugString().c_str(), newClip.getDebugString().c_str());
 			_activeClip = _clips.getVal(clipId);
 		} else {
 			_activeClip.id = clipId;
-			warning("%s: Sprite clip %d not found in sprite %s", __func__, clipId, debugName());
+			warning("[%s] %s: Clip %s not found", debugName(), __func__, _activeClip.getDebugString().c_str());
 		}
-	}
 
-	setCurrentFrameToInitial();
+		setCurrentFrameToInitial();
+	}
 }
 
 void SpriteMovieActor::setCurrentFrameToInitial() {
+	debugC(3, kDebugSpriteMovie, "[%s] %s: currentFrameIndex: %d, activeClip.firstFrameIndex: %d",
+		debugName(), __func__, _currentFrameIndex, _activeClip.firstFrameIndex);
 	if (_currentFrameIndex != _activeClip.firstFrameIndex) {
+		dirtyIfVisible();
 		_currentFrameIndex = _activeClip.firstFrameIndex;
 		dirtyIfVisible();
 	}
 }
 
 void SpriteMovieActor::setCurrentFrameToFinal() {
+	debugC(3, kDebugSpriteMovie, "[%s] %s: currentFrameIndex: %d, activeClip.lastFrameIndex: %d",
+		debugName(), __func__, _currentFrameIndex, _activeClip.lastFrameIndex);
 	if (_currentFrameIndex != _activeClip.lastFrameIndex) {
+		dirtyIfVisible();
 		_currentFrameIndex = _activeClip.lastFrameIndex;
 		dirtyIfVisible();
 	}
@@ -320,16 +375,36 @@ void SpriteMovieActor::scheduleNextFrame() {
 		return;
 	}
 
-	if (_currentFrameIndex < _activeClip.lastFrameIndex) {
-		scheduleNextTimerEvent();
+	debugC(3, kDebugSpriteMovie, "[%s] %s: currentFrame: %d; activeClip: [%d, %d]",
+		debugName(), __func__, _currentFrameIndex,
+		_activeClip.firstFrameIndex, _activeClip.lastFrameIndex);
+	int firstFrameIndex = _activeClip.firstFrameIndex;
+	int lastFrameIndex = _activeClip.lastFrameIndex;
+
+	// For backward clips, we've "passed" the last frame when currentFrameIndex <= lastFrameIndex.
+	bool clipMovesBackward = lastFrameIndex < firstFrameIndex;
+	bool currentIsAtOrBeyondLast = lastFrameIndex <= _currentFrameIndex;
+	bool needsStopEvaluation = clipMovesBackward || currentIsAtOrBeyondLast;
+	bool backwardClipContinues = clipMovesBackward && currentIsAtOrBeyondLast;
+
+	if (needsStopEvaluation) {
+		if (backwardClipContinues) {
+			// Backward clip still has frames to show (current > last).
+			scheduleNextTimerEvent();
+		} else {
+			// We reached the end of the clip, regardless of which direction we were moving.
+			stop();
+		}
 	} else {
-		stop();
+		// The forward clip still in progress.
+		scheduleNextTimerEvent();
 	}
 }
 
 void SpriteMovieActor::scheduleNextTimerEvent() {
 	uint frameDuration = 1000 / _frameRate;
 	_nextFrameTime += frameDuration;
+	debugC(3, kDebugSpriteMovie, "[%s] %s", debugName(), __func__);
 }
 
 void SpriteMovieActor::updateFrameState() {
@@ -339,7 +414,8 @@ void SpriteMovieActor::updateFrameState() {
 
 	uint currentTime = g_system->getMillis() - _startTime;
 	bool drawNextFrame = currentTime >= _nextFrameTime;
-	debugC(kDebugGraphics, "nextFrameTime: %d; startTime: %d, currentTime: %d", _nextFrameTime, _startTime, currentTime);
+	debugC(3, kDebugSpriteMovie, "[%s] %s: nextFrameTime: %d; startTime: %d, currentTime: %d",
+		debugName(), __func__, _nextFrameTime, _startTime, currentTime);
 	if (drawNextFrame) {
 		timerEvent();
 	}
@@ -347,16 +423,16 @@ void SpriteMovieActor::updateFrameState() {
 
 void SpriteMovieActor::timerEvent() {
 	if (!_isPlaying) {
-		error("%s: Attempt to activate sprite frame when sprite is not playing", __func__);
+		warning("[%s] %s: Not playing", debugName(), __func__);
 		return;
 	}
 
-	bool result = activateNextFrame();
-	if (!result) {
-		stop();
-	} else {
+	bool moreFramesToShow = activateNextFrame();
+	if (moreFramesToShow) {
 		postMovieEndEventIfNecessary();
 		scheduleNextFrame();
+	} else {
+		stop();
 	}
 }
 
@@ -368,6 +444,7 @@ void SpriteMovieActor::postMovieEndEventIfNecessary() {
 	_isPlaying = false;
 	_startTime = 0;
 	_nextFrameTime = 0;
+	debugC(3, kDebugSpriteMovie, "[%s] %s: Posting movie end", debugName(), __func__);
 
 	ScriptValue value;
 	value.setToParamToken(_activeClip.id);
@@ -375,10 +452,18 @@ void SpriteMovieActor::postMovieEndEventIfNecessary() {
 }
 
 void SpriteMovieActor::draw(DisplayContext &displayContext) {
+	if (static_cast<uint>(_currentFrameIndex) >= _asset->frames.size()) {
+		warning("[%s] %s: Requested frame %d, but we only have %d frames. Showing last frame",
+			debugName(), __func__, _currentFrameIndex, _asset->frames.size());
+		_currentFrameIndex = _asset->frames.size() - 1;
+	}
+
 	SpriteFrame *activeFrame = _asset->frames[_currentFrameIndex];
 	if (_isVisible) {
 		Common::Rect frameBbox = activeFrame->boundingBox();
 		frameBbox.translate(_boundingBox.left, _boundingBox.top);
+		debugC(8, kDebugSpriteMovie, "[%s] %s: frame %d",
+			debugName(), __func__, _currentFrameIndex);
 		g_engine->getDisplayManager()->imageBlit(frameBbox.origin(), activeFrame, _dissolveFactor, &displayContext);
 	}
 }
diff --git a/engines/mediastation/actors/sprite.h b/engines/mediastation/actors/sprite.h
index 58441bd11b8..389365298ed 100644
--- a/engines/mediastation/actors/sprite.h
+++ b/engines/mediastation/actors/sprite.h
@@ -34,10 +34,15 @@
 
 namespace MediaStation {
 
-struct SpriteClip {
+struct SpriteMovieClip {
 	uint id = 0;
-	uint firstFrameIndex = 0;
-	uint lastFrameIndex = 0;
+	int firstFrameIndex = 0;
+	int lastFrameIndex = 0;
+
+	SpriteMovieClip() = default;
+	SpriteMovieClip(uint clipId, int first, int last)
+		: id(clipId), firstFrameIndex(first), lastFrameIndex(last) {}
+	Common::String getDebugString() const;
 };
 
 class SpriteFrameHeader : public BitmapHeader {
@@ -68,6 +73,7 @@ private:
 struct SpriteAsset {
 	~SpriteAsset();
 
+	uint _frameCount = 0;
 	Common::Array<SpriteFrame *> frames;
 };
 
@@ -82,24 +88,25 @@ public:
 	virtual void draw(DisplayContext &displayContext) override;
 
 	virtual void readParameter(Chunk &chunk, ActorHeaderSectionType paramType) override;
+	virtual void loadIsComplete() override;
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 
-	virtual bool isVisible() const override { return _isVisible; }
-
 	virtual void readChunk(Chunk &chunk) override;
 
 private:
-	static const uint DEFAULT_CLIP_ID = 1200;
+	const uint DEFAULT_FORWARD_CLIP_ID = 0x4B0;
+	const uint DEFAULT_BACKWARD_CLIP_ID = 0x4B1;
+
 	uint _loadType = 0;
 	uint _frameRate = 0;
-	uint _frameCount = 0;
 	uint _actorReference = 0;
-	Common::HashMap<uint, SpriteClip> _clips;
+	Common::HashMap<uint, SpriteMovieClip> _clips;
 	Common::SharedPtr<SpriteAsset> _asset;
 	bool _isPlaying = false;
 	uint _currentFrameIndex = 0;
 	uint _nextFrameTime = 0;
-	SpriteClip _activeClip;
+	uint _defaultClipId = DEFAULT_FORWARD_CLIP_ID;
+	SpriteMovieClip _activeClip;
 
 	void play();
 	void stop();
diff --git a/engines/mediastation/debugchannels.h b/engines/mediastation/debugchannels.h
index dfbc6bc428b..bf75c59acda 100644
--- a/engines/mediastation/debugchannels.h
+++ b/engines/mediastation/debugchannels.h
@@ -38,6 +38,7 @@ enum DebugChannels {
 	kDebugScript,
 	kDebugEvents,
 	kDebugLoading,
+	kDebugSpriteMovie,
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/detection.cpp b/engines/mediastation/detection.cpp
index 40c3b9a1d22..ccfd49d637c 100644
--- a/engines/mediastation/detection.cpp
+++ b/engines/mediastation/detection.cpp
@@ -40,6 +40,7 @@ const DebugChannelDef MediaStationMetaEngineDetection::debugFlagList[] = {
 	{ MediaStation::kDebugScript, "script", "Enable debug script dump" },
 	{ MediaStation::kDebugEvents, "events", "Events processing" },
 	{ MediaStation::kDebugLoading, "loading", "File loading" },
+	{ MediaStation::kDebugSpriteMovie, "spritemovie", "Sprite movie debug level" },
 	DEBUG_CHANNEL_END
 };
 
diff --git a/engines/mediastation/mediastation.cpp b/engines/mediastation/mediastation.cpp
index c99212b81fd..7ec07977235 100644
--- a/engines/mediastation/mediastation.cpp
+++ b/engines/mediastation/mediastation.cpp
@@ -302,6 +302,7 @@ void MediaStationEngine::draw(bool dirtyOnly) {
 	} else {
 		_stageDirector->drawAll();
 	}
+	_stageDirector->clearDirtyRegion();
 	_displayManager->updateScreen();
 	_displayManager->doTransitionOnSync();
 }


Commit: f6b3fd2ab80e0f7795af25a04871d1487c64f4e5
    https://github.com/scummvm/scummvm/commit/f6b3fd2ab80e0f7795af25a04871d1487c64f4e5
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-02-24T20:49:55-05:00

Commit Message:
MEDIASTATION: Add helper function for getting an actor of a specific type

Changed paths:
    engines/mediastation/actor.cpp
    engines/mediastation/actors/camera.cpp
    engines/mediastation/actors/image.cpp
    engines/mediastation/actors/sprite.cpp
    engines/mediastation/actors/stage.cpp
    engines/mediastation/graphics.cpp
    engines/mediastation/mediastation.cpp
    engines/mediastation/mediastation.h


diff --git a/engines/mediastation/actor.cpp b/engines/mediastation/actor.cpp
index 99ccb72e361..18730db7d40 100644
--- a/engines/mediastation/actor.cpp
+++ b/engines/mediastation/actor.cpp
@@ -385,13 +385,7 @@ void SpatialEntity::readParameter(Chunk &chunk, ActorHeaderSectionType paramType
 void SpatialEntity::loadIsComplete() {
 	Actor::loadIsComplete();
 	if (_stageId != 0) {
-		Actor *pendingParentStageActor = g_engine->getActorById(_stageId);
-		if (pendingParentStageActor == nullptr) {
-			error("%s: Actor %d doesn't exist", __func__, _stageId);
-		} else if (pendingParentStageActor->type() != kActorTypeStage) {
-			error("%s: Requested parent stage %d is not a stage", __func__, _stageId);
-		}
-		StageActor *pendingParentStage = static_cast<StageActor *>(pendingParentStageActor);
+		StageActor *pendingParentStage = static_cast<StageActor *>(g_engine->getActorByIdAndType(_stageId, kActorTypeStage));
 		pendingParentStage->addChildSpatialEntity(this);
 	}
 }
diff --git a/engines/mediastation/actors/camera.cpp b/engines/mediastation/actors/camera.cpp
index 8870604a16b..1dac8fc3333 100644
--- a/engines/mediastation/actors/camera.cpp
+++ b/engines/mediastation/actors/camera.cpp
@@ -68,15 +68,8 @@ void CameraActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType)
 
 	case kActorHeaderCameraImageActor: {
 		uint actorReference = chunk.readTypedUint16();
-		Actor *referencedActor = g_engine->getActorById(actorReference);
-		if (referencedActor == nullptr) {
-			error("%s: Referenced actor %d doesn't exist or has not been read yet in this title", __func__, actorReference);
-		}
-		if (referencedActor->type() != kActorTypeCamera) {
-			error("%s: Type mismatch of referenced actor %d", __func__, actorReference);
-		}
-		CameraActor *referencedImage = static_cast<CameraActor *>(referencedActor);
-		_image = referencedImage->_image;
+		CameraActor *referencedCamera = static_cast<CameraActor *>(g_engine->getActorByIdAndType(actorReference, kActorTypeCamera));
+		_image = referencedCamera->_image;
 		break;
 	}
 
diff --git a/engines/mediastation/actors/image.cpp b/engines/mediastation/actors/image.cpp
index 62b11720129..637d294193c 100644
--- a/engines/mediastation/actors/image.cpp
+++ b/engines/mediastation/actors/image.cpp
@@ -60,14 +60,7 @@ void ImageActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 
 	case kActorHeaderActorReference: {
 		_actorReference = chunk.readTypedUint16();
-		Actor *referencedActor = g_engine->getActorById(_actorReference);
-		if (referencedActor == nullptr) {
-			error("%s: Referenced actor %d doesn't exist or has not been read yet in this title", __func__, _actorReference);
-		}
-		if (referencedActor->type() != kActorTypeImage) {
-			error("%s: Type mismatch of referenced actor %d", __func__, _actorReference);
-		}
-		ImageActor *referencedImage = static_cast<ImageActor *>(referencedActor);
+		ImageActor *referencedImage = static_cast<ImageActor *>(g_engine->getActorByIdAndType(_actorReference, kActorTypeImage));
 		_asset = referencedImage->_asset;
 		break;
 	}
diff --git a/engines/mediastation/actors/sprite.cpp b/engines/mediastation/actors/sprite.cpp
index 8b1c2fb0ebc..868e4fe2a7c 100644
--- a/engines/mediastation/actors/sprite.cpp
+++ b/engines/mediastation/actors/sprite.cpp
@@ -111,14 +111,7 @@ void SpriteMovieActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramT
 
 	case kActorHeaderActorReference: {
 		_actorReference = chunk.readTypedUint16();
-		Actor *referencedActor = g_engine->getActorById(_actorReference);
-		if (referencedActor == nullptr) {
-			error("%s: Referenced actor %d doesn't exist or has not been read yet in this title", __func__, _actorReference);
-		}
-		if (referencedActor->type() != kActorTypeSprite) {
-			error("%s: Type mismatch of referenced actor %d", __func__, _actorReference);
-		}
-		SpriteMovieActor *referencedSprite = static_cast<SpriteMovieActor *>(referencedActor);
+		SpriteMovieActor *referencedSprite = static_cast<SpriteMovieActor *>(g_engine->getActorByIdAndType(_actorReference, kActorTypeSprite));
 		_asset = referencedSprite->_asset;
 		break;
 	}
diff --git a/engines/mediastation/actors/stage.cpp b/engines/mediastation/actors/stage.cpp
index bb3df7c7fee..4c786877ffa 100644
--- a/engines/mediastation/actors/stage.cpp
+++ b/engines/mediastation/actors/stage.cpp
@@ -45,7 +45,7 @@ void StageActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 		// In stages, this basically has the oppose meaning it has outside of stages. Here,
 		// it specifies an actor that is a parent of this stage.
 		uint parentActorId = chunk.readTypedUint16();
-		_pendingParent = g_engine->getSpatialEntityById(parentActorId);
+		_pendingParent = static_cast<SpatialEntity *>(g_engine->getActorByIdAndType(parentActorId, kActorTypeStage));
 		break;
 	}
 
@@ -265,9 +265,6 @@ void StageActor::loadIsComplete() {
 	Actor::loadIsComplete();
 
 	if (_pendingParent != nullptr) {
-		if (_pendingParent->type() != kActorTypeStage) {
-			error("%s: Parent must be a stage", __func__);
-		}
 		StageActor *parentStage = static_cast<StageActor *>(_pendingParent);
 		parentStage->addChildSpatialEntity(this);
 		_pendingParent = nullptr;
diff --git a/engines/mediastation/graphics.cpp b/engines/mediastation/graphics.cpp
index fb55ddb9cd7..06c5c43ed97 100644
--- a/engines/mediastation/graphics.cpp
+++ b/engines/mediastation/graphics.cpp
@@ -696,39 +696,21 @@ void VideoDisplayManager::_colorShiftCurrentPalette(uint startIndex, uint shiftA
 }
 
 void VideoDisplayManager::_fadeToPaletteObject(uint paletteId, double fadeTime, uint startIndex, uint colorCount) {
-	Actor *actor = _vm->getActorById(paletteId);
-	if (actor == nullptr) {
-		error("%s: Got null target palette", __func__);
-	} else if (actor->type() != kActorTypePalette) {
-		error("%s: Actor %d is not a palette", __func__, paletteId);
-	}
-
-	Graphics::Palette *palette = static_cast<PaletteActor *>(actor)->_palette;
+	PaletteActor *paletteActor = static_cast<PaletteActor *>(_vm->getActorByIdAndType(paletteId, kActorTypePalette));
+	Graphics::Palette *palette = paletteActor->_palette;
 	_fadeToPalette(fadeTime, *palette, startIndex, colorCount);
 }
 
 void VideoDisplayManager::_setToPaletteObject(uint paletteId, uint startIndex, uint colorCount) {
-	Actor *actor = _vm->getActorById(paletteId);
-	if (actor == nullptr) {
-		error("%s: Got null target palette", __func__);
-	} else if (actor->type() != kActorTypePalette) {
-		error("%s: Actor %d is not a palette", __func__, paletteId);
-	}
-
-	Graphics::Palette *palette = static_cast<PaletteActor *>(actor)->_palette;
+	PaletteActor *paletteActor = static_cast<PaletteActor *>(_vm->getActorByIdAndType(paletteId, kActorTypePalette));
+	Graphics::Palette *palette = paletteActor->_palette;
 	_setPalette(*palette, startIndex, colorCount);
 }
 
 void VideoDisplayManager::_setPercentToPaletteObject(double percent, uint paletteId, uint startIndex, uint colorCount) {
-	Actor *actor = _vm->getActorById(paletteId);
-	if (actor == nullptr) {
-		error("%s: Got null target palette", __func__);
-	} else if (actor->type() != kActorTypePalette) {
-		error("%s: Actor %d is not a palette", __func__, paletteId);
-	}
-
-	Graphics::Palette *targetPalette = static_cast<PaletteActor *>(actor)->_palette;
-	_setToPercentPalette(percent, *_registeredPalette, *targetPalette, startIndex, colorCount);
+	PaletteActor *paletteActor = static_cast<PaletteActor *>(_vm->getActorByIdAndType(paletteId, kActorTypePalette));
+	Graphics::Palette *palette = paletteActor->_palette;
+	_setToPercentPalette(percent, *_registeredPalette, *palette, startIndex, colorCount);
 }
 
 void VideoDisplayManager::imageBlit(
diff --git a/engines/mediastation/mediastation.cpp b/engines/mediastation/mediastation.cpp
index 7ec07977235..5d9d60ac9f0 100644
--- a/engines/mediastation/mediastation.cpp
+++ b/engines/mediastation/mediastation.cpp
@@ -102,11 +102,21 @@ Actor *MediaStationEngine::getActorById(uint actorId) {
 	return _actors.getValOrDefault(actorId);
 }
 
+Actor *MediaStationEngine::getActorByIdAndType(uint actorId, ActorType expectedType) {
+	Actor *actor = getActorById(actorId);
+	if (actor == nullptr) {
+		error("[%s] %s: Actor doesn't exist", g_engine->formatActorName(actorId).c_str(), __func__);
+	} else if (actor->type() != expectedType) {
+		error("[%s] %s: Expected type %s, got %s", actor->debugName(), __func__, actorTypeToStr(actor->type()), actorTypeToStr(expectedType));
+	}
+	return actor;
+}
+
 SpatialEntity *MediaStationEngine::getSpatialEntityById(uint spatialEntityId) {
 	Actor *actor = getActorById(spatialEntityId);
 	if (actor != nullptr) {
 		if (!actor->isSpatialActor()) {
-			error("[%s] %s: Not a spatial actor", formatActorName(spatialEntityId).c_str(), __func__);
+			error("[%s] %s: Not a spatial actor", actor->debugName(), __func__);
 		}
 		return static_cast<SpatialEntity *>(actor);
 	}
@@ -309,7 +319,7 @@ void MediaStationEngine::draw(bool dirtyOnly) {
 
 void MediaStationEngine::registerActor(Actor *actorToAdd) {
 	if (getActorById(actorToAdd->id())) {
-		error("[%s] %s: Already defined in this title", formatActorName(actorToAdd).c_str(), __func__);
+		error("[%s] %s: Already defined in this title", actorToAdd->debugName(), __func__);
 	}
 	_actors.setVal(actorToAdd->id(), actorToAdd);
 }
diff --git a/engines/mediastation/mediastation.h b/engines/mediastation/mediastation.h
index f57c6c27c10..d7074215ba8 100644
--- a/engines/mediastation/mediastation.h
+++ b/engines/mediastation/mediastation.h
@@ -98,6 +98,7 @@ public:
 	void readHeaderSections(Subfile &subfile, Chunk &chunk);
 
 	Actor *getActorById(uint actorId);
+	Actor *getActorByIdAndType(uint actorId, ActorType expectedType);
 	SpatialEntity *getSpatialEntityById(uint spatialEntityId);
 	ChannelClient *getChannelClientByChannelIdent(uint channelIdent);
 	ScriptValue *getVariable(uint variableId);


Commit: 11b97406c5b6647b58f748b97fe586034c010018
    https://github.com/scummvm/scummvm/commit/11b97406c5b6647b58f748b97fe586034c010018
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-02-24T20:49:55-05:00

Commit Message:
MEDIASTATION: Issue warnings, not asserts, when script argument conditions are not met

Changed paths:
    engines/mediastation/actor.cpp
    engines/mediastation/actor.h
    engines/mediastation/actors/camera.cpp
    engines/mediastation/actors/document.cpp
    engines/mediastation/actors/hotspot.cpp
    engines/mediastation/actors/image.cpp
    engines/mediastation/actors/movie.cpp
    engines/mediastation/actors/path.cpp
    engines/mediastation/actors/sound.cpp
    engines/mediastation/actors/sprite.cpp
    engines/mediastation/actors/stage.cpp
    engines/mediastation/actors/timer.cpp
    engines/mediastation/graphics.cpp
    engines/mediastation/mediascript/collection.cpp
    engines/mediastation/mediascript/function.cpp
    engines/mediastation/mediascript/scriptvalue.cpp
    engines/mediastation/mediascript/scriptvalue.h


diff --git a/engines/mediastation/actor.cpp b/engines/mediastation/actor.cpp
index 18730db7d40..2ea0a7a6861 100644
--- a/engines/mediastation/actor.cpp
+++ b/engines/mediastation/actor.cpp
@@ -213,7 +213,7 @@ ScriptValue SpatialEntity::callMethod(BuiltInMethod methodId, Common::Array<Scri
 	ScriptValue returnValue;
 	switch (methodId) {
 	case kSpatialMoveToMethod: {
-		assert(args.size() == 2);
+		ARGCOUNTCHECK(2);
 		int16 x = static_cast<int16>(args[0].asFloat());
 		int16 y = static_cast<int16>(args[1].asFloat());
 		moveTo(x, y);
@@ -221,7 +221,7 @@ ScriptValue SpatialEntity::callMethod(BuiltInMethod methodId, Common::Array<Scri
 	}
 
 	case kSpatialMoveToByOffsetMethod: {
-		assert(args.size() == 2);
+		ARGCOUNTCHECK(2);
 		int16 dx = static_cast<int16>(args[0].asFloat());
 		int16 dy = static_cast<int16>(args[1].asFloat());
 		int16 newX = _boundingBox.left + dx;
@@ -231,14 +231,14 @@ ScriptValue SpatialEntity::callMethod(BuiltInMethod methodId, Common::Array<Scri
 	}
 
 	case kSpatialZMoveToMethod: {
-		assert(args.size() == 1);
+		ARGCOUNTCHECK(1);
 		int zIndex = static_cast<int>(args[0].asFloat());
 		setZIndex(zIndex);
 		break;
 	}
 
 	case kSpatialCenterMoveToMethod: {
-		assert(args.size() == 2);
+		ARGCOUNTCHECK(2);
 		int16 x = static_cast<int16>(args[0].asFloat());
 		int16 y = static_cast<int16>(args[1].asFloat());
 		moveToCentered(x, y);
@@ -246,58 +246,58 @@ ScriptValue SpatialEntity::callMethod(BuiltInMethod methodId, Common::Array<Scri
 	}
 
 	case kGetLeftXMethod:
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		returnValue.setToFloat(_boundingBox.left);
 		break;
 
 	case kGetTopYMethod:
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		returnValue.setToFloat(_boundingBox.top);
 		break;
 
 	case kGetWidthMethod:
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		returnValue.setToFloat(_boundingBox.width());
 		break;
 
 	case kGetHeightMethod:
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		returnValue.setToFloat(_boundingBox.height());
 		break;
 
 	case kGetCenterXMethod: {
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		int centerX = _boundingBox.left + (_boundingBox.width() / 2);
 		returnValue.setToFloat(centerX);
 		break;
 	}
 
 	case kGetCenterYMethod: {
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		int centerY = _boundingBox.top + (_boundingBox.height() / 2);
 		returnValue.setToFloat(centerY);
 		break;
 	}
 
 	case kGetZCoordinateMethod:
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		returnValue.setToFloat(_zIndex);
 		break;
 
 	case kSetDissolveFactorMethod: {
-		assert(args.size() == 1);
+		ARGCOUNTCHECK(1);
 		double dissolveFactor = args[0].asFloat();
 		setDissolveFactor(dissolveFactor);
 		break;
 	}
 
 	case kIsVisibleMethod:
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		returnValue.setToBool(isVisible());
 		break;
 
 	case kSetMousePositionMethod: {
-		assert(args.size() == 2);
+		ARGCOUNTCHECK(2);
 		int16 x = static_cast<int16>(args[0].asFloat());
 		int16 y = static_cast<int16>(args[1].asFloat());
 		setMousePosition(x, y);
@@ -306,31 +306,31 @@ ScriptValue SpatialEntity::callMethod(BuiltInMethod methodId, Common::Array<Scri
 
 	case kGetXScaleMethod1:
 	case kGetXScaleMethod2:
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		returnValue.setToFloat(_scaleX);
 		break;
 
 	case kSetScaleMethod:
-		assert(args.size() == 1);
+		ARGCOUNTCHECK(1);
 		invalidateLocalBounds();
 		_scaleX = _scaleY = args[0].asFloat();
 		invalidateLocalBounds();
 		break;
 
 	case kSetXScaleMethod:
-		assert(args.size() == 1);
+		ARGCOUNTCHECK(1);
 		invalidateLocalBounds();
 		_scaleX = args[0].asFloat();
 		invalidateLocalBounds();
 		break;
 
 	case kGetYScaleMethod:
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		returnValue.setToFloat(_scaleY);
 		break;
 
 	case kSetYScaleMethod:
-		assert(args.size() == 1);
+		ARGCOUNTCHECK(1);
 		invalidateLocalBounds();
 		_scaleY = args[0].asFloat();
 		invalidateLocalBounds();
diff --git a/engines/mediastation/actor.h b/engines/mediastation/actor.h
index 736ca22987b..781d068900d 100644
--- a/engines/mediastation/actor.h
+++ b/engines/mediastation/actor.h
@@ -163,6 +163,25 @@ enum MouseEventFlag {
 	// There is no key up event.
 };
 
+// Argument count validation macros for built-in script methods.
+// For exact argument count.
+#define ARGCOUNTCHECK(n) \
+	if (args.size() != (n)) { \
+		warning("%s: Expected %d arguments, got %d", builtInMethodToStr(methodId), (n), args.size()); \
+	}
+
+// For a range of valid argument counts (min to max).
+#define ARGCOUNTRANGE(min, max) \
+	if (args.size() < (size_t)(min) || args.size() > (max)) { \
+		warning("%s: Expected %d to %d arguments, got %d", builtInMethodToStr(methodId), (min), (max), args.size()); \
+	}
+
+// For minimum argument count (no maximum).
+#define ARGCOUNTMIN(min) \
+	if (args.size() < (min)) { \
+		warning("%s: Expected at least %d arguments, got %d", builtInMethodToStr(methodId), (min), args.size()); \
+	}
+
 class Actor {
 public:
 	Actor(ActorType type) : _type(type) {};
diff --git a/engines/mediastation/actors/camera.cpp b/engines/mediastation/actors/camera.cpp
index 1dac8fc3333..94bd895d025 100644
--- a/engines/mediastation/actors/camera.cpp
+++ b/engines/mediastation/actors/camera.cpp
@@ -95,7 +95,7 @@ ScriptValue CameraActor::callMethod(BuiltInMethod methodId, Common::Array<Script
 		break;
 
 	case kAddToStageMethod:
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		addToStage();
 		break;
 
@@ -109,12 +109,12 @@ ScriptValue CameraActor::callMethod(BuiltInMethod methodId, Common::Array<Script
 	}
 
 	case kAddedToStageMethod:
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		returnValue.setToBool(_addedToStage);
 		break;
 
 	case kStartPanMethod: {
-		assert(args.size() == 3);
+		ARGCOUNTCHECK(3);
 		int16 deltaX = static_cast<uint16>(args[0].asFloat());
 		int16 deltaY = static_cast<int16>(args[1].asFloat());
 		double duration = args[2].asTime();
@@ -125,17 +125,17 @@ ScriptValue CameraActor::callMethod(BuiltInMethod methodId, Common::Array<Script
 	}
 
 	case kStopPanMethod:
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		stopPan();
 		break;
 
 	case kIsPanningMethod:
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		returnValue.setToBool(_panState);
 		break;
 
 	case kViewportMoveToMethod: {
-		assert(args.size() == 2);
+		ARGCOUNTCHECK(2);
 		int16 x = static_cast<int16>(args[0].asFloat());
 		int16 y = static_cast<int16>(args[1].asFloat());
 		_nextViewportOrigin = Common::Point(x, y);
@@ -151,7 +151,7 @@ ScriptValue CameraActor::callMethod(BuiltInMethod methodId, Common::Array<Script
 	}
 
 	case kAdjustCameraViewportMethod: {
-		assert(args.size() == 2);
+		ARGCOUNTCHECK(2);
 		int16 xDiff = static_cast<int16>(args[0].asFloat());
 		int16 yDiff = static_cast<int16>(args[1].asFloat());
 		Common::Point viewportDelta(xDiff, yDiff);
@@ -169,7 +169,7 @@ ScriptValue CameraActor::callMethod(BuiltInMethod methodId, Common::Array<Script
 	}
 
 	case kAdjustCameraViewportSpatialCenterMethod: {
-		assert(args.size() == 2);
+		ARGCOUNTCHECK(2);
 		int16 xDiff = static_cast<int16>(args[0].asFloat());
 		int16 yDiff = static_cast<int16>(args[1].asFloat());
 
@@ -192,7 +192,7 @@ ScriptValue CameraActor::callMethod(BuiltInMethod methodId, Common::Array<Script
 	}
 
 	case kSetCameraBoundsMethod: {
-		assert(args.size() == 2);
+		ARGCOUNTCHECK(2);
 		int16 width = static_cast<int16>(args[0].asFloat());
 		int16 height = static_cast<int16>(args[1].asFloat());
 		Common::Rect newBounds(_originalBoundingBox.origin(), width, height);
@@ -206,17 +206,17 @@ ScriptValue CameraActor::callMethod(BuiltInMethod methodId, Common::Array<Script
 	}
 
 	case kXViewportPositionMethod:
-		assert(args.size() == 0);
+		ARGCOUNTCHECK(0);
 		returnValue.setToFloat(getViewportOrigin().x);
 		break;
 
 	case kYViewportPositionMethod:
-		assert(args.size() == 0);
+		ARGCOUNTCHECK(0);
 		returnValue.setToFloat(getViewportOrigin().y);
 		break;
 
 	case kPanToMethod: {
-		assert(args.size() >= 3);
+		ARGCOUNTRANGE(3, 4);
 		int16 x = static_cast<int16>(args[0].asFloat());
 		int16 y = static_cast<int16>(args[1].asFloat());
 
@@ -224,11 +224,9 @@ ScriptValue CameraActor::callMethod(BuiltInMethod methodId, Common::Array<Script
 			uint panSteps = static_cast<uint>(args[2].asFloat());
 			double duration = args[3].asFloat();
 			panToByStepCount(x, y, panSteps, duration);
-		} else if (args.size() == 3) {
+		} else {
 			double duration = args[2].asFloat();
 			panToByTime(x, y, duration);
-		} else {
-			error("%s: Incorrect number of args for method %s", __func__, builtInMethodToStr(methodId));
 		}
 		break;
 	}
diff --git a/engines/mediastation/actors/document.cpp b/engines/mediastation/actors/document.cpp
index ec58e48934f..2e66ee9f409 100644
--- a/engines/mediastation/actors/document.cpp
+++ b/engines/mediastation/actors/document.cpp
@@ -30,15 +30,17 @@ ScriptValue DocumentActor::callMethod(BuiltInMethod methodId, Common::Array<Scri
 	ScriptValue returnValue;
 	switch (methodId) {
 	case kDocumentBranchToScreenMethod:
+		ARGCOUNTMIN(1);
 		processBranch(args);
 		break;
 
 	case kDocumentQuitMethod:
+		ARGCOUNTCHECK(0);
 		g_engine->quitGame();
 		break;
 
 	case kDocumentContextLoadInProgressMethod: {
-		assert(args.size() == 1);
+		ARGCOUNTCHECK(1);
 		uint contextId = args[0].asActorId();
 		bool isLoading = g_engine->getDocument()->isContextLoadInProgress(contextId);
 		returnValue.setToBool(isLoading);
@@ -47,28 +49,28 @@ ScriptValue DocumentActor::callMethod(BuiltInMethod methodId, Common::Array<Scri
 
 	case kDocumentSetMultipleStreamsMethod:
 	case kDocumentSetMultipleSoundsMethod: {
-		assert(args.size() == 1);
+		ARGCOUNTCHECK(1);
 		bool value = args[0].asBool();
 		warning("[%s] %s: STUB: %s: %d", debugName(), __func__, builtInMethodToStr(methodId), value);
 		break;
 	}
 
 	case kDocumentLoadContextMethod: {
-		assert(args.size() == 1);
+		ARGCOUNTCHECK(1);
 		uint contextId = args[0].asActorId();
 		g_engine->getDocument()->startContextLoad(contextId);
 		break;
 	}
 
 	case kDocumentReleaseContextMethod: {
-		assert(args.size() == 1);
+		ARGCOUNTCHECK(1);
 		uint contextId = args[0].asActorId();
 		g_engine->getDocument()->scheduleContextRelease(contextId);
 		break;
 	}
 
 	case kDocumentContextIsLoadedMethod: {
-		assert(args.size() == 1);
+		ARGCOUNTCHECK(1);
 		uint contextId = args[0].asActorId();
 
 		// We are looking for the screen actor with the same ID as the context.
@@ -86,7 +88,6 @@ ScriptValue DocumentActor::callMethod(BuiltInMethod methodId, Common::Array<Scri
 }
 
 void DocumentActor::processBranch(Common::Array<ScriptValue> &args) {
-	assert(args.size() >= 1);
 	uint contextId = args[0].asActorId();
 	if (args.size() > 1) {
 		bool disableUpdates = static_cast<bool>(args[1].asParamToken());
diff --git a/engines/mediastation/actors/hotspot.cpp b/engines/mediastation/actors/hotspot.cpp
index 808f186ef1c..cccaabe2643 100644
--- a/engines/mediastation/actors/hotspot.cpp
+++ b/engines/mediastation/actors/hotspot.cpp
@@ -96,30 +96,32 @@ ScriptValue HotspotActor::callMethod(BuiltInMethod methodId, Common::Array<Scrip
 
 	switch (methodId) {
 	case kMouseActivateMethod: {
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		activate();
 		return returnValue;
 	}
 
 	case kMouseDeactivateMethod: {
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		deactivate();
 		return returnValue;
 	}
 
 	case kIsActiveMethod: {
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		returnValue.setToBool(_isActive);
 		return returnValue;
 	}
 
 	case kTriggerAbsXPositionMethod: {
+		ARGCOUNTCHECK(0);
 		double mouseX = static_cast<double>(g_system->getEventManager()->getMousePos().x);
 		returnValue.setToFloat(mouseX);
 		return returnValue;
 	}
 
 	case kTriggerAbsYPositionMethod: {
+		ARGCOUNTCHECK(0);
 		double mouseY = static_cast<double>(g_system->getEventManager()->getMousePos().y);
 		returnValue.setToFloat(mouseY);
 		return returnValue;
diff --git a/engines/mediastation/actors/image.cpp b/engines/mediastation/actors/image.cpp
index 637d294193c..d7dcccbcb95 100644
--- a/engines/mediastation/actors/image.cpp
+++ b/engines/mediastation/actors/image.cpp
@@ -74,13 +74,13 @@ ScriptValue ImageActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptV
 	ScriptValue returnValue;
 	switch (methodId) {
 	case kSpatialShowMethod: {
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		spatialShow();
 		return returnValue;
 	}
 
 	case kSpatialHideMethod: {
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		spatialHide();
 		return returnValue;
 	}
diff --git a/engines/mediastation/actors/movie.cpp b/engines/mediastation/actors/movie.cpp
index 3514702a552..92c7967d22f 100644
--- a/engines/mediastation/actors/movie.cpp
+++ b/engines/mediastation/actors/movie.cpp
@@ -151,45 +151,45 @@ ScriptValue StreamMovieActor::callMethod(BuiltInMethod methodId, Common::Array<S
 
 	switch (methodId) {
 	case kTimePlayMethod: {
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		timePlay();
 		return returnValue;
 	}
 
 	case kSpatialShowMethod: {
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		setVisibility(true);
 		updateFrameState();
 		return returnValue;
 	}
 
 	case kTimeStopMethod: {
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		timeStop();
 		return returnValue;
 	}
 
 	case kSpatialHideMethod: {
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		setVisibility(false);
 		return returnValue;
 	}
 
 	case kIsPlayingMethod: {
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		returnValue.setToBool(_isPlaying);
 		return returnValue;
 	}
 
 	case kGetLeftXMethod: {
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		double left = static_cast<double>(_boundingBox.left);
 		returnValue.setToFloat(left);
 		return returnValue;
 	}
 
 	case kGetTopYMethod: {
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		double top = static_cast<double>(_boundingBox.top);
 		returnValue.setToFloat(top);
 		return returnValue;
diff --git a/engines/mediastation/actors/path.cpp b/engines/mediastation/actors/path.cpp
index af8db5da1cc..49954b1550e 100644
--- a/engines/mediastation/actors/path.cpp
+++ b/engines/mediastation/actors/path.cpp
@@ -63,26 +63,26 @@ ScriptValue PathActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptVa
 
 	switch (methodId) {
 	case kTimePlayMethod: {
-		assert(args.size() == 0);
+		ARGCOUNTCHECK(0);
 		timePlay();
 		return returnValue;
 	}
 
 	case kSetDurationMethod: {
-		assert(args.size() == 1);
+		ARGCOUNTCHECK(1);
 		uint durationInMilliseconds = static_cast<uint>(args[0].asTime() * 1000);
 		setDuration(durationInMilliseconds);
 		return returnValue;
 	}
 
 	case kPercentCompleteMethod: {
-		assert(args.size() == 0);
+		ARGCOUNTCHECK(0);
 		returnValue.setToFloat(percentComplete());
 		return returnValue;
 	}
 
 	case kIsPlayingMethod: {
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		returnValue.setToBool(_isPlaying);
 		return returnValue;
 	}
diff --git a/engines/mediastation/actors/sound.cpp b/engines/mediastation/actors/sound.cpp
index 5b2505c967e..6b5bded9ec8 100644
--- a/engines/mediastation/actors/sound.cpp
+++ b/engines/mediastation/actors/sound.cpp
@@ -94,17 +94,17 @@ ScriptValue SoundActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptV
 		// timer_6c06_AnsweringMachine, which calls SpatialShow on a sound.
 		// Since the engine is currently flagging errors on unimplemented
 		// methods for easier debugging, a no-op is used here to avoid the error.
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		return returnValue;
 
 	case kTimePlayMethod: {
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		timePlay();
 		return returnValue;
 	}
 
 	case kTimeStopMethod: {
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		timeStop();
 		return returnValue;
 	}
diff --git a/engines/mediastation/actors/sprite.cpp b/engines/mediastation/actors/sprite.cpp
index 868e4fe2a7c..b56d378ef77 100644
--- a/engines/mediastation/actors/sprite.cpp
+++ b/engines/mediastation/actors/sprite.cpp
@@ -143,37 +143,37 @@ ScriptValue SpriteMovieActor::callMethod(BuiltInMethod methodId, Common::Array<S
 
 	switch (methodId) {
 	case kSpatialShowMethod: {
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		setVisibility(true);
 		return returnValue;
 	}
 
 	case kSpatialHideMethod: {
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		setVisibility(false);
 		return returnValue;
 	}
 
 	case kTimePlayMethod: {
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		play();
 		return returnValue;
 	}
 
 	case kTimeStopMethod: {
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		stop();
 		return returnValue;
 	}
 
 	case kMovieResetMethod: {
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		setCurrentFrameToInitial();
 		return returnValue;
 	}
 
 	case kSetCurrentClipMethod: {
-		assert(args.size() <= 1);
+		ARGCOUNTRANGE(0, 1);
 		uint clipId = DEFAULT_FORWARD_CLIP_ID;
 		if (args.size() == 1) {
 			clipId = args[0].asParamToken();
@@ -183,7 +183,7 @@ ScriptValue SpriteMovieActor::callMethod(BuiltInMethod methodId, Common::Array<S
 	}
 
 	case kIncrementFrameMethod: {
-		assert(args.size() <= 1);
+		ARGCOUNTRANGE(0, 1);
 		bool loopAround = false;
 		if (args.size() == 1) {
 			loopAround = args[0].asBool();
diff --git a/engines/mediastation/actors/stage.cpp b/engines/mediastation/actors/stage.cpp
index 4c786877ffa..ca7193cafa0 100644
--- a/engines/mediastation/actors/stage.cpp
+++ b/engines/mediastation/actors/stage.cpp
@@ -318,21 +318,21 @@ ScriptValue StageActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptV
 	ScriptValue returnValue;
 	switch (methodId) {
 	case kAddActorToStageMethod: {
-		assert(args.size() == 1);
+		ARGCOUNTCHECK(1);
 		uint actorId = args[0].asActorId();
 		addActorToStage(actorId);
 		return returnValue;
 	}
 
 	case kRemoveActorFromStageMethod: {
-		assert(args.size() == 1);
+		ARGCOUNTCHECK(1);
 		uint actorId = args[0].asActorId();
 		removeActorFromStage(actorId);
 		return returnValue;
 	}
 
 	case kSetBoundsMethod: {
-		assert(args.size() == 4);
+		ARGCOUNTCHECK(4);
 		int16 x = static_cast<int16>(args[0].asFloat());
 		int16 y = static_cast<int16>(args[1].asFloat());
 		int16 width = static_cast<int16>(args[2].asFloat());
@@ -343,16 +343,18 @@ ScriptValue StageActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptV
 	}
 
 	case kStageSetSizeMethod:
-		assert(args.size() == 2);
+		ARGCOUNTCHECK(2);
 		_boundingBox.setWidth(static_cast<int16>(args[0].asFloat()));
 		_boundingBox.setHeight(static_cast<int16>(args[1].asFloat()));
 		return returnValue;
 
 	case kStageGetWidthMethod:
+		ARGCOUNTCHECK(0);
 		returnValue.setToFloat(_boundingBox.width());
 		return returnValue;
 
 	case kStageGetHeightMethod:
+		ARGCOUNTCHECK(0);
 		returnValue.setToFloat(_boundingBox.height());
 		return returnValue;
 
diff --git a/engines/mediastation/actors/timer.cpp b/engines/mediastation/actors/timer.cpp
index 8119a458c22..9493ab43efa 100644
--- a/engines/mediastation/actors/timer.cpp
+++ b/engines/mediastation/actors/timer.cpp
@@ -31,19 +31,19 @@ ScriptValue TimerActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptV
 
 	switch (methodId) {
 	case kTimePlayMethod: {
-		assert(args.size() == 0);
+		ARGCOUNTCHECK(0);
 		timePlay();
 		return returnValue;
 	}
 
 	case kTimeStopMethod: {
-		assert(args.size() == 0);
+		ARGCOUNTCHECK(0);
 		timeStop();
 		return returnValue;
 	}
 
 	case kIsPlayingMethod: {
-		assert(args.size() == 0);
+		ARGCOUNTCHECK(0);
 		returnValue.setToBool(_isPlaying);
 		return returnValue;
 	}
diff --git a/engines/mediastation/graphics.cpp b/engines/mediastation/graphics.cpp
index 06c5c43ed97..c4eb654f00b 100644
--- a/engines/mediastation/graphics.cpp
+++ b/engines/mediastation/graphics.cpp
@@ -264,11 +264,6 @@ void VideoDisplayManager::readAndRegisterPalette(Chunk &chunk) {
 }
 
 void VideoDisplayManager::effectTransition(Common::Array<ScriptValue> &args) {
-	if (args.empty()) {
-		warning("%s: Script args cannot be empty", __func__);
-		return;
-	}
-
 	TransitionType transitionType = static_cast<TransitionType>(args[0].asParamToken());
 	switch (transitionType) {
 	case kTransitionFadeToBlack:
@@ -426,7 +421,8 @@ void VideoDisplayManager::fadeToColor(Common::Array<ScriptValue> &args) {
 
 void VideoDisplayManager::setToColor(Common::Array<ScriptValue> &args) {
 	if (args.size() < 6) {
-		error("%s: Too few script args", __func__);
+		warning("%s: Too few script args", __func__);
+		return;
 	}
 
 	byte r = static_cast<byte>(args[1].asFloat());
@@ -442,7 +438,8 @@ void VideoDisplayManager::setToColor(Common::Array<ScriptValue> &args) {
 
 void VideoDisplayManager::setToPercentOfPalette(Common::Array<ScriptValue> &args) {
 	if (args.size() < 7) {
-		error("%s: Too few script args", __func__);
+		warning("%s: Too few script args", __func__);
+		return;
 	}
 
 	double percent = args[1].asFloat();
@@ -513,7 +510,7 @@ void VideoDisplayManager::setToPercentOfPaletteObject(Common::Array<ScriptValue>
 		percent = args[1].asFloat();
 		paletteId = args[2].asActorId();
 	} else {
-		error("%s: Too few script args", __func__);
+		warning("%s: Too few script args", __func__);
 		return;
 	}
 	if (args.size() >= 5) {
diff --git a/engines/mediastation/mediascript/collection.cpp b/engines/mediastation/mediascript/collection.cpp
index 195a84f68f5..4c38285ab14 100644
--- a/engines/mediastation/mediascript/collection.cpp
+++ b/engines/mediastation/mediascript/collection.cpp
@@ -27,9 +27,9 @@
 
 namespace MediaStation {
 
-ScriptValue Collection::callMethod(BuiltInMethod method, Common::Array<ScriptValue> &args) {
+ScriptValue Collection::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
 	ScriptValue returnValue;
-	switch (method) {
+	switch (methodId) {
 	case kAppendMethod:
 		for (ScriptValue value : args) {
 			push_back(value);
@@ -41,12 +41,12 @@ ScriptValue Collection::callMethod(BuiltInMethod method, Common::Array<ScriptVal
 		break;
 
 	case kCountMethod:
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		returnValue.setToFloat(size());
 		break;
 
 	case kDeleteFirstMethod:
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		if (size() > 0) {
 			returnValue = remove_at(0);
 			debugC(7, kDebugScript, "%s: %s", __func__, returnValue.getDebugString().c_str());
@@ -56,7 +56,7 @@ ScriptValue Collection::callMethod(BuiltInMethod method, Common::Array<ScriptVal
 		break;
 
 	case kDeleteLastMethod:
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		if (size() > 0) {
 			returnValue = remove_at(size() - 1);
 			debugC(7, kDebugScript, "%s: %s", __func__, returnValue.getDebugString().c_str());
@@ -66,12 +66,12 @@ ScriptValue Collection::callMethod(BuiltInMethod method, Common::Array<ScriptVal
 		break;
 
 	case kEmptyMethod:
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		clear();
 		break;
 
 	case kGetAtMethod: {
-		assert(args.size() == 1);
+		ARGCOUNTCHECK(1);
 		uint index = static_cast<uint>(args[0].asFloat());
 		if (index < size()) {
 			returnValue = operator[](index);
@@ -82,28 +82,29 @@ ScriptValue Collection::callMethod(BuiltInMethod method, Common::Array<ScriptVal
 	}
 
 	case kIsEmptyMethod:
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		returnValue.setToBool(empty());
 		break;
 
 	case kJumbleMethod:
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		jumble();
 		break;
 
 	case kSeekMethod: {
-		assert(args.size() == 1);
+		ARGCOUNTCHECK(1);
 		int index = seek(args[0]);
 		returnValue.setToFloat(index);
 		break;
 	}
 
 	case kSendMethod:
+		ARGCOUNTMIN(1);
 		send(args);
 		break;
 
 	case kDeleteAtMethod: {
-		assert(args.size() == 1);
+		ARGCOUNTCHECK(1);
 		uint index = static_cast<uint>(args[0].asFloat());
 		if (index < size()) {
 			returnValue = remove_at(index);
@@ -115,7 +116,7 @@ ScriptValue Collection::callMethod(BuiltInMethod method, Common::Array<ScriptVal
 	}
 
 	case kInsertAtMethod: {
-		assert(args.size() == 2);
+		ARGCOUNTCHECK(2);
 		uint index = static_cast<uint>(args[1].asFloat());
 		if (index <= size()) {
 			insert_at(index, args[0]);
@@ -126,7 +127,7 @@ ScriptValue Collection::callMethod(BuiltInMethod method, Common::Array<ScriptVal
 	}
 
 	case kReplaceAtMethod: {
-		assert(args.size() == 2);
+		ARGCOUNTCHECK(2);
 		uint index = static_cast<uint>(args[1].asFloat());
 		if (index < size()) {
 			operator[](index) = args[0];
@@ -137,16 +138,17 @@ ScriptValue Collection::callMethod(BuiltInMethod method, Common::Array<ScriptVal
 	}
 
 	case kPrependListMethod:
+		ARGCOUNTMIN(1);
 		insert_at(0, args);
 		break;
 
 	case kSortMethod:
-		assert(args.empty());
+		ARGCOUNTCHECK(0);
 		Common::sort(begin(), end());
 		break;
 
 	default:
-		error("%s: Attempt to call unimplemented method %s (%d)", __func__, builtInMethodToStr(method), static_cast<uint>(method));
+		error("%s: Attempt to call unimplemented method %s (%d)", __func__, builtInMethodToStr(methodId), static_cast<uint>(methodId));
 	}
 	return returnValue;
 }
diff --git a/engines/mediastation/mediascript/function.cpp b/engines/mediastation/mediascript/function.cpp
index 490b968e978..76fff863a4c 100644
--- a/engines/mediastation/mediascript/function.cpp
+++ b/engines/mediastation/mediascript/function.cpp
@@ -24,6 +24,25 @@
 #include "mediastation/mediastation.h"
 
 namespace MediaStation {
+
+// For exact argument count.
+#define FUNCARGCHECK(n) \
+	if (args.size() != (n)) { \
+		warning("%s: expected %d argument%s, got %d", builtInFunctionToStr(static_cast<BuiltInFunction>(functionId)), (n), ((n) == 1 ? "" : "s"), args.size()); \
+	}
+
+// For a range of valid argument counts (min to max).
+#define FUNCARGRANGE(min, max) \
+	if (args.size() < (min) || args.size() > (max)) { \
+		warning("%s: expected %d to %d argument, got %d", builtInFunctionToStr(static_cast<BuiltInFunction>(functionId)), (min), (max), args.size()); \
+	}
+
+// For minimum argument count (no maximum).
+#define FUNCARGMIN(min) \
+	if (args.size() < (min)) { \
+		warning("%s: expected at least %d argument%s, got %d", builtInFunctionToStr(static_cast<BuiltInFunction>(functionId)), (min), ((min) == 1 ? "" : "s"), args.size()); \
+	}
+
 ScriptFunction::ScriptFunction(Chunk &chunk) {
 	_contextId = chunk.readTypedUint16();
 	_id = chunk.readTypedUint16();
@@ -84,12 +103,13 @@ ScriptValue FunctionManager::call(uint functionId, Common::Array<ScriptValue> &a
 	switch (functionId) {
 	case kRandomFunction:
 	case kLegacy_RandomFunction:
-		assert(args.size() == 2);
+		FUNCARGCHECK(2);
 		script_Random(args, returnValue);
 		break;
 
 	case kTimeOfDayFunction:
 	case kLegacy_TimeOfDayFunction:
+		FUNCARGCHECK(0);
 		script_TimeOfDay(args, returnValue);
 		break;
 
@@ -105,45 +125,49 @@ ScriptValue FunctionManager::call(uint functionId, Common::Array<ScriptValue> &a
 
 	case kPlatformFunction:
 	case kLegacy_PlatformFunction:
-		assert(args.empty());
+		FUNCARGCHECK(0);
 		script_GetPlatform(args, returnValue);
 		break;
 
 	case kSquareRootFunction:
 	case kLegacy_SquareRootFunction:
-		assert(args.size() == 1);
+		FUNCARGCHECK(1);
 		script_SquareRoot(args, returnValue);
 		break;
 
 	case kGetUniqueRandomFunction:
 	case kLegacy_GetUniqueRandomFunction:
-		assert(args.size() >= 2);
+		FUNCARGMIN(2);
 		script_GetUniqueRandom(args, returnValue);
 		break;
 
 	case kCurrentRunTimeFunction:
+		FUNCARGCHECK(0);
 		script_CurrentRunTime(args, returnValue);
 		break;
 
 	case kSetGammaCorrectionFunction:
+		FUNCARGRANGE(1, 3);
 		script_SetGammaCorrection(args, returnValue);
 		break;
 
 	case kGetDefaultGammaCorrectionFunction:
+		FUNCARGCHECK(0);
 		script_GetDefaultGammaCorrection(args, returnValue);
 		break;
 
 	case kGetCurrentGammaCorrectionFunction:
+		FUNCARGCHECK(0);
 		script_GetCurrentGammaCorrection(args, returnValue);
 		break;
 
 	case kSetAudioVolumeFunction:
-		assert(args.size() == 1);
+		FUNCARGCHECK(1);
 		script_SetAudioVolume(args, returnValue);
 		break;
 
 	case kGetAudioVolumeFunction:
-		assert(args.empty());
+		FUNCARGCHECK(0);
 		script_GetAudioVolume(args, returnValue);
 		break;
 
@@ -189,6 +213,7 @@ ScriptValue FunctionManager::call(uint functionId, Common::Array<ScriptValue> &a
 		break;
 
 	case kLegacy_DebugPrintFunction:
+		// We don't need to check arg counts here. This just prints however many args we have.
 		script_DebugPrint(args, returnValue);
 		break;
 
@@ -371,11 +396,6 @@ void FunctionManager::script_CurrentRunTime(Common::Array<ScriptValue> &args, Sc
 }
 
 void FunctionManager::script_SetGammaCorrection(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
-	if (args.size() != 1 && args.size() != 3) {
-		warning("%s: Expected 1 or 3 arguments, got %u", __func__, args.size());
-		return;
-	}
-
 	double red = 1.0;
 	double green = 1.0;
 	double blue = 1.0;
diff --git a/engines/mediastation/mediascript/scriptvalue.cpp b/engines/mediastation/mediascript/scriptvalue.cpp
index 283814e5eaf..1f5010af0d5 100644
--- a/engines/mediastation/mediascript/scriptvalue.cpp
+++ b/engines/mediastation/mediascript/scriptvalue.cpp
@@ -23,6 +23,8 @@
 #include "mediastation/mediascript/function.h"
 #include "mediastation/mediastation.h"
 
+#define VALUETYPEMISMATCH(expectedType) warning("%s: Script value type mismatch: Expected %s, got %s", __func__, scriptValueTypeToStr(expectedType), scriptValueTypeToStr(_type))
+
 namespace MediaStation {
 
 ScriptValue::ScriptValue(ParameterReadStream *stream) {
@@ -117,7 +119,7 @@ double ScriptValue::asFloat() const {
 	if (_type == kScriptValueTypeFloat) {
 		return _u.d;
 	} else {
-		issueValueMismatchWarning(kScriptValueTypeFloat);
+		VALUETYPEMISMATCH(kScriptValueTypeFloat);
 		return 0.0;
 	}
 }
@@ -131,7 +133,7 @@ bool ScriptValue::asBool() const {
 	if (_type == kScriptValueTypeBool) {
 		return _u.b;
 	} else {
-		issueValueMismatchWarning(kScriptValueTypeBool);
+		VALUETYPEMISMATCH(kScriptValueTypeBool);
 		return false;
 	}
 }
@@ -145,7 +147,7 @@ double ScriptValue::asTime() const {
 	if (_type == kScriptValueTypeTime) {
 		return _u.d;
 	} else {
-		issueValueMismatchWarning(kScriptValueTypeTime);
+		VALUETYPEMISMATCH(kScriptValueTypeTime);
 		return 0.0;
 	}
 }
@@ -159,7 +161,7 @@ uint ScriptValue::asParamToken() const {
 	if (_type == kScriptValueTypeParamToken) {
 		return _u.paramToken;
 	} else {
-		issueValueMismatchWarning(kScriptValueTypeParamToken);
+		VALUETYPEMISMATCH(kScriptValueTypeParamToken);
 		return 0;
 	}
 }
@@ -173,7 +175,7 @@ uint ScriptValue::asActorId() const {
 	if (_type == kScriptValueTypeActorId) {
 		return _u.actorId;
 	} else {
-		issueValueMismatchWarning(kScriptValueTypeActorId);
+		VALUETYPEMISMATCH(kScriptValueTypeActorId);
 		return 0;
 	}
 }
@@ -200,7 +202,7 @@ Common::SharedPtr<Collection> ScriptValue::asCollection() const {
 	if (_type == kScriptValueTypeCollection) {
 		return _collection;
 	} else {
-		issueValueMismatchWarning(kScriptValueTypeCollection);
+		VALUETYPEMISMATCH(kScriptValueTypeCollection);
 		return nullptr;
 	}
 }
@@ -214,7 +216,7 @@ uint ScriptValue::asFunctionId() const {
 	if (_type == kScriptValueTypeFunctionId) {
 		return _u.functionId;
 	} else {
-		issueValueMismatchWarning(kScriptValueTypeFunctionId);
+		VALUETYPEMISMATCH(kScriptValueTypeFunctionId);
 		return 0;
 	}
 }
@@ -228,7 +230,7 @@ BuiltInMethod ScriptValue::asMethodId() const {
 	if (_type == kScriptValueTypeMethodId) {
 		return _u.methodId;
 	} else {
-		issueValueMismatchWarning(kScriptValueTypeMethodId);
+		VALUETYPEMISMATCH(kScriptValueTypeMethodId);
 		return kInvalidMethod;
 	}
 }
@@ -576,11 +578,4 @@ ScriptValue ScriptValue::operator-() const {
 	return returnValue;
 }
 
-void ScriptValue::issueValueMismatchWarning(ScriptValueType expectedType) const {
-	// The original just blithely returns 0 (or equivalent) when you call a
-	// getter for the wrong type (for instance, calling asFloat() on a bool),
-	// but for debugging purposes we'll issue a warning.
-	warning("%s: Script value type mismatch: Expected %s, got %s", __func__, scriptValueTypeToStr(expectedType), scriptValueTypeToStr(_type));
-}
-
 } // End of namespace MediaStation
diff --git a/engines/mediastation/mediascript/scriptvalue.h b/engines/mediastation/mediascript/scriptvalue.h
index 6b8fc130f24..7b4270fd88b 100644
--- a/engines/mediastation/mediascript/scriptvalue.h
+++ b/engines/mediastation/mediascript/scriptvalue.h
@@ -113,8 +113,6 @@ private:
 
 	static ScriptValue evalMathOperation(Opcode op, const ScriptValue &left, const ScriptValue &right);
 	static double binaryMathOperation(Opcode op, double left, double right);
-
-	void issueValueMismatchWarning(ScriptValueType actualType) const;
 };
 
 } // End of namespace MediaStation


Commit: 90cbd70cd36d5d1afc95e7268e7b56262e869b70
    https://github.com/scummvm/scummvm/commit/90cbd70cd36d5d1afc95e7268e7b56262e869b70
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-02-24T20:49:55-05:00

Commit Message:
MEDIASTATION: Rename BitmapHeader to ImageInfo

And Bitmap to PixMapImage. Both of these changes make the
names more accurate to those found in debug symbols.

Changed paths:
    engines/mediastation/actors/camera.cpp
    engines/mediastation/actors/font.cpp
    engines/mediastation/actors/font.h
    engines/mediastation/actors/image.cpp
    engines/mediastation/actors/image.h
    engines/mediastation/actors/movie.cpp
    engines/mediastation/actors/movie.h
    engines/mediastation/actors/sprite.cpp
    engines/mediastation/actors/sprite.h
    engines/mediastation/bitmap.cpp
    engines/mediastation/bitmap.h
    engines/mediastation/graphics.cpp
    engines/mediastation/graphics.h
    engines/mediastation/mediastation.h


diff --git a/engines/mediastation/actors/camera.cpp b/engines/mediastation/actors/camera.cpp
index 94bd895d025..4afcbbd6f7f 100644
--- a/engines/mediastation/actors/camera.cpp
+++ b/engines/mediastation/actors/camera.cpp
@@ -79,8 +79,8 @@ void CameraActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType)
 }
 
 void CameraActor::readChunk(Chunk &chunk) {
-	BitmapHeader *bitmapHeader = new BitmapHeader(chunk);
-	_image->bitmap = new Bitmap(chunk, bitmapHeader);
+	ImageInfo bitmapHeader(chunk);
+	_image->bitmap = new PixMapImage(chunk, bitmapHeader);
 }
 
 ScriptValue CameraActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
diff --git a/engines/mediastation/actors/font.cpp b/engines/mediastation/actors/font.cpp
index 0614a63dd89..72cd2f1e604 100644
--- a/engines/mediastation/actors/font.cpp
+++ b/engines/mediastation/actors/font.cpp
@@ -24,7 +24,7 @@
 
 namespace MediaStation {
 
-FontGlyph::FontGlyph(Chunk &chunk, uint asciiCode, uint unk1, uint unk2, BitmapHeader *header) : Bitmap(chunk, header) {
+FontGlyph::FontGlyph(Chunk &chunk, uint asciiCode, int unk1, int unk2, const ImageInfo &header) : PixMapImage(chunk, header) {
 	_asciiCode = asciiCode;
 	_unk1 = unk1;
 	_unk2 = unk2;
@@ -55,7 +55,7 @@ void FontActor::readChunk(Chunk &chunk) {
 	uint asciiCode = chunk.readTypedUint16();
 	int unk1 = chunk.readTypedUint16();
 	int unk2 = chunk.readTypedUint16();
-	BitmapHeader *header = new BitmapHeader(chunk);
+	ImageInfo header(chunk);
 	FontGlyph *glyph = new FontGlyph(chunk, asciiCode, unk1, unk2, header);
 	if (_glyphs.getValOrDefault(asciiCode) != nullptr) {
 		error("%s: Glyph for ASCII code 0x%x already exists", __func__, asciiCode);
diff --git a/engines/mediastation/actors/font.h b/engines/mediastation/actors/font.h
index b6b0eb69db5..654f269fca1 100644
--- a/engines/mediastation/actors/font.h
+++ b/engines/mediastation/actors/font.h
@@ -30,12 +30,10 @@
 
 namespace MediaStation {
 
-class FontGlyph : public Bitmap {
+class FontGlyph : public PixMapImage {
 public:
-	FontGlyph(Chunk &chunk, uint asciiCode, uint unk1, uint unk2, BitmapHeader *header);
+	FontGlyph(Chunk &chunk, uint asciiCode, int unk1, int unk2, const ImageInfo &header);
 	uint _asciiCode = 0;
-
-private:
 	int _unk1 = 0;
 	int _unk2 = 0;
 };
diff --git a/engines/mediastation/actors/image.cpp b/engines/mediastation/actors/image.cpp
index d7dcccbcb95..48d405fd521 100644
--- a/engines/mediastation/actors/image.cpp
+++ b/engines/mediastation/actors/image.cpp
@@ -114,8 +114,8 @@ Common::Rect ImageActor::getBbox() const {
 }
 
 void ImageActor::readChunk(Chunk &chunk) {
-	BitmapHeader *bitmapHeader = new BitmapHeader(chunk);
-	_asset->bitmap = new Bitmap(chunk, bitmapHeader);
+	ImageInfo bitmapHeader = ImageInfo(chunk);
+	_asset->bitmap = new PixMapImage(chunk, bitmapHeader);
 }
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/actors/image.h b/engines/mediastation/actors/image.h
index 7fa57a9b1e5..e005fedc9ee 100644
--- a/engines/mediastation/actors/image.h
+++ b/engines/mediastation/actors/image.h
@@ -37,7 +37,7 @@ namespace MediaStation {
 struct ImageAsset {
 	~ImageAsset();
 
-	Bitmap *bitmap = nullptr;
+	PixMapImage *bitmap = nullptr;
 };
 
 class ImageActor : public SpatialEntity, public ChannelClient {
diff --git a/engines/mediastation/actors/movie.cpp b/engines/mediastation/actors/movie.cpp
index 92c7967d22f..e417919ecb2 100644
--- a/engines/mediastation/actors/movie.cpp
+++ b/engines/mediastation/actors/movie.cpp
@@ -25,9 +25,9 @@
 
 namespace MediaStation {
 
-MovieFrameHeader::MovieFrameHeader(Chunk &chunk) : BitmapHeader(chunk) {
+MovieFrameInfo::MovieFrameInfo(Chunk &chunk) : ImageInfo(chunk) {
 	_index = chunk.readTypedUint32();
-	debugC(5, kDebugLoading, "MovieFrameHeader::MovieFrameHeader(): _index = 0x%x (@0x%llx)", _index, static_cast<long long int>(chunk.pos()));
+	debugC(5, kDebugLoading, "MovieFrameInfo::MovieFrameInfo(): _index = 0x%x (@0x%llx)", _index, static_cast<long long int>(chunk.pos()));
 	_keyframeEndInMilliseconds = chunk.readTypedUint32();
 }
 
@@ -67,13 +67,7 @@ MovieFrame::MovieFrame(Chunk &chunk) {
 	}
 }
 
-MovieFrameImage::MovieFrameImage(Chunk &chunk, MovieFrameHeader *header) : Bitmap(chunk, header) {
-	_bitmapHeader = header;
-}
-
-MovieFrameImage::~MovieFrameImage() {
-	// The base class destructor takes care of deleting the bitmap header, so
-	// we don't need to delete that here.
+MovieFrameImage::MovieFrameImage(Chunk &chunk, const MovieFrameInfo &header) : PixMapImage(chunk, header), _frameInfo(header) {
 }
 
 StreamMovieActor::~StreamMovieActor() {
@@ -458,7 +452,7 @@ void StreamMovieActor::decompressIntoAuxImage(MovieFrame *frame) {
 }
 
 void StreamMovieActorFrames::readImageData(Chunk &chunk) {
-	MovieFrameHeader *header = new MovieFrameHeader(chunk);
+	MovieFrameInfo header(chunk);
 	MovieFrameImage *frame = new MovieFrameImage(chunk, header);
 	_images.push_back(frame);
 }
diff --git a/engines/mediastation/actors/movie.h b/engines/mediastation/actors/movie.h
index 01a0e0e1131..e2ce06e3f71 100644
--- a/engines/mediastation/actors/movie.h
+++ b/engines/mediastation/actors/movie.h
@@ -39,23 +39,24 @@ enum MovieBlitType {
 	kCompressedDeltaMovieBlit = 3,
 };
 
-class MovieFrameHeader : public BitmapHeader {
+class MovieFrameInfo : public ImageInfo {
 public:
-	MovieFrameHeader(Chunk &chunk);
+	MovieFrameInfo() = default;
+	MovieFrameInfo(Chunk &chunk);
 
 	uint _index = 0;
 	uint _keyframeEndInMilliseconds = 0;
 };
 
-class MovieFrameImage : public Bitmap {
+class MovieFrameImage : public PixMapImage {
 public:
-	MovieFrameImage(Chunk &chunk, MovieFrameHeader *header);
-	virtual ~MovieFrameImage() override;
+	MovieFrameImage(Chunk &chunk, const MovieFrameInfo &header);
+	~MovieFrameImage() = default;
 
-	uint32 index() { return _bitmapHeader->_index; }
+	uint32 index() { return _frameInfo._index; }
 
 private:
-	MovieFrameHeader *_bitmapHeader = nullptr;
+	MovieFrameInfo _frameInfo;
 };
 
 enum MovieSectionType {
diff --git a/engines/mediastation/actors/sprite.cpp b/engines/mediastation/actors/sprite.cpp
index b56d378ef77..c6f4f66cc8d 100644
--- a/engines/mediastation/actors/sprite.cpp
+++ b/engines/mediastation/actors/sprite.cpp
@@ -25,7 +25,7 @@
 
 namespace MediaStation {
 
-SpriteFrameHeader::SpriteFrameHeader(Chunk &chunk) : BitmapHeader(chunk) {
+SpriteFrameInfo::SpriteFrameInfo(Chunk &chunk) : ImageInfo(chunk) {
 	_index = chunk.readTypedUint16();
 	_offset = chunk.readTypedPoint();
 }
@@ -34,20 +34,15 @@ Common::String SpriteMovieClip::getDebugString() const {
 	return Common::String::format("%s: [%d, %d]", g_engine->formatParamTokenName(id).c_str(), firstFrameIndex, lastFrameIndex);
 }
 
-SpriteFrame::SpriteFrame(Chunk &chunk, SpriteFrameHeader *header) : Bitmap(chunk, header) {
-	_bitmapHeader = header;
-}
-
-SpriteFrame::~SpriteFrame() {
-	// The base class destructor takes care of deleting the bitmap header.
+SpriteFrame::SpriteFrame(Chunk &chunk, const SpriteFrameInfo &header) : PixMapImage(chunk, header), _frameInfo(header) {
 }
 
 uint32 SpriteFrame::left() {
-	return _bitmapHeader->_offset.x;
+	return _frameInfo._offset.x;
 }
 
 uint32 SpriteFrame::top() {
-	return _bitmapHeader->_offset.y;
+	return _frameInfo._offset.y;
 }
 
 Common::Point SpriteFrame::topLeft() {
@@ -59,7 +54,7 @@ Common::Rect SpriteFrame::boundingBox() {
 }
 
 uint32 SpriteFrame::index() {
-	return _bitmapHeader->_index;
+	return _frameInfo._index;
 }
 
 SpriteAsset::~SpriteAsset() {
@@ -352,7 +347,7 @@ void SpriteMovieActor::process() {
 
 void SpriteMovieActor::readChunk(Chunk &chunk) {
 	// Reads one frame from the sprite.
-	SpriteFrameHeader *header = new SpriteFrameHeader(chunk);
+	SpriteFrameInfo header(chunk);
 	SpriteFrame *frame = new SpriteFrame(chunk, header);
 	_asset->frames.push_back(frame);
 
diff --git a/engines/mediastation/actors/sprite.h b/engines/mediastation/actors/sprite.h
index 389365298ed..c805ee32325 100644
--- a/engines/mediastation/actors/sprite.h
+++ b/engines/mediastation/actors/sprite.h
@@ -45,18 +45,18 @@ struct SpriteMovieClip {
 	Common::String getDebugString() const;
 };
 
-class SpriteFrameHeader : public BitmapHeader {
+class SpriteFrameInfo : public ImageInfo {
 public:
-	SpriteFrameHeader(Chunk &chunk);
+	SpriteFrameInfo() = default;
+	SpriteFrameInfo(Chunk &chunk);
 
 	uint _index;
 	Common::Point _offset;
 };
 
-class SpriteFrame : public Bitmap {
+class SpriteFrame : public PixMapImage {
 public:
-	SpriteFrame(Chunk &chunk, SpriteFrameHeader *header);
-	virtual ~SpriteFrame() override;
+	SpriteFrame(Chunk &chunk, const SpriteFrameInfo &frameInfo);
 
 	uint32 left();
 	uint32 top();
@@ -65,7 +65,7 @@ public:
 	uint32 index();
 
 private:
-	SpriteFrameHeader *_bitmapHeader = nullptr;
+	SpriteFrameInfo _frameInfo;
 };
 
 // The original had a separate class that did reference counting,
diff --git a/engines/mediastation/bitmap.cpp b/engines/mediastation/bitmap.cpp
index 3e1019d1f8e..e3698780862 100644
--- a/engines/mediastation/bitmap.cpp
+++ b/engines/mediastation/bitmap.cpp
@@ -24,16 +24,16 @@
 
 namespace MediaStation {
 
-BitmapHeader::BitmapHeader(Chunk &chunk) {
+ImageInfo::ImageInfo(Chunk &chunk) {
 	uint headerSizeInBytes = chunk.readTypedUint16();
 	_dimensions = chunk.readTypedGraphicSize();
 	_compressionType = static_cast<BitmapCompressionType>(chunk.readTypedUint16());
 	_stride = chunk.readTypedUint16();
-	debugC(5, kDebugLoading, "BitmapHeader::BitmapHeader(): headerSize: %d, _compressionType = 0x%x, _stride = %d",
-		headerSizeInBytes, static_cast<uint>(_compressionType), _stride);
+	debugC(5, kDebugLoading, "%s: headerSize: %d, _compressionType: 0x%x, _stride: %d",
+		__func__, headerSizeInBytes, static_cast<uint>(_compressionType), _stride);
 }
 
-Bitmap::Bitmap(Chunk &chunk, BitmapHeader *bitmapHeader) : _bitmapHeader(bitmapHeader) {
+PixMapImage::PixMapImage(Chunk &chunk, const ImageInfo &imageInfo) : _imageInfo(imageInfo) {
 	if (stride() < width()) {
 		warning("%s: Got stride less than width", __func__);
 	}
@@ -57,15 +57,12 @@ Bitmap::Bitmap(Chunk &chunk, BitmapHeader *bitmapHeader) : _bitmapHeader(bitmapH
 	}
 }
 
-Bitmap::~Bitmap() {
-	delete _bitmapHeader;
-	_bitmapHeader = nullptr;
-
+PixMapImage::~PixMapImage() {
 	delete _compressedStream;
 	_compressedStream = nullptr;
 }
 
-bool Bitmap::isCompressed() const {
+bool PixMapImage::isCompressed() const {
 	return (getCompressionType() != kUncompressedBitmap) && \
 		(getCompressionType() != kUncompressedTransparentBitmap);
 }
diff --git a/engines/mediastation/bitmap.h b/engines/mediastation/bitmap.h
index a68cf4b2db6..f3cfc7205e1 100644
--- a/engines/mediastation/bitmap.h
+++ b/engines/mediastation/bitmap.h
@@ -38,31 +38,32 @@ enum BitmapCompressionType {
 	kUncompressedTransparentBitmap = 7,
 };
 
-class BitmapHeader {
+class ImageInfo {
 public:
-	BitmapHeader(Chunk &chunk);
+	ImageInfo() = default;
+	ImageInfo(Chunk &chunk);
 
 	Common::Point _dimensions;
 	BitmapCompressionType _compressionType = kUncompressedBitmap;
 	int16 _stride = 0;
 };
 
-class Bitmap {
+class PixMapImage {
 public:
-	Bitmap(Chunk &chunk, BitmapHeader *bitmapHeader);
-	virtual ~Bitmap();
+	PixMapImage(Chunk &chunk, const ImageInfo &imageInfo);
+	virtual ~PixMapImage();
 
 	bool isCompressed() const;
-	BitmapCompressionType getCompressionType() const { return _bitmapHeader->_compressionType; }
-	int16 width() const { return _bitmapHeader->_dimensions.x; }
-	int16 height() const { return _bitmapHeader->_dimensions.y; }
-	int16 stride() const { return _bitmapHeader->_stride; }
+	BitmapCompressionType getCompressionType() const { return _imageInfo._compressionType; }
+	int16 width() const { return _imageInfo._dimensions.x; }
+	int16 height() const { return _imageInfo._dimensions.y; }
+	int16 stride() const { return _imageInfo._stride; }
 
 	Common::SeekableReadStream *_compressedStream = nullptr;
 	Graphics::ManagedSurface _image;
 
 private:
-	BitmapHeader *_bitmapHeader = nullptr;
+	ImageInfo _imageInfo;
 	uint _unk1 = 0;
 };
 
diff --git a/engines/mediastation/graphics.cpp b/engines/mediastation/graphics.cpp
index c4eb654f00b..c9d4ecb5810 100644
--- a/engines/mediastation/graphics.cpp
+++ b/engines/mediastation/graphics.cpp
@@ -712,7 +712,7 @@ void VideoDisplayManager::_setPercentToPaletteObject(double percent, uint palett
 
 void VideoDisplayManager::imageBlit(
 	Common::Point destinationPoint,
-	const Bitmap *sourceImage,
+	const PixMapImage *sourceImage,
 	double dissolveFactor,
 	DisplayContext *displayContext,
 	Graphics::ManagedSurface *targetImage) {
@@ -832,7 +832,7 @@ void VideoDisplayManager::blitRectsClip(
 void VideoDisplayManager::rleBlitRectsClip(
 	Graphics::ManagedSurface *dest,
 	const Common::Point &destLocation,
-	const Bitmap *source,
+	const PixMapImage *source,
 	const Common::Array<Common::Rect> &dirtyRegion) {
 
 	Graphics::ManagedSurface surface = decompressRle8Bitmap(source);
@@ -852,7 +852,7 @@ void VideoDisplayManager::rleBlitRectsClip(
 void VideoDisplayManager::dissolveBlitRectsClip(
 	Graphics::ManagedSurface *dest,
 	const Common::Point &destPos,
-	const Bitmap *source,
+	const PixMapImage *source,
 	const Common::Array<Common::Rect> &dirtyRegion,
 	const uint integralDissolveFactor) {
 
@@ -878,7 +878,7 @@ void VideoDisplayManager::dissolveBlit1Rect(
 	Graphics::ManagedSurface *dest,
 	const Common::Rect &areaToRedraw,
 	const Common::Point &originOnScreen,
-	const Bitmap *source,
+	const PixMapImage *source,
 	const Common::Rect &dirtyRegion,
 	const DissolvePattern &pattern) {
 
@@ -940,8 +940,8 @@ void VideoDisplayManager::dissolveBlit1Rect(
 void VideoDisplayManager::imageDeltaBlit(
 	Common::Point deltaFramePos,
 	const Common::Point &keyFrameOffset,
-	const Bitmap *deltaFrame,
-	const Bitmap *keyFrame,
+	const PixMapImage *deltaFrame,
+	const PixMapImage *keyFrame,
 	const double dissolveFactor,
 	DisplayContext *displayContext) {
 
@@ -981,8 +981,8 @@ void VideoDisplayManager::fullDeltaRleBlitRectsClip(
 	Graphics::ManagedSurface *destinationImage,
 	const Common::Point &deltaFramePos,
 	const Common::Point &keyFrameOffset,
-	const Bitmap *deltaFrame,
-	const Bitmap *keyFrame,
+	const PixMapImage *deltaFrame,
+	const PixMapImage *keyFrame,
 	const Common::Array<Common::Rect> &dirtyRegion) {
 
 	Graphics::ManagedSurface surface = decompressRle8Bitmap(deltaFrame, &keyFrame->_image, &keyFrameOffset);
@@ -1005,8 +1005,8 @@ void VideoDisplayManager::fullDeltaRleBlitRectsClip(
 void VideoDisplayManager::deltaRleBlitRectsClip(
 	Graphics::ManagedSurface *destinationImage,
 	const Common::Point &deltaFramePos,
-	const Bitmap *deltaFrame,
-	const Bitmap *keyFrame,
+	const PixMapImage *deltaFrame,
+	const PixMapImage *keyFrame,
 	const Common::Array<Common::Rect> &dirtyRegion) {
 
 	Common::Rect deltaFrameBounds = Common::Rect(deltaFramePos, deltaFrame->width(), deltaFrame->height());
@@ -1020,8 +1020,8 @@ void VideoDisplayManager::deltaRleBlitRectsClip(
 void VideoDisplayManager::deltaRleBlit1Rect(
 	Graphics::ManagedSurface *destinationImage,
 	const Common::Point &destinationPoint,
-	const Bitmap *deltaFrame,
-	const Bitmap *keyFrame,
+	const PixMapImage *deltaFrame,
+	const PixMapImage *keyFrame,
 	const Common::Rect &dirtyRect) {
 
 	// This is a very complex function that attempts to decompress the keyframe
@@ -1033,7 +1033,7 @@ void VideoDisplayManager::deltaRleBlit1Rect(
 }
 
 Graphics::ManagedSurface VideoDisplayManager::decompressRle8Bitmap(
-	const Bitmap *source,
+	const PixMapImage *source,
 	const Graphics::ManagedSurface *keyFrame,
 	const Common::Point *keyFrameOffset) {
 
diff --git a/engines/mediastation/graphics.h b/engines/mediastation/graphics.h
index cd1fc1e7bbc..49b95d85d8b 100644
--- a/engines/mediastation/graphics.h
+++ b/engines/mediastation/graphics.h
@@ -35,7 +35,7 @@ namespace MediaStation {
 
 class MediaStationEngine;
 struct DissolvePattern;
-class Bitmap;
+class PixMapImage;
 
 enum BlitMode {
 	kUncompressedBlit = 0x00,
@@ -136,7 +136,7 @@ public:
 
 	void imageBlit(
 		Common::Point destinationPoint,
-		const Bitmap *image,
+		const PixMapImage *image,
 		double dissolveFactor,
 		DisplayContext *displayContext,
 		Graphics::ManagedSurface *destinationImage = nullptr);
@@ -144,8 +144,8 @@ public:
 	void imageDeltaBlit(
 		Common::Point deltaFramePos,
 		const Common::Point &keyFrameOffset,
-		const Bitmap *deltaFrame,
-		const Bitmap *keyFrame,
+		const PixMapImage *deltaFrame,
+		const PixMapImage *keyFrame,
 		const double dissolveFactor,
 		DisplayContext *displayContext);
 
@@ -188,43 +188,43 @@ private:
 	void rleBlitRectsClip(
 		Graphics::ManagedSurface *dest,
 		const Common::Point &destLocation,
-		const Bitmap *source,
+		const PixMapImage *source,
 		const Common::Array<Common::Rect> &dirtyRegion);
 	Graphics::ManagedSurface decompressRle8Bitmap(
-		const Bitmap *source,
+		const PixMapImage *source,
 		const Graphics::ManagedSurface *keyFrame = nullptr,
 		const Common::Point *keyFrameOffset = nullptr);
 	void dissolveBlitRectsClip(
 		Graphics::ManagedSurface *dest,
 		const Common::Point &destPos,
-		const Bitmap *source,
+		const PixMapImage *source,
 		const Common::Array<Common::Rect> &dirtyRegion,
 		const uint dissolveFactor);
 	void dissolveBlit1Rect(
 		Graphics::ManagedSurface *dest,
 		const Common::Rect &areaToRedraw,
 		const Common::Point &originOnScreen,
-		const Bitmap *source,
+		const PixMapImage *source,
 		const Common::Rect &dirtyRegion,
 		const DissolvePattern &pattern);
 	void fullDeltaRleBlitRectsClip(
 		Graphics::ManagedSurface *destinationImage,
 		const Common::Point &deltaFramePos,
 		const Common::Point &keyFrameOffset,
-		const Bitmap *deltaFrame,
-		const Bitmap *keyFrame,
+		const PixMapImage *deltaFrame,
+		const PixMapImage *keyFrame,
 		const Common::Array<Common::Rect> &dirtyRegion);
 	void deltaRleBlitRectsClip(
 		Graphics::ManagedSurface *destinationImage,
 		const Common::Point &deltaFramePos,
-		const Bitmap *deltaFrame,
-		const Bitmap *keyFrame,
+		const PixMapImage *deltaFrame,
+		const PixMapImage *keyFrame,
 		const Common::Array<Common::Rect> &dirtyRegion);
 	void deltaRleBlit1Rect(
 		Graphics::ManagedSurface *destinationImage,
 		const Common::Point &destinationPoint,
-		const Bitmap *sourceImage,
-		const Bitmap *deltaImage,
+		const PixMapImage *sourceImage,
+		const PixMapImage *deltaImage,
 		const Common::Rect &dirtyRect);
 
 	// Transition methods.
diff --git a/engines/mediastation/mediastation.h b/engines/mediastation/mediastation.h
index d7074215ba8..43246c715d3 100644
--- a/engines/mediastation/mediastation.h
+++ b/engines/mediastation/mediastation.h
@@ -52,7 +52,7 @@ namespace MediaStation {
 struct MediaStationGameDescription;
 class HotspotActor;
 class RootStage;
-class Bitmap;
+class PixMapImage;
 
 // Most Media Station titles follow this file structure from the root directory
 // of the CD-ROM:


Commit: a86401a6951744baa6e6c33cda0246ab8ddc8817
    https://github.com/scummvm/scummvm/commit/a86401a6951744baa6e6c33cda0246ab8ddc8817
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-02-24T20:49:55-05:00

Commit Message:
MEDIASTATION: Add initial support for stream movie proxies

Changed paths:
    engines/mediastation/actor.cpp
    engines/mediastation/actor.h
    engines/mediastation/actors/movie.cpp
    engines/mediastation/actors/movie.h
    engines/mediastation/datafile.cpp
    engines/mediastation/mediascript/scriptconstants.cpp
    engines/mediastation/mediascript/scriptconstants.h


diff --git a/engines/mediastation/actor.cpp b/engines/mediastation/actor.cpp
index 2ea0a7a6861..0f7f661f367 100644
--- a/engines/mediastation/actor.cpp
+++ b/engines/mediastation/actor.cpp
@@ -62,6 +62,8 @@ const char *actorTypeToStr(ActorType type) {
 		return "ImageSet";
 	case kActorTypeMovie:
 		return "Movie";
+	case kActorTypeStreamMovieProxy:
+		return "StreamMovieProxy";
 	case kActorTypePalette:
 		return "Palette";
 	case kActorTypePrinter:
diff --git a/engines/mediastation/actor.h b/engines/mediastation/actor.h
index 781d068900d..1f69b662c83 100644
--- a/engines/mediastation/actor.h
+++ b/engines/mediastation/actor.h
@@ -54,6 +54,7 @@ enum ActorType {
 	kActorTypePrinter = 0x0019, // PRT
 	kActorTypeMovie = 0x0016, // MOV
 	kActorTypePalette = 0x0017,
+	kActorTypeStreamMovieProxy = 0x18,
 	kActorTypeText = 0x001a, // TXT
 	kActorTypeFont = 0x001b, // FON
 	kActorTypeCamera = 0x001c, // CAM
@@ -97,6 +98,7 @@ enum ActorHeaderSectionType {
 	kActorHeaderScaleY = 0x77d,
 	kActorHeaderUnk0 = 0x7d0,
 	kActorHeaderActorName = 0x0bb8,
+	kStreamMovieProxyInfo = 0x06ac,
 
 	// PATH FIELDS.
 	kActorHeaderStartPoint = 0x060e,
diff --git a/engines/mediastation/actors/movie.cpp b/engines/mediastation/actors/movie.cpp
index e417919ecb2..d01ca3ccf7d 100644
--- a/engines/mediastation/actors/movie.cpp
+++ b/engines/mediastation/actors/movie.cpp
@@ -25,9 +25,38 @@
 
 namespace MediaStation {
 
+StreamMovieProxy::StreamMovieProxy(Chunk &chunk, StreamMovieActor *parent) : SpatialEntity(kActorTypeStreamMovieProxy) {
+	_layerId = chunk.readTypedUint32();
+	_scriptId = chunk.readTypedUint16();
+	uint zIndex = chunk.readTypedSint16();
+	setZIndex(zIndex);
+	setBounds(parent->getBbox());
+	debugC(5, kDebugLoading, "[%s] %s: layerId: %d; scriptId: %d; zIndex: %d",
+		parent->debugName(), __func__, _layerId, _scriptId, zIndex);
+	_isVisible = true;
+	_parent = parent;
+}
+
+void StreamMovieProxy::draw(DisplayContext &displayContext) {
+	if (_parent != nullptr) {
+		_parent->drawLayer(displayContext, _layerId);
+	} else {
+		warning("[%s] %s: Stream movie proxy has no parent", debugName(), __func__);
+	}
+}
+
+bool StreamMovieProxy::isVisible() const {
+	if (_isVisible) {
+		if (_parentStage != nullptr) {
+			return _parentStage->isVisible();
+		}
+	}
+	return false;
+}
+
 MovieFrameInfo::MovieFrameInfo(Chunk &chunk) : ImageInfo(chunk) {
 	_index = chunk.readTypedUint32();
-	debugC(5, kDebugLoading, "MovieFrameInfo::MovieFrameInfo(): _index = 0x%x (@0x%llx)", _index, static_cast<long long int>(chunk.pos()));
+	debugC(5, kDebugLoading, "%s: frame 0x%x", __func__, _index);
 	_keyframeEndInMilliseconds = chunk.readTypedUint32();
 }
 
@@ -59,11 +88,6 @@ MovieFrame::MovieFrame(Chunk &chunk) {
 		index = chunk.readTypedUint32();
 		keyframeIndex = chunk.readTypedUint32();
 		keepAfterEnd = chunk.readTypedByte();
-		debugC(5, kDebugLoading, "MovieFrame::MovieFrame(): _blitType = %d, _startInMilliseconds = %d, \
-			_endInMilliseconds = %d, _left = %d, _top = %d, _zIndex = %d, _diffBetweenKeyframeAndFrameX = %d, \
-			_diffBetweenKeyframeAndFrameY = %d, _index = %d, _keyframeIndex = %d, _keepAfterEnd = %d (@0x%llx)",
-			blitType, startInMilliseconds, endInMilliseconds, leftTop.x, leftTop.y, zIndex, diffBetweenKeyframeAndFrame.x, \
-			diffBetweenKeyframeAndFrame.y, index, keyframeIndex, keepAfterEnd, static_cast<long long int>(chunk.pos()));
 	}
 }
 
@@ -82,6 +106,11 @@ StreamMovieActor::~StreamMovieActor() {
 
 	delete _streamSound;
 	_streamSound = nullptr;
+
+	for (StreamMovieProxy *proxy : _proxies) {
+		delete proxy;
+	}
+	_proxies.clear();
 }
 
 void StreamMovieActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
@@ -91,7 +120,7 @@ void StreamMovieActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramT
 		// as the ID we have already read.
 		uint32 duplicateActorId = chunk.readTypedUint16();
 		if (duplicateActorId != _id) {
-			warning("%s: Duplicate actor ID %d does not match original ID %d", __func__, duplicateActorId, _id);
+			warning("[%s] %s: Duplicate actor ID %d does not match original ID %d", debugName(), __func__, duplicateActorId, _id);
 		}
 		break;
 	}
@@ -135,6 +164,12 @@ void StreamMovieActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramT
 		_streamSound->_audioSequence.readParameters(chunk);
 		break;
 
+	case kStreamMovieProxyInfo: {
+		StreamMovieProxy *proxy = new StreamMovieProxy(chunk, this);
+		_proxies.push_back(proxy);
+		break;
+	}
+
 	default:
 		SpatialEntity::readParameter(chunk, paramType);
 	}
@@ -144,16 +179,51 @@ ScriptValue StreamMovieActor::callMethod(BuiltInMethod methodId, Common::Array<S
 	ScriptValue returnValue;
 
 	switch (methodId) {
-	case kTimePlayMethod: {
-		ARGCOUNTCHECK(0);
-		timePlay();
+	case kSpatialShowMethod: {
+		if (args.empty()) {
+			// Set our visibility directly.
+			setVisibility(true);
+			updateFrameState();
+		} else {
+			// Set the visibility of a proxy.
+			uint scriptId = args[0].asParamToken();
+			StreamMovieProxy *proxy = proxyOfScriptId(scriptId);
+			if (proxy != nullptr) {
+				if (!proxy->isVisible()) {
+					proxy->_isVisible = true;
+				}
+			} else {
+				warning("[%s] %s: Stream movie proxy with script ID %s doesn't exist",
+					debugName(), __func__, g_engine->formatParamTokenName(scriptId).c_str());
+			}
+		}
 		return returnValue;
 	}
 
-	case kSpatialShowMethod: {
+	case kSpatialHideMethod: {
+		if (args.empty()) {
+			// Set our visibility directly.
+			setVisibility(false);
+			updateFrameState();
+		} else {
+			// Set the visibility of a proxy.
+			uint scriptId = args[0].asParamToken();
+			StreamMovieProxy *proxy = proxyOfScriptId(scriptId);
+			if (proxy != nullptr) {
+				if (proxy->isVisible()) {
+					proxy->_isVisible = false;
+				}
+			} else {
+				warning("[%s] %s: Stream movie proxy with script ID %s doesn't exist",
+					debugName(), __func__, g_engine->formatParamTokenName(scriptId).c_str());
+			}
+		}
+		return returnValue;
+	}
+
+	case kTimePlayMethod: {
 		ARGCOUNTCHECK(0);
-		setVisibility(true);
-		updateFrameState();
+		timePlay();
 		return returnValue;
 	}
 
@@ -163,9 +233,30 @@ ScriptValue StreamMovieActor::callMethod(BuiltInMethod methodId, Common::Array<S
 		return returnValue;
 	}
 
-	case kSpatialHideMethod: {
-		ARGCOUNTCHECK(0);
-		setVisibility(false);
+	case kStreamMovieSetProxyZIndex: {
+		ARGCOUNTCHECK(2);
+		uint scriptId = args[0].asParamToken();
+		int zIndex = static_cast<int>(args[1].asFloat());
+		StreamMovieProxy *proxy = proxyOfScriptId(scriptId);
+		if (proxy != nullptr) {
+			proxy->setZIndex(zIndex);
+		} else {
+			warning("[%s] %s: Stream movie proxy with script ID %s doesn't exist",
+				debugName(), __func__, g_engine->formatParamTokenName(scriptId).c_str());
+		}
+		return returnValue;
+	}
+
+	case kStreamMovieGetProxyZIndex: {
+		ARGCOUNTCHECK(2);
+		uint scriptId = args[0].asParamToken();
+		StreamMovieProxy *proxy = proxyOfScriptId(scriptId);
+		if (proxy != nullptr) {
+			returnValue.setToFloat(proxy->zIndex());
+		} else {
+			warning("[%s] %s: Stream movie proxy with script ID %s doesn't exist",
+				debugName(), __func__, g_engine->formatParamTokenName(scriptId).c_str());
+		}
 		return returnValue;
 	}
 
@@ -189,6 +280,52 @@ ScriptValue StreamMovieActor::callMethod(BuiltInMethod methodId, Common::Array<S
 		return returnValue;
 	}
 
+	case kStreamMovieMoveProxyToStageMethod: {
+		ARGCOUNTCHECK(2);
+		uint scriptId = args[0].asParamToken();
+		uint targetStageId = args[1].asActorId();
+		StreamMovieProxy *proxy = proxyOfScriptId(scriptId);
+		if (proxy == nullptr) {
+			warning("[%s] %s: Stream movie proxy with script ID %s doesn't exist",
+				debugName(), __func__, g_engine->formatParamTokenName(scriptId).c_str());
+			return returnValue;
+		}
+		StageActor *parentStage = static_cast<StageActor *>(g_engine->getActorByIdAndType(targetStageId, kActorTypeStage));
+		if (parentStage == nullptr) {
+			warning("[%s] %s: Stream movie proxy with script ID %s has null parent stage",
+				debugName(), __func__, g_engine->formatParamTokenName(scriptId).c_str());
+			return returnValue;
+		}
+
+		proxy->getParentStage()->removeChildSpatialEntity(proxy);
+		parentStage->addChildSpatialEntity(proxy);
+		return returnValue;
+	}
+
+	case kStreamMovieMoveProxyToRootStageMethod: {
+		ARGCOUNTCHECK(2);
+		uint scriptId = args[0].asParamToken();
+		uint sourceStageId = args[1].asActorId();
+		StreamMovieProxy *proxy = proxyOfScriptId(scriptId);
+		if (proxy == nullptr) {
+			warning("[%s] %s: Stream movie proxy with script ID %s doesn't exist",
+				debugName(), __func__, g_engine->formatParamTokenName(scriptId).c_str());
+			return returnValue;
+		}
+
+		RootStage *rootStage = g_engine->getRootStage();
+		StageActor *sourceStage = static_cast<StageActor *>(g_engine->getActorByIdAndType(sourceStageId, kActorTypeStage));
+		if (sourceStage == nullptr) {
+			warning("[%s] %s: Stream movie proxy with script ID %s has null parent stage",
+				debugName(), __func__, g_engine->formatParamTokenName(scriptId).c_str());
+			return returnValue;
+		}
+
+		sourceStage->removeChildSpatialEntity(proxy);
+		rootStage->addChildSpatialEntity(proxy);
+		return returnValue;
+	}
+
 	default:
 		return SpatialEntity::callMethod(methodId, args);
 	}
@@ -314,10 +451,19 @@ void StreamMovieActor::updateFrameState() {
 }
 
 void StreamMovieActor::draw(DisplayContext &displayContext) {
+	const uint DEFAULT_LAYER_ID = 0;
+	drawLayer(displayContext, DEFAULT_LAYER_ID);
+}
+
+void StreamMovieActor::drawLayer(DisplayContext &displayContext, uint layerId) {
 	for (MovieFrame *frame : _framesOnScreen) {
+		if (frame->layerId != layerId) {
+			continue;
+		}
+
 		Common::Rect bbox = getFrameBoundingBox(frame);
-		debugC(8, kDebugGraphics, "%s: %s: frame %d (%d, %d, %d, %d)",
-			__func__, debugName(), frame->index, PRINT_RECT(bbox));
+		debugC(8, kDebugGraphics, "[%s] %s: layer %d, frame %d (%d, %d, %d, %d)",
+			debugName(), __func__, layerId, frame->index, PRINT_RECT(bbox));
 
 		switch (frame->blitType) {
 		case kUncompressedMovieBlit:
@@ -345,6 +491,15 @@ void StreamMovieActor::draw(DisplayContext &displayContext) {
 	}
 }
 
+void StreamMovieActor::invalidateLocalBounds() {
+	for (StreamMovieProxy *proxy : _proxies) {
+		// Our bounds might have changed, so pass that on to the proxies.
+		proxy->setBounds(getBbox());
+	}
+	SpatialEntity::invalidateLocalBounds();
+}
+
+
 Common::Rect StreamMovieActor::getFrameBoundingBox(MovieFrame *frame) {
 	// Use _boundingBox directly (which may be temporarily offset by camera rendering)
 	// The camera offset is already applied to _boundingBox by pushBoundingBoxOffset()
@@ -369,7 +524,7 @@ StreamMovieActorFrames::~StreamMovieActorFrames() {
 
 void StreamMovieActorFrames::readChunk(Chunk &chunk) {
 	uint sectionType = chunk.readTypedUint16();
-	switch ((MovieSectionType)sectionType) {
+	switch (static_cast<MovieSectionType>(sectionType)) {
 	case kMovieImageDataSection:
 		readImageData(chunk);
 		break;
@@ -396,6 +551,11 @@ void StreamMovieActorFrames::readChunk(Chunk &chunk) {
 	}
 }
 
+void StreamMovieActor::loadIsComplete() {
+	SpatialEntity::loadIsComplete();
+	updateFrameState();
+}
+
 StreamMovieActorSound::~StreamMovieActorSound() {
 	unregisterWithStreamManager();
 }
@@ -461,6 +621,9 @@ void StreamMovieActorFrames::readFrameData(Chunk &chunk) {
 	uint frameDataToRead = chunk.readTypedUint16();
 	for (uint i = 0; i < frameDataToRead; i++) {
 		MovieFrame *frame = new MovieFrame(chunk);
+		if (!_parent->isLayerInSeparateZPlane(frame->layerId)) {
+			frame->layerId = 0;
+		}
 
 		// We cannot use a hashmap here because multiple frames can have the
 		// same index, and frames are not necessarily in index order. So we'll
@@ -485,6 +648,31 @@ void StreamMovieActorFrames::readFrameData(Chunk &chunk) {
 	}
 }
 
+StreamMovieProxy *StreamMovieActor::proxyOfId(uint layerId) {
+	// TODO: Why can this not be a hashmap?
+	for (StreamMovieProxy *proxy : _proxies) {
+		if (proxy->_layerId == layerId) {
+			return proxy;
+		}
+	}
+	return nullptr;
+}
+
+StreamMovieProxy *StreamMovieActor::proxyOfScriptId(uint scriptId) {
+	// TODO: Why can this not be a hashmap?
+	for (StreamMovieProxy *proxy : _proxies) {
+		if (proxy->_scriptId == scriptId) {
+			return proxy;
+		}
+	}
+	return nullptr;
+}
+
+bool StreamMovieActor::isLayerInSeparateZPlane(uint layerId) {
+	bool proxyExistsForLayer = proxyOfId(layerId) != nullptr;
+	return proxyExistsForLayer;
+}
+
 int StreamMovieActor::compareFramesByZIndex(const MovieFrame *a, const MovieFrame *b) {
 	if (b->zIndex > a->zIndex) {
 		return 1;
diff --git a/engines/mediastation/actors/movie.h b/engines/mediastation/actors/movie.h
index e2ce06e3f71..b6c900c7612 100644
--- a/engines/mediastation/actors/movie.h
+++ b/engines/mediastation/actors/movie.h
@@ -86,6 +86,27 @@ struct MovieFrame {
 
 class StreamMovieActor;
 
+// Represents an individually controllable layer of a stream movie that is its own spatial entity
+// and can be in a different stage than its parent stream movie, but which references the parent's
+// frames at all times. Only frames with a layer ID matching a proxy's layer ID will be drawn when
+// that proxy is drawn. For example, this is used in the last section of the Dalmatians hide-and-seek
+// minigame to show a "light" layer that only appears when the player shines a flashlight on an area,
+// while a "dark" layer seamlessly displays otherwise.
+class StreamMovieProxy : public SpatialEntity {
+friend class StreamMovieActor;
+
+public:
+	StreamMovieProxy(Chunk &chunk, StreamMovieActor *parent);
+	virtual void draw(DisplayContext &displayContext) override;
+	virtual bool isVisible() const override;
+
+	uint _layerId = 0;
+	uint _scriptId = 0;
+
+private:
+	StreamMovieActor *_parent = nullptr;
+};
+
 // This is called `RT_stmvFrames` in the original.
 class StreamMovieActorFrames : public ChannelClient {
 public:
@@ -122,14 +143,15 @@ public:
 	virtual ~StreamMovieActor() override;
 
 	virtual void readChunk(Chunk &chunk) override;
-
+	virtual void loadIsComplete() override;
 	virtual void readParameter(Chunk &chunk, ActorHeaderSectionType paramType) override;
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 	virtual void process() override;
 
 	virtual void draw(DisplayContext &displayContext) override;
-
-	virtual bool isVisible() const override { return _isVisible; }
+	void drawLayer(DisplayContext &displayContext, uint layerId);
+	virtual void invalidateLocalBounds() override;
+	bool isLayerInSeparateZPlane(uint layerId);
 
 private:
 	ImtStreamFeed *_streamFeed = nullptr;
@@ -145,6 +167,7 @@ private:
 	StreamMovieActorSound *_streamSound = nullptr;
 
 	Common::Array<MovieFrame *> _framesNotYetShown;
+	Common::Array<StreamMovieProxy *> _proxies;
 	Common::SortedArray<MovieFrame *, const MovieFrame *> _framesOnScreen;
 
 	// Script method implementations.
@@ -160,6 +183,8 @@ private:
 	void parseMovieChunkMarker(Chunk &chunk);
 
 	Common::Rect getFrameBoundingBox(MovieFrame *frame);
+	StreamMovieProxy *proxyOfId(uint layerId);
+	StreamMovieProxy *proxyOfScriptId(uint scriptId);
 	static int compareFramesByZIndex(const MovieFrame *a, const MovieFrame *b);
 };
 
diff --git a/engines/mediastation/datafile.cpp b/engines/mediastation/datafile.cpp
index 6a365d17da4..016d1feeb7c 100644
--- a/engines/mediastation/datafile.cpp
+++ b/engines/mediastation/datafile.cpp
@@ -145,7 +145,7 @@ Chunk::Chunk(Common::SeekableReadStream *stream) : _parentStream(stream) {
 	_length = _parentStream->readUint32LE();
 	_dataStartOffset = pos();
 	_dataEndOffset = _dataStartOffset + _length;
-	debugC(5, kDebugLoading, "Chunk::Chunk(): Got chunk with ID \"%s\" and size 0x%x", tag2str(_id), _length);
+	debugC(5, kDebugLoading, "%s: Got chunk with ID \"%s\" and size 0x%x", __func__, tag2str(_id), _length);
 }
 
 uint32 Chunk::bytesRemaining() {
diff --git a/engines/mediastation/mediascript/scriptconstants.cpp b/engines/mediastation/mediascript/scriptconstants.cpp
index 833dc63158c..2e41e0dc9f4 100644
--- a/engines/mediastation/mediascript/scriptconstants.cpp
+++ b/engines/mediastation/mediascript/scriptconstants.cpp
@@ -237,8 +237,22 @@ const char *builtInMethodToStr(BuiltInMethod method) {
 		return "GetMouseXOffset";
 	case kGetMouseYOffsetMethod:
 		return "GetMouseYOffset";
+	case kStreamMovieSetProxyZIndex:
+		return "SetProxyZIndex";
+	case kStreamMovieGetProxyZIndex:
+		return "GetProxyZIndex";
 	case kIsVisibleMethod:
 		return "IsVisible";
+	case kStartCachingMethod:
+		return "StartCaching";
+	case kIsCachingMethod:
+		return "IsCaching";
+	case kPauseMethod:
+		return "PauseWhileStarting";
+	case kResumeMethod:
+		return "ResumeStart";
+	case kIsPausedMethod:
+		return "SetMultipleSounds/IsPaused";
 	case kSetMousePositionMethod:
 		return "SetMousePosition";
 	case kGetXScaleMethod1:
@@ -298,6 +312,10 @@ const char *builtInMethodToStr(BuiltInMethod method) {
 		return "PanTo";
 	case kClearToPaletteMethod:
 		return "ClearToPalette";
+	case kStreamMovieMoveProxyToStageMethod:
+		return "MoveProxyToStage";
+	case kStreamMovieMoveProxyToRootStageMethod:
+		return "MoveProxyToRootStage";
 	case kDocumentLoadContextMethod:
 		return "LoadContext";
 	case kDocumentReleaseContextMethod:
@@ -308,8 +326,6 @@ const char *builtInMethodToStr(BuiltInMethod method) {
 		return "Quit";
 	case kDocumentContextLoadInProgressMethod:
 		return "ContextLoadInProgress";
-	case kDocumentSetMultipleSoundsMethod:
-		return "SetMultipleSounds";
 	case kDocumentContextIsLoadedMethod:
 		return "IsLoaded";
 	case kSetDurationMethod:
diff --git a/engines/mediastation/mediascript/scriptconstants.h b/engines/mediastation/mediascript/scriptconstants.h
index ff8c366645a..622e4385488 100644
--- a/engines/mediastation/mediascript/scriptconstants.h
+++ b/engines/mediastation/mediascript/scriptconstants.h
@@ -148,6 +148,17 @@ enum BuiltInMethod {
 	kSetXScaleMethod = 0x17F,
 	kGetYScaleMethod = 0x180,
 	kSetYScaleMethod = 0x181,
+	kStartCachingMethod = 0x113,
+	kIsCachingMethod = 0x114,
+	kPauseMethod = 0xD0,
+	kResumeMethod = 0xD1,
+	kIsPausedMethod = 0x175,
+
+	// STREAM MOVIE METHODS.
+	kStreamMovieSetProxyZIndex = 0x10B,
+	kStreamMovieGetProxyZIndex = 0x10C,
+	kStreamMovieMoveProxyToStageMethod = 0x17C,
+	kStreamMovieMoveProxyToRootStageMethod = 0x17D,
 
 	// HOTSPOT METHODS.
 	// NOTE: IDs 0xD2 and 0xD3 seem to be double-assigned


Commit: 98528cadc1e3917ce681571e188fd9c58c33e70a
    https://github.com/scummvm/scummvm/commit/98528cadc1e3917ce681571e188fd9c58c33e70a
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-02-24T20:49:55-05:00

Commit Message:
MEDIASTATION: Add support for camera images

This is needed to get the "flashlight" screen of the Dalmatians puppy-finding
minigame working, among other things.

Changed paths:
    engines/mediastation/actors/camera.cpp
    engines/mediastation/actors/camera.h
    engines/mediastation/bitmap.cpp
    engines/mediastation/bitmap.h
    engines/mediastation/graphics.cpp
    engines/mediastation/graphics.h


diff --git a/engines/mediastation/actors/camera.cpp b/engines/mediastation/actors/camera.cpp
index 4afcbbd6f7f..92b6fb5175c 100644
--- a/engines/mediastation/actors/camera.cpp
+++ b/engines/mediastation/actors/camera.cpp
@@ -34,6 +34,9 @@ CameraActor::~CameraActor() {
 		_parentStage->removeCamera(this);
 		_parentStage->removeChildSpatialEntity(this);
 	}
+	delete _childrenWithOverlaySurface;
+	_childrenWithOverlaySurface = nullptr;
+	_childrenWithOverlayContext._destImage = nullptr;
 }
 
 void CameraActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
@@ -41,7 +44,7 @@ void CameraActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType)
 	case kActorHeaderChannelIdent:
 		_channelIdent = chunk.readTypedChannelIdent();
 		registerWithStreamManager();
-		_image = Common::SharedPtr<ImageAsset>(new ImageAsset);
+		_overlayImage = Common::SharedPtr<ImageAsset>(new ImageAsset);
 		break;
 
 	case kActorHeaderStartup:
@@ -69,7 +72,7 @@ void CameraActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType)
 	case kActorHeaderCameraImageActor: {
 		uint actorReference = chunk.readTypedUint16();
 		CameraActor *referencedCamera = static_cast<CameraActor *>(g_engine->getActorByIdAndType(actorReference, kActorTypeCamera));
-		_image = referencedCamera->_image;
+		_overlayImage = referencedCamera->_overlayImage;
 		break;
 	}
 
@@ -80,7 +83,7 @@ void CameraActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType)
 
 void CameraActor::readChunk(Chunk &chunk) {
 	ImageInfo bitmapHeader(chunk);
-	_image->bitmap = new PixMapImage(chunk, bitmapHeader);
+	_overlayImage->bitmap = new PixMapImage(chunk, bitmapHeader);
 }
 
 ScriptValue CameraActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
@@ -170,15 +173,17 @@ ScriptValue CameraActor::callMethod(BuiltInMethod methodId, Common::Array<Script
 
 	case kAdjustCameraViewportSpatialCenterMethod: {
 		ARGCOUNTCHECK(2);
-		int16 xDiff = static_cast<int16>(args[0].asFloat());
-		int16 yDiff = static_cast<int16>(args[1].asFloat());
+		int16 x = static_cast<int16>(args[0].asFloat());
+		int16 y = static_cast<int16>(args[1].asFloat());
 
 		// Apply centering adjustment, which is indeed based on the entire camera actor's
 		// bounds, not just the current viewport bounds.
-		int16 centeredXDiff = xDiff - (getBbox().width() / 2);
-		int16 centeredYDiff = yDiff - (getBbox().height() / 2);
-		Common::Point viewportDelta(centeredXDiff, centeredYDiff);
-		_nextViewportOrigin = getViewportOrigin() + viewportDelta;
+		int16 centeredX = x - (getBbox().width() / 2);
+		int16 centeredY = y - (getBbox().height() / 2);
+		_nextViewportOrigin = Common::Point(centeredX, centeredY);
+		debugC(6, kDebugCamera, "%s: currentViewportOrigin: (%d, %d); nextViewportOrigin: (%d, %d)",
+			__func__, _currentViewportOrigin.x, _currentViewportOrigin.y, _nextViewportOrigin.x, _nextViewportOrigin.y);
+
 		adjustCameraViewport(_nextViewportOrigin);
 		if (!_addedToStage) {
 			_currentViewportOrigin = _nextViewportOrigin;
@@ -233,7 +238,6 @@ ScriptValue CameraActor::callMethod(BuiltInMethod methodId, Common::Array<Script
 
 	default:
 		returnValue = SpatialEntity::callMethod(methodId, args);
-		break;
 	}
 	return returnValue;
 }
@@ -250,8 +254,21 @@ void CameraActor::loadIsComplete() {
 		addToStage();
 	}
 
-	if (_image != nullptr) {
-		warning("%s: STUB: Camera image asset not handled yet", __func__);
+	if (_overlayImage != nullptr) {
+		// Create the intermediate surface where we'll draw the actors and the overlay.
+		ImageInfo imageInfo;
+		imageInfo._dimensions = Common::Point(getBbox().width(), getBbox().height());
+		imageInfo._stride = getBbox().width();
+		_childrenWithOverlaySurface = new PixMapImage(imageInfo);
+		_childrenWithOverlayContext._destImage = &_childrenWithOverlaySurface->_image;
+		_childrenWithOverlayContext.verifyClipSize();
+
+		// Mark this whole region dirty.
+		Region region;
+		Common::Rect cameraRect(0, 0, getBbox().width(), getBbox().height());
+		region.addRect(cameraRect);
+		_childrenWithOverlayContext.addClip();
+		_childrenWithOverlayContext.setClipTo(region);
 	}
 }
 
@@ -287,42 +304,62 @@ Common::Rect CameraActor::getViewportBounds() {
 	return viewportBounds;
 }
 
-void CameraActor::drawUsingCamera(DisplayContext &displayContext, const Common::Array<SpatialEntity *> &entitiesToDraw) {
-	Clip *currentClip = displayContext.currentClip();
+void CameraActor::drawUsingCamera(DisplayContext &destContext, const Common::Array<SpatialEntity *> &entitiesToDraw) {
+	// Establish the initial clipping region.
+	Clip *currentClip = destContext.currentClip();
 	if (currentClip != nullptr) {
-		Clip *previousClip = displayContext.previousClip();
+		Clip *previousClip = destContext.previousClip();
 		if (previousClip == nullptr) {
+			// Initialize the clip.
 			currentClip->addToRegion(currentClip->_bounds);
 		} else {
+			// Copy the previous clip to the current clip.
 			*currentClip = *previousClip;
 		}
 	}
 
-	Common::Rect cameraBounds = getBbox();
-	displayContext.intersectClipWith(cameraBounds);
-	displayContext.pushOrigin();
-
+	destContext.intersectClipWith(getBbox());
+	destContext.pushOrigin();
 	Common::Point viewportOrigin = getViewportOrigin();
-	Common::Point viewportOffset(
-		-viewportOrigin.x + cameraBounds.left,
-		-viewportOrigin.y + cameraBounds.top
-	);
-	displayContext._origin.x += viewportOffset.x;
-	displayContext._origin.y += viewportOffset.y;
+	destContext._origin += (getBbox().origin() - viewportOrigin);
 
-	if (_image != nullptr) {
-		// TODO: Handle image asset stuff.
-		warning("%s: Camera image asset not handled yet", __func__);
+	if (_overlayImage != nullptr) {
+		// Make sure we are ready to draw the overlay image.
+		_childrenWithOverlayContext.pushOrigin();
+		_childrenWithOverlayContext._origin -= _offset;
+		_childrenWithOverlayContext._origin -= viewportOrigin;
 	}
 
 	for (SpatialEntity *entityToDraw : entitiesToDraw) {
+		debugCN(6, kDebugGraphics, "[%s] %s: %s (viewport: %d, %d) (bounds: %d, %d, %d, %d) ", debugName(), __func__, entityToDraw->debugName(),
+			_currentViewportOrigin.x, _currentViewportOrigin.y, PRINT_RECT(entityToDraw->getBbox()));
+
 		if (entityToDraw->isVisible()) {
-			drawObject(displayContext, displayContext, entityToDraw);
+			if (_overlayImage == nullptr) {
+				// Draw this image directly to the provided display context.
+				debugC(6, kDebugGraphics, "(no overlay)");
+				drawObject(destContext, destContext, entityToDraw);
+			} else {
+				// Draw this image to our internal display context, so we can apply the
+				// overlay to the drawn items afterward.
+				debugC(6, kDebugGraphics, "(overlay)");
+				drawObject(destContext, _childrenWithOverlayContext, entityToDraw);
+			}
 		}
 	}
 
-	displayContext.popOrigin();
-	displayContext.emptyCurrentClip();
+	if (_overlayImage != nullptr) {
+		// Now actually apply the overlay.
+		destContext._origin += _offset;
+		g_engine->getDisplayManager()->imageDeltaBlit(
+			getViewportOrigin(), Common::Point(0, 0), _overlayImage->bitmap, _childrenWithOverlaySurface, 1.0, &destContext
+		);
+
+		_childrenWithOverlayContext.popOrigin();
+	}
+
+	destContext.popOrigin();
+	destContext.emptyCurrentClip();
 }
 
 void CameraActor::drawObject(DisplayContext &sourceContext, DisplayContext &destContext, SpatialEntity *objectToDraw) {
diff --git a/engines/mediastation/actors/camera.h b/engines/mediastation/actors/camera.h
index c093c6f8f56..2606b3008f4 100644
--- a/engines/mediastation/actors/camera.h
+++ b/engines/mediastation/actors/camera.h
@@ -44,7 +44,7 @@ struct ImageAsset;
 class CameraActor : public SpatialEntity, public ChannelClient {
 public:
 	CameraActor() : SpatialEntity(kActorTypeCamera) {};
-	~CameraActor();
+	virtual ~CameraActor() override;
 
 	virtual void readParameter(Chunk &chunk, ActorHeaderSectionType paramType) override;
 	virtual void readChunk(Chunk &chunk) override;
@@ -73,8 +73,12 @@ private:
 	Common::Point _panStart;
 	Common::Point _panDest;
 	Common::Point _panDelta;
-	Common::SharedPtr<ImageAsset> _image;
-	DisplayContext _displayContext;
+
+	// A camera can have an image that overlays its contents. To do this, we need a
+	// surface on which to put the actors shown through the camera before we draw the overlay.
+	Common::SharedPtr<ImageAsset> _overlayImage;
+	PixMapImage *_childrenWithOverlaySurface = nullptr;
+	DisplayContext _childrenWithOverlayContext;
 
 	void addToStage();
 	void removeFromStage(bool stopPan);
diff --git a/engines/mediastation/bitmap.cpp b/engines/mediastation/bitmap.cpp
index e3698780862..4b86d6f74eb 100644
--- a/engines/mediastation/bitmap.cpp
+++ b/engines/mediastation/bitmap.cpp
@@ -57,6 +57,10 @@ PixMapImage::PixMapImage(Chunk &chunk, const ImageInfo &imageInfo) : _imageInfo(
 	}
 }
 
+PixMapImage::PixMapImage(const ImageInfo &imageInfo) : _imageInfo(imageInfo) {
+	_image.create(stride(), height(), Graphics::PixelFormat::createFormatCLUT8());
+}
+
 PixMapImage::~PixMapImage() {
 	delete _compressedStream;
 	_compressedStream = nullptr;
diff --git a/engines/mediastation/bitmap.h b/engines/mediastation/bitmap.h
index f3cfc7205e1..3182db65dba 100644
--- a/engines/mediastation/bitmap.h
+++ b/engines/mediastation/bitmap.h
@@ -51,6 +51,7 @@ public:
 class PixMapImage {
 public:
 	PixMapImage(Chunk &chunk, const ImageInfo &imageInfo);
+	PixMapImage(const ImageInfo &imageInfo);
 	virtual ~PixMapImage();
 
 	bool isCompressed() const;
diff --git a/engines/mediastation/graphics.cpp b/engines/mediastation/graphics.cpp
index c9d4ecb5810..dc1fb7cf274 100644
--- a/engines/mediastation/graphics.cpp
+++ b/engines/mediastation/graphics.cpp
@@ -161,6 +161,10 @@ void DisplayContext::verifyClipSize() {
 	}
 }
 
+void DisplayContext::deleteClips() {
+	_clips.empty();
+}
+
 bool DisplayContext::clipIsEmpty() {
 	Clip *clip = currentClip();
 	if (clip != nullptr) {
@@ -757,18 +761,19 @@ void VideoDisplayManager::imageBlit(
 	Common::Array<Common::Rect> dirtyRegion;
 	if (displayContext == nullptr) {
 		if (targetImage == nullptr) {
-			warning("%s: Neither display context nor target image was provided", __func__);
+			warning("%s: Neither display context nor target image was provided. Drawing cannot continue", __func__);
+			return;
 		}
 		Common::Rect targetImageBounds(0, 0, targetImage->w, targetImage->h);
 		dirtyRegion.push_back(targetImageBounds);
 	} else {
 		Clip *currentClip = displayContext->currentClip();
 		dirtyRegion = currentClip->_region._rects;
-		destinationPoint += _displayContext._origin;
-	}
+		destinationPoint += displayContext->_origin;
 
-	if (targetImage == nullptr) {
-		targetImage = _screen;
+		if (targetImage == nullptr) {
+			targetImage = displayContext->_destImage;
+		}
 	}
 
 	// In the disasm, this whole function has complex blit flag logic
@@ -792,7 +797,7 @@ void VideoDisplayManager::imageBlit(
 	case kCccBlit | kClipEnabled:
 	case kCccTransparentBlit | kClipEnabled:
 		// CCC blitting is unimplemented for now because few, if any, titles actually use it.
-		error("%s: CCC blitting not implemented yet", __func__);
+		warning("%s: CCC blitting not implemented yet", __func__);
 		break;
 
 	case kPartialDissolve | kClipEnabled:
@@ -945,36 +950,23 @@ void VideoDisplayManager::imageDeltaBlit(
 	const double dissolveFactor,
 	DisplayContext *displayContext) {
 
-	if (deltaFrame->getCompressionType() != kRle8BitmapCompression) {
-		error("%s: Unsupported delta frame compression type for delta blit: %d",
-			__func__, static_cast<uint>(keyFrame->getCompressionType()));
-	} else if (dissolveFactor != 1.0) {
-		warning("%s: Delta blit does not support dissolving", __func__);
-	}
-
 	Common::Array<Common::Rect> dirtyRegion;
-	if (displayContext == nullptr) {
-		error("%s: Display context must be provided", __func__);
-	} else {
+	if (displayContext != nullptr) {
 		Clip *currentClip = displayContext->currentClip();
 		dirtyRegion = currentClip->_region._rects;
-		deltaFramePos += _displayContext._origin;
+		deltaFramePos += displayContext->_origin;
+	} else {
+		warning("%s: Display context must be provided", __func__);
+		return;
 	}
 
-	switch (keyFrame->getCompressionType()) {
-	case kUncompressedBitmap:
-	case kUncompressedTransparentBitmap:
-		deltaRleBlitRectsClip(_screen, deltaFramePos, deltaFrame, keyFrame, dirtyRegion);
-		break;
-
-	case kRle8BitmapCompression:
-		fullDeltaRleBlitRectsClip(_screen, deltaFramePos, keyFrameOffset, deltaFrame, keyFrame, dirtyRegion);
-		break;
-
-	default:
-		error("%s: Unsupported keyframe image type for delta blit: %d",
-			__func__, static_cast<uint>(deltaFrame->getCompressionType()));
+	if (dissolveFactor != 1.0) {
+		warning("%s: Delta blit does not support dissolving", __func__);
 	}
+
+	// This is deliberately simplified logic for now. If we are trying to use an incorrect blitting
+	// mode, we will get an error in this call, rather than checking the blitting mode here.
+	fullDeltaRleBlitRectsClip(displayContext->_destImage, deltaFramePos, keyFrameOffset, deltaFrame, keyFrame, dirtyRegion);
 }
 
 void VideoDisplayManager::fullDeltaRleBlitRectsClip(
@@ -1045,7 +1037,7 @@ Graphics::ManagedSurface VideoDisplayManager::decompressRle8Bitmap(
 
 	Common::SeekableReadStream *chunk = source->_compressedStream;
 	if (chunk == nullptr) {
-		warning("%s: Got empty image", __func__);
+		warning("%s: No image to decompress", __func__);
 		return dest;
 	}
 
@@ -1077,40 +1069,43 @@ Graphics::ManagedSurface VideoDisplayManager::decompressRle8Bitmap(
 
 				} else if (operation == 0x02) {
 					// Copy from the keyframe region.
-					assert((keyFrame != nullptr) && (keyFrameOffset != nullptr));
 					byte xToCopy = chunk->readByte();
 					byte yToCopy = chunk->readByte();
 
-					// If we requested to copy multiple lines, do that first.
-					for (int lineOffset = 0; lineOffset < yToCopy; lineOffset++) {
-						Common::Point keyFramePos = sourcePos - *keyFrameOffset + Common::Point(0, lineOffset);
-						Common::Point destPos = sourcePos + Common::Point(0, lineOffset);
+					if ((keyFrame == nullptr) || (keyFrameOffset == nullptr)) {
+						warning("%s: Keyframe copy (%d, %d) requested but keyframe or offset is null", __func__, xToCopy, yToCopy);
+					} else {
+						// If we requested to copy multiple lines, do that first.
+						for (int lineOffset = 0; lineOffset < yToCopy; lineOffset++) {
+							Common::Point keyFramePos = sourcePos - *keyFrameOffset + Common::Point(0, lineOffset);
+							Common::Point destPos = sourcePos + Common::Point(0, lineOffset);
+
+							bool sourceXInBounds = (keyFramePos.x >= 0) && (keyFramePos.x + xToCopy <= keyFrame->w);
+							bool sourceYInBounds = (keyFramePos.y >= 0) && (keyFramePos.y < keyFrame->h);
+							bool destInBounds = (destPos.y * dest.w) + (destPos.x + xToCopy) <= destSizeInBytes;
+							if (sourceXInBounds && sourceYInBounds && destInBounds) {
+								const byte *srcPtr = static_cast<const byte *>(keyFrame->getBasePtr(keyFramePos.x, keyFramePos.y));
+								byte *destPtr = static_cast<byte *>(dest.getBasePtr(destPos.x, destPos.y));
+								memcpy(destPtr, srcPtr, xToCopy);
+							} else {
+								warning("%s: Keyframe copy (multi-line) exceeds bounds", __func__);
+							}
+						}
 
+						// Then copy the pixels in the same line.
+						Common::Point keyFramePos = sourcePos - *keyFrameOffset;
 						bool sourceXInBounds = (keyFramePos.x >= 0) && (keyFramePos.x + xToCopy <= keyFrame->w);
 						bool sourceYInBounds = (keyFramePos.y >= 0) && (keyFramePos.y < keyFrame->h);
-						bool destInBounds = (destPos.y * dest.w) + (destPos.x + xToCopy) <= destSizeInBytes;
+						bool destInBounds = (sourcePos.y * dest.w) + (sourcePos.x + xToCopy) <= destSizeInBytes;
 						if (sourceXInBounds && sourceYInBounds && destInBounds) {
 							const byte *srcPtr = static_cast<const byte *>(keyFrame->getBasePtr(keyFramePos.x, keyFramePos.y));
-							byte *destPtr = static_cast<byte *>(dest.getBasePtr(destPos.x, destPos.y));
+							byte *destPtr = static_cast<byte *>(dest.getBasePtr(sourcePos.x, sourcePos.y));
 							memcpy(destPtr, srcPtr, xToCopy);
 						} else {
-							warning("%s: Keyframe copy (multi-line) exceeds bounds", __func__);
+							warning("%s: Keyframe copy (same line) exceeds bounds", __func__);
 						}
 					}
 
-					// Then copy the pixels in the same line.
-					Common::Point keyFramePos = sourcePos - *keyFrameOffset;
-					bool sourceXInBounds = (keyFramePos.x >= 0) && (keyFramePos.x + xToCopy <= keyFrame->w);
-					bool sourceYInBounds = (keyFramePos.y >= 0) && (keyFramePos.y < keyFrame->h);
-					bool destInBounds = (sourcePos.y * dest.w) + (sourcePos.x + xToCopy) <= destSizeInBytes;
-					if (sourceXInBounds && sourceYInBounds && destInBounds) {
-						const byte *srcPtr = static_cast<const byte *>(keyFrame->getBasePtr(keyFramePos.x, keyFramePos.y));
-						byte *destPtr = static_cast<byte *>(dest.getBasePtr(sourcePos.x, sourcePos.y));
-						memcpy(destPtr, srcPtr, xToCopy);
-					} else {
-						warning("%s: Keyframe copy (same line) exceeds bounds", __func__);
-					}
-
 					sourcePos += Common::Point(xToCopy, yToCopy);
 
 				} else if (operation == 0x03) {
diff --git a/engines/mediastation/graphics.h b/engines/mediastation/graphics.h
index 49b95d85d8b..5a8c08fa1b3 100644
--- a/engines/mediastation/graphics.h
+++ b/engines/mediastation/graphics.h
@@ -104,6 +104,7 @@ public:
 	bool rectIsInClip(const Common::Rect &rect);
 	void setClipTo(Region region);
 	void emptyCurrentClip();
+	void deleteClips();
 
 	void addClip();
 	Clip *currentClip();


Commit: b9e38dd7e7af24a4f8561a97eebae7a6cd0188a6
    https://github.com/scummvm/scummvm/commit/b9e38dd7e7af24a4f8561a97eebae7a6cd0188a6
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-02-24T20:49:55-05:00

Commit Message:
MEDIASTATION: Add Cursor actor

Changed paths:
  A engines/mediastation/actors/cursor.cpp
  A engines/mediastation/actors/cursor.h
    engines/mediastation/context.cpp
    engines/mediastation/mediascript/scriptconstants.h
    engines/mediastation/module.mk


diff --git a/engines/mediastation/actors/cursor.cpp b/engines/mediastation/actors/cursor.cpp
new file mode 100644
index 00000000000..59521d786ba
--- /dev/null
+++ b/engines/mediastation/actors/cursor.cpp
@@ -0,0 +1,52 @@
+/* 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 "mediastation/actors/cursor.h"
+#include "mediastation/mediastation.h"
+
+namespace MediaStation {
+
+void CursorActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
+	switch (paramType) {
+	case kActorHeaderCursorResourceId:
+		_cursorId = chunk.readUint32LE();
+		break;
+
+	default:
+		Actor::readParameter(chunk, paramType);
+	}
+}
+
+ScriptValue CursorActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
+	ScriptValue returnValue;
+	switch (methodId) {
+	case kCursorSetMethod:
+		g_engine->getCursorManager()->setAsPermanent(_cursorId);
+		break;
+
+	default:
+		returnValue = Actor::callMethod(methodId, args);
+	}
+
+	return returnValue;
+}
+
+} // End of namespace MediaStation
diff --git a/engines/mediastation/actors/cursor.h b/engines/mediastation/actors/cursor.h
new file mode 100644
index 00000000000..c5a5cf752f6
--- /dev/null
+++ b/engines/mediastation/actors/cursor.h
@@ -0,0 +1,46 @@
+/* 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 MEDIASTATION_ACTORS_CURSOR_H
+#define MEDIASTATION_ACTORS_CURSOR_H
+
+#include "mediastation/actor.h"
+#include "mediastation/mediascript/scriptvalue.h"
+#include "mediastation/mediascript/scriptconstants.h"
+
+namespace MediaStation {
+
+// The cursor actor's only purpose seems to be activating its pre-set cursor
+// when the appropriate method is called.
+class CursorActor : public Actor {
+public:
+	CursorActor() : Actor(kActorTypeCursor) {};
+
+	virtual void readParameter(Chunk &chunk, ActorHeaderSectionType paramType) override;
+	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
+
+private:
+	uint _cursorId = 0;
+};
+
+} // End of namespace MediaStation
+
+#endif
diff --git a/engines/mediastation/context.cpp b/engines/mediastation/context.cpp
index b93ee4ba7cf..7cd09806d01 100644
--- a/engines/mediastation/context.cpp
+++ b/engines/mediastation/context.cpp
@@ -28,6 +28,7 @@
 #include "mediastation/mediascript/function.h"
 #include "mediastation/actors/camera.h"
 #include "mediastation/actors/canvas.h"
+#include "mediastation/actors/cursor.h"
 #include "mediastation/actors/palette.h"
 #include "mediastation/actors/image.h"
 #include "mediastation/actors/path.h"
@@ -155,6 +156,10 @@ void MediaStationEngine::readCreateActorData(Chunk &chunk) {
 		actor = new TextActor();
 		break;
 
+	case kActorTypeCursor:
+		actor = new CursorActor();
+		break;
+
 	default:
 		error("%s: No class for actor type 0x%x", __func__, static_cast<uint>(type));
 	}
diff --git a/engines/mediastation/mediascript/scriptconstants.h b/engines/mediastation/mediascript/scriptconstants.h
index 622e4385488..d6bcd7b7602 100644
--- a/engines/mediastation/mediascript/scriptconstants.h
+++ b/engines/mediastation/mediascript/scriptconstants.h
@@ -112,12 +112,6 @@ const char *builtInFunctionToStr(BuiltInFunction function);
 
 enum BuiltInMethod {
 	kInvalidMethod = 0,
-	// TODO: What object types does CursorSet apply to?
-	// Currently it's only in var_7be1_cursor_currentTool in
-	// IBM/Crayola.
-	kCursorSetMethod = 0xC8,
-
-	// SPATIAL ENTITY METHODS.
 	kSpatialHideMethod = 0xCB,
 	kSpatialMoveToMethod = 0xCC,
 	kSpatialMoveToByOffsetMethod = 0xCD,
@@ -250,6 +244,9 @@ enum BuiltInMethod {
 	// between two camera methods and two printer methods.
 	kOpenLensMethod = 0x15A,
 	kCloseLensMethod = 0x15B,
+
+	// CURSOR METHODS.
+	kCursorSetMethod = 0xC8,
 };
 const char *builtInMethodToStr(BuiltInMethod method);
 
diff --git a/engines/mediastation/module.mk b/engines/mediastation/module.mk
index 8f1d9a51e75..c8c7779b3fc 100644
--- a/engines/mediastation/module.mk
+++ b/engines/mediastation/module.mk
@@ -4,6 +4,7 @@ MODULE_OBJS = \
 	actor.o \
 	actors/camera.o \
 	actors/canvas.o \
+	actors/cursor.o \
 	actors/document.o \
 	actors/font.o \
 	actors/hotspot.o \


Commit: 8cd32db3f4ee9c6a0a14222c95ee595be04fb2f4
    https://github.com/scummvm/scummvm/commit/8cd32db3f4ee9c6a0a14222c95ee595be04fb2f4
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-02-24T20:49:55-05:00

Commit Message:
MEDIASTATION: Make Path actor more accurate

Changed paths:
    engines/mediastation/actors/path.cpp
    engines/mediastation/actors/path.h
    engines/mediastation/mediascript/scriptconstants.cpp
    engines/mediastation/mediascript/scriptconstants.h


diff --git a/engines/mediastation/actors/path.cpp b/engines/mediastation/actors/path.cpp
index 49954b1550e..b1028e78925 100644
--- a/engines/mediastation/actors/path.cpp
+++ b/engines/mediastation/actors/path.cpp
@@ -35,23 +35,22 @@ void PathActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 		_endPoint = chunk.readTypedPoint();
 		break;
 
-	case kActorHeaderStepRate: {
-		double _stepRateFloat = chunk.readTypedDouble();
-		// This should always be an integer anyway,
-		// so we'll cast away any fractional part.
-		_stepRate = static_cast<uint32>(_stepRateFloat);
+	case kActorHeaderPathTotalSteps:
+		_totalSteps = chunk.readTypedUint16();
 		break;
-	}
 
-	case kActorHeaderDuration:
-		// These are stored in the file as fractional seconds,
-		// but we want milliseconds.
-		_duration = static_cast<uint32>(chunk.readTypedTime() * 1000);
+	case kActorHeaderStepRate:
+		_stepRate = chunk.readTypedDouble();
 		break;
 
-	case kActorHeaderPathTotalSteps:
-		_totalSteps = chunk.readTypedUint16();
+	case kActorHeaderDuration: {
+		// These are stored in the file as fractional seconds,
+		// but we want milliseconds.
+		const uint MILLISECONDS_IN_ONE_SECOND = 1000;
+		_duration = chunk.readTypedTime() * MILLISECONDS_IN_ONE_SECOND;
+		_useTimeForCompletion = true;
 		break;
+	}
 
 	default:
 		Actor::readParameter(chunk, paramType);
@@ -62,104 +61,204 @@ ScriptValue PathActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptVa
 	ScriptValue returnValue;
 
 	switch (methodId) {
-	case kTimePlayMethod: {
+	case kTimePlayMethod:
+		ARGCOUNTCHECK(0);
+		startPath();
+		break;
+
+	case kTimeStopMethod:
 		ARGCOUNTCHECK(0);
-		timePlay();
-		return returnValue;
+		stopPath();
+		break;
+
+	case kPauseMethod:
+		ARGCOUNTCHECK(0);
+		pausePath();
+		break;
+
+	case kResumeMethod: {
+		ARGCOUNTRANGE(0, 1);
+		bool shouldRestart = false;
+		if (args.size() == 1) {
+			shouldRestart = args[0].asBool();
+		}
+		resumePath(shouldRestart);
+		break;
 	}
 
-	case kSetDurationMethod: {
+	case kGetLeftXMethod:
+		ARGCOUNTCHECK(0);
+		returnValue.setToFloat(_currentPoint.x);
+		break;
+
+	case kGetTopYMethod:
+		ARGCOUNTCHECK(0);
+		returnValue.setToFloat(_currentPoint.y);
+		break;
+
+	case kPathSetStartPointMethod:
+		ARGCOUNTCHECK(2);
+		_startPoint.x = static_cast<int16>(args[0].asFloat());
+		_startPoint.y = static_cast<int16>(args[1].asFloat());
+		break;
+
+	case kPathSetEndPointMethod:
+		ARGCOUNTCHECK(2);
+		_endPoint.x = static_cast<int16>(args[0].asFloat());
+		_endPoint.y = static_cast<int16>(args[1].asFloat());
+		break;
+
+	case kPathSetTotalStepsMethod:
+		ARGCOUNTCHECK(1);
+		_totalSteps = static_cast<uint>(args[0].asFloat());
+		_useTimeForCompletion = false;
+		break;
+
+	case kPathSetStepRateMethod:
 		ARGCOUNTCHECK(1);
-		uint durationInMilliseconds = static_cast<uint>(args[0].asTime() * 1000);
-		setDuration(durationInMilliseconds);
-		return returnValue;
+		_stepRate = static_cast<uint>(args[0].asFloat());
+		break;
+
+	case kPathSetDurationMethod: {
+		ARGCOUNTCHECK(1);
+		// Convert from seconds to milliseconds.
+		const uint MILLISECONDS_IN_ONE_SECOND = 1000;
+		_duration = args[0].asTime() * MILLISECONDS_IN_ONE_SECOND;
+		_useTimeForCompletion = true;
+		break;
 	}
 
-	case kPercentCompleteMethod: {
+	case kPathGetPercentCompleteMethod:
 		ARGCOUNTCHECK(0);
-		returnValue.setToFloat(percentComplete());
-		return returnValue;
-	}
+		if (_playState == kPathPlaying) {
+			returnValue.setToFloat(getPercentComplete());
+		} else {
+			returnValue.setToFloat(1.0);
+		}
+		break;
 
-	case kIsPlayingMethod: {
+	case kIsPlayingMethod:
 		ARGCOUNTCHECK(0);
-		returnValue.setToBool(_isPlaying);
-		return returnValue;
-	}
+		returnValue.setToBool(_playState == kPathPlaying || _playState == kPathPaused);
+		break;
+
+	case kIsPausedMethod:
+		ARGCOUNTCHECK(0);
+		returnValue.setToBool(_playState == kPathPaused);
+		break;
 
 	default:
-		return Actor::callMethod(methodId, args);
+		returnValue = Actor::callMethod(methodId, args);
 	}
+	return returnValue;
 }
 
-void PathActor::timePlay() {
-	if (_isPlaying) {
-		return;
+void PathActor::process() {
+	if (_playState == kPathPlaying) {
+		uint currentTime = g_system->getMillis();
+		if (currentTime >= _nextPathStepTime) {
+			timerEvent();
+		}
 	}
+}
+
+void PathActor::startPath() {
+	_playState = kPathPlaying;
+	_startTime = g_system->getMillis();
 
-	if (_duration == 0) {
-		warning("%s: Got zero duration", __func__);
-	} else if (_stepRate == 0) {
-		error("%s: Got zero step rate", __func__);
+	if (_stepRate <= 0.0) {
+		error("[%s] %s: Got zero or negative step rate", debugName(), __func__);
 	}
 
-	_isPlaying = true;
-	_startTime = g_system->getMillis();
-	_lastProcessedTime = 0;
-	_percentComplete = 0;
-	_nextPathStepTime = 0;
+	_currentPoint = _startPoint;
+	_stepDurationInMilliseconds = static_cast<uint>((1.0 / _stepRate) * 1000);
+	_nextPathStepTime = _startTime + _stepDurationInMilliseconds;
 	_currentStep = 0;
-	_totalSteps = (_duration * _stepRate) / 1000;
-	_stepDurationInMilliseconds = 1000 / _stepRate;
 
-	// TODO: Run the path start event. Haven't seen one the wild yet, don't know its ID.
-	debugC(5, kDebugScript, "Path::timePlay(): No PathStart event handler");
+	// There is no path start event handler.
 }
 
-void PathActor::process() {
-	if (!_isPlaying) {
-		return;
+void PathActor::stopPath() {
+	if (_playState == kPathPlaying || _playState == kPathPaused) {
+		_playState = kPathStopped;
+		runEventHandlerIfExists(kPathStoppedEvent);
 	}
+}
 
-	uint currentTime = g_system->getMillis();
-	uint pathTime = currentTime - _startTime;
+void PathActor::pausePath() {
+	if (_playState == kPathPlaying) {
+		_playState = kPathPaused;
+		_pauseTime = g_system->getMillis();
+	}
+}
+
+void PathActor::resumePath(bool shouldRestart) {
+	if (_playState == kPathPaused) {
+		// Calculate how long we were paused, to make sure we resume at the right point.
+		uint currentTime = g_system->getMillis();
+		uint pauseDuration = currentTime - _pauseTime;
+		_startTime += pauseDuration;
+		_playState = kPathPlaying;
+		scheduleNextTimerEvent();
+	} else if (_playState != kPathPlaying && shouldRestart) {
+		startPath();
+	}
+}
 
-	bool doNextStep = pathTime >= _nextPathStepTime;
-	if (!doNextStep) {
-		return;
+double PathActor::getPercentComplete() {
+	double percentComplete = 1.0;
+	if (!_useTimeForCompletion) {
+		if (_totalSteps > 0) {
+			percentComplete = static_cast<double>(_currentStep) / _totalSteps;
+		}
+	} else {
+		uint currentTime = g_system->getMillis();
+		if (currentTime > _startTime && _duration > 0) {
+			double timeElapsed = currentTime - _startTime;
+			percentComplete = timeElapsed / _duration;
+			if (percentComplete > 1.0) {
+				percentComplete = 1.0;
+			}
+		}
 	}
 
-	_percentComplete = static_cast<double>(_currentStep + 1) / _totalSteps;
-	debugC(2, kDebugScript, "Path::timePlay(): Step %d of %d", _currentStep, _totalSteps);
+	return percentComplete;
+}
 
-	if (_currentStep < _totalSteps) {
-		// TODO: Actually step the path. It seems they mostly just use this for
-		// palette animation in the On Step event handler, so nothing is actually drawn on the screen now.
+bool PathActor::step() {
+	double percentComplete = getPercentComplete();
+	if (percentComplete < 1.0) {
+		double nextX = _startPoint.x + (_endPoint.x - _startPoint.x) * percentComplete;
+		double nextY = _startPoint.y + (_endPoint.y - _startPoint.y) * percentComplete;
+		_currentPoint = Common::Point(static_cast<int16>(nextX), static_cast<int16>(nextY));
+		debugC(4, kDebugEvents, "[%s] %s: %f%% complete (startPoint: (%d, %d)) (endPoint: (%d, %d)) (currentPoint: (%d, %d))",
+			debugName(), __func__, percentComplete,
+			_endPoint.x, _endPoint.y, _startPoint.x, _startPoint.y, _currentPoint.x, _currentPoint.y);
 
 		// We don't run a step event for the last step.
 		runEventHandlerIfExists(kPathStepEvent);
-		_nextPathStepTime = ++_currentStep * _stepDurationInMilliseconds;
+		return false;
+	}
+	return true;
+}
+
+void PathActor::scheduleNextTimerEvent() {
+	_nextPathStepTime += _stepDurationInMilliseconds;
+}
+
+void PathActor::timerEvent() {
+	_currentStep += 1;
+	bool finishedPlaying = step();
+	if (!finishedPlaying) {
+		scheduleNextTimerEvent();
 	} else {
-		_isPlaying = false;
+		_playState = kPathStopped;
 		_percentComplete = 0;
 		_nextPathStepTime = 0;
 		_currentStep = 0;
-		_totalSteps = 0;
 		_stepDurationInMilliseconds = 0;
-
 		runEventHandlerIfExists(kPathEndEvent);
 	}
 }
 
-void PathActor::setDuration(uint durationInMilliseconds) {
-	// TODO: Do we need to save the original duration?
-	debugC(5, kDebugScript, "Path::setDuration(): Setting duration to %d ms", durationInMilliseconds);
-	_duration = durationInMilliseconds;
-}
-
-double PathActor::percentComplete() {
-	debugC(5, kDebugScript, "Path::percentComplete(): Returning percent complete %f%%", _percentComplete * 100);
-	return _percentComplete;
-}
-
 } // End of namespace MediaStation
diff --git a/engines/mediastation/actors/path.h b/engines/mediastation/actors/path.h
index 1e6c42b6efb..0ef45a14a51 100644
--- a/engines/mediastation/actors/path.h
+++ b/engines/mediastation/actors/path.h
@@ -28,6 +28,12 @@
 
 namespace MediaStation {
 
+enum PathPlayState {
+	kPathStopped = 1,
+	kPathPlaying = 2,
+	kPathPaused = 3,
+};
+
 class PathActor : public Actor {
 public:
 	PathActor() : Actor(kActorTypePath) {};
@@ -38,22 +44,31 @@ public:
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 
 private:
+	PathPlayState _playState = kPathStopped;
 	double _percentComplete = 0.0;
-	uint _totalSteps = 0;
+	bool _useTimeForCompletion = false;
+	double _duration = 0.0;
+	double _stepRate = 0.0;
+	uint _stepDurationInMilliseconds = 0;
 	uint _currentStep = 0;
+	uint _startTime = 0;
+	uint _pauseTime = 0;
+	uint _totalSteps = 0;
 	uint _nextPathStepTime = 0;
-	uint _stepDurationInMilliseconds = 0;
-	bool _isPlaying = false;
 
 	Common::Point _startPoint;
 	Common::Point _endPoint;
-	uint32 _stepRate = 0;
-	uint32 _duration = 0;
+	Common::Point _currentPoint;
+
+	void startPath();
+	void stopPath();
+	void pausePath();
+	void resumePath(bool shouldRestart);
 
-	// Method implementations.
-	void timePlay();
-	void setDuration(uint durationInMilliseconds);
-	double percentComplete();
+	double getPercentComplete();
+	bool step();
+	void timerEvent();
+	void scheduleNextTimerEvent();
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/mediascript/scriptconstants.cpp b/engines/mediastation/mediascript/scriptconstants.cpp
index 2e41e0dc9f4..793834fc1e6 100644
--- a/engines/mediastation/mediascript/scriptconstants.cpp
+++ b/engines/mediastation/mediascript/scriptconstants.cpp
@@ -328,10 +328,18 @@ const char *builtInMethodToStr(BuiltInMethod method) {
 		return "ContextLoadInProgress";
 	case kDocumentContextIsLoadedMethod:
 		return "IsLoaded";
-	case kSetDurationMethod:
+	case kPathSetDurationMethod:
 		return "SetDuration";
-	case kPercentCompleteMethod:
+	case kPathGetPercentCompleteMethod:
 		return "PercentComplete";
+	case kPathSetStartPointMethod:
+		return "SetStartPoint";
+	case kPathSetEndPointMethod:
+		return "SetEndPoint";
+	case kPathSetTotalStepsMethod:
+		return "SetTotalSteps";
+	case kPathSetStepRateMethod:
+		return "SetStepRate";
 	case kTextMethod:
 		return "Text";
 	case kSetTextMethod:
diff --git a/engines/mediastation/mediascript/scriptconstants.h b/engines/mediastation/mediascript/scriptconstants.h
index d6bcd7b7602..b4d5a4aada4 100644
--- a/engines/mediastation/mediascript/scriptconstants.h
+++ b/engines/mediastation/mediascript/scriptconstants.h
@@ -212,8 +212,12 @@ enum BuiltInMethod {
 	kDocumentContextIsLoadedMethod = 0x178,
 
 	// PATH METHODS.
-	kSetDurationMethod = 0x106,
-	kPercentCompleteMethod = 0x107,
+	kPathSetDurationMethod = 0x106,
+	kPathGetPercentCompleteMethod = 0x107,
+	kPathSetStartPointMethod = 0xf2,
+	kPathSetEndPointMethod = 0xf3,
+	kPathSetTotalStepsMethod = 0xf4,
+	kPathSetStepRateMethod = 0xf5,
 
 	// TEXT METHODS.
 	kTextMethod = 0x122,


Commit: 987b07ddde448bcd46409f0a0a340eef0aed0b51
    https://github.com/scummvm/scummvm/commit/987b07ddde448bcd46409f0a0a340eef0aed0b51
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-02-24T20:49:55-05:00

Commit Message:
MEDIASTATION: Implement more builtin script functions/methods

Most of these are necessary for Pocahontas minigames.

Changed paths:
    engines/mediastation/actor.cpp
    engines/mediastation/actor.h
    engines/mediastation/actors/hotspot.cpp
    engines/mediastation/actors/sound.cpp
    engines/mediastation/actors/sound.h
    engines/mediastation/actors/stage.cpp
    engines/mediastation/actors/stage.h
    engines/mediastation/audio.cpp
    engines/mediastation/audio.h
    engines/mediastation/graphics.cpp
    engines/mediastation/mediascript/codechunk.cpp
    engines/mediastation/mediascript/function.cpp
    engines/mediastation/mediascript/scriptconstants.cpp
    engines/mediastation/mediascript/scriptconstants.h


diff --git a/engines/mediastation/actor.cpp b/engines/mediastation/actor.cpp
index 0f7f661f367..3df9403c035 100644
--- a/engines/mediastation/actor.cpp
+++ b/engines/mediastation/actor.cpp
@@ -286,6 +286,16 @@ ScriptValue SpatialEntity::callMethod(BuiltInMethod methodId, Common::Array<Scri
 		returnValue.setToFloat(_zIndex);
 		break;
 
+	case kIsPointInsideMethod: {
+		ARGCOUNTCHECK(2);
+		int16 xToCheck = static_cast<int16>(args[0].asFloat());
+		int16 yToCheck = static_cast<int16>(args[1].asFloat());
+		Common::Point pointToCheck(xToCheck, yToCheck);
+		bool pointIsInside = getBbox().contains(pointToCheck);
+		returnValue.setToBool(pointIsInside);
+		break;
+	}
+
 	case kSetDissolveFactorMethod: {
 		ARGCOUNTCHECK(1);
 		double dissolveFactor = args[0].asFloat();
@@ -293,6 +303,22 @@ ScriptValue SpatialEntity::callMethod(BuiltInMethod methodId, Common::Array<Scri
 		break;
 	}
 
+	case kGetMouseXOffsetMethod: {
+		Common::Point mouseOffset;
+		currentMousePosition(mouseOffset);
+		mouseOffset -= _originalBoundingBox.origin();
+		returnValue.setToFloat(static_cast<double>(mouseOffset.x));
+		break;
+	}
+
+	case kGetMouseYOffsetMethod: {
+		Common::Point mouseOffset;
+		currentMousePosition(mouseOffset);
+		mouseOffset -= _originalBoundingBox.origin();
+		returnValue.setToFloat(static_cast<double>(mouseOffset.y));
+		break;
+	}
+
 	case kIsVisibleMethod:
 		ARGCOUNTCHECK(0);
 		returnValue.setToBool(isVisible());
@@ -392,6 +418,12 @@ void SpatialEntity::loadIsComplete() {
 	}
 }
 
+void SpatialEntity::currentMousePosition(Common::Point &point) {
+	if (_parentStage != nullptr) {
+		_parentStage->currentMousePosition(point);
+	}
+}
+
 void SpatialEntity::invalidateMouse() {
 	// TODO: Invalidate the mouse properly when we have custom events.
 	// For now, we simulate the mouse update event with a mouse moved event.
@@ -487,7 +519,9 @@ void SpatialEntity::invalidateLocalBounds() {
 }
 
 void SpatialEntity::invalidateLocalZIndex() {
-	warning("[%s] %s: STUB", debugName(), __func__);
+	if (_parentStage != nullptr) {
+		_parentStage->invalidateZIndexOf(this);
+	}
 }
 
 void SpatialEntity::setAdjustedBounds(CylindricalWrapMode alignmentMode) {
diff --git a/engines/mediastation/actor.h b/engines/mediastation/actor.h
index 1f69b662c83..fb0f36699be 100644
--- a/engines/mediastation/actor.h
+++ b/engines/mediastation/actor.h
@@ -244,6 +244,7 @@ public:
 	virtual Common::Rect getBbox() const { return _boundingBox; }
 	int zIndex() const { return _zIndex; }
 
+	virtual void currentMousePosition(Common::Point &point);
 	virtual void invalidateMouse();
 	virtual bool interactsWithMouse() const { return false; }
 
diff --git a/engines/mediastation/actors/hotspot.cpp b/engines/mediastation/actors/hotspot.cpp
index cccaabe2643..ef859235a40 100644
--- a/engines/mediastation/actors/hotspot.cpp
+++ b/engines/mediastation/actors/hotspot.cpp
@@ -107,9 +107,14 @@ ScriptValue HotspotActor::callMethod(BuiltInMethod methodId, Common::Array<Scrip
 		return returnValue;
 	}
 
-	case kIsActiveMethod: {
-		ARGCOUNTCHECK(0);
-		returnValue.setToBool(_isActive);
+
+	case kIsPointInsideMethod: {
+		ARGCOUNTCHECK(2);
+		int16 xToCheck = static_cast<int16>(args[0].asFloat());
+		int16 yToCheck = static_cast<int16>(args[1].asFloat());
+		Common::Point pointToCheck(xToCheck, yToCheck);
+		bool pointIsInside = isInside(pointToCheck);
+		returnValue.setToBool(pointIsInside);
 		return returnValue;
 	}
 
@@ -127,6 +132,12 @@ ScriptValue HotspotActor::callMethod(BuiltInMethod methodId, Common::Array<Scrip
 		return returnValue;
 	}
 
+	case kIsActiveMethod: {
+		ARGCOUNTCHECK(0);
+		returnValue.setToBool(_isActive);
+		return returnValue;
+	}
+
 	default:
 		return SpatialEntity::callMethod(methodId, args);
 	}
diff --git a/engines/mediastation/actors/sound.cpp b/engines/mediastation/actors/sound.cpp
index 6b5bded9ec8..15d1d5b699a 100644
--- a/engines/mediastation/actors/sound.cpp
+++ b/engines/mediastation/actors/sound.cpp
@@ -69,13 +69,13 @@ void SoundActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 }
 
 void SoundActor::process() {
-	if (!_isPlaying) {
+	if (_playState != kSoundPlaying) {
 		return;
 	}
 
 	processTimeEventHandlers();
 	if (!_sequence.isActive()) {
-		_isPlaying = false;
+		_playState = kSoundStopped;
 		_sequence.stop();
 		runEventHandlerIfExists(kSoundEndEvent);
 	}
@@ -97,53 +97,91 @@ ScriptValue SoundActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptV
 		ARGCOUNTCHECK(0);
 		return returnValue;
 
-	case kTimePlayMethod: {
+	case kTimePlayMethod:
 		ARGCOUNTCHECK(0);
-		timePlay();
+		start();
+		return returnValue;
+
+	case kTimeStopMethod:
+		ARGCOUNTCHECK(0);
+		stop();
 		return returnValue;
-	}
 
-	case kTimeStopMethod: {
+	case kPauseMethod:
 		ARGCOUNTCHECK(0);
-		timeStop();
+		pause();
+		return returnValue;
+
+	case kResumeMethod: {
+		ARGCOUNTRANGE(0, 1);
+		bool shouldRestart = false;
+		if (args.size() == 1) {
+			shouldRestart = args[0].asBool();
+		}
+		resume(shouldRestart);
 		return returnValue;
 	}
 
+	case kIsPlayingMethod:
+		returnValue.setToBool(_playState == kSoundPlaying || _playState == kSoundPaused);
+		return returnValue;
+
+	case kIsPausedMethod:
+		returnValue.setToBool(_playState == kSoundPaused);
+		return returnValue;
+
 	default:
 		return Actor::callMethod(methodId, args);
 	}
 }
 
-void SoundActor::timePlay() {
-	if (_streamFeed == nullptr && !_isLoadedFromChunk) {
-		_streamFeed = g_engine->getStreamFeedManager()->openStreamFeed(_id);
-		_streamFeed->readData();
-	}
+void SoundActor::start() {
+	if (_loadIsComplete) {
+		if (_playState == kSoundPlaying || _playState == kSoundPaused) {
+			stop();
+		}
 
-	if (_isPlaying) {
-		return;
+		openStream();
+		_playState = kSoundPlaying;
+		_startTime = g_system->getMillis();
+		_lastProcessedTime = 0;
+		_sequence.play();
+		runEventHandlerIfExists(kSoundBeginEvent);
+	} else {
+		warning("[%s] %s: Attempted to play sound before it was loaded", debugName(), __func__);
 	}
+}
 
-	if (_sequence.isEmpty()) {
-		_isPlaying = false;
-		return;
+void SoundActor::stop() {
+	if (_playState == kSoundPlaying || _playState == kSoundPaused) {
+		_playState = kSoundStopped;
+		_sequence.stop();
+		runEventHandlerIfExists(kSoundStoppedEvent);
 	}
+}
 
-	_isPlaying = true;
-	_startTime = g_system->getMillis();
-	_lastProcessedTime = 0;
-	_sequence.play();
-	runEventHandlerIfExists(kSoundBeginEvent);
+void SoundActor::pause() {
+	if (_playState == kSoundPlaying) {
+		_sequence.pause();
+		_playState = kSoundPaused;
+		// There don't seem to be script events to trigger in this instance.
+	}
 }
 
-void SoundActor::timeStop() {
-	if (!_isPlaying) {
-		return;
+void SoundActor::resume(bool restart) {
+	if (_playState == kSoundPaused) {
+		_sequence.resume();
+	} else if (restart) {
+		start();
 	}
+	// There don't seem to be script events to trigger in this instance.
+}
 
-	_isPlaying = false;
-	_sequence.stop();
-	runEventHandlerIfExists(kSoundStoppedEvent);
+void SoundActor::openStream() {
+	if (_streamFeed == nullptr && !_isLoadedFromChunk) {
+		_streamFeed = g_engine->getStreamFeedManager()->openStreamFeed(_id);
+		_streamFeed->readData();
+	}
 }
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/actors/sound.h b/engines/mediastation/actors/sound.h
index 416f2e277b3..23754a26a0c 100644
--- a/engines/mediastation/actors/sound.h
+++ b/engines/mediastation/actors/sound.h
@@ -30,6 +30,12 @@
 
 namespace MediaStation {
 
+enum SoundPlayState {
+	kSoundStopped = 1,
+	kSoundPlaying = 2,
+	kSoundPaused = 3,
+};
+
 class SoundActor : public Actor, public ChannelClient {
 public:
 	SoundActor() : Actor(kActorTypeSound) {};
@@ -46,12 +52,15 @@ private:
 	bool _isLoadedFromChunk = false;
 	uint _loadType = 0;
 	bool _hasOwnSubfile = false;
-	bool _isPlaying = false;
+	SoundPlayState _playState = kSoundStopped;
 	AudioSequence _sequence;
 
-	// Script method implementations
-	void timePlay();
-	void timeStop();
+	void start();
+	void stop();
+	void pause();
+	void resume(bool restart);
+
+	void openStream();
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/actors/stage.cpp b/engines/mediastation/actors/stage.cpp
index ca7193cafa0..7c9e0b79072 100644
--- a/engines/mediastation/actors/stage.cpp
+++ b/engines/mediastation/actors/stage.cpp
@@ -317,14 +317,16 @@ void StageActor::setCurrentCamera(CameraActor *camera) {
 ScriptValue StageActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
 	ScriptValue returnValue;
 	switch (methodId) {
-	case kAddActorToStageMethod: {
+	case kAddActorToStageMethod:
+	case kAddActorToStageMethod2: {
 		ARGCOUNTCHECK(1);
 		uint actorId = args[0].asActorId();
 		addActorToStage(actorId);
 		return returnValue;
 	}
 
-	case kRemoveActorFromStageMethod: {
+	case kRemoveActorFromStageMethod:
+	case kRemoveActorFromStageMethod2: {
 		ARGCOUNTCHECK(1);
 		uint actorId = args[0].asActorId();
 		removeActorFromStage(actorId);
diff --git a/engines/mediastation/actors/stage.h b/engines/mediastation/actors/stage.h
index 2cce143da20..d087745d807 100644
--- a/engines/mediastation/actors/stage.h
+++ b/engines/mediastation/actors/stage.h
@@ -91,7 +91,7 @@ public:
 		uint16 asciiCode,
 		uint16 eventMask,
 		MouseActorState &state) override;
-	virtual void currentMousePosition(Common::Point &point);
+	virtual void currentMousePosition(Common::Point &point) override;
 	virtual void setMousePosition(int16 x, int16 y) override;
 
 	void invalidateZIndexOf(const SpatialEntity *entity);
diff --git a/engines/mediastation/audio.cpp b/engines/mediastation/audio.cpp
index 97d3f662815..a28819e129c 100644
--- a/engines/mediastation/audio.cpp
+++ b/engines/mediastation/audio.cpp
@@ -50,6 +50,14 @@ void AudioSequence::play() {
 	}
 }
 
+void AudioSequence::pause() {
+	g_engine->_mixer->pauseHandle(_handle, true);
+}
+
+void AudioSequence::resume() {
+	g_engine->_mixer->pauseHandle(_handle, false);
+}
+
 void AudioSequence::stop() {
 	g_engine->_mixer->stopHandle(_handle);
 	_handle = Audio::SoundHandle();
diff --git a/engines/mediastation/audio.h b/engines/mediastation/audio.h
index 6a1d152af78..9b064cd1e7e 100644
--- a/engines/mediastation/audio.h
+++ b/engines/mediastation/audio.h
@@ -36,6 +36,8 @@ public:
 	~AudioSequence();
 
 	void play();
+	void pause();
+	void resume(); // Unpause
 	void stop();
 
 	void readParameters(Chunk &chunk);
diff --git a/engines/mediastation/graphics.cpp b/engines/mediastation/graphics.cpp
index dc1fb7cf274..ce35f62c492 100644
--- a/engines/mediastation/graphics.cpp
+++ b/engines/mediastation/graphics.cpp
@@ -864,8 +864,8 @@ void VideoDisplayManager::dissolveBlitRectsClip(
 	byte dissolveIndex = DISSOLVE_PATTERN_COUNT;
 	if (integralDissolveFactor != 50) {
 		dissolveIndex = ((integralDissolveFactor + 2) / 4) - 1;
-		dissolveIndex = CLIP<byte>(dissolveIndex, 0, (DISSOLVE_PATTERN_COUNT - 1));
 	}
+	dissolveIndex = CLIP<byte>(dissolveIndex, 0, (DISSOLVE_PATTERN_COUNT - 1));
 
 	Common::Rect destRect(Common::Rect(destPos, source->width(), source->height()));
 	for (const Common::Rect &dirtyRect : dirtyRegion) {
diff --git a/engines/mediastation/mediascript/codechunk.cpp b/engines/mediastation/mediascript/codechunk.cpp
index b2e619c7a03..dda70b3ed94 100644
--- a/engines/mediastation/mediascript/codechunk.cpp
+++ b/engines/mediastation/mediascript/codechunk.cpp
@@ -305,19 +305,23 @@ ScriptValue *CodeChunk::readAndReturnVariable() {
 	}
 
 	case kVariableScopeLocal: {
+		// The ID is actually a one-based index.
 		uint index = id - 1;
 		variable = &_locals.operator[](index);
 		break;
 	}
 
 	case kVariableScopeIndirectParameter: {
+		// The ID is actually a one-based index.
+		uint baseIndex = id - 1;
 		ScriptValue indexValue = evaluateExpression();
-		uint index = static_cast<uint>(indexValue.asFloat() + id);
+		uint index = static_cast<uint>(indexValue.asFloat()) + baseIndex;
 		variable = &_args->operator[](index);
 		break;
 	}
 
 	case kVariableScopeParameter: {
+		// The ID is actually a one-based index.
 		uint index = id - 1;
 		if (_args == nullptr) {
 			error("%s: Requested a parameter in a code chunk that has no parameters", __func__);
diff --git a/engines/mediastation/mediascript/function.cpp b/engines/mediastation/mediascript/function.cpp
index 76fff863a4c..1f74591612d 100644
--- a/engines/mediastation/mediascript/function.cpp
+++ b/engines/mediastation/mediascript/function.cpp
@@ -142,31 +142,37 @@ ScriptValue FunctionManager::call(uint functionId, Common::Array<ScriptValue> &a
 		break;
 
 	case kCurrentRunTimeFunction:
+	case kLegacy_GetCurrentRunTimeFunction:
 		FUNCARGCHECK(0);
 		script_CurrentRunTime(args, returnValue);
 		break;
 
 	case kSetGammaCorrectionFunction:
+	case kLegacy_SetGammaCorrectionFunction:
 		FUNCARGRANGE(1, 3);
 		script_SetGammaCorrection(args, returnValue);
 		break;
 
 	case kGetDefaultGammaCorrectionFunction:
+	case kLegacy_GetDefaultGammaCorrectionFunction:
 		FUNCARGCHECK(0);
 		script_GetDefaultGammaCorrection(args, returnValue);
 		break;
 
 	case kGetCurrentGammaCorrectionFunction:
+	case kLegacy_GetCurrentGammaCorrectionFunction:
 		FUNCARGCHECK(0);
 		script_GetCurrentGammaCorrection(args, returnValue);
 		break;
 
 	case kSetAudioVolumeFunction:
+	case kLegacy_SetAudioVolumeFunction:
 		FUNCARGCHECK(1);
 		script_SetAudioVolume(args, returnValue);
 		break;
 
 	case kGetAudioVolumeFunction:
+	case kLegacy_GetAudioVolumeFunction:
 		FUNCARGCHECK(0);
 		script_GetAudioVolume(args, returnValue);
 		break;
@@ -321,7 +327,11 @@ void FunctionManager::script_Random(Common::Array<ScriptValue> &args, ScriptValu
 }
 
 void FunctionManager::script_TimeOfDay(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
-	warning("STUB: TimeOfDay");
+	TimeDate timeDate;
+	// Calculate seconds since midnight.
+	g_system->getTimeAndDate(timeDate);
+	uint32 secondsSinceMidnight = (timeDate.tm_hour * 60 + timeDate.tm_min) * 60 + timeDate.tm_sec;
+	returnValue.setToTime(static_cast<double>(secondsSinceMidnight));
 }
 
 void FunctionManager::script_SquareRoot(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
@@ -362,10 +372,9 @@ void FunctionManager::script_GetUniqueRandom(Common::Array<ScriptValue> &args, S
 		SWAP(top, bottom);
 	}
 
-	// Build list of unused (non-excluded) numbers in the range. For this numeric type,
-	// everything is treated as an integer (even though it's stored as a double).
+	// Build list of unused (non-excluded) integers in the range.
 	Common::Array<double> unusedNumbers;
-	for (double currentValue = bottom; currentValue < top; currentValue += 1.0) {
+	for (double currentValue = bottom; currentValue <= top; currentValue += 1.0) {
 		// Check if this value appears in the exclusion list (args 2 onwards).
 		bool isExcluded = false;
 		for (uint i = 2; i < args.size(); i++) {
@@ -381,7 +390,7 @@ void FunctionManager::script_GetUniqueRandom(Common::Array<ScriptValue> &args, S
 	}
 
 	if (unusedNumbers.size() > 0) {
-		uint randomIndex = g_engine->_randomSource.getRandomNumberRng(0, unusedNumbers.size());
+		uint randomIndex = g_engine->_randomSource.getRandomNumberRng(0, unusedNumbers.size() - 1);
 		returnValue.setToFloat(unusedNumbers[randomIndex]);
 	} else {
 		warning("%s: No unused numbers to choose from", __func__);
diff --git a/engines/mediastation/mediascript/scriptconstants.cpp b/engines/mediastation/mediascript/scriptconstants.cpp
index 793834fc1e6..df59b46fc70 100644
--- a/engines/mediastation/mediascript/scriptconstants.cpp
+++ b/engines/mediastation/mediascript/scriptconstants.cpp
@@ -174,8 +174,20 @@ const char *builtInFunctionToStr(BuiltInFunction function) {
 		return "Legacy SquareRoot";
 	case kLegacy_GetUniqueRandomFunction:
 		return "Legacy GetUniqueRandom";
+	case kLegacy_GetCurrentRunTimeFunction:
+		return "Legacy GetCurrentRunTime";
+	case kLegacy_SetGammaCorrectionFunction:
+		return "Legacy SetGammaCorrection";
+	case kLegacy_GetDefaultGammaCorrectionFunction:
+		return "Legacy GetDefaultGammaCorrection";
+	case kLegacy_GetCurrentGammaCorrectionFunction:
+		return "Legacy GetCurrentGammaCorrection";
 	case kLegacy_DebugPrintFunction:
 		return "DebugPrint";
+	case kLegacy_SetAudioVolumeFunction:
+		return "Legacy SetAudioVolume";
+	case kLegacy_GetAudioVolumeFunction:
+		return "Legacy GetAudioVolume";
 	case kLegacy_SystemLanguagePreferenceFunction:
 		return "Legacy SystemLanguagePreference";
 	default:
@@ -185,6 +197,8 @@ const char *builtInFunctionToStr(BuiltInFunction function) {
 
 const char *builtInMethodToStr(BuiltInMethod method) {
 	switch (method) {
+	case kInvalidMethod:
+		return "Invalid";
 	case kCursorSetMethod:
 		return "CursorSet";
 	case kSpatialHideMethod:
@@ -285,8 +299,10 @@ const char *builtInMethodToStr(BuiltInMethod method) {
 	case kStageGetHeightMethod:
 		return "StageGetHeight";
 	case kAddToStageMethod:
+	case kAddActorToStageMethod2:
 		return "AddToStage\\OpenLens";
 	case kRemoveFromStageMethod:
+	case kRemoveActorFromStageMethod2:
 		return "RemoveFromStage\\CloseLens";
 	case kAddedToStageMethod:
 		return "AddedToStage";
diff --git a/engines/mediastation/mediascript/scriptconstants.h b/engines/mediastation/mediascript/scriptconstants.h
index b4d5a4aada4..6e356359482 100644
--- a/engines/mediastation/mediascript/scriptconstants.h
+++ b/engines/mediastation/mediascript/scriptconstants.h
@@ -105,7 +105,13 @@ enum BuiltInFunction {
 	kLegacy_PlatformFunction = 0x68,
 	kLegacy_SquareRootFunction = 0x69,
 	kLegacy_GetUniqueRandomFunction = 0x6A,
+	kLegacy_GetCurrentRunTimeFunction = 0x6B,
+	kLegacy_SetGammaCorrectionFunction = 0xAA,
+	kLegacy_GetDefaultGammaCorrectionFunction = 0xAB,
+	kLegacy_GetCurrentGammaCorrectionFunction = 0xAC,
 	kLegacy_DebugPrintFunction = 0xB4,
+	kLegacy_SetAudioVolumeFunction = 0xBE,
+	kLegacy_GetAudioVolumeFunction = 0xBF,
 	kLegacy_SystemLanguagePreferenceFunction = 0xC8,
 };
 const char *builtInFunctionToStr(BuiltInFunction function);
@@ -119,6 +125,8 @@ enum BuiltInMethod {
 	kSpatialShowMethod = 0xCA,
 	kTimePlayMethod = 0xCE,
 	kTimeStopMethod = 0xCF,
+	kTimePauseMethod = 0xD0,
+	kTimeResumeMethod = 0xD1,
 	kIsPlayingMethod = 0x174,
 	kSetDissolveFactorMethod = 0xF1,
 	kSpatialCenterMoveToMethod = 0xE6,
@@ -174,7 +182,9 @@ enum BuiltInMethod {
 	// NOTE: IDs 0xD2 and 0xD3 seem to be double-assigned
 	// between two hotspot methods and two stage methods.
 	kAddActorToStageMethod = 0xD2,
+	kAddActorToStageMethod2 = 0x170,
 	kRemoveActorFromStageMethod = 0xD3,
+	kRemoveActorFromStageMethod2 = 0x171,
 	kSetWorldSpaceExtentMethod = 0x16B,
 	kSetBoundsMethod = 0x11F,
 	kStageSetSizeMethod = 0x16B,


Commit: 62184a96ddcad35e5559781156f204156d138867
    https://github.com/scummvm/scummvm/commit/62184a96ddcad35e5559781156f204156d138867
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-02-24T20:49:55-05:00

Commit Message:
MEDIASTATION: Fix numerous inconsistencies with image data reading

Changed paths:
    engines/mediastation/actors/movie.cpp
    engines/mediastation/actors/movie.h
    engines/mediastation/actors/sprite.cpp
    engines/mediastation/actors/sprite.h
    engines/mediastation/bitmap.cpp
    engines/mediastation/bitmap.h
    engines/mediastation/datafile.h


diff --git a/engines/mediastation/actors/movie.cpp b/engines/mediastation/actors/movie.cpp
index d01ca3ccf7d..2284fc82213 100644
--- a/engines/mediastation/actors/movie.cpp
+++ b/engines/mediastation/actors/movie.cpp
@@ -54,12 +54,6 @@ bool StreamMovieProxy::isVisible() const {
 	return false;
 }
 
-MovieFrameInfo::MovieFrameInfo(Chunk &chunk) : ImageInfo(chunk) {
-	_index = chunk.readTypedUint32();
-	debugC(5, kDebugLoading, "%s: frame 0x%x", __func__, _index);
-	_keyframeEndInMilliseconds = chunk.readTypedUint32();
-}
-
 MovieFrame::MovieFrame(Chunk &chunk) {
 	if (g_engine->isFirstGenerationEngine()) {
 		blitType = static_cast<MovieBlitType>(chunk.readTypedUint16());
@@ -91,7 +85,9 @@ MovieFrame::MovieFrame(Chunk &chunk) {
 	}
 }
 
-MovieFrameImage::MovieFrameImage(Chunk &chunk, const MovieFrameInfo &header) : PixMapImage(chunk, header), _frameInfo(header) {
+MovieFrameImage::MovieFrameImage(Chunk &chunk, uint index, uint keyframeEndInMilliseconds, const ImageInfo &imageInfo) :
+	PixMapImage(chunk, imageInfo), _index(index), _keyframeEndInMilliseconds(keyframeEndInMilliseconds) {
+	debugC(5, kDebugLoading, "%s: frame 0x%x", __func__, _index);
 }
 
 StreamMovieActor::~StreamMovieActor() {
@@ -612,8 +608,10 @@ void StreamMovieActor::decompressIntoAuxImage(MovieFrame *frame) {
 }
 
 void StreamMovieActorFrames::readImageData(Chunk &chunk) {
-	MovieFrameInfo header(chunk);
-	MovieFrameImage *frame = new MovieFrameImage(chunk, header);
+	ImageInfo imageInfo(chunk);
+	uint index = chunk.readTypedUint32();
+	uint keyframeEndInMilliseconds = chunk.readTypedUint32();
+	MovieFrameImage *frame = new MovieFrameImage(chunk, index, keyframeEndInMilliseconds, imageInfo);
 	_images.push_back(frame);
 }
 
@@ -629,7 +627,7 @@ void StreamMovieActorFrames::readFrameData(Chunk &chunk) {
 		// same index, and frames are not necessarily in index order. So we'll
 		// do a linear search, which is how the original does it.
 		for (MovieFrameImage *image : _images) {
-			if (image->index() == frame->index) {
+			if (image->_index == frame->index) {
 				frame->image = image;
 				break;
 			}
@@ -637,7 +635,7 @@ void StreamMovieActorFrames::readFrameData(Chunk &chunk) {
 
 		if (frame->keyframeIndex != 0) {
 			for (MovieFrameImage *image : _images) {
-				if (image->index() == frame->keyframeIndex) {
+				if (image->_index == frame->keyframeIndex) {
 					frame->keyframeImage = image;
 					break;
 				}
diff --git a/engines/mediastation/actors/movie.h b/engines/mediastation/actors/movie.h
index b6c900c7612..fd017c1641b 100644
--- a/engines/mediastation/actors/movie.h
+++ b/engines/mediastation/actors/movie.h
@@ -39,26 +39,14 @@ enum MovieBlitType {
 	kCompressedDeltaMovieBlit = 3,
 };
 
-class MovieFrameInfo : public ImageInfo {
+class MovieFrameImage : public PixMapImage {
 public:
-	MovieFrameInfo() = default;
-	MovieFrameInfo(Chunk &chunk);
+	MovieFrameImage(Chunk &chunk, uint index, uint keyframeEndInMilliseconds, const ImageInfo &imageInfo);
 
 	uint _index = 0;
 	uint _keyframeEndInMilliseconds = 0;
 };
 
-class MovieFrameImage : public PixMapImage {
-public:
-	MovieFrameImage(Chunk &chunk, const MovieFrameInfo &header);
-	~MovieFrameImage() = default;
-
-	uint32 index() { return _frameInfo._index; }
-
-private:
-	MovieFrameInfo _frameInfo;
-};
-
 enum MovieSectionType {
 	kMovieRootSection = 0x06a8,
 	kMovieImageDataSection = 0x06a9,
diff --git a/engines/mediastation/actors/sprite.cpp b/engines/mediastation/actors/sprite.cpp
index c6f4f66cc8d..361a058e667 100644
--- a/engines/mediastation/actors/sprite.cpp
+++ b/engines/mediastation/actors/sprite.cpp
@@ -25,36 +25,17 @@
 
 namespace MediaStation {
 
-SpriteFrameInfo::SpriteFrameInfo(Chunk &chunk) : ImageInfo(chunk) {
-	_index = chunk.readTypedUint16();
-	_offset = chunk.readTypedPoint();
+SpriteMovieClip::SpriteMovieClip(uint clipId, int first, int last) :
+	id(clipId), firstFrameIndex(first), lastFrameIndex(last) {
 }
 
 Common::String SpriteMovieClip::getDebugString() const {
 	return Common::String::format("%s: [%d, %d]", g_engine->formatParamTokenName(id).c_str(), firstFrameIndex, lastFrameIndex);
 }
 
-SpriteFrame::SpriteFrame(Chunk &chunk, const SpriteFrameInfo &header) : PixMapImage(chunk, header), _frameInfo(header) {
-}
-
-uint32 SpriteFrame::left() {
-	return _frameInfo._offset.x;
-}
-
-uint32 SpriteFrame::top() {
-	return _frameInfo._offset.y;
-}
-
-Common::Point SpriteFrame::topLeft() {
-	return Common::Point(left(), top());
-}
-
-Common::Rect SpriteFrame::boundingBox() {
-	return Common::Rect(topLeft(), width(), height());
-}
-
-uint32 SpriteFrame::index() {
-	return _frameInfo._index;
+SpriteFrame::SpriteFrame(Chunk &chunk, uint index, Common::Point offset, const ImageInfo &imageInfo) :
+	PixMapImage(chunk, imageInfo), _index(index), _origin(offset) {
+	debugC(5, kDebugLoading, "%s: frame 0x%x", __func__, _index);
 }
 
 SpriteAsset::~SpriteAsset() {
@@ -88,7 +69,7 @@ void SpriteMovieActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramT
 		break;
 
 	case kActorHeaderSpriteChunkCount:
-		_asset->_frameCount = chunk.readTypedUint16();
+		_asset->frameCount = chunk.readTypedUint16();
 		break;
 
 	case kActorHeaderSpriteClip: {
@@ -118,13 +99,13 @@ void SpriteMovieActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramT
 
 void SpriteMovieActor::loadIsComplete() {
 	// This clip goes forward through all the sprite's frames.
-	SpriteMovieClip forwardClip(DEFAULT_FORWARD_CLIP_ID, 0, _asset->_frameCount - 1);
+	SpriteMovieClip forwardClip(DEFAULT_FORWARD_CLIP_ID, 0, _asset->frameCount - 1);
 	if (!_clips.contains(DEFAULT_FORWARD_CLIP_ID)) {
 		_clips.setVal(forwardClip.id, forwardClip);
 	}
 
 	// This clip goes backward through all the sprite's frames.
-	SpriteMovieClip backwardClip(DEFAULT_BACKWARD_CLIP_ID, _asset->_frameCount - 1, 0);
+	SpriteMovieClip backwardClip(DEFAULT_BACKWARD_CLIP_ID, _asset->frameCount - 1, 0);
 	if (!_clips.contains(DEFAULT_BACKWARD_CLIP_ID)) {
 		_clips.setVal(backwardClip.id, backwardClip);
 	}
@@ -309,7 +290,7 @@ void SpriteMovieActor::setCurrentClip(uint clipId) {
 		if (_clips.contains(clipId)) {
 			SpriteMovieClip newClip = _clips.getVal(clipId);
 			debugC(3, kDebugSpriteMovie, "[%s] %s: (frameCount: %d) activeClip: %s; newClip: %s",
-				debugName(), __func__, _asset->_frameCount, _activeClip.getDebugString().c_str(), newClip.getDebugString().c_str());
+				debugName(), __func__, _asset->frameCount, _activeClip.getDebugString().c_str(), newClip.getDebugString().c_str());
 			_activeClip = _clips.getVal(clipId);
 		} else {
 			_activeClip.id = clipId;
@@ -346,15 +327,17 @@ void SpriteMovieActor::process() {
 }
 
 void SpriteMovieActor::readChunk(Chunk &chunk) {
-	// Reads one frame from the sprite.
-	SpriteFrameInfo header(chunk);
-	SpriteFrame *frame = new SpriteFrame(chunk, header);
+	// Read one frame from the sprite.
+	ImageInfo imageInfo(chunk);
+	uint index = chunk.readTypedUint16();
+	Common::Point offset = chunk.readTypedPoint();
+	SpriteFrame *frame = new SpriteFrame(chunk, index, offset, imageInfo);
 	_asset->frames.push_back(frame);
 
 	// TODO: Are these in exactly reverse order? If we can just reverse the
 	// whole thing once.
 	Common::sort(_asset->frames.begin(), _asset->frames.end(), [](SpriteFrame *a, SpriteFrame *b) {
-		return a->index() < b->index();
+		return a->_index < b->_index;
 	});
 }
 
@@ -448,11 +431,10 @@ void SpriteMovieActor::draw(DisplayContext &displayContext) {
 
 	SpriteFrame *activeFrame = _asset->frames[_currentFrameIndex];
 	if (_isVisible) {
-		Common::Rect frameBbox = activeFrame->boundingBox();
-		frameBbox.translate(_boundingBox.left, _boundingBox.top);
-		debugC(8, kDebugSpriteMovie, "[%s] %s: frame %d",
-			debugName(), __func__, _currentFrameIndex);
-		g_engine->getDisplayManager()->imageBlit(frameBbox.origin(), activeFrame, _dissolveFactor, &displayContext);
+		Common::Point originToDraw = _boundingBox.origin() + activeFrame->_origin;
+		debugC(7, kDebugSpriteMovie, "[%s] %s: frame %d (%d, %d)",
+			debugName(), __func__, activeFrame->_index, originToDraw.x, originToDraw.y);
+		g_engine->getDisplayManager()->imageBlit(originToDraw, activeFrame, _dissolveFactor, &displayContext);
 	}
 }
 
diff --git a/engines/mediastation/actors/sprite.h b/engines/mediastation/actors/sprite.h
index c805ee32325..9c026931073 100644
--- a/engines/mediastation/actors/sprite.h
+++ b/engines/mediastation/actors/sprite.h
@@ -35,37 +35,21 @@
 namespace MediaStation {
 
 struct SpriteMovieClip {
-	uint id = 0;
-	int firstFrameIndex = 0;
-	int lastFrameIndex = 0;
-
 	SpriteMovieClip() = default;
-	SpriteMovieClip(uint clipId, int first, int last)
-		: id(clipId), firstFrameIndex(first), lastFrameIndex(last) {}
+	SpriteMovieClip(uint clipId, int first, int last);
 	Common::String getDebugString() const;
-};
-
-class SpriteFrameInfo : public ImageInfo {
-public:
-	SpriteFrameInfo() = default;
-	SpriteFrameInfo(Chunk &chunk);
 
-	uint _index;
-	Common::Point _offset;
+	uint id = 0;
+	int firstFrameIndex = 0;
+	int lastFrameIndex = 0;
 };
 
 class SpriteFrame : public PixMapImage {
 public:
-	SpriteFrame(Chunk &chunk, const SpriteFrameInfo &frameInfo);
-
-	uint32 left();
-	uint32 top();
-	Common::Point topLeft();
-	Common::Rect boundingBox();
-	uint32 index();
+	SpriteFrame(Chunk &chunk, uint index, Common::Point origin, const ImageInfo &imageInfo);
 
-private:
-	SpriteFrameInfo _frameInfo;
+	int _index = 0;
+	Common::Point _origin;
 };
 
 // The original had a separate class that did reference counting,
@@ -73,7 +57,7 @@ private:
 struct SpriteAsset {
 	~SpriteAsset();
 
-	uint _frameCount = 0;
+	uint frameCount = 0;
 	Common::Array<SpriteFrame *> frames;
 };
 
@@ -103,7 +87,7 @@ private:
 	Common::HashMap<uint, SpriteMovieClip> _clips;
 	Common::SharedPtr<SpriteAsset> _asset;
 	bool _isPlaying = false;
-	uint _currentFrameIndex = 0;
+	int _currentFrameIndex = 0;
 	uint _nextFrameTime = 0;
 	uint _defaultClipId = DEFAULT_FORWARD_CLIP_ID;
 	SpriteMovieClip _activeClip;
diff --git a/engines/mediastation/bitmap.cpp b/engines/mediastation/bitmap.cpp
index 4b86d6f74eb..21b92b658a3 100644
--- a/engines/mediastation/bitmap.cpp
+++ b/engines/mediastation/bitmap.cpp
@@ -25,12 +25,12 @@
 namespace MediaStation {
 
 ImageInfo::ImageInfo(Chunk &chunk) {
-	uint headerSizeInBytes = chunk.readTypedUint16();
+	_imageDataStartOffset = chunk.readTypedUint16();
 	_dimensions = chunk.readTypedGraphicSize();
 	_compressionType = static_cast<BitmapCompressionType>(chunk.readTypedUint16());
 	_stride = chunk.readTypedUint16();
-	debugC(5, kDebugLoading, "%s: headerSize: %d, _compressionType: 0x%x, _stride: %d",
-		__func__, headerSizeInBytes, static_cast<uint>(_compressionType), _stride);
+	debugC(5, kDebugLoading, "%s: imageDataStartOffset: 0x%x, _compressionType: 0x%x, _stride: %d",
+		__func__, _imageDataStartOffset, static_cast<uint>(_compressionType), _stride);
 }
 
 PixMapImage::PixMapImage(Chunk &chunk, const ImageInfo &imageInfo) : _imageInfo(imageInfo) {
@@ -38,7 +38,10 @@ PixMapImage::PixMapImage(Chunk &chunk, const ImageInfo &imageInfo) : _imageInfo(
 		warning("%s: Got stride less than width", __func__);
 	}
 
-	_unk1 = chunk.readUint16LE();
+	// Make sure we are at the start of the image data.
+	uint imageDataStartPos = chunk.startPos() + _imageInfo._imageDataStartOffset;
+	chunk.seek(imageDataStartPos);
+
 	if (chunk.bytesRemaining() > 0) {
 		if (isCompressed()) {
 			_compressedStream = chunk.readStream(chunk.bytesRemaining());
diff --git a/engines/mediastation/bitmap.h b/engines/mediastation/bitmap.h
index 3182db65dba..d99004ac76b 100644
--- a/engines/mediastation/bitmap.h
+++ b/engines/mediastation/bitmap.h
@@ -46,6 +46,7 @@ public:
 	Common::Point _dimensions;
 	BitmapCompressionType _compressionType = kUncompressedBitmap;
 	int16 _stride = 0;
+	uint _imageDataStartOffset = 0;
 };
 
 class PixMapImage {
@@ -65,7 +66,6 @@ public:
 
 private:
 	ImageInfo _imageInfo;
-	uint _unk1 = 0;
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/datafile.h b/engines/mediastation/datafile.h
index 798c6441f45..9afe2b49063 100644
--- a/engines/mediastation/datafile.h
+++ b/engines/mediastation/datafile.h
@@ -105,6 +105,7 @@ public:
 	Chunk(Common::SeekableReadStream *stream);
 
 	uint32 bytesRemaining();
+	uint32 startPos() const { return _dataStartOffset; }
 
 	uint32 _id = 0;
 	uint32 _length = 0;


Commit: 64fb8b2037cb0c2f3aa88d19a39d846fcbc2c877
    https://github.com/scummvm/scummvm/commit/64fb8b2037cb0c2f3aa88d19a39d846fcbc2c877
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-02-24T21:10:49-05:00

Commit Message:
MEDIASTATION: Add initial support for Canvas and Text actors

Changed paths:
    engines/mediastation/actor.h
    engines/mediastation/actors/canvas.cpp
    engines/mediastation/actors/canvas.h
    engines/mediastation/actors/font.cpp
    engines/mediastation/actors/font.h
    engines/mediastation/actors/text.cpp
    engines/mediastation/actors/text.h
    engines/mediastation/graphics.cpp
    engines/mediastation/graphics.h
    engines/mediastation/mediascript/scriptconstants.cpp
    engines/mediastation/mediascript/scriptconstants.h


diff --git a/engines/mediastation/actor.h b/engines/mediastation/actor.h
index fb0f36699be..f4d6da1edb0 100644
--- a/engines/mediastation/actor.h
+++ b/engines/mediastation/actor.h
@@ -96,7 +96,6 @@ enum ActorHeaderSectionType {
 	kActorHeaderScaleXAndY = 0x77a,
 	kActorHeaderScaleX = 0x77c,
 	kActorHeaderScaleY = 0x77d,
-	kActorHeaderUnk0 = 0x7d0,
 	kActorHeaderActorName = 0x0bb8,
 	kStreamMovieProxyInfo = 0x06ac,
 
@@ -113,10 +112,8 @@ enum ActorHeaderSectionType {
 	kActorHeaderCameraImageActor = 0x77b,
 
 	// CANVAS FIELDS.
-	kActorHeaderCanvasUnk1 = 0x491,
 	kActorHeaderCanvasDissolveFactor = 0x493,
-	kActorHeaderCanvasUnk2 = 0x494,
-	kActorHeaderCanvasUnk3 = 0x495,
+	kActorHeaderCanvasTransparency = 0x7d0,
 
 	// STAGE FIELDS.
 	kActorHeaderStageExtent = 0x0771,
@@ -125,14 +122,16 @@ enum ActorHeaderSectionType {
 
 	// TEXT FIELDS.
 	kActorHeaderEditable = 0x03eb,
-	kActorHeaderFontId = 0x0258,
+	kActorHeaderFontActorId = 0x0258,
 	kActorHeaderInitialText = 0x0259,
 	kActorHeaderTextMaxLength = 0x25a,
 	kActorHeaderTextJustification = 0x025b,
 	kActorHeaderTextPosition = 0x25f,
-	kActorHeaderTextUnk1 = 0x262,
-	kActorHeaderTextUnk2 = 0x263,
-	kActorHeaderTextCharacterClass = 0x0266,
+	kActorHeaderTextCursorIsVisible = 0x262,
+	kActorHeaderTextConstrainToWidth = 0x263,
+	kActorHeaderTextOverwriteMode = 0x264,
+	kActorHeaderTextAcceptedCharRange = 0x265,
+	kActorHeaderTextAcceptedCharRangeWithOffset = 0x0266,
 
 	// SPRITE FIELDS.
 	kActorHeaderSpriteClip = 0x03e9,
@@ -243,6 +242,7 @@ public:
 	virtual bool isVisible() const { return _isVisible; }
 	virtual Common::Rect getBbox() const { return _boundingBox; }
 	int zIndex() const { return _zIndex; }
+	void moveTo(int16 x, int16 y);
 
 	virtual void currentMousePosition(Common::Point &point);
 	virtual void invalidateMouse();
@@ -276,7 +276,7 @@ public:
 protected:
 	uint _stageId = 0;
 	int _zIndex = 0;
-	double _dissolveFactor = 0.0;
+	double _dissolveFactor = 1.0;
 	double _scaleX = 0.0;
 	double _scaleY = 0.0;
 	Common::Rect _boundingBox;
@@ -286,7 +286,6 @@ protected:
 	bool _getOffstageEvents = false;
 	StageActor *_parentStage = nullptr;
 
-	void moveTo(int16 x, int16 y);
 	void moveToCentered(int16 x, int16 y);
 	void setBounds(const Common::Rect &bounds);
 	void setZIndex(int zIndex);
diff --git a/engines/mediastation/actors/canvas.cpp b/engines/mediastation/actors/canvas.cpp
index 57550ca4dd1..4b8a923faaa 100644
--- a/engines/mediastation/actors/canvas.cpp
+++ b/engines/mediastation/actors/canvas.cpp
@@ -20,28 +20,195 @@
  */
 
 #include "mediastation/actors/canvas.h"
+#include "mediastation/actors/image.h"
+#include "mediastation/actors/stage.h"
+#include "mediastation/debugchannels.h"
+#include "mediastation/mediastation.h"
 
 namespace MediaStation {
 
 void CanvasActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 	switch (paramType) {
+	case kActorHeaderChannelIdent:
+		// The original seems to read this and then throws it away!
+		_channelIdent = chunk.readTypedChannelIdent();
+		break;
+
 	case kActorHeaderStartup:
 		_isVisible = static_cast<bool>(chunk.readTypedByte());
 		break;
 
+	case kActorHeaderTransparency:
+	case kActorHeaderCanvasTransparency:
+		_hasTransparency = static_cast<bool>(chunk.readTypedByte());
+		break;
+
+	case kActorHeaderDissolveFactor:
+		_dissolveFactor = chunk.readTypedDouble();
+		break;
+
+	case kActorHeaderX:
+		_offset.x = chunk.readTypedGraphicUnit();
+		break;
+
+	case kActorHeaderY:
+		_offset.y = chunk.readTypedGraphicUnit();
+		break;
+
+	case kActorHeaderLoadType:
+		// The original seems to read this and then throws it away!
+		chunk.readByte();
+		break;
+
 	default:
 		SpatialEntity::readParameter(chunk, paramType);
 	}
 }
 
 ScriptValue CanvasActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
+	ScriptValue returnValue;
 	switch (methodId) {
-	case kClearToPaletteMethod: {
-		error("%s: clearToPalette is not implemented yet", __func__);
+	case kSpatialShowMethod:
+		ARGCOUNTCHECK(0);
+		setVisibility(true);
+		break;
+
+	case kSpatialHideMethod:
+		ARGCOUNTCHECK(0);
+		setVisibility(false);
+		break;
+
+	case kCanvasClearToTransparencyMethod:
+		ARGCOUNTCHECK(0);
+		clearToTransparency();
+		break;
+
+	case kCanvasStampImageMethod: {
+		ARGCOUNTCHECK(3);
+		int16 x = static_cast<int16>(args[0].asFloat());
+		int16 y = static_cast<int16>(args[1].asFloat());
+		Common::Point stampPosition(x, y);
+		uint actorId = args[2].asActorId();
+		stampImage(stampPosition, actorId);
+		break;
+	}
+
+	case kCanvasCopyScreenToMethod: {
+		ARGCOUNTCHECK(2);
+		int16 x = static_cast<int16>(args[0].asFloat());
+		int16 y = static_cast<int16>(args[1].asFloat());
+		Common::Point dest(x, y);
+		copyScreenTo(dest);
+		break;
+	}
+
+	case kCanvasClearToPaletteMethod: {
+		ARGCOUNTCHECK(1);
+		uint colorIndex = static_cast<uint>(args[0].asFloat());
+		clearToPalette(colorIndex);
+		break;
 	}
 
 	default:
-		return SpatialEntity::callMethod(methodId, args);
+		returnValue = SpatialEntity::callMethod(methodId, args);
+	}
+	return returnValue;
+}
+
+void CanvasActor::loadIsComplete() {
+	ImageInfo imageInfo;
+	if (_hasTransparency) {
+		imageInfo._compressionType = kUncompressedTransparentBitmap;
+	} else {
+		imageInfo._compressionType = kUncompressedBitmap;
+	}
+
+	imageInfo._dimensions = Common::Point(getBbox().width(), getBbox().height());
+	imageInfo._stride = getBbox().width();
+
+	_image = Common::SharedPtr<ImageAsset>(new ImageAsset);
+	_image->bitmap = new PixMapImage(imageInfo);
+	if (_hasTransparency) {
+		fillCanvas(0x00);
+	} else {
+		fillCanvas(0xFE);
+	}
+
+	SpatialEntity::loadIsComplete();
+}
+
+void CanvasActor::setVisibility(bool visibility) {
+	if (visibility != _isVisible) {
+		_isVisible = visibility;
+		invalidateLocalBounds();
+	}
+}
+
+void CanvasActor::fillCanvas(uint paletteIndex) {
+	if (_image != nullptr && _image->bitmap != nullptr) {
+		Graphics::ManagedSurface &surface = _image->bitmap->_image;
+		surface.fillRect(Common::Rect(0, 0, surface.w, surface.h), paletteIndex);
+	}
+}
+
+void CanvasActor::clearToTransparency() {
+	fillCanvas(0);
+	invalidateLocalBounds();
+}
+
+void CanvasActor::stampImage(const Common::Point &dest, uint actorId) {
+	// Set up the display context to draw to the canvas's image surface.
+	if (_image != nullptr) {
+		_displayContext._destImage = &_image->bitmap->_image;
+		if (_displayContext._destImage == nullptr) {
+			_displayContext.deleteClips();
+		} else {
+			_displayContext.verifyClipSize();
+		}
+	}
+
+	// Although this method is named stampImage, it can actually stamp other spatial entities too.
+	debugC(5, kDebugGraphics, "[%s] %s: %s at (%d, %d)",
+		debugName(), __func__, g_engine->formatActorName(actorId).c_str(), dest.x, dest.y);
+	SpatialEntity *imageToStamp = g_engine->getSpatialEntityById (actorId);
+	Common::Point imageToStampOriginalBoundsOrigin = imageToStamp->getBbox().origin();
+	imageToStamp->moveTo(dest.x, dest.y);
+
+	// The idea is to draw the entity in the proper place on the canvas (with a manufactured dirty region
+	// so the draw actually happens), then restore the entity to its original place.
+	Region dirtyRegion;
+	Common::Rect imageToStampBounds = imageToStamp->getBbox();
+	dirtyRegion.addRect(imageToStampBounds);
+	if (_displayContext._destImage != nullptr) {
+		_displayContext.addClip();
+	}
+	_displayContext.setClipTo(dirtyRegion);
+	imageToStamp->draw(_displayContext);
+
+	// Clean up display context if we're drawing to our own image.
+	_displayContext.emptyCurrentClip();
+	if (_image != nullptr && &_image->bitmap->_image == _displayContext._destImage) {
+		_displayContext._destImage = nullptr;
+		_displayContext.deleteClips();
+	}
+
+	imageToStamp->moveTo(imageToStampOriginalBoundsOrigin.x, imageToStampOriginalBoundsOrigin.y);
+	invalidateLocalBounds();
+}
+
+void CanvasActor::copyScreenTo(const Common::Point &dest) {
+	warning("[%s] %s: STUB", debugName(), __func__);
+}
+
+void CanvasActor::clearToPalette(uint colorIndex) {
+	fillCanvas(colorIndex);
+	invalidateLocalBounds();
+}
+
+void CanvasActor::draw(DisplayContext &displayContext) {
+	if (_image != nullptr) {
+		Common::Point drawPosition = getBbox().origin() + _offset;
+		g_engine->getDisplayManager()->imageBlit(drawPosition, _image->bitmap, _dissolveFactor, &displayContext, nullptr, true);
 	}
 }
 
diff --git a/engines/mediastation/actors/canvas.h b/engines/mediastation/actors/canvas.h
index 904bc5b9e5b..c607d4d24b5 100644
--- a/engines/mediastation/actors/canvas.h
+++ b/engines/mediastation/actors/canvas.h
@@ -22,18 +22,41 @@
 #ifndef MEDIASTATION_CANVAS_H
 #define MEDIASTATION_CANVAS_H
 
+#include "common/rect.h"
+
 #include "mediastation/actor.h"
+#include "mediastation/graphics.h"
 #include "mediastation/mediascript/scriptvalue.h"
 #include "mediastation/mediascript/scriptconstants.h"
 
 namespace MediaStation {
 
-class CanvasActor : public SpatialEntity {
+struct ImageAsset;
+
+class CanvasActor : public SpatialEntity, public ChannelClient {
 public:
-	CanvasActor() : SpatialEntity(kActorTypeCanvas) {};
+	CanvasActor() : SpatialEntity(kActorTypeCanvas) {
+		_dissolveFactor = 1.0;
+		_isVisible = true;
+		_hasTransparency = true;
+	}
 
 	virtual void readParameter(Chunk &chunk, ActorHeaderSectionType paramType) override;
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
+	virtual void loadIsComplete() override;
+	virtual void draw(DisplayContext &displayContext) override;
+
+private:
+	Common::Point _offset;
+	Common::SharedPtr<ImageAsset> _image;
+	DisplayContext _displayContext;
+
+	void setVisibility(bool visibility);
+	void fillCanvas(uint paletteIndex);
+	void clearToTransparency();
+	void stampImage(const Common::Point &dest, uint actorId);
+	void copyScreenTo(const Common::Point &dest);
+	void clearToPalette(uint colorIndex);
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/actors/font.cpp b/engines/mediastation/actors/font.cpp
index 72cd2f1e604..68d3b8dd4d8 100644
--- a/engines/mediastation/actors/font.cpp
+++ b/engines/mediastation/actors/font.cpp
@@ -21,21 +21,23 @@
 
 #include "mediastation/debugchannels.h"
 #include "mediastation/actors/font.h"
+#include "mediastation/mediastation.h"
 
 namespace MediaStation {
 
-FontGlyph::FontGlyph(Chunk &chunk, uint asciiCode, int unk1, int unk2, const ImageInfo &header) : PixMapImage(chunk, header) {
-	_asciiCode = asciiCode;
-	_unk1 = unk1;
-	_unk2 = unk2;
+FontCharacter::FontCharacter(Chunk &chunk, uint charCode, int horizontalSpacing, int baselineOffset, const ImageInfo &header) :
+	PixMapImage(chunk, header),
+	_charCode(charCode),
+	_horizontalSpacing(horizontalSpacing),
+	_baselineOffset(baselineOffset) {
 }
 
 FontActor::~FontActor() {
 	unregisterWithStreamManager();
-	for (auto it = _glyphs.begin(); it != _glyphs.end(); ++it) {
+	for (auto it = _characters.begin(); it != _characters.end(); ++it) {
 		delete it->_value;
 	}
-	_glyphs.clear();
+	_characters.clear();
 }
 
 void FontActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
@@ -51,16 +53,35 @@ void FontActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 }
 
 void FontActor::readChunk(Chunk &chunk) {
-	debugC(5, kDebugLoading, "FontActor::readChunk(): Reading font glyph (@0x%llx)", static_cast<long long int>(chunk.pos()));
-	uint asciiCode = chunk.readTypedUint16();
-	int unk1 = chunk.readTypedUint16();
-	int unk2 = chunk.readTypedUint16();
+	// This is always 16-bit because there are some special char codes above 0xFF,
+	// such as the cursor and arrow keys.
+	uint charCode = chunk.readTypedUint16();
+	int16 horizontalSpacing = static_cast<int16>(chunk.readTypedUint16());
+	int16 baselineOffset = static_cast<int16>(chunk.readTypedUint16());
 	ImageInfo header(chunk);
-	FontGlyph *glyph = new FontGlyph(chunk, asciiCode, unk1, unk2, header);
-	if (_glyphs.getValOrDefault(asciiCode) != nullptr) {
-		error("%s: Glyph for ASCII code 0x%x already exists", __func__, asciiCode);
+	FontCharacter *glyph = new FontCharacter(chunk, charCode, horizontalSpacing, baselineOffset, header);
+	if (_characters.getValOrDefault(charCode) != nullptr) {
+		warning("[%s] %s: Glyph for char code 0x%x already exists", debugName(), __func__, charCode);
 	}
-	_glyphs.setVal(asciiCode, glyph);
+	_characters.setVal(charCode, glyph);
+	_totalHeightOfAllChars += glyph->height();
+	_totalWidthOfAllChars += glyph->width();
+
+	// Track the maximum ascent across all glyphs.
+	// The ascent is either the specified baseline offset, or the full glyph height if not specified.
+	_maxAscent = MAX(_maxAscent, glyph->ascent());
+
+	// Calculate descent (distance from baseline to bottom of glyph)
+	int16 charDescent = glyph->height() - glyph->ascent();
+	_maxDescent = MAX(_maxDescent, charDescent);
+}
+
+void FontActor::loadIsComplete() {
+	if (_characters.size() > 0) {
+		_averageCharWidth = _totalWidthOfAllChars / _characters.size();
+		_averageCharHeight = _totalHeightOfAllChars / _characters.size();
+	}
+	Actor::loadIsComplete();
 }
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/actors/font.h b/engines/mediastation/actors/font.h
index 654f269fca1..ea6638aac16 100644
--- a/engines/mediastation/actors/font.h
+++ b/engines/mediastation/actors/font.h
@@ -30,12 +30,17 @@
 
 namespace MediaStation {
 
-class FontGlyph : public PixMapImage {
+class FontCharacter : public PixMapImage {
 public:
-	FontGlyph(Chunk &chunk, uint asciiCode, int unk1, int unk2, const ImageInfo &header);
-	uint _asciiCode = 0;
-	int _unk1 = 0;
-	int _unk2 = 0;
+	FontCharacter(Chunk &chunk, uint charCode, int horizontalSpacing, int baselineOffset, const ImageInfo &header);
+
+	// Returns the ascent (baseline position). Falls back to full height if baseline offset is not specified.
+	int16 ascent() const { return (_baselineOffset != 0) ? _baselineOffset : height(); }
+	uint _charCode = 0;
+	int16 _horizontalSpacing = 0; // Additional horizontal spacing added after the glyph width to get total advance
+
+private:
+	int16 _baselineOffset = 0;    // Baseline position within the glyph bitmap (ascent - distance from top edge to baseline)
 };
 
 class FontActor : public Actor, public ChannelClient {
@@ -45,9 +50,19 @@ public:
 
 	virtual void readParameter(Chunk &chunk, ActorHeaderSectionType paramType) override;
 	virtual void readChunk(Chunk &chunk) override;
+	virtual void loadIsComplete() override;
+
+	FontCharacter *lookupCharacter(uint charCode) { return _characters.getValOrDefault(charCode); }
+
+	int16 _totalWidthOfAllChars = 0;
+	int16 _totalHeightOfAllChars = 0;
+	int16 _averageCharWidth = 0;
+	int16 _averageCharHeight = 0;
+	int16 _maxAscent = 0;  // Maximum ascent (distance from top to baseline) across all glyphs.
+	int16 _maxDescent = 0; // Maximum descent (distance from baseline to bottom) across all glyphs.
 
 private:
-	Common::HashMap<uint, FontGlyph *> _glyphs;
+	Common::HashMap<uint, FontCharacter *> _characters;
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/actors/text.cpp b/engines/mediastation/actors/text.cpp
index 2dedfa8b7da..346d474db80 100644
--- a/engines/mediastation/actors/text.cpp
+++ b/engines/mediastation/actors/text.cpp
@@ -20,6 +20,7 @@
  */
 
 #include "mediastation/actors/text.h"
+#include "mediastation/mediastation.h"
 
 namespace MediaStation {
 
@@ -29,26 +30,24 @@ void TextActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 		_isVisible = static_cast<bool>(chunk.readTypedByte());
 		break;
 
-	case kActorHeaderEditable:
-		_editable = chunk.readTypedByte();
-		break;
-
 	case kActorHeaderLoadType:
 		_loadType = chunk.readTypedByte();
 		break;
 
-	case kActorHeaderFontId:
-		_fontActorId = chunk.readTypedUint16();
-		break;
-
-	case kActorHeaderTextMaxLength:
-		_maxTextLength = chunk.readTypedUint16();
+	case kActorHeaderFontActorId: {
+		uint fontActorId = chunk.readTypedUint16();
+		_fontActor = static_cast<FontActor *>(g_engine->getActorByIdAndType(fontActorId, kActorTypeFont));
 		break;
+	}
 
 	case kActorHeaderInitialText:
 		_text = chunk.readTypedString();
 		break;
 
+	case kActorHeaderTextMaxLength:
+		_maxLength = chunk.readTypedUint16();
+		break;
+
 	case kActorHeaderTextJustification:
 		_justification = static_cast<TextJustification>(chunk.readTypedUint16());
 		break;
@@ -57,58 +56,428 @@ void TextActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 		_position = static_cast<TextPosition>(chunk.readTypedUint16());
 		break;
 
-	case kActorHeaderTextCharacterClass: {
-		CharacterClass characterClass;
-		characterClass.firstAsciiCode = chunk.readTypedUint16();
-		characterClass.lastAsciiCode = chunk.readTypedUint16();
-		_acceptedInput.push_back(characterClass);
+	case kActorHeaderTextAcceptedCharRangeWithOffset: {
+		uint firstCharCode = chunk.readTypedUint16();
+		uint lastCharCode = chunk.readTypedUint16();
+		uint charCodeOffset = chunk.readTypedUint16();
+		addAcceptedChars(firstCharCode, lastCharCode, charCodeOffset);
+		break;
+	}
+
+	case kActorHeaderTextAcceptedCharRange: {
+		uint firstCharCode = chunk.readTypedUint16();
+		uint lastCharCode = chunk.readTypedUint16();
+		addAcceptedChars(firstCharCode, lastCharCode);
 		break;
 	}
 
+	case kActorHeaderEditable:
+		_isEditable = chunk.readTypedByte();
+		break;
+
+	case kActorHeaderTextCursorIsVisible:
+		_cursorIsVisible = static_cast<bool>(chunk.readTypedByte());
+		break;
+
+	case kActorHeaderTextConstrainToWidth:
+		_constrainToWidth = static_cast<bool>(chunk.readTypedByte());
+		break;
+
+	case kActorHeaderTextOverwriteMode:
+		_overwriteMode = static_cast<bool>(chunk.readTypedByte());
+		break;
+
 	default:
 		SpatialEntity::readParameter(chunk, paramType);
 	}
 }
 
+void TextActor::loadIsComplete() {
+	setText();
+	SpatialEntity::loadIsComplete();
+}
+
 ScriptValue TextActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
 	ScriptValue returnValue;
 
 	switch (methodId) {
-	case kTextMethod: {
-		assert(args.empty());
-		error("%s: Text() method not implemented yet", __func__);
+	case kSpatialShowMethod:
+		ARGCOUNTCHECK(0);
+		if (!_isVisible) {
+			_isVisible = true;
+			invalidateLocalBounds();
+		}
+		break;
+
+	case kSpatialHideMethod:
+		ARGCOUNTCHECK(0);
+		if (_isVisible) {
+			_isVisible = false;
+			invalidateLocalBounds();
+		}
+		break;
+
+	case kTextSetEditableMethod:
+		ARGCOUNTCHECK(0);
+		_isEditable = true;
+		break;
+
+	case kTextSetNonEditableMethod:
+		ARGCOUNTCHECK(0);
+		_isEditable = false;
+		break;
+
+	case kTextGetFontActorMethod: {
+		ARGCOUNTCHECK(0);
+		returnValue.setToActorId(_fontActor->id());
+		break;
+	}
+
+	case kTextSetFontActorMethod: {
+		ARGCOUNTCHECK(1);
+		uint fontActorId = args[0].asActorId();
+		_fontActor = static_cast<FontActor *>(g_engine->getActorByIdAndType(fontActorId, kActorTypeFont));
+		invalidateLocalBounds();
+		break;
+	}
+
+	case kTextGetTextMethod:
+		ARGCOUNTCHECK(0);
+		returnValue.setToString(_text);
+		break;
+
+	case kTextSetTextMethod: {
+		ARGCOUNTCHECK(1);
+		_text = args[0].asString();
+		setText();
+		invalidateLocalBounds();
+		break;
+	}
+
+	case kTextGetMaxLengthMethod:
+		ARGCOUNTCHECK(0);
+		returnValue.setToFloat(_maxLength);
+		break;
+
+	case kTextSetMaxLengthMethod:
+		ARGCOUNTCHECK(1);
+		_maxLength = static_cast<uint>(args[0].asFloat());
+		invalidateLocalBounds();
+		break;
+
+	case kGetLastPressedCharCodeMethod:
+		ARGCOUNTCHECK(0);
+		returnValue.setToFloat(_pressedCharCode);
+		break;
+
+	case kTextGetCursorPositionMethod:
+		ARGCOUNTCHECK(0);
+		returnValue.setToFloat(_cursorPosition);
+		break;
+
+	case kTextSetCursorPositionMethod: {
+		ARGCOUNTCHECK(1);
+		uint newPos = static_cast<uint>(args[0].asFloat());
+		if (newPos > _text.size()) {
+			newPos = _text.size();
+		}
+		_cursorPosition = newPos;
+		invalidateLocalBounds();
+		break;
+	}
+
+	case kTextGetJustificationMethod:
+		ARGCOUNTCHECK(0);
+		returnValue.setToParamToken(static_cast<uint>(_justification));
+		break;
+
+	case kTextSetJustificationMethod:
+		ARGCOUNTCHECK(1);
+		_justification = static_cast<TextJustification>(args[0].asParamToken());
+		invalidateLocalBounds();
+		break;
+
+	case kTextGetPositionMethod:
+		ARGCOUNTCHECK(0);
+		returnValue.setToParamToken(static_cast<uint>(_position));
+		break;
+
+	case kTextSetPositionMethod:
+		ARGCOUNTCHECK(1);
+		_position = static_cast<TextPosition>(args[0].asParamToken());
+		invalidateLocalBounds();
+		break;
+
+	case kTextGetConstrainToWidthMethod:
+		ARGCOUNTCHECK(0);
+		returnValue.setToBool(_constrainToWidth);
+		break;
+
+	case kTextSetConstrainToWidthMethod:
+		ARGCOUNTCHECK(1);
+		_constrainToWidth = static_cast<bool>(args[0].asBool());
+		invalidateLocalBounds();
+		break;
+
+	case kTextGetCursorIsVisibleMethod:
+		ARGCOUNTCHECK(0);
+		returnValue.setToBool(_cursorIsVisible);
+		break;
+
+	case kTextSetCursorIsVisibleMethod:
+		ARGCOUNTCHECK(1);
+		_cursorIsVisible = static_cast<bool>(args[0].asBool());
+		invalidateLocalBounds();
+		break;
+
+	case kTextGetOverwriteModeMethod:
+		ARGCOUNTCHECK(0);
+		returnValue.setToBool(_overwriteMode);
+		break;
+
+	case kTextSetOverwriteModeMethod:
+		ARGCOUNTCHECK(1);
+		_overwriteMode = static_cast<bool>(args[0].asBool());
+		break;
+
+	case kTextGetTranslatedCharCode: {
+		ARGCOUNTCHECK(1);
+		uint charId = args[0].asParamToken();
+		uint translatedChar = _acceptedChars.getValOrDefault(charId, 0);
+		if (translatedChar != 0) {
+			returnValue.setToFloat(translatedChar);
+		} else {
+			// Character not found, so return the input as-is.
+			returnValue = args[0];
+		}
+		break;
 	}
 
-	case kSetTextMethod: {
-		assert(args.size() == 1);
-		error("%s: getText() method not implemented yet", __func__);
+	case kTextAddAcceptedCharsMethod: {
+		ARGCOUNTMIN(2);
+		uint startCharId = static_cast<uint>(args[0].asFloat());
+		uint endCharId = static_cast<uint>(args[1].asFloat());
+		uint category = 0;
+		if (args.size() >= 3) {
+			category = static_cast<uint>(args[2].asFloat());
+		}
+
+		addAcceptedChars(startCharId, endCharId, category);
+		break;
 	}
 
-	case kSpatialShowMethod: {
-		assert(args.empty());
-		_isVisible = true;
-		warning("%s: spatialShow method not implemented yet", __func__);
-		return returnValue;
+	case kTextIsCharacterAcceptedMethod: {
+		ARGCOUNTCHECK(1);
+		uint charId = static_cast<uint>(args[0].asFloat());
+		bool isAccepted = _acceptedChars.contains(charId);
+		returnValue.setToBool(isAccepted);
+		break;
 	}
 
-	case kSpatialHideMethod: {
-		assert(args.empty());
-		_isVisible = false;
-		warning("%s: spatialHide method not implemented yet", __func__);
-		return returnValue;
+	case kTextEnableDisableCharacterMethod: {
+		ARGCOUNTCHECK(2);
+		uint charId = static_cast<uint>(args[0].asFloat());
+		bool shouldEnable = static_cast<bool>(args[1].asBool());
+		if (shouldEnable) {
+			// Mark character as accepted.
+			addAcceptedChars(charId, charId, 0);
+		} else {
+			// No longer mark character as accepted.
+			_acceptedChars.erase(charId);
+		}
+		break;
 	}
 
+	case kTextIsEditableMethod:
+		returnValue.setToBool(_isEditable);
+		break;
+
 	default:
-		return SpatialEntity::callMethod(methodId, args);
+		returnValue = SpatialEntity::callMethod(methodId, args);
+	}
+	return returnValue;
+}
+
+void TextActor::draw(DisplayContext &displayContext) {
+	if (_fontActor == nullptr) {
+		warning("[%s] %s: No font", debugName(), __func__);
+		return;
 	}
+
+	Common::Point positionOnScreen(calcStartingXPosition(), calcBaseline());
+	for (uint positionInString = 0; positionInString < _text.size(); positionInString++) {
+		FontCharacter *fontChar = nullptr;
+		char currentChar = _text[positionInString];
+
+		fontChar = _fontActor->lookupCharacter(currentChar);
+		if (fontChar != nullptr) {
+			// We have the character, so draw it.
+			drawCharacter(fontChar, positionOnScreen.x, positionOnScreen.y, displayContext);
+
+			// Also draw the cursor if necessary.
+			if (positionInString == _cursorPosition && _cursorIsVisible) {
+				drawCursor(positionOnScreen.x, positionOnScreen.y, displayContext);
+			}
+
+			positionOnScreen.x += fontChar->_horizontalSpacing + fontChar->width();
+
+		} else if (currentChar == ' ') {
+			// Handle space character, which might not be in the font.
+			if (positionInString == _cursorPosition && _cursorIsVisible) {
+				drawCursor(positionOnScreen.x, positionOnScreen.y, displayContext);
+			}
+
+			positionOnScreen.x += _fontActor->_averageCharWidth;
+
+		} else {
+			warning("[%s] %s: No font char for %d", debugName(), __func__, currentChar);
+		}
+	}
+
+	// Draw cursor at end of the text if necessary.
+	if (_cursorPosition == _text.size() && _cursorIsVisible) {
+		drawCursor(positionOnScreen.x, positionOnScreen.y, displayContext);
+	}
+}
+
+uint16 TextActor::findActorToAcceptKeyboardEvents(uint16 charCode, uint16 eventMask, MouseActorState &state) {
+	uint16 result = 0;
+	if (_loadIsComplete && (eventMask & kKeyDownFlag) && _isEditable) {
+		// If we have accepted character restrictions, check if character is valid.
+		if (!_acceptedChars.empty()) {
+			if (!_acceptedChars.contains(charCode)) {
+				return 0;
+			}
+		}
+
+		state.keyDown = this;
+		result = kNoFlag;
+	}
+
+	return result;
 }
 
-Common::String TextActor::text() const {
-	return _text;
+void TextActor::keyboardEvent(const Common::Event &event) {
+	// TODO: Implement this once we have a title that actually uses it.
+	warning("STUB: %s", __func__);
 }
 
-void TextActor::setText(Common::String text) {
-	error("%s: Setting text not implemented yet", __func__);
+bool TextActor::hasEventHandler(EventType eventType, const ScriptValue &arg) const {
+	const Common::Array<EventHandler *> &eventHandlers = _eventHandlers.getValOrDefault(eventType);
+	for (const EventHandler *eventHandler : eventHandlers) {
+		const ScriptValue &argToCheck = eventHandler->_argumentValue;
+
+		if (arg.getType() != argToCheck.getType()) {
+			continue;
+		}
+
+		if (arg == argToCheck) {
+			return true;
+		}
+	}
+	return false;
+}
+
+void TextActor::setText() {
+	// Remove double-quotes if they're the first or last characters.
+	if (_text.firstChar() == '"') {
+		_text.deleteChar(0);
+	}
+	if (_text.lastChar() == '"') {
+		_text.deleteLastChar();
+	}
+
+	// Apply character translation if we have any.
+	for (uint positionInString = 0; positionInString < _text.size(); positionInString++) {
+		char currentChar = _text[positionInString];
+		uint translatedChar = _acceptedChars.getValOrDefault(currentChar);
+		if (translatedChar != 0) {
+			_text.setChar(translatedChar, positionInString);
+		}
+	}
+}
+
+void TextActor::addAcceptedChars(uint firstCharCode, uint lastCharCode, uint charCodeOffset) {
+	for (uint charCode = firstCharCode; charCode <= lastCharCode; charCode++) {
+		_acceptedChars.setVal(charCode, charCodeOffset);
+		if (charCodeOffset != 0) {
+			charCodeOffset++;
+		}
+	}
+}
+
+int16 TextActor::calcStartingXPosition() {
+	int16 xPos = 0;
+	Common::Rect bounds = getBbox();
+	if (_justification == kTextJustificationLeft) {
+		xPos = bounds.left;
+
+	} else if (_justification == kTextJustificationRight) {
+		int16 textPixelLength = calcPixelLength(_text);
+		xPos = bounds.right - textPixelLength;
+
+	} else if (_justification == kTextJustificationCenter) {
+		int16 textPixelLength = calcPixelLength(_text);
+		int16 boundsMidwidth = bounds.left + (bounds.width() / 2);
+		xPos = boundsMidwidth - (textPixelLength / 2);
+	}
+	return xPos;
+}
+
+int16 TextActor::calcBaseline() {
+	int16 yPos = 0;
+	Common::Rect bounds = getBbox();
+	if (_position == kTextPositionTop) {
+		yPos =  bounds.top + _fontActor->_maxAscent;
+
+	} else if (_position == kTextPositionBottom) {
+		yPos = bounds.bottom - _fontActor->_maxDescent;
+
+	} else if (_position == kTextPositionMiddle) {
+		int16 boundsMidheight = bounds.top + (bounds.height() / 2);
+		yPos = boundsMidheight + (_fontActor->_maxAscent / 2);
+	}
+	return yPos;
+}
+
+void TextActor::drawCharacter(FontCharacter *glyph, int16 x, int16 y, DisplayContext &displayContext) {
+	// Draw the glyph at the baseline position (y) minus its ascent.
+	Common::Point destPoint(x, y - glyph->ascent());
+	g_engine->getDisplayManager()->imageBlit(destPoint, glyph, _dissolveFactor, &displayContext);
+}
+
+void TextActor::drawCursor(int16 x, int16 y, DisplayContext &displayContext) {
+	FontCharacter *cursorChar = _fontActor->lookupCharacter(CURSOR_CHAR_ID);
+	if (cursorChar != nullptr) {
+		drawCharacter(cursorChar, x, y, displayContext);
+	}
+}
+
+int16 TextActor::calcPixelLength(const Common::String &text) {
+	// Add the width of the main text.
+	int16 totalXOffset = 0;
+	for (uint positionInString = 0; positionInString < text.size(); positionInString++) {
+		char currentChar = text[positionInString];
+
+		FontCharacter *fontChar = _fontActor->lookupCharacter(currentChar);
+		if (fontChar != nullptr) {
+			totalXOffset += fontChar->_horizontalSpacing + fontChar->width();
+
+		} else if (currentChar == ' ') {
+			// As before, the space character might not have a defined font character.
+			totalXOffset += _fontActor->_averageCharWidth;
+		}
+	}
+
+	// Add cursor width if we are needing to show the cursor.
+	if (_cursorPosition == _text.size() && _cursorIsVisible) {
+		FontCharacter *cursorChar = _fontActor->lookupCharacter(CURSOR_CHAR_ID);
+		if (cursorChar != nullptr) {
+			totalXOffset += cursorChar->width();
+		}
+	}
+
+	return totalXOffset;
 }
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/actors/text.h b/engines/mediastation/actors/text.h
index 595c0582e37..b53ffbdd8ba 100644
--- a/engines/mediastation/actors/text.h
+++ b/engines/mediastation/actors/text.h
@@ -25,6 +25,8 @@
 #include "common/str.h"
 
 #include "mediastation/actor.h"
+#include "mediastation/actors/font.h"
+#include "mediastation/graphics.h"
 #include "mediastation/mediascript/scriptvalue.h"
 #include "mediastation/mediascript/scriptconstants.h"
 
@@ -39,36 +41,45 @@ enum TextJustification {
 enum TextPosition {
 	kTextPositionMiddle = 0x25e,
 	kTextPositionTop = 0x260,
-	kTextPositionBotom = 0x261
-};
-
-struct CharacterClass {
-	uint firstAsciiCode = 0;
-	uint lastAsciiCode = 0;
+	kTextPositionBottom = 0x261
 };
 
 class TextActor : public SpatialEntity {
 public:
 	TextActor() : SpatialEntity(kActorTypeText) {};
 
-	virtual bool isVisible() const override { return _isVisible; }
+	virtual void loadIsComplete() override;
 	virtual void readParameter(Chunk &chunk, ActorHeaderSectionType paramType) override;
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
+	virtual void draw(DisplayContext &displayContext) override;
+	virtual uint16 findActorToAcceptKeyboardEvents(uint16 charCode, uint16 eventMask, MouseActorState &state) override;
+	virtual void keyboardEvent(const Common::Event &event) override;
 
 private:
-	bool _editable = false;
+	static const uint CURSOR_CHAR_ID = 0x104;
+	bool _isEditable = false;
 	uint _loadType = 0;
-	bool _isVisible = false;
 	Common::String _text;
-	uint _maxTextLength = 0;
-	uint _fontActorId = 0;
-	TextJustification _justification;
-	TextPosition _position;
-	Common::Array<CharacterClass> _acceptedInput;
+	uint _maxLength = 0;
+	FontActor *_fontActor = nullptr;
+	TextJustification _justification = kTextJustificationLeft;
+	TextPosition _position = kTextPositionTop;
+	Common::HashMap<uint, uint> _acceptedChars;
+	uint _cursorPosition = 0;
+	uint _pressedCharCode = 0;
+	bool _cursorIsVisible = false;
+	bool _constrainToWidth = false;
+	bool _overwriteMode = false;
+
+	void setText();
+	void addAcceptedChars(uint firstCharCode, uint lastCharCode, uint charCodeOffset = 0);
+	bool hasEventHandler(EventType eventType, const ScriptValue &arg) const;
 
-	// Method implementations.
-	Common::String text() const;
-	void setText(Common::String text);
+	int16 calcStartingXPosition();
+	int16 calcBaseline();
+	void drawCharacter(FontCharacter *glyph, int16 x, int16 y, DisplayContext &displayContext);
+	void drawCursor(int16 x, int16 y, DisplayContext &displayContext);
+	int16 calcPixelLength(const Common::String &text);
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/graphics.cpp b/engines/mediastation/graphics.cpp
index ce35f62c492..75891668563 100644
--- a/engines/mediastation/graphics.cpp
+++ b/engines/mediastation/graphics.cpp
@@ -719,7 +719,8 @@ void VideoDisplayManager::imageBlit(
 	const PixMapImage *sourceImage,
 	double dissolveFactor,
 	DisplayContext *displayContext,
-	Graphics::ManagedSurface *targetImage) {
+	Graphics::ManagedSurface *targetImage,
+	bool useTransBlit) {
 
 	byte blitFlags = kClipEnabled;
 	switch (sourceImage->getCompressionType()) {
@@ -787,7 +788,7 @@ void VideoDisplayManager::imageBlit(
 		// non-transparent blitting, but we will just use simpleBlitFrom in both
 		// cases. It will pick the better method if there is no transparent
 		// color set.
-		blitRectsClip(targetImage, destinationPoint, sourceImage->_image, dirtyRegion);
+		blitRectsClip(targetImage, destinationPoint, sourceImage->_image, dirtyRegion, useTransBlit);
 		break;
 
 	case kRle8Blit | kClipEnabled:
@@ -819,7 +820,8 @@ void VideoDisplayManager::blitRectsClip(
 	Graphics::ManagedSurface *dest,
 	const Common::Point &destLocation,
 	const Graphics::ManagedSurface &source,
-	const Common::Array<Common::Rect> &dirtyRegion) {
+	const Common::Array<Common::Rect> &dirtyRegion,
+	bool useTransBlit) {
 
 	for (const Common::Rect &dirtyRect : dirtyRegion) {
 		Common::Rect destRect(destLocation, source.w, source.h);
@@ -829,7 +831,11 @@ void VideoDisplayManager::blitRectsClip(
 			// Calculate source coordinates (relative to source image).
 			Common::Point originOnScreen(areaToRedraw.origin());
 			areaToRedraw.translate(-destLocation.x, -destLocation.y);
-			dest->simpleBlitFrom(source, areaToRedraw, originOnScreen);
+			if (useTransBlit) {
+				dest->transBlitFrom(source, areaToRedraw, originOnScreen);
+			} else {
+				dest->simpleBlitFrom(source, areaToRedraw, originOnScreen);
+			}
 		}
 	}
 }
diff --git a/engines/mediastation/graphics.h b/engines/mediastation/graphics.h
index 5a8c08fa1b3..1d7e8c533a6 100644
--- a/engines/mediastation/graphics.h
+++ b/engines/mediastation/graphics.h
@@ -140,7 +140,8 @@ public:
 		const PixMapImage *image,
 		double dissolveFactor,
 		DisplayContext *displayContext,
-		Graphics::ManagedSurface *destinationImage = nullptr);
+		Graphics::ManagedSurface *destinationImage = nullptr,
+		bool useTransBlit = false);
 
 	void imageDeltaBlit(
 		Common::Point deltaFramePos,
@@ -185,7 +186,8 @@ private:
 		Graphics::ManagedSurface *dest,
 		const Common::Point &destLocation,
 		const Graphics::ManagedSurface &source,
-		const Common::Array<Common::Rect> &dirtyRegion);
+		const Common::Array<Common::Rect> &dirtyRegion,
+		bool useTransBlit = false);
 	void rleBlitRectsClip(
 		Graphics::ManagedSurface *dest,
 		const Common::Point &destLocation,
diff --git a/engines/mediastation/mediascript/scriptconstants.cpp b/engines/mediastation/mediascript/scriptconstants.cpp
index df59b46fc70..65424fc87cf 100644
--- a/engines/mediastation/mediascript/scriptconstants.cpp
+++ b/engines/mediastation/mediascript/scriptconstants.cpp
@@ -221,10 +221,11 @@ const char *builtInMethodToStr(BuiltInMethod method) {
 		return "IsPlaying/SetMultipleStreams";
 	case kSetDissolveFactorMethod:
 		return "SetDissolveFactor";
+	// NOTE: IDs 0xD2 and 0xD3 are double-assigned between hotspot, stage, and text methods.
 	case kMouseActivateMethod:
-		return "MouseActivate";
+		return "Activate";
 	case kMouseDeactivateMethod:
-		return "MouseDeactivate";
+		return "Deactivate";
 	case kGetLeftXMethod:
 		return "GetLeftX";
 	case kGetTopYMethod:
@@ -233,8 +234,9 @@ const char *builtInMethodToStr(BuiltInMethod method) {
 		return "TriggerAbsXPosition";
 	case kTriggerAbsYPositionMethod:
 		return "TriggerAbsYPosition";
+	// NOTE: ID 0x173 is double-assigned between hotspot and text methods.
 	case kIsActiveMethod:
-		return "IsActive";
+		return "IsActive/IsEditable";
 	case kGetWidthMethod:
 		return "GetWidth";
 	case kGetHeightMethod:
@@ -325,8 +327,12 @@ const char *builtInMethodToStr(BuiltInMethod method) {
 	case kYViewportPositionMethod:
 		return "YViewportPosition";
 	case kPanToMethod:
-		return "PanTo";
-	case kClearToPaletteMethod:
+		return "PanTo/CanvasClearToTransparency";
+	case kCanvasStampImageMethod:
+		return "CanvasStampImage";
+	case kCanvasCopyScreenToMethod:
+		return "CanvasCopyScreenTo";
+	case kCanvasClearToPaletteMethod:
 		return "ClearToPalette";
 	case kStreamMovieMoveProxyToStageMethod:
 		return "MoveProxyToStage";
@@ -356,12 +362,52 @@ const char *builtInMethodToStr(BuiltInMethod method) {
 		return "SetTotalSteps";
 	case kPathSetStepRateMethod:
 		return "SetStepRate";
-	case kTextMethod:
+	case kTextGetFontActorMethod:
+		return "GetFontActor";
+	case kTextSetFontActorMethod:
+		return "SetFontActor";
+	case kTextGetTextMethod:
 		return "Text";
-	case kSetTextMethod:
+	case kTextSetTextMethod:
 		return "SetText";
-	case kSetMaximumTextLengthMethod:
-		return "SetMaximumTextLength";
+	case kTextGetMaxLengthMethod:
+		return "GetMaxLength";
+	case kTextSetMaxLengthMethod:
+		return "SetMaxLength";
+	case kGetLastPressedCharCodeMethod:
+		return "GetLastPressedCharCode";
+	case kTextGetCursorPositionMethod:
+		return "GetCursorPosition";
+	case kTextSetCursorPositionMethod:
+		return "SetCursorPosition";
+	case kTextGetJustificationMethod:
+		return "GetJustification";
+	case kTextSetJustificationMethod:
+		return "SetJustification";
+	case kTextGetPositionMethod:
+		return "GetPosition";
+	case kTextSetPositionMethod:
+		return "SetPosition";
+	case kTextGetConstrainToWidthMethod:
+		return "GetConstrainToWidth";
+	case kTextSetConstrainToWidthMethod:
+		return "SetConstrainToWidth";
+	case kTextGetCursorIsVisibleMethod:
+		return "GetCursorIsVisible";
+	case kTextSetCursorIsVisibleMethod:
+		return "SetCursorIsVisible";
+	case kTextGetOverwriteModeMethod:
+		return "GetOverwriteMode";
+	case kTextSetOverwriteModeMethod:
+		return "SetOverwriteMode";
+	case kTextGetTranslatedCharCode:
+		return "GetTranslatedCharCode";
+	case kTextAddAcceptedCharsMethod:
+		return "AddAcceptedChars";
+	case kTextIsCharacterAcceptedMethod:
+		return "IsCharacterAccepted";
+	case kTextEnableDisableCharacterMethod:
+		return "EnableDisableCharacter";
 	case kAppendMethod:
 		return "Append";
 	case kApplyMethod:
diff --git a/engines/mediastation/mediascript/scriptconstants.h b/engines/mediastation/mediascript/scriptconstants.h
index 6e356359482..3a23a7bbffe 100644
--- a/engines/mediastation/mediascript/scriptconstants.h
+++ b/engines/mediastation/mediascript/scriptconstants.h
@@ -209,7 +209,10 @@ enum BuiltInMethod {
 	kPanToMethod = 0x172,
 
 	// CANVAS METHODS.
-	kClearToPaletteMethod = 0x17B,
+	kCanvasClearToTransparencyMethod = 0x172,
+	kCanvasStampImageMethod = 0x179,
+	kCanvasCopyScreenToMethod = 0x17A,
+	kCanvasClearToPaletteMethod = 0x17B,
 
 	// DOCUMENT METHODS.
 	kDocumentBranchToScreenMethod = 0xC9,
@@ -230,9 +233,32 @@ enum BuiltInMethod {
 	kPathSetStepRateMethod = 0xf5,
 
 	// TEXT METHODS.
-	kTextMethod = 0x122,
-	kSetTextMethod = 0x123,
-	kSetMaximumTextLengthMethod = 0x125,
+	kTextSetEditableMethod = 0xD2,
+	kTextSetNonEditableMethod = 0xD3,
+	kTextGetFontActorMethod = 0x120,
+	kTextSetFontActorMethod = 0x121,
+	kTextGetTextMethod = 0x122,
+	kTextSetTextMethod = 0x123,
+	kTextGetMaxLengthMethod = 0x124,
+	kTextSetMaxLengthMethod = 0x125,
+	kGetLastPressedCharCodeMethod = 0x126,
+	kTextGetCursorPositionMethod = 0x127,
+	kTextSetCursorPositionMethod = 0x128,
+	kTextGetJustificationMethod = 0x14b,
+	kTextSetJustificationMethod = 0x14c,
+	kTextGetPositionMethod = 0x14d,
+	kTextSetPositionMethod = 0x14e,
+	kTextGetConstrainToWidthMethod = 0x150,
+	kTextSetConstrainToWidthMethod = 0x151,
+	kTextGetCursorIsVisibleMethod = 0x152,
+	kTextSetCursorIsVisibleMethod = 0x153,
+	kTextGetOverwriteModeMethod = 0x154,
+	kTextSetOverwriteModeMethod = 0x155,
+	kTextGetTranslatedCharCode = 0x156,
+	kTextAddAcceptedCharsMethod = 0x157,
+	kTextIsCharacterAcceptedMethod = 0x158,
+	kTextEnableDisableCharacterMethod = 0x159,
+	kTextIsEditableMethod = 0x173,
 
 	// COLLECTION METHODS.
 	// These are arrays used in Media Script.


Commit: 34af4483c49b4e805f32547c9b780a8cd84e7eed
    https://github.com/scummvm/scummvm/commit/34af4483c49b4e805f32547c9b780a8cd84e7eed
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-02-24T21:10:58-05:00

Commit Message:
MEDIASTATION: Enforce consistent switch breaks, rather than returns

Changed paths:
    engines/mediastation/actors/hotspot.cpp
    engines/mediastation/actors/image.cpp
    engines/mediastation/actors/movie.cpp
    engines/mediastation/actors/sound.cpp
    engines/mediastation/actors/sprite.cpp
    engines/mediastation/actors/stage.cpp
    engines/mediastation/actors/timer.cpp
    engines/mediastation/mediascript/codechunk.cpp


diff --git a/engines/mediastation/actors/hotspot.cpp b/engines/mediastation/actors/hotspot.cpp
index ef859235a40..11eb4ac07f0 100644
--- a/engines/mediastation/actors/hotspot.cpp
+++ b/engines/mediastation/actors/hotspot.cpp
@@ -98,13 +98,13 @@ ScriptValue HotspotActor::callMethod(BuiltInMethod methodId, Common::Array<Scrip
 	case kMouseActivateMethod: {
 		ARGCOUNTCHECK(0);
 		activate();
-		return returnValue;
+		break;
 	}
 
 	case kMouseDeactivateMethod: {
 		ARGCOUNTCHECK(0);
 		deactivate();
-		return returnValue;
+		break;
 	}
 
 
@@ -115,32 +115,33 @@ ScriptValue HotspotActor::callMethod(BuiltInMethod methodId, Common::Array<Scrip
 		Common::Point pointToCheck(xToCheck, yToCheck);
 		bool pointIsInside = isInside(pointToCheck);
 		returnValue.setToBool(pointIsInside);
-		return returnValue;
+		break;
 	}
 
 	case kTriggerAbsXPositionMethod: {
 		ARGCOUNTCHECK(0);
 		double mouseX = static_cast<double>(g_system->getEventManager()->getMousePos().x);
 		returnValue.setToFloat(mouseX);
-		return returnValue;
+		break;
 	}
 
 	case kTriggerAbsYPositionMethod: {
 		ARGCOUNTCHECK(0);
 		double mouseY = static_cast<double>(g_system->getEventManager()->getMousePos().y);
 		returnValue.setToFloat(mouseY);
-		return returnValue;
+		break;
 	}
 
 	case kIsActiveMethod: {
 		ARGCOUNTCHECK(0);
 		returnValue.setToBool(_isActive);
-		return returnValue;
+		break;
 	}
 
 	default:
-		return SpatialEntity::callMethod(methodId, args);
+		returnValue = SpatialEntity::callMethod(methodId, args);
 	}
+	return returnValue;
 }
 
 uint16 HotspotActor::findActorToAcceptMouseEvents(
diff --git a/engines/mediastation/actors/image.cpp b/engines/mediastation/actors/image.cpp
index 48d405fd521..593df22a2cc 100644
--- a/engines/mediastation/actors/image.cpp
+++ b/engines/mediastation/actors/image.cpp
@@ -76,18 +76,19 @@ ScriptValue ImageActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptV
 	case kSpatialShowMethod: {
 		ARGCOUNTCHECK(0);
 		spatialShow();
-		return returnValue;
+		break;
 	}
 
 	case kSpatialHideMethod: {
 		ARGCOUNTCHECK(0);
 		spatialHide();
-		return returnValue;
+		break;
 	}
 
 	default:
 		return SpatialEntity::callMethod(methodId, args);
 	}
+	return returnValue;
 }
 
 void ImageActor::draw(DisplayContext &displayContext) {
diff --git a/engines/mediastation/actors/movie.cpp b/engines/mediastation/actors/movie.cpp
index 2284fc82213..1c9630177da 100644
--- a/engines/mediastation/actors/movie.cpp
+++ b/engines/mediastation/actors/movie.cpp
@@ -193,7 +193,7 @@ ScriptValue StreamMovieActor::callMethod(BuiltInMethod methodId, Common::Array<S
 					debugName(), __func__, g_engine->formatParamTokenName(scriptId).c_str());
 			}
 		}
-		return returnValue;
+		break;
 	}
 
 	case kSpatialHideMethod: {
@@ -214,19 +214,19 @@ ScriptValue StreamMovieActor::callMethod(BuiltInMethod methodId, Common::Array<S
 					debugName(), __func__, g_engine->formatParamTokenName(scriptId).c_str());
 			}
 		}
-		return returnValue;
+		break;
 	}
 
 	case kTimePlayMethod: {
 		ARGCOUNTCHECK(0);
 		timePlay();
-		return returnValue;
+		break;
 	}
 
 	case kTimeStopMethod: {
 		ARGCOUNTCHECK(0);
 		timeStop();
-		return returnValue;
+		break;
 	}
 
 	case kStreamMovieSetProxyZIndex: {
@@ -240,7 +240,7 @@ ScriptValue StreamMovieActor::callMethod(BuiltInMethod methodId, Common::Array<S
 			warning("[%s] %s: Stream movie proxy with script ID %s doesn't exist",
 				debugName(), __func__, g_engine->formatParamTokenName(scriptId).c_str());
 		}
-		return returnValue;
+		break;
 	}
 
 	case kStreamMovieGetProxyZIndex: {
@@ -253,27 +253,27 @@ ScriptValue StreamMovieActor::callMethod(BuiltInMethod methodId, Common::Array<S
 			warning("[%s] %s: Stream movie proxy with script ID %s doesn't exist",
 				debugName(), __func__, g_engine->formatParamTokenName(scriptId).c_str());
 		}
-		return returnValue;
+		break;
 	}
 
 	case kIsPlayingMethod: {
 		ARGCOUNTCHECK(0);
 		returnValue.setToBool(_isPlaying);
-		return returnValue;
+		break;
 	}
 
 	case kGetLeftXMethod: {
 		ARGCOUNTCHECK(0);
 		double left = static_cast<double>(_boundingBox.left);
 		returnValue.setToFloat(left);
-		return returnValue;
+		break;
 	}
 
 	case kGetTopYMethod: {
 		ARGCOUNTCHECK(0);
 		double top = static_cast<double>(_boundingBox.top);
 		returnValue.setToFloat(top);
-		return returnValue;
+		break;
 	}
 
 	case kStreamMovieMoveProxyToStageMethod: {
@@ -284,18 +284,18 @@ ScriptValue StreamMovieActor::callMethod(BuiltInMethod methodId, Common::Array<S
 		if (proxy == nullptr) {
 			warning("[%s] %s: Stream movie proxy with script ID %s doesn't exist",
 				debugName(), __func__, g_engine->formatParamTokenName(scriptId).c_str());
-			return returnValue;
+			break;
 		}
 		StageActor *parentStage = static_cast<StageActor *>(g_engine->getActorByIdAndType(targetStageId, kActorTypeStage));
 		if (parentStage == nullptr) {
 			warning("[%s] %s: Stream movie proxy with script ID %s has null parent stage",
 				debugName(), __func__, g_engine->formatParamTokenName(scriptId).c_str());
-			return returnValue;
+			break;
 		}
 
 		proxy->getParentStage()->removeChildSpatialEntity(proxy);
 		parentStage->addChildSpatialEntity(proxy);
-		return returnValue;
+		break;
 	}
 
 	case kStreamMovieMoveProxyToRootStageMethod: {
@@ -306,7 +306,7 @@ ScriptValue StreamMovieActor::callMethod(BuiltInMethod methodId, Common::Array<S
 		if (proxy == nullptr) {
 			warning("[%s] %s: Stream movie proxy with script ID %s doesn't exist",
 				debugName(), __func__, g_engine->formatParamTokenName(scriptId).c_str());
-			return returnValue;
+			break;
 		}
 
 		RootStage *rootStage = g_engine->getRootStage();
@@ -314,17 +314,18 @@ ScriptValue StreamMovieActor::callMethod(BuiltInMethod methodId, Common::Array<S
 		if (sourceStage == nullptr) {
 			warning("[%s] %s: Stream movie proxy with script ID %s has null parent stage",
 				debugName(), __func__, g_engine->formatParamTokenName(scriptId).c_str());
-			return returnValue;
+			break;
 		}
 
 		sourceStage->removeChildSpatialEntity(proxy);
 		rootStage->addChildSpatialEntity(proxy);
-		return returnValue;
+		break;
 	}
 
 	default:
-		return SpatialEntity::callMethod(methodId, args);
+		returnValue = SpatialEntity::callMethod(methodId, args);
 	}
+	return returnValue;
 }
 
 void StreamMovieActor::timePlay() {
diff --git a/engines/mediastation/actors/sound.cpp b/engines/mediastation/actors/sound.cpp
index 15d1d5b699a..70350092eab 100644
--- a/engines/mediastation/actors/sound.cpp
+++ b/engines/mediastation/actors/sound.cpp
@@ -95,22 +95,22 @@ ScriptValue SoundActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptV
 		// Since the engine is currently flagging errors on unimplemented
 		// methods for easier debugging, a no-op is used here to avoid the error.
 		ARGCOUNTCHECK(0);
-		return returnValue;
+		break;
 
 	case kTimePlayMethod:
 		ARGCOUNTCHECK(0);
 		start();
-		return returnValue;
+		break;
 
 	case kTimeStopMethod:
 		ARGCOUNTCHECK(0);
 		stop();
-		return returnValue;
+		break;
 
 	case kPauseMethod:
 		ARGCOUNTCHECK(0);
 		pause();
-		return returnValue;
+		break;
 
 	case kResumeMethod: {
 		ARGCOUNTRANGE(0, 1);
@@ -119,20 +119,21 @@ ScriptValue SoundActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptV
 			shouldRestart = args[0].asBool();
 		}
 		resume(shouldRestart);
-		return returnValue;
+		break;
 	}
 
 	case kIsPlayingMethod:
 		returnValue.setToBool(_playState == kSoundPlaying || _playState == kSoundPaused);
-		return returnValue;
+		break;
 
 	case kIsPausedMethod:
 		returnValue.setToBool(_playState == kSoundPaused);
-		return returnValue;
+		break;
 
 	default:
-		return Actor::callMethod(methodId, args);
+		returnValue = Actor::callMethod(methodId, args);
 	}
+	return returnValue;
 }
 
 void SoundActor::start() {
diff --git a/engines/mediastation/actors/sprite.cpp b/engines/mediastation/actors/sprite.cpp
index 361a058e667..d312e745adc 100644
--- a/engines/mediastation/actors/sprite.cpp
+++ b/engines/mediastation/actors/sprite.cpp
@@ -121,31 +121,31 @@ ScriptValue SpriteMovieActor::callMethod(BuiltInMethod methodId, Common::Array<S
 	case kSpatialShowMethod: {
 		ARGCOUNTCHECK(0);
 		setVisibility(true);
-		return returnValue;
+		break;
 	}
 
 	case kSpatialHideMethod: {
 		ARGCOUNTCHECK(0);
 		setVisibility(false);
-		return returnValue;
+		break;
 	}
 
 	case kTimePlayMethod: {
 		ARGCOUNTCHECK(0);
 		play();
-		return returnValue;
+		break;
 	}
 
 	case kTimeStopMethod: {
 		ARGCOUNTCHECK(0);
 		stop();
-		return returnValue;
+		break;
 	}
 
 	case kMovieResetMethod: {
 		ARGCOUNTCHECK(0);
 		setCurrentFrameToInitial();
-		return returnValue;
+		break;
 	}
 
 	case kSetCurrentClipMethod: {
@@ -155,7 +155,7 @@ ScriptValue SpriteMovieActor::callMethod(BuiltInMethod methodId, Common::Array<S
 			clipId = args[0].asParamToken();
 		}
 		setCurrentClip(clipId);
-		return returnValue;
+		break;
 	}
 
 	case kIncrementFrameMethod: {
@@ -171,7 +171,7 @@ ScriptValue SpriteMovieActor::callMethod(BuiltInMethod methodId, Common::Array<S
 				setCurrentFrameToInitial();
 			}
 		}
-		return returnValue;
+		break;
 	}
 
 	case kDecrementFrameMethod: {
@@ -186,22 +186,23 @@ ScriptValue SpriteMovieActor::callMethod(BuiltInMethod methodId, Common::Array<S
 				setCurrentFrameToFinal();
 			}
 		}
-		return returnValue;
+		break;
 	}
 
 	case kGetCurrentClipIdMethod: {
 		returnValue.setToParamToken(_activeClip.id);
-		return returnValue;
+		break;
 	}
 
 	case kIsPlayingMethod: {
 		returnValue.setToBool(_isPlaying);
-		return returnValue;
+		break;
 	}
 
 	default:
-		return SpatialEntity::callMethod(methodId, args);
+		returnValue = SpatialEntity::callMethod(methodId, args);
 	}
+	return returnValue;
 }
 
 bool SpriteMovieActor::activateNextFrame() {
diff --git a/engines/mediastation/actors/stage.cpp b/engines/mediastation/actors/stage.cpp
index 7c9e0b79072..65cdf74c976 100644
--- a/engines/mediastation/actors/stage.cpp
+++ b/engines/mediastation/actors/stage.cpp
@@ -322,7 +322,7 @@ ScriptValue StageActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptV
 		ARGCOUNTCHECK(1);
 		uint actorId = args[0].asActorId();
 		addActorToStage(actorId);
-		return returnValue;
+		break;
 	}
 
 	case kRemoveActorFromStageMethod:
@@ -330,7 +330,7 @@ ScriptValue StageActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptV
 		ARGCOUNTCHECK(1);
 		uint actorId = args[0].asActorId();
 		removeActorFromStage(actorId);
-		return returnValue;
+		break;
 	}
 
 	case kSetBoundsMethod: {
@@ -341,28 +341,29 @@ ScriptValue StageActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptV
 		int16 height = static_cast<int16>(args[3].asFloat());
 		Common::Rect newBounds(Common::Point(x, y), width, height);
 		setBounds(newBounds);
-		return returnValue;
+		break;
 	}
 
 	case kStageSetSizeMethod:
 		ARGCOUNTCHECK(2);
 		_boundingBox.setWidth(static_cast<int16>(args[0].asFloat()));
 		_boundingBox.setHeight(static_cast<int16>(args[1].asFloat()));
-		return returnValue;
+		break;
 
 	case kStageGetWidthMethod:
 		ARGCOUNTCHECK(0);
 		returnValue.setToFloat(_boundingBox.width());
-		return returnValue;
+		break;
 
 	case kStageGetHeightMethod:
 		ARGCOUNTCHECK(0);
 		returnValue.setToFloat(_boundingBox.height());
-		return returnValue;
+		break;
 
 	default:
-		return SpatialEntity::callMethod(methodId, args);
+		returnValue = SpatialEntity::callMethod(methodId, args);
 	}
+	return returnValue;
 }
 
 void StageActor::addChildSpatialEntity(SpatialEntity *entity) {
diff --git a/engines/mediastation/actors/timer.cpp b/engines/mediastation/actors/timer.cpp
index 9493ab43efa..52adc2d9a51 100644
--- a/engines/mediastation/actors/timer.cpp
+++ b/engines/mediastation/actors/timer.cpp
@@ -33,24 +33,25 @@ ScriptValue TimerActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptV
 	case kTimePlayMethod: {
 		ARGCOUNTCHECK(0);
 		timePlay();
-		return returnValue;
+		break;
 	}
 
 	case kTimeStopMethod: {
 		ARGCOUNTCHECK(0);
 		timeStop();
-		return returnValue;
+		break;
 	}
 
 	case kIsPlayingMethod: {
 		ARGCOUNTCHECK(0);
 		returnValue.setToBool(_isPlaying);
-		return returnValue;
+		break;
 	}
 
 	default:
-		return Actor::callMethod(methodId, args);
+		returnValue = Actor::callMethod(methodId, args);
 	}
+	return returnValue;
 }
 
 void TimerActor::timePlay() {
diff --git a/engines/mediastation/mediascript/codechunk.cpp b/engines/mediastation/mediascript/codechunk.cpp
index dda70b3ed94..a7d41eae002 100644
--- a/engines/mediastation/mediascript/codechunk.cpp
+++ b/engines/mediastation/mediascript/codechunk.cpp
@@ -208,14 +208,14 @@ ScriptValue CodeChunk::evaluateValue() {
 		}
 		debugC(5, kDebugScript, "%d ", b);
 		returnValue.setToBool(b == 1 ? true : false);
-		return returnValue;
+		break;
 	}
 
 	case kOperandTypeFloat: {
 		double f = _bytecode->readTypedDouble();
 		debugC(5, kDebugScript, "%f ", f);
 		returnValue.setToFloat(f);
-		return returnValue;
+		break;
 	}
 
 	case kOperandTypeInt: {
@@ -223,7 +223,7 @@ ScriptValue CodeChunk::evaluateValue() {
 		debugC(5, kDebugScript, "%d ", i);
 		// Ints are stored internally as doubles.
 		returnValue.setToFloat(static_cast<double>(i));
-		return returnValue;
+		break;
 	}
 
 	case kOperandTypeString: {
@@ -232,7 +232,7 @@ ScriptValue CodeChunk::evaluateValue() {
 		Common::String string = _bytecode->readString('\0', size);
 		debugC(5, kDebugScript, "%s ", string.c_str());
 		returnValue.setToString(string);
-		return returnValue;
+		break;
 	}
 
 	case kOperandTypeParamToken: {
@@ -240,7 +240,7 @@ ScriptValue CodeChunk::evaluateValue() {
 		Common::String tokenName = g_engine->formatParamTokenName(literal);
 		debugC(5, kDebugScript, "%s ", tokenName.c_str());
 		returnValue.setToParamToken(literal);
-		return returnValue;
+		break;
 	}
 
 	case kOperandTypeActorId: {
@@ -248,19 +248,19 @@ ScriptValue CodeChunk::evaluateValue() {
 		Common::String actorName = g_engine->formatActorName(actorId, true);
 		debugC(5, kDebugScript, "%s ", actorName.c_str());
 		returnValue.setToActorId(actorId);
-		return returnValue;
+		break;
 	}
 
 	case kOperandTypeTime: {
 		double d = _bytecode->readTypedTime();
 		debugC(5, kDebugScript, "%f ", d);
 		returnValue.setToTime(d);
-		return returnValue;
+		break;
 	}
 
 	case kOperandTypeVariable: {
 		returnValue = ScriptValue(_bytecode);
-		return returnValue;
+		break;
 	}
 
 	case kOperandTypeFunctionId: {
@@ -269,19 +269,20 @@ ScriptValue CodeChunk::evaluateValue() {
 		Common::String functionName = g_engine->formatFunctionName(functionId);
 		debugC(5, kDebugScript, "%s ", functionName.c_str());
 		returnValue.setToFunctionId(functionId);
-		return returnValue;
+		break;
 	}
 
 	case kOperandTypeMethodId: {
 		BuiltInMethod methodId = static_cast<BuiltInMethod>(_bytecode->readTypedUint16());
 		debugC(5, kDebugScript, "%s ", builtInMethodToStr(methodId));
 		returnValue.setToMethodId(methodId);
-		return returnValue;
+		break;
 	}
 
 	default:
 		error("%s: Got unknown ScriptValue type %s (%d)", __func__, operandTypeToStr(operandType), static_cast<uint>(operandType));
 	}
+	return returnValue;
 }
 
 ScriptValue CodeChunk::evaluateVariable() {
@@ -531,7 +532,7 @@ ScriptValue CodeChunk::evaluateMethodCall(BuiltInMethod method, uint paramCount)
 			// It seems to be valid to call a method on a null actor ID, in
 			// which case nothing happens. Still issue warning for traceability.
 			warning("%s: Attempt to call method %s (%d) on null actor ID", __func__, builtInMethodToStr(method), static_cast<uint>(method));
-			return returnValue;
+			break;
 		} else {
 			// This is a regular actor that we can process directly.
 			uint actorId = target.asActorId();
@@ -540,14 +541,14 @@ ScriptValue CodeChunk::evaluateMethodCall(BuiltInMethod method, uint paramCount)
 				error("[%s] %s: Actor not loaded", g_engine->formatActorName(target.asActorId()).c_str(), __func__);
 			}
 			returnValue = targetActor->callMethod(method, args);
-			return returnValue;
+			break;
 		}
 	}
 
 	case kScriptValueTypeCollection: {
 		Common::SharedPtr<Collection> collection = target.asCollection();
 		returnValue = collection->callMethod(method, args);
-		return returnValue;
+		break;
 	}
 
 	default:
@@ -555,6 +556,7 @@ ScriptValue CodeChunk::evaluateMethodCall(BuiltInMethod method, uint paramCount)
 			builtInMethodToStr(method), static_cast<uint>(method),
 			scriptValueTypeToStr(target.getType()), static_cast<uint>(target.getType()));
 	}
+	return returnValue;
 }
 
 void CodeChunk::evaluateDeclareLocals() {


Commit: 9d426237f60a1e908110bceb1bb43885319d2ed0
    https://github.com/scummvm/scummvm/commit/9d426237f60a1e908110bceb1bb43885319d2ed0
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-02-24T21:10:58-05:00

Commit Message:
MEDIASTATION: Enforce correct copy semantics for script values/variables

Changed paths:
    engines/mediastation/mediascript/codechunk.cpp
    engines/mediastation/mediascript/codechunk.h
    engines/mediastation/mediascript/collection.cpp
    engines/mediastation/mediascript/function.cpp
    engines/mediastation/mediascript/scriptvalue.cpp
    engines/mediastation/mediascript/scriptvalue.h


diff --git a/engines/mediastation/mediascript/codechunk.cpp b/engines/mediastation/mediascript/codechunk.cpp
index a7d41eae002..3e6fda47f52 100644
--- a/engines/mediastation/mediascript/codechunk.cpp
+++ b/engines/mediastation/mediascript/codechunk.cpp
@@ -91,6 +91,29 @@ ScriptValue CodeChunk::evaluateExpression() {
 	return returnValue;
 }
 
+void CodeChunk::evaluateLValue(ScriptValue *&targetPtr) {
+	// Evaluate an expression as an lvalue (something that can be modified in place).
+	ExpressionType expressionType = static_cast<ExpressionType>(_bytecode->readTypedUint16());
+
+	switch (expressionType) {
+	case kExpressionTypeVariable:
+		// Make target point directly to the variable. This permits modifications to the variable
+		// (like adding to a collection) to persist in the original variable.
+		targetPtr = readAndReturnVariable();
+		break;
+
+	case kExpressionTypeValue:
+	case kExpressionTypeOperation:
+		// For values/operations, just fill in the temporary value the caller passed to us.
+		// This means modifications to the evaluated expression will not be persisted like variables.
+		*targetPtr = evaluateExpression(expressionType);
+		break;
+
+	default:
+		error("%s: Unexpected expression type %s", __func__, expressionTypeToStr(expressionType));
+	}
+}
+
 ScriptValue CodeChunk::evaluateExpression(ExpressionType expressionType) {
 	debugCN(5, kDebugScript, "(%s) ", expressionTypeToStr(expressionType));
 
@@ -517,7 +540,10 @@ ScriptValue CodeChunk::evaluateMethodCall(BuiltInMethod method, uint paramCount)
 	debugC(5, kDebugScript, "%s (%d params)", builtInMethodToStr(method), paramCount);
 	debugCN(5, kDebugScript, "  Self: ");
 
-	ScriptValue target = evaluateExpression();
+	// Evaluate target as an lvalue to get a pointer to the actual variable if there is one.
+	ScriptValue methodCallTarget;
+	ScriptValue *methodCallTargetPtr = &methodCallTarget;
+	evaluateLValue(methodCallTargetPtr);
 	Common::Array<ScriptValue> args;
 	for (uint i = 0; i < paramCount; i++) {
 		debugCN(5, kDebugScript, "  Param %d: ", i);
@@ -526,19 +552,19 @@ ScriptValue CodeChunk::evaluateMethodCall(BuiltInMethod method, uint paramCount)
 	}
 
 	ScriptValue returnValue;
-	switch (target.getType()) {
+	switch (methodCallTargetPtr->getType()) {
 	case kScriptValueTypeActorId: {
-		if (target.asActorId() == 0) {
+		if (methodCallTargetPtr->asActorId() == 0) {
 			// It seems to be valid to call a method on a null actor ID, in
 			// which case nothing happens. Still issue warning for traceability.
 			warning("%s: Attempt to call method %s (%d) on null actor ID", __func__, builtInMethodToStr(method), static_cast<uint>(method));
 			break;
 		} else {
 			// This is a regular actor that we can process directly.
-			uint actorId = target.asActorId();
+			uint actorId = methodCallTargetPtr->asActorId();
 			Actor *targetActor = g_engine->getActorById(actorId);
 			if (targetActor == nullptr) {
-				error("[%s] %s: Actor not loaded", g_engine->formatActorName(target.asActorId()).c_str(), __func__);
+				error("[%s] %s: Actor not loaded", g_engine->formatActorName(actorId).c_str(), __func__);
 			}
 			returnValue = targetActor->callMethod(method, args);
 			break;
@@ -546,7 +572,7 @@ ScriptValue CodeChunk::evaluateMethodCall(BuiltInMethod method, uint paramCount)
 	}
 
 	case kScriptValueTypeCollection: {
-		Common::SharedPtr<Collection> collection = target.asCollection();
+		Collection *collection = methodCallTargetPtr->asCollection();
 		returnValue = collection->callMethod(method, args);
 		break;
 	}
@@ -554,7 +580,7 @@ ScriptValue CodeChunk::evaluateMethodCall(BuiltInMethod method, uint paramCount)
 	default:
 		error("%s: Attempt to call method %s (%d) on unimplemented value type %s (%d)", __func__,
 			builtInMethodToStr(method), static_cast<uint>(method),
-			scriptValueTypeToStr(target.getType()), static_cast<uint>(target.getType()));
+			scriptValueTypeToStr(methodCallTargetPtr->getType()), static_cast<uint>(methodCallTargetPtr->getType()));
 	}
 	return returnValue;
 }
diff --git a/engines/mediastation/mediascript/codechunk.h b/engines/mediastation/mediascript/codechunk.h
index cc0798d56c0..c9d497a564e 100644
--- a/engines/mediastation/mediascript/codechunk.h
+++ b/engines/mediastation/mediascript/codechunk.h
@@ -43,6 +43,7 @@ private:
 
 	ScriptValue evaluateExpression();
 	ScriptValue evaluateExpression(ExpressionType expressionType);
+	void evaluateLValue(ScriptValue *&targetPtr);
 	ScriptValue evaluateOperation();
 	ScriptValue evaluateValue();
 	ScriptValue evaluateVariable();
diff --git a/engines/mediastation/mediascript/collection.cpp b/engines/mediastation/mediascript/collection.cpp
index 4c38285ab14..77f639edcb2 100644
--- a/engines/mediastation/mediascript/collection.cpp
+++ b/engines/mediastation/mediascript/collection.cpp
@@ -28,6 +28,14 @@
 namespace MediaStation {
 
 ScriptValue Collection::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
+	// Debug print the collection contents.
+	debugC(7, kDebugScript, "	COLLECTION: [");
+	for (uint i = 0; i < size(); i++) {
+		const ScriptValue &rhs = operator[](i);
+		debugC(7, kDebugScript, "		%d of %d: %s", i, size(), rhs.getDebugString().c_str());
+	}
+	debugC(7, kDebugScript, "	]");
+
 	ScriptValue returnValue;
 	switch (methodId) {
 	case kAppendMethod:
@@ -190,7 +198,9 @@ int Collection::seek(const ScriptValue &lhs) {
 		const ScriptValue &rhs = operator[](i);
 		debugC(7, kDebugScript, "%s: %d of %d: Checking (%s) == (%s)",
 			__func__, i, size(), lhs.getDebugString().c_str(), rhs.getDebugString().c_str());
-		if (lhs == rhs) {
+
+		// Only compare values if types match.
+		if (lhs.getType() == rhs.getType() && lhs == rhs) {
 			return i;
 		}
 	}
@@ -198,9 +208,11 @@ int Collection::seek(const ScriptValue &lhs) {
 }
 
 void Collection::jumble() {
-	for (uint i = size() - 1; i > 0; --i) {
-		uint j = g_engine->_randomSource.getRandomNumber(size() - 1);
-		SWAP(operator[](i), operator[](j));
+	if (!empty()) {
+		for (uint i = size() - 1; i > 0; --i) {
+			uint j = g_engine->_randomSource.getRandomNumber(size() - 1);
+			SWAP(operator[](i), operator[](j));
+		}
 	}
 }
 
diff --git a/engines/mediastation/mediascript/function.cpp b/engines/mediastation/mediascript/function.cpp
index 1f74591612d..1ec1719b17d 100644
--- a/engines/mediastation/mediascript/function.cpp
+++ b/engines/mediastation/mediascript/function.cpp
@@ -426,7 +426,7 @@ void FunctionManager::script_SetGammaCorrection(Common::Array<ScriptValue> &args
 			return;
 		}
 
-		Common::SharedPtr<Collection> collection = args[0].asCollection();
+		Collection *collection = args[0].asCollection();
 		if (collection->size() != 3) {
 			warning("%s: Collection must contain exactly 3 elements, got %u", __func__, collection->size());
 			return;
@@ -456,7 +456,7 @@ void FunctionManager::script_GetDefaultGammaCorrection(Common::Array<ScriptValue
 	double red, green, blue;
 	g_engine->getDisplayManager()->getDefaultGammaValues(red, green, blue);
 
-	Common::SharedPtr<Collection> collection = Common::SharedPtr<Collection>(new Collection());
+	Collection *collection = new Collection();
 	ScriptValue redValue;
 	redValue.setToFloat(red);
 	collection->push_back(redValue);
@@ -480,7 +480,7 @@ void FunctionManager::script_GetCurrentGammaCorrection(Common::Array<ScriptValue
 
 	double red, green, blue;
 	g_engine->getDisplayManager()->getGammaValues(red, green, blue);
-	Common::SharedPtr<Collection> collection = Common::SharedPtr<Collection>(new Collection());
+	Collection *collection = new Collection();
 
 	ScriptValue redValue;
 	redValue.setToFloat(red);
diff --git a/engines/mediastation/mediascript/scriptvalue.cpp b/engines/mediastation/mediascript/scriptvalue.cpp
index 1f5010af0d5..c673cf2f9f5 100644
--- a/engines/mediastation/mediascript/scriptvalue.cpp
+++ b/engines/mediastation/mediascript/scriptvalue.cpp
@@ -76,7 +76,7 @@ ScriptValue::ScriptValue(ParameterReadStream *stream) {
 
 	case kScriptValueTypeCollection: {
 		uint totalItems = stream->readTypedUint16();
-		Common::SharedPtr<Collection> collection(new Collection);
+		Collection *collection = new Collection;
 		for (uint i = 0; i < totalItems; i++) {
 			ScriptValue collectionValue = ScriptValue(stream);
 			collection->push_back(collectionValue);
@@ -102,6 +102,79 @@ ScriptValue::ScriptValue(ParameterReadStream *stream) {
 	}
 }
 
+void ScriptValue::clearCollection() {
+	if (_collection) {
+		delete _collection;
+		_collection = nullptr;
+	}
+}
+
+ScriptValue::~ScriptValue() {
+	clearCollection();
+}
+
+ScriptValue::ScriptValue(const ScriptValue &other) {
+	clearCollection();
+	copyFrom(other);
+}
+
+void ScriptValue::operator=(const ScriptValue &other) {
+	clearCollection();
+	copyFrom(other);
+}
+
+void ScriptValue::copyFrom(const ScriptValue &other) {
+	_type = other._type;
+
+	switch (_type) {
+	case kScriptValueTypeEmpty:
+		// Nothing to copy for empty type.
+		break;
+
+	case kScriptValueTypeFloat:
+		_u.d = other._u.d;
+		break;
+
+	case kScriptValueTypeBool:
+		_u.b = other._u.b;
+		break;
+
+	case kScriptValueTypeTime:
+		_u.d = other._u.d;
+		break;
+
+	case kScriptValueTypeParamToken:
+		_u.paramToken = other._u.paramToken;
+		break;
+
+	case kScriptValueTypeActorId:
+		_u.actorId = other._u.actorId;
+		break;
+
+	case kScriptValueTypeString:
+		_string = other._string;
+		break;
+
+	case kScriptValueTypeCollection:
+		if (other._collection) {
+			// We always need a deep copy.
+			_collection = new Collection(*other._collection);
+		}
+		break;
+
+	case kScriptValueTypeFunctionId:
+		_u.functionId = other._u.functionId;
+		break;
+
+	case kScriptValueTypeMethodId:
+		_u.methodId = other._u.methodId;
+		break;
+
+	default:
+		error("%s: Got unknown script value type %s", __func__, scriptValueTypeToStr(_type));
+	}
+}
+
 void ScriptValue::setToFloat(uint i) {
 	setToFloat(static_cast<double>(i));
 }
@@ -193,12 +266,13 @@ Common::String ScriptValue::asString() const {
 	}
 }
 
-void ScriptValue::setToCollection(Common::SharedPtr<Collection> collection) {
+void ScriptValue::setToCollection(Collection *collection) {
 	_type = kScriptValueTypeCollection;
+	clearCollection();
 	_collection = collection;
 }
 
-Common::SharedPtr<Collection> ScriptValue::asCollection() const {
+Collection *ScriptValue::asCollection() const {
 	if (_type == kScriptValueTypeCollection) {
 		return _collection;
 	} else {
@@ -409,7 +483,7 @@ bool ScriptValue::compare(Opcode op, double left, double right) {
 	}
 }
 
-bool ScriptValue::compare(Opcode op, Common::SharedPtr<Collection> left, Common::SharedPtr<Collection> right) {
+bool ScriptValue::compare(Opcode op, Collection *left, Collection *right) {
 	switch (op) {
 	case kOpcodeEquals:
 		return (left == right);
diff --git a/engines/mediastation/mediascript/scriptvalue.h b/engines/mediastation/mediascript/scriptvalue.h
index 7b4270fd88b..e247ec55f06 100644
--- a/engines/mediastation/mediascript/scriptvalue.h
+++ b/engines/mediastation/mediascript/scriptvalue.h
@@ -22,7 +22,6 @@
 #ifndef MEDIASTATION_MEDIASCRIPT_SCRIPTVALUE_H
 #define MEDIASTATION_MEDIASCRIPT_SCRIPTVALUE_H
 
-#include "common/ptr.h"
 #include "common/str.h"
 
 #include "mediastation/datafile.h"
@@ -35,8 +34,10 @@ class Actor;
 
 class ScriptValue {
 public:
-	ScriptValue() : _type(kScriptValueTypeEmpty) {}
+	ScriptValue() : _type(kScriptValueTypeEmpty), _collection(nullptr) {}
 	ScriptValue(ParameterReadStream *stream);
+	ScriptValue(const ScriptValue &other);
+	~ScriptValue();
 
 	ScriptValueType getType() const { return _type; }
 
@@ -61,8 +62,8 @@ public:
 	void setToString(const Common::String &string);
 	Common::String asString() const;
 
-	void setToCollection(Common::SharedPtr<Collection> collection);
-	Common::SharedPtr<Collection> asCollection() const;
+	void setToCollection(Collection *collection);
+	Collection *asCollection() const;
 
 	void setToFunctionId(uint functionId);
 	uint asFunctionId() const;
@@ -72,6 +73,7 @@ public:
 
 	Common::String getDebugString() const;
 
+	void operator=(const ScriptValue &other);
 	bool operator==(const ScriptValue &other) const;
 	bool operator!=(const ScriptValue &other) const;
 	bool operator<(const ScriptValue &other) const;
@@ -101,7 +103,9 @@ private:
 		BuiltInMethod methodId;
 	} _u;
 	Common::String _string;
-	Common::SharedPtr<Collection> _collection;
+	Collection *_collection = nullptr;
+	void clearCollection();
+	void copyFrom(const ScriptValue &other);
 
 	static bool compare(Opcode op, const ScriptValue &left, const ScriptValue &right);
 	static bool compareEmptyValues(Opcode op);
@@ -109,7 +113,7 @@ private:
 	static bool compare(Opcode op, uint left, uint right);
 	static bool compare(Opcode op, bool left, bool right);
 	static bool compare(Opcode op, double left, double right);
-	static bool compare(Opcode op, Common::SharedPtr<Collection> left, Common::SharedPtr<Collection> right);
+	static bool compare(Opcode op, Collection *left, Collection *right);
 
 	static ScriptValue evalMathOperation(Opcode op, const ScriptValue &left, const ScriptValue &right);
 	static double binaryMathOperation(Opcode op, double left, double right);


Commit: 2505aa1acb73464145ab7639df6ee17b1fb208af
    https://github.com/scummvm/scummvm/commit/2505aa1acb73464145ab7639df6ee17b1fb208af
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-02-24T21:10:58-05:00

Commit Message:
MEDIASTATION: Properly pass script return values back

Changed paths:
    engines/mediastation/mediascript/codechunk.cpp
    engines/mediastation/mediascript/codechunk.h


diff --git a/engines/mediastation/mediascript/codechunk.cpp b/engines/mediastation/mediascript/codechunk.cpp
index 3e6fda47f52..e64b8841d9b 100644
--- a/engines/mediastation/mediascript/codechunk.cpp
+++ b/engines/mediastation/mediascript/codechunk.cpp
@@ -40,17 +40,15 @@ ScriptValue CodeChunk::executeNextBlock() {
 	debugC(7, kDebugScript, "%s: Entering new block (blockSize: %d, startingPos: %lld)",
 		__func__, blockSize, static_cast<long long int>(startingPos));
 
-	ScriptValue returnValue;
 	ExpressionType expressionType = static_cast<ExpressionType>(_bytecode->readTypedUint16());
 	while (expressionType != kExpressionTypeEmpty && !_returnImmediately) {
-		returnValue = evaluateExpression(expressionType);
+		evaluateExpression(expressionType);
 		expressionType = static_cast<ExpressionType>(_bytecode->readTypedUint16());
 
-		if (expressionType == kExpressionTypeEmpty) {
-			debugC(7, kDebugScript, "%s: Done executing block due to end of chunk", __func__);
-		}
 		if (_returnImmediately) {
-			debugC(7, kDebugScript, "%s: Done executing block due to script requesting immediate return", __func__);
+			debugC(7, kDebugScript, "%s: Done executing block due to script returning value (%s)", __func__, _returnValue.getDebugString().c_str());
+		} else if (expressionType == kExpressionTypeEmpty) {
+			debugC(7, kDebugScript, "%s: Done executing block due to end of chunk", __func__);
 		}
 	}
 
@@ -61,7 +59,8 @@ ScriptValue CodeChunk::executeNextBlock() {
 			error("%s: Expected to have read %d script bytes, actually read %d", __func__, blockSize, bytesRead);
 		}
 	}
-	return returnValue;
+
+	return _returnValue;
 }
 
 void CodeChunk::skipNextBlock() {
@@ -193,7 +192,7 @@ ScriptValue CodeChunk::evaluateOperation() {
 		break;
 
 	case kOpcodeReturn:
-		returnValue = evaluateReturn();
+		evaluateReturn();
 		break;
 
 	case kOpcodeReturnNoValue:
@@ -594,10 +593,9 @@ void CodeChunk::evaluateDeclareLocals() {
 	_locals = Common::Array<ScriptValue>(localVariableCount);
 }
 
-ScriptValue CodeChunk::evaluateReturn() {
-	ScriptValue returnValue = evaluateExpression();
+void CodeChunk::evaluateReturn() {
+	_returnValue = evaluateExpression();
 	_returnImmediately = true;
-	return returnValue;
 }
 
 void CodeChunk::evaluateReturnNoValue() {
diff --git a/engines/mediastation/mediascript/codechunk.h b/engines/mediastation/mediascript/codechunk.h
index c9d497a564e..1959502227b 100644
--- a/engines/mediastation/mediascript/codechunk.h
+++ b/engines/mediastation/mediascript/codechunk.h
@@ -60,12 +60,13 @@ private:
 	ScriptValue evaluateMethodCall(bool isIndirect = false);
 	ScriptValue evaluateMethodCall(BuiltInMethod method, uint paramCount);
 	void evaluateDeclareLocals();
-	ScriptValue evaluateReturn();
+	void evaluateReturn();
 	void evaluateReturnNoValue();
 	void evaluateWhileLoop();
 
 	static const uint MAX_LOOP_ITERATION_COUNT = 1000;
 	bool _returnImmediately = false;
+	ScriptValue _returnValue;
 	Common::Array<ScriptValue> _locals;
 	Common::Array<ScriptValue> *_args = nullptr;
 	ParameterReadStream *_bytecode = nullptr;


Commit: 1ca640151e36825c99c4813b5558f5d9c1ac6269
    https://github.com/scummvm/scummvm/commit/1ca640151e36825c99c4813b5558f5d9c1ac6269
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-02-24T21:10:58-05:00

Commit Message:
MEDIASTATION: Make caching-related parameters consistently read

Changed paths:
    engines/mediastation/actor.h
    engines/mediastation/actors/image.cpp
    engines/mediastation/actors/image.h
    engines/mediastation/actors/movie.cpp
    engines/mediastation/actors/movie.h
    engines/mediastation/actors/sound.cpp
    engines/mediastation/actors/sound.h
    engines/mediastation/actors/sprite.cpp
    engines/mediastation/actors/sprite.h
    engines/mediastation/actors/text.cpp
    engines/mediastation/actors/text.h


diff --git a/engines/mediastation/actor.h b/engines/mediastation/actor.h
index f4d6da1edb0..9c3b10a9084 100644
--- a/engines/mediastation/actor.h
+++ b/engines/mediastation/actor.h
@@ -81,12 +81,13 @@ enum ActorHeaderSectionType {
 	kActorHeaderZIndex = 0x001e,
 	kActorHeaderStartup = 0x001f,
 	kActorHeaderTransparency = 0x0020,
-	kActorHeaderHasOwnSubfile = 0x0021,
+	kActorHeaderDiscardAfterUse = 0x0021,
 	kActorHeaderCursorResourceId = 0x0022,
 	kActorHeaderFrameRate = 0x0024,
 	kActorHeaderLoadType = 0x0032,
 	kActorHeaderSoundInfo = 0x0033,
-	kActorHeaderMovieLoadType = 0x0037,
+	kActorHeaderCachingEnabled = 0x0034,
+	kActorHeaderInstallType = 0x0037,
 	kActorHeaderSpriteChunkCount = 0x03e8,
 	kActorHeaderPalette = 0x05aa,
 	kActorHeaderDissolveFactor = 0x05dc,
diff --git a/engines/mediastation/actors/image.cpp b/engines/mediastation/actors/image.cpp
index 593df22a2cc..a60ddf48314 100644
--- a/engines/mediastation/actors/image.cpp
+++ b/engines/mediastation/actors/image.cpp
@@ -47,7 +47,7 @@ void ImageActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 		break;
 
 	case kActorHeaderLoadType:
-		_loadType = chunk.readTypedByte();
+		_decompressImmediately = chunk.readTypedByte();
 		break;
 
 	case kActorHeaderX:
diff --git a/engines/mediastation/actors/image.h b/engines/mediastation/actors/image.h
index e005fedc9ee..7ff77a56d20 100644
--- a/engines/mediastation/actors/image.h
+++ b/engines/mediastation/actors/image.h
@@ -53,7 +53,7 @@ public:
 
 private:
 	Common::SharedPtr<ImageAsset> _asset;
-	uint _loadType = 0;
+	bool _decompressImmediately = false;
 	int _xOffset = 0;
 	int _yOffset = 0;
 	uint _actorReference = 0;
diff --git a/engines/mediastation/actors/movie.cpp b/engines/mediastation/actors/movie.cpp
index 1c9630177da..8050888b3c0 100644
--- a/engines/mediastation/actors/movie.cpp
+++ b/engines/mediastation/actors/movie.cpp
@@ -121,27 +121,21 @@ void StreamMovieActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramT
 		break;
 	}
 
-	case kActorHeaderMovieLoadType:
-		_loadType = chunk.readTypedByte();
-		break;
-
 	case kActorHeaderChannelIdent:
 		_channelIdent = chunk.readTypedChannelIdent();
 		registerWithStreamManager();
 		break;
 
-	case kActorHeaderHasOwnSubfile: {
-		bool hasOwnSubfile = static_cast<bool>(chunk.readTypedByte());
-		if (!hasOwnSubfile) {
-			error("%s: StreamMovieActor doesn't have a subfile", __func__);
-		}
-		break;
-	}
 
 	case kActorHeaderStartup:
 		_isVisible = static_cast<bool>(chunk.readTypedByte());
 		break;
 
+	case kActorHeaderDiscardAfterUse:
+		// The original just reads this and throws it away.
+		chunk.readTypedByte();
+		break;
+
 	case kActorHeaderMovieAudioChannelIdent: {
 		ChannelIdent soundChannelIdent = chunk.readTypedChannelIdent();
 		_streamSound->setChannelIdent(soundChannelIdent);
@@ -149,6 +143,17 @@ void StreamMovieActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramT
 		break;
 	}
 
+	case kActorHeaderCachingEnabled:
+		_shouldCache = static_cast<bool>(chunk.readTypedByte());
+		break;
+
+	case kActorHeaderInstallType:
+		// In the original, this controls behavior if the files are NOT installed. But since
+		// the "installation" is just copying from the CD-ROM, we can treat the game as always
+		// installed. So just throw away this value.
+		chunk.readTypedByte();
+		break;
+
 	case kActorHeaderMovieAnimationChannelIdent: {
 		ChannelIdent framesChannelIdent = chunk.readTypedChannelIdent();
 		_streamFrames->setChannelIdent(framesChannelIdent);
diff --git a/engines/mediastation/actors/movie.h b/engines/mediastation/actors/movie.h
index fd017c1641b..bfeaef3138d 100644
--- a/engines/mediastation/actors/movie.h
+++ b/engines/mediastation/actors/movie.h
@@ -147,7 +147,7 @@ private:
 	uint _chunkCount = 0;
 	double _frameRate = 0;
 
-	uint _loadType = 0;
+	bool _shouldCache = false;
 	bool _isPlaying = false;
 	bool _hasStill = false;
 
diff --git a/engines/mediastation/actors/sound.cpp b/engines/mediastation/actors/sound.cpp
index 70350092eab..2e0daff3601 100644
--- a/engines/mediastation/actors/sound.cpp
+++ b/engines/mediastation/actors/sound.cpp
@@ -51,16 +51,25 @@ void SoundActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 		registerWithStreamManager();
 		break;
 
-	case kActorHeaderHasOwnSubfile:
-		_hasOwnSubfile = static_cast<bool>(chunk.readTypedByte());
+	case kActorHeaderDiscardAfterUse:
+		_discardAfterUse = static_cast<bool>(chunk.readTypedByte());
 		break;
 
 	case kActorHeaderSoundInfo:
 		_sequence.readParameters(chunk);
 		break;
 
-	case kActorHeaderMovieLoadType:
-		_loadType = chunk.readTypedByte();
+	case kActorHeaderCachingEnabled:
+		// This controls some caching behavior in the original, but since that is not currently
+		// implemented here, just throw it away.
+		chunk.readTypedByte();
+		break;
+
+	case kActorHeaderInstallType:
+		// In the original, this controls behavior if the files are NOT installed. But since
+		// the "installation" is just copying from the CD-ROM, we can treat the game as always
+		// installed. So just throw away this value.
+		chunk.readTypedByte();
 		break;
 
 	default:
diff --git a/engines/mediastation/actors/sound.h b/engines/mediastation/actors/sound.h
index 23754a26a0c..df1bd2e1221 100644
--- a/engines/mediastation/actors/sound.h
+++ b/engines/mediastation/actors/sound.h
@@ -50,8 +50,7 @@ public:
 private:
 	ImtStreamFeed *_streamFeed = nullptr;
 	bool _isLoadedFromChunk = false;
-	uint _loadType = 0;
-	bool _hasOwnSubfile = false;
+	bool _discardAfterUse = false;
 	SoundPlayState _playState = kSoundStopped;
 	AudioSequence _sequence;
 
diff --git a/engines/mediastation/actors/sprite.cpp b/engines/mediastation/actors/sprite.cpp
index d312e745adc..79701b8f20f 100644
--- a/engines/mediastation/actors/sprite.cpp
+++ b/engines/mediastation/actors/sprite.cpp
@@ -56,16 +56,16 @@ void SpriteMovieActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramT
 		_asset = Common::SharedPtr<SpriteAsset>(new SpriteAsset);
 		break;
 
+	case kActorHeaderStartup:
+		_isVisible = static_cast<bool>(chunk.readTypedByte());
+		break;
+
 	case kActorHeaderFrameRate:
 		_frameRate = static_cast<uint32>(chunk.readTypedDouble());
 		break;
 
 	case kActorHeaderLoadType:
-		_loadType = chunk.readTypedByte();
-		break;
-
-	case kActorHeaderStartup:
-		_isVisible = static_cast<bool>(chunk.readTypedByte());
+		_decompressImmediately = static_cast<bool>(chunk.readTypedByte());
 		break;
 
 	case kActorHeaderSpriteChunkCount:
diff --git a/engines/mediastation/actors/sprite.h b/engines/mediastation/actors/sprite.h
index 9c026931073..0311f82d115 100644
--- a/engines/mediastation/actors/sprite.h
+++ b/engines/mediastation/actors/sprite.h
@@ -81,7 +81,7 @@ private:
 	const uint DEFAULT_FORWARD_CLIP_ID = 0x4B0;
 	const uint DEFAULT_BACKWARD_CLIP_ID = 0x4B1;
 
-	uint _loadType = 0;
+	bool _decompressImmediately = false;
 	uint _frameRate = 0;
 	uint _actorReference = 0;
 	Common::HashMap<uint, SpriteMovieClip> _clips;
diff --git a/engines/mediastation/actors/text.cpp b/engines/mediastation/actors/text.cpp
index 346d474db80..8845c0f3c6d 100644
--- a/engines/mediastation/actors/text.cpp
+++ b/engines/mediastation/actors/text.cpp
@@ -31,7 +31,8 @@ void TextActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 		break;
 
 	case kActorHeaderLoadType:
-		_loadType = chunk.readTypedByte();
+		// The original doesn't seem to use this, so we'll throw it away.
+		chunk.readTypedByte();
 		break;
 
 	case kActorHeaderFontActorId: {
diff --git a/engines/mediastation/actors/text.h b/engines/mediastation/actors/text.h
index b53ffbdd8ba..e5c2a12b155 100644
--- a/engines/mediastation/actors/text.h
+++ b/engines/mediastation/actors/text.h
@@ -58,7 +58,6 @@ public:
 private:
 	static const uint CURSOR_CHAR_ID = 0x104;
 	bool _isEditable = false;
-	uint _loadType = 0;
 	Common::String _text;
 	uint _maxLength = 0;
 	FontActor *_fontActor = nullptr;




More information about the Scummvm-git-logs mailing list