aboutsummaryrefslogtreecommitdiff
path: root/azalea-client/src/plugins/movement.rs
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2025-02-22 21:45:26 -0600
committerGitHub <noreply@github.com>2025-02-22 21:45:26 -0600
commite21e1b97bf9337e9f4747cd1b545b1b3a03e2ce7 (patch)
treeadd6f8bfce40d0c07845d8aa4c9945a0b918444c /azalea-client/src/plugins/movement.rs
parentf8130c3c92946d2293634ba4e252d6bc93026c3c (diff)
downloadazalea-drasl-e21e1b97bf9337e9f4747cd1b545b1b3a03e2ce7.tar.xz
Refactor azalea-client (#205)
* start organizing packet_handling more by moving packet handlers into their own functions * finish writing all the handler functions for packets * use macro for generating match statement for packet handler functions * fix set_entity_data * update config state to also use handler functions * organize az-client file structure by moving things into plugins directory * fix merge issues
Diffstat (limited to 'azalea-client/src/plugins/movement.rs')
-rw-r--r--azalea-client/src/plugins/movement.rs580
1 files changed, 580 insertions, 0 deletions
diff --git a/azalea-client/src/plugins/movement.rs b/azalea-client/src/plugins/movement.rs
new file mode 100644
index 00000000..17b92e65
--- /dev/null
+++ b/azalea-client/src/plugins/movement.rs
@@ -0,0 +1,580 @@
+use std::backtrace::Backtrace;
+
+use azalea_core::position::Vec3;
+use azalea_core::tick::GameTick;
+use azalea_entity::{Attributes, Jumping, metadata::Sprinting};
+use azalea_entity::{InLoadedChunk, LastSentPosition, LookDirection, Physics, Position};
+use azalea_physics::{PhysicsSet, ai_step};
+use azalea_protocol::packets::game::{ServerboundPlayerCommand, ServerboundPlayerInput};
+use azalea_protocol::packets::{
+ Packet,
+ game::{
+ s_move_player_pos::ServerboundMovePlayerPos,
+ s_move_player_pos_rot::ServerboundMovePlayerPosRot,
+ s_move_player_rot::ServerboundMovePlayerRot,
+ s_move_player_status_only::ServerboundMovePlayerStatusOnly,
+ },
+};
+use azalea_world::{MinecraftEntityId, MoveEntityError};
+use bevy_app::{App, Plugin, Update};
+use bevy_ecs::prelude::{Event, EventWriter};
+use bevy_ecs::schedule::SystemSet;
+use bevy_ecs::system::Commands;
+use bevy_ecs::{
+ component::Component, entity::Entity, event::EventReader, query::With,
+ schedule::IntoSystemConfigs, system::Query,
+};
+use thiserror::Error;
+
+use crate::client::Client;
+use crate::packet::game::SendPacketEvent;
+
+#[derive(Error, Debug)]
+pub enum MovePlayerError {
+ #[error("Player is not in world")]
+ PlayerNotInWorld(Backtrace),
+ #[error("{0}")]
+ Io(#[from] std::io::Error),
+}
+
+impl From<MoveEntityError> for MovePlayerError {
+ fn from(err: MoveEntityError) -> Self {
+ match err {
+ MoveEntityError::EntityDoesNotExist(backtrace) => {
+ MovePlayerError::PlayerNotInWorld(backtrace)
+ }
+ }
+ }
+}
+
+pub struct MovementPlugin;
+
+impl Plugin for MovementPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_event::<StartWalkEvent>()
+ .add_event::<StartSprintEvent>()
+ .add_event::<KnockbackEvent>()
+ .add_systems(
+ Update,
+ (handle_sprint, handle_walk, handle_knockback)
+ .chain()
+ .in_set(MoveEventsSet),
+ )
+ .add_systems(
+ GameTick,
+ (
+ (tick_controls, local_player_ai_step)
+ .chain()
+ .in_set(PhysicsSet)
+ .before(ai_step)
+ .before(azalea_physics::fluids::update_in_water_state_and_do_fluid_pushing),
+ send_sprinting_if_needed.after(azalea_entity::update_in_loaded_chunk),
+ send_player_input_packet,
+ send_position.after(PhysicsSet),
+ )
+ .chain(),
+ );
+ }
+}
+
+#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
+pub struct MoveEventsSet;
+
+impl Client {
+ /// Set whether we're jumping. This acts as if you held space in
+ /// vanilla. If you want to jump once, use the `jump` function.
+ ///
+ /// If you're making a realistic client, calling this function every tick is
+ /// recommended.
+ pub fn set_jumping(&mut self, jumping: bool) {
+ let mut ecs = self.ecs.lock();
+ let mut jumping_mut = self.query::<&mut Jumping>(&mut ecs);
+ **jumping_mut = jumping;
+ }
+
+ /// Returns whether the player will try to jump next tick.
+ pub fn jumping(&self) -> bool {
+ *self.component::<Jumping>()
+ }
+
+ /// 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.
+ /// `y_rot` goes from -180 to 180, and `x_rot` goes from -90 to 90.
+ pub fn set_direction(&mut self, y_rot: f32, x_rot: f32) {
+ let mut ecs = self.ecs.lock();
+ let mut look_direction = self.query::<&mut LookDirection>(&mut ecs);
+
+ (look_direction.y_rot, look_direction.x_rot) = (y_rot, x_rot);
+ }
+
+ /// Returns the direction the client is looking. The first value is the y
+ /// rotation (ie. yaw, looking to the side) and the second value is the x
+ /// rotation (ie. pitch, looking up and down).
+ pub fn direction(&self) -> (f32, f32) {
+ let look_direction = self.component::<LookDirection>();
+ (look_direction.y_rot, look_direction.x_rot)
+ }
+}
+
+/// A component that contains the look direction that was last sent over the
+/// network.
+#[derive(Debug, Component, Clone, Default)]
+pub struct LastSentLookDirection {
+ pub x_rot: f32,
+ 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 forward_impulse: f32,
+ pub left_impulse: f32,
+}
+
+#[allow(clippy::type_complexity)]
+pub fn send_position(
+ mut query: Query<
+ (
+ Entity,
+ &Position,
+ &LookDirection,
+ &mut PhysicsState,
+ &mut LastSentPosition,
+ &mut Physics,
+ &mut LastSentLookDirection,
+ ),
+ With<InLoadedChunk>,
+ >,
+ mut send_packet_events: EventWriter<SendPacketEvent>,
+) {
+ for (
+ entity,
+ position,
+ direction,
+ mut physics_state,
+ mut last_sent_position,
+ mut physics,
+ mut last_direction,
+ ) in query.iter_mut()
+ {
+ let packet = {
+ // TODO: the camera being able to be controlled by other entities isn't
+ // implemented yet if !self.is_controlled_camera() { return };
+
+ let x_delta = position.x - last_sent_position.x;
+ let y_delta = position.y - last_sent_position.y;
+ let z_delta = position.z - last_sent_position.z;
+ let y_rot_delta = (direction.y_rot - last_direction.y_rot) as f64;
+ let x_rot_delta = (direction.x_rot - last_direction.x_rot) as f64;
+
+ physics_state.position_remainder += 1;
+
+ // boolean sendingPosition = Mth.lengthSquared(xDelta, yDelta, zDelta) >
+ // Mth.square(2.0E-4D) || this.positionReminder >= 20;
+ let sending_position = ((x_delta.powi(2) + y_delta.powi(2) + z_delta.powi(2))
+ > 2.0e-4f64.powi(2))
+ || physics_state.position_remainder >= 20;
+ let sending_direction = y_rot_delta != 0.0 || x_rot_delta != 0.0;
+
+ // if self.is_passenger() {
+ // TODO: posrot packet for being a passenger
+ // }
+ let packet = if sending_position && sending_direction {
+ Some(
+ ServerboundMovePlayerPosRot {
+ pos: **position,
+ look_direction: *direction,
+ on_ground: physics.on_ground(),
+ }
+ .into_variant(),
+ )
+ } else if sending_position {
+ Some(
+ ServerboundMovePlayerPos {
+ pos: **position,
+ on_ground: physics.on_ground(),
+ }
+ .into_variant(),
+ )
+ } else if sending_direction {
+ Some(
+ ServerboundMovePlayerRot {
+ look_direction: *direction,
+ on_ground: physics.on_ground(),
+ }
+ .into_variant(),
+ )
+ } else if physics.last_on_ground() != physics.on_ground() {
+ Some(
+ ServerboundMovePlayerStatusOnly {
+ on_ground: physics.on_ground(),
+ }
+ .into_variant(),
+ )
+ } else {
+ None
+ };
+
+ if sending_position {
+ **last_sent_position = **position;
+ physics_state.position_remainder = 0;
+ }
+ if sending_direction {
+ last_direction.y_rot = direction.y_rot;
+ last_direction.x_rot = direction.x_rot;
+ }
+
+ let on_ground = physics.on_ground();
+ physics.set_last_on_ground(on_ground);
+ // minecraft checks for autojump here, but also autojump is bad so
+
+ packet
+ };
+
+ if let Some(packet) = packet {
+ send_packet_events.send(SendPacketEvent {
+ sent_by: entity,
+ packet,
+ });
+ }
+ }
+}
+
+#[derive(Debug, Default, Component, Clone, PartialEq, Eq)]
+pub struct LastSentInput(pub ServerboundPlayerInput);
+pub fn send_player_input_packet(
+ mut query: Query<(Entity, &PhysicsState, &Jumping, Option<&LastSentInput>)>,
+ mut send_packet_events: EventWriter<SendPacketEvent>,
+ mut commands: Commands,
+) {
+ for (entity, physics_state, jumping, last_sent_input) in query.iter_mut() {
+ let dir = physics_state.move_direction;
+ type D = WalkDirection;
+ let input = ServerboundPlayerInput {
+ forward: matches!(dir, D::Forward | D::ForwardLeft | D::ForwardRight),
+ backward: matches!(dir, D::Backward | D::BackwardLeft | D::BackwardRight),
+ 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,
+ sprint: physics_state.trying_to_sprint,
+ };
+
+ // if LastSentInput isn't present, we default to assuming we're not pressing any
+ // keys and insert it anyways every time it changes
+ let last_sent_input = last_sent_input.cloned().unwrap_or_default();
+
+ if input != last_sent_input.0 {
+ send_packet_events.send(SendPacketEvent {
+ sent_by: entity,
+ packet: input.clone().into_variant(),
+ });
+ commands.entity(entity).insert(LastSentInput(input));
+ }
+ }
+}
+
+fn send_sprinting_if_needed(
+ mut query: Query<(Entity, &MinecraftEntityId, &Sprinting, &mut PhysicsState)>,
+ mut send_packet_events: EventWriter<SendPacketEvent>,
+) {
+ for (entity, minecraft_entity_id, sprinting, mut physics_state) in query.iter_mut() {
+ let was_sprinting = physics_state.was_sprinting;
+ if **sprinting != was_sprinting {
+ let sprinting_action = if **sprinting {
+ azalea_protocol::packets::game::s_player_command::Action::StartSprinting
+ } else {
+ azalea_protocol::packets::game::s_player_command::Action::StopSprinting
+ };
+ send_packet_events.send(SendPacketEvent::new(
+ entity,
+ ServerboundPlayerCommand {
+ id: *minecraft_entity_id,
+ action: sprinting_action,
+ data: 0,
+ },
+ ));
+ physics_state.was_sprinting = **sprinting;
+ }
+ }
+}
+
+/// Update the impulse from self.move_direction. The multiplier is used for
+/// sneaking.
+pub(crate) fn tick_controls(mut query: Query<&mut PhysicsState>) {
+ for mut physics_state in query.iter_mut() {
+ let multiplier: Option<f32> = None;
+
+ let mut forward_impulse: f32 = 0.;
+ let mut left_impulse: f32 = 0.;
+ let move_direction = physics_state.move_direction;
+ match move_direction {
+ WalkDirection::Forward | WalkDirection::ForwardRight | WalkDirection::ForwardLeft => {
+ forward_impulse += 1.;
+ }
+ WalkDirection::Backward
+ | WalkDirection::BackwardRight
+ | WalkDirection::BackwardLeft => {
+ forward_impulse -= 1.;
+ }
+ _ => {}
+ };
+ match move_direction {
+ WalkDirection::Right | WalkDirection::ForwardRight | WalkDirection::BackwardRight => {
+ left_impulse += 1.;
+ }
+ WalkDirection::Left | WalkDirection::ForwardLeft | WalkDirection::BackwardLeft => {
+ left_impulse -= 1.;
+ }
+ _ => {}
+ };
+ physics_state.forward_impulse = forward_impulse;
+ physics_state.left_impulse = left_impulse;
+
+ if let Some(multiplier) = multiplier {
+ physics_state.forward_impulse *= multiplier;
+ physics_state.left_impulse *= multiplier;
+ }
+ }
+}
+
+/// Makes the bot do one physics tick. Note that this is already handled
+/// automatically by the client.
+pub fn local_player_ai_step(
+ mut query: Query<
+ (&PhysicsState, &mut Physics, &mut Sprinting, &mut Attributes),
+ With<InLoadedChunk>,
+ >,
+) {
+ for (physics_state, mut physics, mut sprinting, mut attributes) in query.iter_mut() {
+ // server ai step
+ physics.x_acceleration = physics_state.left_impulse;
+ physics.z_acceleration = physics_state.forward_impulse;
+
+ // 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;
+
+ // 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
+ )
+ {
+ set_sprinting(true, &mut sprinting, &mut attributes);
+ }
+ }
+}
+
+impl Client {
+ /// Start walking in the given direction. To sprint, use
+ /// [`Client::sprint`]. To stop walking, call walk with
+ /// `WalkDirection::None`.
+ ///
+ /// # Examples
+ ///
+ /// Walk for 1 second
+ /// ```rust,no_run
+ /// # use azalea_client::{Client, WalkDirection};
+ /// # use std::time::Duration;
+ /// # async fn example(mut bot: Client) {
+ /// bot.walk(WalkDirection::Forward);
+ /// tokio::time::sleep(Duration::from_secs(1)).await;
+ /// bot.walk(WalkDirection::None);
+ /// # }
+ /// ```
+ pub fn walk(&mut self, direction: WalkDirection) {
+ let mut ecs = self.ecs.lock();
+ ecs.send_event(StartWalkEvent {
+ entity: self.entity,
+ direction,
+ });
+ }
+
+ /// Start sprinting in the given direction. To stop moving, call
+ /// [`Client::walk(WalkDirection::None)`]
+ ///
+ /// # Examples
+ ///
+ /// Sprint for 1 second
+ /// ```rust,no_run
+ /// # use azalea_client::{Client, WalkDirection, SprintDirection};
+ /// # use std::time::Duration;
+ /// # async fn example(mut bot: Client) {
+ /// bot.sprint(SprintDirection::Forward);
+ /// tokio::time::sleep(Duration::from_secs(1)).await;
+ /// bot.walk(WalkDirection::None);
+ /// # }
+ /// ```
+ pub fn sprint(&mut self, direction: SprintDirection) {
+ let mut ecs = self.ecs.lock();
+ ecs.send_event(StartSprintEvent {
+ entity: self.entity,
+ direction,
+ });
+ }
+}
+
+/// An event sent when the client starts walking. This does not get sent for
+/// non-local entities.
+///
+/// To stop walking or sprinting, send this event with `WalkDirection::None`.
+#[derive(Event, Debug)]
+pub struct StartWalkEvent {
+ pub entity: Entity,
+ pub direction: WalkDirection,
+}
+
+/// The system that makes the player start walking when they receive a
+/// [`StartWalkEvent`].
+pub fn handle_walk(
+ mut events: EventReader<StartWalkEvent>,
+ mut query: Query<(&mut PhysicsState, &mut Sprinting, &mut Attributes)>,
+) {
+ for event in events.read() {
+ if let Ok((mut physics_state, mut sprinting, mut attributes)) = query.get_mut(event.entity)
+ {
+ physics_state.move_direction = event.direction;
+ physics_state.trying_to_sprint = false;
+ set_sprinting(false, &mut sprinting, &mut attributes);
+ }
+ }
+}
+
+/// An event sent when the client starts sprinting. This does not get sent for
+/// non-local entities.
+#[derive(Event)]
+pub struct StartSprintEvent {
+ pub entity: Entity,
+ pub direction: SprintDirection,
+}
+/// The system that makes the player start sprinting when they receive a
+/// [`StartSprintEvent`].
+pub fn handle_sprint(
+ mut query: Query<&mut PhysicsState>,
+ mut events: EventReader<StartSprintEvent>,
+) {
+ for event in events.read() {
+ if let Ok(mut physics_state) = query.get_mut(event.entity) {
+ physics_state.move_direction = WalkDirection::from(event.direction);
+ physics_state.trying_to_sprint = true;
+ }
+ }
+}
+
+/// 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.
+fn set_sprinting(
+ sprinting: bool,
+ currently_sprinting: &mut Sprinting,
+ attributes: &mut Attributes,
+) -> bool {
+ **currently_sprinting = sprinting;
+ if sprinting {
+ attributes
+ .speed
+ .try_insert(azalea_entity::attributes::sprinting_modifier())
+ .is_ok()
+ } else {
+ attributes
+ .speed
+ .remove(&azalea_entity::attributes::sprinting_modifier().id)
+ .is_none()
+ }
+}
+
+// Whether the player is moving fast enough to be able to start sprinting.
+fn has_enough_impulse_to_start_sprinting(physics_state: &PhysicsState) -> bool {
+ // if self.underwater() {
+ // self.has_forward_impulse()
+ // } else {
+ physics_state.forward_impulse > 0.8
+ // }
+}
+
+/// An event sent by the server that sets or adds to our velocity. Usually
+/// `KnockbackKind::Set` is used for normal knockback and `KnockbackKind::Add`
+/// is used for explosions, but some servers (notably Hypixel) use explosions
+/// for knockback.
+#[derive(Event)]
+pub struct KnockbackEvent {
+ pub entity: Entity,
+ pub knockback: KnockbackType,
+}
+
+pub enum KnockbackType {
+ Set(Vec3),
+ Add(Vec3),
+}
+
+pub fn handle_knockback(mut query: Query<&mut Physics>, mut events: EventReader<KnockbackEvent>) {
+ for event in events.read() {
+ if let Ok(mut physics) = query.get_mut(event.entity) {
+ match event.knockback {
+ KnockbackType::Set(velocity) => {
+ physics.velocity = velocity;
+ }
+ KnockbackType::Add(velocity) => {
+ physics.velocity += velocity;
+ }
+ }
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+pub enum WalkDirection {
+ #[default]
+ None,
+ Forward,
+ Backward,
+ Left,
+ Right,
+ ForwardRight,
+ ForwardLeft,
+ BackwardRight,
+ BackwardLeft,
+}
+
+/// The directions that we can sprint in. It's a subset of [`WalkDirection`].
+#[derive(Clone, Copy, Debug)]
+pub enum SprintDirection {
+ Forward,
+ ForwardRight,
+ ForwardLeft,
+}
+
+impl From<SprintDirection> for WalkDirection {
+ fn from(d: SprintDirection) -> Self {
+ match d {
+ SprintDirection::Forward => WalkDirection::Forward,
+ SprintDirection::ForwardRight => WalkDirection::ForwardRight,
+ SprintDirection::ForwardLeft => WalkDirection::ForwardLeft,
+ }
+ }
+}