diff --git a/README.md b/README.md index 21a3ec8..bc1d4ea 100644 --- a/README.md +++ b/README.md @@ -96,8 +96,8 @@ an integration's own range can restrict it further. Normal chest interaction keeps its usual range. Capacity and construction costs are fixed. Optional adapters were tested in the native fixture with **AzuAutoStore 3.1.7** -and **AzuCraftyBoxes 1.8.27**. Neither is required or bundled. AutoStore supports -player hotkey/single-item deposits; CraftyBoxes supports nearby resource previews, +and **AzuCraftyBoxes 1.8.27 and 1.8.28**. Neither is required or bundled. AutoStore +supports player hotkey/single-item deposits; CraftyBoxes supports nearby resource previews, crafting supply, and building after acknowledged transfers. AutoStore ground-drop automation and CraftyBoxes fuel/ore feeding and Epic Loot consumers exclude guild storage. Other storage mods need explicit integration. See diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md index b2f4b34..180cfa0 100644 --- a/docs/COMPATIBILITY.md +++ b/docs/COMPATIBILITY.md @@ -43,6 +43,13 @@ If a sent transfer loses its acknowledgement during shutdown, that reason explicitly says the host may already have accepted it; world and character saves still do not form one atomic transaction. +The current source accepts inventory formats **109 and 110**. Valheim 1.0.20 +writes format 110; builds before this compatibility fix reject those snapshots +with `Unsupported inventory format 110`, preventing access and transfers. +Existing format-109 guild contents still load, and Valheim's native item loader +handles the version-specific metadata. Uninspected inventory formats remain +blocked with their saved bytes retained. + ## AzuAutoStore The adapter targets **AzuAutoStore 3.1.7**, obtained from its author's Hexium @@ -70,7 +77,14 @@ continues to work for ordinary containers. ## AzuCraftyBoxes -The adapter targets **AzuCraftyBoxes 1.8.27** from the author's Hexium release. +The adapter supports **AzuCraftyBoxes 1.8.27 and 1.8.28** from the author's +Hexium releases. Inspection of both release DLLs found no changes to the +container wrappers, nearby queries, count caches, crafting/building patches or +pull-permission methods used by Guild Chest. Version 1.8.28 adds the `Cookables` +filter group; the existing adapter delegates pull restrictions to CraftyBoxes, +so no adapter change is needed for that group. The separate inventory-format +fix described above is required when running on Valheim 1.0.20. + Nearby guild contents are polled for recipe/resource previews and counted once, even with several nearby guild chests. The adapter respects the mod's container selection, pull restrictions and Leave One setting. @@ -171,7 +185,7 @@ multiplayer checks. ## Upstream releases inspected - [AzuAutoStore 3.1.7](https://valheim.hexium.gg/mods/Azumatt/AzuAutoStore) -- [AzuCraftyBoxes 1.8.27](https://valheim.hexium.gg/mods/Azumatt/AzuCraftyBoxes) +- [AzuCraftyBoxes 1.8.27 and 1.8.28](https://valheim.hexium.gg/mods/Azumatt/AzuCraftyBoxes/versions) Optional hooks use reflection; neither third-party DLL is bundled or required. Each adapter resolves and validates its required bindings before installing any diff --git a/docs/VALIDATION.md b/docs/VALIDATION.md index 39ca31c..62f6933 100644 --- a/docs/VALIDATION.md +++ b/docs/VALIDATION.md @@ -11,7 +11,8 @@ The user confirmed on **2026-10-17** that Guild Chest works correctly on the latest server version, **Valheim 1.0.20**. Historical native fixture results below use **Valheim 1.0.16**, Steam build **25527701**, obtained on 2026-10-05. Automated unit tests do not load Valheim, and compiling the native fixture does -not execute it; the fixture has not been rerun on 1.0.20. +not execute it. The AzuCraftyBoxes 1.8.28 check below separately runs the native +fixture on 1.0.20. ## Server reference verification (2026-10-17) @@ -29,8 +30,55 @@ not execute it; the fixture has not been rerun on 1.0.20. references. Product builds have no warnings or errors. The native fixture restore/build reports **NU1900** because NuGet vulnerability metadata could not be fetched (`Permission denied (api.nuget.org:443)`); compilation succeeds. - No automated native fixture was run on 1.0.20; live gameplay is confirmed by - the user above. + That reference-verification command did not execute the native fixture; + the later 1.8.28 runtime result is recorded below. + +## AzuCraftyBoxes 1.8.28 compatibility (2026-10-17) + +- Downloaded the author's [1.8.28 Hexium release](https://valheim.hexium.gg/mods/Azumatt/AzuCraftyBoxes/versions) + and compared its decompiled DLL with the previously inspected 1.8.27 release. + The new `Cookables` group and version metadata account for the code changes; + container wrappers, nearby queries, count caches, crafting/building patches + and pull-permission methods are unchanged. The release ZIP has SHA-256 + `e21292c883168145b768faf462de7ff4856a380ca38e9906621636ab658959af`; + its DLL has SHA-256 + `381883fe4625d1dd119bb425a2f3c7522457fbc08ae1db60159f8256de940d6e`. +- The unmodified Guild Chest 1.1.0 source successfully installs the 1.8.28 + adapter on Valheim 1.0.20, but the native transport fixture fails before + reaching its CraftyBoxes checks: `Unsupported inventory format 110`. + Compilation and the normal unit tests did not detect this runtime issue. +- The compatibility fix accepts formats 109 and 110 and passes the recorded + version to Valheim's native item loader. Inspection confirms both formats + retain the record layout used by the unresolved-item guard; format 110 + interprets the existing trailing cheat flag. Native regression checks cover + current-format round trips, legacy-109 contents and reserialization, cheat + metadata, unchanged client-only items, and rejection of formats 108 and 111. +- `dotnet run --project build -- --target Test Compile Package NativeSmoke` + passes **15 C#** and **162 Python** tests, compiles Debug and Release, creates + the local test ZIP, and compiles the native fixture. Product builds have no + warnings or errors; the native fixture reports the existing NuGet **NU1900** + vulnerability-metadata warning described above. +- With the fix, the full native compatibility fixture passes on **Valheim + 1.0.20**, Steam build **25909431**, with **AzuCraftyBoxes 1.8.28**, + **AzuAutoStore 3.1.7**, **BepInExPack 5.4.2351** and **Jötunn 2.30.2** in an + isolated disposable world. It emits `NATIVE_SMOKE_INVENTORY_FORMATS_PASSED`, + `NATIVE_SMOKE_CRAFTY_COOKABLES_PASSED` and `NATIVE_SMOKE_COMPAT_PASSED`, exits + with code zero, and creates the success marker without a failure marker. + Passing paths include normal chest transfers, group exclusions and individual + overrides, shared previews, crafting, ingredient-quality and batch rules, + Leave One, AutoStore continuation, delayed/stale preview replies, transaction + fault injection, and single-click portal placement/cancellation. This is a + scripted host fixture with a synthetic player; it does not exercise a remote + client or interactive UI. Logs and the marker are retained locally in + `.local/compat/crafty-1.8.28/native-fixed/`. +- The user confirmed on **2026-10-17** that + `artifacts/GuildChest-1.1.0-compat-test.zip` works in game with + **AzuCraftyBoxes 1.8.28**, completing the requested final compatibility check. + The tested ZIP has SHA-256 + `e137f968b0fd8d73f53417432d9e4b20e774df05b1c8ebc063442c90db5e8703`. + This unpublished test build retains version **1.1.0** and includes the + inventory-format fix. Individual gameplay checklist results below remain + limited to the scenarios separately documented. ## Hexium publication (2026-10-17) @@ -451,7 +499,7 @@ libraries, including PulseAudio libraries; these are not part of the development dev container. Run BepInEx with the normal server loader for your platform. For the storage-mod fixture, install the author's **AzuAutoStore 3.1.7** and -**AzuCraftyBoxes 1.8.27** DLLs in that isolated installation, and additionally set +**AzuCraftyBoxes 1.8.28** DLLs in that isolated installation, and additionally set `GUILDCHEST_SMOKE_COMPAT=1`. This adds a 0.5-second commit-reply delay to exercise the pending-inventory guards. The storage regressions additionally delay open replies by 0.5 seconds, preview replies by 1.2 seconds, and drop one preview. @@ -470,6 +518,15 @@ The adapter fixture checks nested staged inventory restoration and required-hook validation before the compatibility flows. It emits `NATIVE_SMOKE_ADAPTER_BINDINGS_PASSED`. Its deliberate missing-prefix lookup produces a Harmony warning; no partial hooks may remain after that rejection. +On CraftyBoxes 1.8.28 or newer, the fixture also checks `Cookables` membership, +group exclusions and individual overrides through the Guild Chest adapter, +emitting `NATIVE_SMOKE_CRAFTY_COOKABLES_PASSED`. Earlier 1.8.27 fixture runs skip +this group check. + +Before the transport checks, `NATIVE_SMOKE_INVENTORY_FORMATS_PASSED` verifies +native inventory decoding, legacy format-109 contents, format-110 cheat metadata +when supported by the game, and rejection of uninspected formats. The unresolved +player-prefab checks also reject changes to the serialized cheat flag. ## Automated and container checks diff --git a/src/GuildChest/InventoryCodec.cs b/src/GuildChest/InventoryCodec.cs index 2296025..34da3b0 100644 --- a/src/GuildChest/InventoryCodec.cs +++ b/src/GuildChest/InventoryCodec.cs @@ -47,7 +47,7 @@ private static List ReadEntries(byte[] bytes, int width, int height) if (bytes.Length > MaxBytes) throw new InvalidOperationException("Inventory exceeds the supported package size."); var package = new ZPackage(bytes); int version = package.ReadInt(); - if (version != 109) throw new InvalidOperationException($"Unsupported inventory format {version}; saved data has been retained."); + if (version != 109 && version != 110) throw new InvalidOperationException($"Unsupported inventory format {version}; saved data has been retained."); int count = package.ReadUShort(); if (count > width * height) throw new InvalidOperationException("Too many inventory entries."); var result = new List(); @@ -62,7 +62,7 @@ private static List ReadEntries(byte[] bytes, int width, int height) throw new InvalidOperationException($"Invalid item prefab {hash}: stack {item.m_stack}, quality {item.m_quality}, slot ({item.m_gridPos.x}, {item.m_gridPos.y}) in {width} x {height} inventory, or duplicate slot."); var encoded = new byte[package.GetPos() - start]; Array.Copy(bytes, start, encoded, 0, encoded.Length); - // Format 109 starts with durability (4 bytes), then grid X/Y. Allow local + // Formats 109 and 110 start with durability (4 bytes), then grid X/Y. Allow local // rearrangement while preserving every other byte of an unresolved item. encoded[4] = 0; encoded[5] = 0; result.Add(new Entry { Hash = hash, Item = item, UnchangedKey = Convert.ToBase64String(encoded) }); diff --git a/tests/GuildChest.NativeSmoke/AdapterBindingChecks.cs b/tests/GuildChest.NativeSmoke/AdapterBindingChecks.cs index c961930..8a1b629 100644 --- a/tests/GuildChest.NativeSmoke/AdapterBindingChecks.cs +++ b/tests/GuildChest.NativeSmoke/AdapterBindingChecks.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; using HarmonyLib; @@ -10,6 +11,33 @@ public sealed partial class NativeSmokeHarness private static int BindingTarget() => 7; private static bool BindingPrefix(ref int __result) { __result = 9; return false; } + private void VerifyCraftyCookables(Container chest) + { + var info = BepInEx.Bootstrap.Chainloader.PluginInfos["Azumatt.AzuCraftyBoxes"]; + if (info.Metadata.Version < new System.Version(1, 8, 28)) return; + var plugin = info.Instance.GetType(); + var groups = (Dictionary>)AccessTools.Field(plugin, "groups").GetValue(null); + Check(groups.TryGetValue("Cookables", out var cookables) && cookables.Contains("NeckTail") && cookables.Contains("SerpentMeat"), + "CraftyBoxes registers raw ingredients in Cookables"); + var wrapperType = plugin.Assembly.GetType("AzuCraftyBoxes.IContainers.VanillaContainer")!; + var wrapper = AccessTools.Method(wrapperType, "Create").Invoke(null, new object[] { chest }); + var prefab = (string)AccessTools.Method(wrapperType, "GetPrefabName").Invoke(wrapper, null); + var pullable = AccessTools.Method(typeof(GuildChest.Plugin).Assembly.GetType("GuildChest.CraftyBoxesAdapter"), "Pullable"); + bool CanPull(string item) => (bool)pullable.Invoke(null, new[] { wrapper, item }); + var yaml = AccessTools.Field(plugin, "yamlData"); + var original = yaml.GetValue(null); + try + { + var rules = new Dictionary> { ["exclude"] = new() { "Cookables" } }; + yaml.SetValue(null, new Dictionary>> { [prefab] = rules }); + Check(!CanPull("NeckTail") && !CanPull("SerpentMeat") && CanPull("Stone"), "Guild adapter honors Cookables exclusion"); + rules["includeOverride"] = new() { "NeckTail" }; + Check(CanPull("NeckTail") && !CanPull("SerpentMeat"), "Guild adapter honors individual Cookables override"); + } + finally { yaml.SetValue(null, original); } + Logger.LogInfo("NATIVE_SMOKE_CRAFTY_COOKABLES_PASSED"); + } + private void VerifyAdapterBindings(Player player, Container chest, Container second) { var assembly = typeof(GuildChest.Plugin).Assembly; diff --git a/tests/GuildChest.NativeSmoke/InventoryCodecChecks.cs b/tests/GuildChest.NativeSmoke/InventoryCodecChecks.cs new file mode 100644 index 0000000..8bf9f30 --- /dev/null +++ b/tests/GuildChest.NativeSmoke/InventoryCodecChecks.cs @@ -0,0 +1,48 @@ +using System; +using System.Reflection; + +namespace GuildChest.NativeSmoke; + +public sealed partial class NativeSmokeHarness +{ + private void VerifyInventoryFormats() + { + var inventory = CreateFixtureInventory(); + var current = Encode(inventory); + int version = new ZPackage(current).ReadInt(); + Check(version == 109 || version == 110, "Native inventory writes an inspected format"); + ValidateItems(Decode(current)); + + // Both inspected formats have the same record layout. Valheim 1.0.20 + // starts reading the existing trailing cheat flag only in format 110. + var legacy = new ZPackage(); + legacy.Write(109); + legacy.Write((ushort)inventory.NrOfItems()); + foreach (var item in inventory.GetAllItems()) item.Save(legacy); + var restored = Decode(legacy.GetArray()); + ValidateItems(restored); + ValidateItems(Decode(Encode(restored))); + + if (version == 110) + { + inventory.GetItemAt(1, 0).m_cheated = true; + Check(Decode(Encode(inventory)).GetItemAt(1, 0).m_cheated, "Format 110 retains the native cheat flag"); + } + + foreach (int unsupported in new[] { 108, 111 }) + { + var package = new ZPackage(); + package.Write(unsupported); + package.Write((ushort)0); + bool rejected = false; + try { Decode(package.GetArray()); } + catch (TargetInvocationException exception) + { + rejected = exception.InnerException is InvalidOperationException && + exception.InnerException.Message.Contains($"Unsupported inventory format {unsupported}"); + } + Check(rejected, $"Uninspected inventory format {unsupported} is rejected"); + } + Logger.LogInfo("NATIVE_SMOKE_INVENTORY_FORMATS_PASSED"); + } +} diff --git a/tests/GuildChest.NativeSmoke/NativeSmokeHarness.cs b/tests/GuildChest.NativeSmoke/NativeSmokeHarness.cs index 77b2571..3a41387 100644 --- a/tests/GuildChest.NativeSmoke/NativeSmokeHarness.cs +++ b/tests/GuildChest.NativeSmoke/NativeSmokeHarness.cs @@ -224,6 +224,7 @@ void Select(InventoryGrid grid, ItemDrop.ItemData item, Vector2i position, Inven Player player; Container chest; try { + VerifyInventoryFormats(); var position = new Vector3(0, 1000, 0); // Prevent the headless Game.Update spawn lifecycle from replacing or // teleporting the synthetic player once world generation completes. @@ -346,6 +347,7 @@ private IEnumerator RunCompatibility(Player player, Container chest) AccessTools.Method(crafty.GetType("AzuCraftyBoxes.Util.Functions.Boxes"), "AddContainer").Invoke(null, new object[] { chest }); AccessTools.Method(crafty.GetType("AzuCraftyBoxes.Util.Functions.Boxes"), "AddContainer").Invoke(null, new object[] { second }); VerifyAdapterBindings(player, chest, second); + VerifyCraftyCookables(chest); Check(chest.GetInventory().GetEmptySlots() == 0, "Unadapted guild inventory exposes no writable capacity"); var ore = ObjectDB.instance.GetItemPrefab("Iron").GetComponent().m_itemData.Clone(); ore.m_dropPrefab = ObjectDB.instance.GetItemPrefab("Iron"); @@ -642,6 +644,9 @@ private void ValidateUnresolvedItems(Inventory player) unknown.m_customData["client-only"] = "changed"; Check(!Conserves(empty, player, empty, after), "Unknown item metadata mutation rejected"); unknown.m_customData["client-only"] = "preserved"; + unknown.m_cheated = !unknown.m_cheated; + Check(!Conserves(empty, player, empty, after), "Unknown item cheat flag mutation rejected"); + unknown.m_cheated = !unknown.m_cheated; unknown.m_stack = 2; Check(!Conserves(empty, player, empty, after), "Unknown item quantity mutation rejected"); bool rejected = false;