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