diff options
| author | mat <27899617+mat-1@users.noreply.github.com> | 2025-12-28 21:54:12 -0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-12-28 21:54:12 -0600 |
| commit | 39488a6585ce969af93f43ece1ffb1174dc95e1d (patch) | |
| tree | 49b63b2321b974a7c6425e53b8602a0b4500f092 | |
| parent | 25e441944412038da2be4e64854e59169d58305b (diff) | |
| download | azalea-drasl-39488a6585ce969af93f43ece1ffb1174dc95e1d.tar.xz | |
Implement `EntityRef` (#299)
* start implementing EntityRef struct
* use EntityRef and impl more functions for it
* fix doctests
* typo
* slightly reword some docs
* update changelog
| -rw-r--r-- | CHANGELOG.md | 2 | ||||
| -rw-r--r-- | azalea/examples/testbot/commands.rs | 5 | ||||
| -rw-r--r-- | azalea/examples/testbot/commands/debug.rs | 12 | ||||
| -rw-r--r-- | azalea/examples/testbot/commands/movement.rs | 20 | ||||
| -rw-r--r-- | azalea/examples/testbot/killaura.rs | 2 | ||||
| -rw-r--r-- | azalea/examples/todo/README.md | 2 | ||||
| -rw-r--r-- | azalea/examples/todo/pvp.rs | 2 | ||||
| -rw-r--r-- | azalea/src/client_impl/entity_query.rs | 106 | ||||
| -rw-r--r-- | azalea/src/client_impl/mod.rs | 120 | ||||
| -rw-r--r-- | azalea/src/entity_ref/mod.rs | 123 | ||||
| -rw-r--r-- | azalea/src/entity_ref/shared_impls.rs | 178 | ||||
| -rw-r--r-- | azalea/src/lib.rs | 3 |
12 files changed, 420 insertions, 155 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index fb9b8f21..41f463b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,12 +11,14 @@ is breaking anyways, semantic versioning is not followed. ### Added - Re-implement `Client::map_component` and `map_get_component`. +- Add an `EntityRef` type to simplify interactions with entities. ### Changed - Move the `Client` struct out of `azalea-client` into `azalea`. - `Client::ecs` is now an `RwLock` instead of a `Mutex`. - `Client::component` and `entity_component` now return a mapped RwLock guard instead of cloning the component. +- Most functions on `Client` that previously returned `Entity` now return `EntityRef` instead. ### Fixed diff --git a/azalea/examples/testbot/commands.rs b/azalea/examples/testbot/commands.rs index 9d9d9b8a..beb87510 100644 --- a/azalea/examples/testbot/commands.rs +++ b/azalea/examples/testbot/commands.rs @@ -3,9 +3,10 @@ pub mod debug; pub mod movement; use azalea::{ - Client, brigadier::prelude::*, chat::ChatPacket, ecs::prelude::*, entity::metadata::Player, + Client, brigadier::prelude::*, chat::ChatPacket, entity::metadata::Player, player::GameProfileComponent, }; +use bevy_ecs::query::With; use parking_lot::Mutex; use crate::State; @@ -29,7 +30,7 @@ impl CommandSource { } } - pub fn entity(&mut self) -> Option<Entity> { + pub fn entity(&mut self) -> Option<azalea::EntityRef> { let username = self.chat.sender()?; self.bot .any_entity_by::<&GameProfileComponent, With<Player>>( diff --git a/azalea/examples/testbot/commands/debug.rs b/azalea/examples/testbot/commands/debug.rs index f11ced45..36c699a4 100644 --- a/azalea/examples/testbot/commands/debug.rs +++ b/azalea/examples/testbot/commands/debug.rs @@ -6,13 +6,11 @@ use azalea::{ BlockPos, brigadier::prelude::*, chunks::ReceiveChunkEvent, - entity::Position, packet::game, pathfinder::{ExecutingPath, Pathfinder}, - world::MinecraftEntityId, }; use azalea_core::hit_result::HitResult; -use azalea_entity::{EntityKindComponent, EntityUuid, metadata}; +use azalea_entity::{EntityKindComponent, metadata}; use azalea_inventory::components::MaxStackSize; use azalea_world::InstanceContainer; use bevy_app::AppExit; @@ -40,7 +38,7 @@ pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) { source.reply("You aren't in render distance!"); return 0; }; - let position = source.bot.entity_component::<Position>(entity); + let position = entity.position(); source.reply(format!( "You are at {}, {}, {}", position.x, position.y, position.z @@ -54,7 +52,7 @@ pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) { source.reply("You aren't in render distance!"); return 0; }; - let entity_id = source.bot.entity_component::<MinecraftEntityId>(entity); + let entity_id = entity.minecraft_id(); source.reply(format!( "Your Minecraft ID is {} and your ECS ID is {entity:?}", *entity_id @@ -219,10 +217,10 @@ pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) { .nearest_entities_by::<(), With<metadata::Player>>(|_: ()| true); let tab_list = source.bot.tab_list(); for player_entity in player_entities { - let uuid = source.bot.entity_component::<EntityUuid>(player_entity); + let uuid = player_entity.uuid(); source.reply(format!( "{} - {} ({:?})", - player_entity, + player_entity.id(), tab_list.get(&uuid).map_or("?", |p| p.profile.name.as_str()), uuid )); diff --git a/azalea/examples/testbot/commands/movement.rs b/azalea/examples/testbot/commands/movement.rs index 6f43a021..c1af4143 100644 --- a/azalea/examples/testbot/commands/movement.rs +++ b/azalea/examples/testbot/commands/movement.rs @@ -3,11 +3,9 @@ use std::time::Duration; use azalea::{ BlockPos, SprintDirection, WalkDirection, brigadier::prelude::*, - entity::Position, pathfinder::goals::{BlockPosGoal, RadiusGoal, XZGoal}, prelude::*, }; -use azalea_entity::dimensions::EntityDimensions; use parking_lot::Mutex; use super::{CommandSource, Ctx}; @@ -24,11 +22,7 @@ pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) { source.reply("I can't see you!"); return 0; }; - let Some(position) = source.bot.get_entity_component::<Position>(entity) else { - source.reply("I can't see you!"); - return 0; - }; - let position = position.clone(); + let position = entity.position(); source.reply("ok"); source .bot @@ -99,16 +93,8 @@ pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) { source.reply("I can't see you!"); return 0; }; - let Some(position) = source.bot.get_entity_component::<Position>(entity) else { - source.reply("I can't see you!"); - return 0; - }; - let eye_height = source - .bot - .get_entity_component::<EntityDimensions>(entity) - .map(|h| h.eye_height) - .unwrap_or_default(); - source.bot.look_at(position.up(eye_height as f64)); + let eye_position = entity.eye_position(); + source.bot.look_at(eye_position); 1 }) .then(argument("x", integer()).then(argument("y", integer()).then( diff --git a/azalea/examples/testbot/killaura.rs b/azalea/examples/testbot/killaura.rs index e6eb40ba..4f29a0f2 100644 --- a/azalea/examples/testbot/killaura.rs +++ b/azalea/examples/testbot/killaura.rs @@ -26,7 +26,7 @@ pub fn tick(bot: Client, state: State) -> anyhow::Result<()> { if let Some(nearest_entity) = nearest_entity { println!("attacking {nearest_entity:?}"); - bot.attack(nearest_entity); + nearest_entity.attack(); } Ok(()) diff --git a/azalea/examples/todo/README.md b/azalea/examples/todo/README.md index ab31cf22..9655617e 100644 --- a/azalea/examples/todo/README.md +++ b/azalea/examples/todo/README.md @@ -1 +1 @@ -These examples don't work yet and were only written to help design APIs. They will work in the future (probably with minor changes). +These examples don't work yet and were only written to help design APIs. They will work in the future (with some changes). diff --git a/azalea/examples/todo/pvp.rs b/azalea/examples/todo/pvp.rs index 3c86778f..8da55f3a 100644 --- a/azalea/examples/todo/pvp.rs +++ b/azalea/examples/todo/pvp.rs @@ -47,7 +47,7 @@ async fn swarm_handle(swarm: Swarm, event: SwarmEvent, state: SwarmState) -> any for (bot, bot_state) in swarm { bot.tick_goto_goal(pathfinder::Goals::Reach(target_bounding_box)); // if target.bounding_box.distance(bot.eyes) < bot.reach_distance() { - if azalea::entities::can_reach(bot.entity(), target_bounding_box) { + if bot.can_reach(target_bounding_box) { bot.swing(); } if !bot.using_held_item() && bot.hunger() <= 17 { diff --git a/azalea/src/client_impl/entity_query.rs b/azalea/src/client_impl/entity_query.rs index 683d755e..8de48478 100644 --- a/azalea/src/client_impl/entity_query.rs +++ b/azalea/src/client_impl/entity_query.rs @@ -11,7 +11,7 @@ use bevy_ecs::{ }; use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard}; -use crate::Client; +use crate::{Client, entity_ref::EntityRef}; impl Client { /// Get a component from the client. @@ -117,7 +117,7 @@ impl Client { ) -> R { self.try_query_entity(entity, f).unwrap_or_else(|_| { panic!( - "Entity is missing a required component {:?}", + "Querying entity {entity} failed when getting {:?}", any::type_name::<D>() ) }) @@ -138,40 +138,44 @@ impl Client { qs.get_mut(&mut ecs, entity).map(f) } + /// Quickly returns an [`EntityRef`] for an arbitrary entity that + /// matches the given predicate function that is in the same + /// [`Instance`] as the client. + /// + /// [`Instance`]: azalea_world::Instance + pub fn any_entity_by<Q: QueryData, F: QueryFilter>( + &self, + predicate: impl EntityPredicate<Q, F>, + ) -> Option<EntityRef> { + self.any_entity_id_by(predicate) + .map(|e| self.entity_ref_for(e)) + } /// Quickly returns a lightweight [`Entity`] for an arbitrary entity that /// matches the given predicate function that is in the same /// [`Instance`] as the client. /// - /// You can then use [`Self::entity_component`] to get components from this - /// entity. + /// To get an [`EntityRef`], consider using [`Self::any_entity_by`] + /// instead. /// /// If you want to find the nearest entity, consider using - /// [`Self::nearest_entity_by`] instead. If you want to find all entities - /// that match the predicate, use [`Self::nearest_entities_by`]. + /// [`Self::nearest_entity_id_by`] instead. If you want to find all entities + /// that match the predicate, use [`Self::nearest_entity_ids_by`]. /// /// # Example + /// /// ``` - /// use azalea::{ - /// Client, - /// entity::{Position, metadata::Player}, - /// player::GameProfileComponent, - /// }; + /// use azalea::{entity::metadata::Player, player::GameProfileComponent}; /// use bevy_ecs::query::With; /// - /// # fn example(mut bot: Client, sender_name: String) { - /// let entity = bot.any_entity_by::<&GameProfileComponent, With<Player>>( + /// # fn example(mut bot: azalea::Client, sender_name: String) { + /// let entity = bot.any_entity_id_by::<&GameProfileComponent, With<Player>>( /// |profile: &GameProfileComponent| profile.name == sender_name, /// ); - /// if let Some(entity) = entity { - /// let position = bot.entity_component::<Position>(entity); - /// // ... - /// } /// # } /// ``` /// - /// [`Entity`]: bevy_ecs::entity::Entity /// [`Instance`]: azalea_world::Instance - pub fn any_entity_by<Q: QueryData, F: QueryFilter>( + pub fn any_entity_id_by<Q: QueryData, F: QueryFilter>( &self, predicate: impl EntityPredicate<Q, F>, ) -> Option<Entity> { @@ -179,43 +183,59 @@ impl Client { predicate.find_any(self.ecs.clone(), &instance_name) } - /// Return a lightweight [`Entity`] for the nearest entity that matches the + /// Return an [`EntityRef`] for the nearest entity that matches the /// given predicate function. /// - /// You can then use [`Self::entity_component`] to get components from this - /// entity. - /// /// If you don't need the entity to be the nearest one, it may be more /// efficient to use [`Self::any_entity_by`] instead. You can also use /// [`Self::nearest_entities_by`] to get all nearby entities. /// - /// ``` - /// use azalea_entity::{LocalEntity, Position, metadata::Player}; - /// use bevy_ecs::query::{With, Without}; + /// Also see [`Self::nearest_entity_id_by`] if you only need the lightweight + /// [`Entity`] identifier. + pub fn nearest_entity_by<Q: QueryData, F: QueryFilter>( + &self, + predicate: impl EntityPredicate<Q, F>, + ) -> Option<EntityRef> { + self.nearest_entity_id_by(predicate) + .map(|e| self.entity_ref_for(e)) + } + /// Return a lightweight [`Entity`] for the nearest entity that matches the + /// given predicate function. /// - /// # fn example(mut bot: azalea::Client, sender_name: String) { - /// // get the position of the nearest player - /// if let Some(nearest_player) = - /// bot.nearest_entity_by::<(), (With<Player>, Without<LocalEntity>)>(|_: ()| true) - /// { - /// let nearest_player_pos = **bot.entity_component::<Position>(nearest_player); - /// bot.chat(format!("You are at {nearest_player_pos}")); - /// } - /// # } - /// ``` + /// To get an [`EntityRef`], consider using [`Self::nearest_entity_by`] + /// instead. /// - /// [`Entity`]: bevy_ecs::entity::Entity - pub fn nearest_entity_by<Q: QueryData, F: QueryFilter>( + /// If you don't need the entity to be the nearest one, it may be more + /// efficient to use [`Self::any_entity_id_by`] instead. You can also use + /// [`Self::nearest_entity_ids_by`] to get all nearby entities. + pub fn nearest_entity_id_by<Q: QueryData, F: QueryFilter>( &self, predicate: impl EntityPredicate<Q, F>, ) -> Option<Entity> { - self.nearest_entities_by(predicate).first().copied() + self.nearest_entity_ids_by(predicate).first().copied() } - /// Similar to [`Self::nearest_entity_by`] but returns a `Vec<Entity>` of - /// all entities in our instance that match the predicate. + /// Returns an array of all [`EntityRef`]s in the instance that match the + /// predicate, sorted by nearest first. + /// + /// To only get the nearest entity, consider using + /// [`Self::nearest_entity_by`]. If you only need the [`Entity`] + /// identifiers, you can use [`Self::nearest_entity_ids_by`] instead. + pub fn nearest_entities_by<Q: QueryData, F: QueryFilter>( + &self, + predicate: impl EntityPredicate<Q, F>, + ) -> Box<[EntityRef]> { + self.nearest_entity_ids_by(predicate) + .into_iter() + .map(|e| self.entity_ref_for(e)) + .collect() + } + /// Returns an array of all [`Entity`]s in the instance that match the + /// predicate, sorted by nearest first. /// - /// The first entity is the nearest one. + /// To only get the nearest entity, consider using + /// [`Self::nearest_entity_id_by`]. To get the [`EntityRef`]s instead, you + /// can use [`Self::nearest_entities_by`]. /// /// ``` /// # use azalea_entity::{LocalEntity, Position, metadata::Player}; @@ -225,7 +245,7 @@ impl Client { /// bot.nearest_entities_by::<(), (With<Player>, Without<LocalEntity>)>(|_: ()| true); /// # } /// ``` - pub fn nearest_entities_by<Q: QueryData, F: QueryFilter>( + pub fn nearest_entity_ids_by<Q: QueryData, F: QueryFilter>( &self, predicate: impl EntityPredicate<Q, F>, ) -> Box<[Entity]> { diff --git a/azalea/src/client_impl/mod.rs b/azalea/src/client_impl/mod.rs index 419fc27e..e3876f2e 100644 --- a/azalea/src/client_impl/mod.rs +++ b/azalea/src/client_impl/mod.rs @@ -11,16 +11,8 @@ use azalea_client::{ player::{GameProfileComponent, PlayerInfo}, start_ecs_runner, }; -use azalea_core::{ - data_registry::{DataRegistryWithKey, ResolvableDataRegistry}, - position::Vec3, -}; -use azalea_entity::{ - Attributes, Position, - dimensions::EntityDimensions, - indexing::{EntityIdIndex, EntityUuidIndex}, - metadata::Health, -}; +use azalea_core::data_registry::{DataRegistryWithKey, ResolvableDataRegistry}; +use azalea_entity::indexing::{EntityIdIndex, EntityUuidIndex}; use azalea_protocol::{ address::{ResolvableAddr, ResolvedAddr}, connect::Proxy, @@ -39,7 +31,10 @@ use parking_lot::RwLock; use tokio::sync::mpsc; use uuid::Uuid; -use crate::events::{Event, LocalPlayerEvents}; +use crate::{ + entity_ref::EntityRef, + events::{Event, LocalPlayerEvents}, +}; pub mod attack; pub mod chat; @@ -309,47 +304,19 @@ impl Client { self.query_self::<Option<&InstanceName>, _>(|ins| ins.is_some()) } - /// Get the position of this client. - /// - /// This is a shortcut for `Vec3::from(&bot.component::<Position>())`. - /// - /// Note that this value is given a default of [`Vec3::ZERO`] when it - /// receives the login packet, its true position may be set ticks - /// later. - pub fn position(&self) -> Vec3 { - Vec3::from( - &*self - .get_component::<Position>() - .expect("the client's position hasn't been initialized yet"), - ) - } - - /// Get the bounding box dimensions for our client, which contains our - /// width, height, and eye height. - /// - /// This is a shortcut for - /// `self.component::<EntityDimensions>()`. - pub fn dimensions(&self) -> EntityDimensions { - self.component::<EntityDimensions>().clone() - } - - /// Get the position of this client's eyes. - /// - /// This is a shortcut for - /// `bot.position().up(bot.dimensions().eye_height)`. - pub fn eye_position(&self) -> Vec3 { - self.query_self::<(&Position, &EntityDimensions), _>(|(pos, dim)| { - pos.up(dim.eye_height as f64) - }) + /// Returns the client as an [`EntityRef`], allowing you to treat it as any + /// other entity. + pub fn entity(&self) -> EntityRef { + self.entity_ref_for(self.entity) } - /// Get the health of this client. - /// - /// This is a shortcut for `*bot.component::<Health>()`. - pub fn health(&self) -> f32 { - **self.component::<Health>() + /// Create an [`EntityRef`] for the given ECS entity. + pub fn entity_ref_for(&self, entity: Entity) -> EntityRef { + EntityRef::new(self.clone(), entity) } +} +impl Client { /// Get the hunger level of this client, which includes both food and /// saturation. /// @@ -366,13 +333,6 @@ impl Client { self.profile().name.to_owned() } - /// Get the Minecraft UUID of this client. - /// - /// This is a shortcut for `bot.component::<GameProfileComponent>().uuid`. - pub fn uuid(&self) -> Uuid { - self.profile().uuid - } - /// Get a map of player UUIDs to their information in the tab list. /// /// This is a shortcut for `*bot.component::<TabList>()`. @@ -398,23 +358,6 @@ impl Client { self.component::<Account>().clone() } - /// Returns the attribute values of our player, which can be used to - /// determine things like our movement speed. - pub fn attributes(&self) -> Attributes { - // this *could* return a mapped read guard for performance but that rarely - // matters and it's just easier for the user if it doesn't. - self.component::<Attributes>().clone() - } - - /// Get the name of the instance (world) that the bot is in. - /// - /// This can be used to check if the client is in the same world as another - /// entity. - #[doc(alias("world_name", "dimension_name"))] - pub fn instance_name(&self) -> InstanceName { - (*self.component::<InstanceName>()).clone() - } - /// A convenience function to get the Minecraft Uuid of a player by their /// username, if they're present in the tab list. /// @@ -427,23 +370,36 @@ impl Client { .map(|player| player.profile.uuid) } - /// Get an ECS `Entity` in the world by its Minecraft UUID, if it's within + /// Get an [`Entity`] in the world by its Minecraft UUID, if it's within /// render distance. - pub fn entity_by_uuid(&self, uuid: Uuid) -> Option<Entity> { + /// + /// Also see [`Self::entity_by_uuid`] and + /// [`Self::entity_id_by_minecraft_id`]. + pub fn entity_id_by_uuid(&self, uuid: Uuid) -> Option<Entity> { self.map_resource::<EntityUuidIndex, _>(|entity_uuid_index| entity_uuid_index.get(&uuid)) } + /// Get an [`EntityRef`] in the world by its Minecraft UUID, if it's within + /// render distance. + /// + /// Also see [`Self::entity_id_by_uuid`]. + pub fn entity_by_uuid(&self, uuid: Uuid) -> Option<EntityRef> { + self.entity_id_by_uuid(uuid).map(|e| self.entity_ref_for(e)) + } - /// Convert an ECS `Entity` to a [`MinecraftEntityId`]. - pub fn minecraft_entity_by_ecs_entity(&self, entity: Entity) -> Option<MinecraftEntityId> { + /// Get an [`Entity`] in the world by its [`MinecraftEntityId`]. + /// + /// Also see [`Self::entity_by_uuid`] and [`Self::entity_id_by_uuid`]. + pub fn entity_id_by_minecraft_id(&self, id: MinecraftEntityId) -> Option<Entity> { self.query_self::<&EntityIdIndex, _>(|entity_id_index| { - entity_id_index.get_by_ecs_entity(entity) + entity_id_index.get_by_minecraft_entity(id) }) } - /// Convert a [`MinecraftEntityId`] to an ECS `Entity`. - pub fn ecs_entity_by_minecraft_entity(&self, entity: MinecraftEntityId) -> Option<Entity> { - self.query_self::<&EntityIdIndex, _>(|entity_id_index| { - entity_id_index.get_by_minecraft_entity(entity) - }) + /// Get an [`EntityRef`] in the world by its [`MinecraftEntityId`]. + /// + /// Also see [`Self::entity_id_by_uuid`]. + pub fn entity_by_minecraft_id(&self, id: MinecraftEntityId) -> Option<EntityRef> { + self.entity_id_by_minecraft_id(id) + .map(|e| EntityRef::new(self.clone(), e)) } /// Call the given function with the client's [`RegistryHolder`]. diff --git a/azalea/src/entity_ref/mod.rs b/azalea/src/entity_ref/mod.rs new file mode 100644 index 00000000..3e09d336 --- /dev/null +++ b/azalea/src/entity_ref/mod.rs @@ -0,0 +1,123 @@ +pub mod shared_impls; + +use std::fmt::Debug; + +use azalea_entity::EntityKindComponent; +use azalea_registry::builtin::EntityKind; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::{QueryData, QueryItem}, +}; +use parking_lot::MappedRwLockReadGuard; + +use crate::Client; + +/// A reference to an entity in a world. +/// +/// This is different from [`Entity`], since you can perform actions with just +/// an `EntityRef` instead of it only being an identifier. +/// +/// Most functions on `EntityRef` that return a value will result in a panic if +/// the client has despawned, so if your code involves waiting, you should check +/// [`Self::is_alive`] before calling those functions. +/// +/// Also, since `EntityRef` stores the [`Client`] alongside the entity, this +/// means that it supports interactions such as [`Self::attack`]. +/// +/// Not to be confused with Bevy's [`EntityRef`](bevy_ecs::world::EntityRef). +#[derive(Clone)] +pub struct EntityRef { + client: Client, + entity: Entity, +} + +impl EntityRef { + pub fn new(client: Client, entity: Entity) -> Self { + Self { client, entity } + } + + /// Returns the ECS identifier for the entity. + pub fn id(&self) -> Entity { + self.entity + } + + /// Get a component on the entity. + /// + /// This allows you to access certain data stored about the entity that + /// isn't accessible in a simpler way. + /// + /// See [`Client::component`] for more details. + /// + /// # 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.client.entity_component(self.entity) + } + + /// 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`]. + /// + /// See [`Client::component`] for more details. + pub fn get_component<T: Component>(&self) -> Option<MappedRwLockReadGuard<'_, T>> { + self.client.get_entity_component(self.entity) + } + + /// Query the ECS for data from the entity. + /// + /// You can use this to mutate data on the entity. + /// + /// Also see [`Client::query_self`] and [`Client::query_entity`]. + /// + /// # Panics + /// + /// This will panic if the entity is missing a component required by the + /// query. + pub fn query_self<D: QueryData, R>(&self, f: impl FnOnce(QueryItem<D>) -> R) -> R { + self.client.query_entity(self.entity, f) + } +} + +impl Debug for EntityRef { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EntityRef") + .field("client", &self.client.entity) + .field("entity", &self.entity) + .finish() + } +} + +impl EntityRef { + /// Returns the type of entity that this is. + pub fn kind(&self) -> EntityKind { + **self.component::<EntityKindComponent>() + } +} + +impl EntityRef { + /// Attack this entity from the client that created this `EntityRef`. + /// + /// Also see [`Client::attack`]. + pub fn attack(&self) { + self.client.attack(self.entity); + } + + /// Right-click this entity from the client that created this `EntityRef`. + /// + /// See [`Client::entity_interact`] for more information. + pub fn interact(&self) { + self.client.entity_interact(self.entity); + } +} diff --git a/azalea/src/entity_ref/shared_impls.rs b/azalea/src/entity_ref/shared_impls.rs new file mode 100644 index 00000000..3c24b836 --- /dev/null +++ b/azalea/src/entity_ref/shared_impls.rs @@ -0,0 +1,178 @@ +use azalea_core::position::Vec3; +use azalea_entity::{ + Attributes, Dead, EntityUuid, Position, dimensions::EntityDimensions, metadata::Health, +}; +use azalea_world::{InstanceName, MinecraftEntityId}; +use uuid::Uuid; + +use super::EntityRef; +use crate::Client; + +macro_rules! impl_entity_functions { + ( $( + Client: + $(#[$client_doc:meta])* + EntityRef: + $(#[$entityref_doc:meta])* + pub fn $fn_name:ident (&$fn_self:ident) -> $fn_returns:ty $fn_impl:block + )* ) => { + $( + impl Client { + $(#[$client_doc])* + pub fn $fn_name(&$fn_self) -> $fn_returns $fn_impl + } + impl EntityRef { + $(#[$entityref_doc])* + pub fn $fn_name(&$fn_self) -> $fn_returns $fn_impl + } + )* + }; +} + +// these functions are defined this way because we want them to be duplicated +// for both Client and EntityRef but still have their own documentation +impl_entity_functions! { + Client: + /// Get the client's position in the world, which is the same as its feet + /// position. + /// + /// This is a shortcut for `**bot.component::<Position>()`. + /// + /// To get the client's eye position, use [`Self::eye_position`]. + /// + /// Note that this value is given a default of [`Vec3::ZERO`] when it + /// receives the login packet, its true position may be set ticks + /// later. + EntityRef: + /// Get the entity's position in the world, which is the same as its feet + /// position. + /// + /// To get the client's eye position, use [`Self::eye_position`]. + /// + /// Also see [`Client::position`]. + pub fn position(&self) -> Vec3 { + **self.component::<Position>() + } + + Client: + /// Get the bounding box dimensions for our client, which contains our + /// width, height, and eye height. + /// + /// This is a shortcut for + /// `self.component::<EntityDimensions>()`. + EntityRef: + /// Get the bounding box dimensions for the entity, which contains its + /// width, height, and eye height. + /// + /// Also see [`Client::dimensions`] + pub fn dimensions(&self) -> EntityDimensions { + self.component::<EntityDimensions>().clone() + } + + Client: + /// Get the position of this client's eyes. + /// + /// Also see [`Self::position`]. + /// + /// This is a shortcut for + /// `bot.position().up(bot.dimensions().eye_height)`. + EntityRef: + /// Get the position of this entity's eyes. + /// + /// Also see [`Client::eye_position`]. + pub fn eye_position(&self) -> Vec3 { + self.query_self::<(&Position, &EntityDimensions), _>(|(pos, dim)| { + pos.up(dim.eye_height as f64) + }) + } + + Client: + /// Get the health of this client. + /// + /// This is a shortcut for `*bot.component::<Health>()`. + EntityRef: + /// Get the health of this entity. + /// + /// Also see [`Client::health`]. + pub fn health(&self) -> f32 { + **self.component::<Health>() + } + + Client: + /// Get the Minecraft UUID of this client. + /// + /// This is a shortcut for `**self.component::<EntityUuid>()`. + EntityRef: + /// Get the Minecraft UUID of this entity. + /// + /// Also see [`Client::uuid`]. + pub fn uuid(&self) -> Uuid { + **self.component::<EntityUuid>() + } + + Client: + /// Get the Minecraft ID of this client. + /// + /// See [`MinecraftEntityId`] for more details. For persistent identifiers, + /// consider using [`Self::uuid`] instead. + /// + /// This is a shortcut for `**self.component::<MinecraftEntityId>()`. + EntityRef: + /// Get the Minecraft UUID of this entity. + /// + /// See [`MinecraftEntityId`] for more details. For persistent identifiers, + /// consider using [`Self::uuid`] instead. + /// + /// Also see [`Client::minecraft_id`]. + pub fn minecraft_id(&self) -> MinecraftEntityId { + *self.component::<MinecraftEntityId>() + } + + Client: + /// Returns the attribute values of our player, which can be used to + /// determine things like our movement speed. + EntityRef: + /// Returns the attribute values of the entity, which can be used to + /// determine things like its movement speed. + pub fn attributes(&self) -> Attributes { + // this *could* return a mapped read guard for performance but that rarely + // matters and it's just easier for the user if it doesn't. + self.component::<Attributes>().clone() + } + + Client: + /// Get the name of the instance (world) that the bot is in. + /// + /// This can be used to check if the client is in the same world as another + /// entity. + #[doc(alias("world_name", "dimension_name"))] + EntityRef: + /// Get the name of the instance (world) that the entity is in. + /// + /// This can be used to check if the entity is in the same world as another + /// entity. + /// + /// Also see [`Client::instance_name`], + #[doc(alias("world_name", "dimension_name"))] + pub fn instance_name(&self) -> InstanceName { + (*self.component::<InstanceName>()).clone() + } + + Client: + /// Returns whether the client is alive and in the world. + /// + /// You should avoid using this if you have auto-respawn enabled (which is + /// the default), instead consider watching for + /// [`Event::Death`](crate::Event::Death) instead. + EntityRef: + /// Returns whether the entity is alive and hasn't despawned. + /// + /// Unlike most functions in `EntityRef`, this one will not panic if the + /// entity is despawned. Because of this, it may be useful to check `is_alive` + /// before calling functions that request data from the world. + /// + /// Also see [`Client::is_alive`]. + pub fn is_alive(&self) -> bool { + self.query_self::<Option<&Dead>, _>(|dead| dead.is_none()) + } +} diff --git a/azalea/src/lib.rs b/azalea/src/lib.rs index 351c956a..332dc565 100644 --- a/azalea/src/lib.rs +++ b/azalea/src/lib.rs @@ -9,6 +9,7 @@ pub mod bot; mod builder; mod client_impl; pub mod container; +mod entity_ref; pub mod events; mod join_opts; pub mod nearest_entity; @@ -53,7 +54,7 @@ pub use builder::ClientBuilder; use futures::future::BoxFuture; pub use join_opts::JoinOpts; -pub use crate::{client_impl::Client, events::Event}; +pub use crate::{client_impl::Client, entity_ref::EntityRef, events::Event}; pub type BoxHandleFn<S, R> = Box<dyn Fn(Client, Event, S) -> BoxFuture<'static, R> + Send>; pub type HandleFn<S, Fut> = fn(Client, Event, S) -> Fut; |
