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,
};
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 (start_join_callback_tx, mut start_join_callback_rx) =
mpsc::unbounded_channel::<Result<Entity, JoinError>>();
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::<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>()

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 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());

View file

@ -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,