From 50a173ef252a6708ee0a891fb00ebb5b0da5b068 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 25 Sep 2026 12:58:43 +0200 Subject: [PATCH] Build against plugin API 0.1.19-mm.1; item header words from the host The contract now reports each object's description header, so the item record carries the words the original's framework filed from it: the weenie and second header flags, the physics description flags, the setup id, the scale key (0.0 whenever a scale was sent, as recorded), the hook type, the parent and parent location, and the framework's own summary word (key 218103831: stackable, has a position, lockable, inscribable, usable, container), derived from the header by a rule that matches all 2,220 recorded items carrying both words. With the header flags known, key presence follows the header exactly instead of guessing from non-zero values: a recorded Pyreal Mote now rebuilds key for key. Co-Authored-By: Claude Opus 5.5 (1M context) --- .gitignore | 1 + Directory.Build.props | 2 +- .../Inventory/ItemHeaderSource.cs | 52 +++++++++++++- src/OpenAC.MosswartMassacre/plugin.json | 2 +- .../Inventory/RecordedItemTests.cs | 69 +++++++++++++++++++ 5 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 tests/OpenAC.MosswartMassacre.Tests/Inventory/RecordedItemTests.cs diff --git a/.gitignore b/.gitignore index 4a897d1..c875e6d 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ Thumbs.db # Frame recordings from live runs *.jsonl +packages-staging/ diff --git a/Directory.Build.props b/Directory.Build.props index 5f5ccc0..1f1ab3d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -15,7 +15,7 @@ - 0.1.19-mmpreview.1 + 0.1.19-mm.1 false diff --git a/src/OpenAC.MosswartMassacre/Inventory/ItemHeaderSource.cs b/src/OpenAC.MosswartMassacre/Inventory/ItemHeaderSource.cs index 329374a..801a855 100644 --- a/src/OpenAC.MosswartMassacre/Inventory/ItemHeaderSource.cs +++ b/src/OpenAC.MosswartMassacre/Inventory/ItemHeaderSource.cs @@ -61,8 +61,9 @@ internal interface IItemHeaderSource } /// -/// The contract this plugin builds against exposes none of these words, so -/// every one is absent. Replace this when the contract grows them. +/// The header words from the object description the host kept for the item. +/// An item without one (built from a vendor listing, say) has none, and every +/// word is then absent. /// internal sealed class HostItemHeaderSource : IItemHeaderSource { @@ -72,5 +73,50 @@ internal sealed class HostItemHeaderSource : IItemHeaderSource { } - public ItemHeaderWords Read(in PluginInventoryItem item) => default; + public ItemHeaderWords Read(in PluginInventoryItem item) + { + if (item.Header is not { } header) + return default; + return new ItemHeaderWords + { + WeenieHeaderFlags = header.WeenieHeaderFlags, + WeenieHeaderFlags2 = header.WeenieHeaderFlags2, + PhysicsDescriptionFlags = header.PhysicsDescriptionFlags, + PhysicsWord = header.WeenieHeaderFlags is uint weenie + ? FrameworkFlags(weenie, header.PhysicsDescriptionFlags ?? 0u, item.ItemType, header.ObjectDescriptionFlags ?? item.PublicFlags) + : null, + SetupId = header.SetupId, + // The framework filed 0.0 under its scale key whenever the + // physics description carried a scale, whatever the scale was. + Scale = header.Scale is null ? null : 0.0, + ParentObjectId = header.ParentObjectId, + ParentLocation = header.ParentLocation is uint location ? unchecked((int)location) : null, + HookType = header.HookType is uint hook ? unchecked((int)hook) : null, + }; + } + + /// + /// The framework's own summary word it kept per object (key 218103831), + /// derived from the header: stackable, has a position, lockable, + /// inscribable, usable, a container. Checked against every recorded item + /// that carries both this word and the header flags. + /// + internal static uint FrameworkFlags(uint weenieFlags, uint physicsFlags, uint itemType, uint descriptionFlags) + { + uint flags = 0u; + if ((weenieFlags & 0x3000u) != 0u) + flags |= 0x01u; // stack size or maximum stack size sent + if ((physicsFlags & 0x8000u) != 0u) + flags |= 0x02u; // a position sent + if ((itemType & 0x20000u) != 0u) + flags |= 0x04u; // a lockable type + if ((descriptionFlags & 0x2u) != 0u) + flags |= 0x10u; // inscribable + if ((weenieFlags & 0xC00u) != 0u) + flags |= 0x20u; // uses left or total uses sent + if ((weenieFlags & 0x6u) != 0u) + flags |= 0x80u; // item or pack capacity sent + return flags; + } } + diff --git a/src/OpenAC.MosswartMassacre/plugin.json b/src/OpenAC.MosswartMassacre/plugin.json index 8587c2c..44fc070 100644 --- a/src/OpenAC.MosswartMassacre/plugin.json +++ b/src/OpenAC.MosswartMassacre/plugin.json @@ -6,7 +6,7 @@ "apiVersion": 1, "kinds": ["Gameplay"], "hosts": ["Graphical", "Headless"], - "minHostVersion": "0.1.18", + "minHostVersion": "0.1.19", "capabilitiesVersion": 1, "capabilities": [ { "name": "network", "note": "Streams telemetry, vitals, chat, inventory and stats to the Mosswart Overlord backend, and receives shared vitals and chat-box commands from it." }, diff --git a/tests/OpenAC.MosswartMassacre.Tests/Inventory/RecordedItemTests.cs b/tests/OpenAC.MosswartMassacre.Tests/Inventory/RecordedItemTests.cs new file mode 100644 index 0000000..1e38f5c --- /dev/null +++ b/tests/OpenAC.MosswartMassacre.Tests/Inventory/RecordedItemTests.cs @@ -0,0 +1,69 @@ +using AcDream.Plugin.Abstractions; +using OpenAC.MosswartMassacre.Inventory; + +namespace OpenAC.MosswartMassacre.Tests.Inventory; + +/// +/// Items exactly as the original recorded them from a live game (the dumps in +/// the Mag-Plugins repository), rebuilt from the header words the host now +/// reports: the key tables must come out the same, value for value. +/// +public sealed class RecordedItemTests +{ + [Fact] + public void A_recorded_pyreal_mote_rebuilds_key_for_key() + { + // Barris.json, the first item: a Pyreal Mote in a side pack, never + // appraised. Its IntValues and DoubleValues as recorded: + var expectedInts = new Dictionary + { + [218103847] = 137345, [218103830] = 33556406, [218103832] = 271151128, + [19] = 10, [218103808] = 6353, [218103809] = 7208, [218103834] = 128, + [218103843] = 524296, [218103835] = 18, [218103826] = 128, [218103831] = 17, + [218103814] = 1, [218103815] = 1, [5] = 1, [218103810] = 1342431769, + [218103822] = 0, [218103828] = 2, + }; + var expectedDoubles = new Dictionary { [167772170] = 0.0 }; + + var item = new PluginInventoryItem( + unchecked((uint)-2126483018), 6353u, "Pyreal Mote", 128u, 1342431769u, 0u, 0u, 0u, + 524296u, 128u, 18u, 1, 0, 0, 0u, 0, 0, 0u, false, 0d, 0, 0, 0, 0d, 0, 0, 0) + { + ObjectClass = PluginObjectClass.Misc, + IconId = 0x06000000u + 7208u, + MaximumStackSize = 1, + Burden = 1, + Value = 10, + Header = new PluginObjectHeader( + WeenieHeaderFlags: 271151128u, + WeenieHeaderFlags2: null, + PhysicsDescriptionFlags: 137345u, + PhysicsState: null, + ObjectDescriptionFlags: 18u, + SetupId: 33556406u, + Scale: 1.0f, + HookType: 2u, + ParentObjectId: null, + ParentLocation: null, + UseRadius: null), + }; + + MyWorldObject record = DecalItemBuilder.Create(new ItemSource( + item, null, false, 0, HostItemHeaderSource.Instance.Read(item))); + + Assert.Equal(expectedInts.OrderBy(p => p.Key), record.IntValues.OrderBy(p => p.Key)); + Assert.Equal(expectedDoubles.OrderBy(p => p.Key), record.DoubleValues.OrderBy(p => p.Key)); + Assert.Equal("Pyreal Mote", record.StringValues[1]); + } + + [Theory] + // Values of key 218103831 recorded for items, with their header words. + [InlineData(0x1000u, 0u, 0u, 0u, 0x01u)] // a stack + [InlineData(0u, 0x8000u, 0u, 0u, 0x02u)] // on the ground + [InlineData(0u, 0u, 0x20000u, 0u, 0x04u)] // lockable + [InlineData(0u, 0u, 0u, 0x2u, 0x10u)] // inscribable + [InlineData(0x800u, 0u, 0u, 0u, 0x20u)] // uses + [InlineData(0x2u, 0u, 0u, 0x12u, 0x90u)] // a pack + public void The_frameworks_summary_word(uint weenie, uint physics, uint itemType, uint description, uint expected) => + Assert.Equal(expected, HostItemHeaderSource.FrameworkFlags(weenie, physics, itemType, description)); +}