642 lines
37 KiB
Markdown
642 lines
37 KiB
Markdown
# acdream — code structure & extraction sequence
|
||
|
||
**Status:** Living document. Created 2026-05-16; implementation reconciliation
|
||
completed 2026-07-21; Slices 1–3 landed the same day. This 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
|
||
```
|
||
|
||
`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 # per-frame draw order (sky → terrain → opaque → trans → particles → debug → UI)
|
||
│ ├── 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
|
||
│ ├── 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 core owner** | `LiveEntityRuntime` owns incarnation identity, accepted snapshots/timestamps, runtime components, logical/spatial lifetime, and teardown. `GameWindow` still owns large hydration and wire-to-presentation adapter bodies. |
|
||
| Inbound/object-frame order | **Partial** | `RetailInboundEventDispatcher`, `RetailLiveFrameCoordinator`, `LiveEntityAnimationScheduler`, `LiveEntityAnimationPresenter`, static scheduler, remote/projectile updaters, and teleport owners are shipped. `AdvanceLiveObjectRuntimeCore` retains the visible cross-owner order until Slice 6. |
|
||
| 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 | **Not extracted** | Residency scheduling is extracted, but DAT build, scenery/EnvCell construction, apply, collision/resource publication, and retirement glue remain a large `GameWindow` body. |
|
||
| 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
|
||
|
||
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)
|
||
(active).
|
||
|
||
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
|
||
(Position/Vector/State/Movement and equal-generation CreateObject routing) is
|
||
active.
|
||
|
||
#### Slice 5 — extract landblock presentation
|
||
|
||
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. Run the deterministic world-lifecycle gate
|
||
and the seven-destination resource soak.
|
||
|
||
#### Slice 6 — extract update-frame orchestration
|
||
|
||
After the stateful bodies above have owners, make the update path a real
|
||
orchestrator instead of delegates back into the window. It owns the fixed
|
||
retail phase order: input/local object → ordinary/static objects → hooks/
|
||
particles/scripts → inbound network → command interpreter → non-advancing
|
||
spatial reconcile → streaming/UI updates. `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.
|
||
|
||
#### Slice 7 — extract `RenderFrameOrchestrator`
|
||
|
||
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.
|