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