Record the shipped presentation owner, exact frame-handoff lifetime, retail short-frame behavior, current GameWindow metrics, and Slice 3 as the next structural boundary.
87 lines
4.3 KiB
Markdown
87 lines
4.3 KiB
Markdown
# Animation Runtime Crib
|
|
|
|
## Current live presentation ownership (2026-07-21)
|
|
|
|
`LiveEntityAnimationScheduler` owns ordinary-object time, PartArray advance,
|
|
root motion, and the retail manager tail. `RetailStaticAnimatingObjectScheduler`
|
|
owns the separate retail static-object workset. `LiveEntityAnimationPresenter`
|
|
is the sole consumer that composes final drawable part transforms, publishes
|
|
rigid part/effect poses, and binds `MotionDone`. `GameWindow` wires these owners
|
|
but no longer contains the presentation body.
|
|
|
|
The frame handoff is identity- and lifetime-bound. A schedule carries the exact
|
|
`LiveEntityRecord`, `WorldEntity`, animation state, sequencer, object-clock
|
|
epoch, projection mutation version, and presentation revision. Both ordinary
|
|
and static schedulers copy the sequencer's borrowed PartArray view into their
|
|
own buffers before invoking callbacks. Presentation is non-reentrant because
|
|
it consumes legacy elapsed time and static prepared frames exactly once.
|
|
|
|
Appearance rebinding increments `PresentationRevision`. That rejects a pose
|
|
prepared against the old part template without replacing the live animation
|
|
component. For a static owner, rejecting only that stale visual pose must retain
|
|
the same sequencer's pending `process_hooks` tail so `MotionDone` is not lost;
|
|
owner, sequencer, residency, projection, or clock mismatches still invalidate
|
|
the entire handoff.
|
|
|
|
Retail `CPartArray::UpdateParts` updates only the authored AnimFrame prefix.
|
|
When a frame contains fewer parts than the Setup, trailing parts keep their
|
|
previous pose (or their rest pose on first presentation) in both channels:
|
|
|
|
- visual: `DefaultScale * rotation * translation(origin) * ObjScale`;
|
|
- rigid/effect: `rotation * translation(origin * ObjScale)`.
|
|
|
|
Canonical per-frame order remains scheduler tick, static tick, live presenter,
|
|
equipped-child update, static `process_hooks`, deferred hook drain, attached
|
|
effects/lights, particles, and draw. The end-to-end order assertion belongs to
|
|
Slice 6's extracted update orchestrator; do not reintroduce local ordering into
|
|
the presenter.
|
|
|
|
## Live entity lifecycle invariant
|
|
|
|
`CreateObject` creates a live entity and a real `DeleteObject` destroys it.
|
|
`ObjDescEvent (0xF625)` is neither: it changes appearance on the existing object.
|
|
|
|
For an appearance update, preserve:
|
|
|
|
- `WorldEntity` object identity and local `Id`
|
|
- `AnimatedEntity`, `AnimationSequencer`, frame/range/rate state
|
|
- movement-controller and animation-manager bindings
|
|
- physics host, collision registration, dead reckoning, and selection state
|
|
|
|
Replace only resolved `MeshRefs`, palette override, part overrides, and visual
|
|
bounds, then invalidate the entity-classification cache. The paperdoll may clone
|
|
the updated appearance separately.
|
|
|
|
Retail oracle chain:
|
|
|
|
`SmartBox::HandleObjDescEvent` → `SmartBox::UpdateVisualDesc` →
|
|
`ACCObjectMaint::SetVisualDesc` →
|
|
`CPhysicsObj::DoObjDescChangesFromDefault` →
|
|
`CPartArray::DoObjDescChanges`.
|
|
|
|
Regression history: treating `0xF625` as despawn/respawn severed the player
|
|
controller from the active animation state, so unequipping armor froze character
|
|
animation and made animation-dependent door interaction appear frozen. Fixed as
|
|
issue #203 on 2026-07-11 and user-gated in a Release client.
|
|
|
|
## CreateObject enter-world invariant
|
|
|
|
The wire motion is applied while the new physics object is still detached, and
|
|
only then is the object placed in the world:
|
|
|
|
`ACCObjectMaint::CreateObject` → `CPhysicsObj::set_description` (`0x00514F40`)
|
|
→ apply MovementData → `CPhysicsObj::enter_world` (`0x00516170`) →
|
|
`CPartArray::HandleEnterWorld` (`0x00517D70`) →
|
|
`MotionTableManager::HandleEnterWorld` (`0x0051BDD0`).
|
|
|
|
The final call strips every description-time transition link and drains the
|
|
manager queue. This is not optional cleanup. A corpse CreateObject carries
|
|
`NonCombat + Dead`, while a door carries `NonCombat + On/Off`; both come from
|
|
the same wire MovementData path.
|
|
|
|
Issue #204 exposed a structural trap: the persistent Dead rest pose can be
|
|
static, so corpses use the same static/reactive registration branch as doors.
|
|
That branch had been data-driven at its gate but door-hardcoded in its
|
|
initializer, which discarded Dead and left Ready playing. Every spawn branch
|
|
must call the shared wire-driven description-then-enter-world initializer,
|
|
keyed by data and never by object name/type. 2026-07-12.
|