From fd9bf168716f195e7e6225b93dfb099aa01b1fde Mon Sep 17 00:00:00 2001 From: mat Date: Tue, 17 Jun 2025 09:30:09 +1200 Subject: [PATCH] implement EntityHitResult --- CHANGELOG.md | 15 +- README.md | 2 +- .../plugins/{interact.rs => interact/mod.rs} | 208 +++++--------- azalea-client/src/plugins/interact/pick.rs | 268 ++++++++++++++++++ azalea-client/src/plugins/mining.rs | 8 +- azalea-client/src/plugins/packet/game/mod.rs | 6 +- azalea-core/src/hit_result.rs | 67 ++++- azalea-core/src/position.rs | 6 + azalea-entity/src/attributes.rs | 19 +- azalea-entity/src/lib.rs | 27 +- azalea-world/src/world.rs | 7 +- azalea/examples/testbot/commands/debug.rs | 31 +- azalea/src/pathfinder/goals.rs | 2 +- azalea/src/pathfinder/simulation.rs | 8 +- 14 files changed, 485 insertions(+), 189 deletions(-) rename azalea-client/src/plugins/{interact.rs => interact/mod.rs} (75%) create mode 100644 azalea-client/src/plugins/interact/pick.rs 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 similarity index 75% rename from azalea-client/src/plugins/interact.rs rename to 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, -) { - 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, 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, Changed)>, ) { 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, Changed)>, +) { + 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, + >, + instance_container: Res, + 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, Without, Without), +>; + +/// 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 { + 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 location(&self) -> Vec3 { + match self { + 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 { - match self { - HitResult::Block(block_hit_result) => !block_hit_result.miss, - HitResult::Entity => false, - } + 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 for HitResult { + fn from(value: BlockHitResult) -> Self { + HitResult::Block(value) + } +} +impl From 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>) { let hit_result = source.bot.component::(); - 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::(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(), } }