use std::{collections::VecDeque, fmt::Debug, sync::Arc}; use azalea_auth::game_profile::GameProfile; use azalea_block::BlockState; use azalea_buf::AzaleaWrite; use azalea_core::{ delta::PositionDelta8, game_type::{GameMode, OptionalGameType}, position::{BlockPos, ChunkPos, Vec3}, resource_location::ResourceLocation, tick::GameTick, }; use azalea_entity::metadata::PlayerMetadataBundle; use azalea_protocol::{ common::client_information::ClientInformation, packets::{ ConnectionProtocol, Packet, ProtocolPacket, common::CommonPlayerSpawnInfo, config::{ClientboundFinishConfiguration, ClientboundRegistryData}, game::{ ClientboundAddEntity, ClientboundLevelChunkWithLight, ClientboundLogin, ClientboundRespawn, ServerboundGamePacket, c_level_chunk_with_light::ClientboundLevelChunkPacketData, c_light_update::ClientboundLightUpdatePacketData, }, }, }; use azalea_registry::{Biome, DimensionType, EntityKind}; use azalea_world::{Chunk, Instance, MinecraftEntityId, Section, palette::PalettedContainer}; use bevy_app::App; use bevy_ecs::{component::Mutable, prelude::*, schedule::ExecutorKind}; use parking_lot::{Mutex, RwLock}; use simdnbt::owned::{NbtCompound, NbtTag}; use uuid::Uuid; use crate::{ InConfigState, LocalPlayerBundle, connection::RawConnection, disconnect::DisconnectEvent, local_player::InstanceHolder, packet::game::SendPacketEvent, player::GameProfileComponent, }; /// A way to simulate a client in a server, used for some internal tests. 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, } 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, rt) = create_local_player_bundle(entity.id(), ConnectionProtocol::Configuration); entity.insert((player, ClientInformation::default())); let entity = entity.id(); tick_app(&mut app); // start in the config state app.world_mut().entity_mut(entity).insert(( InConfigState, GameProfileComponent(GameProfile::new( Uuid::from_u128(1234), "azalea".to_string(), )), )); tick_app(&mut app); let mut simulation = Self { app, entity, rt }; #[allow(clippy::single_match)] match initial_connection_protocol { ConnectionProtocol::Configuration => {} ConnectionProtocol::Game => { simulation.receive_packet(ClientboundRegistryData { registry_id: ResourceLocation::new("minecraft:dimension_type"), entries: vec![( ResourceLocation::new("minecraft:overworld"), Some(NbtCompound::from_values(vec![ ("height".into(), NbtTag::Int(384)), ("min_y".into(), NbtTag::Int(-64)), ])), )] .into_iter() .collect(), }); simulation.receive_packet(ClientboundFinishConfiguration); simulation.tick(); } _ => unimplemented!("unsupported ConnectionProtocol {initial_connection_protocol:?}"), } simulation } pub fn receive_packet(&mut self, packet: impl Packet

) { let buf = azalea_protocol::write::serialize_packet(&packet.into_variant()).unwrap(); self.with_component_mut::(|raw_conn| { raw_conn.injected_clientbound_packets.push(buf); }); } pub fn send_event(&mut self, event: impl bevy_ecs::event::Event) { self.app.world_mut().send_event(event); } pub fn tick(&mut self) { tick_app(&mut self.app); } pub fn update(&mut self) { self.app.update(); } pub fn minecraft_entity_id(&self) -> MinecraftEntityId { self.component::() } 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 with_component_mut>( &mut self, f: impl FnOnce(&mut T), ) { f(&mut self .app .world_mut() .entity_mut(self.entity) .get_mut::() .unwrap()); } pub fn resource(&self) -> T { self.app.world().get_resource::().unwrap().clone() } pub fn with_resource(&self, f: impl FnOnce(&T)) { f(self.app.world().get_resource::().unwrap()); } pub fn with_resource_mut(&mut self, f: impl FnOnce(Mut)) { f(self.app.world_mut().get_resource_mut::().unwrap()); } pub fn chunk(&self, chunk_pos: ChunkPos) -> Option>> { self.component::() .instance .read() .chunks .get(&chunk_pos) } pub fn get_block_state(&self, pos: BlockPos) -> Option { self.component::() .instance .read() .get_block_state(pos) } pub fn disconnect(&mut self) { // send DisconnectEvent self.app.world_mut().send_event(DisconnectEvent { entity: self.entity, reason: None, }); } } #[derive(Clone)] pub struct SentPackets { pub list: Arc>>, } impl SentPackets { pub fn new(simulation: &mut Simulation) -> Self { let sent_packets = SentPackets { list: Default::default(), }; let simulation_entity = simulation.entity; let sent_packets_clone = sent_packets.clone(); simulation .app .add_observer(move |trigger: Trigger| { if trigger.sent_by == simulation_entity { sent_packets_clone .list .lock() .push_back(trigger.event().packet.clone()) } }); sent_packets } pub fn clear(&self) { self.list.lock().clear(); } pub fn expect_tick_end(&self) { self.expect("TickEnd", |p| { matches!(p, ServerboundGamePacket::ClientTickEnd(_)) }); } pub fn expect_empty(&self) { let sent_packet = self.next(); if sent_packet.is_some() { panic!("Expected no packet, got {sent_packet:?}"); } } pub fn expect( &self, expected_formatted: &str, check: impl FnOnce(&ServerboundGamePacket) -> bool, ) { let sent_packet = self.next(); if let Some(sent_packet) = sent_packet { if !check(&sent_packet) { panic!("Expected {expected_formatted}, got {sent_packet:?}"); } } else { panic!("Expected {expected_formatted}, got nothing"); } } pub fn next(&self) -> Option { self.list.lock().pop_front() } } #[allow(clippy::type_complexity)] fn create_local_player_bundle( entity: Entity, connection_protocol: ConnectionProtocol, ) -> (LocalPlayerBundle, tokio::runtime::Runtime) { // unused since we'll trigger ticks ourselves let rt = tokio::runtime::Runtime::new().unwrap(); let raw_connection = RawConnection::new_networkless(connection_protocol); let instance = Instance::default(); let instance_holder = InstanceHolder::new(entity, Arc::new(RwLock::new(instance))); let local_player_bundle = LocalPlayerBundle { raw_connection, instance_holder, metadata: PlayerMetadataBundle::default(), }; (local_player_bundle, rt) } fn create_simulation_app() -> App { let mut app = App::new(); #[cfg(feature = "log")] app.add_plugins( bevy_app::PluginGroup::build(crate::DefaultPlugins).disable::(), ); app.edit_schedule(bevy_app::Main, |schedule| { // makes test results more reproducible schedule.set_executor_kind(ExecutorKind::SingleThreaded); }); app } fn tick_app(app: &mut App) { app.update(); app.world_mut().run_schedule(GameTick); } pub fn make_basic_login_packet( dimension_type: DimensionType, dimension: ResourceLocation, ) -> ClientboundLogin { ClientboundLogin { player_id: MinecraftEntityId(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, dimension, 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, } } pub fn make_basic_respawn_packet( dimension_type: DimensionType, dimension: ResourceLocation, ) -> ClientboundRespawn { ClientboundRespawn { common: CommonPlayerSpawnInfo { dimension_type, dimension, 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, }, data_to_keep: 0, } } pub fn make_basic_empty_chunk( pos: ChunkPos, section_count: usize, ) -> ClientboundLevelChunkWithLight { let mut chunk_bytes = Vec::new(); let mut sections = Vec::new(); for _ in 0..section_count { sections.push(Section { block_count: 0, states: PalettedContainer::::new(), biomes: PalettedContainer::::new(), }); } sections.azalea_write(&mut chunk_bytes).unwrap(); ClientboundLevelChunkWithLight { x: pos.x, z: pos.z, chunk_data: ClientboundLevelChunkPacketData { heightmaps: Default::default(), data: Arc::new(chunk_bytes.into()), block_entities: vec![], }, light_data: ClientboundLightUpdatePacketData::default(), } } pub fn make_basic_add_entity( entity_type: EntityKind, id: i32, position: impl Into, ) -> ClientboundAddEntity { ClientboundAddEntity { id: id.into(), uuid: Uuid::from_u128(1234), entity_type, position: position.into(), x_rot: 0, y_rot: 0, y_head_rot: 0, data: 0, velocity: PositionDelta8::default(), } }