azalea_physics/collision/
world_collisions.rs

1use std::{collections::HashSet, sync::Arc};
2
3use azalea_block::{BlockState, fluid_state::FluidState};
4use azalea_core::{
5    cursor3d::{Cursor3d, CursorIteration, CursorIterationType},
6    math::EPSILON,
7    position::{BlockPos, ChunkBlockPos, ChunkPos, ChunkSectionBlockPos, ChunkSectionPos, Vec3},
8};
9use azalea_inventory::ItemStack;
10use azalea_world::{Chunk, Instance};
11use bevy_ecs::entity::Entity;
12use parking_lot::RwLock;
13
14use super::{BLOCK_SHAPE, Shapes};
15use crate::collision::{AABB, BlockWithShape, VoxelShape};
16
17pub fn get_block_collisions(world: &Instance, aabb: &AABB) -> Vec<VoxelShape> {
18    let mut state = BlockCollisionsState::new(world, aabb, EntityCollisionContext::of(None));
19    let mut block_collisions = Vec::new();
20
21    let initial_chunk_pos = ChunkPos::from(state.cursor.origin());
22    let initial_chunk = world.chunks.get(&initial_chunk_pos);
23    let initial_chunk = initial_chunk.as_deref().map(RwLock::read);
24
25    while let Some(item) = state.cursor.next() {
26        state.compute_next(
27            item,
28            &mut block_collisions,
29            initial_chunk_pos,
30            initial_chunk.as_deref(),
31        );
32    }
33
34    block_collisions
35}
36
37pub fn get_block_and_liquid_collisions(world: &Instance, aabb: &AABB) -> Vec<VoxelShape> {
38    let mut state = BlockCollisionsState::new(
39        world,
40        aabb,
41        EntityCollisionContext::of(None).with_include_liquids(true),
42    );
43    let mut block_collisions = Vec::new();
44
45    let initial_chunk_pos = ChunkPos::from(state.cursor.origin());
46    let initial_chunk = world.chunks.get(&initial_chunk_pos);
47    let initial_chunk = initial_chunk.as_deref().map(RwLock::read);
48
49    while let Some(item) = state.cursor.next() {
50        state.compute_next(
51            item,
52            &mut block_collisions,
53            initial_chunk_pos,
54            initial_chunk.as_deref(),
55        );
56    }
57
58    block_collisions
59}
60
61pub struct BlockCollisionsState<'a> {
62    pub world: &'a Instance,
63    pub aabb: &'a AABB,
64    pub entity_shape: VoxelShape,
65    pub cursor: Cursor3d,
66
67    _context: EntityCollisionContext,
68
69    cached_sections: Vec<(ChunkSectionPos, azalea_world::Section)>,
70    cached_block_shapes: Vec<(BlockState, &'static VoxelShape)>,
71}
72
73impl<'a> BlockCollisionsState<'a> {
74    fn compute_next(
75        &mut self,
76        item: CursorIteration,
77        block_collisions: &mut Vec<VoxelShape>,
78        initial_chunk_pos: ChunkPos,
79        initial_chunk: Option<&Chunk>,
80    ) {
81        if item.iteration_type == CursorIterationType::Corner {
82            return;
83        }
84
85        let item_chunk_pos = ChunkPos::from(item.pos);
86        let block_state: BlockState = if item_chunk_pos == initial_chunk_pos {
87            match &initial_chunk {
88                Some(initial_chunk) => initial_chunk
89                    .get(&ChunkBlockPos::from(item.pos), self.world.chunks.min_y)
90                    .unwrap_or(BlockState::AIR),
91                _ => BlockState::AIR,
92            }
93        } else {
94            self.get_block_state(item.pos)
95        };
96
97        if block_state.is_air() {
98            // fast path since we can't collide with air
99            return;
100        }
101
102        // TODO: continue if self.only_suffocating_blocks and the block is not
103        // suffocating
104
105        // if it's a full block do a faster collision check
106        if block_state.is_collision_shape_full() {
107            if !self.aabb.intersects_aabb(&AABB {
108                min: item.pos.to_vec3_floored(),
109                max: (item.pos + 1).to_vec3_floored(),
110            }) {
111                return;
112            }
113
114            block_collisions.push(BLOCK_SHAPE.move_relative(item.pos.to_vec3_floored()));
115            return;
116        }
117
118        let block_shape = self.get_block_shape(block_state);
119
120        let block_shape = block_shape.move_relative(item.pos.to_vec3_floored());
121        // if the entity shape and block shape don't collide, continue
122        if !Shapes::matches_anywhere(&block_shape, &self.entity_shape, |a, b| a && b) {
123            return;
124        }
125
126        block_collisions.push(block_shape);
127    }
128
129    pub fn new(world: &'a Instance, aabb: &'a AABB, context: EntityCollisionContext) -> Self {
130        let origin = BlockPos {
131            x: (aabb.min.x - EPSILON).floor() as i32 - 1,
132            y: (aabb.min.y - EPSILON).floor() as i32 - 1,
133            z: (aabb.min.z - EPSILON).floor() as i32 - 1,
134        };
135
136        let end = BlockPos {
137            x: (aabb.max.x + EPSILON).floor() as i32 + 1,
138            y: (aabb.max.y + EPSILON).floor() as i32 + 1,
139            z: (aabb.max.z + EPSILON).floor() as i32 + 1,
140        };
141
142        let cursor = Cursor3d::new(origin, end);
143
144        Self {
145            world,
146            aabb,
147            entity_shape: VoxelShape::from(aabb),
148            cursor,
149
150            _context: context,
151
152            cached_sections: Vec::new(),
153            cached_block_shapes: Vec::new(),
154        }
155    }
156
157    fn get_chunk(&self, block_x: i32, block_z: i32) -> Option<Arc<RwLock<Chunk>>> {
158        let chunk_x = ChunkSectionPos::block_to_section_coord(block_x);
159        let chunk_z = ChunkSectionPos::block_to_section_coord(block_z);
160        let chunk_pos = ChunkPos::new(chunk_x, chunk_z);
161
162        // TODO: minecraft caches chunk here
163        // int chunkX = SectionPos.blockToSectionCoord(blockX);
164        // int chunkZ = SectionPos.blockToSectionCoord(blockZ);
165        // long chunkPosLong = ChunkPos.asLong(chunkX, chunkZ);
166        // if (this.cachedBlockGetter != null && this.cachedBlockGetterPos == var5) {
167        //    return this.cachedBlockGetter;
168        // } else {
169        //    BlockGetter var7 = this.collisionGetter.getChunkForCollisions(chunkX,
170        // chunkZ);    this.cachedBlockGetter = var7;
171        //    this.cachedBlockGetterPos = chunkPosLong;
172        //    return var7;
173        // }
174
175        self.world.chunks.get(&chunk_pos)
176    }
177
178    fn get_block_state(&mut self, block_pos: BlockPos) -> BlockState {
179        if block_pos.y < self.world.chunks.min_y {
180            // below the world
181            return BlockState::AIR;
182        }
183
184        let section_pos = ChunkSectionPos::from(block_pos);
185        let section_block_pos = ChunkSectionBlockPos::from(block_pos);
186
187        for (cached_section_pos, cached_section) in &self.cached_sections {
188            if section_pos == *cached_section_pos {
189                return cached_section.get(section_block_pos);
190            }
191        }
192
193        let chunk = self.get_chunk(block_pos.x, block_pos.z);
194        let Some(chunk) = chunk else {
195            return BlockState::AIR;
196        };
197        let chunk = chunk.read();
198
199        let sections = &chunk.sections;
200        let section_index =
201            azalea_world::chunk_storage::section_index(block_pos.y, self.world.chunks.min_y)
202                as usize;
203
204        let Some(section) = sections.get(section_index) else {
205            return BlockState::AIR;
206        };
207
208        self.cached_sections.push((section_pos, section.clone()));
209
210        // println!("chunk section palette: {:?}", section.states.palette);
211        // println!("chunk section data: {:?}", section.states.storage.data);
212        // println!("biome length: {}", section.biomes.storage.data.len());
213
214        section.get(section_block_pos)
215    }
216
217    fn get_block_shape(&mut self, block_state: BlockState) -> &'static VoxelShape {
218        for (cached_block_state, cached_shape) in &self.cached_block_shapes {
219            if block_state == *cached_block_state {
220                return cached_shape;
221            }
222        }
223
224        let shape = block_state.collision_shape();
225        self.cached_block_shapes.push((block_state, shape));
226
227        shape
228    }
229}
230
231pub struct EntityCollisionContext {
232    pub descending: bool,
233    pub entity_bottom: f64,
234    pub held_item: ItemStack,
235    can_stand_on_fluid_predicate: CanStandOnFluidPredicate,
236    pub entity: Option<Entity>,
237}
238
239impl EntityCollisionContext {
240    pub fn of(entity: Option<Entity>) -> Self {
241        Self {
242            descending: false,
243            entity_bottom: 0.0,
244            held_item: ItemStack::Empty,
245            can_stand_on_fluid_predicate: CanStandOnFluidPredicate::PassToEntity,
246            entity,
247        }
248    }
249    pub fn with_include_liquids(mut self, include_liquids: bool) -> Self {
250        self.can_stand_on_fluid_predicate = if include_liquids {
251            CanStandOnFluidPredicate::AlwaysTrue
252        } else {
253            CanStandOnFluidPredicate::PassToEntity
254        };
255        self
256    }
257
258    pub fn can_stand_on_fluid(&self, above: &FluidState, target: &FluidState) -> bool {
259        self.can_stand_on_fluid_predicate.matches(target) && !above.is_same_kind(target)
260    }
261}
262
263enum CanStandOnFluidPredicate {
264    PassToEntity,
265    AlwaysTrue,
266}
267impl CanStandOnFluidPredicate {
268    pub fn matches(&self, _state: &FluidState) -> bool {
269        match self {
270            Self::AlwaysTrue => true,
271            // minecraft sometimes returns true for striders here, false for every other entity
272            // though
273            Self::PassToEntity => false,
274        }
275    }
276}
277
278/// This basically gets all the chunks that an entity colliding with
279/// that bounding box could be in.
280///
281/// This is forEachAccessibleNonEmptySection in vanilla Minecraft because they
282/// sort entities into sections instead of just chunks. In theory this might be
283/// a performance loss for Azalea. If this ever turns out to be a bottleneck,
284/// then maybe you should try having it do that instead.
285pub fn for_entities_in_chunks_colliding_with(
286    world: &Instance,
287    aabb: &AABB,
288    mut consumer: impl FnMut(ChunkPos, &HashSet<Entity>),
289) {
290    let min_section = ChunkSectionPos::from(aabb.min - Vec3::new(2., 4., 2.));
291    let max_section = ChunkSectionPos::from(aabb.max + Vec3::new(2., 0., 2.));
292
293    let min_chunk = ChunkPos::from(min_section);
294    let max_chunk = ChunkPos::from(max_section);
295
296    for chunk_x in min_chunk.x..=max_chunk.x {
297        for chunk_z in min_chunk.z..=max_chunk.z {
298            let chunk_pos = ChunkPos::new(chunk_x, chunk_z);
299            if let Some(entities) = world.entities_by_chunk.get(&chunk_pos) {
300                consumer(chunk_pos, entities);
301            }
302        }
303    }
304}