From c7021d8645a47ce087e4846b8ad680e69796a43f Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 10 May 2026 19:22:49 +0200 Subject: [PATCH] docs(phase-m): sharpen Phase M into design spec + opcode coverage matrix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Captures Phase M (Network Stack Conformance) as a fully-formed phase ready to be picked up later. Three deliverables: 1. Design spec at docs/superpowers/specs/2026-05-10-phase-m-network-stack-design.md (~700 lines, 8 sections): - Bar C completeness target ("wireable on demand"): every wire opcode a 2013 EoR retail client receives or sends gets a parser/builder + golden-vector test + typed event in the new layered stack. - Three-layer architecture: INetTransport / IReliableSession / IGameProtocol, with WorldSession as a thin behavior consumer. Concrete C# interface signatures, sub-component decomposition. - Worktree-branch big-bang migration on claude/phase-m-network-stack; weekly rebase cadence; single --no-ff merge ships the phase. - Per-sub-phase entry/exit gates, conformance test plan (golden vectors + live capture replay + live ACE smoke), 10-row risk register, scope- cut order if calendar compresses. - Cost: 256 hours / ~6.4 weeks single-developer; 4-6 weeks calendar with subagent parallelization on M.1 + M.6. 2. Opcode coverage matrix at docs/research/2026-05-10-phase-m-opcode-matrix.md (~284 rows across 5 sections): - Section 1: 22 transport flags (14 implemented). - Section 2: 12 optional-header fields (10 partial). - Section 3: 51 top-level GameMessages (21 implemented). - Section 4: 103 GameEvent sub-opcodes inside 0xF7B0 (27 parsed, 26 wired). - Section 5: 96 GameAction sub-opcodes inside 0xF7B1 (24 built, 8 with live callers). - Roll-up: ~34% complete by raw opcode count. Biggest single unblocking step is wiring the 16 dead builders in section 5 (Phase B.4 surface — Use / UseWithTarget / Allegiance / Inventory / Social / Cast / Appraise). - Sources cited per row: holtburger (629695a), ACE, named retail decomp, acdream current state. - Produced by 4 parallel research agents (one per class). Spot-check pass owed before M.1 closes. 3. Roadmap update: Phase M section trimmed to summary + status + pointer to the spec; the previously-tracked M.0 Tier 1 quick-wins are folded into M.3 / M.4 / M.6 per the spec; M.1 retained as the matrix construction sub-lane with status note. Why this shape: the user goal is a complete, layered, testable network stack that can be wired in as gameplay phases need it — independent of whether each opcode is yet hooked to game state. The matrix is the source of truth for "done"; the spec is the architecture the matrix implements against; the roadmap is the index that points at both. Decisions captured during the design discussion (in case they need revisiting): - Bar C ("wireable on demand") chosen over Bar A (holtburger parity) or Bar B (named-retail completeness). - Three layers (INetTransport / IReliableSession / IGameProtocol) chosen over holtburger's two-layer split. - Big-bang on a feature branch (worktree) chosen over strangler pattern; preserves live-ACE testing on main throughout the phase. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/plans/2026-04-11-roadmap.md | 79 +- .../2026-05-10-phase-m-opcode-matrix.md | 543 ++++++++++++ ...2026-05-10-phase-m-network-stack-design.md | 786 ++++++++++++++++++ 3 files changed, 1360 insertions(+), 48 deletions(-) create mode 100644 docs/research/2026-05-10-phase-m-opcode-matrix.md create mode 100644 docs/superpowers/specs/2026-05-10-phase-m-network-stack-design.md diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md index e83b5a6..2478aa4 100644 --- a/docs/plans/2026-04-11-roadmap.md +++ b/docs/plans/2026-04-11-roadmap.md @@ -432,57 +432,40 @@ EchoRequest/EchoResponse handling, runtime ping/timeout policy, and a typed protocol/action layer. These gaps will become expensive as movement, dungeons, inventory, combat, and plugins depend on stable packet semantics. -**Plan of record:** create -`docs/superpowers/specs/2026-05-02-network-stack-conformance.md` before -implementation starts. Treat holtburger as the client-behavior oracle for this -phase; cross-check wire details against named retail, ACE, Chorizite, and AC2D -before porting. +**Plan of record:** Detailed design spec at +[`docs/superpowers/specs/2026-05-10-phase-m-network-stack-design.md`](../superpowers/specs/2026-05-10-phase-m-network-stack-design.md) +(supersedes the planned-but-never-written `2026-05-02-network-stack-conformance.md` +the original entry referenced). The spec defines: **Bar C** ("wireable on demand") +as the completeness target; a **three-layer architecture** (`INetTransport` / +`IReliableSession` / `IGameProtocol`) with `WorldSession` as a thin behavior +consumer on top; a **worktree-branch big-bang** migration model on +`claude/phase-m-network-stack` with weekly rebase cadence and single-merge ship; +per-sub-phase entry/exit gates with hour estimates; conformance test plan +(golden vectors + live capture replay + live ACE smoke); risk register; and a +**256-hour / ~6.4-week single-developer cost estimate** (4–6 weeks calendar +with subagent parallelization on M.1 and M.6). Treat holtburger as the +client-behavior oracle, ACE as server-outbound authority, named retail decomp +as wire-format ground truth. **2026-05-10 update:** holtburger pulled to `629695a` (+237 commits since -last audit). First parity-pass written to -[`docs/research/2026-05-10-holtburger-network-stack-study.md`](../research/2026-05-10-holtburger-network-stack-study.md) -— that doc is the M.1 deliverable in draft form. Study identified six -high-ROI "Tier 1" fixes that are individually small and can ship as a -focused pre-pass before the bigger M.1-M.8 lift; tracked as **M.0** below. -Most relevant recent holtburger commits to consult: `99974cc` (session -crate split + retransmit core), `403bc98` (port-switch race), `336cbad` -(turning + locomotion fix), `797aece` (disconnect carries client_id). +last audit). First parity-pass at +[`docs/research/2026-05-10-holtburger-network-stack-study.md`](../research/2026-05-10-holtburger-network-stack-study.md); +formal opcode coverage matrix (M.1's main deliverable) under construction +at `docs/research/2026-05-10-phase-m-opcode-matrix.md` via parallel +class-by-class agent dispatch. Most relevant recent holtburger commits: +`99974cc` (session crate split + retransmit core), `403bc98` (port-switch +race), `336cbad` (turning + locomotion fix), `797aece` (disconnect +carries client_id). Six "Tier 1" quick-wins identified by the study +(originally tracked as M.0) are folded into M.3 / M.4 / M.6 per the +spec — they no longer ship as a separate sub-phase. -**Sub-lanes:** -- **M.0 — Tier 1 quick-win polish pre-pass.** Six small, high-confidence - fixes that don't require the full M.1-M.8 layer extraction and can ship - as one focused PR (~1 day). Sourced from - [`docs/research/2026-05-10-holtburger-network-stack-study.md`](../research/2026-05-10-holtburger-network-stack-study.md) - §1 Tier 1. May ship independently of M.1-M.8. - 1. **MoveToState wire-format audit** (study §1.1.a-e). Side-by-side - compare `Messages/MoveToState.cs` against holtburger - `client/movement/common.rs:122-186`. Pin: `current_hold_key` always - set, empty `commands[]` on held WASD, `turn_speed` always with - TURN_COMMAND, gait-aware dedup, no `turning` when locomotion ≠ 0. - Likely candidate for the longstanding "remote retail observer sees - us not perfect" bug. - 2. **LoginComplete on every PlayerTeleport** (study §1.2). Currently - only sent on first PlayerCreate. - 3. **EchoRequest → EchoResponse reply** (study §1.3). We parse and - ignore; ACE pings periodically — likely contributor to long-session - timeouts. - 4. **Port-switch race fix** (study §1.4, holtburger commit `403bc98`). - Track pending vs confirmed `_connectEndpoint`. - 5. **Disconnect packet carries client_id** (study §4, holtburger commit - `797aece`). Currently `id = 0`. - 6. **Verify `IsaacRandom` has search-and-stash mode for out-of-order - ENCRYPTED_CHECKSUM packets** (study §1.7, holtburger - `crypto.rs:73-93`). 5-minute check; ~20 LOC port if missing — - latent bug under any UDP reorder event. -- **M.1 — Audit & parity map.** Produce a source-by-source comparison of - acdream `AcDream.Core.Net` and holtburger `holtburger-session`, - `holtburger-protocol`, and `holtburger-core` networking code. Inventory each - packet flag, optional header, session transition, control packet, fragment - path, game message, and game action. Mark each as `parity`, `partial`, - `missing`, or `intentional divergence`. **Status (2026-05-10): first pass - done at [`docs/research/2026-05-10-holtburger-network-stack-study.md`](../research/2026-05-10-holtburger-network-stack-study.md); - the formal parity table can extend that doc rather than start from - scratch.** +**Sub-lanes:** *(brief summary; the spec has full entry/exit criteria, +conformance gates, and hour estimates for each.)* +- **M.1 — Audit & opcode matrix.** Build the per-opcode coverage table + citing holtburger / ACE / named retail / acdream-today / Phase M target. + Status: parity-pass done; matrix construction in flight via per-class + agent dispatch (transport flags + optional headers, GameMessages, + GameEvents, GameActions). 16h. - **M.2 — Layer extraction.** Split the low-level stack under `WorldSession` into testable components: `INetTransport`, `PacketCodec`, `ReliablePacketSession`, `FragmentSession`, `GameMessageSession`, and the diff --git a/docs/research/2026-05-10-phase-m-opcode-matrix.md b/docs/research/2026-05-10-phase-m-opcode-matrix.md new file mode 100644 index 0000000..2e28b5c --- /dev/null +++ b/docs/research/2026-05-10-phase-m-opcode-matrix.md @@ -0,0 +1,543 @@ +# Phase M — Network Opcode Coverage Matrix + +**Date:** 2026-05-10 +**Status:** Initial population complete (4 parallel research agents). Spot-check pass + intentional-divergence ratification owed before M.1 closes. +**Companion spec:** [`docs/superpowers/specs/2026-05-10-phase-m-network-stack-design.md`](../superpowers/specs/2026-05-10-phase-m-network-stack-design.md) +**Companion study:** [`docs/research/2026-05-10-holtburger-network-stack-study.md`](2026-05-10-holtburger-network-stack-study.md) + +This matrix is the **source of truth for Phase M completeness**. Every row defines: what the opcode is, who currently sends/receives it across our three reference sources, what acdream does today, and what Phase M must do. The spec's M.6 work plan reduces to "for every row where `acdream today` ≠ `Phase M target`, implement the delta and add tests." + +--- + +## Roll-up + +| Section | In-scope | Acdream today | Phase M delta | +|---------|----------|---------------|---------------| +| 1 — Transport flags | 22 | 14 parse / 5 build | 8 | +| 2 — Optional-header fields | 12 | 10 partial | builder + decoder gaps | +| 3 — GameMessage opcodes (top-level) | 51 | 21 implemented | 30 | +| 4 — GameEvent sub-opcodes (inside 0xF7B0) | 103 | 27 parsed / 26 wired | 76 new (~50 deferable to gameplay phases) | +| 5 — GameAction sub-opcodes (inside 0xF7B1) | 96 | 24 built / 8 live callers | 72 new + 16 dead-builder wirings | +| **Total** | **~284** | **~96** | **~190** | + +Roughly **34% complete by raw opcode count.** The biggest single Phase-M unblocking step is wiring the 16 dead builders in section 5 (Phase B.4 surface — Use / UseWithTarget / Allegiance / Inventory / Social / Cast / Appraise / etc.). + +--- + +## Cell-value vocabulary + +| Code | Meaning | +|------|---------| +| `P` | Parses inbound | +| `B` | Builds outbound | +| `PB` | Parses + builds (both directions) | +| `W` | Wired — typed handler exists AND state is updated by it | +| `H` | (ACE only) Server has a handler that processes this client-sent opcode | +| `–` | Not implemented | +| `N/A` | Not applicable for this side (e.g., server-only message in ACE column) | +| `?` | Could not determine — needs verification | + +**Phase M target column:** + +| Target | Meaning | +|--------|---------| +| `PB+W` | Must parse, build (if outbound), wire to typed event by phase end | +| `PB` | Must parse + build, no wiring required | +| `P+W` | Inbound only, must parse + dispatch typed event | +| `B+W` | Outbound only, must build + have a live caller | +| `B` | Build only, no live caller required (typed for future use) | +| `–defer:` | Explicitly deferred to a named gameplay phase | +| `–skip:` | Out of scope, with justification | + +--- + +## Section 1 — Transport flags + +In-scope: 22. Implemented in acdream: 14 (parse path + 5 build path). Phase M target delta: 8 (4 inbound parse gaps to wire, 4 outbound builders, plus 6 to retire/skip-justify). + +| Code | Direction | Name | Named-retail symbol | Holtburger | ACE | acdream today | Phase M target | Notes | +|---|---|---|---|---|---|---|---|---| +| `0x00000000` | N/A | None | – | N/A | N/A | N/A | N/A | Identity flag value [^t-a] | +| `0x00000001` | both | Retransmission | – | P (set on retx) | PB+W | – | PB+W | We never echo/honor this bit [^t-b] | +| `0x00000002` | both | EncryptedChecksum | `FlowQueue::EncryptChecksum` | PB | PB+W | PB+W | PB+W | Codec covers in/out + ISAAC | +| `0x00000004` | both | BlobFragments | `MessageFragment` group | PB+W | PB+W | PB+W | PB+W | Fragment list parsed/built | +| `0x00000100` | inbound | ServerSwitch | `ClientNet::HandleServerSwitch` | P (size-skip) | PB | P (size-skip) | P+W | Handler missing; just consumes 8 bytes | +| `0x00000200` | inbound | LogonServerAddr | – | – | – | – | –defer:M2 | Login-server bounce; no client logic yet [^t-c] | +| `0x00000400` | inbound | EmptyHeader1 | `CEmptyHeader<0x400,2>` | – | – | – | –skip:dead-flag | Retail struct exists, never sent | +| `0x00000800` | inbound | Referral | `ClientNet::HandleReferral` | – | B only (server) | – | –defer:M2 | Server-only path until login bounce | +| `0x00001000` | both | RequestRetransmit | `FlowQueue::TransmitNaks` | PB+W | PB+W | P (size-skip) | PB+W | NAK list parsed but ignored [^t-d] | +| `0x00002000` | both | RejectRetransmit | `FlowQueue::EnqueueEmptyAck` | P+W | PB | P (size-skip) | P+W | Inbound only (server tells us "no") | +| `0x00004000` | both | AckSequence | `FlowQueue::EnqueueAcks` | PB+W | PB+W | PB+W | PB+W | Per-packet ack pump shipped | +| `0x00008000` | both | Disconnect | `Client::Disconnect` | – | P+W | B only | P+W | Inbound parse-and-tear-down missing [^t-e] | +| `0x00010000` | outbound | LoginRequest | `ClientNet::SendLoginRequest` | B | P+W | B | B | Auth-only, parsed by server [^t-f] | +| `0x00020000` | inbound | WorldLoginRequest | `CEmptyHeader<0x20000,1>` | – | P (8 bytes) | P (size-skip) | P (size-skip) | Server-only on relay [^t-g] | +| `0x00040000` | inbound | ConnectRequest | `ClientNet::HandleConnectionRequest` | P+W | B | P+W | P+W | Handshake oracle, ISAAC seeded | +| `0x00080000` | outbound | ConnectResponse | `ClientNet::SendConnectAck` | B | P+W | B | B | 8-byte cookie echo | +| `0x00100000` | inbound | NetError | `NetError::UnPack` | – | – | – | P+W | Drop session + surface error to UI | +| `0x00200000` | inbound | NetErrorDisconnect | `NetError::UnPack` | – | P+W | – | P+W | Same parse, hard-disconnect variant | +| `0x00400000` | inbound | CICMDCommand | – | P (size-skip) | P+W | P (size-skip) | –defer:M3 | Server-debug only; not honored by retail clients | +| `0x01000000` | inbound | TimeSync | `ClientNet::HandleTimeSynch` | P+W | P+W | P+W | P+W | Drives `WorldTimeService` | +| `0x02000000` | inbound | EchoRequest | `CEchoRequestHeader::CreateFromData` | P+W (mirrors out) | P+W | P (no reply) | PB+W | Must build EchoResponse mirror [^t-h] | +| `0x04000000` | outbound | EchoResponse | – | B | B | – | B | Reply path for incoming EchoRequest | +| `0x08000000` | both | Flow | `FlowQueue::TransmitNewPackets` | P (size-skip) | P+W | P (size-skip) | –defer:M2 | Throttle hint; safe to ignore until M2 | + +**Footnotes:** + +[^t-a]: The `None=0` value isn't a wire bit, but it's in our enum so callers can default-initialize headers — keep it. +[^t-b]: ACE sets `Retransmission` when re-sending a cached packet; clients should accept it as informational. We currently treat the bit as a no-op (works because we don't dedupe on it). +[^t-c]: A login-server-side handshake step; only relevant when ACE adds login-bounce, which it doesn't today. +[^t-d]: We need to actually retransmit on inbound NAK and need to send NAKs for our own missing inbound. M3 reliability-core phase. +[^t-e]: Inbound `Disconnect` must close the session cleanly and notify upper layers; right now the connection just times out on client side too. +[^t-f]: `LoginRequest` is a server-decode case but our codec consumes it on encode for hashing. +[^t-g]: Retail server uses this for world-server entry confirmation; the holtburger ref has no parse, ACE writer-side is `Pack`. Our consumer just skips 8 bytes for hashing. +[^t-h]: Servers do periodically EchoRequest to the client; we must mirror the 4-byte client-time as an `EchoResponse` per `FlowQueue::DequeueAck` semantics. + +--- + +## Section 2 — Optional-header fields + +In-scope: 12. Implemented in acdream: 12 of 12 sized-skip; 6 of 12 surface decoded fields. Phase M target delta: needs (a) builders for the ones we only parse, (b) ConnectRequest + EchoRequest builder paths for symmetric tests, (c) golden-vector test file. + +| Code | Direction | Name | Named-retail symbol | Holtburger | ACE | acdream today | Phase M target | Notes | +|---|---|---|---|---|---|---|---|---| +| `0x100` | inbound | ServerSwitch (8 bytes) | `UCServerSwitchStruct` | P (skip) | PB | P (skip) | P (decode)+W | Decode `serverIp:u32, port:u16, pad:u16` [^o-a] | +| `0x1000` | inbound | RequestRetransmit (4+N\*4) | `FlowQueue::TransmitNaks` | PB | PB | P (parsed list) | PB+W | List stored; build path missing | +| `0x2000` | inbound | RejectRetransmit (4+N\*4) | `FlowQueue::CompileEmptyAcks` | P | PB | P (size-skip) | P (decode)+W | List currently consumed without storage | +| `0x4000` | both | AckSequence (4 bytes) | `FlowQueue::EnqueueAcks` | PB | PB | PB | PB | Stored as `AckSequence:u32` | +| `0x10000` | outbound | LoginRequest (rest of pkt) | `ClientNet::SendLoginRequest` | B | P (full) | B (via `LoginRequest.Build`) | B | Variable-length tail; raw bytes hashed | +| `0x20000` | inbound | WorldLoginRequest (8 bytes) | `CEmptyHeader<0x20000,1>` | – | P (8B peek) | P (size-skip) | P (decode)+W | Decode purpose unknown, store raw | +| `0x40000` | inbound | ConnectRequest (32 bytes) | `CConnectHeader` | P+W | B (server) | P+W | PB | We need encode path for round-trip tests | +| `0x80000` | outbound | ConnectResponse (8 bytes) | – | B | P (8B peek) | B | PB | Decode on inbound test fixtures | +| `0x400000` | inbound | CICMDCommand (8 bytes) | – | P (skip) | P (8B) | P (size-skip) | –defer:M3 | Decode + handler deferred | +| `0x1000000` | inbound | TimeSync (8 bytes) | `CTimeSyncHeader` | P+W | P+W | P+W | PB | Add build for symmetry; double LE | +| `0x2000000` | inbound | EchoRequest (4 bytes) | `CEchoRequestHeader` | P+W | P+W | P (no reply) | PB+W | Wire to `SendEchoResponse` builder | +| `0x8000000` | both | Flow (6 bytes) | `UCFlowStruct` | P (skip) | P+W | P (decode) | –defer:M2 | `FlowBytes:u32, FlowInterval:u16` decoded | + +**Footnotes:** + +[^o-a]: ServerSwitch struct layout per retail `UCServerSwitchStruct` — confirmed via named-retail symbol `?CreateFromData@?$COnePrimHeader@$0BAA@$0GA@UCServerSwitchStruct@@@@`. M3 needs the IP/port to actually re-target the socket; today we'd silently drop traffic from a relocated server. + +**Cross-cutting Phase M deliverables for sections 1+2:** + +1. **Goldens fixture file** — `tests/AcDream.Core.Net.Tests/Packets/PacketHeaderOptionalTests.cs` does not exist; only indirect coverage via `PacketCodecTests` and `ConnectRequestTests`. M needs one fixture per non-skip flag covering parse + build symmetry. +2. **Typed events** — currently the only `WorldSession`-side flag-driven event is `ServerTimeUpdated` (from `TimeSync`). Phase M target adds: `ServerSwitchRequested(ip, port)`, `ServerDisconnect(reason)`, `ServerNetError(NetErrorCode, message)`, `EchoRequested(clientTime)` (internal), `RetransmitRequested(seqs)`, `RetransmitRejected(seqs)`. +3. **`PacketHeaderOptional` storage gaps** — `RejectRetransmit` list is consumed but discarded; `WorldLoginRequest` 8-byte body is skipped; `CICMDCommand` 8-byte body is skipped; `ConnectResponse` 8-byte cookie is decoded only inside `Connect()`'s send path, not on inbound parse. M target: lift each into a typed property on `PacketHeaderOptional`. +4. **Builder-side parity** — `PacketHeaderOptional.Parse` exists; there is no `PacketHeaderOptional.Build` — every outbound flag's body bytes are hand-rolled at the call site (`SendAck`, `Connect`, `Dispose`). Phase M should add a single `Build(PacketHeaderFlags, body fields)` to mirror parse. + +--- + +## Section 3 — GameMessage opcodes (top-level) + +In-scope: 51. Implemented in acdream: 21. Phase M target delta: 30. + +| Code | Direction | Name | Named-retail symbol | Holtburger | ACE | acdream today | Phase M target | Notes | +|------|-----------|------|---------------------|------------|-----|---------------|----------------|-------| +| 0x0000 | both | None | – | PB | N/A | – | –skip:heartbeat-only | Internal/heartbeat sentinel | +| 0x0024 | inbound | InventoryRemoveObject | – | P | B | – | P+W | Out of bubble or destroyed | +| 0x0197 | inbound | SetStackSize | – | P | B | – | P+W | Container stack size delta | +| 0x019E | inbound | PlayerKilled | – | PB | B | P+W | P+W | victim+killer broadcast | +| 0x01E0 | inbound | EmoteText | `CM_Communication::DispatchUI_HearEmote` | PB | B | P+W | P+W | Server-driven 3rd-person emote | +| 0x01E2 | inbound | SoulEmote | `CM_Communication::DispatchUI_HearSoulEmote` | PB | B | P+W | P+W | Complex emote w/ animation | +| 0x02BB | inbound | HearSpeech | `ClientCommunicationSystem::Handle_Communication__HearSpeech` | PB | B | P+W | P+W | Local chat | +| 0x02BC | inbound | HearRangedSpeech | `ClientCommunicationSystem::Handle_Communication__HearRangedSpeech` | PB | B | P+W | P+W | Shouts; same parser as 0x02BB | +| 0x02CD | inbound | PrivateUpdatePropertyInt | `ClientObjMaintSystem::Handle_Qualities__PrivateUpdateInt` | PB | B | – | P+W | Owner-only int property | +| 0x02CE | inbound | PublicUpdatePropertyInt | – | PB | B | – | P+W | Broadcast int property | +| 0x02CF | inbound | PrivateUpdatePropertyInt64 | – | PB | B | – | P+W | Owner-only int64 | +| 0x02D0 | inbound | PublicUpdatePropertyInt64 | – | PB | B | – | P+W | Broadcast int64 | +| 0x02D1 | inbound | PrivateUpdatePropertyBool | `ClientObjMaintSystem::Handle_Qualities__PrivateUpdateBool` | PB | B | – | P+W | Owner-only bool | +| 0x02D2 | inbound | PublicUpdatePropertyBool | – | PB | B | – | P+W | Broadcast bool | +| 0x02D3 | inbound | PrivateUpdatePropertyFloat | `ClientObjMaintSystem::Handle_Qualities__PrivateUpdateFloat` | PB | B | – | P+W | Owner-only float | +| 0x02D4 | inbound | PublicUpdatePropertyFloat | – | PB | B | – | P+W | Broadcast float | +| 0x02D5 | inbound | PrivateUpdatePropertyString | – | PB | B | – | P+W | Owner-only string | +| 0x02D6 | inbound | PublicUpdatePropertyString | – | PB | B | – | P+W | Broadcast string | +| 0x02D7 | inbound | PrivateUpdatePropertyDataID | – | PB | B | – | P+W | Owner-only DataID | +| 0x02D8 | inbound | PublicUpdatePropertyDataID | – | PB | B | – | P+W | Broadcast DataID | +| 0x02D9 | inbound | PrivateUpdatePropertyInstanceID | `CM_Qualities::DispatchUI_PrivateUpdateInstanceID` | PB | B | – | P+W | Owner-only InstanceID | +| 0x02DA | inbound | PublicUpdateInstanceID | – | PB | B | – | P+W | Broadcast InstanceID | +| 0x02DB | inbound | PrivateUpdatePosition | `CM_Qualities::DispatchUI_PrivateUpdatePosition` | PB | B | – | –defer:F.x | Owner-only position; redundant with 0xF748 | +| 0x02DC | inbound | PublicUpdatePosition | – | PB | B | – | –defer:F.x | Public position; redundant with 0xF748 | +| 0x02DD | inbound | PrivateUpdateSkill | – | PB | B | – | P+W | Owner-only skill XP | +| 0x02DE | inbound | PublicUpdateSkill | – | PB | B | – | P+W | Public skill | +| 0x02DF | inbound | PrivateUpdateSkillLevel | – | PB | B | – | P+W | Owner-only skill base level | +| 0x02E0 | inbound | PublicUpdateSkillLevel | – | PB | B | – | P+W | Public skill base level | +| 0x02E3 | inbound | PrivateUpdateAttribute | `ClientObjMaintSystem::Handle_Qualities__PrivateUpdateAttribute` | PB | B | – | P+W | Strength/Stamina/etc base | +| 0x02E4 | inbound | PublicUpdateAttribute | – | PB | B | – | P+W | Public attribute | +| 0x02E7 | inbound | PrivateUpdateVital | – | PB | B | P+W | P+W | Max HP/Stam/Mana — vitals panel | +| 0x02E8 | inbound | PublicUpdateVital | – | PB | B | – | P+W | Public vital | +| 0x02E9 | inbound | PrivateUpdateAttribute2ndLevel | `ClientObjMaintSystem::Handle_Qualities__PrivateUpdateAttribute2ndLevel` | PB [^m-1] | B | P+W | P+W | Current-only vital delta | +| 0xEA60 | inbound | AdminEnvirons | `CPlayerSystem::Handle_Admin__Environs` | – | B | P+W | P+W | Fog presets / sound cues | +| 0xF625 | inbound | ObjDescEvent | `SmartBox::HandleObjDescEvent` | PB | B | P+W | P+W | Per-entity appearance update | +| 0xF643 | inbound | CharacterCreateResponse | – | PB | B | – | –defer:char-creation | Char-creation flow not yet built | +| 0xF653 | outbound | CharacterLogOff | – | PB | P | B | PB+W | Sent on Dispose; ACE accepts | +| 0xF655 | both | CharacterDelete | – | PB | P | – | –defer:char-mgmt | Char-management UI deferred | +| 0xF656 | outbound | CharacterCreate | – | PB | P | – | –defer:char-creation | Char-creation flow not yet built | +| 0xF657 | outbound | CharacterEnterWorld | `CM_Login::SendNotice_BeginEnterWorld` [^m-2] | PB | P | B | PB+W | Built; sent during handshake | +| 0xF658 | inbound | CharacterList | `CPlayerSystem::Handle_Login__CharacterSet` | PB | B | P+W | P+W | Login char picker | +| 0xF659 | inbound | CharacterError | `CPlayerSystem::Handle_CharacterError` | PB | B | – | P+W | Login/restore failures | +| 0xF6EA | both | ForceObjectDescSend | – | PB | P | – | –defer:F.x | Server requests client re-send ObjDesc; rare | +| 0xF745 | inbound | CreateObject (ObjectCreate) | `SmartBox::HandleCreateObject` | PB | B | P+W | P+W | Spawn entity in bubble | +| 0xF746 | inbound | PlayerCreate | `SmartBox::HandleCreatePlayer` | PB | B | P+W [^m-3] | P+W | Triggers LoginComplete | +| 0xF747 | inbound | DeleteObject (ObjectDelete) | `SmartBox::HandleDeleteObject` | PB | B | P+W | P+W | Despawn | +| 0xF748 | inbound | UpdatePosition | `CM_Qualities::DispatchUI_UpdatePosition` | PB | B | P+W | P+W | Periodic position sync | +| 0xF749 | inbound | ParentEvent | `SmartBox::HandleParentEvent` | PB | B | – | P+W | Equip/wield parent change | +| 0xF74A | inbound | PickupEvent | `SmartBox::HandlePickupEvent` | PB | B | – | P+W | Pickup confirmation | +| 0xF74B | inbound | SetState | `SmartBox::HandleSetState` | PB | B | – | P+W | Door open/close, container state | +| 0xF74C | inbound | UpdateMotion (Motion) | – | PB | B | P+W | P+W | Animation cycle change | +| 0xF74E | inbound | VectorUpdate | `SmartBox::HandleVectorUpdate` | PB | B | P+W | P+W | Remote jump velocity, missile arc | +| 0xF750 | inbound | Sound | `SmartBox::HandleSoundEvent` | PB | B | – | P+W | Positional sound trigger | +| 0xF751 | inbound | PlayerTeleport | `SmartBox::HandlePlayerTeleport` | PB | B | P+W | P+W | Portal/teleport screen | +| 0xF752 | inbound | AutonomyLevel | `CommandInterpreter::SetAutonomyLevel` | P [^m-4] | – | – | P+W | Server tells client physics-trust level | +| 0xF753 | both | AutonomousPosition | `CM_Movement::Event_AutonomousPosition` | PB | – | B | PB+W | Outbound built; inbound parser missing | +| 0xF754 | inbound | PlayScript (PlayScriptId) | `SmartBox::HandlePlayScriptID` | – | – | P+W [^m-5] | P+W | Inline parser; lightning, spell FX, emotes | +| 0xF755 | inbound | PlayEffect | – | PB | B | – | P+W | Particle/visual scripts; ACE uses for PlayScript wrapper | +| 0xF7B0 | inbound | GameEvent (envelope) | – | PB | B | P+W | P+W | Envelope for sub-opcodes (see §4) | +| 0xF7B1 | outbound | GameAction (envelope) | – | PB | P | B+W | PB+W | Envelope for sub-opcodes (see §5) | +| 0xF7C1 | inbound | AccountBanned | – | – | B | – | –defer:F.x | ACE-only, rarely seen | +| 0xF7C8 | outbound | CharacterEnterWorldRequest | – | PB | P | B | PB+W | Built; sent before 0xF657 | +| 0xF7CC | both | GetServerVersion | `Proto_UI::SendAdminGetServerVersion` | – | P | – | –defer:F.x | Admin-only | +| 0xF7CD | both | FriendsOld | – | – | P | – | –defer:F.x | Obsolete; ACE drops it | +| 0xF7D9 | outbound | CharacterRestore | – | PB | P | – | –defer:char-mgmt | Char-management UI deferred | +| 0xF7DB | inbound | UpdateObject | `SmartBox::HandleUpdateObject` | PB | B | – | P+W | Heavy re-send of object visual+physics | +| 0xF7DC | inbound | AccountBoot | `CPlayerSystem::Handle_AccountBooted` | PB | B | – | P+W | Kicked from server | +| 0xF7DE | both | TurbineChat | `CCommunicationSystem::IsUsingTurbineChat` | PB | PB [^m-6] | PB+W | PB+W | Global community chat | +| 0xF7DF | inbound | CharacterEnterWorldServerReady | – | P [^m-7] | B | P+W [^m-8] | P+W | Handshake gate during enter-world | +| 0xF7E0 | inbound | ServerMessage | – | PB | B | P+W | P+W | System message / announcements | +| 0xF7E1 | inbound | ServerName | `ECM_Login::SendNotice_WorldName` | PB | B | – | P+W | Shard name during login | +| 0xF7E2 | both | DDD_DataMessage | – | – | – | – | –defer:dat-streaming | DDD download channel (we ship dats locally) | +| 0xF7E3 | both | DDD_RequestDataMessage | – | – | P | – | –defer:dat-streaming | Client requests dat data | +| 0xF7E4 | both | DDD_ErrorMessage | – | – | – | – | –defer:dat-streaming | DDD error channel | +| 0xF7E5 | inbound | DDD_Interrogation | `DDD_InterrogationMessage::Serialize` | PB [^m-9] | B | P+W | P+W | Server asks "what dat versions?" | +| 0xF7E6 | outbound | DDD_InterrogationResponse | – | PB | P | B | PB+W | Built; sent in response to 0xF7E5 | +| 0xF7E7 | both | DDD_BeginDDD | – | – | – | – | –defer:dat-streaming | DDD start | +| 0xF7E8 | both | DDD_BeginPullDDD | – | – | – | – | –defer:dat-streaming | DDD pull start | +| 0xF7E9 | both | DDD_IterationData | – | – | – | – | –defer:dat-streaming | DDD chunk iteration | +| 0xF7EA | inbound | DDD_EndDDD | – | – | P | – | –defer:dat-streaming | DDD end signal | + +**Footnotes:** + +[^m-1]: ACE calls 0x02E9 `PrivateUpdateAttribute2ndLevel`; holtburger calls it `PrivateUpdateVitalCurrent` (current-only delta). +[^m-2]: Retail-side trigger of the enter-world flow; the wire opcode 0xF657 is constructed from the request. +[^m-3]: PlayerCreate fires LoginComplete when guid matches own char; CreateObject body is parsed for the player too. +[^m-4]: AutonomyLevel is in holtburger's `GameMessage` enum + unpack/pack, but its enum value (0xF752) is mapped via opcode dispatch. +[^m-5]: 0xF754 PlayScript is parsed inline in `WorldSession.cs:850` (no dedicated `Messages/PlayScript.cs`); routed to `PlayScriptReceived` event for VFX runtime. +[^m-6]: ACE handles inbound TurbineChat via `TurbineChatHandler` and emits outbound via `GameMessageTurbineChat`, hence both directions. +[^m-7]: CharacterEnterWorldServerReady is unit variant in holtburger (no payload); only an opcode marker. +[^m-8]: acdream uses 0xF7DF as a handshake gate (`WorldSession.cs:495`), no dedicated parser file. +[^m-9]: DddInterrogation in holtburger is a unit variant — opcode marker only, no payload to parse. + +**Caveats and unknowns:** +- `0xF7C1 AccountBanned` is in ACE's enum + has a `GameMessageAccountBanned.cs`, but holtburger has it commented out. Marked `–defer` since the channel exists in retail but rarely fires. +- `0xF7CC GetServerVersion`, `0xF7CD FriendsOld`: ACE has handlers for them (i.e. accepts them inbound from a client that sends them), but no acdream sends them today. Listed as `–defer`. +- `0xF619 PositionAndMovement`: holtburger documents this as a "ghost" opcode (defined but never emitted by ACE/retail). Excluded from the table — confirmed dead code per holtburger comment + grep on ACE shows no `Writer.Write` site. +- `0xF754 PlayScriptId` vs `0xF755 PlayEffect`: ACE has the `Script.cs` GameMessage tagged with `PlayEffect (0xF755)`, while retail's `SmartBox::HandlePlayScriptID` is the 0xF754 handler. acdream's inline parser at `WorldSession.cs:850` reads `[u32 opcode][u32 guid][u32 scriptId]` matching the 0xF754 layout. + +--- + +## Section 4 — GameEvent sub-opcodes (inside 0xF7B0 envelope) + +In-scope: 103. Implemented (parsed) in acdream today: 27. Wired (`W`) in acdream today: 26. Phase M target delta: 76 new parsers + ~50 deferred to later phases. + +All rows are `inbound` direction (GameEvents are server→client only). + +| Code | Direction | Name | Named-retail symbol | Holtburger | ACE | acdream today | Phase M target | Notes | +|---|---|---|---|---|---|---|---|---| +| 0x0003 | inbound | AllegianceUpdateAborted | `ClientAllegianceSystem::Handle_Allegiance__AllegianceUpdateAborted` | – | W | – | –defer:Allegiance | scope deferred — no allegiance UI yet | +| 0x0004 | inbound | PopupString | `ClientCommunicationSystem::Handle_Communication__PopUpString` | W | W | W | W | modal text → ChatLog.OnPopup | +| 0x0013 | inbound | PlayerDescription | `CPlayerSystem::Handle_PlayerDescription` | W | W | W | W | full local-player snapshot at login [^e-a] | +| 0x0020 | inbound | AllegianceUpdate | `ClientAllegianceSystem::Handle_Allegiance__AllegianceUpdate` | – | W | – | –defer:Allegiance | needs CAllegianceProfile parser | +| 0x0021 | inbound | FriendsListUpdate | `CM_Social::SendNotice_UpdateFriendsList` | – | W | – | P+W | FriendDataList; small parser, high UX value | +| 0x0022 | inbound | InventoryPutObjInContainer | – (CM_Inventory) | W | W | W | W | (item, container, slot) — items.MoveItem | +| 0x0023 | inbound | WieldObject | – (CM_Inventory) | W | W | W | W | server-driven equip | +| 0x0029 | inbound | CharacterTitle | `CM_Social::SendNotice_AddCharacterTitle` | – | W | – | –defer:Social | gmCharacterTitleUI | +| 0x002B | inbound | UpdateTitle | `CM_Social::SendNotice_SetDisplayCharacterTitle` | – | W | – | –defer:Social | titles UI not yet built | +| 0x0052 | inbound | CloseGroundContainer | – (gmInventoryUI) | W | W | P | P+W | parser exists, needs ItemRepository wiring | +| 0x0062 | inbound | ApproachVendor | – (CM_Vendor) | W | W | – | –defer:VendorPanel | needs VendorProfile + ItemProfile list parser | +| 0x0075 | inbound | StartBarber | `ClientUISystem::Handle_Character__StartBarber` | – | W | – | –defer:Barber | gmBarberUI not yet built | +| 0x00A0 | inbound | InventoryServerSaveFailed | – (CM_Inventory) | W | W | P | P+W | parser exists; needs revert hook | +| 0x00A3 | inbound | FellowshipQuit | `ClientFellowshipSystem::Handle_Fellowship__Quit` | W | W | – | –defer:Fellowship | scope deferred — no fellowship state | +| 0x00A4 | inbound | FellowshipDismiss | `ClientFellowshipSystem::Handle_Fellowship__Dismiss` | W | W | – | –defer:Fellowship | scope deferred | +| 0x00B4 | inbound | BookDataResponse | `CM_Writing::Event_BookData` | W | W | – | –defer:Books | gmBookUI not yet built | +| 0x00B5 | inbound | BookModifyPageResponse | `CM_Writing::Event_BookModifyPage` | – | W | – | –defer:Books | | +| 0x00B6 | inbound | BookAddPageResponse | `CM_Writing::SendNotice_BookAddPageResponse` | – | W | – | –defer:Books | | +| 0x00B7 | inbound | BookDeletePageResponse | `CM_Writing::SendNotice_BookDeletePageResponse` | – | W | – | –defer:Books | | +| 0x00B8 | inbound | BookPageDataResponse | `CM_Writing::SendNotice_BookPageDataResponse` | W | W | – | –defer:Books | | +| 0x00C3 | inbound | GetInscriptionResponse | – | – | W | – | –defer:Books | inscription on caster items | +| 0x00C9 | inbound | IdentifyObjectResponse | `ClientUISystem::Handle_Item__AppraiseDone` [^e-b] | W | W | W | W | AppraiseInfoParser feeds ItemRepository | +| 0x0147 | inbound | ChannelBroadcast | `ClientCommunicationSystem::Handle_Communication__ChannelBroadcast` | W | W | W | W | (channelId, sender, msg) → ChatLog | +| 0x0148 | inbound | ChannelList | `ClientCommunicationSystem::Handle_Communication__ChannelList` | – | W | – | P+W | PackableList; admin/list response | +| 0x0149 | inbound | ChannelIndex | `ClientCommunicationSystem::Handle_Communication__ChannelIndex` | – | W | – | P+W | PackableList | +| 0x0196 | inbound | ViewContents | `ClientUISystem::OnViewContents` | W | W | – | P+W | server view of remote container — needed for sidepacks | +| 0x019A | inbound | InventoryPutObjectIn3D | – (CM_Inventory) | W | W | P | P+W | parser exists; needs spawn-into-world wiring | +| 0x01A7 | inbound | AttackDone | – | W | W | W | W | combat seq complete | +| 0x01A8 | inbound | MagicRemoveSpell | `ClientMagicSystem::Handle_Magic__RemoveSpell` | W | W | W | W | spell removed from spellbook | +| 0x01AC | inbound | VictimNotification | `ClientCombatSystem::HandleVictimNotificationEvent` | W | W | W | W | death msg for victim | +| 0x01AD | inbound | KillerNotification | `ClientCombatSystem::HandleKillerNotificationEvent` | W | W | W | W | death msg for killer | +| 0x01B1 | inbound | AttackerNotification | `ClientCombatSystem::HandleAttackerNotificationEvent` | W | W | W | W | "you hit X" | +| 0x01B2 | inbound | DefenderNotification | `ClientCombatSystem::HandleDefenderNotificationEvent` | W | W | W | W | "X hit you" | +| 0x01B3 | inbound | EvasionAttackerNotification | `ClientCombatSystem::HandleEvasionAttackerNotificationEvent` | W | W | W | W | "X evaded" | +| 0x01B4 | inbound | EvasionDefenderNotification | `ClientCombatSystem::HandleEvasionDefenderNotificationEvent` | W | W | W | W | "you evaded X" | +| 0x01B8 | inbound | CombatCommenceAttack | – | W | W | W | W | empty payload | +| 0x01C0 | inbound | UpdateHealth | `CM_Combat::SendNotice_UpdateObjectHealth` | W | W | W | W | (guid, healthPct) → CombatState | +| 0x01C3 | inbound | QueryAgeResponse | `ClientCommunicationSystem::Handle_Character__QueryAgeResponse` | – | W | – | P | small string parser; chat panel display | +| 0x01C7 | inbound | UseDone | `ClientUISystem::Handle_Item__UseDone` | W | W | P | P+W | parser exists; needs InteractionState wiring | +| 0x01C8 | inbound | AllegianceUpdateDone | – | – | W | – | –defer:Allegiance | | +| 0x01C9 | inbound | FellowshipFellowUpdateDone | `ClientFellowshipSystem::Handle_Fellowship__FellowUpdateDone` | W | W | – | –defer:Fellowship | empty payload | +| 0x01CA | inbound | FellowshipFellowStatsDone | `ClientFellowshipSystem::Handle_Fellowship__FellowStatsDone` | W | W | – | –defer:Fellowship | empty payload | +| 0x01CB | inbound | ItemAppraiseDone | `ClientUISystem::Handle_Item__AppraiseDone` | – | W | – | P | post-IdentifyObjectResponse signal | +| 0x01E2 | inbound | Emote | `ClientCommunicationSystem::Handle_Communication__HearEmote` [^e-c] | – | W | – | P | "*X waves*" — chat broadcast | +| 0x01EA | inbound | PingResponse | `ClientUISystem::Handle_Character__ReturnPing` | W | W | P | P+W | parser exists; needs latency/heartbeat wiring | +| 0x01F4 | inbound | SetSquelchDB | `ClientCommunicationSystem::Handle_Communication__SetSquelchDB` | – | W | – | –defer:SquelchUI | SquelchDB blob; ignore-list state | +| 0x01FD | inbound | RegisterTrade | `ClientTradeSystem::Handle_Trade__Recv_RegisterTrade` | W | W | – | –defer:TradePanel | (guid, accepterGuid, ackTimer) | +| 0x01FE | inbound | OpenTrade | `ClientTradeSystem::Handle_Trade__Recv_OpenTrade` | W | W | – | –defer:TradePanel | initiator guid | +| 0x01FF | inbound | CloseTrade | `ClientTradeSystem::Handle_Trade__Recv_CloseTrade` | W | W | – | –defer:TradePanel | closer guid | +| 0x0200 | inbound | AddToTrade | `ClientTradeSystem::Handle_Trade__Recv_AddToTrade` | W | W | P | –defer:TradePanel | parser exists; needs TradeState | +| 0x0201 | inbound | RemoveFromTrade | `ClientTradeSystem::Handle_Trade__Recv_RemoveFromTrade` | – | W | – | –defer:TradePanel | (initiatorGuid, itemGuid) | +| 0x0202 | inbound | AcceptTrade | `ClientTradeSystem::Handle_Trade__Recv_AcceptTrade` | W | W | P | –defer:TradePanel | parser exists | +| 0x0203 | inbound | DeclineTrade | `ClientTradeSystem::Handle_Trade__Recv_DeclineTrade` | W | W | – | –defer:TradePanel | initiator guid | +| 0x0205 | inbound | ResetTrade | `ClientTradeSystem::Handle_Trade__Recv_ResetTrade` | W | W | – | –defer:TradePanel | reset to-trade list | +| 0x0207 | inbound | TradeFailure | `ClientTradeSystem::Handle_Trade__Recv_TradeFailure` | W | W | P | –defer:TradePanel | parser exists | +| 0x0208 | inbound | ClearTradeAcceptance | `ClientTradeSystem::Handle_Trade__Recv_ClearTradeAcceptance` | W | W | – | –defer:TradePanel | empty payload | +| 0x021D | inbound | HouseProfile | `ClientHousingSystem::Handle_House__Recv_HouseProfile` | – | W | – | –defer:Housing | HouseProfile blob | +| 0x0225 | inbound | HouseData | `ClientHousingSystem::Handle_House__Recv_HouseData` | – | W | – | –defer:Housing | HouseData blob | +| 0x0226 | inbound | HouseStatus | `ClientHousingSystem::Handle_House__Recv_HouseStatus` | – | W | – | –defer:Housing | scalar status code | +| 0x0227 | inbound | UpdateRentTime | `ClientHousingSystem::Handle_House__Recv_UpdateRentTime` | – | W | – | –defer:Housing | i32 timestamp | +| 0x0228 | inbound | UpdateRentPayment | `ClientHousingSystem::Handle_House__Recv_UpdateRentPayment` | – | W | – | –defer:Housing | HousePaymentList | +| 0x0248 | inbound | HouseUpdateRestrictions | `ClientHousingSystem::Handle_House__Recv_UpdateRestrictions` | – | W | – | –defer:Housing | RestrictionDB blob | +| 0x0257 | inbound | UpdateHAR | `ClientHousingSystem::Handle_House__Recv_UpdateHAR` | – | W | – | –defer:Housing | HAR blob | +| 0x0259 | inbound | HouseTransaction | `ClientHousingSystem::Handle_House__Recv_HouseTransaction` | – | W | – | –defer:Housing | scalar txn code | +| 0x0264 | inbound | QueryItemManaResponse | `ClientUISystem::Handle_Item__QueryItemManaResponse` | W | W | P | P+W | parser exists; needs ItemRepository wiring | +| 0x0271 | inbound | AvailableHouses | `ClientHousingSystem::Handle_House__Recv_AvailableHouses` | – | W | – | –defer:Housing | PackableList + flag | +| 0x0274 | inbound | CharacterConfirmationRequest | `ClientUISystem::Handle_Character__ConfirmationRequest` | W | W | P | P+W | parser exists; needs modal-confirm wiring | +| 0x0276 | inbound | CharacterConfirmationDone | `ClientUISystem::Handle_Character__ConfirmationDone` | W | W | – | P+W | (type, contextId); confirms client ACK | +| 0x027A | inbound | AllegianceLoginNotification | `ClientAllegianceSystem::Handle_Allegiance__AllegianceLoginNotificationEvent` | – | W | – | –defer:Allegiance | (guid, login/logout flag) | +| 0x027C | inbound | AllegianceInfoResponse | `ClientAllegianceSystem::Handle_Allegiance__AllegianceInfoResponseEvent` | – | W | – | –defer:Allegiance | CAllegianceProfile | +| 0x0281 | inbound | JoinGameResponse | `ClientMiniGameSystem::Handle_Game__Recv_JoinGameResponse` | – | W | – | –defer:MiniGame | chess/dice/etc — minimal value | +| 0x0282 | inbound | StartGame | `ClientMiniGameSystem::Handle_Game__Recv_StartGame` | W | W | – | –defer:MiniGame | empty payload | +| 0x0283 | inbound | MoveResponse | `ClientMiniGameSystem::Handle_Game__Recv_MoveResponse` | – | W | – | –defer:MiniGame | minigame move ack | +| 0x0284 | inbound | OpponentTurn | `ClientMiniGameSystem::Handle_Game__Recv_OpponentTurn` | – | W | – | –defer:MiniGame | GameMoveData blob | +| 0x0285 | inbound | OpponentStalemate | `ClientMiniGameSystem::Handle_Game__Recv_OppenentStalemateState` | – | W | – | –defer:MiniGame | typo preserved (retail name) | +| 0x028A | inbound | WeenieError | `ClientCommunicationSystem::Handle_Communication__WeenieError` | W | W | W | W | error code → ChatLog.OnWeenieError | +| 0x028B | inbound | WeenieErrorWithString | `ClientCommunicationSystem::Handle_Communication__WeenieErrorWithString` | W | W | W | W | (code, interp) → ChatLog | +| 0x028C | inbound | GameOver | `ClientMiniGameSystem::Handle_Game__Recv_GameOver` | – | W | – | –defer:MiniGame | (gameId, winner) | +| 0x0295 | inbound | SetTurbineChatChannels | `ClientCommunicationSystem::Handle_Communication__Recv_ChatRoomTracker` [^e-d] | W | W | W | W | per-room ids → TurbineChatState | +| 0x02AE | inbound | AdminQueryPluginList | – (admin tooling) | – | W | – | –skip:admin-only | server-admin path; not retail-emitted to player | +| 0x02B1 | inbound | AdminQueryPlugin | – | – | W | – | –skip:admin-only | | +| 0x02B3 | inbound | AdminQueryPluginResponse | – | – | W | – | –skip:admin-only | | +| 0x02B4 | inbound | SalvageOperationsResult | `ClientUISystem::Handle_Inventory__Recv_SalvageOperationsResultData` | – | W | – | –defer:SalvageUI | SalvageOperationsResultData blob | +| 0x02BD | inbound | Tell | – (CM_Communication) | W | W | W | W | direct whisper → ChatLog | +| 0x02BE | inbound | FellowshipFullUpdate | `ClientFellowshipSystem::Handle_Fellowship__FullUpdate` | W | W | – | –defer:Fellowship | CFellowship blob | +| 0x02BF | inbound | FellowshipDisband | `ClientFellowshipSystem::Handle_Fellowship__Disband` | W | W | – | –defer:Fellowship | empty payload | +| 0x02C0 | inbound | FellowshipUpdateFellow | `ClientFellowshipSystem::Handle_Fellowship__UpdateFellow` | W | W | – | –defer:Fellowship | (memberGuid, Fellow, flag) | +| 0x02C1 | inbound | MagicUpdateSpell | `ClientMagicSystem::Handle_Magic__UpdateSpell` | W | W | W | W | learned spellId → Spellbook | +| 0x02C2 | inbound | MagicUpdateEnchantment | `ClientMagicSystem::Handle_Magic__UpdateEnchantment` | W | W | W | W | Enchantment blob → Spellbook | +| 0x02C3 | inbound | MagicRemoveEnchantment | `ClientMagicSystem::Handle_Magic__RemoveEnchantment` | W | W | W | W | (layerId, spellId) | +| 0x02C4 | inbound | MagicUpdateMultipleEnchantments | `ClientMagicSystem::Handle_Magic__UpdateMultipleEnchantments` | W | W | – | P+W | PackableList | +| 0x02C5 | inbound | MagicRemoveMultipleEnchantments | `ClientMagicSystem::Handle_Magic__RemoveMultipleEnchantments` | W | W | – | P+W | PackableList | +| 0x02C6 | inbound | MagicPurgeEnchantments | `ClientMagicSystem::Handle_Magic__PurgeEnchantments` | W | W | W | W | empty payload → Spellbook.OnPurgeAll | +| 0x02C7 | inbound | MagicDispelEnchantment | `ClientMagicSystem::Handle_Magic__DispelEnchantment` | W | W | W | W | shared parser w/ MagicRemoveEnchantment | +| 0x02C8 | inbound | MagicDispelMultipleEnchantments | `ClientMagicSystem::Handle_Magic__DispelMultipleEnchantments` | W | W | – | P+W | PackableList | +| 0x02C9 | inbound | PortalStormBrewing | `ClientUISystem::Handle_Misc__PortalStormBrewing` | – | W | – | P+W | float intensity → ChatLog system message | +| 0x02CA | inbound | PortalStormImminent | `ClientUISystem::Handle_Misc__PortalStormImminent` | – | W | – | P+W | float intensity | +| 0x02CB | inbound | PortalStorm | `ClientUISystem::Handle_Misc__PortalStorm` | – | W | – | P+W | empty payload — actual storm trigger | +| 0x02CC | inbound | PortalStormSubsided | `ClientUISystem::Handle_Misc__PortalStormSubsided` | – | W | – | P+W | empty payload | +| 0x02EB | inbound | CommunicationTransientString | `ClientCommunicationSystem::Handle_Communication__TransientString` | W | W | W | W | (msg, chatType) → ChatLog system msg | +| 0x0312 | inbound | MagicPurgeBadEnchantments | `ClientMagicSystem::Handle_Magic__PurgeBadEnchantments` | W | W | – | P+W | empty payload | +| 0x0314 | inbound | SendClientContractTrackerTable | `ClientUISystem::Handle_Social__SendClientContractTrackerTable` | – | W | – | –defer:Quests | CContractTrackerTable blob | +| 0x0315 | inbound | SendClientContractTracker | `ClientUISystem::Handle_Social__SendClientContractTracker` | – | W | – | –defer:Quests | (CContractTracker, flag, flag) | + +**Footnotes:** + +[^e-a]: PlayerDescription has its own dedicated parser (`PlayerDescriptionParser.TryParse`) rather than living in `GameEvents.cs`. Wires into `LocalPlayerState` (vitals 7/8/9), `Spellbook` (learned spells + enchantments), `ItemRepository` (inventory + equipped), and the `onSkillsUpdated` callback (Run/Jump skills for movement). +[^e-b]: IdentifyObjectResponse uses `AppraiseInfoParser.TryParse` (separate file) rather than the simple header-only parser in `GameEvents.cs`. Returns full property bundle (int / int64 / bool / float / string / DID tables) plus SpellBook list. The retail handler `Handle_Item__AppraiseDone` (0x01CB) is the post-arrival completion signal, not the data carrier itself. +[^e-c]: 0x01E2 Emote sub-opcode is distinct from `HearEmote` (top-level GameMessage 0x02BC); the sub-opcode form is documented in ACE's `GameEventType.cs` but the named-retail decomp doesn't expose a dedicated handler — likely re-routed through the chat broadcast path. +[^e-d]: Named retail's `Recv_ChatRoomTracker` is the underlying handler symbol; ACE/Holtburger renamed to `SetTurbineChatChannels` for clarity. Same wire payload (per-room session ids for General/Trade/LFG/Roleplay/Society/Olthoi/Allegiance). + +--- + +## Section 5 — GameAction sub-opcodes (inside 0xF7B1 envelope) + +In-scope: 96. Implemented (built) in acdream: 24. Live callers in acdream: 8. Phase M target delta: 72 new builders + golden-vector tests. + +All rows are `outbound` direction (GameActions are client→server only). + +| Code | Direction | Name | Named-retail symbol | Holtburger | ACE | acdream today | Phase M target | Notes | +|------|-----------|------|---------------------|------------|-----|---------------|----------------|-------| +| 0x0005 | outbound | SetSingleCharacterOption | – | W | H | – | B | Per-option toggle; sibling of 0x01A1 bitmap | +| 0x0008 | outbound | TargetedMeleeAttack | `CM_Combat::Event_TargetedMeleeAttack` | W | H | W | B+W | Wired in WorldSession.SendMeleeAttack | +| 0x000A | outbound | TargetedMissileAttack | `CM_Combat::Event_TargetedMissileAttack` | W | H | W | B+W | Wired in WorldSession.SendMissileAttack | +| 0x000F | outbound | SetAfkMode | `CM_Communication::Event_SetAFKMode` | – | H | – | B | Toggle AFK | +| 0x0010 | outbound | SetAfkMessage | `CM_Communication::Event_SetAFKMessage` | – | H | – | B | Custom AFK string | +| 0x0015 | outbound | Talk | `CM_Communication::Event_Talk` | W | H | W | B+W | Wired in WorldSession.SendTalk | +| 0x0017 | outbound | RemoveFriend | `CM_Social::Event_RemoveFriend` | – | H | – | B | Friends list mutation | +| 0x0018 | outbound | AddFriend | `CM_Social::Event_AddFriend` | – | H | – | B | Friends list mutation | +| 0x0019 | outbound | PutItemInContainer | `CM_Inventory::Event_PutItemInContainer` | W | H | – | B | Inventory move; high priority | +| 0x001A | outbound | GetAndWieldItem | `CM_Inventory::Event_GetAndWieldItem` | W | H | – | B | Equip item | +| 0x001B | outbound | DropItem | `CM_Inventory::Event_DropItem` | W | H | – | B | Drop to ground | +| 0x001D | outbound | SwearAllegiance | `CM_Allegiance::Event_SwearAllegiance` | W | H | B | B+W | AllegianceRequests dead [^a-1] | +| 0x001E | outbound | BreakAllegiance | `CM_Allegiance::Event_BreakAllegiance` | W | H | B | B+W | AllegianceRequests dead [^a-1] | +| 0x001F | outbound | AllegianceUpdateRequest | – | – | H | – | B | Refresh allegiance tree | +| 0x0025 | outbound | RemoveAllFriends | – | – | H | – | B | Clear friends list | +| 0x0026 | outbound | TeleToPklArena | – | W | H | – | B | PK-lite arena recall | +| 0x0027 | outbound | TeleToPkArena | – | – | H | – | B | PK arena recall | +| 0x002C | outbound | TitleSet | – | – | H | – | B | Equip title | +| 0x0030 | outbound | QueryAllegianceName | `CM_Allegiance::Event_QueryAllegianceName` | – | H | – | B | – | +| 0x0031 | outbound | ClearAllegianceName | `CM_Allegiance::Event_ClearAllegianceName` | – | H | – | B | Officer-only | +| 0x0032 | outbound | TalkDirect | `CM_Communication::Event_TalkDirect` | – | H | – | B | Targeted /say (rarely used) | +| 0x0033 | outbound | SetAllegianceName | `CM_Allegiance::Event_SetAllegianceName` | – | H | – | B | Monarch-only | +| 0x0035 | outbound | UseWithTarget | `CM_Inventory::Event_UseWithTargetEvent` | W | H | B | B+W | InteractRequests dead [^a-1] | +| 0x0036 | outbound | Use | `CM_Inventory::Event_UseEvent` | W | H | B | B+W | InteractRequests dead [^a-1] | +| 0x003B | outbound | SetAllegianceOfficer | `CM_Allegiance::Event_SetAllegianceOfficer` | – | H | – | B | – | +| 0x003C | outbound | SetAllegianceOfficerTitle | `CM_Allegiance::Event_SetAllegianceOfficerTitle` | – | H | – | B | – | +| 0x003D | outbound | ListAllegianceOfficerTitles | `CM_Allegiance::Event_ListAllegianceOfficerTitles` | – | H | – | B | – | +| 0x003E | outbound | ClearAllegianceOfficerTitles | `CM_Allegiance::Event_ClearAllegianceOfficerTitles` | – | H | – | B | – | +| 0x003F | outbound | DoAllegianceLockAction | `CM_Allegiance::Event_DoAllegianceLockAction` | – | H | – | B | Lock recruitment | +| 0x0040 | outbound | SetAllegianceApprovedVassal | `CM_Allegiance::Event_SetAllegianceApprovedVassal` | – | – | – | B | – | +| 0x0041 | outbound | AllegianceChatGag | `CM_Allegiance::Event_AllegianceChatGag` | – | H | – | B | – | +| 0x0042 | outbound | DoAllegianceHouseAction | `CM_Allegiance::Event_DoAllegianceHouseAction` | – | H | – | B | – | +| 0x0044 | outbound | RaiseVital | – | W | H | B | B+W | CharacterActions builder dead [^a-1] | +| 0x0045 | outbound | RaiseAttribute | – | W | H | B | B+W | CharacterActions builder dead [^a-1] | +| 0x0046 | outbound | RaiseSkill | – | W | H | B | B+W | CharacterActions builder dead [^a-1] | +| 0x0047 | outbound | TrainSkill | `CM_Train::Event_TrainSkill` | W | H | B | B+W | CharacterActions builder dead [^a-1] | +| 0x0048 | outbound | CastUntargetedSpell | `CM_Magic::Event_CastUntargetedSpell` | W | H | B | B+W | CastSpellRequest builder dead [^a-1] | +| 0x004A | outbound | CastTargetedSpell | `CM_Magic::Event_CastTargetedSpell` | W | H | B | B+W | CastSpellRequest builder dead [^a-1] | +| 0x0053 | outbound | ChangeCombatMode | `CM_Combat::Event_ChangeCombatMode` | W | H | W | B+W | Wired in WorldSession.SendChangeCombatMode | +| 0x0054 | outbound | StackableMerge | `CM_Inventory::Event_StackableMerge` | W | H | B | B+W | InventoryActions builder dead [^a-1] | +| 0x0055 | outbound | StackableSplitToContainer | `CM_Inventory::Event_StackableSplitToContainer` | W | H | B | B+W | InventoryActions builder dead [^a-1] | +| 0x0056 | outbound | StackableSplitTo3D | `CM_Inventory::Event_StackableSplitTo3D` | – | H | B | B+W | InventoryActions builder dead [^a-1] | +| 0x0058 | outbound | ModifyCharacterSquelch | `CM_Communication::Event_ModifyCharacterSquelch` | – | H | – | B | Mute one player | +| 0x0059 | outbound | ModifyAccountSquelch | `CM_Communication::Event_ModifyAccountSquelch` | – | H | – | B | Mute account | +| 0x005B | outbound | ModifyGlobalSquelch | `CM_Communication::Event_ModifyGlobalSquelch` | – | H | – | B | Mute pattern | +| 0x005D | outbound | Tell | – | W | H | W | B+W | Wired in WorldSession.SendTell [^a-2] | +| 0x005F | outbound | Buy | `CM_Vendor::Event_Buy` | W | H | – | B | Vendor purchase | +| 0x0060 | outbound | Sell | `CM_Vendor::Event_Sell` | W | H | – | B | Vendor sell | +| 0x0063 | outbound | TeleToLifestone | `CM_Character::Event_TeleToLifestone` | W | H | B | B+W | InteractRequests builder dead [^a-1] | +| 0x00A1 | outbound | LoginComplete | `CM_Character::Event_LoginCompleteNotification` | W | H | W | B+W | Wired in GameWindow.cs:4423 | +| 0x00A2 | outbound | FellowshipCreate | `CM_Fellowship::Event_Create` | W | H | B | B+W | SocialActions builder dead [^a-1] | +| 0x00A3 | outbound | FellowshipQuit | `CM_Fellowship::Event_Quit` | W | H | B | B+W | SocialActions builder dead [^a-1] | +| 0x00A4 | outbound | FellowshipDismiss | `CM_Fellowship::Event_Dismiss` | W | H | B | B+W | SocialActions builder dead [^a-1] | +| 0x00A5 | outbound | FellowshipRecruit | `CM_Fellowship::Event_Recruit` | W | H | B | B+W | SocialActions builder dead [^a-1] | +| 0x00A6 | outbound | FellowshipUpdateRequest | `CM_Fellowship::Event_UpdateRequest` | W | H | B | B+W | SocialActions builder dead [^a-1] | +| 0x00AA | outbound | BookData | `CM_Writing::Event_BookData` | – | H | – | B | Open book contents | +| 0x00AB | outbound | BookModifyPage | `CM_Writing::Event_BookModifyPage` | – | H | – | B | Edit page text | +| 0x00AC | outbound | BookAddPage | `CM_Writing::Event_BookAddPage` | – | H | – | B | – | +| 0x00AD | outbound | BookDeletePage | `CM_Writing::Event_BookDeletePage` | – | H | – | B | – | +| 0x00AE | outbound | BookPageData | `CM_Writing::Event_BookPageData` | W | – | – | B | Read one page | +| 0x00B1 | outbound | TeleToPoi | – | – | – | B | B | InventoryActions builder dead; ACE handler unclear [^a-1][^a-3] | +| 0x00BF | outbound | SetInscription | `CM_Writing::Event_SetInscription` | – | – | – | B | Inscribe item | +| 0x00C8 | outbound | IdentifyObject | `CM_Item::Event_Appraise` | W | H | B | B+W | AppraiseRequest builder dead [^a-1] | +| 0x00CD | outbound | GiveObjectRequest | `CM_Inventory::Event_GiveObjectRequest` | W | H | B | B+W | InventoryActions builder dead [^a-1] | +| 0x00D6 | outbound | AdvocateTeleport | – | – | H | – | B | GM-only teleport | +| 0x0140 | outbound | AbuseLogRequest | `CM_Character::Event_AbuseLogRequest` | – | – | – | B | Player report tool | +| 0x0145 | outbound | AddChannel | `CM_Communication::Event_ChannelList` | – | H | B | B+W | SocialActions builder dead [^a-1][^a-4] | +| 0x0146 | outbound | RemoveChannel | – | – | H | B | B+W | SocialActions builder dead [^a-1] | +| 0x0147 | outbound | ChatChannel | `CM_Communication::Event_ChannelBroadcast` | W | H | W | B+W | Wired in WorldSession.SendChannel; same code as inbound 0x0147 [^a-5] | +| 0x0148 | outbound | ListChannels | – | – | – | – | B | – | +| 0x0149 | outbound | IndexChannels | `CM_Communication::Event_ChannelIndex` | – | – | – | B | – | +| 0x0195 | outbound | NoLongerViewingContents | `CM_Inventory::Event_NoLongerViewingContents` | W | H | – | B | Container UI close | +| 0x019B | outbound | StackableSplitToWield | `CM_Inventory::Event_StackableSplitToWield` | W | H | B | B+W | InventoryActions builder dead [^a-1] | +| 0x019C | outbound | AddShortcut | `CM_Character::Event_AddShortCut` | – | H | B | B+W | InventoryActions builder dead [^a-1] | +| 0x019D | outbound | RemoveShortcut | `CM_Character::Event_RemoveShortCut` | – | H | B | B+W | InventoryActions builder dead [^a-1] | +| 0x01A1 | outbound | SetCharacterOptions | – | – | H | B | B+W | SocialActions builder dead [^a-1] | +| 0x01A8 | outbound | RemoveSpellC2S | `CM_Magic::Event_RemoveSpell` | – | H | – | B | Self-cancel buff | +| 0x01B7 | outbound | CancelAttack | `CM_Combat::Event_CancelAttack` | W | H | W | B+W | Wired in WorldSession.SendCancelAttack | +| 0x01BF | outbound | QueryHealth | `CM_Combat::Event_QueryHealth` | W | H | B | B+W | SocialActions builder dead [^a-1] | +| 0x01C2 | outbound | QueryAge | `CM_Character::Event_QueryAge` | – | H | – | B | – | +| 0x01C4 | outbound | QueryBirth | `CM_Character::Event_QueryBirth` | – | H | – | B | – | +| 0x01DF | outbound | Emote | `CM_Communication::Event_Emote` | W | H | – | B | Custom /e text | +| 0x01E1 | outbound | SoulEmote | `CM_Communication::Event_SoulEmote` | W | H | – | B | /soulemote | +| 0x01E3 | outbound | AddSpellFavorite | `CM_Character::Event_AddSpellFavorite` | – | H | – | B | Spellbook pin | +| 0x01E4 | outbound | RemoveSpellFavorite | `CM_Character::Event_RemoveSpellFavorite` | – | – | – | B | Spellbook unpin | +| 0x01E9 | outbound | PingRequest | – | W | H | B | B+W | SocialActions builder dead; keepalive [^a-1] | +| 0x01F6 | outbound | OpenTradeNegotiations | `CM_Trade::Event_OpenTradeNegotiations` | W | H | – | B | Begin trade | +| 0x01F7 | outbound | CloseTradeNegotiations | `CM_Trade::Event_CloseTradeNegotiations` | W | H | – | B | Cancel trade | +| 0x01F8 | outbound | AddToTrade | `CM_Trade::Event_AddToTrade` | W | H | – | B | Add item to trade | +| 0x01FA | outbound | AcceptTrade | `CM_Trade::Event_AcceptTrade` | W | H | – | B | Confirm trade | +| 0x01FB | outbound | DeclineTrade | `CM_Trade::Event_DeclineTrade` | W | H | – | B | Reject trade | +| 0x0204 | outbound | ResetTrade | `CM_Trade::Event_ResetTrade` | W | H | – | B | Clear pending items | +| 0x0216 | outbound | ClearPlayerConsentList | `CM_Character::Event_ClearPlayerConsentList` | – | H | – | B | Resurrection consent | +| 0x0217 | outbound | DisplayPlayerConsentList | `CM_Character::Event_DisplayPlayerConsentList` | – | H | – | B | – | +| 0x0218 | outbound | RemoveFromPlayerConsentList | `CM_Character::Event_RemoveFromPlayerConsentList` | – | – | – | B | – | +| 0x0219 | outbound | AddPlayerPermission | `CM_Character::Event_AddPlayerPermission` | W | H | – | B | Storage / consent perm | +| 0x021A | outbound | RemovePlayerPermission | `CM_Character::Event_RemovePlayerPermission` | W | H | – | B | – | +| 0x021C | outbound | BuyHouse | `CM_House::Event_BuyHouse` | – | H | – | –defer:Phase Q | Housing — out of M baseline scope | +| 0x021E | outbound | HouseQuery | – | – | H | – | –defer:Phase Q | Housing | +| 0x021F | outbound | AbandonHouse | `CM_House::Event_AbandonHouse` | – | H | – | –defer:Phase Q | Housing | +| 0x0221 | outbound | RentHouse | `CM_House::Event_RentHouse` | – | – | – | –defer:Phase Q | Housing | +| 0x0224 | outbound | SetDesiredComponentLevel | – | – | – | – | B | Component-buy preference | +| 0x0245 | outbound | AddPermanentGuest | `CM_House::Event_AddPermanentGuest_Event` | – | H | – | –defer:Phase Q | Housing | +| 0x0246 | outbound | RemovePermanentGuest | `CM_House::Event_RemovePermanentGuest_Event` | – | H | – | –defer:Phase Q | Housing | +| 0x0247 | outbound | SetOpenHouseStatus | `CM_House::Event_SetOpenHouseStatus_Event` | – | H | – | –defer:Phase Q | Housing | +| 0x0249 | outbound | ChangeStoragePermission | `CM_House::Event_ChangeStoragePermission_Event` | – | H | – | –defer:Phase Q | Housing | +| 0x024A | outbound | BootSpecificHouseGuest | `CM_House::Event_BootSpecificHouseGuest_Event` | – | H | – | –defer:Phase Q | Housing | +| 0x024C | outbound | RemoveAllStoragePermission | `CM_House::Event_RemoveAllStoragePermission` | – | H | – | –defer:Phase Q | Housing | +| 0x024D | outbound | RequestFullGuestList | `CM_House::Event_RequestFullGuestList_Event` | – | – | – | –defer:Phase Q | Housing | +| 0x0254 | outbound | SetMotd | `CM_Allegiance::Event_SetMotd` | – | – | – | B | Allegiance message-of-the-day | +| 0x0255 | outbound | QueryMotd | `CM_Allegiance::Event_QueryMotd` | – | – | – | B | – | +| 0x0256 | outbound | ClearMotd | `CM_Allegiance::Event_ClearMotd` | – | H | – | B | – | +| 0x0258 | outbound | QueryLord | `CM_House::Event_QueryLord` | – | – | – | –defer:Phase Q | Housing | +| 0x025C | outbound | AddAllStoragePermission | `CM_House::Event_AddAllStoragePermission` | – | – | – | –defer:Phase Q | Housing | +| 0x025E | outbound | RemoveAllPermanentGuests | `CM_House::Event_RemoveAllPermanentGuests_Event` | – | H | – | –defer:Phase Q | Housing | +| 0x025F | outbound | BootEveryone | `CM_House::Event_BootEveryone_Event` | – | H | – | –defer:Phase Q | Housing | +| 0x0262 | outbound | TeleToHouse | `CM_House::Event_TeleToHouse_Event` | – | – | – | –defer:Phase Q | Housing | +| 0x0263 | outbound | QueryItemMana | `CM_Item::Event_QueryItemMana` | W | H | – | B | Mana-meter check | +| 0x0266 | outbound | SetHooksVisibility | `CM_House::Event_SetHooksVisibility` | – | H | – | –defer:Phase Q | Housing | +| 0x0267 | outbound | ModifyAllegianceGuestPermission | `CM_House::Event_ModifyAllegianceGuestPermission` | – | – | – | –defer:Phase Q | Housing | +| 0x0268 | outbound | ModifyAllegianceStoragePermission | `CM_House::Event_ModifyAllegianceStoragePermission` | – | – | – | –defer:Phase Q | Housing | +| 0x0269 | outbound | ChessJoin | – | – | H | – | –skip:minigame | Chess | +| 0x026A | outbound | ChessQuit | – | – | H | – | –skip:minigame | Chess | +| 0x026B | outbound | ChessMove | – | – | H | – | –skip:minigame | Chess | +| 0x026D | outbound | ChessMovePass | – | – | H | – | –skip:minigame | Chess | +| 0x026E | outbound | ChessStalemate | – | – | H | – | –skip:minigame | Chess | +| 0x0270 | outbound | ListAvailableHouses | `CM_House::Event_ListAvailableHouses` | – | – | – | –defer:Phase Q | Housing | +| 0x0275 | outbound | ConfirmationResponse | `CM_Character::Event_ConfirmationResponse` | W | H | – | B | Yes/No popups | +| 0x0277 | outbound | BreakAllegianceBoot | `CM_Allegiance::Event_BreakAllegianceBoot` | – | H | – | B | Officer kick | +| 0x0278 | outbound | TeleToMansion | `CM_House::Event_TeleToMansion_Event` | W | – | – | –defer:Phase Q | Housing recall | +| 0x0279 | outbound | Suicide | `CM_Character::Event_Suicide` | W | – | – | B | /suicide cmd | +| 0x027B | outbound | AllegianceInfoRequest | `CM_Allegiance::Event_AllegianceInfoRequest` | – | H | – | B | Tree info | +| 0x027D | outbound | CreateTinkeringTool / SalvageItemsWith | `CM_Inventory::Event_CreateTinkeringTool` | W | H | – | B | Salvage UI [^a-6] | +| 0x0286 | outbound | SpellbookFilter | `CM_Character::Event_SpellbookFilterEvent` | – | – | – | B | School filter | +| 0x028D | outbound | TeleToMarketPlace | – | W | – | – | B | MP recall | +| 0x028F | outbound | EnterPkLite | – | W | – | – | B | PK-lite toggle | +| 0x0290 | outbound | FellowshipAssignNewLeader | `CM_Fellowship::Event_AssignNewLeader` | W | H | – | B | – | +| 0x0291 | outbound | FellowshipChangeOpenness | `CM_Fellowship::Event_ChangeFellowOpeness` | – | H | – | B | – | +| 0x02A0 | outbound | AllegianceChatBoot | `CM_Allegiance::Event_AllegianceChatBoot` | – | – | – | B | Officer chat-mute | +| 0x02A1 | outbound | AddAllegianceBan | `CM_Allegiance::Event_AddAllegianceBan` | – | H | – | B | – | +| 0x02A2 | outbound | RemoveAllegianceBan | `CM_Allegiance::Event_RemoveAllegianceBan` | – | – | – | B | – | +| 0x02A3 | outbound | ListAllegianceBans | `CM_Allegiance::Event_ListAllegianceBans` | – | – | – | B | – | +| 0x02A5 | outbound | RemoveAllegianceOfficer | `CM_Allegiance::Event_RemoveAllegianceOfficer` | – | H | – | B | – | +| 0x02A6 | outbound | ListAllegianceOfficers | `CM_Allegiance::Event_ListAllegianceOfficers` | – | – | – | B | – | +| 0x02A7 | outbound | ClearAllegianceOfficers | `CM_Allegiance::Event_ClearAllegianceOfficers` | – | – | – | B | – | +| 0x02AB | outbound | RecallAllegianceHometown | `CM_Allegiance::Event_RecallAllegianceHometown` | – | – | – | B | Bind to monarch lifestone | +| 0x02AF | outbound | QueryPluginListResponse | `CM_Admin::Event_QueryPluginListResponse` | – | – | – | –skip:plugin-c2s | Decal-era plugin probe | +| 0x02B2 | outbound | QueryPluginResponse | `CM_Admin::Event_QueryPluginResponse` | – | – | – | –skip:plugin-c2s | Decal-era plugin probe | +| 0x0311 | outbound | FinishBarber | `CM_Character::Event_FinishBarber` | – | H | – | B | Char appearance commit | +| 0x0316 | outbound | AbandonContract | `CM_Social::Event_AbandonContract` | – | H | – | B | Drop quest | + +**Footnotes:** + +[^a-1]: "Builder dead" = the byte-array builder is implemented in `src/AcDream.Core.Net/Messages/.cs` but no caller in `src/AcDream.App/` or a `WorldSession.Send*` wrapper invokes it. Phase M wires these to game-state actions (UI clicks, command bus, key bindings) and adds golden-vector tests against holtburger fixtures. +[^a-2]: ACE's wire field order for Tell is `message FIRST then target` (see `ChatRequests.BuildTell` doc comment). Sept-2013 PDB has no `Event_Tell` symbol — it routes through `CM_Communication::Event_TalkDirectByName` plus a server-side rename. +[^a-3]: TeleToPoi (0x00B1) is listed in `InventoryActions.cs` but not in ACE's `GameActionType` enum. Cross-reference holtburger to confirm; may be a dead-letter opcode that retail's vendored 2013 ACE branch dropped. Verify before shipping the test vector. +[^a-4]: AddChannel (0x0145) — named-retail's matching symbol is `Event_ChannelList` (0x0148 according to retail enum), so the symbol mapping is approximate; AddChannel in pseudo-C may be unsymbolicated. Confirm by greping `acclient_2013_pseudo_c.txt` before publishing. +[^a-5]: 0x0147 ChannelBroadcast is the same numeric code in both directions (outbound GameAction = client sends to channel; inbound GameEvent = server broadcasts to channel members). Listed under outbound here per Section-5 scope; inbound version is in §4. +[^a-6]: ACE GameActionType lists 0x027D as `CreateTinkeringTool`; holtburger names the same opcode `SalvageItemsWith`. Both behaviors funnel through the salvage UI in retail. Either name is acceptable in acdream; pick one and leave the other as an alias constant. + +--- + +## Source attribution + +- **Holtburger** — `references/holtburger/` at `629695a` (2026-05-10). Primary client-behavior oracle. +- **ACE** — `references/ACE/Source/ACE.Server/Network/`. Server-side authority for GameMessages, GameEvents, GameActions, and accept rules. +- **Named retail decomp** — `docs/research/named-retail/` (Sept 2013 EoR PDB + Binary Ninja pseudo-C). Wire-format ground truth for the 2013 client. +- **acdream current state** — `src/AcDream.Core.Net/` and `src/AcDream.App/`. Inventoried by parallel agents on 2026-05-10. + +## Caveats + +This is the **initial population**, produced by four parallel research agents (one per opcode class) on 2026-05-10. Spot-check pass + intentional-divergence ratification is owed before M.1 closes. Specifically: + +- A handful of named-retail symbol citations are tentative (marked in footnotes); spot-check by greping `acclient_2013_pseudo_c.txt` and `symbols.json`. +- Holtburger / ACE / acdream cells were determined by reading the actual code (not guessing); when an agent couldn't determine a value, it used `?`. The `?` cells need a follow-up read. +- "Dead builder" calls (rows where acdream `B` but Phase M target is `B+W`) are based on a grep for `WorldSession.Send*` patterns and `worldSession.Send` calls in `src/AcDream.App/`. Edge cases (call sites in test code, command-bus indirection) may have been missed. +- Total opcode count in scope (~284) is approximate; deduplication of cross-section codes (e.g., 0x0147 in §4 and §5) is tracked in footnotes but the headline count treats them as distinct rows. + +This matrix lives on as a long-term reference. Phase M.6 implementation tracks progress against it; gameplay phases consuming Phase M will reference the rows they wire as part of their phase acceptance. diff --git a/docs/superpowers/specs/2026-05-10-phase-m-network-stack-design.md b/docs/superpowers/specs/2026-05-10-phase-m-network-stack-design.md new file mode 100644 index 0000000..63fb9c1 --- /dev/null +++ b/docs/superpowers/specs/2026-05-10-phase-m-network-stack-design.md @@ -0,0 +1,786 @@ +# Phase M — Network Stack Conformance — Design Spec + +**Date:** 2026-05-10 +**Status:** Draft (sections 1–3 of 8 written; sections 4–8 pending; opcode matrix in flight) +**Phase identifier:** M (per `docs/plans/2026-04-11-roadmap.md:414`) +**Supersedes the planned-but-never-written** `docs/superpowers/specs/2026-05-02-network-stack-conformance.md` + +**Related research:** +- [`docs/research/2026-05-10-holtburger-network-stack-study.md`](../../research/2026-05-10-holtburger-network-stack-study.md) — first-pass parity study, source of recent commit references +- [`docs/research/2026-05-10-phase-m-opcode-matrix.md`](../../research/2026-05-10-phase-m-opcode-matrix.md) — opcode coverage matrix (in flight; this spec links to it as the source of "done") + +**Reference repos:** +- `references/holtburger/` — fast-forwarded to `629695a` on 2026-05-10 +- `references/ACE/` — server-side opcode authority +- `docs/research/named-retail/` — Sept 2013 EoR PDB-named decomp + +--- + +## 1. Goal and non-goals + +### 1.1 Goal + +Build a **complete, layered, testable network protocol library** for acdream that covers every wire opcode a 2013 EoR retail client receives, sends, or both — independent of whether each opcode is yet wired into game state. The library is delivered behind three interfaces (`INetTransport`, `IReliableSession`, `IGameProtocol`); the existing `WorldSession` shrinks to a thin behavior consumer on top. Every parser, builder, and transport feature is unit-tested with golden-vector fixtures and survives a live ACE smoke loop before the phase ships. + +The bar is **Bar C — "wireable on demand."** For every in-scope opcode: +- A typed message struct exists (record with named fields, no raw byte arrays) +- A parser exists if inbound (wire bytes → typed message) +- A builder exists if outbound (typed message → wire bytes) +- A round-trip test exists where applicable +- A golden-vector test exists pinning at least one canonical wire encoding +- Either the opcode is dispatched to a typed event observable by `WorldSession`, or its dispatch is documented as deferred to the gameplay phase that needs it (with the deferred-target named, e.g., "wired in Phase F") + +The **behavior layer** (what to DO with each message in game state) remains the responsibility of the gameplay phase that needs it. Phase M does not wire `HouseStatusUpdate` into a house-status panel; it ensures `HouseStatusUpdate` parses correctly into a typed event so the future Phase consuming it has zero protocol work. + +### 1.2 What "complete" is measured against + +The opcode coverage matrix at `docs/research/2026-05-10-phase-m-opcode-matrix.md` is the **source of truth** for "done." Per opcode it cites: + +- Holtburger coverage (`references/holtburger/crates/holtburger-{session,protocol,core}/`) +- ACE coverage (`references/ACE/Source/ACE.Server/Network/GameMessages/Messages/` for outbound; `Source/ACE.Server/Network/Handlers/` for inbound accept rules) +- Named retail decomp (`docs/research/named-retail/acclient_2013_pseudo_c.txt`, `symbols.json` — by `class::method` or address) +- Acdream's current state (parser? builder? wired? deferred? unknown?) +- Phase M target (parse, build, both, or "skip with documented justification") + +An opcode is **in scope** if any of: +- Holtburger or ACE actively sends/receives it +- The named retail decomp shows the 2013 client invoking it +- It appears in observed live ACE traffic on `127.0.0.1:9000` + +An opcode is **out of scope** if all of: +- Holtburger doesn't touch it +- ACE marks it server-internal-only or post-2013 (visible in ACE's commit history or comments) +- The named retail decomp shows no client-side reference +- It hasn't been observed on live ACE + +Out-of-scope opcodes get one row in the matrix with the justification, no code work. + +### 1.3 Non-goals + +- **Not** reimplementing ACE server behavior. Validations, accept rules, and game-side decisions live in ACE; we mirror only what the client must produce or consume. +- **Not** replacing acdream's stricter inbound checksum verification. Our `PacketCodec` validates more aggressively than retail did (per the existing class doc); we keep that unless named retail proves it's wrong. +- **Not** rewriting renderer, animation, audio, UI, plugin, or chat layers. Those have their own phases. The new network stack must compile under, and run alongside, the current rendering and gameplay code. +- **Not** introducing async/await across the codebase. The current `Tick()`-driven recv-loop model is preserved; layer extraction is structural, not asynchrony-restructuring. (We MAY add a dedicated network thread if M.7's runtime work warrants it, but that decision is internal to M.7.) +- **Not** handling opcodes that are ACE-only invented for emulation purposes (e.g., debug echos that retail never had). The matrix calls these out per row. +- **Not** optimizing for throughput. Correctness first. Allocation profile and CPU cost tuning is a follow-up phase if the live loop measurably regresses. +- **Not** plugin-API exposure of network internals. The plugin API gets typed-event subscriptions where useful; raw packet introspection is dev-only. + +### 1.4 What ships at the end of Phase M + +When M.8 closes: +- `src/AcDream.Net/` (new namespace) contains `INetTransport`, `IReliableSession`, `IGameProtocol`, their concrete implementations, and the typed message library. +- `src/AcDream.Core.Net/WorldSession.cs` is a behavior consumer ~200–400 LOC, not the current 1213 LOC monolith. +- The `tests/AcDream.Net.Tests/` project covers every protocol-layer surface with unit tests. +- A `tools/network-conformance-replay/` harness can replay a captured ACE session and verify byte-perfect outputs. +- `dotnet build` green, `dotnet test` green, live ACE smoke green: login → walk → chat → combat action → portal → logout, verified by user. +- The roadmap entry for Phase M moves from "PLANNED" to "shipped" with a one-line summary and commit reference. + +--- + +## 2. Coverage definition + +### 2.1 The opcode matrix + +The matrix is a markdown table at `docs/research/2026-05-10-phase-m-opcode-matrix.md`, grouped by layer: + +1. **Transport flags** — every value in `PacketHeaderFlags` (LoginRequest, ConnectRequest, AckSequence, EncryptedChecksum, BlobFragments, RequestRetransmit, RejectRetransmit, EchoRequest, EchoResponse, Flow, ServerSwitch, TimeSync, Disconnect, …). Each row says what the flag means, who sets it, and what acdream must do on receive. +2. **Optional-header fields** — every variable-length section (RequestRetransmit list, RejectRetransmit list, AckSequence, ConnectRequest payload, LoginRequest payload, CICMD, TimeSync, EchoRequest/EchoResponse times, Flow). Each row defines the byte layout and our parse/build status. +3. **GameMessage opcodes** — every top-level opcode the client sees (0xF658 CharacterList, 0xF745 CreateObject, 0xF74C UpdateMotion, 0xF7B0 GameEvent envelope, 0xF7DE TurbineChat, 0xEA60 AdminEnvirons, …) and every top-level opcode the client sends (0xF7C8 CharacterEnterWorldRequest, 0xF657 CharacterEnterWorld, 0xF61C MoveToState, 0xF61B JumpAction, 0xF753 AutonomousPosition, 0xF7E4 DddInterrogationResponse, …). +4. **GameEvent sub-opcodes** — every entry in `GameEventType.cs` (94 currently named; ~70+ currently unhandled). Each row identifies the parsing target plus the acdream wiring status. +5. **GameAction sub-opcodes** — every typed game-action ID (Talk, Tell, Channel, Use, UseWithTarget, MoveToObject, JumpAbsolute, CastSpell, Appraise, Identify, AttackTargetMelee/Missile, Allegiance ops, Inventory ops, Social ops, Skill/Attribute raise, Train, …). + +Each row has these columns: + +| Code | Direction | Name | Named-retail symbol or address | Holtburger | ACE | acdream today | Phase M target | Notes | + +Cell values for "Holtburger" / "ACE" / "acdream today": +- **`P`** — parses inbound +- **`B`** — builds outbound +- **`PB`** — both +- **`W`** — wired (parser/builder + dispatched to typed event consumed somewhere) +- **`–`** — not implemented +- **`N/A`** — not applicable for this side (e.g., a server-only message in ACE column) + +"Phase M target" cell values: +- **`PB+W`** — must parse, build (if outbound), wire to a typed event by phase end +- **`PB`** — must parse, build (if outbound), no wiring required +- **`P+W`** — inbound only, must parse and dispatch typed event +- **`–defer:`** — explicitly deferred to a named gameplay phase +- **`–skip:`** — out of scope, with justification + +### 2.2 Inbound parser obligations + +For every in-scope inbound opcode: + +- A typed C# record represents the message. Fields are named, typed, and ordered to match the wire layout (so a future reader can map field-to-byte without re-reading the parser). +- The parser is a static method on the record (`public static MyMessage Parse(ref BinaryReader r)`), throws `InvalidOperationException` on malformed input with a message containing the opcode and offset. +- A round-trip test exists if the opcode is also outbound. A golden-vector test always exists with at least one specific captured wire encoding. +- The parser dispatches to a typed event on `IGameProtocol` (`event Action OnMyMessage`). If wiring to game state is deferred, the matrix row says `–defer:` and the typed event still exists — gameplay-phase wiring is then a one-line subscription. + +### 2.3 Outbound builder obligations + +For every in-scope outbound opcode: + +- A typed C# record represents the message. +- A `Build(ref BinaryWriter w)` instance method writes the wire encoding. +- A golden-vector test pins at least one specific wire encoding. +- The high-level entry point lives on `IGameProtocol` (`Send(MyAction act)` or `Send(MyMessage msg)`). +- `WorldSession` exposes a behavior-friendly wrapper (`SendTalk(string text)` rather than `_protocol.Send(new TalkMessage { … })`) only for opcodes the user-facing app currently triggers. Less-used outbound builders stay on `IGameProtocol` directly until a gameplay phase needs the convenience wrapper. + +### 2.4 Three test fixture sources + +- **Golden vectors.** Hand-computed bytes for representative messages. Source: named retail decomp (extract via `tools/pdb-extract/`), holtburger captures, or by manual trace. Stored in `tests/AcDream.Net.Tests/Fixtures/Golden/.bin` plus a sibling `.json` describing the fields. +- **Live capture replay.** A captured session log (raw datagrams + timestamps) replayed offline against the new stack. Captures come from running acdream itself with a `ACDREAM_PCAP=1` env-var that dumps every datagram to disk. The first capture is recorded once Phase M.7's runtime is in place; subsequent captures replace it as features land. +- **Live ACE smoke.** Per-sub-phase, a live `dotnet run` against `127.0.0.1:9000` that exercises the relevant features. Final M.8 smoke covers login → walk → chat → combat action → teleport → reconnect → logout end-to-end. + +### 2.5 Acceptance for an in-scope opcode + +An opcode is "done" for Phase M when: + +1. Its matrix row is filled completely. +2. The typed message struct exists and matches the documented byte layout. +3. The parser and/or builder exist and pass round-trip tests where applicable. +4. At least one golden-vector test pins a canonical encoding. +5. The typed event is exposed on `IGameProtocol` (inbound) or the high-level send method exists (outbound). +6. The matrix row's `acdream today` column is updated to match `Phase M target`. + +The opcode-class agents working on the matrix produce the per-row data. Phase M.6 implementation work is then "for each row in the matrix where target ≠ today, write the code and tests." + +--- + +## 3. Three-layer architecture + +### 3.1 Layer overview + +``` +┌─────────────────────────────────────────────────────────────┐ +│ WorldSession (behavior layer — not part of Phase M's │ +│ protocol library; consumes IGameProtocol) │ +└────────────────────────────┬────────────────────────────────┘ + │ subscribes to typed events, + │ calls Send(IGameMessage|IGameAction) +┌────────────────────────────▼────────────────────────────────┐ +│ IGameProtocol — typed message routing │ +│ • opcode dispatch table │ +│ • GameAction sequence counter │ +│ • per-message typed events │ +│ • outbound: typed message → bytes via builder │ +└────────────────────────────┬────────────────────────────────┘ + │ delivers fully-assembled GameMessage + │ payloads; receives outbound payloads +┌────────────────────────────▼────────────────────────────────┐ +│ IReliableSession — wire correctness │ +│ • PacketCodec (header + optional + body framing, CRC, │ +│ ISAAC c2s/s2c, fragment header layout) │ +│ • inbound ordering buffer + RequestRetransmit issuing │ +│ • outbound packet cache + retransmit on server request │ +│ • ACK queue + piggyback │ +│ • EchoRequest reply, TimeSync forwarding │ +│ • port-switch state machine │ +│ • fragment assembly (inbound) + splitting (outbound) │ +└────────────────────────────┬────────────────────────────────┘ + │ INetTransport.Send(bytes, endpoint) + │ INetTransport.TryReceive(out bytes, out endpoint) +┌────────────────────────────▼────────────────────────────────┐ +│ INetTransport — UDP only │ +│ • Send / TryReceive / Close │ +│ • no protocol knowledge │ +│ • UdpNetTransport (prod) / MockTransport (test) │ +└─────────────────────────────────────────────────────────────┘ +``` + +**Hard rules on direction:** +- Higher layers know about lower layers; lower layers do not know about higher layers. +- `IGameProtocol` does not call into `INetTransport`; it must go through `IReliableSession`. +- `WorldSession` does not directly construct UDP packets, ISAAC streams, or fragment headers. +- A unit test for any layer can mock the layer below it. + +### 3.2 `INetTransport` + +```csharp +public interface INetTransport : IDisposable +{ + /// + /// Send a single UDP datagram to the given endpoint. Synchronous. + /// Returns the number of bytes sent (always == datagram.Length on + /// success). Throws on socket error. + /// + int Send(ReadOnlySpan datagram, IPEndPoint remote); + + /// + /// Non-blocking receive. Returns false if no datagram is available. + /// On true, datagram contains the bytes (caller must not retain + /// the returned span past the next call) and remote contains the + /// source endpoint. + /// + bool TryReceive(out ReadOnlySpan datagram, out IPEndPoint remote); + + /// + /// Local endpoint we are bound to (after construction). + /// + IPEndPoint LocalEndpoint { get; } +} +``` + +**Concrete implementations:** + +- `UdpNetTransport` — wraps `UdpClient` + `Socket`. Sets a 2 MiB recv buffer (matches holtburger). Bound to `0.0.0.0:0` by default; constructor accepts an explicit local endpoint for tests that need port reproducibility. +- `MockTransport` — in-memory channel with two queues: outbound (datagrams the SUT sent) and inbound (datagrams the test wants the SUT to receive). Tests assert against outbound, inject into inbound. No threads, no async, no time. + +**Forbidden in `INetTransport`:** +- Any knowledge of `PacketHeader`, `PacketHeaderFlags`, ISAAC, fragments, GameMessages. +- Dispatching to event handlers (it returns bytes; routing is the next layer up). +- Owning a recv loop. The recv loop lives in `IReliableSession.Tick()` or its async equivalent. + +### 3.3 `IReliableSession` + +This is the largest layer. It owns the wire. + +```csharp +public interface IReliableSession : IDisposable +{ + /// Drive the recv loop once. Call from the host loop or a + /// dedicated network thread. Drains all available inbound datagrams, + /// fires events for completed GameMessages, flushes pending ACKs and + /// retransmits, and emits time-sync updates. + void Tick(); + + /// Send a GameMessage payload. The reliable session + /// allocates a sequence number, encodes the header, computes the + /// CRC (encrypted if flags require), splits into fragments if the + /// payload exceeds the single-fragment limit, and ships via + /// INetTransport. + void SendGameMessage(ReadOnlySpan payload); + + /// Send a control packet (handshake, disconnect, echo response). + /// Bypasses the GameMessage path; caller supplies the optional-header + /// content directly. + void SendControl(PacketHeaderFlags flags, ReadOnlySpan optionalContent); + + /// Begin the handshake. Drives LoginRequest → + /// ConnectRequest → ConnectResponse → CharacterList ready, then + /// transitions to "ready for EnterWorld" state. + void BeginHandshake(string account, string password); + + /// Advance from CharacterSelection to InWorld. Sends + /// CharacterEnterWorldRequest; waits for ServerReady; sends + /// CharacterEnterWorld. + void EnterWorld(uint characterGuid, string account); + + /// Disconnect cleanly. Sends Disconnect packet with + /// client_id, then flushes and closes the transport. + void Disconnect(); + + // Events surfaced upward: + event Action> OnGameMessageReceived; // payload only + event Action OnTimeSync; // server time + event Action OnHandshakeStateChanged; + event Action OnDisconnected; + event Action OnEchoStatsUpdated; // optional, dev-mode +} +``` + +**Concrete implementation:** `ReliableSession`. Composes seven sub-components: + +1. `PacketCodec` — pure functions: encode, decode, CRC, fragment header pack/parse. Stateless except for the ISAAC streams it borrows. +2. `IsaacStreamPair` — owns `IsaacRandom c2s, s2c` plus a shared "search-and-stash" implementation for out-of-order encrypted-checksum recovery (port from holtburger `crypto.rs:73-93`). +3. `InboundOrderingBuffer` — `BTreeMap`-equivalent (`SortedDictionary` works in C#). Tracks `last_server_seq`, gaps, and feeds `RequestRetransmit` when gaps exceed the rate-limit threshold (1 second, max 115 seq IDs in a 256-seq window — match holtburger constants). +4. `OutboundPacketCache` — LRU dictionary (`max=512`) of recently-sent packets keyed by sequence. On server-issued `RequestRetransmit`, looks up + re-encrypts with current ISAAC + `RETRANSMISSION` flag. Uses `Iteration` field correctly. +5. `AckQueue` — pending-ack list. `IReliableSession.Tick` flushes via piggyback on the next outbound data packet; if no data goes out within the idle threshold, sends a standalone ACK packet. Piggybacks are automatic on every `SendGameMessage`. +6. `FragmentAssembler` — inbound: keyed by `(sequence, fragmentId)`, with TTL eviction (default 30s) for orphaned partials. Outbound: splits payloads >448 bytes into multiple fragments with consistent `id`/`count`/`index`/`queue` per holtburger and ACE conventions. +7. `HandshakeMachine` — state machine: `Idle` → `LoginSent` → `ConnectRequestReceived` → `ConnectResponseQueued` (with 200ms deferred send, non-blocking) → `PortPending` → `PortConfirmed` → `Ready` → `EnterWorldSent` → `InWorld`. Each transition is logged with timestamps for diagnostic replay. + +**Forbidden in `IReliableSession`:** +- Knowing the structure of GameMessage payloads beyond "they are bytes." +- Dispatching to typed events for specific opcodes. +- Calling into `WorldSession` or game state. + +### 3.4 `IGameProtocol` + +```csharp +public interface IGameProtocol : IDisposable +{ + /// Send a typed game action (0xF7B1 envelope, bumps the + /// per-action sequence counter). The implementation builds the + /// payload and hands it to IReliableSession.SendGameMessage. + void Send(IGameAction action); + + /// Send a non-action GameMessage (e.g., 0xF657 + /// CharacterEnterWorld, 0xF7C8 CharacterEnterWorldRequest, 0xF7E4 + /// DddInterrogationResponse, 0xF753 AutonomousPosition, + /// 0xF61C MoveToState). + void Send(IGameMessage message); + + // Inbound typed events (one per in-scope opcode): + event Action OnCharacterList; + event Action OnCreateObject; + event Action OnUpdateMotion; + event Action OnUpdatePosition; + event Action OnDddInterrogation; + event Action OnPlayerCreate; + event Action OnPlayerTeleport; + event Action OnTurbineChat; + // ...one per opcode in the matrix... + + // GameEvent sub-opcode events (one per sub-opcode): + event Action OnChannelBroadcast; + event Action OnTell; + event Action OnUpdateHealth; + // ...one per sub-opcode in the matrix... + + // Unknown / unhandled: + event Action OnUnknownMessage; // includes opcode, raw bytes, telemetry +} +``` + +The dispatch table is generated from the opcode matrix at build time (or maintained by hand from the matrix; this is a M.6 sub-decision). Every in-scope opcode has its own typed event; unknown opcodes go to `OnUnknownMessage` with full byte payload so devtools can render them. + +**Forbidden in `IGameProtocol`:** +- Direct UDP I/O. +- ISAAC, CRC, fragment work. +- Holding onto game state (Characters, current player guid, login state — those live in `WorldSession`). + +### 3.5 `WorldSession` (the behavior consumer — not protocol library) + +After Phase M, `WorldSession` is a thin layer: + +```csharp +public sealed class WorldSession : IDisposable +{ + private readonly IGameProtocol _protocol; + private readonly IReliableSession _reliable; + + // High-level state + public CharacterListEntry[] Characters { get; private set; } + public CharacterListEntry? CurrentCharacter { get; private set; } + public uint? PlayerGuid { get; private set; } + + // High-level commands (convenience wrappers around _protocol.Send) + public void Login(string account, string password) { ... } + public void EnterWorld(int characterIndex) { ... } + public void SendTalk(string text) { ... } + public void SendTell(string target, string text) { ... } + public void SendMove(MoveToState moveState) { ... } + + // Subscribes to _protocol events in the constructor; routes them + // to public events GameWindow / plugins consume. + public event Action OnCreateObject; + public event Action OnUpdateMotion; + // ...etc, mirroring _protocol.On... but at the WorldSession surface + // so callers don't reach into the protocol layer directly. +} +``` + +Target line count after migration: 200–400 LOC vs the current 1213 LOC. + +### 3.6 Layer dependencies and project structure + +New project: **`src/AcDream.Net/`**. + +- `AcDream.Net.Transport` namespace — `INetTransport`, `UdpNetTransport`, `MockTransport`. +- `AcDream.Net.Reliable` namespace — `IReliableSession`, `ReliableSession`, sub-components (`PacketCodec`, `IsaacStreamPair`, `InboundOrderingBuffer`, `OutboundPacketCache`, `AckQueue`, `FragmentAssembler`, `HandshakeMachine`), plus `PacketHeader`, `PacketHeaderFlags`, `PacketHeaderOptional`, `MessageFragment` (moved here from `AcDream.Core.Net.Packets`). +- `AcDream.Net.Protocol` namespace — `IGameProtocol`, `GameProtocol`, every typed message record, every typed event payload record. Subdivided by class: `Protocol/Messages/`, `Protocol/Events/`, `Protocol/Actions/`. + +The existing `src/AcDream.Core.Net/` namespace is **deleted at end of phase**. `WorldSession` moves to `src/AcDream.Core/` (it's behavior, not network plumbing). Any helpers in the old namespace migrate into `AcDream.Net.*` if still needed; otherwise they're deleted. + +Project references: +- `AcDream.Net` references `AcDream.Core` (for `IPlatformLogger`, shared types). +- `AcDream.Core` references `AcDream.Net` (for the interfaces — `WorldSession` needs `IGameProtocol`, `IReliableSession`). + +This implies one logical cycle that's broken by interface-only references: `AcDream.Net` only references `AcDream.Core`'s types that don't transitively depend on network code (i.e., logging + result types). If the cycle resists clean breaking, the fallback is a third project `AcDream.Net.Abstractions` for the interfaces, with `AcDream.Net.Implementation` and `AcDream.Core` both depending on it. + +### 3.7 What stays out of the architecture (and where it goes) + +- **Auth / GLS ticket flow** — currently absent. If Phase M needs to support GLS-ticketed login (real retail server flow, not just account/password against ACE), it lives in `AcDream.Net.Reliable.HandshakeMachine` as an additional pre-LoginRequest stage. For now, ACE only accepts account/password, so this is documented as a non-goal until a real-server phase. +- **Plugin packet introspection** — surface lives on `WorldSession` (or a separate dev-tool API), not in the protocol library. Exposing raw fragments to plugins is risky; we expose typed events. +- **Capture/replay tooling** — lives in `tools/network-conformance-replay/`, depends on `AcDream.Net` but not vice-versa. + +--- + +## 4. Migration strategy + +### 4.1 Worktree branch model + +Phase M ships entirely on a long-lived feature branch off `main`: + +- Branch name: `claude/phase-m-network-stack` +- Worktree path: `.claude/worktrees/phase-m-network-stack/` (per existing repo convention) +- All sub-phase commits land on this branch. +- `main` is untouched until M.8 acceptance gates close. +- Live-ACE testing of the new stack happens by `dotnet run` from the worktree. +- Live-ACE testing of the old stack continues to happen from `main`. + +### 4.2 Branch lifetime and rebase cadence + +- **Estimated lifetime:** 6–8 weeks (per cost estimate in §8). +- **Rebase cadence:** weekly minimum, plus an immediate rebase whenever any of the following lands on main: + - Touches `src/AcDream.Core.Net/`, `src/AcDream.App/Input/PlayerMovementController.cs`, or any networking-adjacent code + - Updates `references/holtburger/` (we re-pull and re-baseline our research) + - Updates `docs/research/named-retail/` (new symbols may invalidate matrix rows) + - Modifies the roadmap in any way that changes Phase M scope + +- **Conflict resolution policy:** + - Wire-format conflicts (main lands a fix to `MoveToState` while we're rewriting it): we adopt the main fix into the new stack, file an issue to verify the same behavior is reproduced post-port. + - Test conflicts (main adds a test that exercises the old `WorldSession`): the test moves to test the new stack via the same call site after migration; if the call site is gone, the test is rewritten against the new equivalent. + - Build conflicts: standard rebase resolution. + +- **Frequency check:** if rebase frequency exceeds 2× per week or rebase work consistently exceeds 30 minutes, the branch is too stale. Pause feature work, catch up, then resume. + +### 4.3 What ships on the branch vs in separate commits to main + +- All Phase M code: branch only. +- All Phase M tests: branch only. +- Roadmap updates (ongoing status, not the final "shipped" entry): cherry-pick to main as the phase progresses, so other agents see status. +- Research notes (e.g., new opcode-matrix updates, new findings against ACE/holtburger): land directly on main since they're useful to other phases independent of M. +- The opcode matrix doc itself: lives on main from the start (it's reference data, not protected by the migration). + +### 4.4 Final merge: M.8 ship gate + +When M.8 closes: +1. Branch is rebased one final time against current `main`. +2. Full `dotnet build` + `dotnet test` green on the branch. +3. Live-ACE smoke run from the worktree by user: login → walk → chat → combat → portal → logout. +4. Old `src/AcDream.Core.Net/` deleted in a final branch commit (NOT before — this is the load-bearing flip). +5. Branch merged to main as a single `--no-ff` merge commit, message names every sub-phase shipped. +6. Roadmap entry for Phase M moves to "shipped" in the same merge. +7. Memory crib written summarizing the architecture for future sessions. + +### 4.5 Rollback path + +If post-merge live ACE breaks unexpectedly, the rollback is: +- `git revert` the merge commit on main +- File a bug with the live-ACE failure mode +- Cherry-pick the fix onto a new branch off the reverted main +- Re-merge + +Since the merge is a single commit, revert is mechanical. The 6–8 weeks of work isn't lost — it's reachable via the original branch tip + the revert undoing the merge. + +### 4.6 Work-in-flight protocol + +During Phase M, other agents may want to work on other features. The protocol: +- Other agents work off main as usual. +- They are NOT permitted to touch `src/AcDream.Core.Net/` or any file the spec lists as Phase-M-owned. +- If they need to add a new outbound message (e.g., a new gameplay phase needs a new opcode), they file an issue tagged `phase-m-followup` and we incorporate post-merge. +- The Phase M branch is the only place network changes happen until M.8 closes. + +This is enforced by convention, not tooling. The Phase M agent (or human equivalent) communicates in commits + roadmap updates about what's locked. + +--- + +## 5. Sub-phase definitions of done + +Each sub-phase has: **entry criteria**, **exit criteria**, **conformance test gates**, and an **hour estimate**. + +### 5.1 M.1 — Audit & parity map + +**Entry:** Phase M kickoff. `references/holtburger/` is at known commit (`629695a` as of 2026-05-10). + +**Exit:** +- Opcode matrix at `docs/research/2026-05-10-phase-m-opcode-matrix.md` is filled to ≥95% completeness across all five sections (transport flags, optional headers, GameMessages, GameEvents, GameActions). +- For every row marked `–skip:`, the reason is documented and ratified by spec review. +- For every row marked `–defer:`, the deferred phase exists in the roadmap. +- A meta-section at the top of the matrix lists totals: "in-scope opcodes: N", "currently-implemented: M", "Phase M target delta: N-M". + +**Conformance gates:** +- Spot-check 10 randomly-selected rows by hand against all three sources (holtburger / ACE / named retail). Discrepancies block exit. + +**Hour estimate:** 16 hours. + +**Notes:** the holtburger study at `docs/research/2026-05-10-holtburger-network-stack-study.md` is a partial M.1 deliverable. M.1 completion includes building the formal matrix table from that study + per-opcode source citation. + +### 5.2 M.2 — Layer extraction (skeleton) + +**Entry:** M.1 exit gates green. + +**Exit:** +- New project `src/AcDream.Net/` exists with three namespaces (`Transport` / `Reliable` / `Protocol`). +- All three interfaces (`INetTransport`, `IReliableSession`, `IGameProtocol`) compile with their full signatures from §3. +- `MockTransport` and `UdpNetTransport` implement `INetTransport` with passing unit tests. +- Stub implementations of `IReliableSession` and `IGameProtocol` exist (throw `NotImplementedException` on member calls; pass interface compliance tests via the mock). +- The new project compiles. The old `src/AcDream.Core.Net/` is unchanged and still works. + +**Conformance gates:** +- `dotnet build` green. +- `dotnet test` green for any tests in `tests/AcDream.Net.Tests/` (which at this point covers only `MockTransport` and `UdpNetTransport`). + +**Hour estimate:** 40 hours. + +### 5.3 M.3 — Reliability core + +**Entry:** M.2 exit gates green. + +**Exit:** +- `IReliableSession`'s `ReliableSession` implementation is functionally complete: codec, ISAAC pair with search-and-stash, inbound ordering buffer, outbound packet cache, retransmit (both directions), `Iteration` field handling, RequestRetransmit issuing on gaps with rate-limit, RejectRetransmit handling. +- Sub-component unit tests pass. +- An integration test connects to a `MockTransport`, simulates an entire ACE session (login → walk → disconnect) with synthetic loss/reorder, verifies state. +- Holtburger study items 1.4 (port-switch race) and 1.7 (retransmit machinery) and ISAAC search-mode (item 6) are landed in this sub-phase. + +**Conformance gates:** +- 100% of unit tests pass. +- Integration test with synthetic 5% packet loss: 100% of GameMessages are eventually delivered; no false positives in retransmit requests. +- Integration test with synthetic 10% reordering: 100% of GameMessages are delivered in correct order; ISAAC search-mode keys are correctly stashed and consumed. + +**Hour estimate:** 40 hours. + +### 5.4 M.4 — ACK and control-packet policy + +**Entry:** M.3 exit gates green. + +**Exit:** +- ACK queue with piggyback works: every outbound `SendGameMessage` on `IReliableSession` carries the latest server seq automatically; standalone ACKs flush only when no data goes out within an idle threshold. +- EchoRequest handling: inbound EchoRequest triggers an outbound EchoResponse with mirrored time field. +- Disconnect packet carries `client_id` (study item 5). +- LoginComplete is sent on every PlayerTeleport and on first PlayerCreate (study item 1.2 — but the dispatch happens at the protocol layer, M.6, not here; M.4 ensures the underlying control-packet send path is correct). +- Idle ping/timeout: 1 Hz net tick, 15s timeout. + +**Conformance gates:** +- ACK piggyback test: send a series of GameMessages, verify each carries the most recent server seq. +- EchoResponse test: receive synthetic EchoRequest, verify EchoResponse goes out within 1 frame with correct time. +- Idle timeout test: don't send anything for 15s, verify keepalive fires and timeout doesn't trigger. + +**Hour estimate:** 16 hours. + +### 5.5 M.5 — Fragment and payload completeness + +**Entry:** M.4 exit gates green. + +**Exit:** +- Inbound fragment assembly with TTL eviction (default 30s) for orphaned partials. +- Outbound multi-fragment splitting for payloads >448 bytes. Handles correct `id` / `count` / `index` / `queue` per fragment. +- Round-trip tests for: single-fragment, 2-fragment, 5-fragment payloads. + +**Conformance gates:** +- Round-trip test with a 2KB payload: 5 fragments, all assembled correctly on receive. +- TTL test: orphan a fragment, verify it's evicted at 30s. +- Capture from holtburger or ACE of a real multi-fragment packet (e.g., long appraise text), our fragment assembler reproduces the same field values byte-perfect. + +**Hour estimate:** 24 hours. + +### 5.6 M.6 — Typed protocol surface + +**Entry:** M.5 exit gates green. Opcode matrix complete (M.1 exit + any deltas from M.2-M.5). + +**Exit:** +- For every opcode marked `PB+W`, `PB`, or `P+W` in the matrix: + - Typed message struct exists in `AcDream.Net.Protocol.Messages`, `Events`, or `Actions`. + - Parser/builder exists. + - Typed event exists on `IGameProtocol` for inbound opcodes. + - Round-trip test passes if applicable. + - Golden-vector test pins at least one canonical encoding. +- The dispatch table in `GameProtocol` routes inbound bytes to the correct typed event. +- Unknown opcodes route to `OnUnknownMessage` with full byte payload. + +**Conformance gates:** +- 100% of in-scope opcodes have green tests. +- A "round-trip every opcode" meta-test exists that, given a list of golden-vector samples, encodes + decodes each and asserts bit-for-bit equivalence. +- The MoveToState wire-format audit (study items 1.1.a-e) lands as part of M.6 — i.e., the new typed `MoveToStateMessage` builder produces wire output matching holtburger's `common.rs:122-186` encoding. + +**Hour estimate:** 80 hours. + +**Note:** This is the largest sub-phase. M.6 is parallelizable via agent dispatch — one agent per opcode class (transport flags, GameMessages, GameEvents, GameActions). Estimated single-developer time is 80h; with effective agent dispatch on the implementation, calendar time may compress to 3-5 days. + +### 5.7 M.7 — Runtime loop and diagnostics + +**Entry:** M.6 exit gates green. + +**Exit:** +- The new stack drives a recv loop that drains all available inbound, fires events, flushes pending ACKs/retransmits/ECHO replies, all within a single `Tick()`. +- Decode/order/reassembly is moved out of the render tick into either (a) the same render-tick `Tick()` call or (b) a dedicated network thread, depending on M.7's internal decision (logged in the sub-phase commit). +- Byte counters: per-direction, per-opcode, exposed via `IGameProtocol.GetTelemetry()`. +- Packet capture: `ACDREAM_PCAP=1` env-var dumps every datagram to disk in a parseable format. +- Replay tool: `tools/network-conformance-replay/` reads a capture, replays it against the new stack, asserts no decode errors and matching event sequence. +- Dev-panel diagnostics: a debug overlay shows current handshake state, ACK depth, retransmit queue depth, byte counters. + +**Conformance gates:** +- A 5-minute live ACE session captures a clean replay; replay against the new stack: zero decode errors. +- The render thread's per-frame budget for network work is < 0.5ms median (measured via existing perf instrumentation). + +**Hour estimate:** 16 hours. + +### 5.8 M.8 — Conformance tests and live validation + +**Entry:** M.7 exit gates green. + +**Exit:** +- All `tests/AcDream.Net.Tests/` tests green: unit, round-trip, golden-vector, integration with synthetic loss/reorder, replay-against-capture. +- Live ACE smoke: login → walk to lifestone → chat in /general → engage NPC for combat (one attack) → portal recall → logout. User-confirmed visually + via decode-error counter (must be 0). +- The `WorldSession` shrinkage is complete: pre-migration ~1213 LOC, post-migration ≤400 LOC. +- The `src/AcDream.Core.Net/` namespace is deleted. +- Memory crib written: `memory/project_phase_m_network.md` summarizing layer architecture, key gotchas discovered during implementation, location of opcode matrix. +- Roadmap updated: Phase M moves from "PLANNED" to "shipped" with merge commit reference. + +**Conformance gates:** +- All M.1–M.7 exit gates remain green. +- Final live ACE smoke green. + +**Hour estimate:** 24 hours. + +### 5.9 Total + +| Sub-phase | Hours | Cumulative | +|-----------|-------|------------| +| M.1 — Audit & matrix | 16 | 16 | +| M.2 — Layer extraction | 40 | 56 | +| M.3 — Reliability core | 40 | 96 | +| M.4 — ACK + control | 16 | 112 | +| M.5 — Fragments | 24 | 136 | +| M.6 — Typed protocol | 80 | 216 | +| M.7 — Runtime + diagnostics | 16 | 232 | +| M.8 — Tests + live val | 24 | 256 | + +**Total: 256 hours ≈ 32 working days ≈ 6.4 weeks single-developer.** + +Realistic with subagent parallelization on M.6 (typed-message implementation) and M.1 (matrix population): 4-6 weeks calendar time. + +--- + +## 6. Conformance test plan + +### 6.1 Test surfaces per layer + +| Layer | Test surface | Backing project | +|-------|--------------|-----------------| +| Transport | Mock + Udp behavior, recv-buffer sizing, error paths | `tests/AcDream.Net.Tests/Transport/` | +| Reliable | Codec round-trip, CRC encrypted+unencrypted, ISAAC search edge cases, ordering buffer scenarios, retransmit cycles, ACK piggyback, Echo, port-switch state machine, fragment assembly + splitting | `tests/AcDream.Net.Tests/Reliable/` | +| Protocol | Per-opcode round-trip + golden-vector + unknown-opcode telemetry | `tests/AcDream.Net.Tests/Protocol/` | +| End-to-end | Replay-against-capture, live-ACE smoke | `tests/AcDream.Net.Tests/Replay/` + `tools/network-conformance-replay/` | + +### 6.2 Golden-vector library structure + +``` +tests/AcDream.Net.Tests/Fixtures/Golden/ +├── Transport/ +│ ├── login_request.bin +│ ├── connect_request.bin +│ ├── ack_only.bin +│ ├── echo_request.bin +│ └── ... +├── Messages/ +│ ├── 0xF658_character_list.bin +│ ├── 0xF61C_movetostate_run_forward.bin +│ ├── 0xF753_autonomous_position.bin +│ └── ... +├── Events/ +│ ├── 0x0147_channel_broadcast.bin +│ ├── 0x02BD_tell.bin +│ └── ... +└── manifests/ + └── all-golden.json # (filename, opcode, decoded fields, source citation) +``` + +Each `.bin` has a sibling `.json` with the decoded fields and source attribution (holtburger capture / named retail trace / ACE-generated). + +### 6.3 Live capture replay + +`tools/network-conformance-replay/` is a small console app: +- Reads a `.pcap`-like capture from disk (binary format defined as part of M.7). +- For each datagram, hands bytes to a fresh `ReliableSession` + `GameProtocol`. +- Asserts: no decode errors, every typed event fires in the expected order (event order is part of the capture metadata), final session state matches the capture's recorded final state. +- Output: PASS/FAIL with detailed first-failure diff. + +### 6.4 Live ACE smoke flows + +Two tiers: + +- **Per-sub-phase smoke** (lightweight, automated where possible): + - M.3: handshake completes; CharacterList received; clean disconnect. + - M.4: 60-second idle session with ECHO traffic flowing both ways; 0 disconnects. + - M.5: a multi-fragment payload from ACE (e.g., long appraise text) parses correctly. + - M.6: every opcode the live session naturally produces (login → walk → chat → portal) parses to its typed event. + +- **M.8 final smoke** (manual, user-driven): + - Account login: user enters credentials, picks +Acdream, enters world. + - Walk: WASD around Holtburg for 30s; observe local + retail-observer view (via parallel retail client) for blippy movement. + - Chat: /general "hello", /tell to a name, /a (allegiance), /f (fellowship). + - Combat: target a guard, swing once, observe damage notification + animation. + - Portal recall: cast Portal Recall, watch teleport. + - Logout: clean disconnect, verify ACE shows session ended. + - Decode-error counter must be 0 throughout. + +### 6.5 What's not tested at this layer + +- Game-state correctness: that's per-feature in gameplay phases. +- Rendering correctness: that's the existing renderer test surface. +- Plugin behavior: separate test surface. + +--- + +## 7. Risk register + +| # | Risk | Probability | Impact | Mitigation | +|---|------|-------------|--------|------------| +| 1 | **Branch drift** — main moves faster than expected, rebase work overwhelms. | Medium | High (could double phase calendar time) | Weekly rebase minimum + watchpoints on key files. Pause and catch up if conflict effort exceeds 30min/week. | +| 2 | **Opcode ambiguity** — three sources (holtburger / ACE / named retail) disagree on a field layout. | Medium | Medium (delays the affected M.6 row) | Per-row triage: cross-check against live ACE traffic if available; file a research note documenting disagreement; pick the source with strongest evidence; revisit if a real-server-deploy phase invalidates the choice. | +| 3 | **ISAAC stream desync** — search-mode port has a subtle bug that corrupts the keystream. | Low | Critical (silent corruption looks like ACE incompat) | Parallel-run old + new ISAAC for 1 week in dev mode; log every divergence; smoke-test with synthetic out-of-order injection. | +| 4 | **Live ACE incompat** — new stack works in unit tests but real ACE rejects something subtle. | Medium | High (blocks M.8) | Per-sub-phase live smoke (not just final). Catches incompats early. | +| 5 | **Dead-builder integration drift** — Phase B.4 surface (Use/UseWithTarget/PickUp) was built without wiring; we may rebuild without verifying the wiring works. | Medium | Medium (fixes one bug, introduces another) | Every typed builder must have a golden-vector test. The matrix row's "Phase M target" includes "verified against live ACE" for any opcode previously dead-built. | +| 6 | **`Iteration` field** — current code always writes 0; if retail uses non-zero iteration on retransmits in a way ACE validates, we get rejected. | Low | Medium (breaks retransmit specifically) | M.3's retransmit test exercises iteration values 0, 1, 2; live-ACE smoke with synthetic loss to trigger real retransmits. | +| 7 | **Project structure refactor breaks downstream code** — moving `WorldSession` or deleting `AcDream.Core.Net` shifts a namespace many files reference. | High | Low (compile errors are immediate) | M.8 deletion is the last commit; entire branch compiles up to that point; deletion + namespace fix lands in one commit, single rebuild. | +| 8 | **Threading model regression** — if M.7 introduces a network thread, render-thread races appear. | Medium | High (intermittent crashes) | Default to keeping single-threaded model; threading is opt-in via a flag for one test session before becoming default. | +| 9 | **Test fixture rot** — golden vectors capture a 2026-05 ACE version; future ACE versions diverge. | Low | Low (fixtures still valid for retail-conformance baseline) | Golden vectors are pinned to retail behavior, not ACE-specific. Live capture replay is from acdream itself (most reproducible). | +| 10 | **Calendar overrun** — 6.4 weeks expands to 12+ weeks. | Medium | Medium (delays Phase F+ gameplay phases) | Mid-phase checkpoint at M.4 close (week 3 in plan). If hours-spent ≥ 1.5× estimate, scope-cut M.6 to "matrix-deferred opcodes only, batch the long tail to M.6.b post-merge." | + +--- + +## 8. Cost estimate + +### 8.1 Summary + +**Total estimate: 256 hours ≈ 6.4 working weeks single-developer.** + +With effective subagent dispatch (especially on M.1 matrix population and M.6 typed-message implementation), realistic calendar compression to **4–6 weeks**. + +### 8.2 Cost breakdown by sub-phase (repeating for visibility) + +| Sub-phase | Hours | Calendar weeks | Subagent-friendly? | +|-----------|-------|----------------|--------------------| +| M.1 — Audit & matrix | 16 | 0.4 | Yes (per-class agents) | +| M.2 — Layer extraction | 40 | 1.0 | Limited (architecture-driven, single voice) | +| M.3 — Reliability core | 40 | 1.0 | Limited (ISAAC + ordering buffer interact) | +| M.4 — ACK + control | 16 | 0.4 | Limited | +| M.5 — Fragments | 24 | 0.6 | Limited | +| M.6 — Typed protocol | 80 | 2.0 | **Yes (per-opcode-class agents)** | +| M.7 — Runtime + diagnostics | 16 | 0.4 | Limited | +| M.8 — Tests + live val | 24 | 0.6 | Limited (live val needs human) | +| **Total** | **256** | **6.4** | | + +### 8.3 Critical path + +``` +M.1 → M.2 → M.3 → M.4 → M.5 → M.6 → M.7 → M.8 + (mostly sequential within a single-developer flow) +``` + +M.1 can partially overlap M.2 (matrix work continues while skeleton lands). +M.3 / M.4 / M.5 are conceptually parallel within the reliable layer, but practically sequenced because they share state. +M.6 is the parallelization cliff — agents work on different opcode classes simultaneously. +M.7 / M.8 are sequential. + +### 8.4 Resource assumptions + +- One primary developer driving the architecture and integration. +- Subagent dispatch budget: liberal (acdream's sustained pattern is to use Sonnet agents heavily for bounded chunks; per CLAUDE.md "Subagent policy"). +- Live ACE on `127.0.0.1:9000` available throughout for smoke tests. +- User available for M.8 final visual gate (the only step that genuinely needs human eyes). + +### 8.5 What buys schedule slack + +If budget compresses (e.g., 4 weeks max), the following are scope-cuts in order: + +1. **Long-tail GameEvent sub-opcodes** (House*, Trade*, Book*, Vendor*, Barber*, Allegiance updates, ContractTracker*) — 30+ rows that gameplay phases will need eventually but not for M.8 acceptance. Move to a `M.6.b` follow-up. +2. **Outbound multi-fragment splitting** (M.5 second half) — defer until a gameplay phase needs >448-byte outbound payload. +3. **M.7 dev-panel diagnostics** — keep the byte counters and capture, drop the visual overlay. +4. **M.8 replay harness** — keep the smoke gate, drop the automated replay testing (move to follow-up). + +These cuts get total down to ~150–180 hours / 4 weeks if necessary. The architecture is preserved; the long-tail completeness regresses to "covers everything observed in live ACE during normal play, not the long tail." + +--- + +## Status & next steps + +**Spec status as of 2026-05-10:** Sections 1–8 written. Awaiting: +1. **Opcode matrix construction** (M.1's main deliverable). Dispatch agents: one per opcode class. Output: `docs/research/2026-05-10-phase-m-opcode-matrix.md`. +2. **Roadmap update.** Phase M entry shrinks to a one-paragraph summary + status table + pointer to this spec. M.0 sub-lane folds into M.3 / M.4 / M.6 (no longer ships separately). + +**When implementation starts:** create the worktree, branch off main, begin M.1 matrix completion → M.2 skeleton. +