diff --git a/items/test_patches.json b/items/test_patches.json new file mode 100644 index 0000000..e57afe4 --- /dev/null +++ b/items/test_patches.json @@ -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" + } + ] +} diff --git a/mod.jpg b/mod.jpg index 7f6ac09..ed52d07 100644 Binary files a/mod.jpg and b/mod.jpg differ diff --git a/patches/item.json b/patches/item.json index d8dbace..6020eac 100644 --- a/patches/item.json +++ b/patches/item.json @@ -9,6 +9,26 @@ "Id": "*", "Durability": 50, "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 } ] } diff --git a/patches/item/explain.json b/patches/item/explain.json new file mode 100644 index 0000000..4a4dd93 --- /dev/null +++ b/patches/item/explain.json @@ -0,0 +1,8 @@ +{ + "Items": [ + { + "Id": "test_patches/child", + "Description": "items/food/wolf_meat/desc" + } + ] +} diff --git a/src/PropertyPatchesTest.cs b/src/PropertyPatchesTest.cs index 2d627ff..3fbabde 100644 --- a/src/PropertyPatchesTest.cs +++ b/src/PropertyPatchesTest.cs @@ -38,7 +38,9 @@ namespace Mods.TestDefaultsAndPatches /// 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. + /// survive; patch ids match case-insensitively, a broken patch is skipped + /// alone, $delete removes an entity, Extends inherits from a + /// parent resolved with its own patches and a cycle skips its members. Prints [TEST_RESULT] name=property_patches and quits. /// public partial class PropertyPatchesTestDriverNode : Node { @@ -113,9 +115,67 @@ namespace Mods.TestDefaultsAndPatches Check("food/ration resolved", false); if (items.TryGetValue("food/wolf_meat", out var meat)) + { 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 - 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(); + } + + /// + /// EntityResolver.Explain on an Extends chain with patches on the parent, the + /// child (a second file) and a wildcard: every field names its layer, entity, mod and file. + /// + 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(); + 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() == 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 properties, string key, string expected, string what)