F2: the logoff inventory is skipped when the host already let go of the items (it may reset the session before reporting the logoff): an empty list would have emptied the backend's inventory and overwritten the saved copy. F3/F4: each socket start owns its run; Stop no longer blocks the game thread and gives the run up to ten seconds to send what was queued (a large logoff inventory) before closing, flushing that run's own send loop; a relog never hears the previous run's frames. F7: on connect, share_subscribe goes out before the first telemetry frame, as the original sent them. F8: a rare announcement still waiting at logoff is dropped rather than sent as the next character. F9: each dungeon map is sent once per login, as the original (reloaded per login) did. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
296 lines
12 KiB
C#
296 lines
12 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 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<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"]!);
|
|
}
|
|
}
|