Update test_defaults_and_patches implementation, assets and documentation

This commit is contained in:
cthangover committed 2026-10-31 15:28:57 +03:00
1 parent 039c68d448
commit a6f790fb4d
5 files changed
+126 -2

No files matched your search

+36
View File
@@ -0,0 +1,36 @@
{
"Kind": "item",
"Items": [
{
"Id": "test_patches/base",
"Name": "items/food/ration/name",
"Description": "items/food/ration/desc",
"Cost": 10,
"Sprite": "food/ration",
"ItemType": "Food",
"Shelf": {"Row": 1, "Col": 2}
},
{
"Id": "test_patches/child",
"Extends": "test_patches/base",
"Cost": 20,
"Shelf": {"Col": 5}
},
{
"Id": "test_patches/doomed",
"Name": "items/food/ration/name",
"Sprite": "food/ration",
"ItemType": "Food"
},
{
"Id": "test_patches/loop_a",
"Extends": "test_patches/loop_b",
"Sprite": "food/ration"
},
{
"Id": "test_patches/loop_b",
"Extends": "test_patches/loop_a",
"Sprite": "food/ration"
}
]
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 142 KiB

+20
View File
@@ -9,6 +9,26 @@
"Id": "*", "Id": "*",
"Durability": 50, "Durability": 50,
"GlobalItemProp": "patched" "GlobalItemProp": "patched"
},
{
"Id": "FOOD/WOLF_MEAT",
"Cost": 77
},
{
"Id": "food/wolf_meat",
"Cost": "many"
},
{
"Id": "food/wolf_meat",
"Name$add": ["broken"]
},
{
"Id": "test_patches/base",
"Sprite": "food/wolf_meat"
},
{
"Id": "test_patches/doomed",
"$delete": true
} }
] ]
} }
+8
View File
@@ -0,0 +1,8 @@
{
"Items": [
{
"Id": "test_patches/child",
"Description": "items/food/wolf_meat/desc"
}
]
}
+62 -2
View File
@@ -38,7 +38,9 @@ namespace Mods.TestDefaultsAndPatches
/// <c>patches/item.json</c> into the real entities of other mods, as the /// <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 /// game resolves them: a targeted patch wins over the wildcard, the
/// wildcard reaches every entity of the kind, and untouched typed fields /// wildcard reaches every entity of the kind, and untouched typed fields
/// survive. Prints <c>[TEST_RESULT] name=property_patches</c> and quits. /// survive; patch ids match case-insensitively, a broken patch is skipped
/// alone, <c>$delete</c> removes an entity, <c>Extends</c> inherits from a
/// parent resolved with its own patches and a cycle skips its members. Prints <c>[TEST_RESULT] name=property_patches</c> and quits.
/// </summary> /// </summary>
public partial class PropertyPatchesTestDriverNode : Node public partial class PropertyPatchesTestDriverNode : Node
{ {
@@ -113,9 +115,67 @@ namespace Mods.TestDefaultsAndPatches
Check("food/ration resolved", false); Check("food/ration resolved", false);
if (items.TryGetValue("food/wolf_meat", out var meat)) if (items.TryGetValue("food/wolf_meat", out var meat))
{
Expect(meat.ExtraProperties, "Durability", "50", "wolf_meat: wildcard value"); Expect(meat.ExtraProperties, "Durability", "50", "wolf_meat: wildcard value");
Check($"wolf_meat: patch id is case-insensitive (FOOD/WOLF_MEAT → Cost 77, got {meat.Cost})", meat.Cost == 77);
Check("wolf_meat: broken patches (Cost \"many\", Name$add) are skipped, the item keeps its name",
meat.Name == "items/food/wolf_meat/name");
}
else else
Check("food/wolf_meat resolved", false); Check("food/wolf_meat resolved (a broken patch must not drop the kind)", false);
Check("$delete removes test_patches/doomed", !items.ContainsKey("test_patches/doomed"));
Check("an Extends cycle skips both entities", !items.ContainsKey("test_patches/loop_a") && !items.ContainsKey("test_patches/loop_b"));
if (items.TryGetValue("test_patches/child", out var child))
{
Check("Extends: child inherits Name", child.Name == "items/food/ration/name");
Check($"Extends: child's own Cost wins (got {child.Cost})", child.Cost == 20);
Check($"Extends: the parent is resolved with its patches first (Sprite {child.Sprite})", child.Sprite == "food/wolf_meat");
var shelf = child.ExtraProperties != null && child.ExtraProperties.TryGetValue("Shelf", out var value) && value is JsonElement element
&& element.ValueKind == JsonValueKind.Object ? element : default;
Check($"Extends: nested objects deep-merge (Shelf {(shelf.ValueKind == JsonValueKind.Object ? shelf.GetRawText() : "missing")})",
shelf.ValueKind == JsonValueKind.Object && shelf.TryGetProperty("Row", out var row) && row.GetInt32() == 1
&& shelf.TryGetProperty("Col", out var col) && col.GetInt32() == 5);
Expect(child.ExtraProperties, "Durability", "50", "child: wildcard patch applies after Extends");
}
else
Check("test_patches/child resolved", false);
ExplainChild();
}
/// <summary>
/// <c>EntityResolver.Explain</c> on an <c>Extends</c> chain with patches on the parent, the
/// child (a second file) and a wildcard: every field names its layer, entity, mod and file.
/// </summary>
private void ExplainChild()
{
const string mod = "test_defaults_and_patches";
const string data = "items/test_patches.json";
var resolver = Cthangover.Core.Mods.ModManager.Instance.GetResolver<EntityResolver>();
var explanation = resolver?.Explain("item", "test_patches/child");
Check("Explain(item, test_patches/child) returns a value", explanation?.Value != null);
if (explanation?.Value == null)
return;
Check($"Explain value matches Collect (Cost {explanation.Value["Cost"]})", explanation.Value["Cost"]?.GetValue<int>() == 20);
ExpectOrigin(explanation, "Name", EntityLayer.Definition, "test_patches/base", mod, data);
ExpectOrigin(explanation, "Cost", EntityLayer.Definition, "test_patches/child", mod, data);
ExpectOrigin(explanation, "Shelf.Row", EntityLayer.Definition, "test_patches/base", mod, data);
ExpectOrigin(explanation, "Shelf.Col", EntityLayer.Definition, "test_patches/child", mod, data);
ExpectOrigin(explanation, "Sprite", EntityLayer.Patch, "test_patches/base", mod, "patches/item.json");
ExpectOrigin(explanation, "Description", EntityLayer.Patch, "test_patches/child", mod, "patches/item/explain.json");
ExpectOrigin(explanation, "Durability", EntityLayer.Patch, "*", mod, "patches/item.json");
Check("Explain of a $delete-d entity says Deleted", resolver.Explain("item", "test_patches/doomed")?.Deleted == true);
Check("Explain of an Extends cycle member is null", resolver.Explain("item", "test_patches/loop_a") == null);
}
private void ExpectOrigin(EntityExplanation explanation, string path, EntityLayer layer, string entity, string mod, string file)
{
explanation.Fields.TryGetValue(path, out var origin);
var actual = origin == null ? "default" : $"{origin.Layer} {origin.EntityId} {origin.ModId}/{origin.File}";
Check($"Explain {path}: {layer} {entity} {mod}/{file} (got {actual})",
origin != null && origin.Layer == layer && origin.EntityId == entity && origin.ModId == mod && origin.File == file);
} }
private void Expect(Dictionary<string, object> properties, string key, string expected, string what) private void Expect(Dictionary<string, object> properties, string key, string expected, string what)