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