azalea_client/plugins/interact/
pick.rs

1use azalea_core::{
2    aabb::AABB,
3    direction::Direction,
4    hit_result::{BlockHitResult, EntityHitResult, HitResult},
5    position::Vec3,
6};
7use azalea_entity::{
8    Attributes, Dead, LocalEntity, LookDirection, Physics, Position,
9    dimensions::EntityDimensions,
10    metadata::{ArmorStandMarker, Marker},
11    view_vector,
12};
13use azalea_physics::{
14    clip::{BlockShapeType, ClipContext, FluidPickType},
15    collision::entity_collisions::{PhysicsQuery, get_entities},
16};
17use azalea_world::{Instance, InstanceContainer, InstanceName};
18use bevy_ecs::prelude::*;
19use derive_more::{Deref, DerefMut};
20
21/// A component that contains the block or entity that the player is currently
22/// looking at.
23#[doc(alias("looking at", "looking at block", "crosshair"))]
24#[derive(Component, Clone, Debug, Deref, DerefMut)]
25pub struct HitResultComponent(HitResult);
26
27#[allow(clippy::type_complexity)]
28pub fn update_hit_result_component(
29    mut commands: Commands,
30    mut query: Query<
31        (
32            Entity,
33            Option<&mut HitResultComponent>,
34            &Position,
35            &EntityDimensions,
36            &LookDirection,
37            &InstanceName,
38            &Physics,
39            &Attributes,
40        ),
41        With<LocalEntity>,
42    >,
43    instance_container: Res<InstanceContainer>,
44    physics_query: PhysicsQuery,
45    pickable_query: PickableEntityQuery,
46) {
47    for (
48        entity,
49        hit_result_ref,
50        position,
51        dimensions,
52        look_direction,
53        world_name,
54        physics,
55        attributes,
56    ) in &mut query
57    {
58        let block_pick_range = attributes.block_interaction_range.calculate();
59        let entity_pick_range = attributes.entity_interaction_range.calculate();
60
61        let eye_position = position.up(dimensions.eye_height.into());
62
63        let Some(world_lock) = instance_container.get(world_name) else {
64            continue;
65        };
66        let world = world_lock.read();
67
68        let hit_result = pick(PickOpts {
69            source_entity: entity,
70            look_direction: *look_direction,
71            eye_position,
72            aabb: &physics.bounding_box,
73            world: &world,
74            entity_pick_range,
75            block_pick_range,
76            physics_query: &physics_query,
77            pickable_query: &pickable_query,
78        });
79        if let Some(mut hit_result_ref) = hit_result_ref {
80            **hit_result_ref = hit_result;
81        } else {
82            commands
83                .entity(entity)
84                .insert(HitResultComponent(hit_result));
85        }
86    }
87}
88
89pub type PickableEntityQuery<'world, 'state, 'a> = Query<
90    'world,
91    'state,
92    Option<&'a ArmorStandMarker>,
93    (Without<Dead>, Without<Marker>, Without<LocalEntity>),
94>;
95
96pub struct PickOpts<'world, 'state, 'a, 'b, 'c> {
97    source_entity: Entity,
98    look_direction: LookDirection,
99    eye_position: Vec3,
100    aabb: &'a AABB,
101    world: &'a Instance,
102    entity_pick_range: f64,
103    block_pick_range: f64,
104    physics_query: &'a PhysicsQuery<'world, 'state, 'b>,
105    pickable_query: &'a PickableEntityQuery<'world, 'state, 'c>,
106}
107
108/// Get the block or entity that a player would be looking at if their eyes were
109/// at the given direction and position.
110///
111/// If you need to get the block/entity the player is looking at right now, use
112/// [`HitResultComponent`].
113///
114/// Also see [`pick_block`].
115pub fn pick(opts: PickOpts<'_, '_, '_, '_, '_>) -> HitResult {
116    // vanilla does extra math here to calculate the pick result in between ticks by
117    // interpolating, but since clients can still only interact on exact ticks, that
118    // isn't relevant for us.
119
120    let mut max_range = opts.entity_pick_range.max(opts.block_pick_range);
121    let mut max_range_squared = max_range.powi(2);
122
123    let block_hit_result = pick_block(
124        opts.look_direction,
125        opts.eye_position,
126        &opts.world.chunks,
127        max_range,
128    );
129    let block_hit_result_dist_squared = block_hit_result
130        .location
131        .distance_squared_to(opts.eye_position);
132    if !block_hit_result.miss {
133        max_range_squared = block_hit_result_dist_squared;
134        max_range = block_hit_result_dist_squared.sqrt();
135    }
136
137    let view_vector = view_vector(opts.look_direction);
138    let end_position = opts.eye_position + (view_vector * max_range);
139    let inflate_by = 1.;
140    let pick_aabb = opts
141        .aabb
142        .expand_towards(view_vector * max_range)
143        .inflate_all(inflate_by);
144
145    let is_pickable = |entity: Entity| {
146        // TODO: ender dragon and projectiles have extra logic here. also, we shouldn't
147        // be able to pick spectators.
148        if let Ok(armor_stand_marker) = opts.pickable_query.get(entity) {
149            if let Some(armor_stand_marker) = armor_stand_marker
150                && armor_stand_marker.0
151            {
152                false
153            } else {
154                true
155            }
156        } else {
157            true
158        }
159    };
160    let entity_hit_result = pick_entity(PickEntityOpts {
161        source_entity: opts.source_entity,
162        eye_position: opts.eye_position,
163        end_position,
164        world: opts.world,
165        pick_range_squared: max_range_squared,
166        predicate: &is_pickable,
167        aabb: &pick_aabb,
168        physics_query: opts.physics_query,
169    });
170
171    if let Some(entity_hit_result) = entity_hit_result
172        && entity_hit_result
173            .location
174            .distance_squared_to(opts.eye_position)
175            < block_hit_result_dist_squared
176    {
177        filter_hit_result(
178            HitResult::Entity(entity_hit_result),
179            opts.eye_position,
180            opts.entity_pick_range,
181        )
182    } else {
183        filter_hit_result(
184            HitResult::Block(block_hit_result),
185            opts.eye_position,
186            opts.block_pick_range,
187        )
188    }
189}
190
191fn filter_hit_result(hit_result: HitResult, eye_position: Vec3, range: f64) -> HitResult {
192    let location = hit_result.location();
193    if !location.closer_than(eye_position, range) {
194        let direction = Direction::nearest(location - eye_position);
195        HitResult::new_miss(location, direction, location.into())
196    } else {
197        hit_result
198    }
199}
200
201/// Get the block that a player would be looking at if their eyes were at the
202/// given direction and position.
203///
204/// Also see [`pick`].
205pub fn pick_block(
206    look_direction: LookDirection,
207    eye_position: Vec3,
208    chunks: &azalea_world::ChunkStorage,
209    pick_range: f64,
210) -> BlockHitResult {
211    let view_vector = view_vector(look_direction);
212    let end_position = eye_position + (view_vector * pick_range);
213
214    azalea_physics::clip::clip(
215        chunks,
216        ClipContext {
217            from: eye_position,
218            to: end_position,
219            block_shape_type: BlockShapeType::Outline,
220            fluid_pick_type: FluidPickType::None,
221        },
222    )
223}
224
225struct PickEntityOpts<'world, 'state, 'a, 'b> {
226    source_entity: Entity,
227    eye_position: Vec3,
228    end_position: Vec3,
229    world: &'a azalea_world::Instance,
230    pick_range_squared: f64,
231    predicate: &'a dyn Fn(Entity) -> bool,
232    aabb: &'a AABB,
233    physics_query: &'a PhysicsQuery<'world, 'state, 'b>,
234}
235
236// port of getEntityHitResult
237fn pick_entity(opts: PickEntityOpts) -> Option<EntityHitResult> {
238    let mut picked_distance_squared = opts.pick_range_squared;
239    let mut result = None;
240
241    for (candidate, candidate_aabb) in get_entities(
242        opts.world,
243        Some(opts.source_entity),
244        opts.aabb,
245        opts.predicate,
246        opts.physics_query,
247    ) {
248        // TODO: if the entity is "REDIRECTABLE_PROJECTILE" then this should be 1.0.
249        // azalea needs support for entity tags first for this to be possible. see
250        // getPickRadius in decompiled minecraft source
251        let candidate_pick_radius = 0.;
252        let candidate_aabb = candidate_aabb.inflate_all(candidate_pick_radius);
253        let clip_location = candidate_aabb.clip(opts.eye_position, opts.end_position);
254
255        if candidate_aabb.contains(opts.eye_position) {
256            if picked_distance_squared >= 0. {
257                result = Some(EntityHitResult {
258                    location: clip_location.unwrap_or(opts.eye_position),
259                    entity: candidate,
260                });
261                picked_distance_squared = 0.;
262            }
263        } else if let Some(clip_location) = clip_location {
264            let distance_squared = opts.eye_position.distance_squared_to(clip_location);
265            if distance_squared < picked_distance_squared || picked_distance_squared == 0. {
266                // TODO: don't pick the entity we're riding on
267                // if candidate_root_vehicle == entity_root_vehicle {
268                //     if picked_distance_squared == 0. {
269                //         picked_entity = Some(candidate);
270                //         picked_location = Some(clip_location);
271                //     }
272                // } else {
273                result = Some(EntityHitResult {
274                    location: clip_location,
275                    entity: candidate,
276                });
277                picked_distance_squared = distance_squared;
278            }
279        }
280    }
281
282    result
283}