Skip to main content

azalea_world/chunk/
mod.rs

1pub mod partial;
2pub mod storage;
3
4use std::{
5    collections::HashMap,
6    fmt::Debug,
7    io,
8    io::{Cursor, Write},
9};
10
11use azalea_block::block_state::{BlockState, BlockStateIntegerRepr};
12use azalea_buf::{AzBuf, BufReadError};
13use azalea_core::{
14    heightmap_kind::HeightmapKind,
15    position::{ChunkBiomePos, ChunkBlockPos, ChunkSectionBiomePos, ChunkSectionBlockPos},
16};
17use azalea_registry::data::Biome;
18use tracing::warn;
19
20use crate::{heightmap::Heightmap, palette::PalettedContainer};
21
22const SECTION_HEIGHT: u32 = 16;
23
24/// A single chunk in a world (16*?*16 blocks).
25///
26/// This only contains blocks and biomes. You can derive the height of the chunk
27/// from the number of sections, but you need a [`ChunkStorage`] to get the
28/// minimum Y coordinate.
29///
30/// [`ChunkStorage`]: crate::ChunkStorage
31#[derive(Debug)]
32pub struct Chunk {
33    pub sections: Box<[Section]>,
34    /// Heightmaps are used for identifying the surface blocks in a chunk.
35    /// Usually for clients only `WorldSurface` and `MotionBlocking` are
36    /// present.
37    pub heightmaps: HashMap<HeightmapKind, Heightmap>,
38}
39
40/// A section of a chunk, i.e. a 16*16*16 block area.
41#[derive(Clone, Debug, Default, PartialEq)]
42pub struct Section {
43    /// The number of non-empty blocks in the section, which is initialized
44    /// based on a value sent to us by the server.
45    ///
46    /// This may be updated every time [`Self::get_and_set_block_state`] is
47    /// called.
48    pub block_count: u16,
49    /// Similar to [`Self::block_count`], but for fluids.
50    ///
51    /// Unlike [`Self::block_count`], this is currently not updated by Azalea.
52    pub fluid_count: u16,
53    pub states: PalettedContainer<BlockState>,
54    pub biomes: PalettedContainer<Biome>,
55}
56
57/// Get the actual stored view distance for the selected view distance.
58///
59/// For some reason, Minecraft stores an extra 3 chunks.
60pub fn calculate_chunk_storage_range(view_distance: u32) -> u32 {
61    u32::max(view_distance, 2) + 3
62}
63
64impl Default for Chunk {
65    fn default() -> Self {
66        Chunk {
67            sections: vec![Section::default(); (384 / 16) as usize].into(),
68            heightmaps: HashMap::new(),
69        }
70    }
71}
72
73impl Chunk {
74    pub fn read_with_dimension_height(
75        buf: &mut Cursor<&[u8]>,
76        dimension_height: u32,
77        min_y: i32,
78        heightmaps_data: &[(HeightmapKind, Box<[u64]>)],
79    ) -> Result<Self, BufReadError> {
80        let section_count = dimension_height / SECTION_HEIGHT;
81        let mut sections = Vec::with_capacity(section_count as usize);
82        for _ in 0..section_count {
83            let section = Section::azalea_read(buf)?;
84            sections.push(section);
85        }
86        let sections = sections.into_boxed_slice();
87
88        let mut heightmaps = HashMap::new();
89        for (kind, data) in heightmaps_data {
90            let data = data.clone();
91            let heightmap = Heightmap::new(*kind, dimension_height, min_y, data);
92            heightmaps.insert(*kind, heightmap);
93        }
94
95        Ok(Chunk {
96            sections,
97            heightmaps,
98        })
99    }
100
101    pub fn get_block_state(&self, pos: &ChunkBlockPos, min_y: i32) -> Option<BlockState> {
102        get_block_state_from_sections(&self.sections, pos, min_y)
103    }
104
105    #[must_use = "Use Chunk::set_block_state instead if you don't need the previous state"]
106    pub fn get_and_set_block_state(
107        &mut self,
108        pos: &ChunkBlockPos,
109        state: BlockState,
110        min_y: i32,
111    ) -> BlockState {
112        let section_index = section_index(pos.y, min_y);
113        let Some(section) = self.sections.get_mut(section_index as usize) else {
114            warn!(
115                "Tried to get and set block state {state:?} at out-of-bounds relative chunk position {pos:?}",
116            );
117            return BlockState::AIR;
118        };
119        let chunk_section_pos = ChunkSectionBlockPos::from(pos);
120        let previous_state = section.get_and_set_block_state(chunk_section_pos, state);
121
122        for heightmap in self.heightmaps.values_mut() {
123            heightmap.update(pos, state, &self.sections);
124        }
125
126        previous_state
127    }
128
129    pub fn set_block_state(&mut self, pos: &ChunkBlockPos, state: BlockState, min_y: i32) {
130        let section_index = section_index(pos.y, min_y);
131        let Some(section) = self.sections.get_mut(section_index as usize) else {
132            warn!(
133                "Tried to set block state {state:?} at out-of-bounds relative chunk position {pos:?}",
134            );
135            return;
136        };
137        let chunk_section_pos = ChunkSectionBlockPos::from(pos);
138        section.get_and_set_block_state(chunk_section_pos, state);
139
140        for heightmap in self.heightmaps.values_mut() {
141            heightmap.update(pos, state, &self.sections);
142        }
143    }
144
145    /// Get the biome at the given position, or `None` if it's out of bounds.
146    pub fn get_biome(&self, pos: ChunkBiomePos, min_y: i32) -> Option<Biome> {
147        if pos.y < min_y {
148            // y position is out of bounds
149            return None;
150        }
151        let section_index = section_index(pos.y, min_y);
152        let Some(section) = self.sections.get(section_index as usize) else {
153            warn!("Tried to get biome at out-of-bounds relative chunk position {pos:?}",);
154            return None;
155        };
156        let chunk_section_pos = ChunkSectionBiomePos::from(pos);
157        Some(section.get_biome(chunk_section_pos))
158    }
159}
160
161/// Get the block state at the given position from a list of sections. Returns
162/// `None` if the position is out of bounds.
163#[inline]
164pub fn get_block_state_from_sections(
165    sections: &[Section],
166    pos: &ChunkBlockPos,
167    min_y: i32,
168) -> Option<BlockState> {
169    if pos.y < min_y {
170        // y position is out of bounds
171        return None;
172    }
173    let section_index = section_index(pos.y, min_y) as usize;
174    if section_index >= sections.len() {
175        // y position is out of bounds
176        return None;
177    };
178    let section = &sections[section_index];
179
180    let chunk_section_pos = ChunkSectionBlockPos::from(pos);
181    Some(section.get_block_state(chunk_section_pos))
182}
183
184impl AzBuf for Section {
185    fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
186        let block_count = u16::azalea_read(buf)?;
187        let fluid_count = u16::azalea_read(buf)?;
188
189        // this is commented out because the vanilla server is wrong
190        // TODO: ^ this comment was written ages ago. needs more investigation.
191        // assert!(
192        //     block_count <= 16 * 16 * 16,
193        //     "A section has more blocks than what should be possible. This is a bug!"
194        // );
195
196        let states = PalettedContainer::<BlockState>::read(buf)?;
197
198        for i in 0..states.storage.size() {
199            if !BlockState::is_valid_state(states.storage.get(i) as BlockStateIntegerRepr) {
200                return Err(BufReadError::Custom(format!(
201                    "Invalid block state {} (index {i}) found in section.",
202                    states.storage.get(i)
203                )));
204            }
205        }
206
207        let biomes = PalettedContainer::<Biome>::read(buf)?;
208        Ok(Section {
209            block_count,
210            fluid_count,
211            states,
212            biomes,
213        })
214    }
215    fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> {
216        self.block_count.azalea_write(buf)?;
217        self.fluid_count.azalea_write(buf)?;
218        self.states.write(buf)?;
219        self.biomes.write(buf)?;
220        Ok(())
221    }
222}
223
224impl Section {
225    pub fn get_block_state(&self, pos: ChunkSectionBlockPos) -> BlockState {
226        self.states.get(pos)
227    }
228    pub fn get_and_set_block_state(
229        &mut self,
230        pos: ChunkSectionBlockPos,
231        state: BlockState,
232    ) -> BlockState {
233        let previous_state = self.states.get_and_set(pos, state);
234
235        if previous_state.is_air() && !state.is_air() {
236            self.block_count += 1;
237        } else if !previous_state.is_air() && state.is_air() {
238            self.block_count -= 1;
239        }
240
241        previous_state
242    }
243
244    pub fn get_biome(&self, pos: ChunkSectionBiomePos) -> Biome {
245        self.biomes.get(pos)
246    }
247    pub fn set_biome(&mut self, pos: ChunkSectionBiomePos, biome: Biome) {
248        self.biomes.set(pos, biome);
249    }
250    pub fn get_and_set_biome(&mut self, pos: ChunkSectionBiomePos, biome: Biome) -> Biome {
251        self.biomes.get_and_set(pos, biome)
252    }
253}
254
255/// Get the index of where a section is in a chunk based on its y coordinate
256/// and the minimum y coordinate of the world.
257#[inline]
258pub fn section_index(y: i32, min_y: i32) -> u32 {
259    if y < min_y {
260        #[cfg(debug_assertions)]
261        tracing::warn!("y ({y}) must be at least {min_y}");
262        #[cfg(not(debug_assertions))]
263        tracing::trace!("y ({y}) must be at least {min_y}")
264    };
265    let min_section_index = min_y >> 4;
266    ((y >> 4) - min_section_index) as u32
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272    use crate::palette::SectionPos;
273
274    #[test]
275    fn test_section_index() {
276        assert_eq!(section_index(0, 0), 0);
277        assert_eq!(section_index(128, 0), 8);
278        assert_eq!(section_index(127, 0), 7);
279        assert_eq!(section_index(0, -64), 4);
280        assert_eq!(section_index(-64, -64), 0);
281        assert_eq!(section_index(-49, -64), 0);
282        assert_eq!(section_index(-48, -64), 1);
283        assert_eq!(section_index(128, -64), 12);
284    }
285
286    #[test]
287    fn serialize_and_deserialize_section() {
288        let mut states = PalettedContainer::new();
289
290        states.set(
291            SectionPos::new(1, 2, 3),
292            BlockState::try_from(BlockState::MAX_STATE).unwrap(),
293        );
294        states.set(
295            SectionPos::new(4, 5, 6),
296            BlockState::try_from(BlockState::MAX_STATE).unwrap(),
297        );
298        let biomes = PalettedContainer::new();
299        let section = Section {
300            block_count: 2,
301            fluid_count: 0,
302            states,
303            biomes,
304        };
305
306        let mut buf = Vec::new();
307        section.azalea_write(&mut buf).unwrap();
308
309        let mut cur = Cursor::new(buf.as_slice());
310        let deserialized_section = Section::azalea_read(&mut cur).unwrap();
311        assert_eq!(cur.position(), buf.len() as u64);
312
313        assert_eq!(section, deserialized_section);
314    }
315}