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