azalea_world/
find_blocks.rs

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