acdream/docs/architecture/code-structure.md

691 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# acdream — code structure & extraction sequence
**Status:** Living document. Created 2026-05-16; implementation reconciliation
completed 2026-07-21; Slices 16 landed by 2026-07-22. Slice 7 draw-frame
orchestration is the active structural program before new M4 subsystems enter
the App layer.
**Purpose:** Describe the desired structural state of the App layer,
explain the rules we've adopted, and lay out the safe extraction
sequence from today's reality (one 15,723-line `GameWindow.cs` at the
2026-07-21 audit) to the
target (thin `GameWindow`, small focused collaborators).
**Companion to:** [`acdream-architecture.md`](acdream-architecture.md)
(the layered architecture) and
[`worldbuilder-inventory.md`](worldbuilder-inventory.md) (what we take
from WB vs port ourselves).
---
## 1. The structural problem we're solving
The layered architecture works: `AcDream.Core` is GL-free, the network
layer is wire-compatible, the UI has a stable contract, plugins load.
The structural debt is concentrated in **one file**:
```
baseline cf50ee3d 15,723 lines / 278 fields / 205 methods
after Slice 1 14,912 lines / 278 fields / 191 methods
after Slice 2 14,546 lines / 277 fields / 190 methods
after Slice 3 14,310 lines / 274 fields / 190 methods
after Slice 4 10,301 lines / 267 fields / 163 methods
after Slice 5 closeout 8,811 lines / 247 fields / 153 methods
after Slice 6 closeout 7,026 lines / 241 fields / 108 methods
```
`GameWindow` is the single object that:
- Owns the GL context, the window, input, and shaders.
- Reads ~40 different environment variables across its lifetime.
- Composes the shipped `LiveSessionController`/host/router boundary; it no
longer owns a parallel session, command bus, subscription lifetime, connect,
character-entry, reconnect, or graceful-close body.
- Owns the adapters that hydrate canonical `LiveEntityRuntime` records into
animation, collision, rendering, and DAT-backed appearance resources.
- Composes the shipped `WorldSelectionQuery` and
`SelectionInteractionController`; it no longer owns world-picking,
selection intent, Use/PickUp, or auto-walk deferral algorithms.
- Drives per-frame render orchestration (sky → terrain → opaque mesh →
transparent mesh → particles → debug lines → UI).
- Builds and applies streamed landblock presentation (DAT decode, scenery,
EnvCells, mesh publication, collision, and retirement) around the shipped
`StreamingController` and `GpuWorldState` owners.
- Wires up every plugin hook sink, every diagnostic, every panel.
The extracted controllers are valuable and tested, but line count alone proves
that creating collaborators has not yet made the window thin. A collaborator
is a completed extraction only when it owns the state and behavior body;
wrapping a `GameWindow` method in a delegate preserves ordering but remains a
partial extraction.
The fix is **not** "rewrite `GameWindow` in one pass" — that is a
high-risk change. The fix is to **extract one
collaborator at a time**, verify behavior is unchanged, ship, and
move on. This document defines that sequence.
---
## 2. Code Structure Rules — the discipline
Recap of the rules from `CLAUDE.md` with the rationale:
### Rule 1: No new substantial feature bodies in `GameWindow.cs`
**Why:** Every line we add to `GameWindow` makes the eventual decomposition
harder. New features that "live in" `GameWindow` instead of being
extracted are the reason the file is 10k lines.
**How to apply:** A new feature gets its own class under
`src/AcDream.App/<Subsystem>/` (or deeper in `AcDream.Core` if it's pure
logic). `GameWindow` owns a field and a wiring call, nothing more. If
you find yourself adding a 200-line method to `GameWindow`, stop and
extract.
**Exemption:** Trivial wiring that *must* stay in `GameWindow` because
it touches GL state during `OnLoad` is acceptable, but should still
delegate to a collaborator for the substance.
### Rule 2: `AcDream.Core` must not depend on window / GL / backend projects
**Why:** Core is the GL-free, testable layer. The moment Core imports
a GL or windowing namespace, we've lost the ability to test it without
a graphics context, and the layer split becomes fiction.
**How to apply:** Phase O removed both external WorldBuilder/backend project
references. The only currently allowed seams are the GL-free helpers owned in
our tree under `src/AcDream.Core/Rendering/Wb/`: `TerrainUtils`,
`TerrainEntry`, `RegionInfo`, `SceneryHelpers`, and `TextureHelpers`.
`ObjectMeshManager` and every GL resource owner remain in App. If Core needs a
new capability, define a narrow Core interface and implement it in App; adding
a new project reference requires an inventory-doc update explaining why.
### Rule 3: UI panels target `AcDream.UI.Abstractions` only
**Why:** This is the one rule that keeps D.2b (the future retail-look
backend) viable. Every panel that imports `ImGuiNET` directly is a panel
we'd have to rewrite when the backend swaps.
**How to apply:** A panel's `using` block must mention
`AcDream.UI.Abstractions.*` and nothing from `AcDream.UI.ImGui`. The
panel writes against `IPanelRenderer`. The `ImGuiPanelRenderer`
translates those calls to ImGui at runtime. Plugin-facing UI follows the
same rule.
### Rule 4: Startup env vars enter through `RuntimeOptions`
**Why:** Environment variables are global mutable state. Reading them
at random call sites means (a) duplicated `Environment.GetEnvironmentVariable`
boilerplate, (b) no single place to see "what flags does the client
respond to?", (c) impossible to unit-test parsing.
**How to apply:** `src/AcDream.App/RuntimeOptions.cs` is the typed
options object. `Program.cs` builds it once from args + env and passes
it to `GameWindow`. New startup flags add a field to `RuntimeOptions`
and a parser in `RuntimeOptions.FromEnvironment`. They don't add
`Environment.GetEnvironmentVariable` reads.
**Scope:** `RuntimeOptions` is for **startup-time** configuration —
things that don't change once the window is up. Runtime diagnostic
toggles are Rule 5's domain.
### Rule 5: Runtime diagnostic toggles live in diagnostic owner classes
**Why:** Diagnostic flags (`ACDREAM_DUMP_MOTION`, `ACDREAM_PROBE_*`,
etc.) need to be both env-readable at startup *and* runtime-toggleable
from the DebugPanel. Per-call-site env reads can't be runtime-toggled.
**How to apply:** Today's template is
`src/AcDream.Core/Physics/PhysicsDiagnostics.cs` — one static class with
typed `Probe*` properties read from env vars once at startup, plus
runtime setters that the DebugPanel binds. New diagnostic flags follow
this shape, not the per-call-site pattern that dominates `GameWindow.cs`.
**Cleanup direction:** The dozens of existing `ACDREAM_DUMP_*` reads
inside `GameWindow.cs` are tech debt. We do NOT bulk-migrate them as
part of this refactor — they're working, they're scattered, and
moving them carries risk without a current acceptor. We migrate them
opportunistically: when a `GameWindow` extraction lands and a diagnostic
moves with it, route it through the new owner's diagnostic class.
### Rule 6: Tests live in the project matching the layer
**Why:** Test discoverability + dependency hygiene. A test for a Core
class belongs next to other Core tests; a test for an App class belongs
in an App test project. Co-locating tests across layers makes the
dependency graph dishonest.
**How to apply:** One test project per source project that has tests.
Today:
- `tests/AcDream.Core.Tests/``src/AcDream.Core/`
- `tests/AcDream.Core.Net.Tests/``src/AcDream.Core.Net/`
- `tests/AcDream.UI.Abstractions.Tests/``src/AcDream.UI.Abstractions/`
- `tests/AcDream.App.Tests/``src/AcDream.App/`
`tests/AcDream.App.Tests/` now exists and owns App-layer controller, streaming,
render-resource lifetime, retained-UI, and `RuntimeOptions` tests. New App tests
belong there; do not place GL-free Core behavior in that project merely because
App currently wires it.
---
## 3. Target structure of the App layer
The end state — not what we're shipping in one pass, but the shape
we're aiming at.
```
src/AcDream.App/
├── Program.cs # parse args + env → RuntimeOptions, build GameWindow
├── RuntimeOptions.cs # typed startup options (Rule 4)
├── Rendering/
│ ├── GameWindow.cs # thin: GL/window lifecycle + delegates per-frame to RenderFrameOrchestrator
│ ├── RenderFrameOrchestrator.cs # GPU-flight boundary + typed world/private/UI render phases
│ ├── LiveEntityAnimationScheduler.cs # shipped: ordinary live-object update workset
│ ├── LiveEntityAnimationPresenter.cs # final part-pose/mesh/effect composition after scheduler output
│ ├── RetailStaticAnimatingObjectScheduler.cs # shipped: separate static-animation workset
│ ├── StaticLiveRootCommitter.cs # live static root → pose + collision boundary
│ ├── TerrainModernRenderer.cs # (already exists)
│ ├── TextureCache.cs # (already exists)
│ ├── ParticleRenderer.cs # (already exists)
│ ├── Sky/ # (already exists)
│ ├── Wb/ # WB seam + EnvCellLandblockBuild transaction
│ └── Vfx/ # (already exists)
├── Net/
│ ├── LiveSessionController.cs # owns complete WorldSession connect/enter/logout/reconnect lifecycle
│ ├── LiveSessionLifecycleHost.cs # narrow App composition + exact borrowed-session attachment
│ └── LiveSessionEventRouter.cs # typed subscriptions → focused domain handlers
├── Physics/
│ ├── ProjectileController.cs # canonical live-record projectile orchestration
│ ├── RemotePhysicsUpdater.cs # ordinary/Hidden remote narrow-tick integration
│ ├── LiveEntityOrdinaryPhysicsUpdater.cs # manager-less canonical body Transition path
│ ├── LiveEntityNetworkUpdateController.cs # Position/Vector/State/Motion App integration
│ ├── LiveEntityInboundAuthorityGate.cs # exact per-channel timestamp/version admission
│ ├── LiveEntityMotionRuntimeController.cs # shared host/setup/MoveTo/Sticky runtime policy
│ ├── DeferredLiveEntityMotionRuntimeBindings.cs # fail-fast construction-order bridge
│ ├── LiveEntityShadowPublisher.cs # authoritative exact-owner/residency collision gate
│ ├── RemoteInboundMotionDispatcher.cs # shared animated/headless UpdateMotion funnel
│ ├── RemoteTeleportController.cs # loaded/pending teleport placement ownership
│ ├── RemoteTeleportHook.cs # ordered retail teleport teardown actions
│ └── RemoteTeleportPlacement.cs # collision-seated SetPosition transition commit
├── World/
│ ├── InboundPhysicsStateController.cs # timestamps + accepted spawn snapshots
│ ├── LiveEntityRuntime.cs # shipped: logical lifetime + ServerGuid↔entity.Id translation
│ ├── LiveEntityHydrationController.cs # spawn/appearance/parent/delete resource integration
│ ├── RetailInboundEventDispatcher.cs # update-thread packet/frame FIFO barrier
│ ├── RetailLiveFrameCoordinator.cs # shipped: object/network/command/reconcile phase order
│ ├── LiveEntityPresentationController.cs # ordered Hidden/NoDraw/effect/collision side effects
│ ├── LiveEntityTeardown.cs # failure-isolated multi-owner lifecycle drain
│ └── ParentAttachmentState.cs # parent generations + pending ParentEvent relations
├── Interaction/
│ ├── WorldSelectionQuery.cs # read-only picking/classification/description queries
│ └── SelectionInteractionController.cs # owns selection intents, Use/PickUp, auto-walk deferral
├── Streaming/
│ ├── LandblockPresentationPipeline.cs # DAT build/apply/retire transaction and resource publication
│ └── ... # shipped streamer/world-state/reveal owners
├── Input/ # (already exists)
├── Audio/ # (already exists)
└── Plugins/ # (already exists)
```
What `GameWindow` keeps:
- `IWindow` / `GL` / `IInputContext` lifecycle (constructor + `OnLoad` +
`Run` + `OnClosing`).
- `RuntimeOptions` reference (the typed startup config).
- GL resource construction and top-level collaborator composition. Construction
is allowed here; feature algorithms and mutable subsystem state are not.
- One field per top-level collaborator (`_liveSessionController`,
`_liveEntityRuntime`, `_selectionInteraction`, `_streamingPresentation`,
`_liveObjectFrame`, `_renderFrameOrchestrator`).
- The Silk.NET event-handler stubs that delegate to collaborators.
What `GameWindow` loses:
- Live connect/enter/logout and event-subscription bodies → completed inside
`LiveSessionController` / `LiveSessionEventRouter`.
- Live-object hydration adapters and final animated-part presentation → focused
world/render owners over canonical `LiveEntityRuntime` records.
- `WorldPicker`, target queries, and selection-driven Use/PickUp/auto-walk →
`WorldSelectionQuery` + `SelectionInteractionController`. Core
`SelectionState` remains the injected session owner.
- Landblock DAT build/apply/retirement presentation →
`LandblockPresentationPipeline`; `StreamingController` remains the residency
scheduler and `GpuWorldState` remains the spatial registry.
- Per-frame draw orchestration and its frame-local scratch state →
`RenderFrameOrchestrator`.
The eventual `GameEntity` aggregation (target state described in
`acdream-architecture.md` §"GameEntity: The Unified Entity") happens
**after** `LiveEntityRuntime` is the single owner of entity state.
Until then, the parallel-dicts problem is bounded inside one class
instead of spread across `GameWindow`.
`LiveEntityRuntime` is now that single boundary. It composes
`InboundPhysicsStateController` for the nine-channel retail timestamp gates and
latest accepted immutable CreateObject snapshot, owns the canonical local ID
and optional runtime components, and separates logical registration from
spatial projection. It also retains raw PhysicsState separately from the final
state produced by retail's ordered side effects;
`LiveEntityPresentationController` projects those transitions into draw,
collision, effect, child-NoDraw, and target visibility without becoming a
second lifetime or GUID owner. `ParentAttachmentState` is runtime-owned and keys unresolved
relations by child and parent generation. `Rendering/Vfx/EntityEffectController`
owns the focused mixed F754/F755 pending FIFO, effect profiles, typed-table
resolution, and a readiness set; canonical ServerGuid-to-local-ID translation
always stays in `LiveEntityRuntime`. `EntityScriptActivator` uses the same
canonical `WorldEntity.Id` as rendering and physics; the disjoint static ID
allocators fail fast instead of wrapping into another landblock's namespace.
All other non-Parent packet
families still need the future general queue tracked by divergence AD-32.
The per-frame object scheduler is extracted, but final animated-part
presentation is not. `LiveEntityAnimationScheduler` snapshots canonical spatial
root records and advances the incarnation-stable object clock, PartArray,
hooks, one selected movement owner, and manager tail in retail order. The
remaining `GameWindow.TickAnimations` loop still composes part transforms,
drawable `MeshRef`s, and effect poses from the scheduler output; moving that
body into `LiveEntityAnimationPresenter` is an explicit pending slice.
Manager-less
bodies delegate their candidate/Transition/cell/shadow commit to
`LiveEntityOrdinaryPhysicsUpdater`; retained projectile bodies and remote
MovementManagers remain mutually exclusive movement owners. Static animation
is deliberately separate: `RetailStaticAnimatingObjectScheduler` owns the
`CPhysics::static_animating_objects` workset for DAT and live PhysicsState-
Static owners with Setup DefaultAnimation. Both schedulers are wired by
`GameWindow`, but neither owns GUID identity or logical resources.
The typed animation view fills its reusable snapshot and render-ID set from
`LiveEntityRuntime`'s concrete spatial dictionary, avoiding interface-enumerator
boxing on both update and render hot paths.
`RemoteInboundMotionDispatcher` similarly keeps UpdateMotion protocol behavior
outside `GameWindow`: GameWindow resolves the canonical record/body and the
optional PartArray sink, while one dispatcher owns retail's interrupt, style,
MoveTo/type-0, sticky, and standing-long-jump order. Static root projection is
bounded by `StaticLiveRootCommitter`, which synchronizes changed roots to
effects and collision without rebuilding zero-omega shadows or resurrecting a
Hidden/withdrawn registration.
Synchronous network and lifecycle callbacks are bounded by
`RetailInboundEventDispatcher`. It owns no wire state and no identity; it only
serializes nested live-object operations until the current packet or full
object-frame tail completes. State-bearing direct dispatch is allocation-free;
only a genuinely nested operation allocates its retained queue wrapper.
`LiveEntityRecord` then supplies exact-incarnation
and per-channel authority versions at callback boundaries. Position, State,
Vector, and Movement remain independent, while a separate velocity version
invalidates only an older operation that would overwrite a newer velocity
installed by Position, Vector, or Movement. This prevents re-entrant App
observers from creating call-stack ordering that retail's update-thread packet
FIFO cannot produce.
Pose-dependent hook deferral is similarly incarnation-scoped rather than GUID-
or local-ID-scoped. `EntityEffectPoseRegistry` publishes a monotonic pose-owner
lifetime, and `AnimationHookFrameQueue` captures it before semantic callbacks
and rechecks it before each semantic AnimationDone and each routed hook. Static animation retains `process_hooks`
until its root, live parts, and children are published; withdrawal invalidates
both its prepared pose and pending hook tail.
Resolved ordinary motion commits its body/root/contact state before the
canonical full-cell setter enters `LiveEntityRuntime.RebucketLiveEntity`.
Because that setter may synchronously move the projection to pending or replace
the GUID, both `RemotePhysicsUpdater` and `LiveEntityOrdinaryPhysicsUpdater`
revalidate the exact incarnation before collision or manager-tail publication.
Collision residency itself is projection-owned, not updater-owned:
`LiveEntityPresentationController` suspends retained non-projectile shadows on
every unavailable-projection edge, restores them on hydration, and reconciles
the pending-first case at `OnLiveEntityReady` after collision registration.
Local projection and both authoritative remote UpdatePosition tails commit the
complete root before rebucketing, then publish collision only through an
exact-record/spatial-residency gate. Remote reflood tracks translation,
sign-invariant complete orientation, and cell changes so an in-place turn or a
same-pose EnvCell crossing cannot leave offset Setup shapes stale.
The projectile controller retains its separate body/InWorld/shadow edge owner.
Remote teleport placement is bounded in `Physics/RemoteTeleportController`,
not `GameWindow`: it retains at most one pending request per materialized
incarnation, scopes it by the live generation and accepted PositionSequence,
and asks `RemoteTeleportPlacement` to collision-seat the current body when the
destination projection is available. `GameWindow` supplies lifecycle and
shadow-sync callbacks only; canonical identity remains in `LiveEntityRuntime`.
Failed hydration restores the captured source and delegates an
incarnation-scoped shadow restore to `LiveEntityPresentationController` while
that source is unloaded, so Hidden/UnHide and teleport never become competing
restore owners. A newer placement transfers that restore into an explicit
generation-scoped active-placement state before its rebucket visibility edge
even while Hidden. All intervening Hidden/UnHide and projection edges defer to
that owner until stable success or rollback completes; only then can it restore,
re-defer the source, or hand a Hidden result back for UnHide. The
`ILiveEntityRemotePlacementRuntime` seam keeps the complete cell/contact
handoff available across same-body runtime-wrapper replacement; replacing the
canonical body or dropping the placement contract within one incarnation is
rejected even after an operational component clear. `RemoteMotion.Body` is
constructor-owned; hydration compares pending/current wrappers directly to the
record body rather than trusting wrapper-to-wrapper equality. Binding reads an
interface Body getter once and reuses that snapshot. `GpuWorldState`
performs remove+place as one spatial rebucket,
then commits and serially drains visibility edges; `LiveEntityRuntime` filters
delayed duplicates. A rollback inside an observer cannot race the outer
destination-visible notification or expose an intermediate false pulse.
`LiveEntityTeardown` executes those independent owner callbacks to completion
and aggregates failures afterwards, so a throwing effect/plugin sink cannot
strand teleport, movement, shadow, light, or GUID-scoped state.
---
## 4. Reconciled ownership ledger — 2026-07-21
This section replaces the original six-step sketch. That sketch correctly
identified the target but overstated some extractions and omitted large bodies
added during M2/M3. The current audit is based on implementation ownership, not
whether a class with the planned name exists.
### 4.1 What counts as extracted
An extraction is **complete** only when:
1. the collaborator owns the mutable state and behavior body;
2. `GameWindow` constructs it and delegates through a narrow method;
3. the collaborator does not call back into arbitrary `GameWindow` methods;
4. focused tests exercise the collaborator without constructing a Silk window;
5. the accepted connected/visual behavior is unchanged.
A class that stores delegates back to substantial `GameWindow` methods is a
useful ordering seam, but its ownership status is **partial**.
### 4.2 Current implementation truth
| Area | Status | Current truth |
|---|---|---|
| Startup options | **Complete** | `RuntimeOptions` owns startup configuration (`eda936dc`). Remaining direct environment reads are legacy runtime diagnostics, not startup configuration. |
| Network session | **Complete** | `LiveSessionController` owns resolve/create/bind/Connect/selection/EnterWorld/Tick/stop/reconnect/disposal; `LiveSessionLifecycleHost` is the narrow App composition boundary; `LiveSessionEventRouter` and `LiveSessionCommandRouter` own exact inbound/outbound lifetimes. `GameWindow` retains one controller field and focused domain sink factories only (`d9ccf8a6`, `6a5d9e2e`). |
| Live identity/lifetime | **Complete App integration** | `LiveEntityRuntime` owns incarnation identity, accepted snapshots/timestamps, runtime components, and logical/spatial lifetime. `LiveEntityHydrationController`, `LiveEntityRuntimeTeardownController`, and `LiveEntityNetworkUpdateController` own DAT-backed hydration, retryable cleanup, and accepted Position/Vector/State/Movement presentation without a second GUID owner (`aa90c646`). |
| Inbound/object-frame order | **Complete App orchestration** | `UpdateFrameOrchestrator` owns the complete typed host phase graph; `RetailInboundEventDispatcher`, `RetailLiveFrameCoordinator`, `LiveObjectFrameController`, `LiveSpatialPresentationReconciler`, streaming/input/teleport/player-mode/camera owners preserve the accepted order. `GameWindow.OnUpdate` is one profiler-scoped handoff (`e91f3102`). |
| World reveal | **Complete** | `WorldRevealCoordinator` owns login/portal readiness and reveal lifetime (`a4ef5788`). The accepted deterministic lifecycle trace did not change after extraction. |
| Retained gameplay UI | **Mostly complete feature ownership** | `RetailUiRuntime` and focused panel/controllers own layout and behavior. `GameWindow.OnLoad` still performs substantial service composition, which is allowed until the final composition cleanup. |
| Selection/interaction | **Complete** | `WorldSelectionQuery` owns read-only picking/classification/range queries; `SelectionInteractionController` owns selection intent, Use/PickUp transport, exact-incarnation queues, and auto-walk deferral; `ItemInteractionController` owns ItemHolder policy plus the shared retail inventory-request transaction. `GameWindow` retains construction and narrow lifecycle forwarding only. |
| Landblock presentation | **Complete** | `LandblockBuildFactory` owns the captured-origin DAT transaction; concrete render/physics/DAT-static publishers and `LandblockPresentationPipeline` own publication and exact retryable retirement. `StreamingOriginRecenterCoordinator` serializes old-window retirement with teleport/session origin lifetimes. `GameWindow` retains construction and one pipeline field only (`c79d0a49`, closeout `4a205a3e`). |
| Render-frame orchestration | **Not extracted** | `OnRender` plus its portal/PView/alpha/particle helpers still own the draw graph and frame-local scratch state. There is no `RenderFrameOrchestrator` yet. |
| Unified `GameEntity` | **Deferred target** | `LiveEntityRuntime` already supplies the critical canonical owner. Full type aggregation is a separate high-risk migration and is not required to make `GameWindow` thin. |
### 4.3 Revised extraction sequence
Every numbered slice is behavior-preserving and independently committed. A
slice does not include feature work or opportunistic gameplay fixes.
#### Gate 0 — deterministic baseline — COMPLETE
The Release suite, connected R6 soak, world-lifecycle screenshots/checkpoints,
graceful reconnect, local locomotion/collision/projectile/teleport gate, and
two-client portal observer gate form the pre-refactor baseline. Each later
slice runs the subset capable of detecting its risk; render/session slices run
the complete connected lifecycle gate.
#### Slice 1 — finish selection/interaction ownership — COMPLETE 2026-07-21
Detailed execution plan:
[`docs/plans/2026-07-21-gamewindow-slice-1-selection-interaction.md`](../plans/2026-07-21-gamewindow-slice-1-selection-interaction.md).
Split the old Step 4 into three reviewable commits:
1. `WorldSelectionQuery` receives camera/scene/live-object read seams and owns
picking, target classification, selection bounds, closest-target lookup,
names, and descriptions. It cannot mutate selection or send packets.
2. `SelectionInteractionController` owns selection intents, double-click Use,
Use/PickUp packet requests, range decisions, speculative facing, and the
pending auto-walk action. It composes the existing item/combat controllers
rather than duplicating their rules.
3. Selection/Use/PickUp/combat-target `InputAction` cases delegate to the new
controller; the old methods and fields are deleted from `GameWindow`.
Tests cover read-only query classification separately from stateful intents,
including direct use, distant auto-walk completion, corpse/container opening,
pickup placement, hostile-only targeting, Hidden objects, and same-GUID reuse.
The cutover landed in `047a4c83`, `52dbb574`, `e74f2ca9`, `fa8d5232`,
`d2bb5af4`, and review-correction commit `5acc3f01`. The final review also
ported the shared `ACCWeenieObject::prevRequest` transaction shape across
inventory, toolbar, paperdoll, external-container, give, split, merge, and
drop routes. Queued actions, pending placements, and responses are bound to
exact object incarnations/tokens; optimistic projection, rollback projection,
and authoritative response notices are distinct.
Measured against `cf50ee3d`, `GameWindow` fell from 15,723 to 14,912 lines and
from 205 to 191 methods; field count stayed at 278 because the slice exchanged
legacy state for composed owners. Three independent retail, architecture, and
adversarial review loops finished clean. Release build passed; 6,558 tests
passed and five fixture/conformance tests skipped intentionally.
#### Slice 2 — finish live animation presentation — COMPLETE 2026-07-21
Move `TickAnimations`, final rigid/visual part composition, effect-pose
publication, and motion-done binding into `LiveEntityAnimationPresenter`.
`LiveEntityAnimationScheduler` remains the time/movement owner; the presenter
only consumes one scheduler result and publishes the final draw/effect pose.
Move the surviving motion diagnostics with this owner instead of adding more
environment reads to `GameWindow`.
Tests pin legacy and sequencer paths, Hidden/static eligibility, ObjScale versus
rigid effect poses, part availability, same-frame hook pose publication, and
incarnation replacement. Run the R6 object-frame and projectile/effect suites
plus the connected locomotion/projectile gate.
Result: `LiveEntityAnimationPresenter` owns final visual/rigid part
composition, effect-pose publication, MotionDone binding, and typed diagnostics.
Schedules carry exact record/entity/component/epoch/projection/presentation
identity and own their authored frame prefix until the next scheduler tick.
Appearance and static-owner rebinding invalidate stale work. Named-retail
`CPartArray::UpdateParts` short-frame retention now preserves both visual and
rigid trailing poses. `GameWindow` lost 366 lines, two methods, and one net
field. Three independent retail, architecture, and adversarial review loops
finished clean; focused App/Core tests and the complete App suite pass.
#### Slice 3 — complete live-session ownership — COMPLETE 2026-07-21
Detailed execution plan:
[`docs/plans/2026-07-21-gamewindow-slice-3-live-session.md`](../plans/2026-07-21-gamewindow-slice-3-live-session.md).
`LiveSessionController` now owns the complete resolve/create/bind/Connect/
CharacterList/select/EnterWorld/Tick/stop/reconnect/dispose transaction. Exact
generation and operation-depth gates prevent synchronous lifecycle callbacks
from resurrecting a superseded session. Staged teardown makes commands inert,
detaches events, gracefully disposes the exact `WorldSession`, detaches the
host, and runs the full reset manifest to convergence before reuse.
`LiveSessionLifecycleHost` is the narrow App adapter; the router and command
owners are transactional and session-bound. `GameWindow` lost `_liveSession`,
the router/command fields, and the old startup/reset/wiring/disposal methods.
Both UI stacks resolve `controller.Commands`; direct feature sends borrow
`CurrentSession` at call time. Forty-seven controller cases plus host,
transport-allocation, and structural gates cover failure/reentrancy/reuse.
Three independent retail, architecture, and adversarial reviews finished
clean. The Release suite passes 6,714 tests with five intentional skips. The
306-second connected lifecycle gate passed capped login, five travel/revisit
checkpoints, exact graceful close, and uncapped fresh-process reconnect; both
client processes exited normally.
`GameWindow` fell another 236 lines and three fields to 14,310 lines / 274
fields / 190 methods. Slice 4 is next.
#### Slice 4 — extract live-entity App integration — COMPLETE 2026-07-21
Detailed execution ledger:
[`docs/plans/2026-07-21-gamewindow-slice-4-live-entity-integration.md`](../plans/2026-07-21-gamewindow-slice-4-live-entity-integration.md).
This is two owners, not one replacement god object:
- `LiveEntityHydrationController` owns CreateObject/ObjDesc/Parent/Pickup/Delete
integration, DAT-backed appearance hydration, resource registration, and
exact teardown callbacks over `LiveEntityRuntime`.
- `LiveEntityNetworkUpdateController` owns App routing for accepted
Position/Vector/State/Movement updates into the existing remote physics,
motion, projectile, presentation, and teleport controllers.
Neither may own a GUID dictionary. Both resolve the current incarnation only
through `LiveEntityRuntime` and must preserve the inbound FIFO/authority-token
rules. Run the complete live-entity stress suite, R6 connected route, inventory
equip/parenting, death/corpse, and portal gates.
Checkpoints A and B are complete. `d68c83d1`, `5882b308`, and `fcb66198`
established the shared origin, lifecycle, remote-motion, and exact-record
physics-host seams. `69a2ca0c` extracted appearance/collision/default-pose and
projection-withdrawal mechanics, including exact recursive `leave_world`
subtree handling and retained retry ownership. Three independent reviews were
clean and the Release suite passes 6,799 tests with five intentional skips.
Checkpoints C and D are complete. `d10c5f2d` moved CreateObject, first
materialization, origin bootstrap, and landblock recovery into
`LiveEntityHydrationController`; `fe551496` moved ObjDesc, Parent, Pickup,
child-ready, and exact leave-world/re-entry routing. Appearance and attached
subtree recovery now use independent authority transactions and preserve the
same logical/runtime identities. Checkpoint E is complete in `f38822c4`:
Delete, visibility prune, retryable exact-incarnation component cleanup, and
derived remote stop-observation lifetime now have focused owners. Checkpoint F
completed in `aa90c646`: accepted Position/Vector/State/Movement and the
equal-generation CreateObject tail now route through focused owners.
Independent authority/version captures survive reentrant callbacks and GUID
reuse; shared motion-runtime policy is one-way bound and no extracted owner
keeps a second identity dictionary. ForcePosition serializes and stamps one
exact canonical outbound Position. Three corrected-diff reviews, the complete
6,940-test Release suite, and the 310-second seven-destination connected R6
route all passed. `GameWindow` is 10,301 lines / 267 fields / 163 methods.
Slice 5 checkpoints AG are complete. The pipeline owns the serialized worker
build, concrete render/physics/static publication, and retryable Near/full
retirement. The direct cutover deleted the legacy apply/facade/scratch body and
added one retained shared-origin lifetime transaction for teleport, logout,
and fast relogin. `GameWindow.cs` was 8,793 raw lines at checkpoint G and no
longer owns landblock build/apply/retirement behavior. H then corrected native
shutdown ordering so live-session reset converges before the streamer and
other reset dependencies are disposed. The final measurement is 8,811 raw
lines / 247 fields / 153 methods. The complete Release suite (7,054 pass / 5
skip), the 311-second capped/reconnect lifecycle gate, and the synchronized
394-second nine-stop resource soak all pass (`4a205a3e`).
#### Slice 5 — extract landblock presentation — COMPLETE
Detailed execution ledger:
[`docs/plans/2026-07-21-gamewindow-slice-5-landblock-presentation.md`](../plans/2026-07-21-gamewindow-slice-5-landblock-presentation.md)
(complete).
Create `LandblockPresentationPipeline` around the existing immutable
`LandblockBuild` transaction. It owns DAT build context, scenery/EnvCell
construction, mesh/collision/light publication, demotion/full-retirement, and
the single-reader DAT lock contract. `StreamingController` continues deciding
what is resident; `GpuWorldState` continues owning spatial buckets.
Tests pin loaded/pending/demoted/unloaded symmetry, stale build generations,
resource pin/release balance, collision footprints across landblock seams, and
first-login bootstrap replacement. The deterministic world-lifecycle gate and
nine-destination resource soak pass.
#### Slice 6 — extract update-frame orchestration — COMPLETE 2026-07-22
Detailed execution ledger:
[`docs/plans/2026-07-21-gamewindow-slice-6-update-frame-orchestration.md`](../plans/2026-07-21-gamewindow-slice-6-update-frame-orchestration.md)
(complete).
After the stateful bodies above have owners, make the update path a real
orchestrator instead of delegates back into the window. It preserves the
accepted production order—not an overclaimed retail host order—covering
teardown/clock/streaming convergence, input, the retail-shaped object → inbound
network → command barrier, conditional spatial reconciles, liveness, local
teleport, player-mode entry, and camera presentation. The exact twelve-phase
graph and registered TS-53 host-order adaptation live in the detailed ledger.
`GameWindow.OnUpdate` becomes a short time/input handoff.
Frame-order tests and the existing R6 gate must produce the same lifecycle and
movement traces before and after extraction.
The production cutover is complete in `e91f3102`. `GameWindow.OnUpdate` now
contains the profiler scope and one `UpdateFrameOrchestrator.Tick` handoff;
streaming, input, the object/network barrier, liveness, teleport, player-mode,
and camera phases have typed owners with no substantial callback facade into
the window. The complete Release suite passes 7,182 tests with five intentional
skips. The 314-second lifecycle/reconnect gate and 394-second synchronized
nine-stop resource soak both pass with graceful exits, stable live-owner
counts, and a 0.8 ms update-frame p95 at the final Caul checkpoint.
`GameWindow` is 7,026 raw lines / 241 fields / 108 methods: 8,697 lines (55.3%)
smaller than the campaign baseline. TS-50, TS-51, and TS-53 remain accurately
registered; this behavior-preserving extraction introduced no new divergence.
#### Slice 7 — extract `RenderFrameOrchestrator`
Detailed execution ledger:
[`docs/plans/2026-07-22-gamewindow-slice-7-render-frame-orchestration.md`](../plans/2026-07-22-gamewindow-slice-7-render-frame-orchestration.md)
(active).
Move the complete draw graph and its reusable frame-local scratch state into a
GL-owning App collaborator. Preserve the exact modern pipeline order, clip
routing, PView flood, landscape/opaque/shared-alpha flush boundaries,
particles, debug draw, paperdoll, retained UI, and frame fences. Do not pass a
hundred individual delegates or let the orchestrator reach back into
`GameWindow`; inject a small immutable service set plus explicit per-frame
input.
Automated acceptance uses framebuffer artifacts and render/resource
checkpoints. Visual acceptance compares outdoor, building, dungeon, portal
exit, translucent lifestone/particles, and UI at the same camera positions.
#### Slice 8 — composition and shutdown cleanup
Keep GL/window construction in `GameWindow.OnLoad`, but group creation into
small composition functions and delete feature state left behind by prior
slices. `OnClosing` delegates to the existing retryable shutdown transaction.
Silk callbacks become narrow calls into the input, update, render, resize,
focus, and shutdown owners.
### 4.4 Exit criteria
The campaign is complete when:
- `GameWindow` contains no AC gameplay algorithm, entity scan, packet builder,
DAT landblock builder, animation-part composer, or draw-graph body;
- `OnUpdate`, `OnRender`, `OnInputAction`, and live-session callbacks are short
delegation methods;
- every extracted owner has App-layer tests and symmetric teardown;
- the Release suite and connected lifecycle/resource gates stay green after
every slice;
- the final local visual matrix passes unchanged.
Line count is a progress signal, not the acceptance test. The expected result
is below roughly 5,000 lines, but ownership and dependency direction decide
completion. Full `GameEntity` type aggregation is evaluated only afterwards as
a separate migration; it is not folded into this campaign.
---
## 5. Rules of the road during the extraction
1. **One slice at a time.** Each commit ships one ownership boundary or
mechanical call-site cutover. Bundling slices makes failures hard to
isolate.
2. **Behavior preservation is the acceptance criterion.** Every slice
must build clean, all tests pass, and visual verification at the
appropriate accepted milestone scenarios must succeed. We're moving code, not
changing it.
3. **No new features during an extraction step.** If you spot a real
bug while extracting, file it in `docs/ISSUES.md` and address it in
a separate commit (before or after the extraction, not folded into
it).
4. **Diagnostic toggle migrations are opportunistic.** When a method
moves to a new owner, the diagnostic flag inside it can move to a
diagnostic class as part of the same commit. We do not do a bulk
diagnostic-cleanup pass.
5. **Update this document when the plan changes.** If a slice turns out
to need a different ownership shape than described above, update §4 in the
same session you discover the divergence.
6. **No façade-only completion claims.** A delegate from a new class back to a
substantial `GameWindow` method is a useful intermediate seam, not a
completed extraction.
7. **No duplicate ownership.** New collaborators query `LiveEntityRuntime`,
`GpuWorldState`, `SelectionState`, or the relevant existing owner. They do
not create replacement GUID, visibility, session, or resource maps.
---
## 6. What this document is **not**
- **Not a full rewrite plan.** The point is the *opposite* — small
steps, verified at each boundary.
- **Not a feature phase.** Following the 2026-07-21 user decision, this
behavior-preserving campaign is the structural prerequisite before new M4
subsystem bodies are added. Severe regressions remain fixable in separate
commits; ordinary feature work waits.
- **Not a substitute for the milestones / roadmap.** Those drive the
feature work. This drives the structural work that runs underneath.