Skip to content

fix(entity): interpolate actors over ticks like the game's lerpTo - #65

Merged
notchyves merged 1 commit into
theselauraproject:mainfrom
NaySurGithub:fix/actor-lerp
Nov 6, 2026
Merged

notchyves merged 1 commit into
theselauraproject:mainfrom
NaySurGithub:fix/actor-lerp

Conversation

@NaySurGithub

Copy link
Copy Markdown
Contributor

Problem

Once #62 fixed the click position, a precise hitbox check kept flagging melee hits in PvP (hitbox.b). That check takes the attack's clickPosition (InventoryTransaction → UseItemOnEntity, action Attack) and flags it when it lands more than 0.0085 block outside the target's padded hitbox, using the target's last two ACK-confirmed positions.

Report Deviation H (blocks) ack CPS
foul 5 0.5 4 ms 7
foul 10 0.408 3 ms 3
foul 15 0.33 32 ms 6
foul 20 0.419 23 ms 7

A moving player covers 0.28 (sprint) to 0.4 (sprint-jump) block per tick, so these deviations are one to two ticks of target movement.

Cause

EntityTracker::Move / MoveDelta moved a remote actor straight to every move_entity / move_entity_delta / move_player. UpdateAim raycasts against that position, so the click was tested against the newest position, often before the client has acknowledged it. Meanwhile the renderer drew the actor from its own sample buffer, about 100 ms behind. What you saw, what Perlin hit and what the server expected were three different positions.

What the vanilla client does

The behaviour below comes from IDA on the Windows client 1.26.60.5:

  • Move handler 0x1422C88B0 → 0x1422C8D20. It calls Actor::lerpTo(pos, rot, steps, forceCompletion) (0x142C74350) with steps = ticks when ticks >= 4, else 3. It places the actor at once (0x1422C8ED0) instead when the packet is a teleport moving 0.5 block or more (dist² >= 0.25), or when the destination block is not loaded.
  • Actor::lerpTo writes MovementInterpolatorComponent: target position and rotation, remaining/total steps, and the forceCompletion flag. If an interpolation started with forceCompletion is still running, a new target instead goes to QueuedMovementInterpolationComponent.
  • MovementInterpolatorSystem::handleQueuedMovementInterpolation (0x143634AE0): while the forced interpolation runs, the queued target loses a step each tick (max(steps, 2) - 1). After that it becomes the active interpolation.
  • MovementInterpolatorSystem::tickClient (0x143630D70): once per tick, pos += (target - pos) / remaining, landing exactly on the target on the last step, then remaining--.

So the client's HitResult (and the attack click, see #62) is tested against an actor position that trails the newest server position by up to the step count. That keeps it between the positions the server has seen acknowledged.

Change

  • EntityTracker keeps the actor's tick position in pos and its previous tick in prevPos / prev*. Received poses become a lerp target with the rules above (MoveKind::Lerp / Teleport; Place stays for our own predicted vehicles). move_entity_delta fills missing lanes from the last received pose, and spawning resets the interpolation.
  • EntityTracker::TickInterpolation() runs the queued and tick steps for every actor. It runs once per client tick in LocalPlayer::Update, just before the local player's tick, and on its own 20 Hz clock while a replay plays (there is no local tick then).
  • Handlers pass the packet's forceMove / ticks / forceCompletion (move_entity_delta), flag bit 1 (move_entity), and Teleport/Reset modes (move_player).
  • EntityRenderer draws remote actors between prevPos and pos with the client partial tick, instead of the sample buffer and its adaptive delay. What is drawn is now exactly what the attack ray is tested against. Projectiles and our own player keep their existing paths.
  • tests/actor_lerp_selftest.cpp covers the 3-step curve, replacement, ticks above 3, delta lanes, the forceCompletion queue, near and far teleports, and moves before the first tick.

Not done

  • Placing at once when the destination block is not loaded (TODO in Receive).
  • Head yaw follows the same steps as body rotation. The client may lerp the head separately; this is not verified.

Testing

  • Build and actor_lerp_selftest are running; results will follow on this PR.
  • Still to do: PvP on the same server with a packet capture, checking that hitbox.b no longer fires; replays still look smooth.

Remote actors jumped straight to every move_entity / move_entity_delta, and
the attack ray and click were tested against that newest position. The game
moves them toward it over 3 ticks (or the packet's ticks), so its hit
position sits between the positions the server has seen acknowledged;
precise hitbox checks flagged ours by 0.3 to 0.5 block on moving targets.

The renderer now draws between the last two ticks instead of its own
~100 ms sample buffer, so what is drawn is what the hit is tested against.
@notchyves
notchyves marked this pull request as ready for review November 6, 2026 21:32
@notchyves
notchyves merged commit 2ab0aac into theselauraproject:main Nov 6, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants