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.
This commit is contained in:
cthangover committed 2026-10-17 14:28:02 +00:00
1 parent 9fff3539c8
commit 33a9f681a6
2 files changed
+164 -2

No files matched your search

+12 -2
View File
@@ -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"
]
}
+152
View File
@@ -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
{
/// <summary>
/// Entry point of the property patches itest: active only with
/// <c>--itest=property_patches</c>. The fixture itself is loaded only by
/// <c>--with-mods=test_defaults_and_patches</c> (manifest <c>fixture</c>).
/// </summary>
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());
}
}
/// <summary>
/// Checks the merge of <c>patches/character.json</c> and
/// <c>patches/item.json</c> 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 <c>[TEST_RESULT] name=property_patches</c> and quits.
/// </summary>
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<CharacterInfo>();
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<ItemInfo>();
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<string, object> 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);
}
}
}