# 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)<br>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:

```c
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 `UpdatePosition`s → **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
