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