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