diff --git a/src/AcDream.Core/Items/ClientObject.cs b/src/AcDream.Core/Items/ClientObject.cs
index 2ec5d309..cc3695b6 100644
--- a/src/AcDream.Core/Items/ClientObject.cs
+++ b/src/AcDream.Core/Items/ClientObject.cs
@@ -57,46 +57,46 @@ public enum ItemType : uint
}
///
-/// Equipment slot bitmask. 31 slots from head to Aetheria. Paperdoll
-/// widget offsets +0x604..+0x660 in the retail panel correspond
-/// to these bits 1:1 (see r06 §2 and UI slice 05 paperdoll section).
+/// Equipment slot bitmask — the verbatim retail INVENTORY_LOC enum
+/// (docs/research/named-retail/acclient.h:3193; identical to ACE's EquipMask).
+/// The wire (ValidLocations / CurrentWieldedLocation / WieldObject EquipLoc) delivers
+/// these exact bits. Pinned by EquipMaskTests — do NOT renumber.
///
[Flags]
public enum EquipMask : uint
{
- None = 0,
- HeadWear = 0x00000001,
- ChestWear = 0x00000002,
- AbdomenWear = 0x00000004,
- UpperArmWear = 0x00000008,
- LowerArmWear = 0x00000010,
- HandWear = 0x00000020,
- UpperLegWear = 0x00000040,
- LowerLegWear = 0x00000080,
- FootWear = 0x00000100,
- ChestArmor = 0x00000200,
- AbdomenArmor = 0x00000400,
- UpperArmArmor = 0x00000800,
- LowerArmArmor = 0x00001000,
- HandArmor = 0x00002000,
- UpperLegArmor = 0x00004000,
- LowerLegArmor = 0x00008000,
- FootArmor = 0x00010000,
- Necklace = 0x00020000,
- LeftBracelet = 0x00040000,
- RightBracelet = 0x00080000,
- LeftRing = 0x00100000,
- RightRing = 0x00200000,
- MeleeWeapon = 0x00400000,
- Shield = 0x00800000,
- MissileWeapon = 0x01000000,
- Held = 0x02000000, // lit torch, book in hand
- MissileAmmo = 0x04000000,
- Cloak = 0x08000000,
- TrinketOne = 0x10000000,
- AetheriaRed = 0x20000000,
- AetheriaYellow= 0x40000000,
- AetheriaBlue = 0x80000000u,
+ None = 0x00000000,
+ HeadWear = 0x00000001,
+ ChestWear = 0x00000002,
+ AbdomenWear = 0x00000004,
+ UpperArmWear = 0x00000008,
+ LowerArmWear = 0x00000010,
+ HandWear = 0x00000020,
+ UpperLegWear = 0x00000040,
+ LowerLegWear = 0x00000080,
+ FootWear = 0x00000100,
+ ChestArmor = 0x00000200,
+ AbdomenArmor = 0x00000400,
+ UpperArmArmor = 0x00000800,
+ LowerArmArmor = 0x00001000,
+ UpperLegArmor = 0x00002000,
+ LowerLegArmor = 0x00004000,
+ NeckWear = 0x00008000,
+ WristWearLeft = 0x00010000,
+ WristWearRight = 0x00020000,
+ FingerWearLeft = 0x00040000,
+ FingerWearRight= 0x00080000,
+ MeleeWeapon = 0x00100000,
+ Shield = 0x00200000,
+ MissileWeapon = 0x00400000,
+ MissileAmmo = 0x00800000,
+ Held = 0x01000000,
+ TwoHanded = 0x02000000,
+ TrinketOne = 0x04000000,
+ Cloak = 0x08000000,
+ SigilOne = 0x10000000,
+ SigilTwo = 0x20000000,
+ SigilThree = 0x40000000,
}
///
diff --git a/tests/AcDream.Core.Tests/Items/EquipMaskTests.cs b/tests/AcDream.Core.Tests/Items/EquipMaskTests.cs
new file mode 100644
index 00000000..d3576603
--- /dev/null
+++ b/tests/AcDream.Core.Tests/Items/EquipMaskTests.cs
@@ -0,0 +1,54 @@
+using AcDream.Core.Items;
+using Xunit;
+
+namespace AcDream.Core.Tests.Items;
+
+///
+/// Pins every EquipMask member to the verbatim retail INVENTORY_LOC value
+/// (docs/research/named-retail/acclient.h:3193). The wire delivers these exact bits
+/// (ACE EquipMask == retail INVENTORY_LOC); any drift desyncs the paperdoll element-id→mask
+/// map and the GetAndWieldItem wire. This test is the anti-regression lock.
+///
+public sealed class EquipMaskTests
+{
+ [Theory]
+ [InlineData(0x00000000u, EquipMask.None)]
+ [InlineData(0x00000001u, EquipMask.HeadWear)]
+ [InlineData(0x00000002u, EquipMask.ChestWear)]
+ [InlineData(0x00000004u, EquipMask.AbdomenWear)]
+ [InlineData(0x00000008u, EquipMask.UpperArmWear)]
+ [InlineData(0x00000010u, EquipMask.LowerArmWear)]
+ [InlineData(0x00000020u, EquipMask.HandWear)]
+ [InlineData(0x00000040u, EquipMask.UpperLegWear)]
+ [InlineData(0x00000080u, EquipMask.LowerLegWear)]
+ [InlineData(0x00000100u, EquipMask.FootWear)]
+ [InlineData(0x00000200u, EquipMask.ChestArmor)]
+ [InlineData(0x00000400u, EquipMask.AbdomenArmor)]
+ [InlineData(0x00000800u, EquipMask.UpperArmArmor)]
+ [InlineData(0x00001000u, EquipMask.LowerArmArmor)]
+ [InlineData(0x00002000u, EquipMask.UpperLegArmor)]
+ [InlineData(0x00004000u, EquipMask.LowerLegArmor)]
+ [InlineData(0x00008000u, EquipMask.NeckWear)]
+ [InlineData(0x00010000u, EquipMask.WristWearLeft)]
+ [InlineData(0x00020000u, EquipMask.WristWearRight)]
+ [InlineData(0x00040000u, EquipMask.FingerWearLeft)]
+ [InlineData(0x00080000u, EquipMask.FingerWearRight)]
+ [InlineData(0x00100000u, EquipMask.MeleeWeapon)]
+ [InlineData(0x00200000u, EquipMask.Shield)]
+ [InlineData(0x00400000u, EquipMask.MissileWeapon)]
+ [InlineData(0x00800000u, EquipMask.MissileAmmo)]
+ [InlineData(0x01000000u, EquipMask.Held)]
+ [InlineData(0x02000000u, EquipMask.TwoHanded)]
+ [InlineData(0x04000000u, EquipMask.TrinketOne)]
+ [InlineData(0x08000000u, EquipMask.Cloak)]
+ [InlineData(0x10000000u, EquipMask.SigilOne)]
+ [InlineData(0x20000000u, EquipMask.SigilTwo)]
+ [InlineData(0x40000000u, EquipMask.SigilThree)]
+ public void Member_has_canonical_retail_value(uint expected, EquipMask member)
+ => Assert.Equal(expected, (uint)member);
+
+ [Fact]
+ public void Weapon_ready_slot_composite_is_0x3500000() // acclient.h:3235 WEAPON_READY_SLOT_LOC
+ => Assert.Equal(0x3500000u,
+ (uint)(EquipMask.MeleeWeapon | EquipMask.MissileWeapon | EquipMask.Held | EquipMask.TwoHanded));
+}