aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormat <git@matdoes.dev>2025-06-17 09:30:09 +1200
committermat <git@matdoes.dev>2025-06-16 21:31:04 +0000
commitfd9bf168716f195e7e6225b93dfb099aa01b1fde (patch)
treee617f464e2df32cbc8678b56c5c1df8cae1c4dcb
parent713dae7110ad4119469323b87fd95a7f2a544ed0 (diff)
downloadazalea-drasl-fd9bf168716f195e7e6225b93dfb099aa01b1fde.tar.xz
implement EntityHitResult
-rw-r--r--CHANGELOG.md15
-rw-r--r--README.md2
-rw-r--r--azalea-client/src/plugins/interact/mod.rs (renamed from azalea-client/src/plugins/interact.rs)208
-rw-r--r--azalea-client/src/plugins/interact/pick.rs268
-rw-r--r--azalea-client/src/plugins/mining.rs8
-rw-r--r--azalea-client/src/plugins/packet/game/mod.rs6
-rw-r--r--azalea-core/src/hit_result.rs67
-rw-r--r--azalea-core/src/position.rs6
-rw-r--r--azalea-entity/src/attributes.rs19
-rw-r--r--azalea-entity/src/lib.rs27
-rw-r--r--azalea-world/src/world.rs7
-rw-r--r--azalea/examples/testbot/commands/debug.rs31
-rw-r--r--azalea/src/pathfinder/goals.rs2
-rw-r--r--azalea/src/pathfinder/simulation.rs8
14 files changed, 485 insertions, 189 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4b2f4bae..0bf29649 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
Due to the complexity of Azalea and the fact that almost every Minecraft version
is breaking anyways, semantic versioning is not followed.
+## [Unreleased]
+
+### Added
+
+- `HitResult` now contains the entity that's being looked at.
+
+### Changed
+
+- Renamed `azalea_entity::EntityKind` to `EntityKindComponent` to disambiguate with `azalea_registry::EntityKind`.
+- Moved functions and types related to hit results from `azalea::interact` to `azalea::interact::pick`.
+
+### Fixed
+
## [0.13.0] - 2025-06-15
### Added
@@ -44,7 +57,7 @@ is breaking anyways, semantic versioning is not followed.
- Replace `wait_one_tick` and `wait_one_update` with `wait_ticks` and `wait_updates`.
- Functions that took `&Vec3` or `&BlockPos` as arguments now only take them as owned types.
- Rename `azalea_block::Block` to `BlockTrait` to disambiguate with `azalea_registry::Block`.
-- `GotoEvent` is now non-enhaustive, it should be constructed by calling its methods now.
+- `GotoEvent` is now non-enhaustive and should instead be constructed by calling its methods.
### Fixed
diff --git a/README.md b/README.md
index 05c46004..252a8d80 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@ _Currently supported Minecraft version: `1.21.5`._
- [Breaking blocks](https://azalea.matdoes.dev/azalea/struct.Client.html#method.mine)
- [Block interactions & building](https://azalea.matdoes.dev/azalea/struct.Client.html#method.block_interact) (this doesn't predict the block interactions/placement on the client yet, but it's usually fine)
- [Inventories](https://azalea.matdoes.dev/azalea/struct.Client.html#impl-ContainerClientExt-for-Client)
-- [Attacking entities](https://azalea.matdoes.dev/azalea/struct.Client.html#method.attack) (but you can't get the entity at the crosshair yet)
+- [Attacking entities](https://azalea.matdoes.dev/azalea/struct.Client.html#method.attack)
- [Plugins](#plugins)
## Docs
diff --git a/azalea-client/src/plugins/interact.rs b/azalea-client/src/plugins/interact/mod.rs
index da1fa78e..079b57f6 100644
--- a/azalea-client/src/plugins/interact.rs
+++ b/azalea-client/src/plugins/interact/mod.rs
@@ -1,3 +1,5 @@
+pub mod pick;
+
use std::collections::HashMap;
use azalea_block::BlockState;
@@ -9,27 +11,30 @@ use azalea_core::{
tick::GameTick,
};
use azalea_entity::{
- Attributes, EyeHeight, LocalEntity, LookDirection, Position, clamp_look_direction, view_vector,
+ Attributes, LocalEntity, LookDirection,
+ attributes::{
+ creative_block_interaction_range_modifier, creative_entity_interaction_range_modifier,
+ },
+ clamp_look_direction,
};
use azalea_inventory::{ItemStack, ItemStackData, components};
-use azalea_physics::{
- PhysicsSet,
- clip::{BlockShapeType, ClipContext, FluidPickType},
-};
+use azalea_physics::PhysicsSet;
use azalea_protocol::packets::game::{
- ServerboundUseItem, s_interact::InteractionHand, s_swing::ServerboundSwing,
+ ServerboundInteract, ServerboundUseItem,
+ s_interact::{self, InteractionHand},
+ s_swing::ServerboundSwing,
s_use_item_on::ServerboundUseItemOn,
};
-use azalea_world::{Instance, InstanceContainer, InstanceName};
+use azalea_world::{Instance, MinecraftEntityId};
use bevy_app::{App, Plugin, Update};
use bevy_ecs::prelude::*;
-use derive_more::{Deref, DerefMut};
use tracing::warn;
use super::mining::Mining;
use crate::{
Client,
attack::handle_attack_event,
+ interact::pick::{HitResultComponent, update_hit_result_component},
inventory::{Inventory, InventorySet},
local_player::{LocalGameMode, PermissionLevel, PlayerAbilities},
movement::MoveEventsSet,
@@ -47,24 +52,29 @@ impl Plugin for InteractPlugin {
Update,
(
(
- handle_start_use_item_event,
- update_hit_result_component.after(clamp_look_direction),
- handle_swing_arm_event,
+ update_attributes_for_held_item,
+ update_attributes_for_gamemode,
)
- .after(InventorySet)
- .after(perform_respawn)
- .after(handle_attack_event)
+ .in_set(UpdateAttributesSet)
.chain(),
- update_modifiers_for_held_item
- .after(InventorySet)
- .after(MoveEventsSet),
- ),
+ handle_start_use_item_event,
+ update_hit_result_component.after(clamp_look_direction),
+ handle_swing_arm_event,
+ )
+ .after(InventorySet)
+ .after(MoveEventsSet)
+ .after(perform_respawn)
+ .after(handle_attack_event)
+ .chain(),
)
.add_systems(GameTick, handle_start_use_item_queued.before(PhysicsSet))
.add_observer(handle_swing_arm_trigger);
}
}
+#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
+pub struct UpdateAttributesSet;
+
impl Client {
/// Right-click a block.
///
@@ -190,12 +200,6 @@ impl BlockStatePredictionHandler {
}
}
-/// A component that contains the block or entity that the player is currently
-/// looking at.
-#[doc(alias("looking at", "looking at block", "crosshair"))]
-#[derive(Component, Clone, Debug, Deref, DerefMut)]
-pub struct HitResultComponent(HitResult);
-
/// An event that makes one of our clients simulate a right-click.
///
/// This event just inserts the [`StartUseItemQueued`] component on the given
@@ -248,6 +252,7 @@ pub fn handle_start_use_item_queued(
&LookDirection,
Option<&Mining>,
)>,
+ entity_id_query: Query<&MinecraftEntityId>,
) {
for (entity, start_use_item, mut prediction_handler, hit_result, look_direction, mining) in
query
@@ -261,7 +266,7 @@ pub fn handle_start_use_item_queued(
// TODO: this also skips if LocalPlayer.handsBusy is true, which is used when
// rowing a boat
- let mut hit_result = hit_result.0.clone();
+ let mut hit_result = (**hit_result).clone();
if let Some(force_block) = start_use_item.force_block {
let hit_result_matches = if let HitResult::Block(block_hit_result) = &hit_result {
@@ -284,9 +289,9 @@ pub fn handle_start_use_item_queued(
}
match &hit_result {
- HitResult::Block(block_hit_result) => {
+ HitResult::Block(r) => {
let seq = prediction_handler.start_predicting();
- if block_hit_result.miss {
+ if r.miss {
commands.trigger(SendPacketEvent::new(
entity,
ServerboundUseItem {
@@ -301,124 +306,41 @@ pub fn handle_start_use_item_queued(
entity,
ServerboundUseItemOn {
hand: start_use_item.hand,
- block_hit: block_hit_result.into(),
+ block_hit: r.into(),
seq,
},
));
// TODO: depending on the result of useItemOn, this might
// also need to send a SwingArmEvent.
- // basically, this TODO is for
- // simulating block interactions/placements on the
- // client-side.
+ // basically, this TODO is for simulating block
+ // interactions/placements on the client-side.
}
}
- HitResult::Entity => {
- // TODO: implement HitResult::Entity
-
+ HitResult::Entity(r) => {
// TODO: worldborder check
- // commands.trigger(SendPacketEvent::new(
- // entity,
- // ServerboundInteract {
- // entity_id: todo!(),
- // action: todo!(),
- // using_secondary_action: todo!(),
- // },
- // ));
+ let Ok(entity_id) = entity_id_query.get(r.entity).copied() else {
+ warn!("tried to interact with an entity that doesn't have MinecraftEntityId");
+ continue;
+ };
+
+ commands.trigger(SendPacketEvent::new(
+ entity,
+ ServerboundInteract {
+ entity_id,
+ action: s_interact::ActionType::InteractAt {
+ location: r.location,
+ hand: InteractionHand::MainHand,
+ },
+ // TODO: sneaking
+ using_secondary_action: false,
+ },
+ ));
}
}
}
}
-#[allow(clippy::type_complexity)]
-pub fn update_hit_result_component(
- mut commands: Commands,
- mut query: Query<(
- Entity,
- Option<&mut HitResultComponent>,
- &LocalGameMode,
- &Position,
- &EyeHeight,
- &LookDirection,
- &InstanceName,
- )>,
- instance_container: Res<InstanceContainer>,
-) {
- for (entity, hit_result_ref, game_mode, position, eye_height, look_direction, world_name) in
- &mut query
- {
- let pick_range = if game_mode.current == GameMode::Creative {
- 6.
- } else {
- 4.5
- };
- let eye_position = Vec3 {
- x: position.x,
- y: position.y + **eye_height as f64,
- z: position.z,
- };
-
- let Some(instance_lock) = instance_container.get(world_name) else {
- continue;
- };
- let instance = instance_lock.read();
-
- let hit_result = pick(*look_direction, eye_position, &instance.chunks, pick_range);
- if let Some(mut hit_result_ref) = hit_result_ref {
- **hit_result_ref = hit_result;
- } else {
- commands
- .entity(entity)
- .insert(HitResultComponent(hit_result));
- }
- }
-}
-
-/// Get the block or entity that a player would be looking at if their eyes were
-/// at the given direction and position.
-///
-/// If you need to get the block/entity the player is looking at right now, use
-/// [`HitResultComponent`].
-///
-/// Also see [`pick_block`].
-///
-/// TODO: does not currently check for entities
-pub fn pick(
- look_direction: LookDirection,
- eye_position: Vec3,
- chunks: &azalea_world::ChunkStorage,
- pick_range: f64,
-) -> HitResult {
- // TODO
- // let entity_hit_result = ;
-
- HitResult::Block(pick_block(look_direction, eye_position, chunks, pick_range))
-}
-
-/// Get the block that a player would be looking at if their eyes were at the
-/// given direction and position.
-///
-/// Also see [`pick`].
-pub fn pick_block(
- look_direction: LookDirection,
- eye_position: Vec3,
- chunks: &azalea_world::ChunkStorage,
- pick_range: f64,
-) -> BlockHitResult {
- let view_vector = view_vector(look_direction);
- let end_position = eye_position + (view_vector * pick_range);
-
- azalea_physics::clip::clip(
- chunks,
- ClipContext {
- from: eye_position,
- to: end_position,
- block_shape_type: BlockShapeType::Outline,
- fluid_pick_type: FluidPickType::None,
- },
- )
-}
-
/// Whether we can't interact with the block, based on your gamemode. If
/// this is false, then we can interact with the block.
///
@@ -504,7 +426,7 @@ pub fn handle_swing_arm_event(mut events: EventReader<SwingArmEvent>, mut comman
}
#[allow(clippy::type_complexity)]
-fn update_modifiers_for_held_item(
+fn update_attributes_for_held_item(
mut query: Query<(&mut Attributes, &Inventory), (With<LocalEntity>, Changed<Inventory>)>,
) {
for (mut attributes, inventory) in &mut query {
@@ -558,3 +480,25 @@ fn update_modifiers_for_held_item(
));
}
}
+
+fn update_attributes_for_gamemode(
+ query: Query<(&mut Attributes, &LocalGameMode), (With<LocalEntity>, Changed<LocalGameMode>)>,
+) {
+ for (mut attributes, game_mode) in query {
+ if game_mode.current == GameMode::Creative {
+ attributes
+ .block_interaction_range
+ .insert(creative_block_interaction_range_modifier());
+ attributes
+ .entity_interaction_range
+ .insert(creative_entity_interaction_range_modifier());
+ } else {
+ attributes
+ .block_interaction_range
+ .remove(&creative_block_interaction_range_modifier().id);
+ attributes
+ .entity_interaction_range
+ .remove(&creative_entity_interaction_range_modifier().id);
+ }
+ }
+}
diff --git a/azalea-client/src/plugins/interact/pick.rs b/azalea-client/src/plugins/interact/pick.rs
new file mode 100644
index 00000000..5b62762c
--- /dev/null
+++ b/azalea-client/src/plugins/interact/pick.rs
@@ -0,0 +1,268 @@
+use azalea_core::{
+ aabb::AABB,
+ direction::Direction,
+ hit_result::{BlockHitResult, EntityHitResult, HitResult},
+ position::Vec3,
+};
+use azalea_entity::{
+ Attributes, Dead, EyeHeight, LocalEntity, LookDirection, Physics, Position,
+ metadata::{ArmorStandMarker, Marker},
+ view_vector,
+};
+use azalea_physics::{
+ clip::{BlockShapeType, ClipContext, FluidPickType},
+ collision::entity_collisions::{PhysicsQuery, get_entities},
+};
+use azalea_world::{Instance, InstanceContainer, InstanceName};
+use bevy_ecs::prelude::*;
+use derive_more::{Deref, DerefMut};
+
+/// A component that contains the block or entity that the player is currently
+/// looking at.
+#[doc(alias("looking at", "looking at block", "crosshair"))]
+#[derive(Component, Clone, Debug, Deref, DerefMut)]
+pub struct HitResultComponent(HitResult);
+
+#[allow(clippy::type_complexity)]
+pub fn update_hit_result_component(
+ mut commands: Commands,
+ mut query: Query<
+ (
+ Entity,
+ Option<&mut HitResultComponent>,
+ &Position,
+ &EyeHeight,
+ &LookDirection,
+ &InstanceName,
+ &Physics,
+ &Attributes,
+ ),
+ With<LocalEntity>,
+ >,
+ instance_container: Res<InstanceContainer>,
+ physics_query: PhysicsQuery,
+ pickable_query: PickableEntityQuery,
+) {
+ for (
+ entity,
+ hit_result_ref,
+ position,
+ eye_height,
+ look_direction,
+ world_name,
+ physics,
+ attributes,
+ ) in &mut query
+ {
+ let block_pick_range = attributes.block_interaction_range.calculate();
+ let entity_pick_range = attributes.entity_interaction_range.calculate();
+
+ let eye_position = position.up(eye_height.into());
+
+ let Some(world_lock) = instance_container.get(world_name) else {
+ continue;
+ };
+ let world = world_lock.read();
+
+ let aabb = &physics.bounding_box;
+ let hit_result = pick(
+ entity,
+ *look_direction,
+ eye_position,
+ aabb,
+ &world,
+ entity_pick_range,
+ block_pick_range,
+ &physics_query,
+ &pickable_query,
+ );
+ if let Some(mut hit_result_ref) = hit_result_ref {
+ **hit_result_ref = hit_result;
+ } else {
+ commands
+ .entity(entity)
+ .insert(HitResultComponent(hit_result));
+ }
+ }
+}
+
+pub type PickableEntityQuery<'world, 'state, 'a> = Query<
+ 'world,
+ 'state,
+ Option<&'a ArmorStandMarker>,
+ (Without<Dead>, Without<Marker>, Without<LocalEntity>),
+>;
+
+/// Get the block or entity that a player would be looking at if their eyes were
+/// at the given direction and position.
+///
+/// If you need to get the block/entity the player is looking at right now, use
+/// [`HitResultComponent`].
+///
+/// Also see [`pick_block`].
+///
+/// TODO: does not currently check for entities
+pub fn pick(
+ source_entity: Entity,
+ look_direction: LookDirection,
+ eye_position: Vec3,
+ aabb: &AABB,
+ world: &Instance,
+ entity_pick_range: f64,
+ block_pick_range: f64,
+ physics_query: &PhysicsQuery,
+ pickable_query: &PickableEntityQuery,
+) -> HitResult {
+ // vanilla does extra math here to calculate the pick result in between ticks by
+ // interpolating, but since clients can still only interact on exact ticks, that
+ // isn't relevant for us.
+
+ let mut max_range = entity_pick_range.max(block_pick_range);
+ let mut max_range_squared = max_range.powi(2);
+
+ let block_hit_result = pick_block(look_direction, eye_position, &world.chunks, max_range);
+ let block_hit_result_dist_squared = block_hit_result.location.distance_squared_to(eye_position);
+ if !block_hit_result.miss {
+ max_range_squared = block_hit_result_dist_squared;
+ max_range = block_hit_result_dist_squared.sqrt();
+ }
+
+ let view_vector = view_vector(look_direction);
+ let end_position = eye_position + (view_vector * max_range);
+ let inflate_by = 1.;
+ let pick_aabb = aabb
+ .expand_towards(view_vector * max_range)
+ .inflate_all(inflate_by);
+
+ let is_pickable = |entity: Entity| {
+ // TODO: ender dragon and projectiles have extra logic here. also, we shouldn't
+ // be able to pick spectators.
+ if let Ok(armor_stand_marker) = pickable_query.get(entity) {
+ if let Some(armor_stand_marker) = armor_stand_marker
+ && armor_stand_marker.0
+ {
+ false
+ } else {
+ true
+ }
+ } else {
+ true
+ }
+ };
+ let entity_hit_result = pick_entity(
+ source_entity,
+ eye_position,
+ end_position,
+ world,
+ max_range_squared,
+ &is_pickable,
+ &pick_aabb,
+ physics_query,
+ );
+
+ // TODO
+ if let Some(entity_hit_result) = entity_hit_result
+ && entity_hit_result.location.distance_squared_to(eye_position)
+ < block_hit_result_dist_squared
+ {
+ filter_hit_result(
+ HitResult::Entity(entity_hit_result),
+ eye_position,
+ entity_pick_range,
+ )
+ } else {
+ filter_hit_result(
+ HitResult::Block(block_hit_result),
+ eye_position,
+ block_pick_range,
+ )
+ }
+}
+
+fn filter_hit_result(hit_result: HitResult, eye_position: Vec3, range: f64) -> HitResult {
+ let location = hit_result.location();
+ if !location.closer_than(eye_position, range) {
+ let direction = Direction::nearest(location - eye_position);
+ HitResult::new_miss(location, direction, location.into())
+ } else {
+ hit_result
+ }
+}
+
+/// Get the block that a player would be looking at if their eyes were at the
+/// given direction and position.
+///
+/// Also see [`pick`].
+pub fn pick_block(
+ look_direction: LookDirection,
+ eye_position: Vec3,
+ chunks: &azalea_world::ChunkStorage,
+ pick_range: f64,
+) -> BlockHitResult {
+ let view_vector = view_vector(look_direction);
+ let end_position = eye_position + (view_vector * pick_range);
+
+ azalea_physics::clip::clip(
+ chunks,
+ ClipContext {
+ from: eye_position,
+ to: end_position,
+ block_shape_type: BlockShapeType::Outline,
+ fluid_pick_type: FluidPickType::None,
+ },
+ )
+}
+
+// port of getEntityHitResult
+fn pick_entity(
+ source_entity: Entity,
+ eye_position: Vec3,
+ end_position: Vec3,
+ world: &azalea_world::Instance,
+ pick_range_squared: f64,
+ predicate: &dyn Fn(Entity) -> bool,
+ aabb: &AABB,
+ physics_query: &PhysicsQuery,
+) -> Option<EntityHitResult> {
+ let mut picked_distance_squared = pick_range_squared;
+ let mut result = None;
+
+ for (candidate, candidate_aabb) in
+ get_entities(world, Some(source_entity), aabb, predicate, physics_query)
+ {
+ // TODO: if the entity is "REDIRECTABLE_PROJECTILE" then this should be 1.0.
+ // azalea needs support for entity tags first for this to be possible. see
+ // getPickRadius in decompiled minecraft source
+ let candidate_pick_radius = 0.;
+ let candidate_aabb = candidate_aabb.inflate_all(candidate_pick_radius);
+ let clip_location = candidate_aabb.clip(eye_position, end_position);
+
+ if candidate_aabb.contains(eye_position) {
+ if picked_distance_squared >= 0. {
+ result = Some(EntityHitResult {
+ location: clip_location.unwrap_or(eye_position),
+ entity: candidate,
+ });
+ picked_distance_squared = 0.;
+ }
+ } else if let Some(clip_location) = clip_location {
+ let distance_squared = eye_position.distance_squared_to(clip_location);
+ if distance_squared < picked_distance_squared || picked_distance_squared == 0. {
+ // TODO: don't pick the entity we're riding on
+ // if candidate_root_vehicle == entity_root_vehicle {
+ // if picked_distance_squared == 0. {
+ // picked_entity = Some(candidate);
+ // picked_location = Some(clip_location);
+ // }
+ // } else {
+ result = Some(EntityHitResult {
+ location: clip_location,
+ entity: candidate,
+ });
+ picked_distance_squared = distance_squared;
+ }
+ }
+ }
+
+ result
+}
diff --git a/azalea-client/src/plugins/mining.rs b/azalea-client/src/plugins/mining.rs
index 88bd3be8..541633df 100644
--- a/azalea-client/src/plugins/mining.rs
+++ b/azalea-client/src/plugins/mining.rs
@@ -13,8 +13,8 @@ use tracing::trace;
use crate::{
Client,
interact::{
- BlockStatePredictionHandler, HitResultComponent, SwingArmEvent, can_use_game_master_blocks,
- check_is_interaction_restricted,
+ BlockStatePredictionHandler, SwingArmEvent, can_use_game_master_blocks,
+ check_is_interaction_restricted, pick::HitResultComponent,
},
inventory::{Inventory, InventorySet},
local_player::{InstanceHolder, LocalGameMode, PermissionLevel, PlayerAbilities},
@@ -57,7 +57,7 @@ impl Plugin for MiningPlugin {
.after(MoveEventsSet)
.before(azalea_entity::update_bounding_box)
.after(azalea_entity::update_fluid_on_eyes)
- .after(crate::interact::update_hit_result_component)
+ .after(crate::interact::pick::update_hit_result_component)
.after(crate::attack::handle_attack_event)
.after(crate::interact::handle_start_use_item_queued)
.before(crate::interact::handle_swing_arm_event),
@@ -143,7 +143,7 @@ fn handle_auto_mine(
entity,
position: block_pos,
});
- } else if mining.is_some() && hit_result_component.is_miss() {
+ } else if mining.is_some() && hit_result_component.miss() {
stop_mining_block_event.write(StopMiningBlockEvent { entity });
}
}
diff --git a/azalea-client/src/plugins/packet/game/mod.rs b/azalea-client/src/plugins/packet/game/mod.rs
index e1477d21..de226e49 100644
--- a/azalea-client/src/plugins/packet/game/mod.rs
+++ b/azalea-client/src/plugins/packet/game/mod.rs
@@ -7,8 +7,8 @@ use azalea_core::{
position::{ChunkPos, Vec3},
};
use azalea_entity::{
- Dead, EntityBundle, EntityKind, LastSentPosition, LoadedBy, LocalEntity, LookDirection,
- Physics, Position, RelativeEntityUpdate,
+ Dead, EntityBundle, EntityKindComponent, LastSentPosition, LoadedBy, LocalEntity,
+ LookDirection, Physics, Position, RelativeEntityUpdate,
indexing::{EntityIdIndex, EntityUuidIndex},
metadata::{Health, apply_metadata},
};
@@ -665,7 +665,7 @@ impl GamePacketHandler<'_> {
Query<(&EntityIdIndex, &InstanceHolder)>,
// this is a separate query since it's applied on the entity id that's being updated
// instead of the player that received the packet
- Query<&EntityKind>,
+ Query<&EntityKindComponent>,
)>(self.ecs, |(mut commands, query, entity_kind_query)| {
let (entity_id_index, instance_holder) = query.get(self.player).unwrap();
diff --git a/azalea-core/src/hit_result.rs b/azalea-core/src/hit_result.rs
index 76f7ca84..3b1e160d 100644
--- a/azalea-core/src/hit_result.rs
+++ b/azalea-core/src/hit_result.rs
@@ -1,3 +1,5 @@
+use bevy_ecs::entity::Entity;
+
use crate::{
direction::Direction,
position::{BlockPos, Vec3},
@@ -9,30 +11,47 @@ use crate::{
#[derive(Debug, Clone, PartialEq)]
pub enum HitResult {
Block(BlockHitResult),
- /// TODO
- Entity,
+ Entity(EntityHitResult),
}
+
impl HitResult {
- pub fn is_miss(&self) -> bool {
+ pub fn miss(&self) -> bool {
match self {
- HitResult::Block(block_hit_result) => block_hit_result.miss,
- HitResult::Entity => false,
+ HitResult::Block(r) => r.miss,
+ HitResult::Entity(_) => false,
}
}
-
- pub fn is_block_hit_and_not_miss(&self) -> bool {
+ pub fn location(&self) -> Vec3 {
match self {
- HitResult::Block(block_hit_result) => !block_hit_result.miss,
- HitResult::Entity => false,
+ HitResult::Block(r) => r.location,
+ HitResult::Entity(r) => r.location,
}
}
+ pub fn new_miss(location: Vec3, direction: Direction, block_pos: BlockPos) -> Self {
+ HitResult::Block(BlockHitResult {
+ location,
+ miss: true,
+ direction,
+ block_pos,
+ inside: false,
+ world_border: false,
+ })
+ }
+
+ pub fn is_block_hit_and_not_miss(&self) -> bool {
+ matches!(self, HitResult::Block(r) if !r.miss)
+ }
+
/// Returns the [`BlockHitResult`], if we were looking at a block and it
/// wasn't a miss.
pub fn as_block_hit_result_if_not_miss(&self) -> Option<&BlockHitResult> {
- match self {
- HitResult::Block(block_hit_result) if !block_hit_result.miss => Some(block_hit_result),
- _ => None,
+ if let HitResult::Block(r) = self
+ && !r.miss
+ {
+ Some(r)
+ } else {
+ None
}
}
}
@@ -40,20 +59,21 @@ impl HitResult {
#[derive(Debug, Clone, PartialEq)]
pub struct BlockHitResult {
pub location: Vec3,
+ pub miss: bool,
+
pub direction: Direction,
pub block_pos: BlockPos,
pub inside: bool,
pub world_border: bool,
- pub miss: bool,
}
-
impl BlockHitResult {
pub fn miss(location: Vec3, direction: Direction, block_pos: BlockPos) -> Self {
Self {
location,
+ miss: true,
+
direction,
block_pos,
- miss: true,
inside: false,
world_border: false,
}
@@ -66,3 +86,20 @@ impl BlockHitResult {
Self { block_pos, ..*self }
}
}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct EntityHitResult {
+ pub location: Vec3,
+ pub entity: Entity,
+}
+
+impl From<BlockHitResult> for HitResult {
+ fn from(value: BlockHitResult) -> Self {
+ HitResult::Block(value)
+ }
+}
+impl From<EntityHitResult> for HitResult {
+ fn from(value: EntityHitResult) -> Self {
+ HitResult::Entity(value)
+ }
+}
diff --git a/azalea-core/src/position.rs b/azalea-core/src/position.rs
index 5a8d3e0c..c0c25639 100644
--- a/azalea-core/src/position.rs
+++ b/azalea-core/src/position.rs
@@ -350,6 +350,12 @@ impl Vec3 {
z: self.z.ceil() as i32,
}
}
+
+ /// Whether the distance between this point and `other` is less than
+ /// `range`.
+ pub fn closer_than(&self, other: Vec3, range: f64) -> bool {
+ self.distance_squared_to(other) < range.powi(2)
+ }
}
/// The coordinates of a block in the world.
diff --git a/azalea-entity/src/attributes.rs b/azalea-entity/src/attributes.rs
index 12c9b908..7af845f8 100644
--- a/azalea-entity/src/attributes.rs
+++ b/azalea-entity/src/attributes.rs
@@ -12,6 +12,9 @@ pub struct Attributes {
pub speed: AttributeInstance,
pub attack_speed: AttributeInstance,
pub water_movement_efficiency: AttributeInstance,
+
+ pub block_interaction_range: AttributeInstance,
+ pub entity_interaction_range: AttributeInstance,
}
#[derive(Clone, Debug)]
@@ -93,7 +96,6 @@ pub fn sprinting_modifier() -> AttributeModifier {
operation: AttributeModifierOperation::MultiplyTotal,
}
}
-
pub fn base_attack_speed_modifier(amount: f64) -> AttributeModifier {
AttributeModifier {
id: ResourceLocation::new("base_attack_speed"),
@@ -101,3 +103,18 @@ pub fn base_attack_speed_modifier(amount: f64) -> AttributeModifier {
operation: AttributeModifierOperation::Addition,
}
}
+pub fn creative_block_interaction_range_modifier() -> AttributeModifier {
+ AttributeModifier {
+ id: ResourceLocation::new("creative_mode_block_range"),
+ amount: 0.5,
+ operation: AttributeModifierOperation::Addition,
+ }
+}
+
+pub fn creative_entity_interaction_range_modifier() -> AttributeModifier {
+ AttributeModifier {
+ id: ResourceLocation::new("creative_mode_entity_range"),
+ amount: 2.0,
+ operation: AttributeModifierOperation::Addition,
+ }
+}
diff --git a/azalea-entity/src/lib.rs b/azalea-entity/src/lib.rs
index cf2222d4..b8644546 100644
--- a/azalea-entity/src/lib.rs
+++ b/azalea-entity/src/lib.rs
@@ -25,6 +25,7 @@ use azalea_core::{
position::{BlockPos, ChunkPos, Vec3},
resource_location::ResourceLocation,
};
+use azalea_registry::EntityKind;
use azalea_world::{ChunkStorage, InstanceName};
use bevy_ecs::{bundle::Bundle, component::Component};
pub use data::*;
@@ -426,13 +427,13 @@ impl From<&EyeHeight> for f64 {
/// Most of the time, you should be using `azalea_registry::EntityKind`
/// directly instead.
#[derive(Component, Clone, Copy, Debug, PartialEq, Deref)]
-pub struct EntityKind(pub azalea_registry::EntityKind);
+pub struct EntityKindComponent(pub azalea_registry::EntityKind);
/// A bundle of components that every entity has. This doesn't contain metadata,
/// that has to be added separately.
#[derive(Bundle)]
pub struct EntityBundle {
- pub kind: EntityKind,
+ pub kind: EntityKindComponent,
pub uuid: EntityUuid,
pub world_name: InstanceName,
pub position: Position,
@@ -465,7 +466,7 @@ impl EntityBundle {
};
Self {
- kind: EntityKind(kind),
+ kind: EntityKindComponent(kind),
uuid: EntityUuid(uuid),
world_name: InstanceName(world_name),
position: Position(pos),
@@ -475,13 +476,7 @@ impl EntityBundle {
eye_height: EyeHeight(eye_height),
direction: LookDirection::default(),
- attributes: Attributes {
- // TODO: do the correct defaults for everything, some
- // entities have different defaults
- speed: AttributeInstance::new(0.1),
- attack_speed: AttributeInstance::new(4.0),
- water_movement_efficiency: AttributeInstance::new(0.0),
- },
+ attributes: default_attributes(EntityKind::Player),
jumping: Jumping(false),
fluid_on_eyes: FluidOnEyes(FluidKind::Empty),
@@ -490,6 +485,18 @@ impl EntityBundle {
}
}
+pub fn default_attributes(_entity_kind: EntityKind) -> Attributes {
+ // TODO: do the correct defaults for everything, some
+ // entities have different defaults
+ Attributes {
+ speed: AttributeInstance::new(0.1),
+ attack_speed: AttributeInstance::new(4.0),
+ water_movement_efficiency: AttributeInstance::new(0.0),
+ block_interaction_range: AttributeInstance::new(4.5),
+ entity_interaction_range: AttributeInstance::new(3.0),
+ }
+}
+
/// A marker component that signifies that this entity is "local" and shouldn't
/// be updated by other clients.
///
diff --git a/azalea-world/src/world.rs b/azalea-world/src/world.rs
index 89132a73..29e8e547 100644
--- a/azalea-world/src/world.rs
+++ b/azalea-world/src/world.rs
@@ -147,11 +147,8 @@ impl PartialEntityInfos {
///
/// Also see [`PartialInstance`].
///
-/// The reason this is called "instance" instead of "world" or "dimension" is
-/// because "world" already means the entire ECS (which can contain multiple
-/// instances if we're in a swarm) and "dimension" can be ambiguous (for
-/// instance there can be multiple overworlds, and "dimension" is also a math
-/// term)
+/// This is sometimes interchangably called a "world". However, this type is
+/// called `Instance` to avoid colliding with the `World` type from Bevy ECS.
#[derive(Default, Debug)]
pub struct Instance {
pub chunks: ChunkStorage,
diff --git a/azalea/examples/testbot/commands/debug.rs b/azalea/examples/testbot/commands/debug.rs
index ea5dbe6f..d721fddc 100644
--- a/azalea/examples/testbot/commands/debug.rs
+++ b/azalea/examples/testbot/commands/debug.rs
@@ -7,11 +7,13 @@ use azalea::{
brigadier::prelude::*,
chunks::ReceiveChunkEvent,
entity::{LookDirection, Position},
- interact::HitResultComponent,
+ interact::pick::HitResultComponent,
packet::game,
pathfinder::{ExecutingPath, Pathfinder},
world::MinecraftEntityId,
};
+use azalea_core::hit_result::HitResult;
+use azalea_entity::EntityKindComponent;
use azalea_world::InstanceContainer;
use bevy_ecs::event::Events;
use parking_lot::Mutex;
@@ -104,15 +106,24 @@ pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) {
let hit_result = source.bot.component::<HitResultComponent>();
- let Some(hit_result) = hit_result.as_block_hit_result_if_not_miss() else {
- source.reply("I'm not looking at anything");
- return 1;
- };
-
- let block_pos = hit_result.block_pos;
- let block = source.bot.world().read().get_block_state(block_pos);
-
- source.reply(&format!("I'm looking at {block:?} at {block_pos:?}"));
+ match &*hit_result {
+ HitResult::Block(r) => {
+ if r.miss {
+ source.reply("I'm not looking at anything");
+ return 0;
+ }
+ let block_pos = r.block_pos;
+ let block = source.bot.world().read().get_block_state(block_pos);
+ source.reply(&format!("I'm looking at {block:?} at {block_pos:?}"));
+ }
+ HitResult::Entity(r) => {
+ let entity_kind = *source.bot.entity_component::<EntityKindComponent>(r.entity);
+ source.reply(&format!(
+ "I'm looking at {entity_kind} ({:?}) at {}",
+ r.entity, r.location
+ ));
+ }
+ }
1
}));
diff --git a/azalea/src/pathfinder/goals.rs b/azalea/src/pathfinder/goals.rs
index 95786561..7a830973 100644
--- a/azalea/src/pathfinder/goals.rs
+++ b/azalea/src/pathfinder/goals.rs
@@ -237,7 +237,7 @@ impl Goal for ReachBlockPosGoal {
let eye_position = n.center_bottom().up(1.62);
let look_direction = crate::direction_looking_at(eye_position, self.pos.center());
- let block_hit_result = azalea_client::interact::pick_block(
+ let block_hit_result = azalea_client::interact::pick::pick_block(
look_direction,
eye_position,
&self.chunk_storage,
diff --git a/azalea/src/pathfinder/simulation.rs b/azalea/src/pathfinder/simulation.rs
index 5c548aa2..7f93a671 100644
--- a/azalea/src/pathfinder/simulation.rs
+++ b/azalea/src/pathfinder/simulation.rs
@@ -10,7 +10,7 @@ use azalea_core::{
game_type::GameMode, position::Vec3, resource_location::ResourceLocation, tick::GameTick,
};
use azalea_entity::{
- Attributes, EntityDimensions, LookDirection, Physics, Position, attributes::AttributeInstance,
+ Attributes, EntityDimensions, LookDirection, Physics, Position, default_attributes,
};
use azalea_registry::EntityKind;
use azalea_world::{ChunkStorage, Instance, InstanceContainer, MinecraftEntityId, PartialInstance};
@@ -38,11 +38,7 @@ impl SimulatedPlayerBundle {
physics: Physics::new(dimensions, position),
physics_state: PhysicsState::default(),
look_direction: LookDirection::default(),
- attributes: Attributes {
- speed: AttributeInstance::new(0.1),
- attack_speed: AttributeInstance::new(4.0),
- water_movement_efficiency: AttributeInstance::new(0.0),
- },
+ attributes: default_attributes(EntityKind::Player),
inventory: Inventory::default(),
}
}