aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md4
-rw-r--r--Cargo.lock2
-rw-r--r--azalea-client/README.md4
-rw-r--r--azalea-client/src/client.rs503
-rw-r--r--azalea-client/src/lib.rs4
-rw-r--r--azalea-client/src/local_player.rs11
-rw-r--r--azalea-client/src/plugins/attack.rs45
-rw-r--r--azalea-client/src/plugins/chat/mod.rs48
-rw-r--r--azalea-client/src/plugins/client_information.rs37
-rw-r--r--azalea-client/src/plugins/interact/mod.rs47
-rw-r--r--azalea-client/src/plugins/inventory/mod.rs41
-rw-r--r--azalea-client/src/plugins/mining.rs26
-rw-r--r--azalea-client/src/plugins/mod.rs2
-rw-r--r--azalea-client/src/plugins/movement.rs101
-rw-r--r--azalea-client/src/plugins/tick_broadcast.rs50
-rw-r--r--azalea-client/src/plugins/tick_counter.rs10
-rw-r--r--azalea-world/Cargo.toml2
-rw-r--r--azalea-world/src/find_blocks.rs2
-rw-r--r--azalea/examples/testbot/commands/debug.rs1
-rw-r--r--azalea/src/auto_tool.rs14
-rw-r--r--azalea/src/bot.rs103
-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.rs (renamed from azalea-client/src/entity_query.rs)13
-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
-rw-r--r--azalea/src/container.rs108
-rw-r--r--azalea/src/lib.rs4
-rw-r--r--azalea/src/pathfinder/mod.rs6
-rw-r--r--azalea/src/prelude.rs6
-rw-r--r--azalea/src/swarm/mod.rs4
-rw-r--r--azalea/src/tick_broadcast.rs93
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.
diff --git a/Cargo.lock b/Cargo.lock
index ba67e69e..d62649d3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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()
+ }
+}