acdream/docs/research/2026-07-29-enum-verification-campaign.md
Erik f27ad9ee43 feat(core): adopt retail's full WeenieError code table
acdream carried 16 status codes, curated by hand out of the CMotionInterp
and MoveToManager decompilation passes. The other 362 were unnamed, which
made every one of them a cast site waiting to happen. This slice takes the
whole table: 372 values under 378 names.

The oracle set is finally complete. All six vendored reference repos were
empty when the 2026-07-29 enum campaign ran, which is why it deferred this
decision; they are re-cloned now, so ACE's WeenieError could be read
directly instead of leaning on the UtilityBelt catalog alone.

The two agree without a single conflict. ACE has 369 members, no internal
value collisions. The catalog has 372, shares all 369 ACE names, and
disagrees on none of their values. Its three extras -- IsNowOpenFellowship
(0x050B), IsNowClosedFellowship (0x050C), LockedFellowshipCannotRecruit
(0x0518) -- each turn up in ACE's separate WeenieErrorWithString enum with
a `_` marking the interpolated name, so the catalog is just the less-split
view of the same client enum. All three are adopted on agreement between
two oracles, not on one.

Retail cannot arbitrate any of this. acclient.h has no counterpart enum;
its charError (26) is character-creation only. Recorded, not guessed
around.

Six values keep two names. acdream's NotGrounded, CrouchInCombatStance,
SitInCombatStance, SleepInCombatStance, ChatEmoteOutsideNonCombat and
ActionDepthExceeded are each anchored to a retail decompilation site, where
ACE's names for those values are server-side coinages. Rather than pick,
both are declared, acdream's first so ToString() is untouched.

Behaviour is unchanged, and there is no way for it not to be: nothing in
the tree branches on a WeenieError member. MotionInterpreter's switch is on
a motion type and merely returns one of these; WeenieErrorText.For switches
on a raw uint; the chat translation table WeenieErrorMessages is keyed on
uint throughout, so naming a code does not make it render. The one site
that moved is RemoteTeleportHook, where the (WeenieError)0x3Cu cast becomes
the now-named WeenieError.ITeleported at the same value.

Register row AP-15 is narrowed rather than retired. Its code-catalog caveat
is superseded -- an unnamed code is no longer a way for it to bite -- but
the sentences are still ACE's doc comments rather than retail's
string_table.bin, and that part stands.

The enum moved out of MotionInterpreter.cs into its own file at the same
namespace. At 372 members it does not belong inside a physics class file.

Core tests 3903 passed / 2 skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 07:33:27 +02:00

19 KiB

Enum verification campaign — 2026-07-29

Bucket B item 1 of docs/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 uints 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 closedWaterContact, CheckEthereal adopted
PhysicsStateFlags PhysicsState closedReservedUnused1/2 adopted
ObjectInfoState ObjectInfoEnum closed — was already exact (None = DEFAULT_OI)
AttackHeight ATTACK_HEIGHT closedUndef adopted; NUM_ATTACK_HEIGHTS is a count, deliberately not a member
HoldKey HoldKey closedNum_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 RESOLVED 2026-07-29. All six were re-cloned from their upstreams into the main checkout's references/ (still gitignored). See §7. The 456 single-sourced property members (§3.1) can now be promoted to two-oracle confirmed by a follow-up pass; that pass has not been run.
  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. RESOLVED 2026-07-29 by user decision: all 362 adopted. See §8.1. What remains open is only the sentences — retail's string_table.bin — which stays as register row AP-15, now narrowed to the translation table alone.
  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

7. The reference tree, restored (2026-07-29)

Open question 2 is closed. All six vendored repos were re-cloned from their upstreams into the main checkout at C:\Users\erikn\source\repos\acdream\references\, which every worktree shares. references/* is gitignored (only WorldBuilder is checked in as a gitlink), so none of this enters our history.

Each candidate URL was cross-checked against the description in CLAUDE.md and memory/reference_repos.md before cloning, and the clone was verified to contain the files those descriptions name.

repo upstream commit size how the URL was confirmed
ACE https://github.com/ACEmulator/ACE.git 65f092dd 56 MB git remote -v of the recovered copy at repos/client/ACE; the three enum files this slice reads are byte-identical between the two
ACViewer https://github.com/ACEmulator/ACViewer.git c5c54fb3 97 MB cited by URL in docs/research/2026-07-13-retail-projectile-vfx-pseudocode.md; also the remote of repos/ACViewer. Cloned --recurse-submodules, so its ACE.DatLoader submodule (382edb23) is present as the docs describe
Chorizite.ACProtocol https://github.com/Chorizite/Chorizite.ACProtocol ff7dffd6 4.5 MB Chorizite org + exact repo name; contains the Chorizite.ACProtocol + .SourceGen projects CLAUDE.md describes as XML-generated
holtburger https://github.com/merklejerk/holtburger a7e806cb 25 MB named by URL in docs/research/2026-05-10-holtburger-network-stack-study.md; also the remote of repos/holtburger
AC2D https://github.com/deregtd/AC2D 3451f0d2 17 MB no URL was recorded anywhere in the repo, so the candidate was verified by content: it is C++, and it holds cNetwork.cpp, cInterface.cpp, cPictureBox.h/cStaticText.h/cEditBox.h, and FSplitNESW in Landblocks.cpp — every file the docs attribute to AC2D
DatReaderWriter https://github.com/Chorizite/DatReaderWriter c5359870 4.2 MB cited by URL in docs/research/2026-04-26-datreaderwriter-reference.md. Not on the restore list, but it was empty for the same reason and is unambiguously identified

8. Adoptions made on the restored oracle set

8.1 WeenieError — the full 372-code table

Open question 4, closed by user decision. acdream carried 16 members; it now carries 372 values under 378 names.

The oracles agree completely. ACE's WeenieError has 369 members with no internal value collisions; the UtilityBelt catalog's StatusMessage has 372, shares all 369 ACE names, and disagrees on none of their values — it is a strict superset. Its three extra codes are IsNowOpenFellowship (0x050B), IsNowClosedFellowship (0x050C) and LockedFellowshipCannotRecruit (0x0518), and each is independently present in ACE's separate WeenieErrorWithString enum carrying a _ where the interpolated name goes. So the catalog is simply the less-split view of what ACE keeps as two enums, and the three are adopted on two-oracle agreement rather than on the catalog alone.

Retail cannot arbitrate any of this: acclient.h has no counterpart enum, and its charError (26 members) is character-creation only. That is recorded, not guessed around.

Ten of acdream's 16 members matched an ACE name at an identical value. The other six sit at values ACE names differently, and acdream's names are the better ones — each is anchored to a retail decompilation site from the A10/R4 passes, where ACE's is a server-side coinage. Rather than choose, the six values carry both names, acdream's declared first:

value acdream (decomp-anchored) ACE / catalog
0x0024 NotGrounded YouCantJumpWhileInTheAir
0x003F CrouchInCombatStance CantCrouchInCombat
0x0040 SitInCombatStance CantSitInCombat
0x0041 SleepInCombatStance CantLieDownInCombat
0x0042 ChatEmoteOutsideNonCombat CantChatEmoteInCombat
0x0045 ActionDepthExceeded TooManyActions

Behaviour: unchanged. Nothing in the tree branches on a WeenieError member. The two switches that mention the type switch on something else — MotionInterpreter.cs:971 switches on a motion type and returns a WeenieError, and WeenieErrorText.For switches on a raw uint. The chat translation table WeenieErrorMessages is keyed on uint throughout, so naming a code does not make it render. The only site touched is RemoteTeleportHook, whose (WeenieError)0x3Cu cast could become the now-named WeenieError.ITeleported — same value, no behavioural edge.

The enum also moved out of MotionInterpreter.cs into its own file at the same namespace; at 372 members it no longer belongs inside a physics class file.