Record the shipped presentation owner, exact frame-handoff lifetime, retail short-frame behavior, current GameWindow metrics, and Slice 3 as the next structural boundary.
4.3 KiB
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:
WorldEntityobject identity and localIdAnimatedEntity,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.