openac-mosswartmassacre/tests/OpenAC.MosswartMassacre.Tests/Inventory/InventoryGoldenTests.cs
Erik 5a94e1cf0b Wire: write doubles and floats with .NET Framework's round-trip digits
The original ran Newtonsoft.Json on .NET Framework, whose "R" format writes
15 significant digits when they read back exactly and 17 otherwise (floats: 7,
else 9). Modern .NET writes the shortest round trip instead, so a value like
-0.066666670143604279 came out as -0.06666667014360428: equal when parsed,
different bytes. Every frame now goes through a converter that repeats the
Framework rule, with Newtonsoft's '.0' suffix and non-finite handling kept.
Goldens that encoded the modern digits are corrected.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 12:44:34 +02:00

155 lines
13 KiB
C#

using AcDream.Plugin.Abstractions;
using OpenAC.MosswartMassacre.Inventory;
using static OpenAC.MosswartMassacre.Tests.Inventory.InventoryWorld;
namespace OpenAC.MosswartMassacre.Tests.Inventory;
/// <summary>
/// Byte-for-byte frames, derived by hand from the original plugin's code.
/// Shape sources (original plugin, head 48cf13b):
/// <list type="bullet">
/// <item>Item record: <c>Shared/MyWorldObject.cs:16-29</c> (fields, in order) and
/// <c>:118-315</c> (getters, in order; doubles stay doubles so -1 is written -1.0).</item>
/// <item>Record contents: <c>Shared/MyWorldObjectCreator.cs:20-39</c> — the framework's
/// key tables, spells, active spells, id, appraisal time, class, icon, container.</item>
/// <item>Which framework keys exist and what they hold: the original's own item
/// dumps (3058 items), see <see cref="DecalItemBuilder"/>.</item>
/// <item>Envelopes: <c>MosswartMassacre/WebSocket.cs:339-380</c> (full_inventory,
/// inventory_delta add/update and remove) and <c>:411-423</c> (equipment_cantrip_state);
/// the per-item mana entry <c>EquipmentCantripStateTracker.cs:222-229</c>.</item>
/// </list>
/// </summary>
public sealed class InventoryGoldenTests
{
// The harness clock, 2026-09-25 08:30:15.123 UTC, in Unix seconds: the
// original stamped an appraisal with the wall clock when it arrived.
private const string IdStamp = "1790325015";
// Getters from ElementalDmgBonus onward for an item with none of their keys
// except Value/Burden (MyWorldObject.cs:163-315).
private const string TaperRecord =
"""{"HasIdData":false,"Id":-2147483391,"LastIdTime":0,"ObjectClass":12,"Icon":11658,"ContainerId":1342177281,"BoolValues":{},"DoubleValues":{},"IntValues":{"5":57,"19":570,"218103808":20631,"218103809":11658,"218103810":1342177281,"218103814":57,"218103815":100,"218103834":4096,"218103835":18,"218103843":1},"StringValues":{"1":"Prismatic Taper","184549376":"Prismatic Tapers"},"ActiveSpells":[],"Spells":[],"Material":null,"Name":"Prismatic Taper","EquipSkill":null,"Mastery":null,"ItemSet":null,"ArmorLevel":-1,"Imbue":null,"Tinks":-1,"MaxDamage":-1,"ElementalDmgBonus":-1,"Variance":-1.0,"DamageBonus":-1.0,"ElementalDamageVersusMonsters":-1.0,"AttackBonus":-1.0,"MeleeDefenseBonus":-1.0,"MagicDBonus":-1.0,"MissileDBonus":-1.0,"ManaCBonus":-1.0,"WieldLevel":-1,"SkillLevel":-1,"LoreRequirement":-1,"SalvageWorkmanship":-1.0,"Workmanship":-1,"Value":570,"Burden":57,"DamRating":-1,"DamResistRating":-1,"CritRating":-1,"CritResistRating":-1,"CritDamRating":-1,"CritDamResistRating":-1,"HealBoostRating":-1,"VitalityRating":-1,"TotalRating":-1,"CalcedStartingArmorLevel":-1,"CalcedBuffedTinkedDoT":-1.0,"CalcedBuffedMissileDamage":-1.0,"BuffedElementalDamageVersusMonsters":-1.0,"BuffedAttackBonus":-1.0,"BuffedMeleeDefenseBonus":-1.0,"BuffedManaCBonus":-1.0}""";
// Armor profile -> 167772160..166 (slash, pierce, bludgeon, acid, lightning, fire,
// cold), floats widened; spell book split on the 0x80000000 bit; ItemSet 5 from
// Dictionaries.AttributeSetInfo; CalcedStartingArmorLevel (MyWorldObject.cs:232-265):
// 600 - 200 (no material) - (240 active Incantation of Impenetrability - 20 Minor
// Impenetrability bonus) = 180.
private const string BreastplateRecord =
"""{"HasIdData":true,"Id":-2147482877,"LastIdTime":""" + IdStamp + ""","ObjectClass":2,"Icon":4660,"ContainerId":1342177281,"BoolValues":{"69":true},"DoubleValues":{"167772160":1.2000000476837158,"167772161":1.0,"167772162":0.800000011920929,"167772163":0.20000000298023224,"167772164":1.5,"167772165":0.40000000596046448,"167772166":0.60000002384185791},"IntValues":{"5":800,"19":5000,"28":600,"265":5,"218103808":6044,"218103809":4660,"218103810":1342177281,"218103821":1024,"218103822":512,"218103824":1,"218103834":2,"218103835":18,"218103838":2,"218103843":1,"218103848":2},"StringValues":{"1":"Celdon Breastplate"},"ActiveSpells":[4407,1483],"Spells":[1486,2604],"Material":null,"Name":"Celdon Breastplate","EquipSkill":null,"Mastery":null,"ItemSet":"Noble Relic Set","ArmorLevel":600,"Imbue":null,"Tinks":-1,"MaxDamage":-1,"ElementalDmgBonus":-1,"Variance":-1.0,"DamageBonus":-1.0,"ElementalDamageVersusMonsters":-1.0,"AttackBonus":-1.0,"MeleeDefenseBonus":-1.0,"MagicDBonus":-1.0,"MissileDBonus":-1.0,"ManaCBonus":-1.0,"WieldLevel":-1,"SkillLevel":-1,"LoreRequirement":-1,"SalvageWorkmanship":-1.0,"Workmanship":-1,"Value":5000,"Burden":800,"DamRating":-1,"DamResistRating":-1,"CritRating":-1,"CritResistRating":-1,"CritDamRating":-1,"CritDamResistRating":-1,"HealBoostRating":-1,"VitalityRating":-1,"TotalRating":-1,"CalcedStartingArmorLevel":180,"CalcedBuffedTinkedDoT":-1.0,"CalcedBuffedMissileDamage":-1.0,"BuffedElementalDamageVersusMonsters":-1.0,"BuffedAttackBonus":-1.0,"BuffedMeleeDefenseBonus":-1.0,"BuffedManaCBonus":-1.0}""";
// Wielded: the container key and ContainerId are the wielder. The second header
// (public flags 0x04000000) brings the underlay key, here 0.
private const string TachiUnappraisedRecord =
"""{"HasIdData":false,"Id":-2147483134,"LastIdTime":0,"ObjectClass":1,"Icon":13162,"ContainerId":1342177281,"BoolValues":{},"DoubleValues":{"167772169":8.0},"IntValues":{"5":270,"10":1048576,"19":12000,"131":64,"218103808":30310,"218103809":13162,"218103810":1342177281,"218103822":1048576,"218103823":1,"218103824":1,"218103826":16,"218103834":1,"218103835":67108882,"218103843":6291460,"218103849":6958,"218103850":0},"StringValues":{"1":"Fire Tachi"},"ActiveSpells":[],"Spells":[],"Material":"Steel","Name":"Fire Tachi","EquipSkill":null,"Mastery":null,"ItemSet":null,"ArmorLevel":-1,"Imbue":null,"Tinks":-1,"MaxDamage":-1,"ElementalDmgBonus":-1,"Variance":-1.0,"DamageBonus":-1.0,"ElementalDamageVersusMonsters":-1.0,"AttackBonus":-1.0,"MeleeDefenseBonus":-1.0,"MagicDBonus":-1.0,"MissileDBonus":-1.0,"ManaCBonus":-1.0,"WieldLevel":-1,"SkillLevel":-1,"LoreRequirement":-1,"SalvageWorkmanship":8.0,"Workmanship":-1,"Value":12000,"Burden":270,"DamRating":-1,"DamResistRating":-1,"CritRating":-1,"CritResistRating":-1,"CritDamRating":-1,"CritDamResistRating":-1,"HealBoostRating":-1,"VitalityRating":-1,"TotalRating":-1,"CalcedStartingArmorLevel":-1,"CalcedBuffedTinkedDoT":-1.0,"CalcedBuffedMissileDamage":-1.0,"BuffedElementalDamageVersusMonsters":-1.0,"BuffedAttackBonus":-1.0,"BuffedMeleeDefenseBonus":-1.0,"BuffedManaCBonus":-1.0}""";
// Weapon profile -> 218103839..842 and 167772171..174. Getters: EquipSkill 45 ->
// "Light Weapons", Mastery 2 -> "Sword", Imbue 1 -> "CS", SkillLevel (158 != 7) 370,
// TotalRating 2. CalcedBuffedTinkedDoT (MyWorldObject.cs:270-302): max damage
// 50 - 24 (active Incantation of Blood Drinker) + 2 (Minor Blood Thirst) = 28, seven
// tinks left, giving 55.431200000000004. BuffedAttackBonus: 1.2000000476837158 + .03
// (Minor Heart Thirst) = 1.2300000476837158.
private const string TachiAppraisedRecord =
"""{"HasIdData":true,"Id":-2147483134,"LastIdTime":""" + IdStamp + ""","ObjectClass":1,"Icon":13162,"ContainerId":1342177281,"BoolValues":{},"DoubleValues":{"5":-0.05,"29":1.15,"167772169":8.0,"167772171":0.5,"167772172":1.2000000476837158,"167772173":1.0,"167772174":1.0},"IntValues":{"5":270,"10":1048576,"19":12000,"105":8,"107":1500,"108":2000,"109":300,"131":64,"158":2,"159":45,"160":370,"171":3,"179":1,"353":2,"370":2,"218103808":30310,"218103809":13162,"218103810":1342177281,"218103822":1048576,"218103823":1,"218103824":1,"218103826":16,"218103834":1,"218103835":67108882,"218103838":2,"218103839":30,"218103840":45,"218103841":16,"218103842":50,"218103843":6291460,"218103848":1,"218103849":6958,"218103850":0},"StringValues":{"1":"Fire Tachi","16":"Fire Tachi of Blood Drinker"},"ActiveSpells":[5183],"Spells":[2598,2603],"Material":"Steel","Name":"Fire Tachi","EquipSkill":"Light Weapons","Mastery":"Sword","ItemSet":null,"ArmorLevel":-1,"Imbue":"CS","Tinks":3,"MaxDamage":50,"ElementalDmgBonus":-1,"Variance":0.5,"DamageBonus":1.0,"ElementalDamageVersusMonsters":-1.0,"AttackBonus":1.2000000476837158,"MeleeDefenseBonus":1.15,"MagicDBonus":-1.0,"MissileDBonus":-1.0,"ManaCBonus":-1.0,"WieldLevel":-1,"SkillLevel":370,"LoreRequirement":300,"SalvageWorkmanship":8.0,"Workmanship":8,"Value":12000,"Burden":270,"DamRating":2,"DamResistRating":-1,"CritRating":-1,"CritResistRating":-1,"CritDamRating":-1,"CritDamResistRating":-1,"HealBoostRating":-1,"VitalityRating":-1,"TotalRating":2,"CalcedStartingArmorLevel":-1,"CalcedBuffedTinkedDoT":55.431200000000004,"CalcedBuffedMissileDamage":-1.0,"BuffedElementalDamageVersusMonsters":-1.0,"BuffedAttackBonus":1.2300000476837158,"BuffedMeleeDefenseBonus":1.15,"BuffedManaCBonus":-1.0}""";
[Fact]
public void Full_inventory_frame_after_the_first_login()
{
using var harness = new PluginHarness("Mossy", arrange: host =>
{
Own(host, Tapers());
Own(host, Breastplate(), BreastplateProps(), hostIdTime: 777);
});
LoginAndSettle(harness);
harness.Tick(0.1); // the login upload: no saved copy, so appraise and wait
harness.Tick(0.1); // nothing needed appraising: send
// WebSocket.cs:341-348: type, timestamp, character_name, item_count, items.
string expected =
"""{"type":"full_inventory","timestamp":"2026-09-25T08:30:15.1230000Z","character_name":"Mossy","item_count":2,"items":["""
+ TaperRecord + "," + BreastplateRecord + "]}";
Assert.Equal([expected], harness.FramesOfTypeRaw("full_inventory"));
}
[Fact]
public void Inventory_delta_add_then_update_after_appraisal()
{
using var harness = new PluginHarness("Mossy");
InventoryFeature feature = LoginAndSettle(harness);
harness.Tick(0.1);
harness.Tick(0.1);
harness.Frames.Clear();
Own(harness.Host, Tachi(appraised: false));
Raise(harness.Host, 0x80000202u, PluginObjectChangeKind.Created);
// WebSocket.cs:356-363: type, timestamp, character_name, action, item.
Assert.Equal(
["""{"type":"inventory_delta","timestamp":"2026-09-25T08:30:15.1230000Z","character_name":"Mossy","action":"add","item":""" + TachiUnappraisedRecord + "}"],
harness.FramesOfTypeRaw("inventory_delta"));
harness.Tick(0.015);
Assert.Contains(0x80000202u, harness.Host.Automation.Objects.IdentifyRequests);
Own(harness.Host, Tachi(appraised: true), TachiProps(), hostIdTime: 5000);
Raise(harness.Host, 0x80000202u, PluginObjectChangeKind.IdentReceived);
harness.Frames.Clear();
feature.FlushDirtyItems();
Assert.Equal(
["""{"type":"inventory_delta","timestamp":"2026-09-25T08:30:15.1230000Z","character_name":"Mossy","action":"update","item":""" + TachiAppraisedRecord + "}"],
harness.FramesOfTypeRaw("inventory_delta"));
}
[Fact]
public void Inventory_delta_remove_carries_only_the_id()
{
using var harness = new PluginHarness("Mossy", arrange: host => Own(host, Tapers()));
LoginAndSettle(harness);
harness.Frames.Clear();
Forget(harness.Host, 0x80000101u);
harness.Host.Events.RaiseObjectChanged(new PluginObjectChange(0x80000101u, PluginObjectChangeKind.Released));
// WebSocket.cs:370-377: the id is the framework's signed 32-bit id.
Assert.Equal(
["""{"type":"inventory_delta","timestamp":"2026-09-25T08:30:15.1230000Z","character_name":"Mossy","action":"remove","item_id":-2147483391}"""],
harness.FramesOfTypeRaw("inventory_delta"));
}
[Fact]
public void Equipment_cantrip_state_for_an_active_weapon_and_an_unappraised_ring()
{
using var harness = new PluginHarness("Mossy", arrange: host =>
{
Own(host, Tachi(appraised: true), TachiProps(), hostIdTime: 5000);
Own(host, Ring());
host.Automation.Spells.All =
[
Spell(2598, family: 100, difficulty: 150),
Spell(2603, family: 101, difficulty: 150),
Spell(5183, family: 200, difficulty: 400),
Spell(6089, family: 100, difficulty: 400),
Spell(6094, family: 101, difficulty: 400),
];
host.Automation.Character.ActiveEnchantments =
[
new PluginActiveEnchantment(6089, 100, 8, double.PositiveInfinity),
new PluginActiveEnchantment(6094, 101, 8, double.PositiveInfinity),
];
});
harness.Login();
harness.Frames.Clear();
// Mana was last reported at login; 65 s later three points have burned at
// one per ceil(-0.2 / -0.05) * 5 = 20 s (EquipmentCantripStateTracker.cs:296-345).
harness.Clock.Advance(TimeSpan.FromSeconds(65));
harness.Feature<EquipmentCantripStateFeature>().SendSnapshot();
// WebSocket.cs:413-419 envelope; items from EquipmentCantripStateTracker.cs:222-229.
Assert.Equal(
["""{"type":"equipment_cantrip_state","timestamp":"2026-09-25T08:31:20.1230000Z","character_name":"Mossy","items":[{"item_id":-2147483134,"state":"active","current_mana":1497,"max_mana":2000,"mana_time_remaining_seconds":29940.0},{"item_id":-2147482620,"state":"unknown","current_mana":null,"max_mana":null,"mana_time_remaining_seconds":null}]}"""],
harness.FramesOfTypeRaw("equipment_cantrip_state"));
}
}