acdream/docs/research/2026-07-29-enum-verification-campaign.md
Erik 39c1737bda feat(core): adopt retail's SoundType catalog; retire AC2D
SoundId was not a subset of retail's table, the way its comment claimed.
It was an invention: 23 acdream-local names on acdream-local values, and
the values were wrong in the way that matters. FootstepDefault = 0x02 is
retail's Random. SwingSword = 0x10 is retail's Death2. Death = 0x60 is
retail's Explode. Anyone who reached for one of those names to compare
against a wire or dat value would have got a different sound.

Nothing referenced any of them by name -- grep for `SoundId.` across src
and tests returns nothing -- so this was a trap rather than a live defect,
the same shape the enum campaign found in DamageType. All 22 invented names
are deleted and retail's 205 replace them.

Three oracles agree exactly, on every name and every value: retail
acclient.h:4569 enum SoundType, ACE's Sound, and DatReaderWriter's Sound.
The third matters most. AudioHookSink already resolves SoundTable lookups
through DatReaderWriter.Enums.Sound, so that is the enum acdream actually
reads at runtime; our catalog now agrees with the values already flowing
through the dat path, and a conformance test pins the two so they cannot
drift apart.

On the "206 sounds" figure: retail's block holds 207 entries, being 205
sounds followed by NUM_SOUND_TYPES = 0xCD and FORCE_SoundType_32_BIT. The
first is a count and the second a width pin. Counting the former is where
206 came from. Neither is a member here, matching how the campaign treated
NUM_ATTACK_HEIGHTS and Num_HoldKeys -- a count is not a value the wire can
carry.

Behaviour is unchanged and could not be otherwise: the enum had no
consumers. IAudioEngine's three SoundId overloads are no-op stubs and the
live path takes wave ids and DatReaderWriter values.

The user's separate report that sound is "not working that good" is a
triggering, selection and attenuation question rather than a catalog one,
and is filed as its own Bucket B row in the post-Vulkan intake.

Also in this commit, by user decision: AC2D is retired as a reference. Its
clone and directory are gone and it must not be re-cloned. Everything we
took from it still stands and is written down -- the FSplitNESW terrain
split constants, the 0xF61C movement packet layout, the finding that a
client need not compute terrain Z itself -- so CLAUDE.md's reference list,
its hierarchy table, and the architecture doc's protocol row now point at
docs/research/2026-04-12-movement-deep-dive.md rather than erasing the
history. The reference count drops from six to five.

Core tests 3907 passed / 2 skipped.

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

387 lines
22 KiB
Markdown

# 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:
> This paragraph describes the environment the campaign *ran in*, later the same
> day. It is no longer current: §7 records the restore, and AC2D was retired, so
> CLAUDE.md now names five vendored repos rather than six.
| # | 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**~~ **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**~~ **RESOLVED 2026-07-29**
by user decision: retail's table adopted wholesale. See §8.2.
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. The 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.
**AC2D was cloned, then dropped by user decision the same day and deleted along
with its directory.** It is a retired reference: do not re-clone it. What we took
from it — the `FSplitNESW` terrain split constants, the `0xF61C` movement packet
layout, and the finding that a client need not compute terrain Z itself — is
already written down in `docs/research/2026-04-12-movement-deep-dive.md` and the
`docs/research/retail-ui/` dat-id work, and those citations stand. CLAUDE.md's
reference list and hierarchy table, and the architecture doc's protocol row, were
updated to say so rather than to erase the 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`~~ | — | **Retired 2026-07-29, deleted.** No URL was recorded anywhere in the repo, so the candidate had been verified by content: C++, holding `cNetwork.cpp`, `cInterface.cpp`, `cPictureBox.h`/`cStaticText.h`/`cEditBox.h`, and `FSplitNESW` in `Landblocks.cpp` — every file the docs attributed to AC2D. Identification was sound; the reference is simply no longer kept |
| 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.
### 8.2 `SoundId` — retail's full `SoundType` catalog
Open question 7, closed by user decision. This one was not a subset; it was an
invention. The 23 members were acdream-local names on acdream-local values, and
the values were wrong in the way that matters: the old `FootstepDefault = 0x02`
is retail's `Random`, `SwingSword = 0x10` is retail's `Death2`, `Death = 0x60` is
retail's `Explode`. The header comment called it "a sparse subset of retail
enums", which it never was.
Nothing referenced any of the 23 by name — `grep` for `SoundId.` across `src/`
and `tests/` returns nothing — so this was a trap rather than a live defect,
exactly the shape the campaign found in `DamageType` (§2.1). All 22 invented
names are deleted; retail's 205 replace them.
Three oracles agree exactly, on every name and every value:
| oracle | members | agreement |
|---|---|---|
| retail `acclient.h:4569 enum SoundType` | 205 named sounds | decides |
| ACE `ACE.Entity.Enum.Sound` | 205 | identical names and values to retail |
| DatReaderWriter `DatReaderWriter.Enums.Sound` | 205 | identical names and values to retail |
The third is the interesting one. `AudioHookSink` already resolves SoundTable
lookups through `DatReaderWriter.Enums.Sound`, so that enum is what acdream
actually reads at runtime — the adoption makes our own catalog agree with the
values already flowing through the dat path, and a conformance test pins the two
together so they cannot drift.
**On "206".** Retail's block holds 207 entries: 205 sounds, then
`NUM_SOUND_TYPES = 0xCD` and `FORCE_SoundType_32_BIT`. The first is a count and
the second a width pin. Counting `NUM_SOUND_TYPES` is where the 206 figure in
open question 7 came from. Neither is a member here, matching how §3.2 treated
`NUM_ATTACK_HEIGHTS` and `Num_HoldKeys` — a count is not a value the wire can
carry.
**Behaviour: unchanged**, for the same reason as §8.1 and more strongly — the
enum had no consumers at all. `IAudioEngine`'s three `SoundId` overloads
(`PlayUi`, `Play3D`, `StartAmbient`) are no-op stubs; the live audio path takes
wave ids and `DatReaderWriter.Enums.Sound` values, not this type.
The enum moved out of `AudioModel.cs` into its own file at the same namespace.
**Follow-up filed.** The user reports sound "not working that good" generally.
That is a triggering/selection/attenuation question, not a catalog question, and
is now a Bucket B row in
[`docs/plans/2026-07-29-post-vulkan-work-intake.md`](../plans/2026-07-29-post-vulkan-work-intake.md).