From a36a7015c4aa9c5cb6c1e4ca46d18c6d4e3eb32d Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 22 Jul 2026 00:06:55 +0200 Subject: [PATCH] docs(architecture): plan GameWindow update-frame extraction --- docs/architecture/code-structure.md | 16 +- .../retail-divergence-register.md | 5 +- docs/plans/2026-04-11-roadmap.md | 4 +- docs/plans/2026-05-12-milestones.md | 3 +- ...ndow-slice-6-update-frame-orchestration.md | 442 ++++++++++++++++++ ...07-19-r6-update-object-order-pseudocode.md | 161 +++++-- 6 files changed, 594 insertions(+), 37 deletions(-) create mode 100644 docs/plans/2026-07-21-gamewindow-slice-6-update-frame-orchestration.md diff --git a/docs/architecture/code-structure.md b/docs/architecture/code-structure.md index efb4d015..9fc6cd54 100644 --- a/docs/architecture/code-structure.md +++ b/docs/architecture/code-structure.md @@ -574,12 +574,18 @@ nine-destination resource soak pass. #### Slice 6 — extract update-frame orchestration +Detailed execution ledger: +[`docs/plans/2026-07-21-gamewindow-slice-6-update-frame-orchestration.md`](../plans/2026-07-21-gamewindow-slice-6-update-frame-orchestration.md) +(active). + 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. +orchestrator instead of delegates back into the window. It preserves the +accepted production order—not an overclaimed retail host order—covering +teardown/clock/streaming convergence, input, the retail-shaped object → inbound +network → command barrier, conditional spatial reconciles, liveness, local +teleport, player-mode entry, and camera presentation. The exact twelve-phase +graph and registered TS-53 host-order adaptation live in the detailed ledger. +`GameWindow.OnUpdate` becomes a short time/input handoff. Frame-order tests and the existing R6 gate must produce the same lifecycle and movement traces before and after extraction. diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index 39fcd22c..b2f3dd95 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -218,7 +218,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps. | AP-119 | Equal-generation CreateObject refresh applies the packet's complete `PhysicsDesc` to the existing `EntityEffectProfile`, including replacing its network sound/PES-table/default-script description. Retail's equal-`INSTANCE_TS` branch applies the individual ObjDesc/Parent-or-Position/Movement/State/Vector/WeenieDesc tail and does not call `CPhysicsObj::set_description` again. | `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs` (`ILiveEntitySameGenerationUpdateSink.OnDescription`); `LiveEntitySameGenerationUpdateRouter.cs` | Existing spell/projectile/portal VFX tests and connected behavior were accepted with this refresh. Slice 4 records and isolates it rather than silently changing DAT-effect ownership during an architecture extraction. | A same-generation CreateObject whose PeTable/sound/default-script fields differ from the original can replace effect lookup state where retail would retain the original table, producing a different later typed effect. | `SmartBox::HandleCreateObject @ 0x00454C80`; `CPhysicsObj::set_description @ 0x00514F40`; `docs/research/2026-07-13-retail-projectile-vfx-pseudocode.md` | | AP-120 | `ObjectTableWiring.ApplyEntitySpawn` publishes the CreateObject's WeenieDesc/item state before the same-generation physics update tail. Retail applies WeenieDesc after ObjDesc, Parent-or-Position/Pickup, Movement, State, and Vector. | `src/AcDream.Core.Net/ObjectTableWiring.cs` (`ApplyEntitySpawn`); `src/AcDream.App/World/LiveEntitySameGenerationUpdateRouter.cs` | Core.Net owns item-table ingestion before App callbacks and the current single-thread FIFO prevents a second network packet from interleaving; changing publication order crosses the Core.Net/App ownership boundary and requires a separately tested event transaction. | A synchronous item-table observer can see the refreshed WeenieDesc while the same object's physics/parent/state still reflects the prior snapshot; retail observers see the completed physics tail first. | `SmartBox::HandleCreateObject @ 0x00454C80`; `ACCObjectMaint::CreateObject @ 0x00558870` | -## 4. Temporary stopgap (TS) — 34 active rows + TS-37 historical note (TS-20 retired 2026-07-16 — the later named-retail audit disproved the proposed DrawingBSP polygon filter; TS-37 is a retired-row historical note, not an active count; TS-39 retired R5-V3 — sticky seams bound to the ported PositionManager/StickyManager, radii threaded; TS-45 retired 2026-07-07 — hand-rolled `SphereCollision` replaced by the faithful CSphere family port, fixing the player-vs-monster crowd wedge; TS-3 retired 2026-07-07 — `frames_stationary_fall` accounting ported in the #182 verbatim UpdateObjectInternal rebuild, fixing the airborne falling-animation wedge; TS-41 retired 2026-07-07 — SERVERVEL synth-velocity remote body-drive replaced by the retail interp catch-up + unconditional MovementManager::UseTime, the remote-creature de-overlap #184; TS-42 retired 2026-07-19 — semantic animation completion now precedes the ordered Target/Movement/PartArray/Position tail; TS-44 narrowed again 2026-07-19 — complete orientation joined interpolation, only during-stick enqueue suppression remains) +## 4. Temporary stopgap (TS) — 35 active rows + TS-37 historical note (TS-20 retired 2026-07-16 — the later named-retail audit disproved the proposed DrawingBSP polygon filter; TS-37 is a retired-row historical note, not an active count; TS-39 retired R5-V3 — sticky seams bound to the ported PositionManager/StickyManager, radii threaded; TS-45 retired 2026-07-07 — hand-rolled `SphereCollision` replaced by the faithful CSphere family port, fixing the player-vs-monster crowd wedge; TS-3 retired 2026-07-07 — `frames_stationary_fall` accounting ported in the #182 verbatim UpdateObjectInternal rebuild, fixing the airborne falling-animation wedge; TS-41 retired 2026-07-07 — SERVERVEL synth-velocity remote body-drive replaced by the retail interp catch-up + unconditional MovementManager::UseTime, the remote-creature de-overlap #184; TS-42 retired 2026-07-19 — semantic animation completion now precedes the ordered Target/Movement/PartArray/Position tail; TS-44 narrowed again 2026-07-19 — complete orientation joined interpolation, only during-stick enqueue suppression remains) | # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle | |---|---|---|---|---|---| @@ -258,8 +258,9 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps. | TS-48 | Dragging an item onto another player honors the authoritative `DragItemOnPlayerOpensSecureTrade` option, but the option's default-true branch stops at the existing unavailable toast because the secure-trade transaction and UI are not ported. Direct player giving through `GiveObjectRequest 0x00CD` works when the option is disabled; NPC giving is complete. | `src/AcDream.App/UI/ItemInteractionController.cs` (`PlaceIn3D`, `PolicyActionMessage`); `src/AcDream.Core/Items/ItemInteractionPolicy.cs` | The player/NPC distinction and character preference are now faithful; inventing a direct gift while the option requests secure trade would be a worse behavioral divergence. Secure trade is a separate multi-party state machine beyond the starter-dungeon NPC-give slice. | With retail's default character options, an item dragged onto another player cannot be exchanged until the secure-trade subsystem lands. | `ItemHolder::AttemptPlaceIn3D @ 0x00588600`; `PlayerModule::DragItemOnPlayerOpensSecureTrade @ 0x005D31B0`; `ClientTradeSystem`; `docs/research/2026-07-13-retail-give-item-pseudocode.md` | | TS-49 | Hidden-object availability is bridged through `TargetManager.NotifyVoyeurOfEventAndClear(ExitWorld)` because acdream has not ported retail's DetectionManager. Retail `CObjCell::hide_object` sends `LeftDetection` to detection voyeurs; acdream instead withholds Hidden hosts from ordinary `GetObjectA` relationship creation and uses the existing non-Ok target update to tear down MoveTo/Sticky consumers and clear watched-role subscriptions while preserving the hidden object's own watcher role. | `src/AcDream.App/Physics/EntityPhysicsHost.cs` (`NotifyHidden`); `src/AcDream.App/Physics/LiveEntityMotionRuntimeController.cs` (`ResolvePhysicsHost`); `src/AcDream.Core/Physics/Motion/TargetManager.cs` (`NotifyVoyeurOfEventAndClear`) | The current movement consumers already share TargetManager's status fan-out; the bridge prevents pursuit of an unavailable object without inventing a second partial detection database. | Plugins or future systems listening specifically for retail detection enter/leave events receive no `LeftDetection`; only movement/sticky target consumers observe the equivalent availability loss. | `CObjCell::hide_object @ 0x0052BE30`; retire by porting DetectionManager/CObjCell detection-voyeur delivery and routing Hidden through `LeftDetection` | | TS-50 | `AnimationDone` executes semantically at each owner's retail `CPhysicsObj::process_hooks` boundary, but all other animation hooks are retained in `AnimationHookFrameQueue` until final root/part/equipped-child pose publication. Retail executes the complete hook stream before transition and the Target/Movement/PartArray/Position manager tail because its current CPartArray pose already exists in-place. Static owners correctly reach `process_hooks` only after their root, parts, and children are current. | `src/AcDream.App/Rendering/Vfx/AnimationHookFrameQueue.cs`; `src/AcDream.App/Rendering/RetailStaticAnimatingObjectScheduler.cs`; frame drain in `src/AcDream.App/Rendering/GameWindow.cs` | The modern renderer publishes immutable effect-pose snapshots after all root/child composition; deferred visual sinks avoid attaching particles/lights/audio to the previous pose. Semantic `AnimationDone` is split out and exact, so motion completion and manager behavior are not delayed. Pose-owner lifetime tokens prevent deferred hooks from crossing delete/local-ID reuse. | A non-AnimationDone hook with same-quantum semantic consequences (notably `CallPES`, default-script chaining, audio/particle creation relative to a transition) runs later than retail and can observe post-tail state or start one render frame late. | `CPhysicsObj::process_hooks @ 0x00511550`; `CPhysicsObj::UpdatePositionInternal @ 0x00512C30`; `CPhysicsObj::animate_static_object @ 0x00513DF0`; retire by publishing the current per-object/child pose before hook routing or splitting semantic and presentation sinks without changing authored hook order | -| TS-51 | Particle and PhysicsScript tails advance once per render frame after the complete ordinary/static object worksets. Retail advances each object's ParticleManager and ScriptManager inside every admitted `UpdateObjectInternal` quantum; `animate_static_object` also advances that static owner's managers using its whole admitted elapsed interval. | `src/AcDream.App/Rendering/GameWindow.cs` (`AdvanceLiveObjectRuntime` final `_particleSystem.Tick(dt)` / `_scriptRunner.Tick(...)`) | The current managers are shared presentation/runtime owners rather than per-object manager instances. R6 makes root motion, animation, object clocks, workset membership, and manager order faithful without pretending the shared tails have per-owner timing. Splitting ownership safely requires a later effect-lifetime slice. | A render fragment below retail's minimum object quantum can advance an effect while its owner waits; a catch-up frame advances an owner's root through several quanta but its effect tail only once; static default scripts/particles use render elapsed rather than `animate_static_object` elapsed/discard timing. | `CPhysicsObj::UpdateObjectInternal @ 0x005156B0`; `CPhysicsObj::animate_static_object @ 0x00513DF0`; retire by giving live/static owners incarnation-bound particle/script managers and ticking each manager in the owning object quantum | +| TS-51 | Particle and PhysicsScript tails advance once per render frame after the complete ordinary/static object worksets. Retail advances each ordinary object's ParticleManager then ScriptManager inside every admitted `UpdateObjectInternal` quantum; `animate_static_object` instead advances that static owner's ScriptManager then ParticleManager and only then `process_hooks`, using its whole admitted elapsed interval. acdream's shared tail is Particle → Script after static hook capture. | `src/AcDream.App/Rendering/GameWindow.cs` (`AdvanceLiveObjectRuntime` final `_particleSystem.Tick(dt)` / `_scriptRunner.Tick(...)`); `src/AcDream.App/Rendering/RetailStaticAnimatingObjectScheduler.cs` | The current managers are shared presentation/runtime owners rather than per-object manager instances. R6 makes root motion, animation, object clocks, workset membership, and ordinary manager order faithful without pretending the shared tails have per-owner timing or static-tail order. Splitting ownership safely requires a later effect-lifetime slice. | A render fragment below retail's minimum object quantum can advance an effect while its owner waits; a catch-up frame advances an owner's root through several quanta but its effect tail only once; static hooks can route before their script/particle managers and static default scripts/particles use render elapsed in Particle → Script order rather than `animate_static_object` elapsed/discard and Script → Particle → hooks timing. | `CPhysicsObj::UpdateObjectInternal @ 0x005156B0`; `CPhysicsObj::animate_static_object @ 0x00513DF0`; retire by giving live/static owners incarnation-bound particle/script managers and ticking each manager in the owning object quantum/order | | TS-52 | The terrain shader applies retail-authored base/overlay/road `TerrainTex.TexTiling` but omits the separate Environment Detail Textures pass and its viewer-distance fade (**#226**). | `src/AcDream.App/Rendering/TerrainAtlas.cs`; `src/AcDream.App/Rendering/TerrainModernRenderer.cs`; `src/AcDream.App/Rendering/Shaders/terrain_modern.frag` | `bb5acab9` fixed the user-visible stretched/blurry regression by porting the distinct base-tiling contract. An earlier experimental detail array darkened the whole ground because its source/neutral blend contract was wrong, so it was correctly reverted rather than guessed into production. | With retail's Environment Detail Textures preference enabled, close terrain lacks the extra high-frequency modulation/fade even though authored base texture scale is correct. | `LScape::GenerateDetailSurfaces` / `SetDetailTexturing @ 0x00506B40`; `ACRender::landPolyDraw @ 0x006B6450..0x006B6525`; issue #226 | +| TS-53 | acdream advances retained UI time on the draw seam and local teleport/UI-camera presentation after its SmartBox-shaped object → inbound network → CommandInterpreter barrier. Retail `Client::UseTime` calls `UIElementManager::UseTime` first, whose global time message reaches `gmSmartBoxUI::UseTime`, and publishes player-camera work from the physics/player callback rather than one post-network camera tail. Slice 6 preserves the accepted host order as an ownership-only extraction. | `src/AcDream.App/Rendering/GameWindow.cs` (`OnUpdate` teleport/camera tails and `_retailUiRuntime.Tick` in `OnRender`); `docs/plans/2026-07-21-gamewindow-slice-6-update-frame-orchestration.md` | Current retained UI, portal transit, reveal, camera, and connected movement traces are accepted; changing cross-subsystem host order while extracting ownership would combine a behavior change with the structural cutover. | Retained UI, teleport, and camera presentation can observe same-frame object/inbound/player state one host update earlier or later than retail at transition boundaries; a future exact host-order port must prove UI, input, reveal, and camera consequences together. | `Client::UseTime @ 0x00411C40`; `UIElementManager::UseTime`; `gmSmartBoxUI::UseTime @ 0x004D6E30`; `CPhysics::UseTime @ 0x00509950`; retire only with a focused host-order port and connected portal/camera comparison | --- diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md index 87c4edf4..f2023076 100644 --- a/docs/plans/2026-04-11-roadmap.md +++ b/docs/plans/2026-04-11-roadmap.md @@ -56,7 +56,9 @@ session replacement, including failure/retry and fast relogin. Shutdown keeps the reset graph alive until disposal converges. `GameWindow` is 8,811 raw lines / 247 fields / 153 methods (44% below the campaign baseline); 7,054 Release tests, the capped/reconnect lifecycle gate, and the synchronized -nine-stop resource soak pass. Slice 6 update-frame orchestration is active. +nine-stop resource soak pass. Slice 6 update-frame orchestration is active; +its detailed checkpoint ledger is +[`2026-07-21-gamewindow-slice-6-update-frame-orchestration.md`](2026-07-21-gamewindow-slice-6-update-frame-orchestration.md). This is a behavior-preserving structural program. Severe regressions still get root-cause fixes in separate commits; ordinary feature work resumes with M4 diff --git a/docs/plans/2026-05-12-milestones.md b/docs/plans/2026-05-12-milestones.md index 0b248c75..ff932553 100644 --- a/docs/plans/2026-05-12-milestones.md +++ b/docs/plans/2026-05-12-milestones.md @@ -27,7 +27,8 @@ origin lifetimes. `GameWindow` is 8,811 raw lines / 247 fields / 153 methods, down 1,490 lines from the Slice-5 baseline and 6,912 (44%) from the campaign baseline. The full Release suite passes 7,054 tests / 5 skips; the 311-second capped/reconnect lifecycle gate and synchronized 394-second nine-destination -resource soak both pass. Slice 6 update-frame orchestration is next. +resource soak both pass. Slice 6 update-frame orchestration is active under +[`2026-07-21-gamewindow-slice-6-update-frame-orchestration.md`](2026-07-21-gamewindow-slice-6-update-frame-orchestration.md). Carried: #153 far-teleport residual, #116 slide-response, the remaining R6 ownership cleanup plus diff --git a/docs/plans/2026-07-21-gamewindow-slice-6-update-frame-orchestration.md b/docs/plans/2026-07-21-gamewindow-slice-6-update-frame-orchestration.md new file mode 100644 index 00000000..6de9b5d2 --- /dev/null +++ b/docs/plans/2026-07-21-gamewindow-slice-6-update-frame-orchestration.md @@ -0,0 +1,442 @@ +# GameWindow Slice 6 — update-frame orchestration + +**Status:** Active 2026-07-21. +**Parent program:** [`docs/architecture/code-structure.md`](../architecture/code-structure.md), Slice 6. +**Baseline:** `d4ecac1d`; `GameWindow.cs` is 8,811 raw lines, 247 fields, and +153 methods. Release baseline: 7,054 passed / 5 skipped. The connected +lifecycle gate and synchronized nine-stop resource soak both pass. +**Behavior rule:** This is an ownership extraction. Preserve the accepted +retail object/network order, asynchronous streaming-readiness adaptation, +portal transit, input semantics, and camera presentation. A defect exposed by +the boundary receives its own deterministic oracle and bisectable fix; it is +never hidden behind a retry, delay, suppression flag, or reordered callback. + +## Progress ledger + +- [ ] A — freeze the complete production phase graph and introduce the typed + orchestration contract plus structural/order guards. +- [ ] B — extract the pre-network live-object presentation phase and the + non-advancing post-network spatial reconciler; remove callbacks into + `GameWindow` from `RetailLiveFrameCoordinator`. +- [ ] C — extract streaming-origin convergence, observer selection, dungeon + collapse, streaming tick, and rescued-entity rebucketing. +- [ ] D — extract dispatcher/raw-mouse/combat input sampling without changing + movement-command or UI-capture semantics. +- [ ] E — extract the complete local-player teleport owner and wire it between + the existing post-network liveness and player-mode auto-entry phases. +- [ ] F — extract the shared player-mode lifecycle plus fly/chase/player + camera presentation and cut production over to one + `UpdateFrameOrchestrator`. +- [ ] G — delete the old `OnUpdate` bodies, callback facades, and obsolete + frame state; run focused corrected-diff reviews after each ownership edge. +- [ ] H — full Release suite, connected lifecycle/reconnect gate, synchronized + resource soak, documentation, durable memory, and line/field/method closeout. + +Every checked checkpoint is committed as one bisectable architectural unit. +Documentation records the exact commit and accepted test count as work lands. + +## 1. Outcome and non-goals + +At slice exit, `GameWindow.OnUpdate` starts the profiler scope and delegates one +host delta to `UpdateFrameOrchestrator`. `GameWindow` composes concrete owners; +it no longer selects a streaming observer, advances animations/effects, +samples raw mouse input, sequences inbound network traffic, advances portal +transit, or updates gameplay cameras. + +The owner graph is explicit rather than a list of anonymous callbacks: + +```text +UpdateFrameOrchestrator + ├── live teardown convergence retry retained tombstones + ├── UpdateFrameClock normalized host time / PES clock + ├── StreamingFrameController residence/readiness pre-pass + ├── GameplayInputFrameController semantic input + mouse/combat intent + ├── RetailLiveFrameCoordinator retail SmartBox object barrier + │ ├── LiveObjectFrameController local/ordinary/static/effect tails + │ ├── LiveSessionController inbound network drain + │ ├── RetailLocalPlayerFrame CommandInterpreter position tail + │ └── LiveSpatialReconciler non-advancing authoritative refresh + ├── LiveEntityLivenessController expiry after accepted inbound state + ├── LocalPlayerTeleportController local transit/reveal/tunnel state + ├── PlayerModeAutoEntry one-shot ready-world entry + ├── PlayerModeController enter/exit/current-player lifetime + └── CameraFrameController fly/chase/player presentation +``` + +Concrete names may be narrowed during checkpoint A's source audit, but the +ownership boundaries and order do not change. The orchestrator receives a +small immutable per-frame input and concrete phase owners. It does not receive +a hundred delegates, one mega `IUpdateFrameServices` context, or a service +locator, and no transitive update collaborator calls a substantial method back +on `GameWindow`. + +This slice does not change simulation rate, object quantum rules, movement +speed, interpolation, streaming radii, reveal readiness, portal visuals, +particle range, combat cadence, key bindings, camera formulas, render order, +or shutdown order. It adds no GL work and does not move retained-UI timers out +of their render seam. Slice 7 owns the draw graph; Slice 8 owns final +composition and Silk callback cleanup. + +## 2. Retail oracle and accepted adaptation + +The object/network barrier is pinned by +[`2026-07-19-r6-update-object-order-pseudocode.md`](../research/2026-07-19-r6-update-object-order-pseudocode.md): + +- `CPhysicsObj::update_object @ 0x00515D10` applies the strict object quantum; +- `CPhysicsObj::UpdatePositionInternal @ 0x00512C30` advances the PartArray, + position/physics candidate, then processes semantic hooks; +- `CPhysicsObj::UpdateObjectInternal @ 0x005156B0` runs Detection → Target → + Movement → `CPartArray::HandleMovement` → Position → Particle → Script; +- `SmartBox::UseTime @ 0x00455410` advances object/physics time before the + inbound queue and then calls `CommandInterpreter::UseTime`; +- `gmSmartBoxUI::UseTime @ 0x004D6E30` owns UI/viewport time outside that + object barrier. + +At the retail host level, `Client::UseTime @ 0x00411C40` calls +`UIElementManager::UseTime` before `SmartBox::UseTime`; the UI time broadcast +reaches `gmSmartBoxUI::UseTime`. Retail also publishes player-camera work from +the physics/player callback rather than from one post-network camera tail. +acdream currently advances teleport/UI-camera presentation after its +object/network/command barrier. Slice 6 preserves that existing order as +registered TS-53; it does not cite `gmSmartBoxUI` as proof that the current +post-network placement is retail-exact. + +acdream preserves two documented presentation adaptations: + +1. only semantic `AnimationDone` executes at retail's in-object hook slot; + every other captured animation hook retains the current deferred path after + final root, part, and equipped-child poses are published, including + particles, audio, lights, translucency, `CallPES`, and default-script + chaining (TS-50); +2. shared particle simulation and PhysicsScript queues tick once per host + update after both worksets rather than once per object's admitted quantum. + This also means the current shared Particle → Script order does not reproduce + the static workset's retail Script → Particle → hooks order (TS-51). + +Asynchronous landblock streaming has no one-call retail equivalent. Its +convergence pre-pass remains before inbound CreateObject dispatch so a newly +accepted projection can find a resident bucket in the same frame. This +pre-pass does not advance an object's retail clock. `WorldRevealCoordinator` +remains the single login/portal readiness owner. + +## 3. Fixed production phase order + +The complete host-update graph is frozen as follows: + +1. retry canonical live-entity teardown tombstones; an `AggregateException` is + logged and later phases continue, while any other exception propagates; +2. normalize the host delta, advance the shared PES clock once, and publish it; +3. advance pending origin retirement/recenter, select the observer, tick + streaming, and rebucket rescued persistent entities; +4. tick `InputDispatcher`, consume one raw mouse sample, and advance combat + attack intent; +5. enter the non-reentrant inbound-event envelope and advance the pre-network + live-object phase: + - local object and pre-network movement/jump output; + - queued selection/use output after movement edges; + - ordinary live objects; + - the distinct static-animation workset; + - equipped children and static hook completion; + - fades, every deferred non-`AnimationDone` hook, and current effect/light + anchors; + - particle visibility and particle simulation; + - PhysicsScript queues; +6. drain inbound session traffic inside one `GpuWorldState.MutationBatch`; +7. run the local player's post-network `CommandInterpreter` position check; +8. perform the ordinary post-network non-advancing root/child/emitter/light + reconcile; +9. advance liveness expiry from the absolute client timer; +10. activate/tick local teleport transit. A Place edge commits the player and + live root, immediately performs its own non-advancing reconcile, then + notifies world reveal and continues tunnel state; +11. evaluate one-shot player-mode auto-entry; +12. update fly or chase/player camera presentation. If the local player was + first created by this frame's inbound pass, publish its non-advancing + projection, immediately reconcile a second time, then update the camera. + +Those three reconcile edges—ordinary post-network, teleport Place, and +inbound-created-player projection—are separate accepted production edges. Any +future consolidation is a behavior change with its own oracle, not part of +this extraction. + +An invalid/non-positive host delta becomes zero for the PES/object/particle/ +tunnel delta but never blocks inbound traffic, absolute-time liveness, +recenter convergence, or camera-state publication. Raw mouse idle uses its +independent monotonic input clock. A missing gameplay camera/input owner never +suppresses outer phases; the streaming controller may skip observer-driven +streaming when it has no valid live or offline observer, while origin +retirement still advances. Devtools keyboard capture pauses player/camera +control including autorun; retained chat capture suppresses held WASD while +autorun continues; mouse capture ends mouse-look but does not suppress +keyboard, combat, network, streaming, teleport, or teardown. + +## 4. Architecture and interfaces + +### 4.1 Per-frame input + +Use an immutable value equivalent to: + +```csharp +public readonly record struct UpdateFrameInput(double HostDeltaSeconds); +``` + +Derived facts are read by their owning controller from stable runtime owners +or from a narrowly typed state source. Do not pass `GameWindow`, expose its +private fields wholesale, or create a service locator. Mutable player/session +owners that legitimately change during login are represented by one focused +state/source interface per domain, not anonymous closures into window methods. + +`UpdateFrameClock` is constructed before any materializer/network owner that +needs the current PES time. It owns the normalized host/PES clock and exposes a +narrow `CurrentScriptTime` source; `_physicsScriptGameTime` does not remain a +`GameWindow` field captured by later owners. Absolute liveness time and raw +mouse idle time remain distinct monotonic sources and are not derived from the +normalized simulation delta. + +### 4.2 Object phase + +`LiveObjectFrameController` owns the body currently in +`AdvanceLiveObjectRuntimeCore`. It composes existing focused owners and keeps +their retail order; it does not absorb their algorithms or identity maps. +`LiveSpatialPresentationReconciler` owns the current no-time refresh body. +`RetailLiveFrameCoordinator` depends on those typed owners plus the existing +session and local-player owners. Its public `Tick` remains the one explicit +object → network → command → reconcile barrier. + +`RetailInboundEventDispatcher` continues wrapping the complete object phase. +Reentrant packets queue until the current object's manager tail completes. +The hot path remains allocation-free after warmup. + +The extraction inventory includes the complete transitive update graph, not +only `RetailLiveFrameCoordinator`: `RetailLocalPlayerFrameController` loses +substantial window captures in favor of a focused local-player slot, movement +input source, outbound owner, and projection owner; +`LiveEntityAnimationPresenter` receives a concrete animation-presentation +context instead of retaining `GameWindow` as +`ILiveAnimationPresentationContext`; scheduler clock/player/projectile sources +are similarly narrow. Existing small value/policy callbacks are acceptable +only when they do not call a substantial body back on the window or create a +second identity/state owner. + +### 4.3 Streaming phase + +`StreamingFrameController` owns observer selection and the sealed-dungeon +gate. It receives the existing `StreamingOriginRecenterCoordinator`, +`StreamingController`, `LiveWorldOriginState`/center state, camera/player +state, physics current cell, `GpuWorldState`, and `LiveEntityRuntime` through +typed seams. It never builds, publishes, or retires a landblock; those remain +Slice 5 owners. + +The controller preserves: + +- no pre-login hardcoded-Holtburg streaming; +- real-player-origin readiness before first streaming; +- destination-origin pinning while in portal space; +- authoritative player landblock during the login hold; +- offline fly-camera observation only outside live in-world mode; +- sealed EnvCell collapse using the cell's own landblock identity; +- teleport-hold suppression of the stale source-cell dungeon gate; +- rescued-entity rebucketing after the accepted streaming tick. + +### 4.4 Input and camera phases + +`GameplayInputFrameController` owns per-frame dispatcher hold delivery, +UI-capture transitions, raw mouse sample/filter/idle handling, combat attack +ticking, and the complete mouse-look/cursor-capture lifetime. It is the single +owner of mouse-look state, saved cursor mode, begin/end/hide/restore, and +movement teardown across press/release, capture change, focus loss, teleport, +session reset, player-mode exit, and shutdown. Action press/release callbacks +remain on `InputDispatcher` and movement stays owned by +`PlayerMovementController`. + +`PlayerModeController` owns enter/exit/toggle construction and the current +player-controller, physics-host, chase-camera, and chase-mode slot. Manual +input, `PlayerModeAutoEntry`, and local teleport activation all enter through +that one owner. Streaming, camera, local-frame, and teleport owners receive +narrow read sources from it; no path calls the substantial current +`EnterPlayerModeNow`/exit bodies back on `GameWindow` or duplicates player-mode +state. + +`CameraFrameController` owns held fly-camera controls, retail chase offsets, +post-network player presentation sampling, and both chase-camera updates. It +does not own physics, target selection, or camera formulas. The local player +created by the inbound pass gets a non-advancing projection before its first +draw exactly as today, followed by the current second spatial reconcile before +the camera samples that projection. + +### 4.5 Teleport phase + +`LocalPlayerTeleportController` owns only the current local-player transit +state machine and presentation edges. It composes +`TeleportTransitCoordinator`, +`TeleportViewPlaneController`, `PortalTunnelPresentation`, +`WorldRevealCoordinator`, streaming priority, and the session LoginComplete +send. Checkpoint E first extracts the currently window-owned local aim, +destination, hold/forced, activation, placement, reset, and session-clear +state into this owner plus a concrete local placement collaborator where +needed. It does not reuse `RemoteTeleportPlacement`, and network/session sinks +do not retain `AimTeleportDestination` or reset callbacks into `GameWindow`. +`LiveEntityLivenessController`, tombstone retry, and `PlayerModeAutoEntry` +remain separate focused phases; teleport does not absorb them. + +### 4.6 Composition, thread, and lifetime + +The clock and focused dynamic sources are constructed before their first +consumer. The final orchestrator is composed exactly once at the end of a +successful `OnLoad`; `OnUpdate` fails fast if that invariant is broken rather +than running a nullable partial graph. Every phase mutates state only on the +existing update/render thread. Streaming worker completion remains behind +`StreamingController.Tick`; the extraction adds no lock, task, or async entry. + +Phase controllers borrow their input, session, streaming, portal, camera, and +render-presentation owners; they do not dispose shared dependencies. Any new +event subscription has explicit symmetric detach. During shutdown the update +collaborators remain alive through the first-stage `LiveSessionController` +reset, then retire before their underlying input/camera/portal/streaming +resources. Existing retryable shutdown ordering remains authoritative. + +## 5. Checkpoints and acceptance + +### A — contract and guards + +- record the exact production phase graph and every mutable dependency; +- correct and pin the retail oracle for physics-hook retention/reentrant + append, inactive object behavior, and static Script → Particle → hooks; pin + the existing TS-50/TS-51 divergences without reordering runtime behavior in + this ownership-only slice; +- add an explicit orchestrator order test covering all twelve phases and the + three conditional reconcile edges; +- pin normalized simulation/PES/particle/tunnel time separately from absolute + liveness and raw-mouse idle clocks for NaN, both infinities, non-positive + values, and finite values above `float.MaxValue`; +- pin missing live/offline camera behavior and the separate devtools keyboard, + retained-chat keyboard, and mouse-capture contracts; +- pin teardown retry success, `AggregateException` log-and-continue, other + exception propagation, and next-frame retry; +- add source guards that prevent `OnUpdate` or the coordinator from regaining + substantial callback bodies, and prevent any transitive update owner from + retaining `GameWindow` or one mega service context; +- verify current production traces before cutover. + +### B — object/network barrier + +- move the pre-network animation/effect body into + `LiveObjectFrameController`; +- move no-time root/child/effect refresh into + `LiveSpatialPresentationReconciler`; +- replace coordinator callbacks with typed collaborators; +- preserve semantic/visual hook separation, owner-incarnation revalidation, + static-after-ordinary order, ordinary Particle-before-Script, the current + shared-tail TS-51 adaptation, and once-per-host tails; +- delete `AdvanceLiveObjectRuntimeCore` and + `ReconcileLiveObjectSpatialPresentation` from `GameWindow`. + +Focused tests cover `RetailObjectActivityGate` and +`RetailObjectQuantumClock`: parented/cell-less/Frozen suspension, inactive +Particle/Script-only time, Hidden+Active adjustment/hooks/manager time, and +separate ordinary epsilon/Min/Max/Huge plus static epsilon/Huge boundaries. +They also cover incomplete physics-hook +retention, same-drain animation-hook append, deferred `CallPES`/default-script/ +translucency routing, physics-hook append deferred until the next drain, +completed physics-hook removal preserving the snapshotted successor, static +hooks, callback GUID reuse, and reentrant inbound packets. Particle then Script +must run exactly once for an empty world, +multiple ordinary/static owners, invalid delta, and reentrant inbound work; +the one published PES clock remains visible from hook capture through the +script tick. + +### C — streaming frame + +- extract readiness/recenter advancement, observer selection, dungeon gate, + streaming tick, and rescued projection rebucketing; +- test login-before-player-spawn, outdoor/dungeon/portal-hold/offline observer + cases, missing live/offline camera sources, and cell-landblock identity; +- prove same-frame streaming completion precedes CreateObject projection into + the resident bucket, unknown pre-login origin enqueues nothing, and recenter + retirement continues while ordinary streaming is gated; +- verify no build/publication/retirement ownership leaks back from Slice 5. + +### D — input frame + +- extract dispatcher tick, UI mouse-capture edge, raw sample/filter/idle path, + combat intent tick, and the one cross-lifetime mouse-look/cursor owner; +- test capture entry while mouse-look is active, zero-idle drift stop, + production action ordering, devtools-vs-chat autorun semantics, mouse-only + capture, and focus/teleport teardown; +- prove every non-input phase still runs under each capture mode; +- retain diagnostic semantic input as the unattended connected gate seam. + +### E — local teleport and post-network world frame + +- extract the currently window-owned local-player teleport state and every + aim/destination/activation/place/reset/session port before moving its tick; +- retain liveness and auto-entry as separate orchestrator calls around pending + transit activation, readiness/hold, tunnel events, materialization, and + reveal completion; +- test loaded and pending destinations, forced-readiness boundary, same-cell + respawn, LoginComplete ordering, delete/reset, and no second reveal owner; +- pin Place → immediate reconcile → reveal and old-GUID liveness candidate → + inbound delete/recreate generation safety; +- preserve the completed portal visuals and world-reveal barrier unchanged. + +### F/G — camera, cutover, and deletion + +- extract player-mode enter/exit/toggle and current-owner state before camera + or teleport cutover; retain `PlayerModeAutoEntry` only as its one-shot + readiness guard; +- extract fly camera, retail chase adjustment, local post-network projection, + combat target tracking, and chase-camera publication; +- pin inbound-created projection → immediate second reconcile → camera, with + exact root/child/emitter/light refresh counts and no duplicate time advance; +- compose `UpdateFrameOrchestrator` only after all dependencies exist; +- replace `OnUpdate` with profiler scope plus one `Tick` call; +- remove obsolete frame fields/facades and prove there is no back-reference to + `GameWindow` from an extracted owner; +- measure line/field/method count. The target is below 8,000 lines, but the + ownership/ordering exit criteria outrank the count. + +### H — release and connected gates + +After all three independent corrected-diff reviews are clean: + +1. run every focused App/Core/Net/UI test project; +2. run `dotnet build AcDream.slnx -c Release`; +3. run the full Release suite; +4. run the connected lifecycle/reconnect gate; +5. run the synchronized nine-stop resource soak with movement, turn, jump, + combat, portal materialization waits, fatal-log scan, and graceful close; +6. compare lifecycle, outbound movement, materialization, entity/animation, + frame-profile, resource, and teardown traces with the Slice 5 baseline; +7. audit the divergence register, architecture, milestones, roadmap, issues, + `AGENTS.md`, `CLAUDE.md`, and durable memory. + +The slice requires no new visual behavior. If connected screenshots or traces +change, return to the responsible checkpoint; do not defer the difference to +Slice 7. + +## 6. Review gate for every checkpoint + +After each implementation checkpoint: + +1. run focused tests and inspect the complete diff; +2. retail-conformance review checks named functions, exact order, Hidden/ + quantum semantics, and adaptation boundaries; +3. architecture review checks dependency direction, absence of a replacement + god object/service locator, same-thread mutation, and symmetric teardown; +4. adversarial review checks independent clocks, callback reentrancy, + owner/GUID reuse, login/portal transitions, capture modes, missing observer + sources, teardown failures, and early-return phase suppression; +5. fix every confirmed root cause and repeat review until clean; +6. build/test Release and commit the checkpoint before starting the next one. + +## 7. Divergence and documentation bookkeeping + +TS-50, TS-51, and the newly documented existing host-order TS-53 remain until +their exact adaptations are ported; this slice must not overclaim retail +parity. IA-14/AD-22 and the lifetime/resource rows are audited against the +final owner graph but changed only if their mechanism actually changes. Any +newly discovered unavoidable adaptation gets a register row in the same +commit. The Slice 6 closeout updates the code-structure table, +roadmap, milestones, session docs, and +`project_gamewindow_decomposition.md` with exact commits, test counts, gates, +and final metrics. diff --git a/docs/research/2026-07-19-r6-update-object-order-pseudocode.md b/docs/research/2026-07-19-r6-update-object-order-pseudocode.md index c4e87243..74a5ab55 100644 --- a/docs/research/2026-07-19-r6-update-object-order-pseudocode.md +++ b/docs/research/2026-07-19-r6-update-object-order-pseudocode.md @@ -8,18 +8,23 @@ Retail oracle: Asheron's Call v11.4186 (September 2013 EoR) This note pins the control-flow order needed by R6. It deliberately separates two kinds of animation-hook work: -- **control hooks** such as `AnimDoneHook`, whose result changes the motion +- the immediate **`AnimDoneHook` control path**, whose result changes the motion queues observed later in the same object update; and -- **pose-dependent presentation hooks** such as particles and sounds, whose - App-layer delivery may wait until the final root/part pose has been - published without changing motion state. +- **all other captured animation hooks**, which acdream currently defers until + the final root/part pose has been published. This includes presentation + hooks such as particles and sounds, but also semantically relevant + `CallPES`, default-script chaining, and translucency hooks. -That split is an acdream presentation boundary. It does not change retail's -logical hook order. +That split is an acdream presentation boundary. Authored FIFO order is +preserved within acdream's deferred non-`AnimationDone` stream, but moving that +stream past retail's manager tail changes manager-relative timing; that is the +registered TS-50 divergence. ## Named-retail sources - `CPhysicsObj::process_hooks` `0x00511550` +- `CPhysics::UseTime` `0x00509950` +- `CPhysicsObj::animate_static_object` `0x00513DF0` - `CPhysicsObj::UpdatePositionInternal` `0x00512C30` - `CPhysicsObj::UpdateObjectInternal` `0x005156B0` - `CPhysicsObj::update_object` `0x00515D10` @@ -58,6 +63,17 @@ CPhysicsObj::update_object -> PositionManager::UseTime -> ParticleManager::UpdateParticles -> ScriptManager::UpdateScripts + +CPhysics::UseTime + -> ordinary object table in the order above + -> static_animating_objects + -> CPhysicsObj::animate_static_object + -> CPartArray::Update + -> UpdatePartsInternal + -> UpdateChildrenInternal + -> ScriptManager::UpdateScripts + -> ParticleManager::UpdateParticles + -> CPhysicsObj::process_hooks ``` ## Pseudocode @@ -66,11 +82,16 @@ CPhysicsObj::update_object ```text if parent != null or cell == null or state contains Frozen: + clear Active transient state return refresh active/view-distance state elapsed = current_physics_time - last_update_time +if elapsed <= FrameEpsilon (0.000199999995 seconds): + last_update_time = current_physics_time + return + if elapsed > HugeQuantum: last_update_time = current_physics_time return @@ -85,8 +106,19 @@ if elapsed > MinQuantum: UpdateObjectInternal(elapsed) ``` -The comparisons are strict (`>`), not `>=`. A sub-minimum remainder stays in -the clock for a later call. +The comparisons are strict (`>`), not `>=`. Elapsed time at or below the +roughly 0.2-millisecond frame epsilon is explicitly rebased/discarded. Above +that epsilon, a remainder at or below `MinQuantum` stays in the clock for a +later call; `MaxQuantum` controls subdivision and `HugeQuantum` controls stale +discard. These are separate gates. + +`UpdateObjectInternal` then branches on the transient Active bit. An inactive +object skips position integration, hook processing, Detection, Target, +Movement, PartArray, and PositionManager, but still advances its +ParticleManager and ScriptManager tails. A Hidden object that remains Active +is different: it enters `UpdatePositionInternal`, suppresses only PartArray +advance and physics integration, and retains PositionManager adjustment, +hook processing, and the complete manager tail. ### `CPhysicsObj::UpdatePositionInternal(dt, candidate)` @@ -120,13 +152,25 @@ manager tail. ### `CPhysicsObj::process_hooks` ```text -execute each queued physics hook in FIFO order -clear physics-hook queue +for each physics hook present in the linked list walk: + completed = hook.Execute(owner) + if completed: + unlink and destroy that hook + else: + retain it for a later object update -execute each queued animation hook in FIFO order -clear animation-hook queue +for i from zero while i < current animation-hook count: + animation_hooks[i].Execute(owner) + // the bound is live: a hook appended during execution can run in this drain +shrink animation-hook storage and set the count to zero ``` +The physics-hook walk snapshots each current node's `next` pointer before +execution. A completed hook is removed; an incomplete hook remains linked. +The animation-hook loop deliberately rereads `m_num` after each callback, so +reentrant append is consumed in the same drain. Neither collection is an +unconditional snapshot-and-clear queue. + For an `AnimDoneHook`: ```text @@ -175,8 +219,52 @@ if script_manager != null: ``` The tail is reached for both visible and Hidden objects when the object quantum -runs. A missing manager is simply skipped. No manager is reordered around -another manager based on local/remote/player/NPC classification. +runs and the object is Active. An inactive object skips this position/manager +body but still reaches Particle then Script. A missing manager is simply +skipped. No manager is reordered around another manager based on +local/remote/player/NPC classification. + +### `CPhysicsObj::animate_static_object` + +```text +if cell == null: + return + +elapsed = current_time - update_time +if elapsed <= FrameEpsilon (0.000199999995 seconds): + update_time = current_time + return +if elapsed > HugeQuantum: + update_time = current_time + return + +if part_array != null: + if state contains HasDefaultAnim: + part_array.Update(elapsed) + rotate root by the current angular vector + UpdatePartsInternal() + UpdateChildrenInternal() + + if state contains HasDefaultScript and script_manager != null: + script_manager.UpdateScripts() + + if particle_manager != null: + particle_manager.UpdateParticles() + + process_hooks() + +update_time = current_time +``` + +The static workset has no ordinary-object `MinQuantum` remainder gate or +`MaxQuantum` subdivision loop: it uses only the frame epsilon and +`HugeQuantum`, then passes the complete admitted elapsed time to PartArray. +It therefore has a distinct Script → Particle → hooks tail, not ordinary +objects' Particle → Script tail. acdream currently shares one +ParticleSystem and one PhysicsScriptRunner across every owner and advances +them once after the complete ordinary/static worksets. That is the registered +TS-51 adaptation and must not be silently reordered by an ownership-only +extraction. ## acdream conformance boundary @@ -186,9 +274,11 @@ unsupported `DetectionManager` is represented by an absent callback rather than an invented behavior. Animation-hook capture performs `AnimationDone` control effects immediately, -at the `process_hooks` boundary. Pose-dependent routing remains deferred until -the current frame's root/part poses are published. Deferred routing must never -invoke `AnimationDone` a second time and must never own +at the `process_hooks` boundary. Every other captured animation hook currently +remains deferred until the final root/part poses are published, including +particle/audio/light/translucency hooks, `CallPES`, and default-script +chaining. Deferred routing must never invoke `AnimationDone` a second time and +must never own `MotionTableManager.UseTime`; that belongs only to the PartArray manager-tail slot. @@ -199,17 +289,32 @@ slot. 2. Target runs before Movement; Movement runs before PartArray; PartArray runs before PositionManager. 3. A missing manager does not shift or duplicate another callback. -4. Hidden objects skip sequence advance but retain the same manager tail. -5. A visual hook sees a pose published after capture, while an `AnimDoneHook` - changes motion state at capture time. -6. Deferred visual-hook drain never completes an animation twice and never - performs a zero-tick motion sweep. -7. A sub-MinQuantum frame performs no object-tail call; accumulated time is - consumed by a later quantum. +4. Parented, cell-less, and Frozen objects return before clock admission. +5. Inactive objects skip position/hooks/manager time but retain Particle then + Script; Hidden + Active objects skip sequence/physics but retain adjustment, + hooks, and the manager tail. +6. A non-`AnimationDone` hook sees a pose published after capture, while an + `AnimDoneHook` changes motion state at capture time. +7. Deferred hook drain never completes an animation twice and never performs + a zero-tick motion sweep; representative `CallPES`, default-script, and + translucency hooks retain their current deferred path. +8. An incomplete physics hook remains linked, and an animation hook appended + by another animation hook executes in the same retail drain. +9. Static objects retain their distinct Script → Particle → hooks order. +10. Ordinary epsilon/Min/Max/Huge boundaries and static epsilon/Huge + boundaries preserve their separate strict retail comparisons. ## Cross-reference notes -ACE's physics/motion ports remain interpretation aids for manager ownership -and state naming; the retail binary above is authoritative for order. The -existing `RemoteChaseEndToEndHarnessTests` hand-codes acdream's pre-R6 order -and is therefore a migration target, not an oracle. +ACE's `PhysicsObj.cs` corroborates ordinary Particle → Script and static +Script → Particle → hooks order. It does **not** corroborate the retail clock +or hook-lifetime gates: ACE uses its server `TickRate`/Min/Max policy and +clears physics hooks unconditionally, unlike retail's epsilon/Huge behavior +and completion-based hook retention. ACViewer exposes the same client-shaped +methods and manager order but shares ancestry with ACE, so that agreement is +corroboration rather than a second oracle. holtburger's native protocol crates +have no client physics-loop equivalent (its vendored ACViewer copy is not +independent). The retail binary above is authoritative for every boundary. +The existing +`RemoteChaseEndToEndHarnessTests` hand-codes acdream's pre-R6 order and is +therefore a migration target, not an oracle.