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
@@ -2,6 +2,7 @@ namespace Cthangover.Mods.Dialog.Tests
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Cthangover.Core.Events;
|
||||
using Cthangover.Core.Mods.Testing;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Mods.Dialog.Action.Impls;
|
||||
@@ -49,6 +50,30 @@ namespace Cthangover.Mods.Dialog.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[ModTest("box/scene_exit_ends_running_dialog", TimeoutSeconds = 10)]
|
||||
static async Task SceneExitEndsRunningDialog(ModTestContext ctx)
|
||||
{
|
||||
var stage = await DialogStage.Create();
|
||||
try
|
||||
{
|
||||
var running = new DialogQueue();
|
||||
running.Queue.Add(new ProbeAction());
|
||||
ModAssert.True(stage.Box.SetDialogQueueAndRun(running, null, 0, null), "a dialog starts");
|
||||
ModAssert.True(stage.Box.IsRunning, "the dialog occupies the box");
|
||||
|
||||
ModEventBus.Publish(new SceneExitedEvent { SceneName = "dialog_base_test_scene" });
|
||||
ModAssert.False(stage.Box.IsRunning, "leaving the scene releases the box");
|
||||
|
||||
var next = new DialogQueue();
|
||||
next.Queue.Add(new ProbeAction());
|
||||
ModAssert.True(stage.Box.SetDialogQueueAndRun(next, null, 0, null), "the next scene's dialog starts on the freed box");
|
||||
}
|
||||
finally
|
||||
{
|
||||
stage.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[ModTest("box/held_delay_is_not_skipped", TimeoutSeconds = 10)]
|
||||
static async Task HeldDelayIsNotSkipped(ModTestContext ctx)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
namespace Cthangover.Mods.Dialog.Tests
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Cthangover.Core.Mods.Testing;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Core.Utils;
|
||||
using Cthangover.Mods.Dialog.Scenarios;
|
||||
|
||||
/// <summary>
|
||||
/// <c>self.</c> in a dialog (contract <c>rpg-model-v2.md</c> 5.5): the queue's
|
||||
/// <see cref="DialogQueue.Scope"/> — the placement that started it — turns <c>self.open</c> into the
|
||||
/// story flag <c><placement>:open</c> in <c>flag</c>, <c>when</c> and option conditions. Two
|
||||
/// instances of one object keep their own state, which is an ordinary saved flag.
|
||||
/// </summary>
|
||||
public class InstanceScopeTests
|
||||
{
|
||||
private const string First = "test_scope_map/barrel_1";
|
||||
private const string Second = "test_scope_map/barrel_2";
|
||||
|
||||
/// <summary>The shared scenario of both barrels: open once, then say so.</summary>
|
||||
private const string Barrel = "when \"self.open == 1\" -> :opened\nflag self.open 1\nset outcome opened_now\nend\n:opened\nset outcome already_open\nend\n";
|
||||
|
||||
[ModTest("scope/two_instances_are_independent_and_survive_a_save", TimeoutSeconds = 10)]
|
||||
static async Task TwoInstancesAreIndependentAndSurviveASave(ModTestContext ctx)
|
||||
{
|
||||
var stage = await DialogStage.Create();
|
||||
var runtime = GameData.Instance.Runtime;
|
||||
var savedFlags = runtime.Flags;
|
||||
runtime.Flags = new Dictionary<string, string>();
|
||||
try
|
||||
{
|
||||
ModAssert.Equal("opened_now", Play(stage, Barrel, First), "the first barrel opens");
|
||||
ModAssert.Equal("already_open", Play(stage, Barrel, First), "the first barrel remembers it");
|
||||
ModAssert.Equal("opened_now", Play(stage, Barrel, Second), "the second barrel is still closed");
|
||||
ModAssert.Equal("1", runtime.Flags.TryGetValue(First + ":open", out var value) ? value : null, "the flag has the placement's full name");
|
||||
ModAssert.Equal(2, runtime.Flags.Count, "no global 'open' flag is written");
|
||||
ModAssert.True(ScenarioCondition.Evaluate(First + ":open == 1"), "other scenarios read it by its full name");
|
||||
|
||||
var slot = RuntimeSerializer.BuildCoreSlot(runtime, "1.0.0", "test", new HashSet<string>());
|
||||
var json = JsonSerializer.Serialize(slot, JsonOptionsProvider.WriteCompactOptions);
|
||||
runtime.Flags = new Dictionary<string, string>();
|
||||
ModAssert.False(ScenarioCondition.Evaluate("self.open == 1", First), "state cleared before the load");
|
||||
RuntimeSerializer.RestoreCoreState(JsonSerializer.Deserialize<ModSlot>(json, JsonOptionsProvider.ReadOptions), runtime);
|
||||
|
||||
ModAssert.True(ScenarioCondition.Evaluate("self.open == 1", First), "the first barrel is open after the load");
|
||||
ModAssert.True(ScenarioCondition.Evaluate("self.open == 1", Second), "the second barrel is open after the load");
|
||||
ModAssert.Equal("already_open", Play(stage, Barrel, Second), "the loaded state drives the dialog");
|
||||
}
|
||||
finally
|
||||
{
|
||||
runtime.Flags = savedFlags;
|
||||
stage.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[ModTest("scope/option_conditions_read_the_instance", TimeoutSeconds = 10)]
|
||||
static async Task OptionConditionsReadTheInstance(ModTestContext ctx)
|
||||
{
|
||||
var stage = await DialogStage.Create();
|
||||
var runtime = GameData.Instance.Runtime;
|
||||
var savedFlags = runtime.Flags;
|
||||
runtime.Flags = new Dictionary<string, string> { [First + ":open"] = "1" };
|
||||
try
|
||||
{
|
||||
var dlg = ScenarioParser.Parse("select \"?\"\noption \"Close\" \"if=self.open == 1\" -> :a\noption \"Open\" \"if=self.open != 1\" -> :b\noption \"Leave\" -> :c\n:a\nend\n:b\nend\n:c\nend\n");
|
||||
dlg.Scope = First;
|
||||
ModAssert.True(stage.Box.SetDialogQueueAndRun(dlg, null, 0, null), "dialog starts");
|
||||
ModAssert.Equal(2, stage.Box.AnswerVariantCount, "only the option of the open instance and the plain one");
|
||||
}
|
||||
finally
|
||||
{
|
||||
runtime.Flags = savedFlags;
|
||||
stage.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[ModTest("scope/self_without_a_scope_writes_nothing", TimeoutSeconds = 10)]
|
||||
static async Task SelfWithoutAScopeWritesNothing(ModTestContext ctx)
|
||||
{
|
||||
var stage = await DialogStage.Create();
|
||||
var runtime = GameData.Instance.Runtime;
|
||||
var savedFlags = runtime.Flags;
|
||||
runtime.Flags = new Dictionary<string, string>();
|
||||
try
|
||||
{
|
||||
ModAssert.Equal("opened_now", Play(stage, Barrel, null), "the dialog still runs to its end");
|
||||
ModAssert.Equal(0, runtime.Flags.Count, "self. outside a scope is never a global flag");
|
||||
}
|
||||
finally
|
||||
{
|
||||
runtime.Flags = savedFlags;
|
||||
stage.Free();
|
||||
}
|
||||
}
|
||||
|
||||
private static string Play(DialogStage stage, string scenario, string scope)
|
||||
{
|
||||
var dlg = new ScenarioConverter().Convert(scenario);
|
||||
dlg.Scope = scope;
|
||||
ModAssert.True(stage.Box.SetDialogQueueAndRun(dlg, null, 0, null), "dialog starts");
|
||||
return stage.Box.Runtime.GetVariable("outcome");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
namespace Cthangover.Mods.Dialog.Tests
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using Cthangover.Core.Factories.Impls;
|
||||
using Cthangover.Core.Mods.Testing;
|
||||
using Cthangover.Mods.Dialog.Scenarios;
|
||||
using Godot;
|
||||
|
||||
/// <summary>Old string scripts, calculated assignments and action arguments share the existing DSL strategy pipeline.</summary>
|
||||
public class NumericAssignmentTests
|
||||
{
|
||||
[ModTest("numeric_assignment/literals_case_and_assignment_syntax")]
|
||||
static void Assignments(ModTestContext ctx)
|
||||
{
|
||||
var runtime=Run("set greeting Hello\nset literal 1+2\nset Value 7\ncset total \"1 + 2 + ${Value}\"\nset Money=rnd(3,3)\ncset quoted=\"${total} * 2\"\nend\n");
|
||||
ModAssert.Equal("Hello",runtime.GetVariable("greeting"),"old strings stay strings");
|
||||
ModAssert.Equal("1+2",runtime.GetVariable("literal"),"set does not guess expressions");
|
||||
ModAssert.Equal("10",runtime.GetVariable("total"),"earlier numeric string is read at runtime");
|
||||
ModAssert.Equal("3",runtime.GetVariable("Money"),"assigned variable keeps its case");
|
||||
ModAssert.Equal("20",runtime.GetVariable("quoted"),"quoted assignment expression retains spaces");
|
||||
ModAssert.Null(runtime.LastExpressionError,"valid assignments have no errors");
|
||||
}
|
||||
[ModTest("numeric_assignment/errors_abort_dependents_without_overwriting")]
|
||||
static void Errors(ModTestContext ctx)
|
||||
{
|
||||
foreach(var expression in new[]{"1/0","${missing}+1","rnd(5,3)","max()"})
|
||||
{
|
||||
var runtime=Run("set amount 9\ncset amount \""+expression+"\"\nset after wrong\nend\n");
|
||||
ModAssert.Equal("9",runtime.GetVariable("amount"),"bad assignment preserves the previous value for inspection");
|
||||
ModAssert.Null(runtime.GetVariable("after"),"dependent actions never run after a bad calculation");
|
||||
ModAssert.True(runtime.LastExpressionError?.Contains(":2:")==true,"error reports the source line");
|
||||
}
|
||||
var text=Run("set value abc\ncset result ${value}+1\nset after wrong\nend\n");
|
||||
ModAssert.True(text.LastExpressionError?.Contains("not numeric")==true,"non-numeric variables are diagnosed");
|
||||
}
|
||||
[ModTest("numeric_assignment/invariant_format_and_json_tokens")]
|
||||
static void Formatting(ModTestContext ctx)
|
||||
{
|
||||
var culture=CultureInfo.CurrentCulture;
|
||||
try
|
||||
{
|
||||
// A hand-built comma culture: ru-RU/es-ES cannot be created where ICU is missing
|
||||
// (invariant globalization), and the rule must hold for any culture anyway.
|
||||
var comma=(CultureInfo)CultureInfo.InvariantCulture.Clone();
|
||||
comma.NumberFormat.NumberDecimalSeparator=",";
|
||||
comma.NumberFormat.NumberGroupSeparator=" ";
|
||||
CultureInfo.CurrentCulture=comma;
|
||||
ModAssert.Equal("2.5",Run("cset amount 5/2\nend\n").GetVariable("amount"),"stored numeric syntax does not depend on UI culture");
|
||||
}
|
||||
finally{CultureInfo.CurrentCulture=culture;}
|
||||
var positional=new List<string>();var named=new Dictionary<string,string>();
|
||||
ScenarioParser.SplitLine("light_set [{\"x\":0.2,\"color\":\"#fff\"}]",positional,named,out _);
|
||||
ModAssert.Equal("[{\"x\":0.2,\"color\":\"#fff\"}]",positional[0],"JSON quotes are not stripped by assignment quoting");
|
||||
ModAssert.Equal(0,named.Count,"JSON never becomes a named argument");
|
||||
}
|
||||
[ModTest("numeric_assignment/command_owned_validation")]
|
||||
static void Validation(ModTestContext ctx)
|
||||
{
|
||||
IScenarioCommandSpec spec=new CsetCommandStrategy();
|
||||
ModAssert.NotNull(spec.ValidateArguments(new(){"amount"},new(),""),"missing operand diagnosed");
|
||||
ModAssert.NotNull(spec.ValidateArguments(new(){"amount","rnd(1)"},new(),""),"function arity checked without rolling");
|
||||
ModAssert.Null(spec.ValidateArguments(new(),new(){["Money"]="rnd(3,5)"},""),"assignment form recognized by strategy contract");
|
||||
var positional=new List<string>();var named=new Dictionary<string,string>{["Money"]="rnd(3,5)"};
|
||||
spec.NormalizeArguments(positional,named);
|
||||
ModAssert.Equal("Money",positional[0],"schema normalization preserves case");
|
||||
ModAssert.Equal("rnd(3,5)",positional[1],"schema receives the unevaluated expression");
|
||||
ModAssert.Equal(0,named.Count,"normalized arguments are canonical");
|
||||
}
|
||||
[ModTest("numeric_assignment/localized_text_and_action_share_one_value",TimeoutSeconds=10)]
|
||||
static async Task TextAndAction(ModTestContext ctx)
|
||||
{
|
||||
var stage=await DialogStage.Create();var capture=new NumericCaptureAction();
|
||||
ScenarioActionFactory.Instance.Register(capture);
|
||||
try
|
||||
{
|
||||
var dialog=ScenarioParser.Parse("cset money=rnd(3,3)\naction dsl_test.capture id=money/coin count=${money}\nptext \"Fallback\" key=mod_dialog_base_name\nend\n",new NumericTextLocale());
|
||||
stage.Box.SetDialogQueueAndRun(dialog,null,0,null);
|
||||
ModAssert.Equal("3",capture.Amount,"action receives a number, never the placeholder");
|
||||
ModAssert.Equal("money/coin",capture.Item,"ordinary string parameters still work");
|
||||
ModAssert.Equal("Received 3 coins.",stage.Box.GetNode<Label>("DialogBody/TextField").Text,"localized ptext displays the same stored value");
|
||||
stage.Box.NextAction();
|
||||
ModAssert.Equal("3",stage.Box.Runtime.GetVariable("money"),"continuing the text does not reroll");
|
||||
}
|
||||
finally{ScenarioActionFactory.Instance.UnregisterAssembly(typeof(NumericCaptureAction).Assembly);stage.Free();}
|
||||
}
|
||||
[ModTest("numeric_assignment/undefined_action_parameter_stops_action")]
|
||||
static void MissingParameter(ModTestContext ctx)
|
||||
{
|
||||
var capture=new NumericCaptureAction();ScenarioActionFactory.Instance.Register(capture);
|
||||
try
|
||||
{
|
||||
var runtime=Run("set marker kept\naction dsl_test.capture count=${missing}\nset marker wrong\nend\n");
|
||||
ModAssert.Null(capture.Amount,"invalid parameter is never delivered to the action");
|
||||
ModAssert.Equal("kept",runtime.GetVariable("marker"),"later state changes are skipped");
|
||||
ModAssert.True(runtime.LastExpressionError?.Contains("missing")==true,"undefined parameter is reported");
|
||||
}
|
||||
finally{ScenarioActionFactory.Instance.UnregisterAssembly(typeof(NumericCaptureAction).Assembly);}
|
||||
}
|
||||
private static DialogRuntime Run(string script)
|
||||
{
|
||||
var dialog=ScenarioParser.Parse(script);var runtime=new DialogRuntime();
|
||||
runtime.SetDialogQueueAndRun(null,dialog.Queue,null);runtime.Run();return runtime;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Cthangover.Mods.Dialog.Tests
|
||||
{
|
||||
using Cthangover.Core.Actions;
|
||||
internal class NumericCaptureAction : IScenarioAction
|
||||
{
|
||||
public string Name=>"dsl_test.capture";
|
||||
public string Amount {get;private set;}
|
||||
public string Item {get;private set;}
|
||||
public void Run(IActionContext context){Amount=context.GetParam("count");Item=context.GetParam("id");}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Cthangover.Mods.Dialog.Tests
|
||||
{
|
||||
using Cthangover.Core.Localization;
|
||||
internal class NumericTextLocale : ILocalizationProvider
|
||||
{
|
||||
public string Get(string key)=>key=="mod_dialog_base_name"?"Received ${money} coins.":null;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user