From 251dd68a92484918556f4feeb26af1fc949b6700 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 29 Jul 2026 01:11:16 +0200 Subject: [PATCH 1/5] feat(core): give AC's seven property tables names, verified against two oracles acdream has carried property IDs as bare uints since the beginning. The wire parsers read `u32 property` and hand it to a `Dictionary`, and every call site that cared re-derived the meaning from a comment - `EncumbranceVal` was spelled `private const uint EncumbranceValProperty = 5u` in two different files, `UiEffects` lived as "ACE enum value 18" in a doc comment, and `AetheriaBitfield` as "322 / 0x142". That is 864 pieces of vocabulary the codebase was expected to remember in prose. This adds the seven enums - PropertyInt, PropertyInt64, PropertyBool, PropertyFloat, PropertyString, PropertyDataId, PropertyInstanceId - under AcDream.Core.Properties. Every member is transcribed from an oracle; none is invented. Two independent sources were extracted and diffed against each other: the vendored client-side enum catalog at references/acclientlib/UtilityBelt.Common/Enums/Enums.cs (which names these tables IntId/BoolId/FloatId/...), and the 38,985-file ACE weenie export corpus at references/weenies/, whose every stat entry carries the numeric key beside the enum member name in its `_comment`. The corpus attests 408 of the 864 members directly. Across all seven tables the two oracles produced zero value conflicts, and the corpus contained no key the catalog was missing - the catalog is a strict superset of everything 38,985 weenies actually set. Three members disagree on spelling, never on value: the catalog says ObjectType/HookObjectType/MerchandiseObjectTypes where ACE says ItemType/HookItemType/MerchandiseItemTypes. acdream takes ACE's spelling, which is what the weenie corpus emits (37,329 attestations for ItemType alone) and what acdream's own ItemType enum already calls it. The catalog's alias is recorded on each member. This commit is vocabulary only - no parser reads these enums yet, so no branch changes and no wire behavior moves. The bundles stay `Dictionary` precisely because an unknown key must still round-trip untouched; the enums describe the keys we know, they do not constrain the ones we receive. PropertyEnumConformanceTests pins the result: the full name/value table per family, the uint underlying type, no two members sharing a value, and a separate 408-case theory asserting each weenie-attested pairing individually. A hand edit to any enum now fails loudly instead of quietly mis-reading the wire. Core tests 3,297 -> 3,726. Co-Authored-By: Claude Fable 5 --- src/AcDream.Core/Properties/PropertyBool.cs | 221 +++ src/AcDream.Core/Properties/PropertyDataId.cs | 134 ++ src/AcDream.Core/Properties/PropertyFloat.cs | 289 ++++ .../Properties/PropertyInstanceId.cs | 79 + src/AcDream.Core/Properties/PropertyInt.cs | 611 +++++++ src/AcDream.Core/Properties/PropertyInt64.cs | 43 + src/AcDream.Core/Properties/PropertyString.cs | 112 ++ .../PropertyEnumConformanceTests.cs | 1398 +++++++++++++++++ 8 files changed, 2887 insertions(+) create mode 100644 src/AcDream.Core/Properties/PropertyBool.cs create mode 100644 src/AcDream.Core/Properties/PropertyDataId.cs create mode 100644 src/AcDream.Core/Properties/PropertyFloat.cs create mode 100644 src/AcDream.Core/Properties/PropertyInstanceId.cs create mode 100644 src/AcDream.Core/Properties/PropertyInt.cs create mode 100644 src/AcDream.Core/Properties/PropertyInt64.cs create mode 100644 src/AcDream.Core/Properties/PropertyString.cs create mode 100644 tests/AcDream.Core.Tests/Properties/PropertyEnumConformanceTests.cs diff --git a/src/AcDream.Core/Properties/PropertyBool.cs b/src/AcDream.Core/Properties/PropertyBool.cs new file mode 100644 index 00000000..3989866d --- /dev/null +++ b/src/AcDream.Core/Properties/PropertyBool.cs @@ -0,0 +1,221 @@ +// +// Regenerate with the 2026-07-29 enum verification campaign tooling; see +// docs/research/2026-07-29-enum-verification-campaign.md for the oracle set, +// the extraction method, and the open-question ledger. +// Hand edits are pinned by PropertyEnumConformanceTests — change the oracle, +// not the enum. +// + +namespace AcDream.Core.Properties; + +/// +/// AC's PropertyBool property table — the numeric keys the server sends in +/// PrivateUpdatePropertyBool (0x02C9) / PublicUpdatePropertyBool (0x02CA) +/// and in the property bundles carried by CreateObject / PlayerDescription / +/// IdentifyResponse. The CLR payload for this table is bool. +/// +/// Verified 2026-07-29 against two independent oracles: the vendored +/// client-side enum catalog (references/acclientlib/UtilityBelt.Common/Enums/Enums.cs, +/// which names this table BoolId) and the 38,985-file ACE weenie export corpus +/// (references/weenies/), whose per-stat _comment carries the enum member +/// name beside the numeric key. 59 of these 131 members are attested by BOTH +/// oracles with zero value conflicts; the remainder are single-sourced from the +/// catalog because no weenie in the corpus sets them. +/// +/// This enum is a vocabulary, not a parser contract: the wire carries a raw +/// uint and unknown keys must still round-trip untouched, so never assume a +/// received key is a defined member. +/// +public enum PropertyBool : uint +{ + Undef = 0, + /// 21884 weenie attestations. + Stuck = 1, + /// 1077 weenie attestations. + Open = 2, + /// 646 weenie attestations. + Locked = 3, + RotProof = 4, + AllegianceUpdateRequest = 5, + /// 1282 weenie attestations. + AiUsesMana = 6, + /// 16 weenie attestations. + AiUseHumanMagicAnimations = 7, + /// 853 weenie attestations. + AllowGive = 8, + CurrentlyAttacking = 9, + AttackerAi = 10, + /// 6212 weenie attestations. + IgnoreCollisions = 11, + /// 9020 weenie attestations. + ReportCollisions = 12, + /// 15282 weenie attestations. + Ethereal = 13, + /// 7376 weenie attestations. + GravityStatus = 14, + /// 3382 weenie attestations. + LightsStatus = 15, + /// 139 weenie attestations. + ScriptedCollision = 16, + /// 475 weenie attestations. + Inelastic = 17, + /// 1677 weenie attestations. + Visibility = 18, + /// 3972 weenie attestations. + Attackable = 19, + SafeSpellComponents = 20, + AdvocateState = 21, + /// 15455 weenie attestations. + Inscribable = 22, + /// 7755 weenie attestations. + DestroyOnSell = 23, + /// 6728 weenie attestations. + UiHidden = 24, + IgnoreHouseBarriers = 25, + HiddenAdmin = 26, + PkWounder = 27, + PkKiller = 28, + /// 262 weenie attestations. + NoCorpse = 29, + UnderLifestoneProtection = 30, + ItemManaUpdatePending = 31, + GeneratorStatus = 32, + /// 831 weenie attestations. + ResetMessagePending = 33, + /// 831 weenie attestations. + DefaultOpen = 34, + /// 511 weenie attestations. + DefaultLocked = 35, + DefaultOn = 36, + OpenForBusiness = 37, + IsFrozen = 38, + /// 1049 weenie attestations. + DealMagicalItems = 39, + LogoffImDead = 40, + /// 2208 weenie attestations. + ReportCollisionsAsEnvironment = 41, + /// 1327 weenie attestations. + AllowEdgeSlide = 42, + AdvocateQuest = 43, + IsAdmin = 44, + IsArch = 45, + IsSentinel = 46, + IsAdvocate = 47, + CurrentlyPoweringUp = 48, + GeneratorEnteredWorld = 49, + /// 890 weenie attestations. + NeverFailCasting = 50, + /// 264 weenie attestations. + VendorService = 51, + /// 1421 weenie attestations. + AiImmobile = 52, + /// 1 weenie attestations. + DamagedByCollisions = 53, + /// 34 weenie attestations. + IsDynamic = 54, + /// 81 weenie attestations. + IsHot = 55, + IsAffecting = 56, + /// 49 weenie attestations. + AffectsAis = 57, + SpellQueueActive = 58, + GeneratorDisabled = 59, + IsAcceptingTells = 60, + /// 1 weenie attestations. + LoggingChannel = 61, + /// 5 weenie attestations. + OpensAnyLock = 62, + /// 20 weenie attestations. + UnlimitedUse = 63, + GeneratedTreasureItem = 64, + /// 97 weenie attestations. + IgnoreMagicResist = 65, + /// 87 weenie attestations. + IgnoreMagicArmor = 66, + AiAllowTrade = 67, + /// 2 weenie attestations. + SpellComponentsRequired = 68, + /// 4617 weenie attestations. + IsSellable = 69, + /// 4 weenie attestations. + IgnoreShieldsBySkill = 70, + /// 6275 weenie attestations. + NoDraw = 71, + ActivationUntargeted = 72, + HouseHasGottenPriorityBootPos = 73, + /// 12 weenie attestations. + GeneratorAutomaticDestruction = 74, + HouseHooksVisible = 75, + /// 8 weenie attestations. + HouseRequiresMonarch = 76, + HouseHooksEnabled = 77, + HouseNotifiedHudOfHookCount = 78, + /// 15 weenie attestations. + AiAcceptEverything = 79, + IgnorePortalRestrictions = 80, + /// 5 weenie attestations. + RequiresBackpackSlot = 81, + /// 444 weenie attestations. + DontTurnOrMoveWhenGiving = 82, + /// 442 weenie attestations. + NpcLooksLikeObject = 83, + /// 210 weenie attestations. + IgnoreCloIcons = 84, + /// 178 weenie attestations. + AppraisalHasAllowedWielder = 85, + ChestRegenOnClose = 86, + LogoffInMinigame = 87, + /// 17 weenie attestations. + PortalShowDestination = 88, + /// 30 weenie attestations. + PortalIgnoresPkAttackTimer = 89, + /// 43 weenie attestations. + NpcInteractsSilently = 90, + /// 230 weenie attestations. + Retained = 91, + /// 1 weenie attestations. + IgnoreAuthor = 92, + Limbo = 93, + AppraisalHasAllowedActivator = 94, + ExistedBeforeAllegianceXpChanges = 95, + IsDeaf = 96, + IsPsr = 97, + Invincible = 98, + /// 1068 weenie attestations. + Ivoryable = 99, + /// 687 weenie attestations. + Dyable = 100, + CanGenerateRare = 101, + CorpseGeneratedRare = 102, + /// 14 weenie attestations. + NonProjectileMagicImmune = 103, + ActdReceivedItems = 104, + Unknown105 = 105, + FirstEnterWorldDone = 106, + RecallsDisabled = 107, + /// 13 weenie attestations. + RareUsesTimer = 108, + ActdPreorderReceivedItems = 109, + Afk = 110, + IsGagged = 111, + ProcSpellSelfTargeted = 112, + IsAllegianceGagged = 113, + EquipmentSetTriggerPiece = 114, + Uninscribe = 115, + WieldOnUse = 116, + ChestClearedWhenClosed = 117, + NeverAttack = 118, + SuppressGenerateEffect = 119, + TreasureCorpse = 120, + EquipmentSetAddLevel = 121, + BarberActive = 122, + TopLayerPriority = 123, + NoHeldItemShown = 124, + LoginAtLifestone = 125, + OlthoiPk = 126, + Account15Days = 127, + HadNoVitae = 128, + NoOlthoiTalk = 129, + /// 14 weenie attestations. + AutowieldLeft = 130, +} diff --git a/src/AcDream.Core/Properties/PropertyDataId.cs b/src/AcDream.Core/Properties/PropertyDataId.cs new file mode 100644 index 00000000..2d22b902 --- /dev/null +++ b/src/AcDream.Core/Properties/PropertyDataId.cs @@ -0,0 +1,134 @@ +// +// Regenerate with the 2026-07-29 enum verification campaign tooling; see +// docs/research/2026-07-29-enum-verification-campaign.md for the oracle set, +// the extraction method, and the open-question ledger. +// Hand edits are pinned by PropertyEnumConformanceTests — change the oracle, +// not the enum. +// + +namespace AcDream.Core.Properties; + +/// +/// AC's PropertyDataId property table — the numeric keys the server sends in +/// PrivateUpdatePropertyDataId (0x02E1) / PublicUpdatePropertyDataId (0x02E2) +/// and in the property bundles carried by CreateObject / PlayerDescription / +/// IdentifyResponse. The CLR payload for this table is uint (DAT file id). +/// +/// Verified 2026-07-29 against two independent oracles: the vendored +/// client-side enum catalog (references/acclientlib/UtilityBelt.Common/Enums/Enums.cs, +/// which names this table DataId) and the 38,985-file ACE weenie export corpus +/// (references/weenies/), whose per-stat _comment carries the enum member +/// name beside the numeric key. 41 of these 62 members are attested by BOTH +/// oracles with zero value conflicts; the remainder are single-sourced from the +/// catalog because no weenie in the corpus sets them. +/// +/// This enum is a vocabulary, not a parser contract: the wire carries a raw +/// uint and unknown keys must still round-trip untouched, so never assume a +/// received key is a defined member. +/// +public enum PropertyDataId : uint +{ + Undef = 0, + /// 38985 weenie attestations. + Setup = 1, + /// 12219 weenie attestations. + MotionTable = 2, + /// 23696 weenie attestations. + SoundTable = 3, + /// 4319 weenie attestations. + CombatTable = 4, + /// 2 weenie attestations. + QualityFilter = 5, + /// 13875 weenie attestations. + PaletteBase = 6, + /// 9049 weenie attestations. + ClothingBase = 7, + /// 38985 weenie attestations. + Icon = 8, + /// 741 weenie attestations. + EyesTexture = 9, + /// 741 weenie attestations. + NoseTexture = 10, + /// 741 weenie attestations. + MouthTexture = 11, + DefaultEyesTexture = 12, + DefaultNoseTexture = 13, + DefaultMouthTexture = 14, + /// 741 weenie attestations. + HairPalette = 15, + /// 741 weenie attestations. + EyesPalette = 16, + /// 741 weenie attestations. + SkinPalette = 17, + HeadObject = 18, + /// 4 weenie attestations. + ActivationAnimation = 19, + /// 4 weenie attestations. + InitMotion = 20, + ActivationSound = 21, + /// 23266 weenie attestations. + PhysicsEffectTable = 22, + /// 138 weenie attestations. + UseSound = 23, + /// 48 weenie attestations. + UseTargetAnimation = 24, + /// 4 weenie attestations. + UseTargetSuccessAnimation = 25, + /// 2 weenie attestations. + UseTargetFailureAnimation = 26, + /// 232 weenie attestations. + UseUserAnimation = 27, + /// 3696 weenie attestations. + Spell = 28, + /// 156 weenie attestations. + SpellComponent = 29, + /// 794 weenie attestations. + PhysicsScript = 30, + /// 497 weenie attestations. + LinkedPortalOne = 31, + /// 861 weenie attestations. + WieldedTreasureType = 32, + /// 5 weenie attestations. + UnknownGuessedname = 33, + UnknownGuessedname2 = 34, + /// 2000 weenie attestations. + DeathTreasureType = 35, + /// 2276 weenie attestations. + MutateFilter = 36, + /// 1253 weenie attestations. + ItemSkillLimit = 37, + /// 1 weenie attestations. + UseCreateItem = 38, + DeathSpell = 39, + VendorsClassId = 40, + /// 13 weenie attestations. + ItemSpecializedOnly = 41, + /// 6274 weenie attestations. + HouseId = 42, + AccountHouseId = 43, + /// 6274 weenie attestations. + RestrictionEffect = 44, + CreationMutationFilter = 45, + /// 584 weenie attestations. + TsysMutationFilter = 46, + LastPortal = 47, + /// 1 weenie attestations. + LinkedPortalTwo = 48, + OriginalPortal = 49, + /// 1000 weenie attestations. + IconOverlay = 50, + /// 16 weenie attestations. + IconOverlaySecondary = 51, + /// 554 weenie attestations. + IconUnderlay = 52, + AugmentationMutationFilter = 53, + AugmentationEffect = 54, + /// 84 weenie attestations. + ProcSpell = 55, + AugmentationCreateItem = 56, + AlternateCurrency = 57, + BlueSurgeSpell = 58, + YellowSurgeSpell = 59, + RedSurgeSpell = 60, + OlthoiDeathTreasureType = 61, +} diff --git a/src/AcDream.Core/Properties/PropertyFloat.cs b/src/AcDream.Core/Properties/PropertyFloat.cs new file mode 100644 index 00000000..e58caebb --- /dev/null +++ b/src/AcDream.Core/Properties/PropertyFloat.cs @@ -0,0 +1,289 @@ +// +// Regenerate with the 2026-07-29 enum verification campaign tooling; see +// docs/research/2026-07-29-enum-verification-campaign.md for the oracle set, +// the extraction method, and the open-question ledger. +// Hand edits are pinned by PropertyEnumConformanceTests — change the oracle, +// not the enum. +// + +namespace AcDream.Core.Properties; + +/// +/// AC's PropertyFloat property table — the numeric keys the server sends in +/// PrivateUpdatePropertyFloat (0x02D5) / PublicUpdatePropertyFloat (0x02D6) +/// and in the property bundles carried by CreateObject / PlayerDescription / +/// IdentifyResponse. The CLR payload for this table is double. +/// +/// Verified 2026-07-29 against two independent oracles: the vendored +/// client-side enum catalog (references/acclientlib/UtilityBelt.Common/Enums/Enums.cs, +/// which names this table FloatId) and the 38,985-file ACE weenie export corpus +/// (references/weenies/), whose per-stat _comment carries the enum member +/// name beside the numeric key. 86 of these 172 members are attested by BOTH +/// oracles with zero value conflicts; the remainder are single-sourced from the +/// catalog because no weenie in the corpus sets them. +/// +/// This enum is a vocabulary, not a parser contract: the wire carries a raw +/// uint and unknown keys must still round-trip untouched, so never assume a +/// received key is a defined member. +/// +public enum PropertyFloat : uint +{ + Undef = 0, + /// 4243 weenie attestations. + HeartbeatInterval = 1, + /// 4243 weenie attestations. + HeartbeatTimestamp = 2, + /// 4361 weenie attestations. + HealthRate = 3, + /// 4361 weenie attestations. + StaminaRate = 4, + /// 7706 weenie attestations. + ManaRate = 5, + /// 79 weenie attestations. + HealthUponResurrection = 6, + /// 79 weenie attestations. + StaminaUponResurrection = 7, + /// 79 weenie attestations. + ManaUponResurrection = 8, + StartTime = 9, + StopTime = 10, + /// 1749 weenie attestations. + ResetInterval = 11, + /// 3290 weenie attestations. + Shade = 12, + /// 6001 weenie attestations. + ArmorModVsSlash = 13, + /// 6005 weenie attestations. + ArmorModVsPierce = 14, + /// 6005 weenie attestations. + ArmorModVsBludgeon = 15, + /// 6005 weenie attestations. + ArmorModVsCold = 16, + /// 6005 weenie attestations. + ArmorModVsFire = 17, + /// 6005 weenie attestations. + ArmorModVsAcid = 18, + /// 6005 weenie attestations. + ArmorModVsElectric = 19, + CombatSpeed = 20, + /// 2733 weenie attestations. + WeaponLength = 21, + /// 3174 weenie attestations. + DamageVariance = 22, + CurrentPowerMod = 23, + AccuracyMod = 24, + StrengthMod = 25, + /// 1386 weenie attestations. + MaximumVelocity = 26, + /// 233 weenie attestations. + RotationSpeed = 27, + MotionTimestamp = 28, + /// 3892 weenie attestations. + WeaponDefense = 29, + WimpyLevel = 30, + /// 2169 weenie attestations. + VisualAwarenessRange = 31, + AuralAwarenessRange = 32, + PerceptionLevel = 33, + /// 1987 weenie attestations. + PowerupTime = 34, + MaxChargeDistance = 35, + /// 1892 weenie attestations. + ChargeSpeed = 36, + /// 1049 weenie attestations. + BuyPrice = 37, + /// 1049 weenie attestations. + SellPrice = 38, + /// 18233 weenie attestations. + DefaultScale = 39, + /// 10 weenie attestations. + LockpickMod = 40, + /// 2195 weenie attestations. + RegenerationInterval = 41, + RegenerationTimestamp = 42, + /// 2135 weenie attestations. + GeneratorRadius = 43, + /// 66 weenie attestations. + TimeToRot = 44, + DeathTimestamp = 45, + PkTimestamp = 46, + VictimTimestamp = 47, + LoginTimestamp = 48, + CreationTimestamp = 49, + /// 4 weenie attestations. + MinimumTimeSincePk = 50, + DeprecatedHousekeepingPriority = 51, + AbuseLoggingTimestamp = 52, + LastPortalTeleportTimestamp = 53, + /// 9152 weenie attestations. + UseRadius = 54, + /// 46 weenie attestations. + HomeRadius = 55, + ReleasedTimestamp = 56, + MinHomeRadius = 57, + Facing = 58, + ResetTimestamp = 59, + LogoffTimestamp = 60, + EconRecoveryInterval = 61, + /// 3523 weenie attestations. + WeaponOffense = 62, + /// 1223 weenie attestations. + DamageMod = 63, + /// 4361 weenie attestations. + ResistSlash = 64, + /// 4361 weenie attestations. + ResistPierce = 65, + /// 4361 weenie attestations. + ResistBludgeon = 66, + /// 4361 weenie attestations. + ResistFire = 67, + /// 4361 weenie attestations. + ResistCold = 68, + /// 4361 weenie attestations. + ResistAcid = 69, + /// 4361 weenie attestations. + ResistElectric = 70, + /// 4361 weenie attestations. + ResistHealthBoost = 71, + /// 4361 weenie attestations. + ResistStaminaDrain = 72, + /// 4361 weenie attestations. + ResistStaminaBoost = 73, + /// 4361 weenie attestations. + ResistManaDrain = 74, + /// 4361 weenie attestations. + ResistManaBoost = 75, + /// 794 weenie attestations. + Translucency = 76, + /// 269 weenie attestations. + PhysicsScriptIntensity = 77, + /// 761 weenie attestations. + Friction = 78, + /// 761 weenie attestations. + Elasticity = 79, + /// 1076 weenie attestations. + AiUseMagicDelay = 80, + ItemMinSpellcraftMod = 81, + ItemMaxSpellcraftMod = 82, + ItemRankProbability = 83, + Shade2 = 84, + Shade3 = 85, + Shade4 = 86, + /// 15 weenie attestations. + ItemEfficiency = 87, + ItemManaUpdateTimestamp = 88, + SpellGestureSpeedMod = 89, + SpellStanceSpeedMod = 90, + AllegianceAppraisalTimestamp = 91, + PowerLevel = 92, + AccuracyLevel = 93, + AttackAngle = 94, + AttackTimestamp = 95, + CheckpointTimestamp = 96, + SoldTimestamp = 97, + UseTimestamp = 98, + UseLockTimestamp = 99, + /// 22 weenie attestations. + HealkitMod = 100, + FrozenTimestamp = 101, + HealthRateMod = 102, + AllegianceSwearTimestamp = 103, + /// 4361 weenie attestations. + ObviousRadarRange = 104, + /// 81 weenie attestations. + HotspotCycleTime = 105, + /// 74 weenie attestations. + HotspotCycleTimeVariance = 106, + SpamTimestamp = 107, + SpamRate = 108, + /// 26 weenie attestations. + BondWieldedTreasure = 109, + /// 1039 weenie attestations. + BulkMod = 110, + /// 1039 weenie attestations. + SizeMod = 111, + GagTimestamp = 112, + GeneratorUpdateTimestamp = 113, + DeathSpamTimestamp = 114, + DeathSpamRate = 115, + WildAttackProbability = 116, + /// 312 weenie attestations. + FocusedProbability = 117, + CrashAndTurnProbability = 118, + CrashAndTurnRadius = 119, + CrashAndTurnBias = 120, + /// 186 weenie attestations. + GeneratorInitialDelay = 121, + /// 575 weenie attestations. + AiAcquireHealth = 122, + /// 6 weenie attestations. + AiAcquireStamina = 123, + /// 6 weenie attestations. + AiAcquireMana = 124, + /// 4361 weenie attestations. + ResistHealthDrain = 125, + LifestoneProtectionTimestamp = 126, + /// 72 weenie attestations. + AiCounteractEnchantment = 127, + /// 10 weenie attestations. + AiDispelEnchantment = 128, + TradeTimestamp = 129, + AiTargetedDetectionRadius = 130, + /// 29 weenie attestations. + EmotePriority = 131, + LastTeleportStartTimestamp = 132, + EventSpamTimestamp = 133, + EventSpamRate = 134, + /// 5 weenie attestations. + InventoryOffset = 135, + /// 396 weenie attestations. + CriticalMultiplier = 136, + /// 15 weenie attestations. + ManaStoneDestroyChance = 137, + /// 351 weenie attestations. + SlayerDamageBonus = 138, + AllegianceInfoSpamTimestamp = 139, + AllegianceInfoSpamRate = 140, + NextSpellcastTimestamp = 141, + AppraisalRequestedTimestamp = 142, + AppraisalHeartbeatDueTimestamp = 143, + /// 209 weenie attestations. + ManaConversionMod = 144, + LastPkAttackTimestamp = 145, + FellowshipUpdateTimestamp = 146, + /// 395 weenie attestations. + CriticalFrequency = 147, + LimboStartTimestamp = 148, + /// 356 weenie attestations. + WeaponMissileDefense = 149, + /// 368 weenie attestations. + WeaponMagicDefense = 150, + /// 87 weenie attestations. + IgnoreShield = 151, + /// 135 weenie attestations. + ElementalDamageMod = 152, + StartMissileAttackTimestamp = 153, + LastRareUsedTimestamp = 154, + /// 69 weenie attestations. + IgnoreArmor = 155, + ProcSpellRate = 156, + /// 93 weenie attestations. + ResistanceModifier = 157, + AllegianceGagTimestamp = 158, + /// 18 weenie attestations. + AbsorbMagicDamage = 159, + CachedMaxAbsorbMagicDamage = 160, + GagDuration = 161, + AllegianceGagDuration = 162, + GlobalXpMod = 163, + HealingModifier = 164, + /// 391 weenie attestations. + ArmorModVsNether = 165, + ResistNether = 166, + /// 369 weenie attestations. + CooldownDuration = 167, + WeaponAuraOffense = 168, + WeaponAuraDefense = 169, + WeaponAuraElemental = 170, + WeaponAuraManaConv = 171, +} diff --git a/src/AcDream.Core/Properties/PropertyInstanceId.cs b/src/AcDream.Core/Properties/PropertyInstanceId.cs new file mode 100644 index 00000000..c386c1ed --- /dev/null +++ b/src/AcDream.Core/Properties/PropertyInstanceId.cs @@ -0,0 +1,79 @@ +// +// Regenerate with the 2026-07-29 enum verification campaign tooling; see +// docs/research/2026-07-29-enum-verification-campaign.md for the oracle set, +// the extraction method, and the open-question ledger. +// Hand edits are pinned by PropertyEnumConformanceTests — change the oracle, +// not the enum. +// + +namespace AcDream.Core.Properties; + +/// +/// AC's PropertyInstanceId property table — the numeric keys the server sends in +/// PrivateUpdatePropertyInstanceId (0x02E5) / PublicUpdatePropertyInstanceId (0x02E6) +/// and in the property bundles carried by CreateObject / PlayerDescription / +/// IdentifyResponse. The CLR payload for this table is uint (object guid). +/// +/// Verified 2026-07-29 against two independent oracles: the vendored +/// client-side enum catalog (references/acclientlib/UtilityBelt.Common/Enums/Enums.cs, +/// which names this table InstanceId) and the 38,985-file ACE weenie export corpus +/// (references/weenies/), whose per-stat _comment carries the enum member +/// name beside the numeric key. 2 of these 46 members are attested by BOTH +/// oracles with zero value conflicts; the remainder are single-sourced from the +/// catalog because no weenie in the corpus sets them. +/// +/// This enum is a vocabulary, not a parser contract: the wire carries a raw +/// uint and unknown keys must still round-trip untouched, so never assume a +/// received key is a defined member. +/// +public enum PropertyInstanceId : uint +{ + Undef = 0, + Owner = 1, + Container = 2, + Wielder = 3, + Freezer = 4, + Viewer = 5, + Generator = 6, + Scribe = 7, + CurrentCombatTarget = 8, + CurrentEnemy = 9, + ProjectileLauncher = 10, + CurrentAttacker = 11, + CurrentDamager = 12, + CurrentFollowTarget = 13, + CurrentAppraisalTarget = 14, + CurrentFellowshipAppraisalTarget = 15, + /// 130 weenie attestations. + ActivationTarget = 16, + Creator = 17, + Victim = 18, + Killer = 19, + Vendor = 20, + Customer = 21, + Bonded = 22, + Wounder = 23, + Allegiance = 24, + Patron = 25, + Monarch = 26, + CombatTarget = 27, + HealthQueryTarget = 28, + LastUnlocker = 29, + CrashAndTurnTarget = 30, + AllowedActivator = 31, + HouseOwner = 32, + House = 33, + Slumlord = 34, + ManaQueryTarget = 35, + CurrentGame = 36, + RequestedAppraisalTarget = 37, + /// 2 weenie attestations. + AllowedWielder = 38, + AssignedTarget = 39, + LimboSource = 40, + Snooper = 41, + TeleportedCharacter = 42, + Pet = 43, + PetOwner = 44, + PetDevice = 45, +} diff --git a/src/AcDream.Core/Properties/PropertyInt.cs b/src/AcDream.Core/Properties/PropertyInt.cs new file mode 100644 index 00000000..b288dda3 --- /dev/null +++ b/src/AcDream.Core/Properties/PropertyInt.cs @@ -0,0 +1,611 @@ +// +// Regenerate with the 2026-07-29 enum verification campaign tooling; see +// docs/research/2026-07-29-enum-verification-campaign.md for the oracle set, +// the extraction method, and the open-question ledger. +// Hand edits are pinned by PropertyEnumConformanceTests — change the oracle, +// not the enum. +// + +namespace AcDream.Core.Properties; + +/// +/// AC's PropertyInt property table — the numeric keys the server sends in +/// PrivateUpdatePropertyInt (0x02CD) / PublicUpdatePropertyInt (0x02CE) +/// and in the property bundles carried by CreateObject / PlayerDescription / +/// IdentifyResponse. The CLR payload for this table is int. +/// +/// Verified 2026-07-29 against two independent oracles: the vendored +/// client-side enum catalog (references/acclientlib/UtilityBelt.Common/Enums/Enums.cs, +/// which names this table IntId) and the 38,985-file ACE weenie export corpus +/// (references/weenies/), whose per-stat _comment carries the enum member +/// name beside the numeric key. 189 of these 391 members are attested by BOTH +/// oracles with zero value conflicts; the remainder are single-sourced from the +/// catalog because no weenie in the corpus sets them. +/// +/// This enum is a vocabulary, not a parser contract: the wire carries a raw +/// uint and unknown keys must still round-trip untouched, so never assume a +/// received key is a defined member. +/// +public enum PropertyInt : uint +{ + Undef = 0, + /// UtilityBelt catalog calls this ObjectType; 37329 weenie attestations. + ItemType = 1, + /// 6140 weenie attestations. + CreatureType = 2, + /// 8344 weenie attestations. + PaletteTemplate = 3, + /// 1597 weenie attestations. + ClothingPriority = 4, + /// 25793 weenie attestations. + EncumbranceVal = 5, + /// 8633 weenie attestations. + ItemsCapacity = 6, + /// 8618 weenie attestations. + ContainersCapacity = 7, + /// 23605 weenie attestations. + Mass = 8, + /// 20797 weenie attestations. + ValidLocations = 9, + CurrentWieldedLocation = 10, + /// 4373 weenie attestations. + MaxStackSize = 11, + /// 4373 weenie attestations. + StackSize = 12, + /// 3181 weenie attestations. + StackUnitEncumbrance = 13, + /// 3181 weenie attestations. + StackUnitMass = 14, + /// 3181 weenie attestations. + StackUnitValue = 15, + /// 37280 weenie attestations. + ItemUseable = 16, + /// 21 weenie attestations. + RareId = 17, + /// 5705 weenie attestations. + UiEffects = 18, + /// 25852 weenie attestations. + Value = 19, + /// 1 weenie attestations. + CoinValue = 20, + TotalExperience = 21, + AvailableCharacter = 22, + TotalSkillCredits = 23, + AvailableSkillCredits = 24, + /// 6140 weenie attestations. + Level = 25, + /// 28 weenie attestations. + AccountRequirements = 26, + /// 5476 weenie attestations. + ArmorType = 27, + /// 1657 weenie attestations. + ArmorLevel = 28, + AllegianceCpPool = 29, + /// 1 weenie attestations. + AllegianceRank = 30, + ChannelsAllowed = 31, + ChannelsActive = 32, + /// 6252 weenie attestations. + Bonded = 33, + MonarchsRank = 34, + AllegianceFollowers = 35, + /// 997 weenie attestations. + ResistMagic = 36, + /// 575 weenie attestations. + ResistItemAppraisal = 37, + /// 677 weenie attestations. + ResistLockpick = 38, + DeprecatedResistRepair = 39, + /// 1634 weenie attestations. + CombatMode = 40, + CurrentAttackHeight = 41, + CombatCollisions = 42, + /// 1 weenie attestations. + NumDeaths = 43, + /// 3717 weenie attestations. + Damage = 44, + /// 3357 weenie attestations. + DamageType = 45, + /// 2973 weenie attestations. + DefaultCombatStyle = 46, + /// 2364 weenie attestations. + AttackType = 47, + /// 3393 weenie attestations. + WeaponSkill = 48, + /// 3394 weenie attestations. + WeaponTime = 49, + /// 972 weenie attestations. + AmmoType = 50, + /// 4499 weenie attestations. + CombatUse = 51, + /// 440 weenie attestations. + ParentLocation = 52, + /// 440 weenie attestations. + PlacementPosition = 53, + WeaponEncumbrance = 54, + WeaponMass = 55, + ShieldValue = 56, + ShieldEncumbrance = 57, + MissileInventoryLocation = 58, + FullDamageType = 59, + /// 545 weenie attestations. + WeaponRange = 60, + AttackersSkill = 61, + DefendersSkill = 62, + AttackersSkillValue = 63, + AttackersClass = 64, + Placement = 65, + /// 179 weenie attestations. + CheckpointStatus = 66, + /// 219 weenie attestations. + Tolerance = 67, + /// 2151 weenie attestations. + TargetingTactic = 68, + /// 48 weenie attestations. + CombatTactic = 69, + HomesickTargetingTactic = 70, + NumFollowFailures = 71, + /// 494 weenie attestations. + FriendType = 72, + FoeType = 73, + /// UtilityBelt catalog calls this MerchandiseObjectTypes; 1049 weenie attestations. + MerchandiseItemTypes = 74, + /// 1049 weenie attestations. + MerchandiseMinValue = 75, + /// 1049 weenie attestations. + MerchandiseMaxValue = 76, + NumItemsSold = 77, + NumItemsBought = 78, + MoneyIncome = 79, + MoneyOutflow = 80, + /// 2195 weenie attestations. + MaxGeneratedObjects = 81, + /// 2195 weenie attestations. + InitGeneratedObjects = 82, + /// 787 weenie attestations. + ActivationResponse = 83, + OriginalValue = 84, + NumMoveFailures = 85, + /// 815 weenie attestations. + MinLevel = 86, + /// 115 weenie attestations. + MaxLevel = 87, + /// 10 weenie attestations. + LockpickMod = 88, + /// 525 weenie attestations. + BoosterEnum = 89, + /// 532 weenie attestations. + BoostValue = 90, + /// 800 weenie attestations. + MaxStructure = 91, + /// 713 weenie attestations. + Structure = 92, + /// 38985 weenie attestations. + PhysicsState = 93, + /// 3570 weenie attestations. + TargetType = 94, + /// 2668 weenie attestations. + RadarBlipColor = 95, + /// 472 weenie attestations. + EncumbranceCapacity = 96, + LoginTimestamp = 97, + /// 119 weenie attestations. + CreationTimestamp = 98, + /// 4 weenie attestations. + PkLevelModifier = 99, + /// 575 weenie attestations. + GeneratorType = 100, + /// 1265 weenie attestations. + AiAllowedCombatStyle = 101, + LogoffTimestamp = 102, + /// 158 weenie attestations. + GeneratorDestructionType = 103, + ActivationCreateClass = 104, + /// 693 weenie attestations. + ItemWorkmanship = 105, + /// 4213 weenie attestations. + ItemSpellcraft = 106, + /// 4173 weenie attestations. + ItemCurMana = 107, + /// 4211 weenie attestations. + ItemMaxMana = 108, + /// 3091 weenie attestations. + ItemDifficulty = 109, + /// 1043 weenie attestations. + ItemAllegianceRankLimit = 110, + /// 2940 weenie attestations. + PortalBitmask = 111, + AdvocateLevel = 112, + /// 749 weenie attestations. + Gender = 113, + /// 4502 weenie attestations. + Attuned = 114, + /// 1640 weenie attestations. + ItemSkillLevelLimit = 115, + GateLogic = 116, + /// 66 weenie attestations. + ItemManaCost = 117, + Logoff = 118, + /// 365 weenie attestations. + Active = 119, + AttackHeight = 120, + NumAttackFailures = 121, + AiCpThreshold = 122, + AiAdvancementStrategy = 123, + Version = 124, + /// 1 weenie attestations. + Age = 125, + /// 1048 weenie attestations. + VendorHappyMean = 126, + /// 1048 weenie attestations. + VendorHappyVariance = 127, + CloakStatus = 128, + VitaeCpPool = 129, + NumServicesSold = 130, + /// 586 weenie attestations. + MaterialType = 131, + NumAllegianceBreaks = 132, + /// 10541 weenie attestations. + ShowableOnRadar = 133, + /// 3138 weenie attestations. + PlayerKillerStatus = 134, + VendorHappyMaxItems = 135, + /// 5 weenie attestations. + ScorePageNum = 136, + /// 5 weenie attestations. + ScoreConfigNum = 137, + /// 5 weenie attestations. + ScoreNumScores = 138, + DeathLevel = 139, + /// 1411 weenie attestations. + AiOptions = 140, + OpenToEveryone = 141, + /// 297 weenie attestations. + GeneratorTimeType = 142, + /// 51 weenie attestations. + GeneratorStartTime = 143, + /// 50 weenie attestations. + GeneratorEndTime = 144, + /// 257 weenie attestations. + GeneratorEndDestructionType = 145, + /// 4361 weenie attestations. + XpOverride = 146, + NumCrashAndTurns = 147, + ComponentWarningThreshold = 148, + /// 7 weenie attestations. + HouseStatus = 149, + /// 4715 weenie attestations. + HookPlacement = 150, + /// 6799 weenie attestations. + HookType = 151, + /// UtilityBelt catalog calls this HookObjectType; 5 weenie attestations. + HookItemType = 152, + AiPpThreshold = 153, + GeneratorVersion = 154, + /// 6274 weenie attestations. + HouseType = 155, + /// 10 weenie attestations. + PickupEmoteOffset = 156, + /// 1 weenie attestations. + WeenieIteration = 157, + /// 2557 weenie attestations. + WieldRequirements = 158, + /// 2557 weenie attestations. + WieldSkilltype = 159, + /// 2557 weenie attestations. + WieldDifficulty = 160, + /// 654 weenie attestations. + HouseMaxHooksUsable = 161, + HouseCurrentHooksUsable = 162, + /// 8 weenie attestations. + AllegianceMinLevel = 163, + AllegianceMaxLevel = 164, + HouseRelinkHookCount = 165, + /// 537 weenie attestations. + SlayerCreatureType = 166, + ConfirmationInProgress = 167, + ConfirmationTypeInProgress = 168, + /// 728 weenie attestations. + TsysMutationData = 169, + /// 36 weenie attestations. + NumItemsInMaterial = 170, + /// 228 weenie attestations. + NumTimesTinkered = 171, + /// 358 weenie attestations. + AppraisalLongDescDecoration = 172, + /// 133 weenie attestations. + AppraisalLockpickSuccessPercent = 173, + /// 103 weenie attestations. + AppraisalPages = 174, + /// 103 weenie attestations. + AppraisalMaxPages = 175, + /// 355 weenie attestations. + AppraisalItemSkill = 176, + /// 281 weenie attestations. + GemCount = 177, + /// 281 weenie attestations. + GemType = 178, + /// 495 weenie attestations. + ImbuedEffect = 179, + AttackersRawSkillValue = 180, + ChessRank = 181, + ChessTotalGames = 182, + ChessGamesWon = 183, + ChessGamesLost = 184, + /// 71 weenie attestations. + TypeOfAlteration = 185, + /// 71 weenie attestations. + SkillToBeAltered = 186, + SkillAlterationCount = 187, + /// 807 weenie attestations. + HeritageGroup = 188, + /// 30 weenie attestations. + TransferFromAttribute = 189, + /// 30 weenie attestations. + TransferToAttribute = 190, + AttributeTransferCount = 191, + FakeFishingSkill = 192, + /// 3 weenie attestations. + NumKeys = 193, + DeathTimestamp = 194, + PkTimestamp = 195, + VictimTimestamp = 196, + /// 22 weenie attestations. + HookGroup = 197, + AllegianceSwearTimestamp = 198, + HousePurchaseTimestamp = 199, + RedirectableEquippedArmorCount = 200, + MeleedefenseImbuedEffectTypeCache = 201, + MissileDefenseImbuedEffectTypeCache = 202, + MagicDefenseImbuedEffectTypeCache = 203, + /// 121 weenie attestations. + ElementalDamageBonus = 204, + ImbueAttempts = 205, + ImbueSuccesses = 206, + CreatureKills = 207, + PlayerKillsPk = 208, + PlayerKillsPkl = 209, + RaresTierOne = 210, + RaresTierTwo = 211, + RaresTierThree = 212, + RaresTierFour = 213, + RaresTierFive = 214, + AugmentationStat = 215, + AugmentationFamilyStat = 216, + AugmentationInnateFamily = 217, + AugmentationInnateStrength = 218, + AugmentationInnateEndurance = 219, + AugmentationInnateCoordination = 220, + AugmentationInnateQuickness = 221, + AugmentationInnateFocus = 222, + AugmentationInnateSelf = 223, + AugmentationSpecializeSalvaging = 224, + AugmentationSpecializeItemTinkering = 225, + AugmentationSpecializeArmorTinkering = 226, + AugmentationSpecializeMagicItemTinkering = 227, + AugmentationSpecializeWeaponTinkering = 228, + AugmentationExtraPackSlot = 229, + AugmentationIncreasedCarryingCapacity = 230, + AugmentationLessDeathItemLoss = 231, + AugmentationSpellsRemainPastDeath = 232, + AugmentationCriticalDefense = 233, + AugmentationBonusXp = 234, + AugmentationBonusSalvage = 235, + AugmentationBonusImbueChance = 236, + AugmentationFasterRegen = 237, + AugmentationIncreasedSpellDuration = 238, + AugmentationResistanceFamily = 239, + AugmentationResistanceSlash = 240, + AugmentationResistancePierce = 241, + AugmentationResistanceBlunt = 242, + AugmentationResistanceAcid = 243, + AugmentationResistanceFire = 244, + AugmentationResistanceFrost = 245, + AugmentationResistanceLightning = 246, + RaresTierOneLogin = 247, + RaresTierTwoLogin = 248, + RaresTierThreeLogin = 249, + RaresTierFourLogin = 250, + RaresTierFiveLogin = 251, + RaresLoginTimestamp = 252, + RaresTierSix = 253, + RaresTierSeven = 254, + RaresTierSixLogin = 255, + RaresTierSevenLogin = 256, + /// 1 weenie attestations. + ItemAttributeLimit = 257, + /// 1 weenie attestations. + ItemAttributeLevelLimit = 258, + ItemAttribute2ndLimit = 259, + ItemAttribute2ndLevelLimit = 260, + /// 2 weenie attestations. + CharacterTitleId = 261, + /// 1 weenie attestations. + NumCharacterTitles = 262, + /// 93 weenie attestations. + ResistanceModifierType = 263, + FreeTinkersBitfield = 264, + /// 316 weenie attestations. + EquipmentSetId = 265, + PetClass = 266, + /// 119 weenie attestations. + Lifespan = 267, + /// 119 weenie attestations. + RemainingLifespan = 268, + UseCreateQuantity = 269, + /// 62 weenie attestations. + WieldRequirements2 = 270, + /// 62 weenie attestations. + WieldSkilltype2 = 271, + /// 62 weenie attestations. + WieldDifficulty2 = 272, + /// 3 weenie attestations. + WieldRequirements3 = 273, + /// 3 weenie attestations. + WieldSkilltype3 = 274, + /// 3 weenie attestations. + WieldDifficulty3 = 275, + /// 17 weenie attestations. + WieldRequirements4 = 276, + /// 17 weenie attestations. + WieldSkilltype4 = 277, + /// 17 weenie attestations. + WieldDifficulty4 = 278, + /// 14 weenie attestations. + Unique = 279, + /// 365 weenie attestations. + SharedCooldown = 280, + /// 177 weenie attestations. + Faction1Bits = 281, + Faction2Bits = 282, + Faction3Bits = 283, + Hatred1Bits = 284, + Hatred2Bits = 285, + Hatred3Bits = 286, + /// 61 weenie attestations. + SocietyRankCelhan = 287, + /// 60 weenie attestations. + SocietyRankEldweb = 288, + /// 56 weenie attestations. + SocietyRankRadblo = 289, + HearLocalSignals = 290, + HearLocalSignalsRadius = 291, + /// 67 weenie attestations. + Cleaving = 292, + AugmentationSpecializeGearcraft = 293, + AugmentationInfusedCreatureMagic = 294, + AugmentationInfusedItemMagic = 295, + AugmentationInfusedLifeMagic = 296, + AugmentationInfusedWarMagic = 297, + AugmentationCriticalExpertise = 298, + AugmentationCriticalPower = 299, + AugmentationSkilledMelee = 300, + AugmentationSkilledMissile = 301, + AugmentationSkilledMagic = 302, + /// 124 weenie attestations. + ImbuedEffect2 = 303, + /// 124 weenie attestations. + ImbuedEffect3 = 304, + /// 117 weenie attestations. + ImbuedEffect4 = 305, + /// 117 weenie attestations. + ImbuedEffect5 = 306, + /// 1403 weenie attestations. + DamageRating = 307, + /// 729 weenie attestations. + DamageResistRating = 308, + AugmentationDamageBonus = 309, + AugmentationDamageReduction = 310, + ImbueStackingBits = 311, + HealOverTime = 312, + /// 880 weenie attestations. + CritRating = 313, + /// 843 weenie attestations. + CritDamageRating = 314, + /// 713 weenie attestations. + CritResistRating = 315, + /// 732 weenie attestations. + CritDamageResistRating = 316, + HealingResistRating = 317, + DamageOverTime = 318, + /// 79 weenie attestations. + ItemMaxLevel = 319, + /// 79 weenie attestations. + ItemXpStyle = 320, + EquipmentSetExtra = 321, + AetheriaBitfield = 322, + HealingBoostRating = 323, + /// 29 weenie attestations. + HeritageSpecificArmor = 324, + AlternateRacialSkills = 325, + AugmentationJackOfAllTrades = 326, + AugmentationResistanceNether = 327, + AugmentationInfusedVoidMagic = 328, + WeaknessRating = 329, + NetherOverTime = 330, + NetherResistRating = 331, + LuminanceAward = 332, + LumAugDamageRating = 333, + LumAugDamageReductionRating = 334, + LumAugCritDamageRating = 335, + LumAugCritReductionRating = 336, + LumAugSurgeEffectRating = 337, + LumAugSurgeChanceRating = 338, + LumAugItemManaUsage = 339, + LumAugItemManaGain = 340, + LumAugVitality = 341, + LumAugHealingRating = 342, + LumAugSkilledCraft = 343, + LumAugSkilledSpec = 344, + LumAugNoDestroyCraft = 345, + RestrictInteraction = 346, + OlthoiLootTimestamp = 347, + OlthoiLootStep = 348, + UseCreatesContractId = 349, + DotResistRating = 350, + LifeResistRating = 351, + /// 13 weenie attestations. + CloakWeaveProc = 352, + /// 492 weenie attestations. + WeaponType = 353, + MeleeMastery = 354, + RangedMastery = 355, + SneakAttackRating = 356, + RecklessnessRating = 357, + DeceptionRating = 358, + CombatPetRange = 359, + WeaponAuraDamage = 360, + WeaponAuraSpeed = 361, + SummoningMastery = 362, + HeartbeatLifespan = 363, + UseLevelRequirement = 364, + LumAugAllSkills = 365, + /// 259 weenie attestations. + UseRequiresSkill = 366, + /// 259 weenie attestations. + UseRequiresSkillLevel = 367, + /// 36 weenie attestations. + UseRequiresSkillSpec = 368, + /// 271 weenie attestations. + UseRequiresLevel = 369, + /// 869 weenie attestations. + GearDamage = 370, + /// 868 weenie attestations. + GearDamageResist = 371, + /// 854 weenie attestations. + GearCrit = 372, + /// 854 weenie attestations. + GearCritResist = 373, + /// 955 weenie attestations. + GearCritDamage = 374, + /// 953 weenie attestations. + GearCritDamageResist = 375, + /// 610 weenie attestations. + GearHealingBoost = 376, + /// 602 weenie attestations. + GearNetherResist = 377, + /// 602 weenie attestations. + GearLifeResist = 378, + /// 640 weenie attestations. + GearMaxHealth = 379, + Unknown380 = 380, + /// 616 weenie attestations. + PKDamageRating = 381, + /// 602 weenie attestations. + PKDamageResistRating = 382, + /// 643 weenie attestations. + GearPKDamageRating = 383, + /// 640 weenie attestations. + GearPKDamageResistRating = 384, + Unknown385 = 385, + /// 766 weenie attestations. + Overpower = 386, + /// 602 weenie attestations. + OverpowerResist = 387, + /// 602 weenie attestations. + GearOverpower = 388, + /// 602 weenie attestations. + GearOverpowerResist = 389, + /// 2 weenie attestations. + Enlightenment = 390, +} diff --git a/src/AcDream.Core/Properties/PropertyInt64.cs b/src/AcDream.Core/Properties/PropertyInt64.cs new file mode 100644 index 00000000..e6792529 --- /dev/null +++ b/src/AcDream.Core/Properties/PropertyInt64.cs @@ -0,0 +1,43 @@ +// +// Regenerate with the 2026-07-29 enum verification campaign tooling; see +// docs/research/2026-07-29-enum-verification-campaign.md for the oracle set, +// the extraction method, and the open-question ledger. +// Hand edits are pinned by PropertyEnumConformanceTests — change the oracle, +// not the enum. +// + +namespace AcDream.Core.Properties; + +/// +/// AC's PropertyInt64 property table — the numeric keys the server sends in +/// PrivateUpdatePropertyInt64 (0x02D1) +/// and in the property bundles carried by CreateObject / PlayerDescription / +/// IdentifyResponse. The CLR payload for this table is long. +/// +/// Verified 2026-07-29 against two independent oracles: the vendored +/// client-side enum catalog (references/acclientlib/UtilityBelt.Common/Enums/Enums.cs, +/// which names this table Int64Id) and the 38,985-file ACE weenie export corpus +/// (references/weenies/), whose per-stat _comment carries the enum member +/// name beside the numeric key. 3 of these 9 members are attested by BOTH +/// oracles with zero value conflicts; the remainder are single-sourced from the +/// catalog because no weenie in the corpus sets them. +/// +/// This enum is a vocabulary, not a parser contract: the wire carries a raw +/// uint and unknown keys must still round-trip untouched, so never assume a +/// received key is a defined member. +/// +public enum PropertyInt64 : uint +{ + Undef = 0, + TotalExperience = 1, + AvailableExperience = 2, + /// 7 weenie attestations. + AugmentationCost = 3, + /// 79 weenie attestations. + ItemTotalXp = 4, + /// 79 weenie attestations. + ItemBaseXp = 5, + AvailableLuminance = 6, + MaximumLuminance = 7, + InteractionReqs = 8, +} diff --git a/src/AcDream.Core/Properties/PropertyString.cs b/src/AcDream.Core/Properties/PropertyString.cs new file mode 100644 index 00000000..0e2f0b03 --- /dev/null +++ b/src/AcDream.Core/Properties/PropertyString.cs @@ -0,0 +1,112 @@ +// +// Regenerate with the 2026-07-29 enum verification campaign tooling; see +// docs/research/2026-07-29-enum-verification-campaign.md for the oracle set, +// the extraction method, and the open-question ledger. +// Hand edits are pinned by PropertyEnumConformanceTests — change the oracle, +// not the enum. +// + +namespace AcDream.Core.Properties; + +/// +/// AC's PropertyString property table — the numeric keys the server sends in +/// PrivateUpdatePropertyString (0x02DD) / PublicUpdatePropertyString (0x02DE) +/// and in the property bundles carried by CreateObject / PlayerDescription / +/// IdentifyResponse. The CLR payload for this table is string. +/// +/// Verified 2026-07-29 against two independent oracles: the vendored +/// client-side enum catalog (references/acclientlib/UtilityBelt.Common/Enums/Enums.cs, +/// which names this table StringId) and the 38,985-file ACE weenie export corpus +/// (references/weenies/), whose per-stat _comment carries the enum member +/// name beside the numeric key. 28 of these 53 members are attested by BOTH +/// oracles with zero value conflicts; the remainder are single-sourced from the +/// catalog because no weenie in the corpus sets them. +/// +/// This enum is a vocabulary, not a parser contract: the wire carries a raw +/// uint and unknown keys must still round-trip untouched, so never assume a +/// received key is a defined member. +/// +public enum PropertyString : uint +{ + Undef = 0, + /// 38984 weenie attestations. + Name = 1, + Title = 2, + /// 1697 weenie attestations. + Sex = 3, + /// 1693 weenie attestations. + HeritageGroup = 4, + /// 1622 weenie attestations. + Template = 5, + AttackersName = 6, + /// 93 weenie attestations. + Inscription = 7, + /// 93 weenie attestations. + ScribeName = 8, + VendorsName = 9, + /// 1 weenie attestations. + Fellowship = 10, + MonarchsName = 11, + /// 449 weenie attestations. + LockCode = 12, + /// 238 weenie attestations. + KeyCode = 13, + /// 3219 weenie attestations. + Use = 14, + /// 5597 weenie attestations. + ShortDesc = 15, + /// 7107 weenie attestations. + LongDesc = 16, + /// 109 weenie attestations. + ActivationTalk = 17, + /// 10 weenie attestations. + UseMessage = 18, + /// 45 weenie attestations. + ItemHeritageGroupRestriction = 19, + /// 1318 weenie attestations. + PluralName = 20, + MonarchsTitle = 21, + /// 96 weenie attestations. + ActivationFailure = 22, + ScribeAccount = 23, + /// 591 weenie attestations. + TownName = 24, + /// 2 weenie attestations. + CraftsmanName = 25, + /// 4 weenie attestations. + UsePkServerError = 26, + ScoreCachedText = 27, + /// 5 weenie attestations. + ScoreDefaultEntryFormat = 28, + /// 5 weenie attestations. + ScoreFirstEntryFormat = 29, + /// 5 weenie attestations. + ScoreLastEntryFormat = 30, + /// 5 weenie attestations. + ScoreOnlyEntryFormat = 31, + /// 5 weenie attestations. + ScoreNoEntry = 32, + /// 859 weenie attestations. + Quest = 33, + /// 249 weenie attestations. + GeneratorEvent = 34, + PatronsTitle = 35, + HouseOwnerName = 36, + /// 170 weenie attestations. + QuestRestriction = 37, + AppraisalPortalDestination = 38, + TinkerName = 39, + ImbuerName = 40, + HouseOwnerAccount = 41, + DisplayName = 42, + DateOfBirth = 43, + ThirdPartyApi = 44, + KillQuest = 45, + Afk = 46, + AllegianceName = 47, + AugmentationAddQuest = 48, + KillQuest2 = 49, + KillQuest3 = 50, + UseSendsSignal = 51, + GearPlatingName = 52, +} diff --git a/tests/AcDream.Core.Tests/Properties/PropertyEnumConformanceTests.cs b/tests/AcDream.Core.Tests/Properties/PropertyEnumConformanceTests.cs new file mode 100644 index 00000000..f6e7ed03 --- /dev/null +++ b/tests/AcDream.Core.Tests/Properties/PropertyEnumConformanceTests.cs @@ -0,0 +1,1398 @@ +// +// Golden conformance table for the seven Property* enums. Regenerate with the +// 2026-07-29 enum verification campaign tooling; see +// docs/research/2026-07-29-enum-verification-campaign.md. +// +using System; +using System.Collections.Generic; +using System.Linq; +using AcDream.Core.Properties; +using Xunit; + +namespace AcDream.Core.Tests.Properties; + +/// +/// Pins every member of AC's seven property tables to its wire value. The table +/// below is the verbatim output of the 2026-07-29 oracle cross-check, so any hand +/// edit to a Property* enum — a renamed member, a changed value, an added or +/// dropped key — fails here rather than silently mis-reading the wire. +/// +/// is the stronger subset: members whose +/// name/value pairing is independently attested by the ACE weenie export corpus, +/// not just by the client-side enum catalog. +/// +public sealed class PropertyEnumConformanceTests +{ + private static readonly (string Name, uint Value)[] PropertyIntGolden = + [ + ("Undef", 0), + ("ItemType", 1), + ("CreatureType", 2), + ("PaletteTemplate", 3), + ("ClothingPriority", 4), + ("EncumbranceVal", 5), + ("ItemsCapacity", 6), + ("ContainersCapacity", 7), + ("Mass", 8), + ("ValidLocations", 9), + ("CurrentWieldedLocation", 10), + ("MaxStackSize", 11), + ("StackSize", 12), + ("StackUnitEncumbrance", 13), + ("StackUnitMass", 14), + ("StackUnitValue", 15), + ("ItemUseable", 16), + ("RareId", 17), + ("UiEffects", 18), + ("Value", 19), + ("CoinValue", 20), + ("TotalExperience", 21), + ("AvailableCharacter", 22), + ("TotalSkillCredits", 23), + ("AvailableSkillCredits", 24), + ("Level", 25), + ("AccountRequirements", 26), + ("ArmorType", 27), + ("ArmorLevel", 28), + ("AllegianceCpPool", 29), + ("AllegianceRank", 30), + ("ChannelsAllowed", 31), + ("ChannelsActive", 32), + ("Bonded", 33), + ("MonarchsRank", 34), + ("AllegianceFollowers", 35), + ("ResistMagic", 36), + ("ResistItemAppraisal", 37), + ("ResistLockpick", 38), + ("DeprecatedResistRepair", 39), + ("CombatMode", 40), + ("CurrentAttackHeight", 41), + ("CombatCollisions", 42), + ("NumDeaths", 43), + ("Damage", 44), + ("DamageType", 45), + ("DefaultCombatStyle", 46), + ("AttackType", 47), + ("WeaponSkill", 48), + ("WeaponTime", 49), + ("AmmoType", 50), + ("CombatUse", 51), + ("ParentLocation", 52), + ("PlacementPosition", 53), + ("WeaponEncumbrance", 54), + ("WeaponMass", 55), + ("ShieldValue", 56), + ("ShieldEncumbrance", 57), + ("MissileInventoryLocation", 58), + ("FullDamageType", 59), + ("WeaponRange", 60), + ("AttackersSkill", 61), + ("DefendersSkill", 62), + ("AttackersSkillValue", 63), + ("AttackersClass", 64), + ("Placement", 65), + ("CheckpointStatus", 66), + ("Tolerance", 67), + ("TargetingTactic", 68), + ("CombatTactic", 69), + ("HomesickTargetingTactic", 70), + ("NumFollowFailures", 71), + ("FriendType", 72), + ("FoeType", 73), + ("MerchandiseItemTypes", 74), + ("MerchandiseMinValue", 75), + ("MerchandiseMaxValue", 76), + ("NumItemsSold", 77), + ("NumItemsBought", 78), + ("MoneyIncome", 79), + ("MoneyOutflow", 80), + ("MaxGeneratedObjects", 81), + ("InitGeneratedObjects", 82), + ("ActivationResponse", 83), + ("OriginalValue", 84), + ("NumMoveFailures", 85), + ("MinLevel", 86), + ("MaxLevel", 87), + ("LockpickMod", 88), + ("BoosterEnum", 89), + ("BoostValue", 90), + ("MaxStructure", 91), + ("Structure", 92), + ("PhysicsState", 93), + ("TargetType", 94), + ("RadarBlipColor", 95), + ("EncumbranceCapacity", 96), + ("LoginTimestamp", 97), + ("CreationTimestamp", 98), + ("PkLevelModifier", 99), + ("GeneratorType", 100), + ("AiAllowedCombatStyle", 101), + ("LogoffTimestamp", 102), + ("GeneratorDestructionType", 103), + ("ActivationCreateClass", 104), + ("ItemWorkmanship", 105), + ("ItemSpellcraft", 106), + ("ItemCurMana", 107), + ("ItemMaxMana", 108), + ("ItemDifficulty", 109), + ("ItemAllegianceRankLimit", 110), + ("PortalBitmask", 111), + ("AdvocateLevel", 112), + ("Gender", 113), + ("Attuned", 114), + ("ItemSkillLevelLimit", 115), + ("GateLogic", 116), + ("ItemManaCost", 117), + ("Logoff", 118), + ("Active", 119), + ("AttackHeight", 120), + ("NumAttackFailures", 121), + ("AiCpThreshold", 122), + ("AiAdvancementStrategy", 123), + ("Version", 124), + ("Age", 125), + ("VendorHappyMean", 126), + ("VendorHappyVariance", 127), + ("CloakStatus", 128), + ("VitaeCpPool", 129), + ("NumServicesSold", 130), + ("MaterialType", 131), + ("NumAllegianceBreaks", 132), + ("ShowableOnRadar", 133), + ("PlayerKillerStatus", 134), + ("VendorHappyMaxItems", 135), + ("ScorePageNum", 136), + ("ScoreConfigNum", 137), + ("ScoreNumScores", 138), + ("DeathLevel", 139), + ("AiOptions", 140), + ("OpenToEveryone", 141), + ("GeneratorTimeType", 142), + ("GeneratorStartTime", 143), + ("GeneratorEndTime", 144), + ("GeneratorEndDestructionType", 145), + ("XpOverride", 146), + ("NumCrashAndTurns", 147), + ("ComponentWarningThreshold", 148), + ("HouseStatus", 149), + ("HookPlacement", 150), + ("HookType", 151), + ("HookItemType", 152), + ("AiPpThreshold", 153), + ("GeneratorVersion", 154), + ("HouseType", 155), + ("PickupEmoteOffset", 156), + ("WeenieIteration", 157), + ("WieldRequirements", 158), + ("WieldSkilltype", 159), + ("WieldDifficulty", 160), + ("HouseMaxHooksUsable", 161), + ("HouseCurrentHooksUsable", 162), + ("AllegianceMinLevel", 163), + ("AllegianceMaxLevel", 164), + ("HouseRelinkHookCount", 165), + ("SlayerCreatureType", 166), + ("ConfirmationInProgress", 167), + ("ConfirmationTypeInProgress", 168), + ("TsysMutationData", 169), + ("NumItemsInMaterial", 170), + ("NumTimesTinkered", 171), + ("AppraisalLongDescDecoration", 172), + ("AppraisalLockpickSuccessPercent", 173), + ("AppraisalPages", 174), + ("AppraisalMaxPages", 175), + ("AppraisalItemSkill", 176), + ("GemCount", 177), + ("GemType", 178), + ("ImbuedEffect", 179), + ("AttackersRawSkillValue", 180), + ("ChessRank", 181), + ("ChessTotalGames", 182), + ("ChessGamesWon", 183), + ("ChessGamesLost", 184), + ("TypeOfAlteration", 185), + ("SkillToBeAltered", 186), + ("SkillAlterationCount", 187), + ("HeritageGroup", 188), + ("TransferFromAttribute", 189), + ("TransferToAttribute", 190), + ("AttributeTransferCount", 191), + ("FakeFishingSkill", 192), + ("NumKeys", 193), + ("DeathTimestamp", 194), + ("PkTimestamp", 195), + ("VictimTimestamp", 196), + ("HookGroup", 197), + ("AllegianceSwearTimestamp", 198), + ("HousePurchaseTimestamp", 199), + ("RedirectableEquippedArmorCount", 200), + ("MeleedefenseImbuedEffectTypeCache", 201), + ("MissileDefenseImbuedEffectTypeCache", 202), + ("MagicDefenseImbuedEffectTypeCache", 203), + ("ElementalDamageBonus", 204), + ("ImbueAttempts", 205), + ("ImbueSuccesses", 206), + ("CreatureKills", 207), + ("PlayerKillsPk", 208), + ("PlayerKillsPkl", 209), + ("RaresTierOne", 210), + ("RaresTierTwo", 211), + ("RaresTierThree", 212), + ("RaresTierFour", 213), + ("RaresTierFive", 214), + ("AugmentationStat", 215), + ("AugmentationFamilyStat", 216), + ("AugmentationInnateFamily", 217), + ("AugmentationInnateStrength", 218), + ("AugmentationInnateEndurance", 219), + ("AugmentationInnateCoordination", 220), + ("AugmentationInnateQuickness", 221), + ("AugmentationInnateFocus", 222), + ("AugmentationInnateSelf", 223), + ("AugmentationSpecializeSalvaging", 224), + ("AugmentationSpecializeItemTinkering", 225), + ("AugmentationSpecializeArmorTinkering", 226), + ("AugmentationSpecializeMagicItemTinkering", 227), + ("AugmentationSpecializeWeaponTinkering", 228), + ("AugmentationExtraPackSlot", 229), + ("AugmentationIncreasedCarryingCapacity", 230), + ("AugmentationLessDeathItemLoss", 231), + ("AugmentationSpellsRemainPastDeath", 232), + ("AugmentationCriticalDefense", 233), + ("AugmentationBonusXp", 234), + ("AugmentationBonusSalvage", 235), + ("AugmentationBonusImbueChance", 236), + ("AugmentationFasterRegen", 237), + ("AugmentationIncreasedSpellDuration", 238), + ("AugmentationResistanceFamily", 239), + ("AugmentationResistanceSlash", 240), + ("AugmentationResistancePierce", 241), + ("AugmentationResistanceBlunt", 242), + ("AugmentationResistanceAcid", 243), + ("AugmentationResistanceFire", 244), + ("AugmentationResistanceFrost", 245), + ("AugmentationResistanceLightning", 246), + ("RaresTierOneLogin", 247), + ("RaresTierTwoLogin", 248), + ("RaresTierThreeLogin", 249), + ("RaresTierFourLogin", 250), + ("RaresTierFiveLogin", 251), + ("RaresLoginTimestamp", 252), + ("RaresTierSix", 253), + ("RaresTierSeven", 254), + ("RaresTierSixLogin", 255), + ("RaresTierSevenLogin", 256), + ("ItemAttributeLimit", 257), + ("ItemAttributeLevelLimit", 258), + ("ItemAttribute2ndLimit", 259), + ("ItemAttribute2ndLevelLimit", 260), + ("CharacterTitleId", 261), + ("NumCharacterTitles", 262), + ("ResistanceModifierType", 263), + ("FreeTinkersBitfield", 264), + ("EquipmentSetId", 265), + ("PetClass", 266), + ("Lifespan", 267), + ("RemainingLifespan", 268), + ("UseCreateQuantity", 269), + ("WieldRequirements2", 270), + ("WieldSkilltype2", 271), + ("WieldDifficulty2", 272), + ("WieldRequirements3", 273), + ("WieldSkilltype3", 274), + ("WieldDifficulty3", 275), + ("WieldRequirements4", 276), + ("WieldSkilltype4", 277), + ("WieldDifficulty4", 278), + ("Unique", 279), + ("SharedCooldown", 280), + ("Faction1Bits", 281), + ("Faction2Bits", 282), + ("Faction3Bits", 283), + ("Hatred1Bits", 284), + ("Hatred2Bits", 285), + ("Hatred3Bits", 286), + ("SocietyRankCelhan", 287), + ("SocietyRankEldweb", 288), + ("SocietyRankRadblo", 289), + ("HearLocalSignals", 290), + ("HearLocalSignalsRadius", 291), + ("Cleaving", 292), + ("AugmentationSpecializeGearcraft", 293), + ("AugmentationInfusedCreatureMagic", 294), + ("AugmentationInfusedItemMagic", 295), + ("AugmentationInfusedLifeMagic", 296), + ("AugmentationInfusedWarMagic", 297), + ("AugmentationCriticalExpertise", 298), + ("AugmentationCriticalPower", 299), + ("AugmentationSkilledMelee", 300), + ("AugmentationSkilledMissile", 301), + ("AugmentationSkilledMagic", 302), + ("ImbuedEffect2", 303), + ("ImbuedEffect3", 304), + ("ImbuedEffect4", 305), + ("ImbuedEffect5", 306), + ("DamageRating", 307), + ("DamageResistRating", 308), + ("AugmentationDamageBonus", 309), + ("AugmentationDamageReduction", 310), + ("ImbueStackingBits", 311), + ("HealOverTime", 312), + ("CritRating", 313), + ("CritDamageRating", 314), + ("CritResistRating", 315), + ("CritDamageResistRating", 316), + ("HealingResistRating", 317), + ("DamageOverTime", 318), + ("ItemMaxLevel", 319), + ("ItemXpStyle", 320), + ("EquipmentSetExtra", 321), + ("AetheriaBitfield", 322), + ("HealingBoostRating", 323), + ("HeritageSpecificArmor", 324), + ("AlternateRacialSkills", 325), + ("AugmentationJackOfAllTrades", 326), + ("AugmentationResistanceNether", 327), + ("AugmentationInfusedVoidMagic", 328), + ("WeaknessRating", 329), + ("NetherOverTime", 330), + ("NetherResistRating", 331), + ("LuminanceAward", 332), + ("LumAugDamageRating", 333), + ("LumAugDamageReductionRating", 334), + ("LumAugCritDamageRating", 335), + ("LumAugCritReductionRating", 336), + ("LumAugSurgeEffectRating", 337), + ("LumAugSurgeChanceRating", 338), + ("LumAugItemManaUsage", 339), + ("LumAugItemManaGain", 340), + ("LumAugVitality", 341), + ("LumAugHealingRating", 342), + ("LumAugSkilledCraft", 343), + ("LumAugSkilledSpec", 344), + ("LumAugNoDestroyCraft", 345), + ("RestrictInteraction", 346), + ("OlthoiLootTimestamp", 347), + ("OlthoiLootStep", 348), + ("UseCreatesContractId", 349), + ("DotResistRating", 350), + ("LifeResistRating", 351), + ("CloakWeaveProc", 352), + ("WeaponType", 353), + ("MeleeMastery", 354), + ("RangedMastery", 355), + ("SneakAttackRating", 356), + ("RecklessnessRating", 357), + ("DeceptionRating", 358), + ("CombatPetRange", 359), + ("WeaponAuraDamage", 360), + ("WeaponAuraSpeed", 361), + ("SummoningMastery", 362), + ("HeartbeatLifespan", 363), + ("UseLevelRequirement", 364), + ("LumAugAllSkills", 365), + ("UseRequiresSkill", 366), + ("UseRequiresSkillLevel", 367), + ("UseRequiresSkillSpec", 368), + ("UseRequiresLevel", 369), + ("GearDamage", 370), + ("GearDamageResist", 371), + ("GearCrit", 372), + ("GearCritResist", 373), + ("GearCritDamage", 374), + ("GearCritDamageResist", 375), + ("GearHealingBoost", 376), + ("GearNetherResist", 377), + ("GearLifeResist", 378), + ("GearMaxHealth", 379), + ("Unknown380", 380), + ("PKDamageRating", 381), + ("PKDamageResistRating", 382), + ("GearPKDamageRating", 383), + ("GearPKDamageResistRating", 384), + ("Unknown385", 385), + ("Overpower", 386), + ("OverpowerResist", 387), + ("GearOverpower", 388), + ("GearOverpowerResist", 389), + ("Enlightenment", 390), + ]; + + private static readonly (string Name, uint Value)[] PropertyInt64Golden = + [ + ("Undef", 0), + ("TotalExperience", 1), + ("AvailableExperience", 2), + ("AugmentationCost", 3), + ("ItemTotalXp", 4), + ("ItemBaseXp", 5), + ("AvailableLuminance", 6), + ("MaximumLuminance", 7), + ("InteractionReqs", 8), + ]; + + private static readonly (string Name, uint Value)[] PropertyBoolGolden = + [ + ("Undef", 0), + ("Stuck", 1), + ("Open", 2), + ("Locked", 3), + ("RotProof", 4), + ("AllegianceUpdateRequest", 5), + ("AiUsesMana", 6), + ("AiUseHumanMagicAnimations", 7), + ("AllowGive", 8), + ("CurrentlyAttacking", 9), + ("AttackerAi", 10), + ("IgnoreCollisions", 11), + ("ReportCollisions", 12), + ("Ethereal", 13), + ("GravityStatus", 14), + ("LightsStatus", 15), + ("ScriptedCollision", 16), + ("Inelastic", 17), + ("Visibility", 18), + ("Attackable", 19), + ("SafeSpellComponents", 20), + ("AdvocateState", 21), + ("Inscribable", 22), + ("DestroyOnSell", 23), + ("UiHidden", 24), + ("IgnoreHouseBarriers", 25), + ("HiddenAdmin", 26), + ("PkWounder", 27), + ("PkKiller", 28), + ("NoCorpse", 29), + ("UnderLifestoneProtection", 30), + ("ItemManaUpdatePending", 31), + ("GeneratorStatus", 32), + ("ResetMessagePending", 33), + ("DefaultOpen", 34), + ("DefaultLocked", 35), + ("DefaultOn", 36), + ("OpenForBusiness", 37), + ("IsFrozen", 38), + ("DealMagicalItems", 39), + ("LogoffImDead", 40), + ("ReportCollisionsAsEnvironment", 41), + ("AllowEdgeSlide", 42), + ("AdvocateQuest", 43), + ("IsAdmin", 44), + ("IsArch", 45), + ("IsSentinel", 46), + ("IsAdvocate", 47), + ("CurrentlyPoweringUp", 48), + ("GeneratorEnteredWorld", 49), + ("NeverFailCasting", 50), + ("VendorService", 51), + ("AiImmobile", 52), + ("DamagedByCollisions", 53), + ("IsDynamic", 54), + ("IsHot", 55), + ("IsAffecting", 56), + ("AffectsAis", 57), + ("SpellQueueActive", 58), + ("GeneratorDisabled", 59), + ("IsAcceptingTells", 60), + ("LoggingChannel", 61), + ("OpensAnyLock", 62), + ("UnlimitedUse", 63), + ("GeneratedTreasureItem", 64), + ("IgnoreMagicResist", 65), + ("IgnoreMagicArmor", 66), + ("AiAllowTrade", 67), + ("SpellComponentsRequired", 68), + ("IsSellable", 69), + ("IgnoreShieldsBySkill", 70), + ("NoDraw", 71), + ("ActivationUntargeted", 72), + ("HouseHasGottenPriorityBootPos", 73), + ("GeneratorAutomaticDestruction", 74), + ("HouseHooksVisible", 75), + ("HouseRequiresMonarch", 76), + ("HouseHooksEnabled", 77), + ("HouseNotifiedHudOfHookCount", 78), + ("AiAcceptEverything", 79), + ("IgnorePortalRestrictions", 80), + ("RequiresBackpackSlot", 81), + ("DontTurnOrMoveWhenGiving", 82), + ("NpcLooksLikeObject", 83), + ("IgnoreCloIcons", 84), + ("AppraisalHasAllowedWielder", 85), + ("ChestRegenOnClose", 86), + ("LogoffInMinigame", 87), + ("PortalShowDestination", 88), + ("PortalIgnoresPkAttackTimer", 89), + ("NpcInteractsSilently", 90), + ("Retained", 91), + ("IgnoreAuthor", 92), + ("Limbo", 93), + ("AppraisalHasAllowedActivator", 94), + ("ExistedBeforeAllegianceXpChanges", 95), + ("IsDeaf", 96), + ("IsPsr", 97), + ("Invincible", 98), + ("Ivoryable", 99), + ("Dyable", 100), + ("CanGenerateRare", 101), + ("CorpseGeneratedRare", 102), + ("NonProjectileMagicImmune", 103), + ("ActdReceivedItems", 104), + ("Unknown105", 105), + ("FirstEnterWorldDone", 106), + ("RecallsDisabled", 107), + ("RareUsesTimer", 108), + ("ActdPreorderReceivedItems", 109), + ("Afk", 110), + ("IsGagged", 111), + ("ProcSpellSelfTargeted", 112), + ("IsAllegianceGagged", 113), + ("EquipmentSetTriggerPiece", 114), + ("Uninscribe", 115), + ("WieldOnUse", 116), + ("ChestClearedWhenClosed", 117), + ("NeverAttack", 118), + ("SuppressGenerateEffect", 119), + ("TreasureCorpse", 120), + ("EquipmentSetAddLevel", 121), + ("BarberActive", 122), + ("TopLayerPriority", 123), + ("NoHeldItemShown", 124), + ("LoginAtLifestone", 125), + ("OlthoiPk", 126), + ("Account15Days", 127), + ("HadNoVitae", 128), + ("NoOlthoiTalk", 129), + ("AutowieldLeft", 130), + ]; + + private static readonly (string Name, uint Value)[] PropertyFloatGolden = + [ + ("Undef", 0), + ("HeartbeatInterval", 1), + ("HeartbeatTimestamp", 2), + ("HealthRate", 3), + ("StaminaRate", 4), + ("ManaRate", 5), + ("HealthUponResurrection", 6), + ("StaminaUponResurrection", 7), + ("ManaUponResurrection", 8), + ("StartTime", 9), + ("StopTime", 10), + ("ResetInterval", 11), + ("Shade", 12), + ("ArmorModVsSlash", 13), + ("ArmorModVsPierce", 14), + ("ArmorModVsBludgeon", 15), + ("ArmorModVsCold", 16), + ("ArmorModVsFire", 17), + ("ArmorModVsAcid", 18), + ("ArmorModVsElectric", 19), + ("CombatSpeed", 20), + ("WeaponLength", 21), + ("DamageVariance", 22), + ("CurrentPowerMod", 23), + ("AccuracyMod", 24), + ("StrengthMod", 25), + ("MaximumVelocity", 26), + ("RotationSpeed", 27), + ("MotionTimestamp", 28), + ("WeaponDefense", 29), + ("WimpyLevel", 30), + ("VisualAwarenessRange", 31), + ("AuralAwarenessRange", 32), + ("PerceptionLevel", 33), + ("PowerupTime", 34), + ("MaxChargeDistance", 35), + ("ChargeSpeed", 36), + ("BuyPrice", 37), + ("SellPrice", 38), + ("DefaultScale", 39), + ("LockpickMod", 40), + ("RegenerationInterval", 41), + ("RegenerationTimestamp", 42), + ("GeneratorRadius", 43), + ("TimeToRot", 44), + ("DeathTimestamp", 45), + ("PkTimestamp", 46), + ("VictimTimestamp", 47), + ("LoginTimestamp", 48), + ("CreationTimestamp", 49), + ("MinimumTimeSincePk", 50), + ("DeprecatedHousekeepingPriority", 51), + ("AbuseLoggingTimestamp", 52), + ("LastPortalTeleportTimestamp", 53), + ("UseRadius", 54), + ("HomeRadius", 55), + ("ReleasedTimestamp", 56), + ("MinHomeRadius", 57), + ("Facing", 58), + ("ResetTimestamp", 59), + ("LogoffTimestamp", 60), + ("EconRecoveryInterval", 61), + ("WeaponOffense", 62), + ("DamageMod", 63), + ("ResistSlash", 64), + ("ResistPierce", 65), + ("ResistBludgeon", 66), + ("ResistFire", 67), + ("ResistCold", 68), + ("ResistAcid", 69), + ("ResistElectric", 70), + ("ResistHealthBoost", 71), + ("ResistStaminaDrain", 72), + ("ResistStaminaBoost", 73), + ("ResistManaDrain", 74), + ("ResistManaBoost", 75), + ("Translucency", 76), + ("PhysicsScriptIntensity", 77), + ("Friction", 78), + ("Elasticity", 79), + ("AiUseMagicDelay", 80), + ("ItemMinSpellcraftMod", 81), + ("ItemMaxSpellcraftMod", 82), + ("ItemRankProbability", 83), + ("Shade2", 84), + ("Shade3", 85), + ("Shade4", 86), + ("ItemEfficiency", 87), + ("ItemManaUpdateTimestamp", 88), + ("SpellGestureSpeedMod", 89), + ("SpellStanceSpeedMod", 90), + ("AllegianceAppraisalTimestamp", 91), + ("PowerLevel", 92), + ("AccuracyLevel", 93), + ("AttackAngle", 94), + ("AttackTimestamp", 95), + ("CheckpointTimestamp", 96), + ("SoldTimestamp", 97), + ("UseTimestamp", 98), + ("UseLockTimestamp", 99), + ("HealkitMod", 100), + ("FrozenTimestamp", 101), + ("HealthRateMod", 102), + ("AllegianceSwearTimestamp", 103), + ("ObviousRadarRange", 104), + ("HotspotCycleTime", 105), + ("HotspotCycleTimeVariance", 106), + ("SpamTimestamp", 107), + ("SpamRate", 108), + ("BondWieldedTreasure", 109), + ("BulkMod", 110), + ("SizeMod", 111), + ("GagTimestamp", 112), + ("GeneratorUpdateTimestamp", 113), + ("DeathSpamTimestamp", 114), + ("DeathSpamRate", 115), + ("WildAttackProbability", 116), + ("FocusedProbability", 117), + ("CrashAndTurnProbability", 118), + ("CrashAndTurnRadius", 119), + ("CrashAndTurnBias", 120), + ("GeneratorInitialDelay", 121), + ("AiAcquireHealth", 122), + ("AiAcquireStamina", 123), + ("AiAcquireMana", 124), + ("ResistHealthDrain", 125), + ("LifestoneProtectionTimestamp", 126), + ("AiCounteractEnchantment", 127), + ("AiDispelEnchantment", 128), + ("TradeTimestamp", 129), + ("AiTargetedDetectionRadius", 130), + ("EmotePriority", 131), + ("LastTeleportStartTimestamp", 132), + ("EventSpamTimestamp", 133), + ("EventSpamRate", 134), + ("InventoryOffset", 135), + ("CriticalMultiplier", 136), + ("ManaStoneDestroyChance", 137), + ("SlayerDamageBonus", 138), + ("AllegianceInfoSpamTimestamp", 139), + ("AllegianceInfoSpamRate", 140), + ("NextSpellcastTimestamp", 141), + ("AppraisalRequestedTimestamp", 142), + ("AppraisalHeartbeatDueTimestamp", 143), + ("ManaConversionMod", 144), + ("LastPkAttackTimestamp", 145), + ("FellowshipUpdateTimestamp", 146), + ("CriticalFrequency", 147), + ("LimboStartTimestamp", 148), + ("WeaponMissileDefense", 149), + ("WeaponMagicDefense", 150), + ("IgnoreShield", 151), + ("ElementalDamageMod", 152), + ("StartMissileAttackTimestamp", 153), + ("LastRareUsedTimestamp", 154), + ("IgnoreArmor", 155), + ("ProcSpellRate", 156), + ("ResistanceModifier", 157), + ("AllegianceGagTimestamp", 158), + ("AbsorbMagicDamage", 159), + ("CachedMaxAbsorbMagicDamage", 160), + ("GagDuration", 161), + ("AllegianceGagDuration", 162), + ("GlobalXpMod", 163), + ("HealingModifier", 164), + ("ArmorModVsNether", 165), + ("ResistNether", 166), + ("CooldownDuration", 167), + ("WeaponAuraOffense", 168), + ("WeaponAuraDefense", 169), + ("WeaponAuraElemental", 170), + ("WeaponAuraManaConv", 171), + ]; + + private static readonly (string Name, uint Value)[] PropertyStringGolden = + [ + ("Undef", 0), + ("Name", 1), + ("Title", 2), + ("Sex", 3), + ("HeritageGroup", 4), + ("Template", 5), + ("AttackersName", 6), + ("Inscription", 7), + ("ScribeName", 8), + ("VendorsName", 9), + ("Fellowship", 10), + ("MonarchsName", 11), + ("LockCode", 12), + ("KeyCode", 13), + ("Use", 14), + ("ShortDesc", 15), + ("LongDesc", 16), + ("ActivationTalk", 17), + ("UseMessage", 18), + ("ItemHeritageGroupRestriction", 19), + ("PluralName", 20), + ("MonarchsTitle", 21), + ("ActivationFailure", 22), + ("ScribeAccount", 23), + ("TownName", 24), + ("CraftsmanName", 25), + ("UsePkServerError", 26), + ("ScoreCachedText", 27), + ("ScoreDefaultEntryFormat", 28), + ("ScoreFirstEntryFormat", 29), + ("ScoreLastEntryFormat", 30), + ("ScoreOnlyEntryFormat", 31), + ("ScoreNoEntry", 32), + ("Quest", 33), + ("GeneratorEvent", 34), + ("PatronsTitle", 35), + ("HouseOwnerName", 36), + ("QuestRestriction", 37), + ("AppraisalPortalDestination", 38), + ("TinkerName", 39), + ("ImbuerName", 40), + ("HouseOwnerAccount", 41), + ("DisplayName", 42), + ("DateOfBirth", 43), + ("ThirdPartyApi", 44), + ("KillQuest", 45), + ("Afk", 46), + ("AllegianceName", 47), + ("AugmentationAddQuest", 48), + ("KillQuest2", 49), + ("KillQuest3", 50), + ("UseSendsSignal", 51), + ("GearPlatingName", 52), + ]; + + private static readonly (string Name, uint Value)[] PropertyDataIdGolden = + [ + ("Undef", 0), + ("Setup", 1), + ("MotionTable", 2), + ("SoundTable", 3), + ("CombatTable", 4), + ("QualityFilter", 5), + ("PaletteBase", 6), + ("ClothingBase", 7), + ("Icon", 8), + ("EyesTexture", 9), + ("NoseTexture", 10), + ("MouthTexture", 11), + ("DefaultEyesTexture", 12), + ("DefaultNoseTexture", 13), + ("DefaultMouthTexture", 14), + ("HairPalette", 15), + ("EyesPalette", 16), + ("SkinPalette", 17), + ("HeadObject", 18), + ("ActivationAnimation", 19), + ("InitMotion", 20), + ("ActivationSound", 21), + ("PhysicsEffectTable", 22), + ("UseSound", 23), + ("UseTargetAnimation", 24), + ("UseTargetSuccessAnimation", 25), + ("UseTargetFailureAnimation", 26), + ("UseUserAnimation", 27), + ("Spell", 28), + ("SpellComponent", 29), + ("PhysicsScript", 30), + ("LinkedPortalOne", 31), + ("WieldedTreasureType", 32), + ("UnknownGuessedname", 33), + ("UnknownGuessedname2", 34), + ("DeathTreasureType", 35), + ("MutateFilter", 36), + ("ItemSkillLimit", 37), + ("UseCreateItem", 38), + ("DeathSpell", 39), + ("VendorsClassId", 40), + ("ItemSpecializedOnly", 41), + ("HouseId", 42), + ("AccountHouseId", 43), + ("RestrictionEffect", 44), + ("CreationMutationFilter", 45), + ("TsysMutationFilter", 46), + ("LastPortal", 47), + ("LinkedPortalTwo", 48), + ("OriginalPortal", 49), + ("IconOverlay", 50), + ("IconOverlaySecondary", 51), + ("IconUnderlay", 52), + ("AugmentationMutationFilter", 53), + ("AugmentationEffect", 54), + ("ProcSpell", 55), + ("AugmentationCreateItem", 56), + ("AlternateCurrency", 57), + ("BlueSurgeSpell", 58), + ("YellowSurgeSpell", 59), + ("RedSurgeSpell", 60), + ("OlthoiDeathTreasureType", 61), + ]; + + private static readonly (string Name, uint Value)[] PropertyInstanceIdGolden = + [ + ("Undef", 0), + ("Owner", 1), + ("Container", 2), + ("Wielder", 3), + ("Freezer", 4), + ("Viewer", 5), + ("Generator", 6), + ("Scribe", 7), + ("CurrentCombatTarget", 8), + ("CurrentEnemy", 9), + ("ProjectileLauncher", 10), + ("CurrentAttacker", 11), + ("CurrentDamager", 12), + ("CurrentFollowTarget", 13), + ("CurrentAppraisalTarget", 14), + ("CurrentFellowshipAppraisalTarget", 15), + ("ActivationTarget", 16), + ("Creator", 17), + ("Victim", 18), + ("Killer", 19), + ("Vendor", 20), + ("Customer", 21), + ("Bonded", 22), + ("Wounder", 23), + ("Allegiance", 24), + ("Patron", 25), + ("Monarch", 26), + ("CombatTarget", 27), + ("HealthQueryTarget", 28), + ("LastUnlocker", 29), + ("CrashAndTurnTarget", 30), + ("AllowedActivator", 31), + ("HouseOwner", 32), + ("House", 33), + ("Slumlord", 34), + ("ManaQueryTarget", 35), + ("CurrentGame", 36), + ("RequestedAppraisalTarget", 37), + ("AllowedWielder", 38), + ("AssignedTarget", 39), + ("LimboSource", 40), + ("Snooper", 41), + ("TeleportedCharacter", 42), + ("Pet", 43), + ("PetOwner", 44), + ("PetDevice", 45), + ]; + + public static TheoryData Families => + new("PropertyInt", "PropertyInt64", "PropertyBool", "PropertyFloat", "PropertyString", "PropertyDataId", "PropertyInstanceId"); + + private static (Type Enum, (string Name, uint Value)[] Golden) Resolve(string family) => family switch + { + "PropertyInt" => (typeof(PropertyInt), PropertyIntGolden), + "PropertyInt64" => (typeof(PropertyInt64), PropertyInt64Golden), + "PropertyBool" => (typeof(PropertyBool), PropertyBoolGolden), + "PropertyFloat" => (typeof(PropertyFloat), PropertyFloatGolden), + "PropertyString" => (typeof(PropertyString), PropertyStringGolden), + "PropertyDataId" => (typeof(PropertyDataId), PropertyDataIdGolden), + "PropertyInstanceId" => (typeof(PropertyInstanceId), PropertyInstanceIdGolden), + _ => throw new ArgumentOutOfRangeException(nameof(family), family, "unknown property family"), + }; + + [Theory] + [MemberData(nameof(Families))] + public void EnumMatchesGoldenTableExactly(string family) + { + var (type, golden) = Resolve(family); + var actual = Enum.GetNames(type) + .Select(n => (Name: n, Value: Convert.ToUInt32(Enum.Parse(type, n)))) + .OrderBy(e => e.Value).ThenBy(e => e.Name, StringComparer.Ordinal) + .ToArray(); + var expected = golden + .OrderBy(e => e.Value).ThenBy(e => e.Name, StringComparer.Ordinal) + .ToArray(); + Assert.Equal(expected, actual); + } + + [Theory] + [MemberData(nameof(Families))] + public void UnderlyingTypeIsUInt32(string family) + { + var (type, _) = Resolve(family); + Assert.Equal(typeof(uint), Enum.GetUnderlyingType(type)); + } + + [Theory] + [MemberData(nameof(Families))] + public void NoTwoMembersShareAValue(string family) + { + var (_, golden) = Resolve(family); + var dupes = golden.GroupBy(e => e.Value) + .Where(g => g.Count() > 1) + .Select(g => g.Key + ": " + string.Join(", ", g.Select(e => e.Name))) + .ToArray(); + Assert.Empty(dupes); + } + + /// + /// The name/value pairs the ACE weenie export corpus attests directly. These are + /// the two-oracle-confirmed members; the campaign found zero value conflicts + /// between the corpus and the client-side catalog across all seven tables. + /// + public static TheoryData WeenieAttested + { + get + { + var d = new TheoryData(); + d.Add("PropertyInt", "ItemType", 1); + d.Add("PropertyInt", "CreatureType", 2); + d.Add("PropertyInt", "PaletteTemplate", 3); + d.Add("PropertyInt", "ClothingPriority", 4); + d.Add("PropertyInt", "EncumbranceVal", 5); + d.Add("PropertyInt", "ItemsCapacity", 6); + d.Add("PropertyInt", "ContainersCapacity", 7); + d.Add("PropertyInt", "Mass", 8); + d.Add("PropertyInt", "ValidLocations", 9); + d.Add("PropertyInt", "MaxStackSize", 11); + d.Add("PropertyInt", "StackSize", 12); + d.Add("PropertyInt", "StackUnitEncumbrance", 13); + d.Add("PropertyInt", "StackUnitMass", 14); + d.Add("PropertyInt", "StackUnitValue", 15); + d.Add("PropertyInt", "ItemUseable", 16); + d.Add("PropertyInt", "RareId", 17); + d.Add("PropertyInt", "UiEffects", 18); + d.Add("PropertyInt", "Value", 19); + d.Add("PropertyInt", "CoinValue", 20); + d.Add("PropertyInt", "Level", 25); + d.Add("PropertyInt", "AccountRequirements", 26); + d.Add("PropertyInt", "ArmorType", 27); + d.Add("PropertyInt", "ArmorLevel", 28); + d.Add("PropertyInt", "AllegianceRank", 30); + d.Add("PropertyInt", "Bonded", 33); + d.Add("PropertyInt", "ResistMagic", 36); + d.Add("PropertyInt", "ResistItemAppraisal", 37); + d.Add("PropertyInt", "ResistLockpick", 38); + d.Add("PropertyInt", "CombatMode", 40); + d.Add("PropertyInt", "NumDeaths", 43); + d.Add("PropertyInt", "Damage", 44); + d.Add("PropertyInt", "DamageType", 45); + d.Add("PropertyInt", "DefaultCombatStyle", 46); + d.Add("PropertyInt", "AttackType", 47); + d.Add("PropertyInt", "WeaponSkill", 48); + d.Add("PropertyInt", "WeaponTime", 49); + d.Add("PropertyInt", "AmmoType", 50); + d.Add("PropertyInt", "CombatUse", 51); + d.Add("PropertyInt", "ParentLocation", 52); + d.Add("PropertyInt", "PlacementPosition", 53); + d.Add("PropertyInt", "WeaponRange", 60); + d.Add("PropertyInt", "CheckpointStatus", 66); + d.Add("PropertyInt", "Tolerance", 67); + d.Add("PropertyInt", "TargetingTactic", 68); + d.Add("PropertyInt", "CombatTactic", 69); + d.Add("PropertyInt", "FriendType", 72); + d.Add("PropertyInt", "MerchandiseItemTypes", 74); + d.Add("PropertyInt", "MerchandiseMinValue", 75); + d.Add("PropertyInt", "MerchandiseMaxValue", 76); + d.Add("PropertyInt", "MaxGeneratedObjects", 81); + d.Add("PropertyInt", "InitGeneratedObjects", 82); + d.Add("PropertyInt", "ActivationResponse", 83); + d.Add("PropertyInt", "MinLevel", 86); + d.Add("PropertyInt", "MaxLevel", 87); + d.Add("PropertyInt", "LockpickMod", 88); + d.Add("PropertyInt", "BoosterEnum", 89); + d.Add("PropertyInt", "BoostValue", 90); + d.Add("PropertyInt", "MaxStructure", 91); + d.Add("PropertyInt", "Structure", 92); + d.Add("PropertyInt", "PhysicsState", 93); + d.Add("PropertyInt", "TargetType", 94); + d.Add("PropertyInt", "RadarBlipColor", 95); + d.Add("PropertyInt", "EncumbranceCapacity", 96); + d.Add("PropertyInt", "CreationTimestamp", 98); + d.Add("PropertyInt", "PkLevelModifier", 99); + d.Add("PropertyInt", "GeneratorType", 100); + d.Add("PropertyInt", "AiAllowedCombatStyle", 101); + d.Add("PropertyInt", "GeneratorDestructionType", 103); + d.Add("PropertyInt", "ItemWorkmanship", 105); + d.Add("PropertyInt", "ItemSpellcraft", 106); + d.Add("PropertyInt", "ItemCurMana", 107); + d.Add("PropertyInt", "ItemMaxMana", 108); + d.Add("PropertyInt", "ItemDifficulty", 109); + d.Add("PropertyInt", "ItemAllegianceRankLimit", 110); + d.Add("PropertyInt", "PortalBitmask", 111); + d.Add("PropertyInt", "Gender", 113); + d.Add("PropertyInt", "Attuned", 114); + d.Add("PropertyInt", "ItemSkillLevelLimit", 115); + d.Add("PropertyInt", "ItemManaCost", 117); + d.Add("PropertyInt", "Active", 119); + d.Add("PropertyInt", "Age", 125); + d.Add("PropertyInt", "VendorHappyMean", 126); + d.Add("PropertyInt", "VendorHappyVariance", 127); + d.Add("PropertyInt", "MaterialType", 131); + d.Add("PropertyInt", "ShowableOnRadar", 133); + d.Add("PropertyInt", "PlayerKillerStatus", 134); + d.Add("PropertyInt", "ScorePageNum", 136); + d.Add("PropertyInt", "ScoreConfigNum", 137); + d.Add("PropertyInt", "ScoreNumScores", 138); + d.Add("PropertyInt", "AiOptions", 140); + d.Add("PropertyInt", "GeneratorTimeType", 142); + d.Add("PropertyInt", "GeneratorStartTime", 143); + d.Add("PropertyInt", "GeneratorEndTime", 144); + d.Add("PropertyInt", "GeneratorEndDestructionType", 145); + d.Add("PropertyInt", "XpOverride", 146); + d.Add("PropertyInt", "HouseStatus", 149); + d.Add("PropertyInt", "HookPlacement", 150); + d.Add("PropertyInt", "HookType", 151); + d.Add("PropertyInt", "HookItemType", 152); + d.Add("PropertyInt", "HouseType", 155); + d.Add("PropertyInt", "PickupEmoteOffset", 156); + d.Add("PropertyInt", "WeenieIteration", 157); + d.Add("PropertyInt", "WieldRequirements", 158); + d.Add("PropertyInt", "WieldSkilltype", 159); + d.Add("PropertyInt", "WieldDifficulty", 160); + d.Add("PropertyInt", "HouseMaxHooksUsable", 161); + d.Add("PropertyInt", "AllegianceMinLevel", 163); + d.Add("PropertyInt", "SlayerCreatureType", 166); + d.Add("PropertyInt", "TsysMutationData", 169); + d.Add("PropertyInt", "NumItemsInMaterial", 170); + d.Add("PropertyInt", "NumTimesTinkered", 171); + d.Add("PropertyInt", "AppraisalLongDescDecoration", 172); + d.Add("PropertyInt", "AppraisalLockpickSuccessPercent", 173); + d.Add("PropertyInt", "AppraisalPages", 174); + d.Add("PropertyInt", "AppraisalMaxPages", 175); + d.Add("PropertyInt", "AppraisalItemSkill", 176); + d.Add("PropertyInt", "GemCount", 177); + d.Add("PropertyInt", "GemType", 178); + d.Add("PropertyInt", "ImbuedEffect", 179); + d.Add("PropertyInt", "TypeOfAlteration", 185); + d.Add("PropertyInt", "SkillToBeAltered", 186); + d.Add("PropertyInt", "HeritageGroup", 188); + d.Add("PropertyInt", "TransferFromAttribute", 189); + d.Add("PropertyInt", "TransferToAttribute", 190); + d.Add("PropertyInt", "NumKeys", 193); + d.Add("PropertyInt", "HookGroup", 197); + d.Add("PropertyInt", "ElementalDamageBonus", 204); + d.Add("PropertyInt", "ItemAttributeLimit", 257); + d.Add("PropertyInt", "ItemAttributeLevelLimit", 258); + d.Add("PropertyInt", "CharacterTitleId", 261); + d.Add("PropertyInt", "NumCharacterTitles", 262); + d.Add("PropertyInt", "ResistanceModifierType", 263); + d.Add("PropertyInt", "EquipmentSetId", 265); + d.Add("PropertyInt", "Lifespan", 267); + d.Add("PropertyInt", "RemainingLifespan", 268); + d.Add("PropertyInt", "WieldRequirements2", 270); + d.Add("PropertyInt", "WieldSkilltype2", 271); + d.Add("PropertyInt", "WieldDifficulty2", 272); + d.Add("PropertyInt", "WieldRequirements3", 273); + d.Add("PropertyInt", "WieldSkilltype3", 274); + d.Add("PropertyInt", "WieldDifficulty3", 275); + d.Add("PropertyInt", "WieldRequirements4", 276); + d.Add("PropertyInt", "WieldSkilltype4", 277); + d.Add("PropertyInt", "WieldDifficulty4", 278); + d.Add("PropertyInt", "Unique", 279); + d.Add("PropertyInt", "SharedCooldown", 280); + d.Add("PropertyInt", "Faction1Bits", 281); + d.Add("PropertyInt", "SocietyRankCelhan", 287); + d.Add("PropertyInt", "SocietyRankEldweb", 288); + d.Add("PropertyInt", "SocietyRankRadblo", 289); + d.Add("PropertyInt", "Cleaving", 292); + d.Add("PropertyInt", "ImbuedEffect2", 303); + d.Add("PropertyInt", "ImbuedEffect3", 304); + d.Add("PropertyInt", "ImbuedEffect4", 305); + d.Add("PropertyInt", "ImbuedEffect5", 306); + d.Add("PropertyInt", "DamageRating", 307); + d.Add("PropertyInt", "DamageResistRating", 308); + d.Add("PropertyInt", "CritRating", 313); + d.Add("PropertyInt", "CritDamageRating", 314); + d.Add("PropertyInt", "CritResistRating", 315); + d.Add("PropertyInt", "CritDamageResistRating", 316); + d.Add("PropertyInt", "ItemMaxLevel", 319); + d.Add("PropertyInt", "ItemXpStyle", 320); + d.Add("PropertyInt", "HeritageSpecificArmor", 324); + d.Add("PropertyInt", "CloakWeaveProc", 352); + d.Add("PropertyInt", "WeaponType", 353); + d.Add("PropertyInt", "UseRequiresSkill", 366); + d.Add("PropertyInt", "UseRequiresSkillLevel", 367); + d.Add("PropertyInt", "UseRequiresSkillSpec", 368); + d.Add("PropertyInt", "UseRequiresLevel", 369); + d.Add("PropertyInt", "GearDamage", 370); + d.Add("PropertyInt", "GearDamageResist", 371); + d.Add("PropertyInt", "GearCrit", 372); + d.Add("PropertyInt", "GearCritResist", 373); + d.Add("PropertyInt", "GearCritDamage", 374); + d.Add("PropertyInt", "GearCritDamageResist", 375); + d.Add("PropertyInt", "GearHealingBoost", 376); + d.Add("PropertyInt", "GearNetherResist", 377); + d.Add("PropertyInt", "GearLifeResist", 378); + d.Add("PropertyInt", "GearMaxHealth", 379); + d.Add("PropertyInt", "PKDamageRating", 381); + d.Add("PropertyInt", "PKDamageResistRating", 382); + d.Add("PropertyInt", "GearPKDamageRating", 383); + d.Add("PropertyInt", "GearPKDamageResistRating", 384); + d.Add("PropertyInt", "Overpower", 386); + d.Add("PropertyInt", "OverpowerResist", 387); + d.Add("PropertyInt", "GearOverpower", 388); + d.Add("PropertyInt", "GearOverpowerResist", 389); + d.Add("PropertyInt", "Enlightenment", 390); + d.Add("PropertyInt64", "AugmentationCost", 3); + d.Add("PropertyInt64", "ItemTotalXp", 4); + d.Add("PropertyInt64", "ItemBaseXp", 5); + d.Add("PropertyBool", "Stuck", 1); + d.Add("PropertyBool", "Open", 2); + d.Add("PropertyBool", "Locked", 3); + d.Add("PropertyBool", "AiUsesMana", 6); + d.Add("PropertyBool", "AiUseHumanMagicAnimations", 7); + d.Add("PropertyBool", "AllowGive", 8); + d.Add("PropertyBool", "IgnoreCollisions", 11); + d.Add("PropertyBool", "ReportCollisions", 12); + d.Add("PropertyBool", "Ethereal", 13); + d.Add("PropertyBool", "GravityStatus", 14); + d.Add("PropertyBool", "LightsStatus", 15); + d.Add("PropertyBool", "ScriptedCollision", 16); + d.Add("PropertyBool", "Inelastic", 17); + d.Add("PropertyBool", "Visibility", 18); + d.Add("PropertyBool", "Attackable", 19); + d.Add("PropertyBool", "Inscribable", 22); + d.Add("PropertyBool", "DestroyOnSell", 23); + d.Add("PropertyBool", "UiHidden", 24); + d.Add("PropertyBool", "NoCorpse", 29); + d.Add("PropertyBool", "ResetMessagePending", 33); + d.Add("PropertyBool", "DefaultOpen", 34); + d.Add("PropertyBool", "DefaultLocked", 35); + d.Add("PropertyBool", "DealMagicalItems", 39); + d.Add("PropertyBool", "ReportCollisionsAsEnvironment", 41); + d.Add("PropertyBool", "AllowEdgeSlide", 42); + d.Add("PropertyBool", "NeverFailCasting", 50); + d.Add("PropertyBool", "VendorService", 51); + d.Add("PropertyBool", "AiImmobile", 52); + d.Add("PropertyBool", "DamagedByCollisions", 53); + d.Add("PropertyBool", "IsDynamic", 54); + d.Add("PropertyBool", "IsHot", 55); + d.Add("PropertyBool", "AffectsAis", 57); + d.Add("PropertyBool", "LoggingChannel", 61); + d.Add("PropertyBool", "OpensAnyLock", 62); + d.Add("PropertyBool", "UnlimitedUse", 63); + d.Add("PropertyBool", "IgnoreMagicResist", 65); + d.Add("PropertyBool", "IgnoreMagicArmor", 66); + d.Add("PropertyBool", "SpellComponentsRequired", 68); + d.Add("PropertyBool", "IsSellable", 69); + d.Add("PropertyBool", "IgnoreShieldsBySkill", 70); + d.Add("PropertyBool", "NoDraw", 71); + d.Add("PropertyBool", "GeneratorAutomaticDestruction", 74); + d.Add("PropertyBool", "HouseRequiresMonarch", 76); + d.Add("PropertyBool", "AiAcceptEverything", 79); + d.Add("PropertyBool", "RequiresBackpackSlot", 81); + d.Add("PropertyBool", "DontTurnOrMoveWhenGiving", 82); + d.Add("PropertyBool", "NpcLooksLikeObject", 83); + d.Add("PropertyBool", "IgnoreCloIcons", 84); + d.Add("PropertyBool", "AppraisalHasAllowedWielder", 85); + d.Add("PropertyBool", "PortalShowDestination", 88); + d.Add("PropertyBool", "PortalIgnoresPkAttackTimer", 89); + d.Add("PropertyBool", "NpcInteractsSilently", 90); + d.Add("PropertyBool", "Retained", 91); + d.Add("PropertyBool", "IgnoreAuthor", 92); + d.Add("PropertyBool", "Ivoryable", 99); + d.Add("PropertyBool", "Dyable", 100); + d.Add("PropertyBool", "NonProjectileMagicImmune", 103); + d.Add("PropertyBool", "RareUsesTimer", 108); + d.Add("PropertyBool", "AutowieldLeft", 130); + d.Add("PropertyFloat", "HeartbeatInterval", 1); + d.Add("PropertyFloat", "HeartbeatTimestamp", 2); + d.Add("PropertyFloat", "HealthRate", 3); + d.Add("PropertyFloat", "StaminaRate", 4); + d.Add("PropertyFloat", "ManaRate", 5); + d.Add("PropertyFloat", "HealthUponResurrection", 6); + d.Add("PropertyFloat", "StaminaUponResurrection", 7); + d.Add("PropertyFloat", "ManaUponResurrection", 8); + d.Add("PropertyFloat", "ResetInterval", 11); + d.Add("PropertyFloat", "Shade", 12); + d.Add("PropertyFloat", "ArmorModVsSlash", 13); + d.Add("PropertyFloat", "ArmorModVsPierce", 14); + d.Add("PropertyFloat", "ArmorModVsBludgeon", 15); + d.Add("PropertyFloat", "ArmorModVsCold", 16); + d.Add("PropertyFloat", "ArmorModVsFire", 17); + d.Add("PropertyFloat", "ArmorModVsAcid", 18); + d.Add("PropertyFloat", "ArmorModVsElectric", 19); + d.Add("PropertyFloat", "WeaponLength", 21); + d.Add("PropertyFloat", "DamageVariance", 22); + d.Add("PropertyFloat", "MaximumVelocity", 26); + d.Add("PropertyFloat", "RotationSpeed", 27); + d.Add("PropertyFloat", "WeaponDefense", 29); + d.Add("PropertyFloat", "VisualAwarenessRange", 31); + d.Add("PropertyFloat", "PowerupTime", 34); + d.Add("PropertyFloat", "ChargeSpeed", 36); + d.Add("PropertyFloat", "BuyPrice", 37); + d.Add("PropertyFloat", "SellPrice", 38); + d.Add("PropertyFloat", "DefaultScale", 39); + d.Add("PropertyFloat", "LockpickMod", 40); + d.Add("PropertyFloat", "RegenerationInterval", 41); + d.Add("PropertyFloat", "GeneratorRadius", 43); + d.Add("PropertyFloat", "TimeToRot", 44); + d.Add("PropertyFloat", "MinimumTimeSincePk", 50); + d.Add("PropertyFloat", "UseRadius", 54); + d.Add("PropertyFloat", "HomeRadius", 55); + d.Add("PropertyFloat", "WeaponOffense", 62); + d.Add("PropertyFloat", "DamageMod", 63); + d.Add("PropertyFloat", "ResistSlash", 64); + d.Add("PropertyFloat", "ResistPierce", 65); + d.Add("PropertyFloat", "ResistBludgeon", 66); + d.Add("PropertyFloat", "ResistFire", 67); + d.Add("PropertyFloat", "ResistCold", 68); + d.Add("PropertyFloat", "ResistAcid", 69); + d.Add("PropertyFloat", "ResistElectric", 70); + d.Add("PropertyFloat", "ResistHealthBoost", 71); + d.Add("PropertyFloat", "ResistStaminaDrain", 72); + d.Add("PropertyFloat", "ResistStaminaBoost", 73); + d.Add("PropertyFloat", "ResistManaDrain", 74); + d.Add("PropertyFloat", "ResistManaBoost", 75); + d.Add("PropertyFloat", "Translucency", 76); + d.Add("PropertyFloat", "PhysicsScriptIntensity", 77); + d.Add("PropertyFloat", "Friction", 78); + d.Add("PropertyFloat", "Elasticity", 79); + d.Add("PropertyFloat", "AiUseMagicDelay", 80); + d.Add("PropertyFloat", "ItemEfficiency", 87); + d.Add("PropertyFloat", "HealkitMod", 100); + d.Add("PropertyFloat", "ObviousRadarRange", 104); + d.Add("PropertyFloat", "HotspotCycleTime", 105); + d.Add("PropertyFloat", "HotspotCycleTimeVariance", 106); + d.Add("PropertyFloat", "BondWieldedTreasure", 109); + d.Add("PropertyFloat", "BulkMod", 110); + d.Add("PropertyFloat", "SizeMod", 111); + d.Add("PropertyFloat", "FocusedProbability", 117); + d.Add("PropertyFloat", "GeneratorInitialDelay", 121); + d.Add("PropertyFloat", "AiAcquireHealth", 122); + d.Add("PropertyFloat", "AiAcquireStamina", 123); + d.Add("PropertyFloat", "AiAcquireMana", 124); + d.Add("PropertyFloat", "ResistHealthDrain", 125); + d.Add("PropertyFloat", "AiCounteractEnchantment", 127); + d.Add("PropertyFloat", "AiDispelEnchantment", 128); + d.Add("PropertyFloat", "EmotePriority", 131); + d.Add("PropertyFloat", "InventoryOffset", 135); + d.Add("PropertyFloat", "CriticalMultiplier", 136); + d.Add("PropertyFloat", "ManaStoneDestroyChance", 137); + d.Add("PropertyFloat", "SlayerDamageBonus", 138); + d.Add("PropertyFloat", "ManaConversionMod", 144); + d.Add("PropertyFloat", "CriticalFrequency", 147); + d.Add("PropertyFloat", "WeaponMissileDefense", 149); + d.Add("PropertyFloat", "WeaponMagicDefense", 150); + d.Add("PropertyFloat", "IgnoreShield", 151); + d.Add("PropertyFloat", "ElementalDamageMod", 152); + d.Add("PropertyFloat", "IgnoreArmor", 155); + d.Add("PropertyFloat", "ResistanceModifier", 157); + d.Add("PropertyFloat", "AbsorbMagicDamage", 159); + d.Add("PropertyFloat", "ArmorModVsNether", 165); + d.Add("PropertyFloat", "CooldownDuration", 167); + d.Add("PropertyString", "Name", 1); + d.Add("PropertyString", "Sex", 3); + d.Add("PropertyString", "HeritageGroup", 4); + d.Add("PropertyString", "Template", 5); + d.Add("PropertyString", "Inscription", 7); + d.Add("PropertyString", "ScribeName", 8); + d.Add("PropertyString", "Fellowship", 10); + d.Add("PropertyString", "LockCode", 12); + d.Add("PropertyString", "KeyCode", 13); + d.Add("PropertyString", "Use", 14); + d.Add("PropertyString", "ShortDesc", 15); + d.Add("PropertyString", "LongDesc", 16); + d.Add("PropertyString", "ActivationTalk", 17); + d.Add("PropertyString", "UseMessage", 18); + d.Add("PropertyString", "ItemHeritageGroupRestriction", 19); + d.Add("PropertyString", "PluralName", 20); + d.Add("PropertyString", "ActivationFailure", 22); + d.Add("PropertyString", "TownName", 24); + d.Add("PropertyString", "CraftsmanName", 25); + d.Add("PropertyString", "UsePkServerError", 26); + d.Add("PropertyString", "ScoreDefaultEntryFormat", 28); + d.Add("PropertyString", "ScoreFirstEntryFormat", 29); + d.Add("PropertyString", "ScoreLastEntryFormat", 30); + d.Add("PropertyString", "ScoreOnlyEntryFormat", 31); + d.Add("PropertyString", "ScoreNoEntry", 32); + d.Add("PropertyString", "Quest", 33); + d.Add("PropertyString", "GeneratorEvent", 34); + d.Add("PropertyString", "QuestRestriction", 37); + d.Add("PropertyDataId", "Setup", 1); + d.Add("PropertyDataId", "MotionTable", 2); + d.Add("PropertyDataId", "SoundTable", 3); + d.Add("PropertyDataId", "CombatTable", 4); + d.Add("PropertyDataId", "QualityFilter", 5); + d.Add("PropertyDataId", "PaletteBase", 6); + d.Add("PropertyDataId", "ClothingBase", 7); + d.Add("PropertyDataId", "Icon", 8); + d.Add("PropertyDataId", "EyesTexture", 9); + d.Add("PropertyDataId", "NoseTexture", 10); + d.Add("PropertyDataId", "MouthTexture", 11); + d.Add("PropertyDataId", "HairPalette", 15); + d.Add("PropertyDataId", "EyesPalette", 16); + d.Add("PropertyDataId", "SkinPalette", 17); + d.Add("PropertyDataId", "ActivationAnimation", 19); + d.Add("PropertyDataId", "InitMotion", 20); + d.Add("PropertyDataId", "PhysicsEffectTable", 22); + d.Add("PropertyDataId", "UseSound", 23); + d.Add("PropertyDataId", "UseTargetAnimation", 24); + d.Add("PropertyDataId", "UseTargetSuccessAnimation", 25); + d.Add("PropertyDataId", "UseTargetFailureAnimation", 26); + d.Add("PropertyDataId", "UseUserAnimation", 27); + d.Add("PropertyDataId", "Spell", 28); + d.Add("PropertyDataId", "SpellComponent", 29); + d.Add("PropertyDataId", "PhysicsScript", 30); + d.Add("PropertyDataId", "LinkedPortalOne", 31); + d.Add("PropertyDataId", "WieldedTreasureType", 32); + d.Add("PropertyDataId", "UnknownGuessedname", 33); + d.Add("PropertyDataId", "DeathTreasureType", 35); + d.Add("PropertyDataId", "MutateFilter", 36); + d.Add("PropertyDataId", "ItemSkillLimit", 37); + d.Add("PropertyDataId", "UseCreateItem", 38); + d.Add("PropertyDataId", "ItemSpecializedOnly", 41); + d.Add("PropertyDataId", "HouseId", 42); + d.Add("PropertyDataId", "RestrictionEffect", 44); + d.Add("PropertyDataId", "TsysMutationFilter", 46); + d.Add("PropertyDataId", "LinkedPortalTwo", 48); + d.Add("PropertyDataId", "IconOverlay", 50); + d.Add("PropertyDataId", "IconOverlaySecondary", 51); + d.Add("PropertyDataId", "IconUnderlay", 52); + d.Add("PropertyDataId", "ProcSpell", 55); + d.Add("PropertyInstanceId", "ActivationTarget", 16); + d.Add("PropertyInstanceId", "AllowedWielder", 38); + return d; + } + } + + [Theory] + [MemberData(nameof(WeenieAttested))] + public void WeenieAttestedMemberHasExactWireValue(string family, string name, uint value) + { + var (type, _) = Resolve(family); + Assert.True(Enum.IsDefined(type, value), $"{family}.{name} ({value}) is not defined"); + Assert.Equal(name, Enum.GetName(type, value)); + } +} From f3e95a3ebdff2663e6637c564183f4612bd5384b Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 29 Jul 2026 01:21:10 +0200 Subject: [PATCH 2/5] fix(core): correct DamageType's rotated bits and ItemType's shifted craft ladder Two enums disagreed with the retail client, and both disagreements were the quiet kind - nothing read the wrong members, so nothing was visibly broken. They were traps armed for the first person to write a comparison against them. DamageType had its four drain/restore bits rotated. acdream assigned Nether/Mana/Health/Stamina to 0x80/0x100/0x200/0x400; retail's DAMAGE_TYPE (acclient.h:3788) assigns Health/Stamina/Mana/Nether. The ACE weenie corpus attests retail's order independently - 0x100 Stamina, 0x200 Mana, 0x400 Nether - and so does the vendored client-side enum catalog. Tellingly, both of acdream's live damage-type name tables, CombatChatTranslator.FormatDamageType (ported from holtburger) and ItemAppraisalTextFormatter.TryDamageTypeName, already used retail's order reading the raw wire uint directly. The enum was the only thing in the tree that was wrong. Retail's BASE_DAMAGE_TYPE (0x10000000) was also missing; CombatChatTranslator already knew about it. ItemType had two separate problems. The craft ladder was shifted one bit: CraftAlchemyIntermediate sat on 0x02000000, which retail leaves unused, and an invented CraftCookingIntermediate occupied 0x04000000, which is retail's real alchemy-intermediate bit. The weenie corpus attests 0x04000000 as Craft_Alchemy_Intermediate 235 times and contains no cooking-intermediate at all - there is no such item type. Separately, the composite masks were recomputed locally from the bits above them instead of transcribed, which is exactly how the ladder drifted in the first place. That made Weapon (retail 0x101, melee|missile) an exact alias of WeaponOrCaster (0x8101), and left Item at 0x830F where retail's TYPE_ITEM is 0x2DFBEF - a mask two orders of magnitude broader. The composites are now transcribed as literals with retail's value, not derived, and the five retail-only masks acdream never had (portal/lockable magic targets, the enchantable and redirectable targets, and the two vendor masks) come along. Note for the reader wondering why the campaign trusted retail over the catalog here: on CraftFletchingBase the catalog is the one that is wrong (it says 0x02000000; retail and acdream both say 0x01000000). No single oracle was assumed correct - retail's header decided, with the weenie corpus as the tiebreak. Behavior: no production code reads any changed member. The only reference in the tree is a test that wants a nonzero HookItemTypes and does not care which. So no branch changes and no wire behavior moves - but the values did change, which is why this is a fix commit and not a data commit. No divergence-register row: these were unintentional errors, now retired, not deviations we chose. RetailEnumConformanceTests pins both enums to the acclient.h tables, asserts acdream declares nothing retail does not, and calls out the two specific traps - that 0x02000000 stays unclaimed, and that Weapon and WeaponOrCaster are no longer the same value. Core tests 3,726 -> 3,785. Co-Authored-By: Claude Fable 5 --- src/AcDream.Core/Combat/CombatModel.cs | 21 ++- src/AcDream.Core/Items/ClientObject.cs | 36 +++-- .../Properties/RetailEnumConformanceTests.cs | 152 ++++++++++++++++++ 3 files changed, 196 insertions(+), 13 deletions(-) create mode 100644 tests/AcDream.Core.Tests/Properties/RetailEnumConformanceTests.cs diff --git a/src/AcDream.Core/Combat/CombatModel.cs b/src/AcDream.Core/Combat/CombatModel.cs index 1e752102..2e7b8b5d 100644 --- a/src/AcDream.Core/Combat/CombatModel.cs +++ b/src/AcDream.Core/Combat/CombatModel.cs @@ -204,6 +204,18 @@ public enum AttackType : uint | OffhandDoubleThrust | OffhandTripleThrust, } +/// +/// Verbatim retail DAMAGE_TYPE (docs/research/named-retail/acclient.h:3788). +/// +/// The four drain/restore bits used to be rotated here: Nether/Mana/Health/Stamina +/// were assigned 0x80/0x100/0x200/0x400 where retail assigns Health/Stamina/Mana/Nether. +/// Retail, the vendored client-side enum catalog, and the ACE weenie corpus all agree on +/// retail's order (the corpus attests 0x100=Stamina, 0x200=Mana, 0x400=Nether directly), +/// and both of acdream's live damage-type name tables — CombatChatTranslator and +/// ItemAppraisalTextFormatter — were already reading the retail order off the raw +/// wire uint. Nothing consumed the rotated members, so this corrects a latent trap rather +/// than a live mislabel. +/// [Flags] public enum DamageType : uint { @@ -215,10 +227,11 @@ public enum DamageType : uint Fire = 0x0010, Acid = 0x0020, Electric = 0x0040, - Nether = 0x0080, - Mana = 0x0100, - Health = 0x0200, - Stamina = 0x0400, + Health = 0x0080, + Stamina = 0x0100, + Mana = 0x0200, + Nether = 0x0400, + Base = 0x10000000, } /// diff --git a/src/AcDream.Core/Items/ClientObject.cs b/src/AcDream.Core/Items/ClientObject.cs index 8cb5f0d4..fc8b5a6d 100644 --- a/src/AcDream.Core/Items/ClientObject.cs +++ b/src/AcDream.Core/Items/ClientObject.cs @@ -10,9 +10,17 @@ namespace AcDream.Core.Items; /// /// AC's ItemType is a 32-bit flags enum — a single dat weenie can -/// assert multiple type bits. From ACE.Entity.Enum.ItemType -/// cross-checked against the decompile paperdoll tooltip dispatcher. -/// Full bit list in the research doc §1. +/// assert multiple type bits. Verbatim retail ITEM_TYPE +/// (docs/research/named-retail/acclient.h:3300). +/// +/// The craft ladder used to be shifted: CraftAlchemyIntermediate sat on +/// 0x02000000 (retail leaves that bit unused and puts alchemy-intermediate on +/// 0x04000000), and an invented CraftCookingIntermediate occupied the real +/// alchemy-intermediate bit. The ACE weenie corpus attests 0x04000000 = +/// Craft_Alchemy_Intermediate 235 times and contains no cooking-intermediate at all. +/// The composite masks were likewise recomputed locally rather than transcribed, which +/// made Weapon an alias of WeaponOrCaster and left Item two orders +/// of magnitude narrower than retail's. Full bit list in the research doc §1. /// [Flags] public enum ItemType : uint @@ -43,17 +51,27 @@ public enum ItemType : uint CraftCookingBase = 0x00400000, CraftAlchemyBase = 0x00800000, CraftFletchingBase = 0x01000000, - CraftAlchemyIntermediate= 0x02000000, - CraftCookingIntermediate= 0x04000000, + // 0x02000000 is deliberately unused in retail's ladder. + CraftAlchemyIntermediate= 0x04000000, CraftFletchingIntermediate = 0x08000000, LifeStone = 0x10000000, TinkeringTool = 0x20000000, TinkeringMaterial = 0x40000000, Gameboard = 0x80000000u, - Vestements = Armor | Clothing, - Weapon = MeleeWeapon | MissileWeapon | Caster, - WeaponOrCaster = Weapon, - Item = Weapon | Armor | Clothing | Jewelry | Container, + + // Composite masks, transcribed verbatim rather than recomputed from the bits + // above — retail's TYPE_ITEM in particular is far broader than any obvious + // union, and deriving these locally is how the craft ladder drifted. + Vestements = 0x00000006, // TYPE_VESTEMENTS + Weapon = 0x00000101, // TYPE_WEAPON: melee | missile, no caster + WeaponOrCaster = 0x00008101, // TYPE_WEAPON_OR_CASTER + LockableMagicTarget = 0x00000280, // TYPE_LOCKABLE_MAGIC_TARGET + RedirectableItemEnchantmentTarget = 0x00008107, + PortalMagicTarget = 0x10010000, // TYPE_PORTAL_MAGIC_TARGET + ItemEnchantableTarget = 0x00088B8F, // TYPE_ITEM_ENCHANTABLE_TARGET + Item = 0x002DFBEF, // TYPE_ITEM + VendorShopkeep = 0x480467A7, // TYPE_VENDOR_SHOPKEEP + VendorGrocer = 0x00446220, // TYPE_VENDOR_GROCER } /// diff --git a/tests/AcDream.Core.Tests/Properties/RetailEnumConformanceTests.cs b/tests/AcDream.Core.Tests/Properties/RetailEnumConformanceTests.cs new file mode 100644 index 00000000..f5641e9d --- /dev/null +++ b/tests/AcDream.Core.Tests/Properties/RetailEnumConformanceTests.cs @@ -0,0 +1,152 @@ +using System; +using System.Linq; +using AcDream.Core.Combat; +using AcDream.Core.Items; +using Xunit; + +namespace AcDream.Core.Tests.Properties; + +/// +/// Pins the wire-adjacent enums acdream shares with the retail client to the values +/// in the Sept 2013 EoR header, docs/research/named-retail/acclient.h. Each +/// table below is transcribed from the cited enum block, so a hand edit that +/// drifts from retail fails here. +/// +/// These tables are the reason the 2026-07-29 enum campaign found real bugs: +/// had its four drain/restore bits rotated, and +/// had a shifted craft ladder plus locally recomputed composite +/// masks. Both are corrected and pinned below. +/// +public sealed class RetailEnumConformanceTests +{ + /// acclient.h:3788, enum DAMAGE_TYPE. FORCE_*_32_BIT omitted. + public static TheoryData RetailDamageType => new() + { + { "Undef", 0x0 }, + { "Slash", 0x1 }, + { "Pierce", 0x2 }, + { "Bludgeon", 0x4 }, + { "Cold", 0x8 }, + { "Fire", 0x10 }, + { "Acid", 0x20 }, + { "Electric", 0x40 }, + { "Health", 0x80 }, + { "Stamina", 0x100 }, + { "Mana", 0x200 }, + { "Nether", 0x400 }, + { "Base", 0x10000000 }, + }; + + [Theory] + [MemberData(nameof(RetailDamageType))] + public void DamageTypeMatchesRetail(string name, uint value) + { + Assert.True(Enum.IsDefined(typeof(DamageType), name), + $"DamageType.{name} is missing"); + Assert.Equal(value, (uint)Enum.Parse(name)); + } + + [Fact] + public void DamageTypeDeclaresNothingRetailDoesNot() + { + var expected = RetailDamageType.Select(r => (string)r[0]).OrderBy(n => n, StringComparer.Ordinal); + var actual = Enum.GetNames().OrderBy(n => n, StringComparer.Ordinal); + Assert.Equal(expected, actual); + } + + /// + /// acclient.h:3300, enum ITEM_TYPE. TYPE_UNDEF and TYPE_SELF both sit on 0 + /// in retail; acdream spells that single zero None. FORCE_*_32_BIT omitted. + /// + public static TheoryData RetailItemType => new() + { + { "None", 0x0 }, + { "MeleeWeapon", 0x1 }, + { "Armor", 0x2 }, + { "Vestements", 0x6 }, + { "Clothing", 0x4 }, + { "Jewelry", 0x8 }, + { "Creature", 0x10 }, + { "Food", 0x20 }, + { "Money", 0x40 }, + { "Misc", 0x80 }, + { "MissileWeapon", 0x100 }, + { "Weapon", 0x101 }, + { "Container", 0x200 }, + { "LockableMagicTarget", 0x280 }, + { "Useless", 0x400 }, + { "Gem", 0x800 }, + { "SpellComponents", 0x1000 }, + { "Writable", 0x2000 }, + { "Key", 0x4000 }, + { "Caster", 0x8000 }, + { "WeaponOrCaster", 0x8101 }, + { "RedirectableItemEnchantmentTarget", 0x8107 }, + { "Portal", 0x10000 }, + { "Lockable", 0x20000 }, + { "PromissoryNote", 0x40000 }, + { "ManaStone", 0x80000 }, + { "ItemEnchantableTarget", 0x88B8F }, + { "Service", 0x100000 }, + { "MagicWieldable", 0x200000 }, + { "Item", 0x2DFBEF }, + { "CraftCookingBase", 0x400000 }, + { "VendorGrocer", 0x446220 }, + { "CraftAlchemyBase", 0x800000 }, + { "CraftFletchingBase", 0x1000000 }, + { "CraftAlchemyIntermediate", 0x4000000 }, + { "CraftFletchingIntermediate", 0x8000000 }, + { "LifeStone", 0x10000000 }, + { "PortalMagicTarget", 0x10010000 }, + { "TinkeringTool", 0x20000000 }, + { "TinkeringMaterial", 0x40000000 }, + { "VendorShopkeep", 0x480467A7 }, + { "Gameboard", 0x80000000 }, + }; + + [Theory] + [MemberData(nameof(RetailItemType))] + public void ItemTypeMatchesRetail(string name, uint value) + { + Assert.True(Enum.IsDefined(typeof(ItemType), name), + $"ItemType.{name} is missing"); + Assert.Equal(value, (uint)Enum.Parse(name)); + } + + [Fact] + public void ItemTypeDeclaresNothingRetailDoesNot() + { + var expected = RetailItemType.Select(r => (string)r[0]).OrderBy(n => n, StringComparer.Ordinal); + var actual = Enum.GetNames().OrderBy(n => n, StringComparer.Ordinal); + Assert.Equal(expected, actual); + } + + /// + /// The craft ladder specifically: retail leaves 0x02000000 unused, and the ACE + /// weenie corpus attests 0x04000000 as Craft_Alchemy_Intermediate 235 times. This + /// is the pairing acdream had wrong. + /// + [Fact] + public void CraftLadderMatchesRetailAndLeavesTheUnusedBitUnclaimed() + { + Assert.Equal(0x00400000u, (uint)ItemType.CraftCookingBase); + Assert.Equal(0x00800000u, (uint)ItemType.CraftAlchemyBase); + Assert.Equal(0x01000000u, (uint)ItemType.CraftFletchingBase); + Assert.Equal(0x04000000u, (uint)ItemType.CraftAlchemyIntermediate); + Assert.Equal(0x08000000u, (uint)ItemType.CraftFletchingIntermediate); + Assert.DoesNotContain(Enum.GetValues(), t => (uint)t == 0x02000000u); + } + + /// + /// Weapon and WeaponOrCaster were aliases before the campaign, because + /// Weapon was recomputed locally as melee|missile|caster. Retail keeps them + /// distinct and only the latter includes the caster bit. + /// + [Fact] + public void WeaponExcludesCasterAndWeaponOrCasterIncludesIt() + { + Assert.NotEqual(ItemType.Weapon, ItemType.WeaponOrCaster); + Assert.Equal(ItemType.MeleeWeapon | ItemType.MissileWeapon, ItemType.Weapon); + Assert.Equal(ItemType.Weapon | ItemType.Caster, ItemType.WeaponOrCaster); + } +} From 8ccaf72ae7d91c1ebafa3939031dc5afb0fa5ad7 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 29 Jul 2026 01:27:57 +0200 Subject: [PATCH 3/5] 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); + } } From 3efa266a6120d29fa4d8902514d85ae296b09ce8 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 29 Jul 2026 01:31:01 +0200 Subject: [PATCH 4/5] feat(core): name AmmoType, CombatUse, and ItemUseable Three more fields acdream already pulls off the wire and then carries as bare numbers. AmmoType and MaterialType ride PublicWeenieDesc through CreateObject and land on ClientObject as ushort/uint; ItemUseable and CombatUse arrive as PropertyInt 16 and 51. Nothing named them, so every site that reasoned about them did it in hex. AmmoType (acclient.h:4221) and CombatUse (acclient.h:6523) are small and unsurprising. ItemUseable (acclient.h:6478) is neither: it is two 16-bit halves, low for where the used object must be and high for where its target must be, and retail names roughly thirty specific combinations rather than expecting callers to compose them. They are transcribed rather than composed because at least one is not the union it looks like - SOURCE_CONTAINED_TARGET_OBJSELF_OR_CONTAINED is 0x880008, where composing ObjSelf|Contained|(Contained shifted 16) gives 0x800088. A test asserts that specific non-equality so the shortcut cannot be reintroduced. ItemAppraisalTextFormatter's ammunition sentence now reads through AmmoType instead of matching 0x08/0x40/0x10/0x80/0x20/0x100 literals. The fold it performs - crystal and chorizite variants collapsing to their base arrow/bolt/atlatl kind - was already exactly right against retail's bit layout; this only gives it vocabulary. No behavior change, and the appraisal tests confirm it. Core tests 3,836 -> 3,894. Full suite 9,759 passed / 5 skipped, no failures. Co-Authored-By: Claude Fable 5 --- .../UI/Layout/ItemAppraisalTextFormatter.cs | 24 ++-- src/AcDream.Core/Items/ItemWireEnums.cs | 104 ++++++++++++++ .../ItemWireEnumConformanceTests.cs | 133 ++++++++++++++++++ 3 files changed, 250 insertions(+), 11 deletions(-) create mode 100644 src/AcDream.Core/Items/ItemWireEnums.cs create mode 100644 tests/AcDream.Core.Tests/Properties/ItemWireEnumConformanceTests.cs diff --git a/src/AcDream.App/UI/Layout/ItemAppraisalTextFormatter.cs b/src/AcDream.App/UI/Layout/ItemAppraisalTextFormatter.cs index 8aaa60fc..82400411 100644 --- a/src/AcDream.App/UI/Layout/ItemAppraisalTextFormatter.cs +++ b/src/AcDream.App/UI/Layout/ItemAppraisalTextFormatter.cs @@ -398,22 +398,24 @@ public static class ItemAppraisalTextFormatter if (ammoType == 0u) return; - uint baseAmmoType = ammoType switch + // Retail gives the crystal and chorizite variants their own bits + // (acclient.h:4221); each folds back to the base kind for this sentence. + AmmoType baseAmmoType = (AmmoType)ammoType switch { - 0x08u or 0x40u => 1u, - 0x10u or 0x80u => 2u, - 0x20u or 0x100u => 4u, - _ => ammoType, + AmmoType.ArrowCrystal or AmmoType.ArrowChorizite => AmmoType.Arrow, + AmmoType.BoltCrystal or AmmoType.BoltChorizite => AmmoType.Bolt, + AmmoType.AtlatlCrystal or AmmoType.AtlatlChorizite => AmmoType.Atlatl, + var other => other, }; bool launcher = (validLocations & (uint)EquipMask.MissileWeapon) != 0; string? description = (launcher, baseAmmoType) switch { - (true, 1u) => "Uses arrows as ammunition.", - (true, 2u) => "Uses quarrels as ammunition.", - (true, 4u) => "Uses atlatl darts as ammunition.", - (false, 1u) => "Used as ammunition by bows.", - (false, 2u) => "Used as ammunition by crossbows.", - (false, 4u) => "Used as ammunition by atlatls.", + (true, AmmoType.Arrow) => "Uses arrows as ammunition.", + (true, AmmoType.Bolt) => "Uses quarrels as ammunition.", + (true, AmmoType.Atlatl) => "Uses atlatl darts as ammunition.", + (false, AmmoType.Arrow) => "Used as ammunition by bows.", + (false, AmmoType.Bolt) => "Used as ammunition by crossbows.", + (false, AmmoType.Atlatl) => "Used as ammunition by atlatls.", _ => null, }; if (description is not null) diff --git a/src/AcDream.Core/Items/ItemWireEnums.cs b/src/AcDream.Core/Items/ItemWireEnums.cs new file mode 100644 index 00000000..af0df8a3 --- /dev/null +++ b/src/AcDream.Core/Items/ItemWireEnums.cs @@ -0,0 +1,104 @@ +using System; + +namespace AcDream.Core.Items; + +/// +/// Verbatim retail AMMO_TYPE (docs/research/named-retail/acclient.h:4221). +/// Arrives on the wire as PublicWeenieDesc._ammo_type (CreateObject weenie flag +/// 0x00000100) and in the appraisal weapon profile. +/// +/// The three base kinds each have a crystal and a chorizite variant on their own +/// bit, so a launcher's "uses arrows" test must fold the variants back to the base — see +/// ItemAppraisalTextFormatter, which does exactly that. +/// +[Flags] +public enum AmmoType : uint +{ + None = 0x000, + Arrow = 0x001, + Bolt = 0x002, + Atlatl = 0x004, + ArrowCrystal = 0x008, + BoltCrystal = 0x010, + AtlatlCrystal = 0x020, + ArrowChorizite = 0x040, + BoltChorizite = 0x080, + AtlatlChorizite = 0x100, +} + +/// +/// Verbatim retail COMBAT_USE (docs/research/named-retail/acclient.h:6523) — +/// how a wieldable participates in combat. This is a plain sequence, not a bitfield. +/// Arrives as PropertyInt.CombatUse (51). +/// +public enum CombatUse : uint +{ + None = 0, + Melee = 1, + Missile = 2, + Ammo = 3, + Shield = 4, + TwoHanded = 5, +} + +/// +/// Verbatim retail ITEM_USEABLE (docs/research/named-retail/acclient.h:6478) — +/// where an object must be, and where its target must be, for a use attempt to be legal. +/// Arrives as PropertyInt.ItemUseable (16). +/// +/// The layout is two 16-bit halves: the low half () constrains +/// the object being used, the high half () constrains its target. +/// Retail names the useful combinations explicitly rather than expecting callers to compose +/// them, and they are transcribed here the same way — several are not the obvious union +/// ( is 0x880008, not +/// ObjSelf | Contained | (Contained << 16)). +/// +[Flags] +public enum ItemUseable : uint +{ + Undef = 0x0, + No = 0x1, + Self = 0x2, + Wielded = 0x4, + Contained = 0x8, + Viewed = 0x10, + Remote = 0x20, + NeverWalk = 0x40, + ObjSelf = 0x80, + + ContainedViewed = 0x18, + ViewedRemote = 0x30, + ContainedViewedRemote = 0x38, + RemoteNeverWalk = 0x60, + ViewedRemoteNeverWalk = 0x70, + ContainedViewedRemoteNeverWalk = 0x78, + + SourceWieldedTargetWielded = 0x00040004, + SourceWieldedTargetContained = 0x00080004, + SourceWieldedTargetViewed = 0x00100004, + SourceWieldedTargetRemote = 0x00200004, + SourceWieldedTargetRemoteNeverWalk = 0x00600004, + + SourceContainedTargetWielded = 0x00040008, + SourceContainedTargetContained = 0x00080008, + SourceContainedTargetSelfOrContained = 0x000A0008, + SourceContainedTargetViewed = 0x00100008, + SourceContainedTargetRemote = 0x00200008, + SourceContainedTargetRemoteOrSelf = 0x00220008, + SourceContainedTargetRemoteNeverWalk = 0x00600008, + SourceContainedTargetObjSelfOrContained = 0x00880008, + + SourceViewedTargetWielded = 0x00040010, + SourceViewedTargetContained = 0x00080010, + SourceViewedTargetViewed = 0x00100010, + SourceViewedTargetRemote = 0x00200010, + + SourceRemoteTargetWielded = 0x00040020, + SourceRemoteTargetContained = 0x00080020, + SourceRemoteTargetViewed = 0x00100020, + SourceRemoteTargetRemote = 0x00200020, + SourceRemoteTargetRemoteNeverWalk = 0x00600020, + + SourceMask = 0x0000FFFF, + TargetMask = 0xFFFF0000, +} diff --git a/tests/AcDream.Core.Tests/Properties/ItemWireEnumConformanceTests.cs b/tests/AcDream.Core.Tests/Properties/ItemWireEnumConformanceTests.cs new file mode 100644 index 00000000..8c4c1bad --- /dev/null +++ b/tests/AcDream.Core.Tests/Properties/ItemWireEnumConformanceTests.cs @@ -0,0 +1,133 @@ +using System; +using System.Linq; +using AcDream.Core.Items; +using Xunit; + +namespace AcDream.Core.Tests.Properties; + +/// +/// Pins the three item-data enums the 2026-07-29 campaign added — all of them describe +/// fields acdream already parses off the wire and stored as bare numbers. Tables are +/// transcribed from docs/research/named-retail/acclient.h. +/// +public sealed class ItemWireEnumConformanceTests +{ + /// acclient.h:4221, enum AMMO_TYPE. + public static TheoryData RetailAmmoType => new() + { + { "None", 0x0 }, + { "Arrow", 0x1 }, + { "Bolt", 0x2 }, + { "Atlatl", 0x4 }, + { "ArrowCrystal", 0x8 }, + { "BoltCrystal", 0x10 }, + { "AtlatlCrystal", 0x20 }, + { "ArrowChorizite", 0x40 }, + { "BoltChorizite", 0x80 }, + { "AtlatlChorizite", 0x100 }, + }; + + [Theory] + [MemberData(nameof(RetailAmmoType))] + public void AmmoTypeMatchesRetail(string name, uint value) + { + Assert.True(Enum.IsDefined(typeof(AmmoType), name), $"AmmoType.{name} is missing"); + Assert.Equal(value, (uint)Enum.Parse(name)); + } + + [Fact] + public void AmmoTypeDeclaresNothingRetailDoesNot() + { + var expected = RetailAmmoType.Select(r => (string)r[0]).OrderBy(n => n, StringComparer.Ordinal); + Assert.Equal(expected, Enum.GetNames().OrderBy(n => n, StringComparer.Ordinal)); + } + + /// acclient.h:6523, enum COMBAT_USE. + [Theory] + [InlineData("None", 0u)] + [InlineData("Melee", 1u)] + [InlineData("Missile", 2u)] + [InlineData("Ammo", 3u)] + [InlineData("Shield", 4u)] + [InlineData("TwoHanded", 5u)] + public void CombatUseMatchesRetail(string name, uint value) + { + Assert.True(Enum.IsDefined(typeof(CombatUse), name), $"CombatUse.{name} is missing"); + Assert.Equal(value, (uint)Enum.Parse(name)); + } + + /// acclient.h:6478, enum ITEM_USEABLE, including the composites. + public static TheoryData RetailItemUseable => new() + { + { "Undef", 0x0 }, + { "No", 0x1 }, + { "Self", 0x2 }, + { "Wielded", 0x4 }, + { "Contained", 0x8 }, + { "Viewed", 0x10 }, + { "ContainedViewed", 0x18 }, + { "Remote", 0x20 }, + { "ViewedRemote", 0x30 }, + { "ContainedViewedRemote", 0x38 }, + { "NeverWalk", 0x40 }, + { "RemoteNeverWalk", 0x60 }, + { "ViewedRemoteNeverWalk", 0x70 }, + { "ContainedViewedRemoteNeverWalk", 0x78 }, + { "ObjSelf", 0x80 }, + { "SourceMask", 0xFFFF }, + { "SourceWieldedTargetWielded", 0x40004 }, + { "SourceContainedTargetWielded", 0x40008 }, + { "SourceViewedTargetWielded", 0x40010 }, + { "SourceRemoteTargetWielded", 0x40020 }, + { "SourceWieldedTargetContained", 0x80004 }, + { "SourceContainedTargetContained", 0x80008 }, + { "SourceViewedTargetContained", 0x80010 }, + { "SourceRemoteTargetContained", 0x80020 }, + { "SourceContainedTargetSelfOrContained", 0xA0008 }, + { "SourceWieldedTargetViewed", 0x100004 }, + { "SourceContainedTargetViewed", 0x100008 }, + { "SourceViewedTargetViewed", 0x100010 }, + { "SourceRemoteTargetViewed", 0x100020 }, + { "SourceWieldedTargetRemote", 0x200004 }, + { "SourceContainedTargetRemote", 0x200008 }, + { "SourceViewedTargetRemote", 0x200010 }, + { "SourceRemoteTargetRemote", 0x200020 }, + { "SourceContainedTargetRemoteOrSelf", 0x220008 }, + { "SourceWieldedTargetRemoteNeverWalk", 0x600004 }, + { "SourceContainedTargetRemoteNeverWalk", 0x600008 }, + { "SourceRemoteTargetRemoteNeverWalk", 0x600020 }, + { "SourceContainedTargetObjSelfOrContained", 0x880008 }, + { "TargetMask", 0xFFFF0000 }, + }; + + [Theory] + [MemberData(nameof(RetailItemUseable))] + public void ItemUseableMatchesRetail(string name, uint value) + { + Assert.True(Enum.IsDefined(typeof(ItemUseable), name), $"ItemUseable.{name} is missing"); + Assert.Equal(value, (uint)Enum.Parse(name)); + } + + [Fact] + public void ItemUseableDeclaresNothingRetailDoesNot() + { + var expected = RetailItemUseable.Select(r => (string)r[0]).OrderBy(n => n, StringComparer.Ordinal); + Assert.Equal(expected, Enum.GetNames().OrderBy(n => n, StringComparer.Ordinal)); + } + + /// + /// The two halves partition the word, and at least one retail composite is + /// deliberately not the obvious union — which is why they are transcribed rather + /// than composed. + /// + [Fact] + public void ItemUseableSourceAndTargetMasksPartitionTheWord() + { + Assert.Equal(0u, (uint)ItemUseable.SourceMask & (uint)ItemUseable.TargetMask); + Assert.Equal(uint.MaxValue, (uint)ItemUseable.SourceMask | (uint)ItemUseable.TargetMask); + + uint naiveUnion = (uint)ItemUseable.ObjSelf | (uint)ItemUseable.Contained + | ((uint)ItemUseable.Contained << 16); + Assert.NotEqual(naiveUnion, (uint)ItemUseable.SourceContainedTargetObjSelfOrContained); + } +} From c19680fd690fa3bc2b2138212fec1bd5e7a93995 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 29 Jul 2026 01:33:50 +0200 Subject: [PATCH 5/5] docs(enums): the 2026-07-29 verification campaign, end to end The ledger the campaign owed: which oracles were actually available, what each family's end state is, what got fixed and why, and - the part that matters most for whoever picks this up - the twelve things that could not be settled from an oracle and are therefore recorded as open questions rather than guessed. Two findings deserve to survive past the morning report. The first is that five of the six vendored reference repos named in CLAUDE.md are empty directories in this environment. ACE, Chorizite, holtburger, ACViewer, AC2D and DatReaderWriter contain nothing, so the campaign re-anchored on the retail header itself - which CLAUDE.md ranks above ACE anyway - with the UtilityBelt enum catalog and the 38,985-file ACE weenie corpus as cross-checks. That turned out to be the more rigorous arrangement rather than a compromise, because of the second finding: the catalog is wrong about CraftFletchingBase, where retail and acdream agree. Trusting any single source, including the one the brief nominated, would have introduced a bug. Retail's header decided every disagreement and the weenie corpus broke ties. Also recorded: the 2026-06-04 property-enum divergence note that this work was supposed to build on does not exist - not in the tree, not under any ref, not in the memory directory, which has no research/ subfolder at all. The MEMORY.md index points at both it and a magic-number audit that is equally absent. The sweep was regenerated from scratch instead, and landed on 864 property members against the missing note's remembered 929. Someone should repoint those index entries. The Bucket B intake row is marked done and points here. Co-Authored-By: Claude Fable 5 --- .../2026-07-29-post-vulkan-work-intake.md | 2 +- .../2026-07-29-enum-verification-campaign.md | 260 ++++++++++++++++++ 2 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 docs/research/2026-07-29-enum-verification-campaign.md diff --git a/docs/plans/2026-07-29-post-vulkan-work-intake.md b/docs/plans/2026-07-29-post-vulkan-work-intake.md index e6dd0df7..c83224ba 100644 --- a/docs/plans/2026-07-29-post-vulkan-work-intake.md +++ b/docs/plans/2026-07-29-post-vulkan-work-intake.md @@ -17,7 +17,7 @@ once Campaign V closes. | Item | Existing foundation | Shape of the work | |---|---|---| -| **VERIFY all enums** | `docs/research/2026-06-04-property-enum-divergence.md`: 929 values missing across 7 `Property*` enums, plus the `acdream-property-enum-sweep` skill for regeneration. | Extend the sweep beyond Property* to every wire-adjacent enum family; adopt values with ACE as oracle; conformance tests per enum. | +| **VERIFY all enums** — **DONE 2026-07-29**, see [`docs/research/2026-07-29-enum-verification-campaign.md`](../research/2026-07-29-enum-verification-campaign.md) | The 2026-06-04 divergence note turned out to be missing from the repo entirely (open question 1 in the successor), so the sweep was regenerated from scratch. | Seven `Property*` tables created (864 members); `DamageType` and `ItemType` value bugs fixed; equipment/physics/item families closed against retail. Twelve open questions remain, headed by five empty `references/` repos. | | **Verify networking stack** | `docs/research/2026-06-04-wire-message-catalog.md`: 256 opcodes catalogued, 114 parsed. | Audit parsed-vs-stub-vs-missing against the catalog; prioritize by what live servers actually send (Coldeve session logs are now a real corpus); holtburger remains the client-side oracle. | | **Physics review — retail landing bounce et al.** | The named-retail decomp workflow; carried debt #116 (slide response), TS-50/51/53 in the divergence register. | A focused fidelity pass: start with the user's observed case — long-jump landing should bounce/recoil as retail does (grep `named-retail` for the landing/impact path in `MovementManager`/`SetMotion` land states before writing anything). Each fix follows grep→decompile→pseudocode→port→conformance. | diff --git a/docs/research/2026-07-29-enum-verification-campaign.md b/docs/research/2026-07-29-enum-verification-campaign.md new file mode 100644 index 00000000..25fa66a0 --- /dev/null +++ b/docs/research/2026-07-29-enum-verification-campaign.md @@ -0,0 +1,260 @@ +# Enum verification campaign — 2026-07-29 + +Bucket B item 1 of [`docs/plans/2026-07-29-post-vulkan-work-intake.md`](../plans/2026-07-29-post-vulkan-work-intake.md). +Successor to the (missing — see Open question 1) 2026-06-04 property-enum +divergence note. Run unattended against base `b70b9832`. + +**Headline: the campaign found two real value bugs and one wrong comment, all in +enums nothing currently reads.** They were traps armed for the next person to +write a comparison, not live defects. Everything else diffs clean. + +--- + +## 1. The oracle set actually used + +CLAUDE.md's reference hierarchy names six vendored repos. In this environment +**`references/ACE`, `references/Chorizite.ACProtocol`, `references/holtburger`, +`references/ACViewer`, `references/AC2D` and `references/DatReaderWriter` are all +empty directories** (`references/*` is gitignored; only WorldBuilder is checked +in). The campaign therefore re-anchored on the oracles that *are* present, which +is no loss of rigour — CLAUDE.md ranks the first of them above ACE anyway: + +| # | Oracle | What it is | Weight | +|---|---|---|---| +| 1 | `docs/research/named-retail/acclient.h` | The Sept 2013 EoR retail header, verbatim. 348 parseable `enum` blocks. | **Decides.** CLAUDE.md: "beats every other reference for what the real client does." | +| 2 | `references/acclientlib/UtilityBelt.Common/Enums/Enums.cs` | 18,313-line client-side enum catalog, shipped beside a vendored `ACE.Entity` tree. Holds the seven property tables as `IntId`/`BoolId`/`FloatId`/… | Strong, **not infallible** — see §2. | +| 3 | `references/weenies/**/*.json` | 38,985 ACE weenie exports. Every stat entry carries the numeric key *and* the enum member name in its `_comment`; enum-valued ints also name the value member after `=`. | Independent mechanical attestation, limited to values some weenie actually sets. | +| 4 | `references/WorldBuilder/.../ACEnums.cs` | 233 lines, a handful of property members. | Spot check only. | + +Retail does **not** name the seven property tables anywhere in `acclient.h` +(searched as `SType*`, `Property*`, and by member name). The client receives +property keys as opaque `u32`, so oracles 2 and 3 carry those alone. + +**Why having more than one oracle mattered:** on `ItemType.CraftFletchingBase` +the *catalog* is wrong (says `0x02000000`) and acdream was right (`0x01000000`, +matching retail). Had the campaign trusted the catalog the way the mission brief +assumed it would trust ACE, it would have introduced a bug. No single source was +taken on faith; retail's header decided every disagreement, with the weenie +corpus as tiebreak. + +--- + +## 2. Bugs found and fixed + +### 2.1 `DamageType` — four bits rotated (`f3e95a3e`) + +acdream assigned `Nether/Mana/Health/Stamina` to `0x80/0x100/0x200/0x400`. +Retail `DAMAGE_TYPE` (acclient.h:3788) assigns `Health/Stamina/Mana/Nether`. + +| value | acdream (before) | retail | catalog | weenie corpus | +|---|---|---|---|---| +| 0x80 | Nether | **Health** | — | — | +| 0x100 | Mana | **Stamina** | — | Stamina (n=2) | +| 0x200 | Health | **Mana** | — | Mana (n=8) | +| 0x400 | Stamina | **Nether** | — | Nether (n=9) | + +`BASE_DAMAGE_TYPE = 0x10000000` was also absent. Both of acdream's live +damage-type name tables — `CombatChatTranslator.FormatDamageType` (ported from +holtburger) and `ItemAppraisalTextFormatter.TryDamageTypeName` — already used +retail's order reading the raw wire `uint`, and `CombatChatTranslator` already +knew about `base`. The enum was the only thing in the tree that was wrong, and +nothing consumed the rotated members. **No live mislabel; latent trap removed.** + +### 2.2 `ItemType` — shifted craft ladder + locally recomputed composites (`f3e95a3e`) + +Retail `ITEM_TYPE` (acclient.h:3300) leaves `0x02000000` unused and puts +`CRAFT_ALCHEMY_INTERMEDIATE` on `0x04000000`. acdream had alchemy-intermediate +on `0x02000000` and an **invented `CraftCookingIntermediate`** squatting on +`0x04000000`. The weenie corpus attests `0x04000000` as +`Craft_Alchemy_Intermediate` 235 times and contains no cooking-intermediate at +all — there is no such item type. + +Separately the composite masks were derived from the local primitives instead of +transcribed, which is how the ladder drifted in the first place: + +| member | acdream (before) | retail | +|---|---|---| +| `Weapon` | `0x8101` (= melee\|missile\|caster) | `0x101` — melee\|missile, **no caster** | +| `WeaponOrCaster` | `0x8101` (alias of `Weapon`) | `0x8101` | +| `Item` | `0x830F` | `0x2DFBEF` | + +Five retail composites acdream never had (`PortalMagicTarget`, +`LockableMagicTarget`, `ItemEnchantableTarget`, +`RedirectableItemEnchantmentTarget`, `VendorShopkeep`, `VendorGrocer`) came +along. Only one site in the tree referenced any changed member — a test that +wants a nonzero `HookItemTypes` and does not care which — so **no branch changed.** + +### 2.3 `EquipMask` — a wrong type remark (`8ccaf72a`) + +A remark 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. Caught by writing the composite test. + +--- + +## 3. Per-family end state + +### 3.1 The seven `Property*` tables (`251dd68a`) + +acdream had **no** `Property*` enums at all — property IDs were bare `uint`s with +the meaning carried in prose (`private const uint EncumbranceValProperty = 5u` +duplicated across two files, `UiEffects` as "ACE enum value 18" in a doc comment). +All seven are now created under `AcDream.Core.Properties`. + +| family | members adopted | attested by both oracles | single-sourced | +|---|---|---|---| +| `PropertyInt` | 391 | 189 | 202 | +| `PropertyFloat` | 172 | 86 | 86 | +| `PropertyBool` | 131 | 59 | 72 | +| `PropertyDataId` | 62 | 41 | 21 | +| `PropertyString` | 53 | 28 | 25 | +| `PropertyInstanceId` | 46 | 2 | 44 | +| `PropertyInt64` | 9 | 3 | 6 | +| **total** | **864** | **408** | **456** | + +Zero value conflicts between the two oracles across all seven, and the corpus +contained no key the catalog lacked — the catalog is a strict superset of +everything 38,985 weenies set. Three members differ in *spelling* only; acdream +took ACE's (`ItemType`/`HookItemType`/`MerchandiseItemTypes` over the catalog's +`ObjectType`/…), which is what the corpus emits and what acdream's own `ItemType` +already called it. + +### 3.2 Existing wire-adjacent enums, diffed against retail + +Legend: **closed** = every retail member present at retail's value. + +| acdream enum | retail enum | end state | +|---|---|---| +| `ItemType` | `ITEM_TYPE` | **closed** (2 fixed) — retail's `TYPE_UNDEF`/`TYPE_SELF` are both 0; acdream spells the single 0 `None` | +| `DamageType` | `DAMAGE_TYPE` | **closed** (4 fixed, 1 added) | +| `EquipMask` | `INVENTORY_LOC` | **closed** — 43/43, 11 composites adopted | +| `TransientStateFlags` | `TransientState` | **closed** — `WaterContact`, `CheckEthereal` adopted | +| `PhysicsStateFlags` | `PhysicsState` | **closed** — `ReservedUnused1/2` adopted | +| `ObjectInfoState` | `ObjectInfoEnum` | **closed** — was already exact (`None` = `DEFAULT_OI`) | +| `AttackHeight` | `ATTACK_HEIGHT` | **closed** — `Undef` adopted; `NUM_ATTACK_HEIGHTS` is a count, deliberately not a member | +| `HoldKey` | `HoldKey` | **closed** — `Num_HoldKeys` is a count | +| `AttackType` | `AttackType` | **already correct**, incl. both composites (`Unarmed` 0x19, `MultiStrike` 0x79E0) | +| `RadarBlipShape` | `RadarBlipShape` | already exact (14/14) | +| `RadarBehavior` | `RadarEnum` | already exact (5/5) | +| `MovementType` | `MovementTypes::Type` | already exact (10/10) | +| `ParticleType` | `ParticleType` | already exact (14/14) | +| `PhysicsDescriptionFlag` | `PhysicsDesc::PhysicsDescInfo` | already exact (naming only: `AnimationFrame`/`ANIMFRAME_ID`) | + +### 3.3 New enums for wire fields acdream parsed but never named (`3efa266a`) + +| enum | retail source | members | why | +|---|---|---|---| +| `AmmoType` | acclient.h:4221 | 10 | parsed via `PublicWeenieDesc._ammo_type`; the appraisal sentence matched raw hex | +| `CombatUse` | acclient.h:6523 | 6 | `PropertyInt.CombatUse` (51) | +| `ItemUseable` | acclient.h:6478 | 39 | `PropertyInt.ItemUseable` (16); two 16-bit halves, ~30 named combinations | + +`ItemAppraisalTextFormatter`'s ammunition fold now reads through `AmmoType`. Its +crystal/chorizite → base collapse was verified correct against retail's bit +layout before the change; behaviour is unchanged. + +### 3.4 Verified correct, no change needed + +- `ItemAppraisalTextFormatter` ammo fold (§3.3) against `AMMO_TYPE`. +- `CombatChatTranslator.FormatDamageType` and + `ItemAppraisalTextFormatter.TryDamageTypeName` against `DAMAGE_TYPE` — both + already retail-correct, which is what exposed §2.1. + +--- + +## 4. Open questions + +Each is something the campaign could **not** settle from an oracle. Per +CLAUDE.md these are recorded rather than guessed. + +1. **The 2026-06-04 research drop is missing.** `claude-memory/MEMORY.md` indexes + `research/2026-06-04-property-enum-divergence.md` (the "929 values across 7 + enums" ledger) and `research/2026-06-04-magic-number-audit.md`. Neither exists + in the working tree, under any ref (`git log --all --diff-filter=A`), or in the + memory directory — which has no `research/` subfolder at all. The 929 figure + could not be reproduced or audited; this campaign regenerated from scratch and + arrived at 864 property members. **Blocker:** source documents absent. Either + they were never committed or they lived in a discarded worktree. Suggest + fixing the MEMORY.md index entries to point at this doc. +2. **Five of six reference repos are empty** (§1). Everything sourced here came + from retail + the UtilityBelt catalog + the weenie corpus. **Blocker:** repos + not cloned in this environment. Re-cloning ACE and Chorizite would let the 456 + single-sourced property members (§3.1) be promoted to two-oracle confirmed. +3. **456 of 864 property members are single-sourced.** They are transcribed from + the catalog, not invented, but no weenie in the corpus sets them so there is no + independent attestation. **Blocker:** needs ACE source, or the client DAT's own + `EnumMapper` file type (`acclientlib` has a reader for it — a genuinely retail + oracle, and the most promising unexplored lead). +4. **`WeenieError` — 362 unadopted status codes.** acdream has 16 members; the + catalog's `StatusMessage` has 372, with 10 shared and zero conflicts. Retail's + `charError` (26) is a *different* enum — character-creation errors only — so + retail cannot arbitrate. Register row **AP-15** already tracks that acdream's + translation table covers only ~30 common codes. **Blocker:** needs a decision + on importing 362 server-side codes wholesale, and ideally retail's + `string_table.bin` for the real sentences. Deliberately not done here. +5. **`CombatMode` has no located retail counterpart.** acdream's 7 members match + the catalog's `CombatMode` (`Magic = 8`). Retail's `CombatStyle` (25 members, + `Magic_CombatStyle = 512`) is a different enum — weapon-style, not combat mode. + A retail `COMBAT_MODE` was not found in `acclient.h`. **Blocker:** oracle not + located; acdream is probably right but is single-sourced. +6. **`PublicWeenieFlags` counterpart unidentified.** acdream's 17 members + (`Attackable`, `Door`, `Vendor`, …) share exactly one name with retail's + `PublicWeenieDescPackHeader` (34), which is the *pack-header* bitfield — a + different thing. `PublicWeenieDesc::BitfieldIndex` (31 members) is the likely + real counterpart. **Blocker:** pairing unconfirmed; not diffed. +7. **`SoundId` is a curated 23-member local subset** of retail's `SoundType` + (206), with acdream-local names (`FootstepGrass`, `BuffApplied`) that do not + correspond 1:1 to retail members. **Blocker:** needs a decision — adopt + retail's table wholesale, or record the subset as an intentional divergence + with a register row. Neither was done. +8. **`ChatType` / `BlobType` / `DispatchType`** (TurbineChat) share no members + with retail's `eChatTypes` (26) or `ChatTypeEnum` (12); they describe chat + *channels*, not text-display types. **Blocker:** correct counterpart not + identified. Relevant to the Bucket C chatbox cleanup. +9. **`GameEventType` (103 members) was not verified.** No counterpart found under + a matching name in either retail or the catalog. **Blocker:** needs the wire + catalog (Bucket B item 2) to supply the pairing. +10. **`MaterialType` not adopted.** acdream parses it as raw `uint` from + `PublicWeenieDesc`; the catalog has 108 members; no retail counterpart located + in `acclient.h`. **Blocker:** single-sourced, and unused so far. +11. **`SpellTargetType`, `ItemPrimaryUseResult`, `PositionFlags`, + `EnchantmentMask`/`EnchantmentBucket`, `GameMessageGroup`, `InventoryRequestKind` + are acdream-local** with no retail counterpart at the same semantics. Confirmed + for `EnchantmentMask`/`EnchantmentBucket`: they are PlayerDescription wire + *trailer bucket* flags (ACE's `EnchantmentMask`), not retail's + `EnchantmentTypeEnum` — the apparent value conflict is a false pairing. The + others were not individually run down. +12. **Tooling limitation, for whoever re-runs this.** The extractor reads one line + per enum member, so a member whose value spans multiple lines is truncated. + This produced a false `AttackType.MultiStrike` conflict (read as `0x1E0`, + actually `0x79E0`). Any automated re-run must join continuation lines before + trusting a reported conflict. + +--- + +## 5. Reproducing + +Extraction and diff scripts were scratch tooling, not committed (they hardcode +absolute paths into `references/`). The method, in order: + +1. Parse `acclient.h` for `enum NAME {` blocks; resolve member expressions + (hex, digit separators, references to earlier members). +2. Parse the UtilityBelt catalog the same way. +3. Walk `references/weenies/**/*.json`, pulling `(key, value, _comment)` from each + `*Stats` array; the comment's left side is the property name, the right side + (after `=`) names the value member for enum-typed ints. +4. Normalise names across conventions (retail `SCREAMING_SNAKE` with family + prefix/suffix ↔ acdream `PascalCase`) and diff on the normalised key, comparing + values exactly. +5. Any disagreement: retail decides, corpus breaks ties, and anything still + unresolved becomes an open question above rather than a guess. + +## 6. Commits + +| commit | slice | +|---|---| +| `251dd68a` | the seven `Property*` tables (864 members, 429 tests) | +| `f3e95a3e` | `DamageType` rotation + `ItemType` craft ladder and composites | +| `8ccaf72a` | `EquipMask` composites, `TransientState`, `PhysicsState`, `AttackHeight` | +| `3efa266a` | `AmmoType`, `CombatUse`, `ItemUseable` |