Make the retained toolbar consume retail's payload-free selection notice from canonical live state so a reentrant Auto Target replacement cannot be erased by the outer clear. Restrict automatic acquisition to the requested hostile non-player monster policy and record the intentional PK edge divergence. Co-Authored-By: Codex <codex@openai.com>
7 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.
Selection-notice reentrancy
Retail's selection notice has no selected-object payload:
ACCWeenieObject::SetSelectedObject(newId):
selectedID = newId
CM_UI::SendNotice_SelectionChanged()
CM_UI::SendNotice_SelectionChanged():
for each registered handler:
handler.RecvNotice_SelectionChanged()
gmToolbarUI::HandleSelectionChanged():
id = ACCWeenieObject::selectedID // read current global state
clear and repopulate selected-object strip from id
If ClientCombatSystem::RecvNotice_SelectionChanged runs first for a clear,
its AutoTarget() may select a replacement reentrantly. A later toolbar
handler reads the replacement from global state; it does not receive and apply
the stale outer clear. acdream's retained toolbar must likewise read the
canonical SelectionState.SelectedObjectId when notified rather than use the
captured transition payload.
acdream automatic monster narrowing
Retail's combat-mode SelectNext(COMPASS_ITEM) rejects candidates that fail
ClientCombatSystem::ObjectIsAttackable @ 0x0056A600; that predicate rejects
friendly NPCs, pets, corpses, and non-creatures but can accept enemy players in
compatible PK states. By explicit product direction, acdream narrows automatic
acquisition further to non-player hostile monsters. Manual player selection
remains separate. This intentional PK edge divergence is register IA-19.