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 <codex@openai.com>
This commit is contained in:
Erik 2026-07-21 09:30:04 +02:00
parent 9ad8113c79
commit 9760ff5a8d

View file

@ -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<uint, LiveEntityAnimationSchedule> 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.