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