1
2
Fork 0
mirror of https://github.com/mat-1/azalea.git synced 2025-08-02 14:26:04 +00:00

add StartJoinServerEvent to allow joining servers exclusively from ecs

This commit is contained in:
mat 2025-04-19 23:51:19 -04:30
parent ae3722d72c
commit 8045b4eda2
4 changed files with 226 additions and 102 deletions

View file

@ -15,19 +15,17 @@ use azalea_core::{
tick::GameTick, tick::GameTick,
}; };
use azalea_entity::{ use azalea_entity::{
EntityUpdateSet, EyeHeight, LocalEntity, Position, EntityUpdateSet, EyeHeight, Position,
indexing::{EntityIdIndex, EntityUuidIndex}, indexing::{EntityIdIndex, EntityUuidIndex},
metadata::Health, metadata::Health,
}; };
use azalea_protocol::{ use azalea_protocol::{
ServerAddress, ServerAddress,
common::client_information::ClientInformation, common::client_information::ClientInformation,
connect::{Connection, ConnectionError, Proxy}, connect::{ConnectionError, Proxy},
packets::{ packets::{
self, ClientIntention, ConnectionProtocol, PROTOCOL_VERSION, Packet, self, Packet,
game::{self, ServerboundGamePacket}, game::{self, ServerboundGamePacket},
handshake::s_intention::ServerboundIntention,
login::s_hello::ServerboundHello,
}, },
resolver, resolver,
}; };
@ -38,7 +36,7 @@ use bevy_ecs::{
component::Component, component::Component,
entity::Entity, entity::Entity,
schedule::{InternedScheduleLabel, IntoSystemConfigs, LogLevel, ScheduleBuildSettings}, schedule::{InternedScheduleLabel, IntoSystemConfigs, LogLevel, ScheduleBuildSettings},
system::{Commands, Resource}, system::Resource,
world::World, world::World,
}; };
use parking_lot::{Mutex, RwLock}; use parking_lot::{Mutex, RwLock};
@ -57,19 +55,16 @@ use crate::{
chunks::ChunkBatchInfo, chunks::ChunkBatchInfo,
connection::RawConnection, connection::RawConnection,
disconnect::DisconnectEvent, disconnect::DisconnectEvent,
events::{Event, LocalPlayerEvents}, events::Event,
interact::CurrentSequenceNumber, interact::CurrentSequenceNumber,
inventory::Inventory, inventory::Inventory,
join::{StartJoinCallback, StartJoinServerEvent},
local_player::{ local_player::{
GameProfileComponent, Hunger, InstanceHolder, PermissionLevel, PlayerAbilities, TabList, GameProfileComponent, Hunger, InstanceHolder, PermissionLevel, PlayerAbilities, TabList,
}, },
mining::{self}, mining::{self},
movement::{LastSentLookDirection, PhysicsState}, movement::{LastSentLookDirection, PhysicsState},
packet::{ packet::game::SendPacketEvent,
as_system,
game::SendPacketEvent,
login::{InLoginState, SendLoginPacketEvent},
},
player::retroactively_add_game_profile_component, player::retroactively_add_game_profile_component,
}; };
@ -233,100 +228,26 @@ impl Client {
event_sender, event_sender,
}: StartClientOpts<'_>, }: StartClientOpts<'_>,
) -> Result<Self, JoinError> { ) -> Result<Self, JoinError> {
// check if an entity with our uuid already exists in the ecs and if so then // send a StartJoinServerEvent
// just use that
let entity = {
let mut ecs = ecs_lock.lock();
let entity_uuid_index = ecs.resource::<EntityUuidIndex>(); let (start_join_callback_tx, mut start_join_callback_rx) =
let uuid = account.uuid_or_offline(); mpsc::unbounded_channel::<Result<Entity, JoinError>>();
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 (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 mut ecs = ecs_lock.lock();
ecs.send_event(StartJoinServerEvent {
let instance = Instance::default(); account: account.clone(),
let instance_holder = crate::local_player::InstanceHolder::new( address: address.clone(),
entity, resolved_address: *resolved_address,
// default to an empty world, it'll be set correctly later when we proxy,
// get the login packet event_sender: event_sender.clone(),
Arc::new(RwLock::new(instance)), start_join_callback_tx: Some(StartJoinCallback(start_join_callback_tx)),
); });
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(),
},
));
} }
as_system::<Commands>(&mut ecs_lock.lock(), |mut commands| { let entity = start_join_callback_rx.recv().await.expect(
commands.entity(entity).insert((InLoginState,)); "StartJoinCallback should not be dropped before sending a message, this is a bug in Azalea",
commands.trigger(SendLoginPacketEvent::new( )?;
entity,
ServerboundHello {
name: account.username.clone(),
profile_id: account.uuid_or_offline(),
},
))
});
let client = Client::new(entity, ecs_lock.clone()); let client = Client::new(entity, ecs_lock.clone());
Ok(client) Ok(client)
@ -732,7 +653,9 @@ impl Plugin for AzaleaPlugin {
Update, Update,
( (
// add GameProfileComponent when we get an AddPlayerEvent // 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>() .init_resource::<InstanceContainer>()

View file

@ -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));
}
}
}
}

View file

@ -9,6 +9,7 @@ pub mod disconnect;
pub mod events; pub mod events;
pub mod interact; pub mod interact;
pub mod inventory; pub mod inventory;
pub mod join;
pub mod login; pub mod login;
pub mod mining; pub mod mining;
pub mod movement; pub mod movement;
@ -49,7 +50,8 @@ impl PluginGroup for DefaultPlugins {
.add(tick_broadcast::TickBroadcastPlugin) .add(tick_broadcast::TickBroadcastPlugin)
.add(pong::PongPlugin) .add(pong::PongPlugin)
.add(connection::ConnectionPlugin) .add(connection::ConnectionPlugin)
.add(login::LoginPlugin); .add(login::LoginPlugin)
.add(join::JoinPlugin);
#[cfg(feature = "log")] #[cfg(feature = "log")]
{ {
group = group.add(bevy_log::LogPlugin::default()); group = group.add(bevy_log::LogPlugin::default());

View file

@ -262,6 +262,7 @@ pub enum ConnectionError {
use socks5_impl::protocol::UserKey; use socks5_impl::protocol::UserKey;
/// An address and authentication method for connecting to a Socks5 proxy.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Proxy { pub struct Proxy {
pub addr: SocketAddr, pub addr: SocketAddr,