[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 ¶mTokenName = _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