azalea_physics/
clip.rs

1use std::collections::HashSet;
2
3use azalea_block::{
4    BlockState,
5    fluid_state::{FluidKind, FluidState},
6};
7use azalea_core::{
8    aabb::Aabb,
9    direction::{Axis, Direction},
10    hit_result::BlockHitResult,
11    math::{self, EPSILON, lerp},
12    position::{BlockPos, Vec3},
13};
14use azalea_registry::{builtin::BlockKind, tags};
15use azalea_world::ChunkStorage;
16
17use crate::collision::{BlockWithShape, EMPTY_SHAPE, VoxelShape};
18
19#[derive(Clone, Debug)]
20pub struct ClipContext {
21    pub from: Vec3,
22    pub to: Vec3,
23    pub block_shape_type: BlockShapeType,
24    pub fluid_pick_type: FluidPickType,
25    // pub collision_context: EntityCollisionContext,
26}
27impl ClipContext {
28    /// Get the shape of given block, using the type of shape set in
29    /// [`Self::block_shape_type`].
30    pub fn block_shape(&self, block_state: BlockState) -> &VoxelShape {
31        // minecraft passes in the world and blockpos to this function but it's not
32        // actually necessary. it is for fluid_shape though
33        match self.block_shape_type {
34            BlockShapeType::Collider => block_state.collision_shape(),
35            BlockShapeType::Outline => block_state.outline_shape(),
36            BlockShapeType::Visual => block_state.collision_shape(),
37            BlockShapeType::FallDamageResetting => {
38                if tags::blocks::FALL_DAMAGE_RESETTING.contains(&BlockKind::from(block_state)) {
39                    block_state.collision_shape()
40                } else {
41                    &EMPTY_SHAPE
42                }
43            }
44        }
45    }
46
47    pub fn fluid_shape(
48        &self,
49        fluid_state: FluidState,
50        world: &ChunkStorage,
51        pos: BlockPos,
52    ) -> &VoxelShape {
53        if self.fluid_pick_type.can_pick(&fluid_state) {
54            crate::collision::fluid_shape(&fluid_state, world, pos)
55        } else {
56            &EMPTY_SHAPE
57        }
58    }
59}
60
61#[derive(Clone, Copy, Debug)]
62pub enum BlockShapeType {
63    /// The shape that's used for collision.
64    Collider,
65    /// The block outline that renders when your cursor is over a block.
66    Outline,
67    /// Used by entities when considering their line of sight.
68    ///
69    /// TODO: visual block shape isn't implemented (it'll just return the
70    /// collider shape), that's correct for most blocks though
71    Visual,
72    FallDamageResetting,
73}
74#[derive(Clone, Copy, Debug)]
75pub enum FluidPickType {
76    None,
77    SourceOnly,
78    Any,
79    Water,
80}
81impl FluidPickType {
82    pub fn can_pick(&self, fluid_state: &FluidState) -> bool {
83        match self {
84            Self::None => false,
85            Self::SourceOnly => fluid_state.amount == 8,
86            Self::Any => fluid_state.kind != FluidKind::Empty,
87            Self::Water => fluid_state.kind == FluidKind::Water,
88        }
89    }
90}
91
92pub fn clip(chunk_storage: &ChunkStorage, context: ClipContext) -> BlockHitResult {
93    traverse_blocks(
94        context.from,
95        context.to,
96        context,
97        |ctx, block_pos| {
98            let block_state = chunk_storage.get_block_state(block_pos).unwrap_or_default();
99            let fluid_state = FluidState::from(block_state);
100
101            let block_shape = ctx.block_shape(block_state);
102            let interaction_clip = clip_with_interaction_override(
103                ctx.from,
104                ctx.to,
105                block_pos,
106                block_shape,
107                block_state,
108            );
109            let fluid_shape = ctx.fluid_shape(fluid_state, chunk_storage, block_pos);
110            let fluid_clip = fluid_shape.clip(ctx.from, ctx.to, block_pos);
111
112            let distance_to_interaction = interaction_clip
113                .as_ref()
114                .map(|hit| ctx.from.distance_squared_to(hit.location))
115                .unwrap_or(f64::MAX);
116            let distance_to_fluid = fluid_clip
117                .as_ref()
118                .map(|hit| ctx.from.distance_squared_to(hit.location))
119                .unwrap_or(f64::MAX);
120
121            if distance_to_interaction <= distance_to_fluid {
122                interaction_clip
123            } else {
124                fluid_clip
125            }
126        },
127        |context| {
128            let vec = context.from - context.to;
129            BlockHitResult::miss(context.to, Direction::nearest(vec))
130        },
131    )
132}
133
134fn clip_with_interaction_override(
135    from: Vec3,
136    to: Vec3,
137    block_pos: BlockPos,
138    block_shape: &VoxelShape,
139    _block_state: BlockState,
140) -> Option<BlockHitResult> {
141    let block_hit_result = block_shape.clip(from, to, block_pos);
142
143    if let Some(block_hit_result) = block_hit_result {
144        // TODO: minecraft calls .getInteractionShape here
145        // getInteractionShape is empty for almost every shape except cauldons,
146        // compostors, hoppers, and scaffolding.
147        let interaction_shape = &*EMPTY_SHAPE;
148        let interaction_hit_result = interaction_shape.clip(from, to, block_pos);
149        if let Some(interaction_hit_result) = interaction_hit_result
150            && interaction_hit_result.location.distance_squared_to(from)
151                < block_hit_result.location.distance_squared_to(from)
152        {
153            return Some(block_hit_result.with_direction(interaction_hit_result.direction));
154        }
155
156        Some(block_hit_result)
157    } else {
158        None
159    }
160}
161
162pub fn traverse_blocks<C, T>(
163    from: Vec3,
164    to: Vec3,
165    context: C,
166    get_hit_result: impl Fn(&C, BlockPos) -> Option<T>,
167    get_miss_result: impl Fn(&C) -> T,
168) -> T {
169    if from == to {
170        return get_miss_result(&context);
171    }
172
173    let right_after_end = Vec3 {
174        x: lerp(-EPSILON, to.x, from.x),
175        y: lerp(-EPSILON, to.y, from.y),
176        z: lerp(-EPSILON, to.z, from.z),
177    };
178
179    let right_before_start = Vec3 {
180        x: lerp(-EPSILON, from.x, to.x),
181        y: lerp(-EPSILON, from.y, to.y),
182        z: lerp(-EPSILON, from.z, to.z),
183    };
184
185    let mut current_block = BlockPos::from(right_before_start);
186    if let Some(data) = get_hit_result(&context, current_block) {
187        return data;
188    }
189
190    let vec = right_after_end - right_before_start;
191
192    let vec_sign = Vec3 {
193        x: math::sign(vec.x),
194        y: math::sign(vec.y),
195        z: math::sign(vec.z),
196    };
197
198    #[rustfmt::skip]
199    let percentage_step = Vec3 {
200        x: if vec_sign.x == 0. { f64::MAX } else { vec_sign.x / vec.x },
201        y: if vec_sign.y == 0. { f64::MAX } else { vec_sign.y / vec.y },
202        z: if vec_sign.z == 0. { f64::MAX } else { vec_sign.z / vec.z },
203    };
204
205    let mut percentage = Vec3 {
206        x: percentage_step.x
207            * if vec_sign.x > 0. {
208                1. - math::fract(right_before_start.x)
209            } else {
210                math::fract(right_before_start.x)
211            },
212        y: percentage_step.y
213            * if vec_sign.y > 0. {
214                1. - math::fract(right_before_start.y)
215            } else {
216                math::fract(right_before_start.y)
217            },
218        z: percentage_step.z
219            * if vec_sign.z > 0. {
220                1. - math::fract(right_before_start.z)
221            } else {
222                math::fract(right_before_start.z)
223            },
224    };
225
226    loop {
227        if percentage.x > 1. && percentage.y > 1. && percentage.z > 1. {
228            return get_miss_result(&context);
229        }
230
231        if percentage.x < percentage.y {
232            if percentage.x < percentage.z {
233                current_block.x += vec_sign.x as i32;
234                percentage.x += percentage_step.x;
235            } else {
236                current_block.z += vec_sign.z as i32;
237                percentage.z += percentage_step.z;
238            }
239        } else if percentage.y < percentage.z {
240            current_block.y += vec_sign.y as i32;
241            percentage.y += percentage_step.y;
242        } else {
243            current_block.z += vec_sign.z as i32;
244            percentage.z += percentage_step.z;
245        }
246
247        if let Some(data) = get_hit_result(&context, current_block) {
248            return data;
249        }
250    }
251}
252
253pub fn box_traverse_blocks(from: Vec3, to: Vec3, aabb: &Aabb) -> HashSet<BlockPos> {
254    let delta = to - from;
255    let traversed_blocks = BlockPos::between_closed_aabb(aabb);
256    if delta.length_squared() < (0.99999_f32 * 0.99999) as f64 {
257        return traversed_blocks.into_iter().collect();
258    }
259
260    let mut traversed_and_collided_blocks = HashSet::new();
261    let target_min_pos = aabb.min;
262    let from_min_pos = target_min_pos - delta;
263    add_collisions_along_travel(
264        &mut traversed_and_collided_blocks,
265        from_min_pos,
266        target_min_pos,
267        *aabb,
268    );
269    traversed_and_collided_blocks.extend(traversed_blocks);
270    traversed_and_collided_blocks
271}
272
273pub fn add_collisions_along_travel(
274    collisions: &mut HashSet<BlockPos>,
275    from: Vec3,
276    to: Vec3,
277    aabb: Aabb,
278) {
279    let delta = to - from;
280    let mut min_x = from.x.floor() as i32;
281    let mut min_y = from.y.floor() as i32;
282    let mut min_z = from.z.floor() as i32;
283    let direction_x = math::sign_as_int(delta.x);
284    let direction_y = math::sign_as_int(delta.y);
285    let direction_z = math::sign_as_int(delta.z);
286    let step_x = if direction_x == 0 {
287        f64::MAX
288    } else {
289        direction_x as f64 / delta.x
290    };
291    let step_y = if direction_y == 0 {
292        f64::MAX
293    } else {
294        direction_y as f64 / delta.y
295    };
296    let step_z = if direction_z == 0 {
297        f64::MAX
298    } else {
299        direction_z as f64 / delta.z
300    };
301    let mut cur_x = step_x
302        * if direction_x > 0 {
303            1. - math::fract(from.x)
304        } else {
305            math::fract(from.x)
306        };
307    let mut cur_y = step_y
308        * if direction_y > 0 {
309            1. - math::fract(from.y)
310        } else {
311            math::fract(from.y)
312        };
313    let mut cur_z = step_z
314        * if direction_z > 0 {
315            1. - math::fract(from.z)
316        } else {
317            math::fract(from.z)
318        };
319    let mut step_count = 0;
320
321    while cur_x <= 1. || cur_y <= 1. || cur_z <= 1. {
322        if cur_x < cur_y {
323            if cur_x < cur_z {
324                min_x += direction_x;
325                cur_x += step_x;
326            } else {
327                min_z += direction_z;
328                cur_z += step_z;
329            }
330        } else if cur_y < cur_z {
331            min_y += direction_y;
332            cur_y += step_y;
333        } else {
334            min_z += direction_z;
335            cur_z += step_z;
336        }
337
338        if step_count > 16 {
339            break;
340        }
341        step_count += 1;
342
343        let Some(clip_location) = Aabb::clip_with_from_and_to(
344            Vec3::new(min_x as f64, min_y as f64, min_z as f64),
345            Vec3::new((min_x + 1) as f64, (min_y + 1) as f64, (min_z + 1) as f64),
346            from,
347            to,
348        ) else {
349            continue;
350        };
351
352        let initial_max_x = clip_location
353            .x
354            .clamp(min_x as f64 + 1.0E-5, min_x as f64 + 1.0 - 1.0E-5);
355        let initial_max_y = clip_location
356            .y
357            .clamp(min_y as f64 + 1.0E-5, min_y as f64 + 1.0 - 1.0E-5);
358        let initial_max_z = clip_location
359            .z
360            .clamp(min_z as f64 + 1.0E-5, min_z as f64 + 1.0 - 1.0E-5);
361        let max_x = (initial_max_x + aabb.get_size(Axis::X)).floor() as i32;
362        let max_y = (initial_max_y + aabb.get_size(Axis::Y)).floor() as i32;
363        let max_z = (initial_max_z + aabb.get_size(Axis::Z)).floor() as i32;
364
365        for x in min_x..=max_x {
366            for y in min_y..=max_y {
367                for z in min_z..=max_z {
368                    collisions.insert(BlockPos::new(x, y, z));
369                }
370            }
371        }
372    }
373}