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

2.3 KiB

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::HandleObjDescEventSmartBox::UpdateVisualDescACCObjectMaint::SetVisualDescCPhysicsObj::DoObjDescChangesFromDefaultCPartArray::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::CreateObjectCPhysicsObj::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.