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>
114 lines
5 KiB
C#
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 };
|
|
}
|