diff --git a/docs/plans/2026-07-29-post-vulkan-work-intake.md b/docs/plans/2026-07-29-post-vulkan-work-intake.md index e6dd0df7..c83224ba 100644 --- a/docs/plans/2026-07-29-post-vulkan-work-intake.md +++ b/docs/plans/2026-07-29-post-vulkan-work-intake.md @@ -17,7 +17,7 @@ once Campaign V closes. | Item | Existing foundation | Shape of the work | |---|---|---| -| **VERIFY all enums** | `docs/research/2026-06-04-property-enum-divergence.md`: 929 values missing across 7 `Property*` enums, plus the `acdream-property-enum-sweep` skill for regeneration. | Extend the sweep beyond Property* to every wire-adjacent enum family; adopt values with ACE as oracle; conformance tests per enum. | +| **VERIFY all enums** — **DONE 2026-07-29**, see [`docs/research/2026-07-29-enum-verification-campaign.md`](../research/2026-07-29-enum-verification-campaign.md) | The 2026-06-04 divergence note turned out to be missing from the repo entirely (open question 1 in the successor), so the sweep was regenerated from scratch. | Seven `Property*` tables created (864 members); `DamageType` and `ItemType` value bugs fixed; equipment/physics/item families closed against retail. Twelve open questions remain, headed by five empty `references/` repos. | | **Verify networking stack** | `docs/research/2026-06-04-wire-message-catalog.md`: 256 opcodes catalogued, 114 parsed. | Audit parsed-vs-stub-vs-missing against the catalog; prioritize by what live servers actually send (Coldeve session logs are now a real corpus); holtburger remains the client-side oracle. | | **Physics review — retail landing bounce et al.** | The named-retail decomp workflow; carried debt #116 (slide response), TS-50/51/53 in the divergence register. | A focused fidelity pass: start with the user's observed case — long-jump landing should bounce/recoil as retail does (grep `named-retail` for the landing/impact path in `MovementManager`/`SetMotion` land states before writing anything). Each fix follows grep→decompile→pseudocode→port→conformance. | diff --git a/docs/research/2026-07-29-enum-verification-campaign.md b/docs/research/2026-07-29-enum-verification-campaign.md new file mode 100644 index 00000000..25fa66a0 --- /dev/null +++ b/docs/research/2026-07-29-enum-verification-campaign.md @@ -0,0 +1,260 @@ +# Enum verification campaign — 2026-07-29 + +Bucket B item 1 of [`docs/plans/2026-07-29-post-vulkan-work-intake.md`](../plans/2026-07-29-post-vulkan-work-intake.md). +Successor to the (missing — see Open question 1) 2026-06-04 property-enum +divergence note. Run unattended against base `b70b9832`. + +**Headline: the campaign found two real value bugs and one wrong comment, all in +enums nothing currently reads.** They were traps armed for the next person to +write a comparison, not live defects. Everything else diffs clean. + +--- + +## 1. The oracle set actually used + +CLAUDE.md's reference hierarchy names six vendored repos. In this environment +**`references/ACE`, `references/Chorizite.ACProtocol`, `references/holtburger`, +`references/ACViewer`, `references/AC2D` and `references/DatReaderWriter` are all +empty directories** (`references/*` is gitignored; only WorldBuilder is checked +in). The campaign therefore re-anchored on the oracles that *are* present, which +is no loss of rigour — CLAUDE.md ranks the first of them above ACE anyway: + +| # | Oracle | What it is | Weight | +|---|---|---|---| +| 1 | `docs/research/named-retail/acclient.h` | The Sept 2013 EoR retail header, verbatim. 348 parseable `enum` blocks. | **Decides.** CLAUDE.md: "beats every other reference for what the real client does." | +| 2 | `references/acclientlib/UtilityBelt.Common/Enums/Enums.cs` | 18,313-line client-side enum catalog, shipped beside a vendored `ACE.Entity` tree. Holds the seven property tables as `IntId`/`BoolId`/`FloatId`/… | Strong, **not infallible** — see §2. | +| 3 | `references/weenies/**/*.json` | 38,985 ACE weenie exports. Every stat entry carries the numeric key *and* the enum member name in its `_comment`; enum-valued ints also name the value member after `=`. | Independent mechanical attestation, limited to values some weenie actually sets. | +| 4 | `references/WorldBuilder/.../ACEnums.cs` | 233 lines, a handful of property members. | Spot check only. | + +Retail does **not** name the seven property tables anywhere in `acclient.h` +(searched as `SType*`, `Property*`, and by member name). The client receives +property keys as opaque `u32`, so oracles 2 and 3 carry those alone. + +**Why having more than one oracle mattered:** on `ItemType.CraftFletchingBase` +the *catalog* is wrong (says `0x02000000`) and acdream was right (`0x01000000`, +matching retail). Had the campaign trusted the catalog the way the mission brief +assumed it would trust ACE, it would have introduced a bug. No single source was +taken on faith; retail's header decided every disagreement, with the weenie +corpus as tiebreak. + +--- + +## 2. Bugs found and fixed + +### 2.1 `DamageType` — four bits rotated (`f3e95a3e`) + +acdream assigned `Nether/Mana/Health/Stamina` to `0x80/0x100/0x200/0x400`. +Retail `DAMAGE_TYPE` (acclient.h:3788) assigns `Health/Stamina/Mana/Nether`. + +| value | acdream (before) | retail | catalog | weenie corpus | +|---|---|---|---|---| +| 0x80 | Nether | **Health** | — | — | +| 0x100 | Mana | **Stamina** | — | Stamina (n=2) | +| 0x200 | Health | **Mana** | — | Mana (n=8) | +| 0x400 | Stamina | **Nether** | — | Nether (n=9) | + +`BASE_DAMAGE_TYPE = 0x10000000` was also absent. Both of acdream's live +damage-type name tables — `CombatChatTranslator.FormatDamageType` (ported from +holtburger) and `ItemAppraisalTextFormatter.TryDamageTypeName` — already used +retail's order reading the raw wire `uint`, and `CombatChatTranslator` already +knew about `base`. The enum was the only thing in the tree that was wrong, and +nothing consumed the rotated members. **No live mislabel; latent trap removed.** + +### 2.2 `ItemType` — shifted craft ladder + locally recomputed composites (`f3e95a3e`) + +Retail `ITEM_TYPE` (acclient.h:3300) leaves `0x02000000` unused and puts +`CRAFT_ALCHEMY_INTERMEDIATE` on `0x04000000`. acdream had alchemy-intermediate +on `0x02000000` and an **invented `CraftCookingIntermediate`** squatting on +`0x04000000`. The weenie corpus attests `0x04000000` as +`Craft_Alchemy_Intermediate` 235 times and contains no cooking-intermediate at +all — there is no such item type. + +Separately the composite masks were derived from the local primitives instead of +transcribed, which is how the ladder drifted in the first place: + +| member | acdream (before) | retail | +|---|---|---| +| `Weapon` | `0x8101` (= melee\|missile\|caster) | `0x101` — melee\|missile, **no caster** | +| `WeaponOrCaster` | `0x8101` (alias of `Weapon`) | `0x8101` | +| `Item` | `0x830F` | `0x2DFBEF` | + +Five retail composites acdream never had (`PortalMagicTarget`, +`LockableMagicTarget`, `ItemEnchantableTarget`, +`RedirectableItemEnchantmentTarget`, `VendorShopkeep`, `VendorGrocer`) came +along. Only one site in the tree referenced any changed member — a test that +wants a nonzero `HookItemTypes` and does not care which — so **no branch changed.** + +### 2.3 `EquipMask` — a wrong type remark (`8ccaf72a`) + +A remark claimed retail's `CLOTHING_LOC` composite "also sets bit 31, +0x80000000, which is not a named INVENTORY_LOC primitive". It does not. +`CLOTHING_LOC` is `0x080001FF`: the nine wear slots plus **bit 27**, which is the +perfectly well-named `Cloak` slot. No `INVENTORY_LOC` member touches bit 31 at +all — `ALL_LOC` stops at bit 30. Caught by writing the composite test. + +--- + +## 3. Per-family end state + +### 3.1 The seven `Property*` tables (`251dd68a`) + +acdream had **no** `Property*` enums at all — property IDs were bare `uint`s with +the meaning carried in prose (`private const uint EncumbranceValProperty = 5u` +duplicated across two files, `UiEffects` as "ACE enum value 18" in a doc comment). +All seven are now created under `AcDream.Core.Properties`. + +| family | members adopted | attested by both oracles | single-sourced | +|---|---|---|---| +| `PropertyInt` | 391 | 189 | 202 | +| `PropertyFloat` | 172 | 86 | 86 | +| `PropertyBool` | 131 | 59 | 72 | +| `PropertyDataId` | 62 | 41 | 21 | +| `PropertyString` | 53 | 28 | 25 | +| `PropertyInstanceId` | 46 | 2 | 44 | +| `PropertyInt64` | 9 | 3 | 6 | +| **total** | **864** | **408** | **456** | + +Zero value conflicts between the two oracles across all seven, and the corpus +contained no key the catalog lacked — the catalog is a strict superset of +everything 38,985 weenies set. Three members differ in *spelling* only; acdream +took ACE's (`ItemType`/`HookItemType`/`MerchandiseItemTypes` over the catalog's +`ObjectType`/…), which is what the corpus emits and what acdream's own `ItemType` +already called it. + +### 3.2 Existing wire-adjacent enums, diffed against retail + +Legend: **closed** = every retail member present at retail's value. + +| acdream enum | retail enum | end state | +|---|---|---| +| `ItemType` | `ITEM_TYPE` | **closed** (2 fixed) — retail's `TYPE_UNDEF`/`TYPE_SELF` are both 0; acdream spells the single 0 `None` | +| `DamageType` | `DAMAGE_TYPE` | **closed** (4 fixed, 1 added) | +| `EquipMask` | `INVENTORY_LOC` | **closed** — 43/43, 11 composites adopted | +| `TransientStateFlags` | `TransientState` | **closed** — `WaterContact`, `CheckEthereal` adopted | +| `PhysicsStateFlags` | `PhysicsState` | **closed** — `ReservedUnused1/2` adopted | +| `ObjectInfoState` | `ObjectInfoEnum` | **closed** — was already exact (`None` = `DEFAULT_OI`) | +| `AttackHeight` | `ATTACK_HEIGHT` | **closed** — `Undef` adopted; `NUM_ATTACK_HEIGHTS` is a count, deliberately not a member | +| `HoldKey` | `HoldKey` | **closed** — `Num_HoldKeys` is a count | +| `AttackType` | `AttackType` | **already correct**, incl. both composites (`Unarmed` 0x19, `MultiStrike` 0x79E0) | +| `RadarBlipShape` | `RadarBlipShape` | already exact (14/14) | +| `RadarBehavior` | `RadarEnum` | already exact (5/5) | +| `MovementType` | `MovementTypes::Type` | already exact (10/10) | +| `ParticleType` | `ParticleType` | already exact (14/14) | +| `PhysicsDescriptionFlag` | `PhysicsDesc::PhysicsDescInfo` | already exact (naming only: `AnimationFrame`/`ANIMFRAME_ID`) | + +### 3.3 New enums for wire fields acdream parsed but never named (`3efa266a`) + +| enum | retail source | members | why | +|---|---|---|---| +| `AmmoType` | acclient.h:4221 | 10 | parsed via `PublicWeenieDesc._ammo_type`; the appraisal sentence matched raw hex | +| `CombatUse` | acclient.h:6523 | 6 | `PropertyInt.CombatUse` (51) | +| `ItemUseable` | acclient.h:6478 | 39 | `PropertyInt.ItemUseable` (16); two 16-bit halves, ~30 named combinations | + +`ItemAppraisalTextFormatter`'s ammunition fold now reads through `AmmoType`. Its +crystal/chorizite → base collapse was verified correct against retail's bit +layout before the change; behaviour is unchanged. + +### 3.4 Verified correct, no change needed + +- `ItemAppraisalTextFormatter` ammo fold (§3.3) against `AMMO_TYPE`. +- `CombatChatTranslator.FormatDamageType` and + `ItemAppraisalTextFormatter.TryDamageTypeName` against `DAMAGE_TYPE` — both + already retail-correct, which is what exposed §2.1. + +--- + +## 4. Open questions + +Each is something the campaign could **not** settle from an oracle. Per +CLAUDE.md these are recorded rather than guessed. + +1. **The 2026-06-04 research drop is missing.** `claude-memory/MEMORY.md` indexes + `research/2026-06-04-property-enum-divergence.md` (the "929 values across 7 + enums" ledger) and `research/2026-06-04-magic-number-audit.md`. Neither exists + in the working tree, under any ref (`git log --all --diff-filter=A`), or in the + memory directory — which has no `research/` subfolder at all. The 929 figure + could not be reproduced or audited; this campaign regenerated from scratch and + arrived at 864 property members. **Blocker:** source documents absent. Either + they were never committed or they lived in a discarded worktree. Suggest + fixing the MEMORY.md index entries to point at this doc. +2. **Five of six reference repos are empty** (§1). Everything sourced here came + from retail + the UtilityBelt catalog + the weenie corpus. **Blocker:** repos + not cloned in this environment. Re-cloning ACE and Chorizite would let the 456 + single-sourced property members (§3.1) be promoted to two-oracle confirmed. +3. **456 of 864 property members are single-sourced.** They are transcribed from + the catalog, not invented, but no weenie in the corpus sets them so there is no + independent attestation. **Blocker:** needs ACE source, or the client DAT's own + `EnumMapper` file type (`acclientlib` has a reader for it — a genuinely retail + oracle, and the most promising unexplored lead). +4. **`WeenieError` — 362 unadopted status codes.** acdream has 16 members; the + catalog's `StatusMessage` has 372, with 10 shared and zero conflicts. Retail's + `charError` (26) is a *different* enum — character-creation errors only — so + retail cannot arbitrate. Register row **AP-15** already tracks that acdream's + translation table covers only ~30 common codes. **Blocker:** needs a decision + on importing 362 server-side codes wholesale, and ideally retail's + `string_table.bin` for the real sentences. Deliberately not done here. +5. **`CombatMode` has no located retail counterpart.** acdream's 7 members match + the catalog's `CombatMode` (`Magic = 8`). Retail's `CombatStyle` (25 members, + `Magic_CombatStyle = 512`) is a different enum — weapon-style, not combat mode. + A retail `COMBAT_MODE` was not found in `acclient.h`. **Blocker:** oracle not + located; acdream is probably right but is single-sourced. +6. **`PublicWeenieFlags` counterpart unidentified.** acdream's 17 members + (`Attackable`, `Door`, `Vendor`, …) share exactly one name with retail's + `PublicWeenieDescPackHeader` (34), which is the *pack-header* bitfield — a + different thing. `PublicWeenieDesc::BitfieldIndex` (31 members) is the likely + real counterpart. **Blocker:** pairing unconfirmed; not diffed. +7. **`SoundId` is a curated 23-member local subset** of retail's `SoundType` + (206), with acdream-local names (`FootstepGrass`, `BuffApplied`) that do not + correspond 1:1 to retail members. **Blocker:** needs a decision — adopt + retail's table wholesale, or record the subset as an intentional divergence + with a register row. Neither was done. +8. **`ChatType` / `BlobType` / `DispatchType`** (TurbineChat) share no members + with retail's `eChatTypes` (26) or `ChatTypeEnum` (12); they describe chat + *channels*, not text-display types. **Blocker:** correct counterpart not + identified. Relevant to the Bucket C chatbox cleanup. +9. **`GameEventType` (103 members) was not verified.** No counterpart found under + a matching name in either retail or the catalog. **Blocker:** needs the wire + catalog (Bucket B item 2) to supply the pairing. +10. **`MaterialType` not adopted.** acdream parses it as raw `uint` from + `PublicWeenieDesc`; the catalog has 108 members; no retail counterpart located + in `acclient.h`. **Blocker:** single-sourced, and unused so far. +11. **`SpellTargetType`, `ItemPrimaryUseResult`, `PositionFlags`, + `EnchantmentMask`/`EnchantmentBucket`, `GameMessageGroup`, `InventoryRequestKind` + are acdream-local** with no retail counterpart at the same semantics. Confirmed + for `EnchantmentMask`/`EnchantmentBucket`: they are PlayerDescription wire + *trailer bucket* flags (ACE's `EnchantmentMask`), not retail's + `EnchantmentTypeEnum` — the apparent value conflict is a false pairing. The + others were not individually run down. +12. **Tooling limitation, for whoever re-runs this.** The extractor reads one line + per enum member, so a member whose value spans multiple lines is truncated. + This produced a false `AttackType.MultiStrike` conflict (read as `0x1E0`, + actually `0x79E0`). Any automated re-run must join continuation lines before + trusting a reported conflict. + +--- + +## 5. Reproducing + +Extraction and diff scripts were scratch tooling, not committed (they hardcode +absolute paths into `references/`). The method, in order: + +1. Parse `acclient.h` for `enum NAME {` blocks; resolve member expressions + (hex, digit separators, references to earlier members). +2. Parse the UtilityBelt catalog the same way. +3. Walk `references/weenies/**/*.json`, pulling `(key, value, _comment)` from each + `*Stats` array; the comment's left side is the property name, the right side + (after `=`) names the value member for enum-typed ints. +4. Normalise names across conventions (retail `SCREAMING_SNAKE` with family + prefix/suffix ↔ acdream `PascalCase`) and diff on the normalised key, comparing + values exactly. +5. Any disagreement: retail decides, corpus breaks ties, and anything still + unresolved becomes an open question above rather than a guess. + +## 6. Commits + +| commit | slice | +|---|---| +| `251dd68a` | the seven `Property*` tables (864 members, 429 tests) | +| `f3e95a3e` | `DamageType` rotation + `ItemType` craft ladder and composites | +| `8ccaf72a` | `EquipMask` composites, `TransientState`, `PhysicsState`, `AttackHeight` | +| `3efa266a` | `AmmoType`, `CombatUse`, `ItemUseable` |