diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index cfef34d1..7c1aca78 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -326,7 +326,7 @@ impl Client { client_information: crate::ClientInformation::default(), instance_holder, }, - InConfigurationState, + InConfigState, )); Ok((client, rx)) @@ -757,8 +757,8 @@ pub struct JoinedClientBundle { /// A marker component for local players that are currently in the /// `configuration` state. -#[derive(Component)] -pub struct InConfigurationState; +#[derive(Component, Clone, Debug)] +pub struct InConfigState; pub struct AzaleaPlugin; impl Plugin for AzaleaPlugin { diff --git a/azalea-client/src/configuration.rs b/azalea-client/src/configuration.rs index bfaa36f0..bf07710b 100644 --- a/azalea-client/src/configuration.rs +++ b/azalea-client/src/configuration.rs @@ -10,7 +10,7 @@ use azalea_protocol::{ use bevy_app::prelude::*; use bevy_ecs::prelude::*; -use crate::{client::InConfigurationState, packet_handling::configuration::SendConfigurationEvent}; +use crate::{client::InConfigState, packet_handling::configuration::SendConfigurationEvent}; pub struct ConfigurationPlugin; impl Plugin for ConfigurationPlugin { @@ -18,13 +18,13 @@ impl Plugin for ConfigurationPlugin { app.add_systems( Update, handle_in_configuration_state - .after(crate::packet_handling::configuration::handle_send_packet_event), + .before(crate::packet_handling::configuration::handle_send_packet_event), ); } } fn handle_in_configuration_state( - query: Query<(Entity, &ClientInformation), Added>, + query: Query<(Entity, &ClientInformation), Added>, mut send_packet_events: EventWriter, ) { for (entity, client_information) in query.iter() { diff --git a/azalea-client/src/disconnect.rs b/azalea-client/src/disconnect.rs index 37bb37dd..5b377ce2 100644 --- a/azalea-client/src/disconnect.rs +++ b/azalea-client/src/disconnect.rs @@ -13,6 +13,7 @@ use bevy_ecs::{ system::{Commands, Query}, }; use derive_more::Deref; +use tracing::trace; use crate::{client::JoinedClientBundle, events::LocalPlayerEvents, raw_connection::RawConnection}; @@ -45,6 +46,7 @@ pub fn remove_components_from_disconnected_players( mut events: EventReader, ) { for DisconnectEvent { entity, .. } in events.read() { + trace!("Got DisconnectEvent for {entity:?}"); commands .entity(*entity) .remove::() diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs index 68354c87..652ae439 100644 --- a/azalea-client/src/lib.rs +++ b/azalea-client/src/lib.rs @@ -33,8 +33,8 @@ pub mod task_pool; pub use account::{Account, AccountOpts}; pub use azalea_protocol::common::client_information::ClientInformation; pub use client::{ - start_ecs_runner, Client, DefaultPlugins, JoinError, JoinedClientBundle, StartClientOpts, - TickBroadcast, + start_ecs_runner, Client, DefaultPlugins, InConfigState, JoinError, JoinedClientBundle, + LocalPlayerBundle, StartClientOpts, TickBroadcast, }; pub use events::Event; pub use local_player::{GameProfileComponent, Hunger, InstanceHolder, TabList}; diff --git a/azalea-client/src/local_player.rs b/azalea-client/src/local_player.rs index 7c9254a7..2d691826 100644 --- a/azalea-client/src/local_player.rs +++ b/azalea-client/src/local_player.rs @@ -20,14 +20,23 @@ use crate::{ /// A component that keeps strong references to our [`PartialInstance`] and /// [`Instance`] for local players. +/// +/// This can also act as a convenience for accessing the player's Instance since +/// the alternative is to look up the player's [`InstanceName`] in the +/// [`InstanceContainer`]. +/// +/// [`InstanceContainer`]: azalea_world::InstanceContainer +/// [`InstanceName`]: azalea_world::InstanceName #[derive(Component, Clone)] pub struct InstanceHolder { /// The partial instance is the world this client currently has loaded. It /// has a limited render distance. pub partial_instance: Arc>, /// The world is the combined [`PartialInstance`]s of all clients in the - /// same world. (Only relevant if you're using a shared world, i.e. a - /// swarm) + /// same world. + /// + /// This is only relevant if you're using a shared world (i.e. a + /// swarm). pub instance: Arc>, } @@ -114,12 +123,16 @@ impl Default for Hunger { } impl InstanceHolder { - /// Create a new `InstanceHolder`. - pub fn new(entity: Entity, world: Arc>) -> Self { + /// Create a new `InstanceHolder` for the given entity. + /// + /// The partial instance will be created for you. The render distance will + /// be set to a default value, which you can change by creating a new + /// partial_instance. + pub fn new(entity: Entity, instance: Arc>) -> Self { let client_information = ClientInformation::default(); InstanceHolder { - instance: world, + instance, partial_instance: Arc::new(RwLock::new(PartialInstance::new( azalea_world::chunk_storage::calculate_chunk_storage_range( client_information.view_distance.into(), diff --git a/azalea-client/src/packet_handling/configuration.rs b/azalea-client/src/packet_handling/configuration.rs index 8eccebf5..9124f6aa 100644 --- a/azalea-client/src/packet_handling/configuration.rs +++ b/azalea-client/src/packet_handling/configuration.rs @@ -14,7 +14,7 @@ use bevy_ecs::prelude::*; use bevy_ecs::system::SystemState; use tracing::{debug, error, warn}; -use crate::client::InConfigurationState; +use crate::client::InConfigState; use crate::disconnect::DisconnectEvent; use crate::local_player::Hunger; use crate::packet_handling::game::KeepAliveEvent; @@ -30,7 +30,7 @@ pub struct ConfigurationEvent { } pub fn send_packet_events( - query: Query<(Entity, &RawConnection), With>, + query: Query<(Entity, &RawConnection), With>, mut packet_events: ResMut>, ) { // we manually clear and send the events at the beginning of each update @@ -110,7 +110,7 @@ pub fn process_packet_events(ecs: &mut World) { let mut raw_conn = query.get_mut(player_entity).unwrap(); raw_conn - .write_packet(ServerboundFinishConfiguration {}) + .write_packet(ServerboundFinishConfiguration) .expect( "we should be in the right state and encoding this packet shouldn't fail", ); @@ -118,7 +118,7 @@ pub fn process_packet_events(ecs: &mut World) { // these components are added now that we're going to be in the Game state ecs.entity_mut(player_entity) - .remove::() + .remove::() .insert(crate::JoinedClientBundle { physics_state: crate::PhysicsState::default(), inventory: crate::inventory::Inventory::default(), @@ -251,7 +251,7 @@ impl SendConfigurationEvent { pub fn handle_send_packet_event( mut send_packet_events: EventReader, - mut query: Query<(&mut RawConnection, Option<&InConfigurationState>)>, + mut query: Query<(&mut RawConnection, Option<&InConfigState>)>, ) { for event in send_packet_events.read() { if let Ok((raw_conn, in_configuration_state)) = query.get_mut(event.sent_by) { diff --git a/azalea-client/src/packet_handling/game.rs b/azalea-client/src/packet_handling/game.rs index d715d6c6..a7326198 100644 --- a/azalea-client/src/packet_handling/game.rs +++ b/azalea-client/src/packet_handling/game.rs @@ -1470,7 +1470,7 @@ pub fn process_packet_events(ecs: &mut World) { commands .entity(player_entity) - .insert(crate::client::InConfigurationState) + .insert(crate::client::InConfigState) .remove::(); system_state.apply(ecs); diff --git a/azalea-client/src/raw_connection.rs b/azalea-client/src/raw_connection.rs index 2091c14e..50f41049 100644 --- a/azalea-client/src/raw_connection.rs +++ b/azalea-client/src/raw_connection.rs @@ -18,26 +18,26 @@ use tracing::error; /// yourself. It will do the compression and encryption for you though. #[derive(Component)] pub struct RawConnection { - reader: RawConnectionReader, - writer: RawConnectionWriter, + pub reader: RawConnectionReader, + pub writer: RawConnectionWriter, /// Packets sent to this will be sent to the server. /// A task that reads packets from the server. The client is disconnected /// when this task ends. - read_packets_task: tokio::task::JoinHandle<()>, + pub read_packets_task: tokio::task::JoinHandle<()>, /// A task that writes packets from the server. - write_packets_task: tokio::task::JoinHandle<()>, + pub write_packets_task: tokio::task::JoinHandle<()>, - connection_protocol: ConnectionProtocol, + pub connection_protocol: ConnectionProtocol, } #[derive(Clone)] -struct RawConnectionReader { +pub struct RawConnectionReader { pub incoming_packet_queue: Arc>>>, pub run_schedule_sender: mpsc::UnboundedSender<()>, } #[derive(Clone)] -struct RawConnectionWriter { +pub struct RawConnectionWriter { pub outgoing_packets_sender: mpsc::UnboundedSender>, } diff --git a/azalea-client/tests/simulation.rs b/azalea-client/tests/simulation.rs new file mode 100644 index 00000000..7b3c0e1e --- /dev/null +++ b/azalea-client/tests/simulation.rs @@ -0,0 +1,227 @@ +use std::{fmt::Debug, sync::Arc, time::Duration}; + +use azalea_auth::game_profile::GameProfile; +use azalea_client::{ + events::LocalPlayerEvents, + raw_connection::{RawConnection, RawConnectionReader, RawConnectionWriter}, + ClientInformation, GameProfileComponent, InConfigState, InstanceHolder, LocalPlayerBundle, +}; +use azalea_core::{ + game_type::{GameMode, OptionalGameType}, + position::Vec3, + resource_location::ResourceLocation, + tick::GameTick, +}; +use azalea_entity::{metadata::Health, LocalEntity, Position}; +use azalea_protocol::packets::{ + common::CommonPlayerSpawnInfo, + config::ClientboundFinishConfiguration, + game::{ClientboundLogin, ClientboundSetHealth}, + ConnectionProtocol, Packet, ProtocolPacket, +}; +use azalea_registry::DimensionType; +use azalea_world::Instance; +use bevy_app::App; +use bevy_app::PluginGroup; +use bevy_ecs::{prelude::*, schedule::ExecutorKind}; +use bevy_log::{tracing_subscriber, LogPlugin}; +use parking_lot::{Mutex, RwLock}; +use tokio::{sync::mpsc, time::sleep}; +use uuid::Uuid; + +#[test] +fn test_set_health_before_login() { + let _ = tracing_subscriber::fmt::try_init(); + + let mut simulation = Simulation::new(ConnectionProtocol::Configuration); + assert!(simulation.has_component::()); + + simulation.receive_packet(ClientboundFinishConfiguration); + simulation.tick(); + + assert!(!simulation.has_component::()); + assert!(simulation.has_component::()); + + simulation.receive_packet(ClientboundSetHealth { + health: 15., + food: 20, + saturation: 20., + }); + simulation.tick(); + assert_eq!(*simulation.component::(), 15.); + + simulation.receive_packet(ClientboundLogin { + player_id: 0, + hardcore: false, + levels: vec![], + max_players: 20, + chunk_radius: 8, + simulation_distance: 8, + reduced_debug_info: false, + show_death_screen: true, + do_limited_crafting: false, + common: CommonPlayerSpawnInfo { + dimension_type: DimensionType::Overworld, + dimension: ResourceLocation::new("overworld"), + seed: 0, + game_type: GameMode::Survival, + previous_game_type: OptionalGameType(None), + is_debug: false, + is_flat: false, + last_death_location: None, + portal_cooldown: 0, + sea_level: 63, + }, + enforces_secure_chat: false, + }); + simulation.tick(); + + // health should stay the same + assert_eq!(*simulation.component::(), 15.); +} + +pub fn create_local_player_bundle( + entity: Entity, + connection_protocol: ConnectionProtocol, +) -> ( + LocalPlayerBundle, + mpsc::UnboundedReceiver>, + Arc>>>, + tokio::runtime::Runtime, +) { + // unused since we'll trigger ticks ourselves + let (run_schedule_sender, _run_schedule_receiver) = tokio::sync::mpsc::unbounded_channel(); + + let (outgoing_packets_sender, outgoing_packets_receiver) = mpsc::unbounded_channel(); + let incoming_packet_queue = Arc::new(Mutex::new(Vec::new())); + let reader = RawConnectionReader { + incoming_packet_queue: incoming_packet_queue.clone(), + run_schedule_sender, + }; + let writer = RawConnectionWriter { + outgoing_packets_sender, + }; + + let rt = tokio::runtime::Runtime::new().unwrap(); + + // the tasks can't die since that would make us send a DisconnectEvent + let read_packets_task = rt.spawn(async { + loop { + sleep(Duration::from_secs(60)).await; + } + }); + let write_packets_task = rt.spawn(async { + loop { + sleep(Duration::from_secs(60)).await; + } + }); + + let raw_connection = RawConnection { + reader, + writer, + read_packets_task, + write_packets_task, + connection_protocol, + }; + + let (local_player_events_sender, local_player_events_receiver) = mpsc::unbounded_channel(); + + let instance = Instance::default(); + let instance_holder = InstanceHolder::new(entity, Arc::new(RwLock::new(instance))); + + let local_player_bundle = LocalPlayerBundle { + raw_connection, + local_player_events: LocalPlayerEvents(local_player_events_sender), + game_profile: GameProfileComponent(GameProfile::new(Uuid::nil(), "azalea".to_owned())), + client_information: ClientInformation::default(), + instance_holder, + }; + ( + local_player_bundle, + outgoing_packets_receiver, + incoming_packet_queue, + rt, + ) +} + +fn simulation_instance_name() -> ResourceLocation { + ResourceLocation::new("azalea:simulation") +} + +fn create_simulation_app() -> App { + let mut app = App::new(); + app.add_plugins(azalea_client::DefaultPlugins.build().disable::()); + app.edit_schedule(bevy_app::Main, |schedule| { + // makes test results more reproducible + schedule.set_executor_kind(ExecutorKind::SingleThreaded); + }); + app +} + +pub struct Simulation { + pub app: App, + pub entity: Entity, + + // the runtime needs to be kept around for the tasks to be considered alive + pub rt: tokio::runtime::Runtime, + + pub incoming_packet_queue: Arc>>>, + pub outgoing_packets_receiver: mpsc::UnboundedReceiver>, +} + +impl Simulation { + pub fn new(initial_connection_protocol: ConnectionProtocol) -> Self { + let mut app = create_simulation_app(); + let mut entity = app.world_mut().spawn_empty(); + let (player, outgoing_packets_receiver, incoming_packet_queue, rt) = + create_local_player_bundle(entity.id(), initial_connection_protocol); + entity.insert(player); + + let entity = entity.id(); + + tick_app(&mut app); + + match initial_connection_protocol { + ConnectionProtocol::Configuration => { + app.world_mut().entity_mut(entity).insert(InConfigState); + tick_app(&mut app); + } + _ => {} + } + + Self { + app, + entity, + rt, + incoming_packet_queue, + outgoing_packets_receiver, + } + } + + pub fn receive_packet(&mut self, packet: impl Packet

) { + let buf = azalea_protocol::write::serialize_packet(&packet.into_variant()).unwrap(); + self.incoming_packet_queue.lock().push(buf.into()); + println!("added to incoming_packet_queue"); + } + + pub fn tick(&mut self) { + tick_app(&mut self.app); + } + pub fn component(&self) -> T { + self.app.world().get::(self.entity).unwrap().clone() + } + pub fn get_component(&self) -> Option { + self.app.world().get::(self.entity).cloned() + } + pub fn has_component(&self) -> bool { + self.app.world().get::(self.entity).is_some() + } + pub fn position(&self) -> Vec3 { + *self.component::() + } +} + +fn tick_app(app: &mut App) { + app.update(); + app.world_mut().run_schedule(GameTick); +} diff --git a/azalea-entity/src/lib.rs b/azalea-entity/src/lib.rs index 4c0201ce..6019261c 100644 --- a/azalea-entity/src/lib.rs +++ b/azalea-entity/src/lib.rs @@ -489,7 +489,7 @@ pub struct PlayerBundle { /// be updated by other clients. /// /// If this is for a client then all of our clients will have this. -#[derive(Component, Clone)] +#[derive(Component, Clone, Debug)] pub struct LocalEntity; #[derive(Component, Clone, Debug, PartialEq, Deref, DerefMut)] diff --git a/azalea-protocol/src/packets/game/c_add_player.rs b/azalea-protocol/src/packets/game/c_add_player.rs deleted file mode 100755 index 7b36567d..00000000 --- a/azalea-protocol/src/packets/game/c_add_player.rs +++ /dev/null @@ -1,27 +0,0 @@ -use azalea_buf::AzBuf; -use azalea_core::{ResourceLocation, Vec3}; -use azalea_entity::{metadata::PlayerMetadataBundle, EntityBundle, PlayerBundle}; -use azalea_protocol_macros::ClientboundGamePacket; -use azalea_registry::EntityKind; -use uuid::Uuid; - -/// This packet is sent by the server when a player comes into visible range, -/// not when a player joins. -#[derive(Clone, Debug, AzBuf, ClientboundGamePacket)] -pub struct ClientboundAddPlayer { - #[var] - pub id: u32, - pub uuid: Uuid, - pub position: Vec3, - pub x_rot: i8, - pub y_rot: i8, -} - -impl ClientboundAddPlayer { - pub fn as_player_bundle(&self, world_name: ResourceLocation) -> PlayerBundle { - PlayerBundle { - entity: EntityBundle::new(self.uuid, self.position, EntityKind::Player, world_name), - metadata: PlayerMetadataBundle::default(), - } - } -} diff --git a/azalea/src/pathfinder/simulation.rs b/azalea/src/pathfinder/simulation.rs index 1f29ad24..0067c19f 100644 --- a/azalea/src/pathfinder/simulation.rs +++ b/azalea/src/pathfinder/simulation.rs @@ -7,6 +7,7 @@ use azalea_core::{position::Vec3, resource_location::ResourceLocation, tick::Gam use azalea_entity::{ attributes::AttributeInstance, Attributes, EntityDimensions, LookDirection, Physics, Position, }; +use azalea_registry::EntityKind; use azalea_world::{ChunkStorage, Instance, InstanceContainer, MinecraftEntityId, PartialInstance}; use bevy_app::App; use bevy_ecs::prelude::*; @@ -25,16 +26,13 @@ pub struct SimulatedPlayerBundle { impl SimulatedPlayerBundle { pub fn new(position: Vec3) -> Self { - let dimensions = EntityDimensions { - width: 0.6, - height: 1.8, - }; + let dimensions = EntityDimensions::from(EntityKind::Player); SimulatedPlayerBundle { position: Position::new(position), physics: Physics::new(dimensions, position), physics_state: PhysicsState::default(), - look_direction: LookDirection::new(0.0, 0.0), + look_direction: LookDirection::default(), attributes: Attributes { speed: AttributeInstance::new(0.1), attack_speed: AttributeInstance::new(4.0),