merge(core): the enum verification campaign, onto the post-deletion tree

Brings `github/overnight/enums` (`c19680fd`) forward onto the V11 tree. The
branch was cut at `b70b9832`, before the OpenGL deletion, and the two lines of
work turned out to be disjoint: the enum campaign lives entirely in
`AcDream.Core` and its tests, while V11 emptied `AcDream.App`. The merge is
clean — no conflicting file on either side.

What it carries: names for AC's seven property tables verified against two
oracles, a correction to `DamageType`'s rotated bits and `ItemType`'s shifted
craft ladder, the retail members the equipment and physics enums were missing,
and names for `AmmoType`, `CombatUse` and `ItemUseable`. Five commits, seventeen
files, +3,767 / -27 lines.

Verified on the merge result rather than on the branch: Release build 0 errors,
no new warning attributable to any file the branch touches, and
`AcDream.Core.Tests` at 3,893 passed / 2 skipped / 3,895. The campaign's
claimed +597 is exact — the `Properties` namespace alone runs 597 tests, all
passing.

The campaign's open decision items — whether to adopt `WeenieError` wholesale,
whether the `SoundId` subset is the right cut, and the re-clone of the ACE and
Chorizite references that `references/` no longer holds — are not settled here.
They are carried into the morning report as questions for the user.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-29 03:29:16 +02:00
commit cd2f3feae2
17 changed files with 3767 additions and 27 deletions

View file

@ -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. |

View file

@ -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` |

View file

@ -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)

View file

@ -22,8 +22,14 @@ public enum CombatMode
CombatCombat = Melee | Missile | Magic,
}
/// <summary>
/// Verbatim retail <c>ATTACK_HEIGHT</c> (<c>docs/research/named-retail/acclient.h:4371</c>).
/// 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.
/// </summary>
public enum AttackHeight
{
Undef = 0,
High = 1,
Medium = 2,
Low = 3,
@ -204,6 +210,18 @@ public enum AttackType : uint
| OffhandDoubleThrust | OffhandTripleThrust,
}
/// <summary>
/// Verbatim retail <c>DAMAGE_TYPE</c> (<c>docs/research/named-retail/acclient.h:3788</c>).
///
/// <para>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 — <c>CombatChatTranslator</c> and
/// <c>ItemAppraisalTextFormatter</c> — 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.</para>
/// </summary>
[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,
}
/// <summary>

View file

@ -10,9 +10,17 @@ namespace AcDream.Core.Items;
/// <summary>
/// AC's <c>ItemType</c> is a 32-bit flags enum — a single dat weenie can
/// assert multiple type bits. From <c>ACE.Entity.Enum.ItemType</c>
/// cross-checked against the decompile paperdoll tooltip dispatcher.
/// Full bit list in the research doc §1.
/// assert multiple type bits. Verbatim retail <c>ITEM_TYPE</c>
/// (<c>docs/research/named-retail/acclient.h:3300</c>).
///
/// <para>The craft ladder used to be shifted: <c>CraftAlchemyIntermediate</c> sat on
/// 0x02000000 (retail leaves that bit unused and puts alchemy-intermediate on
/// 0x04000000), and an invented <c>CraftCookingIntermediate</c> 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 <c>Weapon</c> an alias of <c>WeaponOrCaster</c> and left <c>Item</c> two orders
/// of magnitude narrower than retail's. Full bit list in the research doc §1.</para>
/// </summary>
[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
}
/// <summary>
@ -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 <c>CLOTHING_LOC</c> composite also sets bit 31, 0x80000000, which is
/// not a named INVENTORY_LOC primitive and has no member here; <c>ALL_LOC</c> tops out at bit 30.)
/// (A remark here used to claim the header's <c>CLOTHING_LOC</c> composite sets an
/// unnamed bit 31. It does not: <c>CLOTHING_LOC</c> is 0x080001FF, the nine wear slots
/// plus bit 27, which is the named <see cref="EquipMask.Cloak"/> slot. <c>ALL_LOC</c>
/// tops out at bit 30, so no INVENTORY_LOC member uses bit 31 at all.)
/// </summary>
[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
/// <summary>Retail gives CAN_GO_IN_READY_SLOT_LOC the same value as ALL_LOC.</summary>
CanGoInReadySlot = 0x7FFFFFFF,
}
/// <summary>

View file

@ -0,0 +1,104 @@
using System;
namespace AcDream.Core.Items;
/// <summary>
/// Verbatim retail <c>AMMO_TYPE</c> (<c>docs/research/named-retail/acclient.h:4221</c>).
/// Arrives on the wire as <c>PublicWeenieDesc._ammo_type</c> (CreateObject weenie flag
/// 0x00000100) and in the appraisal weapon profile.
///
/// <para>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
/// <c>ItemAppraisalTextFormatter</c>, which does exactly that.</para>
/// </summary>
[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,
}
/// <summary>
/// Verbatim retail <c>COMBAT_USE</c> (<c>docs/research/named-retail/acclient.h:6523</c>) —
/// how a wieldable participates in combat. This is a plain sequence, not a bitfield.
/// Arrives as <c>PropertyInt.CombatUse</c> (51).
/// </summary>
public enum CombatUse : uint
{
None = 0,
Melee = 1,
Missile = 2,
Ammo = 3,
Shield = 4,
TwoHanded = 5,
}
/// <summary>
/// Verbatim retail <c>ITEM_USEABLE</c> (<c>docs/research/named-retail/acclient.h:6478</c>) —
/// where an object must be, and where its target must be, for a use attempt to be legal.
/// Arrives as <c>PropertyInt.ItemUseable</c> (16).
///
/// <para>The layout is two 16-bit halves: the low half (<see cref="SourceMask"/>) constrains
/// the object being used, the high half (<see cref="TargetMask"/>) 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
/// (<see cref="SourceContainedTargetObjSelfOrContained"/> is 0x880008, not
/// <c>ObjSelf | Contained | (Contained &lt;&lt; 16)</c>).</para>
/// </summary>
[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,
}

View file

@ -27,6 +27,12 @@ public enum PhysicsStateFlags : uint
{
None = 0x00000000,
Static = 0x00000001,
/// <summary>
/// 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.
/// </summary>
ReservedUnused1 = 0x00000002,
Ethereal = 0x00000004,
ReportCollisions = 0x00000008,
IgnoreCollisions = 0x00000010,
@ -38,6 +44,8 @@ public enum PhysicsStateFlags : uint
Gravity = 0x00000400,
Lighting = 0x00000800,
ParticleEmitter = 0x00001000,
/// <summary>Retail UNNUSED2_PS — reserved, never set. See <see cref="ReservedUnused1"/>.</summary>
ReservedUnused2 = 0x00002000,
Hidden = 0x00004000,
ScriptedCollision = 0x00008000,
/// <summary>
@ -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
}
/// <summary>

View file

@ -0,0 +1,221 @@
// <auto-generated-source>
// 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.
// </auto-generated-source>
namespace AcDream.Core.Properties;
/// <summary>
/// AC's <c>PropertyBool</c> property table — the numeric keys the server sends in
/// <c>PrivateUpdatePropertyBool (0x02C9) / PublicUpdatePropertyBool (0x02CA)</c>
/// and in the property bundles carried by <c>CreateObject</c> / <c>PlayerDescription</c> /
/// <c>IdentifyResponse</c>. The CLR payload for this table is <c>bool</c>.
///
/// <para>Verified 2026-07-29 against two independent oracles: the vendored
/// client-side enum catalog (<c>references/acclientlib/UtilityBelt.Common/Enums/Enums.cs</c>,
/// which names this table <c>BoolId</c>) and the 38,985-file ACE weenie export corpus
/// (<c>references/weenies/</c>), whose per-stat <c>_comment</c> 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.</para>
///
/// <para>This enum is a <b>vocabulary</b>, not a parser contract: the wire carries a raw
/// <c>uint</c> and unknown keys must still round-trip untouched, so never assume a
/// received key is a defined member.</para>
/// </summary>
public enum PropertyBool : uint
{
Undef = 0,
/// <summary>21884 weenie attestations.</summary>
Stuck = 1,
/// <summary>1077 weenie attestations.</summary>
Open = 2,
/// <summary>646 weenie attestations.</summary>
Locked = 3,
RotProof = 4,
AllegianceUpdateRequest = 5,
/// <summary>1282 weenie attestations.</summary>
AiUsesMana = 6,
/// <summary>16 weenie attestations.</summary>
AiUseHumanMagicAnimations = 7,
/// <summary>853 weenie attestations.</summary>
AllowGive = 8,
CurrentlyAttacking = 9,
AttackerAi = 10,
/// <summary>6212 weenie attestations.</summary>
IgnoreCollisions = 11,
/// <summary>9020 weenie attestations.</summary>
ReportCollisions = 12,
/// <summary>15282 weenie attestations.</summary>
Ethereal = 13,
/// <summary>7376 weenie attestations.</summary>
GravityStatus = 14,
/// <summary>3382 weenie attestations.</summary>
LightsStatus = 15,
/// <summary>139 weenie attestations.</summary>
ScriptedCollision = 16,
/// <summary>475 weenie attestations.</summary>
Inelastic = 17,
/// <summary>1677 weenie attestations.</summary>
Visibility = 18,
/// <summary>3972 weenie attestations.</summary>
Attackable = 19,
SafeSpellComponents = 20,
AdvocateState = 21,
/// <summary>15455 weenie attestations.</summary>
Inscribable = 22,
/// <summary>7755 weenie attestations.</summary>
DestroyOnSell = 23,
/// <summary>6728 weenie attestations.</summary>
UiHidden = 24,
IgnoreHouseBarriers = 25,
HiddenAdmin = 26,
PkWounder = 27,
PkKiller = 28,
/// <summary>262 weenie attestations.</summary>
NoCorpse = 29,
UnderLifestoneProtection = 30,
ItemManaUpdatePending = 31,
GeneratorStatus = 32,
/// <summary>831 weenie attestations.</summary>
ResetMessagePending = 33,
/// <summary>831 weenie attestations.</summary>
DefaultOpen = 34,
/// <summary>511 weenie attestations.</summary>
DefaultLocked = 35,
DefaultOn = 36,
OpenForBusiness = 37,
IsFrozen = 38,
/// <summary>1049 weenie attestations.</summary>
DealMagicalItems = 39,
LogoffImDead = 40,
/// <summary>2208 weenie attestations.</summary>
ReportCollisionsAsEnvironment = 41,
/// <summary>1327 weenie attestations.</summary>
AllowEdgeSlide = 42,
AdvocateQuest = 43,
IsAdmin = 44,
IsArch = 45,
IsSentinel = 46,
IsAdvocate = 47,
CurrentlyPoweringUp = 48,
GeneratorEnteredWorld = 49,
/// <summary>890 weenie attestations.</summary>
NeverFailCasting = 50,
/// <summary>264 weenie attestations.</summary>
VendorService = 51,
/// <summary>1421 weenie attestations.</summary>
AiImmobile = 52,
/// <summary>1 weenie attestations.</summary>
DamagedByCollisions = 53,
/// <summary>34 weenie attestations.</summary>
IsDynamic = 54,
/// <summary>81 weenie attestations.</summary>
IsHot = 55,
IsAffecting = 56,
/// <summary>49 weenie attestations.</summary>
AffectsAis = 57,
SpellQueueActive = 58,
GeneratorDisabled = 59,
IsAcceptingTells = 60,
/// <summary>1 weenie attestations.</summary>
LoggingChannel = 61,
/// <summary>5 weenie attestations.</summary>
OpensAnyLock = 62,
/// <summary>20 weenie attestations.</summary>
UnlimitedUse = 63,
GeneratedTreasureItem = 64,
/// <summary>97 weenie attestations.</summary>
IgnoreMagicResist = 65,
/// <summary>87 weenie attestations.</summary>
IgnoreMagicArmor = 66,
AiAllowTrade = 67,
/// <summary>2 weenie attestations.</summary>
SpellComponentsRequired = 68,
/// <summary>4617 weenie attestations.</summary>
IsSellable = 69,
/// <summary>4 weenie attestations.</summary>
IgnoreShieldsBySkill = 70,
/// <summary>6275 weenie attestations.</summary>
NoDraw = 71,
ActivationUntargeted = 72,
HouseHasGottenPriorityBootPos = 73,
/// <summary>12 weenie attestations.</summary>
GeneratorAutomaticDestruction = 74,
HouseHooksVisible = 75,
/// <summary>8 weenie attestations.</summary>
HouseRequiresMonarch = 76,
HouseHooksEnabled = 77,
HouseNotifiedHudOfHookCount = 78,
/// <summary>15 weenie attestations.</summary>
AiAcceptEverything = 79,
IgnorePortalRestrictions = 80,
/// <summary>5 weenie attestations.</summary>
RequiresBackpackSlot = 81,
/// <summary>444 weenie attestations.</summary>
DontTurnOrMoveWhenGiving = 82,
/// <summary>442 weenie attestations.</summary>
NpcLooksLikeObject = 83,
/// <summary>210 weenie attestations.</summary>
IgnoreCloIcons = 84,
/// <summary>178 weenie attestations.</summary>
AppraisalHasAllowedWielder = 85,
ChestRegenOnClose = 86,
LogoffInMinigame = 87,
/// <summary>17 weenie attestations.</summary>
PortalShowDestination = 88,
/// <summary>30 weenie attestations.</summary>
PortalIgnoresPkAttackTimer = 89,
/// <summary>43 weenie attestations.</summary>
NpcInteractsSilently = 90,
/// <summary>230 weenie attestations.</summary>
Retained = 91,
/// <summary>1 weenie attestations.</summary>
IgnoreAuthor = 92,
Limbo = 93,
AppraisalHasAllowedActivator = 94,
ExistedBeforeAllegianceXpChanges = 95,
IsDeaf = 96,
IsPsr = 97,
Invincible = 98,
/// <summary>1068 weenie attestations.</summary>
Ivoryable = 99,
/// <summary>687 weenie attestations.</summary>
Dyable = 100,
CanGenerateRare = 101,
CorpseGeneratedRare = 102,
/// <summary>14 weenie attestations.</summary>
NonProjectileMagicImmune = 103,
ActdReceivedItems = 104,
Unknown105 = 105,
FirstEnterWorldDone = 106,
RecallsDisabled = 107,
/// <summary>13 weenie attestations.</summary>
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,
/// <summary>14 weenie attestations.</summary>
AutowieldLeft = 130,
}

View file

@ -0,0 +1,134 @@
// <auto-generated-source>
// 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.
// </auto-generated-source>
namespace AcDream.Core.Properties;
/// <summary>
/// AC's <c>PropertyDataId</c> property table — the numeric keys the server sends in
/// <c>PrivateUpdatePropertyDataId (0x02E1) / PublicUpdatePropertyDataId (0x02E2)</c>
/// and in the property bundles carried by <c>CreateObject</c> / <c>PlayerDescription</c> /
/// <c>IdentifyResponse</c>. The CLR payload for this table is <c>uint (DAT file id)</c>.
///
/// <para>Verified 2026-07-29 against two independent oracles: the vendored
/// client-side enum catalog (<c>references/acclientlib/UtilityBelt.Common/Enums/Enums.cs</c>,
/// which names this table <c>DataId</c>) and the 38,985-file ACE weenie export corpus
/// (<c>references/weenies/</c>), whose per-stat <c>_comment</c> 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.</para>
///
/// <para>This enum is a <b>vocabulary</b>, not a parser contract: the wire carries a raw
/// <c>uint</c> and unknown keys must still round-trip untouched, so never assume a
/// received key is a defined member.</para>
/// </summary>
public enum PropertyDataId : uint
{
Undef = 0,
/// <summary>38985 weenie attestations.</summary>
Setup = 1,
/// <summary>12219 weenie attestations.</summary>
MotionTable = 2,
/// <summary>23696 weenie attestations.</summary>
SoundTable = 3,
/// <summary>4319 weenie attestations.</summary>
CombatTable = 4,
/// <summary>2 weenie attestations.</summary>
QualityFilter = 5,
/// <summary>13875 weenie attestations.</summary>
PaletteBase = 6,
/// <summary>9049 weenie attestations.</summary>
ClothingBase = 7,
/// <summary>38985 weenie attestations.</summary>
Icon = 8,
/// <summary>741 weenie attestations.</summary>
EyesTexture = 9,
/// <summary>741 weenie attestations.</summary>
NoseTexture = 10,
/// <summary>741 weenie attestations.</summary>
MouthTexture = 11,
DefaultEyesTexture = 12,
DefaultNoseTexture = 13,
DefaultMouthTexture = 14,
/// <summary>741 weenie attestations.</summary>
HairPalette = 15,
/// <summary>741 weenie attestations.</summary>
EyesPalette = 16,
/// <summary>741 weenie attestations.</summary>
SkinPalette = 17,
HeadObject = 18,
/// <summary>4 weenie attestations.</summary>
ActivationAnimation = 19,
/// <summary>4 weenie attestations.</summary>
InitMotion = 20,
ActivationSound = 21,
/// <summary>23266 weenie attestations.</summary>
PhysicsEffectTable = 22,
/// <summary>138 weenie attestations.</summary>
UseSound = 23,
/// <summary>48 weenie attestations.</summary>
UseTargetAnimation = 24,
/// <summary>4 weenie attestations.</summary>
UseTargetSuccessAnimation = 25,
/// <summary>2 weenie attestations.</summary>
UseTargetFailureAnimation = 26,
/// <summary>232 weenie attestations.</summary>
UseUserAnimation = 27,
/// <summary>3696 weenie attestations.</summary>
Spell = 28,
/// <summary>156 weenie attestations.</summary>
SpellComponent = 29,
/// <summary>794 weenie attestations.</summary>
PhysicsScript = 30,
/// <summary>497 weenie attestations.</summary>
LinkedPortalOne = 31,
/// <summary>861 weenie attestations.</summary>
WieldedTreasureType = 32,
/// <summary>5 weenie attestations.</summary>
UnknownGuessedname = 33,
UnknownGuessedname2 = 34,
/// <summary>2000 weenie attestations.</summary>
DeathTreasureType = 35,
/// <summary>2276 weenie attestations.</summary>
MutateFilter = 36,
/// <summary>1253 weenie attestations.</summary>
ItemSkillLimit = 37,
/// <summary>1 weenie attestations.</summary>
UseCreateItem = 38,
DeathSpell = 39,
VendorsClassId = 40,
/// <summary>13 weenie attestations.</summary>
ItemSpecializedOnly = 41,
/// <summary>6274 weenie attestations.</summary>
HouseId = 42,
AccountHouseId = 43,
/// <summary>6274 weenie attestations.</summary>
RestrictionEffect = 44,
CreationMutationFilter = 45,
/// <summary>584 weenie attestations.</summary>
TsysMutationFilter = 46,
LastPortal = 47,
/// <summary>1 weenie attestations.</summary>
LinkedPortalTwo = 48,
OriginalPortal = 49,
/// <summary>1000 weenie attestations.</summary>
IconOverlay = 50,
/// <summary>16 weenie attestations.</summary>
IconOverlaySecondary = 51,
/// <summary>554 weenie attestations.</summary>
IconUnderlay = 52,
AugmentationMutationFilter = 53,
AugmentationEffect = 54,
/// <summary>84 weenie attestations.</summary>
ProcSpell = 55,
AugmentationCreateItem = 56,
AlternateCurrency = 57,
BlueSurgeSpell = 58,
YellowSurgeSpell = 59,
RedSurgeSpell = 60,
OlthoiDeathTreasureType = 61,
}

View file

@ -0,0 +1,289 @@
// <auto-generated-source>
// 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.
// </auto-generated-source>
namespace AcDream.Core.Properties;
/// <summary>
/// AC's <c>PropertyFloat</c> property table — the numeric keys the server sends in
/// <c>PrivateUpdatePropertyFloat (0x02D5) / PublicUpdatePropertyFloat (0x02D6)</c>
/// and in the property bundles carried by <c>CreateObject</c> / <c>PlayerDescription</c> /
/// <c>IdentifyResponse</c>. The CLR payload for this table is <c>double</c>.
///
/// <para>Verified 2026-07-29 against two independent oracles: the vendored
/// client-side enum catalog (<c>references/acclientlib/UtilityBelt.Common/Enums/Enums.cs</c>,
/// which names this table <c>FloatId</c>) and the 38,985-file ACE weenie export corpus
/// (<c>references/weenies/</c>), whose per-stat <c>_comment</c> 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.</para>
///
/// <para>This enum is a <b>vocabulary</b>, not a parser contract: the wire carries a raw
/// <c>uint</c> and unknown keys must still round-trip untouched, so never assume a
/// received key is a defined member.</para>
/// </summary>
public enum PropertyFloat : uint
{
Undef = 0,
/// <summary>4243 weenie attestations.</summary>
HeartbeatInterval = 1,
/// <summary>4243 weenie attestations.</summary>
HeartbeatTimestamp = 2,
/// <summary>4361 weenie attestations.</summary>
HealthRate = 3,
/// <summary>4361 weenie attestations.</summary>
StaminaRate = 4,
/// <summary>7706 weenie attestations.</summary>
ManaRate = 5,
/// <summary>79 weenie attestations.</summary>
HealthUponResurrection = 6,
/// <summary>79 weenie attestations.</summary>
StaminaUponResurrection = 7,
/// <summary>79 weenie attestations.</summary>
ManaUponResurrection = 8,
StartTime = 9,
StopTime = 10,
/// <summary>1749 weenie attestations.</summary>
ResetInterval = 11,
/// <summary>3290 weenie attestations.</summary>
Shade = 12,
/// <summary>6001 weenie attestations.</summary>
ArmorModVsSlash = 13,
/// <summary>6005 weenie attestations.</summary>
ArmorModVsPierce = 14,
/// <summary>6005 weenie attestations.</summary>
ArmorModVsBludgeon = 15,
/// <summary>6005 weenie attestations.</summary>
ArmorModVsCold = 16,
/// <summary>6005 weenie attestations.</summary>
ArmorModVsFire = 17,
/// <summary>6005 weenie attestations.</summary>
ArmorModVsAcid = 18,
/// <summary>6005 weenie attestations.</summary>
ArmorModVsElectric = 19,
CombatSpeed = 20,
/// <summary>2733 weenie attestations.</summary>
WeaponLength = 21,
/// <summary>3174 weenie attestations.</summary>
DamageVariance = 22,
CurrentPowerMod = 23,
AccuracyMod = 24,
StrengthMod = 25,
/// <summary>1386 weenie attestations.</summary>
MaximumVelocity = 26,
/// <summary>233 weenie attestations.</summary>
RotationSpeed = 27,
MotionTimestamp = 28,
/// <summary>3892 weenie attestations.</summary>
WeaponDefense = 29,
WimpyLevel = 30,
/// <summary>2169 weenie attestations.</summary>
VisualAwarenessRange = 31,
AuralAwarenessRange = 32,
PerceptionLevel = 33,
/// <summary>1987 weenie attestations.</summary>
PowerupTime = 34,
MaxChargeDistance = 35,
/// <summary>1892 weenie attestations.</summary>
ChargeSpeed = 36,
/// <summary>1049 weenie attestations.</summary>
BuyPrice = 37,
/// <summary>1049 weenie attestations.</summary>
SellPrice = 38,
/// <summary>18233 weenie attestations.</summary>
DefaultScale = 39,
/// <summary>10 weenie attestations.</summary>
LockpickMod = 40,
/// <summary>2195 weenie attestations.</summary>
RegenerationInterval = 41,
RegenerationTimestamp = 42,
/// <summary>2135 weenie attestations.</summary>
GeneratorRadius = 43,
/// <summary>66 weenie attestations.</summary>
TimeToRot = 44,
DeathTimestamp = 45,
PkTimestamp = 46,
VictimTimestamp = 47,
LoginTimestamp = 48,
CreationTimestamp = 49,
/// <summary>4 weenie attestations.</summary>
MinimumTimeSincePk = 50,
DeprecatedHousekeepingPriority = 51,
AbuseLoggingTimestamp = 52,
LastPortalTeleportTimestamp = 53,
/// <summary>9152 weenie attestations.</summary>
UseRadius = 54,
/// <summary>46 weenie attestations.</summary>
HomeRadius = 55,
ReleasedTimestamp = 56,
MinHomeRadius = 57,
Facing = 58,
ResetTimestamp = 59,
LogoffTimestamp = 60,
EconRecoveryInterval = 61,
/// <summary>3523 weenie attestations.</summary>
WeaponOffense = 62,
/// <summary>1223 weenie attestations.</summary>
DamageMod = 63,
/// <summary>4361 weenie attestations.</summary>
ResistSlash = 64,
/// <summary>4361 weenie attestations.</summary>
ResistPierce = 65,
/// <summary>4361 weenie attestations.</summary>
ResistBludgeon = 66,
/// <summary>4361 weenie attestations.</summary>
ResistFire = 67,
/// <summary>4361 weenie attestations.</summary>
ResistCold = 68,
/// <summary>4361 weenie attestations.</summary>
ResistAcid = 69,
/// <summary>4361 weenie attestations.</summary>
ResistElectric = 70,
/// <summary>4361 weenie attestations.</summary>
ResistHealthBoost = 71,
/// <summary>4361 weenie attestations.</summary>
ResistStaminaDrain = 72,
/// <summary>4361 weenie attestations.</summary>
ResistStaminaBoost = 73,
/// <summary>4361 weenie attestations.</summary>
ResistManaDrain = 74,
/// <summary>4361 weenie attestations.</summary>
ResistManaBoost = 75,
/// <summary>794 weenie attestations.</summary>
Translucency = 76,
/// <summary>269 weenie attestations.</summary>
PhysicsScriptIntensity = 77,
/// <summary>761 weenie attestations.</summary>
Friction = 78,
/// <summary>761 weenie attestations.</summary>
Elasticity = 79,
/// <summary>1076 weenie attestations.</summary>
AiUseMagicDelay = 80,
ItemMinSpellcraftMod = 81,
ItemMaxSpellcraftMod = 82,
ItemRankProbability = 83,
Shade2 = 84,
Shade3 = 85,
Shade4 = 86,
/// <summary>15 weenie attestations.</summary>
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,
/// <summary>22 weenie attestations.</summary>
HealkitMod = 100,
FrozenTimestamp = 101,
HealthRateMod = 102,
AllegianceSwearTimestamp = 103,
/// <summary>4361 weenie attestations.</summary>
ObviousRadarRange = 104,
/// <summary>81 weenie attestations.</summary>
HotspotCycleTime = 105,
/// <summary>74 weenie attestations.</summary>
HotspotCycleTimeVariance = 106,
SpamTimestamp = 107,
SpamRate = 108,
/// <summary>26 weenie attestations.</summary>
BondWieldedTreasure = 109,
/// <summary>1039 weenie attestations.</summary>
BulkMod = 110,
/// <summary>1039 weenie attestations.</summary>
SizeMod = 111,
GagTimestamp = 112,
GeneratorUpdateTimestamp = 113,
DeathSpamTimestamp = 114,
DeathSpamRate = 115,
WildAttackProbability = 116,
/// <summary>312 weenie attestations.</summary>
FocusedProbability = 117,
CrashAndTurnProbability = 118,
CrashAndTurnRadius = 119,
CrashAndTurnBias = 120,
/// <summary>186 weenie attestations.</summary>
GeneratorInitialDelay = 121,
/// <summary>575 weenie attestations.</summary>
AiAcquireHealth = 122,
/// <summary>6 weenie attestations.</summary>
AiAcquireStamina = 123,
/// <summary>6 weenie attestations.</summary>
AiAcquireMana = 124,
/// <summary>4361 weenie attestations.</summary>
ResistHealthDrain = 125,
LifestoneProtectionTimestamp = 126,
/// <summary>72 weenie attestations.</summary>
AiCounteractEnchantment = 127,
/// <summary>10 weenie attestations.</summary>
AiDispelEnchantment = 128,
TradeTimestamp = 129,
AiTargetedDetectionRadius = 130,
/// <summary>29 weenie attestations.</summary>
EmotePriority = 131,
LastTeleportStartTimestamp = 132,
EventSpamTimestamp = 133,
EventSpamRate = 134,
/// <summary>5 weenie attestations.</summary>
InventoryOffset = 135,
/// <summary>396 weenie attestations.</summary>
CriticalMultiplier = 136,
/// <summary>15 weenie attestations.</summary>
ManaStoneDestroyChance = 137,
/// <summary>351 weenie attestations.</summary>
SlayerDamageBonus = 138,
AllegianceInfoSpamTimestamp = 139,
AllegianceInfoSpamRate = 140,
NextSpellcastTimestamp = 141,
AppraisalRequestedTimestamp = 142,
AppraisalHeartbeatDueTimestamp = 143,
/// <summary>209 weenie attestations.</summary>
ManaConversionMod = 144,
LastPkAttackTimestamp = 145,
FellowshipUpdateTimestamp = 146,
/// <summary>395 weenie attestations.</summary>
CriticalFrequency = 147,
LimboStartTimestamp = 148,
/// <summary>356 weenie attestations.</summary>
WeaponMissileDefense = 149,
/// <summary>368 weenie attestations.</summary>
WeaponMagicDefense = 150,
/// <summary>87 weenie attestations.</summary>
IgnoreShield = 151,
/// <summary>135 weenie attestations.</summary>
ElementalDamageMod = 152,
StartMissileAttackTimestamp = 153,
LastRareUsedTimestamp = 154,
/// <summary>69 weenie attestations.</summary>
IgnoreArmor = 155,
ProcSpellRate = 156,
/// <summary>93 weenie attestations.</summary>
ResistanceModifier = 157,
AllegianceGagTimestamp = 158,
/// <summary>18 weenie attestations.</summary>
AbsorbMagicDamage = 159,
CachedMaxAbsorbMagicDamage = 160,
GagDuration = 161,
AllegianceGagDuration = 162,
GlobalXpMod = 163,
HealingModifier = 164,
/// <summary>391 weenie attestations.</summary>
ArmorModVsNether = 165,
ResistNether = 166,
/// <summary>369 weenie attestations.</summary>
CooldownDuration = 167,
WeaponAuraOffense = 168,
WeaponAuraDefense = 169,
WeaponAuraElemental = 170,
WeaponAuraManaConv = 171,
}

View file

@ -0,0 +1,79 @@
// <auto-generated-source>
// 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.
// </auto-generated-source>
namespace AcDream.Core.Properties;
/// <summary>
/// AC's <c>PropertyInstanceId</c> property table — the numeric keys the server sends in
/// <c>PrivateUpdatePropertyInstanceId (0x02E5) / PublicUpdatePropertyInstanceId (0x02E6)</c>
/// and in the property bundles carried by <c>CreateObject</c> / <c>PlayerDescription</c> /
/// <c>IdentifyResponse</c>. The CLR payload for this table is <c>uint (object guid)</c>.
///
/// <para>Verified 2026-07-29 against two independent oracles: the vendored
/// client-side enum catalog (<c>references/acclientlib/UtilityBelt.Common/Enums/Enums.cs</c>,
/// which names this table <c>InstanceId</c>) and the 38,985-file ACE weenie export corpus
/// (<c>references/weenies/</c>), whose per-stat <c>_comment</c> 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.</para>
///
/// <para>This enum is a <b>vocabulary</b>, not a parser contract: the wire carries a raw
/// <c>uint</c> and unknown keys must still round-trip untouched, so never assume a
/// received key is a defined member.</para>
/// </summary>
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,
/// <summary>130 weenie attestations.</summary>
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,
/// <summary>2 weenie attestations.</summary>
AllowedWielder = 38,
AssignedTarget = 39,
LimboSource = 40,
Snooper = 41,
TeleportedCharacter = 42,
Pet = 43,
PetOwner = 44,
PetDevice = 45,
}

View file

@ -0,0 +1,611 @@
// <auto-generated-source>
// 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.
// </auto-generated-source>
namespace AcDream.Core.Properties;
/// <summary>
/// AC's <c>PropertyInt</c> property table — the numeric keys the server sends in
/// <c>PrivateUpdatePropertyInt (0x02CD) / PublicUpdatePropertyInt (0x02CE)</c>
/// and in the property bundles carried by <c>CreateObject</c> / <c>PlayerDescription</c> /
/// <c>IdentifyResponse</c>. The CLR payload for this table is <c>int</c>.
///
/// <para>Verified 2026-07-29 against two independent oracles: the vendored
/// client-side enum catalog (<c>references/acclientlib/UtilityBelt.Common/Enums/Enums.cs</c>,
/// which names this table <c>IntId</c>) and the 38,985-file ACE weenie export corpus
/// (<c>references/weenies/</c>), whose per-stat <c>_comment</c> 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.</para>
///
/// <para>This enum is a <b>vocabulary</b>, not a parser contract: the wire carries a raw
/// <c>uint</c> and unknown keys must still round-trip untouched, so never assume a
/// received key is a defined member.</para>
/// </summary>
public enum PropertyInt : uint
{
Undef = 0,
/// <summary>UtilityBelt catalog calls this <c>ObjectType</c>; 37329 weenie attestations.</summary>
ItemType = 1,
/// <summary>6140 weenie attestations.</summary>
CreatureType = 2,
/// <summary>8344 weenie attestations.</summary>
PaletteTemplate = 3,
/// <summary>1597 weenie attestations.</summary>
ClothingPriority = 4,
/// <summary>25793 weenie attestations.</summary>
EncumbranceVal = 5,
/// <summary>8633 weenie attestations.</summary>
ItemsCapacity = 6,
/// <summary>8618 weenie attestations.</summary>
ContainersCapacity = 7,
/// <summary>23605 weenie attestations.</summary>
Mass = 8,
/// <summary>20797 weenie attestations.</summary>
ValidLocations = 9,
CurrentWieldedLocation = 10,
/// <summary>4373 weenie attestations.</summary>
MaxStackSize = 11,
/// <summary>4373 weenie attestations.</summary>
StackSize = 12,
/// <summary>3181 weenie attestations.</summary>
StackUnitEncumbrance = 13,
/// <summary>3181 weenie attestations.</summary>
StackUnitMass = 14,
/// <summary>3181 weenie attestations.</summary>
StackUnitValue = 15,
/// <summary>37280 weenie attestations.</summary>
ItemUseable = 16,
/// <summary>21 weenie attestations.</summary>
RareId = 17,
/// <summary>5705 weenie attestations.</summary>
UiEffects = 18,
/// <summary>25852 weenie attestations.</summary>
Value = 19,
/// <summary>1 weenie attestations.</summary>
CoinValue = 20,
TotalExperience = 21,
AvailableCharacter = 22,
TotalSkillCredits = 23,
AvailableSkillCredits = 24,
/// <summary>6140 weenie attestations.</summary>
Level = 25,
/// <summary>28 weenie attestations.</summary>
AccountRequirements = 26,
/// <summary>5476 weenie attestations.</summary>
ArmorType = 27,
/// <summary>1657 weenie attestations.</summary>
ArmorLevel = 28,
AllegianceCpPool = 29,
/// <summary>1 weenie attestations.</summary>
AllegianceRank = 30,
ChannelsAllowed = 31,
ChannelsActive = 32,
/// <summary>6252 weenie attestations.</summary>
Bonded = 33,
MonarchsRank = 34,
AllegianceFollowers = 35,
/// <summary>997 weenie attestations.</summary>
ResistMagic = 36,
/// <summary>575 weenie attestations.</summary>
ResistItemAppraisal = 37,
/// <summary>677 weenie attestations.</summary>
ResistLockpick = 38,
DeprecatedResistRepair = 39,
/// <summary>1634 weenie attestations.</summary>
CombatMode = 40,
CurrentAttackHeight = 41,
CombatCollisions = 42,
/// <summary>1 weenie attestations.</summary>
NumDeaths = 43,
/// <summary>3717 weenie attestations.</summary>
Damage = 44,
/// <summary>3357 weenie attestations.</summary>
DamageType = 45,
/// <summary>2973 weenie attestations.</summary>
DefaultCombatStyle = 46,
/// <summary>2364 weenie attestations.</summary>
AttackType = 47,
/// <summary>3393 weenie attestations.</summary>
WeaponSkill = 48,
/// <summary>3394 weenie attestations.</summary>
WeaponTime = 49,
/// <summary>972 weenie attestations.</summary>
AmmoType = 50,
/// <summary>4499 weenie attestations.</summary>
CombatUse = 51,
/// <summary>440 weenie attestations.</summary>
ParentLocation = 52,
/// <summary>440 weenie attestations.</summary>
PlacementPosition = 53,
WeaponEncumbrance = 54,
WeaponMass = 55,
ShieldValue = 56,
ShieldEncumbrance = 57,
MissileInventoryLocation = 58,
FullDamageType = 59,
/// <summary>545 weenie attestations.</summary>
WeaponRange = 60,
AttackersSkill = 61,
DefendersSkill = 62,
AttackersSkillValue = 63,
AttackersClass = 64,
Placement = 65,
/// <summary>179 weenie attestations.</summary>
CheckpointStatus = 66,
/// <summary>219 weenie attestations.</summary>
Tolerance = 67,
/// <summary>2151 weenie attestations.</summary>
TargetingTactic = 68,
/// <summary>48 weenie attestations.</summary>
CombatTactic = 69,
HomesickTargetingTactic = 70,
NumFollowFailures = 71,
/// <summary>494 weenie attestations.</summary>
FriendType = 72,
FoeType = 73,
/// <summary>UtilityBelt catalog calls this <c>MerchandiseObjectTypes</c>; 1049 weenie attestations.</summary>
MerchandiseItemTypes = 74,
/// <summary>1049 weenie attestations.</summary>
MerchandiseMinValue = 75,
/// <summary>1049 weenie attestations.</summary>
MerchandiseMaxValue = 76,
NumItemsSold = 77,
NumItemsBought = 78,
MoneyIncome = 79,
MoneyOutflow = 80,
/// <summary>2195 weenie attestations.</summary>
MaxGeneratedObjects = 81,
/// <summary>2195 weenie attestations.</summary>
InitGeneratedObjects = 82,
/// <summary>787 weenie attestations.</summary>
ActivationResponse = 83,
OriginalValue = 84,
NumMoveFailures = 85,
/// <summary>815 weenie attestations.</summary>
MinLevel = 86,
/// <summary>115 weenie attestations.</summary>
MaxLevel = 87,
/// <summary>10 weenie attestations.</summary>
LockpickMod = 88,
/// <summary>525 weenie attestations.</summary>
BoosterEnum = 89,
/// <summary>532 weenie attestations.</summary>
BoostValue = 90,
/// <summary>800 weenie attestations.</summary>
MaxStructure = 91,
/// <summary>713 weenie attestations.</summary>
Structure = 92,
/// <summary>38985 weenie attestations.</summary>
PhysicsState = 93,
/// <summary>3570 weenie attestations.</summary>
TargetType = 94,
/// <summary>2668 weenie attestations.</summary>
RadarBlipColor = 95,
/// <summary>472 weenie attestations.</summary>
EncumbranceCapacity = 96,
LoginTimestamp = 97,
/// <summary>119 weenie attestations.</summary>
CreationTimestamp = 98,
/// <summary>4 weenie attestations.</summary>
PkLevelModifier = 99,
/// <summary>575 weenie attestations.</summary>
GeneratorType = 100,
/// <summary>1265 weenie attestations.</summary>
AiAllowedCombatStyle = 101,
LogoffTimestamp = 102,
/// <summary>158 weenie attestations.</summary>
GeneratorDestructionType = 103,
ActivationCreateClass = 104,
/// <summary>693 weenie attestations.</summary>
ItemWorkmanship = 105,
/// <summary>4213 weenie attestations.</summary>
ItemSpellcraft = 106,
/// <summary>4173 weenie attestations.</summary>
ItemCurMana = 107,
/// <summary>4211 weenie attestations.</summary>
ItemMaxMana = 108,
/// <summary>3091 weenie attestations.</summary>
ItemDifficulty = 109,
/// <summary>1043 weenie attestations.</summary>
ItemAllegianceRankLimit = 110,
/// <summary>2940 weenie attestations.</summary>
PortalBitmask = 111,
AdvocateLevel = 112,
/// <summary>749 weenie attestations.</summary>
Gender = 113,
/// <summary>4502 weenie attestations.</summary>
Attuned = 114,
/// <summary>1640 weenie attestations.</summary>
ItemSkillLevelLimit = 115,
GateLogic = 116,
/// <summary>66 weenie attestations.</summary>
ItemManaCost = 117,
Logoff = 118,
/// <summary>365 weenie attestations.</summary>
Active = 119,
AttackHeight = 120,
NumAttackFailures = 121,
AiCpThreshold = 122,
AiAdvancementStrategy = 123,
Version = 124,
/// <summary>1 weenie attestations.</summary>
Age = 125,
/// <summary>1048 weenie attestations.</summary>
VendorHappyMean = 126,
/// <summary>1048 weenie attestations.</summary>
VendorHappyVariance = 127,
CloakStatus = 128,
VitaeCpPool = 129,
NumServicesSold = 130,
/// <summary>586 weenie attestations.</summary>
MaterialType = 131,
NumAllegianceBreaks = 132,
/// <summary>10541 weenie attestations.</summary>
ShowableOnRadar = 133,
/// <summary>3138 weenie attestations.</summary>
PlayerKillerStatus = 134,
VendorHappyMaxItems = 135,
/// <summary>5 weenie attestations.</summary>
ScorePageNum = 136,
/// <summary>5 weenie attestations.</summary>
ScoreConfigNum = 137,
/// <summary>5 weenie attestations.</summary>
ScoreNumScores = 138,
DeathLevel = 139,
/// <summary>1411 weenie attestations.</summary>
AiOptions = 140,
OpenToEveryone = 141,
/// <summary>297 weenie attestations.</summary>
GeneratorTimeType = 142,
/// <summary>51 weenie attestations.</summary>
GeneratorStartTime = 143,
/// <summary>50 weenie attestations.</summary>
GeneratorEndTime = 144,
/// <summary>257 weenie attestations.</summary>
GeneratorEndDestructionType = 145,
/// <summary>4361 weenie attestations.</summary>
XpOverride = 146,
NumCrashAndTurns = 147,
ComponentWarningThreshold = 148,
/// <summary>7 weenie attestations.</summary>
HouseStatus = 149,
/// <summary>4715 weenie attestations.</summary>
HookPlacement = 150,
/// <summary>6799 weenie attestations.</summary>
HookType = 151,
/// <summary>UtilityBelt catalog calls this <c>HookObjectType</c>; 5 weenie attestations.</summary>
HookItemType = 152,
AiPpThreshold = 153,
GeneratorVersion = 154,
/// <summary>6274 weenie attestations.</summary>
HouseType = 155,
/// <summary>10 weenie attestations.</summary>
PickupEmoteOffset = 156,
/// <summary>1 weenie attestations.</summary>
WeenieIteration = 157,
/// <summary>2557 weenie attestations.</summary>
WieldRequirements = 158,
/// <summary>2557 weenie attestations.</summary>
WieldSkilltype = 159,
/// <summary>2557 weenie attestations.</summary>
WieldDifficulty = 160,
/// <summary>654 weenie attestations.</summary>
HouseMaxHooksUsable = 161,
HouseCurrentHooksUsable = 162,
/// <summary>8 weenie attestations.</summary>
AllegianceMinLevel = 163,
AllegianceMaxLevel = 164,
HouseRelinkHookCount = 165,
/// <summary>537 weenie attestations.</summary>
SlayerCreatureType = 166,
ConfirmationInProgress = 167,
ConfirmationTypeInProgress = 168,
/// <summary>728 weenie attestations.</summary>
TsysMutationData = 169,
/// <summary>36 weenie attestations.</summary>
NumItemsInMaterial = 170,
/// <summary>228 weenie attestations.</summary>
NumTimesTinkered = 171,
/// <summary>358 weenie attestations.</summary>
AppraisalLongDescDecoration = 172,
/// <summary>133 weenie attestations.</summary>
AppraisalLockpickSuccessPercent = 173,
/// <summary>103 weenie attestations.</summary>
AppraisalPages = 174,
/// <summary>103 weenie attestations.</summary>
AppraisalMaxPages = 175,
/// <summary>355 weenie attestations.</summary>
AppraisalItemSkill = 176,
/// <summary>281 weenie attestations.</summary>
GemCount = 177,
/// <summary>281 weenie attestations.</summary>
GemType = 178,
/// <summary>495 weenie attestations.</summary>
ImbuedEffect = 179,
AttackersRawSkillValue = 180,
ChessRank = 181,
ChessTotalGames = 182,
ChessGamesWon = 183,
ChessGamesLost = 184,
/// <summary>71 weenie attestations.</summary>
TypeOfAlteration = 185,
/// <summary>71 weenie attestations.</summary>
SkillToBeAltered = 186,
SkillAlterationCount = 187,
/// <summary>807 weenie attestations.</summary>
HeritageGroup = 188,
/// <summary>30 weenie attestations.</summary>
TransferFromAttribute = 189,
/// <summary>30 weenie attestations.</summary>
TransferToAttribute = 190,
AttributeTransferCount = 191,
FakeFishingSkill = 192,
/// <summary>3 weenie attestations.</summary>
NumKeys = 193,
DeathTimestamp = 194,
PkTimestamp = 195,
VictimTimestamp = 196,
/// <summary>22 weenie attestations.</summary>
HookGroup = 197,
AllegianceSwearTimestamp = 198,
HousePurchaseTimestamp = 199,
RedirectableEquippedArmorCount = 200,
MeleedefenseImbuedEffectTypeCache = 201,
MissileDefenseImbuedEffectTypeCache = 202,
MagicDefenseImbuedEffectTypeCache = 203,
/// <summary>121 weenie attestations.</summary>
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,
/// <summary>1 weenie attestations.</summary>
ItemAttributeLimit = 257,
/// <summary>1 weenie attestations.</summary>
ItemAttributeLevelLimit = 258,
ItemAttribute2ndLimit = 259,
ItemAttribute2ndLevelLimit = 260,
/// <summary>2 weenie attestations.</summary>
CharacterTitleId = 261,
/// <summary>1 weenie attestations.</summary>
NumCharacterTitles = 262,
/// <summary>93 weenie attestations.</summary>
ResistanceModifierType = 263,
FreeTinkersBitfield = 264,
/// <summary>316 weenie attestations.</summary>
EquipmentSetId = 265,
PetClass = 266,
/// <summary>119 weenie attestations.</summary>
Lifespan = 267,
/// <summary>119 weenie attestations.</summary>
RemainingLifespan = 268,
UseCreateQuantity = 269,
/// <summary>62 weenie attestations.</summary>
WieldRequirements2 = 270,
/// <summary>62 weenie attestations.</summary>
WieldSkilltype2 = 271,
/// <summary>62 weenie attestations.</summary>
WieldDifficulty2 = 272,
/// <summary>3 weenie attestations.</summary>
WieldRequirements3 = 273,
/// <summary>3 weenie attestations.</summary>
WieldSkilltype3 = 274,
/// <summary>3 weenie attestations.</summary>
WieldDifficulty3 = 275,
/// <summary>17 weenie attestations.</summary>
WieldRequirements4 = 276,
/// <summary>17 weenie attestations.</summary>
WieldSkilltype4 = 277,
/// <summary>17 weenie attestations.</summary>
WieldDifficulty4 = 278,
/// <summary>14 weenie attestations.</summary>
Unique = 279,
/// <summary>365 weenie attestations.</summary>
SharedCooldown = 280,
/// <summary>177 weenie attestations.</summary>
Faction1Bits = 281,
Faction2Bits = 282,
Faction3Bits = 283,
Hatred1Bits = 284,
Hatred2Bits = 285,
Hatred3Bits = 286,
/// <summary>61 weenie attestations.</summary>
SocietyRankCelhan = 287,
/// <summary>60 weenie attestations.</summary>
SocietyRankEldweb = 288,
/// <summary>56 weenie attestations.</summary>
SocietyRankRadblo = 289,
HearLocalSignals = 290,
HearLocalSignalsRadius = 291,
/// <summary>67 weenie attestations.</summary>
Cleaving = 292,
AugmentationSpecializeGearcraft = 293,
AugmentationInfusedCreatureMagic = 294,
AugmentationInfusedItemMagic = 295,
AugmentationInfusedLifeMagic = 296,
AugmentationInfusedWarMagic = 297,
AugmentationCriticalExpertise = 298,
AugmentationCriticalPower = 299,
AugmentationSkilledMelee = 300,
AugmentationSkilledMissile = 301,
AugmentationSkilledMagic = 302,
/// <summary>124 weenie attestations.</summary>
ImbuedEffect2 = 303,
/// <summary>124 weenie attestations.</summary>
ImbuedEffect3 = 304,
/// <summary>117 weenie attestations.</summary>
ImbuedEffect4 = 305,
/// <summary>117 weenie attestations.</summary>
ImbuedEffect5 = 306,
/// <summary>1403 weenie attestations.</summary>
DamageRating = 307,
/// <summary>729 weenie attestations.</summary>
DamageResistRating = 308,
AugmentationDamageBonus = 309,
AugmentationDamageReduction = 310,
ImbueStackingBits = 311,
HealOverTime = 312,
/// <summary>880 weenie attestations.</summary>
CritRating = 313,
/// <summary>843 weenie attestations.</summary>
CritDamageRating = 314,
/// <summary>713 weenie attestations.</summary>
CritResistRating = 315,
/// <summary>732 weenie attestations.</summary>
CritDamageResistRating = 316,
HealingResistRating = 317,
DamageOverTime = 318,
/// <summary>79 weenie attestations.</summary>
ItemMaxLevel = 319,
/// <summary>79 weenie attestations.</summary>
ItemXpStyle = 320,
EquipmentSetExtra = 321,
AetheriaBitfield = 322,
HealingBoostRating = 323,
/// <summary>29 weenie attestations.</summary>
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,
/// <summary>13 weenie attestations.</summary>
CloakWeaveProc = 352,
/// <summary>492 weenie attestations.</summary>
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,
/// <summary>259 weenie attestations.</summary>
UseRequiresSkill = 366,
/// <summary>259 weenie attestations.</summary>
UseRequiresSkillLevel = 367,
/// <summary>36 weenie attestations.</summary>
UseRequiresSkillSpec = 368,
/// <summary>271 weenie attestations.</summary>
UseRequiresLevel = 369,
/// <summary>869 weenie attestations.</summary>
GearDamage = 370,
/// <summary>868 weenie attestations.</summary>
GearDamageResist = 371,
/// <summary>854 weenie attestations.</summary>
GearCrit = 372,
/// <summary>854 weenie attestations.</summary>
GearCritResist = 373,
/// <summary>955 weenie attestations.</summary>
GearCritDamage = 374,
/// <summary>953 weenie attestations.</summary>
GearCritDamageResist = 375,
/// <summary>610 weenie attestations.</summary>
GearHealingBoost = 376,
/// <summary>602 weenie attestations.</summary>
GearNetherResist = 377,
/// <summary>602 weenie attestations.</summary>
GearLifeResist = 378,
/// <summary>640 weenie attestations.</summary>
GearMaxHealth = 379,
Unknown380 = 380,
/// <summary>616 weenie attestations.</summary>
PKDamageRating = 381,
/// <summary>602 weenie attestations.</summary>
PKDamageResistRating = 382,
/// <summary>643 weenie attestations.</summary>
GearPKDamageRating = 383,
/// <summary>640 weenie attestations.</summary>
GearPKDamageResistRating = 384,
Unknown385 = 385,
/// <summary>766 weenie attestations.</summary>
Overpower = 386,
/// <summary>602 weenie attestations.</summary>
OverpowerResist = 387,
/// <summary>602 weenie attestations.</summary>
GearOverpower = 388,
/// <summary>602 weenie attestations.</summary>
GearOverpowerResist = 389,
/// <summary>2 weenie attestations.</summary>
Enlightenment = 390,
}

View file

@ -0,0 +1,43 @@
// <auto-generated-source>
// 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.
// </auto-generated-source>
namespace AcDream.Core.Properties;
/// <summary>
/// AC's <c>PropertyInt64</c> property table — the numeric keys the server sends in
/// <c>PrivateUpdatePropertyInt64 (0x02D1)</c>
/// and in the property bundles carried by <c>CreateObject</c> / <c>PlayerDescription</c> /
/// <c>IdentifyResponse</c>. The CLR payload for this table is <c>long</c>.
///
/// <para>Verified 2026-07-29 against two independent oracles: the vendored
/// client-side enum catalog (<c>references/acclientlib/UtilityBelt.Common/Enums/Enums.cs</c>,
/// which names this table <c>Int64Id</c>) and the 38,985-file ACE weenie export corpus
/// (<c>references/weenies/</c>), whose per-stat <c>_comment</c> 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.</para>
///
/// <para>This enum is a <b>vocabulary</b>, not a parser contract: the wire carries a raw
/// <c>uint</c> and unknown keys must still round-trip untouched, so never assume a
/// received key is a defined member.</para>
/// </summary>
public enum PropertyInt64 : uint
{
Undef = 0,
TotalExperience = 1,
AvailableExperience = 2,
/// <summary>7 weenie attestations.</summary>
AugmentationCost = 3,
/// <summary>79 weenie attestations.</summary>
ItemTotalXp = 4,
/// <summary>79 weenie attestations.</summary>
ItemBaseXp = 5,
AvailableLuminance = 6,
MaximumLuminance = 7,
InteractionReqs = 8,
}

View file

@ -0,0 +1,112 @@
// <auto-generated-source>
// 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.
// </auto-generated-source>
namespace AcDream.Core.Properties;
/// <summary>
/// AC's <c>PropertyString</c> property table — the numeric keys the server sends in
/// <c>PrivateUpdatePropertyString (0x02DD) / PublicUpdatePropertyString (0x02DE)</c>
/// and in the property bundles carried by <c>CreateObject</c> / <c>PlayerDescription</c> /
/// <c>IdentifyResponse</c>. The CLR payload for this table is <c>string</c>.
///
/// <para>Verified 2026-07-29 against two independent oracles: the vendored
/// client-side enum catalog (<c>references/acclientlib/UtilityBelt.Common/Enums/Enums.cs</c>,
/// which names this table <c>StringId</c>) and the 38,985-file ACE weenie export corpus
/// (<c>references/weenies/</c>), whose per-stat <c>_comment</c> 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.</para>
///
/// <para>This enum is a <b>vocabulary</b>, not a parser contract: the wire carries a raw
/// <c>uint</c> and unknown keys must still round-trip untouched, so never assume a
/// received key is a defined member.</para>
/// </summary>
public enum PropertyString : uint
{
Undef = 0,
/// <summary>38984 weenie attestations.</summary>
Name = 1,
Title = 2,
/// <summary>1697 weenie attestations.</summary>
Sex = 3,
/// <summary>1693 weenie attestations.</summary>
HeritageGroup = 4,
/// <summary>1622 weenie attestations.</summary>
Template = 5,
AttackersName = 6,
/// <summary>93 weenie attestations.</summary>
Inscription = 7,
/// <summary>93 weenie attestations.</summary>
ScribeName = 8,
VendorsName = 9,
/// <summary>1 weenie attestations.</summary>
Fellowship = 10,
MonarchsName = 11,
/// <summary>449 weenie attestations.</summary>
LockCode = 12,
/// <summary>238 weenie attestations.</summary>
KeyCode = 13,
/// <summary>3219 weenie attestations.</summary>
Use = 14,
/// <summary>5597 weenie attestations.</summary>
ShortDesc = 15,
/// <summary>7107 weenie attestations.</summary>
LongDesc = 16,
/// <summary>109 weenie attestations.</summary>
ActivationTalk = 17,
/// <summary>10 weenie attestations.</summary>
UseMessage = 18,
/// <summary>45 weenie attestations.</summary>
ItemHeritageGroupRestriction = 19,
/// <summary>1318 weenie attestations.</summary>
PluralName = 20,
MonarchsTitle = 21,
/// <summary>96 weenie attestations.</summary>
ActivationFailure = 22,
ScribeAccount = 23,
/// <summary>591 weenie attestations.</summary>
TownName = 24,
/// <summary>2 weenie attestations.</summary>
CraftsmanName = 25,
/// <summary>4 weenie attestations.</summary>
UsePkServerError = 26,
ScoreCachedText = 27,
/// <summary>5 weenie attestations.</summary>
ScoreDefaultEntryFormat = 28,
/// <summary>5 weenie attestations.</summary>
ScoreFirstEntryFormat = 29,
/// <summary>5 weenie attestations.</summary>
ScoreLastEntryFormat = 30,
/// <summary>5 weenie attestations.</summary>
ScoreOnlyEntryFormat = 31,
/// <summary>5 weenie attestations.</summary>
ScoreNoEntry = 32,
/// <summary>859 weenie attestations.</summary>
Quest = 33,
/// <summary>249 weenie attestations.</summary>
GeneratorEvent = 34,
PatronsTitle = 35,
HouseOwnerName = 36,
/// <summary>170 weenie attestations.</summary>
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,
}

View file

@ -0,0 +1,133 @@
using System;
using System.Linq;
using AcDream.Core.Items;
using Xunit;
namespace AcDream.Core.Tests.Properties;
/// <summary>
/// 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 <c>docs/research/named-retail/acclient.h</c>.
/// </summary>
public sealed class ItemWireEnumConformanceTests
{
/// <summary>acclient.h:4221, <c>enum AMMO_TYPE</c>.</summary>
public static TheoryData<string, uint> 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<AmmoType>(name));
}
[Fact]
public void AmmoTypeDeclaresNothingRetailDoesNot()
{
var expected = RetailAmmoType.Select(r => (string)r[0]).OrderBy(n => n, StringComparer.Ordinal);
Assert.Equal(expected, Enum.GetNames<AmmoType>().OrderBy(n => n, StringComparer.Ordinal));
}
/// <summary>acclient.h:6523, <c>enum COMBAT_USE</c>.</summary>
[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<CombatUse>(name));
}
/// <summary>acclient.h:6478, <c>enum ITEM_USEABLE</c>, including the composites.</summary>
public static TheoryData<string, uint> 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<ItemUseable>(name));
}
[Fact]
public void ItemUseableDeclaresNothingRetailDoesNot()
{
var expected = RetailItemUseable.Select(r => (string)r[0]).OrderBy(n => n, StringComparer.Ordinal);
Assert.Equal(expected, Enum.GetNames<ItemUseable>().OrderBy(n => n, StringComparer.Ordinal));
}
/// <summary>
/// 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.
/// </summary>
[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);
}
}

File diff suppressed because it is too large Load diff

View file

@ -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;
/// <summary>
/// Pins the wire-adjacent enums acdream shares with the retail client to the values
/// in the Sept 2013 EoR header, <c>docs/research/named-retail/acclient.h</c>. Each
/// table below is transcribed from the cited <c>enum</c> block, so a hand edit that
/// drifts from retail fails here.
///
/// <para>These tables are the reason the 2026-07-29 enum campaign found real bugs:
/// <see cref="DamageType"/> had its four drain/restore bits rotated, and
/// <see cref="ItemType"/> had a shifted craft ladder plus locally recomputed composite
/// masks. Both are corrected and pinned below.</para>
/// </summary>
public sealed class RetailEnumConformanceTests
{
/// <summary>acclient.h:3788, <c>enum DAMAGE_TYPE</c>. FORCE_*_32_BIT omitted.</summary>
public static TheoryData<string, uint> 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<DamageType>(name));
}
[Fact]
public void DamageTypeDeclaresNothingRetailDoesNot()
{
var expected = RetailDamageType.Select(r => (string)r[0]).OrderBy(n => n, StringComparer.Ordinal);
var actual = Enum.GetNames<DamageType>().OrderBy(n => n, StringComparer.Ordinal);
Assert.Equal(expected, actual);
}
/// <summary>
/// acclient.h:3300, <c>enum ITEM_TYPE</c>. TYPE_UNDEF and TYPE_SELF both sit on 0
/// in retail; acdream spells that single zero <c>None</c>. FORCE_*_32_BIT omitted.
/// </summary>
public static TheoryData<string, uint> 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<ItemType>(name));
}
[Fact]
public void ItemTypeDeclaresNothingRetailDoesNot()
{
var expected = RetailItemType.Select(r => (string)r[0]).OrderBy(n => n, StringComparer.Ordinal);
var actual = Enum.GetNames<ItemType>().OrderBy(n => n, StringComparer.Ordinal);
Assert.Equal(expected, actual);
}
/// <summary>
/// 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.
/// </summary>
[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<ItemType>(), t => (uint)t == 0x02000000u);
}
/// <summary>
/// <c>Weapon</c> and <c>WeaponOrCaster</c> were aliases before the campaign, because
/// <c>Weapon</c> was recomputed locally as melee|missile|caster. Retail keeps them
/// distinct and only the latter includes the caster bit.
/// </summary>
[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);
}
/// <summary>
/// acclient.h:3193, <c>enum INVENTORY_LOC</c> — the composite slot groups only. The
/// 32 primitive slots were already correct and are pinned by <c>EquipMaskTests</c>.
/// </summary>
public static TheoryData<string, uint> 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<EquipMask>(name));
}
/// <summary>
/// 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 <see cref="EquipMask.Cloak"/>.
/// </summary>
[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);
}
/// <summary>acclient.h:3688, <c>enum TransientState</c>.</summary>
public static TheoryData<string, uint> 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<TransientStateFlags>(name));
}
/// <summary>acclient.h:2815, <c>enum PhysicsState</c>, including the two bits retail itself reserves.</summary>
public static TheoryData<string, uint> 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<PhysicsStateFlags>(name));
}
/// <summary>acclient.h:4371, <c>enum ATTACK_HEIGHT</c>. NUM_ATTACK_HEIGHTS is a count, not a height.</summary>
[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<AttackHeight>(name));
}
/// <summary>
/// acclient.h:3807, <c>enum AttackType</c>. 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.
/// </summary>
[Fact]
public void AttackTypeCompositesMatchRetailLiterals()
{
Assert.Equal(0x19u, (uint)AttackType.Unarmed);
Assert.Equal(0x79E0u, (uint)AttackType.MultiStrike);
}
}