mirror of
https://github.com/mat-1/azalea.git
synced 2025-08-02 06:16:04 +00:00
* start implementing fluid physics * Initial implementation of fluid pushing * different travel function in water * bubble columns * jumping in water * cleanup * change ultrawarm to be required * fix for clippy
543 lines
18 KiB
Rust
543 lines
18 KiB
Rust
use std::backtrace::Backtrace;
|
|
|
|
use azalea_core::position::Vec3;
|
|
use azalea_core::tick::GameTick;
|
|
use azalea_entity::{metadata::Sprinting, Attributes, Jumping};
|
|
use azalea_entity::{InLoadedChunk, LastSentPosition, LookDirection, Physics, Position};
|
|
use azalea_physics::{ai_step, PhysicsSet};
|
|
use azalea_protocol::packets::game::ServerboundPlayerCommand;
|
|
use azalea_protocol::packets::{
|
|
game::{
|
|
s_move_player_pos::ServerboundMovePlayerPos,
|
|
s_move_player_pos_rot::ServerboundMovePlayerPosRot,
|
|
s_move_player_rot::ServerboundMovePlayerRot,
|
|
s_move_player_status_only::ServerboundMovePlayerStatusOnly,
|
|
},
|
|
Packet,
|
|
};
|
|
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::{
|
|
component::Component, entity::Entity, event::EventReader, query::With,
|
|
schedule::IntoSystemConfigs, system::Query,
|
|
};
|
|
use thiserror::Error;
|
|
|
|
use crate::client::Client;
|
|
use crate::packet_handling::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 PlayerMovePlugin;
|
|
|
|
impl Plugin for PlayerMovePlugin {
|
|
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_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,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
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)]
|
|
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,
|
|
}
|
|
}
|
|
}
|