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

View file

@ -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<R, T>(reader: R) -> Result<T>
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<P, T>(path: P) -> Result<T>
where
P: AsRef<Path>,

View file

@ -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<T, F>(path: &Path, f: F) -> Result<T>
where
F: FnOnce(&mut BufWriter<File>) -> Result<T>,
@ -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<bool> {
let mut file1 = BufReader::new(
fs::File::open(path1)
@ -81,6 +105,12 @@ pub fn equal(path1: &Path, path2: &Path) -> Result<bool> {
})
}
/// 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<T, F>(path: &Path, f: F) -> Result<T>
where
F: FnOnce(&mut BufWriter<File>) -> Result<T>,
@ -104,6 +134,7 @@ where
ret
}
/// Returns the time of last modification for a given file path
pub fn modified_timestamp(path: &Path) -> Result<SystemTime> {
fs::metadata(path)
.and_then(|meta| meta.modified())
@ -115,6 +146,8 @@ pub fn modified_timestamp(path: &Path) -> Result<SystemTime> {
})
}
/// 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<SystemTime> {
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<SystemTim
Some(meta.timestamp)
}
/// Creates a new file, temporarily storing its contents in a temporary file
/// like [create_with_tmpfile], and storing a timestamp in a metadata file
/// if successful
///
/// The timestamp can be retrieved later using [read_timestamp].
pub fn create_with_timestamp<T, F>(
path: &Path,
version: FileMetaVersion,

View file

@ -1,3 +1,5 @@
//! Input/output functions
pub mod data;
pub mod fs;
pub mod region;

View file

@ -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<u32>) -> Vec<ChunkDesc> {
let mut chunks: Vec<_> = header
.iter()
@ -45,6 +56,7 @@ fn parse_header(header: &ChunkArray<u32>) -> Vec<ChunkDesc> {
chunks
}
/// Decompresses chunk data and deserializes to a given data structure
fn decode_chunk<T>(buf: &[u8]) -> Result<T>
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<R: Read + Seek> {
/// The wrapper reader
reader: R,
}
impl<R: Read + Seek> Region<R> {
/// 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<T, F>(self, mut f: F) -> Result<()>
where
R: Read + Seek,
@ -126,6 +144,7 @@ impl<R: Read + Seek> Region<R> {
}
}
/// Creates a new [Region] from a reader
pub fn from_reader<R>(reader: R) -> Region<R>
where
R: Read + Seek,
@ -133,6 +152,7 @@ where
Region { reader }
}
/// Creates a new [Region] for a file
pub fn from_file<P>(path: P) -> Result<Region<File>>
where
P: AsRef<Path>,

View file

@ -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<T: Serialize>(
path: &Path,
value: &T,
@ -29,6 +36,7 @@ pub fn write<T: Serialize>(
})
}
/// Reads data from a file and deserializes it
pub fn read<T: DeserializeOwned>(path: &Path) -> Result<T> {
(|| -> Result<T> {
let mut file = File::open(path)?;

View file

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

View file

@ -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<Color>,
/// Foliage color override
pub foliage_color: Option<Color>,
/// Grass color override
pub grass_color: Option<Color>,
/// Grass color modifier
pub grass_color_modifier: Option<BiomeGrassColorModifier>,
}
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",

View file

@ -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::*;

View file

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

View file

@ -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<BlockFlag>,
/// 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<String, BlockType>,
/// 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<BlockType> {
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<BlockType> {
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<String, &'static Biome>,
/// 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])

View file

@ -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<const AXIS: u8>(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<T> IndexMut<LayerBlockCoords> for LayerBlockArray<T> {
/// 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<T>(pub [[T; CHUNKS_PER_REGION]; CHUNKS_PER_REGION]);
impl<T> ChunkArray<T> {
/// Iterates over all possible chunk coordinate pairs used as [ChunkArray] keys
pub fn keys() -> impl Iterator<Item = ChunkCoords> + 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<Item = &T> + 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<Item = (ChunkCoords, &T)> + Clone + Debug {
Self::keys().map(|k| (k, &self[k]))
}

View file

@ -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<const AXIS: u8>(
region: i8,
@ -36,6 +42,7 @@ pub fn to_flat_coord<const AXIS: u8>(
(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<const AXIS: u8>(coord: i32) -> (i8, ChunkCoord<AXIS>, BlockCoord<AXIS>) {
let (region_chunk, block) = coord.shift_mask(BLOCK_BITS);

View file

@ -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<SectionY, (SectionV1_13<'a>, 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<SectionY, (SectionV1_13<'a>, 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<SectionY, (SectionV0<'a>, 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<Item = SectionIterItem<'a>> + 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<F, T>(&mut self, f: F) -> T
where
F: FnOnce(&mut dyn SectionIterTrait<'a>) -> T,

View file

@ -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<BlockStatePaletteEntry>,
/// Block data
pub data: Option<fastnbt::LongArray>,
}
/// 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<String>,
/// Biome data
pub data: Option<fastnbt::LongArray>,
}
/// 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<fastnbt::ByteArray>,
}
@ -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<BlockStatePaletteEntry>,
},
/// 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<fastnbt::ByteArray>,
/// 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<SectionV0>,
/// Biome data
pub biomes: Option<BiomesV0>,
}
@ -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<SectionV1_18>,
},
/// 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<u32>,
/// 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,
}

View file

@ -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<Option<BlockType>>;
/// 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<Option<NonZeroU16>>;
/// Array storing a block light value for each coordinate for a chunk
pub type BlockLightArray = LayerBlockArray<u8>;
/// Array optionally storing a depth value for each coordinate for a chunk
pub type DepthArray = LayerBlockArray<Option<BlockHeight>>;
/// References to LayerData entries for a single coordinate pair
struct LayerEntry<'a> {
/// The block type of the referenced entry
block: &'a mut Option<BlockType>,
/// The biome type of the referenced entry
biome: &'a mut Option<NonZeroU16>,
/// The block light of the referenced entry
block_light: &'a mut u8,
/// The depth value of the referenced entry
depth: &'a mut Option<BlockHeight>,
}
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<Biome>,
@ -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<BlockArray>,
/// Biome data
pub biomes: Box<BiomeArray>,
/// Block light data
pub block_light: Box<BlockLightArray>,
/// Depth data
pub depths: Box<DepthArray>,
}
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<Biome>, chunk: &Chunk) -> Result<Option<LayerData>> {
use BLOCKS_PER_CHUNK as N;

View file

@ -1,3 +1,5 @@
//! Data structures describing Minecraft save data
pub mod chunk;
pub mod de;
pub mod layer;

View file

@ -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<u8> {
/// 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<Option<BlockType>>;
}
/// 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<Option<BlockType>>,
/// 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<Self> {
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<Option<&Biome>>;
}
@ -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<Option<&'a Biome>>,
/// 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<Self> {
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<Option<&Biome>> {
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<Self> {
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;