azalea_world/
chunk_storage.rs

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