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:
parent
9ad8113c79
commit
9760ff5a8d
1 changed files with 381 additions and 0 deletions
|
|
@ -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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue