aboutsummaryrefslogtreecommitdiff
path: root/azalea-client/src/client.rs
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2025-12-27 22:02:00 -0600
committerGitHub <noreply@github.com>2025-12-27 22:02:00 -0600
commit9513f42e87f64c409cdb2a100500a50e5a713bac (patch)
treebb6aa8b6d50fddf967bcb1f759e023754ea84e49 /azalea-client/src/client.rs
parent588902ba4a3965982bdd84d92b20c6f7613f3978 (diff)
downloadazalea-drasl-9513f42e87f64c409cdb2a100500a50e5a713bac.tar.xz
Move Client struct to azalea crate (#297)
* move the Client struct out of azalea-client into azalea * actually add client impls in azalea
Diffstat (limited to 'azalea-client/src/client.rs')
-rw-r--r--azalea-client/src/client.rs503
1 files changed, 8 insertions, 495 deletions
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.
///