aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormat <git@matdoes.dev>2025-02-01 20:35:57 +0000
committermat <git@matdoes.dev>2025-02-01 20:35:57 +0000
commit87c34e1c3355e2b950f3c6f4ae44dcdfb775bbca (patch)
treef725697c27f1959a97d48d5ed6e4a25266dcdd6b
parent4aa5010ea2dec8633be29b0a06c9b2233d7a6522 (diff)
downloadazalea-drasl-87c34e1c3355e2b950f3c6f4ae44dcdfb775bbca.tar.xz
add failing test_set_health_before_login test
-rw-r--r--azalea-client/src/client.rs6
-rw-r--r--azalea-client/src/configuration.rs6
-rw-r--r--azalea-client/src/disconnect.rs2
-rw-r--r--azalea-client/src/lib.rs4
-rw-r--r--azalea-client/src/local_player.rs23
-rw-r--r--azalea-client/src/packet_handling/configuration.rs10
-rw-r--r--azalea-client/src/packet_handling/game.rs2
-rw-r--r--azalea-client/src/raw_connection.rs14
-rw-r--r--azalea-client/tests/simulation.rs227
-rw-r--r--azalea-entity/src/lib.rs2
-rwxr-xr-xazalea-protocol/src/packets/game/c_add_player.rs27
-rw-r--r--azalea/src/pathfinder/simulation.rs8
12 files changed, 272 insertions, 59 deletions
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<InConfigurationState>>,
+ query: Query<(Entity, &ClientInformation), Added<InConfigState>>,
mut send_packet_events: EventWriter<SendConfigurationEvent>,
) {
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<DisconnectEvent>,
) {
for DisconnectEvent { entity, .. } in events.read() {
+ trace!("Got DisconnectEvent for {entity:?}");
commands
.entity(*entity)
.remove::<JoinedClientBundle>()
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<RwLock<PartialInstance>>,
/// 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<RwLock<Instance>>,
}
@@ -114,12 +123,16 @@ impl Default for Hunger {
}
impl InstanceHolder {
- /// Create a new `InstanceHolder`.
- pub fn new(entity: Entity, world: Arc<RwLock<Instance>>) -> 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<RwLock<Instance>>) -> 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<InConfigurationState>>,
+ query: Query<(Entity, &RawConnection), With<InConfigState>>,
mut packet_events: ResMut<Events<ConfigurationEvent>>,
) {
// 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::<InConfigurationState>()
+ .remove::<InConfigState>()
.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<SendConfigurationEvent>,
- 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::<crate::JoinedClientBundle>();
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<Mutex<Vec<Box<[u8]>>>>,
pub run_schedule_sender: mpsc::UnboundedSender<()>,
}
#[derive(Clone)]
-struct RawConnectionWriter {
+pub struct RawConnectionWriter {
pub outgoing_packets_sender: mpsc::UnboundedSender<Box<[u8]>>,
}
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::<InConfigState>());
+
+ simulation.receive_packet(ClientboundFinishConfiguration);
+ simulation.tick();
+
+ assert!(!simulation.has_component::<InConfigState>());
+ assert!(simulation.has_component::<LocalEntity>());
+
+ simulation.receive_packet(ClientboundSetHealth {
+ health: 15.,
+ food: 20,
+ saturation: 20.,
+ });
+ simulation.tick();
+ assert_eq!(*simulation.component::<Health>(), 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::<Health>(), 15.);
+}
+
+pub fn create_local_player_bundle(
+ entity: Entity,
+ connection_protocol: ConnectionProtocol,
+) -> (
+ LocalPlayerBundle,
+ mpsc::UnboundedReceiver<Box<[u8]>>,
+ Arc<Mutex<Vec<Box<[u8]>>>>,
+ 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::<LogPlugin>());
+ 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<Mutex<Vec<Box<[u8]>>>>,
+ pub outgoing_packets_receiver: mpsc::UnboundedReceiver<Box<[u8]>>,
+}
+
+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<P: ProtocolPacket + Debug>(&mut self, packet: impl Packet<P>) {
+ 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<T: Component + Clone>(&self) -> T {
+ self.app.world().get::<T>(self.entity).unwrap().clone()
+ }
+ pub fn get_component<T: Component + Clone>(&self) -> Option<T> {
+ self.app.world().get::<T>(self.entity).cloned()
+ }
+ pub fn has_component<T: Component>(&self) -> bool {
+ self.app.world().get::<T>(self.entity).is_some()
+ }
+ pub fn position(&self) -> Vec3 {
+ *self.component::<Position>()
+ }
+}
+
+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),