azalea_physics/
clip.rs

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