From 9760ff5a8d597eadad52df77211816f0b3762b3f Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 21 Jul 2026 09:30:04 +0200 Subject: [PATCH] docs(animation): plan GameWindow live presentation slice Define the exact extraction boundary, incarnation-safe handoff, retail trailing-part correction, and review gates before moving the final PartArray presentation path out of GameWindow. Co-Authored-By: OpenAI Codex --- ...dow-slice-2-live-animation-presentation.md | 381 ++++++++++++++++++ 1 file changed, 381 insertions(+) create mode 100644 docs/plans/2026-07-21-gamewindow-slice-2-live-animation-presentation.md diff --git a/docs/plans/2026-07-21-gamewindow-slice-2-live-animation-presentation.md b/docs/plans/2026-07-21-gamewindow-slice-2-live-animation-presentation.md new file mode 100644 index 00000000..f7cbac56 --- /dev/null +++ b/docs/plans/2026-07-21-gamewindow-slice-2-live-animation-presentation.md @@ -0,0 +1,381 @@ +# GameWindow Slice 2 — live animation presentation ownership + +**Status:** Approved for execution 2026-07-21. +**Parent program:** [`docs/architecture/code-structure.md`](../architecture/code-structure.md), Slice 2. +**Baseline:** `9ad8113c`; `GameWindow.cs` is 14,912 lines, 278 fields, and +191 methods before this slice. +**Behavior rule:** This is an ownership extraction over the accepted R6 object +frame plus one named-retail conformance correction: short AnimFrames retain +the prior/rest pose of trailing CPartArray parts. It must not otherwise change +sequence time, root motion, collision, manager order, hook semantics, part +transforms, render eligibility, or effect attachment. + +## 1. Outcome + +`GameWindow` stops owning final animated-part presentation. A focused +`LiveEntityAnimationPresenter` consumes the already-completed scheduler output +and publishes the exact same draw and effect state: + +- final drawable `MeshRef` transforms; +- indexed rigid part poses and availability; +- `EntityEffectPoseRegistry` publication; +- live static-animation frame handoff; +- legacy animation interpolation state; +- one-time `MotionDone` binding; +- presentation-specific sequence/part diagnostics. + +The shipped owners remain unchanged: + +- `LiveEntityAnimationScheduler` owns ordinary-object clocks, PartArray + advance, root motion, one movement owner, control-hook capture, and the + retail manager tail; +- `RetailStaticAnimatingObjectScheduler` owns the separate retail + `static_animating_objects` time/workset and its deferred process-hooks tail; +- `EquippedChildRenderController` owns equipped-child composition; +- `AnimationHookFrameQueue` owns incarnation-scoped semantic/presentation-hook + delivery; +- `LiveEntityRuntime` remains the only identity, incarnation, component, and + spatial-lifetime owner. + +`GameWindow.AdvanceLiveObjectRuntimeCore` retains only the explicit frame-order +calls between these owners. + +## 2. Retail and project oracles + +No new AC algorithm is introduced. The extraction is pinned to: + +- [`2026-07-19-r6-update-object-order-pseudocode.md`](../research/2026-07-19-r6-update-object-order-pseudocode.md) + — `UpdatePositionInternal`, `process_hooks`, and manager-tail ordering. +- [`2026-07-19-r6-complete-root-frame-pseudocode.md`](../research/2026-07-19-r6-complete-root-frame-pseudocode.md) + — complete root `Frame`, ordinary/static worksets, part/effect publication. +- [`memory/project_animation_runtime.md`](../../memory/project_animation_runtime.md) + — appearance-update and description-before-enter-world lifetime invariants. +- `CPhysicsObj::UpdatePositionInternal @ 0x00512C30`. +- `CPhysicsObj::UpdateObjectInternal @ 0x005156B0`. +- `CPhysicsObj::process_hooks @ 0x00511550`. +- `CPhysicsObj::animate_static_object @ 0x00513DF0`. +- `CPartArray::Update @ 0x00517DB0`. +- `CPartArray::UpdateParts @ 0x005190F0`. +- `CPartArray::AnimationDone @ 0x00517D30`. +- `CPhysicsObj::MotionDone @ 0x0050FDB0`. +- `Frame::combine @ 0x005122E0`. + +ACE/DatReaderWriter and the installed retail DAT remain secondary fixtures for +motion-table/animation schema interpretation. Named retail is authoritative +for ordering and transform semantics. + +## 3. Fixed frame-order contract + +One ordinary live-object frame remains: + +```text +local player pre-network object phase +selection outbound boundary +publish PhysicsScript clock +LiveEntityAnimationScheduler.Tick +RetailStaticAnimatingObjectScheduler.Tick +LiveEntityAnimationPresenter.Present +EquippedChildRenderController.Tick +RetailStaticAnimatingObjectScheduler.ProcessHooks +translucency advance +AnimationHookFrameQueue.Drain +effect-root refresh +attached-emitter refresh +live-light refresh +particle visibility +particle simulation +PhysicsScript simulation +``` + +The presenter advances no clock and drains no hook. It only consumes poses +produced earlier in the same frame. Static `process_hooks` remains after live +parts and equipped children, as retail's `animate_static_object` requires. + +## 4. Ownership and interfaces + +### 4.1 Animation component state + +Move `GameWindow.AnimatedEntity` and `GameWindow.AnimatedPartTemplate` to +top-level App rendering types: + +```text +LiveEntityAnimationState +LiveAnimationPartTemplate +``` + +`LiveEntityAnimationState` remains the exact component stored in +`LiveEntityRecord.AnimationRuntime`. It owns no GUID map and no lifetime. It +retains: + +- the exact `WorldEntity`, Setup, Animation, and optional sequencer; +- legacy low/high/current frame and framerate; +- ObjScale, part template, and part availability; +- prepared local-player pose and complete root-frame scratch; +- reusable `MeshRefs`, effect-part-pose, and diagnostics scratch. + +Appearance updates mutate this same component and preserve sequencer/playback +state. Logical replacement creates a new component. + +### 4.2 Presentation context + +Define a narrow App interface equivalent to: + +```csharp +interface ILiveAnimationPresentationContext +{ + uint LocalPlayerGuid { get; } + MotionInterpreter? ResolveMotionInterpreter(LiveEntityRecord record); +} +``` + +`GameWindow` implements only these constant-time lookups. The presenter cannot +reach windowing, GL, networking, input, streaming, physics, or arbitrary +`GameWindow` methods. + +### 4.3 Presenter + +Use an API equivalent to: + +```csharp +void Present( + IReadOnlyDictionary schedules); + +void PrepareAnimation( + LiveEntityRecord record, + LiveEntityAnimationState state); +``` + +The presenter receives: + +- canonical `LiveEntityRuntime` provider for exact-record validation; +- `EntityEffectPoseRegistry`; +- `ILiveAnimationPresentationContext`; +- a late-bound `ILiveStaticPartFrameSource` (the static scheduler is created + after the presenter); +- startup-resolved hidden-part diagnostic index; +- typed animation diagnostic flags. + +The presenter owns its own reusable `LiveEntityRecord` snapshot. It does not +borrow `LiveEntityAnimationRuntimeView`'s reusable enumerator snapshot across +the synchronous `EffectPoseChanged` callback boundary. Nested enumeration by a +subscriber therefore cannot invalidate the outer presentation pass. + +Each schedule carries the exact `LiveEntityRecord`, `WorldEntity`, animation +component, `ObjectClockEpoch`, and projection-mutation version which produced +it. The presenter validates all of those identities before consuming ordinary +or static frames and again before each externally observable publication. A +dictionary key/local ID is an index only; it is never proof of incarnation. +The static prepared-frame source is queried late, only after this validation. +Its take operation receives the exact record, entity, animation component, +object-clock epoch, and projection/residency versions; it validates those +tokens before clearing and returning prepared frames. + +It keeps no GUID map, no animation clock, no packet/session reference, and no +render-backend object. + +### 4.4 Diagnostics + +Move the surviving `SEQSTATE`, `CURRNODE`, `PARTSDIAG`, and `MOTIONDONE` gates +behind one typed animation diagnostic owner. Environment variables are read +once; the presenter does not perform per-frame environment lookups. Throttle +state follows the exact animation component lifetime instead of living on the +remote-motion component. + +## 5. Transform invariants + +For every part index, preserve these two deliberately different transforms. + +### Drawable visual transform + +```text +Setup DefaultScale +× animated part orientation +× animated part translation +× entity ObjScale +``` + +This is the existing renderer/hydration contract. It scales geometry and the +part's translated placement. + +### Rigid effect/attachment transform + +```text +animated part orientation +× translation(animated origin × entity ObjScale) +``` + +Retail `CPartArray::UpdateParts` scales the part-frame origin only. Setup +DefaultScale is `gfxobj_scale`, not rigid part-frame state, so particles, +lights, attachments, and multipart collision must never inherit it. + +Additional invariants: + +- Sequencer frames win when present. Retail updates only + `min(CPartArray.num_parts, AnimFrame.num_parts)`; missing trailing frames + retain both their previous drawable visual transform and their previous + indexed rigid/effect pose (the two distinct initial rest transforms on first + presentation) instead of snapping either channel to identity. +- Legacy animation interpolates origin linearly and orientation spherically, + clamps interpolation to `[0,1]`, and wraps over the inclusive frame span. +- Live static prepared frames override the ordinary schedule for that owner. +- `ComposeParts == false` changes no mesh or indexed pose. +- Non-drawable/debug-hidden parts remain in indexed effect poses but not + drawable `MeshRef`s. +- `PartAvailability` is published unchanged and controls effect part lookup. +- Mesh identity, surface overrides, and list reuse remain unchanged. +- Entity mesh/indexed-part state is installed before effect-pose publication. + +## 6. MotionDone invariants + +- Bind before any sequencer can complete; repeated calls are idempotent. +- The scheduler validates the exact owner before invoking `PrepareAnimation`. +- Resolve the player/remote `MotionInterpreter` at callback time because that + consumer may be created after the animation component. +- Never capture a legitimate null interpreter permanently. +- Never complete a motion from pose publication; `AnimationHookFrameQueue` + remains the semantic `AnimationDone` boundary. +- Logical removal/replacement stops the old component from advancing; no + presenter-owned callback queue survives it. +- The callback revalidates the stable captured record, entity, and animation + component before resolving a same-GUID interpreter. It deliberately does + not capture object-clock/projection epochs: the same retail CPartArray and + completion consumer survive legitimate rebucketing and leave/re-enter. + GUID reuse still cannot deliver an old completion to a replacement. +- `MOTIONDONE` diagnostics observe but never alter queue state. + +## 7. Execution sequence + +### Commit 0 — plan and baseline + +- Land this plan. +- Record exact baseline metrics and current frame order. +- Register no new divergence; this slice moves an accepted mechanism. + +### Commit 1 — extract component types and pure presenter + +- Move animation component/template types out of `GameWindow`. +- Add the presentation context and typed diagnostics owner. +- Implement `LiveEntityAnimationPresenter` by moving the current composition + body line-for-line, retaining the baseline trailing-part fallback initially. +- Add focused tests before runtime cutover. + +### Commit 2 — cut over runtime ownership + +- Construct the presenter beside the scheduler. +- Route scheduler preparation, local-player pre-advance, spawn-time binding, + and final presentation through it. +- Delete `TickAnimations`, `EnsureMotionDoneBinding`, nested animation types, + and presentation-only remote diagnostic fields from `GameWindow`. +- Keep child/static-hook/effect ordering visible in + `AdvanceLiveObjectRuntimeCore`. + +### Commit 3 — retail trailing-part conformance + +- Record the `CPartArray::UpdateParts` minimum-count/retain behavior in the + R6 pseudocode note. +- Preserve both visual and rigid prior/rest trailing transforms. +- Add first-presentation and later-shorter-frame conformance tests plus + ordinary/live-static/DAT-static parity coverage. +- Update the divergence register in this commit if the audited register + contains a corresponding row; introduce no silent behavior delta. + +### Commit 4 — review, gate, and ledger close + +- Run retail-conformance, architecture/integration, and adversarial read-only + reviews against the complete slice. +- Fix every confirmed finding and repeat review until clean. +- Run focused animation, scheduler, static, effect, projectile, remote-motion, + and local-player tests. +- Run Release solution build and full Release suite. +- Record new `GameWindow` line/field/method counts. +- Update code structure, roadmap, milestones, issues if any, durable animation + memory, `AGENTS.md`, and `CLAUDE.md`. + +## 8. Automated acceptance matrix + +### Composition + +- Sequencer pose composes every part and preserves surface overrides. +- Missing sequencer trailing parts retain their previous/rest indexed pose. +- Legacy current/next frame interpolation and inclusive wrapping are exact. +- Legacy hidden/single-frame owners compose only when scheduled. +- Static prepared frames override ordinary frames exactly once. +- `ComposeParts == false` preserves prior mesh/pose state. + +### Visual versus rigid transforms + +- Setup nonuniform DefaultScale affects drawable geometry only. +- ObjScale affects drawable geometry and translated placement. +- Rigid effect poses scale only the translated origin. +- Nondrawable and debug-hidden parts remain available to effects. +- Unavailable parts remain indexed but fail effect-part lookup. + +### Lifetime and ordering + +- A removed/replaced animation component is skipped by the runtime-view + snapshot and cannot publish into its replacement. +- A schedule prepared for incarnation A is rejected if A is withdrawn, + replaced, or reprojected before presentation, including same-local-ID ABA. +- Reentrant deletion/replacement during pose publication prevents every later + write for the stale owner; no borrowed scratch buffer crosses a callback. +- Effect-pose publication sees the entity's already-installed indexed parts. +- Same-frame animation hooks drain only after current parts and children. +- Static process-hooks runs only after static parts and children are current. +- Presentation allocates no per-frame mesh/effect lists in steady state. + +### Motion completion + +- Binding is idempotent. +- A consumer created after binding receives completion. +- Player and remote resolution select the correct interpreter. +- Missing consumer is a no-op. +- Diagnostics do not duplicate `MotionDone`. +- A legitimate rebucket/object-clock rebase preserves the binding. +- Delete plus GUID/local-ID reuse rejects completion from the old component. + +### Cross-owner order and reconciliation + +- One instrumented object-frame test pins presenter, children, static hooks, + fades, ordinary hook drain, effect roots, emitters, lights, particles, and + scripts. A PES-created particle first simulates on the following frame. +- Authoritative spatial reconciliation performs only root/effect refresh, + child composition, emitter refresh, and light refresh; it advances no + animation, hook, fade, particle, script, or object clock. +- Appearance shrink from three parts to one preserves playback while removing + stale MeshRefs, indexed poses, availability, and effect-registry entries. +- Ordinary, live-static handoff, and DAT-static composition share conformance + fixtures for scale, sparse availability, and surface overrides. +- An equipped child consumes the just-published parent indexed pose in the + same frame, including ObjScale and nested attachment composition. +- An animated projectile's trail/light anchor observes the same-quantum root + and part pose, never the previous frame. + +`EquippedChildRenderController` callback-mutation hardening is an adjacent +Slice-6 frame-orchestration concern. Slice 2 does not claim it: the known +reentrancy case is recorded as a follow-up unless a focused test demonstrates +the existing traversal is already safe. +- A nested animation-runtime enumeration triggered by `EffectPoseChanged` + cannot overwrite or truncate the presenter's private outer snapshot. + +## 9. Connected gate + +Run one normal capped Release client and verify behavior remains unchanged: + +1. local idle, walk, run, strafe, turn, jump, combat, and stop transitions; +2. observe a remote player walk/run/turn without pose stalls or blips; +3. open/close a door and kill a creature; static/reactive final poses persist; +4. equip/unequip armor and switch melee/bow/wand without animation loss; +5. fire an arrow and cast a projectile spell; trails remain attached to the + current missile/hand pose; +6. cast a protection and recall/portal; body effects follow current parts; +7. portal/relog and confirm no old pose or completion reaches a replacement. + +Any failure returns to the owning commit. No fallback pose cache, delayed +retry, grace period, or GUID-only suppression is acceptable. + +## 10. Subagent policy + +One primary agent performs all edits. Read-only agents independently audit +retail conformance, architecture/lifetime, and adversarial tests before the +plan and after every implementation correction. No subagent edits the shared +slice concurrently.