Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 16 additions & 2 deletions docs/COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
65 changes: 61 additions & 4 deletions docs/VALIDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)

Expand Down Expand Up @@ -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.
Expand All @@ -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

Expand Down
4 changes: 2 additions & 2 deletions src/GuildChest/InventoryCodec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ private static List<Entry> 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<Entry>();
Expand All @@ -62,7 +62,7 @@ private static List<Entry> 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) });
Expand Down
28 changes: 28 additions & 0 deletions tests/GuildChest.NativeSmoke/AdapterBindingChecks.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using HarmonyLib;
Expand All @@ -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<string, HashSet<string>>)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<string, List<string>> { ["exclude"] = new() { "Cookables" } };
yaml.SetValue(null, new Dictionary<string, Dictionary<string, List<string>>> { [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;
Expand Down
48 changes: 48 additions & 0 deletions tests/GuildChest.NativeSmoke/InventoryCodecChecks.cs
Original file line number Diff line number Diff line change
@@ -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");
}
}
5 changes: 5 additions & 0 deletions tests/GuildChest.NativeSmoke/NativeSmokeHarness.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<ItemDrop>().m_itemData.Clone();
ore.m_dropPrefab = ObjectDB.instance.GetItemPrefab("Iron");
Expand Down Expand Up @@ -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;
Expand Down