docs(architecture): reconcile GameWindow extraction program

Close the completed M3 and portal observer gates, make the behavior-preserving GameWindow ownership campaign the explicit M4 prerequisite, and replace the stale extraction sketch with an implementation-audited sequence.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-21 06:44:29 +02:00
parent f4cf802330
commit cf50ee3d50
8 changed files with 354 additions and 220 deletions

View file

@ -829,12 +829,18 @@ Current movement/collision ownership:
- **G.3 is shipped**: dungeon streaming, portal-space presentation, collision,
lighting, doors, and the full round-trip gate landed. The separate far-
teleport unstreamed-edge residual is issue #153, not unfinished G.3.
- **M3** owns current connected magic/portal acceptance; Track LH is the future
presentation-independent Linux/headless extraction.
- **M3 landed 2026-07-21** with connected magic/portal acceptance and the final
two-client observer gate; Track LH is the future presentation-independent
Linux/headless extraction.
- **The behavior-preserving thin-`GameWindow` ownership campaign is the active
prerequisite before new M4 subsystem bodies.** Its reconciled ledger and
sequence live in `docs/architecture/code-structure.md`. This decision does
not reclassify the remaining L.2 conformance work or Track LH.
The GameEntity / thin GameWindow refactor remains a valid target architecture,
but it is not a prerequisite for L.2. Do not resurrect old R1-R8 phase numbers
for new work; add or update roadmap phases instead.
Full `GameEntity` type aggregation remains a separate, higher-risk target after
the thin-`GameWindow` campaign. The current campaign reuses canonical
`LiveEntityRuntime` rather than introducing another entity owner. Do not
resurrect old R1-R8 phase numbers for new work; update the roadmap instead.
---

View file

@ -1,11 +1,12 @@
# acdream — code structure & extraction sequence
**Status:** Living document. Created 2026-05-16 as the companion to the
"Code Structure Rules" section in `CLAUDE.md`.
**Status:** Living document. Created 2026-05-16; implementation reconciliation
completed 2026-07-21. 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 14,557-line `GameWindow.cs` at the
2026-07-20 audit) to the
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
@ -21,30 +22,37 @@ layer is wire-compatible, the UI has a stable contract, plugins load.
The structural debt is concentrated in **one file**:
```
src/AcDream.App/Rendering/GameWindow.cs 14,557 lines (2026-07-20)
src/AcDream.App/Rendering/GameWindow.cs 15,723 lines (2026-07-21)
258 fields / ~259 methods
```
`GameWindow` is the single object that:
- Owns the GL context, the window, input, and shaders.
- Reads ~40 different environment variables across its lifetime.
- Hosts the live network session (`WorldSession`) and the offline
pre-login state.
- Owns parallel dictionaries for entity lookup (`_entitiesByServerGuid`,
the per-landblock entity lists in `GpuWorldState`, plus the player
controller's own state).
- Drives selection / interaction (`WorldPicker`, `SendUse`,
`SendPickUp`).
- 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.
- Still drives selection / interaction (`WorldPicker`, target classification,
`SendUse`, `SendPickUp`, auto-walk deferral) despite the shipped item and
combat controllers.
- 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.
Almost every M1 / M2 bug touches this file. Every new feature adds a
field plus a method plus a wiring call. It is not getting better on its
own.
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's a
high-risk change that would block M2. The fix is to **extract one
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.
@ -167,6 +175,7 @@ src/AcDream.App/
│ ├── 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)
@ -176,11 +185,13 @@ src/AcDream.App/
│ ├── Wb/ # WB seam + EnvCellLandblockBuild transaction
│ └── Vfx/ # (already exists)
├── Net/
│ └── LiveSessionController.cs # owns WorldSession lifecycle, login/handshake, reconnect
│ ├── 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
@ -189,13 +200,18 @@ src/AcDream.App/
├── 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/
│ └── SelectionInteractionController.cs # owns WorldPicker, selection state, Use/PickUp dispatch
├── Streaming/ # LandblockStreamer + immutable LandblockBuild completion
│ ├── 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)
@ -206,22 +222,27 @@ What `GameWindow` keeps:
- `IWindow` / `GL` / `IInputContext` lifecycle (constructor + `OnLoad` +
`Run` + `OnClosing`).
- `RuntimeOptions` reference (the typed startup config).
- One field per collaborator (`_liveSessionController`,
`_liveEntityRuntime`, `_selectionInteraction`,
`_renderFrameOrchestrator`).
- 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:
- The 7 startup-time env var fields → moved into `RuntimeOptions`.
- `TryStartLiveSession` + the post-login network drain → moved into
`LiveSessionController`.
- `_entitiesByServerGuid` + per-entity dictionaries + ServerGuid↔Id
translation → moved into `LiveEntityRuntime`.
- `WorldPicker` + selection-driven `SendUse` / `SendPickUp` orchestration → moved
into `SelectionInteractionController`; Core `SelectionState` remains the
already-shipped session owner and is injected into that controller.
- Per-frame draw orchestration → moved into `RenderFrameOrchestrator`.
- 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
@ -247,10 +268,14 @@ 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 body is no longer an animation-dictionary loop inside
`GameWindow`. `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. Manager-less
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
@ -336,145 +361,194 @@ strand teleport, movement, shadow, light, or GUID-scoped state.
---
## 4. Extraction sequence — safest first
## 4. Reconciled ownership ledger — 2026-07-21
Each step is **one PR-sized refactor**. Each must build clean, all
tests pass, and visual verification at Holtburg looks identical to
the previous step. Don't bundle two steps.
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.
### Step 1 — `RuntimeOptions` — SHIPPED (`eda936dc`)
### 4.1 What counts as extracted
**Scope:** Replace startup-time env var reads with a typed options
object built once in `Program.cs`.
An extraction is **complete** only when:
**Behavior change:** None. Same env vars, same defaults, same effects.
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.
**Risk:** Low. Mechanical substitution at ~10-15 call sites in
`GameWindow.cs` + one constructor signature change.
A class that stores delegates back to substantial `GameWindow` methods is a
useful ordering seam, but its ownership status is **partial**.
**Test:** Unit tests for `RuntimeOptions.FromEnvironment` parsing (the
new `tests/AcDream.App.Tests/` project).
### 4.2 Current implementation truth
**Verification:** `dotnet build` + `dotnet test` green. Visual launch
verifies live mode + dat dir resolution still work.
| 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 | **Partial** | `ItemInteractionController`, `CombatTargetController`, and `CombatAttackController` own focused rules. `GameWindow` still owns picking, target queries, selection intent, Use/PickUp sends, range/auto-walk deferral, and related input cases. |
| 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. |
### Step 2 — `LiveSessionController` — SHIPPED (`0b25df53`)
### 4.3 Revised extraction sequence
**Scope:** Extract `TryStartLiveSession` + the WorldSession ownership +
the post-EnterWorld drain (`OnLiveStateUpdated`, `OnLiveEntityDeleted`,
etc.) into a controller class.
Every numbered slice is behavior-preserving and independently committed. A
slice does not include feature work or opportunistic gameplay fixes.
**Behavior change:** None. Same wire behavior, same handshake.
#### Gate 0 — deterministic baseline — COMPLETE
**Risk:** Medium. WorldSession lifecycle is load-bearing — every
session-state crash would surface here. The change is a class
extraction with the same event subscriptions, not a rewrite.
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.
**Test:** Existing `AcDream.Core.Net.Tests` already cover the wire
layer. The controller itself gets a smoke test that verifies it can be
constructed without a live socket (offline mode).
#### Slice 1 — finish selection/interaction ownership — NEXT
**Verification:** Visual login + Holtburg traversal + door interaction
identical to pre-extraction.
Split the old Step 4 into three reviewable commits:
### Step 3 — `LiveEntityRuntime` — SHIPPED 2026-07-14
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`.
**Shipped scope:** One `LiveEntityRecord` per server-object incarnation now owns
ServerGuid↔local-ID translation, accepted state, runtime components, parent
relations, logical resource activation, exact teardown, and spatial projection
state. `RegisterLiveEntity`, `RebucketLiveEntity`, and `UnregisterLiveEntity`
make the lifetime boundary explicit. Landblock unload/reload moves the same
`WorldEntity`; it cannot reconstruct from a stale CreateObject or replay setup
scripts. Equipped children use an attached projection and never enter the
top-level target/radar/status view. Canonical materialized lookup remains
available while a projection is pending; the separate visible view is the only
surface radar, picking, status, and targeting consume. Pickup/parent leave-world
clears cell membership and pauses root movement/animation without destroying the
retained owners. Top-level spawn publication is one-shot per incarnation, so
leave/re-entry restores presentation without duplicating plugin event replay.
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 connected gate is select → use door/NPC → distant corpse → loot/pickup →
combat target. This is the safest next slice because its outbound boundaries
are typed and existing controllers already hold most retail rules.
**Remaining target:** the player-specific controller is still a separate
aggregation. The focused effect queue is shipped; AD-32 now tracks only the
future non-effect, non-Parent packet queue.
#### Slice 2 — finish live animation presentation
**Behavior change:** Spatial withdrawal and re-entry now preserve logical
identity and active resources instead of replaying create-time effects.
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`.
**Risk:** Medium-high. Entity lookup is in every hot path. The change
is structural (one owner instead of three) but the lookup semantics
must be byte-identical.
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.
**Test:** `LiveEntityRuntimeTests` cover duplicate CreateObject, generation
replacement, appearance mutation, loaded/pending rebucketing, attached
projection, pickup leave/re-entry, canonical-versus-visible lookup, resource
rollback, GUID reuse, and idempotent session teardown.
#### Slice 3 — complete live-session ownership
**Verification:** Walk Holtburg, click NPC, open door, pick up item.
All four M1 demo targets must still work.
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.
### Step 4 — `SelectionInteractionController`
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.
**Scope:** Extract `WorldPicker`, the Core `SelectionState` consumers, `SendUse`,
`SendPickUp`, and the `InputAction.Select*` / `UseSelected` /
`SelectionPickUp` switch cases into one controller. Depends on Step 3
(uses `LiveEntityRuntime`).
#### Slice 4 — extract live-entity App integration
**Behavior change:** None.
This is two owners, not one replacement god object:
**Risk:** Low-medium. Selection state is local to interactions; the
network outbound side is well-defined (`InteractRequests.BuildUse` /
`BuildPickUp`).
- `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.
**Test:** Selection state machine tests in `tests/AcDream.App.Tests/`.
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.
**Verification:** Click-to-select, double-click-to-Use, F-key pickup
all still work.
#### Slice 5 — extract landblock presentation
### Step 5 — `RenderFrameOrchestrator`
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.
**Scope:** Extract the per-frame draw sequence (sky → terrain →
opaque mesh → translucent mesh → particles → debug → UI) into a
dedicated orchestrator that `GameWindow.OnRender` delegates to.
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.
**Behavior change:** None. Same draw order, same GL state.
#### Slice 6 — extract update-frame orchestration
**Risk:** Medium. GL state management is touchy; the orchestrator
must hand the GL context to the same renderers in the same order with
the same per-pass state setup.
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.
**Test:** Visual verification only. Render orchestration is hard to
unit-test without a GL context.
Frame-order tests and the existing R6 gate must produce the same lifecycle and
movement traces before and after extraction.
**Verification:** Holtburg at radius 4, radius 8, radius 12 looks
identical across all four quality presets.
#### Slice 7 — extract `RenderFrameOrchestrator`
### Step 6 — `GameEntity` aggregation (the big one)
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.
**Scope:** Consolidate `WorldEntity` + `AnimatedEntity` + the per-entity
state in `LiveEntityRuntime` into one `GameEntity` class (the target
described in `acdream-architecture.md`). Every entity in the world —
player, NPC, monster, door, item — becomes a single `GameEntity`.
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.
**Behavior change:** None at the wire / visual level; substantial at
the call-site level (everyone moves to the new entity API).
#### Slice 8 — composition and shutdown cleanup
**Risk:** High. Touches every system that reads entity state.
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.
**Test:** All existing tests + the new `AcDream.App.Tests` suite. Visual
verification at every M1 / M2 scenario.
### 4.4 Exit criteria
**Verification:** Full M2 demo loop (equip sword, kill drudge, pick up
loot, open inventory) works identically.
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 step at a time.** A PR that ships Step 1 ships only Step 1.
Bundling steps makes failures hard to isolate.
2. **Behavior preservation is the acceptance criterion.** Every step
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 M1 / M2 scenarios must succeed. We're moving code, not
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
@ -484,9 +558,15 @@ loot, open inventory) works identically.
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 Step 3 turns out
to need a different shape than described above, update §4 in the
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.
---
@ -494,8 +574,9 @@ loot, open inventory) works identically.
- **Not a full rewrite plan.** The point is the *opposite* — small
steps, verified at each boundary.
- **Not blocking M2.** Step 1 is small enough to ship without
disrupting M2 work. Later steps interleave with M2 / M3 phases as
the corresponding code paths come into focus.
- **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.