fix(motion): restore retail object manager order

Process animation completion at the retail process_hooks boundary, then run targeting, movement, PartArray completion, and PositionManager in the named UpdateObjectInternal order for local, remote, hidden, and position-less animated objects. Retire TS-42 with deterministic conformance coverage.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-19 21:40:14 +02:00
parent 0f996db747
commit c5ab99081c
17 changed files with 493 additions and 98 deletions

View file

@ -1126,7 +1126,7 @@ public sealed class PlayerMovementController
/// live. A composed offset still commits through CTransition so cell
/// membership and contact state cannot become stale behind the hidden mesh.
/// </summary>
public MovementResult TickHidden(float dt)
public MovementResult TickHidden(float dt, Action? handleTargeting = null)
{
_simTimeSeconds += dt;
_physicsAccum = 0f;
@ -1180,8 +1180,14 @@ public sealed class PlayerMovementController
UpdateCellId(resolved.CellId, "hidden-position-manager");
}
Movement.UseTime();
PositionManager?.UseTime();
RetailObjectManagerTail.Run(
checkDetection: null,
handleTargeting,
movementUseTime: Movement.UseTime,
partArrayHandleMovement: _motion.CheckForCompletedMotions,
positionUseTime: PositionManager is { } positionManager
? positionManager.UseTime
: null);
_prevPhysicsPos = _body.Position;
_currPhysicsPos = _body.Position;
_wasAirborneLastFrame = !_body.OnWalkable;
@ -1201,42 +1207,13 @@ public sealed class PlayerMovementController
CurrentStyle: _motion.RawState.CurrentStyle);
}
public MovementResult Update(float dt, MovementInput input)
public MovementResult Update(
float dt,
MovementInput input,
Action? handleTargeting = null)
{
_simTimeSeconds += dt;
// R4-V5: tick the verbatim MoveToManager at the slot the deleted
// DriveServerAutoWalk occupied — after inbound wire routing, before
// the input-driven motion sections. UseTime runs the retail per-tick
// moveto drivers (HandleMoveToPosition aux-turn steering + arrival +
// fail-distance / HandleTurnToHeading); the motions it dispatches
// land in InterpretedState through _DoMotion → DoInterpretedMotion,
// and sections 1/2 below turn that state into Yaw rotation + body
// velocity — the same per-frame apply user input gets. No
// synthesized input exists, so the outbound-packet pipeline sees no
// user motion and never builds a MoveToState mid-moveto (the #75
// invariant, now by construction). Provisional tick placement until
// R6 ports retail's UpdateObjectInternal ordering (r4-port-plan.md
// §3 placement decision). R5-V5: the facade relay
// (MovementManager::UseTime 0x005242f0 — moveto side only).
Movement.UseTime();
// R4-V5 wedge fix (2026-07-03 live door bug), retail's per-tick
// completion slot: CPartArray::HandleMovement — a tailcall chain to
// MotionTableManager::CheckForCompletedMotions (0x00517d60 →
// 0x0051bfd0) — runs every tick right AFTER MovementManager::UseTime
// in UpdateObjectInternal (@005159a4). It flushes pending_animations
// entries whose animations already ran out so their AnimationDone →
// MotionDone pops land each tick even when no op fired that frame.
// (apply_current_movement is NOT part of the per-tick order — its
// retail callers are all event-driven: hold-key toggles, HitGround/
// LeaveGround, ReportExhaustion.) Full tick ordering remains R6
// scope; the companion fix is StopCompletely's previously-missing
// StopCompletely_Internal animation dispatch (see
// MotionInterpreter.StopCompletely), whose completable entry this
// slot drains.
_motion.CheckForCompletedMotions?.Invoke();
// Portal-space guard: while teleporting, no input is processed and
// no physics is resolved. Return a zero-movement result so the caller
// can detect the frozen state (MotionStateChanged = false, no commands).
@ -1710,13 +1687,6 @@ public sealed class PlayerMovementController
{
_prevPhysicsPos = oldTickEndPos;
_currPhysicsPos = _body.Position;
// R5-V3 (#171): retail UpdateObjectInternal TAIL — PositionManager::
// UseTime (@0x005159b3) runs AFTER the quantum's integration
// (whose head hosts adjust_offset) and only on frames where a
// physics quantum executed (retail's update_object MinQuantum
// gate skips the whole UpdateObjectInternal).
PositionManager?.UseTime();
}
// SetPositionInternal contact determination (pc:283468-510). acdream's resolver
@ -1778,6 +1748,22 @@ public sealed class PlayerMovementController
_wasAirborneLastFrame = !_body.OnWalkable;
UpdateCellId(resolveResult.CellId, "resolver");
// Named retail CPhysicsObj::UpdateObjectInternal (0x005156B0):
// process_hooks has already completed inside UpdatePositionInternal,
// then the transition commits, and only then does the ordered manager
// tail run. DetectionManager is not ported yet, so its slot is absent.
// PositionManager's clock stays tied to a consumed physics quantum;
// the remaining per-render versus per-quantum unification belongs to
// the R6 object scheduler rather than this ordering cutover.
RetailObjectManagerTail.Run(
checkDetection: null,
handleTargeting,
movementUseTime: Movement.UseTime,
partArrayHandleMovement: _motion.CheckForCompletedMotions,
positionUseTime: physicsTickRan && PositionManager is { } positionManager
? positionManager.UseTime
: null);
// ── 6. Determine outbound motion commands ─────────────────────────────
uint? outForwardCmd = null;
float? outForwardSpeed = null;