aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTING.md6
-rw-r--r--Cargo.toml2
-rw-r--r--azalea-client/src/plugins/packet/game/mod.rs142
-rw-r--r--azalea-client/src/plugins/packet/mod.rs50
-rw-r--r--azalea-client/src/plugins/packet/relative_updates.rs (renamed from azalea-entity/src/plugin/relative_updates.rs)118
-rw-r--r--azalea-entity/src/lib.rs4
-rw-r--r--azalea-entity/src/plugin/components.rs4
-rw-r--r--azalea-entity/src/plugin/mod.rs52
-rw-r--r--azalea/examples/testbot/main.rs2
-rw-r--r--azalea/examples/testbot/mspt.rs69
10 files changed, 308 insertions, 141 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index bf26bf5a..589a113d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -18,6 +18,8 @@ If you're working with low-level physics or packet related code, it's quite easy
The second major thing to watch out for is accidentally introducing performance regressions. Certain parts of Azalea are highly performance sensitive (notably, the pathfinder), so most changes in these areas should be benchmarked to avoid accidentally hurting performance.
+You're encouraged to write relevant tests and benchmarks.
+
## Profiling
Please see [the chapter about profiling in the Rust performance book](https://nnethercote.github.io/perf-book/profiling.html).
@@ -29,12 +31,10 @@ cargo install flamegraph
RUSTFLAGS="-C force-frame-pointers=yes" cargo r -r --example testbot
# wait a few seconds so chunks being loaded doesn't affect the flamegraph, and
# then run this in a separate window:
-flamegraph -p $(pidof testbot)
+flamegraph -p $(pidof testbot) --deterministic
# wait about 15 seconds, then ctrl+c, and view the flamegraph.svg
```
## AI Policy
Please avoid using generative AI to make contributions to Azalea. We do not enjoy working with code that wasn't written by people.
-
-
diff --git a/Cargo.toml b/Cargo.toml
index a254da3e..9f050d7f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -135,7 +135,7 @@ cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
# --- Profile Settings ---
[profile.release]
-debug = true
+debug = "line-tables-only"
# decoding packets takes forever if we don't do this
[profile.dev.package.azalea-crypto]
diff --git a/azalea-client/src/plugins/packet/game/mod.rs b/azalea-client/src/plugins/packet/game/mod.rs
index 9a1c022b..9f16c204 100644
--- a/azalea-client/src/plugins/packet/game/mod.rs
+++ b/azalea-client/src/plugins/packet/game/mod.rs
@@ -9,7 +9,7 @@ use azalea_core::{
};
use azalea_entity::{
ActiveEffects, Dead, EntityBundle, EntityKindComponent, HasClientLoaded, LoadedBy, LocalEntity,
- LookDirection, Physics, PlayerAbilities, Position, RelativeEntityUpdate,
+ LookDirection, Physics, PlayerAbilities, Position,
indexing::{EntityIdIndex, EntityUuidIndex},
inventory::Inventory,
metadata::{Health, apply_metadata},
@@ -36,7 +36,10 @@ use crate::{
inventory::{ClientsideCloseContainerEvent, MenuOpenedEvent, SetContainerContentEvent},
local_player::{Experience, Hunger, LocalGameMode, TabList, WorldHolder},
movement::{KnockbackData, KnockbackEvent},
- packet::{as_system, declare_packet_handlers},
+ packet::{
+ as_system, declare_packet_handlers,
+ relative_updates::{EntityUpdateQuery, RelativeEntityUpdate, should_apply_entity_update},
+ },
player::{GameProfileComponent, PlayerInfo},
tick_counter::TicksConnected,
};
@@ -716,36 +719,37 @@ impl GamePacketHandler<'_> {
// vanilla servers use this packet for knockback, but note that the Explode
// packet is also sometimes used by servers for knockback
- as_system::<(Commands, Query<(&EntityIdIndex, &WorldHolder)>)>(
- self.ecs,
- |(mut commands, query)| {
- let (entity_id_index, world_holder) = query.get(self.player).unwrap();
-
- let Some(entity) = entity_id_index.get_by_minecraft_entity(p.id) else {
- // note that this log (and some other ones like the one in RemoveEntities)
- // sometimes happens when killing mobs. it seems to be a vanilla bug, which is
- // why it's a debug log instead of a warning
- debug!(
- "Got set entity motion packet for unknown entity id {}",
- p.id
- );
- return;
- };
-
- // this is to make sure the same entity velocity update doesn't get sent
- // multiple times when in swarms
+ as_system::<(
+ Commands,
+ Query<(&EntityIdIndex, &WorldHolder)>,
+ EntityUpdateQuery,
+ )>(self.ecs, |(mut commands, query, entity_update_query)| {
+ let (entity_id_index, world_holder) = query.get(self.player).unwrap();
- let data = KnockbackData::Set(p.delta.to_vec3());
+ let Some(entity) = entity_id_index.get_by_minecraft_entity(p.id) else {
+ // note that this log (and some other ones like the one in RemoveEntities)
+ // sometimes happens when killing mobs. it seems to be a vanilla bug, which is
+ // why it's a debug log instead of a warning
+ debug!(
+ "Got set entity motion packet for unknown entity id {}",
+ p.id
+ );
+ return;
+ };
- commands.entity(entity).queue(RelativeEntityUpdate::new(
- world_holder.partial.clone(),
- move |entity_mut| {
- entity_mut
- .world_scope(|world| world.trigger(KnockbackEvent { entity, data }));
- },
- ));
- },
- );
+ let data = KnockbackData::Set(p.delta.to_vec3());
+
+ // this is to make sure the same entity velocity update doesn't get sent
+ // multiple times when in swarms
+ if should_apply_entity_update(
+ &mut commands,
+ &mut world_holder.partial.write(),
+ entity,
+ entity_update_query,
+ ) {
+ commands.trigger(KnockbackEvent { entity, data });
+ }
+ });
}
pub fn set_entity_link(&mut self, p: &ClientboundSetEntityLink) {
@@ -795,8 +799,8 @@ impl GamePacketHandler<'_> {
as_system::<(Commands, Query<(&EntityIdIndex, &WorldHolder)>)>(
self.ecs,
- |(mut commands, mut query)| {
- let (entity_id_index, world_holder) = query.get_mut(self.player).unwrap();
+ |(mut commands, query)| {
+ let (entity_id_index, world_holder) = query.get(self.player).unwrap();
let Some(entity) = entity_id_index.get_by_minecraft_entity(p.id) else {
warn!("Got teleport entity packet for unknown entity id {}", p.id);
@@ -840,8 +844,8 @@ impl GamePacketHandler<'_> {
pub fn move_entity_pos(&mut self, p: &ClientboundMoveEntityPos) {
as_system::<(Commands, Query<(&EntityIdIndex, &WorldHolder)>)>(
self.ecs,
- |(mut commands, mut query)| {
- let (entity_id_index, world_holder) = query.get_mut(self.player).unwrap();
+ |(mut commands, query)| {
+ let (entity_id_index, world_holder) = query.get(self.player).unwrap();
debug!("Got move entity pos packet {p:?}");
@@ -879,8 +883,8 @@ impl GamePacketHandler<'_> {
pub fn move_entity_pos_rot(&mut self, p: &ClientboundMoveEntityPosRot) {
as_system::<(Commands, Query<(&EntityIdIndex, &WorldHolder)>)>(
self.ecs,
- |(mut commands, mut query)| {
- let (entity_id_index, world_holder) = query.get_mut(self.player).unwrap();
+ |(mut commands, query)| {
+ let (entity_id_index, world_holder) = query.get(self.player).unwrap();
debug!("Got move entity pos rot packet {p:?}");
@@ -929,8 +933,8 @@ impl GamePacketHandler<'_> {
pub fn move_entity_rot(&mut self, p: &ClientboundMoveEntityRot) {
as_system::<(Commands, Query<(&EntityIdIndex, &WorldHolder)>)>(
self.ecs,
- |(mut commands, mut query)| {
- let (entity_id_index, world_holder) = query.get_mut(self.player).unwrap();
+ |(mut commands, query)| {
+ let (entity_id_index, world_holder) = query.get(self.player).unwrap();
let entity = entity_id_index.get_by_minecraft_entity(p.entity_id);
if let Some(entity) = entity {
@@ -1527,10 +1531,28 @@ impl GamePacketHandler<'_> {
}
pub fn entity_position_sync(&mut self, p: &ClientboundEntityPositionSync) {
- as_system::<(Commands, Query<(&EntityIdIndex, &WorldHolder)>)>(
+ as_system::<(
+ Commands,
+ Query<(
+ &EntityIdIndex,
+ &WorldHolder,
+ Option<&LocalEntity>,
+ &mut Physics,
+ &mut Position,
+ &mut LookDirection,
+ )>,
+ EntityUpdateQuery,
+ )>(
self.ecs,
- |(mut commands, mut query)| {
- let (entity_id_index, world_holder) = query.get_mut(self.player).unwrap();
+ |(mut commands, mut query, entity_update_query)| {
+ let (
+ entity_id_index,
+ world_holder,
+ local_entity,
+ mut physics,
+ mut position,
+ mut look_direction,
+ ) = query.get_mut(self.player).unwrap();
let Some(entity) = entity_id_index.get_by_minecraft_entity(p.id) else {
debug!("Got teleport entity packet for unknown entity id {}", p.id);
@@ -1541,28 +1563,32 @@ impl GamePacketHandler<'_> {
let new_on_ground = p.on_ground;
let new_look_direction = p.values.look_direction;
- commands.entity(entity).queue(RelativeEntityUpdate::new(
- world_holder.partial.clone(),
- move |entity_mut| {
- let is_local_entity = entity_mut.get::<LocalEntity>().is_some();
- let mut physics = entity_mut.get_mut::<Physics>().unwrap();
+ if !should_apply_entity_update(
+ &mut commands,
+ &mut world_holder.partial.write(),
+ entity,
+ entity_update_query,
+ ) {
+ return;
+ }
+ let is_local_entity = local_entity.is_some();
- physics.vec_delta_codec.set_base(new_position);
+ physics.vec_delta_codec.set_base(new_position);
- if is_local_entity {
- debug!("Ignoring entity position sync packet for local player");
- return;
- }
+ if is_local_entity {
+ debug!("Ignoring entity position sync packet for local player");
+ return;
+ }
- physics.set_on_ground(new_on_ground);
+ physics.set_on_ground(new_on_ground);
- let mut position = entity_mut.get_mut::<Position>().unwrap();
- **position = new_position;
+ if **position != new_position {
+ **position = new_position;
+ }
- let mut look_direction = entity_mut.get_mut::<LookDirection>().unwrap();
- *look_direction = new_look_direction;
- },
- ));
+ if *look_direction != new_look_direction {
+ *look_direction = new_look_direction;
+ }
},
);
}
diff --git a/azalea-client/src/plugins/packet/mod.rs b/azalea-client/src/plugins/packet/mod.rs
index 63a94ee0..4309850e 100644
--- a/azalea-client/src/plugins/packet/mod.rs
+++ b/azalea-client/src/plugins/packet/mod.rs
@@ -11,6 +11,7 @@ use crate::chat::ChatReceivedEvent;
pub mod config;
pub mod game;
pub mod login;
+pub mod relative_updates;
pub struct PacketPlugin;
@@ -30,23 +31,27 @@ pub fn death_event_on_0_health(
impl Plugin for PacketPlugin {
fn build(&self, app: &mut App) {
- app.add_observer(game::handle_outgoing_packets_observer)
- .add_observer(config::handle_outgoing_packets_observer)
- .add_observer(login::handle_outgoing_packets_observer)
- .add_systems(Update, death_event_on_0_health)
- .add_message::<game::ReceiveGamePacketEvent>()
- .add_message::<config::ReceiveConfigPacketEvent>()
- .add_message::<login::ReceiveLoginPacketEvent>()
- //
- .add_message::<game::AddPlayerEvent>()
- .add_message::<game::RemovePlayerEvent>()
- .add_message::<game::UpdatePlayerEvent>()
- .add_message::<ChatReceivedEvent>()
- .add_message::<game::DeathEvent>()
- .add_message::<game::KeepAliveEvent>()
- .add_message::<game::ResourcePackEvent>()
- .add_message::<game::WorldLoadedEvent>()
- .add_message::<login::ReceiveCustomQueryEvent>();
+ app.add_systems(
+ Update,
+ relative_updates::debug_detect_updates_received_on_local_entities,
+ )
+ .add_observer(game::handle_outgoing_packets_observer)
+ .add_observer(config::handle_outgoing_packets_observer)
+ .add_observer(login::handle_outgoing_packets_observer)
+ .add_systems(Update, death_event_on_0_health)
+ .add_message::<game::ReceiveGamePacketEvent>()
+ .add_message::<config::ReceiveConfigPacketEvent>()
+ .add_message::<login::ReceiveLoginPacketEvent>()
+ //
+ .add_message::<game::AddPlayerEvent>()
+ .add_message::<game::RemovePlayerEvent>()
+ .add_message::<game::UpdatePlayerEvent>()
+ .add_message::<ChatReceivedEvent>()
+ .add_message::<game::DeathEvent>()
+ .add_message::<game::KeepAliveEvent>()
+ .add_message::<game::ResourcePackEvent>()
+ .add_message::<game::WorldLoadedEvent>()
+ .add_message::<login::ReceiveCustomQueryEvent>();
}
}
@@ -70,12 +75,21 @@ macro_rules! __declare_packet_handlers {
pub(crate) use __declare_packet_handlers as declare_packet_handlers;
+#[derive(Resource)]
+struct CachedSystemState<T: SystemParam + 'static>(SystemState<T>);
+
pub(crate) fn as_system<T>(ecs: &mut World, f: impl FnOnce(T::Item<'_, '_>))
where
T: SystemParam + 'static,
{
- let mut system_state = SystemState::<T>::new(ecs);
+ // creating a new SystemState is expensive, so we save them as a Resource in the
+ // ecs
+ let mut system_state = match ecs.remove_resource::<CachedSystemState<T>>() {
+ Some(s) => s.0,
+ None => SystemState::<T>::new(ecs),
+ };
let values = system_state.get_mut(ecs);
f(values);
system_state.apply(ecs);
+ ecs.insert_resource(CachedSystemState(system_state));
}
diff --git a/azalea-entity/src/plugin/relative_updates.rs b/azalea-client/src/plugins/packet/relative_updates.rs
index 53eb4c95..2f7112b8 100644
--- a/azalea-entity/src/plugin/relative_updates.rs
+++ b/azalea-client/src/plugins/packet/relative_updates.rs
@@ -18,13 +18,14 @@
use std::sync::Arc;
use azalea_core::entity_id::MinecraftEntityId;
+use azalea_entity::LocalEntity;
use azalea_world::PartialWorld;
use bevy_ecs::prelude::*;
use derive_more::{Deref, DerefMut};
use parking_lot::RwLock;
use tracing::warn;
-use crate::LocalEntity;
+use crate::packet::as_system;
/// An [`EntityCommand`] that applies a "relative update" to an entity, which
/// means this update won't be run multiple times by different clients in the
@@ -64,42 +65,89 @@ impl RelativeEntityUpdate {
#[derive(Component, Debug, Deref, DerefMut)]
pub struct UpdatesReceived(u32);
-impl EntityCommand for RelativeEntityUpdate {
- fn apply(self, mut entity: EntityWorldMut) {
- let partial_entity_infos = &mut self.partial_world.write().entity_infos;
-
- if Some(entity.id()) == partial_entity_infos.owner_entity {
- // if the entity owns this partial world, it's always allowed to update itself
- (self.update)(&mut entity);
- return;
- };
-
- let entity_id = *entity.get::<MinecraftEntityId>().unwrap();
- if entity.contains::<LocalEntity>() {
- // a client tried to update another client, which isn't allowed
- return;
- }
+pub type EntityUpdateQuery<'world, 'state, 'a> = Query<
+ 'world,
+ 'state,
+ (
+ &'a MinecraftEntityId,
+ Option<&'a UpdatesReceived>,
+ Option<&'a LocalEntity>,
+ ),
+>;
+
+/// See [`RelativeEntityUpdate`] for details.
+///
+/// Calling this function will have the same effect as using the Command, but
+/// it's more performant than the Command.
+pub fn should_apply_entity_update(
+ commands: &mut Commands,
+ partial_world: &mut PartialWorld,
+ entity: Entity,
+ entity_update_query: EntityUpdateQuery,
+) -> bool {
+ let partial_entity_infos = &mut partial_world.entity_infos;
+
+ if Some(entity) == partial_entity_infos.owner_entity {
+ // if the entity owns this partial world, it's always allowed to update itself
+ return true;
+ };
+
+ let Ok((minecraft_entity_id, updates_received, local_entity)) = entity_update_query.get(entity)
+ else {
+ warn!("called should_apply_entity_update on an entity with missing components");
+ return false;
+ };
+
+ if local_entity.is_some() {
+ // a client tried to update another client, which isn't allowed
+ return false;
+ }
+
+ let this_client_updates_received = partial_entity_infos
+ .updates_received
+ .get(&minecraft_entity_id)
+ .copied();
- let this_client_updates_received = partial_entity_infos
+ let can_update = if let Some(updates_received) = updates_received {
+ this_client_updates_received.unwrap_or(1) == **updates_received
+ } else {
+ // no UpdatesReceived means the entity was just spawned
+ true
+ };
+ if can_update {
+ let new_updates_received = this_client_updates_received.unwrap_or(0) + 1;
+ partial_entity_infos
.updates_received
- .get(&entity_id)
- .copied();
-
- let can_update = if let Some(updates_received) = entity.get::<UpdatesReceived>() {
- this_client_updates_received.unwrap_or(1) == **updates_received
- } else {
- // no UpdatesReceived means the entity was just spawned
- true
- };
- if can_update {
- let new_updates_received = this_client_updates_received.unwrap_or(0) + 1;
- partial_entity_infos
- .updates_received
- .insert(entity_id, new_updates_received);
-
- entity.insert(UpdatesReceived(new_updates_received));
-
- (self.update)(&mut entity);
+ .insert(*minecraft_entity_id, new_updates_received);
+
+ commands
+ .entity(entity)
+ .insert(UpdatesReceived(new_updates_received));
+
+ return true;
+ }
+ false
+}
+
+impl EntityCommand for RelativeEntityUpdate {
+ fn apply(self, mut entity_mut: EntityWorldMut) {
+ let partial_world = self.partial_world.clone();
+ let mut should_update = false;
+ let entity = entity_mut.id();
+
+ entity_mut.world_scope(|ecs| {
+ as_system::<(Commands, EntityUpdateQuery)>(ecs, |(mut commands, query)| {
+ should_update = should_apply_entity_update(
+ &mut commands,
+ &mut partial_world.write(),
+ entity,
+ query,
+ );
+ });
+ });
+
+ if should_update {
+ (self.update)(&mut entity_mut);
}
}
}
diff --git a/azalea-entity/src/lib.rs b/azalea-entity/src/lib.rs
index fa4feffb..aacc3f54 100644
--- a/azalea-entity/src/lib.rs
+++ b/azalea-entity/src/lib.rs
@@ -424,6 +424,10 @@ pub struct PlayerAbilities {
/// The type of fluid that is at an entity's eye position, while also accounting
/// for fluid height.
+///
+/// This is only updated for [`AbstractLiving`] entities.
+///
+/// [`AbstractLiving`]: metadata::AbstractLiving
#[cfg_attr(feature = "bevy_ecs", derive(bevy_ecs::component::Component))]
#[derive(Clone, Copy, Debug, Deref, DerefMut, PartialEq)]
pub struct FluidOnEyes(FluidKind);
diff --git a/azalea-entity/src/plugin/components.rs b/azalea-entity/src/plugin/components.rs
index 4698a808..eaeeb0d0 100644
--- a/azalea-entity/src/plugin/components.rs
+++ b/azalea-entity/src/plugin/components.rs
@@ -94,8 +94,8 @@ pub struct OnClimbable(bool);
/// A component that indicates whether the player is currently sneaking.
///
-/// If the entity isn't a local player, then this is just a shortcut for
-/// checking if the [`Pose`] is `Crouching`.
+/// If the entity is a player but isn't a local player, then this is just a
+/// shortcut for checking if the [`Pose`] is `Crouching`.
///
/// If you need to modify this value, use
/// `azalea_client::PhysicsState::trying_to_crouch` or `Client::set_crouching`
diff --git a/azalea-entity/src/plugin/mod.rs b/azalea-entity/src/plugin/mod.rs
index e40c24f2..b86c6b7d 100644
--- a/azalea-entity/src/plugin/mod.rs
+++ b/azalea-entity/src/plugin/mod.rs
@@ -1,6 +1,5 @@
mod components;
pub mod indexing;
-mod relative_updates;
use std::collections::HashSet;
@@ -17,13 +16,12 @@ use bevy_ecs::prelude::*;
pub use components::*;
use derive_more::{Deref, DerefMut};
use indexing::EntityUuidIndex;
-pub use relative_updates::RelativeEntityUpdate;
use tracing::debug;
use crate::{
FluidOnEyes, LookDirection, Physics, Pose, Position,
dimensions::{EntityDimensions, calculate_dimensions},
- metadata::Health,
+ metadata::{self, Health, Player},
};
/// A Bevy [`SystemSet`] for various types of entity updates.
@@ -57,7 +55,6 @@ impl Plugin for EntityPlugin {
.chain()
.in_set(EntityUpdateSystems::Index),
(
- relative_updates::debug_detect_updates_received_on_local_entities,
debug_new_entity,
add_dead,
clamp_look_direction,
@@ -97,27 +94,32 @@ pub fn add_dead(mut commands: Commands, query: Query<(Entity, &Health), Changed<
}
pub fn update_fluid_on_eyes(
- mut query: Query<(&mut FluidOnEyes, &Position, &EntityDimensions, &WorldName)>,
+ mut query: Query<
+ (&mut FluidOnEyes, &Position, &EntityDimensions, &WorldName),
+ With<metadata::AbstractLiving>,
+ >,
worlds: Res<Worlds>,
) {
- for (mut fluid_on_eyes, position, dimensions, world_name) in query.iter_mut() {
- let Some(world) = worlds.get(world_name) else {
- continue;
- };
-
- let adjusted_eye_y = position.y + (dimensions.eye_height as f64) - 0.1111111119389534;
- let eye_block_pos = BlockPos::from(position.with_y(adjusted_eye_y));
- let fluid_at_eye = world
- .read()
- .get_fluid_state(eye_block_pos)
- .unwrap_or_default();
- let fluid_cutoff_y = (eye_block_pos.y as f32 + fluid_at_eye.height()) as f64;
- if fluid_cutoff_y > adjusted_eye_y {
- **fluid_on_eyes = fluid_at_eye.kind;
- } else {
- **fluid_on_eyes = FluidKind::Empty;
- }
- }
+ query
+ .par_iter_mut()
+ .for_each(|(mut fluid_on_eyes, position, dimensions, world_name)| {
+ let Some(world) = worlds.get(world_name) else {
+ return;
+ };
+
+ let adjusted_eye_y = position.y + (dimensions.eye_height as f64) - 0.1111111119389534;
+ let eye_block_pos = BlockPos::from(position.with_y(adjusted_eye_y));
+ let fluid_at_eye = world
+ .read()
+ .get_fluid_state(eye_block_pos)
+ .unwrap_or_default();
+ let fluid_cutoff_y = (eye_block_pos.y as f32 + fluid_at_eye.height()) as f64;
+ if fluid_cutoff_y > adjusted_eye_y {
+ **fluid_on_eyes = fluid_at_eye.kind;
+ } else {
+ **fluid_on_eyes = FluidKind::Empty;
+ }
+ });
}
pub fn update_on_climbable(
@@ -229,7 +231,9 @@ pub fn update_dimensions(
}
}
-pub fn update_crouching(query: Query<(&mut Crouching, &Pose), Without<LocalEntity>>) {
+pub fn update_crouching(
+ query: Query<(&mut Crouching, &Pose), (Without<LocalEntity>, With<Player>)>,
+) {
for (mut crouching, pose) in query {
let new_crouching = *pose == Pose::Crouching;
// avoid triggering change detection
diff --git a/azalea/examples/testbot/main.rs b/azalea/examples/testbot/main.rs
index c74dcee7..63d83c8a 100644
--- a/azalea/examples/testbot/main.rs
+++ b/azalea/examples/testbot/main.rs
@@ -24,6 +24,7 @@
mod commands;
pub mod killaura;
+pub mod mspt;
use std::{env, process, sync::Arc, thread, time::Duration};
@@ -75,6 +76,7 @@ async fn main() -> AppExit {
args,
commands: Arc::new(commands),
})
+ // .add_plugins(mspt::MsptPlugin)
.start(join_address)
.await
}
diff --git a/azalea/examples/testbot/mspt.rs b/azalea/examples/testbot/mspt.rs
new file mode 100644
index 00000000..b4931d71
--- /dev/null
+++ b/azalea/examples/testbot/mspt.rs
@@ -0,0 +1,69 @@
+//! A debugging plugin that logs the duration of ECS `Update`s every tick.
+
+use std::time::{Duration, Instant};
+
+use azalea::prelude::Resource;
+use bevy_app::Plugin;
+use bevy_ecs::{schedule::IntoScheduleConfigs, system::ResMut};
+
+pub struct MsptPlugin;
+impl Plugin for MsptPlugin {
+ fn build(&self, app: &mut bevy_app::App) {
+ app.insert_resource(MsptData {
+ last_log_time: Instant::now(),
+ this_update_start_time: Instant::now(),
+ update_times: Vec::new(),
+ })
+ .add_systems(bevy_app::PreUpdate, on_update_start)
+ .add_systems(
+ bevy_app::PostUpdate,
+ (on_update_end, log_mspt_stats).chain(),
+ );
+ }
+}
+
+#[derive(Resource)]
+struct MsptData {
+ this_update_start_time: Instant,
+ last_log_time: Instant,
+
+ update_times: Vec<Duration>,
+}
+fn log_mspt_stats(mut stats: ResMut<MsptData>) {
+ if stats.last_log_time.elapsed() < Duration::from_secs(1) {
+ return;
+ }
+ stats.last_log_time = Instant::now();
+
+ let mut fastest_update_duration = None;
+ let mut summed_update_durations = Duration::ZERO;
+ let mut num_updates = 0;
+ for update in stats.update_times.drain(..) {
+ summed_update_durations += update;
+ num_updates += 1;
+ let Some(fastest_update) = &mut fastest_update_duration else {
+ fastest_update_duration = Some(update);
+ continue;
+ };
+ if update < *fastest_update {
+ *fastest_update = update;
+ }
+ }
+
+ if num_updates > 0
+ && let Some(fastest_update_duration) = fastest_update_duration
+ {
+ let avg_update_duration = summed_update_durations / num_updates;
+
+ println!();
+ println!("Average update duration: {avg_update_duration:?}");
+ println!("Fastest update duration: {fastest_update_duration:?}");
+ }
+}
+fn on_update_start(mut data: ResMut<MsptData>) {
+ data.this_update_start_time = Instant::now();
+}
+fn on_update_end(mut data: ResMut<MsptData>) {
+ let elapsed = data.this_update_start_time.elapsed();
+ data.update_times.push(elapsed);
+}