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:
parent
f4cf802330
commit
cf50ee3d50
8 changed files with 354 additions and 220 deletions
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue