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>
This commit is contained in:
Erik 2026-09-25 12:39:55 +02:00
parent f4a44bd9fc
commit 7a6b0a7f9d
11 changed files with 2206 additions and 2 deletions

View file

@ -0,0 +1,88 @@
using AcDream.Plugin.Abstractions;
using OpenAC.MosswartMassacre.Inventory;
using OpenAC.MosswartMassacre.Tests.Fakes;
namespace OpenAC.MosswartMassacre.Tests.Inventory;
public sealed class IdentifyQueueTests
{
private static (IdentifyQueue Queue, FakeAutomation Automation, List<(uint, bool)> Finished) Create()
{
var automation = new FakeAutomation { IsAvailable = true };
var queue = new IdentifyQueue(() => automation);
var finished = new List<(uint, bool)>();
queue.Finished += (id, ok) => finished.Add((id, ok));
return (queue, automation, finished);
}
[Fact]
public void Requests_go_out_one_at_a_time_and_the_next_waits_for_the_answer()
{
var (queue, automation, finished) = Create();
queue.Enqueue(1);
queue.Enqueue(2);
queue.Enqueue(1);
queue.Pump(0.015);
queue.Pump(0.015);
Assert.Equal([1u], automation.Objects.IdentifyRequests);
queue.OnIdentReceived(1);
queue.Pump(0.015);
Assert.Equal([1u, 2u], automation.Objects.IdentifyRequests);
Assert.Equal([(1u, true)], finished);
Assert.False(queue.IsIdle);
queue.OnIdentReceived(2);
Assert.True(queue.IsIdle);
}
[Fact]
public void A_busy_slot_is_asked_again_on_a_later_tick()
{
var (queue, automation, _) = Create();
automation.Objects.IdentifyBusyForCalls[7] = 2;
queue.Enqueue(7);
queue.Pump(0.015);
queue.Pump(0.015);
queue.Pump(0.015);
Assert.Equal([7u, 7u, 7u], automation.Objects.IdentifyRequests);
Assert.Equal(1, queue.Count);
}
[Fact]
public void The_host_answer_or_abandon_finishes_a_request()
{
var (queue, automation, finished) = Create();
queue.Enqueue(3);
queue.Enqueue(4);
queue.Pump(0.015);
automation.Loot.Appraisal = new PluginAppraisalState(5, 0u, 3u);
queue.Pump(0.015);
automation.Loot.Appraisal = new PluginAppraisalState(6, 0u, 3u, LastAbandonedObjectId: 4u);
queue.Pump(0.015);
Assert.Equal([(3u, true), (4u, false)], finished);
}
[Fact]
public void An_unanswered_request_is_given_up_after_the_timeout_and_a_refusal_at_once()
{
var (queue, automation, finished) = Create();
automation.Objects.IdentifyResultOverrides[8] = PluginItemCommandStatus.InvalidItem;
queue.Enqueue(8);
queue.Enqueue(9);
queue.Pump(0.015);
Assert.Equal([(8u, false)], finished);
for (int i = 0; i < 700; i++)
queue.Pump(0.015);
Assert.Equal([(8u, false), (9u, false)], finished);
Assert.True(queue.IsIdle);
}
}