aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--azalea-client/src/client.rs125
-rw-r--r--azalea-client/src/plugins/join.rs198
-rw-r--r--azalea-client/src/plugins/mod.rs4
-rw-r--r--azalea-protocol/src/connect.rs1
4 files changed, 226 insertions, 102 deletions
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<Self, JoinError> {
- // 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::<EntityUuidIndex>();
- 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::<EntityUuidIndex>();
- entity_uuid_index.insert(uuid, entity);
- entity
- };
-
- 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 (start_join_callback_tx, mut start_join_callback_rx) =
+ mpsc::unbounded_channel::<Result<Entity, JoinError>>();
- 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::<Commands>(&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::<InstanceContainer>()
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::<StartJoinServerEvent>().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<Proxy>,
+ pub event_sender: Option<mpsc::UnboundedSender<crate::Event>>,
+
+ pub start_join_callback_tx: Option<StartJoinCallback>,
+}
+
+// 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<Result<Entity, JoinError>>);
+
+pub fn handle_start_join_server_event(
+ mut commands: Commands,
+ mut events: EventReader<StartJoinServerEvent>,
+ mut entity_uuid_index: ResMut<EntityUuidIndex>,
+) {
+ 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<Proxy>,
+) -> Result<LoginConn, ConnectionError> {
+ 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<ClientboundLoginPacket, ServerboundLoginPacket>;
+
+#[derive(Component)]
+pub struct CreateConnectionTask(pub Task<Result<LoginConn, ConnectionError>>);
+
+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::<CreateConnectionTask>();
+ 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,