init mod
This commit is contained in:
commit
5de3fd7ad8
121 files changed
+6632
No files matched your search
@@ -0,0 +1,326 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Events;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Mods.Dialog.Action;
|
||||
using Cthangover.Core.UI.Event;
|
||||
using Cthangover.Mods.Dialog.Executable;
|
||||
using Cthangover.Core.Utils;
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
using Cthangover.Core.UI.Dialog;
|
||||
using Cthangover.Core.UI.Executable;
|
||||
using Cthangover.Core.Scenarios;
|
||||
using Cthangover.Core.Interactive;
|
||||
|
||||
namespace Cthangover.Mods.Dialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Dialog execution engine: manages the action queue with goto-based branching
|
||||
/// and variable substitution. Run() iterates actions in a tight loop, breaking
|
||||
/// only on wait points (WaitClick/Select/Time/Event). Text variables use the
|
||||
/// "${name}" syntax and are resolved via GetVariable/SetVariable — values are
|
||||
/// stored in a simple Dictionary, not a cascading scope, keeping lookups O(1).
|
||||
/// GoTo searches the queue linearly by action ID and jumps the index; the
|
||||
/// source action's destruct fires on jump. The isLastGoTo flag prevents double
|
||||
/// advancement when a GoTo is the last action in its branch. End() processes
|
||||
/// an optional cleanup queue (endDialogQueue) before tearing down, and raises
|
||||
/// the dialog-end event via the SceneEventController for chaining.
|
||||
/// Infinite loop protection caps at 100k iterations per Run call.
|
||||
/// </summary>
|
||||
public class DialogRuntime
|
||||
{
|
||||
|
||||
private List<IActionCommand> dialogQueue;
|
||||
private List<IActionCommand> endDialogQueue;
|
||||
private int goToActionIndex;
|
||||
private int index;
|
||||
private bool isLastGoTo;
|
||||
private IDictionary<string, string> variables = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>The dialog box instance this runtime drives. Set when a new queue is loaded.</summary>
|
||||
public DialogBox DialogBox { get; private set; }
|
||||
/// <summary>The currently active dialog queue. Set by <see cref="SetDialogQueueAndRun"/>.</summary>
|
||||
public DialogQueue Dialog { get; set; }
|
||||
|
||||
/// <summary>The action at the current queue index. Automatically constructs it if <see cref="ConstructType"/> is OnStartAction and it hasn't been constructed yet.</summary>
|
||||
public IActionCommand CurrentAction => GetByIndex(index);
|
||||
|
||||
/// <summary>True when the current action is waiting for a player choice (<see cref="WaitType.WaitSelect"/>).</summary>
|
||||
public bool IsWaitAnswer => IsWaitType(WaitType.WaitSelect);
|
||||
/// <summary>True when the current action is waiting for a timer (<see cref="WaitType.WaitTime"/>).</summary>
|
||||
public bool IsWaitTime => IsWaitType(WaitType.WaitTime);
|
||||
/// <summary>True when the current action is waiting for an external event (<see cref="WaitType.WaitEvent"/>).</summary>
|
||||
public bool IsWaitEvent => IsWaitType(WaitType.WaitEvent);
|
||||
|
||||
/// <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>Retrieves a stored variable value by name. Returns null if not set.</summary>
|
||||
public string GetVariable(string name)
|
||||
{
|
||||
variables.TryGetValue(name, out var val);
|
||||
return val;
|
||||
}
|
||||
|
||||
/// <summary>Stores a variable for later substitution via ${name} syntax. Overwrites existing values.</summary>
|
||||
public void SetVariable(string name, string value)
|
||||
{
|
||||
variables[name] = value;
|
||||
}
|
||||
|
||||
/// <summary>Scans <paramref name="text"/> for ${key} patterns and replaces them with stored variable values. Unresolved keys become "?".</summary>
|
||||
public string ProcessText(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return null;
|
||||
|
||||
var vars = text.GetIncludesInQuotes("${", "}");
|
||||
if (Lists.IsNotEmpty(vars))
|
||||
foreach (var variable in vars)
|
||||
{
|
||||
if (!variables.TryGetValue(variable, out var val))
|
||||
val = "?";
|
||||
text = text.Replace("${" + variable + "}", val);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
private bool IsWaitType(WaitType type)
|
||||
{
|
||||
if (Lists.IsEmpty(dialogQueue))
|
||||
return false;
|
||||
if (index < 0 || index >= dialogQueue.Count)
|
||||
return false;
|
||||
var action = dialogQueue[index];
|
||||
return action != null && action.WaitType == type;
|
||||
}
|
||||
|
||||
private IActionCommand GetByIndex(int idx)
|
||||
{
|
||||
if (Lists.IsEmpty(dialogQueue))
|
||||
return null;
|
||||
if (idx < 0 || idx >= dialogQueue.Count)
|
||||
return null;
|
||||
var action = dialogQueue[idx];
|
||||
if (action != null && action.ConstructType == ConstructType.OnStartAction && !action.IsConstructed)
|
||||
action.Construct();
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a new dialog queue and prepares for execution. Resets variables, sets the dialog box reference,
|
||||
/// ends any previous queue (destructing its actions), copies the new queue, and constructs OnStartQueue actions.
|
||||
/// Note: does NOT call Run() — the caller must call Run() separately.
|
||||
/// </summary>
|
||||
public void SetDialogQueueAndRun(DialogBox dialogBox,
|
||||
IEnumerable<IActionCommand> queue,
|
||||
IEnumerable<IActionCommand> endQueue,
|
||||
int startIndex = 0)
|
||||
{
|
||||
variables = new Dictionary<string, string>();
|
||||
DialogBox = dialogBox;
|
||||
|
||||
this.endDialogQueue = endQueue == null ? null : endQueue.ToList();
|
||||
if (Lists.IsNotEmpty(this.dialogQueue))
|
||||
End();
|
||||
|
||||
this.dialogQueue = queue.ToList();
|
||||
|
||||
index = startIndex;
|
||||
foreach (var action in this.dialogQueue)
|
||||
if (action != null && action.ConstructType == ConstructType.OnStartQueue && !action.IsConstructed)
|
||||
action.Construct();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches the queue linearly for an action with ID == <paramref name="actionID"/> and jumps the index there.
|
||||
/// Destructs the source action before jumping. If not found, calls <see cref="End"/>.
|
||||
/// <paramref name="lastGoTo"/> prevents the runtime from auto-advancing past the target when the GoTo is the last action in its branch.
|
||||
/// </summary>
|
||||
public void TryGoTo(string actionID, bool lastGoTo = false)
|
||||
{
|
||||
goToActionIndex = index;
|
||||
|
||||
if (string.IsNullOrEmpty(actionID))
|
||||
return;
|
||||
|
||||
for (var i = 0; i < dialogQueue.Count; i++)
|
||||
{
|
||||
var action = dialogQueue[i];
|
||||
if (action?.ID == actionID)
|
||||
{
|
||||
index = i;
|
||||
var actionGoTo = GetByIndex(goToActionIndex);
|
||||
if (actionGoTo != null && actionGoTo.IsConstructed && actionGoTo.DestructType == DestructType.OnEndAction)
|
||||
actionGoTo.Destruct();
|
||||
this.isLastGoTo = lastGoTo;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
GameLogger.Log("DIALOG", "Dialog goto action not found!", LogLevel.Error);
|
||||
|
||||
End();
|
||||
}
|
||||
|
||||
/// <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,
|
||||
/// WaitTime, WaitEvent) so the runtime yields control until the condition is satisfied. Protected against infinite loops
|
||||
/// with a 100,000-iteration cap.
|
||||
/// </summary>
|
||||
public void Run()
|
||||
{
|
||||
var iteration = 0;
|
||||
for (;;)
|
||||
{
|
||||
var action = CurrentAction;
|
||||
if (action == null)
|
||||
{
|
||||
GameLogger.Log("DIALOG", "Run: action is null, ending", LogLevel.Debug);
|
||||
|
||||
End();
|
||||
return;
|
||||
}
|
||||
|
||||
GameLogger.Log("DIALOG", $"Run: idx={index}/{dialogQueue.Count-1} type={action.GetType().Name} wait={action.WaitType}", LogLevel.Debug);
|
||||
|
||||
action.Run(this);
|
||||
|
||||
if (iteration++ > 100000)
|
||||
{
|
||||
GameLogger.Log("DIALOG", "Run: infinite loop protection (100k iterations)", LogLevel.Warning);
|
||||
break;
|
||||
}
|
||||
|
||||
switch (action.WaitType)
|
||||
{
|
||||
case WaitType.WaitClick:
|
||||
case WaitType.WaitSelect:
|
||||
case WaitType.WaitTime:
|
||||
case WaitType.WaitEvent:
|
||||
break;
|
||||
case WaitType.NoWait:
|
||||
Next();
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Advances the queue index by one. Destructs the current action if its <see cref="DestructType"/> is OnEndAction.
|
||||
/// Processes any delayed-destruct targets registered by the current action. Calls <see cref="End"/> if the queue is exhausted.
|
||||
/// Respects <see cref="isLastGoTo"/> — if a GoTo was the final action, the index is not incremented.
|
||||
/// </summary>
|
||||
public void Next()
|
||||
{
|
||||
if (isLastGoTo)
|
||||
{
|
||||
GameLogger.Log("DIALOG", "Next: last goto, returning");
|
||||
isLastGoTo = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var action = CurrentAction;
|
||||
if (action != null && action.IsConstructed && action.DestructType == DestructType.OnEndAction)
|
||||
action.Destruct();
|
||||
|
||||
if (action != null && Lists.IsNotEmpty(dialogQueue) && Lists.IsNotEmpty(action.DelayedDestruct))
|
||||
foreach (var item in dialogQueue)
|
||||
if (action.DelayedDestruct.Contains(item.ID))
|
||||
item.Destruct();
|
||||
|
||||
if (Lists.IsEmpty(dialogQueue) || index < 0 || index >= dialogQueue.Count)
|
||||
{
|
||||
GameLogger.Log("DIALOG", "Next: queue exhausted, ending");
|
||||
End();
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Terminates the dialog queue. Destructs all remaining constructed actions, hides the dialog box, processes the
|
||||
/// end-dialog cleanup queue (if any), and raises the dialog-end event via <see cref="SceneEventController"/>.
|
||||
/// Guards against end-queue actions starting a new dialog mid-cleanup by checking if the queue changed.
|
||||
/// </summary>
|
||||
public void End()
|
||||
{
|
||||
GameLogger.Log("DIALOG", $"End: {dialogQueue?.Count ?? 0} actions in queue, {(endDialogQueue?.Count ?? 0)} end actions");
|
||||
|
||||
if (Lists.IsNotEmpty(dialogQueue))
|
||||
{
|
||||
foreach (var action in dialogQueue)
|
||||
if (action != null && action.IsConstructed)
|
||||
action.Destruct();
|
||||
dialogQueue.Clear();
|
||||
}
|
||||
|
||||
ExecutableEvent locker = null;
|
||||
if (DialogBox != null)
|
||||
{
|
||||
locker = DialogBox.Locker;
|
||||
TrackOneRunCompletion(locker);
|
||||
DialogBox.Locker = null;
|
||||
DialogBox.Hide();
|
||||
}
|
||||
|
||||
if (Lists.IsNotEmpty(endDialogQueue))
|
||||
{
|
||||
GameLogger.Log("DIALOG", "End: processing end actions...");
|
||||
var oldQueue = dialogQueue;
|
||||
foreach (var action in endDialogQueue)
|
||||
{
|
||||
if (action == null || action.IsDestructed)
|
||||
continue;
|
||||
if (!action.IsConstructed) action.Construct();
|
||||
action.Run(this);
|
||||
action.Destruct();
|
||||
}
|
||||
endDialogQueue.Clear();
|
||||
|
||||
if (dialogQueue != oldQueue)
|
||||
{
|
||||
GameLogger.Log("DIALOG", "End: new dialog started during end actions, skipping cleanup", LogLevel.Warning);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
dialogQueue = null;
|
||||
endDialogQueue = null;
|
||||
index = 0;
|
||||
|
||||
if (DialogBox != null)
|
||||
{
|
||||
ModEventBus.Publish(new DialogEndedEvent { LockerId = locker?.ID });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a finished one-run scenario in the per-game completion
|
||||
/// history (<see cref="RuntimeData.CompletedScenarioIds"/>) so the
|
||||
/// scenario runner never re-triggers it on later visits to the
|
||||
/// scene or after save/load. Only <see cref="ScenarioEvent"/> with
|
||||
/// an explicit ScenarioId is tracked: its ID matches the scenario
|
||||
/// definition name used by the runner's filters, whereas generic
|
||||
/// lockers (e.g. the ones flagged OneRun by ActionSwitchScene) carry
|
||||
/// type-name IDs that would poison the history.
|
||||
/// </summary>
|
||||
private static void TrackOneRunCompletion(ExecutableEvent locker)
|
||||
{
|
||||
if (!(locker is ScenarioEvent scenario) || !scenario.IsOneRun || string.IsNullOrEmpty(scenario.ScenarioId))
|
||||
return;
|
||||
|
||||
var runtime = GameData.Instance?.Runtime;
|
||||
if (runtime != null && runtime.CompletedScenarioIds.Add(scenario.ID))
|
||||
GameLogger.Log("DIALOG", $"One-run scenario '{scenario.ID}' marked as completed");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user