1
2
Fork 0
mirror of https://github.com/mat-1/azalea.git synced 2025-08-02 06:16:04 +00:00

azalea compiles lol

This commit is contained in:
Ubuntu 2023-01-13 19:53:04 +00:00
parent 259e7b44ce
commit 9f634a4d83
17 changed files with 515 additions and 348 deletions

7
Cargo.lock generated
View file

@ -174,8 +174,11 @@ dependencies = [
"azalea-protocol",
"azalea-registry",
"azalea-world",
"bevy_app",
"bevy_ecs",
"env_logger",
"futures",
"iyes_loopless",
"log",
"nohash-hasher",
"num-traits",
@ -351,6 +354,9 @@ dependencies = [
"azalea-block",
"azalea-core",
"azalea-world",
"bevy_app",
"bevy_ecs",
"iyes_loopless",
"once_cell",
"parking_lot",
"uuid",
@ -430,6 +436,7 @@ dependencies = [
"bevy_ecs",
"derive_more",
"enum-as-inner",
"iyes_loopless",
"log",
"nohash-hasher",
"once_cell",

View file

@ -3,7 +3,7 @@ use crate::{
local_player::{
death_event, send_tick_event, update_in_loaded_chunk, LocalPlayer, PhysicsState,
},
movement::{local_player_ai_step, send_position},
movement::{local_player_ai_step, send_position, sprint_listener, walk_listener},
packet_handling::{self, PacketHandlerPlugin},
plugins::PluginStates,
Account, PlayerInfo,
@ -42,7 +42,7 @@ use bevy_ecs::{
use iyes_loopless::prelude::*;
use log::{debug, error};
use parking_lot::{Mutex, RwLock};
use std::{fmt::Debug, io, ops::DerefMut, sync::Arc, time::Duration};
use std::{fmt::Debug, io, net::SocketAddr, ops::DerefMut, sync::Arc, time::Duration};
use thiserror::Error;
use tokio::{sync::mpsc, time};
@ -82,6 +82,7 @@ pub enum Event {
/// Client has the things that a user interacting with the library will want.
/// Things that a player in the world will want to know are in [`LocalPlayer`].
#[derive(Clone)]
pub struct Client {
/// The [`GameProfile`] for our client. This contains your username, UUID,
/// and skin data.
@ -174,8 +175,36 @@ impl Client {
let address: ServerAddress = address.try_into().map_err(|_| JoinError::InvalidAddress)?;
let resolved_address = resolver::resolve_address(&address).await?;
let conn = Connection::new(&resolved_address).await?;
let (conn, game_profile) = Self::handshake(conn, account, &address).await?;
// An event that causes the schedule to run. This is only used internally.
let (run_schedule_sender, run_schedule_receiver) = mpsc::unbounded_channel();
let ecs_lock = start_ecs(run_schedule_receiver, run_schedule_sender.clone());
{
let mut ecs = ecs_lock.lock();
ecs.init_resource::<EntityInfos>();
}
Self::start_client(
ecs_lock,
account,
&address,
&resolved_address,
run_schedule_sender,
)
.await
}
/// Create a [`Client`] when you already have the ECS made with
/// [`start_ecs`]. You'd usually want to use [`Self::join`] instead.
pub async fn start_client(
ecs_lock: Arc<Mutex<bevy_ecs::world::World>>,
account: &Account,
address: &ServerAddress,
resolved_address: &SocketAddr,
run_schedule_sender: mpsc::UnboundedSender<()>,
) -> Result<(Self, mpsc::UnboundedReceiver<Event>), JoinError> {
let conn = Connection::new(resolved_address).await?;
let (conn, game_profile) = Self::handshake(conn, account, address).await?;
let (read_conn, write_conn) = conn.into_split();
// The buffer has to be 1 to avoid a bug where if it lags events are
@ -184,14 +213,9 @@ impl Client {
let (tx, rx) = mpsc::unbounded_channel();
tx.send(Event::Init).unwrap();
// An event that causes the schedule to run. This is only used internally.
let (run_schedule_sender, run_schedule_receiver) = mpsc::unbounded_channel();
let ecs_lock = start_ecs(run_schedule_receiver, run_schedule_sender.clone()).await;
let mut ecs = ecs_lock.lock();
ecs.init_resource::<EntityInfos>();
// Make the ecs entity for this client
let entity_mut = ecs.spawn_empty();
let entity = entity_mut.id();
@ -448,20 +472,20 @@ impl Client {
Ok(())
}
/// Query data of our player's entity.
/// A convenience function for getting components of our player's entity.
pub fn query<'w, Q: WorldQuery>(
&self,
ecs: &'w mut bevy_ecs::world::World,
) -> <Q as WorldQuery>::Item<'w> {
ecs.query::<Q>()
.get_mut(ecs, self.entity)
.expect("Player entity should always exist when being queried")
.expect("Our client is missing a required component.")
}
}
/// Start the protocol and game tick loop.
#[doc(hidden)]
pub async fn start_ecs(
pub fn start_ecs(
run_schedule_receiver: mpsc::UnboundedReceiver<()>,
run_schedule_sender: mpsc::UnboundedSender<()>,
) -> Arc<Mutex<bevy_ecs::world::World>> {
@ -470,16 +494,22 @@ pub async fn start_ecs(
// you might be able to just drop the lock or put it in its own scope to fix
let mut app = App::new();
app.add_fixed_timestep(Duration::from_millis(50), "tick")
.add_fixed_timestep_system_set(
"tick",
0,
SystemSet::new()
.with_system(send_position)
.with_system(update_in_loaded_chunk)
.with_system(local_player_ai_step)
.with_system(send_tick_event),
);
app.add_fixed_timestep(Duration::from_millis(50), "tick");
app.add_fixed_timestep_system_set(
"tick",
0,
SystemSet::new()
.with_system(send_position)
.with_system(update_in_loaded_chunk)
.with_system(sprint_listener.label("sprint_listener").before("travel"))
.with_system(walk_listener.label("walk_listener").before("travel"))
.with_system(
local_player_ai_step
.before("ai_step")
.after("sprint_listener"),
)
.with_system(send_tick_event),
);
// fire the Death event when the player dies.
app.add_system(death_event.after("tick").after("packet"));

View file

@ -22,7 +22,8 @@ mod player;
mod plugins;
pub use account::Account;
pub use client::{ChatPacket, Client, ClientInformation, Event, JoinError};
pub use movement::{SprintDirection, WalkDirection};
pub use client::{start_ecs, ChatPacket, Client, ClientInformation, Event, JoinError};
pub use local_player::LocalPlayer;
pub use movement::{SprintDirection, StartSprintEvent, StartWalkEvent, WalkDirection};
pub use player::PlayerInfo;
pub use plugins::{Plugin, PluginState, PluginStates, Plugins};

View file

@ -8,8 +8,10 @@ use azalea_protocol::packets::game::{
serverbound_move_player_status_only_packet::ServerboundMovePlayerStatusOnlyPacket,
};
use azalea_world::entity::metadata::Sprinting;
use azalea_world::entity::{Attributes, MinecraftEntityId};
use azalea_world::entity::{Attributes, Entity, Jumping, MinecraftEntityId};
use azalea_world::{entity, MoveEntityError};
use bevy_ecs::event::EventReader;
use bevy_ecs::prelude::With;
use bevy_ecs::system::Query;
use std::backtrace::Backtrace;
use thiserror::Error;
@ -40,15 +42,15 @@ impl Client {
/// recommended.
pub fn set_jumping(&mut self, jumping: bool) {
let mut ecs = self.ecs.lock();
let mut physics = self.query::<&mut entity::Physics>(&mut ecs);
physics.jumping = jumping;
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 {
let mut ecs = self.ecs.lock();
let physics = self.query::<&mut entity::Physics>(&mut ecs);
physics.jumping
let jumping_ref = self.query::<&Jumping>(&mut ecs);
**jumping_ref
}
/// Sets your rotation. `y_rot` is yaw (looking to the side), `x_rot` is
@ -248,7 +250,7 @@ pub fn local_player_ai_step(
&mut entity::metadata::Sprinting,
&mut entity::Attributes,
),
&LocalPlayerInLoadedChunk,
With<LocalPlayerInLoadedChunk>,
>,
) {
println!("local_player_ai_step");
@ -289,14 +291,6 @@ pub fn local_player_ai_step(
{
set_sprinting(true, &mut sprinting, &mut attributes);
}
azalea_physics::ai_step(
&local_player.world.read(),
&mut physics,
&mut position,
&sprinting,
&attributes,
)
}
}
@ -319,14 +313,10 @@ impl Client {
/// ```
pub fn walk(&mut self, direction: WalkDirection) {
let mut ecs = self.ecs.lock();
let (mut physics_state, mut sprinting, mut attributes) =
self.query::<(&mut PhysicsState, &mut Sprinting, &mut Attributes)>(&mut ecs);
walk(
ecs.send_event(StartWalkEvent {
entity: self.entity,
direction,
&mut physics_state,
&mut sprinting,
&mut attributes,
)
});
}
/// Start sprinting in the given direction. To stop moving, call
@ -346,28 +336,49 @@ impl Client {
/// ```
pub fn sprint(&mut self, direction: SprintDirection) {
let mut ecs = self.ecs.lock();
let mut physics_state = self.query::<&mut PhysicsState>(&mut ecs);
sprint(direction, &mut physics_state);
ecs.send_event(StartSprintEvent {
entity: self.entity,
direction,
});
}
}
pub struct StartWalkEvent {
pub entity: Entity,
pub direction: WalkDirection,
}
/// Start walking in the given direction. To sprint, use
/// [`Client::sprint`]. To stop walking, call walk with
/// `WalkDirection::None`.
pub fn walk(
direction: WalkDirection,
physics_state: &mut PhysicsState,
sprinting: &mut entity::metadata::Sprinting,
attributes: &mut entity::Attributes,
pub fn walk_listener(
mut events: EventReader<StartWalkEvent>,
mut query: Query<(&mut PhysicsState, &mut Sprinting, &mut Attributes)>,
) {
physics_state.move_direction = direction;
set_sprinting(false, sprinting, attributes);
for event in events.iter() {
if let Ok((mut physics_state, mut sprinting, mut attributes)) = query.get_mut(event.entity)
{
physics_state.move_direction = event.direction;
set_sprinting(false, &mut sprinting, &mut attributes);
}
}
}
pub struct StartSprintEvent {
pub entity: Entity,
pub direction: SprintDirection,
}
/// Start sprinting in the given direction.
pub fn sprint(direction: SprintDirection, physics_state: &mut PhysicsState) {
physics_state.move_direction = WalkDirection::from(direction);
physics_state.trying_to_sprint = true;
pub fn sprint_listener(
mut query: Query<&mut PhysicsState>,
mut events: EventReader<StartSprintEvent>,
) {
for event in events.iter() {
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
@ -375,8 +386,8 @@ pub fn sprint(direction: SprintDirection, physics_state: &mut PhysicsState) {
/// Returns if the operation was successful.
fn set_sprinting(
sprinting: bool,
currently_sprinting: &mut entity::metadata::Sprinting,
attributes: &mut entity::Attributes,
currently_sprinting: &mut Sprinting,
attributes: &mut Attributes,
) -> bool {
**currently_sprinting = sprinting;
if sprinting {

View file

@ -12,6 +12,9 @@ version = "0.5.0"
azalea-block = {path = "../azalea-block", version = "^0.5.0" }
azalea-core = {path = "../azalea-core", version = "^0.5.0" }
azalea-world = {path = "../azalea-world", version = "^0.5.0" }
bevy_app = { version = "0.9.1", default-features = false }
bevy_ecs = { version = "0.9.1", default-features = false }
iyes_loopless = "0.9.1"
once_cell = "1.16.0"
parking_lot = "^0.12.1"

View file

@ -5,154 +5,195 @@ pub mod collision;
use azalea_block::{Block, BlockState};
use azalea_core::{BlockPos, Vec3};
use azalea_world::{
entity::{self},
World,
entity::{
metadata::Sprinting, move_relative, Attributes, Entity, Jumping, Physics, Position,
WorldName,
},
World, WorldContainer,
};
use bevy_app::Plugin;
use bevy_ecs::{
event::{EventReader, EventWriter},
schedule::IntoSystemDescriptor,
system::Res,
};
use bevy_ecs::{schedule::SystemSet, system::Query};
use collision::{move_colliding, MoverType};
use iyes_loopless::prelude::*;
pub struct PhysicsPlugin;
impl Plugin for PhysicsPlugin {
fn build(&self, app: &mut bevy_app::App) {
app.add_event::<ForceJumpEvent>()
.add_fixed_timestep_system_set(
"tick",
0,
SystemSet::new()
.with_system(ai_step.label("ai_step"))
.with_system(
force_jump_listener
.label("force_jump_listener")
.after("ai_step"),
)
.with_system(
travel
.label("travel")
.after("ai_step")
.after("force_jump_listener"),
),
);
}
}
/// Move the entity with the given acceleration while handling friction,
/// gravity, collisions, and some other stuff.
fn travel(
world: &World,
physics: &mut entity::Physics,
position: &mut entity::Position,
attributes: &entity::Attributes,
acceleration: &Vec3,
mut query: Query<(&mut Physics, &mut Position, &Attributes, &WorldName)>,
world_container: Res<WorldContainer>,
) {
// if !self.is_effective_ai() && !self.is_controlled_by_local_instance() {
// // this.calculateEntityAnimation(this, this instanceof FlyingAnimal);
// return;
// }
for (mut physics, mut position, attributes, world_name) in &mut query {
let world_lock = world_container
.get(world_name)
.expect("All entities should be in a valid world");
let world = world_lock.read();
// if !self.is_effective_ai() && !self.is_controlled_by_local_instance() {
// // this.calculateEntityAnimation(this, this instanceof FlyingAnimal);
// return;
// }
let gravity: f64 = 0.08;
let gravity: f64 = 0.08;
// TODO: slow falling effect
// let is_falling = self.delta.y <= 0.;
// TODO: slow falling effect
// let is_falling = self.delta.y <= 0.;
// TODO: fluids
// TODO: fluids
// TODO: elytra
// TODO: elytra
let block_pos_below = get_block_pos_below_that_affects_movement(position);
let block_pos_below = get_block_pos_below_that_affects_movement(&position);
let block_state_below = world
.chunks
.get_block_state(&block_pos_below)
.unwrap_or(BlockState::Air);
let block_below: Box<dyn Block> = block_state_below.into();
let block_friction = block_below.behavior().friction;
let block_state_below = world
.chunks
.get_block_state(&block_pos_below)
.unwrap_or(BlockState::Air);
let block_below: Box<dyn Block> = block_state_below.into();
let block_friction = block_below.behavior().friction;
let inertia = if physics.on_ground {
block_friction * 0.91
} else {
0.91
};
// this applies the current delta
let mut movement = handle_relative_friction_and_calculate_movement(
acceleration,
block_friction,
world,
physics,
position,
attributes,
);
movement.y -= gravity;
// if (this.shouldDiscardFriction()) {
// this.setDeltaMovement(movement.x, yMovement, movement.z);
// } else {
// this.setDeltaMovement(movement.x * (double)inertia, yMovement *
// 0.9800000190734863D, movement.z * (double)inertia); }
// if should_discard_friction(self) {
if false {
physics.delta = movement;
} else {
physics.delta = Vec3 {
x: movement.x * inertia as f64,
y: movement.y * 0.98f64,
z: movement.z * inertia as f64,
let inertia = if physics.on_ground {
block_friction * 0.91
} else {
0.91
};
// this applies the current delta
let mut movement = handle_relative_friction_and_calculate_movement(
block_friction,
&world,
&mut physics,
&mut position,
attributes,
);
movement.y -= gravity;
// if (this.shouldDiscardFriction()) {
// this.setDeltaMovement(movement.x, yMovement, movement.z);
// } else {
// this.setDeltaMovement(movement.x * (double)inertia, yMovement *
// 0.9800000190734863D, movement.z * (double)inertia); }
// if should_discard_friction(self) {
if false {
physics.delta = movement;
} else {
physics.delta = Vec3 {
x: movement.x * inertia as f64,
y: movement.y * 0.98f64,
z: movement.z * inertia as f64,
};
}
}
}
/// applies air resistance, calls self.travel(), and some other random
/// stuff.
pub fn ai_step(
world: &World,
physics: &mut entity::Physics,
position: &mut entity::Position,
sprinting: &entity::metadata::Sprinting,
attributes: &entity::Attributes,
mut query: Query<
(Entity, &mut Physics, &Jumping),
// TODO: ai_step should only run for players in loaded chunks
// With<LocalPlayerInLoadedChunk> maybe there should be an InLoadedChunk/InUnloadedChunk
// component?
>,
mut force_jump_events: EventWriter<ForceJumpEvent>,
) {
// vanilla does movement interpolation here, doesn't really matter much for a
// bot though
for (entity, mut physics, jumping) in &mut query {
// vanilla does movement interpolation here, doesn't really matter much for a
// bot though
if physics.delta.x.abs() < 0.003 {
physics.delta.x = 0.;
}
if physics.delta.y.abs() < 0.003 {
physics.delta.y = 0.;
}
if physics.delta.z.abs() < 0.003 {
physics.delta.z = 0.;
}
if physics.delta.x.abs() < 0.003 {
physics.delta.x = 0.;
}
if physics.delta.y.abs() < 0.003 {
physics.delta.y = 0.;
}
if physics.delta.z.abs() < 0.003 {
physics.delta.z = 0.;
}
if physics.jumping {
// TODO: jumping in liquids and jump delay
if **jumping {
// TODO: jumping in liquids and jump delay
if physics.on_ground {
jump_from_ground(world, physics, position, sprinting);
if physics.on_ground {
force_jump_events.send(ForceJumpEvent(entity));
}
}
physics.xxa *= 0.98;
physics.zza *= 0.98;
// TODO: freezing, pushEntities, drowning damage (in their own systems,
// after `travel`)
}
}
/// Jump even if we aren't on the ground.
pub struct ForceJumpEvent(pub Entity);
fn force_jump_listener(
mut query: Query<(&mut Physics, &Position, &Sprinting, &WorldName)>,
world_container: Res<WorldContainer>,
mut events: EventReader<ForceJumpEvent>,
) {
for event in events.iter() {
if let Ok((mut physics, position, sprinting, world_name)) = query.get_mut(event.0) {
let world_lock = world_container
.get(world_name)
.expect("All entities should be in a valid world");
let world = world_lock.read();
let jump_power: f64 = jump_power(&world, position) as f64 + jump_boost_power();
let old_delta_movement = physics.delta;
physics.delta = Vec3 {
x: old_delta_movement.x,
y: jump_power,
z: old_delta_movement.z,
};
if **sprinting {
// sprint jumping gives some extra velocity
let y_rot = physics.y_rot * 0.017453292;
physics.delta += Vec3 {
x: (-f32::sin(y_rot) * 0.2) as f64,
y: 0.,
z: (f32::cos(y_rot) * 0.2) as f64,
};
}
physics.has_impulse = true;
}
}
physics.xxa *= 0.98;
physics.zza *= 0.98;
travel(
world,
physics,
position,
attributes,
&Vec3 {
x: physics.xxa as f64,
y: physics.yya as f64,
z: physics.zza as f64,
},
);
// freezing
// pushEntities
// drowning damage
}
fn jump_from_ground(
world: &World,
physics: &mut entity::Physics,
position: &entity::Position,
sprinting: &entity::metadata::Sprinting,
) {
let jump_power: f64 = jump_power(world, position) as f64 + jump_boost_power();
let old_delta_movement = physics.delta;
physics.delta = Vec3 {
x: old_delta_movement.x,
y: jump_power,
z: old_delta_movement.z,
};
if **sprinting {
let y_rot = physics.y_rot * 0.017453292;
physics.delta += Vec3 {
x: (-f32::sin(y_rot) * 0.2) as f64,
y: 0.,
z: (f32::cos(y_rot) * 0.2) as f64,
};
}
physics.has_impulse = true;
}
fn get_block_pos_below_that_affects_movement(position: &entity::Position) -> BlockPos {
fn get_block_pos_below_that_affects_movement(position: &Position) -> BlockPos {
BlockPos::new(
position.x.floor() as i32,
// TODO: this uses bounding_box.min_y instead of position.y
@ -162,17 +203,20 @@ fn get_block_pos_below_that_affects_movement(position: &entity::Position) -> Blo
}
fn handle_relative_friction_and_calculate_movement(
acceleration: &Vec3,
block_friction: f32,
world: &World,
physics: &mut entity::Physics,
position: &mut entity::Position,
attributes: &entity::Attributes,
physics: &mut Physics,
position: &mut Position,
attributes: &Attributes,
) -> Vec3 {
entity::move_relative(
move_relative(
physics,
get_friction_influenced_speed(physics, attributes, block_friction),
acceleration,
&Vec3 {
x: physics.xxa as f64,
y: physics.yya as f64,
z: physics.zza as f64,
},
);
// entity.delta = entity.handle_on_climbable(entity.delta);
move_colliding(
@ -197,11 +241,7 @@ fn handle_relative_friction_and_calculate_movement(
// private float getFrictionInfluencedSpeed(float friction) {
// return this.onGround ? this.getSpeed() * (0.21600002F / (friction *
// friction * friction)) : this.flyingSpeed; }
fn get_friction_influenced_speed(
physics: &entity::Physics,
attributes: &entity::Attributes,
friction: f32,
) -> f32 {
fn get_friction_influenced_speed(physics: &Physics, attributes: &Attributes, friction: f32) -> f32 {
// TODO: have speed & flying_speed fields in entity
if physics.on_ground {
let speed: f32 = attributes.speed.calculate() as f32;
@ -214,7 +254,7 @@ fn get_friction_influenced_speed(
/// Returns the what the entity's jump should be multiplied by based on the
/// block they're standing on.
fn block_jump_factor(world: &World, position: &entity::Position) -> f32 {
fn block_jump_factor(world: &World, position: &Position) -> f32 {
let block_at_pos = world.chunks.get_block_state(&position.into());
let block_below = world
.chunks
@ -242,7 +282,7 @@ fn block_jump_factor(world: &World, position: &entity::Position) -> f32 {
// public double getJumpBoostPower() {
// return this.hasEffect(MobEffects.JUMP) ? (double)(0.1F *
// (float)(this.getEffect(MobEffects.JUMP).getAmplifier() + 1)) : 0.0D; }
fn jump_power(world: &World, position: &entity::Position) -> f32 {
fn jump_power(world: &World, position: &Position) -> f32 {
0.42 * block_jump_factor(world, position)
}

View file

@ -19,6 +19,7 @@ bevy_app = { version = "0.9.1", default-features = false }
bevy_ecs = {version = "0.9.1", default-features = false}
derive_more = {version = "0.99.17", features = ["deref", "deref_mut"]}
enum-as-inner = "0.5.1"
iyes_loopless = "0.9.1"
log = "0.4.17"
nohash-hasher = "0.2.0"
once_cell = "1.16.0"

View file

@ -177,6 +177,13 @@ impl From<&LastSentPosition> for BlockPos {
#[derive(Component, Clone, Debug, PartialEq, Deref, DerefMut)]
pub struct WorldName(ResourceLocation);
/// A component for entities that can jump.
///
/// If this is true, the entity will try to jump every tick. (It's equivalent to
/// the space key being held in vanilla.)
#[derive(Debug, Component, Deref, DerefMut)]
pub struct Jumping(bool);
/// The physics data relating to the entity, such as position, velocity, and
/// bounding box.
#[derive(Debug, Component)]
@ -205,10 +212,6 @@ pub struct Physics {
/// unlike dimensions.
pub bounding_box: AABB,
/// Whether the entity will try to jump every tick
/// (equivalent to the space key being held down in vanilla).
pub jumping: bool,
pub has_impulse: bool,
}
@ -251,6 +254,7 @@ pub struct EntityBundle {
pub last_sent_position: LastSentPosition,
pub physics: Physics,
pub attributes: Attributes,
pub jumping: Jumping,
}
impl EntityBundle {
@ -293,8 +297,6 @@ impl EntityBundle {
dimensions,
has_impulse: false,
jumping: false,
},
attributes: Attributes {
@ -302,6 +304,8 @@ impl EntityBundle {
// entities have different defaults
speed: AttributeInstance::new(0.1),
},
jumping: Jumping(false),
}
}
}

View file

@ -18,7 +18,6 @@ use uuid::Uuid;
pub struct EntityPlugin;
impl Plugin for EntityPlugin {
fn build(&self, app: &mut App) {
// Since it's PostUpdate, these will run after every tick or packet
app.add_system_set(
SystemSet::new()
.after("tick")

View file

@ -31,6 +31,9 @@ priority-queue = "1.3.0"
thiserror = "^1.0.37"
tokio = "^1.21.2"
uuid = "1.2.2"
bevy_app = { version = "0.9.1", default-features = false }
bevy_ecs = { version = "0.9.1", default-features = false }
iyes_loopless = "0.9.1"
[dev-dependencies]
anyhow = "^1.0.65"

View file

@ -1,56 +1,97 @@
use crate::{Client, Event};
use async_trait::async_trait;
use azalea_core::Vec3;
use parking_lot::Mutex;
use std::{f64::consts::PI, sync::Arc};
use azalea_world::entity::{set_rotation, Entity, Jumping, Physics, Position};
use bevy_app::App;
use bevy_ecs::schedule::IntoSystemDescriptor;
use bevy_ecs::{event::EventReader, prelude::Component, schedule::SystemSet, system::Query};
use iyes_loopless::prelude::*;
use std::f64::consts::PI;
#[derive(Clone, Default)]
pub struct Plugin;
impl crate::Plugin for Plugin {
type State = State;
fn build(&self) -> State {
State::default()
impl bevy_app::Plugin for Plugin {
fn build(&self, app: &mut App) {
app.add_event::<LookAtEvent>()
.add_event::<JumpEvent>()
.add_system(look_at_listener)
.add_system(jump_listener.label("jump_listener"))
.add_fixed_timestep_system_set(
"tick",
0,
// make sure tick_jump happens the tick after a jump event
SystemSet::new().with_system(tick_jump.before("jump_listener")),
);
}
}
#[derive(Default, Clone)]
pub struct State {
jumping_once: Arc<Mutex<bool>>,
/// Component for all bots.
#[derive(Default, Component)]
pub struct Bot {
jumping_once: bool,
}
#[async_trait]
impl crate::PluginState for State {
async fn handle(self: Box<Self>, event: Event, mut bot: Client) {
if let Event::Tick = event {
if *self.jumping_once.lock() && bot.jumping() {
*self.jumping_once.lock() = false;
bot.set_jumping(false);
}
fn tick_jump(mut query: Query<(&mut Jumping, &mut Bot)>) {
for (mut jumping, mut bot) in &mut query {
if bot.jumping_once && **jumping {
bot.jumping_once = false;
**jumping = false;
}
}
}
pub trait BotTrait {
pub trait BotClientExt {
fn jump(&mut self);
fn look_at(&mut self, pos: &Vec3);
fn look_at(&mut self, pos: Vec3);
}
impl BotTrait for azalea_client::Client {
impl BotClientExt for azalea_client::Client {
/// Queue a jump for the next tick.
fn jump(&mut self) {
self.set_jumping(true);
let state = self.plugins.get::<State>().unwrap().clone();
*state.jumping_once.lock() = true;
let mut ecs = self.ecs.lock();
ecs.send_event(JumpEvent(self.entity));
}
/// Turn the bot's head to look at the coordinate in the world.
fn look_at(&mut self, pos: &Vec3) {
let (y_rot, x_rot) = direction_looking_at(self.entity().pos(), pos);
self.set_rotation(y_rot, x_rot);
fn look_at(&mut self, position: Vec3) {
let mut ecs = self.ecs.lock();
ecs.send_event(LookAtEvent {
entity: self.entity,
position,
});
}
}
/// Event to jump once.
pub struct JumpEvent(pub Entity);
fn jump_listener(mut query: Query<(&mut Jumping, &mut Bot)>, mut events: EventReader<JumpEvent>) {
for event in events.iter() {
if let Ok((mut jumping, mut bot)) = query.get_mut(event.0) {
**jumping = true;
bot.jumping_once = true;
}
}
}
/// Make an entity look towards a certain position in the world.
pub struct LookAtEvent {
pub entity: Entity,
/// The position we want the entity to be looking at.
pub position: Vec3,
}
fn look_at_listener(
mut events: EventReader<LookAtEvent>,
mut query: Query<(&Position, &mut Physics)>,
) {
for event in events.iter() {
if let Ok((position, mut physics)) = query.get_mut(event.entity) {
let (y_rot, x_rot) = direction_looking_at(&position, &event.position);
set_rotation(&mut physics, y_rot, x_rot);
}
}
}
/// Return the (y_rot, x_rot) that would make a client at `current` be looking
/// at `target`.
fn direction_looking_at(current: &Vec3, target: &Vec3) -> (f32, f32) {
// borrowed from mineflayer's Bot.lookAt because i didn't want to do math
let delta = target - current;

View file

@ -89,6 +89,7 @@ pub use azalea_block::*;
pub use azalea_client::*;
pub use azalea_core::{BlockPos, Vec3};
pub use azalea_registry::EntityKind;
pub use azalea_world::{entity, World};
pub use start::{start, Options};
pub use swarm::*;

View file

@ -1,57 +1,73 @@
mod moves;
mod mtdstarlite;
use crate::{prelude::*, SprintDirection, WalkDirection};
use crate::{Client, Event};
use async_trait::async_trait;
use crate::bot::{JumpEvent, LookAtEvent};
use crate::{SprintDirection, WalkDirection};
use azalea_client::{LocalPlayer, StartSprintEvent, StartWalkEvent};
use azalea_core::{BlockPos, CardinalDirection};
use azalea_world::entity::{Entity, Physics, Position, WorldName};
use azalea_world::WorldContainer;
use bevy_ecs::event::EventReader;
use bevy_ecs::schedule::SystemSet;
use bevy_ecs::system::{Query, Res};
use bevy_ecs::{component::Component, event::EventWriter};
use iyes_loopless::prelude::*;
use log::{debug, error};
use mtdstarlite::Edge;
pub use mtdstarlite::MTDStarLite;
use parking_lot::Mutex;
use std::collections::VecDeque;
use std::sync::Arc;
#[derive(Clone, Default)]
pub struct Plugin;
impl crate::Plugin for Plugin {
type State = State;
fn build(&self) -> State {
State::default()
impl bevy_app::Plugin for Plugin {
fn build(&self, app: &mut bevy_app::App) {
app.add_fixed_timestep_system_set(
"tick",
0,
SystemSet::new()
.with_system(tick_execute_path)
.with_system(goto_listener),
);
}
}
#[derive(Default, Clone)]
pub struct State {
// pathfinder: Option<MTDStarLite<Node, f32>>,
pub path: Arc<Mutex<VecDeque<Node>>>,
/// A component that makes this entity able to pathfind.
#[derive(Component)]
pub struct Pathfinder {
pub path: VecDeque<Node>,
}
#[async_trait]
impl crate::PluginState for State {
async fn handle(self: Box<Self>, event: Event, mut bot: Client) {
if let Event::Tick = event {
let mut path = self.path.lock();
pub trait PathfinderClientExt {
fn goto(&self, goal: impl Goal + Send + Sync + 'static);
}
if !path.is_empty() {
tick_execute_path(&mut bot, &mut path);
}
}
impl PathfinderClientExt for azalea_client::Client {
fn goto(&self, goal: impl Goal + Send + Sync + 'static) {
self.ecs.lock().send_event(GotoEvent {
entity: self.entity,
goal: Box::new(goal),
})
}
}
pub trait Trait {
fn goto(&self, goal: impl Goal);
pub struct GotoEvent {
pub entity: Entity,
pub goal: Box<dyn Goal + Send + Sync>,
}
impl Trait for azalea_client::Client {
fn goto(&self, goal: impl Goal) {
fn goto_listener(
mut events: EventReader<GotoEvent>,
mut query: Query<(&Position, &WorldName, &mut Pathfinder)>,
world_container: Res<WorldContainer>,
) {
for event in events.iter() {
let (position, world_name, mut pathfinder) = query
.get_mut(event.entity)
.expect("Called goto on an entity that can't pathfind");
let start = Node {
pos: BlockPos::from(self.entity().pos()),
pos: BlockPos::from(position),
vertical_vel: VerticalVel::None,
};
let end = goal.goal_node();
let end = event.goal.goal_node();
debug!("start: {start:?}, end: {end:?}");
let possible_moves: Vec<&dyn moves::Move> = vec![
@ -79,7 +95,10 @@ impl Trait for azalea_client::Client {
let successors = |node: &Node| {
let mut edges = Vec::new();
let world = &self.world.read().shared;
let world_lock = world_container
.get(world_name)
.expect("Entity tried to pathfind but the entity isn't in a valid world");
let world = world_lock.read();
for possible_move in possible_moves.iter() {
edges.push(Edge {
target: possible_move.next_node(node),
@ -92,10 +111,10 @@ impl Trait for azalea_client::Client {
let mut pf = MTDStarLite::new(
start,
end,
|n| goal.heuristic(n),
|n| event.goal.heuristic(n),
successors,
successors,
|n| goal.success(n),
|n| event.goal.success(n),
);
let start = std::time::Instant::now();
@ -104,43 +123,58 @@ impl Trait for azalea_client::Client {
debug!("path: {p:?}");
debug!("time: {:?}", end - start);
let state = self
.plugins
.get::<State>()
.expect("Pathfinder plugin not installed!")
.clone();
// convert the Option<Vec<Node>> to a VecDeque<Node>
if let Some(p) = p {
*state.path.lock() = p.into_iter().collect();
pathfinder.path = p.into_iter().collect();
} else {
error!("no path found");
}
}
}
fn tick_execute_path(bot: &mut Client, path: &mut VecDeque<Node>) {
let target = if let Some(target) = path.front() {
target
} else {
return;
};
let center = target.pos.center();
// println!("going to {center:?} (at {pos:?})", pos = bot.entity().pos());
bot.look_at(&center);
bot.sprint(SprintDirection::Forward);
// check if we should jump
if target.pos.y > bot.entity().pos().y.floor() as i32 {
bot.jump();
}
fn tick_execute_path(
mut query: Query<(Entity, &LocalPlayer, &mut Pathfinder, &Position, &Physics)>,
mut look_at_events: EventWriter<LookAtEvent>,
mut sprint_events: EventWriter<StartSprintEvent>,
mut walk_events: EventWriter<StartWalkEvent>,
mut jump_events: EventWriter<JumpEvent>,
) {
for (entity, local_player, mut pathfinder, position, physics) in &mut query {
loop {
let target = if let Some(target) = pathfinder.path.front() {
target
} else {
return;
};
let center = target.pos.center();
// println!("going to {center:?} (at {pos:?})", pos = bot.entity().pos());
look_at_events.send(LookAtEvent {
entity,
position: center,
});
sprint_events.send(StartSprintEvent {
entity,
direction: SprintDirection::Forward,
});
// check if we should jump
if target.pos.y > position.y.floor() as i32 {
jump_events.send(JumpEvent(entity));
}
if target.is_reached(&bot.entity()) {
// println!("ok target {target:?} reached");
path.pop_front();
if path.is_empty() {
bot.walk(WalkDirection::None);
if target.is_reached(position, physics) {
// println!("ok target {target:?} reached");
pathfinder.path.pop_front();
if pathfinder.path.is_empty() {
walk_events.send(StartWalkEvent {
entity,
direction: WalkDirection::None,
});
}
// tick again, maybe we already reached the next node!
} else {
break;
}
}
// tick again, maybe we already reached the next node!
tick_execute_path(bot, path);
}
}
@ -171,7 +205,7 @@ pub trait Goal {
impl Node {
/// Returns whether the entity is at the node and should start going to the
/// next node.
pub fn is_reached(&self, entity: &EntityData) -> bool {
pub fn is_reached(&self, position: &Position, physics: &Physics) -> bool {
// println!(
// "entity.delta.y: {} {:?}=={:?}, self.vertical_vel={:?}",
// entity.delta.y,
@ -179,11 +213,11 @@ impl Node {
// self.pos,
// self.vertical_vel
// );
BlockPos::from(entity.pos()) == self.pos
BlockPos::from(position) == self.pos
&& match self.vertical_vel {
VerticalVel::NoneMidair => entity.delta.y > -0.1 && entity.delta.y < 0.1,
VerticalVel::None => entity.on_ground,
VerticalVel::FallingLittle => entity.delta.y < -0.1,
VerticalVel::NoneMidair => physics.delta.y > -0.1 && physics.delta.y < 0.1,
VerticalVel::None => physics.on_ground,
VerticalVel::FallingLittle => physics.delta.y < -0.1,
}
}
}

View file

@ -1,7 +1,7 @@
//! The Azalea prelude. Things that are necessary for a bare-bones bot are
//! re-exported here.
pub use crate::bot::BotTrait;
pub use crate::pathfinder::Trait;
pub use crate::bot::BotClientExt;
pub use crate::pathfinder::PathfinderClientExt;
pub use crate::{plugins, swarm_plugins, Plugin};
pub use azalea_client::{Account, Client, Event};

View file

@ -1,7 +1,7 @@
use crate::{bot, pathfinder, HandleFn};
use crate::HandleFn;
use azalea_client::{Account, Client, Plugins};
use azalea_protocol::ServerAddress;
use std::{future::Future, sync::Arc};
use std::future::Future;
use thiserror::Error;
/// A helper macro that generates a [`Plugins`] struct from a list of objects
@ -112,14 +112,9 @@ pub async fn start<
Err(_) => return Err(StartError::InvalidAddress),
};
let (mut bot, mut rx) = Client::join(&options.account, address).await?;
let (bot, mut rx) = Client::join(&options.account, address).await?;
let mut plugins = options.plugins;
// DEFAULT PLUGINS
plugins.add(bot::Plugin);
plugins.add(pathfinder::Plugin);
bot.plugins = Arc::new(plugins.build());
let plugins = options.plugins;
let state = options.state;

View file

@ -3,10 +3,10 @@ mod chat;
mod plugins;
pub use self::plugins::*;
use crate::{bot, HandleFn};
use azalea_client::{Account, ChatPacket, Client, Event, JoinError, Plugins};
use crate::HandleFn;
use azalea_client::{start_ecs, Account, ChatPacket, Client, Event, JoinError, Plugins};
use azalea_protocol::{
connect::{Connection, ConnectionError},
connect::ConnectionError,
resolver::{self, ResolverError},
ServerAddress,
};
@ -53,11 +53,14 @@ pub struct Swarm<S> {
resolved_address: SocketAddr,
address: ServerAddress,
pub worlds: Arc<RwLock<WorldContainer>>,
pub ecs_lock: Arc<Mutex<bevy_ecs::world::World>>,
/// Plugins that are set for new bots
plugins: Plugins,
bots_tx: UnboundedSender<(Option<Event>, (Client, S))>,
swarm_tx: UnboundedSender<SwarmEvent>,
run_schedule_sender: UnboundedSender<()>,
}
/// An event about something that doesn't have to do with a single bot.
@ -230,18 +233,15 @@ pub async fn start_swarm<
let world_container = Arc::new(RwLock::new(WorldContainer::default()));
let mut plugins = options.plugins;
let swarm_plugins = options.swarm_plugins;
// DEFAULT CLIENT PLUGINS
plugins.add(bot::Plugin);
plugins.add(crate::pathfinder::Plugin);
// DEFAULT SWARM PLUGINS
let plugins = options.plugins;
// we can't modify the swarm plugins after this
let (bots_tx, mut bots_rx) = mpsc::unbounded_channel();
let (swarm_tx, mut swarm_rx) = mpsc::unbounded_channel();
let (run_schedule_sender, run_schedule_receiver) = mpsc::unbounded_channel();
let ecs_lock = start_ecs(run_schedule_receiver, run_schedule_sender.clone());
let mut swarm = Swarm {
bot_datas: Arc::new(Mutex::new(Vec::new())),
@ -253,6 +253,9 @@ pub async fn start_swarm<
bots_tx,
swarm_tx: swarm_tx.clone(),
ecs_lock,
run_schedule_sender,
};
{
@ -264,8 +267,6 @@ pub async fn start_swarm<
});
}
let swarm_plugins = swarm_plugins.build();
let mut swarm_clone = swarm.clone();
let join_task = tokio::spawn(async move {
if let Some(join_delay) = options.join_delay {
@ -294,14 +295,10 @@ pub async fn start_swarm<
let swarm_state = options.swarm_state;
let mut internal_state = InternalSwarmState::default();
// Watch swarm_rx and send those events to the plugins and swarm_handle.
// Watch swarm_rx and send those events to the swarm_handle.
let swarm_clone = swarm.clone();
let swarm_plugins_clone = swarm_plugins.clone();
tokio::spawn(async move {
while let Some(event) = swarm_rx.recv().await {
for plugin in swarm_plugins_clone.clone().into_iter() {
tokio::spawn(plugin.handle(event.clone(), swarm_clone.clone()));
}
tokio::spawn((options.swarm_handle)(
swarm_clone.clone(),
event,
@ -312,14 +309,8 @@ pub async fn start_swarm<
// bot events
while let Some((Some(event), (bot, state))) = bots_rx.recv().await {
// bot event handling
let cloned_plugins = (*bot.plugins).clone();
for plugin in cloned_plugins.into_iter() {
tokio::spawn(plugin.handle(event.clone(), bot.clone()));
}
// swarm event handling
// remove this #[allow] when more checks are added
// TODO: actually it'd be better to just have this in a system
#[allow(clippy::single_match)]
match &event {
Event::Login => {
@ -330,7 +321,6 @@ pub async fn start_swarm<
}
_ => {}
}
tokio::spawn((options.handle)(bot, event, state));
}
@ -346,17 +336,19 @@ where
/// Add a new account to the swarm. You can remove it later by calling
/// [`Client::disconnect`].
pub async fn add(&mut self, account: &Account, state: S) -> Result<Client, JoinError> {
let conn = Connection::new(&self.resolved_address).await?;
let (conn, game_profile) = Client::handshake(conn, account, &self.address.clone()).await?;
// tx is moved to the bot so it can send us events
// rx is used to receive events from the bot
let (tx, mut rx) = mpsc::channel(1);
let mut bot = Client::new(game_profile, conn, Some(self.worlds.clone()));
tx.send(Event::Init).await.expect("Failed to send event");
bot.start_tasks(tx);
bot.plugins = Arc::new(self.plugins.clone().build());
// An event that causes the schedule to run. This is only used internally.
// let (run_schedule_sender, run_schedule_receiver) = mpsc::unbounded_channel();
// let ecs_lock = start_ecs(run_schedule_receiver, run_schedule_sender.clone());
let (bot, mut rx) = Client::start_client(
self.ecs_lock.clone(),
account,
&self.address,
&self.resolved_address,
self.run_schedule_sender.clone(),
)
.await?;
let cloned_bots_tx = self.bots_tx.clone();
let cloned_bot = bot.clone();
@ -432,7 +424,7 @@ where
/// }
/// ```
fn into_iter(self) -> Self::IntoIter {
self.bot_datas.clone().lock().into_iter()
self.bot_datas.lock().clone().into_iter()
}
}

View file

@ -1,3 +1,4 @@
use azalea::entity::metadata::Player;
use azalea::pathfinder::BlockPosGoal;
// use azalea::ClientInformation;
use azalea::{prelude::*, BlockPos, Swarm, SwarmEvent, WalkDirection};
@ -79,20 +80,24 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
// .await?;
}
Event::Login => {
bot.chat("Hello world").await?;
bot.chat("Hello world");
}
Event::Chat(m) => {
if m.content() == bot.profile.name {
bot.chat("Bye").await?;
bot.chat("Bye");
tokio::time::sleep(Duration::from_millis(50)).await;
bot.disconnect().await?;
bot.disconnect();
}
let Some(sender) = m.username() else {
return Ok(())
};
let mut ecs = bot.ecs.lock();
let entity = bot
.world()
.entity_by(|e| e.kind() == azalea::EntityKind::Player && e.name() == Some(sender));
.ecs
.lock()
.query::<&Player>()
.iter(&mut ecs)
.find(|e| e.name() == Some(sender));
// let entity = None;
if let Some(entity) = entity {
if m.content() == "goto" {
@ -119,7 +124,7 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
Event::Death(_) => {
bot.write_packet(ServerboundClientCommandPacket {
action: azalea_protocol::packets::game::serverbound_client_command_packet::Action::PerformRespawn,
}.get()).await?;
}.get());
}
_ => {}
}
@ -144,7 +149,7 @@ async fn swarm_handle(
for (name, world) in &swarm.worlds.read().worlds {
println!("world name: {}", name);
if let Some(w) = world.upgrade() {
for chunk_pos in w.chunks.read().chunks.values() {
for chunk_pos in w.read().chunks.chunks.values() {
println!("chunk: {:?}", chunk_pos);
}
} else {
@ -154,7 +159,7 @@ async fn swarm_handle(
}
if m.message().to_string() == "<py5> hi" {
for (bot, _) in swarm {
bot.chat("hello").await?;
bot.chat("hello");
}
}
}