// SPDX-License-Identifier: AGPL-3.0-only //! The `SYNR` region index: the header table and side tables that frame a region file. //! //! A region file is this fixed header and its three tables, followed by the `SYNC` chunk //! records the header table points at. This module owns only the index; the record bytes //! and their placement are managed by the durability layer. All integers are little-endian. use std::collections::BTreeMap; use super::cursor::Reader; use super::error::SaveError; use crate::world::ChunkPos; /// The magic tag every region file begins with. const MAGIC: [u8; 4] = *b"SYNR"; /// The current region-file framing version, written on encode and checked on decode. pub const REGION_FORMAT_VERSION: u32 = 1; /// The location and flags of one chunk record within the region file. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct HeaderEntry { /// The byte offset of the record from the start of the region file. pub offset: u64, /// The length of the record in bytes. pub length: u32, /// Record flags; currently always zero. pub flags: u32, } /// A reclaimable span of free space left by a removed or shrunken record. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct FreeSpan { /// The byte offset of the free span from the start of the region file. pub offset: u64, /// The length of the free span in bytes. pub length: u32, } /// The in-memory index of a region file: where every resident chunk record lives, the free spans between them, and the worldgen-version exceptions. #[derive(Clone, Debug, PartialEq, Eq)] pub struct RegionIndex { /// Monotonic cache-coherence counter for derived LOD tiles; only ever increases. region_tile_version: u32, /// The worldgen version most chunks in the region are pinned to; the stamp table holds only exceptions. base_worldgen_version: u32, /// Location of every resident chunk record, keyed by chunk position. header_table: BTreeMap, /// Reclaimable holes in the file, in no particular order. free_list: Vec, /// Worldgen-version exceptions: chunks pinned to a version other than `base_worldgen_version`. stamps: BTreeMap, } impl RegionIndex { /// Creates an empty index whose chunks default to `base_worldgen_version`. #[must_use] pub fn new(base_worldgen_version: u32) -> Self { Self { region_tile_version: 0, base_worldgen_version, header_table: BTreeMap::new(), free_list: Vec::new(), stamps: BTreeMap::new(), } } /// Returns the monotonic LOD-tile version. #[must_use] pub fn region_tile_version(&self) -> u32 { self.region_tile_version } /// Increments the LOD-tile version; called once per save that commits a rebuild-triggering change. pub fn bump_tile_version(&mut self) { self.region_tile_version = self.region_tile_version.saturating_add(1); } /// Returns the region's base worldgen version. #[must_use] pub fn base_worldgen_version(&self) -> u32 { self.base_worldgen_version } /// Looks up the record location for `pos`, if the chunk is resident. #[must_use] pub fn entry(&self, pos: ChunkPos) -> Option<&HeaderEntry> { self.header_table.get(&pos) } /// Records the location of the chunk at `pos`, replacing any existing entry. pub fn insert(&mut self, pos: ChunkPos, entry: HeaderEntry) { self.header_table.insert(pos, entry); } /// Removes the chunk at `pos` from the header table, returning its former location. pub fn remove(&mut self, pos: ChunkPos) -> Option { self.header_table.remove(&pos) } /// Iterates the resident records in ascending position order. pub fn entries(&self) -> impl Iterator { self.header_table.iter() } /// Returns the pinned worldgen version for `pos`: its stamp exception, or the region base. #[must_use] pub fn worldgen_version(&self, pos: ChunkPos) -> u32 { self.stamps .get(&pos) .copied() .unwrap_or(self.base_worldgen_version) } /// Pins `pos` to `version`, recording it as a stamp exception only when it differs from the base. pub fn set_worldgen_version(&mut self, pos: ChunkPos, version: u32) { if version == self.base_worldgen_version { self.stamps.remove(&pos); } else { self.stamps.insert(pos, version); } } /// Returns the reclaimable free spans. #[must_use] pub fn free_spans(&self) -> &[FreeSpan] { &self.free_list } /// Adds a reclaimable free span. pub fn push_free(&mut self, span: FreeSpan) { self.free_list.push(span); } /// Serializes the index to its on-disk framing bytes. /// /// # Errors /// /// Returns [`SaveError::PayloadTooLarge`] if the header, free-list, or stamp table holds more than `u32::MAX` entries. pub fn encode(&self) -> Result, SaveError> { let mut out = Vec::new(); out.extend_from_slice(&MAGIC); out.extend_from_slice(®ION_FORMAT_VERSION.to_le_bytes()); out.extend_from_slice(&self.base_worldgen_version.to_le_bytes()); out.extend_from_slice(&self.region_tile_version.to_le_bytes()); out.extend_from_slice(&len_u32(self.header_table.len())?.to_le_bytes()); for (pos, entry) in &self.header_table { out.extend_from_slice(&pos.x.to_le_bytes()); out.extend_from_slice(&pos.y.to_le_bytes()); out.extend_from_slice(&pos.z.to_le_bytes()); out.extend_from_slice(&entry.offset.to_le_bytes()); out.extend_from_slice(&entry.length.to_le_bytes()); out.extend_from_slice(&entry.flags.to_le_bytes()); } out.extend_from_slice(&len_u32(self.free_list.len())?.to_le_bytes()); for span in &self.free_list { out.extend_from_slice(&span.offset.to_le_bytes()); out.extend_from_slice(&span.length.to_le_bytes()); } // The stamp value is widened to u32 to match ChunkData's u32 worldgen version and avoid // truncation; the spec's u16 stamp field (Save format.md) is treated as an oversight. out.extend_from_slice(&len_u32(self.stamps.len())?.to_le_bytes()); for (pos, version) in &self.stamps { out.extend_from_slice(&pos.x.to_le_bytes()); out.extend_from_slice(&pos.y.to_le_bytes()); out.extend_from_slice(&pos.z.to_le_bytes()); out.extend_from_slice(&version.to_le_bytes()); } Ok(out) } /// Parses a region index from its framing bytes, ignoring any chunk records that follow it. /// /// # Errors /// /// Returns [`SaveError::Truncated`] if the buffer ends mid-field, [`SaveError::BadMagic`] if the leading tag is not the region magic, or [`SaveError::UnsupportedVersion`] if the format version is not recognised. pub fn decode(bytes: &[u8]) -> Result { let mut reader = Reader::new(bytes); let magic = reader.take_array::<4>()?; if magic != MAGIC { return Err(SaveError::BadMagic { expected: MAGIC, found: magic, }); } let format_version = u32::from_le_bytes(reader.take_array()?); if format_version != REGION_FORMAT_VERSION { return Err(SaveError::UnsupportedVersion { expected: REGION_FORMAT_VERSION, found: format_version, }); } let base_worldgen_version = u32::from_le_bytes(reader.take_array()?); let region_tile_version = u32::from_le_bytes(reader.take_array()?); let header_len = u32::from_le_bytes(reader.take_array()?); let mut header_table = BTreeMap::new(); for _ in 0..header_len { let pos = read_pos(&mut reader)?; let entry = HeaderEntry { offset: u64::from_le_bytes(reader.take_array()?), length: u32::from_le_bytes(reader.take_array()?), flags: u32::from_le_bytes(reader.take_array()?), }; header_table.insert(pos, entry); } let free_len = u32::from_le_bytes(reader.take_array()?); let mut free_list = Vec::with_capacity(free_len as usize); for _ in 0..free_len { free_list.push(FreeSpan { offset: u64::from_le_bytes(reader.take_array()?), length: u32::from_le_bytes(reader.take_array()?), }); } let stamp_len = u32::from_le_bytes(reader.take_array()?); let mut stamps = BTreeMap::new(); for _ in 0..stamp_len { let pos = read_pos(&mut reader)?; stamps.insert(pos, u32::from_le_bytes(reader.take_array()?)); } Ok(Self { region_tile_version, base_worldgen_version, header_table, free_list, stamps, }) } } /// Reads a chunk position as three little-endian `i32`s. /// /// # Errors /// /// Returns [`SaveError::Truncated`] if fewer than twelve bytes remain. fn read_pos(reader: &mut Reader) -> Result { let x = i32::from_le_bytes(reader.take_array()?); let y = i32::from_le_bytes(reader.take_array()?); let z = i32::from_le_bytes(reader.take_array()?); Ok(ChunkPos::new(x, y, z)) } /// Narrows a table length to the `u32` the framing uses, failing loudly rather than truncating. /// /// # Errors /// /// Returns [`SaveError::PayloadTooLarge`] if `len` exceeds `u32::MAX`. fn len_u32(len: usize) -> Result { u32::try_from(len).map_err(|_| SaveError::PayloadTooLarge { len }) } #[cfg(test)] #[path = "../tests/region.rs"] mod tests;