Skip to main content

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