mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
A small player's server box follows minecraft:scale
The server's copy of a player never ran LivingEntity.refreshDirtyAttributes, so after /attribute ... minecraft:scale it kept the old 0.6 x 1.8 box until the pose changed: a small player was sent back from every wall they walked along and suffocated next to one. guest_server_tick now runs aiStep's spin countdown, refreshDirtyAttributes (onAttributeUpdated(SCALE) -> refreshDimensions) and updatePlayerPose in vanilla's order, and in_wall takes the eye box from the body's width (Entity.isInWall, width * 0.8) instead of a fixed 0.48. Part of #3684. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
6daad36de2
commit
5b3b108515
3 files changed
+111
-14
No files matched your search
@@ -20,6 +20,7 @@ use horizon_client::{
|
||||
menu::{Rules, Slots},
|
||||
},
|
||||
motion::{MotionEvent, MotionWorld, PlayerMotion},
|
||||
pose::Dimensions,
|
||||
};
|
||||
use horizon_core::{geometry::Vec3, random::RandomSource};
|
||||
use horizon_protocol::{
|
||||
@@ -2569,7 +2570,7 @@ impl Game<'_> {
|
||||
self.as_guest(host, conn, |game| game.handle_shoulder_entities());
|
||||
return;
|
||||
}
|
||||
let in_wall = !g.sleeping && self.in_wall(g.position, g.eye_height());
|
||||
let in_wall = !g.sleeping && self.in_wall(g.position, g.dimensions());
|
||||
let at = g.position;
|
||||
let (sneaking, sleeping) = (g.sneaking, g.sleeping);
|
||||
let riding = g.vehicle.is_some();
|
||||
@@ -2658,10 +2659,10 @@ impl Game<'_> {
|
||||
let _ = w.server_hazards_tick(in_wall);
|
||||
// LivingEntity.aiStep: a Riptide spin counts down on the server's
|
||||
// copy too, or it would stay in the spin pose (eye 0.4) for good.
|
||||
w.player.count_down_server_spin();
|
||||
// Player.tick ends with updatePlayerPose: a swimmer, or a player
|
||||
// crawling under a block, takes the short box its moves are checked with.
|
||||
let _ = w.update_player_pose();
|
||||
// LivingEntity.tick: a changed scale rebuilds the box. Player.tick
|
||||
// ends with updatePlayerPose: a swimmer, or a player crawling
|
||||
// under a block, takes the short box its moves are checked with.
|
||||
let _ = w.server_player_pose_tick();
|
||||
let mut events = std::mem::take(&mut w.player.events);
|
||||
// LayeredCauldronBlock.handleEntityOnFireInside along those moves.
|
||||
for event in &events {
|
||||
@@ -2884,15 +2885,16 @@ impl Game<'_> {
|
||||
});
|
||||
}
|
||||
|
||||
/// `Player.isInWall`: a suffocating block in the 0.48-wide box at the
|
||||
/// eyes. Suffocating is a full collision box that is also a full render
|
||||
/// cube (`BlockBehaviour.isSuffocating`'s default; glass and the like
|
||||
/// opt out), as the mobs' check has it.
|
||||
fn in_wall(&self, at: [f64; 3], eye_height: f64) -> bool {
|
||||
let (y, half) = ((at[1] + eye_height).floor() as i32, 0.24);
|
||||
let range = |v: f64| (v - half).floor() as i32..=(v + half).floor() as i32;
|
||||
range(at[0]).any(|x| {
|
||||
range(at[2]).any(|z| {
|
||||
/// `Entity.isInWall`: a suffocating block in the box at the eyes as wide
|
||||
/// as 0.8 of the body (0.48 at scale 1, 0.048 at scale 0.1). Suffocating
|
||||
/// is a full collision box that is also a full render cube
|
||||
/// (`BlockBehaviour.isSuffocating`'s default; glass and the like opt
|
||||
/// out), as the mobs' check has it.
|
||||
fn in_wall(&self, at: [f64; 3], body: Dimensions) -> bool {
|
||||
let y = (at[1] + f64::from(body.eye_height)).floor() as i32;
|
||||
let [xs, zs] = eye_columns(at, body);
|
||||
xs.into_iter().any(|x| {
|
||||
zs.clone().any(|z| {
|
||||
let f = self.res.facts().get(self.world.block(x, y, z));
|
||||
f.full_collision && f.solid_render
|
||||
})
|
||||
@@ -3373,3 +3375,37 @@ fn swing_packet(id: i32, hand: Hand, animation: SwingAnimation) -> C {
|
||||
duration: animation.duration,
|
||||
})
|
||||
}
|
||||
|
||||
/// The block columns `Entity.isInWall` looks at: `AABB.ofSize` around the
|
||||
/// eyes, 0.8 of the body's width across (`BlockPos.betweenClosedStream`).
|
||||
fn eye_columns(at: [f64; 3], body: Dimensions) -> [std::ops::RangeInclusive<i32>; 2] {
|
||||
let half = f64::from(body.width * 0.8) / 2.0;
|
||||
let range = |v: f64| (v - half).floor() as i32..=(v + half).floor() as i32;
|
||||
[range(at[0]), range(at[2])]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::eye_columns;
|
||||
use horizon_client::pose::Pose;
|
||||
|
||||
/// #3684: a player shrunk to `minecraft:scale` 0.1 who stands 0.05 from a
|
||||
/// wall has eyes 0.048 wide, which don't reach into it: they don't
|
||||
/// suffocate there, as a full-sized player 0.3 from the wall doesn't.
|
||||
#[test]
|
||||
fn a_small_player_checks_only_the_blocks_at_their_small_eyes() {
|
||||
let at = [205.95, 100.0, 530.5];
|
||||
assert_eq!(
|
||||
eye_columns(at, Pose::Standing.dimensions(0.1)),
|
||||
[205..=205, 530..=530]
|
||||
);
|
||||
assert_eq!(
|
||||
eye_columns(at, Pose::Standing.dimensions(1.0)),
|
||||
[205..=206, 530..=530]
|
||||
);
|
||||
assert_eq!(
|
||||
eye_columns([205.7, 100.0, 530.5], Pose::Standing.dimensions(1.0)),
|
||||
[205..=205, 530..=530]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3585,6 +3585,56 @@ mod tests {
|
||||
w.player.count_down_server_spin();
|
||||
assert_eq!(w.player.auto_spin_attack_ticks, 0);
|
||||
}
|
||||
/// #3684: `/attribute @s minecraft:scale base set 0.1` on the server's
|
||||
/// copy of a player. `LivingEntity.tick`'s `refreshDirtyAttributes` runs
|
||||
/// `onAttributeUpdated(SCALE)`, which calls `refreshDimensions`: the next
|
||||
/// tick the server checks the player's moves with a 0.06 × 0.18 box, not
|
||||
/// the 0.6 × 1.8 one that sent a small player back from every wall.
|
||||
#[test]
|
||||
fn a_server_copy_takes_its_new_scale_box_the_next_tick() {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../.local/horizon-client")
|
||||
.join(horizon_core::VANILLA_VERSION);
|
||||
if !root.join("movement-properties.nbt").exists() {
|
||||
return;
|
||||
}
|
||||
let assets = Assets::load(&root).unwrap();
|
||||
let render = assets.reload(&root).unwrap();
|
||||
let binding = crate::collisions::load(&root, assets.registry).unwrap();
|
||||
let resources = MotionResources::load(&root, &assets).unwrap();
|
||||
let mut scene = World::test_scene(&assets, &render, 2).unwrap();
|
||||
let mut player = PlayerMotion::new(&resources, Vec3::new(20.5, 10.0, 20.5)).unwrap();
|
||||
let mut w = MotionWorld {
|
||||
entities: &[],
|
||||
player: &mut player,
|
||||
scene: &mut scene,
|
||||
assets: &assets,
|
||||
collision_binding: &binding,
|
||||
resources: &resources,
|
||||
};
|
||||
w.server_player_pose_tick().unwrap();
|
||||
assert_eq!(w.player.dimensions.width, 0.6);
|
||||
assert_eq!(w.player.dimensions.height, 1.8);
|
||||
// AttributeCommand's base set, as the server runs it.
|
||||
let key = w.player.attributes.key("minecraft:scale").unwrap();
|
||||
let handle = w
|
||||
.player
|
||||
.attributes
|
||||
.get_instance(Some(key))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
w.player
|
||||
.attributes
|
||||
.edit(&handle, |value, observer| {
|
||||
value.set_base_value(0.1, observer)
|
||||
})
|
||||
.unwrap();
|
||||
w.server_player_pose_tick().unwrap();
|
||||
assert_eq!(w.player.pose, crate::pose::Pose::Standing);
|
||||
assert_eq!(w.player.dimensions.width, 0.6 * 0.1);
|
||||
assert_eq!(w.player.dimensions.height, 1.8 * 0.1);
|
||||
assert_eq!(w.player.dimensions.eye_height, 1.62 * 0.1);
|
||||
}
|
||||
/// #3308: a swimmer who boards a boat stops swimming
|
||||
/// (`Entity.updateSwimming` needs `!isPassenger()`) and `updatePlayerPose`
|
||||
/// sits them up, eye at 1.62 instead of the swim's 0.4.
|
||||
|
||||
@@ -167,6 +167,17 @@ impl MotionWorld<'_, '_, '_> {
|
||||
};
|
||||
self.player.set_pose(pose)
|
||||
}
|
||||
/// The end of a server's copy of a player's tick (`ServerPlayer.doTick`
|
||||
/// runs `Player.tick`): `LivingEntity.aiStep` counts a Riptide spin down,
|
||||
/// `LivingEntity.tick` then runs `refreshDirtyAttributes` (a changed
|
||||
/// `minecraft:scale` rebuilds the box, `onAttributeUpdated`), and
|
||||
/// `Player.tick` ends with `updatePlayerPose`. The server checks the
|
||||
/// player's moves with that box (#3684).
|
||||
pub fn server_player_pose_tick(&mut self) -> Result<()> {
|
||||
self.player.count_down_server_spin();
|
||||
self.player.refresh_attributes()?;
|
||||
self.update_player_pose()
|
||||
}
|
||||
/// The pose part of `Player.tick` for a passenger (`Entity.rideTick` runs
|
||||
/// `tick`, not the travel): `Entity.updateSwimming` stops a swim, since it
|
||||
/// needs `!isPassenger()`, and `updatePlayerPose` follows. A player who
|
||||
|
||||
Reference in new issue
Block a user