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); + } }