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:
parent
ae3722d72c
commit
8045b4eda2
4 changed files with 226 additions and 102 deletions
|
@ -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>()
|
||||||
|
|
198
azalea-client/src/plugins/join.rs
Normal file
198
azalea-client/src/plugins/join.rs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Add table
Reference in a new issue