diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index 8ae236df..c6530e75 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -15,19 +15,17 @@ use azalea_core::{ tick::GameTick, }; use azalea_entity::{ - EntityUpdateSet, EyeHeight, LocalEntity, Position, + EntityUpdateSet, EyeHeight, Position, indexing::{EntityIdIndex, EntityUuidIndex}, metadata::Health, }; use azalea_protocol::{ ServerAddress, common::client_information::ClientInformation, - connect::{Connection, ConnectionError, Proxy}, + connect::{ConnectionError, Proxy}, packets::{ - self, ClientIntention, ConnectionProtocol, PROTOCOL_VERSION, Packet, + self, Packet, game::{self, ServerboundGamePacket}, - handshake::s_intention::ServerboundIntention, - login::s_hello::ServerboundHello, }, resolver, }; @@ -38,7 +36,7 @@ use bevy_ecs::{ component::Component, entity::Entity, schedule::{InternedScheduleLabel, IntoSystemConfigs, LogLevel, ScheduleBuildSettings}, - system::{Commands, Resource}, + system::Resource, world::World, }; use parking_lot::{Mutex, RwLock}; @@ -57,19 +55,16 @@ use crate::{ chunks::ChunkBatchInfo, connection::RawConnection, disconnect::DisconnectEvent, - events::{Event, LocalPlayerEvents}, + events::Event, interact::CurrentSequenceNumber, inventory::Inventory, + join::{StartJoinCallback, StartJoinServerEvent}, local_player::{ GameProfileComponent, Hunger, InstanceHolder, PermissionLevel, PlayerAbilities, TabList, }, mining::{self}, movement::{LastSentLookDirection, PhysicsState}, - packet::{ - as_system, - game::SendPacketEvent, - login::{InLoginState, SendLoginPacketEvent}, - }, + packet::game::SendPacketEvent, player::retroactively_add_game_profile_component, }; @@ -233,100 +228,26 @@ impl Client { event_sender, }: StartClientOpts<'_>, ) -> Result { - // check if an entity with our uuid already exists in the ecs and if so then - // just use that - let entity = { - let mut ecs = ecs_lock.lock(); + // send a StartJoinServerEvent - let entity_uuid_index = ecs.resource::(); - let uuid = account.uuid_or_offline(); - let entity = if let Some(entity) = entity_uuid_index.get(&account.uuid_or_offline()) { - debug!("Reusing entity {entity:?} for client"); - entity - } else { - let entity = ecs.spawn_empty().id(); - debug!("Created new entity {entity:?} for client"); - // add to the uuid index - let mut entity_uuid_index = ecs.resource_mut::(); - entity_uuid_index.insert(uuid, entity); - entity - }; + let (start_join_callback_tx, mut start_join_callback_rx) = + mpsc::unbounded_channel::>(); - let mut entity_mut = ecs.entity_mut(entity); - entity_mut.insert(( - InLoginState, - // add the Account to the entity now so plugins can access it earlier - account.to_owned(), - // localentity is always present for our clients, even if we're not actually logged - // in - LocalEntity, - )); - if let Some(event_sender) = event_sender { - // this is optional so we don't leak memory in case the user doesn't want to - // handle receiving packets - entity_mut.insert(LocalPlayerEvents(event_sender)); - } - - entity - }; - - let mut conn = if let Some(proxy) = proxy { - Connection::new_with_proxy(resolved_address, proxy).await? - } else { - Connection::new(resolved_address).await? - }; - debug!("Created connection to {resolved_address:?}"); - - conn.write(ServerboundIntention { - protocol_version: PROTOCOL_VERSION, - hostname: address.host.clone(), - port: address.port, - intention: ClientIntention::Login, - }) - .await?; - let conn = conn.login(); - - let (read_conn, write_conn) = conn.into_split(); - let (read_conn, write_conn) = (read_conn.raw, write_conn.raw); - - // insert the client into the ecs so it finishes logging in { let mut ecs = ecs_lock.lock(); - - let instance = Instance::default(); - let instance_holder = crate::local_player::InstanceHolder::new( - entity, - // default to an empty world, it'll be set correctly later when we - // get the login packet - Arc::new(RwLock::new(instance)), - ); - - let mut entity = ecs.entity_mut(entity); - entity.insert(( - // these stay when we switch to the game state - LocalPlayerBundle { - raw_connection: RawConnection::new( - read_conn, - write_conn, - ConnectionProtocol::Login, - ), - client_information: crate::ClientInformation::default(), - instance_holder, - metadata: azalea_entity::metadata::PlayerMetadataBundle::default(), - }, - )); + ecs.send_event(StartJoinServerEvent { + account: account.clone(), + address: address.clone(), + resolved_address: *resolved_address, + proxy, + event_sender: event_sender.clone(), + start_join_callback_tx: Some(StartJoinCallback(start_join_callback_tx)), + }); } - as_system::(&mut ecs_lock.lock(), |mut commands| { - commands.entity(entity).insert((InLoginState,)); - commands.trigger(SendLoginPacketEvent::new( - entity, - ServerboundHello { - name: account.username.clone(), - profile_id: account.uuid_or_offline(), - }, - )) - }); + let entity = start_join_callback_rx.recv().await.expect( + "StartJoinCallback should not be dropped before sending a message, this is a bug in Azalea", + )?; let client = Client::new(entity, ecs_lock.clone()); Ok(client) @@ -732,7 +653,9 @@ impl Plugin for AzaleaPlugin { Update, ( // add GameProfileComponent when we get an AddPlayerEvent - retroactively_add_game_profile_component.after(EntityUpdateSet::Index), + retroactively_add_game_profile_component + .after(EntityUpdateSet::Index) + .after(crate::join::handle_start_join_server_event), ), ) .init_resource::() diff --git a/azalea-client/src/plugins/join.rs b/azalea-client/src/plugins/join.rs new file mode 100644 index 00000000..3f47d90c --- /dev/null +++ b/azalea-client/src/plugins/join.rs @@ -0,0 +1,198 @@ +use std::{net::SocketAddr, sync::Arc}; + +use azalea_entity::{LocalEntity, indexing::EntityUuidIndex}; +use azalea_protocol::{ + ServerAddress, + connect::{Connection, ConnectionError, Proxy}, + packets::{ + ClientIntention, ConnectionProtocol, PROTOCOL_VERSION, + handshake::ServerboundIntention, + login::{ClientboundLoginPacket, ServerboundHello, ServerboundLoginPacket}, + }, +}; +use azalea_world::Instance; +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; +use bevy_tasks::{IoTaskPool, Task, futures_lite::future}; +use parking_lot::RwLock; +use tokio::sync::mpsc; +use tracing::{debug, warn}; + +use super::events::LocalPlayerEvents; +use crate::{ + Account, JoinError, LocalPlayerBundle, + connection::RawConnection, + packet::login::{InLoginState, SendLoginPacketEvent}, +}; + +/// A plugin that allows bots to join servers. +pub struct JoinPlugin; +impl Plugin for JoinPlugin { + fn build(&self, app: &mut App) { + app.add_event::().add_systems( + Update, + (handle_start_join_server_event, poll_create_connection_task), + ); + } +} + +#[derive(Event, Debug)] +pub struct StartJoinServerEvent { + pub account: Account, + pub address: ServerAddress, + pub resolved_address: SocketAddr, + pub proxy: Option, + pub event_sender: Option>, + + pub start_join_callback_tx: Option, +} + +// this is mpsc instead of oneshot so it can be cloned (since it's sent in an +// event) +#[derive(Component, Debug, Clone)] +pub struct StartJoinCallback(pub mpsc::UnboundedSender>); + +pub fn handle_start_join_server_event( + mut commands: Commands, + mut events: EventReader, + mut entity_uuid_index: ResMut, +) { + for event in events.read() { + let uuid = event.account.uuid_or_offline(); + let entity = if let Some(entity) = entity_uuid_index.get(&uuid) { + debug!("Reusing entity {entity:?} for client"); + entity + } else { + let entity = commands.spawn_empty().id(); + debug!("Created new entity {entity:?} for client"); + // add to the uuid index + entity_uuid_index.insert(uuid, entity); + entity + }; + + let mut entity_mut = commands.entity(entity); + entity_mut.insert(( + // add the Account to the entity now so plugins can access it earlier + event.account.to_owned(), + // localentity is always present for our clients, even if we're not actually logged + // in + LocalEntity, + // we don't insert InLoginState until we actually create the connection. note that + // there's no InHandshakeState component since we switch off of the handshake state + // immediately when the connection is created + )); + + if let Some(event_sender) = &event.event_sender { + // this is optional so we don't leak memory in case the user doesn't want to + // handle receiving packets + entity_mut.insert(LocalPlayerEvents(event_sender.clone())); + } + if let Some(start_join_callback) = &event.start_join_callback_tx { + entity_mut.insert(start_join_callback.clone()); + } + + let task_pool = IoTaskPool::get(); + let resolved_addr = event.resolved_address; + let address = event.address.clone(); + let proxy = event.proxy.clone(); + let task = task_pool.spawn(async_compat::Compat::new( + create_conn_and_send_intention_packet(resolved_addr, address, proxy), + )); + + entity_mut.insert(CreateConnectionTask(task)); + } +} + +async fn create_conn_and_send_intention_packet( + resolved_addr: SocketAddr, + address: ServerAddress, + proxy: Option, +) -> Result { + let mut conn = if let Some(proxy) = proxy { + Connection::new_with_proxy(&resolved_addr, proxy).await? + } else { + Connection::new(&resolved_addr).await? + }; + + conn.write(ServerboundIntention { + protocol_version: PROTOCOL_VERSION, + hostname: address.host.clone(), + port: address.port, + intention: ClientIntention::Login, + }) + .await?; + + let conn = conn.login(); + + Ok(conn) +} + +type LoginConn = Connection; + +#[derive(Component)] +pub struct CreateConnectionTask(pub Task>); + +pub fn poll_create_connection_task( + mut commands: Commands, + mut query: Query<( + Entity, + &mut CreateConnectionTask, + &Account, + Option<&StartJoinCallback>, + )>, +) { + for (entity, mut task, account, mut start_join_callback) in query.iter_mut() { + if let Some(poll_res) = future::block_on(future::poll_once(&mut task.0)) { + let mut entity_mut = commands.entity(entity); + entity_mut.remove::(); + let conn = match poll_res { + Ok(conn) => conn, + Err(err) => { + warn!("failed to create connection: {err}"); + if let Some(cb) = start_join_callback.take() { + let _ = cb.0.send(Err(err.into())); + } + return; + } + }; + + let (read_conn, write_conn) = conn.into_split(); + let (read_conn, write_conn) = (read_conn.raw, write_conn.raw); + + let instance = Instance::default(); + let instance_holder = crate::local_player::InstanceHolder::new( + entity, + // default to an empty world, it'll be set correctly later when we + // get the login packet + Arc::new(RwLock::new(instance)), + ); + + entity_mut.insert(( + // these stay when we switch to the game state + LocalPlayerBundle { + raw_connection: RawConnection::new( + read_conn, + write_conn, + ConnectionProtocol::Login, + ), + client_information: crate::ClientInformation::default(), + instance_holder, + metadata: azalea_entity::metadata::PlayerMetadataBundle::default(), + }, + InLoginState, + )); + + commands.trigger(SendLoginPacketEvent::new( + entity, + ServerboundHello { + name: account.username.clone(), + profile_id: account.uuid_or_offline(), + }, + )); + + if let Some(cb) = start_join_callback.take() { + let _ = cb.0.send(Ok(entity)); + } + } + } +} diff --git a/azalea-client/src/plugins/mod.rs b/azalea-client/src/plugins/mod.rs index 16b34205..431d59b2 100644 --- a/azalea-client/src/plugins/mod.rs +++ b/azalea-client/src/plugins/mod.rs @@ -9,6 +9,7 @@ pub mod disconnect; pub mod events; pub mod interact; pub mod inventory; +pub mod join; pub mod login; pub mod mining; pub mod movement; @@ -49,7 +50,8 @@ impl PluginGroup for DefaultPlugins { .add(tick_broadcast::TickBroadcastPlugin) .add(pong::PongPlugin) .add(connection::ConnectionPlugin) - .add(login::LoginPlugin); + .add(login::LoginPlugin) + .add(join::JoinPlugin); #[cfg(feature = "log")] { group = group.add(bevy_log::LogPlugin::default()); diff --git a/azalea-protocol/src/connect.rs b/azalea-protocol/src/connect.rs index 77968eed..3d375dfc 100644 --- a/azalea-protocol/src/connect.rs +++ b/azalea-protocol/src/connect.rs @@ -262,6 +262,7 @@ pub enum ConnectionError { use socks5_impl::protocol::UserKey; +/// An address and authentication method for connecting to a Socks5 proxy. #[derive(Debug, Clone)] pub struct Proxy { pub addr: SocketAddr,