diff options
36 files changed, 1086 insertions, 1080 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 33037c51..bb0032c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,12 @@ is breaking anyways, semantic versioning is not followed. ### Added +- Re-implement `Client::map_component` and `map_get_component`. + ### Changed +- Move the `Client` struct out of `azalea-client` into `azalea`. + ### Fixed - Serializing `FormattedText` with serde was writing `extra` twice. @@ -549,9 +549,9 @@ dependencies = [ name = "azalea-world" version = "0.15.0+mc1.21.11" dependencies = [ + "azalea", "azalea-block", "azalea-buf", - "azalea-client", "azalea-core", "azalea-registry", "bevy_ecs", diff --git a/azalea-client/README.md b/azalea-client/README.md index 73ef1769..3f7b149d 100644 --- a/azalea-client/README.md +++ b/azalea-client/README.md @@ -1,5 +1,5 @@ # `azalea-client` -A library that can mimic everything that a normal Minecraft client can do. +A library for creating Minecraft clients with Bevy. -To make a bot with higher-level functions, consider using the `azalea` crate instead. +If you intend on creating a bot, consider using the [`azalea`](https://docs.rs/azalea) crate instead. diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index 0b6de086..4c45b537 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -1,5 +1,4 @@ use std::{ - collections::HashMap, fmt::Debug, mem, sync::Arc, @@ -7,521 +6,35 @@ use std::{ time::{Duration, Instant}, }; -use azalea_auth::game_profile::GameProfile; -use azalea_core::{ - data_registry::{DataRegistryWithKey, ResolvableDataRegistry}, - position::Vec3, - tick::GameTick, -}; +use azalea_core::tick::GameTick; use azalea_entity::{ - Attributes, EntityUpdateSystems, PlayerAbilities, Position, - dimensions::EntityDimensions, - indexing::{EntityIdIndex, EntityUuidIndex}, - inventory::Inventory, - metadata::Health, + EntityUpdateSystems, PlayerAbilities, indexing::EntityIdIndex, inventory::Inventory, }; use azalea_physics::local_player::PhysicsState; -use azalea_protocol::{ - address::{ResolvableAddr, ResolvedAddr}, - connect::Proxy, - packets::{Packet, game::ServerboundGamePacket}, - resolve::ResolveError, -}; -use azalea_registry::{DataRegistryKeyRef, identifier::Identifier}; -use azalea_world::{Instance, InstanceContainer, InstanceName, MinecraftEntityId, PartialInstance}; +use azalea_world::InstanceContainer; use bevy_app::{App, AppExit, Plugin, PluginsState, SubApp, Update}; use bevy_ecs::{ message::MessageCursor, prelude::*, schedule::{InternedScheduleLabel, LogLevel, ScheduleBuildSettings}, }; -use parking_lot::{Mutex, RwLock}; -use tokio::{ - sync::{ - mpsc::{self}, - oneshot, - }, - time, -}; +use parking_lot::Mutex; +use tokio::{sync::oneshot, time}; use tracing::{info, warn}; -use uuid::Uuid; use crate::{ - Account, DefaultPlugins, - attack::{self}, + attack, block_update::QueuedServerBlockUpdates, chunks::ChunkBatchInfo, connection::RawConnection, cookies::ServerCookies, - disconnect::DisconnectEvent, - events::Event, interact::BlockStatePredictionHandler, - join::{ConnectOpts, StartJoinServerEvent}, local_player::{Hunger, InstanceHolder, PermissionLevel, TabList}, - mining::{self}, + mining, movement::LastSentLookDirection, - packet::game::SendGamePacketEvent, - player::{GameProfileComponent, PlayerInfo, retroactively_add_game_profile_component}, + player::retroactively_add_game_profile_component, }; -/// 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_client::{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::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::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)) - }) - } -} - /// A bundle of components that's inserted right when we switch to the `login` /// state and stay present on our clients until we disconnect. /// diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs index cff0b03a..d9e66333 100644 --- a/azalea-client/src/lib.rs +++ b/azalea-client/src/lib.rs @@ -4,7 +4,6 @@ mod account; mod client; -mod entity_query; pub mod local_player; pub mod ping; pub mod player; @@ -21,8 +20,7 @@ pub use azalea_protocol::common::client_information::ClientInformation; // version. pub use bevy_tasks; pub use client::{ - Client, InConfigState, InGameState, JoinedClientBundle, LocalPlayerBundle, StartClientOpts, - start_ecs_runner, + InConfigState, InGameState, JoinedClientBundle, LocalPlayerBundle, start_ecs_runner, }; pub use events::Event; pub use movement::{StartSprintEvent, StartWalkEvent}; diff --git a/azalea-client/src/local_player.rs b/azalea-client/src/local_player.rs index 4062e32e..977b38f3 100644 --- a/azalea-client/src/local_player.rs +++ b/azalea-client/src/local_player.rs @@ -63,13 +63,12 @@ pub struct PermissionLevel(pub u8); /// /// ``` /// # use azalea_client::local_player::TabList; -/// # fn example(client: &azalea_client::Client) { -/// let tab_list = client.component::<TabList>(); -/// println!("Online players:"); -/// for (uuid, player_info) in tab_list.iter() { -/// println!("- {} ({}ms)", player_info.profile.name, player_info.latency); +/// fn example(tab_list: &TabList) { +/// println!("Online players:"); +/// for (uuid, player_info) in tab_list.iter() { +/// println!("- {} ({}ms)", player_info.profile.name, player_info.latency); +/// } /// } -/// # } /// ``` /// /// For convenience, `TabList` is also a resource in the ECS. diff --git a/azalea-client/src/plugins/attack.rs b/azalea-client/src/plugins/attack.rs index baab4333..41ce114d 100644 --- a/azalea-client/src/plugins/attack.rs +++ b/azalea-client/src/plugins/attack.rs @@ -12,7 +12,7 @@ use tracing::warn; use super::packet::game::SendGamePacketEvent; use crate::{ - Client, interact::SwingArmEvent, local_player::LocalGameMode, movement::MoveEventsSystems, + interact::SwingArmEvent, local_player::LocalGameMode, movement::MoveEventsSystems, respawn::perform_respawn, }; @@ -43,49 +43,6 @@ impl Plugin for AttackPlugin { } } -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 - } -} - /// A component that indicates that this client will be attacking the given /// entity next tick. #[derive(Clone, Component, Debug)] diff --git a/azalea-client/src/plugins/chat/mod.rs b/azalea-client/src/plugins/chat/mod.rs index bd90a8d6..11ad742c 100644 --- a/azalea-client/src/plugins/chat/mod.rs +++ b/azalea-client/src/plugins/chat/mod.rs @@ -14,8 +14,6 @@ use bevy_ecs::prelude::*; use handler::{SendChatKindEvent, handle_send_chat_kind_event}; use uuid::Uuid; -use crate::client::Client; - pub struct ChatPlugin; impl Plugin for ChatPlugin { fn build(&self, app: &mut App) { @@ -187,52 +185,6 @@ impl ChatPacket { } } -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::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(), - }); - } -} - /// A client received a chat message packet. #[derive(Clone, Debug, Message)] pub struct ChatReceivedEvent { diff --git a/azalea-client/src/plugins/client_information.rs b/azalea-client/src/plugins/client_information.rs index 98f69a2d..92b4f70a 100644 --- a/azalea-client/src/plugins/client_information.rs +++ b/azalea-client/src/plugins/client_information.rs @@ -1,13 +1,13 @@ use azalea_protocol::{ common::client_information::ClientInformation, - packets::{config::s_client_information::ServerboundClientInformation, game}, + packets::config::s_client_information::ServerboundClientInformation, }; use bevy_app::prelude::*; use bevy_ecs::prelude::*; use tracing::{debug, warn}; use super::packet::config::SendConfigPacketEvent; -use crate::{Client, brand::send_brand, packet::login::InLoginState}; +use crate::{brand::send_brand, packet::login::InLoginState}; /// Send [`ServerboundClientInformation`] on join. pub struct ClientInformationPlugin; @@ -42,36 +42,3 @@ pub fn send_client_information( )); } } - -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::{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-client/src/plugins/interact/mod.rs b/azalea-client/src/plugins/interact/mod.rs index df614b8a..d2a4ef44 100644 --- a/azalea-client/src/plugins/interact/mod.rs +++ b/azalea-client/src/plugins/interact/mod.rs @@ -38,7 +38,6 @@ use tracing::warn; use super::mining::Mining; use crate::{ - Client, attack::handle_attack_event, interact::pick::{HitResultComponent, update_hit_result_component}, inventory::InventorySystems, @@ -85,52 +84,6 @@ impl Plugin for InteractPlugin { #[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)] pub struct UpdateAttributesSystems; -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, - }); - } -} - /// A component that contains information about our local block state /// predictions. #[derive(Clone, Component, Debug, Default)] diff --git a/azalea-client/src/plugins/inventory/mod.rs b/azalea-client/src/plugins/inventory/mod.rs index 09c0d78f..740decb1 100644 --- a/azalea-client/src/plugins/inventory/mod.rs +++ b/azalea-client/src/plugins/inventory/mod.rs @@ -18,7 +18,6 @@ use indexmap::IndexMap; use tracing::{error, warn}; use crate::{ - Client, inventory::equipment_effects::{collect_equipment_changes, handle_equipment_changes}, packet::game::SendGamePacketEvent, }; @@ -56,46 +55,6 @@ impl Plugin for InventoryPlugin { #[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)] pub struct InventorySystems; -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::<&Inv, _>(|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::<&Inv, _>(|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, - }); - } -} - /// A Bevy trigger that's fired when our client should show a new screen (like a /// chest or crafting table). /// diff --git a/azalea-client/src/plugins/mining.rs b/azalea-client/src/plugins/mining.rs index 56136362..e9dcbe59 100644 --- a/azalea-client/src/plugins/mining.rs +++ b/azalea-client/src/plugins/mining.rs @@ -15,7 +15,6 @@ use derive_more::{Deref, DerefMut}; use tracing::{debug, trace, warn}; use crate::{ - Client, interact::{ BlockStatePredictionHandler, SwingArmEvent, can_use_game_master_blocks, check_is_interaction_restricted, pick::HitResultComponent, @@ -71,31 +70,6 @@ impl Plugin for MiningPlugin { #[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)] pub struct MiningSystems; -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>(); - } - } -} - /// A component that simulates the client holding down left click to mine the /// block that it's facing, but this only interacts with blocks and not /// entities. diff --git a/azalea-client/src/plugins/mod.rs b/azalea-client/src/plugins/mod.rs index 2ccbb3cc..a4aec19f 100644 --- a/azalea-client/src/plugins/mod.rs +++ b/azalea-client/src/plugins/mod.rs @@ -24,7 +24,6 @@ pub mod packet; pub mod pong; pub mod respawn; pub mod task_pool; -pub mod tick_broadcast; pub mod tick_counter; pub mod tick_end; @@ -58,7 +57,6 @@ impl PluginGroup for DefaultPlugins { .add(loading::PlayerLoadedPlugin) .add(brand::BrandPlugin) .add(client_information::ClientInformationPlugin) - .add(tick_broadcast::TickBroadcastPlugin) .add(tick_counter::TickCounterPlugin) .add(pong::PongPlugin) .add(connection::ConnectionPlugin) diff --git a/azalea-client/src/plugins/movement.rs b/azalea-client/src/plugins/movement.rs index c4409722..c9473ebf 100644 --- a/azalea-client/src/plugins/movement.rs +++ b/azalea-client/src/plugins/movement.rs @@ -35,7 +35,6 @@ use bevy_app::{App, Plugin, Update}; use bevy_ecs::prelude::*; use crate::{ - client::Client, local_player::{Hunger, InstanceHolder, LocalGameMode}, packet::game::SendGamePacketEvent, }; @@ -77,55 +76,6 @@ impl Plugin for MovementPlugin { #[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)] pub struct MoveEventsSystems; -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()) - } -} - /// A component that contains the look direction that was last sent over the /// network. #[derive(Clone, Component, Debug, Default)] @@ -515,57 +465,6 @@ fn distance_to_unit_square(v: Vec2) -> f32 { (1. + ratio * ratio).sqrt() } -impl Client { - /// 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::{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::{Client, WalkDirection, SprintDirection}; - /// # use std::time::Duration; - /// # async fn example(mut 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, - }); - } -} - /// An event sent when the client starts walking. /// /// This does not get sent for non-local entities. diff --git a/azalea-client/src/plugins/tick_broadcast.rs b/azalea-client/src/plugins/tick_broadcast.rs deleted file mode 100644 index e51716cc..00000000 --- a/azalea-client/src/plugins/tick_broadcast.rs +++ /dev/null @@ -1,50 +0,0 @@ -use azalea_core::tick::GameTick; -use bevy_app::prelude::*; -use bevy_ecs::prelude::*; -use derive_more::Deref; -use tokio::sync::broadcast; - -/// A resource that contains a [`broadcast::Sender`] that will be sent every -/// Minecraft tick. -/// -/// This is useful for running code every schedule from async user code. -/// -/// ``` -/// use azalea_client::tick_broadcast::TickBroadcast; -/// # async fn example(client: azalea_client::Client) { -/// let mut receiver = { -/// let ecs = client.ecs.lock(); -/// let tick_broadcast = ecs.resource::<TickBroadcast>(); -/// tick_broadcast.subscribe() -/// }; -/// while receiver.recv().await.is_ok() { -/// // do something -/// } -/// # } -/// ``` -#[derive(Deref, Resource)] -pub struct TickBroadcast(broadcast::Sender<()>); -/// A resource that contains a [`broadcast::Sender`] that will be sent every -/// Azalea ECS Update. -/// -/// Also see [`TickBroadcast`]. -#[derive(Deref, Resource)] -pub struct UpdateBroadcast(broadcast::Sender<()>); - -pub fn send_tick_broadcast(tick_broadcast: ResMut<TickBroadcast>) { - let _ = tick_broadcast.0.send(()); -} -pub fn send_update_broadcast(update_broadcast: ResMut<UpdateBroadcast>) { - let _ = update_broadcast.0.send(()); -} -/// A plugin that makes the [`UpdateBroadcast`] and [`TickBroadcast`] resources -/// available. -pub struct TickBroadcastPlugin; -impl Plugin for TickBroadcastPlugin { - fn build(&self, app: &mut App) { - app.insert_resource(TickBroadcast(broadcast::channel(1).0)) - .insert_resource(UpdateBroadcast(broadcast::channel(1).0)) - .add_systems(GameTick, send_tick_broadcast) - .add_systems(Update, send_update_broadcast); - } -} diff --git a/azalea-client/src/plugins/tick_counter.rs b/azalea-client/src/plugins/tick_counter.rs index 2f4086a0..e4b5f0a4 100644 --- a/azalea-client/src/plugins/tick_counter.rs +++ b/azalea-client/src/plugins/tick_counter.rs @@ -4,7 +4,7 @@ use azalea_world::InstanceName; use bevy_app::{App, Plugin}; use bevy_ecs::prelude::*; -use crate::{mining::MiningSystems, movement::send_position, tick_broadcast::send_tick_broadcast}; +use crate::{mining::MiningSystems, movement::send_position}; /// Counts the number of game ticks elapsed on the **local client** since the /// `login` packet was received. @@ -22,14 +22,14 @@ impl Plugin for TickCounterPlugin { increment_counter .before(PhysicsSystems) .before(MiningSystems) - .before(send_position) - .before(send_tick_broadcast), + .before(send_position), ); } } -/// Increment the [`GameTickCounter`] on every entity that lives in an instance. -fn increment_counter(mut query: Query<&mut TicksConnected, With<InstanceName>>) { +/// Increment the [`TicksConnected`] component on every entity +/// that lives in an instance. +pub fn increment_counter(mut query: Query<&mut TicksConnected, With<InstanceName>>) { for mut counter in &mut query { counter.0 += 1; } diff --git a/azalea-world/Cargo.toml b/azalea-world/Cargo.toml index 5df028b8..31e42a80 100644 --- a/azalea-world/Cargo.toml +++ b/azalea-world/Cargo.toml @@ -7,7 +7,7 @@ license.workspace = true repository.workspace = true [dev-dependencies] -azalea-client.path = "../azalea-client" +azalea.path = "../azalea" criterion.workspace = true [dependencies] diff --git a/azalea-world/src/find_blocks.rs b/azalea-world/src/find_blocks.rs index 5edf3ea2..bd4ec09a 100644 --- a/azalea-world/src/find_blocks.rs +++ b/azalea-world/src/find_blocks.rs @@ -11,7 +11,7 @@ impl Instance { /// /// ``` /// # use azalea_registry::builtin::BlockKind; - /// # fn example(client: &azalea_client::Client) { + /// # fn example(client: &azalea::Client) { /// client /// .world() /// .read() diff --git a/azalea/examples/testbot/commands/debug.rs b/azalea/examples/testbot/commands/debug.rs index 68f06dc3..edadd697 100644 --- a/azalea/examples/testbot/commands/debug.rs +++ b/azalea/examples/testbot/commands/debug.rs @@ -10,7 +10,6 @@ use azalea::{ interact::pick::HitResultComponent, packet::game, pathfinder::{ExecutingPath, Pathfinder}, - prelude::ContainerClientExt, world::MinecraftEntityId, }; use azalea_core::hit_result::HitResult; diff --git a/azalea/src/auto_tool.rs b/azalea/src/auto_tool.rs index 06103976..519c5dc9 100644 --- a/azalea/src/auto_tool.rs +++ b/azalea/src/auto_tool.rs @@ -1,11 +1,10 @@ use azalea_block::{BlockState, BlockTrait, fluid_state::FluidKind}; -use azalea_client::Client; use azalea_core::position::BlockPos; use azalea_entity::{ActiveEffects, Attributes, FluidOnEyes, Physics, inventory::Inventory}; use azalea_inventory::{ItemStack, Menu, components}; use azalea_registry::builtin::{BlockKind, EntityKind, ItemKind}; -use crate::bot::BotClientExt; +use crate::Client; #[derive(Debug)] pub struct BestToolResult { @@ -13,13 +12,8 @@ pub struct BestToolResult { pub percentage_per_tick: f32, } -pub trait AutoToolClientExt { - fn best_tool_in_hotbar_for_block(&self, block: BlockState) -> BestToolResult; - fn mine_with_auto_tool(&self, block_pos: BlockPos) -> impl Future<Output = ()> + Send; -} - -impl AutoToolClientExt for Client { - fn best_tool_in_hotbar_for_block(&self, block: BlockState) -> BestToolResult { +impl Client { + pub fn best_tool_in_hotbar_for_block(&self, block: BlockState) -> BestToolResult { self.query_self::<( &Inventory, &Physics, @@ -41,7 +35,7 @@ impl AutoToolClientExt for Client { ) } - async fn mine_with_auto_tool(&self, block_pos: BlockPos) { + pub async fn mine_with_auto_tool(&self, block_pos: BlockPos) { let block_state = self .world() .read() diff --git a/azalea/src/bot.rs b/azalea/src/bot.rs index 33293466..6f39c2fe 100644 --- a/azalea/src/bot.rs +++ b/azalea/src/bot.rs @@ -1,9 +1,6 @@ use std::f64::consts::PI; -use azalea_client::{ - mining::Mining, - tick_broadcast::{TickBroadcast, UpdateBroadcast}, -}; +use azalea_client::mining::Mining; use azalea_core::{ position::{BlockPos, Vec3}, tick::GameTick, @@ -15,21 +12,17 @@ use azalea_entity::{ use azalea_physics::PhysicsSystems; use bevy_app::Update; use bevy_ecs::prelude::*; -use futures_lite::Future; use tracing::trace; use crate::{ - accept_resource_packs::AcceptResourcePacksPlugin, + Client, app::{App, Plugin, PluginGroup, PluginGroupBuilder}, - auto_respawn::AutoRespawnPlugin, - container::ContainerPlugin, ecs::{ component::Component, entity::Entity, query::{With, Without}, system::{Commands, Query}, }, - pathfinder::PathfinderPlugin, }; #[derive(Clone, Default)] @@ -86,41 +79,19 @@ fn stop_jumping(mut query: Query<(&mut Jumping, &mut Bot)>) { } } -/// A trait that adds a few additional functions to -/// [`Client`](azalea_client::Client) that help with making bots. -pub trait BotClientExt { +impl Client { /// Queue a jump for the next tick. - fn jump(&self); - /// Turn the bot's head to look at the coordinate in the world. - /// - /// To look at the center of a block, you should call [`BlockPos::center`]. - fn look_at(&self, pos: Vec3); - /// Get a receiver that will receive a message every tick. - fn get_tick_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()>; - /// Get a receiver that will receive a message every ECS Update. - fn get_update_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()>; - /// Wait for the specified number of game ticks. - fn wait_ticks(&self, n: usize) -> impl Future<Output = ()> + Send; - /// Wait for the specified number of ECS `Update`s. - fn wait_updates(&self, n: usize) -> impl Future<Output = ()> + Send; - /// Mine a block. - /// - /// This won't turn the bot's head towards the block, so if that's necessary - /// you'll have to do that yourself with [`look_at`]. - /// - /// [`look_at`]: crate::prelude::BotClientExt::look_at - fn mine(&self, position: BlockPos) -> impl Future<Output = ()> + Send; -} - -impl BotClientExt for azalea_client::Client { - fn jump(&self) { + pub fn jump(&self) { let mut ecs = self.ecs.lock(); ecs.write_message(JumpEvent { entity: self.entity, }); } - fn look_at(&self, position: Vec3) { + /// Turn the bot's head to look at the coordinate in the world. + /// + /// To look at the center of a block, you should call [`BlockPos::center`]. + pub fn look_at(&self, position: Vec3) { let mut ecs = self.ecs.lock(); ecs.write_message(LookAtEvent { entity: self.entity, @@ -128,50 +99,13 @@ impl BotClientExt for azalea_client::Client { }); } - /// Returns a Receiver that receives a message every game tick. - /// - /// This is useful if you want to efficiently loop until a certain condition - /// is met. - /// - /// ``` - /// # use azalea::prelude::*; - /// # use azalea::container::WaitingForInventoryOpen; - /// # async fn example(bot: &mut azalea::Client) { - /// let mut ticks = bot.get_tick_broadcaster(); - /// while ticks.recv().await.is_ok() { - /// let ecs = bot.ecs.lock(); - /// if ecs.get::<WaitingForInventoryOpen>(bot.entity).is_none() { - /// break; - /// } - /// } - /// # } - /// ``` - fn get_tick_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()> { - let ecs = self.ecs.lock(); - let tick_broadcast = ecs.resource::<TickBroadcast>(); - tick_broadcast.subscribe() - } - - /// Returns a Receiver that receives a message every ECS Update. - /// - /// ECS Updates happen at least at the frequency of game ticks, usually - /// faster. - /// - /// This is useful if you're sending an ECS event and want to make sure it's - /// been handled before continuing. - fn get_update_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()> { - let ecs = self.ecs.lock(); - let update_broadcast = ecs.resource::<UpdateBroadcast>(); - update_broadcast.subscribe() - } - /// Wait for the specified number of ticks using /// [`Self::get_tick_broadcaster`]. /// /// If you're going to run this in a loop, you may want to use that function /// instead and use the `Receiver` from it to avoid accidentally skipping /// ticks and having to wait longer. - async fn wait_ticks(&self, n: usize) { + pub async fn wait_ticks(&self, n: usize) { let mut receiver = self.get_tick_broadcaster(); for _ in 0..n { let _ = receiver.recv().await; @@ -186,14 +120,20 @@ impl BotClientExt for azalea_client::Client { /// If you're going to run this in a loop, you may want to use that function /// instead and use the `Receiver` from it to avoid accidentally skipping /// ticks and having to wait longer. - async fn wait_updates(&self, n: usize) { + pub async fn wait_updates(&self, n: usize) { let mut receiver = self.get_update_broadcaster(); for _ in 0..n { let _ = receiver.recv().await; } } - async fn mine(&self, position: BlockPos) { + /// Mine a block. + /// + /// This won't turn the bot's head towards the block, so if that's necessary + /// you'll have to do that yourself with [`look_at`]. + /// + /// [`look_at`]: crate::prelude::BotClientExt::look_at + pub async fn mine(&self, position: BlockPos) { self.start_mining(position); let mut receiver = self.get_tick_broadcaster(); @@ -266,9 +206,10 @@ impl PluginGroup for DefaultBotPlugins { fn build(self) -> PluginGroupBuilder { PluginGroupBuilder::start::<Self>() .add(BotPlugin) - .add(PathfinderPlugin) - .add(ContainerPlugin) - .add(AutoRespawnPlugin) - .add(AcceptResourcePacksPlugin) + .add(crate::pathfinder::PathfinderPlugin) + .add(crate::container::ContainerPlugin) + .add(crate::auto_respawn::AutoRespawnPlugin) + .add(crate::accept_resource_packs::AcceptResourcePacksPlugin) + .add(crate::tick_broadcast::TickBroadcastPlugin) } } 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-client/src/entity_query.rs b/azalea/src/client_impl/entity_query.rs index b291fe7d..85e46525 100644 --- a/azalea-client/src/entity_query.rs +++ b/azalea/src/client_impl/entity_query.rs @@ -21,7 +21,7 @@ impl Client { /// # Examples /// ``` /// # use azalea_world::InstanceName; - /// # fn example(mut client: azalea_client::Client) { + /// # fn example(mut client: azalea::Client) { /// let is_logged_in = client.query_self::<Option<&InstanceName>, _>(|ins| ins.is_some()); /// # } /// ``` @@ -91,8 +91,11 @@ impl Client { /// /// # Example /// ``` - /// use azalea_client::{Client, player::GameProfileComponent}; - /// use azalea_entity::{Position, metadata::Player}; + /// use azalea::{ + /// Client, + /// entity::{Position, metadata::Player}, + /// player::GameProfileComponent, + /// }; /// use bevy_ecs::query::With; /// /// # fn example(mut bot: Client, sender_name: String) { @@ -130,7 +133,7 @@ impl Client { /// use azalea_entity::{LocalEntity, Position, metadata::Player}; /// use bevy_ecs::query::{With, Without}; /// - /// # fn example(mut bot: azalea_client::Client, sender_name: String) { + /// # 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) @@ -157,7 +160,7 @@ impl Client { /// ``` /// # use azalea_entity::{LocalEntity, Position, metadata::Player}; /// # use bevy_ecs::query::{With, Without}; - /// # fn example(mut bot: azalea_client::Client, sender_name: String) { + /// # fn example(mut bot: azalea::Client, sender_name: String) { /// let nearby_players = /// bot.nearest_entities_by::<(), (With<Player>, Without<LocalEntity>)>(|_: ()| true); /// # } 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, + }); + } +} diff --git a/azalea/src/container.rs b/azalea/src/container.rs index 3ae45e28..3d5f6f01 100644 --- a/azalea/src/container.rs +++ b/azalea/src/container.rs @@ -2,7 +2,6 @@ use std::{fmt, fmt::Debug}; use azalea_chat::FormattedText; use azalea_client::{ - Client, inventory::{CloseContainerEvent, ContainerClickEvent}, packet::game::ReceiveGamePacketEvent, }; @@ -17,9 +16,8 @@ use azalea_protocol::packets::game::ClientboundGamePacket; use bevy_app::{App, Plugin, Update}; use bevy_ecs::{component::Component, prelude::MessageReader, system::Commands}; use derive_more::Deref; -use futures_lite::Future; -use crate::bot::BotClientExt; +use crate::Client; pub struct ContainerPlugin; impl Plugin for ContainerPlugin { @@ -28,7 +26,7 @@ impl Plugin for ContainerPlugin { } } -pub trait ContainerClientExt { +impl Client { /// Open a container in the world, like a chest. /// /// Use [`Client::open_inventory`] to open your own inventory. @@ -51,10 +49,10 @@ pub trait ContainerClientExt { /// let container = bot.open_container_at(target_pos).await; /// # } /// ``` - fn open_container_at( - &self, - pos: BlockPos, - ) -> impl Future<Output = Option<ContainerHandle>> + Send; + pub async fn open_container_at(&self, pos: BlockPos) -> Option<ContainerHandle> { + self.open_container_at_with_timeout_ticks(pos, Some(20 * 5)) + .await + } /// Open a container in the world, or time out after a specified amount of /// ticks. @@ -67,59 +65,7 @@ pub trait ContainerClientExt { /// /// The timeout is measured in game ticks (on the client, not the server), /// i.e. 1/20th of a second. - fn open_container_at_with_timeout_ticks( - &self, - pos: BlockPos, - timeout_ticks: Option<usize>, - ) -> impl Future<Output = Option<ContainerHandle>> + Send; - - /// Wait until a container is open, up to the specified number of ticks. - /// - /// Returns `None` if the container was immediately opened and closed, or if - /// the timeout expired. - /// - /// If `timeout_ticks` is None, there will be no timeout. - fn wait_for_container_open( - &self, - timeout_ticks: Option<usize>, - ) -> impl Future<Output = Option<ContainerHandle>> + Send; - - /// Open the player's inventory. - /// - /// This will return None if another container is open. - /// - /// Note that this will send a packet to the server once it's dropped. Also, - /// due to how it's implemented, you could call this function multiple times - /// while another inventory handle already exists (but you shouldn't). - /// - /// If you just want to get the items in the player's inventory without - /// sending any packets, use [`Client::menu`], [`Menu::player_slots_range`], - /// and [`Menu::slots`]. - fn open_inventory(&self) -> Option<ContainerHandle>; - /// Returns a [`ContainerHandleRef`] to the client's currently open - /// container, or their inventory. - /// - /// This will not send a packet to close the container when it's dropped, - /// which may cause anticheat compatibility issues if you modify your - /// inventory without closing it afterwards. - /// - /// To simulate opening your own inventory (like pressing 'e') in a way that - /// won't trigger anticheats, use [`Client::open_inventory`]. - /// - /// To open a container in the world, use [`Client::open_container_at`]. - fn get_inventory(&self) -> ContainerHandleRef; - /// Get the item in the bot's hotbar that is currently being held in its - /// main hand. - fn get_held_item(&self) -> ItemStack; -} - -impl ContainerClientExt for Client { - async fn open_container_at(&self, pos: BlockPos) -> Option<ContainerHandle> { - self.open_container_at_with_timeout_ticks(pos, Some(20 * 5)) - .await - } - - async fn open_container_at_with_timeout_ticks( + pub async fn open_container_at_with_timeout_ticks( &self, pos: BlockPos, timeout_ticks: Option<usize>, @@ -143,7 +89,13 @@ impl ContainerClientExt for Client { self.wait_for_container_open(timeout_ticks).await } - async fn wait_for_container_open( + /// Wait until a container is open, up to the specified number of ticks. + /// + /// Returns `None` if the container was immediately opened and closed, or if + /// the timeout expired. + /// + /// If `timeout_ticks` is None, there will be no timeout. + pub async fn wait_for_container_open( &self, timeout_ticks: Option<usize>, ) -> Option<ContainerHandle> { @@ -172,7 +124,18 @@ impl ContainerClientExt for Client { } } - fn open_inventory(&self) -> Option<ContainerHandle> { + /// Open the player's inventory. + /// + /// This will return None if another container is open. + /// + /// Note that this will send a packet to the server once it's dropped. Also, + /// due to how it's implemented, you could call this function multiple times + /// while another inventory handle already exists (but you shouldn't). + /// + /// If you just want to get the items in the player's inventory without + /// sending any packets, use [`Client::menu`], [`Menu::player_slots_range`], + /// and [`Menu::slots`]. + pub fn open_inventory(&self) -> Option<ContainerHandle> { let ecs = self.ecs.lock(); let inventory = ecs.get::<Inventory>(self.entity).expect("no inventory"); if inventory.id == 0 { @@ -182,11 +145,24 @@ impl ContainerClientExt for Client { } } - fn get_inventory(&self) -> ContainerHandleRef { + /// Returns a [`ContainerHandleRef`] to the client's currently open + /// container, or their inventory. + /// + /// This will not send a packet to close the container when it's dropped, + /// which may cause anticheat compatibility issues if you modify your + /// inventory without closing it afterwards. + /// + /// To simulate opening your own inventory (like pressing 'e') in a way that + /// won't trigger anticheats, use [`Client::open_inventory`]. + /// + /// To open a container in the world, use [`Client::open_container_at`]. + pub fn get_inventory(&self) -> ContainerHandleRef { self.query_self::<&Inventory, _>(|inv| ContainerHandleRef::new(inv.id, self.clone())) } - fn get_held_item(&self) -> ItemStack { + /// Get the item in the bot's hotbar that is currently being held in its + /// main hand. + pub fn get_held_item(&self) -> ItemStack { self.query_self::<&Inventory, _>(|inv| inv.held_item().clone()) } } @@ -274,7 +250,7 @@ impl ContainerHandleRef { /// /// ```no_run /// # use azalea::prelude::*; - /// # fn example(bot: &azalea::Client) { + /// # fn example(bot: &Client) { /// let inventory = bot.get_inventory(); /// let inventory_title = inventory.title().unwrap_or_default().to_string(); /// // would be true if an unnamed chest is open diff --git a/azalea/src/lib.rs b/azalea/src/lib.rs index 0b656896..2105daee 100644 --- a/azalea/src/lib.rs +++ b/azalea/src/lib.rs @@ -6,12 +6,14 @@ pub mod auto_respawn; pub mod auto_tool; pub mod bot; mod builder; +mod client_impl; pub mod container; mod join_opts; pub mod nearest_entity; pub mod pathfinder; pub mod prelude; pub mod swarm; +pub mod tick_broadcast; pub use azalea_auth as auth; pub use azalea_block as block; @@ -49,6 +51,8 @@ pub use builder::ClientBuilder; use futures::future::BoxFuture; pub use join_opts::JoinOpts; +pub use crate::client_impl::Client; + pub type BoxHandleFn<S, R> = Box<dyn Fn(Client, azalea_client::Event, S) -> BoxFuture<'static, R> + Send>; pub type HandleFn<S, Fut> = fn(Client, azalea_client::Event, S) -> Fut; diff --git a/azalea/src/pathfinder/mod.rs b/azalea/src/pathfinder/mod.rs index 90b506e9..155261cc 100644 --- a/azalea/src/pathfinder/mod.rs +++ b/azalea/src/pathfinder/mod.rs @@ -66,9 +66,9 @@ use self::{ moves::{ExecuteCtx, IsReachedCtx, SuccessorsFn}, }; use crate::{ - WalkDirection, + Client, WalkDirection, app::{App, Plugin}, - bot::{BotClientExt, JumpEvent, LookAtEvent}, + bot::{JumpEvent, LookAtEvent}, ecs::{ component::Component, entity::Entity, @@ -229,7 +229,7 @@ pub trait PathfinderClientExt { fn is_goto_target_reached(&self) -> bool; } -impl PathfinderClientExt for azalea_client::Client { +impl PathfinderClientExt for Client { async fn goto(&self, goal: impl Goal + 'static) { self.goto_with_opts(goal, PathfinderOpts::new()).await; } diff --git a/azalea/src/prelude.rs b/azalea/src/prelude.rs index 335244cc..2675c8f5 100644 --- a/azalea/src/prelude.rs +++ b/azalea/src/prelude.rs @@ -1,16 +1,14 @@ //! The Azalea prelude. Things that are necessary for a bare-bones bot are //! re-exported here. -pub use azalea_client::{Account, Client, Event}; +pub use azalea_client::{Account, Event}; pub use azalea_core::tick::GameTick; pub use bevy_app::AppExit; // this is necessary to make the macros that reference bevy_ecs work pub use crate::ecs as bevy_ecs; pub use crate::{ - ClientBuilder, - bot::BotClientExt, - container::ContainerClientExt, + Client, ClientBuilder, ecs::{component::Component, resource::Resource}, pathfinder::PathfinderClientExt, }; diff --git a/azalea/src/swarm/mod.rs b/azalea/src/swarm/mod.rs index 05efa083..c0f9cbca 100644 --- a/azalea/src/swarm/mod.rs +++ b/azalea/src/swarm/mod.rs @@ -12,7 +12,7 @@ use std::sync::{ atomic::{self, AtomicBool}, }; -use azalea_client::{Account, Client, Event, StartClientOpts, chat::ChatPacket, join::ConnectOpts}; +use azalea_client::{Account, Event, chat::ChatPacket, join::ConnectOpts}; use azalea_entity::LocalEntity; use azalea_protocol::address::ResolvedAddr; use azalea_world::InstanceContainer; @@ -24,7 +24,7 @@ use parking_lot::{Mutex, RwLock}; use tokio::{sync::mpsc, task}; use tracing::{debug, error, warn}; -use crate::JoinOpts; +use crate::{Client, JoinOpts, client_impl::StartClientOpts}; /// A swarm is a way to conveniently control many bots at once, while also /// being able to control bots at an individual level when desired. diff --git a/azalea/src/tick_broadcast.rs b/azalea/src/tick_broadcast.rs new file mode 100644 index 00000000..479466e2 --- /dev/null +++ b/azalea/src/tick_broadcast.rs @@ -0,0 +1,93 @@ +use azalea_core::tick::GameTick; +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; +use derive_more::Deref; +use tokio::sync::broadcast; + +use crate::Client; + +/// A plugin that makes the [`UpdateBroadcast`] and [`TickBroadcast`] resources +/// available. +pub struct TickBroadcastPlugin; +impl Plugin for TickBroadcastPlugin { + fn build(&self, app: &mut App) { + app.insert_resource(TickBroadcast(broadcast::channel(1).0)) + .insert_resource(UpdateBroadcast(broadcast::channel(1).0)) + .add_systems( + GameTick, + send_tick_broadcast.after(azalea_client::tick_counter::increment_counter), + ) + .add_systems(Update, send_update_broadcast); + } +} + +/// A resource that contains a [`broadcast::Sender`] that will be sent every +/// Minecraft tick (see [`GameTick`]). +/// +/// Also see [`Client::wait_ticks`] and [`Client::get_tick_broadcaster`]. +/// +/// ``` +/// use azalea::tick_broadcast::TickBroadcast; +/// async fn example(tick_broadcast: &TickBroadcast) { +/// let mut receiver = tick_broadcast.subscribe(); +/// +/// while receiver.recv().await.is_ok() { +/// // do something +/// } +/// } +/// ``` +#[derive(Deref, Resource)] +pub struct TickBroadcast(broadcast::Sender<()>); + +/// A resource that contains a [`broadcast::Sender`] that will be sent every +/// Azalea ECS `Update`. +/// +/// Also see [`TickBroadcast`]. +#[derive(Deref, Resource)] +pub struct UpdateBroadcast(broadcast::Sender<()>); + +pub fn send_tick_broadcast(tick_broadcast: ResMut<TickBroadcast>) { + let _ = tick_broadcast.0.send(()); +} +pub fn send_update_broadcast(update_broadcast: ResMut<UpdateBroadcast>) { + let _ = update_broadcast.0.send(()); +} + +impl Client { + /// Returns a Receiver that receives a message every game tick. + /// + /// This is useful if you want to efficiently loop until a certain condition + /// is met. + /// + /// ``` + /// # use azalea::prelude::*; + /// # use azalea::container::WaitingForInventoryOpen; + /// # async fn example(bot: &mut azalea::Client) { + /// let mut ticks = bot.get_tick_broadcaster(); + /// while ticks.recv().await.is_ok() { + /// let ecs = bot.ecs.lock(); + /// if ecs.get::<WaitingForInventoryOpen>(bot.entity).is_none() { + /// break; + /// } + /// } + /// # } + /// ``` + pub fn get_tick_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()> { + let ecs = self.ecs.lock(); + let tick_broadcast = ecs.resource::<TickBroadcast>(); + tick_broadcast.subscribe() + } + + /// Returns a Receiver that receives a message every ECS Update. + /// + /// ECS Updates happen at least at the frequency of game ticks, usually + /// faster. + /// + /// This is useful if you're sending an ECS event and want to make sure it's + /// been handled before continuing. + pub fn get_update_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()> { + let ecs = self.ecs.lock(); + let update_broadcast = ecs.resource::<UpdateBroadcast>(); + update_broadcast.subscribe() + } +} |
