aboutsummaryrefslogtreecommitdiff
path: root/azalea-client
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2025-08-14 20:40:13 -0500
committerGitHub <noreply@github.com>2025-08-14 20:40:13 -0500
commite74ed047dbaf3877db4a89a2d589e992abd0bb11 (patch)
tree0a728c8be167a1d59a5492ed9df666f41cf12e57 /azalea-client
parent6695132ddb31780786c67b8b9ff5df8ab3891438 (diff)
downloadazalea-drasl-e74ed047dbaf3877db4a89a2d589e992abd0bb11.tar.xz
Sneaking (#237)
* start implementing sneaking * fix horizontal_collision being inverted and cleanup * clippy * change dimensions and eye height based on pose * proper support for automatically crouching in certain cases * fix anticheat issues * add line to changelog and update a comment
Diffstat (limited to 'azalea-client')
-rw-r--r--azalea-client/src/client.rs21
-rw-r--r--azalea-client/src/lib.rs5
-rw-r--r--azalea-client/src/local_player.rs38
-rw-r--r--azalea-client/src/plugins/attack.rs10
-rw-r--r--azalea-client/src/plugins/interact/mod.rs19
-rw-r--r--azalea-client/src/plugins/interact/pick.rs9
-rw-r--r--azalea-client/src/plugins/inventory.rs5
-rw-r--r--azalea-client/src/plugins/mining.rs5
-rw-r--r--azalea-client/src/plugins/movement.rs322
-rw-r--r--azalea-client/src/plugins/packet/game/mod.rs4
-rw-r--r--azalea-client/tests/correct_sneak_movement.rs135
-rw-r--r--azalea-client/tests/correct_sprint_sneak_movement.rs153
12 files changed, 587 insertions, 139 deletions
diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs
index 9481ba2d..be9c8e99 100644
--- a/azalea-client/src/client.rs
+++ b/azalea-client/src/client.rs
@@ -14,10 +14,12 @@ use azalea_core::{
tick::GameTick,
};
use azalea_entity::{
- EntityUpdateSet, EyeHeight, Position,
+ EntityUpdateSet, PlayerAbilities, Position,
+ dimensions::EntityDimensions,
indexing::{EntityIdIndex, EntityUuidIndex},
metadata::Health,
};
+use azalea_physics::local_player::PhysicsState;
use azalea_protocol::{
ServerAddress,
common::client_information::ClientInformation,
@@ -55,9 +57,9 @@ use crate::{
interact::BlockStatePredictionHandler,
inventory::Inventory,
join::{ConnectOpts, StartJoinServerEvent},
- local_player::{Hunger, InstanceHolder, PermissionLevel, PlayerAbilities, TabList},
+ local_player::{Hunger, InstanceHolder, PermissionLevel, TabList},
mining::{self},
- movement::{LastSentLookDirection, PhysicsState},
+ movement::LastSentLookDirection,
packet::game::SendPacketEvent,
player::{GameProfileComponent, PlayerInfo, retroactively_add_game_profile_component},
};
@@ -427,12 +429,21 @@ impl Client {
)
}
+ /// Get the bounding box dimensions for our client, which contains our
+ /// width, height, and eye height.
+ ///
+ /// This is a shortcut for
+ /// `self.component::<EntityDimensions>()`.
+ pub fn dimensions(&self) -> EntityDimensions {
+ self.component::<EntityDimensions>()
+ }
+
/// Get the position of this client's eyes.
///
/// This is a shortcut for
- /// `bot.position().up(bot.component::<EyeHeight>())`.
+ /// `bot.position().up(bot.dimensions().eye_height)`.
pub fn eye_position(&self) -> Vec3 {
- self.position().up((*self.component::<EyeHeight>()) as f64)
+ self.position().up(self.dimensions().eye_height as f64)
}
/// Get the health of this client.
diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs
index 19e13253..c818d96a 100644
--- a/azalea-client/src/lib.rs
+++ b/azalea-client/src/lib.rs
@@ -21,6 +21,7 @@ mod plugins;
pub mod test_utils;
pub use account::{Account, AccountOpts};
+pub use azalea_physics::local_player::{PhysicsState, SprintDirection, WalkDirection};
pub use azalea_protocol::common::client_information::ClientInformation;
// Re-export bevy-tasks so plugins can make sure that they're using the same
// version.
@@ -30,7 +31,5 @@ pub use client::{
StartClientOpts, start_ecs_runner,
};
pub use events::Event;
-pub use movement::{
- PhysicsState, SprintDirection, StartSprintEvent, StartWalkEvent, WalkDirection,
-};
+pub use movement::{StartSprintEvent, StartWalkEvent};
pub use plugins::*;
diff --git a/azalea-client/src/local_player.rs b/azalea-client/src/local_player.rs
index 4a937ec7..09232d12 100644
--- a/azalea-client/src/local_player.rs
+++ b/azalea-client/src/local_player.rs
@@ -5,7 +5,6 @@ use std::{
};
use azalea_core::game_type::GameMode;
-use azalea_protocol::packets::game::c_player_abilities::ClientboundPlayerAbilities;
use azalea_world::{Instance, PartialInstance};
use bevy_ecs::{component::Component, prelude::*};
use derive_more::{Deref, DerefMut};
@@ -55,34 +54,6 @@ impl From<GameMode> for LocalGameMode {
}
}
-/// A component that contains the abilities the player has, like flying
-/// or instantly breaking blocks. This is only present on local players.
-#[derive(Clone, Debug, Component, Default)]
-pub struct PlayerAbilities {
- pub invulnerable: bool,
- pub flying: bool,
- pub can_fly: bool,
- /// Whether the player can instantly break blocks and can duplicate blocks
- /// in their inventory.
- pub instant_break: bool,
-
- pub flying_speed: f32,
- /// Used for the fov
- pub walking_speed: f32,
-}
-impl From<&ClientboundPlayerAbilities> for PlayerAbilities {
- fn from(packet: &ClientboundPlayerAbilities) -> Self {
- Self {
- invulnerable: packet.flags.invulnerable,
- flying: packet.flags.flying,
- can_fly: packet.flags.can_fly,
- instant_break: packet.flags.instant_break,
- flying_speed: packet.flying_speed,
- walking_speed: packet.walking_speed,
- }
- }
-}
-
/// Level must be 0..=4
#[derive(Component, Clone, Default, Deref, DerefMut)]
pub struct PermissionLevel(pub u8);
@@ -127,6 +98,15 @@ impl Default for Hunger {
}
}
}
+impl Hunger {
+ /// Returns true if we have enough food level to sprint.
+ ///
+ /// Note that this doesn't consider our gamemode or passenger status.
+ pub fn is_enough_to_sprint(&self) -> bool {
+ // hasEnoughFoodToSprint
+ self.food >= 6
+ }
+}
impl InstanceHolder {
/// Create a new `InstanceHolder` for the given entity.
diff --git a/azalea-client/src/plugins/attack.rs b/azalea-client/src/plugins/attack.rs
index ec4337e5..7d730bb7 100644
--- a/azalea-client/src/plugins/attack.rs
+++ b/azalea-client/src/plugins/attack.rs
@@ -1,8 +1,6 @@
use azalea_core::{game_type::GameMode, tick::GameTick};
use azalea_entity::{
- Attributes, Physics,
- indexing::EntityIdIndex,
- metadata::{ShiftKeyDown, Sprinting},
+ Attributes, Crouching, Physics, indexing::EntityIdIndex, metadata::Sprinting,
update_bounding_box,
};
use azalea_physics::PhysicsSet;
@@ -100,7 +98,7 @@ pub fn handle_attack_queued(
&mut Sprinting,
&AttackQueued,
&LocalGameMode,
- &ShiftKeyDown,
+ &Crouching,
&EntityIdIndex,
)>,
) {
@@ -111,7 +109,7 @@ pub fn handle_attack_queued(
mut sprinting,
attack_queued,
game_mode,
- sneaking,
+ crouching,
entity_id_index,
) in &mut query
{
@@ -128,7 +126,7 @@ pub fn handle_attack_queued(
ServerboundInteract {
entity_id: target_entity_id,
action: s_interact::ActionType::Attack,
- using_secondary_action: **sneaking,
+ using_secondary_action: **crouching,
},
));
commands.trigger(SwingArmEvent {
diff --git a/azalea-client/src/plugins/interact/mod.rs b/azalea-client/src/plugins/interact/mod.rs
index 634d492c..0275ca97 100644
--- a/azalea-client/src/plugins/interact/mod.rs
+++ b/azalea-client/src/plugins/interact/mod.rs
@@ -11,7 +11,7 @@ use azalea_core::{
tick::GameTick,
};
use azalea_entity::{
- Attributes, LocalEntity, LookDirection,
+ Attributes, Crouching, LocalEntity, LookDirection, PlayerAbilities,
attributes::{
creative_block_interaction_range_modifier, creative_entity_interaction_range_modifier,
},
@@ -36,7 +36,7 @@ use crate::{
attack::handle_attack_event,
interact::pick::{HitResultComponent, update_hit_result_component},
inventory::{Inventory, InventorySet},
- local_player::{LocalGameMode, PermissionLevel, PlayerAbilities},
+ local_player::{LocalGameMode, PermissionLevel},
movement::MoveEventsSet,
packet::game::SendPacketEvent,
respawn::perform_respawn,
@@ -250,12 +250,20 @@ pub fn handle_start_use_item_queued(
&mut BlockStatePredictionHandler,
&HitResultComponent,
&LookDirection,
+ &Crouching,
Option<&Mining>,
)>,
entity_id_query: Query<&MinecraftEntityId>,
) {
- for (entity, start_use_item, mut prediction_handler, hit_result, look_direction, mining) in
- query
+ for (
+ entity,
+ start_use_item,
+ mut prediction_handler,
+ hit_result,
+ look_direction,
+ crouching,
+ mining,
+ ) in query
{
commands.entity(entity).remove::<StartUseItemQueued>();
@@ -332,8 +340,7 @@ pub fn handle_start_use_item_queued(
location: r.location,
hand: InteractionHand::MainHand,
},
- // TODO: sneaking
- using_secondary_action: false,
+ using_secondary_action: **crouching,
},
));
}
diff --git a/azalea-client/src/plugins/interact/pick.rs b/azalea-client/src/plugins/interact/pick.rs
index cebbf905..a0a75910 100644
--- a/azalea-client/src/plugins/interact/pick.rs
+++ b/azalea-client/src/plugins/interact/pick.rs
@@ -5,7 +5,8 @@ use azalea_core::{
position::Vec3,
};
use azalea_entity::{
- Attributes, Dead, EyeHeight, LocalEntity, LookDirection, Physics, Position,
+ Attributes, Dead, LocalEntity, LookDirection, Physics, Position,
+ dimensions::EntityDimensions,
metadata::{ArmorStandMarker, Marker},
view_vector,
};
@@ -31,7 +32,7 @@ pub fn update_hit_result_component(
Entity,
Option<&mut HitResultComponent>,
&Position,
- &EyeHeight,
+ &EntityDimensions,
&LookDirection,
&InstanceName,
&Physics,
@@ -47,7 +48,7 @@ pub fn update_hit_result_component(
entity,
hit_result_ref,
position,
- eye_height,
+ dimensions,
look_direction,
world_name,
physics,
@@ -57,7 +58,7 @@ pub fn update_hit_result_component(
let block_pick_range = attributes.block_interaction_range.calculate();
let entity_pick_range = attributes.entity_interaction_range.calculate();
- let eye_position = position.up(eye_height.into());
+ let eye_position = position.up(dimensions.eye_height.into());
let Some(world_lock) = instance_container.get(world_name) else {
continue;
diff --git a/azalea-client/src/plugins/inventory.rs b/azalea-client/src/plugins/inventory.rs
index 732e1154..ecc8e826 100644
--- a/azalea-client/src/plugins/inventory.rs
+++ b/azalea-client/src/plugins/inventory.rs
@@ -4,6 +4,7 @@ use std::{
};
use azalea_chat::FormattedText;
+use azalea_entity::PlayerAbilities;
pub use azalea_inventory::*;
use azalea_inventory::{
item::MaxStackSizeExt,
@@ -23,9 +24,7 @@ use bevy_app::{App, Plugin, Update};
use bevy_ecs::prelude::*;
use tracing::{error, warn};
-use crate::{
- Client, local_player::PlayerAbilities, packet::game::SendPacketEvent, respawn::perform_respawn,
-};
+use crate::{Client, packet::game::SendPacketEvent, respawn::perform_respawn};
pub struct InventoryPlugin;
impl Plugin for InventoryPlugin {
diff --git a/azalea-client/src/plugins/mining.rs b/azalea-client/src/plugins/mining.rs
index f584bacb..1b7adadc 100644
--- a/azalea-client/src/plugins/mining.rs
+++ b/azalea-client/src/plugins/mining.rs
@@ -1,6 +1,6 @@
use azalea_block::{BlockState, BlockTrait, fluid_state::FluidState};
use azalea_core::{direction::Direction, game_type::GameMode, position::BlockPos, tick::GameTick};
-use azalea_entity::{FluidOnEyes, Physics, Position, mining::get_mine_progress};
+use azalea_entity::{FluidOnEyes, Physics, PlayerAbilities, Position, mining::get_mine_progress};
use azalea_inventory::ItemStack;
use azalea_physics::{PhysicsSet, collision::BlockWithShape};
use azalea_protocol::packets::game::s_player_action::{self, ServerboundPlayerAction};
@@ -17,7 +17,7 @@ use crate::{
check_is_interaction_restricted, pick::HitResultComponent,
},
inventory::{Inventory, InventorySet},
- local_player::{InstanceHolder, LocalGameMode, PermissionLevel, PlayerAbilities},
+ local_player::{InstanceHolder, LocalGameMode, PermissionLevel},
movement::MoveEventsSet,
packet::game::SendPacketEvent,
};
@@ -55,7 +55,6 @@ impl Plugin for MiningPlugin {
.in_set(MiningSet)
.after(InventorySet)
.after(MoveEventsSet)
- .before(azalea_entity::update_bounding_box)
.after(azalea_entity::update_fluid_on_eyes)
.after(crate::interact::pick::update_hit_result_component)
.after(crate::attack::handle_attack_event)
diff --git a/azalea-client/src/plugins/movement.rs b/azalea-client/src/plugins/movement.rs
index c9b3b070..ad6779fa 100644
--- a/azalea-client/src/plugins/movement.rs
+++ b/azalea-client/src/plugins/movement.rs
@@ -1,14 +1,23 @@
use std::{backtrace::Backtrace, io};
use azalea_core::{
+ game_type::GameMode,
position::{Vec2, Vec3},
tick::GameTick,
};
use azalea_entity::{
- Attributes, HasClientLoaded, Jumping, LastSentPosition, LookDirection, Physics, Position,
- metadata::Sprinting,
+ Attributes, Crouching, HasClientLoaded, Jumping, LastSentPosition, LocalEntity, LookDirection,
+ Physics, PlayerAbilities, Pose, Position,
+ dimensions::calculate_dimensions,
+ metadata::{self, Sprinting},
+ update_bounding_box,
+};
+use azalea_physics::{
+ PhysicsSet, ai_step,
+ collision::entity_collisions::{CollidableEntityQuery, PhysicsQuery},
+ local_player::{PhysicsState, SprintDirection, WalkDirection},
+ travel::{no_collision, travel},
};
-use azalea_physics::{PhysicsSet, ai_step};
use azalea_protocol::{
common::movements::MoveFlags,
packets::{
@@ -22,12 +31,17 @@ use azalea_protocol::{
},
},
};
-use azalea_world::{MinecraftEntityId, MoveEntityError};
+use azalea_registry::EntityKind;
+use azalea_world::{Instance, MinecraftEntityId, MoveEntityError};
use bevy_app::{App, Plugin, Update};
use bevy_ecs::prelude::*;
use thiserror::Error;
-use crate::{client::Client, packet::game::SendPacketEvent};
+use crate::{
+ client::Client,
+ local_player::{Hunger, InstanceHolder, LocalGameMode},
+ packet::game::SendPacketEvent,
+};
#[derive(Error, Debug)]
pub enum MovePlayerError {
@@ -58,18 +72,21 @@ impl Plugin for MovementPlugin {
Update,
(handle_sprint, handle_walk, handle_knockback)
.chain()
- .in_set(MoveEventsSet),
+ .in_set(MoveEventsSet)
+ .after(update_bounding_box),
)
.add_systems(
GameTick,
(
- (tick_controls, local_player_ai_step)
+ (tick_controls, local_player_ai_step, update_pose)
.chain()
.in_set(PhysicsSet)
.before(ai_step)
.before(azalea_physics::fluids::update_in_water_state_and_do_fluid_pushing),
send_player_input_packet,
- send_sprinting_if_needed.after(azalea_entity::update_in_loaded_chunk),
+ send_sprinting_if_needed
+ .after(azalea_entity::update_in_loaded_chunk)
+ .after(travel),
send_position.after(PhysicsSet),
)
.chain(),
@@ -97,6 +114,21 @@ impl Client {
*self.component::<Jumping>()
}
+ pub fn set_crouching(&self, crouching: bool) {
+ let mut ecs = self.ecs.lock();
+ let mut physics_state = self.query::<&mut PhysicsState>(&mut ecs);
+ physics_state.trying_to_crouch = crouching;
+ }
+
+ /// Whether the client is currently trying to sneak.
+ ///
+ /// You may want to check the [`Pose`] instead.
+ pub fn crouching(&self) -> bool {
+ let mut ecs = self.ecs.lock();
+ let physics_state = self.query::<&PhysicsState>(&mut ecs);
+ physics_state.trying_to_crouch
+ }
+
/// Sets the direction the client is looking. `y_rot` is yaw (looking to the
/// side), `x_rot` is pitch (looking up and down). You can get these
/// numbers from the vanilla f3 screen.
@@ -125,24 +157,6 @@ pub struct LastSentLookDirection {
pub y_rot: f32,
}
-/// Component for entities that can move and sprint. Usually only in
-/// [`LocalEntity`]s.
-///
-/// [`LocalEntity`]: azalea_entity::LocalEntity
-#[derive(Default, Component, Clone)]
-pub struct PhysicsState {
- /// Minecraft only sends a movement packet either after 20 ticks or if the
- /// player moved enough. This is that tick counter.
- pub position_remainder: u32,
- pub was_sprinting: bool,
- // Whether we're going to try to start sprinting this tick. Equivalent to
- // holding down ctrl for a tick.
- pub trying_to_sprint: bool,
-
- pub move_direction: WalkDirection,
- pub move_vector: Vec2,
-}
-
#[allow(clippy::type_complexity)]
pub fn send_position(
mut query: Query<
@@ -266,8 +280,7 @@ pub fn send_player_input_packet(
left: matches!(dir, D::Left | D::ForwardLeft | D::BackwardLeft),
right: matches!(dir, D::Right | D::ForwardRight | D::BackwardRight),
jump: **jumping,
- // TODO: implement sneaking
- shift: false,
+ shift: physics_state.trying_to_crouch,
sprint: physics_state.trying_to_sprint,
};
@@ -345,46 +358,144 @@ pub(crate) fn tick_controls(mut query: Query<&mut PhysicsState>) {
/// Makes the bot do one physics tick. Note that this is already handled
/// automatically by the client.
+#[allow(clippy::type_complexity)]
pub fn local_player_ai_step(
mut query: Query<
- (&PhysicsState, &mut Physics, &mut Sprinting, &mut Attributes),
- With<HasClientLoaded>,
+ (
+ Entity,
+ &PhysicsState,
+ &PlayerAbilities,
+ &metadata::Swimming,
+ &metadata::SleepingPos,
+ &InstanceHolder,
+ &Position,
+ Option<&Hunger>,
+ Option<&LastSentInput>,
+ &mut Physics,
+ &mut Sprinting,
+ &mut Crouching,
+ &mut Attributes,
+ ),
+ (With<HasClientLoaded>, With<LocalEntity>),
>,
+ physics_query: PhysicsQuery,
+ collidable_entity_query: CollidableEntityQuery,
) {
- for (physics_state, mut physics, mut sprinting, mut attributes) in query.iter_mut() {
+ for (
+ entity,
+ physics_state,
+ abilities,
+ swimming,
+ sleeping_pos,
+ instance_holder,
+ position,
+ hunger,
+ last_sent_input,
+ mut physics,
+ mut sprinting,
+ mut crouching,
+ mut attributes,
+ ) in query.iter_mut()
+ {
// server ai step
- // TODO: replace those booleans when using items, passengers, and sneaking are
- // properly implemented
- let move_vector = modify_input(physics_state.move_vector, false, false, false, &attributes);
- physics.x_acceleration = move_vector.x;
- physics.z_acceleration = move_vector.y;
+ let is_swimming = **swimming;
+ // TODO: implement passengers
+ let is_passenger = false;
+ let is_sleeping = sleeping_pos.is_some();
+
+ let world = instance_holder.instance.read();
+ let ctx = CanPlayerFitCtx {
+ world: &world,
+ entity,
+ position: *position,
+ physics_query: &physics_query,
+ collidable_entity_query: &collidable_entity_query,
+ physics: &physics,
+ };
+
+ let new_crouching = !abilities.flying
+ && !is_swimming
+ && !is_passenger
+ && can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Crouching)
+ && (last_sent_input.is_some_and(|i| i.0.shift)
+ || !is_sleeping
+ && !can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Standing));
+ if **crouching != new_crouching {
+ **crouching = new_crouching;
+ }
// TODO: food data and abilities
// let has_enough_food_to_sprint = self.food_data().food_level ||
// self.abilities().may_fly;
- let has_enough_food_to_sprint = true;
+ let has_enough_food_to_sprint = hunger.is_none_or(Hunger::is_enough_to_sprint);
// TODO: double tapping w to sprint i think
let trying_to_sprint = physics_state.trying_to_sprint;
- if !**sprinting
- && (
- // !self.is_in_water()
- // || self.is_underwater() &&
- has_enough_impulse_to_start_sprinting(physics_state)
- && has_enough_food_to_sprint
- // && !self.using_item()
- // && !self.has_effect(MobEffects.BLINDNESS)
- && trying_to_sprint
- )
- {
+ // TODO: swimming
+ let is_underwater = false;
+ let is_in_water = physics.is_in_water();
+ // TODO: elytra
+ let is_fall_flying = false;
+ // TODO: passenger
+ let is_passenger = false;
+ // TODO: using items
+ let using_item = false;
+ // TODO: status effects
+ let has_blindness = false;
+
+ let has_enough_impulse = has_enough_impulse_to_start_sprinting(physics_state);
+
+ // LocalPlayer.canStartSprinting
+ let can_start_sprinting = !**sprinting
+ && has_enough_impulse
+ && has_enough_food_to_sprint
+ && !using_item
+ && !has_blindness
+ && (!is_passenger || is_underwater)
+ && (!is_fall_flying || is_underwater)
+ && (!is_moving_slowly(&crouching) || is_underwater)
+ && (!is_in_water || is_underwater);
+ if trying_to_sprint && can_start_sprinting {
set_sprinting(true, &mut sprinting, &mut attributes);
}
+
+ if **sprinting {
+ // TODO: swimming
+
+ let vehicle_can_sprint = false;
+ // shouldStopRunSprinting
+ let should_stop_sprinting = has_blindness
+ || (is_passenger && !vehicle_can_sprint)
+ || !has_enough_impulse
+ || !has_enough_food_to_sprint
+ || (physics.horizontal_collision && !physics.minor_horizontal_collision)
+ || (is_in_water && !is_underwater);
+ if should_stop_sprinting {
+ set_sprinting(false, &mut sprinting, &mut attributes);
+ }
+ }
+
+ // TODO: replace those booleans when using items and passengers are properly
+ // implemented
+ let move_vector = modify_input(
+ physics_state.move_vector,
+ false,
+ false,
+ **crouching,
+ &attributes,
+ );
+ physics.x_acceleration = move_vector.x;
+ physics.z_acceleration = move_vector.y;
}
}
+fn is_moving_slowly(crouching: &Crouching) -> bool {
+ **crouching
+}
+
// LocalPlayer.modifyInput
fn modify_input(
mut move_vector: Vec2,
@@ -523,8 +634,12 @@ pub fn handle_sprint(
}
/// Change whether we're sprinting by adding an attribute modifier to the
-/// player. You should use the [`walk`] and [`sprint`] methods instead.
-/// Returns if the operation was successful.
+/// player.
+///
+/// You should use the [`Client::walk`] and [`Client::sprint`] functions
+/// instead.
+///
+/// Returns true if the operation was successful.
fn set_sprinting(
sprinting: bool,
currently_sprinting: &mut Sprinting,
@@ -533,12 +648,12 @@ fn set_sprinting(
**currently_sprinting = sprinting;
if sprinting {
attributes
- .speed
+ .movement_speed
.try_insert(azalea_entity::attributes::sprinting_modifier())
.is_ok()
} else {
attributes
- .speed
+ .movement_speed
.remove(&azalea_entity::attributes::sprinting_modifier().id)
.is_none()
}
@@ -583,34 +698,85 @@ pub fn handle_knockback(mut query: Query<&mut Physics>, mut events: EventReader<
}
}
-#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
-pub enum WalkDirection {
- #[default]
- None,
- Forward,
- Backward,
- Left,
- Right,
- ForwardRight,
- ForwardLeft,
- BackwardRight,
- BackwardLeft,
-}
+pub fn update_pose(
+ mut query: Query<(
+ Entity,
+ &mut Pose,
+ &Physics,
+ &PhysicsState,
+ &LocalGameMode,
+ &InstanceHolder,
+ &Position,
+ )>,
+ physics_query: PhysicsQuery,
+ collidable_entity_query: CollidableEntityQuery,
+) {
+ for (entity, mut pose, physics, physics_state, game_mode, instance_holder, position) in
+ query.iter_mut()
+ {
+ let world = instance_holder.instance.read();
+ let world = &*world;
+ let ctx = CanPlayerFitCtx {
+ world,
+ entity,
+ position: *position,
+ physics_query: &physics_query,
+ collidable_entity_query: &collidable_entity_query,
+ physics,
+ };
-/// The directions that we can sprint in. It's a subset of [`WalkDirection`].
-#[derive(Clone, Copy, Debug)]
-pub enum SprintDirection {
- Forward,
- ForwardRight,
- ForwardLeft,
-}
+ if !can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Swimming) {
+ continue;
+ }
+
+ // TODO: implement everything else from getDesiredPose: sleeping, swimming,
+ // fallFlying, spinAttack
+ let desired_pose = if physics_state.trying_to_crouch {
+ Pose::Crouching
+ } else {
+ Pose::Standing
+ };
-impl From<SprintDirection> for WalkDirection {
- fn from(d: SprintDirection) -> Self {
- match d {
- SprintDirection::Forward => WalkDirection::Forward,
- SprintDirection::ForwardRight => WalkDirection::ForwardRight,
- SprintDirection::ForwardLeft => WalkDirection::ForwardLeft,
+ // TODO: passengers
+ let is_passenger = false;
+
+ // canPlayerFitWithinBlocksAndEntitiesWhen
+ let new_pose = if game_mode.current == GameMode::Spectator
+ || is_passenger
+ || can_player_fit_within_blocks_and_entities_when(&ctx, desired_pose)
+ {
+ desired_pose
+ } else if can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Crouching) {
+ Pose::Crouching
+ } else {
+ Pose::Swimming
+ };
+
+ // avoid triggering change detection
+ if new_pose != *pose {
+ *pose = new_pose;
}
}
}
+
+struct CanPlayerFitCtx<'world, 'state, 'a, 'b> {
+ world: &'a Instance,
+ entity: Entity,
+ position: Position,
+ physics_query: &'a PhysicsQuery<'world, 'state, 'b>,
+ collidable_entity_query: &'a CollidableEntityQuery<'world, 'state>,
+ physics: &'a Physics,
+}
+fn can_player_fit_within_blocks_and_entities_when(ctx: &CanPlayerFitCtx, pose: Pose) -> bool {
+ // return this.level().noCollision(this,
+ // this.getDimensions(var1).makeBoundingBox(this.position()).deflate(1.0E-7));
+ no_collision(
+ ctx.world,
+ Some(ctx.entity),
+ ctx.physics_query,
+ ctx.collidable_entity_query,
+ ctx.physics,
+ &calculate_dimensions(EntityKind::Player, pose).make_bounding_box(*ctx.position),
+ false,
+ )
+}
diff --git a/azalea-client/src/plugins/packet/game/mod.rs b/azalea-client/src/plugins/packet/game/mod.rs
index 26d83195..49523002 100644
--- a/azalea-client/src/plugins/packet/game/mod.rs
+++ b/azalea-client/src/plugins/packet/game/mod.rs
@@ -8,7 +8,7 @@ use azalea_core::{
};
use azalea_entity::{
Dead, EntityBundle, EntityKindComponent, HasClientLoaded, LoadedBy, LocalEntity, LookDirection,
- Physics, Position, RelativeEntityUpdate,
+ Physics, PlayerAbilities, Position, RelativeEntityUpdate,
indexing::{EntityIdIndex, EntityUuidIndex},
metadata::{Health, apply_metadata},
};
@@ -33,7 +33,7 @@ use crate::{
inventory::{
ClientSideCloseContainerEvent, Inventory, MenuOpenedEvent, SetContainerContentEvent,
},
- local_player::{Hunger, InstanceHolder, LocalGameMode, PlayerAbilities, TabList},
+ local_player::{Hunger, InstanceHolder, LocalGameMode, TabList},
movement::{KnockbackEvent, KnockbackType},
packet::as_system,
player::{GameProfileComponent, PlayerInfo},
diff --git a/azalea-client/tests/correct_sneak_movement.rs b/azalea-client/tests/correct_sneak_movement.rs
new file mode 100644
index 00000000..cf1d17b5
--- /dev/null
+++ b/azalea-client/tests/correct_sneak_movement.rs
@@ -0,0 +1,135 @@
+use azalea_client::{PhysicsState, StartWalkEvent, WalkDirection, 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, ServerboundGamePacket,
+ ServerboundPlayerInput,
+ },
+ },
+};
+use azalea_registry::{Block, DataRegistry, DimensionType};
+use simdnbt::owned::{NbtCompound, NbtTag};
+
+#[test]
+fn test_correct_sneak_movement() {
+ 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();
+
+ simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), (384 + 64) / 16));
+ simulation.receive_packet(ClientboundBlockUpdate {
+ pos: BlockPos::new(0, 119, 0),
+ block_state: Block::Stone.into(),
+ });
+ simulation.receive_packet(ClientboundBlockUpdate {
+ pos: BlockPos::new(0, 119, 1),
+ block_state: Block::Stone.into(),
+ });
+ simulation.receive_packet(ClientboundPlayerPosition {
+ id: 1,
+ change: PositionMoveRotation {
+ pos: Vec3::new(0.5, 120., 0.5),
+ delta: Vec3::ZERO,
+ look_direction: LookDirection::default(),
+ },
+ relative: RelativeMovements::all_absolute(),
+ });
+ simulation.tick();
+ simulation.tick();
+ simulation.tick();
+ sent_packets.clear();
+
+ simulation.with_component_mut::<PhysicsState>(|p| p.trying_to_crouch = true);
+ simulation.tick();
+ sent_packets.expect("PlayerInput", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::PlayerInput(p)
+ if *p == ServerboundPlayerInput { shift: true, ..Default::default() }
+ )
+ });
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+
+ simulation.tick();
+ simulation.send_event(StartWalkEvent {
+ entity: simulation.entity,
+ direction: WalkDirection::Forward,
+ });
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+
+ simulation.tick();
+ sent_packets.expect("PlayerInput", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::PlayerInput(p)
+ if *p == ServerboundPlayerInput { forward: true, shift: true, ..Default::default() }
+ )
+ });
+ sent_packets.expect("MovePlayerPos { z: 0.5294000033944846 }", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::MovePlayerPos(p)
+ if p.pos == Vec3::new(0.5, 120., 0.5294000033944846)
+ )
+ });
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+
+ simulation.tick();
+ sent_packets.expect("MovePlayerPos { z: 0.5748524105068866 }", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::MovePlayerPos(p)
+ if p.pos == Vec3::new(0.5, 120., 0.5748524105068866)
+ )
+ });
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+
+ simulation.tick();
+ sent_packets.expect("MovePlayerPos: { z: 0.6290694310673044 }", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::MovePlayerPos(p)
+ if p.pos == Vec3::new(0.5, 120., 0.6290694310673044)
+ )
+ });
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+}
diff --git a/azalea-client/tests/correct_sprint_sneak_movement.rs b/azalea-client/tests/correct_sprint_sneak_movement.rs
new file mode 100644
index 00000000..5b26a8a3
--- /dev/null
+++ b/azalea-client/tests/correct_sprint_sneak_movement.rs
@@ -0,0 +1,153 @@
+use azalea_client::{PhysicsState, SprintDirection, StartSprintEvent, 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, ServerboundGamePacket,
+ ServerboundPlayerInput,
+ },
+ },
+};
+use azalea_registry::{Block, DataRegistry, DimensionType};
+use simdnbt::owned::{NbtCompound, NbtTag};
+
+#[test]
+fn test_correct_sprint_sneak_movement() {
+ 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();
+
+ simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), (384 + 64) / 16));
+ simulation.receive_packet(ClientboundBlockUpdate {
+ pos: BlockPos::new(0, 119, 0),
+ block_state: Block::Stone.into(),
+ });
+ simulation.receive_packet(ClientboundBlockUpdate {
+ pos: BlockPos::new(0, 119, 1),
+ block_state: Block::Stone.into(),
+ });
+ simulation.receive_packet(ClientboundPlayerPosition {
+ id: 1,
+ change: PositionMoveRotation {
+ pos: Vec3::new(0.5, 120., 0.5),
+ delta: Vec3::ZERO,
+ look_direction: LookDirection::default(),
+ },
+ relative: RelativeMovements::all_absolute(),
+ });
+ simulation.tick();
+ simulation.tick();
+ simulation.tick();
+ sent_packets.clear();
+
+ // start sprinting
+ simulation.send_event(StartSprintEvent {
+ entity: simulation.entity,
+ direction: SprintDirection::Forward,
+ });
+ simulation.tick();
+ sent_packets.expect("PlayerInput", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::PlayerInput(p)
+ if *p == ServerboundPlayerInput { forward: true, sprint: true, ..Default::default() }
+ )
+ });
+ sent_packets.expect("PlayerCommand", |p| {
+ matches!(p, ServerboundGamePacket::PlayerCommand(_))
+ });
+ sent_packets.expect("MovePlayerPos { z: 0.6274000124096872 }", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::MovePlayerPos(p)
+ if p.pos == Vec3::new(0.5, 120., 0.6274000124096872)
+ )
+ });
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+
+ simulation.tick();
+ sent_packets.expect("MovePlayerPos { z: 0.8243604396746886 }", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::MovePlayerPos(p)
+ if p.pos == Vec3::new(0.5, 120., 0.8243604396746886)
+ )
+ });
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+ simulation.with_component_mut::<PhysicsState>(|p| p.trying_to_crouch = true);
+
+ simulation.tick();
+ sent_packets.expect("PlayerInput", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::PlayerInput(p)
+ if *p == ServerboundPlayerInput { forward: true, sprint: true, shift: true, ..Default::default() }
+ )
+ });
+ sent_packets.expect("MovePlayerPos { z: 1.0593008578621674 }", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::MovePlayerPos(p)
+ if p.pos == Vec3::new(0.5, 120., 1.0593008578621674)
+ )
+ });
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+
+ simulation.tick();
+ sent_packets.expect("MovePlayerPos { z: 1.2257983479146455 }", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::MovePlayerPos(p)
+ if p.pos == Vec3::new(0.5, 120., 1.2257983479146455)
+ )
+ });
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+
+ simulation.tick();
+ sent_packets.expect("MovePlayerPos: { z: 1.3549259948648078 }", |p| {
+ matches!(
+ p,
+ ServerboundGamePacket::MovePlayerPos(p)
+ if p.pos == Vec3::new(0.5, 120., 1.3549259948648078)
+ )
+ });
+ sent_packets.expect_tick_end();
+ sent_packets.expect_empty();
+}