Add documentation comments for all items

This commit is contained in:
Matthias Schiffer 2023-08-18 19:13:43 +02:00
parent ba86dc8c06
commit 05a8056cbf
Signed by: neocturne
GPG key ID: 16EF3F64CB201D9C
26 changed files with 576 additions and 42 deletions

View file

@ -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<i32, BTreeSet<i32>>);
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<layer::BlockArray>,
/// Biome data
pub biomes: Box<layer::BiomeArray>,
/// Block height/depth data
pub depths: Box<layer::DepthArray>,
}
/// 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<Biome>,
/// Processed chunk data
pub chunks: ChunkArray<Option<Box<ProcessedChunk>>>,
}
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<I, J>(image: &mut I, chunk: &J, coords: ChunkCoords)
where
I: image::GenericImage,

View file

@ -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)

View file

@ -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<Mipmap<'t>>,
/// 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<de::LevelDat> {
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()?;

View file

@ -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<T> {
/// 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<T>; 9],
}
impl<T> RegionGroup<T> {
/// 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: F) -> Self
where
F: Fn(i8, i8) -> Option<T>,
@ -36,10 +48,14 @@ impl<T> RegionGroup<T> {
}
}
/// 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<T> RegionGroup<T> {
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<U, F>(self, mut f: F) -> RegionGroup<U>
where
F: FnMut(T) -> U,
@ -60,6 +77,10 @@ impl<T> RegionGroup<T> {
}
}
/// 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<U, F>(self, mut f: F) -> Result<RegionGroup<U>>
where
F: FnMut(T) -> Result<U>,
@ -70,6 +91,7 @@ impl<T> RegionGroup<T> {
Ok(RegionGroup { center, neighs })
}
/// Runs an asynchronous closure on each element to construct a new RegionGroup
#[allow(dead_code)]
pub async fn async_map<U, F, Fut>(self, mut f: F) -> RegionGroup<U>
where
@ -88,6 +110,10 @@ impl<T> RegionGroup<T> {
}
}
/// 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<U, F, Fut>(self, mut f: F) -> Result<RegionGroup<U>>
where
Fut: Future<Output = Result<U>>,
@ -110,6 +136,7 @@ impl<T> RegionGroup<T> {
Ok(RegionGroup { center, neighs })
}
/// Returns an [Iterator] over all populated elements
pub fn iter(&self) -> impl Iterator<Item = &T> {
iter::once(&self.center).chain(self.neighs.iter().filter_map(Option::as_ref))
}

View file

@ -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<TileCoords> {
}
/// 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<Vec<TileCoords>> {
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<world::layer::BlockLightArray>,
) -> 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();

View file

@ -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<P: image::PixelWithColorType>(
&self,
kind: TileKind,
@ -49,6 +64,7 @@ impl<'a> TileMipmapper<'a> {
[P::Subpixel]: image::EncodableLayout,
image::ImageBuffer<P, Vec<P::Subpixel>>: Into<image::DynamicImage>,
{
/// 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<Vec<TileCoordMap>> {
let mut tile_stack = {
let mut tile_map = TileCoordMap::default();

View file

@ -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<ProcessedRegion>;
/// 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<RegionRef>,
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<TileCoords>,
/// Cache of previously loaded regions
region_cache: Mutex<LruCache<PathBuf, Arc<OnceCell<RegionRef>>>>,
}
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<RegionRef> {
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<PathBuf>,
@ -105,18 +124,29 @@ impl<'a> TileRenderer<'a> {
.await
}
/// Computes the color of a tile pixel
fn block_color_at(
region_group: &RegionGroup<RegionRef>,
chunk: &ProcessedChunk,
chunk_coords: ChunkCoords,
block_coords: LayerBlockCoords,
) -> Option<Vec3> {
/// 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<RegionRef>,
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<RegionRef>) {
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<PathBuf>, 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))?;

View file

@ -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

View file

@ -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