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::Mutex;
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) -> (Arc<Mutex<World>>, impl FnOnce(), oneshot::Receiver<AppExit>) {
122 if app.plugins_state() != PluginsState::Cleaned {
125 if app.plugins_state() == PluginsState::Adding {
127 info!("Waiting for plugins to load ...");
128 while app.plugins_state() == PluginsState::Adding {
129 thread::yield_now();
130 }
131 }
132 app.finish();
134 app.cleanup();
135 }
136
137 let ecs = Arc::new(Mutex::new(mem::take(app.world_mut())));
140
141 let ecs_clone = ecs.clone();
142 let outer_schedule_label = *app.update_schedule.as_ref().unwrap();
143
144 let (appexit_tx, appexit_rx) = oneshot::channel();
145 let start_running_systems = move || {
146 tokio::task::spawn_local(async move {
147 let appexit = run_schedule_loop(ecs_clone, outer_schedule_label).await;
148 appexit_tx.send(appexit)
149 });
150 };
151
152 (ecs, start_running_systems, appexit_rx)
153}
154
155async fn run_schedule_loop(
160 ecs: Arc<Mutex<World>>,
161 outer_schedule_label: InternedScheduleLabel,
162) -> AppExit {
163 let mut last_update: Option<Instant> = None;
164 let mut last_tick: Option<Instant> = None;
165
166 const UPDATE_DURATION_TARGET: Duration = Duration::from_micros(1_000_000 / 60);
170 const GAME_TICK_DURATION_TARGET: Duration = Duration::from_micros(1_000_000 / 20);
172
173 loop {
174 let now = Instant::now();
176 if let Some(last_update) = last_update {
177 let elapsed = now.duration_since(last_update);
178 if elapsed < UPDATE_DURATION_TARGET {
179 time::sleep(UPDATE_DURATION_TARGET - elapsed).await;
180 }
181 }
182 last_update = Some(now);
183
184 let mut ecs = ecs.lock();
185
186 ecs.run_schedule(outer_schedule_label);
188 if last_tick
189 .map(|last_tick| last_tick.elapsed() > GAME_TICK_DURATION_TARGET)
190 .unwrap_or(true)
191 {
192 if let Some(last_tick) = &mut last_tick {
193 *last_tick += GAME_TICK_DURATION_TARGET;
194
195 if (now - *last_tick) > GAME_TICK_DURATION_TARGET * 10 {
198 warn!(
199 "GameTick is more than 10 ticks behind, skipping ticks so we don't have to burst too much"
200 );
201 *last_tick = now;
202 }
203 } else {
204 last_tick = Some(now);
205 }
206 ecs.run_schedule(GameTick);
207 }
208
209 ecs.clear_trackers();
210 if let Some(exit) = should_exit(&mut ecs) {
211 ecs.clear_all();
213 return exit;
217 }
218 }
219}
220
221fn should_exit(ecs: &mut World) -> Option<AppExit> {
225 let mut reader = MessageCursor::default();
226
227 let events = ecs.get_resource::<Messages<AppExit>>()?;
228 let mut events = reader.read(events);
229
230 if events.len() != 0 {
231 return Some(
232 events
233 .find(|exit| exit.is_error())
234 .cloned()
235 .unwrap_or(AppExit::Success),
236 );
237 }
238
239 None
240}
241
242pub struct AmbiguityLoggerPlugin;
243impl Plugin for AmbiguityLoggerPlugin {
244 fn build(&self, app: &mut App) {
245 app.edit_schedule(Update, |schedule| {
246 schedule.set_build_settings(ScheduleBuildSettings {
247 ambiguity_detection: LogLevel::Warn,
248 ..Default::default()
249 });
250 });
251 app.edit_schedule(GameTick, |schedule| {
252 schedule.set_build_settings(ScheduleBuildSettings {
253 ambiguity_detection: LogLevel::Warn,
254 ..Default::default()
255 });
256 });
257 }
258}