This commit is contained in:
iisaev committed 2026-10-18 12:41:38 +03:00
commit 5de3fd7ad8
121 files changed
+6632

No files matched your search

@@ -0,0 +1,2 @@
mod_dialog_base__gdscript_name = GDScript Integration Test Mod
mod_dialog_base__gdscript_desc = Integration tests for GDScript mod actions: .gd load, compile, action registration and scenario execution.
@@ -0,0 +1,2 @@
mod_dialog_base__gdscript_name = GDScript-мод интеграционных тестов
mod_dialog_base__gdscript_desc = Интеграционные тесты GDScript-действий модов: загрузка .gd, компиляция, регистрация действий и выполнение сценария.
+13
View File
@@ -0,0 +1,13 @@
{
"name": "GDScript test",
"description": "Integration test of GDScript mods: the suite scripts (actions, conditions, an item action, an initializer) go through .gd load, compile and registration; the driver checks every action is registered and evaluates the conditions. Mounted by --itest=gdscript.",
"sources": [
"src/*.cs"
],
"gd_sources": [
"scripts/*.gd"
],
"depends": [
"visual_novel_core>=1"
]
}
@@ -0,0 +1,8 @@
scene: home_kitchen
priority: 8
condition: true
is_one_run: true
---
action gds.greet name=Always
text "Fallback scene always runs."
end
@@ -0,0 +1,9 @@
scene: home_kitchen
priority: 7
condition: gds.flag_equals("victory")
is_one_run: true
---
set custom_flag victory
action gds.inventory_demo item_id=health_potion
text "GDScript condition with args and inventory action executed."
end
@@ -0,0 +1,9 @@
scene: home_kitchen
priority: 6
condition: gds.is_rain
is_one_run: true
---
set weather rain
action gds.greet name=RainVisitor
text "It's raining! GDScript condition gds.is_rain passed."
end
@@ -0,0 +1,10 @@
scene: home_kitchen
priority: 5
condition: true
is_one_run: true
---
set my_name Marao
action gds.greet name=my_name
action gds.quest_log quest_id=test_quest message=Scenario started from GDScript
text "GDScript test actions executed. Check logs."
end
@@ -0,0 +1,21 @@
extends RefCounted
# Custom battle action executor: deals triple damage based on the user's Attack.
# ActionId must match the battle action ID in the character's JSON data.
# The execute method receives plain dictionaries (action/user/target) and must
# return a dictionary with any of: source_damage, source_defence, target_damage,
# target_defence, result.
func get_action_id():
return "gds.triple_attack"
func execute(action, user, target):
var damage = user["attack_value"] * 3 - target["defence_value"]
if damage < 1:
damage = 1
return {
"target_damage": damage,
"target_defence": target["defence_value"],
"result": true,
}
@@ -0,0 +1,14 @@
extends RefCounted
func get_name():
return "gds.char_welcome"
func run(ctx):
var char_type = ctx.GetParam("char_type")
if char_type == "":
ctx.LogWarning("GDS", "gds.char_welcome: missing char_type")
return
ctx.Character.AddToParty(char_type)
ctx.Character.SendNotification(char_type)
ctx.Log("GDS", "Character '" + char_type + "' added to party via GDScript")
@@ -0,0 +1,13 @@
extends RefCounted
# Custom condition with arguments: checks if a flag equals the passed value
# Usage in scenario: condition: gds.flag_equals("expected_value")
func get_condition_name():
return "gds.flag_equals"
func evaluate(flags, args):
var expected = args.get("0", "")
if expected == "":
return false
return flags.get("custom_flag", "") == expected
@@ -0,0 +1,11 @@
extends RefCounted
# Custom condition: checks if the "weather" flag equals "rain"
# Usage in scenario: condition: gds.is_rain
# Or: condition: gds.is_rain && Quest.hasTag("active")
func get_condition_name():
return "gds.is_rain"
func evaluate(flags, args):
return flags.get("weather", "") == "rain"
@@ -0,0 +1,10 @@
extends RefCounted
func get_name():
return "gds.greet"
func run(ctx):
var name = ctx.GetParam("name")
if name == "":
name = "Stranger"
ctx.Log("EVENT", "GDScript greet action: Hello, " + name + "!")
@@ -0,0 +1,21 @@
extends RefCounted
# Demonstrates inventory queries and mod presence checks from a scenario action
# Usage in scenario DSL: action gds.inventory_demo
func get_name():
return "gds.inventory_demo"
func run(ctx):
var itemId = ctx.GetParam("item_id")
if ctx.Inventory.HasItem(itemId):
var count = ctx.Inventory.CheckCount(itemId)
ctx.Log("EVENT", "Inventory has " + itemId + " x" + str(count))
else:
ctx.Log("EVENT", "Inventory does NOT have " + itemId)
if ctx.ModRegistry.IsLoaded("core"):
ctx.Log("EVENT", "Core mod is loaded")
else:
ctx.LogWarning("EVENT", "Core mod is NOT loaded")
@@ -0,0 +1,16 @@
extends RefCounted
# Custom item action: heals the character by a fixed amount when the item is used
# ID must match the ItemAction field in the item's JSON data
# The use(item) method receives a GDItem (with fields Id, Name, Description, Cost, ItemType)
# Returns true on success
func get_item_action_id():
return "gds.heal_item"
func use(item):
print("[GDS_ITEM] Using item: " + item.Name + " (id=" + item.Id + ")")
# In a real mod, perform actual healing logic here (e.g. via player stats)
# For demonstration, simply return success
return true
@@ -0,0 +1,9 @@
extends RefCounted
func get_name():
return "gds.light_reset"
func run(ctx):
ctx.Lighting.ClearDepthMap()
ctx.Lighting.SetUseTime(false)
ctx.Log("GDS", "Lighting reset via GDScript")
@@ -0,0 +1,12 @@
extends RefCounted
func get_name():
return "gds.quest_log"
func run(ctx):
var quest_id = ctx.GetParam("quest_id")
var message = ctx.GetParam("message")
if quest_id == "":
ctx.LogWarning("GDS", "gds.quest_log: missing quest_id param")
return
ctx.Log("QUEST", "[" + quest_id + "] " + message)
@@ -0,0 +1,16 @@
extends RefCounted
func get_name():
return "gds.quest_setup"
func run(ctx):
var quest_id = ctx.GetParam("quest_id")
if quest_id == "":
ctx.LogWarning("GDS", "gds.quest_setup: missing quest_id")
return
if ctx.Quests.Exists(quest_id):
ctx.Quests.SetStatus(quest_id, "Active")
ctx.Quests.AddTag(quest_id, "gds_started")
ctx.Quests.SendNotification(quest_id)
ctx.Log("GDS", "Quest '" + quest_id + "' activated by GDScript")
@@ -0,0 +1,7 @@
extends RefCounted
func on_mod_loaded(mod_id):
print("[GDS_TEST] on_mod_loaded: " + mod_id)
func on_mod_resources_ready():
print("[GDS_TEST] on_mod_resources_ready: all mods loaded")
+129
View File
@@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Cthangover.Core.Bootstrap;
using Cthangover.Core.Factories.Impls;
using Cthangover.Core.Mods;
using Cthangover.Core.Scenes;
using Cthangover.Core.Utils;
using Godot;
namespace Mods.DialogBase.Tests
{
/// <summary>Entry point of the GDScript itest; the suite is mounted only with <c>--itest=gdscript</c>.</summary>
public class GdscriptTestInitializer : IModInitializer
{
private static bool queued;
public void OnModLoaded(string modId) { }
public void OnModResourcesReady()
{
if (queued || !OS.GetCmdlineUserArgs().Contains("--itest=gdscript"))
return;
queued = true;
GameLogger.Log("TEST", "GdscriptTest: activating");
(Godot.Engine.GetMainLoop() as SceneTree)?.Root?.CallDeferred("add_child", new GdscriptTestDriverNode());
}
}
/// <summary>
/// Checks the GDScript mod pipeline end to end on the suite's own
/// <c>scripts/*.gd</c>: every script with <c>get_name()</c> + <c>run()</c>
/// is a registered scenario action, and every <c>get_condition_name()</c>
/// script is a working condition — <c>gds.is_rain</c> and
/// <c>gds.flag_equals("victory")</c> are evaluated through the same
/// <see cref="ConditionParser"/> scenarios use, against flags that must
/// make them true and false. Prints <c>[TEST_RESULT] name=gdscript</c>.
/// </summary>
public partial class GdscriptTestDriverNode : Node
{
private const string ResultName = "gdscript";
private const string SuiteModId = "dialog_base__gdscript";
private const double TimeoutSeconds = 60;
private static readonly Regex ReturnedName = new(@"func\s+(get_name|get_condition_name)\s*\(\s*\)\s*:\s*\n\s*return\s+""([^""]+)""", RegexOptions.Compiled);
private int passed;
private int failed;
private double elapsed;
private bool done;
public override void _Process(double delta)
{
if (done)
return;
elapsed += delta;
if (elapsed > TimeoutSeconds)
{
Check("boot finished in time", false);
Finish();
return;
}
if (!Boot.IsReady)
return;
try
{
Run();
}
catch (Exception ex)
{
Check($"no exception ({ex.GetType().Name}: {ex.Message})", false);
}
Finish();
}
private void Run()
{
var actions = new List<string>();
var conditions = new List<string>();
foreach (var file in ModManager.Instance.ListFiles(SuiteModId, "scripts").Where(f => f.EndsWith(".gd", StringComparison.Ordinal)))
{
var text = (ModManager.Instance.ReadFileText(SuiteModId, file) ?? string.Empty).Replace("\r\n", "\n");
foreach (Match match in ReturnedName.Matches(text))
{
if (match.Groups[1].Value == "get_condition_name")
conditions.Add(match.Groups[2].Value);
else if (text.Contains("func run("))
actions.Add(match.Groups[2].Value);
}
}
Check($"suite scripts found ({actions.Count} action(s), {conditions.Count} condition(s))", actions.Count > 0 && conditions.Count > 0);
foreach (var name in actions)
Check($"action '{name}' registered", ScenarioActionFactory.Instance.Get(name) != null);
Expect("gds.is_rain", new() { ["weather"] = "rain" }, true);
Expect("gds.is_rain", new() { ["weather"] = "sun" }, false);
Expect("gds.flag_equals(\"victory\")", new() { ["custom_flag"] = "victory" }, true);
Expect("gds.flag_equals(\"victory\")", new() { ["custom_flag"] = "defeat" }, false);
Expect("gds.is_rain && gds.flag_equals(\"victory\")", new() { ["weather"] = "rain", ["custom_flag"] = "victory" }, true);
}
private void Expect(string condition, Dictionary<string, string> flags, bool expected)
{
var node = ConditionParser.Parse(condition);
var actual = node != null && node.Evaluate(new EvaluationContext { Flags = flags });
Check($"'{condition}' with {string.Join(",", flags.Select(f => $"{f.Key}={f.Value}"))} → {expected}" + (actual == expected ? string.Empty : $" (got {actual})"), actual == expected);
}
private void Check(string what, bool ok)
{
if (ok)
passed++;
else
failed++;
GameLogger.Log("TEST", $" [{(ok ? "PASS" : "FAIL")}] {what}");
}
private void Finish()
{
done = true;
GameLogger.Log("TEST", $"[TEST_RESULT] name={ResultName} passed={passed} failed={failed}");
GetTree().Quit(failed == 0 ? 0 : 1);
}
}
}