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