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
36pub struct Simulation {
38 pub app: App,
39 pub entity: Entity,
40
41 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 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 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 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 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 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}