From f12589ab809ece7dfd34c58b2ddc67dd5801ccb3 Mon Sep 17 00:00:00 2001 From: mat Date: Thu, 26 Jun 2025 15:24:41 +0930 Subject: [PATCH] fix invalid look directions on teleport --- CHANGELOG.md | 1 + azalea-client/src/test_utils/simulation.rs | 69 +++++++++++++++- .../tests/clamp_look_direction_on_teleport.rs | 81 +++++++++++++++++++ azalea-client/tests/packet_order.rs | 72 +---------------- azalea-entity/src/lib.rs | 4 +- azalea-entity/src/plugin/mod.rs | 8 +- 6 files changed, 157 insertions(+), 78 deletions(-) create mode 100644 azalea-client/tests/clamp_look_direction_on_teleport.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e1c0931..c245a34c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ is breaking anyways, semantic versioning is not followed. ### Fixed - Fix packet order for loading (`PlayerLoaded`/`MovePlayerPos`) and sprinting (`PlayerInput`/`PlayerCommand`). +- Clients no longer send invalid look directions if the server teleports us with one. ## [0.13.0+mc1.21.5] - 2025-06-15 diff --git a/azalea-client/src/test_utils/simulation.rs b/azalea-client/src/test_utils/simulation.rs index e7ac8d5b..d898293c 100644 --- a/azalea-client/src/test_utils/simulation.rs +++ b/azalea-client/src/test_utils/simulation.rs @@ -1,4 +1,4 @@ -use std::{fmt::Debug, sync::Arc}; +use std::{collections::VecDeque, fmt::Debug, sync::Arc}; use azalea_auth::game_profile::GameProfile; use azalea_block::BlockState; @@ -19,7 +19,8 @@ use azalea_protocol::{ config::{ClientboundFinishConfiguration, ClientboundRegistryData}, game::{ ClientboundAddEntity, ClientboundLevelChunkWithLight, ClientboundLogin, - ClientboundRespawn, c_level_chunk_with_light::ClientboundLevelChunkPacketData, + ClientboundRespawn, ServerboundGamePacket, + c_level_chunk_with_light::ClientboundLevelChunkPacketData, c_light_update::ClientboundLightUpdatePacketData, }, }, @@ -28,13 +29,13 @@ use azalea_registry::{Biome, DimensionType, EntityKind}; use azalea_world::{Chunk, Instance, MinecraftEntityId, Section, palette::PalettedContainer}; use bevy_app::App; use bevy_ecs::{component::Mutable, prelude::*, schedule::ExecutorKind}; -use parking_lot::RwLock; +use parking_lot::{Mutex, RwLock}; use simdnbt::owned::{NbtCompound, NbtTag}; use uuid::Uuid; use crate::{ InConfigState, LocalPlayerBundle, connection::RawConnection, disconnect::DisconnectEvent, - local_player::InstanceHolder, player::GameProfileComponent, + local_player::InstanceHolder, packet::game::SendPacketEvent, player::GameProfileComponent, }; /// A way to simulate a client in a server, used for some internal tests. @@ -170,6 +171,66 @@ impl Simulation { } } +#[derive(Clone)] +pub struct SentPackets { + pub list: Arc>>, +} +impl SentPackets { + pub fn new(simulation: &mut Simulation) -> Self { + let sent_packets = SentPackets { + list: Default::default(), + }; + + let simulation_entity = simulation.entity; + let sent_packets_clone = sent_packets.clone(); + simulation + .app + .add_observer(move |trigger: Trigger| { + if trigger.sent_by == simulation_entity { + sent_packets_clone + .list + .lock() + .push_back(trigger.event().packet.clone()) + } + }); + + sent_packets + } + + pub fn clear(&self) { + self.list.lock().clear(); + } + + pub fn expect_tick_end(&self) { + self.expect("TickEnd", |p| { + matches!(p, ServerboundGamePacket::ClientTickEnd(_)) + }); + } + pub fn expect_empty(&self) { + let sent_packet = self.next(); + if sent_packet.is_some() { + panic!("Expected no packet, got {sent_packet:?}"); + } + } + pub fn expect( + &self, + expected_formatted: &str, + check: impl FnOnce(&ServerboundGamePacket) -> bool, + ) { + let sent_packet = self.next(); + if let Some(sent_packet) = sent_packet { + if !check(&sent_packet) { + panic!("Expected {expected_formatted}, got {sent_packet:?}"); + } + } else { + panic!("Expected {expected_formatted}, got nothing"); + } + } + pub fn next(&self) -> Option { + self.list.lock().pop_front() + } +} + #[allow(clippy::type_complexity)] fn create_local_player_bundle( entity: Entity, diff --git a/azalea-client/tests/clamp_look_direction_on_teleport.rs b/azalea-client/tests/clamp_look_direction_on_teleport.rs new file mode 100644 index 00000000..10b3215b --- /dev/null +++ b/azalea-client/tests/clamp_look_direction_on_teleport.rs @@ -0,0 +1,81 @@ +use azalea_client::test_utils::prelude::*; +use azalea_core::{ + position::{BlockPos, ChunkPos, Vec3}, + resource_location::ResourceLocation, +}; +use azalea_entity::LookDirection; +use azalea_protocol::{ + common::movements::{PositionMoveRotation, RelativeMovements}, + packets::{ + ConnectionProtocol, + config::{ClientboundFinishConfiguration, ClientboundRegistryData}, + game::{ + ClientboundBlockUpdate, ClientboundPlayerPosition, ServerboundAcceptTeleportation, + ServerboundGamePacket, + }, + }, +}; +use azalea_registry::{Block, DataRegistry, DimensionType}; +use simdnbt::owned::{NbtCompound, NbtTag}; + +#[test] +fn test_clamp_look_direction_on_teleport() { + init_tracing(); + + let mut simulation = Simulation::new(ConnectionProtocol::Configuration); + let sent_packets = SentPackets::new(&mut simulation); + + simulation.receive_packet(ClientboundRegistryData { + registry_id: ResourceLocation::new("minecraft:dimension_type"), + entries: vec![( + ResourceLocation::new("minecraft:overworld"), + Some(NbtCompound::from_values(vec![ + ("height".into(), NbtTag::Int(384)), + ("min_y".into(), NbtTag::Int(-64)), + ])), + )] + .into_iter() + .collect(), + }); + simulation.tick(); + simulation.receive_packet(ClientboundFinishConfiguration); + simulation.tick(); + + simulation.receive_packet(make_basic_login_packet( + DimensionType::new_raw(0), // overworld + ResourceLocation::new("minecraft:overworld"), + )); + simulation.tick(); + + sent_packets.expect_tick_end(); + sent_packets.expect_empty(); + + // receive a chunk so the player is "loaded" now + simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), (384 + 64) / 16)); + simulation.receive_packet(ClientboundBlockUpdate { + pos: BlockPos::new(1, 1, 3), + block_state: Block::Stone.into(), + }); + simulation.receive_packet(ClientboundPlayerPosition { + id: 1, + change: PositionMoveRotation { + pos: Vec3::ZERO, + delta: Vec3::ZERO, + look_direction: LookDirection::new(-134.99998, 0.0), + }, + relative: RelativeMovements::all_absolute(), + }); + simulation.tick(); + sent_packets.expect("AcceptTeleportation", |p| { + matches!( + p, + ServerboundGamePacket::AcceptTeleportation(ServerboundAcceptTeleportation { id: 1 }) + ) + }); + sent_packets.expect("MovePlayerPosRot", |p| { + let ServerboundGamePacket::MovePlayerPosRot(p) = p else { + return false; + }; + p.look_direction == LookDirection::new(225.00002, 0.) + }); +} diff --git a/azalea-client/tests/packet_order.rs b/azalea-client/tests/packet_order.rs index d4dd2558..014d9f46 100644 --- a/azalea-client/tests/packet_order.rs +++ b/azalea-client/tests/packet_order.rs @@ -1,8 +1,4 @@ -use std::{collections::VecDeque, sync::Arc}; - -use azalea_client::{ - SprintDirection, StartSprintEvent, packet::game::SendPacketEvent, test_utils::prelude::*, -}; +use azalea_client::{SprintDirection, StartSprintEvent, test_utils::prelude::*}; use azalea_core::{ position::{BlockPos, ChunkPos, Vec3}, resource_location::ResourceLocation, @@ -21,8 +17,6 @@ use azalea_protocol::{ }, }; use azalea_registry::{Block, DataRegistry, DimensionType}; -use bevy_ecs::observer::Trigger; -use parking_lot::Mutex; use simdnbt::owned::{NbtCompound, NbtTag}; #[test] @@ -73,13 +67,10 @@ fn test_packet_order() { relative: RelativeMovements::all_absolute(), }); simulation.tick(); - assert_eq!( simulation.get_block_state(BlockPos::new(1, 1, 3)), Some(Block::Stone.into()) ); - - println!("sent_packets: {:?}", sent_packets.list.lock()); sent_packets.expect("AcceptTeleportation", |p| { matches!( p, @@ -160,64 +151,3 @@ fn test_packet_order() { sent_packets.expect_tick_end(); sent_packets.expect_empty(); } - -#[derive(Clone)] -pub struct SentPackets { - list: Arc>>, -} -impl SentPackets { - pub fn new(simulation: &mut Simulation) -> Self { - let sent_packets = SentPackets { - list: Default::default(), - }; - - let simulation_entity = simulation.entity; - let sent_packets_clone = sent_packets.clone(); - simulation - .app - .add_observer(move |trigger: Trigger| { - if trigger.sent_by == simulation_entity { - sent_packets_clone - .list - .lock() - .push_back(trigger.event().packet.clone()) - } - }); - - sent_packets - } - - pub fn clear(&self) { - self.list.lock().clear(); - } - - pub fn expect_tick_end(&self) { - self.expect("TickEnd", |p| { - matches!(p, ServerboundGamePacket::ClientTickEnd(_)) - }); - } - pub fn expect_empty(&self) { - let sent_packet = self.next(); - if let None = sent_packet { - } else { - panic!("Expected no packet, got {sent_packet:?}"); - } - } - pub fn expect( - &self, - expected_formatted: &str, - check: impl FnOnce(&ServerboundGamePacket) -> bool, - ) { - let sent_packet = self.next(); - if let Some(sent_packet) = sent_packet { - if !check(&sent_packet) { - panic!("Expected {expected_formatted}, got {sent_packet:?}"); - } - } else { - panic!("Expected {expected_formatted}, got nothing"); - } - } - pub fn next(&self) -> Option { - self.list.lock().pop_front() - } -} diff --git a/azalea-entity/src/lib.rs b/azalea-entity/src/lib.rs index b8644546..340b6f25 100644 --- a/azalea-entity/src/lib.rs +++ b/azalea-entity/src/lib.rs @@ -226,8 +226,10 @@ pub struct LookDirection { } impl LookDirection { + /// Create a new look direction, while clamping and wrapping the rotations + /// to the allowed values. pub fn new(y_rot: f32, x_rot: f32) -> Self { - Self { y_rot, x_rot } + apply_clamp_look_direction(Self { y_rot, x_rot }) } } diff --git a/azalea-entity/src/plugin/mod.rs b/azalea-entity/src/plugin/mod.rs index 65b28a59..88e87de1 100644 --- a/azalea-entity/src/plugin/mod.rs +++ b/azalea-entity/src/plugin/mod.rs @@ -187,10 +187,14 @@ pub struct LoadedBy(pub HashSet); pub fn clamp_look_direction(mut query: Query<&mut LookDirection>) { for mut look_direction in &mut query { - look_direction.y_rot = look_direction.y_rot.rem_euclid(360.0); - look_direction.x_rot = look_direction.x_rot.clamp(-90.0, 90.0) % 360.0; + *look_direction = apply_clamp_look_direction(*look_direction); } } +pub fn apply_clamp_look_direction(mut look_direction: LookDirection) -> LookDirection { + look_direction.y_rot = look_direction.y_rot.rem_euclid(360.0); + look_direction.x_rot = look_direction.x_rot.clamp(-90.0, 90.0) % 360.0; + look_direction +} /// Sets the position of the entity. This doesn't update the cache in /// azalea-world, and should only be used within azalea-world!