azalea_client/
test_simulation.rs

1use std::{fmt::Debug, sync::Arc, time::Duration};
2
3use azalea_auth::game_profile::GameProfile;
4use azalea_buf::AzaleaWrite;
5use azalea_core::delta::PositionDelta8;
6use azalea_core::game_type::{GameMode, OptionalGameType};
7use azalea_core::position::{ChunkPos, Vec3};
8use azalea_core::resource_location::ResourceLocation;
9use azalea_core::tick::GameTick;
10use azalea_entity::metadata::PlayerMetadataBundle;
11use azalea_protocol::packets::common::CommonPlayerSpawnInfo;
12use azalea_protocol::packets::config::{ClientboundFinishConfiguration, ClientboundRegistryData};
13use azalea_protocol::packets::game::c_level_chunk_with_light::ClientboundLevelChunkPacketData;
14use azalea_protocol::packets::game::c_light_update::ClientboundLightUpdatePacketData;
15use azalea_protocol::packets::game::{
16    ClientboundAddEntity, ClientboundLevelChunkWithLight, ClientboundLogin, ClientboundRespawn,
17};
18use azalea_protocol::packets::{ConnectionProtocol, Packet, ProtocolPacket};
19use azalea_registry::{DimensionType, EntityKind};
20use azalea_world::palette::{PalettedContainer, PalettedContainerKind};
21use azalea_world::{Chunk, Instance, MinecraftEntityId, Section};
22use bevy_app::App;
23use bevy_ecs::{prelude::*, schedule::ExecutorKind};
24use parking_lot::{Mutex, RwLock};
25use simdnbt::owned::{NbtCompound, NbtTag};
26use tokio::task::JoinHandle;
27use tokio::{sync::mpsc, time::sleep};
28use uuid::Uuid;
29
30use crate::disconnect::DisconnectEvent;
31use crate::{
32    ClientInformation, GameProfileComponent, InConfigState, InstanceHolder, LocalPlayerBundle,
33    raw_connection::{RawConnection, RawConnectionReader, RawConnectionWriter},
34};
35
36/// A way to simulate a client in a server, used for some internal tests.
37pub struct Simulation {
38    pub app: App,
39    pub entity: Entity,
40
41    // the runtime needs to be kept around for the tasks to be considered alive
42    pub rt: tokio::runtime::Runtime,
43
44    pub incoming_packet_queue: Arc<Mutex<Vec<Box<[u8]>>>>,
45    pub clear_outgoing_packets_receiver_task: JoinHandle<!>,
46}
47
48impl Simulation {
49    pub fn new(initial_connection_protocol: ConnectionProtocol) -> Self {
50        let mut app = create_simulation_app();
51        let mut entity = app.world_mut().spawn_empty();
52        let (player, clear_outgoing_packets_receiver_task, incoming_packet_queue, rt) =
53            create_local_player_bundle(entity.id(), ConnectionProtocol::Configuration);
54        entity.insert(player);
55
56        let entity = entity.id();
57
58        tick_app(&mut app);
59
60        // start in the config state
61        app.world_mut().entity_mut(entity).insert(InConfigState);
62        tick_app(&mut app);
63
64        let mut simulation = Self {
65            app,
66            entity,
67            rt,
68            incoming_packet_queue,
69            clear_outgoing_packets_receiver_task,
70        };
71
72        #[allow(clippy::single_match)]
73        match initial_connection_protocol {
74            ConnectionProtocol::Configuration => {}
75            ConnectionProtocol::Game => {
76                simulation.receive_packet(ClientboundRegistryData {
77                    registry_id: ResourceLocation::new("minecraft:dimension_type"),
78                    entries: vec![(
79                        ResourceLocation::new("minecraft:overworld"),
80                        Some(NbtCompound::from_values(vec![
81                            ("height".into(), NbtTag::Int(384)),
82                            ("min_y".into(), NbtTag::Int(-64)),
83                        ])),
84                    )]
85                    .into_iter()
86                    .collect(),
87                });
88
89                simulation.receive_packet(ClientboundFinishConfiguration);
90                simulation.tick();
91            }
92            _ => unimplemented!("unsupported ConnectionProtocol {initial_connection_protocol:?}"),
93        }
94
95        simulation
96    }
97
98    pub fn receive_packet<P: ProtocolPacket + Debug>(&mut self, packet: impl Packet<P>) {
99        let buf = azalea_protocol::write::serialize_packet(&packet.into_variant()).unwrap();
100        self.incoming_packet_queue.lock().push(buf);
101    }
102
103    pub fn tick(&mut self) {
104        tick_app(&mut self.app);
105    }
106    pub fn component<T: Component + Clone>(&self) -> T {
107        self.app.world().get::<T>(self.entity).unwrap().clone()
108    }
109    pub fn get_component<T: Component + Clone>(&self) -> Option<T> {
110        self.app.world().get::<T>(self.entity).cloned()
111    }
112    pub fn has_component<T: Component>(&self) -> bool {
113        self.app.world().get::<T>(self.entity).is_some()
114    }
115    pub fn resource<T: Resource + Clone>(&self) -> T {
116        self.app.world().get_resource::<T>().unwrap().clone()
117    }
118    pub fn with_resource<T: Resource>(&self, f: impl FnOnce(&T)) {
119        f(self.app.world().get_resource::<T>().unwrap());
120    }
121    pub fn with_resource_mut<T: Resource>(&mut self, f: impl FnOnce(Mut<T>)) {
122        f(self.app.world_mut().get_resource_mut::<T>().unwrap());
123    }
124
125    pub fn chunk(&self, chunk_pos: ChunkPos) -> Option<Arc<RwLock<Chunk>>> {
126        self.component::<InstanceHolder>()
127            .instance
128            .read()
129            .chunks
130            .get(&chunk_pos)
131    }
132
133    pub fn disconnect(&mut self) {
134        // send DisconnectEvent
135        self.app.world_mut().send_event(DisconnectEvent {
136            entity: self.entity,
137            reason: None,
138        });
139    }
140}
141
142#[allow(clippy::type_complexity)]
143fn create_local_player_bundle(
144    entity: Entity,
145    connection_protocol: ConnectionProtocol,
146) -> (
147    LocalPlayerBundle,
148    JoinHandle<!>,
149    Arc<Mutex<Vec<Box<[u8]>>>>,
150    tokio::runtime::Runtime,
151) {
152    // unused since we'll trigger ticks ourselves
153    let (run_schedule_sender, _run_schedule_receiver) = mpsc::channel(1);
154
155    let (outgoing_packets_sender, mut outgoing_packets_receiver) = mpsc::unbounded_channel();
156    let incoming_packet_queue = Arc::new(Mutex::new(Vec::new()));
157    let reader = RawConnectionReader {
158        incoming_packet_queue: incoming_packet_queue.clone(),
159        run_schedule_sender,
160    };
161    let writer = RawConnectionWriter {
162        outgoing_packets_sender,
163    };
164
165    let rt = tokio::runtime::Runtime::new().unwrap();
166
167    // the tasks can't die since that would make us send a DisconnectEvent
168    let read_packets_task = rt.spawn(async {
169        loop {
170            sleep(Duration::from_secs(60)).await;
171        }
172    });
173    let write_packets_task = rt.spawn(async {
174        loop {
175            sleep(Duration::from_secs(60)).await;
176        }
177    });
178
179    let clear_outgoing_packets_receiver_task = rt.spawn(async move {
180        loop {
181            let _ = outgoing_packets_receiver.recv().await;
182        }
183    });
184
185    let raw_connection = RawConnection {
186        reader,
187        writer,
188        read_packets_task,
189        write_packets_task,
190        connection_protocol,
191    };
192
193    let instance = Instance::default();
194    let instance_holder = InstanceHolder::new(entity, Arc::new(RwLock::new(instance)));
195
196    let local_player_bundle = LocalPlayerBundle {
197        raw_connection,
198        game_profile: GameProfileComponent(GameProfile::new(Uuid::nil(), "azalea".to_owned())),
199        client_information: ClientInformation::default(),
200        instance_holder,
201        metadata: PlayerMetadataBundle::default(),
202    };
203
204    (
205        local_player_bundle,
206        clear_outgoing_packets_receiver_task,
207        incoming_packet_queue,
208        rt,
209    )
210}
211
212fn create_simulation_app() -> App {
213    let mut app = App::new();
214
215    #[cfg(feature = "log")]
216    app.add_plugins(
217        bevy_app::PluginGroup::build(crate::DefaultPlugins).disable::<bevy_log::LogPlugin>(),
218    );
219
220    app.edit_schedule(bevy_app::Main, |schedule| {
221        // makes test results more reproducible
222        schedule.set_executor_kind(ExecutorKind::SingleThreaded);
223    });
224    app
225}
226
227fn tick_app(app: &mut App) {
228    app.update();
229    app.world_mut().run_schedule(GameTick);
230}
231
232pub fn make_basic_login_packet(
233    dimension_type: DimensionType,
234    dimension: ResourceLocation,
235) -> ClientboundLogin {
236    ClientboundLogin {
237        player_id: MinecraftEntityId(0),
238        hardcore: false,
239        levels: vec![],
240        max_players: 20,
241        chunk_radius: 8,
242        simulation_distance: 8,
243        reduced_debug_info: false,
244        show_death_screen: true,
245        do_limited_crafting: false,
246        common: CommonPlayerSpawnInfo {
247            dimension_type,
248            dimension,
249            seed: 0,
250            game_type: GameMode::Survival,
251            previous_game_type: OptionalGameType(None),
252            is_debug: false,
253            is_flat: false,
254            last_death_location: None,
255            portal_cooldown: 0,
256            sea_level: 63,
257        },
258        enforces_secure_chat: false,
259    }
260}
261
262pub fn make_basic_respawn_packet(
263    dimension_type: DimensionType,
264    dimension: ResourceLocation,
265) -> ClientboundRespawn {
266    ClientboundRespawn {
267        common: CommonPlayerSpawnInfo {
268            dimension_type,
269            dimension,
270            seed: 0,
271            game_type: GameMode::Survival,
272            previous_game_type: OptionalGameType(None),
273            is_debug: false,
274            is_flat: false,
275            last_death_location: None,
276            portal_cooldown: 0,
277            sea_level: 63,
278        },
279        data_to_keep: 0,
280    }
281}
282
283pub fn make_basic_empty_chunk(
284    pos: ChunkPos,
285    section_count: usize,
286) -> ClientboundLevelChunkWithLight {
287    let mut chunk_bytes = Vec::new();
288    let mut sections = Vec::new();
289    for _ in 0..section_count {
290        sections.push(Section {
291            block_count: 0,
292            states: PalettedContainer::new(PalettedContainerKind::BlockStates),
293            biomes: PalettedContainer::new(PalettedContainerKind::Biomes),
294        });
295    }
296    sections.azalea_write(&mut chunk_bytes).unwrap();
297
298    ClientboundLevelChunkWithLight {
299        x: pos.x,
300        z: pos.z,
301        chunk_data: ClientboundLevelChunkPacketData {
302            heightmaps: Default::default(),
303            data: Arc::new(chunk_bytes.into()),
304            block_entities: vec![],
305        },
306        light_data: ClientboundLightUpdatePacketData::default(),
307    }
308}
309
310pub fn make_basic_add_entity(
311    entity_type: EntityKind,
312    id: i32,
313    position: impl Into<Vec3>,
314) -> ClientboundAddEntity {
315    ClientboundAddEntity {
316        id: id.into(),
317        uuid: Uuid::from_u128(1234),
318        entity_type,
319        position: position.into(),
320        x_rot: 0,
321        y_rot: 0,
322        y_head_rot: 0,
323        data: 0,
324        velocity: PositionDelta8::default(),
325    }
326}