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}