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::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/// 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) -> (
122    Arc<RwLock<World>>,
123    impl FnOnce(),
124    oneshot::Receiver<AppExit>,
125) {
126    // this block is based on Bevy's default runner:
127    // https://github.com/bevyengine/bevy/blob/390877cdae7a17095a75c8f9f1b4241fe5047e83/crates/bevy_app/src/schedule_runner.rs#L77-L85
128    if app.plugins_state() != PluginsState::Cleaned {
129        // Wait for plugins to load
130        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        // Finish adding plugins and cleanup
137        app.finish();
138        app.cleanup();
139    }
140
141    // all resources should have been added by now so we can take the ecs from the
142    // app
143    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
159/// Runs the `Update` schedule 60 times per second and the `GameTick` schedule
160/// 20 times per second.
161///
162/// Exits when we receive an `AppExit` event.
163async 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    // azalea runs the Update schedule at most 60 times per second to simulate
171    // framerate. unlike vanilla though, we also only handle packets during Updates
172    // due to everything running in ecs systems.
173    const UPDATE_DURATION_TARGET: Duration = Duration::from_micros(1_000_000 / 60);
174    // minecraft runs at 20 tps
175    const GAME_TICK_DURATION_TARGET: Duration = Duration::from_micros(1_000_000 / 20);
176
177    loop {
178        // sleep until the next update if necessary
179        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        // if last tick is None or more than 50ms ago, run the GameTick schedule
191        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 we're more than 10 ticks behind, set last_tick to now.
200                // vanilla doesn't do it in exactly the same way but it shouldn't really matter
201                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            // it's possible for references to the World to stay around, so we clear the ecs
216            ecs.clear_all();
217            // ^ note that this also forcefully disconnects all of our bots without sending
218            // a disconnect packet (which is fine because we want to disconnect immediately)
219
220            return exit;
221        }
222    }
223}
224
225/// Checks whether the [`AppExit`] event was sent, and if so returns it.
226///
227/// This is based on Bevy's `should_exit` function: https://github.com/bevyengine/bevy/blob/b9fd7680e78c4073dfc90fcfdc0867534d92abe0/crates/bevy_app/src/app.rs#L1292
228fn 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}