use azalea_core::{
entity_id::MinecraftEntityId,
game_type::GameMode,
position::{Vec2, Vec3},
tick::GameTick,
};
use azalea_entity::{
Attributes, Crouching, HasClientLoaded, Jumping, LastSentPosition, LocalEntity, LookDirection,
Physics, PlayerAbilities, Pose, Position,
dimensions::calculate_dimensions,
metadata::{self, Sprinting},
update_bounding_box,
};
use azalea_physics::{
PhysicsSystems, ai_step,
collision::entity_collisions::{AabbQuery, CollidableEntityQuery, update_last_bounding_box},
local_player::{PhysicsState, SprintDirection, WalkDirection},
travel::{no_collision, travel},
};
use azalea_protocol::{
common::movements::MoveFlags,
packets::{
Packet,
game::{
ServerboundPlayerCommand, ServerboundPlayerInput,
s_move_player_pos::ServerboundMovePlayerPos,
s_move_player_pos_rot::ServerboundMovePlayerPosRot,
s_move_player_rot::ServerboundMovePlayerRot,
s_move_player_status_only::ServerboundMovePlayerStatusOnly, s_player_command,
},
},
};
use azalea_registry::builtin::EntityKind;
use azalea_world::World;
use bevy_app::{App, Plugin, Update};
use bevy_ecs::prelude::*;
use crate::{
local_player::{Hunger, LocalGameMode, WorldHolder},
packet::game::SendGamePacketEvent,
};
pub struct MovementPlugin;
impl Plugin for MovementPlugin {
fn build(&self, app: &mut App) {
app.add_message::<StartWalkEvent>()
.add_message::<StartSprintEvent>()
.add_systems(
Update,
(handle_sprint, handle_walk)
.chain()
.in_set(MoveEventsSystems)
.after(update_bounding_box)
.after(update_last_bounding_box),
)
.add_systems(
GameTick,
(
(tick_controls, local_player_ai_step, update_pose)
.chain()
.in_set(PhysicsSystems)
.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)
.after(travel),
send_position.after(PhysicsSystems),
)
.chain(),
)
.add_observer(handle_knockback);
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)]
pub struct MoveEventsSystems;
#[derive(Clone, Component, Debug, Default)]
pub struct LastSentLookDirection {
pub x_rot: f32,
pub y_rot: f32,
}
#[allow(clippy::type_complexity)]
pub fn send_position(
mut query: Query<
(
Entity,
&Position,
&LookDirection,
&mut PhysicsState,
&mut LastSentPosition,
&mut Physics,
&mut LastSentLookDirection,
),
With<HasClientLoaded>,
>,
mut commands: Commands,
) {
for (
entity,
position,
direction,
mut physics_state,
mut last_sent_position,
mut physics,
mut last_direction,
) in query.iter_mut()
{
let packet = {
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;
let is_delta_large_enough =
(x_delta.powi(2) + y_delta.powi(2) + z_delta.powi(2)) > 2.0e-4f64.powi(2);
let sending_position = is_delta_large_enough || physics_state.position_remainder >= 20;
let sending_direction = y_rot_delta != 0.0 || x_rot_delta != 0.0;
let flags = MoveFlags {
on_ground: physics.on_ground(),
horizontal_collision: physics.horizontal_collision,
};
let packet = if sending_position && sending_direction {
Some(
ServerboundMovePlayerPosRot {
pos: **position,
look_direction: *direction,
flags,
}
.into_variant(),
)
} else if sending_position {
Some(
ServerboundMovePlayerPos {
pos: **position,
flags,
}
.into_variant(),
)
} else if sending_direction {
Some(
ServerboundMovePlayerRot {
look_direction: *direction,
flags,
}
.into_variant(),
)
} else if physics.last_on_ground() != physics.on_ground() {
Some(ServerboundMovePlayerStatusOnly { flags }.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);
packet
};
if let Some(packet) = packet {
commands.trigger(SendGamePacketEvent {
sent_by: entity,
packet,
});
}
}
}
#[derive(Clone, Component, Debug, Default, Eq, PartialEq)]
pub struct LastSentInput(pub ServerboundPlayerInput);
pub fn send_player_input_packet(
mut query: Query<(Entity, &PhysicsState, &Jumping, Option<&LastSentInput>)>,
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,
shift: physics_state.trying_to_crouch,
sprint: physics_state.trying_to_sprint,
};
let last_sent_input = last_sent_input.cloned().unwrap_or_default();
if input != last_sent_input.0 {
commands.trigger(SendGamePacketEvent {
sent_by: entity,
packet: input.clone().into_variant(),
});
commands.entity(entity).insert(LastSentInput(input));
}
}
}
pub fn send_sprinting_if_needed(
mut query: Query<(Entity, &MinecraftEntityId, &Sprinting, &mut PhysicsState)>,
mut commands: Commands,
) {
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 {
s_player_command::Action::StartSprinting
} else {
s_player_command::Action::StopSprinting
};
commands.trigger(SendGamePacketEvent::new(
entity,
ServerboundPlayerCommand {
id: *minecraft_entity_id,
action: sprinting_action,
data: 0,
},
));
physics_state.was_sprinting = **sprinting;
}
}
}
pub(crate) fn tick_controls(mut query: Query<&mut PhysicsState>) {
for mut physics_state in query.iter_mut() {
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.;
}
_ => {}
};
let move_vector = Vec2::new(left_impulse, forward_impulse).normalized();
physics_state.move_vector = move_vector;
}
}
#[allow(clippy::type_complexity)]
pub fn local_player_ai_step(
mut query: Query<
(
Entity,
&PhysicsState,
&PlayerAbilities,
&metadata::Swimming,
&metadata::SleepingPos,
&WorldHolder,
&Position,
Option<&Hunger>,
Option<&LastSentInput>,
&mut Physics,
&mut Sprinting,
&mut Crouching,
&mut Attributes,
),
(With<HasClientLoaded>, With<LocalEntity>),
>,
aabb_query: AabbQuery,
collidable_entity_query: CollidableEntityQuery,
) {
for (
entity,
physics_state,
abilities,
swimming,
sleeping_pos,
world_holder,
position,
hunger,
last_sent_input,
mut physics,
mut sprinting,
mut crouching,
mut attributes,
) in query.iter_mut()
{
let is_swimming = **swimming;
let is_passenger = false;
let is_sleeping = sleeping_pos.is_some();
let world = world_holder.shared.read();
let ctx = CanPlayerFitCtx {
world: &world,
entity,
position: *position,
aabb_query: &aabb_query,
collidable_entity_query: &collidable_entity_query,
physics: &physics,
};
let new_crouching = !abilities.flying
&& !is_swimming
&& !is_passenger
&& (last_sent_input.is_some_and(|i| i.0.shift)
|| !is_sleeping
&& !can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Standing))
&& can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Crouching);
if **crouching != new_crouching {
**crouching = new_crouching;
}
let has_enough_food_to_sprint = hunger.is_none_or(Hunger::is_enough_to_sprint);
let trying_to_sprint = physics_state.trying_to_sprint;
let is_underwater = false;
let is_in_water = physics.is_in_water();
let is_fall_flying = false;
let is_passenger = false;
let using_item = false;
let has_blindness = false;
let has_enough_impulse = has_enough_impulse_to_start_sprinting(physics_state);
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 {
let vehicle_can_sprint = false;
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);
}
}
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
}
fn modify_input(
mut move_vector: Vec2,
is_using_item: bool,
is_passenger: bool,
moving_slowly: bool,
attributes: &Attributes,
) -> Vec2 {
if move_vector.length_squared() == 0. {
return move_vector;
}
move_vector *= 0.98;
if is_using_item && !is_passenger {
move_vector *= 0.2;
}
if moving_slowly {
let sneaking_speed = attributes.sneaking_speed.calculate() as f32;
move_vector *= sneaking_speed;
}
modify_input_speed_for_square_movement(move_vector)
}
fn modify_input_speed_for_square_movement(move_vector: Vec2) -> Vec2 {
let length = move_vector.length();
if length == 0. {
return move_vector;
}
let scaled_to_inverse_length = move_vector * (1. / length);
let dist = distance_to_unit_square(scaled_to_inverse_length);
let scale = (length * dist).min(1.);
scaled_to_inverse_length * scale
}
fn distance_to_unit_square(v: Vec2) -> f32 {
let x = v.x.abs();
let y = v.y.abs();
let ratio = if y > x { x / y } else { y / x };
(1. + ratio * ratio).sqrt()
}
#[derive(Debug, Message)]
pub struct StartWalkEvent {
pub entity: Entity,
pub direction: WalkDirection,
}
pub fn handle_walk(
mut events: MessageReader<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);
}
}
}
#[derive(Message)]
pub struct StartSprintEvent {
pub entity: Entity,
pub direction: SprintDirection,
}
pub fn handle_sprint(
mut query: Query<&mut PhysicsState>,
mut events: MessageReader<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;
}
}
}
fn set_sprinting(
sprinting: bool,
currently_sprinting: &mut Sprinting,
attributes: &mut Attributes,
) -> bool {
**currently_sprinting = sprinting;
if sprinting {
attributes
.movement_speed
.try_insert(azalea_entity::attributes::sprinting_modifier())
.is_ok()
} else {
attributes
.movement_speed
.remove(&azalea_entity::attributes::sprinting_modifier().id)
.is_none()
}
}
fn has_enough_impulse_to_start_sprinting(physics_state: &PhysicsState) -> bool {
physics_state.move_vector.y > 0.8
}
#[derive(EntityEvent, Debug, Clone)]
pub struct KnockbackEvent {
pub entity: Entity,
pub data: KnockbackData,
}
#[derive(Debug, Clone)]
pub enum KnockbackData {
Set(Vec3),
Add(Vec3),
}
pub fn handle_knockback(knockback: On<KnockbackEvent>, mut query: Query<&mut Physics>) {
if let Ok(mut physics) = query.get_mut(knockback.entity) {
match knockback.data {
KnockbackData::Set(velocity) => {
physics.velocity = velocity;
}
KnockbackData::Add(velocity) => {
physics.velocity += velocity;
}
}
}
}
pub fn update_pose(
mut query: Query<(
Entity,
&mut Pose,
&Physics,
&PhysicsState,
&LocalGameMode,
&WorldHolder,
&Position,
)>,
aabb_query: AabbQuery,
collidable_entity_query: CollidableEntityQuery,
) {
for (entity, mut pose, physics, physics_state, game_mode, world_holder, position) in
query.iter_mut()
{
let world = world_holder.shared.read();
let world = &*world;
let ctx = CanPlayerFitCtx {
world,
entity,
position: *position,
aabb_query: &aabb_query,
collidable_entity_query: &collidable_entity_query,
physics,
};
if !can_player_fit_within_blocks_and_entities_when(&ctx, Pose::Swimming) {
continue;
}
let desired_pose = if physics_state.trying_to_crouch {
Pose::Crouching
} else {
Pose::Standing
};
let is_passenger = false;
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
};
if new_pose != *pose {
*pose = new_pose;
}
}
}
struct CanPlayerFitCtx<'world, 'state, 'a, 'b> {
world: &'a World,
entity: Entity,
position: Position,
aabb_query: &'a AabbQuery<'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 {
no_collision(
ctx.world,
Some(ctx.entity),
ctx.aabb_query,
ctx.collidable_entity_query,
ctx.physics,
&calculate_dimensions(EntityKind::Player, pose)
.make_bounding_box(*ctx.position)
.deflate_all(1.0e-7),
false,
)
}