Compare commits

...
Author SHA1 Message Date
Horizon agentandClaude Opus 5.5 1340e54376 cargo fmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 17:23:08 +01:00
Horizon agentandClaude Opus 5.5 5ae9f06129 cargo fmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 16:06:26 +01:00
Horizon agentandClaude Opus 5.5 13061c109f lan_bot: the record is the guest's
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 15:41:16 +01:00
Horizon agentandClaude Opus 5.5 40eb5c7fd7 Record: the teleport's restart reads the guest's record
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 15:40:35 +01:00
Horizon agentandClaude Opus 5.5 04a8acc5a3 Movement prediction: the client-tick record moves from horizon-anticheat into the engine's replay
Layer plan M6, step toward dropping horizon-client-lib -> horizon-anticheat. The record
(horizon_anticheat::prediction) is what the engine's movement replay (lan::prediction) plays again:
it moves to lan/prediction/record.rs unchanged. The guest holds it and the client's sprint
(was Player.record / Player.sprinting). Behaviour unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 15:32:06 +01:00
8 changed files with 34 additions and 39 deletions

No files matched your search

+2 -12
View File
@@ -21,7 +21,6 @@ use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
pub mod combat;
pub mod prediction;
pub mod timer;
/// What one check's violation level leads to.
@@ -162,8 +161,8 @@ checks! {
/// Prediction: each move played again from the client's own inputs with vanilla's movement code,
/// and compared (speed, step, fly, jesus...). Each player's client ticks are recorded
/// (`prediction::Record`), with a ping a tick to place what the server changes, and played again
/// (horizon-client's `lan::prediction`). A move that lands farther than `tolerance` from the
/// (horizon-client's `lan::prediction::record`), with a ping a tick to place what the server changes,
/// and played again (`lan::prediction`). A move that lands farther than `tolerance` from the
/// replay flags, by how far it lands (one `tolerance` off weighs 1, at most 5 a tick), and the
/// setback puts the player back where its moves last agreed. A tick near what the replay can't
/// play yet (another entity, a block change or abilities the client hadn't seen, a moving piston,
@@ -279,15 +278,6 @@ pub struct Player {
pub timer: timer::Balance,
/// The server tick and height where the ground last held the player.
pub ground: Option<(u64, f64)>,
/// The client ticks the prediction plays again (only while it runs).
pub record: Option<Box<prediction::Record>>,
/// Sprinting as the client's own commands left it (ServerboundPlayerCommandPacket's
/// START_SPRINTING and STOP_SPRINTING: LocalPlayer.wasSprinting), which the record's ticks take.
/// Not the server's player: a hit with extra knockback stops its sprint
/// (Player.causeExtraKnockback), the client's own `Player.attack` doesn't (a mob's `hurtClient`
/// is false), and once the server's flag reaches it LocalPlayer.aiStep starts the sprint again
/// while the sprint key is held, with nothing to say: wasSprinting never changed.
pub sprinting: bool,
/// The attacks of the client tick being heard.
pub attacks: combat::Attacks,
}
+1 -2
View File
@@ -66,8 +66,7 @@ impl Game<'_> {
motion.item_use_speed = item_use.map(|(speed, _)| speed);
motion.item_use_slows_sprint = item_use.is_some_and(|(_, slows)| slows);
}
let (Some(record), Some(server)) = (g.anticheat.record.as_deref_mut(), g.motion.as_deref())
else {
let (Some(record), Some(server)) = (g.record.as_deref_mut(), g.motion.as_deref()) else {
return;
};
let mut run = |body: &mut PlayerMotion, keys, sprinting| {
+5 -8
View File
@@ -157,11 +157,8 @@ impl Hub {
};
if ac.config.runs(Check::Prediction) {
let g = &mut self.guests[i];
let (input, sprinting) = (g.input, g.anticheat.sprinting);
g.anticheat
.record
.get_or_insert_default()
.tick_end(input, sprinting);
let (input, sprinting) = (g.input, g.client_sprinting);
g.record.get_or_insert_default().tick_end(input, sprinting);
}
if !ac.config.runs(Check::Timer) {
return;
@@ -249,7 +246,7 @@ impl Hub {
flags: u8,
) {
if self.recording() {
let record = self.guests[i].anticheat.record.get_or_insert_default();
let record = self.guests[i].record.get_or_insert_default();
record.moved(pos, rot, flags);
}
}
@@ -259,7 +256,7 @@ impl Hub {
pub(super) fn anticheat_pong(&mut self, i: usize, id: i32) {
let ticks = self.ticks;
if self.recording() {
let record = self.guests[i].anticheat.record.get_or_insert_default();
let record = self.guests[i].record.get_or_insert_default();
// Ids are server ticks, cut to 32 bits.
record.pong(u64::from(id as u32), ticks);
}
@@ -280,7 +277,7 @@ impl Hub {
.get_mut()
.unwrap_or_else(std::sync::PoisonError::into_inner),
);
if let Some(record) = g.anticheat.record.as_deref_mut() {
if let Some(record) = g.record.as_deref_mut() {
for change in changes {
record.changed(ticks, change);
}
+14 -3
View File
@@ -1,6 +1,7 @@
//! A remote player: connection, position, chunk and entity views, and the
//! player state (inventory, health, game mode) that the host's gameplay code
//! operates on when it acts for them.
use super::prediction::record::{Change, Record};
use super::{Joined, PlayerSender, entity::Tracker};
use crate::{
items::ItemStack,
@@ -221,10 +222,19 @@ pub struct Guest {
pub position_this_tick: bool,
/// Violation levels and the checks' state (the anticheat, when on).
pub anticheat: horizon_anticheat::Player,
/// The client ticks the movement prediction plays again (only while it records).
pub record: Option<Box<Record>>,
/// Sprinting as the client's own commands left it (ServerboundPlayerCommandPacket's
/// START_SPRINTING and STOP_SPRINTING: LocalPlayer.wasSprinting), which the record's ticks take.
/// Not the server's player: a hit with extra knockback stops its sprint
/// (Player.causeExtraKnockback), the client's own `Player.attack` doesn't (a mob's `hurtClient`
/// is false), and once the server's flag reaches it LocalPlayer.aiStep starts the sprint again
/// while the sprint key is held, with nothing to say: wasSprinting never changed.
pub client_sprinting: bool,
/// The movement prediction's replay of the client's own moves (only while it runs).
pub replay: Option<Box<super::prediction::Replay>>,
/// What the server sent this tick that changes how the client moves (only while it records).
pub changes: std::sync::Mutex<Vec<horizon_anticheat::prediction::Change>>,
pub changes: std::sync::Mutex<Vec<Change>>,
/// It accepted a move since the last entity range checks
/// (ServerChunkCache.move), which then check all of its pairs again.
pub moved: bool,
@@ -515,6 +525,8 @@ impl Guest {
support_blocks_sent: 0,
position_this_tick: false,
anticheat: Default::default(),
record: None,
client_sprinting: false,
replay: None,
changes: Default::default(),
moved: false,
@@ -626,7 +638,7 @@ impl Guest {
if self.fake && !super::conn::fake_wants_id(packet.id()) {
return;
}
if self.anticheat.record.is_some() {
if self.record.is_some() {
self.note_change(packet);
}
self.sender.send(packet.encode());
@@ -653,7 +665,6 @@ impl Guest {
/// What `packet` changes about how the client moves, for the prediction's replay: kept until
/// the tick's ping places it (`Hub::anticheat_ping`).
fn note_change(&self, packet: &play::Clientbound) {
use horizon_anticheat::prediction::Change;
use play::Clientbound as C;
let own = |id: i32| id == self.entity_id;
let change = match packet {
+6 -9
View File
@@ -3168,7 +3168,7 @@ impl Hub {
// changed, so the one nearest the player stands for them.
for &i in &viewers {
let g = &mut self.guests[i];
let Some(record) = g.anticheat.record.as_deref_mut() else {
let Some(record) = g.record.as_deref_mut() else {
continue;
};
let slack = super::prediction::slack(std::array::from_fn(|i| {
@@ -3252,7 +3252,7 @@ impl Hub {
if recording {
for &i in &viewers {
let g = &mut self.guests[i];
let Some(record) = g.anticheat.record.as_deref_mut() else {
let Some(record) = g.record.as_deref_mut() else {
continue;
};
let slack = super::prediction::slack(std::array::from_fn(|i| {
@@ -3643,7 +3643,7 @@ impl Hub {
}));
for at in gone.drain(..) {
let block = at.map(|v| v.floor() as i32);
if let Some(record) = g.anticheat.record.as_deref_mut()
if let Some(record) = g.record.as_deref_mut()
&& super::prediction::near(g.position, block, slack)
{
record.entity_gone(ticks);
@@ -4045,7 +4045,7 @@ impl Hub {
fn set_sprinting(&mut self, i: usize, sprinting: bool) {
let g = &mut self.guests[i];
g.sprinting = sprinting;
g.anticheat.sprinting = sprinting;
g.client_sprinting = sprinting;
if let Some(m) = g.motion.as_mut() {
let _ = m.set_sprint_modifier(sprinting);
}
@@ -4132,10 +4132,7 @@ impl Hub {
// The prediction's replay starts again where the client now is.
if recording {
let kept = g.awaiting_keeps_motion;
g.anticheat
.record
.get_or_insert_default()
.teleported(at, kept);
g.record.get_or_insert_default().teleported(at, kept);
}
let rot = (p.yaw, p.pitch);
self.position_change(i, [p.x, p.y, p.z], rot, 0, ctx, events);
@@ -5119,7 +5116,7 @@ impl Hub {
// ClientPacketListener.handleRespawn without KEEP_ENTITY_DATA: the client's new player
// starts with wasSprinting false. (A level change keeps it: the new player sprints on, or
// says STOP.)
g.anticheat.sprinting = false;
g.client_sprinting = false;
// The respawned player is a new one, without effects; so is the
// client's (ClientPacketListener.handleRespawn).
g.effects = Default::default();
+5 -3
View File
@@ -1,5 +1,5 @@
//! The movement prediction's replay (docs/anticheat.md): each client tick a player's record holds
//! ([`horizon_anticheat::prediction::Record`]) is played again with the client's own movement code,
//! ([`Record`]) is played again with the client's own movement code,
//! from where the last one ended, and compared with where the client said it went.
//!
//! The inputs are known exactly: a vanilla client sends ServerboundPlayerInputPacket after its tick's
@@ -21,8 +21,10 @@
use std::collections::VecDeque;
use horizon_anticheat::prediction::{Change, ClientTick, KEPT, Record, Restart};
pub mod record;
use horizon_core::geometry::Vec3;
use record::{Change, ClientTick, KEPT, Record, Restart};
use crate::motion::PlayerMotion;
use crate::sprint::Keys;
@@ -650,13 +652,13 @@ fn sync(body: &mut PlayerMotion, server: &PlayerMotion) {
#[cfg(test)]
mod tests {
use super::record::Record;
use super::*;
use crate::{
assets::Assets,
motion::{MotionResources, MotionWorld},
world::World,
};
use horizon_anticheat::prediction::Record;
use std::path::Path;
/// Keys for each tick: 20 walking, 12 sprint-jumping, 12 strafing while turning, 6 still.
+1 -2
View File
@@ -1522,7 +1522,6 @@ fn the_prediction_records_each_client_tick_and_the_server_ticks_it_had_seen() {
inbox.push_back((S::ClientTickEnd(Empty), 0));
s.tick(&[]);
let record = s.hub.guests[0]
.anticheat
.record
.as_deref()
.expect("the prediction records");
@@ -1546,7 +1545,7 @@ fn the_prediction_records_each_client_tick_and_the_server_ticks_it_had_seen() {
0,
));
s.tick(&[]);
let record = s.hub.guests[0].anticheat.record.as_deref().unwrap();
let record = s.hub.guests[0].record.as_deref().unwrap();
assert_eq!(record.seen(), Some(pinged));
}