Vehicle teleport: review fixes (rotation packet, cooldown per tick, rider's vehicle)

- /rotate on a server's player sends ClientboundPlayerRotationPacket
  (ServerPlayer.forceSetRotation) when its turn ends, riding or not,
  instead of a zero-motion teleport (or nothing for a rider).
- The guest's boarding cooldown counts down once per tick in
  guest_server_tick, not once per enter_guest; a vehicle dropped in
  guest_vehicle_tick also starts it.
- A teleported vehicle's riding guests hear the vehicle's teleport
  (ClientboundTeleportEntityPacket), so a driven boat is not put back
  by the client's next MoveVehicle.

Found by martipops's review.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Ludovic MarieandClaude Opus 5.5 committed 2026-11-04 15:06:22 +01:00
1 parent 7bacc86a5e
commit 2c7b49062e
4 files changed
+54 -4

No files matched your search

+4
View File
@@ -2862,6 +2862,9 @@ struct Game<'a> {
/// last tick (`game/boarding.rs`).
boarding_cooldown: i32,
riding_at_tick: Option<u32>,
/// A rotation-only write (`/rotate`, `Entity.forceSetRotation`) to the player of this
/// turn: `leave_guest` sends it as `ClientboundPlayerRotationPacket`, not a teleport.
rotation_forced: bool,
/// The mount the last `position_rider` put the player on, to tell the tick
/// a seat is taken (`Entity.moveTo`, no camera sweep) from the ones that
/// follow it (`positionRider`, interpolated as usual).
@@ -19156,6 +19159,7 @@ impl<'a> Game<'a> {
riding: None,
boarding_cooldown: 0,
riding_at_tick: None,
rotation_forced: false,
posed_rider: None,
pending_vehicle: None,
ender_items: vec![None; 27],
+19 -4
View File
@@ -523,9 +523,10 @@ impl Game<'_> {
self.right = play.using.is_some() || self.using.is_some();
// The guest's vehicle (Entity.vehicle) for its turn.
let riding = std::mem::replace(&mut self.riding, g.vehicle);
// Entity.baseTick: the guest's boarding cooldown, for its turn.
// The guest's boarding cooldown for its turn; `guest_server_tick`
// counts it down once per tick (Entity.baseTick), not per turn.
let host_boarding = (self.boarding_cooldown, self.riding_at_tick);
self.boarding_cooldown = super::boarding::tick_down(g.boarding_cooldown);
self.boarding_cooldown = g.boarding_cooldown;
self.riding_at_tick = g.vehicle;
// `Entity.isPassenger()` for that turn, from the same vehicle: the
// guest's body keeps no swim (`Entity.updateSwimming` needs it false).
@@ -979,9 +980,19 @@ impl Game<'_> {
std::mem::swap(&mut self.armor, &mut g.armor);
std::mem::swap(&mut self.offhand, &mut g.offhand);
std::mem::swap(&mut self.carried, &mut g.carried);
// A teleport with a rotation (commands) turns the guest.
let turned = [self.yaw, self.pitch] != [g.yaw, g.pitch];
// A teleport with a rotation (commands) turns the guest. `/rotate`
// (ServerPlayer.forceSetRotation) sends the rotation alone, rider or not.
let forced = std::mem::take(&mut self.rotation_forced);
let turned = !forced && [self.yaw, self.pitch] != [g.yaw, g.pitch];
[g.yaw, g.pitch] = [self.yaw, self.pitch];
if forced {
g.send(&C::PlayerRotation(play::PlayerRotation {
yaw: g.yaw,
relative_yaw: false,
pitch: g.pitch,
relative_pitch: false,
}));
}
self.yaw = yaw;
self.pitch = pitch;
self.eye = eye.1;
@@ -4304,6 +4315,10 @@ impl Game<'_> {
/// regeneration and the food tick (ServerPlayer.doTick). Their hurt
/// sounds are heard at the guest.
fn guest_server_tick(&mut self, host: &mut LanHost, conn: u32) {
// Entity.baseTick: the boarding cooldown counts down once a tick.
if let Some(g) = host.hub.guest_mut(conn) {
g.boarding_cooldown = super::boarding::tick_down(g.boarding_cooldown);
}
self.guest_replay(host, conn);
self.guest_vehicle_tick(host, conn);
// The last move's fall stands in for the server's delta movement.
@@ -234,6 +234,8 @@ impl Game<'_> {
if dead || (boat.is_none() && !cart && !mount && !cushion) {
let g = &mut host.hub.guests[gi];
(g.vehicle, g.vehicle_good) = (None, None);
// Entity.removePassenger: the rider waits 60 ticks to board again.
g.boarding_cooldown = super::boat_passengers::BOARDING_COOLDOWN;
return;
}
let mut ejected = false;
@@ -6,6 +6,7 @@
use super::Game;
use super::commands::Target;
use horizon_core::geometry::Vec3;
use horizon_protocol::packets::play::{self, Clientbound as C};
/// `Entity.calculatePassengerTransition` with no relative axes: the passenger
/// keeps its offset from the vehicle.
@@ -68,11 +69,24 @@ impl Game<'_> {
p.position = passenger_destination(old, new, p.position);
self.previous_position = p.position;
}
let [yaw, pitch] = self.vehicle_rotation(vehicle);
if let Some(host) = self.lan.as_mut() {
for i in 0..host.hub.guests.len() {
if host.hub.guests[i].vehicle != Some(vehicle) {
continue;
}
// Entity.sendTeleportTransitionToRidingPlayers: the rider's
// client moves the vehicle it drives itself, so it hears the
// vehicle's teleport (else its next MoveVehicle undoes it).
host.hub.guests[i].send(&C::TeleportEntity(play::TeleportEntity {
id: vehicle as i32,
position: [new.x, new.y, new.z],
delta: [0.0; 3],
yaw,
pitch,
relatives: 0,
on_ground: false,
}));
let g = host.hub.guests[i].position;
let at = passenger_destination(old, new, Vec3::new(g[0], g[1], g[2]));
host.hub.teleport_keeping_motion(i, [at.x, at.y, at.z]);
@@ -80,6 +94,19 @@ impl Game<'_> {
}
}
/// A vehicle's yaw and pitch, for the riders' teleport packet.
fn vehicle_rotation(&self, id: u32) -> [f32; 2] {
if let Some(b) = self.boats.iter().find(|b| b.id == id) {
[b.yaw, 0.0]
} else if let Some(c) = self.carts.iter().find(|c| c.id == id) {
[c.yaw, c.pitch]
} else if let Some(m) = self.mobs.iter().find(|m| m.id == id) {
[m.yaw, m.head_pitch]
} else {
[0.0, 0.0]
}
}
/// `Entity.forceSetRotation`: yaw, head yaw, pitch and their old values
/// only. Position, motion, the vehicle and the riders stay.
pub(super) fn set_rotation(&mut self, t: Target, rotation: [f32; 2]) {
@@ -100,6 +127,8 @@ impl Game<'_> {
if let Some(pitch) = pitch {
self.pitch = pitch;
}
// A server's player hears the turn alone when its turn ends.
self.rotation_forced = self.lan_acting.is_some();
}
Target::Mob(id) => {
if let Some(m) = self.mobs.iter_mut().find(|m| m.id == id) {