[Scummvm-git-logs] scummvm master -> 925f9cb9bcabcd08993c733dd01698ed86bbfab1

npjg noreply at scummvm.org
Wed Jul 1 19:05:36 UTC 2026


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

Summary:
6d2037b91f MEDIASTATION: Address warnings that happen on some platforms
197e43b7bb MEDIASTATION: Define remaining transition types
6c92a99ebf MEDIASTATION: Don't unnecessarily allocate strings in script execution debug printing
5d71b58d66 MEDIASTATION: Implement basic text debugger and script decompiler
2d8cda6bd8 MEDIASTATION: Implement hardcoded constellations minigame
925f9cb9bc MEDIASTATION: Implement Color Cell Compression (CCC) blitting modes


Commit: 6d2037b91f7f01c931c3a8f70738a5af8736cc26
    https://github.com/scummvm/scummvm/commit/6d2037b91f7f01c931c3a8f70738a5af8736cc26
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-07-01T13:36:00-04:00

Commit Message:
MEDIASTATION: Address warnings that happen on some platforms

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


diff --git a/engines/mediastation/actor.h b/engines/mediastation/actor.h
index db613153b1b..354de7bdb63 100644
--- a/engines/mediastation/actor.h
+++ b/engines/mediastation/actor.h
@@ -192,7 +192,7 @@ private:
 
 // For a range of valid argument counts (min to max).
 #define ARGCOUNTRANGE(min, max) \
-	if ((int64)(min) > args.size() || args.size() > (int64)(max)) { \
+	if ((int64)(min) > (int64)args.size() || (int64)args.size() > (int64)(max)) { \
 		warning("%s: Expected %d to %d arguments, got %d", builtInMethodToStr(methodId), (min), (max), args.size()); \
 	}
 
diff --git a/engines/mediastation/graphics.cpp b/engines/mediastation/graphics.cpp
index 29747a311dd..72fec811a34 100644
--- a/engines/mediastation/graphics.cpp
+++ b/engines/mediastation/graphics.cpp
@@ -625,7 +625,7 @@ void VideoDisplayManager::_setPalette(Graphics::Palette &palette, uint startInde
 }
 
 void VideoDisplayManager::_setPaletteToColor(Graphics::Palette &targetPalette, byte r, byte g, byte b) {
-	for (uint colorIndex = 0; colorIndex < Graphics::PALETTE_COUNT; colorIndex++) {
+	for (int colorIndex = 0; colorIndex < Graphics::PALETTE_COUNT; colorIndex++) {
 		targetPalette.set(colorIndex, r, g, b);
 	}
 }


Commit: 197e43b7bb97b170eacf0c26e4e6cce3b2cb05f3
    https://github.com/scummvm/scummvm/commit/197e43b7bb97b170eacf0c26e4e6cce3b2cb05f3
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-07-01T13:36:00-04:00

Commit Message:
MEDIASTATION: Define remaining transition types

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


diff --git a/engines/mediastation/graphics.cpp b/engines/mediastation/graphics.cpp
index 72fec811a34..80a14315491 100644
--- a/engines/mediastation/graphics.cpp
+++ b/engines/mediastation/graphics.cpp
@@ -32,6 +32,71 @@
 
 namespace MediaStation {
 
+const char *transitionTypeToString(TransitionType type) {
+	switch (type) {
+	case kTransitionFadeToBlack:
+		return "FadeToBlack";
+	case kTransitionFadeToPalette:
+		return "FadeToPalette";
+	case kTransitionSetToPalette:
+		return "SetToPalette";
+	case kTransitionSetToBlack:
+		return "SetToBlack";
+	case kTransitionFadeToColor:
+		return "FadeToColor";
+	case kTransitionSetToColor:
+		return "SetToColor";
+	case kTransitionSetToPercentOfPalette:
+		return "SetToPercentOfPalette";
+	case kTransitionFadeToPaletteObject:
+		return "FadeToPaletteObject";
+	case kTransitionSetToPaletteObject:
+		return "SetToPaletteObject";
+	case kTransitionSetToPercentOfPaletteObject:
+		return "SetToPercentOfPaletteObject";
+	case kTransitionColorShiftCurrentPalette:
+		return "ColorShiftCurrentPalette";
+	case kTransitionScrollLeft:
+		return "ScrollLeft";
+	case kTransitionScrollRight:
+		return "ScrollRight";
+	case kTransitionScrollUp:
+		return "ScrollUp";
+	case kTransitionScrollDown:
+		return "ScrollDown";
+	case kTransitionWipeLeft:
+		return "WipeLeft";
+	case kTransitionWipeRight:
+		return "WipeRight";
+	case kTransitionWipeUp:
+		return "WipeUp";
+	case kTransitionWipeDown:
+		return "WipeDown";
+	case kTransitionSlideLeft:
+		return "SlideLeft";
+	case kTransitionSlideRight:
+		return "SlideRight";
+	case kTransitionSlideUp:
+		return "SlideUp";
+	case kTransitionSlideDown:
+		return "SlideDown";
+	case kTransitionSlitLROpen:
+		return "SlitLROpen";
+	case kTransitionSlitLRClose:
+		return "SlitLRClose";
+	case kTransitionSlitUDOpen:
+		return "SlitUDOpen";
+	case kTransitionSlitUDClose:
+		return "SlitUDClose";
+	case kTransitionCircleIn:
+		return "CircleIn";
+	case kTransitionCircleOut:
+		return "CircleOut";
+	default:
+		return "UNKNOWN";
+	}
+}
+
 void Region::addRect(const Common::Rect &rect) {
 	if (rect.isEmpty()) {
 		return;
@@ -346,6 +411,7 @@ void VideoDisplayManager::readAndRegisterPalette(Chunk &chunk) {
 
 void VideoDisplayManager::effectTransition(Common::Array<ScriptValue> &args) {
 	TransitionType transitionType = static_cast<TransitionType>(args[0].asParamToken());
+	debugC(5, kDebugGraphics, "%s: %s", __func__, transitionTypeToString(transitionType));
 	switch (transitionType) {
 	case kTransitionFadeToBlack:
 		fadeToBlack(args);
@@ -396,7 +462,7 @@ void VideoDisplayManager::effectTransition(Common::Array<ScriptValue> &args) {
 		break;
 
 	default:
-		warning("%s: Got unknown transition type %d", __func__, static_cast<uint>(transitionType));
+		warning("%s: Got unknown transition type %d (%s)", __func__, static_cast<uint>(transitionType), transitionTypeToString(transitionType));
 	}
 }
 
diff --git a/engines/mediastation/graphics.h b/engines/mediastation/graphics.h
index 3f35d26ed54..02143f5aaec 100644
--- a/engines/mediastation/graphics.h
+++ b/engines/mediastation/graphics.h
@@ -61,8 +61,26 @@ enum TransitionType {
 	kTransitionSetToPaletteObject = 308,
 	kTransitionSetToPercentOfPaletteObject = 309,
 	kTransitionColorShiftCurrentPalette = 310,
+	kTransitionScrollLeft = 311,
+	kTransitionScrollRight = 312,
+	kTransitionScrollUp = 313,
+	kTransitionScrollDown = 314,
+	kTransitionWipeLeft = 315,
+	kTransitionWipeRight = 316,
+	kTransitionWipeUp = 317,
+	kTransitionWipeDown = 318,
+	kTransitionSlideLeft = 319,
+	kTransitionSlideRight = 320,
+	kTransitionSlideUp = 321,
+	kTransitionSlideDown = 322,
+	kTransitionSlitLROpen = 323,
+	kTransitionSlitLRClose = 324,
+	kTransitionSlitUDOpen = 325,
+	kTransitionSlitUDClose = 326,
+	kTransitionCircleIn = 327,
 	kTransitionCircleOut = 328
 };
+const char *transitionTypeToStr(TransitionType type);
 
 enum VideoDisplayManagerSectionType {
 	kVideoDisplayManagerUpdateDirty = 0x578,


Commit: 6c92a99ebf9cf987bef86a943f85ccc3f6d60430
    https://github.com/scummvm/scummvm/commit/6c92a99ebf9cf987bef86a943f85ccc3f6d60430
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-07-01T13:36:00-04:00

Commit Message:
MEDIASTATION: Don't unnecessarily allocate strings in script execution debug printing

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


diff --git a/engines/mediastation/mediascript/codechunk.cpp b/engines/mediastation/mediascript/codechunk.cpp
index ba3f9e23008..db1ed9bb4f1 100644
--- a/engines/mediastation/mediascript/codechunk.cpp
+++ b/engines/mediastation/mediascript/codechunk.cpp
@@ -28,12 +28,9 @@
 
 namespace MediaStation {
 
-Common::String CodeChunk::makeDebugIndent() const {
-	Common::String indentation;
-	for (uint i = 0; i < _debugIndentLevel; ++i) {
-		indentation += "    ";
-	}
-	return indentation;
+static uint getIndentSize(uint indentLevel) {
+	constexpr uint INDENT_SIZE_IN_SPACES = 4;
+	return indentLevel * INDENT_SIZE_IN_SPACES;
 }
 
 ScriptValue CodeChunk::executeNextBlock() {
@@ -363,19 +360,19 @@ ScriptValue *CodeChunk::readAndReturnVariable() {
 
 void CodeChunk::evaluateIf() {
 	_debugIndentLevel++;
-	debugCN(5, kDebugScript, "\n%scondition: ", makeDebugIndent().c_str());
+	debugCN(5, kDebugScript, "\n%*scondition: ", getIndentSize(_debugIndentLevel), "");
 	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());
+		debugC(5, kDebugScript, "%*s=> TRUE", getIndentSize(_debugIndentLevel), "");
 		_debugIndentLevel--;
 		executeNextBlock();
 		debugC(6, kDebugScript, "%s: Taking TRUE branch", __func__);
 	} else {
-		debugC(5, kDebugScript, "%s=> FALSE", makeDebugIndent().c_str());
+		debugC(5, kDebugScript, "%*s=> FALSE", getIndentSize(_debugIndentLevel), "");
 		_debugIndentLevel--;
 		skipNextBlock();
 		debugC(6, kDebugScript, "%s: Skipping TRUE branch", __func__);
@@ -384,14 +381,14 @@ void CodeChunk::evaluateIf() {
 
 void CodeChunk::evaluateIfElse() {
 	_debugIndentLevel++;
-	debugCN(5, kDebugScript, "\n%scondition: ", makeDebugIndent().c_str());
+	debugCN(5, kDebugScript, "\n%*scondition: ", getIndentSize(_debugIndentLevel), "");
 	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());
+		debugC(5, kDebugScript, "%*s=> TRUE", getIndentSize(_debugIndentLevel), "");
 		_debugIndentLevel--;
 
 		debugC(6, kDebugScript, "%s: Taking TRUE branch", __func__);
@@ -400,7 +397,7 @@ void CodeChunk::evaluateIfElse() {
 		debugC(6, kDebugScript, "%s: Skipping FALSE branch", __func__);
 		skipNextBlock();
 	} else {
-		debugC(5, kDebugScript, "%s=> FALSE", makeDebugIndent().c_str());
+		debugC(5, kDebugScript, "%*s=> FALSE", getIndentSize(_debugIndentLevel), "");
 		_debugIndentLevel--;
 
 		debugC(6, kDebugScript, "%s: Skipping TRUE branch", __func__);
@@ -413,9 +410,9 @@ void CodeChunk::evaluateIfElse() {
 
 ScriptValue CodeChunk::evaluateAssign() {
 	_debugIndentLevel++;
-	debugCN(5, kDebugScript, "\n%svariable: ", makeDebugIndent().c_str());
+	debugCN(5, kDebugScript, "\n%*svariable: ", getIndentSize(_debugIndentLevel), "");
 	ScriptValue *targetVariable = readAndReturnVariable();
-	debugCN(5, kDebugScript, "%svalue: ", makeDebugIndent().c_str());
+	debugCN(5, kDebugScript, "%*svalue: ", getIndentSize(_debugIndentLevel), "");
 	ScriptValue value = evaluateExpression();
 	_debugIndentLevel--;
 
@@ -433,9 +430,9 @@ ScriptValue CodeChunk::evaluateAssign() {
 
 ScriptValue CodeChunk::evaluateBinaryOperation(Opcode op) {
 	_debugIndentLevel++;
-	debugCN(5, kDebugScript, "\n%slhs: ", makeDebugIndent().c_str());
+	debugCN(5, kDebugScript, "\n%*slhs: ", getIndentSize(_debugIndentLevel), "");
 	ScriptValue value1 = evaluateExpression();
-	debugCN(5, kDebugScript, "%srhs: ", makeDebugIndent().c_str());
+	debugCN(5, kDebugScript, "%*srhs: ", getIndentSize(_debugIndentLevel), "");
 	ScriptValue value2 = evaluateExpression();
 
 	ScriptValue returnValue;
@@ -505,7 +502,7 @@ ScriptValue CodeChunk::evaluateBinaryOperation(Opcode op) {
 	    op == kOpcodeEquals || op == kOpcodeNotEquals ||
 	    op == kOpcodeLessThan || op == kOpcodeGreaterThan ||
 	    op == kOpcodeLessThanOrEqualTo || op == kOpcodeGreaterThanOrEqualTo) {
-		debugC(5, kDebugScript, "%s=> %s", makeDebugIndent().c_str(), returnValue.asBool() ? "TRUE" : "FALSE");
+		debugC(5, kDebugScript, "%*s=> %s", getIndentSize(_debugIndentLevel), "", returnValue.asBool() ? "TRUE" : "FALSE");
 	}
 
 	_debugIndentLevel--;
@@ -515,7 +512,7 @@ ScriptValue CodeChunk::evaluateBinaryOperation(Opcode op) {
 ScriptValue CodeChunk::evaluateUnaryOperation() {
 	// The only supported unary operation seems to be negation.
 	_debugIndentLevel++;
-	debugCN(5, kDebugScript, "\n%svalue: ", makeDebugIndent().c_str());
+	debugCN(5, kDebugScript, "\n%*svalue: ", getIndentSize(_debugIndentLevel), "");
 	ScriptValue value = evaluateExpression();
 	_debugIndentLevel--;
 	return -value;
@@ -542,7 +539,7 @@ ScriptValue CodeChunk::evaluateFunctionCall(uint functionId, uint paramCount) {
 	Common::Array<ScriptValue> args;
 	_debugIndentLevel++;
 	for (uint i = 0; i < paramCount; i++) {
-		debugCN(5, kDebugScript, "%sparam %d: ", makeDebugIndent().c_str(), i);
+		debugCN(5, kDebugScript, "%*sparam %d: ", getIndentSize(_debugIndentLevel), "", i);
 		ScriptValue arg = evaluateExpression();
 		args.push_back(arg);
 	}
@@ -574,7 +571,7 @@ ScriptValue CodeChunk::evaluateMethodCall(BuiltInMethod method, uint paramCount)
 	// But here, we're only looking for built-in methods.
 	debugC(5, kDebugScript, "%s (%d params)", builtInMethodToStr(method), paramCount);
 	_debugIndentLevel++;
-	debugCN(5, kDebugScript, "%sself: ", makeDebugIndent().c_str());
+	debugCN(5, kDebugScript, "%*sself: ", getIndentSize(_debugIndentLevel), "");
 
 	// Evaluate target as an lvalue to get a pointer to the actual variable if there is one.
 	ScriptValue methodCallTarget;
@@ -582,7 +579,7 @@ ScriptValue CodeChunk::evaluateMethodCall(BuiltInMethod method, uint paramCount)
 	evaluateLValue(methodCallTargetPtr);
 	Common::Array<ScriptValue> args;
 	for (uint i = 0; i < paramCount; i++) {
-		debugCN(5, kDebugScript, "%sparam %d: ", makeDebugIndent().c_str(), i);
+		debugCN(5, kDebugScript, "%*sparam %d: ", getIndentSize(_debugIndentLevel), "", i);
 		ScriptValue arg = evaluateExpression();
 		args.push_back(arg);
 	}
@@ -649,7 +646,7 @@ void CodeChunk::evaluateWhileLoop() {
 		// Seek to the top of the loop bytecode.
 		_bytecode->seek(loopStartPosition);
 		_debugIndentLevel++;
-		debugCN(5, kDebugScript, "\n%scondition: ", makeDebugIndent().c_str());
+		debugCN(5, kDebugScript, "\n%*scondition: ", getIndentSize(_debugIndentLevel), "");
 		ScriptValue condition = evaluateExpression();
 		_debugIndentLevel--;
 		if (condition.getType() != kScriptValueTypeBool) {
@@ -661,10 +658,10 @@ void CodeChunk::evaluateWhileLoop() {
 		}
 
 		if (condition.asBool()) {
-			debugC(5, kDebugScript, "%s=> TRUE (continue loop)", makeDebugIndent().c_str());
+			debugC(5, kDebugScript, "%*s=> TRUE (continue loop)", getIndentSize(_debugIndentLevel), "");
 			executeNextBlock();
 		} else {
-			debugC(5, kDebugScript, "%s=> FALSE (exit loop)", makeDebugIndent().c_str());
+			debugC(5, kDebugScript, "%*s=> FALSE (exit loop)", getIndentSize(_debugIndentLevel), "");
 			skipNextBlock();
 			break;
 		}
diff --git a/engines/mediastation/mediascript/codechunk.h b/engines/mediastation/mediascript/codechunk.h
index d4c0abbe566..c6923bdaf89 100644
--- a/engines/mediastation/mediascript/codechunk.h
+++ b/engines/mediastation/mediascript/codechunk.h
@@ -77,7 +77,6 @@ private:
 
 	// Debug output indentation tracking.
 	uint _debugIndentLevel = 0;
-	Common::String makeDebugIndent() const;
 };
 
 } // End of namespace MediaStation


Commit: 5d71b58d66a596fa4c025b554510a6452dc8d14d
    https://github.com/scummvm/scummvm/commit/5d71b58d66a596fa4c025b554510a6452dc8d14d
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-07-01T14:58:29-04:00

Commit Message:
MEDIASTATION: Implement basic text debugger and script decompiler

Assisted-by: claude-sonnet-4-20250514

Changed paths:
  A engines/mediastation/debugger.cpp
  A engines/mediastation/debugger.h
    engines/mediastation/actor.h
    engines/mediastation/actors/hotspot.h
    engines/mediastation/actors/movie.h
    engines/mediastation/actors/sound.h
    engines/mediastation/actors/sprite.h
    engines/mediastation/actors/timer.h
    engines/mediastation/clients.cpp
    engines/mediastation/clients.h
    engines/mediastation/mediascript/codechunk.cpp
    engines/mediastation/mediascript/codechunk.h
    engines/mediastation/mediascript/function.cpp
    engines/mediastation/mediascript/function.h
    engines/mediastation/mediascript/scriptconstants.cpp
    engines/mediastation/mediascript/scriptresponse.cpp
    engines/mediastation/mediascript/scriptresponse.h
    engines/mediastation/mediascript/scriptvalue.cpp
    engines/mediastation/mediascript/scriptvalue.h
    engines/mediastation/mediastation.cpp
    engines/mediastation/mediastation.h
    engines/mediastation/module.mk
    engines/mediastation/profile.cpp
    engines/mediastation/profile.h


diff --git a/engines/mediastation/actor.h b/engines/mediastation/actor.h
index 354de7bdb63..9cd27cd1c47 100644
--- a/engines/mediastation/actor.h
+++ b/engines/mediastation/actor.h
@@ -227,6 +227,11 @@ public:
 	void setContextId(uint id) { _contextId = id; }
 	virtual bool isSpatialActor() const { return false; }
 
+	// Helper functions for the debugger.
+	virtual bool isActive() const { return true; }
+	virtual Common::String debugString() const { return "<no additional info>"; }
+	const Common::HashMap<uint, Common::Array<ScriptResponse *> > &scriptResponses() const { return _scriptResponses; }
+
 	const char *debugName() const;
 
 protected:
diff --git a/engines/mediastation/actors/hotspot.h b/engines/mediastation/actors/hotspot.h
index e58285acacd..0ceac8f326d 100644
--- a/engines/mediastation/actors/hotspot.h
+++ b/engines/mediastation/actors/hotspot.h
@@ -35,7 +35,7 @@ public:
 
 	bool inBounds(const Common::Point &point);
 	virtual bool isVisible() const override { return false; }
-	bool isActive() const { return _isActive; }
+	virtual bool isActive() const override { return _isActive; }
 	virtual bool interactsWithMouse() const override { return isActive(); }
 
 	virtual void readParameter(Chunk &chunk, ActorHeaderSectionType paramType) override;
diff --git a/engines/mediastation/actors/movie.h b/engines/mediastation/actors/movie.h
index d671a4b7242..a8f76244033 100644
--- a/engines/mediastation/actors/movie.h
+++ b/engines/mediastation/actors/movie.h
@@ -140,6 +140,7 @@ public:
 	virtual void loadIsComplete() override;
 	virtual void readParameter(Chunk &chunk, ActorHeaderSectionType paramType) override;
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
+	virtual bool isActive() const override { return _isPlaying; }
 
 	virtual PreDisplaySyncState preDisplaySync() override;
 	virtual void onEvent(const ActorEvent &event) override;
diff --git a/engines/mediastation/actors/sound.h b/engines/mediastation/actors/sound.h
index 4631b2e8fe7..a53b7b84f1c 100644
--- a/engines/mediastation/actors/sound.h
+++ b/engines/mediastation/actors/sound.h
@@ -38,6 +38,7 @@ public:
 	virtual void readParameter(Chunk &chunk, ActorHeaderSectionType paramType) override;
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 	virtual void readChunk(Chunk &chunk) override;
+	virtual bool isActive() const override { return _playState == kSoundPlayStatePlaying; }
 
 	virtual void onEvent(const ActorEvent &event) override;
 	virtual void timerEvent(const TimerEvent &event) override;
diff --git a/engines/mediastation/actors/sprite.h b/engines/mediastation/actors/sprite.h
index a5799874d7c..d4f6cfccec8 100644
--- a/engines/mediastation/actors/sprite.h
+++ b/engines/mediastation/actors/sprite.h
@@ -74,6 +74,7 @@ public:
 	virtual void readParameter(Chunk &chunk, ActorHeaderSectionType paramType) override;
 	virtual void loadIsComplete() override;
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
+	virtual bool isActive() const override { return _isPlaying; }
 
 	virtual void onEvent(const ActorEvent &event) override;
 	virtual void timerEvent(const TimerEvent &event) override;
diff --git a/engines/mediastation/actors/timer.h b/engines/mediastation/actors/timer.h
index ee063644f03..4e9cab9bccf 100644
--- a/engines/mediastation/actors/timer.h
+++ b/engines/mediastation/actors/timer.h
@@ -34,6 +34,7 @@ public:
 
 	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
 	virtual void timerEvent(const TimerEvent &event) override;
+	virtual bool isActive() const override { return _startTime > 0; }
 
 private:
 	uint32 _pauseStartTime = 0;
diff --git a/engines/mediastation/clients.cpp b/engines/mediastation/clients.cpp
index af0238e8a24..43f499213c6 100644
--- a/engines/mediastation/clients.cpp
+++ b/engines/mediastation/clients.cpp
@@ -334,4 +334,19 @@ void Document::contextAlreadyReleased(uint contextId) {
 	g_engine->getEventLoop()->queueEvent(actorEvent);
 }
 
+Common::String Document::getDebugString() {
+	return Common::String::format(
+		"currentScreen: %s\n"
+		"loadingScreen: %s\n"
+		"loadingContext: %s\n"
+		"entryScreen: %s\n"
+		"entryStream: %u",
+		g_engine->formatActorName(_currentScreenActorId).c_str(),
+		g_engine->formatActorName(_loadingScreenActorId).c_str(),
+		g_engine->formatActorName(_loadingContextId).c_str(),
+		g_engine->formatActorName(_entryPointScreenId).c_str(),
+		_entryPointStreamId
+	);
+}
+
 } // End of namespace MediaStation
diff --git a/engines/mediastation/clients.h b/engines/mediastation/clients.h
index 5b00f2c53c0..6b0894567ea 100644
--- a/engines/mediastation/clients.h
+++ b/engines/mediastation/clients.h
@@ -26,6 +26,8 @@
 
 namespace MediaStation {
 
+class ScreenActor;
+
 class ParameterClient {
 public:
 	ParameterClient();
@@ -54,6 +56,8 @@ enum DocumentSectionType {
 };
 
 class Document : public ParameterClient {
+friend class Debugger;
+
 public:
 	virtual bool attemptToReadFromStream(Chunk &chunk, uint sectionType) override;
 	void readStartupInformation(Chunk &chunk);
@@ -78,6 +82,8 @@ public:
 	void contextReleaseComplete(uint contextId);
 	void contextAlreadyReleased(uint contextId);
 
+	Common::String getDebugString();
+
 private:
 	uint _currentScreenActorId = 0;
 	StreamFeed *_currentStreamFeed = nullptr;
diff --git a/engines/mediastation/debugger.cpp b/engines/mediastation/debugger.cpp
new file mode 100644
index 00000000000..1c1379c076d
--- /dev/null
+++ b/engines/mediastation/debugger.cpp
@@ -0,0 +1,195 @@
+/* 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/actor.h"
+#include "mediastation/actors/hotspot.h"
+#include "mediastation/actors/movie.h"
+#include "mediastation/actors/screen.h"
+#include "mediastation/actors/sound.h"
+#include "mediastation/actors/sprite.h"
+#include "mediastation/actors/timer.h"
+#include "mediastation/context.h"
+#include "mediastation/debugger.h"
+#include "mediastation/mediascript/function.h"
+#include "mediastation/mediascript/scriptvalue.h"
+#include "mediastation/mediastation.h"
+
+namespace MediaStation {
+
+Debugger::Debugger(MediaStationEngine *vm) : GUI::Debugger(), _vm(vm) {
+	registerCmd("actors", WRAP_METHOD(Debugger, Cmd_ListActors));
+	registerCmd("actor", WRAP_METHOD(Debugger, Cmd_PrintActor));
+	registerCmd("functions", WRAP_METHOD(Debugger, Cmd_ListFunctions));
+	registerCmd("function", WRAP_METHOD(Debugger, Cmd_DecompileFunction));
+	registerCmd("document", WRAP_METHOD(Debugger, Cmd_GetDocumentInfo));
+	registerCmd("branch", WRAP_METHOD(Debugger, Cmd_BranchToScreen));
+	registerCmd("variables", WRAP_METHOD(Debugger, Cmd_ListVariables));
+}
+
+bool Debugger::Cmd_ListActors(int argc, const char **argv) {
+	debugPrintf("Currently loaded actors\n");
+	debugPrintf("+----+------------------------------+----------+-----+---+------+-----+-----+-----+-----+-----+-----+\n");
+	debugPrintf("| id | name                         | type     | ctx |vis|active|  x  |  y  |  w  |  h  |  z  | stg |\n");
+	debugPrintf("+----+------------------------------+----------+-----+---+------+-----+-----+-----+-----+-----+-----+\n");
+	for (auto it = _vm->getImtGod()->_actors.begin(); it != _vm->getImtGod()->_actors.end(); ++it) {
+		Actor *actor = it->_value;
+		const Common::String actorName = _vm->formatActorName(actor, false);
+		const char *activeStateText = actor->isActive() ? "yes" : "no";
+
+		if (actor->isSpatialActor()) {
+			SpatialEntity *spatialActor = static_cast<SpatialEntity *>(actor);
+			const Common::Rect bbox = spatialActor->getBbox();
+			const int width = bbox.width();
+			const int height = bbox.height();
+			const uint stageId = spatialActor->getParentStage() ? spatialActor->getParentStage()->id() : 0;
+
+			debugPrintf("|%4u|%-30.30s|%-10.10s|%5u|%3s|%6.6s|%5d|%5d|%5d|%5d|%5d|%5u|\n",
+				actor->id(), actorName.c_str(), actorTypeToStr(actor->type()), actor->contextId(),
+				spatialActor->isVisible() ? "yes" : "no", activeStateText, bbox.left, bbox.top, width, height,
+				spatialActor->zIndex(), stageId);
+		} else {
+			// Since we don't have a spatial actor, just fill all the spatial fields with blanks.
+			debugPrintf("|%4u|%-30.30s|%-10.10s|%5u|%3s|%6.6s|%5s|%5s|%5s|%5s|%5s|%5s|\n",
+				actor->id(), actorName.c_str(), actorTypeToStr(actor->type()), actor->contextId(),
+				"", activeStateText, "", "", "", "", "", "");
+		}
+	}
+	debugPrintf("\n");
+	return true;
+}
+
+bool Debugger::Cmd_PrintActor(int argc, const char **argv) {
+	if (argc != 2) {
+		debugPrintf("Usage: %s <actorId>\n", argv[0]);
+		return true;
+	}
+
+	// TODO: Also accept actor names.
+	uint actorId = atoi(argv[1]);
+	Actor *actor = _vm->getImtGod()->getActorById(actorId);
+	if (!actor) {
+		debugPrintf("<actor %u doesn't exist or not loaded>\n", actorId);
+		return true;
+	}
+
+	debugPrintf("%s\n\n", actor->debugString().c_str());
+
+	// Print script response information.
+	const Common::HashMap<uint, Common::Array<ScriptResponse *> > &scriptResponses = actor->scriptResponses();
+	if (!scriptResponses.empty()) {
+		for (auto it = scriptResponses.begin(); it != scriptResponses.end(); ++it) {
+			const Common::Array<ScriptResponse *> &responses = it->_value;
+			for (const ScriptResponse *response : responses) {
+				Common::String decompiledCode = response->decompile();
+				debugPrintf("%s\n", decompiledCode.c_str());
+			}
+		}
+	} else {
+		debugPrintf("<no script responses>\n");
+	}
+
+	return true;
+}
+
+bool Debugger::Cmd_ListFunctions(int argc, const char **argv) {
+	debugPrintf("Currently loaded script functions\n");
+	debugPrintf("+------+------------------------------+-----+-------+\n");
+	debugPrintf("| id   | name                         | ctx | bytes |\n");
+	debugPrintf("+------+------------------------------+-----+-------+\n");
+	for (auto it = _vm->getFunctionManager()->_functions.begin(); it != _vm->getFunctionManager()->_functions.end(); ++it) {
+		ScriptFunction *function = it->_value;
+		const Common::String functionName = _vm->formatFunctionName(function->_id, false);
+
+		debugPrintf("|%6u|%-30.30s|%5u|%7u|\n", function->_id, functionName.c_str(), function->_contextId, function->bytecodeSize());
+	}
+	debugPrintf("\n");
+	return true;
+}
+
+bool Debugger::Cmd_GetDocumentInfo(int argc, const char **argv) {
+	Document *document = g_engine->getDocument();
+	debugPrintf(
+		"currentScreen: %s\n"
+		"loadingScreen: %s\n"
+		"loadingContext: %s\n"
+		"entryScreen: %s\n"
+		"entryStream: %u\n\n",
+		g_engine->formatActorName(document->_currentScreenActorId, true).c_str(),
+		g_engine->formatActorName(document->_loadingScreenActorId, true).c_str(),
+		g_engine->formatActorName(document->_loadingContextId, true).c_str(),
+		g_engine->formatActorName(document->_entryPointScreenId, true).c_str(),
+		g_engine->getDocument()->_entryPointStreamId
+	);
+	return true;
+}
+
+bool Debugger::Cmd_BranchToScreen(int argc, const char **argv) {
+	if (argc != 2) {
+		debugPrintf("Usage: %s [screenId]\n", argv[0]);
+		debugPrintf("  screenId - The numeric screen ID\n");
+		return true;
+	}
+
+	uint contextId = atoi(argv[1]);
+	_vm->getDocument()->branchToScreen(contextId, false);
+	// Close the debugger immediately to execute the branch.
+	return false;
+}
+
+bool Debugger::Cmd_DecompileFunction(int argc, const char **argv) {
+	if (argc != 2 || atoi(argv[1]) <= 0) {
+		debugPrintf("Usage: %s <functionId>\n", argv[0]);
+		return true;
+	}
+
+	int functionId = atoi(argv[1]);
+	ScriptFunction *function = _vm->getFunctionManager()->getFunctionById(functionId);
+	if (function == nullptr) {
+		debugPrintf("<function %d doens't exist or not loaded>\n", functionId);
+		return true;
+	}
+
+	debugPrintf("%s\n", function->decompile().c_str());
+	return true;
+}
+
+bool Debugger::Cmd_ListVariables(int argc, const char **argv) {
+	debugPrintf("Currently loaded global variables\n");
+	debugPrintf("+------+-----+------------------------------+------------+------------------------------+\n");
+	debugPrintf("| id   | ctx | name                         | type       | value                        |\n");
+	debugPrintf("+------+-----+------------------------------+------------+------------------------------+\n");
+	for (auto contextIt = _vm->getImtGod()->_loadedContexts.begin(); contextIt != _vm->getImtGod()->_loadedContexts.end(); ++contextIt) {
+		const Context *context = contextIt->_value;
+		for (auto variableIt = context->_variables.begin(); variableIt != context->_variables.end(); ++variableIt) {
+			const uint variableId = variableIt->_key;
+			const ScriptValue *value = variableIt->_value;
+			const Common::String variableName = _vm->formatVariableName(variableId, false);
+			const char *typeName = scriptValueTypeToStr(value->getType());
+			const Common::String valueText = value->getDebugString(false);
+			debugPrintf("|%6u|%5u|%-30.30s|%-12.12s|%-30.30s|\n",
+				variableId, context->_id, variableName.c_str(), typeName, valueText.c_str());
+		}
+	}
+	debugPrintf("\n");
+	return true;
+}
+
+} // End of namespace MediaStation
diff --git a/engines/mediastation/debugger.h b/engines/mediastation/debugger.h
new file mode 100644
index 00000000000..99b7cf26ecb
--- /dev/null
+++ b/engines/mediastation/debugger.h
@@ -0,0 +1,53 @@
+/* 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_DEBUGGER_H
+#define MEDIASTATION_DEBUGGER_H
+
+#include "gui/debugger.h"
+
+namespace MediaStation {
+
+class MediaStationEngine;
+
+class Debugger : public GUI::Debugger {
+public:
+	Debugger(MediaStationEngine *vm);
+	~Debugger() override {}
+
+protected:
+	bool Cmd_ListActors(int argc, const char **argv);
+	bool Cmd_PrintActor(int argc, const char **argv);
+	bool Cmd_ListFunctions(int argc, const char **argv);
+	bool Cmd_GetDocumentInfo(int argc, const char **argv);
+	bool Cmd_BranchToScreen(int argc, const char **argv);
+	bool Cmd_DecompileFunction(int argc, const char **argv);
+	bool Cmd_ListVariables(int argc, const char **argv);
+
+private:
+	MediaStationEngine *_vm;
+
+	void showScreenInfo();
+};
+
+} // End of namespace MediaStation
+
+#endif
diff --git a/engines/mediastation/mediascript/codechunk.cpp b/engines/mediastation/mediascript/codechunk.cpp
index db1ed9bb4f1..62d7ff6ed48 100644
--- a/engines/mediastation/mediascript/codechunk.cpp
+++ b/engines/mediastation/mediascript/codechunk.cpp
@@ -676,4 +676,399 @@ CodeChunk::~CodeChunk() {
 	_bytecode = nullptr;
 }
 
+CodeChunkDecompiler::CodeChunkDecompiler(ParameterReadStream *bytecode, uint indentLevel)
+	: _indentLevel(indentLevel), _bytecode(bytecode) {
+}
+
+Common::String CodeChunkDecompiler::decompileNextBlock() {
+	// Mirrors CodeChunk::executeNextBlock
+	Common::String result;
+	uint blockSize = _bytecode->readTypedUint32();
+	int64 blockStart = _bytecode->pos();
+	int64 blockEnd = blockStart + blockSize;
+
+	ExpressionType expressionType = static_cast<ExpressionType>(_bytecode->readTypedUint16());
+	while (expressionType != kExpressionTypeEmpty) {
+		if (_bytecode->pos() >= blockEnd) {
+			warning("%s: Reached end of bytecode stream without finding end of code", __func__);
+			break;
+		}
+
+		Common::String line = decompileExpression(expressionType);
+		if (!line.empty()) {
+			result += Common::String::format("%*s", getIndentSize(_indentLevel), "") + line + ";\n";
+		}
+
+		if (_bytecode->pos() < blockEnd) {
+			expressionType = static_cast<ExpressionType>(_bytecode->readTypedUint16());
+		} else {
+			break;
+		}
+	}
+
+	return result;
+}
+
+Common::String CodeChunkDecompiler::decompileExpression() {
+	// Mirrors CodeChunk::evaluateExpression
+	ExpressionType expressionType = static_cast<ExpressionType>(_bytecode->readTypedUint16());
+	return decompileExpression(expressionType);
+}
+
+Common::String CodeChunkDecompiler::decompileExpression(ExpressionType expressionType) {
+	// Mirrors CodeChunk::evaluateExpression
+	switch (expressionType) {
+	case kExpressionTypeEmpty:
+		return "";
+
+	case kExpressionTypeOperation:
+		return decompileOperation();
+
+	case kExpressionTypeValue:
+		return decompileValue();
+
+	case kExpressionTypeVariable:
+		return decompileVariable();
+
+	default:
+		return Common::String::format("<unknown_expression_%d>", static_cast<uint>(expressionType));
+	}
+}
+
+Common::String CodeChunkDecompiler::decompileOperation() {
+	// Mirrors CodeChunk::evaluateOperation
+	Opcode opcode = static_cast<Opcode>(_bytecode->readTypedUint16());
+	Common::String result;
+
+	switch (opcode) {
+	case kOpcodeIf:
+		return decompileIf();
+
+	case kOpcodeIfElse:
+		return decompileIfElse();
+
+	case kOpcodeAssignVariable:
+		return decompileAssign();
+
+	case kOpcodeOr:
+	case kOpcodeXor:
+	case kOpcodeAnd:
+	case kOpcodeEquals:
+	case kOpcodeNotEquals:
+	case kOpcodeLessThan:
+	case kOpcodeGreaterThan:
+	case kOpcodeLessThanOrEqualTo:
+	case kOpcodeGreaterThanOrEqualTo:
+	case kOpcodeAdd:
+	case kOpcodeSubtract:
+	case kOpcodeMultiply:
+	case kOpcodeDivide:
+	case kOpcodeModulo:
+		return decompileBinaryOperation(opcode);
+
+	case kOpcodeNegate:
+		return decompileUnaryOperation();
+
+	case kOpcodeCallFunction:
+		return decompileFunctionCall(false);
+
+	case kOpcodeCallMethod:
+		return decompileMethodCall(false);
+
+	case kOpcodeDeclareLocals:
+		return decompileDeclareLocals();
+
+	case kOpcodeReturn:
+		return decompileReturn();
+
+	case kOpcodeReturnNoValue:
+		return decompileReturnNoValue();
+
+	case kOpcodeWhile:
+		return decompileWhileLoop();
+
+	case kOpcodeCallFunctionInVariable:
+		return decompileFunctionCall(true);
+
+	case kOpcodeCallMethodInVariable:
+		return decompileMethodCall(true);
+
+	default:
+		return Common::String::format("<unknown_opcode_%d>", static_cast<uint>(opcode));
+	}
+}
+
+Common::String CodeChunkDecompiler::decompileValue() {
+	// Mirrors CodeChunk::evaluateValue
+	OperandType operandType = static_cast<OperandType>(_bytecode->readTypedUint16());
+
+	switch (operandType) {
+	case kOperandTypeBool: {
+		int b = _bytecode->readTypedByte();
+		return b ? "TRUE" : "FALSE";
+	}
+
+	case kOperandTypeFloat: {
+		double f = _bytecode->readTypedDouble();
+		return Common::String::format("%g", f);
+	}
+
+	case kOperandTypeInt: {
+		int i = _bytecode->readTypedSint32();
+		return Common::String::format("%d", i);
+	}
+
+	case kOperandTypeString: {
+		// This doesn't escape quotes in the string.
+		uint size = _bytecode->readTypedUint16();
+		Common::String string = _bytecode->readString('\0', size);
+		return "\"" + string + "\"";
+	}
+
+	case kOperandTypeParamToken: {
+		uint paramToken = _bytecode->readTypedUint16();
+		return g_engine->formatParamTokenName(paramToken, false);
+	}
+
+	case kOperandTypeActorId: {
+		uint actorId = _bytecode->readTypedUint16();
+		Common::String actorName = g_engine->formatActorName(actorId, true, false);
+		return "@" + actorName;
+	}
+
+	case kOperandTypeTime: {
+		double time = _bytecode->readTypedTime();
+		return Common::String::format("%g", time);
+	}
+
+	case kOperandTypeVariable: {
+		return decompileVariable();
+	}
+
+	case kOperandTypeFunctionId: {
+		uint functionId = _bytecode->readTypedUint16();
+		return g_engine->formatFunctionName(functionId, false);
+	}
+
+	case kOperandTypeMethodId: {
+		BuiltInMethod methodId = static_cast<BuiltInMethod>(_bytecode->readTypedUint16());
+		return Common::String(builtInMethodToStr(methodId));
+	}
+
+	default:
+		return Common::String::format("<unknown_value_type_%d>", static_cast<uint>(operandType));
+	}
+}
+
+Common::String CodeChunkDecompiler::decompileVariable() {
+	// Mirrors CodeChunk::evaluateVariable and CodeChunk::readAndReturnVariable
+	uint id = _bytecode->readTypedUint16();
+	VariableScope scope = static_cast<VariableScope>(_bytecode->readTypedUint16());
+
+	switch (scope) {
+	case kVariableScopeGlobal:
+		// Variable names are NOT prefixed with "@" like actor names are.
+		return g_engine->formatVariableName(id, false);
+
+	case kVariableScopeLocal:
+		// Locals never have saved names, so just give a generic name.
+		return Common::String::format("local_%d", id);
+
+	case kVariableScopeParameter:
+		// Params never have saved names, so just give a generic name.
+		return Common::String::format("param_%d", id);
+
+	case kVariableScopeIndirectParameter: {
+		Common::String indexExpr = decompileExpression();
+		return Common::String::format("indirect_param(%d, %s)", id, indexExpr.c_str());
+	}
+
+	default:
+		return Common::String::format("<unknown_var_scope_%d_%d>", static_cast<uint>(scope), id);
+	}
+}
+
+Common::String CodeChunkDecompiler::decompileIf() {
+	// Mirrors CodeChunk::evaluateIf
+	Common::String condition = decompileExpression();
+	Common::String result = "if (" + condition + ") then\n";
+
+	// Increase indent level for the block.
+	_indentLevel++;
+	result += decompileNextBlock();
+	_indentLevel--;
+
+	result += Common::String::format("%*s", getIndentSize(_indentLevel), "") + "endif";
+	return result;
+}
+
+Common::String CodeChunkDecompiler::decompileIfElse() {
+	// Mirrors CodeChunk::evaluateIfElse
+	Common::String condition = decompileExpression();
+	Common::String result = "if (" + condition + ") then\n";
+
+	// Decompile true branch.
+	_indentLevel++;
+	result += decompileNextBlock();
+	_indentLevel--;
+
+	// Decompile false branch.
+	_indentLevel++;
+	Common::String elseBlock = decompileNextBlock();
+	_indentLevel--;
+
+	// Only print "else" if the else block is not empty.
+	if (!elseBlock.empty()) {
+		result += Common::String::format("%*s", getIndentSize(_indentLevel), "") + "else\n";
+		result += elseBlock;
+	}
+
+	result += Common::String::format("%*s", getIndentSize(_indentLevel), "") + "endif";
+	return result;
+}
+
+Common::String CodeChunkDecompiler::decompileAssign() {
+	// Mirrors CodeChunk::evaluateAssign
+	Common::String variable = decompileVariable();
+	Common::String value = decompileExpression();
+	return variable + " = " + value;
+}
+
+Common::String CodeChunkDecompiler::decompileBinaryOperation(Opcode op) {
+	// Mirrors CodeChunk::evaluateBinaryOperation
+	Common::String lhs = decompileExpression();
+	Common::String rhs = decompileExpression();
+	Common::String opStr;
+
+	switch (op) {
+	case kOpcodeOr: opStr = " or "; break;
+	case kOpcodeXor: opStr = " xor "; break;
+	case kOpcodeAnd: opStr = " and "; break;
+	case kOpcodeEquals: opStr = " == "; break;
+	case kOpcodeNotEquals: opStr = " != "; break;
+	case kOpcodeLessThan: opStr = " < "; break;
+	case kOpcodeGreaterThan: opStr = " > "; break;
+	case kOpcodeLessThanOrEqualTo: opStr = " <= "; break;
+	case kOpcodeGreaterThanOrEqualTo: opStr = " >= "; break;
+	case kOpcodeAdd: opStr = " + "; break;
+	case kOpcodeSubtract: opStr = " - "; break;
+	case kOpcodeMultiply: opStr = " * "; break;
+	case kOpcodeDivide: opStr = " / "; break;
+	case kOpcodeModulo: opStr = " % "; break;
+	default: opStr = Common::String::format(" <unknown_op_%u> ", static_cast<uint>(op)); break;
+	}
+
+	return "(" + lhs + opStr + rhs + ")";
+}
+
+Common::String CodeChunkDecompiler::decompileUnaryOperation() {
+	// Mirrors CodeChunk::evaluateUnaryOperation
+	Common::String value = decompileExpression();
+	return "-" + value;
+}
+
+void CodeChunkDecompiler::appendDecompiledParameterList(Common::String &result, uint paramCount) {
+	for (uint parameterIndex = 0; parameterIndex < paramCount; ++parameterIndex) {
+		if (parameterIndex > 0) {
+			result += ", ";
+		}
+		result += decompileExpression();
+	}
+}
+
+Common::String CodeChunkDecompiler::decompileFunctionCall(bool isIndirect) {
+	// Mirrors CodeChunk::evaluateFunctionCall
+	uint functionId;
+	uint paramCount;
+
+	if (isIndirect) {
+		paramCount = _bytecode->readTypedUint16();
+		Common::String functionVar = decompileExpression();
+		Common::String result = functionVar + "(";
+		appendDecompiledParameterList(result, paramCount);
+		result += ")";
+		return result;
+	} else {
+		functionId = _bytecode->readTypedUint16();
+		paramCount = _bytecode->readTypedUint16();
+		return decompileFunctionCall(functionId, paramCount);
+	}
+}
+
+Common::String CodeChunkDecompiler::decompileFunctionCall(uint functionId, uint paramCount) {
+	// Mirrors CodeChunk::evaluateFunctionCall
+	Common::String functionName = g_engine->formatFunctionName(functionId, false);
+	Common::String result = functionName + "(";
+	appendDecompiledParameterList(result, paramCount);
+	result += ")";
+	return result;
+}
+
+Common::String CodeChunkDecompiler::decompileMethodCall(bool isIndirect) {
+	// Mirrors CodeChunk::evaluateMethodCall
+	BuiltInMethod method;
+	uint paramCount;
+
+	if (isIndirect) {
+		paramCount = _bytecode->readTypedUint16();
+		Common::String methodVar = decompileExpression();
+		Common::String target = decompileExpression();
+		Common::String result = target + ".(" + methodVar + ")(";
+		appendDecompiledParameterList(result, paramCount);
+		result += ")";
+		return result;
+	} else {
+		method = static_cast<BuiltInMethod>(_bytecode->readTypedUint16());
+		paramCount = _bytecode->readTypedUint16();
+		return decompileMethodCall(method, paramCount);
+	}
+}
+
+Common::String CodeChunkDecompiler::decompileMethodCall(BuiltInMethod method, uint paramCount) {
+	// Mirrors CodeChunk::evaluateMethodCall
+	Common::String target = decompileExpression();
+	Common::String result = target + "." + builtInMethodToStr(method) + "(";
+	appendDecompiledParameterList(result, paramCount);
+	result += ")";
+	return result;
+}
+
+Common::String CodeChunkDecompiler::decompileDeclareLocals() {
+	// Mirrors CodeChunk::evaluateDeclareLocals
+	uint localCount = _bytecode->readTypedUint16();
+	Common::String result = "declare ";
+	for (uint i = 0; i < localCount; ++i) {
+		if (i > 0) {
+			result += ", ";
+		}
+		result += Common::String::format("local_%d", i + 1);
+	}
+	return result;
+}
+
+Common::String CodeChunkDecompiler::decompileReturn() {
+	// Mirrors CodeChunk::evaluateReturn
+	Common::String value = decompileExpression();
+	return "return " + value;
+}
+
+Common::String CodeChunkDecompiler::decompileReturnNoValue() {
+	// Mirrors CodeChunk::evaluateReturnNoValue
+	return "return";
+}
+
+Common::String CodeChunkDecompiler::decompileWhileLoop() {
+	// Mirrors CodeChunk::evaluateWhileLoop
+	Common::String condition = decompileExpression();
+	Common::String result = "while (" + condition + ") do\n";
+
+	// Temporarily increase indent level for the block.
+	_indentLevel++;
+	result += decompileNextBlock();
+	_indentLevel--;
+
+	result += Common::String::format("%*s", getIndentSize(_indentLevel), "") + "endwhile";
+	return result;
+}
+
 } // End of namespace MediaStation
diff --git a/engines/mediastation/mediascript/codechunk.h b/engines/mediastation/mediascript/codechunk.h
index c6923bdaf89..5b52b148ab5 100644
--- a/engines/mediastation/mediascript/codechunk.h
+++ b/engines/mediastation/mediascript/codechunk.h
@@ -79,6 +79,47 @@ private:
 	uint _debugIndentLevel = 0;
 };
 
+// These decompilation methods are intended to directly mirror the relevant code chunk
+// methods from the original. While this does duplicate some opcode parsing and such,
+// given that this part of the engine is pretty stable, the additional complexity
+// (and departure from the disassembly) of a full decoder/visitor pattern was judged
+// to not be worth it.
+//
+// The decompiled script format is based on the scripts shipped as debug statements
+// in If You Give a Mouse a Cookie, which debug-printed every script line before executing
+// the bytecode.
+class CodeChunkDecompiler {
+public:
+	CodeChunkDecompiler(ParameterReadStream *bytecode, uint indentLevel = 0);
+
+	Common::String decompileNextBlock();
+
+private:
+	Common::String decompileExpression();
+	Common::String decompileExpression(ExpressionType expressionType);
+	Common::String decompileOperation();
+	Common::String decompileValue();
+	Common::String decompileVariable();
+
+	Common::String decompileIf();
+	Common::String decompileIfElse();
+	Common::String decompileAssign();
+	Common::String decompileBinaryOperation(Opcode op);
+	Common::String decompileUnaryOperation();
+	Common::String decompileFunctionCall(bool isIndirect = false);
+	Common::String decompileFunctionCall(uint functionId, uint paramCount);
+	Common::String decompileMethodCall(bool isIndirect = false);
+	Common::String decompileMethodCall(BuiltInMethod method, uint paramCount);
+	void appendDecompiledParameterList(Common::String &result, uint paramCount);
+	Common::String decompileDeclareLocals();
+	Common::String decompileReturn();
+	Common::String decompileReturnNoValue();
+	Common::String decompileWhileLoop();
+
+	uint _indentLevel = 0;
+	ParameterReadStream *_bytecode = nullptr;
+};
+
 } // End of namespace MediaStation
 
 #endif
diff --git a/engines/mediastation/mediascript/function.cpp b/engines/mediastation/mediascript/function.cpp
index be12a23f2f8..8fe2133f552 100644
--- a/engines/mediastation/mediascript/function.cpp
+++ b/engines/mediastation/mediascript/function.cpp
@@ -79,6 +79,18 @@ ScriptValue ScriptFunction::execute(Common::Array<ScriptValue> &args) {
 	return returnValue;
 }
 
+Common::String ScriptFunction::decompile() const {
+	Common::String functionName = g_engine->formatFunctionName(_id, false);
+	Common::String result = "Function " + functionName + "\n";
+	Common::SeekableReadStream *baseStream = new Common::MemoryReadStream(_bytecodeBuffer, _bytecodeSize, DisposeAfterUse::NO);
+	ParameterReadStream *bytecodeStream = static_cast<ParameterReadStream *>(baseStream);
+	// The decompiled code will be put in an indented block, so start with one level of indentation.
+	CodeChunkDecompiler decompiler(bytecodeStream, 1);
+	result += decompiler.decompileNextBlock();
+	delete baseStream;
+	result += "End // " + functionName + "\n";
+	return result;
+}
 
 FunctionManager::~FunctionManager() {
 	for (auto it = _functions.begin(); it != _functions.end(); ++it) {
@@ -257,6 +269,10 @@ ScriptValue FunctionManager::call(uint functionId, Common::Array<ScriptValue> &a
 	return returnValue;
 }
 
+ScriptFunction *FunctionManager::getFunctionById(uint functionId) {
+	return _functions.getValOrDefault(functionId, nullptr);
+}
+
 void FunctionManager::script_GetPlatform(Common::Array<ScriptValue> &args, ScriptValue &returnValue) {
 	Common::Platform platform = g_engine->getPlatform();
 	switch (platform) {
diff --git a/engines/mediastation/mediascript/function.h b/engines/mediastation/mediascript/function.h
index 8914eda6d2a..1da34af9610 100644
--- a/engines/mediastation/mediascript/function.h
+++ b/engines/mediastation/mediascript/function.h
@@ -43,6 +43,8 @@ public:
 	~ScriptFunction();
 
 	ScriptValue execute(Common::Array<ScriptValue> &args);
+	Common::String decompile() const;
+	uint32 bytecodeSize() const { return _bytecodeSize; }
 
 	uint _contextId = 0;
 	uint _id = 0;
@@ -53,12 +55,15 @@ private:
 };
 
 class FunctionManager : public ParameterClient {
+friend class Debugger;
+
 public:
 	FunctionManager() {};
 	virtual ~FunctionManager();
 
 	virtual bool attemptToReadFromStream(Chunk &chunk, uint sectionType) override;
 	ScriptValue call(uint functionId, Common::Array<ScriptValue> &args);
+	ScriptFunction *getFunctionById(uint functionId);
 	void deleteFunctionsForContext(uint contextId);
 
 	uint _scriptBlockCallDepth = 0;
diff --git a/engines/mediastation/mediascript/scriptconstants.cpp b/engines/mediastation/mediascript/scriptconstants.cpp
index d3349029b7f..ee821fe5438 100644
--- a/engines/mediastation/mediascript/scriptconstants.cpp
+++ b/engines/mediastation/mediascript/scriptconstants.cpp
@@ -163,37 +163,37 @@ const char *builtInFunctionToStr(uint function) {
 	case kDrawingFunction:
 		return "Drawing";
 	case kLegacy_RandomFunction:
-		return "Legacy Random";
+		return "Random_Legacy";
 	case kLegacy_TimeOfDayFunction:
-		return "Legacy TimeOfDay";
+		return "TimeOfDay_Legacy";
 	case kLegacy_EffectTransitionFunction:
-		return "Legacy EffectTransition";
+		return "EffectTransition_Legacy";
 	case kLegacy_EffectTransitionOnSyncFunction:
-		return "Legacy EffectTransitionOnSync";
+		return "EffectTransitionOnSync_Legacy";
 	case kLegacy_PlatformFunction:
-		return "Legacy Platform";
+		return "Platform_Legacy";
 	case kLegacy_SquareRootFunction:
-		return "Legacy SquareRoot";
+		return "SquareRoot_Legacy";
 	case kLegacy_GetUniqueRandomFunction:
-		return "Legacy GetUniqueRandom";
+		return "GetUniqueRandom_Legacy";
 	case kLegacy_GetCurrentRunTimeFunction:
-		return "Legacy GetCurrentRunTime";
+		return "GetCurrentRunTime_Legacy";
 	case kLegacy_SetGammaCorrectionFunction:
-		return "Legacy SetGammaCorrection";
+		return "SetGammaCorrection_Legacy";
 	case kLegacy_GetDefaultGammaCorrectionFunction:
-		return "Legacy GetDefaultGammaCorrection";
+		return "GetDefaultGammaCorrection_Legacy";
 	case kLegacy_GetCurrentGammaCorrectionFunction:
-		return "Legacy GetCurrentGammaCorrection";
+		return "GetCurrentGammaCorrection_Legacy";
 	case kLegacy_DebugPrintFunction:
-		return "DebugPrint";
+		return "DebugPrint_Legacy";
 	case kLegacy_SetAudioVolumeFunction:
-		return "Legacy SetAudioVolume";
+		return "SetAudioVolume_Legacy";
 	case kLegacy_GetAudioVolumeFunction:
-		return "Legacy GetAudioVolume";
+		return "GetAudioVolume_Legacy";
 	case kLegacy_SystemLanguagePreferenceFunction:
-		return "Legacy SystemLanguagePreference";
+		return "SystemLanguagePreference_Legacy";
 	default:
-		return "UNKNOWN";
+		return "func";
 	}
 }
 
@@ -466,7 +466,7 @@ const char *eventTypeToStr(EventType type) {
 	case kTimerServiceAlarmEvent:
 		return "TimerServiceAlarm";
 	case kTimerScriptEvent:
-		return "ScriptTimer";
+		return "ScriptTime";
 	case kMouseDownEvent:
 		return "MouseDown";
 	case kMouseUpEvent:
diff --git a/engines/mediastation/mediascript/scriptresponse.cpp b/engines/mediastation/mediascript/scriptresponse.cpp
index 436126d2e7c..899440287c4 100644
--- a/engines/mediastation/mediascript/scriptresponse.cpp
+++ b/engines/mediastation/mediascript/scriptresponse.cpp
@@ -68,4 +68,46 @@ int64 ScriptResponse::lengthInBytes() const {
 	return _bytecodeSize;
 }
 
+Common::String ScriptResponse::decompile() const {
+	if (_bytecodeBuffer == nullptr || _bytecodeSize == 0) {
+		return "<no code>";
+	}
+
+	// Get the code body first.
+	Common::SeekableReadStream *baseStream = new Common::MemoryReadStream(_bytecodeBuffer, _bytecodeSize, DisposeAfterUse::NO);
+	ParameterReadStream *bytecodeStream = static_cast<ParameterReadStream *>(baseStream);
+	// This code will be put in an indented On ... block, so start with one level of indentation.
+	CodeChunkDecompiler decompiler(bytecodeStream, 1);
+	Common::String decompiledBody = decompiler.decompileNextBlock();
+	delete baseStream;
+
+	// The argument format is determined by the event type, not inferred from the value type.
+	Common::String argumentText;
+	const ScriptValueType argumentType = _argumentValue.getType();
+	switch (_type) {
+	case kTimerScriptEvent:
+		argumentText = Common::String::format("%g", _argumentValue.asTime());
+		break;
+
+	case kKeyDownEvent:
+		// Key arguments are ASCII codes stored as floats but always whole numbers.
+		argumentText = Common::String::format("%d", static_cast<int>(_argumentValue.asFloat()));
+		break;
+
+	default:
+		// Don't set any argument text, as the arg is most likely empty.
+		break;
+	}
+
+	// A newline is not needed after the decompiled code because the decompiled code string
+	// already ends in a newline.
+	const bool hasArgument = (argumentType != kScriptValueTypeEmpty);
+	Common::String argumentComment = hasArgument
+		? Common::String::format(" // %s", _argumentValue.getDebugString(false).c_str())
+		: "";
+	return Common::String::format("On %s %s%s\n%sEnd\n",
+		eventTypeToStr(_type), argumentText.c_str(),
+		argumentComment.c_str(), decompiledBody.c_str());
+}
+
 } // End of namespace MediaStation
diff --git a/engines/mediastation/mediascript/scriptresponse.h b/engines/mediastation/mediascript/scriptresponse.h
index 66185dacced..e59f762468e 100644
--- a/engines/mediastation/mediascript/scriptresponse.h
+++ b/engines/mediastation/mediascript/scriptresponse.h
@@ -38,6 +38,7 @@ public:
 
 	int64 lengthInBytes() const;
 	ScriptValue execute(uint actorId);
+	Common::String decompile() const;
 	EventType _type;
 	ScriptValue _argumentValue;
 
diff --git a/engines/mediastation/mediascript/scriptvalue.cpp b/engines/mediastation/mediascript/scriptvalue.cpp
index 4c943bd26b7..9ae3fdb7a25 100644
--- a/engines/mediastation/mediascript/scriptvalue.cpp
+++ b/engines/mediastation/mediascript/scriptvalue.cpp
@@ -320,7 +320,7 @@ BuiltInMethod ScriptValue::asMethodId() const {
 	}
 }
 
-Common::String ScriptValue::getDebugString() const {
+Common::String ScriptValue::getDebugString(bool includeDefaultName) const {
 	switch (getType()) {
 	case kScriptValueTypeEmpty:
 		return "empty";
@@ -332,7 +332,7 @@ Common::String ScriptValue::getDebugString() const {
 		return Common::String::format("float: %f", asFloat());
 
 	case kScriptValueTypeActorId: {
-		Common::String actorName = g_engine->formatActorName(asActorId(), true);
+		Common::String actorName = g_engine->formatActorName(asActorId(), true, includeDefaultName);
 		return Common::String::format("actor: %s", actorName.c_str());
 	}
 
@@ -340,7 +340,7 @@ Common::String ScriptValue::getDebugString() const {
 		return Common::String::format("time: %f", asTime());
 
 	case kScriptValueTypeParamToken: {
-		Common::String tokenName = g_engine->formatParamTokenName(asParamToken());
+		Common::String tokenName = g_engine->formatParamTokenName(asParamToken(), includeDefaultName);
 		return Common::String::format("token: %s", tokenName.c_str());
 	}
 
@@ -354,7 +354,7 @@ Common::String ScriptValue::getDebugString() const {
 	}
 
 	case kScriptValueTypeFunctionId: {
-		Common::String functionName = g_engine->formatFunctionName(asFunctionId());
+		Common::String functionName = g_engine->formatFunctionName(asFunctionId(), includeDefaultName);
 		return Common::String::format("function: %s", functionName.c_str());
 	}
 
diff --git a/engines/mediastation/mediascript/scriptvalue.h b/engines/mediastation/mediascript/scriptvalue.h
index cbd366d5985..1037ef1b241 100644
--- a/engines/mediastation/mediascript/scriptvalue.h
+++ b/engines/mediastation/mediascript/scriptvalue.h
@@ -73,7 +73,7 @@ public:
 	void setToMethodId(BuiltInMethod methodId);
 	BuiltInMethod asMethodId() const;
 
-	Common::String getDebugString() const;
+	Common::String getDebugString(bool includeDefaultName = true) const;
 
 	void operator=(const ScriptValue &other);
 	bool operator==(const ScriptValue &other) const;
diff --git a/engines/mediastation/mediastation.cpp b/engines/mediastation/mediastation.cpp
index 00c58c75875..8b2bcb94e69 100644
--- a/engines/mediastation/mediastation.cpp
+++ b/engines/mediastation/mediastation.cpp
@@ -22,6 +22,7 @@
 #include "common/config-manager.h"
 
 #include "mediastation/mediastation.h"
+#include "mediastation/debugger.h"
 #include "mediastation/debugchannels.h"
 #include "mediastation/detection.h"
 #include "mediastation/boot.h"
@@ -53,6 +54,7 @@ MediaStationEngine::MediaStationEngine(OSystem *syst, const ADGameDescription *g
 }
 
 MediaStationEngine::~MediaStationEngine() {
+	// The base Engine cleans up the debugger.
 	_imtGod->destroyAllContexts();
 	delete _deviceOwner;
 	// _cacheManager->removeCache();
@@ -141,6 +143,8 @@ bool MediaStationEngine::hasFeature(EngineFeature f) const {
 }
 
 Common::Error MediaStationEngine::run() {
+	_debugger = new Debugger(this);
+	setDebugger(_debugger);
 	_eventLoop = new EventLoop;
 	_timerService = new TimerService;
 	_streamFeedManager = new StreamFeedManager;
diff --git a/engines/mediastation/mediastation.h b/engines/mediastation/mediastation.h
index 6ccc8412e12..f3fd80c5091 100644
--- a/engines/mediastation/mediastation.h
+++ b/engines/mediastation/mediastation.h
@@ -43,6 +43,7 @@
 #include "mediastation/context.h"
 #include "mediastation/cursors.h"
 #include "mediastation/datafile.h"
+#include "mediastation/debugger.h"
 #include "mediastation/detection.h"
 #include "mediastation/events.h"
 #include "mediastation/graphics.h"
@@ -58,6 +59,7 @@ class RootStage;
 class PixMapImage;
 class ImtGod;
 class EventLoop;
+class Debugger;
 
 // Most Media Station titles follow this file structure from the root directory
 // of the CD-ROM:
@@ -102,13 +104,13 @@ public:
 	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); }
-	Common::String formatFileName(uint fileId) { return _profile->formatFileName(fileId); }
-	Common::String formatVariableName(uint variableId) { return _profile->formatVariableName(variableId); }
-	Common::String formatParamTokenName(uint paramToken) { return _profile->formatParamTokenName(paramToken); }
-	Common::String formatAssetNameForChannelIdent(uint channelIdent) { return _profile->formatAssetNameForChannelIdent(channelIdent); }
+	Common::String formatActorName(uint actorId, bool attemptToGetType = false, bool includeDefaultName = true) { return _profile->formatActorName(actorId, attemptToGetType, includeDefaultName); }
+	Common::String formatActorName(const Actor *actor, bool includeDefaultName = true) { return _profile->formatActorName(actor, includeDefaultName); }
+	Common::String formatFunctionName(uint functionId, bool includeDefaultName = true) { return _profile->formatFunctionName(functionId, includeDefaultName); }
+	Common::String formatFileName(uint fileId, bool includeDefaultName = true) { return _profile->formatFileName(fileId, includeDefaultName); }
+	Common::String formatVariableName(uint variableId, bool includeDefaultName = true) { return _profile->formatVariableName(variableId, includeDefaultName); }
+	Common::String formatParamTokenName(uint paramToken, bool includeDefaultName = true) { return _profile->formatParamTokenName(paramToken, includeDefaultName); }
+	Common::String formatAssetNameForChannelIdent(uint channelIdent, bool includeDefaultName = true) { return _profile->formatAssetNameForChannelIdent(channelIdent, includeDefaultName); }
 
 	SpatialEntity *getMouseInsideHotspot() { return _mouseInsideHotspot; }
 	void setMouseInsideHotspot(SpatialEntity *entity) { _mouseInsideHotspot = entity; }
@@ -133,6 +135,7 @@ private:
 	SpatialEntity *_mouseInsideHotspot = nullptr;
 	SpatialEntity *_mouseDownHotspot = nullptr;
 
+	Debugger *_debugger = nullptr;
 	EventLoop *_eventLoop = nullptr;
 	TimerService *_timerService = nullptr;
 	StreamFeedManager *_streamFeedManager = nullptr;
@@ -157,6 +160,7 @@ private:
 
 class ImtGod : public ChannelClient {
 friend class EventLoop;
+friend class Debugger;
 
 public:
 	ImtGod(MediaStationEngine *vm);
diff --git a/engines/mediastation/module.mk b/engines/mediastation/module.mk
index 468c31581ab..d69698f2b8d 100644
--- a/engines/mediastation/module.mk
+++ b/engines/mediastation/module.mk
@@ -26,6 +26,7 @@ MODULE_OBJS = \
 	context.o \
 	cursors.o \
 	datafile.o \
+	debugger.o \
 	events.o \
 	graphics.o \
 	mediascript/codechunk.o \
diff --git a/engines/mediastation/profile.cpp b/engines/mediastation/profile.cpp
index 6a2b3b5b902..df2d1db2b66 100644
--- a/engines/mediastation/profile.cpp
+++ b/engines/mediastation/profile.cpp
@@ -211,26 +211,30 @@ void Profile::parseScriptConstantInfo(const Common::String &line) {
 		__func__, line.c_str(), constantInfo.name.c_str(), constantInfo.value.c_str());
 }
 
-Common::String Profile::formatActorName(uint actorId, bool attemptToGetType) {
-	// If requested, try to get the actor type by looking up the loaded actor
+Common::String Profile::formatActorName(uint actorId, bool attemptToGetType, bool includeDefaultName) {
+	// If requested, try to get the actor type by looking up the loaded actor.
 	if (attemptToGetType) {
 		Actor *actor = g_engine->getImtGod()->getActorById(actorId);
 		if (actor != nullptr) {
-			return formatActorName(actor);
+			return formatActorName(actor, includeDefaultName);
 		}
 	}
 
 	Common::String formattedName;
 	const Common::String &actorName = _assets.getValOrDefault(actorId).name;
 	if (!actorName.empty()) {
-		formattedName = Common::String::format("%s (%d)", actorName.c_str(), actorId);
+		if (includeDefaultName) {
+			formattedName = Common::String::format("%s_%d)", actorName.c_str(), actorId);
+		} else {
+			formattedName = actorName;
+		}
 	} else {
-		formattedName = Common::String::format("%d", actorId);
+		formattedName = Common::String::format("actor_%d", actorId);
 	}
 	return formattedName;
 }
 
-Common::String Profile::formatActorName(const Actor *actor) {
+Common::String Profile::formatActorName(const Actor *actor, bool includeDefaultName) {
 	if (actor == nullptr) {
 		return "<null>";
 	}
@@ -238,15 +242,19 @@ Common::String Profile::formatActorName(const Actor *actor) {
 	Common::String formattedName;
 	const Common::String &actorName = _assets.getValOrDefault(actor->id()).name;
 	if (!actorName.empty()) {
-		formattedName = Common::String::format("%s [%s %d]", actorName.c_str(), actorTypeToStr(actor->type()), actor->id());
+		if (includeDefaultName) {
+			formattedName = Common::String::format("%s [%s_%d]", actorName.c_str(), actorTypeToStr(actor->type()), actor->id());
+		} else {
+			formattedName = actorName;
+		}
 	} else {
 		// Even if we don't have a name, try to give at least some visibility by including the type.
-		formattedName = Common::String::format("%s %d", actorTypeToStr(actor->type()), actor->id());
+		formattedName = Common::String::format("%s_%d", actorTypeToStr(actor->type()), actor->id());
 	}
 	return formattedName;
 }
 
-Common::String Profile::formatFunctionName(uint functionId) {
+Common::String Profile::formatFunctionName(uint functionId, bool includeDefaultName) {
 	// Only in PROFILE._ST, the function ID is reported with 19900 added,
 	// so function 100 would be reported as 20000. But in bytecode, the
 	// zero-based ID is used.
@@ -254,49 +262,66 @@ Common::String Profile::formatFunctionName(uint functionId) {
 	uint offsetFunctionId = functionId + 19900;
 	const Common::String &functionName = _assets.getValOrDefault(offsetFunctionId).name;
 	if (!functionName.empty()) {
-		// Report the function ID as it appears in bytecode, so without the odd offset added.
-		formattedName = Common::String::format("%s (%d)", functionName.c_str(), functionId);
+		if (includeDefaultName) {
+			// Report the function ID as it appears in bytecode, so without the odd offset added.
+			formattedName = Common::String::format("%s (%d)", functionName.c_str(), functionId);
+		} else {
+			formattedName = functionName;
+		}
 	} 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(functionId), functionId);
+		// TODO: Check and make sure if it IS a built-in function or if we need to return the raw function ID.
+		formattedName = Common::String::format("%s_%d", builtInFunctionToStr(functionId), functionId);
 	}
 	return formattedName;
 }
 
-Common::String Profile::formatFileName(uint fileId) {
+Common::String Profile::formatFileName(uint fileId, bool includeDefaultName) {
 	Common::String formattedName;
 	const Common::String &fileName = _files.getValOrDefault(fileId).name;
 	if (!fileName.empty()) {
-		formattedName = Common::String::format("%s (%d)", fileName.c_str(), fileId);
+		if (includeDefaultName) {
+			formattedName = Common::String::format("%s (%d)", fileName.c_str(), fileId);
+		} else {
+			formattedName = fileName;
+		}
 	} else {
 		formattedName = Common::String::format("%d", fileId);
 	}
 	return formattedName;
 }
 
-Common::String Profile::formatVariableName(uint variableId) {
+Common::String Profile::formatVariableName(uint variableId, bool includeDefaultName) {
 	Common::String formattedName;
 	const Common::String &variableName = _variables.getValOrDefault(variableId).name;
 	if (!variableName.empty()) {
-		formattedName = Common::String::format("%s (%d)", variableName.c_str(), variableId);
+		if (includeDefaultName) {
+			formattedName = Common::String::format("%s (%d)", variableName.c_str(), variableId);
+		} else {
+			formattedName = variableName;
+		}
 	} else {
-		formattedName = Common::String::format("%d", variableId);
+		formattedName = Common::String::format("global_%d", variableId);
 	}
 	return formattedName;
 }
 
-Common::String Profile::formatParamTokenName(uint paramToken) {
+Common::String Profile::formatParamTokenName(uint paramToken, bool includeDefaultName) {
 	Common::String formattedName;
 	const Common::String &paramTokenName = _paramTokens.getValOrDefault(paramToken).name;
 	if (!paramTokenName.empty()) {
-		formattedName = Common::String::format("%s (%d)", paramTokenName.c_str(), paramToken);
+		if (includeDefaultName) {
+			formattedName = Common::String::format("%s (%d)", paramTokenName.c_str(), paramToken);
+		} else {
+			formattedName = paramTokenName;
+		}
 	} else {
 		formattedName = Common::String::format("%d", paramToken);
 	}
 	return formattedName;
 }
 
-Common::String Profile::formatAssetNameForChannelIdent(uint channelIdentAsTag) {
+Common::String Profile::formatAssetNameForChannelIdent(uint channelIdentAsTag, bool includeDefaultName) {
 	Common::String formattedName;
 	if (channelIdentAsTag == MKTAG('i', 'g', 'o', 'd')) {
 		formattedName = "ImtGod";
@@ -305,7 +330,7 @@ Common::String Profile::formatAssetNameForChannelIdent(uint channelIdentAsTag) {
 		uint channelIdentAsInt = strtol(channelIdentAsString.c_str(), nullptr, 16);
 		if (_channelIdentsAsIntToAssetId.contains(channelIdentAsInt)) {
 			uint assetId = _channelIdentsAsIntToAssetId.getVal(channelIdentAsInt);
-			formattedName = Common::String::format("%s [%s]", channelIdentAsString.c_str(), formatActorName(assetId).c_str());
+			formattedName = Common::String::format("%s [%s]", channelIdentAsString.c_str(), formatActorName(assetId, false, includeDefaultName).c_str());
 		} else {
 			formattedName = Common::String::format("%s", tag2str(channelIdentAsTag));
 		}
diff --git a/engines/mediastation/profile.h b/engines/mediastation/profile.h
index a9266173047..d70b969691a 100644
--- a/engines/mediastation/profile.h
+++ b/engines/mediastation/profile.h
@@ -73,14 +73,14 @@ class Profile {
 public:
 	void load();
 
-	Common::String formatActorName(uint actorId, bool attemptToGetType = false);
-	Common::String formatActorName(const Actor *actor);
-
-	Common::String formatFunctionName(uint assetId);
-	Common::String formatFileName(uint fileId);
-	Common::String formatVariableName(uint variableId);
-	Common::String formatParamTokenName(uint paramToken);
-	Common::String formatAssetNameForChannelIdent(uint channelIdent);
+	Common::String formatActorName(uint actorId, bool attemptToGetType = false, bool includeDefaultName = true);
+	Common::String formatActorName(const Actor *actor, bool includeDefaultName = true);
+
+	Common::String formatFunctionName(uint assetId, bool includeDefaultName = true);
+	Common::String formatFileName(uint fileId, bool includeDefaultName = true);
+	Common::String formatVariableName(uint variableId, bool includeDefaultName = true);
+	Common::String formatParamTokenName(uint paramToken, bool includeDefaultName = true);
+	Common::String formatAssetNameForChannelIdent(uint channelIdent, bool includeDefaultName = true);
 
 	const Common::String &getFileName(uint16 fileId) const { return _files.getValOrDefault(fileId).name; }
 	const Common::String &getResourceName(uint16 resourceId) const { return _paramTokens.getValOrDefault(resourceId).name; }


Commit: 2d8cda6bd8b42d20a3ecd60eb39cfd58b240f9d1
    https://github.com/scummvm/scummvm/commit/2d8cda6bd8b42d20a3ecd60eb39cfd58b240f9d1
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-07-01T15:03:20-04:00

Commit Message:
MEDIASTATION: Implement hardcoded constellations minigame

Changed paths:
  A engines/mediastation/actors/dotgame.cpp
  A engines/mediastation/actors/dotgame.h
    engines/mediastation/actor.cpp
    engines/mediastation/actor.h
    engines/mediastation/context.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 86bc52f0b4a..534570433d2 100644
--- a/engines/mediastation/actor.cpp
+++ b/engines/mediastation/actor.cpp
@@ -54,8 +54,8 @@ const char *actorTypeToStr(ActorType type) {
 		return "Sprite";
 	case kActorTypeLKZazu:
 		return "LKZazu";
-	case kActorTypeLKConstellations:
-		return "LKConstellations";
+	case kActorTypeDotGame:
+		return "DotGame";
 	case kActorTypeDocument:
 		return "Document";
 	case kActorTypeDiskImage:
diff --git a/engines/mediastation/actor.h b/engines/mediastation/actor.h
index 9cd27cd1c47..e1c54463a1e 100644
--- a/engines/mediastation/actor.h
+++ b/engines/mediastation/actor.h
@@ -48,7 +48,7 @@ enum ActorType {
 	kActorTypeHotspot = 0x000b, // HSP
 	kActorTypeSprite = 0x000e, // SPR
 	kActorTypeLKZazu = 0x000f,
-	kActorTypeLKConstellations = 0x0010,
+	kActorTypeDotGame = 0x0010,
 	kActorTypeDocument = 0x0011,
 	kActorTypeDiskImage = 0x001d,
 	kActorTypeCursor = 0x000c, // CSR
@@ -59,7 +59,6 @@ enum ActorType {
 	kActorTypeText = 0x001a, // TXT
 	kActorTypeFont = 0x001b, // FON
 	kActorTypeCamera = 0x001c, // CAM
-	kActorTypeDiskImageActor = 0x001d,
 	kActorTypeCanvas = 0x001e, // CVS
 	kActorTypeXsnd = 0x001f,
 	kActorTypeXsndMidi = 0x0020,
@@ -145,7 +144,16 @@ enum ActorHeaderSectionType {
 
 	// SPRITE FIELDS.
 	kActorHeaderSpriteClip = 0x03e9,
-	kActorHeaderDefaultSpriteClip = 0x03ea
+	kActorHeaderDefaultSpriteClip = 0x03ea,
+
+	// DOT GAME FIELDS.
+	kActorHeaderDotGameMaxDots = 0x001d,
+	kActorHeaderDotGameHelperSprite1 = 0x0514,
+	kActorHeaderDotGameHelperSprite2 = 0x0515,
+	kActorHeaderDotGameState = 0x0516,
+	kActorHeaderDotGameSpeed = 0x0517,
+	kActorHeaderDotGameLineThickness = 0x0518,
+	kActorHeaderDotGameColor = 0x0519,
 };
 
 enum CylindricalWrapMode : int;
diff --git a/engines/mediastation/actors/dotgame.cpp b/engines/mediastation/actors/dotgame.cpp
new file mode 100644
index 00000000000..d27e709efc5
--- /dev/null
+++ b/engines/mediastation/actors/dotgame.cpp
@@ -0,0 +1,337 @@
+/* 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/dotgame.h"
+#include "mediastation/datafile.h"
+#include "mediastation/debugchannels.h"
+#include "mediastation/mediastation.h"
+
+#include "graphics/paletteman.h"
+
+namespace MediaStation {
+
+void DotGameActor::readParameter(Chunk &chunk, ActorHeaderSectionType paramType) {
+	switch (paramType) {
+	case kActorHeaderDotGameMaxDots:
+		// Read all dot positions from stream.
+		_totalDots = chunk.readTypedUint16();
+		for (uint16 i = 0; i < _totalDots; i++) {
+			_dotPositions.push_back(chunk.readTypedPoint());
+		}
+		break;
+
+	case kActorHeaderEditable:
+		_isVisible = static_cast<bool>(chunk.readTypedByte());
+		break;
+
+	case kActorHeaderDotGameHelperSprite1:
+		_startHotspotId = chunk.readTypedUint16();
+		break;
+
+	case kActorHeaderDotGameHelperSprite2:
+		_endHotspotId = chunk.readTypedUint16();
+		break;
+
+	case kActorHeaderDotGameState:
+		_markerActorId = chunk.readTypedUint16();
+		break;
+
+	case kActorHeaderDotGameSpeed:
+		_speed = chunk.readTypedUint16();
+		break;
+
+	case kActorHeaderDotGameLineThickness:
+		_lineThickness = chunk.readTypedByte();
+		break;
+
+	case kActorHeaderDotGameColor:
+		_lineColorR = chunk.readTypedByte();
+		_lineColorG = chunk.readTypedByte();
+		_lineColorB = chunk.readTypedByte();
+		break;
+
+	default:
+		SpatialEntity::readParameter(chunk, paramType);
+	}
+}
+
+ScriptValue DotGameActor::callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) {
+	ScriptValue returnValue;
+	switch (methodId) {
+	case kDotGameResetMethod: {
+		ARGCOUNTCHECK(1);
+		int16 targetDot = static_cast<int16>(args[0].asFloat());
+		doReset(targetDot);
+		break;
+	}
+
+	case kDotGameShowMethod:
+		ARGCOUNTCHECK(0);
+		if (!_isVisible) {
+			_isVisible = true;
+			activateHelpers();
+			updateHelpers();
+			invalidateLocalBounds();
+		}
+		break;
+
+	case kDotGameHideMethod:
+		ARGCOUNTCHECK(0);
+		if (_isVisible) {
+			_isVisible = false;
+			deActivateHelpers();
+			invalidateLocalBounds();
+		}
+		break;
+
+	case kDotGameHitMethod:
+		ARGCOUNTCHECK(0);
+		doHit();
+		break;
+
+	default:
+		returnValue = Actor::callMethod(methodId, args);
+	}
+
+	return returnValue;
+}
+
+void DotGameActor::updateHelpers() {
+	if (!_isVisible) {
+		return;
+	} else if (_currentDotIndex >= _totalDots) {
+		warning("[%s] %s: Current dot count (%d) exceeds max dots (%d)", debugName(), __func__, _currentDotIndex, _totalDots);
+		return;
+	}
+
+	// Determine next dot position in the sequence, wrapping around to close the shape.
+	uint16 nextDotIndex = (_currentDotIndex + 1) % _totalDots;
+	Common::Point nextDotPosition = _dotPositions[nextDotIndex] + getBbox().origin();
+
+	Common::Array<ScriptValue> moveArgs;
+	ScriptValue xArg, yArg;
+	xArg.setToFloat(static_cast<double>(nextDotPosition.x));
+	moveArgs.push_back(xArg);
+	yArg.setToFloat(static_cast<double>(nextDotPosition.y));
+	moveArgs.push_back(yArg);
+	Actor *startHotspot = g_engine->getImtGod()->getActorById(_startHotspotId);
+	if (startHotspot != nullptr) {
+		startHotspot->callMethod(kSpatialCenterMoveToMethod, moveArgs);
+	}
+
+	Actor *endHotspot = g_engine->getImtGod()->getActorById(_endHotspotId);
+	if (endHotspot != nullptr) {
+		endHotspot->callMethod(kSpatialCenterMoveToMethod, moveArgs);
+	}
+}
+
+void DotGameActor::draw(DisplayContext &displayContext) {
+	if (!_isVisible || _currentDotIndex == 0) {
+		return;
+	}
+
+	Graphics::ManagedSurface *targetSurface = displayContext._destImage;
+	if (targetSurface == nullptr) {
+		warning("[%s] %s: No target surface to draw", debugName(), __func__);
+		return;
+	}
+
+	// Draw lines connecting consecutive dots that have been reached so far.
+	Common::Point drawOrigin = getBbox().origin() + displayContext._origin;
+	for (uint16 i = 0; i < _currentDotIndex - 1; i++) {
+		Common::Point startPoint = drawOrigin + _dotPositions[i];
+		Common::Point endPoint = drawOrigin + _dotPositions[i + 1];
+		targetSurface->drawThickLine(
+			startPoint.x, startPoint.y, endPoint.x, endPoint.y,
+			_lineThickness, _lineThickness, _linePaletteIndex);
+	}
+
+	if (_animationProgress < 100) {
+		// If animation not complete, draw line from last dot to current position.
+		Common::Point lastDot = drawOrigin + _dotPositions[_currentDotIndex - 1];
+		Common::Point currentPos = drawOrigin + _currentPosition;
+		targetSurface->drawThickLine(
+			lastDot.x, lastDot.y, currentPos.x, currentPos.y,
+			_lineThickness, _lineThickness, _linePaletteIndex);
+
+	} else if (_totalDots == _currentDotIndex) {
+		// If all dots connected, close the loop from last dot to first dot.
+		Common::Point firstDot = drawOrigin + _dotPositions[0];
+		Common::Point lastDot = drawOrigin + _dotPositions[_currentDotIndex - 1];
+		targetSurface->drawThickLine(
+			lastDot.x, lastDot.y, firstDot.x, firstDot.y,
+			_lineThickness, _lineThickness, _linePaletteIndex);
+
+	} else {
+		// Otherwise draw from last dot to next dot in the sequence.
+		Common::Point lastDot = drawOrigin + _dotPositions[_currentDotIndex - 1];
+		Common::Point nextDot = drawOrigin + _dotPositions[_currentDotIndex];
+		targetSurface->drawThickLine(
+			lastDot.x, lastDot.y, nextDot.x, nextDot.y,
+			_lineThickness, _lineThickness, _linePaletteIndex);
+	}
+}
+
+void DotGameActor::doHit() {
+	if (!_isVisible) {
+		return;
+	} else if (_currentDotIndex >= _totalDots) {
+		warning("[%s] %s: Current dot count (%d) exceeds max dots (%d)", debugName(), __func__, _currentDotIndex, _totalDots);
+		return;
+	}
+
+	_currentDotIndex++;
+
+	// Show the marker actor to show "tracing" the path to the next dot.
+	Actor *markerActor = g_engine->getImtGod()->getActorById(_markerActorId);
+	Common::Array<ScriptValue> emptyArgs;
+	if (markerActor != nullptr) {
+		markerActor->callMethod(kSpatialShowMethod, emptyArgs);
+	}
+
+	deActivateHelpers();
+	_animationProgress = 0;
+	while (_animationProgress < 100) {
+		_animationProgress += _speed;
+
+		Common::Point startPoint = _dotPositions[_currentDotIndex - 1];
+		Common::Point endPoint;
+		if (_totalDots == _currentDotIndex) {
+			// The next dot is the first dot again to close the loop.
+			endPoint = _dotPositions[0];
+		} else {
+			// Move to next dot.
+			endPoint = _dotPositions[_currentDotIndex];
+		}
+
+		// Interpolate position between start and end.
+		Common::Point delta = endPoint - startPoint;
+		Common::Point interpolated = startPoint + (delta * _animationProgress / 100);
+		_currentPosition = interpolated;
+
+		// Update the marker actor position.
+		if (markerActor != nullptr) {
+			Common::Array<ScriptValue> moveArgs;
+			ScriptValue xArg, yArg;
+			Common::Point originOnScreen = getBbox().origin() + _currentPosition;
+			xArg.setToFloat(static_cast<double>(originOnScreen.x));
+			yArg.setToFloat(static_cast<double>(originOnScreen.y));
+			moveArgs.push_back(xArg);
+			moveArgs.push_back(yArg);
+			markerActor->callMethod(kSpatialCenterMoveToMethod, moveArgs);
+
+			Common::Array<ScriptValue> frameArgs;
+			ScriptValue trueArg;
+			trueArg.setToBool(true);
+			frameArgs.push_back(trueArg);
+			markerActor->callMethod(kIncrementFrameMethod, frameArgs);
+		}
+
+		invalidateLocalBounds();
+		g_engine->getDisplayUpdateManager()->performUpdateDirty();
+	}
+
+	_animationProgress = 100;
+
+	// We traced the path to the next dot, so we can now hide the marker actor again.
+	if (markerActor != nullptr) {
+		markerActor->callMethod(kSpatialHideMethod, emptyArgs);
+	}
+	invalidateLocalBounds();
+
+	bool allDotsCompleted = (_totalDots - 1 == _currentDotIndex);
+	if (allDotsCompleted) {
+		callMethod(kDotGameHideMethod, emptyArgs);
+		_currentDotIndex = 0;
+		_animationProgress = 100;
+		runScriptResponseIfExists(kDotGameCompleteEvent);
+	} else {
+		updateHelpers();
+		activateHelpers();
+	}
+}
+
+void DotGameActor::doReset(int16 targetDotIndex) {
+	if (_totalDots == 0) {
+		_currentDotIndex = 0;
+		return;
+	}
+
+	const int16 maxDotIndex = static_cast<int16>(_totalDots - 1);
+	targetDotIndex = CLIP<int16>(targetDotIndex, 0, maxDotIndex);
+	_currentDotIndex = targetDotIndex;
+
+	if (_isVisible) {
+		updateHelpers();
+		invalidateLocalBounds();
+	}
+
+	// The original apparently supported arbitrary colors here, but
+	// ScummVM forces us to choose a palette index. The only known
+	// use of this actor (for the Lion King constellations minigame)
+	// only uses a white line anyway. So this shouldn't be a problem.
+	// This must be done here because at load time we don't have an active palette yet.
+	Graphics::Palette currentPalette = g_system->getPaletteManager()->grabPalette(0, Graphics::PALETTE_COUNT);
+	_linePaletteIndex = currentPalette.findBestColor(_lineColorR, _lineColorG, _lineColorB);
+}
+
+void DotGameActor::activateHelpers() {
+	Common::Array<ScriptValue> emptyArgs;
+
+	Actor *startHotspot = g_engine->getImtGod()->getActorById(_startHotspotId);
+	if (startHotspot != nullptr) {
+		startHotspot->callMethod(kMouseActivateMethod, emptyArgs);
+	}
+
+	Actor *endHotspot = g_engine->getImtGod()->getActorById(_endHotspotId);
+	if (endHotspot != nullptr) {
+		endHotspot->callMethod(kSpatialShowMethod, emptyArgs);
+		endHotspot->callMethod(kTimePlayMethod, emptyArgs);
+	}
+}
+
+void DotGameActor::deActivateHelpers() {
+	Common::Array<ScriptValue> emptyArgs;
+
+	Actor *startHotspot = g_engine->getImtGod()->getActorById(_startHotspotId);
+	if (startHotspot != nullptr) {
+		startHotspot->callMethod(kMouseDeactivateMethod, emptyArgs);
+	}
+
+	Actor *endHotspot = g_engine->getImtGod()->getActorById(_endHotspotId);
+	if (endHotspot != nullptr) {
+		endHotspot->callMethod(kSpatialHideMethod, emptyArgs);
+		endHotspot->callMethod(kTimeStopMethod, emptyArgs);
+	}
+}
+
+void DotGameActor::loadIsComplete() {
+	// If the actor is initially visible, hide it on load completion.
+	if (_isVisible) {
+		_isVisible = false;
+		Common::Array<ScriptValue> emptyArgs;
+		callMethod(kDotGameHideMethod, emptyArgs);
+	}
+
+	SpatialEntity::loadIsComplete();
+}
+
+} // End namespace MediaStation
diff --git a/engines/mediastation/actors/dotgame.h b/engines/mediastation/actors/dotgame.h
new file mode 100644
index 00000000000..53a6a5b49c3
--- /dev/null
+++ b/engines/mediastation/actors/dotgame.h
@@ -0,0 +1,63 @@
+/* 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_DOTGAME_H
+#define MEDIASTATION_DOTGAME_H
+
+#include "mediastation/actors/stage.h"
+
+namespace MediaStation {
+
+class DotGameActor : public SpatialEntity {
+public:
+	DotGameActor() : SpatialEntity(kActorTypeDotGame) {};
+
+	virtual void readParameter(Chunk &chunk, ActorHeaderSectionType paramType) override;
+	virtual ScriptValue callMethod(BuiltInMethod methodId, Common::Array<ScriptValue> &args) override;
+	virtual void draw(DisplayContext &displayContext) override;
+	virtual void loadIsComplete() override;
+
+private:
+	uint16 _totalDots = 0;
+	Common::Array<Common::Point> _dotPositions;
+	uint16 _startHotspotId = 0;
+	uint16 _endHotspotId = 0;
+	uint16 _markerActorId = 0;
+	uint16 _currentDotIndex = 0;
+	uint16 _speed = 1;
+	uint8 _lineThickness = 3;
+	byte _lineColorR = 0;
+	byte _lineColorG = 0;
+	byte _lineColorB = 0;
+	byte _linePaletteIndex = 0;
+	Common::Point _currentPosition;
+	uint16 _animationProgress = 100;
+
+	void activateHelpers();
+	void deActivateHelpers();
+	void doHit();
+	void doReset(int16 targetDot);
+	void updateHelpers();
+};
+
+} // End namespace MediaStation
+
+#endif
diff --git a/engines/mediastation/context.cpp b/engines/mediastation/context.cpp
index 2554422d418..bb6f7be1994 100644
--- a/engines/mediastation/context.cpp
+++ b/engines/mediastation/context.cpp
@@ -30,6 +30,7 @@
 #include "mediastation/actors/canvas.h"
 #include "mediastation/actors/cursor.h"
 #include "mediastation/actors/diskimage.h"
+#include "mediastation/actors/dotgame.h"
 #include "mediastation/actors/palette.h"
 #include "mediastation/actors/image.h"
 #include "mediastation/actors/path.h"
@@ -165,6 +166,10 @@ void ImtGod::readCreateActorData(Chunk &chunk) {
 		actor = new DiskImageActor();
 		break;
 
+	case kActorTypeDotGame:
+		actor = new DotGameActor();
+		break;
+
 	default:
 		error("%s: No class for actor type 0x%x", __func__, static_cast<uint>(type));
 	}
diff --git a/engines/mediastation/mediascript/scriptconstants.cpp b/engines/mediastation/mediascript/scriptconstants.cpp
index ee821fe5438..abec7d8f86e 100644
--- a/engines/mediastation/mediascript/scriptconstants.cpp
+++ b/engines/mediastation/mediascript/scriptconstants.cpp
@@ -450,6 +450,14 @@ const char *builtInMethodToStr(BuiltInMethod method) {
 		return "StopLoad";
 	case kIsRectInMemoryMethod:
 		return "IsRectInMemory";
+	case kDotGameResetMethod:
+		return "DotGameReset";
+	case kDotGameShowMethod:
+		return "DotGameShow";
+	case kDotGameHideMethod:
+		return "DotGameHide";
+	case kDotGameHitMethod:
+		return "DotGameHit";
 	default:
 		return "UNKNOWN";
 	}
@@ -503,6 +511,8 @@ const char *eventTypeToStr(EventType type) {
 		return "MovieFailure";
 	case kSpriteMovieEndEvent:
 		return "SpriteMovieEnd";
+	case kDotGameCompleteEvent:
+		return "DotGameComplete";
 	case kScreenExitEvent:
 		return "ScreenExit";
 	case kPathStepEvent:
diff --git a/engines/mediastation/mediascript/scriptconstants.h b/engines/mediastation/mediascript/scriptconstants.h
index a851f8d26ce..146ef5f1b32 100644
--- a/engines/mediastation/mediascript/scriptconstants.h
+++ b/engines/mediastation/mediascript/scriptconstants.h
@@ -294,6 +294,12 @@ enum BuiltInMethod {
 	kPurgeMethod = 0x167,
 	kStopLoadMethod = 0x168,
 	kIsRectInMemoryMethod = 0x16A,
+
+	// DOT GAME ACTOR METHODS.
+	kDotGameResetMethod = 0xE2,
+	kDotGameShowMethod = 0xE3,
+	kDotGameHideMethod = 0xE4,
+	kDotGameHitMethod = 0xE5,
 };
 const char *builtInMethodToStr(BuiltInMethod method);
 
@@ -321,6 +327,7 @@ enum EventType {
 	kMovieAbortEvent = 0x15,
 	kMovieFailureEvent = 0x16,
 	kSpriteMovieEndEvent = 0x17,
+	kDotGameCompleteEvent = 0x18,
 	kScreenExitEvent = 0x1B,
 	kPathStepEvent = 0x1C,
 	kSoundStoppedEvent = 0x1D,
diff --git a/engines/mediastation/module.mk b/engines/mediastation/module.mk
index d69698f2b8d..5761a2fcbf6 100644
--- a/engines/mediastation/module.mk
+++ b/engines/mediastation/module.mk
@@ -7,6 +7,7 @@ MODULE_OBJS = \
 	actors/cursor.o \
 	actors/diskimage.o \
 	actors/document.o \
+	actors/dotgame.o \
 	actors/font.o \
 	actors/hotspot.o \
 	actors/image.o \


Commit: 925f9cb9bcabcd08993c733dd01698ed86bbfab1
    https://github.com/scummvm/scummvm/commit/925f9cb9bcabcd08993c733dd01698ed86bbfab1
Author: Nathanael Gentry (nathanael.gentrydb8 at gmail.com)
Date: 2026-07-01T15:03:20-04:00

Commit Message:
MEDIASTATION: Implement Color Cell Compression (CCC) blitting modes

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


diff --git a/engines/mediastation/graphics.cpp b/engines/mediastation/graphics.cpp
index 80a14315491..1b6cff89829 100644
--- a/engines/mediastation/graphics.cpp
+++ b/engines/mediastation/graphics.cpp
@@ -865,11 +865,11 @@ void VideoDisplayManager::imageBlit(
 		break;
 
 	case kCccBitmapCompression:
-		blitFlags |= kCccBlit;
+		blitFlags |= kColorCellCompressionBlit;
 		break;
 
 	case kCccTransparentBitmapCompression:
-		blitFlags |= kCccTransparentBlit;
+		blitFlags |= kColorCellCompressionTransparentBlit;
 		break;
 
 	case kUncompressedTransparentBitmap:
@@ -928,10 +928,12 @@ void VideoDisplayManager::imageBlit(
 		rleBlitRectsClip(targetImage, destinationPoint, sourceImage, dirtyRegion);
 		break;
 
-	case kCccBlit | kClipEnabled:
-	case kCccTransparentBlit | kClipEnabled:
-		// CCC blitting is unimplemented for now because few, if any, titles actually use it.
-		warning("%s: CCC blitting not implemented yet", __func__);
+	case kColorCellCompressionBlit | kClipEnabled:
+		cccBlitRectsClip(targetImage, destinationPoint, sourceImage, dirtyRegion);
+		break;
+
+	case kColorCellCompressionTransparentBlit | kClipEnabled:
+		cccTransparentBlitRectsClip(targetImage, destinationPoint, sourceImage, dirtyRegion);
 		break;
 
 	case kPartialDissolve | kClipEnabled:
@@ -993,6 +995,46 @@ void VideoDisplayManager::rleBlitRectsClip(
 	}
 }
 
+void VideoDisplayManager::cccBlitRectsClip(
+	Graphics::ManagedSurface *dest,
+	const Common::Point &destLocation,
+	const PixMapImage *source,
+	const Common::Array<Common::Rect> &dirtyRegion) {
+
+	Graphics::ManagedSurface surface = decompressCccBitmap(source);
+	Common::Rect destRect(destLocation, source->width(), source->height());
+	for (const Common::Rect &dirtyRect : dirtyRegion) {
+		Common::Rect areaToRedraw = dirtyRect.findIntersectingRect(destRect);
+
+		if (!areaToRedraw.isEmpty()) {
+			// Calculate source coordinates (relative to source image).
+			Common::Point originOnScreen(areaToRedraw.origin());
+			areaToRedraw.translate(-destLocation.x, -destLocation.y);
+			dest->simpleBlitFrom(surface, areaToRedraw, originOnScreen);
+		}
+	}
+}
+
+void VideoDisplayManager::cccTransparentBlitRectsClip(
+	Graphics::ManagedSurface *dest,
+	const Common::Point &destLocation,
+	const PixMapImage *source,
+	const Common::Array<Common::Rect> &dirtyRegion) {
+
+	Graphics::ManagedSurface surface = decompressCccTransparentBitmap(source);
+	Common::Rect destRect(destLocation, source->width(), source->height());
+	for (const Common::Rect &dirtyRect : dirtyRegion) {
+		Common::Rect areaToRedraw = dirtyRect.findIntersectingRect(destRect);
+
+		if (!areaToRedraw.isEmpty()) {
+			// Calculate source coordinates (relative to source image).
+			Common::Point originOnScreen(areaToRedraw.origin());
+			areaToRedraw.translate(-destLocation.x, -destLocation.y);
+			dest->transBlitFrom(surface, areaToRedraw, originOnScreen);
+		}
+	}
+}
+
 void VideoDisplayManager::dissolveBlitRectsClip(
 	Graphics::ManagedSurface *dest,
 	const Common::Point &destPos,
@@ -1289,6 +1331,157 @@ Graphics::ManagedSurface VideoDisplayManager::decompressRle8Bitmap(
 	return dest;
 }
 
+Graphics::ManagedSurface VideoDisplayManager::decompressCccBitmap(const PixMapImage *source) {
+	// In CCC (Color Cell Compression), the image is divided into 4x4 pixel blocks.
+	// Each block consists of color 1 (1 byte), color 2 (1 byte), and 4x4 bitmask (2 bytes).
+	// Each bit in the mask specifies which color to use.
+
+	// Create a surface to hold the decompressed bitmap.
+	Graphics::ManagedSurface dest;
+	dest.create(source->width(), source->height(), Graphics::PixelFormat::createFormatCLUT8());
+	dest.setTransparentColor(0);
+
+	Common::SeekableReadStream *compressedData = source->_compressedStream;
+	if (compressedData == nullptr) {
+		warning("%s: No image to decompress", __func__);
+		return dest;
+	}
+	compressedData->seek(0);
+
+	// Process each block of the image.
+	for (int16 blockY = 0; blockY < source->height(); blockY += CCC_BLOCK_DIMENSION) {
+		for (int16 blockX = 0; blockX < source->width(); blockX += CCC_BLOCK_DIMENSION) {
+			// Read the compressed block data.
+			byte color1 = compressedData->readByte();
+			byte color2 = compressedData->readByte();
+			uint16 bitmask = compressedData->readUint16BE();
+
+			// Calculate actual block dimensions and handle cases where the image boundary
+			// forces blocks smaller than 4x4.
+			int16 blockHeight = MIN<int16>(source->height() - blockY, CCC_BLOCK_DIMENSION);
+			int16 blockWidth = MIN<int16>(source->width() - blockX, CCC_BLOCK_DIMENSION);
+
+			// Decompress the block. In the original, a separate decompression method didn't exist.
+			// However, to reduce code duplication between the non-transparent and transparent CCC
+			// versions, we will use this helper.
+			decompressCccBlock(dest, blockX, blockY, color1, color2, bitmask, blockWidth, blockHeight);
+		}
+	}
+
+	return dest;
+}
+
+void VideoDisplayManager::decompressCccBlock(
+	Graphics::ManagedSurface &dest,
+	int16 blockX,
+	int16 blockY,
+	byte color1,
+	byte color2,
+	uint16 bitMask,
+	int16 blockWidth,
+	int16 blockHeight,
+	const byte *transparencyColor) {
+
+	// In CCC (Color Cell Compression), the image is divided into 4x4 pixel blocks (CCC blocks).
+	// The compressed stream consists of color 1 (1 byte), color 2 (1 byte), and 4x4 bitmask (2 bytes).
+	// Each bit in the mask specifies which color to use for each pixel. If transparency is enabled
+	// and we are requesting color 1, nothing is drawn.
+	bool backgroundIsTransparent = (transparencyColor != nullptr && color1 == *transparencyColor);
+
+	// Decompress the block.
+	for (int16 y = 0; y < blockHeight; y++) {
+		byte *rowPtr = static_cast<byte *>(dest.getBasePtr(blockX, blockY + y));
+
+		for (int16 x = 0; x < blockWidth; x++) {
+			// Check top bit of the bitmask.
+			if (bitMask & 0x8000) {
+				rowPtr[x] = color2;
+			} else if (!backgroundIsTransparent) {
+				rowPtr[x] = color1;
+			}
+
+			// Shift bitmask left so we can always check the top bit.
+			bitMask <<= 1;
+		}
+
+		// Skip unused bits in the mask for partial blocks.
+		int16 unusedBits = 4 - blockWidth;
+		bitMask <<= unusedBits;
+	}
+}
+
+Graphics::ManagedSurface VideoDisplayManager::decompressCccTransparentBitmap(const PixMapImage *source) {
+	// The CCC transparent format encodes CCC blocks framed in a command stream:
+	//   FF 00 00 00              End of image
+	//   FE ?? CC 00              Set transparency color to CC
+	//   ZZ ZZ YY XX              Position/count
+	//                              - ZZ: Number of CCC blocks following
+	//                              - YY: New Y position in 4x4 block units
+	//                              - XX: New X position in 4x4 block units
+	// Block data, as described above, follows each command.
+
+	// Create a surface to hold the decompressed bitmap.
+	Graphics::ManagedSurface dest;
+	dest.create(source->width(), source->height(), Graphics::PixelFormat::createFormatCLUT8());
+	byte transparencyColor = 0;
+	dest.setTransparentColor(transparencyColor);
+
+	Common::SeekableReadStream *compressedData = source->_compressedStream;
+	if (compressedData == nullptr) {
+		warning("%s: No image to decompress", __func__);
+		return dest;
+	}
+	compressedData->seek(0);
+
+	// Process command stream.
+	Common::Point blockPos;
+	while (true) {
+		// Check for end command.
+		uint32_t command = compressedData->readUint32BE();
+		if (command == 0xFF000000) {
+			break;
+		}
+
+		// Check for a new transparency color.
+		if ((command & 0xFF000000) == 0xFE000000) {
+			transparencyColor = (command >> 16) & 0xFF;
+			continue;
+		}
+
+		// Update drawing position.
+		uint16 blockCount = (command >> 16) & 0xFFFF;
+		byte yDelta = (command >> 8) & 0xFF;
+		byte xDelta = command & 0xFF;
+		if (yDelta == 0) {
+			// Do NOT change row position. Only skip xDelta blocks horizontally.
+			blockPos.x += xDelta * CCC_BLOCK_DIMENSION;
+		} else {
+			// Move to new row and column.
+			blockPos.y += yDelta * CCC_BLOCK_DIMENSION;
+			blockPos.x = xDelta * CCC_BLOCK_DIMENSION;
+		}
+
+		// Process each block in this run.
+		for (uint16 i = 0; i < blockCount; i++) {
+			// Read block data.
+			byte color1 = compressedData->readByte();
+			byte color2 = compressedData->readByte();
+			uint16 bitMask = compressedData->readUint16BE();
+
+			// Calculate actual block dimensions and handle cases where the image boundary
+			// forces blocks smaller than 4x4.
+			int16 blockHeight = MIN<int16>(source->height() - blockPos.y, CCC_BLOCK_DIMENSION);
+			int16 blockWidth = MIN<int16>(source->width() - blockPos.x, CCC_BLOCK_DIMENSION);
+
+			// Move to next block horizontally.
+			decompressCccBlock(dest, blockPos.x, blockPos.y, color1, color2, bitMask, blockWidth, blockHeight, &transparencyColor);
+			blockPos.x += CCC_BLOCK_DIMENSION;
+		}
+	}
+
+	return dest;
+}
+
 void VideoDisplayManager::setGammaValues(double red, double green, double blue) {
 	_redGamma = red;
 	_blueGamma = blue;
diff --git a/engines/mediastation/graphics.h b/engines/mediastation/graphics.h
index 02143f5aaec..c9cefa80b3f 100644
--- a/engines/mediastation/graphics.h
+++ b/engines/mediastation/graphics.h
@@ -45,8 +45,11 @@ enum BlitMode {
 	kUncompressedTransparentBlit = 0x08,
 	kPartialDissolve = 0x10,
 	kFullDissolve = 0x20,
-	kCccBlit = 0x40,
-	kCccTransparentBlit = 0x80
+	// These are variants of Color Cell Compression to compress low framerate
+	// "videos" as opposed to just animations. They seem only to be used in
+	// the Lamb Chop cutscenes.
+	kColorCellCompressionBlit = 0x40,
+	kColorCellCompressionTransparentBlit = 0x80
 };
 
 enum TransitionType {
@@ -195,6 +198,20 @@ public:
 		const Graphics::ManagedSurface *keyFrame = nullptr,
 		const Common::Point *keyFrameOffset = nullptr);
 
+	const int16 CCC_BLOCK_DIMENSION = 4;
+	Graphics::ManagedSurface decompressCccBitmap(const PixMapImage *source);
+	Graphics::ManagedSurface decompressCccTransparentBitmap(const PixMapImage *source);
+	void decompressCccBlock(
+		Graphics::ManagedSurface &dest,
+		int16_t blockX,
+		int16_t blockY,
+		uint8_t backgroundColor,
+		uint8_t foregroundColor,
+		uint16_t bitMask,
+		int16_t blockWidth,
+		int16_t blockHeight,
+		const uint8_t *transparencyColor = nullptr);
+
 	void effectTransition(Common::Array<ScriptValue> &args);
 	void setTransitionOnSync(Common::Array<ScriptValue> &args) { _scheduledTransitionOnSync = args; }
 	void doTransitionOnSync();
@@ -234,6 +251,16 @@ private:
 		const Common::Point &destLocation,
 		const PixMapImage *source,
 		const Common::Array<Common::Rect> &dirtyRegion);
+	void cccBlitRectsClip(
+		Graphics::ManagedSurface *dest,
+		const Common::Point &destLocation,
+		const PixMapImage *source,
+		const Common::Array<Common::Rect> &dirtyRegion);
+	void cccTransparentBlitRectsClip(
+		Graphics::ManagedSurface *dest,
+		const Common::Point &destLocation,
+		const PixMapImage *source,
+		const Common::Array<Common::Rect> &dirtyRegion);
 	void dissolveBlitRectsClip(
 		Graphics::ManagedSurface *dest,
 		const Common::Point &destPos,




More information about the Scummvm-git-logs mailing list