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

add failing test_set_health_before_login test

This commit is contained in:
mat 2025-02-01 20:35:57 +00:00
parent 4aa5010ea2
commit 87c34e1c33
12 changed files with 272 additions and 59 deletions

View file

@ -326,7 +326,7 @@ impl Client {
client_information: crate::ClientInformation::default(), client_information: crate::ClientInformation::default(),
instance_holder, instance_holder,
}, },
InConfigurationState, InConfigState,
)); ));
Ok((client, rx)) Ok((client, rx))
@ -757,8 +757,8 @@ pub struct JoinedClientBundle {
/// A marker component for local players that are currently in the /// A marker component for local players that are currently in the
/// `configuration` state. /// `configuration` state.
#[derive(Component)] #[derive(Component, Clone, Debug)]
pub struct InConfigurationState; pub struct InConfigState;
pub struct AzaleaPlugin; pub struct AzaleaPlugin;
impl Plugin for AzaleaPlugin { impl Plugin for AzaleaPlugin {

View file

@ -10,7 +10,7 @@ use azalea_protocol::{
use bevy_app::prelude::*; use bevy_app::prelude::*;
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use crate::{client::InConfigurationState, packet_handling::configuration::SendConfigurationEvent}; use crate::{client::InConfigState, packet_handling::configuration::SendConfigurationEvent};
pub struct ConfigurationPlugin; pub struct ConfigurationPlugin;
impl Plugin for ConfigurationPlugin { impl Plugin for ConfigurationPlugin {
@ -18,13 +18,13 @@ impl Plugin for ConfigurationPlugin {
app.add_systems( app.add_systems(
Update, Update,
handle_in_configuration_state 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( fn handle_in_configuration_state(
query: Query<(Entity, &ClientInformation), Added<InConfigurationState>>, query: Query<(Entity, &ClientInformation), Added<InConfigState>>,
mut send_packet_events: EventWriter<SendConfigurationEvent>, mut send_packet_events: EventWriter<SendConfigurationEvent>,
) { ) {
for (entity, client_information) in query.iter() { for (entity, client_information) in query.iter() {

View file

@ -13,6 +13,7 @@ use bevy_ecs::{
system::{Commands, Query}, system::{Commands, Query},
}; };
use derive_more::Deref; use derive_more::Deref;
use tracing::trace;
use crate::{client::JoinedClientBundle, events::LocalPlayerEvents, raw_connection::RawConnection}; use crate::{client::JoinedClientBundle, events::LocalPlayerEvents, raw_connection::RawConnection};
@ -45,6 +46,7 @@ pub fn remove_components_from_disconnected_players(
mut events: EventReader<DisconnectEvent>, mut events: EventReader<DisconnectEvent>,
) { ) {
for DisconnectEvent { entity, .. } in events.read() { for DisconnectEvent { entity, .. } in events.read() {
trace!("Got DisconnectEvent for {entity:?}");
commands commands
.entity(*entity) .entity(*entity)
.remove::<JoinedClientBundle>() .remove::<JoinedClientBundle>()

View file

@ -33,8 +33,8 @@ pub mod task_pool;
pub use account::{Account, AccountOpts}; pub use account::{Account, AccountOpts};
pub use azalea_protocol::common::client_information::ClientInformation; pub use azalea_protocol::common::client_information::ClientInformation;
pub use client::{ pub use client::{
start_ecs_runner, Client, DefaultPlugins, JoinError, JoinedClientBundle, StartClientOpts, start_ecs_runner, Client, DefaultPlugins, InConfigState, JoinError, JoinedClientBundle,
TickBroadcast, LocalPlayerBundle, StartClientOpts, TickBroadcast,
}; };
pub use events::Event; pub use events::Event;
pub use local_player::{GameProfileComponent, Hunger, InstanceHolder, TabList}; pub use local_player::{GameProfileComponent, Hunger, InstanceHolder, TabList};

View file

@ -20,14 +20,23 @@ use crate::{
/// A component that keeps strong references to our [`PartialInstance`] and /// A component that keeps strong references to our [`PartialInstance`] and
/// [`Instance`] for local players. /// [`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)] #[derive(Component, Clone)]
pub struct InstanceHolder { pub struct InstanceHolder {
/// The partial instance is the world this client currently has loaded. It /// The partial instance is the world this client currently has loaded. It
/// has a limited render distance. /// has a limited render distance.
pub partial_instance: Arc<RwLock<PartialInstance>>, pub partial_instance: Arc<RwLock<PartialInstance>>,
/// The world is the combined [`PartialInstance`]s of all clients in the /// 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 /// same world.
/// swarm) ///
/// This is only relevant if you're using a shared world (i.e. a
/// swarm).
pub instance: Arc<RwLock<Instance>>, pub instance: Arc<RwLock<Instance>>,
} }
@ -114,12 +123,16 @@ impl Default for Hunger {
} }
impl InstanceHolder { impl InstanceHolder {
/// Create a new `InstanceHolder`. /// Create a new `InstanceHolder` for the given entity.
pub fn new(entity: Entity, world: Arc<RwLock<Instance>>) -> Self { ///
/// 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(); let client_information = ClientInformation::default();
InstanceHolder { InstanceHolder {
instance: world, instance,
partial_instance: Arc::new(RwLock::new(PartialInstance::new( partial_instance: Arc::new(RwLock::new(PartialInstance::new(
azalea_world::chunk_storage::calculate_chunk_storage_range( azalea_world::chunk_storage::calculate_chunk_storage_range(
client_information.view_distance.into(), client_information.view_distance.into(),

View file

@ -14,7 +14,7 @@ use bevy_ecs::prelude::*;
use bevy_ecs::system::SystemState; use bevy_ecs::system::SystemState;
use tracing::{debug, error, warn}; use tracing::{debug, error, warn};
use crate::client::InConfigurationState; use crate::client::InConfigState;
use crate::disconnect::DisconnectEvent; use crate::disconnect::DisconnectEvent;
use crate::local_player::Hunger; use crate::local_player::Hunger;
use crate::packet_handling::game::KeepAliveEvent; use crate::packet_handling::game::KeepAliveEvent;
@ -30,7 +30,7 @@ pub struct ConfigurationEvent {
} }
pub fn send_packet_events( pub fn send_packet_events(
query: Query<(Entity, &RawConnection), With<InConfigurationState>>, query: Query<(Entity, &RawConnection), With<InConfigState>>,
mut packet_events: ResMut<Events<ConfigurationEvent>>, mut packet_events: ResMut<Events<ConfigurationEvent>>,
) { ) {
// we manually clear and send the events at the beginning of each update // 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(); let mut raw_conn = query.get_mut(player_entity).unwrap();
raw_conn raw_conn
.write_packet(ServerboundFinishConfiguration {}) .write_packet(ServerboundFinishConfiguration)
.expect( .expect(
"we should be in the right state and encoding this packet shouldn't fail", "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 // these components are added now that we're going to be in the Game state
ecs.entity_mut(player_entity) ecs.entity_mut(player_entity)
.remove::<InConfigurationState>() .remove::<InConfigState>()
.insert(crate::JoinedClientBundle { .insert(crate::JoinedClientBundle {
physics_state: crate::PhysicsState::default(), physics_state: crate::PhysicsState::default(),
inventory: crate::inventory::Inventory::default(), inventory: crate::inventory::Inventory::default(),
@ -251,7 +251,7 @@ impl SendConfigurationEvent {
pub fn handle_send_packet_event( pub fn handle_send_packet_event(
mut send_packet_events: EventReader<SendConfigurationEvent>, 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() { for event in send_packet_events.read() {
if let Ok((raw_conn, in_configuration_state)) = query.get_mut(event.sent_by) { if let Ok((raw_conn, in_configuration_state)) = query.get_mut(event.sent_by) {

View file

@ -1470,7 +1470,7 @@ pub fn process_packet_events(ecs: &mut World) {
commands commands
.entity(player_entity) .entity(player_entity)
.insert(crate::client::InConfigurationState) .insert(crate::client::InConfigState)
.remove::<crate::JoinedClientBundle>(); .remove::<crate::JoinedClientBundle>();
system_state.apply(ecs); system_state.apply(ecs);

View file

@ -18,26 +18,26 @@ use tracing::error;
/// yourself. It will do the compression and encryption for you though. /// yourself. It will do the compression and encryption for you though.
#[derive(Component)] #[derive(Component)]
pub struct RawConnection { pub struct RawConnection {
reader: RawConnectionReader, pub reader: RawConnectionReader,
writer: RawConnectionWriter, pub writer: RawConnectionWriter,
/// Packets sent to this will be sent to the server. /// Packets sent to this will be sent to the server.
/// A task that reads packets from the server. The client is disconnected /// A task that reads packets from the server. The client is disconnected
/// when this task ends. /// 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. /// 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)] #[derive(Clone)]
struct RawConnectionReader { pub struct RawConnectionReader {
pub incoming_packet_queue: Arc<Mutex<Vec<Box<[u8]>>>>, pub incoming_packet_queue: Arc<Mutex<Vec<Box<[u8]>>>>,
pub run_schedule_sender: mpsc::UnboundedSender<()>, pub run_schedule_sender: mpsc::UnboundedSender<()>,
} }
#[derive(Clone)] #[derive(Clone)]
struct RawConnectionWriter { pub struct RawConnectionWriter {
pub outgoing_packets_sender: mpsc::UnboundedSender<Box<[u8]>>, pub outgoing_packets_sender: mpsc::UnboundedSender<Box<[u8]>>,
} }

View file

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

View file

@ -489,7 +489,7 @@ pub struct PlayerBundle {
/// be updated by other clients. /// be updated by other clients.
/// ///
/// If this is for a client then all of our clients will have this. /// If this is for a client then all of our clients will have this.
#[derive(Component, Clone)] #[derive(Component, Clone, Debug)]
pub struct LocalEntity; pub struct LocalEntity;
#[derive(Component, Clone, Debug, PartialEq, Deref, DerefMut)] #[derive(Component, Clone, Debug, PartialEq, Deref, DerefMut)]

View file

@ -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(),
}
}
}

View file

@ -7,6 +7,7 @@ use azalea_core::{position::Vec3, resource_location::ResourceLocation, tick::Gam
use azalea_entity::{ use azalea_entity::{
attributes::AttributeInstance, Attributes, EntityDimensions, LookDirection, Physics, Position, attributes::AttributeInstance, Attributes, EntityDimensions, LookDirection, Physics, Position,
}; };
use azalea_registry::EntityKind;
use azalea_world::{ChunkStorage, Instance, InstanceContainer, MinecraftEntityId, PartialInstance}; use azalea_world::{ChunkStorage, Instance, InstanceContainer, MinecraftEntityId, PartialInstance};
use bevy_app::App; use bevy_app::App;
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
@ -25,16 +26,13 @@ pub struct SimulatedPlayerBundle {
impl SimulatedPlayerBundle { impl SimulatedPlayerBundle {
pub fn new(position: Vec3) -> Self { pub fn new(position: Vec3) -> Self {
let dimensions = EntityDimensions { let dimensions = EntityDimensions::from(EntityKind::Player);
width: 0.6,
height: 1.8,
};
SimulatedPlayerBundle { SimulatedPlayerBundle {
position: Position::new(position), position: Position::new(position),
physics: Physics::new(dimensions, position), physics: Physics::new(dimensions, position),
physics_state: PhysicsState::default(), physics_state: PhysicsState::default(),
look_direction: LookDirection::new(0.0, 0.0), look_direction: LookDirection::default(),
attributes: Attributes { attributes: Attributes {
speed: AttributeInstance::new(0.1), speed: AttributeInstance::new(0.1),
attack_speed: AttributeInstance::new(4.0), attack_speed: AttributeInstance::new(4.0),