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::{ use std::{
collections::{BTreeMap, BTreeSet}, collections::{BTreeMap, BTreeSet},
fmt::Debug, fmt::Debug,
@ -9,12 +11,19 @@ use serde::{Deserialize, Serialize};
use super::core::{io::fs::FileMetaVersion, resource::Biome, types::*, world::layer}; 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); 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)] #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TileCoords { pub struct TileCoords {
/// The X coordinate
pub x: i32, pub x: i32,
/// The Z coordinate
pub z: i32, 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)] #[derive(Debug, Clone, Default, Serialize)]
#[serde(transparent)] #[serde(transparent)]
pub struct TileCoordMap(pub BTreeMap<i32, BTreeSet<i32>>); pub struct TileCoordMap(pub BTreeMap<i32, BTreeSet<i32>>);
impl TileCoordMap { impl TileCoordMap {
/// Checks whether the map contains a given coordinate pair
pub fn contains(&self, coords: TileCoords) -> bool { pub fn contains(&self, coords: TileCoords) -> bool {
let Some(xs) = self.0.get(&coords.z) else { let Some(xs) = self.0.get(&coords.z) else {
return false; return false;
@ -38,39 +52,62 @@ impl TileCoordMap {
} }
} }
/// Data structure for storing chunk data between processing and rendering steps
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProcessedChunk { pub struct ProcessedChunk {
/// Block type data
pub blocks: Box<layer::BlockArray>, pub blocks: Box<layer::BlockArray>,
/// Biome data
pub biomes: Box<layer::BiomeArray>, pub biomes: Box<layer::BiomeArray>,
/// Block height/depth data
pub depths: Box<layer::DepthArray>, pub depths: Box<layer::DepthArray>,
} }
/// Data structure for storing region data between processing and rendering steps
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ProcessedRegion { pub struct ProcessedRegion {
/// List of biomes used in the region
///
/// Indexed by [ProcessedChunk] biome data
pub biome_list: IndexSet<Biome>, pub biome_list: IndexSet<Biome>,
/// Processed chunk data
pub chunks: ChunkArray<Option<Box<ProcessedChunk>>>, pub chunks: ChunkArray<Option<Box<ProcessedChunk>>>,
} }
pub struct Config { /// Derives a filename from region coordinates and a file extension
pub num_threads: usize, ///
pub region_dir: PathBuf, /// Can be used for input regions, processed data or rendered tiles
pub processed_dir: PathBuf,
pub output_dir: PathBuf,
pub level_dat_path: PathBuf,
pub metadata_path: PathBuf,
}
fn coord_filename(coords: TileCoords, ext: &str) -> String { fn coord_filename(coords: TileCoords, ext: &str) -> String {
format!("r.{}.{}.{}", coords.x, coords.z, ext) format!("r.{}.{}.{}", coords.x, coords.z, ext)
} }
/// Tile kind corresponding to a map layer
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum TileKind { pub enum TileKind {
/// Regular map tile contains block colors
Map, Map,
/// Lightmap tile for illumination layer
Lightmap, 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 { impl Config {
/// Crates a new [Config] from [command line arguments](super::Args)
pub fn new(args: &super::Args) -> Self { pub fn new(args: &super::Args) -> Self {
let num_threads = match args.jobs { let num_threads = match args.jobs {
Some(0) => num_cpus::get(), Some(0) => num_cpus::get(),
@ -79,30 +116,33 @@ impl Config {
}; };
let region_dir = [&args.input_dir, Path::new("region")].iter().collect(); 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 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(); let metadata_path = [&args.output_dir, Path::new("info.json")].iter().collect();
Config { Config {
num_threads, num_threads,
region_dir, region_dir,
processed_dir,
output_dir: args.output_dir.clone(),
level_dat_path, level_dat_path,
output_dir: args.output_dir.clone(),
processed_dir,
metadata_path, metadata_path,
} }
} }
/// Constructs the path to an input region file
pub fn region_path(&self, coords: TileCoords) -> PathBuf { pub fn region_path(&self, coords: TileCoords) -> PathBuf {
let filename = coord_filename(coords, "mca"); let filename = coord_filename(coords, "mca");
[&self.region_dir, Path::new(&filename)].iter().collect() [&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 { pub fn processed_path(&self, coords: TileCoords) -> PathBuf {
let filename = coord_filename(coords, "bin"); let filename = coord_filename(coords, "bin");
[&self.processed_dir, Path::new(&filename)].iter().collect() [&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 { pub fn tile_dir(&self, kind: TileKind, level: usize) -> PathBuf {
let prefix = match kind { let prefix = match kind {
TileKind::Map => "map", TileKind::Map => "map",
@ -112,6 +152,7 @@ impl Config {
[&self.output_dir, Path::new(&dir)].iter().collect() [&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 { pub fn tile_path(&self, kind: TileKind, level: usize, coords: TileCoords) -> PathBuf {
let filename = coord_filename(coords, "png"); let filename = coord_filename(coords, "png");
let dir = self.tile_dir(kind, level); 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) pub fn overlay_chunk<I, J>(image: &mut I, chunk: &J, coords: ChunkCoords)
where where
I: image::GenericImage, 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 common;
mod metadata_writer; mod metadata_writer;
mod region_group; mod region_group;
@ -18,6 +23,7 @@ use region_processor::RegionProcessor;
use tile_mipmapper::TileMipmapper; use tile_mipmapper::TileMipmapper;
use tile_renderer::TileRenderer; use tile_renderer::TileRenderer;
/// Command line arguments for minedmap
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
pub struct Args { pub struct Args {
/// Number of parallel threads to use for processing /// Number of parallel threads to use for processing
@ -32,6 +38,7 @@ pub struct Args {
pub output_dir: PathBuf, pub output_dir: PathBuf,
} }
/// Configures the Rayon thread pool for parallel processing
fn setup_threads(num_threads: usize) -> Result<()> { fn setup_threads(num_threads: usize) -> Result<()> {
rayon::ThreadPoolBuilder::new() rayon::ThreadPoolBuilder::new()
.num_threads(num_threads) .num_threads(num_threads)

View file

@ -1,3 +1,5 @@
//! The [MetadataWriter] and related types
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use serde::Serialize; use serde::Serialize;
@ -6,43 +8,62 @@ use super::{
core::{self, io::fs, world::de}, core::{self, io::fs, world::de},
}; };
/// Minimum and maximum X and Z tile coordinates for a mipmap level
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct Bounds { struct Bounds {
/// Minimum X coordinate
min_x: i32, min_x: i32,
/// Maximum X coordinate
max_x: i32, max_x: i32,
/// Minimum Z coordinate
min_z: i32, min_z: i32,
/// Maximum Z coordinate
max_z: i32, max_z: i32,
} }
/// Mipmap level information in viewer metadata file
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
struct Mipmap<'t> { struct Mipmap<'t> {
/// Minimum and maximum tile coordinates of the mipmap level
bounds: Bounds, bounds: Bounds,
/// Map of populated tiles for the mipmap level
regions: &'t TileCoordMap, regions: &'t TileCoordMap,
} }
/// Initial spawn point for new players
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
struct Spawn { struct Spawn {
/// Spawn X coordinate
x: i32, x: i32,
/// Spawn Z coordinate
z: i32, z: i32,
} }
/// Viewer metadata JSON data structure
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
struct Metadata<'t> { struct Metadata<'t> {
/// Tile information for each mipmap level
mipmaps: Vec<Mipmap<'t>>, mipmaps: Vec<Mipmap<'t>>,
/// Initial spawn point for new players
spawn: Spawn, spawn: Spawn,
} }
/// The MetadataWriter is used to generate the viewer metadata file
pub struct MetadataWriter<'a> { pub struct MetadataWriter<'a> {
/// Common MinedMap configuration from command line
config: &'a Config, config: &'a Config,
/// Map of generated tiles for each mipmap level
tiles: &'a [TileCoordMap], tiles: &'a [TileCoordMap],
} }
impl<'a> MetadataWriter<'a> { impl<'a> MetadataWriter<'a> {
/// Creates a new MetadataWriter
pub fn new(config: &'a Config, tiles: &'a [TileCoordMap]) -> Self { pub fn new(config: &'a Config, tiles: &'a [TileCoordMap]) -> Self {
MetadataWriter { config, tiles } MetadataWriter { config, tiles }
} }
/// Helper to construct a [Mipmap] data structure from a [TileCoordMap]
fn mipmap_entry(regions: &TileCoordMap) -> Mipmap { fn mipmap_entry(regions: &TileCoordMap) -> Mipmap {
let mut min_x = i32::MAX; let mut min_x = i32::MAX;
let mut max_x = i32::MIN; 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> { fn read_level_dat(&self) -> Result<de::LevelDat> {
core::io::data::from_file(&self.config.level_dat_path).context("Failed to read level.dat") 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 { fn spawn(level_dat: &de::LevelDat) -> Spawn {
Spawn { Spawn {
x: level_dat.data.spawn_x, 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<()> { pub fn run(self) -> Result<()> {
let level_dat = self.read_level_dat()?; let level_dat = self.read_level_dat()?;

View file

@ -1,3 +1,5 @@
//! The generic [RegionGroup] data structure
use std::{future::Future, iter}; use std::{future::Future, iter};
use anyhow::Result; use anyhow::Result;
@ -5,17 +7,27 @@ use futures_util::future::OptionFuture;
/// A generic array of 3x3 elements /// 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. /// regions.
/// ///
/// The center element is always populated, while the 8 adjacent elements may be None. /// The center element is always populated, while the 8 adjacent elements may be None.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct RegionGroup<T> { pub struct RegionGroup<T> {
/// The element corresponding to the center of the 9x9 neighborhood
center: T, center: T,
/// The remaining elements, stored in row-first order
///
/// The center element is always None.
neighs: [Option<T>; 9], neighs: [Option<T>; 9],
} }
impl<T> RegionGroup<T> { 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 pub fn new<F>(f: F) -> Self
where where
F: Fn(i8, i8) -> Option<T>, 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 { pub fn center(&self) -> &T {
&self.center &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> { pub fn get(&self, x: i8, z: i8) -> Option<&T> {
if (x, z) == (0, 0) { if (x, z) == (0, 0) {
return Some(&self.center); return Some(&self.center);
@ -50,6 +66,7 @@ impl<T> RegionGroup<T> {
self.neighs.get((3 * x + z + 4) as usize)?.as_ref() 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> pub fn map<U, F>(self, mut f: F) -> RegionGroup<U>
where where
F: FnMut(T) -> U, 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>> pub fn try_map<U, F>(self, mut f: F) -> Result<RegionGroup<U>>
where where
F: FnMut(T) -> Result<U>, F: FnMut(T) -> Result<U>,
@ -70,6 +91,7 @@ impl<T> RegionGroup<T> {
Ok(RegionGroup { center, neighs }) Ok(RegionGroup { center, neighs })
} }
/// Runs an asynchronous closure on each element to construct a new RegionGroup
#[allow(dead_code)] #[allow(dead_code)]
pub async fn async_map<U, F, Fut>(self, mut f: F) -> RegionGroup<U> pub async fn async_map<U, F, Fut>(self, mut f: F) -> RegionGroup<U>
where 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>> pub async fn async_try_map<U, F, Fut>(self, mut f: F) -> Result<RegionGroup<U>>
where where
Fut: Future<Output = Result<U>>, Fut: Future<Output = Result<U>>,
@ -110,6 +136,7 @@ impl<T> RegionGroup<T> {
Ok(RegionGroup { center, neighs }) Ok(RegionGroup { center, neighs })
} }
/// Returns an [Iterator] over all populated elements
pub fn iter(&self) -> impl Iterator<Item = &T> { pub fn iter(&self) -> impl Iterator<Item = &T> {
iter::once(&self.center).chain(self.neighs.iter().filter_map(Option::as_ref)) 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 std::{ffi::OsStr, path::Path, time::SystemTime};
use anyhow::{Context, Result}; 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 /// 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> { pub struct RegionProcessor<'a> {
/// Registry of known block types
block_types: resource::BlockTypes, block_types: resource::BlockTypes,
/// Registry of known biome types
biome_types: resource::BiomeTypes, biome_types: resource::BiomeTypes,
/// Common MinedMap configuration from command line
config: &'a Config, config: &'a Config,
} }
impl<'a> RegionProcessor<'a> { impl<'a> RegionProcessor<'a> {
/// Constructs a new RegionProcessor
pub fn new(config: &'a Config) -> Self { pub fn new(config: &'a Config) -> Self {
RegionProcessor { RegionProcessor {
block_types: resource::BlockTypes::default(), 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>> { fn collect_regions(&self) -> Result<Vec<TileCoords>> {
Ok(self Ok(self
.config .config
@ -80,9 +90,11 @@ impl<'a> RegionProcessor<'a> {
world::layer::top_layer(biome_list, &chunk) world::layer::top_layer(biome_list, &chunk)
} }
/// Renders a lightmap subtile from chunk block light data
fn render_chunk_lightmap( fn render_chunk_lightmap(
block_light: Box<world::layer::BlockLightArray>, block_light: Box<world::layer::BlockLightArray>,
) -> image::GrayAlphaImage { ) -> image::GrayAlphaImage {
/// Width/height of generated chunk lightmap
const N: u32 = BLOCKS_PER_CHUNK as u32; const N: u32 = BLOCKS_PER_CHUNK as u32;
image::GrayAlphaImage::from_fn(N, N, |x, z| { 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( fn save_region(
path: &Path, path: &Path,
processed_region: &ProcessedRegion, processed_region: &ProcessedRegion,
@ -103,6 +118,9 @@ impl<'a> RegionProcessor<'a> {
storage::write(path, processed_region, FILE_META_VERSION, timestamp) 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( fn save_lightmap(
path: &Path, path: &Path,
lightmap: &image::GrayAlphaImage, lightmap: &image::GrayAlphaImage,
@ -117,6 +135,7 @@ impl<'a> RegionProcessor<'a> {
/// Processes a single region file /// Processes a single region file
fn process_region(&self, coords: TileCoords) -> Result<()> { fn process_region(&self, coords: TileCoords) -> Result<()> {
/// Width/height of the region data
const N: u32 = (BLOCKS_PER_CHUNK * CHUNKS_PER_REGION) as u32; const N: u32 = (BLOCKS_PER_CHUNK * CHUNKS_PER_REGION) as u32;
let mut processed_region = ProcessedRegion::default(); let mut processed_region = ProcessedRegion::default();

View file

@ -1,3 +1,5 @@
//! The [TileMipmapper]
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use rayon::prelude::*; use rayon::prelude::*;
@ -6,16 +8,24 @@ use super::{
core::{io::fs, types::*}, core::{io::fs, types::*},
}; };
/// Generates mipmap tiles from full-resolution tile images
pub struct TileMipmapper<'a> { pub struct TileMipmapper<'a> {
/// Common MinedMap configuration from command line
config: &'a Config, config: &'a Config,
/// List of populated tiles for base mipmap level (level 0)
regions: &'a [TileCoords], regions: &'a [TileCoords],
} }
impl<'a> TileMipmapper<'a> { impl<'a> TileMipmapper<'a> {
/// Constructs a new TileMipmapper
pub fn new(config: &'a Config, regions: &'a [TileCoords]) -> Self { pub fn new(config: &'a Config, regions: &'a [TileCoords]) -> Self {
TileMipmapper { config, regions } 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 { fn done(tiles: &TileCoordMap) -> bool {
tiles tiles
.0 .0
@ -23,6 +33,7 @@ impl<'a> TileMipmapper<'a> {
.all(|(z, xs)| (-1..=0).contains(z) && xs.iter().all(|x| (-1..=0).contains(x))) .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 { fn map_coords(tiles: &TileCoordMap) -> TileCoordMap {
let mut ret = TileCoordMap::default(); let mut ret = TileCoordMap::default();
@ -38,6 +49,10 @@ impl<'a> TileMipmapper<'a> {
ret 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>( fn render_mipmap<P: image::PixelWithColorType>(
&self, &self,
kind: TileKind, kind: TileKind,
@ -49,6 +64,7 @@ impl<'a> TileMipmapper<'a> {
[P::Subpixel]: image::EncodableLayout, [P::Subpixel]: image::EncodableLayout,
image::ImageBuffer<P, Vec<P::Subpixel>>: Into<image::DynamicImage>, image::ImageBuffer<P, Vec<P::Subpixel>>: Into<image::DynamicImage>,
{ {
/// Tile width/height
const N: u32 = (BLOCKS_PER_CHUNK * CHUNKS_PER_REGION) as u32; const N: u32 = (BLOCKS_PER_CHUNK * CHUNKS_PER_REGION) as u32;
let output_path = self.config.tile_path(kind, level, coords); 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>> { pub fn run(self) -> Result<Vec<TileCoordMap>> {
let mut tile_stack = { let mut tile_stack = {
let mut tile_map = TileCoordMap::default(); let mut tile_map = TileCoordMap::default();

View file

@ -1,3 +1,5 @@
//! The [TileRenderer] and related types and functions
use std::{ use std::{
num::NonZeroUsize, num::NonZeroUsize,
path::PathBuf, path::PathBuf,
@ -22,8 +24,16 @@ use super::{
region_group::RegionGroup, region_group::RegionGroup,
}; };
/// Type for referencing loaded [ProcessedRegion] data
type RegionRef = Arc<ProcessedRegion>; 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( fn biome_at(
region_group: &RegionGroup<RegionRef>, region_group: &RegionGroup<RegionRef>,
chunk: ChunkCoords, chunk: ChunkCoords,
@ -49,15 +59,22 @@ fn biome_at(
)) ))
} }
/// The TileRenderer generates map tiles from processed region data
pub struct TileRenderer<'a> { pub struct TileRenderer<'a> {
/// Common MinedMap configuration from command line
config: &'a Config, config: &'a Config,
/// Runtime for asynchronous region loading
rt: &'a tokio::runtime::Runtime, rt: &'a tokio::runtime::Runtime,
/// List of populated regions to render tiles for
regions: &'a [TileCoords], regions: &'a [TileCoords],
/// Set of populated regions for fast existence checking
region_set: rustc_hash::FxHashSet<TileCoords>, region_set: rustc_hash::FxHashSet<TileCoords>,
/// Cache of previously loaded regions
region_cache: Mutex<LruCache<PathBuf, Arc<OnceCell<RegionRef>>>>, region_cache: Mutex<LruCache<PathBuf, Arc<OnceCell<RegionRef>>>>,
} }
impl<'a> TileRenderer<'a> { impl<'a> TileRenderer<'a> {
/// Constructs a new TileRenderer
pub fn new( pub fn new(
config: &'a Config, config: &'a Config,
rt: &'a tokio::runtime::Runtime, 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> { async fn load_region(&self, processed_path: PathBuf) -> Result<RegionRef> {
let region_loader = { let region_loader = {
let mut region_cache = self.region_cache.lock().unwrap(); let mut region_cache = self.region_cache.lock().unwrap();
@ -96,6 +114,7 @@ impl<'a> TileRenderer<'a> {
.cloned() .cloned()
} }
/// Loads a 3x3 neighborhood of processed region data
async fn load_region_group( async fn load_region_group(
&self, &self,
processed_paths: RegionGroup<PathBuf>, processed_paths: RegionGroup<PathBuf>,
@ -105,18 +124,29 @@ impl<'a> TileRenderer<'a> {
.await .await
} }
/// Computes the color of a tile pixel
fn block_color_at( fn block_color_at(
region_group: &RegionGroup<RegionRef>, region_group: &RegionGroup<RegionRef>,
chunk: &ProcessedChunk, chunk: &ProcessedChunk,
chunk_coords: ChunkCoords, chunk_coords: ChunkCoords,
block_coords: LayerBlockCoords, block_coords: LayerBlockCoords,
) -> Option<Vec3> { ) -> 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 { 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 (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]]; 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; 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; const Z: isize = SMOOTH.len() as isize - 1;
let block = chunk.blocks[block_coords]?; let block = chunk.blocks[block_coords]?;
@ -168,12 +198,14 @@ impl<'a> TileRenderer<'a> {
Some(color / total) Some(color / total)
} }
/// Renders a chunk subtile into a region tile image
fn render_chunk( fn render_chunk(
image: &mut image::RgbaImage, image: &mut image::RgbaImage,
region_group: &RegionGroup<RegionRef>, region_group: &RegionGroup<RegionRef>,
chunk: &ProcessedChunk, chunk: &ProcessedChunk,
chunk_coords: ChunkCoords, chunk_coords: ChunkCoords,
) { ) {
/// Width/height of a chunk subtile
const N: u32 = BLOCKS_PER_CHUNK as u32; const N: u32 = BLOCKS_PER_CHUNK as u32;
let chunk_image = image::RgbaImage::from_fn(N, N, |x, z| { 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); overlay_chunk(image, &chunk_image, chunk_coords);
} }
/// Renders a region tile image
fn render_region(image: &mut image::RgbaImage, region_group: &RegionGroup<RegionRef>) { fn render_region(image: &mut image::RgbaImage, region_group: &RegionGroup<RegionRef>) {
for (coords, chunk) in region_group.center().chunks.iter() { for (coords, chunk) in region_group.center().chunks.iter() {
let Some(chunk) = chunk else { 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)> { fn processed_source(&self, coords: TileCoords) -> Result<(PathBuf, SystemTime)> {
let path = self.config.processed_path(coords); let path = self.config.processed_path(coords);
let timestamp = fs::modified_timestamp(&path)?; let timestamp = fs::modified_timestamp(&path)?;
Ok((path, timestamp)) 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)> { fn processed_sources(&self, coords: TileCoords) -> Result<(RegionGroup<PathBuf>, SystemTime)> {
let sources = RegionGroup::new(|x, z| { let sources = RegionGroup::new(|x, z| {
Some(TileCoords { Some(TileCoords {
@ -228,7 +264,9 @@ impl<'a> TileRenderer<'a> {
Ok((paths, max_timestamp)) Ok((paths, max_timestamp))
} }
/// Renders and saves a region tile image
fn render_tile(&self, coords: TileCoords) -> Result<()> { fn render_tile(&self, coords: TileCoords) -> Result<()> {
/// Width/height of a tile image
const N: u32 = (BLOCKS_PER_CHUNK * CHUNKS_PER_REGION) as u32; const N: u32 = (BLOCKS_PER_CHUNK * CHUNKS_PER_REGION) as u32;
let (processed_paths, processed_timestamp) = self.processed_sources(coords)?; 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<()> { pub fn run(self) -> Result<()> {
fs::create_dir_all(&self.config.tile_dir(TileKind::Map, 0))?; 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 std::path::PathBuf;
use anyhow::Result; use anyhow::Result;
use clap::Parser; use clap::Parser;
/// Command line arguments for nbtdump
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
struct Args { struct Args {
/// Filename to dump /// 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 std::path::PathBuf;
use anyhow::Result; use anyhow::Result;
use clap::Parser; use clap::Parser;
/// Command line arguments for regiondump
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
struct Args { struct Args {
/// Filename to dump /// 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 std::{fs::File, io::prelude::*, path::Path};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use flate2::read::GzDecoder; use flate2::read::GzDecoder;
use serde::de::DeserializeOwned; 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> pub fn from_reader<R, T>(reader: R) -> Result<T>
where where
R: Read, R: Read,
@ -18,6 +21,7 @@ where
fastnbt::from_bytes(&buf).context("Failed to decode NBT data") 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> pub fn from_file<P, T>(path: P) -> Result<T>
where where
P: AsRef<Path>, P: AsRef<Path>,

View file

@ -1,3 +1,5 @@
//! Helpers and common functions for filesystem access
use std::{ use std::{
fs::{self, File}, fs::{self, File},
io::{BufReader, BufWriter, Read, Write}, io::{BufReader, BufWriter, Read, Write},
@ -8,15 +10,25 @@ use std::{
use anyhow::{Context, Ok, Result}; use anyhow::{Context, Ok, Result};
use serde::{Deserialize, Serialize}; 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)] #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub struct FileMetaVersion(pub u32); pub struct FileMetaVersion(pub u32);
/// Metadata stored with generated files to track required incremental updates
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
struct FileMeta { struct FileMeta {
/// Version of data described by the FileMeta
version: FileMetaVersion, 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, timestamp: SystemTime,
} }
/// Helper for creating suffixed file paths
fn suffix_name(path: &Path, suffix: &str) -> PathBuf { fn suffix_name(path: &Path, suffix: &str) -> PathBuf {
let mut file_name = path.file_name().unwrap_or_default().to_os_string(); let mut file_name = path.file_name().unwrap_or_default().to_os_string();
file_name.push(suffix); file_name.push(suffix);
@ -26,24 +38,35 @@ fn suffix_name(path: &Path, suffix: &str) -> PathBuf {
ret ret
} }
/// Derives the filename for temporary storage of data during generation
fn tmpfile_name(path: &Path) -> PathBuf { fn tmpfile_name(path: &Path) -> PathBuf {
suffix_name(path, ".tmp") suffix_name(path, ".tmp")
} }
/// Derives the filename for associated metadata for generated files
fn metafile_name(path: &Path) -> PathBuf { fn metafile_name(path: &Path) -> PathBuf {
suffix_name(path, ".meta") 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<()> { pub fn create_dir_all(path: &Path) -> Result<()> {
fs::create_dir_all(path) fs::create_dir_all(path)
.with_context(|| format!("Failed to create directory {}", path.display(),)) .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<()> { pub fn rename(from: &Path, to: &Path) -> Result<()> {
fs::rename(from, to) fs::rename(from, to)
.with_context(|| format!("Failed to rename {} to {}", from.display(), to.display())) .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> pub fn create<T, F>(path: &Path, f: F) -> Result<T>
where where
F: FnOnce(&mut BufWriter<File>) -> Result<T>, F: FnOnce(&mut BufWriter<File>) -> Result<T>,
@ -60,6 +83,7 @@ where
.with_context(|| format!("Failed to write file {}", path.display())) .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> { pub fn equal(path1: &Path, path2: &Path) -> Result<bool> {
let mut file1 = BufReader::new( let mut file1 = BufReader::new(
fs::File::open(path1) 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> pub fn create_with_tmpfile<T, F>(path: &Path, f: F) -> Result<T>
where where
F: FnOnce(&mut BufWriter<File>) -> Result<T>, F: FnOnce(&mut BufWriter<File>) -> Result<T>,
@ -104,6 +134,7 @@ where
ret ret
} }
/// Returns the time of last modification for a given file path
pub fn modified_timestamp(path: &Path) -> Result<SystemTime> { pub fn modified_timestamp(path: &Path) -> Result<SystemTime> {
fs::metadata(path) fs::metadata(path)
.and_then(|meta| meta.modified()) .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> { pub fn read_timestamp(path: &Path, version: FileMetaVersion) -> Option<SystemTime> {
let meta_path = metafile_name(path); let meta_path = metafile_name(path);
let mut file = BufReader::new(fs::File::open(meta_path).ok()?); 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) 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>( pub fn create_with_timestamp<T, F>(
path: &Path, path: &Path,
version: FileMetaVersion, version: FileMetaVersion,

View file

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

View file

@ -1,3 +1,5 @@
//! Functions for reading and deserializing region data
use std::{ use std::{
fs::File, fs::File,
io::{prelude::*, SeekFrom}, io::{prelude::*, SeekFrom},
@ -10,15 +12,24 @@ use serde::de::DeserializeOwned;
use crate::types::*; 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; const BLOCKSIZE: usize = 4096;
/// Chunk descriptor extracted from region file header
#[derive(Debug)] #[derive(Debug)]
struct ChunkDesc { struct ChunkDesc {
/// Offset of data block where the chunk starts
offset: u32, offset: u32,
/// Number of data block used by the chunk
len: u8, len: u8,
/// Coodinates of chunk described by this descriptor
coords: ChunkCoords, coords: ChunkCoords,
} }
/// Parses the header of a region data file
fn parse_header(header: &ChunkArray<u32>) -> Vec<ChunkDesc> { fn parse_header(header: &ChunkArray<u32>) -> Vec<ChunkDesc> {
let mut chunks: Vec<_> = header let mut chunks: Vec<_> = header
.iter() .iter()
@ -45,6 +56,7 @@ fn parse_header(header: &ChunkArray<u32>) -> Vec<ChunkDesc> {
chunks chunks
} }
/// Decompresses chunk data and deserializes to a given data structure
fn decode_chunk<T>(buf: &[u8]) -> Result<T> fn decode_chunk<T>(buf: &[u8]) -> Result<T>
where where
T: DeserializeOwned, T: DeserializeOwned,
@ -63,12 +75,18 @@ where
fastnbt::from_bytes(&decode_buffer).context("Failed to decode NBT data") fastnbt::from_bytes(&decode_buffer).context("Failed to decode NBT data")
} }
/// Wraps a reader used to read a region data file
#[derive(Debug)] #[derive(Debug)]
pub struct Region<R: Read + Seek> { pub struct Region<R: Read + Seek> {
/// The wrapper reader
reader: R, reader: R,
} }
impl<R: Read + Seek> Region<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<()> pub fn foreach_chunk<T, F>(self, mut f: F) -> Result<()>
where where
R: Read + Seek, 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> pub fn from_reader<R>(reader: R) -> Region<R>
where where
R: Read + Seek, R: Read + Seek,
@ -133,6 +152,7 @@ where
Region { reader } Region { reader }
} }
/// Creates a new [Region] for a file
pub fn from_file<P>(path: P) -> Result<Region<File>> pub fn from_file<P>(path: P) -> Result<Region<File>>
where where
P: AsRef<Path>, 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::{ use std::{
fs::File, fs::File,
io::{Read, Write}, io::{Read, Write},
@ -10,6 +14,9 @@ use serde::{de::DeserializeOwned, Serialize};
use super::fs; 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>( pub fn write<T: Serialize>(
path: &Path, path: &Path,
value: &T, 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> { pub fn read<T: DeserializeOwned>(path: &Path) -> Result<T> {
(|| -> Result<T> { (|| -> Result<T> {
let mut file = File::open(path)?; 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 io;
pub mod resource; pub mod resource;
pub mod types; pub mod types;

View file

@ -1,25 +1,51 @@
//! Biome data structures
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::Color; use super::Color;
/// Grass color modifier used by a biome
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum BiomeGrassColorModifier { pub enum BiomeGrassColorModifier {
/// Grass color modifier used by the dark forest biome
DarkForest, DarkForest,
/// Grass color modifier used by swamp biomes
Swamp, 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)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Biome { 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, 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, pub downfall: i8,
/// Water color override
pub water_color: Option<Color>, pub water_color: Option<Color>,
/// Foliage color override
pub foliage_color: Option<Color>, pub foliage_color: Option<Color>,
/// Grass color override
pub grass_color: Option<Color>, pub grass_color: Option<Color>,
/// Grass color modifier
pub grass_color_modifier: Option<BiomeGrassColorModifier>, pub grass_color_modifier: Option<BiomeGrassColorModifier>,
} }
impl Biome { impl Biome {
/// Constructs a new Biome
const fn new(temp: i16, downfall: i16) -> 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 { const fn encode(v: i16) -> i8 {
(v / 5) as 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 { const fn water(self, water_color: [u8; 3]) -> Biome {
Biome { Biome {
water_color: Some(Color(water_color)), 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 { const fn foliage(self, foliage_color: [u8; 3]) -> Biome {
Biome { Biome {
foliage_color: Some(Color(foliage_color)), 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 { const fn grass(self, grass_color: [u8; 3]) -> Biome {
Biome { Biome {
grass_color: Some(Color(grass_color)), 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 { const fn modify(self, grass_color_modifier: BiomeGrassColorModifier) -> Biome {
Biome { Biome {
grass_color_modifier: Some(grass_color_modifier), 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 { fn decode(val: i8) -> f32 {
f32::from(val) / 20.0 f32::from(val) / 20.0
} }
/// Returns the biome's temperature decoded to its original float value
pub fn temp(&self) -> f32 { pub fn temp(&self) -> f32 {
Self::decode(self.temp) Self::decode(self.temp)
} }
/// Returns the biome's downfall decoded to its original float value
pub fn downfall(&self) -> f32 { pub fn downfall(&self) -> f32 {
Self::decode(self.downfall) Self::decode(self.downfall)
} }
} }
// Data extracted from Minecraft code decompiled using https://github.com/Hexeption/MCP-Reborn /// Standard biome specifications
#[allow(clippy::zero_prefixed_literal)]
pub const BIOMES: &[(&str, Biome)] = { pub const BIOMES: &[(&str, Biome)] = {
use BiomeGrassColorModifier::*; 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 // 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)] = &[ pub const BIOME_ALIASES: &[(&str, &str)] = &[
// Biomes fix // Biomes fix
("beaches", "beach"), ("beaches", "beach"),
@ -292,6 +335,7 @@ pub const BIOME_ALIASES: &[(&str, &str)] = &[
("deep_warm_ocean", "warm_ocean"), ("deep_warm_ocean", "warm_ocean"),
]; ];
/// Maps old numeric biome IDs to new string IDs
pub fn legacy_biome(index: u8) -> &'static str { pub fn legacy_biome(index: u8) -> &'static str {
match index { match index {
0 => "ocean", 0 => "ocean",

View file

@ -1,15 +1,23 @@
//! Functions for computations of block colors
use super::{Biome, BlockType, Color}; use super::{Biome, BlockType, Color};
use glam::Vec3; use glam::Vec3;
/// Converts an u8 RGB color to a float vector
fn color_vec_unscaled(color: Color) -> Vec3 { fn color_vec_unscaled(color: Color) -> Vec3 {
Vec3::from_array(color.0.map(f32::from)) 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 { fn color_vec(color: Color) -> Vec3 {
color_vec_unscaled(color) / 255.0 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 { 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 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; 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] colors[0] + temp * colors[1] + downfall * colors[2]
} }
/// Extension trait with helpers for computing biome-specific block colors
trait BiomeExt { trait BiomeExt {
/// Returns the grass color of the biome at a given depth
fn grass_color(&self, depth: f32) -> Vec3; 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; fn foliage_color(&self, depth: f32) -> Vec3;
/// Returns the water color of the biome
fn water_color(&self) -> Vec3; fn water_color(&self) -> Vec3;
} }
@ -27,12 +39,15 @@ impl BiomeExt for Biome {
fn grass_color(&self, depth: f32) -> Vec3 { fn grass_color(&self, depth: f32) -> Vec3 {
use super::BiomeGrassColorModifier::*; use super::BiomeGrassColorModifier::*;
/// Color matrix extracted from grass color texture
const GRASS_COLORS: [Vec3; 3] = [ const GRASS_COLORS: [Vec3; 3] = [
Vec3::new(0.502, 0.706, 0.592), // lower right 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.247, 0.012, -0.259), // lower left - lower right
Vec3::new(-0.471, 0.086, -0.133), // upper left - lower left 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])) 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])) const SWAMP_GRASS_COLOR: Vec3 = Vec3::new(0.416, 0.439, 0.224); // == color_vec(Color([106, 112, 57]))
let regular_color = || { let regular_color = || {
@ -49,6 +64,7 @@ impl BiomeExt for Biome {
} }
fn foliage_color(&self, depth: f32) -> Vec3 { fn foliage_color(&self, depth: f32) -> Vec3 {
/// Color matrix extracted from foliage color texture
const FOLIAGE_COLORS: [Vec3; 3] = [ const FOLIAGE_COLORS: [Vec3; 3] = [
Vec3::new(0.376, 0.631, 0.482), // lower right Vec3::new(0.376, 0.631, 0.482), // lower right
Vec3::new(0.306, 0.012, -0.317), // lower left - 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 { 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])) const DEFAULT_WATER_COLOR: Vec3 = Vec3::new(0.247, 0.463, 0.894); // == color_vec(Color([63, 118, 228]))
self.water_color 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])) 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])) 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 { pub fn needs_biome(block: BlockType) -> bool {
use super::BlockFlag::*; use super::BlockFlag::*;
block.is(Grass) || block.is(Foliage) || block.is(Water) 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 { pub fn block_color(block: BlockType, biome: Option<&Biome>, depth: f32) -> Vec3 {
use super::BlockFlag::*; 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] { const fn simple(id: &str) -> [&str; 16] {
[ [
id, id, id, id, id, id, id, id, id, id, id, id, id, id, id, id, 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"; const DEF: &str = "air";
/// Default entry for block type numbers that are unassigned regardless of subtype
const EMPTY: [&str; 16] = simple(DEF); 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] = [ pub const LEGACY_BLOCK_TYPES: [[&str; 16]; 256] = [
/* 0 */ /* 0 */
simple("air"), simple("air"),

View file

@ -1,43 +1,62 @@
//! Data describing Minecraft biomes and block types
mod biomes; mod biomes;
mod block_color; mod block_color;
mod block_types;
mod legacy_block_types; mod legacy_block_types;
#[allow(clippy::missing_docs_in_private_items)] // Generated module
mod block_types;
use std::collections::HashMap; use std::collections::HashMap;
use enumflags2::{bitflags, BitFlags}; use enumflags2::{bitflags, BitFlags};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Flags describing special properties of [BlockType]s
#[bitflags] #[bitflags]
#[repr(u8)] #[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum BlockFlag { pub enum BlockFlag {
/// The block type is opaque
Opaque, Opaque,
/// The block type is colored using biome grass colors
Grass, Grass,
/// The block type is colored using biome foliage colors
Foliage, Foliage,
/// The block type is birch foliage
Birch, Birch,
/// The block type is spurce foliage
Spruce, Spruce,
/// The block type is colored using biome water colors
Water, Water,
} }
/// An RGB color
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Color(pub [u8; 3]); pub struct Color(pub [u8; 3]);
/// A block type specification
#[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct BlockType { pub struct BlockType {
/// Bit set of [BlockFlag]s describing special properties of the block type
pub flags: BitFlags<BlockFlag>, pub flags: BitFlags<BlockFlag>,
/// Base color of the block type
pub color: Color, pub color: Color,
} }
impl BlockType { impl BlockType {
/// Checks whether a block type has a given [BlockFlag] set
pub fn is(&self, flag: BlockFlag) -> bool { pub fn is(&self, flag: BlockFlag) -> bool {
self.flags.contains(flag) self.flags.contains(flag)
} }
} }
/// Used to look up standard Minecraft block types
#[derive(Debug)] #[derive(Debug)]
pub struct BlockTypes { pub struct BlockTypes {
/// Map of string IDs to block types
block_type_map: HashMap<String, BlockType>, 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]>, legacy_block_types: Box<[[BlockType; 16]; 256]>,
} }
@ -59,12 +78,14 @@ impl Default for BlockTypes {
} }
impl BlockTypes { impl BlockTypes {
/// Resolves a Minecraft 1.13+ string block type ID
#[inline] #[inline]
pub fn get(&self, id: &str) -> Option<BlockType> { pub fn get(&self, id: &str) -> Option<BlockType> {
let suffix = id.strip_prefix("minecraft:")?; let suffix = id.strip_prefix("minecraft:")?;
self.block_type_map.get(suffix).copied() self.block_type_map.get(suffix).copied()
} }
/// Resolves a Minecraft pre-1.13 numeric block type ID
#[inline] #[inline]
pub fn get_legacy(&self, id: u8, data: u8) -> Option<BlockType> { pub fn get_legacy(&self, id: u8, data: u8) -> Option<BlockType> {
Some(self.legacy_block_types[id as usize][data as usize]) Some(self.legacy_block_types[id as usize][data as usize])
@ -74,9 +95,12 @@ impl BlockTypes {
pub use biomes::{Biome, BiomeGrassColorModifier}; pub use biomes::{Biome, BiomeGrassColorModifier};
pub use block_color::{block_color, needs_biome}; pub use block_color::{block_color, needs_biome};
/// Used to look up standard Minecraft biome types
#[derive(Debug)] #[derive(Debug)]
pub struct BiomeTypes { pub struct BiomeTypes {
/// Map of string IDs to biome types
biome_map: HashMap<String, &'static Biome>, biome_map: HashMap<String, &'static Biome>,
/// Array used to look up old numeric biome IDs
legacy_biomes: Box<[&'static Biome; 256]>, legacy_biomes: Box<[&'static Biome; 256]>,
} }
@ -112,12 +136,14 @@ impl Default for BiomeTypes {
} }
impl BiomeTypes { impl BiomeTypes {
/// Resolves a Minecraft 1.18+ string biome type ID
#[inline] #[inline]
pub fn get(&self, id: &str) -> Option<&Biome> { pub fn get(&self, id: &str) -> Option<&Biome> {
let suffix = id.strip_prefix("minecraft:")?; let suffix = id.strip_prefix("minecraft:")?;
self.biome_map.get(suffix).copied() self.biome_map.get(suffix).copied()
} }
/// Resolves a Minecraft pre-1.18 numeric biome type ID
#[inline] #[inline]
pub fn get_legacy(&self, id: u8) -> Option<&Biome> { pub fn get_legacy(&self, id: u8) -> Option<&Biome> {
Some(self.legacy_biomes[id as usize]) Some(self.legacy_biomes[id as usize])

View file

@ -1,3 +1,5 @@
//! Common types used by MinedMap
use std::{ use std::{
fmt::Debug, fmt::Debug,
iter::FusedIterator, iter::FusedIterator,
@ -7,14 +9,20 @@ use std::{
use itertools::iproduct; use itertools::iproduct;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Const generic AXIS arguments for coordinate types
pub mod axis { pub mod axis {
/// The X axis
pub const X: u8 = 0; pub const X: u8 = 0;
/// The Y axis (height)
pub const Y: u8 = 1; pub const Y: u8 = 1;
/// The Z axis
pub const Z: u8 = 2; pub const Z: u8 = 2;
} }
/// Generates a generic coordinate type with a given range
macro_rules! coord_type { macro_rules! coord_type {
($t:ident, $max:expr) => { ($t:ident, $max:expr, $doc:expr $(,)?) => {
#[doc = $doc]
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct $t<const AXIS: u8>(pub u8); 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; pub const BLOCK_BITS: u8 = 4;
/// Number of blocks per chunk in each dimension
pub const BLOCKS_PER_CHUNK: usize = 1 << BLOCK_BITS; 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 /// A block X coordinate relative to a chunk
pub type BlockX = BlockCoord<{ axis::X }>; 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 /// X and Z coordinates of a block in a chunk
#[derive(Clone, Copy, PartialEq, Eq)] #[derive(Clone, Copy, PartialEq, Eq)]
pub struct LayerBlockCoords { pub struct LayerBlockCoords {
/// The X coordinate
pub x: BlockX, pub x: BlockX,
/// The Z coordinate
pub z: BlockZ, 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 /// X, Y and Z coordinates of a block in a chunk section
#[derive(Clone, Copy, PartialEq, Eq)] #[derive(Clone, Copy, PartialEq, Eq)]
pub struct SectionBlockCoords { pub struct SectionBlockCoords {
/// The X and Z coordinates
pub xz: LayerBlockCoords, pub xz: LayerBlockCoords,
/// The Y coordinate
pub y: BlockY, pub y: BlockY,
} }
@ -137,9 +155,15 @@ impl Debug for SectionBlockCoords {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct SectionY(pub i32); pub struct SectionY(pub i32);
/// Number of bits required to store a chunk coordinate
pub const CHUNK_BITS: u8 = 5; pub const CHUNK_BITS: u8 = 5;
/// Number of chunks per region in each dimension
pub const CHUNKS_PER_REGION: usize = 1 << CHUNK_BITS; 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 /// A chunk X coordinate relative to a region
pub type ChunkX = ChunkCoord<{ axis::X }>; 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 /// A pair of chunk coordinates relative to a region
#[derive(Clone, Copy, PartialEq, Eq)] #[derive(Clone, Copy, PartialEq, Eq)]
pub struct ChunkCoords { pub struct ChunkCoords {
/// The X coordinate
pub x: ChunkX, pub x: ChunkX,
/// The Z coordinate
pub z: ChunkZ, pub z: ChunkZ,
} }
@ -167,14 +193,17 @@ impl Debug for ChunkCoords {
pub struct ChunkArray<T>(pub [[T; CHUNKS_PER_REGION]; CHUNKS_PER_REGION]); pub struct ChunkArray<T>(pub [[T; CHUNKS_PER_REGION]; CHUNKS_PER_REGION]);
impl<T> ChunkArray<T> { impl<T> ChunkArray<T> {
/// Iterates over all possible chunk coordinate pairs used as [ChunkArray] keys
pub fn keys() -> impl Iterator<Item = ChunkCoords> + Clone + Debug { pub fn keys() -> impl Iterator<Item = ChunkCoords> + Clone + Debug {
iproduct!(ChunkZ::iter(), ChunkX::iter()).map(|(z, x)| ChunkCoords { x, z }) 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 { pub fn values(&self) -> impl Iterator<Item = &T> + Clone + Debug {
Self::keys().map(|k| &self[k]) 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 { pub fn iter(&self) -> impl Iterator<Item = (ChunkCoords, &T)> + Clone + Debug {
Self::keys().map(|k| (k, &self[k])) Self::keys().map(|k| (k, &self[k]))
} }

View file

@ -1,6 +1,10 @@
//! Utility functions and extension traits
use crate::types::*; use crate::types::*;
/// Extension trait for combined bit shift and mask
pub trait ShiftMask: Sized { pub trait ShiftMask: Sized {
/// Output type of shift operation
type MaskedOutput; type MaskedOutput;
/// Apply a right shift to a value, and return both the result and the /// 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] #[inline]
pub fn to_flat_coord<const AXIS: u8>( pub fn to_flat_coord<const AXIS: u8>(
region: i8, 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) (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] #[inline]
pub fn from_flat_coord<const AXIS: u8>(coord: i32) -> (i8, ChunkCoord<AXIS>, BlockCoord<AXIS>) { pub fn from_flat_coord<const AXIS: u8>(coord: i32) -> (i8, ChunkCoord<AXIS>, BlockCoord<AXIS>) {
let (region_chunk, block) = coord.shift_mask(BLOCK_BITS); 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::{ use std::{
collections::{btree_map, BTreeMap}, collections::{btree_map, BTreeMap},
iter::{self, FusedIterator}, iter::{self, FusedIterator},
@ -17,6 +22,7 @@ use crate::{
pub enum Chunk<'a> { pub enum Chunk<'a> {
/// Minecraft v1.18+ chunk with biome data moved into sections /// Minecraft v1.18+ chunk with biome data moved into sections
V1_18 { V1_18 {
/// Section data
section_map: BTreeMap<SectionY, (SectionV1_13<'a>, BiomesV1_18<'a>, BlockLight<'a>)>, section_map: BTreeMap<SectionY, (SectionV1_13<'a>, BiomesV1_18<'a>, BlockLight<'a>)>,
}, },
/// Minecraft v1.13+ chunk /// Minecraft v1.13+ chunk
@ -26,14 +32,18 @@ pub enum Chunk<'a> {
/// section), and a palette mapping these indices to namespaced /// section), and a palette mapping these indices to namespaced
/// block IDs /// block IDs
V1_13 { V1_13 {
/// Section data
section_map: BTreeMap<SectionY, (SectionV1_13<'a>, BlockLight<'a>)>, section_map: BTreeMap<SectionY, (SectionV1_13<'a>, BlockLight<'a>)>,
/// Biome data
biomes: BiomesV0<'a>, biomes: BiomesV0<'a>,
}, },
/// Original pre-1.13 chunk /// Original pre-1.13 chunk
/// ///
/// The original chunk format with fixed 8-bit numeric block IDs /// The original chunk format with fixed 8-bit numeric block IDs
V0 { V0 {
/// Section data
section_map: BTreeMap<SectionY, (SectionV0<'a>, BlockLight<'a>)>, section_map: BTreeMap<SectionY, (SectionV0<'a>, BlockLight<'a>)>,
/// Biome data
biomes: BiomesV0<'a>, biomes: BiomesV0<'a>,
}, },
/// Unpopulated chunk without any block data /// Unpopulated chunk without any block data
@ -45,16 +55,21 @@ pub enum Chunk<'a> {
enum SectionIterInner<'a> { enum SectionIterInner<'a> {
/// Iterator over sections of [Chunk::V1_18] /// Iterator over sections of [Chunk::V1_18]
V1_18 { V1_18 {
/// Inner iterator into section map
iter: btree_map::Iter<'a, SectionY, (SectionV1_13<'a>, BiomesV1_18<'a>, BlockLight<'a>)>, iter: btree_map::Iter<'a, SectionY, (SectionV1_13<'a>, BiomesV1_18<'a>, BlockLight<'a>)>,
}, },
/// Iterator over sections of [Chunk::V1_13] /// Iterator over sections of [Chunk::V1_13]
V1_13 { V1_13 {
/// Inner iterator into section map
iter: btree_map::Iter<'a, SectionY, (SectionV1_13<'a>, BlockLight<'a>)>, iter: btree_map::Iter<'a, SectionY, (SectionV1_13<'a>, BlockLight<'a>)>,
/// Chunk biome data
biomes: &'a BiomesV0<'a>, biomes: &'a BiomesV0<'a>,
}, },
/// Iterator over sections of [Chunk::V0] /// Iterator over sections of [Chunk::V0]
V0 { V0 {
/// Inner iterator into section map
iter: btree_map::Iter<'a, SectionY, (SectionV0<'a>, BlockLight<'a>)>, iter: btree_map::Iter<'a, SectionY, (SectionV0<'a>, BlockLight<'a>)>,
/// Chunk biome data
biomes: &'a BiomesV0<'a>, biomes: &'a BiomesV0<'a>,
}, },
/// Empty iterator over an unpopulated chunk ([Chunk::Empty]) /// Empty iterator over an unpopulated chunk ([Chunk::Empty])
@ -64,6 +79,7 @@ enum SectionIterInner<'a> {
/// Iterator over the sections of a [Chunk] /// Iterator over the sections of a [Chunk]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SectionIter<'a> { pub struct SectionIter<'a> {
/// Inner iterator enum
inner: SectionIterInner<'a>, 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 { pub fn is_empty(&self) -> bool {
match self { match self {
Chunk::V1_18 { section_map } => section_map.is_empty(), 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)] #[derive(Debug, Clone, Copy)]
pub struct SectionIterItem<'a> { pub struct SectionIterItem<'a> {
/// The Y coordinate of the section
pub y: SectionY, pub y: SectionY,
/// Section block data
pub section: &'a dyn Section, pub section: &'a dyn Section,
/// Section biome data
pub biomes: &'a dyn Biomes, pub biomes: &'a dyn Biomes,
/// Section block light data
pub block_light: BlockLight<'a>, pub block_light: BlockLight<'a>,
} }
/// Helper trait to specify section iterator trait bounds
trait SectionIterTrait<'a>: trait SectionIterTrait<'a>:
Iterator<Item = SectionIterItem<'a>> + DoubleEndedIterator + ExactSizeIterator + FusedIterator Iterator<Item = SectionIterItem<'a>> + DoubleEndedIterator + ExactSizeIterator + FusedIterator
{ {
@ -252,6 +275,7 @@ impl<'a, T> SectionIterTrait<'a> for T where
} }
impl<'a> SectionIter<'a> { impl<'a> SectionIter<'a> {
/// Helper to run a closure on the inner section iterator
fn with_iter<F, T>(&mut self, f: F) -> T fn with_iter<F, T>(&mut self, f: F) -> T
where where
F: FnOnce(&mut dyn SectionIterTrait<'a>) -> T, F: FnOnce(&mut dyn SectionIterTrait<'a>) -> T,

View file

@ -6,30 +6,39 @@ use serde::Deserialize;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")] #[serde(rename_all = "PascalCase")]
pub struct BlockStatePaletteEntry { pub struct BlockStatePaletteEntry {
/// Block type ID
pub name: String, pub name: String,
} }
/// 1.18+ `block_states` element found in a [section](SectionV1_18) /// 1.18+ `block_states` element found in a [section](SectionV1_18)
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct BlockStatesV1_18 { pub struct BlockStatesV1_18 {
/// Palette of block types, indexed by block data
pub palette: Vec<BlockStatePaletteEntry>, pub palette: Vec<BlockStatePaletteEntry>,
/// Block data
pub data: Option<fastnbt::LongArray>, pub data: Option<fastnbt::LongArray>,
} }
/// 1.18+ `biomes` element found in a [section](SectionV1_18) /// 1.18+ `biomes` element found in a [section](SectionV1_18)
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct BiomesV1_18 { pub struct BiomesV1_18 {
/// Palette of biome types, indexed by biome data
pub palette: Vec<String>, pub palette: Vec<String>,
/// Biome data
pub data: Option<fastnbt::LongArray>, pub data: Option<fastnbt::LongArray>,
} }
/// Element of the 1.18+ `sections` list found in a [Chunk] /// Element of the 1.18+ `sections` list found in a [Chunk]
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct SectionV1_18 { pub struct SectionV1_18 {
/// Y coordinate
#[serde(rename = "Y")] #[serde(rename = "Y")]
pub y: i32, pub y: i32,
/// Block type data
pub block_states: BlockStatesV1_18, pub block_states: BlockStatesV1_18,
/// Biome data
pub biomes: BiomesV1_18, pub biomes: BiomesV1_18,
/// Block light data
#[serde(rename = "BlockLight")] #[serde(rename = "BlockLight")]
pub block_light: Option<fastnbt::ByteArray>, pub block_light: Option<fastnbt::ByteArray>,
} }
@ -38,16 +47,23 @@ pub struct SectionV1_18 {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum SectionV0Variants { pub enum SectionV0Variants {
/// v1.13+ data
#[serde(rename_all = "PascalCase")] #[serde(rename_all = "PascalCase")]
V1_13 { V1_13 {
/// Block data
block_states: fastnbt::LongArray, block_states: fastnbt::LongArray,
/// Block type palette, indexed by block data
palette: Vec<BlockStatePaletteEntry>, palette: Vec<BlockStatePaletteEntry>,
}, },
/// Pre-1.13 data
#[serde(rename_all = "PascalCase")] #[serde(rename_all = "PascalCase")]
V0 { V0 {
/// Block type data
blocks: fastnbt::ByteArray, blocks: fastnbt::ByteArray,
/// Block damage / subtype data
data: fastnbt::ByteArray, data: fastnbt::ByteArray,
}, },
/// Empty section
Empty {}, Empty {},
} }
@ -55,8 +71,11 @@ pub enum SectionV0Variants {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")] #[serde(rename_all = "PascalCase")]
pub struct SectionV0 { pub struct SectionV0 {
/// Y coordinate
pub y: i8, pub y: i8,
/// Block light data
pub block_light: Option<fastnbt::ByteArray>, pub block_light: Option<fastnbt::ByteArray>,
/// Version-specific data
#[serde(flatten)] #[serde(flatten)]
pub section: SectionV0Variants, pub section: SectionV0Variants,
} }
@ -65,7 +84,9 @@ pub struct SectionV0 {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum BiomesV0 { pub enum BiomesV0 {
/// Data for Minecraft versions storing biome data as an IntArray
IntArray(fastnbt::IntArray), IntArray(fastnbt::IntArray),
/// Data for Minecraft versions storing biome data as an ByteArray
ByteArray(fastnbt::ByteArray), ByteArray(fastnbt::ByteArray),
} }
@ -73,8 +94,10 @@ pub enum BiomesV0 {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")] #[serde(rename_all = "PascalCase")]
pub struct LevelV0 { pub struct LevelV0 {
/// Section data
#[serde(default)] #[serde(default)]
pub sections: Vec<SectionV0>, pub sections: Vec<SectionV0>,
/// Biome data
pub biomes: Option<BiomesV0>, pub biomes: Option<BiomesV0>,
} }
@ -82,11 +105,15 @@ pub struct LevelV0 {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum ChunkVariants { pub enum ChunkVariants {
/// 1.18+ chunk data
V1_18 { V1_18 {
/// List of chunk sections
sections: Vec<SectionV1_18>, sections: Vec<SectionV1_18>,
}, },
/// Pre-1.18 chunk data
#[serde(rename_all = "PascalCase")] #[serde(rename_all = "PascalCase")]
V0 { V0 {
/// `Level` field of the chunk
level: LevelV0, level: LevelV0,
}, },
} }
@ -95,16 +122,20 @@ pub enum ChunkVariants {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")] #[serde(rename_all = "PascalCase")]
pub struct Chunk { pub struct Chunk {
/// The data version of the chunk
pub data_version: Option<u32>, pub data_version: Option<u32>,
/// Version-specific chunk data
#[serde(flatten)] #[serde(flatten)]
pub chunk: ChunkVariants, pub chunk: ChunkVariants,
} }
/// "Data" compound element of level.dat /// `Data` compound element of level.dat
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")] #[serde(rename_all = "PascalCase")]
pub struct LevelDatData { pub struct LevelDatData {
/// X coordinate of spawn point for new players
pub spawn_x: i32, pub spawn_x: i32,
/// Z coordinate of spawn point for new players
pub spawn_z: i32, pub spawn_z: i32,
} }
@ -112,5 +143,6 @@ pub struct LevelDatData {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")] #[serde(rename_all = "PascalCase")]
pub struct LevelDat { pub struct LevelDat {
/// The `Data` field
pub data: LevelDatData, pub data: LevelDatData,
} }

View file

@ -1,3 +1,5 @@
//! Functions to search the "top" layer of a chunk
use std::num::NonZeroU16; use std::num::NonZeroU16;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
@ -10,6 +12,7 @@ use crate::{
types::*, types::*,
}; };
/// Height (Y coordinate) of a block
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct BlockHeight(pub i32); 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>>; 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>>; pub type BiomeArray = LayerBlockArray<Option<NonZeroU16>>;
/// Array storing a block light value for each coordinate for a chunk
pub type BlockLightArray = LayerBlockArray<u8>; pub type BlockLightArray = LayerBlockArray<u8>;
/// Array optionally storing a depth value for each coordinate for a chunk
pub type DepthArray = LayerBlockArray<Option<BlockHeight>>; pub type DepthArray = LayerBlockArray<Option<BlockHeight>>;
/// References to LayerData entries for a single coordinate pair
struct LayerEntry<'a> { struct LayerEntry<'a> {
/// The block type of the referenced entry
block: &'a mut Option<BlockType>, block: &'a mut Option<BlockType>,
/// The biome type of the referenced entry
biome: &'a mut Option<NonZeroU16>, biome: &'a mut Option<NonZeroU16>,
/// The block light of the referenced entry
block_light: &'a mut u8, block_light: &'a mut u8,
/// The depth value of the referenced entry
depth: &'a mut Option<BlockHeight>, depth: &'a mut Option<BlockHeight>,
} }
impl<'a> LayerEntry<'a> { 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 { fn is_empty(&self) -> bool {
self.block.is_none() 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 { fn done(&self) -> bool {
self.depth.is_some() 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( fn fill(
&mut self, &mut self,
biome_list: &mut IndexSet<Biome>, 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)] #[derive(Debug, Default)]
pub struct LayerData { pub struct LayerData {
/// Block type data
pub blocks: Box<BlockArray>, pub blocks: Box<BlockArray>,
/// Biome data
pub biomes: Box<BiomeArray>, pub biomes: Box<BiomeArray>,
/// Block light data
pub block_light: Box<BlockLightArray>, pub block_light: Box<BlockLightArray>,
/// Depth data
pub depths: Box<DepthArray>, pub depths: Box<DepthArray>,
} }
impl LayerData { impl LayerData {
/// Builds a [LayerEntry] referencing the LayerData at a given coordinate pair
fn entry(&mut self, coords: LayerBlockCoords) -> LayerEntry { fn entry(&mut self, coords: LayerBlockCoords) -> LayerEntry {
LayerEntry { LayerEntry {
block: &mut self.blocks[coords], 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 /// block layer
/// ///
/// For each (X, Z) coordinate pair, the topmost opaque block is /// For each (X, Z) coordinate pair, the topmost opaque block is
/// determined as the block that should be visible on the rendered /// determined as the block that should be visible on the rendered
/// map. For water blocks, the height of the first non-water block /// 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>> { pub fn top_layer(biome_list: &mut IndexSet<Biome>, chunk: &Chunk) -> Result<Option<LayerData>> {
use BLOCKS_PER_CHUNK as N; use BLOCKS_PER_CHUNK as N;

View file

@ -1,3 +1,5 @@
//! Data structures describing Minecraft save data
pub mod chunk; pub mod chunk;
pub mod de; pub mod de;
pub mod layer; 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 std::fmt::Debug;
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
@ -9,6 +14,14 @@ use crate::{
types::*, 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 /// 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 /// 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] /// Trait for common functions of [SectionV1_13] and [SectionV0]
pub trait Section: Debug { pub trait Section: Debug {
/// Returns the [BlockType] at a coordinate tuple inside the section
fn block_at(&self, coords: SectionBlockCoords) -> Result<Option<BlockType>>; fn block_at(&self, coords: SectionBlockCoords) -> Result<Option<BlockType>>;
} }
/// Minecraft v1.13+ section block data /// Minecraft v1.13+ section block data
#[derive(Debug)] #[derive(Debug)]
pub struct SectionV1_13<'a> { pub struct SectionV1_13<'a> {
/// Packed block type data
block_states: Option<&'a [i64]>, block_states: Option<&'a [i64]>,
/// List of block types indexed by entries encoded in *block_states*
palette: Vec<Option<BlockType>>, palette: Vec<Option<BlockType>>,
/// Number of bits per block in *block_states*
bits: u8, 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, aligned_blocks: bool,
} }
impl<'a> SectionV1_13<'a> { impl<'a> SectionV1_13<'a> {
/// Constructs a new [SectionV1_13] from deserialized data structures /// 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( pub fn new(
data_version: u32, data_version: u32,
block_states: Option<&'a [i64]>, block_states: Option<&'a [i64]>,
@ -127,16 +151,21 @@ impl<'a> Section for SectionV1_13<'a> {
/// Pre-1.13 section block data /// Pre-1.13 section block data
#[derive(Debug)] #[derive(Debug)]
pub struct SectionV0<'a> { pub struct SectionV0<'a> {
/// Block type data
///
/// Each i8 entry corresponds to a block in the 16x16x16 section
blocks: &'a [i8], blocks: &'a [i8],
/// Block damage/subtype data
///
/// Uses 4 bits for each block in the 16x16x16 section
data: &'a [i8], data: &'a [i8],
/// Used to look up block type IDs
block_types: &'a BlockTypes, block_types: &'a BlockTypes,
} }
impl<'a> SectionV0<'a> { impl<'a> SectionV0<'a> {
/// Constructs a new [SectionV0] from deserialized data structures /// Constructs a new [SectionV0] from deserialized data structures
pub fn new(blocks: &'a [i8], data: &'a [i8], block_types: &'a BlockTypes) -> Result<Self> { 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 { if blocks.len() != N * N * N {
bail!("Invalid section block data"); bail!("Invalid section block data");
} }
@ -171,6 +200,7 @@ impl<'a> Section for SectionV0<'a> {
/// Trait for common functions of [BiomesV1_18] and [BiomesV0] /// Trait for common functions of [BiomesV1_18] and [BiomesV0]
pub trait Biomes: Debug { 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>>; fn biome_at(&self, section: SectionY, coords: SectionBlockCoords) -> Result<Option<&Biome>>;
} }
@ -181,13 +211,21 @@ pub trait Biomes: Debug {
/// v1.13+ block data. /// v1.13+ block data.
#[derive(Debug)] #[derive(Debug)]
pub struct BiomesV1_18<'a> { 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]>, biomes: Option<&'a [i64]>,
/// Biome palette indexed by entries encoded in *biomes*
palette: Vec<Option<&'a Biome>>, palette: Vec<Option<&'a Biome>>,
/// Number of bits used for each entry in *biomes*
bits: u8, bits: u8,
} }
impl<'a> BiomesV1_18<'a> { 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( pub fn new(
biomes: Option<&'a [i64]>, biomes: Option<&'a [i64]>,
palette: &'a [String], palette: &'a [String],
@ -223,9 +261,6 @@ impl<'a> BiomesV1_18<'a> {
/// Looks up the block type palette index at the given coordinates /// Looks up the block type palette index at the given coordinates
fn palette_index_at(&self, coords: SectionBlockCoords) -> usize { 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 { let Some(biomes) = self.biomes else {
return 0; return 0;
}; };
@ -262,28 +297,31 @@ impl<'a> Biomes for BiomesV1_18<'a> {
/// different pre-v1.18 Minecraft versions /// different pre-v1.18 Minecraft versions
#[derive(Debug)] #[derive(Debug)]
enum BiomesV0Data<'a> { 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), IntArrayV15(&'a fastnbt::IntArray),
/// Biome data stored as IntArray in some pre-1.15 versions
IntArrayV0(&'a fastnbt::IntArray), IntArrayV0(&'a fastnbt::IntArray),
/// Biome data stored as ByteArray in some pre-1.15 versions
ByteArray(&'a fastnbt::ByteArray), ByteArray(&'a fastnbt::ByteArray),
} }
/// Pre-v1.18 section biome data /// Pre-v1.18 section biome data
#[derive(Debug)] #[derive(Debug)]
pub struct BiomesV0<'a> { pub struct BiomesV0<'a> {
/// Biome data from save data
data: BiomesV0Data<'a>, data: BiomesV0Data<'a>,
/// Used to look up biome IDs
biome_types: &'a BiomeTypes, biome_types: &'a BiomeTypes,
} }
impl<'a> BiomesV0<'a> { impl<'a> BiomesV0<'a> {
/// Constructs a new [BiomesV0] from deserialized data structures /// Constructs a new [BiomesV0] from deserialized data structures
pub fn new(biomes: Option<&'a de::BiomesV0>, biome_types: &'a BiomeTypes) -> Result<Self> { 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 { 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) BiomesV0Data::IntArrayV15(data)
} }
Some(de::BiomesV0::IntArray(data)) if data.len() == N * N => { 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>> { fn biome_at(&self, section: SectionY, coords: SectionBlockCoords) -> Result<Option<&Biome>> {
let id = match self.data { let id = match self.data {
BiomesV0Data::IntArrayV15(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 LayerBlockCoords { x, z } = coords.xz;
let y = section let y = section
.0 .0
.checked_mul(BLOCKS_PER_CHUNK as i32) .checked_mul(BLOCKS_PER_CHUNK as i32)
.and_then(|y| y.checked_add_unsigned(coords.y.0.into())) .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; .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 offset = (y >> 2) * BN * BN + (z.0 >> 2) as usize * BN + (x.0 >> 2) as usize;
let id = data[offset] as u32; 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)] #[derive(Debug, Clone, Copy)]
pub struct BlockLight<'a>(Option<&'a [i8]>); pub struct BlockLight<'a>(Option<&'a [i8]>);
impl<'a> BlockLight<'a> { impl<'a> BlockLight<'a> {
/// Creates a new [BlockLight], checking validity
pub fn new(block_light: Option<&'a [i8]>) -> Result<Self> { pub fn new(block_light: Option<&'a [i8]>) -> Result<Self> {
use BLOCKS_PER_CHUNK as N;
if let Some(block_light) = block_light { if let Some(block_light) = block_light {
if block_light.len() != N * N * N / 2 { if block_light.len() != N * N * N / 2 {
bail!("Invalid section block light data"); bail!("Invalid section block light data");
@ -341,6 +376,7 @@ impl<'a> BlockLight<'a> {
Ok(BlockLight(block_light)) Ok(BlockLight(block_light))
} }
/// Returns the block light value at the given coordinates
pub fn block_light_at(&self, coords: SectionBlockCoords) -> u8 { pub fn block_light_at(&self, coords: SectionBlockCoords) -> u8 {
let Some(block_light) = self.0 else { let Some(block_light) = self.0 else {
return 0; return 0;