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>
5.6 KiB
Persistent death and post-death Auto Target pseudocode
Sources: Sept 2013 EoR named retail client.
ClientCombatSystem::AutoTarget(0x0056BC80)ClientCombatSystem::RecvNotice_SelectionChanged(0x0056BD80)CPlayerSystem::SelectNext(0x0055F9A0)MovementManager::unpack_movement(0x00524440)CPhysicsObj::MoveOrTeleport(0x00516330)SmartBox::HandleCreateObject(0x00454C80)ACCObjectMaint::CreateObject(0x00558870)CPhysicsObj::set_description(0x00514F40)CPhysicsObj::enter_world(0x00516170)CPartArray::HandleEnterWorld(0x00517D70)MotionTableManager::HandleEnterWorld(0x0051BDD0)
Animation authority
on UpdateMotion(interpretedState):
pass the state through MovementManager and CMotionInterp
Dead is a persistent substate selected by the motion table
on UpdatePosition(position, velocity):
update the physics pose and velocity
do not select Ready, Walk, Run, or any other animation
acdream retains a documented AP-80 adaptation for ACE actors that move only through position snapshots: velocity may choose a visible locomotion cycle. That adaptation is now restricted to replacing only Ready/Walk/Run-family states. An authoritative action or substate such as attack, hit reaction, or Dead always wins. This preserves the adaptation without allowing a late position delta to make a corpse stand again.
CreateObject motion lifecycle
ACE creates a new corpse object after the victim's death animation finishes.
Its CreateObject packet correctly carries NonCombat + Dead; this was verified
in the 2026-07-12 live trace for six corpse GUIDs. The remaining stand-up was
not a parser or server-authority bug. The decisive follow-up trace showed the
replacement corpse completing Ready immediately after spawn. Dead is a
static persistent rest pose for this setup, so the corpse entered acdream's
"reactive" static-animation branch rather than the normal multi-frame branch.
That branch's gate had been generalized from doors to every nonzero motion
table, but its initializer was still door-specific: it inferred On/Off from
PhysicsState, discarded the wire's Dead command, and left the corpse on the
table's Ready default.
Retail performs these operations in this order:
SmartBox::HandleCreateObject(packet):
object = ACCObjectMaint::CreateObject(...)
ACCObjectMaint::CreateObject(...):
object.set_description(physicsDesc, applyMovement=true)
CPhysicsObj::set_description(...):
SetMotionTableID(physicsDesc.motionTable)
-> initialize the motion table's default Ready state
unpack_movement(physicsDesc.movement)
-> apply the wire's NonCombat + Dead state
-> temporarily build [Ready-to-Dead transition link, Dead cycle]
SmartBox::HandleCreateObject(...), after CreateObject returns:
object.enter_world(position)
CPhysicsObj::enter_world(...):
partArray.HandleEnterWorld()
CPartArray::HandleEnterWorld():
motionTableManager.HandleEnterWorld()
MotionTableManager::HandleEnterWorld():
sequence.remove_all_link_animations()
while pendingAnimations is not empty:
AnimationDone(success=false)
Therefore the first in-world corpse pose comes from the persistent Dead cycle, not the Ready-to-Dead transition that the already-deleted victim just played. This is a generic CreateObject lifecycle rule for every motion-table object; there is no corpse-name or corpse-type special case in the animation path.
acdream must apply this sequence identically in both of its render-registration paths:
initialize_spawn_motion(motionTable, wireMovementData):
sequencer.initialize_state() // table default
sequencer.set_cycle(wire.stance, wire.motion)
sequencer.handle_enter_world() // strip transient links
normal multi-frame spawn:
sequencer = initialize_spawn_motion(...)
static/reactive spawn (doors, corpses, gates, ...):
sequencer = initialize_spawn_motion(...)
Cross-checks:
- ACE
WorldObject_Networking.SerializePhysicsDatawrites the corpse'sCurrentMotionStateinto CreateObject MovementData. - ACE
Corpse.SetEphemeralValuessets that state toNonCombat + Dead. - ACE
Door.SetEphemeralValueslikewise suppliesNonCombat + On/Off; the client does not need to infer door motion fromPhysicsState. - The existing acdream
MotionTableManager.HandleEnterWorldport and its #174 conformance tests already implement the retail strip-and-drain semantics. The defect was that the static/reactive spawn path neither used the wire motion generically nor called that lifecycle step.
Auto Target after the selected creature dies
when authoritative Dead motion is applied to the selected object:
clear selected object
publish SelectionChanged
RecvNotice_SelectionChanged():
update target tracking
if selected object is zero
and combat mode is Melee or Missile
and Auto Target is enabled:
AutoTarget()
AutoTarget():
if a recent valid last-attacked object exists:
select it
else:
SelectNext(forward=true, includeCurrent=true, COMPASS_ITEM)
SelectNext(... COMPASS_ITEM) while in Melee/Missile:
reject non-attackable, fellowship, hidden, and out-of-radar candidates
rank eligible candidates by the retail weighted spatial distance
select the best candidate
The current M2 world-state projection has no retail lastAttacked instance-id
quality, so its existing closest eligible creature scan implements the
fallback branch. The important lifecycle behavior is exact: authoritative
death clears selection, and the SelectionChanged consumer reacquires only
when Auto Target is enabled in Melee/Missile combat.