acdream/memory/project_animation_runtime.md
Erik 8be933fc94 fix(animation): preserve wire motion on every spawn path
Replace the door-specific static-animation seed with one retail description-then-enter-world initializer shared by normal and reactive spawns. This preserves authoritative Dead and On/Off states, prevents replacement corpses from returning to Ready, and pins the lifecycle with Core and App tests.

Co-Authored-By: Codex <codex@openai.com>
2026-07-12 14:27:44 +02:00

51 lines
2.3 KiB
Markdown

# Animation Runtime Crib
## 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.