azalea_world/
find_blocks.rs

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