azalea_world/
find_blocks.rs

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