From 0d3a091c232d409939db82dfb30f700e57583c85 Mon Sep 17 00:00:00 2001 From: mat Date: Sun, 5 Feb 2023 14:31:52 -0600 Subject: improve docs --- azalea-client/src/account.rs | 6 +- azalea-client/src/client.rs | 36 ++++--- azalea-client/src/entity_query.rs | 9 ++ azalea-client/src/local_player.rs | 24 +++-- azalea-client/src/movement.rs | 4 + azalea-client/src/packet_handling.rs | 42 +++++--- azalea-client/src/task_pool.rs | 2 +- azalea-core/src/position.rs | 6 ++ azalea-ecs/src/lib.rs | 14 ++- azalea-world/src/entity/attributes.rs | 2 +- azalea-world/src/entity/data.rs | 1 + azalea-world/src/entity/mod.rs | 2 +- azalea-world/src/entity_info.rs | 31 ------ azalea/README.md | 22 ++-- azalea/examples/matbot.rs | 194 +++++++++++++++++++++++++++++++++ azalea/examples/matbot/main.rs | 195 ---------------------------------- azalea/src/lib.rs | 9 ++ 17 files changed, 315 insertions(+), 284 deletions(-) create mode 100644 azalea/examples/matbot.rs delete mode 100644 azalea/examples/matbot/main.rs diff --git a/azalea-client/src/account.rs b/azalea-client/src/account.rs index 3c2c7d1b..b6073209 100755 --- a/azalea-client/src/account.rs +++ b/azalea-client/src/account.rs @@ -8,7 +8,8 @@ use uuid::Uuid; /// Something that can join Minecraft servers. /// -/// To join a server using this account, use [`crate::Client::join`]. +/// To join a server using this account, use [`Client::join`] or +/// [`azalea::ClientBuilder`]. /// /// # Examples /// @@ -21,6 +22,9 @@ use uuid::Uuid; /// // or Account::offline("example"); /// # } /// ``` +/// +/// [`Client::join`]: crate::Client::join +/// [`azalea::ClientBuilder`]: https://docs.rs/azalea/latest/azalea/struct.ClientBuilder.html #[derive(Clone, Debug)] pub struct Account { /// The Minecraft username of the account. diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index bc1d8d62..ddeeeef3 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -42,7 +42,7 @@ use azalea_protocol::{ }, resolver, ServerAddress, }; -use azalea_world::{EntityPlugin, Local, PartialWorld, World, WorldContainer}; +use azalea_world::{entity::WorldName, EntityPlugin, Local, PartialWorld, World, WorldContainer}; use log::{debug, error}; use parking_lot::{Mutex, RwLock}; use std::{collections::HashMap, fmt::Debug, io, net::SocketAddr, sync::Arc}; @@ -52,8 +52,13 @@ use uuid::Uuid; pub type ClientInformation = ServerboundClientInformationPacket; -/// Client has the things that a user interacting with the library will want. +/// `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`]. +/// +/// To make a new client, use either [`azalea::ClientBuilder`] or +/// [`Client::join`]. +/// +/// [`azalea::ClientBuilder`]: https://docs.rs/azalea/latest/azalea/struct.ClientBuilder.html #[derive(Clone)] pub struct Client { /// The [`GameProfile`] for our client. This contains your username, UUID, @@ -362,6 +367,17 @@ impl Client { /// Get a component from this client. This will clone the component and /// return it. + /// + /// # Panics + /// + /// This will panic if the component doesn't exist on the client. + /// + /// # Examples + /// + /// ``` + /// # fn example(client: &azalea::Client) { + /// let world_name = client.component::(); + /// # } pub fn component(&self) -> T { self.query::<&T>(&mut self.ecs.lock()).clone() } @@ -373,17 +389,8 @@ impl Client { /// If the client using a shared world, then the shared world will be a /// superset of the client's world. pub fn world(&self) -> Arc> { - let mut ecs = self.ecs.lock(); - - let world_name = { - let local_player = self.local_player(&mut ecs); - local_player - .world_name - .as_ref() - .expect("World name must be known if we're doing Client::world") - .clone() - }; - + let world_name = self.component::(); + let ecs = self.ecs.lock(); let world_container = ecs.resource::(); world_container.get(&world_name).unwrap() } @@ -391,7 +398,8 @@ impl Client { /// Returns whether we have a received the login packet yet. pub fn logged_in(&self) -> bool { // the login packet tells us the world name - self.local_player(&mut self.ecs.lock()).world_name.is_some() + self.query::>(&mut self.ecs.lock()) + .is_some() } /// Tell the server we changed our game options (i.e. render distance, main diff --git a/azalea-client/src/entity_query.rs b/azalea-client/src/entity_query.rs index 0e486741..7ac0e325 100644 --- a/azalea-client/src/entity_query.rs +++ b/azalea-client/src/entity_query.rs @@ -12,6 +12,15 @@ use crate::Client; impl Client { /// A convenience function for getting components of our player's entity. + /// + /// # Examples + /// ``` + /// # fn example(mut client: azalea_client::Client) { + /// let is_logged_in = client + /// .query::>(&mut client.ecs.lock()) + /// .is_some(); + /// # } + /// ``` pub fn query<'w, Q: WorldQuery>(&self, ecs: &'w mut Ecs) -> ::Item<'w> { ecs.query::() .get_mut(ecs, self.entity) diff --git a/azalea-client/src/local_player.rs b/azalea-client/src/local_player.rs index 0165a5f5..a6921d76 100644 --- a/azalea-client/src/local_player.rs +++ b/azalea-client/src/local_player.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, io, sync::Arc}; use azalea_auth::game_profile::GameProfile; -use azalea_core::{ChunkPos, ResourceLocation}; +use azalea_core::ChunkPos; use azalea_ecs::component::Component; use azalea_ecs::entity::Entity; use azalea_ecs::{query::Added, system::Query}; @@ -21,22 +21,29 @@ use crate::{ ClientInformation, PlayerInfo, WalkDirection, }; -/// A player that you control that is currently in a Minecraft server. +/// This is a component for our local player entities that are probably in a +/// world. If you have access to a [`Client`], you probably don't need to care +/// about this since `Client` gives you access to everything here. +/// +/// You can also use the [`Local`] marker component for queries if you're only +/// checking for a local player and don't need the contents of this component. +/// +/// [`Local`]: azalea_world::Local +/// [`Client`]: crate::Client #[derive(Component)] pub struct LocalPlayer { - pub packet_writer: mpsc::UnboundedSender, - + packet_writer: mpsc::UnboundedSender, + /// Some of the "settings" for this client that are sent to the server, such + /// as render distance. pub client_information: ClientInformation, - /// A map of player uuids to their information in the tab list + /// A map of player UUIDs to their information in the tab list pub players: HashMap, - /// The partial world is the world this client currently has loaded. It has /// a limited render distance. pub partial_world: Arc>, /// The world is the combined [`PartialWorld`]s of all clients in the same /// world. (Only relevant if you're using a shared world, i.e. a swarm) pub world: Arc>, - pub world_name: Option, /// A list of async tasks that are running and will stop running when this /// LocalPlayer is dropped or disconnected with [`Self::disconnect`] @@ -93,13 +100,12 @@ impl LocalPlayer { client_information.view_distance.into(), Some(entity), ))), - world_name: None, tasks: Vec::new(), } } - /// Spawn a task to write a packet directly to the server. + /// Write a packet directly to the server. pub fn write_packet(&mut self, packet: ServerboundGamePacket) { self.packet_writer .send(packet) diff --git a/azalea-client/src/movement.rs b/azalea-client/src/movement.rs index 8d6faabe..f379501c 100644 --- a/azalea-client/src/movement.rs +++ b/azalea-client/src/movement.rs @@ -333,6 +333,8 @@ impl Client { } } +/// An event sent when the client starts walking. This does not get sent for +/// non-local entities. pub struct StartWalkEvent { pub entity: Entity, pub direction: WalkDirection, @@ -354,6 +356,8 @@ pub fn walk_listener( } } +/// An event sent when the client starts sprinting. This does not get sent for +/// non-local entities. pub struct StartSprintEvent { pub entity: Entity, pub direction: SprintDirection, diff --git a/azalea-client/src/packet_handling.rs b/azalea-client/src/packet_handling.rs index db2c3c45..2d591531 100644 --- a/azalea-client/src/packet_handling.rs +++ b/azalea-client/src/packet_handling.rs @@ -26,7 +26,7 @@ use azalea_world::{ entity::{ metadata::{apply_metadata, Health, PlayerMetadataBundle}, set_rotation, Dead, EntityBundle, EntityKind, LastSentPosition, MinecraftEntityId, Physics, - PlayerBundle, Position, + PlayerBundle, Position, WorldName, }, LoadedBy, PartialWorld, RelativeEntityUpdate, WorldContainer, }; @@ -128,11 +128,16 @@ fn handle_packets(ecs: &mut Ecs) { #[allow(clippy::type_complexity)] let mut system_state: SystemState<( Commands, - Query<(&mut LocalPlayer, &GameProfileComponent)>, + Query<( + &mut LocalPlayer, + Option<&mut WorldName>, + &GameProfileComponent, + )>, ResMut, )> = SystemState::new(ecs); let (mut commands, mut query, mut world_container) = system_state.get_mut(ecs); - let (mut local_player, game_profile) = query.get_mut(player_entity).unwrap(); + let (mut local_player, world_name, game_profile) = + query.get_mut(player_entity).unwrap(); { // TODO: have registry_holder be a struct because this sucks rn @@ -188,12 +193,19 @@ fn handle_packets(ecs: &mut Ecs) { .as_int() .expect("min_y tag is not an int"); - let world_name = p.dimension.clone(); + let new_world_name = p.dimension.clone(); - local_player.world_name = Some(world_name.clone()); + if let Some(mut world_name) = world_name { + *world_name = world_name.clone(); + } else { + commands + .entity(player_entity) + .insert(WorldName(new_world_name.clone())); + } // add this world to the world_container (or don't if it's already // there) - let weak_world = world_container.insert(world_name.clone(), height, min_y); + let weak_world = + world_container.insert(new_world_name.clone(), height, min_y); // set the partial_world to an empty world // (when we add chunks or entities those will be in the // world_container) @@ -212,7 +224,7 @@ fn handle_packets(ecs: &mut Ecs) { game_profile.uuid, Vec3::default(), azalea_registry::EntityKind::Player, - world_name, + new_world_name, ), metadata: PlayerMetadataBundle::default(), }; @@ -506,12 +518,12 @@ fn handle_packets(ecs: &mut Ecs) { ClientboundGamePacket::AddEntity(p) => { debug!("Got add entity packet {:?}", p); - let mut system_state: SystemState<(Commands, Query<&mut LocalPlayer>)> = + let mut system_state: SystemState<(Commands, Query>)> = SystemState::new(ecs); let (mut commands, mut query) = system_state.get_mut(ecs); - let local_player = query.get_mut(player_entity).unwrap(); + let world_name = query.get_mut(player_entity).unwrap(); - if let Some(world_name) = &local_player.world_name { + if let Some(WorldName(world_name)) = world_name { let bundle = p.as_entity_bundle(world_name.clone()); let mut entity_commands = commands.spawn(( MinecraftEntityId(p.id), @@ -570,12 +582,14 @@ fn handle_packets(ecs: &mut Ecs) { ClientboundGamePacket::AddPlayer(p) => { debug!("Got add player packet {:?}", p); - let mut system_state: SystemState<(Commands, Query<&mut LocalPlayer>)> = - SystemState::new(ecs); + let mut system_state: SystemState<( + Commands, + Query<(&mut LocalPlayer, Option<&WorldName>)>, + )> = SystemState::new(ecs); let (mut commands, mut query) = system_state.get_mut(ecs); - let local_player = query.get_mut(player_entity).unwrap(); + let (local_player, world_name) = query.get_mut(player_entity).unwrap(); - if let Some(world_name) = &local_player.world_name { + if let Some(WorldName(world_name)) = world_name { let bundle = p.as_player_bundle(world_name.clone()); let mut spawned = commands.spawn(( MinecraftEntityId(p.id), diff --git a/azalea-client/src/task_pool.rs b/azalea-client/src/task_pool.rs index 2a3afbbc..59f70487 100644 --- a/azalea-client/src/task_pool.rs +++ b/azalea-client/src/task_pool.rs @@ -30,7 +30,7 @@ impl Plugin for TaskPoolPlugin { } /// Helper for configuring and creating the default task pools. For end-users -/// who want full control, set up [`TaskPoolPlugin`](super::TaskPoolPlugin) +/// who want full control, set up [`TaskPoolPlugin`](TaskPoolPlugin) #[derive(Clone, Resource)] pub struct TaskPoolOptions { /// If the number of physical cores is less than min_total_threads, force diff --git a/azalea-core/src/position.rs b/azalea-core/src/position.rs index d04f58cc..5ba7143e 100755 --- a/azalea-core/src/position.rs +++ b/azalea-core/src/position.rs @@ -109,6 +109,8 @@ macro_rules! vec3_impl { }; } +/// Used to represent an exact position in the world where an entity could be. +/// For blocks, [`BlockPos`] is used instead. #[derive(Clone, Copy, Debug, Default, PartialEq, McBuf)] pub struct Vec3 { pub x: f64, @@ -117,6 +119,8 @@ pub struct Vec3 { } vec3_impl!(Vec3, f64); +/// The coordinates of a block in the world. For entities (if the coordinate +/// with decimals), use [`Vec3`] instead. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] pub struct BlockPos { pub x: i32, @@ -137,6 +141,8 @@ impl BlockPos { } } +/// Chunk coordinates are used to represent where a chunk is in the world. You +/// can convert the x and z to block coordinates by multiplying them by 16. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] pub struct ChunkPos { pub x: i32, diff --git a/azalea-ecs/src/lib.rs b/azalea-ecs/src/lib.rs index 44579c5d..571bd741 100644 --- a/azalea-ecs/src/lib.rs +++ b/azalea-ecs/src/lib.rs @@ -1,16 +1,20 @@ #![feature(trait_alias)] -//! Re-export important parts of `bevy_ecs` and `bevy_app` and make them more -//! compatible with Azalea. +//! Re-export important parts of [`bevy_ecs`] and [`bevy_app`] and make them +//! more compatible with Azalea. //! //! This is completely compatible with `bevy_ecs`, so it won't cause issues if //! you use plugins meant for Bevy. //! //! Changes: -//! - Add [`TickPlugin`], [`TickStage`] and [`AppTickExt`] +//! - Add [`TickPlugin`], [`TickStage`] and [`AppTickExt`] (which adds +//! `app.add_tick_system` and `app.add_tick_system_set`). //! - Change the macros to use azalea/azalea_ecs instead of bevy/bevy_ecs -//! - Rename bevy_ecs::world::World to azalea_ecs::ecs::Ecs -//! - Re-export `bevy_app` in the `app` module. +//! - Rename `world::World` to [`ecs::Ecs`] +//! - Re-export `bevy_app` in the [`app`] module. +//! +//! [`bevy_ecs`]: https://docs.rs/bevy_ecs +//! [`bevy_app`]: https://docs.rs/bevy_app use std::time::{Duration, Instant}; diff --git a/azalea-world/src/entity/attributes.rs b/azalea-world/src/entity/attributes.rs index fd78a328..6b10a2b4 100644 --- a/azalea-world/src/entity/attributes.rs +++ b/azalea-world/src/entity/attributes.rs @@ -1,4 +1,4 @@ -//! +//! See . use std::{ collections::HashMap, diff --git a/azalea-world/src/entity/data.rs b/azalea-world/src/entity/data.rs index 14d257e3..346b277b 100755 --- a/azalea-world/src/entity/data.rs +++ b/azalea-world/src/entity/data.rs @@ -103,6 +103,7 @@ impl McBufWritable for OptionalUnsignedInt { } } +/// A set of x, y, and z rotations. This is used for armor stands. #[derive(Clone, Debug, McBuf, Default)] pub struct Rotations { pub x: f32, diff --git a/azalea-world/src/entity/mod.rs b/azalea-world/src/entity/mod.rs index bf758a12..60a00bfd 100644 --- a/azalea-world/src/entity/mod.rs +++ b/azalea-world/src/entity/mod.rs @@ -172,7 +172,7 @@ impl From<&LastSentPosition> for BlockPos { /// The name of the world the entity is in. If two entities share the same world /// name, we assume they're in the same world. #[derive(Component, Clone, Debug, PartialEq, Deref, DerefMut)] -pub struct WorldName(ResourceLocation); +pub struct WorldName(pub ResourceLocation); /// A component for entities that can jump. /// diff --git a/azalea-world/src/entity_info.rs b/azalea-world/src/entity_info.rs index 4109e9ce..428336f0 100644 --- a/azalea-world/src/entity_info.rs +++ b/azalea-world/src/entity_info.rs @@ -324,34 +324,3 @@ impl Debug for EntityInfos { f.debug_struct("EntityInfos").finish() } } - -// #[cfg(test)] -// mod tests { -// use crate::entity::metadata; - -// use super::*; -// use azalea_core::Vec3; - -// #[test] -// fn test_store_entity() { -// let mut storage = PartialEntityInfos::default(); -// assert!(storage.limited_get_by_id(0).is_none()); -// assert!(storage.shared.read().get_by_id(0).is_none()); - -// let uuid = Uuid::from_u128(100); -// storage.insert( -// 0, -// EntityData::new( -// uuid, -// Vec3::default(), -// EntityMetadata::Player(metadata::Player::default()), -// ), -// ); -// assert_eq!(storage.limited_get_by_id(0).unwrap().uuid, uuid); -// assert_eq!(storage.shared.read().get_by_id(0).unwrap().uuid, uuid); - -// storage.remove_by_id(0); -// assert!(storage.limited_get_by_id(0).is_none()); -// assert!(storage.shared.read().get_by_id(0).is_none()); -// } -// } diff --git a/azalea/README.md b/azalea/README.md index ef822d9f..ec694d4f 100755 --- a/azalea/README.md +++ b/azalea/README.md @@ -37,7 +37,7 @@ opt-level = 3 # Examples ```rust,no_run -A bot that logs chat messages sent in the server to the console. +//! A bot that logs chat messages sent in the server to the console. use azalea::prelude::*; use parking_lot::Mutex; @@ -48,15 +48,13 @@ async fn main() { let account = Account::offline("bot"); // or Account::microsoft("example@example.com").await.unwrap(); - azalea::start(azalea::Options { - account, - address: "localhost", - state: State::default(), - plugins: plugins![], - handle, - }) - .await - .unwrap(); + loop { + let e = azalea::ClientBuilder::new() + .set_handler(handle) + .start(account, "localhost") + .await; + eprintln!("{:?}", e); + } } #[derive(Default, Clone, Component)] @@ -76,10 +74,10 @@ async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> { # Plugins -Azalea uses [Bevy ECS](https://docs.rs/bevy_ecs) internally to store information about the world and clients. Bevy plugins are more powerful than async handler functions, but more difficult to use. See [pathfinder](azalea/src/pathfinder/mod.rs) as an example of how to make a plugin. You can then use a plugin by adding `.add_plugin(ExamplePlugin)` in the client or swarm builder. +Azalea uses [Bevy ECS](https://docs.rs/bevy_ecs) internally to store information about the world and clients. Bevy plugins are more powerful than async handler functions, but more difficult to use. See [pathfinder](azalea/src/pathfinder/mod.rs) as an example of how to make a plugin. You can then enable a plugin by adding `.add_plugin(ExamplePlugin)` in your client/swarm builder. Also note that just because something is an entity in the ECS doesn't mean that it's a Minecraft entity. You can filter for that by having `With` as a filter. -See the [https://bevy-cheatbook.github.io/programming/ecs-intro.html](Bevy Cheatbook) to learn more about Bevy ECS (and ECS in general). +See the [Bevy Cheatbook](https://bevy-cheatbook.github.io/programming/ecs-intro.html) to learn more about Bevy ECS (and the ECS paradigm in general). [`azalea_client`]: https://docs.rs/azalea-client \ No newline at end of file diff --git a/azalea/examples/matbot.rs b/azalea/examples/matbot.rs new file mode 100644 index 00000000..17736eab --- /dev/null +++ b/azalea/examples/matbot.rs @@ -0,0 +1,194 @@ +//! mat's bot for testing new azalea features + +#![feature(type_alias_impl_trait)] + +use azalea::ecs::query::With; +use azalea::entity::metadata::Player; +use azalea::entity::Position; +use azalea::pathfinder::BlockPosGoal; +use azalea::{prelude::*, BlockPos, GameProfileComponent, Swarm, SwarmEvent, WalkDirection}; +use azalea::{Account, Client, Event}; +use azalea_protocol::packets::game::serverbound_client_command_packet::ServerboundClientCommandPacket; +use std::time::Duration; + +#[derive(Default, Clone, Component)] +struct State {} + +#[derive(Default, Clone, Component)] +struct SwarmState {} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + env_logger::init(); + + { + use parking_lot::deadlock; + use std::thread; + use std::time::Duration; + + // Create a background thread which checks for deadlocks every 10s + thread::spawn(move || loop { + thread::sleep(Duration::from_secs(10)); + let deadlocks = deadlock::check_deadlock(); + if deadlocks.is_empty() { + continue; + } + + println!("{} deadlocks detected", deadlocks.len()); + for (i, threads) in deadlocks.iter().enumerate() { + println!("Deadlock #{i}"); + for t in threads { + println!("Thread Id {:#?}", t.thread_id()); + println!("{:#?}", t.backtrace()); + } + } + }); + } + + let mut accounts = Vec::new(); + let mut states = Vec::new(); + + for i in 0..1 { + accounts.push(Account::offline(&format!("bot{i}"))); + states.push(State::default()); + } + + loop { + let e = azalea::SwarmBuilder::new() + .add_accounts(accounts.clone()) + .set_handler(handle) + .set_swarm_handler(swarm_handle) + .join_delay(Duration::from_millis(1000)) + .start("localhost") + .await; + // let e = azalea::ClientBuilder::new() + // .set_handler(handle) + // .start(Account::offline("bot"), "localhost") + // .await; + eprintln!("{e:?}"); + } +} + +async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<()> { + match event { + Event::Init => { + println!("bot init"); + // bot.set_client_information(ClientInformation { + // view_distance: 2, + // ..Default::default() + // }) + // .await?; + } + Event::Login => { + bot.chat("Hello world"); + } + Event::Chat(m) => { + println!("client chat message: {}", m.content()); + if m.content() == bot.profile.name { + bot.chat("Bye"); + tokio::time::sleep(Duration::from_millis(50)).await; + bot.disconnect(); + } + let Some(sender) = m.username() else { + return Ok(()) + }; + // let mut ecs = bot.ecs.lock(); + // let entity = bot + // .ecs + // .lock() + // .query::<&Player>() + // .iter(&mut ecs) + // .find(|e| e.name() == Some(sender)); + // let entity = bot.entity_by::>(|name: &Name| name == sender); + let entity = bot.entity_by::, (&GameProfileComponent,)>( + |profile: &&GameProfileComponent| { + println!("entity {profile:?}"); + profile.name == sender + }, + ); + println!("sender entity: {entity:?}"); + if let Some(entity) = entity { + match m.content().as_str() { + "whereami" => { + let pos = bot.entity_component::(entity); + bot.chat(&format!("You're at {pos:?}",)); + } + "whereareyou" => { + let pos = bot.component::(); + bot.chat(&format!("I'm at {pos:?}",)); + } + "goto" => { + let entity_pos = bot.entity_component::(entity); + let target_pos: BlockPos = entity_pos.into(); + println!("going to {target_pos:?}"); + bot.goto(BlockPosGoal::from(target_pos)); + } + "look" => { + let entity_pos = bot.entity_component::(entity); + let target_pos: BlockPos = entity_pos.into(); + println!("target_pos: {target_pos:?}"); + bot.look_at(target_pos.center()); + } + "jump" => { + bot.set_jumping(true); + } + "walk" => { + bot.walk(WalkDirection::Forward); + } + "stop" => { + bot.set_jumping(false); + bot.walk(WalkDirection::None); + } + "lag" => { + std::thread::sleep(Duration::from_millis(1000)); + } + _ => {} + } + } + } + Event::Death(_) => { + bot.write_packet(ServerboundClientCommandPacket { + action: azalea_protocol::packets::game::serverbound_client_command_packet::Action::PerformRespawn, + }.get()); + } + _ => {} + } + + Ok(()) +} + +async fn swarm_handle( + mut swarm: Swarm, + event: SwarmEvent, + _state: SwarmState, +) -> anyhow::Result<()> { + match &event { + SwarmEvent::Disconnect(account) => { + println!("bot got kicked! {}", account.username); + tokio::time::sleep(Duration::from_secs(5)).await; + swarm.add(account, State::default()).await?; + } + SwarmEvent::Chat(m) => { + println!("swarm chat message: {}", m.message().to_ansi()); + if m.message().to_string() == " world" { + for (name, world) in &swarm.world_container.read().worlds { + println!("world name: {name}"); + if let Some(w) = world.upgrade() { + for chunk_pos in w.read().chunks.chunks.values() { + println!("chunk: {chunk_pos:?}"); + } + } else { + println!("nvm world is gone"); + } + } + } + if m.message().to_string() == " hi" { + for bot in swarm { + bot.chat("hello"); + } + } + } + _ => {} + } + Ok(()) +} diff --git a/azalea/examples/matbot/main.rs b/azalea/examples/matbot/main.rs deleted file mode 100644 index a7901929..00000000 --- a/azalea/examples/matbot/main.rs +++ /dev/null @@ -1,195 +0,0 @@ -//! mat's bot for testing new azalea features - -#![feature(type_alias_impl_trait)] - -use azalea::ecs::query::With; -use azalea::entity::metadata::Player; -use azalea::entity::Position; -use azalea::pathfinder::BlockPosGoal; -// use azalea::ClientInformation; -use azalea::{prelude::*, BlockPos, GameProfileComponent, Swarm, SwarmEvent, WalkDirection}; -use azalea::{Account, Client, Event}; -use azalea_protocol::packets::game::serverbound_client_command_packet::ServerboundClientCommandPacket; -use std::time::Duration; - -#[derive(Default, Clone, Component)] -struct State {} - -#[derive(Default, Clone, Component)] -struct SwarmState {} - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - env_logger::init(); - - { - use parking_lot::deadlock; - use std::thread; - use std::time::Duration; - - // Create a background thread which checks for deadlocks every 10s - thread::spawn(move || loop { - thread::sleep(Duration::from_secs(10)); - let deadlocks = deadlock::check_deadlock(); - if deadlocks.is_empty() { - continue; - } - - println!("{} deadlocks detected", deadlocks.len()); - for (i, threads) in deadlocks.iter().enumerate() { - println!("Deadlock #{i}"); - for t in threads { - println!("Thread Id {:#?}", t.thread_id()); - println!("{:#?}", t.backtrace()); - } - } - }); - } - - let mut accounts = Vec::new(); - let mut states = Vec::new(); - - for i in 0..1 { - accounts.push(Account::offline(&format!("bot{i}"))); - states.push(State::default()); - } - - loop { - let e = azalea::SwarmBuilder::new() - .add_accounts(accounts.clone()) - .set_handler(handle) - .set_swarm_handler(swarm_handle) - .join_delay(Duration::from_millis(1000)) - .start("localhost") - .await; - // let e = azalea::ClientBuilder::new() - // .set_handler(handle) - // .start(Account::offline("bot"), "localhost") - // .await; - println!("{e:?}"); - } -} - -async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<()> { - match event { - Event::Init => { - println!("bot init"); - // bot.set_client_information(ClientInformation { - // view_distance: 2, - // ..Default::default() - // }) - // .await?; - } - Event::Login => { - bot.chat("Hello world"); - } - Event::Chat(m) => { - println!("client chat message: {}", m.content()); - if m.content() == bot.profile.name { - bot.chat("Bye"); - tokio::time::sleep(Duration::from_millis(50)).await; - bot.disconnect(); - } - let Some(sender) = m.username() else { - return Ok(()) - }; - // let mut ecs = bot.ecs.lock(); - // let entity = bot - // .ecs - // .lock() - // .query::<&Player>() - // .iter(&mut ecs) - // .find(|e| e.name() == Some(sender)); - // let entity = bot.entity_by::>(|name: &Name| name == sender); - let entity = bot.entity_by::, (&GameProfileComponent,)>( - |profile: &&GameProfileComponent| { - println!("entity {profile:?}"); - profile.name == sender - }, - ); - println!("sender entity: {entity:?}"); - if let Some(entity) = entity { - match m.content().as_str() { - "whereami" => { - let pos = bot.entity_component::(entity); - bot.chat(&format!("You're at {pos:?}",)); - } - "whereareyou" => { - let pos = bot.component::(); - bot.chat(&format!("I'm at {pos:?}",)); - } - "goto" => { - let entity_pos = bot.entity_component::(entity); - let target_pos: BlockPos = entity_pos.into(); - println!("going to {target_pos:?}"); - bot.goto(BlockPosGoal::from(target_pos)); - } - "look" => { - let entity_pos = bot.entity_component::(entity); - let target_pos: BlockPos = entity_pos.into(); - println!("target_pos: {target_pos:?}"); - bot.look_at(target_pos.center()); - } - "jump" => { - bot.set_jumping(true); - } - "walk" => { - bot.walk(WalkDirection::Forward); - } - "stop" => { - bot.set_jumping(false); - bot.walk(WalkDirection::None); - } - "lag" => { - std::thread::sleep(Duration::from_millis(1000)); - } - _ => {} - } - } - } - Event::Death(_) => { - bot.write_packet(ServerboundClientCommandPacket { - action: azalea_protocol::packets::game::serverbound_client_command_packet::Action::PerformRespawn, - }.get()); - } - _ => {} - } - - Ok(()) -} - -async fn swarm_handle( - mut swarm: Swarm, - event: SwarmEvent, - _state: SwarmState, -) -> anyhow::Result<()> { - match &event { - SwarmEvent::Disconnect(account) => { - println!("bot got kicked! {}", account.username); - tokio::time::sleep(Duration::from_secs(5)).await; - swarm.add(account, State::default()).await?; - } - SwarmEvent::Chat(m) => { - println!("swarm chat message: {}", m.message().to_ansi()); - if m.message().to_string() == " world" { - for (name, world) in &swarm.world_container.read().worlds { - println!("world name: {name}"); - if let Some(w) = world.upgrade() { - for chunk_pos in w.read().chunks.chunks.values() { - println!("chunk: {chunk_pos:?}"); - } - } else { - println!("nvm world is gone"); - } - } - } - if m.message().to_string() == " hi" { - for bot in swarm { - bot.chat("hello"); - } - } - } - _ => {} - } - Ok(()) -} diff --git a/azalea/src/lib.rs b/azalea/src/lib.rs index 026a35f5..b7707b92 100644 --- a/azalea/src/lib.rs +++ b/azalea/src/lib.rs @@ -39,6 +39,15 @@ pub enum StartError { Join(#[from] azalea_client::JoinError), } +/// A builder for creating new [`Client`]s. This is the recommended way of +/// making Azalea bots. +/// +/// ```no_run +/// azalea::ClientBuilder::new() +/// .set_handler(handle) +/// .start(Account::offline("bot"), "localhost") +/// .await; +/// ``` pub struct ClientBuilder where S: Default + Send + Sync + Clone + 'static, -- cgit v1.2.3