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