acdream/docs/plans/2026-07-21-gamewindow-slice-2-live-animation-presentation.md
Erik 9a150e2421 docs(architecture): close live animation slice
Record the shipped presentation owner, exact frame-handoff lifetime, retail short-frame behavior, current GameWindow metrics, and Slice 3 as the next structural boundary.
2026-07-21 10:03:20 +02:00

17 KiB
Raw Blame History

GameWindow Slice 2 — live animation presentation ownership

Status: Complete 2026-07-21. Automated and three-agent review gates pass; connected visual regression gate is carried to the final campaign gate. Parent program: docs/architecture/code-structure.md, Slice 2. Baseline: 9ad8113c; GameWindow.cs is 14,912 lines, 278 fields, and 191 methods before this slice. Result: GameWindow.cs is 14,546 lines, 277 fields, and 190 methods. The slice also closed stale-schedule ABA across replacement, appearance rebinding, static rebind, and borrowed sequencer buffers. Forty-one focused Release tests and the full 6,575-pass / 5-skip Release suite are green. 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.mdUpdatePositionInternal, process_hooks, and manager-tail ordering.
  • 2026-07-19-r6-complete-root-frame-pseudocode.md — complete root Frame, ordinary/static worksets, part/effect publication.
  • 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:

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:

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:

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:

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

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

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 MeshRefs.
  • 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

The presenter-level portions below are covered by Slice 2. The full instrumented owner-order trace and non-advancing spatial-reconcile assertion remain assigned to Slice 6, where the update sequence becomes one testable orchestrator instead of a GameWindow method. Slice 2 preserves that order but does not claim an end-to-end orchestrator test prematurely.

  • 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.