azalea_client/
client.rs

1use std::{
2    collections::HashMap,
3    fmt::Debug,
4    io, mem,
5    net::SocketAddr,
6    sync::Arc,
7    thread,
8    time::{Duration, Instant},
9};
10
11use azalea_auth::game_profile::GameProfile;
12use azalea_chat::FormattedText;
13use azalea_core::{
14    data_registry::ResolvableDataRegistry, position::Vec3, resource_location::ResourceLocation,
15    tick::GameTick,
16};
17use azalea_entity::{
18    EntityUpdateSet, EyeHeight, Position,
19    indexing::{EntityIdIndex, EntityUuidIndex},
20    metadata::Health,
21};
22use azalea_protocol::{
23    ServerAddress,
24    common::client_information::ClientInformation,
25    connect::{ConnectionError, Proxy},
26    packets::{
27        self, Packet,
28        game::{self, ServerboundGamePacket},
29    },
30    resolver,
31};
32use azalea_world::{Instance, InstanceContainer, InstanceName, MinecraftEntityId, PartialInstance};
33use bevy_app::{App, Plugin, PluginsState, SubApp, Update};
34use bevy_ecs::{
35    prelude::*,
36    schedule::{InternedScheduleLabel, LogLevel, ScheduleBuildSettings},
37};
38use parking_lot::{Mutex, RwLock};
39use simdnbt::owned::NbtCompound;
40use thiserror::Error;
41use tokio::{
42    sync::mpsc::{self},
43    time,
44};
45use tracing::{debug, error, info, warn};
46use uuid::Uuid;
47
48use crate::{
49    Account, DefaultPlugins, PlayerInfo,
50    attack::{self},
51    chunks::ChunkBatchInfo,
52    connection::RawConnection,
53    disconnect::DisconnectEvent,
54    events::Event,
55    interact::CurrentSequenceNumber,
56    inventory::Inventory,
57    join::{ConnectOpts, StartJoinCallback, StartJoinServerEvent},
58    local_player::{
59        GameProfileComponent, Hunger, InstanceHolder, PermissionLevel, PlayerAbilities, TabList,
60    },
61    mining::{self},
62    movement::{LastSentLookDirection, PhysicsState},
63    packet::game::SendPacketEvent,
64    player::retroactively_add_game_profile_component,
65};
66
67/// `Client` has the things that a user interacting with the library will want.
68///
69/// To make a new client, use either [`azalea::ClientBuilder`] or
70/// [`Client::join`].
71///
72/// Note that `Client` is inaccessible from systems (i.e. plugins), but you can
73/// achieve everything that client can do with events.
74///
75/// [`azalea::ClientBuilder`]: https://docs.rs/azalea/latest/azalea/struct.ClientBuilder.html
76#[derive(Clone)]
77pub struct Client {
78    /// The entity for this client in the ECS.
79    pub entity: Entity,
80
81    /// The entity component system. You probably don't need to access this
82    /// directly. Note that if you're using a shared world (i.e. a swarm), this
83    /// will contain all entities in all worlds.
84    pub ecs: Arc<Mutex<World>>,
85}
86
87/// An error that happened while joining the server.
88#[derive(Error, Debug)]
89pub enum JoinError {
90    #[error("{0}")]
91    Resolver(#[from] resolver::ResolverError),
92    #[error("{0}")]
93    Connection(#[from] ConnectionError),
94    #[error("{0}")]
95    ReadPacket(#[from] Box<azalea_protocol::read::ReadPacketError>),
96    #[error("{0}")]
97    Io(#[from] io::Error),
98    #[error("Failed to encrypt the challenge from the server for {0:?}")]
99    EncryptionError(packets::login::ClientboundHello),
100    #[error("{0}")]
101    SessionServer(#[from] azalea_auth::sessionserver::ClientSessionServerError),
102    #[error("The given address could not be parsed into a ServerAddress")]
103    InvalidAddress,
104    #[error("Couldn't refresh access token: {0}")]
105    Auth(#[from] azalea_auth::AuthError),
106    #[error("Disconnected: {reason}")]
107    Disconnect { reason: FormattedText },
108}
109
110pub struct StartClientOpts {
111    pub ecs_lock: Arc<Mutex<World>>,
112    pub account: Account,
113    pub connect_opts: ConnectOpts,
114    pub event_sender: Option<mpsc::UnboundedSender<Event>>,
115}
116
117impl StartClientOpts {
118    pub fn new(
119        account: Account,
120        address: ServerAddress,
121        resolved_address: SocketAddr,
122        event_sender: Option<mpsc::UnboundedSender<Event>>,
123    ) -> StartClientOpts {
124        let mut app = App::new();
125        app.add_plugins(DefaultPlugins);
126
127        let (ecs_lock, start_running_systems) = start_ecs_runner(app.main_mut());
128        start_running_systems();
129
130        Self {
131            ecs_lock,
132            account,
133            connect_opts: ConnectOpts {
134                address,
135                resolved_address,
136                proxy: None,
137            },
138            event_sender,
139        }
140    }
141
142    pub fn proxy(mut self, proxy: Proxy) -> Self {
143        self.connect_opts.proxy = Some(proxy);
144        self
145    }
146}
147
148impl Client {
149    /// Create a new client from the given [`GameProfile`], ECS Entity, ECS
150    /// World, and schedule runner function.
151    /// You should only use this if you want to change these fields from the
152    /// defaults, otherwise use [`Client::join`].
153    pub fn new(entity: Entity, ecs: Arc<Mutex<World>>) -> Self {
154        Self {
155            // default our id to 0, it'll be set later
156            entity,
157
158            ecs,
159        }
160    }
161
162    /// Connect to a Minecraft server.
163    ///
164    /// To change the render distance and other settings, use
165    /// [`Client::set_client_information`]. To watch for events like packets
166    /// sent by the server, use the `rx` variable this function returns.
167    ///
168    /// # Examples
169    ///
170    /// ```rust,no_run
171    /// use azalea_client::{Client, Account};
172    ///
173    /// #[tokio::main]
174    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
175    ///     let account = Account::offline("bot");
176    ///     let (client, rx) = Client::join(account, "localhost").await?;
177    ///     client.chat("Hello, world!");
178    ///     client.disconnect();
179    ///     Ok(())
180    /// }
181    /// ```
182    pub async fn join(
183        account: Account,
184        address: impl TryInto<ServerAddress>,
185    ) -> Result<(Self, mpsc::UnboundedReceiver<Event>), JoinError> {
186        let address: ServerAddress = address.try_into().map_err(|_| JoinError::InvalidAddress)?;
187        let resolved_address = resolver::resolve_address(&address).await?;
188        let (tx, rx) = mpsc::unbounded_channel();
189
190        let client = Self::start_client(StartClientOpts::new(
191            account,
192            address,
193            resolved_address,
194            Some(tx),
195        ))
196        .await?;
197        Ok((client, rx))
198    }
199
200    pub async fn join_with_proxy(
201        account: Account,
202        address: impl TryInto<ServerAddress>,
203        proxy: Proxy,
204    ) -> Result<(Self, mpsc::UnboundedReceiver<Event>), JoinError> {
205        let address: ServerAddress = address.try_into().map_err(|_| JoinError::InvalidAddress)?;
206        let resolved_address = resolver::resolve_address(&address).await?;
207        let (tx, rx) = mpsc::unbounded_channel();
208
209        let client = Self::start_client(
210            StartClientOpts::new(account, address, resolved_address, Some(tx)).proxy(proxy),
211        )
212        .await?;
213        Ok((client, rx))
214    }
215
216    /// Create a [`Client`] when you already have the ECS made with
217    /// [`start_ecs_runner`]. You'd usually want to use [`Self::join`] instead.
218    pub async fn start_client(
219        StartClientOpts {
220            ecs_lock,
221            account,
222            connect_opts,
223            event_sender,
224        }: StartClientOpts,
225    ) -> Result<Self, JoinError> {
226        // send a StartJoinServerEvent
227
228        let (start_join_callback_tx, mut start_join_callback_rx) =
229            mpsc::unbounded_channel::<Result<Entity, JoinError>>();
230
231        ecs_lock.lock().send_event(StartJoinServerEvent {
232            account,
233            connect_opts,
234            event_sender,
235            start_join_callback_tx: Some(StartJoinCallback(start_join_callback_tx)),
236        });
237
238        let entity = start_join_callback_rx.recv().await.expect(
239            "StartJoinCallback should not be dropped before sending a message, this is a bug in Azalea",
240        )?;
241
242        let client = Client::new(entity, ecs_lock.clone());
243        Ok(client)
244    }
245
246    /// Write a packet directly to the server.
247    pub fn write_packet(&self, packet: impl Packet<ServerboundGamePacket>) {
248        let packet = packet.into_variant();
249        self.ecs
250            .lock()
251            .commands()
252            .trigger(SendPacketEvent::new(self.entity, packet));
253    }
254
255    /// Disconnect this client from the server by ending all tasks.
256    ///
257    /// The OwnedReadHalf for the TCP connection is in one of the tasks, so it
258    /// automatically closes the connection when that's dropped.
259    pub fn disconnect(&self) {
260        self.ecs.lock().send_event(DisconnectEvent {
261            entity: self.entity,
262            reason: None,
263        });
264    }
265
266    pub fn raw_connection<'a>(&'a self, ecs: &'a mut World) -> &'a RawConnection {
267        self.query::<&RawConnection>(ecs)
268    }
269    pub fn raw_connection_mut<'a>(
270        &'a self,
271        ecs: &'a mut World,
272    ) -> bevy_ecs::world::Mut<'a, RawConnection> {
273        self.query::<&mut RawConnection>(ecs)
274    }
275
276    /// Get a component from this client. This will clone the component and
277    /// return it.
278    ///
279    ///
280    /// If the component can't be cloned, try [`Self::map_component`] instead.
281    /// If it isn't guaranteed to be present, use [`Self::get_component`] or
282    /// [`Self::map_get_component`].
283    ///
284    /// You may also use [`Self::ecs`] and [`Self::query`] directly if you need
285    /// more control over when the ECS is locked.
286    ///
287    /// # Panics
288    ///
289    /// This will panic if the component doesn't exist on the client.
290    ///
291    /// # Examples
292    ///
293    /// ```
294    /// # use azalea_world::InstanceName;
295    /// # fn example(client: &azalea_client::Client) {
296    /// let world_name = client.component::<InstanceName>();
297    /// # }
298    pub fn component<T: Component + Clone>(&self) -> T {
299        self.query::<&T>(&mut self.ecs.lock()).clone()
300    }
301
302    /// Get a component from this client, or `None` if it doesn't exist.
303    ///
304    /// If the component can't be cloned, try [`Self::map_component`] instead.
305    /// You may also have to use [`Self::ecs`] and [`Self::query`] directly.
306    pub fn get_component<T: Component + Clone>(&self) -> Option<T> {
307        self.query::<Option<&T>>(&mut self.ecs.lock()).cloned()
308    }
309
310    /// Get a resource from the ECS. This will clone the resource and return it.
311    pub fn resource<T: Resource + Clone>(&self) -> T {
312        self.ecs.lock().resource::<T>().clone()
313    }
314
315    /// Get a required ECS resource and call the given function with it.
316    pub fn map_resource<T: Resource, R>(&self, f: impl FnOnce(&T) -> R) -> R {
317        let ecs = self.ecs.lock();
318        let value = ecs.resource::<T>();
319        f(value)
320    }
321
322    /// Get an optional ECS resource and call the given function with it.
323    pub fn map_get_resource<T: Resource, R>(&self, f: impl FnOnce(Option<&T>) -> R) -> R {
324        let ecs = self.ecs.lock();
325        let value = ecs.get_resource::<T>();
326        f(value)
327    }
328
329    /// Get a required component for this client and call the given function.
330    ///
331    /// Similar to [`Self::component`], but doesn't clone the component since
332    /// it's passed as a reference. [`Self::ecs`] will remain locked while the
333    /// callback is being run.
334    ///
335    /// If the component is not guaranteed to be present, use
336    /// [`Self::get_component`] instead.
337    ///
338    /// # Panics
339    ///
340    /// This will panic if the component doesn't exist on the client.
341    ///
342    /// ```
343    /// # use azalea_client::{Client, Hunger};
344    /// # fn example(bot: &Client) {
345    /// let hunger = bot.map_component::<Hunger, _>(|h| h.food);
346    /// # }
347    /// ```
348    pub fn map_component<T: Component, R>(&self, f: impl FnOnce(&T) -> R) -> R {
349        let mut ecs = self.ecs.lock();
350        let value = self.query::<&T>(&mut ecs);
351        f(value)
352    }
353
354    /// Optionally get a component for this client and call the given function.
355    ///
356    /// Similar to [`Self::get_component`], but doesn't clone the component
357    /// since it's passed as a reference. [`Self::ecs`] will remain locked
358    /// while the callback is being run.
359    ///
360    /// ```
361    /// # use azalea_client::{Client, mining::Mining};
362    /// # fn example(bot: &Client) {
363    /// let is_mining = bot.map_get_component::<Mining, _>(|m| m.is_some());
364    /// # }
365    /// ```
366    pub fn map_get_component<T: Component, R>(&self, f: impl FnOnce(Option<&T>) -> R) -> R {
367        let mut ecs = self.ecs.lock();
368        let value = self.query::<Option<&T>>(&mut ecs);
369        f(value)
370    }
371
372    /// Get an `RwLock` with a reference to our (potentially shared) world.
373    ///
374    /// This gets the [`Instance`] from the client's [`InstanceHolder`]
375    /// component. If it's a normal client, then it'll be the same as the
376    /// world the client has loaded. If the client is using a shared world,
377    /// then the shared world will be a superset of the client's world.
378    pub fn world(&self) -> Arc<RwLock<Instance>> {
379        let instance_holder = self.component::<InstanceHolder>();
380        instance_holder.instance.clone()
381    }
382
383    /// Get an `RwLock` with a reference to the world that this client has
384    /// loaded.
385    ///
386    /// ```
387    /// # use azalea_core::position::ChunkPos;
388    /// # fn example(client: &azalea_client::Client) {
389    /// let world = client.partial_world();
390    /// let is_0_0_loaded = world.read().chunks.limited_get(&ChunkPos::new(0, 0)).is_some();
391    /// # }
392    pub fn partial_world(&self) -> Arc<RwLock<PartialInstance>> {
393        let instance_holder = self.component::<InstanceHolder>();
394        instance_holder.partial_instance.clone()
395    }
396
397    /// Returns whether we have a received the login packet yet.
398    pub fn logged_in(&self) -> bool {
399        // the login packet tells us the world name
400        self.query::<Option<&InstanceName>>(&mut self.ecs.lock())
401            .is_some()
402    }
403
404    /// Tell the server we changed our game options (i.e. render distance, main
405    /// hand). If this is not set before the login packet, the default will
406    /// be sent.
407    ///
408    /// ```rust,no_run
409    /// # use azalea_client::{Client, ClientInformation};
410    /// # async fn example(bot: Client) -> Result<(), Box<dyn std::error::Error>> {
411    /// bot.set_client_information(ClientInformation {
412    ///     view_distance: 2,
413    ///     ..Default::default()
414    /// })
415    /// .await;
416    /// # Ok(())
417    /// # }
418    /// ```
419    pub async fn set_client_information(&self, client_information: ClientInformation) {
420        {
421            let mut ecs = self.ecs.lock();
422            let mut client_information_mut = self.query::<&mut ClientInformation>(&mut ecs);
423            *client_information_mut = client_information.clone();
424        }
425
426        if self.logged_in() {
427            debug!(
428                "Sending client information (already logged in): {:?}",
429                client_information
430            );
431            self.write_packet(game::s_client_information::ServerboundClientInformation {
432                client_information,
433            });
434        }
435    }
436}
437
438impl Client {
439    /// Get the position of this client.
440    ///
441    /// This is a shortcut for `Vec3::from(&bot.component::<Position>())`.
442    ///
443    /// Note that this value is given a default of [`Vec3::ZERO`] when it
444    /// receives the login packet, its true position may be set ticks
445    /// later.
446    pub fn position(&self) -> Vec3 {
447        Vec3::from(
448            &self
449                .get_component::<Position>()
450                .expect("the client's position hasn't been initialized yet"),
451        )
452    }
453
454    /// Get the position of this client's eyes.
455    ///
456    /// This is a shortcut for
457    /// `bot.position().up(bot.component::<EyeHeight>())`.
458    pub fn eye_position(&self) -> Vec3 {
459        self.position().up((*self.component::<EyeHeight>()) as f64)
460    }
461
462    /// Get the health of this client.
463    ///
464    /// This is a shortcut for `*bot.component::<Health>()`.
465    pub fn health(&self) -> f32 {
466        *self.component::<Health>()
467    }
468
469    /// Get the hunger level of this client, which includes both food and
470    /// saturation.
471    ///
472    /// This is a shortcut for `self.component::<Hunger>().to_owned()`.
473    pub fn hunger(&self) -> Hunger {
474        self.component::<Hunger>().to_owned()
475    }
476
477    /// Get the username of this client.
478    ///
479    /// This is a shortcut for
480    /// `bot.component::<GameProfileComponent>().name.to_owned()`.
481    pub fn username(&self) -> String {
482        self.profile().name.to_owned()
483    }
484
485    /// Get the Minecraft UUID of this client.
486    ///
487    /// This is a shortcut for `bot.component::<GameProfileComponent>().uuid`.
488    pub fn uuid(&self) -> Uuid {
489        self.profile().uuid
490    }
491
492    /// Get a map of player UUIDs to their information in the tab list.
493    ///
494    /// This is a shortcut for `*bot.component::<TabList>()`.
495    pub fn tab_list(&self) -> HashMap<Uuid, PlayerInfo> {
496        (*self.component::<TabList>()).clone()
497    }
498
499    /// Returns the [`GameProfile`] for our client. This contains your username,
500    /// UUID, and skin data.
501    ///
502    /// These values are set by the server upon login, which means they might
503    /// not match up with your actual game profile. Also, note that the username
504    /// and skin that gets displayed in-game will actually be the ones from
505    /// the tab list, which you can get from [`Self::tab_list`].
506    ///
507    /// This as also available from the ECS as [`GameProfileComponent`].
508    pub fn profile(&self) -> GameProfile {
509        (*self.component::<GameProfileComponent>()).clone()
510    }
511
512    /// A convenience function to get the Minecraft Uuid of a player by their
513    /// username, if they're present in the tab list.
514    ///
515    /// You can chain this with [`Client::entity_by_uuid`] to get the ECS
516    /// `Entity` for the player.
517    pub fn player_uuid_by_username(&self, username: &str) -> Option<Uuid> {
518        self.tab_list()
519            .values()
520            .find(|player| player.profile.name == username)
521            .map(|player| player.profile.uuid)
522    }
523
524    /// Get an ECS `Entity` in the world by its Minecraft UUID, if it's within
525    /// render distance.
526    pub fn entity_by_uuid(&self, uuid: Uuid) -> Option<Entity> {
527        self.map_resource::<EntityUuidIndex, _>(|entity_uuid_index| entity_uuid_index.get(&uuid))
528    }
529
530    /// Convert an ECS `Entity` to a [`MinecraftEntityId`].
531    pub fn minecraft_entity_by_ecs_entity(&self, entity: Entity) -> Option<MinecraftEntityId> {
532        self.map_component::<EntityIdIndex, _>(|entity_id_index| {
533            entity_id_index.get_by_ecs_entity(entity)
534        })
535    }
536    /// Convert a [`MinecraftEntityId`] to an ECS `Entity`.
537    pub fn ecs_entity_by_minecraft_entity(&self, entity: MinecraftEntityId) -> Option<Entity> {
538        self.map_component::<EntityIdIndex, _>(|entity_id_index| {
539            entity_id_index.get_by_minecraft_entity(entity)
540        })
541    }
542
543    /// Call the given function with the client's [`RegistryHolder`].
544    ///
545    /// The player's instance (aka world) will be locked during this time, which
546    /// may result in a deadlock if you try to access the instance again while
547    /// in the function.
548    ///
549    /// [`RegistryHolder`]: azalea_core::registry_holder::RegistryHolder
550    pub fn with_registry_holder<R>(
551        &self,
552        f: impl FnOnce(&azalea_core::registry_holder::RegistryHolder) -> R,
553    ) -> R {
554        let instance = self.world();
555        let registries = &instance.read().registries;
556        f(registries)
557    }
558
559    /// Resolve the given registry to its name.
560    ///
561    /// This is necessary for data-driven registries like [`Enchantment`].
562    ///
563    /// [`Enchantment`]: azalea_registry::Enchantment
564    pub fn resolve_registry_name(
565        &self,
566        registry: &impl ResolvableDataRegistry,
567    ) -> Option<ResourceLocation> {
568        self.with_registry_holder(|registries| registry.resolve_name(registries))
569    }
570    /// Resolve the given registry to its name and data and call the given
571    /// function with it.
572    ///
573    /// This is necessary for data-driven registries like [`Enchantment`].
574    ///
575    /// If you just want the value name, use [`Self::resolve_registry_name`]
576    /// instead.
577    ///
578    /// [`Enchantment`]: azalea_registry::Enchantment
579    pub fn with_resolved_registry<R>(
580        &self,
581        registry: impl ResolvableDataRegistry,
582        f: impl FnOnce(&ResourceLocation, &NbtCompound) -> R,
583    ) -> Option<R> {
584        self.with_registry_holder(|registries| {
585            registry
586                .resolve(registries)
587                .map(|(name, data)| f(name, data))
588        })
589    }
590}
591
592/// A bundle of components that's inserted right when we switch to the `login`
593/// state and stay present on our clients until we disconnect.
594///
595/// For the components that are only present in the `game` state, see
596/// [`JoinedClientBundle`].
597#[derive(Bundle)]
598pub struct LocalPlayerBundle {
599    pub raw_connection: RawConnection,
600    pub client_information: ClientInformation,
601    pub instance_holder: InstanceHolder,
602
603    pub metadata: azalea_entity::metadata::PlayerMetadataBundle,
604}
605
606/// A bundle for the components that are present on a local player that is
607/// currently in the `game` protocol state. If you want to filter for this, use
608/// [`InGameState`].
609#[derive(Bundle, Default)]
610pub struct JoinedClientBundle {
611    // note that InstanceHolder isn't here because it's set slightly before we fully join the world
612    pub physics_state: PhysicsState,
613    pub inventory: Inventory,
614    pub tab_list: TabList,
615    pub current_sequence_number: CurrentSequenceNumber,
616    pub last_sent_direction: LastSentLookDirection,
617    pub abilities: PlayerAbilities,
618    pub permission_level: PermissionLevel,
619    pub chunk_batch_info: ChunkBatchInfo,
620    pub hunger: Hunger,
621
622    pub entity_id_index: EntityIdIndex,
623
624    pub mining: mining::MineBundle,
625    pub attack: attack::AttackBundle,
626
627    pub in_game_state: InGameState,
628}
629
630/// A marker component for local players that are currently in the
631/// `game` state.
632#[derive(Component, Clone, Debug, Default)]
633pub struct InGameState;
634/// A marker component for local players that are currently in the
635/// `configuration` state.
636#[derive(Component, Clone, Debug, Default)]
637pub struct InConfigState;
638
639pub struct AzaleaPlugin;
640impl Plugin for AzaleaPlugin {
641    fn build(&self, app: &mut App) {
642        app.add_systems(
643            Update,
644            (
645                // add GameProfileComponent when we get an AddPlayerEvent
646                retroactively_add_game_profile_component
647                    .after(EntityUpdateSet::Index)
648                    .after(crate::join::handle_start_join_server_event),
649            ),
650        )
651        .init_resource::<InstanceContainer>()
652        .init_resource::<TabList>();
653    }
654}
655
656/// Create the ECS world, and return a function that begins running systems.
657/// This exists to allow you to make last-millisecond updates to the world
658/// before any systems start running.
659///
660/// You can create your app with `App::new()`, but don't forget to add
661/// [`DefaultPlugins`].
662#[doc(hidden)]
663pub fn start_ecs_runner(app: &mut SubApp) -> (Arc<Mutex<World>>, impl FnOnce()) {
664    // this block is based on Bevy's default runner:
665    // https://github.com/bevyengine/bevy/blob/390877cdae7a17095a75c8f9f1b4241fe5047e83/crates/bevy_app/src/schedule_runner.rs#L77-L85
666    if app.plugins_state() != PluginsState::Cleaned {
667        // Wait for plugins to load
668        if app.plugins_state() == PluginsState::Adding {
669            info!("Waiting for plugins to load ...");
670            while app.plugins_state() == PluginsState::Adding {
671                thread::yield_now();
672            }
673        }
674        // Finish adding plugins and cleanup
675        app.finish();
676        app.cleanup();
677    }
678
679    // all resources should have been added by now so we can take the ecs from the
680    // app
681    let ecs = Arc::new(Mutex::new(mem::take(app.world_mut())));
682
683    let ecs_clone = ecs.clone();
684    let outer_schedule_label = *app.update_schedule.as_ref().unwrap();
685    let start_running_systems = move || {
686        tokio::spawn(run_schedule_loop(ecs_clone, outer_schedule_label));
687    };
688
689    (ecs, start_running_systems)
690}
691
692async fn run_schedule_loop(ecs: Arc<Mutex<World>>, outer_schedule_label: InternedScheduleLabel) {
693    let mut last_update: Option<Instant> = None;
694    let mut last_tick: Option<Instant> = None;
695
696    // azalea runs the Update schedule at most 60 times per second to simulate
697    // framerate. unlike vanilla though, we also only handle packets during Updates
698    // due to everything running in ecs systems.
699    const UPDATE_DURATION_TARGET: Duration = Duration::from_micros(1_000_000 / 60);
700    // minecraft runs at 20 tps
701    const GAME_TICK_DURATION_TARGET: Duration = Duration::from_micros(1_000_000 / 20);
702
703    loop {
704        // sleep until the next update if necessary
705        let now = Instant::now();
706        if let Some(last_update) = last_update {
707            let elapsed = now.duration_since(last_update);
708            if elapsed < UPDATE_DURATION_TARGET {
709                time::sleep(UPDATE_DURATION_TARGET - elapsed).await;
710            }
711        }
712        last_update = Some(now);
713
714        let mut ecs = ecs.lock();
715
716        // if last tick is None or more than 50ms ago, run the GameTick schedule
717        ecs.run_schedule(outer_schedule_label);
718        if last_tick
719            .map(|last_tick| last_tick.elapsed() > GAME_TICK_DURATION_TARGET)
720            .unwrap_or(true)
721        {
722            if let Some(last_tick) = &mut last_tick {
723                *last_tick += GAME_TICK_DURATION_TARGET;
724
725                // if we're more than 10 ticks behind, set last_tick to now.
726                // vanilla doesn't do it in exactly the same way but it shouldn't really matter
727                if (now - *last_tick) > GAME_TICK_DURATION_TARGET * 10 {
728                    warn!(
729                        "GameTick is more than 10 ticks behind, skipping ticks so we don't have to burst too much"
730                    );
731                    *last_tick = now;
732                }
733            } else {
734                last_tick = Some(now);
735            }
736            ecs.run_schedule(GameTick);
737        }
738
739        ecs.clear_trackers();
740    }
741}
742
743pub struct AmbiguityLoggerPlugin;
744impl Plugin for AmbiguityLoggerPlugin {
745    fn build(&self, app: &mut App) {
746        app.edit_schedule(Update, |schedule| {
747            schedule.set_build_settings(ScheduleBuildSettings {
748                ambiguity_detection: LogLevel::Warn,
749                ..Default::default()
750            });
751        });
752        app.edit_schedule(GameTick, |schedule| {
753            schedule.set_build_settings(ScheduleBuildSettings {
754                ambiguity_detection: LogLevel::Warn,
755                ..Default::default()
756            });
757        });
758    }
759}