Compare commits

...
5 Commits
Author SHA1 Message Date
iisaev 9f86bdbb0c move debug cursor in runtime 2026-10-19 17:39:30 +03:00
cthangover 5200db1562 Rename cthangover_story to cthangover_game
The mod holding the main game's own scenes and story follows the <game>_game
naming rule; further parts stay <game>_<part>, reusable content is <theme>_pack.
2026-10-18 19:45:49 +02:00
cthangover 2d3f4761e7 Mod naming rule and engine UI in engine mods
Mods are renamed by role (shell2d_base, menu_base, ui_base, live2d_base,
editor_tools, cthangover_story, cthangover_murakami, cthangover_party) and
every reference follows. Engine UI prefabs (dialog, answers, message, game
menu, inventory, map, skills) and their panel entries moved out of the story
mod into the subsystem mods; the card_novel and vn_starter_novel collections
are removed.
2026-10-18 19:00:22 +02:00
cthangover 88964cab6b Dialog box ignores input meant for other controls
Enter/Space do not advance the dialog while a text field has focus, and a
click over another control that stops the mouse (a tool panel drawn over the
dialog area) belongs to that control. DialogBox reads input in _Input, before
the GUI, so both used to advance the dialog as well.
2026-10-18 15:31:00 +02:00
cthangover aca89c631f Debugger support for the scenario studio
Actions carry the scenario line they were parsed from (SourceLine);
ScenarioParser.SplitLine exposes the parser's line split. ScenarioEvent
remembers its source mod/file and can start at a label. DialogRuntime asks an
optional IDialogDebugger before every action and can be held; DialogBox
resumes a held dialog and aborts one without completing it.
2026-10-18 14:21:47 +02:00
17 changed files with 472 additions and 51 deletions

No files matched your search

+1 -1
View File
@@ -8,7 +8,7 @@
"sources": [ "sources": [
"src/**/*.cs" "src/**/*.cs"
], ],
"depends": ["base2d>=1", "interactive_base>=1", "effects_base>=1", "time_base>=1", "map_base>=1", "interface>=1"], "depends": ["shell2d_base>=1", "interactive_base>=1", "effects_base>=1", "time_base>=1", "map_base>=1", "ui_base>=1"],
"ui": [], "ui": [],
"test_suites": [ "test_suites": [
{"id": "autoplay", "activate": ["--choices=", "--timeout="]}, {"id": "autoplay", "activate": ["--choices=", "--timeout="]},
+5 -2
View File
@@ -1,7 +1,10 @@
[gd_scene load_steps=3 format=3] [gd_scene load_steps=4 format=3]
[ext_resource type="PackedScene" path="res://mods/base2d/scenes/ui/base_scene.tscn" id="1_target"] [ext_resource type="PackedScene" path="res://mods/shell2d_base/scenes/ui/base_scene.tscn" id="1_target"]
[ext_resource type="Script" path="res://mods/dialog_base/src/UI/Executable/ExecutableMainEventChain.cs" id="2_chain"] [ext_resource type="Script" path="res://mods/dialog_base/src/UI/Executable/ExecutableMainEventChain.cs" id="2_chain"]
[ext_resource type="PackedScene" path="res://scenes/ui/dialog_canvas.tscn" id="3_dialog"]
[node name="BaseScene" instance=ExtResource("1_target")] [node name="BaseScene" instance=ExtResource("1_target")]
script = ExtResource("2_chain") script = ExtResource("2_chain")
[node name="DialogCanvas" parent="Scene" index="2" instance=ExtResource("3_dialog")]
+26
View File
@@ -0,0 +1,26 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://mods/dialog_base/src/UI/Dialog/AnswerBox.cs" id="1_answerbox"]
[ext_resource type="PackedScene" path="res://mods/dialog_base/scenes/ui/answer_item.tscn" id="2_answeritem"]
[node name="AnswerBox" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_answerbox")
itemScene = ExtResource("2_answeritem")
content = NodePath("Content")
[node name="Content" type="VBoxContainer" parent="."]
layout_mode = 1
anchor_left = 0.6
anchor_top = 0.35
anchor_right = 0.95
anchor_bottom = 0.65
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
alignment = 1
+30
View File
@@ -0,0 +1,30 @@
[gd_scene format=3 uid="uid://bv80qy227xxee"]
[ext_resource type="Script" uid="uid://dbrnsck0vtfb3" path="res://mods/dialog_base/src/UI/Dialog/AnswerItem.cs" id="1_answeritem"]
[node name="AnswerItem" type="Control" unique_id=1180917887]
custom_minimum_size = Vector2(0, 50)
layout_mode = 3
anchors_preset = 0
size_flags_horizontal = 3
size_flags_vertical = 0
script = ExtResource("1_answeritem")
[node name="Button" type="Button" parent="." unique_id=2087954476]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="TextLabel" type="Label" parent="Button" unique_id=1302827224]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_font_sizes/font_size = 22
horizontal_alignment = 1
vertical_alignment = 1
+105
View File
@@ -0,0 +1,105 @@
[gd_scene format=3 uid="uid://b8vnxfoixyust"]
[ext_resource type="Script" uid="uid://b41bti3wq4m88" path="res://mods/dialog_base/src/UI/Dialog/DialogBox.cs" id="1_dialogbox"]
[ext_resource type="PackedScene" path="res://mods/dialog_base/scenes/ui/answer_box.tscn" id="2_answerbox"]
[node name="DialogBox" type="Control" unique_id=702809053]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_dialogbox")
answerBoxScene = ExtResource("2_answerbox")
[node name="DialogBody" type="ColorRect" parent="." unique_id=1510369637]
layout_mode = 1
anchors_preset = 12
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_top = -300.0
grow_horizontal = 2
grow_vertical = 0
color = Color(0.054902, 0.058824, 0.082353, 0.9)
[node name="TextField" type="Label" parent="DialogBody" unique_id=2065569930]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 20.0
offset_top = 10.0
offset_right = -20.0
offset_bottom = -10.0
grow_horizontal = 2
grow_vertical = 2
theme_override_font_sizes/font_size = 24
theme_override_colors/font_color = Color(0.909804, 0.898039, 0.870588, 1)
vertical_alignment = 1
autowrap_mode = 2
[node name="FirstAvatar" type="TextureRect" parent="." unique_id=1696535122]
visible = false
texture_filter = 1
layout_mode = 1
anchors_preset = -1
anchor_top = 0.6
anchor_right = 0.13
anchor_bottom = 1.0
offset_left = 90.0
offset_top = -300.0
offset_right = 90.0
offset_bottom = -300.0
grow_horizontal = 0
grow_vertical = 0
expand_mode = 1
stretch_mode = 4
[node name="SecondAvatar" type="TextureRect" parent="." unique_id=1913233391]
visible = false
texture_filter = 1
layout_mode = 1
anchors_preset = -1
anchor_left = 0.87
anchor_top = 0.6
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = -90.0
offset_top = -300.0
offset_right = -90.0
offset_bottom = -300.0
grow_horizontal = 0
grow_vertical = 0
expand_mode = 1
stretch_mode = 4
[node name="TitleBody" type="Control" parent="." unique_id=1986161675]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -400.0
offset_top = -60.0
offset_right = 400.0
offset_bottom = 60.0
grow_horizontal = 2
grow_vertical = 2
[node name="TitleField" type="Label" parent="TitleBody" unique_id=2046072321]
visible = false
layout_mode = 1
anchors_preset = -1
anchor_left = -0.5
anchor_right = 1.5
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_font_sizes/font_size = 28
theme_override_colors/font_color = Color(0.956863, 0.941176, 0.905882, 1)
horizontal_alignment = 1
vertical_alignment = 1
+12
View File
@@ -0,0 +1,12 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://mods/dialog_base/src/UI/Dialog/DialogCanvas.cs" id="1_canvas"]
[node name="DialogCanvas" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_canvas")
+4 -2
View File
@@ -17,8 +17,8 @@ namespace Cthangover.Mods.Dialog
return new ScenarioEvent { ScenarioText = scenarioText }; return new ScenarioEvent { ScenarioText = scenarioText };
} }
/// <summary>Builds an event from a scene event definition's scenario fields.</summary> /// <summary>Builds an event from a scene event definition's scenario fields, remembering the source file.</summary>
public IExecutableEvent CreateFromDefinition(string scenarioText, string scenarioId, string condition, bool? lightUseTime, bool isOneRun) public IExecutableEvent CreateFromDefinition(string scenarioText, string scenarioId, string condition, bool? lightUseTime, bool isOneRun, string modId, string filePath)
{ {
return new ScenarioEvent return new ScenarioEvent
{ {
@@ -27,6 +27,8 @@ namespace Cthangover.Mods.Dialog
Condition = condition, Condition = condition,
LightUseTime = lightUseTime, LightUseTime = lightUseTime,
IsOneRun = isOneRun, IsOneRun = isOneRun,
SourceModId = modId,
SourcePath = filePath,
}; };
} }
} }
+61 -39
View File
@@ -36,9 +36,10 @@ namespace Cthangover.Mods.Dialog.Scenarios
var lines = text.Split('\n'); var lines = text.Split('\n');
ActionSelect pendingSelect = null; ActionSelect pendingSelect = null;
foreach (var rawLine in lines) for (var lineIndex = 0; lineIndex < lines.Length; lineIndex++)
{ {
var line = rawLine.Trim(); var lineNumber = lineIndex + 1;
var line = lines[lineIndex].Trim();
if (line.Length == 0 || line.StartsWith('#')) if (line.Length == 0 || line.StartsWith('#'))
{ {
FinalizePendingSelect(dlg, ref pendingSelect); FinalizePendingSelect(dlg, ref pendingSelect);
@@ -48,51 +49,19 @@ namespace Cthangover.Mods.Dialog.Scenarios
if (line.StartsWith(':')) if (line.StartsWith(':'))
{ {
FinalizePendingSelect(dlg, ref pendingSelect); FinalizePendingSelect(dlg, ref pendingSelect);
dlg.Point(line.Substring(1)); dlg.Point(line.Substring(1)).SourceLine = lineNumber;
continue; continue;
} }
var tokens = Tokenize(line);
if (tokens.Count == 0) continue;
var cmd = tokens[0].ToLowerInvariant();
tokens.RemoveAt(0);
var positional = new List<string>(); var positional = new List<string>();
var named = new Dictionary<string, string>(); var named = new Dictionary<string, string>();
var arrowTarget = ""; var cmd = SplitLine(line, positional, named, out var arrowTarget);
if (cmd == null) continue;
for (var i = 0; i < tokens.Count; i++)
{
var t = tokens[i];
if (t == "->")
{
if (i + 1 < tokens.Count)
{
arrowTarget = tokens[i + 1].TrimStart(':');
i++;
}
continue;
}
var eqIdx = t.IndexOf('=');
if (eqIdx > 0)
{
var name = t.Substring(0, eqIdx);
if (IsValidIdent(name))
{
named[name.ToLowerInvariant()] = t.Substring(eqIdx + 1);
continue;
}
}
positional.Add(t);
}
if (cmd == "select") if (cmd == "select")
{ {
FinalizePendingSelect(dlg, ref pendingSelect); FinalizePendingSelect(dlg, ref pendingSelect);
pendingSelect = new ActionSelect(); pendingSelect = new ActionSelect { SourceLine = lineNumber };
var promptText = positional.Count > 0 ? positional[0] : null; var promptText = positional.Count > 0 ? positional[0] : null;
if (named.TryGetValue("key", out var key) && locale != null) if (named.TryGetValue("key", out var key) && locale != null)
@@ -141,14 +110,67 @@ namespace Cthangover.Mods.Dialog.Scenarios
FinalizePendingSelect(dlg, ref pendingSelect); FinalizePendingSelect(dlg, ref pendingSelect);
var strategy = ScenarioCommandStrategyFactory.Get(cmd); var strategy = ScenarioCommandStrategyFactory.Get(cmd);
if (strategy != null) if (strategy == null)
continue;
var firstNew = dlg.Queue.Count;
strategy.Execute(dlg, positional, named, arrowTarget, locale); strategy.Execute(dlg, positional, named, arrowTarget, locale);
for (var i = firstNew; i < dlg.Queue.Count; i++)
{
if (dlg.Queue[i] != null && dlg.Queue[i].SourceLine == 0)
dlg.Queue[i].SourceLine = lineNumber;
}
} }
FinalizePendingSelect(dlg, ref pendingSelect); FinalizePendingSelect(dlg, ref pendingSelect);
return dlg; return dlg;
} }
/// <summary>
/// Splits one trimmed, non-empty DSL line the way <see cref="Parse"/> does:
/// returns the lowercase command word (null for an empty line) and fills
/// the positional tokens, the <c>name=value</c> pairs (names lowercased)
/// and the <c>-&gt; :label</c> target. Tools that read scenarios without
/// running them (the scenario studio) use it to agree with the parser.
/// </summary>
public static string SplitLine(string line, List<string> positional, Dictionary<string, string> named, out string arrowTarget)
{
arrowTarget = "";
var tokens = Tokenize(line);
if (tokens.Count == 0)
return null;
var cmd = tokens[0].ToLowerInvariant();
for (var i = 1; i < tokens.Count; i++)
{
var t = tokens[i];
if (t == "->")
{
if (i + 1 < tokens.Count)
{
arrowTarget = tokens[i + 1].TrimStart(':');
i++;
}
continue;
}
var eqIdx = t.IndexOf('=');
if (eqIdx > 0)
{
var name = t.Substring(0, eqIdx);
if (IsValidIdent(name))
{
named[name.ToLowerInvariant()] = t.Substring(eqIdx + 1);
continue;
}
}
positional.Add(t);
}
return cmd;
}
/// <summary> /// <summary>
/// Extracts YAML-like front-matter metadata from script text. Lines before /// Extracts YAML-like front-matter metadata from script text. Lines before
/// a <c>---</c> separator or the first non-key:value line are parsed as /// a <c>---</c> separator or the first non-key:value line are parsed as
+2
View File
@@ -44,6 +44,8 @@ namespace Cthangover.Mods.Dialog.Action
public float StartTime { get; private set; } public float StartTime { get; private set; }
/// <summary>Action IDs whose <see cref="Destruct"/> will be triggered when this action destructs. Used for cross-action cleanup dependencies.</summary> /// <summary>Action IDs whose <see cref="Destruct"/> will be triggered when this action destructs. Used for cross-action cleanup dependencies.</summary>
public ISet<string> DelayedDestruct { get; } = new HashSet<string>(); public ISet<string> DelayedDestruct { get; } = new HashSet<string>();
/// <summary>1-based scenario line the action was parsed from; 0 when built in code.</summary>
public int SourceLine { get; set; }
/// <summary> /// <summary>
/// Scene node lookup seam for subclasses. Replaces direct /// Scene node lookup seam for subclasses. Replaces direct
+6
View File
@@ -39,6 +39,12 @@ namespace Cthangover.Mods.Dialog.Action
float WaitTime { get; } float WaitTime { get; }
/// <summary>Timestamp of the last <see cref="Run"/> call, in seconds since epoch.</summary> /// <summary>Timestamp of the last <see cref="Run"/> call, in seconds since epoch.</summary>
float StartTime { get; } float StartTime { get; }
/// <summary>
/// 1-based line of the scenario text this action was parsed from, 0 for
/// actions built in code. Set by <see cref="Scenarios.ScenarioParser"/>;
/// debugging tools map the running action back to the script with it.
/// </summary>
int SourceLine { get; set; }
#endregion #endregion
+80 -2
View File
@@ -209,7 +209,7 @@ namespace Cthangover.Mods.Dialog
/// </summary> /// </summary>
public void NextAction() public void NextAction()
{ {
if (Runtime.IsWaitAnswer) if (Runtime.IsHeld || Runtime.IsWaitAnswer)
return; return;
if (Runtime.IsEnd) if (Runtime.IsEnd)
{ {
@@ -242,11 +242,13 @@ namespace Cthangover.Mods.Dialog
/// <summary>Advances the dialog on <c>ui_accept</c> or on a click outside the body, unless a choice is pending or the queue has ended.</summary> /// <summary>Advances the dialog on <c>ui_accept</c> or on a click outside the body, unless a choice is pending or the queue has ended.</summary>
protected override void OnInput(InputEvent @event) protected override void OnInput(InputEvent @event)
{ {
if (Runtime.IsEnd || Runtime.IsWaitAnswer) if (Runtime.IsHeld || Runtime.IsEnd || Runtime.IsWaitAnswer)
return; return;
if (@event.IsActionPressed("ui_accept")) if (@event.IsActionPressed("ui_accept"))
{ {
if (IsTyping())
return;
GameLogger.Log("DIALOG", "input: advancing dialog"); GameLogger.Log("DIALOG", "input: advancing dialog");
NextAction(); NextAction();
} }
@@ -256,6 +258,9 @@ namespace Cthangover.Mods.Dialog
if (action == null || action.WaitType == WaitType.WaitTime || action.WaitType == WaitType.WaitEvent) if (action == null || action.WaitType == WaitType.WaitTime || action.WaitType == WaitType.WaitEvent)
return; return;
if (IsOverOtherControl())
return;
if (Body is Control bodyControl) if (Body is Control bodyControl)
{ {
var bodyRect = bodyControl.GetGlobalRect(); var bodyRect = bodyControl.GetGlobalRect();
@@ -269,6 +274,79 @@ namespace Cthangover.Mods.Dialog
} }
} }
/// <summary>
/// True while a text field has keyboard focus: Enter and Space belong to
/// it, not to the dialog (the dialog reads <c>ui_accept</c> in
/// <c>_Input</c>, before the GUI).
/// </summary>
private bool IsTyping() => GetViewport()?.GuiGetFocusOwner() is LineEdit or TextEdit;
/// <summary>
/// True when the mouse is over a control that is not part of this box and
/// stops the mouse (a tool panel, a button drawn over the dialog area):
/// the click is meant for that control.
/// </summary>
private bool IsOverOtherControl()
{
var hovered = GetViewport()?.GuiGetHoveredControl();
return hovered != null && hovered != this && !IsAncestorOf(hovered)
&& hovered.MouseFilter == MouseFilterEnum.Stop;
}
/// <summary>
/// Continues a dialog the debugger held (<see cref="DialogRuntime.IsHeld"/>):
/// runs from the held action on, exactly as if it had never stopped.
/// Does nothing when the runtime is not held.
/// </summary>
public void Resume()
{
if (!Runtime.IsHeld)
return;
Runtime.IsHeld = false;
Runtime.Run();
ScheduleWaitTimer();
}
/// <summary>
/// Moves a running dialog to another action and holds it there
/// (<see cref="DialogRuntime.MoveTo"/>): a pending wait timer and an open
/// answer box are dropped, since they belong to the action left behind;
/// the shown text stays until the new action replaces it.
/// </summary>
public bool MoveTo(int queueIndex)
{
if (!IsRunning)
return false;
ClearWaitTimer();
if (answerBox != null)
{
answerBox.Hide();
answerBox = null;
}
return Runtime.MoveTo(queueIndex);
}
/// <summary>
/// Stops the running dialog without finishing it (<see cref="DialogRuntime.Abort"/>)
/// and frees the box for another queue: closes the answer box, hides
/// and releases the locker. The scenario does not count as completed.
/// </summary>
public void Abort()
{
ClearWaitTimer();
if (answerBox != null)
{
answerBox.Hide();
answerBox = null;
}
Runtime.Abort();
Hide();
Locker = null;
}
/// <summary>Whether a dialog is currently running (Locker is not null).</summary> /// <summary>Whether a dialog is currently running (Locker is not null).</summary>
public bool IsRunning => Locker != null; public bool IsRunning => Locker != null;
+76
View File
@@ -57,6 +57,26 @@ namespace Cthangover.Mods.Dialog
/// <summary>True when the dialog queue is exhausted (index past end or queue is empty).</summary> /// <summary>True when the dialog queue is exhausted (index past end or queue is empty).</summary>
public bool IsEnd => Lists.IsEmpty(dialogQueue) || index >= dialogQueue.Count; public bool IsEnd => Lists.IsEmpty(dialogQueue) || index >= dialogQueue.Count;
/// <summary>
/// Process-wide debugging hook asked before every action (see
/// <see cref="IDialogDebugger"/>); null in normal play, which costs one
/// null check per action.
/// </summary>
public static IDialogDebugger Debugger { get; set; }
/// <summary>
/// True while the debugger holds the runtime before <see cref="CurrentAction"/>:
/// that action has not run yet, so advancing must wait for
/// <see cref="DialogBox.Resume"/> instead of skipping it.
/// </summary>
public bool IsHeld { get; set; }
/// <summary>Queue index of <see cref="CurrentAction"/>.</summary>
public int Index => index;
/// <summary>Current <c>${name}</c> variables of the dialog, for inspection.</summary>
public IReadOnlyDictionary<string, string> Variables => (IReadOnlyDictionary<string, string>)variables;
/// <summary>Retrieves a stored variable value by name. Returns null if not set.</summary> /// <summary>Retrieves a stored variable value by name. Returns null if not set.</summary>
public string GetVariable(string name) public string GetVariable(string name)
{ {
@@ -122,6 +142,7 @@ namespace Cthangover.Mods.Dialog
{ {
variables = new Dictionary<string, string>(); variables = new Dictionary<string, string>();
DialogBox = dialogBox; DialogBox = dialogBox;
IsHeld = false;
this.endDialogQueue = endQueue == null ? null : endQueue.ToList(); this.endDialogQueue = endQueue == null ? null : endQueue.ToList();
if (Lists.IsNotEmpty(this.dialogQueue)) if (Lists.IsNotEmpty(this.dialogQueue))
@@ -166,6 +187,31 @@ namespace Cthangover.Mods.Dialog
End(); End();
} }
/// <summary>
/// Debugger "set next statement": makes the action at
/// <paramref name="queueIndex"/> the next one to run and holds the
/// runtime before it (<see cref="IsHeld"/>), so nothing runs until
/// <see cref="DialogBox.Resume"/>. The current action is destructed like
/// on a goto; actions between the old and the new position neither run
/// nor roll back — variables and game state stay as they are. Jumping
/// back to an action that already ran re-runs it, exactly like a
/// looping goto does. Returns false for an index outside the queue.
/// </summary>
public bool MoveTo(int queueIndex)
{
if (Lists.IsEmpty(dialogQueue) || queueIndex < 0 || queueIndex >= dialogQueue.Count)
return false;
var current = index >= 0 && index < dialogQueue.Count ? dialogQueue[index] : null;
if (current != null && current.IsConstructed && current.DestructType == DestructType.OnEndAction)
current.Destruct();
index = queueIndex;
isLastGoTo = false;
IsHeld = true;
return true;
}
/// <summary> /// <summary>
/// Main execution loop. Iterates through actions, calling <see cref="IActionCommand.Run"/> on each and advancing /// Main execution loop. Iterates through actions, calling <see cref="IActionCommand.Run"/> on each and advancing
/// automatically for <see cref="WaitType.NoWait"/> actions. Stops the loop on any wait-type action (WaitClick, WaitSelect, /// automatically for <see cref="WaitType.NoWait"/> actions. Stops the loop on any wait-type action (WaitClick, WaitSelect,
@@ -188,6 +234,12 @@ namespace Cthangover.Mods.Dialog
GameLogger.Log("DIALOG", $"Run: idx={index}/{dialogQueue.Count-1} type={action.GetType().Name} wait={action.WaitType}", LogLevel.Debug); GameLogger.Log("DIALOG", $"Run: idx={index}/{dialogQueue.Count-1} type={action.GetType().Name} wait={action.WaitType}", LogLevel.Debug);
if (Debugger != null && Debugger.ShouldHold(this, action))
{
IsHeld = true;
return;
}
action.Run(this); action.Run(this);
if (iteration++ > 100000) if (iteration++ > 100000)
@@ -301,6 +353,30 @@ namespace Cthangover.Mods.Dialog
} }
} }
/// <summary>
/// Drops the running queue without finishing it: destructs constructed
/// actions and forgets the queue, but — unlike <see cref="End"/> — runs no
/// end actions, does not mark a one-run scenario completed and publishes
/// no <see cref="DialogEndedEvent"/>. Used to restart a scenario that is
/// being edited; the caller hides the box (<see cref="DialogBox.Abort"/>).
/// </summary>
public void Abort()
{
if (Lists.IsNotEmpty(dialogQueue))
{
foreach (var action in dialogQueue)
if (action != null && action.IsConstructed)
action.Destruct();
}
dialogQueue = null;
endDialogQueue = null;
index = 0;
isLastGoTo = false;
IsHeld = false;
GameLogger.Log("DIALOG", "Abort: queue dropped without ending");
}
/// <summary> /// <summary>
/// Records a finished one-run scenario in the per-game completion /// Records a finished one-run scenario in the per-game completion
/// history (<see cref="RuntimeData.CompletedScenarioIds"/>) so the /// history (<see cref="RuntimeData.CompletedScenarioIds"/>) so the
+23
View File
@@ -0,0 +1,23 @@
using Cthangover.Mods.Dialog.Action;
namespace Cthangover.Mods.Dialog
{
/// <summary>
/// Hook a debugging tool installs in <see cref="DialogRuntime.Debugger"/> to
/// stop a dialog before an action runs (pause, step, breakpoints). The
/// runtime asks before every action of every dialog — including standalone
/// runtimes without a <see cref="DialogBox"/> (hover commands) — so the
/// implementation must answer quickly and hold only dialogs it can resume
/// (<see cref="DialogBox.Resume"/>).
/// </summary>
public interface IDialogDebugger
{
/// <summary>
/// Returns true to hold <paramref name="runtime"/> before
/// <paramref name="action"/>: the runtime marks itself
/// <see cref="DialogRuntime.IsHeld"/> and returns without running it;
/// clicks are ignored until <see cref="DialogBox.Resume"/>.
/// </summary>
bool ShouldHold(DialogRuntime runtime, IActionCommand action);
}
}
+4 -1
View File
@@ -159,7 +159,7 @@ namespace Cthangover.Mods.Dialog.Executable
var dlg = new DialogQueue(); var dlg = new DialogQueue();
CreateDialog(dlg); CreateDialog(dlg);
dialogBox.SetDialogQueueAndRun(dlg, null, 0, this); dialogBox.SetDialogQueueAndRun(dlg, null, StartIndex(dlg), this);
IsFirstRun = false; IsFirstRun = false;
return true; return true;
} }
@@ -167,6 +167,9 @@ namespace Cthangover.Mods.Dialog.Executable
protected virtual void CreateDialog(DialogQueue dlg) protected virtual void CreateDialog(DialogQueue dlg)
{ {
} }
/// <summary>Queue index the dialog starts at; 0 unless a subclass starts mid-script.</summary>
protected virtual int StartIndex(DialogQueue dlg) => 0;
} }
} }
+34 -1
View File
@@ -22,7 +22,7 @@ namespace Cthangover.Mods.Dialog.Executable
/// dialog queue. Resolves the scenario path via convention: if ScenarioPath /// dialog queue. Resolves the scenario path via convention: if ScenarioPath
/// is set, uses that; otherwise searches all mods for a matching file name /// is set, uses that; otherwise searches all mods for a matching file name
/// derived from the class name (stripping "Event" suffix), with a fallback /// derived from the class name (stripping "Event" suffix), with a fallback
/// to res://mods/visual_novel_core/scenarios/. CustomMappings allows code to register /// to res://mods/cthangover_game/scenarios/. CustomMappings allows code to register
/// arbitrary class-to-path bindings for mod-provided scenarios. Supports /// arbitrary class-to-path bindings for mod-provided scenarios. Supports
/// inline ScenarioText for embedded scenarios that don't need a separate /// inline ScenarioText for embedded scenarios that don't need a separate
/// file. Checks conditions through ScenarioCondition.Evaluate before triggering. /// file. Checks conditions through ScenarioCondition.Evaluate before triggering.
@@ -81,6 +81,23 @@ namespace Cthangover.Mods.Dialog.Executable
/// </summary> /// </summary>
public override string ID => ScenarioId ?? base.ID; public override string ID => ScenarioId ?? base.ID;
/// <summary>
/// Mod (or content overlay) the scenario text was read from; null for
/// generated text (test play, inline scenarios). With <see cref="SourcePath"/>
/// it lets debugging tools open and edit the file behind a running event.
/// </summary>
public string SourceModId { get; set; }
/// <summary>Mod-relative path of the scenario file; see <see cref="SourceModId"/>.</summary>
public string SourcePath { get; set; }
/// <summary>
/// Label (<c>:name</c> in the script, without the colon) to start at
/// instead of the first line. Used to restart a scenario from where an
/// edited part begins; an unknown label falls back to the start.
/// </summary>
public string StartLabel { get; set; }
protected override void CreateDialog(DialogQueue dlg) protected override void CreateDialog(DialogQueue dlg)
{ {
if (LightUseTime.HasValue) if (LightUseTime.HasValue)
@@ -111,6 +128,22 @@ namespace Cthangover.Mods.Dialog.Executable
dlg.RuntimeObjectList.Add(obj); dlg.RuntimeObjectList.Add(obj);
} }
/// <summary>Index of the <see cref="StartLabel"/> point in the built queue, or 0.</summary>
protected override int StartIndex(DialogQueue dlg)
{
if (string.IsNullOrEmpty(StartLabel))
return 0;
for (var i = 0; i < dlg.Queue.Count; i++)
{
if (dlg.Queue[i]?.ID == StartLabel)
return i;
}
GameLogger.Log("SCENE", $"ScenarioEvent '{ID}': start label '{StartLabel}' not found, starting from the top", LogLevel.Warning);
return 0;
}
private long _cachedVersion = long.MinValue; private long _cachedVersion = long.MinValue;
private bool _cachedResult; private bool _cachedResult;
+2 -2
View File
@@ -1,10 +1,10 @@
{ {
"name": "Test Runner", "name": "Test Runner",
"description": "Test harness mod: ports the integration DialogAutoDriver into an engine mod. Activated by --choices=/--timeout= user args; auto-plays the active dialog, prints [TEST_RESULT] and quits. Depends on visual_novel_core and dialog_base.", "description": "Test harness mod: ports the integration DialogAutoDriver into an engine mod. Activated by --choices=/--timeout= user args; auto-plays the active dialog, prints [TEST_RESULT] and quits. Depends on cthangover_game and dialog_base.",
"sources": [ "sources": [
"src/*.cs" "src/*.cs"
], ],
"depends": [ "depends": [
"visual_novel_core>=1" "cthangover_game>=1"
] ]
} }
+1 -1
View File
@@ -8,6 +8,6 @@
"scripts/*.gd" "scripts/*.gd"
], ],
"depends": [ "depends": [
"visual_novel_core>=1" "cthangover_game>=1"
] ]
} }