diff options
Diffstat (limited to 'azalea/src/client_impl')
| -rw-r--r-- | azalea/src/client_impl/attack.rs | 50 | ||||
| -rw-r--r-- | azalea/src/client_impl/chat.rs | 49 | ||||
| -rw-r--r-- | azalea/src/client_impl/client_information.rs | 38 | ||||
| -rw-r--r-- | azalea/src/client_impl/entity_query.rs | 264 | ||||
| -rw-r--r-- | azalea/src/client_impl/interact.rs | 52 | ||||
| -rw-r--r-- | azalea/src/client_impl/inventory.rs | 45 | ||||
| -rw-r--r-- | azalea/src/client_impl/mining.rs | 29 | ||||
| -rw-r--r-- | azalea/src/client_impl/mod.rs | 508 | ||||
| -rw-r--r-- | azalea/src/client_impl/movement.rs | 104 |
9 files changed, 1139 insertions, 0 deletions
diff --git a/azalea/src/client_impl/attack.rs b/azalea/src/client_impl/attack.rs new file mode 100644 index 00000000..f4cac51f --- /dev/null +++ b/azalea/src/client_impl/attack.rs @@ -0,0 +1,50 @@ +use azalea_client::attack::{ + AttackEvent, AttackStrengthScale, TicksSinceLastAttack, get_attack_strength_delay, +}; +use azalea_entity::Attributes; +use bevy_ecs::entity::Entity; + +use crate::Client; + +impl Client { + /// Attack an entity in the world. + /// + /// This doesn't automatically look at the entity or perform any + /// range/visibility checks, so it might trigger anticheats. + pub fn attack(&self, entity: Entity) { + self.ecs.lock().write_message(AttackEvent { + entity: self.entity, + target: entity, + }); + } + + /// Whether the player has an attack cooldown. + /// + /// Also see [`Client::attack_cooldown_remaining_ticks`]. + pub fn has_attack_cooldown(&self) -> bool { + let Some(attack_strength_scale) = self.get_component::<AttackStrengthScale>() else { + // they don't even have an AttackStrengthScale so they probably can't even + // attack? whatever, just return false + return false; + }; + *attack_strength_scale < 1.0 + } + + /// Returns the number of ticks until we can attack at full strength again. + /// + /// Also see [`Client::has_attack_cooldown`]. + pub fn attack_cooldown_remaining_ticks(&self) -> usize { + let mut ecs = self.ecs.lock(); + let Ok((attributes, ticks_since_last_attack)) = ecs + .query::<(&Attributes, &TicksSinceLastAttack)>() + .get(&ecs, self.entity) + else { + return 0; + }; + + let attack_strength_delay = get_attack_strength_delay(attributes); + let remaining_ticks = attack_strength_delay - **ticks_since_last_attack as f32; + + remaining_ticks.max(0.).ceil() as usize + } +} diff --git a/azalea/src/client_impl/chat.rs b/azalea/src/client_impl/chat.rs new file mode 100644 index 00000000..3ca98631 --- /dev/null +++ b/azalea/src/client_impl/chat.rs @@ -0,0 +1,49 @@ +use azalea_client::chat::{ChatKind, SendChatEvent, handler::SendChatKindEvent}; + +use crate::Client; + +impl Client { + /// Send a chat message to the server. + /// + /// This only sends the chat packet and not the command packet, which means + /// on some servers you can use this to send chat messages that start + /// with a `/`. The [`Client::chat`] function handles checking whether + /// the message is a command and using the proper packet for you, so you + /// should use that instead. + pub fn write_chat_packet(&self, message: &str) { + self.ecs.lock().write_message(SendChatKindEvent { + entity: self.entity, + content: message.to_owned(), + kind: ChatKind::Message, + }); + } + + /// Send a command packet to the server. The `command` argument should not + /// include the slash at the front. + /// + /// You can also just use [`Client::chat`] and start your message with a `/` + /// to send a command. + pub fn write_command_packet(&self, command: &str) { + self.ecs.lock().write_message(SendChatKindEvent { + entity: self.entity, + content: command.to_owned(), + kind: ChatKind::Command, + }); + } + + /// Send a message in chat. + /// + /// ```rust,no_run + /// # use azalea::Client; + /// # async fn example(bot: Client) -> anyhow::Result<()> { + /// bot.chat("Hello, world!"); + /// # Ok(()) + /// # } + /// ``` + pub fn chat(&self, content: impl Into<String>) { + self.ecs.lock().write_message(SendChatEvent { + entity: self.entity, + content: content.into(), + }); + } +} diff --git a/azalea/src/client_impl/client_information.rs b/azalea/src/client_impl/client_information.rs new file mode 100644 index 00000000..b3cf7927 --- /dev/null +++ b/azalea/src/client_impl/client_information.rs @@ -0,0 +1,38 @@ +use azalea_client::ClientInformation; +use azalea_protocol::packets::game; +use tracing::debug; + +use crate::Client; + +impl Client { + /// Tell the server we changed our game options (i.e. render distance, main + /// hand). + /// + /// If this is not set before the login packet, the default will be sent. + /// + /// ```rust,no_run + /// # use azalea::{Client, ClientInformation}; + /// # async fn example(bot: Client) -> Result<(), Box<dyn std::error::Error>> { + /// bot.set_client_information(ClientInformation { + /// view_distance: 2, + /// ..Default::default() + /// }); + /// # Ok(()) + /// # } + /// ``` + pub fn set_client_information(&self, client_information: ClientInformation) { + self.query_self::<&mut ClientInformation, _>(|mut ci| { + *ci = client_information.clone(); + }); + + if self.logged_in() { + debug!( + "Sending client information (already logged in): {:?}", + client_information + ); + self.write_packet(game::s_client_information::ServerboundClientInformation { + client_information, + }); + } + } +} diff --git a/azalea/src/client_impl/entity_query.rs b/azalea/src/client_impl/entity_query.rs new file mode 100644 index 00000000..85e46525 --- /dev/null +++ b/azalea/src/client_impl/entity_query.rs @@ -0,0 +1,264 @@ +use std::{any, sync::Arc}; + +use azalea_core::position::Vec3; +use azalea_entity::Position; +use azalea_world::InstanceName; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::{QueryData, QueryEntityError, QueryFilter, QueryItem, ROQueryItem}, + world::World, +}; +use parking_lot::Mutex; + +use crate::Client; + +impl Client { + /// A convenience function for getting components from our client's entity. + /// + /// To query another entity, you can use [`Self::query_entity`]. + /// + /// # Examples + /// ``` + /// # use azalea_world::InstanceName; + /// # fn example(mut client: azalea::Client) { + /// let is_logged_in = client.query_self::<Option<&InstanceName>, _>(|ins| ins.is_some()); + /// # } + /// ``` + /// + /// # Panics + /// + /// This will panic if the component doesn't exist on the client. + pub fn query_self<D: QueryData, R>(&self, f: impl FnOnce(QueryItem<D>) -> R) -> R { + let mut ecs = self.ecs.lock(); + let mut qs = ecs.query::<D>(); + let res = qs.get_mut(&mut ecs, self.entity).unwrap_or_else(|_| { + panic!( + "Our client is missing a required component {:?}", + any::type_name::<D>() + ) + }); + f(res) + } + + /// A convenience function for getting components from any entity. + /// + /// If you're querying the client, you should use [`Self::query_self`]. + /// + /// # Panics + /// + /// This will panic if the entity doesn't exist or if the query isn't valid + /// for the entity. For a non-panicking version, you may use + /// [`Self::try_query_entity`]. + pub fn query_entity<D: QueryData, R>( + &self, + entity: Entity, + f: impl FnOnce(QueryItem<D>) -> R, + ) -> R { + self.try_query_entity(entity, f).unwrap_or_else(|_| { + panic!( + "Entity is missing a required component {:?}", + any::type_name::<D>() + ) + }) + } + + /// A convenience function for getting components from any entity, or None + /// if the query fails. + /// + /// If you're sure that the entity exists and that the query will succeed, + /// you can use [`Self::query_entity`]. + pub fn try_query_entity<D: QueryData, R>( + &self, + entity: Entity, + f: impl FnOnce(QueryItem<D>) -> R, + ) -> Result<R, QueryEntityError> { + let mut ecs = self.ecs.lock(); + let mut qs = ecs.query::<D>(); + qs.get_mut(&mut ecs, entity).map(f) + } + + /// Quickly returns a lightweight [`Entity`] for an arbitrary entity that + /// matches the given predicate function that is in the same + /// [`Instance`] as the client. + /// + /// You can then use [`Self::entity_component`] to get components from this + /// entity. + /// + /// If you want to find the nearest entity, consider using + /// [`Self::nearest_entity_by`] instead. If you want to find all entities + /// that match the predicate, use [`Self::nearest_entities_by`]. + /// + /// # Example + /// ``` + /// use azalea::{ + /// Client, + /// entity::{Position, metadata::Player}, + /// player::GameProfileComponent, + /// }; + /// use bevy_ecs::query::With; + /// + /// # fn example(mut bot: Client, sender_name: String) { + /// let entity = bot.any_entity_by::<&GameProfileComponent, With<Player>>( + /// |profile: &GameProfileComponent| profile.name == sender_name, + /// ); + /// if let Some(entity) = entity { + /// let position = bot.entity_component::<Position>(entity); + /// // ... + /// } + /// # } + /// ``` + /// + /// [`Entity`]: bevy_ecs::entity::Entity + /// [`Instance`]: azalea_world::Instance + pub fn any_entity_by<Q: QueryData, F: QueryFilter>( + &self, + predicate: impl EntityPredicate<Q, F>, + ) -> Option<Entity> { + let instance_name = self.get_component::<InstanceName>()?; + predicate.find_any(self.ecs.clone(), &instance_name) + } + + /// Return a lightweight [`Entity`] for the nearest entity that matches the + /// given predicate function. + /// + /// You can then use [`Self::entity_component`] to get components from this + /// entity. + /// + /// If you don't need the entity to be the nearest one, it may be more + /// efficient to use [`Self::any_entity_by`] instead. You can also use + /// [`Self::nearest_entities_by`] to get all nearby entities. + /// + /// ``` + /// use azalea_entity::{LocalEntity, Position, metadata::Player}; + /// use bevy_ecs::query::{With, Without}; + /// + /// # fn example(mut bot: azalea::Client, sender_name: String) { + /// // get the position of the nearest player + /// if let Some(nearest_player) = + /// bot.nearest_entity_by::<(), (With<Player>, Without<LocalEntity>)>(|_: ()| true) + /// { + /// let nearest_player_pos = *bot.entity_component::<Position>(nearest_player); + /// bot.chat(format!("You are at {nearest_player_pos}")); + /// } + /// # } + /// ``` + /// + /// [`Entity`]: bevy_ecs::entity::Entity + pub fn nearest_entity_by<Q: QueryData, F: QueryFilter>( + &self, + predicate: impl EntityPredicate<Q, F>, + ) -> Option<Entity> { + self.nearest_entities_by(predicate).first().copied() + } + + /// Similar to [`Self::nearest_entity_by`] but returns a `Vec<Entity>` of + /// all entities in our instance that match the predicate. + /// + /// The first entity is the nearest one. + /// + /// ``` + /// # use azalea_entity::{LocalEntity, Position, metadata::Player}; + /// # use bevy_ecs::query::{With, Without}; + /// # fn example(mut bot: azalea::Client, sender_name: String) { + /// let nearby_players = + /// bot.nearest_entities_by::<(), (With<Player>, Without<LocalEntity>)>(|_: ()| true); + /// # } + /// ``` + pub fn nearest_entities_by<Q: QueryData, F: QueryFilter>( + &self, + predicate: impl EntityPredicate<Q, F>, + ) -> Vec<Entity> { + let Some(instance_name) = self.get_component::<InstanceName>() else { + return vec![]; + }; + let Some(position) = self.get_component::<Position>() else { + return vec![]; + }; + predicate.find_all_sorted(self.ecs.clone(), &instance_name, (&position).into()) + } + + /// Get a component from an entity. + /// + /// Note that this will return an owned type (i.e. not a reference) so it + /// may be expensive for larger types. + /// + /// If you're trying to get a component for this client, use + /// [`Self::component`]. + pub fn entity_component<Q: Component + Clone>(&self, entity: Entity) -> Q { + let mut ecs = self.ecs.lock(); + let mut q = ecs.query::<&Q>(); + let components = q.get(&ecs, entity).unwrap_or_else(|_| { + panic!( + "Entity is missing a required component {:?}", + any::type_name::<Q>() + ) + }); + components.clone() + } + + /// Get a component from an entity, if it exists. + /// + /// This is similar to [`Self::entity_component`] but returns an `Option` + /// instead of panicking if the component isn't present. + pub fn get_entity_component<Q: Component + Clone>(&self, entity: Entity) -> Option<Q> { + let mut ecs = self.ecs.lock(); + let mut q = ecs.query::<&Q>(); + let components = q.get(&ecs, entity).ok(); + components.cloned() + } +} + +pub trait EntityPredicate<Q: QueryData, Filter: QueryFilter> { + fn find_any(&self, ecs_lock: Arc<Mutex<World>>, instance_name: &InstanceName) + -> Option<Entity>; + fn find_all_sorted( + &self, + ecs_lock: Arc<Mutex<World>>, + instance_name: &InstanceName, + nearest_to: Vec3, + ) -> Vec<Entity>; +} +impl<F, Q: QueryData, Filter: QueryFilter> EntityPredicate<Q, Filter> for F +where + F: Fn(ROQueryItem<Q>) -> bool, + for<'w, 's> <<Q as QueryData>::ReadOnly as QueryData>::Item<'w, 's>: Copy, +{ + fn find_any( + &self, + ecs_lock: Arc<Mutex<World>>, + instance_name: &InstanceName, + ) -> Option<Entity> { + let mut ecs = ecs_lock.lock(); + let mut query = ecs.query_filtered::<(Entity, &InstanceName, Q), Filter>(); + query + .iter(&ecs) + .find(|(_, e_instance_name, q)| *e_instance_name == instance_name && (self)(*q)) + .map(|(e, _, _)| e) + } + + fn find_all_sorted( + &self, + ecs_lock: Arc<Mutex<World>>, + instance_name: &InstanceName, + nearest_to: Vec3, + ) -> Vec<Entity> { + let mut ecs = ecs_lock.lock(); + let mut query = ecs.query_filtered::<(Entity, &InstanceName, &Position, Q), Filter>(); + let mut entities = query + .iter(&ecs) + .filter(|(_, e_instance_name, _, q)| *e_instance_name == instance_name && (self)(*q)) + .map(|(e, _, position, _)| (e, Vec3::from(position))) + .collect::<Vec<(Entity, Vec3)>>(); + + entities.sort_by_cached_key(|(_, position)| { + // to_bits is fine here as long as the number is positive + position.distance_squared_to(nearest_to).to_bits() + }); + + entities + .into_iter() + .map(|(e, _)| e) + .collect::<Vec<Entity>>() + } +} diff --git a/azalea/src/client_impl/interact.rs b/azalea/src/client_impl/interact.rs new file mode 100644 index 00000000..6ff93549 --- /dev/null +++ b/azalea/src/client_impl/interact.rs @@ -0,0 +1,52 @@ +use azalea_client::interact::{EntityInteractEvent, StartUseItemEvent}; +use azalea_core::position::BlockPos; +use azalea_protocol::packets::game::s_interact::InteractionHand; +use bevy_ecs::entity::Entity; + +use crate::Client; + +impl Client { + /// Right-click a block. + /// + /// The behavior of this depends on the target block, + /// and it'll either place the block you're holding in your hand or use the + /// block you clicked (like toggling a lever). + /// + /// Note that this may trigger anticheats as it doesn't take into account + /// whether you're actually looking at the block. + pub fn block_interact(&self, position: BlockPos) { + self.ecs.lock().write_message(StartUseItemEvent { + entity: self.entity, + hand: InteractionHand::MainHand, + force_block: Some(position), + }); + } + + /// Right-click an entity. + /// + /// This can click through walls, which may trigger anticheats. If that + /// behavior isn't desired, consider using [`Client::start_use_item`] + /// instead. + pub fn entity_interact(&self, entity: Entity) { + self.ecs.lock().trigger(EntityInteractEvent { + client: self.entity, + target: entity, + location: None, + }); + } + + /// Right-click the currently held item. + /// + /// If the item is consumable, then it'll act as if right-click was held + /// until the item finishes being consumed. You can use this to eat food. + /// + /// If we're looking at a block or entity, then it will be clicked. Also see + /// [`Client::block_interact`] and [`Client::entity_interact`]. + pub fn start_use_item(&self) { + self.ecs.lock().write_message(StartUseItemEvent { + entity: self.entity, + hand: InteractionHand::MainHand, + force_block: None, + }); + } +} diff --git a/azalea/src/client_impl/inventory.rs b/azalea/src/client_impl/inventory.rs new file mode 100644 index 00000000..0ea11477 --- /dev/null +++ b/azalea/src/client_impl/inventory.rs @@ -0,0 +1,45 @@ +use azalea_client::inventory::SetSelectedHotbarSlotEvent; +use azalea_entity::inventory::Inventory; +use azalea_inventory::Menu; + +use crate::Client; + +impl Client { + /// Return the menu that is currently open, or the player's inventory if no + /// menu is open. + pub fn menu(&self) -> Menu { + self.query_self::<&Inventory, _>(|inv| inv.menu().clone()) + } + + /// Returns the index of the hotbar slot that's currently selected. + /// + /// If you want to access the actual held item, you can get the current menu + /// with [`Client::menu`] and then get the slot index by offsetting from + /// the start of [`azalea_inventory::Menu::hotbar_slots_range`]. + /// + /// You can use [`Self::set_selected_hotbar_slot`] to change it. + pub fn selected_hotbar_slot(&self) -> u8 { + self.query_self::<&Inventory, _>(|inv| inv.selected_hotbar_slot) + } + + /// Update the selected hotbar slot index. + /// + /// This will run next `Update`, so you might want to call + /// `bot.wait_updates(1)` after calling this if you're using `azalea`. + /// + /// # Panics + /// + /// This will panic if `new_hotbar_slot_index` is not in the range 0..=8. + pub fn set_selected_hotbar_slot(&self, new_hotbar_slot_index: u8) { + assert!( + new_hotbar_slot_index < 9, + "Hotbar slot index must be in the range 0..=8" + ); + + let mut ecs = self.ecs.lock(); + ecs.trigger(SetSelectedHotbarSlotEvent { + entity: self.entity, + slot: new_hotbar_slot_index, + }); + } +} diff --git a/azalea/src/client_impl/mining.rs b/azalea/src/client_impl/mining.rs new file mode 100644 index 00000000..14794765 --- /dev/null +++ b/azalea/src/client_impl/mining.rs @@ -0,0 +1,29 @@ +use azalea_client::mining::{LeftClickMine, StartMiningBlockEvent}; +use azalea_core::position::BlockPos; + +use crate::Client; + +impl Client { + pub fn start_mining(&self, position: BlockPos) { + let mut ecs = self.ecs.lock(); + + ecs.write_message(StartMiningBlockEvent { + entity: self.entity, + position, + force: true, + }); + } + + /// When enabled, the bot will mine any block that it is looking at if it is + /// reachable. + pub fn left_click_mine(&self, enabled: bool) { + let mut ecs = self.ecs.lock(); + let mut entity_mut = ecs.entity_mut(self.entity); + + if enabled { + entity_mut.insert(LeftClickMine); + } else { + entity_mut.remove::<LeftClickMine>(); + } + } +} diff --git a/azalea/src/client_impl/mod.rs b/azalea/src/client_impl/mod.rs new file mode 100644 index 00000000..2f5fbf7d --- /dev/null +++ b/azalea/src/client_impl/mod.rs @@ -0,0 +1,508 @@ +use std::{collections::HashMap, sync::Arc}; + +use azalea_auth::game_profile::GameProfile; +use azalea_client::{ + Account, DefaultPlugins, Event, + connection::RawConnection, + disconnect::DisconnectEvent, + join::{ConnectOpts, StartJoinServerEvent}, + local_player::{Hunger, InstanceHolder, TabList}, + packet::game::SendGamePacketEvent, + player::{GameProfileComponent, PlayerInfo}, + start_ecs_runner, +}; +use azalea_core::{ + data_registry::{DataRegistryWithKey, ResolvableDataRegistry}, + position::Vec3, +}; +use azalea_entity::{ + Attributes, Position, + dimensions::EntityDimensions, + indexing::{EntityIdIndex, EntityUuidIndex}, + metadata::Health, +}; +use azalea_protocol::{ + address::{ResolvableAddr, ResolvedAddr}, + connect::Proxy, + packets::{Packet, game::ServerboundGamePacket}, + resolve::ResolveError, +}; +use azalea_registry::{DataRegistryKeyRef, identifier::Identifier}; +use azalea_world::{Instance, InstanceName, MinecraftEntityId, PartialInstance}; +use bevy_app::App; +use bevy_ecs::{ + component::Component, + entity::Entity, + resource::Resource, + world::{Mut, World}, +}; +use parking_lot::{Mutex, RwLock}; +use tokio::sync::mpsc; +use uuid::Uuid; + +pub mod attack; +pub mod chat; +pub mod client_information; +pub mod entity_query; +pub mod interact; +pub mod inventory; +pub mod mining; +pub mod movement; + +/// A Minecraft client instance that can interact with the world. +/// +/// To make a new client, use either [`azalea::ClientBuilder`] or +/// [`Client::join`]. +/// +/// Note that `Client` is inaccessible from systems (i.e. plugins), but you can +/// achieve everything that client can do with ECS events. +/// +/// [`azalea::ClientBuilder`]: https://docs.rs/azalea/latest/azalea/struct.ClientBuilder.html +#[derive(Clone)] +pub struct Client { + /// The entity for this client in the ECS. + pub entity: Entity, + + /// A mutually exclusive reference to the entity component system (ECS). + /// + /// You probably don't need to access this directly. Note that if you're + /// using a shared world (i.e. a swarm), the ECS will contain all entities + /// in all instances/dimensions. + pub ecs: Arc<Mutex<World>>, +} + +pub struct StartClientOpts { + pub ecs_lock: Arc<Mutex<World>>, + pub account: Account, + pub connect_opts: ConnectOpts, + pub event_sender: Option<mpsc::UnboundedSender<Event>>, +} + +impl StartClientOpts { + pub fn new( + account: Account, + address: ResolvedAddr, + event_sender: Option<mpsc::UnboundedSender<Event>>, + ) -> StartClientOpts { + let mut app = App::new(); + app.add_plugins(DefaultPlugins); + + // appexit_rx is unused here since the user should be able to handle it + // themselves if they're using StartClientOpts::new + let (ecs_lock, start_running_systems, _appexit_rx) = start_ecs_runner(app.main_mut()); + start_running_systems(); + + Self { + ecs_lock, + account, + connect_opts: ConnectOpts { + address, + server_proxy: None, + sessionserver_proxy: None, + }, + event_sender, + } + } + + /// Configure the SOCKS5 proxy used for connecting to the server and for + /// authenticating with Mojang. + /// + /// To configure these separately, for example to only use the proxy for the + /// Minecraft server and not for authentication, you may use + /// [`Self::server_proxy`] and [`Self::sessionserver_proxy`] individually. + pub fn proxy(self, proxy: Proxy) -> Self { + self.server_proxy(proxy.clone()).sessionserver_proxy(proxy) + } + /// Configure the SOCKS5 proxy that will be used for connecting to the + /// Minecraft server. + /// + /// To avoid errors on servers with the "prevent-proxy-connections" option + /// set, you should usually use [`Self::proxy`] instead. + /// + /// Also see [`Self::sessionserver_proxy`]. + pub fn server_proxy(mut self, proxy: Proxy) -> Self { + self.connect_opts.server_proxy = Some(proxy); + self + } + /// Configure the SOCKS5 proxy that this bot will use for authenticating the + /// server join with Mojang's API. + /// + /// Also see [`Self::proxy`] and [`Self::server_proxy`]. + pub fn sessionserver_proxy(mut self, proxy: Proxy) -> Self { + self.connect_opts.sessionserver_proxy = Some(proxy); + self + } +} + +impl Client { + /// Create a new client from the given [`GameProfile`], ECS Entity, ECS + /// World, and schedule runner function. + /// You should only use this if you want to change these fields from the + /// defaults, otherwise use [`Client::join`]. + pub fn new(entity: Entity, ecs: Arc<Mutex<World>>) -> Self { + Self { + // default our id to 0, it'll be set later + entity, + + ecs, + } + } + + /// Connect to a Minecraft server. + /// + /// To change the render distance and other settings, use + /// [`Client::set_client_information`]. To watch for events like packets + /// sent by the server, use the `rx` variable this function returns. + /// + /// # Examples + /// + /// ```rust,no_run + /// use azalea::{Account, Client}; + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Box<dyn std::error::Error>> { + /// let account = Account::offline("bot"); + /// let (client, rx) = Client::join(account, "localhost").await?; + /// client.chat("Hello, world!"); + /// client.disconnect(); + /// Ok(()) + /// } + /// ``` + pub async fn join( + account: Account, + address: impl ResolvableAddr, + ) -> Result<(Self, mpsc::UnboundedReceiver<Event>), ResolveError> { + let address = address.resolve().await?; + let (tx, rx) = mpsc::unbounded_channel(); + + let client = Self::start_client(StartClientOpts::new(account, address, Some(tx))).await; + Ok((client, rx)) + } + + pub async fn join_with_proxy( + account: Account, + address: impl ResolvableAddr, + proxy: Proxy, + ) -> Result<(Self, mpsc::UnboundedReceiver<Event>), ResolveError> { + let address = address.resolve().await?; + let (tx, rx) = mpsc::unbounded_channel(); + + let client = + Self::start_client(StartClientOpts::new(account, address, Some(tx)).proxy(proxy)).await; + Ok((client, rx)) + } + + /// Create a [`Client`] when you already have the ECS made with + /// [`start_ecs_runner`]. You'd usually want to use [`Self::join`] instead. + pub async fn start_client( + StartClientOpts { + ecs_lock, + account, + connect_opts, + event_sender, + }: StartClientOpts, + ) -> Self { + // send a StartJoinServerEvent + + let (start_join_callback_tx, mut start_join_callback_rx) = + mpsc::unbounded_channel::<Entity>(); + + ecs_lock.lock().write_message(StartJoinServerEvent { + account, + connect_opts, + event_sender, + start_join_callback_tx: Some(start_join_callback_tx), + }); + + let entity = start_join_callback_rx.recv().await.expect( + "start_join_callback should not be dropped before sending a message, this is a bug in Azalea", + ); + + Client::new(entity, ecs_lock) + } + + /// Write a packet directly to the server. + pub fn write_packet(&self, packet: impl Packet<ServerboundGamePacket>) { + let packet = packet.into_variant(); + self.ecs + .lock() + .commands() + .trigger(SendGamePacketEvent::new(self.entity, packet)); + } + + /// Disconnect this client from the server by ending all tasks. + /// + /// The OwnedReadHalf for the TCP connection is in one of the tasks, so it + /// automatically closes the connection when that's dropped. + pub fn disconnect(&self) { + self.ecs.lock().write_message(DisconnectEvent { + entity: self.entity, + reason: None, + }); + } + + pub fn with_raw_connection<R>(&self, f: impl FnOnce(&RawConnection) -> R) -> R { + self.query_self::<&RawConnection, _>(f) + } + pub fn with_raw_connection_mut<R>(&self, f: impl FnOnce(Mut<'_, RawConnection>) -> R) -> R { + self.query_self::<&mut RawConnection, _>(f) + } + + /// Get a component from this client. This will clone the component and + /// return it. + /// + /// + /// If the component can't be cloned, try [`Self::query_self`] instead. + /// If it isn't guaranteed to be present, you can use + /// [`Self::get_component`] or [`Self::query_self`]. + /// + /// + /// You may also use [`Self::ecs`] directly if you need more control over + /// when the ECS is locked. + /// + /// # Panics + /// + /// This will panic if the component doesn't exist on the client. + /// + /// # Examples + /// + /// ``` + /// # use azalea_world::InstanceName; + /// # fn example(client: &azalea::Client) { + /// let world_name = client.component::<InstanceName>(); + /// # } + pub fn component<T: Component + Clone>(&self) -> T { + self.query_self::<&T, _>(|t| t.clone()) + } + + /// Get a component from this client, or `None` if it doesn't exist. + /// + /// If the component can't be cloned, consider using [`Self::query_self`] + /// with `Option<&T>` instead. + /// + /// You may also have to use [`Self::query_self`] directly. + pub fn get_component<T: Component + Clone>(&self) -> Option<T> { + self.query_self::<Option<&T>, _>(|t| t.cloned()) + } + + /// Get a resource from the ECS. This will clone the resource and return it. + pub fn resource<T: Resource + Clone>(&self) -> T { + self.ecs.lock().resource::<T>().clone() + } + + /// Get a required ECS resource and call the given function with it. + pub fn map_resource<T: Resource, R>(&self, f: impl FnOnce(&T) -> R) -> R { + let ecs = self.ecs.lock(); + let value = ecs.resource::<T>(); + f(value) + } + + /// Get an optional ECS resource and call the given function with it. + pub fn map_get_resource<T: Resource, R>(&self, f: impl FnOnce(Option<&T>) -> R) -> R { + let ecs = self.ecs.lock(); + let value = ecs.get_resource::<T>(); + f(value) + } + + /// Get an `RwLock` with a reference to our (potentially shared) world. + /// + /// This gets the [`Instance`] from the client's [`InstanceHolder`] + /// component. If it's a normal client, then it'll be the same as the + /// world the client has loaded. If the client is using a shared world, + /// then the shared world will be a superset of the client's world. + pub fn world(&self) -> Arc<RwLock<Instance>> { + let instance_holder = self.component::<InstanceHolder>(); + instance_holder.instance.clone() + } + + /// Get an `RwLock` with a reference to the world that this client has + /// loaded. + /// + /// ``` + /// # use azalea_core::position::ChunkPos; + /// # fn example(client: &azalea::Client) { + /// let world = client.partial_world(); + /// let is_0_0_loaded = world.read().chunks.limited_get(&ChunkPos::new(0, 0)).is_some(); + /// # } + pub fn partial_world(&self) -> Arc<RwLock<PartialInstance>> { + let instance_holder = self.component::<InstanceHolder>(); + instance_holder.partial_instance.clone() + } + + /// 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.query_self::<Option<&InstanceName>, _>(|ins| ins.is_some()) + } +} + +impl Client { + /// Get the position of this client. + /// + /// This is a shortcut for `Vec3::from(&bot.component::<Position>())`. + /// + /// Note that this value is given a default of [`Vec3::ZERO`] when it + /// receives the login packet, its true position may be set ticks + /// later. + pub fn position(&self) -> Vec3 { + Vec3::from( + &self + .get_component::<Position>() + .expect("the client's position hasn't been initialized yet"), + ) + } + + /// Get the bounding box dimensions for our client, which contains our + /// width, height, and eye height. + /// + /// This is a shortcut for + /// `self.component::<EntityDimensions>()`. + pub fn dimensions(&self) -> EntityDimensions { + self.component::<EntityDimensions>() + } + + /// Get the position of this client's eyes. + /// + /// This is a shortcut for + /// `bot.position().up(bot.dimensions().eye_height)`. + pub fn eye_position(&self) -> Vec3 { + self.query_self::<(&Position, &EntityDimensions), _>(|(pos, dim)| { + pos.up(dim.eye_height as f64) + }) + } + + /// Get the health of this client. + /// + /// This is a shortcut for `*bot.component::<Health>()`. + pub fn health(&self) -> f32 { + *self.component::<Health>() + } + + /// Get the hunger level of this client, which includes both food and + /// saturation. + /// + /// This is a shortcut for `self.component::<Hunger>().to_owned()`. + pub fn hunger(&self) -> Hunger { + self.component::<Hunger>().to_owned() + } + + /// Get the username of this client. + /// + /// This is a shortcut for + /// `bot.component::<GameProfileComponent>().name.to_owned()`. + pub fn username(&self) -> String { + self.profile().name.to_owned() + } + + /// Get the Minecraft UUID of this client. + /// + /// This is a shortcut for `bot.component::<GameProfileComponent>().uuid`. + pub fn uuid(&self) -> Uuid { + self.profile().uuid + } + + /// Get a map of player UUIDs to their information in the tab list. + /// + /// This is a shortcut for `*bot.component::<TabList>()`. + pub fn tab_list(&self) -> HashMap<Uuid, PlayerInfo> { + (*self.component::<TabList>()).clone() + } + + /// Returns the [`GameProfile`] for our client. This contains your username, + /// UUID, and skin data. + /// + /// These values are set by the server upon login, which means they might + /// not match up with your actual game profile. Also, note that the username + /// and skin that gets displayed in-game will actually be the ones from + /// the tab list, which you can get from [`Self::tab_list`]. + /// + /// This as also available from the ECS as [`GameProfileComponent`]. + pub fn profile(&self) -> GameProfile { + (*self.component::<GameProfileComponent>()).clone() + } + + /// Returns the attribute values of our player, which can be used to + /// determine things like our movement speed. + pub fn attributes(&self) -> Attributes { + self.component::<Attributes>() + } + + /// A convenience function to get the Minecraft Uuid of a player by their + /// username, if they're present in the tab list. + /// + /// You can chain this with [`Client::entity_by_uuid`] to get the ECS + /// `Entity` for the player. + pub fn player_uuid_by_username(&self, username: &str) -> Option<Uuid> { + self.tab_list() + .values() + .find(|player| player.profile.name == username) + .map(|player| player.profile.uuid) + } + + /// Get an ECS `Entity` in the world by its Minecraft UUID, if it's within + /// render distance. + pub fn entity_by_uuid(&self, uuid: Uuid) -> Option<Entity> { + self.map_resource::<EntityUuidIndex, _>(|entity_uuid_index| entity_uuid_index.get(&uuid)) + } + + /// Convert an ECS `Entity` to a [`MinecraftEntityId`]. + pub fn minecraft_entity_by_ecs_entity(&self, entity: Entity) -> Option<MinecraftEntityId> { + self.query_self::<&EntityIdIndex, _>(|entity_id_index| { + entity_id_index.get_by_ecs_entity(entity) + }) + } + /// Convert a [`MinecraftEntityId`] to an ECS `Entity`. + pub fn ecs_entity_by_minecraft_entity(&self, entity: MinecraftEntityId) -> Option<Entity> { + self.query_self::<&EntityIdIndex, _>(|entity_id_index| { + entity_id_index.get_by_minecraft_entity(entity) + }) + } + + /// Call the given function with the client's [`RegistryHolder`]. + /// + /// The player's instance (aka world) will be locked during this time, which + /// may result in a deadlock if you try to access the instance again while + /// in the function. + /// + /// [`RegistryHolder`]: azalea_core::registry_holder::RegistryHolder + pub fn with_registry_holder<R>( + &self, + f: impl FnOnce(&azalea_core::registry_holder::RegistryHolder) -> R, + ) -> R { + let instance = self.world(); + let registries = &instance.read().registries; + f(registries) + } + + /// Resolve the given registry to its name. + /// + /// This is necessary for data-driven registries like [`Enchantment`]. + /// + /// [`Enchantment`]: azalea_registry::data::Enchantment + pub fn resolve_registry_name( + &self, + registry: &impl ResolvableDataRegistry, + ) -> Option<Identifier> { + self.with_registry_holder(|registries| registry.key(registries).map(|r| r.into_ident())) + } + /// Resolve the given registry to its name and data and call the given + /// function with it. + /// + /// This is necessary for data-driven registries like [`Enchantment`]. + /// + /// If you just want the value name, use [`Self::resolve_registry_name`] + /// instead. + /// + /// [`Enchantment`]: azalea_registry::data::Enchantment + pub fn with_resolved_registry<R: ResolvableDataRegistry, Ret>( + &self, + registry: R, + f: impl FnOnce(&Identifier, &R::DeserializesTo) -> Ret, + ) -> Option<Ret> { + self.with_registry_holder(|registries| { + registry + .resolve(registries) + .map(|(name, data)| f(name, data)) + }) + } +} diff --git a/azalea/src/client_impl/movement.rs b/azalea/src/client_impl/movement.rs new file mode 100644 index 00000000..08624263 --- /dev/null +++ b/azalea/src/client_impl/movement.rs @@ -0,0 +1,104 @@ +use azalea_client::{ + PhysicsState, SprintDirection, StartSprintEvent, StartWalkEvent, WalkDirection, +}; +use azalea_entity::{Jumping, LookDirection}; + +use crate::Client; + +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 in `azalea`. + /// + /// If you're making a realistic client, calling this function every tick is + /// recommended. + pub fn set_jumping(&self, jumping: bool) { + self.query_self::<&mut Jumping, _>(|mut j| **j = jumping); + } + + /// Returns whether the player will try to jump next tick. + pub fn jumping(&self) -> bool { + *self.component::<Jumping>() + } + + pub fn set_crouching(&self, crouching: bool) { + self.query_self::<&mut PhysicsState, _>(|mut p| p.trying_to_crouch = crouching); + } + + /// Whether the client is currently trying to sneak. + /// + /// You may want to check the [`Pose`] instead. + pub fn crouching(&self) -> bool { + self.query_self::<&PhysicsState, _>(|p| p.trying_to_crouch) + } + + /// Sets the direction the client is looking. + /// + /// `y_rot` is yaw (looking to the side, between -180 to 180), and `x_rot` + /// is pitch (looking up and down, between -90 to 90). + /// + /// You can get these numbers from the vanilla f3 screen. + pub fn set_direction(&self, y_rot: f32, x_rot: f32) { + self.query_self::<&mut LookDirection, _>(|mut ld| { + ld.update(LookDirection::new(y_rot, x_rot)); + }); + } + + /// Returns the direction the client is looking. + /// + /// See [`Self::set_direction`] for more details. + pub fn direction(&self) -> (f32, f32) { + let look_direction: LookDirection = self.component::<LookDirection>(); + (look_direction.y_rot(), look_direction.x_rot()) + } + + /// Start walking in the given direction. + /// + /// To sprint, use [`Client::sprint`]. To stop walking, call walk with + /// [`WalkDirection::None`]. + /// + /// # Example + /// + /// ```rust,no_run + /// # use azalea::{Client, WalkDirection}; + /// # use std::time::Duration; + /// # async fn example(mut bot: &Client) { + /// // walk for one second + /// bot.walk(WalkDirection::Forward); + /// tokio::time::sleep(Duration::from_secs(1)).await; + /// bot.walk(WalkDirection::None); + /// # } + /// ``` + pub fn walk(&self, direction: WalkDirection) { + let mut ecs = self.ecs.lock(); + ecs.write_message(StartWalkEvent { + entity: self.entity, + direction, + }); + } + + /// Start sprinting in the given direction. + /// + /// o stop moving, call [`bot.walk(WalkDirection::None)`](Self::walk) + /// + /// # Example + /// + /// ```rust,no_run + /// # use azalea::{Client, WalkDirection, SprintDirection}; + /// # use std::time::Duration; + /// # async fn example(bot: &Client) { + /// // sprint for one second + /// bot.sprint(SprintDirection::Forward); + /// tokio::time::sleep(Duration::from_secs(1)).await; + /// bot.walk(WalkDirection::None); + /// # } + /// ``` + pub fn sprint(&self, direction: SprintDirection) { + let mut ecs = self.ecs.lock(); + ecs.write_message(StartSprintEvent { + entity: self.entity, + direction, + }); + } +} |
