diff --git a/docs/research/2026-07-29-wire-stack-audit.md b/docs/research/2026-07-29-wire-stack-audit.md new file mode 100644 index 00000000..7bae43f9 --- /dev/null +++ b/docs/research/2026-07-29-wire-stack-audit.md @@ -0,0 +1,1251 @@ +# Wire-stack audit — 2026-07-29 + +**Bucket B item 2** of [`docs/plans/2026-07-29-post-vulkan-work-intake.md`](../plans/2026-07-29-post-vulkan-work-intake.md): +"Verify networking stack — audit parsed-vs-stub-vs-missing, prioritize by what +live servers actually send, holtburger remains the client-side oracle." + +**Base commit:** `b70b9832` (`docs: capture the post-Campaign-V work intake, sorted`), +the newest reachable commit of `claude/git-sync-status-5fb1d2`. +**Baseline suite at base:** 9,162 passed / 5 skipped / 0 failed +(`AcDream.Core.Net.Tests` = 600). + +This audit is **report-first**. The ledger is the deliverable; parser work is +bonus and was held to a deliberately high bar (below). + +--- + +## 0. Two blockers found before any auditing could start + +Both are process findings that outlast this audit, so they lead. + +### 0.1 The vendored reference repos are empty + +`references/ACE/`, `references/holtburger/`, and +`references/Chorizite.ACProtocol/` are **empty directories** — not just inside +agent worktrees, but in the main checkout at +`C:\Users\erikn\source\repos\acdream\references\`. Only +`references/WorldBuilder` is a real submodule (`.gitmodules` declares exactly +one). Every CLAUDE.md instruction of the form "cross-reference at least two of +them" is currently unexecutable as written. + +Working copies were recovered elsewhere on disk and used for this audit: + +| Oracle | Recovered path | Extent | +|---|---|---| +| ACE | `C:\Users\erikn\source\repos\client\ACE` | 1,771 `.cs` — complete | +| holtburger | `C:\Users\erikn\source\repos\holtburger` | 124 `.rs` — complete | +| Chorizite.ACProtocol | **none found anywhere on disk** | absent | + +Three further identical ACE copies exist under `%TEMP%` +(`ACE-ref`, `ACE-k4-source`, `acdream-ace-reference`) and one under +`C:\Users\erikn\source\repos\holtburger\ACE`. The duplication is itself a +symptom: previous sessions have each re-solved this problem privately. + +**Chorizite.ACProtocol was unavailable for this audit.** Where CLAUDE.md's +hierarchy names it as a cross-check for protocol field order, this audit used +ACE plus holtburger, or ACE plus `docs/research/named-retail/`, instead. + +### 0.2 The 2026-06-04 wire catalog does not exist + +`docs/research/2026-06-04-wire-message-catalog.md` — cited by the intake doc as +"256 opcodes catalogued, 114 parsed", and indexed in `MEMORY.md` — **was never +committed and is not on disk.** + +- `git log --all -- "docs/research/2026-06-04-wire-message-catalog.md"` → empty +- `git log --all --diff-filter=A -- "*wire-message-catalog*"` → empty +- filesystem sweep of `C:\Users\erikn` for `*wire-message-catalog*` → no hits +- the `research/` subdirectory that `MEMORY.md` links into + (`C:\Users\erikn\.claude\projects\C--Users-erikn-source-repos-acdream\memory\research\`) + does not exist + +It was produced by the `acdream-wire-message-catalog` skill into the user-level +memory tree and lost when that tree was pruned. `MEMORY.md` still carries the +dangling link, as do the sibling links for the 2026-06-04 combat-math and +property-enum research drops — those should be checked too. + +**Consequence for this audit.** "Diff against the catalog's status column" is +not possible. Two substitutions were made: + +1. **The row universe** comes from ACE's three opcode enums rather than the + catalog's 256. This is *more* complete, not less — see §1. +2. **The drift comparison** is against + [`docs/research/2026-05-10-phase-m-opcode-matrix.md`](2026-05-10-phase-m-opcode-matrix.md), + which is in-repo, covers ~284 rows, and is the closest surviving prior + status artifact. It is 11 weeks old rather than 7, and its own header warns + that a "spot-check pass + intentional-divergence ratification" was still + owed, so treat drift-vs-matrix as indicative rather than authoritative. + +--- + +## 1. Row universe + +ACE enumerates the protocol in three enums. Together they are the audit's +universe: + +| Family | Enum | Rows | +|---|---|---| +| Top-level GameMessage | `Network/GameMessages/GameMessageOpcode.cs` | 82 | +| GameEvent (inside `0xF7B0`) | `Network/GameEvent/GameEventType.cs` | 104 | +| GameAction (inside `0xF7B1`) | `Network/GameAction/GameActionType.cs` | 163 | +| **Total** | | **349** | + +acdream's own `src/AcDream.Core.Net/Messages/GameEventType.cs` carries 103 +entries against ACE's 104. + +### Where acdream's handling lives + +- **Top-level:** one `if / else if` chain in + `src/AcDream.Core.Net/WorldSession.cs`, in the fragment loop that begins at + line 1215. Each arm is a recognized opcode. The final arm + (`else if (DumpOpcodesEnabled)`, line 1572) is the unhandled fallback and + only emits under `ACDREAM_DUMP_OPCODES=1`, deduplicated to the first + occurrence per opcode. +- **GameEvent:** `Messages/GameEventDispatcher.cs` routes by sub-opcode and + counts misses in an `_unhandledCounts` bag. +- **GameAction:** outbound builders across `Messages/*.cs`, sent through + `WorldSession.SendGameAction`. Live callers live outside `Core.Net`. + +--- + +## 2. Corpus reality check + +The mission was to rank gaps by observed frequency. **The corpus cannot +support that ranking**, and the reason is worth recording. + +Logs examined (`%TEMP%\claude\`): `coldeve-vk.log` (3,226 lines, live +production Coldeve server), `coldeve-vk.log.err`, `user-vk-session.log`, +`v4d-connected.log`, `v4d-connected2.log`, `prerevert-launch.log`, +`prerevert-launch2.log`, `verify3.log`, and the smaller Vulkan scene logs. + +**Zero unhandled-opcode lines across the entire corpus.** The only diagnostic +that would have produced them, `ACDREAM_DUMP_OPCODES=1`, was not set for any +of those sessions. (`0xF682` appears 15 times in `coldeve-vk.log` and is a red +herring — it is the high half of cell id `0xF6820033` in `[world-reveal]` +lines, not an opcode.) + +So criterion (a) of the implementation bar — "observed in the corpus" — is +satisfiable for essentially nothing in the MISSING/STUB set. Per the mission's +own rule, that means **no new parsers were written for unobserved gaps**, and +the ledger is the deliverable. + +### What the corpus does prove + +It is positive evidence of which paths a live production server exercised: + +| Observation | Count | Wire path implicated | +|---|---|---| +| `teleport queued` / `presentation started` / `arrival` | 14 each | `PlayerTeleport 0xF751` | +| `teleport materialized - snapped to ...` | 14 | `0xF751` + position follow-up | +| `SetTurbineChatChannels parsed` | 4 (Coldeve) + 12 (local) | `0xF7DE` family | +| `equipment: attached child=... parent=...` | 118 | `ParentEvent 0xF749` | +| `in world - CreateObject stream active` | 1 per session | `CreateObject 0xF745` | +| `entering world as 0x... Barris` | 1 | `CharacterList 0xF658` + EnterWorld | +| `combat: melee attack` / `missile attack` | 3 | outbound attack actions | + +Notably the Coldeve session logged in as `Barris` from a **multi-character +account**, which the intake doc flags as evidence that the character-select +path is half-built already. + +### The cheap fix + +Re-running one connected session with `ACDREAM_DUMP_OPCODES=1` would convert +this section from "cannot rank" to a real frequency table, at the cost of one +environment variable. That is the single highest-value follow-up from this +audit and is listed first in §7. It needs a connected client, so it is +user-gated and was out of scope here. + +--- + +## 3. The ledger + +Every row below is grounded in acdream source read during this audit, with a +`file:line` citation, and compared field-by-field against ACE's serializer or +handler for that opcode. The prior status columns in the Phase-M matrix were +**not** used as input — they are compared against the result in section 4. + +**Verification depth.** The three family tables were produced by parallel audit +agents working to one contract. Five findings were then re-verified by hand +against primary source before publication: the `HearRangedSpeech` range field, +`CommunicationTransientString`'s phantom chat type, the `xpSpent` width, the +`SetCharacterOptions` payload, and `CharacterError`'s absence. All five held. +The remaining rows carry their evidence inline; treat them as +audited-with-citation rather than hand-re-verified, and spot-check before +acting on any single row — the same caveat the Phase-M matrix set for itself. + +**Status vocabulary.** For inbound families: `PARSED` (recognized, every field +ACE serializes is decoded), `PARTIAL` (decoded but fields skipped or assumed), +`STUB` (recognized but body dropped / no state effect), `MISSING` (not +recognized). For the outbound GameAction family the axis is different: +`BUILT+WIRED`, `BUILT+PARTIAL`, `BUILDER-ONLY` (dead builder, no production +caller), `MISSING`, `N/A (server-side)`. + +> **The tables below describe the state at base commit `b70b9832`.** This +> audit then fixed three of those rows itself — `0x02BC HearRangedSpeech` +> (was PARTIAL), `0x02EB CommunicationTransientString` (was PARTIAL), and +> the `RaiseAttribute`/`RaiseVital`/`RaiseSkill` trio (were BUILT+PARTIAL). +> Those four rows read PARSED / BUILT+WIRED as of `f57db35c`. Every other +> row is unchanged. See sections 5 and 6. + +### Roll-up across all three families + +| Family | Universe | PARSED / BUILT+WIRED | PARTIAL | STUB / BUILDER-ONLY | MISSING | N/A | +|---|---|---|---|---|---|---| +| Top-level GameMessage | 82 | 36 | 2 | 2 | 42 | — | +| GameEvent (0xF7B0) | 104 | 38 | 3 | 63 | 0 | — | +| GameAction (0xF7B1) | 163 | 56 | 6 | 10 | 86 | 5 | +| **Total** | **349** | **130** | **11** | **75** | **128** | **5** | + +Two adjustments matter before reading those numbers as a grade: + +- **Dead enum entries.** 12 top-level opcodes and 13 GameEvent sub-opcodes are + in ACE's enum but ACE never puts them on the wire. Excluding them, the + top-level family is 36 PARSED / 2 PARTIAL / 2 STUB / 30 MISSING out of 70 + live opcodes, and the real GameEvent gap is about 49 rows rather than 63. +- **GameEvent MISSING is zero, and that is not as good as it sounds.** Every + ACE sub-opcode value exists in acdream's enum, so nothing is literally + unrecognized — but 62 of them have no registered handler and land in the + dispatcher's `_unhandledCounts` bag. STUB is the honest status for those. + +The concentration is worth naming: the GameEvent STUB block is almost entirely +**fellowship, housing, trade, allegiance, chess, books, and vendor** — which is +precisely the surface the intake doc's bucket-C items (fellowship/allegiance +tabs, vendor management) will need. + +### 3.1 Top-level GameMessage opcodes (82 rows) + +Row universe: all 82 entries of ACE `Source/ACE.Server/Network/GameMessages/GameMessageOpcode.cs` +(includes the two intentional duplicate names `CharacterCreateResponse`/`CharacterRestoreResponse` +at `0xF643` and `MovementEvent`/`Motion` at `0xF74C`). + +acdream paths are repo-relative. +The dispatch chain is the `foreach (BorrowedMessageFragment frag ...)` loop in +`src\AcDream.Core.Net\WorldSession.cs:1215`; the unhandled fallback is +`WorldSession.cs:1572` (`else if (DumpOpcodesEnabled)`). + +ACE serializer column names the file under +`C:\Users\erikn\source\repos\client\ACE\Source\ACE.Server\Network\GameMessages\Messages\` +unless stated otherwise. + +| Opcode | ACE name | Dir | acdream status | acdream site (file:line) | ACE serializer | Field-coverage notes | +|---|---|---|---|---|---|---| +| 0x0000 | None | — | MISSING | — | none | Sentinel only. `GameMessage` base skips writing the opcode word when `Opcode == None`, so it never appears on the wire. No coverage needed. | +| 0x0024 | InventoryRemoveObject | S→C | PARSED | dispatch `src\AcDream.Core.Net\WorldSession.cs:1484`; parser `src\AcDream.Core.Net\Messages\InventoryRemoveObject.cs:276` | GameMessageInventoryRemoveObject.cs | ACE writes only `u32 guid` (size 8). acdream reads guid and raises `InventoryObjectRemoved`. Wired to `ClientObjectTable` at `src\AcDream.Core.Net\ObjectTableWiring.cs:87`. Complete. | +| 0x0197 | SetStackSize | S→C | PARSED | dispatch `WorldSession.cs:1477`; parser `Messages\SetStackSize.cs:241` | GameMessageSetStackSize.cs | ACE order: `byte seq (ByteSequence)`, `u32 guid`, `i32 stackSize`, `i32 value`. acdream reads all four; the sequence byte is deliberately not honored (latest-wins, cited as divergence DR-4 at `SetStackSize.cs:138`/`:246`). Sequence widths confirmed via `Network\Sequence\SequenceManager.cs:179` (default = `ByteSequence`). | +| 0x019E | PlayerKilled | S→C | PARSED | dispatch `WorldSession.cs:1414`; parser `Messages\PlayerKilled.cs:341` | GameMessagePlayerKilled.cs | ACE: `String16L deathMessage`, `u32 victimGuid`, `u32 killerGuid`. acdream reads all three in order. Complete. | +| 0x01E0 | EmoteText | S→C | PARSED | dispatch `WorldSession.cs:1388`; parser `Messages\EmoteText.cs:126` | GameMessageEmoteText.cs | ACE: `u32 senderId`, `String16L senderName`, `String16L emoteText`. acdream matches exactly. Complete. | +| 0x01E2 | SoulEmote | S→C | PARSED | dispatch `WorldSession.cs:1397`; parser `Messages\SoulEmote.cs:177` | GameMessageSoulEmote.cs | Same layout as 0x01E0; acdream matches. Complete. | +| 0x02BB | HearSpeech | S→C | PARSED | dispatch `WorldSession.cs:1379`; parser `Messages\HearSpeech.cs:49` | GameMessageHearSpeech.cs | ACE: `String16L text`, `String16L senderName`, `u32 senderId`, `u32 chatType`. acdream matches. Complete for the *local* variant only. | +| 0x02BC | HearRangedSpeech | S→C | **PARTIAL** | dispatch `WorldSession.cs:1379`; parser `Messages\HearSpeech.cs:49` | GameMessageHearRangedSpeech.cs | **Missing field: `f32 range`.** ACE writes `text, senderName, senderId, range(f32), chatType`; acdream's shared parser reads `chatType` from offset `senderId+4`, i.e. from the `range` float's bits. See Finding 1. | +| 0x02CD | PrivateUpdatePropertyInt | S→C | PARSED | dispatch `WorldSession.cs:1461`; parser `Messages\PrivateUpdatePropertyInt.cs:133` | GameMessagePrivateUpdatePropertyInt.cs | ACE: `byte seq`, `u32 property`, `i32 value`. acdream reads property + value; sequence deliberately skipped with cited reason (DR-4, `PrivateUpdatePropertyInt.cs:121`). 92 ACE construction sites — the highest-volume property message. | +| 0x02CE | PublicUpdatePropertyInt | S→C | PARSED | dispatch `WorldSession.cs:1454`; parser `Messages\PublicUpdatePropertyInt.cs:176` | GameMessagePublicUpdatePropertyInt.cs | ACE: `byte seq`, `u32 guid`, `u32 property`, `i32 value`. acdream reads guid/property/value; sequence documented-skipped (DR-4). | +| 0x02CF | PrivateUpdatePropertyInt64 | S→C | PARSED | dispatch `WorldSession.cs:1468`; parser `Messages\PrivateUpdatePropertyInt64.cs:212` | GameMessagePrivateUpdatePropertyInt64.cs | ACE: `byte seq`, `u32 property`, `i64 value`. acdream reads property at +5 and `i64` at +9. Retail anchor cited (`CM_Qualities::DispatchUI_PrivateUpdateInt64 @ 0x006AEAD0`). | +| 0x02D0 | PublicUpdatePropertyInt64 | S→C | MISSING | — (falls to `WorldSession.cs:1572`) | GameMessagePublicUpdatePropertyInt64.cs | ACE constructs it twice (Enlightenment / Player_Properties). Layout `byte seq, u32 guid, u32 property, i64 value`. Not recognized anywhere in `src\`. | +| 0x02D1 | PrivateUpdatePropertyBool | S→C | MISSING | — | GameMessagePrivateUpdatePropertyBool.cs | 7 ACE construction sites. Layout `byte seq, u32 property, u32 value`. Not recognized. Local-player bool qualities never update live after the 0xF7B0/0x0013 PlayerDescription dump. | +| 0x02D2 | PublicUpdatePropertyBool | S→C | MISSING | — | GameMessagePublicUpdatePropertyBool.cs | 9 ACE construction sites incl. `WorldObjects\Door.cs:220` (`PropertyBool.Locked`), `Lock.cs:275/310`, `Hook.cs:259` (`UiHidden`). Layout `byte seq, u32 guid, u32 property, u32 value`. See Finding 4. | +| 0x02D3 | PrivateUpdatePropertyFloat | S→C | MISSING | — | GameMessagePrivateUpdatePropertyFloat.cs | Class exists but ACE has **zero** construction sites — never sent in normal play. Layout `byte seq, u32 property, f64 value`. | +| 0x02D4 | PublicUpdatePropertyFloat | S→C | MISSING | — | GameMessagePublicUpdatePropertyFloat.cs | 2 ACE sites (`EmoteManager.cs:1159`, `Player_Properties.cs:1153`). Layout `byte seq, u32 guid, u32 property, f64 value`. Not recognized. | +| 0x02D5 | PrivateUpdatePropertyString | S→C | MISSING | — | GameMessagePrivateUpdatePropertyString.cs | Class exists but ACE has zero construction sites — never sent. Layout `byte seq, u32 property, align, String16L`. | +| 0x02D6 | PublicUpdatePropertyString | S→C | MISSING | — | GameMessagePublicUpdatePropertyString.cs | 2 ACE sites (`Player_Properties.cs:1200`, `SlumLord.cs:230` — renames a house sign). Note ACE's field order here is `seq, property, guid, align, String16L`, i.e. property BEFORE guid, unlike every other `Public*` message. See Finding 6. | +| 0x02D7 | PrivateUpdatePropertyDataID | S→C | MISSING | — | GameMessagePrivateUpdateDataID.cs | 4 ACE sites. Layout `byte seq, u32 property, u32 value`. Not recognized. | +| 0x02D8 | PublicUpdatePropertyDataID | S→C | MISSING | — | GameMessagePublicUpdateDataID.cs (class `GameMessagePublicUpdatePropertyDataID`) | 1 ACE site. Layout `byte seq, u32 guid, u32 property, u32 value`. Not recognized. | +| 0x02D9 | PrivateUpdatePropertyInstanceID | S→C | MISSING | — | GameMessagePrivateUpdateInstanceID.cs | 1 ACE site. Layout `byte seq, u32 property, u32 value`. Not recognized. | +| 0x02DA | PublicUpdateInstanceId | S→C | MISSING | — | GameMessagePublicUpdateInstanceID.cs | **12 ACE construction sites** — the live container/wielder re-parent broadcast. Layout `byte seq, u32 guid, u32 property, u32 instanceGuid`. Not recognized. See Finding 5. | +| 0x02DB | PrivateUpdatePosition | S→C | MISSING | — | GameMessagePrivateUpdatePosition.cs | 1 ACE site. Layout `byte seq, u32 positionType, Position.Serialize`. Carries lifestone/marketplace/last-outside anchors. Not recognized. | +| 0x02DC | PublicUpdatePosition | S→C | MISSING | — | GameMessagePublicUpdatePosition.cs | Class exists; **zero** ACE construction sites — never sent in normal play. | +| 0x02DD | PrivateUpdateSkill | S→C | MISSING | — | GameMessagePrivateUpdateSkill.cs | **10 ACE construction sites** (`Player_Skills.cs`, `SkillAlterationDevice.cs`). Layout `byte seq, u32 skill, u32 ranks, u16 adjustPP, u32 advancementClass, u32 xpSpent, u32 initLevel, u32 resistanceAtLastCheck, f64 lastUsedTime`. Not recognized — skills never refresh live. | +| 0x02DE | PublicUpdateSkill | S→C | MISSING | — | none | No ACE serializer class and no handler — enum entry only; ACE never sends it. | +| 0x02DF | PrivateUpdateSkillLevel | S→C | MISSING | — | none | No ACE serializer class — enum entry only; never sent. | +| 0x02E0 | PublicUpdateSkillLevel | S→C | MISSING | — | none | No ACE serializer class — enum entry only; never sent. | +| 0x02E3 | PrivateUpdateAttribute | S→C | MISSING | — | GameMessagePrivateUpdateAttribute.cs | **8 ACE construction sites** (`Player_Attributes.cs`, `AttributeTransferDevice.cs`, `Enlightenment.cs`). Layout `byte seq, u32 attribute, u32 ranks, u32 startingValue, u32 xpSpent`. Not recognized — attributes never refresh live. | +| 0x02E4 | PublicUpdateAttribute | S→C | MISSING | — | none | No ACE serializer class — enum entry only; never sent. | +| 0x02E7 | PrivateUpdateVital | S→C | PARSED | dispatch `WorldSession.cs:1433`; parser `Messages\PrivateUpdateVital.cs:71` | GameMessagePrivateUpdateVital.cs | ACE: `byte seq, u32 vital, u32 ranks, u32 startingValue, u32 xpSpent, u32 current`. acdream reads all six including the sequence byte. Complete. | +| 0x02E8 | PublicUpdateVital | S→C | MISSING | — | GameMessagePublicUpdateVital.cs | Class exists; **zero** ACE construction sites — never sent in normal play. Layout would be `byte seq, u32 guid, u32 attribute, u32 ranks, u32 base, u32 totalInvestment, u32 current`. | +| 0x02E9 | PrivateUpdateAttribute2ndLevel | S→C | PARSED | dispatch `WorldSession.cs:1444`; parser `Messages\PrivateUpdateVital.cs:92` | GameMessagePrivateUpdateAttribute2ndLevel.cs | ACE: `byte seq, u32 vital, u32 current`. acdream reads all three. 11 ACE construction sites (regen/drain ticks). Complete. | +| 0xEA60 | AdminEnvirons | S→C | PARSED | dispatch (inline) `WorldSession.cs:1499-1510` | GameMessageAdminEnvirons.cs | ACE writes exactly one field, `u32 environChangeType` (size hint 8). acdream reads it at offset 4 and raises `EnvironChanged`. Complete. | +| 0xF619 | PositionAndMovement | — | MISSING | — | none | No ACE serializer, no ACE handler; only appears in the enum and in a commented-out field at `Physics\Common\SmartBox.cs:35`. Never on the wire. | +| 0xF625 | ObjDescEvent | S→C | PARSED | dispatch `WorldSession.cs:1542`; parser `Messages\ObjDescEvent.cs:51` | GameMessageObjDescEvent.cs → `WorldObjects\WorldObject_Networking.cs:48` `SerializeUpdateModelData` | ACE: `u32 guid`, ModelData block, `u16 instanceSeq`, `u16 visualDescSeq`. acdream reads all of it via the shared `CreateObject.ReadModelData`, and additionally hard-rejects a non-4-byte tail (`ObjDescEvent.cs:70`) so cursor errors cannot produce a silently-wrong appearance. Complete. | +| 0xF643 | CharacterCreateResponse | S→C | MISSING | — | GameMessageCharacterCreateResponse.cs | 1 ACE site. Layout `u32 response` + (when Ok) `u32 guid, String16L name, u32 0`. acdream never creates characters, so this never arrives in normal acdream play, but it is unrecognized. | +| 0xF643 | CharacterRestoreResponse | S→C | MISSING | — | GameMessageCharacterRestore.cs | Duplicate opcode value with CharacterCreateResponse. 1 ACE site. Layout `u32 1, u32 guid, String16L name, u32 secondsDisabled`. Unrecognized; acdream never sends the 0xF7D9 request that triggers it. | +| 0xF653 | CharacterLogOff | both | PARSED | inbound `WorldSession.cs:1236` (`CharacterLogOff.IsConfirmation`); outbound builder `Messages\CharacterLogOff.cs:186` | GameMessageCharacterLogOff.cs (S→C, opcode-only) + `Network\Handlers\CharacterHandler.cs:266` (C→S) | ACE's S→C confirmation is opcode-only (size 4) — acdream's `IsConfirmation` requires `body.Length == 4` and matches the opcode. The C→S request builder writes `opcode + u32 characterId` per `Proto_UI::LogOffCharacter @ 0x00546A20`. Both directions complete. Note: the confirmation check runs *before* the `dispatchWorldEvents` gate so it still lands during Dispose drain. | +| 0xF655 | CharacterDelete | both | MISSING | — | GameMessageCharacterDelete.cs (S→C, opcode-only) + `CharacterHandler.cs:273` (C→S) | acdream neither sends the C→S delete request nor recognizes the S→C ack. Not reachable in acdream's flow today. | +| 0xF656 | CharacterCreate | C→S | MISSING | — | `CharacterHandler.cs:26` | acdream has no character-creation path; it always selects an existing identity from `CharacterList`. | +| 0xF657 | CharacterEnterWorld | C→S | PARSED | builder `Messages\CharacterEnterWorld.cs:49`; send `WorldSession.cs:926` | `CharacterHandler.cs:198` | acdream writes `opcode, u32 characterGuid, String16L accountName` — matching what ACE's handler reads. Uses the account name returned by `CharacterSet::UnPack` (not the login-form spelling) per `CPlayerSystem::LogOnCharacter @ 0x0055F890`, which is what ACE validates. | +| 0xF658 | CharacterList | S→C | PARSED | dispatch `WorldSession.cs:1245`; parser `Messages\CharacterList.cs:57` | GameMessageCharacterList.cs | ACE: `u32 status(0)`, `i32 activeCount`, N × (`u32 id`, `String16L name`, `u32 secondsGreyedOut`), `u32 deletedCount(0)`, `u32 slotCount`, `String16L account`, `u32 useTurbineChat`, `u32 hasThroneOfDestiny`. acdream reads every field including both collections, with a payload-derived count bound (`CharacterList.cs:123`). Complete. | +| 0xF659 | CharacterError | S→C | MISSING | — | GameMessageCharacterError.cs | **12 ACE construction sites**, including every login-rejection path (`AuthenticationHandler.cs:145/152/160/189/192`, `Session.cs:339`, `Player_Tick.cs:45/59`). Layout is a single `u32 CharacterError`. Unrecognized — see Finding 3. | +| 0xF6EA | ForceObjectDescSend | C→S | MISSING | — | `Network\Handlers\ControlHandler.cs:8` | Client-to-server only (`u32 itemGuid`); also duplicated in `GameActionType.cs:159`. acdream never sends it, so it can never request a fresh ObjDesc for a stale item. | +| 0xF745 | ObjectCreate | S→C | **PARTIAL** | dispatch `WorldSession.cs:1271`; parser `Messages\CreateObject.cs:~540-1140` | GameMessageCreateObject.cs → `WorldObject_Networking.cs:56` `SerializeCreateObject` | Physics block: fully covered (flags, state, Movement/AnimationFrame, Position, MTable, STable, PeTable, CSetup, Parent, Children, ObjScale, Friction, Elasticity, Translucency, Velocity, Acceleration, Omega, DefaultScript(+Intensity), all 9 u16 timestamps, align). Weenie block: correct field ORDER and bit map (verified against `ACE.Entity\Enum\WeenieHeaderFlags.cs`). **Skipped-but-cursor-consumed (value discarded, no typed output):** `PScript` (0x08000000, u16), `HouseOwner` (0x02000000, u32), `HouseRestrictions` (0x04000000, RestrictionDB), `Monarch` (0x00000040, u32). Also `Workmanship` is captured but `CombatUse` is read as unsigned where ACE writes `sbyte` (`CreateObject.cs:973` vs. the `unchecked((sbyte)…)` used for ItemsCapacity at `:929`) — benign for real enum values. | +| 0xF746 | PlayerCreate | S→C | **STUB** | dispatch `WorldSession.cs:1260-1270` | GameMessagePlayerCreate.cs | ACE writes `u32 guid` (size 8). acdream matches the opcode only as the LoginComplete trigger and **never reads the guid**; the local identity comes from the CharacterList selection (`WorldSession.cs:907`). Also the arm is gated on `&& !_loginCompleteSent`, so any later 0xF746 falls through to the unhandled branch. | +| 0xF747 | ObjectDelete | S→C | PARSED | dispatch `WorldSession.cs:1279`; parser `Messages\DeleteObject.cs:29` | GameMessageDeleteObject.cs | ACE: `u32 guid`, `u16 instanceSeq`, `Align()`. acdream reads guid + instanceSeq (min length 10; ACE emits 12 after align). Retail anchor `CM_Physics::DispatchSB_DeleteObject @ 0x006AC6A0`. Complete. | +| 0xF748 | UpdatePosition | S→C | PARSED | dispatch `WorldSession.cs:1319`; parser `Messages\UpdatePosition.cs:163` | GameMessageUpdatePosition.cs → `Network\Structure\PositionPack.cs` | ACE: `u32 guid`, `u32 flags`, Origin(`u32 cell` + 3×f32), conditional rotation W/X/Y/Z (present when the matching `OrientationHasNo*` bit is CLEAR), conditional Velocity(3×f32), conditional PlacementID(u32), then 4×u16 sequences. acdream reproduces every branch, in order, including the inverted-flag rotation semantics and all four sequences. Complete. | +| 0xF749 | ParentEvent | S→C | PARSED | dispatch `WorldSession.cs:1293`; parser `Messages\ParentEvent.cs:116` | GameMessageParentEvent.cs | ACE: `u32 creatureGuid`, `u32 childGuid`, `i32 parentLocation`, `i32 placement`, `u16 parentInstanceSeq`, `u16 childPositionSeq` (24 bytes). acdream reads all six. Complete. | +| 0xF74A | PickupEvent | S→C | PARSED | dispatch `WorldSession.cs:1285`; parser `Messages\PickupEvent.cs:76` | GameMessagePickupEvent.cs | ACE: `u32 guid`, `u16 instanceSeq`, `u16 positionSeq`. acdream reads all three and routes to a distinct `EntityPickedUp` event (deliberately NOT collapsed into 0xF747, since pickup advances POSITION_TS and retains the logical object). Complete. | +| 0xF74B | SetState | S→C | PARSED | dispatch `WorldSession.cs:1355`; parser `Messages\SetState.cs:66` | GameMessageSetState.cs | ACE: `u32 guid`, `u32 physicsState`, `u16 instanceSeq`, `u16 stateSeq` (16 bytes). acdream reads all four. Complete. | +| 0xF74C | MovementEvent | S→C | PARSED | dispatch `WorldSession.cs:1299`; parser `Messages\UpdateMotion.cs:94` | GameMessageUpdateMotion.cs → `Network\Motion\MovementData.cs:184` | Same opcode as `Motion` (next row). See that row for the field analysis. Also duplicated as `GameActionType.MovementEvent = 0xF74C` (C→S), which acdream does not send. | +| 0xF74C | Motion | S→C | PARSED | dispatch `WorldSession.cs:1299`; parser `Messages\UpdateMotion.cs:94` | GameMessageUpdateMotion.cs | ACE: `u32 guid`, `u16 instanceSeq`, then `MovementData.Write(header:true)` = `u16 movementSeq`, `u16 serverControlSeq`, `byte isAutonomous`, `Align()`, `byte movementType`, `byte motionFlags`, `u16 currentStyle`, then the type-selected body. acdream covers movementType 0 (InterpretedMotionState with the correct non-sequential flag-bit → write-order mapping, the Commands list, and the `MotionFlags.StickToObject` trailing guid), 6/7 (MoveTo\*: optional target guid, Origin, 7-dword MovementParameters, runRate) and 8/9 (TurnTo\*: optional target guid + standalone wire heading, 3-dword TurnToParameters). The `Align()` pad is computed correctly against ACE's ABSOLUTE-stream `CalculatePadMultiple` (`Network\Extensions.cs:55`) — one pad byte at stream length 15. Complete. | +| 0xF74E | VectorUpdate | S→C | PARSED | dispatch `WorldSession.cs:1341`; parser `Messages\VectorUpdate.cs:179` | GameMessageVectorUpdate.cs | ACE: `u32 guid`, `Vector3 velocity`, `Vector3 omega`, `u16 instanceSeq`, `u16 vectorSeq` (36 bytes). acdream reads all four. Complete. | +| 0xF750 | Sound | S→C | MISSING | — | GameMessageSound.cs | **63 ACE construction sites — the highest-volume unhandled opcode.** Layout `u32 guid, u32 soundId, f32 volume` (16 bytes). No `0xF750` reference anywhere in `src\`. See Finding 7. | +| 0xF751 | PlayerTeleport | S→C | PARSED | dispatch (inline) `WorldSession.cs:1524-1540` | GameMessagePlayerTeleport.cs | ACE writes exactly one field, `u16 teleportSequence`, then `Align()` (total 8 bytes). acdream reads the u16 at offset 4 and raises `TeleportStarted`. Complete. | +| 0xF753 | AutonomousPosition | C→S | PARSED | builder `Messages\AutonomousPosition.cs:32` (GameAction sub-opcode inside the 0xF7B1 envelope) | `Network\GameAction\GameActionType.cs:165`; the S→C `GameMessageAutonomousPosition.cs` exists but its only send site is commented out (`WorldObjects\Player.cs:683`) | ACE never sends 0xF753 as a top-level GameMessage. acdream correctly treats it as a client→server GameAction (`u32 0xF7B1, u32 sequence, u32 0xF753, …`), matching retail. | +| 0xF754 | PlayScriptId | S→C | PARSED | dispatch `WorldSession.cs:1512`; parser `Messages\PlayPhysicsScript.cs:216` | none — ACE has **no** serializer or handler for `PlayScriptId`; the enum entry is unused | acdream parses `u32 guid, u32 scriptDid` (12 bytes exact) per retail `SmartBox::HandlePlayScriptID @ 0x00452020`. Complete against retail, but ACE never emits it, so it is dead on an ACE server. | +| 0xF755 | PlayEffect | S→C | PARSED | dispatch `WorldSession.cs:1518`; parser `Messages\PlayPhysicsScriptType.cs:246` | GameMessageScript.cs | ACE: `u32 guid, u32 scriptId, f32 speed` (16 bytes). acdream reads all three (`RawScriptType`, `Intensity`) with an exact-length gate. 27 ACE construction sites. Complete. | +| 0xF7B0 | GameEvent | S→C | PARSED | dispatch `WorldSession.cs:1489`; envelope parser `Messages\GameEventEnvelope.cs:53` | `Network\GameEvent\GameEventMessage.cs` | Envelope only: `u32 playerGuid, u32 gameEventSequence, u32 eventType`, then a borrowed payload view. All four header words are read and the payload is dispatched by sub-opcode via `Messages\GameEventDispatcher.cs`. Per-sub-opcode coverage is a separate family and is out of scope for this ledger. | +| 0xF7B1 | GameAction | C→S | PARSED | builders across `Messages\*Requests.cs` / `*Actions.cs`; send seam `WorldSession.cs:1589` | `Network\GameAction\GameActionPacket.cs:9` | Outbound-only envelope (`u32 0xF7B1, u32 sequence, u32 actionType, …`). acdream implements ~50 sub-actions (chat, movement, inventory, combat, casting, social, recalls). Per-sub-opcode coverage is a separate family. | +| 0xF7C1 | AccountBanned | S→C | MISSING | — | GameMessageAccountBanned.cs | Sent from `AuthenticationHandler.cs:220` on a banned account. Layout `u32 secondsUntilExpiry` + optional `String16L reason`. Unrecognized — the ban reason is silently dropped and the client just times out. | +| 0xF7C8 | CharacterEnterWorldRequest | C→S | PARSED | builder `Messages\CharacterEnterWorld.cs:36`; send `WorldSession.cs:910` | `CharacterHandler.cs:183` | Opcode-only body (4 bytes), matching what ACE's handler expects. Complete. | +| 0xF7CC | GetServerVersion | C→S | MISSING | — | `Network\Handlers\GetServerVersionHandler.cs:10` | acdream never sends the `@version` query. ACE would reply with a `0xF7E0` SystemChat line (which acdream does parse). No inbound work needed. | +| 0xF7CD | FriendsOld | C→S | PARSED | builder `Messages\ClientCommandRequests.cs:40` (`LegacyFriendsOpcode`); send `WorldSession.cs:1778` | `Network\Handlers\FriendsOldHandler.cs:9` | Client-to-server legacy friends command; ACE replies with a `0xF7E0` SystemChat ("not used in the emulator"). acdream builds and sends it as a control message. Complete for the direction that exists. | +| 0xF7D9 | CharacterRestore | C→S | MISSING | — | `CharacterHandler.cs:331` | acdream has no character-restore path. | +| 0xF7DB | UpdateObject | S→C | MISSING | — (only a comment mentions it: `WorldSession.cs:662`) | GameMessageUpdateObject.cs → `WorldObject_Networking.cs:28` `SerializeUpdateObject` | **9 ACE construction sites** (Tailoring ×2, RecipeManager, Aetheria, DeveloperCommands…). The payload is byte-identical to `0xF745 ObjectCreate` — `SerializeUpdateObject` simply calls `SerializeCreateObject`. acdream's parser hard-rejects it because `CreateObject.TryParse` compares the opcode word against `0xF745`. See Finding 2. | +| 0xF7DC | AccountBoot | S→C | MISSING | — | GameMessageBootAccount.cs | **13 ACE construction sites** (Sentinel `@boot`, session termination paths). Layout: optional `String16L reason` (may be a bare opcode). Unrecognized — a booted client shows no reason and just falls off the wire. | +| 0xF7DE | TurbineChat | both | PARSED | dispatch `WorldSession.cs:1421`; parser `Messages\TurbineChat.cs:145`, builder in the same file | GameMessageTurbineChat.cs + `Network\Handlers\TurbineChatHandler.cs:19` | 9-u32 header (`sizeFirst, blobType, dispatchType, targetType, targetId, transportType, transportId, cookie, sizeSecond`) then a variant payload. acdream handles `(EventBinary, SendToRoomByName)` and `(ResponseBinary, *)`, which are exactly the two combinations ACE emits (`TurbineChatHandler.cs:112` and the `NETBLOB_RESPONSE_BINARY` sites all pass `ASYNCMETHOD_SENDTOROOMBYNAME`). Turbine 1-or-2-byte-prefix UTF-16LE strings handled separately from CP1252 String16L. `sizeFirst` is read but intentionally not validated (`TurbineChat.cs:~275`). Complete. | +| 0xF7DF | CharacterEnterWorldServerReady | S→C | PARSED | `WorldSession.cs:919` (handshake loop) | GameMessageCharacterEnterWorldServerReady.cs | Opcode-only body (4 bytes) — there are no fields to decode. acdream gates `EnterWorld` on seeing it. Note it is only matched in the handshake drain; after login it falls to the unhandled branch, which is harmless because ACE sends it once. | +| 0xF7E0 | ServerMessage | S→C | PARSED | dispatch `WorldSession.cs:1406`; parser `Messages\ServerMessage.cs:292` | GameMessageSystemChat.cs | ACE: `String16L message`, `i32 chatMessageType`. acdream reads both. **724 ACE construction sites** — by far the most-sent message on the server. Complete. | +| 0xF7E1 | ServerName | S→C | MISSING | — | GameMessageServerName.cs | Sent twice at login (`AuthenticationHandler.cs:258`, `Session.cs:274`). Layout `i32 currentConnections, i32 maxConnections, String16L serverName`. Unrecognized — acdream never learns the world name or population. | +| 0xF7E2 | DDD_DataMessage | S→C | MISSING | — | GameMessageDDDDataMessage.cs | 7 ACE construction sites, but only reachable when the client claims stale dat iterations. acdream's interrogation response claims zero lists (see 0xF7E5/0xF7E6), so ACE never enters the DDD patch flow in practice. | +| 0xF7E3 | DDD_RequestDataMessage | C→S | MISSING | — | `Network\Handlers\DDDHandler.cs:195` | Client-to-server dat pull request; acdream never sends it (no DDD patching). | +| 0xF7E4 | DDD_ErrorMessage | S→C | MISSING | — | GameMessageDDDErrorMessage.cs | 1 ACE construction site, inside the DDD patch flow acdream never enters. Layout `u32 resourceType, u32 dataId, u32 errorType`. | +| 0xF7E5 | DDD_Interrogation | S→C | **STUB** | dispatch `WorldSession.cs:1250-1259` | GameMessageDDDInterrogation.cs | ACE writes `u32 serversRegion, u32 nameRuleLanguage, u32 productId, u32 supportedLanguagesCount(2), u32 0, u32 1` (28 bytes). acdream recognizes the opcode and immediately sends a canned `0xF7E6` reply but **decodes zero body fields** — region, language, product id and the supported-language list are all dropped. | +| 0xF7E6 | DDD_InterrogationResponse | C→S | PARSED | builder `Messages\DddInterrogationResponse.cs:34`; send `WorldSession.cs:1258` | `DDDHandler.cs:24` | acdream writes `opcode, u32 language(1=English), u32 count(0)`. ACE's handler reads `ReadUInt32()` then `ReadCAllIterationList()` which starts with an `i32 numElements` — so a count of 0 terminates the read cleanly (`Network\Structure\CAllIterationList.cs:19`). Byte-exact for the empty case. | +| 0xF7E7 | DDD_BeginDDD | S→C | MISSING | — | GameMessageDDDBeginDDD.cs | 1 ACE construction site, inside the DDD patch flow acdream never enters. | +| 0xF7E8 | DDD_BeginPullDDD | — | MISSING | — | none | No ACE serializer and no ACE handler — enum entry only; never on the wire. | +| 0xF7E9 | DDD_IterationData | — | MISSING | — | none | No ACE serializer and no ACE handler — enum entry only; never on the wire. | +| 0xF7EA | DDD_EndDDD | both | MISSING | — | GameMessageDDDEndDDD.cs (S→C, opcode-only) + `DDDHandler.cs:176` (C→S) | 2 ACE construction sites, inside the DDD patch flow acdream never enters. Unrecognized in either direction. | + +#### Roll-up + +| Status | Count | +|---|---| +| PARSED | 36 | +| PARTIAL | 2 | +| STUB | 2 | +| MISSING | 42 | +| **Total rows** | **82** | + +Restricting to opcodes ACE actually constructs/handles in normal play (i.e. excluding the +12 enum-only or dead entries: `None`, `PositionAndMovement`, `PublicUpdateSkill`, +`PrivateUpdateSkillLevel`, `PublicUpdateSkillLevel`, `PublicUpdateAttribute`, +`PublicUpdateVital`, `PublicUpdatePosition`, `PrivateUpdatePropertyFloat`, +`PrivateUpdatePropertyString`, `DDD_BeginPullDDD`, `DDD_IterationData`): +**36 PARSED / 2 PARTIAL / 2 STUB / 30 MISSING out of 70 live opcodes.** + +#### Findings + +##### Finding 1 — `HearRangedSpeech` (0x02BC) decodes `chatType` from the `range` float's bits + +`src\AcDream.Core.Net\Messages\HearSpeech.cs:49` uses **one** parser for both `0x02BB` +(HearSpeech) and `0x02BC` (HearRangedSpeech), reading: + +``` +string16L text; string16L senderName; u32 senderGuid; u32 chatType; +``` + +But ACE's `GameMessageHearRangedSpeech.cs` writes an extra `float range` between the sender +id and the chat type: + +```csharp +Writer.WriteString16L(messageText); +Writer.WriteString16L(senderName); +Writer.Write(senderID); +Writer.Write(range); // <-- acdream never reads this +Writer.Write((uint)chatMessageType); +``` + +Independently confirmed by holtburger +(`crates\holtburger-protocol\src\messages\chat\types.rs:50-67`, `HearRangedSpeechData::unpack` +advances 12 bytes: `sender`, `range`, `chat_type`) versus `HearSpeechData::unpack` (8 bytes). + +Consequence: for every ranged/shout message, `Parsed.ChatType` is the IEEE-754 bit pattern of +the range float (e.g. a 20 m range → `0x41A00000` = 1 101 004 800) instead of a +`ChatMessageType` in 0x01–0x1F, and the real chat type is never read at all. The +`IsRanged` flag on `HearSpeech.Parsed` is derived from the opcode, so the parser *knows* +which variant it is — it just does not branch on it. Evidence that the field genuinely +diverges: the min-length gate is also wrong for the ranged case (`body.Length < 16` at +`HearSpeech.cs:51` vs. a 20-byte minimum). ACE has 1 `GameMessageHearRangedSpeech` +construction site, so the blast radius is narrow but the decode is definitively wrong. + +##### Finding 2 — `UpdateObject` (0xF7DB) is dropped despite being byte-identical to `ObjectCreate` + +`ACE.Server\WorldObjects\WorldObject_Networking.cs:28`: + +```csharp +public virtual void SerializeUpdateObject(BinaryWriter writer, bool adminvision = false, bool changenodraw = false) +{ + // content of these 2 is the same? TODO: Validate that? + SerializeCreateObject(writer, false, adminvision, changenodraw); +} +``` + +so `0xF7DB` and `0xF745` carry the same ModelData + PhysicsData + WeenieHeader payload. +acdream already has a full parser for that payload, but `CreateObject.TryParse` +(`src\AcDream.Core.Net\Messages\CreateObject.cs:~545`) rejects anything whose opcode word +is not `0xF745`, and no dispatch arm matches `0xF7DB`, so the message falls to the unhandled +branch at `WorldSession.cs:1572`. ACE has 9 construction sites — `Entity\Tailoring.cs:504`, +`Tailoring.cs:563`, `Managers\RecipeManager.cs:397`, `Entity\Aetheria.cs:241`, plus dev +commands — i.e. every "this item's whole description changed" refresh is silently discarded. +A stale `ClientObjectTable` row is the visible symptom (retailored armor keeps its old +name/icon/model until the object is re-spawned). Note also that `WorldSession.cs:662` +documents `ACDREAM_DUMP_APPEARANCE=1` as logging "every 0xF625 ObjDescEvent + 0xF7DB +UpdateObject" — the 0xF7DB half of that comment describes behavior that does not exist. + +##### Finding 3 — Login/session rejection reasons (`CharacterError` 0xF659) are silently discarded + +ACE terminates a session with an explicit `GameMessageCharacterError` carrying a single +`u32 CharacterError` code. Send sites include `Network\Handlers\AuthenticationHandler.cs:145` +(`AccountInvalid`), `:152` (`AccountDoesntExist`), `:160` and `:189`/`:192` (`Logon` — the +"account already logged in" case), `:248` (`LogonServerFull`), `Network\Session.cs:339`, and +`WorldObjects\Player_Tick.cs:45/59` (save failures). acdream recognizes none of them +(`grep 0xF659` over `src\` returns nothing), so the message hits the unhandled branch and the +session instead dies on a generic timeout: `throw new TimeoutException("CharacterList not +received")` at `WorldSession.cs:889` or `"ServerReady not received"` at `:921`. + +This is the exact mechanism behind the project's documented "exit 29 after a hard kill" +mystery (CLAUDE.md, "Logout-before-reconnect"): ACE *is* telling the client +`CharacterError.Logon` — acdream just throws the reason away and reports a timeout. Decoding +one `u32` would turn a 3-minute guessing game into a one-line diagnostic. + +##### Finding 4 — Live `PropertyBool` broadcasts (0x02D2) are dropped, including door lock state + +`GameMessagePublicUpdatePropertyBool` has 9 ACE construction sites, notably +`WorldObjects\Door.cs:220` and `WorldObjects\Lock.cs:275/310` (both `PropertyBool.Locked`) +and `WorldObjects\Hook.cs:259` (`PropertyBool.UiHidden`). acdream has no parser and no +dispatch arm. Consequence: after the initial `CreateObject`, a door's locked/unlocked state +and a hook's hidden state never change client-side. This sits adjacent to the door work +already tracked in the render/physics digests, so it is worth flagging even though it is a +gap rather than a decode error. The sibling private form (0x02D1, 7 sites) is likewise +unhandled. + +##### Finding 5 — `PublicUpdateInstanceId` (0x02DA) unhandled — 12 send sites, container/wielder re-parenting + +`GameMessagePublicUpdateInstanceID` (`byte seq, u32 guid, u32 property, u32 instanceGuid`) is +ACE's live re-parent broadcast (`PropertyInstanceId.Container` / `.Wielder`), with 12 +construction sites. acdream tracks parent relations from `0xF749 ParentEvent` and the +`CreateObject` Parent block only, so any instance-id change delivered through this opcode is +lost. Same for the private form 0x02D9 (1 site). + +##### Finding 6 — ACE's `PublicUpdatePropertyString` (0x02D6) has anomalous field order (documentation hazard) + +Every other `Public*` property message writes `seq, guid, property, value`. But +`GameMessagePublicUpdatePropertyString.cs` writes `seq, property, guid, Align(), String16L` — +property BEFORE guid, plus an `Align()` that the int/bool/float variants do not have. acdream +does not parse this opcode today, so there is no live bug; recording it here so a future +implementation does not copy the layout from the neighbouring `Public*` classes and get it +wrong. (Its private sibling 0x02D5 has zero ACE send sites and can be skipped.) + +##### Finding 7 — `Sound` (0xF750) is the single highest-volume unhandled opcode + +63 ACE construction sites — locks, chests, drops, trait raises, UI feedback — all writing +`u32 guid, u32 soundId, f32 volume`. Nothing in `src\` references `0xF750`. This is a pure +coverage gap (no decode error), but by send frequency it is the largest one in the family +after the already-handled `0xF7E0`. + +##### Non-findings verified clean (checked field-by-field, no divergence) + +- `UpdatePosition` (0xF748) — including the inverted `OrientationHasNo*` semantics and all + four trailing u16 sequences, against `Network\Structure\PositionPack.cs`. +- `UpdateMotion` (0xF74C) — including ACE's `Align()` being computed on the **absolute** + stream length (`Network\Extensions.cs:55`), which yields exactly one pad byte at offset 15; + and the non-sequential `MovementStateFlag` bit→write-order mapping + (`0x01,0x02,0x08,0x20` ushorts then `0x04,0x10,0x40` floats). +- All `WeenieHeaderFlag` bit values and field widths in `CreateObject` against + `ACE.Entity\Enum\WeenieHeaderFlags.cs` and the `WorldObject_Properties.cs` property types + (`ItemCapacity`/`ContainerCapacity` are `byte?` → 1 byte, `Script`/`HookType`/`MaxStructure` + are `ushort?` → 2 bytes, `Workmanship` is `float?` → 4 bytes, `CooldownDuration` promotes to + `double` → 8 bytes). +- The "sequence" prefix on every `0x02xx` property message is a **1-byte** `ByteSequence` + (`Network\Sequence\SequenceManager.cs:179` default arm), which is what acdream assumes + everywhere it parses one. +- `TurbineChat` (0xF7DE) blob/dispatch pairs — acdream's two implemented cases are exactly the + two ACE emits (`TurbineChatHandler.cs` always passes `ASYNCMETHOD_SENDTOROOMBYNAME`). +- `DddInterrogationResponse` (0xF7E6) empty-list encoding is accepted byte-exactly by ACE's + `ReadCAllIterationList`. + +--- + +### 3.2 GameEvent sub-opcodes, inside 0xF7B0 (104 rows) + +Audit date: 2026-07-29. Read-only. + +**Oracles** +- Row universe + field layout: `C:\Users\erikn\source\repos\client\ACE\Source\ACE.Server\Network\GameEvent\GameEventType.cs` (104 entries, 103 distinct values — `VendorInfoEvent` and `ApproachVendor` share `0x0062`) and `...\GameEvent\Events\*.cs` (+ `...\Network\Structure\*.cs` for the shared blobs). +- Retail cross-check: `docs\research\named-retail\acclient_2013_pseudo_c.txt`, `acclient.h`. + +**acdream sides** +- Envelope: `src\AcDream.Core.Net\Messages\GameEventEnvelope.cs:53` (16-byte header, payload borrowed). +- Dispatch: `src\AcDream.Core.Net\WorldSession.cs:1489` → `GameEventDispatcher.Dispatch` (`src\AcDream.Core.Net\Messages\GameEventDispatcher.cs:95`). +- Registrations: **only two files** — `src\AcDream.Core.Net\GameEventWiring.cs` (40 owned) and `src\AcDream.Core.Net\WorldSession.cs:775,780` (2 legacy). 42 registrations total; every other sub-opcode falls into `_unhandledCounts` (`GameEventDispatcher.cs:114`). +- Production call site supplies every optional collaborator (`src\AcDream.Runtime\Session\LiveSessionEventRouter.cs:146-175`), so all conditionally-registered handlers **are** live in production. + +All `file:line` below are repo-relative. + +--- + +| Sub-opcode | ACE name | acdream enum? | acdream status | acdream site (file:line) | ACE serializer | Field-coverage notes | +|---|---|---|---|---|---|---| +| 0x0003 | AllegianceUpdateAborted | yes — `AllegianceUpdateAborted` | STUB | none (falls to `GameEventDispatcher.cs:114`) | *(no serializer class in ACE)* | ACE has no `GameEventAllegianceUpdateAborted` and never sends it. Zero risk vs ACE. | +| 0x0004 | PopupString | yes — `PopupString` | PARSED | reg `GameEventWiring.cs:113`; parse `Messages/GameEvents.cs:87` | `GameEventPopupString.cs` | ACE writes one `String16L`. acdream reads it (CP-1252 + 4-byte pad, `GameEvents.cs:535`). Complete. | +| 0x0013 | PlayerDescription | yes — `PlayerDescription` | PARSED | reg `GameEventWiring.cs:477`; parse `Messages/PlayerDescriptionParser.cs:268` | `GameEventPlayerDescription.cs` | Every ACE-written section is walked in ACE's exact order: propertyFlags/weenieType → Int32/Int64/Bool/Double/String/Did/Iid/Position tables → vectorFlags+hasHealth → attributes → skills → spells → enchantment registry → optionFlags/options1 → shortcuts → SpellLists8 → DesiredComps → SpellbookFilters → Options2 → GameplayOptions → inventory → equipped. Enum bit values verified identical (`DescriptionPropertyFlag`, `AttributeCache` bit=(id-1), `CharacterOptionDataFlag`, `EnchantmentMask`). Deliberate documented skips: skill `u16 const_one` (`PlayerDescriptionParser.cs:637`) and hash-bucket hints. **Two caveats:** (a) `GameplayOptions` is an unlength-prefixed raw `byte[]` in ACE (`GameEventPlayerDescription.cs:571`) so acdream recovers its end by a 4-byte-aligned forward scan (`PlayerDescriptionParser.cs:755`) and stores it opaque; (b) the shortcut `Spell` field is ACE's `LayeredSpell` (u16+u16) read as one u32 (`PlayerDescriptionParser.cs:357`) — see Finding 2. | +| 0x0020 | AllegianceUpdate | yes — `AllegianceUpdate` | STUB | none | `GameEventAllegianceUpdate.cs` (rank u32 + `AllegianceProfile`) | ACE constructs it 5×; sent whenever allegiance data changes. Body dropped; `AllegianceTree` (`src\AcDream.Core\Allegiance\AllegianceTree.cs:28`) documents it as the intended source but nothing feeds it. | +| 0x0021 | FriendsListUpdate | yes — `FriendsListUpdate` | PARSED | reg `GameEventWiring.cs:149`; parse `Messages/SocialStateMessages.cs:13` | `GameEventFriendsListUpdate.cs` | ACE writes count, then per friend `friendId, isOnline, appearOffline, name(String16L), numFriends(0), numFriendsOf(0)`, then trailing `updateType`. acdream reads all of them including both empty u32 lists and the trailing type. Minor: `Enum.IsDefined` gate (`SocialStateMessages.cs:37`) rejects any value outside `FriendsUpdateType{0..4}` — ACE's `FriendsUpdateTypeFlag` is `[Flags]`, so a combined value would drop the whole update (ACE only sends singletons today). | +| 0x0022 | InventoryPutObjInContainer | yes — `InventoryPutObjInContainer` | PARSED | reg `GameEventWiring.cs:346`; parse `Messages/GameEvents.cs:387` | `GameEventItemServerSaysContainId.cs` | 4×u32: itemGuid, containerGuid, placement, containerType. All read. | +| 0x0023 | WieldObject | yes — `WieldObject` | PARSED | reg `GameEventWiring.cs:335`; parse `Messages/GameEvents.cs:369` | `GameEventWieldItem.cs` | 2×u32: objectId, `(int)EquipMask`. Both read (`EquipLoc` widened to uint — ACE writes it as signed int, harmless for a bitmask). | +| 0x0029 | CharacterTitle | yes — `CharacterTitle` | STUB | none | `GameEventCharacterTitle.cs` (1u, currentTitleId, count, titleId[]) | Sent once per title query/login path. Body dropped; no title UI consumes it. | +| 0x002B | UpdateTitle | yes — `UpdateTitle` | STUB | none | `GameEventUpdateTitle.cs` (title u32, setAsDisplayTitle u32) | ACE sends on title grant. Dropped. | +| 0x0052 | CloseGroundContainer | yes — `CloseGroundContainer` | PARSED | reg `GameEventWiring.cs:425`; parse `Messages/GameEvents.cs:450` | `GameEventCloseGroundContainer.cs` | Single u32 container guid. Complete. | +| 0x0062 | VendorInfoEvent | **no** (alias absent; value present as `ApproachVendor`) | STUB | none | *(alias of `GameEventApproachVendor.cs`)* | Name-only divergence — same wire value as the row below. See Finding 5. | +| 0x0062 | ApproachVendor | yes — `ApproachVendor` | STUB | none | `GameEventApproachVendor.cs` | ACE sends on every vendor Use: vendorGuid, merchandiseItemTypes, min/max value, dealMagical, buy/sellPrice, altCurrency, altCurrencyCount, altCurrencyPluralName(String16L), numItems, then per item `stackSize|0xFF000000` + `SerializeGameDataOnly`, then Align. Wholly dropped — this is the blocker for the pre-M4 "vendor browsing" slice. | +| 0x0075 | StartBarber | yes — `StartBarber` | STUB | none | `GameEventStartBarber.cs` (16 u32 appearance fields) | Only on barber Use. Dropped. | +| 0x00A0 | InventoryServerSaveFailed | yes — `InventoryServerSaveFailed` | PARSED | reg `GameEventWiring.cs:394`; parse `Messages/GameEvents.cs:441` | `GameEventInventoryServerSaveFailed.cs` | 2×u32: itemGuid, WeenieError. Both read and both used (`RejectMove`). ACE's 2nd-most-constructed event (179 sites). | +| 0x00A3 | FellowshipQuit | yes — `FellowshipQuit` | STUB | none | `GameEventFellowshipQuit.cs` (playerId u32) | ACE sends on fellow quit. Dropped (no fellowship subsystem). | +| 0x00A4 | FellowshipDismiss | yes — `FellowshipDismiss` | STUB | none | `GameEventFellowshipDismiss.cs` (guid u32) | Same. | +| 0x00B4 | BookDataResponse | yes — `BookDataResponse` | STUB | none | `GameEventBookDataResponse.cs` | Sent on book Use. Dropped; no book UI. | +| 0x00B5 | BookModifyPageResponse | yes — `BookModifyPageResponse` | STUB | none | `GameEventBookModifyPageResponse.cs` (bookGuid, page, success) | Dropped. | +| 0x00B6 | BookAddPageResponse | yes — `BookAddPageResponse` | STUB | none | `GameEventBookAddPageResponse.cs` (bookGuid, page, success) | Dropped. | +| 0x00B7 | BookDeletePageResponse | yes — `BookDeletePageResponse` | STUB | none | `GameEventBookDeletePageResponse.cs` (bookGuid, page, success) | Dropped. | +| 0x00B8 | BookPageDataResponse | yes — `BookPageDataResponse` | STUB | none | `GameEventBookPageDataResponse.cs` | Dropped. | +| 0x00C3 | GetInscriptionResponse | yes — `GetInscriptionResponse` | STUB | none | `GameEventInscriptionResponse.cs` (guid, inscription, playerGuid, scribeName, scribeAccount, Align) | ACE sends on inscription query. Dropped — note the pre-M4 "inscription transaction" work is accepted, so this is likely a real gap to confirm against the outbound path. | +| 0x00C9 | IdentifyObjectResponse | yes — `IdentifyObjectResponse` | PARSED | reg `GameEventWiring.cs:433`; parse `Messages/AppraiseInfoParser.cs:154` | `GameEventIdentifyObjectResponse.cs` + `Structure/AppraiseInfo.cs:735` | Flag bit values match ACE's `IdentifyResponseFlags` exactly. Read order matches ACE's write order (Int, Int64, Bool, Float, String, Did, SpellBook, ArmorProfile, CreatureProfile, WeaponProfile, HookProfile, Armor/Weapon/Resist enchantment bitfields, ArmorLevels). Sub-blob layouts verified field-for-field against `ArmorProfile.cs`, `CreatureProfile.cs` (incl. the 0x8 ShowAttributes and 0x1 HasBuffsDebuffs gates), `WeaponProfile.cs` (10 fields), `HookProfile.cs` (3 u32), `ArmorLevel.cs` (9 u32). Complete. | +| 0x0147 | ChannelBroadcast | yes — `ChannelBroadcast` | PARSED | reg `GameEventWiring.cs:98`; parse `Messages/GameEvents.cs:31` | `GameEventChannelBroadcast.cs` | channel u32 + senderName + messageText. All read. | +| 0x0148 | ChannelList | yes — `ChannelList` | STUB | none | `GameEventChannelList.cs` (count + names) | ACE sends only in response to a channel-list request. Dropped. | +| 0x0149 | ChannelIndex | yes — `ChannelIndex` | STUB | none | `GameEventChannelIndex.cs` (count + admin channel names) | Admin/advocate only. Dropped. | +| 0x0196 | ViewContents | yes — `ViewContents` | PARSED | reg `GameEventWiring.cs:363`; parse `Messages/GameEvents.cs:403` | `GameEventViewContents.cs` | containerGuid, count, count×(guid, containerType). All read with a 64-bit-safe bounds check (`GameEvents.cs:409`). | +| 0x019A | InventoryPutObjectIn3D | yes — `InventoryPutObjectIn3D` | PARSED | reg `GameEventWiring.cs:379`; parse `Messages/GameEvents.cs:430` | `GameEventItemServerSaysMoveItem.cs` | Single u32 guid. Complete. | +| 0x01A7 | AttackDone | yes — `AttackDone` | PARSED | reg `GameEventWiring.cs:265`; parse `Messages/GameEvents.cs:261` | `GameEventAttackDone.cs` | ACE writes exactly one u32 (`WeenieError`); acdream reads it. The record's `AttackSequence` is **fabricated as 0** (`GameEvents.cs:264`) — nothing on the wire, so no coverage gap vs ACE, but it is not a decoded field. See Finding 4. | +| 0x01A8 | MagicRemoveSpell | yes — `MagicRemoveSpell` | **PARTIAL** | reg `GameEventWiring.cs:287`; parse `Messages/GameEvents.cs:288` | `GameEventMagicRemoveSpell.cs` | **Missing field: `u16 layer`.** ACE writes `(ushort spellId, ushort layer)`; acdream reads a single little-endian u32 and treats the whole thing as the spell id. Correct only while `layer == 0` (ACE's default). A non-zero layer silently corrupts the spell id by `layer << 16`. | +| 0x01AC | VictimNotification | yes — `VictimNotification` | PARSED | reg `GameEventWiring.cs:237`; parse `Messages/GameEvents.cs:170` | `GameEventVictimNotification.cs` | One `String16L` death message. Complete. | +| 0x01AD | KillerNotification | yes — `KillerNotification` | PARSED | reg `GameEventWiring.cs:275`; parse `Messages/GameEvents.cs:180` | `GameEventKillerNotification.cs` | One `String16L`. Complete. | +| 0x01B1 | AttackerNotification | yes — `AttackerNotification` | PARSED | reg `GameEventWiring.cs:249`; parse `Messages/GameEvents.cs:196` | `GameEventAttackerNotification.cs` | All 6 fields decoded (name, damageType u32, percent **f64**, damage u32, critical u32, attackConditions u64). Parser is complete; the **wiring** forwards only name/damageType/damage/percent — `Critical` and `AttackConditions` never reach `CombatState` (`GameEventWiring.cs:252-253`). See Finding 6. | +| 0x01B2 | DefenderNotification | yes — `DefenderNotification` | PARSED | reg `GameEventWiring.cs:242`; parse `Messages/GameEvents.cs:223` | `GameEventDefenderNotification.cs` | All 7 fields decoded (name, damageType, f64 percent, damage, damageLocation, critical, u64 attackConditions). Parser complete. **Wiring** drops `HealthPercent` + `AttackConditions` and passes a hardcoded `attackerGuid = 0u` (`GameEventWiring.cs:245-247`). See Finding 6. | +| 0x01B3 | EvasionAttackerNotification | yes — `EvasionAttackerNotification` | PARSED | reg `GameEventWiring.cs:255`; parse `Messages/GameEvents.cs:242` | `GameEventEvasionAttackerNotification.cs` | One `String16L`. Complete. | +| 0x01B4 | EvasionDefenderNotification | yes — `EvasionDefenderNotification` | PARSED | reg `GameEventWiring.cs:260`; parse `Messages/GameEvents.cs:249` | `GameEventEvasionDefenderNotification.cs` | One `String16L`. Complete. | +| 0x01B8 | CombatCommenceAttack | yes — `CombatCommenceAttack` | PARSED | reg `GameEventWiring.cs:270`; parse `Messages/GameEvents.cs:256` | `GameEventCombatCommenceAttack.cs` | Zero-length body (ACE's `4` ctor arg is a MemoryStream capacity hint, not content — `GameEventMessage.cs:34`). acdream requires `payload.Length == 0`. Complete. | +| 0x01C0 | UpdateHealth | yes — `UpdateHealth` | PARSED | reg `GameEventWiring.cs:222`; parse `Messages/GameEvents.cs:142` | `GameEventUpdateHealth.cs` | objectId u32 + health **f32**. Both read at the right widths. | +| 0x01C3 | QueryAgeResponse | yes — `QueryAgeResponse` | PARSED | reg `GameEventWiring.cs:118`; parse `Messages/GameEvents.cs:100` | `GameEventQueryAgeResponse.cs` | targetName + age, both `String16L`. Complete. | +| 0x01C7 | UseDone | yes — `UseDone` | PARSED | reg `GameEventWiring.cs:412`; parse `Messages/GameEvents.cs:423` | `GameEventUseDone.cs` | One u32 WeenieError. Complete. | +| 0x01C8 | AllegianceAllegianceUpdateDone | yes — **name mismatch**: `AllegianceUpdateDone` | STUB | none | `GameEventAllegianceAllegianceUpdateDone.cs` (WeenieError u32) | ACE constructs it 5×. Dropped. | +| 0x01C9 | FellowshipFellowUpdateDone | yes — `FellowshipFellowUpdateDone` | STUB | none | `GameEventFellowshipFellowUpdateDone.cs` | ACE writes **no payload** (the `errorType` ctor arg is never serialized — ACE bug/oversight). Dropped. | +| 0x01CA | FellowshipFellowStatsDone | yes — `FellowshipFellowStatsDone` | STUB | none | *(no serializer class in ACE)* | ACE never sends it. Zero risk vs ACE. | +| 0x01CB | ItemAppraiseDone | yes — `ItemAppraiseDone` | STUB | none | *(no serializer class in ACE)* | ACE never sends it. Retail uses it as the appraise-complete signal; acdream instead keys off 0x00C9 arrival. | +| 0x01E2 | Emote | yes — `Emote` | STUB | none | *(no serializer class in ACE)* | ACE never sends it (emotes ride `HearEmote`/`SoulEmote` top-level opcodes, handled at `Messages/SoulEmote.cs`). Zero risk vs ACE. | +| 0x01EA | PingResponse | yes — `PingResponse` | PARSED | reg `WorldSession.cs:780`; parse `Messages/GameEvents.cs:153` | `GameEventPingResponse.cs` | Zero-length body; acdream asserts `payload.IsEmpty` and records the RTT. Complete. | +| 0x01F4 | SetSquelchDB | yes — `SetSquelchDB` | PARSED | reg `GameEventWiring.cs:158`; parse `Messages/SocialStateMessages.cs:45` | `GameEventCommunicationSetSquelch.cs` + `Structure/SquelchDB.cs`, `SquelchInfo.cs` | Reads accounts `PackableHashTable` (u16 count / u16 buckets — matches `PackableHashTable.WriteHeader`), characters table, then the global `SquelchInfo`. `SquelchInfo` order (filters → name → accountWide) matches both ACE and retail `SquelchInfo::UnPack @ 0x006B11C0`. All bytes consumed. **Semantic hazard**, not a coverage gap: acdream models `_squelch_msgs` as retail's `AC1Legacy::vlong` bit-vector (bit id = `word*32+bit`), but ACE writes the *same* `SquelchMask` 4× (`SquelchInfo.cs:27`), so acdream synthesizes phantom message-type ids 32–127. See Finding 8. | +| 0x01FD | RegisterTrade | yes — `RegisterTrade` | STUB | none | `GameEventRegisterTrade.cs` (initiator, partner, 0L) | ACE sends on trade open. Dropped — no trade subsystem. | +| 0x01FE | OpenTrade | yes — `OpenTrade` | STUB | none | *(no serializer class in ACE)* | ACE never sends it. | +| 0x01FF | CloseTrade | yes — `CloseTrade` | STUB | none | `GameEventCloseTrade.cs` (EndTradeReason u32) | Dropped. | +| 0x0200 | AddToTrade | yes — `AddToTrade` | STUB | none — **dead parser** at `Messages/GameEvents.cs:466` | `GameEventAddToTrade.cs` (objectGuid, tradeSide, 0) | Parser `ParseAddToTrade` exists but reads only 2 of ACE's 3 u32 (misses the trailing slot/location) and is **never registered**. See Finding 7. | +| 0x0201 | RemoveFromTrade | yes — `RemoveFromTrade` | STUB | none | *(no serializer class in ACE)* | ACE never sends it. | +| 0x0202 | AcceptTrade | yes — `AcceptTrade` | STUB | none — **dead parser** at `Messages/GameEvents.cs:475` | `GameEventAcceptTrade.cs` (whoAccepted guid) | Parser exists, never registered. | +| 0x0203 | DeclineTrade | yes — `DeclineTrade` | STUB | none | `GameEventDeclineTrade.cs` (whoDeclined guid) | Dropped. | +| 0x0205 | ResetTrade | yes — `ResetTrade` | STUB | none | `GameEventResetTrade.cs` (whoReset guid) | Dropped. | +| 0x0207 | TradeFailure | yes — `TradeFailure` | STUB | none — **dead parser** at `Messages/GameEvents.cs:457` | `GameEventTradeFailure.cs` (objectGuid, WeenieError) | Parser reads only 1 of ACE's 2 u32 (misses `objectGuid`, actually reads it *as* the reason) and is never registered. See Finding 7. | +| 0x0208 | ClearTradeAcceptance | yes — `ClearTradeAcceptance` | STUB | none | `GameEventClearTradeAcceptance.cs` (empty body) | Dropped. | +| 0x021D | HouseProfile | yes — `HouseProfile` | STUB | none | `GameEventHouseProfile.cs` (crystal guid + `HouseProfile`) | Dropped — no housing subsystem. | +| 0x0225 | HouseData | yes — `HouseData` | STUB | none | `GameEventHouseData.cs` (`HouseData` blob) | Dropped. | +| 0x0226 | HouseStatus | yes — `HouseStatus` | STUB | none | `GameEventHouseStatus.cs` (WeenieError u32) | Dropped. | +| 0x0227 | UpdateRentTime | yes — `UpdateRentTime` | STUB | none | `GameEventHouseUpdateRentTime.cs` — **never constructed anywhere in ACE.Server** | Zero risk vs ACE. | +| 0x0228 | UpdateRentPayment | yes — `UpdateRentPayment` | STUB | none | `GameEventHouseUpdateRentPayment.cs` — **never constructed in ACE.Server** | Zero risk vs ACE. | +| 0x0248 | HouseUpdateRestrictions | yes — `HouseUpdateRestrictions` | STUB | none | `GameEventHouseUpdateRestrictions.cs` (sequence, guid, `RestrictionDB`) | Dropped. | +| 0x0257 | UpdateHAR | yes — `UpdateHAR` | STUB | none | `GameEventHouseUpdateHAR.cs` (`HouseAccess`) | Dropped. | +| 0x0259 | HouseTransaction | yes — `HouseTransaction` | STUB | none | `GameEventHouseTransaction.cs` — **never constructed in ACE.Server** | Zero risk vs ACE. | +| 0x0264 | QueryItemManaResponse | yes — `QueryItemManaResponse` | PARSED | reg `GameEventWiring.cs:229`; parse `Messages/GameEvents.cs:488` | `GameEventQueryItemManaResponse.cs` | 3 fields: target u32, mana **f32**, success u32. All read at the right widths. | +| 0x0271 | AvailableHouses | yes — `AvailableHouses` | STUB | none | `GameEventHouseAvailableHouses.cs` (type, `List`, totalAvailable) | Dropped. | +| 0x0274 | CharacterConfirmationRequest | yes — `CharacterConfirmationRequest` | PARSED | reg `GameEventWiring.cs:129`; parse `Messages/GameEvents.cs:503` | `GameEventConfirmationRequest.cs` | confirmationType u32, context u32, text `String16L`. All read. | +| 0x0276 | CharacterConfirmationDone | yes — `CharacterConfirmationDone` | PARSED | reg `GameEventWiring.cs:139`; parse `Messages/GameEvents.cs:524` | `GameEventConfirmationDone.cs` | confirmationType u32, contextId u32. Both read. | +| 0x027A | AllegianceLoginNotification | yes — `AllegianceLoginNotification` | STUB | none | `GameEventAllegianceLoginNotification.cs` (playerGuid, isLoggedIn) | ACE sends on allegiance-member login/logout. Dropped. | +| 0x027C | AllegianceInfoResponse | yes — `AllegianceInfoResponse` | STUB | none | `GameEventAllegianceInfoResponse.cs` (playerGuid + `AllegianceProfile`) | Dropped. | +| 0x0281 | JoinGameResponse | yes — `JoinGameResponse` | STUB | none | `GameEventJoinGameResponse.cs` (boardGuid, color i32) | Chess only. Dropped. | +| 0x0282 | StartGame | yes — `StartGame` | STUB | none | `GameEventStartGame.cs` (boardGuid, color i32) | Chess only. Dropped. | +| 0x0283 | MoveResponse | yes — `MoveResponse` | STUB | none | `GameEventMoveResponse.cs` (boardGuid, result i32) | Chess only. Dropped. | +| 0x0284 | OpponentTurn | yes — `OpponentTurn` | STUB | none | `GameEventOpponentTurn.cs` (boardGuid, color, `ChessMoveData`) | Chess only. Dropped. | +| 0x0285 | OpponentStalemate | yes — `OpponentStalemate` | STUB | none | `GameEventOpponentStalemate.cs` (boardGuid, color, stalemate) | Chess only. Dropped. | +| 0x028A | WeenieError | yes — `WeenieError` | PARSED | reg `GameEventWiring.cs:210`; parse `Messages/GameEvents.cs:115` | `GameEventWeenieError.cs` | One u32. Complete. ACE's single most-constructed event (239 sites). | +| 0x028B | WeenieErrorWithString | yes — `WeenieErrorWithString` | PARSED | reg `GameEventWiring.cs:215`; parse `Messages/GameEvents.cs:124` | `GameEventWeenieErrorWithString.cs` | errorType u32 + message `String16L`. Both read; order matches retail `CM_Communication::DispatchUI_WeenieErrorWithString @ 0x006A5070` (u32 then string). | +| 0x028C | GameOver | yes — `GameOver` | STUB | none | `GameEventGameOver.cs` (boardGuid, teamWinner i32) | Chess only. Dropped. | +| 0x0295 | SetTurbineChatChannels | yes — `SetTurbineChatChannels` | PARSED | reg `GameEventWiring.cs:175` **and** `WorldSession.cs:775`; parse `Messages/SetTurbineChatChannels.cs:67` | `GameEventSetTurbineChatChannels.cs` | 10 sequential u32 read in ACE's exact order (allegiance, General, Trade, LFG, Roleplay, **Olthoi**, society, SocietyCelestialHand, SocietyEldrytchWeb, SocietyRadiantBlood). Complete. **Double-registered**: the dispatcher is single-handler-per-opcode, so the owned `GameEventWiring` handler shadows the `WorldSession` one while a session is live; `WorldSession.TurbineChannelsReceived` has zero subscribers anyway. See Finding 10. | +| 0x02AE | AdminQueryPluginList | yes — `AdminQueryPluginList` | STUB | none | *(no serializer class in ACE)* | ACE never sends it. | +| 0x02B1 | AdminQueryPlugin | yes — `AdminQueryPlugin` | STUB | none | *(no serializer class in ACE)* | ACE never sends it. | +| 0x02B3 | AdminQueryPluginResponse | yes — `AdminQueryPluginResponse` | STUB | none | *(no serializer class in ACE)* | ACE never sends it. | +| 0x02B4 | SalvageOperationsResult | yes — `SalvageOperationsResult` | STUB | none | `GameEventSalvageOperationsResult.cs` (skill, 0, count, `SalvageResult[]`, augBonus) | ACE sends on salvage. Dropped. | +| 0x02BD | Tell | yes — `Tell` | **PARTIAL** | reg `GameEventWiring.cs:103`; parse `Messages/GameEvents.cs:54` | `GameEventTell.cs` | **Missing field: the trailing `u32` after `chatMessageType`** (`GameEventTell.cs:831`/`:841` — "not documented in the xml's, but is found in the pcaps"). acdream stops after `chatType`. Benign today (constant 0, nothing follows in the envelope), but it is an ACE-serialized field that is neither read nor documented as skipped; the `Tell` record's XML comment lists only 5 fields. | +| 0x02BE | FellowshipFullUpdate | yes — `FellowshipFullUpdate` | STUB | none | `GameEventFellowshipFullUpdate.cs` (PackableHashTable of fellows + name/leader/flags) | ACE constructs it 6×; sent on any fellowship change. Dropped. | +| 0x02BF | FellowshipDisband | yes — `FellowshipDisband` | STUB | none | `GameEventFellowshipDisband.cs` (empty body) | Dropped. | +| 0x02C0 | FellowshipUpdateFellow | yes — `FellowshipUpdateFellow` | STUB | none | `GameEventFellowshipUpdateFellow.cs` (guid, cp, lum, level, 3 max vitals, 3 cur vitals, shareLoot, name, updateType) | Dropped. | +| 0x02C1 | MagicUpdateSpell | yes — `MagicUpdateSpell` | **PARTIAL** | reg `GameEventWiring.cs:282`; parse `Messages/GameEvents.cs:159` | `GameEventMagicUpdateSpell.cs` | **Missing field: `u16 layer`.** ACE writes `(ushort spellId, ushort layer)`; acdream reads one u32 as the spell id. Same latent corruption as 0x01A8. | +| 0x02C2 | MagicUpdateEnchantment | yes — `MagicUpdateEnchantment` | PARSED | reg `GameEventWiring.cs:292`; parse `Messages/GameEvents.cs:300` → `Messages/EnchantmentWireReader.cs:19` | `GameEventMagicUpdateEnchantment.cs` + `Structure/Enchantment.cs:150` | All 14 base fields + the conditional `spellSetId` decoded in ACE's exact order and widths (u16 spellId, u16 layer, u16 category, u16 hasSpellSetId, u32 power, f64 startTime, f64 duration, u32 casterGuid, f32 degradeMod, f32 degradeLimit, f64 lastTimeDegraded, u32 statModType, u32 statModKey, f32 statModValue, [u32 spellSetId]) = 60 (+4). Complete. | +| 0x02C3 | MagicRemoveEnchantment | yes — `MagicRemoveEnchantment` | PARSED | reg `GameEventWiring.cs:307`; parse `Messages/GameEvents.cs:279` | `GameEventMagicRemoveEnchantment.cs` | Correctly reads `(u16 spellId, u16 layer)` in ACE's order and hands them to `Spellbook.OnEnchantmentRemoved(layer, spellId)` matching that method's `(layerId, spellId)` signature. Complete. (The XML comment at `GameEvents.cs:270` says "(layerId, spellId)" — comment order is wrong, code order is right.) | +| 0x02C4 | MagicUpdateMultipleEnchantments | yes — `MagicUpdateMultipleEnchantments` | PARSED | reg `GameEventWiring.cs:297`; parse `Messages/GameEvents.cs:308` → `EnchantmentWireReader.cs:60` | `GameEventMagicUpdateMultipleEnchantments.cs` — **never constructed in ACE.Server** | i32 count + N × the 0x02C2 record. Complete; unexercised against ACE. | +| 0x02C5 | MagicRemoveMultipleEnchantments | yes — `MagicRemoveMultipleEnchantments` | PARSED | reg `GameEventWiring.cs:312`; parse `Messages/GameEvents.cs:323` | `GameEventMagicRemoveMultipleEnchantments.cs` — **never constructed in ACE.Server** | i32 count + N × `LayeredSpell(u16 spellId, u16 layer)`; order matches `Structure/LayeredSpell.cs:39`. Complete; unexercised against ACE. | +| 0x02C6 | MagicPurgeEnchantments | yes — `MagicPurgeEnchantments` | PARSED | reg `GameEventWiring.cs:329` | `GameEventMagicPurgeEnchantments.cs` (empty body) | No fields to decode; handler calls `Spellbook.OnPurgeAll()`. Complete. | +| 0x02C7 | MagicDispelEnchantment | yes — `MagicDispelEnchantment` | PARSED | reg `GameEventWiring.cs:318`; parse `Messages/GameEvents.cs:320` | `GameEventMagicDispelEnchantment.cs` | Same `(u16 spellId, u16 layer)` shape as 0x02C3. Complete. | +| 0x02C8 | MagicDispelMultipleEnchantments | yes — `MagicDispelMultipleEnchantments` | PARSED | reg `GameEventWiring.cs:323`; parse `Messages/GameEvents.cs:323` | `GameEventMagicDispelMultipleEnchantments.cs` | i32 count + N × `LayeredSpell`. Both ACE ctor overloads produce the same wire shape. Complete. | +| 0x02C9 | MiscPortalStormBrewing | yes — **name mismatch**: `PortalStormBrewing` | STUB | none | `GameEventPortalStormBrewing.cs` (**f32 extent**, default 0.4) | ACE writes one float; acdream drops it. Portal-storm UI cue absent. | +| 0x02CA | MiscPortalStormImminent | yes — **name mismatch**: `PortalStormImminent` | STUB | none | `GameEventPortalStormImminent.cs` (**f32 extent**, default 0.6) | Same. | +| 0x02CB | MiscPortalStorm | yes — **name mismatch**: `PortalStorm` | STUB | none | `GameEventPortalStorm.cs` (empty body) | Dropped. | +| 0x02CC | MiscPortalstormSubsided | yes — **name mismatch**: `PortalStormSubsided` | STUB | none | `GameEventPortalStormSubsided.cs` (empty body) | Dropped. | +| 0x02EB | CommunicationTransientString | yes — `CommunicationTransientString` | **STUB (broken parser)** | reg `GameEventWiring.cs:108`; parse `Messages/GameEvents.cs:73` | `GameEventCommunicationTransientString.cs` | **A handler IS registered, but it produces zero state effect on every real payload.** ACE writes exactly one `String16L` and nothing else; acdream's `ParseTransient` then requires 4 more bytes for a `ChatType` u32 and returns `null` when they are absent (`GameEvents.cs:79`). `WriteString16L` pads to a 4-byte multiple, so the remainder after the string is always exactly 0 bytes → 100% drop. ACE constructs this event at **134 sites** — it is the third-most-common GameEvent on the wire. See Finding 1. | +| 0x0312 | MagicPurgeBadEnchantments | yes — `MagicPurgeBadEnchantments` | PARSED | reg `GameEventWiring.cs:331` | `GameEventMagicPurgeBadEnchantments.cs` (empty body) | No fields; handler calls `Spellbook.OnPurgeBadEnchantments()`. Complete. | +| 0x0314 | SendClientContractTrackerTable | yes — `SendClientContractTrackerTable` | STUB | none | `GameEventSendClientContractTrackerTable.cs` (`ContractManager`) | ACE sends at login when contracts exist. Dropped. | +| 0x0315 | SendClientContractTracker | yes — `SendClientContractTracker` | STUB | none | `GameEventSendClientContractTracker.cs` (`ContractTracker`, deleteContract, setAsDisplay) | Dropped. | + +--- + +#### Roll-up + +Rows written: **104** (one per ACE enum entry; `0x0062` appears twice because ACE declares `VendorInfoEvent` and `ApproachVendor` at the same value). + +| Status | Rows | Notes | +|---|---|---| +| **PARSED** | **38** | Every ACE-serialized field decoded into typed output. | +| **PARTIAL** | **3** | 0x01A8 MagicRemoveSpell, 0x02C1 MagicUpdateSpell (both drop `u16 layer`), 0x02BD Tell (drops ACE's trailing u32). | +| **STUB** | **63** | 62 rows with no registered handler (→ `_unhandledCounts`) + 1 registered-but-always-null (0x02EB). | +| **MISSING** | **0** | acdream's enum covers all 103 distinct ACE values; nothing is absent from the enum. | + +Registered handlers: 42 (40 in `GameEventWiring.cs`, 2 in `WorldSession.cs`). Of the 62 unregistered rows, **13 correspond to sub-opcodes ACE never emits at all** (0x0003, 0x0062-alias, 0x01CA, 0x01CB, 0x01E2, 0x01FE, 0x0201, 0x0227, 0x0228, 0x0259, 0x02AE, 0x02B1, 0x02B3), so the real ACE-facing gap is ~49 rows, dominated by fellowship (6), housing (8), chess (6), trade (8), allegiance (4), and books (5). + +#### acdream-vs-ACE enum delta + +- **Value coverage: identical.** All 103 distinct ACE values are present in `src\AcDream.Core.Net\Messages\GameEventType.cs`; acdream declares no value ACE does not. +- **Missing alias (1):** ACE's `VendorInfoEvent = 0x0062` has no acdream counterpart (acdream keeps only `ApproachVendor`). Harmless — same value. +- **Name mismatches (5):** `AllegianceAllegianceUpdateDone`→`AllegianceUpdateDone` (0x01C8); `MiscPortalStormBrewing`→`PortalStormBrewing` (0x02C9); `MiscPortalStormImminent`→`PortalStormImminent` (0x02CA); `MiscPortalStorm`→`PortalStorm` (0x02CB); `MiscPortalstormSubsided`→`PortalStormSubsided` (0x02CC). All are cosmetic (values agree) but they defeat cross-repo grep. +- **Stale doc:** `GameEventType.cs:4` says "All 94 `GameEventType` sub-opcodes"; the enum has 103. It also cites `references/ACE/...`, a path that is now empty in this tree. + +--- + +#### Findings + +##### 1. `CommunicationTransientString (0x02EB)` is dropped 100% of the time — a phantom trailing `ChatType` field + +`GameEvents.ParseTransient` (`src\AcDream.Core.Net\Messages\GameEvents.cs:73-84`): + +```csharp +string message = ReadString16L(payload, ref pos); +if (payload.Length - pos < 4) return null; // <-- always taken +uint chatType = BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(pos)); +``` + +- **ACE** `GameEventCommunicationTransientString.cs` writes **only** `Writer.WriteString16L(message)`. `WriteString16L` pads to a multiple of 4 including the 2 length bytes (`ACE.Server\Network\Extensions.cs:12-21`), and the 16-byte GameEvent header is already 4-aligned, so after the string there are **exactly 0** bytes left. The `< 4` guard fires every time and the handler at `GameEventWiring.cs:108` never calls `chat.OnSystemMessage`. +- **Retail agrees with ACE.** `CM_Communication::DispatchUI_TransientString @ 0x006A4FD0` unpacks a single `PStringBase` and calls `Handle_Communication__TransientString(system, &string)` — no second field. The "string + u32 type" shape acdream implemented belongs to a *different* message: `CM_Communication::DispatchUI_TextboxString @ 0x006A4F30`, which gates on opcode **`0xF7E0`** (a top-level GameMessage, not a 0xF7B0 sub-opcode) and passes `*(uint32_t*)arg2` as the type. +- **Impact:** ACE constructs this event at **134 call sites** — it is the standard "system says something to you" channel (skill-up notices, quest text, transient status). All of it is silently discarded. +- **The unit test pins the wrong shape:** `tests\AcDream.Core.Net.Tests\Messages\GameEventDispatcherTests.cs:377-390` synthesizes `MakeString16L(...) + u32(5)` and asserts `ChatType == 5`. It is green against a payload ACE/retail never produce. (Same class as the "tests can pin wrong values" lesson in `feedback_phase_a1_hotfix_saga.md`.) + +##### 2. Three sites read ACE's `(u16 spellId, u16 layer)` pair as a single u32 + +| Site | ACE writer | +|---|---| +| `Messages\GameEvents.cs:159` `ParseMagicUpdateSpell` | `GameEventMagicUpdateSpell.cs:225-227` | +| `Messages\GameEvents.cs:288` `ParseMagicRemoveSpell` | `GameEventMagicRemoveSpell.cs:189-190` | +| `Messages\PlayerDescriptionParser.cs:357` (shortcut `spellId`) | `Structure\Shortcut.cs:44-49` → `LayeredSpell` | + +Each returns `spellId | (layer << 16)`. All three are correct **only** because ACE currently passes `layer = 0` / a default `LayeredSpell`. The neighbouring 0x02C3/0x02C7 parsers get the same pair right (`GameEvents.cs:279`), so this is an inconsistency inside acdream, not a missing spec. Spell ids are `ushort` on the wire; the `uint` return type hides the truncation. + +##### 3. `Tell (0x02BD)` does not consume ACE's trailing u32 + +`GameEventTell.cs:831` and `:841` both end with `Writer.Write(0u)`. `ParseTell` (`GameEvents.cs:54-68`) stops after `chatType`. No desync results (nothing follows in the envelope) but the field is neither read nor documented as skipped, and the `Tell` record's XML doc (`GameEvents.cs:46-52`) presents the message as 5 fields. + +##### 4. `AttackDone` fabricates an `AttackSequence` that is not on the wire + +`GameEvents.cs:264` returns `new AttackDone(0u, )` and `GameEventWiring.cs:268` forwards that constant into `CombatState.OnAttackDone(attackSequence, weenieError)`. ACE's `GameEventAttackDone.cs` writes a single u32. Any consumer that treats `AttackSequence` as meaningful is reading a hardcoded zero. + +##### 5. Enum divergences (see the delta section above) + +Five renamed members, one missing alias, and a stale "All 94" doc comment at `GameEventType.cs:4`. Values are all correct, so this is a maintainability/grep risk rather than a wire risk. + +##### 6. Wiring-level field loss on the two damage notifications + +Both parsers are complete, but `GameEventWiring.cs` discards decoded fields before they reach `CombatState`: + +- `:245-247` `OnDefenderNotification(name, **0u**, damageType, damage, hitQuadrant, critical)` — `attackerGuid` is hardcoded `0u` (ACE genuinely does not send it, so the parameter is unsatisfiable from this message), and `HealthPercent` + `AttackConditions` are dropped. +- `:252-253` `OnAttackerNotification(name, damageType, damage, (float)HealthPercent)` — `Critical` and `AttackConditions` are dropped. Critical-hit presentation therefore cannot be driven from 0x01B1. + +##### 7. Four parsers exist with no registration (dead code that also has bugs) + +`ParseTradeFailure` (`GameEvents.cs:457`), `ParseAddToTrade` (`:466`), `ParseAcceptTrade` (`:475`), and `ParseIdentifyResponseHeader` (`:355`) are never wired into the dispatcher. Two of them are also wrong against ACE: + +- `ParseTradeFailure` reads **one** u32 and calls it the error code; `GameEventTradeFailure.cs` writes `(objectGuid, reason)` — the parser would return the guid. +- `ParseAddToTrade` reads `(itemGuid, slotIndex)`; `GameEventAddToTrade.cs` writes `(objectGuid, tradeSide, 0 /*slot*/)` — the second field is `TradeSide`, not a slot, and the real slot u32 is never read. + +##### 8. `SquelchInfo` filter semantics: acdream is retail-correct, ACE is not — expect phantom message-type ids + +`SocialStateMessages.ReadSquelchInfo` (`SocialStateMessages.cs:95-113`) decodes the filter block as retail's `AC1Legacy::vlong` bit-vector (`acclient.h:39464` — `AC1Legacy::vlong _squelch_msgs`), i.e. word *w* bit *b* ⇒ message type `w*32+b`. ACE writes the **same** `SquelchMask` four times (`Structure\SquelchInfo.cs:27`, with the comment "not sure why this is sent 4x"). Against ACE, acdream therefore materialises the same mask at bit offsets 0-31, 32-63, 64-95 and 96-127 — i.e. up to 3× phantom message-type ids per squelch entry. Byte consumption stays correct (no desync); only `SquelchInfo.MessageTypes` set membership is polluted. Worth a divergence-register row if `SquelchState` ever gates on specific ids. + +##### 9. `PlayerDescription` trailer boundary is recovered heuristically, and failure is silent + +ACE writes `Character.GameplayOptions` as a raw `byte[]` with **no length prefix** (`GameEventPlayerDescription.cs:571`). `PlayerDescriptionParser.TryHeuristicInventoryStart` (`:755-786`) brute-forces 4-byte-aligned offsets until `TryUnpackInventoryStrict` happens to consume exactly to end-of-buffer. When no offset qualifies (or the trailer throws), the catch at `:425-435` sets `TrailerTruncated = true` and returns **empty** inventory + equipment lists — and `GameEventWiring.cs:628-662` then calls `InitializeInventoryManifest` / `InitializeEquipmentManifest` with those empty arrays without ever checking `TrailerTruncated`. A trailer mis-scan therefore presents as "logged in with no inventory and nothing equipped", with no error surfaced outside `ACDREAM_DUMP_VITALS=1`. + +##### 10. Dispatcher blind spots: swallowed handler exceptions and a shadowed double registration + +- `GameEventDispatcher.Dispatch` (`GameEventDispatcher.cs:99-110`) catches every handler exception and writes to `Console.Error` **without** incrementing `_unhandledCounts`. A parser that throws is invisible to the "which server events are firing that we don't parse?" diagnostic the class advertises at `:11-13`. +- `0x0295` is registered twice — legacy at `WorldSession.cs:775` and owned at `GameEventWiring.cs:175`. The dispatcher stores one handler per opcode (`:63`, `:76`), so the wiring handler wins for the whole session lifetime and `WorldSession.TurbineChannelsReceived` (declared `WorldSession.cs:459`) never fires. It has zero subscribers in `src\`, so this is dead wiring rather than a live bug, but it is the only opcode with competing owners. + +##### 11. Sub-opcodes ACE cannot emit (context for the STUB column) + +No serializer class exists for `0x0003`, `0x01CA`, `0x01CB`, `0x01E2`, `0x01FE`, `0x0201`, `0x02AE`, `0x02B1`, `0x02B3`. Serializer files exist but are **never constructed** anywhere in `ACE.Server` for `0x0227`, `0x0228`, `0x0259`, `0x02C4`, `0x02C5`. The last two matter in the opposite direction: acdream's 0x02C4/0x02C5 parsers are complete but have never been exercised against a real ACE payload, so they are untested-in-practice PARSED rows. + +--- + +### 3.3 GameAction sub-opcodes, inside 0xF7B1 (163 rows) + +**Row universe:** all 163 entries of +`C:\Users\erikn\source\repos\client\ACE\Source\ACE.Server\Network\GameAction\GameActionType.cs`. + +**acdream paths:** all `src\...` / `tests\...` paths below are repo-relative. + +**ACE handler root:** +`C:\Users\erikn\source\repos\client\ACE\Source\ACE.Server\Network\GameAction\Actions\` +— the `ACE handler` column gives the bare filename under that folder. + +**Classification rule used:** a builder counts as *wired* only when a call site +**outside `src\AcDream.Core.Net\`** reaches it. Nearly every builder is fronted by a +`WorldSession.SendXxx()` wrapper (`src\AcDream.Core.Net\WorldSession.cs:1589-2097`); +the "Live caller" column names the first out-of-Core.Net site that invokes that wrapper +(or a delegate bound to it). Builders reachable only from +`tests\AcDream.Core.Net.Tests\` are **BUILDER-ONLY**. + +**Envelope note:** every acdream builder writes the same 12-byte header +(`u32 0xF7B1`, `u32 gameActionSequence`, `u32 subOpcode`) before its payload; +that is verified once here and not repeated per row. + +| Sub-opcode | ACE name | acdream status | Builder (file:line) | Live caller (file:line) | ACE handler | Field-coverage notes | +|---|---|---|---|---|---|---| +| 0x0005 | SetSingleCharacterOption | MISSING | — | — | GameActionSetSingleCharacterOption.cs | ACE reads `u32 option`, `u32 value`. No acdream builder — see Finding F1: acdream uses 0x01A1 instead and gets it wrong. | +| 0x0008 | TargetedMeleeAttack | BUILT+WIRED | `src\AcDream.Core.Net\Messages\AttackTargetRequest.cs:39` | `src\AcDream.App\Combat\LiveCombatAttackOperations.cs:212`; `src\AcDream.Headless\Hosting\HeadlessGameplayOperations.cs:106` | GameActionTargetedMeleeAttack.cs | ACE reads `u32 targetGuid`, `u32 attackHeight`, `f32 powerLevel`. Builder writes exactly that order/width. | +| 0x000A | TargetedMissileAttack | BUILT+WIRED | `src\AcDream.Core.Net\Messages\AttackTargetRequest.cs:56` | `src\AcDream.App\Combat\LiveCombatAttackOperations.cs:206`; `src\AcDream.Headless\Hosting\HeadlessGameplayOperations.cs:104` | GameActionTargetedMissileAttack.cs | ACE reads `u32 targetGuid`, `u32 attackHeight`, `f32 accuracyLevel`. Match. | +| 0x000F | SetAfkMode | BUILT+WIRED | `src\AcDream.Core.Net\Messages\ClientCommandRequests.cs:94` | `src\AcDream.App\Net\LiveSessionRuntimeFactory.cs:339` → `src\AcDream.App\UI\ClientCommandController.cs:247` | GameActionSetAFKMode.cs | ACE reads `u32 afk`. Builder writes `u32 (away?1:0)`. Match. | +| 0x0010 | SetAfkMessage | BUILT+WIRED | `src\AcDream.Core.Net\Messages\ClientCommandRequests.cs:98` | `src\AcDream.App\Net\LiveSessionRuntimeFactory.cs:340` → `src\AcDream.App\UI\ClientCommandController.cs:262` | GameActionSetAFKMessage.cs | ACE reads `String16L`. Builder writes CP-1252 String16L padded to 4. Match. | +| 0x0015 | Talk | BUILT+WIRED | `src\AcDream.Core.Net\Messages\ChatRequests.cs:43` | `src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:143`; `src\AcDream.App\Net\LiveSessionCommandRouter.cs:109` | GameActionTalk.cs | ACE reads one `String16L`. Match. | +| 0x0017 | RemoveFriend | BUILT+WIRED | `src\AcDream.Core.Net\Messages\ClientCommandRequests.cs:110` | `src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:684` | GameActionRemoveFriend.cs | ACE reads `u32 friendGuid`. Match. | +| 0x0018 | AddFriend | BUILT+WIRED | `src\AcDream.Core.Net\Messages\ClientCommandRequests.cs:107` | `src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:680` | GameActionAddFriend.cs | ACE reads `String16L` name. Match. | +| 0x0019 | PutItemInContainer | BUILT+WIRED | `src\AcDream.Core.Net\Messages\InteractRequests.cs:97` | `src\AcDream.App\Composition\InteractionRetainedUiComposition.cs:344`; `src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:802` | GameActionPutItemInContainer.cs | ACE reads `u32 item`, `u32 container`, `i32 placement`. Builder writes u32/u32/i32. Match. | +| 0x001A | GetAndWieldItem | BUILT+WIRED | `src\AcDream.Core.Net\Messages\InventoryActions.cs:155` | `src\AcDream.App\Composition\InteractionRetainedUiComposition.cs:314` | GameActionGetAndWieldItem.cs | ACE reads `u32 itemGuid`, `i32 EquipMask`. Builder writes u32/u32 — same 4-byte width, same bits. Match. | +| 0x001B | DropItem | BUILT+WIRED | `src\AcDream.Core.Net\Messages\InventoryActions.cs:143` | `src\AcDream.App\Composition\InteractionRetainedUiComposition.cs:315` | GameActionDropItem.cs | ACE reads `u32 itemGuid`. Match. | +| 0x001D | SwearAllegiance | BUILDER-ONLY | `src\AcDream.Core.Net\Messages\AllegianceRequests.cs:34` | no caller found (only `tests\AcDream.Core.Net.Tests\Messages\AllegianceRequestsTests.cs:13`) | GameActionAllegianceSwearAllegiance.cs | Payload correct (`u32 patronGuid`) but nothing in `src\` calls it — no `WorldSession` wrapper exists either. Dead builder. | +| 0x001E | BreakAllegiance | BUILDER-ONLY | `src\AcDream.Core.Net\Messages\AllegianceRequests.cs:43` | no caller found (only `tests\AcDream.Core.Net.Tests\Messages\AllegianceRequestsTests.cs:29`) | GameActionAllegianceBreakAllegiance.cs | Payload correct (`u32 targetGuid`). Dead builder. | +| 0x001F | AllegianceUpdateRequest | MISSING | — | — | GameActionAllegianceUpdateRequest.cs | ACE reads `u32 uiPanel` (bool). | +| 0x0025 | RemoveAllFriends | BUILT+WIRED | `src\AcDream.Core.Net\Messages\ClientCommandRequests.cs:113` | `src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:687` | GameActionRemoveAllFriends.cs | Parameterless on both sides. Match. | +| 0x0026 | TeleToPklArena | BUILT+WIRED | `src\AcDream.Core.Net\Messages\ClientCommandRequests.cs:54` | `src\AcDream.App\Net\LiveSessionRuntimeFactory.cs:305` → `src\AcDream.App\UI\ClientCommandController.cs:95` | GameActionTeleToPklArena.cs | Parameterless. Match. | +| 0x0027 | TeleToPkArena | BUILT+WIRED | `src\AcDream.Core.Net\Messages\ClientCommandRequests.cs:51` | `src\AcDream.App\Net\LiveSessionRuntimeFactory.cs:304` → `src\AcDream.App\UI\ClientCommandController.cs:88` | GameActionTeleToPkArena.cs | Parameterless. Match. | +| 0x002C | TitleSet | MISSING | — | — | GameActionSetTitle.cs | ACE reads `u32 title`. | +| 0x0030 | QueryAllegianceName | MISSING | — | — | GameActionQueryAllegianceName.cs | Parameterless. | +| 0x0031 | ClearAllegianceName | MISSING | — | — | GameActionClearAllegianceName.cs | Parameterless. | +| 0x0032 | TalkDirect | MISSING | — | — | GameActionTalkDirect.cs | ACE reads `String16L message`, `u32 targetGuid`. acdream only has name-addressed Tell (0x005D). | +| 0x0033 | SetAllegianceName | MISSING | — | — | GameActionSetAllegianceName.cs | ACE reads `String16L`. | +| 0x0035 | UseWithTarget | BUILT+WIRED | `src\AcDream.Core.Net\Messages\InteractRequests.cs:52` | `src\AcDream.App\Composition\InteractionRetainedUiComposition.cs:312` | GameActionUseWithTarget.cs | ACE reads `u32 sourceObjectGuid`, `u32 targetObjectGuid`. Builder writes source then target. Match. | +| 0x0036 | Use | BUILT+WIRED | `src\AcDream.Core.Net\Messages\InteractRequests.cs:38` | `src\AcDream.App\Interaction\SelectionInteractionTransport.cs:27`; `src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:778` | GameActionUseItem.cs | ACE reads `u32 itemGuid`. Match. | +| 0x003B | SetAllegianceOfficer | MISSING | — | — | GameActionSetAllegianceOfficer.cs | ACE reads `String16L name`, `u32 officerLevel`. | +| 0x003C | SetAllegianceOfficerTitle | MISSING | — | — | GameActionSetAllegianceOfficerTitle.cs | ACE reads `u32 level`, `String16L title`. | +| 0x003D | ListAllegianceOfficerTitles | MISSING | — | — | GameActionListAllegianceOfficerTitles.cs | Parameterless. | +| 0x003E | ClearAllegianceOfficerTitles | MISSING | — | — | GameActionClearAllegianceOfficerTitles.cs | Parameterless. | +| 0x003F | DoAllegianceLockAction | MISSING | — | — | GameActionDoAllegianceLockAction.cs | ACE reads `u32 AllegianceLockAction`. | +| 0x0040 | SetAllegianceApprovedVassal | MISSING | — | — | GameActionSetAllegianceApprovedVassal.cs | ACE reads `String16L`. | +| 0x0041 | AllegianceChatGag | MISSING | — | — | GameActionAllegianceChatGag.cs | ACE reads `String16L name`, `u32 enabled`. | +| 0x0042 | DoAllegianceHouseAction | MISSING | — | — | GameActionDoAllegianceHouseAction.cs | ACE reads `u32 AllegianceHouseAction`. | +| 0x0044 | RaiseVital | BUILT+PARTIAL | `src\AcDream.Core.Net\Messages\CharacterActions.cs:43` (→ `:77`) | `src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:625` | GameActionRaiseVital.cs | **Width bug (F2).** ACE reads `u32 vital`, `u32 xpSpent`; builder writes `u32 id` + **`u64` xp** (`WriteUInt64LittleEndian`, `CharacterActions.cs:84`), a 24-byte body where ACE expects 20. | +| 0x0045 | RaiseAttribute | BUILT+PARTIAL | `src\AcDream.Core.Net\Messages\CharacterActions.cs:39` (→ `:77`) | `src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:620` | GameActionRaiseAttribute.cs | Same `u64` vs `u32` xpSpent bug (F2). | +| 0x0046 | RaiseSkill | BUILT+PARTIAL | `src\AcDream.Core.Net\Messages\CharacterActions.cs:47` (→ `:77`) | `src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:630` | GameActionRaiseSkill.cs | Same `u64` vs `u32` xpSpent bug (F2). | +| 0x0047 | TrainSkill | BUILT+WIRED | `src\AcDream.Core.Net\Messages\CharacterActions.cs:51` | `src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:636` | GameActionTrainSkill.cs | ACE reads `u32 skill`, `i32 creditsSpent`. Builder writes u32/u32 — same width. Match. | +| 0x0048 | CastUntargetedSpell | BUILT+WIRED | `src\AcDream.Core.Net\Messages\CastSpellRequest.cs:41` | `src\AcDream.App\Composition\InteractionRetainedUiComposition.cs:376`; `src\AcDream.Headless\Hosting\HeadlessGameplayOperations.cs:217` | GameActionMagicCastUntargetedSpell.cs | ACE reads `u32 spellId`. Match. | +| 0x004A | CastTargetedSpell | BUILT+WIRED | `src\AcDream.Core.Net\Messages\CastSpellRequest.cs:54` | `src\AcDream.App\Composition\InteractionRetainedUiComposition.cs:378`; `src\AcDream.Headless\Hosting\HeadlessGameplayOperations.cs:223` | GameActionMagicCastTargetedSpell.cs | ACE reads `u32 targetGuid`, `u32 spellId`. Match. | +| 0x0053 | ChangeCombatMode | BUILT+WIRED | `src\AcDream.Core.Net\Messages\CharacterActions.cs:67` | `src\AcDream.Runtime\Gameplay\RuntimeCombatModeState.cs:88`; `src\AcDream.App\Combat\LiveCombatModeCommandController.cs:35` | GameActionChangeCombatMode.cs | ACE reads `u32 newCombatMode`. Match. | +| 0x0054 | StackableMerge | BUILT+WIRED | `src\AcDream.Core.Net\Messages\InventoryActions.cs:36` | `src\AcDream.App\Composition\InteractionRetainedUiComposition.cs:676` | GameActionStackableMerge.cs | ACE reads `u32 from`, `u32 to`, `i32 amount`. Match (amount written unsigned, same bits). | +| 0x0055 | StackableSplitToContainer | BUILT+WIRED | `src\AcDream.Core.Net\Messages\InventoryActions.cs:49` | `src\AcDream.App\Composition\InteractionRetainedUiComposition.cs:349`, `:670`, `:696` | GameActionStackableSplitToContainer.cs | ACE reads `u32 stack`, `u32 container`, `i32 place`, `i32 amount`. Match. | +| 0x0056 | StackableSplitTo3D | BUILT+WIRED | `src\AcDream.Core.Net\Messages\InventoryActions.cs:64` | `src\AcDream.App\Composition\InteractionRetainedUiComposition.cs:337` | GameActionStackableSplitTo3D.cs | ACE reads `u32 stackId`, `i32 amount`. Match. | +| 0x0058 | ModifyCharacterSquelch | BUILT+WIRED | `src\AcDream.Core.Net\Messages\ClientCommandRequests.cs:129` | `src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:718` | GameActionModifyCharacterSquelch.cs | ACE reads `u32 squelch`, `u32 playerGuid`, `String16L name`, `u32 messageType`. Builder writes exactly that order. Match. | +| 0x0059 | ModifyAccountSquelch | BUILT+WIRED | `src\AcDream.Core.Net\Messages\ClientCommandRequests.cs:146` | `src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:726` | GameActionModifyAccountSquelch.cs | ACE reads `u32 squelch`, `String16L name`. Match. | +| 0x005B | ModifyGlobalSquelch | BUILT+WIRED | `src\AcDream.Core.Net\Messages\ClientCommandRequests.cs:156` | `src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:731` | GameActionModifyGlobalSquelch.cs | ACE reads `u32 squelch`, `u32 messageType`. Match. | +| 0x005D | Tell | BUILT+WIRED | `src\AcDream.Core.Net\Messages\ChatRequests.cs:73` | `src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:147`; `src\AcDream.App\Net\LiveSessionCommandRouter.cs:238` | GameActionTell.cs | ACE reads `String16L message` **then** `String16L target`. Builder writes message-first (`ChatRequests.cs:83-84`) — a documented 2026-04-25 fix. Match. | +| 0x005F | Buy | MISSING | — | — | GameActionBuyItems.cs | ACE reads `u32 vendorGuid`, `u32 numItems`, then per item `i32 amount` + `u32 objectID`. Slice-5 vendor work is still pending. | +| 0x0060 | Sell | MISSING | — | — | GameActionSellItems.cs | ACE reads `u32 vendorGuid`, `u32 numItems`, then per item `i32 amount` + `u32 objectGuid`. | +| 0x0063 | TeleToLifestone | BUILT+WIRED | `src\AcDream.Core.Net\Messages\InteractRequests.cs:69` | `src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:179` | GameActionTeleToLifestone.cs | Parameterless. Match. | +| 0x00A1 | LoginComplete | BUILT+WIRED | `src\AcDream.Core.Net\Messages\GameActionLoginComplete.cs:45` | `src\AcDream.Runtime\Session\RuntimeLiveEntitySessionController.cs:327`; `src\AcDream.App\Streaming\LocalPlayerTeleportController.cs:306` | GameActionLoginComplete.cs | Parameterless on both sides. Builder hardcodes sequence `0` (`:49`) with a comment that ACE ignores it — true for this handler, but it means LoginComplete does not advance the shared GameAction sequence. | +| 0x00A2 | FellowshipCreate | BUILDER-ONLY | `src\AcDream.Core.Net\Messages\SocialActions.cs:88` | no caller found (only `tests\...\Messages\SocialActionsTests.cs:53`) | GameActionFellowshipCreate.cs | Dead **and wrong** (F3): ACE reads `String16L name` then **one `u32` shareXp**; builder writes name + two loose bytes (`openness`, `shareXp`) at `SocialActions.cs:100-101`, so ACE would decode `shareXp = openness \| shareXp<<8`. | +| 0x00A3 | FellowshipQuit | BUILDER-ONLY | `src\AcDream.Core.Net\Messages\SocialActions.cs:106` | no caller found (tests only) | GameActionFellowshipQuit.cs | ACE reads `u32 disband`; builder writes 1 byte into a 4-byte zeroed slot → same u32 value. Layout OK, but dead. | +| 0x00A4 | FellowshipDismiss | BUILDER-ONLY | `src\AcDream.Core.Net\Messages\SocialActions.cs:117` | no caller found (tests only) | GameActionFellowshipDismiss.cs | ACE reads `u32 guid`. Layout OK, dead. | +| 0x00A5 | FellowshipRecruit | BUILDER-ONLY | `src\AcDream.Core.Net\Messages\SocialActions.cs:121` | no caller found (tests only) | GameActionFellowshipRecruit.cs | ACE reads `u32 guid`. Layout OK, dead. | +| 0x00A6 | FellowshipUpdateRequest | BUILDER-ONLY | `src\AcDream.Core.Net\Messages\SocialActions.cs:125` | no caller found (tests only) | GameActionFellowshipUpdateRequest.cs | ACE reads `i32 panelOpen`; builder writes 1 byte into a zeroed 4-byte slot. Layout OK, dead. | +| 0x00AA | BookData | MISSING | — | — | GameActionBookData.cs | ACE reads `u32 bookGuid`. | +| 0x00AB | BookModifyPage | MISSING | — | — | GameActionBookModifyPage.cs | ACE reads `u32 bookGuid`, `i32 page`, `String16L text`. | +| 0x00AC | BookAddPage | MISSING | — | — | GameActionBookAddPage.cs | ACE reads `u32 bookGuid`. | +| 0x00AD | BookDeletePage | MISSING | — | — | GameActionBookDeletePage.cs | ACE reads `u32 bookGuid`, `i32 page`. | +| 0x00AE | BookPageData | MISSING | — | — | GameActionBookPageData.cs | ACE reads `u32 bookGuid`, `i32 pageNum`. | +| 0x00BF | SetInscription | BUILT+WIRED | `src\AcDream.Core.Net\Messages\InventoryActions.cs:182` | `src\AcDream.App\Composition\InteractionRetainedUiComposition.cs:712` | GameActionSetInscription.cs | ACE reads `u32 objectGuid`, `String16L text`. Builder writes guid + CP-1252 String16L padded to 4 (`:194`). Match. | +| 0x00C8 | IdentifyObject | BUILT+WIRED | `src\AcDream.Core.Net\Messages\AppraiseRequest.cs:33` | `src\AcDream.App\Composition\InteractionRetainedUiComposition.cs:310` | GameActionIdentifyObject.cs | ACE reads `u32 objectGuid`. Match. | +| 0x00CD | GiveObjectRequest | BUILT+WIRED | `src\AcDream.Core.Net\Messages\InventoryActions.cs:90` | `src\AcDream.App\Composition\InteractionRetainedUiComposition.cs:317` | GameActionGiveObjectRequest.cs | ACE reads `u32 targetGuid`, `u32 objectGuid`, `i32 amount`. Match. | +| 0x00D6 | AdvocateTeleport | MISSING | — | — | GameActionAdvocateTeleport.cs | ACE reads `String16L target` + a Position. | +| 0x0140 | AbuseLogRequest | MISSING | — | — | *(none — ACE has no handler)* | ACE declares the enum value but ships no `[GameAction]` handler; sending it is a no-op server-side. | +| 0x0145 | AddChannel | BUILDER-ONLY | `src\AcDream.Core.Net\Messages\SocialActions.cs:147` | no caller found (only `tests\...\Messages\SocialActionsTests.cs:115`) | GameActionAddChannel.cs | Dead **and wrong** (F4): ACE reads `u32 Channel` id; builder writes a `String16L` channel *name*. | +| 0x0146 | RemoveChannel | BUILDER-ONLY | `src\AcDream.Core.Net\Messages\SocialActions.cs:151` | no caller found (tests only) | GameActionRemoveChannel.cs | Same `u32` vs `String16L` mismatch as 0x0145 (F4). | +| 0x0147 | ChatChannel | BUILT+WIRED | `src\AcDream.Core.Net\Messages\ChatRequests.cs:89` | `src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:987`; `src\AcDream.App\Net\LiveSessionCommandRouter.cs:281` | GameActionChatChannel.cs | ACE reads `u32 Channel`, `String16L message`. Match. | +| 0x0148 | ListChannels | MISSING | — | — | GameActionChannelList.cs | ACE reads `u32 Channel`. | +| 0x0149 | IndexChannels | MISSING | — | — | GameActionChannelIndex.cs | Parameterless. | +| 0x0195 | NoLongerViewingContents | BUILT+WIRED | `src\AcDream.Core.Net\Messages\InventoryActions.cs:167` | `src\AcDream.App\Composition\InteractionRetainedUiComposition.cs:296` | GameActionNoLongerViewingContents.cs | ACE reads `u32 objectGuid`. Match. | +| 0x019B | StackableSplitToWield | BUILDER-ONLY | `src\AcDream.Core.Net\Messages\InventoryActions.cs:76` | no caller found (only `tests\...\Messages\InventoryActionsTests.cs:56`) | GameActionStackableSplitToWield.cs | Layout correct (`u32 stack`, `i32 EquipMask`, `i32 amount`) but no `WorldSession` wrapper and no production caller. Dead builder. | +| 0x019C | AddShortCut | BUILT+WIRED | `src\AcDream.Core.Net\Messages\InventoryActions.cs:108` | `src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:442` | GameActionAddShortcut.cs | ACE `ReadShortcut()` = `u32 Index`, `u32 ObjectId`, then `LayeredSpell` = `u16 SpellId` + `u16 Layer`. Builder writes `i32 Index`, `u32 ObjectId`, `u32 SpellId` — 12 bytes either way; the third dword is only correct if `ShortcutEntry.SpellId` really is the packed spell+layer word its comment claims (F7). | +| 0x019D | RemoveShortCut | BUILT+WIRED | `src\AcDream.Core.Net\Messages\InventoryActions.cs:121` | `src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:463` | GameActionRemoveShortcut.cs | ACE reads `u32 index`. Match. | +| 0x01A1 | SetCharacterOptions | BUILT+PARTIAL | `src\AcDream.Core.Net\Messages\SocialActions.cs:136` | `src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:660` | GameActionSetCharacterOptions.cs | **Severe (F1).** ACE reads a whole `PlayerModule`: `u32 flags`, `i32 characterOptions1`, optional shortcut list, `u32 numTab1Spells` + list, optional spell tabs, desired components, `u32 spellbookFilters`, `i32 characterOptions2`, `String16L`, generic qualities, gameplay-options blob (handler lines 45-189). Builder writes **one** `u32`, which ACE consumes as `flags` — not as the options bitmap — then under-runs the payload on the very next `ReadInt32()`. | +| 0x01A8 | RemoveSpellC2S | BUILT+WIRED | `src\AcDream.Core.Net\Messages\ClientCommandRequests.cs:210` | `src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:554` | GameActionMagicRemoveSpellId.cs | ACE reads `u32 spellId`. Match. | +| 0x01B7 | CancelAttack | BUILT+WIRED | `src\AcDream.Core.Net\Messages\AttackTargetRequest.cs:73` | `src\AcDream.Runtime\Gameplay\RuntimeCombatAttackState.cs:267`; `src\AcDream.Headless\Hosting\HeadlessGameplayOperations.cs:113` | GameActionCancelAttack.cs | Parameterless on both sides. Match. | +| 0x01BF | QueryHealth | BUILT+WIRED | `src\AcDream.Core.Net\Messages\SocialActions.cs:50` | `src\AcDream.App\Composition\InteractionRetainedUiComposition.cs:646` | GameActionQueryHealth.cs | ACE reads `u32 objectGuid`. Match. | +| 0x01C2 | QueryAge | BUILT+PARTIAL | `src\AcDream.Core.Net\Messages\ClientCommandRequests.cs:66` (→ `:259`) | `src\AcDream.App\Net\LiveSessionRuntimeFactory.cs:308` → `src\AcDream.App\UI\ClientCommandController.cs:104` | GameActionQueryAge.cs | **Latent type bug (F5).** ACE reads a `String16L target`; builder writes `u32 objectId`. Works today only because every caller passes the default `0`, whose 4 zero bytes are byte-identical to an empty String16L. Any non-zero id becomes a bogus 16-bit string length. | +| 0x01C4 | QueryBirth | BUILT+PARTIAL | `src\AcDream.Core.Net\Messages\ClientCommandRequests.cs:69` (→ `:259`) | `src\AcDream.App\Net\LiveSessionRuntimeFactory.cs:309` → `src\AcDream.App\UI\ClientCommandController.cs:107` | GameActionQueryBirth.cs | Same `u32`-instead-of-`String16L` issue as 0x01C2 (F5). | +| 0x01DF | Emote | BUILT+WIRED | `src\AcDream.Core.Net\Messages\ClientCommandRequests.cs:102` | `src\AcDream.App\Net\LiveSessionCommandRouter.cs:326`; `src\AcDream.App\UI\ClientCommandController.cs:174` | GameActionEmote.cs | ACE reads `String16L`. Match. | +| 0x01E1 | SoulEmote | MISSING | — | — | GameActionSoulEmote.cs | ACE reads `String16L`. acdream's `SoulEmote.cs` is the **inbound** 0x01E2 event parser, not this outbound action. | +| 0x01E3 | AddSpellFavorite | BUILT+WIRED | `src\AcDream.Core.Net\Messages\ClientCommandRequests.cs:185` | `src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:487` | GameActionAddSpellFavorite.cs | ACE reads `u32 spellId`, `u32 spellBarPositionId`, `u32 spellBarId`. Builder writes spellId/position/tabIndex as u32/i32/i32 — same widths and order. Match. | +| 0x01E4 | RemoveSpellFavorite | BUILT+WIRED | `src\AcDream.Core.Net\Messages\ClientCommandRequests.cs:196` | `src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:513` | GameActionRemoveSpellFavorite.cs | ACE reads `u32 spellId`, `u32 spellBarId`. Match. | +| 0x01E9 | PingRequest | BUILT+WIRED | `src\AcDream.Core.Net\Messages\SocialActions.cs:78` | `src\AcDream.App\Composition\InteractionRetainedUiComposition.cs:623` | GameActionPingRequest.cs | Parameterless. Match. | +| 0x01F6 | OpenTradeNegotiations | MISSING | — | — | GameActionOpenTradeNegotiations.cs | ACE reads `u32 tradePartnerGuid`. | +| 0x01F7 | CloseTradeNegotiations | MISSING | — | — | GameActionCloseTradeNegotiations.cs | Parameterless. | +| 0x01F8 | AddToTrade | MISSING | — | — | GameActionAddToTrade.cs | ACE reads `u32 itemGuid`, `u32 tradeSlot`. | +| 0x01FA | AcceptTrade | MISSING | — | — | GameActionAcceptTrade.cs | ACE reads `u32 partner`, `f64 tradeStamp`, `u32 status`, `u32 initiator`, `u32 initiatorAccepts`, `u32 partnerAccepts`. | +| 0x01FB | DeclineTrade | MISSING | — | — | GameActionDeclineTrade.cs | Parameterless. | +| 0x0204 | ResetTrade | MISSING | — | — | GameActionResetTrade.cs | Parameterless. | +| 0x0216 | ClearPlayerConsentList | BUILT+WIRED | `src\AcDream.Core.Net\Messages\ClientCommandRequests.cs:165` | `src\AcDream.App\Net\LiveSessionRuntimeFactory.cs:345` → `src\AcDream.App\UI\ClientCommandController.cs:293` | GameActionClearPlayerConsentList.cs | Parameterless. Match. | +| 0x0217 | DisplayPlayerConsentList | BUILT+WIRED | `src\AcDream.Core.Net\Messages\ClientCommandRequests.cs:168` | `src\AcDream.App\Net\LiveSessionRuntimeFactory.cs:344` → `src\AcDream.App\UI\ClientCommandController.cs:289` | GameActionDisplayPlayerConsentList.cs | Parameterless. Match. | +| 0x0218 | RemoveFromPlayerConsentList | BUILT+WIRED | `src\AcDream.Core.Net\Messages\ClientCommandRequests.cs:171` | `src\AcDream.App\Net\LiveSessionRuntimeFactory.cs:346` → `src\AcDream.App\UI\ClientCommandController.cs:302` | GameActionRemoveFromPlayerConsentList.cs | ACE reads `String16L playerName`. Match. | +| 0x0219 | AddPlayerPermission | MISSING | — | — | GameActionAddPlayerPermission.cs | ACE reads `String16L playerName`. | +| 0x021A | RemovePlayerPermission | MISSING | — | — | GameActionRemovePlayerPermission.cs | ACE reads `String16L playerName`. | +| 0x021C | BuyHouse | MISSING | — | — | GameActionHouseBuyHouse.cs | ACE reads `u32 slumlord`, `ListUInt32 items`. | +| 0x021E | HouseQuery | MISSING | — | — | GameActionHouseQuery.cs | Parameterless. | +| 0x021F | AbandonHouse | MISSING | — | — | GameActionHouseAbandon.cs | Parameterless. | +| 0x0221 | RentHouse | MISSING | — | — | GameActionHouseRentHouse.cs | ACE reads `u32 slumlord`, `ListUInt32 items`. | +| 0x0224 | SetDesiredComponentLevel | BUILT+WIRED | `src\AcDream.Core.Net\Messages\ClientCommandRequests.cs:175` | `src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:575`; `src\AcDream.App\Net\LiveSessionRuntimeFactory.cs:361` | GameActionSetDesiredComponentLevel.cs | ACE reads `u32 component_wcid`, `u32 amount`. Match. | +| 0x0245 | AddPermanentGuest | MISSING | — | — | GameActionHouseAddPermanentGuest.cs | ACE reads `String16L guestName`. | +| 0x0246 | RemovePermanentGuest | MISSING | — | — | GameActionHouseRemovePermanentGuest.cs | ACE reads `String16L guestName`. | +| 0x0247 | SetOpenHouseStatus | MISSING | — | — | GameActionHouseSetOpenStatus.cs | ACE reads `u32 openHouse`. | +| 0x0249 | ChangeStoragePermission | MISSING | — | — | GameActionHouseChangeStoragePermission.cs | ACE reads `String16L guestName`, `u32 hasPermission`. | +| 0x024A | BootSpecificHouseGuest | MISSING | — | — | GameActionHouseBootSpecificGuest.cs | ACE reads `String16L playerName`. | +| 0x024C | RemoveAllStoragePermission | MISSING | — | — | GameActionHouseRemoveAllStoragePermission.cs | Parameterless. | +| 0x024D | RequestFullGuestList | MISSING | — | — | GameActionHouseRequestFullGuestList.cs | Parameterless. | +| 0x0254 | SetMotd | MISSING | — | — | GameActionSetMotd.cs | ACE reads `String16L motd`. | +| 0x0255 | QueryMotd | MISSING | — | — | GameActionQueryMotd.cs | Parameterless. | +| 0x0256 | ClearMotd | MISSING | — | — | GameActionClearMotd.cs | Parameterless. | +| 0x0258 | QueryLord | MISSING | — | — | GameActionHouseQueryLord.cs | ACE reads `u32 lord` (slumlord id). | +| 0x025C | AddAllStoragePermission | MISSING | — | — | GameActionHouseAddAllStoragePermission.cs | Parameterless. | +| 0x025E | RemoveAllPermanentGuests | MISSING | — | — | GameActionHouseRemoveAllPermanentGuests.cs | Parameterless. | +| 0x025F | BootEveryone | MISSING | — | — | GameActionHouseBootEveryone.cs | Parameterless. | +| 0x0262 | TeleToHouse | BUILT+WIRED | `src\AcDream.Core.Net\Messages\ClientCommandRequests.cs:57` | `src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:185` | GameActionTeleToHouse.cs | Parameterless. Match. | +| 0x0263 | QueryItemMana | BUILT+WIRED | `src\AcDream.Core.Net\Messages\SocialActions.cs:64` | `src\AcDream.App\Composition\InteractionRetainedUiComposition.cs:647` | GameActionQueryItemMana.cs | ACE reads `u32 objectGuid`. Match (guid 0 cancels). | +| 0x0266 | SetHooksVisibility | MISSING | — | — | GameActionHouseSetHooksVisibility.cs | ACE reads `u32 visible`. | +| 0x0267 | ModifyAllegianceGuestPermission | MISSING | — | — | GameActionHouseModifyAllegianceGuestPermission.cs | ACE reads `u32 add`. | +| 0x0268 | ModifyAllegianceStoragePermission | MISSING | — | — | GameActionHouseModifyAllegianceStoragePermission.cs | ACE reads `u32 add`. | +| 0x0269 | ChessJoin | MISSING | — | — | GameActionChessJoin.cs | ACE reads `u32 boardGuid`, `i32 color`. | +| 0x026A | ChessQuit | MISSING | — | — | GameActionChessQuit.cs | Parameterless. | +| 0x026B | ChessMove | MISSING | — | — | GameActionChessMove.cs | ACE reads two `ChessPieceCoord`. | +| 0x026D | ChessMovePass | MISSING | — | — | GameActionChessMovePass.cs | Parameterless. | +| 0x026E | ChessStalemate | MISSING | — | — | GameActionChessStalemate.cs | ACE reads `i32 stalemate`. | +| 0x0270 | ListAvailableHouses | MISSING | — | — | GameActionHouseListAvailable.cs | ACE reads `u32 HouseType`. | +| 0x0275 | ConfirmationResponse | BUILT+WIRED | `src\AcDream.Core.Net\Messages\ClientCommandRequests.cs:73` | `src\AcDream.App\Composition\InteractionRetainedUiComposition.cs:705` | GameActionConfirmationResponse.cs | ACE reads `i32 confirmType`, `u32 context`, `i32 response`. Builder writes u32/u32/u32 — same widths, same order. Match. | +| 0x0277 | BreakAllegianceBoot | MISSING | — | — | GameActionBreakAllegianceBoot.cs | ACE reads `String16L playerName`, `u32 accountBoot`. | +| 0x0278 | TeleToMansion | BUILT+WIRED | `src\AcDream.Core.Net\Messages\ClientCommandRequests.cs:60` | `src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:188` | GameActionTeleToMansion.cs | Parameterless. Match. | +| 0x0279 | Suicide | BUILT+WIRED | `src\AcDream.Core.Net\Messages\ClientCommandRequests.cs:90` | `src\AcDream.App\Net\LiveSessionRuntimeFactory.cs:330` → `src\AcDream.App\UI\ClientCommandController.cs:145` | GameActionDie.cs | Parameterless. Match. | +| 0x027B | AllegianceInfoRequest | MISSING | — | — | GameActionAllegianceInfoRequest.cs | ACE reads `String16L playerName`. | +| 0x027D | CreateTinkeringTool | MISSING | — | — | GameActionCreateTinkeringTool.cs | ACE reads `u32 toolGuid`, `u32 itemCount`, then N `u32` item guids. | +| 0x0286 | SpellbookFilter | BUILT+WIRED | `src\AcDream.Core.Net\Messages\ClientCommandRequests.cs:206` | `src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:535` | GameActionSpellbookFilter.cs | ACE reads `u32 filters`. Match. | +| 0x028D | TeleToMarketPlace | BUILT+WIRED | `src\AcDream.Core.Net\Messages\ClientCommandRequests.cs:48` | `src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:182` | GameActionTeleToMarketplace.cs | Parameterless. Match. | +| 0x028F | EnterPkLite | MISSING | — | — | GameActionEnterPkLite.cs | Parameterless. | +| 0x0290 | FellowshipAssignNewLeader | MISSING | — | — | GameActionFellowshipAssignNewLeader.cs | ACE reads `u32 newLeaderID`. | +| 0x0291 | FellowshipChangeOpenness | MISSING | — | — | GameActionFellowshipChangeOpenness.cs | ACE reads `u32 isOpen`. | +| 0x02A0 | AllegianceChatBoot | MISSING | — | — | GameActionAllegianceChatBoot.cs | ACE reads `String16L playerName`, `String16L reason`. | +| 0x02A1 | AddAllegianceBan | MISSING | — | — | GameActionAddAllegianceBan.cs | ACE reads `String16L playerName`. | +| 0x02A2 | RemoveAllegianceBan | MISSING | — | — | GameActionRemoveAllegianceBan.cs | ACE reads `String16L playerName`. | +| 0x02A3 | ListAllegianceBans | MISSING | — | — | GameActionListAllegianceBans.cs | Parameterless. | +| 0x02A5 | RemoveAllegianceOfficer | MISSING | — | — | GameActionRemoveAllegianceOfficer.cs | ACE reads `String16L officerName`. | +| 0x02A6 | ListAllegianceOfficers | MISSING | — | — | GameActionListAllegianceOfficers.cs | Parameterless. | +| 0x02A7 | ClearAllegianceOfficers | MISSING | — | — | GameActionClearAllegianceOfficers.cs | Parameterless. | +| 0x02AB | RecallAllegianceHometown | MISSING | — | — | GameActionRecallAllegianceHometown.cs | Parameterless. | +| 0x02AF | QueryPluginListResponse | MISSING | — | — | *(none — ACE has no handler)* | Client-side plugin-enumeration reply; ACE declares the enum value but ships no handler. | +| 0x02B2 | QueryPluginResponse | MISSING | — | — | *(none — ACE has no handler)* | Same as 0x02AF. | +| 0x0311 | FinishBarber | MISSING | — | — | GameActionFinishBarber.cs | ACE forwards the raw payload to `Player.HandleActionFinishBarber(message)` (appearance/palette blob). | +| 0x0316 | AbandonContract | MISSING | — | — | GameActionAbandonContract.cs | ACE reads `u32 contractId`. | +| 0xF61B | Jump | BUILT+WIRED | `src\AcDream.Core.Net\Messages\JumpAction.cs:39` | `src\AcDream.Runtime\Gameplay\LocalPlayerOutboundController.cs:73` | GameActionJump.cs | **Oracle conflict, benign in practice (F6).** Builder writes retail's `JumpPack` verbatim — `f32 extent`, `vec3 velocity`, `Position` (32 B), 4×`u16` timestamps — confirmed by `docs\research\named-retail\acclient.h:54020` and `JumpPack::Pack/UnPack` @ 0x00516d10/0x00516df0. ACE's reader omits the Position and then reads `u32 objectGuid` + `u32 spellId` we never write, so ACE's four sequence values and both trailing dwords are garbage; harmless only because `Player.HandleActionJump` (Player.cs:866) uses just `Extent` and `Velocity`. | +| 0xF61C | MoveToState | BUILT+WIRED | `src\AcDream.Core.Net\Messages\MoveToState.cs:59` | `src\AcDream.Runtime\Gameplay\LocalPlayerOutboundController.cs:206` | GameActionMoveToState.cs | ACE's `Network\Motion\MoveToState.cs` reads `RawMotionState`, `Position` (cell + xyz + wxyz = 32 B), 4×`u16`, `u8 contactLongJump`, then aligns. Builder emits exactly this, including the `0x02\|0x01` trailing byte and `AlignTo4`. Match. | +| 0xF61E | DoMovementCommand | MISSING | — | — | *(none — ACE has no handler)* | ACE declares the value only; movement rides 0xF61C / 0xF753. | +| 0xF649 | TurnTo | MISSING | — | — | *(none — ACE has no handler)* | Enum-only in ACE. | +| 0xF661 | StopMovementCommand | MISSING | — | — | *(none — ACE has no handler)* | Enum-only in ACE. | +| 0xF6EA | ForceObjectDescSend | MISSING | — | — | *(none — ACE has no handler)* | Enum-only in ACE. | +| 0xF745 | ObjectCreate | N/A (server-side) | — | — | *(none)* | Server→client object creation (acdream parses it at `src\AcDream.Core.Net\Messages\CreateObject.cs:44`). Never sent by a client. | +| 0xF747 | ObjectDelete | N/A (server-side) | — | — | *(none)* | Server→client despawn (parsed at `src\AcDream.Core.Net\Messages\DeleteObject.cs:18`). | +| 0xF74C | MovementEvent | N/A (server-side) | — | — | *(none)* | Server→client `UpdateMotion` broadcast (parsed at `src\AcDream.Core.Net\Messages\UpdateMotion.cs:65`). | +| 0xF750 | ApplySoundEffect | N/A (server-side) | — | — | *(none)* | Server→client sound broadcast. | +| 0xF752 | AutonomyLevel | MISSING | — | — | *(none — ACE has no handler)* | Enum-only in ACE; retail sends it around server-control handoff. | +| 0xF753 | AutonomousPosition | BUILT+WIRED | `src\AcDream.Core.Net\Messages\AutonomousPosition.cs:47` | `src\AcDream.Runtime\Gameplay\LocalPlayerOutboundController.cs:122`, `:170` | GameActionAutonomousPosition.cs | ACE reads `Position` (32 B), 4×`u16`, `u8 lastContact`, then aligns. Builder emits exactly that. Match. | +| 0xF755 | ApplyVisualEffect | N/A (server-side) | — | — | *(none)* | Server→client effect broadcast (parsed at `src\AcDream.Core.Net\Messages\PlayPhysicsScriptType.cs:17`). | +| 0xF7C9 | JumpNonAutonomous | MISSING | — | — | *(none — ACE has no handler)* | Enum-only in ACE; retail's server-controlled jump variant. | + +#### Roll-up + +| Status | Count | +|---|---| +| BUILT+WIRED | 56 | +| BUILT+PARTIAL | 6 | +| BUILDER-ONLY | 10 | +| MISSING | 86 | +| N/A (server-side) | 5 | +| **Total rows** | **163** | + +Of the 86 MISSING rows, 9 are also unhandled by ACE (0x0140, 0x02AF, 0x02B2, +0xF61E, 0xF649, 0xF661, 0xF6EA, 0xF752, 0xF7C9), so 77 represent server-reachable +actions acdream cannot currently send. + +Coverage of the *ACE-handled, client-originated* subset (149 rows after removing +the 5 server-side and 9 ACE-unhandled entries): 56 wired + 6 partial + 10 dead = +72 with builders, 77 with none — **~48.3% builder coverage, ~37.6% fully wired**. + +#### Findings + +##### F1 — `SetCharacterOptions` (0x01A1) sends 1 dword where ACE parses a whole `PlayerModule` +`SocialActions.BuildSetCharacterOptions` (`src\AcDream.Core.Net\Messages\SocialActions.cs:136-144`) +writes a 16-byte body: envelope + one `u32 optionsBitmap`. +`GameActionSetCharacterOptions.Handle` +(`...\Actions\GameActionSetCharacterOptions.cs:45-189`) reads, in order: +`u32 flags`, `i32 characterOptions1Flag` (immediately applied via +`session.Player.SetCharacterOptions1`), conditional shortcut array, +`u32 numTab1Spells` + array, conditional 4-tab spell lists, desired-components +map, `u32 spellbookFilters`, `i32 characterOptions2Flag`, a `String16L`, a +generic-qualities block, and a trailing gameplay-options byte blob. + +Two consequences: (a) our single dword lands in `flags`, i.e. it is interpreted +as the *presence bitmask*, never as the options value; (b) the very next +`ReadInt32()` runs off the end of a 4-byte payload. The correct single-toggle +message is **0x0005 `SetSingleCharacterOption`** (`u32 option`, `u32 value`), +for which acdream has no builder at all. Live caller: +`src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:660`. + +##### F2 — `RaiseAttribute` / `RaiseVital` / `RaiseSkill` write a 64-bit `xpSpent`; the wire field is 32-bit +`CharacterActions.BuildAttrOrVital` (`src\AcDream.Core.Net\Messages\CharacterActions.cs:77-86`) +allocates 24 bytes and does +`BinaryPrimitives.WriteUInt64LittleEndian(body.AsSpan(16), xp)`. +Both independent client-side/server-side oracles use 32 bits: + +- ACE `GameActionRaiseAttribute.cs:11` — `var xpSpent = message.Payload.ReadUInt32();` + (identically in `GameActionRaiseVital.cs:12` and `GameActionRaiseSkill.cs:11`). +- holtburger `crates\holtburger-core\src\client\types.rs:99-109` — `xp_spent: u32` + for all three commands. + +Effect: the server reads only the low dword (correct by luck for values under +4 G) and leaves 4 stray bytes at the tail of the fragment. The header comments at +`CharacterActions.cs:19-21` document the intended layout as `u64`, so the comment +is wrong too. All three are live-wired +(`src\AcDream.Runtime\Session\DirectGameRuntimeCommandAdapter.cs:620/625/630`), +so this ships on the wire today whenever a player spends XP. + +##### F3 — `FellowshipCreate` (0x00A2) packs two loose bools where ACE reads one dword +`SocialActions.BuildFellowshipCreate` (`src\AcDream.Core.Net\Messages\SocialActions.cs:88-103`) +writes `String16L name`, then `body[12+len] = openness`, `body[13+len] = shareXp`, +padded to 4. `GameActionFellowshipCreate.cs:11-12` reads `String16L +fellowshipName` then a single `u32 shareXp`. ACE therefore computes +`shareXp = openness | (shareXp << 8)` — i.e. an unshared fellowship created with +`openness=true` still reads as "share XP on". Currently harmless only because the +builder is dead (F8). + +##### F4 — `AddChannel` / `RemoveChannel` (0x0145 / 0x0146) send a channel *name string*; ACE reads a channel *id dword* +`SocialActions.BuildAddChannel` / `BuildRemoveChannel` +(`src\AcDream.Core.Net\Messages\SocialActions.cs:147-152`, via `SingleString` at `:166`) +emit a `String16L`. ACE reads `(Channel)message.Payload.ReadUInt32()` +(`GameActionAddChannel.cs:10`, `GameActionRemoveChannel.cs:10`). The declaring +comments at `SocialActions.cs:46-47` assert "string16L channelName", which is the +source of the error. Both builders are dead (F8), so nothing ships today — but +these are exactly the two the tests lock in +(`tests\AcDream.Core.Net.Tests\Messages\SocialActionsTests.cs:115,126`), so the +tests currently pin the wrong shape. + +##### F5 — `QueryAge` / `QueryBirth` (0x01C2 / 0x01C4) write a `u32`; ACE reads a `String16L` +`ClientCommandRequests.BuildObjectQuery` (`src\AcDream.Core.Net\Messages\ClientCommandRequests.cs:259-267`) +appends `u32 objectId`. ACE reads `String16L target` +(`GameActionQueryAge.cs:13`, `GameActionQueryBirth.cs:13`). This only works today +because both live call sites take the `objectId = 0` default +(`ClientCommandRequests.cs:66,69`; wired at +`src\AcDream.App\Net\LiveSessionRuntimeFactory.cs:308-309`), and four zero bytes +happen to decode as an empty String16L + pad. The moment anyone passes a real +object id the low 16 bits become a string length and the parse desynchronises. + +##### F6 — `Jump` (0xF61B): acdream is retail-correct, ACE's reader is not — record the divergence +`JumpAction.Build` (`src\AcDream.Core.Net\Messages\JumpAction.cs:39-81`) writes +`f32 extent`, `vec3 velocity`, `Position` (cell + xyz + quaternion = 32 B), 4×`u16` +timestamps, aligned — a 56-byte body. That matches the retail truth in +`docs\research\named-retail\acclient.h:54020-54029` +(`struct JumpPack : PackObj { float extent; Vector3 velocity; Position position; +u16 instance_timestamp; u16 server_control_timestamp; u16 teleport_timestamp; +u16 force_position_ts; }`) and the `JumpPack::Pack` / `JumpPack::UnPack` bodies +at 0x00516d10 / 0x00516df0, both of which call `position.vtable->Pack/UnPack` +between the velocity and the timestamps and size the buffer as +`position_size + 0x18`. + +ACE's `Network\Structure\JumpPack.cs` reads extent + velocity + 4×`u16` and then +`GameActionJump.cs:12-13` reads `u32 objectGuid` and `u32 spellId` — a 32-byte +layout with no Position. Against acdream (and against retail) ACE's four sequence +numbers are actually halves of `cellId`/`position.X`, and its `objectGuid`/ +`spellId` are `position.Y`/`position.Z` bit patterns. This is currently harmless +because `Player.HandleActionJump` +(`...\ACE.Server\WorldObjects\Player.cs:866-896`) only consumes `Extent` and +`Velocity` and never touches the sequences. holtburger's `JumpData` +(`crates\holtburger-protocol\src\messages\movement\actions.rs:73-139`) agrees with +ACE, not with the retail header — worth noting so nobody "fixes" acdream toward it. + +No change recommended; this belongs in the divergence register as a +server-oracle-vs-retail note, not as an acdream defect. + +##### F7 — `AddShortcut` (0x019C) third dword is only correct if `ShortcutEntry.SpellId` is the packed spell word +`InventoryActions.BuildAddShortcut` (`src\AcDream.Core.Net\Messages\InventoryActions.cs:108-118`) +writes `i32 Index`, `u32 ObjectId`, `u32 SpellId`. ACE's `ReadShortcut` +(`...\Network\Structure\Shortcut.cs:33-42`) reads `u32 Index`, `u32 ObjectId`, +then `ReadLayeredSpell()` = `u16 SpellId` + `u16 Layer` +(`...\Network\Structure\LayeredSpell.cs:30-37`). Total width matches, so this is +only a hazard if `ShortcutEntry.SpellId` ever holds a plain spell id in all 32 +bits: ACE would then read the high half as a spell *layer*. The builder comment +claims a "raw 32-bit spell word", which would be correct — flagging it as +unverified rather than as a confirmed bug. + +##### F8 — Dead builders (BUILDER-ONLY): 10 sub-opcodes with a builder and no production caller +Each of these has a builder in `src\AcDream.Core.Net\Messages\` reachable only +from `tests\AcDream.Core.Net.Tests\`; none has a `WorldSession.SendXxx` wrapper +except where noted, and no site in `src\AcDream.Runtime\`, `src\AcDream.App\`, or +`src\AcDream.Headless\` invokes them. + +| Sub-opcode | Builder | Payload correct vs ACE? | +|---|---|---| +| 0x001D SwearAllegiance | `AllegianceRequests.cs:34` | yes | +| 0x001E BreakAllegiance | `AllegianceRequests.cs:43` | yes | +| 0x00A2 FellowshipCreate | `SocialActions.cs:88` | **no — see F3** | +| 0x00A3 FellowshipQuit | `SocialActions.cs:106` | yes | +| 0x00A4 FellowshipDismiss | `SocialActions.cs:117` | yes | +| 0x00A5 FellowshipRecruit | `SocialActions.cs:121` | yes | +| 0x00A6 FellowshipUpdateRequest | `SocialActions.cs:125` | yes | +| 0x0145 AddChannel | `SocialActions.cs:147` | **no — see F4** | +| 0x0146 RemoveChannel | `SocialActions.cs:151` | **no — see F4** | +| 0x019B StackableSplitToWield | `InventoryActions.cs:76` | yes | + +Seven of the ten are byte-correct and need only a wrapper plus a UI/command call +site to become BUILT+WIRED — the cheapest coverage wins in this family. + +##### F9 — One builder targets a sub-opcode that is not in ACE's enum at all +`InventoryActions.BuildTeleToPoi` (`src\AcDream.Core.Net\Messages\InventoryActions.cs:132`) +declares `TeleToPoiOpcode = 0x00B1` (`:25`). `0x00B1` does not appear in ACE's +163-entry `GameActionType`, and there is no `[GameAction(0x00B1)]` handler. It is +also dead (only `tests\AcDream.Core.Net.Tests\Messages\InventoryActionsTests.cs:111` +touches it). Not a row above — recorded here so it is not mistaken for coverage. + +##### F10 — `LoginComplete` (0x00A1) hardcodes GameAction sequence 0 +`GameActionLoginComplete.Build` (`src\AcDream.Core.Net\Messages\GameActionLoginComplete.cs:45-52`) +writes `0u` for the sequence rather than calling `WorldSession.NextGameActionSequence()`. +ACE's handler ignores the sequence, so this is safe against ACE, but it is the one +GameAction acdream emits that does not participate in the outbound sequence +counter — worth knowing if a stricter server or a wire-capture comparison ever +looks at monotonicity. It is sent from three sites +(`src\AcDream.Core.Net\WorldSession.cs:1269`, +`src\AcDream.Runtime\Session\RuntimeLiveEntitySessionController.cs:327`, +`src\AcDream.App\Streaming\LocalPlayerTeleportController.cs:306`). + +--- + +## 4. Drift vs the Phase-M matrix + +The 2026-06-04 catalog this audit was meant to diff against does not exist +(section 0.2). The comparison below is therefore against +[`docs/research/2026-05-10-phase-m-opcode-matrix.md`](2026-05-10-phase-m-opcode-matrix.md), +11 weeks older than the missing catalog. + +**The universes are not the same size**, so raw counts are not directly +comparable. Phase-M scoped an "in-scope" subset per family; this audit takes +ACE's full enums. Both are given below. + +| Family | Phase-M universe | Phase-M "acdream today" | This audit's universe | This audit's recognized | +|---|---|---|---|---| +| Transport flags | 22 | 14 parse / 5 build | 23 | 23/23 values match ACE | +| Optional-header fields | 12 | 10 partial | 12 | all parsed in ACE's order | +| Top-level GameMessage | 51 | 21 implemented | 82 | 40 (36 parsed + 2 partial + 2 stub) | +| GameEvent (0xF7B0) | 103 | 27 parsed / 26 wired | 104 | 41 parsed-or-partial, 42 handlers | +| GameAction (0xF7B1) | 96 | 24 built / 8 live callers | 163 | 72 builders, 56 wired | + +### Drift in our favour + +Coverage roughly doubled in every family since 2026-05-10, which is what you +would expect after M3 and M4 shipped casting, inventory, combat, and the +world-interaction slices. GameAction live callers moved the most: 8 to 56. + +### Drift against us — the part worth reading + +**The matrix recorded a bug as correct.** Its row for `0x02BC +HearRangedSpeech` reads `acdream today = P+W`, `Phase M target = P+W`, with +the note *"Shouts; same parser as 0x02BB"*. That note is the bug. Ranged +speech carries an extra `f32 range` that local speech does not, so sharing the +parser meant reading the range float's bits as the chat type. The matrix +marked the row complete and moved on. Fixed in `7e95c45e`. + +This is the strongest argument for the mission's ground-in-source rule. A +status column is a claim about code at a moment in time; it decays, and worse, +it can be wrong on the day it is written. Three of this audit's confirmed bugs +sat in rows the matrix had already marked done. + +**Two further rows the matrix over-credits:** + +- `0x02EB CommunicationTransientString` — the matrix lists the GameEvent as + covered; it was dropping 100% of messages. Fixed in `f416c577`. +- `RaiseAttribute` / `RaiseVital` / `RaiseSkill` — counted among the built + actions; all three had the wrong field width. Fixed in `f57db35c`. + +**Unresolved from the matrix.** Its section 1 transport rows still list four +inbound parse gaps that remain open: `Disconnect (0x8000)` inbound teardown, +`NetError (0x100000)` / `NetErrorDisconnect (0x200000)`, and the +`EchoRequest (0x2000000)` mirror-response builder. This audit confirms the +flag *values* are correct but did not re-verify those four behaviours; they +are carried in section 7. + +--- + +## 5. Tests added + +All test work landed in `AcDream.Core.Net.Tests`, which goes **600 to 659 +(+59)**, zero skips. + +| Commit | What | Tests | +|---|---|---| +| `d5b0765e` | `AceWireWriter` harness + `VectorUpdate` (0xF74E) and `PlayScriptId`/`PlayEffect` (0xF754/0xF755) golden tests | +17 | +| `7e95c45e` | `HearSpeech`/`HearRangedSpeech` (0x02BB/0x02BC) golden tests | +13 | +| `61193643` | `PacketHeaderFlags` exhaustive value conformance vs ACE | +24 | +| `f416c577` | `CommunicationTransientString` corrected round-trip + padding-residue sweep | +1 | +| `f57db35c` | `RaiseAttribute` corrected width + `RaiseVital`/`RaiseSkill` theory | +4 | + +### The harness is the durable part + +`tests/AcDream.Core.Net.Tests/Messages/AceWireWriter.cs` is a line-for-line +mirror of ACE's `Source/ACE.Server/Network/Extensions.cs` writers — +`WriteString16L` (including its pad-to-a-multiple-of-4-counting-the-length-bytes +rule), `WritePackedDword`, `WriteGuid`, `Align`, `Pad` — with each primitive +citing the ACE line it ports. It also exposes `GameMessage(opcode)` and +`GameEvent(guid, seq, type)` constructors, the latter mirroring +`GameEvent/GameEventMessage.cs:21-25`. + +This matters more than the individual tests. A hand-typed hex fixture proves +only that a parser agrees with whoever typed the hex; if the author misread +the oracle, the test cements the misreading. That is exactly how the +`HearRangedSpeech`, `TransientString`, and `xpSpent` bugs each survived — +**every one of them had a passing test built on the same wrong assumption as +the parser.** Generating fixtures from the server's own algorithm removes that +failure mode. Future message families should extend this harness rather than +hand-rolling bytes. + +Three pre-existing tests were **corrected, not deleted**, each with a comment +explaining what it used to assert and why that was wrong. + +--- + +## 6. Parser changes + +**No new parsers were written for MISSING or STUB opcodes.** The mission's bar +was (a) observed in the corpus AND (b) fully specified by two oracles. +Criterion (a) is unsatisfiable against this corpus (section 2), so the bar was +not met by anything. That is the correct outcome, not a shortfall — and it is +why section 7's first item is the instrumented re-run. + +What did change is three **confirmed decode/encode bugs in already-covered +messages**, each specified by at least two oracles and each verified by hand +against primary source before the fix: + +| Commit | Message | Bug | Oracles | +|---|---|---|---| +| `7e95c45e` | `HearRangedSpeech` 0x02BC | Shared 0x02BB's layout, so the `f32 range` field was read as `chatType` and the real chat type was never read | ACE `GameMessageHearRangedSpeech.cs`; holtburger `chat/types.rs` `HearRangedSpeechData` | +| `f416c577` | `CommunicationTransientString` 0x02EB | Required a trailing `u32 chatType` the server never sends; because the string is 4-byte padded, the guard tripped and **every** transient string was dropped | ACE `GameEventCommunicationTransientString.cs`; retail `Handle_Communication__TransientString` at `0x0057d460`; holtburger | +| `f57db35c` | `RaiseAttribute`/`Vital`/`Skill` 0x0045/0x0044/0x0046 | Wrote a 64-bit `xpSpent`, making a 24-byte action where the server expects 20 | ACE `GameActionRaiseAttribute.cs:11`; holtburger `RaiseAttributeData.xp_spent: u32` | + +Severity honestly stated: only the transient-string bug had continuous +user-visible effect. The ranged-speech chat type is not consumed downstream +today, and ACE reads the low dword of `xpSpent` and ignores the tail, so the +raise actions worked by luck about value ranges. All three were nonetheless +wire divergences with passing tests defending them. + +### Deliberately not fixed + +`SetCharacterOptions` (0x01A1) is a fourth confirmed live bug — acdream sends +one dword where ACE parses an entire `PlayerModule`, and the correct +single-toggle message is `0x0005 SetSingleCharacterOption`, for which there is +no builder. It is left open because the fix is a new builder plus a caller +change in `DirectGameRuntimeCommandAdapter`, not a field-width correction, and +it deserves its own slice. See section 7. + +--- + +## 7. Open questions + +Ordered by leverage. + +1. **Re-run one connected session with `ACDREAM_DUMP_OPCODES=1`.** This is the + cheapest high-value action available and it unblocks the ranking this audit + could not produce (section 2). One environment variable on an otherwise + normal Coldeve session converts "128 MISSING rows, unknown priority" into a + real work order. User-gated: it needs a connected client. + +2. **Restore `references/`** (section 0.1). Three of the six documented oracles + are empty directories in the main checkout, and CLAUDE.md's "cross-reference + at least two of them" is currently unexecutable as written. Working copies + exist at `C:\Users\erikn\source\repos\client\ACE` and + `C:\Users\erikn\source\repos\holtburger`; + **Chorizite.ACProtocol was not found anywhere on disk** and may need + re-cloning. Until this is fixed every session re-solves it privately — four + duplicate ACE copies under `%TEMP%` are the evidence. + +3. **Fix the dangling `MEMORY.md` research links** (section 0.2). The wire + catalog, and by the same pattern the 2026-06-04 combat-math and + property-enum drops, are indexed but absent. Either restore them or mark the + index entries as lost — a link that silently resolves to nothing is worse + than no link, because a future session will plan around a document that does + not exist. + +4. **`SetCharacterOptions` (0x01A1) is wrong on the wire today.** Needs a + `0x0005 SetSingleCharacterOption` builder (`u32 option, u32 value`) and a + caller change. Confirmed by hand against ACE's handler. + +5. **`UpdateObject` (0xF7DB) is dropped.** ACE's `SerializeUpdateObject` just + calls `SerializeCreateObject`, so the body is byte-identical to one we + already parse — 9 ACE send sites (tailoring, recipes, aetheria) are silently + lost. A comment at `WorldSession.cs:662` claims coverage that does not + exist. Cheapest MISSING row on the board. + +6. **`CharacterError` (0xF659) is discarded.** ACE terminates sessions with an + explicit reason code, including `CharacterError.Logon` for account-in-use + (`AuthenticationHandler.cs:160/189/192`). We recognize none of them and die + on a generic `TimeoutException("CharacterList not received")`. This does not + change the root cause of the documented "exit 29 after a hard kill" wait — + that is still ACE's session-cleanup delay — but it means the server is + telling us why and we throw it away. Decoding one `u32` turns a three-minute + guessing game into a log line. + +7. **Narrow the `xpSpent` chain to `uint` end to end.** `f57db35c` fixed the + wire; the API still carries `ulong` from `CharacterSheetProvider` through + `LiveSessionCommandRouter` to `WorldSession`. Cosmetic, but it is the + surface that allowed the mismatch. + +8. **Ten dead GameAction builders**, seven of which are already byte-correct + and need only a wrapper plus a call site: `SwearAllegiance` (0x001D), + `BreakAllegiance` (0x001E), the four Fellowship Quit/Dismiss/Recruit/Update + (0x00A3-0x00A6), and `StackableSplitToWield` (0x019B). The other three + (`FellowshipCreate` 0x00A2, `AddChannel` 0x0145, `RemoveChannel` 0x0146) + have wrong payloads *and* tests pinning the wrong shape. + +9. **GameEvent enum hygiene.** One missing alias (`VendorInfoEvent = 0x0062`), + five name mismatches vs ACE (`AllegianceAllegianceUpdateDone`, and the four + `MiscPortalStorm*`), and a stale doc comment at `GameEventType.cs:4` + claiming "All 94" for a 103-entry enum while citing the now-empty + `references/ACE/...` path. Values are all correct; only names and docs drift. + +10. **Unverified by this audit, carried from the Phase-M matrix**: inbound + `Disconnect (0x8000)` teardown, `NetError` / `NetErrorDisconnect` + (0x100000 / 0x200000) parsing, and the `EchoRequest (0x2000000)` + mirror-response builder. Flag values are confirmed correct; the behaviours + were not re-checked. + +11. **Open questions the agents raised but could not settle**, carried for + whoever picks them up: + - `AddShortcut` (0x019C): the third dword is correct only if + `ShortcutEntry.SpellId` is already the packed spell+layer word. + Unverified. + - `InventoryActions.BuildTeleToPoi` targets `0x00B1`, which is not in ACE's + `GameActionType` enum and has no handler. Where did that number come + from? + - `Jump` (0xF61B): acdream matches retail's `JumpPack` decomp exactly, but + ACE's reader omits the Position and reads two phantom dwords. Benign + today because ACE only consumes Extent/Velocity — but it is a divergence + between our two oracles and belongs in the divergence register rather + than being silently tolerated. + - `SquelchInfo` filters: acdream implements retail's `vlong` bit-vector + correctly; ACE's 4x duplicate mask yields phantom message-type ids + 32-127. Another oracle-vs-oracle disagreement where we currently follow + retail.