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    block_hit_result::BlockHitResult,
10    direction::{Axis, Direction},
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                .map(|hit| ctx.from.distance_squared_to(&hit.location))
115                .unwrap_or(f64::MAX);
116            let distance_to_fluid = fluid_clip
117                .map(|hit| ctx.from.distance_squared_to(&hit.location))
118                .unwrap_or(f64::MAX);
119
120            if distance_to_interaction <= distance_to_fluid {
121                interaction_clip
122            } else {
123                fluid_clip
124            }
125        },
126        |context| {
127            let vec = context.from - context.to;
128            BlockHitResult::miss(
129                context.to,
130                Direction::nearest(vec),
131                BlockPos::from(context.to),
132            )
133        },
134    )
135}
136
137fn clip_with_interaction_override(
138    from: &Vec3,
139    to: &Vec3,
140    block_pos: &BlockPos,
141    block_shape: &VoxelShape,
142    _block_state: &BlockState,
143) -> Option<BlockHitResult> {
144    let block_hit_result = block_shape.clip(from, to, block_pos);
145
146    if let Some(block_hit_result) = block_hit_result {
147        // TODO: minecraft calls .getInteractionShape here
148        // getInteractionShape is empty for almost every shape except cauldons,
149        // compostors, hoppers, and scaffolding.
150        let interaction_shape = &*EMPTY_SHAPE;
151        let interaction_hit_result = interaction_shape.clip(from, to, block_pos);
152        if let Some(interaction_hit_result) = interaction_hit_result {
153            if interaction_hit_result.location.distance_squared_to(from)
154                < block_hit_result.location.distance_squared_to(from)
155            {
156                return Some(block_hit_result.with_direction(interaction_hit_result.direction));
157            }
158        }
159
160        Some(block_hit_result)
161    } else {
162        None
163    }
164}
165
166pub fn traverse_blocks<C, T>(
167    from: Vec3,
168    to: Vec3,
169    context: C,
170    get_hit_result: impl Fn(&C, &BlockPos) -> Option<T>,
171    get_miss_result: impl Fn(&C) -> T,
172) -> T {
173    if from == to {
174        return get_miss_result(&context);
175    }
176
177    let right_after_end = Vec3 {
178        x: lerp(-EPSILON, to.x, from.x),
179        y: lerp(-EPSILON, to.y, from.y),
180        z: lerp(-EPSILON, to.z, from.z),
181    };
182
183    let right_before_start = Vec3 {
184        x: lerp(-EPSILON, from.x, to.x),
185        y: lerp(-EPSILON, from.y, to.y),
186        z: lerp(-EPSILON, from.z, to.z),
187    };
188
189    let mut current_block = BlockPos::from(right_before_start);
190    if let Some(data) = get_hit_result(&context, &current_block) {
191        return data;
192    }
193
194    let vec = right_after_end - right_before_start;
195
196    let vec_sign = Vec3 {
197        x: math::sign(vec.x),
198        y: math::sign(vec.y),
199        z: math::sign(vec.z),
200    };
201
202    #[rustfmt::skip]
203    let percentage_step = Vec3 {
204        x: if vec_sign.x == 0. { f64::MAX } else { vec_sign.x / vec.x },
205        y: if vec_sign.y == 0. { f64::MAX } else { vec_sign.y / vec.y },
206        z: if vec_sign.z == 0. { f64::MAX } else { vec_sign.z / vec.z },
207    };
208
209    let mut percentage = Vec3 {
210        x: percentage_step.x
211            * if vec_sign.x > 0. {
212                1. - math::fract(right_before_start.x)
213            } else {
214                math::fract(right_before_start.x)
215            },
216        y: percentage_step.y
217            * if vec_sign.y > 0. {
218                1. - math::fract(right_before_start.y)
219            } else {
220                math::fract(right_before_start.y)
221            },
222        z: percentage_step.z
223            * if vec_sign.z > 0. {
224                1. - math::fract(right_before_start.z)
225            } else {
226                math::fract(right_before_start.z)
227            },
228    };
229
230    loop {
231        if percentage.x > 1. && percentage.y > 1. && percentage.z > 1. {
232            return get_miss_result(&context);
233        }
234
235        if percentage.x < percentage.y {
236            if percentage.x < percentage.z {
237                current_block.x += vec_sign.x as i32;
238                percentage.x += percentage_step.x;
239            } else {
240                current_block.z += vec_sign.z as i32;
241                percentage.z += percentage_step.z;
242            }
243        } else if percentage.y < percentage.z {
244            current_block.y += vec_sign.y as i32;
245            percentage.y += percentage_step.y;
246        } else {
247            current_block.z += vec_sign.z as i32;
248            percentage.z += percentage_step.z;
249        }
250
251        if let Some(data) = get_hit_result(&context, &current_block) {
252            return data;
253        }
254    }
255}
256
257pub fn box_traverse_blocks(from: &Vec3, to: &Vec3, aabb: &AABB) -> HashSet<BlockPos> {
258    let delta = to - from;
259    let traversed_blocks = BlockPos::between_closed_aabb(aabb);
260    if delta.length_squared() < (0.99999_f32 * 0.99999) as f64 {
261        return traversed_blocks.into_iter().collect();
262    }
263
264    let mut traversed_and_collided_blocks = HashSet::new();
265    let target_min_pos = aabb.min;
266    let from_min_pos = target_min_pos - delta;
267    add_collisions_along_travel(
268        &mut traversed_and_collided_blocks,
269        from_min_pos,
270        target_min_pos,
271        *aabb,
272    );
273    traversed_and_collided_blocks.extend(traversed_blocks);
274    traversed_and_collided_blocks
275}
276
277pub fn add_collisions_along_travel(
278    collisions: &mut HashSet<BlockPos>,
279    from: Vec3,
280    to: Vec3,
281    aabb: AABB,
282) {
283    let delta = to - from;
284    let mut min_x = from.x.floor() as i32;
285    let mut min_y = from.y.floor() as i32;
286    let mut min_z = from.z.floor() as i32;
287    let direction_x = math::sign_as_int(delta.x);
288    let direction_y = math::sign_as_int(delta.y);
289    let direction_z = math::sign_as_int(delta.z);
290    let step_x = if direction_x == 0 {
291        f64::MAX
292    } else {
293        direction_x as f64 / delta.x
294    };
295    let step_y = if direction_y == 0 {
296        f64::MAX
297    } else {
298        direction_y as f64 / delta.y
299    };
300    let step_z = if direction_z == 0 {
301        f64::MAX
302    } else {
303        direction_z as f64 / delta.z
304    };
305    let mut cur_x = step_x
306        * if direction_x > 0 {
307            1. - math::fract(from.x)
308        } else {
309            math::fract(from.x)
310        };
311    let mut cur_y = step_y
312        * if direction_y > 0 {
313            1. - math::fract(from.y)
314        } else {
315            math::fract(from.y)
316        };
317    let mut cur_z = step_z
318        * if direction_z > 0 {
319            1. - math::fract(from.z)
320        } else {
321            math::fract(from.z)
322        };
323    let mut step_count = 0;
324
325    while cur_x <= 1. || cur_y <= 1. || cur_z <= 1. {
326        if cur_x < cur_y {
327            if cur_x < cur_z {
328                min_x += direction_x;
329                cur_x += step_x;
330            } else {
331                min_z += direction_z;
332                cur_z += step_z;
333            }
334        } else if cur_y < cur_z {
335            min_y += direction_y;
336            cur_y += step_y;
337        } else {
338            min_z += direction_z;
339            cur_z += step_z;
340        }
341
342        if step_count > 16 {
343            break;
344        }
345        step_count += 1;
346
347        let Some(clip_location) = AABB::clip_with_from_and_to(
348            &Vec3::new(min_x as f64, min_y as f64, min_z as f64),
349            &Vec3::new((min_x + 1) as f64, (min_y + 1) as f64, (min_z + 1) as f64),
350            &from,
351            &to,
352        ) else {
353            continue;
354        };
355
356        let initial_max_x = clip_location
357            .x
358            .clamp(min_x as f64 + 1.0E-5, min_x as f64 + 1.0 - 1.0E-5);
359        let initial_max_y = clip_location
360            .y
361            .clamp(min_y as f64 + 1.0E-5, min_y as f64 + 1.0 - 1.0E-5);
362        let initial_max_z = clip_location
363            .z
364            .clamp(min_z as f64 + 1.0E-5, min_z as f64 + 1.0 - 1.0E-5);
365        let max_x = (initial_max_x + aabb.get_size(Axis::X)).floor() as i32;
366        let max_y = (initial_max_y + aabb.get_size(Axis::Y)).floor() as i32;
367        let max_z = (initial_max_z + aabb.get_size(Axis::Z)).floor() as i32;
368
369        for x in min_x..=max_x {
370            for y in min_y..=max_y {
371                for z in min_z..=max_z {
372                    collisions.insert(BlockPos::new(x, y, z));
373                }
374            }
375        }
376    }
377}