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, 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#[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
107pub fn pick(opts: PickOpts<'_, '_, '_, '_, '_>) -> HitResult {
115 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 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
200pub 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
235fn 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 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 result = Some(EntityHitResult {
273 location: clip_location,
274 entity: candidate,
275 });
276 picked_distance_squared = distance_squared;
277 }
278 }
279 }
280
281 result
282}