acdream/docs/research/2026-05-02-remote-entity-motion/resolved-via-cdb.md
Erik 08cb7f9614 docs(spec): Phase L.3 — Remote Entity Motion Conformance design
Port retail's InterpolationManager + MoveOrTeleport routing into
acdream so remote players, creatures, and NPCs stop popping at every
server position update and instead glide smoothly between sparse
authoritative updates the way retail does.

Three sub-lanes (incremental, each visually verifiable):
- L.3.1 — InterpolationManager core + routing + Omega + soft-snap teardown
- L.3.2 — PositionManager (root-motion + interp-offset combiner)
- L.3.3 — MoveToManager (server-controlled creature MoveTo)

This commit specs L.3.1 in detail and sketches L.3.2/L.3.3.

Research baseline (cdb live-trace + named-decomp dive 2026-05-02)
captured in docs/research/2026-05-02-remote-entity-motion/
resolved-via-cdb.md. All key constants confirmed from binary, not
guessed: MAX_PHYSICS_DISTANCE=96, MAX_INTERPOLATED_VELOCITY_MOD=2.0,
MAX_INTERPOLATED_VELOCITY=7.5, MIN_DISTANCE_TO_REACH_POSITION=0.20,
DESIRED_DISTANCE=0.05, queue cap 20, stall window 5/30%/3.

Rollout: ACDREAM_INTERP_MANAGER=1 env-var gate during development
(dual-path), single cleanup commit after visual verification removes
the flag + old hard-snap path + dead RemoteMotion soft-snap fields.

Test plan: ~15 unit tests against the InterpolationManager class
(pure-data, no game/window deps). Visual verification primary —
parallel retail observer of +Acdream walking/running/strafing/
jumping/turning, all should glide.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 18:12:18 +02:00

8.5 KiB
Raw Blame History

Remote-entity motion — open questions resolved via cdb live trace

Date: 2026-05-02 Companion to: the three agent research reports (paste them in alongside if needed; they live in worktree adoring-torvalds-d796cf, sleepy-grothendieck-9d7483, gracious-wright-7af984).

The three reports converged on the same overall pipeline (queued position-chase via InterpolationManager) but diverged on three specifics. We resolved all three with a focused cdb attach to the live retail acclient.exe v11.4186.

Resolution method

  1. Static decomp dive into MoveOrTeleport, InterpolateTo, InterpolationManager::adjust_offset, set_velocity, GetAutonomyBlipDistance, set_local_velocity.
  2. Constant-value lookup for every named distance/velocity referenced by those functions (.formats poi(acclient!NAME)).
  3. Live trace with bps on the routing functions while the user did a ~30-second mixed-motion scenario in retail (walk, strafe, jump, run-jump). Captured per-bp hit counts + set_velocity caller return addresses.

Scripts: interp_discovery.cdb, interp_constants.cdb, interp_const2.cdb, interp_trace.cdb. Logs in worktree.

Question 1 — distance threshold value(s)

Answer: the agents weren't disagreeing — they were describing two different thresholds doing two different jobs.

Threshold Constant Value Where Decision
Routing gate MAX_PHYSICS_DISTANCE 96 m CPhysicsObj::MoveOrTeleport within → InterpolateTo (queue); beyond → SetPositionSimple (slide-snap)
Enqueue blip GetAutonomyBlipDistance() 100 m outdoor / 20 m indoor (creature)
100 m outdoor / 25 m indoor (player)
CPhysicsObj::InterpolateTo beyond → enqueue InterpolationNode; within → simpler path (set_heading + StopInterpolating)
Reach / duplicate-prune DESIRED_DISTANCE 0.05 m InterpolateTo + adjust_offset node "reached"; tail-prune duplicates

The agents who said "96" cited MAX_PHYSICS_DISTANCE in MoveOrTeleport correctly. The one who said "100/25" cited GetAutonomyBlipDistance correctly but had the indoor-creature value off by 5 (it's 20, not 25 — 25 is the PLAYER indoor distance, used for self-correction not for remote entities).

GetAutonomyBlipDistance decoded:

float CPhysicsObj::GetAutonomyBlipDistance(this) {
    bool isPlayer = (this == CPhysicsObj::player_object);
    bool isIndoor = (this->cell_id & 0xFFFF) >= 0x100;  // 2-byte cell IDs are dungeon/inside
    if (isPlayer) {
        return isIndoor ? PLAYER_INSIDE_BLIP_DISTANCE      // 25
                        : PLAYER_OUTSIDE_BLIP_DISTANCE;     // 100
    } else {
        return isIndoor ? CREATURE_INSIDE_BLIP_DISTANCE    // 20
                        : CREATURE_OUTSIDE_BLIP_DISTANCE;   // 100
    }
}

Question 2 — polarity of the 96 m branch

Answer: within 96 m → queue (InterpolateTo). Beyond 96 m → snap (SetPositionSimple). Decoded from the disasm AND confirmed by trace.

Disasm of CPhysicsObj::MoveOrTeleport at +0x60:

fld   [esi+20h]                   ; this->distance_to_player
fcomp MAX_PHYSICS_DISTANCE         ; vs 96.0
fnstsw ax
test  ah, 5
jp    +0x91                        ; JP fires when distance > 96 → snap branch

; FALL-THROUGH (distance ≤ 96): queue path
call  CPhysicsObj::InterpolateTo

+0x91:                             ; (distance > 96): snap branch
call  PositionManager::StopInterpolating
call  CPhysicsObj::SetPositionSimple

Trace results (~30 sec mixed motion in Holtburg, all entities visible within ~30 m of camera):

BP Hits Notes
MoveOrTeleport 207 every inbound UpdatePosition
InterpolateTo 207 100 % routed to queue
SetPositionSimple 0 no slide-snaps
SetPosition 0 no teleports/no-cell

Confirmed: every routing decision in the test went to the queue. SetPositionSimple is the rare exception, only used when the entity is beyond camera range of significance.

Question 3 — does walking-remote use UpdatePosition velocity (set_velocity)?

Answer: NO. R3 was correct. Walking remote entities never call set_velocity; their m_velocityVector stays at zero (or whatever prior). Position progress is achieved entirely through adjust_offset walking the body toward queued waypoints.

Trace evidence:

BP Hits Notes
set_velocity 7 All 7 had caller = 0x00511534
HandleVectorUpdate (inbound 0xF74E) 4 jumps

Resolved 0x00511534 → it's inside CPhysicsObj::set_local_velocity (starts at 0x005114d0). And set_local_velocity is what retail's local-jump path (CMotionInterp::LeaveGround) uses to stuff the launch velocity into the body. So:

  • Local player jumps 4 times → LeaveGround → set_local_velocity → set_velocity fires repeatedly (7 hits across 4 jumps; charge-frames
    • release).
  • Inbound 0xF74E packets arrive (4) — these did NOT cause additional set_velocity hits on remote physobjs in our window. Either retail gates inside DoVectorUpdate based on entity type, or the velocity field got applied via a different path that doesn't trip our bp.
  • 207 walking-remote UpdatePositions → zero set_velocity hits.

So for walking remotes, m_velocityVector is zero and the UpdatePhysicsInternal Euler integration position += velocity*dt contributes nothing. All visible motion is from adjust_offset walking the body toward queue head.

Bonus — the rest of the constants we needed

Resolved while we were in there:

Constant Value Where used
MAX_INTERPOLATED_VELOCITY_MOD 2.0 adjust_offset — multiplier on minterp->get_adjusted_max_speed() (the catch-up gain)
MAX_INTERPOLATED_VELOCITY 7.5 m/s adjust_offset fallback when minterp is unavailable
MIN_DISTANCE_TO_REACH_POSITION 0.20 m adjust_offset per-5-frame progress threshold
max_velocity 50 m/s set_velocity magnitude clamp
BIG_DISTANCE 999999 m sentinel
CAMERA_MAP_DISTANCE 450 m unrelated; map render only

Constraint distances (used by the constraint sub-system, not the queue):

Constant Value
PLAYER_OUTSIDE_CONSTRAINT_DISTANCE_START 10
PLAYER_OUTSIDE_CONSTRAINT_DISTANCE_MAX 50
PLAYER_INSIDE_CONSTRAINT_DISTANCE_START 5
PLAYER_INSIDE_CONSTRAINT_DISTANCE_MAX 20
CREATURE_OUTSIDE_CONSTRAINT_DISTANCE_START 10
CREATURE_OUTSIDE_CONSTRAINT_DISTANCE_MAX 50
CREATURE_INSIDE_CONSTRAINT_DISTANCE_START 5
CREATURE_INSIDE_CONSTRAINT_DISTANCE_MAX 20

Where this leaves the implementation plan

The picture is now fully drawn. To match retail, acdream needs to:

  1. Implement InterpolationManager — FIFO queue (cap 20), InterpolateTo(targetPosition, isMovingTo) enqueues with GetAutonomyBlipDistance + DESIRED_DISTANCE prune, adjust_offset(dt) per-tick walks toward head at min(minterp.get_adjusted_max_speed() × 2, MAX_INTERPOLATED_VELOCITY_FALLBACK 7.5) × dt, NodeCompleted pops on arrival within DESIRED_DISTANCE 0.05, UseTime periodic stall detection (every 5 frames; if progress < 30 % of expected → fail counter; > 3 fails → blip-to head).
  2. Implement MoveOrTeleport routing in the inbound UpdatePosition handler. Replace acdream's current hard-snap with:
    • Stale-sequence (instance/position) → ignore.
    • Teleport-sequence newer or no-cell → SetPosition (hard-snap).
    • has_contact false → no-op.
    • has_contact true && distance ≤ 96 → InterpolateTo.
    • has_contact true && distance > 96 → SetPositionSimple slide-snap.
  3. Drop velocity-based dead-reckoning for walking remotes. Remote m_velocityVector should stay at zero unless an inbound 0xF74E VectorUpdate sets it. The body's progress comes from adjust_offset, not from Euler integration of state-derived velocity.
  4. Apply VectorUpdate.Omega. Currently parsed but not applied — fix to make jumping/turning remote arcs match.

acdream code that goes away when this lands:

  • RemoteMotion.SnapResidualDecayRate and the soft-snap residual blend.
  • The locally-recomputed velocity drive between UpdatePosition packets (apply_current_movement → get_state_velocity → Euler path on remote entities).

Files

  • interp_discovery.cdb / .log — symbol resolution + prologues
  • interp_constants.cdb / .log — first constant lookup
  • interp_const2.cdb / .log — remaining constant lookup
  • interp_trace.cdb / .log — live routing distribution + set_velocity callers
  • This doc consolidates the answers