15 Commits
Author SHA1 Message Date
Ludovic MarieandClaude Opus 5.5 b65e532430 Anticheat prediction: the record's sprint is the client's own, not the server's player's
A sprinting hit stops the server player's sprint (Player.causeExtraKnockback); the
client's own attack on a mob doesn't, and once the server's flag reaches it the
client starts the sprint again with the sprint key held, without a START_SPRINTING
(wasSprinting never changed). The record took the server's flag, so the replay
walked while the client sprinted: the live 'ground/air water, keys forward sprint'
flags. The record now takes the client's sprint commands; a sprint the server
stopped is played on only if vanilla's client could start it again (the sprint key,
or forward pressed again).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 16:00:41 +01:00
Ludovic MarieandClaude Opus 5.5 2a1dd555d0 Anticheat prediction: the server's sprinting flag reaches the replay
A client whose STOP_SPRINTING and START_SPRINTING reach the server in two
server ticks gets the STOP's shared flags and movement speed (no sprint
modifier) after it sprints again: its flag cleared, LocalPlayer.aiStep
starts the sprint again, modifier included, and sends nothing. The
replay took the attributes but not the flag, so it sprinted on without
the modifier until the START's attributes came: 0.029 then 0.045 off,
the official server's "ground sprinting" flags (#5935).

Record the sprinting bit of the shared flags the server sends the player
(Change::Sprinting) and apply it to the replay's flag alone, as
SynchedEntityData.assignValues does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 14:04:46 +01:00
Ludovic MarieandClaude Opus 5.5 787a59fd85 Anticheat prediction: a log line says the tick's keys, where it ended and the replay's velocity
So a flag can be found again in the world and played there (#5879: water
flags that don't reproduce in the harness).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 12:05:04 +01:00
Ludovic MarieandClaude Opus 5.5 6fa9bef54f Anticheat prediction: log what the worst tick did
A prediction line said only how far off a move was. On the official server
it logs about 15,000 lines an hour, from ordinary players (mostly 0.1 to
0.2 blocks) and cheaters (1 to 10 blocks) alike, and nothing in a line
tells a glide, water, a ladder or a lag from a cheat, so the check can't
be trusted to act.

Each line now ends with its worst tick as the replay played it: where the
client said it went less where the replay ended (x, y, z), how far the
client said it moved, and what the replayed body was doing (ground or air,
water, gliding, sprinting, sneaking, swimming, flying, riding). Only
flagged ticks pay for the text; the state is a few bits per played tick.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 05:47:06 +01:00
Ludovic MarieandClaude Opus 5.5 ffcba19aa8 Anticheat ground: drop the claim, never send back or feed the floating kick
When ground acted, a false claim sent the player back to where it was:
into the air it claimed to stand in. A player there claims it again, is
sent back again (+0.5 VL twice a second at the same spot) and hovers until
vanilla's "floating too long" kick: in 11 minutes on the official server,
four players got it 20 times. Only the anticheat's own kick (logged, told
to the operators, counted toward the ban) should end a false claim.

ground's correction is now the dropped claim alone (vanilla's fall damage
follows), logged as "claim dropped"; it never sends the player back, and a
move whose claim was dropped never counts toward clientIsFloating.
setback = 0 still makes the check log only. The ground e2e runs with
allow-flight off and fails on a setback or a floating kick.

Fixes #5572

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 01:43:32 +01:00
Ludovic MarieandClaude Opus 5.5 6ca8da1ace Anticheat: ground acts by default (kick at 30, ban at the second kick)
The official server's first two hours with the anticheat logging only
(67 players): every player who wasn't flying or walking on water peaked
at a ground level of 7, and that was 4 claims standing still in one
second (a block the server didn't have after a long teleport). The seven
who were (no-fall, fly, jesus: claims every tick while rising or falling
through the air, or walking on water; most also kicked by vanilla's
floating check) peaked at 113 to 3749.

ground now defaults to setback 10, alert 15, kick 30, ban_after 2, and a
claim standing still isn't judged (no fall to skip; hovering is
vanilla's floating kick). Replayed on that log, the others peak at 5
and the seven are kicked, then banned. timer and prediction still only
log: prediction flagged nearly everyone, and timer's slow decay lets lag
bursts add up over a session.

anticheat_e2e's ground check now runs at the defaults with cheat bots
that move, and wants the ban count in the kick line.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 22:29:48 +01:00
Ludovic MarieandClaude Opus 5.5 c9ed92a2a9 Anticheat: ban at a check's ban_after kick (combat: the second)
A check that kicks now counts each player's kicks, across sessions and
restarts (anticheat-strikes.json). The kick that reaches the check's
`ban_after` bans instead: banned-players.json through the normal ban list
(source "Horizon anticheat", reason "Horizon anticheat: <check>"), the
player sent away with vanilla's ban message. combat bans at its second
kick; timer, ground and prediction keep ban_after = 0 until trusted.

Every kick and ban is logged with its evidence and told to the console
and online operators, so an op can /pardon a wrong one.

Also merges hub.rs's two S::Attack arms: the anticheat's shadowed #5123's
(unreachable pattern), so an attack never stopped the moves queued behind
it. Both apply now.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 20:09:20 +01:00
Ludovic MarieandClaude Opus 5.5 fdfa4fbfab Anticheat on by default: a combat check that acts, the other checks log only
Ludovic, 10-31: turn the anticheat on. Checks a vanilla client can't trip act by default; the
others run but only log until real players show no false positive. `[anticheat] enabled = false`,
or `enabled = false` in a check's table, turns them off.

New `combat` check, acting by default: vanilla's client attacks only from Minecraft.startAttack,
once a client tick, on the entity of the tick's hitResult, and swings (ServerboundPunchPacket) with
each attack. A client tick with more attacks than swings, or a second entity attacked in the same
tick, flags; from `setback` (2) on the attacks are dropped, `kick` 20. The swing counts before or
after the attack (older clients behind a version proxy swing first).

`timer`, `ground` and `prediction` default to setback, alert and kick 0 (log only); a log-only ground
check keeps the claim. The thresholds once trusted are in docs/anticheat.md.

horizon-bots swing after each attack as vanilla does; --no-swing and --attack A,B are the cheats for
tools/anticheat_e2e.py --check combat, which runs with no server.toml (the defaults).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 15:44:15 +01:00
Ludovic MarieandClaude Opus 5.5 71d7921132 Anticheat prediction (c): flag moves off the replay, set back to where they agreed
A move that lands farther than `[anticheat.prediction] tolerance` (0.03 blocks) from the replay
flags, weighing its offset in tolerances (at most 5 a tick); the setback sends the player to where
its moves last agreed. What the replay can't play yet excuses the tick and the ones after until the
moves agree again (40 ticks at most): another entity or player near (farther for a fast player), a
block change or an entity removal near the player that the client hadn't seen, a block change the
server hasn't sent, abilities the client hadn't seen, a moving piston, a chunk the client doesn't
have, a riptide launch, a teleport that keeps motion. Under an excuse the replay takes the client's
velocity, so it agrees again within a few ticks.

The replay now also nudges the player out of a block it stands in (LocalPlayer
.moveTowardsClosestSpace, worked out from the blocks as the game does for its own player), keeps
its body across setbacks, and plays the ticks that came in a burst with a teleport's accept before
starting again there, so their jump delay goes on through it.

The bot harness checks and the bench keep the prediction from flagging the bots, which move by
their own rules.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 09:29:57 +01:00
Ludovic MarieandClaude Opus 5.5 84f633b56b Anticheat prediction (b2): replay what the server sent, on the tick the client took it
What the server sends a player that changes how its client moves goes
into the record with the server tick that sent it: a velocity
(SetEntityMotion for the player itself, as the client decodes its
LpVec3), an explosion's knockback (added), and attributes and effects
(UpdateAttributes, mob effects). Guest::send notes them while recording;
the tick's ping places them. The replay applies each before the first
client tick whose pong had seen that server tick, which is when the
client handled it. A client that ignores a push (anti-knockback) is off
from that tick on. The attributes and effects are copied when they
change, no longer every tick.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 07:17:26 +01:00
Ludovic MarieandClaude Opus 5.5 7f969cb3a5 Anticheat prediction (b): play each client tick again and compare
Each client tick the record holds is played again with the client's own
movement code (MotionWorld::replayed_tick: LocalPlayer.aiStep with the
client's keys and the sprinting it said it had), on a copy of the
player, from where the last tick ended, starting at a teleport the
client accepted, and compared with where the client said it went. The
replay then goes on from the client's position. The keys and sprinting
are known exactly and never guessed; what the server can't know is
tried, every combination, the tick as heard first: the ground on the
first tick after a start, and the item use on the tick the server's
changed. It flags nothing yet: the check is the next PR.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 06:26:20 +01:00
Ludovic MarieandClaude Opus 5.5 d26c8fba0a Anticheat prediction (a): record each client tick and the server ticks it saw
The first step of the 1:1 movement prediction (planned with rdbtCVS on
(the packets between two ServerboundClientTickEndPackets: keys held,
sprinting, rotation, the move and its flags), the last two seconds of
them. Each server tick ends with a ClientboundPingPacket whose id is the
tick; the client's pong, which comes before its next tick, says from
which client tick on that server tick's packets were in effect.

A `prediction` check in [anticheat] turns it on (with the anticheat). It
records only and flags nothing yet; the next PRs play each tick again
with vanilla's movement code and compare. Pongs don't count as actions
(the idle timeout), as in vanilla.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 03:23:22 +01:00
Ludovic MarieandClaude Opus 5.5 f82149a3dd docs: the anticheat moves into a native mod once the hooks exist
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 00:05:00 +01:00
Ludovic MarieandClaude Opus 5.5 8975278e5e Anticheat: the ground check (no-fall, walking on water)
A move that says it ends on the ground is judged when nothing there can
hold a player: only air and fluids around the feet, no collision a tenth
of a block down, no boat, minecart, shulker or happy ghast near. The
false claim is dropped, so vanilla's fall damage follows, and the levels
send back, alert and kick as configured. Creative, spectators and
gliders are skipped. Off unless [anticheat] turns it on.

horizon-bots: --height and --ground. anticheat_e2e --check ground: a
hovering bot and one on water that claim the ground are flagged and
kicked, bots walking and standing on the ground never are.
server_bench --ground: survival bots on a flat world's ground.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 21:42:45 +01:00
Ludovic MarieandClaude Opus 5.5 876d0003d5 Anticheat framework, off by default, with a timer check
A new crate, horizon-anticheat, holds server.toml's [anticheat] settings, each player's violation
levels (they decay every second) and the checks' rules, with no game state. The dedicated server's
hub runs the checks where it handles packets and carries out a flag: send the player back, alert the
console, the log file and online operators, then kick; never a ban. Each check has its own toggle and
thresholds. Without [anticheat] (the default, and the generated server.toml only has a commented
example) the hub holds no anticheat and the server does what vanilla's does.

The first check is timer: vanilla's client ends each tick with ServerboundClientTickEndPacket at the
server's tick rate, so a client further ahead of the wall clock than 300 ms is flagged, with a capped
balance for lag spikes and slow server ticks.

horizon-bots gets --timer F (a sped-up client), tools/anticheat_e2e.py runs a real dedicated server
with legit and cheating bots, and server_bench.py --anticheat measures the cost.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 19:49:49 +01:00