diff --git a/src/AcDream.Core/Combat/CombatModel.cs b/src/AcDream.Core/Combat/CombatModel.cs
index 2e7b8b5d..3400e4cb 100644
--- a/src/AcDream.Core/Combat/CombatModel.cs
+++ b/src/AcDream.Core/Combat/CombatModel.cs
@@ -22,8 +22,14 @@ public enum CombatMode
CombatCombat = Melee | Missile | Magic,
}
+///
+/// Verbatim retail ATTACK_HEIGHT (docs/research/named-retail/acclient.h:4371).
+/// The three real heights are 1-based; retail reserves 0 for "not chosen yet", which the
+/// wire does send, so it is named rather than left to fall through as an undefined cast.
+///
public enum AttackHeight
{
+ Undef = 0,
High = 1,
Medium = 2,
Low = 3,
diff --git a/src/AcDream.Core/Items/ClientObject.cs b/src/AcDream.Core/Items/ClientObject.cs
index fc8b5a6d..394a1e71 100644
--- a/src/AcDream.Core/Items/ClientObject.cs
+++ b/src/AcDream.Core/Items/ClientObject.cs
@@ -79,8 +79,10 @@ public enum ItemType : uint
/// (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.
-/// (The header's CLOTHING_LOC composite also sets bit 31, 0x80000000, which is
-/// not a named INVENTORY_LOC primitive and has no member here; ALL_LOC tops out at bit 30.)
+/// (A remark here used to claim the header's CLOTHING_LOC composite sets an
+/// unnamed bit 31. It does not: CLOTHING_LOC is 0x080001FF, the nine wear slots
+/// plus bit 27, which is the named slot. ALL_LOC
+/// tops out at bit 30, so no INVENTORY_LOC member uses bit 31 at all.)
///
[Flags]
public enum EquipMask : uint
@@ -117,6 +119,23 @@ public enum EquipMask : uint
SigilOne = 0x10000000,
SigilTwo = 0x20000000,
SigilThree = 0x40000000,
+
+ // Retail's composite slot groups, transcribed verbatim from the same header
+ // block rather than recomputed from the primitives above. Clothing in
+ // particular is not the union of the wear slots — it also carries the cloak
+ // slot (bit 27).
+ Clothing = 0x080001FF, // CLOTHING_LOC: the nine wear slots | Cloak
+ Armor = 0x00007E00, // ARMOR_LOC
+ Jewelry = 0x7C0F8000, // JEWELRY_LOC
+ WristWear = 0x00030000, // WRIST_WEAR_LOC
+ FingerWear = 0x000C0000, // FINGER_WEAR_LOC
+ Sigil = 0x70000000, // SIGIL_LOC
+ ReadySlot = 0x03F00000, // READY_SLOT_LOC
+ Weapon = 0x02500000, // WEAPON_LOC
+ WeaponReadySlot = 0x03500000, // WEAPON_READY_SLOT_LOC
+ All = 0x7FFFFFFF, // ALL_LOC
+ /// Retail gives CAN_GO_IN_READY_SLOT_LOC the same value as ALL_LOC.
+ CanGoInReadySlot = 0x7FFFFFFF,
}
///
diff --git a/src/AcDream.Core/Physics/PhysicsBody.cs b/src/AcDream.Core/Physics/PhysicsBody.cs
index 00cdfb97..acb47ea3 100644
--- a/src/AcDream.Core/Physics/PhysicsBody.cs
+++ b/src/AcDream.Core/Physics/PhysicsBody.cs
@@ -27,6 +27,12 @@ public enum PhysicsStateFlags : uint
{
None = 0x00000000,
Static = 0x00000001,
+ ///
+ /// Retail declares 0x2 and 0x2000 as UNUSED1_PS / UNNUSED2_PS (acclient.h:2818,
+ /// 2830) — reserved, never set. Named here so neither bit gets repurposed for an
+ /// acdream-local flag and then collides with a server that starts using it.
+ ///
+ ReservedUnused1 = 0x00000002,
Ethereal = 0x00000004,
ReportCollisions = 0x00000008,
IgnoreCollisions = 0x00000010,
@@ -38,6 +44,8 @@ public enum PhysicsStateFlags : uint
Gravity = 0x00000400,
Lighting = 0x00000800,
ParticleEmitter = 0x00001000,
+ /// Retail UNNUSED2_PS — reserved, never set. See .
+ ReservedUnused2 = 0x00002000,
Hidden = 0x00004000,
ScriptedCollision = 0x00008000,
///
@@ -88,6 +96,11 @@ public enum TransientStateFlags : uint
StationaryStop = 0x00000020, // bit 5 — fsf == 2
StationaryStuck = 0x00000040, // bit 6 — fsf == 3
Active = 0x00000080, // bit 7 — object needs per-frame update
+ // Declared to complete retail's TransientState (acclient.h:3688). Neither bit is
+ // produced or consumed by acdream's transition yet; they are here so the two free
+ // slots cannot be reused for something else and quietly collide with the wire.
+ WaterContact = 0x00000008, // bit 3 — WATER_CONTACT_TS
+ CheckEthereal = 0x00000100, // bit 8 — CHECK_ETHEREAL_TS
}
///
diff --git a/tests/AcDream.Core.Tests/Properties/RetailEnumConformanceTests.cs b/tests/AcDream.Core.Tests/Properties/RetailEnumConformanceTests.cs
index f5641e9d..f9dfd970 100644
--- a/tests/AcDream.Core.Tests/Properties/RetailEnumConformanceTests.cs
+++ b/tests/AcDream.Core.Tests/Properties/RetailEnumConformanceTests.cs
@@ -2,6 +2,7 @@ using System;
using System.Linq;
using AcDream.Core.Combat;
using AcDream.Core.Items;
+using AcDream.Core.Physics;
using Xunit;
namespace AcDream.Core.Tests.Properties;
@@ -149,4 +150,136 @@ public sealed class RetailEnumConformanceTests
Assert.Equal(ItemType.MeleeWeapon | ItemType.MissileWeapon, ItemType.Weapon);
Assert.Equal(ItemType.Weapon | ItemType.Caster, ItemType.WeaponOrCaster);
}
+
+ ///
+ /// acclient.h:3193, enum INVENTORY_LOC — the composite slot groups only. The
+ /// 32 primitive slots were already correct and are pinned by EquipMaskTests.
+ ///
+ public static TheoryData RetailEquipMaskComposites => new()
+ {
+ { "Clothing", 0x080001FF },
+ { "Armor", 0x00007E00 },
+ { "Jewelry", 0x7C0F8000 },
+ { "WristWear", 0x00030000 },
+ { "FingerWear", 0x000C0000 },
+ { "Sigil", 0x70000000 },
+ { "ReadySlot", 0x03F00000 },
+ { "Weapon", 0x02500000 },
+ { "WeaponReadySlot", 0x03500000 },
+ { "All", 0x7FFFFFFF },
+ { "CanGoInReadySlot", 0x7FFFFFFF },
+ };
+
+ [Theory]
+ [MemberData(nameof(RetailEquipMaskComposites))]
+ public void EquipMaskCompositeMatchesRetail(string name, uint value)
+ {
+ Assert.True(Enum.IsDefined(typeof(EquipMask), name), $"EquipMask.{name} is missing");
+ Assert.Equal(value, (uint)Enum.Parse(name));
+ }
+
+ ///
+ /// Retail's CLOTHING_LOC is the nine wear slots plus the cloak slot (bit 27) — it is
+ /// NOT the union of the wear slots alone. A type remark here used to claim the extra
+ /// bit was 31 and unnamed; it is 27 and it is .
+ ///
+ [Fact]
+ public void ClothingCompositeIsTheWearSlotsPlusCloak()
+ {
+ EquipMask wearSlots =
+ EquipMask.HeadWear | EquipMask.ChestWear | EquipMask.AbdomenWear
+ | EquipMask.UpperArmWear | EquipMask.LowerArmWear | EquipMask.HandWear
+ | EquipMask.UpperLegWear | EquipMask.LowerLegWear | EquipMask.FootWear;
+
+ Assert.Equal(0x000001FFu, (uint)wearSlots);
+ Assert.Equal(EquipMask.Clothing, wearSlots | EquipMask.Cloak);
+ Assert.NotEqual(EquipMask.Clothing, wearSlots);
+ Assert.Equal(0u, (uint)EquipMask.Clothing & 0x80000000u);
+ }
+
+ /// acclient.h:3688, enum TransientState.
+ public static TheoryData RetailTransientState => new()
+ {
+ { "Contact", 0x1 },
+ { "OnWalkable", 0x2 },
+ { "Sliding", 0x4 },
+ { "WaterContact", 0x8 },
+ { "StationaryFall", 0x10 },
+ { "StationaryStop", 0x20 },
+ { "StationaryStuck", 0x40 },
+ { "Active", 0x80 },
+ { "CheckEthereal", 0x100 },
+ };
+
+ [Theory]
+ [MemberData(nameof(RetailTransientState))]
+ public void TransientStateFlagsMatchesRetail(string name, uint value)
+ {
+ Assert.True(Enum.IsDefined(typeof(TransientStateFlags), name),
+ $"TransientStateFlags.{name} is missing");
+ Assert.Equal(value, (uint)Enum.Parse(name));
+ }
+
+ /// acclient.h:2815, enum PhysicsState, including the two bits retail itself reserves.
+ public static TheoryData RetailPhysicsState => new()
+ {
+ { "Static", 0x1 },
+ { "ReservedUnused1", 0x2 },
+ { "Ethereal", 0x4 },
+ { "ReportCollisions", 0x8 },
+ { "IgnoreCollisions", 0x10 },
+ { "NoDraw", 0x20 },
+ { "Missile", 0x40 },
+ { "Pushable", 0x80 },
+ { "AlignPath", 0x100 },
+ { "PathClipped", 0x200 },
+ { "Gravity", 0x400 },
+ { "Lighting", 0x800 },
+ { "ParticleEmitter", 0x1000 },
+ { "ReservedUnused2", 0x2000 },
+ { "Hidden", 0x4000 },
+ { "ScriptedCollision", 0x8000 },
+ { "HasPhysicsBsp", 0x10000 },
+ { "Inelastic", 0x20000 },
+ { "HasDefaultAnim", 0x40000 },
+ { "HasDefaultScript", 0x80000 },
+ { "Cloaked", 0x100000 },
+ { "ReportAsEnvironment", 0x200000 },
+ { "EdgeSlide", 0x400000 },
+ { "Sledding", 0x800000 },
+ { "Frozen", 0x1000000 },
+ };
+
+ [Theory]
+ [MemberData(nameof(RetailPhysicsState))]
+ public void PhysicsStateFlagsMatchesRetail(string name, uint value)
+ {
+ Assert.True(Enum.IsDefined(typeof(PhysicsStateFlags), name),
+ $"PhysicsStateFlags.{name} is missing");
+ Assert.Equal(value, (uint)Enum.Parse(name));
+ }
+
+ /// acclient.h:4371, enum ATTACK_HEIGHT. NUM_ATTACK_HEIGHTS is a count, not a height.
+ [Theory]
+ [InlineData("Undef", 0)]
+ [InlineData("High", 1)]
+ [InlineData("Medium", 2)]
+ [InlineData("Low", 3)]
+ public void AttackHeightMatchesRetail(string name, int value)
+ {
+ Assert.True(Enum.IsDefined(typeof(AttackHeight), name), $"AttackHeight.{name} is missing");
+ Assert.Equal(value, (int)Enum.Parse(name));
+ }
+
+ ///
+ /// acclient.h:3807, enum AttackType. The two composites are the interesting
+ /// part: retail spells Unarmed 0x19 and MultiStrike 0x79E0, and acdream derives both
+ /// from its primitives. This asserts the derivation lands on retail's literal.
+ ///
+ [Fact]
+ public void AttackTypeCompositesMatchRetailLiterals()
+ {
+ Assert.Equal(0x19u, (uint)AttackType.Unarmed);
+ Assert.Equal(0x79E0u, (uint)AttackType.MultiStrike);
+ }
}