aboutsummaryrefslogtreecommitdiff
path: root/azalea-client/src/plugins/interact
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 /azalea-client/src/plugins/interact
parent713dae7110ad4119469323b87fd95a7f2a544ed0 (diff)
downloadazalea-drasl-fd9bf168716f195e7e6225b93dfb099aa01b1fde.tar.xz
implement EntityHitResult
Diffstat (limited to 'azalea-client/src/plugins/interact')
-rw-r--r--azalea-client/src/plugins/interact/mod.rs504
-rw-r--r--azalea-client/src/plugins/interact/pick.rs268
2 files changed, 772 insertions, 0 deletions
diff --git a/azalea-client/src/plugins/interact/mod.rs b/azalea-client/src/plugins/interact/mod.rs
new file mode 100644
index 00000000..079b57f6
--- /dev/null
+++ b/azalea-client/src/plugins/interact/mod.rs
@@ -0,0 +1,504 @@
+pub mod pick;
+
+use std::collections::HashMap;
+
+use azalea_block::BlockState;
+use azalea_core::{
+ direction::Direction,
+ game_type::GameMode,
+ hit_result::{BlockHitResult, HitResult},
+ position::{BlockPos, Vec3},
+ tick::GameTick,
+};
+use azalea_entity::{
+ 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;
+use azalea_protocol::packets::game::{
+ ServerboundInteract, ServerboundUseItem,
+ s_interact::{self, InteractionHand},
+ s_swing::ServerboundSwing,
+ s_use_item_on::ServerboundUseItemOn,
+};
+use azalea_world::{Instance, MinecraftEntityId};
+use bevy_app::{App, Plugin, Update};
+use bevy_ecs::prelude::*;
+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,
+ packet::game::SendPacketEvent,
+ respawn::perform_respawn,
+};
+
+/// A plugin that allows clients to interact with blocks in the world.
+pub struct InteractPlugin;
+impl Plugin for InteractPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_event::<StartUseItemEvent>()
+ .add_event::<SwingArmEvent>()
+ .add_systems(
+ Update,
+ (
+ (
+ update_attributes_for_held_item,
+ update_attributes_for_gamemode,
+ )
+ .in_set(UpdateAttributesSet)
+ .chain(),
+ 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.
+ ///
+ /// The behavior of this depends on the target block,
+ /// and it'll either place the block you're holding in your hand or use the
+ /// block you clicked (like toggling a lever).
+ ///
+ /// Note that this may trigger anticheats as it doesn't take into account
+ /// whether you're actually looking at the block.
+ pub fn block_interact(&self, position: BlockPos) {
+ self.ecs.lock().send_event(StartUseItemEvent {
+ entity: self.entity,
+ hand: InteractionHand::MainHand,
+ force_block: Some(position),
+ });
+ }
+
+ /// Right-click the currently held item.
+ ///
+ /// If the item is consumable, then it'll act as if right-click was held
+ /// until the item finishes being consumed. You can use this to eat food.
+ ///
+ /// If we're looking at a block or entity, then it will be clicked. Also see
+ /// [`Client::block_interact`].
+ pub fn start_use_item(&self) {
+ self.ecs.lock().send_event(StartUseItemEvent {
+ entity: self.entity,
+ hand: InteractionHand::MainHand,
+ force_block: None,
+ });
+ }
+}
+
+/// A component that contains information about our local block state
+/// predictions.
+#[derive(Component, Clone, Debug, Default)]
+pub struct BlockStatePredictionHandler {
+ /// The total number of changes that this client has made to blocks.
+ seq: u32,
+ server_state: HashMap<BlockPos, ServerVerifiedState>,
+}
+#[derive(Clone, Debug)]
+struct ServerVerifiedState {
+ seq: u32,
+ block_state: BlockState,
+ /// Used for teleporting the player back if we're colliding with the block
+ /// that got placed back.
+ #[allow(unused)]
+ player_pos: Vec3,
+}
+
+impl BlockStatePredictionHandler {
+ /// Get the next sequence number that we're going to use and increment the
+ /// value.
+ pub fn start_predicting(&mut self) -> u32 {
+ self.seq += 1;
+ self.seq
+ }
+
+ /// Should be called right before the client updates a block with its
+ /// prediction.
+ ///
+ /// This is used to make sure that we can rollback to this state if the
+ /// server acknowledges the sequence number (with
+ /// [`ClientboundBlockChangedAck`]) without having sent a block update.
+ ///
+ /// [`ClientboundBlockChangedAck`]: azalea_protocol::packets::game::ClientboundBlockChangedAck
+ pub fn retain_known_server_state(
+ &mut self,
+ pos: BlockPos,
+ old_state: BlockState,
+ player_pos: Vec3,
+ ) {
+ self.server_state
+ .entry(pos)
+ .and_modify(|s| s.seq = self.seq)
+ .or_insert(ServerVerifiedState {
+ seq: self.seq,
+ block_state: old_state,
+ player_pos,
+ });
+ }
+
+ /// Save this update as the correct server state so when the server sends a
+ /// [`ClientboundBlockChangedAck`] we don't roll back this new update.
+ ///
+ /// This should be used when we receive a block update from the server.
+ ///
+ /// [`ClientboundBlockChangedAck`]: azalea_protocol::packets::game::ClientboundBlockChangedAck
+ pub fn update_known_server_state(&mut self, pos: BlockPos, state: BlockState) -> bool {
+ if let Some(s) = self.server_state.get_mut(&pos) {
+ s.block_state = state;
+ true
+ } else {
+ false
+ }
+ }
+
+ pub fn end_prediction_up_to(&mut self, seq: u32, world: &Instance) {
+ let mut to_remove = Vec::new();
+ for (pos, state) in &self.server_state {
+ if state.seq > seq {
+ continue;
+ }
+ to_remove.push(*pos);
+
+ // syncBlockState
+ let client_block_state = world.get_block_state(*pos).unwrap_or_default();
+ let server_block_state = state.block_state;
+ if client_block_state == server_block_state {
+ continue;
+ }
+ world.set_block_state(*pos, server_block_state);
+ // TODO: implement these two functions
+ // if is_colliding(player, *pos, server_block_state) {
+ // abs_snap_to(state.player_pos);
+ // }
+ }
+
+ for pos in to_remove {
+ self.server_state.remove(&pos);
+ }
+ }
+}
+
+/// An event that makes one of our clients simulate a right-click.
+///
+/// This event just inserts the [`StartUseItemQueued`] component on the given
+/// entity.
+#[doc(alias("right click"))]
+#[derive(Event)]
+pub struct StartUseItemEvent {
+ pub entity: Entity,
+ pub hand: InteractionHand,
+ /// See [`StartUseItemQueued::force_block`].
+ pub force_block: Option<BlockPos>,
+}
+pub fn handle_start_use_item_event(
+ mut commands: Commands,
+ mut events: EventReader<StartUseItemEvent>,
+) {
+ for event in events.read() {
+ commands.entity(event.entity).insert(StartUseItemQueued {
+ hand: event.hand,
+ force_block: event.force_block,
+ });
+ }
+}
+
+/// A component that makes our client simulate a right-click on the next
+/// [`GameTick`]. It's removed after that tick.
+///
+/// You may find it more convenient to use [`StartUseItemEvent`] instead, which
+/// just inserts this component for you.
+///
+/// [`GameTick`]: azalea_core::tick::GameTick
+#[derive(Component, Debug)]
+pub struct StartUseItemQueued {
+ pub hand: InteractionHand,
+ /// Optionally force us to send a [`ServerboundUseItemOn`] on the given
+ /// block.
+ ///
+ /// This is useful if you want to interact with a block without looking at
+ /// it, but should be avoided to stay compatible with anticheats.
+ pub force_block: Option<BlockPos>,
+}
+#[allow(clippy::type_complexity)]
+pub fn handle_start_use_item_queued(
+ mut commands: Commands,
+ query: Query<(
+ Entity,
+ &StartUseItemQueued,
+ &mut BlockStatePredictionHandler,
+ &HitResultComponent,
+ &LookDirection,
+ Option<&Mining>,
+ )>,
+ entity_id_query: Query<&MinecraftEntityId>,
+) {
+ for (entity, start_use_item, mut prediction_handler, hit_result, look_direction, mining) in
+ query
+ {
+ commands.entity(entity).remove::<StartUseItemQueued>();
+
+ if mining.is_some() {
+ warn!("Got a StartUseItemEvent for a client that was mining");
+ }
+
+ // TODO: this also skips if LocalPlayer.handsBusy is true, which is used when
+ // rowing a boat
+
+ 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 {
+ block_hit_result.block_pos == force_block
+ } else {
+ false
+ };
+
+ if !hit_result_matches {
+ // we're not looking at the block, so make up some numbers
+ hit_result = HitResult::Block(BlockHitResult {
+ location: force_block.center(),
+ direction: Direction::Up,
+ block_pos: force_block,
+ inside: false,
+ world_border: false,
+ miss: false,
+ });
+ }
+ }
+
+ match &hit_result {
+ HitResult::Block(r) => {
+ let seq = prediction_handler.start_predicting();
+ if r.miss {
+ commands.trigger(SendPacketEvent::new(
+ entity,
+ ServerboundUseItem {
+ hand: start_use_item.hand,
+ seq,
+ x_rot: look_direction.x_rot,
+ y_rot: look_direction.y_rot,
+ },
+ ));
+ } else {
+ commands.trigger(SendPacketEvent::new(
+ entity,
+ ServerboundUseItemOn {
+ hand: start_use_item.hand,
+ 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.
+ }
+ }
+ HitResult::Entity(r) => {
+ // TODO: worldborder check
+
+ 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,
+ },
+ ));
+ }
+ }
+ }
+}
+
+/// Whether we can't interact with the block, based on your gamemode. If
+/// this is false, then we can interact with the block.
+///
+/// Passing the inventory, block position, and instance is necessary for the
+/// adventure mode check.
+pub fn check_is_interaction_restricted(
+ instance: &Instance,
+ block_pos: BlockPos,
+ game_mode: &GameMode,
+ inventory: &Inventory,
+) -> bool {
+ match game_mode {
+ GameMode::Adventure => {
+ // vanilla checks for abilities.mayBuild here but servers have no
+ // way of modifying that
+
+ let held_item = inventory.held_item();
+ match &held_item {
+ ItemStack::Present(item) => {
+ let block = instance.chunks.get_block_state(block_pos);
+ let Some(block) = block else {
+ // block isn't loaded so just say that it is restricted
+ return true;
+ };
+ check_block_can_be_broken_by_item_in_adventure_mode(item, &block)
+ }
+ _ => true,
+ }
+ }
+ GameMode::Spectator => true,
+ _ => false,
+ }
+}
+
+/// Check if the item has the `CanDestroy` tag for the block.
+pub fn check_block_can_be_broken_by_item_in_adventure_mode(
+ item: &ItemStackData,
+ _block: &BlockState,
+) -> bool {
+ // minecraft caches the last checked block but that's kind of an unnecessary
+ // optimization and makes the code too complicated
+
+ if !item.components.has::<components::CanBreak>() {
+ // no CanDestroy tag
+ return false;
+ };
+
+ false
+
+ // for block_predicate in can_destroy {
+ // // TODO
+ // // defined in BlockPredicateArgument.java
+ // }
+
+ // true
+}
+
+pub fn can_use_game_master_blocks(
+ abilities: &PlayerAbilities,
+ permission_level: &PermissionLevel,
+) -> bool {
+ abilities.instant_break && **permission_level >= 2
+}
+
+/// Swing your arm. This is purely a visual effect and won't interact with
+/// anything in the world.
+#[derive(Event, Clone, Debug)]
+pub struct SwingArmEvent {
+ pub entity: Entity,
+}
+pub fn handle_swing_arm_trigger(trigger: Trigger<SwingArmEvent>, mut commands: Commands) {
+ commands.trigger(SendPacketEvent::new(
+ trigger.event().entity,
+ ServerboundSwing {
+ hand: InteractionHand::MainHand,
+ },
+ ));
+}
+pub fn handle_swing_arm_event(mut events: EventReader<SwingArmEvent>, mut commands: Commands) {
+ for event in events.read() {
+ commands.trigger(event.clone());
+ }
+}
+
+#[allow(clippy::type_complexity)]
+fn update_attributes_for_held_item(
+ mut query: Query<(&mut Attributes, &Inventory), (With<LocalEntity>, Changed<Inventory>)>,
+) {
+ for (mut attributes, inventory) in &mut query {
+ let held_item = inventory.held_item();
+
+ use azalea_registry::Item;
+ let added_attack_speed = match held_item.kind() {
+ Item::WoodenSword => -2.4,
+ Item::WoodenShovel => -3.0,
+ Item::WoodenPickaxe => -2.8,
+ Item::WoodenAxe => -3.2,
+ Item::WoodenHoe => -3.0,
+
+ Item::StoneSword => -2.4,
+ Item::StoneShovel => -3.0,
+ Item::StonePickaxe => -2.8,
+ Item::StoneAxe => -3.2,
+ Item::StoneHoe => -2.0,
+
+ Item::GoldenSword => -2.4,
+ Item::GoldenShovel => -3.0,
+ Item::GoldenPickaxe => -2.8,
+ Item::GoldenAxe => -3.0,
+ Item::GoldenHoe => -3.0,
+
+ Item::IronSword => -2.4,
+ Item::IronShovel => -3.0,
+ Item::IronPickaxe => -2.8,
+ Item::IronAxe => -3.1,
+ Item::IronHoe => -1.0,
+
+ Item::DiamondSword => -2.4,
+ Item::DiamondShovel => -3.0,
+ Item::DiamondPickaxe => -2.8,
+ Item::DiamondAxe => -3.0,
+ Item::DiamondHoe => 0.0,
+
+ Item::NetheriteSword => -2.4,
+ Item::NetheriteShovel => -3.0,
+ Item::NetheritePickaxe => -2.8,
+ Item::NetheriteAxe => -3.0,
+ Item::NetheriteHoe => 0.0,
+
+ Item::Trident => -2.9,
+ _ => 0.,
+ };
+ attributes
+ .attack_speed
+ .insert(azalea_entity::attributes::base_attack_speed_modifier(
+ added_attack_speed,
+ ));
+ }
+}
+
+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
+}