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