Skip to content

Redundant per-tick SetHealth forwarding triggers GrimAC movement setbacks for controlling clients #309

Description

@iiroak

Description

Redundant per-tick ClientboundSetHealthPacket forwarding triggers GrimAC movement setbacks for controlling clients

Summary

When a client is controlling the proxy on a server running GrimAC, ZenithProxy forwards a ClientboundSetHealthPacket to the controlling client on (nearly) every server tick, even when health/food/saturation are unchanged. GrimAC emits a transaction (ClientboundPing) sandwich for every SetHealth packet it sends, so this roughly doubles the transaction rate and delivers transactions in bursts. That corrupts Grim's movement clock and produces a stream of Simulation (and Timer) setbacks.

For a human player this is mostly invisible (manual movement tolerates the rubber-banding), but for an automated/pathfinding client it is severe: the client crawls at a fraction of normal walking speed or gets stuck in a setback loop, because a pathfinder cannot absorb constant sub-block rewinds.

Environment

  • ZenithProxy 1.21.4 branch (observed on current HEAD, MCProtocolLib 1.21.4.62)
  • Backend: a 1.21.4 server running GrimAC (a local Folia test server here; 2b2t also runs Grim)
  • Controlling client: a headless/automated 1.21.4 client (no ViaVersion involved — native 1.21.4 on both hops)

How it was found

While debugging why an automated client walked normally when connected directly to the backend but crawled when connected through ZenithProxy (same account, same backend, same Grim), I compared the inbound packet streams on both paths:

  • Direct to backend: ~0 SetHealth packets while idle; ~16–17 transactions/s (≈ 1 per tick, normal).
  • Through ZenithProxy: ~22 SetHealth(20/20/20) per second while completely idle (not moving, not placing) — a constant, unchanging value — and ~40+ transactions/s arriving in bursts.

Enabling ZenithProxy's own packet log (debug.packetLog, filter sethealth) confirmed the packets arrive from the backend on the ClientSession and are forwarded 1:1 to the controlling client:

[ClientSession] Received: ClientboundSetHealthPacket(health=20.0, food=20, saturation=20.0)
[ServerSession] Sending:  ClientboundSetHealthPacket(health=20.0, food=20, saturation=20.0)

On the Grim side, PacketPlayerRespawn#onPacketSend (the UPDATE_HEALTH handler) only early-returns for pre-1.9 servers, so on modern servers it calls player.sendTransaction() and queues another transaction after send for every health packet — two transactions per redundant health update. At ~20 redundant packets/s this is what desynchronizes Grim's clock and yields constant Simulation setbacks.

Local patch (for validation)

I patched SetHealthHandler to still update the cache and fire events for every packet, but to suppress forwarding a SetHealth to the controlling client(s) when it is byte-for-byte identical to the last one forwarded. A freshly connected controller still receives its current health through the login cache sync (EntityPlayer#addPackets), so nothing observable changes client-side; only the redundant per-tick stream is dropped.

Built as a shadowJar and deployed to two live instances. Results, same map/route/account, through the proxy:

Metric Before After
SetHealth to client (idle) ~22/s 0/s
Grim Simulation setbacks constant 0
Grim Timer setbacks constant 0
Automated client throughput crawl / stuck normal walking speed

Proposal

De-duplicate identical consecutive SetHealth packets in the forward-to-client path (only forward when health/food/saturation actually change). I'm happy to open a PR with the patch I've been running.

Open question for the maintainer

Is the current 1:1 forwarding intentional (e.g. some client/anticheat elsewhere relies on the per-tick health stream), or is this an unintended passthrough? If suppressing is undesirable by default, it could be gated behind a config flag. Deferring to your judgment on the right approach here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions