Compare commits
5
Commits
8982ad7092
...
9f86bdbb0c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f86bdbb0c | ||
|
|
5200db1562 | ||
|
|
2d3f4761e7 | ||
|
|
88964cab6b | ||
|
|
aca89c631f |
No files matched your search
+1
-1
@@ -8,7 +8,7 @@
|
||||
"sources": [
|
||||
"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": [],
|
||||
"test_suites": [
|
||||
{"id": "autoplay", "activate": ["--choices=", "--timeout="]},
|
||||
|
||||
@@ -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="PackedScene" path="res://scenes/ui/dialog_canvas.tscn" id="3_dialog"]
|
||||
|
||||
[node name="BaseScene" instance=ExtResource("1_target")]
|
||||
script = ExtResource("2_chain")
|
||||
|
||||
[node name="DialogCanvas" parent="Scene" index="2" instance=ExtResource("3_dialog")]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -17,8 +17,8 @@ namespace Cthangover.Mods.Dialog
|
||||
return new ScenarioEvent { ScenarioText = scenarioText };
|
||||
}
|
||||
|
||||
/// <summary>Builds an event from a scene event definition's scenario fields.</summary>
|
||||
public IExecutableEvent CreateFromDefinition(string scenarioText, string scenarioId, string condition, bool? lightUseTime, bool isOneRun)
|
||||
/// <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, string modId, string filePath)
|
||||
{
|
||||
return new ScenarioEvent
|
||||
{
|
||||
@@ -27,6 +27,8 @@ namespace Cthangover.Mods.Dialog
|
||||
Condition = condition,
|
||||
LightUseTime = lightUseTime,
|
||||
IsOneRun = isOneRun,
|
||||
SourceModId = modId,
|
||||
SourcePath = filePath,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,9 +36,10 @@ namespace Cthangover.Mods.Dialog.Scenarios
|
||||
var lines = text.Split('\n');
|
||||
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('#'))
|
||||
{
|
||||
FinalizePendingSelect(dlg, ref pendingSelect);
|
||||
@@ -48,51 +49,19 @@ namespace Cthangover.Mods.Dialog.Scenarios
|
||||
if (line.StartsWith(':'))
|
||||
{
|
||||
FinalizePendingSelect(dlg, ref pendingSelect);
|
||||
dlg.Point(line.Substring(1));
|
||||
dlg.Point(line.Substring(1)).SourceLine = lineNumber;
|
||||
continue;
|
||||
}
|
||||
|
||||
var tokens = Tokenize(line);
|
||||
if (tokens.Count == 0) continue;
|
||||
|
||||
var cmd = tokens[0].ToLowerInvariant();
|
||||
tokens.RemoveAt(0);
|
||||
|
||||
var positional = new List<string>();
|
||||
var named = new Dictionary<string, string>();
|
||||
var arrowTarget = "";
|
||||
|
||||
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);
|
||||
}
|
||||
var cmd = SplitLine(line, positional, named, out var arrowTarget);
|
||||
if (cmd == null) continue;
|
||||
|
||||
if (cmd == "select")
|
||||
{
|
||||
FinalizePendingSelect(dlg, ref pendingSelect);
|
||||
pendingSelect = new ActionSelect();
|
||||
pendingSelect = new ActionSelect { SourceLine = lineNumber };
|
||||
var promptText = positional.Count > 0 ? positional[0] : null;
|
||||
|
||||
if (named.TryGetValue("key", out var key) && locale != null)
|
||||
@@ -141,14 +110,67 @@ namespace Cthangover.Mods.Dialog.Scenarios
|
||||
FinalizePendingSelect(dlg, ref pendingSelect);
|
||||
|
||||
var strategy = ScenarioCommandStrategyFactory.Get(cmd);
|
||||
if (strategy != null)
|
||||
if (strategy == null)
|
||||
continue;
|
||||
|
||||
var firstNew = dlg.Queue.Count;
|
||||
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);
|
||||
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>-> :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>
|
||||
/// 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
|
||||
|
||||
@@ -44,6 +44,8 @@ namespace Cthangover.Mods.Dialog.Action
|
||||
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>
|
||||
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>
|
||||
/// Scene node lookup seam for subclasses. Replaces direct
|
||||
|
||||
@@ -39,6 +39,12 @@ namespace Cthangover.Mods.Dialog.Action
|
||||
float WaitTime { get; }
|
||||
/// <summary>Timestamp of the last <see cref="Run"/> call, in seconds since epoch.</summary>
|
||||
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
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ namespace Cthangover.Mods.Dialog
|
||||
/// </summary>
|
||||
public void NextAction()
|
||||
{
|
||||
if (Runtime.IsWaitAnswer)
|
||||
if (Runtime.IsHeld || Runtime.IsWaitAnswer)
|
||||
return;
|
||||
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>
|
||||
protected override void OnInput(InputEvent @event)
|
||||
{
|
||||
if (Runtime.IsEnd || Runtime.IsWaitAnswer)
|
||||
if (Runtime.IsHeld || Runtime.IsEnd || Runtime.IsWaitAnswer)
|
||||
return;
|
||||
|
||||
if (@event.IsActionPressed("ui_accept"))
|
||||
{
|
||||
if (IsTyping())
|
||||
return;
|
||||
GameLogger.Log("DIALOG", "input: advancing dialog");
|
||||
NextAction();
|
||||
}
|
||||
@@ -256,6 +258,9 @@ namespace Cthangover.Mods.Dialog
|
||||
if (action == null || action.WaitType == WaitType.WaitTime || action.WaitType == WaitType.WaitEvent)
|
||||
return;
|
||||
|
||||
if (IsOverOtherControl())
|
||||
return;
|
||||
|
||||
if (Body is Control bodyControl)
|
||||
{
|
||||
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>
|
||||
public bool IsRunning => Locker != null;
|
||||
|
||||
|
||||
@@ -57,6 +57,26 @@ namespace Cthangover.Mods.Dialog
|
||||
/// <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;
|
||||
|
||||
/// <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>
|
||||
public string GetVariable(string name)
|
||||
{
|
||||
@@ -122,6 +142,7 @@ namespace Cthangover.Mods.Dialog
|
||||
{
|
||||
variables = new Dictionary<string, string>();
|
||||
DialogBox = dialogBox;
|
||||
IsHeld = false;
|
||||
|
||||
this.endDialogQueue = endQueue == null ? null : endQueue.ToList();
|
||||
if (Lists.IsNotEmpty(this.dialogQueue))
|
||||
@@ -166,6 +187,31 @@ namespace Cthangover.Mods.Dialog
|
||||
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>
|
||||
/// 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,
|
||||
@@ -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);
|
||||
|
||||
if (Debugger != null && Debugger.ShouldHold(this, action))
|
||||
{
|
||||
IsHeld = true;
|
||||
return;
|
||||
}
|
||||
|
||||
action.Run(this);
|
||||
|
||||
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>
|
||||
/// Records a finished one-run scenario in the per-game completion
|
||||
/// history (<see cref="RuntimeData.CompletedScenarioIds"/>) so the
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -159,7 +159,7 @@ namespace Cthangover.Mods.Dialog.Executable
|
||||
var dlg = new DialogQueue();
|
||||
CreateDialog(dlg);
|
||||
|
||||
dialogBox.SetDialogQueueAndRun(dlg, null, 0, this);
|
||||
dialogBox.SetDialogQueueAndRun(dlg, null, StartIndex(dlg), this);
|
||||
IsFirstRun = false;
|
||||
return true;
|
||||
}
|
||||
@@ -167,6 +167,9 @@ namespace Cthangover.Mods.Dialog.Executable
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,7 +22,7 @@ namespace Cthangover.Mods.Dialog.Executable
|
||||
/// dialog queue. Resolves the scenario path via convention: if ScenarioPath
|
||||
/// is set, uses that; otherwise searches all mods for a matching file name
|
||||
/// 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
|
||||
/// inline ScenarioText for embedded scenarios that don't need a separate
|
||||
/// file. Checks conditions through ScenarioCondition.Evaluate before triggering.
|
||||
@@ -81,6 +81,23 @@ namespace Cthangover.Mods.Dialog.Executable
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
if (LightUseTime.HasValue)
|
||||
@@ -111,6 +128,22 @@ namespace Cthangover.Mods.Dialog.Executable
|
||||
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 bool _cachedResult;
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"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": [
|
||||
"src/*.cs"
|
||||
],
|
||||
"depends": [
|
||||
"visual_novel_core>=1"
|
||||
"cthangover_game>=1"
|
||||
]
|
||||
}
|
||||
@@ -8,6 +8,6 @@
|
||||
"scripts/*.gd"
|
||||
],
|
||||
"depends": [
|
||||
"visual_novel_core>=1"
|
||||
"cthangover_game>=1"
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user