azalea_client/
client.rs

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