Resolved FeatureCatalog; telemetry now reads the taper count from the inventory feature. The logoff inventory is sent from the feature's own OnLogoff instead of a second host-logoff subscription whose correctness depended on subscription order: the root logs features off in reverse, the socket last, so the frame still leaves. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
273 lines
11 KiB
C#
273 lines
11 KiB
C#
using AcDream.Plugin.Abstractions;
|
|
using Newtonsoft.Json.Linq;
|
|
using OpenAC.MosswartMassacre.Inventory;
|
|
using static OpenAC.MosswartMassacre.Tests.Inventory.InventoryWorld;
|
|
|
|
namespace OpenAC.MosswartMassacre.Tests.Inventory;
|
|
|
|
public sealed class InventoryFeatureTests
|
|
{
|
|
private const string Prefix = "[Mosswart Massacre] ";
|
|
|
|
[Fact]
|
|
public void First_login_appraises_what_needs_it_and_sends_only_when_the_answers_are_in()
|
|
{
|
|
using var harness = new PluginHarness("Mossy", arrange: host =>
|
|
{
|
|
Own(host, Tachi(appraised: false));
|
|
Own(host, Tapers());
|
|
});
|
|
LoginAndSettle(harness);
|
|
harness.Tick(0.1);
|
|
|
|
Assert.Contains(Prefix + "Requesting id information for all armor/weapon inventory. This will take a few minutes...", harness.ChatLines);
|
|
Assert.Equal([0x80000202u], harness.Host.Automation.Objects.IdentifyRequests);
|
|
harness.Tick(0.1);
|
|
Assert.Empty(harness.FramesOfTypeRaw("full_inventory"));
|
|
|
|
Own(harness.Host, Tachi(appraised: true), TachiProps(), hostIdTime: 5000);
|
|
Raise(harness.Host, 0x80000202u, PluginObjectChangeKind.IdentReceived);
|
|
harness.Tick(0.1);
|
|
|
|
JObject frame = JObject.Parse(harness.FramesOfTypeRaw("full_inventory").Single());
|
|
Assert.Equal(2, (int)frame["item_count"]!);
|
|
Assert.True((bool)frame["items"]![0]!["HasIdData"]!);
|
|
Assert.Contains(Prefix + "Requesting id information for all armor/weapon inventory completed. Log file written.", harness.ChatLines);
|
|
Assert.Contains(Prefix + "Inventory sent to MosswartOverlord", harness.ChatLines);
|
|
Assert.NotNull(harness.Host.Storage.ReadText(InventoryFeature.CacheKey));
|
|
}
|
|
|
|
[Fact]
|
|
public void A_later_login_sends_at_once_and_keeps_the_saved_appraisal_of_an_unappraised_item()
|
|
{
|
|
using var harness = new PluginHarness("Mossy", arrange: host =>
|
|
Own(host, Tachi(appraised: true), TachiProps(), hostIdTime: 5000));
|
|
LoginAndSettle(harness);
|
|
harness.Tick(0.1);
|
|
harness.Tick(0.1);
|
|
Assert.Single(harness.FramesOfTypeRaw("full_inventory"));
|
|
|
|
// Relog: the session's appraisal is gone, the saved copy is not.
|
|
harness.Plugin.OnLogoff();
|
|
Own(harness.Host, Tachi(appraised: false));
|
|
harness.Frames.Clear();
|
|
harness.Host.Automation.Objects.IdentifyRequests.Clear();
|
|
LoginAndSettle(harness);
|
|
harness.Tick(0.1);
|
|
|
|
JObject frame = JObject.Parse(harness.FramesOfTypeRaw("full_inventory").Single());
|
|
JToken item = frame["items"]![0]!;
|
|
Assert.True((bool)item["HasIdData"]!);
|
|
Assert.Equal(50, (int)item["MaxDamage"]!);
|
|
Assert.Equal("Light Weapons", (string)item["EquipSkill"]!);
|
|
// Saved data present, so the original did not ask again from the dump
|
|
// (MossyInventory.cs:253); the login stream already asked once.
|
|
Assert.Equal([0x80000202u], harness.Host.Automation.Objects.IdentifyRequests);
|
|
}
|
|
|
|
[Fact]
|
|
public void Sendinventory_sends_at_once_when_everything_is_appraised()
|
|
{
|
|
using var harness = new PluginHarness("Mossy", arrange: host => Own(host, Tapers()));
|
|
LoginAndSettle(harness);
|
|
harness.Tick(0.1);
|
|
harness.Tick(0.1);
|
|
harness.Frames.Clear();
|
|
|
|
harness.Context.Commands.Dispatch("sendinventory");
|
|
|
|
Assert.Single(harness.FramesOfTypeRaw("full_inventory"));
|
|
Assert.Contains(Prefix + "[INV] All 1 items already identified. Sending...", harness.ChatLines);
|
|
Assert.Contains(Prefix + "[INV] Inventory upload completed", harness.ChatLines);
|
|
}
|
|
|
|
[Fact]
|
|
public void Sendinventory_waits_for_appraisals_and_refuses_when_streaming_is_off()
|
|
{
|
|
using var harness = new PluginHarness("Mossy", arrange: host => Own(host, Tapers()));
|
|
LoginAndSettle(harness);
|
|
harness.Tick(0.1);
|
|
harness.Tick(0.1);
|
|
harness.Frames.Clear();
|
|
Own(harness.Host, Ring());
|
|
|
|
harness.Context.Commands.Dispatch("sendinventory");
|
|
Assert.Contains(Prefix + "[INV] Scanning 2 items, requesting ID for 1. Waiting for identification...", harness.ChatLines);
|
|
harness.Tick(0.1);
|
|
Assert.Empty(harness.FramesOfTypeRaw("full_inventory"));
|
|
|
|
Own(harness.Host, Ring(), Props(ints: new() { [107] = 10, [108] = 20 }), hostIdTime: 42);
|
|
Raise(harness.Host, 0x80000404u, PluginObjectChangeKind.IdentReceived);
|
|
harness.Tick(0.1);
|
|
Assert.Single(harness.FramesOfTypeRaw("full_inventory"));
|
|
Assert.Contains(Prefix + "[INV] All items identified. Inventory upload completed.", harness.ChatLines);
|
|
|
|
harness.Context.Settings.Current.WebSocketEnabled = false;
|
|
harness.Context.Commands.Dispatch("sendinventory");
|
|
Assert.Contains(Prefix + "[INV] WebSocket streaming is disabled", harness.ChatLines);
|
|
}
|
|
|
|
[Fact]
|
|
public void The_login_stream_is_tracked_without_adds_and_items_after_it_are_added()
|
|
{
|
|
using var harness = new PluginHarness("Mossy");
|
|
harness.Login();
|
|
InventoryFeature feature = harness.Feature<InventoryFeature>();
|
|
feature.IsBackendReady = () => true;
|
|
|
|
Own(harness.Host, Tapers());
|
|
Raise(harness.Host, 0x80000101u, PluginObjectChangeKind.Created);
|
|
Assert.False(feature.IsSettled);
|
|
for (int i = 0; i < 40 && !feature.IsSettled; i++)
|
|
harness.Tick(0.1);
|
|
|
|
Assert.Empty(harness.FramesOfTypeRaw("inventory_delta"));
|
|
Assert.Equal(1, feature.TrackedCount);
|
|
|
|
Own(harness.Host, Breastplate(), BreastplateProps(), hostIdTime: 9);
|
|
Raise(harness.Host, 0x80000303u, PluginObjectChangeKind.Created);
|
|
Assert.Equal("add", (string)JObject.Parse(harness.FramesOfTypeRaw("inventory_delta").Single())["action"]!);
|
|
}
|
|
|
|
[Fact]
|
|
public void Changes_are_held_until_the_jittered_flush_between_two_and_five_minutes()
|
|
{
|
|
using var harness = new PluginHarness("Mossy", arrange: host => Own(host, Tapers()));
|
|
InventoryFeature feature = LoginAndSettle(harness);
|
|
harness.Frames.Clear();
|
|
|
|
Own(harness.Host, Tapers(60));
|
|
Raise(harness.Host, 0x80000101u, PluginObjectChangeKind.Updated);
|
|
Raise(harness.Host, 0x80000101u, PluginObjectChangeKind.Updated);
|
|
Assert.Equal(1, feature.DirtyCount);
|
|
|
|
for (int i = 0; i < 1190; i++)
|
|
harness.Tick(0.1);
|
|
Assert.Empty(harness.FramesOfTypeRaw("inventory_delta"));
|
|
|
|
for (int i = 0; i < 1820 && harness.FramesOfTypeRaw("inventory_delta").Count == 0; i++)
|
|
harness.Tick(0.1);
|
|
|
|
JObject update = JObject.Parse(harness.FramesOfTypeRaw("inventory_delta").Single());
|
|
Assert.Equal("update", (string)update["action"]!);
|
|
Assert.Equal(60, (int)update["item"]!["IntValues"]!["218103814"]!);
|
|
}
|
|
|
|
[Fact]
|
|
public void The_flush_interval_is_two_minutes_plus_up_to_three()
|
|
{
|
|
using var harness = new PluginHarness("Mossy");
|
|
InventoryFeature feature = harness.Feature<InventoryFeature>();
|
|
|
|
int[] samples = Enumerable.Range(0, 2000).Select(_ => feature.NextFlushIntervalMs()).ToArray();
|
|
|
|
Assert.All(samples, ms => Assert.InRange(ms, 120000, 300000));
|
|
Assert.True(samples.Max() - samples.Min() > 150000);
|
|
}
|
|
|
|
[Fact]
|
|
public void An_item_moved_out_of_the_inventory_is_removed_and_one_in_a_side_pack_is_added()
|
|
{
|
|
const uint pack = 0x80000900u;
|
|
using var harness = new PluginHarness("Mossy", arrange: host =>
|
|
{
|
|
Own(host, Tapers());
|
|
host.Automation.Objects.Replace(new PluginWorldObject(pack, 1u, "Pack", PluginObjectClass.Container, 0x200u, Me, 0u));
|
|
});
|
|
LoginAndSettle(harness);
|
|
harness.Frames.Clear();
|
|
|
|
Own(harness.Host, Ring() with { ContainerObjectId = pack, WielderObjectId = 0u, EquippedLocation = 0u });
|
|
Raise(harness.Host, 0x80000404u, PluginObjectChangeKind.Moved);
|
|
Own(harness.Host, Tapers() with { ContainerObjectId = 0x7000AAAAu });
|
|
Raise(harness.Host, 0x80000101u, PluginObjectChangeKind.Moved);
|
|
|
|
string[] actions = harness.FramesOfTypeRaw("inventory_delta")
|
|
.Select(f => (string)JObject.Parse(f)["action"]!).ToArray();
|
|
Assert.Equal(["add", "remove"], actions);
|
|
}
|
|
|
|
[Fact]
|
|
public void A_relog_starts_tracking_afresh()
|
|
{
|
|
using var harness = new PluginHarness("Mossy", arrange: host => Own(host, Tapers()));
|
|
InventoryFeature feature = LoginAndSettle(harness);
|
|
Own(harness.Host, Tapers(60));
|
|
Raise(harness.Host, 0x80000101u, PluginObjectChangeKind.Updated);
|
|
Assert.Equal(1, feature.DirtyCount);
|
|
|
|
harness.Plugin.OnLogoff();
|
|
Assert.Equal(0, feature.DirtyCount);
|
|
Assert.Equal(0, feature.TrackedCount);
|
|
|
|
harness.Host.Automation.Character.Name = "Horan";
|
|
harness.Frames.Clear();
|
|
LoginAndSettle(harness);
|
|
Raise(harness.Host, 0x80000101u, PluginObjectChangeKind.Updated);
|
|
|
|
Assert.Empty(harness.FramesOfTypeRaw("inventory_delta"));
|
|
Assert.Equal(1, feature.DirtyCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void Logoff_sends_the_inventory_while_the_socket_is_still_up()
|
|
{
|
|
using var harness = new PluginHarness("Mossy", arrange: host => Own(host, Tapers()));
|
|
LoginAndSettle(harness);
|
|
harness.Tick(0.1);
|
|
harness.Tick(0.1);
|
|
harness.Frames.Clear();
|
|
|
|
harness.Plugin.OnLogoff();
|
|
|
|
Assert.Single(harness.FramesOfTypeRaw("full_inventory"));
|
|
}
|
|
|
|
[Fact]
|
|
public void The_taper_count_follows_the_stacks_in_the_packs()
|
|
{
|
|
using var harness = new PluginHarness("Mossy", arrange: host => Own(host, Tapers()));
|
|
InventoryFeature feature = LoginAndSettle(harness);
|
|
Assert.Equal(57, feature.PrismaticTaperCount);
|
|
|
|
Own(harness.Host, Tapers(12));
|
|
Raise(harness.Host, 0x80000101u, PluginObjectChangeKind.Updated);
|
|
Assert.Equal(12, feature.PrismaticTaperCount);
|
|
|
|
Forget(harness.Host, 0x80000101u);
|
|
harness.Host.Events.RaiseObjectChanged(new PluginObjectChange(0x80000101u, PluginObjectChangeKind.Released));
|
|
Assert.Equal(0, feature.PrismaticTaperCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void Inventory_log_off_sends_no_login_inventory()
|
|
{
|
|
using var harness = new PluginHarness("Mossy", arrange: host => Own(host, Tapers()));
|
|
harness.Login();
|
|
harness.Context.Settings.Current.InventoryLog = false;
|
|
InventoryFeature feature = harness.Feature<InventoryFeature>();
|
|
feature.IsBackendReady = () => true;
|
|
for (int i = 0; i < 40; i++)
|
|
harness.Tick(0.1);
|
|
|
|
Assert.Empty(harness.FramesOfTypeRaw("full_inventory"));
|
|
}
|
|
|
|
[Fact]
|
|
public void The_appraisal_time_is_the_wall_clock_when_the_appraisal_arrived()
|
|
{
|
|
using var harness = new PluginHarness("Mossy");
|
|
InventoryFeature feature = LoginAndSettle(harness);
|
|
Own(harness.Host, Tachi(appraised: false));
|
|
Raise(harness.Host, 0x80000202u, PluginObjectChangeKind.Created);
|
|
|
|
Own(harness.Host, Tachi(appraised: true), TachiProps(), hostIdTime: 5000);
|
|
Raise(harness.Host, 0x80000202u, PluginObjectChangeKind.IdentReceived);
|
|
harness.Clock.Advance(TimeSpan.FromMinutes(3));
|
|
harness.Frames.Clear();
|
|
feature.FlushDirtyItems();
|
|
|
|
JObject update = JObject.Parse(harness.FramesOfTypeRaw("inventory_delta").Single());
|
|
Assert.Equal(1790325015, (int)update["item"]!["LastIdTime"]!);
|
|
}
|
|
}
|