Update dialog_base implementation, assets and documentation
This commit is contained in:
1 parent
2c9e6805a9
commit
1894857eff
36 files changed
+622
-84
No files matched your search
@@ -13,7 +13,6 @@ using Cthangover.Core.UI.Executable;
|
||||
using Cthangover.Core.Scenarios;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Core.Interactive;
|
||||
using Cthangover.Mods.Map;
|
||||
using Cthangover.Mods.Time;
|
||||
|
||||
namespace Cthangover.Mods.Dialog
|
||||
@@ -70,12 +69,12 @@ namespace Cthangover.Mods.Dialog
|
||||
/// </summary>
|
||||
public IInventory Inventory { get; }
|
||||
/// <summary>
|
||||
/// Map zone control. Stateless adapter over the
|
||||
/// <c>MapZoneService</c> singleton, which owns the actual state;
|
||||
/// safe to instantiate per-context like the quest service, because the
|
||||
/// state does not live in the adapter.
|
||||
/// Map zone control, resolved from the kernel slot
|
||||
/// <see cref="MapZoneServiceRegistry"/> that the map_base engine mod
|
||||
/// fills (the dialog engine does not depend on the map). <c>null</c>
|
||||
/// in games without the world map, where no <c>map.*</c> action exists.
|
||||
/// </summary>
|
||||
public IMapZoneService Map { get; } = new MapZoneServiceImpl();
|
||||
public IMapZoneService Map => MapZoneServiceRegistry.Provider;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a context wired to the given DialogRuntime. The
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace Cthangover.Mods.Dialog
|
||||
{
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Cthangover.Mods.Expressions;
|
||||
using Cthangover.Mods.Stats.Formulas;
|
||||
/// <summary>Numeric reads from the current dialog only; strings never become executable expression fragments.</summary>
|
||||
public class DialogExpressionScope : IFormulaScope
|
||||
{
|
||||
private readonly DialogRuntime runtime;
|
||||
public DialogExpressionScope(DialogRuntime runtime){this.runtime=runtime;}
|
||||
public Random Random=>runtime.ExpressionRandom;
|
||||
public bool TryGet(string name,out double value)
|
||||
{
|
||||
var text=runtime.GetVariable(name);value=0;
|
||||
if(text==null)return false;
|
||||
if(!double.TryParse(text,NumberStyles.Float,CultureInfo.InvariantCulture,out value))
|
||||
throw new ExpressionEvaluationException("Variable '"+name+"' is not numeric.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,15 @@ namespace Cthangover.Mods.Dialog.Scenarios
|
||||
/// </summary>
|
||||
public interface IScenarioCommandSpec
|
||||
{
|
||||
/// <summary>Assignment variable names retain their case; ordinary named parameters remain case insensitive.</summary>
|
||||
bool PreserveNamedArgumentCase => false;
|
||||
|
||||
/// <summary>Optional command-owned semantic validation, shared by runtime construction and static author tools.</summary>
|
||||
string ValidateArguments(List<string> positional, Dictionary<string,string> named, string arrowTarget) => null;
|
||||
|
||||
/// <summary>Normalize a copy of parsed arguments to the canonical schema form; runtime syntax stays untouched.</summary>
|
||||
void NormalizeArguments(List<string> positional, Dictionary<string, string> named) { }
|
||||
|
||||
/// <summary>Minimum number of positional arguments. Default: 0.</summary>
|
||||
int MinPositional => 0;
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace Cthangover.Mods.Dialog.Scenarios
|
||||
foreach (var kv in named)
|
||||
{
|
||||
if (kv.Key is "name" or "key") continue;
|
||||
dlg.Set(kv.Key, kv.Value);
|
||||
dlg.Set(kv.Key, kv.Value).UseProcessText = true;
|
||||
}
|
||||
|
||||
dlg.Action(actionName);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Cthangover.Mods.Dialog.Scenarios
|
||||
{
|
||||
/// <summary>Explicit numeric assignment; calculation happens when the queue reaches the action, never while parsing.</summary>
|
||||
public class CsetCommandStrategy : NumericAssignmentCommandStrategy
|
||||
{
|
||||
public override string Command => "cset";
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Dialog;
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Mods.Interface.Base.Lists;
|
||||
using Cthangover.Core.UI.Dialog;
|
||||
using Cthangover.Core.UI.Event;
|
||||
using Cthangover.Core.UI.Executable;
|
||||
using Cthangover.Core.Scenarios;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Core.Interactive;
|
||||
|
||||
namespace Cthangover.Mods.Dialog.Scenarios
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the <c>set</c> DSL command. Stores an arbitrary key-value pair
|
||||
/// into the dialog session's variable state via <see cref="DialogQueue.Set"/>.
|
||||
/// Values are read back later through <c>${var}</c> substitution in
|
||||
/// <c>ptext</c> lines and via <see cref="Cthangover.Core.Actions.IActionContext.GetParam"/>
|
||||
/// in scenario actions.
|
||||
/// </summary>
|
||||
public class SetCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec
|
||||
/// <summary>Legacy set name value remains literal. The previously unsupported name=expression form calculates a number.</summary>
|
||||
public class SetCommandStrategy : NumericAssignmentCommandStrategy
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public string Command => "set";
|
||||
/// <inheritdoc/>
|
||||
public int MinPositional => 2;
|
||||
/// <inheritdoc/>
|
||||
public int MaxPositional => 2;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(DialogQueue dlg, List<string> positional, Dictionary<string, string> named, string arrowTarget, ILocalizationProvider locale)
|
||||
{
|
||||
var name = positional.Count > 0 ? positional[0] : "";
|
||||
var value = positional.Count > 1 ? positional[1] : "";
|
||||
dlg.Set(name, value);
|
||||
}
|
||||
public override string Command => "set";
|
||||
protected override bool CalculatePositional => false;
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,8 @@ namespace Cthangover.Mods.Dialog.Scenarios
|
||||
{
|
||||
var condition = string.Join(" ", positional);
|
||||
named.TryGetValue("else", out var otherwise);
|
||||
dlg.IfGoTo(() => ScenarioCondition.Evaluate(condition), Label(arrowTarget), Label(otherwise));
|
||||
var branch = dlg.IfGoTo(null, Label(arrowTarget), Label(otherwise));
|
||||
branch.ScopedCondition = scope => ScenarioCondition.Evaluate(condition, scope);
|
||||
}
|
||||
|
||||
private static string Label(string target) => string.IsNullOrWhiteSpace(target) ? null : target.Trim().TrimStart(':');
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
namespace Cthangover.Mods.Dialog.Scenarios
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Localization;
|
||||
using Cthangover.Mods.Stats.Formulas;
|
||||
|
||||
/// <summary>Shared assignment syntax and validation; each command strategy chooses literal or calculated positional values.</summary>
|
||||
public abstract class NumericAssignmentCommandStrategy : IScenarioCommandStrategy, IScenarioCommandSpec
|
||||
{
|
||||
public abstract string Command { get; }
|
||||
protected virtual bool CalculatePositional => true;
|
||||
public int MinArguments => 1;
|
||||
public int MaxPositional => 2;
|
||||
public bool PreserveNamedArgumentCase => true;
|
||||
public string ValidateArguments(List<string> positional, Dictionary<string,string> named, string arrowTarget)
|
||||
{
|
||||
if(!string.IsNullOrEmpty(arrowTarget))return "Assignments do not accept a jump target.";
|
||||
if(!TryAssignment(positional,named,out var name,out var value))return "Use name value or name=expression, with one assignment per line.";
|
||||
if(string.IsNullOrWhiteSpace(name))return "Assignment name is empty.";
|
||||
if((CalculatePositional||named.Count>0)&&!Formula.TryParse(value,out _,out var error))return error;
|
||||
return null;
|
||||
}
|
||||
public void Execute(DialogQueue dialog,List<string> positional,Dictionary<string,string> named,string arrowTarget,ILocalizationProvider locale)
|
||||
{
|
||||
var error=ValidateArguments(positional,named,arrowTarget);
|
||||
TryAssignment(positional,named,out var name,out var value);
|
||||
if(error!=null||CalculatePositional||named.Count>0)dialog.Calculate(name,value,error);
|
||||
else dialog.Set(name,value);
|
||||
}
|
||||
public void NormalizeArguments(List<string> positional, Dictionary<string, string> named)
|
||||
{
|
||||
if (positional.Count == 0 && named.Count == 1 && TryAssignment(positional, named, out var name, out var value))
|
||||
{
|
||||
positional.Add(name);
|
||||
positional.Add(value);
|
||||
named.Clear();
|
||||
}
|
||||
}
|
||||
private static bool TryAssignment(List<string> positional,Dictionary<string,string> named,out string name,out string value)
|
||||
{
|
||||
name=value=null;
|
||||
if(positional.Count==2&&named.Count==0){name=positional[0];value=positional[1];return true;}
|
||||
if(positional.Count==0&&named.Count==1)
|
||||
foreach(var pair in named){name=pair.Key;value=pair.Value;return true;}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,6 +144,7 @@ namespace Cthangover.Mods.Dialog.Scenarios
|
||||
return null;
|
||||
|
||||
var cmd = tokens[0].ToLowerInvariant();
|
||||
var spec = ScenarioCommandStrategyFactory.Get(cmd) as IScenarioCommandSpec;
|
||||
for (var i = 1; i < tokens.Count; i++)
|
||||
{
|
||||
var t = tokens[i];
|
||||
@@ -163,7 +164,7 @@ namespace Cthangover.Mods.Dialog.Scenarios
|
||||
var name = t.Substring(0, eqIdx);
|
||||
if (IsValidIdent(name))
|
||||
{
|
||||
named[name.ToLowerInvariant()] = t.Substring(eqIdx + 1);
|
||||
named[spec?.PreserveNamedArgumentCase == true ? name : name.ToLowerInvariant()] = t.Substring(eqIdx + 1);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -242,39 +243,34 @@ namespace Cthangover.Mods.Dialog.Scenarios
|
||||
|
||||
private static List<string> Tokenize(string line)
|
||||
{
|
||||
var result = new List<string>();
|
||||
var i = 0;
|
||||
|
||||
while (i < line.Length)
|
||||
var tokens=new List<string>();
|
||||
var i=0;
|
||||
while(i<line.Length)
|
||||
{
|
||||
if (char.IsWhiteSpace(line[i]))
|
||||
if(char.IsWhiteSpace(line[i])){i++;continue;}
|
||||
var token=new System.Text.StringBuilder();
|
||||
while(i<line.Length&&!char.IsWhiteSpace(line[i]))
|
||||
{
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line[i] == '"')
|
||||
{
|
||||
i++;
|
||||
var start = i;
|
||||
while (i < line.Length && line[i] != '"')
|
||||
// Preserve quotes embedded in JSON. Quotes at the start of a value, including name="...",
|
||||
// delimit one DSL argument and allow spaces without changing existing JSON commands.
|
||||
var quoted=false;
|
||||
if(line[i]=='"')
|
||||
{
|
||||
if (line[i] == '\\' && i + 1 < line.Length)
|
||||
i++;
|
||||
i++;
|
||||
var prefix=token.ToString();
|
||||
quoted=token.Length==0||prefix.EndsWith("=")&&IsValidIdent(prefix.Substring(0,prefix.Length-1));
|
||||
}
|
||||
result.Add(line[start..i].Replace("\\\"", "\""));
|
||||
if (i < line.Length) i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var tokenStart = i;
|
||||
while (i < line.Length && !char.IsWhiteSpace(line[i]))
|
||||
if(!quoted){token.Append(line[i++]);continue;}
|
||||
i++;
|
||||
result.Add(line[tokenStart..i]);
|
||||
while(i<line.Length&&line[i]!='"')
|
||||
{
|
||||
if(line[i]==(char)92&&i+1<line.Length&&line[i+1]=='"'){token.Append('"');i+=2;}
|
||||
else token.Append(line[i++]);
|
||||
}
|
||||
if(i<line.Length)i++;
|
||||
}
|
||||
tokens.Add(token.ToString());
|
||||
}
|
||||
|
||||
return result;
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private static bool IsValidIdent(string s)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace Cthangover.Mods.Dialog.Action.Impls
|
||||
{
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Cthangover.Mods.Stats.Formulas;
|
||||
/// <summary>Evaluate once at the queue position; dependent actions are aborted on invalid input instead of seeing a stale result.</summary>
|
||||
public class ActionCalculate : ActionCommand
|
||||
{
|
||||
public string Name {get;set;}
|
||||
public string Expression {get;set;}
|
||||
public string ArgumentError {get;set;}
|
||||
public override WaitType WaitType {get;set;}=WaitType.NoWait;
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
try
|
||||
{
|
||||
if(ArgumentError!=null)throw new ArgumentException(ArgumentError);
|
||||
var value=Formula.Parse(Expression).EvaluateStrict(new DialogExpressionScope(runtime));
|
||||
runtime.SetVariable(Name,value.ToString("G17",CultureInfo.InvariantCulture));
|
||||
}
|
||||
catch(Exception ex){runtime.FailExpression($"Assignment '{Name}': {ex.Message}",SourceLine);}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,13 @@ namespace Cthangover.Mods.Dialog.Action.Impls
|
||||
{
|
||||
/// <summary>Delegate evaluated at runtime. Must return true/false to select the branch target.</summary>
|
||||
public Func<bool> Condition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Used instead of <see cref="Condition"/> when set: evaluated with the instance scope of the
|
||||
/// running dialog (<see cref="DialogQueue.Scope"/>), so <c>when "self.open == 1"</c> reads the flag of
|
||||
/// the object that started the dialog, even when the line was parsed into another queue first.
|
||||
/// </summary>
|
||||
public Func<string, bool> ScopedCondition { get; set; }
|
||||
/// <summary>Target action ID if <see cref="Condition"/> returns true. Empty or null causes fall-through.</summary>
|
||||
public string TrueGoTo { get; set; }
|
||||
/// <summary>Target action ID if <see cref="Condition"/> returns false. Empty or null causes fall-through.</summary>
|
||||
@@ -34,13 +41,13 @@ namespace Cthangover.Mods.Dialog.Action.Impls
|
||||
/// <summary>Evaluates <see cref="Condition"/> and jumps to <see cref="TrueGoTo"/> or <see cref="FalseGoTo"/>; an empty target falls through to the next action.</summary>
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
if (Condition == null)
|
||||
if (Condition == null && ScopedCondition == null)
|
||||
{
|
||||
GameLogger.Log("DIALOG", "ActionIf: Condition delegate is null", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var result = Condition();
|
||||
var result = ScopedCondition != null ? ScopedCondition(runtime?.Dialog?.Scope) : Condition();
|
||||
var goTo = result ? TrueGoTo : FalseGoTo;
|
||||
|
||||
if (!string.IsNullOrEmpty(goTo))
|
||||
|
||||
@@ -48,7 +48,8 @@ namespace Cthangover.Mods.Dialog.Action.Impls
|
||||
if (dialogBox == null)
|
||||
return;
|
||||
|
||||
var shown = Variants.Where(v => v.IsShown()).ToList();
|
||||
var scope = runtime.Dialog?.Scope;
|
||||
var shown = Variants.Where(v => v.IsShown(scope)).ToList();
|
||||
WaitType = shown.Count == 0 ? WaitType.NoWait : WaitType.WaitSelect;
|
||||
if (shown.Count == 0)
|
||||
{
|
||||
|
||||
@@ -25,6 +25,8 @@ namespace Cthangover.Mods.Dialog.Action.Impls
|
||||
public string Value { get; set; }
|
||||
/// <summary>Lazy evaluation delegate. When non-null, invoked at runtime to compute the value dynamically.</summary>
|
||||
public Func<string> Callback { get; set; }
|
||||
/// <summary>Only action parameter templates expand references; ordinary set literals retain their historical behavior.</summary>
|
||||
public bool UseProcessText { get; set; }
|
||||
|
||||
/// <summary>Variable assignment is instant — the dialog continues without pause.</summary>
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
@@ -33,6 +35,12 @@ namespace Cthangover.Mods.Dialog.Action.Impls
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
var val = Callback != null ? Callback() : Value;
|
||||
if (UseProcessText)
|
||||
{
|
||||
try { val = runtime.ProcessParameter(val); }
|
||||
catch (Cthangover.Mods.Expressions.ExpressionEvaluationException ex)
|
||||
{ runtime.FailExpression(ex.Message, SourceLine); return; }
|
||||
}
|
||||
runtime.SetVariable(Name, val ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace Cthangover.Mods.Dialog.Action.Impls
|
||||
/// flag is game state: it is saved, read by scenario conditions
|
||||
/// (<c>condition: door == "open"</c>) and its change rescans the scene's
|
||||
/// event chain, so a scenario waiting for it can start right after.
|
||||
/// <c>flag self.open 1</c> writes the flag of the dialog's instance scope
|
||||
/// (<see cref="DialogQueue.Scope"/>), e.g. <c>rpg_village/supply_1:open</c>.
|
||||
/// </summary>
|
||||
public class ActionSetFlag : ActionCommand
|
||||
{
|
||||
@@ -20,6 +22,11 @@ namespace Cthangover.Mods.Dialog.Action.Impls
|
||||
public override WaitType WaitType { get; set; } = WaitType.NoWait;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DoRun(DialogRuntime runtime) => ScenarioCondition.SetFlag(Name, Value ?? string.Empty);
|
||||
public override void DoRun(DialogRuntime runtime)
|
||||
{
|
||||
var key = ScenarioCondition.FlagKey(Name, runtime?.Dialog?.Scope);
|
||||
if (key != null)
|
||||
ScenarioCondition.SetFlag(key, Value ?? string.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,8 +30,8 @@ namespace Cthangover.Mods.Dialog.Action.Impls
|
||||
/// </summary>
|
||||
public string Condition { get; set; }
|
||||
|
||||
/// <summary>Whether the option is offered now.</summary>
|
||||
public bool IsShown() => string.IsNullOrWhiteSpace(Condition) || ScenarioCondition.Evaluate(Condition);
|
||||
/// <summary>Whether the option is offered now; <paramref name="scope"/> is the dialog's instance scope (<see cref="DialogQueue.Scope"/>, may be null).</summary>
|
||||
public bool IsShown(string scope) => string.IsNullOrWhiteSpace(Condition) || ScenarioCondition.Evaluate(Condition, scope);
|
||||
|
||||
/// <summary>Generates a new unique action ID for use as a GoTo target (e.g. with <see cref="DialogQueue.Point"/>).</summary>
|
||||
public static string Point => Guid.NewGuid().ToString();
|
||||
|
||||
@@ -18,6 +18,7 @@ using Cthangover.Core.Interactive;
|
||||
|
||||
namespace Cthangover.Mods.Dialog
|
||||
{
|
||||
using Cthangover.Core.Events;
|
||||
/// <summary>
|
||||
/// The visible dialog UI: dual avatars with shader-based hide-color mode,
|
||||
/// answer box spawning, and click/keyboard advance. Validates that no existing
|
||||
@@ -47,6 +48,7 @@ namespace Cthangover.Mods.Dialog
|
||||
protected override void OnReady()
|
||||
{
|
||||
MouseFilter = MouseFilterEnum.Ignore;
|
||||
ModEventBus.Subscribe<SceneExitedEvent>(OnSceneExited);
|
||||
|
||||
if (Body == null)
|
||||
{
|
||||
@@ -373,6 +375,26 @@ namespace Cthangover.Mods.Dialog
|
||||
Locker = null;
|
||||
}
|
||||
|
||||
/// <summary>Detaches the scene-exit hook when the box leaves the tree.</summary>
|
||||
protected override void OnExitTree()
|
||||
{
|
||||
ModEventBus.Unsubscribe<SceneExitedEvent>(OnSceneExited);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Leaving a logical scene ends a dialog that is still running in it. The box belongs to the shell and
|
||||
/// survives scene switches, so nothing else would release its locker: a switch in the middle of a dialog
|
||||
/// would keep the box busy, and the next scene's events and the player's walking would wait for it. The
|
||||
/// dialog is aborted, not finished: its end actions and <see cref="DialogEndedEvent"/> do not run.
|
||||
/// </summary>
|
||||
private void OnSceneExited(SceneExitedEvent evt)
|
||||
{
|
||||
if (!IsRunning)
|
||||
return;
|
||||
GameLogger.Log("DIALOG", $"scene '{evt.SceneName}' left during a dialog ('{Locker?.ID ?? "click dialog"}'): dialog aborted", LogLevel.Warning);
|
||||
Abort();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a dialog occupies the box: one started by an event (Locker is not null) or a locker-less queue
|
||||
/// that has not ended yet (an interactive's click dialog runs without a locker). Event chains and the click
|
||||
|
||||
@@ -33,6 +33,15 @@ namespace Cthangover.Mods.Dialog
|
||||
/// <summary>The ordered sequence of dialog actions. Appended via fluent builder methods; consumed by <see cref="DialogRuntime"/>.</summary>
|
||||
public List<IActionCommand> Queue { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The instance scope of this dialog — the placement id of the object whose interaction runs it —
|
||||
/// or null. Inside it <c>self.<name></c> in <c>flag</c>, <c>when</c> and option conditions means
|
||||
/// the story flag <c><scope>:<name></c> (<see cref="ScenarioCondition.FlagKey"/>). Set by
|
||||
/// whoever starts the dialog (the RPG runtime) before it runs; a queue with no scope rejects
|
||||
/// <c>self.</c> with an ERROR instead of writing a global flag.
|
||||
/// </summary>
|
||||
public string Scope { get; set; }
|
||||
|
||||
/// <summary>Spawns a visual effect identified by <paramref name="effectID"/> through <see cref="EffectFactory"/>. Returns the action for further configuration.</summary>
|
||||
public ActionEffect Effect(string effectID)
|
||||
{
|
||||
@@ -130,6 +139,14 @@ namespace Cthangover.Mods.Dialog
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Enqueues a strict numeric assignment; expressions and random functions are evaluated at execution.</summary>
|
||||
public ActionCalculate Calculate(string name, string expression, string argumentError = null)
|
||||
{
|
||||
var action = new ActionCalculate { Name = name, Expression = expression, ArgumentError = argumentError };
|
||||
Queue.Add(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>Sets a story flag of the game (saved, read by scenario conditions) when the queue reaches this point.</summary>
|
||||
public ActionSetFlag Flag(string name, string value)
|
||||
{
|
||||
|
||||
@@ -81,6 +81,35 @@ namespace Cthangover.Mods.Dialog
|
||||
/// <summary>Queue index of <see cref="CurrentAction"/>.</summary>
|
||||
public int Index => index;
|
||||
|
||||
/// <summary>Inject a seeded generator for reproducible scripts and tests; expressions never roll while parsing.</summary>
|
||||
public System.Random ExpressionRandom { get; set; } = System.Random.Shared;
|
||||
public string LastExpressionError { get; private set; }
|
||||
|
||||
/// <summary>Abort a malformed calculation without completing a one-run scenario or running later reward actions.</summary>
|
||||
public void FailExpression(string message, int sourceLine = 0)
|
||||
{
|
||||
var scenario = DialogBox?.Locker as Cthangover.Mods.Dialog.Executable.ScenarioEvent;
|
||||
var origin = scenario?.SourcePath ?? DialogBox?.Locker?.ID ?? "<inline>";
|
||||
LastExpressionError = origin + ":" + sourceLine + ": " + message;
|
||||
GameLogger.Log("DIALOG", LastExpressionError, LogLevel.Error);
|
||||
if (DialogBox != null) DialogBox.Abort(); else Abort();
|
||||
}
|
||||
|
||||
/// <summary>Resolve action parameters at execution, preserving string values and rejecting undefined references.</summary>
|
||||
public string ProcessParameter(string text)
|
||||
{
|
||||
if (text == null) return null;
|
||||
var names = text.GetIncludesInQuotes("${", "}");
|
||||
if (names != null)
|
||||
foreach (var name in names)
|
||||
{
|
||||
if (!variables.TryGetValue(name, out var value))
|
||||
throw new Cthangover.Mods.Expressions.ExpressionEvaluationException("Undefined action parameter variable: " + name);
|
||||
text = text.Replace("${" + name + "}", value);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/// <summary>Current <c>${name}</c> variables of the dialog, for inspection.</summary>
|
||||
public IReadOnlyDictionary<string, string> Variables => (IReadOnlyDictionary<string, string>)variables;
|
||||
|
||||
@@ -153,6 +182,7 @@ namespace Cthangover.Mods.Dialog
|
||||
|
||||
generation++;
|
||||
variables = new Dictionary<string, string>();
|
||||
LastExpressionError = null;
|
||||
DialogBox = dialogBox;
|
||||
IsHeld = false;
|
||||
isLastGoTo = false;
|
||||
|
||||
Reference in new issue
Block a user