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