aboutsummaryrefslogtreecommitdiff
path: root/azalea/src/client_impl
diff options
context:
space:
mode:
Diffstat (limited to 'azalea/src/client_impl')
-rw-r--r--azalea/src/client_impl/attack.rs50
-rw-r--r--azalea/src/client_impl/chat.rs49
-rw-r--r--azalea/src/client_impl/client_information.rs38
-rw-r--r--azalea/src/client_impl/entity_query.rs264
-rw-r--r--azalea/src/client_impl/interact.rs52
-rw-r--r--azalea/src/client_impl/inventory.rs45
-rw-r--r--azalea/src/client_impl/mining.rs29
-rw-r--r--azalea/src/client_impl/mod.rs508
-rw-r--r--azalea/src/client_impl/movement.rs104
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,
+ });
+ }
+}