diff --git a/src/bin/minedmap/common.rs b/src/bin/minedmap/common.rs index df15d99..4183b64 100644 --- a/src/bin/minedmap/common.rs +++ b/src/bin/minedmap/common.rs @@ -1,3 +1,5 @@ +//! Common data types and functions used by multiple generation steps + use std::{ collections::{BTreeMap, BTreeSet}, fmt::Debug, @@ -9,12 +11,19 @@ use serde::{Deserialize, Serialize}; use super::core::{io::fs::FileMetaVersion, resource::Biome, types::*, world::layer}; -// Increase to force regeneration of all output files +/// MinedMap data version number +/// +/// Increase to force regeneration of all output files pub const FILE_META_VERSION: FileMetaVersion = FileMetaVersion(0); +/// Coordinate pair of a generated tile +/// +/// Each tile corresponds to one Minecraft region file #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct TileCoords { + /// The X coordinate pub x: i32, + /// The Z coordinate pub z: i32, } @@ -24,11 +33,16 @@ impl Debug for TileCoords { } } +/// Set of tile coordinates +/// +/// Used to store list of populated tiles for each mipmap level in the +/// viewer metadata file. #[derive(Debug, Clone, Default, Serialize)] #[serde(transparent)] pub struct TileCoordMap(pub BTreeMap>); impl TileCoordMap { + /// Checks whether the map contains a given coordinate pair pub fn contains(&self, coords: TileCoords) -> bool { let Some(xs) = self.0.get(&coords.z) else { return false; @@ -38,39 +52,62 @@ impl TileCoordMap { } } +/// Data structure for storing chunk data between processing and rendering steps #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProcessedChunk { + /// Block type data pub blocks: Box, + /// Biome data pub biomes: Box, + /// Block height/depth data pub depths: Box, } +/// Data structure for storing region data between processing and rendering steps #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct ProcessedRegion { + /// List of biomes used in the region + /// + /// Indexed by [ProcessedChunk] biome data pub biome_list: IndexSet, + /// Processed chunk data pub chunks: ChunkArray>>, } -pub struct Config { - pub num_threads: usize, - pub region_dir: PathBuf, - pub processed_dir: PathBuf, - pub output_dir: PathBuf, - pub level_dat_path: PathBuf, - pub metadata_path: PathBuf, -} - +/// Derives a filename from region coordinates and a file extension +/// +/// Can be used for input regions, processed data or rendered tiles fn coord_filename(coords: TileCoords, ext: &str) -> String { format!("r.{}.{}.{}", coords.x, coords.z, ext) } +/// Tile kind corresponding to a map layer #[derive(Debug, Clone, Copy)] pub enum TileKind { + /// Regular map tile contains block colors Map, + /// Lightmap tile for illumination layer Lightmap, } +/// Common configuration based on command line arguments +pub struct Config { + /// Number of threads for parallel processing + pub num_threads: usize, + /// Path of input region directory + pub region_dir: PathBuf, + /// Path of input `level.dat` file + pub level_dat_path: PathBuf, + /// Base path for storage of rendered tile data + pub output_dir: PathBuf, + /// Path for storage of intermediate processed data files + pub processed_dir: PathBuf, + /// Path of viewer metadata file + pub metadata_path: PathBuf, +} + impl Config { + /// Crates a new [Config] from [command line arguments](super::Args) pub fn new(args: &super::Args) -> Self { let num_threads = match args.jobs { Some(0) => num_cpus::get(), @@ -79,30 +116,33 @@ impl Config { }; let region_dir = [&args.input_dir, Path::new("region")].iter().collect(); - let processed_dir = [&args.output_dir, Path::new("processed")].iter().collect(); let level_dat_path = [&args.input_dir, Path::new("level.dat")].iter().collect(); + let processed_dir = [&args.output_dir, Path::new("processed")].iter().collect(); let metadata_path = [&args.output_dir, Path::new("info.json")].iter().collect(); Config { num_threads, region_dir, - processed_dir, - output_dir: args.output_dir.clone(), level_dat_path, + output_dir: args.output_dir.clone(), + processed_dir, metadata_path, } } + /// Constructs the path to an input region file pub fn region_path(&self, coords: TileCoords) -> PathBuf { let filename = coord_filename(coords, "mca"); [&self.region_dir, Path::new(&filename)].iter().collect() } + /// Constructs the path of an intermediate processed region file pub fn processed_path(&self, coords: TileCoords) -> PathBuf { let filename = coord_filename(coords, "bin"); [&self.processed_dir, Path::new(&filename)].iter().collect() } + /// Constructs the base output path for a [TileKind] and mipmap level pub fn tile_dir(&self, kind: TileKind, level: usize) -> PathBuf { let prefix = match kind { TileKind::Map => "map", @@ -112,6 +152,7 @@ impl Config { [&self.output_dir, Path::new(&dir)].iter().collect() } + /// Constructs the path of an output tile image pub fn tile_path(&self, kind: TileKind, level: usize, coords: TileCoords) -> PathBuf { let filename = coord_filename(coords, "png"); let dir = self.tile_dir(kind, level); @@ -119,6 +160,7 @@ impl Config { } } +/// Copies a chunk image into a region tile pub fn overlay_chunk(image: &mut I, chunk: &J, coords: ChunkCoords) where I: image::GenericImage, diff --git a/src/bin/minedmap/main.rs b/src/bin/minedmap/main.rs index 046fbb5..925a38b 100644 --- a/src/bin/minedmap/main.rs +++ b/src/bin/minedmap/main.rs @@ -1,3 +1,8 @@ +//! The minedmap generator renders map tile images from Minecraft save data + +#![warn(missing_docs)] +#![warn(clippy::missing_docs_in_private_items)] + mod common; mod metadata_writer; mod region_group; @@ -18,6 +23,7 @@ use region_processor::RegionProcessor; use tile_mipmapper::TileMipmapper; use tile_renderer::TileRenderer; +/// Command line arguments for minedmap #[derive(Debug, Parser)] pub struct Args { /// Number of parallel threads to use for processing @@ -32,6 +38,7 @@ pub struct Args { pub output_dir: PathBuf, } +/// Configures the Rayon thread pool for parallel processing fn setup_threads(num_threads: usize) -> Result<()> { rayon::ThreadPoolBuilder::new() .num_threads(num_threads) diff --git a/src/bin/minedmap/metadata_writer.rs b/src/bin/minedmap/metadata_writer.rs index 98b086a..2ba6038 100644 --- a/src/bin/minedmap/metadata_writer.rs +++ b/src/bin/minedmap/metadata_writer.rs @@ -1,3 +1,5 @@ +//! The [MetadataWriter] and related types + use anyhow::{Context, Result}; use serde::Serialize; @@ -6,43 +8,62 @@ use super::{ core::{self, io::fs, world::de}, }; +/// Minimum and maximum X and Z tile coordinates for a mipmap level #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] struct Bounds { + /// Minimum X coordinate min_x: i32, + /// Maximum X coordinate max_x: i32, + /// Minimum Z coordinate min_z: i32, + /// Maximum Z coordinate max_z: i32, } +/// Mipmap level information in viewer metadata file #[derive(Debug, Serialize)] struct Mipmap<'t> { + /// Minimum and maximum tile coordinates of the mipmap level bounds: Bounds, + /// Map of populated tiles for the mipmap level regions: &'t TileCoordMap, } +/// Initial spawn point for new players #[derive(Debug, Serialize)] struct Spawn { + /// Spawn X coordinate x: i32, + /// Spawn Z coordinate z: i32, } +/// Viewer metadata JSON data structure #[derive(Debug, Serialize)] struct Metadata<'t> { + /// Tile information for each mipmap level mipmaps: Vec>, + /// Initial spawn point for new players spawn: Spawn, } +/// The MetadataWriter is used to generate the viewer metadata file pub struct MetadataWriter<'a> { + /// Common MinedMap configuration from command line config: &'a Config, + /// Map of generated tiles for each mipmap level tiles: &'a [TileCoordMap], } impl<'a> MetadataWriter<'a> { + /// Creates a new MetadataWriter pub fn new(config: &'a Config, tiles: &'a [TileCoordMap]) -> Self { MetadataWriter { config, tiles } } + /// Helper to construct a [Mipmap] data structure from a [TileCoordMap] fn mipmap_entry(regions: &TileCoordMap) -> Mipmap { let mut min_x = i32::MAX; let mut max_x = i32::MIN; @@ -78,10 +99,12 @@ impl<'a> MetadataWriter<'a> { } } + /// Reads and deserializes the `level.dat` of the Minecraft save data fn read_level_dat(&self) -> Result { core::io::data::from_file(&self.config.level_dat_path).context("Failed to read level.dat") } + /// Generates [Spawn] data from a [de::LevelDat] fn spawn(level_dat: &de::LevelDat) -> Spawn { Spawn { x: level_dat.data.spawn_x, @@ -89,6 +112,7 @@ impl<'a> MetadataWriter<'a> { } } + /// Runs the viewer metadata file generation pub fn run(self) -> Result<()> { let level_dat = self.read_level_dat()?; diff --git a/src/bin/minedmap/region_group.rs b/src/bin/minedmap/region_group.rs index be46511..4357a8f 100644 --- a/src/bin/minedmap/region_group.rs +++ b/src/bin/minedmap/region_group.rs @@ -1,3 +1,5 @@ +//! The generic [RegionGroup] data structure + use std::{future::Future, iter}; use anyhow::Result; @@ -5,17 +7,27 @@ use futures_util::future::OptionFuture; /// A generic array of 3x3 elements /// -/// A RegionGroup is used to store information about a 3x3 neighbourhood of +/// A RegionGroup is used to store information about a 3x3 neighborhood of /// regions. /// /// The center element is always populated, while the 8 adjacent elements may be None. #[derive(Debug, Clone, Copy)] pub struct RegionGroup { + /// The element corresponding to the center of the 9x9 neighborhood center: T, + /// The remaining elements, stored in row-first order + /// + /// The center element is always None. neighs: [Option; 9], } impl RegionGroup { + /// Constructs a new RegionGroup from a closure called for each element + /// + /// The X and Z coordinates relative to the center (in the range -1..1) + /// are passed to the closure. + /// + /// Panics of the closure returns None for the center element. pub fn new(f: F) -> Self where F: Fn(i8, i8) -> Option, @@ -36,10 +48,14 @@ impl RegionGroup { } } + /// Returns a reference to the center element pub fn center(&self) -> &T { &self.center } + /// Returns a reference to an element of the RegionGroup, if populated + /// + /// Always returns None for X and Z coordinates outside of the -1..1 range. pub fn get(&self, x: i8, z: i8) -> Option<&T> { if (x, z) == (0, 0) { return Some(&self.center); @@ -50,6 +66,7 @@ impl RegionGroup { self.neighs.get((3 * x + z + 4) as usize)?.as_ref() } + /// Runs a closure on each element to construct a new RegionGroup pub fn map(self, mut f: F) -> RegionGroup where F: FnMut(T) -> U, @@ -60,6 +77,10 @@ impl RegionGroup { } } + /// Runs a fallible closure on each element to construct a new RegionGroup + /// + /// [Err] return values for the center element are passed up. Outer elements + /// become unpopulated when the closure fails. pub fn try_map(self, mut f: F) -> Result> where F: FnMut(T) -> Result, @@ -70,6 +91,7 @@ impl RegionGroup { Ok(RegionGroup { center, neighs }) } + /// Runs an asynchronous closure on each element to construct a new RegionGroup #[allow(dead_code)] pub async fn async_map(self, mut f: F) -> RegionGroup where @@ -88,6 +110,10 @@ impl RegionGroup { } } + /// Runs a fallible asynchronous closure on each element to construct a new RegionGroup + /// + /// [Err] return values for the center element are passed up. Outer elements + /// become unpopulated when the closure fails. pub async fn async_try_map(self, mut f: F) -> Result> where Fut: Future>, @@ -110,6 +136,7 @@ impl RegionGroup { Ok(RegionGroup { center, neighs }) } + /// Returns an [Iterator] over all populated elements pub fn iter(&self) -> impl Iterator { iter::once(&self.center).chain(self.neighs.iter().filter_map(Option::as_ref)) } diff --git a/src/bin/minedmap/region_processor.rs b/src/bin/minedmap/region_processor.rs index 983aa03..7168ebd 100644 --- a/src/bin/minedmap/region_processor.rs +++ b/src/bin/minedmap/region_processor.rs @@ -1,3 +1,5 @@ +//! The [RegionProcessor] and related functions + use std::{ffi::OsStr, path::Path, time::SystemTime}; use anyhow::{Context, Result}; @@ -32,13 +34,20 @@ fn parse_region_filename(file_name: &OsStr) -> Option { } /// Type with methods for processing the regions of a Minecraft save directory +/// +/// The RegionProcessor builds lightmap tiles as well as processed region data +/// consumed by subsequent generation steps. pub struct RegionProcessor<'a> { + /// Registry of known block types block_types: resource::BlockTypes, + /// Registry of known biome types biome_types: resource::BiomeTypes, + /// Common MinedMap configuration from command line config: &'a Config, } impl<'a> RegionProcessor<'a> { + /// Constructs a new RegionProcessor pub fn new(config: &'a Config) -> Self { RegionProcessor { block_types: resource::BlockTypes::default(), @@ -47,6 +56,7 @@ impl<'a> RegionProcessor<'a> { } } + /// Generates a list of all regions of the input Minecraft save data fn collect_regions(&self) -> Result> { Ok(self .config @@ -80,9 +90,11 @@ impl<'a> RegionProcessor<'a> { world::layer::top_layer(biome_list, &chunk) } + /// Renders a lightmap subtile from chunk block light data fn render_chunk_lightmap( block_light: Box, ) -> image::GrayAlphaImage { + /// Width/height of generated chunk lightmap const N: u32 = BLOCKS_PER_CHUNK as u32; image::GrayAlphaImage::from_fn(N, N, |x, z| { @@ -95,6 +107,9 @@ impl<'a> RegionProcessor<'a> { }) } + /// Saves processed region data + /// + /// The timestamp is the time of the last modification of the input region data. fn save_region( path: &Path, processed_region: &ProcessedRegion, @@ -103,6 +118,9 @@ impl<'a> RegionProcessor<'a> { storage::write(path, processed_region, FILE_META_VERSION, timestamp) } + /// Saves a lightmap tile + /// + /// The timestamp is the time of the last modification of the input region data. fn save_lightmap( path: &Path, lightmap: &image::GrayAlphaImage, @@ -117,6 +135,7 @@ impl<'a> RegionProcessor<'a> { /// Processes a single region file fn process_region(&self, coords: TileCoords) -> Result<()> { + /// Width/height of the region data const N: u32 = (BLOCKS_PER_CHUNK * CHUNKS_PER_REGION) as u32; let mut processed_region = ProcessedRegion::default(); diff --git a/src/bin/minedmap/tile_mipmapper.rs b/src/bin/minedmap/tile_mipmapper.rs index 14a2ec0..59c2880 100644 --- a/src/bin/minedmap/tile_mipmapper.rs +++ b/src/bin/minedmap/tile_mipmapper.rs @@ -1,3 +1,5 @@ +//! The [TileMipmapper] + use anyhow::{Context, Result}; use rayon::prelude::*; @@ -6,16 +8,24 @@ use super::{ core::{io::fs, types::*}, }; +/// Generates mipmap tiles from full-resolution tile images pub struct TileMipmapper<'a> { + /// Common MinedMap configuration from command line config: &'a Config, + /// List of populated tiles for base mipmap level (level 0) regions: &'a [TileCoords], } impl<'a> TileMipmapper<'a> { + /// Constructs a new TileMipmapper pub fn new(config: &'a Config, regions: &'a [TileCoords]) -> Self { TileMipmapper { config, regions } } + /// Helper to determine if no further mipmap levels are needed + /// + /// If all tile coordinates are -1 or 0, further mipmap levels will not + /// decrease the number of tiles and mipmap generated is considered finished. fn done(tiles: &TileCoordMap) -> bool { tiles .0 @@ -23,6 +33,7 @@ impl<'a> TileMipmapper<'a> { .all(|(z, xs)| (-1..=0).contains(z) && xs.iter().all(|x| (-1..=0).contains(x))) } + /// Derives the map of populated tile coordinates for the next mipmap level fn map_coords(tiles: &TileCoordMap) -> TileCoordMap { let mut ret = TileCoordMap::default(); @@ -38,6 +49,10 @@ impl<'a> TileMipmapper<'a> { ret } + /// Renders and saves a single mipmap tile image + /// + /// Each mipmap tile is rendered by taking 2x2 tiles from the + /// previous level and scaling them down by 50%. fn render_mipmap( &self, kind: TileKind, @@ -49,6 +64,7 @@ impl<'a> TileMipmapper<'a> { [P::Subpixel]: image::EncodableLayout, image::ImageBuffer>: Into, { + /// Tile width/height const N: u32 = (BLOCKS_PER_CHUNK * CHUNKS_PER_REGION) as u32; let output_path = self.config.tile_path(kind, level, coords); @@ -131,6 +147,7 @@ impl<'a> TileMipmapper<'a> { }) } + /// Runs the mipmap generation pub fn run(self) -> Result> { let mut tile_stack = { let mut tile_map = TileCoordMap::default(); diff --git a/src/bin/minedmap/tile_renderer.rs b/src/bin/minedmap/tile_renderer.rs index 409db6d..d5b7644 100644 --- a/src/bin/minedmap/tile_renderer.rs +++ b/src/bin/minedmap/tile_renderer.rs @@ -1,3 +1,5 @@ +//! The [TileRenderer] and related types and functions + use std::{ num::NonZeroUsize, path::PathBuf, @@ -22,8 +24,16 @@ use super::{ region_group::RegionGroup, }; +/// Type for referencing loaded [ProcessedRegion] data type RegionRef = Arc; +/// Returns the index of the biome at a block coordinate +/// +/// The passed chunk and block coordinates relative to the center of the +/// region group is offset by *dx* and *dz*. +/// +/// The returned tuple contains the relative region coordinates the offset coordinate +/// ends up in (in the range -1..1) and the index in that region's biome list. fn biome_at( region_group: &RegionGroup, chunk: ChunkCoords, @@ -49,15 +59,22 @@ fn biome_at( )) } +/// The TileRenderer generates map tiles from processed region data pub struct TileRenderer<'a> { + /// Common MinedMap configuration from command line config: &'a Config, + /// Runtime for asynchronous region loading rt: &'a tokio::runtime::Runtime, + /// List of populated regions to render tiles for regions: &'a [TileCoords], + /// Set of populated regions for fast existence checking region_set: rustc_hash::FxHashSet, + /// Cache of previously loaded regions region_cache: Mutex>>>, } impl<'a> TileRenderer<'a> { + /// Constructs a new TileRenderer pub fn new( config: &'a Config, rt: &'a tokio::runtime::Runtime, @@ -76,6 +93,7 @@ impl<'a> TileRenderer<'a> { } } + /// Loads [ProcessedRegion] for a region or returns previously loaded data from the region cache async fn load_region(&self, processed_path: PathBuf) -> Result { let region_loader = { let mut region_cache = self.region_cache.lock().unwrap(); @@ -96,6 +114,7 @@ impl<'a> TileRenderer<'a> { .cloned() } + /// Loads a 3x3 neighborhood of processed region data async fn load_region_group( &self, processed_paths: RegionGroup, @@ -105,18 +124,29 @@ impl<'a> TileRenderer<'a> { .await } + /// Computes the color of a tile pixel fn block_color_at( region_group: &RegionGroup, chunk: &ProcessedChunk, chunk_coords: ChunkCoords, block_coords: LayerBlockCoords, ) -> Option { + /// Helper for keys in the weight table + /// + /// Hashing the value as a single u32 is more efficient than hashing + /// the tuple elements separately. fn biome_key((dx, dz, index): (i8, i8, u16)) -> u32 { (dx as u8 as u32) | (dz as u8 as u32) << 8 | (index as u32) << 16 } + /// One quadrant of the kernel used to smooth biome edges + /// + /// The kernel is mirrored in X und Z direction to build the full 5x5 + /// smoothing kernel. const SMOOTH: [[f32; 3]; 3] = [[41.0, 26.0, 7.0], [26.0, 16.0, 4.0], [7.0, 4.0, 1.0]]; + /// Maximum X coordinate offset to take into account for biome smoothing const X: isize = SMOOTH[0].len() as isize - 1; + /// Maximum Z coordinate offset to take into account for biome smoothing const Z: isize = SMOOTH.len() as isize - 1; let block = chunk.blocks[block_coords]?; @@ -168,12 +198,14 @@ impl<'a> TileRenderer<'a> { Some(color / total) } + /// Renders a chunk subtile into a region tile image fn render_chunk( image: &mut image::RgbaImage, region_group: &RegionGroup, chunk: &ProcessedChunk, chunk_coords: ChunkCoords, ) { + /// Width/height of a chunk subtile const N: u32 = BLOCKS_PER_CHUNK as u32; let chunk_image = image::RgbaImage::from_fn(N, N, |x, z| { @@ -191,6 +223,7 @@ impl<'a> TileRenderer<'a> { overlay_chunk(image, &chunk_image, chunk_coords); } + /// Renders a region tile image fn render_region(image: &mut image::RgbaImage, region_group: &RegionGroup) { for (coords, chunk) in region_group.center().chunks.iter() { let Some(chunk) = chunk else { @@ -201,12 +234,15 @@ impl<'a> TileRenderer<'a> { } } + /// Returns the filename of the processed data for a region and the time of its last modification fn processed_source(&self, coords: TileCoords) -> Result<(PathBuf, SystemTime)> { let path = self.config.processed_path(coords); let timestamp = fs::modified_timestamp(&path)?; Ok((path, timestamp)) } + /// Returns the filenames of the processed data for a 3x3 neighborhood of a region + /// and the time of last modification for any of them fn processed_sources(&self, coords: TileCoords) -> Result<(RegionGroup, SystemTime)> { let sources = RegionGroup::new(|x, z| { Some(TileCoords { @@ -228,7 +264,9 @@ impl<'a> TileRenderer<'a> { Ok((paths, max_timestamp)) } + /// Renders and saves a region tile image fn render_tile(&self, coords: TileCoords) -> Result<()> { + /// Width/height of a tile image const N: u32 = (BLOCKS_PER_CHUNK * CHUNKS_PER_REGION) as u32; let (processed_paths, processed_timestamp) = self.processed_sources(coords)?; @@ -274,6 +312,7 @@ impl<'a> TileRenderer<'a> { ) } + /// Runs the tile generation pub fn run(self) -> Result<()> { fs::create_dir_all(&self.config.tile_dir(TileKind::Map, 0))?; diff --git a/src/bin/nbtdump.rs b/src/bin/nbtdump.rs index e68c376..da5e502 100644 --- a/src/bin/nbtdump.rs +++ b/src/bin/nbtdump.rs @@ -1,8 +1,14 @@ +//! Dumps data from a NBT data file in a human-readable format + +#![warn(missing_docs)] +#![warn(clippy::missing_docs_in_private_items)] + use std::path::PathBuf; use anyhow::Result; use clap::Parser; +/// Command line arguments for nbtdump #[derive(Debug, Parser)] struct Args { /// Filename to dump diff --git a/src/bin/regiondump.rs b/src/bin/regiondump.rs index 9dfac2f..21351f7 100644 --- a/src/bin/regiondump.rs +++ b/src/bin/regiondump.rs @@ -1,8 +1,14 @@ +//! Dumps data from a region data file in a human-readable format + +#![warn(missing_docs)] +#![warn(clippy::missing_docs_in_private_items)] + use std::path::PathBuf; use anyhow::Result; use clap::Parser; +/// Command line arguments for regiondump #[derive(Debug, Parser)] struct Args { /// Filename to dump diff --git a/src/io/data.rs b/src/io/data.rs index d475465..bb2ac3b 100644 --- a/src/io/data.rs +++ b/src/io/data.rs @@ -1,9 +1,12 @@ +//! Functions for reading and deserializing compressed NBT data + use std::{fs::File, io::prelude::*, path::Path}; use anyhow::{Context, Result}; use flate2::read::GzDecoder; use serde::de::DeserializeOwned; +/// Reads compressed NBT data from a reader and deserializes to a given data structure pub fn from_reader(reader: R) -> Result where R: Read, @@ -18,6 +21,7 @@ where fastnbt::from_bytes(&buf).context("Failed to decode NBT data") } +/// Reads compressed NBT data from a file and deserializes to a given data structure pub fn from_file(path: P) -> Result where P: AsRef, diff --git a/src/io/fs.rs b/src/io/fs.rs index 8f6a79a..7960b25 100644 --- a/src/io/fs.rs +++ b/src/io/fs.rs @@ -1,3 +1,5 @@ +//! Helpers and common functions for filesystem access + use std::{ fs::{self, File}, io::{BufReader, BufWriter, Read, Write}, @@ -8,15 +10,25 @@ use std::{ use anyhow::{Context, Ok, Result}; use serde::{Deserialize, Serialize}; +/// A file metadata version number +/// +/// Deserialized metadata with non-current version number are considered invalid #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] pub struct FileMetaVersion(pub u32); +/// Metadata stored with generated files to track required incremental updates #[derive(Debug, Serialize, Deserialize)] struct FileMeta { + /// Version of data described by the FileMeta version: FileMetaVersion, + /// Timestamp stored with generated data + /// + /// This timestamp is always the time of last modification of the inputs + /// that were used to generate the file described by the FileMeta. timestamp: SystemTime, } +/// Helper for creating suffixed file paths fn suffix_name(path: &Path, suffix: &str) -> PathBuf { let mut file_name = path.file_name().unwrap_or_default().to_os_string(); file_name.push(suffix); @@ -26,24 +38,35 @@ fn suffix_name(path: &Path, suffix: &str) -> PathBuf { ret } +/// Derives the filename for temporary storage of data during generation fn tmpfile_name(path: &Path) -> PathBuf { suffix_name(path, ".tmp") } +/// Derives the filename for associated metadata for generated files fn metafile_name(path: &Path) -> PathBuf { suffix_name(path, ".meta") } +/// Creates a directory including all its parents +/// +/// Wrapper around [fs::create_dir_all] that adds a more descriptive error message pub fn create_dir_all(path: &Path) -> Result<()> { fs::create_dir_all(path) .with_context(|| format!("Failed to create directory {}", path.display(),)) } +/// Renames a file or directory +/// +/// Wrapper around [fs::rename] that adds a more descriptive error message pub fn rename(from: &Path, to: &Path) -> Result<()> { fs::rename(from, to) .with_context(|| format!("Failed to rename {} to {}", from.display(), to.display())) } +/// Creates a new file +/// +/// The contents of the file are defined by the passed function. pub fn create(path: &Path, f: F) -> Result where F: FnOnce(&mut BufWriter) -> Result, @@ -60,6 +83,7 @@ where .with_context(|| format!("Failed to write file {}", path.display())) } +/// Checks whether the contents of two files are equal pub fn equal(path1: &Path, path2: &Path) -> Result { let mut file1 = BufReader::new( fs::File::open(path1) @@ -81,6 +105,12 @@ pub fn equal(path1: &Path, path2: &Path) -> Result { }) } +/// Creates a new file, temporarily storing its contents in a temporary file +/// +/// Storing the data in a temporary file prevents leaving half-written files +/// when the function is interrupted. In addition, the old and new contents of +/// the file are compared if a file with the same name already exists, and the +/// file timestamp is only updated if the contents have changed. pub fn create_with_tmpfile(path: &Path, f: F) -> Result where F: FnOnce(&mut BufWriter) -> Result, @@ -104,6 +134,7 @@ where ret } +/// Returns the time of last modification for a given file path pub fn modified_timestamp(path: &Path) -> Result { fs::metadata(path) .and_then(|meta| meta.modified()) @@ -115,6 +146,8 @@ pub fn modified_timestamp(path: &Path) -> Result { }) } +/// Reads the stored timestamp from file metadata for a file previously written +/// using [create_with_timestamp] pub fn read_timestamp(path: &Path, version: FileMetaVersion) -> Option { let meta_path = metafile_name(path); let mut file = BufReader::new(fs::File::open(meta_path).ok()?); @@ -127,6 +160,11 @@ pub fn read_timestamp(path: &Path, version: FileMetaVersion) -> Option( path: &Path, version: FileMetaVersion, diff --git a/src/io/mod.rs b/src/io/mod.rs index 681d75d..590ca2a 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -1,3 +1,5 @@ +//! Input/output functions + pub mod data; pub mod fs; pub mod region; diff --git a/src/io/region.rs b/src/io/region.rs index e5c2528..82f5604 100644 --- a/src/io/region.rs +++ b/src/io/region.rs @@ -1,3 +1,5 @@ +//! Functions for reading and deserializing region data + use std::{ fs::File, io::{prelude::*, SeekFrom}, @@ -10,15 +12,24 @@ use serde::de::DeserializeOwned; use crate::types::*; +/// Data block size of region data files +/// +/// After one header block, the region file consists of one or more consecutive blocks +/// of data for each populated chunk. const BLOCKSIZE: usize = 4096; +/// Chunk descriptor extracted from region file header #[derive(Debug)] struct ChunkDesc { + /// Offset of data block where the chunk starts offset: u32, + /// Number of data block used by the chunk len: u8, + /// Coodinates of chunk described by this descriptor coords: ChunkCoords, } +/// Parses the header of a region data file fn parse_header(header: &ChunkArray) -> Vec { let mut chunks: Vec<_> = header .iter() @@ -45,6 +56,7 @@ fn parse_header(header: &ChunkArray) -> Vec { chunks } +/// Decompresses chunk data and deserializes to a given data structure fn decode_chunk(buf: &[u8]) -> Result where T: DeserializeOwned, @@ -63,12 +75,18 @@ where fastnbt::from_bytes(&decode_buffer).context("Failed to decode NBT data") } +/// Wraps a reader used to read a region data file #[derive(Debug)] pub struct Region { + /// The wrapper reader reader: R, } impl Region { + /// Iterates over the chunks of the region data + /// + /// The order of iteration is based on the order the chunks appear in the + /// data file. pub fn foreach_chunk(self, mut f: F) -> Result<()> where R: Read + Seek, @@ -126,6 +144,7 @@ impl Region { } } +/// Creates a new [Region] from a reader pub fn from_reader(reader: R) -> Region where R: Read + Seek, @@ -133,6 +152,7 @@ where Region { reader } } +/// Creates a new [Region] for a file pub fn from_file

(path: P) -> Result> where P: AsRef, diff --git a/src/io/storage.rs b/src/io/storage.rs index 4ba6050..99a8d63 100644 --- a/src/io/storage.rs +++ b/src/io/storage.rs @@ -1,3 +1,7 @@ +//! Functions for serializing and deserializing MinedMap data structures efficiently +//! +//! Data is serialized using Bincode and compressed using zstd. + use std::{ fs::File, io::{Read, Write}, @@ -10,6 +14,9 @@ use serde::{de::DeserializeOwned, Serialize}; use super::fs; +/// Serializes data and stores it in a file +/// +/// A timestamp is stored in an assiciated metadata file. pub fn write( path: &Path, value: &T, @@ -29,6 +36,7 @@ pub fn write( }) } +/// Reads data from a file and deserializes it pub fn read(path: &Path) -> Result { (|| -> Result { let mut file = File::open(path)?; diff --git a/src/lib.rs b/src/lib.rs index f46a9d2..14d97f9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,8 @@ +//! Common library for MinedMap generator and dump utilities + +#![warn(missing_docs)] +#![warn(clippy::missing_docs_in_private_items)] + pub mod io; pub mod resource; pub mod types; diff --git a/src/resource/biomes.rs b/src/resource/biomes.rs index 0515e93..54859df 100644 --- a/src/resource/biomes.rs +++ b/src/resource/biomes.rs @@ -1,25 +1,51 @@ +//! Biome data structures + use serde::{Deserialize, Serialize}; use super::Color; +/// Grass color modifier used by a biome #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum BiomeGrassColorModifier { + /// Grass color modifier used by the dark forest biome DarkForest, + /// Grass color modifier used by swamp biomes Swamp, } +/// A biome specification +/// +/// A Biome contains all information about a biome necessary to compute a block +/// color given a block type and depth #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Biome { + /// Temperature value + /// + /// For more efficient storage, the temperature is stored as an integer + /// after mutiplying the raw value by 20 pub temp: i8, + /// Downfall value + /// + /// For more efficient storage, the downfall is stored as an integer + /// after mutiplying the raw value by 20 pub downfall: i8, + /// Water color override pub water_color: Option, + /// Foliage color override pub foliage_color: Option, + /// Grass color override pub grass_color: Option, + /// Grass color modifier pub grass_color_modifier: Option, } impl Biome { + /// Constructs a new Biome const fn new(temp: i16, downfall: i16) -> Biome { + /// Helper to encode temperature and downfall values + /// + /// Converts temperatue and downfall from the input format + /// (mutiplied by 100) to i8 range for more efficient storage. const fn encode(v: i16) -> i8 { (v / 5) as i8 } @@ -33,6 +59,7 @@ impl Biome { } } + /// Builder function to override the biome water color const fn water(self, water_color: [u8; 3]) -> Biome { Biome { water_color: Some(Color(water_color)), @@ -40,6 +67,7 @@ impl Biome { } } + /// Builder function to override the biome foliage color const fn foliage(self, foliage_color: [u8; 3]) -> Biome { Biome { foliage_color: Some(Color(foliage_color)), @@ -47,6 +75,7 @@ impl Biome { } } + /// Builder function to override the biome grass color const fn grass(self, grass_color: [u8; 3]) -> Biome { Biome { grass_color: Some(Color(grass_color)), @@ -54,6 +83,7 @@ impl Biome { } } + /// Builder function to set a grass color modifier const fn modify(self, grass_color_modifier: BiomeGrassColorModifier) -> Biome { Biome { grass_color_modifier: Some(grass_color_modifier), @@ -61,25 +91,34 @@ impl Biome { } } + /// Decodes a temperature or downfall value from the storage format to + /// f32 for further calculation fn decode(val: i8) -> f32 { f32::from(val) / 20.0 } + /// Returns the biome's temperature decoded to its original float value pub fn temp(&self) -> f32 { Self::decode(self.temp) } + /// Returns the biome's downfall decoded to its original float value pub fn downfall(&self) -> f32 { Self::decode(self.downfall) } } -// Data extracted from Minecraft code decompiled using https://github.com/Hexeption/MCP-Reborn - -#[allow(clippy::zero_prefixed_literal)] +/// Standard biome specifications pub const BIOMES: &[(&str, Biome)] = { use BiomeGrassColorModifier::*; + // Data extracted from Minecraft code decompiled using https://github.com/Hexeption/MCP-Reborn + + // We can't use floats in const functions, to temperature and downfall values + // are specified multipled by 100. The underscore is used in place of the decimal point + // of the original values. + + #[allow(clippy::zero_prefixed_literal)] &[ // Overworld ( @@ -189,6 +228,10 @@ pub const BIOMES: &[(&str, Biome)] = { ] }; +/// Biome ID aliases +/// +/// Some biomes have been renamed or merged in recent Minecraft versions. +/// Maintain a list of aliases to support chunks saved by older versions. pub const BIOME_ALIASES: &[(&str, &str)] = &[ // Biomes fix ("beaches", "beach"), @@ -292,6 +335,7 @@ pub const BIOME_ALIASES: &[(&str, &str)] = &[ ("deep_warm_ocean", "warm_ocean"), ]; +/// Maps old numeric biome IDs to new string IDs pub fn legacy_biome(index: u8) -> &'static str { match index { 0 => "ocean", diff --git a/src/resource/block_color.rs b/src/resource/block_color.rs index b98aec9..3df9a54 100644 --- a/src/resource/block_color.rs +++ b/src/resource/block_color.rs @@ -1,15 +1,23 @@ +//! Functions for computations of block colors + use super::{Biome, BlockType, Color}; use glam::Vec3; +/// Converts an u8 RGB color to a float vector fn color_vec_unscaled(color: Color) -> Vec3 { Vec3::from_array(color.0.map(f32::from)) } +/// Converts an u8 RGB color to a float vector, scaling the components to 0.0..1.0 fn color_vec(color: Color) -> Vec3 { color_vec_unscaled(color) / 255.0 } +/// Helper for grass and foliage colors +/// +/// Biome temperature and downfall are modified based on the depth value +/// before using them to compute the final color fn color_from_params(colors: &[Vec3; 3], biome: &Biome, depth: f32) -> Vec3 { let temp = (biome.temp() - f32::max((depth - 64.0) / 600.0, 0.0)).clamp(0.0, 1.0); let downfall = biome.downfall().clamp(0.0, 1.0) * temp; @@ -17,9 +25,13 @@ fn color_from_params(colors: &[Vec3; 3], biome: &Biome, depth: f32) -> Vec3 { colors[0] + temp * colors[1] + downfall * colors[2] } +/// Extension trait with helpers for computing biome-specific block colors trait BiomeExt { + /// Returns the grass color of the biome at a given depth fn grass_color(&self, depth: f32) -> Vec3; + /// Returns the foliage color of the biome at a given depth fn foliage_color(&self, depth: f32) -> Vec3; + /// Returns the water color of the biome fn water_color(&self) -> Vec3; } @@ -27,12 +39,15 @@ impl BiomeExt for Biome { fn grass_color(&self, depth: f32) -> Vec3 { use super::BiomeGrassColorModifier::*; + /// Color matrix extracted from grass color texture const GRASS_COLORS: [Vec3; 3] = [ Vec3::new(0.502, 0.706, 0.592), // lower right Vec3::new(0.247, 0.012, -0.259), // lower left - lower right Vec3::new(-0.471, 0.086, -0.133), // upper left - lower left ]; + /// Used for dark forst grass color modifier const DARK_FOREST_GRASS_COLOR: Vec3 = Vec3::new(0.157, 0.204, 0.039); // == color_vec(Color([40, 52, 10])) + /// Grass color in swamp biomes const SWAMP_GRASS_COLOR: Vec3 = Vec3::new(0.416, 0.439, 0.224); // == color_vec(Color([106, 112, 57])) let regular_color = || { @@ -49,6 +64,7 @@ impl BiomeExt for Biome { } fn foliage_color(&self, depth: f32) -> Vec3 { + /// Color matrix extracted from foliage color texture const FOLIAGE_COLORS: [Vec3; 3] = [ Vec3::new(0.376, 0.631, 0.482), // lower right Vec3::new(0.306, 0.012, -0.317), // lower left - lower right @@ -61,6 +77,9 @@ impl BiomeExt for Biome { } fn water_color(&self) -> Vec3 { + /// Default biome water color + /// + /// Used for biomes that don't explicitly set a water color const DEFAULT_WATER_COLOR: Vec3 = Vec3::new(0.247, 0.463, 0.894); // == color_vec(Color([63, 118, 228])) self.water_color @@ -69,15 +88,22 @@ impl BiomeExt for Biome { } } +/// Color multiplier for birch leaves const BIRCH_COLOR: Vec3 = Vec3::new(0.502, 0.655, 0.333); // == color_vec(Color([128, 167, 85])) +/// Color multiplier for spruce leaves const EVERGREEN_COLOR: Vec3 = Vec3::new(0.380, 0.600, 0.380); // == color_vec(Color([97, 153, 97])) +/// Determined if calling [block_color] for a given [BlockType] needs biome information pub fn needs_biome(block: BlockType) -> bool { use super::BlockFlag::*; block.is(Grass) || block.is(Foliage) || block.is(Water) } +/// Determined the block color to display for a given [BlockType] +/// +/// [needs_biome] must be used to determine whether passing a [Biome] is necessary. +/// Will panic if a [Biome] is necessary, but none is passed. pub fn block_color(block: BlockType, biome: Option<&Biome>, depth: f32) -> Vec3 { use super::BlockFlag::*; diff --git a/src/resource/legacy_block_types.rs b/src/resource/legacy_block_types.rs index c5d9462..c027ac3 100644 --- a/src/resource/legacy_block_types.rs +++ b/src/resource/legacy_block_types.rs @@ -1,12 +1,18 @@ +//! Mapping of old numeric block type and damage/subtype IDs to new string IDs + +/// Helper for block types that don't use the damage/subtype data const fn simple(id: &str) -> [&str; 16] { [ id, id, id, id, id, id, id, id, id, id, id, id, id, id, id, id, ] } +/// Default block type for unassigned numeric IDs const DEF: &str = "air"; +/// Default entry for block type numbers that are unassigned regardless of subtype const EMPTY: [&str; 16] = simple(DEF); +/// Mapping from each numeric block type and damage/subtype ID to new string ID pub const LEGACY_BLOCK_TYPES: [[&str; 16]; 256] = [ /* 0 */ simple("air"), diff --git a/src/resource/mod.rs b/src/resource/mod.rs index 501156a..459e783 100644 --- a/src/resource/mod.rs +++ b/src/resource/mod.rs @@ -1,43 +1,62 @@ +//! Data describing Minecraft biomes and block types + mod biomes; mod block_color; -mod block_types; mod legacy_block_types; +#[allow(clippy::missing_docs_in_private_items)] // Generated module +mod block_types; + use std::collections::HashMap; use enumflags2::{bitflags, BitFlags}; use serde::{Deserialize, Serialize}; +/// Flags describing special properties of [BlockType]s #[bitflags] #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum BlockFlag { + /// The block type is opaque Opaque, + /// The block type is colored using biome grass colors Grass, + /// The block type is colored using biome foliage colors Foliage, + /// The block type is birch foliage Birch, + /// The block type is spurce foliage Spruce, + /// The block type is colored using biome water colors Water, } +/// An RGB color #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Color(pub [u8; 3]); +/// A block type specification #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct BlockType { + /// Bit set of [BlockFlag]s describing special properties of the block type pub flags: BitFlags, + /// Base color of the block type pub color: Color, } impl BlockType { + /// Checks whether a block type has a given [BlockFlag] set pub fn is(&self, flag: BlockFlag) -> bool { self.flags.contains(flag) } } +/// Used to look up standard Minecraft block types #[derive(Debug)] pub struct BlockTypes { + /// Map of string IDs to block types block_type_map: HashMap, + /// Array used to look up old numeric block type and subtype values legacy_block_types: Box<[[BlockType; 16]; 256]>, } @@ -59,12 +78,14 @@ impl Default for BlockTypes { } impl BlockTypes { + /// Resolves a Minecraft 1.13+ string block type ID #[inline] pub fn get(&self, id: &str) -> Option { let suffix = id.strip_prefix("minecraft:")?; self.block_type_map.get(suffix).copied() } + /// Resolves a Minecraft pre-1.13 numeric block type ID #[inline] pub fn get_legacy(&self, id: u8, data: u8) -> Option { Some(self.legacy_block_types[id as usize][data as usize]) @@ -74,9 +95,12 @@ impl BlockTypes { pub use biomes::{Biome, BiomeGrassColorModifier}; pub use block_color::{block_color, needs_biome}; +/// Used to look up standard Minecraft biome types #[derive(Debug)] pub struct BiomeTypes { + /// Map of string IDs to biome types biome_map: HashMap, + /// Array used to look up old numeric biome IDs legacy_biomes: Box<[&'static Biome; 256]>, } @@ -112,12 +136,14 @@ impl Default for BiomeTypes { } impl BiomeTypes { + /// Resolves a Minecraft 1.18+ string biome type ID #[inline] pub fn get(&self, id: &str) -> Option<&Biome> { let suffix = id.strip_prefix("minecraft:")?; self.biome_map.get(suffix).copied() } + /// Resolves a Minecraft pre-1.18 numeric biome type ID #[inline] pub fn get_legacy(&self, id: u8) -> Option<&Biome> { Some(self.legacy_biomes[id as usize]) diff --git a/src/types.rs b/src/types.rs index a28583d..045750b 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,3 +1,5 @@ +//! Common types used by MinedMap + use std::{ fmt::Debug, iter::FusedIterator, @@ -7,14 +9,20 @@ use std::{ use itertools::iproduct; use serde::{Deserialize, Serialize}; +/// Const generic AXIS arguments for coordinate types pub mod axis { + /// The X axis pub const X: u8 = 0; + /// The Y axis (height) pub const Y: u8 = 1; + /// The Z axis pub const Z: u8 = 2; } +/// Generates a generic coordinate type with a given range macro_rules! coord_type { - ($t:ident, $max:expr) => { + ($t:ident, $max:expr, $doc:expr $(,)?) => { + #[doc = $doc] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct $t(pub u8); @@ -47,9 +55,15 @@ macro_rules! coord_type { }; } +/// Number of bits required to store a block coordinate pub const BLOCK_BITS: u8 = 4; +/// Number of blocks per chunk in each dimension pub const BLOCKS_PER_CHUNK: usize = 1 << BLOCK_BITS; -coord_type!(BlockCoord, BLOCKS_PER_CHUNK); +coord_type!( + BlockCoord, + BLOCKS_PER_CHUNK, + "A block coordinate relative to a chunk", +); /// A block X coordinate relative to a chunk pub type BlockX = BlockCoord<{ axis::X }>; @@ -63,7 +77,9 @@ pub type BlockZ = BlockCoord<{ axis::Z }>; /// X and Z coordinates of a block in a chunk #[derive(Clone, Copy, PartialEq, Eq)] pub struct LayerBlockCoords { + /// The X coordinate pub x: BlockX, + /// The Z coordinate pub z: BlockZ, } @@ -110,7 +126,9 @@ impl IndexMut for LayerBlockArray { /// X, Y and Z coordinates of a block in a chunk section #[derive(Clone, Copy, PartialEq, Eq)] pub struct SectionBlockCoords { + /// The X and Z coordinates pub xz: LayerBlockCoords, + /// The Y coordinate pub y: BlockY, } @@ -137,9 +155,15 @@ impl Debug for SectionBlockCoords { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct SectionY(pub i32); +/// Number of bits required to store a chunk coordinate pub const CHUNK_BITS: u8 = 5; +/// Number of chunks per region in each dimension pub const CHUNKS_PER_REGION: usize = 1 << CHUNK_BITS; -coord_type!(ChunkCoord, CHUNKS_PER_REGION); +coord_type!( + ChunkCoord, + CHUNKS_PER_REGION, + "A chunk coordinate relative to a region", +); /// A chunk X coordinate relative to a region pub type ChunkX = ChunkCoord<{ axis::X }>; @@ -150,7 +174,9 @@ pub type ChunkZ = ChunkCoord<{ axis::Z }>; /// A pair of chunk coordinates relative to a region #[derive(Clone, Copy, PartialEq, Eq)] pub struct ChunkCoords { + /// The X coordinate pub x: ChunkX, + /// The Z coordinate pub z: ChunkZ, } @@ -167,14 +193,17 @@ impl Debug for ChunkCoords { pub struct ChunkArray(pub [[T; CHUNKS_PER_REGION]; CHUNKS_PER_REGION]); impl ChunkArray { + /// Iterates over all possible chunk coordinate pairs used as [ChunkArray] keys pub fn keys() -> impl Iterator + Clone + Debug { iproduct!(ChunkZ::iter(), ChunkX::iter()).map(|(z, x)| ChunkCoords { x, z }) } + /// Iterates over all values stored in the [ChunkArray] pub fn values(&self) -> impl Iterator + Clone + Debug { Self::keys().map(|k| &self[k]) } + /// Iterates over pairs of chunk coordinate pairs and corresponding stored values pub fn iter(&self) -> impl Iterator + Clone + Debug { Self::keys().map(|k| (k, &self[k])) } diff --git a/src/util.rs b/src/util.rs index b4fdefe..a128ef9 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,6 +1,10 @@ +//! Utility functions and extension traits + use crate::types::*; +/// Extension trait for combined bit shift and mask pub trait ShiftMask: Sized { + /// Output type of shift operation type MaskedOutput; /// Apply a right shift to a value, and return both the result and the @@ -27,6 +31,8 @@ impl ShiftMask for i32 { } } +/// Combines a coordinate split into region, chunk and block number to +/// a single linear coordinate #[inline] pub fn to_flat_coord( region: i8, @@ -36,6 +42,7 @@ pub fn to_flat_coord( (region as i32) << (BLOCK_BITS + CHUNK_BITS) | ((chunk.0 as i32) << BLOCK_BITS | block.0 as i32) } +/// Splits a flat (linear) coordinate into region, chunk and block numbers #[inline] pub fn from_flat_coord(coord: i32) -> (i8, ChunkCoord, BlockCoord) { let (region_chunk, block) = coord.shift_mask(BLOCK_BITS); diff --git a/src/world/chunk.rs b/src/world/chunk.rs index 4f743fc..53ee165 100644 --- a/src/world/chunk.rs +++ b/src/world/chunk.rs @@ -1,3 +1,8 @@ +//! Higher-level interfaces to chunk data +//! +//! The data types in this module attempt to provide interfaces abstracting +//! over different data versions as much as possible. + use std::{ collections::{btree_map, BTreeMap}, iter::{self, FusedIterator}, @@ -17,6 +22,7 @@ use crate::{ pub enum Chunk<'a> { /// Minecraft v1.18+ chunk with biome data moved into sections V1_18 { + /// Section data section_map: BTreeMap, BiomesV1_18<'a>, BlockLight<'a>)>, }, /// Minecraft v1.13+ chunk @@ -26,14 +32,18 @@ pub enum Chunk<'a> { /// section), and a palette mapping these indices to namespaced /// block IDs V1_13 { + /// Section data section_map: BTreeMap, BlockLight<'a>)>, + /// Biome data biomes: BiomesV0<'a>, }, /// Original pre-1.13 chunk /// /// The original chunk format with fixed 8-bit numeric block IDs V0 { + /// Section data section_map: BTreeMap, BlockLight<'a>)>, + /// Biome data biomes: BiomesV0<'a>, }, /// Unpopulated chunk without any block data @@ -45,16 +55,21 @@ pub enum Chunk<'a> { enum SectionIterInner<'a> { /// Iterator over sections of [Chunk::V1_18] V1_18 { + /// Inner iterator into section map iter: btree_map::Iter<'a, SectionY, (SectionV1_13<'a>, BiomesV1_18<'a>, BlockLight<'a>)>, }, /// Iterator over sections of [Chunk::V1_13] V1_13 { + /// Inner iterator into section map iter: btree_map::Iter<'a, SectionY, (SectionV1_13<'a>, BlockLight<'a>)>, + /// Chunk biome data biomes: &'a BiomesV0<'a>, }, /// Iterator over sections of [Chunk::V0] V0 { + /// Inner iterator into section map iter: btree_map::Iter<'a, SectionY, (SectionV0<'a>, BlockLight<'a>)>, + /// Chunk biome data biomes: &'a BiomesV0<'a>, }, /// Empty iterator over an unpopulated chunk ([Chunk::Empty]) @@ -64,6 +79,7 @@ enum SectionIterInner<'a> { /// Iterator over the sections of a [Chunk] #[derive(Debug, Clone)] pub struct SectionIter<'a> { + /// Inner iterator enum inner: SectionIterInner<'a>, } @@ -193,6 +209,7 @@ impl<'a> Chunk<'a> { ) } + /// Returns true if the chunk does not contain any sections pub fn is_empty(&self) -> bool { match self { Chunk::V1_18 { section_map } => section_map.is_empty(), @@ -230,14 +247,20 @@ impl<'a> Chunk<'a> { } } +/// Reference to block, biome and block light data of a section #[derive(Debug, Clone, Copy)] pub struct SectionIterItem<'a> { + /// The Y coordinate of the section pub y: SectionY, + /// Section block data pub section: &'a dyn Section, + /// Section biome data pub biomes: &'a dyn Biomes, + /// Section block light data pub block_light: BlockLight<'a>, } +/// Helper trait to specify section iterator trait bounds trait SectionIterTrait<'a>: Iterator> + DoubleEndedIterator + ExactSizeIterator + FusedIterator { @@ -252,6 +275,7 @@ impl<'a, T> SectionIterTrait<'a> for T where } impl<'a> SectionIter<'a> { + /// Helper to run a closure on the inner section iterator fn with_iter(&mut self, f: F) -> T where F: FnOnce(&mut dyn SectionIterTrait<'a>) -> T, diff --git a/src/world/de.rs b/src/world/de.rs index 82aa11c..cbedf31 100644 --- a/src/world/de.rs +++ b/src/world/de.rs @@ -6,30 +6,39 @@ use serde::Deserialize; #[derive(Debug, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct BlockStatePaletteEntry { + /// Block type ID pub name: String, } /// 1.18+ `block_states` element found in a [section](SectionV1_18) #[derive(Debug, Deserialize)] pub struct BlockStatesV1_18 { + /// Palette of block types, indexed by block data pub palette: Vec, + /// Block data pub data: Option, } /// 1.18+ `biomes` element found in a [section](SectionV1_18) #[derive(Debug, Deserialize)] pub struct BiomesV1_18 { + /// Palette of biome types, indexed by biome data pub palette: Vec, + /// Biome data pub data: Option, } /// Element of the 1.18+ `sections` list found in a [Chunk] #[derive(Debug, Deserialize)] pub struct SectionV1_18 { + /// Y coordinate #[serde(rename = "Y")] pub y: i32, + /// Block type data pub block_states: BlockStatesV1_18, + /// Biome data pub biomes: BiomesV1_18, + /// Block light data #[serde(rename = "BlockLight")] pub block_light: Option, } @@ -38,16 +47,23 @@ pub struct SectionV1_18 { #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum SectionV0Variants { + /// v1.13+ data #[serde(rename_all = "PascalCase")] V1_13 { + /// Block data block_states: fastnbt::LongArray, + /// Block type palette, indexed by block data palette: Vec, }, + /// Pre-1.13 data #[serde(rename_all = "PascalCase")] V0 { + /// Block type data blocks: fastnbt::ByteArray, + /// Block damage / subtype data data: fastnbt::ByteArray, }, + /// Empty section Empty {}, } @@ -55,8 +71,11 @@ pub enum SectionV0Variants { #[derive(Debug, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct SectionV0 { + /// Y coordinate pub y: i8, + /// Block light data pub block_light: Option, + /// Version-specific data #[serde(flatten)] pub section: SectionV0Variants, } @@ -65,7 +84,9 @@ pub struct SectionV0 { #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum BiomesV0 { + /// Data for Minecraft versions storing biome data as an IntArray IntArray(fastnbt::IntArray), + /// Data for Minecraft versions storing biome data as an ByteArray ByteArray(fastnbt::ByteArray), } @@ -73,8 +94,10 @@ pub enum BiomesV0 { #[derive(Debug, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct LevelV0 { + /// Section data #[serde(default)] pub sections: Vec, + /// Biome data pub biomes: Option, } @@ -82,11 +105,15 @@ pub struct LevelV0 { #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum ChunkVariants { + /// 1.18+ chunk data V1_18 { + /// List of chunk sections sections: Vec, }, + /// Pre-1.18 chunk data #[serde(rename_all = "PascalCase")] V0 { + /// `Level` field of the chunk level: LevelV0, }, } @@ -95,16 +122,20 @@ pub enum ChunkVariants { #[derive(Debug, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct Chunk { + /// The data version of the chunk pub data_version: Option, + /// Version-specific chunk data #[serde(flatten)] pub chunk: ChunkVariants, } -/// "Data" compound element of level.dat +/// `Data` compound element of level.dat #[derive(Debug, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct LevelDatData { + /// X coordinate of spawn point for new players pub spawn_x: i32, + /// Z coordinate of spawn point for new players pub spawn_z: i32, } @@ -112,5 +143,6 @@ pub struct LevelDatData { #[derive(Debug, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct LevelDat { + /// The `Data` field pub data: LevelDatData, } diff --git a/src/world/layer.rs b/src/world/layer.rs index 3abc30b..ce88107 100644 --- a/src/world/layer.rs +++ b/src/world/layer.rs @@ -1,3 +1,5 @@ +//! Functions to search the "top" layer of a chunk + use std::num::NonZeroU16; use anyhow::{Context, Result}; @@ -10,6 +12,7 @@ use crate::{ types::*, }; +/// Height (Y coordinate) of a block #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct BlockHeight(pub i32); @@ -28,27 +31,52 @@ impl BlockHeight { } } +/// Array optionally storing a [BlockType] for each coordinate of a chunk pub type BlockArray = LayerBlockArray>; + +/// Array optionally storing a biome index for each coordinate of a chunk +/// +/// The entries refer to a biome list generated with the top layer data. +/// Indices are stored incremented by 1 to allow using a [NonZeroU16]. pub type BiomeArray = LayerBlockArray>; + +/// Array storing a block light value for each coordinate for a chunk pub type BlockLightArray = LayerBlockArray; + +/// Array optionally storing a depth value for each coordinate for a chunk pub type DepthArray = LayerBlockArray>; +/// References to LayerData entries for a single coordinate pair struct LayerEntry<'a> { + /// The block type of the referenced entry block: &'a mut Option, + /// The biome type of the referenced entry biome: &'a mut Option, + /// The block light of the referenced entry block_light: &'a mut u8, + /// The depth value of the referenced entry depth: &'a mut Option, } impl<'a> LayerEntry<'a> { + /// Returns true if the entry has not been filled yet (no opaque block has been encountered) + /// + /// The depth value is filled separately when a non-water block is encountered after the block type + /// has already been filled. fn is_empty(&self) -> bool { self.block.is_none() } + /// Returns true if the entry has been filled including its depth (an opaque non-water block has been + /// encountered) fn done(&self) -> bool { self.depth.is_some() } + /// Fills in the LayerEntry + /// + /// Checks whether the passed coordinates point at an opaque or non-water block and + /// fills in the entry accordingly. Returns true when the block has been filled including its depth. fn fill( &mut self, biome_list: &mut IndexSet, @@ -90,15 +118,24 @@ impl<'a> LayerEntry<'a> { } } +/// Top layer data +/// +/// A LayerData stores block type, biome, block light and depth data for +/// each coordinate of a chunk. #[derive(Debug, Default)] pub struct LayerData { + /// Block type data pub blocks: Box, + /// Biome data pub biomes: Box, + /// Block light data pub block_light: Box, + /// Depth data pub depths: Box, } impl LayerData { + /// Builds a [LayerEntry] referencing the LayerData at a given coordinate pair fn entry(&mut self, coords: LayerBlockCoords) -> LayerEntry { LayerEntry { block: &mut self.blocks[coords], @@ -109,13 +146,14 @@ impl LayerData { } } -/// Fills in a [BlockInfoArray] with the information of the chunk's top +/// Fills in a [LayerData] with the information of the chunk's top /// block layer /// /// For each (X, Z) coordinate pair, the topmost opaque block is /// determined as the block that should be visible on the rendered /// map. For water blocks, the height of the first non-water block -/// is additionally filled in as the water depth. +/// is additionally filled in as the water depth (the block height is +/// used as depth otherwise). pub fn top_layer(biome_list: &mut IndexSet, chunk: &Chunk) -> Result> { use BLOCKS_PER_CHUNK as N; diff --git a/src/world/mod.rs b/src/world/mod.rs index 9879066..57aa7ed 100644 --- a/src/world/mod.rs +++ b/src/world/mod.rs @@ -1,3 +1,5 @@ +//! Data structures describing Minecraft save data + pub mod chunk; pub mod de; pub mod layer; diff --git a/src/world/section.rs b/src/world/section.rs index 5f6553d..8f47216 100644 --- a/src/world/section.rs +++ b/src/world/section.rs @@ -1,3 +1,8 @@ +//! Higher-level interfaces to section data +//! +//! The data types in this module attempt to provide interfaces abstracting +//! over different data versions as much as possible. + use std::fmt::Debug; use anyhow::{bail, Context, Result}; @@ -9,6 +14,14 @@ use crate::{ types::*, }; +use BLOCKS_PER_CHUNK as N; +/// Maximum height of pre-1.18 levels +const HEIGHT: usize = 256; +/// Number of biome entries per chunk in each direction +const BN: usize = N >> 2; +/// Pre-1.18 height of level measured in 4-block spans (resolution of 1.15+ biome data) +const BHEIGHT: usize = HEIGHT >> 2; + /// Determine the number of bits required for indexing into a palette of a given length /// /// This is basically a base-2 logarithm, with clamping to a minimum value and @@ -29,20 +42,31 @@ fn palette_bits(len: usize, min: u8, max: u8) -> Option { /// Trait for common functions of [SectionV1_13] and [SectionV0] pub trait Section: Debug { + /// Returns the [BlockType] at a coordinate tuple inside the section fn block_at(&self, coords: SectionBlockCoords) -> Result>; } /// Minecraft v1.13+ section block data #[derive(Debug)] pub struct SectionV1_13<'a> { + /// Packed block type data block_states: Option<&'a [i64]>, + /// List of block types indexed by entries encoded in *block_states* palette: Vec>, + /// Number of bits per block in *block_states* bits: u8, + /// Set to true if packed block entries in *block_states* are aligned to i64 + /// + /// In older data formats, entries are unaligned and a single block can span + /// two i64 entries. aligned_blocks: bool, } impl<'a> SectionV1_13<'a> { /// Constructs a new [SectionV1_13] from deserialized data structures + /// + /// The block IDs in the section's palette are resolved to their [BlockType]s + /// to allow for faster lookup later. pub fn new( data_version: u32, block_states: Option<&'a [i64]>, @@ -127,16 +151,21 @@ impl<'a> Section for SectionV1_13<'a> { /// Pre-1.13 section block data #[derive(Debug)] pub struct SectionV0<'a> { + /// Block type data + /// + /// Each i8 entry corresponds to a block in the 16x16x16 section blocks: &'a [i8], + /// Block damage/subtype data + /// + /// Uses 4 bits for each block in the 16x16x16 section data: &'a [i8], + /// Used to look up block type IDs block_types: &'a BlockTypes, } impl<'a> SectionV0<'a> { /// Constructs a new [SectionV0] from deserialized data structures pub fn new(blocks: &'a [i8], data: &'a [i8], block_types: &'a BlockTypes) -> Result { - use BLOCKS_PER_CHUNK as N; - if blocks.len() != N * N * N { bail!("Invalid section block data"); } @@ -171,6 +200,7 @@ impl<'a> Section for SectionV0<'a> { /// Trait for common functions of [BiomesV1_18] and [BiomesV0] pub trait Biomes: Debug { + /// Returns the [Biome] at a coordinate tuple inside the chunk fn biome_at(&self, section: SectionY, coords: SectionBlockCoords) -> Result>; } @@ -181,13 +211,21 @@ pub trait Biomes: Debug { /// v1.13+ block data. #[derive(Debug)] pub struct BiomesV1_18<'a> { + /// Packed biome data + /// + /// Each entry specifies the biome of a 4x4x4 block area. + /// + /// Unlike block type data in [SectionV1_13], biome data is always aligned + /// to whole i64 values. biomes: Option<&'a [i64]>, + /// Biome palette indexed by entries encoded in *biomes* palette: Vec>, + /// Number of bits used for each entry in *biomes* bits: u8, } impl<'a> BiomesV1_18<'a> { - /// Constructs a new [BiomesV18] from deserialized data structures + /// Constructs a new [BiomesV1_18] from deserialized data structures pub fn new( biomes: Option<&'a [i64]>, palette: &'a [String], @@ -223,9 +261,6 @@ impl<'a> BiomesV1_18<'a> { /// Looks up the block type palette index at the given coordinates fn palette_index_at(&self, coords: SectionBlockCoords) -> usize { - const N: usize = BLOCKS_PER_CHUNK; - const BN: usize = N >> 2; - let Some(biomes) = self.biomes else { return 0; }; @@ -262,28 +297,31 @@ impl<'a> Biomes for BiomesV1_18<'a> { /// different pre-v1.18 Minecraft versions #[derive(Debug)] enum BiomesV0Data<'a> { + /// Biome data stored as IntArray in 1.15+ format + /// + /// Minecraft 1.15 switched to 3-dimensional biome information, but reduced + /// the resolution to only use one entry for every 4x4x4 block area. IntArrayV15(&'a fastnbt::IntArray), + /// Biome data stored as IntArray in some pre-1.15 versions IntArrayV0(&'a fastnbt::IntArray), + /// Biome data stored as ByteArray in some pre-1.15 versions ByteArray(&'a fastnbt::ByteArray), } /// Pre-v1.18 section biome data #[derive(Debug)] pub struct BiomesV0<'a> { + /// Biome data from save data data: BiomesV0Data<'a>, + /// Used to look up biome IDs biome_types: &'a BiomeTypes, } impl<'a> BiomesV0<'a> { /// Constructs a new [BiomesV0] from deserialized data structures pub fn new(biomes: Option<&'a de::BiomesV0>, biome_types: &'a BiomeTypes) -> Result { - const N: usize = BLOCKS_PER_CHUNK; - const MAXY: usize = 256; - const BN: usize = N >> 2; - const BMAXY: usize = MAXY >> 2; - let data = match biomes { - Some(de::BiomesV0::IntArray(data)) if data.len() == BN * BN * BMAXY => { + Some(de::BiomesV0::IntArray(data)) if data.len() == BN * BN * BHEIGHT => { BiomesV0Data::IntArrayV15(data) } Some(de::BiomesV0::IntArray(data)) if data.len() == N * N => { @@ -302,16 +340,12 @@ impl<'a> Biomes for BiomesV0<'a> { fn biome_at(&self, section: SectionY, coords: SectionBlockCoords) -> Result> { let id = match self.data { BiomesV0Data::IntArrayV15(data) => { - const N: usize = BLOCKS_PER_CHUNK; - const MAXY: usize = 256; - const BN: usize = N >> 2; - let LayerBlockCoords { x, z } = coords.xz; let y = section .0 .checked_mul(BLOCKS_PER_CHUNK as i32) .and_then(|y| y.checked_add_unsigned(coords.y.0.into())) - .filter(|&height| height >= 0 && (height as usize) < MAXY) + .filter(|&height| height >= 0 && (height as usize) < HEIGHT) .context("Y coordinate out of range")? as usize; let offset = (y >> 2) * BN * BN + (z.0 >> 2) as usize * BN + (x.0 >> 2) as usize; let id = data[offset] as u32; @@ -327,12 +361,13 @@ impl<'a> Biomes for BiomesV0<'a> { } } +/// Wrapper around chunk block light data array #[derive(Debug, Clone, Copy)] pub struct BlockLight<'a>(Option<&'a [i8]>); impl<'a> BlockLight<'a> { + /// Creates a new [BlockLight], checking validity pub fn new(block_light: Option<&'a [i8]>) -> Result { - use BLOCKS_PER_CHUNK as N; if let Some(block_light) = block_light { if block_light.len() != N * N * N / 2 { bail!("Invalid section block light data"); @@ -341,6 +376,7 @@ impl<'a> BlockLight<'a> { Ok(BlockLight(block_light)) } + /// Returns the block light value at the given coordinates pub fn block_light_at(&self, coords: SectionBlockCoords) -> u8 { let Some(block_light) = self.0 else { return 0;