Record the measured GameWindow reduction, final review and test gates, durable selection/inventory ownership rules, and live animation presentation as Slice 2. Co-authored-by: OpenAI Codex <codex@openai.com>
595 lines
34 KiB
Markdown
595 lines
34 KiB
Markdown
# acdream — code structure & extraction sequence
|
|
|
|
**Status:** Living document. Created 2026-05-16; implementation reconciliation
|
|
completed 2026-07-21; Slice 1 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
|
|
```
|
|
|
|
`GameWindow` is the single object that:
|
|
|
|
- Owns the GL context, the window, input, and shaders.
|
|
- Reads ~40 different environment variables across its lifetime.
|
|
- Still owns live-session bootstrap, character entry, roughly 440 lines of
|
|
event subscription, and most App-side packet handlers even though
|
|
`LiveSessionController` owns the socket/session shell.
|
|
- 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
|
|
│ └── 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 | **Partial** | `LiveSessionController` owns endpoint resolution, `WorldSession` construction, ticking, and disposal (`0b25df53`). `TryStartLiveSession`, character choice/entry, session clearing, about 440 lines of subscriptions, and most App packet handlers remain in `GameWindow`. |
|
|
| 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`, static scheduler, remote/projectile updaters, and teleport owners are shipped. `AdvanceLiveObjectRuntimeCore`, final part/mesh/effect composition, and several integration tails remain in `GameWindow`. |
|
|
| 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 — NEXT
|
|
|
|
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.
|
|
|
|
#### Slice 3 — complete live-session ownership
|
|
|
|
Expand `LiveSessionController` to own connect, character-list validation,
|
|
character selection, EnterWorld, graceful character logout, reconnect, and
|
|
session-scoped disposal. Add `LiveSessionEventRouter` for typed subscriptions;
|
|
it routes events to focused domain handlers and owns unsubscription, but owns
|
|
no entity or UI state.
|
|
|
|
Move `TryStartLiveSession`, `ClearInboundEntityState`, and
|
|
`WireLiveSessionEvents` out of `GameWindow`. Do not create a giant context bag
|
|
or a second world-state owner. The connected gate must prove login, fresh world
|
|
reveal, command/chat, portal, exact `0xF653` logout, ACE endpoint disconnect,
|
|
and fresh-process reconnect.
|
|
|
|
#### Slice 4 — extract live-entity App integration
|
|
|
|
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.
|
|
|
|
#### 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.
|