azalea_world/
chunk_storage.rs

1use std::collections::hash_map::Entry;
2use std::str::FromStr;
3use std::{
4    collections::HashMap,
5    fmt::Debug,
6    io::{Cursor, Write},
7    sync::{Arc, Weak},
8};
9
10use azalea_block::block_state::{BlockState, BlockStateIntegerRepr};
11use azalea_block::fluid_state::FluidState;
12use azalea_buf::{AzaleaRead, AzaleaWrite, BufReadError};
13use azalea_core::position::{BlockPos, ChunkBlockPos, ChunkPos, ChunkSectionBlockPos};
14use nohash_hasher::IntMap;
15use parking_lot::RwLock;
16use simdnbt::owned::NbtCompound;
17use tracing::{debug, trace, warn};
18
19use crate::heightmap::Heightmap;
20use crate::heightmap::HeightmapKind;
21use crate::palette::PalettedContainer;
22use crate::palette::PalettedContainerKind;
23
24const SECTION_HEIGHT: u32 = 16;
25
26/// An efficient storage of chunks for a client that has a limited render
27/// distance. This has support for using a shared [`ChunkStorage`].
28pub struct PartialChunkStorage {
29    /// The center of the view, i.e. the chunk the player is currently in.
30    view_center: ChunkPos,
31    chunk_radius: u32,
32    view_range: u32,
33    // chunks is a list of size chunk_radius * chunk_radius
34    chunks: Vec<Option<Arc<RwLock<Chunk>>>>,
35}
36
37/// A storage for chunks where they're only stored weakly, so if they're not
38/// actively being used somewhere else they'll be forgotten. This is used for
39/// shared worlds.
40#[derive(Debug, Clone)]
41pub struct ChunkStorage {
42    pub height: u32,
43    pub min_y: i32,
44    pub map: IntMap<ChunkPos, Weak<RwLock<Chunk>>>,
45}
46
47/// A single chunk in a world (16*?*16 blocks). This only contains the blocks
48/// and biomes. You can derive the height of the chunk from the number of
49/// sections, but you need a [`ChunkStorage`] to get the minimum Y
50/// coordinate.
51#[derive(Debug)]
52pub struct Chunk {
53    pub sections: Vec<Section>,
54    /// Heightmaps are used for identifying the surface blocks in a chunk.
55    /// Usually for clients only `WorldSurface` and `MotionBlocking` are
56    /// present.
57    pub heightmaps: HashMap<HeightmapKind, Heightmap>,
58}
59
60/// A section of a chunk, i.e. a 16*16*16 block area.
61#[derive(Clone, Debug)]
62pub struct Section {
63    pub block_count: u16,
64    pub states: PalettedContainer,
65    pub biomes: PalettedContainer,
66}
67
68/// Get the actual stored view distance for the selected view distance. For some
69/// reason Minecraft actually stores an extra 3 chunks.
70pub fn calculate_chunk_storage_range(view_distance: u32) -> u32 {
71    u32::max(view_distance, 2) + 3
72}
73
74impl Default for Section {
75    fn default() -> Self {
76        Section {
77            block_count: 0,
78            states: PalettedContainer::new(PalettedContainerKind::BlockStates),
79            biomes: PalettedContainer::new(PalettedContainerKind::Biomes),
80        }
81    }
82}
83
84impl Default for Chunk {
85    fn default() -> Self {
86        Chunk {
87            sections: vec![Section::default(); (384 / 16) as usize],
88            heightmaps: HashMap::new(),
89        }
90    }
91}
92
93impl PartialChunkStorage {
94    pub fn new(chunk_radius: u32) -> Self {
95        let view_range = chunk_radius * 2 + 1;
96        PartialChunkStorage {
97            view_center: ChunkPos::new(0, 0),
98            chunk_radius,
99            view_range,
100            chunks: vec![None; (view_range * view_range) as usize],
101        }
102    }
103
104    /// Update the chunk to center the view on. This should be called when the
105    /// client receives a `SetChunkCacheCenter` packet.
106    pub fn update_view_center(&mut self, view_center: ChunkPos) {
107        // this code block makes it force unload the chunks that are out of range after
108        // updating the view center. it's usually fine without it but the commented code
109        // is there in case you want to temporarily uncomment to test something
110
111        // ```
112        // for index in 0..self.chunks.len() {
113        //     let chunk_pos = self.chunk_pos_from_index(index);
114        //     if !in_range_for_view_center_and_radius(&chunk_pos, view_center, self.chunk_radius) {
115        //         self.chunks[index] = None;
116        //     }
117        // }
118        // ```
119
120        self.view_center = view_center;
121    }
122
123    /// Get the center of the view. This is usually the chunk that the player is
124    /// in.
125    pub fn view_center(&self) -> ChunkPos {
126        self.view_center
127    }
128
129    pub fn view_range(&self) -> u32 {
130        self.view_range
131    }
132
133    pub fn index_from_chunk_pos(&self, chunk_pos: &ChunkPos) -> usize {
134        let view_range = self.view_range as i32;
135
136        let x = i32::rem_euclid(chunk_pos.x, view_range) * view_range;
137        let z = i32::rem_euclid(chunk_pos.z, view_range);
138        (x + z) as usize
139    }
140
141    pub fn chunk_pos_from_index(&self, index: usize) -> ChunkPos {
142        let view_range = self.view_range as i32;
143
144        // find the base from the view center
145        let base_x = self.view_center.x.div_euclid(view_range) * view_range;
146        let base_z = self.view_center.z.div_euclid(view_range) * view_range;
147
148        // add the offset from the base
149        let offset_x = index as i32 / view_range;
150        let offset_z = index as i32 % view_range;
151
152        ChunkPos::new(base_x + offset_x, base_z + offset_z)
153    }
154
155    pub fn in_range(&self, chunk_pos: &ChunkPos) -> bool {
156        in_range_for_view_center_and_radius(chunk_pos, self.view_center, self.chunk_radius)
157    }
158
159    pub fn set_block_state(
160        &self,
161        pos: &BlockPos,
162        state: BlockState,
163        chunk_storage: &ChunkStorage,
164    ) -> Option<BlockState> {
165        if pos.y < chunk_storage.min_y
166            || pos.y >= (chunk_storage.min_y + chunk_storage.height as i32)
167        {
168            return None;
169        }
170        let chunk_pos = ChunkPos::from(pos);
171        let chunk_lock = chunk_storage.get(&chunk_pos)?;
172        let mut chunk = chunk_lock.write();
173        Some(chunk.get_and_set(&ChunkBlockPos::from(pos), state, chunk_storage.min_y))
174    }
175
176    pub fn replace_with_packet_data(
177        &mut self,
178        pos: &ChunkPos,
179        data: &mut Cursor<&[u8]>,
180        heightmaps: &NbtCompound,
181        chunk_storage: &mut ChunkStorage,
182    ) -> Result<(), BufReadError> {
183        debug!("Replacing chunk at {:?}", pos);
184        if !self.in_range(pos) {
185            warn!("Ignoring chunk since it's not in the view range: {pos:?}");
186            return Ok(());
187        }
188
189        let chunk = Chunk::read_with_dimension_height(
190            data,
191            chunk_storage.height,
192            chunk_storage.min_y,
193            heightmaps,
194        )?;
195
196        self.set(pos, Some(chunk), chunk_storage);
197        trace!("Loaded chunk {pos:?}");
198
199        Ok(())
200    }
201
202    /// Get a [`Chunk`] within render distance, or `None` if it's not loaded.
203    /// Use [`ChunkStorage::get`] to get a chunk from the shared storage.
204    pub fn limited_get(&self, pos: &ChunkPos) -> Option<&Arc<RwLock<Chunk>>> {
205        if !self.in_range(pos) {
206            warn!(
207                "Chunk at {:?} is not in the render distance (center: {:?}, {} chunks)",
208                pos, self.view_center, self.chunk_radius,
209            );
210            return None;
211        }
212
213        let index = self.index_from_chunk_pos(pos);
214        self.chunks[index].as_ref()
215    }
216    /// Get a mutable reference to a [`Chunk`] within render distance, or
217    /// `None` if it's not loaded. Use [`ChunkStorage::get`] to get
218    /// a chunk from the shared storage.
219    pub fn limited_get_mut(&mut self, pos: &ChunkPos) -> Option<&mut Option<Arc<RwLock<Chunk>>>> {
220        if !self.in_range(pos) {
221            return None;
222        }
223
224        let index = self.index_from_chunk_pos(pos);
225
226        Some(&mut self.chunks[index])
227    }
228
229    /// Set a chunk in the shared storage and reference it from the limited
230    /// storage. Use [`Self::limited_set`] if you already have an
231    /// `Arc<RwLock<Chunk>>`.
232    ///
233    /// # Panics
234    /// If the chunk is not in the render distance.
235    pub fn set(&mut self, pos: &ChunkPos, chunk: Option<Chunk>, chunk_storage: &mut ChunkStorage) {
236        let new_chunk;
237
238        // add the chunk to the shared storage
239        if let Some(chunk) = chunk {
240            match chunk_storage.map.entry(*pos) {
241                Entry::Occupied(mut e) => {
242                    if let Some(old_chunk) = e.get_mut().upgrade() {
243                        *old_chunk.write() = chunk;
244                        new_chunk = Some(old_chunk);
245                    } else {
246                        let chunk_lock = Arc::new(RwLock::new(chunk));
247                        e.insert(Arc::downgrade(&chunk_lock));
248                        new_chunk = Some(chunk_lock);
249                    }
250                }
251                Entry::Vacant(e) => {
252                    let chunk_lock = Arc::new(RwLock::new(chunk));
253                    e.insert(Arc::downgrade(&chunk_lock));
254                    new_chunk = Some(chunk_lock);
255                }
256            }
257        } else {
258            // don't remove it from the shared storage, since it'll be removed
259            // automatically if this was the last reference
260
261            new_chunk = None;
262        }
263
264        self.limited_set(pos, new_chunk);
265    }
266
267    /// Set a chunk in our limited storage, useful if your chunk is already
268    /// referenced somewhere else and you want to make it also be referenced by
269    /// this storage.
270    ///
271    /// Use [`Self::set`] if you don't already have an `Arc<RwLock<Chunk>>`.
272    ///
273    /// # Panics
274    /// If the chunk is not in the render distance.
275    pub fn limited_set(&mut self, pos: &ChunkPos, chunk: Option<Arc<RwLock<Chunk>>>) {
276        if let Some(chunk_mut) = self.limited_get_mut(pos) {
277            *chunk_mut = chunk;
278        }
279    }
280
281    /// Get an iterator over all the chunks in the storage.
282    pub fn chunks(&self) -> impl Iterator<Item = &Option<Arc<RwLock<Chunk>>>> {
283        self.chunks.iter()
284    }
285}
286impl ChunkStorage {
287    pub fn new(height: u32, min_y: i32) -> Self {
288        ChunkStorage {
289            height,
290            min_y,
291            map: IntMap::default(),
292        }
293    }
294
295    pub fn get(&self, pos: &ChunkPos) -> Option<Arc<RwLock<Chunk>>> {
296        self.map.get(pos).and_then(|chunk| chunk.upgrade())
297    }
298
299    pub fn get_block_state(&self, pos: &BlockPos) -> Option<BlockState> {
300        let chunk_pos = ChunkPos::from(pos);
301        let chunk = self.get(&chunk_pos)?;
302        let chunk = chunk.read();
303        chunk.get(&ChunkBlockPos::from(pos), self.min_y)
304    }
305
306    pub fn get_fluid_state(&self, pos: &BlockPos) -> Option<FluidState> {
307        let block_state = self.get_block_state(pos)?;
308        Some(FluidState::from(block_state))
309    }
310
311    pub fn set_block_state(&self, pos: &BlockPos, state: BlockState) -> Option<BlockState> {
312        if pos.y < self.min_y || pos.y >= (self.min_y + self.height as i32) {
313            return None;
314        }
315        let chunk_pos = ChunkPos::from(pos);
316        let chunk = self.get(&chunk_pos)?;
317        let mut chunk = chunk.write();
318        Some(chunk.get_and_set(&ChunkBlockPos::from(pos), state, self.min_y))
319    }
320}
321
322pub fn in_range_for_view_center_and_radius(
323    chunk_pos: &ChunkPos,
324    view_center: ChunkPos,
325    chunk_radius: u32,
326) -> bool {
327    (chunk_pos.x - view_center.x).unsigned_abs() <= chunk_radius
328        && (chunk_pos.z - view_center.z).unsigned_abs() <= chunk_radius
329}
330
331impl Chunk {
332    pub fn read_with_dimension_height(
333        buf: &mut Cursor<&[u8]>,
334        dimension_height: u32,
335        min_y: i32,
336        heightmaps_nbt: &NbtCompound,
337    ) -> Result<Self, BufReadError> {
338        let section_count = dimension_height / SECTION_HEIGHT;
339        let mut sections = Vec::with_capacity(section_count as usize);
340        for _ in 0..section_count {
341            let section = Section::azalea_read(buf)?;
342            sections.push(section);
343        }
344
345        let mut heightmaps = HashMap::new();
346        for (name, heightmap) in heightmaps_nbt.iter() {
347            let Ok(kind) = HeightmapKind::from_str(&name.to_str()) else {
348                warn!("Unknown heightmap kind: {name}");
349                continue;
350            };
351            let Some(data) = heightmap.long_array() else {
352                warn!("Heightmap {name} is not a long array");
353                continue;
354            };
355            let data: Vec<u64> = data.iter().map(|x| *x as u64).collect();
356            let heightmap = Heightmap::new(kind, dimension_height, min_y, data);
357            heightmaps.insert(kind, heightmap);
358        }
359
360        Ok(Chunk {
361            sections,
362            heightmaps,
363        })
364    }
365
366    pub fn get(&self, pos: &ChunkBlockPos, min_y: i32) -> Option<BlockState> {
367        get_block_state_from_sections(&self.sections, pos, min_y)
368    }
369
370    #[must_use = "Use Chunk::set instead if you don't need the previous state"]
371    pub fn get_and_set(
372        &mut self,
373        pos: &ChunkBlockPos,
374        state: BlockState,
375        min_y: i32,
376    ) -> BlockState {
377        let section_index = section_index(pos.y, min_y);
378        // TODO: make sure the section exists
379        let section = &mut self.sections[section_index as usize];
380        let chunk_section_pos = ChunkSectionBlockPos::from(pos);
381        let previous_state = section.get_and_set(chunk_section_pos, state);
382
383        for heightmap in self.heightmaps.values_mut() {
384            heightmap.update(pos, state, &self.sections);
385        }
386
387        previous_state
388    }
389
390    pub fn set(&mut self, pos: &ChunkBlockPos, state: BlockState, min_y: i32) {
391        let section_index = section_index(pos.y, min_y);
392        // TODO: make sure the section exists
393        let section = &mut self.sections[section_index as usize];
394        let chunk_section_pos = ChunkSectionBlockPos::from(pos);
395        section.set(chunk_section_pos, state);
396
397        for heightmap in self.heightmaps.values_mut() {
398            heightmap.update(pos, state, &self.sections);
399        }
400    }
401}
402
403/// Get the block state at the given position from a list of sections. Returns
404/// `None` if the position is out of bounds.
405#[inline]
406pub fn get_block_state_from_sections(
407    sections: &[Section],
408    pos: &ChunkBlockPos,
409    min_y: i32,
410) -> Option<BlockState> {
411    if pos.y < min_y {
412        // y position is out of bounds
413        return None;
414    }
415    let section_index = section_index(pos.y, min_y) as usize;
416    if section_index >= sections.len() {
417        // y position is out of bounds
418        return None;
419    };
420    let section = &sections[section_index];
421    let chunk_section_pos = ChunkSectionBlockPos::from(pos);
422    Some(section.get(chunk_section_pos))
423}
424
425impl AzaleaWrite for Chunk {
426    fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
427        for section in &self.sections {
428            section.azalea_write(buf)?;
429        }
430        Ok(())
431    }
432}
433
434impl Debug for PartialChunkStorage {
435    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
436        f.debug_struct("PartialChunkStorage")
437            .field("view_center", &self.view_center)
438            .field("chunk_radius", &self.chunk_radius)
439            .field("view_range", &self.view_range)
440            // .field("chunks", &self.chunks)
441            .field("chunks", &format_args!("{} items", self.chunks.len()))
442            .finish()
443    }
444}
445
446impl AzaleaRead for Section {
447    fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> {
448        let block_count = u16::azalea_read(buf)?;
449
450        // this is commented out because the vanilla server is wrong
451        // assert!(
452        //     block_count <= 16 * 16 * 16,
453        //     "A section has more blocks than what should be possible. This is a bug!"
454        // );
455
456        let states = PalettedContainer::read_with_type(buf, &PalettedContainerKind::BlockStates)?;
457
458        for i in 0..states.storage.size() {
459            if !BlockState::is_valid_state(states.storage.get(i) as BlockStateIntegerRepr) {
460                return Err(BufReadError::Custom(format!(
461                    "Invalid block state {} (index {i}) found in section.",
462                    states.storage.get(i)
463                )));
464            }
465        }
466
467        let biomes = PalettedContainer::read_with_type(buf, &PalettedContainerKind::Biomes)?;
468        Ok(Section {
469            block_count,
470            states,
471            biomes,
472        })
473    }
474}
475
476impl AzaleaWrite for Section {
477    fn azalea_write(&self, buf: &mut impl Write) -> Result<(), std::io::Error> {
478        self.block_count.azalea_write(buf)?;
479        self.states.azalea_write(buf)?;
480        self.biomes.azalea_write(buf)?;
481        Ok(())
482    }
483}
484
485impl Section {
486    pub fn get(&self, pos: ChunkSectionBlockPos) -> BlockState {
487        // TODO: use the unsafe method and do the check earlier
488        let state = self
489            .states
490            .get(pos.x as usize, pos.y as usize, pos.z as usize);
491        // if there's an unknown block assume it's air
492        BlockState::try_from(state).unwrap_or(BlockState::AIR)
493    }
494
495    pub fn get_and_set(&mut self, pos: ChunkSectionBlockPos, state: BlockState) -> BlockState {
496        let previous_state =
497            self.states
498                .get_and_set(pos.x as usize, pos.y as usize, pos.z as usize, state.id);
499        // if there's an unknown block assume it's air
500        BlockState::try_from(previous_state).unwrap_or(BlockState::AIR)
501    }
502
503    pub fn set(&mut self, pos: ChunkSectionBlockPos, state: BlockState) {
504        self.states
505            .set(pos.x as usize, pos.y as usize, pos.z as usize, state.id);
506    }
507}
508
509impl Default for PartialChunkStorage {
510    fn default() -> Self {
511        Self::new(8)
512    }
513}
514impl Default for ChunkStorage {
515    fn default() -> Self {
516        Self::new(384, -64)
517    }
518}
519
520/// Get the index of where a section is in a chunk based on its y coordinate
521/// and the minimum y coordinate of the world.
522#[inline]
523pub fn section_index(y: i32, min_y: i32) -> u32 {
524    if y < min_y {
525        #[cfg(debug_assertions)]
526        warn!("y ({y}) must be at least {min_y}");
527        #[cfg(not(debug_assertions))]
528        trace!("y ({y}) must be at least {min_y}")
529    };
530    let min_section_index = min_y >> 4;
531    ((y >> 4) - min_section_index) as u32
532}
533
534#[cfg(test)]
535mod tests {
536    use super::*;
537
538    #[test]
539    fn test_section_index() {
540        assert_eq!(section_index(0, 0), 0);
541        assert_eq!(section_index(128, 0), 8);
542        assert_eq!(section_index(127, 0), 7);
543        assert_eq!(section_index(0, -64), 4);
544        assert_eq!(section_index(-64, -64), 0);
545        assert_eq!(section_index(-49, -64), 0);
546        assert_eq!(section_index(-48, -64), 1);
547        assert_eq!(section_index(128, -64), 12);
548    }
549
550    #[test]
551    fn test_out_of_bounds_y() {
552        let mut chunk_storage = ChunkStorage::default();
553        let mut partial_chunk_storage = PartialChunkStorage::default();
554        partial_chunk_storage.set(
555            &ChunkPos { x: 0, z: 0 },
556            Some(Chunk::default()),
557            &mut chunk_storage,
558        );
559        assert!(chunk_storage
560            .get_block_state(&BlockPos { x: 0, y: 319, z: 0 })
561            .is_some());
562        assert!(chunk_storage
563            .get_block_state(&BlockPos { x: 0, y: 320, z: 0 })
564            .is_none());
565        assert!(chunk_storage
566            .get_block_state(&BlockPos { x: 0, y: 338, z: 0 })
567            .is_none());
568        assert!(chunk_storage
569            .get_block_state(&BlockPos { x: 0, y: -64, z: 0 })
570            .is_some());
571        assert!(chunk_storage
572            .get_block_state(&BlockPos { x: 0, y: -65, z: 0 })
573            .is_none());
574    }
575
576    #[test]
577    fn test_chunk_pos_from_index() {
578        let mut partial_chunk_storage = PartialChunkStorage::new(5);
579        partial_chunk_storage.update_view_center(ChunkPos::new(0, -1));
580        assert_eq!(
581            partial_chunk_storage.chunk_pos_from_index(
582                partial_chunk_storage.index_from_chunk_pos(&ChunkPos::new(2, -1))
583            ),
584            ChunkPos::new(2, -1),
585        );
586    }
587}