[Scummvm-git-logs] scummvm master -> 761bf9511afd93fb2eaddfcbf3693dbdea852640

npjg noreply at scummvm.org
Wed Dec 31 16:46:26 UTC 2025


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

Summary:
5b82b45306 MEDIASTATION: Implement some built-in script functions
e3b87d5243 MEDIASTATION: Make searching Windows cursors by name be case-insensitive
d875bada24 MEDIASTATION: Give some script events better names
cacdde38ce MEDIASTATION: Fix playing some sprites
1a566e410b MEDIASTATION: Implement stream loading much closer to the original
77b8966081 MEDIASTATION: Factor out event processing loop
4f6f8bba30 MEDIASTATION: Correctly reset script function return status
6fa5930d13 MEDIASTATION: Add initial support for camera actors
761bf9511a MEDIASTATION: Add Mac detection entries and update some others


Commit: 5b82b453063e4ebdc45e7327f5732f2f2e8baf00
    https://github.com/scummvm/scummvm/commit/5b82b453063e4ebdc45e7327f5732f2f2e8baf00
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2025-12-31T11:45:01-05:00

Commit Message:
MEDIASTATION: Implement some built-in script functions

Changed paths:
    engines/mediastation/actor.cpp
    engines/mediastation/graphics.cpp
    engines/mediastation/graphics.h
    engines/mediastation/mediascript/collection.cpp
    engines/mediastation/mediascript/eventhandler.cpp
    engines/mediastation/mediascript/eventhandler.h
    engines/mediastation/mediascript/function.cpp
    engines/mediastation/mediascript/function.h
    engines/mediastation/mediascript/scriptconstants.cpp
    engines/mediastation/mediascript/scriptconstants.h
    engines/mediastation/mediascript/scriptvalue.cpp
    engines/mediastation/mediascript/scriptvalue.h


diff --git a/engines/mediastation/actor.cpp b/engines/mediastation/actor.cpp
index cbe77e3f9bd..d62c23024b7 100644
--- a/engines/mediastation/actor.cpp
+++ b/engines/mediastation/actor.cpp
@@ -62,7 +62,7 @@ void Actor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 		for (EventHandler *existingEventHandler : eventHandlersForType) {
 			if (existingEventHandler->_argumentValue == eventHandler->_argumentValue) {
 				error("%s: Event handler for %s (%s) already exists", __func__,
-					  eventTypeToStr(eventHandler->_type), eventHandler->getDebugHeader().c_str());
+					  eventTypeToStr(eventHandler->_type), eventHandler->_argumentValue.getDebugString().c_str());
 			}
 		}
 		eventHandlersForType.push_back(eventHandler);
@@ -82,7 +82,8 @@ void Actor::loadIsComplete() {
 }
 
 ScriptValue Actor::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
-	error("%s: Got unimplemented method call 0x%x (%s)", __func__, static_cast<uint>(methodId), builtInMethodToStr(methodId));
+	warning("%s: Got unimplemented method call 0x%x (%s)", __func__, static_cast<uint>(methodId), builtInMethodToStr(methodId));
+	return ScriptValue();
 }
 
 void Actor::readChunk(Chunk &chunk) {
diff --git a/engines/mediastation/graphics.cpp b/engines/mediastation/graphics.cpp
index aa889782f18..e6d9dd0f264 100644
--- a/engines/mediastation/graphics.cpp
+++ b/engines/mediastation/graphics.cpp
@@ -965,4 +965,23 @@ Graphics::ManagedSurface VideoDisplayManager::decompressRle8Bitmap(
 	return dest;
 }
 
+void VideoDisplayManager::setGammaValues(double red, double green, double blue) {
+	_redGamma = red;
+	_blueGamma = blue;
+	_greenGamma = green;
+	// TODO: Actually perform gamma correction.
+}
+
+void VideoDisplayManager::getDefaultGammaValues(double &red, double &green, double &blue) {
+	red = DEFAULT_GAMMA_VALUE;
+	green = DEFAULT_GAMMA_VALUE;
+	blue = DEFAULT_GAMMA_VALUE;
+}
+
+void VideoDisplayManager::getGammaValues(double &red, double &green, double &blue) {
+	red = _redGamma;
+	green = _greenGamma;
+	blue = _blueGamma;
+}
+
 } // End of namespace MediaStation
diff --git a/engines/mediastation/graphics.h b/engines/mediastation/graphics.h
index 5077c1a446b..55807923946 100644
--- a/engines/mediastation/graphics.h
+++ b/engines/mediastation/graphics.h
@@ -103,6 +103,10 @@ public:
 	void performUpdateDirty();
 	void performUpdateAll();
 
+	void setGammaValues(double red, double green, double blue);
+	void getDefaultGammaValues(double &red, double &green, double &blue);
+	void getGammaValues(double &red, double &green, double &blue);
+
 private:
 	MediaStationEngine *_vm = nullptr;
 	Graphics::Screen *_screen = nullptr;
@@ -110,6 +114,11 @@ private:
 	Common::Array<ScriptValue> _scheduledTransitionOnSync;
 	double _defaultTransitionTime = 0.0;
 
+	const double DEFAULT_GAMMA_VALUE = 1.0;
+	double _redGamma = 1.0;
+	double _greenGamma = 1.0;
+	double _blueGamma = 1.0;
+
 	void readAndEffectTransition(Chunk &chunk);
 	void readAndRegisterPalette(Chunk &chunk);
 
diff --git a/engines/mediastation/mediascript/collection.cpp b/engines/mediastation/mediascript/collection.cpp
index 72825d56d76..52b64037bb7 100644
--- a/engines/mediastation/mediascript/collection.cpp
+++ b/engines/mediastation/mediascript/collection.cpp
@@ -130,8 +130,7 @@ void Collection::apply(const Common::Array<ScriptValue> &args) {
 	uint functionId = args[0].asFunctionId();
 	for (const ScriptValue &item : *this) {
 		argsToApply[0] = item;
-		// TODO: Need to create and call FunctionManager.
-		warning("%s: Applying function %d not implemented", __func__, functionId);
+		g_engine->getFunctionManager()->call(functionId, argsToApply);
 	}
 }
 
diff --git a/engines/mediastation/mediascript/eventhandler.cpp b/engines/mediastation/mediascript/eventhandler.cpp
index 3a5a01f9c1e..d97321475bf 100644
--- a/engines/mediastation/mediascript/eventhandler.cpp
+++ b/engines/mediastation/mediascript/eventhandler.cpp
@@ -37,7 +37,7 @@ 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 argValue = getDebugHeader();
+	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());
 
 	// The only argument that can be provided to an
@@ -53,26 +53,4 @@ EventHandler::~EventHandler() {
 	_code = nullptr;
 }
 
-Common::String EventHandler::getDebugHeader() {
-	switch (_argumentValue.getType()) {
-	case kScriptValueTypeEmpty:
-		return "(no argument)";
-
-	case kScriptValueTypeFloat:
-		return Common::String::format("(float = %f)", _argumentValue.asFloat());
-
-	case kScriptValueTypeActorId:
-		return Common::String::format("(context = %d)", _argumentValue.asActorId());
-
-	case kScriptValueTypeTime:
-		return Common::String::format("(time = %f)", _argumentValue.asTime());
-
-	case kScriptValueTypeParamToken:
-		return Common::String::format("(token = %d)", _argumentValue.asParamToken());
-
-	default:
-		return Common::String::format("(arg type %s)", scriptValueTypeToStr(_argumentValue.getType()));
-	}
-}
-
 } // End of namespace MediaStation
diff --git a/engines/mediastation/mediascript/eventhandler.h b/engines/mediastation/mediascript/eventhandler.h
index bee34d4c7ae..66c36f9f4df 100644
--- a/engines/mediastation/mediascript/eventhandler.h
+++ b/engines/mediastation/mediascript/eventhandler.h
@@ -36,7 +36,6 @@ public:
 	~EventHandler();
 
 	ScriptValue execute(uint actorId);
-	Common::String getDebugHeader();
 	EventType _type;
 	ScriptValue _argumentValue;
 
diff --git a/engines/mediastation/mediascript/function.cpp b/engines/mediastation/mediascript/function.cpp
index a829d9e4485..5075cbc4b21 100644
--- a/engines/mediastation/mediascript/function.cpp
+++ b/engines/mediastation/mediascript/function.cpp
@@ -72,38 +72,479 @@ ScriptValue FunctionManager::call(uint functionId, Common::Array<ScriptValue> &a
 	ScriptValue returnValue;
 
 	// The original had a complex function registration system that I deemed too uselessly complex to
-	// reimplement. Here, we get basically the same behaviour by checking for default functions first,
-	// then falling through to title-defined functions.
+	// reimplement. First, we try executing the title-defined function. We try this first because
+	// later engine versions used some functions IDs that previously mapped to built-in functions in
+	// earlier engine versions. So we will try executing the title-defined function first and only then
+	// fall back to the built-in functions.
+	ScriptFunction *scriptFunction = _functions.getValOrDefault(functionId);
+	if (scriptFunction != nullptr) {
+		returnValue = scriptFunction->execute(args);
+		return returnValue;
+	}
+
+	// If there was no title-defined function, next check for built-in functions.
 	switch (functionId) {
+	case kRandomFunction:
+	case kLegacy_RandomFunction:
+		assert(args.size() == 2);
+		script_Random(args, returnValue);
+		break;
+
+	case kTimeOfDayFunction:
+	case kLegacy_TimeOfDayFunction:
+		script_TimeOfDay(args, returnValue);
+		break;
+
 	case kEffectTransitionFunction:
+	case kLegacy_EffectTransitionFunction:
 		g_engine->getDisplayManager()->effectTransition(args);
 		break;
 
 	case kEffectTransitionOnSyncFunction:
+	case kLegacy_EffectTransitionOnSyncFunction:
 		g_engine->getDisplayManager()->setTransitionOnSync(args);
 		break;
 
+	case kPlatformFunction:
+	case kLegacy_PlatformFunction:
+		assert(args.empty());
+		script_GetPlatform(args, returnValue);
+		break;
+
+	case kSquareRootFunction:
+	case kLegacy_SquareRootFunction:
+		assert(args.size() == 1);
+		script_SquareRoot(args, returnValue);
+		break;
+
+	case kGetUniqueRandomFunction:
+	case kLegacy_GetUniqueRandomFunction:
+		assert(args.size() >= 2);
+		script_GetUniqueRandom(args, returnValue);
+		break;
+
+	case kCurrentRunTimeFunction:
+		script_CurrentRunTime(args, returnValue);
+		break;
+
+	case kSetGammaCorrectionFunction:
+		script_SetGammaCorrection(args, returnValue);
+		break;
+
+	case kGetDefaultGammaCorrectionFunction:
+		script_GetDefaultGammaCorrection(args, returnValue);
+		break;
+
+	case kGetCurrentGammaCorrectionFunction:
+		script_GetCurrentGammaCorrection(args, returnValue);
+		break;
+
+	case kSetAudioVolumeFunction:
+		assert(args.size() == 1);
+		script_SetAudioVolume(args, returnValue);
+		break;
+
+	case kGetAudioVolumeFunction:
+		assert(args.empty());
+		script_GetAudioVolume(args, returnValue);
+		break;
+
+	case kSystemLanguagePreferenceFunction:
+	case kLegacy_SystemLanguagePreferenceFunction:
+		script_SystemLanguagePreference(args, returnValue);
+		break;
+
+	case kSetRegistryFunction:
+		script_SetRegistry(args, returnValue);
+		break;
+
+	case kGetRegistryFunction:
+		script_GetRegistry(args, returnValue);
+		break;
+
+	case kSetProfileFunction:
+		script_SetProfile(args, returnValue);
+		break;
+
+	case kMazeGenerateFunction:
+		script_MazeGenerate(args, returnValue);
+		break;
+
+	case kMazeApplyMoveMaskFunction:
+		script_MazeApplyMoveMask(args, returnValue);
+		break;
+
+	case kMazeSolveFunction:
+		script_MazeSolve(args, returnValue);
+		break;
+
+	case kBeginTimedIntervalFunction:
+		script_BeginTimedInterval(args, returnValue);
+		break;
+
+	case kEndTimedIntervalFunction:
+		script_EndTimedInterval(args, returnValue);
+		break;
+
 	case kDrawingFunction:
-		warning("STUB: %s", builtInFunctionToStr(static_cast<BuiltInFunction>(functionId)));
+		script_Drawing(args, returnValue);
 		break;
 
-	case kUnk1Function:
-		warning("%s: Function Unk1 Not implemented", __func__);
-		returnValue.setToFloat(1.0);
+	case kLegacy_DebugPrintFunction:
+		script_DebugPrint(args, returnValue);
 		break;
 
-	default: {
-		// Execute the title-defined function here.
-		ScriptFunction *scriptFunction = _functions.getValOrDefault(functionId);
-		if (scriptFunction != nullptr) {
-			returnValue = scriptFunction->execute(args);
-		} else {
-			error("%s: Unimplemented function 0x%02x", __func__, functionId);
+	default:
+		// If we got here, that means there was neither a title-defined nor a built-in function
+		// for this ID, so we can now declare it unimplemented. This is a warning instead of an error
+		// so execution can continue, but if the function is expected to return anything, there will
+		// likely be an error about attempting to assign a null value to a variable.
+		warning("%s: Unimplemented function 0x%02x", __func__, functionId);
+	}
+
+	return returnValue;
+}
+
+void FunctionManager::script_GetPlatform(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+	Common::Platform platform = g_engine->getPlatform();
+	switch (platform) {
+	case Common::Platform::kPlatformWindows:
+		returnValue.setToParamToken(kPlatformParamTokenWindows);
+		break;
+
+	case Common::Platform::kPlatformMacintosh:
+		returnValue.setToParamToken(kPlatformParamTokenWindows);
+		break;
+
+	default:
+		warning("%s: Unknown platform %d", __func__, static_cast<int>(platform));
+		returnValue.setToParamToken(kPlatformParamTokenUnknown);
+	}
+}
+
+void FunctionManager::script_Random(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+	// This function takes in a range, and then generates a random value within that range.
+	ScriptValue bottomArg = args[0];
+	ScriptValue topArg = args[1];
+	if (bottomArg.getType() != topArg.getType()) {
+		error("%s: Both arguments must be of same type", __func__);
+	}
+
+	ScriptValueType type = args[0].getType();
+	double bottom = 0.0;
+	double top = 0.0;
+	bool treatAsInteger = false;
+	switch (type) {
+	case kScriptValueTypeFloat: {
+		// For numeric values, treat them as integers (floor values).
+		bottom = floor(bottomArg.asFloat());
+		top = floor(topArg.asFloat());
+		treatAsInteger = true;
+		break;
+	}
+
+	case kScriptValueTypeBool: {
+		// Convert boolean values to numbers.
+		bottom = bottomArg.asBool() ? 1.0 : 0.0;
+		top = topArg.asBool() ? 1.0 : 0.0;
+		treatAsInteger = true;
+		break;
+	}
+
+	case kScriptValueTypeTime: {
+		// Treat time values as capable of having fractional seconds.
+		bottom = bottomArg.asTime();
+		top = topArg.asTime();
+		treatAsInteger = false;
+		break;
+	}
+
+	default:
+		error("%s: Invalid argument type: %s", __func__, scriptValueTypeToStr(type));
+	}
+
+	// Ensure proper inclusive ordering of bottom and top.
+	if (top < bottom) {
+		SWAP(top, bottom);
+	}
+
+	// Calculate random value in range.
+	double range = top - bottom;
+	uint randomValue = g_engine->_randomSource.getRandomNumber(UINT32_MAX);
+	double randomFloat = (static_cast<double>(randomValue) * range) / static_cast<double>(UINT32_MAX) + bottom;
+	if (treatAsInteger) {
+		randomFloat = floor(randomFloat);
+	}
+
+	// Set result based on original argument type.
+	switch (type) {
+	case kScriptValueTypeFloat:
+		returnValue.setToFloat(randomFloat);
+		break;
+
+	case kScriptValueTypeBool: {
+		bool boolResult = (randomFloat != 0.0);
+		returnValue.setToBool(boolResult);
+		break;
+	}
+
+	case kScriptValueTypeTime:
+		returnValue.setToTime(randomFloat);
+		break;
+
+	default:
+		error("%s: Invalid argument type: %s", __func__, scriptValueTypeToStr(type));
+	}
+}
+
+void FunctionManager::script_TimeOfDay(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+	warning("STUB: TimeOfDay");
+}
+
+void FunctionManager::script_SquareRoot(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+	if (args[0].getType() != kScriptValueTypeFloat) {
+		error("%s: Numeric value required", __func__);
+	}
+
+	double value = args[0].asFloat();
+	if (value < 0.0) {
+		error("%s: Argument must be nonnegative", __func__);
+	}
+
+	double result = sqrt(value);
+	returnValue.setToFloat(result);
+}
+
+void FunctionManager::script_GetUniqueRandom(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+	// Unlike the regular Random which simply returns any random number in a range, GetUniqueRandom allows the caller
+	// to specify numbers that should NOT be returned (the third arg and onward), making it useful for generating random
+	// values that haven't been used before or avoiding specific unwanted values.
+	for (ScriptValue arg : args) {
+		if (arg.getType() != kScriptValueTypeFloat) {
+			error("%s: All arguments must be numeric", __func__);
 		}
 	}
+
+	// The original forces that the list of excluded numbers (and the range to choose from)
+	// can be at max 100 numbers. With the two args for the range, the max is thus 102.
+	const uint MAX_ARGS_SIZE = 102;
+	if (args.size() > MAX_ARGS_SIZE) {
+		args.resize(MAX_ARGS_SIZE);
 	}
 
-	return returnValue;
+	// Ensure that the range is properly constructed.
+	double bottom = floor(args[0].asFloat());
+	double top = floor(args[1].asFloat());
+	if (top < bottom) {
+		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).
+	Common::Array<double> unusedNumbers;
+	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++) {
+			if (args[i].asFloat() == currentValue) {
+				isExcluded = true;
+				break;
+			}
+		}
+
+		if (!isExcluded) {
+			unusedNumbers.push_back(currentValue);
+		}
+	}
+
+	if (unusedNumbers.size() > 0) {
+		uint randomIndex = g_engine->_randomSource.getRandomNumberRng(0, unusedNumbers.size());
+		returnValue.setToFloat(unusedNumbers[randomIndex]);
+	} else {
+		warning("%s: No unused numbers to choose from", __func__);
+	}
+}
+
+void FunctionManager::script_CurrentRunTime(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+	// The current runtime is expected to be returned in seconds.
+	const uint MILLISECONDS_IN_ONE_SECOND = 1000;
+	double runtimeInSeconds = g_system->getMillis() / MILLISECONDS_IN_ONE_SECOND;
+	returnValue.setToFloat(runtimeInSeconds);
+}
+
+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;
+	if (args.size() >= 3) {
+		if (args[0].getType() != kScriptValueTypeFloat ||
+				args[1].getType() != kScriptValueTypeFloat ||
+				args[2].getType() != kScriptValueTypeFloat) {
+			warning("%s: Expected float arguments", __func__);
+			return;
+		}
+
+		red = args[0].asFloat();
+		green = args[1].asFloat();
+		blue = args[2].asFloat();
+
+	} else if (args.size() >= 1) {
+		if (args[0].getType() != kScriptValueTypeCollection) {
+			warning("%s: Expected collection argument", __func__);
+			return;
+		}
+
+		Common::SharedPtr<Collection> collection = args[0].asCollection();
+		if (collection->size() != 3) {
+			warning("%s: Collection must contain exactly 3 elements, got %u", __func__, collection->size());
+			return;
+		}
+
+		if (collection->operator[](0).getType() != kScriptValueTypeFloat ||
+				collection->operator[](1).getType() != kScriptValueTypeFloat ||
+				collection->operator[](2).getType() != kScriptValueTypeFloat) {
+			warning("%s: Expected float arguments", __func__);
+			return;
+		}
+
+		red = collection->operator[](0).asFloat();
+		green = collection->operator[](1).asFloat();
+		blue = collection->operator[](2).asFloat();
+	}
+
+	g_engine->getDisplayManager()->setGammaValues(red, green, blue);
+}
+
+void FunctionManager::script_GetDefaultGammaCorrection(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+	if (args.size() != 0) {
+		warning("%s: Expected 0 arguments, got %u", __func__, args.size());
+		return;
+	}
+
+	double red, green, blue;
+	g_engine->getDisplayManager()->getDefaultGammaValues(red, green, blue);
+
+	Common::SharedPtr<Collection> collection = Common::SharedPtr<Collection>(new Collection());
+	ScriptValue redValue;
+	redValue.setToFloat(red);
+	collection->push_back(redValue);
+
+	ScriptValue greenValue;
+	greenValue.setToFloat(green);
+	collection->push_back(greenValue);
+
+	ScriptValue blueValue;
+	blueValue.setToFloat(blue);
+	collection->push_back(blueValue);
+
+	returnValue.setToCollection(collection);
+}
+
+void FunctionManager::script_GetCurrentGammaCorrection(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+	if (args.size() != 0) {
+		warning("%s: Expected 0 arguments, got %u", __func__, args.size());
+		return;
+	}
+
+	double red, green, blue;
+	g_engine->getDisplayManager()->getGammaValues(red, green, blue);
+	Common::SharedPtr<Collection> collection = Common::SharedPtr<Collection>(new Collection());
+
+	ScriptValue redValue;
+	redValue.setToFloat(red);
+	collection->push_back(redValue);
+
+	ScriptValue greenValue;
+	greenValue.setToFloat(green);
+	collection->push_back(greenValue);
+
+	ScriptValue blueValue;
+	blueValue.setToFloat(blue);
+	collection->push_back(blueValue);
+
+	returnValue.setToCollection(collection);
+}
+
+void FunctionManager::script_SetAudioVolume(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+	if (args[0].getType() != kScriptValueTypeFloat) {
+		warning("%s: Expected float argument", __func__);
+		return;
+	}
+
+	// Convert from 0.0 - 1.0 to ScummVM's mixer range.
+	double volume = args[0].asFloat();
+	volume = CLIP(volume, 0.0, 1.0);
+	int mixerVolume = static_cast<int>(volume * Audio::Mixer::kMaxMixerVolume);
+	g_system->getMixer()->setVolumeForSoundType(Audio::Mixer::kPlainSoundType, mixerVolume);
+}
+
+void FunctionManager::script_GetAudioVolume(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+	// Convert from ScummVM's mixer range to 0.0 - 1.0.
+	int mixerVolume = g_system->getMixer()->getVolumeForSoundType(Audio::Mixer::kPlainSoundType);
+	double volume = static_cast<double>(mixerVolume) / static_cast<double>(Audio::Mixer::kMaxMixerVolume);
+	CLIP(volume, 0.0, 1.0);
+	returnValue.setToFloat(volume);
+}
+
+void FunctionManager::script_SystemLanguagePreference(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+	warning("STUB: SystemLanguagePreference");
+}
+
+void FunctionManager::script_SetRegistry(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+	warning("STUB: SetRegistry");
+}
+
+void FunctionManager::script_GetRegistry(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+	warning("STUB: GetRegistry");
+}
+
+void FunctionManager::script_SetProfile(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+	warning("STUB: SetProfile");
+}
+
+void FunctionManager::script_DebugPrint(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+	// The original reports time in seconds, but milliseconds is fine.
+	// The "IMT @ clock ..." format is from the original's debug printing style.
+	Common::String output = Common::String::format("IMT @ clock %d", g_system->getMillis());
+	for (uint i = 0; i < args.size(); i++) {
+		// Append all provided arguments.
+		if (i != 0) {
+			output += ", ";
+		} else {
+			output += " ";
+		}
+		output += args[i].getDebugString();
+	}
+	debug("%s", output.c_str());
+}
+
+void FunctionManager::script_MazeGenerate(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+	warning("STUB: MazeGenerate");
+}
+
+void FunctionManager::script_MazeApplyMoveMask(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+	warning("STUB: MazeApplyMoveMask");
+}
+
+void FunctionManager::script_MazeSolve(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+	warning("STUB: MazeSolve");
+}
+
+void FunctionManager::script_BeginTimedInterval(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+	warning("STUB: BeginTimedInterval");
+}
+
+void FunctionManager::script_EndTimedInterval(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+	warning("STUB: EndTimedInterval");
+}
+
+void FunctionManager::script_Drawing(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+	warning("STUB: Drawing");
 }
 
 void FunctionManager::deleteFunctionsForContext(uint contextId) {
diff --git a/engines/mediastation/mediascript/function.h b/engines/mediastation/mediascript/function.h
index d92ffa9e737..cf2a5c5a420 100644
--- a/engines/mediastation/mediascript/function.h
+++ b/engines/mediastation/mediascript/function.h
@@ -31,6 +31,12 @@
 
 namespace MediaStation {
 
+enum Platform {
+	kPlatformParamTokenUnknown = 0,
+	kPlatformParamTokenWindows = 0x76D,
+	kPlatformParakTokenMacintosh = 0x76E
+};
+
 class ScriptFunction {
 public:
 	ScriptFunction(Chunk &chunk);
@@ -54,10 +60,35 @@ public:
 	ScriptValue call(uint functionId, Common::Array<ScriptValue> &args);
 	void deleteFunctionsForContext(uint contextId);
 
-	ScriptValue script_Random(Common::Array<ScriptValue> &args);
-
 private:
 	Common::HashMap<uint, ScriptFunction *> _functions;
+
+	void script_GetPlatform(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
+	void script_Random(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
+	void script_TimeOfDay(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
+	void script_SquareRoot(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
+	void script_GetUniqueRandom(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
+	void script_CurrentRunTime(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
+	void script_SetGammaCorrection(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
+	void script_GetDefaultGammaCorrection(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
+	void script_GetCurrentGammaCorrection(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
+	void script_SetAudioVolume(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
+	void script_GetAudioVolume(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
+	void script_SystemLanguagePreference(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
+	void script_SetRegistry(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
+	void script_GetRegistry(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
+	void script_SetProfile(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
+	void script_DebugPrint(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
+
+	// 101 Dalmatians.
+	void script_MazeGenerate(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
+	void script_MazeApplyMoveMask(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
+	void script_MazeSolve(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
+	void script_BeginTimedInterval(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
+	void script_EndTimedInterval(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
+
+	// IBM/Crayola.
+	void script_Drawing(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/mediascript/scriptconstants.cpp b/engines/mediastation/mediascript/scriptconstants.cpp
index 27143d7aabd..e20cb2f7b1d 100644
--- a/engines/mediastation/mediascript/scriptconstants.cpp
+++ b/engines/mediastation/mediascript/scriptconstants.cpp
@@ -114,16 +114,70 @@ const char *variableScopeToStr(VariableScope scope) {
 
 const char *builtInFunctionToStr(BuiltInFunction function) {
 	switch (function) {
-	case kUnk1Function:
-		return "Unk1Function";
+	case kRandomFunction:
+		return "Random";
+	case kTimeOfDayFunction:
+		return "TimeOfDay";
 	case kEffectTransitionFunction:
 		return "EffectTransition";
 	case kEffectTransitionOnSyncFunction:
 		return "EffectTransitionOnSync";
+	case kPlatformFunction:
+		return "Platform";
+	case kSquareRootFunction:
+		return "SquareRoot";
+	case kGetUniqueRandomFunction:
+		return "GetUniqueRandom";
+	case kCurrentRunTimeFunction:
+		return "CurrentRunTime";
+	case kSetGammaCorrectionFunction:
+		return "SetGammaCorrection";
+	case kGetDefaultGammaCorrectionFunction:
+		return "GetDefaultGammaCorrection";
+	case kGetCurrentGammaCorrectionFunction:
+		return "GetCurrentGammaCorrection";
+	case kSetAudioVolumeFunction:
+		return "SetAudioVolume";
+	case kGetAudioVolumeFunction:
+		return "GetAudioVolume";
+	case kSystemLanguagePreferenceFunction:
+		return "SystemLanguagePreference";
+	case kSetRegistryFunction:
+		return "SetRegistry";
+	case kGetRegistryFunction:
+		return "GetRegistry";
+	case kSetProfileFunction:
+		return "SetProfile";
+	case kMazeGenerateFunction:
+		return "MazeGenerate";
+	case kMazeApplyMoveMaskFunction:
+		return "MazeApplyMoveMask";
+	case kMazeSolveFunction:
+		return "MazeSolve";
+	case kBeginTimedIntervalFunction:
+		return "BeginTimedInterval";
+	case kEndTimedIntervalFunction:
+		return "EndTimedInterval";
 	case kDrawingFunction:
 		return "Drawing";
-	case kDebugPrintFunction:
+	case kLegacy_RandomFunction:
+		return "Legacy Random";
+	case kLegacy_TimeOfDayFunction:
+		return "Legacy TimeOfDay";
+	case kLegacy_EffectTransitionFunction:
+		return "Legacy EffectTransition";
+	case kLegacy_EffectTransitionOnSyncFunction:
+		return "Legacy EffectTransitionOnSync";
+	case kLegacy_PlatformFunction:
+		return "Legacy Platform";
+	case kLegacy_SquareRootFunction:
+		return "Legacy SquareRoot";
+	case kLegacy_GetUniqueRandomFunction:
+		return "Legacy GetUniqueRandom";
+	case kLegacy_DebugPrintFunction:
 		return "DebugPrint";
+	case kLegacy_SystemLanguagePreferenceFunction:
+		return "Legacy SystemLanguagePreference";
 	default:
 		return "UNKNOWN";
 	}
diff --git a/engines/mediastation/mediascript/scriptconstants.h b/engines/mediastation/mediascript/scriptconstants.h
index 1c405148260..fc2b258d331 100644
--- a/engines/mediastation/mediascript/scriptconstants.h
+++ b/engines/mediastation/mediascript/scriptconstants.h
@@ -71,15 +71,42 @@ enum VariableScope {
 const char *variableScopeToStr(VariableScope scope);
 
 enum BuiltInFunction {
-	kUnk1Function = 0xA,
-	// TODO: Figure out if effectTransitionOnSync = 13 is consistent across titles?
-	kEffectTransitionFunction = 0xC,
-	kEffectTransitionOnSyncFunction = 0xD,
+	kRandomFunction = 0x0A,
+	kTimeOfDayFunction = 0x0B,
+	kEffectTransitionFunction = 0x0C,
+	kEffectTransitionOnSyncFunction = 0x0D,
+	kPlatformFunction = 0x0E,
+	kSquareRootFunction = 0x0F,
+	kGetUniqueRandomFunction = 0x10,
+	kCurrentRunTimeFunction = 0x11,
+	kSetGammaCorrectionFunction = 0x12,
+	kGetDefaultGammaCorrectionFunction = 0x13,
+	kGetCurrentGammaCorrectionFunction = 0x14,
+	kSetAudioVolumeFunction = 0x17,
+	kGetAudioVolumeFunction = 0x18,
+	kSystemLanguagePreferenceFunction = 0x19,
+	kSetRegistryFunction = 0x1A,
+	kGetRegistryFunction = 0x1B,
+	kSetProfileFunction = 0x1C,
+	kMazeGenerateFunction = 0x1F,
+	kMazeApplyMoveMaskFunction = 0x20,
+	kMazeSolveFunction = 0x21,
+	kBeginTimedIntervalFunction = 0x22,
+	kEndTimedIntervalFunction = 0x23,
 	kDrawingFunction = 0x25,
-	// TODO: Figure out if TimeOfDay = 101 is consistent across titles.
-	kDebugPrintFunction = 0xB4,
-	// TODO: Figure out code for DebugPrint.
-	// TODO: Figure out code for Quit.
+
+	// Early engine versions (like for Lion King and such), had different opcodes
+	// for some functions, even though the functions were the same. So those are
+	// defined here.
+	kLegacy_RandomFunction = 0x64,
+	kLegacy_TimeOfDayFunction = 0x65,
+	kLegacy_EffectTransitionFunction = 0x66,
+	kLegacy_EffectTransitionOnSyncFunction = 0x67,
+	kLegacy_PlatformFunction = 0x68,
+	kLegacy_SquareRootFunction = 0x69,
+	kLegacy_GetUniqueRandomFunction = 0x6A,
+	kLegacy_DebugPrintFunction = 0xB4,
+	kLegacy_SystemLanguagePreferenceFunction = 0xC8,
 };
 const char *builtInFunctionToStr(BuiltInFunction function);
 
diff --git a/engines/mediastation/mediascript/scriptvalue.cpp b/engines/mediastation/mediascript/scriptvalue.cpp
index de078081677..b6b67e0dcc5 100644
--- a/engines/mediastation/mediascript/scriptvalue.cpp
+++ b/engines/mediastation/mediascript/scriptvalue.cpp
@@ -232,9 +232,31 @@ BuiltInMethod ScriptValue::asMethodId() const {
 	}
 }
 
+Common::String ScriptValue::getDebugString() {
+	switch (getType()) {
+	case kScriptValueTypeEmpty:
+		return "empty";
+
+	case kScriptValueTypeFloat:
+		return Common::String::format("float: %f", asFloat());
+
+	case kScriptValueTypeActorId:
+		return Common::String::format("actor: %d", asActorId());
+
+	case kScriptValueTypeTime:
+		return Common::String::format("time: %f", asTime());
+
+	case kScriptValueTypeParamToken:
+		return Common::String::format("token: %d", asParamToken());
+
+	default:
+		return Common::String::format("arg type %s", scriptValueTypeToStr(getType()));
+	}
+}
+
 bool ScriptValue::compare(Opcode op, const ScriptValue &lhs, const ScriptValue &rhs) {
 	if (lhs.getType() != rhs.getType()) {
-		error("%s: Attempt to compare mismatched types %s and %s", __func__, scriptValueTypeToStr(lhs.getType()), scriptValueTypeToStr(rhs.getType()));
+		warning("%s: Attempt to compare mismatched types %s and %s", __func__, scriptValueTypeToStr(lhs.getType()), scriptValueTypeToStr(rhs.getType()));
 	}
 
 	switch (lhs.getType()) {
@@ -292,7 +314,8 @@ bool ScriptValue::compareEmptyValues(Opcode op) {
 		return false;
 
 	default:
-		error("%s: Got invalid empty value operation %s", __func__, opcodeToStr(op));
+		warning("%s: Got invalid empty value operation %s", __func__, opcodeToStr(op));
+		return false;
 	}
 }
 
diff --git a/engines/mediastation/mediascript/scriptvalue.h b/engines/mediastation/mediascript/scriptvalue.h
index 2c72067dd38..eb738b77751 100644
--- a/engines/mediastation/mediascript/scriptvalue.h
+++ b/engines/mediastation/mediascript/scriptvalue.h
@@ -70,6 +70,8 @@ public:
 	void setToMethodId(BuiltInMethod methodId);
 	BuiltInMethod asMethodId() const;
 
+	Common::String getDebugString();
+
 	bool operator==(const ScriptValue &other) const;
 	bool operator!=(const ScriptValue &other) const;
 	bool operator<(const ScriptValue &other) const;


Commit: e3b87d5243c810b1762cdc83c7f3354a97cd37a7
    https://github.com/scummvm/scummvm/commit/e3b87d5243c810b1762cdc83c7f3354a97cd37a7
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2025-12-31T11:45:08-05:00

Commit Message:
MEDIASTATION: Make searching Windows cursors by name be case-insensitive

Changed paths:
    engines/mediastation/cursors.cpp


diff --git a/engines/mediastation/cursors.cpp b/engines/mediastation/cursors.cpp
index 42c8cb1a9a4..125c66a5904 100644
--- a/engines/mediastation/cursors.cpp
+++ b/engines/mediastation/cursors.cpp
@@ -187,7 +187,7 @@ WindowsCursorManager::WindowsCursorManager(const Common::Path &appName) : Cursor
 	}
 
 	Common::WinResources *exe = Common::WinResources::createFromEXE(appName);
-	if (!exe->loadFromEXE(appName)) {
+	if (exe == nullptr || !exe->loadFromEXE(appName)) {
 		error("%s: Could not load resources from executable %s", __func__, appName.toString().c_str());
 	}
 
@@ -215,13 +215,15 @@ WindowsCursorManager::~WindowsCursorManager() {
 }
 
 Graphics::Cursor *WindowsCursorManager::loadResourceCursor(const Common::String &name) {
-	Graphics::WinCursorGroup *group = _cursorGroups.getValOrDefault(name);
-	if (group != nullptr) {
-		Graphics::Cursor *cursor = group->cursors[0].cursor;
-		return cursor;
-	} else {
-		error("%s: Reqested Windows cursor %s not found", __func__, name.c_str());
+	// Search for case-insensitive match since resource names are expected to be case-insensitive.
+	for (auto it = _cursorGroups.begin(); it != _cursorGroups.end(); ++it) {
+		if (it->_key.equalsIgnoreCase(name)) {
+			Graphics::Cursor *cursor = it->_value->cursors[0].cursor;
+			return cursor;
+		}
 	}
+
+	error("%s: Reqested Windows cursor %s not found", __func__, name.c_str());
 }
 
 MacCursorManager::MacCursorManager(const Common::Path &appName) : CursorManager(appName) {


Commit: d875bada2405cffdb18f1ab40b383fa770c8ed32
    https://github.com/scummvm/scummvm/commit/d875bada2405cffdb18f1ab40b383fa770c8ed32
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2025-12-31T11:45:08-05:00

Commit Message:
MEDIASTATION: Give some script events better names

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


diff --git a/engines/mediastation/mediascript/scriptconstants.cpp b/engines/mediastation/mediascript/scriptconstants.cpp
index e20cb2f7b1d..70ad334c8d8 100644
--- a/engines/mediastation/mediascript/scriptconstants.cpp
+++ b/engines/mediastation/mediascript/scriptconstants.cpp
@@ -377,22 +377,28 @@ const char *eventTypeToStr(EventType type) {
 		return "MovieBegin";
 	case kSpriteMovieEndEvent:
 		return "SpriteMovieEnd";
-	case kEntryEvent:
-		return "EntryEvent";
-	case kExitEvent:
-		return "ExitEvent";
-	case kLoadCompleteEvent:
-		return "LoadComplete";
-	case kInputEvent:
-		return "Input";
-	case kErrorEvent:
-		return "Error";
-	case kPanAbortEvent:
-		return "PanAbort";
-	case kPanEndEvent:
-		return "PanEnd";
-	case kStepEvent:
-		return "StepEvent";
+	case kScreenEntryEvent:
+		return "ScreenEntry";
+	case kScreenExitEvent:
+		return "ScreenExit";
+	case kContextLoadCompleteEvent:
+		return "ContextLoadComplete";
+	case kContextLoadCompleteEvent2:
+		return "ContextLoadComplete2";
+	case kContextLoadAbortEvent:
+		return "ContextLoadAbort";
+	case kContextLoadFailureEvent:
+		return "ContextLoadFailure";
+	case kTextInputEvent:
+		return "TextInput";
+	case kTextErrorEvent:
+		return "TextError";
+	case kCameraPanAbortEvent:
+		return "CameraPanAbort";
+	case kCameraPanEndEvent:
+		return "CameraPanEnd";
+	case kPathStepEvent:
+		return "PathStep";
 	case kPathStoppedEvent:
 		return "PathStopped";
 	case kPathEndEvent:
diff --git a/engines/mediastation/mediascript/scriptconstants.h b/engines/mediastation/mediascript/scriptconstants.h
index fc2b258d331..e8d6fa40a18 100644
--- a/engines/mediastation/mediascript/scriptconstants.h
+++ b/engines/mediastation/mediascript/scriptconstants.h
@@ -226,54 +226,39 @@ enum BuiltInMethod {
 const char *builtInMethodToStr(BuiltInMethod method);
 
 enum EventType {
-	// TIMER EVENTS.
-	kTimerEvent = 0x5,
-
-	// HOTSPOT EVENTS.
-	kMouseDownEvent = 0x6,
-	kMouseUpEvent = 0x7,
-	kMouseMovedEvent = 0x8,
-	kMouseEnteredEvent = 0x9,
-	kMouseExitedEvent = 0xA,
-	kKeyDownEvent = 0xD,
-
-	// SOUND EVENTS.
-	kSoundEndEvent = 0xE,
+	kTimerEvent = 0x05,
+	kMouseDownEvent = 0x06,
+	kMouseUpEvent = 0x07,
+	kMouseMovedEvent = 0x08,
+	kMouseEnteredEvent = 0x09,
+	kMouseExitedEvent = 0x0A,
+	kKeyDownEvent = 0x0D,
+	kSoundEndEvent = 0x0E,
+	kMovieEndEvent = 0x0F,
+	kPathEndEvent = 0x10,
+	kScreenEntryEvent = 0x11,
 	kSoundAbortEvent = 0x13,
 	kSoundFailureEvent = 0x14,
-	kSoundStoppedEvent = 0x1D,
-	kSoundBeginEvent = 0x1E,
-
-	// MOVIE EVENTS.
-	kMovieEndEvent = 0xF,
 	kMovieAbortEvent = 0x15,
 	kMovieFailureEvent = 0x16,
+	kSpriteMovieEndEvent = 0x17,
+	kScreenExitEvent = 0x1B,
+	kPathStepEvent = 0x1C,
+	kSoundStoppedEvent = 0x1D,
+	kSoundBeginEvent = 0x1E,
 	kMovieStoppedEvent = 0x1F,
 	kMovieBeginEvent = 0x20,
-
-	// SPRITE EVENTS.
-	// Just "MovieEnd" in source.
-	kSpriteMovieEndEvent = 0x17,
-
-	// SCREEN EVENTS.
-	kEntryEvent = 0x11,
-	kExitEvent = 0x1B,
-
-	// CONTEXT EVENTS.
-	kLoadCompleteEvent = 0x2C,
-
-	// TEXT EVENTS.
-	kInputEvent = 0x25,
-	kErrorEvent = 0x26,
-
-	// CAMERA EVENTS.
-	kPanAbortEvent = 0x2B,
-	kPanEndEvent = 0x2A,
-
-	// PATH EVENTS.
-	kStepEvent = 0x1C,
 	kPathStoppedEvent = 0x21,
-	kPathEndEvent = 0x10
+	kTextInputEvent = 0x25,
+	kTextErrorEvent = 0x26,
+	kCameraPanEndEvent = 0x2A,
+	kCameraPanAbortEvent = 0x2B,
+	kContextLoadCompleteEvent = 0x2C,
+	// TODO: These last 3 events appear as valid event types, but I haven't found
+	// scripts that actually use them. So the names might be wrong.
+	kContextLoadCompleteEvent2 = 0x2D,
+	kContextLoadAbortEvent = 0x2E,
+	kContextLoadFailureEvent = 0x2F,
 };
 const char *eventTypeToStr(EventType type);
 


Commit: cacdde38ce32ac43672ab17cf3a52d8e257e88b2
    https://github.com/scummvm/scummvm/commit/cacdde38ce32ac43672ab17cf3a52d8e257e88b2
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2025-12-31T11:45:08-05:00

Commit Message:
MEDIASTATION: Fix playing some sprites

Changed paths:
    engines/mediastation/actors/sprite.cpp
    engines/mediastation/bitmap.cpp


diff --git a/engines/mediastation/actors/sprite.cpp b/engines/mediastation/actors/sprite.cpp
index 5c20b7e0607..e30d4ed37d5 100644
--- a/engines/mediastation/actors/sprite.cpp
+++ b/engines/mediastation/actors/sprite.cpp
@@ -27,9 +27,7 @@ namespace MediaStation {
 
 SpriteFrameHeader::SpriteFrameHeader(Chunk &chunk) : BitmapHeader(chunk) {
 	_index = chunk.readTypedUint16();
-	debugC(5, kDebugLoading, "SpriteFrameHeader::SpriteFrameHeader(): _index = 0x%x (@0x%llx)", _index, static_cast<long long int>(chunk.pos()));
-	_boundingBox = chunk.readTypedPoint();
-	debugC(5, kDebugLoading, "SpriteFrameHeader::SpriteFrameHeader(): _boundingBox (@0x%llx)", static_cast<long long int>(chunk.pos()));
+	_offset = chunk.readTypedPoint();
 }
 
 SpriteFrame::SpriteFrame(Chunk &chunk, SpriteFrameHeader *header) : Bitmap(chunk, header) {
@@ -41,11 +39,11 @@ SpriteFrame::~SpriteFrame() {
 }
 
 uint32 SpriteFrame::left() {
-	return _bitmapHeader->_boundingBox.x;
+	return _bitmapHeader->_offset.x;
 }
 
 uint32 SpriteFrame::top() {
-	return _bitmapHeader->_boundingBox.y;
+	return _bitmapHeader->_offset.y;
 }
 
 Common::Point SpriteFrame::topLeft() {
diff --git a/engines/mediastation/bitmap.cpp b/engines/mediastation/bitmap.cpp
index 13f59016d4d..3e1019d1f8e 100644
--- a/engines/mediastation/bitmap.cpp
+++ b/engines/mediastation/bitmap.cpp
@@ -43,11 +43,16 @@ Bitmap::Bitmap(Chunk &chunk, BitmapHeader *bitmapHeader) : _bitmapHeader(bitmapH
 		if (isCompressed()) {
 			_compressedStream = chunk.readStream(chunk.bytesRemaining());
 		} else {
-			_image.create(width(), height(), Graphics::PixelFormat::createFormatCLUT8());
+			_image.create(stride(), height(), Graphics::PixelFormat::createFormatCLUT8());
 			if (getCompressionType() == kUncompressedTransparentBitmap)
 				_image.setTransparentColor(0);
 			byte *pixels = static_cast<byte *>(_image.getPixels());
-			chunk.read(pixels, chunk.bytesRemaining());
+
+			chunk.read(pixels, stride() * height());
+			if (chunk.bytesRemaining() > 0) {
+				warning("%s: %d bytes remaining in bitmap chunk after reading image data", __func__, chunk.bytesRemaining());
+				chunk.skip(chunk.bytesRemaining());
+			}
 		}
 	}
 }


Commit: 1a566e410b50a9102d77a5a8af028bacc0577dee
    https://github.com/scummvm/scummvm/commit/1a566e410b50a9102d77a5a8af028bacc0577dee
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2025-12-31T11:45:08-05:00

Commit Message:
MEDIASTATION: Implement stream loading much closer to the original

As a consequence of these changes, many titles will now finally start that previously couldn't,
and titles that rely on background sounds and such in INSTALL.CXT will now be able
to load this actors.

Changed paths:
    engines/mediastation/actor.cpp
    engines/mediastation/actor.h
    engines/mediastation/actors/document.cpp
    engines/mediastation/actors/document.h
    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/path.cpp
    engines/mediastation/actors/sound.cpp
    engines/mediastation/actors/sound.h
    engines/mediastation/actors/sprite.cpp
    engines/mediastation/actors/sprite.h
    engines/mediastation/actors/stage.cpp
    engines/mediastation/audio.cpp
    engines/mediastation/audio.h
    engines/mediastation/boot.cpp
    engines/mediastation/boot.h
    engines/mediastation/clients.cpp
    engines/mediastation/clients.h
    engines/mediastation/context.cpp
    engines/mediastation/context.h
    engines/mediastation/datafile.cpp
    engines/mediastation/datafile.h
    engines/mediastation/mediascript/scriptconstants.cpp
    engines/mediastation/mediascript/scriptconstants.h
    engines/mediastation/mediastation.cpp
    engines/mediastation/mediastation.h


diff --git a/engines/mediastation/actor.cpp b/engines/mediastation/actor.cpp
index d62c23024b7..b0cc4c5803b 100644
--- a/engines/mediastation/actor.cpp
+++ b/engines/mediastation/actor.cpp
@@ -86,14 +86,6 @@ ScriptValue Actor::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 	return ScriptValue();
 }
 
-void Actor::readChunk(Chunk &chunk) {
-	error("%s: Chunk reading for actor type 0x%x is not implemented", __func__, static_cast<uint>(_type));
-}
-
-void Actor::readSubfile(Subfile &subfile, Chunk &chunk) {
-	error("%s: Subfile reading for actor type 0x%x is not implemented", __func__, static_cast<uint>(_type));
-}
-
 void Actor::processTimeEventHandlers() {
 	// TODO: Replace with a queue.
 	uint currentTime = g_system->getMillis();
@@ -292,10 +284,6 @@ void SpatialEntity::readParameter(Chunk &chunk, ActorHeaderSectionType paramType
 		_stageId = chunk.readTypedUint16();
 		break;
 
-	case kActorHeaderActorReference:
-		_actorReference = chunk.readTypedUint16();
-		break;
-
 	case kActorHeaderScaleXAndY:
 		_scaleX = _scaleY = chunk.readTypedDouble();
 		break;
diff --git a/engines/mediastation/actor.h b/engines/mediastation/actor.h
index 151fc0d0f63..e2e13c00160 100644
--- a/engines/mediastation/actor.h
+++ b/engines/mediastation/actor.h
@@ -65,9 +65,9 @@ enum ActorHeaderSectionType {
 	kActorHeaderEventHandler = 0x0017,
 	kActorHeaderChildActorId = 0x0019,
 	kActorHeaderActorId = 0x001a,
-	kActorHeaderChunkReference = 0x001b,
-	kActorHeaderMovieAnimationChunkReference = 0x06a4,
-	kActorHeaderMovieAudioChunkReference = 0x06a5,
+	kActorHeaderChannelIdent = 0x001b,
+	kActorHeaderMovieAnimationChannelIdent = 0x06a4,
+	kActorHeaderMovieAudioChannelIdent = 0x06a5,
 	kActorHeaderActorReference = 0x077b,
 	kActorHeaderBoundingBox = 0x001c,
 	kActorHeaderMouseActiveArea = 0x001d,
@@ -169,11 +169,6 @@ public:
 	virtual void readParameter(Chunk &chunk, ActorHeaderSectionType paramType);
 	virtual void loadIsComplete();
 
-	// These are not pure virtual so if an actor doesn't read any chunks or
-	// subfiles it doesn't need to just implement these with an error message.
-	virtual void readChunk(Chunk &chunk);
-	virtual void readSubfile(Subfile &subfile, Chunk &chunk);
-
 	void processTimeEventHandlers();
 	void runEventHandlerIfExists(EventType eventType, const ScriptValue &arg);
 	void runEventHandlerIfExists(EventType eventType);
@@ -184,9 +179,6 @@ public:
 	void setId(uint id) { _id = id; }
 	void setContextId(uint id) { _contextId = id; }
 
-	uint32 _chunkReference = 0;
-	uint _actorReference = 0;
-
 protected:
 	ActorType _type = kActorTypeEmpty;
 	bool _loadIsComplete = false;
diff --git a/engines/mediastation/actors/document.cpp b/engines/mediastation/actors/document.cpp
index 8429b456fcf..9b930d28cd6 100644
--- a/engines/mediastation/actors/document.cpp
+++ b/engines/mediastation/actors/document.cpp
@@ -24,24 +24,65 @@
 
 namespace MediaStation {
 
+const uint MediaStation::DocumentActor::DOCUMENT_ACTOR_ID;
+
 ScriptValue DocumentActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
 	ScriptValue returnValue;
-
 	switch (methodId) {
-	case kBranchToScreenMethod:
+	case kDocumentBranchToScreenMethod:
 		processBranch(args);
-		return returnValue;
+		break;
+
+	case kDocumentQuitMethod:
+		g_engine->quitGame();
+		break;
+
+	case kDocumentContextLoadInProgressMethod: {
+		assert(args.size() == 1);
+		uint contextId = args[0].asActorId();
+		bool isLoading = g_engine->getDocument()->isContextLoadInProgress(contextId);
+		returnValue.setToBool(isLoading);
+		break;
+	}
+
+	case kDocumentSetMultipleStreamsMethod:
+	case kDocumentSetMultipleSoundsMethod: {
+		assert(args.size() == 1);
+		bool value = args[0].asBool();
+		warning("%s: STUB: %s: %d", __func__, builtInMethodToStr(methodId), value);
+		break;
+	}
+
+	case kDocumentLoadContextMethod: {
+		assert(args.size() == 1);
+		uint contextId = args[0].asActorId();
+		g_engine->getDocument()->startContextLoad(contextId);
+		break;
+	}
 
-	case kReleaseContextMethod: {
+	case kDocumentReleaseContextMethod: {
 		assert(args.size() == 1);
-		uint32 contextId = args[0].asActorId();
-		g_engine->scheduleContextRelease(contextId);
-		return returnValue;
+		uint contextId = args[0].asActorId();
+		g_engine->getDocument()->scheduleContextRelease(contextId);
+		break;
+	}
+
+	case kDocumentContextIsLoadedMethod: {
+		assert(args.size() == 1);
+		uint contextId = args[0].asActorId();
+
+		// We are looking for the screen actor with the same ID as the context.
+		Actor *screenActor = g_engine->getActorById(contextId);
+		bool contextIsLoading = g_engine->getDocument()->isContextLoadInProgress(contextId);
+		bool contextIsLoaded = (screenActor != nullptr) && !contextIsLoading;
+		returnValue.setToBool(contextIsLoaded);
+		break;
 	}
 
 	default:
-		return Actor::callMethod(methodId, args);
+		returnValue = Actor::callMethod(methodId, args);
 	}
+	return returnValue;
 }
 
 void DocumentActor::processBranch(Common::Array<ScriptValue> &args) {
@@ -53,7 +94,7 @@ void DocumentActor::processBranch(Common::Array<ScriptValue> &args) {
 			warning("%s: disableUpdates parameter not handled yet", __func__);
 	}
 
-	g_engine->scheduleScreenBranch(contextId);
+	g_engine->getDocument()->scheduleScreenBranch(contextId);
 }
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/actors/document.h b/engines/mediastation/actors/document.h
index 3b049e6eeb6..57b23203e0b 100644
--- a/engines/mediastation/actors/document.h
+++ b/engines/mediastation/actors/document.h
@@ -30,7 +30,8 @@ namespace MediaStation {
 
 class DocumentActor : public Actor {
 public:
-	DocumentActor() : Actor(kActorTypeDocument) { _id = 1; };
+	static const uint DOCUMENT_ACTOR_ID = 1;
+	DocumentActor() : Actor(kActorTypeDocument) { _id = DOCUMENT_ACTOR_ID; };
 
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 
diff --git a/engines/mediastation/actors/font.cpp b/engines/mediastation/actors/font.cpp
index e405337ff1e..0614a63dd89 100644
--- a/engines/mediastation/actors/font.cpp
+++ b/engines/mediastation/actors/font.cpp
@@ -31,6 +31,7 @@ FontGlyph::FontGlyph(Chunk &chunk, uint asciiCode, uint unk1, uint unk2, BitmapH
 }
 
 FontActor::~FontActor() {
+	unregisterWithStreamManager();
 	for (auto it = _glyphs.begin(); it != _glyphs.end(); ++it) {
 		delete it->_value;
 	}
@@ -39,8 +40,9 @@ FontActor::~FontActor() {
 
 void FontActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 	switch (paramType) {
-	case kActorHeaderChunkReference:
-		_chunkReference = chunk.readTypedChunkReference();
+	case kActorHeaderChannelIdent:
+		_channelIdent = chunk.readTypedChannelIdent();
+		registerWithStreamManager();
 		break;
 
 	default:
diff --git a/engines/mediastation/actors/font.h b/engines/mediastation/actors/font.h
index 98629fce76c..b6b0eb69db5 100644
--- a/engines/mediastation/actors/font.h
+++ b/engines/mediastation/actors/font.h
@@ -40,7 +40,7 @@ private:
 	int _unk2 = 0;
 };
 
-class FontActor : public Actor {
+class FontActor : public Actor, public ChannelClient {
 public:
 	FontActor() : Actor(kActorTypeFont) {};
 	~FontActor();
diff --git a/engines/mediastation/actors/image.cpp b/engines/mediastation/actors/image.cpp
index dc823e4f82e..6ac0a3ec2fc 100644
--- a/engines/mediastation/actors/image.cpp
+++ b/engines/mediastation/actors/image.cpp
@@ -25,19 +25,21 @@
 
 namespace MediaStation {
 
+ImageAsset::~ImageAsset() {
+	delete bitmap;
+	bitmap = nullptr;
+}
+
 ImageActor::~ImageActor() {
-	if (_actorReference == 0) {
-		// If we're just referencing another actor's bitmap,
-		// don't delete that bitmap.
-		delete _bitmap;
-	}
-	_bitmap = nullptr;
+	unregisterWithStreamManager();
 }
 
 void ImageActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 	switch (paramType) {
-	case kActorHeaderChunkReference:
-		_chunkReference = chunk.readTypedChunkReference();
+	case kActorHeaderChannelIdent:
+		_channelIdent = chunk.readTypedChannelIdent();
+		registerWithStreamManager();
+		_asset = Common::SharedPtr<ImageAsset>(new ImageAsset);
 		break;
 
 	case kActorHeaderStartup:
@@ -56,6 +58,20 @@ void ImageActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 		_yOffset = chunk.readTypedUint16();
 		break;
 
+	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);
+		_asset = referencedImage->_asset;
+		break;
+	}
+
 	default:
 		SpatialEntity::readParameter(chunk, paramType);
 	}
@@ -84,7 +100,7 @@ ScriptValue ImageActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptV
 void ImageActor::draw(const Common::Array<Common::Rect> &dirtyRegion) {
 	if (_isVisible) {
 		Common::Point origin = getBbox().origin();
-		g_engine->getDisplayManager()->imageBlit(origin, _bitmap, _dissolveFactor, dirtyRegion);
+		g_engine->getDisplayManager()->imageBlit(origin, _asset->bitmap, _dissolveFactor, dirtyRegion);
 	}
 }
 
@@ -104,13 +120,13 @@ void ImageActor::spatialHide() {
 
 Common::Rect ImageActor::getBbox() const {
 	Common::Point origin(_xOffset + _boundingBox.left, _yOffset + _boundingBox.top);
-	Common::Rect bbox(origin, _bitmap->width(), _bitmap->height());
+	Common::Rect bbox(origin, _asset->bitmap->width(), _asset->bitmap->height());
 	return bbox;
 }
 
 void ImageActor::readChunk(Chunk &chunk) {
 	BitmapHeader *bitmapHeader = new BitmapHeader(chunk);
-	_bitmap = new Bitmap(chunk, bitmapHeader);
+	_asset->bitmap = new Bitmap(chunk, bitmapHeader);
 }
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/actors/image.h b/engines/mediastation/actors/image.h
index ac14e33a9a1..b28cf0d15f4 100644
--- a/engines/mediastation/actors/image.h
+++ b/engines/mediastation/actors/image.h
@@ -22,6 +22,8 @@
 #ifndef MEDIASTATION_IMAGE_H
 #define MEDIASTATION_IMAGE_H
 
+#include "common/ptr.h"
+
 #include "mediastation/actor.h"
 #include "mediastation/datafile.h"
 #include "mediastation/bitmap.h"
@@ -30,9 +32,15 @@
 
 namespace MediaStation {
 
-class ImageActor : public SpatialEntity {
-friend class Context;
+// The original had a separate class that did reference counting,
+// for sharing an asset across actors, but we can just use a SharedPtr.
+struct ImageAsset {
+	~ImageAsset();
+
+	Bitmap *bitmap = nullptr;
+};
 
+class ImageActor : public SpatialEntity, public ChannelClient {
 public:
 	ImageActor() : SpatialEntity(kActorTypeImage) {};
 	virtual ~ImageActor() override;
@@ -45,10 +53,11 @@ public:
 	virtual Common::Rect getBbox() const override;
 
 private:
-	Bitmap *_bitmap = nullptr;
+	Common::SharedPtr<ImageAsset> _asset;
 	uint _loadType = 0;
 	int _xOffset = 0;
 	int _yOffset = 0;
+	uint _actorReference = 0;
 
 	// Script method implementations.
 	void spatialShow();
diff --git a/engines/mediastation/actors/movie.cpp b/engines/mediastation/actors/movie.cpp
index 71f63675879..d1b041cd807 100644
--- a/engines/mediastation/actors/movie.cpp
+++ b/engines/mediastation/actors/movie.cpp
@@ -19,9 +19,9 @@
  *
  */
 
-#include "mediastation/mediastation.h"
 #include "mediastation/actors/movie.h"
 #include "mediastation/debugchannels.h"
+#include "mediastation/mediastation.h"
 
 namespace MediaStation {
 
@@ -77,15 +77,17 @@ MovieFrameImage::~MovieFrameImage() {
 }
 
 StreamMovieActor::~StreamMovieActor() {
-	for (MovieFrame *frame : _frames) {
-		delete frame;
+	unregisterWithStreamManager();
+	if (_streamFeed != nullptr) {
+		g_engine->getStreamFeedManager()->closeStreamFeed(_streamFeed);
+		_streamFeed = nullptr;
 	}
-	_frames.clear();
 
-	for (MovieFrameImage *image : _images) {
-		delete image;
-	}
-	_images.clear();
+	delete _streamFrames;
+	_streamFrames = nullptr;
+
+	delete _streamSound;
+	_streamSound = nullptr;
 }
 
 void StreamMovieActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
@@ -104,8 +106,9 @@ void StreamMovieActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramT
 		_loadType = chunk.readTypedByte();
 		break;
 
-	case kActorHeaderChunkReference:
-		_chunkReference = chunk.readTypedChunkReference();
+	case kActorHeaderChannelIdent:
+		_channelIdent = chunk.readTypedChannelIdent();
+		registerWithStreamManager();
 		break;
 
 	case kActorHeaderHasOwnSubfile: {
@@ -120,17 +123,22 @@ void StreamMovieActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramT
 		_isVisible = static_cast<bool>(chunk.readTypedByte());
 		break;
 
-	case kActorHeaderMovieAudioChunkReference:
-		_audioChunkReference = chunk.readTypedChunkReference();
+	case kActorHeaderMovieAudioChannelIdent: {
+		ChannelIdent soundChannelIdent = chunk.readTypedChannelIdent();
+		_streamSound->setChannelIdent(soundChannelIdent);
+		_streamSound->registerWithStreamManager();
 		break;
+	}
 
-	case kActorHeaderMovieAnimationChunkReference:
-		_animationChunkReference = chunk.readTypedChunkReference();
+	case kActorHeaderMovieAnimationChannelIdent: {
+		ChannelIdent framesChannelIdent = chunk.readTypedChannelIdent();
+		_streamFrames->setChannelIdent(framesChannelIdent);
+		_streamFrames->registerWithStreamManager();
 		break;
+	}
 
 	case kActorHeaderSoundInfo:
-		_audioChunkCount = chunk.readTypedUint16();
-		_audioSequence.readParameters(chunk);
+		_streamSound->_audioSequence.readParameters(chunk);
 		break;
 
 	default:
@@ -193,14 +201,17 @@ ScriptValue StreamMovieActor::callMethod(BuiltInMethod methodId, Common::Array<S
 }
 
 void StreamMovieActor::timePlay() {
-	// TODO: Play movies one chunk at a time, which more directly approximates
-	// the original's reading from the CD one chunk at a time.
+	if (_streamFeed == nullptr) {
+		_streamFeed = g_engine->getStreamFeedManager()->openStreamFeed(_id);
+		_streamFeed->readData();
+	}
+
 	if (_isPlaying) {
 		return;
 	}
 
-	_audioSequence.play();
-	_framesNotYetShown = _frames;
+	_streamSound->_audioSequence.play();
+	_framesNotYetShown = _streamFrames->_frames;
 	_framesOnScreen.clear();
 	_isPlaying = true;
 	_startTime = g_system->getMillis();
@@ -217,10 +228,10 @@ void StreamMovieActor::timeStop() {
 	for (MovieFrame *frame : _framesOnScreen) {
 		invalidateRect(getFrameBoundingBox(frame));
 	}
-	_audioSequence.stop();
+	_streamSound->_audioSequence.stop();
 	_framesNotYetShown.empty();
 	if (_hasStill) {
-		_framesNotYetShown = _frames;
+		_framesNotYetShown = _streamFrames->_frames;
 	}
 	_framesOnScreen.clear();
 	_startTime = 0;
@@ -290,7 +301,7 @@ void StreamMovieActor::updateFrameState() {
 			if (_framesOnScreen.empty() && movieTime >= _fullTime) {
 				_isPlaying = false;
 				if (_hasStill) {
-					_framesNotYetShown = _frames;
+					_framesNotYetShown = _streamFrames->_frames;
 					updateFrameState();
 				}
 				runEventHandlerIfExists(kMovieEndEvent);
@@ -340,8 +351,21 @@ Common::Rect StreamMovieActor::getFrameBoundingBox(MovieFrame *frame) {
 	return bbox;
 }
 
-void StreamMovieActor::readChunk(Chunk &chunk) {
-	// Individual chunks are "stills" and are stored in the first subfile.
+StreamMovieActorFrames::~StreamMovieActorFrames() {
+	unregisterWithStreamManager();
+
+	for (MovieFrame *frame : _frames) {
+		delete frame;
+	}
+	_frames.clear();
+
+	for (MovieFrameImage *image : _images) {
+		delete image;
+	}
+	_images.clear();
+}
+
+void StreamMovieActorFrames::readChunk(Chunk &chunk) {
 	uint sectionType = chunk.readTypedUint16();
 	switch ((MovieSectionType)sectionType) {
 	case kMovieImageDataSection:
@@ -355,90 +379,66 @@ void StreamMovieActor::readChunk(Chunk &chunk) {
 	default:
 		error("%s: Unknown movie still section type", __func__);
 	}
-	_hasStill = true;
-}
 
-void StreamMovieActor::readSubfile(Subfile &subfile, Chunk &chunk) {
-	uint expectedRootSectionType = chunk.readTypedUint16();
-	debugC(5, kDebugLoading, "StreamMovieActor::readSubfile(): sectionType = 0x%x (@0x%llx)", static_cast<uint>(expectedRootSectionType), static_cast<long long int>(chunk.pos()));
-	if (kMovieRootSection != (MovieSectionType)expectedRootSectionType) {
-		error("%s: Expected ROOT section type, got 0x%x", __func__, expectedRootSectionType);
+	for (MovieFrame *frame : _frames) {
+		if (frame->endInMilliseconds > _parent->_fullTime) {
+			_parent->_fullTime = frame->endInMilliseconds;
+		}
+		if (frame->keepAfterEnd) {
+			_parent->_hasStill = true;
+		}
 	}
-	uint chunkCount = chunk.readTypedUint16();
-	double unk1 = chunk.readTypedDouble();
-	debugC(5, kDebugLoading, "StreamMovieActor::readSubfile(): chunkCount = 0x%x, unk1 = %f (@0x%llx)", chunkCount, unk1, static_cast<long long int>(chunk.pos()));
 
-	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);
+	if (_parent->_hasStill) {
+		_parent->_framesNotYetShown = _frames;
 	}
+}
 
-	// RAD THE INTERLEAVED AUDIO AND ANIMATION DATA.
-	for (uint i = 0; i < chunkCount; i++) {
-		debugC(5, kDebugLoading, "\nMovie::readSubfile(): Reading frameset %d of %d in subfile (@0x%llx)", i, chunkCount, static_cast<long long int>(chunk.pos()));
-		chunk = subfile.nextChunk();
-
-		// READ ALL THE FRAMES IN THIS CHUNK.
-		debugC(5, kDebugLoading, "StreamMovieActor::readSubfile(): (Frameset %d of %d) Reading animation chunks... (@0x%llx)", i, chunkCount, static_cast<long long int>(chunk.pos()));
-		bool isAnimationChunk = (chunk._id == _animationChunkReference);
-		if (!isAnimationChunk) {
-			warning("%s: (Frameset %d of %d) No animation chunks found (@0x%llx)", __func__, i, chunkCount, static_cast<long long int>(chunk.pos()));
-		}
-		while (isAnimationChunk) {
-			uint sectionType = chunk.readTypedUint16();
-			debugC(5, kDebugLoading, "StreamMovieActor::readSubfile(): sectionType = 0x%x (@0x%llx)", static_cast<uint>(sectionType), static_cast<long long int>(chunk.pos()));
-			switch (MovieSectionType(sectionType)) {
-			case kMovieImageDataSection:
-				readImageData(chunk);
-				break;
-
-			case kMovieFrameDataSection:
-				readFrameData(chunk);
-				break;
-
-			default:
-				error("%s: Unknown movie animation section type 0x%x (@0x%llx)", __func__, static_cast<uint>(sectionType), static_cast<long long int>(chunk.pos()));
-			}
+StreamMovieActorSound::~StreamMovieActorSound() {
+	unregisterWithStreamManager();
+}
 
-			chunk = subfile.nextChunk();
-			isAnimationChunk = (chunk._id == _animationChunkReference);
-		}
+void StreamMovieActorSound::readChunk(Chunk &chunk) {
+	_audioSequence.readChunk(chunk);
+}
 
-		// READ THE AUDIO.
-		debugC(5, kDebugLoading, "StreamMovieActor::readSubfile(): (Frameset %d of %d) Reading audio chunk... (@0x%llx)", i, chunkCount, static_cast<long long int>(chunk.pos()));
-		bool isAudioChunk = (chunk._id == _audioChunkReference);
-		if (isAudioChunk) {
-			_audioSequence.readChunk(chunk);
-			chunk = subfile.nextChunk();
-		} else {
-			debugC(5, kDebugLoading, "StreamMovieActor::readSubfile(): (Frameset %d of %d) No audio chunk to read. (@0x%llx)", i, chunkCount, static_cast<long long int>(chunk.pos()));
-		}
+StreamMovieActor::StreamMovieActor() : _framesOnScreen(StreamMovieActor::compareFramesByZIndex), SpatialEntity(kActorTypeMovie) {
+	_streamFrames = new StreamMovieActorFrames(this);
+	_streamSound = new StreamMovieActorSound();
+}
 
-		debugC(5, kDebugLoading, "StreamMovieActor::readSubfile(): (Frameset %d of %d) Reading header chunk... (@0x%llx)", i, chunkCount, static_cast<long long int>(chunk.pos()));
-		bool isHeaderChunk = (chunk._id == _chunkReference);
-		if (isHeaderChunk) {
-			if (chunk._length != 0x04) {
-				error("%s: Expected movie header chunk of size 0x04, got 0x%x (@0x%llx)", __func__, chunk._length, static_cast<long long int>(chunk.pos()));
-			}
-			chunk.skip(chunk._length);
-		} else {
-			error("%s: Expected header chunk, got %s (@0x%llx)", __func__, tag2str(chunk._id), static_cast<long long int>(chunk.pos()));
-		}
+void StreamMovieActor::readChunk(Chunk &chunk) {
+	MovieSectionType sectionType = static_cast<MovieSectionType>(chunk.readTypedUint16());
+	if (sectionType == kMovieRootSection) {
+		parseMovieHeader(chunk);
+	} else if (sectionType == kMovieChunkMarkerSection) {
+		parseMovieChunkMarker(chunk);
+	} else {
+		error("%s: Got unused movie chunk header section", __func__);
 	}
+}
 
-	for (MovieFrame *frame : _frames) {
-		if (frame->endInMilliseconds > _fullTime) {
-			_fullTime = frame->endInMilliseconds;
-		}
-	}
+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()));
 
-	if (_hasStill) {
-		_framesNotYetShown = _frames;
+	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);
 	}
 }
 
+void StreamMovieActor::parseMovieChunkMarker(Chunk &chunk) {
+	// TODO: There is no warning here because that would spam with thousands of warnings.
+	// This takes care of scheduling stream load and such - it doesn't actually read from the
+	// chunk that is passed in. Since we don't need that scheduling since we are currently reading
+	// the whole movie at once rather than streaming it from the CD-ROM, we don't currently need
+	// to do much here anyway.
+}
+
 void StreamMovieActor::invalidateRect(const Common::Rect &rect) {
 	g_engine->addDirtyRect(rect);
 }
@@ -453,13 +453,13 @@ void StreamMovieActor::decompressIntoAuxImage(MovieFrame *frame) {
 	g_engine->getDisplayManager()->imageBlit(origin, frame->keyframeImage, 1.0, allDirty, &frame->keyframeImage->_image);
 }
 
-void StreamMovieActor::readImageData(Chunk &chunk) {
+void StreamMovieActorFrames::readImageData(Chunk &chunk) {
 	MovieFrameHeader *header = new MovieFrameHeader(chunk);
 	MovieFrameImage *frame = new MovieFrameImage(chunk, header);
 	_images.push_back(frame);
 }
 
-void StreamMovieActor::readFrameData(Chunk &chunk) {
+void StreamMovieActorFrames::readFrameData(Chunk &chunk) {
 	uint frameDataToRead = chunk.readTypedUint16();
 	for (uint i = 0; i < frameDataToRead; i++) {
 		MovieFrame *frame = new MovieFrame(chunk);
diff --git a/engines/mediastation/actors/movie.h b/engines/mediastation/actors/movie.h
index 9b7221f89c6..033ae5cb96d 100644
--- a/engines/mediastation/actors/movie.h
+++ b/engines/mediastation/actors/movie.h
@@ -61,7 +61,8 @@ private:
 enum MovieSectionType {
 	kMovieRootSection = 0x06a8,
 	kMovieImageDataSection = 0x06a9,
-	kMovieFrameDataSection = 0x06aa
+	kMovieFrameDataSection = 0x06aa,
+	kMovieChunkMarkerSection = 0x06ab
 };
 
 struct MovieFrame {
@@ -82,13 +83,44 @@ struct MovieFrame {
 	MovieFrameImage *keyframeImage = nullptr;
 };
 
-class StreamMovieActor : public SpatialEntity {
+class StreamMovieActor;
+
+// This is called `RT_stmvFrames` in the original.
+class StreamMovieActorFrames : public ChannelClient {
+public:
+	StreamMovieActorFrames(StreamMovieActor *parent) : ChannelClient(), _parent(parent) {}
+	~StreamMovieActorFrames();
+
+	virtual void readChunk(Chunk &chunk) override;
+
+	Common::Array<MovieFrame *> _frames;
+	Common::Array<MovieFrameImage *> _images;
+
+private:
+	StreamMovieActor *_parent = nullptr;
+
+	void readImageData(Chunk &chunk);
+	void readFrameData(Chunk &chunk);
+};
+
+// This is called `RT_stmvSound` in the original.
+class StreamMovieActorSound : public ChannelClient {
 public:
-	StreamMovieActor() : _framesOnScreen(StreamMovieActor::compareFramesByZIndex), SpatialEntity(kActorTypeMovie) {}
+	~StreamMovieActorSound();
+	virtual void readChunk(Chunk &chunk) override;
+
+	AudioSequence _audioSequence;
+};
+
+class StreamMovieActor : public SpatialEntity, public ChannelClient {
+friend class StreamMovieActorFrames;
+friend class StreamMovieActorSound;
+
+public:
+	StreamMovieActor();
 	virtual ~StreamMovieActor() override;
 
 	virtual void readChunk(Chunk &chunk) override;
-	virtual void readSubfile(Subfile &subfile, Chunk &chunk) override;
 
 	virtual void readParameter(Chunk &chunk, ActorHeaderSectionType paramType) override;
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
@@ -98,20 +130,18 @@ public:
 
 	virtual bool isVisible() const override { return _isVisible; }
 
-	uint32 _audioChunkReference = 0;
-	uint32 _animationChunkReference = 0;
-
 private:
-	AudioSequence _audioSequence;
-	uint _audioChunkCount = 0;
+	ImtStreamFeed *_streamFeed = nullptr;
 	uint _fullTime = 0;
+	uint _chunkCount = 0;
+	double _frameRate = 0;
 
 	uint _loadType = 0;
 	bool _isPlaying = false;
 	bool _hasStill = false;
 
-	Common::Array<MovieFrame *> _frames;
-	Common::Array<MovieFrameImage *> _images;
+	StreamMovieActorFrames *_streamFrames = nullptr;
+	StreamMovieActorSound *_streamSound = nullptr;
 
 	Common::Array<MovieFrame *> _framesNotYetShown;
 	Common::SortedArray<MovieFrame *, const MovieFrame *> _framesOnScreen;
@@ -125,8 +155,8 @@ private:
 	void invalidateRect(const Common::Rect &rect);
 	void decompressIntoAuxImage(MovieFrame *frame);
 
-	void readImageData(Chunk &chunk);
-	void readFrameData(Chunk &chunk);
+	void parseMovieHeader(Chunk &chunk);
+	void parseMovieChunkMarker(Chunk &chunk);
 
 	Common::Rect getFrameBoundingBox(MovieFrame *frame);
 	static int compareFramesByZIndex(const MovieFrame *a, const MovieFrame *b);
diff --git a/engines/mediastation/actors/path.cpp b/engines/mediastation/actors/path.cpp
index ec54edf1336..af8db5da1cc 100644
--- a/engines/mediastation/actors/path.cpp
+++ b/engines/mediastation/actors/path.cpp
@@ -137,7 +137,7 @@ void PathActor::process() {
 		// palette animation in the On Step event handler, so nothing is actually drawn on the screen now.
 
 		// We don't run a step event for the last step.
-		runEventHandlerIfExists(kStepEvent);
+		runEventHandlerIfExists(kPathStepEvent);
 		_nextPathStepTime = ++_currentStep * _stepDurationInMilliseconds;
 	} else {
 		_isPlaying = false;
diff --git a/engines/mediastation/actors/sound.cpp b/engines/mediastation/actors/sound.cpp
index e705a2e6b9e..ce7e2a426e5 100644
--- a/engines/mediastation/actors/sound.cpp
+++ b/engines/mediastation/actors/sound.cpp
@@ -22,9 +22,18 @@
 #include "mediastation/audio.h"
 #include "mediastation/debugchannels.h"
 #include "mediastation/actors/sound.h"
+#include "mediastation/mediastation.h"
 
 namespace MediaStation {
 
+SoundActor::~SoundActor() {
+	unregisterWithStreamManager();
+	if (_streamFeed != nullptr) {
+		g_engine->getStreamFeedManager()->closeStreamFeed(_streamFeed);
+		_streamFeed = nullptr;
+	}
+}
+
 void SoundActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 	switch (paramType) {
 	case kActorHeaderActorId: {
@@ -37,8 +46,9 @@ void SoundActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 		break;
 	}
 
-	case kActorHeaderChunkReference:
-		_chunkReference = chunk.readTypedChunkReference();
+	case kActorHeaderChannelIdent:
+		_channelIdent = chunk.readTypedChannelIdent();
+		registerWithStreamManager();
 		break;
 
 	case kActorHeaderHasOwnSubfile:
@@ -46,7 +56,6 @@ void SoundActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 		break;
 
 	case kActorHeaderSoundInfo:
-		_chunkCount = chunk.readTypedUint16();
 		_sequence.readParameters(chunk);
 		break;
 
@@ -71,6 +80,10 @@ void SoundActor::process() {
 		runEventHandlerIfExists(kSoundEndEvent);
 	}
 }
+void SoundActor::readChunk(Chunk &chunk) {
+	_isLoadedFromChunk = true;
+	_sequence.readChunk(chunk);
+}
 
 ScriptValue SoundActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
 	ScriptValue returnValue;
@@ -101,28 +114,17 @@ ScriptValue SoundActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptV
 	}
 }
 
-void SoundActor::readSubfile(Subfile &subfile, Chunk &chunk) {
-	uint32 expectedChunkId = chunk._id;
-
-	debugC(5, kDebugLoading, "Sound::readSubfile(): Reading %d chunks", _chunkCount);
-	readChunk(chunk);
-	for (uint i = 1; i < _chunkCount; i++) {
-		debugC(5, kDebugLoading, "Sound::readSubfile(): Reading chunk %d of %d", i, _chunkCount);
-		chunk = subfile.nextChunk();
-		if (chunk._id != expectedChunkId) {
-			error("%s: Expected chunk %s, got %s", __func__, tag2str(expectedChunkId), tag2str(chunk._id));
-		}
-		readChunk(chunk);
+void SoundActor::timePlay() {
+	if (_streamFeed == nullptr && !_isLoadedFromChunk) {
+		_streamFeed = g_engine->getStreamFeedManager()->openStreamFeed(_id);
+		_streamFeed->readData();
 	}
-}
 
-void SoundActor::timePlay() {
 	if (_isPlaying) {
 		return;
 	}
 
 	if (_sequence.isEmpty()) {
-		warning("%s: Sound has no contents, probably because the sound is in INSTALL.CXT and isn't loaded yet", __func__);
 		_isPlaying = false;
 		return;
 	}
diff --git a/engines/mediastation/actors/sound.h b/engines/mediastation/actors/sound.h
index 119839c1278..416f2e277b3 100644
--- a/engines/mediastation/actors/sound.h
+++ b/engines/mediastation/actors/sound.h
@@ -30,22 +30,23 @@
 
 namespace MediaStation {
 
-class SoundActor : public Actor {
+class SoundActor : public Actor, public ChannelClient {
 public:
 	SoundActor() : Actor(kActorTypeSound) {};
+	~SoundActor();
 
 	virtual void readParameter(Chunk &chunk, ActorHeaderSectionType paramType) override;
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 	virtual void process() override;
 
-	virtual void readChunk(Chunk &chunk) override { _sequence.readChunk(chunk); }
-	virtual void readSubfile(Subfile &subFile, Chunk &chunk) override;
+	virtual void readChunk(Chunk &chunk) override;
 
 private:
+	ImtStreamFeed *_streamFeed = nullptr;
+	bool _isLoadedFromChunk = false;
 	uint _loadType = 0;
 	bool _hasOwnSubfile = false;
 	bool _isPlaying = false;
-	uint _chunkCount = 0;
 	AudioSequence _sequence;
 
 	// Script method implementations
diff --git a/engines/mediastation/actors/sprite.cpp b/engines/mediastation/actors/sprite.cpp
index e30d4ed37d5..36b59ce7030 100644
--- a/engines/mediastation/actors/sprite.cpp
+++ b/engines/mediastation/actors/sprite.cpp
@@ -58,21 +58,22 @@ uint32 SpriteFrame::index() {
 	return _bitmapHeader->_index;
 }
 
-SpriteMovieActor::~SpriteMovieActor() {
-	// If we're just referencing another actor's frames,
-	// don't delete those frames.
-	if (_actorReference == 0) {
-		for (SpriteFrame *frame : _frames) {
-			delete frame;
-		}
+SpriteAsset::~SpriteAsset() {
+	for (SpriteFrame *frame : frames) {
+		delete frame;
 	}
-	_frames.clear();
+}
+
+SpriteMovieActor::~SpriteMovieActor() {
+	unregisterWithStreamManager();
 }
 
 void SpriteMovieActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 	switch (paramType) {
-	case kActorHeaderChunkReference:
-		_chunkReference = chunk.readTypedChunkReference();
+	case kActorHeaderChannelIdent:
+		_channelIdent = chunk.readTypedChannelIdent();
+		registerWithStreamManager();
+		_asset = Common::SharedPtr<SpriteAsset>(new SpriteAsset);
 		break;
 
 	case kActorHeaderFrameRate:
@@ -115,6 +116,20 @@ void SpriteMovieActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramT
 		break;
 	}
 
+	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);
+		_asset = referencedSprite->_asset;
+		break;
+	}
+
 	default:
 		SpatialEntity::readParameter(chunk, paramType);
 	}
@@ -292,11 +307,11 @@ void SpriteMovieActor::readChunk(Chunk &chunk) {
 	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);
-	_frames.push_back(frame);
+	_asset->frames.push_back(frame);
 
 	// TODO: Are these in exactly reverse order? If we can just reverse the
 	// whole thing once.
-	Common::sort(_frames.begin(), _frames.end(), [](SpriteFrame *a, SpriteFrame *b) {
+	Common::sort(_asset->frames.begin(), _asset->frames.end(), [](SpriteFrame *a, SpriteFrame *b) {
 		return a->index() < b->index();
 	});
 }
@@ -361,7 +376,7 @@ void SpriteMovieActor::postMovieEndEventIfNecessary() {
 }
 
 void SpriteMovieActor::draw(const Common::Array<Common::Rect> &dirtyRegion) {
-	SpriteFrame *activeFrame = _frames[_currentFrameIndex];
+	SpriteFrame *activeFrame = _asset->frames[_currentFrameIndex];
 	if (_isVisible) {
 		Common::Rect frameBbox = activeFrame->boundingBox();
 		frameBbox.translate(_boundingBox.left, _boundingBox.top);
diff --git a/engines/mediastation/actors/sprite.h b/engines/mediastation/actors/sprite.h
index 12433b11d7b..01a2e1447c9 100644
--- a/engines/mediastation/actors/sprite.h
+++ b/engines/mediastation/actors/sprite.h
@@ -24,6 +24,7 @@
 
 #include "common/rect.h"
 #include "common/array.h"
+#include "common/ptr.h"
 
 #include "mediastation/actor.h"
 #include "mediastation/datafile.h"
@@ -44,7 +45,7 @@ public:
 	SpriteFrameHeader(Chunk &chunk);
 
 	uint _index;
-	Common::Point _boundingBox;
+	Common::Point _offset;
 };
 
 class SpriteFrame : public Bitmap {
@@ -62,11 +63,17 @@ private:
 	SpriteFrameHeader *_bitmapHeader = nullptr;
 };
 
+// The original had a separate class that did reference counting,
+// for sharing an asset across actors, but we can just use a SharedPtr.
+struct SpriteAsset {
+	~SpriteAsset();
+
+	Common::Array<SpriteFrame *> frames;
+};
+
 // Sprites are somewhat like movies, but they strictly show one frame at a time
 // and don't have sound. They are intended for background/recurrent animations.
-class SpriteMovieActor : public SpatialEntity {
-friend class Context;
-
+class SpriteMovieActor : public SpatialEntity, public ChannelClient {
 public:
 	SpriteMovieActor() : SpatialEntity(kActorTypeSprite) {};
 	~SpriteMovieActor();
@@ -86,8 +93,9 @@ private:
 	uint _loadType = 0;
 	uint _frameRate = 0;
 	uint _frameCount = 0;
+	uint _actorReference = 0;
 	Common::HashMap<uint, SpriteClip> _clips;
-	Common::Array<SpriteFrame *> _frames;
+	Common::SharedPtr<SpriteAsset> _asset;
 	bool _isPlaying = false;
 	uint _currentFrameIndex = 0;
 	uint _nextFrameTime = 0;
diff --git a/engines/mediastation/actors/stage.cpp b/engines/mediastation/actors/stage.cpp
index 94c2e46b881..3b215408991 100644
--- a/engines/mediastation/actors/stage.cpp
+++ b/engines/mediastation/actors/stage.cpp
@@ -402,7 +402,7 @@ StageDirector::StageDirector() {
 }
 
 StageDirector::~StageDirector() {
-	delete _rootStage;
+	g_engine->destroyActor(RootStage::ROOT_STAGE_ACTOR_ID);
 	_rootStage = nullptr;
 }
 
diff --git a/engines/mediastation/audio.cpp b/engines/mediastation/audio.cpp
index f80c9d543fa..ba76640e03e 100644
--- a/engines/mediastation/audio.cpp
+++ b/engines/mediastation/audio.cpp
@@ -56,25 +56,22 @@ void AudioSequence::stop() {
 }
 
 void AudioSequence::readParameters(Chunk &chunk) {
+	_chunkCount = chunk.readTypedUint16();
 	_rate = chunk.readTypedUint32();
 	_channelCount = chunk.readTypedUint16();
 	_bitsPerSample = chunk.readTypedUint16();
 }
 
 void AudioSequence::readChunk(Chunk &chunk) {
-	byte *buffer = (byte *)malloc(chunk._length);
-	chunk.read((void *)buffer, chunk._length);
+	Common::SeekableReadStream *buffer = chunk.readStream(chunk._length);
 	Audio::SeekableAudioStream *stream = nullptr;
 	switch (_bitsPerSample) {
 	case 16:
-		stream = Audio::makeRawStream(buffer, chunk._length, _rate, Audio::FLAG_16BITS | Audio::FLAG_LITTLE_ENDIAN);
+		stream = Audio::makeRawStream(buffer, _rate, Audio::FLAG_16BITS | Audio::FLAG_LITTLE_ENDIAN);
 		break;
 
-	case 4: // IMA ADPCM-encoded
-		// TODO: The interface here is different. We can't pass in the
-		// buffers directly. We have to make a stream first.
-		warning("%s: ADPCM decoding not implemented yet", __func__);
-		chunk.skip(chunk.bytesRemaining());
+	case 4: // IMA ADPCM-encoded, raw nibbles (no headers).
+		stream = Audio::makeADPCMStream(buffer, DisposeAfterUse::YES, 0, Audio::kADPCMDVI, _rate, 1, 8);
 		break;
 
 	default:
diff --git a/engines/mediastation/audio.h b/engines/mediastation/audio.h
index bf62956cc20..6a1d152af78 100644
--- a/engines/mediastation/audio.h
+++ b/engines/mediastation/audio.h
@@ -46,6 +46,7 @@ public:
 	uint _rate = 0;
 	uint _channelCount = 0;
 	uint _bitsPerSample = 0;
+	uint _chunkCount = 0;
 
 private:
 	Common::Array<Audio::SeekableAudioStream *> _streams;
diff --git a/engines/mediastation/boot.cpp b/engines/mediastation/boot.cpp
index 974036e8d37..a8093c8b6f6 100644
--- a/engines/mediastation/boot.cpp
+++ b/engines/mediastation/boot.cpp
@@ -25,17 +25,17 @@
 
 namespace MediaStation {
 
-#pragma region ContextDeclaration
-ContextDeclaration::ContextDeclaration(Chunk &chunk) {
+#pragma region ContextReference
+ContextReference::ContextReference(Chunk &chunk) {
 	// Read the file number.
-	ContextDeclarationSectionType sectionType = getSectionType(chunk);
-	if (kContextDeclarationContextId != sectionType) {
+	ContextReferenceSectionType sectionType = getSectionType(chunk);
+	if (kContextReferenceContextId != sectionType) {
 		error("%s: Got unexpected section type %d", __func__, static_cast<uint>(sectionType));
 	}
 	_contextId = chunk.readTypedUint16();
 
 	sectionType = getSectionType(chunk);
-	if (kContextDeclarationStreamId != sectionType) {
+	if (kContextReferenceStreamId != sectionType) {
 		error("%s: Got unexpected section type %d", __func__, static_cast<uint>(sectionType));
 	}
 	_streamId = chunk.readTypedUint16();
@@ -44,7 +44,7 @@ ContextDeclaration::ContextDeclaration(Chunk &chunk) {
 	// and unfortunately we can't determine which just by relying
 	// on the title compiler version number.
 	sectionType = getSectionType(chunk);
-	if (kContextDeclarationName == sectionType) {
+	if (kContextReferenceName == sectionType) {
 		_name = chunk.readTypedString();
 		sectionType = getSectionType(chunk);
 	}
@@ -53,7 +53,7 @@ ContextDeclaration::ContextDeclaration(Chunk &chunk) {
 	// references there are beforehand, so we'll just read until
 	// we get something else.
 	uint rewindOffset = chunk.pos();
-	while (kContextDeclarationParentContextId == sectionType) {
+	while (kContextReferenceParentContextId == sectionType) {
 		int fileReference = chunk.readTypedUint16();
 		_parentContextIds.push_back(fileReference);
 		rewindOffset = chunk.pos();
@@ -62,44 +62,44 @@ ContextDeclaration::ContextDeclaration(Chunk &chunk) {
 	chunk.seek(rewindOffset);
 }
 
-ContextDeclarationSectionType ContextDeclaration::getSectionType(Chunk &chunk) {
-	return static_cast<ContextDeclarationSectionType>(chunk.readTypedUint16());
+ContextReferenceSectionType ContextReference::getSectionType(Chunk &chunk) {
+	return static_cast<ContextReferenceSectionType>(chunk.readTypedUint16());
 }
 #pragma endregion
 
-#pragma region ScreenDeclaration
-ScreenDeclaration::ScreenDeclaration(Chunk &chunk) {
+#pragma region ScreenReference
+ScreenReference::ScreenReference(Chunk &chunk) {
 	// Make sure this declaration isn't empty.
-	ScreenDeclarationSectionType sectionType = getSectionType(chunk);
-	if (kScreenDeclarationActorId != sectionType) {
+	ScreenReferenceSectionType sectionType = getSectionType(chunk);
+	if (kScreenReferenceScreenId != sectionType) {
 		error("%s: Got unexpected section type %d", __func__, static_cast<uint>(sectionType));
 	}
-	_actorId = chunk.readTypedUint16();
+	_screenActorId = chunk.readTypedUint16();
 
 	sectionType = getSectionType(chunk);
-	if (kScreenDeclarationScreenId != sectionType) {
+	if (kScreenReferenceContextId != sectionType) {
 		error("%s: Got unexpected section type %d", __func__, static_cast<uint>(sectionType));
 	}
-	_screenId = chunk.readTypedUint16();
+	_contextId = chunk.readTypedUint16();
 }
 
-ScreenDeclarationSectionType ScreenDeclaration::getSectionType(Chunk &chunk) {
-	return static_cast<ScreenDeclarationSectionType>(chunk.readTypedUint16());
+ScreenReferenceSectionType ScreenReference::getSectionType(Chunk &chunk) {
+	return static_cast<ScreenReferenceSectionType>(chunk.readTypedUint16());
 }
 #pragma endregion
 
-#pragma region FileDeclaration
-FileDeclaration::FileDeclaration(Chunk &chunk) {
+#pragma region FileInfo
+FileInfo::FileInfo(Chunk &chunk) {
 	// Read the file ID.
-	FileDeclarationSectionType sectionType = getSectionType(chunk);
-	if (kFileDeclarationFileId != sectionType) {
+	FileInfoSectionType sectionType = getSectionType(chunk);
+	if (kFileInfoFileId != sectionType) {
 		error("%s: Got unexpected section type %d", __func__, static_cast<uint>(sectionType));
 	}
 	_id = chunk.readTypedUint16();
 
 	// Read the intended file location.
 	sectionType = getSectionType(chunk);
-	if (kFileDeclarationFileNameAndType != sectionType) {
+	if (kFileInfoFileNameAndType != sectionType) {
 		error("%s: Got unexpected section type %d", __func__, static_cast<uint>(sectionType));
 	}
 	_intendedLocation = static_cast<IntendedFileLocation>(chunk.readTypedUint16());
@@ -110,37 +110,37 @@ FileDeclaration::FileDeclaration(Chunk &chunk) {
 	_name = chunk.readTypedFilename();
 }
 
-FileDeclarationSectionType FileDeclaration::getSectionType(Chunk &chunk) {
-	return static_cast<FileDeclarationSectionType>(chunk.readTypedUint16());
+FileInfoSectionType FileInfo::getSectionType(Chunk &chunk) {
+	return static_cast<FileInfoSectionType>(chunk.readTypedUint16());
 }
 #pragma endregion
 
-#pragma region SubfileDeclaration
-SubfileDeclaration::SubfileDeclaration(Chunk &chunk) {
+#pragma region StreamInfo
+StreamInfo::StreamInfo(Chunk &chunk) {
 	// Read the actor ID.
-	SubfileDeclarationSectionType sectionType = getSectionType(chunk);
-	if (kSubfileDeclarationActorId != sectionType) {
+	StreamInfoSectionType sectionType = getSectionType(chunk);
+	if (kStreamInfoActorId != sectionType) {
 		error("%s: Got unexpected section type %d", __func__, static_cast<uint>(sectionType));
 	}
 	_actorId = chunk.readTypedUint16();
 
 	// Read the file ID.
 	sectionType = getSectionType(chunk);
-	if (kSubfileDeclarationFileId != sectionType) {
+	if (kStreamInfoFileId != sectionType) {
 		error("%s: Expected section type FILE_ID, got 0x%x", __func__, static_cast<uint>(sectionType));
 	}
 	_fileId = chunk.readTypedUint16();
 
 	// Read the start offset from the absolute start of the file.
 	sectionType = getSectionType(chunk);
-	if (kSubfileDeclarationStartOffset != sectionType) {
+	if (kStreamInfoStartOffset != sectionType) {
 		error("%s: Expected section type START_OFFSET, got 0x%x", __func__, static_cast<uint>(sectionType));
 	}
 	_startOffsetInFile = chunk.readTypedUint32();
 }
 
-SubfileDeclarationSectionType SubfileDeclaration::getSectionType(Chunk &chunk) {
-	return static_cast<SubfileDeclarationSectionType>(chunk.readTypedUint16());
+StreamInfoSectionType StreamInfo::getSectionType(Chunk &chunk) {
+	return static_cast<StreamInfoSectionType>(chunk.readTypedUint16());
 }
 #pragma endregion
 
@@ -155,43 +155,10 @@ CursorDeclaration::CursorDeclaration(Chunk &chunk) {
 #pragma endregion
 
 #pragma region Boot
-Boot::Boot(const Common::Path &path) : Datafile(path) {
-	Subfile subfile = getNextSubfile();
-	Chunk chunk = subfile.nextChunk();
-
-	// TODO: This is really analogous to RT_ImtGod::notifyBufferFilled.
-	// But we are currently only handling the DocumentDef part of it.
-	BootStreamType streamType = static_cast<BootStreamType>(chunk.readTypedUint16());
-	switch (streamType) {
-		case kBootDocumentDef:
-			readDocumentDef(chunk);
-			break;
-
-		case kBootControlCommands:
-			error("%s: STUB: readControlComments", __func__);
-			break;
-
-		default:
-			error("%s: Unhandled section type 0x%x", __func__, static_cast<uint>(streamType));
-	}
-}
-
-BootSectionType Boot::getSectionType(Chunk &chunk) {
-	return static_cast<BootSectionType>(chunk.readTypedUint16());
-}
-
-Boot::~Boot() {
-	_contextDeclarations.clear();
-	_streamMap.clear();
-	_engineResourceDeclarations.clear();
-	_screenDeclarations.clear();
-	_fileMap.clear();
-}
-
-void Boot::readDocumentDef(Chunk &chunk) {
+void MediaStationEngine::readDocumentDef(Chunk &chunk) {
 	BootSectionType sectionType = kBootLastSection;
 	while (true) {
-		sectionType = getSectionType(chunk);
+		sectionType = static_cast<BootSectionType>(chunk.readTypedUint16());
 		if (sectionType == kBootLastSection) {
 			break;
 		}
@@ -199,25 +166,25 @@ void Boot::readDocumentDef(Chunk &chunk) {
 	}
 }
 
-void Boot::readDocumentInfoFromStream(Chunk &chunk, BootSectionType sectionType) {
+void MediaStationEngine::readDocumentInfoFromStream(Chunk &chunk, BootSectionType sectionType) {
 	switch (sectionType) {
 	case kBootVersionInformation:
 		readVersionInfoFromStream(chunk);
 		break;
 
-	case kBootContextDeclaration:
+	case kBootContextReference:
 		readContextReferencesFromStream(chunk);
 		break;
 
-	case kBootScreenDeclaration:
-		readScreenDeclarationsFromStream(chunk);
+	case kBootScreenReference:
+		readScreenReferencesFromStream(chunk);
 		break;
 
-	case kBootFileDeclaration:
+	case kBootFileInfo:
 		readAndAddFileMaps(chunk);
 		break;
 
-	case kBootSubfileDeclaration:
+	case kBootStreamInfo:
 		readAndAddStreamMaps(chunk);
 		break;
 
@@ -236,48 +203,48 @@ void Boot::readDocumentInfoFromStream(Chunk &chunk, BootSectionType sectionType)
 	default:
 		// See if any registered parameter clients know how to
 		// handle this parameter.
-		g_engine->readUnrecognizedFromStream(chunk, static_cast<uint>(sectionType));
+		readUnrecognizedFromStream(chunk, static_cast<uint>(sectionType));
 	}
 }
 
-void Boot::readVersionInfoFromStream(Chunk &chunk) {
+void MediaStationEngine::readVersionInfoFromStream(Chunk &chunk) {
 	_gameTitle = chunk.readTypedString();
 	_versionInfo = chunk.readTypedVersion();
 	_engineInfo = chunk.readTypedString();
 	_sourceString = chunk.readTypedString();
 }
 
-void Boot::readContextReferencesFromStream(Chunk &chunk) {
+void MediaStationEngine::readContextReferencesFromStream(Chunk &chunk) {
 	uint flag = chunk.readTypedUint16();
 	while (flag != 0) {
-		ContextDeclaration contextDeclaration(chunk);
-		_contextDeclarations.setVal(contextDeclaration._contextId, contextDeclaration);
+		ContextReference contextReference(chunk);
+		_contextReferences.setVal(contextReference._contextId, contextReference);
 		flag = chunk.readTypedUint16();
 	}
 }
 
-void Boot::readScreenDeclarationsFromStream(Chunk &chunk) {
+void MediaStationEngine::readScreenReferencesFromStream(Chunk &chunk) {
 	uint flag = chunk.readTypedUint16();
 	while (flag != 0) {
-		ScreenDeclaration screenDeclaration(chunk);
-		_screenDeclarations.setVal(screenDeclaration._screenId, screenDeclaration);
+		ScreenReference screenDeclaration(chunk);
+		_screenReferences.setVal(screenDeclaration._screenActorId, screenDeclaration);
 		flag = chunk.readTypedUint16();
 	}
 }
 
-void Boot::readAndAddFileMaps(Chunk &chunk) {
+void MediaStationEngine::readAndAddFileMaps(Chunk &chunk) {
 	uint flag = chunk.readTypedUint16();
 	while (flag != 0) {
-		FileDeclaration fileDeclaration(chunk);
+		FileInfo fileDeclaration(chunk);
 		_fileMap.setVal(fileDeclaration._id, fileDeclaration);
 		flag = chunk.readTypedUint16();
 	}
 }
 
-void Boot::readAndAddStreamMaps(Chunk &chunk) {
+void MediaStationEngine::readAndAddStreamMaps(Chunk &chunk) {
 	uint flag = chunk.readTypedUint16();
 	while (flag != 0) {
-		SubfileDeclaration subfileDeclaration(chunk);
+		StreamInfo subfileDeclaration(chunk);
 		_streamMap.setVal(subfileDeclaration._actorId, subfileDeclaration);
 		flag = chunk.readTypedUint16();
 	}
diff --git a/engines/mediastation/boot.h b/engines/mediastation/boot.h
index 2119225fafd..c64647ec99b 100644
--- a/engines/mediastation/boot.h
+++ b/engines/mediastation/boot.h
@@ -31,18 +31,18 @@
 
 namespace MediaStation {
 
-enum ContextDeclarationSectionType {
-	kContextDeclarationPlaceholder = 0x0003,
-	kContextDeclarationContextId = 0x0004,
-	kContextDeclarationStreamId = 0x0005,
-	kContextDeclarationParentContextId = 0x0006,
-	kContextDeclarationName = 0x0bb8
+enum ContextReferenceSectionType {
+	kContextReferencePlaceholder = 0x0003,
+	kContextReferenceContextId = 0x0004,
+	kContextReferenceStreamId = 0x0005,
+	kContextReferenceParentContextId = 0x0006,
+	kContextReferenceName = 0x0bb8
 };
 
-class ContextDeclaration {
+class ContextReference {
 public:
-	ContextDeclaration(Chunk &chunk);
-	ContextDeclaration() {};
+	ContextReference(Chunk &chunk);
+	ContextReference() {};
 
 	uint _contextId = 0;
 	uint _streamId = 0;
@@ -50,30 +50,30 @@ public:
 	Common::Array<uint> _parentContextIds;
 
 private:
-	ContextDeclarationSectionType getSectionType(Chunk &chunk);
+	ContextReferenceSectionType getSectionType(Chunk &chunk);
 };
 
-enum ScreenDeclarationSectionType {
-	kScreenDeclarationActorId = 0x0009,
-	kScreenDeclarationScreenId = 0x0004
+enum ScreenReferenceSectionType {
+	kScreenReferenceScreenId = 0x0009,
+	kScreenReferenceContextId = 0x0004
 };
 
-class ScreenDeclaration {
+class ScreenReference {
 public:
-	ScreenDeclaration(Chunk &chunk);
-	ScreenDeclaration() {};
+	ScreenReference(Chunk &chunk);
+	ScreenReference() {};
 
-	uint _actorId = 0;
-	uint _screenId = 0;
+	uint _screenActorId = 0;
+	uint _contextId = 0;
 
 private:
-	ScreenDeclarationSectionType getSectionType(Chunk &chunk);
+	ScreenReferenceSectionType getSectionType(Chunk &chunk);
 };
 
-enum FileDeclarationSectionType {
-	kFileDeclarationEmptySection = 0x0000,
-	kFileDeclarationFileId = 0x002b,
-	kFileDeclarationFileNameAndType = 0x002d
+enum FileInfoSectionType {
+	kFileInfoEmptySection = 0x0000,
+	kFileInfoFileId = 0x002b,
+	kFileInfoFileNameAndType = 0x002d
 };
 
 // Indicates where a file is intended to be stored.
@@ -89,37 +89,37 @@ enum IntendedFileLocation {
 	kFileIntendedOnHardDisk = 0x000b
 };
 
-class FileDeclaration {
+class FileInfo {
 public:
-	FileDeclaration(Chunk &chunk);
-	FileDeclaration() {};
+	FileInfo(Chunk &chunk);
+	FileInfo() {};
 
 	uint _id = 0;
 	IntendedFileLocation _intendedLocation = kFileLocationEmpty;
 	Common::String _name;
 
 private:
-	FileDeclarationSectionType getSectionType(Chunk &chunk);
+	FileInfoSectionType getSectionType(Chunk &chunk);
 };
 
-enum SubfileDeclarationSectionType {
-	kSubfileDeclarationEmptySection = 0x0000,
-	kSubfileDeclarationActorId = 0x002a,
-	kSubfileDeclarationFileId = 0x002b,
-	kSubfileDeclarationStartOffset = 0x002c
+enum StreamInfoSectionType {
+	kStreamInfoEmptySection = 0x0000,
+	kStreamInfoActorId = 0x002a,
+	kStreamInfoFileId = 0x002b,
+	kStreamInfoStartOffset = 0x002c
 };
 
-class SubfileDeclaration {
+class StreamInfo {
 public:
-	SubfileDeclaration(Chunk &chunk);
-	SubfileDeclaration() {};
+	StreamInfo(Chunk &chunk);
+	StreamInfo() {};
 
 	uint _actorId = 0;
 	uint _fileId = 0;
 	uint _startOffsetInFile = 0;
 
 private:
-	SubfileDeclarationSectionType getSectionType(Chunk &chunk);
+	StreamInfoSectionType getSectionType(Chunk &chunk);
 };
 
 // Declares a cursor, which is stored as a cursor resource in the game executable.
@@ -142,55 +142,18 @@ public:
 	int _id = 0;
 };
 
-enum BootStreamType {
-	kBootDocumentDef = 0x01,
-	kBootControlCommands = 0x0d,
-};
-
 enum BootSectionType {
 	kBootLastSection = 0x0000,
-	kBootContextDeclaration = 0x0002,
+	kBootContextReference = 0x0002,
 	kBootVersionInformation = 0x0190,
 	kBootUnk1 = 0x0191,
 	kBootFunctionTableSize = 0x0192,
 	kBootUnk3 = 0x0193,
 	kBootEngineResource = 0x0bba,
 	kBootEngineResourceId = 0x0bbb,
-	kBootScreenDeclaration = 0x0007,
-	kBootFileDeclaration = 0x000a,
-	kBootSubfileDeclaration = 0x000b,
-};
-
-class Boot : Datafile {
-private:
-	BootSectionType getSectionType(Chunk &chunk);
-
-public:
-	Common::String _gameTitle;
-	VersionInfo _versionInfo;
-	Common::String _engineInfo;
-	Common::String _sourceString;
-	Common::HashMap<uint32, ContextDeclaration> _contextDeclarations;
-	Common::HashMap<uint32, ScreenDeclaration> _screenDeclarations;
-	Common::HashMap<uint32, FileDeclaration> _fileMap;
-	Common::HashMap<uint32, SubfileDeclaration> _streamMap;
-	Common::HashMap<uint32, EngineResourceDeclaration> _engineResourceDeclarations;
-	uint _unk1 = 0;
-	uint _functionTableSize = 0;
-	uint _unk3 = 0;
-
-	void readDocumentDef(Chunk &chunk);
-	void readDocumentInfoFromStream(Chunk &chunk, BootSectionType sectionType);
-	void readVersionInfoFromStream(Chunk &chunk);
-	void readContextReferencesFromStream(Chunk &chunk);
-	void readScreenDeclarationsFromStream(Chunk &chunk);
-	void readAndAddFileMaps(Chunk &chunk);
-	void readAndAddStreamMaps(Chunk &chunk);
-
-	void readStartupInformation(Chunk &chunk);
-
-	Boot(const Common::Path &path);
-	~Boot();
+	kBootScreenReference = 0x0007,
+	kBootFileInfo = 0x000a,
+	kBootStreamInfo = 0x000b,
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/clients.cpp b/engines/mediastation/clients.cpp
index 5c4afdba8d6..0a0e16146db 100644
--- a/engines/mediastation/clients.cpp
+++ b/engines/mediastation/clients.cpp
@@ -19,7 +19,11 @@
  *
  */
 
+#include "mediastation/actors/screen.h"
+#include "mediastation/debugchannels.h"
 #include "mediastation/clients.h"
+#include "mediastation/context.h"
+#include "mediastation/mediastation.h"
 
 namespace MediaStation {
 
@@ -41,36 +45,262 @@ bool DeviceOwner::attemptToReadFromStream(Chunk &chunk, uint sectionType) {
 	return handledParam;
 }
 
+bool Document::attemptToReadFromStream(Chunk &chunk, uint sectionType) {
+	bool handledParam = true;
+	switch (sectionType) {
+	case kDocumentContextLoadComplete:
+		readContextLoadComplete(chunk);
+		break;
+
+	case kDocumentStartupInformation:
+		readStartupInformation(chunk);
+		break;
+
+	default:
+		handledParam = false;
+	}
+
+	return handledParam;
+}
+
 void Document::readStartupInformation(Chunk &chunk) {
 	DocumentSectionType sectionType = static_cast<DocumentSectionType>(chunk.readTypedUint16());
+	debugC(5, kDebugLoading, "%s: sectionType = 0x%x", __func__, static_cast<uint>(sectionType));
 	switch (sectionType) {
-	case kDocumentEntryScreen:
-		_entryScreenId = chunk.readTypedUint16();
+	case kDocumentEntryScreen: {
+		uint entryPointScreenId = chunk.readTypedUint16();
+		if (_entryPointScreenId == 0) {
+			// We don't want to reset the overridden screen entry point.
+			_entryPointScreenId = entryPointScreenId;
+		}
 		break;
+	}
 
 	default:
 		error("%s: Unhandled section type 0x%x", __func__, static_cast<uint>(sectionType));
 	}
 }
 
-bool Document::attemptToReadFromStream(Chunk &chunk, uint sectionType) {
-	bool handledParam = true;
-	switch (sectionType) {
-	case kDocumentContextLoadComplete: {
-		uint contextId = chunk.readTypedUint16();
-		warning("STUB: readContextLoadCompleteFromStream %d", contextId);
-		break;
+void Document::readContextLoadComplete(Chunk &chunk) {
+	uint contextId = chunk.readTypedUint16();
+	debugC(5, kDebugLoading, "%s: Context %d", __func__, contextId);
+	if (contextId == _loadingContextId) {
+		contextLoadDidComplete();
 	}
 
-	case kDocumentStartupInformation:
-		readStartupInformation(chunk);
-		break;
+	if (_loadingScreenActorId != 0) {
+		uint loadingScreenActorContextId = contextIdForScreenActorId(_loadingScreenActorId);
+		if (contextId == loadingScreenActorContextId) {
+			screenLoadDidComplete();
+		}
+	}
+}
 
-	default:
-		handledParam = false;
+void Document::beginTitle(uint overriddenEntryPointScreenId) {
+	_entryPointStreamId = MediaStationEngine::BOOT_STREAM_ID;
+	if (overriddenEntryPointScreenId != 0) {
+		// This lets us override the default entry screen for development purposes.
+		_entryPointScreenId = overriddenEntryPointScreenId;
+		_entryPointScreenIdWasOverridden = true;
 	}
+	startFeed(_entryPointStreamId);
+}
 
-	return handledParam;
+
+void Document::startContextLoad(uint contextId) {
+	debugC(5, kDebugLoading, "%s: Loading context %d", __func__, contextId);
+	Context *existingContext = g_engine->_loadedContexts.getValOrDefault(contextId);
+	if (existingContext == nullptr) {
+		if (_loadingContextId == 0) {
+			const ContextReference &contextRef = g_engine->contextRefWithId(contextId);
+			if (contextRef._contextId != 0) {
+				_loadingContextId = contextId;
+				startFeed(contextRef._streamId);
+			}
+		} else {
+			addToContextLoadQueue(contextId);
+		}
+	} else {
+		if (_currentScreenActorId != 0 && contextId != _loadingContextId) {
+			Actor *currentScreen = g_engine->getActorById(_currentScreenActorId);
+			ScriptValue arg;
+			arg.setToActorId(contextId);
+			currentScreen->runEventHandlerIfExists(kContextLoadCompleteEvent2, arg);
+		}
+
+		if (_loadingContextId == 0) {
+			checkQueuedContextLoads();
+		}
+	}
+}
+
+bool Document::isContextLoadInProgress(uint contextId) {
+	if (contextId == 0) {
+		// If we don't have a valid context ID, just check if we are loading any context.
+		return _loadingContextId != 0;
+	} else {
+		// If the context ID is valid, check if we are loading specifically that context.
+		return contextId == _loadingContextId;
+	}
+}
+
+void Document::branchToScreen() {
+	if (_loadingScreenActorId == 0) {
+		_loadingScreenActorId = _requestedScreenBranchId;
+		_requestedScreenBranchId = 0;
+		uint contextId = contextIdForScreenActorId(_loadingScreenActorId);
+
+		blowAwayCurrentScreen();
+		preloadParentContexts(contextId);
+		addToContextLoadQueue(contextId);
+
+		if (_loadingContextId == 0) {
+			checkQueuedContextLoads();
+		}
+	}
+}
+
+void Document::scheduleScreenBranch(uint screenActorId) {
+	// This is to support not immediately branching to the wrong screen
+	// when we are starting at a user-defined context. This is because the click
+	// handler usually points to the main menu screen rather than the screen
+	// we're starting at. It would be way too complicated to find the right variable
+	// and change it at runtime, so we will just branch to the screen we are already on.
+	// (We have to branch to something because by this point we have already faded out the screen and such,
+	// so we need to run another screen entry event to bring things back in again.)
+	if (_entryPointScreenIdWasOverridden) {
+		_entryPointScreenIdWasOverridden = false;
+		screenActorId = _currentScreenActorId;
+	}
+
+	_requestedScreenBranchId = screenActorId;
+}
+
+void Document::scheduleContextRelease(uint contextId) {
+	if (!g_engine->contextIsLocked(contextId)) {
+		_requestedContextReleaseId.push_back(contextId);
+	}
+}
+
+void Document::streamDidClose(uint streamId) {
+	bool currentStreamIsTargetStream = _currentStreamFeed != nullptr && streamId == _currentStreamFeed->_id;
+	if (!currentStreamIsTargetStream) {
+		return;
+	}
+
+	_currentStreamFeed = nullptr;
+}
+
+void Document::streamDidFinish(uint streamId) {
+	bool currentStreamIsTargetStream = _currentStreamFeed != nullptr && streamId == _currentStreamFeed->_id;
+	if (currentStreamIsTargetStream) {
+		stopFeed();
+		if (streamId == _entryPointStreamId) {
+			_requestedScreenBranchId = _entryPointScreenId;
+			branchToScreen();
+		} else {
+			checkQueuedContextLoads();
+		}
+	}
+}
+
+void Document::contextLoadDidComplete() {
+	if (_currentScreenActorId != 0) {
+		ScriptValue arg;
+		arg.setToActorId(_loadingContextId);
+		Actor *currentScreen = g_engine->getActorById(_currentScreenActorId);
+		if (currentScreen != nullptr) {
+			currentScreen->runEventHandlerIfExists(kContextLoadCompleteEvent, arg);
+		}
+	}
+	_loadingContextId = 0;
+}
+
+void Document::screenLoadDidComplete() {
+	_currentScreenActorId = _loadingScreenActorId;
+	Actor *currentScreen = g_engine->getActorById(_loadingScreenActorId);
+	currentScreen->runEventHandlerIfExists(kScreenEntryEvent);
+	_loadingScreenActorId = 0;
+}
+
+void Document::process() {
+	if (!_requestedContextReleaseId.empty()) {
+		for (uint contextId : _requestedContextReleaseId) {
+			g_engine->destroyContext(contextId);
+		}
+		_requestedContextReleaseId.clear();
+	}
+
+	if (_requestedScreenBranchId != 0) {
+		branchToScreen();
+	}
+}
+
+void Document::blowAwayCurrentScreen() {
+	if (_currentScreenActorId != 0) {
+		uint contextId = contextIdForScreenActorId(_currentScreenActorId);
+		if (contextId != 0) {
+			Actor *currentScreen = g_engine->getActorById(_currentScreenActorId);
+			currentScreen->runEventHandlerIfExists(kScreenExitEvent);
+			g_engine->destroyContext(contextId);
+		}
+	}
+}
+
+uint Document::contextIdForScreenActorId(uint screenActorId) {
+	ScreenReference screenRef = g_engine->screenRefWithId(screenActorId);
+	return screenRef._contextId;
+}
+
+void Document::startFeed(uint streamId) {
+	// The original had some more stuff here, including cache management and device ownership,
+	// but since we don't need these things right now, this function is rather empty.
+	_currentStreamFeed = g_engine->getStreamFeedManager()->openStreamFeed(streamId);
+	_currentStreamFeed->readData();
+}
+
+void Document::stopFeed() {
+	_currentStreamFeed->stopFeed();
+	g_engine->getStreamFeedManager()->closeStreamFeed(_currentStreamFeed);
+	_currentStreamFeed = nullptr;
+}
+
+void Document::preloadParentContexts(uint contextId) {
+	ContextReference contextReference = g_engine->contextRefWithId(contextId);
+	for (uint parentContextId : contextReference._parentContextIds) {
+		if (parentContextId != 0) {
+			Context *existingContext = g_engine->_loadedContexts.getValOrDefault(parentContextId);
+			if (existingContext == nullptr && parentContextId != contextId) {
+				debugC(5, kDebugLoading, "%s: Loading parent context %d", __func__, parentContextId);
+				addToContextLoadQueue(parentContextId);
+			}
+		}
+	}
+}
+
+void Document::addToContextLoadQueue(uint contextId) {
+	if (!isContextLoadQueued(contextId)) {
+		_contextLoadQueue.push_back(contextId);
+	} else {
+		warning("%s: Context %d already queued for load", __func__, contextId);
+	}
+}
+
+bool Document::isContextLoadQueued(uint contextId) {
+	for (uint queuedContextId : _contextLoadQueue) {
+		if (queuedContextId == contextId) {
+			return true;
+		}
+	}
+	return false;
+}
+
+void Document::checkQueuedContextLoads() {
+	while (!_contextLoadQueue.empty()) {
+		uint contextId = _contextLoadQueue.front();
+		_contextLoadQueue.erase(_contextLoadQueue.begin());
+		startContextLoad(contextId);
+	}
 }
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/clients.h b/engines/mediastation/clients.h
index be2fb728e0a..e317c655fcf 100644
--- a/engines/mediastation/clients.h
+++ b/engines/mediastation/clients.h
@@ -57,8 +57,50 @@ class Document : public ParameterClient {
 public:
 	virtual bool attemptToReadFromStream(Chunk &chunk, uint sectionType) override;
 	void readStartupInformation(Chunk &chunk);
+	void readContextLoadComplete(Chunk &chunk);
 
-	uint _entryScreenId = 0;
+	void beginTitle(uint overriddenEntryPointScreenId = 0);
+	void startContextLoad(uint contextId);
+	bool isContextLoadInProgress(uint contextId);
+	void branchToScreen();
+	void scheduleScreenBranch(uint screenActorId);
+	void scheduleContextRelease(uint contextId);
+
+	void streamDidClose(uint streamId);
+	void streamDidFinish(uint streamId);
+	// These implementations are left empty because they are empty in the original,
+	// but they are kept because they are technically still defined in the original.
+	void streamDidOpen(uint streamId) {};
+	void streamWillRead(uint streamId) {};
+
+	void process();
+	uint contextIdForScreenActorId(uint screenActorId);
+
+private:
+	uint _currentScreenActorId = 0;
+	StreamFeed *_currentStreamFeed = nullptr;
+	Common::Array<uint> _requestedContextReleaseId;
+	Common::Array<uint> _contextLoadQueue;
+	uint _requestedScreenBranchId = 0;
+	bool _entryPointScreenIdWasOverridden = false;
+	uint _entryPointScreenId = 0;
+	uint _entryPointStreamId = 0;
+	uint _loadingContextId = 0;
+	uint _loadingScreenActorId = 0;
+
+	void contextLoadDidComplete();
+	void screenLoadDidComplete();
+	void startFeed(uint streamId);
+	// This is named stopFeed in the original, but it is a bit of a misnomer
+	// because it also closes the stream feed. In the lower-level stream feed manager
+	// and stream feeds themselves, stopping the stream feed and closing it is
+	// two separate operations.
+	void stopFeed();
+	void blowAwayCurrentScreen();
+	void preloadParentContexts(uint contextId);
+	void addToContextLoadQueue(uint contextId);
+	bool isContextLoadQueued(uint contextId);
+	void checkQueuedContextLoads();
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/context.cpp b/engines/mediastation/context.cpp
index 83afb44df82..f1bdd54245b 100644
--- a/engines/mediastation/context.cpp
+++ b/engines/mediastation/context.cpp
@@ -41,128 +41,59 @@
 
 namespace MediaStation {
 
-Context::Context(const Common::Path &path) : Datafile(path) {
-	uint32 signature = _handle->readUint32BE();
-	if (signature != MKTAG('I', 'I', '\0', '\0')) {
-		error("%s: Wrong signature for file %s: 0x%08x", __func__, _name.c_str(), signature);
-	}
-
-	_unk1 = _handle->readUint32LE();
-	_subfileCount = _handle->readUint32LE();
-	_fileSize = _handle->readUint32LE();
-	debugC(5, kDebugLoading, "Context::Context(): _unk1 = 0x%x", _unk1);
-
-	// Read headers and actors in the first subfile.
-	Subfile subfile = getNextSubfile();
-	Chunk chunk = subfile.nextChunk();
-	readHeaderSections(subfile, chunk);
-
-	// Read actors in the rest of the subfiles.
-	for (uint i = 1; i < _subfileCount; i++) {
-		subfile = getNextSubfile();
-		readActorFromLaterSubfile(subfile);
-	}
-
-	// Some sprites and images don't have any image data themselves, they just
-	// reference the same image data in another actor. So we need to check for
-	// these and create the appropriate references.
-	for (auto it = _actors.begin(); it != _actors.end(); ++it) {
-		Actor *actor = it->_value;
-		uint referencedActorId = actor->_actorReference;
-		if (referencedActorId != 0) {
-			switch (actor->type()) {
-			case kActorTypeImage: {
-				ImageActor *image = static_cast<ImageActor *>(actor);
-				ImageActor *referencedImage = static_cast<ImageActor *>(getActorById(referencedActorId));
-				if (referencedImage == nullptr) {
-					error("%s: Actor %d references non-existent actor %d", __func__, actor->id(), referencedActorId);
-				}
-				image->_bitmap = referencedImage->_bitmap;
-				break;
-			}
-
-			case kActorTypeSprite: {
-				SpriteMovieActor *sprite = static_cast<SpriteMovieActor *>(actor);
-				SpriteMovieActor *referencedSprite = static_cast<SpriteMovieActor *>(getActorById(referencedActorId));
-				if (referencedSprite == nullptr) {
-					error("%s: Actor %d references non-existent actor %d", __func__, actor->id(), referencedActorId);
-				}
-				sprite->_frames = referencedSprite->_frames;
-				sprite->_clips = referencedSprite->_clips;
-				break;
-			}
-
-			default:
-				error("%s: Actor type %d referenced, but reference not implemented yet", __func__, actor->type());
-			}
-		}
-	}
-}
-
 Context::~Context() {
-	for (auto it = _actors.begin(); it != _actors.end(); ++it) {
-		delete it->_value;
-	}
-	_actors.clear();
-	// The same actor pointers are in here, so don't delete again.
-	_actorsByChunkReference.clear();
-
 	for (auto it = _variables.begin(); it != _variables.end(); ++it) {
 		delete it->_value;
 	}
 	_variables.clear();
 }
 
-Actor *Context::getActorById(uint actorId) {
-	return _actors.getValOrDefault(actorId);
-}
-
-Actor *Context::getActorByChunkReference(uint chunkReference) {
-	return _actorsByChunkReference.getValOrDefault(chunkReference);
-}
-
-ScriptValue *Context::getVariable(uint variableId) {
-	return _variables.getValOrDefault(variableId);
-}
-
-void Context::readControlCommands(Chunk &chunk) {
-	ContextSectionType command = kEndOfContextData;
+void MediaStationEngine::readControlCommands(Chunk &chunk) {
+	ContextSectionType sectionType = kContextEndOfSection;
 	do {
-		command = static_cast<ContextSectionType>(chunk.readTypedUint16());
-		if (command != kEndOfContextData) {
-			readCommandFromStream(command, chunk);
+		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()));
+		if (sectionType != kContextEndOfSection) {
+			readCommandFromStream(chunk, sectionType);
 		}
-	} while (command != kEndOfContextData);
+	} while (sectionType != kContextEndOfSection);
 }
 
-void Context::readCreateContextData(Chunk &chunk) {
-	// The original had contexts created from the base engine class,
-	// but things are currently structured a bit differently, so this
-	// is a no-op for now.
-	_id = chunk.readTypedUint16();
+void MediaStationEngine::readCreateContextData(Chunk &chunk) {
+	uint contextId = chunk.readTypedUint16();
+	debugC(5, kDebugLoading, "%s: Context %d", __func__, contextId);
+	Context *context = _loadedContexts.getValOrDefault(contextId);
+	if (context == nullptr) {
+		context = new Context();
+		context->_id = contextId;
+		_loadedContexts.setVal(contextId, context);
+	}
 }
 
-void Context::readDestroyContextData(Chunk &chunk) {
+void MediaStationEngine::readDestroyContextData(Chunk &chunk) {
 	uint contextId = chunk.readTypedUint16();
-	g_engine->releaseContext(contextId);
+	debugC(5, kDebugLoading, "%s: Context %d", __func__, contextId);
+	destroyContext(contextId);
 }
 
-void Context::readDestroyActorData(Chunk &chunk) {
+void MediaStationEngine::readDestroyActorData(Chunk &chunk) {
 	uint actorId = chunk.readTypedUint16();
-	g_engine->destroyActor(actorId);
+	debugC(5, kDebugLoading, "%s: Actor %d", __func__, actorId);
+	destroyActor(actorId);
 }
 
-void Context::readActorLoadComplete(Chunk &chunk) {
+void MediaStationEngine::readActorLoadComplete(Chunk &chunk) {
 	uint actorId = chunk.readTypedUint16();
 	Actor *actor = g_engine->getActorById(actorId);
 	actor->loadIsComplete();
+	debugC(5, kDebugLoading, "%s: Actor %d", __func__, actorId);
 }
 
-void Context::readCreateActorData(Chunk &chunk) {
+void MediaStationEngine::readCreateActorData(Chunk &chunk) {
 	uint contextId = chunk.readTypedUint16();
 	ActorType type = static_cast<ActorType>(chunk.readTypedUint16());
 	uint id = chunk.readTypedUint16();
-	debugC(4, kDebugLoading, "_type = 0x%x, _id = 0x%x", static_cast<uint>(type), id);
+	debugC(5, kDebugLoading, "%s: Actor %d, type 0x%x", __func__, id, static_cast<uint>(type));
 
 	Actor *actor = nullptr;
 	switch (type) {
@@ -204,7 +135,6 @@ void Context::readCreateActorData(Chunk &chunk) {
 
 	case kActorTypeScreen:
 		actor = new ScreenActor();
-		_screenActor = static_cast<ScreenActor *>(actor);
 		break;
 
 	case kActorTypeFont:
@@ -221,61 +151,33 @@ void Context::readCreateActorData(Chunk &chunk) {
 	actor->setId(id);
 	actor->setContextId(contextId);
 	actor->initFromParameterStream(chunk);
-
-	_actors.setVal(actor->id(), actor);
 	g_engine->registerActor(actor);
-	if (actor->_chunkReference != 0) {
-		debugC(5, kDebugLoading, "Context::readHeaderSection(): Storing actor with chunk ID \"%s\" (0x%x)", tag2str(actor->_chunkReference), actor->_chunkReference);
-		_actorsByChunkReference.setVal(actor->_chunkReference, actor);
-	}
-
-	if (actor->type() == kActorTypeMovie) {
-		StreamMovieActor *movie = static_cast<StreamMovieActor *>(actor);
-		if (movie->_audioChunkReference != 0) {
-			_actorsByChunkReference.setVal(movie->_audioChunkReference, actor);
-		}
-		if (movie->_animationChunkReference != 0) {
-			_actorsByChunkReference.setVal(movie->_animationChunkReference, actor);
-		}
-	}
 }
 
-void Context::readCreateVariableData(Chunk &chunk) {
+void MediaStationEngine::readCreateVariableData(Chunk &chunk) {
 	uint contextId = chunk.readTypedUint16();
-	if (contextId != _id) {
-		warning("%s: Repeated context ID didn't match: %d != %d", __func__, contextId, _id);
-	}
-
 	uint id = chunk.readTypedUint16();
 	if (g_engine->getVariable(id) != nullptr) {
 		error("%s: Global variable %d already exists", __func__, id);
 	}
 
 	ScriptValue *value = new ScriptValue(&chunk);
-	_variables.setVal(id, value);
-	debugC(5, kDebugScript, "Created global variable %d (type: %s)",
-		id, scriptValueTypeToStr(value->getType()));
+	Context *context = _loadedContexts.getValOrDefault(contextId);
+	if (context == nullptr) {
+		error("%s: Context %d does not exist or has not been loaded yet in this title", __func__, contextId);
+	}
+
+	context->_variables.setVal(id, value);
+	debugC(5, kDebugScript, "%s: %d (type: %s)", __func__, id, scriptValueTypeToStr(value->getType()));
 }
 
-void Context::readHeaderSections(Subfile &subfile, Chunk &chunk) {
+void MediaStationEngine::readHeaderSections(Subfile &subfile, Chunk &chunk) {
 	do {
-		if (chunk._id == MKTAG('i', 'g', 'o', 'd')) {
-			StreamType streamType = static_cast<StreamType>(chunk.readTypedUint16());
-			if (streamType != kControlCommandsStream) {
-				error("%s: Expected header chunk, got %s (@0x%llx)", __func__, tag2str(chunk._id), static_cast<long long int>(chunk.pos()));
-			}
-
-			readControlCommands(chunk);
-		} else {
-			Actor *actor = getActorByChunkReference(chunk._id);
-			if (actor == nullptr) {
-				// We should only need to look in the global scope when there is an
-				// install cache (INSTALL.CXT).
-				actor = g_engine->getActorByChunkReference(chunk._id);
-				if (actor == nullptr) {
-					error("%s: Actor for chunk \"%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()));
-				}
-			}
+		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()));
+		}
+		if (chunk.bytesRemaining() > 0) {
 			actor->readChunk(chunk);
 		}
 
@@ -289,32 +191,17 @@ void Context::readHeaderSections(Subfile &subfile, Chunk &chunk) {
 	} while (!subfile.atEnd());
 }
 
-void Context::readActorFromLaterSubfile(Subfile &subfile) {
-	Chunk chunk = subfile.nextChunk();
-	Actor *actor = getActorByChunkReference(chunk._id);
-	if (actor == nullptr) {
-		// We should only need to look in the global scope when there is an
-		// install cache (INSTALL.CXT).
-		actor = g_engine->getActorByChunkReference(chunk._id);
-		if (actor == nullptr) {
-			error("%s: Actor for chunk \"%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()));
-		}
-	}
-	debugC(5, kDebugLoading, "\nContext::readActorFromLaterSubfile(): Got actor with chunk ID %s in later subfile (type: 0x%x) (@0x%llx)", tag2str(chunk._id), actor->type(), static_cast<long long int>(chunk.pos()));
-	actor->readSubfile(subfile, chunk);
-}
-
-void Context::readContextNameData(Chunk &chunk) {
+void MediaStationEngine::readContextNameData(Chunk &chunk) {
 	uint contextId = chunk.readTypedUint16();
-	if (contextId != _id) {
-		warning("%s: Repeated context ID didn't match: %d != %d", __func__, contextId, _id);
+	debugC(5, kDebugLoading, "%s: Context %d", __func__, contextId);
+	Context *context = _loadedContexts.getValOrDefault(contextId);
+	if (context == nullptr) {
+		error("%s: Context %d does not exist or has not been loaded yet in this title", __func__, contextId);
 	}
-
-	_name = chunk.readTypedString();
+	context->_name = chunk.readTypedString();
 }
 
-void Context::readCommandFromStream(ContextSectionType sectionType, Chunk &chunk) {
-	debugC(5, kDebugLoading, "%s: %d", __func__, static_cast<uint>(sectionType));
+void MediaStationEngine::readCommandFromStream(Chunk &chunk, ContextSectionType sectionType) {
 	switch (sectionType) {
 	case kContextCreateData:
 		readCreateContextData(chunk);
@@ -345,7 +232,8 @@ void Context::readCommandFromStream(ContextSectionType sectionType, Chunk &chunk
 		break;
 
 	default:
-		g_engine->readUnrecognizedFromStream(chunk, static_cast<uint>(sectionType));
+		readUnrecognizedFromStream(chunk, static_cast<uint>(sectionType));
+		break;
 	}
 }
 
diff --git a/engines/mediastation/context.h b/engines/mediastation/context.h
index 4a5722ad092..fe56a795508 100644
--- a/engines/mediastation/context.h
+++ b/engines/mediastation/context.h
@@ -38,7 +38,8 @@ enum StreamType {
 };
 
 enum ContextSectionType {
-	kEndOfContextData = 0x00,
+	kContextEndOfSection = 0x00,
+	kContextControlCommands = 0x0d,
 	kContextCreateData = 0x0e,
 	kContextDestroyData = 0x0f,
 	kContextLoadCompleteSection = 0x10,
@@ -50,46 +51,17 @@ enum ContextSectionType {
 	kContextNameData = 0xbb8
 };
 
-class ScreenActor;
-
-class Context : public Datafile {
+class Context {
 public:
-	Context(const Common::Path &path);
 	~Context();
 
-	uint32 _unk1;
-	uint32 _subfileCount;
-	uint32 _fileSize;
-	ScreenActor *_screenActor = nullptr;
-
-	Actor *getActorById(uint actorId);
-	Actor *getActorByChunkReference(uint chunkReference);
-	ScriptValue *getVariable(uint variableId);
+	Common::String _name;
+	Common::HashMap<uint, ScriptValue *> _variables;
 
-private:
 	// This is not an internal file ID, but the number of the file
 	// as it appears in the filename. For instance, the context in
 	// "100.cxt" would have file number 100.
 	uint _id = 0;
-	Common::String _contextName;
-
-	Common::HashMap<uint, Actor *> _actors;
-	Common::HashMap<uint, Actor *> _actorsByChunkReference;
-	Common::HashMap<uint, ScriptValue *> _variables;
-
-	void readHeaderSections(Subfile &subfile, Chunk &chunk);
-
-	void readControlCommands(Chunk &chunk);
-	void readCommandFromStream(ContextSectionType sectionType, Chunk &chunk);
-	void readCreateContextData(Chunk &chunk);
-	void readDestroyContextData(Chunk &chunk);
-	void readCreateActorData(Chunk &chunk);
-	void readDestroyActorData(Chunk &chunk);
-	void readActorLoadComplete(Chunk &chunk);
-	void readCreateVariableData(Chunk &chunk);
-	void readContextNameData(Chunk &chunk);
-
-	void readActorFromLaterSubfile(Subfile &subfile);
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/datafile.cpp b/engines/mediastation/datafile.cpp
index d0cebdd6c73..0d71ae00863 100644
--- a/engines/mediastation/datafile.cpp
+++ b/engines/mediastation/datafile.cpp
@@ -21,6 +21,7 @@
 
 #include "mediastation/datafile.h"
 #include "mediastation/debugchannels.h"
+#include "mediastation/mediastation.h"
 
 namespace MediaStation {
 
@@ -123,8 +124,8 @@ VersionInfo ParameterReadStream::readTypedVersion() {
 	return version;
 }
 
-uint32 ParameterReadStream::readTypedChunkReference() {
-	readAndVerifyType(kDatumTypeChunkReference);
+ChannelIdent ParameterReadStream::readTypedChannelIdent() {
+	readAndVerifyType(kDatumTypeChannelIdent);
 	// This one is always BE.
 	return readUint32BE();
 }
@@ -145,8 +146,6 @@ Chunk::Chunk(Common::SeekableReadStream *stream) : _parentStream(stream) {
 	_dataStartOffset = pos();
 	_dataEndOffset = _dataStartOffset + _length;
 	debugC(5, kDebugLoading, "Chunk::Chunk(): Got chunk with ID \"%s\" and size 0x%x", tag2str(_id), _length);
-	if (_length == 0)
-		error("%s: Encountered a zero-length chunk. This usually indicates corrupted data - maybe a CD-ROM read error.", __func__);
 }
 
 uint32 Chunk::bytesRemaining() {
@@ -211,18 +210,122 @@ Chunk Subfile::nextChunk() {
 }
 
 bool Subfile::atEnd() {
-	// TODO: Is this the best place to put this and approach to use?
-	return _rootChunk.bytesRemaining() == 0;
+	// There might be a padding byte at the end of the subfile, so
+	// we need to check for that.
+	if (_rootChunk.pos() % 2 == 0) {
+		return _rootChunk.bytesRemaining() == 0;
+	} else {
+		return _rootChunk.bytesRemaining() == 1;
+	}
 }
 
-Datafile::Datafile(const Common::Path &path) {
-	if (!open(path)) {
-		error("%s: Failed to open %s", __func__, path.toString().c_str());
+void CdRomStream::openStream(uint streamId) {
+	const StreamInfo &streamInfo = g_engine->streamInfoForIdent(streamId);
+	if (streamInfo._fileId == 0) {
+		error("%s: Stream %d not found in current title", __func__, 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);
+	}
+
+	bool requestedStreamAlreadyOpen = isOpen() && _fileId == streamInfo._fileId;
+	if (requestedStreamAlreadyOpen) {
+		seek(streamInfo._startOffsetInFile);
+	} else {
+		if (isOpen()) {
+			close();
+		}
+
+		Common::Path filename(fileInfo._name);
+		if (!open(filename)) {
+			error("%s: Failed to open %s", __func__, filename.toString().c_str());
+		}
+		seek(streamInfo._startOffsetInFile);
+		_fileId = streamInfo._fileId;
 	}
 }
 
-Subfile Datafile::getNextSubfile() {
+Subfile CdRomStream::getNextSubfile() {
 	return Subfile(_handle);
 }
 
+void ChannelClient::registerWithStreamManager() {
+	g_engine->getStreamFeedManager()->registerChannelClient(this);
+}
+
+void ChannelClient::unregisterWithStreamManager() {
+	g_engine->getStreamFeedManager()->unregisterChannelClient(this);
+}
+
+ImtStreamFeed::ImtStreamFeed(uint actorId) : StreamFeed(actorId) {
+	_stream = new CdRomStream();
+}
+
+ImtStreamFeed::~ImtStreamFeed() {
+	delete _stream;
+	_stream = nullptr;
+}
+
+void ImtStreamFeed::closeFeed() {
+	_stream->closeStream();
+	g_engine->getDocument()->streamDidClose(_id);
+}
+
+void ImtStreamFeed::openFeed(uint streamId, uint startOffset) {
+	// For CXT files, there is a 0x10-byte header before the first stream, but this header
+	// isn't actually used by the engine. This header is not present in BOOT.STM.
+	// [0x00-0x04] Byte order - either II\0\0 for Intel byte order or MM\0\0 for Motorola byte order.
+	//       Motorola byte order never actually seems to be used for data files, even on big-endian
+	//       platforms. Other parts of the engine perform the byte swapping on the fly.
+	// [0x05-0x08] Unknown.
+	// [0x09-0x0c] Stream (subfile) count in this file, uint32le.
+	// [0x0d-0x10] Total file size, uint32le.
+
+	// The original had an intermediary StreamProgress class here, with a StreamProgressClient
+	// class, but there as only
+	g_engine->getDocument()->streamDidOpen(streamId);
+	_stream->openStream(streamId);
+}
+
+void ImtStreamFeed::readData() {
+	Subfile subfile = _stream->getNextSubfile();
+	Chunk chunk = subfile.nextChunk();
+	g_engine->getDocument()->streamWillRead(_id);
+	g_engine->readHeaderSections(subfile, chunk);
+	g_engine->getDocument()->streamDidFinish(_id);
+}
+
+StreamFeedManager::~StreamFeedManager() {
+	for (auto it = _streamFeeds.begin(); it != _streamFeeds.end(); ++it) {
+		delete it->_value;
+	}
+	_streamFeeds.clear();
+}
+
+void StreamFeedManager::closeStreamFeed(StreamFeed *streamFeed) {
+	streamFeed->closeFeed();
+	_streamFeeds.erase(streamFeed->_id);
+	delete streamFeed;
+}
+
+void StreamFeedManager::registerChannelClient(ChannelClient *client) {
+	if (_channelClients.getValOrDefault(client->channelIdent()) != nullptr) {
+		error("%s: Channel ident %d already has a client", __func__, client->channelIdent());
+	}
+	_channelClients.setVal(client->channelIdent(), client);
+}
+
+void StreamFeedManager::unregisterChannelClient(ChannelClient *client) {
+	_channelClients.erase(client->channelIdent());
+}
+
+ImtStreamFeed *StreamFeedManager::openStreamFeed(uint actorId, uint offsetInStream, uint maxBytesToRead) {
+	ImtStreamFeed *streamFeed = new ImtStreamFeed(actorId);
+	streamFeed->openFeed(actorId, offsetInStream);
+	_streamFeeds.setVal(actorId, streamFeed);
+	return streamFeed;
+}
+
 } // End of namespace MediaStation
diff --git a/engines/mediastation/datafile.h b/engines/mediastation/datafile.h
index 87e577437b9..798c6441f45 100644
--- a/engines/mediastation/datafile.h
+++ b/engines/mediastation/datafile.h
@@ -66,7 +66,7 @@ enum DatumType {
 	kDatumTypeTime = 0x11,
 	kDatumTypeString = 0x12,
 	kDatumTypeVersion = 0x13,
-	kDatumTypeChunkReference = 0x1b,
+	kDatumTypeChannelIdent = 0x1b,
 	kDatumTypePolygon = 0x1d,
 };
 
@@ -92,7 +92,7 @@ public:
 	double readTypedTime();
 	Common::String readTypedString();
 	VersionInfo readTypedVersion();
-	uint32 readTypedChunkReference();
+	uint32 readTypedChannelIdent();
 	Polygon readTypedPolygon();
 
 private:
@@ -141,11 +141,86 @@ private:
 	Chunk _rootChunk;
 };
 
-class Datafile : public Common::File {
+// The stream loading class hierarchy presented below is a bit complex for reading directly
+// from streams, like we can do on modern computers, without needing to worry about
+// buffering from CD-ROM. But we are staying close to the original logic and class
+// hierarchy where possible, so some of that original architecture is reflected here.
+typedef uint32 ChannelIdent;
+
+class CdRomStream : public Common::File {
 public:
-	Datafile(const Common::Path &path);
+	CdRomStream() {};
+	void openStream(uint streamId);
+	void closeStream() { close(); }
 
 	Subfile getNextSubfile();
+
+private:
+	uint _fileId = 0;
+};
+
+class ChannelClient {
+public:
+	virtual ~ChannelClient() {};
+
+	void setChannelIdent(ChannelIdent channelIdent) { _channelIdent = channelIdent; }
+	ChannelIdent channelIdent() const { return _channelIdent; }
+
+	virtual void readChunk(Chunk &chunk) {};
+
+	void registerWithStreamManager();
+	void unregisterWithStreamManager();
+
+protected:
+	ChannelIdent _channelIdent = 0;
+};
+
+class StreamFeed {
+public:
+	StreamFeed(uint streamId) : _id(streamId) {};
+	virtual ~StreamFeed() {};
+
+	virtual void openFeed(uint streamId, uint startOffset) = 0;
+
+	// The original also has forceCloseFeed, which doesn't do some other cleanup
+	// that the regular closeFeed does. However, since we are not doing caching and
+	// some other functionality in the original, we don't need this.
+	virtual void closeFeed() = 0;
+	virtual void stopFeed() = 0;
+	virtual void readData() = 0;
+
+	uint _id = 0;
+
+protected:
+	CdRomStream *_stream = nullptr;
+};
+
+class ImtStreamFeed : public StreamFeed {
+public:
+	ImtStreamFeed(uint streamId);
+	~ImtStreamFeed();
+
+	virtual void openFeed(uint streamId, uint startOffset) override;
+	virtual void closeFeed() override;
+	// This implementation is currently empty because all this has to do with read timing.
+	virtual void stopFeed() override {};
+	virtual void readData() override;
+};
+
+class StreamFeedManager {
+public:
+	~StreamFeedManager();
+
+	void registerChannelClient(ChannelClient *client);
+	void unregisterChannelClient(ChannelClient *client);
+	ChannelClient *channelClientForChannel(uint clientId) { return _channelClients.getValOrDefault(clientId); }
+
+	ImtStreamFeed *openStreamFeed(uint streamId, uint offsetInStream = 0, uint maxBytesToRead = 0);
+	void closeStreamFeed(StreamFeed *streamFeed);
+
+private:
+	Common::HashMap<uint, StreamFeed *> _streamFeeds;
+	Common::HashMap<ChannelIdent, ChannelClient *> _channelClients;
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/mediascript/scriptconstants.cpp b/engines/mediastation/mediascript/scriptconstants.cpp
index 70ad334c8d8..d503b8c2ebc 100644
--- a/engines/mediastation/mediascript/scriptconstants.cpp
+++ b/engines/mediastation/mediascript/scriptconstants.cpp
@@ -204,7 +204,7 @@ const char *builtInMethodToStr(BuiltInMethod method) {
 	case kTimeStopMethod:
 		return "TimeStop";
 	case kIsPlayingMethod:
-		return "IsPlaying";
+		return "IsPlaying/SetMultipleStreams";
 	case kSetDissolveFactorMethod:
 		return "SetDissolveFactor";
 	case kMouseActivateMethod:
@@ -280,13 +280,19 @@ const char *builtInMethodToStr(BuiltInMethod method) {
 		return "PanTo";
 	case kClearToPaletteMethod:
 		return "ClearToPalette";
-	case kLoadContextMethod:
+	case kDocumentLoadContextMethod:
 		return "LoadContext";
-	case kReleaseContextMethod:
+	case kDocumentReleaseContextMethod:
 		return "ReleaseContext";
-	case kBranchToScreenMethod:
+	case kDocumentBranchToScreenMethod:
 		return "BranchToScreen";
-	case kIsLoadedMethod:
+	case kDocumentQuitMethod:
+		return "Quit";
+	case kDocumentContextLoadInProgressMethod:
+		return "ContextLoadInProgress";
+	case kDocumentSetMultipleSoundsMethod:
+		return "SetMultipleSounds";
+	case kDocumentContextIsLoadedMethod:
 		return "IsLoaded";
 	case kSetDurationMethod:
 		return "SetDuration";
diff --git a/engines/mediastation/mediascript/scriptconstants.h b/engines/mediastation/mediascript/scriptconstants.h
index e8d6fa40a18..7020287b476 100644
--- a/engines/mediastation/mediascript/scriptconstants.h
+++ b/engines/mediastation/mediascript/scriptconstants.h
@@ -186,10 +186,14 @@ enum BuiltInMethod {
 	kClearToPaletteMethod = 0x17B,
 
 	// DOCUMENT METHODS.
-	kLoadContextMethod = 0x176,
-	kReleaseContextMethod = 0x177,
-	kBranchToScreenMethod = 0xC9,
-	kIsLoadedMethod = 0x178,
+	kDocumentBranchToScreenMethod = 0xC9,
+	kDocumentQuitMethod = 0xD9,
+	kDocumentContextLoadInProgressMethod = 0x169,
+	kDocumentSetMultipleStreamsMethod = 0x174,
+	kDocumentSetMultipleSoundsMethod = 0x175,
+	kDocumentLoadContextMethod = 0x176,
+	kDocumentReleaseContextMethod = 0x177,
+	kDocumentContextIsLoadedMethod = 0x178,
 
 	// PATH METHODS.
 	kSetDurationMethod = 0x106,
diff --git a/engines/mediastation/mediastation.cpp b/engines/mediastation/mediastation.cpp
index 9651f1b817e..28231785eef 100644
--- a/engines/mediastation/mediastation.cpp
+++ b/engines/mediastation/mediastation.cpp
@@ -50,9 +50,21 @@ MediaStationEngine::MediaStationEngine(OSystem *syst, const ADGameDescription *g
 		Common::String directoryGlob = directoryGlobs[i];
 		SearchMan.addSubDirectoryMatching(_gameDataDir, directoryGlob, 0, 5);
 	}
+
+	_channelIdent = MKTAG('i', 'g', 'o', 'd'); // ImtGod
 }
 
 MediaStationEngine::~MediaStationEngine() {
+	for (auto it = _loadedContexts.begin(); it != _loadedContexts.end(); ++it) {
+		destroyContext(it->_value->_id, false);
+	}
+	_loadedContexts.clear();
+
+	// Only delete the document actor.
+	// The root stage is deleted from stage director, and
+	// the other actors are deleted from their contexts.
+	destroyActor(DocumentActor::DOCUMENT_ACTOR_ID);
+
 	delete _displayManager;
 	_displayManager = nullptr;
 
@@ -68,21 +80,18 @@ MediaStationEngine::~MediaStationEngine() {
 	delete _deviceOwner;
 	_deviceOwner = nullptr;
 
-	delete _boot;
-	_boot = nullptr;
-
 	delete _stageDirector;
 	_stageDirector = nullptr;
 
-	for (auto it = _loadedContexts.begin(); it != _loadedContexts.end(); ++it) {
-		delete it->_value;
-	}
-	_loadedContexts.clear();
+	unregisterWithStreamManager();
+	delete _streamFeedManager;
+	_streamFeedManager = nullptr;
 
-	// Only delete the document actor.
-	// The root stage is deleted from stage director, and
-	// the other actors are deleted from their contexts.
-	delete _actors.getVal(1);
+	_contextReferences.clear();
+	_streamMap.clear();
+	_engineResourceDeclarations.clear();
+	_screenReferences.clear();
+	_fileMap.clear();
 	_actors.clear();
 }
 
@@ -90,7 +99,7 @@ Actor *MediaStationEngine::getActorById(uint actorId) {
 	return _actors.getValOrDefault(actorId);
 }
 
-SpatialEntity *MediaStationEngine::getSpatialEntityById(uint spatialEntityId){
+SpatialEntity *MediaStationEngine::getSpatialEntityById(uint spatialEntityId) {
 	Actor *actor = getActorById(spatialEntityId);
 	if (actor != nullptr) {
 		if (!actor->isSpatialActor()) {
@@ -101,19 +110,13 @@ SpatialEntity *MediaStationEngine::getSpatialEntityById(uint spatialEntityId){
 	return nullptr;
 }
 
-Actor *MediaStationEngine::getActorByChunkReference(uint chunkReference) {
-	for (auto it = _loadedContexts.begin(); it != _loadedContexts.end(); ++it) {
-		Actor *actor = it->_value->getActorByChunkReference(chunkReference);
-		if (actor != nullptr) {
-			return actor;
-		}
-	}
-	return nullptr;
+ChannelClient *MediaStationEngine::getChannelClientByChannelIdent(uint channelIdent) {
+	return _streamFeedManager->channelClientForChannel(channelIdent);
 }
 
 ScriptValue *MediaStationEngine::getVariable(uint variableId) {
 	for (auto it = _loadedContexts.begin(); it != _loadedContexts.end(); ++it) {
-		ScriptValue *variable = it->_value->getVariable(variableId);
+		ScriptValue *variable = it->_value->_variables.getValOrDefault(variableId);
 		if (variable != nullptr) {
 			return variable;
 		}
@@ -138,11 +141,7 @@ const char *MediaStationEngine::getAppName() const {
 }
 
 bool MediaStationEngine::isFirstGenerationEngine() {
-	if (_boot == nullptr) {
-		error("%s: Attempted to get engine version before BOOT.STM was read", __func__);
-	} else {
-		return (_boot->_versionInfo.major == 0);
-	}
+	return _versionInfo.major == 0;
 }
 
 void MediaStationEngine::addDirtyRect(const Common::Rect &rect) {
@@ -156,38 +155,25 @@ Common::Error MediaStationEngine::run() {
 	initDocument();
 	initDeviceOwner();
 	initStageDirector();
-
-	Common::Path bootStmFilepath = Common::Path("BOOT.STM");
-	_boot = new Boot(bootStmFilepath);
+	initStreamFeedManager();
+	setupInitialStreamMap();
 
 	if (ConfMan.hasKey("entry_context")) {
 		// For development purposes, we can choose to start at an arbitrary context
 		// in this title. This might not work in all cases.
 		uint entryContextId = ConfMan.get("entry_context").asUint64();
 		warning("%s: Starting at user-requested context %d", __func__, entryContextId);
-		_requestedScreenBranchId = entryContextId;
+		_document->beginTitle(entryContextId);
 	} else {
-		_requestedScreenBranchId = _document->_entryScreenId;
+		_document->beginTitle();
 	}
-	doBranchToScreen();
 
 	while (true) {
 		processEvents();
 		if (shouldQuit()) {
 			break;
 		}
-
-		if (!_requestedContextReleaseId.empty()) {
-			for (uint contextId : _requestedContextReleaseId) {
-				releaseContext(contextId);
-			}
-			_requestedContextReleaseId.clear();
-		}
-
-		if (_requestedScreenBranchId != 0) {
-			doBranchToScreen();
-			_requestedScreenBranchId = 0;
-		}
+		_document->process();
 
 		debugC(5, kDebugGraphics, "***** START SCREEN UPDATE ***");
 		for (auto it = _actors.begin(); it != _actors.end(); ++it) {
@@ -241,6 +227,25 @@ void MediaStationEngine::initStageDirector() {
 	_stageDirector = new StageDirector;
 }
 
+void MediaStationEngine::initStreamFeedManager() {
+	_streamFeedManager = new StreamFeedManager;
+	registerWithStreamManager();
+}
+
+void MediaStationEngine::setupInitialStreamMap() {
+	StreamInfo streamInfo;
+	streamInfo._actorId = 0;
+	streamInfo._fileId = MediaStationEngine::BOOT_STREAM_ID;
+	streamInfo._startOffsetInFile = 0;
+	_streamMap.setVal(streamInfo._fileId, streamInfo);
+
+	const Common::String BOOT_STM_FILENAME("BOOT.STM");
+	FileInfo fileInfo;
+	fileInfo._id = MediaStationEngine::BOOT_STREAM_ID;
+	fileInfo._name = BOOT_STM_FILENAME;
+	_fileMap.setVal(fileInfo._id, fileInfo);
+}
+
 void MediaStationEngine::processEvents() {
 	while (g_system->getEventManager()->pollEvent(_event)) {
 		debugC(9, kDebugEvents, "\n@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
@@ -293,48 +298,6 @@ void MediaStationEngine::draw(bool dirtyOnly) {
 	_displayManager->doTransitionOnSync();
 }
 
-Context *MediaStationEngine::loadContext(uint32 contextId) {
-	if (_boot == nullptr) {
-		error("%s: Cannot load contexts before BOOT.STM is read", __func__);
-	}
-
-	debugC(5, kDebugLoading, "MediaStationEngine::loadContext(): Loading context %d", contextId);
-	if (_loadedContexts.contains(contextId)) {
-		warning("%s: Context %d already loaded, returning existing context", __func__, contextId);
-		return _loadedContexts.getVal(contextId);
-	}
-
-	// Get the file ID.
-	const SubfileDeclaration &subfileDeclaration = _boot->_streamMap.getValOrDefault(contextId);
-	// There are other actors in a subfile too, so we need to make sure we're
-	// referencing the screen actor, at the start of the file.
-	if (subfileDeclaration._startOffsetInFile != 16) {
-		warning("%s: Requested ID wasn't for a context.", __func__);
-		return nullptr;
-	}
-	uint fileId = subfileDeclaration._fileId;
-
-	// Get the filename.
-	const FileDeclaration &fileDeclaration = _boot->_fileMap.getVal(fileId);
-	Common::Path entryCxtFilepath(fileDeclaration._name);
-
-	// Load any child contexts before we actually load this one. The child
-	// contexts must be unloaded explicitly later.
-	ContextDeclaration contextDeclaration = _boot->_contextDeclarations.getVal(contextId);
-	for (uint childContextId : contextDeclaration._parentContextIds) {
-		// The root context is referred to by an ID of 0, regardless of what its
-		// actual ID is. The root context is already always loaded.
-		if (childContextId != 0) {
-			debugC(5, kDebugLoading, "MediaStationEngine::loadContext(): Loading child context %d", childContextId);
-			loadContext(childContextId);
-		}
-	}
-	Context *context = new Context(entryCxtFilepath);
-
-	_loadedContexts.setVal(contextId, context);
-	return context;
-}
-
 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());
@@ -345,54 +308,46 @@ void MediaStationEngine::registerActor(Actor *actorToAdd) {
 void MediaStationEngine::destroyActor(uint actorId) {
 	Actor *actorToDestroy = getActorById(actorId);
 	if (actorToDestroy) {
+		delete _actors[actorId];
 		_actors.erase(actorId);
-		// The actor will actually be deleted when the context is destroyed.
 	} else {
 		warning("%s: Actor %d is not currently loaded", __func__, actorId);
 	}
 }
 
-void MediaStationEngine::scheduleScreenBranch(uint screenId) {
-	_requestedScreenBranchId = screenId;
-}
-
-void MediaStationEngine::scheduleContextRelease(uint contextId) {
-	_requestedContextReleaseId.push_back(contextId);
-}
-
-void MediaStationEngine::doBranchToScreen() {
-	if (_currentContext != nullptr) {
-		_currentContext->_screenActor->runEventHandlerIfExists(kExitEvent);
-		releaseContext(_currentContext->_screenActor->id());
+void MediaStationEngine::destroyContext(uint contextId, bool eraseFromLoadedContexts) {
+	debugC(5, kDebugScript, "%s: Destroying 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);
 	}
 
-	_currentContext = loadContext(_requestedScreenBranchId);
+	getRootStage()->deleteChildrenFromContextId(contextId);
+	destroyActorsInContext(contextId);
+	_functionManager->deleteFunctionsForContext(contextId);
 
-	if (_currentContext->_screenActor != nullptr) {
-		_currentContext->_screenActor->runEventHandlerIfExists(kEntryEvent);
+	delete context;
+	if (eraseFromLoadedContexts) {
+		// If we are deleting all contexts at once, we don't want to actually do this,
+		// as it will mess up our iterators - the whole structure should be cleared after this.
+		_loadedContexts.erase(contextId);
 	}
-
-	_requestedScreenBranchId = 0;
 }
 
-void MediaStationEngine::releaseContext(uint32 contextId) {
-	debugC(5, kDebugScript, "MediaStationEngine::releaseContext(): Releasing context %d", contextId);
-	Context *context = _loadedContexts.getValOrDefault(contextId);
-	if (context == nullptr) {
-		error("%s: Attempted to unload context %d that is not currently loaded", __func__, contextId);
-	}
-
-	// Make sure nothing is still using this context.
+bool MediaStationEngine::contextIsLocked(uint contextId) {
 	for (auto it = _loadedContexts.begin(); it != _loadedContexts.end(); ++it) {
 		uint id = it->_key;
-		ContextDeclaration contextDeclaration = _boot->_contextDeclarations.getVal(id);
-		for (uint32 childContextId : contextDeclaration._parentContextIds) {
+		ContextReference contextReference = _contextReferences.getVal(id);
+		for (uint childContextId : contextReference._parentContextIds) {
 			if (childContextId == contextId) {
-				return;
+				return true;
 			}
 		}
 	}
+	return false;
+}
 
+void MediaStationEngine::destroyActorsInContext(uint contextId) {
 	// Collect actors to remove first, then delete them.
 	// This is necessary because calling erase on a hashmap invalidates
 	// the iterators, so collecting them all first makes more sense.
@@ -406,14 +361,8 @@ void MediaStationEngine::releaseContext(uint32 contextId) {
 
 	// Now remove the collected actors.
 	for (uint actorId : actorsToRemove) {
-		_actors.erase(actorId);
+		destroyActor(actorId);
 	}
-
-	getRootStage()->deleteChildrenFromContextId(contextId);
-	_functionManager->deleteFunctionsForContext(contextId);
-
-	delete context;
-	_loadedContexts.erase(contextId);
 }
 
 void MediaStationEngine::readUnrecognizedFromStream(Chunk &chunk, uint sectionType) {
@@ -426,7 +375,23 @@ void MediaStationEngine::readUnrecognizedFromStream(Chunk &chunk, uint sectionTy
 	}
 
 	if (!paramHandled) {
-		error("%s: Unhandled section type 0x%x", __func__, static_cast<uint>(sectionType));
+		warning("%s: Parameter %d not handled", __func__, sectionType);
+	}
+}
+
+void MediaStationEngine::readChunk(Chunk &chunk) {
+	StreamType streamType = static_cast<StreamType>(chunk.readTypedUint16());
+	switch (streamType) {
+	case kDocumentDefStream:
+		readDocumentDef(chunk);
+		break;
+
+	case kControlCommandsStream:
+		readControlCommands(chunk);
+		break;
+
+	default:
+		error("%s: Unhandled section type 0x%x", __func__, static_cast<uint>(streamType));
 	}
 }
 
diff --git a/engines/mediastation/mediastation.h b/engines/mediastation/mediastation.h
index c32aa0f2eb7..fc8ec74214d 100644
--- a/engines/mediastation/mediastation.h
+++ b/engines/mediastation/mediastation.h
@@ -66,7 +66,11 @@ static const char *const directoryGlobs[] = {
 	nullptr
 };
 
-class MediaStationEngine : public Engine {
+// As this is currently structured, some of the methods in the main engine class are from
+// the RT_ImtGod class in the original, and others are from the RT_App class in the original.
+// In the interest of avoiding more indirection than is already present in the original, we will
+// just keep these together for now.
+class MediaStationEngine : public Engine, public ChannelClient {
 public:
 	MediaStationEngine(OSystem *syst, const ADGameDescription *gameDesc);
 	~MediaStationEngine() override;
@@ -87,22 +91,30 @@ public:
 
 	void registerActor(Actor *actorToAdd);
 	void destroyActor(uint actorId);
+	void destroyContext(uint contextId, bool eraseFromLoadedContexts = true);
+	bool contextIsLocked(uint contextId);
 
-	void scheduleScreenBranch(uint screenId);
-	void scheduleContextRelease(uint contextId);
 	void readUnrecognizedFromStream(Chunk &chunk, uint sectionType);
-	void releaseContext(uint32 contextId);
+	void readHeaderSections(Subfile &subfile, Chunk &chunk);
 
 	Actor *getActorById(uint actorId);
 	SpatialEntity *getSpatialEntityById(uint spatialEntityId);
-	Actor *getActorByChunkReference(uint chunkReference);
+	ChannelClient *getChannelClientByChannelIdent(uint channelIdent);
 	ScriptValue *getVariable(uint variableId);
 	VideoDisplayManager *getDisplayManager() { return _displayManager; }
 	CursorManager *getCursorManager() { return _cursorManager; }
 	FunctionManager *getFunctionManager() { return _functionManager; }
 	RootStage *getRootStage() { return _stageDirector->getRootStage(); }
+	StreamFeedManager *getStreamFeedManager() { return _streamFeedManager; }
+	Document *getDocument() { return _document; }
+
+	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); }
+	const ContextReference &contextRefWithId(uint contextId) { return _contextReferences.getValOrDefault(contextId); }
 
 	Common::Array<ParameterClient *> _parameterClients;
+	Common::HashMap<uint, Context *> _loadedContexts;
 
 	SpatialEntity *getMouseInsideHotspot() { return _mouseInsideHotspot; }
 	void setMouseInsideHotspot(SpatialEntity *entity) { _mouseInsideHotspot = entity; }
@@ -114,10 +126,9 @@ public:
 
 	Common::RandomSource _randomSource;
 
-	Context *_currentContext = nullptr;
-
 	static const uint SCREEN_WIDTH = 640;
 	static const uint SCREEN_HEIGHT = 480;
+	static const uint BOOT_STREAM_ID = 1;
 
 protected:
 	Common::Error run() override;
@@ -133,14 +144,24 @@ private:
 	Document *_document = nullptr;
 	DeviceOwner *_deviceOwner = nullptr;
 	StageDirector *_stageDirector = nullptr;
+	StreamFeedManager *_streamFeedManager = nullptr;
 
-	Boot *_boot = nullptr;
 	Common::HashMap<uint, Actor *> _actors;
-	Common::HashMap<uint, Context *> _loadedContexts;
 	SpatialEntity *_mouseInsideHotspot = nullptr;
 	SpatialEntity *_mouseDownHotspot = nullptr;
-	uint _requestedScreenBranchId = 0;
-	Common::Array<uint> _requestedContextReleaseId;
+
+	Common::String _gameTitle;
+	VersionInfo _versionInfo;
+	Common::String _engineInfo;
+	Common::String _sourceString;
+	Common::HashMap<uint, ContextReference> _contextReferences;
+	Common::HashMap<uint, ScreenReference> _screenReferences;
+	Common::HashMap<uint, FileInfo> _fileMap;
+	Common::HashMap<uint, StreamInfo> _streamMap;
+	Common::HashMap<uint, EngineResourceDeclaration> _engineResourceDeclarations;
+	uint _unk1 = 0;
+	uint _functionTableSize = 0;
+	uint _unk3 = 0;
 
 	void initDisplayManager();
 	void initCursorManager();
@@ -148,9 +169,29 @@ private:
 	void initDocument();
 	void initDeviceOwner();
 	void initStageDirector();
-
-	void doBranchToScreen();
-	Context *loadContext(uint32 contextId);
+	void initStreamFeedManager();
+	void setupInitialStreamMap();
+
+	virtual void readChunk(Chunk &chunk) override;
+	void readDocumentDef(Chunk &chunk);
+	void readDocumentInfoFromStream(Chunk &chunk, BootSectionType sectionType);
+	void readVersionInfoFromStream(Chunk &chunk);
+	void readContextReferencesFromStream(Chunk &chunk);
+	void readScreenReferencesFromStream(Chunk &chunk);
+	void readAndAddFileMaps(Chunk &chunk);
+	void readAndAddStreamMaps(Chunk &chunk);
+
+	void readControlCommands(Chunk &chunk);
+	void readCommandFromStream(Chunk &chunk, ContextSectionType sectionType);
+	void readCreateContextData(Chunk &chunk);
+	void readDestroyContextData(Chunk &chunk);
+	void readCreateActorData(Chunk &chunk);
+	void readDestroyActorData(Chunk &chunk);
+	void readActorLoadComplete(Chunk &chunk);
+	void readCreateVariableData(Chunk &chunk);
+	void readContextNameData(Chunk &chunk);
+
+	void destroyActorsInContext(uint contextId);
 };
 
 extern MediaStationEngine *g_engine;


Commit: 77b896608194f1f072f2e67ecf522143ed64d815
    https://github.com/scummvm/scummvm/commit/77b896608194f1f072f2e67ecf522143ed64d815
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2025-12-31T11:45:08-05:00

Commit Message:
MEDIASTATION: Factor out event processing loop

This is the initial step in a much larger refactor of events that are required
to get event handling more like the original.

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


diff --git a/engines/mediastation/mediastation.cpp b/engines/mediastation/mediastation.cpp
index 28231785eef..6970b0d6d4f 100644
--- a/engines/mediastation/mediastation.cpp
+++ b/engines/mediastation/mediastation.cpp
@@ -168,8 +168,13 @@ Common::Error MediaStationEngine::run() {
 		_document->beginTitle();
 	}
 
+	runEventLoop();
+	return Common::kNoError;
+}
+
+void MediaStationEngine::runEventLoop() {
 	while (true) {
-		processEvents();
+		dispatchSystemEvents();
 		if (shouldQuit()) {
 			break;
 		}
@@ -184,8 +189,6 @@ Common::Error MediaStationEngine::run() {
 
 		g_system->delayMillis(10);
 	}
-
-	return Common::kNoError;
 }
 
 void MediaStationEngine::initDisplayManager() {
@@ -246,10 +249,10 @@ void MediaStationEngine::setupInitialStreamMap() {
 	_fileMap.setVal(fileInfo._id, fileInfo);
 }
 
-void MediaStationEngine::processEvents() {
+void MediaStationEngine::dispatchSystemEvents() {
 	while (g_system->getEventManager()->pollEvent(_event)) {
 		debugC(9, kDebugEvents, "\n@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
-		debugC(9, kDebugEvents, "@@@@   Processing events");
+		debugC(9, kDebugEvents, "@@@@   Dispatching system events");
 		debugC(9, kDebugEvents, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n");
 
 		switch (_event.type) {
diff --git a/engines/mediastation/mediastation.h b/engines/mediastation/mediastation.h
index fc8ec74214d..240f4171dd6 100644
--- a/engines/mediastation/mediastation.h
+++ b/engines/mediastation/mediastation.h
@@ -85,7 +85,7 @@ public:
 	};
 
 	bool isFirstGenerationEngine();
-	void processEvents();
+	void dispatchSystemEvents();
 	void addDirtyRect(const Common::Rect &rect);
 	void draw(bool dirtyOnly = true);
 
@@ -172,6 +172,8 @@ private:
 	void initStreamFeedManager();
 	void setupInitialStreamMap();
 
+	void runEventLoop();
+
 	virtual void readChunk(Chunk &chunk) override;
 	void readDocumentDef(Chunk &chunk);
 	void readDocumentInfoFromStream(Chunk &chunk, BootSectionType sectionType);


Commit: 4f6f8bba30a76c6bbcc035f30b0121fe241d10e5
    https://github.com/scummvm/scummvm/commit/4f6f8bba30a76c6bbcc035f30b0121fe241d10e5
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2025-12-31T11:45:08-05:00

Commit Message:
MEDIASTATION: Correctly reset script function return status

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


diff --git a/engines/mediastation/mediascript/codechunk.cpp b/engines/mediastation/mediascript/codechunk.cpp
index b9c932da97e..f0db162afd1 100644
--- a/engines/mediastation/mediascript/codechunk.cpp
+++ b/engines/mediastation/mediascript/codechunk.cpp
@@ -36,13 +36,22 @@ CodeChunk::CodeChunk(Chunk &chunk) {
 
 ScriptValue CodeChunk::executeNextBlock() {
 	uint blockSize = _bytecode->readTypedUint32();
-	uint startingPos = _bytecode->pos();
+	int64 startingPos = _bytecode->pos();
+	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);
 		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__);
+		}
 	}
 
 	// Verify we consumed the right number of script bytes.
@@ -67,6 +76,7 @@ ScriptValue CodeChunk::execute(Common::Array<ScriptValue> *args) {
 	// Rewind the stream once we're finished, in case we need to execute
 	// this code again!
 	_bytecode->seek(0);
+	_returnImmediately = false;
 	_locals.clear();
 	// We don't own the args, so we will prevent a potentially out-of-scope
 	// variable from being re-accessed.


Commit: 6fa5930d13dbb62da5c435d97eef2e3e3240688b
    https://github.com/scummvm/scummvm/commit/6fa5930d13dbb62da5c435d97eef2e3e3240688b
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2025-12-31T11:45:08-05:00

Commit Message:
MEDIASTATION: Add initial support for camera actors

Changed paths:
  A engines/mediastation/actors/camera.cpp
  A engines/mediastation/actors/camera.h
    engines/mediastation/actor.cpp
    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/sprite.cpp
    engines/mediastation/actors/sprite.h
    engines/mediastation/actors/stage.cpp
    engines/mediastation/actors/stage.h
    engines/mediastation/context.cpp
    engines/mediastation/debugchannels.h
    engines/mediastation/detection.cpp
    engines/mediastation/graphics.cpp
    engines/mediastation/graphics.h
    engines/mediastation/mediascript/scriptconstants.cpp
    engines/mediastation/mediascript/scriptconstants.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 b0cc4c5803b..d97523ad135 100644
--- a/engines/mediastation/actor.cpp
+++ b/engines/mediastation/actor.cpp
@@ -22,6 +22,7 @@
 #include "common/util.h"
 
 #include "mediastation/actor.h"
+#include "mediastation/actors/camera.h"
 #include "mediastation/actors/stage.h"
 #include "mediastation/debugchannels.h"
 #include "mediastation/mediascript/scriptconstants.h"
@@ -129,6 +130,12 @@ void Actor::runEventHandlerIfExists(EventType eventType) {
 	runEventHandlerIfExists(eventType, scriptValue);
 }
 
+SpatialEntity::~SpatialEntity() {
+	if (_parentStage != nullptr) {
+		_parentStage->removeChildSpatialEntity(this);
+	}
+}
+
 ScriptValue SpatialEntity::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
 	ScriptValue returnValue;
 	switch (methodId) {
@@ -265,7 +272,8 @@ ScriptValue SpatialEntity::callMethod(BuiltInMethod methodId, Common::Array<Scri
 void SpatialEntity::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 	switch (paramType) {
 	case kActorHeaderBoundingBox:
-		_boundingBox = chunk.readTypedRect();
+		_originalBoundingBox = chunk.readTypedRect();
+		setAdjustedBounds(kWrapNone);
 		break;
 
 	case kActorHeaderDissolveFactor:
@@ -303,14 +311,16 @@ void SpatialEntity::readParameter(Chunk &chunk, ActorHeaderSectionType paramType
 
 void SpatialEntity::loadIsComplete() {
 	Actor::loadIsComplete();
-	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);
-	pendingParentStage->addChildSpatialEntity(this);
+	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);
+		pendingParentStage->addChildSpatialEntity(this);
+	}
 }
 
 void SpatialEntity::invalidateMouse() {
@@ -332,7 +342,8 @@ void SpatialEntity::moveTo(int16 x, int16 y) {
 	if (isVisible()) {
 		invalidateLocalBounds();
 	}
-	_boundingBox.moveTo(dest);
+	_originalBoundingBox.moveTo(dest);
+	setAdjustedBounds(kWrapNone);
 	if (isVisible()) {
 		invalidateLocalBounds();
 	}
@@ -356,7 +367,8 @@ void SpatialEntity::setBounds(const Common::Rect &bounds) {
 	if (isVisible()) {
 		invalidateLocalBounds();
 	}
-	_boundingBox = bounds;
+	_originalBoundingBox = bounds;
+	setAdjustedBounds(kWrapNone);
 	if (isVisible()) {
 		invalidateLocalBounds();
 	}
@@ -393,11 +405,90 @@ void SpatialEntity::setDissolveFactor(double dissolveFactor) {
 }
 
 void SpatialEntity::invalidateLocalBounds() {
-	g_engine->addDirtyRect(getBbox());
+	if (_parentStage != nullptr) {
+		_parentStage->setAdjustedBounds(kWrapNone);
+		_parentStage->invalidateRect(getBbox());
+	} else {
+		error("%s: No parent stage for entity %d", __func__, _id);
+	}
 }
 
 void SpatialEntity::invalidateLocalZIndex() {
 	warning("STUB: %s", __func__);
 }
 
+void SpatialEntity::setAdjustedBounds(CylindricalWrapMode alignmentMode) {
+	_boundingBox = _originalBoundingBox;
+	if (_parentStage == nullptr) {
+		return;
+	}
+
+	Common::Point offset(0, 0);
+	Common::Point stageExtent = _parentStage->extent();
+	switch (alignmentMode) {
+	case kWrapRight: {
+		offset.x = stageExtent.x;
+		offset.y = 0;
+		break;
+	}
+
+	case kWrapLeft: {
+		offset.x = -stageExtent.x;
+		offset.y = 0;
+		break;
+	}
+
+	case kWrapBottom: {
+		offset.x = 0;
+		offset.y = stageExtent.y;
+		break;
+	}
+
+	case kWrapLeftTop: {
+		offset.x = 0;
+		offset.y = -stageExtent.y;
+		break;
+	}
+
+	case kWrapTop: {
+		offset.x = stageExtent.x;
+		offset.y = stageExtent.y;
+		break;
+	}
+
+	case kWrapRightBottom: {
+		offset.x = -stageExtent.x;
+		offset.y = -stageExtent.y;
+		break;
+	}
+
+	case kWrapRightTop: {
+		offset.x = -stageExtent.x;
+		offset.y = stageExtent.y;
+		break;
+	}
+
+	case kWrapLeftBottom: {
+		offset.x = stageExtent.x;
+		offset.y = -stageExtent.y;
+		break;
+	}
+
+	case kWrapNone:
+	default:
+		// No offset adjustment.
+		break;
+	}
+
+	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);
+	}
+
+	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);
+	}
+}
+
 } // End of namespace MediaStation
diff --git a/engines/mediastation/actor.h b/engines/mediastation/actor.h
index e2e13c00160..9898ae076e7 100644
--- a/engines/mediastation/actor.h
+++ b/engines/mediastation/actor.h
@@ -32,6 +32,10 @@
 
 namespace MediaStation {
 
+class DisplayContext;
+class SpatialEntity;
+class StageActor;
+
 enum ActorType {
 	kActorTypeEmpty = 0x0000,
 	kActorTypeScreen = 0x0001, // SCR
@@ -53,6 +57,7 @@ enum ActorType {
 	kActorTypeText = 0x001a, // TXT
 	kActorTypeFont = 0x001b, // FON
 	kActorTypeCamera = 0x001c, // CAM
+	kActorTypeDiskImageActor = 0x001d,
 	kActorTypeCanvas = 0x001e, // CVS
 	kActorTypeXsnd = 0x001f,
 	kActorTypeXsndMidi = 0x0020,
@@ -100,11 +105,18 @@ enum ActorHeaderSectionType {
 	kActorHeaderDuration = 0x0612,
 
 	// CAMERA FIELDS.
-	kActorHeaderViewportOrigin = 0x076f,
-	kActorHeaderLensOpen = 0x0770,
+	kActorHeaderCameraViewportOrigin = 0x076f,
+	kActorHeaderCameraLensOpen = 0x0770,
+	kActorHeaderCameraImageActor = 0x77b,
+
+	// CANVAS FIELDS.
+	kActorHeaderCanvasUnk1 = 0x491,
+	kActorHeaderCanvasDissolveFactor = 0x493,
+	kActorHeaderCanvasUnk2 = 0x494,
+	kActorHeaderCanvasUnk3 = 0x495,
 
 	// STAGE FIELDS.
-	kActorHeaderStageSize = 0x0771,
+	kActorHeaderStageExtent = 0x0771,
 	kActorHeaderCylindricalX = 0x0772,
 	kActorHeaderCylindricalY = 0x0773,
 
@@ -124,7 +136,7 @@ enum ActorHeaderSectionType {
 	kActorHeaderCurrentSpriteClip = 0x03ea
 };
 
-class SpatialEntity;
+enum CylindricalWrapMode : int;
 
 struct MouseActorState {
 	SpatialEntity *keyDown = nullptr;
@@ -150,8 +162,6 @@ enum MouseEventFlag {
 	// There is no key up event.
 };
 
-class StageActor;
-
 class Actor {
 public:
 	Actor(ActorType type) : _type(type) {};
@@ -194,11 +204,15 @@ protected:
 class SpatialEntity : public Actor {
 public:
 	SpatialEntity(ActorType type) : Actor(type) {};
+	~SpatialEntity();
 
-	virtual void draw(const Common::Array<Common::Rect> &dirtyRegion) { return; }
+	virtual void draw(DisplayContext &displayContext) { return; }
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 	virtual void readParameter(Chunk &chunk, ActorHeaderSectionType paramType) override;
 	virtual void loadIsComplete() override;
+	virtual void preload(const Common::Rect &rect) {};
+	virtual bool isRectInMemory(const Common::Rect &rect) { return true; }
+	virtual bool isLoading() { return false; }
 
 	virtual bool isSpatialActor() const override { return true; }
 	virtual bool isVisible() const { return _isVisible; }
@@ -231,6 +245,7 @@ public:
 	StageActor *getParentStage() const { return _parentStage; }
 
 	virtual void invalidateLocalBounds();
+	virtual void setAdjustedBounds(CylindricalWrapMode alignmentMode);
 
 protected:
 	uint _stageId = 0;
@@ -239,6 +254,7 @@ protected:
 	double _scaleX = 0.0;
 	double _scaleY = 0.0;
 	Common::Rect _boundingBox;
+	Common::Rect _originalBoundingBox;
 	bool _isVisible = false;
 	bool _hasTransparency = false;
 	bool _getOffstageEvents = false;
diff --git a/engines/mediastation/actors/camera.cpp b/engines/mediastation/actors/camera.cpp
new file mode 100644
index 00000000000..ab61816ec6b
--- /dev/null
+++ b/engines/mediastation/actors/camera.cpp
@@ -0,0 +1,637 @@
+/* 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/camera.h"
+#include "mediastation/actors/stage.h"
+#include "mediastation/actors/image.h"
+#include "mediastation/debugchannels.h"
+#include "mediastation/mediastation.h"
+
+#include "common/util.h"
+
+namespace MediaStation {
+
+CameraActor::~CameraActor() {
+	if (_parentStage != nullptr) {
+		_parentStage->removeCamera(this);
+		_parentStage->removeChildSpatialEntity(this);
+	}
+}
+
+void CameraActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
+	switch (paramType) {
+	case kActorHeaderChannelIdent:
+		_channelIdent = chunk.readTypedChannelIdent();
+		registerWithStreamManager();
+		_image = Common::SharedPtr<ImageAsset>(new ImageAsset);
+		break;
+
+	case kActorHeaderStartup:
+		_isVisible = static_cast<bool>(chunk.readTypedByte());
+		break;
+
+	case kActorHeaderX:
+		_offset.x = chunk.readTypedUint16();
+		break;
+
+	case kActorHeaderY:
+		_offset.y = chunk.readTypedUint16();
+		break;
+
+	case kActorHeaderCameraViewportOrigin: {
+		Common::Point origin = chunk.readTypedPoint();
+		setViewportOrigin(origin);
+		break;
+	}
+
+	case kActorHeaderCameraLensOpen:
+		_lensOpen = static_cast<bool>(chunk.readTypedByte());
+		break;
+
+	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;
+		break;
+	}
+
+	default:
+		SpatialEntity::readParameter(chunk, paramType);
+	}
+}
+
+void CameraActor::readChunk(Chunk &chunk) {
+	BitmapHeader *bitmapHeader = new BitmapHeader(chunk);
+	_image->bitmap = new Bitmap(chunk, bitmapHeader);
+}
+
+ScriptValue CameraActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
+	ScriptValue returnValue;
+	switch (methodId) {
+	case kSpatialMoveToMethod:
+	case kSpatialMoveToByOffsetMethod:
+	case kSpatialCenterMoveToMethod:
+		invalidateLocalBounds();
+		returnValue = SpatialEntity::callMethod(methodId, args);
+		invalidateLocalBounds();
+		break;
+
+	case kAddToStageMethod:
+		assert(args.empty());
+		addToStage();
+		break;
+
+	case kRemoveFromStageMethod: {
+		bool stopPan = false;
+		if (args.size() >= 1) {
+			stopPan = args[0].asBool();
+		}
+		removeFromStage(stopPan);
+		break;
+	}
+
+	case kAddedToStageMethod:
+		assert(args.empty());
+		returnValue.setToBool(_addedToStage);
+		break;
+
+	case kStartPanMethod: {
+		assert(args.size() == 3);
+		int16 deltaX = static_cast<uint16>(args[0].asFloat());
+		int16 deltaY = static_cast<int16>(args[1].asFloat());
+		double duration = args[2].asTime();
+		_nextViewportOrigin = Common::Point(deltaX, deltaY) + _currentViewportOrigin;
+		adjustCameraViewport(_nextViewportOrigin);
+		startPan(deltaX, deltaY, duration);
+		break;
+	}
+
+	case kStopPanMethod:
+		assert(args.empty());
+		stopPan();
+		break;
+
+	case kIsPanningMethod:
+		assert(args.empty());
+		returnValue.setToBool(_panState);
+		break;
+
+	case kViewportMoveToMethod: {
+		assert(args.size() == 2);
+		int16 x = static_cast<int16>(args[0].asFloat());
+		int16 y = static_cast<int16>(args[1].asFloat());
+		_nextViewportOrigin = Common::Point(x, y);
+		if (!_addedToStage) {
+			_currentViewportOrigin = _nextViewportOrigin;
+		} else {
+			bool viewportMovedSuccessfully = processViewportMove();
+			if (!viewportMovedSuccessfully) {
+				startPan(0, 0, 0.0);
+			}
+		}
+		break;
+	}
+
+	case kAdjustCameraViewportMethod: {
+		assert(args.size() == 2);
+		int16 xDiff = static_cast<int16>(args[0].asFloat());
+		int16 yDiff = static_cast<int16>(args[1].asFloat());
+		Common::Point viewportDelta(xDiff, yDiff);
+		_nextViewportOrigin = getViewportOrigin() + viewportDelta;
+		adjustCameraViewport(_nextViewportOrigin);
+		if (!_addedToStage) {
+			_currentViewportOrigin = _nextViewportOrigin;
+		} else {
+			bool viewportMovedSuccessfully = processViewportMove();
+			if (!viewportMovedSuccessfully) {
+				startPan(0, 0, 0.0);
+			}
+		}
+		break;
+	}
+
+	case kAdjustCameraViewportSpatialCenterMethod: {
+		assert(args.size() == 2);
+		int16 xDiff = static_cast<int16>(args[0].asFloat());
+		int16 yDiff = 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;
+		adjustCameraViewport(_nextViewportOrigin);
+		if (!_addedToStage) {
+			_currentViewportOrigin = _nextViewportOrigin;
+		} else {
+			bool viewportMovedSuccessfully = processViewportMove();
+			if (!viewportMovedSuccessfully) {
+				startPan(0, 0, 0.0);
+			}
+		}
+		break;
+	}
+
+	case kSetCameraBoundsMethod: {
+		assert(args.size() == 2);
+		int16 width = static_cast<int16>(args[0].asFloat());
+		int16 height = static_cast<int16>(args[1].asFloat());
+		Common::Rect newBounds(_originalBoundingBox.origin(), width, height);
+
+		// invalidateLocalBounds is already called in the setBounds call, but these extra calls are
+		// in the original, so they are kept.
+		invalidateLocalBounds();
+		setBounds(newBounds);
+		invalidateLocalBounds();
+		break;
+	}
+
+	case kXViewportPositionMethod:
+		assert(args.size() == 0);
+		returnValue.setToFloat(getViewportOrigin().x);
+		break;
+
+	case kYViewportPositionMethod:
+		assert(args.size() == 0);
+		returnValue.setToFloat(getViewportOrigin().y);
+		break;
+
+	case kPanToMethod: {
+		assert(args.size() >= 3);
+		int16 x = static_cast<int16>(args[0].asFloat());
+		int16 y = static_cast<int16>(args[1].asFloat());
+
+		if (args.size() == 4) {
+			uint panSteps = static_cast<uint>(args[2].asFloat());
+			double duration = args[3].asFloat();
+			panToByStepCount(x, y, panSteps, duration);
+		} else if (args.size() == 3) {
+			double duration = args[2].asFloat();
+			panToByTime(x, y, duration);
+		} else {
+			error("%s: Incorrect number of args for method %s", __func__, builtInMethodToStr(methodId));
+		}
+		break;
+	}
+
+	default:
+		returnValue = SpatialEntity::callMethod(methodId, args);
+		break;
+	}
+	return returnValue;
+}
+
+void CameraActor::invalidateLocalBounds() {
+	if (_parentStage != nullptr) {
+		_parentStage->invalidateLocalBounds();
+	}
+}
+
+void CameraActor::loadIsComplete() {
+	SpatialEntity::loadIsComplete();
+	if (_lensOpen) {
+		addToStage();
+	}
+
+	if (_image != nullptr) {
+		warning("%s: STUB: Camera image asset not handled yet", __func__);
+	}
+}
+
+void CameraActor::addToStage() {
+	if (_parentStage != nullptr) {
+		_parentStage->addCamera(this);
+		invalidateLocalBounds();
+	}
+}
+
+void CameraActor::removeFromStage(bool shouldStopPan) {
+	if (_parentStage != nullptr) {
+		_parentStage->removeCamera(this);
+		invalidateLocalBounds();
+		_addedToStage = false;
+		if (shouldStopPan && _panState != kCameraNotPanning) {
+			stopPan();
+		}
+	}
+}
+
+void CameraActor::setViewportOrigin(const Common::Point &newViewpointOrigin) {
+	_currentViewportOrigin = newViewpointOrigin;
+}
+
+Common::Point CameraActor::getViewportOrigin() {
+	return _currentViewportOrigin;
+}
+
+Common::Rect CameraActor::getViewportBounds() {
+	Common::Rect viewportBounds(getBbox());
+	viewportBounds.moveTo(_currentViewportOrigin);
+	return viewportBounds;
+}
+
+void CameraActor::drawUsingCamera(DisplayContext &displayContext, const Common::Array<SpatialEntity *> &entitiesToDraw) {
+	Clip *currentClip = displayContext.currentClip();
+	if (currentClip != nullptr) {
+		Clip *previousClip = displayContext.previousClip();
+		if (previousClip == nullptr) {
+			currentClip->addToRegion(currentClip->_bounds);
+		} else {
+			*currentClip = *previousClip;
+		}
+	}
+
+	Common::Rect cameraBounds = getBbox();
+	displayContext.intersectClipWith(cameraBounds);
+	displayContext.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;
+
+	if (_image != nullptr) {
+		// TODO: Handle image asset stuff.
+		warning("%s: Camera image asset not handled yet", __func__);
+	}
+
+	for (SpatialEntity *entityToDraw : entitiesToDraw) {
+		if (entityToDraw->isVisible()) {
+			drawObject(displayContext, displayContext, entityToDraw);
+		}
+	}
+
+	displayContext.popOrigin();
+	displayContext.emptyCurrentClip();
+}
+
+void CameraActor::drawObject(DisplayContext &sourceContext, DisplayContext &destContext, SpatialEntity *objectToDraw) {
+	if (_parentStage == nullptr) {
+		warning("%s: No parent stage", __func__);
+		return;
+	}
+
+	objectToDraw->setAdjustedBounds(kWrapNone);
+	Common::Rect visibleBounds = objectToDraw->getBbox();
+	if (sourceContext.rectIsInClip(visibleBounds)) {
+		objectToDraw->draw(destContext);
+	}
+
+	if (_parentStage->cylindricalX()) {
+		warning("%s: CylindricalX not handled yet", __func__);
+	}
+
+	if (_parentStage->cylindricalY()) {
+		warning("%s: CylindricalY not handled yet", __func__);
+	}
+	objectToDraw->setAdjustedBounds(kWrapNone);
+}
+
+void CameraActor::setXYDelta(uint xDelta, uint yDelta) {
+	_panDelta.x = xDelta;
+	_panDelta.y = yDelta;
+	debugC(6, kDebugCamera, "%s: (%d, %d)", __func__, _panDelta.x, _panDelta.y);
+}
+
+void CameraActor::setXYDelta() {
+	// If we have no parameters for setting the delta,
+	// just set the delta to 1 in whatever direction we are going.
+	if (_panStart.x < _panDest.x) {
+		_panDelta.x = 1;
+	} else if (_panDest.x < _panStart.x) {
+		_panDelta.x = -1;
+	}
+
+	if (_panStart.y < _panDest.y) {
+		_panDelta.y = 1;
+	} else if (_panDest.y < _panStart.y) {
+		_panDelta.y = -1;
+	}
+	debugC(6, kDebugCamera, "%s: (%d, %d)", __func__, _panDelta.x, _panDelta.y);
+}
+
+void CameraActor::panToByTime(int16 x, int16 y, double duration) {
+	_panState = kCameraPanToByTime;
+	_panStart = _currentViewportOrigin;
+	_panDest = Common::Point(x, y);
+	_panDuration = 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);
+	setXYDelta();
+	calcNewViewportOrigin();
+}
+
+void CameraActor::panToByStepCount(int16 x, int16 y, uint panSteps, double duration) {
+	_panState = kCameraPanByStepCount;
+	_panStart = _currentViewportOrigin;
+	_panDest = Common::Point(x, y);
+	_panDuration = duration;
+	_currentPanStep = 1;
+	_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);
+	setXYDelta();
+	calcNewViewportOrigin();
+}
+
+void CameraActor::startPan(uint xOffset, uint yOffset, double duration) {
+	_panState = kCameraPanningStarted;
+	_panDuration = duration;
+	_startTime = g_system->getMillis();
+	_nextPanStepTime = 0;
+	_currentPanStep = 0;
+	_maxPanStep = 0;
+	setXYDelta(xOffset, yOffset);
+	debugC(6, kDebugCamera, "%s: xOffset: %u, yOffset: %u, duration: %f", __func__, xOffset, yOffset, duration);
+}
+
+void CameraActor::stopPan() {
+	_panState = kCameraNotPanning;
+	_panDuration = 0.0;
+	_startTime = 0;
+	_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);
+}
+
+bool CameraActor::continuePan() {
+	bool panShouldContinue = true;
+	if (_panState == kCameraPanningStarted) {
+		if (_panDelta == Common::Point(0, 0)) {
+			panShouldContinue = false;
+		}
+	} else {
+		if (percentComplete() >= 1.0) {
+			panShouldContinue = false;
+		}
+	}
+	debugC(6, kDebugCamera, "%s: %s", __func__, panShouldContinue ? "true": "false");
+	return panShouldContinue;
+}
+
+void CameraActor::process() {
+	// Only process panning if we're actively panning.
+	if (_panState == kCameraNotPanning) {
+		return;
+	}
+
+	// Check if it's time for the next pan step.
+	uint currentTime = g_system->getMillis() - _startTime;
+	if (currentTime < _nextPanStepTime) {
+		return;
+	}
+
+	debugC(7, kDebugCamera, "*** START PAN STEP ***");
+	timerEvent();
+	debugC(7, kDebugCamera, "*** END PAN STEP ***");
+}
+
+void CameraActor::timerEvent() {
+	if (_parentStage != nullptr) {
+		if (processViewportMove()) {
+			processNextPanStep();
+			if (continuePan()) {
+				if (cameraWithinStage(_nextViewportOrigin)) {
+					adjustCameraViewport(_nextViewportOrigin);
+
+					// The original had logic to pre-load the items that were going to be scrolled
+					// into view next, but since we load actors more all-at-once, we don't actually need this.
+					// The calls that would be made are kept commented out.
+					// Common::Rect advanceRect = getAdvanceRect();
+					// _parentStage->preload(advanceRect);
+				} else {
+					runEventHandlerIfExists(kCameraPanAbortEvent);
+					stopPan();
+				}
+			} else {
+				bool success = true;
+				if (_panState == kCameraPanToByTime) {
+					_nextViewportOrigin = _panDest;
+					adjustCameraViewport(_nextViewportOrigin);
+					success = processViewportMove();
+				}
+				if (success) {
+					runEventHandlerIfExists(kCameraPanEndEvent);
+					stopPan();
+				} else {
+					Common::Rect currentBounds = getBbox();
+					Common::Rect preloadBounds(_nextViewportOrigin, currentBounds.width(), currentBounds.height());
+					_parentStage->preload(preloadBounds);
+				}
+			}
+		} else {
+			Common::Rect currentBounds = getBbox();
+			Common::Rect preloadBounds(_nextViewportOrigin, currentBounds.width(), currentBounds.height());
+			_parentStage->preload(preloadBounds);
+		}
+	}
+}
+
+bool CameraActor::processViewportMove() {
+	bool isRectInMemory = true;
+	if (_parentStage != nullptr) {
+		Common::Rect boundsInViewport = getBbox();
+		boundsInViewport.moveTo(_nextViewportOrigin);
+		_parentStage->setCurrentCamera(this);
+		isRectInMemory = _parentStage->isRectInMemory(boundsInViewport);
+		if (isRectInMemory) {
+			invalidateLocalBounds();
+			setViewportOrigin(_nextViewportOrigin);
+			invalidateLocalBounds();
+		}
+	}
+	return isRectInMemory;
+}
+
+void CameraActor::processNextPanStep() {
+	// If pan type includes per-step updates (4-arg pan in original engine),
+	// advance the pan step counter. Then compute the new viewport origin
+	// and notify any script handlers registered for the pan-step event.
+	if (_panState == kCameraPanByStepCount) {
+		_currentPanStep += 1;
+	}
+
+	calcNewViewportOrigin();
+	runEventHandlerIfExists(kCameraPanStepEvent);
+
+	uint stepDurationInMilliseconds = 20; // Visually smooth.
+	_nextPanStepTime += stepDurationInMilliseconds;
+}
+
+void CameraActor::adjustCameraViewport(Common::Point &viewportToAdjust) {
+	if (_parentStage == nullptr) {
+		warning("%s: No parent stage", __func__);
+		return;
+	}
+
+	if (_parentStage->cylindricalX()) {
+		warning("%s: CylindricalX not handled yet", __func__);
+	}
+
+	if (_parentStage->cylindricalY()) {
+		warning("%s: CylindricalY not handled yet", __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);
+	} else {
+		// Interpolate from the start to the dest based on percent complete.
+		double progress = percentComplete();
+		double startX = static_cast<double>(_panStart.x);
+		double endX = static_cast<double>(_panDest.x);
+		double interpolatedX = startX + (endX - startX) * progress + 0.5;
+		_nextViewportOrigin.x = static_cast<int16>(interpolatedX);
+
+		double startY = static_cast<double>(_panStart.y);
+		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);
+	}
+}
+
+bool CameraActor::cameraWithinStage(const Common::Point &candidate) {
+	if (_parentStage == nullptr) {
+		return true;
+	}
+
+	bool result = true;
+	// We can only be out of horizontal bounds if we have a requested delta and
+	// are not doing X axis wrapping.
+	bool canBeOutOfHorizontalBounds = !_parentStage->cylindricalX() && _panDelta.x != 0;
+	if (canBeOutOfHorizontalBounds) {
+		int16 candidateRightBoundary = getBbox().width() + candidate.x;
+		bool cameraPastRightBoundary = _parentStage->extent().x < candidateRightBoundary;
+		if (cameraPastRightBoundary) {
+			result = false;
+		} else if (candidate.x < 0) {
+			result = false;
+		}
+		debugC(6, kDebugCamera, "%s: %s [rightBoundary: %d, extent: %d]", __func__, result ? "true" : "false", candidateRightBoundary, _parentStage->extent().x);
+	}
+
+	// We can only be out of vertical bounds if we have a requested delta and
+	// are not doing Y axis wrapping.
+	bool canBeOutOfVerticalBounds = !_parentStage->cylindricalY() && _panDelta.y != 0;
+	if (canBeOutOfVerticalBounds) {
+		int16 candidateBottomBoundary = getBbox().height() + candidate.y;
+		bool cameraPastBottomBoundary = _parentStage->extent().y < candidateBottomBoundary;
+		if (cameraPastBottomBoundary) {
+			result = false;
+		} else if (candidate.y < 0) {
+			result = false;
+		}
+		debugC(6, kDebugCamera, "%s: %s [bottomBoundary: %d, extent: %d]", __func__, result ? "true" : "false", candidateBottomBoundary, _parentStage->extent().y);
+	}
+	return result;
+}
+
+double CameraActor::percentComplete() {
+	double percentValue = 0.0;
+	switch (_panState) {
+	case kCameraPanByStepCount: {
+		percentValue = static_cast<double>(_maxPanStep - _currentPanStep) / static_cast<double>(_maxPanStep);
+		percentValue = 1.0 - percentValue;
+		break;
+	}
+
+	case kCameraPanToByTime: {
+		const double MILLISECONDS_IN_ONE_SECOND = 1000.0;
+		uint currentRuntime = g_system->getMillis();
+		uint elapsedTime = currentRuntime - _startTime;
+		double elapsedSeconds = elapsedTime / MILLISECONDS_IN_ONE_SECOND;
+		percentValue = elapsedSeconds / _panDuration;
+		break;
+	}
+
+	default:
+		percentValue = 0.0;
+		break;
+	}
+
+	percentValue = CLIP<double>(percentValue, 0.0, 1.0);
+	return percentValue;
+}
+
+} // End of namespace MediaStation
diff --git a/engines/mediastation/actors/camera.h b/engines/mediastation/actors/camera.h
new file mode 100644
index 00000000000..c093c6f8f56
--- /dev/null
+++ b/engines/mediastation/actors/camera.h
@@ -0,0 +1,103 @@
+/* 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_CAMERA_H
+#define MEDIASTATION_CAMERA_H
+
+#include "mediastation/actor.h"
+#include "mediastation/graphics.h"
+#include "mediastation/mediascript/scriptvalue.h"
+#include "mediastation/mediascript/scriptconstants.h"
+
+namespace MediaStation {
+
+enum CameraPanState {
+	kCameraNotPanning = 0,
+	kCameraPanningStarted = 1,
+	// We pan for a certain total amount of time.
+	kCameraPanToByTime = 2,
+	// We pan for a certain number of steps, waiting a given time between each step.
+	kCameraPanByStepCount = 3
+};
+
+struct ImageAsset;
+
+// A Camera's main purpose is panning around a stage that is too large to fit on screen all at once.
+class CameraActor : public SpatialEntity, public ChannelClient {
+public:
+	CameraActor() : SpatialEntity(kActorTypeCamera) {};
+	~CameraActor();
+
+	virtual void readParameter(Chunk &chunk, ActorHeaderSectionType paramType) override;
+	virtual void readChunk(Chunk &chunk) override;
+	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
+	virtual void loadIsComplete() override;
+	virtual void process() override;
+
+	Common::Point getViewportOrigin();
+	Common::Rect getViewportBounds();
+	virtual void invalidateLocalBounds() override;
+
+	void drawUsingCamera(DisplayContext &displayContext, const Common::Array<SpatialEntity *> &entitiesToDraw);
+
+private:
+	bool _lensOpen = false;
+	bool _addedToStage = false;
+	double _panDuration = 0.0;
+	uint _currentPanStep = 0;
+	uint _maxPanStep = 0;
+	uint _startTime = 0;
+	uint _nextPanStepTime = 0;
+	CameraPanState _panState = kCameraNotPanning;
+	Common::Point _offset;
+	Common::Point _currentViewportOrigin;
+	Common::Point _nextViewportOrigin;
+	Common::Point _panStart;
+	Common::Point _panDest;
+	Common::Point _panDelta;
+	Common::SharedPtr<ImageAsset> _image;
+	DisplayContext _displayContext;
+
+	void addToStage();
+	void removeFromStage(bool stopPan);
+	void setViewportOrigin(const Common::Point &newViewportOrigin);
+	void drawObject(DisplayContext &sourceContext, DisplayContext &destContext, SpatialEntity *objectToDraw);
+	void setXYDelta(uint xDelta, uint yDelta);
+	void setXYDelta();
+	bool cameraWithinStage(const Common::Point &candidate);
+
+	void panToByTime(int16 x, int16 y, double duration);
+	void panToByStepCount(int16 x, int16 y, uint maxPanStep, double duration);
+	void startPan(uint xOffset, uint yOffset, double duration);
+	void stopPan();
+	bool continuePan();
+
+	void timerEvent();
+	bool processViewportMove();
+	void processNextPanStep();
+	void adjustCameraViewport(Common::Point &viewportToAdjust);
+	void calcNewViewportOrigin();
+	double percentComplete();
+};
+
+} // End of namespace MediaStation
+
+#endif
diff --git a/engines/mediastation/actors/image.cpp b/engines/mediastation/actors/image.cpp
index 6ac0a3ec2fc..62b11720129 100644
--- a/engines/mediastation/actors/image.cpp
+++ b/engines/mediastation/actors/image.cpp
@@ -97,17 +97,13 @@ ScriptValue ImageActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptV
 	}
 }
 
-void ImageActor::draw(const Common::Array<Common::Rect> &dirtyRegion) {
+void ImageActor::draw(DisplayContext &displayContext) {
 	if (_isVisible) {
 		Common::Point origin = getBbox().origin();
-		g_engine->getDisplayManager()->imageBlit(origin, _asset->bitmap, _dissolveFactor, dirtyRegion);
+		g_engine->getDisplayManager()->imageBlit(origin, _asset->bitmap, _dissolveFactor, &displayContext);
 	}
 }
 
-void ImageActor::invalidateLocalBounds() {
-	g_engine->addDirtyRect(getBbox());
-}
-
 void ImageActor::spatialShow() {
 	_isVisible = true;
 	invalidateLocalBounds();
diff --git a/engines/mediastation/actors/image.h b/engines/mediastation/actors/image.h
index b28cf0d15f4..7fa57a9b1e5 100644
--- a/engines/mediastation/actors/image.h
+++ b/engines/mediastation/actors/image.h
@@ -48,8 +48,7 @@ public:
 	virtual void readChunk(Chunk &chunk) override;
 	virtual void readParameter(Chunk &chunk, ActorHeaderSectionType paramType) override;
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
-	virtual void draw(const Common::Array<Common::Rect> &dirtyRegion) override;
-	virtual void invalidateLocalBounds() override;
+	virtual void draw(DisplayContext &displayContext) override;
 	virtual Common::Rect getBbox() const override;
 
 private:
diff --git a/engines/mediastation/actors/movie.cpp b/engines/mediastation/actors/movie.cpp
index d1b041cd807..b29b3918cdd 100644
--- a/engines/mediastation/actors/movie.cpp
+++ b/engines/mediastation/actors/movie.cpp
@@ -319,24 +319,28 @@ void StreamMovieActor::updateFrameState() {
 	}
 }
 
-void StreamMovieActor::draw(const Common::Array<Common::Rect> &dirtyRegion) {
+void StreamMovieActor::draw(DisplayContext &displayContext) {
 	for (MovieFrame *frame : _framesOnScreen) {
 		Common::Rect bbox = getFrameBoundingBox(frame);
 
 		switch (frame->blitType) {
 		case kUncompressedMovieBlit:
-			g_engine->getDisplayManager()->imageBlit(bbox.origin(), frame->image, _dissolveFactor, dirtyRegion);
+			g_engine->getDisplayManager()->imageBlit(bbox.origin(), frame->image, _dissolveFactor, &displayContext);
 			break;
 
 		case kUncompressedDeltaMovieBlit:
-			g_engine->getDisplayManager()->imageDeltaBlit(bbox.origin(), frame->diffBetweenKeyframeAndFrame, frame->image, frame->keyframeImage, _dissolveFactor, dirtyRegion);
+			g_engine->getDisplayManager()->imageDeltaBlit(
+				bbox.origin(), frame->diffBetweenKeyframeAndFrame,
+				frame->image, frame->keyframeImage, _dissolveFactor, &displayContext);
 			break;
 
 		case kCompressedDeltaMovieBlit:
 			if (frame->keyframeImage->isCompressed()) {
 				decompressIntoAuxImage(frame);
 			}
-			g_engine->getDisplayManager()->imageDeltaBlit(bbox.origin(), frame->diffBetweenKeyframeAndFrame, frame->image, frame->keyframeImage, _dissolveFactor, dirtyRegion);
+			g_engine->getDisplayManager()->imageDeltaBlit(
+				bbox.origin(), frame->diffBetweenKeyframeAndFrame,
+				frame->image, frame->keyframeImage, _dissolveFactor, &displayContext);
 			break;
 
 		default:
@@ -346,6 +350,8 @@ void StreamMovieActor::draw(const Common::Array<Common::Rect> &dirtyRegion) {
 }
 
 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()
 	Common::Point origin = _boundingBox.origin() + frame->leftTop;
 	Common::Rect bbox = Common::Rect(origin, frame->image->width(), frame->image->height());
 	return bbox;
@@ -440,17 +446,14 @@ void StreamMovieActor::parseMovieChunkMarker(Chunk &chunk) {
 }
 
 void StreamMovieActor::invalidateRect(const Common::Rect &rect) {
-	g_engine->addDirtyRect(rect);
+	invalidateLocalBounds();
 }
 
 void StreamMovieActor::decompressIntoAuxImage(MovieFrame *frame) {
 	const Common::Point origin(0, 0);
-	Common::Rect test = Common::Rect(frame->keyframeImage->width(), frame->keyframeImage->height());
-	Common::Array<Common::Rect> allDirty(1);
-	allDirty.push_back(test);
 	frame->keyframeImage->_image.create(frame->keyframeImage->width(), frame->keyframeImage->height(), Graphics::PixelFormat::createFormatCLUT8());
 	frame->keyframeImage->_image.setTransparentColor(0);
-	g_engine->getDisplayManager()->imageBlit(origin, frame->keyframeImage, 1.0, allDirty, &frame->keyframeImage->_image);
+	g_engine->getDisplayManager()->imageBlit(origin, frame->keyframeImage, 1.0, nullptr, &frame->keyframeImage->_image);
 }
 
 void StreamMovieActorFrames::readImageData(Chunk &chunk) {
diff --git a/engines/mediastation/actors/movie.h b/engines/mediastation/actors/movie.h
index 033ae5cb96d..01a0e0e1131 100644
--- a/engines/mediastation/actors/movie.h
+++ b/engines/mediastation/actors/movie.h
@@ -126,7 +126,7 @@ public:
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 	virtual void process() override;
 
-	virtual void draw(const Common::Array<Common::Rect> &dirtyRegion) override;
+	virtual void draw(DisplayContext &displayContext) override;
 
 	virtual bool isVisible() const override { return _isVisible; }
 
diff --git a/engines/mediastation/actors/sprite.cpp b/engines/mediastation/actors/sprite.cpp
index 36b59ce7030..19dfa5f02cd 100644
--- a/engines/mediastation/actors/sprite.cpp
+++ b/engines/mediastation/actors/sprite.cpp
@@ -375,12 +375,12 @@ void SpriteMovieActor::postMovieEndEventIfNecessary() {
 	runEventHandlerIfExists(kSpriteMovieEndEvent, value);
 }
 
-void SpriteMovieActor::draw(const Common::Array<Common::Rect> &dirtyRegion) {
+void SpriteMovieActor::draw(DisplayContext &displayContext) {
 	SpriteFrame *activeFrame = _asset->frames[_currentFrameIndex];
 	if (_isVisible) {
 		Common::Rect frameBbox = activeFrame->boundingBox();
 		frameBbox.translate(_boundingBox.left, _boundingBox.top);
-		g_engine->getDisplayManager()->imageBlit(frameBbox.origin(), activeFrame, _dissolveFactor, dirtyRegion);
+		g_engine->getDisplayManager()->imageBlit(frameBbox.origin(), activeFrame, _dissolveFactor, &displayContext);
 	}
 }
 
diff --git a/engines/mediastation/actors/sprite.h b/engines/mediastation/actors/sprite.h
index 01a2e1447c9..58441bd11b8 100644
--- a/engines/mediastation/actors/sprite.h
+++ b/engines/mediastation/actors/sprite.h
@@ -79,7 +79,7 @@ public:
 	~SpriteMovieActor();
 
 	virtual void process() override;
-	virtual void draw(const Common::Array<Common::Rect> &dirtyRegion) override;
+	virtual void draw(DisplayContext &displayContext) override;
 
 	virtual void readParameter(Chunk &chunk, ActorHeaderSectionType paramType) override;
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
diff --git a/engines/mediastation/actors/stage.cpp b/engines/mediastation/actors/stage.cpp
index 3b215408991..cf8687ca357 100644
--- a/engines/mediastation/actors/stage.cpp
+++ b/engines/mediastation/actors/stage.cpp
@@ -20,33 +20,38 @@
  */
 
 #include "mediastation/mediastation.h"
+#include "mediastation/actors/camera.h"
 #include "mediastation/actors/stage.h"
 #include "mediastation/debugchannels.h"
 
 namespace MediaStation {
 
 StageActor::StageActor() : SpatialEntity(kActorTypeStage),
-	_children(StageActor::compareSpatialActorByZIndex) {
+	_children(StageActor::compareSpatialActorByZIndex),
+	_cameras(StageActor::compareSpatialActorByZIndex) {
 }
 
 StageActor::~StageActor() {
 	removeAllChildren();
+	if (_parentStage != nullptr) {
+		_parentStage->removeChildSpatialEntity(this);
+	}
+	_currentCamera = nullptr;
 }
 
 void StageActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 	switch (paramType) {
 	case kActorHeaderChildActorId: {
-		uint actorId = chunk.readTypedUint16();
-		_pendingChild = g_engine->getSpatialEntityById(actorId);
+		// 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);
 		break;
 	}
 
-	case kActorHeaderStageSize: {
-		Common::Point size = chunk.readTypedGraphicSize();
-		_boundingBox.setWidth(size.x);
-		_boundingBox.setHeight(size.y);
+	case kActorHeaderStageExtent:
+		_extent = chunk.readTypedGraphicSize();
 		break;
-	}
 
 	case kActorHeaderCylindricalX:
 		_cylindricalX = static_cast<bool>(chunk.readTypedByte());
@@ -61,11 +66,105 @@ void StageActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 	}
 }
 
-void StageActor::draw(const Common::Array<Common::Rect> &dirtyRegion) {
+void StageActor::preload(const Common::Rect &rect) {
+	if (cylindricalX()) {
+		preloadTest(rect, kWrapLeft);
+	}
+	if (cylindricalY()) {
+		preloadTest(rect, kWrapTop);
+	}
+	if (cylindricalX() && cylindricalY()) {
+		preloadTest(rect, kWrapLeftTop);
+	}
+	preloadTest(rect, kWrapNone);
+}
+
+void StageActor::preloadTest(const Common::Rect &rect, CylindricalWrapMode wrapMode) {
 	for (SpatialEntity *entity : _children) {
+		entity->setAdjustedBounds(wrapMode);
+		if (!entity->isRectInMemory(rect) && !entity->isLoading()) {
+			entity->preload(rect);
+		}
+	}
+}
+
+bool StageActor::isRectInMemory(const Common::Rect &rect) {
+	bool result = true;
+	if (cylindricalX()) {
+		result = isRectInMemoryTest(rect, kWrapLeft);
+	}
+	if (result && cylindricalY()) {
+		result = isRectInMemoryTest(rect, kWrapTop);
+	}
+	if (result && cylindricalY() && cylindricalX()) {
+		result = isRectInMemoryTest(rect, kWrapLeftTop);
+	}
+	if (result) {
+		result = isRectInMemoryTest(rect, kWrapNone);
+	}
+	return result;
+}
+
+bool StageActor::isRectInMemoryTest(const Common::Rect &rect, CylindricalWrapMode wrapMode) {
+	for (SpatialEntity *entity : _children) {
+		entity->setAdjustedBounds(wrapMode);
+		if (!entity->isRectInMemory(rect)) {
+			return false;
+		}
+	}
+	return true;
+}
+
+void StageActor::draw(DisplayContext &displayContext) {
+	Clip *currentClip = displayContext.currentClip();
+	if (currentClip != nullptr) {
+		Clip *previousClip = displayContext.previousClip();
+		if (previousClip == nullptr) {
+			currentClip->addToRegion(currentClip->_bounds);
+		} else {
+			currentClip = previousClip;
+		}
+	}
+
+	displayContext.intersectClipWith(getBbox());
+	if (displayContext.clipIsEmpty()) {
+		return;
+	}
+
+	bool boundsNeedsAdjustment = false;
+	Common::Rect bounds = getBbox();
+	if (bounds.left != 0 || bounds.top != 0) {
+		boundsNeedsAdjustment = true;
+	}
+	if (boundsNeedsAdjustment) {
+		displayContext.pushOrigin();
+		displayContext._origin.x = bounds.left + displayContext._origin.x;
+		displayContext._origin.y = bounds.top + displayContext._origin.y;
+	}
+
+	if (_cameras.empty()) {
+		drawUsingStage(displayContext);
+	} else {
+		for (CameraActor *camera : _cameras) {
+			setCurrentCamera(camera);
+			camera->drawUsingCamera(displayContext, _children);
+		}
+	}
+
+	if (boundsNeedsAdjustment) {
+		displayContext.popOrigin();
+	}
+	displayContext.emptyCurrentClip();
+}
+
+void StageActor::drawUsingStage(DisplayContext &displayContext) {
+	for (SpatialEntity *entity : _children) {
+		entity->setAdjustedBounds(kWrapNone);
 		if (entity->isVisible()) {
-			debugC(5, kDebugGraphics, "%s: Redrawing actor %d", __func__, entity->id());
-			entity->draw(dirtyRegion);
+			if (displayContext.rectIsInClip(entity->getBbox())) {
+				debugC(5, kDebugGraphics, "%s: Redrawing actor %d", __func__, entity->id());
+				entity->draw(displayContext);
+			}
 		}
 	}
 }
@@ -75,17 +174,107 @@ void StageActor::invalidateRect(const Common::Rect &rect) {
 		Common::Point origin = _boundingBox.origin();
 		Common::Rect rectRelativeToParent = rect;
 		rectRelativeToParent.translate(origin.x, origin.y);
-		_parentStage->invalidateRect(rect);
+
+		if (_cameras.size() == 0) {
+			_parentStage->invalidateRect(rectRelativeToParent);
+		} else {
+			invalidateUsingCameras(rectRelativeToParent);
+		}
 	} else {
 		error("%s: Attempt to invalidate rect without a parent stage", __func__);
 	}
 }
 
+void StageActor::invalidateUsingCameras(const Common::Rect &rect) {
+	for (CameraActor *camera : _cameras) {
+		Common::Rect adjustedRectToInvalidate = rect;
+		Common::Rect cameraBounds = camera->getBbox();
+
+		Common::Rect cameraBoundsInStageCoordinates = cameraBounds;
+		Common::Rect stageOrigin = getBbox();
+		cameraBoundsInStageCoordinates.translate(stageOrigin.left, stageOrigin.top);
+
+		Common::Point cameraViewportOrigin = camera->getViewportOrigin();
+		Common::Point viewportOffsetFromCameraBounds(
+			cameraViewportOrigin.x - cameraBounds.left,
+			cameraViewportOrigin.y - cameraBounds.top
+		);
+
+		adjustedRectToInvalidate.translate(
+			-viewportOffsetFromCameraBounds.x,
+			-viewportOffsetFromCameraBounds.y
+		);
+		invalidateObject(adjustedRectToInvalidate, cameraBoundsInStageCoordinates);
+	}
+}
+
+void StageActor::invalidateObject(const Common::Rect &rect, const Common::Rect &visibleRegion) {
+	Common::Point xyAdjustment(0, 0);
+	invalidateTest(rect, visibleRegion, xyAdjustment);
+
+	if (_cylindricalX) {
+		xyAdjustment.x = _extent.x;
+		xyAdjustment.y = 0;
+		invalidateTest(rect, visibleRegion, xyAdjustment);
+
+		xyAdjustment.x = -_extent.x;
+		xyAdjustment.y = 0;
+		invalidateTest(rect, visibleRegion, xyAdjustment);
+	}
+
+	if (_cylindricalY) {
+		xyAdjustment.x = 0;
+		xyAdjustment.y = _extent.y;
+		invalidateTest(rect, visibleRegion, xyAdjustment);
+
+		xyAdjustment.x = 0;
+		xyAdjustment.y = -_extent.y;
+		invalidateTest(rect, visibleRegion, xyAdjustment);
+	}
+
+	if (_cylindricalX && _cylindricalY) {
+		xyAdjustment.x = _extent.x;
+		xyAdjustment.y = _extent.y;
+		invalidateTest(rect, visibleRegion, xyAdjustment);
+
+		xyAdjustment.x = -_extent.x;
+		xyAdjustment.y = -_extent.y;
+		invalidateTest(rect, visibleRegion, xyAdjustment);
+
+		xyAdjustment.x = -_extent.x;
+		xyAdjustment.y = _extent.y;
+		invalidateTest(rect, visibleRegion, xyAdjustment);
+
+		xyAdjustment.x = _extent.x;
+		xyAdjustment.y = -_extent.y;
+		invalidateTest(rect, visibleRegion, xyAdjustment);
+	}
+}
+
+void StageActor::invalidateTest(const Common::Rect &rect, const Common::Rect &visibleRegion, const Common::Point &originAdjustment) {
+	Common::Rect rectToInvalidateRelative = rect;
+	rectToInvalidateRelative.translate(-originAdjustment.x, -originAdjustment.y);
+	rectToInvalidateRelative.clip(visibleRegion);
+	_parentStage->invalidateRect(rectToInvalidateRelative);
+}
+
 void StageActor::loadIsComplete() {
+	// This is deliberately calling down to Actor, rather than calling
+	// to SpatialEntity first.
 	Actor::loadIsComplete();
-	if (_pendingChild != nullptr) {
-		addChildSpatialEntity(_pendingChild);
-		_pendingChild = nullptr;
+
+	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;
+	}
+
+	if (_extent.x == 0 || _extent.y == 0) {
+		_extent.x = _boundingBox.width();
+		_extent.y = _boundingBox.height();
 	}
 }
 
@@ -103,14 +292,30 @@ void StageActor::removeActorFromStage(uint actorId) {
 	SpatialEntity *spatialEntity = g_engine->getSpatialEntityById(actorId);
 	StageActor *currentParent = spatialEntity->getParentStage();
 	if (currentParent == this) {
-		// Remove the actor from this stage, and add it
-		// back to the root stage.
+		// Remove the actor from this stage, and add it back to the root stage.
 		removeChildSpatialEntity(spatialEntity);
 		RootStage *rootStage = g_engine->getRootStage();
 		rootStage->addChildSpatialEntity(spatialEntity);
 	}
 }
 
+void StageActor::addCamera(CameraActor *camera) {
+	_cameras.insert(camera);
+}
+
+void StageActor::removeCamera(CameraActor *camera) {
+	for (auto it = _cameras.begin(); it != _cameras.end(); ++it) {
+		if (*it == camera) {
+			_cameras.erase(it);
+			break;
+		}
+	}
+}
+
+void StageActor::setCurrentCamera(CameraActor *camera) {
+	_currentCamera = camera;
+}
+
 ScriptValue StageActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
 	ScriptValue returnValue;
 	switch (methodId) {
@@ -189,30 +394,144 @@ void StageActor::removeChildSpatialEntity(SpatialEntity *entity) {
 	}
 }
 
-uint16 StageActor::findActorToAcceptMouseEvents(
+uint16 StageActor::queryChildrenAboutMouseEvents(
 	const Common::Point &point,
 	uint16 eventMask,
 	MouseActorState &state,
-	bool inBounds) {
-
-	debugC(6, kDebugEvents, " --- %s ---", __func__);
-	if (!inBounds) {
-		if (!_boundingBox.contains(point)) {
-			inBounds = true;
-		}
-	}
+	CylindricalWrapMode wrapMode) {
 
 	uint16 result = 0;
 	Common::Point adjustedPoint = 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)", __func__, child->id(), child->zIndex(), eventMask, result);
-		uint16 handledEvents = child->findActorToAcceptMouseEvents(adjustedPoint, eventMask, state, inBounds);
+		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);
+
+		child->setAdjustedBounds(wrapMode);
+		uint16 handledEvents = child->findActorToAcceptMouseEvents(adjustedPoint, eventMask, state, true);
+		child->setAdjustedBounds(kWrapNone);
 
 		eventMask &= ~handledEvents;
 		result |= handledEvents;
 	}
+	return result;
+}
+
+uint16 StageActor::findActorToAcceptMouseEventsObject(
+	const Common::Point &point,
+	uint16 eventMask,
+	MouseActorState &state,
+	bool inBounds) {
+
+	uint16 result = 0;
+
+	uint16 handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, kWrapNone);
+	if (handledEvents != 0) {
+		eventMask &= ~handledEvents;
+		result |= handledEvents;
+	}
+
+	if ((eventMask != 0) && cylindricalX()) {
+		handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, kWrapLeft);
+		if (handledEvents != 0) {
+			eventMask &= ~handledEvents;
+			result |= handledEvents;
+		}
+	}
+
+	if ((eventMask != 0) && cylindricalX()) {
+		handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, kWrapRight);
+		if (handledEvents != 0) {
+			eventMask &= ~handledEvents;
+			result |= handledEvents;
+		}
+	}
+
+	if ((eventMask != 0) && cylindricalY()) {
+		handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, kWrapTop);
+		if (handledEvents != 0) {
+			eventMask &= ~handledEvents;
+			result |= handledEvents;
+		}
+	}
+
+	if ((eventMask != 0) && cylindricalY()) {
+		handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, kWrapBottom);
+		if (handledEvents != 0) {
+			eventMask &= ~handledEvents;
+			result |= handledEvents;
+		}
+	}
+
+	if ((eventMask != 0) && cylindricalY() && cylindricalX()) {
+		handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, kWrapLeftTop);
+		if (handledEvents != 0) {
+			eventMask &= ~handledEvents;
+			result |= handledEvents;
+		}
+	}
+
+	if ((eventMask != 0) && cylindricalY() && cylindricalX()) {
+		handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, kWrapRightBottom);
+		if (handledEvents != 0) {
+			result |= handledEvents;
+		}
+	}
+
+	return result;
+}
+
+uint16 StageActor::findActorToAcceptMouseEventsCamera(
+	const Common::Point &point,
+	uint16 eventMask,
+	MouseActorState &state,
+	bool inBounds) {
+
+	uint16 result = 0;
+	for (CameraActor *camera : _cameras) {
+		Common::Point mousePosRelativeToCamera = point;
+		setCurrentCamera(camera);
+
+		Common::Point cameraViewportOrigin = camera->getViewportOrigin();
+		mousePosRelativeToCamera.x += cameraViewportOrigin.x;
+		mousePosRelativeToCamera.y += cameraViewportOrigin.y;
+
+		if (!inBounds) {
+			Common::Rect viewportBounds = camera->getViewportBounds();
+			if (!viewportBounds.contains(mousePosRelativeToCamera)) {
+				inBounds = true;
+			}
+		}
+
+		result = findActorToAcceptMouseEventsObject(mousePosRelativeToCamera, eventMask, state, inBounds);
+	}
+
+	return result;
+}
+
+uint16 StageActor::findActorToAcceptMouseEvents(
+	const Common::Point &point,
+	uint16 eventMask,
+	MouseActorState &state,
+	bool inBounds) {
+
+	debugC(6, kDebugEvents, " --- %s ---", __func__);
+
+	Common::Point mousePosAdjustedByStageOrigin = point;
+	mousePosAdjustedByStageOrigin.x -= _boundingBox.left;
+	mousePosAdjustedByStageOrigin.y -= _boundingBox.top;
+
+	uint16 result;
+	if (_cameras.empty()) {
+		if (!inBounds) {
+			if (!_boundingBox.contains(point)) {
+				inBounds = true;
+			}
+		}
+		result = queryChildrenAboutMouseEvents(mousePosAdjustedByStageOrigin, eventMask, state, kWrapNone);
+	} else {
+		result = findActorToAcceptMouseEventsCamera(mousePosAdjustedByStageOrigin, eventMask, state, inBounds);
+	}
 
 	debugC(6, kDebugEvents, " --- END %s ---", __func__);
 	return result;
@@ -322,7 +641,7 @@ void StageActor::currentMousePosition(Common::Point &point) {
 void RootStage::invalidateRect(const Common::Rect &rect) {
 	Common::Rect rectToAdd = rect;
 	rectToAdd.clip(_boundingBox);
-	_dirtyRegion.push_back(rectToAdd);
+	_dirtyRegion.addRect(rectToAdd);
 }
 
 void RootStage::currentMousePosition(Common::Point &point) {
@@ -330,17 +649,14 @@ void RootStage::currentMousePosition(Common::Point &point) {
 	point -= getBbox().origin();
 }
 
-void RootStage::drawAll() {
-	Common::Array<Common::Rect> region;
-	region.push_back(_boundingBox);
-	StageActor::draw(region);
+void RootStage::drawAll(DisplayContext &displayContext) {
+	StageActor::draw(displayContext);
 }
 
-void RootStage::drawDirtyRegion() {
-	if (!_dirtyRegion.empty()) {
-		StageActor::draw(_dirtyRegion);
-		_dirtyRegion.clear();
-	}
+void RootStage::drawDirtyRegion(DisplayContext &displayContext) {
+	displayContext.setClipTo(_dirtyRegion);
+	StageActor::draw(displayContext);
+	displayContext.emptyCurrentClip();
 }
 
 uint16 RootStage::findActorToAcceptMouseEvents(
@@ -407,24 +723,16 @@ StageDirector::~StageDirector() {
 }
 
 void StageDirector::drawAll() {
-	// TODO: This just passes through for now,
-	// but once the original DisplayContext stuff
-	// is reimplemented, there will be more here.
-	_rootStage->drawAll();
+	_rootStage->drawAll(g_engine->getDisplayManager()->_displayContext);
 }
 
 void StageDirector::drawDirtyRegion() {
-	// TODO: This just passes through for now,
-	// but once the original DisplayContext stuff
-	// is reimplemented, there will be more here.
-	_rootStage->drawDirtyRegion();
+	_rootStage->drawDirtyRegion(g_engine->getDisplayManager()->_displayContext);
 }
 
 void StageDirector::clearDirtyRegion() {
-	// TODO: This just passes through for now,
-	// but once the original DisplayContext stuff
-	// is reimplemented, there will be more here.
-	_rootStage->clearDirtyRegion();
+	_rootStage->_dirtyRegion._rects.clear();
+	_rootStage->_dirtyRegion._bounds = Common::Rect(0, 0, 0, 0);
 }
 
 void StageDirector::handleKeyboardEvent(const Common::Event &event) {
diff --git a/engines/mediastation/actors/stage.h b/engines/mediastation/actors/stage.h
index 7c7bf41f770..2cce143da20 100644
--- a/engines/mediastation/actors/stage.h
+++ b/engines/mediastation/actors/stage.h
@@ -25,24 +25,63 @@
 #include "common/events.h"
 
 #include "mediastation/actor.h"
+#include "mediastation/graphics.h"
 #include "mediastation/mediascript/scriptvalue.h"
 #include "mediastation/mediascript/scriptconstants.h"
 
 namespace MediaStation {
 
+// Cylindrical wrapping allows content on a stage to wrap around like a cylinder - for example, when you scroll past the
+// right edge, content from the left edge appears, creating the illusion of an infinite looping world.
+enum CylindricalWrapMode : int {
+	kWrapNone = 0,        // No offset (default)
+	kWrapRight = 1,       // Right wrap (X + extent.x)
+	kWrapBottom = 2,      // Bottom wrap (Y + extent.y)
+	kWrapLeftTop = 3,     // Left + Top wrap (X - extent.x, Y - extent.y)
+	kWrapLeft = 4,        // Left wrap (X - extent.x)
+	kWrapRightBottom = 5, // Right + Bottom wrap (X + extent.x, Y + extent.y)
+	kWrapTop = 6,         // Top wrap (Y - extent.y)
+	kWrapLeftBottom = 7,  // Left + Bottom wrap (X - extent.x, Y + extent.y)
+	kWrapRightTop = 8     // Right + Top wrap (X + extent.x, Y - extent.y)
+};
+
+class CameraActor;
 class StageActor : public SpatialEntity {
 public:
 	StageActor();
 	virtual ~StageActor();
 
-	virtual void draw(const Common::Array<Common::Rect> &dirtyRegion) override;
+	virtual void draw(DisplayContext &displayContext) override;
+	void drawUsingStage(DisplayContext &displayContext);
+
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 	virtual void readParameter(Chunk &chunk, ActorHeaderSectionType paramType) override;
+	virtual void preload(const Common::Rect &rect) override;
 	virtual bool isVisible() const override { return _children.size() > 0; }
+	virtual bool isRectInMemory(const Common::Rect &rect) override;
 
 	void addChildSpatialEntity(SpatialEntity *entity);
 	void removeChildSpatialEntity(SpatialEntity *entity);
+	void addCamera(CameraActor *camera);
+	void removeCamera(CameraActor *camera);
+	void setCurrentCamera(CameraActor *camera);
+	CameraActor *getCurrentCamera() const { return _currentCamera; }
 
+	uint16 queryChildrenAboutMouseEvents(
+		const Common::Point &point,
+		uint16 eventMask,
+		MouseActorState &state,
+		CylindricalWrapMode wrapMode = kWrapNone);
+	uint16 findActorToAcceptMouseEventsObject(
+		const Common::Point &point,
+		uint16 eventMask,
+		MouseActorState &state,
+		bool inBounds);
+	uint16 findActorToAcceptMouseEventsCamera(
+		const Common::Point &point,
+		uint16 eventMask,
+		MouseActorState &state,
+		bool inBounds);
 	virtual uint16 findActorToAcceptMouseEvents(
 		const Common::Point &point,
 		uint16 eventMask,
@@ -56,28 +95,43 @@ public:
 	virtual void setMousePosition(int16 x, int16 y) override;
 
 	void invalidateZIndexOf(const SpatialEntity *entity);
+	virtual void invalidateLocalBounds() override;
+	virtual void invalidateRect(const Common::Rect &rect);
+
+	bool cylindricalX() const { return _cylindricalX; }
+	bool cylindricalY() const { return _cylindricalY; }
+	Common::Point extent() const { return _extent; }
 
 protected:
+	// Whether or not cameras on this stage should wrap around this stage.
 	bool _cylindricalX = false;
 	bool _cylindricalY = false;
-	SpatialEntity *_pendingChild = nullptr;
+	Common::Point _extent;
+	SpatialEntity *_pendingParent = nullptr;
+	CameraActor *_currentCamera = nullptr;
 
 	void addActorToStage(uint actorId);
 	void removeActorFromStage(uint actorId);
+	bool isRectInMemoryTest(const Common::Rect &rect, CylindricalWrapMode wrapMode);
+	void preloadTest(const Common::Rect &rect, CylindricalWrapMode wrapMode);
 
 	bool assertHasNoParent(const SpatialEntity *entity);
 	bool assertHasParentThatIsNotMe(const SpatialEntity *entity) { return !assertIsMyChild(entity); }
 	bool assertIsMyChild(const SpatialEntity *entity) { return this == entity->getParentStage();}
 	void removeAllChildren();
 
-	virtual void invalidateLocalBounds() override;
 	virtual void invalidateLocalZIndex() override;
-	virtual void invalidateRect(const Common::Rect &rect);
+	void invalidateUsingCameras(const Common::Rect &rect);
+	void invalidateObject(const Common::Rect &rect, const Common::Rect &visibleRegion);
+	// The function is called this in the original; not sure why though.
+	void invalidateTest(const Common::Rect &rect, const Common::Rect &clip, const Common::Point &originAdjustment);
+
 	virtual void loadIsComplete() override;
 
 	static int compareSpatialActorByZIndex(const SpatialEntity *a, const SpatialEntity *b);
 
 	Common::SortedArray<SpatialEntity *, const SpatialEntity *> _children;
+	Common::SortedArray<CameraActor *, const SpatialEntity *> _cameras;
 };
 
 class RootStage : public StageActor {
@@ -85,9 +139,6 @@ public:
 	friend class StageDirector;
 	RootStage() : StageActor() { _id = ROOT_STAGE_ACTOR_ID; };
 
-	const Common::Array<Common::Rect> &getDirtyRegion() { return _dirtyRegion; }
-	void clearDirtyRegion() { _dirtyRegion.clear(); }
-
 	virtual uint16 findActorToAcceptMouseEvents(
 		const Common::Point &point,
 		uint16 eventMask,
@@ -102,13 +153,13 @@ public:
 	virtual void mouseExitedEvent(const Common::Event &event) override;
 	virtual void mouseOutOfFocusEvent(const Common::Event &event) override;
 
-	void drawAll();
-	void drawDirtyRegion();
-	void invalidateAll();
+	void drawAll(DisplayContext &displayContext);
+	void drawDirtyRegion(DisplayContext &displayContext);
+
+	Region _dirtyRegion;
 
 private:
 	static const uint ROOT_STAGE_ACTOR_ID = 2;
-	Common::Array<Common::Rect> _dirtyRegion;
 	bool _isMouseInside = false;
 };
 
diff --git a/engines/mediastation/context.cpp b/engines/mediastation/context.cpp
index f1bdd54245b..95c39c55d3c 100644
--- a/engines/mediastation/context.cpp
+++ b/engines/mediastation/context.cpp
@@ -26,6 +26,7 @@
 #include "mediastation/bitmap.h"
 #include "mediastation/mediascript/collection.h"
 #include "mediastation/mediascript/function.h"
+#include "mediastation/actors/camera.h"
 #include "mediastation/actors/canvas.h"
 #include "mediastation/actors/palette.h"
 #include "mediastation/actors/image.h"
@@ -33,6 +34,7 @@
 #include "mediastation/actors/sound.h"
 #include "mediastation/actors/movie.h"
 #include "mediastation/actors/sprite.h"
+#include "mediastation/actors/stage.h"
 #include "mediastation/actors/hotspot.h"
 #include "mediastation/actors/timer.h"
 #include "mediastation/actors/screen.h"
@@ -84,9 +86,9 @@ void MediaStationEngine::readDestroyActorData(Chunk &chunk) {
 
 void MediaStationEngine::readActorLoadComplete(Chunk &chunk) {
 	uint actorId = chunk.readTypedUint16();
+	debugC(5, kDebugLoading, "%s: Actor %d", __func__, actorId);
 	Actor *actor = g_engine->getActorById(actorId);
 	actor->loadIsComplete();
-	debugC(5, kDebugLoading, "%s: Actor %d", __func__, actorId);
 }
 
 void MediaStationEngine::readCreateActorData(Chunk &chunk) {
@@ -133,6 +135,14 @@ void MediaStationEngine::readCreateActorData(Chunk &chunk) {
 		actor = new CanvasActor();
 		break;
 
+	case kActorTypeCamera:
+		actor = new CameraActor();
+		break;
+
+	case kActorTypeStage:
+		actor = new StageActor();
+		break;
+
 	case kActorTypeScreen:
 		actor = new ScreenActor();
 		break;
diff --git a/engines/mediastation/debugchannels.h b/engines/mediastation/debugchannels.h
index 015172ea2ed..dfbc6bc428b 100644
--- a/engines/mediastation/debugchannels.h
+++ b/engines/mediastation/debugchannels.h
@@ -32,6 +32,7 @@ namespace MediaStation {
 // TODO: Finish comments that describe the various debug levels.
 enum DebugChannels {
 	kDebugGraphics = 1,
+	kDebugCamera,
 	kDebugPath,
 	kDebugScan,
 	kDebugScript,
diff --git a/engines/mediastation/detection.cpp b/engines/mediastation/detection.cpp
index fa788957b2e..40c3b9a1d22 100644
--- a/engines/mediastation/detection.cpp
+++ b/engines/mediastation/detection.cpp
@@ -34,6 +34,7 @@
 
 const DebugChannelDef MediaStationMetaEngineDetection::debugFlagList[] = {
 	{ MediaStation::kDebugGraphics, "graphics", "Graphics debug level" },
+	{ MediaStation::kDebugCamera, "camera", "Camera panning debug level" },
 	{ MediaStation::kDebugPath, "path", "Pathfinding debug level" },
 	{ MediaStation::kDebugScan, "scan", "Scan for unrecognised games" },
 	{ MediaStation::kDebugScript, "script", "Enable debug script dump" },
diff --git a/engines/mediastation/graphics.cpp b/engines/mediastation/graphics.cpp
index e6d9dd0f264..bea28202896 100644
--- a/engines/mediastation/graphics.cpp
+++ b/engines/mediastation/graphics.cpp
@@ -32,9 +32,176 @@
 
 namespace MediaStation {
 
+void Region::addRect(const Common::Rect &rect) {
+	if (rect.isEmpty()) {
+		return;
+	}
+
+	// TODO: This is replicating some behavior of the original's IM_Rect::operator|=,
+	// which maybe we SHOULD in fact implement here.
+	if (_bounds.isEmpty()) {
+		_bounds = rect;
+	} else {
+		_bounds.extend(rect);
+	}
+	_rects.clear();
+	_rects.push_back(_bounds);
+}
+
+bool Region::intersects(const Common::Rect &rect) {
+	return _bounds.intersects(rect);
+}
+
+void Region::operator&=(const Common::Rect &rect) {
+	_bounds.clip(rect);
+	_rects.clear();
+	_rects.push_back(_bounds);
+}
+
+void Region::operator+=(const Common::Point &point) {
+	_bounds.translate(point.x, point.y);
+	_rects.clear();
+	_rects.push_back(_bounds);
+}
+
+void Clip::addToRegion(const Region &region) {
+	for (const Common::Rect &rect : region._rects) {
+		addToRegion(rect);
+	}
+}
+
+void Clip::addToRegion(const Common::Rect &rect) {
+	Common::Rect rectRelativeToOrigin = rect;
+	rectRelativeToOrigin.clip(_bounds);
+	if (!rectRelativeToOrigin.isEmpty()) {
+		rectRelativeToOrigin.translate(-_bounds.left, -_bounds.top);
+		_region.addRect(rectRelativeToOrigin);
+	}
+}
+
+bool Clip::clipIntersectsRect(const Common::Rect &rect) {
+	return _region.intersects(rect);
+}
+
+void Clip::intersectWithRegion(const Common::Rect &rect) {
+	Common::Rect rectRelativeToOrigin = rect;
+	rectRelativeToOrigin.clip(_bounds);
+	if (!_region._rects.empty()) {
+		if (_bounds != rectRelativeToOrigin) {
+			rectRelativeToOrigin.translate(-_bounds.left, -_bounds.top);
+			_region &= rectRelativeToOrigin;
+		}
+	}
+}
+
+void Clip::makeEmpty() {
+	_region._bounds.setEmpty();
+	_region._rects.clear();
+}
+
+
+Clip::Clip(const Common::Rect &rect) {
+	_bounds = rect;
+	Common::Rect initialRegion(Common::Rect(0, 0, rect.width(), rect.height()));
+	_region.addRect(initialRegion);
+}
+
+void DisplayContext::addClip() {
+	if (_destImage != nullptr) {
+		Common::Rect rect(0, 0, _destImage->w, _destImage->h);
+		Clip clip(rect);
+		_clips.push(clip);
+	}
+}
+
+Clip *DisplayContext::currentClip() {
+	if (_destImage == nullptr) {
+		return nullptr;
+	}
+
+	if (_clips.empty()) {
+		// The original did some crazy allocation stuff with an external expected
+		// item counter, but we will just do this using the ScummVM classes.
+		addClip();
+	}
+	return &_clips.top();
+}
+
+void DisplayContext::emptyCurrentClip() {
+	if (!_clips.empty()) {
+		_clips.pop();
+	}
+}
+
+Clip *DisplayContext::previousClip() {
+	if (_clips.size() < 2) {
+		return nullptr;
+	} else {
+		return &_clips[_clips.size() - 2];
+	}
+}
+
+void DisplayContext::pushOrigin() {
+	_origins.push(_origin);
+}
+
+void DisplayContext::popOrigin() {
+	if (!_origins.empty()) {
+		_origin = _origins.top();
+		_origins.pop();
+	}
+}
+
+void DisplayContext::verifyClipSize() {
+	if (_destImage != nullptr && !_clips.empty()) {
+		const Clip &firstClip = _clips[0];
+		if (firstClip._bounds.width() != _destImage->w || firstClip._bounds.height() != _destImage->h) {
+			_clips.clear();
+		}
+	}
+}
+
+bool DisplayContext::clipIsEmpty() {
+	Clip *clip = currentClip();
+	if (clip != nullptr) {
+		return clip->_region._rects.empty();
+	}
+	return true;
+}
+
+void DisplayContext::intersectClipWith(const Common::Rect &rect) {
+	Clip *clip = currentClip();
+	if (clip != nullptr) {
+		Common::Rect rectInAbsoluteCoordinates = rect;
+		rectInAbsoluteCoordinates.translate(_origin.x, _origin.y);
+		clip->intersectWithRegion(rectInAbsoluteCoordinates);
+	}
+}
+
+bool DisplayContext::rectIsInClip(const Common::Rect &rect) {
+	bool result = false;
+	Clip *clip = currentClip();
+	if (clip != nullptr) {
+		Common::Rect rectInAbsoluteCoordinates = rect;
+		rectInAbsoluteCoordinates.translate(_origin.x, _origin.y);
+		result = clip->clipIntersectsRect(rectInAbsoluteCoordinates);
+	}
+	return result;
+}
+
+void DisplayContext::setClipTo(Region region) {
+	Clip *clip = currentClip();
+	if (clip != nullptr) {
+		clip->makeEmpty();
+		region += _origin;
+		clip->addToRegion(region);
+	}
+}
+
 VideoDisplayManager::VideoDisplayManager(MediaStationEngine *vm) : _vm(vm) {
 	initGraphics(MediaStationEngine::SCREEN_WIDTH, MediaStationEngine::SCREEN_HEIGHT);
 	_screen = new Graphics::Screen();
+	_displayContext._destImage = _screen;
 }
 
 VideoDisplayManager::~VideoDisplayManager() {
@@ -543,10 +710,10 @@ void VideoDisplayManager::_setPercentToPaletteObject(double percent, uint palett
 }
 
 void VideoDisplayManager::imageBlit(
-	const Common::Point &destinationPoint,
+	Common::Point destinationPoint,
 	const Bitmap *sourceImage,
 	double dissolveFactor,
-	const Common::Array<Common::Rect> &dirtyRegion,
+	DisplayContext *displayContext,
 	Graphics::ManagedSurface *targetImage) {
 
 	byte blitFlags = kClipEnabled;
@@ -586,6 +753,19 @@ void VideoDisplayManager::imageBlit(
 	}
 	uint integralDissolveFactor = static_cast<uint>(dissolveFactor * 100 + 0.5);
 
+	Common::Array<Common::Rect> dirtyRegion;
+	if (displayContext == nullptr) {
+		if (targetImage == nullptr) {
+			warning("%s: Neither display context nor target image was provided", __func__);
+		}
+		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;
+	}
+
 	if (targetImage == nullptr) {
 		targetImage = _screen;
 	}
@@ -757,12 +937,12 @@ void VideoDisplayManager::dissolveBlit1Rect(
 }
 
 void VideoDisplayManager::imageDeltaBlit(
-	const Common::Point &deltaFramePos,
+	Common::Point deltaFramePos,
 	const Common::Point &keyFrameOffset,
 	const Bitmap *deltaFrame,
 	const Bitmap *keyFrame,
 	const double dissolveFactor,
-	const Common::Array<Common::Rect> &dirtyRegion) {
+	DisplayContext *displayContext) {
 
 	if (deltaFrame->getCompressionType() != kRle8BitmapCompression) {
 		error("%s: Unsupported delta frame compression type for delta blit: %d",
@@ -771,6 +951,15 @@ void VideoDisplayManager::imageDeltaBlit(
 		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 {
+		Clip *currentClip = displayContext->currentClip();
+		dirtyRegion = currentClip->_region._rects;
+		deltaFramePos += _displayContext._origin;
+	}
+
 	switch (keyFrame->getCompressionType()) {
 	case kUncompressedBitmap:
 	case kUncompressedTransparentBitmap:
diff --git a/engines/mediastation/graphics.h b/engines/mediastation/graphics.h
index 55807923946..cd1fc1e7bbc 100644
--- a/engines/mediastation/graphics.h
+++ b/engines/mediastation/graphics.h
@@ -24,6 +24,7 @@
 
 #include "common/rect.h"
 #include "common/array.h"
+#include "common/stack.h"
 #include "graphics/managed_surface.h"
 #include "graphics/screen.h"
 
@@ -70,6 +71,58 @@ enum VideoDisplayManagerSectionType {
 	kVideoDisplayManagerLoadPalette = 0x5aa,
 };
 
+class Region {
+public:
+	void addRect(const Common::Rect &rect);
+	bool intersects(const Common::Rect &rect);
+	void operator&=(const Common::Rect &rect);
+	void operator+=(const Common::Point &point);
+
+	Common::Array<Common::Rect> _rects;
+	Common::Rect _bounds;
+};
+
+class Clip {
+public:
+	Clip() {}
+	Clip(const Common::Rect &rect);
+
+	void addToRegion(const Region &region);
+	void addToRegion(const Common::Rect &rect);
+	bool clipIntersectsRect(const Common::Rect &rect);
+	void intersectWithRegion(const Common::Rect &rect);
+	void makeEmpty();
+
+	Region _region;
+	Common::Rect _bounds;
+};
+
+class DisplayContext {
+public:
+	bool clipIsEmpty();
+	void intersectClipWith(const Common::Rect &rect);
+	bool rectIsInClip(const Common::Rect &rect);
+	void setClipTo(Region region);
+	void emptyCurrentClip();
+
+	void addClip();
+	Clip *currentClip();
+	Clip *previousClip();
+	void verifyClipSize();
+
+	// These are not named as such in the original, but are helper functions
+	// for things that likely were macros or something similar in the original.
+	void pushOrigin();
+	void popOrigin();
+
+	Common::Point _origin;
+	Graphics::ManagedSurface *_destImage = nullptr;
+
+private:
+	Common::Stack<Common::Point> _origins;
+	Common::Stack<Clip> _clips;
+};
+
 class VideoDisplayManager : public ParameterClient {
 public:
 	VideoDisplayManager(MediaStationEngine *vm);
@@ -82,19 +135,19 @@ public:
 	void setRegisteredPalette(Graphics::Palette *palette) { _registeredPalette = palette; }
 
 	void imageBlit(
-		const Common::Point &destinationPoint,
+		Common::Point destinationPoint,
 		const Bitmap *image,
 		double dissolveFactor,
-		const Common::Array<Common::Rect> &dirtyRegion,
+		DisplayContext *displayContext,
 		Graphics::ManagedSurface *destinationImage = nullptr);
 
 	void imageDeltaBlit(
-		const Common::Point &deltaFramePos,
+		Common::Point deltaFramePos,
 		const Common::Point &keyFrameOffset,
 		const Bitmap *deltaFrame,
 		const Bitmap *keyFrame,
 		const double dissolveFactor,
-		const Common::Array<Common::Rect> &dirtyRegion);
+		DisplayContext *displayContext);
 
 	void effectTransition(Common::Array<ScriptValue> &args);
 	void setTransitionOnSync(Common::Array<ScriptValue> &args) { _scheduledTransitionOnSync = args; }
@@ -107,6 +160,8 @@ public:
 	void getDefaultGammaValues(double &red, double &green, double &blue);
 	void getGammaValues(double &red, double &green, double &blue);
 
+	DisplayContext _displayContext;
+
 private:
 	MediaStationEngine *_vm = nullptr;
 	Graphics::Screen *_screen = nullptr;
diff --git a/engines/mediastation/mediascript/scriptconstants.cpp b/engines/mediastation/mediascript/scriptconstants.cpp
index d503b8c2ebc..75af8bcb854 100644
--- a/engines/mediastation/mediascript/scriptconstants.cpp
+++ b/engines/mediastation/mediascript/scriptconstants.cpp
@@ -270,10 +270,28 @@ const char *builtInMethodToStr(BuiltInMethod method) {
 		return "StageGetWidth";
 	case kStageGetHeightMethod:
 		return "StageGetHeight";
+	case kAddToStageMethod:
+		return "AddToStage\\OpenLens";
+	case kRemoveFromStageMethod:
+		return "RemoveFromStage\\CloseLens";
+	case kAddedToStageMethod:
+		return "AddedToStage";
+	case kStartPanMethod:
+		return "StartPan";
 	case kStopPanMethod:
 		return "StopPan";
+	case kIsPanningMethod:
+		return "IsPanning";
 	case kViewportMoveToMethod:
 		return "ViewportMoveTo";
+	case kAdjustCameraViewportMethod:
+		return "AdjustCameraViewport";
+	case kAdjustCameraViewportSpatialCenterMethod:
+		return "AdjustCameraViewportSpatialCenter";
+	case kSetCameraBoundsMethod:
+		return "SetCameraBounds";
+	case kXViewportPositionMethod:
+		return "XViewportPosition";
 	case kYViewportPositionMethod:
 		return "YViewportPosition";
 	case kPanToMethod:
@@ -336,10 +354,6 @@ const char *builtInMethodToStr(BuiltInMethod method) {
 		return "PrependList";
 	case kSortMethod:
 		return "Sort";
-	case kOpenLensMethod:
-		return "OpenLens";
-	case kCloseLensMethod:
-		return "CloseLens";
 	default:
 		return "UNKNOWN";
 	}
@@ -399,6 +413,8 @@ const char *eventTypeToStr(EventType type) {
 		return "TextInput";
 	case kTextErrorEvent:
 		return "TextError";
+	case kCameraPanStepEvent:
+		return "CameraPanStep";
 	case kCameraPanAbortEvent:
 		return "CameraPanAbort";
 	case kCameraPanEndEvent:
diff --git a/engines/mediastation/mediascript/scriptconstants.h b/engines/mediastation/mediascript/scriptconstants.h
index 7020287b476..ff8c366645a 100644
--- a/engines/mediastation/mediascript/scriptconstants.h
+++ b/engines/mediastation/mediascript/scriptconstants.h
@@ -177,8 +177,19 @@ enum BuiltInMethod {
 	kStageGetHeightMethod = 0x16D,
 
 	// CAMERA METHODS.
+	// NOTE: IDs 0x15A and 0x15B seem to be double-assigned
+	// between two camera methods and two printer methods.
+	kAddToStageMethod = 0x15A,
+	kRemoveFromStageMethod = 0x15B,
+	kAddedToStageMethod = 0x15C,
+	kStartPanMethod = 0x15D,
 	kStopPanMethod = 0x15E,
+	kIsPanningMethod = 0x15F,
 	kViewportMoveToMethod = 0x160,
+	kAdjustCameraViewportMethod = 0x161,
+	kAdjustCameraViewportSpatialCenterMethod = 0x162,
+	kSetCameraBoundsMethod = 0x163,
+	kXViewportPositionMethod = 0x164,
 	kYViewportPositionMethod = 0x165,
 	kPanToMethod = 0x172,
 
@@ -224,6 +235,8 @@ enum BuiltInMethod {
 	kSortMethod = 0x10A,
 
 	// PRINTER METHODS.
+	// NOTE: IDs 0x15A and 0x15B seem to be double-assigned
+	// between two camera methods and two printer methods.
 	kOpenLensMethod = 0x15A,
 	kCloseLensMethod = 0x15B,
 };
@@ -255,6 +268,7 @@ enum EventType {
 	kPathStoppedEvent = 0x21,
 	kTextInputEvent = 0x25,
 	kTextErrorEvent = 0x26,
+	kCameraPanStepEvent = 0x29,
 	kCameraPanEndEvent = 0x2A,
 	kCameraPanAbortEvent = 0x2B,
 	kContextLoadCompleteEvent = 0x2C,
diff --git a/engines/mediastation/mediastation.cpp b/engines/mediastation/mediastation.cpp
index 6970b0d6d4f..f187ed526d7 100644
--- a/engines/mediastation/mediastation.cpp
+++ b/engines/mediastation/mediastation.cpp
@@ -144,10 +144,6 @@ bool MediaStationEngine::isFirstGenerationEngine() {
 	return _versionInfo.major == 0;
 }
 
-void MediaStationEngine::addDirtyRect(const Common::Rect &rect) {
-	getRootStage()->invalidateRect(rect);
-}
-
 Common::Error MediaStationEngine::run() {
 	initDisplayManager();
 	initCursorManager();
diff --git a/engines/mediastation/mediastation.h b/engines/mediastation/mediastation.h
index 240f4171dd6..d5d20411980 100644
--- a/engines/mediastation/mediastation.h
+++ b/engines/mediastation/mediastation.h
@@ -86,7 +86,6 @@ public:
 
 	bool isFirstGenerationEngine();
 	void dispatchSystemEvents();
-	void addDirtyRect(const Common::Rect &rect);
 	void draw(bool dirtyOnly = true);
 
 	void registerActor(Actor *actorToAdd);
diff --git a/engines/mediastation/module.mk b/engines/mediastation/module.mk
index 7823a91d258..475e70fea9d 100644
--- a/engines/mediastation/module.mk
+++ b/engines/mediastation/module.mk
@@ -2,6 +2,7 @@ MODULE := engines/mediastation
 
 MODULE_OBJS = \
 	actor.o \
+	actors/camera.o \
 	actors/canvas.o \
 	actors/document.o \
 	actors/font.o \


Commit: 761bf9511afd93fb2eaddfcbf3693dbdea852640
    https://github.com/scummvm/scummvm/commit/761bf9511afd93fb2eaddfcbf3693dbdea852640
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2025-12-31T11:45:08-05:00

Commit Message:
MEDIASTATION: Add Mac detection entries and update some others

Changed paths:
    engines/mediastation/detection_tables.h


diff --git a/engines/mediastation/detection_tables.h b/engines/mediastation/detection_tables.h
index c57d7d621db..0da2c0fc4e6 100644
--- a/engines/mediastation/detection_tables.h
+++ b/engines/mediastation/detection_tables.h
@@ -25,13 +25,14 @@ const PlainGameDescriptor mediastationGames[] = {
 	{ "georgeshrinks", "George Shrinks Interactive Storybook" },
 	{ "mousecookie", "If You Give a Mouse a Cookie Interactive Storybook" },
 	{ "lionking", "Disney's Animated Storybook: The Lion King" },
+	{ "janpienkowski", "Jan Pieńkowski Haunted House" },
 	{ "lambchop", "Lamb Chop Loves Music" },
 	{ "frogprince", "Fractured Fairy Tales: The Frog Prince" },
 	{ "honeytree", "Disney's Animated Storybook: Winnie the Pooh and the Honey Tree" },
 	{ "notredame", "Disney's Animated Storybook: The Hunchback of Notre Dame" },
 	{ "puzzlecastle", "Puzzle Castle" },
 	{ "ibmcrayola", "IBM/Crayola Print Factory" },
-	{ "ibmcrayolaholiday", "IBM/Crayola Print Factory Holiday Activity Pack" },
+	{ "ibmcrayolaholiday", "IBM/Crayola Print Factory Holiday Activity" },
 	{ "101dalmatians", "Disney's Animated Storybook: 101 Dalmatians" },
 	{ "herculesasb", "Disney's Animated Storybook: Hercules" },
 	{ "pocahontas", "Disney's Animated Storybook: Pocahontas"},
@@ -52,6 +53,7 @@ const PlainGameDescriptor mediastationGames[] = {
 // (e.g. "v1.0/DE" or "v1.0/US") but others do not (e.g. "v1.1").
 const ADGameDescription gameDescriptions[] = {
 	// George Shrinks Interactive Storybook
+	// This title seems to be Windows-only.
 	{
 		"georgeshrinks",
 		"v1.0",
@@ -80,6 +82,19 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSTABLE,
 		GUIO1(GUIO_NOASPECT)
 	},
+	{
+		"mousecookie",
+		"v2.0",
+		AD_ENTRY3s(
+			"If You Give a Mouse a Cookie", "r:d23bc24ab6da4d97547a6f2c35946f12", 265297,
+			"BOOT.STM", "11d11b2067519d8368175cc8e8caa94f", 59454,
+			"100.CXT", "cac48b9bb5f327d035a831cd15f1688c", 1762032
+		),
+		Common::EN_USA,
+		Common::kPlatformMacintosh,
+		ADGF_UNSTABLE,
+		GUIO1(GUIO_NOASPECT)
+	},
 
 	// Disney's Animated Storybook: The Lion King
 	{
@@ -95,6 +110,47 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSTABLE,
 		GUIO1(GUIO_NOASPECT)
 	},
+	{
+		"lionking",
+		"v2.0/GB",
+		AD_ENTRY3s(
+			"The Lion King", "r:24a4a5f56188c78d2ef16cd1646379f6", 311245,
+			"BOOT.STM", "dd83fd1fb899b680f00c586404cc7b7c", 23610,
+			"100.CXT", "d5dc4d49df2ea6f2ff0aa33a3f385506", 1455740
+		),
+		Common::EN_GRB,
+		Common::kPlatformMacintosh,
+		ADGF_UNSTABLE,
+		GUIO1(GUIO_NOASPECT)
+	},
+
+	// Jan Pieńkowski Haunted House
+	{
+		"janpienkowski",
+		"v1.0",
+		AD_ENTRY3s(
+			"HAUNTED.EXE", "869bcbae21bcfd87a63474f9efb042df", 330336,
+			"BOOT.STM", "d01f338d6a3912056aff8f9022867bba", 137830,
+			"100.CXT", "6154f6efef3a8e54476c10d522dcec20", 1164800
+		),
+		Common::EN_USA,
+		Common::kPlatformWindows,
+		ADGF_UNSTABLE,
+		GUIO1(GUIO_NOASPECT)
+	},
+	{
+		"janpienkowski",
+		"v1.0",
+		AD_ENTRY3s(
+			"Haunted House", "r:9d4e1623d86d0474d3813e2a9ddf51e0", 266166,
+			"BOOT.STM", "d01f338d6a3912056aff8f9022867bba", 137830,
+			"100.CXT", "6154f6efef3a8e54476c10d522dcec20", 1164800
+		),
+		Common::EN_USA,
+		Common::kPlatformMacintosh,
+		ADGF_UNSTABLE,
+		GUIO1(GUIO_NOASPECT)
+	},
 
 	// Lamb Chop Loves Music
 	{
@@ -110,6 +166,19 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSTABLE,
 		GUIO1(GUIO_NOASPECT)
 	},
+	{
+		"lambchop",
+		"v1.0",
+		AD_ENTRY3s(
+			"Lamb Chop Loves Music", "r:5ae7f4d1c5c74470c8f09ca37238c33b", 265388,
+			"BOOT.STM", "c90200e52bcaad52524520d461caef2b", 29884,
+			"100.CXT", "ce40843604b8c52701694cd543072a88", 3253600
+		),
+		Common::EN_USA,
+		Common::kPlatformMacintosh,
+		ADGF_UNSTABLE,
+		GUIO1(GUIO_NOASPECT)
+	},
 
 	// Fractured Fairy Tales: The Frog Prince
 	{
@@ -125,6 +194,32 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSTABLE,
 		GUIO1(GUIO_NOASPECT)
 	},
+	{
+		"frogprince",
+		"v1.0",
+		AD_ENTRY3s(
+			"FPRINCE.EXE", "a8f3a63ef2032de300f46397cea625df", 518400,
+			"BOOT.STM", "1c6d14c87790d009702be8ba4e4e5906", 13652,
+			"100.CXT", "a5ec9a32c3741a20b82e1793e76234b2", 1630762
+		),
+		Common::EN_USA,
+		Common::kPlatformWindows,
+		ADGF_UNSTABLE,
+		GUIO1(GUIO_NOASPECT)
+	},
+	{
+		"frogprince",
+		"v1.0",
+		AD_ENTRY3s(
+			"The Frog Prince", "r:d77fd2a5dfb81a7fde03d175f93244bd", 1005600,
+			"BOOT.STM", "1c6d14c87790d009702be8ba4e4e5906", 13652,
+			"100.CXT", "a5ec9a32c3741a20b82e1793e76234b2", 1630762
+		),
+		Common::EN_USA,
+		Common::kPlatformMacintosh,
+		ADGF_UNSTABLE,
+		GUIO1(GUIO_NOASPECT)
+	},
 
 	// Disney's Animated Storybook: Winnie the Pooh and the Honey Tree
 	{
@@ -140,7 +235,19 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSTABLE,
 		GUIO1(GUIO_NOASPECT)
 	},
-
+	{
+		"honeytree",
+		"v2.0/US", // Also includes Spanish as an in-game language option.
+		AD_ENTRY3s(
+			"Pooh and the Honey Tree", "r:3f0077774418305ccd4c20cb6fa4c765", 764753,
+			"BOOT.STM", "9b9f528bf9c9b8ebe194b0c47dbe485e", 55422,
+			"100.CXT", "30f010077fd0489933989a562db81ad6", 1971940
+		),
+		Common::EN_USA,
+		Common::kPlatformMacintosh,
+		ADGF_UNSTABLE,
+		GUIO1(GUIO_NOASPECT)
+	},
 	{
 		"honeytree",
 		"v2.0/SE", // Also includes English as an in-game language option.
@@ -154,7 +261,6 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSTABLE,
 		GUIO1(GUIO_NOASPECT)
 	},
-
 	{
 		"honeytree",
 		"v2.0/SE", // Also includes English as an in-game language option.
@@ -183,6 +289,19 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSTABLE,
 		GUIO1(GUIO_NOASPECT)
 	},
+	{
+		"notredame",
+		"v1.0/US",
+		AD_ENTRY3s(
+			"Hunchback Animated StoryBook", "r:711b5eba6477f801177cd9095a51e1a5", 1044920,
+			"BOOT.STM", "7949e1253a62531e53963a2fffe57211", 55300,
+			"100.CXT", "54c11a94888a1b747e1c8935b7315889", 4766278
+		),
+		Common::EN_USA,
+		Common::kPlatformMacintosh,
+		ADGF_UNSTABLE,
+		GUIO1(GUIO_NOASPECT)
+	},
 
 	// Puzzle Castle
 	{
@@ -198,6 +317,19 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSTABLE,
 		GUIO1(GUIO_NOASPECT)
 	},
+	{
+		"puzzlecastle",
+		"v1.0",
+		AD_ENTRY3s(
+			"Puzzle Castle", "r:aa2c5c12ec9e0d3cba54efcbd143f8e0", 959690,
+			"BOOT.STM", "7b0faf38da2d76df40b4085eed6f4fc8", 22080,
+			"100.CXT", "ebc4b6247b742733c81456dfd299aa55", 3346944
+		),
+		Common::EN_USA,
+		Common::kPlatformMacintosh,
+		ADGF_UNSTABLE,
+		GUIO1(GUIO_NOASPECT)
+	},
 	{
 		"puzzlecastle",
 		"v1.0 Demo",
@@ -211,6 +343,19 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSTABLE | ADGF_DEMO,
 		GUIO1(GUIO_NOASPECT)
 	},
+	{
+		"puzzlecastle",
+		"v1.0 Demo",
+		AD_ENTRY3s(
+			"Puzzle Castle Demo", "r:f9f4715f39e084ac80e6b222cb69b20e", 959563,
+			"BOOT.STM", "b7ce005e0d67021f792ebb73e7fbe34c", 5960,
+			"100.CXT", "cc64a6fcb3af2736d622658cff3ef2b5", 1262
+		),
+		Common::EN_USA,
+		Common::kPlatformMacintosh,
+		ADGF_UNSTABLE | ADGF_DEMO,
+		GUIO1(GUIO_NOASPECT)
+	},
 
 	// IBM/Crayola Print Factory
 	{
@@ -226,6 +371,19 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSTABLE,
 		GUIO1(GUIO_NOASPECT)
 	},
+	{
+		"ibmcrayola",
+		"v1.0/US",
+		AD_ENTRY3s(
+			"IBM and Crayola Print Factory", "r:f30fbc530b26feb0ed23164e632c1a88", 1583870,
+			"BOOT.STM", "359542015c6665c70252cf21a8467cdb", 11044,
+			"100.CXT", "42bffe4165640dd1e64a6e8565f48af3", 5125226
+		),
+		Common::EN_USA,
+		Common::kPlatformMacintosh,
+		ADGF_UNSTABLE,
+		GUIO1(GUIO_NOASPECT)
+	},
 
 	// IBM/Crayola Print Factory Holiday Activity Pack
 	{
@@ -241,6 +399,19 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSTABLE,
 		GUIO1(GUIO_NOASPECT)
 	},
+	{
+		"ibmcrayolaholiday",
+		"v1.0/US",
+		AD_ENTRY3s(
+			"HOLIDAY.EXE", "r:bf5345cf76a4ee24823634b420739771", 1576023,
+			"BOOT.STM", "50f30298bf700f357d98c4390f75cb7a", 10932,
+			"100.CXT", "8110f70f1d01d0f42cac9b1bb6d2de12", 4967390
+		),
+		Common::EN_USA,
+		Common::kPlatformMacintosh,
+		ADGF_UNSTABLE,
+		GUIO1(GUIO_NOASPECT)
+	},
 
 	// Disney's Animated Storybook: 101 Dalmatians
 	{
@@ -260,7 +431,7 @@ const ADGameDescription gameDescriptions[] = {
 		"101dalmatians",
 		"v1.0/US",
 		AD_ENTRY3s(
-			"101 Dalmatians StoryBook", "1611f83747b3ac4dd33c8b866535e425", 1046272,
+			"101 Dalmatians StoryBook", "r:9ec3f0b3fd6c28650e00475b6bf08615", 1042456,
 			"BOOT.STM", "ee6725a718cbce640d02acec2b84825f", 47970,
 			"100.CXT", "2df853283a3fd2d079b06bc27b50527f", 6784502
 		),
@@ -284,6 +455,19 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSTABLE,
 		GUIO1(GUIO_NOASPECT)
 	},
+	{
+		"herculesasb",
+		"v1.0/US",
+		AD_ENTRY3s(
+			"Hercules Animated Storybook", "r:d870f6a3ab564f25428c7a7179cc1e53", 1173749,
+			"BOOT.STM", "afc773416e46e30873f743e234794957", 26924,
+			"100.CXT", "56875e1640320909e9697f11b5a8c9a6", 4895998
+		),
+		Common::EN_USA,
+		Common::kPlatformMacintosh,
+		ADGF_UNSTABLE,
+		GUIO1(GUIO_NOASPECT)
+	},
 
 	// Disney's Animated Storybook: Pocahontas
 	{
@@ -299,8 +483,6 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSTABLE,
 		GUIO1(GUIO_NOASPECT)
 	},
-
-	// Disney's Animated Storybook: Pocahontas
 	{
 		"pocahontas",
 		"v1.0/SE", // Also includes English as an in-game language option
@@ -314,6 +496,32 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSTABLE,
 		GUIO1(GUIO_NOASPECT)
 	},
+	{
+		"pocahontas",
+		"v1.1.01 English",
+		AD_ENTRY3s(
+			"POCA_ASB.EXE", "0287ca22a971d305e9a67fc4fc87239d", 367552,
+			"BOOT.STM", "d35338f9f014b73a8471523f3b141f2d", 324138,
+			"100.CXT", "8d7b65d4c50515c8b28a91980017d327", 1107900
+		),
+		Common::EN_USA,
+		Common::kPlatformWindows,
+		ADGF_UNSTABLE,
+		GUIO1(GUIO_NOASPECT)
+	},
+	{
+		"pocahontas",
+		"v1.1.01 English",
+		AD_ENTRY3s(
+			"Pocahontas", "r:6b0d52f286954d7dbb5fd703ff696b40", 319909,
+			"BOOT.STM", "d35338f9f014b73a8471523f3b141f2d", 324138,
+			"100.CXT", "8d7b65d4c50515c8b28a91980017d327", 1107900
+		),
+		Common::EN_USA,
+		Common::kPlatformMacintosh,
+		ADGF_UNSTABLE,
+		GUIO1(GUIO_NOASPECT)
+	},
 
 	// Magic Fairy Tales: Barbie as Rapunzel
 	{
@@ -329,6 +537,19 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSTABLE,
 		GUIO1(GUIO_NOASPECT)
 	},
+	{
+		"barbieasrapunzel",
+		"v1.0",
+		AD_ENTRY3s(
+			"Rapunzel", "r:b8b5fc7def85c07ed4c673e8a80ed338", 1043127,
+			"BOOT.STM", "eef6bdf54d2ae25af0ec29361fd4c126", 17530,
+			"100.CXT", "f0bcc27b61bfb33328db2dd537b2b6e3", 1688902
+		),
+		Common::EN_USA,
+		Common::kPlatformMacintosh,
+		ADGF_UNSTABLE,
+		GUIO1(GUIO_NOASPECT)
+	},
 
 	// Tonka Search and Rescue
 	{
@@ -344,6 +565,19 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSTABLE,
 		GUIO1(GUIO_NOASPECT)
 	},
+	{
+		"tonkasearchandrescue",
+		"v1.0/US",
+		AD_ENTRY3s(
+			"Tonka Search and Rescue", "r:432c202bcc7b259c2927e0f6fb1bbd64", 567011,
+			"BOOT.STM", "90c5f17734219c3a442316d21e6833f8", 25362,
+			"100.CXT", "85a05487b6c499ba3ce86d043305ddfd", 6410562
+		),
+		Common::EN_USA,
+		Common::kPlatformMacintosh,
+		ADGF_UNSTABLE,
+		GUIO1(GUIO_NOASPECT)
+	},
 
 	// Disney presents Ariel's Story Studio
 	{
@@ -359,6 +593,19 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSTABLE,
 		GUIO1(GUIO_NOASPECT)
 	},
+	{
+		"arielstorystudio",
+		"v1.0/US",
+		AD_ENTRY3s(
+			"Ariel's Story Studio", "r:7a21e3080828b50c43e4b7805617db9c", 1523037,
+			"BOOT.STM", "297670b908f887ed6c97b364406575d0", 65480,
+			"100.CXT", "c12c5b784ad931eca293a9816c11043b", 6532022
+		),
+		Common::EN_USA,
+		Common::kPlatformMacintosh,
+		ADGF_UNSTABLE,
+		GUIO1(GUIO_NOASPECT)
+	},
 	{
 		"arielstorystudio",
 		"v1.1/US",
@@ -374,11 +621,12 @@ const ADGameDescription gameDescriptions[] = {
 	},
 
 	// Tonka Garage
+	// This title seems to be Windows-only.
 	{
 		"tonkagarage",
 		"v1.1/US",
 		AD_ENTRY3s(
-			"TONKA_GR.EXE", "4e7e75ac11c996454b334f9add38c691", 1297408,
+			"TONKA_GR.EXE", "4e7e75ac11c996454b334f9add38c691", 1297408, // 32-bit (PE)
 			"BOOT.STM", "fc8863bb302e94d3b778b3a97556601b", 25208,
 			"100.CXT", "13683c2a06275920181d9dda5b2b69e7", 2691398
 		),
@@ -402,8 +650,35 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSTABLE,
 		GUIO1(GUIO_NOASPECT)
 	},
+	{
+		"dwpickyeater",
+		"v1.0/US (16-bit)",
+		AD_ENTRY3s(
+			"DW_16.EXE", "56553be9eee6130b4458fe9dfa2bcacf", 1007184,
+			"BOOT.STM", "80cc94e3e894ee8c5a22a9c07a33d891", 26402,
+			"100.CXT", "e65e359ab25d7a639cf369a01b9a21c0", 2163750
+		),
+		Common::EN_USA,
+		Common::kPlatformWindows,
+		ADGF_UNSTABLE,
+		GUIO1(GUIO_NOASPECT)
+	},
+	{
+		"dwpickyeater",
+		"v1.0/US",
+		AD_ENTRY3s(
+			"D.W. the Picky Eater", "r:4f85d13c1d241ffd37787884d0da2aef", 2502801,
+			"BOOT.STM", "80cc94e3e894ee8c5a22a9c07a33d891", 26402,
+			"100.CXT", "e65e359ab25d7a639cf369a01b9a21c0", 2163750
+		),
+		Common::EN_USA,
+		Common::kPlatformMacintosh,
+		ADGF_UNSTABLE,
+		GUIO1(GUIO_NOASPECT)
+	},
 
 	// Tonka Workshop
+	// This title seems to be Windows-only.
 	{
 		"tonkaworkshop",
 		"v1.0/US",
@@ -419,11 +694,12 @@ const ADGameDescription gameDescriptions[] = {
 	},
 
 	// Tonka Raceway
+	// This title seems to be Windows-only.
 	{
 		"tonkaraceway",
 		"v1.0/US",
 		AD_ENTRY3s(
-			"TONKA_RA.EXE", "cccd33d4d9e824bada6a1ca115794226", 1735680, // 32-bit (PE)
+			"TONKA_RACEWAY.EXE", "cccd33d4d9e824bada6a1ca115794226", 1735680, // 32-bit (PE)
 			"BOOT.STM", "da512cb9bcd18465294e544ed790881c", 12272,
 			"100.CXT", "30802327b29fbfa722a707c3d3b0f8f8", 2691398
 		),
@@ -434,6 +710,7 @@ const ADGameDescription gameDescriptions[] = {
 	},
 
 	// Stuart Little: Big City Adventures
+	// This title seems to be Windows-only.
 	{
 		"stuartlittlebigcity",
 		"v1.0/US",




More information about the Scummvm-git-logs mailing list