azalea_client/
client.rs

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/// A bundle of components that's inserted right when we switch to the `login`
39/// state and stay present on our clients until we disconnect.
40///
41/// For the components that are only present in the `game` state, see
42/// [`JoinedClientBundle`].
43#[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/// A bundle for the components that are present on a local player that is
52/// currently in the `game` protocol state.
53///
54/// All of these components are also removed when the client disconnects.
55///
56/// If you want to filter for this, use [`InGameState`].
57#[derive(Bundle, Default)]
58pub struct JoinedClientBundle {
59    // note that InstanceHolder isn't here because it's set slightly before we fully join the world
60    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/// A marker component for local players that are currently in the
81/// `game` state.
82#[derive(Clone, Component, Debug, Default)]
83pub struct InGameState;
84/// A marker component for local players that are currently in the
85/// `configuration` state.
86#[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                // add GameProfileComponent when we get an AddPlayerEvent
96                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/// Create the ECS world, and return a function that begins running systems.
107/// This exists to allow you to make last-millisecond updates to the world
108/// before any systems start running.
109///
110/// You can create your app with `App::new()`, but don't forget to add
111/// [`DefaultPlugins`].
112///
113/// # Panics
114///
115/// This function panics if it's called outside of a Tokio `LocalSet` (or
116/// `LocalRuntime`). This exists so Azalea doesn't unexpectedly run game ticks
117/// in the middle of blocking user code.
118#[doc(hidden)]
119pub fn start_ecs_runner(
120    app: &mut SubApp,
121) -> (Arc<Mutex<World>>, impl FnOnce(), oneshot::Receiver<AppExit>) {
122    // this block is based on Bevy's default runner:
123    // https://github.com/bevyengine/bevy/blob/390877cdae7a17095a75c8f9f1b4241fe5047e83/crates/bevy_app/src/schedule_runner.rs#L77-L85
124    if app.plugins_state() != PluginsState::Cleaned {
125        // Wait for plugins to load
126        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        // Finish adding plugins and cleanup
133        app.finish();
134        app.cleanup();
135    }
136
137    // all resources should have been added by now so we can take the ecs from the
138    // app
139    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
155/// Runs the `Update` schedule 60 times per second and the `GameTick` schedule
156/// 20 times per second.
157///
158/// Exits when we receive an `AppExit` event.
159async 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    // azalea runs the Update schedule at most 60 times per second to simulate
167    // framerate. unlike vanilla though, we also only handle packets during Updates
168    // due to everything running in ecs systems.
169    const UPDATE_DURATION_TARGET: Duration = Duration::from_micros(1_000_000 / 60);
170    // minecraft runs at 20 tps
171    const GAME_TICK_DURATION_TARGET: Duration = Duration::from_micros(1_000_000 / 20);
172
173    loop {
174        // sleep until the next update if necessary
175        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        // if last tick is None or more than 50ms ago, run the GameTick schedule
187        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 we're more than 10 ticks behind, set last_tick to now.
196                // vanilla doesn't do it in exactly the same way but it shouldn't really matter
197                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            // it's possible for references to the World to stay around, so we clear the ecs
212            ecs.clear_all();
213            // ^ note that this also forcefully disconnects all of our bots without sending
214            // a disconnect packet (which is fine because we want to disconnect immediately)
215
216            return exit;
217        }
218    }
219}
220
221/// Checks whether the [`AppExit`] event was sent, and if so returns it.
222///
223/// This is based on Bevy's `should_exit` function: https://github.com/bevyengine/bevy/blob/b9fd7680e78c4073dfc90fcfdc0867534d92abe0/crates/bevy_app/src/app.rs#L1292
224fn 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}