openac-mosswartmassacre/tests/OpenAC.MosswartMassacre.Tests/Inventory/DecalItemBuilderTests.cs
Erik 7a6b0a7f9d Inventory: full_inventory, inventory_delta, equipment_cantrip_state
The backend's inventory views and mana panel are fed by three streams the
original sent: the whole inventory after login, at logoff and on
/mm sendinventory; an add, update or remove per item as it enters,
changes in or leaves the packs; and the per-item cantrip state of worn and
wielded gear. This ports all three onto the host's object events, with the
frames built exactly as the original built them.

Behaviour kept: adds and removes go out at once, changes are coalesced into
one update per item on a two-to-five-minute timer re-rolled per flush; the
first login appraises everything worth appraising and waits before
sending; later logins merge in the saved appraisal data (kept in the
character's storage, as the original kept a file per character); the
mana panel debounces a quarter second and projects mana down between
reports.

Adapted to the host: appraisals go through a tick-driven queue, one at a
time, retrying while the host's single slot is busy; the login upload waits
for the inventory to stop arriving and for the socket, so it is not
written into a closed socket; the logoff upload subscribes ahead of the
root so the socket is still open; enchantment changes are polled once a
second (the host has no event); an item-cast enchantment is the one that
never runs out. A request the server never answers no longer holds the
upload back for good.

The Prismatic Taper count telemetry reads is InventoryFeature.PrismaticTaperCount.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 12:39:55 +02:00

114 lines
5 KiB
C#

using AcDream.Plugin.Abstractions;
using OpenAC.MosswartMassacre.Backend;
using OpenAC.MosswartMassacre.Inventory;
using static OpenAC.MosswartMassacre.Tests.Inventory.InventoryWorld;
namespace OpenAC.MosswartMassacre.Tests.Inventory;
public sealed class DecalItemBuilderTests
{
[Fact]
public void With_the_header_flags_known_a_field_sent_as_zero_is_present_and_an_unsent_one_absent()
{
// The original's dumps hold "218103822": 0 for items whose header
// carried a zero equipable-slots field (1153 of 3058 items).
PluginInventoryItem tapers = Tapers();
uint flags = HeaderFlags.Value | HeaderFlags.Usable | HeaderFlags.ValidLocations
| HeaderFlags.StackSize | HeaderFlags.MaxStackSize | HeaderFlags.PluralName;
MyWorldObject record = DecalItemBuilder.Create(new ItemSource(
tapers, null, false, 0, new ItemHeaderWords { WeenieHeaderFlags = flags }));
Assert.Equal(0, record.IntValues[DecalKeys.EquipableSlots]);
Assert.Equal(unchecked((int)flags), record.IntValues[DecalKeys.CreateFlags1]);
Assert.False(record.IntValues.ContainsKey(DecalKeys.RawBurden));
}
[Fact]
public void Without_the_header_flags_a_zero_field_is_left_out_and_the_value_key_reads_minus_one()
{
PluginInventoryItem bare = FakeItemsBare(0x80000555u, "Bare Thing");
MyWorldObject record = DecalItemBuilder.Create(new ItemSource(bare, null, false, 0, default));
Assert.Equal(-1, record.IntValues[DecalKeys.RawValue]);
Assert.Equal(-1, record.Value);
Assert.False(record.IntValues.ContainsKey(DecalKeys.EquipableSlots));
Assert.False(record.IntValues.ContainsKey(DecalKeys.CreateFlags1));
Assert.False(record.IntValues.ContainsKey(DecalKeys.PhysicsDataFlags));
Assert.False(record.IntValues.ContainsKey(DecalKeys.Setup));
}
[Fact]
public void The_physics_and_second_header_words_are_written_when_known()
{
PluginInventoryItem tachi = Tachi(appraised: false);
var words = new ItemHeaderWords
{
WeenieHeaderFlags2 = 1u,
PhysicsDescriptionFlags = 0x21881u,
PhysicsWord = 17u,
SetupId = 0x02000155u,
Scale = 0d,
ParentObjectId = Me,
ParentLocation = 1,
HookType = 2,
};
MyWorldObject record = DecalItemBuilder.Create(new ItemSource(tachi, null, false, 0, words));
Assert.Equal(1, record.IntValues[DecalKeys.CreateFlags2]);
Assert.Equal(0x21881, record.IntValues[DecalKeys.PhysicsDataFlags]);
Assert.Equal(17, record.IntValues[DecalKeys.PhysicsWord]);
Assert.Equal(0x02000155, record.IntValues[DecalKeys.Setup]);
Assert.Equal(0d, record.DoubleValues[DecalKeys.Scale]);
Assert.Equal(unchecked((int)Me), record.IntValues[DecalKeys.Wielder]);
Assert.Equal(1, record.IntValues[DecalKeys.WieldingSlot]);
Assert.Equal(2, record.IntValues[DecalKeys.HookType]);
}
[Fact]
public void Combine_keeps_an_earlier_appraisal_and_refreshes_it_from_the_live_item()
{
// MyWorldObjectCreator.cs:44-61.
MyWorldObject older = DecalItemBuilder.Create(new ItemSource(
Tachi(appraised: true), TachiProps(), true, 1700000000, default));
PluginInventoryItem moved = Tachi(appraised: false) with { WielderObjectId = 0u, ContainerObjectId = 0x80000900u };
MyWorldObject newer = DecalItemBuilder.Create(new ItemSource(moved, null, false, 0, default));
MyWorldObject combined = DecalItemBuilder.Combine(older, newer);
Assert.Same(older, combined);
Assert.True(combined.HasIdData);
Assert.Equal(1700000000, combined.LastIdTime);
Assert.Equal([2598, 2603], combined.Spells);
Assert.Equal(50, combined.MaxDamage);
Assert.Equal(unchecked((int)0x80000900u), combined.ContainerId);
Assert.Equal(unchecked((int)0x80000900u), combined.IntValues[DecalKeys.Container]);
Assert.Equal(combined.IntValues.Keys.Order(), combined.IntValues.Keys);
}
[Fact]
public void Combine_takes_the_live_item_when_it_is_appraised_or_the_old_one_was_not()
{
MyWorldObject older = DecalItemBuilder.Create(new ItemSource(Tapers(), null, false, 0, default));
MyWorldObject newer = DecalItemBuilder.Create(new ItemSource(Tapers(60), null, false, 0, default));
Assert.Same(newer, DecalItemBuilder.Combine(older, newer));
}
[Fact]
public void A_saved_record_reads_back_and_writes_the_same_frame()
{
MyWorldObject record = DecalItemBuilder.Create(new ItemSource(
Breastplate(), BreastplateProps(), true, 1790325015, default));
string json = Wire.Serialize(record);
MyWorldObject? back = Newtonsoft.Json.JsonConvert.DeserializeObject<MyWorldObject>(json);
Assert.Equal(json, Wire.Serialize(back));
}
private static PluginInventoryItem FakeItemsBare(uint id, string name) =>
Fakes.FakeItems.Item(id, name) with { ObjectClass = PluginObjectClass.Misc, MaximumStackSize = 1 };
}