[Scummvm-git-logs] scummvm master -> d28be41ad6d815c02ec849b9a5e8410b6c645da0
sev-
noreply at scummvm.org
Fri Aug 7 20:33:11 UTC 2026
This automated email contains information about 14 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
ebf084d636 DIRECTOR: DT: Fix format-string crashes in the script viewer
8f6ff969e2 DIRECTOR: DT: Add keyboard shortcuts for debugger stepping
a9008ffb84 DIRECTOR: DT: Cache the cast browser rows
6d8c79a5e0 DIRECTOR: DT: Add copy-value to the variable views
93d550ef73 DIRECTOR: DT: Sortable cast list and complete the type icon table
20731a911c DIRECTOR: DT: Add a name filter to the Vars window
26e529445b DIRECTOR: DT: Refresh cached thumbnails when a cast member changes
cb189df3e6 DIRECTOR: DT: Identify windows by archive path, not Mac name
7a6c52b1f4 DIRECTOR: Guard endOfVideo() against a null channel
3e8fe965a3 DIRECTOR: DT: Add a movie cast member score viewer
bc035f8599 DIRECTOR: DT: Add a quick-open palette (Ctrl+P)
04cd014d84 DIRECTOR: DT: Add pick-from-stage
483425c957 DIRECTOR: DT: Conditional function breakpoints
d28be41ad6 DIRECTOR: DT: Add rebindable keyboard shortcuts
Commit: ebf084d636b20bbcbb3d6808e7b8562d2260398a
https://github.com/scummvm/scummvm/commit/ebf084d636b20bbcbb3d6808e7b8562d2260398a
Author: ramyak-sharma (ramyaksharma1 at gmail.com)
Date: 2026-08-07T22:33:01+02:00
Commit Message:
DIRECTOR: DT: Fix format-string crashes in the script viewer
Several ImGui::Text/TextColored calls in the D4 script renderer passed
script and runtime datum text straight in as the printf format string,
so any '%' in a handler name, property, or datum value could misparse
or crash. Pass the text as a "%s" argument instead.
Changed paths:
engines/director/debugger/dt-script-d4.cpp
diff --git a/engines/director/debugger/dt-script-d4.cpp b/engines/director/debugger/dt-script-d4.cpp
index 5b8d1ed3f9b..e903d4c6c89 100644
--- a/engines/director/debugger/dt-script-d4.cpp
+++ b/engines/director/debugger/dt-script-d4.cpp
@@ -364,7 +364,7 @@ public:
ImGui::Text(".");
ImGui::SameLine();
- ImGui::Text(node.name.c_str());
+ ImGui::Text("%s", node.name.c_str());
ImGui::SameLine();
ImGui::Text("(");
ImGui::SameLine();
@@ -462,7 +462,7 @@ public:
virtual void visit(const LingoDec::MemberExprNode &node) override {
bool hasCastID = node.castID && !(node.castID->type == LingoDec::kLiteralNode && node.castID->getValue()->type == LingoDec::kDatumInt && node.castID->getValue()->i == 0);
- ImGui::Text(node.type.c_str());
+ ImGui::Text("%s", node.type.c_str());
ImGui::SameLine();
ImGui::Text(" ");
ImGui::SameLine();
@@ -543,7 +543,7 @@ public:
virtual void visit(const LingoDec::ThePropExprNode &node) override {
ImGui::TextColored(ImColor(_state->theme->keyword_color), "the ");
ImGui::SameLine();
- ImGui::Text(node.prop.c_str());
+ ImGui::Text("%s", node.prop.c_str());
ImGui::SameLine();
ImGui::TextColored(ImColor(_state->theme->keyword_color), " of ");
ImGui::SameLine();
@@ -579,7 +579,7 @@ public:
virtual void visit(const LingoDec::SoundCmdStmtNode &node) override {
write(node._startOffset, "sound ", _state->theme->keyword_color);
ImGui::SameLine();
- ImGui::Text(node.cmd.c_str());
+ ImGui::Text("%s", node.cmd.c_str());
ImGui::SameLine();
ImGui::Text(" ");
ImGui::SameLine();
@@ -721,7 +721,7 @@ public:
ImGui::Text(".");
ImGui::SameLine();
- ImGui::Text(node.prop.c_str());
+ ImGui::Text("%s", node.prop.c_str());
ImGui::SameLine();
ImGui::Text("[");
ImGui::SameLine();
@@ -870,7 +870,7 @@ private:
ImGui::SameLine();
return;
case LingoDec::kDatumVarRef:
- ImGui::TextColored(_state->theme->var_color, datum.s.c_str());
+ ImGui::TextColored(_state->theme->var_color, "%s", datum.s.c_str());
ImGui::SameLine();
return;
case LingoDec::kDatumString:
Commit: 8f6ff969e21a4e0310b94e4e2b1304626d57ecf5
https://github.com/scummvm/scummvm/commit/8f6ff969e21a4e0310b94e4e2b1304626d57ecf5
Author: ramyak-sharma (ramyaksharma1 at gmail.com)
Date: 2026-08-07T22:33:01+02:00
Commit Message:
DIRECTOR: DT: Add keyboard shortcuts for debugger stepping
Wire F5 (continue/break), F10 (step over), F11 (step into) and
Shift+F11 (step out) to the existing step helpers, routed globally so
they work regardless of the focused window. Note the keys in the
control-panel button tooltips.
Changed paths:
engines/director/debugger/debugtools.cpp
engines/director/debugger/dt-controlpanel.cpp
engines/director/debugger/dt-internal.h
diff --git a/engines/director/debugger/debugtools.cpp b/engines/director/debugger/debugtools.cpp
index ff1d78da300..b7ce380906e 100644
--- a/engines/director/debugger/debugtools.cpp
+++ b/engines/director/debugger/debugtools.cpp
@@ -1029,6 +1029,8 @@ void onImGuiRender() {
ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport(), ImGuiDockNodeFlags_PassthruCentralNode);
+ handleDebuggerShortcuts();
+
if (ImGui::BeginMainMenuBar()) {
if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_2, ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverFocused))
_state->_w.controlPanel = !_state->_w.controlPanel;
diff --git a/engines/director/debugger/dt-controlpanel.cpp b/engines/director/debugger/dt-controlpanel.cpp
index cecc946c5ad..52eca10f1e4 100644
--- a/engines/director/debugger/dt-controlpanel.cpp
+++ b/engines/director/debugger/dt-controlpanel.cpp
@@ -119,6 +119,44 @@ static void dbgStepOut() {
_state->_dbg._isScriptDirty = true;
}
+// Global debugger step keys. Lives here to reach the static step helpers;
+// called each frame so it works regardless of the focused window.
+void handleDebuggerShortcuts() {
+ Movie *movie = g_director->getCurrentMovie();
+ if (!movie)
+ return;
+ Score *score = movie->getScore();
+
+ const ImGuiInputFlags route = ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverFocused;
+ const bool running = (g_lingo->_exec._state == kRunning);
+
+ if (ImGui::Shortcut(ImGuiKey_F5, route)) {
+ if (running) {
+ score->_playState = kPlayPaused;
+ dgbStop();
+ g_system->displayMessageOnOSD(Common::U32String("Paused"));
+ } else {
+ score->_playState = (score->_playState == kPlayPausedAfterLoading) ? kPlayLoaded : kPlayStarted;
+ g_lingo->_exec._state = kRunning;
+ g_lingo->_exec._shouldPause = nullptr;
+ }
+ return;
+ }
+
+ // Match the step buttons: pause when running, step when paused.
+ // Shift+F11 must be tested before F11.
+ if (ImGui::Shortcut(ImGuiMod_Shift | ImGuiKey_F11, route)) {
+ score->_playState = kPlayStarted;
+ running ? dgbStop() : dbgStepOut();
+ } else if (ImGui::Shortcut(ImGuiKey_F11, route)) {
+ score->_playState = kPlayStarted;
+ running ? dgbStop() : dbgStepInto();
+ } else if (ImGui::Shortcut(ImGuiKey_F10, route)) {
+ score->_playState = kPlayStarted;
+ running ? dgbStop() : dbgStepOver();
+ }
+}
+
void showControlPanel() {
if (!_state->_w.controlPanel)
return;
@@ -218,7 +256,7 @@ void showControlPanel() {
ImU32 stopColor = (score->_playState == kPlayPaused || score->_playState == kPlayPausedAfterLoading) ? active_color : color;
dl->AddRectFilled(ImVec2(p.x, p.y), ImVec2(p.x + 16, p.y + 16), stopColor);
- ImGui::SetItemTooltip("Stop");
+ ImGui::SetItemTooltip("Pause (F5)");
ImGui::SameLine();
}
@@ -269,7 +307,7 @@ void showControlPanel() {
dl->AddTriangleFilled(ImVec2(p.x, p.y), ImVec2(p.x, p.y + 16), ImVec2(p.x + 14, p.y + 8), color);
- ImGui::SetItemTooltip("Play");
+ ImGui::SetItemTooltip("Play / Continue (F5)");
ImGui::SameLine();
}
@@ -326,7 +364,7 @@ void showControlPanel() {
dl->AddLine(ImVec2(p.x + 14, p.y + 10), ImVec2(p.x + 18, p.y + 10), color_red, 2);
dl->AddCircleFilled(ImVec2(p.x + 9, p.y + 15), 2.0f, color);
- ImGui::SetItemTooltip("Step Over");
+ ImGui::SetItemTooltip("Step Over (F10)");
ImGui::SameLine();
}
@@ -351,7 +389,7 @@ void showControlPanel() {
dl->AddLine(ImVec2(p.x + 12, p.y + 6), ImVec2(p.x + 8.5f, p.y + 9), color_red, 2);
dl->AddCircleFilled(ImVec2(p.x + 9, p.y + 15), 2.0f, color);
- ImGui::SetItemTooltip("Step Into");
+ ImGui::SetItemTooltip("Step Into (F11)");
ImGui::SameLine();
}
@@ -376,7 +414,7 @@ void showControlPanel() {
dl->AddLine(ImVec2(p.x + 12, p.y + 5), ImVec2(p.x + 8.5f, p.y + 1), color_red, 2);
dl->AddCircleFilled(ImVec2(p.x + 9, p.y + 15), 2.0f, color);
- ImGui::SetItemTooltip("Step Out");
+ ImGui::SetItemTooltip("Step Out (Shift+F11)");
}
}
ImGui::End();
diff --git a/engines/director/debugger/dt-internal.h b/engines/director/debugger/dt-internal.h
index 5c8ae45c02b..692fc29a664 100644
--- a/engines/director/debugger/dt-internal.h
+++ b/engines/director/debugger/dt-internal.h
@@ -383,6 +383,7 @@ void showCast(); // dt-cast.cpp
void showImageViewer(); // dt-castdetails.cpp
void showCastDetails(); // dt-castdetails.cpp
void showControlPanel();// dt-controlpanel.cpp
+void handleDebuggerShortcuts(); // dt-controlpanel.cpp
// dt-lists.cpp
void showVars();
Commit: a9008ffb84afdf58142f86656a6223abdec1d1e5
https://github.com/scummvm/scummvm/commit/a9008ffb84afdf58142f86656a6223abdec1d1e5
Author: ramyak-sharma (ramyaksharma1 at gmail.com)
Date: 2026-08-07T22:33:01+02:00
Commit Message:
DIRECTOR: DT: Cache the cast browser rows
showCast() re-gathered every cast member (load(), display-name
formatting, filtering) each frame; for large casts that is thousands of
allocations per frame. Cache the gathered rows and rebuild only when the
movie, filters, or cast size change.
Changed paths:
engines/director/debugger/dt-cast.cpp
diff --git a/engines/director/debugger/dt-cast.cpp b/engines/director/debugger/dt-cast.cpp
index f52e09cec18..9555af6e398 100644
--- a/engines/director/debugger/dt-cast.cpp
+++ b/engines/director/debugger/dt-cast.cpp
@@ -24,6 +24,7 @@
#include "director/director.h"
#include "director/debugger/dt-internal.h"
+#include "director/archive.h"
#include "director/cast.h"
#include "director/castmember/bitmap.h"
#include "director/castmember/text.h"
@@ -318,17 +319,29 @@ void showCast() {
}
_state->_cast._nameFilter.Draw();
- Common::Array<CastRowEntry> rows;
+ // Gathering rows (load, name formatting, filtering) is costly for big
+ // casts, so cache them and rebuild only when movie/filters/size change.
+ static Common::Array<CastRowEntry> rows;
+ static Common::String rowsKey;
+
int total = 0;
- for (auto it : *movie->getCasts()) {
- gatherCastMembers(it._value, rows);
+ for (auto it : *movie->getCasts())
if (it._value->_loadedCast)
total += it._value->_loadedCast->size();
- }
- gatherCastMembers(movie->getSharedCast(), rows);
if (movie->getSharedCast() && movie->getSharedCast()->_loadedCast)
total += movie->getSharedCast()->_loadedCast->size();
+ Common::String moviePath = movie->getArchive() ? movie->getArchive()->getPathName().toString() : movie->getMacName();
+ Common::String key = Common::String::format("%s|%s|%d|%d", moviePath.c_str(),
+ _state->_cast._nameFilter.InputBuf, _state->_cast._typeFilter, total);
+ if (key != rowsKey) {
+ rowsKey = key;
+ rows.clear();
+ for (auto it : *movie->getCasts())
+ gatherCastMembers(it._value, rows);
+ gatherCastMembers(movie->getSharedCast(), rows);
+ }
+
ImGui::SameLine();
if ((int)rows.size() == total)
ImGui::Text("%d members", total);
Commit: 6d8c79a5e07e0b685949ea9e55b5825bc229b7ca
https://github.com/scummvm/scummvm/commit/6d8c79a5e07e0b685949ea9e55b5825bc229b7ca
Author: ramyak-sharma (ramyaksharma1 at gmail.com)
Date: 2026-08-07T22:33:01+02:00
Commit Message:
DIRECTOR: DT: Add copy-value to the variable views
Right-clicking a global/local/property/watched variable now offers
"Copy value", writing the formatted value to the clipboard.
Changed paths:
engines/director/debugger/dt-lists.cpp
diff --git a/engines/director/debugger/dt-lists.cpp b/engines/director/debugger/dt-lists.cpp
index bd5b25c06d1..e073ab626f9 100644
--- a/engines/director/debugger/dt-lists.cpp
+++ b/engines/director/debugger/dt-lists.cpp
@@ -77,6 +77,11 @@ void showVars() {
displayVariable(i, changed);
ImGui::SameLine();
ImGui::Text(" - [%s] %s", val.type2str(), formatStringForDump(val.asString(true)).c_str());
+ if (ImGui::BeginPopupContextItem("v")) {
+ if (ImGui::MenuItem("Copy value"))
+ ImGui::SetClipboardText(val.asString(true).c_str());
+ ImGui::EndPopup();
+ }
ImGui::PopID();
id += 1;
}
@@ -97,6 +102,11 @@ void showVars() {
displayVariable(i, changed);
ImGui::SameLine();
ImGui::Text(" - [%s] %s", val.type2str(), formatStringForDump(val.asString(true)).c_str());
+ if (ImGui::BeginPopupContextItem("v")) {
+ if (ImGui::MenuItem("Copy value"))
+ ImGui::SetClipboardText(val.asString(true).c_str());
+ ImGui::EndPopup();
+ }
ImGui::PopID();
id += 1;
}
@@ -120,6 +130,11 @@ void showVars() {
displayVariable(i, false);
ImGui::SameLine();
ImGui::Text(" - [%s] %s", val.type2str(), formatStringForDump(val.asString(true)).c_str());
+ if (ImGui::BeginPopupContextItem("v")) {
+ if (ImGui::MenuItem("Copy value"))
+ ImGui::SetClipboardText(val.asString(true).c_str());
+ ImGui::EndPopup();
+ }
ImGui::PopID();
id += 1;
}
@@ -152,10 +167,15 @@ void showWatchedVars() {
id += 1;
ImGui::PushID(id);
displayVariable(v._key, false, outOfScope);
- ImGui::PopID();
ImGui::SameLine();
ImGui::Text(" - [%s] %s", val.type2str(), formatStringForDump(val.asString(true)).c_str());
+ if (ImGui::BeginPopupContextItem("v")) {
+ if (ImGui::MenuItem("Copy value"))
+ ImGui::SetClipboardText(val.asString(true).c_str());
+ ImGui::EndPopup();
+ }
+ ImGui::PopID();
}
if (_state->_variables.empty())
Commit: 93d550ef73af79af33fd84fc22d11ebf7c98e2ad
https://github.com/scummvm/scummvm/commit/93d550ef73af79af33fd84fc22d11ebf7c98e2ad
Author: ramyak-sharma (ramyaksharma1 at gmail.com)
Date: 2026-08-07T22:33:01+02:00
Commit Message:
DIRECTOR: DT: Sortable cast list and complete the type icon table
Make the cast browser's list columns (Name, ID, Type) sortable, keeping
the sort after the row cache is rebuilt. Also extend the cast-type icon
table to cover Xtra and bound-check against kCastXtra.
Changed paths:
engines/director/debugger/dt-cast.cpp
diff --git a/engines/director/debugger/dt-cast.cpp b/engines/director/debugger/dt-cast.cpp
index 9555af6e398..527488158d0 100644
--- a/engines/director/debugger/dt-cast.cpp
+++ b/engines/director/debugger/dt-cast.cpp
@@ -68,10 +68,11 @@ static const char *toIcon(CastType castType) {
ICON_MS_MOVIE, // Movie
ICON_MS_ANIMATED_IMAGES, // DigitalVideo
ICON_MS_FORMS_APPS_SCRIPT, // Script
- ICON_MS_BRAND_FAMILY, // RTE
- "?", // ???
- ICON_MS_TRANSITION_FADE}; // Transition
- if (castType < 0 || castType > kCastTransition)
+ ICON_MS_BRAND_FAMILY, // RichText
+ "?", // OLE
+ ICON_MS_TRANSITION_FADE, // Transition
+ ""}; // Xtra
+ if (castType < 0 || castType > kCastXtra)
return "";
return castTypes[(int)castType];
}
@@ -149,6 +150,21 @@ static void gatherCastMembers(const Cast *cast, Common::Array<CastRowEntry> &row
}
}
+static void sortCastRows(Common::Array<CastRowEntry> &rows, int col, bool asc) {
+ Common::sort(rows.begin(), rows.end(), [col, asc](const CastRowEntry &a, const CastRowEntry &b) {
+ int c;
+ switch (col) {
+ case 1: c = a.name.compareToIgnoreCase(b.name); break; // Name
+ case 4: c = (int)a.member->_type - (int)b.member->_type; break; // Type
+ case 2: // ID
+ default:
+ c = a.id - b.id;
+ break;
+ }
+ return asc ? (c < 0) : (c > 0);
+ });
+}
+
static ImGuiImage getThumbnail(CastMember *member) {
switch (member->_type) {
case kCastBitmap:
@@ -334,7 +350,8 @@ void showCast() {
Common::String moviePath = movie->getArchive() ? movie->getArchive()->getPathName().toString() : movie->getMacName();
Common::String key = Common::String::format("%s|%s|%d|%d", moviePath.c_str(),
_state->_cast._nameFilter.InputBuf, _state->_cast._typeFilter, total);
- if (key != rowsKey) {
+ const bool rowsRebuilt = (key != rowsKey);
+ if (rowsRebuilt) {
rowsKey = key;
rows.clear();
for (auto it : *movie->getCasts())
@@ -355,15 +372,24 @@ void showCast() {
ImGui::BeginChild("##cast", ImVec2(childsize.x, childsize.y - sliderHeight));
if (_state->_cast._listView) {
- if (ImGui::BeginTable("Resources", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg)) {
- ImGui::TableSetupColumn("No.", 0, 30.f);
+ if (ImGui::BeginTable("Resources", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg | ImGuiTableFlags_Sortable)) {
+ ImGui::TableSetupColumn("No.", ImGuiTableColumnFlags_NoSort, 30.f);
ImGui::TableSetupColumn("Name", 0, 120.f);
- ImGui::TableSetupColumn("ID", 0, 20.f);
- ImGui::TableSetupColumn("Script", 0, 80.f);
+ ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_DefaultSort, 20.f);
+ ImGui::TableSetupColumn("Script", ImGuiTableColumnFlags_NoSort, 80.f);
ImGui::TableSetupColumn("Type", 0, 80.f);
- ImGui::TableSetupColumn("Preview", ImGuiTableColumnFlags_WidthStretch, 50.f);
+ ImGui::TableSetupColumn("Preview", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoSort, 50.f);
ImGui::TableHeadersRow();
+ // re-sort on a header click, or after the row cache was rebuilt
+ if (ImGuiTableSortSpecs *ss = ImGui::TableGetSortSpecs()) {
+ if ((ss->SpecsDirty || rowsRebuilt) && ss->SpecsCount > 0) {
+ sortCastRows(rows, ss->Specs[0].ColumnIndex,
+ ss->Specs[0].SortDirection == ImGuiSortDirection_Ascending);
+ ss->SpecsDirty = false;
+ }
+ }
+
// only submit the visible rows, not the whole cast, each frame
ImGuiListClipper clipper;
clipper.Begin((int)rows.size());
Commit: 20731a911cf1f17a27e7d459ab9f4217d562cbd9
https://github.com/scummvm/scummvm/commit/20731a911cf1f17a27e7d459ab9f4217d562cbd9
Author: ramyak-sharma (ramyaksharma1 at gmail.com)
Date: 2026-08-07T22:33:01+02:00
Commit Message:
DIRECTOR: DT: Add a name filter to the Vars window
Filter global/local/property variables by name, matching the filtering
the Cast and Functions windows already offer. (Clicking a variable name
already toggles it in the Watched Vars list.)
Changed paths:
engines/director/debugger/dt-internal.h
engines/director/debugger/dt-lists.cpp
diff --git a/engines/director/debugger/dt-internal.h b/engines/director/debugger/dt-internal.h
index 692fc29a664..b8ee21ccb0e 100644
--- a/engines/director/debugger/dt-internal.h
+++ b/engines/director/debugger/dt-internal.h
@@ -263,6 +263,7 @@ typedef struct ImGuiState {
DatumHash _prevGlobals;
uint32 _lastTimeRefreshed = 0;
+ ImGuiTextFilter _nameFilter;
} _vars;
struct {
diff --git a/engines/director/debugger/dt-lists.cpp b/engines/director/debugger/dt-lists.cpp
index e073ab626f9..c52081311a3 100644
--- a/engines/director/debugger/dt-lists.cpp
+++ b/engines/director/debugger/dt-lists.cpp
@@ -62,6 +62,7 @@ void showVars() {
ImGui::SetNextWindowSize(ImVec2(300, 250), ImGuiCond_FirstUseEver);
if (ImGui::Begin("Vars", &_state->_w.vars)) {
Common::Array<Common::String> keyBuffer;
+ _state->_vars._nameFilter.Draw("Filter");
if (ImGui::CollapsingHeader("Global vars:", ImGuiTreeNodeFlags_DefaultOpen)) {
for (auto &it : _state->_vars._globals) {
@@ -71,6 +72,8 @@ void showVars() {
uint32 id = 0;
for (auto &i : keyBuffer) {
+ if (!_state->_vars._nameFilter.PassFilter(i.c_str()))
+ continue;
ImGui::PushID(id);
Datum &val = _state->_vars._globals.getVal(i);
bool changed = !_state->_vars._prevGlobals.contains(i) || !(_state->_vars._globals.getVal(i) == _state->_vars._prevGlobals.getVal(i));
@@ -96,6 +99,8 @@ void showVars() {
uint32 id = 0;
for (auto &i : keyBuffer) {
+ if (!_state->_vars._nameFilter.PassFilter(i.c_str()))
+ continue;
ImGui::PushID(id);
Datum &val = _state->_vars._locals.getVal(i);
bool changed = !_state->_vars._prevLocals.contains(i) || !(_state->_vars._locals.getVal(i) == _state->_vars._prevLocals.getVal(i));
@@ -125,6 +130,8 @@ void showVars() {
uint32 id = 0;
for (auto &i : keyBuffer) {
+ if (!_state->_vars._nameFilter.PassFilter(i.c_str()))
+ continue;
ImGui::PushID(id);
Datum val = script->getProp(i);
displayVariable(i, false);
Commit: 26e529445b7c43475b322a14994eb909c3ed6cb6
https://github.com/scummvm/scummvm/commit/26e529445b7c43475b322a14994eb909c3ed6cb6
Author: ramyak-sharma (ramyaksharma1 at gmail.com)
Date: 2026-08-07T22:33:01+02:00
Commit Message:
DIRECTOR: DT: Refresh cached thumbnails when a cast member changes
The cast texture cache only dropped entries on a movie switch, so an
edited bitmap/shape/text member kept a stale thumbnail. Drop and
regenerate a cached texture when the member reports isModified().
Changed paths:
engines/director/debugger/debugtools.cpp
diff --git a/engines/director/debugger/debugtools.cpp b/engines/director/debugger/debugtools.cpp
index b7ce380906e..588e4da5877 100644
--- a/engines/director/debugger/debugtools.cpp
+++ b/engines/director/debugger/debugtools.cpp
@@ -339,6 +339,20 @@ Director::Breakpoint *getBreakpoint(const Common::String &handlerName, uint16 sc
return nullptr;
}
+// Return the member's cached texture, dropping it first if the member changed
+// (isModified) so the caller regenerates it. Keeps thumbnails in sync with edits.
+static bool tryGetCachedTexture(CastMember *key, ImGuiImage &out) {
+ if (!_state->_cast._textures.contains(key))
+ return false;
+ if (key->isModified()) {
+ g_system->freeImGuiTexture((void *)(intptr_t)_state->_cast._textures[key].id);
+ _state->_cast._textures.erase(key);
+ return false;
+ }
+ out = _state->_cast._textures[key];
+ return true;
+}
+
ImGuiImage getImageID(CastMember *castMember) {
if (castMember->_type != CastType::kCastBitmap) {
return {};
@@ -346,9 +360,9 @@ ImGuiImage getImageID(CastMember *castMember) {
BitmapCastMember *bmpMember = (BitmapCastMember *)castMember;
- if (_state->_cast._textures.contains(bmpMember)) {
- return _state->_cast._textures[bmpMember];
- }
+ ImGuiImage cached;
+ if (tryGetCachedTexture(bmpMember, cached))
+ return cached;
bmpMember->load();
Picture *pic = bmpMember->_picture;
@@ -419,9 +433,9 @@ ImGuiImage getShapeID(CastMember *castMember) {
return {};
}
- if (_state->_cast._textures.contains(castMember)) {
- return _state->_cast._textures[castMember];
- }
+ ImGuiImage cached;
+ if (tryGetCachedTexture(castMember, cached))
+ return cached;
ShapeCastMember *shapeMember = (ShapeCastMember *)castMember;
@@ -477,9 +491,9 @@ ImGuiImage getTextID(CastMember *castMember) {
return {};
}
- if (_state->_cast._textures.contains(castMember)) {
- return _state->_cast._textures[castMember];
- }
+ ImGuiImage cached;
+ if (tryGetCachedTexture(castMember, cached))
+ return cached;
Common::Rect bbox(castMember->getBbox());
Commit: cb189df3e691398a78a26e25150dc95275c34146
https://github.com/scummvm/scummvm/commit/cb189df3e691398a78a26e25150dc95275c34146
Author: ramyak-sharma (ramyaksharma1 at gmail.com)
Date: 2026-08-07T22:33:01+02:00
Commit Message:
DIRECTOR: DT: Identify windows by archive path, not Mac name
The window selector and continuation cache matched movies by Mac name,
so two movies sharing a name were indistinguishable. Match on the
archive path instead while still displaying the Mac name, and scope the
combo entries by path so duplicates stay selectable.
Changed paths:
engines/director/debugger/debugtools.cpp
engines/director/debugger/dt-internal.h
engines/director/debugger/dt-score.cpp
diff --git a/engines/director/debugger/debugtools.cpp b/engines/director/debugger/debugtools.cpp
index 588e4da5877..499407e10bd 100644
--- a/engines/director/debugger/debugtools.cpp
+++ b/engines/director/debugger/debugtools.cpp
@@ -688,15 +688,29 @@ ImColor brightenColor(const ImColor& color, float factor) {
return ImColor(col);
}
+// Identity for a window's movie: the archive path, which is stable even when
+// two movies share a Mac name. Falls back to the Mac name if there's no archive.
+Common::String movieId(const Movie *m) {
+ if (!m)
+ return Common::String();
+ return m->getArchive() ? m->getArchive()->getPathName().toString() : m->getMacName();
+}
+
+// Human-readable label for the window selector (the movie's Mac name).
+static Common::String movieLabel(Window *w) {
+ Movie *m = w ? w->getCurrentMovie() : nullptr;
+ return m ? m->getMacName() : Common::String("(no movie)");
+}
+
Window *findWindowByName(const Common::String &name) {
if (name.empty())
return nullptr;
Movie *stageMovie = g_director->getStage()->getCurrentMovie();
- if (stageMovie && stageMovie->getMacName() == name)
+ if (stageMovie && movieId(stageMovie) == name)
return g_director->getStage();
for (auto window : *g_director->getWindowList()) {
Movie *movie = window->getCurrentMovie();
- if (movie && movie->getMacName() == name)
+ if (movie && movieId(movie) == name)
return window;
}
return nullptr;
@@ -704,18 +718,16 @@ Window *findWindowByName(const Common::String &name) {
Window *windowListCombo(Common::String *target) {
const Common::Array<Window *> *windowList = g_director->getWindowList();
- const Common::String selWin = *target;
Window *res = nullptr;
- // windows may not have a movie loaded yet
- Movie *stageMovie = g_director->getStage()->getCurrentMovie();
- Common::String stage = stageMovie ? stageMovie->getMacName() : Common::String();
+ Window *stage = g_director->getStage();
+ Common::String stageId = movieId(stage->getCurrentMovie());
// Check if the relevant window is gone
bool found = false;
for (auto window : (*windowList)) {
- if (window->getCurrentMovie() && window->getCurrentMovie()->getMacName() == selWin) {
- // Found the selected window
+ Movie *m = window->getCurrentMovie();
+ if (m && movieId(m) == *target) {
found = true;
res = window;
break;
@@ -723,39 +735,41 @@ Window *windowListCombo(Common::String *target) {
}
// Our default is Stage
- if (selWin.empty() || windowList->empty() || !found) {
- *target = stage;
- res = g_director->getStage();
+ if (target->empty() || windowList->empty() || !found) {
+ *target = stageId;
+ res = stage;
}
ImGui::Text("Window:");
ImGui::SameLine();
- if (ImGui::BeginCombo("##window", selWin.c_str())) {
- bool selected = (*target == stage);
- if (ImGui::Selectable(stage.c_str(), selected))
- *target = stage;
-
- if (selected) {
- ImGui::SetItemDefaultFocus();
- res = g_director->getStage();
+ if (ImGui::BeginCombo("##window", movieLabel(res).c_str())) {
+ bool selected = (*target == stageId);
+ if (ImGui::Selectable(movieLabel(stage).c_str(), selected)) {
+ *target = stageId;
+ res = stage;
}
+ if (selected)
+ ImGui::SetItemDefaultFocus();
for (auto window : (*windowList)) {
- if (!window->getCurrentMovie())
+ Movie *m = window->getCurrentMovie();
+ if (!m)
continue;
- Common::String winName = window->getCurrentMovie()->getMacName();
- selected = (*target == winName);
- if (ImGui::Selectable(winName.c_str(), selected)) {
- *target = winName;
+ Common::String id = movieId(m);
+ // scope the ID by the (unique) path so windows that share a Mac
+ // name are still selectable independently
+ ImGui::PushID(id.c_str());
+ selected = (*target == id);
+ if (ImGui::Selectable(m->getMacName().c_str(), selected)) {
+ *target = id;
res = window;
}
-
if (selected) {
ImGui::SetItemDefaultFocus();
res = window;
}
-
+ ImGui::PopID();
}
ImGui::EndCombo();
}
diff --git a/engines/director/debugger/dt-internal.h b/engines/director/debugger/dt-internal.h
index b8ee21ccb0e..bcb94e2d390 100644
--- a/engines/director/debugger/dt-internal.h
+++ b/engines/director/debugger/dt-internal.h
@@ -366,6 +366,7 @@ ImVec4 convertColor(uint32 color);
void displayVariable(const Common::String &name, bool changed, bool outOfScope = false);
ImColor brightenColor(const ImColor &color, float factor);
Window *windowListCombo(Common::String *target);
+Common::String movieId(const Movie *m);
Window *findWindowByName(const Common::String &name);
bool selectableViewButton(const char *label, bool selected);
Common::String formatHandlerName(int scriptId, int castId, Common::String handlerName, ScriptType scriptType, bool childScript);
diff --git a/engines/director/debugger/dt-score.cpp b/engines/director/debugger/dt-score.cpp
index 93c43d673f7..6f86eb7df54 100644
--- a/engines/director/debugger/dt-score.cpp
+++ b/engines/director/debugger/dt-score.cpp
@@ -140,7 +140,7 @@ static Common::String clipText(const Common::String &text, float availWidth) {
}
static void buildContinuationData(Window *window) {
- if (_state->_loadedContinuationData == window->getCurrentMovie()->getMacName()) {
+ if (_state->_loadedContinuationData == movieId(window->getCurrentMovie())) {
return;
}
@@ -227,7 +227,7 @@ static void buildContinuationData(Window *window) {
}
}
- _state->_loadedContinuationData = window->getCurrentMovie()->getMacName();
+ _state->_loadedContinuationData = movieId(window->getCurrentMovie());
}
static void drawSliderY(ImVec2 pos, int numChannels) {
Commit: 7a6c52b1f4262b6343671357c0229b897ed5cef0
https://github.com/scummvm/scummvm/commit/7a6c52b1f4262b6343671357c0229b897ed5cef0
Author: ramyak-sharma (ramyaksharma1 at gmail.com)
Date: 2026-08-07T22:33:01+02:00
Commit Message:
DIRECTOR: Guard endOfVideo() against a null channel
isModified() polls endOfVideo() on every screen update, including for
digital-video members not bound to a channel, which dereferenced a null
_channel (and rewindVideo() the same way). Return false when there is no
decoder or channel.
Changed paths:
engines/director/castmember/digitalvideo.cpp
diff --git a/engines/director/castmember/digitalvideo.cpp b/engines/director/castmember/digitalvideo.cpp
index 523ffae079d..1f68611bc41 100644
--- a/engines/director/castmember/digitalvideo.cpp
+++ b/engines/director/castmember/digitalvideo.cpp
@@ -468,6 +468,9 @@ void DigitalVideoCastMember::rewindVideo() {
}
bool DigitalVideoCastMember::endOfVideo() {
+ // No decoder or channel means nothing is playing; avoid a null deref.
+ if (!_video || !_channel)
+ return false;
return (_video->endOfVideo() ||
(getMovieCurrentTimeMillis() >= (uint)(_channel->_stopTime*1000/getTimeScale())));
}
Commit: 3e8fe965a302067d4b77cee6f02d0c64663b0d46
https://github.com/scummvm/scummvm/commit/3e8fe965a302067d4b77cee6f02d0c64663b0d46
Author: ramyak-sharma (ramyaksharma1 at gmail.com)
Date: 2026-08-07T22:33:01+02:00
Commit Message:
DIRECTOR: DT: Add a movie cast member score viewer
Fill in the Cast Details "Movie" tab for movie cast members: linked-movie
metadata plus the embedded score laid out like the film-loop viewer
(frame nav, frame preview, channel/frame grid), resolving thumbnails
against the linked movie's cast.
Changed paths:
engines/director/debugger/dt-castdetails.cpp
diff --git a/engines/director/debugger/dt-castdetails.cpp b/engines/director/debugger/dt-castdetails.cpp
index bdf1be36075..9f2b094baed 100644
--- a/engines/director/debugger/dt-castdetails.cpp
+++ b/engines/director/debugger/dt-castdetails.cpp
@@ -33,7 +33,9 @@
#include "director/castmember/bitmap.h"
#include "director/sprite.h"
#include "director/castmember/filmloop.h"
+#include "director/castmember/movie.h"
#include "director/castmember/sound.h"
+#include "director/channel.h"
#include "director/frame.h"
#include "director/score.h"
#include "director/sound.h"
@@ -955,6 +957,262 @@ void drawFilmLoopCMprops(FilmLoopCastMember *member) {
}
}
+static const char *moviePlayStateStr(PlayState s) {
+ switch (s) {
+ case kPlayNotStarted: return "not started";
+ case kPlayStarted: return "playing";
+ case kPlayStopped: return "stopped";
+ case kPlayPaused: return "paused";
+ case kPlayPausedAfterLoading: return "paused (loading)";
+ default: return "?";
+ }
+}
+
+// Shared score view (frame nav + preview + grid) for Score-backed members.
+// `resolve` maps a sub-sprite castId to a member for the thumbnails.
+static void drawSubScorePreview(Score *score, CastMemberID memberID, Movie *resolve) {
+ if (!score || score->_scoreCache.empty()) {
+ ImGui::TextDisabled("No frames loaded");
+ return;
+ }
+
+ int numFrames = (int)score->_scoreCache.size();
+
+ int maxChannel = 0;
+ for (int f = 0; f < numFrames; f++) {
+ Frame *frame = score->_scoreCache[f];
+ if (!frame)
+ continue;
+ for (int ch = 1; ch < (int)frame->_sprites.size(); ch++) {
+ if (frame->_sprites[ch] && frame->_sprites[ch]->_castId.member != 0)
+ maxChannel = MAX(maxChannel, ch);
+ }
+ }
+
+ if (maxChannel == 0) {
+ ImGui::TextDisabled("No sprite data");
+ return;
+ }
+
+ auto &frames = _state->_castDetails._filmLoopCurrentFrame;
+ if (!frames.contains(memberID))
+ frames[memberID] = 0;
+ int ¤tFrame = frames[memberID];
+ if (currentFrame >= numFrames)
+ currentFrame = 0;
+
+ const float cellW = 30.0f;
+ const float cellH = 18.0f;
+ const float labelW = 24.0f;
+ const float rulerH = 16.0f;
+ float gridW = labelW + numFrames * cellW;
+ float gridH = rulerH + maxChannel * cellH;
+
+ ImGui::Spacing();
+ ImGui::Text("Score");
+ ImGui::Separator();
+
+ if (ImGui::Button("|<")) currentFrame = 0;
+ ImGui::SameLine();
+ if (ImGui::Button("<") && currentFrame > 0) currentFrame--;
+ ImGui::SameLine();
+ ImGui::Text("Frame %d / %d", currentFrame + 1, numFrames);
+ ImGui::SameLine();
+ if (ImGui::Button(">") && currentFrame < numFrames - 1) currentFrame++;
+ ImGui::SameLine();
+ if (ImGui::Button(">|")) currentFrame = numFrames - 1;
+
+ // Frame preview: thumbnails of the active sprites in the current frame.
+ Frame *previewFrame = score->_scoreCache[currentFrame];
+ if (previewFrame && resolve) {
+ const float thumbSize = 48.0f;
+ bool anySprite = false;
+ for (int ch = 1; ch <= maxChannel; ch++) {
+ if (ch >= (int)previewFrame->_sprites.size())
+ break;
+ Sprite *sp = previewFrame->_sprites[ch];
+ if (!sp || sp->_castId.member == 0)
+ continue;
+ CastMember *cm = resolve->getCastMember(sp->_castId);
+ if (!cm)
+ continue;
+ if (!anySprite) {
+ ImGui::Separator();
+ ImGui::Text("Frame preview");
+ anySprite = true;
+ }
+ ImGuiImage imgID = getImageID(cm);
+ ImGui::BeginGroup();
+ if (imgID.id) {
+ showImage(imgID, cm->getName().c_str(), thumbSize);
+ } else {
+ ImVec2 pos = ImGui::GetCursorScreenPos();
+ ImGui::GetWindowDrawList()->AddRect(pos, ImVec2(pos.x + thumbSize, pos.y + thumbSize), _state->theme->borderColor);
+ ImGui::Dummy(ImVec2(thumbSize, thumbSize));
+ }
+ ImGui::Text("ch%d", ch);
+ ImGui::EndGroup();
+ ImGui::SameLine();
+ }
+ if (anySprite)
+ ImGui::NewLine();
+ }
+ ImGui::Separator();
+
+ float scrollbarH = ImGui::GetStyle().ScrollbarSize;
+ ImGui::BeginChild("##SubScoreChild", ImVec2(0, MIN(gridH + scrollbarH + 4.0f, 200.0f)), false, ImGuiWindowFlags_HorizontalScrollbar);
+
+ ImDrawList *dl = ImGui::GetWindowDrawList();
+ ImVec2 origin = ImGui::GetCursorScreenPos();
+
+ // Ruler
+ for (int f = 0; f < numFrames; f++) {
+ float x = origin.x + labelW + f * cellW;
+ float y = origin.y;
+ ImVec2 rMin = ImVec2(x, y);
+ ImVec2 rMax = ImVec2(x + cellW, y + rulerH);
+ ImU32 rulerCol = ((f + 1) % 5 == 0) ? _state->theme->tableDarkColor : _state->theme->tableLightColor;
+ dl->AddRectFilled(rMin, rMax, rulerCol);
+ addThinRect(dl, rMin, rMax, _state->theme->borderColor);
+ Common::String label = Common::String::format("%d", f + 1);
+ ImVec2 textSz = ImGui::CalcTextSize(label.c_str());
+ dl->AddText(ImVec2(x + (cellW - textSz.x) * 0.5f, y + (rulerH - textSz.y) * 0.5f), _state->theme->gridTextColor, label.c_str());
+ }
+
+ // Playhead
+ {
+ float px = origin.x + labelW + currentFrame * cellW;
+ dl->AddLine(ImVec2(px, origin.y), ImVec2(px, origin.y + gridH), _state->theme->playhead_color, 2.0f);
+ dl->AddTriangleFilled(
+ ImVec2(px - 5.0f, origin.y),
+ ImVec2(px + 5.0f, origin.y),
+ ImVec2(px, origin.y + 8.0f),
+ _state->theme->playhead_color);
+ }
+
+ // Channel rows
+ for (int ch = 1; ch <= maxChannel; ch++) {
+ float y = origin.y + rulerH + (ch - 1) * cellH;
+
+ ImVec2 lblMin = ImVec2(origin.x, y);
+ ImVec2 lblMax = ImVec2(origin.x + labelW, y + cellH);
+ dl->AddRectFilled(lblMin, lblMax, _state->theme->tableDarkColor);
+ addThinRect(dl, lblMin, lblMax, _state->theme->borderColor);
+ Common::String chLabel = Common::String::format("%d", ch);
+ ImVec2 chSz = ImGui::CalcTextSize(chLabel.c_str());
+ dl->AddText(ImVec2(origin.x + (labelW - chSz.x) * 0.5f, y + (cellH - chSz.y) * 0.5f), _state->theme->gridTextColor, chLabel.c_str());
+
+ for (int f = 0; f < numFrames; f++) {
+ float x = origin.x + labelW + f * cellW;
+ ImVec2 cMin = ImVec2(x, y);
+ ImVec2 cMax = ImVec2(x + cellW, y + cellH);
+ ImU32 col = ((f + 1) % 5 == 0) ? _state->theme->tableDarkColor : _state->theme->tableLightColor;
+ dl->AddRectFilled(cMin, cMax, col);
+ addThinRect(dl, cMin, cMax, _state->theme->borderColor);
+ }
+
+ int f = 0;
+ while (f < numFrames) {
+ Frame *frame = score->_scoreCache[f];
+ if (!frame || ch >= (int)frame->_sprites.size() || !frame->_sprites[ch] || frame->_sprites[ch]->_castId.member == 0) {
+ f++;
+ continue;
+ }
+
+ int spanStart = f;
+ int memberNum = frame->_sprites[ch]->_castId.member;
+ int spanEnd = f;
+ while (spanEnd + 1 < numFrames) {
+ Frame *nf = score->_scoreCache[spanEnd + 1];
+ if (nf && ch < (int)nf->_sprites.size() && nf->_sprites[ch] && nf->_sprites[ch]->_castId.member == memberNum)
+ spanEnd++;
+ else
+ break;
+ }
+
+ float x1 = origin.x + labelW + spanStart * cellW;
+ float x2 = origin.x + labelW + (spanEnd + 1) * cellW;
+ float cy = y + cellH * 0.5f;
+ float pad = 1.0f;
+
+ int colorIdx = memberNum % 6;
+ ImU32 barColor = _state->theme->contColors[colorIdx];
+
+ dl->AddRectFilled(ImVec2(x1, y + pad), ImVec2(x2 - 1.0f, y + cellH - pad), barColor);
+ dl->AddLine(ImVec2(x1 + 6.0f, cy), ImVec2(x2 - 6.0f, cy), _state->theme->gridTextColor, 1.0f);
+ dl->AddCircle(ImVec2(x1 + 4.0f, cy), 3.0f, _state->theme->gridTextColor, 0, 1.5f);
+ dl->AddRect(ImVec2(x2 - 7.0f, cy - 3.0f), ImVec2(x2 - 1.0f, cy + 3.0f), _state->theme->gridTextColor, 0.0f, 0, 1.5f);
+
+ float spanW = x2 - x1 - 8.0f;
+ Common::String label = Common::String::format("%d", memberNum);
+ if (spanW > 4.0f) {
+ float textY = y + (cellH - ImGui::GetTextLineHeight()) * 0.5f;
+ Common::String clipped = label;
+ if (ImGui::CalcTextSize(clipped.c_str()).x > spanW)
+ clipped = "";
+ if (!clipped.empty())
+ dl->AddText(ImVec2(x1 + 4.0f, textY), _state->theme->gridTextColor, clipped.c_str());
+ }
+
+ ImGui::SetCursorScreenPos(ImVec2(x1, y));
+ ImGui::InvisibleButton(Common::String::format("##ss_%d_%d", ch, spanStart).c_str(), ImVec2(x2 - x1, cellH));
+ if (ImGui::IsItemHovered()) {
+ ImGui::BeginTooltip();
+ ImGui::Text("Channel %d | Frames %d-%d | Cast member %d", ch, spanStart + 1, spanEnd + 1, memberNum);
+ ImGui::EndTooltip();
+ }
+
+ f = spanEnd + 1;
+ }
+ }
+
+ ImGui::SetCursorScreenPos(ImVec2(origin.x, origin.y + gridH));
+ ImGui::Dummy(ImVec2(gridW, 0));
+
+ ImGui::EndChild();
+}
+
+// A movie cast member links an external movie whose score runs live.
+// Show its metadata plus the embedded score, like the film-loop viewer.
+void drawMovieCMprops(MovieCastMember *member) {
+ assert(member != nullptr);
+ if (ImGui::BeginTabItem("Movie")) {
+ Movie *linked = member->_linkedMovie;
+ Score *score = member->_score;
+
+ if (ImGui::CollapsingHeader("Properties", ImGuiTreeNodeFlags_DefaultOpen)) {
+ if (ImGui::BeginTable("##MovieProps", 2, ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders)) {
+ Common::String path;
+ if (linked && linked->getArchive())
+ path = linked->getArchive()->getPathName().toString();
+ else
+ path = member->getCast()->getLinkedPath(member->getID());
+ showProperty("linkedMovie", "%s", path.empty() ? "(none)" : path.c_str());
+ showPropertyBool("scriptsEnabled", member->_enableScripts);
+ showPropertyBool("enableSound", member->_enableSound);
+ showPropertyBool("looping", member->_looping);
+ showPropertyBool("crop", member->_crop);
+ showPropertyBool("center", member->_center);
+ if (score) {
+ showProperty("frameCount", "%d", (int)score->_scoreCache.size());
+ showProperty("currentFrame", "%d", (int)score->getCurrentFrameNum());
+ showProperty("playState", "%s", moviePlayStateStr(score->_playState));
+ showProperty("channels", "%d", (int)score->_channels.size());
+ }
+ ImGui::EndTable();
+ }
+ }
+
+ if (!linked || !score) {
+ ImGui::TextDisabled("Linked movie not loaded (put the member on stage to load it)");
+ } else {
+ drawSubScorePreview(score, CastMemberID(member->getID(), member->getCast()->_castLibID), linked);
+ }
+
+ ImGui::EndTabItem();
+ }
+}
+
// Channel used exclusively for sound previews in the debugger.
// Chosen high enough to avoid collision with normal score sound channels (1, 2).
static const int kDebugSoundChannel = 8;
@@ -1059,6 +1317,9 @@ void drawCMTypeProps(CastMember *member) {
case kCastFilmLoop:
drawFilmLoopCMprops(static_cast<FilmLoopCastMember *>(member));
break;
+ case kCastMovie:
+ drawMovieCMprops(static_cast<MovieCastMember *>(member));
+ break;
case kCastSound:
drawSoundCMprops(static_cast<SoundCastMember *>(member));
break;
@@ -1066,7 +1327,6 @@ void drawCMTypeProps(CastMember *member) {
case kCastTypeNull:
case kCastPalette:
case kCastPicture:
- case kCastMovie:
case kCastDigitalVideo:
case kCastLingoScript:
case kCastOLE:
Commit: bc035f8599a3393f7829b5f53282c1cd82c7a9f0
https://github.com/scummvm/scummvm/commit/bc035f8599a3393f7829b5f53282c1cd82c7a9f0
Author: ramyak-sharma (ramyaksharma1 at gmail.com)
Date: 2026-08-07T22:33:01+02:00
Commit Message:
DIRECTOR: DT: Add a quick-open palette (Ctrl+P)
Ctrl+P opens a search box that jumps to any cast member (by name/number)
or Lingo handler (by name) in the current movie: cast members open in
Cast Details, handlers open in the Scripts window. Enter selects the top
match, Esc closes.
Changed paths:
engines/director/debugger/debugtools.cpp
engines/director/debugger/dt-internal.h
engines/director/debugger/dt-scripts.cpp
diff --git a/engines/director/debugger/debugtools.cpp b/engines/director/debugger/debugtools.cpp
index 499407e10bd..251de1a54da 100644
--- a/engines/director/debugger/debugtools.cpp
+++ b/engines/director/debugger/debugtools.cpp
@@ -1059,6 +1059,11 @@ void onImGuiRender() {
handleDebuggerShortcuts();
+ if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_P, ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverFocused)) {
+ _state->_quickOpen = true;
+ _state->_quickOpenInput[0] = '\0';
+ }
+
if (ImGui::BeginMainMenuBar()) {
if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_2, ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverFocused))
_state->_w.controlPanel = !_state->_w.controlPanel;
@@ -1117,6 +1122,7 @@ void onImGuiRender() {
showExecutionContext();
showScriptsWindow();
+ showQuickOpen();
showControlPanel();
showVars();
diff --git a/engines/director/debugger/dt-internal.h b/engines/director/debugger/dt-internal.h
index bcb94e2d390..682200e8c7f 100644
--- a/engines/director/debugger/dt-internal.h
+++ b/engines/director/debugger/dt-internal.h
@@ -288,6 +288,10 @@ typedef struct ImGuiState {
ScriptData _openScripts;
bool _showCompleteScript = true;
+ // Quick-open (command palette): jump to a cast member or handler by name.
+ bool _quickOpen = false;
+ char _quickOpenInput[256] = {};
+
Common::HashMap<Common::String, bool, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> _variables;
int _prevFrame = -1;
struct {
@@ -405,6 +409,7 @@ void renderScriptAST(ImGuiScript &script, bool showByteCode, bool scrollTo);
void showFuncList();
void showExecutionContext();
void showScriptsWindow();
+void showQuickOpen();
// dt-save-state.cpp
void saveCurrentState();
diff --git a/engines/director/debugger/dt-scripts.cpp b/engines/director/debugger/dt-scripts.cpp
index 74ab7ec8862..06f320b2c32 100644
--- a/engines/director/debugger/dt-scripts.cpp
+++ b/engines/director/debugger/dt-scripts.cpp
@@ -266,6 +266,143 @@ static void updateCurrentScript() {
setScriptToDisplay(script);
}
+// Quick open (command palette)
+
+struct QuickOpenItem {
+ Common::String label;
+ bool isHandler = false;
+ CastMemberID id;
+ ScriptType scriptType = kScoreScript;
+ Common::String handlerId;
+ Common::String handlerName;
+};
+
+static bool qoMatch(const Common::String &label, const char *q) {
+ if (!q || !q[0])
+ return true;
+ Common::String l = label;
+ l.toLowercase();
+ Common::String s(q);
+ s.toLowercase();
+ return l.contains(s);
+}
+
+static void gatherQuickOpen(Movie *movie, Common::Array<QuickOpenItem> &out) {
+ out.clear();
+ if (!movie)
+ return;
+
+ for (auto it : *movie->getCasts()) {
+ Cast *cast = it._value;
+ if (!cast || !cast->_loadedCast)
+ continue;
+ for (auto &m : *cast->_loadedCast) {
+ if (!m._value)
+ continue;
+ QuickOpenItem qi;
+ qi.id = CastMemberID(m._key, cast->_castLibID);
+ qi.label = getDisplayName(m._value) + " [cast]";
+ out.push_back(qi);
+ }
+ }
+
+ for (auto it : *movie->getCasts()) {
+ Cast *cast = it._value;
+ if (!cast || !cast->_lingoArchive)
+ continue;
+ for (int i = 0; i <= kMaxScriptType; i++) {
+ for (auto &sc : cast->_lingoArchive->scriptContexts[i]) {
+ if (!sc._value)
+ continue;
+ for (auto &fh : sc._value->_functionHandlers) {
+ QuickOpenItem qi;
+ qi.isHandler = true;
+ qi.id = CastMemberID(sc._key, it._key);
+ qi.scriptType = sc._value->_scriptType;
+ qi.handlerId = fh._key;
+ qi.handlerName = getHandlerName(fh._value);
+ qi.label = qi.handlerName + Common::String::format(" [handler, script %d]", sc._key);
+ out.push_back(qi);
+ }
+ }
+ }
+ }
+}
+
+static void openQuickOpen(const QuickOpenItem &qi, Movie *movie) {
+ if (!qi.isHandler) {
+ _state->_castDetails._castMemberID = qi.id;
+ _state->_castDetails._window = movieId(movie);
+ _state->_w.castDetails = true;
+ return;
+ }
+ ScriptContext *ctx = getScriptContext(qi.id);
+ if (!ctx)
+ return;
+ ImGuiScript script = toImGuiScript(qi.scriptType, qi.id, qi.handlerId);
+ script.byteOffsets = ctx->_functionByteOffsets[script.handlerId];
+ if (movie->getArchive())
+ script.moviePath = movie->getArchive()->getPathName().toString();
+ script.handlerName = qi.handlerName;
+ addToOpenHandlers(script);
+}
+
+void showQuickOpen() {
+ static Common::Array<QuickOpenItem> items;
+ static bool gathered = false;
+
+ if (!_state->_quickOpen) {
+ gathered = false;
+ return;
+ }
+
+ Movie *movie = g_director->getCurrentMovie();
+
+ ImVec2 center = ImGui::GetMainViewport()->GetCenter();
+ ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
+ ImGui::SetNextWindowSize(ImVec2(560, 420), ImGuiCond_Appearing);
+ if (!gathered)
+ ImGui::SetNextWindowFocus();
+
+ if (ImGui::Begin("Quick Open", &_state->_quickOpen)) {
+ bool justOpened = !gathered;
+ if (!gathered) {
+ gatherQuickOpen(movie, items);
+ gathered = true;
+ }
+ if (justOpened)
+ ImGui::SetKeyboardFocusHere();
+
+ ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
+ bool enter = ImGui::InputText("##qoInput", _state->_quickOpenInput, sizeof(_state->_quickOpenInput),
+ ImGuiInputTextFlags_EnterReturnsTrue);
+ ImGui::Separator();
+
+ ImGui::BeginChild("##qoList");
+ int shown = 0, firstIdx = -1;
+ for (uint i = 0; i < items.size(); i++) {
+ if (!qoMatch(items[i].label, _state->_quickOpenInput))
+ continue;
+ if (firstIdx < 0)
+ firstIdx = (int)i;
+ bool pick = ImGui::Selectable(items[i].label.c_str());
+ if (pick || (enter && (int)i == firstIdx)) {
+ openQuickOpen(items[i], movie);
+ _state->_quickOpen = false;
+ }
+ if (++shown >= 300)
+ break;
+ }
+ if (shown == 0)
+ ImGui::TextDisabled("No matches");
+ ImGui::EndChild();
+
+ if (ImGui::IsKeyPressed(ImGuiKey_Escape))
+ _state->_quickOpen = false;
+ }
+ ImGui::End();
+}
+
void showFuncList() {
if (!_state->_w.funcList)
return;
Commit: 04cd014d84be4dbcfb80c2105370e8b7598be76d
https://github.com/scummvm/scummvm/commit/04cd014d84be4dbcfb80c2105370e8b7598be76d
Author: ramyak-sharma (ramyaksharma1 at gmail.com)
Date: 2026-08-07T22:33:01+02:00
Commit Message:
DIRECTOR: DT: Add pick-from-stage
"View > Pick from stage" arms a mode where the next click on the stage
selects the sprite under the cursor: it opens that cast member in Cast
Details and highlights its cell in the Score window. Esc cancels.
Changed paths:
engines/director/debugger/debugtools.cpp
engines/director/debugger/dt-internal.h
diff --git a/engines/director/debugger/debugtools.cpp b/engines/director/debugger/debugtools.cpp
index 251de1a54da..1fecb7b4cca 100644
--- a/engines/director/debugger/debugtools.cpp
+++ b/engines/director/debugger/debugtools.cpp
@@ -1034,6 +1034,46 @@ static void invalidateStaleCaches() {
_state->_castDetails._filmLoopCurrentFrame.clear();
}
+// Pick-from-stage: when armed, the next click on the stage (outside any DT
+// window) selects the sprite under the cursor and opens it in Cast Details.
+static void handlePickFromStage() {
+ if (!_state->_pickMode)
+ return;
+
+ ImGui::SetTooltip("Pick: click a sprite on the stage (Esc to cancel)");
+
+ if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
+ _state->_pickMode = false;
+ return;
+ }
+
+ // Select on button-down but stay armed until release, so isMouseInputIgnored()
+ // swallows both mouseDown and mouseUp instead of leaking the release to the game.
+ bool released = ImGui::IsMouseReleased(ImGuiMouseButton_Left);
+
+ if (!ImGui::GetIO().WantCaptureMouse && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
+ Window *stage = g_director->getStage();
+ Movie *movie = stage ? stage->getCurrentMovie() : nullptr;
+ if (movie) {
+ Score *score = movie->getScore();
+ uint16 id = score->getSpriteIDFromPos(stage->getMousePos());
+ Channel *ch = id ? score->getChannelById(id) : nullptr;
+ if (ch && ch->_sprite && !ch->_sprite->_castId.isNull()) {
+ _state->_castDetails._castMemberID = ch->_sprite->_castId;
+ _state->_castDetails._window = movieId(movie);
+ _state->_w.castDetails = true;
+ _state->_scoreWindow = movieId(movie);
+ _state->_selectedScoreCast.frame = MAX(0, (int)score->getCurrentFrameNum() - 1);
+ _state->_selectedScoreCast.channel = id;
+ _state->_scrollToChannel = true;
+ }
+ }
+ }
+
+ if (released)
+ _state->_pickMode = false;
+}
+
void onImGuiRender() {
if (!debugChannelSet(-1, kDebugImGui)) {
ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange | ImGuiConfigFlags_NoMouse;
@@ -1064,6 +1104,8 @@ void onImGuiRender() {
_state->_quickOpenInput[0] = '\0';
}
+ handlePickFromStage();
+
if (ImGui::BeginMainMenuBar()) {
if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_2, ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverFocused))
_state->_w.controlPanel = !_state->_w.controlPanel;
@@ -1105,6 +1147,13 @@ void onImGuiRender() {
ImGui::MenuItem("Windows", NULL, &_state->_w.windows);
ImGui::MenuItem("Execution Context", NULL, &_state->_w.executionContext);
+ ImGui::Separator();
+ if (ImGui::MenuItem("Pick from stage", NULL, _state->_pickMode)) {
+ _state->_pickMode = !_state->_pickMode;
+ if (_state->_pickMode)
+ g_system->displayMessageOnOSD(Common::U32String("Pick: click a sprite on the stage"));
+ }
+
ImGui::SeparatorText("Misc");
if (ImGui::MenuItem("Save state")) {
saveCurrentState();
diff --git a/engines/director/debugger/dt-internal.h b/engines/director/debugger/dt-internal.h
index 682200e8c7f..20f47a3a8f3 100644
--- a/engines/director/debugger/dt-internal.h
+++ b/engines/director/debugger/dt-internal.h
@@ -292,6 +292,9 @@ typedef struct ImGuiState {
bool _quickOpen = false;
char _quickOpenInput[256] = {};
+ // Pick-from-stage: next stage click selects the sprite under the cursor.
+ bool _pickMode = false;
+
Common::HashMap<Common::String, bool, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> _variables;
int _prevFrame = -1;
struct {
Commit: 483425c957aa666b337ecc56fc2cfceef4e12f9a
https://github.com/scummvm/scummvm/commit/483425c957aa666b337ecc56fc2cfceef4e12f9a
Author: ramyak-sharma (ramyaksharma1 at gmail.com)
Date: 2026-08-07T22:33:01+02:00
Commit Message:
DIRECTOR: DT: Conditional function breakpoints
Add an optional Lingo expression to a function breakpoint; it fires only
when the expression evaluates true. The Breakpoints window gains a
condition field per breakpoint, and bpTest() evaluates it (in the
current scope) before pausing, failing safe to a normal break on an
empty or unparseable condition.
Changed paths:
engines/director/debugger.cpp
engines/director/debugger.h
engines/director/debugger/dt-lists.cpp
diff --git a/engines/director/debugger.cpp b/engines/director/debugger.cpp
index 8a76d29461f..3cda88d8f52 100644
--- a/engines/director/debugger.cpp
+++ b/engines/director/debugger.cpp
@@ -1182,8 +1182,17 @@ void Debugger::bpTest(bool forceCheck) {
uint funcOffset = g_lingo->_state->pc;
Score *score = g_director->getCurrentMovie()->getScore();
uint frameOffset = score->getCurrentFrameNum();
- if (_bpCheckFunc) {
- stop |= _bpMatchFuncOffsets.contains(funcOffset);
+ if (_bpCheckFunc && _bpMatchFuncOffsets.contains(funcOffset)) {
+ // Fire only if a matching, enabled breakpoint's condition holds.
+ for (auto &it : g_lingo->getBreakpoints()) {
+ if (!it.enabled || it.type != kBreakpointFunction)
+ continue;
+ if (it.funcName.equalsIgnoreCase(_bpMatchFuncName) && it.scriptId == _bpMatchScriptId
+ && it.funcOffset == funcOffset && evalCondition(it.condition)) {
+ stop = true;
+ break;
+ }
+ }
}
if (_bpCheckMoviePath) {
stop |= _bpMatchFrameOffsets.contains(frameOffset);
@@ -1249,6 +1258,32 @@ bool Debugger::lingoEval(const char *inputOrig) {
return true;
}
+bool Debugger::evalCondition(const Common::String &cond) {
+ // No condition, or we are already inside an eval: fire the breakpoint.
+ if (cond.empty() || _lingoEval)
+ return true;
+
+ // Compile the expression to an anonymous handler that returns its value.
+ ScriptContext *sc = g_lingo->_compiler->compileAnonymous(Common::String("return (") + cond + ")");
+ if (!sc)
+ return true; // unparseable: don't silently swallow the breakpoint
+
+ Symbol sym = sc->_eventHandlers[kEventGeneric];
+ uint depth = g_lingo->_state->stack.size();
+ int targetFrame = (int)g_lingo->_state->callstack.size();
+ _lingoEval = true;
+ LC::call(sym, 0, true);
+ // Stop as soon as the condition handler returns. Without a target frame,
+ // execute() would keep running the game's own frames still on the callstack.
+ g_lingo->execute(targetFrame);
+ _lingoEval = false;
+
+ bool result = true;
+ if (g_lingo->_state->stack.size() > depth)
+ result = g_lingo->pop().asInt() != 0;
+ return result;
+}
+
void Debugger::stepHook() {
bpTest();
if (_step && _nextCounter == 0) {
diff --git a/engines/director/debugger.h b/engines/director/debugger.h
index 165bf14276d..5df60b238b9 100644
--- a/engines/director/debugger.h
+++ b/engines/director/debugger.h
@@ -59,6 +59,9 @@ struct Breakpoint {
bool varRead = false;
bool varWrite = false;
+ // Optional Lingo expression; the breakpoint only fires when it is true.
+ Common::String condition;
+
Common::String format() const;
};
@@ -133,6 +136,7 @@ private:
bool lingoCommandProcessor(const char *inputOrig);
bool lingoEval(const char *inputOrig);
+ bool evalCondition(const Common::String &cond);
Common::DumpFile _out;
diff --git a/engines/director/debugger/dt-lists.cpp b/engines/director/debugger/dt-lists.cpp
index c52081311a3..7d1634094e1 100644
--- a/engines/director/debugger/dt-lists.cpp
+++ b/engines/director/debugger/dt-lists.cpp
@@ -233,9 +233,9 @@ void showBreakpointList() {
ImGui::SetNextWindowSize(ImVec2(480, 240), ImGuiCond_FirstUseEver);
if (ImGui::Begin("Breakpoints", &_state->_w.bpList)) {
auto &bps = g_lingo->getBreakpoints();
- if (ImGui::BeginTable("BreakpointsTable", 5, ImGuiTableFlags_SizingFixedFit)) {
- for (uint i = 0; i < 5; i++)
- ImGui::TableSetupColumn(NULL, i == 2 ? ImGuiTableColumnFlags_WidthStretch : ImGuiTableColumnFlags_NoHeaderWidth);
+ if (ImGui::BeginTable("BreakpointsTable", 6, ImGuiTableFlags_SizingFixedFit)) {
+ for (uint i = 0; i < 6; i++)
+ ImGui::TableSetupColumn(NULL, (i == 2 || i == 5) ? ImGuiTableColumnFlags_WidthStretch : ImGuiTableColumnFlags_NoHeaderWidth);
for (uint i = 0; i < bps.size(); i++) {
if (bps[i].type != kBreakpointFunction)
@@ -303,6 +303,15 @@ void showBreakpointList() {
// offset
ImGui::TableNextColumn();
ImGui::Text("%d", bps[i].funcOffset);
+
+ // condition: fires only when this Lingo expression is true
+ ImGui::TableNextColumn();
+ char cond[128];
+ Common::strlcpy(cond, bps[i].condition.c_str(), sizeof(cond));
+ ImGui::SetNextItemWidth(-FLT_MIN);
+ if (ImGui::InputTextWithHint("##cond", "condition", cond, sizeof(cond)))
+ bps[i].condition = cond;
+
ImGui::PopID();
if (del) {
Commit: d28be41ad6d815c02ec849b9a5e8410b6c645da0
https://github.com/scummvm/scummvm/commit/d28be41ad6d815c02ec849b9a5e8410b6c645da0
Author: ramyak-sharma (ramyaksharma1 at gmail.com)
Date: 2026-08-07T22:33:01+02:00
Commit Message:
DIRECTOR: DT: Add rebindable keyboard shortcuts
Add a Help window with a keyboard-shortcuts table, descriptions, and tips.
Debugger actions now use configurable key bindings that can be rebound at
runtime, reset to their defaults, and saved/restored with the debugger
state. Also centralize shortcut dispatch through a common helper.
Changed paths:
A engines/director/debugger/dt-help.cpp
engines/director/debugger/debugtools.cpp
engines/director/debugger/dt-cast.cpp
engines/director/debugger/dt-controlpanel.cpp
engines/director/debugger/dt-internal.h
engines/director/debugger/dt-save-state.cpp
engines/director/debugger/dt-scripts.cpp
engines/director/module.mk
diff --git a/engines/director/debugger/debugtools.cpp b/engines/director/debugger/debugtools.cpp
index 1fecb7b4cca..2714d38f8f7 100644
--- a/engines/director/debugger/debugtools.cpp
+++ b/engines/director/debugger/debugtools.cpp
@@ -1006,6 +1006,7 @@ void onImGuiInit() {
_state->_logger = new ImGuiEx::ImGuiLogger;
setTheme(_state->_activeThemeID);
+ initShortcuts();
Common::setLogWatcher(onLog);
}
@@ -1088,7 +1089,8 @@ void onImGuiRender() {
ImGuiIO &io = ImGui::GetIO();
io.ConfigFlags &= ~(ImGuiConfigFlags_NoMouseCursorChange | ImGuiConfigFlags_NoMouse);
- if (ImGui::IsKeyChordPressed(ImGuiMod_Ctrl | ImGuiKey_F1)) {
+ if (_state->_shortcutCapture < 0 && _state->_shortcuts[kActToggleMouseIgnore] != ImGuiKey_None
+ && ImGui::IsKeyChordPressed(_state->_shortcuts[kActToggleMouseIgnore])) {
_state->_ignoreMouse = !_state->_ignoreMouse;
Common::String msg = Common::String::format("Debug Mouse Ignore: %s", _state->_ignoreMouse ? "ON" : "OFF");
@@ -1099,19 +1101,25 @@ void onImGuiRender() {
handleDebuggerShortcuts();
- if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_P, ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverFocused)) {
+ if (actionTriggered(kActQuickOpen)) {
_state->_quickOpen = true;
_state->_quickOpenInput[0] = '\0';
}
+ if (actionTriggered(kActPickFromStage)) {
+ _state->_pickMode = !_state->_pickMode;
+ if (_state->_pickMode)
+ g_system->displayMessageOnOSD(Common::U32String("Pick: click a sprite on the stage"));
+ }
+
handlePickFromStage();
if (ImGui::BeginMainMenuBar()) {
- if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_2, ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverFocused))
+ if (actionTriggered(kActToggleControlPanel))
_state->_w.controlPanel = !_state->_w.controlPanel;
- if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_3, ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverFocused))
+ if (actionTriggered(kActToggleCast))
_state->_w.cast = !_state->_w.cast;
- if (ImGui::Shortcut(ImGuiMod_Ctrl | ImGuiKey_4, ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverFocused))
+ if (actionTriggered(kActToggleScore))
_state->_w.score = !_state->_w.score;
if (ImGui::BeginMenu("View")) {
ImGui::SeparatorText("Windows");
@@ -1166,12 +1174,17 @@ void onImGuiRender() {
ImGui::EndMenu();
}
+ if (ImGui::BeginMenu("Help")) {
+ ImGui::MenuItem("Shortcuts & Tips", NULL, &_state->_w.help);
+ ImGui::EndMenu();
+ }
ImGui::EndMainMenuBar();
}
showExecutionContext();
showScriptsWindow();
showQuickOpen();
+ showHelp();
showControlPanel();
showVars();
@@ -1229,7 +1242,15 @@ void setSelectedChannel(int channel) {
}
bool isMouseInputIgnored() {
- if (!_state || !_state->_ignoreMouse)
+ if (!_state)
+ return false;
+
+ // While arming a stage pick, keep the selecting click out of the game so it
+ // only selects the sprite and does not fire its mouseDown/mouseUp handlers.
+ if (_state->_pickMode)
+ return true;
+
+ if (!_state->_ignoreMouse)
return false;
// Holding Shift temporarily allows mouse events to pass to the engine
diff --git a/engines/director/debugger/dt-cast.cpp b/engines/director/debugger/dt-cast.cpp
index 527488158d0..55513259a85 100644
--- a/engines/director/debugger/dt-cast.cpp
+++ b/engines/director/debugger/dt-cast.cpp
@@ -113,13 +113,6 @@ Common::String getDisplayName(CastMember *castMember) {
return Common::String::format("%u", castMember->getID());
}
-struct CastRowEntry {
- const Cast *cast = nullptr;
- CastMember *member = nullptr;
- int id = 0;
- Common::String name;
-};
-
// Collects the cast's members that pass the filters, sorted by member number.
static void gatherCastMembers(const Cast *cast, Common::Array<CastRowEntry> &rows) {
if (!cast || !cast->_loadedCast)
@@ -337,8 +330,8 @@ void showCast() {
// Gathering rows (load, name formatting, filtering) is costly for big
// casts, so cache them and rebuild only when movie/filters/size change.
- static Common::Array<CastRowEntry> rows;
- static Common::String rowsKey;
+ Common::Array<CastRowEntry> &rows = _state->_castRows;
+ Common::String &rowsKey = _state->_castRowsKey;
int total = 0;
for (auto it : *movie->getCasts())
diff --git a/engines/director/debugger/dt-controlpanel.cpp b/engines/director/debugger/dt-controlpanel.cpp
index 52eca10f1e4..b72a1503862 100644
--- a/engines/director/debugger/dt-controlpanel.cpp
+++ b/engines/director/debugger/dt-controlpanel.cpp
@@ -127,10 +127,9 @@ void handleDebuggerShortcuts() {
return;
Score *score = movie->getScore();
- const ImGuiInputFlags route = ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverFocused;
const bool running = (g_lingo->_exec._state == kRunning);
- if (ImGui::Shortcut(ImGuiKey_F5, route)) {
+ if (actionTriggered(kActContinue)) {
if (running) {
score->_playState = kPlayPaused;
dgbStop();
@@ -144,14 +143,14 @@ void handleDebuggerShortcuts() {
}
// Match the step buttons: pause when running, step when paused.
- // Shift+F11 must be tested before F11.
- if (ImGui::Shortcut(ImGuiMod_Shift | ImGuiKey_F11, route)) {
+ // Step Out is tested before Step Into so a shared chord resolves to Out.
+ if (actionTriggered(kActStepOut)) {
score->_playState = kPlayStarted;
running ? dgbStop() : dbgStepOut();
- } else if (ImGui::Shortcut(ImGuiKey_F11, route)) {
+ } else if (actionTriggered(kActStepInto)) {
score->_playState = kPlayStarted;
running ? dgbStop() : dbgStepInto();
- } else if (ImGui::Shortcut(ImGuiKey_F10, route)) {
+ } else if (actionTriggered(kActStepOver)) {
score->_playState = kPlayStarted;
running ? dgbStop() : dbgStepOver();
}
diff --git a/engines/director/debugger/dt-help.cpp b/engines/director/debugger/dt-help.cpp
new file mode 100644
index 00000000000..08cde3ab3a3
--- /dev/null
+++ b/engines/director/debugger/dt-help.cpp
@@ -0,0 +1,201 @@
+/* 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 "director/director.h"
+#include "director/debugger/dt-internal.h"
+
+namespace Director {
+namespace DT {
+
+const ShortcutDef kShortcutDefs[kActCount] = {
+ { "continue", "Continue / Break", "Resume execution, or pause it while running.", ImGuiKey_F5 },
+ { "stepOver", "Step Over", "Step over the current Lingo statement.", ImGuiKey_F10 },
+ { "stepInto", "Step Into", "Step into the called handler.", ImGuiKey_F11 },
+ { "stepOut", "Step Out", "Run until the current handler returns.", ImGuiMod_Shift | ImGuiKey_F11 },
+ { "quickOpen", "Quick Open", "Search and jump to any cast member or handler.", ImGuiMod_Ctrl | ImGuiKey_P },
+ { "pickFromStage", "Pick From Stage", "Arm pick mode: the next stage click selects that sprite.", ImGuiMod_Ctrl | ImGuiKey_K },
+ { "toggleControlPanel","Toggle Control Panel", "Show or hide the Control Panel window.", ImGuiMod_Ctrl | ImGuiKey_2 },
+ { "toggleCast", "Toggle Cast", "Show or hide the Cast window.", ImGuiMod_Ctrl | ImGuiKey_3 },
+ { "toggleScore", "Toggle Score", "Show or hide the Score window.", ImGuiMod_Ctrl | ImGuiKey_4 },
+ { "toggleMouseIgnore", "Toggle Mouse Passthrough","Ignore the debugger's mouse so clicks reach the stage.", ImGuiMod_Ctrl | ImGuiKey_F1 },
+};
+
+void initShortcuts() {
+ for (int i = 0; i < kActCount; i++)
+ _state->_shortcuts[i] = kShortcutDefs[i].defaultChord;
+}
+
+void resetShortcuts() {
+ initShortcuts();
+}
+
+bool actionTriggered(DebuggerAction act) {
+ // While rebinding, keypresses feed the capture UI, not the actions.
+ if (_state->_shortcutCapture >= 0)
+ return false;
+ ImGuiKeyChord chord = _state->_shortcuts[act];
+ if (chord == ImGuiKey_None)
+ return false;
+ return ImGui::Shortcut(chord, ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverFocused);
+}
+
+static Common::String chordName(ImGuiKeyChord chord) {
+ if (chord == ImGuiKey_None)
+ return "(unbound)";
+ Common::String s;
+ if (chord & ImGuiMod_Ctrl) s += "Ctrl+";
+ if (chord & ImGuiMod_Shift) s += "Shift+";
+ if (chord & ImGuiMod_Alt) s += "Alt+";
+ if (chord & ImGuiMod_Super) s += "Super+";
+ const ImGuiKeyChord modMask = ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiMod_Alt | ImGuiMod_Super;
+ ImGuiKey key = (ImGuiKey)(chord & ~modMask);
+ if (key != ImGuiKey_None)
+ s += ImGui::GetKeyName(key);
+ else if (!s.empty())
+ s.deleteLastChar(); // drop the trailing '+' when only modifiers are held
+ return s;
+}
+
+static bool isModifierKey(ImGuiKey k) {
+ return k == ImGuiKey_LeftCtrl || k == ImGuiKey_RightCtrl ||
+ k == ImGuiKey_LeftShift || k == ImGuiKey_RightShift ||
+ k == ImGuiKey_LeftAlt || k == ImGuiKey_RightAlt ||
+ k == ImGuiKey_LeftSuper || k == ImGuiKey_RightSuper;
+}
+
+// While rebinding, accumulate the held chord and commit it on full release.
+// Escape cancels; a bare Backspace/Delete clears the binding.
+static void captureChord() {
+ if (_state->_shortcutCapture < 0) {
+ _state->_shortcutPending = ImGuiKey_None;
+ return;
+ }
+
+ if (ImGui::IsKeyPressed(ImGuiKey_Escape, false)) {
+ _state->_shortcutCapture = -1;
+ _state->_shortcutPending = ImGuiKey_None;
+ return;
+ }
+
+ const ImGuiKeyChord modMask = ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiMod_Alt | ImGuiMod_Super;
+ ImGuiKeyChord mods = ImGui::GetIO().KeyMods;
+
+ // The topmost non-modifier key currently held (if any).
+ ImGuiKey heldKey = ImGuiKey_None;
+ for (ImGuiKey k = ImGuiKey_NamedKey_BEGIN; k < ImGuiKey_NamedKey_END; k = (ImGuiKey)(k + 1)) {
+ if (isModifierKey(k) || k == ImGuiKey_Escape)
+ continue;
+ if (ImGui::IsKeyDown(k)) {
+ heldKey = k;
+ break;
+ }
+ }
+
+ bool anyHeld = (heldKey != ImGuiKey_None) || (mods != 0);
+ if (anyHeld) {
+ // Keep every modifier seen this gesture (so releasing Ctrl early doesn't
+ // drop it) plus the last non-modifier key, making release order irrelevant.
+ ImGuiKeyChord accumMods = (_state->_shortcutPending & modMask) | mods;
+ ImGuiKey key = (heldKey != ImGuiKey_None) ? heldKey : (ImGuiKey)(_state->_shortcutPending & ~modMask);
+ _state->_shortcutPending = accumMods | key;
+ return;
+ }
+
+ // Every key is released. Commit only if a real (non-modifier) key was captured.
+ ImGuiKeyChord pending = _state->_shortcutPending;
+ ImGuiKey key = (ImGuiKey)(pending & ~modMask);
+ if (key == ImGuiKey_None) {
+ // Only modifiers were pressed; discard and keep waiting for a real chord.
+ _state->_shortcutPending = ImGuiKey_None;
+ return;
+ }
+ if ((pending & modMask) == 0 && (key == ImGuiKey_Backspace || key == ImGuiKey_Delete))
+ _state->_shortcuts[_state->_shortcutCapture] = ImGuiKey_None; // clear
+ else
+ _state->_shortcuts[_state->_shortcutCapture] = pending;
+ _state->_shortcutCapture = -1;
+ _state->_shortcutPending = ImGuiKey_None;
+}
+
+void showHelp() {
+ if (!_state->_w.help)
+ return;
+
+ ImGui::SetNextWindowSize(ImVec2(600, 640), ImGuiCond_FirstUseEver);
+ if (ImGui::Begin("Help", &_state->_w.help)) {
+ captureChord();
+
+ if (ImGui::CollapsingHeader("Keyboard shortcuts", ImGuiTreeNodeFlags_DefaultOpen)) {
+ ImGui::TextDisabled("Click a key to rebind. Esc cancels, Backspace/Delete clears.");
+ if (ImGui::BeginTable("##shortcuts", 3,
+ ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp)) {
+ ImGui::TableSetupColumn("Action", ImGuiTableColumnFlags_WidthFixed, 150.0f);
+ ImGui::TableSetupColumn("Description");
+ ImGui::TableSetupColumn("Key", ImGuiTableColumnFlags_WidthFixed, 130.0f);
+ ImGui::TableHeadersRow();
+
+ for (int i = 0; i < kActCount; i++) {
+ ImGui::TableNextRow();
+ ImGui::PushID(i);
+
+ ImGui::TableSetColumnIndex(0);
+ ImGui::TextUnformatted(kShortcutDefs[i].label);
+
+ ImGui::TableSetColumnIndex(1);
+ ImGui::TextWrapped("%s", kShortcutDefs[i].help);
+
+ ImGui::TableSetColumnIndex(2);
+ Common::String key;
+ if (_state->_shortcutCapture == i)
+ key = (_state->_shortcutPending != ImGuiKey_None) ? chordName(_state->_shortcutPending) + " ..." : Common::String("press keys...");
+ else
+ key = chordName(_state->_shortcuts[i]);
+ if (ImGui::Button(key.c_str(), ImVec2(-FLT_MIN, 0)))
+ _state->_shortcutCapture = (_state->_shortcutCapture == i) ? -1 : i;
+
+ ImGui::PopID();
+ }
+ ImGui::EndTable();
+ }
+ if (ImGui::Button("Reset to defaults"))
+ resetShortcuts();
+ ImGui::SameLine();
+ if (ImGui::Button("Save"))
+ saveCurrentState();
+ }
+
+ if (ImGui::CollapsingHeader("Tips", ImGuiTreeNodeFlags_DefaultOpen)) {
+ ImGui::BulletText("Quick Open finds cast members by name/number and handlers by name;\nEnter picks the top match.");
+ ImGui::BulletText("Pick From Stage: arm it, then click a sprite on the stage to open it in\nCast Details (also selects its Score cell). Puppeted sprites work too.");
+ ImGui::BulletText("Breakpoints: each function breakpoint has a Condition field. Enter a Lingo\nexpression and it only fires when the expression is true.");
+ ImGui::BulletText("Vars: right-click a variable for \"Copy value\". Click a variable name to\nadd/remove it from Watched Vars.");
+ ImGui::BulletText("Cast list: click the Name/ID/Type column headers to sort.");
+ ImGui::BulletText("Cast Details on a movie cast member has a Movie tab showing its embedded\nscore, laid out like the film-loop viewer.");
+ ImGui::BulletText("Score: click a cell to open that cast member in Cast Details.");
+ ImGui::BulletText("Settings: Enable Multi-Viewport lets debugger windows leave the main window.");
+ ImGui::BulletText("Save/Load state (View menu) persists open windows, layout, and these shortcuts.");
+ }
+ }
+ ImGui::End();
+}
+
+} // namespace DT
+} // namespace Director
diff --git a/engines/director/debugger/dt-internal.h b/engines/director/debugger/dt-internal.h
index 20f47a3a8f3..26354aee31d 100644
--- a/engines/director/debugger/dt-internal.h
+++ b/engines/director/debugger/dt-internal.h
@@ -103,8 +103,31 @@ typedef struct ImGuiWindows {
bool search = false;
bool imageViewer = false;
bool windows = false;
+ bool help = false;
} ImGuiWindows;
+// Rebindable debugger actions. Keep in sync with kShortcutDefs (dt-help.cpp).
+enum DebuggerAction {
+ kActContinue = 0,
+ kActStepOver,
+ kActStepInto,
+ kActStepOut,
+ kActQuickOpen,
+ kActPickFromStage,
+ kActToggleControlPanel,
+ kActToggleCast,
+ kActToggleScore,
+ kActToggleMouseIgnore,
+ kActCount
+};
+
+typedef struct ShortcutDef {
+ const char *id; // stable key for save/load
+ const char *label; // display name
+ const char *help; // what it does
+ ImGuiKeyChord defaultChord;
+} ShortcutDef;
+
enum SearchMode {
kSearchAll = 0,
@@ -188,6 +211,22 @@ struct DebuggerTheme {
ImVec4 logger_debug;
};
+struct QuickOpenItem {
+ Common::String label;
+ bool isHandler = false;
+ CastMemberID id;
+ ScriptType scriptType = kScoreScript;
+ Common::String handlerId;
+ Common::String handlerName;
+};
+
+struct CastRowEntry {
+ const Cast *cast = nullptr;
+ CastMember *member = nullptr;
+ int id = 0;
+ Common::String name;
+};
+
typedef struct ImGuiState {
struct WatchLogEntry {
@@ -295,6 +334,11 @@ typedef struct ImGuiState {
// Pick-from-stage: next stage click selects the sprite under the cursor.
bool _pickMode = false;
+ // Rebindable shortcut chords, indexed by DebuggerAction; -1 = not capturing.
+ ImGuiKeyChord _shortcuts[kActCount] = {};
+ int _shortcutCapture = -1;
+ ImGuiKeyChord _shortcutPending = ImGuiKey_None; // chord being held during a rebind
+
Common::HashMap<Common::String, bool, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> _variables;
int _prevFrame = -1;
struct {
@@ -348,6 +392,13 @@ typedef struct ImGuiState {
bool _enableMultiViewport = true;
Window *_windowToRedraw = nullptr;
+
+ // Cached UI lists. Kept in the state (not file-static) so their Common::Strings
+ // free in onImGuiCleanup while g_system is alive, not at process exit.
+ Common::Array<CastRowEntry> _castRows;
+ Common::String _castRowsKey;
+ Common::Array<QuickOpenItem> _quickOpenItems;
+ bool _quickOpenGathered = false;
} ImGuiState;
// debugtools.cpp
@@ -394,6 +445,13 @@ void showCastDetails(); // dt-castdetails.cpp
void showControlPanel();// dt-controlpanel.cpp
void handleDebuggerShortcuts(); // dt-controlpanel.cpp
+// dt-help.cpp
+extern const ShortcutDef kShortcutDefs[kActCount];
+void initShortcuts(); // load defaults into _state->_shortcuts
+void resetShortcuts(); // restore defaults
+void showHelp(); // the Help window (shortcuts + tips)
+bool actionTriggered(DebuggerAction act); // Shortcut() honouring the current binding
+
// dt-lists.cpp
void showVars();
void showWatchedVars();
diff --git a/engines/director/debugger/dt-save-state.cpp b/engines/director/debugger/dt-save-state.cpp
index 1e49c762a49..fb3b17842d6 100644
--- a/engines/director/debugger/dt-save-state.cpp
+++ b/engines/director/debugger/dt-save-state.cpp
@@ -98,6 +98,12 @@ void saveCurrentState() {
json["IgnoreMouse"] = new Common::JSONValue(_state->_ignoreMouse);
json["EnableMultiViewport"] = new Common::JSONValue(_state->_enableMultiViewport);
+ // Rebindable shortcuts, keyed by their stable action ids
+ Common::JSONObject shortcuts;
+ for (int i = 0; i < kActCount; i++)
+ shortcuts[kShortcutDefs[i].id] = new Common::JSONValue((long long int)_state->_shortcuts[i]);
+ json["Shortcuts"] = new Common::JSONValue(shortcuts);
+
// Save the JSON
Common::JSONValue save(json);
debugC(7, kDebugImGui, "ImGui::Saved state: %s", save.stringify().c_str());
@@ -200,6 +206,15 @@ void loadSavedState() {
io.ConfigFlags &= ~ImGuiConfigFlags_ViewportsEnable;
}
+ // Rebindable shortcuts (optional; older saves omit it, keep the defaults).
+ if (saved->asObject().contains("Shortcuts") && saved->asObject()["Shortcuts"]->isObject()) {
+ Common::JSONObject shortcuts = saved->asObject()["Shortcuts"]->asObject();
+ for (int i = 0; i < kActCount; i++) {
+ if (shortcuts.contains(kShortcutDefs[i].id) && shortcuts[kShortcutDefs[i].id]->isIntegerNumber())
+ _state->_shortcuts[i] = (ImGuiKeyChord)shortcuts[kShortcutDefs[i].id]->asIntegerNumber();
+ }
+ }
+
free(data);
delete saved;
delete savedState;
diff --git a/engines/director/debugger/dt-scripts.cpp b/engines/director/debugger/dt-scripts.cpp
index 06f320b2c32..a813309be45 100644
--- a/engines/director/debugger/dt-scripts.cpp
+++ b/engines/director/debugger/dt-scripts.cpp
@@ -268,15 +268,6 @@ static void updateCurrentScript() {
// Quick open (command palette)
-struct QuickOpenItem {
- Common::String label;
- bool isHandler = false;
- CastMemberID id;
- ScriptType scriptType = kScoreScript;
- Common::String handlerId;
- Common::String handlerName;
-};
-
static bool qoMatch(const Common::String &label, const char *q) {
if (!q || !q[0])
return true;
@@ -348,8 +339,8 @@ static void openQuickOpen(const QuickOpenItem &qi, Movie *movie) {
}
void showQuickOpen() {
- static Common::Array<QuickOpenItem> items;
- static bool gathered = false;
+ Common::Array<QuickOpenItem> &items = _state->_quickOpenItems;
+ bool &gathered = _state->_quickOpenGathered;
if (!_state->_quickOpen) {
gathered = false;
diff --git a/engines/director/module.mk b/engines/director/module.mk
index 01fb48d516a..f6d5a211e2d 100644
--- a/engines/director/module.mk
+++ b/engines/director/module.mk
@@ -233,6 +233,7 @@ MODULE_OBJS += \
debugger/dt-cast.o \
debugger/dt-castdetails.o \
debugger/dt-controlpanel.o \
+ debugger/dt-help.o \
debugger/dt-lists.o \
debugger/dt-save-state.o \
debugger/dt-score.o \
More information about the Scummvm-git-logs
mailing list