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.
This commit is contained in:
1 parent
8982ad7092
commit
aca89c631f
9 files changed
+222
-46
No files matched your search
@@ -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,7 +242,7 @@ 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"))
|
||||
@@ -269,6 +269,40 @@ namespace Cthangover.Mods.Dialog
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// 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))
|
||||
@@ -187,7 +208,13 @@ 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 +328,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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in new issue
Block a user