aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2025-12-28 21:54:12 -0600
committerGitHub <noreply@github.com>2025-12-28 21:54:12 -0600
commit39488a6585ce969af93f43ece1ffb1174dc95e1d (patch)
tree49b63b2321b974a7c6425e53b8602a0b4500f092
parent25e441944412038da2be4e64854e59169d58305b (diff)
downloadazalea-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.md2
-rw-r--r--azalea/examples/testbot/commands.rs5
-rw-r--r--azalea/examples/testbot/commands/debug.rs12
-rw-r--r--azalea/examples/testbot/commands/movement.rs20
-rw-r--r--azalea/examples/testbot/killaura.rs2
-rw-r--r--azalea/examples/todo/README.md2
-rw-r--r--azalea/examples/todo/pvp.rs2
-rw-r--r--azalea/src/client_impl/entity_query.rs106
-rw-r--r--azalea/src/client_impl/mod.rs120
-rw-r--r--azalea/src/entity_ref/mod.rs123
-rw-r--r--azalea/src/entity_ref/shared_impls.rs178
-rw-r--r--azalea/src/lib.rs3
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;