Skip to main content

silverfish/
region.rs

1//! `region` contains the core [`Region`] struct used to set/get blocks within the specified Region.  
2//!
3//! Contains functions for constructing a [`Region`] and writing itself to a specified buffer.  
4
5use crate::{
6    BLOCKS_PER_REGION, Coords,
7    chunk::ChunkData,
8    config::Config,
9    error::{Error, Result},
10    nbt::Block,
11};
12use ahash::AHashMap;
13use dashmap::{
14    DashMap,
15    mapref::one::{Ref, RefMut},
16};
17use mca::{CompressionType, RegionIter, RegionReader, RegionWriter};
18use simdnbt::owned::{BaseNbt, Nbt, NbtCompound, NbtList, NbtTag};
19use std::{
20    fmt::Debug,
21    io::{Cursor, Read, Write},
22    ops::{Deref, Range},
23};
24
25/// An in-memory region to read and write blocks to the chunks within.  
26#[derive(Clone)]
27pub struct Region {
28    /// The chunks within the Region, mapped to their coordinates
29    pub chunks: DashMap<(u8, u8), ChunkData>,
30    /// Config on how it should handle certain scenarios
31    pub(crate) config: Config,
32    /// Coordinates for this specific region
33    pub region_coords: (i32, i32),
34}
35
36/// Just a [`Block`] but with a set of coordinates attached to them.  
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct BlockWithCoordinate {
39    /// The attached coordinates related to the block.  
40    pub coordinates: Coords,
41    /// The block itself.  
42    pub block: Block,
43}
44
45impl Region {
46    /// Whatever status the chunks needs to be to allow modification.  
47    pub(crate) const REQUIRED_STATUS: &'static str = "minecraft:full";
48    /// the minimum dataversion that this crate works with.  
49    ///
50    /// This is due the massive structural changes in how the nbt is stored that was introduced in `21w39a` & `21w43a`
51    pub const MIN_DATA_VERSION: i32 = 2860;
52
53    /// Returns the Region's [`Config`]
54    pub fn get_config(&self) -> &Config {
55        &self.config
56    }
57
58    /// Sets the [`Config`] and re-inits all internal buffers if `world_height` is different.  
59    ///
60    /// Returns `true` if the `world_height` was different and it did reset all internal buffers.  
61    pub fn set_config(&mut self, config: Config) -> Result<bool> {
62        let changed_world_height = if self.config.world_height != config.world_height {
63            self.set_world_height(config.world_height.clone())?;
64            true
65        } else {
66            false
67        };
68
69        self.config = config;
70
71        Ok(changed_world_height)
72    }
73
74    /// Updates the world height in the [`Config`].  
75    ///
76    /// #### Why is `world_height` private in [`Config`] and only mutated through [`Region`] ?
77    /// Well, when a region is first constructed it defaults an internal bitset to a certain size.  
78    /// for performance reasons, and if you update world_height, we also need to re-init that bitset.
79    /// *(this function also clears all internal buffers related to biomes)*.
80    /// and a config can only be mutated on a region after the consumer has gotten it.  
81    /// So when you get a region, it always defaults to Minecrafts vanilla range of world_height.  
82    ///
83    /// ## Example
84    /// ```
85    /// # use silverfish::Region;
86    /// # let mut region = Region::default();
87    /// region.set_world_height(128..320);
88    /// # Ok::<(), silverfish::Error>(())
89    /// ```
90    pub fn set_world_height(&mut self, range: Range<isize>) -> Result<()> {
91        // clear all the chunks buffers
92        let world_height_count = range.clone().count();
93        for x in 0..32 {
94            for z in 0..32 {
95                let mut chunk = self.get_chunk_mut(x, z)?;
96                chunk.pending_blocks = AHashMap::new();
97                chunk.pending_biomes = AHashMap::new();
98                chunk.seen_blocks = ChunkData::block_bitset(world_height_count);
99                chunk.seen_biomes = ChunkData::biome_bitset(world_height_count);
100            }
101        }
102
103        self.config.world_height = range;
104
105        Ok(())
106    }
107
108    /// Creates an empty [`Region`] with no chunks or anything.  
109    ///
110    /// [`Config::create_chunk_if_missing`] will set to `true` from this  
111    pub fn empty(region_coords: (i32, i32)) -> Self {
112        let config = Config {
113            create_chunk_if_missing: true,
114            ..Default::default()
115        };
116
117        Self {
118            chunks: DashMap::new(),
119            region_coords,
120            config,
121        }
122    }
123
124    /// Creates a full [`Region`] with empty chunks in it.  
125    pub fn full_empty(region_coords: (i32, i32)) -> Self {
126        let mut chunks = AHashMap::new();
127
128        for x in 0..mca::REGION_SIZE as u8 {
129            for z in 0..mca::REGION_SIZE as u8 {
130                chunks.insert(
131                    (x, z),
132                    get_empty_chunk((x, z), region_coords, Config::DEFAULT_WORLD_HEIGHT),
133                );
134            }
135        }
136
137        Self::from_nbt(chunks, region_coords)
138    }
139
140    /// Creates a new [`Region`] with chunks from `chunks`
141    pub fn from_nbt(chunks: AHashMap<(u8, u8), NbtCompound>, region_coords: (i32, i32)) -> Self {
142        let config = Config::default();
143
144        let chunks = chunks
145            .into_iter()
146            .map(|(k, v)| (k, ChunkData::new(v, config.world_height.clone())))
147            .collect();
148
149        Self {
150            chunks,
151            region_coords,
152            config,
153        }
154    }
155
156    /// Creates a [`Region`] from an already existing region
157    ///
158    /// ## Example
159    /// ```
160    /// # use silverfish::Region;
161    /// # use std::fs::File;
162    /// let mut region = Region::from_region(&mut File::open("tests/full_region.mca")?, (0, 0))?;
163    /// # Ok::<(), silverfish::Error>(())
164    /// ```
165    pub fn from_region<R: Read>(reader: &mut R, region_coords: (i32, i32)) -> Result<Self> {
166        let mut bytes = Vec::with_capacity(4_194_304); // 4 MB, just an average start on the vec to skip a few common re-allocations
167        reader.read_to_end(&mut bytes)?;
168        let region_reader = RegionReader::new(&bytes)?;
169
170        let mut chunks = AHashMap::new();
171        for (i, chunk) in region_reader.iter().enumerate() {
172            let chunk = chunk?;
173            let chunk = match chunk {
174                Some(c) => c.decompress()?,
175                None => continue,
176            };
177
178            let chunk_nbt = match simdnbt::owned::read(&mut Cursor::new(&chunk))? {
179                Nbt::Some(nbt) => nbt.as_compound(),
180                Nbt::None => return Err(Error::InvalidNbtType("base_nbt")),
181            };
182            let (x, z) = RegionIter::get_chunk_coordinate(i);
183
184            chunks.insert((x as u8, z as u8), chunk_nbt);
185        }
186
187        Ok(Self::from_nbt(chunks, region_coords))
188    }
189
190    /// Writes the region to the specified writer.  
191    ///
192    /// **Note:** If you haven't called [`Region::write_blocks`] this will most likely  
193    /// just return whatever input you gave it initially
194    ///
195    /// ## Example
196    /// ```
197    /// # let mut region = silverfish::Region::default();
198    /// let mut buf = vec![];
199    /// region.write(&mut buf)?;
200    /// # Ok::<(), silverfish::Error>(())
201    /// ```
202    pub fn write<W: Write>(self, writer: &mut W) -> Result<()> {
203        let mut region_writer = RegionWriter::new();
204
205        for ((x, z), chunk_data) in self.chunks {
206            let mut raw_nbt = vec![];
207            let wrapped = Nbt::Some(BaseNbt::new("", chunk_data.nbt));
208            wrapped.write(&mut raw_nbt);
209            region_writer.push_chunk_with_compression(&raw_nbt, (x, z), CompressionType::Zlib)?;
210        }
211
212        region_writer.write(writer)?;
213
214        Ok(())
215    }
216
217    /// Returns the chunk nbt data found at the given chunk coordinates.  
218    ///
219    /// Do note that these chunk coordinates are local to within the region itself.  
220    ///
221    /// ## Example
222    /// ```
223    /// # let mut region = silverfish::Region::default();
224    /// let chunk = region.get_chunk(5, 17)?;
225    /// # Ok::<(), silverfish::Error>(())
226    /// ```
227    pub fn get_chunk(&self, x: u8, z: u8) -> Result<Option<Ref<'_, (u8, u8), ChunkData>>> {
228        if x >= mca::REGION_SIZE as u8 || z >= mca::REGION_SIZE as u8 {
229            return Err(Error::ChunkOutOfRegionBounds(x, z));
230        }
231
232        Ok(self.chunks.get(&(x, z)))
233    }
234
235    /// Returns the raw [`ChunkData`].  
236    #[cfg(test)]
237    pub(crate) fn get_raw_chunk(
238        &self,
239        x: u8,
240        z: u8,
241    ) -> Result<Option<Ref<'_, (u8, u8), ChunkData>>> {
242        Ok(self.chunks.get(&(x, z)))
243    }
244
245    /// Returns a mutable reference to a chunk entry within the region.  
246    ///
247    /// Do note that these chunk coordinates are local to within the region itself.
248    ///
249    /// ## Example
250    /// ```
251    /// # let mut region = silverfish::Region::default();
252    /// let mut chunk = region.get_chunk_mut(1, 13)?;
253    /// let _ = chunk.set_block((6, 124, 14), "anvil")?;
254    /// # Ok::<(), silverfish::Error>(())
255    /// ```
256    pub fn get_chunk_mut<'a>(&'a self, x: u8, z: u8) -> Result<RefMut<'a, (u8, u8), ChunkData>> {
257        if x >= mca::REGION_SIZE as u8 || z >= mca::REGION_SIZE as u8 {
258            return Err(Error::ChunkOutOfRegionBounds(x, z));
259        }
260
261        match self.chunks.get_mut(&(x, z)) {
262            Some(ch) => return Ok(ch),
263            None if self.config.create_chunk_if_missing => {
264                self.chunks.insert(
265                    (x, z),
266                    ChunkData::new(
267                        get_empty_chunk(
268                            (x, z),
269                            self.region_coords,
270                            self.config.world_height.clone(),
271                        ),
272                        self.config.world_height.clone(),
273                    ),
274                );
275                return Ok(self.chunks.get_mut(&(x, z)).unwrap());
276            }
277            None => return Err(Error::TriedToModifyMissingChunk(x, z)),
278        };
279    }
280
281    /// Returns if all chunks inside the region has been generated and is [`Region::REQUIRED_STATUS`]
282    pub fn is_region_generated(&self) -> Result<bool> {
283        for x in 0..mca::REGION_SIZE as u8 {
284            for z in 0..mca::REGION_SIZE as u8 {
285                let chunk = self.get_chunk(x, z)?;
286                match chunk {
287                    Some(ch) => match ch.nbt.string("Status") {
288                        Some(status) => {
289                            if status.to_str() != Region::REQUIRED_STATUS {
290                                return Ok(false);
291                            }
292                        }
293                        None => return Ok(false),
294                    },
295                    None => return Ok(false),
296                };
297            }
298        }
299
300        Ok(true)
301    }
302}
303
304impl Debug for Region {
305    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306        write!(
307            f,
308            "Region({}, {})\n  > chunks: {}\n  > {:?}",
309            self.region_coords.0,
310            self.region_coords.1,
311            self.chunks.len(),
312            self.config
313        )
314    }
315}
316
317impl Into<(Coords, Block)> for BlockWithCoordinate {
318    fn into(self) -> (Coords, Block) {
319        (self.coordinates, self.block)
320    }
321}
322
323impl<'a> Into<(&'a Coords, &'a Block)> for &'a BlockWithCoordinate {
324    fn into(self) -> (&'a Coords, &'a Block) {
325        (&self.coordinates, &self.block)
326    }
327}
328
329// returns the bit count for whatever palette_len.
330// we dont actually need to calculate anything fancy
331// palette_len cant be more than 4096 so we can pre set it up
332pub(crate) fn get_block_bit_count(len: usize) -> u32 {
333    match len {
334        0..=16 => 4, // i believe this should be 0..=16 since the old math had a .max(4) at the end, thus always getting 4 at the minimum
335        17..=32 => 5,
336        33..=64 => 6,
337        65..=128 => 7,
338        129..=256 => 8,
339        257..=512 => 9,
340        513..=1024 => 10,
341        1025..=2048 => 11,
342        2049..=4096 => 12,
343        _ => 13,
344    }
345}
346
347pub(crate) fn get_biome_bit_count(len: usize) -> u32 {
348    match len {
349        0 | 1 => 0,
350        2 => 1,
351        3 | 4 => 2,
352        5..=8 => 3,
353        9..=16 => 4,
354        17..=32 => 5,
355        _ => 6,
356    }
357}
358
359/// Generates an empty chunk with plains as the default biome and air in all sections  
360///
361/// DataVersion is defaulted to [`Region::MIN_DATA_VERSION`]
362pub fn get_empty_chunk(
363    coords: (u8, u8),
364    region_coords: (i32, i32),
365    world_height: Range<isize>,
366) -> NbtCompound {
367    let mut sections: Vec<NbtCompound> =
368        Vec::with_capacity(Config::DEFAULT_WORLD_HEIGHT.clone().count() / ChunkData::WIDTH);
369    let (section_start, section_end) = (
370        (world_height.start / ChunkData::WIDTH as isize) as i8,
371        (world_height.end / ChunkData::WIDTH as isize) as i8,
372    );
373
374    // one thing would be to move these to world_height.start / 16 and world_height.end / 16
375    // but would be a bit annoying to move around the data to get world_height into this function.
376    for y in section_start..section_end {
377        let biomes = NbtCompound::from_values(vec![(
378            "palette".into(),
379            NbtTag::List(NbtList::String(vec!["minecraft:plains".into()])),
380        )]);
381        let block_states = NbtCompound::from_values(vec![(
382            "palette".into(),
383            NbtTag::List(NbtList::Compound(vec![NbtCompound::from_values(vec![(
384                "Name".into(),
385                NbtTag::String("minecraft:air".into()),
386            )])])),
387        )]);
388
389        sections.push(NbtCompound::from_values(vec![
390            ("Y".into(), NbtTag::Byte(y)),
391            ("biomes".into(), NbtTag::Compound(biomes)),
392            ("block_states".into(), NbtTag::Compound(block_states)),
393        ]));
394    }
395
396    let chunk = NbtCompound::from_values(vec![
397        (
398            "Status".into(),
399            NbtTag::String(Region::REQUIRED_STATUS.into()),
400        ),
401        ("DataVersion".into(), NbtTag::Int(Region::MIN_DATA_VERSION)),
402        ("sections".into(), NbtTag::List(NbtList::Compound(sections))),
403        ("block_entities".into(), NbtTag::List(NbtList::Empty)),
404        ("isLightOn".into(), NbtTag::Byte(0)),
405        (
406            "xPos".into(),
407            NbtTag::Int((region_coords.0 * mca::REGION_SIZE as i32) + coords.0 as i32),
408        ),
409        (
410            "zPos".into(),
411            NbtTag::Int((region_coords.1 * mca::REGION_SIZE as i32) + coords.1 as i32),
412        ),
413    ]);
414
415    chunk
416}
417
418/// Converts a piece of global world coordinates to coordinates within it's region.  
419///
420/// ## Example
421/// ```
422/// # use silverfish::to_region_local;
423/// let coords = (-841, -17, 4821);
424/// let local_coords = to_region_local(coords);
425/// assert_eq!(local_coords, (183, -17, 213))
426/// ```
427pub fn to_region_local(coords: (i32, i32, i32)) -> Coords {
428    (
429        (coords.0 as i32 & (BLOCKS_PER_REGION - 1) as i32) as u32,
430        coords.1,
431        (coords.2 as i32 & (BLOCKS_PER_REGION - 1) as i32) as u32,
432    )
433        .into()
434}
435
436/// Checks the data_version and status of the chunk if it's valid to operate on
437pub(crate) fn is_valid_chunk(chunk: &NbtCompound, coordinate: (u8, u8)) -> Result<()> {
438    let status = chunk
439        .string("Status")
440        .ok_or(Error::MissingNbtTag("Status"))?
441        .to_str();
442    if status != Region::REQUIRED_STATUS {
443        return Err(Error::NotFullyGenerated {
444            chunk: coordinate,
445            status: status.into_owned(),
446        });
447    }
448
449    let data_version = chunk
450        .int("DataVersion")
451        .ok_or(Error::MissingNbtTag("DataVersion"))?;
452    if data_version < Region::MIN_DATA_VERSION {
453        return Err(Error::UnsupportedVersion {
454            chunk: coordinate,
455            data_version,
456        });
457    }
458
459    Ok(())
460}
461
462/// Removes unused elements from the palette and "cleans" it.  
463pub(crate) fn clean_palette<T>(data: &mut [i64], data_len: usize, palette: &mut Vec<T>) {
464    let mut palette_count: Vec<i32> = vec![0; palette.len()];
465    for index in data.deref() {
466        palette_count[*index as usize] += 1;
467    }
468
469    let mut palette_offsets: Vec<i64> = vec![0; palette.len()];
470
471    let mut len = palette.len();
472    let mut i = len as i32 - 1;
473    while i >= 0 {
474        if palette_count[i as usize] == 0 {
475            palette.remove(i as usize);
476            len -= 1;
477
478            for j in (i as usize)..palette_count.len() {
479                palette_offsets[j as usize] += 1;
480            }
481        }
482        i -= 1;
483    }
484
485    for block in 0..data_len {
486        data[block] -= palette_offsets[data[block] as usize];
487    }
488}
489
490impl Default for Region {
491    fn default() -> Self {
492        Region::full_empty((0, 0))
493    }
494}
495
496#[cfg(test)]
497mod test {
498    use super::*;
499    use std::io::BufReader;
500
501    #[test]
502    fn same_region_local_coordinates() {
503        let coords = (52, -81, 381);
504        let local = to_region_local(coords);
505        assert_eq!((52, -81, 381), local);
506    }
507
508    #[test]
509    fn region_local_coordinates() {
510        let coords = (851, 85, -481);
511        let local = to_region_local(coords);
512        assert_eq!((339, 85, 31), local);
513    }
514
515    #[test]
516    fn empty_chunk() -> Result<()> {
517        let chunk = get_empty_chunk((15, 9), (2, -5), Config::DEFAULT_WORLD_HEIGHT);
518        let data_version = chunk
519            .int("DataVersion")
520            .ok_or(Error::MissingNbtTag("DataVersion"))?;
521        let x_pos = chunk.int("xPos").ok_or(Error::MissingNbtTag("xPos"))?;
522        let z_pos = chunk.int("zPos").ok_or(Error::MissingNbtTag("zPos"))?;
523        let sections = chunk
524            .list("sections")
525            .ok_or(Error::MissingNbtTag("sections"))?
526            .compounds()
527            .ok_or(Error::InvalidNbtList("!= compounds"))?;
528
529        assert_eq!(data_version, Region::MIN_DATA_VERSION);
530        assert_eq!(x_pos, 79);
531        assert_eq!(z_pos, -151);
532        assert_eq!(sections.len(), 24);
533
534        Ok(())
535    }
536
537    #[test]
538    fn block_bit_count() {
539        assert_eq!(get_block_bit_count(0), 4);
540        assert_eq!(get_block_bit_count(58), 6);
541        assert_eq!(get_block_bit_count(1754), 11);
542        assert_eq!(get_block_bit_count(8572728), 13);
543    }
544
545    #[test]
546    fn biome_bit_count() {
547        assert_eq!(get_biome_bit_count(0), 0);
548        assert_eq!(get_biome_bit_count(4), 2);
549        assert_eq!(get_biome_bit_count(7), 3);
550        assert_eq!(get_biome_bit_count(25), 5);
551        assert_eq!(get_biome_bit_count(8572728), 6);
552    }
553
554    #[test]
555    fn empty_region() -> Result<()> {
556        let region = Region::empty((0, 0));
557        assert_eq!(region.chunks.len(), 0);
558        assert!(region.get_chunk(0, 0)?.is_none());
559        assert_eq!(region.region_coords, (0, 0));
560        Ok(())
561    }
562
563    #[test]
564    fn full_empty_region() {
565        let region = Region::default();
566        assert_eq!(region.chunks.len(), 1024);
567    }
568
569    #[test]
570    fn empty_from_nbt_region() {
571        let chunks = AHashMap::new();
572        let region = Region::from_nbt(chunks, (0, 0));
573        assert_eq!(region.chunks.len(), 0);
574    }
575
576    #[test]
577    fn from_nbt_region() -> Result<()> {
578        let mut chunks = AHashMap::new();
579        chunks.insert(
580            (4, 8),
581            get_empty_chunk((4, 8), (0, 0), Config::DEFAULT_WORLD_HEIGHT),
582        );
583
584        let region = Region::from_nbt(chunks, (0, 0));
585        assert_eq!(region.chunks.len(), 1);
586        assert_eq!(region.get_raw_chunk(4, 8)?.unwrap().pending_blocks.len(), 0);
587        assert_eq!(
588            region
589                .get_raw_chunk(4, 8)?
590                .unwrap()
591                .seen_blocks
592                .count_ones(..),
593            0
594        );
595        assert_eq!(region.region_coords, (0, 0));
596
597        Ok(())
598    }
599
600    const TEST_REGION: &[u8] = include_bytes!("../tests/full_region.mca");
601
602    #[test]
603    fn from_region_region() -> Result<()> {
604        let mut bytes = BufReader::new(TEST_REGION);
605        let region = Region::from_region(&mut bytes, (0, 0))?;
606        assert_eq!(region.chunks.len(), 1024);
607        Ok(())
608    }
609
610    const EMPTY_REGION: &[u8] = include_bytes!("../tests/empty_region.mca");
611
612    #[test]
613    fn write_region() -> Result<()> {
614        let mut bytes = BufReader::new(EMPTY_REGION);
615        let region = Region::from_region(&mut bytes, (0, 0))?;
616        let mut new_region_buf = vec![];
617        region.write(&mut new_region_buf)?;
618
619        assert_eq!(new_region_buf, EMPTY_REGION);
620
621        Ok(())
622    }
623
624    #[test]
625    fn get_chunk() -> Result<()> {
626        let mut chunks = AHashMap::new();
627        chunks.insert(
628            (9, 1),
629            get_empty_chunk((9, 1), (0, 0), Config::DEFAULT_WORLD_HEIGHT),
630        );
631
632        let region = Region::from_nbt(chunks, (0, 0));
633
634        assert!(region.get_chunk(9, 1)?.is_some());
635        assert!(region.get_chunk(1, 9)?.is_none());
636
637        Ok(())
638    }
639
640    #[test]
641    fn fully_generated() -> Result<()> {
642        let region = Region::default();
643        assert!(region.is_region_generated()?);
644        Ok(())
645    }
646}