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(); 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(); 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 Logoff_after_the_host_let_go_of_the_items_sends_nothing_and_keeps_the_saved_copy() { // The host may reset the session before it reports the logoff; an // empty list then would wipe the backend's inventory. using var harness = new PluginHarness("Mossy", arrange: host => Own(host, Tapers())); LoginAndSettle(harness); harness.Tick(0.1); harness.Tick(0.1); harness.Plugin.OnLogoff(); string? saved = harness.Host.Storage.ReadText("inventory.json"); Assert.NotNull(saved); harness.Plugin.OnLogin(); LoginAndSettle(harness); harness.Frames.Clear(); harness.Host.Automation.Items.Owned.Clear(); harness.Plugin.OnLogoff(); Assert.Empty(harness.FramesOfTypeRaw("full_inventory")); Assert.Equal(saved, harness.Host.Storage.ReadText("inventory.json")); } [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(); 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"]!); } }