azalea_client/plugins/interact/
pick.rs1use 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#[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
108pub fn pick(opts: PickOpts<'_, '_, '_, '_, '_>) -> HitResult {
116 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 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
201pub 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
236fn 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 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 result = Some(EntityHitResult {
274 location: clip_location,
275 entity: candidate,
276 });
277 picked_distance_squared = distance_squared;
278 }
279 }
280 }
281
282 result
283}