1
2
Fork 0
mirror of https://github.com/mat-1/azalea.git synced 2025-08-02 14:26:04 +00:00

implement EntityHitResult

This commit is contained in:
mat 2025-06-17 09:30:09 +12:00
parent 713dae7110
commit fd9bf16871
14 changed files with 485 additions and 189 deletions

View file

@ -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 Due to the complexity of Azalea and the fact that almost every Minecraft version
is breaking anyways, semantic versioning is not followed. 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 ## [0.13.0] - 2025-06-15
### Added ### 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`. - 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. - 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`. - 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 ### Fixed

View file

@ -23,7 +23,7 @@ _Currently supported Minecraft version: `1.21.5`._
- [Breaking blocks](https://azalea.matdoes.dev/azalea/struct.Client.html#method.mine) - [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) - [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) - [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) - [Plugins](#plugins)
## Docs ## Docs

View file

@ -1,3 +1,5 @@
pub mod pick;
use std::collections::HashMap; use std::collections::HashMap;
use azalea_block::BlockState; use azalea_block::BlockState;
@ -9,27 +11,30 @@ use azalea_core::{
tick::GameTick, tick::GameTick,
}; };
use azalea_entity::{ 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_inventory::{ItemStack, ItemStackData, components};
use azalea_physics::{ use azalea_physics::PhysicsSet;
PhysicsSet,
clip::{BlockShapeType, ClipContext, FluidPickType},
};
use azalea_protocol::packets::game::{ 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, s_use_item_on::ServerboundUseItemOn,
}; };
use azalea_world::{Instance, InstanceContainer, InstanceName}; use azalea_world::{Instance, MinecraftEntityId};
use bevy_app::{App, Plugin, Update}; use bevy_app::{App, Plugin, Update};
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use derive_more::{Deref, DerefMut};
use tracing::warn; use tracing::warn;
use super::mining::Mining; use super::mining::Mining;
use crate::{ use crate::{
Client, Client,
attack::handle_attack_event, attack::handle_attack_event,
interact::pick::{HitResultComponent, update_hit_result_component},
inventory::{Inventory, InventorySet}, inventory::{Inventory, InventorySet},
local_player::{LocalGameMode, PermissionLevel, PlayerAbilities}, local_player::{LocalGameMode, PermissionLevel, PlayerAbilities},
movement::MoveEventsSet, movement::MoveEventsSet,
@ -47,24 +52,29 @@ impl Plugin for InteractPlugin {
Update, Update,
( (
( (
handle_start_use_item_event, update_attributes_for_held_item,
update_hit_result_component.after(clamp_look_direction), update_attributes_for_gamemode,
handle_swing_arm_event,
) )
.after(InventorySet) .in_set(UpdateAttributesSet)
.after(perform_respawn)
.after(handle_attack_event)
.chain(), .chain(),
update_modifiers_for_held_item handle_start_use_item_event,
.after(InventorySet) update_hit_result_component.after(clamp_look_direction),
.after(MoveEventsSet), 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_systems(GameTick, handle_start_use_item_queued.before(PhysicsSet))
.add_observer(handle_swing_arm_trigger); .add_observer(handle_swing_arm_trigger);
} }
} }
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub struct UpdateAttributesSet;
impl Client { impl Client {
/// Right-click a block. /// 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. /// An event that makes one of our clients simulate a right-click.
/// ///
/// This event just inserts the [`StartUseItemQueued`] component on the given /// This event just inserts the [`StartUseItemQueued`] component on the given
@ -248,6 +252,7 @@ pub fn handle_start_use_item_queued(
&LookDirection, &LookDirection,
Option<&Mining>, Option<&Mining>,
)>, )>,
entity_id_query: Query<&MinecraftEntityId>,
) { ) {
for (entity, start_use_item, mut prediction_handler, hit_result, look_direction, mining) in for (entity, start_use_item, mut prediction_handler, hit_result, look_direction, mining) in
query 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 // TODO: this also skips if LocalPlayer.handsBusy is true, which is used when
// rowing a boat // 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 { if let Some(force_block) = start_use_item.force_block {
let hit_result_matches = if let HitResult::Block(block_hit_result) = &hit_result { 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 { match &hit_result {
HitResult::Block(block_hit_result) => { HitResult::Block(r) => {
let seq = prediction_handler.start_predicting(); let seq = prediction_handler.start_predicting();
if block_hit_result.miss { if r.miss {
commands.trigger(SendPacketEvent::new( commands.trigger(SendPacketEvent::new(
entity, entity,
ServerboundUseItem { ServerboundUseItem {
@ -301,124 +306,41 @@ pub fn handle_start_use_item_queued(
entity, entity,
ServerboundUseItemOn { ServerboundUseItemOn {
hand: start_use_item.hand, hand: start_use_item.hand,
block_hit: block_hit_result.into(), block_hit: r.into(),
seq, seq,
}, },
)); ));
// TODO: depending on the result of useItemOn, this might // TODO: depending on the result of useItemOn, this might
// also need to send a SwingArmEvent. // also need to send a SwingArmEvent.
// basically, this TODO is for // basically, this TODO is for simulating block
// simulating block interactions/placements on the // interactions/placements on the client-side.
// client-side.
} }
} }
HitResult::Entity => { HitResult::Entity(r) => {
// TODO: implement HitResult::Entity
// TODO: worldborder check // TODO: worldborder check
// commands.trigger(SendPacketEvent::new( let Ok(entity_id) = entity_id_query.get(r.entity).copied() else {
// entity, warn!("tried to interact with an entity that doesn't have MinecraftEntityId");
// ServerboundInteract { continue;
// entity_id: todo!(), };
// action: todo!(),
// using_secondary_action: todo!(), 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 /// Whether we can't interact with the block, based on your gamemode. If
/// this is false, then we can interact with the block. /// 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)] #[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>)>, mut query: Query<(&mut Attributes, &Inventory), (With<LocalEntity>, Changed<Inventory>)>,
) { ) {
for (mut attributes, inventory) in &mut query { 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);
}
}
}

View file

@ -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
}

View file

@ -13,8 +13,8 @@ use tracing::trace;
use crate::{ use crate::{
Client, Client,
interact::{ interact::{
BlockStatePredictionHandler, HitResultComponent, SwingArmEvent, can_use_game_master_blocks, BlockStatePredictionHandler, SwingArmEvent, can_use_game_master_blocks,
check_is_interaction_restricted, check_is_interaction_restricted, pick::HitResultComponent,
}, },
inventory::{Inventory, InventorySet}, inventory::{Inventory, InventorySet},
local_player::{InstanceHolder, LocalGameMode, PermissionLevel, PlayerAbilities}, local_player::{InstanceHolder, LocalGameMode, PermissionLevel, PlayerAbilities},
@ -57,7 +57,7 @@ impl Plugin for MiningPlugin {
.after(MoveEventsSet) .after(MoveEventsSet)
.before(azalea_entity::update_bounding_box) .before(azalea_entity::update_bounding_box)
.after(azalea_entity::update_fluid_on_eyes) .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::attack::handle_attack_event)
.after(crate::interact::handle_start_use_item_queued) .after(crate::interact::handle_start_use_item_queued)
.before(crate::interact::handle_swing_arm_event), .before(crate::interact::handle_swing_arm_event),
@ -143,7 +143,7 @@ fn handle_auto_mine(
entity, entity,
position: block_pos, 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 }); stop_mining_block_event.write(StopMiningBlockEvent { entity });
} }
} }

View file

@ -7,8 +7,8 @@ use azalea_core::{
position::{ChunkPos, Vec3}, position::{ChunkPos, Vec3},
}; };
use azalea_entity::{ use azalea_entity::{
Dead, EntityBundle, EntityKind, LastSentPosition, LoadedBy, LocalEntity, LookDirection, Dead, EntityBundle, EntityKindComponent, LastSentPosition, LoadedBy, LocalEntity,
Physics, Position, RelativeEntityUpdate, LookDirection, Physics, Position, RelativeEntityUpdate,
indexing::{EntityIdIndex, EntityUuidIndex}, indexing::{EntityIdIndex, EntityUuidIndex},
metadata::{Health, apply_metadata}, metadata::{Health, apply_metadata},
}; };
@ -665,7 +665,7 @@ impl GamePacketHandler<'_> {
Query<(&EntityIdIndex, &InstanceHolder)>, Query<(&EntityIdIndex, &InstanceHolder)>,
// this is a separate query since it's applied on the entity id that's being updated // 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 // instead of the player that received the packet
Query<&EntityKind>, Query<&EntityKindComponent>,
)>(self.ecs, |(mut commands, query, entity_kind_query)| { )>(self.ecs, |(mut commands, query, entity_kind_query)| {
let (entity_id_index, instance_holder) = query.get(self.player).unwrap(); let (entity_id_index, instance_holder) = query.get(self.player).unwrap();

View file

@ -1,3 +1,5 @@
use bevy_ecs::entity::Entity;
use crate::{ use crate::{
direction::Direction, direction::Direction,
position::{BlockPos, Vec3}, position::{BlockPos, Vec3},
@ -9,30 +11,47 @@ use crate::{
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum HitResult { pub enum HitResult {
Block(BlockHitResult), Block(BlockHitResult),
/// TODO Entity(EntityHitResult),
Entity,
} }
impl HitResult { impl HitResult {
pub fn is_miss(&self) -> bool { pub fn miss(&self) -> bool {
match self { match self {
HitResult::Block(block_hit_result) => block_hit_result.miss, HitResult::Block(r) => r.miss,
HitResult::Entity => false, 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 { pub fn is_block_hit_and_not_miss(&self) -> bool {
match self { matches!(self, HitResult::Block(r) if !r.miss)
HitResult::Block(block_hit_result) => !block_hit_result.miss,
HitResult::Entity => false,
}
} }
/// Returns the [`BlockHitResult`], if we were looking at a block and it /// Returns the [`BlockHitResult`], if we were looking at a block and it
/// wasn't a miss. /// wasn't a miss.
pub fn as_block_hit_result_if_not_miss(&self) -> Option<&BlockHitResult> { pub fn as_block_hit_result_if_not_miss(&self) -> Option<&BlockHitResult> {
match self { if let HitResult::Block(r) = self
HitResult::Block(block_hit_result) if !block_hit_result.miss => Some(block_hit_result), && !r.miss
_ => None, {
Some(r)
} else {
None
} }
} }
} }
@ -40,20 +59,21 @@ impl HitResult {
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct BlockHitResult { pub struct BlockHitResult {
pub location: Vec3, pub location: Vec3,
pub miss: bool,
pub direction: Direction, pub direction: Direction,
pub block_pos: BlockPos, pub block_pos: BlockPos,
pub inside: bool, pub inside: bool,
pub world_border: bool, pub world_border: bool,
pub miss: bool,
} }
impl BlockHitResult { impl BlockHitResult {
pub fn miss(location: Vec3, direction: Direction, block_pos: BlockPos) -> Self { pub fn miss(location: Vec3, direction: Direction, block_pos: BlockPos) -> Self {
Self { Self {
location, location,
miss: true,
direction, direction,
block_pos, block_pos,
miss: true,
inside: false, inside: false,
world_border: false, world_border: false,
} }
@ -66,3 +86,20 @@ impl BlockHitResult {
Self { block_pos, ..*self } 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)
}
}

View file

@ -350,6 +350,12 @@ impl Vec3 {
z: self.z.ceil() as i32, 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. /// The coordinates of a block in the world.

View file

@ -12,6 +12,9 @@ pub struct Attributes {
pub speed: AttributeInstance, pub speed: AttributeInstance,
pub attack_speed: AttributeInstance, pub attack_speed: AttributeInstance,
pub water_movement_efficiency: AttributeInstance, pub water_movement_efficiency: AttributeInstance,
pub block_interaction_range: AttributeInstance,
pub entity_interaction_range: AttributeInstance,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -93,7 +96,6 @@ pub fn sprinting_modifier() -> AttributeModifier {
operation: AttributeModifierOperation::MultiplyTotal, operation: AttributeModifierOperation::MultiplyTotal,
} }
} }
pub fn base_attack_speed_modifier(amount: f64) -> AttributeModifier { pub fn base_attack_speed_modifier(amount: f64) -> AttributeModifier {
AttributeModifier { AttributeModifier {
id: ResourceLocation::new("base_attack_speed"), id: ResourceLocation::new("base_attack_speed"),
@ -101,3 +103,18 @@ pub fn base_attack_speed_modifier(amount: f64) -> AttributeModifier {
operation: AttributeModifierOperation::Addition, 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,
}
}

View file

@ -25,6 +25,7 @@ use azalea_core::{
position::{BlockPos, ChunkPos, Vec3}, position::{BlockPos, ChunkPos, Vec3},
resource_location::ResourceLocation, resource_location::ResourceLocation,
}; };
use azalea_registry::EntityKind;
use azalea_world::{ChunkStorage, InstanceName}; use azalea_world::{ChunkStorage, InstanceName};
use bevy_ecs::{bundle::Bundle, component::Component}; use bevy_ecs::{bundle::Bundle, component::Component};
pub use data::*; pub use data::*;
@ -426,13 +427,13 @@ impl From<&EyeHeight> for f64 {
/// Most of the time, you should be using `azalea_registry::EntityKind` /// Most of the time, you should be using `azalea_registry::EntityKind`
/// directly instead. /// directly instead.
#[derive(Component, Clone, Copy, Debug, PartialEq, Deref)] #[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, /// A bundle of components that every entity has. This doesn't contain metadata,
/// that has to be added separately. /// that has to be added separately.
#[derive(Bundle)] #[derive(Bundle)]
pub struct EntityBundle { pub struct EntityBundle {
pub kind: EntityKind, pub kind: EntityKindComponent,
pub uuid: EntityUuid, pub uuid: EntityUuid,
pub world_name: InstanceName, pub world_name: InstanceName,
pub position: Position, pub position: Position,
@ -465,7 +466,7 @@ impl EntityBundle {
}; };
Self { Self {
kind: EntityKind(kind), kind: EntityKindComponent(kind),
uuid: EntityUuid(uuid), uuid: EntityUuid(uuid),
world_name: InstanceName(world_name), world_name: InstanceName(world_name),
position: Position(pos), position: Position(pos),
@ -475,13 +476,7 @@ impl EntityBundle {
eye_height: EyeHeight(eye_height), eye_height: EyeHeight(eye_height),
direction: LookDirection::default(), direction: LookDirection::default(),
attributes: Attributes { attributes: default_attributes(EntityKind::Player),
// 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),
},
jumping: Jumping(false), jumping: Jumping(false),
fluid_on_eyes: FluidOnEyes(FluidKind::Empty), 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 /// A marker component that signifies that this entity is "local" and shouldn't
/// be updated by other clients. /// be updated by other clients.
/// ///

View file

@ -147,11 +147,8 @@ impl PartialEntityInfos {
/// ///
/// Also see [`PartialInstance`]. /// Also see [`PartialInstance`].
/// ///
/// The reason this is called "instance" instead of "world" or "dimension" is /// This is sometimes interchangably called a "world". However, this type is
/// because "world" already means the entire ECS (which can contain multiple /// called `Instance` to avoid colliding with the `World` type from Bevy ECS.
/// 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)
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct Instance { pub struct Instance {
pub chunks: ChunkStorage, pub chunks: ChunkStorage,

View file

@ -7,11 +7,13 @@ use azalea::{
brigadier::prelude::*, brigadier::prelude::*,
chunks::ReceiveChunkEvent, chunks::ReceiveChunkEvent,
entity::{LookDirection, Position}, entity::{LookDirection, Position},
interact::HitResultComponent, interact::pick::HitResultComponent,
packet::game, packet::game,
pathfinder::{ExecutingPath, Pathfinder}, pathfinder::{ExecutingPath, Pathfinder},
world::MinecraftEntityId, world::MinecraftEntityId,
}; };
use azalea_core::hit_result::HitResult;
use azalea_entity::EntityKindComponent;
use azalea_world::InstanceContainer; use azalea_world::InstanceContainer;
use bevy_ecs::event::Events; use bevy_ecs::event::Events;
use parking_lot::Mutex; use parking_lot::Mutex;
@ -104,15 +106,24 @@ pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) {
let hit_result = source.bot.component::<HitResultComponent>(); let hit_result = source.bot.component::<HitResultComponent>();
let Some(hit_result) = hit_result.as_block_hit_result_if_not_miss() else { match &*hit_result {
source.reply("I'm not looking at anything"); HitResult::Block(r) => {
return 1; if r.miss {
}; source.reply("I'm not looking at anything");
return 0;
let block_pos = hit_result.block_pos; }
let block = source.bot.world().read().get_block_state(block_pos); 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:?}")); 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 1
})); }));

View file

@ -237,7 +237,7 @@ impl Goal for ReachBlockPosGoal {
let eye_position = n.center_bottom().up(1.62); let eye_position = n.center_bottom().up(1.62);
let look_direction = crate::direction_looking_at(eye_position, self.pos.center()); 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, look_direction,
eye_position, eye_position,
&self.chunk_storage, &self.chunk_storage,

View file

@ -10,7 +10,7 @@ use azalea_core::{
game_type::GameMode, position::Vec3, resource_location::ResourceLocation, tick::GameTick, game_type::GameMode, position::Vec3, resource_location::ResourceLocation, tick::GameTick,
}; };
use azalea_entity::{ use azalea_entity::{
Attributes, EntityDimensions, LookDirection, Physics, Position, attributes::AttributeInstance, Attributes, EntityDimensions, LookDirection, Physics, Position, default_attributes,
}; };
use azalea_registry::EntityKind; use azalea_registry::EntityKind;
use azalea_world::{ChunkStorage, Instance, InstanceContainer, MinecraftEntityId, PartialInstance}; use azalea_world::{ChunkStorage, Instance, InstanceContainer, MinecraftEntityId, PartialInstance};
@ -38,11 +38,7 @@ impl SimulatedPlayerBundle {
physics: Physics::new(dimensions, position), physics: Physics::new(dimensions, position),
physics_state: PhysicsState::default(), physics_state: PhysicsState::default(),
look_direction: LookDirection::default(), look_direction: LookDirection::default(),
attributes: Attributes { attributes: default_attributes(EntityKind::Player),
speed: AttributeInstance::new(0.1),
attack_speed: AttributeInstance::new(4.0),
water_movement_efficiency: AttributeInstance::new(0.0),
},
inventory: Inventory::default(), inventory: Inventory::default(),
} }
} }