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:
parent
f4a44bd9fc
commit
7a6b0a7f9d
11 changed files with 2206 additions and 2 deletions
205
tests/OpenAC.MosswartMassacre.Tests/Inventory/InventoryWorld.cs
Normal file
205
tests/OpenAC.MosswartMassacre.Tests/Inventory/InventoryWorld.cs
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
using AcDream.Plugin.Abstractions;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OpenAC.MosswartMassacre.Inventory;
|
||||
using OpenAC.MosswartMassacre.Tests.Fakes;
|
||||
|
||||
namespace OpenAC.MosswartMassacre.Tests.Inventory;
|
||||
|
||||
/// <summary>
|
||||
/// Puts items into the fake host the way the real one holds them: the item
|
||||
/// in the owned list, its world object in the object table, its property
|
||||
/// tables when it has any.
|
||||
/// </summary>
|
||||
internal static class InventoryWorld
|
||||
{
|
||||
/// <summary>The harness character's id.</summary>
|
||||
public const uint Me = 0x50000001;
|
||||
|
||||
public static PluginItemProperties Props(
|
||||
Dictionary<uint, int>? ints = null,
|
||||
Dictionary<uint, double>? floats = null,
|
||||
Dictionary<uint, bool>? bools = null,
|
||||
Dictionary<uint, string>? strings = null,
|
||||
PluginWeaponProfile? weapon = null,
|
||||
PluginArmorProfile? armor = null)
|
||||
=> new(
|
||||
ints ?? [],
|
||||
new Dictionary<uint, long>(),
|
||||
bools ?? [],
|
||||
floats ?? [],
|
||||
strings ?? [],
|
||||
new Dictionary<uint, uint>(),
|
||||
new Dictionary<uint, uint>())
|
||||
{
|
||||
WeaponProfile = weapon,
|
||||
ArmorProfile = armor,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Adds or replaces an owned item. <paramref name="hostIdTime"/> non-zero
|
||||
/// with properties means appraised.
|
||||
/// </summary>
|
||||
public static void Own(
|
||||
FakeHost host,
|
||||
PluginInventoryItem item,
|
||||
PluginItemProperties? properties = null,
|
||||
int hostIdTime = 0)
|
||||
{
|
||||
List<PluginInventoryItem> owned = host.Automation.Items.Owned;
|
||||
int index = owned.FindIndex(existing => existing.ObjectId == item.ObjectId);
|
||||
if (index >= 0)
|
||||
owned[index] = item;
|
||||
else
|
||||
owned.Add(item);
|
||||
|
||||
if (properties is { } props)
|
||||
host.Automation.Items.Properties[item.ObjectId] = props;
|
||||
else
|
||||
host.Automation.Items.Properties.Remove(item.ObjectId);
|
||||
|
||||
host.Automation.Objects.Replace(World(item, properties is not null, hostIdTime));
|
||||
}
|
||||
|
||||
/// <summary>Takes an item out of the owned list and the object table.</summary>
|
||||
public static void Forget(FakeHost host, uint objectId)
|
||||
{
|
||||
host.Automation.Items.Owned.RemoveAll(item => item.ObjectId == objectId);
|
||||
host.Automation.Items.Properties.Remove(objectId);
|
||||
host.Automation.Objects.Objects.RemoveAll(o => o.ObjectId == objectId);
|
||||
}
|
||||
|
||||
public static PluginWorldObject World(PluginInventoryItem item, bool hasProperties, int hostIdTime)
|
||||
=> new(
|
||||
item.ObjectId,
|
||||
item.WeenieClassId,
|
||||
item.Name,
|
||||
item.ObjectClass,
|
||||
item.ItemType,
|
||||
item.ContainerObjectId,
|
||||
item.WielderObjectId)
|
||||
{
|
||||
IsOwned = true,
|
||||
HasAppraisalData = hasProperties,
|
||||
LastIdTime = hostIdTime,
|
||||
SpellIds = item.AppraisedSpellIds,
|
||||
IconId = item.IconId,
|
||||
};
|
||||
|
||||
/// <summary>A change as the host raises it, with the object's current snapshot.</summary>
|
||||
public static PluginObjectChange Change(FakeHost host, uint objectId, PluginObjectChangeKind kind)
|
||||
=> new(objectId, kind)
|
||||
{
|
||||
Current = host.Automation.Objects.TryGet(objectId, out PluginWorldObject current)
|
||||
? current
|
||||
: null,
|
||||
};
|
||||
|
||||
public static void Raise(FakeHost host, uint objectId, PluginObjectChangeKind kind)
|
||||
=> host.Events.RaiseObjectChanged(Change(host, objectId, kind));
|
||||
|
||||
/// <summary>Logs in and runs the ticks it takes the inventory to settle, without moving the clock.</summary>
|
||||
public static InventoryFeature LoginAndSettle(PluginHarness harness)
|
||||
{
|
||||
harness.Login();
|
||||
InventoryFeature feature = harness.Feature<InventoryFeature>();
|
||||
feature.IsBackendReady = () => true;
|
||||
for (int i = 0; i < 40 && !feature.IsSettled; i++)
|
||||
harness.Tick(0.1);
|
||||
Assert.True(feature.IsSettled);
|
||||
return feature;
|
||||
}
|
||||
|
||||
/// <summary>A stack of 57 Prismatic Tapers in the main pack, never appraised.</summary>
|
||||
public static PluginInventoryItem Tapers(int stack = 57) =>
|
||||
new(
|
||||
0x80000101u, 20631u, "Prismatic Taper", 0x1000u, Me, 0u, 0u, 0u,
|
||||
1u, 0u, 18u, stack, 0, 0, 0u, 0, 0, 0u, false, 0d, 0, 0, 0, 0d, 0, 0, 0)
|
||||
{
|
||||
ObjectClass = PluginObjectClass.SpellComponent,
|
||||
IconId = 0x06002D8Au,
|
||||
MaximumStackSize = 100,
|
||||
ContainerSlot = 0,
|
||||
Burden = stack,
|
||||
Value = stack * 10,
|
||||
PluralName = "Prismatic Tapers",
|
||||
};
|
||||
|
||||
/// <summary>A breastplate in the main pack, appraised, with two carried and two running spells.</summary>
|
||||
public static PluginInventoryItem Breastplate() =>
|
||||
new(
|
||||
0x80000303u, 6044u, "Celdon Breastplate", 0x2u, Me, 0u, 512u, 0u,
|
||||
1u, 0u, 18u, 1, 0, 0, 0u, 0, 0, 0u, false, 0d, 0, 0, 0, 0d, 0, 0, 0)
|
||||
{
|
||||
ObjectClass = PluginObjectClass.Armor,
|
||||
IconId = 0x06001234u,
|
||||
ContainerSlot = 3,
|
||||
Burden = 800,
|
||||
Value = 5000,
|
||||
CoverageMask = 1024u,
|
||||
Effects = 1u,
|
||||
AppraisedSpellIds = [1486u, 2604u, 0x80000000u | 4407u, 0x80000000u | 1483u],
|
||||
};
|
||||
|
||||
public static PluginItemProperties BreastplateProps() =>
|
||||
Props(
|
||||
ints: new() { [28] = 600, [265] = 5 },
|
||||
bools: new() { [69] = true },
|
||||
armor: new PluginArmorProfile(600, 1.2f, 1.0f, 0.8f, 0.6f, 0.4f, 0.2f, 0f, 1.5f));
|
||||
|
||||
/// <summary>A wielded sword, before (no spells) or after appraisal.</summary>
|
||||
public static PluginInventoryItem Tachi(bool appraised) =>
|
||||
new(
|
||||
0x80000202u, 30310u, "Fire Tachi", 0x1u, 0u, Me, 1048576u, 1048576u,
|
||||
6291460u, 16u, 0x04000012u, 1, 0, 0, 0u, 0, 0, 0u, false, 0d, 0, 0, 0, 0d, 0, 0, 0)
|
||||
{
|
||||
ObjectClass = PluginObjectClass.MeleeWeapon,
|
||||
IconId = 0x0600336Au,
|
||||
IconOverlayId = 0x06001B2Eu,
|
||||
IconUnderlayId = 0u,
|
||||
ContainerSlot = -1,
|
||||
CombatUse = 1,
|
||||
Burden = 270,
|
||||
Value = 12000,
|
||||
MaterialType = 64u,
|
||||
Workmanship = 8f,
|
||||
Effects = 1u,
|
||||
AppraisedSpellIds = appraised ? [2598u, 2603u, 0x80000000u | 5183u] : [],
|
||||
};
|
||||
|
||||
public static PluginItemProperties TachiProps() =>
|
||||
Props(
|
||||
ints: new()
|
||||
{
|
||||
[105] = 8, [107] = 1500, [108] = 2000, [109] = 300, [158] = 2, [159] = 45,
|
||||
[160] = 370, [171] = 3, [179] = 1, [353] = 2, [370] = 2,
|
||||
},
|
||||
floats: new() { [5] = -0.05, [29] = 1.15 },
|
||||
strings: new() { [16] = "Fire Tachi of Blood Drinker" },
|
||||
weapon: new PluginWeaponProfile(16, 30, 45u, 50, 0.5, 1.0, 1.5, 1.0, 1.2000000476837158, 0));
|
||||
|
||||
/// <summary>A ring the character wears, never appraised.</summary>
|
||||
public static PluginInventoryItem Ring() =>
|
||||
new(
|
||||
0x80000404u, 2575u, "Gold Ring", 0x8u, 0u, Me, 0xC0000u, 0x40000u,
|
||||
1u, 0u, 18u, 1, 0, 0, 0u, 0, 0, 0u, false, 0d, 0, 0, 0, 0d, 0, 0, 0)
|
||||
{
|
||||
ObjectClass = PluginObjectClass.Jewelry,
|
||||
IconId = 0x06000F6Eu,
|
||||
ContainerSlot = -1,
|
||||
Burden = 30,
|
||||
Value = 400,
|
||||
};
|
||||
|
||||
public static PluginSpellInfo Spell(uint id, uint family, int difficulty, bool debuff = false)
|
||||
=> new(id, $"Spell {id}", family, 1, difficulty, 10, -1f, 3u, string.Empty, false, true)
|
||||
{
|
||||
IsDebuff = debuff,
|
||||
};
|
||||
}
|
||||
|
||||
internal static class InventoryHarnessExtensions
|
||||
{
|
||||
/// <summary>The raw frames of one type, in the order they were sent.</summary>
|
||||
public static List<string> FramesOfTypeRaw(this PluginHarness harness, string type) =>
|
||||
harness.Frames.Where(frame => (string?)JObject.Parse(frame)["type"] == type).ToList();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue