From 8ccaf72ae7d91c1ebafa3939031dc5afb0fa5ad7 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 29 Jul 2026 01:27:57 +0200 Subject: [PATCH] feat(core): adopt the retail members the equipment and physics enums were missing With the two wrong enums corrected, the remaining wire-adjacent families diff cleanly against the retail header - same values everywhere they overlap, just fewer members on our side. This adopts the gaps. EquipMask gains retail's eleven INVENTORY_LOC composite slot groups (acclient.h: 3193). The 32 primitive slots were already exact and stay pinned by EquipMaskTests; what was missing were the groups the wire and the UI actually reason in - Armor, Jewelry, ReadySlot, Weapon, WeaponReadySlot, the wrist/finger/sigil pairs, and All. These are transcribed as literals, not derived, for the reason the previous commit documents at length. That transcription immediately earned itself. A type remark on EquipMask claimed retail's CLOTHING_LOC composite "also sets bit 31, 0x80000000, which is not a named INVENTORY_LOC primitive". It does not. CLOTHING_LOC is 0x080001FF: the nine wear slots plus bit 27, which is the perfectly well-named Cloak slot. No INVENTORY_LOC member touches bit 31 at all - ALL_LOC stops at bit 30. The remark is corrected and a test now asserts the actual decomposition. TransientStateFlags gains WaterContact (0x8) and CheckEthereal (0x100), the two retail bits acdream's transition never declared. Neither is produced or consumed yet; they are named so those slots cannot be quietly reused for an acdream-local flag and then collide. PhysicsStateFlags gains ReservedUnused1 (0x2) and ReservedUnused2 (0x2000), which retail declares as UNUSED1_PS/UNNUSED2_PS. Same reasoning: reserved is a fact worth recording. AttackHeight gains Undef = 0. The three real heights are 1-based and were already right; retail reserves 0 and the wire sends it, so it is now named instead of arriving as an undefined cast. The numeric values are unchanged, so this renames nothing at runtime. Also checked and found already correct, so left alone: ObjectInfoState (matches ObjectInfoEnum exactly, None being DEFAULT_OI), AttackType (every primitive plus both composites - Unarmed 0x19 and MultiStrike 0x79E0 - land on retail's literals), RadarBlipShape, RadarBehavior, MovementType, HoldKey, ParticleType, and PhysicsDescriptionFlag. AttackType is worth calling out because the campaign's extraction tooling reported it as a conflict; the tool reads one line per member and had truncated a multi-line composite. The enum was fine. RetailEnumConformanceTests grows tables for each of the above, each citing its acclient.h line. Core tests 3,785 -> 3,836. Full suite 9,701 passed / 5 skipped, no failures. Co-Authored-By: Claude Fable 5 --- src/AcDream.Core/Combat/CombatModel.cs | 6 + src/AcDream.Core/Items/ClientObject.cs | 23 ++- src/AcDream.Core/Physics/PhysicsBody.cs | 13 ++ .../Properties/RetailEnumConformanceTests.cs | 133 ++++++++++++++++++ 4 files changed, 173 insertions(+), 2 deletions(-) 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); + } }