From 33a9f681a6c5eb746da70829614ba7d2578e09da Mon Sep 17 00:00:00 2001 From: iisaev Date: Sat, 17 Oct 2026 14:28:02 +0000 Subject: [PATCH] Mark as a test fixture with its own property_patches itest - "fixture": true keeps the mod out of --all-mods and every game, so its wildcard patches of all characters and items no longer leak into unrelated tests; it loads only with --with-mods=test_defaults_and_patches. - PropertyPatchesTest (--itest=property_patches) checks the merged entities; the manifest declares the sources and every mod whose entities the patches touch. --- manifest.json | 14 +++- src/PropertyPatchesTest.cs | 152 +++++++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 src/PropertyPatchesTest.cs diff --git a/manifest.json b/manifest.json index e830533..1096ae3 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,17 @@ "name": "Test Defaults and Patches", "version": "1.0.0", "author": "ct", - "description": "Test mod demonstrating property patches system with wildcard and targeted patches", + "description": "Test fixture of the property patches system: a targeted patch (Marao, food/ration) and wildcard patches of every character and item. Marked as a fixture, so --all-mods and games never load it; the property_patches itest loads it explicitly (--with-mods=test_defaults_and_patches --itest=property_patches) and checks the merged entities.", "category": "other", - "depends": ["visual_novel_core>=1"] + "depends": [ + "visual_novel_core>=1", + "murakami>=1", + "cooking>=1", + "characters_base>=1", + "items_base>=1" + ], + "fixture": true, + "sources": [ + "src/*.cs" + ] } diff --git a/src/PropertyPatchesTest.cs b/src/PropertyPatchesTest.cs new file mode 100644 index 0000000..011bd14 --- /dev/null +++ b/src/PropertyPatchesTest.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using Cthangover.Core.Bootstrap; +using Cthangover.Core.Characters; +using Cthangover.Core.Items; +using Cthangover.Core.Mods; +using Cthangover.Core.Mods.Resolvers; +using Cthangover.Core.Utils; +using Godot; + +namespace Mods.TestDefaultsAndPatches +{ + /// + /// Entry point of the property patches itest: active only with + /// --itest=property_patches. The fixture itself is loaded only by + /// --with-mods=test_defaults_and_patches (manifest fixture). + /// + public class PropertyPatchesTestInitializer : IModInitializer + { + private static bool queued; + + public void OnModLoaded(string modId) { } + + public void OnModResourcesReady() + { + if (queued || !OS.GetCmdlineUserArgs().Contains("--itest=property_patches")) + return; + queued = true; + GameLogger.Log("TEST", "PropertyPatchesTest: activating"); + (Godot.Engine.GetMainLoop() as SceneTree)?.Root?.CallDeferred("add_child", new PropertyPatchesTestDriverNode()); + } + } + + /// + /// Checks the merge of patches/character.json and + /// patches/item.json into the real entities of other mods, as the + /// game resolves them: a targeted patch wins over the wildcard, the + /// wildcard reaches every entity of the kind, and untouched typed fields + /// survive. Prints [TEST_RESULT] name=property_patches and quits. + /// + public partial class PropertyPatchesTestDriverNode : Node + { + private const string ResultName = "property_patches"; + private const double TimeoutSeconds = 60; + + 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 characterPatches = Patches.Collect("character"); + Check("character patches collected (Marao + *)", characterPatches != null && characterPatches.ContainsKey("Marao") && characterPatches.ContainsKey("*")); + var itemPatches = Patches.Collect("item"); + Check("item patches collected (food/ration + *)", itemPatches != null && itemPatches.ContainsKey("food/ration") && itemPatches.ContainsKey("*")); + + var characters = Entities.Collect(); + Check("Marao resolved", characters.ContainsKey("Marao")); + if (characters.TryGetValue("Marao", out var marao)) + { + Expect(marao.ExtraProperties, "TestPatched", "42", "Marao: targeted value"); + Expect(marao.ExtraProperties, "TestDefault", "100", "Marao: targeted beats wildcard"); + Expect(marao.ExtraProperties, "TestGlobalPatch", "1", "Marao: wildcard-only key"); + } + + var other = characters.Values.FirstOrDefault(c => c.ID != "Marao"); + Check("another character exists", other != null); + if (other != null) + { + Expect(other.ExtraProperties, "TestDefault", "-20", $"{other.ID}: wildcard value"); + Check($"{other.ID}: no targeted key", other.ExtraProperties == null || !other.ExtraProperties.ContainsKey("TestPatched")); + } + + var items = Entities.Collect(); + if (items.TryGetValue("food/ration", out var ration)) + { + Expect(ration.ExtraProperties, "Durability", "99", "ration: targeted beats wildcard"); + Expect(ration.ExtraProperties, "SpecialProp", "true", "ration: targeted-only key"); + Expect(ration.ExtraProperties, "GlobalItemProp", "patched", "ration: wildcard-only key"); + Check("ration: typed field survives the merge", !string.IsNullOrEmpty(ration.Name)); + } + else + Check("food/ration resolved", false); + + if (items.TryGetValue("food/wolf_meat", out var meat)) + Expect(meat.ExtraProperties, "Durability", "50", "wolf_meat: wildcard value"); + else + Check("food/wolf_meat resolved", false); + } + + private void Expect(Dictionary properties, string key, string expected, string what) + { + var actual = properties != null && properties.TryGetValue(key, out var value) ? Text(value) : null; + Check($"{what}: {key}={expected}" + (actual == expected ? string.Empty : $" (got {actual ?? "nothing"})"), actual == expected); + } + + private static string Text(object value) => value switch + { + JsonElement { ValueKind: JsonValueKind.String } s => s.GetString(), + JsonElement e => e.GetRawText(), + bool b => b ? "true" : "false", + null => null, + _ => Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture), + }; + + 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); + } + } +}