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` | 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/Combat/CombatModel.cs b/src/AcDream.Core/Combat/CombatModel.cs index 1e752102..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, @@ -204,6 +210,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 +233,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..394a1e71 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 } /// @@ -61,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 @@ -99,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/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/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/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/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); + } +} 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)); + } +} diff --git a/tests/AcDream.Core.Tests/Properties/RetailEnumConformanceTests.cs b/tests/AcDream.Core.Tests/Properties/RetailEnumConformanceTests.cs new file mode 100644 index 00000000..f9dfd970 --- /dev/null +++ b/tests/AcDream.Core.Tests/Properties/RetailEnumConformanceTests.cs @@ -0,0 +1,285 @@ +using System; +using System.Linq; +using AcDream.Core.Combat; +using AcDream.Core.Items; +using AcDream.Core.Physics; +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); + } + + /// + /// 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); + } +}