[Scummvm-git-logs] scummvm master -> 2db0e59a8e16e9256bad1d2916b2c9a55b4f8d10

npjg noreply at scummvm.org
Fri May 1 00:37:24 UTC 2026


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

Summary:
7222628738 MEDIASTATION: Add more flexibility when interpreting time script types
3d502ba4df MEDIASTATION: Make code chunk streaming more accurate to the original
8df86f1ad6 MEDIASTATION: Handle image decompression caching more accurately to the original
d71a1ef8a8 MEDIASTATION: Fix reading BOOT.STM for later engine versions
c90634a53d MEDIASTATION: Stub out script functions for Hercules/Hunchback
4e6a6d4617 MEDIASTATION: Fix incorrectly read cursor actor field
d328b0f44d MEDIASTATION: Update debug statements and levels
666fb78f68 MEDIASTATION: Implement Disk Image actor
3717f7f29d MEDIASTATION: Implement hotspot bounds checking more accurately to the original
a5cde81c4b MEDIASTATION: Split out ImtGod from main engine class
2db0e59a8e MEDIASTATION: Rewrite event subsystem for accuracy


Commit: 72226287389fedc0943cc81ea7ce9b0106fc4044
    https://github.com/scummvm/scummvm/commit/72226287389fedc0943cc81ea7ce9b0106fc4044
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-04-30T20:19:20-04:00

Commit Message:
MEDIASTATION: Add more flexibility when interpreting time script types

Changed paths:
    engines/mediastation/actors/camera.cpp
    engines/mediastation/actors/path.cpp
    engines/mediastation/graphics.cpp
    engines/mediastation/mediascript/scriptvalue.cpp
    engines/mediastation/mediascript/scriptvalue.h


diff --git a/engines/mediastation/actors/camera.cpp b/engines/mediastation/actors/camera.cpp
index d253950829b..81282176205 100644
--- a/engines/mediastation/actors/camera.cpp
+++ b/engines/mediastation/actors/camera.cpp
@@ -120,7 +120,7 @@ ScriptValue CameraActor::callMethod(BuiltInMethod methodId, Common::Array<Script
 		ARGCOUNTCHECK(3);
 		int16 deltaX = static_cast<uint16>(args[0].asFloat());
 		int16 deltaY = static_cast<int16>(args[1].asFloat());
-		double duration = args[2].asTime();
+		double duration = args[2].asFloatOrTime();
 		_nextViewportOrigin = Common::Point(deltaX, deltaY) + _currentViewportOrigin;
 		adjustCameraViewport(_nextViewportOrigin);
 		startPan(deltaX, deltaY, duration);
@@ -227,10 +227,10 @@ ScriptValue CameraActor::callMethod(BuiltInMethod methodId, Common::Array<Script
 
 		if (args.size() == 4) {
 			uint panSteps = static_cast<uint>(args[2].asFloat());
-			double duration = args[3].asFloat();
+			double duration = args[3].asFloatOrTime();
 			panToByStepCount(x, y, panSteps, duration);
 		} else {
-			double duration = args[2].asFloat();
+			double duration = args[2].asFloatOrTime();
 			panToByTime(x, y, duration);
 		}
 		break;
diff --git a/engines/mediastation/actors/path.cpp b/engines/mediastation/actors/path.cpp
index 5b7c5b2e87d..f4996e64ba3 100644
--- a/engines/mediastation/actors/path.cpp
+++ b/engines/mediastation/actors/path.cpp
@@ -123,7 +123,8 @@ ScriptValue PathActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptVa
 		ARGCOUNTCHECK(1);
 		// Convert from seconds to milliseconds.
 		const uint MILLISECONDS_IN_ONE_SECOND = 1000;
-		_duration = args[0].asTime() * MILLISECONDS_IN_ONE_SECOND;
+		double durationAsFractionalSeconds = args[0].asFloatOrTime();
+		_duration = durationAsFractionalSeconds * MILLISECONDS_IN_ONE_SECOND;
 		_useTimeForCompletion = true;
 		break;
 	}
diff --git a/engines/mediastation/graphics.cpp b/engines/mediastation/graphics.cpp
index 75891668563..eb7bdee1b6f 100644
--- a/engines/mediastation/graphics.cpp
+++ b/engines/mediastation/graphics.cpp
@@ -346,7 +346,7 @@ void VideoDisplayManager::fadeToBlack(Common::Array<ScriptValue> &args) {
 	uint colorCount = DEFAULT_PALETTE_TRANSITION_COLOR_COUNT;
 
 	if (args.size() >= 2) {
-		fadeTime = args[1].asTime();
+		fadeTime = args[1].asFloatOrTime();
 	}
 	if (args.size() >= 4) {
 		startIndex = static_cast<uint>(args[2].asFloat());
@@ -363,7 +363,7 @@ void VideoDisplayManager::fadeToRegisteredPalette(Common::Array<ScriptValue> &ar
 	uint colorCount = DEFAULT_PALETTE_TRANSITION_COLOR_COUNT;
 
 	if (args.size() >= 2) {
-		fadeTime = args[1].asTime();
+		fadeTime = args[1].asFloatOrTime();
 	}
 	if (args.size() >= 4) {
 		startIndex = static_cast<uint>(args[2].asFloat());
@@ -410,10 +410,10 @@ void VideoDisplayManager::fadeToColor(Common::Array<ScriptValue> &args) {
 		r = static_cast<byte>(args[1].asFloat());
 		g = static_cast<byte>(args[2].asFloat());
 		b = static_cast<byte>(args[3].asFloat());
-		fadeTime = args[4].asTime();
+		fadeTime = args[4].asFloatOrTime();
 	}
 	if (args.size() >= 7) {
-		fadeTime = args[5].asTime();
+		fadeTime = args[5].asFloatOrTime();
 		startIndex = static_cast<uint>(args[6].asFloat());
 		colorCount = static_cast<uint>(args[7].asFloat());
 	}
@@ -471,7 +471,7 @@ void VideoDisplayManager::fadeToPaletteObject(Common::Array<ScriptValue> &args)
 		return;
 	}
 	if (args.size() >= 3) {
-		fadeTime = args[2].asFloat();
+		fadeTime = args[2].asFloatOrTime();
 	}
 	if (args.size() >= 5) {
 		startIndex = static_cast<uint>(args[3].asFloat());
diff --git a/engines/mediastation/mediascript/scriptvalue.cpp b/engines/mediastation/mediascript/scriptvalue.cpp
index c673cf2f9f5..42deb5b66b0 100644
--- a/engines/mediastation/mediascript/scriptvalue.cpp
+++ b/engines/mediastation/mediascript/scriptvalue.cpp
@@ -197,6 +197,17 @@ double ScriptValue::asFloat() const {
 	}
 }
 
+double ScriptValue::asFloatOrTime() const {
+	if (_type == kScriptValueTypeFloat || _type == kScriptValueTypeTime) {
+		return _u.d;
+	} else {
+		// Times usually appear as floats (not the other way around), so
+		// for the one log we will use the time type.
+		VALUETYPEMISMATCH(kScriptValueTypeTime);
+		return 0.0;
+	}
+}
+
 void ScriptValue::setToBool(bool b) {
 	_type = kScriptValueTypeBool;
 	_u.b = b;
@@ -351,7 +362,7 @@ bool ScriptValue::compare(Opcode op, const ScriptValue &lhs, const ScriptValue &
 		break;
 
 	case kScriptValueTypeFloat:
-		result = compare(op, lhs.asFloat(), rhs.asFloat());
+		result = compare(op, lhs.asFloatOrTime(), rhs.asFloatOrTime());
 		break;
 
 	case kScriptValueTypeBool:
@@ -359,7 +370,7 @@ bool ScriptValue::compare(Opcode op, const ScriptValue &lhs, const ScriptValue &
 		break;
 
 	case kScriptValueTypeTime:
-		result = compare(op, lhs.asTime(), rhs.asTime());
+		result = compare(op, lhs.asFloatOrTime(), rhs.asFloatOrTime());
 		break;
 
 	case kScriptValueTypeParamToken:
diff --git a/engines/mediastation/mediascript/scriptvalue.h b/engines/mediastation/mediascript/scriptvalue.h
index e247ec55f06..cbd366d5985 100644
--- a/engines/mediastation/mediascript/scriptvalue.h
+++ b/engines/mediastation/mediascript/scriptvalue.h
@@ -45,7 +45,9 @@ public:
 	void setToFloat(int i);
 	void setToFloat(double d);
 	double asFloat() const;
-	int asIntFromFloat() const;
+	// Some transitions expect either a float or a time depending on engine version.
+	// To keep things simple, we have a method to try both.
+	double asFloatOrTime() const;
 
 	void setToBool(bool b);
 	bool asBool() const;


Commit: 3d502ba4dfbd093db2432ab6c035be49346cec77
    https://github.com/scummvm/scummvm/commit/3d502ba4dfbd093db2432ab6c035be49346cec77
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-04-30T20:19:20-04:00

Commit Message:
MEDIASTATION: Make code chunk streaming more accurate to the original

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


diff --git a/engines/mediastation/mediascript/codechunk.cpp b/engines/mediastation/mediascript/codechunk.cpp
index e64b8841d9b..5cb19587846 100644
--- a/engines/mediastation/mediascript/codechunk.cpp
+++ b/engines/mediastation/mediascript/codechunk.cpp
@@ -28,12 +28,6 @@
 
 namespace MediaStation {
 
-CodeChunk::CodeChunk(Chunk &chunk) {
-	uint lengthInBytes = chunk.readTypedUint32();
-	debugC(5, kDebugLoading, "CodeChunk::CodeChunk(): Length 0x%x (@0x%llx)", lengthInBytes, static_cast<long long int>(chunk.pos()));
-	_bytecode = static_cast<ParameterReadStream *>(chunk.readStream(lengthInBytes));
-}
-
 ScriptValue CodeChunk::executeNextBlock() {
 	uint blockSize = _bytecode->readTypedUint32();
 	int64 startingPos = _bytecode->pos();
@@ -52,7 +46,8 @@ ScriptValue CodeChunk::executeNextBlock() {
 		}
 	}
 
-	// Verify we consumed the right number of script bytes.
+	// Verify we consumed the right number of script bytes. This is not in the original,
+	// but it's a very useful sanity check.
 	if (!_returnImmediately) {
 		uint bytesRead = _bytecode->pos() - startingPos;
 		if (bytesRead != blockSize) {
@@ -68,19 +63,16 @@ void CodeChunk::skipNextBlock() {
 	_bytecode->skip(lengthInBytes);
 }
 
-ScriptValue CodeChunk::execute(Common::Array<ScriptValue> *args) {
+ScriptValue CodeChunk::executeWithArguments(Common::Array<ScriptValue> *args) {
+	// Only functions have this call depth requirement.
+	if (g_engine->getFunctionManager()->_scriptBlockCallDepth >= MAX_CALL_DEPTH) {
+		error("%s: Exceeded max call stack depth", __func__);
+	}
+
+	g_engine->getFunctionManager()->_scriptBlockCallDepth++;
 	_args = args;
 	ScriptValue returnValue = executeNextBlock();
-
-	// 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.
-	_args = nullptr;
-
+	g_engine->getFunctionManager()->_scriptBlockCallDepth--;
 	return returnValue;
 }
 
@@ -630,10 +622,8 @@ void CodeChunk::evaluateWhileLoop() {
 CodeChunk::~CodeChunk() {
 	_locals.clear();
 
-	// We don't own the args, so we don't need to delete it.
+	// We don't own the args or the code stream, so we don't need to delete them.
 	_args = nullptr;
-
-	delete _bytecode;
 	_bytecode = nullptr;
 }
 
diff --git a/engines/mediastation/mediascript/codechunk.h b/engines/mediastation/mediascript/codechunk.h
index 1959502227b..c0dc70f0f8a 100644
--- a/engines/mediastation/mediascript/codechunk.h
+++ b/engines/mediastation/mediascript/codechunk.h
@@ -32,13 +32,17 @@ namespace MediaStation {
 
 class CodeChunk {
 public:
-	CodeChunk(Chunk &chunk);
+	CodeChunk(ParameterReadStream *bytecode) : _bytecode(bytecode) {};
 	~CodeChunk();
 
 	ScriptValue executeNextBlock();
-	ScriptValue execute(Common::Array<ScriptValue> *args = nullptr);
+	ScriptValue executeWithArguments(Common::Array<ScriptValue> *args);
 
 private:
+	// This is not the number of recursive calls, it is as far is the script call stack is
+	// ever allowed to get.
+	static const uint MAX_CALL_DEPTH = 0x0f;
+
 	void skipNextBlock();
 
 	ScriptValue evaluateExpression();
diff --git a/engines/mediastation/mediascript/function.cpp b/engines/mediastation/mediascript/function.cpp
index 1ec1719b17d..44a85e118c9 100644
--- a/engines/mediastation/mediascript/function.cpp
+++ b/engines/mediastation/mediascript/function.cpp
@@ -19,6 +19,8 @@
  *
  */
 
+#include "common/memstream.h"
+
 #include "mediastation/mediascript/function.h"
 #include "mediastation/debugchannels.h"
 #include "mediastation/mediastation.h"
@@ -46,19 +48,33 @@ namespace MediaStation {
 ScriptFunction::ScriptFunction(Chunk &chunk) {
 	_contextId = chunk.readTypedUint16();
 	_id = chunk.readTypedUint16();
-	_code = new CodeChunk(chunk);
+	_bytecodeSize = chunk.readTypedUint32();
+	debugC(5, kDebugLoading, "%s: Context %d, function %d [%d bytes]",
+		__func__, _contextId, _id, _bytecodeSize);
+
+	// Store bytecode as a flat buffer rather than a stream, so we can create
+	// fresh streams for each execution (necessary for recursive function calls).
+	_bytecodeBuffer = static_cast<byte *>(malloc(_bytecodeSize));
+	chunk.read(_bytecodeBuffer, _bytecodeSize);
 }
 
 ScriptFunction::~ScriptFunction() {
-	delete _code;
-	_code = nullptr;
+	free(_bytecodeBuffer);
+	_bytecodeBuffer = nullptr;
 }
 
 ScriptValue ScriptFunction::execute(Common::Array<ScriptValue> &args) {
 	Common::String name = g_engine->formatFunctionName(_id);
 	debugC(5, kDebugScript, "\n********** SCRIPT FUNCTION %s **********", name.c_str());
-	ScriptValue returnValue = _code->execute(&args);
-	debugC(5, kDebugScript, "********** END SCRIPT FUNCTION **********");
+
+	// Create a new stream for this execution to avoid conflicts with recursive calls.
+	Common::SeekableReadStream *baseStream = new Common::MemoryReadStream(_bytecodeBuffer, _bytecodeSize, DisposeAfterUse::NO);
+	ParameterReadStream *bytecodeStream = static_cast<ParameterReadStream *>(baseStream);
+	CodeChunk code(bytecodeStream);
+	ScriptValue returnValue = code.executeWithArguments(&args);
+	delete bytecodeStream;
+
+	debugC(5, kDebugScript, "********** END SCRIPT FUNCTION %s **********", name.c_str());
 	return returnValue;
 }
 
diff --git a/engines/mediastation/mediascript/function.h b/engines/mediastation/mediascript/function.h
index cf2a5c5a420..bca7836a788 100644
--- a/engines/mediastation/mediascript/function.h
+++ b/engines/mediastation/mediascript/function.h
@@ -48,7 +48,8 @@ public:
 	uint _id = 0;
 
 private:
-	CodeChunk *_code = nullptr;
+	byte *_bytecodeBuffer = nullptr;
+	uint32 _bytecodeSize = 0;
 };
 
 class FunctionManager : public ParameterClient {
@@ -60,6 +61,8 @@ public:
 	ScriptValue call(uint functionId, Common::Array<ScriptValue> &args);
 	void deleteFunctionsForContext(uint contextId);
 
+	uint _scriptBlockCallDepth = 0;
+
 private:
 	Common::HashMap<uint, ScriptFunction *> _functions;
 
diff --git a/engines/mediastation/mediascript/scriptresponse.cpp b/engines/mediastation/mediascript/scriptresponse.cpp
index f55e25afa6a..436126d2e7c 100644
--- a/engines/mediastation/mediascript/scriptresponse.cpp
+++ b/engines/mediastation/mediascript/scriptresponse.cpp
@@ -19,6 +19,8 @@
  *
  */
 
+#include "common/memstream.h"
+
 #include "mediastation/mediascript/scriptresponse.h"
 #include "mediastation/debugchannels.h"
 #include "mediastation/mediastation.h"
@@ -27,10 +29,15 @@ namespace MediaStation {
 
 ScriptResponse::ScriptResponse(Chunk &chunk) {
 	_type = static_cast<EventType>(chunk.readTypedUint16());
-	debugC(5, kDebugLoading, "%s: %s (%d)", __func__, eventTypeToStr(_type), static_cast<uint>(_type));
-
 	_argumentValue = ScriptValue(&chunk);
-	_code = new CodeChunk(chunk);
+	_bytecodeSize = chunk.readTypedUint32();
+	debugC(5, kDebugLoading, "%s: %s (%d) [%d bytes]",
+		__func__, eventTypeToStr(_type), static_cast<uint>(_type), _bytecodeSize);
+
+	// Store bytecode as a flat buffer rather than a stream, so we can create
+	// fresh streams for each execution (necessary for recursive function calls).
+	_bytecodeBuffer = static_cast<byte *>(malloc(_bytecodeSize));
+	chunk.read(_bytecodeBuffer, _bytecodeSize);
 }
 
 ScriptValue ScriptResponse::execute(uint actorId) {
@@ -41,16 +48,24 @@ ScriptValue ScriptResponse::execute(uint actorId) {
 	Common::String argValue = Common::String::format("(%s)", _argumentValue.getDebugString().c_str());
 	debugC(5, kDebugScript, "\n********** SCRIPT RESPONSE %s %s **********", actorAndType.c_str(), argValue.c_str());
 
-	// The only argument that can be provided to a script response is the argument value.
-	ScriptValue returnValue = _code->execute();
+	// Create a new stream for this execution to avoid conflicts with recursive calls.
+	Common::SeekableReadStream *baseStream = new Common::MemoryReadStream(_bytecodeBuffer, _bytecodeSize, DisposeAfterUse::NO);
+	ParameterReadStream *bytecodeStream = static_cast<ParameterReadStream *>(baseStream);
+	CodeChunk code(bytecodeStream);
+	ScriptValue returnValue = code.executeNextBlock();
+	delete bytecodeStream;
 
 	debugC(5, kDebugScript, "********** END SCRIPT RESPONSE %s %s **********", actorAndType.c_str(), argValue.c_str());
 	return returnValue;
 }
 
 ScriptResponse::~ScriptResponse() {
-	delete _code;
-	_code = nullptr;
+	free(_bytecodeBuffer);
+	_bytecodeBuffer = nullptr;
+}
+
+int64 ScriptResponse::lengthInBytes() const {
+	return _bytecodeSize;
 }
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/mediascript/scriptresponse.h b/engines/mediastation/mediascript/scriptresponse.h
index fb05e9e00b1..66185dacced 100644
--- a/engines/mediastation/mediascript/scriptresponse.h
+++ b/engines/mediastation/mediascript/scriptresponse.h
@@ -36,12 +36,14 @@ public:
 	ScriptResponse(Chunk &chunk);
 	~ScriptResponse();
 
+	int64 lengthInBytes() const;
 	ScriptValue execute(uint actorId);
 	EventType _type;
 	ScriptValue _argumentValue;
 
 private:
-	CodeChunk *_code = nullptr;
+	byte *_bytecodeBuffer = nullptr;
+	uint32 _bytecodeSize = 0;
 };
 
 } // End of namespace MediaStation


Commit: 8df86f1ad6a8340d893e2be6a0a3a622994b42a7
    https://github.com/scummvm/scummvm/commit/8df86f1ad6a8340d893e2be6a0a3a622994b42a7
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-04-30T20:19:20-04:00

Commit Message:
MEDIASTATION: Handle image decompression caching more accurately to the original

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


diff --git a/engines/mediastation/actors/image.cpp b/engines/mediastation/actors/image.cpp
index a60ddf48314..f6c4ec0e1f4 100644
--- a/engines/mediastation/actors/image.cpp
+++ b/engines/mediastation/actors/image.cpp
@@ -47,7 +47,7 @@ void ImageActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 		break;
 
 	case kActorHeaderLoadType:
-		_decompressImmediately = chunk.readTypedByte();
+		_decompressInPlace = chunk.readTypedByte();
 		break;
 
 	case kActorHeaderX:
@@ -116,7 +116,7 @@ Common::Rect ImageActor::getBbox() const {
 
 void ImageActor::readChunk(Chunk &chunk) {
 	ImageInfo bitmapHeader = ImageInfo(chunk);
-	_asset->bitmap = new PixMapImage(chunk, bitmapHeader);
+	_asset->bitmap = new PixMapImage(chunk, bitmapHeader, _decompressInPlace);
 }
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/actors/image.h b/engines/mediastation/actors/image.h
index 7ff77a56d20..7d70b8266d3 100644
--- a/engines/mediastation/actors/image.h
+++ b/engines/mediastation/actors/image.h
@@ -53,7 +53,7 @@ public:
 
 private:
 	Common::SharedPtr<ImageAsset> _asset;
-	bool _decompressImmediately = false;
+	bool _decompressInPlace = false;
 	int _xOffset = 0;
 	int _yOffset = 0;
 	uint _actorReference = 0;
diff --git a/engines/mediastation/actors/movie.cpp b/engines/mediastation/actors/movie.cpp
index 9544a659dfe..da44ac0a0ff 100644
--- a/engines/mediastation/actors/movie.cpp
+++ b/engines/mediastation/actors/movie.cpp
@@ -611,6 +611,7 @@ void StreamMovieActor::decompressIntoAuxImage(MovieFrame *frame) {
 	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, nullptr, &frame->keyframeImage->_image);
+	frame->keyframeImage->setCompressionType(kUncompressedBitmap);
 }
 
 void StreamMovieActorFrames::readImageData(Chunk &chunk) {
diff --git a/engines/mediastation/actors/sprite.cpp b/engines/mediastation/actors/sprite.cpp
index 45f9d7f5719..3c9b345bd29 100644
--- a/engines/mediastation/actors/sprite.cpp
+++ b/engines/mediastation/actors/sprite.cpp
@@ -33,8 +33,8 @@ Common::String SpriteMovieClip::getDebugString() const {
 	return Common::String::format("%s: [%d, %d]", g_engine->formatParamTokenName(id).c_str(), firstFrameIndex, lastFrameIndex);
 }
 
-SpriteFrame::SpriteFrame(Chunk &chunk, uint index, Common::Point offset, const ImageInfo &imageInfo) :
-	PixMapImage(chunk, imageInfo), _index(index), _origin(offset) {
+SpriteFrame::SpriteFrame(Chunk &chunk, uint index, Common::Point offset, const ImageInfo &imageInfo, bool decompressInPlace) :
+	PixMapImage(chunk, imageInfo, decompressInPlace), _index(index), _origin(offset) {
 	debugC(5, kDebugLoading, "%s: frame 0x%x", __func__, _index);
 }
 
@@ -65,7 +65,7 @@ void SpriteMovieActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramT
 		break;
 
 	case kActorHeaderLoadType:
-		_decompressImmediately = static_cast<bool>(chunk.readTypedByte());
+		_decompressInPlace = static_cast<bool>(chunk.readTypedByte());
 		break;
 
 	case kActorHeaderSpriteChunkCount:
@@ -332,7 +332,7 @@ void SpriteMovieActor::readChunk(Chunk &chunk) {
 	ImageInfo imageInfo(chunk);
 	uint index = chunk.readTypedUint16();
 	Common::Point offset = chunk.readTypedPoint();
-	SpriteFrame *frame = new SpriteFrame(chunk, index, offset, imageInfo);
+	SpriteFrame *frame = new SpriteFrame(chunk, index, offset, imageInfo, _decompressInPlace);
 	_asset->frames.push_back(frame);
 
 	// TODO: Are these in exactly reverse order? If we can just reverse the
diff --git a/engines/mediastation/actors/sprite.h b/engines/mediastation/actors/sprite.h
index 0311f82d115..93daeb20a2a 100644
--- a/engines/mediastation/actors/sprite.h
+++ b/engines/mediastation/actors/sprite.h
@@ -46,7 +46,7 @@ struct SpriteMovieClip {
 
 class SpriteFrame : public PixMapImage {
 public:
-	SpriteFrame(Chunk &chunk, uint index, Common::Point origin, const ImageInfo &imageInfo);
+	SpriteFrame(Chunk &chunk, uint index, Common::Point origin, const ImageInfo &imageInfo, bool decompressInPlace);
 
 	int _index = 0;
 	Common::Point _origin;
@@ -81,7 +81,7 @@ private:
 	const uint DEFAULT_FORWARD_CLIP_ID = 0x4B0;
 	const uint DEFAULT_BACKWARD_CLIP_ID = 0x4B1;
 
-	bool _decompressImmediately = false;
+	bool _decompressInPlace = false;
 	uint _frameRate = 0;
 	uint _actorReference = 0;
 	Common::HashMap<uint, SpriteMovieClip> _clips;
diff --git a/engines/mediastation/bitmap.cpp b/engines/mediastation/bitmap.cpp
index 21b92b658a3..1ffd4d64874 100644
--- a/engines/mediastation/bitmap.cpp
+++ b/engines/mediastation/bitmap.cpp
@@ -21,6 +21,7 @@
 
 #include "mediastation/bitmap.h"
 #include "mediastation/debugchannels.h"
+#include "mediastation/mediastation.h"
 
 namespace MediaStation {
 
@@ -33,7 +34,7 @@ ImageInfo::ImageInfo(Chunk &chunk) {
 		__func__, _imageDataStartOffset, static_cast<uint>(_compressionType), _stride);
 }
 
-PixMapImage::PixMapImage(Chunk &chunk, const ImageInfo &imageInfo) : _imageInfo(imageInfo) {
+PixMapImage::PixMapImage(Chunk &chunk, const ImageInfo &imageInfo, bool decompressInPlace) : _imageInfo(imageInfo) {
 	if (stride() < width()) {
 		warning("%s: Got stride less than width", __func__);
 	}
@@ -45,6 +46,9 @@ PixMapImage::PixMapImage(Chunk &chunk, const ImageInfo &imageInfo) : _imageInfo(
 	if (chunk.bytesRemaining() > 0) {
 		if (isCompressed()) {
 			_compressedStream = chunk.readStream(chunk.bytesRemaining());
+			if (decompressInPlace) {
+				decompress();
+			}
 		} else {
 			_image.create(stride(), height(), Graphics::PixelFormat::createFormatCLUT8());
 			if (getCompressionType() == kUncompressedTransparentBitmap)
@@ -60,7 +64,7 @@ PixMapImage::PixMapImage(Chunk &chunk, const ImageInfo &imageInfo) : _imageInfo(
 	}
 }
 
-PixMapImage::PixMapImage(const ImageInfo &imageInfo) : _imageInfo(imageInfo) {
+PixMapImage::PixMapImage(const ImageInfo &imageInfo, bool decompressInPlace) : _imageInfo(imageInfo) {
 	_image.create(stride(), height(), Graphics::PixelFormat::createFormatCLUT8());
 }
 
@@ -74,4 +78,20 @@ bool PixMapImage::isCompressed() const {
 		(getCompressionType() != kUncompressedTransparentBitmap);
 }
 
+void PixMapImage::decompress() {
+	if (getCompressionType() != kRle8BitmapCompression) {
+		return;
+	} else if (_compressedStream == nullptr) {
+		warning("%s: No compressed data to decompress", __func__);
+		return;
+	}
+
+	// Decompress the image and then delete the compressed stream.
+	_image = g_engine->getDisplayManager()->decompressRle8Bitmap(this, nullptr, nullptr);
+	delete _compressedStream;
+	_compressedStream = nullptr;
+
+	_imageInfo._compressionType = kUncompressedBitmap;
+}
+
 } // End of namespace MediaStation
diff --git a/engines/mediastation/bitmap.h b/engines/mediastation/bitmap.h
index d99004ac76b..844e897cea8 100644
--- a/engines/mediastation/bitmap.h
+++ b/engines/mediastation/bitmap.h
@@ -51,12 +51,13 @@ public:
 
 class PixMapImage {
 public:
-	PixMapImage(Chunk &chunk, const ImageInfo &imageInfo);
-	PixMapImage(const ImageInfo &imageInfo);
+	PixMapImage(Chunk &chunk, const ImageInfo &imageInfo, bool decompressInPlace = false);
+	PixMapImage(const ImageInfo &imageInfo, bool decompressInPlace = false);
 	virtual ~PixMapImage();
 
 	bool isCompressed() const;
 	BitmapCompressionType getCompressionType() const { return _imageInfo._compressionType; }
+	void setCompressionType(BitmapCompressionType compressionType) { _imageInfo._compressionType = compressionType; }
 	int16 width() const { return _imageInfo._dimensions.x; }
 	int16 height() const { return _imageInfo._dimensions.y; }
 	int16 stride() const { return _imageInfo._stride; }
@@ -66,6 +67,7 @@ public:
 
 private:
 	ImageInfo _imageInfo;
+	void decompress();
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/graphics.h b/engines/mediastation/graphics.h
index 1d7e8c533a6..95f609d5f4e 100644
--- a/engines/mediastation/graphics.h
+++ b/engines/mediastation/graphics.h
@@ -151,6 +151,11 @@ public:
 		const double dissolveFactor,
 		DisplayContext *displayContext);
 
+	Graphics::ManagedSurface decompressRle8Bitmap(
+		const PixMapImage *source,
+		const Graphics::ManagedSurface *keyFrame = nullptr,
+		const Common::Point *keyFrameOffset = nullptr);
+
 	void effectTransition(Common::Array<ScriptValue> &args);
 	void setTransitionOnSync(Common::Array<ScriptValue> &args) { _scheduledTransitionOnSync = args; }
 	void doTransitionOnSync();
@@ -193,10 +198,6 @@ private:
 		const Common::Point &destLocation,
 		const PixMapImage *source,
 		const Common::Array<Common::Rect> &dirtyRegion);
-	Graphics::ManagedSurface decompressRle8Bitmap(
-		const PixMapImage *source,
-		const Graphics::ManagedSurface *keyFrame = nullptr,
-		const Common::Point *keyFrameOffset = nullptr);
 	void dissolveBlitRectsClip(
 		Graphics::ManagedSurface *dest,
 		const Common::Point &destPos,


Commit: d71a1ef8a8fef0de2aa90d5b71878b24ecda93e0
    https://github.com/scummvm/scummvm/commit/d71a1ef8a8fef0de2aa90d5b71878b24ecda93e0
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-04-30T20:19:20-04:00

Commit Message:
MEDIASTATION: Fix reading BOOT.STM for later engine versions

Titles using these later engine versions still do not start, but this at least
lets BOOT.STM be read for them.

Changed paths:
    engines/mediastation/actor.cpp
    engines/mediastation/boot.cpp
    engines/mediastation/boot.h
    engines/mediastation/context.cpp
    engines/mediastation/context.h
    engines/mediastation/mediastation.cpp
    engines/mediastation/mediastation.h


diff --git a/engines/mediastation/actor.cpp b/engines/mediastation/actor.cpp
index f2e9c7cc3d2..eee059c7d22 100644
--- a/engines/mediastation/actor.cpp
+++ b/engines/mediastation/actor.cpp
@@ -142,6 +142,10 @@ void Actor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 		break;
 	}
 
+	case kActorHeaderActorName:
+		_debugName = chunk.readTypedString();
+		break;
+
 	default:
 		error("[%s] %s: Got unimplemented actor parameter 0x%x", debugName(), __func__, static_cast<uint>(paramType));
 	}
diff --git a/engines/mediastation/boot.cpp b/engines/mediastation/boot.cpp
index a8093c8b6f6..4ef577c3416 100644
--- a/engines/mediastation/boot.cpp
+++ b/engines/mediastation/boot.cpp
@@ -200,6 +200,20 @@ void MediaStationEngine::readDocumentInfoFromStream(Chunk &chunk, BootSectionTyp
 		_unk3 = chunk.readTypedUint16();
 		break;
 
+	case kBootParamTokenDeclaration: {
+		Common::String paramTokenName = chunk.readTypedString();
+		sectionType = static_cast<BootSectionType>(chunk.readTypedUint16());
+		if (sectionType != kBootParamTokenValue) {
+			warning("%s: Incorrect separator when reading engine resource type", __func__);
+		}
+
+		Common::String paramTokenValueStr = chunk.readTypedString();
+		ScriptValue paramTokenValue;
+		paramTokenValue.setToParamToken(atoi(paramTokenValueStr.c_str()));
+		_paramTokenDeclarations.setVal(paramTokenName, paramTokenValue);
+		break;
+	}
+
 	default:
 		// See if any registered parameter clients know how to
 		// handle this parameter.
diff --git a/engines/mediastation/boot.h b/engines/mediastation/boot.h
index c64647ec99b..2153ef0f9af 100644
--- a/engines/mediastation/boot.h
+++ b/engines/mediastation/boot.h
@@ -133,13 +133,13 @@ public:
 	Common::String _name;
 };
 
-class EngineResourceDeclaration {
+class ParamTokenDeclaration {
 public:
-	EngineResourceDeclaration(Common::String resourceName, int resourceId) : _name(resourceName), _id(resourceId) {};
-	EngineResourceDeclaration() {};
+	ParamTokenDeclaration(Common::String resourceName, Common::String resourceId) : _name(resourceName), _id(resourceId) {};
+	ParamTokenDeclaration() {};
 
 	Common::String _name;
-	int _id = 0;
+	Common::String _id;
 };
 
 enum BootSectionType {
@@ -149,8 +149,8 @@ enum BootSectionType {
 	kBootUnk1 = 0x0191,
 	kBootFunctionTableSize = 0x0192,
 	kBootUnk3 = 0x0193,
-	kBootEngineResource = 0x0bba,
-	kBootEngineResourceId = 0x0bbb,
+	kBootParamTokenDeclaration = 0x0bba,
+	kBootParamTokenValue = 0x0bbb,
 	kBootScreenReference = 0x0007,
 	kBootFileInfo = 0x000a,
 	kBootStreamInfo = 0x000b,
diff --git a/engines/mediastation/context.cpp b/engines/mediastation/context.cpp
index 7cd09806d01..36c0f62ca65 100644
--- a/engines/mediastation/context.cpp
+++ b/engines/mediastation/context.cpp
@@ -208,7 +208,7 @@ void MediaStationEngine::readHeaderSections(Subfile &subfile, Chunk &chunk) {
 	} while (!subfile.atEnd());
 }
 
-void MediaStationEngine::readContextNameData(Chunk &chunk) {
+void MediaStationEngine::readSetContextName(Chunk &chunk) {
 	uint contextId = chunk.readTypedUint16();
 	debugC(5, kDebugLoading, "%s: Context %d", __func__, contextId);
 	Context *context = _loadedContexts.getValOrDefault(contextId);
@@ -245,7 +245,7 @@ void MediaStationEngine::readCommandFromStream(Chunk &chunk, ContextSectionType
 		break;
 
 	case kContextNameData:
-		readContextNameData(chunk);
+		readSetContextName(chunk);
 		break;
 
 	default:
diff --git a/engines/mediastation/context.h b/engines/mediastation/context.h
index fe56a795508..4d4351def72 100644
--- a/engines/mediastation/context.h
+++ b/engines/mediastation/context.h
@@ -48,7 +48,7 @@ enum ContextSectionType {
 	kContextActorLoadComplete = 0x13,
 	kContextCreateVariableData = 0x14,
 	kContextFunctionSection = 0x31,
-	kContextNameData = 0xbb8
+	kContextNameData = 0xbb9
 };
 
 class Context {
diff --git a/engines/mediastation/mediastation.cpp b/engines/mediastation/mediastation.cpp
index 5d9d60ac9f0..b608507dbf5 100644
--- a/engines/mediastation/mediastation.cpp
+++ b/engines/mediastation/mediastation.cpp
@@ -92,7 +92,7 @@ MediaStationEngine::~MediaStationEngine() {
 
 	_contextReferences.clear();
 	_streamMap.clear();
-	_engineResourceDeclarations.clear();
+	_paramTokenDeclarations.clear();
 	_screenReferences.clear();
 	_fileMap.clear();
 	_actors.clear();
diff --git a/engines/mediastation/mediastation.h b/engines/mediastation/mediastation.h
index 43246c715d3..4d49b35ab63 100644
--- a/engines/mediastation/mediastation.h
+++ b/engines/mediastation/mediastation.h
@@ -168,7 +168,7 @@ private:
 	Common::HashMap<uint, ScreenReference> _screenReferences;
 	Common::HashMap<uint, FileInfo> _fileMap;
 	Common::HashMap<uint, StreamInfo> _streamMap;
-	Common::HashMap<uint, EngineResourceDeclaration> _engineResourceDeclarations;
+	Common::HashMap<Common::String, ScriptValue> _paramTokenDeclarations;
 	uint _unk1 = 0;
 	uint _functionTableSize = 0;
 	uint _unk3 = 0;
@@ -202,7 +202,7 @@ private:
 	void readDestroyActorData(Chunk &chunk);
 	void readActorLoadComplete(Chunk &chunk);
 	void readCreateVariableData(Chunk &chunk);
-	void readContextNameData(Chunk &chunk);
+	void readSetContextName(Chunk &chunk);
 
 	void destroyActorsInContext(uint contextId);
 };


Commit: c90634a53d9ba28007baa581d94d73de2d18783a
    https://github.com/scummvm/scummvm/commit/c90634a53d9ba28007baa581d94d73de2d18783a
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-04-30T20:19:20-04:00

Commit Message:
MEDIASTATION: Stub out script functions for Hercules/Hunchback

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


diff --git a/engines/mediastation/actor.h b/engines/mediastation/actor.h
index ef54dd7b2fb..70ff722086f 100644
--- a/engines/mediastation/actor.h
+++ b/engines/mediastation/actor.h
@@ -131,8 +131,8 @@ enum ActorHeaderSectionType {
 	kActorHeaderTextCursorIsVisible = 0x262,
 	kActorHeaderTextConstrainToWidth = 0x263,
 	kActorHeaderTextOverwriteMode = 0x264,
-	kActorHeaderTextAcceptedCharRange = 0x265,
-	kActorHeaderTextAcceptedCharRangeWithOffset = 0x0266,
+	kActorHeaderTextAcceptedCharRangeWithOffset = 0x265,
+	kActorHeaderTextAcceptedCharRange = 0x0266,
 
 	// SPRITE FIELDS.
 	kActorHeaderSpriteClip = 0x03e9,
diff --git a/engines/mediastation/mediascript/function.cpp b/engines/mediastation/mediascript/function.cpp
index 44a85e118c9..ff58e2e3167 100644
--- a/engines/mediastation/mediascript/function.cpp
+++ b/engines/mediastation/mediascript/function.cpp
@@ -20,6 +20,7 @@
  */
 
 #include "common/memstream.h"
+#include "common/str.h"
 
 #include "mediastation/mediascript/function.h"
 #include "mediastation/debugchannels.h"
@@ -203,6 +204,7 @@ ScriptValue FunctionManager::call(uint functionId, Common::Array<ScriptValue> &a
 		break;
 
 	case kGetRegistryFunction:
+		FUNCARGCHECK(3);
 		script_GetRegistry(args, returnValue);
 		break;
 
@@ -234,6 +236,10 @@ ScriptValue FunctionManager::call(uint functionId, Common::Array<ScriptValue> &a
 		script_Drawing(args, returnValue);
 		break;
 
+	case kCheckersFunction:
+		script_Checkers(args, returnValue);
+		break;
+
 	case kLegacy_DebugPrintFunction:
 		// We don't need to check arg counts here. This just prints however many args we have.
 		script_DebugPrint(args, returnValue);
@@ -415,8 +421,8 @@ void FunctionManager::script_GetUniqueRandom(Common::Array<ScriptValue> &args, S
 
 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;
+	const uint32 MILLISECONDS_IN_ONE_SECOND = 1000;
+	double runtimeInSeconds = g_system->getMillis() / static_cast<double>(MILLISECONDS_IN_ONE_SECOND);
 	returnValue.setToFloat(runtimeInSeconds);
 }
 
@@ -535,19 +541,25 @@ void FunctionManager::script_GetAudioVolume(Common::Array<ScriptValue> &args, Sc
 }
 
 void FunctionManager::script_SystemLanguagePreference(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
-	warning("STUB: SystemLanguagePreference");
+	warning("STUB: %s", __func__);
 }
 
 void FunctionManager::script_SetRegistry(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
-	warning("STUB: SetRegistry");
+	warning("STUB: %s", __func__);
 }
 
 void FunctionManager::script_GetRegistry(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
-	warning("STUB: GetRegistry");
+	// Even though this is basically still stubbed out, we need to set a return value or we will get errors.
+	returnValue = args[2];
+	Common::String registryName = args[0].asString();
+	if (registryName.size() != 0) {
+		// TODO: Get the registry (saved game content) with this name.
+		warning("STUB: %s: %s", __func__, registryName.c_str());
+	}
 }
 
 void FunctionManager::script_SetProfile(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
-	warning("STUB: SetProfile");
+	warning("STUB: %s", __func__);
 }
 
 void FunctionManager::script_DebugPrint(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
@@ -567,27 +579,31 @@ void FunctionManager::script_DebugPrint(Common::Array<ScriptValue> &args, Script
 }
 
 void FunctionManager::script_MazeGenerate(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
-	warning("STUB: MazeGenerate");
+	warning("STUB: %s", __func__);
 }
 
 void FunctionManager::script_MazeApplyMoveMask(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
-	warning("STUB: MazeApplyMoveMask");
+	warning("STUB: %s", __func__);
 }
 
 void FunctionManager::script_MazeSolve(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
-	warning("STUB: MazeSolve");
+	warning("STUB: %s", __func__);
 }
 
 void FunctionManager::script_BeginTimedInterval(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
-	warning("STUB: BeginTimedInterval");
+	warning("STUB: %s", __func__);
 }
 
 void FunctionManager::script_EndTimedInterval(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
-	warning("STUB: EndTimedInterval");
+	warning("STUB: %s", __func__);
+}
+
+void FunctionManager::script_Checkers(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
+	warning("STUB: %s", __func__);
 }
 
 void FunctionManager::script_Drawing(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
-	warning("STUB: Drawing");
+	warning("STUB: %s", __func__);
 }
 
 void FunctionManager::deleteFunctionsForContext(uint contextId) {
diff --git a/engines/mediastation/mediascript/function.h b/engines/mediastation/mediascript/function.h
index bca7836a788..8914eda6d2a 100644
--- a/engines/mediastation/mediascript/function.h
+++ b/engines/mediastation/mediascript/function.h
@@ -90,6 +90,9 @@ private:
 	void script_BeginTimedInterval(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
 	void script_EndTimedInterval(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
 
+	// Hercules.
+	void script_Checkers(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
+
 	// IBM/Crayola.
 	void script_Drawing(Common::Array<ScriptValue> &args, ScriptValue &returnValue);
 };
diff --git a/engines/mediastation/mediascript/scriptconstants.cpp b/engines/mediastation/mediascript/scriptconstants.cpp
index 65424fc87cf..7d866881255 100644
--- a/engines/mediastation/mediascript/scriptconstants.cpp
+++ b/engines/mediastation/mediascript/scriptconstants.cpp
@@ -158,6 +158,8 @@ const char *builtInFunctionToStr(BuiltInFunction function) {
 		return "BeginTimedInterval";
 	case kEndTimedIntervalFunction:
 		return "EndTimedInterval";
+	case kCheckersFunction:
+		return "Checkers";
 	case kDrawingFunction:
 		return "Drawing";
 	case kLegacy_RandomFunction:
diff --git a/engines/mediastation/mediascript/scriptconstants.h b/engines/mediastation/mediascript/scriptconstants.h
index 3a23a7bbffe..c73fa7db052 100644
--- a/engines/mediastation/mediascript/scriptconstants.h
+++ b/engines/mediastation/mediascript/scriptconstants.h
@@ -93,7 +93,8 @@ enum BuiltInFunction {
 	kMazeSolveFunction = 0x21,
 	kBeginTimedIntervalFunction = 0x22,
 	kEndTimedIntervalFunction = 0x23,
-	kDrawingFunction = 0x25,
+	kCheckersFunction = 0x24, // Hercules
+	kDrawingFunction = 0x25, // IBM/Crayola
 
 	// 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


Commit: 4e6a6d4617b0ce21bbb7fa9cd757cfa7fcf69018
    https://github.com/scummvm/scummvm/commit/4e6a6d4617b0ce21bbb7fa9cd757cfa7fcf69018
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-04-30T20:19:20-04:00

Commit Message:
MEDIASTATION: Fix incorrectly read cursor actor field

Changed paths:
    engines/mediastation/actors/cursor.cpp
    engines/mediastation/cursors.cpp


diff --git a/engines/mediastation/actors/cursor.cpp b/engines/mediastation/actors/cursor.cpp
index 59521d786ba..49dd7a2af63 100644
--- a/engines/mediastation/actors/cursor.cpp
+++ b/engines/mediastation/actors/cursor.cpp
@@ -27,7 +27,7 @@ namespace MediaStation {
 void CursorActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 	switch (paramType) {
 	case kActorHeaderCursorResourceId:
-		_cursorId = chunk.readUint32LE();
+		_cursorId = chunk.readTypedUint16();
 		break;
 
 	default:
@@ -39,7 +39,9 @@ ScriptValue CursorActor::callMethod(BuiltInMethod methodId, Common::Array<Script
 	ScriptValue returnValue;
 	switch (methodId) {
 	case kCursorSetMethod:
-		g_engine->getCursorManager()->setAsPermanent(_cursorId);
+		if (_cursorId != 0) {
+			g_engine->getCursorManager()->setAsPermanent(_cursorId);
+		}
 		break;
 
 	default:
diff --git a/engines/mediastation/cursors.cpp b/engines/mediastation/cursors.cpp
index 125c66a5904..1c1c6c0eb91 100644
--- a/engines/mediastation/cursors.cpp
+++ b/engines/mediastation/cursors.cpp
@@ -132,15 +132,13 @@ void CursorManager::registerAsPermanent(uint16 id) {
 }
 
 void CursorManager::setAsPermanent(uint16 id) {
-	bool cursorAlreadySet = _currentCursorId == id && _permanentCursorId == id;
-	bool cursorIsEmpty = id == 0;
-	if (cursorAlreadySet || cursorIsEmpty) {
-		return;
+	bool cursorAlreadySet = (_currentCursorId == id) && (_permanentCursorId == id);
+	bool cursorIsEmpty = (id == 0);
+	if (!cursorAlreadySet && !cursorIsEmpty) {
+		_permanentCursorId = id;
+		_currentCursorId = id;
+		resetCurrent();
 	}
-
-	_permanentCursorId = id;
-	_currentCursorId = id;
-	resetCurrent();
 }
 
 void CursorManager::setAsTemporary(uint16 id) {
@@ -168,8 +166,13 @@ void CursorManager::unsetTemporary() {
 
 void CursorManager::resetCurrent() {
 	if (_currentCursorId != 0) {
-		Graphics::Cursor *cursor = _cursors.getVal(_currentCursorId);
-		CursorMan.replaceCursor(cursor);
+		Graphics::Cursor *cursor = _cursors.getValOrDefault(_currentCursorId);
+		if (cursor != nullptr) {
+			CursorMan.replaceCursor(cursor);
+		} else {
+			warning("%s: Cursor %d not found", __func__, _currentCursorId);
+		}
+
 	}
 }
 


Commit: d328b0f44dfcea829b0cab925645851e49891181
    https://github.com/scummvm/scummvm/commit/d328b0f44dfcea829b0cab925645851e49891181
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-04-30T20:19:20-04:00

Commit Message:
MEDIASTATION: Update debug statements and levels

Changed paths:
    engines/mediastation/actor.cpp
    engines/mediastation/actor.h
    engines/mediastation/actors/camera.cpp
    engines/mediastation/actors/hotspot.cpp
    engines/mediastation/actors/stage.cpp
    engines/mediastation/actors/stage.h
    engines/mediastation/clients.cpp
    engines/mediastation/datafile.cpp
    engines/mediastation/graphics.cpp
    engines/mediastation/mediascript/codechunk.cpp
    engines/mediastation/mediascript/codechunk.h
    engines/mediastation/mediascript/collection.cpp
    engines/mediastation/mediascript/function.cpp
    engines/mediastation/mediascript/scriptconstants.cpp
    engines/mediastation/mediascript/scriptconstants.h
    engines/mediastation/mediascript/scriptvalue.cpp
    engines/mediastation/mediastation.cpp
    engines/mediastation/profile.cpp


diff --git a/engines/mediastation/actor.cpp b/engines/mediastation/actor.cpp
index eee059c7d22..0bdfa382a4c 100644
--- a/engines/mediastation/actor.cpp
+++ b/engines/mediastation/actor.cpp
@@ -91,10 +91,6 @@ const char *actorTypeToStr(ActorType type) {
 
 void Actor::setId(uint id) {
 	_id = id;
-	updateDebugName();
-}
-
-void Actor::updateDebugName() {
 	_debugName = g_engine->formatActorName(this);
 }
 
@@ -117,6 +113,7 @@ void Actor::initFromParameterStream(Chunk &chunk) {
 	ActorHeaderSectionType paramType = kActorHeaderEmptySection;
 	while (true) {
 		paramType = static_cast<ActorHeaderSectionType>(chunk.readTypedUint16());
+		debugC(5, kDebugLoading, "[%s] %s: Got section type 0x%x", debugName(), __func__, static_cast<uint>(paramType));
 		if (paramType == 0) {
 			break;
 		} else {
@@ -517,8 +514,6 @@ void SpatialEntity::invalidateLocalBounds() {
 	if (_parentStage != nullptr) {
 		_parentStage->setAdjustedBounds(kWrapNone);
 		_parentStage->invalidateRect(getBbox());
-	} else {
-		warning("[%s] %s: No parent stage", debugName(), __func__);
 	}
 }
 
diff --git a/engines/mediastation/actor.h b/engines/mediastation/actor.h
index 70ff722086f..e74a8597d87 100644
--- a/engines/mediastation/actor.h
+++ b/engines/mediastation/actor.h
@@ -211,7 +211,6 @@ public:
 	void setId(uint id);
 	void setContextId(uint id) { _contextId = id; }
 	const char *debugName() const;
-	void updateDebugName();
 
 protected:
 	ActorType _type = kActorTypeEmpty;
diff --git a/engines/mediastation/actors/camera.cpp b/engines/mediastation/actors/camera.cpp
index 81282176205..2618cd11379 100644
--- a/engines/mediastation/actors/camera.cpp
+++ b/engines/mediastation/actors/camera.cpp
@@ -364,7 +364,6 @@ void CameraActor::drawUsingCamera(DisplayContext &destContext, const Common::Arr
 
 void CameraActor::drawObject(DisplayContext &sourceContext, DisplayContext &destContext, SpatialEntity *objectToDraw) {
 	if (_parentStage == nullptr) {
-		warning("[%s] %s: No parent stage", debugName(), __func__);
 		return;
 	}
 
@@ -469,7 +468,7 @@ bool CameraActor::continuePan() {
 			panShouldContinue = false;
 		}
 	}
-	debugC(6, kDebugCamera, "[%s] %s: %s", debugName(), __func__, panShouldContinue ? "true": "false");
+	debugC(6, kDebugCamera, "[%s] %s: %s", debugName(), __func__, panShouldContinue ? "true" : "false");
 	return panShouldContinue;
 }
 
@@ -564,7 +563,6 @@ void CameraActor::processNextPanStep() {
 
 void CameraActor::adjustCameraViewport(Common::Point &viewportToAdjust) {
 	if (_parentStage == nullptr) {
-		warning("[%s] %s: No parent stage", debugName(), __func__);
 		return;
 	}
 
diff --git a/engines/mediastation/actors/hotspot.cpp b/engines/mediastation/actors/hotspot.cpp
index 3c2cea3e7a2..6eced672215 100644
--- a/engines/mediastation/actors/hotspot.cpp
+++ b/engines/mediastation/actors/hotspot.cpp
@@ -179,7 +179,7 @@ uint16 HotspotActor::findActorToAcceptMouseEvents(
 			result |= kMouseUpFlag;
 		}
 	} else {
-		debugC(6, kDebugEvents, "[%s] %s: Inactive", debugName(),  __func__);
+		debugC(8, kDebugEvents, "[%s] %s: Inactive", debugName(),  __func__);
 	}
 
 	return result;
diff --git a/engines/mediastation/actors/stage.cpp b/engines/mediastation/actors/stage.cpp
index 65cdf74c976..69240f6e44e 100644
--- a/engines/mediastation/actors/stage.cpp
+++ b/engines/mediastation/actors/stage.cpp
@@ -643,6 +643,11 @@ void StageActor::currentMousePosition(Common::Point &point) {
 	}
 }
 
+RootStage::RootStage() : StageActor() {
+	_id = ROOT_STAGE_ACTOR_ID;
+	_debugName = Common::String("Stage ROOT");
+}
+
 void RootStage::invalidateRect(const Common::Rect &rect) {
 	Common::Rect rectToAdd = rect;
 	rectToAdd.clip(_boundingBox);
diff --git a/engines/mediastation/actors/stage.h b/engines/mediastation/actors/stage.h
index d087745d807..3da546cbe53 100644
--- a/engines/mediastation/actors/stage.h
+++ b/engines/mediastation/actors/stage.h
@@ -137,7 +137,7 @@ protected:
 class RootStage : public StageActor {
 public:
 	friend class StageDirector;
-	RootStage() : StageActor() { _id = ROOT_STAGE_ACTOR_ID; };
+	RootStage();
 
 	virtual uint16 findActorToAcceptMouseEvents(
 		const Common::Point &point,
diff --git a/engines/mediastation/clients.cpp b/engines/mediastation/clients.cpp
index c4ad5526eb8..02c4fbc1bee 100644
--- a/engines/mediastation/clients.cpp
+++ b/engines/mediastation/clients.cpp
@@ -149,6 +149,9 @@ void Document::branchToScreen() {
 		_loadingScreenActorId = _requestedScreenBranchId;
 		_requestedScreenBranchId = 0;
 		uint contextId = contextIdForScreenActorId(_loadingScreenActorId);
+		if (contextId == 0) {
+			error("%s: Screen %d doesn't have a context in current title", __func__, _loadingScreenActorId);
+		}
 
 		blowAwayCurrentScreen();
 		preloadParentContexts(contextId);
diff --git a/engines/mediastation/datafile.cpp b/engines/mediastation/datafile.cpp
index 016d1feeb7c..c9438f36433 100644
--- a/engines/mediastation/datafile.cpp
+++ b/engines/mediastation/datafile.cpp
@@ -312,7 +312,7 @@ void StreamFeedManager::closeStreamFeed(StreamFeed *streamFeed) {
 
 void StreamFeedManager::registerChannelClient(ChannelClient *client) {
 	if (_channelClients.getValOrDefault(client->channelIdent()) != nullptr) {
-		error("%s: Channel %s already has a client", __func__, g_engine->formatAssetNameForChannelIdent(client->channelIdent()).c_str());
+		warning("%s: Channel %s already has a client", __func__, g_engine->formatAssetNameForChannelIdent(client->channelIdent()).c_str());
 	}
 	_channelClients.setVal(client->channelIdent(), client);
 }
diff --git a/engines/mediastation/graphics.cpp b/engines/mediastation/graphics.cpp
index eb7bdee1b6f..b7ca392f367 100644
--- a/engines/mediastation/graphics.cpp
+++ b/engines/mediastation/graphics.cpp
@@ -232,7 +232,7 @@ bool VideoDisplayManager::attemptToReadFromStream(Chunk &chunk, uint sectionType
 		break;
 
 	case kVideoDisplayManagerSetTime:
-		debugC(5, kDebugGraphics, "%s", __func__);
+		debugC(7, kDebugGraphics, "%s", __func__);
 		_defaultTransitionTime = chunk.readTypedTime();
 		break;
 
diff --git a/engines/mediastation/mediascript/codechunk.cpp b/engines/mediastation/mediascript/codechunk.cpp
index 5cb19587846..7c4abf98706 100644
--- a/engines/mediastation/mediascript/codechunk.cpp
+++ b/engines/mediastation/mediascript/codechunk.cpp
@@ -28,6 +28,14 @@
 
 namespace MediaStation {
 
+Common::String CodeChunk::makeDebugIndent() const {
+	Common::String indentation;
+	for (uint i = 0; i < _debugIndentLevel; ++i) {
+		indentation += "    ";
+	}
+	return indentation;
+}
+
 ScriptValue CodeChunk::executeNextBlock() {
 	uint blockSize = _bytecode->readTypedUint32();
 	int64 startingPos = _bytecode->pos();
@@ -288,7 +296,7 @@ ScriptValue CodeChunk::evaluateValue() {
 
 	case kOperandTypeMethodId: {
 		BuiltInMethod methodId = static_cast<BuiltInMethod>(_bytecode->readTypedUint16());
-		debugC(5, kDebugScript, "%s ", builtInMethodToStr(methodId));
+		debugC(5, kDebugScript, "%s (%d)", builtInMethodToStr(methodId), static_cast<uint>(methodId));
 		returnValue.setToMethodId(methodId);
 		break;
 	}
@@ -354,41 +362,62 @@ ScriptValue *CodeChunk::readAndReturnVariable() {
 }
 
 void CodeChunk::evaluateIf() {
-	debugCN(5, kDebugScript, "\n    condition: ");
+	_debugIndentLevel++;
+	debugCN(5, kDebugScript, "\n%scondition: ", makeDebugIndent().c_str());
 	ScriptValue condition = evaluateExpression();
 	if (condition.getType() != kScriptValueTypeBool) {
 		error("%s: Expected bool condition, got %s", __func__, scriptValueTypeToStr(condition.getType()));
 	}
 
 	if (condition.asBool()) {
+		debugC(5, kDebugScript, "%s=> TRUE", makeDebugIndent().c_str());
+		_debugIndentLevel--;
 		executeNextBlock();
+		debugC(6, kDebugScript, "%s: Taking TRUE branch", __func__);
 	} else {
+		debugC(5, kDebugScript, "%s=> FALSE", makeDebugIndent().c_str());
+		_debugIndentLevel--;
 		skipNextBlock();
+		debugC(6, kDebugScript, "%s: Skipping TRUE branch", __func__);
 	}
 }
 
 void CodeChunk::evaluateIfElse() {
-	debugCN(5, kDebugScript, "\n    condition: ");
+	_debugIndentLevel++;
+	debugCN(5, kDebugScript, "\n%scondition: ", makeDebugIndent().c_str());
 	ScriptValue condition = evaluateExpression();
 	if (condition.getType() != kScriptValueTypeBool) {
 		error("%s: Expected bool condition, got %s", __func__, scriptValueTypeToStr(condition.getType()));
 	}
 
 	if (condition.asBool()) {
+		debugC(5, kDebugScript, "%s=> TRUE", makeDebugIndent().c_str());
+		_debugIndentLevel--;
+
+		debugC(6, kDebugScript, "%s: Taking TRUE branch", __func__);
 		executeNextBlock();
+
+		debugC(6, kDebugScript, "%s: Skipping FALSE branch", __func__);
 		skipNextBlock();
 	} else {
+		debugC(5, kDebugScript, "%s=> FALSE", makeDebugIndent().c_str());
+		_debugIndentLevel--;
+
+		debugC(6, kDebugScript, "%s: Skipping TRUE branch", __func__);
 		skipNextBlock();
+
+		debugC(6, kDebugScript, "%s: Taking FALSE branch", __func__);
 		executeNextBlock();
 	}
 }
 
 ScriptValue CodeChunk::evaluateAssign() {
-	debugCN(5, kDebugScript, "Variable ");
+	_debugIndentLevel++;
+	debugCN(5, kDebugScript, "\n%svariable: ", makeDebugIndent().c_str());
 	ScriptValue *targetVariable = readAndReturnVariable();
-
-	debugC(5, kDebugScript, "  Value: ");
+	debugCN(5, kDebugScript, "%svalue: ", makeDebugIndent().c_str());
 	ScriptValue value = evaluateExpression();
+	_debugIndentLevel--;
 
 	if (value.getType() == kScriptValueTypeEmpty) {
 		error("%s: Attempt to assign an empty value to a variable", __func__);
@@ -403,9 +432,10 @@ ScriptValue CodeChunk::evaluateAssign() {
 }
 
 ScriptValue CodeChunk::evaluateBinaryOperation(Opcode op) {
-	debugCN(5, kDebugScript, "\n    lhs: ");
+	_debugIndentLevel++;
+	debugCN(5, kDebugScript, "\n%slhs: ", makeDebugIndent().c_str());
 	ScriptValue value1 = evaluateExpression();
-	debugCN(5, kDebugScript, "    rhs: ");
+	debugCN(5, kDebugScript, "%srhs: ", makeDebugIndent().c_str());
 	ScriptValue value2 = evaluateExpression();
 
 	ScriptValue returnValue;
@@ -469,13 +499,25 @@ ScriptValue CodeChunk::evaluateBinaryOperation(Opcode op) {
 	default:
 		error("%s: Got unknown binary operation opcode %s", __func__, opcodeToStr(op));
 	}
+
+	// For comparison operations, show the result.
+	if (op == kOpcodeOr || op == kOpcodeXor || op == kOpcodeAnd ||
+	    op == kOpcodeEquals || op == kOpcodeNotEquals ||
+	    op == kOpcodeLessThan || op == kOpcodeGreaterThan ||
+	    op == kOpcodeLessThanOrEqualTo || op == kOpcodeGreaterThanOrEqualTo) {
+		debugC(5, kDebugScript, "%s=> %s", makeDebugIndent().c_str(), returnValue.asBool() ? "TRUE" : "FALSE");
+	}
+
+	_debugIndentLevel--;
 	return returnValue;
 }
 
 ScriptValue CodeChunk::evaluateUnaryOperation() {
 	// The only supported unary operation seems to be negation.
+	_debugIndentLevel++;
+	debugCN(5, kDebugScript, "\n%svalue: ", makeDebugIndent().c_str());
 	ScriptValue value = evaluateExpression();
-	debugCN(5, kDebugScript, "    value: ");
+	_debugIndentLevel--;
 	return -value;
 }
 
@@ -498,11 +540,13 @@ ScriptValue CodeChunk::evaluateFunctionCall(uint functionId, uint paramCount) {
 	debugC(5, kDebugScript, "%s (%d params)", functionName.c_str(), paramCount);
 
 	Common::Array<ScriptValue> args;
+	_debugIndentLevel++;
 	for (uint i = 0; i < paramCount; i++) {
-		debugCN(5, kDebugScript, "  Param %d: ", i);
+		debugCN(5, kDebugScript, "%sparam %d: ", makeDebugIndent().c_str(), i);
 		ScriptValue arg = evaluateExpression();
 		args.push_back(arg);
 	}
+	_debugIndentLevel--;
 
 	ScriptValue returnValue = g_engine->getFunctionManager()->call(functionId, args);
 	return returnValue;
@@ -529,7 +573,8 @@ ScriptValue CodeChunk::evaluateMethodCall(BuiltInMethod method, uint paramCount)
 	// define. Functions, however, CAN be title-defined.
 	// But here, we're only looking for built-in methods.
 	debugC(5, kDebugScript, "%s (%d params)", builtInMethodToStr(method), paramCount);
-	debugCN(5, kDebugScript, "  Self: ");
+	_debugIndentLevel++;
+	debugCN(5, kDebugScript, "%sself: ", makeDebugIndent().c_str());
 
 	// Evaluate target as an lvalue to get a pointer to the actual variable if there is one.
 	ScriptValue methodCallTarget;
@@ -537,27 +582,29 @@ ScriptValue CodeChunk::evaluateMethodCall(BuiltInMethod method, uint paramCount)
 	evaluateLValue(methodCallTargetPtr);
 	Common::Array<ScriptValue> args;
 	for (uint i = 0; i < paramCount; i++) {
-		debugCN(5, kDebugScript, "  Param %d: ", i);
+		debugCN(5, kDebugScript, "%sparam %d: ", makeDebugIndent().c_str(), i);
 		ScriptValue arg = evaluateExpression();
 		args.push_back(arg);
 	}
+	_debugIndentLevel--;
 
 	ScriptValue returnValue;
 	switch (methodCallTargetPtr->getType()) {
 	case kScriptValueTypeActorId: {
 		if (methodCallTargetPtr->asActorId() == 0) {
 			// It seems to be valid to call a method on a null actor ID, in
-			// which case nothing happens. Still issue warning for traceability.
-			warning("%s: Attempt to call method %s (%d) on null actor ID", __func__, builtInMethodToStr(method), static_cast<uint>(method));
+			// which case nothing happens. Still log for traceability.
+			debugC(5, kDebugScript, "%s: Attempt to call method %s (%d) on null actor ID", __func__, builtInMethodToStr(method), static_cast<uint>(method));
 			break;
 		} else {
 			// This is a regular actor that we can process directly.
 			uint actorId = methodCallTargetPtr->asActorId();
 			Actor *targetActor = g_engine->getActorById(actorId);
 			if (targetActor == nullptr) {
-				error("[%s] %s: Actor not loaded", g_engine->formatActorName(actorId).c_str(), __func__);
+				warning("[%s] %s: Actor not loaded", g_engine->formatActorName(actorId).c_str(), __func__);
+			} else {
+				returnValue = targetActor->callMethod(method, args);
 			}
-			returnValue = targetActor->callMethod(method, args);
 			break;
 		}
 	}
@@ -601,7 +648,10 @@ void CodeChunk::evaluateWhileLoop() {
 	while (true) {
 		// Seek to the top of the loop bytecode.
 		_bytecode->seek(loopStartPosition);
+		_debugIndentLevel++;
+		debugCN(5, kDebugScript, "\n%scondition: ", makeDebugIndent().c_str());
 		ScriptValue condition = evaluateExpression();
+		_debugIndentLevel--;
 		if (condition.getType() != kScriptValueTypeBool) {
 			error("%s: Expected loop condition to be bool, not %s", __func__, scriptValueTypeToStr(condition.getType()));
 		}
@@ -611,8 +661,10 @@ void CodeChunk::evaluateWhileLoop() {
 		}
 
 		if (condition.asBool()) {
+			debugC(5, kDebugScript, "%s=> TRUE (continue loop)", makeDebugIndent().c_str());
 			executeNextBlock();
 		} else {
+			debugC(5, kDebugScript, "%s=> FALSE (exit loop)", makeDebugIndent().c_str());
 			skipNextBlock();
 			break;
 		}
diff --git a/engines/mediastation/mediascript/codechunk.h b/engines/mediastation/mediascript/codechunk.h
index c0dc70f0f8a..d4c0abbe566 100644
--- a/engines/mediastation/mediascript/codechunk.h
+++ b/engines/mediastation/mediascript/codechunk.h
@@ -74,6 +74,10 @@ private:
 	Common::Array<ScriptValue> _locals;
 	Common::Array<ScriptValue> *_args = nullptr;
 	ParameterReadStream *_bytecode = nullptr;
+
+	// Debug output indentation tracking.
+	uint _debugIndentLevel = 0;
+	Common::String makeDebugIndent() const;
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/mediascript/collection.cpp b/engines/mediastation/mediascript/collection.cpp
index 77f639edcb2..1b7250e0dbd 100644
--- a/engines/mediastation/mediascript/collection.cpp
+++ b/engines/mediastation/mediascript/collection.cpp
@@ -186,7 +186,7 @@ void Collection::send(const Common::Array<ScriptValue> &args) {
 		uint actorId = item.asActorId();
 		Actor *targetActor = g_engine->getActorById(actorId);
 		if (targetActor != nullptr) {
-			debugC(7, kDebugScript, "%s: %s: %d", __func__, builtInMethodToStr(methodToSend), actorId);
+			debugC(7, kDebugScript, "%s: %s: %s", __func__, builtInMethodToStr(methodToSend), targetActor->debugName());
 			targetActor->callMethod(methodToSend, argsToSend);
 		}
 	}
diff --git a/engines/mediastation/mediascript/function.cpp b/engines/mediastation/mediascript/function.cpp
index ff58e2e3167..be100e6600b 100644
--- a/engines/mediastation/mediascript/function.cpp
+++ b/engines/mediastation/mediascript/function.cpp
@@ -31,19 +31,19 @@ namespace MediaStation {
 // For exact argument count.
 #define FUNCARGCHECK(n) \
 	if (args.size() != (n)) { \
-		warning("%s: expected %d argument%s, got %d", builtInFunctionToStr(static_cast<BuiltInFunction>(functionId)), (n), ((n) == 1 ? "" : "s"), args.size()); \
+		warning("%s: expected %d argument%s, got %d", builtInFunctionToStr(functionId), (n), ((n) == 1 ? "" : "s"), args.size()); \
 	}
 
 // For a range of valid argument counts (min to max).
 #define FUNCARGRANGE(min, max) \
 	if (args.size() < (min) || args.size() > (max)) { \
-		warning("%s: expected %d to %d argument, got %d", builtInFunctionToStr(static_cast<BuiltInFunction>(functionId)), (min), (max), args.size()); \
+		warning("%s: expected %d to %d argument, got %d", builtInFunctionToStr(functionId), (min), (max), args.size()); \
 	}
 
 // For minimum argument count (no maximum).
 #define FUNCARGMIN(min) \
 	if (args.size() < (min)) { \
-		warning("%s: expected at least %d argument%s, got %d", builtInFunctionToStr(static_cast<BuiltInFunction>(functionId)), (min), ((min) == 1 ? "" : "s"), args.size()); \
+		warning("%s: expected at least %d argument%s, got %d", builtInFunctionToStr(functionId), (min), ((min) == 1 ? "" : "s"), args.size()); \
 	}
 
 ScriptFunction::ScriptFunction(Chunk &chunk) {
diff --git a/engines/mediastation/mediascript/scriptconstants.cpp b/engines/mediastation/mediascript/scriptconstants.cpp
index 7d866881255..119fcc7b3fe 100644
--- a/engines/mediastation/mediascript/scriptconstants.cpp
+++ b/engines/mediastation/mediascript/scriptconstants.cpp
@@ -112,7 +112,7 @@ const char *variableScopeToStr(VariableScope scope) {
 	}
 }
 
-const char *builtInFunctionToStr(BuiltInFunction function) {
+const char *builtInFunctionToStr(uint function) {
 	switch (function) {
 	case kRandomFunction:
 		return "Random";
diff --git a/engines/mediastation/mediascript/scriptconstants.h b/engines/mediastation/mediascript/scriptconstants.h
index c73fa7db052..eedc2622210 100644
--- a/engines/mediastation/mediascript/scriptconstants.h
+++ b/engines/mediastation/mediascript/scriptconstants.h
@@ -22,6 +22,8 @@
 #ifndef MEDIASTATION_MEDIASCRIPT_BUILTINS_H
 #define MEDIASTATION_MEDIASCRIPT_BUILTINS_H
 
+#include "common/scummsys.h"
+
 namespace MediaStation {
 
 enum ExpressionType {
@@ -115,7 +117,7 @@ enum BuiltInFunction {
 	kLegacy_GetAudioVolumeFunction = 0xBF,
 	kLegacy_SystemLanguagePreferenceFunction = 0xC8,
 };
-const char *builtInFunctionToStr(BuiltInFunction function);
+const char *builtInFunctionToStr(uint function);
 
 enum BuiltInMethod {
 	kInvalidMethod = 0,
diff --git a/engines/mediastation/mediascript/scriptvalue.cpp b/engines/mediastation/mediascript/scriptvalue.cpp
index 42deb5b66b0..4c943bd26b7 100644
--- a/engines/mediastation/mediascript/scriptvalue.cpp
+++ b/engines/mediastation/mediascript/scriptvalue.cpp
@@ -325,6 +325,9 @@ Common::String ScriptValue::getDebugString() const {
 	case kScriptValueTypeEmpty:
 		return "empty";
 
+	case kScriptValueTypeBool:
+		return Common::String::format("bool: %s", asBool() ? "TRUE" : "FALSE");
+
 	case kScriptValueTypeFloat:
 		return Common::String::format("float: %f", asFloat());
 
@@ -344,6 +347,21 @@ Common::String ScriptValue::getDebugString() const {
 	case kScriptValueTypeString:
 		return Common::String::format("string: \"%s\"", asString().c_str());
 
+	case kScriptValueTypeCollection: {
+		Collection *collection = asCollection();
+		uint itemCount = collection ? collection->size() : 0;
+		return Common::String::format("collection: [%d items]", itemCount);
+	}
+
+	case kScriptValueTypeFunctionId: {
+		Common::String functionName = g_engine->formatFunctionName(asFunctionId());
+		return Common::String::format("function: %s", functionName.c_str());
+	}
+
+	case kScriptValueTypeMethodId: {
+		return Common::String::format("method: %s", builtInMethodToStr(asMethodId()));
+	}
+
 	default:
 		return Common::String::format("arg type %s", scriptValueTypeToStr(getType()));
 	}
diff --git a/engines/mediastation/mediastation.cpp b/engines/mediastation/mediastation.cpp
index b608507dbf5..ea380333133 100644
--- a/engines/mediastation/mediastation.cpp
+++ b/engines/mediastation/mediastation.cpp
@@ -335,10 +335,10 @@ void MediaStationEngine::destroyActor(uint actorId) {
 }
 
 void MediaStationEngine::destroyContext(uint contextId, bool eraseFromLoadedContexts) {
-	debugC(5, kDebugScript, "%s: Context %d", __func__, contextId);
+	debugC(5, kDebugScript, "%s: Context %s", __func__, formatActorName(contextId).c_str());
 	Context *context = _loadedContexts.getValOrDefault(contextId);
 	if (context == nullptr) {
-		warning("%s: Attempted to unload context %d that is not currently loaded", __func__, contextId);
+		warning("%s: Attempted to unload context %s that is not currently loaded", __func__, formatActorName(contextId).c_str());
 		return;
 	}
 
diff --git a/engines/mediastation/profile.cpp b/engines/mediastation/profile.cpp
index 0c9e20abad0..bbdf399df44 100644
--- a/engines/mediastation/profile.cpp
+++ b/engines/mediastation/profile.cpp
@@ -257,7 +257,7 @@ Common::String Profile::formatFunctionName(uint functionId) {
 		formattedName = Common::String::format("%s (%d)", functionName.c_str(), functionId);
 	} else {
 		// This might be a built-in function, in which case we can try to get the built-in name.
-		formattedName = Common::String::format("%s (%d)", builtInFunctionToStr(static_cast<BuiltInFunction>(functionId)), functionId);
+		formattedName = Common::String::format("%s (%d)", builtInFunctionToStr(functionId), functionId);
 	}
 	return formattedName;
 }


Commit: 666fb78f68f83ec3e1f6e8cc5d4d3fe2795cfad8
    https://github.com/scummvm/scummvm/commit/666fb78f68f83ec3e1f6e8cc5d4d3fe2795cfad8
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-04-30T20:19:20-04:00

Commit Message:
MEDIASTATION: Implement Disk Image actor

As part of this, we also implement the parallax rendering and cylindrical wrapping
for Hunchback, as the disk image actors seem to be the only actor type that actually
uses this mode.

Assisted-by: Claude:claude-sonnet-4.5

Changed paths:
  A engines/mediastation/actors/diskimage.cpp
  A engines/mediastation/actors/diskimage.h
    engines/mediastation/actor.cpp
    engines/mediastation/actor.h
    engines/mediastation/actors/camera.cpp
    engines/mediastation/actors/camera.h
    engines/mediastation/actors/document.cpp
    engines/mediastation/actors/stage.cpp
    engines/mediastation/actors/stage.h
    engines/mediastation/context.cpp
    engines/mediastation/graphics.cpp
    engines/mediastation/mediascript/scriptconstants.cpp
    engines/mediastation/mediascript/scriptconstants.h
    engines/mediastation/module.mk


diff --git a/engines/mediastation/actor.cpp b/engines/mediastation/actor.cpp
index 0bdfa382a4c..30e1411fe4e 100644
--- a/engines/mediastation/actor.cpp
+++ b/engines/mediastation/actor.cpp
@@ -58,7 +58,7 @@ const char *actorTypeToStr(ActorType type) {
 		return "LKConstellations";
 	case kActorTypeDocument:
 		return "Document";
-	case kActorTypeImageSet:
+	case kActorTypeDiskImage:
 		return "ImageSet";
 	case kActorTypeMovie:
 		return "Movie";
@@ -333,35 +333,35 @@ ScriptValue SpatialEntity::callMethod(BuiltInMethod methodId, Common::Array<Scri
 		break;
 	}
 
-	case kGetXScaleMethod1:
-	case kGetXScaleMethod2:
+	case kGetParallaxFactorXMethod1:
+	case kGetParallaxFactorXMethod2:
 		ARGCOUNTCHECK(0);
-		returnValue.setToFloat(_scaleX);
+		returnValue.setToFloat(_parallaxFactorX);
 		break;
 
-	case kSetScaleMethod:
+	case kSetParallaxFactorMethod:
 		ARGCOUNTCHECK(1);
 		invalidateLocalBounds();
-		_scaleX = _scaleY = args[0].asFloat();
+		_parallaxFactorX = _parallaxFactorY = args[0].asFloat();
 		invalidateLocalBounds();
 		break;
 
-	case kSetXScaleMethod:
+	case kSetParallaxFactorXMethod:
 		ARGCOUNTCHECK(1);
 		invalidateLocalBounds();
-		_scaleX = args[0].asFloat();
+		_parallaxFactorX = args[0].asFloat();
 		invalidateLocalBounds();
 		break;
 
-	case kGetYScaleMethod:
+	case kGetParallaxFactorYMethod:
 		ARGCOUNTCHECK(0);
-		returnValue.setToFloat(_scaleY);
+		returnValue.setToFloat(_parallaxFactorY);
 		break;
 
-	case kSetYScaleMethod:
+	case kSetParallaxFactorYMethod:
 		ARGCOUNTCHECK(1);
 		invalidateLocalBounds();
-		_scaleY = args[0].asFloat();
+		_parallaxFactorY = args[0].asFloat();
 		invalidateLocalBounds();
 		break;
 
@@ -378,9 +378,11 @@ void SpatialEntity::readParameter(Chunk &chunk, ActorHeaderSectionType paramType
 		setAdjustedBounds(kWrapNone);
 		break;
 
-	case kActorHeaderDissolveFactor:
-		_dissolveFactor = chunk.readTypedDouble();
+	case kActorHeaderDissolveFactor: {
+		double dissolveFactor = chunk.readTypedDouble();
+		setDissolveFactor(dissolveFactor);
 		break;
+	}
 
 	case kActorHeaderZIndex:
 		_zIndex = chunk.readTypedGraphicUnit();
@@ -395,15 +397,15 @@ void SpatialEntity::readParameter(Chunk &chunk, ActorHeaderSectionType paramType
 		break;
 
 	case kActorHeaderScaleXAndY:
-		_scaleX = _scaleY = chunk.readTypedDouble();
+		_parallaxFactorX = _parallaxFactorY = chunk.readTypedDouble();
 		break;
 
 	case kActorHeaderScaleX:
-		_scaleX = chunk.readTypedDouble();
+		_parallaxFactorX = chunk.readTypedDouble();
 		break;
 
 	case kActorHeaderScaleY:
-		_scaleY = chunk.readTypedDouble();
+		_parallaxFactorY = chunk.readTypedDouble();
 		break;
 
 	default:
@@ -512,7 +514,7 @@ void SpatialEntity::setDissolveFactor(double dissolveFactor) {
 
 void SpatialEntity::invalidateLocalBounds() {
 	if (_parentStage != nullptr) {
-		_parentStage->setAdjustedBounds(kWrapNone);
+		setAdjustedBounds(kWrapNone);
 		_parentStage->invalidateRect(getBbox());
 	}
 }
@@ -523,7 +525,7 @@ void SpatialEntity::invalidateLocalZIndex() {
 	}
 }
 
-void SpatialEntity::setAdjustedBounds(CylindricalWrapMode alignmentMode) {
+void SpatialEntity::setAdjustedBounds(CylindricalWrapMode wrapMode) {
 	_boundingBox = _originalBoundingBox;
 	if (_parentStage == nullptr) {
 		return;
@@ -531,70 +533,113 @@ void SpatialEntity::setAdjustedBounds(CylindricalWrapMode alignmentMode) {
 
 	Common::Point offset(0, 0);
 	Common::Point stageExtent = _parentStage->extent();
-	switch (alignmentMode) {
-	case kWrapRight: {
+
+	// Calculate position offset for cylindrical wrapping.
+	switch (wrapMode) {
+	case kWrapRight:
 		offset.x = stageExtent.x;
 		offset.y = 0;
 		break;
-	}
 
-	case kWrapLeft: {
-		offset.x = -stageExtent.x;
-		offset.y = 0;
+	case kWrapDown:
+		offset.x = 0;
+		offset.y = stageExtent.y;
 		break;
-	}
 
-	case kWrapBottom: {
-		offset.x = 0;
+	case kWrapRightDown:
+		offset.x = stageExtent.x;
 		offset.y = stageExtent.y;
 		break;
-	}
 
-	case kWrapLeftTop: {
-		offset.x = 0;
-		offset.y = -stageExtent.y;
+	case kWrapLeft:
+		offset.x = -stageExtent.x;
+		offset.y = 0;
 		break;
-	}
 
-	case kWrapTop: {
-		offset.x = stageExtent.x;
-		offset.y = stageExtent.y;
+	case kWrapUp:
+		offset.x = 0;
+		offset.y = -stageExtent.y;
 		break;
-	}
 
-	case kWrapRightBottom: {
+	case kWrapLeftUp:
 		offset.x = -stageExtent.x;
 		offset.y = -stageExtent.y;
 		break;
-	}
 
-	case kWrapRightTop: {
+	case kWrapLeftDown:
 		offset.x = -stageExtent.x;
 		offset.y = stageExtent.y;
 		break;
-	}
 
-	case kWrapLeftBottom: {
+	case kWrapRightUp:
 		offset.x = stageExtent.x;
 		offset.y = -stageExtent.y;
 		break;
-	}
 
 	case kWrapNone:
 	default:
 		// No offset adjustment.
 		break;
 	}
+	_boundingBox.translate(-offset.x, -offset.y);
 
-	if (alignmentMode != kWrapNone) {
-		// TODO: Implement this once we have a title that actually uses it.
-		warning("[%s] %s: Wrapping mode %d not handled yet: (%d, %d, %d, %d) -= (%d, %d)", debugName(),  __func__,
-			static_cast<uint>(alignmentMode), PRINT_RECT(_boundingBox), offset.x, offset.y);
+	// Apply parallax scrolling if parallax factors are set.
+	// This simulates depth by adjusting position based on distance from viewport center.
+	// parallax 0.0 means no parallax (ignores camera movement).
+	// parallax 1.0 means neutral depth (moves with camera at focal plane).
+	// parallax >1.0 means appears closer (moves more than camera).
+	// parallax <1.0 means appears farther (moves less than camera).
+	bool hasHorizontalParallax = _parallaxFactorX != 0.0;
+	bool hasVerticalParallax = _parallaxFactorY != 0.0;
+	if (!hasHorizontalParallax && !hasVerticalParallax) {
+		return;
+	}
+
+	// Transform camera viewport to object's local coordinate space (inside any stages).
+	CameraActor *currentCamera = _parentStage->getCurrentCamera();
+	if (currentCamera == nullptr) {
+		return;
 	}
 
-	if (_scaleX != 0.0 || _scaleY != 0.0) {
-		// TODO: Implement this once we have a title that actually uses it.
-		warning("[%s] %s: Scale not handled yet (scaleX: %f, scaleY: %f)", debugName(), __func__, _scaleX, _scaleY);
+	Common::Rect viewportBounds = currentCamera->getViewportBounds();
+	Common::Rect localViewport = viewportBounds;
+	Common::Point accumulatedOffset = viewportBounds.origin();
+	StageActor *currentStage = _parentStage;
+	while (currentStage != nullptr && currentStage != currentCamera->getParentStage()) {
+		Common::Rect parentBounds = currentStage->getBbox();
+		accumulatedOffset -= parentBounds.origin();
+		currentStage = currentStage->getParentStage();
+	}
+	localViewport.moveTo(accumulatedOffset.x, accumulatedOffset.y);
+
+	if (_boundingBox.intersects(localViewport)) {
+		// Apply horizontal parallax.
+		int16 parallaxAdjustedLeft = _boundingBox.left;
+		if (hasHorizontalParallax) {
+			// Calculate offset from viewport center to object center.
+			int16 viewportHalfWidth = localViewport.width() / 2;
+			int16 boundsHalfWidth = _boundingBox.width() / 2;
+			int centerOffset = (_boundingBox.left + boundsHalfWidth) - (localViewport.left + viewportHalfWidth);
+
+			// Multiply by parallax factor to simulate depth.
+			double parallaxOffset = static_cast<double>(centerOffset) * _parallaxFactorX;
+			parallaxAdjustedLeft += static_cast<int16>(parallaxOffset);
+		}
+
+		// Apply vertical parallax.
+		int16 parallaxAdjustedTop = _boundingBox.top;
+		if (hasVerticalParallax) {
+			// Calculate offset from viewport center to object center.
+			int16 viewportHalfHeight = localViewport.height() / 2;
+			int16 boundsHalfHeight = _boundingBox.height() / 2;
+			int centerOffset = (_boundingBox.top + boundsHalfHeight) - (localViewport.top + viewportHalfHeight);
+
+			// Multiply by parallax factor to simulate depth.
+			double parallaxOffset = static_cast<double>(centerOffset) * _parallaxFactorY;
+			parallaxAdjustedTop += static_cast<int16>(parallaxOffset);
+		}
+
+		_boundingBox.moveTo(parallaxAdjustedLeft, parallaxAdjustedTop);
 	}
 }
 
diff --git a/engines/mediastation/actor.h b/engines/mediastation/actor.h
index e74a8597d87..0905f6c68d4 100644
--- a/engines/mediastation/actor.h
+++ b/engines/mediastation/actor.h
@@ -49,7 +49,7 @@ enum ActorType {
 	kActorTypeLKZazu = 0x000f,
 	kActorTypeLKConstellations = 0x0010,
 	kActorTypeDocument = 0x0011,
-	kActorTypeImageSet = 0x001d,
+	kActorTypeDiskImage = 0x001d,
 	kActorTypeCursor = 0x000c, // CSR
 	kActorTypePrinter = 0x0019, // PRT
 	kActorTypeMovie = 0x0016, // MOV
@@ -100,6 +100,14 @@ enum ActorHeaderSectionType {
 	kActorHeaderActorName = 0x0bb8,
 	kStreamMovieProxyInfo = 0x06ac,
 
+	// DISK IMAGE ACTOR FIELDS.
+	kActorHeaderDiskImageMaxStrips = 0x774,
+	kActorHeaderDiskImageStripWidth = 0x775,
+	kActorHeaderDiskImageUnk1 = 0x776,
+	kActorHeaderDiskImageMaxImages = 0x777,
+	kActorHeaderDiskImageStripInfo = 0x778,
+	kActorHeaderDiskImageUnkRect = 0x779,
+
 	// PATH FIELDS.
 	kActorHeaderStartPoint = 0x060e,
 	kActorHeaderEndPoint = 0x060f,
@@ -234,8 +242,9 @@ public:
 	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 void preload(const Common::Rect &rect, bool fireStepEvent = true) {};
 	virtual bool isRectInMemory(const Common::Rect &rect) { return true; }
+	virtual bool isReadyToDraw(DisplayContext &displayContext) { return true; }
 	virtual bool isLoading() { return false; }
 
 	virtual bool isSpatialActor() const override { return true; }
@@ -271,14 +280,15 @@ public:
 	StageActor *getParentStage() const { return _parentStage; }
 
 	virtual void invalidateLocalBounds();
-	virtual void setAdjustedBounds(CylindricalWrapMode alignmentMode);
+	virtual void setAdjustedBounds(CylindricalWrapMode wrapMode);
 
 protected:
 	uint _stageId = 0;
 	int _zIndex = 0;
 	double _dissolveFactor = 1.0;
-	double _scaleX = 0.0;
-	double _scaleY = 0.0;
+
+	double _parallaxFactorX = 0.0;
+	double _parallaxFactorY = 0.0;
 	Common::Rect _boundingBox;
 	Common::Rect _originalBoundingBox;
 	bool _isVisible = false;
diff --git a/engines/mediastation/actors/camera.cpp b/engines/mediastation/actors/camera.cpp
index 2618cd11379..195af832499 100644
--- a/engines/mediastation/actors/camera.cpp
+++ b/engines/mediastation/actors/camera.cpp
@@ -118,7 +118,7 @@ ScriptValue CameraActor::callMethod(BuiltInMethod methodId, Common::Array<Script
 
 	case kStartPanMethod: {
 		ARGCOUNTCHECK(3);
-		int16 deltaX = static_cast<uint16>(args[0].asFloat());
+		int16 deltaX = static_cast<int16>(args[0].asFloat());
 		int16 deltaY = static_cast<int16>(args[1].asFloat());
 		double duration = args[2].asFloatOrTime();
 		_nextViewportOrigin = Common::Point(deltaX, deltaY) + _currentViewportOrigin;
@@ -276,6 +276,7 @@ void CameraActor::addToStage() {
 	if (_parentStage != nullptr) {
 		_parentStage->addCamera(this);
 		invalidateLocalBounds();
+		_addedToStage = true;
 	}
 }
 
@@ -338,6 +339,7 @@ void CameraActor::drawUsingCamera(DisplayContext &destContext, const Common::Arr
 			if (_overlayImage == nullptr) {
 				// Draw this image directly to the provided display context.
 				debugC(6, kDebugGraphics, "(no overlay)");
+				invalidateLocalBounds();
 				drawObject(destContext, destContext, entityToDraw);
 			} else {
 				// Draw this image to our internal display context, so we can apply the
@@ -345,6 +347,8 @@ void CameraActor::drawUsingCamera(DisplayContext &destContext, const Common::Arr
 				debugC(6, kDebugGraphics, "(overlay)");
 				drawObject(destContext, _childrenWithOverlayContext, entityToDraw);
 			}
+		} else {
+			debugC(6, kDebugGraphics, "(not visible)");
 		}
 	}
 
@@ -367,19 +371,70 @@ void CameraActor::drawObject(DisplayContext &sourceContext, DisplayContext &dest
 		return;
 	}
 
+	// Draw object without any wrapping.
 	objectToDraw->setAdjustedBounds(kWrapNone);
 	Common::Rect visibleBounds = objectToDraw->getBbox();
 	if (sourceContext.rectIsInClip(visibleBounds)) {
 		objectToDraw->draw(destContext);
 	}
 
+	// Draw object with only cylindrical X wrapping if required.
 	if (_parentStage->cylindricalX()) {
-		warning("[%s] %s: CylindricalX not handled yet", debugName(), __func__);
+		objectToDraw->setAdjustedBounds(kWrapRight);
+		visibleBounds = objectToDraw->getBbox();
+		if (sourceContext.rectIsInClip(visibleBounds)) {
+			objectToDraw->draw(destContext);
+		}
+
+		objectToDraw->setAdjustedBounds(kWrapLeft);
+		visibleBounds = objectToDraw->getBbox();
+		if (sourceContext.rectIsInClip(visibleBounds)) {
+			objectToDraw->draw(destContext);
+		}
 	}
 
+	// Draw object with only cylindrical Y wrapping if required.
 	if (_parentStage->cylindricalY()) {
-		warning("[%s] %s: CylindricalY not handled yet", debugName(), __func__);
+		objectToDraw->setAdjustedBounds(kWrapDown);
+		visibleBounds = objectToDraw->getBbox();
+		if (sourceContext.rectIsInClip(visibleBounds)) {
+			objectToDraw->draw(destContext);
+		}
+
+		objectToDraw->setAdjustedBounds(kWrapUp);
+		visibleBounds = objectToDraw->getBbox();
+		if (sourceContext.rectIsInClip(visibleBounds)) {
+			objectToDraw->draw(destContext);
+		}
 	}
+
+	// Draw object with both cylindrical X and cylindrical Y wrapping if required.
+	if (_parentStage->cylindricalX() && _parentStage->cylindricalY()) {
+		objectToDraw->setAdjustedBounds(kWrapRightDown);
+		visibleBounds = objectToDraw->getBbox();
+		if (sourceContext.rectIsInClip(visibleBounds)) {
+			objectToDraw->draw(destContext);
+		}
+
+		objectToDraw->setAdjustedBounds(kWrapLeftUp);
+		visibleBounds = objectToDraw->getBbox();
+		if (sourceContext.rectIsInClip(visibleBounds)) {
+			objectToDraw->draw(destContext);
+		}
+
+		objectToDraw->setAdjustedBounds(kWrapLeftDown);
+		visibleBounds = objectToDraw->getBbox();
+		if (sourceContext.rectIsInClip(visibleBounds)) {
+			objectToDraw->draw(destContext);
+		}
+
+		objectToDraw->setAdjustedBounds(kWrapRightUp);
+		visibleBounds = objectToDraw->getBbox();
+		if (sourceContext.rectIsInClip(visibleBounds)) {
+			objectToDraw->draw(destContext);
+		}
+	}
+
 	objectToDraw->setAdjustedBounds(kWrapNone);
 }
 
@@ -496,12 +551,8 @@ void CameraActor::timerEvent() {
 			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);
+					Common::Rect advanceRect = getAdvanceRect();
+					_parentStage->preload(advanceRect, false);
 				} else {
 					runScriptResponseIfExists(kCameraPanAbortEvent);
 					stopPan();
@@ -519,13 +570,13 @@ void CameraActor::timerEvent() {
 				} else {
 					Common::Rect currentBounds = getBbox();
 					Common::Rect preloadBounds(_nextViewportOrigin, currentBounds.width(), currentBounds.height());
-					_parentStage->preload(preloadBounds);
+					_parentStage->preload(preloadBounds, false);
 				}
 			}
 		} else {
 			Common::Rect currentBounds = getBbox();
 			Common::Rect preloadBounds(_nextViewportOrigin, currentBounds.width(), currentBounds.height());
-			_parentStage->preload(preloadBounds);
+			_parentStage->preload(preloadBounds, false);
 		}
 	}
 }
@@ -566,12 +617,25 @@ void CameraActor::adjustCameraViewport(Common::Point &viewportToAdjust) {
 		return;
 	}
 
+	Common::Point stageExtent = _parentStage->extent();
+
+	// Normalize viewport position for cylindrical wrapping.
+	// When the viewport scrolls beyond the stage boundaries,
+	// wrap it back to the opposite edge to create seamless infinite scrolling.
 	if (_parentStage->cylindricalX()) {
-		warning("[%s] %s: CylindricalX not handled yet", debugName(), __func__);
+		if (viewportToAdjust.x >= stageExtent.x) {
+			viewportToAdjust.x -= stageExtent.x;
+		} else if (viewportToAdjust.x < 0) {
+			viewportToAdjust.x += stageExtent.x;
+		}
 	}
 
 	if (_parentStage->cylindricalY()) {
-		warning("[%s] %s: CylindricalY not handled yet", debugName(), __func__);
+		if (viewportToAdjust.y >= stageExtent.y) {
+			viewportToAdjust.y -= stageExtent.y;
+		} else if (viewportToAdjust.y < 0) {
+			viewportToAdjust.y += stageExtent.y;
+		}
 	}
 }
 
@@ -598,34 +662,32 @@ void CameraActor::calcNewViewportOrigin() {
 }
 
 bool CameraActor::cameraWithinStage(const Common::Point &candidate) {
+	bool result = true;
 	if (_parentStage == nullptr) {
-		return true;
+		return result;
 	}
 
-	bool result = true;
 	// We can only be out of horizontal bounds if we have a requested delta and
-	// are not doing X axis wrapping.
+	// are not doing X 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) {
+		bool cameraPastLeftBoundary = candidate.x < 0;
+		if (cameraPastRightBoundary || cameraPastLeftBoundary) {
 			result = false;
 		}
 		debugC(6, kDebugCamera, "[%s] %s: %s [rightBoundary: %d, extent: %d]", debugName(), __func__, result ? "true" : "false", candidateRightBoundary, _parentStage->extent().x);
 	}
 
 	// We can only be out of vertical bounds if we have a requested delta and
-	// are not doing Y axis wrapping.
+	// are not doing Y 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) {
+		bool cameraPastTopBoundary = candidate.y < 0;
+		if (cameraPastBottomBoundary || cameraPastTopBoundary) {
 			result = false;
 		}
 		debugC(6, kDebugCamera, "[%s] %s: %s [bottomBoundary: %d, extent: %d]", debugName(), __func__, result ? "true" : "false", candidateBottomBoundary, _parentStage->extent().y);
@@ -660,4 +722,46 @@ double CameraActor::percentComplete() {
 	return percentValue;
 }
 
+Common::Rect CameraActor::getAdvanceRect() {
+	Common::Rect viewportBounds = getViewportBounds();
+	Common::Point viewportBoundsOrigin = viewportBounds.origin();
+	int16 viewportWidth = viewportBounds.width();
+	int16 viewportHeight = viewportBounds.height();
+
+	// These constants seem to be set for smooth streaming from CD-ROM, but disk
+	// image actors also rely on a properly large advance rect for strip loading.
+	// Divisor for calculating horizontal preload expansion in the pan direction.
+	const double HORIZONTAL_PRELOAD_EXPANSION_FACTOR = 2.25;
+	// Multiplier for vertical preload expansion.
+	const int16 VERTICAL_PRELOAD_EXPANSION_FACTOR = 2;
+
+	// Handle horizontal panning.
+	if (_panDelta.x < 0) {
+		// We're panning left, so expand the advance rect leftward.
+		int16 quotient = viewportWidth / HORIZONTAL_PRELOAD_EXPANSION_FACTOR;
+		viewportBoundsOrigin.x -= quotient;
+		viewportWidth += quotient;
+	} else if (_panDelta.x > 0) {
+		// We're panning right, so expand the advance rect rightward.
+		int16 quotient = viewportWidth / HORIZONTAL_PRELOAD_EXPANSION_FACTOR;
+		viewportWidth += quotient;
+	}
+
+	// Handle vertical panning.
+	if (_panDelta.y < 0) {
+		// We're panning up, so expand the advance rect upward.
+		int16 quotient = viewportHeight / HORIZONTAL_PRELOAD_EXPANSION_FACTOR;
+		viewportBoundsOrigin.y -= quotient;
+		viewportHeight *= VERTICAL_PRELOAD_EXPANSION_FACTOR;
+	} else if (_panDelta.y > 0) {
+		// We're panning down, so expand the advance rect downward.
+		viewportHeight *= VERTICAL_PRELOAD_EXPANSION_FACTOR;
+	}
+
+	// Construct and return the advance rect with the adjusted origin and dimensions.
+	adjustCameraViewport(viewportBoundsOrigin);
+	Common::Rect advanceRect(viewportBoundsOrigin, viewportWidth, viewportHeight);
+	return advanceRect;
+}
+
 } // End of namespace MediaStation
diff --git a/engines/mediastation/actors/camera.h b/engines/mediastation/actors/camera.h
index 2606b3008f4..a3ed09a9843 100644
--- a/engines/mediastation/actors/camera.h
+++ b/engines/mediastation/actors/camera.h
@@ -100,6 +100,7 @@ private:
 	void adjustCameraViewport(Common::Point &viewportToAdjust);
 	void calcNewViewportOrigin();
 	double percentComplete();
+	Common::Rect getAdvanceRect();
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/actors/diskimage.cpp b/engines/mediastation/actors/diskimage.cpp
new file mode 100644
index 00000000000..081ee0f5977
--- /dev/null
+++ b/engines/mediastation/actors/diskimage.cpp
@@ -0,0 +1,505 @@
+/* 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/diskimage.h"
+#include "mediastation/debugchannels.h"
+#include "mediastation/graphics.h"
+#include "mediastation/mediastation.h"
+
+namespace MediaStation {
+
+DiskImageActor::~DiskImageActor() {
+	if (isVisible()) {
+		invalidateLocalBounds();
+	}
+
+	unregisterWithStreamManager();
+	purge();
+}
+
+void DiskImageActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
+	switch (paramType) {
+	case kActorHeaderChannelIdent:
+		_channelIdent = chunk.readTypedChannelIdent();
+		registerWithStreamManager();
+		break;
+
+	case kActorHeaderStartup:
+		_isVisible = static_cast<bool>(chunk.readTypedByte());
+		break;
+
+	case kActorHeaderDiscardAfterUse:
+		// The original just reads this and throws it away.
+		chunk.readTypedUint16();
+		break;
+
+	case kActorHeaderLoadType:
+		_shouldDecompressInPlace = static_cast<bool>(chunk.readTypedByte());
+		break;
+
+	case kActorHeaderInstallType:
+		// In the original, this controls behavior if the files are NOT installed. But since
+		// the "installation" is just copying from the CD-ROM, we can treat the game as always
+		// installed. So just throw away this value.
+		chunk.readTypedByte();
+		break;
+
+	case kActorHeaderDiskImageMaxStrips:
+		_maxStripsInMemory = chunk.readTypedUint16();
+		if (_maxStripsInMemory == 0) {
+			_maxStripsInMemory = 1;
+		}
+		break;
+
+	case kActorHeaderDiskImageStripWidth:
+		_stripThickness = chunk.readTypedUint16();
+		if (_stripThickness == 0) {
+			_stripThickness = MediaStationEngine::SCREEN_WIDTH;
+		}
+		break;
+
+	case kActorHeaderDiskImageUnk1:
+		_useVerticalStrips = (static_cast<bool>(chunk.readTypedByte()));
+		break;
+
+	case kActorHeaderDiskImageMaxImages:
+		_maxImagesInMemory = chunk.readTypedUint16();
+		if (_maxImagesInMemory == 0) {
+			_maxImagesInMemory = 1;
+		}
+		break;
+
+	case kActorHeaderDiskImageStripInfo:
+		setStripInfo(chunk);
+		break;
+
+	case kActorHeaderDiskImageUnkRect:
+		// The original reads this rect and then immediately throws it away.
+		chunk.readTypedRect();
+		break;
+
+	default:
+		SpatialEntity::readParameter(chunk, paramType);
+	}
+}
+
+ScriptValue DiskImageActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
+	ScriptValue returnValue;
+	switch (methodId) {
+	case kSpatialShowMethod:
+		ARGCOUNTCHECK(0);
+		if (_isVisible == false) {
+			_isVisible = true;
+			invalidateLocalBounds();
+		}
+		break;
+
+	case kSpatialHideMethod:
+		ARGCOUNTCHECK(0);
+		if (_isVisible == true) {
+			_isVisible = false;
+			invalidateLocalBounds();
+		}
+		break;
+
+	// setDissolveFactor is handled elsewhere in our reimplementation.
+
+	case kPreloadMethod: {
+		ARGCOUNTCHECK(4);
+		int16 left = static_cast<int16>(args[0].asFloat());
+		int16 top = static_cast<int16>(args[1].asFloat());
+		int16 width = static_cast<int16>(args[2].asFloat());
+		int16 height = static_cast<int16>(args[3].asFloat());
+		Common::Rect rectToPreload(Common::Point(left, top), width, height);
+		Common::Point boundsOrigin = getBbox().origin();
+		rectToPreload.translate(boundsOrigin.x, boundsOrigin.y);
+		preload(rectToPreload);
+		break;
+	}
+
+	case kPurgeMethod:
+		ARGCOUNTCHECK(0);
+		_isVisible = false;
+		invalidateLocalBounds();
+		purge();
+		break;
+
+	case kStopLoadMethod:
+		ARGCOUNTCHECK(0);
+		stopLoad();
+		break;
+
+	case kIsLoadingMethod:
+		ARGCOUNTCHECK(0);
+		returnValue.setToBool(_isLoading);
+		break;
+
+	case kIsRectInMemoryMethod: {
+		ARGCOUNTCHECK(4);
+		int16 left = static_cast<int16>(args[0].asFloat());
+		int16 top = static_cast<int16>(args[1].asFloat());
+		int16 width = static_cast<int16>(args[2].asFloat());
+		int16 height = static_cast<int16>(args[3].asFloat());
+		Common::Rect rectToCheck(Common::Point(left, top), width, height);
+		bool rectIsInMemory = isRectInMemory(rectToCheck);
+		returnValue.setToBool(rectIsInMemory);
+		break;
+	}
+
+	default:
+		returnValue = SpatialEntity::callMethod(methodId, args);
+	}
+
+	return returnValue;
+}
+
+void DiskImageActor::process() {
+	if (_isLoading) {
+		// All timer events are scheduled for 0.0 seconds, so just fire them immediately.
+		// We don't need to track time separately.
+		timerEvent();
+	}
+}
+
+void DiskImageActor::readChunk(Chunk &chunk) {
+	uint index = chunk.readTypedUint16();
+	ImageInfo imageInfo(chunk);
+
+	if (!_stripInfoNodes.contains(index)) {
+		error("[%s] %s: Strip index %d not found", debugName(), __func__, index);
+	}
+	StripInfoNode &stripInfoNode = _stripInfoNodes.getVal(index);
+
+	if (_useVerticalStrips) {
+		// Vertical strips. Arranged horizontally, so the left edge varies by strip index.
+		Common::Point stripOrigin(
+			_originalBoundingBox.left + (_stripThickness * index),
+			_originalBoundingBox.top
+		);
+		stripInfoNode.rect = Common::Rect(stripOrigin, imageInfo._dimensions.x, imageInfo._dimensions.y);
+
+	} else {
+		// Horizontal strips. Arranged vertically, so the top edge varies by strip index.
+		Common::Point stripOrigin(
+			_originalBoundingBox.left,
+			_originalBoundingBox.top + (_stripThickness * index)
+		);
+		stripInfoNode.rect = Common::Rect(stripOrigin, imageInfo._dimensions.x, imageInfo._dimensions.y);
+	}
+	stripInfoNode.isLoaded = true;
+
+	PixMapImage *stripImage = new PixMapImage(chunk, imageInfo, _shouldDecompressInPlace);
+	StripImageNode stripImageNode;
+	stripImageNode.image = stripImage;
+	stripImageNode.lastDrawTime = g_system->getMillis();
+	stripImageNode.isLoaded = true;
+	_stripImageNodes.setVal(index, stripImageNode);
+
+	// This was originally in RT_DiskImageActor::releaseBuffer, but it's here since
+	// we read data synchronously for now.
+	if (_isLoadingStrips) {
+		if (isRectInMemory(_rectToLoad)) {
+			if (_firePreloadEvent) {
+				generateLoadEvent(kDiskImageActorStepEvent);
+			}
+			stopLoad();
+		} else {
+			// The original scheduled a zero-length timer for the next load.
+			unregisterWithStreamManager();
+		}
+	}
+}
+
+void DiskImageActor::draw(DisplayContext &displayContext) {
+	if (!_isVisible || !isReadyToDraw(displayContext)) {
+		// Only draw if all required strips are ready.
+		return;
+	}
+
+	// Draw each loaded strip that intersects the clip region.
+	for (auto it = _stripInfoNodes.begin(); it != _stripInfoNodes.end(); ++it) {
+		uint stripIndex = it->_key;
+		const StripInfoNode &stripInfo = it->_value;
+
+		if (stripInfo.isLoaded) {
+			bool rectInClipRegion = displayContext.rectIsInClip(stripInfo.rect);
+			if (rectInClipRegion) {
+				// Update the last access time for cache management.
+				if (!_stripImageNodes.contains(stripIndex)) {
+					error("[%s] %s: Strip index %d marked as loaded but image node not found", debugName(), __func__, stripIndex);
+				}
+				StripImageNode &imageNode = _stripImageNodes.getVal(stripIndex);
+				imageNode.lastDrawTime = g_system->getMillis();
+
+				// Draw the strip image at its designated position.
+				g_engine->getDisplayManager()->imageBlit(
+					stripInfo.rect.origin(), imageNode.image, _dissolveFactor, &displayContext
+				);
+			}
+		}
+	}
+}
+
+void DiskImageActor::preload(const Common::Rect &rect, bool fireStepEvent) {
+	// Check whether the area is already in memory.
+	debugCN(5, kDebugGraphics, "[%s] %s: (%d, %d, %d, %d)", debugName(), __func__, PRINT_RECT(rect));
+	Common::Rect rectToLoad = rect.findIntersectingRect(getBbox());
+	bool rectIsAlreadyInMemory = isRectInMemory(rectToLoad);
+
+	if (!rectIsAlreadyInMemory) {
+		debugC(5, kDebugGraphics, " (not loaded)");
+		setStripsToLoad(rectToLoad);
+		_isLoading = true;
+		_firePreloadEvent = fireStepEvent;
+		_rectToLoad = rectToLoad;
+
+	} else if (fireStepEvent) {
+		debugC(5, kDebugGraphics, " (loaded)");
+		generateLoadEvent(kDiskImageActorStepEvent);
+	}
+}
+
+bool DiskImageActor::isReadyToDraw(DisplayContext &displayContext) {
+	for (auto it = _stripInfoNodes.begin(); it != _stripInfoNodes.end(); ++it) {
+		const StripInfoNode &stripInfo = it->_value;
+		bool stripRequiredButMissing = displayContext.rectIsInClip(stripInfo.rect) && !stripInfo.isLoaded;
+		if (stripRequiredButMissing) {
+			debugC(7, kDebugGraphics, "[%s] %s: Strip %d not loaded but in clip region", debugName(), __func__, it->_key);
+			return false;
+		}
+	}
+
+	return true;
+}
+
+bool DiskImageActor::isRectInMemory(const Common::Rect &rectToCheck) {
+	// Check whether all strips that intersect with the specified rectangle are loaded.
+	for (auto it = _stripInfoNodes.begin(); it != _stripInfoNodes.end(); ++it) {
+		const StripInfoNode &stripInfo = it->_value;
+		bool stripNotReady = !stripInfo.isLoaded || stripInfo.isLoadScheduled;
+		bool stripNeededButNotReady = stripInfo.rect.intersects(rectToCheck) && stripNotReady;
+		if (stripNeededButNotReady) {
+			debugC(5, kDebugGraphics, "[%s] %s: Strip %d not loaded but in clip region", debugName(), __func__, it->_key);
+			return false;
+		}
+	}
+
+	return true;
+}
+
+void DiskImageActor::setStripInfo(Chunk &chunk) {
+	StripInfoNode stripInfo;
+	uint index = chunk.readTypedUint16();
+	stripInfo.streamId = chunk.readTypedUint16();
+	stripInfo.lengthInBytes = chunk.readTypedUint32();
+
+	if (_useVerticalStrips) {
+		// Calculate the left edge of this strip by offsetting from the original origin.
+		Common::Point stripOrigin(
+			_originalBoundingBox.left + (_stripThickness * index),
+			_originalBoundingBox.top
+		);
+
+		// Each strip spans the full height of the original bounds.
+		stripInfo.rect = Common::Rect(stripOrigin, _stripThickness, _originalBoundingBox.height());
+
+	} else {
+		// Calculate the top edge of this strip by offsetting from the original origin.
+		Common::Point stripOrigin(
+			_originalBoundingBox.left,
+			_originalBoundingBox.top + (_stripThickness * index)
+		);
+
+		// Each strip spans the full width of the original bounds.
+		stripInfo.rect = Common::Rect(stripOrigin, _originalBoundingBox.width(), _stripThickness);
+	}
+
+	_stripInfoNodes.setVal(index, stripInfo);
+}
+
+void DiskImageActor::setStripsToLoad(const Common::Rect &rectToLoad) {
+	// Identify strips that intersect with the specified region and mark them for loading.
+	for (auto it = _stripInfoNodes.begin(); it != _stripInfoNodes.end(); ++it) {
+		StripInfoNode &stripInfo = it->_value;
+
+		// Only consider strips that are not already loaded.
+		if (!stripInfo.isLoaded) {
+			bool stripIntersectsRectToLoad = stripInfo.rect.intersects(rectToLoad);
+			if (stripIntersectsRectToLoad) {
+				stripInfo.isLoadScheduled = true;
+			}
+		}
+	}
+}
+
+void DiskImageActor::unloadLeastRecentlyDrawnStrip() {
+	uint stripToUnload = UINT_MAX;
+	uint oldestTime = UINT_MAX; // Maximum value, will be replaced by any valid strip
+	for (auto it = _stripImageNodes.begin(); it != _stripImageNodes.end(); ++it) {
+		uint stripIndex = it->_key;
+		const StripImageNode &imageNode = it->_value;
+		if (!imageNode.isLoaded) {
+			continue;
+		}
+
+		// Find the oldest strip that doesn't intersect the viewport.
+		const StripInfoNode &stripInfo = _stripInfoNodes.getVal(stripIndex);
+		bool stripIntersectsViewport = stripInfo.rect.intersects(getBbox());
+		bool stripDrawnBeforeCurrentOldest = imageNode.lastDrawTime < oldestTime;
+		bool stripIsEligibleForUnload = !stripIntersectsViewport && stripDrawnBeforeCurrentOldest;
+		if (stripIsEligibleForUnload) {
+			oldestTime = imageNode.lastDrawTime;
+			stripToUnload = stripIndex;
+		}
+	}
+
+	// Unload the strip if it was found.
+	if (stripToUnload != UINT_MAX) {
+		StripImageNode &imageNode = _stripImageNodes.getVal(stripToUnload);
+		delete imageNode.image;
+		imageNode.image = nullptr;
+		imageNode.isLoaded = false;
+		_stripImageNodes.erase(stripToUnload);
+
+		StripInfoNode &stripInfo = _stripInfoNodes.getVal(stripToUnload);
+		stripInfo.isLoaded = false;
+	}
+}
+
+int DiskImageActor::getStripToLoad() {
+	if (_stripImageNodes.size() >= _maxImagesInMemory) {
+		unloadLeastRecentlyDrawnStrip();
+	}
+
+	for (auto it = _stripInfoNodes.begin(); it != _stripInfoNodes.end(); ++it) {
+		uint currentStripIndex = it->_key;
+		const StripInfoNode &stripInfo = it->_value;
+
+		// Find a strip that is scheduled to load but not yet loaded.
+		if (stripInfo.isLoadScheduled && !stripInfo.isLoaded) {
+			return currentStripIndex;
+		}
+	}
+	return -1;
+}
+
+void DiskImageActor::startStripLoad(uint stripIndex) {
+	// Retrieve the stream ID for the specified strip.
+	registerWithStreamManager();
+	StripInfoNode &stripInfo = _stripInfoNodes.getVal(stripIndex);
+	stripInfo.isLoadScheduled = false;
+	uint streamId = stripInfo.streamId;
+
+	// Open a stream feed for this strip.
+	_isLoadingStrips = true;
+	ImtStreamFeed *streamFeed = g_engine->getStreamFeedManager()->openStreamFeed(streamId);
+	streamFeed->readData();
+	g_engine->getStreamFeedManager()->closeStreamFeed(streamFeed);
+	_isLoadingStrips = false;
+}
+
+void DiskImageActor::timerEvent() {
+	int stripId = getStripToLoad();
+	if (stripId != -1) {
+		startStripLoad(stripId);
+	} else {
+		if (_firePreloadEvent) {
+			generateLoadEvent(kDiskImageActorEndEvent);
+		}
+		_isLoading = false;
+	}
+}
+
+void DiskImageActor::purge() {
+	// Delete all loaded images.
+	debugPrintNodes();
+	stopLoad();
+	for (auto it = _stripImageNodes.begin(); it != _stripImageNodes.end(); ++it) {
+		StripImageNode &imageNode = it->_value;
+		delete imageNode.image;
+		imageNode.image = nullptr;
+	}
+	_stripImageNodes.clear();
+
+	// Mark all strips as not loaded.
+	for (auto it = _stripInfoNodes.begin(); it != _stripInfoNodes.end(); ++it) {
+		StripInfoNode &stripInfo = it->_value;
+		stripInfo.isLoaded = false;
+		stripInfo.isLoadScheduled = false;
+	}
+
+	debugPrintNodes();
+}
+
+void DiskImageActor::stopLoad() {
+	_isLoading = false;
+	_firePreloadEvent = false;
+	_rectToLoad = Common::Rect();
+
+	unregisterWithStreamManager();
+}
+
+void DiskImageActor::generateLoadEvent(EventType eventType) {
+	// In the original, this does just queue an event.
+	runScriptResponseIfExists(eventType);
+}
+
+void DiskImageActor::debugPrintNodes() {
+	debugC(5, kDebugGraphics, "[%s] %s: STRIP NODES [", debugName(), __func__);
+	for (auto it = _stripInfoNodes.begin(); it != _stripInfoNodes.end(); ++it) {
+		uint stripIndex = it->_key;
+		const StripInfoNode &stripInfo = it->_value;
+		bool hasImage = _stripImageNodes.contains(stripIndex) && _stripImageNodes.getVal(stripIndex).image != nullptr;
+		debugC(5, kDebugGraphics, "\t\tStrip %d: loaded=%s, scheduled=%s, hasImage=%s, rect=(%d,%d,%d,%d)",
+			stripIndex,
+			stripInfo.isLoaded ? "yes" : "no",
+			stripInfo.isLoadScheduled ? "yes" : "no",
+			hasImage ? "yes" : "no",
+			stripInfo.rect.left, stripInfo.rect.top, stripInfo.rect.right, stripInfo.rect.bottom);
+	}
+	debugC(5, kDebugGraphics, "]");
+}
+
+void DiskImageActor::setAdjustedBounds(CylindricalWrapMode wrapMode) {
+	SpatialEntity::setAdjustedBounds(wrapMode);
+	Common::Point boundsOffset = _boundingBox.origin() - _originalBoundingBox.origin();
+
+	for (auto it = _stripInfoNodes.begin(); it != _stripInfoNodes.end(); ++it) {
+		uint stripIndex = it->_key;
+		StripInfoNode &stripInfo = it->_value;
+		Common::Point adjustedOrigin;
+
+		if (_useVerticalStrips) {
+			// Strips arranged left to right.
+			adjustedOrigin.x = _originalBoundingBox.left + (stripIndex * _stripThickness) + boundsOffset.x;
+			adjustedOrigin.y = _originalBoundingBox.top + boundsOffset.y;
+		} else {
+			// Strips arranged top to bottom.
+			adjustedOrigin.x = _originalBoundingBox.left + boundsOffset.x;
+			adjustedOrigin.y = _originalBoundingBox.top + (stripIndex * _stripThickness) + boundsOffset.y;
+		}
+
+		// Update the strip's origin whilst preserving its dimensions.
+		stripInfo.rect.moveTo(adjustedOrigin.x, adjustedOrigin.y);
+	}
+}
+
+} // End of namespace MediaStation
diff --git a/engines/mediastation/actors/diskimage.h b/engines/mediastation/actors/diskimage.h
new file mode 100644
index 00000000000..cdc49ee15cc
--- /dev/null
+++ b/engines/mediastation/actors/diskimage.h
@@ -0,0 +1,95 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MEDIASTATION_ACTORS_DISKIMAGE_H
+#define MEDIASTATION_ACTORS_DISKIMAGE_H
+
+#include "mediastation/actor.h"
+#include "mediastation/bitmap.h"
+#include "mediastation/datafile.h"
+#include "mediastation/mediascript/scriptvalue.h"
+#include "mediastation/mediascript/scriptconstants.h"
+
+namespace MediaStation {
+
+struct StripInfoNode {
+	bool isLoaded = false;
+	bool isLoadScheduled = false;
+	Common::Rect rect;
+	uint streamId = 0;
+	uint lengthInBytes = 0;
+};
+
+struct StripImageNode {
+	bool isLoaded = false;
+	PixMapImage *image = nullptr;
+	uint lastDrawTime = 0;
+};
+
+class DiskImageActor : public SpatialEntity, public ChannelClient {
+// Despite the name from the original, this is not a "disk image" but
+// a set of graphics (like a large background) that are streamed from
+// disk in a very particular way.
+public:
+	DiskImageActor() : SpatialEntity(kActorTypeDiskImage) {};
+	~DiskImageActor();
+
+	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;
+	virtual void draw(DisplayContext &displayContext) override;
+	virtual void preload(const Common::Rect &rect, bool fireStepEvent = true) override;
+	virtual bool isReadyToDraw(DisplayContext &displayContext) override;
+	virtual bool isRectInMemory(const Common::Rect &rectToCheck) override;
+	virtual void setAdjustedBounds(CylindricalWrapMode wrapMode) override;
+
+private:
+	void setStripInfo(Chunk &chunk);
+	void setStripsToLoad(const Common::Rect &rectToLoad);
+	int getStripToLoad();
+	void startStripLoad(uint stripIndex);
+	void timerEvent();
+
+	void purge();
+	void stopLoad();
+	void generateLoadEvent(EventType eventType);
+	void unloadLeastRecentlyDrawnStrip();
+	void debugPrintNodes();
+
+	Common::HashMap<uint, StripInfoNode> _stripInfoNodes;
+	Common::HashMap<uint, StripImageNode> _stripImageNodes;
+
+	Common::Rect _rectToLoad;
+	bool _shouldDecompressInPlace = false;
+	bool _isLoading = false;
+	bool _isLoadingStrips = false;
+	bool _firePreloadEvent = false;
+	uint _maxStripsInMemory = 0;
+	int16 _stripThickness = 0;
+	uint _maxImagesInMemory = 0;
+	bool _useVerticalStrips = false;
+};
+
+} // End of namespace MediaStation
+
+#endif
diff --git a/engines/mediastation/actors/document.cpp b/engines/mediastation/actors/document.cpp
index 2e66ee9f409..4e131117b2c 100644
--- a/engines/mediastation/actors/document.cpp
+++ b/engines/mediastation/actors/document.cpp
@@ -39,7 +39,7 @@ ScriptValue DocumentActor::callMethod(BuiltInMethod methodId, Common::Array<Scri
 		g_engine->quitGame();
 		break;
 
-	case kDocumentContextLoadInProgressMethod: {
+	case kIsLoadingMethod: {
 		ARGCOUNTCHECK(1);
 		uint contextId = args[0].asActorId();
 		bool isLoading = g_engine->getDocument()->isContextLoadInProgress(contextId);
diff --git a/engines/mediastation/actors/stage.cpp b/engines/mediastation/actors/stage.cpp
index 69240f6e44e..6fd0832e91f 100644
--- a/engines/mediastation/actors/stage.cpp
+++ b/engines/mediastation/actors/stage.cpp
@@ -66,24 +66,24 @@ void StageActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 	}
 }
 
-void StageActor::preload(const Common::Rect &rect) {
+void StageActor::preload(const Common::Rect &rect, bool fireStepEvent) {
 	if (cylindricalX()) {
-		preloadTest(rect, kWrapLeft);
+		preloadTest(rect, kWrapLeft, fireStepEvent);
 	}
 	if (cylindricalY()) {
-		preloadTest(rect, kWrapTop);
+		preloadTest(rect, kWrapUp, fireStepEvent);
 	}
 	if (cylindricalX() && cylindricalY()) {
-		preloadTest(rect, kWrapLeftTop);
+		preloadTest(rect, kWrapLeftUp, fireStepEvent);
 	}
-	preloadTest(rect, kWrapNone);
+	preloadTest(rect, kWrapNone, fireStepEvent);
 }
 
-void StageActor::preloadTest(const Common::Rect &rect, CylindricalWrapMode wrapMode) {
+void StageActor::preloadTest(const Common::Rect &rect, CylindricalWrapMode wrapMode, bool fireStepEvent) {
 	for (SpatialEntity *entity : _children) {
 		entity->setAdjustedBounds(wrapMode);
 		if (!entity->isRectInMemory(rect) && !entity->isLoading()) {
-			entity->preload(rect);
+			entity->preload(rect, fireStepEvent);
 		}
 	}
 }
@@ -94,10 +94,10 @@ bool StageActor::isRectInMemory(const Common::Rect &rect) {
 		result = isRectInMemoryTest(rect, kWrapLeft);
 	}
 	if (result && cylindricalY()) {
-		result = isRectInMemoryTest(rect, kWrapTop);
+		result = isRectInMemoryTest(rect, kWrapUp);
 	}
 	if (result && cylindricalY() && cylindricalX()) {
-		result = isRectInMemoryTest(rect, kWrapLeftTop);
+		result = isRectInMemoryTest(rect, kWrapLeftUp);
 	}
 	if (result) {
 		result = isRectInMemoryTest(rect, kWrapNone);
@@ -108,7 +108,7 @@ bool StageActor::isRectInMemory(const Common::Rect &rect) {
 bool StageActor::isRectInMemoryTest(const Common::Rect &rect, CylindricalWrapMode wrapMode) {
 	for (SpatialEntity *entity : _children) {
 		entity->setAdjustedBounds(wrapMode);
-		if (!entity->isRectInMemory(rect)) {
+		if (entity->isVisible() && !entity->isRectInMemory(rect)) {
 			return false;
 		}
 	}
@@ -171,10 +171,13 @@ void StageActor::drawUsingStage(DisplayContext &displayContext) {
 }
 
 void StageActor::invalidateRect(const Common::Rect &rect) {
+	Common::Point origin = _boundingBox.origin();
+	Common::Rect rectRelativeToParent = rect;
+	rectRelativeToParent.translate(origin.x, origin.y);
+
 	if (_parentStage != nullptr) {
-		Common::Point origin = _boundingBox.origin();
-		Common::Rect rectRelativeToParent = rect;
-		rectRelativeToParent.translate(origin.x, origin.y);
+		debugC(8, kDebugGraphics, "[%s] %s: (%d, %d, %d, %d) -> (%d, %d, %d, %d)",
+				debugName(), __func__, PRINT_RECT(rect), PRINT_RECT(rectRelativeToParent));
 
 		if (_cameras.size() == 0) {
 			_parentStage->invalidateRect(rectRelativeToParent);
@@ -182,7 +185,8 @@ void StageActor::invalidateRect(const Common::Rect &rect) {
 			invalidateUsingCameras(rectRelativeToParent);
 		}
 	} else {
-		warning("[%s] %s: Attempt to invalidate rect without a parent stage", debugName(), __func__);
+		debugC(5, kDebugGraphics, "[%s] %s (%d, %d, %d, %d): Attempt to invalidate rect (%d, %d, %d, %d) without a parent stage",
+			debugName(), __func__, PRINT_RECT(_boundingBox), PRINT_RECT(rectRelativeToParent));
 	}
 }
 
@@ -454,7 +458,7 @@ uint16 StageActor::findActorToAcceptMouseEventsObject(
 	}
 
 	if ((eventMask != 0) && cylindricalY()) {
-		handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, kWrapTop);
+		handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, kWrapUp);
 		if (handledEvents != 0) {
 			eventMask &= ~handledEvents;
 			result |= handledEvents;
@@ -462,7 +466,7 @@ uint16 StageActor::findActorToAcceptMouseEventsObject(
 	}
 
 	if ((eventMask != 0) && cylindricalY()) {
-		handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, kWrapBottom);
+		handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, kWrapDown);
 		if (handledEvents != 0) {
 			eventMask &= ~handledEvents;
 			result |= handledEvents;
@@ -470,7 +474,7 @@ uint16 StageActor::findActorToAcceptMouseEventsObject(
 	}
 
 	if ((eventMask != 0) && cylindricalY() && cylindricalX()) {
-		handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, kWrapLeftTop);
+		handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, kWrapLeftUp);
 		if (handledEvents != 0) {
 			eventMask &= ~handledEvents;
 			result |= handledEvents;
@@ -478,7 +482,7 @@ uint16 StageActor::findActorToAcceptMouseEventsObject(
 	}
 
 	if ((eventMask != 0) && cylindricalY() && cylindricalX()) {
-		handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, kWrapRightBottom);
+		handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, kWrapRightDown);
 		if (handledEvents != 0) {
 			result |= handledEvents;
 		}
diff --git a/engines/mediastation/actors/stage.h b/engines/mediastation/actors/stage.h
index 3da546cbe53..1eea5e72182 100644
--- a/engines/mediastation/actors/stage.h
+++ b/engines/mediastation/actors/stage.h
@@ -34,15 +34,15 @@ 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)
+	kWrapNone = 0,
+	kWrapRight = 1,
+	kWrapDown = 2,
+	kWrapRightDown = 3,
+	kWrapLeft = 4,
+	kWrapUp = 5,
+	kWrapLeftUp = 6,
+	kWrapLeftDown = 7,
+	kWrapRightUp = 8
 };
 
 class CameraActor;
@@ -56,7 +56,7 @@ public:
 
 	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 void preload(const Common::Rect &rect, bool fireStepEvent = true) override;
 	virtual bool isVisible() const override { return _children.size() > 0; }
 	virtual bool isRectInMemory(const Common::Rect &rect) override;
 
@@ -113,7 +113,7 @@ protected:
 	void addActorToStage(uint actorId);
 	void removeActorFromStage(uint actorId);
 	bool isRectInMemoryTest(const Common::Rect &rect, CylindricalWrapMode wrapMode);
-	void preloadTest(const Common::Rect &rect, CylindricalWrapMode wrapMode);
+	void preloadTest(const Common::Rect &rect, CylindricalWrapMode wrapMode, bool fireStepEvent);
 
 	bool assertHasNoParent(const SpatialEntity *entity);
 	bool assertHasParentThatIsNotMe(const SpatialEntity *entity) { return !assertIsMyChild(entity); }
diff --git a/engines/mediastation/context.cpp b/engines/mediastation/context.cpp
index 36c0f62ca65..c02c665b351 100644
--- a/engines/mediastation/context.cpp
+++ b/engines/mediastation/context.cpp
@@ -29,6 +29,7 @@
 #include "mediastation/actors/camera.h"
 #include "mediastation/actors/canvas.h"
 #include "mediastation/actors/cursor.h"
+#include "mediastation/actors/diskimage.h"
 #include "mediastation/actors/palette.h"
 #include "mediastation/actors/image.h"
 #include "mediastation/actors/path.h"
@@ -160,6 +161,10 @@ void MediaStationEngine::readCreateActorData(Chunk &chunk) {
 		actor = new CursorActor();
 		break;
 
+	case kActorTypeDiskImage:
+		actor = new DiskImageActor();
+		break;
+
 	default:
 		error("%s: No class for actor type 0x%x", __func__, static_cast<uint>(type));
 	}
diff --git a/engines/mediastation/graphics.cpp b/engines/mediastation/graphics.cpp
index b7ca392f367..cd327ff53d0 100644
--- a/engines/mediastation/graphics.cpp
+++ b/engines/mediastation/graphics.cpp
@@ -80,7 +80,9 @@ void Clip::addToRegion(const Common::Rect &rect) {
 }
 
 bool Clip::clipIntersectsRect(const Common::Rect &rect) {
-	return _region.intersects(rect);
+	Common::Rect adjustedRect = rect;
+	adjustedRect.translate(-_bounds.origin().x, -_bounds.origin().y);
+	return _region.intersects(adjustedRect);
 }
 
 void Clip::intersectWithRegion(const Common::Rect &rect) {
diff --git a/engines/mediastation/mediascript/scriptconstants.cpp b/engines/mediastation/mediascript/scriptconstants.cpp
index 119fcc7b3fe..9422115dd93 100644
--- a/engines/mediastation/mediascript/scriptconstants.cpp
+++ b/engines/mediastation/mediascript/scriptconstants.cpp
@@ -273,17 +273,17 @@ const char *builtInMethodToStr(BuiltInMethod method) {
 		return "SetMultipleSounds/IsPaused";
 	case kSetMousePositionMethod:
 		return "SetMousePosition";
-	case kGetXScaleMethod1:
-	case kGetXScaleMethod2:
-		return "GetXScale";
-	case kSetScaleMethod:
-		return "SetScale";
-	case kSetXScaleMethod:
-		return "SetXScale";
-	case kGetYScaleMethod:
-		return "GetYScale";
-	case kSetYScaleMethod:
-		return "SetYScale";
+	case kGetParallaxFactorXMethod1:
+	case kGetParallaxFactorXMethod2:
+		return "GetParallaxFactorX";
+	case kSetParallaxFactorMethod:
+		return "SetParallaxFactor";
+	case kSetParallaxFactorXMethod:
+		return "SetParallaxFactorX";
+	case kGetParallaxFactorYMethod:
+		return "GetParallaxFactorY";
+	case kSetParallaxFactorYMethod:
+		return "SetParallaxFactorY";
 	case kMovieResetMethod:
 		return "MovieReset";
 	case kSetCurrentClipMethod:
@@ -348,8 +348,8 @@ const char *builtInMethodToStr(BuiltInMethod method) {
 		return "BranchToScreen";
 	case kDocumentQuitMethod:
 		return "Quit";
-	case kDocumentContextLoadInProgressMethod:
-		return "ContextLoadInProgress";
+	case kIsLoadingMethod:
+		return "IsLoading";
 	case kDocumentContextIsLoadedMethod:
 		return "IsLoaded";
 	case kPathSetDurationMethod:
@@ -442,6 +442,14 @@ const char *builtInMethodToStr(BuiltInMethod method) {
 		return "PrependList";
 	case kSortMethod:
 		return "Sort";
+	case kPreloadMethod:
+		return "Preload";
+	case kPurgeMethod:
+		return "Purge";
+	case kStopLoadMethod:
+		return "StopLoad";
+	case kIsRectInMemoryMethod:
+		return "IsRectInMemory";
 	default:
 		return "UNKNOWN";
 	}
@@ -501,6 +509,10 @@ const char *eventTypeToStr(EventType type) {
 		return "TextInput";
 	case kTextErrorEvent:
 		return "TextError";
+	case kDiskImageActorStepEvent:
+		return "DiskImageActorStep";
+	case kDiskImageActorEndEvent:
+		return "DiskImageActorEnd";
 	case kCameraPanStepEvent:
 		return "CameraPanStep";
 	case kCameraPanAbortEvent:
diff --git a/engines/mediastation/mediascript/scriptconstants.h b/engines/mediastation/mediascript/scriptconstants.h
index eedc2622210..30b051b63d5 100644
--- a/engines/mediastation/mediascript/scriptconstants.h
+++ b/engines/mediastation/mediascript/scriptconstants.h
@@ -147,12 +147,12 @@ enum BuiltInMethod {
 	kSetMousePositionMethod = 0x129,
 	// It isn't clear what the difference is meant to be
 	// between these two, as the code looks the same for both.
-	kGetXScaleMethod1 = 0x16E,
-	kGetXScaleMethod2 = 0x17E,
-	kSetScaleMethod = 0x16F,
-	kSetXScaleMethod = 0x17F,
-	kGetYScaleMethod = 0x180,
-	kSetYScaleMethod = 0x181,
+	kGetParallaxFactorXMethod1 = 0x16E,
+	kGetParallaxFactorXMethod2 = 0x17E,
+	kSetParallaxFactorMethod = 0x16F,
+	kSetParallaxFactorXMethod = 0x17F,
+	kGetParallaxFactorYMethod = 0x180,
+	kSetParallaxFactorYMethod = 0x181,
 	kStartCachingMethod = 0x113,
 	kIsCachingMethod = 0x114,
 	kPauseMethod = 0xD0,
@@ -220,7 +220,7 @@ enum BuiltInMethod {
 	// DOCUMENT METHODS.
 	kDocumentBranchToScreenMethod = 0xC9,
 	kDocumentQuitMethod = 0xD9,
-	kDocumentContextLoadInProgressMethod = 0x169,
+	kIsLoadingMethod = 0x169,
 	kDocumentSetMultipleStreamsMethod = 0x174,
 	kDocumentSetMultipleSoundsMethod = 0x175,
 	kDocumentLoadContextMethod = 0x176,
@@ -290,6 +290,12 @@ enum BuiltInMethod {
 
 	// CURSOR METHODS.
 	kCursorSetMethod = 0xC8,
+
+	// DISK IMAGE ACTOR METHODS.
+	kPreloadMethod = 0x166,
+	kPurgeMethod = 0x167,
+	kStopLoadMethod = 0x168,
+	kIsRectInMemoryMethod = 0x16A,
 };
 const char *builtInMethodToStr(BuiltInMethod method);
 
@@ -319,6 +325,8 @@ enum EventType {
 	kPathStoppedEvent = 0x21,
 	kTextInputEvent = 0x25,
 	kTextErrorEvent = 0x26,
+	kDiskImageActorStepEvent = 0x27,
+	kDiskImageActorEndEvent = 0x28,
 	kCameraPanStepEvent = 0x29,
 	kCameraPanEndEvent = 0x2A,
 	kCameraPanAbortEvent = 0x2B,
diff --git a/engines/mediastation/module.mk b/engines/mediastation/module.mk
index 122dbf3a4d6..4fb162b68fc 100644
--- a/engines/mediastation/module.mk
+++ b/engines/mediastation/module.mk
@@ -5,6 +5,7 @@ MODULE_OBJS = \
 	actors/camera.o \
 	actors/canvas.o \
 	actors/cursor.o \
+	actors/diskimage.o \
 	actors/document.o \
 	actors/font.o \
 	actors/hotspot.o \


Commit: 3717f7f29de7e72cbe6d019b5c055b14b27f6e25
    https://github.com/scummvm/scummvm/commit/3717f7f29de7e72cbe6d019b5c055b14b27f6e25
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-04-30T20:19:20-04:00

Commit Message:
MEDIASTATION: Implement hotspot bounds checking more accurately to the original

Changed paths:
    engines/mediastation/actor.cpp
    engines/mediastation/actor.h
    engines/mediastation/actors/hotspot.cpp
    engines/mediastation/actors/hotspot.h
    engines/mediastation/actors/stage.cpp
    engines/mediastation/actors/stage.h
    engines/mediastation/datafile.cpp
    engines/mediastation/datafile.h


diff --git a/engines/mediastation/actor.cpp b/engines/mediastation/actor.cpp
index 30e1411fe4e..b2cfe54b3e2 100644
--- a/engines/mediastation/actor.cpp
+++ b/engines/mediastation/actor.cpp
@@ -89,6 +89,42 @@ const char *actorTypeToStr(ActorType type) {
 	}
 }
 
+void Polygon::loadFromParameterStream(Chunk & chunk) {
+	uint16 totalPoints = chunk.readTypedUint16();
+	for (uint16 i = 0; i < totalPoints; i++) {
+		Common::Point point = chunk.readTypedPoint();
+		_polygon.push_back(point);
+	}
+}
+
+bool Polygon::containsPoint(const Common::Point &point) const {
+	// We're in the bbox, but there might not be a polygon to check.
+	if (_polygon.empty()) {
+		return true;
+	}
+
+	// Each edge is checked whether it cuts the outgoing stream from the point.
+	int rcross = 0; // Number of right-side overlaps
+	for (unsigned i = 0; i < _polygon.size(); i++) {
+		const Common::Point &edgeStart = _polygon[i];
+		const Common::Point &edgeEnd = _polygon[(i + 1) % _polygon.size()];
+
+		// A vertex is a point? Then it lies on one edge of the polygon.
+		if (point == edgeStart)
+			return true;
+
+		if ((edgeStart.y > point.y) != (edgeEnd.y > point.y)) {
+			int term1 = (edgeStart.x - point.x) * (edgeEnd.y - point.y) - (edgeEnd.x - point.x) * (edgeStart.y - point.y);
+			int term2 = (edgeEnd.y - point.y) - (edgeStart.y - edgeEnd.y);
+			if ((term1 > 0) == (term2 >= 0))
+				rcross++;
+		}
+	}
+
+	// The point is strictly inside the polygon if and only if the number of overlaps is odd.
+	return ((rcross % 2) == 1);
+}
+
 void Actor::setId(uint id) {
 	_id = id;
 	_debugName = g_engine->formatActorName(this);
diff --git a/engines/mediastation/actor.h b/engines/mediastation/actor.h
index 0905f6c68d4..b7e80feb835 100644
--- a/engines/mediastation/actor.h
+++ b/engines/mediastation/actor.h
@@ -173,6 +173,15 @@ enum MouseEventFlag {
 	// There is no key up event.
 };
 
+class Polygon {
+public:
+	void loadFromParameterStream(Chunk &chunk);
+	bool containsPoint(const Common::Point &pointToCheck) const;
+
+private:
+	Common::Array<Common::Point> _polygon;
+};
+
 // Argument count validation macros for built-in script methods.
 // For exact argument count.
 #define ARGCOUNTCHECK(n) \
@@ -261,7 +270,7 @@ public:
 		const Common::Point &point,
 		uint16 eventMask,
 		MouseActorState &state,
-		bool inBounds) { return kNoFlag; }
+		bool clipMouseEvents) { return kNoFlag; }
 	virtual uint16 findActorToAcceptKeyboardEvents(
 		uint16 asciiCode,
 		uint16 eventMask,
@@ -293,7 +302,6 @@ protected:
 	Common::Rect _originalBoundingBox;
 	bool _isVisible = false;
 	bool _hasTransparency = false;
-	bool _getOffstageEvents = false;
 	StageActor *_parentStage = nullptr;
 
 	void moveToCentered(int16 x, int16 y);
diff --git a/engines/mediastation/actors/hotspot.cpp b/engines/mediastation/actors/hotspot.cpp
index 6eced672215..35e1994ddd7 100644
--- a/engines/mediastation/actors/hotspot.cpp
+++ b/engines/mediastation/actors/hotspot.cpp
@@ -27,14 +27,9 @@ namespace MediaStation {
 
 void HotspotActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 	switch (paramType) {
-	case kActorHeaderMouseActiveArea: {
-		uint16 total_points = chunk.readTypedUint16();
-		for (int i = 0; i < total_points; i++) {
-			Common::Point point = chunk.readTypedPoint();
-			_mouseActiveArea.push_back(point);
-		}
+	case kActorHeaderMouseActiveArea:
+		_mouseActiveArea.loadFromParameterStream(chunk);
 		break;
-	}
 
 	case kActorHeaderStartup:
 		_isActive = static_cast<bool>(chunk.readTypedByte());
@@ -53,42 +48,11 @@ void HotspotActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType)
 	}
 }
 
-bool HotspotActor::isInside(const Common::Point &pointToCheck) {
-	// No sense checking the polygon if we're not even in the bbox.
-	if (!_boundingBox.contains(pointToCheck)) {
-		return false;
-	}
-
-	// We're in the bbox, but there might not be a polygon to check.
-	if (_mouseActiveArea.empty()) {
-		return true;
+bool HotspotActor::inBounds(const Common::Point &point) {
+	if (_parentStage != nullptr) {
+		return _parentStage->inBounds(point, getBbox(), _mouseActiveArea);
 	}
-
-	// Polygon intersection code adapted from HADESCH engine, might need more
-	// refinement once more testing is possible.
-	Common::Point point = pointToCheck - Common::Point(_boundingBox.left, _boundingBox.top);
-	int rcross = 0; // Number of right-side overlaps
-
-	// Each edge is checked whether it cuts the outgoing stream from the point
-	Common::Array<Common::Point> _polygon = _mouseActiveArea;
-	for (unsigned i = 0; i < _polygon.size(); i++) {
-		const Common::Point &edgeStart = _polygon[i];
-		const Common::Point &edgeEnd = _polygon[(i + 1) % _polygon.size()];
-
-		// A vertex is a point? Then it lies on one edge of the polygon
-		if (point == edgeStart)
-			return true;
-
-		if ((edgeStart.y > point.y) != (edgeEnd.y > point.y)) {
-			int term1 = (edgeStart.x - point.x) * (edgeEnd.y - point.y) - (edgeEnd.x - point.x) * (edgeStart.y - point.y);
-			int term2 = (edgeEnd.y - point.y) - (edgeStart.y - edgeEnd.y);
-			if ((term1 > 0) == (term2 >= 0))
-				rcross++;
-		}
-	}
-
-	// The point is strictly inside the polygon if and only if the number of overlaps is odd
-	return ((rcross % 2) == 1);
+	return false;
 }
 
 ScriptValue HotspotActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
@@ -113,7 +77,7 @@ ScriptValue HotspotActor::callMethod(BuiltInMethod methodId, Common::Array<Scrip
 		int16 xToCheck = static_cast<int16>(args[0].asFloat());
 		int16 yToCheck = static_cast<int16>(args[1].asFloat());
 		Common::Point pointToCheck(xToCheck, yToCheck);
-		bool pointIsInside = isInside(pointToCheck);
+		bool pointIsInside = inBounds(pointToCheck);
 		returnValue.setToBool(pointIsInside);
 		break;
 	}
@@ -152,7 +116,11 @@ uint16 HotspotActor::findActorToAcceptMouseEvents(
 
 	uint16 result = 0;
 	if (isActive()) {
-		if (isInside(point)) {
+		if (clipMouseEvents && !_getOffstageEvents) {
+			eventMask &= ~(kMouseDownFlag | kMouseMovedFlag | kMouseEnterFlag | kMouseUnk1Flag);
+		}
+
+		if (inBounds(point)) {
 			if (eventMask & kMouseDownFlag) {
 				state.mouseDown = this;
 				result |= kMouseDownFlag;
@@ -199,7 +167,7 @@ void HotspotActor::deactivate() {
 			g_engine->setMouseDownHotspot(nullptr);
 		}
 		if (g_engine->getMouseInsideHotspot() == this) {
-			g_engine->setMouseDownHotspot(nullptr);
+			g_engine->setMouseInsideHotspot(nullptr);
 		}
 
 		invalidateMouse();
diff --git a/engines/mediastation/actors/hotspot.h b/engines/mediastation/actors/hotspot.h
index 44c8654e5d2..351a7859936 100644
--- a/engines/mediastation/actors/hotspot.h
+++ b/engines/mediastation/actors/hotspot.h
@@ -31,9 +31,8 @@ namespace MediaStation {
 class HotspotActor : public SpatialEntity {
 public:
 	HotspotActor() : SpatialEntity(kActorTypeHotspot) {};
-	virtual ~HotspotActor() { _mouseActiveArea.clear(); }
 
-	bool isInside(const Common::Point &pointToCheck);
+	bool inBounds(const Common::Point &point);
 	virtual bool isVisible() const override { return false; }
 	bool isActive() const { return _isActive; }
 	virtual bool interactsWithMouse() const override { return isActive(); }
@@ -45,7 +44,7 @@ public:
 		const Common::Point &point,
 		uint16 eventMask,
 		MouseActorState &state,
-		bool inBounds) override;
+		bool clipMouseEvents) override;
 
 	void activate();
 	void deactivate();
@@ -57,10 +56,11 @@ public:
 	virtual void mouseMovedEvent(const Common::Event &event) override;
 
 	uint _cursorResourceId = 0;
-	Common::Array<Common::Point> _mouseActiveArea;
 
 private:
 	bool _isActive = false;
+	bool _getOffstageEvents = false;
+	Polygon _mouseActiveArea;
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/actors/stage.cpp b/engines/mediastation/actors/stage.cpp
index 6fd0832e91f..f0616a76536 100644
--- a/engines/mediastation/actors/stage.cpp
+++ b/engines/mediastation/actors/stage.cpp
@@ -88,6 +88,88 @@ void StageActor::preloadTest(const Common::Rect &rect, CylindricalWrapMode wrapM
 	}
 }
 
+bool StageActor::inBounds(const Common::Point &point, const Common::Rect &bounds, const Polygon &polygon) {
+	if (_cameras.empty()) {
+		return inBoundsTest(point, bounds, polygon);
+	} else {
+		return inBoundsCamera(point, bounds, polygon);
+	}
+}
+
+bool StageActor::inBoundsTest(Common::Point point, const Common::Rect &bounds, const Polygon &polygon) {
+	if (bounds.contains(point)) {
+		point -= bounds.origin();
+		return polygon.containsPoint(point);
+	} else {
+		return false;
+	}
+}
+
+bool StageActor::inBoundsCamera(const Common::Point &point, const Common::Rect &bounds, const Polygon &polygon) {
+	for (uint i = 0; i < _cameras.size(); i++) {
+		(void)i; // Silence the unused variable warning. The original loop doesn't use this index.
+		bool isInCameraBounds = inBoundsObject(point, bounds, polygon);
+		if (isInCameraBounds) {
+			return true;
+		}
+	}
+	return false;
+}
+
+bool StageActor::inBoundsObject(const Common::Point &point, const Common::Rect &bounds, const Polygon &polygon) {
+	bool isInCameraBounds = inBoundsTest(point, bounds, polygon);
+
+	if (!isInCameraBounds && cylindricalX()) {
+		Common::Rect adjustedBounds = bounds;
+		adjustedBounds.translate(-_extent.x, 0);
+		isInCameraBounds = inBoundsTest(point, adjustedBounds, polygon);
+
+		if (!isInCameraBounds) {
+			adjustedBounds = bounds;
+			adjustedBounds.translate(_extent.x, 0);
+			isInCameraBounds = inBoundsTest(point, adjustedBounds, polygon);
+		}
+	}
+
+	if (!isInCameraBounds && cylindricalY()) {
+		Common::Rect adjustedBounds = bounds;
+		adjustedBounds.translate(0, -_extent.y);
+		isInCameraBounds = inBoundsTest(point, adjustedBounds, polygon);
+
+		if (!isInCameraBounds) {
+			adjustedBounds = bounds;
+			adjustedBounds.translate(0, _extent.y);
+			isInCameraBounds = inBoundsTest(point, adjustedBounds, polygon);
+		}
+	}
+
+	if (!isInCameraBounds && cylindricalX() && cylindricalY()) {
+		Common::Rect adjustedBounds = bounds;
+		adjustedBounds.translate(_extent.x, _extent.y);
+		isInCameraBounds = inBoundsTest(point, adjustedBounds, polygon);
+
+		if (!isInCameraBounds) {
+			adjustedBounds = bounds;
+			adjustedBounds.translate(-_extent.x, -_extent.y);
+			isInCameraBounds = inBoundsTest(point, adjustedBounds, polygon);
+
+			if (!isInCameraBounds) {
+				adjustedBounds = bounds;
+				adjustedBounds.translate(-_extent.x, _extent.y);
+				isInCameraBounds = inBoundsTest(point, adjustedBounds, polygon);
+
+				if (!isInCameraBounds) {
+					adjustedBounds = bounds;
+					adjustedBounds.translate(_extent.x, -_extent.y);
+					isInCameraBounds = inBoundsTest(point, adjustedBounds, polygon);
+				}
+			}
+		}
+	}
+
+	return isInCameraBounds;
+}
+
 bool StageActor::isRectInMemory(const Common::Rect &rect) {
 	bool result = true;
 	if (cylindricalX()) {
@@ -402,47 +484,41 @@ void StageActor::removeChildSpatialEntity(SpatialEntity *entity) {
 }
 
 uint16 StageActor::queryChildrenAboutMouseEvents(
-	const Common::Point &point,
-	uint16 eventMask,
-	MouseActorState &state,
-	CylindricalWrapMode wrapMode) {
+	const Common::Point &point, uint16 eventMask, MouseActorState &state, bool clipMouseEvents, CylindricalWrapMode wrapMode) {
 
 	uint16 result = 0;
-	Common::Point mousePosRelativeToStageOrigin = point - _boundingBox.origin();
 	for (auto childIterator = _children.end(); childIterator != _children.begin();) {
 		--childIterator; // Decrement first, then dereference
 		SpatialEntity *child = *childIterator;
-		debugC(6, kDebugEvents, "  [%s] %s: Checking %s (mousePos: %d, %d -> %d, %d) (bounds: %d, %d, %d, %d) (z-index: %d) (eventMask: 0x%02x) (result: 0x%02x) (wrapMode: %d)",
-			debugName(), __func__, child->debugName(),
-			point.x, point.y, mousePosRelativeToStageOrigin.x, mousePosRelativeToStageOrigin.y,
+		debugC(8, kDebugEvents, "  [%s] %s: Checking %s (mousePos: %d, %d) (bounds: %d, %d, %d, %d) (z-index: %d) (eventMask: 0x%02x) (result: 0x%02x) (wrapMode: %d)",
+			debugName(), __func__, child->debugName(), point.x, point.y,
 			PRINT_RECT(child->getBbox()), child->zIndex(), eventMask, result, wrapMode);
 
 		child->setAdjustedBounds(wrapMode);
-		uint16 handledEvents = child->findActorToAcceptMouseEvents(mousePosRelativeToStageOrigin, eventMask, state, true);
+		uint16 handledEvents = child->findActorToAcceptMouseEvents(point, eventMask, state, clipMouseEvents);
 		child->setAdjustedBounds(kWrapNone);
 
 		eventMask &= ~handledEvents;
 		result |= handledEvents;
+		if (eventMask == 0) {
+			break;
+		}
 	}
 	return result;
 }
 
 uint16 StageActor::findActorToAcceptMouseEventsObject(
-	const Common::Point &point,
-	uint16 eventMask,
-	MouseActorState &state,
-	bool inBounds) {
+	const Common::Point &point, uint16 eventMask, MouseActorState &state, bool clipMouseEvents) {
 
 	uint16 result = 0;
-
-	uint16 handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, kWrapNone);
+	uint16 handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, clipMouseEvents, kWrapNone);
 	if (handledEvents != 0) {
 		eventMask &= ~handledEvents;
 		result |= handledEvents;
 	}
 
 	if ((eventMask != 0) && cylindricalX()) {
-		handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, kWrapLeft);
+		handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, clipMouseEvents, kWrapLeft);
 		if (handledEvents != 0) {
 			eventMask &= ~handledEvents;
 			result |= handledEvents;
@@ -450,7 +526,7 @@ uint16 StageActor::findActorToAcceptMouseEventsObject(
 	}
 
 	if ((eventMask != 0) && cylindricalX()) {
-		handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, kWrapRight);
+		handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, clipMouseEvents, kWrapRight);
 		if (handledEvents != 0) {
 			eventMask &= ~handledEvents;
 			result |= handledEvents;
@@ -458,7 +534,7 @@ uint16 StageActor::findActorToAcceptMouseEventsObject(
 	}
 
 	if ((eventMask != 0) && cylindricalY()) {
-		handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, kWrapUp);
+		handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, clipMouseEvents, kWrapUp);
 		if (handledEvents != 0) {
 			eventMask &= ~handledEvents;
 			result |= handledEvents;
@@ -466,7 +542,7 @@ uint16 StageActor::findActorToAcceptMouseEventsObject(
 	}
 
 	if ((eventMask != 0) && cylindricalY()) {
-		handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, kWrapDown);
+		handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, clipMouseEvents, kWrapDown);
 		if (handledEvents != 0) {
 			eventMask &= ~handledEvents;
 			result |= handledEvents;
@@ -474,7 +550,7 @@ uint16 StageActor::findActorToAcceptMouseEventsObject(
 	}
 
 	if ((eventMask != 0) && cylindricalY() && cylindricalX()) {
-		handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, kWrapLeftUp);
+		handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, clipMouseEvents, kWrapLeftUp);
 		if (handledEvents != 0) {
 			eventMask &= ~handledEvents;
 			result |= handledEvents;
@@ -482,7 +558,7 @@ uint16 StageActor::findActorToAcceptMouseEventsObject(
 	}
 
 	if ((eventMask != 0) && cylindricalY() && cylindricalX()) {
-		handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, kWrapRightDown);
+		handledEvents = queryChildrenAboutMouseEvents(point, eventMask, state, clipMouseEvents, kWrapRightDown);
 		if (handledEvents != 0) {
 			result |= handledEvents;
 		}
@@ -492,55 +568,45 @@ uint16 StageActor::findActorToAcceptMouseEventsObject(
 }
 
 uint16 StageActor::findActorToAcceptMouseEventsCamera(
-	const Common::Point &point,
-	uint16 eventMask,
-	MouseActorState &state,
-	bool inBounds) {
+	const Common::Point &point, uint16 eventMask, MouseActorState &state, bool clipMouseEvents) {
 
 	uint16 result = 0;
-	for (CameraActor *camera : _cameras) {
-		Common::Point mousePosRelativeToCamera = point;
+	for (auto cameraIterator = _cameras.end(); cameraIterator != _cameras.begin();) {
+		--cameraIterator; // Decrement first, then dereference
+		CameraActor *camera = *cameraIterator;
+		Common::Point mousePosRelativeToCamera = point + camera->getViewportOrigin();
 		setCurrentCamera(camera);
 
-		Common::Point cameraViewportOrigin = camera->getViewportOrigin();
-		mousePosRelativeToCamera.x += cameraViewportOrigin.x;
-		mousePosRelativeToCamera.y += cameraViewportOrigin.y;
-
-		if (!inBounds) {
+		if (!clipMouseEvents) {
 			Common::Rect viewportBounds = camera->getViewportBounds();
 			if (!viewportBounds.contains(mousePosRelativeToCamera)) {
-				inBounds = true;
+				clipMouseEvents = true;
 			}
 		}
 
-		result = findActorToAcceptMouseEventsObject(mousePosRelativeToCamera, eventMask, state, inBounds);
+		result = findActorToAcceptMouseEventsObject(mousePosRelativeToCamera, eventMask, state, clipMouseEvents);
 	}
 
 	return result;
 }
 
 uint16 StageActor::findActorToAcceptMouseEvents(
-	const Common::Point &point,
-	uint16 eventMask,
-	MouseActorState &state,
-	bool inBounds) {
+	const Common::Point &point, uint16 eventMask, MouseActorState &state, bool clipMouseEvents) {
 
-	Common::Point mousePosRelativeToStageOrigin = point;
-	mousePosRelativeToStageOrigin.x -= _boundingBox.left;
-	mousePosRelativeToStageOrigin.y -= _boundingBox.top;
-	debugC(4, kDebugEvents, "[%s] %s: mousePos: (%d, %d), relativeToStage: (%d, %d)", debugName(), __func__,
+	Common::Point mousePosRelativeToStageOrigin = point - _boundingBox.origin();
+	debugC(8, kDebugEvents, "[%s] %s: mousePos: (%d, %d), relativeToStage: (%d, %d)", debugName(), __func__,
 		point.x, point.y, mousePosRelativeToStageOrigin.x, mousePosRelativeToStageOrigin.y);
 
 	uint16 result;
 	if (_cameras.empty()) {
-		if (!inBounds) {
+		if (!clipMouseEvents) {
 			if (!_boundingBox.contains(point)) {
-				inBounds = true;
+				clipMouseEvents = true;
 			}
 		}
-		result = queryChildrenAboutMouseEvents(point, eventMask, state, kWrapNone);
+		result = queryChildrenAboutMouseEvents(mousePosRelativeToStageOrigin, eventMask, state, clipMouseEvents, kWrapNone);
 	} else {
-		result = findActorToAcceptMouseEventsCamera(mousePosRelativeToStageOrigin, eventMask, state, inBounds);
+		result = findActorToAcceptMouseEventsCamera(mousePosRelativeToStageOrigin, eventMask, state, clipMouseEvents);
 	}
 
 	return result;
@@ -674,13 +740,9 @@ void RootStage::drawDirtyRegion(DisplayContext &displayContext) {
 }
 
 uint16 RootStage::findActorToAcceptMouseEvents(
-	const Common::Point &point,
-	uint16 eventMask,
-	MouseActorState &state,
-	bool inBounds) {
-
+	const Common::Point &point, uint16 eventMask, MouseActorState &state, bool clipMouseEvents) {
 	// Handle any mouse moved events.
-	uint16 result = StageActor::findActorToAcceptMouseEvents(point, eventMask, state, inBounds);
+	uint16 result = StageActor::findActorToAcceptMouseEvents(point, eventMask, state, clipMouseEvents);
 	eventMask &= ~result;
 	if (eventMask & kMouseEnterFlag) {
 		state.mouseEnter = this;
@@ -815,16 +877,16 @@ void StageDirector::handleMouseOutOfFocusEvent(const Common::Event &event) {
 }
 
 void StageDirector::sendMouseEnterExitEvent(uint16 flags, MouseActorState &state, const Common::Event &event) {
-	if (state.mouseMoved != state.mouseEnter || state.mouseMoved != state.mouseExit) {
-		if (flags & kMouseEnterFlag) {
-			debugC(5, kDebugEvents, "%s: Dispatching mouse enter to %s", __func__, state.mouseEnter->debugName());
-			state.mouseEnter->mouseEnteredEvent(event);
-		}
-
+	if (state.mouseEnter != state.mouseExit) {
 		if (flags & kMouseExitFlag) {
 			debugC(5, kDebugEvents, "%s: Dispatching mouse exit to %s", __func__, state.mouseExit->debugName());
 			state.mouseExit->mouseExitedEvent(event);
 		}
+
+		if (flags & kMouseEnterFlag) {
+			debugC(5, kDebugEvents, "%s: Dispatching mouse enter to %s", __func__, state.mouseEnter->debugName());
+			state.mouseEnter->mouseEnteredEvent(event);
+		}
 	} else {
 		debugC(5, kDebugEvents, "%s: No actor to accept event", __func__);
 	}
diff --git a/engines/mediastation/actors/stage.h b/engines/mediastation/actors/stage.h
index 1eea5e72182..84439844d16 100644
--- a/engines/mediastation/actors/stage.h
+++ b/engines/mediastation/actors/stage.h
@@ -59,6 +59,7 @@ public:
 	virtual void preload(const Common::Rect &rect, bool fireStepEvent = true) override;
 	virtual bool isVisible() const override { return _children.size() > 0; }
 	virtual bool isRectInMemory(const Common::Rect &rect) override;
+	bool inBounds(const Common::Point &point, const Common::Rect &bounds, const Polygon &polygon);
 
 	void addChildSpatialEntity(SpatialEntity *entity);
 	void removeChildSpatialEntity(SpatialEntity *entity);
@@ -71,22 +72,23 @@ public:
 		const Common::Point &point,
 		uint16 eventMask,
 		MouseActorState &state,
+		bool clipMouseEvents,
 		CylindricalWrapMode wrapMode = kWrapNone);
 	uint16 findActorToAcceptMouseEventsObject(
 		const Common::Point &point,
 		uint16 eventMask,
 		MouseActorState &state,
-		bool inBounds);
+		bool clipMouseEvents);
 	uint16 findActorToAcceptMouseEventsCamera(
 		const Common::Point &point,
 		uint16 eventMask,
 		MouseActorState &state,
-		bool inBounds);
+		bool clipMouseEvents);
 	virtual uint16 findActorToAcceptMouseEvents(
 		const Common::Point &point,
 		uint16 eventMask,
 		MouseActorState &state,
-		bool inBounds) override;
+		bool clipMouseEvents) override;
 	virtual uint16 findActorToAcceptKeyboardEvents(
 		uint16 asciiCode,
 		uint16 eventMask,
@@ -114,6 +116,9 @@ protected:
 	void removeActorFromStage(uint actorId);
 	bool isRectInMemoryTest(const Common::Rect &rect, CylindricalWrapMode wrapMode);
 	void preloadTest(const Common::Rect &rect, CylindricalWrapMode wrapMode, bool fireStepEvent);
+	bool inBoundsTest(Common::Point point, const Common::Rect &bounds, const Polygon &polygon);
+	bool inBoundsCamera(const Common::Point &point, const Common::Rect &bounds, const Polygon &polygon);
+	bool inBoundsObject(const Common::Point &point, const Common::Rect &bounds, const Polygon &polygon);
 
 	bool assertHasNoParent(const SpatialEntity *entity);
 	bool assertHasParentThatIsNotMe(const SpatialEntity *entity) { return !assertIsMyChild(entity); }
@@ -143,7 +148,7 @@ public:
 		const Common::Point &point,
 		uint16 eventMask,
 		MouseActorState &state,
-		bool inBounds) override;
+		bool clipMouseEvents) override;
 	virtual void currentMousePosition(Common::Point &point) override;
 	virtual void setMousePosition(int16 x, int16 y) override;
 	virtual void invalidateRect(const Common::Rect &rect) override;
diff --git a/engines/mediastation/datafile.cpp b/engines/mediastation/datafile.cpp
index c9438f36433..c6607f3eddb 100644
--- a/engines/mediastation/datafile.cpp
+++ b/engines/mediastation/datafile.cpp
@@ -130,16 +130,6 @@ ChannelIdent ParameterReadStream::readTypedChannelIdent() {
 	return readUint32BE();
 }
 
-Polygon ParameterReadStream::readTypedPolygon() {
-	Polygon polygon;
-	uint totalPoints = readTypedUint16();
-	for (uint i = 0; i < totalPoints; ++i) {
-		Common::Point point = readTypedGraphicSize();
-		polygon.push_back(point);
-	}
-	return polygon;
-}
-
 Chunk::Chunk(Common::SeekableReadStream *stream) : _parentStream(stream) {
 	_id = _parentStream->readUint32BE();
 	_length = _parentStream->readUint32LE();
diff --git a/engines/mediastation/datafile.h b/engines/mediastation/datafile.h
index 9afe2b49063..dc92e1a48f9 100644
--- a/engines/mediastation/datafile.h
+++ b/engines/mediastation/datafile.h
@@ -38,8 +38,6 @@ struct VersionInfo {
 	uint16 patch = 0;
 };
 
-typedef Common::Array<Common::Point> Polygon;
-
 // A Media Station datafile consists of one or more RIFF-style "subfiles". Aside
 // from some oddness at the start of the subfile, each subfile is basically
 // standard sequence of chunks inside a LIST chunk, like you'd see in any RIFF
@@ -93,7 +91,6 @@ public:
 	Common::String readTypedString();
 	VersionInfo readTypedVersion();
 	uint32 readTypedChannelIdent();
-	Polygon readTypedPolygon();
 
 private:
 	void readAndVerifyType(DatumType type);


Commit: a5cde81c4b1cf324209b9bae01120f43fe94d8e0
    https://github.com/scummvm/scummvm/commit/a5cde81c4b1cf324209b9bae01120f43fe94d8e0
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-04-30T20:19:20-04:00

Commit Message:
MEDIASTATION: Split out ImtGod from main engine class

Changed paths:
    engines/mediastation/actor.cpp
    engines/mediastation/actors/camera.cpp
    engines/mediastation/actors/canvas.cpp
    engines/mediastation/actors/document.cpp
    engines/mediastation/actors/image.cpp
    engines/mediastation/actors/movie.cpp
    engines/mediastation/actors/sprite.cpp
    engines/mediastation/actors/stage.cpp
    engines/mediastation/actors/text.cpp
    engines/mediastation/boot.cpp
    engines/mediastation/clients.cpp
    engines/mediastation/clients.h
    engines/mediastation/context.cpp
    engines/mediastation/datafile.cpp
    engines/mediastation/graphics.cpp
    engines/mediastation/mediascript/codechunk.cpp
    engines/mediastation/mediascript/collection.cpp
    engines/mediastation/mediascript/function.cpp
    engines/mediastation/mediastation.cpp
    engines/mediastation/mediastation.h
    engines/mediastation/profile.cpp
    engines/mediastation/profile.h


diff --git a/engines/mediastation/actor.cpp b/engines/mediastation/actor.cpp
index b2cfe54b3e2..a6bf0f0e7d0 100644
--- a/engines/mediastation/actor.cpp
+++ b/engines/mediastation/actor.cpp
@@ -452,7 +452,7 @@ void SpatialEntity::readParameter(Chunk &chunk, ActorHeaderSectionType paramType
 void SpatialEntity::loadIsComplete() {
 	Actor::loadIsComplete();
 	if (_stageId != 0) {
-		StageActor *pendingParentStage = static_cast<StageActor *>(g_engine->getActorByIdAndType(_stageId, kActorTypeStage));
+		StageActor *pendingParentStage = static_cast<StageActor *>(g_engine->getImtGod()->getActorByIdAndType(_stageId, kActorTypeStage));
 		pendingParentStage->addChildSpatialEntity(this);
 	}
 }
diff --git a/engines/mediastation/actors/camera.cpp b/engines/mediastation/actors/camera.cpp
index 195af832499..e4bc62a9e2d 100644
--- a/engines/mediastation/actors/camera.cpp
+++ b/engines/mediastation/actors/camera.cpp
@@ -71,7 +71,7 @@ void CameraActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType)
 
 	case kActorHeaderCameraImageActor: {
 		uint actorReference = chunk.readTypedUint16();
-		CameraActor *referencedCamera = static_cast<CameraActor *>(g_engine->getActorByIdAndType(actorReference, kActorTypeCamera));
+		CameraActor *referencedCamera = static_cast<CameraActor *>(g_engine->getImtGod()->getActorByIdAndType(actorReference, kActorTypeCamera));
 		_overlayImage = referencedCamera->_overlayImage;
 		break;
 	}
diff --git a/engines/mediastation/actors/canvas.cpp b/engines/mediastation/actors/canvas.cpp
index 4b8a923faaa..6066b05e3da 100644
--- a/engines/mediastation/actors/canvas.cpp
+++ b/engines/mediastation/actors/canvas.cpp
@@ -170,7 +170,7 @@ void CanvasActor::stampImage(const Common::Point &dest, uint actorId) {
 	// Although this method is named stampImage, it can actually stamp other spatial entities too.
 	debugC(5, kDebugGraphics, "[%s] %s: %s at (%d, %d)",
 		debugName(), __func__, g_engine->formatActorName(actorId).c_str(), dest.x, dest.y);
-	SpatialEntity *imageToStamp = g_engine->getSpatialEntityById (actorId);
+	SpatialEntity *imageToStamp = g_engine->getImtGod()->getSpatialEntityById(actorId);
 	Common::Point imageToStampOriginalBoundsOrigin = imageToStamp->getBbox().origin();
 	imageToStamp->moveTo(dest.x, dest.y);
 
diff --git a/engines/mediastation/actors/document.cpp b/engines/mediastation/actors/document.cpp
index 4e131117b2c..3ed12e596b4 100644
--- a/engines/mediastation/actors/document.cpp
+++ b/engines/mediastation/actors/document.cpp
@@ -74,7 +74,7 @@ ScriptValue DocumentActor::callMethod(BuiltInMethod methodId, Common::Array<Scri
 		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);
+		Actor *screenActor = g_engine->getImtGod()->getActorById(contextId);
 		bool contextIsLoading = g_engine->getDocument()->isContextLoadInProgress(contextId);
 		bool contextIsLoaded = (screenActor != nullptr) && !contextIsLoading;
 		returnValue.setToBool(contextIsLoaded);
diff --git a/engines/mediastation/actors/image.cpp b/engines/mediastation/actors/image.cpp
index f6c4ec0e1f4..2aa6b991220 100644
--- a/engines/mediastation/actors/image.cpp
+++ b/engines/mediastation/actors/image.cpp
@@ -60,7 +60,7 @@ void ImageActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 
 	case kActorHeaderActorReference: {
 		_actorReference = chunk.readTypedUint16();
-		ImageActor *referencedImage = static_cast<ImageActor *>(g_engine->getActorByIdAndType(_actorReference, kActorTypeImage));
+		ImageActor *referencedImage = static_cast<ImageActor *>(g_engine->getImtGod()->getActorByIdAndType(_actorReference, kActorTypeImage));
 		_asset = referencedImage->_asset;
 		break;
 	}
diff --git a/engines/mediastation/actors/movie.cpp b/engines/mediastation/actors/movie.cpp
index da44ac0a0ff..6cb762b707a 100644
--- a/engines/mediastation/actors/movie.cpp
+++ b/engines/mediastation/actors/movie.cpp
@@ -55,7 +55,7 @@ bool StreamMovieProxy::isVisible() const {
 }
 
 MovieFrame::MovieFrame(Chunk &chunk) {
-	if (g_engine->isFirstGenerationEngine()) {
+	if (g_engine->getImtGod()->isFirstGenerationEngine()) {
 		blitType = static_cast<MovieBlitType>(chunk.readTypedUint16());
 		startInMilliseconds = chunk.readTypedUint32();
 		endInMilliseconds = chunk.readTypedUint32();
@@ -291,7 +291,7 @@ ScriptValue StreamMovieActor::callMethod(BuiltInMethod methodId, Common::Array<S
 				debugName(), __func__, g_engine->formatParamTokenName(scriptId).c_str());
 			break;
 		}
-		StageActor *parentStage = static_cast<StageActor *>(g_engine->getActorByIdAndType(targetStageId, kActorTypeStage));
+		StageActor *parentStage = static_cast<StageActor *>(g_engine->getImtGod()->getActorByIdAndType(targetStageId, kActorTypeStage));
 		if (parentStage == nullptr) {
 			warning("[%s] %s: Stream movie proxy with script ID %s has null parent stage",
 				debugName(), __func__, g_engine->formatParamTokenName(scriptId).c_str());
@@ -315,7 +315,7 @@ ScriptValue StreamMovieActor::callMethod(BuiltInMethod methodId, Common::Array<S
 		}
 
 		RootStage *rootStage = g_engine->getRootStage();
-		StageActor *sourceStage = static_cast<StageActor *>(g_engine->getActorByIdAndType(sourceStageId, kActorTypeStage));
+		StageActor *sourceStage = static_cast<StageActor *>(g_engine->getImtGod()->getActorByIdAndType(sourceStageId, kActorTypeStage));
 		if (sourceStage == nullptr) {
 			warning("[%s] %s: Stream movie proxy with script ID %s has null parent stage",
 				debugName(), __func__, g_engine->formatParamTokenName(scriptId).c_str());
diff --git a/engines/mediastation/actors/sprite.cpp b/engines/mediastation/actors/sprite.cpp
index 3c9b345bd29..487d457eb45 100644
--- a/engines/mediastation/actors/sprite.cpp
+++ b/engines/mediastation/actors/sprite.cpp
@@ -87,7 +87,7 @@ void SpriteMovieActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramT
 
 	case kActorHeaderActorReference: {
 		_actorReference = chunk.readTypedUint16();
-		SpriteMovieActor *referencedSprite = static_cast<SpriteMovieActor *>(g_engine->getActorByIdAndType(_actorReference, kActorTypeSprite));
+		SpriteMovieActor *referencedSprite = static_cast<SpriteMovieActor *>(g_engine->getImtGod()->getActorByIdAndType(_actorReference, kActorTypeSprite));
 		_asset = referencedSprite->_asset;
 		break;
 	}
diff --git a/engines/mediastation/actors/stage.cpp b/engines/mediastation/actors/stage.cpp
index f0616a76536..5a80e1b901a 100644
--- a/engines/mediastation/actors/stage.cpp
+++ b/engines/mediastation/actors/stage.cpp
@@ -45,7 +45,7 @@ void StageActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 		// In stages, this basically has the oppose meaning it has outside of stages. Here,
 		// it specifies an actor that is a parent of this stage.
 		uint parentActorId = chunk.readTypedUint16();
-		_pendingParent = static_cast<SpatialEntity *>(g_engine->getActorByIdAndType(parentActorId, kActorTypeStage));
+		_pendingParent = static_cast<SpatialEntity *>(g_engine->getImtGod()->getActorByIdAndType(parentActorId, kActorTypeStage));
 		break;
 	}
 
@@ -364,7 +364,7 @@ void StageActor::loadIsComplete() {
 
 void StageActor::addActorToStage(uint actorId) {
 	// If actor has a current parent, remove it from that parent first.
-	SpatialEntity *spatialEntity = g_engine->getSpatialEntityById(actorId);
+	SpatialEntity *spatialEntity = g_engine->getImtGod()->getSpatialEntityById(actorId);
 	StageActor *currentParent = spatialEntity->getParentStage();
 	if (currentParent != nullptr) {
 		currentParent->removeChildSpatialEntity(spatialEntity);
@@ -373,7 +373,7 @@ void StageActor::addActorToStage(uint actorId) {
 }
 
 void StageActor::removeActorFromStage(uint actorId) {
-	SpatialEntity *spatialEntity = g_engine->getSpatialEntityById(actorId);
+	SpatialEntity *spatialEntity = g_engine->getImtGod()->getSpatialEntityById(actorId);
 	StageActor *currentParent = spatialEntity->getParentStage();
 	if (currentParent == this) {
 		// Remove the actor from this stage, and add it back to the root stage.
@@ -790,11 +790,11 @@ StageDirector::StageDirector() {
 	_rootStage = new RootStage;
 	Common::Rect rootStageBounds(MediaStationEngine::SCREEN_WIDTH, MediaStationEngine::SCREEN_HEIGHT);
 	_rootStage->setBounds(rootStageBounds);
-	g_engine->registerActor(_rootStage);
+	g_engine->getImtGod()->addConstructedActor(_rootStage);
 }
 
 StageDirector::~StageDirector() {
-	g_engine->destroyActor(RootStage::ROOT_STAGE_ACTOR_ID);
+	g_engine->getImtGod()->destroyActor(RootStage::ROOT_STAGE_ACTOR_ID);
 	_rootStage = nullptr;
 }
 
diff --git a/engines/mediastation/actors/text.cpp b/engines/mediastation/actors/text.cpp
index db632a0605b..508f3f4f9c6 100644
--- a/engines/mediastation/actors/text.cpp
+++ b/engines/mediastation/actors/text.cpp
@@ -37,7 +37,7 @@ void TextActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 
 	case kActorHeaderFontActorId: {
 		uint fontActorId = chunk.readTypedUint16();
-		_fontActor = static_cast<FontActor *>(g_engine->getActorByIdAndType(fontActorId, kActorTypeFont));
+		_fontActor = static_cast<FontActor *>(g_engine->getImtGod()->getActorByIdAndType(fontActorId, kActorTypeFont));
 		break;
 	}
 
@@ -137,7 +137,7 @@ ScriptValue TextActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptVa
 	case kTextSetFontActorMethod: {
 		ARGCOUNTCHECK(1);
 		uint fontActorId = args[0].asActorId();
-		_fontActor = static_cast<FontActor *>(g_engine->getActorByIdAndType(fontActorId, kActorTypeFont));
+		_fontActor = static_cast<FontActor *>(g_engine->getImtGod()->getActorByIdAndType(fontActorId, kActorTypeFont));
 		invalidateLocalBounds();
 		break;
 	}
diff --git a/engines/mediastation/boot.cpp b/engines/mediastation/boot.cpp
index 4ef577c3416..0fe1d4dadd9 100644
--- a/engines/mediastation/boot.cpp
+++ b/engines/mediastation/boot.cpp
@@ -155,7 +155,7 @@ CursorDeclaration::CursorDeclaration(Chunk &chunk) {
 #pragma endregion
 
 #pragma region Boot
-void MediaStationEngine::readDocumentDef(Chunk &chunk) {
+void ImtGod::readDocumentDef(Chunk &chunk) {
 	BootSectionType sectionType = kBootLastSection;
 	while (true) {
 		sectionType = static_cast<BootSectionType>(chunk.readTypedUint16());
@@ -166,7 +166,7 @@ void MediaStationEngine::readDocumentDef(Chunk &chunk) {
 	}
 }
 
-void MediaStationEngine::readDocumentInfoFromStream(Chunk &chunk, BootSectionType sectionType) {
+void ImtGod::readDocumentInfoFromStream(Chunk &chunk, BootSectionType sectionType) {
 	switch (sectionType) {
 	case kBootVersionInformation:
 		readVersionInfoFromStream(chunk);
@@ -221,14 +221,14 @@ void MediaStationEngine::readDocumentInfoFromStream(Chunk &chunk, BootSectionTyp
 	}
 }
 
-void MediaStationEngine::readVersionInfoFromStream(Chunk &chunk) {
+void ImtGod::readVersionInfoFromStream(Chunk &chunk) {
 	_gameTitle = chunk.readTypedString();
 	_versionInfo = chunk.readTypedVersion();
 	_engineInfo = chunk.readTypedString();
 	_sourceString = chunk.readTypedString();
 }
 
-void MediaStationEngine::readContextReferencesFromStream(Chunk &chunk) {
+void ImtGod::readContextReferencesFromStream(Chunk &chunk) {
 	uint flag = chunk.readTypedUint16();
 	while (flag != 0) {
 		ContextReference contextReference(chunk);
@@ -237,7 +237,7 @@ void MediaStationEngine::readContextReferencesFromStream(Chunk &chunk) {
 	}
 }
 
-void MediaStationEngine::readScreenReferencesFromStream(Chunk &chunk) {
+void ImtGod::readScreenReferencesFromStream(Chunk &chunk) {
 	uint flag = chunk.readTypedUint16();
 	while (flag != 0) {
 		ScreenReference screenDeclaration(chunk);
@@ -246,7 +246,7 @@ void MediaStationEngine::readScreenReferencesFromStream(Chunk &chunk) {
 	}
 }
 
-void MediaStationEngine::readAndAddFileMaps(Chunk &chunk) {
+void ImtGod::readAndAddFileMaps(Chunk &chunk) {
 	uint flag = chunk.readTypedUint16();
 	while (flag != 0) {
 		FileInfo fileDeclaration(chunk);
@@ -255,7 +255,7 @@ void MediaStationEngine::readAndAddFileMaps(Chunk &chunk) {
 	}
 }
 
-void MediaStationEngine::readAndAddStreamMaps(Chunk &chunk) {
+void ImtGod::readAndAddStreamMaps(Chunk &chunk) {
 	uint flag = chunk.readTypedUint16();
 	while (flag != 0) {
 		StreamInfo subfileDeclaration(chunk);
diff --git a/engines/mediastation/clients.cpp b/engines/mediastation/clients.cpp
index 02c4fbc1bee..77cfa632458 100644
--- a/engines/mediastation/clients.cpp
+++ b/engines/mediastation/clients.cpp
@@ -26,8 +26,15 @@
 #include "mediastation/mediastation.h"
 
 namespace MediaStation {
+ParameterClient::ParameterClient() {
+	g_engine->getImtGod()->registerParameterClient(this);
+}
+
+ParameterClient::~ParameterClient() {
+	g_engine->getImtGod()->unregisterParameterClient(this);
+}
 
-bool DeviceOwner::attemptToReadFromStream(Chunk &chunk, uint sectionType) {
+bool ImtDeviceOwner::attemptToReadFromStream(Chunk &chunk, uint sectionType) {
 	bool handledParam = true;
 	switch (sectionType) {
 	case kDeviceOwnerAllowMultipleSounds:
@@ -45,6 +52,7 @@ bool DeviceOwner::attemptToReadFromStream(Chunk &chunk, uint sectionType) {
 	return handledParam;
 }
 
+
 bool Document::attemptToReadFromStream(Chunk &chunk, uint sectionType) {
 	bool handledParam = true;
 	switch (sectionType) {
@@ -109,10 +117,10 @@ void Document::beginTitle(uint overriddenEntryPointScreenId) {
 
 void Document::startContextLoad(uint contextId) {
 	debugC(5, kDebugLoading, "%s: Loading context %d", __func__, contextId);
-	Context *existingContext = g_engine->_loadedContexts.getValOrDefault(contextId);
+	Context *existingContext = g_engine->getImtGod()->getContextById(contextId);
 	if (existingContext == nullptr) {
 		if (_loadingContextId == 0) {
-			const ContextReference &contextRef = g_engine->contextRefWithId(contextId);
+			const ContextReference &contextRef = g_engine->getImtGod()->contextRefWithId(contextId);
 			if (contextRef._contextId != 0) {
 				_loadingContextId = contextId;
 				startFeed(contextRef._streamId);
@@ -122,7 +130,7 @@ void Document::startContextLoad(uint contextId) {
 		}
 	} else {
 		if (_currentScreenActorId != 0 && contextId != _loadingContextId) {
-			Actor *currentScreen = g_engine->getActorById(_currentScreenActorId);
+			Actor *currentScreen = g_engine->getImtGod()->getActorById(_currentScreenActorId);
 			ScriptValue arg;
 			arg.setToActorId(contextId);
 			currentScreen->runScriptResponseIfExists(kContextLoadCompleteEvent2, arg);
@@ -180,7 +188,7 @@ void Document::scheduleScreenBranch(uint screenActorId) {
 }
 
 void Document::scheduleContextRelease(uint contextId) {
-	if (!g_engine->contextIsLocked(contextId)) {
+	if (!g_engine->getImtGod()->contextIsLocked(contextId)) {
 		_requestedContextReleaseId.push_back(contextId);
 	}
 }
@@ -211,7 +219,7 @@ void Document::contextLoadDidComplete() {
 	if (_currentScreenActorId != 0) {
 		ScriptValue arg;
 		arg.setToActorId(_loadingContextId);
-		Actor *currentScreen = g_engine->getActorById(_currentScreenActorId);
+		Actor *currentScreen = g_engine->getImtGod()->getActorById(_currentScreenActorId);
 		if (currentScreen != nullptr) {
 			currentScreen->runScriptResponseIfExists(kContextLoadCompleteEvent, arg);
 		}
@@ -221,7 +229,7 @@ void Document::contextLoadDidComplete() {
 
 void Document::screenLoadDidComplete() {
 	_currentScreenActorId = _loadingScreenActorId;
-	Actor *currentScreen = g_engine->getActorById(_loadingScreenActorId);
+	Actor *currentScreen = g_engine->getImtGod()->getActorById(_loadingScreenActorId);
 	currentScreen->runScriptResponseIfExists(kScreenEntryEvent);
 	_loadingScreenActorId = 0;
 }
@@ -229,7 +237,7 @@ void Document::screenLoadDidComplete() {
 void Document::process() {
 	if (!_requestedContextReleaseId.empty()) {
 		for (uint contextId : _requestedContextReleaseId) {
-			g_engine->destroyContext(contextId);
+			g_engine->getImtGod()->destroyContext(contextId);
 		}
 		_requestedContextReleaseId.clear();
 	}
@@ -243,15 +251,15 @@ void Document::blowAwayCurrentScreen() {
 	if (_currentScreenActorId != 0) {
 		uint contextId = contextIdForScreenActorId(_currentScreenActorId);
 		if (contextId != 0) {
-			Actor *currentScreen = g_engine->getActorById(_currentScreenActorId);
+			Actor *currentScreen = g_engine->getImtGod()->getActorById(_currentScreenActorId);
 			currentScreen->runScriptResponseIfExists(kScreenExitEvent);
-			g_engine->destroyContext(contextId);
+			g_engine->getImtGod()->destroyContext(contextId);
 		}
 	}
 }
 
 uint Document::contextIdForScreenActorId(uint screenActorId) {
-	ScreenReference screenRef = g_engine->screenRefWithId(screenActorId);
+	ScreenReference screenRef = g_engine->getImtGod()->screenRefWithId(screenActorId);
 	return screenRef._contextId;
 }
 
@@ -269,10 +277,10 @@ void Document::stopFeed() {
 }
 
 void Document::preloadParentContexts(uint contextId) {
-	ContextReference contextReference = g_engine->contextRefWithId(contextId);
+	ContextReference contextReference = g_engine->getImtGod()->contextRefWithId(contextId);
 	for (uint parentContextId : contextReference._parentContextIds) {
 		if (parentContextId != 0) {
-			Context *existingContext = g_engine->_loadedContexts.getValOrDefault(parentContextId);
+			Context *existingContext = g_engine->getImtGod()->getContextById(parentContextId);
 			if (existingContext == nullptr && parentContextId != contextId) {
 				debugC(5, kDebugLoading, "%s: Loading parent context %d", __func__, parentContextId);
 				addToContextLoadQueue(parentContextId);
diff --git a/engines/mediastation/clients.h b/engines/mediastation/clients.h
index e317c655fcf..ad19fe84295 100644
--- a/engines/mediastation/clients.h
+++ b/engines/mediastation/clients.h
@@ -28,8 +28,8 @@ namespace MediaStation {
 
 class ParameterClient {
 public:
-	ParameterClient() {};
-	virtual ~ParameterClient() {};
+	ParameterClient();
+	virtual ~ParameterClient();
 
 	virtual bool attemptToReadFromStream(Chunk &chunk, uint sectionType) = 0;
 };
@@ -39,7 +39,7 @@ enum DeviceOwnerSectionType {
 	kDeviceOwnerAllowMultipleStreams = 0x36,
 };
 
-class DeviceOwner : public ParameterClient {
+class ImtDeviceOwner : public ParameterClient {
 public:
 	virtual bool attemptToReadFromStream(Chunk &chunk, uint sectionType) override;
 
diff --git a/engines/mediastation/context.cpp b/engines/mediastation/context.cpp
index c02c665b351..2554422d418 100644
--- a/engines/mediastation/context.cpp
+++ b/engines/mediastation/context.cpp
@@ -52,7 +52,7 @@ Context::~Context() {
 	_variables.clear();
 }
 
-void MediaStationEngine::readControlCommands(Chunk &chunk) {
+void ImtGod::readControlCommands(Chunk &chunk) {
 	ContextSectionType sectionType = kContextEndOfSection;
 	do {
 		sectionType = static_cast<ContextSectionType>(chunk.readTypedUint16());
@@ -63,7 +63,7 @@ void MediaStationEngine::readControlCommands(Chunk &chunk) {
 	} while (sectionType != kContextEndOfSection);
 }
 
-void MediaStationEngine::readCreateContextData(Chunk &chunk) {
+void ImtGod::readCreateContextData(Chunk &chunk) {
 	uint contextId = chunk.readTypedUint16();
 	debugC(5, kDebugLoading, "%s: Context %d", __func__, contextId);
 	Context *context = _loadedContexts.getValOrDefault(contextId);
@@ -74,26 +74,26 @@ void MediaStationEngine::readCreateContextData(Chunk &chunk) {
 	}
 }
 
-void MediaStationEngine::readDestroyContextData(Chunk &chunk) {
+void ImtGod::readDestroyContextData(Chunk &chunk) {
 	uint contextId = chunk.readTypedUint16();
 	debugC(5, kDebugLoading, "%s: Context %d", __func__, contextId);
 	destroyContext(contextId);
 }
 
-void MediaStationEngine::readDestroyActorData(Chunk &chunk) {
+void ImtGod::readDestroyActorData(Chunk &chunk) {
 	uint actorId = chunk.readTypedUint16();
 	debugC(5, kDebugLoading, "[%s] %s", g_engine->formatActorName(actorId).c_str(), __func__);
 	destroyActor(actorId);
 }
 
-void MediaStationEngine::readActorLoadComplete(Chunk &chunk) {
+void ImtGod::readActorLoadComplete(Chunk &chunk) {
 	uint actorId = chunk.readTypedUint16();
-	Actor *actor = g_engine->getActorById(actorId);
+	Actor *actor = g_engine->getImtGod()->getActorById(actorId);
 	debugC(5, kDebugLoading, "[%s] %s", actor->debugName(), __func__);
 	actor->loadIsComplete();
 }
 
-void MediaStationEngine::readCreateActorData(Chunk &chunk) {
+void ImtGod::readCreateActorData(Chunk &chunk) {
 	uint contextId = chunk.readTypedUint16();
 	ActorType type = static_cast<ActorType>(chunk.readTypedUint16());
 	uint id = chunk.readTypedUint16();
@@ -171,13 +171,13 @@ void MediaStationEngine::readCreateActorData(Chunk &chunk) {
 	actor->setId(id);
 	actor->setContextId(contextId);
 	actor->initFromParameterStream(chunk);
-	g_engine->registerActor(actor);
+	g_engine->getImtGod()->addConstructedActor(actor);
 }
 
-void MediaStationEngine::readCreateVariableData(Chunk &chunk) {
+void ImtGod::readCreateVariableData(Chunk &chunk) {
 	uint contextId = chunk.readTypedUint16();
 	uint id = chunk.readTypedUint16();
-	if (g_engine->getVariable(id) != nullptr) {
+	if (g_engine->getImtGod()->getVariable(id) != nullptr) {
 		error("[%s] %s: Global variable already exists", g_engine->formatVariableName(id).c_str(), __func__);
 	}
 
@@ -191,10 +191,10 @@ void MediaStationEngine::readCreateVariableData(Chunk &chunk) {
 	debugC(5, kDebugLoading, "[%s] %s", g_engine->formatVariableName(id).c_str(), __func__);
 }
 
-void MediaStationEngine::readHeaderSections(Subfile &subfile, Chunk &chunk) {
+void ImtGod::readHeaderSections(Subfile &subfile, Chunk &chunk) {
 	do {
 		debugC(5, kDebugLoading, "[%s] %s", g_engine->formatAssetNameForChannelIdent(chunk._id).c_str(), __func__);
-		ChannelClient *actor = g_engine->getChannelClientByChannelIdent(chunk._id);
+		ChannelClient *actor = g_engine->getImtGod()->getChannelClientByChannelIdent(chunk._id);
 		if (actor == nullptr) {
 			error("%s: Client %s does not exist or has not been read yet in this title",
 				__func__, g_engine->formatAssetNameForChannelIdent(chunk._id).c_str());
@@ -213,7 +213,7 @@ void MediaStationEngine::readHeaderSections(Subfile &subfile, Chunk &chunk) {
 	} while (!subfile.atEnd());
 }
 
-void MediaStationEngine::readSetContextName(Chunk &chunk) {
+void ImtGod::readSetContextName(Chunk &chunk) {
 	uint contextId = chunk.readTypedUint16();
 	debugC(5, kDebugLoading, "%s: Context %d", __func__, contextId);
 	Context *context = _loadedContexts.getValOrDefault(contextId);
@@ -223,7 +223,7 @@ void MediaStationEngine::readSetContextName(Chunk &chunk) {
 	context->_name = chunk.readTypedString();
 }
 
-void MediaStationEngine::readCommandFromStream(Chunk &chunk, ContextSectionType sectionType) {
+void ImtGod::readCommandFromStream(Chunk &chunk, ContextSectionType sectionType) {
 	switch (sectionType) {
 	case kContextCreateData:
 		readCreateContextData(chunk);
diff --git a/engines/mediastation/datafile.cpp b/engines/mediastation/datafile.cpp
index c6607f3eddb..4c8f8e0b698 100644
--- a/engines/mediastation/datafile.cpp
+++ b/engines/mediastation/datafile.cpp
@@ -210,12 +210,12 @@ bool Subfile::atEnd() {
 }
 
 void CdRomStream::openStream(uint streamId) {
-	const StreamInfo &streamInfo = g_engine->streamInfoForIdent(streamId);
+	const StreamInfo &streamInfo = g_engine->getImtGod()->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);
+	const FileInfo &fileInfo = g_engine->getImtGod()->fileInfoForIdent(streamInfo._fileId);
 	if (fileInfo._id == 0) {
 		error("%s: File %s for stream %d not found in current title", __func__, g_engine->formatFileName(streamInfo._fileId).c_str(), streamId);
 	}
@@ -283,7 +283,7 @@ void ImtStreamFeed::readData() {
 	Subfile subfile = _stream->getNextSubfile();
 	Chunk chunk = subfile.nextChunk();
 	g_engine->getDocument()->streamWillRead(_id);
-	g_engine->readHeaderSections(subfile, chunk);
+	g_engine->getImtGod()->readHeaderSections(subfile, chunk);
 	g_engine->getDocument()->streamDidFinish(_id);
 }
 
diff --git a/engines/mediastation/graphics.cpp b/engines/mediastation/graphics.cpp
index cd327ff53d0..5b349c5d7c6 100644
--- a/engines/mediastation/graphics.cpp
+++ b/engines/mediastation/graphics.cpp
@@ -699,19 +699,19 @@ void VideoDisplayManager::_colorShiftCurrentPalette(uint startIndex, uint shiftA
 }
 
 void VideoDisplayManager::_fadeToPaletteObject(uint paletteId, double fadeTime, uint startIndex, uint colorCount) {
-	PaletteActor *paletteActor = static_cast<PaletteActor *>(_vm->getActorByIdAndType(paletteId, kActorTypePalette));
+	PaletteActor *paletteActor = static_cast<PaletteActor *>(_vm->getImtGod()->getActorByIdAndType(paletteId, kActorTypePalette));
 	Graphics::Palette *palette = paletteActor->_palette;
 	_fadeToPalette(fadeTime, *palette, startIndex, colorCount);
 }
 
 void VideoDisplayManager::_setToPaletteObject(uint paletteId, uint startIndex, uint colorCount) {
-	PaletteActor *paletteActor = static_cast<PaletteActor *>(_vm->getActorByIdAndType(paletteId, kActorTypePalette));
+	PaletteActor *paletteActor = static_cast<PaletteActor *>(_vm->getImtGod()->getActorByIdAndType(paletteId, kActorTypePalette));
 	Graphics::Palette *palette = paletteActor->_palette;
 	_setPalette(*palette, startIndex, colorCount);
 }
 
 void VideoDisplayManager::_setPercentToPaletteObject(double percent, uint paletteId, uint startIndex, uint colorCount) {
-	PaletteActor *paletteActor = static_cast<PaletteActor *>(_vm->getActorByIdAndType(paletteId, kActorTypePalette));
+	PaletteActor *paletteActor = static_cast<PaletteActor *>(_vm->getImtGod()->getActorByIdAndType(paletteId, kActorTypePalette));
 	Graphics::Palette *palette = paletteActor->_palette;
 	_setToPercentPalette(percent, *_registeredPalette, *palette, startIndex, colorCount);
 }
diff --git a/engines/mediastation/mediascript/codechunk.cpp b/engines/mediastation/mediascript/codechunk.cpp
index 7c4abf98706..ba3f9e23008 100644
--- a/engines/mediastation/mediascript/codechunk.cpp
+++ b/engines/mediastation/mediascript/codechunk.cpp
@@ -320,7 +320,7 @@ ScriptValue *CodeChunk::readAndReturnVariable() {
 	ScriptValue *variable = nullptr;
 	switch (scope) {
 	case kVariableScopeGlobal: {
-		variable = g_engine->getVariable(id);
+		variable = g_engine->getImtGod()->getVariable(id);
 		if (variable == nullptr) {
 			error("%s: Global variable %s doesn't exist", __func__, g_engine->formatVariableName(id).c_str());
 		}
@@ -599,7 +599,7 @@ ScriptValue CodeChunk::evaluateMethodCall(BuiltInMethod method, uint paramCount)
 		} else {
 			// This is a regular actor that we can process directly.
 			uint actorId = methodCallTargetPtr->asActorId();
-			Actor *targetActor = g_engine->getActorById(actorId);
+			Actor *targetActor = g_engine->getImtGod()->getActorById(actorId);
 			if (targetActor == nullptr) {
 				warning("[%s] %s: Actor not loaded", g_engine->formatActorName(actorId).c_str(), __func__);
 			} else {
diff --git a/engines/mediastation/mediascript/collection.cpp b/engines/mediastation/mediascript/collection.cpp
index 1b7250e0dbd..ebf964f8fac 100644
--- a/engines/mediastation/mediascript/collection.cpp
+++ b/engines/mediastation/mediascript/collection.cpp
@@ -184,7 +184,7 @@ void Collection::send(const Common::Array<ScriptValue> &args) {
 	Common::Array<ScriptValue> sendArgs;
 	for (const ScriptValue &item : *this) {
 		uint actorId = item.asActorId();
-		Actor *targetActor = g_engine->getActorById(actorId);
+		Actor *targetActor = g_engine->getImtGod()->getActorById(actorId);
 		if (targetActor != nullptr) {
 			debugC(7, kDebugScript, "%s: %s: %s", __func__, builtInMethodToStr(methodToSend), targetActor->debugName());
 			targetActor->callMethod(methodToSend, argsToSend);
diff --git a/engines/mediastation/mediascript/function.cpp b/engines/mediastation/mediascript/function.cpp
index be100e6600b..be12a23f2f8 100644
--- a/engines/mediastation/mediascript/function.cpp
+++ b/engines/mediastation/mediascript/function.cpp
@@ -79,6 +79,7 @@ ScriptValue ScriptFunction::execute(Common::Array<ScriptValue> &args) {
 	return returnValue;
 }
 
+
 FunctionManager::~FunctionManager() {
 	for (auto it = _functions.begin(); it != _functions.end(); ++it) {
 		delete it->_value;
diff --git a/engines/mediastation/mediastation.cpp b/engines/mediastation/mediastation.cpp
index ea380333133..53449854522 100644
--- a/engines/mediastation/mediastation.cpp
+++ b/engines/mediastation/mediastation.cpp
@@ -50,69 +50,40 @@ 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;
-
-	delete _cursorManager;
-	_cursorManager = nullptr;
-
-	delete _functionManager;
-	_functionManager = nullptr;
-
-	delete _document;
-	_document = nullptr;
-
+	_imtGod->destroyAllContexts();
 	delete _deviceOwner;
-	_deviceOwner = nullptr;
-
+	// _cacheManager->removeCache();
 	delete _stageDirector;
-	_stageDirector = nullptr;
-
-	unregisterWithStreamManager();
+	delete _cursorManager;
+	delete _document;
+	_imtGod->destroyActor(DocumentActor::DOCUMENT_ACTOR_ID);
+	delete _displayManager;
+	delete _functionManager;
+	// delete _printManager;
+	delete _imtGod;
+	// delete _streamProfiler;
 	delete _streamFeedManager;
-	_streamFeedManager = nullptr;
-
 	delete _profile;
-	_profile = nullptr;
-
-	_contextReferences.clear();
-	_streamMap.clear();
-	_paramTokenDeclarations.clear();
-	_screenReferences.clear();
-	_fileMap.clear();
-	_actors.clear();
 }
 
-Actor *MediaStationEngine::getActorById(uint actorId) {
+Actor *ImtGod::getActorById(uint actorId) {
 	return _actors.getValOrDefault(actorId);
 }
 
-Actor *MediaStationEngine::getActorByIdAndType(uint actorId, ActorType expectedType) {
+Actor *ImtGod::getActorByIdAndType(uint actorId, ActorType expectedType) {
 	Actor *actor = getActorById(actorId);
 	if (actor == nullptr) {
-		error("[%s] %s: Actor doesn't exist", g_engine->formatActorName(actorId).c_str(), __func__);
+		error("[%s] %s: Actor doesn't exist", _vm->formatActorName(actorId).c_str(), __func__);
 	} else if (actor->type() != expectedType) {
 		error("[%s] %s: Expected type %s, got %s", actor->debugName(), __func__, actorTypeToStr(actor->type()), actorTypeToStr(expectedType));
 	}
 	return actor;
 }
 
-SpatialEntity *MediaStationEngine::getSpatialEntityById(uint spatialEntityId) {
+SpatialEntity *ImtGod::getSpatialEntityById(uint spatialEntityId) {
 	Actor *actor = getActorById(spatialEntityId);
 	if (actor != nullptr) {
 		if (!actor->isSpatialActor()) {
@@ -123,11 +94,11 @@ SpatialEntity *MediaStationEngine::getSpatialEntityById(uint spatialEntityId) {
 	return nullptr;
 }
 
-ChannelClient *MediaStationEngine::getChannelClientByChannelIdent(uint channelIdent) {
-	return _streamFeedManager->channelClientForChannel(channelIdent);
+ChannelClient *ImtGod::getChannelClientByChannelIdent(uint channelIdent) {
+	return _vm->getStreamFeedManager()->channelClientForChannel(channelIdent);
 }
 
-ScriptValue *MediaStationEngine::getVariable(uint variableId) {
+ScriptValue *ImtGod::getVariable(uint variableId) {
 	for (auto it = _loadedContexts.begin(); it != _loadedContexts.end(); ++it) {
 		ScriptValue *variable = it->_value->_variables.getValOrDefault(variableId);
 		if (variable != nullptr) {
@@ -137,6 +108,14 @@ ScriptValue *MediaStationEngine::getVariable(uint variableId) {
 	return nullptr;
 }
 
+Context *ImtGod::getContextById(uint contextId) {
+	return _loadedContexts.getValOrDefault(contextId);
+}
+
+bool ImtGod::isFirstGenerationEngine() const {
+	return _versionInfo.major == 0;
+}
+
 uint32 MediaStationEngine::getFeatures() const {
 	return _gameDescription->flags;
 }
@@ -153,31 +132,27 @@ const char *MediaStationEngine::getAppName() const {
 	return _gameDescription->filesDescriptions[0].fileName;
 }
 
-bool MediaStationEngine::isFirstGenerationEngine() {
-	return _versionInfo.major == 0;
+bool MediaStationEngine::hasFeature(EngineFeature f) const {
+	return (f == kSupportsReturnToLauncher);
 }
 
 Common::Error MediaStationEngine::run() {
-	initDisplayManager();
+	_streamFeedManager = new StreamFeedManager;
+	// _cacheManager = new CacheManager;
+	// _streamProfiler = new StreamProfiler;
+	_imtGod = new ImtGod(this);
+	_deviceOwner = new ImtDeviceOwner;
+	_functionManager = new FunctionManager;
+	_displayManager = new VideoDisplayManager(this);
+	// _printManager = new PrintManager;
+	_document = new Document;
+	DocumentActor *documentActor = new DocumentActor;
+	_imtGod->addConstructedActor(documentActor);
 	initCursorManager();
-	initFunctionManager();
-	initDocument();
-	initDeviceOwner();
-	initStageDirector();
-	initStreamFeedManager();
-	initProfile();
-	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);
-		_document->beginTitle(entryContextId);
-	} else {
-		_document->beginTitle();
-	}
-
+	_stageDirector = new StageDirector;
+	_profile = new Profile();
+	_profile->load();
+	_document->beginTitle();
 	runEventLoop();
 	return Common::kNoError;
 }
@@ -191,7 +166,7 @@ void MediaStationEngine::runEventLoop() {
 		_document->process();
 
 		debugC(9, kDebugGraphics, "***** START SCREEN UPDATE ***");
-		for (auto it = _actors.begin(); it != _actors.end(); ++it) {
+		for (auto it = _imtGod->_actors.begin(); it != _imtGod->_actors.end(); ++it) {
 			it->_value->process();
 		}
 		draw();
@@ -201,11 +176,6 @@ void MediaStationEngine::runEventLoop() {
 	}
 }
 
-void MediaStationEngine::initDisplayManager() {
-	_displayManager = new VideoDisplayManager(this);
-	_parameterClients.push_back(_displayManager);
-}
-
 void MediaStationEngine::initCursorManager() {
 	if (getPlatform() == Common::kPlatformWindows) {
 		_cursorManager = new WindowsCursorManager(getAppName());
@@ -214,43 +184,16 @@ void MediaStationEngine::initCursorManager() {
 	} else {
 		error("%s: Attempted to use unsupported platform %s", __func__, Common::getPlatformDescription(getPlatform()));
 	}
-	_parameterClients.push_back(_cursorManager);
 	_cursorManager->showCursor();
 }
 
-void MediaStationEngine::initFunctionManager() {
-	_functionManager = new FunctionManager();
-	_parameterClients.push_back(_functionManager);
-}
-
-void MediaStationEngine::initDocument() {
-	_document = new Document();
-	_parameterClients.push_back(_document);
-
-	DocumentActor *documentActor = new DocumentActor;
-	registerActor(documentActor);
-}
-
-void MediaStationEngine::initDeviceOwner() {
-	_deviceOwner = new DeviceOwner();
-	_parameterClients.push_back(_deviceOwner);
-}
-
-void MediaStationEngine::initStageDirector() {
-	_stageDirector = new StageDirector;
-}
-
-void MediaStationEngine::initStreamFeedManager() {
-	_streamFeedManager = new StreamFeedManager;
+ImtGod::ImtGod(MediaStationEngine *vm) : _vm(vm) {
+	_channelIdent = MKTAG('i', 'g', 'o', 'd');
 	registerWithStreamManager();
+	setupInitialStreamMap();
 }
 
-void MediaStationEngine::initProfile() {
-	_profile = new Profile();
-	_profile->load("PROFILE._ST");
-}
-
-void MediaStationEngine::setupInitialStreamMap() {
+void ImtGod::setupInitialStreamMap() {
 	StreamInfo streamInfo;
 	streamInfo._actorId = 0;
 	streamInfo._fileId = MediaStationEngine::BOOT_STREAM_ID;
@@ -317,34 +260,54 @@ void MediaStationEngine::draw(bool dirtyOnly) {
 	_displayManager->doTransitionOnSync();
 }
 
-void MediaStationEngine::registerActor(Actor *actorToAdd) {
+ImtGod::~ImtGod() {
+	unregisterWithStreamManager();
+	destroyAllContexts();
+	_contextReferences.clear();
+	_streamMap.clear();
+	_paramTokenDeclarations.clear();
+	_screenReferences.clear();
+	_fileMap.clear();
+	_actors.clear();
+}
+
+void ImtGod::addConstructedActor(Actor *actorToAdd) {
 	if (getActorById(actorToAdd->id())) {
 		error("[%s] %s: Already defined in this title", actorToAdd->debugName(), __func__);
 	}
 	_actors.setVal(actorToAdd->id(), actorToAdd);
 }
 
-void MediaStationEngine::destroyActor(uint actorId) {
+void ImtGod::destroyActor(uint actorId) {
+	// This performs the role of both destroyActor and removeConstructedActor in the original.
 	Actor *actorToDestroy = getActorById(actorId);
 	if (actorToDestroy) {
 		delete _actors[actorId];
 		_actors.erase(actorId);
 	} else {
-		warning("[%s] %s: Not currently loaded", formatActorName(actorId).c_str(), __func__);
+		warning("[%s] %s: Not currently loaded", _vm->formatActorName(actorId).c_str(), __func__);
 	}
 }
 
-void MediaStationEngine::destroyContext(uint contextId, bool eraseFromLoadedContexts) {
-	debugC(5, kDebugScript, "%s: Context %s", __func__, formatActorName(contextId).c_str());
+void ImtGod::registerParameterClient(ParameterClient *client) {
+	_parameterClients[client] = client;
+}
+
+void ImtGod::unregisterParameterClient(ParameterClient * client) {
+	_parameterClients.erase(client);
+}
+
+void ImtGod::destroyContext(uint contextId, bool eraseFromLoadedContexts) {
+	debugC(5, kDebugScript, "%s: Context %s", __func__, _vm->formatActorName(contextId).c_str());
 	Context *context = _loadedContexts.getValOrDefault(contextId);
 	if (context == nullptr) {
-		warning("%s: Attempted to unload context %s that is not currently loaded", __func__, formatActorName(contextId).c_str());
+		warning("%s: Attempted to unload context %s that is not currently loaded", __func__, _vm->formatActorName(contextId).c_str());
 		return;
 	}
 
-	getRootStage()->deleteChildrenFromContextId(contextId);
+	_vm->getRootStage()->deleteChildrenFromContextId(contextId);
 	destroyActorsInContext(contextId);
-	_functionManager->deleteFunctionsForContext(contextId);
+	_vm->getFunctionManager()->deleteFunctionsForContext(contextId);
 
 	delete context;
 	if (eraseFromLoadedContexts) {
@@ -354,20 +317,19 @@ void MediaStationEngine::destroyContext(uint contextId, bool eraseFromLoadedCont
 	}
 }
 
-bool MediaStationEngine::contextIsLocked(uint contextId) {
-	for (auto it = _loadedContexts.begin(); it != _loadedContexts.end(); ++it) {
-		uint id = it->_key;
-		ContextReference contextReference = _contextReferences.getVal(id);
-		for (uint childContextId : contextReference._parentContextIds) {
-			if (childContextId == contextId) {
-				return true;
-			}
-		}
-	}
-	return false;
+void ImtGod::lockContext(uint contextId) {
+	_lockedContextIds.setVal(contextId, true);
+}
+
+bool ImtGod::contextIsLocked(uint contextId) {
+	return _lockedContextIds.contains(contextId);
 }
 
-void MediaStationEngine::destroyActorsInContext(uint contextId) {
+void ImtGod::clearAllContextLocks() {
+	_lockedContextIds.clear();
+}
+
+void ImtGod::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.
@@ -385,9 +347,19 @@ void MediaStationEngine::destroyActorsInContext(uint contextId) {
 	}
 }
 
-void MediaStationEngine::readUnrecognizedFromStream(Chunk &chunk, uint sectionType) {
+void ImtGod::destroyAllContexts() {
+	// The original had a bug where loaded assets would not be explicitly freed upon quitting
+	// the engine. To avoid these leaks, call the full destroyContext each time.
+	for (auto it = _loadedContexts.begin(); it != _loadedContexts.end(); ++it) {
+		destroyContext(it->_value->_id, false);
+	}
+	_loadedContexts.clear();
+}
+
+void ImtGod::readUnrecognizedFromStream(Chunk &chunk, uint sectionType) {
 	bool paramHandled = false;
-	for (ParameterClient *client : g_engine->_parameterClients) {
+	for (auto it = _parameterClients.begin(); it != _parameterClients.end(); ++it) {
+		ParameterClient *client = it->_value;
 		if (client->attemptToReadFromStream(chunk, sectionType)) {
 			paramHandled = true;
 			break;
@@ -399,7 +371,7 @@ void MediaStationEngine::readUnrecognizedFromStream(Chunk &chunk, uint sectionTy
 	}
 }
 
-void MediaStationEngine::readChunk(Chunk &chunk) {
+void ImtGod::readChunk(Chunk &chunk) {
 	StreamType streamType = static_cast<StreamType>(chunk.readTypedUint16());
 	switch (streamType) {
 	case kDocumentDefStream:
diff --git a/engines/mediastation/mediastation.h b/engines/mediastation/mediastation.h
index 4d49b35ab63..79196a83f1b 100644
--- a/engines/mediastation/mediastation.h
+++ b/engines/mediastation/mediastation.h
@@ -27,6 +27,7 @@
 #include "common/system.h"
 #include "common/error.h"
 #include "common/fs.h"
+#include "common/hash-ptr.h"
 #include "common/hash-str.h"
 #include "common/random.h"
 #include "common/serializer.h"
@@ -35,24 +36,26 @@
 #include "engines/engine.h"
 #include "engines/savestate.h"
 
-#include "mediastation/clients.h"
-#include "mediastation/detection.h"
-#include "mediastation/datafile.h"
+#include "mediastation/actor.h"
+#include "mediastation/actors/stage.h"
 #include "mediastation/boot.h"
+#include "mediastation/clients.h"
 #include "mediastation/context.h"
-#include "mediastation/actor.h"
 #include "mediastation/cursors.h"
+#include "mediastation/datafile.h"
+#include "mediastation/detection.h"
 #include "mediastation/graphics.h"
-#include "mediastation/profile.h"
 #include "mediastation/mediascript/function.h"
-#include "mediastation/actors/stage.h"
+#include "mediastation/profile.h"
 
 namespace MediaStation {
 
 struct MediaStationGameDescription;
+class AudioSequence;
 class HotspotActor;
 class RootStage;
 class PixMapImage;
+class ImtGod;
 
 // Most Media Station titles follow this file structure from the root directory
 // of the CD-ROM:
@@ -67,11 +70,9 @@ static const char *const directoryGlobs[] = {
 	nullptr
 };
 
-// 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 {
+class MediaStationEngine : public Engine {
+friend class ImtGod;
+
 public:
 	MediaStationEngine(OSystem *syst, const ADGameDescription *gameDesc);
 	~MediaStationEngine() override;
@@ -80,34 +81,18 @@ public:
 	Common::String getGameId() const;
 	Common::Platform getPlatform() const;
 	const char *getAppName() const;
-	bool hasFeature(EngineFeature f) const override {
-		return
-		    (f == kSupportsReturnToLauncher);
-	};
-
-	bool isFirstGenerationEngine();
+	bool hasFeature(EngineFeature f) const override;
 	void dispatchSystemEvents();
 	void draw(bool dirtyOnly = true);
 
-	void registerActor(Actor *actorToAdd);
-	void destroyActor(uint actorId);
-	void destroyContext(uint contextId, bool eraseFromLoadedContexts = true);
-	bool contextIsLocked(uint contextId);
-
-	void readUnrecognizedFromStream(Chunk &chunk, uint sectionType);
-	void readHeaderSections(Subfile &subfile, Chunk &chunk);
-
-	Actor *getActorById(uint actorId);
-	Actor *getActorByIdAndType(uint actorId, ActorType expectedType);
-	SpatialEntity *getSpatialEntityById(uint spatialEntityId);
-	ChannelClient *getChannelClientByChannelIdent(uint channelIdent);
-	ScriptValue *getVariable(uint variableId);
 	VideoDisplayManager *getDisplayManager() { return _displayManager; }
 	CursorManager *getCursorManager() { return _cursorManager; }
 	FunctionManager *getFunctionManager() { return _functionManager; }
 	RootStage *getRootStage() { return _stageDirector->getRootStage(); }
+	StageDirector *getStageDirector() { return _stageDirector; }
 	StreamFeedManager *getStreamFeedManager() { return _streamFeedManager; }
 	Document *getDocument() { return _document; }
+	ImtGod *getImtGod() { return _imtGod; }
 
 	Common::String formatActorName(uint actorId, bool attemptToGetType = false) { return _profile->formatActorName(actorId, attemptToGetType); }
 	Common::String formatActorName(const Actor *actor) { return _profile->formatActorName(actor); }
@@ -117,14 +102,6 @@ public:
 	Common::String formatParamTokenName(uint paramToken) { return _profile->formatParamTokenName(paramToken); }
 	Common::String formatAssetNameForChannelIdent(uint channelIdent) { return _profile->formatAssetNameForChannelIdent(channelIdent); }
 
-	const FileInfo &fileInfoForIdent(uint fileId) { return _fileMap.getValOrDefault(fileId); }
-	const StreamInfo &streamInfoForIdent(uint streamId) { return _streamMap.getValOrDefault(streamId); }
-	const ScreenReference &screenRefWithId(uint screenActorId) { return _screenReferences.getValOrDefault(screenActorId); }
-	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; }
 	void clearMouseInsideHotspot() { _mouseInsideHotspot = nullptr; }
@@ -133,11 +110,10 @@ public:
 	void setMouseDownHotspot(SpatialEntity *entity) { _mouseDownHotspot = entity; }
 	void clearMouseDownHotspot() { _mouseDownHotspot = nullptr; }
 
-	Common::RandomSource _randomSource;
-
 	static const uint SCREEN_WIDTH = 640;
 	static const uint SCREEN_HEIGHT = 480;
 	static const uint BOOT_STREAM_ID = 1;
+	Common::RandomSource _randomSource;
 
 protected:
 	Common::Error run() override;
@@ -147,44 +123,84 @@ private:
 	Common::FSNode _gameDataDir;
 	const ADGameDescription *_gameDescription;
 
-	VideoDisplayManager *_displayManager = nullptr;
-	CursorManager *_cursorManager = nullptr;
+	SpatialEntity *_mouseInsideHotspot = nullptr;
+	SpatialEntity *_mouseDownHotspot = nullptr;
+
+	StreamFeedManager *_streamFeedManager = nullptr;
+	// CacheManager *_cacheManager = nullptr;
+	// StreamProfiler *_streamProfiler = nullptr;
+	ImtGod *_imtGod = nullptr;
+	ImtDeviceOwner *_deviceOwner = nullptr;
 	FunctionManager *_functionManager = nullptr;
+	VideoDisplayManager *_displayManager = nullptr;
+	// PrintManager *_printManager = nullptr;
 	Document *_document = nullptr;
-	DeviceOwner *_deviceOwner = nullptr;
+	CursorManager *_cursorManager = nullptr;
 	StageDirector *_stageDirector = nullptr;
-	StreamFeedManager *_streamFeedManager = nullptr;
 	Profile *_profile = nullptr;
 
-	Common::HashMap<uint, Actor *> _actors;
-	SpatialEntity *_mouseInsideHotspot = nullptr;
-	SpatialEntity *_mouseDownHotspot = nullptr;
+	Common::HashMap<AudioSequence *, AudioSequence *> _activeAudioSequences;
+
+	void initCursorManager();
+
+	void runEventLoop();
+};
+
+class ImtGod : public ChannelClient {
+friend class MediaStationEngine;
+
+public:
+	ImtGod(MediaStationEngine *vm);
+	virtual ~ImtGod() override;
+
+	void addConstructedActor(Actor *actorToAdd);
+	void destroyActor(uint actorId);
+	void registerParameterClient(ParameterClient *client);
+	void unregisterParameterClient(ParameterClient *client);
+	void destroyContext(uint contextId, bool eraseFromLoadedContexts = true);
+	void destroyAllContexts();
+	void lockContext(uint contextId);
+	bool contextIsLocked(uint contextId);
+	void clearAllContextLocks();
+
+	void readUnrecognizedFromStream(Chunk &chunk, uint sectionType);
+	void readHeaderSections(Subfile &subfile, Chunk &chunk);
+
+	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); }
+
+	Actor *getActorById(uint actorId);
+	Actor *getActorByIdAndType(uint actorId, ActorType expectedType);
+	SpatialEntity *getSpatialEntityById(uint spatialEntityId);
+	ChannelClient *getChannelClientByChannelIdent(uint channelIdent);
+	ScriptValue *getVariable(uint variableId);
+	Context *getContextById(uint contextId);
+
+	bool isFirstGenerationEngine() const;
+	void setupInitialStreamMap();
+
+private:
+	MediaStationEngine *_vm = nullptr;
 
 	Common::String _gameTitle;
 	VersionInfo _versionInfo;
 	Common::String _engineInfo;
 	Common::String _sourceString;
+	Common::HashMap<uint, Actor *> _actors;
 	Common::HashMap<uint, ContextReference> _contextReferences;
 	Common::HashMap<uint, ScreenReference> _screenReferences;
 	Common::HashMap<uint, FileInfo> _fileMap;
 	Common::HashMap<uint, StreamInfo> _streamMap;
 	Common::HashMap<Common::String, ScriptValue> _paramTokenDeclarations;
+	Common::HashMap<uint, bool> _lockedContextIds;
+	Common::HashMap<ParameterClient *, ParameterClient *> _parameterClients;
+	Common::HashMap<uint, Context *> _loadedContexts;
 	uint _unk1 = 0;
 	uint _functionTableSize = 0;
 	uint _unk3 = 0;
 
-	void initDisplayManager();
-	void initCursorManager();
-	void initFunctionManager();
-	void initDocument();
-	void initDeviceOwner();
-	void initStageDirector();
-	void initStreamFeedManager();
-	void initProfile();
-	void setupInitialStreamMap();
-
-	void runEventLoop();
-
 	virtual void readChunk(Chunk &chunk) override;
 	void readDocumentDef(Chunk &chunk);
 	void readDocumentInfoFromStream(Chunk &chunk, BootSectionType sectionType);
diff --git a/engines/mediastation/profile.cpp b/engines/mediastation/profile.cpp
index bbdf399df44..6a2b3b5b902 100644
--- a/engines/mediastation/profile.cpp
+++ b/engines/mediastation/profile.cpp
@@ -74,11 +74,12 @@ void Profile::readSection(Common::File &file, void (Profile::*sectionParserMetho
 	}
 }
 
-void Profile::load(const Common::Path &filename) {
+void Profile::load() {
+	const Common::Path FILENAME = "PROFILE._ST";
 	Common::File file;
-	if (!file.open(filename)) {
+	if (!file.open(FILENAME)) {
 		debugC(5, kDebugLoading, "%s: Could not open profile %s. Entity names will not be available.",
-			__func__, filename.toString().c_str());
+			__func__, FILENAME.toString().c_str());
 		return;
 	}
 	parseVersionInfo(file.readLine());
@@ -213,7 +214,7 @@ void Profile::parseScriptConstantInfo(const Common::String &line) {
 Common::String Profile::formatActorName(uint actorId, bool attemptToGetType) {
 	// If requested, try to get the actor type by looking up the loaded actor
 	if (attemptToGetType) {
-		Actor *actor = g_engine->getActorById(actorId);
+		Actor *actor = g_engine->getImtGod()->getActorById(actorId);
 		if (actor != nullptr) {
 			return formatActorName(actor);
 		}
diff --git a/engines/mediastation/profile.h b/engines/mediastation/profile.h
index b1af55856e4..a9266173047 100644
--- a/engines/mediastation/profile.h
+++ b/engines/mediastation/profile.h
@@ -71,7 +71,7 @@ struct ProfileScriptConstantInfo {
 // running any games.
 class Profile {
 public:
-	void load(const Common::Path &filename);
+	void load();
 
 	Common::String formatActorName(uint actorId, bool attemptToGetType = false);
 	Common::String formatActorName(const Actor *actor);


Commit: 2db0e59a8e16e9256bad1d2916b2c9a55b4f8d10
    https://github.com/scummvm/scummvm/commit/2db0e59a8e16e9256bad1d2916b2c9a55b4f8d10
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-04-30T20:19:20-04:00

Commit Message:
MEDIASTATION: Rewrite event subsystem for accuracy

There were too many subtle bugs occurring with the old implementation
that was not accurately based on the original, so it was time to more
accurately reimplement.

The overall changes are:
 - Maintain an event queue rather than dispatching script events directly.
 - Centralize timer handling in a timer service.

This fixes several bugs that were occurring due to incorrect event ordering.

Assisted-by: Claude:claude-sonnet-4.5

Changed paths:
  A engines/mediastation/events.cpp
  A engines/mediastation/events.h
    engines/mediastation/actor.cpp
    engines/mediastation/actor.h
    engines/mediastation/actors/camera.cpp
    engines/mediastation/actors/camera.h
    engines/mediastation/actors/diskimage.cpp
    engines/mediastation/actors/diskimage.h
    engines/mediastation/actors/document.cpp
    engines/mediastation/actors/document.h
    engines/mediastation/actors/hotspot.cpp
    engines/mediastation/actors/hotspot.h
    engines/mediastation/actors/movie.cpp
    engines/mediastation/actors/movie.h
    engines/mediastation/actors/path.cpp
    engines/mediastation/actors/path.h
    engines/mediastation/actors/screen.cpp
    engines/mediastation/actors/screen.h
    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/actors/stage.h
    engines/mediastation/actors/text.cpp
    engines/mediastation/actors/text.h
    engines/mediastation/actors/timer.cpp
    engines/mediastation/actors/timer.h
    engines/mediastation/audio.cpp
    engines/mediastation/audio.h
    engines/mediastation/clients.cpp
    engines/mediastation/clients.h
    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 a6bf0f0e7d0..86bc52f0b4a 100644
--- a/engines/mediastation/actor.cpp
+++ b/engines/mediastation/actor.cpp
@@ -197,23 +197,21 @@ ScriptValue Actor::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue>
 	return ScriptValue();
 }
 
-void Actor::processTimeScriptResponses() {
-	// TODO: Replace with a queue.
-	uint currentTime = g_system->getMillis();
-	const Common::Array<ScriptResponse *> &_timeResponses = _scriptResponses.getValOrDefault(kTimerEvent);
+void Actor::onEvent(const ActorEvent &event) {
+	warning("[%s] %s: No handler for engine event %s", debugName(), __func__, eventTypeToStr(event.type));
+}
+
+ScriptResponse *Actor::findNextTimeScriptResponseAfter(uint32 after) const {
+	const Common::Array<ScriptResponse *> &_timeResponses = _scriptResponses.getValOrDefault(kTimerScriptEvent);
 	for (ScriptResponse *timeEvent : _timeResponses) {
-		// Indeed float, not time.
 		double timeEventInFractionalSeconds = timeEvent->_argumentValue.asFloat();
-		uint timeEventInMilliseconds = timeEventInFractionalSeconds * 1000;
-		bool timeEventAlreadyProcessed = timeEventInMilliseconds < _lastProcessedTime;
-		bool timeEventNeedsToBeProcessed = timeEventInMilliseconds < currentTime - _startTime;
-		if (!timeEventAlreadyProcessed && timeEventNeedsToBeProcessed) {
-			debugC(5, kDebugScript, "%s: Running On Time response for time %d ms (lastProcessedTime: %d, currentTime: %d)",
-				__func__, timeEventInMilliseconds, _lastProcessedTime, currentTime);
-			timeEvent->execute(_id);
+		uint32 timeEventInMilliseconds = static_cast<uint32>(timeEventInFractionalSeconds * 1000);
+		if (timeEventInMilliseconds >= after) {
+			return timeEvent;
 		}
 	}
-	_lastProcessedTime = currentTime - _startTime;
+
+	return nullptr;
 }
 
 void Actor::runScriptResponseIfExists(EventType eventType, const ScriptValue &arg) {
@@ -242,6 +240,76 @@ void Actor::runScriptResponseIfExists(EventType eventType) {
 	runScriptResponseIfExists(eventType, scriptValue);
 }
 
+void Actor::processTimeScriptResponses() {
+	// The original had this logic duplicated across several actors, but it made more sense
+	// to consolidate it into the main Actor in the reimplementation. This does NOT set up the
+	// next timer - client code has to do that itself.
+	// Get current runtime time.
+	uint32 currentTimeInMilliseconds = g_engine->getTotalPlayTime();
+	uint32 elapsedTimeInMilliseconds = currentTimeInMilliseconds - _startTime;
+
+	// Process all events that have elapsed up to current time.
+	ScriptResponse *nextTimeScriptResponse = findNextTimeScriptResponseAfter(_lastProcessedTime);
+	while (nextTimeScriptResponse != nullptr) {
+		// If this event is in the future, stop processing.
+		double eventTimeInSeconds = nextTimeScriptResponse->_argumentValue.asFloat();
+		uint32 eventTimeInMilliseconds = eventTimeInSeconds * 1000;
+		if (eventTimeInMilliseconds > elapsedTimeInMilliseconds) {
+			break;
+		}
+
+		// Execute the event.
+		debugC(5, kDebugScript, "[%s] %s: Running On Time response for time %d ms (lastProcessedTime: %d, elapsedTime: %d)",
+			debugName(), __func__, eventTimeInMilliseconds, _lastProcessedTime, elapsedTimeInMilliseconds);
+		nextTimeScriptResponse->execute(_id);
+
+		// Increment by 1 to prevent re-triggering the same event. This works because in the original,
+		// timer events are at least 10 ms apart anyway.
+		_lastProcessedTime = eventTimeInMilliseconds + 1;
+		nextTimeScriptResponse = findNextTimeScriptResponseAfter(_lastProcessedTime);
+	}
+}
+
+bool Actor::setupNextScriptResponseTimer() {
+	// Find the next event after the last processed time.
+	ScriptResponse *nextEvent = findNextTimeScriptResponseAfter(_lastProcessedTime);
+	if (nextEvent == nullptr) {
+		// No more events to schedule.
+		debugC(5, kDebugScript, "[%s] %s: No more events to schedule", debugName(), __func__);
+		return false;
+	}
+
+	// Calculate duration until next event from current elapsed time.
+	double nextEventTimeInFractionalSeconds = nextEvent->_argumentValue.asFloat();
+	uint32 nextEventTimeInMilliseconds = nextEventTimeInFractionalSeconds * 1000;
+	uint32 currentTimeInMilliseconds = g_engine->getTotalPlayTime();
+	uint32 elapsedTimeInMilliseconds = currentTimeInMilliseconds - _startTime;
+	uint32 durationUntilNextEventInMilliseconds = nextEventTimeInMilliseconds - elapsedTimeInMilliseconds;
+	debugC(5, kDebugEvents, "[%s] %s: Scheduling timer for %d ms (next event at %d ms)",
+		debugName(), __func__, durationUntilNextEventInMilliseconds, nextEventTimeInMilliseconds);
+	g_engine->getTimerService()->startTimer(_timer, durationUntilNextEventInMilliseconds);
+	return true;
+}
+
+void Actor::triggerRemainingTimerEvents() {
+	uint32 currentTimeInMilliseconds = g_engine->getTotalPlayTime();
+	uint32 elapsedTimeInMilliseconds = currentTimeInMilliseconds - _startTime;
+
+	ScriptResponse *nextTimeScriptResponse = findNextTimeScriptResponseAfter(_lastProcessedTime);
+	while (nextTimeScriptResponse != nullptr) {
+		double eventTimeInSeconds = nextTimeScriptResponse->_argumentValue.asFloat();
+		uint32 eventTimeInMilliseconds = eventTimeInSeconds * 1000;
+
+		debugC(5, kDebugEvents, "[%s] %s: Running remaining On Time response for time %d ms (lastProcessedTime: %d, elapsedTime: %d)",
+			debugName(), __func__, eventTimeInMilliseconds,  _lastProcessedTime, elapsedTimeInMilliseconds);
+
+		// Increment by 1 to prevent re-triggering the same event.
+		_lastProcessedTime = eventTimeInMilliseconds + 1;
+		nextTimeScriptResponse->execute(_id);
+		nextTimeScriptResponse = findNextTimeScriptResponseAfter(_lastProcessedTime);
+	}
+}
+
 SpatialEntity::~SpatialEntity() {
 	if (_parentStage != nullptr) {
 		_parentStage->removeChildSpatialEntity(this);
@@ -464,12 +532,8 @@ void SpatialEntity::currentMousePosition(Common::Point &point) {
 }
 
 void SpatialEntity::invalidateMouse() {
-	// TODO: Invalidate the mouse properly when we have custom events.
-	// For now, we simulate the mouse update event with a mouse moved event.
-	Common::Event mouseEvent;
-	mouseEvent.type = Common::EVENT_MOUSEMOVE;
-	mouseEvent.mouse = g_system->getEventManager()->getMousePos();
-	g_system->getEventManager()->pushEvent(mouseEvent);
+	MouseEvent event(kMouseEnterExitEvent, g_system->getEventManager()->getMousePos());
+	g_engine->getEventLoop()->queueEvent(event);
 }
 
 void SpatialEntity::moveTo(int16 x, int16 y) {
diff --git a/engines/mediastation/actor.h b/engines/mediastation/actor.h
index b7e80feb835..db613153b1b 100644
--- a/engines/mediastation/actor.h
+++ b/engines/mediastation/actor.h
@@ -26,6 +26,7 @@
 #include "common/keyboard.h"
 
 #include "mediastation/datafile.h"
+#include "mediastation/events.h"
 #include "mediastation/mediascript/scriptresponse.h"
 #include "mediastation/mediascript/scriptconstants.h"
 #include "mediastation/mediascript/scriptvalue.h"
@@ -201,24 +202,21 @@ private:
 		warning("%s: Expected at least %d arguments, got %d", builtInMethodToStr(methodId), (min), args.size()); \
 	}
 
-class Actor {
+class Actor : public TimerEventReceiver {
 public:
-	Actor(ActorType type) : _type(type) {};
+	Actor(ActorType type) : _type(type), _timer(this) {};
 	virtual ~Actor();
 
-	// Does any needed frame drawing, audio playing, script responses, etc.
-	virtual void process() { return; }
+	virtual void timerEvent(const TimerEvent &event) { return; }
 
 	// Runs built-in bytecode methods.
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args);
-
-	virtual bool isSpatialActor() const { return false; }
-
 	virtual void initFromParameterStream(Chunk &chunk);
 	virtual void readParameter(Chunk &chunk, ActorHeaderSectionType paramType);
 	virtual void loadIsComplete();
 
-	void processTimeScriptResponses();
+	virtual void onEvent(const ActorEvent &event);
+	ScriptResponse *findNextTimeScriptResponseAfter(uint32 after) const;
 	void runScriptResponseIfExists(EventType eventType, const ScriptValue &arg);
 	void runScriptResponseIfExists(EventType eventType);
 
@@ -227,6 +225,8 @@ public:
 	uint contextId() const { return _contextId; }
 	void setId(uint id);
 	void setContextId(uint id) { _contextId = id; }
+	virtual bool isSpatialActor() const { return false; }
+
 	const char *debugName() const;
 
 protected:
@@ -236,10 +236,17 @@ protected:
 	uint _contextId = 0;
 	Common::String _debugName;
 
-	uint _startTime = 0;
-	uint _lastProcessedTime = 0;
 	uint _duration = 0;
 	Common::HashMap<uint, Common::Array<ScriptResponse *> > _scriptResponses;
+
+	// The original had these fields duplicated across several actors, but it made more
+	// sense to consolidate it into the main Actor in the reimplementation.
+	TimerEntry _timer;
+	uint _startTime = 0;
+	uint _lastProcessedTime = 0;
+	bool setupNextScriptResponseTimer();
+	void triggerRemainingTimerEvents();
+	void processTimeScriptResponses();
 };
 
 class SpatialEntity : public Actor {
@@ -276,13 +283,13 @@ public:
 		uint16 eventMask,
 		MouseActorState &state) { return kNoFlag; }
 
-	virtual void mouseDownEvent(const Common::Event &event) { return; }
-	virtual void mouseUpEvent(const Common::Event &event) { return; }
-	virtual void mouseEnteredEvent(const Common::Event &event) { return; }
-	virtual void mouseExitedEvent(const Common::Event &event) { return; }
-	virtual void mouseMovedEvent(const Common::Event &event) { return; }
-	virtual void mouseOutOfFocusEvent(const Common::Event &event) { return; }
-	virtual void keyboardEvent(const Common::Event &event) { return; }
+	virtual void mouseDownEvent(const MouseEvent &event) { return; }
+	virtual void mouseUpEvent(const MouseEvent &event) { return; }
+	virtual void mouseEnteredEvent(const MouseEvent &event) { return; }
+	virtual void mouseExitedEvent(const MouseEvent &event) { return; }
+	virtual void mouseMovedEvent(const MouseEvent &event) { return; }
+	virtual void mouseOutOfFocusEvent(const MouseEvent &event) { return; }
+	virtual void keyboardEvent(const KeyboardEvent &event) { return; }
 
 	void setParentStage(StageActor *parentStage) { _parentStage = parentStage; }
 	void setToNoParentStage() { _parentStage = nullptr; }
diff --git a/engines/mediastation/actors/camera.cpp b/engines/mediastation/actors/camera.cpp
index e4bc62a9e2d..da579dd6f96 100644
--- a/engines/mediastation/actors/camera.cpp
+++ b/engines/mediastation/actors/camera.cpp
@@ -242,6 +242,10 @@ ScriptValue CameraActor::callMethod(BuiltInMethod methodId, Common::Array<Script
 	return returnValue;
 }
 
+void CameraActor::onEvent(const ActorEvent &event) {
+	runScriptResponseIfExists(event.type);
+}
+
 void CameraActor::invalidateLocalBounds() {
 	if (_parentStage != nullptr) {
 		_parentStage->invalidateLocalBounds();
@@ -465,47 +469,48 @@ void CameraActor::panToByTime(int16 x, int16 y, double duration) {
 	_panState = kCameraPanToByTime;
 	_panStart = _currentViewportOrigin;
 	_panDest = Common::Point(x, y);
-	_panDuration = duration;
+	_totalPanDuration = duration;
 	_currentPanStep = 1;
-	_startTime = g_system->getMillis();
-	_nextPanStepTime = 0;
+	_startTime = g_engine->getTotalPlayTime();
 	debugC(6, kDebugCamera, "[%s] %s: panStart: (%d, %d); panDest: (%d, %d); panDuration: %f",
-		debugName(), __func__, _panStart.x, _panStart.y, _panDest.x, _panDest.y, _panDuration);
+		debugName(), __func__, _panStart.x, _panStart.y, _panDest.x, _panDest.y, _totalPanDuration);
 	setXYDelta();
 	calcNewViewportOrigin();
+
+	g_engine->getTimerService()->startTimer(_timer, _durationBetweenStepEvents);
 }
 
 void CameraActor::panToByStepCount(int16 x, int16 y, uint panSteps, double duration) {
 	_panState = kCameraPanByStepCount;
 	_panStart = _currentViewportOrigin;
 	_panDest = Common::Point(x, y);
-	_panDuration = duration;
+	_durationBetweenStepEvents = duration;
 	_currentPanStep = 1;
 	_maxPanStep = panSteps;
-	_startTime = g_system->getMillis();
-	_nextPanStepTime = 0;
 	debugC(6, kDebugCamera, "[%s] %s: panStart: (%d, %d); panDest: (%d, %d); panDuration: %f; maxPanStep: %d",
-		debugName(), __func__, _panStart.x, _panStart.y, _panDest.x, _panDest.y, _panDuration, _maxPanStep);
+		debugName(), __func__, _panStart.x, _panStart.y, _panDest.x, _panDest.y, _totalPanDuration, _maxPanStep);
 	setXYDelta();
 	calcNewViewportOrigin();
+	g_engine->getTimerService()->startTimer(_timer, _durationBetweenStepEvents);
 }
 
 void CameraActor::startPan(uint xOffset, uint yOffset, double duration) {
 	_panState = kCameraPanningStarted;
-	_panDuration = duration;
-	_startTime = g_system->getMillis();
-	_nextPanStepTime = 0;
+	g_engine->getTimerService()->startTimer(_timer, duration);
+	_durationBetweenStepEvents = duration;
+	setXYDelta(xOffset, yOffset);
+
 	_currentPanStep = 0;
 	_maxPanStep = 0;
-	setXYDelta(xOffset, yOffset);
 	debugC(6, kDebugCamera, "[%s] %s: xOffset: %u, yOffset: %u, duration: %f", debugName(), __func__, xOffset, yOffset, duration);
 }
 
 void CameraActor::stopPan() {
 	_panState = kCameraNotPanning;
-	_panDuration = 0.0;
+	g_engine->getTimerService()->stopTimer(_timer);
+
+	_totalPanDuration = 0.0;
 	_startTime = 0;
-	_nextPanStepTime = 0;
 	_currentPanStep = 0;
 	_maxPanStep = 0;
 	debugC(6, kDebugCamera, "[%s] %s: nextViewportOrigin: (%d, %d); actualViewportOrigin: (%d, %d)",
@@ -527,24 +532,7 @@ bool CameraActor::continuePan() {
 	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() {
+void CameraActor::timerEvent(const TimerEvent &event) {
 	if (_parentStage != nullptr) {
 		if (processViewportMove()) {
 			processNextPanStep();
@@ -553,8 +541,10 @@ void CameraActor::timerEvent() {
 					adjustCameraViewport(_nextViewportOrigin);
 					Common::Rect advanceRect = getAdvanceRect();
 					_parentStage->preload(advanceRect, false);
+					g_engine->getTimerService()->startTimer(_timer, _durationBetweenStepEvents);
 				} else {
-					runScriptResponseIfExists(kCameraPanAbortEvent);
+					ActorEvent actorEvent(_id, kCameraPanAbortEvent);
+					g_engine->getEventLoop()->queueEvent(actorEvent);
 					stopPan();
 				}
 			} else {
@@ -565,18 +555,21 @@ void CameraActor::timerEvent() {
 					success = processViewportMove();
 				}
 				if (success) {
-					runScriptResponseIfExists(kCameraPanEndEvent);
+					ActorEvent actorEvent(_id, kCameraPanEndEvent);
+					g_engine->getEventLoop()->queueEvent(actorEvent);
 					stopPan();
 				} else {
 					Common::Rect currentBounds = getBbox();
 					Common::Rect preloadBounds(_nextViewportOrigin, currentBounds.width(), currentBounds.height());
 					_parentStage->preload(preloadBounds, false);
+					g_engine->getTimerService()->startTimer(_timer, _durationBetweenStepEvents);
 				}
 			}
 		} else {
 			Common::Rect currentBounds = getBbox();
 			Common::Rect preloadBounds(_nextViewportOrigin, currentBounds.width(), currentBounds.height());
 			_parentStage->preload(preloadBounds, false);
+			g_engine->getTimerService()->startTimer(_timer, _durationBetweenStepEvents);
 		}
 	}
 }
@@ -606,10 +599,8 @@ void CameraActor::processNextPanStep() {
 	}
 
 	calcNewViewportOrigin();
-	runScriptResponseIfExists(kCameraPanStepEvent);
-
-	uint stepDurationInMilliseconds = 20; // Visually smooth.
-	_nextPanStepTime += stepDurationInMilliseconds;
+	ActorEvent event(_id, kCameraPanStepEvent);
+	g_engine->getEventLoop()->queueEvent(event);
 }
 
 void CameraActor::adjustCameraViewport(Common::Point &viewportToAdjust) {
@@ -706,10 +697,10 @@ double CameraActor::percentComplete() {
 
 	case kCameraPanToByTime: {
 		const double MILLISECONDS_IN_ONE_SECOND = 1000.0;
-		uint currentRuntime = g_system->getMillis();
+		uint currentRuntime = g_engine->getTotalPlayTime();
 		uint elapsedTime = currentRuntime - _startTime;
 		double elapsedSeconds = elapsedTime / MILLISECONDS_IN_ONE_SECOND;
-		percentValue = elapsedSeconds / _panDuration;
+		percentValue = elapsedSeconds / _totalPanDuration;
 		break;
 	}
 
diff --git a/engines/mediastation/actors/camera.h b/engines/mediastation/actors/camera.h
index a3ed09a9843..0f6555b03a6 100644
--- a/engines/mediastation/actors/camera.h
+++ b/engines/mediastation/actors/camera.h
@@ -50,7 +50,9 @@ public:
 	virtual void readChunk(Chunk &chunk) override;
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 	virtual void loadIsComplete() override;
-	virtual void process() override;
+
+	virtual void onEvent(const ActorEvent &event) override;
+	virtual void timerEvent(const TimerEvent &event) override;
 
 	Common::Point getViewportOrigin();
 	Common::Rect getViewportBounds();
@@ -61,11 +63,11 @@ public:
 private:
 	bool _lensOpen = false;
 	bool _addedToStage = false;
-	double _panDuration = 0.0;
+	double _totalPanDuration = 0.0;
+	double _durationBetweenStepEvents = 0.0;
 	uint _currentPanStep = 0;
 	uint _maxPanStep = 0;
 	uint _startTime = 0;
-	uint _nextPanStepTime = 0;
 	CameraPanState _panState = kCameraNotPanning;
 	Common::Point _offset;
 	Common::Point _currentViewportOrigin;
@@ -94,7 +96,6 @@ private:
 	void stopPan();
 	bool continuePan();
 
-	void timerEvent();
 	bool processViewportMove();
 	void processNextPanStep();
 	void adjustCameraViewport(Common::Point &viewportToAdjust);
diff --git a/engines/mediastation/actors/diskimage.cpp b/engines/mediastation/actors/diskimage.cpp
index 081ee0f5977..88989b2e80a 100644
--- a/engines/mediastation/actors/diskimage.cpp
+++ b/engines/mediastation/actors/diskimage.cpp
@@ -171,11 +171,18 @@ ScriptValue DiskImageActor::callMethod(BuiltInMethod methodId, Common::Array<Scr
 	return returnValue;
 }
 
-void DiskImageActor::process() {
-	if (_isLoading) {
-		// All timer events are scheduled for 0.0 seconds, so just fire them immediately.
-		// We don't need to track time separately.
-		timerEvent();
+void DiskImageActor::onEvent(const ActorEvent &event) {
+	switch (event.type) {
+	case kCachingStartedEvent:
+	case kCachingEndedEvent:
+	case kCachingFailureEvent:
+		// Caching-related events are not implemented, but they can be implemented
+		// if the original CD-ROM streaming/caching logic is reimplemented.
+		Actor::onEvent(event);
+		break;
+
+	default:
+		runScriptResponseIfExists(event.type);
 	}
 }
 
@@ -209,20 +216,23 @@ void DiskImageActor::readChunk(Chunk &chunk) {
 	PixMapImage *stripImage = new PixMapImage(chunk, imageInfo, _shouldDecompressInPlace);
 	StripImageNode stripImageNode;
 	stripImageNode.image = stripImage;
+	// In the original, this does indeed get the absolute time, not gameplay time.
 	stripImageNode.lastDrawTime = g_system->getMillis();
 	stripImageNode.isLoaded = true;
 	_stripImageNodes.setVal(index, stripImageNode);
 
-	// This was originally in RT_DiskImageActor::releaseBuffer, but it's here since
-	// we read data synchronously for now.
+	// This was in releaseBuffer in the original, but it's here since
+	// we read data synchronously for now in the reimplementation.
 	if (_isLoadingStrips) {
 		if (isRectInMemory(_rectToLoad)) {
 			if (_firePreloadEvent) {
-				generateLoadEvent(kDiskImageActorStepEvent);
+				ActorEvent event(_id, kDiskImageActorStepEvent);
+				g_engine->getEventLoop()->queueEvent(event);
 			}
 			stopLoad();
 		} else {
-			// The original scheduled a zero-length timer for the next load.
+			// The timer is meant to fire immediately.
+			g_engine->getTimerService()->startTimer(_timer, (uint32)0);
 			unregisterWithStreamManager();
 		}
 	}
@@ -247,6 +257,7 @@ void DiskImageActor::draw(DisplayContext &displayContext) {
 					error("[%s] %s: Strip index %d marked as loaded but image node not found", debugName(), __func__, stripIndex);
 				}
 				StripImageNode &imageNode = _stripImageNodes.getVal(stripIndex);
+				// In the original, this does indeed get the absolute time, not gameplay time.
 				imageNode.lastDrawTime = g_system->getMillis();
 
 				// Draw the strip image at its designated position.
@@ -271,9 +282,13 @@ void DiskImageActor::preload(const Common::Rect &rect, bool fireStepEvent) {
 		_firePreloadEvent = fireStepEvent;
 		_rectToLoad = rectToLoad;
 
+		// The timer is meant to fire immediately.
+		g_engine->getTimerService()->startTimer(_timer, (uint32)0);
+
 	} else if (fireStepEvent) {
 		debugC(5, kDebugGraphics, " (loaded)");
-		generateLoadEvent(kDiskImageActorStepEvent);
+		ActorEvent event(_id, kDiskImageActorStepEvent);
+		g_engine->getEventLoop()->queueEvent(event);
 	}
 }
 
@@ -416,13 +431,14 @@ void DiskImageActor::startStripLoad(uint stripIndex) {
 	_isLoadingStrips = false;
 }
 
-void DiskImageActor::timerEvent() {
+void DiskImageActor::timerEvent(const TimerEvent &event) {
 	int stripId = getStripToLoad();
 	if (stripId != -1) {
 		startStripLoad(stripId);
 	} else {
 		if (_firePreloadEvent) {
-			generateLoadEvent(kDiskImageActorEndEvent);
+			ActorEvent actorEvent(_id, kDiskImageActorEndEvent);
+			g_engine->getEventLoop()->queueEvent(actorEvent);
 		}
 		_isLoading = false;
 	}
@@ -457,11 +473,6 @@ void DiskImageActor::stopLoad() {
 	unregisterWithStreamManager();
 }
 
-void DiskImageActor::generateLoadEvent(EventType eventType) {
-	// In the original, this does just queue an event.
-	runScriptResponseIfExists(eventType);
-}
-
 void DiskImageActor::debugPrintNodes() {
 	debugC(5, kDebugGraphics, "[%s] %s: STRIP NODES [", debugName(), __func__);
 	for (auto it = _stripInfoNodes.begin(); it != _stripInfoNodes.end(); ++it) {
diff --git a/engines/mediastation/actors/diskimage.h b/engines/mediastation/actors/diskimage.h
index cdc49ee15cc..d7de57f595c 100644
--- a/engines/mediastation/actors/diskimage.h
+++ b/engines/mediastation/actors/diskimage.h
@@ -44,17 +44,19 @@ struct StripImageNode {
 	uint lastDrawTime = 0;
 };
 
-class DiskImageActor : public SpatialEntity, public ChannelClient {
 // Despite the name from the original, this is not a "disk image" but
 // a set of graphics (like a large background) that are streamed from
 // disk in a very particular way.
+class DiskImageActor : public SpatialEntity, public ChannelClient {
 public:
 	DiskImageActor() : SpatialEntity(kActorTypeDiskImage) {};
 	~DiskImageActor();
 
 	virtual void readParameter(Chunk &chunk, ActorHeaderSectionType paramType) override;
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
-	virtual void process() override;
+
+	virtual void onEvent(const ActorEvent &event) override;
+	virtual void timerEvent(const TimerEvent &event) override;
 
 	virtual void readChunk(Chunk &chunk) override;
 	virtual void draw(DisplayContext &displayContext) override;
@@ -68,11 +70,9 @@ private:
 	void setStripsToLoad(const Common::Rect &rectToLoad);
 	int getStripToLoad();
 	void startStripLoad(uint stripIndex);
-	void timerEvent();
 
 	void purge();
 	void stopLoad();
-	void generateLoadEvent(EventType eventType);
 	void unloadLeastRecentlyDrawnStrip();
 	void debugPrintNodes();
 
diff --git a/engines/mediastation/actors/document.cpp b/engines/mediastation/actors/document.cpp
index 3ed12e596b4..8e961d598cc 100644
--- a/engines/mediastation/actors/document.cpp
+++ b/engines/mediastation/actors/document.cpp
@@ -57,15 +57,15 @@ ScriptValue DocumentActor::callMethod(BuiltInMethod methodId, Common::Array<Scri
 
 	case kDocumentLoadContextMethod: {
 		ARGCOUNTCHECK(1);
-		uint contextId = args[0].asActorId();
-		g_engine->getDocument()->startContextLoad(contextId);
+		ActorEvent event(_id, kContextLoadStartEvent, args[0]);
+		g_engine->getEventLoop()->queueEvent(event);
 		break;
 	}
 
 	case kDocumentReleaseContextMethod: {
 		ARGCOUNTCHECK(1);
-		uint contextId = args[0].asActorId();
-		g_engine->getDocument()->scheduleContextRelease(contextId);
+		ActorEvent event(_id, kContextReleaseStartEvent, args[0]);
+		g_engine->getEventLoop()->queueEvent(event);
 		break;
 	}
 
@@ -73,10 +73,10 @@ ScriptValue DocumentActor::callMethod(BuiltInMethod methodId, Common::Array<Scri
 		ARGCOUNTCHECK(1);
 		uint contextId = args[0].asActorId();
 
-		// We are looking for the screen actor with the same ID as the context.
-		Actor *screenActor = g_engine->getImtGod()->getActorById(contextId);
+		// Check if the context exists in loaded contexts and is not currently loading.
+		Context *context = g_engine->getImtGod()->getContextById(contextId);
 		bool contextIsLoading = g_engine->getDocument()->isContextLoadInProgress(contextId);
-		bool contextIsLoaded = (screenActor != nullptr) && !contextIsLoading;
+		bool contextIsLoaded = (context != nullptr) && !contextIsLoading;
 		returnValue.setToBool(contextIsLoaded);
 		break;
 	}
@@ -87,15 +87,63 @@ ScriptValue DocumentActor::callMethod(BuiltInMethod methodId, Common::Array<Scri
 	return returnValue;
 }
 
+void DocumentActor::onEvent(const ActorEvent &event) {
+	switch (event.type) {
+	case kScreenBranchEvent: {
+		const ScreenBranchEvent &branchEvent = static_cast<const ScreenBranchEvent &>(event);
+		g_engine->getDocument()->branchToScreen(branchEvent.screenId, branchEvent.disableScreenAutoUpdate);
+		break;
+	}
+
+	case kContextLoadStartEvent: {
+		uint contextId = event.arg.asActorId();
+		g_engine->getDocument()->startContextLoad(contextId);
+		break;
+	}
+
+	case kContextReleaseStartEvent: {
+		uint contextId = event.arg.asActorId();
+		if (g_engine->getImtGod()->contextIsLocked(contextId)) {
+			break;
+		}
+
+		Context *context = g_engine->getImtGod()->getContextById(contextId);
+		if (context == nullptr) {
+			if (g_engine->getDocument()->isContextLoadQueued(contextId)) {
+				g_engine->getDocument()->removeFromContextLoadQueue(contextId);
+				g_engine->getDocument()->contextReleaseComplete(contextId);
+			} else {
+				g_engine->getDocument()->contextAlreadyReleased(contextId);
+			}
+		} else {
+			if (g_engine->getDocument()->isContextLoadInProgress(contextId)) {
+				// The original calls interruptFeed here. Not implemented because the
+				// streaming logic isn't complete (and might not be needed).
+			}
+			g_engine->getImtGod()->destroyContext(contextId);
+			g_engine->getDocument()->contextReleaseComplete(contextId);
+		}
+		break;
+	}
+
+	default:
+		Actor::onEvent(event);
+	}
+}
+
 void DocumentActor::processBranch(Common::Array<ScriptValue> &args) {
+	bool disableScreenAutoUpdate = true;
 	uint contextId = args[0].asActorId();
 	if (args.size() > 1) {
-		bool disableUpdates = static_cast<bool>(args[1].asParamToken());
-		if (disableUpdates)
-			warning("[%s] %s: disableUpdates parameter not handled yet", debugName(), __func__);
+		uint disablescreenAutoUpdateUpdateParamToken = args[1].asParamToken();
+		const uint DISABLE_SCREEN_AUTO_UPDATE_PARAM_TOKEN = 0x708;
+		if (disablescreenAutoUpdateUpdateParamToken == DISABLE_SCREEN_AUTO_UPDATE_PARAM_TOKEN) {
+			disableScreenAutoUpdate = false;
+		}
 	}
 
-	g_engine->getDocument()->scheduleScreenBranch(contextId);
+	ScreenBranchEvent event(_id, contextId, disableScreenAutoUpdate);
+	g_engine->getEventLoop()->queueEvent(event);
 }
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/actors/document.h b/engines/mediastation/actors/document.h
index 57b23203e0b..af7c36e274a 100644
--- a/engines/mediastation/actors/document.h
+++ b/engines/mediastation/actors/document.h
@@ -32,6 +32,7 @@ class DocumentActor : public Actor {
 public:
 	static const uint DOCUMENT_ACTOR_ID = 1;
 	DocumentActor() : Actor(kActorTypeDocument) { _id = DOCUMENT_ACTOR_ID; };
+	virtual void onEvent(const ActorEvent &event) override;
 
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 
diff --git a/engines/mediastation/actors/hotspot.cpp b/engines/mediastation/actors/hotspot.cpp
index 35e1994ddd7..357a009919d 100644
--- a/engines/mediastation/actors/hotspot.cpp
+++ b/engines/mediastation/actors/hotspot.cpp
@@ -174,7 +174,7 @@ void HotspotActor::deactivate() {
 	}
 }
 
-void HotspotActor::mouseDownEvent(const Common::Event &event) {
+void HotspotActor::mouseDownEvent(const MouseEvent &event) {
 	if (!_isActive) {
 		warning("[%s] %s: Inactive", debugName(), __func__);
 		return;
@@ -184,7 +184,7 @@ void HotspotActor::mouseDownEvent(const Common::Event &event) {
 	runScriptResponseIfExists(kMouseDownEvent);
 }
 
-void HotspotActor::mouseUpEvent(const Common::Event &event) {
+void HotspotActor::mouseUpEvent(const MouseEvent &event) {
 	if (!_isActive) {
 		warning("[%s] %s: Inactive", debugName(), __func__);
 		return;
@@ -194,7 +194,7 @@ void HotspotActor::mouseUpEvent(const Common::Event &event) {
 	runScriptResponseIfExists(kMouseUpEvent);
 }
 
-void HotspotActor::mouseEnteredEvent(const Common::Event &event) {
+void HotspotActor::mouseEnteredEvent(const MouseEvent &event) {
 	if (!_isActive) {
 		warning("[%s] %s: Inactive", debugName(), __func__);
 		return;
@@ -212,7 +212,7 @@ void HotspotActor::mouseEnteredEvent(const Common::Event &event) {
 	runScriptResponseIfExists(kMouseEnteredEvent);
 }
 
-void HotspotActor::mouseMovedEvent(const Common::Event &event) {
+void HotspotActor::mouseMovedEvent(const MouseEvent &event) {
 	if (!_isActive) {
 		warning("[%s] %s: Inactive", debugName(), __func__);
 		return;
@@ -221,7 +221,7 @@ void HotspotActor::mouseMovedEvent(const Common::Event &event) {
 	runScriptResponseIfExists(kMouseMovedEvent);
 }
 
-void HotspotActor::mouseExitedEvent(const Common::Event &event) {
+void HotspotActor::mouseExitedEvent(const MouseEvent &event) {
 	if (!_isActive) {
 		warning("[%s] %s: Inactive", debugName(), __func__);
 		return;
diff --git a/engines/mediastation/actors/hotspot.h b/engines/mediastation/actors/hotspot.h
index 351a7859936..e58285acacd 100644
--- a/engines/mediastation/actors/hotspot.h
+++ b/engines/mediastation/actors/hotspot.h
@@ -23,6 +23,7 @@
 #define MEDIASTATION_HOTSPOT_H
 
 #include "mediastation/actor.h"
+#include "mediastation/events.h"
 #include "mediastation/mediascript/scriptvalue.h"
 #include "mediastation/mediascript/scriptconstants.h"
 
@@ -49,11 +50,11 @@ public:
 	void activate();
 	void deactivate();
 
-	virtual void mouseDownEvent(const Common::Event &event) override;
-	virtual void mouseUpEvent(const Common::Event &event) override;
-	virtual void mouseEnteredEvent(const Common::Event &event) override;
-	virtual void mouseExitedEvent(const Common::Event &event) override;
-	virtual void mouseMovedEvent(const Common::Event &event) override;
+	virtual void mouseDownEvent(const MouseEvent &event) override;
+	virtual void mouseUpEvent(const MouseEvent &event) override;
+	virtual void mouseEnteredEvent(const MouseEvent &event) override;
+	virtual void mouseExitedEvent(const MouseEvent &event) override;
+	virtual void mouseMovedEvent(const MouseEvent &event) override;
 
 	uint _cursorResourceId = 0;
 
diff --git a/engines/mediastation/actors/movie.cpp b/engines/mediastation/actors/movie.cpp
index 6cb762b707a..a15d6af4de2 100644
--- a/engines/mediastation/actors/movie.cpp
+++ b/engines/mediastation/actors/movie.cpp
@@ -92,6 +92,7 @@ MovieFrameImage::MovieFrameImage(Chunk &chunk, uint index, uint keyframeEndInMil
 
 StreamMovieActor::~StreamMovieActor() {
 	unregisterWithStreamManager();
+	unregisterForSyncCalls();
 	if (_streamFeed != nullptr) {
 		g_engine->getStreamFeedManager()->closeStreamFeed(_streamFeed);
 		_streamFeed = nullptr;
@@ -162,7 +163,7 @@ void StreamMovieActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramT
 	}
 
 	case kActorHeaderSoundInfo:
-		_streamSound->_audioSequence.readParameters(chunk);
+		_streamSound->getAudioSequence().readParameters(chunk);
 		break;
 
 	case kStreamMovieProxyInfo: {
@@ -230,7 +231,7 @@ ScriptValue StreamMovieActor::callMethod(BuiltInMethod methodId, Common::Array<S
 
 	case kTimeStopMethod: {
 		ARGCOUNTCHECK(0);
-		timeStop();
+		timeStop(false);
 		break;
 	}
 
@@ -333,6 +334,40 @@ ScriptValue StreamMovieActor::callMethod(BuiltInMethod methodId, Common::Array<S
 	return returnValue;
 }
 
+void StreamMovieActor::onEvent(const ActorEvent &event) {
+	switch (event.type) {
+	case kMovieEndEvent:
+		triggerRemainingTimerEvents();
+		break;
+
+	case kCachingStartedEvent:
+	case kCachingEndedEvent:
+	case kCachingFailureEvent:
+		// Caching-related events are not implemented, but they can be implemented
+		// if the original CD-ROM streaming/caching logic is reimplemented.
+		Actor::onEvent(event);
+		break;
+
+	case kMovieStoppedEvent:
+	case kMovieAbortEvent:
+	case kMovieFailureEvent:
+		_isPlaying = false;
+		break;
+
+	default:
+		break;
+	}
+
+	runScriptResponseIfExists(event.type);
+}
+
+void StreamMovieActor::timerEvent(const TimerEvent &event) {
+	Actor::processTimeScriptResponses();
+
+	g_engine->getTimerService()->stopTimer(_timer);
+	setupNextScriptResponseTimer();
+}
+
 void StreamMovieActor::timePlay() {
 	if (_streamFeed == nullptr) {
 		_streamFeed = g_engine->getStreamFeedManager()->openStreamFeed(_id);
@@ -343,42 +378,71 @@ void StreamMovieActor::timePlay() {
 		return;
 	}
 
-	_streamSound->_audioSequence.play();
+	setupNextScriptResponseTimer();
+	registerForSyncCalls();
+	_streamSound->getAudioSequence().start();
 	_framesNotYetShown = _streamFrames->_frames;
 	_framesOnScreen.clear();
 	_isPlaying = true;
-	_startTime = g_system->getMillis();
+	_startTime = g_engine->getTotalPlayTime();
 	_lastProcessedTime = 0;
-	runScriptResponseIfExists(kMovieBeginEvent);
-	process();
+
+	ActorEvent actorEvent(_id, kMovieBeginEvent);
+	g_engine->getEventLoop()->queueEvent(actorEvent);
+
+	if (_disableScreenAutoUpdateToken == 0) {
+		_disableScreenAutoUpdateToken = g_engine->getDisplayUpdateManager()->disableAutoUpdate();
+
+		g_engine->getDisplayUpdateManager()->enableAutoUpdate(_disableScreenAutoUpdateToken);
+		_disableScreenAutoUpdateToken = 0;
+	}
+
+	updateFrameState();
 }
 
-void StreamMovieActor::timeStop() {
+void StreamMovieActor::timeStop(bool isMovieEnd) {
 	if (!_isPlaying) {
 		return;
 	}
 
-	for (MovieFrame *frame : _framesOnScreen) {
-		invalidateRect(getFrameBoundingBox(frame));
-	}
-	_streamSound->_audioSequence.stop();
-	_framesNotYetShown.empty();
+	_isPlaying = false;
+	_streamSound->getAudioSequence().stop();
+	g_engine->getTimerService()->stopTimer(_timer);
+	unregisterForSyncCalls();
+
+	_framesNotYetShown.clear();
 	if (_hasStill) {
 		_framesNotYetShown = _streamFrames->_frames;
 	}
 	_framesOnScreen.clear();
-	_startTime = 0;
-	_lastProcessedTime = 0;
-	_isPlaying = false;
-	runScriptResponseIfExists(kMovieStoppedEvent);
+
+	EventType eventType = isMovieEnd ? kMovieEndEvent : kMovieStoppedEvent;
+	ActorEvent actorEvent(_id, eventType);
+	g_engine->getEventLoop()->queueEvent(actorEvent);
+
+	if (_disableScreenAutoUpdateToken == 0) {
+		_disableScreenAutoUpdateToken = g_engine->getDisplayUpdateManager()->disableAutoUpdate();
+
+		DisplayEvent event(kDisplayEnableAutoUpdateEvent, _disableScreenAutoUpdateToken);
+		g_engine->getEventLoop()->queueEvent(event);
+		_disableScreenAutoUpdateToken = 0;
+	}
+
+	updateFrameState();
 }
 
-void StreamMovieActor::process() {
+PreDisplaySyncState StreamMovieActor::preDisplaySync() {
+	if (!_isPlaying) {
+		return kPreDisplaySyncNoScreenUpdateRequested;
+	}
+
+	// Update frame state if visible.
 	if (_isVisible) {
-		if (_isPlaying) {
-			processTimeScriptResponses();
-		}
 		updateFrameState();
+		return kPreDisplaySyncForceScreenUpdate;
+	} else {
+		// Request display update when playing.
+		return kPreDisplaySyncNoScreenUpdateRequested;
 	}
 }
 
@@ -392,7 +456,7 @@ void StreamMovieActor::setVisibility(bool visibility) {
 void StreamMovieActor::updateFrameState() {
 	uint movieTime = 0;
 	if (_isPlaying) {
-		uint currentTime = g_system->getMillis();
+		uint currentTime = g_engine->getTotalPlayTime();
 		movieTime = currentTime - _startTime;
 	}
 	debugC(7, kDebugGraphics, "[%s] %s: Starting update (movie time: %d)", debugName(), __func__, movieTime);
@@ -432,12 +496,7 @@ void StreamMovieActor::updateFrameState() {
 			it = _framesOnScreen.erase(it);
 
 			if (_framesOnScreen.empty() && movieTime >= _fullTime) {
-				_isPlaying = false;
-				if (_hasStill) {
-					_framesNotYetShown = _streamFrames->_frames;
-					updateFrameState();
-				}
-				runScriptResponseIfExists(kMovieEndEvent);
+				timeStop(true);
 				break;
 			}
 		} else {
@@ -501,7 +560,6 @@ void StreamMovieActor::invalidateLocalBounds() {
 	SpatialEntity::invalidateLocalBounds();
 }
 
-
 Common::Rect StreamMovieActor::getFrameBoundingBox(MovieFrame *frame) {
 	// Use _boundingBox directly (which may be temporarily offset by camera rendering)
 	// The camera offset is already applied to _boundingBox by pushBoundingBoxOffset()
@@ -566,9 +624,19 @@ void StreamMovieActorSound::readChunk(Chunk &chunk) {
 	_audioSequence.readChunk(chunk);
 }
 
-StreamMovieActor::StreamMovieActor() : _framesOnScreen(StreamMovieActor::compareFramesByZIndex), SpatialEntity(kActorTypeMovie) {
+void StreamMovieActorSound::soundPlayStateChanged(SoundPlayState state, SoundStopReason why) {
+	if (state == kSoundPlayStateStopped) {
+		if (why == kSoundStopForFailure) {
+			ActorEvent actorEvent(_parent->_id, kMovieFailureEvent);
+			g_engine->getEventLoop()->queueEvent(actorEvent);
+		}
+	}
+}
+
+StreamMovieActor::StreamMovieActor() :
+	_framesOnScreen(StreamMovieActor::compareFramesByZIndex), SpatialEntity(kActorTypeMovie) {
 	_streamFrames = new StreamMovieActorFrames(this);
-	_streamSound = new StreamMovieActorSound();
+	_streamSound = new StreamMovieActorSound(this);
 }
 
 void StreamMovieActor::readChunk(Chunk &chunk) {
diff --git a/engines/mediastation/actors/movie.h b/engines/mediastation/actors/movie.h
index bfeaef3138d..d671a4b7242 100644
--- a/engines/mediastation/actors/movie.h
+++ b/engines/mediastation/actors/movie.h
@@ -114,15 +114,21 @@ private:
 };
 
 // This is called `RT_stmvSound` in the original.
-class StreamMovieActorSound : public ChannelClient {
+class StreamMovieActorSound : public ChannelClient, public SoundClient {
 public:
+	StreamMovieActorSound(StreamMovieActor *parent) : ChannelClient(), _audioSequence(this), _parent(parent) {}
 	~StreamMovieActorSound();
 	virtual void readChunk(Chunk &chunk) override;
+	virtual void soundPlayStateChanged(SoundPlayState state, SoundStopReason why) override;
 
+	AudioSequence &getAudioSequence() { return _audioSequence; }
+
+private:
 	AudioSequence _audioSequence;
+	StreamMovieActor *_parent = nullptr;
 };
 
-class StreamMovieActor : public SpatialEntity, public ChannelClient {
+class StreamMovieActor : public SpatialEntity, public ChannelClient, public PreDisplaySyncClient {
 friend class StreamMovieActorFrames;
 friend class StreamMovieActorSound;
 
@@ -134,7 +140,10 @@ public:
 	virtual void loadIsComplete() override;
 	virtual void readParameter(Chunk &chunk, ActorHeaderSectionType paramType) override;
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
-	virtual void process() override;
+
+	virtual PreDisplaySyncState preDisplaySync() override;
+	virtual void onEvent(const ActorEvent &event) override;
+	virtual void timerEvent(const TimerEvent &event) override;
 
 	virtual void draw(DisplayContext &displayContext) override;
 	void drawLayer(DisplayContext &displayContext, uint layerId);
@@ -146,6 +155,7 @@ private:
 	uint _fullTime = 0;
 	uint _chunkCount = 0;
 	double _frameRate = 0;
+	uint _disableScreenAutoUpdateToken = 0;
 
 	bool _shouldCache = false;
 	bool _isPlaying = false;
@@ -160,7 +170,7 @@ private:
 
 	// Script method implementations.
 	void timePlay();
-	void timeStop();
+	void timeStop(bool isMovieEnd);
 
 	void setVisibility(bool visibility);
 	void updateFrameState();
diff --git a/engines/mediastation/actors/path.cpp b/engines/mediastation/actors/path.cpp
index f4996e64ba3..f110c40757b 100644
--- a/engines/mediastation/actors/path.cpp
+++ b/engines/mediastation/actors/path.cpp
@@ -71,12 +71,12 @@ ScriptValue PathActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptVa
 		stopPath();
 		break;
 
-	case kPauseMethod:
+	case kTimePauseMethod:
 		ARGCOUNTCHECK(0);
 		pausePath();
 		break;
 
-	case kResumeMethod: {
+	case kTimeResumeMethod: {
 		ARGCOUNTRANGE(0, 1);
 		bool shouldRestart = false;
 		if (args.size() == 1) {
@@ -154,26 +154,20 @@ ScriptValue PathActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptVa
 	return returnValue;
 }
 
-void PathActor::process() {
-	if (_playState == kPathPlaying) {
-		uint currentTime = g_system->getMillis();
-		if (currentTime >= _nextPathStepTime) {
-			timerEvent();
-		}
-	}
+void PathActor::onEvent(const ActorEvent &event) {
+	// The original has other logic here, but I like the way I track things better.
+	runScriptResponseIfExists(event.type);
 }
 
 void PathActor::startPath() {
-	_playState = kPathPlaying;
-	_startTime = g_system->getMillis();
-
+	_currentPoint = _startPoint;
 	if (_stepRate <= 0.0) {
 		error("[%s] %s: Got zero or negative step rate", debugName(), __func__);
 	}
-
-	_currentPoint = _startPoint;
 	_stepDurationInMilliseconds = static_cast<uint>((1.0 / _stepRate) * 1000);
-	_nextPathStepTime = _startTime + _stepDurationInMilliseconds;
+	_startTime = g_engine->getTotalPlayTime();
+	_playState = kPathPlaying;
+	scheduleNextTimerEvent();
 	_currentStep = 0;
 
 	// There is no path start script response.
@@ -181,22 +175,24 @@ void PathActor::startPath() {
 
 void PathActor::stopPath() {
 	if (_playState == kPathPlaying || _playState == kPathPaused) {
+		g_engine->getTimerService()->stopTimer(_timer);
 		_playState = kPathStopped;
-		runScriptResponseIfExists(kPathStoppedEvent);
+		ActorEvent actorEvent(_id, kPathStoppedEvent);
+		g_engine->getEventLoop()->queueEvent(actorEvent);
 	}
 }
 
 void PathActor::pausePath() {
 	if (_playState == kPathPlaying) {
 		_playState = kPathPaused;
-		_pauseTime = g_system->getMillis();
+		_pauseTime = g_engine->getTotalPlayTime();
 	}
 }
 
 void PathActor::resumePath(bool shouldRestart) {
 	if (_playState == kPathPaused) {
 		// Calculate how long we were paused, to make sure we resume at the right point.
-		uint currentTime = g_system->getMillis();
+		uint currentTime = g_engine->getTotalPlayTime();
 		uint pauseDuration = currentTime - _pauseTime;
 		_startTime += pauseDuration;
 		_playState = kPathPlaying;
@@ -213,7 +209,7 @@ double PathActor::getPercentComplete() {
 			percentComplete = static_cast<double>(_currentStep) / _totalSteps;
 		}
 	} else {
-		uint currentTime = g_system->getMillis();
+		uint currentTime = g_engine->getTotalPlayTime();
 		if (currentTime > _startTime && _duration > 0) {
 			double timeElapsed = currentTime - _startTime;
 			percentComplete = timeElapsed / _duration;
@@ -237,28 +233,39 @@ bool PathActor::step() {
 			_endPoint.x, _endPoint.y, _startPoint.x, _startPoint.y, _currentPoint.x, _currentPoint.y);
 
 		// We don't run a step event for the last step.
-		runScriptResponseIfExists(kPathStepEvent);
+		ActorEvent actorEvent(_id, kPathStepEvent);
+		g_engine->getEventLoop()->queueEvent(actorEvent);
 		return false;
 	}
 	return true;
 }
 
 void PathActor::scheduleNextTimerEvent() {
+	// Catch up if we are behind.
 	_nextPathStepTime += _stepDurationInMilliseconds;
+	uint32 currentTime = g_engine->getTotalPlayTime();
+	if (_nextPathStepTime < currentTime) {
+		_nextPathStepTime = currentTime;
+	}
+	uint32 delayUntilNextStepInMilliseconds = _nextPathStepTime - currentTime;
+	debugC(5, kDebugEvents, "[%s] %s: next step in %d ms", debugName(), __func__, delayUntilNextStepInMilliseconds);
+	g_engine->getTimerService()->startTimer(_timer, delayUntilNextStepInMilliseconds);
 }
 
-void PathActor::timerEvent() {
+void PathActor::timerEvent(const TimerEvent &event) {
 	_currentStep += 1;
 	bool finishedPlaying = step();
 	if (!finishedPlaying) {
 		scheduleNextTimerEvent();
 	} else {
+		g_engine->getTimerService()->stopTimer(_timer);
 		_playState = kPathStopped;
-		_percentComplete = 0;
 		_nextPathStepTime = 0;
 		_currentStep = 0;
 		_stepDurationInMilliseconds = 0;
-		runScriptResponseIfExists(kPathEndEvent);
+
+		ActorEvent actorEvent(_id, kPathEndEvent);
+		g_engine->getEventLoop()->queueEvent(actorEvent);
 	}
 }
 
diff --git a/engines/mediastation/actors/path.h b/engines/mediastation/actors/path.h
index 0ef45a14a51..57a8f2afe78 100644
--- a/engines/mediastation/actors/path.h
+++ b/engines/mediastation/actors/path.h
@@ -38,14 +38,14 @@ class PathActor : public Actor {
 public:
 	PathActor() : Actor(kActorTypePath) {};
 
-	virtual void process() override;
-
 	virtual void readParameter(Chunk &chunk, ActorHeaderSectionType paramType) override;
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 
+	virtual void onEvent(const ActorEvent &event) override;
+	virtual void timerEvent(const TimerEvent &event) override;
+
 private:
 	PathPlayState _playState = kPathStopped;
-	double _percentComplete = 0.0;
 	bool _useTimeForCompletion = false;
 	double _duration = 0.0;
 	double _stepRate = 0.0;
@@ -67,7 +67,6 @@ private:
 
 	double getPercentComplete();
 	bool step();
-	void timerEvent();
 	void scheduleNextTimerEvent();
 };
 
diff --git a/engines/mediastation/actors/screen.cpp b/engines/mediastation/actors/screen.cpp
index 73347fd449a..bd1faddc63e 100644
--- a/engines/mediastation/actors/screen.cpp
+++ b/engines/mediastation/actors/screen.cpp
@@ -39,4 +39,40 @@ void ScreenActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType)
 	}
 }
 
+void ScreenActor::onEvent(const ActorEvent &event) {
+	switch (event.type) {
+	case kScreenEntryEvent:
+		g_engine->getImtGod()->clearAllContextLocks();
+		runScriptResponseIfExists(event.type);
+		break;
+
+
+	case kScreenExitEvent:
+		runScriptResponseIfExists(event.type);
+		break;
+
+	case kContextLoadCompleteEvent:
+	case kContextAlreadyLoadedEvent: {
+		uint contextId = event.arg.asActorId();
+		Context *context = g_engine->getImtGod()->getContextById(contextId);
+		if (context != nullptr) {
+			runScriptResponseIfExists(event.type, event.arg);
+		}
+		break;
+	}
+
+	case kContextReleaseCompleteEvent:
+	case kContextAlreadyReleasedEvent: {
+		uint contextId = event.arg.asActorId();
+		Context *context = g_engine->getImtGod()->getContextById(contextId);
+		if (context == nullptr) {
+			runScriptResponseIfExists(event.type, event.arg);
+		}
+		break;
+	}
+
+	default:
+		Actor::onEvent(event);
+	}
+}
 } // End of namespace MediaStation
diff --git a/engines/mediastation/actors/screen.h b/engines/mediastation/actors/screen.h
index 335d78ca9cb..35dc0cc332d 100644
--- a/engines/mediastation/actors/screen.h
+++ b/engines/mediastation/actors/screen.h
@@ -36,6 +36,7 @@ public:
 	ScreenActor() : Actor(kActorTypeScreen) {};
 
 	virtual void readParameter(Chunk &chunk, ActorHeaderSectionType paramType) override;
+	virtual void onEvent(const ActorEvent &event) override;
 
 	uint _cursorResourceId = 0;
 };
diff --git a/engines/mediastation/actors/sound.cpp b/engines/mediastation/actors/sound.cpp
index e5936557604..8c8c6229e73 100644
--- a/engines/mediastation/actors/sound.cpp
+++ b/engines/mediastation/actors/sound.cpp
@@ -77,23 +77,63 @@ void SoundActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
 	}
 }
 
-void SoundActor::process() {
-	if (_playState != kSoundPlaying) {
-		return;
-	}
-
-	processTimeScriptResponses();
-	if (!_sequence.isActive()) {
-		_playState = kSoundStopped;
-		_sequence.stop();
-		runScriptResponseIfExists(kSoundEndEvent);
-	}
-}
 void SoundActor::readChunk(Chunk &chunk) {
 	_isLoadedFromChunk = true;
 	_sequence.readChunk(chunk);
 }
 
+void SoundActor::soundPlayStateChanged(SoundPlayState state, SoundStopReason stopReason) {
+	switch (state) {
+	case kSoundPlayStateStopped: {
+		_playState = kSoundPlayStateStopped;
+		g_engine->getTimerService()->stopTimer(_timer);
+
+		EventType eventType = kEventTypeInvalid;
+		switch (stopReason) {
+		case kSoundStopForFailure:
+			eventType = kSoundFailureEvent;
+			break;
+
+		case kSoundStopForEnd:
+			eventType = kSoundEndEvent;
+			break;
+
+		case kSoundStopForScriptStop:
+			eventType = kSoundStoppedEvent;
+			break;
+
+		case kSoundStopForAbort:
+			eventType = kSoundAbortEvent;
+			break;
+
+		case kSoundStopForNone:
+		default:
+			break;
+		}
+
+		ActorEvent event(_id, eventType);
+		g_engine->getEventLoop()->queueEvent(event);
+		break;
+	}
+
+	case kSoundPlayStatePlaying:
+		if (_playState == kSoundPlayStateStopped) {
+			ActorEvent event(_id, kSoundBeginEvent);
+			g_engine->getEventLoop()->queueEvent(event);
+		}
+		_playState = kSoundPlayStatePlaying;
+		break;
+
+	case kSoundPlayStateSleep:
+		_playState = kSoundPlayStatePaused;
+		break;
+
+	default:
+		// Other cases are explicitly not handled.
+		break;
+	}
+}
+
 ScriptValue SoundActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
 	ScriptValue returnValue;
 
@@ -116,12 +156,12 @@ ScriptValue SoundActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptV
 		stop();
 		break;
 
-	case kPauseMethod:
+	case kTimePauseMethod:
 		ARGCOUNTCHECK(0);
 		pause();
 		break;
 
-	case kResumeMethod: {
+	case kTimeResumeMethod: {
 		ARGCOUNTRANGE(0, 1);
 		bool shouldRestart = false;
 		if (args.size() == 1) {
@@ -132,11 +172,11 @@ ScriptValue SoundActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptV
 	}
 
 	case kIsPlayingMethod:
-		returnValue.setToBool(_playState == kSoundPlaying || _playState == kSoundPaused);
+		returnValue.setToBool(_playState == kSoundPlayStatePlaying || _playState == kSoundPlayStatePaused);
 		break;
 
 	case kIsPausedMethod:
-		returnValue.setToBool(_playState == kSoundPaused);
+		returnValue.setToBool(_playState == kSoundPlayStatePaused);
 		break;
 
 	default:
@@ -145,41 +185,81 @@ ScriptValue SoundActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptV
 	return returnValue;
 }
 
+void SoundActor::onEvent(const ActorEvent &event) {
+	switch (event.type) {
+	case kSoundEndEvent:
+		triggerRemainingTimerEvents();
+		break;
+
+	case kCachingStartedEvent:
+	case kCachingEndedEvent:
+	case kCachingFailureEvent:
+		// Caching-related events are not implemented, but they can be implemented
+		// if the original CD-ROM streaming/caching logic is reimplemented.
+		Actor::onEvent(event);
+		break;
+
+	case kSoundStoppedEvent:
+	case kSoundAbortEvent:
+	case kSoundFailureEvent:
+		// Currently, these aren't reimplemented. But if original CD-ROM streaming
+		// logic is implemented later on, some stream cleanup is needed here.
+		break;
+
+	default:
+		break;
+	}
+
+	runScriptResponseIfExists(event.type);
+}
+
+void SoundActor::timerEvent(const TimerEvent &event) {
+	Actor::processTimeScriptResponses();
+
+	// Set up the next timer wakeup.
+	g_engine->getTimerService()->stopTimer(_timer);
+	setupNextScriptResponseTimer();
+}
+
 void SoundActor::start() {
 	if (_loadIsComplete) {
-		if (_playState == kSoundPlaying || _playState == kSoundPaused) {
+		if (_playState == kSoundPlayStatePlaying || _playState == kSoundPlayStatePaused) {
 			stop();
 		}
 
 		openStream();
-		_playState = kSoundPlaying;
-		_startTime = g_system->getMillis();
+		_playState = kSoundPlayStatePlaying;
+		_startTime = g_engine->getTotalPlayTime();
 		_lastProcessedTime = 0;
-		_sequence.play();
-		runScriptResponseIfExists(kSoundBeginEvent);
+		setupNextScriptResponseTimer();
+		_sequence.start();
+
+		ActorEvent actorEvent(_id, kSoundBeginEvent);
+		g_engine->getEventLoop()->queueEvent(actorEvent);
 	} else {
 		warning("[%s] %s: Attempted to play sound before it was loaded", debugName(), __func__);
 	}
 }
 
 void SoundActor::stop() {
-	if (_playState == kSoundPlaying || _playState == kSoundPaused) {
-		_playState = kSoundStopped;
+	if (_playState == kSoundPlayStatePlaying || _playState == kSoundPlayStatePaused) {
+		_playState = kSoundPlayStateStopped;
 		_sequence.stop();
-		runScriptResponseIfExists(kSoundStoppedEvent);
+		g_engine->getTimerService()->stopTimer(_timer);
 	}
 }
 
 void SoundActor::pause() {
-	if (_playState == kSoundPlaying) {
+	if (_playState == kSoundPlayStatePlaying) {
 		_sequence.pause();
-		_playState = kSoundPaused;
+		_sequence.sleep();
 		// There don't seem to be script events to trigger in this instance.
 	}
 }
 
 void SoundActor::resume(bool restart) {
-	if (_playState == kSoundPaused) {
+	if (_playState == kSoundPlayStatePaused) {
+		_sequence.awake();
 		_sequence.resume();
 	} else if (restart) {
 		start();
diff --git a/engines/mediastation/actors/sound.h b/engines/mediastation/actors/sound.h
index df1bd2e1221..4631b2e8fe7 100644
--- a/engines/mediastation/actors/sound.h
+++ b/engines/mediastation/actors/sound.h
@@ -30,28 +30,24 @@
 
 namespace MediaStation {
 
-enum SoundPlayState {
-	kSoundStopped = 1,
-	kSoundPlaying = 2,
-	kSoundPaused = 3,
-};
-
-class SoundActor : public Actor, public ChannelClient {
+class SoundActor : public Actor, public ChannelClient, public SoundClient {
 public:
-	SoundActor() : Actor(kActorTypeSound) {};
+	SoundActor() : Actor(kActorTypeSound), _sequence(this) {};
 	~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;
 
+	virtual void onEvent(const ActorEvent &event) override;
+	virtual void timerEvent(const TimerEvent &event) override;
+	virtual void soundPlayStateChanged(SoundPlayState state, SoundStopReason why) override;
+
 private:
 	ImtStreamFeed *_streamFeed = nullptr;
 	bool _isLoadedFromChunk = false;
 	bool _discardAfterUse = false;
-	SoundPlayState _playState = kSoundStopped;
+	SoundPlayState _playState = kSoundPlayStateStopped;
 	AudioSequence _sequence;
 
 	void start();
diff --git a/engines/mediastation/actors/sprite.cpp b/engines/mediastation/actors/sprite.cpp
index 487d457eb45..86f4b86c3b5 100644
--- a/engines/mediastation/actors/sprite.cpp
+++ b/engines/mediastation/actors/sprite.cpp
@@ -205,6 +205,17 @@ ScriptValue SpriteMovieActor::callMethod(BuiltInMethod methodId, Common::Array<S
 	return returnValue;
 }
 
+void SpriteMovieActor::onEvent(const ActorEvent &event) {
+	switch (event.type) {
+	case kSpriteMovieEndEvent:
+		runScriptResponseIfExists(kSpriteMovieEndEvent, event.arg);
+		break;
+
+	default:
+		Actor::onEvent(event);
+	}
+}
+
 bool SpriteMovieActor::activateNextFrame() {
 	bool clipMovesForward = _activeClip.firstFrameIndex <= _activeClip.lastFrameIndex;
 	if (clipMovesForward) {
@@ -272,9 +283,9 @@ void SpriteMovieActor::setVisibility(bool visibility) {
 
 void SpriteMovieActor::play() {
 	_isPlaying = true;
-	_startTime = g_system->getMillis();
+	_startTime = g_engine->getTotalPlayTime();
 	_lastProcessedTime = 0;
-	_nextFrameTime = 0;
+	_nextFrameTime = _startTime;
 
 	scheduleNextFrame();
 	debugC(3, kDebugSpriteMovie, "[%s] %s", debugName(), __func__);
@@ -283,6 +294,7 @@ void SpriteMovieActor::play() {
 void SpriteMovieActor::stop() {
 	_nextFrameTime = 0;
 	_isPlaying = false;
+	g_engine->getTimerService()->stopTimer(_timer);
 	debugC(3, kDebugSpriteMovie, "[%s] %s", debugName(), __func__);
 }
 
@@ -322,11 +334,6 @@ void SpriteMovieActor::setCurrentFrameToFinal() {
 	}
 }
 
-void SpriteMovieActor::process() {
-	updateFrameState();
-	// Sprites don't have time script responses, separate timers do time handling.
-}
-
 void SpriteMovieActor::readChunk(Chunk &chunk) {
 	// Read one frame from the sprite.
 	ImageInfo imageInfo(chunk);
@@ -374,31 +381,19 @@ void SpriteMovieActor::scheduleNextFrame() {
 }
 
 void SpriteMovieActor::scheduleNextTimerEvent() {
-	uint frameDuration = 1000 / _frameRate;
-	_nextFrameTime += frameDuration;
-	debugC(3, kDebugSpriteMovie, "[%s] %s", debugName(), __func__);
+	uint32 frameDurationInMilliseconds = 1000 / _frameRate;
+	// Catch up if we are behind.
+	_nextFrameTime += frameDurationInMilliseconds;
+	uint32 currentTime = g_engine->getTotalPlayTime();
+	if (_nextFrameTime < currentTime) {
+		_nextFrameTime = currentTime;
+	}
+	uint32 delayUntilNextFrameInMilliseconds = _nextFrameTime - currentTime;
+	debugC(3, kDebugSpriteMovie, "[%s] %s: next frame in %d ms", debugName(), __func__, delayUntilNextFrameInMilliseconds);
+	g_engine->getTimerService()->startTimer(_timer, delayUntilNextFrameInMilliseconds);
 }
 
-void SpriteMovieActor::updateFrameState() {
-	if (!_isPlaying) {
-		return;
-	}
-
-	uint currentTime = g_system->getMillis() - _startTime;
-	bool drawNextFrame = currentTime >= _nextFrameTime;
-	debugC(3, kDebugSpriteMovie, "[%s] %s: nextFrameTime: %d; startTime: %d, currentTime: %d",
-		debugName(), __func__, _nextFrameTime, _startTime, currentTime);
-	if (drawNextFrame) {
-		timerEvent();
-	}
-}
-
-void SpriteMovieActor::timerEvent() {
-	if (!_isPlaying) {
-		warning("[%s] %s: Not playing", debugName(), __func__);
-		return;
-	}
-
+void SpriteMovieActor::timerEvent(const TimerEvent &event) {
 	bool moreFramesToShow = activateNextFrame();
 	if (moreFramesToShow) {
 		postMovieEndEventIfNecessary();
@@ -420,7 +415,8 @@ void SpriteMovieActor::postMovieEndEventIfNecessary() {
 
 	ScriptValue value;
 	value.setToParamToken(_activeClip.id);
-	runScriptResponseIfExists(kSpriteMovieEndEvent, value);
+	ActorEvent actorEvent(_id, kSpriteMovieEndEvent, value);
+	g_engine->getEventLoop()->queueEvent(actorEvent);
 }
 
 void SpriteMovieActor::draw(DisplayContext &displayContext) {
diff --git a/engines/mediastation/actors/sprite.h b/engines/mediastation/actors/sprite.h
index 93daeb20a2a..a5799874d7c 100644
--- a/engines/mediastation/actors/sprite.h
+++ b/engines/mediastation/actors/sprite.h
@@ -68,14 +68,15 @@ public:
 	SpriteMovieActor() : SpatialEntity(kActorTypeSprite) {};
 	~SpriteMovieActor();
 
-	virtual void process() override;
 	virtual void draw(DisplayContext &displayContext) override;
 
+	virtual void readChunk(Chunk &chunk) override;
 	virtual void readParameter(Chunk &chunk, ActorHeaderSectionType paramType) override;
 	virtual void loadIsComplete() override;
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 
-	virtual void readChunk(Chunk &chunk) override;
+	virtual void onEvent(const ActorEvent &event) override;
+	virtual void timerEvent(const TimerEvent &event) override;
 
 private:
 	const uint DEFAULT_FORWARD_CLIP_ID = 0x4B0;
@@ -107,9 +108,6 @@ private:
 	void scheduleNextTimerEvent();
 	void postMovieEndEventIfNecessary();
 	void setVisibility(bool visibility);
-
-	void updateFrameState();
-	void timerEvent();
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/actors/stage.cpp b/engines/mediastation/actors/stage.cpp
index 5a80e1b901a..3b10b0f28b5 100644
--- a/engines/mediastation/actors/stage.cpp
+++ b/engines/mediastation/actors/stage.cpp
@@ -755,16 +755,16 @@ uint16 RootStage::findActorToAcceptMouseEvents(
 	return result;
 }
 
-void RootStage::mouseEnteredEvent(const Common::Event &event) {
+void RootStage::mouseEnteredEvent(const MouseEvent &event) {
 	_isMouseInside = true;
 	g_engine->getCursorManager()->unsetTemporary();
 }
 
-void RootStage::mouseExitedEvent(const Common::Event &event) {
+void RootStage::mouseExitedEvent(const MouseEvent &event) {
 	_isMouseInside = false;
 }
 
-void RootStage::mouseOutOfFocusEvent(const Common::Event &event) {
+void RootStage::mouseOutOfFocusEvent(const MouseEvent &event) {
 	_isMouseInside = true;
 	g_engine->getCursorManager()->unsetTemporary();
 }
@@ -811,59 +811,86 @@ void StageDirector::clearDirtyRegion() {
 	_rootStage->_dirtyRegion._bounds = Common::Rect(0, 0, 0, 0);
 }
 
-void StageDirector::handleKeyboardEvent(const Common::Event &event) {
+void StageDirector::handleMouseEvent(const MouseEvent &event) {
+	switch (event.type) {
+	case kMouseDownEvent:
+		handleMouseDownEvent(event);
+		break;
+
+	case kMouseUpEvent:
+		handleMouseUpEvent(event);
+		break;
+
+	case kMouseMovedEvent:
+		handleMouseMovedEvent(event);
+		break;
+
+	case kMouseEnterExitEvent:
+		handleMouseEnterExitEvent(event);
+		break;
+
+	case kMouseOutOfFocusEvent:
+		handleMouseOutOfFocusEvent(event);
+		break;
+
+	default:
+		warning("%s: Unhandled mouse event: %s", __func__, event.debugString().c_str());
+	}
+}
+
+void StageDirector::handleKeyboardEvent(const KeyboardEvent &event) {
 	MouseActorState state;
-	uint16 flags = _rootStage->findActorToAcceptKeyboardEvents(event.kbd.ascii, kKeyDownFlag, state);
+	uint16 flags = _rootStage->findActorToAcceptKeyboardEvents(event.keyCode, kKeyDownFlag, state);
 	if (flags & kKeyDownFlag) {
 		debugC(5, kDebugEvents, "%s: Dispatching to %s from root stage", __func__, state.keyDown->debugName());
 		state.keyDown->keyboardEvent(event);
 	}
 }
 
-void StageDirector::handleMouseDownEvent(const Common::Event &event) {
+void StageDirector::handleMouseDownEvent(const MouseEvent &event) {
 	MouseActorState state;
-	uint16 flags = _rootStage->findActorToAcceptMouseEvents(event.mouse, kMouseDownFlag, state, false);
+	uint16 flags = _rootStage->findActorToAcceptMouseEvents(event.position, kMouseDownFlag, state, false);
 	if (flags & kMouseDownFlag) {
 		debugC(5, kDebugEvents, "%s: Dispatching to %s from root stage (mousePos: %d, %d) (bounds: %d, %d, %d, %d)",
-			__func__, state.mouseDown->debugName(), event.mouse.x, event.mouse.y, PRINT_RECT(state.mouseDown->getBbox()));
+			__func__, state.mouseDown->debugName(), event.position.x, event.position.y, PRINT_RECT(state.mouseDown->getBbox()));
 		state.mouseDown->mouseDownEvent(event);
 	}
 }
 
-void StageDirector::handleMouseUpEvent(const Common::Event &event) {
+void StageDirector::handleMouseUpEvent(const MouseEvent &event) {
 	MouseActorState state;
-	uint16 flags = _rootStage->findActorToAcceptMouseEvents(event.mouse, kMouseUpFlag, state, false);
+	uint16 flags = _rootStage->findActorToAcceptMouseEvents(event.position, kMouseUpFlag, state, false);
 	if (flags & kMouseUpFlag) {
 		debugC(5, kDebugEvents, "%s: Dispatching to %s from root stage (mousePos: %d, %d) (bounds: %d, %d, %d, %d)",
-			__func__, state.mouseUp->debugName(), event.mouse.x, event.mouse.y, PRINT_RECT(state.mouseUp->getBbox()));
+			__func__, state.mouseUp->debugName(), event.position.x, event.position.y, PRINT_RECT(state.mouseUp->getBbox()));
 		state.mouseUp->mouseUpEvent(event);
 	}
 }
 
-void StageDirector::handleMouseMovedEvent(const Common::Event &event) {
+void StageDirector::handleMouseMovedEvent(const MouseEvent &event) {
 	MouseActorState state;
 	uint16 flags = _rootStage->findActorToAcceptMouseEvents(
-		event.mouse,
+		event.position,
 		kMouseEnterFlag | kMouseExitFlag | kMouseMovedFlag,
 		state, false);
 
 	sendMouseEnterExitEvent(flags, state, event);
 	if (flags & kMouseMovedFlag) {
 		debugC(5, kDebugEvents, "%s: Dispatching to %s (mousePos: %d, %d) (bounds: %d, %d, %d, %d)",
-			__func__, state.mouseMoved->debugName(), event.mouse.x, event.mouse.y, PRINT_RECT(state.mouseMoved->getBbox()));
+			__func__, state.mouseMoved->debugName(), event.position.x, event.position.y, PRINT_RECT(state.mouseMoved->getBbox()));
 		state.mouseMoved->mouseMovedEvent(event);
 	}
 }
 
-void StageDirector::handleMouseEnterExitEvent(const Common::Event &event) {
+void StageDirector::handleMouseEnterExitEvent(const MouseEvent &event) {
 	MouseActorState state;
-	uint16 flags = _rootStage->findActorToAcceptMouseEvents(event.mouse, kMouseEnterFlag | kMouseExitFlag, state, false);
+	uint16 flags = _rootStage->findActorToAcceptMouseEvents(event.position, kMouseEnterFlag | kMouseExitFlag, state, false);
 	sendMouseEnterExitEvent(flags, state, event);
 }
 
-void StageDirector::handleMouseOutOfFocusEvent(const Common::Event &event) {
+void StageDirector::handleMouseOutOfFocusEvent(const MouseEvent &event) {
 	MouseActorState state;
-	uint16 flags = _rootStage->findActorToAcceptMouseEvents(event.mouse, kMouseExitFlag | kMouseOutOfFocusFlag, state, false);
+	uint16 flags = _rootStage->findActorToAcceptMouseEvents(event.position, kMouseExitFlag | kMouseOutOfFocusFlag, state, false);
 
 	if (flags & kMouseExitFlag) {
 		debugC(5, kDebugEvents, "%s: Dispatching mouse enter to %s", __func__, state.mouseExit->debugName());
@@ -876,7 +903,7 @@ void StageDirector::handleMouseOutOfFocusEvent(const Common::Event &event) {
 	}
 }
 
-void StageDirector::sendMouseEnterExitEvent(uint16 flags, MouseActorState &state, const Common::Event &event) {
+void StageDirector::sendMouseEnterExitEvent(uint16 flags, MouseActorState &state, const MouseEvent &event) {
 	if (state.mouseEnter != state.mouseExit) {
 		if (flags & kMouseExitFlag) {
 			debugC(5, kDebugEvents, "%s: Dispatching mouse exit to %s", __func__, state.mouseExit->debugName());
diff --git a/engines/mediastation/actors/stage.h b/engines/mediastation/actors/stage.h
index 84439844d16..33b3a7ffd5f 100644
--- a/engines/mediastation/actors/stage.h
+++ b/engines/mediastation/actors/stage.h
@@ -25,6 +25,7 @@
 #include "common/events.h"
 
 #include "mediastation/actor.h"
+#include "mediastation/events.h"
 #include "mediastation/graphics.h"
 #include "mediastation/mediascript/scriptvalue.h"
 #include "mediastation/mediascript/scriptconstants.h"
@@ -154,9 +155,9 @@ public:
 	virtual void invalidateRect(const Common::Rect &rect) override;
 	virtual void deleteChildrenFromContextId(uint contextId);
 
-	virtual void mouseEnteredEvent(const Common::Event &event) override;
-	virtual void mouseExitedEvent(const Common::Event &event) override;
-	virtual void mouseOutOfFocusEvent(const Common::Event &event) override;
+	virtual void mouseEnteredEvent(const MouseEvent &event) override;
+	virtual void mouseExitedEvent(const MouseEvent &event) override;
+	virtual void mouseOutOfFocusEvent(const MouseEvent &event) override;
 
 	void drawAll(DisplayContext &displayContext);
 	void drawDirtyRegion(DisplayContext &displayContext);
@@ -179,13 +180,14 @@ public:
 	void drawDirtyRegion();
 	void clearDirtyRegion();
 
-	void handleKeyboardEvent(const Common::Event &event);
-	void handleMouseDownEvent(const Common::Event &event);
-	void handleMouseUpEvent(const Common::Event &event);
-	void handleMouseMovedEvent(const Common::Event &event);
-	void handleMouseEnterExitEvent(const Common::Event &event);
-	void handleMouseOutOfFocusEvent(const Common::Event &event);
-	void sendMouseEnterExitEvent(uint16 flags, MouseActorState &state, const Common::Event &event);
+	void handleMouseEvent(const MouseEvent &event);
+	void handleKeyboardEvent(const KeyboardEvent &event);
+	void handleMouseDownEvent(const MouseEvent &event);
+	void handleMouseUpEvent(const MouseEvent &event);
+	void handleMouseMovedEvent(const MouseEvent &event);
+	void handleMouseEnterExitEvent(const MouseEvent &event);
+	void handleMouseOutOfFocusEvent(const MouseEvent &event);
+	void sendMouseEnterExitEvent(uint16 flags, MouseActorState &state, const MouseEvent &event);
 
 private:
 	RootStage *_rootStage = nullptr;
diff --git a/engines/mediastation/actors/text.cpp b/engines/mediastation/actors/text.cpp
index 508f3f4f9c6..4a91b25257a 100644
--- a/engines/mediastation/actors/text.cpp
+++ b/engines/mediastation/actors/text.cpp
@@ -358,7 +358,7 @@ uint16 TextActor::findActorToAcceptKeyboardEvents(uint16 charCode, uint16 eventM
 	return result;
 }
 
-void TextActor::keyboardEvent(const Common::Event &event) {
+void TextActor::keyboardEvent(const KeyboardEvent &event) {
 	// TODO: Implement this once we have a title that actually uses it.
 	warning("STUB: %s", __func__);
 }
diff --git a/engines/mediastation/actors/text.h b/engines/mediastation/actors/text.h
index 74893a8f6b6..f0654a18909 100644
--- a/engines/mediastation/actors/text.h
+++ b/engines/mediastation/actors/text.h
@@ -53,7 +53,7 @@ public:
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 	virtual void draw(DisplayContext &displayContext) override;
 	virtual uint16 findActorToAcceptKeyboardEvents(uint16 charCode, uint16 eventMask, MouseActorState &state) override;
-	virtual void keyboardEvent(const Common::Event &event) override;
+	virtual void keyboardEvent(const KeyboardEvent &event) override;
 
 private:
 	static const uint CURSOR_CHAR_ID = 0x104;
diff --git a/engines/mediastation/actors/timer.cpp b/engines/mediastation/actors/timer.cpp
index 1cf617adf3b..18f96009ec7 100644
--- a/engines/mediastation/actors/timer.cpp
+++ b/engines/mediastation/actors/timer.cpp
@@ -30,66 +30,103 @@ ScriptValue TimerActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptV
 	ScriptValue returnValue;
 
 	switch (methodId) {
-	case kTimePlayMethod: {
+	case kTimePlayMethod:
 		ARGCOUNTCHECK(0);
-		timePlay();
+		start();
 		break;
-	}
 
-	case kTimeStopMethod: {
+	case kTimeStopMethod:
 		ARGCOUNTCHECK(0);
-		timeStop();
+		stop();
 		break;
-	}
 
-	case kIsPlayingMethod: {
+	case kTimePauseMethod:
 		ARGCOUNTCHECK(0);
-		returnValue.setToBool(_isPlaying);
+		pause();
+		break;
+
+	case kTimeResumeMethod: {
+		ARGCOUNTRANGE(0, 1);
+		bool shouldRestart = false;
+		if (args.size() == 1) {
+			shouldRestart = args[0].asBool();
+		}
+		resume(shouldRestart);
 		break;
 	}
 
+	case kIsPlayingMethod:
+		ARGCOUNTCHECK(0);
+		returnValue.setToBool(_startTime > 0);
+		break;
+
 	default:
 		returnValue = Actor::callMethod(methodId, args);
 	}
 	return returnValue;
 }
 
-void TimerActor::timePlay() {
-	_isPlaying = true;
-	_startTime = g_system->getMillis();
+void TimerActor::start() {
+	stop();
+	_startTime = g_engine->getTotalPlayTime();
 	_lastProcessedTime = 0;
+	setupNextScriptResponseTimer();
+}
 
-	// Get the duration of the timer.
-	// TODO: Is there a better way to find out what the max time is? Do we have to look
-	// through each of the timer script responses to figure it out?
-	_duration = 0;
-	const Common::Array<ScriptResponse *> &timeResponses = _scriptResponses.getValOrDefault(kTimerEvent);
-	for (ScriptResponse *timeEvent : timeResponses) {
-		// Indeed float, not time.
-		double timeEventInFractionalSeconds = timeEvent->_argumentValue.asFloat();
-		uint timeEventInMilliseconds = timeEventInFractionalSeconds * 1000;
-		if (timeEventInMilliseconds > _duration) {
-			_duration = timeEventInMilliseconds;
-		}
-	}
+void TimerActor::stop() {
+	g_engine->getTimerService()->stopTimer(_timer);
+	_pauseStartTime = 0;
+	_startTime = 0;
+}
 
-	debugC(5, kDebugScript, "[%s] %s: Now playing for %d ms", debugName(), __func__, _duration);
+void TimerActor::pause() {
+	bool timerIsRunningAndNotPaused = _startTime > 0 && _pauseStartTime == 0;
+	if (timerIsRunningAndNotPaused) {
+		_pauseStartTime = g_engine->getTotalPlayTime();
+		g_engine->getTimerService()->stopTimer(_timer);
+	}
 }
 
-void TimerActor::timeStop() {
-	if (!_isPlaying) {
+void TimerActor::resume(bool shouldRestart) {
+	// Resume a paused timer by compensating for the pause duration.
+	bool isTimerRunningAndPaused = (_startTime > 0 && _pauseStartTime > 0);
+	if (isTimerRunningAndPaused) {
+		uint32 currentTime = g_engine->getTotalPlayTime();
+		uint32 pauseDuration = currentTime - _pauseStartTime;
+		_startTime += pauseDuration;
+		_pauseStartTime = 0;
+		setupNextScriptResponseTimer();
 		return;
 	}
 
-	_isPlaying = false;
-	_startTime = 0;
-	_lastProcessedTime = 0;
+	// Restart a timer that was stopped while paused.
+	bool shouldRestartStoppedTimer = (shouldRestart && _startTime == 0 && _pauseStartTime > 0);
+	if (shouldRestartStoppedTimer) {
+		start();
+	}
 }
 
-void TimerActor::process() {
-	if (_isPlaying) {
-		processTimeScriptResponses();
+void TimerActor::timerEvent(const TimerEvent &event) {
+	// The timer actor is subtly different from other actors that can have timer events called on them,
+	// which is why the default call to process timer events doesn't work here.
+	ScriptResponse *nextTimeScriptResponse = findNextTimeScriptResponseAfter(_lastProcessedTime);
+	double eventTimeInSeconds = nextTimeScriptResponse->_argumentValue.asFloat();
+	uint32 eventTimeInMilliseconds = eventTimeInSeconds * 1000;
+	// Increment by 1 to prevent re-triggering the same event. This works because in the original,
+	// timer events are at least 10 ms apart anyway.
+	_lastProcessedTime = eventTimeInMilliseconds + 1;
+
+	// Unlike the other timer handlers, schedule the next script response FIRST,
+	// before executing the current one. This seems to be because any time event
+	// can re-start the timer.
+	g_engine->getTimerService()->stopTimer(_timer);
+	if (!setupNextScriptResponseTimer()) {
+		// No more events, stop the timer.
+		stop();
 	}
+
+	// Actually execute the script response we found.
+	nextTimeScriptResponse->execute(_id);
 }
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/actors/timer.h b/engines/mediastation/actors/timer.h
index 49a38c12fab..ee063644f03 100644
--- a/engines/mediastation/actors/timer.h
+++ b/engines/mediastation/actors/timer.h
@@ -33,13 +33,15 @@ public:
 	TimerActor() : Actor(kActorTypeTimer) {};
 
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
-	virtual void process() override;
+	virtual void timerEvent(const TimerEvent &event) override;
 
 private:
-	bool _isPlaying = false;
+	uint32 _pauseStartTime = 0;
 
-	void timePlay();
-	void timeStop();
+	void start();
+	void stop();
+	void pause();
+	void resume(bool shouldRestart);
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/audio.cpp b/engines/mediastation/audio.cpp
index a28819e129c..23abb4fe466 100644
--- a/engines/mediastation/audio.cpp
+++ b/engines/mediastation/audio.cpp
@@ -29,7 +29,7 @@
 namespace MediaStation {
 
 AudioSequence::~AudioSequence() {
-	g_engine->_mixer->stopHandle(_handle);
+	stop();
 
 	for (Audio::SeekableAudioStream *stream : _streams) {
 		delete stream;
@@ -37,7 +37,14 @@ AudioSequence::~AudioSequence() {
 	_streams.clear();
 }
 
-void AudioSequence::play() {
+void AudioSequence::start() {
+	if (_state != kSoundPlayStateStopped) {
+		return;
+	}
+
+	g_engine->registerAudioSequence(this);
+	playStateChanged(kSoundPlayStatePlaying);
+
 	_handle = Audio::SoundHandle();
 	if (!_streams.empty()) {
 		Audio::QueuingAudioStream *audio = Audio::makeQueuingAudioStream(22050, false);
@@ -51,16 +58,48 @@ void AudioSequence::play() {
 }
 
 void AudioSequence::pause() {
+	playStateChanged(kSoundPlayStatePaused);
 	g_engine->_mixer->pauseHandle(_handle, true);
 }
 
 void AudioSequence::resume() {
+	playStateChanged(kSoundPlayStatePlaying);
 	g_engine->_mixer->pauseHandle(_handle, false);
 }
 
 void AudioSequence::stop() {
 	g_engine->_mixer->stopHandle(_handle);
 	_handle = Audio::SoundHandle();
+	g_engine->unregisterAudioSequence(this);
+	playStateChanged(kSoundPlayStateStopped, kSoundStopForScriptStop);
+}
+
+void AudioSequence::sleep() {
+	g_engine->unregisterAudioSequence(this);
+	playStateChanged(kSoundPlayStatePaused);
+}
+
+void AudioSequence::awake() {
+	// Not much happens here because the original CD-ROM streaming/caching
+	// logic has not been reimplemented.
+	playStateChanged(kSoundPlayStateAwake);
+}
+
+void AudioSequence::service() {
+	bool soundActuallyPlaying = g_engine->_mixer->isSoundHandleActive(_handle);
+	if (_state == kSoundPlayStatePlaying && !soundActuallyPlaying) {
+		makeSoundIdle(kSoundStopForEnd);
+	}
+}
+
+void AudioSequence::makeSoundIdle(SoundStopReason stopReason) {
+	g_engine->unregisterAudioSequence(this);
+	playStateChanged(kSoundPlayStateStopped, stopReason);
+}
+
+void AudioSequence::playStateChanged(SoundPlayState state, SoundStopReason why) {
+	_state = state;
+	_client->soundPlayStateChanged(state, why);
 }
 
 void AudioSequence::readParameters(Chunk &chunk) {
@@ -88,8 +127,4 @@ void AudioSequence::readChunk(Chunk &chunk) {
 	_streams.push_back(stream);
 }
 
-bool AudioSequence::isActive() {
-	return g_engine->_mixer->isSoundHandleActive(_handle);
-}
-
 } // End of namespace MediaStation
diff --git a/engines/mediastation/audio.h b/engines/mediastation/audio.h
index 9b064cd1e7e..f2f63de8c5f 100644
--- a/engines/mediastation/audio.h
+++ b/engines/mediastation/audio.h
@@ -30,19 +30,47 @@
 
 namespace MediaStation {
 
+enum SoundPlayState {
+	kSoundPlayStateInvalid = 0,
+	kSoundPlayStateStopped = 1,
+	kSoundPlayStatePlaying = 2,
+	kSoundPlayStatePaused = 3,
+	kSoundPlayStateSleep = 4,
+	kSoundPlayStateAwake = 5
+};
+
+enum SoundStopReason {
+	kSoundStopForNone = 0,
+	kSoundStopForFailure = 1,
+	kSoundStopForEnd = 2,
+	kSoundStopForScriptStop = 3,
+	kSoundStopForAbort = 4
+};
+
+class SoundClient {
+public:
+	virtual ~SoundClient() {}
+	virtual void soundPlayStateChanged(SoundPlayState state, SoundStopReason why) = 0;
+};
+
 class AudioSequence {
 public:
-	AudioSequence() {};
+	AudioSequence(SoundClient *client) : _client(client) {};
 	~AudioSequence();
 
-	void play();
+	void start();
 	void pause();
-	void resume(); // Unpause
+	void resume();
 	void stop();
+	void sleep();
+	void awake();
+
+	void service();
+	void makeSoundIdle(SoundStopReason stopReason = kSoundStopForNone);
+	void playStateChanged(SoundPlayState state, SoundStopReason why = kSoundStopForNone);
 
 	void readParameters(Chunk &chunk);
 	void readChunk(Chunk &chunk);
-	bool isActive();
 	bool isEmpty() { return _streams.empty(); }
 
 	uint _rate = 0;
@@ -53,6 +81,8 @@ public:
 private:
 	Common::Array<Audio::SeekableAudioStream *> _streams;
 	Audio::SoundHandle _handle;
+	SoundClient *_client = nullptr;
+	SoundPlayState _state = kSoundPlayStateStopped;
 };
 
 } // End of namespace MediaStation
diff --git a/engines/mediastation/clients.cpp b/engines/mediastation/clients.cpp
index 77cfa632458..af0238e8a24 100644
--- a/engines/mediastation/clients.cpp
+++ b/engines/mediastation/clients.cpp
@@ -19,6 +19,8 @@
  *
  */
 
+#include "common/config-manager.h"
+
 #include "mediastation/actors/screen.h"
 #include "mediastation/debugchannels.h"
 #include "mediastation/clients.h"
@@ -104,13 +106,16 @@ void Document::readContextLoadComplete(Chunk &chunk) {
 	}
 }
 
-void Document::beginTitle(uint overriddenEntryPointScreenId) {
+void Document::beginTitle() {
 	_entryPointStreamId = MediaStationEngine::BOOT_STREAM_ID;
-	if (overriddenEntryPointScreenId != 0) {
-		// This lets us override the default entry screen for development purposes.
-		_entryPointScreenId = overriddenEntryPointScreenId;
+	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.
+		_entryPointScreenId = ConfMan.get("entry_context").asUint64();
 		_entryPointScreenIdWasOverridden = true;
+		warning("%s: Starting at user-requested context %d", __func__, _entryPointScreenId);
 	}
+
 	startFeed(_entryPointStreamId);
 }
 
@@ -130,10 +135,10 @@ void Document::startContextLoad(uint contextId) {
 		}
 	} else {
 		if (_currentScreenActorId != 0 && contextId != _loadingContextId) {
-			Actor *currentScreen = g_engine->getImtGod()->getActorById(_currentScreenActorId);
 			ScriptValue arg;
 			arg.setToActorId(contextId);
-			currentScreen->runScriptResponseIfExists(kContextLoadCompleteEvent2, arg);
+			ActorEvent event(_currentScreenActorId, kContextAlreadyLoadedEvent, arg);
+			g_engine->getEventLoop()->queueEvent(event);
 		}
 
 		if (_loadingContextId == 0) {
@@ -152,10 +157,26 @@ bool Document::isContextLoadInProgress(uint contextId) {
 	}
 }
 
-void Document::branchToScreen() {
+void Document::branchToScreen(uint screenId, bool disableScreenAutoUpdate) {
+	// 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;
+		screenId = _entryPointScreenId;
+	}
+
 	if (_loadingScreenActorId == 0) {
-		_loadingScreenActorId = _requestedScreenBranchId;
-		_requestedScreenBranchId = 0;
+		_loadingScreenActorId = screenId;
+
+		if (disableScreenAutoUpdate) {
+			_disabledScreenAutoUpdateToken = g_engine->getDisplayUpdateManager()->disableAutoUpdate();
+		}
+
 		uint contextId = contextIdForScreenActorId(_loadingScreenActorId);
 		if (contextId == 0) {
 			error("%s: Screen %d doesn't have a context in current title", __func__, _loadingScreenActorId);
@@ -171,28 +192,6 @@ void Document::branchToScreen() {
 	}
 }
 
-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->getImtGod()->contextIsLocked(contextId)) {
-		_requestedContextReleaseId.push_back(contextId);
-	}
-}
-
 void Document::streamDidClose(uint streamId) {
 	bool currentStreamIsTargetStream = _currentStreamFeed != nullptr && streamId == _currentStreamFeed->_id;
 	if (!currentStreamIsTargetStream) {
@@ -207,8 +206,7 @@ void Document::streamDidFinish(uint streamId) {
 	if (currentStreamIsTargetStream) {
 		stopFeed();
 		if (streamId == _entryPointStreamId) {
-			_requestedScreenBranchId = _entryPointScreenId;
-			branchToScreen();
+			branchToScreen(_entryPointScreenId, false);
 		} else {
 			checkQueuedContextLoads();
 		}
@@ -219,31 +217,21 @@ void Document::contextLoadDidComplete() {
 	if (_currentScreenActorId != 0) {
 		ScriptValue arg;
 		arg.setToActorId(_loadingContextId);
-		Actor *currentScreen = g_engine->getImtGod()->getActorById(_currentScreenActorId);
-		if (currentScreen != nullptr) {
-			currentScreen->runScriptResponseIfExists(kContextLoadCompleteEvent, arg);
-		}
+		ActorEvent event(_currentScreenActorId, kContextLoadCompleteEvent, arg);
+		g_engine->getEventLoop()->queueEvent(event);
 	}
 	_loadingContextId = 0;
 }
 
 void Document::screenLoadDidComplete() {
 	_currentScreenActorId = _loadingScreenActorId;
-	Actor *currentScreen = g_engine->getImtGod()->getActorById(_loadingScreenActorId);
-	currentScreen->runScriptResponseIfExists(kScreenEntryEvent);
+	ActorEvent event(_currentScreenActorId, kScreenEntryEvent);
+	g_engine->getEventLoop()->queueEvent(event);
 	_loadingScreenActorId = 0;
-}
-
-void Document::process() {
-	if (!_requestedContextReleaseId.empty()) {
-		for (uint contextId : _requestedContextReleaseId) {
-			g_engine->getImtGod()->destroyContext(contextId);
-		}
-		_requestedContextReleaseId.clear();
-	}
 
-	if (_requestedScreenBranchId != 0) {
-		branchToScreen();
+	if (_disabledScreenAutoUpdateToken != 0) {
+		g_engine->getDisplayUpdateManager()->enableAutoUpdate(_disabledScreenAutoUpdateToken);
+		_disabledScreenAutoUpdateToken = 0;
 	}
 }
 
@@ -251,10 +239,16 @@ void Document::blowAwayCurrentScreen() {
 	if (_currentScreenActorId != 0) {
 		uint contextId = contextIdForScreenActorId(_currentScreenActorId);
 		if (contextId != 0) {
-			Actor *currentScreen = g_engine->getImtGod()->getActorById(_currentScreenActorId);
-			currentScreen->runScriptResponseIfExists(kScreenExitEvent);
+			ScreenActor *screenActor = static_cast<ScreenActor *>(g_engine->getImtGod()->getActorByIdAndType(_currentScreenActorId, kActorTypeScreen));
+			if (screenActor != nullptr) {
+				ActorEvent event(_currentScreenActorId, kScreenExitEvent);
+				screenActor->onEvent(event);
+			}
 			g_engine->getImtGod()->destroyContext(contextId);
+			// There is a contextWasDestroyed call in here, but it just
+			// does OS-specific memory management which we don't need to replicate.
 		}
+		_currentScreenActorId = 0;
 	}
 }
 
@@ -285,6 +279,7 @@ void Document::preloadParentContexts(uint contextId) {
 				debugC(5, kDebugLoading, "%s: Loading parent context %d", __func__, parentContextId);
 				addToContextLoadQueue(parentContextId);
 			}
+			g_engine->getImtGod()->lockContext(parentContextId);
 		}
 	}
 }
@@ -297,6 +292,15 @@ void Document::addToContextLoadQueue(uint contextId) {
 	}
 }
 
+void Document::removeFromContextLoadQueue(uint contextId) {
+	for (uint i = 0; i < _contextLoadQueue.size(); i++) {
+		if (_contextLoadQueue[i] == contextId) {
+			_contextLoadQueue.remove_at(i);
+			return;
+		}
+	}
+}
+
 bool Document::isContextLoadQueued(uint contextId) {
 	for (uint queuedContextId : _contextLoadQueue) {
 		if (queuedContextId == contextId) {
@@ -314,4 +318,20 @@ void Document::checkQueuedContextLoads() {
 	}
 }
 
+void Document::contextReleaseComplete(uint contextId) {
+	if (_currentScreenActorId != 0) {
+		ScriptValue arg;
+		arg.setToActorId(contextId);
+		ActorEvent actorEvent(_currentScreenActorId, kContextReleaseCompleteEvent, arg);
+		g_engine->getEventLoop()->queueEvent(actorEvent);
+	}
+}
+
+void Document::contextAlreadyReleased(uint contextId) {
+	ScriptValue arg;
+	arg.setToActorId(contextId);
+	ActorEvent actorEvent(_currentScreenActorId, kContextAlreadyReleasedEvent, arg);
+	g_engine->getEventLoop()->queueEvent(actorEvent);
+}
+
 } // End of namespace MediaStation
diff --git a/engines/mediastation/clients.h b/engines/mediastation/clients.h
index ad19fe84295..5b00f2c53c0 100644
--- a/engines/mediastation/clients.h
+++ b/engines/mediastation/clients.h
@@ -59,12 +59,10 @@ public:
 	void readStartupInformation(Chunk &chunk);
 	void readContextLoadComplete(Chunk &chunk);
 
-	void beginTitle(uint overriddenEntryPointScreenId = 0);
+	void beginTitle();
 	void startContextLoad(uint contextId);
 	bool isContextLoadInProgress(uint contextId);
-	void branchToScreen();
-	void scheduleScreenBranch(uint screenActorId);
-	void scheduleContextRelease(uint contextId);
+	void branchToScreen(uint screenId, bool disableScreenAutoUpdate);
 
 	void streamDidClose(uint streamId);
 	void streamDidFinish(uint streamId);
@@ -73,20 +71,23 @@ public:
 	void streamDidOpen(uint streamId) {};
 	void streamWillRead(uint streamId) {};
 
-	void process();
 	uint contextIdForScreenActorId(uint screenActorId);
+	void addToContextLoadQueue(uint contextId);
+	void removeFromContextLoadQueue(uint contextId);
+	bool isContextLoadQueued(uint contextId);
+	void contextReleaseComplete(uint contextId);
+	void contextAlreadyReleased(uint contextId);
 
 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;
+	uint _disabledScreenAutoUpdateToken = 0;
 
 	void contextLoadDidComplete();
 	void screenLoadDidComplete();
@@ -98,8 +99,6 @@ private:
 	void stopFeed();
 	void blowAwayCurrentScreen();
 	void preloadParentContexts(uint contextId);
-	void addToContextLoadQueue(uint contextId);
-	bool isContextLoadQueued(uint contextId);
 	void checkQueuedContextLoads();
 };
 
diff --git a/engines/mediastation/events.cpp b/engines/mediastation/events.cpp
new file mode 100644
index 00000000000..1a3ff6b403c
--- /dev/null
+++ b/engines/mediastation/events.cpp
@@ -0,0 +1,402 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/str.h"
+
+#include "mediastation/debugchannels.h"
+#include "mediastation/events.h"
+#include "mediastation/actors/sound.h"
+#include "mediastation/mediastation.h"
+
+namespace MediaStation {
+
+const char *eventClassToStr(EventClass eventClass) {
+	switch (eventClass) {
+	case kEventClassInvalid:
+		return "Invalid";
+	case kEventClassSystem:
+		return "System";
+	case kEventClassMouse:
+		return "Mouse";
+	case kEventClassKeyboard:
+		return "Keyboard";
+	case kEventClassDisplay:
+		return "Display";
+	case kEventClassTimerService:
+		return "TimerServiceAlarm";
+	case kEventClassScriptTimer:
+		return "ScriptTimer";
+	case kEventClassActor:
+		return "Actor";
+	default:
+		return "UNKNOWN";
+	}
+}
+
+Common::String Event::debugString() const {
+	return Common::String::format("%s::%s",
+		eventClassToStr(eventClass), eventTypeToStr(type));
+}
+
+Common::String DisplayEvent::debugString() const {
+	return Common::String::format("%s %s (disableScreenAutoUpdateToken: %u)",
+		eventClassToStr(eventClass), eventTypeToStr(type), disableScreenAutoUpdateToken);
+}
+
+Common::String TimerServiceAlarmEvent::debugString() const {
+	return Common::String::format("%s %s (triggerTime: %u)",
+		eventClassToStr(eventClass), eventTypeToStr(type), triggerTime);
+}
+
+Common::String TimerEvent::debugString() const {
+	return Common::String::format("%s %s",
+		eventClassToStr(eventClass), eventTypeToStr(type));
+}
+
+Common::String ActorEvent::debugString() const {
+	return Common::String::format("%s %s (actorId: %u, arg: %s)",
+		eventClassToStr(eventClass), eventTypeToStr(type),
+		actorId, arg.getDebugString().c_str());
+}
+
+Common::String ScreenBranchEvent::debugString() const {
+	return Common::String::format("%s %s (actorId: %u, screenId: %u, disableScreenAutoUpdate: %s)",
+		eventClassToStr(eventClass), eventTypeToStr(type),
+		actorId, screenId, disableScreenAutoUpdate ? "true" : "false");
+}
+
+Common::String MouseEvent::debugString() const {
+	return Common::String::format("%s %s (position: %d, %d)",
+		eventClassToStr(eventClass), eventTypeToStr(type),
+		position.x, position.y);
+}
+
+Common::String KeyboardEvent::debugString() const {
+	return Common::String::format("%s %s (keyCode: %u)",
+		eventClassToStr(eventClass), eventTypeToStr(type), keyCode);
+}
+
+void EventLoop::run() {
+	while (!g_engine->shouldQuit()) {
+		// Do begin-of-frame sync.
+		debugC(9, kDebugEvents, "***** START EVENT LOOP ***");
+		g_engine->getTimerService()->queueExpiredTimerEvents();
+
+		// Dispatch engine events.
+		bool hadImtEvents = !_incomingQueue.empty();
+		if (hadImtEvents) {
+			dispatchImtEvents();
+		}
+
+		// Dispatch system events.
+		bool hadSystemEvents = g_system->getEventManager()->pollEvent(_queuedSystemEvent);
+		if (hadSystemEvents) {
+			g_engine->dispatchOneSystemEvent(_queuedSystemEvent);
+		}
+
+		// Do pre-display sync. Only movies seem to use this to update their frame state
+		// rather than setting a timer to do it.
+		PreDisplaySyncState preDisplaySyncState = preDisplaySync();
+
+		// Do display update.
+		bool hadAnyEvents = hadSystemEvents || hadImtEvents;
+		bool displayUpdateRequested = hadAnyEvents || preDisplaySyncState == kPreDisplaySyncForceScreenUpdate;
+		bool displayUpdateAllowed = preDisplaySyncState != kPreDisplaySyncStateBlockScreenUpdate;
+		if (displayUpdateRequested && displayUpdateAllowed) {
+			updateDisplay();
+		}
+
+		// Yield CPU.
+		g_system->delayMillis(15);
+
+		// Do end-of-frame sync. The original only services sounds here.
+		g_engine->serviceSounds();
+		debugC(9, kDebugEvents, "***** END EVENT LOOP ***");
+	}
+}
+
+void EventLoop::queueEvent(const Event &event) {
+	_incomingQueue.push(Common::ScopedPtr<Event>(event.clone()));
+}
+
+void EventLoop::dispatchImtEvents() {
+	// Move all incoming events to the dispatch queue so that events
+	// generated during dispatch don't create an infinite loop.
+	while (!_incomingQueue.empty()) {
+		_dispatchQueue.push(_incomingQueue.pop());
+	}
+
+	while (!_dispatchQueue.empty()) {
+		Common::ScopedPtr<Event> event = _dispatchQueue.pop();
+		dispatchImtEvent(*event);
+	}
+}
+
+void EventLoop::dispatchImtEvent(const Event &event) {
+	// The original used polymorphism here that is needlessly complex to understand and
+	// reimplement in this engine. So rather than having an EventHandler base class, we
+	// will just dispatch the old-fashioned way.
+	//
+	// Display events are less likely to be interesting (and there will be a lot of them),
+	// so they deserve a much higher debug channel.
+	uint debugLevel = (event.eventClass == kEventClassDisplay) ? 9 : 6;
+	debugC(debugLevel, kDebugEvents, "%s: %s", __func__, event.debugString().c_str());
+	switch (event.eventClass) {
+	case kEventClassActor: {
+		// ActorEventHandler
+		const ActorEvent &actorEvent = static_cast<const ActorEvent &>(event);
+		Actor *actor = g_engine->getImtGod()->getActorById(actorEvent.actorId);
+		if (actor != nullptr) {
+			actor->onEvent(actorEvent);
+		}
+		break;
+	}
+
+	case kEventClassDisplay: {
+		const DisplayEvent &displayEvent = static_cast<const DisplayEvent &>(event);
+		g_engine->getDisplayUpdateManager()->onEvent(displayEvent);
+		break;
+	}
+
+	case kEventClassTimerService: {
+		const TimerServiceAlarmEvent &alarmEvent = static_cast<const TimerServiceAlarmEvent &>(event);
+		g_engine->getTimerService()->handleEvent(alarmEvent);
+		break;
+	}
+
+	case kEventClassMouse: {
+		const MouseEvent &mouseEvent = static_cast<const MouseEvent &>(event);
+		g_engine->getStageDirector()->handleMouseEvent(mouseEvent);
+		break;
+	}
+
+	case kEventClassKeyboard: {
+		const KeyboardEvent &keyboardEvent = static_cast<const KeyboardEvent &>(event);
+		g_engine->getStageDirector()->handleKeyboardEvent(keyboardEvent);
+		break;
+	}
+
+	default:
+		warning("%s: Unhandled engine event: %s", __func__, event.debugString().c_str());
+	}
+}
+
+void MediaStationEngine::dispatchOneSystemEvent(const Common::Event &event) {
+	switch (event.type) {
+	case Common::EVENT_MOUSEMOVE: {
+		MouseEvent mouseEvent(kMouseMovedEvent, event.mouse);
+		g_engine->getEventLoop()->queueEvent(mouseEvent);
+		break;
+	}
+
+	case Common::EVENT_KEYDOWN: {
+		KeyboardEvent keyboardEvent(kKeyDownEvent, event.kbd.ascii);
+		_stageDirector->handleKeyboardEvent(keyboardEvent);
+		break;
+	}
+
+	case Common::EVENT_LBUTTONDOWN: {
+		MouseEvent mouseEvent(kMouseDownEvent, event.mouse);
+		g_engine->getEventLoop()->queueEvent(mouseEvent);
+		break;
+	}
+
+	case Common::EVENT_LBUTTONUP: {
+		MouseEvent mouseEvent(kMouseUpEvent, event.mouse);
+		g_engine->getEventLoop()->queueEvent(mouseEvent);
+		break;
+	}
+
+	case Common::EVENT_FOCUS_LOST: {
+		MouseEvent mouseEvent(kMouseOutOfFocusEvent, event.mouse);
+		_stageDirector->handleMouseOutOfFocusEvent(mouseEvent);
+		break;
+	}
+
+	default:
+		// Avoid warnings about unimplemented cases by having an explicit
+		// default case.
+		break;
+	}
+}
+
+PreDisplaySyncState EventLoop::preDisplaySync() {
+	PreDisplaySyncState state = kPreDisplaySyncNoScreenUpdateRequested;
+	for (auto it = _preDisplaySyncClients.begin(); it != _preDisplaySyncClients.end(); ++it) {
+		PreDisplaySyncClient *client = it->_value;
+		PreDisplaySyncState result = client->preDisplaySync();
+
+		if (result == kPreDisplaySyncForceScreenUpdate) {
+			if (state != kPreDisplaySyncStateBlockScreenUpdate) {
+				state = kPreDisplaySyncForceScreenUpdate;
+			}
+		} else if (result == kPreDisplaySyncStateBlockScreenUpdate) {
+			state = kPreDisplaySyncStateBlockScreenUpdate;
+		}
+	}
+
+	return state;
+}
+
+void EventLoop::registerForPreDisplaySyncCalls(PreDisplaySyncClient *client) {
+	_preDisplaySyncClients[client] = client;
+}
+
+void EventLoop::unregisterForPreDisplaySyncCalls(PreDisplaySyncClient *client) {
+	_preDisplaySyncClients.erase(client);
+}
+
+void EventLoop::updateDisplay() {
+	const uint NO_DISABLE_DISPLAY_UPDATE_TOKEN = 0;
+	DisplayEvent displayEvent(kDisplayAutoUpdateEvent, NO_DISABLE_DISPLAY_UPDATE_TOKEN);
+	dispatchImtEvent(displayEvent);
+}
+
+void PreDisplaySyncClient::registerForSyncCalls() {
+	g_engine->getEventLoop()->registerForPreDisplaySyncCalls(this);
+}
+
+void PreDisplaySyncClient::unregisterForSyncCalls() {
+	g_engine->getEventLoop()->unregisterForPreDisplaySyncCalls(this);
+}
+
+uint32 TimerEventReceiver::currentReceiverTime() {
+	return g_engine->getTotalPlayTime();
+}
+
+TimerEntry::~TimerEntry() {
+	g_engine->getTimerService()->stopTimer(*this);
+}
+
+uint32 TimerEntry::calculateFirstExpirationTime(uint32 currentTime) {
+	// Get scheduled expiration time from receiver's current time.
+	_expirationTime = currentTime + _duration;
+	return _expirationTime;
+}
+
+uint32 TimerEntry::calculateNextExpirationTime(uint32 currentTime) {
+	// Keep adding duration to the expiration time until it exceeds the current time.
+	// This ensures recurring timers "catch up" if execution was delayed.
+	while (_expirationTime <= currentTime) {
+		_expirationTime += _duration;
+	}
+	return _expirationTime;
+}
+
+uint32 TimerEntry::deltaTimeAtExpiration() const {
+	// Get difference between when timer actually fired and when it was scheduled.
+	if (_actualExpirationTime < _expirationTime) {
+		return 0;
+	} else {
+		return _actualExpirationTime - _expirationTime;
+	}
+}
+
+bool TimerEntry::operator==(const TimerEntry &other) const {
+	return _receiver == other._receiver &&
+		_expirationTime == other._expirationTime &&
+		_actualExpirationTime == other._actualExpirationTime &&
+		_duration == other._duration &&
+		_shouldRepeat == other._shouldRepeat;
+}
+
+bool TimerEntry::operator<(const TimerEntry &other) const {
+	return deltaTimeAtExpiration() < other.deltaTimeAtExpiration();
+}
+
+bool TimerEntry::operator>(const TimerEntry &other) const {
+	return deltaTimeAtExpiration() > other.deltaTimeAtExpiration();
+}
+
+void TimerService::handleEvent(const TimerServiceAlarmEvent &event) {
+	switch (event.type) {
+	case kTimerServiceAlarmEvent: {
+		if (_timers.contains(event.entry)) {
+			stopTimer(*event.entry);
+			TimerEvent timerEvent(event.entry);
+			event.entry->getReceiver()->timerEvent(timerEvent);
+
+			TimerEntry *entry = _timers.getValOrDefault(event.entry);
+			if (entry != nullptr) {
+				// Most timers are NOT rescheduled here but are
+				// rescheduled from the timer event called above.
+				rescheduleTimerEntry(*entry);
+			}
+		}
+		break;
+	}
+
+	default:
+		warning("%s: Unexpected event: %s", __func__, event.debugString().c_str());
+	}
+}
+
+void TimerService::startTimer(TimerEntry &entry, uint32 duration) {
+	stopTimer(entry);
+	entry.setDuration(duration);
+	if (entry.getReceiver() == nullptr) {
+		warning("%s: Can't start timer without receiver", __func__);
+		return;
+	}
+
+	uint32 currentReceiverTime = entry.getReceiver()->currentReceiverTime();
+	uint32 expirationTime = entry.calculateFirstExpirationTime(currentReceiverTime);
+	debugC(5, kDebugEvents, "%s: Starting timer for %d ms (expiration: %d)", __func__, duration, expirationTime);
+	_timers.setVal(&entry, &entry);
+}
+
+void TimerService::startTimer(TimerEntry &entry, double duration) {
+	startTimer(entry, static_cast<uint32>(duration * 1000));
+}
+
+void TimerService::stopTimer(TimerEntry &entry) {
+	_timers.erase(&entry);
+}
+
+void TimerService::rescheduleTimerEntry(TimerEntry &entry) {
+	if (entry.shouldReschedule()) {
+		// Calculate the next expiration time.
+		uint32 currentTime = entry.getReceiver()->currentReceiverTime();
+		entry.calculateNextExpirationTime(currentTime);
+	}
+}
+
+void TimerService::queueExpiredTimerEvents() {
+	debugC(7, kDebugEvents, "*** START CHECKING TIMERS (%d timers) ***", _timers.size());
+	for (auto it = _timers.begin(); it != _timers.end(); ++it) {
+		// Timer has expired if current time is greater than or equal to expiration time.
+		TimerEntry *entry = it->_value;
+		uint32 currentTime = entry->getReceiver()->currentReceiverTime();
+		uint32 expirationTime = entry->expirationTime();
+		bool hasExpired = currentTime >= expirationTime;
+		debugC(7, kDebugEvents, "%s: Checking timer (current: %d ms, expiration: %d ms)", __func__, currentTime, expirationTime);
+
+		if (hasExpired) {
+			// The original put this in a _queueTimerServiceAlarmFor method,
+			// but it is only called here, so it is inlined.
+			TimerServiceAlarmEvent event(entry, currentTime);
+			g_engine->getEventLoop()->queueEvent(event);
+		}
+	}
+	debugC(7, kDebugEvents, "*** END CHECKING TIMERS ***");
+}
+
+} // End of namespace MediaStation
diff --git a/engines/mediastation/events.h b/engines/mediastation/events.h
new file mode 100644
index 00000000000..6db1d4d5fb2
--- /dev/null
+++ b/engines/mediastation/events.h
@@ -0,0 +1,238 @@
+/* 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_EVENTS_H
+#define MEDIASTATION_EVENTS_H
+
+#include "common/events.h"
+#include "common/hash-ptr.h"
+#include "common/ptr.h"
+#include "common/str.h"
+#include "common/queue.h"
+
+#include "mediastation/mediascript/scriptconstants.h"
+#include "mediastation/mediascript/scriptvalue.h"
+
+namespace MediaStation {
+
+enum EventClass {
+	kEventClassInvalid = 0x00,
+	kEventClassSystem = 0x01,
+	kEventClassMouse = 0x02,
+	kEventClassKeyboard = 0x04,
+	kEventClassDisplay = 0x08,
+	kEventClassTimerService = 0x10,
+	kEventClassScriptTimer = 0x20,
+	kEventClassActor = 0x40
+};
+const char *eventClassToStr(EventClass eventClass);
+
+// This reimplementation doesn't use these values because
+// there isn't the indirection of event handler classes. We
+// just use the event class directly when deciding where to route an
+// event. However, this is still included for completeness .
+enum EventClassHandlerType {
+	kNoEventClassHandler = 0x00,
+	kStageDirectorEventClassHandler = 0x06,
+	kDisplayUpdateEventClassHandler = 0x08,
+	kTimerServiceClassHandler = 0x10,
+	kActorEventClassHandler = 0x40,
+};
+
+struct Event {
+	EventClass eventClass = kEventClassInvalid;
+	EventType type = kEventTypeInvalid;
+
+	Event(EventClass eventClass_, EventType eventType_)
+		: eventClass(eventClass_), type(eventType_) {}
+	virtual ~Event() {}
+	virtual Event *clone() const = 0;
+	virtual Common::String debugString() const;
+};
+
+struct DisplayEvent : public Event {
+	uint disableScreenAutoUpdateToken = 0;
+
+	DisplayEvent(EventType eventType_, uint token_)
+		: Event(kEventClassDisplay, eventType_), disableScreenAutoUpdateToken(token_) {}
+	Event *clone() const override { return new DisplayEvent(*this); }
+	Common::String debugString() const override;
+};
+
+// This is an event queued to tell the timer service that a timer has expired. When this event is processed,
+// an actual timer event will get sent to the actual timer event handler (actors and such).
+class TimerEntry;
+struct TimerServiceAlarmEvent : public Event {
+	uint32 triggerTime = 0;
+	TimerEntry *entry = nullptr;
+
+	TimerServiceAlarmEvent(TimerEntry *entry_, uint32 triggerTime_)
+		: Event(kEventClassTimerService, kTimerServiceAlarmEvent), triggerTime(triggerTime_), entry(entry_) {}
+	Event *clone() const override { return new TimerServiceAlarmEvent(*this); }
+	Common::String debugString() const override;
+};
+
+// This is actually sent to timer event handlers (actors and such).
+struct TimerEvent : public Event {
+	TimerEntry *entry = nullptr;
+
+	TimerEvent(TimerEntry *entry_)
+		: Event(kEventClassScriptTimer, kTimerScriptEvent), entry(entry_) {}
+	Event *clone() const override { return new TimerEvent(*this); }
+	Common::String debugString() const override;
+};
+
+struct ActorEvent : public Event {
+	uint actorId = 0;
+	ScriptValue arg;
+
+	ActorEvent(uint16 actorId_, EventType eventType_)
+		: Event(kEventClassActor, eventType_), actorId(actorId_) {}
+	ActorEvent(uint16 actorId_, EventType eventType_, ScriptValue arg_)
+		: Event(kEventClassActor, eventType_), actorId(actorId_), arg(arg_) {}
+	Event *clone() const override { return new ActorEvent(*this); }
+	Common::String debugString() const override;
+};
+
+struct ScreenBranchEvent : public ActorEvent {
+	uint screenId = 0;
+	bool disableScreenAutoUpdate = false;
+
+	ScreenBranchEvent(uint16 actorId_, uint16 screenId_, bool disableScreenAutoUpdate_)
+		: ActorEvent(actorId_, kScreenBranchEvent), screenId(screenId_), disableScreenAutoUpdate(disableScreenAutoUpdate_) {}
+	Event *clone() const override { return new ScreenBranchEvent(*this); }
+	Common::String debugString() const override;
+};
+
+struct MouseEvent : public Event {
+	Common::Point position;
+
+	MouseEvent(EventType eventType_, const Common::Point &position_)
+		: Event(kEventClassMouse, eventType_), position(position_) {}
+	Event *clone() const override { return new MouseEvent(*this); }
+	Common::String debugString() const override;
+};
+
+struct KeyboardEvent : public Event {
+	uint16 keyCode = 0;
+
+	KeyboardEvent(EventType eventType_, uint16 keyCode_)
+		: Event(kEventClassKeyboard, eventType_), keyCode(keyCode_) {}
+	Event *clone() const override { return new KeyboardEvent(*this); }
+	Common::String debugString() const override;
+};
+
+enum PreDisplaySyncState {
+	kPreDisplaySyncForceScreenUpdate = 0,
+	kPreDisplaySyncNoScreenUpdateRequested = 1,
+	kPreDisplaySyncStateBlockScreenUpdate = 2
+};
+
+// Interface for clients that need to update during the pre-display sync phase.
+// Only stream movie actors seem to actually use this.
+class PreDisplaySyncClient {
+public:
+	virtual ~PreDisplaySyncClient() {}
+	virtual PreDisplaySyncState preDisplaySync() = 0;
+
+	void registerForSyncCalls();
+	void unregisterForSyncCalls();
+};
+
+// The main event loop. In the original, much of the functionality lived in a separate
+// EventLoopSupport class (implemented by MAC_App or WIN_App), but in ScummVM there
+// is no need for that extra indirection.
+class EventLoop {
+public:
+	void run();
+	void queueEvent(const Event &event);
+
+	void registerForPreDisplaySyncCalls(PreDisplaySyncClient *client);
+	void unregisterForPreDisplaySyncCalls(PreDisplaySyncClient *client);
+
+private:
+	Common::Queue<Common::ScopedPtr<Event>> _incomingQueue;
+	Common::Queue<Common::ScopedPtr<Event>> _dispatchQueue;
+	Common::Event _queuedSystemEvent;
+	Common::HashMap<PreDisplaySyncClient *, PreDisplaySyncClient *> _preDisplaySyncClients;
+
+	void dispatchImtEvents();
+	PreDisplaySyncState preDisplaySync();
+	void updateDisplay();
+
+	// The original had a separate EventDispatcher, but things are much simpler by not using
+	// the polymorphism that required this intermediate class.
+	void dispatchImtEvent(const Event &event);
+};
+
+class TimerEventReceiver {
+public:
+	virtual ~TimerEventReceiver() {};
+
+	uint32 currentReceiverTime();
+	virtual void timerEvent(const TimerEvent &event) = 0;
+};
+
+class TimerEntry {
+public:
+	TimerEntry() = default;
+	TimerEntry(TimerEventReceiver *receiver) : _receiver(receiver) {}
+	~TimerEntry();
+
+	void setDuration(uint32 duration) { _duration = duration; }
+	uint32 calculateFirstExpirationTime(uint32 currentTime);
+	uint32 calculateNextExpirationTime(uint32 currentTime);
+	uint32 deltaTimeAtExpiration() const;
+	uint32 expirationTime() const { return _expirationTime; }
+	bool shouldReschedule() const { return _shouldRepeat; }
+	TimerEventReceiver *getReceiver() const { return _receiver; }
+
+	bool operator==(const TimerEntry &other) const;
+	bool operator<(const TimerEntry &other) const;
+	bool operator>(const TimerEntry &other) const;
+
+private:
+	TimerEventReceiver *_receiver = nullptr;
+	uint32 _expirationTime = 0;
+	uint32 _actualExpirationTime = 0;
+	uint32 _duration = 0;
+	bool _shouldRepeat = false;
+};
+
+class TimerService {
+public:
+	// This is called to actually dispatch a timer event to the event receiver.
+	void handleEvent(const TimerServiceAlarmEvent &event);
+
+	void startTimer(TimerEntry &entry, uint32 duration);
+	void startTimer(TimerEntry &entry, double duration);
+	void stopTimer(TimerEntry &entry);
+	void queueExpiredTimerEvents();
+
+private:
+	Common::HashMap<TimerEntry *, TimerEntry *> _timers;
+
+	void rescheduleTimerEntry(TimerEntry &entry);
+};
+
+} // End of namespace MediaStation
+
+#endif
diff --git a/engines/mediastation/graphics.cpp b/engines/mediastation/graphics.cpp
index 5b349c5d7c6..29747a311dd 100644
--- a/engines/mediastation/graphics.cpp
+++ b/engines/mediastation/graphics.cpp
@@ -204,6 +204,20 @@ void DisplayContext::setClipTo(Region region) {
 	}
 }
 
+void DisplayUpdateManager::performUpdateAll() {
+	debugC(9, kDebugGraphics, "%s", __func__);
+	g_engine->getStageDirector()->drawAll();
+	g_engine->getStageDirector()->clearDirtyRegion();
+	g_engine->getDisplayManager()->flushToDisplay();
+}
+
+void DisplayUpdateManager::performUpdateDirty() {
+	debugC(9, kDebugGraphics, "%s", __func__);
+	g_engine->getStageDirector()->drawDirtyRegion();
+	g_engine->getStageDirector()->clearDirtyRegion();
+	g_engine->getDisplayManager()->flushToDisplay();
+}
+
 VideoDisplayManager::VideoDisplayManager(MediaStationEngine *vm) : _vm(vm) {
 	initGraphics(MediaStationEngine::SCREEN_WIDTH, MediaStationEngine::SCREEN_HEIGHT);
 	_screen = new Graphics::Screen();
@@ -222,11 +236,11 @@ bool VideoDisplayManager::attemptToReadFromStream(Chunk &chunk, uint sectionType
 	bool handledParam = true;
 	switch (sectionType) {
 	case kVideoDisplayManagerUpdateDirty:
-		performUpdateDirty();
+		g_engine->getDisplayUpdateManager()->performUpdateDirty();
 		break;
 
 	case kVideoDisplayManagerUpdateAll:
-		performUpdateAll();
+		g_engine->getDisplayUpdateManager()->performUpdateAll();
 		break;
 
 	case kVideoDisplayManagerEffectTransition:
@@ -249,6 +263,67 @@ bool VideoDisplayManager::attemptToReadFromStream(Chunk &chunk, uint sectionType
 	return handledParam;
 }
 
+void VideoDisplayManager::flushToDisplay() {
+	_screen->update();
+	doTransitionOnSync();
+}
+
+void DisplayUpdateManager::onEvent(const DisplayEvent &event) {
+	switch (event.type) {
+	case kDisplayAutoUpdateEvent:
+		performAutoUpdateAndFlush();
+		break;
+
+	case kDisplayEnableAutoUpdateEvent:
+		enableAutoUpdate(event.disableScreenAutoUpdateToken);
+		break;
+
+	default:
+		break;
+	}
+}
+
+bool DisplayUpdateManager::needToDisplay() {
+	return !g_engine->getStageDirector()->getRootStage()->_dirtyRegion._rects.empty();
+}
+
+void DisplayUpdateManager::performAutoUpdateAndFlush() {
+	bool screenUpdated = false;
+	if (_autoUpdateEnabled && _forceFlush) {
+		performUpdateDirty();
+		screenUpdated = true;
+		_forceFlush = false;
+	} else if (_autoUpdateEnabled) {
+		if (needToDisplay()) {
+			performUpdateDirty();
+			screenUpdated = true;
+		}
+	}
+
+	// Any mouse movements and such need to be committed even if there
+	// was nothing else to draw.
+	if (!screenUpdated) {
+		g_system->updateScreen();
+	}
+}
+
+void DisplayUpdateManager::enableAutoUpdate(uint disabledScreenAutoUpdateToken) {
+	if (disabledScreenAutoUpdateToken == _disabledScreenAutoUpdateToken) {
+		_autoUpdateEnabled = true;
+		_forceFlush = true;
+	}
+}
+
+uint DisplayUpdateManager::disableAutoUpdate() {
+	_autoUpdateEnabled = false;
+	_forceFlush = false;
+	_disabledScreenAutoUpdateToken += 1;
+	if (_disabledScreenAutoUpdateToken == 0) {
+		_disabledScreenAutoUpdateToken = 1;
+	}
+	return _disabledScreenAutoUpdateToken;
+}
+
 void VideoDisplayManager::readAndEffectTransition(Chunk &chunk) {
 	uint argCount = chunk.readTypedUint16();
 	Common::Array<ScriptValue> args;
@@ -332,16 +407,6 @@ void VideoDisplayManager::doTransitionOnSync() {
 	}
 }
 
-void VideoDisplayManager::performUpdateDirty() {
-	debugC(5, kDebugGraphics, "%s", __func__);
-	g_engine->draw();
-}
-
-void VideoDisplayManager::performUpdateAll() {
-	debugC(5, kDebugGraphics, "%s", __func__);
-	g_engine->draw(false);
-}
-
 void VideoDisplayManager::fadeToBlack(Common::Array<ScriptValue> &args) {
 	double fadeTime = DEFAULT_FADE_TRANSITION_TIME_IN_SECONDS;
 	uint startIndex = DEFAULT_PALETTE_TRANSITION_START_INDEX;
diff --git a/engines/mediastation/graphics.h b/engines/mediastation/graphics.h
index 95f609d5f4e..3f35d26ed54 100644
--- a/engines/mediastation/graphics.h
+++ b/engines/mediastation/graphics.h
@@ -29,6 +29,7 @@
 #include "graphics/screen.h"
 
 #include "mediastation/clients.h"
+#include "mediastation/events.h"
 #include "mediastation/mediascript/scriptvalue.h"
 
 namespace MediaStation {
@@ -124,6 +125,25 @@ private:
 	Common::Stack<Clip> _clips;
 };
 
+class DisplayUpdateManager {
+public:
+	virtual ~DisplayUpdateManager() {}
+	virtual void onEvent(const DisplayEvent &event);
+
+	void performAutoUpdateAndFlush();
+	void performUpdateAll();
+	void performUpdateDirty();
+
+	void enableAutoUpdate(uint disabledUpdateDepthCounter);
+	uint disableAutoUpdate();
+	bool needToDisplay();
+
+private:
+	bool _autoUpdateEnabled = true;
+	bool _forceFlush = false;
+	uint _disabledScreenAutoUpdateToken = 0;
+};
+
 class VideoDisplayManager : public ParameterClient {
 public:
 	VideoDisplayManager(MediaStationEngine *vm);
@@ -131,6 +151,7 @@ public:
 
 	virtual bool attemptToReadFromStream(Chunk &chunk, uint sectionType) override;
 
+	void flushToDisplay();
 	void updateScreen() { _screen->update(); }
 	Graphics::Palette *getRegisteredPalette() { return _registeredPalette; }
 	void setRegisteredPalette(Graphics::Palette *palette) { _registeredPalette = palette; }
@@ -160,9 +181,6 @@ public:
 	void setTransitionOnSync(Common::Array<ScriptValue> &args) { _scheduledTransitionOnSync = args; }
 	void doTransitionOnSync();
 
-	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);
diff --git a/engines/mediastation/mediascript/scriptconstants.cpp b/engines/mediastation/mediascript/scriptconstants.cpp
index 9422115dd93..d3349029b7f 100644
--- a/engines/mediastation/mediascript/scriptconstants.cpp
+++ b/engines/mediastation/mediascript/scriptconstants.cpp
@@ -219,6 +219,10 @@ const char *builtInMethodToStr(BuiltInMethod method) {
 		return "TimePlay";
 	case kTimeStopMethod:
 		return "TimeStop";
+	case kTimePauseMethod:
+		return "Pause";
+	case kTimeResumeMethod:
+		return "Resume";
 	case kIsPlayingMethod:
 		return "IsPlaying/SetMultipleStreams";
 	case kSetDissolveFactorMethod:
@@ -265,10 +269,6 @@ const char *builtInMethodToStr(BuiltInMethod method) {
 		return "StartCaching";
 	case kIsCachingMethod:
 		return "IsCaching";
-	case kPauseMethod:
-		return "PauseWhileStarting";
-	case kResumeMethod:
-		return "ResumeStart";
 	case kIsPausedMethod:
 		return "SetMultipleSounds/IsPaused";
 	case kSetMousePositionMethod:
@@ -457,8 +457,16 @@ const char *builtInMethodToStr(BuiltInMethod method) {
 
 const char *eventTypeToStr(EventType type) {
 	switch (type) {
-	case kTimerEvent:
-		return "Timer";
+	case kEventTypeInvalid:
+		return "Invalid";
+	case kDisplayAutoUpdateEvent:
+		return "DisplayAutoUpdate";
+	case kDisplayEnableAutoUpdateEvent:
+		return "DisplayEnableAutoUpdate";
+	case kTimerServiceAlarmEvent:
+		return "TimerServiceAlarm";
+	case kTimerScriptEvent:
+		return "ScriptTimer";
 	case kMouseDownEvent:
 		return "MouseDown";
 	case kMouseUpEvent:
@@ -469,42 +477,52 @@ const char *eventTypeToStr(EventType type) {
 		return "MouseEntered";
 	case kMouseExitedEvent:
 		return "MouseExited";
+	case kMouseEnterExitEvent:
+		return "MouseEnterExit";
+	case kMouseOutOfFocusEvent:
+		return "MouseOutOfFocus";
 	case kKeyDownEvent:
 		return "KeyDown";
 	case kSoundEndEvent:
 		return "SoundEnd";
+	case kMovieEndEvent:
+		return "MovieEnd";
+	case kPathEndEvent:
+		return "PathEnd";
+	case kScreenEntryEvent:
+		return "ScreenEntry";
+	case kScreenBranchEvent:
+		return "ScreenBranch";
 	case kSoundAbortEvent:
 		return "SoundAbort";
 	case kSoundFailureEvent:
 		return "SoundFailure";
-	case kSoundStoppedEvent:
-		return "SoundStopped";
-	case kSoundBeginEvent:
-		return "SoundBegin";
-	case kMovieEndEvent:
-		return "MovieEnd";
 	case kMovieAbortEvent:
 		return "MovieAbort";
 	case kMovieFailureEvent:
 		return "MovieFailure";
-	case kMovieStoppedEvent:
-		return "MovieStopped";
-	case kMovieBeginEvent:
-		return "MovieBegin";
 	case kSpriteMovieEndEvent:
 		return "SpriteMovieEnd";
-	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 kPathStepEvent:
+		return "PathStep";
+	case kSoundStoppedEvent:
+		return "SoundStopped";
+	case kSoundBeginEvent:
+		return "SoundBegin";
+	case kMovieStoppedEvent:
+		return "MovieStopped";
+	case kMovieBeginEvent:
+		return "MovieBegin";
+	case kPathStoppedEvent:
+		return "PathStopped";
+	case kCachingFailureEvent:
+		return "CachingFailure";
+	case kCachingEndedEvent:
+		return "CachingEnded";
+	case kCachingStartedEvent:
+		return "CachingStarted";
 	case kTextInputEvent:
 		return "TextInput";
 	case kTextErrorEvent:
@@ -515,18 +533,22 @@ const char *eventTypeToStr(EventType type) {
 		return "DiskImageActorEnd";
 	case kCameraPanStepEvent:
 		return "CameraPanStep";
-	case kCameraPanAbortEvent:
-		return "CameraPanAbort";
 	case kCameraPanEndEvent:
 		return "CameraPanEnd";
-	case kPathStepEvent:
-		return "PathStep";
-	case kPathStoppedEvent:
-		return "PathStopped";
-	case kPathEndEvent:
-		return "PathEnd";
+	case kCameraPanAbortEvent:
+		return "CameraPanAbort";
+	case kContextLoadCompleteEvent:
+	case kContextAlreadyLoadedEvent:
+		return "ContextLoadComplete";
+	case kContextReleaseCompleteEvent:
+	case kContextAlreadyReleasedEvent:
+		return "ContextReleaseComplete";
+	case kContextLoadStartEvent:
+		return "ContextLoadStart";
+	case kContextReleaseStartEvent:
+		return "ContextReleaseStart";
 	default:
-		return "UNKNOWN";
+		return "UNKNOWN EVENT TYPE";
 	}
 }
 
diff --git a/engines/mediastation/mediascript/scriptconstants.h b/engines/mediastation/mediascript/scriptconstants.h
index 30b051b63d5..a851f8d26ce 100644
--- a/engines/mediastation/mediascript/scriptconstants.h
+++ b/engines/mediastation/mediascript/scriptconstants.h
@@ -155,8 +155,6 @@ enum BuiltInMethod {
 	kSetParallaxFactorYMethod = 0x181,
 	kStartCachingMethod = 0x113,
 	kIsCachingMethod = 0x114,
-	kPauseMethod = 0xD0,
-	kResumeMethod = 0xD1,
 	kIsPausedMethod = 0x175,
 
 	// STREAM MOVIE METHODS.
@@ -300,17 +298,24 @@ enum BuiltInMethod {
 const char *builtInMethodToStr(BuiltInMethod method);
 
 enum EventType {
-	kTimerEvent = 0x05,
+	kEventTypeInvalid = 0x00,
+	kDisplayAutoUpdateEvent = 0x02,
+	kDisplayEnableAutoUpdateEvent = 0x03,
+	kTimerServiceAlarmEvent = 0x04, // This schedules when the timer is supposed to fire.
+	kTimerScriptEvent = 0x05, // This is specifically a script timer event.
 	kMouseDownEvent = 0x06,
 	kMouseUpEvent = 0x07,
 	kMouseMovedEvent = 0x08,
 	kMouseEnteredEvent = 0x09,
 	kMouseExitedEvent = 0x0A,
+	kMouseEnterExitEvent = 0x0B,
+	kMouseOutOfFocusEvent = 0x0C,
 	kKeyDownEvent = 0x0D,
 	kSoundEndEvent = 0x0E,
 	kMovieEndEvent = 0x0F,
 	kPathEndEvent = 0x10,
 	kScreenEntryEvent = 0x11,
+	kScreenBranchEvent = 0x12,
 	kSoundAbortEvent = 0x13,
 	kSoundFailureEvent = 0x14,
 	kMovieAbortEvent = 0x15,
@@ -323,6 +328,9 @@ enum EventType {
 	kMovieStoppedEvent = 0x1F,
 	kMovieBeginEvent = 0x20,
 	kPathStoppedEvent = 0x21,
+	kCachingFailureEvent = 0x22,
+	kCachingEndedEvent = 0x23,
+	kCachingStartedEvent = 0x24,
 	kTextInputEvent = 0x25,
 	kTextErrorEvent = 0x26,
 	kDiskImageActorStepEvent = 0x27,
@@ -331,11 +339,11 @@ enum EventType {
 	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,
+	kContextAlreadyLoadedEvent = 0x2D,
+	kContextReleaseCompleteEvent = 0x2E,
+	kContextAlreadyReleasedEvent = 0x2F,
+	kContextLoadStartEvent = 0x30,
+	kContextReleaseStartEvent = 0x31
 };
 const char *eventTypeToStr(EventType type);
 
diff --git a/engines/mediastation/mediastation.cpp b/engines/mediastation/mediastation.cpp
index 53449854522..00c58c75875 100644
--- a/engines/mediastation/mediastation.cpp
+++ b/engines/mediastation/mediastation.cpp
@@ -61,11 +61,15 @@ MediaStationEngine::~MediaStationEngine() {
 	delete _document;
 	_imtGod->destroyActor(DocumentActor::DOCUMENT_ACTOR_ID);
 	delete _displayManager;
+	delete _displayUpdateManager;
 	delete _functionManager;
 	// delete _printManager;
 	delete _imtGod;
 	// delete _streamProfiler;
 	delete _streamFeedManager;
+	// delete _cacheManager;
+	delete _timerService;
+	delete _eventLoop;
 	delete _profile;
 }
 
@@ -137,12 +141,15 @@ bool MediaStationEngine::hasFeature(EngineFeature f) const {
 }
 
 Common::Error MediaStationEngine::run() {
+	_eventLoop = new EventLoop;
+	_timerService = new TimerService;
 	_streamFeedManager = new StreamFeedManager;
 	// _cacheManager = new CacheManager;
 	// _streamProfiler = new StreamProfiler;
 	_imtGod = new ImtGod(this);
 	_deviceOwner = new ImtDeviceOwner;
 	_functionManager = new FunctionManager;
+	_displayUpdateManager = new DisplayUpdateManager;
 	_displayManager = new VideoDisplayManager(this);
 	// _printManager = new PrintManager;
 	_document = new Document;
@@ -153,29 +160,10 @@ Common::Error MediaStationEngine::run() {
 	_profile = new Profile();
 	_profile->load();
 	_document->beginTitle();
-	runEventLoop();
+	_eventLoop->run();
 	return Common::kNoError;
 }
 
-void MediaStationEngine::runEventLoop() {
-	while (true) {
-		dispatchSystemEvents();
-		if (shouldQuit()) {
-			break;
-		}
-		_document->process();
-
-		debugC(9, kDebugGraphics, "***** START SCREEN UPDATE ***");
-		for (auto it = _imtGod->_actors.begin(); it != _imtGod->_actors.end(); ++it) {
-			it->_value->process();
-		}
-		draw();
-		debugC(9, kDebugGraphics, "***** END SCREEN UPDATE ***");
-
-		g_system->delayMillis(10);
-	}
-}
-
 void MediaStationEngine::initCursorManager() {
 	if (getPlatform() == Common::kPlatformWindows) {
 		_cursorManager = new WindowsCursorManager(getAppName());
@@ -187,6 +175,21 @@ void MediaStationEngine::initCursorManager() {
 	_cursorManager->showCursor();
 }
 
+void MediaStationEngine::registerAudioSequence(AudioSequence *sequence) {
+	_activeAudioSequences[sequence] = sequence;
+}
+
+void MediaStationEngine::unregisterAudioSequence(AudioSequence *sequence) {
+	_activeAudioSequences.erase(sequence);
+}
+
+void MediaStationEngine::serviceSounds() {
+	for (auto it = _activeAudioSequences.begin(); it != _activeAudioSequences.end(); ++it) {
+		AudioSequence *sequence = it->_value;
+		sequence->service();
+	}
+}
+
 ImtGod::ImtGod(MediaStationEngine *vm) : _vm(vm) {
 	_channelIdent = MKTAG('i', 'g', 'o', 'd');
 	registerWithStreamManager();
@@ -207,59 +210,6 @@ void ImtGod::setupInitialStreamMap() {
 	_fileMap.setVal(fileInfo._id, fileInfo);
 }
 
-void MediaStationEngine::dispatchSystemEvents() {
-	while (g_system->getEventManager()->pollEvent(_event)) {
-		debugC(9, kDebugEvents, "\n@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
-		debugC(9, kDebugEvents, "@@@@   Dispatching system events");
-		debugC(9, kDebugEvents, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n");
-
-		switch (_event.type) {
-		case Common::EVENT_MOUSEMOVE:
-			_stageDirector->handleMouseMovedEvent(_event);
-			break;
-
-		case Common::EVENT_KEYDOWN:
-			_stageDirector->handleKeyboardEvent(_event);
-			break;
-
-		case Common::EVENT_LBUTTONDOWN:
-			_stageDirector->handleMouseDownEvent(_event);
-			break;
-
-		case Common::EVENT_LBUTTONUP:
-			_stageDirector->handleMouseUpEvent(_event);
-			break;
-
-		case Common::EVENT_FOCUS_LOST:
-			_stageDirector->handleMouseOutOfFocusEvent(_event);
-			break;
-
-		case Common::EVENT_RBUTTONDOWN:
-			// We are using the right button as a quick exit since the Media
-			// Station engine doesn't seem to use the right button itself.
-			warning("%s: EVENT_RBUTTONDOWN: Quitting for development purposes", __func__);
-			quitGame();
-			break;
-
-		default:
-			// Avoid warnings about unimplemented cases by having an explicit
-			// default case.
-			break;
-		}
-	}
-}
-
-void MediaStationEngine::draw(bool dirtyOnly) {
-	if (dirtyOnly) {
-		_stageDirector->drawDirtyRegion();
-	} else {
-		_stageDirector->drawAll();
-	}
-	_stageDirector->clearDirtyRegion();
-	_displayManager->updateScreen();
-	_displayManager->doTransitionOnSync();
-}
-
 ImtGod::~ImtGod() {
 	unregisterWithStreamManager();
 	destroyAllContexts();
diff --git a/engines/mediastation/mediastation.h b/engines/mediastation/mediastation.h
index 79196a83f1b..6ccc8412e12 100644
--- a/engines/mediastation/mediastation.h
+++ b/engines/mediastation/mediastation.h
@@ -44,6 +44,7 @@
 #include "mediastation/cursors.h"
 #include "mediastation/datafile.h"
 #include "mediastation/detection.h"
+#include "mediastation/events.h"
 #include "mediastation/graphics.h"
 #include "mediastation/mediascript/function.h"
 #include "mediastation/profile.h"
@@ -56,6 +57,7 @@ class HotspotActor;
 class RootStage;
 class PixMapImage;
 class ImtGod;
+class EventLoop;
 
 // Most Media Station titles follow this file structure from the root directory
 // of the CD-ROM:
@@ -82,18 +84,24 @@ public:
 	Common::Platform getPlatform() const;
 	const char *getAppName() const;
 	bool hasFeature(EngineFeature f) const override;
-	void dispatchSystemEvents();
-	void draw(bool dirtyOnly = true);
+	void dispatchOneSystemEvent(const Common::Event &event);
 
 	VideoDisplayManager *getDisplayManager() { return _displayManager; }
+	DisplayUpdateManager *getDisplayUpdateManager() { return _displayUpdateManager; }
 	CursorManager *getCursorManager() { return _cursorManager; }
 	FunctionManager *getFunctionManager() { return _functionManager; }
 	RootStage *getRootStage() { return _stageDirector->getRootStage(); }
 	StageDirector *getStageDirector() { return _stageDirector; }
 	StreamFeedManager *getStreamFeedManager() { return _streamFeedManager; }
+	EventLoop *getEventLoop() { return _eventLoop; }
 	Document *getDocument() { return _document; }
+	TimerService *getTimerService() { return _timerService; }
 	ImtGod *getImtGod() { return _imtGod; }
 
+	void registerAudioSequence(AudioSequence *sequence);
+	void unregisterAudioSequence(AudioSequence *sequence);
+	void serviceSounds();
+
 	Common::String formatActorName(uint actorId, bool attemptToGetType = false) { return _profile->formatActorName(actorId, attemptToGetType); }
 	Common::String formatActorName(const Actor *actor) { return _profile->formatActorName(actor); }
 	Common::String formatFunctionName(uint functionId) { return _profile->formatFunctionName(functionId); }
@@ -119,19 +127,21 @@ protected:
 	Common::Error run() override;
 
 private:
-	Common::Event _event;
 	Common::FSNode _gameDataDir;
 	const ADGameDescription *_gameDescription;
 
 	SpatialEntity *_mouseInsideHotspot = nullptr;
 	SpatialEntity *_mouseDownHotspot = nullptr;
 
+	EventLoop *_eventLoop = nullptr;
+	TimerService *_timerService = nullptr;
 	StreamFeedManager *_streamFeedManager = nullptr;
 	// CacheManager *_cacheManager = nullptr;
 	// StreamProfiler *_streamProfiler = nullptr;
 	ImtGod *_imtGod = nullptr;
 	ImtDeviceOwner *_deviceOwner = nullptr;
 	FunctionManager *_functionManager = nullptr;
+	DisplayUpdateManager *_displayUpdateManager = nullptr;
 	VideoDisplayManager *_displayManager = nullptr;
 	// PrintManager *_printManager = nullptr;
 	Document *_document = nullptr;
@@ -142,12 +152,11 @@ private:
 	Common::HashMap<AudioSequence *, AudioSequence *> _activeAudioSequences;
 
 	void initCursorManager();
-
-	void runEventLoop();
+	void queueMouseEvent(EventType type, const Common::Point &point);
 };
 
 class ImtGod : public ChannelClient {
-friend class MediaStationEngine;
+friend class EventLoop;
 
 public:
 	ImtGod(MediaStationEngine *vm);
diff --git a/engines/mediastation/module.mk b/engines/mediastation/module.mk
index 4fb162b68fc..468c31581ab 100644
--- a/engines/mediastation/module.mk
+++ b/engines/mediastation/module.mk
@@ -26,6 +26,7 @@ MODULE_OBJS = \
 	context.o \
 	cursors.o \
 	datafile.o \
+	events.o \
 	graphics.o \
 	mediascript/codechunk.o \
 	mediascript/collection.o \




More information about the Scummvm-git-logs mailing list