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>
8.5 KiB
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
- Static decomp dive into
MoveOrTeleport,InterpolateTo,InterpolationManager::adjust_offset,set_velocity,GetAutonomyBlipDistance,set_local_velocity. - Constant-value lookup for every named distance/velocity referenced
by those functions (
.formats poi(acclient!NAME)). - 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_velocitycaller 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_velocityfires repeatedly (7 hits across 4 jumps; charge-frames- release).
- Inbound 0xF74E packets arrive (4) — these did NOT cause additional
set_velocityhits on remote physobjs in our window. Either retail gates insideDoVectorUpdatebased on entity type, or the velocity field got applied via a different path that doesn't trip our bp. - 207 walking-remote
UpdatePositions → zeroset_velocityhits.
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:
- Implement
InterpolationManager— FIFO queue (cap 20),InterpolateTo(targetPosition, isMovingTo)enqueues with GetAutonomyBlipDistance + DESIRED_DISTANCE prune,adjust_offset(dt)per-tick walks toward head atmin(minterp.get_adjusted_max_speed() × 2, MAX_INTERPOLATED_VELOCITY_FALLBACK 7.5) × dt,NodeCompletedpops on arrival withinDESIRED_DISTANCE 0.05,UseTimeperiodic stall detection (every 5 frames; if progress < 30 % of expected → fail counter; > 3 fails → blip-to head). - Implement
MoveOrTeleportrouting 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.
- Drop velocity-based dead-reckoning for walking remotes. Remote
m_velocityVectorshould stay at zero unless an inbound 0xF74E VectorUpdate sets it. The body's progress comes fromadjust_offset, not from Euler integration of state-derived velocity. - 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.SnapResidualDecayRateand the soft-snap residual blend.- The locally-recomputed velocity drive between UpdatePosition packets
(
apply_current_movement → get_state_velocity → Eulerpath on remote entities).
Files
interp_discovery.cdb/.log— symbol resolution + prologuesinterp_constants.cdb/.log— first constant lookupinterp_const2.cdb/.log— remaining constant lookupinterp_trace.cdb/.log— live routing distribution + set_velocity callers- This doc consolidates the answers