1use std::{
2 fmt::Debug,
3 mem,
4 sync::Arc,
5 thread,
6 time::{Duration, Instant},
7};
8
9use azalea_core::tick::GameTick;
10use azalea_entity::{
11 EntityUpdateSystems, PlayerAbilities, indexing::EntityIdIndex, inventory::Inventory,
12};
13use azalea_physics::local_player::PhysicsState;
14use azalea_world::InstanceContainer;
15use bevy_app::{App, AppExit, Plugin, PluginsState, SubApp, Update};
16use bevy_ecs::{
17 message::MessageCursor,
18 prelude::*,
19 schedule::{InternedScheduleLabel, LogLevel, ScheduleBuildSettings},
20};
21use parking_lot::RwLock;
22use tokio::{sync::oneshot, time};
23use tracing::{info, warn};
24
25use crate::{
26 attack,
27 block_update::QueuedServerBlockUpdates,
28 chunks::ChunkBatchInfo,
29 connection::RawConnection,
30 cookies::ServerCookies,
31 interact::BlockStatePredictionHandler,
32 local_player::{Hunger, InstanceHolder, PermissionLevel, TabList},
33 mining,
34 movement::LastSentLookDirection,
35 player::retroactively_add_game_profile_component,
36};
37
38#[derive(Bundle)]
44pub struct LocalPlayerBundle {
45 pub raw_connection: RawConnection,
46 pub instance_holder: InstanceHolder,
47
48 pub metadata: azalea_entity::metadata::PlayerMetadataBundle,
49}
50
51#[derive(Bundle, Default)]
58pub struct JoinedClientBundle {
59 pub physics_state: PhysicsState,
61 pub inventory: Inventory,
62 pub tab_list: TabList,
63 pub block_state_prediction_handler: BlockStatePredictionHandler,
64 pub queued_server_block_updates: QueuedServerBlockUpdates,
65 pub last_sent_direction: LastSentLookDirection,
66 pub abilities: PlayerAbilities,
67 pub permission_level: PermissionLevel,
68 pub chunk_batch_info: ChunkBatchInfo,
69 pub hunger: Hunger,
70 pub cookies: ServerCookies,
71
72 pub entity_id_index: EntityIdIndex,
73
74 pub mining: mining::MineBundle,
75 pub attack: attack::AttackBundle,
76
77 pub in_game_state: InGameState,
78}
79
80#[derive(Clone, Component, Debug, Default)]
83pub struct InGameState;
84#[derive(Clone, Component, Debug, Default)]
87pub struct InConfigState;
88
89pub struct AzaleaPlugin;
90impl Plugin for AzaleaPlugin {
91 fn build(&self, app: &mut App) {
92 app.add_systems(
93 Update,
94 (
95 retroactively_add_game_profile_component
97 .after(EntityUpdateSystems::Index)
98 .after(crate::join::handle_start_join_server_event),
99 ),
100 )
101 .init_resource::<InstanceContainer>()
102 .init_resource::<TabList>();
103 }
104}
105
106#[doc(hidden)]
119pub fn start_ecs_runner(
120 app: &mut SubApp,
121) -> (
122 Arc<RwLock<World>>,
123 impl FnOnce(),
124 oneshot::Receiver<AppExit>,
125) {
126 if app.plugins_state() != PluginsState::Cleaned {
129 if app.plugins_state() == PluginsState::Adding {
131 info!("Waiting for plugins to load ...");
132 while app.plugins_state() == PluginsState::Adding {
133 thread::yield_now();
134 }
135 }
136 app.finish();
138 app.cleanup();
139 }
140
141 let ecs = Arc::new(RwLock::new(mem::take(app.world_mut())));
144
145 let ecs_clone = ecs.clone();
146 let outer_schedule_label = *app.update_schedule.as_ref().unwrap();
147
148 let (appexit_tx, appexit_rx) = oneshot::channel();
149 let start_running_systems = move || {
150 tokio::task::spawn_local(async move {
151 let appexit = run_schedule_loop(ecs_clone, outer_schedule_label).await;
152 appexit_tx.send(appexit)
153 });
154 };
155
156 (ecs, start_running_systems, appexit_rx)
157}
158
159async fn run_schedule_loop(
164 ecs: Arc<RwLock<World>>,
165 outer_schedule_label: InternedScheduleLabel,
166) -> AppExit {
167 let mut last_update: Option<Instant> = None;
168 let mut last_tick: Option<Instant> = None;
169
170 const UPDATE_DURATION_TARGET: Duration = Duration::from_micros(1_000_000 / 60);
174 const GAME_TICK_DURATION_TARGET: Duration = Duration::from_micros(1_000_000 / 20);
176
177 loop {
178 let now = Instant::now();
180 if let Some(last_update) = last_update {
181 let elapsed = now.duration_since(last_update);
182 if elapsed < UPDATE_DURATION_TARGET {
183 time::sleep(UPDATE_DURATION_TARGET - elapsed).await;
184 }
185 }
186 last_update = Some(now);
187
188 let mut ecs = ecs.write();
189
190 ecs.run_schedule(outer_schedule_label);
192 if last_tick
193 .map(|last_tick| last_tick.elapsed() > GAME_TICK_DURATION_TARGET)
194 .unwrap_or(true)
195 {
196 if let Some(last_tick) = &mut last_tick {
197 *last_tick += GAME_TICK_DURATION_TARGET;
198
199 if (now - *last_tick) > GAME_TICK_DURATION_TARGET * 10 {
202 warn!(
203 "GameTick is more than 10 ticks behind, skipping ticks so we don't have to burst too much"
204 );
205 *last_tick = now;
206 }
207 } else {
208 last_tick = Some(now);
209 }
210 ecs.run_schedule(GameTick);
211 }
212
213 ecs.clear_trackers();
214 if let Some(exit) = should_exit(&mut ecs) {
215 ecs.clear_all();
217 return exit;
221 }
222 }
223}
224
225fn should_exit(ecs: &mut World) -> Option<AppExit> {
229 let mut reader = MessageCursor::default();
230
231 let events = ecs.get_resource::<Messages<AppExit>>()?;
232 let mut events = reader.read(events);
233
234 if events.len() != 0 {
235 return Some(
236 events
237 .find(|exit| exit.is_error())
238 .cloned()
239 .unwrap_or(AppExit::Success),
240 );
241 }
242
243 None
244}
245
246pub struct AmbiguityLoggerPlugin;
247impl Plugin for AmbiguityLoggerPlugin {
248 fn build(&self, app: &mut App) {
249 app.edit_schedule(Update, |schedule| {
250 schedule.set_build_settings(ScheduleBuildSettings {
251 ambiguity_detection: LogLevel::Warn,
252 ..Default::default()
253 });
254 });
255 app.edit_schedule(GameTick, |schedule| {
256 schedule.set_build_settings(ScheduleBuildSettings {
257 ambiguity_detection: LogLevel::Warn,
258 ..Default::default()
259 });
260 });
261 }
262}