diff options
| author | mat <27899617+mat-1@users.noreply.github.com> | 2025-12-28 04:31:29 -0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-12-28 04:31:29 -0600 |
| commit | 20c7e27250148f62bab9e7b99e4f0cd6deb82325 (patch) | |
| tree | 163b312ef4dc72c5c23be923f8ca17cdf2a7278c | |
| parent | 7ab3b8924f64f7eadb6b8928b6fae73cb06e4c2f (diff) | |
| download | azalea-drasl-20c7e27250148f62bab9e7b99e4f0cd6deb82325.tar.xz | |
Change Client::component to return a reference (#298)
* change Client::component to return a reference
* write docs
* merge main
* remove unused parking_lot feature
23 files changed, 217 insertions, 170 deletions
diff --git a/azalea-brigadier/tests/bevy_app_usage.rs b/azalea-brigadier/tests/bevy_app_usage.rs index 876666d5..e0bd8d23 100644 --- a/azalea-brigadier/tests/bevy_app_usage.rs +++ b/azalea-brigadier/tests/bevy_app_usage.rs @@ -3,7 +3,7 @@ use std::{mem, ops::Deref, sync::Arc}; use azalea_brigadier::prelude::*; use bevy_app::App; use bevy_ecs::{prelude::*, system::RunSystemOnce}; -use parking_lot::Mutex; +use parking_lot::RwLock; #[test] fn bevy_app() { @@ -89,7 +89,7 @@ impl DispatchStorage { assert_eq!(result, Ok(0)); // Query the World for the spawned entity - let mut world = source.lock(); + let mut world = source.write(); let mut query = world.query_filtered::<(), With<SpawnedEntity>>(); // Ensure only one entity was spawned @@ -109,7 +109,7 @@ impl DispatchStorage { assert_eq!(result, Ok(0)); // Query the World for spawned entities - let mut world = source.lock(); + let mut world = source.write(); let mut query = world.query_filtered::<(), With<SpawnedEntity>>(); // Ensure three additional entities were spawned @@ -131,7 +131,7 @@ impl DispatchStorage { /// /// Spawns an entity with the [`SpawnedEntity`] component. fn command_spawn_entity(context: &CommandContext<WorldAccessor>) -> i32 { - context.source.lock().spawn(SpawnedEntity); + context.source.write().spawn(SpawnedEntity); 0 } @@ -143,7 +143,7 @@ impl DispatchStorage { let num = get_integer(context, "entities").unwrap(); for _ in 0..num { - context.source.lock().spawn(SpawnedEntity); + context.source.write().spawn(SpawnedEntity); } 0 @@ -159,20 +159,20 @@ impl DispatchStorage { /// access from inside a [`CommandDispatcher`]. #[derive(Clone)] struct WorldAccessor { - world: Arc<Mutex<World>>, + world: Arc<RwLock<World>>, } impl WorldAccessor { /// Create a new empty [`WorldAccessor`]. fn empty() -> Self { Self { - world: Arc::new(Mutex::new(World::new())), + world: Arc::new(RwLock::new(World::new())), } } /// Swap the internal [`World`] with the given one. fn swap(&mut self, world: &mut World) { - mem::swap(&mut *self.lock(), world); + mem::swap(&mut *self.write(), world); } } @@ -182,7 +182,7 @@ struct SpawnedEntity; /// Implemented for convenience. impl Deref for WorldAccessor { - type Target = Arc<Mutex<World>>; + type Target = Arc<RwLock<World>>; fn deref(&self) -> &Self::Target { &self.world } diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index 4c45b537..b7d01d58 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -18,7 +18,7 @@ use bevy_ecs::{ prelude::*, schedule::{InternedScheduleLabel, LogLevel, ScheduleBuildSettings}, }; -use parking_lot::Mutex; +use parking_lot::RwLock; use tokio::{sync::oneshot, time}; use tracing::{info, warn}; @@ -118,7 +118,11 @@ impl Plugin for AzaleaPlugin { #[doc(hidden)] pub fn start_ecs_runner( app: &mut SubApp, -) -> (Arc<Mutex<World>>, impl FnOnce(), oneshot::Receiver<AppExit>) { +) -> ( + Arc<RwLock<World>>, + impl FnOnce(), + oneshot::Receiver<AppExit>, +) { // this block is based on Bevy's default runner: // https://github.com/bevyengine/bevy/blob/390877cdae7a17095a75c8f9f1b4241fe5047e83/crates/bevy_app/src/schedule_runner.rs#L77-L85 if app.plugins_state() != PluginsState::Cleaned { @@ -136,7 +140,7 @@ pub fn start_ecs_runner( // all resources should have been added by now so we can take the ecs from the // app - let ecs = Arc::new(Mutex::new(mem::take(app.world_mut()))); + let ecs = Arc::new(RwLock::new(mem::take(app.world_mut()))); let ecs_clone = ecs.clone(); let outer_schedule_label = *app.update_schedule.as_ref().unwrap(); @@ -157,7 +161,7 @@ pub fn start_ecs_runner( /// /// Exits when we receive an `AppExit` event. async fn run_schedule_loop( - ecs: Arc<Mutex<World>>, + ecs: Arc<RwLock<World>>, outer_schedule_label: InternedScheduleLabel, ) -> AppExit { let mut last_update: Option<Instant> = None; @@ -181,7 +185,7 @@ async fn run_schedule_loop( } last_update = Some(now); - let mut ecs = ecs.lock(); + let mut ecs = ecs.write(); // if last tick is None or more than 50ms ago, run the GameTick schedule ecs.run_schedule(outer_schedule_label); diff --git a/azalea/examples/testbot/commands/combat.rs b/azalea/examples/testbot/commands/combat.rs index b440b3ac..af147e96 100644 --- a/azalea/examples/testbot/commands/combat.rs +++ b/azalea/examples/testbot/commands/combat.rs @@ -2,6 +2,7 @@ use azalea::brigadier::prelude::*; use parking_lot::Mutex; use super::{CommandSource, Ctx}; +use crate::State; pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) { commands.register( @@ -9,12 +10,7 @@ pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) { let enabled = get_bool(ctx, "enabled").unwrap(); let source = ctx.source.lock(); let bot = source.bot.clone(); - { - let mut ecs = bot.ecs.lock(); - let mut entity = ecs.entity_mut(bot.entity); - let mut state = entity.get_mut::<crate::State>().unwrap(); - state.killaura = enabled - } + bot.query_self::<&mut State, _>(|mut state| state.killaura = enabled); source.reply(if enabled { "Enabled killaura" } else { diff --git a/azalea/examples/testbot/commands/debug.rs b/azalea/examples/testbot/commands/debug.rs index edadd697..711c3260 100644 --- a/azalea/examples/testbot/commands/debug.rs +++ b/azalea/examples/testbot/commands/debug.rs @@ -110,7 +110,7 @@ pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) { let hit_result = source.bot.component::<HitResultComponent>(); - match &*hit_result { + match &**hit_result { HitResult::Block(r) => { if r.miss { source.reply("I'm not looking at anything"); @@ -121,7 +121,7 @@ pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) { source.reply(format!("I'm looking at {block:?} at {block_pos:?}")); } HitResult::Entity(r) => { - let entity_kind = *source.bot.entity_component::<EntityKindComponent>(r.entity); + let entity_kind = **source.bot.entity_component::<EntityKindComponent>(r.entity); source.reply(format!( "I'm looking at {entity_kind} ({:?}) at {}", r.entity, r.location @@ -180,7 +180,7 @@ pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) { "is_path_partial: {}, path.len: {}, queued_path.len: {}", executing_path.is_path_partial, executing_path.path.len(), - if let Some(queued) = executing_path.queued_path { + if let Some(queued) = &executing_path.queued_path { queued.len().to_string() } else { "n/a".to_owned() @@ -261,9 +261,7 @@ pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) { thread::sleep(Duration::from_secs(1)); // dump the ecs - let mut ecs = ecs.lock(); - - + let mut ecs = ecs.write(); let report_path = env::temp_dir().join("azalea-ecs-leak-report.txt"); let mut report = File::create(&report_path).unwrap(); @@ -357,7 +355,12 @@ pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) { thread::spawn(move || { thread::sleep(Duration::from_secs(1)); - source.lock().bot.ecs.lock().write_message(AppExit::Success); + source + .lock() + .bot + .ecs + .write() + .write_message(AppExit::Success); }); 1 diff --git a/azalea/examples/testbot/commands/movement.rs b/azalea/examples/testbot/commands/movement.rs index a4ac787a..6f43a021 100644 --- a/azalea/examples/testbot/commands/movement.rs +++ b/azalea/examples/testbot/commands/movement.rs @@ -28,6 +28,7 @@ pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) { source.reply("I can't see you!"); return 0; }; + let position = position.clone(); source.reply("ok"); source .bot diff --git a/azalea/examples/testbot/killaura.rs b/azalea/examples/testbot/killaura.rs index 093495ea..e3d9091c 100644 --- a/azalea/examples/testbot/killaura.rs +++ b/azalea/examples/testbot/killaura.rs @@ -17,9 +17,9 @@ pub fn tick(bot: Client, state: State) -> anyhow::Result<()> { let mut nearest_entity = None; let mut nearest_distance = f64::INFINITY; let bot_position = bot.eye_position(); - let bot_instance_name = bot.component::<InstanceName>(); + let bot_instance_name = bot.component::<InstanceName>().clone(); { - let mut ecs = bot.ecs.lock(); + let mut ecs = bot.ecs.write(); let mut query = ecs .query_filtered::<(Entity, &Position, &InstanceName), ( With<AbstractMonster>, diff --git a/azalea/examples/testbot/main.rs b/azalea/examples/testbot/main.rs index aa4a7b99..9889e250 100644 --- a/azalea/examples/testbot/main.rs +++ b/azalea/examples/testbot/main.rs @@ -129,7 +129,7 @@ async fn handle(bot: Client, event: azalea::Event, state: State) -> anyhow::Resu }); if swarm.args.pathfinder_debug_particles { bot.ecs - .lock() + .write() .entity_mut(bot.entity) .insert(PathfinderDebugParticles); } diff --git a/azalea/src/bot.rs b/azalea/src/bot.rs index 73b225b2..d3527938 100644 --- a/azalea/src/bot.rs +++ b/azalea/src/bot.rs @@ -82,7 +82,7 @@ fn stop_jumping(mut query: Query<(&mut Jumping, &mut Bot)>) { impl Client { /// Queue a jump for the next tick. pub fn jump(&self) { - let mut ecs = self.ecs.lock(); + let mut ecs = self.ecs.write(); ecs.write_message(JumpEvent { entity: self.entity, }); @@ -92,7 +92,7 @@ impl Client { /// /// To look at the center of a block, you should call [`BlockPos::center`]. pub fn look_at(&self, position: Vec3) { - let mut ecs = self.ecs.lock(); + let mut ecs = self.ecs.write(); ecs.write_message(LookAtEvent { entity: self.entity, position, @@ -136,7 +136,7 @@ impl Client { let mut receiver = self.get_tick_broadcaster(); while receiver.recv().await.is_ok() { - let ecs = self.ecs.lock(); + let ecs = self.ecs.read(); if ecs.get::<Mining>(self.entity).is_none() { break; } diff --git a/azalea/src/client_impl/attack.rs b/azalea/src/client_impl/attack.rs index f4cac51f..16721c1c 100644 --- a/azalea/src/client_impl/attack.rs +++ b/azalea/src/client_impl/attack.rs @@ -12,7 +12,7 @@ impl Client { /// This doesn't automatically look at the entity or perform any /// range/visibility checks, so it might trigger anticheats. pub fn attack(&self, entity: Entity) { - self.ecs.lock().write_message(AttackEvent { + self.ecs.write().write_message(AttackEvent { entity: self.entity, target: entity, }); @@ -27,18 +27,19 @@ impl Client { // attack? whatever, just return false return false; }; - *attack_strength_scale < 1.0 + **attack_strength_scale < 1.0 } /// Returns the number of ticks until we can attack at full strength again. /// /// Also see [`Client::has_attack_cooldown`]. pub fn attack_cooldown_remaining_ticks(&self) -> usize { - let mut ecs = self.ecs.lock(); - let Ok((attributes, ticks_since_last_attack)) = ecs - .query::<(&Attributes, &TicksSinceLastAttack)>() - .get(&ecs, self.entity) - else { + let ecs = self.ecs.read(); + + let Some(attributes) = ecs.get::<Attributes>(self.entity) else { + return 0; + }; + let Some(ticks_since_last_attack) = ecs.get::<TicksSinceLastAttack>(self.entity) else { return 0; }; diff --git a/azalea/src/client_impl/chat.rs b/azalea/src/client_impl/chat.rs index 3ca98631..e71a208f 100644 --- a/azalea/src/client_impl/chat.rs +++ b/azalea/src/client_impl/chat.rs @@ -11,7 +11,7 @@ impl Client { /// the message is a command and using the proper packet for you, so you /// should use that instead. pub fn write_chat_packet(&self, message: &str) { - self.ecs.lock().write_message(SendChatKindEvent { + self.ecs.write().write_message(SendChatKindEvent { entity: self.entity, content: message.to_owned(), kind: ChatKind::Message, @@ -24,7 +24,7 @@ impl Client { /// You can also just use [`Client::chat`] and start your message with a `/` /// to send a command. pub fn write_command_packet(&self, command: &str) { - self.ecs.lock().write_message(SendChatKindEvent { + self.ecs.write().write_message(SendChatKindEvent { entity: self.entity, content: command.to_owned(), kind: ChatKind::Command, @@ -41,7 +41,7 @@ impl Client { /// # } /// ``` pub fn chat(&self, content: impl Into<String>) { - self.ecs.lock().write_message(SendChatEvent { + self.ecs.write().write_message(SendChatEvent { entity: self.entity, content: content.into(), }); diff --git a/azalea/src/client_impl/entity_query.rs b/azalea/src/client_impl/entity_query.rs index 85e46525..ae623dc7 100644 --- a/azalea/src/client_impl/entity_query.rs +++ b/azalea/src/client_impl/entity_query.rs @@ -9,41 +9,100 @@ use bevy_ecs::{ query::{QueryData, QueryEntityError, QueryFilter, QueryItem, ROQueryItem}, world::World, }; -use parking_lot::Mutex; +use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard}; use crate::Client; impl Client { - /// A convenience function for getting components from our client's entity. + /// Get a component from the client. /// - /// To query another entity, you can use [`Self::query_entity`]. + /// This allows you to access certain data stored about the client entity + /// that isn't accessible in a simpler way. + /// + /// This returns a reference to the component wrapped by a read guard. This + /// makes the component cheap to access, but means that the ECS cannot be + /// mutated while it's in scope. In some cases, it may be simpler for you to + /// immediately clone the component after accessing it. + /// + /// If the component isn't guaranteed to be present, consider using + /// [`Self::get_component`] instead. + /// + /// To do more complex queries or to mutate data, see [`Self::query_self`]. + /// + /// To access data about other entities, you can use + /// [`Self::entity_component`] (and its other related functions). + /// + /// You may also use [`Self::ecs`] directly if you need more control over + /// when the ECS is locked. + /// + /// # Panics + /// + /// This will panic if the component doesn't exist on the client. Use + /// [`Self::get_component`] to avoid this. /// /// # Examples + /// /// ``` /// # use azalea_world::InstanceName; + /// # fn example(client: &azalea::Client) { + /// let world_name = client.component::<InstanceName>(); + /// # } + pub fn component<T: Component>(&self) -> MappedRwLockReadGuard<'_, T> { + self.get_component::<T>().unwrap_or_else(|| { + panic!( + "Our client is missing a required component: {:?}", + any::type_name::<&T>() + ) + }) + } + + /// Get a component on this client, or `None` if it doesn't exist. + /// + /// If the component is guaranteed to be present, consider using + /// [`Self::component`]. Also see that function for more details. + pub fn get_component<T: Component>(&self) -> Option<MappedRwLockReadGuard<'_, T>> { + self.get_entity_component::<T>(self.entity) + } + + /// Query the ECS for data from our client entity. + /// + /// To query another entity, you can use [`Self::query_entity`]. + /// + /// You can use this to mutate data on the client. + /// + /// # Examples + /// + /// ``` + /// # use azalea_entity::Position; /// # fn example(mut client: azalea::Client) { - /// let is_logged_in = client.query_self::<Option<&InstanceName>, _>(|ins| ins.is_some()); + /// // teleport one block up + /// client.query_self::<&mut Position, _>(|mut pos| pos.y += 1.0); /// # } /// ``` /// /// # Panics /// - /// This will panic if the component doesn't exist on the client. + /// This will panic if the client is missing a component required by the + /// query. pub fn query_self<D: QueryData, R>(&self, f: impl FnOnce(QueryItem<D>) -> R) -> R { - let mut ecs = self.ecs.lock(); + let mut ecs = self.ecs.write(); let mut qs = ecs.query::<D>(); let res = qs.get_mut(&mut ecs, self.entity).unwrap_or_else(|_| { panic!( - "Our client is missing a required component {:?}", + "`Client::query_self` failed when querying for {:?}", any::type_name::<D>() ) }); f(res) } - /// A convenience function for getting components from any entity. + /// Query the ECS for data from an entity. /// - /// If you're querying the client, you should use [`Self::query_self`]. + /// Note that it is often simpler to use [`Self::entity_component`]. + /// + /// To query the client, you should use [`Self::query_self`]. + /// + /// You can also use this to mutate data on an entity. /// /// # Panics /// @@ -73,7 +132,7 @@ impl Client { entity: Entity, f: impl FnOnce(QueryItem<D>) -> R, ) -> Result<R, QueryEntityError> { - let mut ecs = self.ecs.lock(); + let mut ecs = self.ecs.write(); let mut qs = ecs.query::<D>(); qs.get_mut(&mut ecs, entity).map(f) } @@ -138,7 +197,7 @@ impl Client { /// if let Some(nearest_player) = /// bot.nearest_entity_by::<(), (With<Player>, Without<LocalEntity>)>(|_: ()| true) /// { - /// let nearest_player_pos = *bot.entity_component::<Position>(nearest_player); + /// let nearest_player_pos = **bot.entity_component::<Position>(nearest_player); /// bot.chat(format!("You are at {nearest_player_pos}")); /// } /// # } @@ -175,46 +234,61 @@ impl Client { let Some(position) = self.get_component::<Position>() else { return vec![]; }; + let (instance_name, position) = (instance_name.clone(), *position); predicate.find_all_sorted(self.ecs.clone(), &instance_name, (&position).into()) } /// Get a component from an entity. /// - /// Note that this will return an owned type (i.e. not a reference) so it - /// may be expensive for larger types. + /// This allows you to access data stored about entities that isn't + /// accessible in a simpler way. + /// + /// This returns a reference to the component wrapped by a read guard. This + /// makes the component cheap to access, but means that the ECS cannot be + /// mutated while it's in scope. In some cases, it may be simpler for you to + /// immediately clone the component after accessing it. + /// + /// If you're trying to get a component for this client, you should use + /// [`Self::component`] instead. + /// + /// To do more complex queries or to mutate data, see + /// [`Self::query_entity`]. /// - /// If you're trying to get a component for this client, use - /// [`Self::component`]. - pub fn entity_component<Q: Component + Clone>(&self, entity: Entity) -> Q { - let mut ecs = self.ecs.lock(); - let mut q = ecs.query::<&Q>(); - let components = q.get(&ecs, entity).unwrap_or_else(|_| { + /// # Panics + /// + /// This will panic if the component doesn't exist on the entity. Use + /// [`Self::get_entity_component`] to avoid this. + pub fn entity_component<T: Component>(&self, entity: Entity) -> MappedRwLockReadGuard<'_, T> { + self.get_entity_component::<T>(entity).unwrap_or_else(|| { panic!( - "Entity is missing a required component {:?}", - any::type_name::<Q>() + "Entity {entity} is missing a required component: {:?}", + any::type_name::<&T>() ) - }); - components.clone() + }) } /// Get a component from an entity, if it exists. /// /// This is similar to [`Self::entity_component`] but returns an `Option` /// instead of panicking if the component isn't present. - pub fn get_entity_component<Q: Component + Clone>(&self, entity: Entity) -> Option<Q> { - let mut ecs = self.ecs.lock(); - let mut q = ecs.query::<&Q>(); - let components = q.get(&ecs, entity).ok(); - components.cloned() + pub fn get_entity_component<T: Component>( + &self, + entity: Entity, + ) -> Option<MappedRwLockReadGuard<'_, T>> { + let ecs = self.ecs.read(); + RwLockReadGuard::try_map(ecs, |ecs: &World| ecs.get(entity)).ok() } } pub trait EntityPredicate<Q: QueryData, Filter: QueryFilter> { - fn find_any(&self, ecs_lock: Arc<Mutex<World>>, instance_name: &InstanceName) - -> Option<Entity>; + fn find_any( + &self, + ecs_lock: Arc<RwLock<World>>, + instance_name: &InstanceName, + ) -> Option<Entity>; fn find_all_sorted( &self, - ecs_lock: Arc<Mutex<World>>, + ecs_lock: Arc<RwLock<World>>, instance_name: &InstanceName, nearest_to: Vec3, ) -> Vec<Entity>; @@ -226,10 +300,10 @@ where { fn find_any( &self, - ecs_lock: Arc<Mutex<World>>, + ecs_lock: Arc<RwLock<World>>, instance_name: &InstanceName, ) -> Option<Entity> { - let mut ecs = ecs_lock.lock(); + let mut ecs = ecs_lock.write(); let mut query = ecs.query_filtered::<(Entity, &InstanceName, Q), Filter>(); query .iter(&ecs) @@ -239,11 +313,11 @@ where fn find_all_sorted( &self, - ecs_lock: Arc<Mutex<World>>, + ecs_lock: Arc<RwLock<World>>, instance_name: &InstanceName, nearest_to: Vec3, ) -> Vec<Entity> { - let mut ecs = ecs_lock.lock(); + let mut ecs = ecs_lock.write(); let mut query = ecs.query_filtered::<(Entity, &InstanceName, &Position, Q), Filter>(); let mut entities = query .iter(&ecs) diff --git a/azalea/src/client_impl/interact.rs b/azalea/src/client_impl/interact.rs index 6ff93549..8a9cf475 100644 --- a/azalea/src/client_impl/interact.rs +++ b/azalea/src/client_impl/interact.rs @@ -15,7 +15,7 @@ impl Client { /// Note that this may trigger anticheats as it doesn't take into account /// whether you're actually looking at the block. pub fn block_interact(&self, position: BlockPos) { - self.ecs.lock().write_message(StartUseItemEvent { + self.ecs.write().write_message(StartUseItemEvent { entity: self.entity, hand: InteractionHand::MainHand, force_block: Some(position), @@ -28,7 +28,7 @@ impl Client { /// behavior isn't desired, consider using [`Client::start_use_item`] /// instead. pub fn entity_interact(&self, entity: Entity) { - self.ecs.lock().trigger(EntityInteractEvent { + self.ecs.write().trigger(EntityInteractEvent { client: self.entity, target: entity, location: None, @@ -43,7 +43,7 @@ impl Client { /// If we're looking at a block or entity, then it will be clicked. Also see /// [`Client::block_interact`] and [`Client::entity_interact`]. pub fn start_use_item(&self) { - self.ecs.lock().write_message(StartUseItemEvent { + self.ecs.write().write_message(StartUseItemEvent { entity: self.entity, hand: InteractionHand::MainHand, force_block: None, diff --git a/azalea/src/client_impl/inventory.rs b/azalea/src/client_impl/inventory.rs index 0ea11477..fc1e2e73 100644 --- a/azalea/src/client_impl/inventory.rs +++ b/azalea/src/client_impl/inventory.rs @@ -36,7 +36,7 @@ impl Client { "Hotbar slot index must be in the range 0..=8" ); - let mut ecs = self.ecs.lock(); + let mut ecs = self.ecs.write(); ecs.trigger(SetSelectedHotbarSlotEvent { entity: self.entity, slot: new_hotbar_slot_index, diff --git a/azalea/src/client_impl/mining.rs b/azalea/src/client_impl/mining.rs index 14794765..980fc47f 100644 --- a/azalea/src/client_impl/mining.rs +++ b/azalea/src/client_impl/mining.rs @@ -5,7 +5,7 @@ use crate::Client; impl Client { pub fn start_mining(&self, position: BlockPos) { - let mut ecs = self.ecs.lock(); + let mut ecs = self.ecs.write(); ecs.write_message(StartMiningBlockEvent { entity: self.entity, @@ -17,7 +17,7 @@ impl Client { /// When enabled, the bot will mine any block that it is looking at if it is /// reachable. pub fn left_click_mine(&self, enabled: bool) { - let mut ecs = self.ecs.lock(); + let mut ecs = self.ecs.write(); let mut entity_mut = ecs.entity_mut(self.entity); if enabled { diff --git a/azalea/src/client_impl/mod.rs b/azalea/src/client_impl/mod.rs index 36995656..bbbe23e0 100644 --- a/azalea/src/client_impl/mod.rs +++ b/azalea/src/client_impl/mod.rs @@ -31,12 +31,11 @@ use azalea_registry::{DataRegistryKeyRef, identifier::Identifier}; use azalea_world::{Instance, InstanceName, MinecraftEntityId, PartialInstance}; use bevy_app::App; use bevy_ecs::{ - component::Component, entity::Entity, resource::Resource, world::{Mut, World}, }; -use parking_lot::{Mutex, RwLock}; +use parking_lot::{MappedRwLockReadGuard, RwLock}; use tokio::sync::mpsc; use uuid::Uuid; @@ -70,11 +69,15 @@ pub struct Client { /// You probably don't need to access this directly. Note that if you're /// using a shared world (i.e. a swarm), the ECS will contain all entities /// in all instances/dimensions. - pub ecs: Arc<Mutex<World>>, + /// + /// You can nearly always use [`Self::component`], [`Self::query_self`], + /// [`Self::query_entity`], or another one of those related functions to + /// access the ECS instead. + pub ecs: Arc<RwLock<World>>, } pub struct StartClientOpts { - pub ecs_lock: Arc<Mutex<World>>, + pub ecs_lock: Arc<RwLock<World>>, pub account: Account, pub connect_opts: ConnectOpts, pub event_sender: Option<mpsc::UnboundedSender<Event>>, @@ -141,7 +144,7 @@ impl Client { /// World, and schedule runner function. /// You should only use this if you want to change these fields from the /// defaults, otherwise use [`Client::join`]. - pub fn new(entity: Entity, ecs: Arc<Mutex<World>>) -> Self { + pub fn new(entity: Entity, ecs: Arc<RwLock<World>>) -> Self { Self { // default our id to 0, it'll be set later entity, @@ -209,7 +212,7 @@ impl Client { let (start_join_callback_tx, mut start_join_callback_rx) = mpsc::unbounded_channel::<Entity>(); - ecs_lock.lock().write_message(StartJoinServerEvent { + ecs_lock.write().write_message(StartJoinServerEvent { account, connect_opts, start_join_callback_tx: Some(start_join_callback_tx), @@ -221,7 +224,7 @@ impl Client { if let Some(event_sender) = event_sender { ecs_lock - .lock() + .write() .entity_mut(entity) .insert(LocalPlayerEvents(event_sender)); } @@ -233,7 +236,7 @@ impl Client { pub fn write_packet(&self, packet: impl Packet<ServerboundGamePacket>) { let packet = packet.into_variant(); self.ecs - .lock() + .write() .commands() .trigger(SendGamePacketEvent::new(self.entity, packet)); } @@ -243,7 +246,7 @@ impl Client { /// The OwnedReadHalf for the TCP connection is in one of the tasks, so it /// automatically closes the connection when that's dropped. pub fn disconnect(&self) { - self.ecs.lock().write_message(DisconnectEvent { + self.ecs.write().write_message(DisconnectEvent { entity: self.entity, reason: None, }); @@ -256,58 +259,21 @@ impl Client { self.query_self::<&mut RawConnection, _>(f) } - /// Get a component from this client. This will clone the component and - /// return it. - /// - /// - /// If the component can't be cloned, try [`Self::query_self`] instead. - /// If it isn't guaranteed to be present, you can use - /// [`Self::get_component`] or [`Self::query_self`]. - /// - /// - /// You may also use [`Self::ecs`] directly if you need more control over - /// when the ECS is locked. - /// - /// # Panics - /// - /// This will panic if the component doesn't exist on the client. - /// - /// # Examples - /// - /// ``` - /// # use azalea_world::InstanceName; - /// # fn example(client: &azalea::Client) { - /// let world_name = client.component::<InstanceName>(); - /// # } - pub fn component<T: Component + Clone>(&self) -> T { - self.query_self::<&T, _>(|t| t.clone()) - } - - /// Get a component from this client, or `None` if it doesn't exist. - /// - /// If the component can't be cloned, consider using [`Self::query_self`] - /// with `Option<&T>` instead. - /// - /// You may also have to use [`Self::query_self`] directly. - pub fn get_component<T: Component + Clone>(&self) -> Option<T> { - self.query_self::<Option<&T>, _>(|t| t.cloned()) - } - /// Get a resource from the ECS. This will clone the resource and return it. pub fn resource<T: Resource + Clone>(&self) -> T { - self.ecs.lock().resource::<T>().clone() + self.ecs.read().resource::<T>().clone() } /// Get a required ECS resource and call the given function with it. pub fn map_resource<T: Resource, R>(&self, f: impl FnOnce(&T) -> R) -> R { - let ecs = self.ecs.lock(); + let ecs = self.ecs.read(); let value = ecs.resource::<T>(); f(value) } /// Get an optional ECS resource and call the given function with it. pub fn map_get_resource<T: Resource, R>(&self, f: impl FnOnce(Option<&T>) -> R) -> R { - let ecs = self.ecs.lock(); + let ecs = self.ecs.read(); let value = ecs.get_resource::<T>(); f(value) } @@ -354,7 +320,7 @@ impl Client { /// later. pub fn position(&self) -> Vec3 { Vec3::from( - &self + &*self .get_component::<Position>() .expect("the client's position hasn't been initialized yet"), ) @@ -366,7 +332,7 @@ impl Client { /// This is a shortcut for /// `self.component::<EntityDimensions>()`. pub fn dimensions(&self) -> EntityDimensions { - self.component::<EntityDimensions>() + self.component::<EntityDimensions>().clone() } /// Get the position of this client's eyes. @@ -383,7 +349,7 @@ impl Client { /// /// This is a shortcut for `*bot.component::<Health>()`. pub fn health(&self) -> f32 { - *self.component::<Health>() + **self.component::<Health>() } /// Get the hunger level of this client, which includes both food and @@ -413,7 +379,7 @@ impl Client { /// /// This is a shortcut for `*bot.component::<TabList>()`. pub fn tab_list(&self) -> HashMap<Uuid, PlayerInfo> { - (*self.component::<TabList>()).clone() + (**self.component::<TabList>()).clone() } /// Returns the [`GameProfile`] for our client. This contains your username, @@ -426,12 +392,12 @@ impl Client { /// /// This as also available from the ECS as [`GameProfileComponent`]. pub fn profile(&self) -> GameProfile { - (*self.component::<GameProfileComponent>()).clone() + (**self.component::<GameProfileComponent>()).clone() } /// Returns the attribute values of our player, which can be used to /// determine things like our movement speed. - pub fn attributes(&self) -> Attributes { + pub fn attributes(&self) -> MappedRwLockReadGuard<'_, Attributes> { self.component::<Attributes>() } diff --git a/azalea/src/client_impl/movement.rs b/azalea/src/client_impl/movement.rs index b47da9a7..a708e5f6 100644 --- a/azalea/src/client_impl/movement.rs +++ b/azalea/src/client_impl/movement.rs @@ -19,7 +19,7 @@ impl Client { /// Returns whether the player will try to jump next tick. pub fn jumping(&self) -> bool { - *self.component::<Jumping>() + **self.component::<Jumping>() } pub fn set_crouching(&self, crouching: bool) { @@ -49,7 +49,7 @@ impl Client { /// /// See [`Self::set_direction`] for more details. pub fn direction(&self) -> (f32, f32) { - let look_direction: LookDirection = self.component::<LookDirection>(); + let look_direction = *self.component::<LookDirection>(); (look_direction.y_rot(), look_direction.x_rot()) } @@ -71,7 +71,7 @@ impl Client { /// # } /// ``` pub fn walk(&self, direction: WalkDirection) { - let mut ecs = self.ecs.lock(); + let mut ecs = self.ecs.write(); ecs.write_message(StartWalkEvent { entity: self.entity, direction, @@ -95,7 +95,7 @@ impl Client { /// # } /// ``` pub fn sprint(&self, direction: SprintDirection) { - let mut ecs = self.ecs.lock(); + let mut ecs = self.ecs.write(); ecs.write_message(StartSprintEvent { entity: self.entity, direction, diff --git a/azalea/src/container.rs b/azalea/src/container.rs index 3d5f6f01..8b46994e 100644 --- a/azalea/src/container.rs +++ b/azalea/src/container.rs @@ -81,7 +81,7 @@ impl Client { } self.ecs - .lock() + .write() .entity_mut(self.entity) .insert(WaitingForInventoryOpen); self.block_interact(pos); @@ -102,7 +102,7 @@ impl Client { let mut ticks = self.get_tick_broadcaster(); let mut elapsed_ticks = 0; while ticks.recv().await.is_ok() { - let ecs = self.ecs.lock(); + let ecs = self.ecs.read(); if ecs.get::<WaitingForInventoryOpen>(self.entity).is_none() { break; } @@ -115,7 +115,7 @@ impl Client { } } - let ecs = self.ecs.lock(); + let ecs = self.ecs.read(); let inventory = ecs.get::<Inventory>(self.entity).expect("no inventory"); if inventory.id == 0 { None @@ -136,8 +136,7 @@ impl Client { /// sending any packets, use [`Client::menu`], [`Menu::player_slots_range`], /// and [`Menu::slots`]. pub fn open_inventory(&self) -> Option<ContainerHandle> { - let ecs = self.ecs.lock(); - let inventory = ecs.get::<Inventory>(self.entity).expect("no inventory"); + let inventory = self.component::<Inventory>(); if inventory.id == 0 { Some(ContainerHandle::new(0, self.clone())) } else { @@ -188,7 +187,7 @@ impl ContainerHandleRef { } pub fn close(&self) { - self.client.ecs.lock().trigger(CloseContainerEvent { + self.client.ecs.write().trigger(CloseContainerEvent { entity: self.client.entity, id: self.id, }); @@ -285,7 +284,7 @@ impl ContainerHandleRef { /// action. pub fn click(&self, operation: impl Into<ClickOperation>) { let operation = operation.into(); - self.client.ecs.lock().trigger(ContainerClickEvent { + self.client.ecs.write().trigger(ContainerClickEvent { entity: self.client.entity, window_id: self.id, operation, diff --git a/azalea/src/pathfinder/debug.rs b/azalea/src/pathfinder/debug.rs index f8dc1193..25f82946 100644 --- a/azalea/src/pathfinder/debug.rs +++ b/azalea/src/pathfinder/debug.rs @@ -22,7 +22,7 @@ use super::ExecutingPath; /// match event { /// azalea::Event::Init => { /// bot.ecs -/// .lock() +/// .write() /// .entity_mut(bot.entity) /// .insert(PathfinderDebugParticles); /// } diff --git a/azalea/src/pathfinder/mod.rs b/azalea/src/pathfinder/mod.rs index 155261cc..8a6e5ef2 100644 --- a/azalea/src/pathfinder/mod.rs +++ b/azalea/src/pathfinder/mod.rs @@ -242,17 +242,17 @@ impl PathfinderClientExt for Client { } fn start_goto_with_opts(&self, goal: impl Goal + 'static, opts: PathfinderOpts) { self.ecs - .lock() + .write() .write_message(GotoEvent::new(self.entity, goal, opts)); } fn stop_pathfinding(&self) { - self.ecs.lock().write_message(StopPathfindingEvent { + self.ecs.write().write_message(StopPathfindingEvent { entity: self.entity, force: false, }); } fn force_stop_pathfinding(&self) { - self.ecs.lock().write_message(StopPathfindingEvent { + self.ecs.write().write_message(StopPathfindingEvent { entity: self.entity, force: true, }); diff --git a/azalea/src/pathfinder/tests.rs b/azalea/src/pathfinder/tests.rs index 6f4f7cb7..0297e425 100644 --- a/azalea/src/pathfinder/tests.rs +++ b/azalea/src/pathfinder/tests.rs @@ -94,7 +94,7 @@ pub fn wait_until_bot_starts_moving(simulation: &mut Simulation) { let start_time = Instant::now(); while simulation.position() == start_pos && !simulation.is_mining() - && start_time.elapsed() < Duration::from_millis(500) + && start_time.elapsed() < Duration::from_millis(1000) { simulation.tick(); thread::yield_now(); diff --git a/azalea/src/swarm/builder.rs b/azalea/src/swarm/builder.rs index 860e47be..4312a485 100644 --- a/azalea/src/swarm/builder.rs +++ b/azalea/src/swarm/builder.rs @@ -448,7 +448,7 @@ where let (ecs_lock, start_running_systems, appexit_rx) = start_ecs_runner(&mut self.app); let swarm = Swarm { - ecs_lock: ecs_lock.clone(), + ecs: ecs_lock.clone(), address: Arc::new(RwLock::new(address)), instance_container, @@ -460,7 +460,7 @@ where // run the main schedule so the startup systems run { - let mut ecs = ecs_lock.lock(); + let mut ecs = ecs_lock.write(); ecs.insert_resource(swarm.clone()); ecs.insert_resource(self.swarm_state.clone()); if let Some(reconnect_after) = self.reconnect_after { @@ -545,7 +545,7 @@ where if let Some(handler) = &self.handler { let ecs_mutex = first_bot.ecs.clone(); - let mut ecs = ecs_mutex.lock(); + let mut ecs = ecs_mutex.write(); let mut query = ecs.query::<Option<&S>>(); let Ok(Some(first_bot_state)) = query.get(&ecs, first_bot.entity) else { error!( diff --git a/azalea/src/swarm/mod.rs b/azalea/src/swarm/mod.rs index 48ac20a3..90fcf4dd 100644 --- a/azalea/src/swarm/mod.rs +++ b/azalea/src/swarm/mod.rs @@ -20,7 +20,7 @@ use bevy_app::{PluginGroup, PluginGroupBuilder}; use bevy_ecs::prelude::*; pub use builder::SwarmBuilder; use futures::future::BoxFuture; -use parking_lot::{Mutex, RwLock}; +use parking_lot::RwLock; use tokio::{sync::mpsc, task}; use tracing::{debug, error, warn}; @@ -37,7 +37,12 @@ use crate::{Client, JoinOpts, client_impl::StartClientOpts}; /// removed with [`Client::disconnect`]. #[derive(Clone, Resource)] pub struct Swarm { - pub ecs_lock: Arc<Mutex<World>>, + /// A way to directly access the ECS. + /// + /// This will not work if called within a system, as the ECS is already + /// locked. + #[doc(alias = "ecs_lock")] // former type name + pub ecs: Arc<RwLock<World>>, // the address is public and mutable so plugins can change it pub address: Arc<RwLock<ResolvedAddr>>, @@ -176,7 +181,7 @@ impl Swarm { let (tx, rx) = mpsc::unbounded_channel(); let client = Client::start_client(StartClientOpts { - ecs_lock: self.ecs_lock.clone(), + ecs_lock: self.ecs.clone(), account: account.clone(), connect_opts: ConnectOpts { address, @@ -188,7 +193,7 @@ impl Swarm { .await; // add the state to the client { - let mut ecs = self.ecs_lock.lock(); + let mut ecs = self.ecs.write(); ecs.entity_mut(client.entity).insert(state); } @@ -251,9 +256,7 @@ impl Swarm { "Sending SwarmEvent::Disconnect due to receiving an Event::Disconnect from client {}", bot.entity ); - let account = bot - .get_component::<Account>() - .expect("bot is missing required Account component"); + let account = bot.component::<Account>().clone(); swarm_tx .send(SwarmEvent::Disconnect( Box::new(account), @@ -284,7 +287,7 @@ impl Swarm { /// /// [`LocalEntity`]: azalea_entity::LocalEntity pub fn client_entities(&self) -> Box<[Entity]> { - let mut ecs = self.ecs_lock.lock(); + let mut ecs = self.ecs.write(); let mut query = ecs.query_filtered::<Entity, With<LocalEntity>>(); query.iter(&ecs).collect::<Box<[Entity]>>() } @@ -312,7 +315,7 @@ impl IntoIterator for Swarm { client_entities .into_iter() - .map(|entity| Client::new(entity, self.ecs_lock.clone())) + .map(|entity| Client::new(entity, self.ecs.clone())) .collect::<Box<[Client]>>() .into_iter() } diff --git a/azalea/src/tick_broadcast.rs b/azalea/src/tick_broadcast.rs index 479466e2..a082e87e 100644 --- a/azalea/src/tick_broadcast.rs +++ b/azalea/src/tick_broadcast.rs @@ -65,7 +65,7 @@ impl Client { /// # async fn example(bot: &mut azalea::Client) { /// let mut ticks = bot.get_tick_broadcaster(); /// while ticks.recv().await.is_ok() { - /// let ecs = bot.ecs.lock(); + /// let ecs = bot.ecs.read(); /// if ecs.get::<WaitingForInventoryOpen>(bot.entity).is_none() { /// break; /// } @@ -73,7 +73,7 @@ impl Client { /// # } /// ``` pub fn get_tick_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()> { - let ecs = self.ecs.lock(); + let ecs = self.ecs.read(); let tick_broadcast = ecs.resource::<TickBroadcast>(); tick_broadcast.subscribe() } @@ -86,7 +86,7 @@ impl Client { /// This is useful if you're sending an ECS event and want to make sure it's /// been handled before continuing. pub fn get_update_broadcaster(&self) -> tokio::sync::broadcast::Receiver<()> { - let ecs = self.ecs.lock(); + let ecs = self.ecs.read(); let update_broadcast = ecs.resource::<UpdateBroadcast>(); update_broadcast.subscribe() } |
