Skip to main content

azalea_world/
find_blocks.rs

1use azalea_block::{BlockState, BlockStates};
2use azalea_core::position::{BlockPos, ChunkPos};
3
4use crate::{Chunk, ChunkStorage, World, iterators::ChunkIterator, palette::Palette};
5
6impl World {
7    /// Find the coordinates of a block in the world.
8    ///
9    /// Note that this is sorted by `x+y+z` and not `x^2+y^2+z^2` for
10    /// performance purposes.
11    ///
12    /// ```
13    /// # use azalea_registry::builtin::BlockKind;
14    /// # fn example(position: azalea_core::position::Vec3, world: &azalea_world::World) {
15    /// let pos = world.find_block(position, &BlockKind::Chest.into());
16    /// # }
17    /// ```
18    pub fn find_block(
19        &self,
20        nearest_to: impl Into<BlockPos>,
21        block_states: &BlockStates,
22    ) -> Option<BlockPos> {
23        // iterate over every chunk in a 3d spiral pattern
24        // and then check the palette for the block state
25
26        let nearest_to: BlockPos = nearest_to.into();
27        let start_chunk: ChunkPos = (&nearest_to).into();
28        let mut iter = ChunkIterator::new(start_chunk, 32);
29
30        let mut nearest_found_pos: Option<BlockPos> = None;
31        let mut nearest_found_distance = 0;
32
33        // we do `while` instead of `for` so we can access iter later
34        while let Some(chunk_pos) = iter.next() {
35            let Some(chunk) = self.chunks.get(&chunk_pos) else {
36                // if the chunk isn't loaded then we skip it.
37                // we don't just return since it *could* cause issues if there's a random
38                // unloaded chunk and then more that are loaded.
39                // unlikely but still something to consider, and it's not like this slows it
40                // down much anyways.
41                continue;
42            };
43
44            find_blocks_in_chunk(
45                block_states,
46                chunk_pos,
47                &chunk.read(),
48                self.chunks.min_y,
49                |this_block_pos| {
50                    let this_block_distance = (nearest_to - this_block_pos).length_manhattan();
51                    // only update if it's closer
52                    if nearest_found_pos.is_none() || this_block_distance < nearest_found_distance {
53                        nearest_found_pos = Some(this_block_pos);
54                        nearest_found_distance = this_block_distance;
55                    }
56                },
57            );
58
59            if let Some(nearest_found_pos) = nearest_found_pos {
60                // this is required because find_block searches chunk-by-chunk, which can cause
61                // us to find blocks first that aren't actually the closest
62                let required_chunk_distance = u32::max(
63                    u32::max(
64                        (chunk_pos.x - start_chunk.x).unsigned_abs(),
65                        (chunk_pos.z - start_chunk.z).unsigned_abs(),
66                    ),
67                    (nearest_to.y - nearest_found_pos.y)
68                        .unsigned_abs()
69                        .div_ceil(16),
70                ) + 1;
71                let nearest_chunk_distance = iter.layer;
72
73                // if we found the position and there's no chance there's something closer,
74                // return it
75                if nearest_chunk_distance > required_chunk_distance {
76                    return Some(nearest_found_pos);
77                }
78            }
79        }
80
81        if nearest_found_pos.is_some() {
82            nearest_found_pos
83        } else {
84            None
85        }
86    }
87
88    /// Find all the coordinates of a block in the world.
89    ///
90    /// This returns an iterator that yields the [`BlockPos`]s of blocks that
91    /// are in the given block states.
92    ///
93    /// Note that this is sorted by `x+y+z` and not `x^2+y^2+z^2` for
94    /// performance purposes.
95    pub fn find_blocks<'a>(
96        &'a self,
97        nearest_to: impl Into<BlockPos>,
98        block_states: &'a BlockStates,
99    ) -> FindBlocks<'a> {
100        FindBlocks::new(nearest_to.into(), &self.chunks, block_states)
101    }
102}
103
104pub struct FindBlocks<'a> {
105    nearest_to: BlockPos,
106    start_chunk: ChunkPos,
107    chunk_iterator: ChunkIterator,
108    chunks: &'a ChunkStorage,
109    block_states: &'a BlockStates,
110
111    queued: Vec<BlockPos>,
112}
113
114impl<'a> FindBlocks<'a> {
115    pub fn new(
116        nearest_to: BlockPos,
117        chunks: &'a ChunkStorage,
118        block_states: &'a BlockStates,
119    ) -> Self {
120        let start_chunk: ChunkPos = (&nearest_to).into();
121        Self {
122            nearest_to,
123            start_chunk,
124            chunk_iterator: ChunkIterator::new(start_chunk, 32),
125            chunks,
126            block_states,
127
128            queued: Vec::new(),
129        }
130    }
131}
132
133impl Iterator for FindBlocks<'_> {
134    type Item = BlockPos;
135
136    fn next(&mut self) -> Option<Self::Item> {
137        if let Some(queued) = self.queued.pop() {
138            return Some(queued);
139        }
140
141        let mut found = Vec::new();
142
143        let mut nearest_found_pos: Option<BlockPos> = None;
144        let mut nearest_found_distance = 0;
145
146        while let Some(chunk_pos) = self.chunk_iterator.next() {
147            let Some(chunk) = self.chunks.get(&chunk_pos) else {
148                // if the chunk isn't loaded then we skip it.
149                // we don't just return since it *could* cause issues if there's a random
150                // unloaded chunk and then more that are loaded.
151                // unlikely but still something to consider, and it's not like this slows it
152                // down much anyways.
153                continue;
154            };
155
156            find_blocks_in_chunk(
157                self.block_states,
158                chunk_pos,
159                &chunk.read(),
160                self.chunks.min_y,
161                |this_block_pos| {
162                    let this_block_distance = (self.nearest_to - this_block_pos).length_manhattan();
163
164                    found.push((this_block_pos, this_block_distance));
165
166                    if nearest_found_pos.is_none() || this_block_distance < nearest_found_distance {
167                        nearest_found_pos = Some(this_block_pos);
168                        nearest_found_distance = this_block_distance;
169                    }
170                },
171            );
172
173            if let Some(nearest_found_pos) = nearest_found_pos {
174                // this is required because find_block searches chunk-by-chunk, which can cause
175                // us to find blocks first that aren't actually the closest
176                let required_chunk_distance = u32::max(
177                    u32::max(
178                        (chunk_pos.x - self.start_chunk.x).unsigned_abs(),
179                        (chunk_pos.z - self.start_chunk.z).unsigned_abs(),
180                    ),
181                    (self.nearest_to.y - nearest_found_pos.y)
182                        .unsigned_abs()
183                        .div_ceil(16),
184                ) + 1;
185                let nearest_chunk_distance = self.chunk_iterator.layer;
186
187                // if we found the position and there's no chance there's something closer,
188                // return it
189                if nearest_chunk_distance > required_chunk_distance {
190                    // sort so nearest is at the end
191                    found.sort_unstable_by_key(|(_, distance)| u32::MAX - distance);
192
193                    self.queued = found.into_iter().map(|(pos, _)| pos).collect();
194                    return self.queued.pop();
195                }
196            }
197        }
198
199        None
200    }
201}
202
203/// An optimized function for finding the block positions in a chunk that match
204/// the given block states.
205///
206/// This is used internally by [`World::find_block`] and
207/// [`World::find_blocks`].
208pub fn find_blocks_in_chunk(
209    block_states: &BlockStates,
210    chunk_pos: ChunkPos,
211    chunk: &Chunk,
212    min_y: i32,
213    mut cb: impl FnMut(BlockPos),
214) {
215    for (section_index, section) in chunk.sections.iter().enumerate() {
216        let maybe_has_block = palette_maybe_has_block(&section.states.palette, block_states);
217        if !maybe_has_block {
218            continue;
219        }
220
221        for i in 0..4096 {
222            let block_state = section.states.get_at_index(i);
223
224            if block_states.contains(&block_state) {
225                let section_pos = section.states.pos_from_index(i);
226                let (x, y, z) = (
227                    chunk_pos.x * 16 + (section_pos.x as i32),
228                    min_y + (section_index * 16) as i32 + section_pos.y as i32,
229                    chunk_pos.z * 16 + (section_pos.z as i32),
230                );
231                let this_block_pos = BlockPos { x, y, z };
232
233                cb(this_block_pos);
234            }
235        }
236    }
237}
238
239fn palette_maybe_has_block(palette: &Palette<BlockState>, block_states: &BlockStates) -> bool {
240    match &palette {
241        Palette::SingleValue(id) => block_states.contains(id),
242        Palette::Linear(ids) => ids.iter().any(|id| block_states.contains(id)),
243        Palette::Hashmap(ids) => ids.iter().any(|id| block_states.contains(id)),
244        Palette::Global => true,
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use azalea_registry::builtin::BlockKind;
251
252    use super::*;
253    use crate::{Chunk, PartialChunkStorage};
254
255    #[test]
256    fn find_block() {
257        let mut world = World::default();
258
259        let chunk_storage = &mut world.chunks;
260        let mut partial_chunk_storage = PartialChunkStorage::default();
261
262        // block at (17, 0, 0) and (0, 18, 0)
263
264        partial_chunk_storage.set(
265            &ChunkPos { x: 0, z: 0 },
266            Some(Chunk::default()),
267            chunk_storage,
268        );
269        partial_chunk_storage.set(
270            &ChunkPos { x: 1, z: 0 },
271            Some(Chunk::default()),
272            chunk_storage,
273        );
274
275        chunk_storage.set_block_state(BlockPos { x: 17, y: 0, z: 0 }, BlockKind::Stone.into());
276        chunk_storage.set_block_state(BlockPos { x: 0, y: 18, z: 0 }, BlockKind::Stone.into());
277
278        let pos = world.find_block(BlockPos { x: 0, y: 0, z: 0 }, &BlockKind::Stone.into());
279        assert_eq!(pos, Some(BlockPos { x: 17, y: 0, z: 0 }));
280    }
281
282    #[test]
283    fn find_block_next_to_chunk_border() {
284        let mut world = World::default();
285
286        let chunk_storage = &mut world.chunks;
287        let mut partial_chunk_storage = PartialChunkStorage::default();
288
289        // block at (-1, 0, 0) and (15, 0, 0)
290
291        partial_chunk_storage.set(
292            &ChunkPos { x: -1, z: 0 },
293            Some(Chunk::default()),
294            chunk_storage,
295        );
296        partial_chunk_storage.set(
297            &ChunkPos { x: 0, z: 0 },
298            Some(Chunk::default()),
299            chunk_storage,
300        );
301
302        chunk_storage.set_block_state(BlockPos { x: -1, y: 0, z: 0 }, BlockKind::Stone.into());
303        chunk_storage.set_block_state(BlockPos { x: 15, y: 0, z: 0 }, BlockKind::Stone.into());
304
305        let pos = world.find_block(BlockPos { x: 0, y: 0, z: 0 }, &BlockKind::Stone.into());
306        assert_eq!(pos, Some(BlockPos { x: -1, y: 0, z: 0 }));
307    }
308}