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