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