Skip to main content

azalea_world/chunk/
partial.rs

1use std::{
2    fmt::{self, Debug},
3    io::Cursor,
4    sync::Arc,
5};
6
7use azalea_block::BlockState;
8use azalea_buf::BufReadError;
9use azalea_core::{
10    heightmap_kind::HeightmapKind,
11    position::{BlockPos, ChunkBlockPos, ChunkPos},
12};
13use parking_lot::RwLock;
14use tracing::{debug, trace, warn};
15
16use crate::{Chunk, chunk::storage::ChunkStorage};
17
18/// An efficient storage of chunks for a client that has a limited render
19/// distance.
20///
21/// This has support for using a shared [`ChunkStorage`].
22pub struct PartialChunkStorage {
23    /// The center of the view, i.e. the chunk the player is currently in.
24    view_center: ChunkPos,
25    pub(crate) chunk_radius: u32,
26    view_range: u32,
27    // chunks is a list of size chunk_radius * chunk_radius
28    chunks: Box<[Option<Arc<RwLock<Chunk>>>]>,
29}
30
31impl PartialChunkStorage {
32    pub fn new(chunk_radius: u32) -> Self {
33        let view_range = chunk_radius * 2 + 1;
34        PartialChunkStorage {
35            view_center: ChunkPos::new(0, 0),
36            chunk_radius,
37            view_range,
38            chunks: vec![None; (view_range * view_range) as usize].into(),
39        }
40    }
41
42    /// Update the chunk to center the view on.
43    ///
44    /// This should be called when the client receives a `SetChunkCacheCenter`
45    /// packet.
46    pub fn update_view_center(&mut self, view_center: ChunkPos) {
47        // this code block makes it force unload the chunks that are out of range after
48        // updating the view center. it's usually fine without it but the commented code
49        // is there in case you want to temporarily uncomment to test something
50
51        // ```
52        // for index in 0..self.chunks.len() {
53        //     let chunk_pos = self.chunk_pos_from_index(index);
54        //     if !in_range_for_view_center_and_radius(&chunk_pos, view_center, self.chunk_radius) {
55        //         self.chunks[index] = None;
56        //     }
57        // }
58        // ```
59
60        self.view_center = view_center;
61    }
62
63    /// Get the center of the view. This is usually the chunk that the player is
64    /// in.
65    pub fn view_center(&self) -> ChunkPos {
66        self.view_center
67    }
68
69    pub fn view_range(&self) -> u32 {
70        self.view_range
71    }
72
73    pub fn index_from_chunk_pos(&self, chunk_pos: &ChunkPos) -> usize {
74        let view_range = self.view_range as i32;
75
76        let x = i32::rem_euclid(chunk_pos.x, view_range) * view_range;
77        let z = i32::rem_euclid(chunk_pos.z, view_range);
78        (x + z) as usize
79    }
80
81    pub fn chunk_pos_from_index(&self, index: usize) -> ChunkPos {
82        let view_range = self.view_range as i32;
83
84        // find the base from the view center
85        let base_x = self.view_center.x.div_euclid(view_range) * view_range;
86        let base_z = self.view_center.z.div_euclid(view_range) * view_range;
87
88        // add the offset from the base
89        let offset_x = index as i32 / view_range;
90        let offset_z = index as i32 % view_range;
91
92        ChunkPos::new(base_x + offset_x, base_z + offset_z)
93    }
94
95    pub fn in_range(&self, chunk_pos: &ChunkPos) -> bool {
96        in_range_for_view_center_and_radius(chunk_pos, self.view_center, self.chunk_radius)
97    }
98
99    pub fn set_block_state(
100        &self,
101        pos: BlockPos,
102        state: BlockState,
103        chunk_storage: &ChunkStorage,
104    ) -> Option<BlockState> {
105        if pos.y < chunk_storage.min_y()
106            || pos.y >= (chunk_storage.min_y() + chunk_storage.height() as i32)
107        {
108            return None;
109        }
110        let chunk_pos = ChunkPos::from(pos);
111        let chunk_lock = chunk_storage.get(&chunk_pos)?;
112        let mut chunk = chunk_lock.write();
113        Some(chunk.get_and_set_block_state(&ChunkBlockPos::from(pos), state, chunk_storage.min_y()))
114    }
115
116    pub fn replace_with_packet_data(
117        &mut self,
118        pos: &ChunkPos,
119        data: &mut Cursor<&[u8]>,
120        heightmaps: &[(HeightmapKind, Box<[u64]>)],
121        chunk_storage: &mut ChunkStorage,
122    ) -> Result<(), BufReadError> {
123        debug!("Replacing chunk at {:?}", pos);
124        if !self.in_range(pos) {
125            warn!("Ignoring chunk since it's not in the view range: {pos:?}");
126            return Ok(());
127        }
128
129        let chunk = Chunk::read_with_dimension_height(
130            data,
131            chunk_storage.height(),
132            chunk_storage.min_y(),
133            heightmaps,
134        )?;
135
136        self.set(pos, Some(chunk), chunk_storage);
137        trace!("Loaded chunk {pos:?}");
138
139        Ok(())
140    }
141
142    /// Get a [`Chunk`] within render distance, or `None` if it's not loaded.
143    /// Use [`ChunkStorageTrait::get`] to get a chunk from the shared storage.
144    ///
145    /// [`ChunkStorageTrait::get`]: crate::chunk::storage::ChunkStorageTrait::get
146    pub fn limited_get(&self, pos: &ChunkPos) -> Option<&Arc<RwLock<Chunk>>> {
147        if !self.in_range(pos) {
148            warn!(
149                "Chunk at {:?} is not in the render distance (center: {:?}, {} chunks)",
150                pos, self.view_center, self.chunk_radius,
151            );
152            return None;
153        }
154
155        let index = self.index_from_chunk_pos(pos);
156        self.chunks[index].as_ref()
157    }
158    /// Get a mutable reference to a [`Chunk`] within render distance, or
159    /// `None` if it's not loaded.
160    ///
161    /// Use [`ChunkStorageTrait::get`] to get a chunk from the shared storage.
162    ///
163    /// [`ChunkStorageTrait::get`]: crate::chunk::storage::ChunkStorageTrait::get
164    pub fn limited_get_mut(&mut self, pos: &ChunkPos) -> Option<&mut Option<Arc<RwLock<Chunk>>>> {
165        if !self.in_range(pos) {
166            return None;
167        }
168
169        let index = self.index_from_chunk_pos(pos);
170
171        Some(&mut self.chunks[index])
172    }
173
174    /// Set a chunk in the shared storage and reference it from the limited
175    /// storage.
176    ///
177    /// Use [`Self::limited_set`] if you already have an `Arc<RwLock<Chunk>>`.
178    ///
179    /// # Panics
180    /// If the chunk is not in the render distance.
181    pub fn set(&mut self, pos: &ChunkPos, chunk: Option<Chunk>, chunk_storage: &mut ChunkStorage) {
182        let new_chunk = chunk.map(|c| chunk_storage.upsert(*pos, c));
183        self.limited_set(pos, new_chunk);
184    }
185
186    /// Set a chunk in our limited storage, useful if your chunk is already
187    /// referenced somewhere else and you want to make it also be referenced by
188    /// this storage.
189    ///
190    /// Use [`Self::set`] if you don't already have an `Arc<RwLock<Chunk>>`.
191    ///
192    /// # Panics
193    /// If the chunk is not in the render distance.
194    pub fn limited_set(&mut self, pos: &ChunkPos, chunk: Option<Arc<RwLock<Chunk>>>) {
195        if let Some(chunk_mut) = self.limited_get_mut(pos) {
196            *chunk_mut = chunk;
197        }
198    }
199
200    /// Get an iterator over all the chunks in the storage.
201    pub fn chunks(&self) -> impl Iterator<Item = &Option<Arc<RwLock<Chunk>>>> {
202        self.chunks.iter()
203    }
204}
205
206impl Debug for PartialChunkStorage {
207    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208        f.debug_struct("PartialChunkStorage")
209            .field("view_center", &self.view_center)
210            .field("chunk_radius", &self.chunk_radius)
211            .field("view_range", &self.view_range)
212            // .field("chunks", &self.chunks)
213            .field("chunks", &format_args!("{} items", self.chunks.len()))
214            .finish()
215    }
216}
217
218impl Default for PartialChunkStorage {
219    fn default() -> Self {
220        Self::new(8)
221    }
222}
223
224pub fn in_range_for_view_center_and_radius(
225    chunk_pos: &ChunkPos,
226    view_center: ChunkPos,
227    chunk_radius: u32,
228) -> bool {
229    (chunk_pos.x - view_center.x).unsigned_abs() <= chunk_radius
230        && (chunk_pos.z - view_center.z).unsigned_abs() <= chunk_radius
231}
232
233#[cfg(test)]
234mod tests {
235    use azalea_core::position::ChunkPos;
236
237    use crate::chunk::partial::PartialChunkStorage;
238
239    #[test]
240    fn test_chunk_pos_from_index() {
241        let mut partial_chunk_storage = PartialChunkStorage::new(5);
242        partial_chunk_storage.update_view_center(ChunkPos::new(0, -1));
243        assert_eq!(
244            partial_chunk_storage.chunk_pos_from_index(
245                partial_chunk_storage.index_from_chunk_pos(&ChunkPos::new(2, -1))
246            ),
247            ChunkPos::new(2, -1),
248        );
249    }
250}