azalea_world/
chunk_storage.rs

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