mirror of
https://github.com/neocturne/MinedMap.git
synced 2025-03-04 17:23:33 +01:00
Add documentation comments for all items
This commit is contained in:
parent
ba86dc8c06
commit
05a8056cbf
26 changed files with 576 additions and 42 deletions
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()?;
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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))?;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>,
|
||||
|
|
38
src/io/fs.rs
38
src/io/fs.rs
|
@ -1,3 +1,5 @@
|
|||
//! Helpers and common functions for filesystem access
|
||||
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::{BufReader, BufWriter, Read, Write},
|
||||
|
@ -8,15 +10,25 @@ use std::{
|
|||
use anyhow::{Context, Ok, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A file metadata version number
|
||||
///
|
||||
/// Deserialized metadata with non-current version number are considered invalid
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct FileMetaVersion(pub u32);
|
||||
|
||||
/// Metadata stored with generated files to track required incremental updates
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct FileMeta {
|
||||
/// Version of data described by the FileMeta
|
||||
version: FileMetaVersion,
|
||||
/// Timestamp stored with generated data
|
||||
///
|
||||
/// This timestamp is always the time of last modification of the inputs
|
||||
/// that were used to generate the file described by the FileMeta.
|
||||
timestamp: SystemTime,
|
||||
}
|
||||
|
||||
/// Helper for creating suffixed file paths
|
||||
fn suffix_name(path: &Path, suffix: &str) -> PathBuf {
|
||||
let mut file_name = path.file_name().unwrap_or_default().to_os_string();
|
||||
file_name.push(suffix);
|
||||
|
@ -26,24 +38,35 @@ fn suffix_name(path: &Path, suffix: &str) -> PathBuf {
|
|||
ret
|
||||
}
|
||||
|
||||
/// Derives the filename for temporary storage of data during generation
|
||||
fn tmpfile_name(path: &Path) -> PathBuf {
|
||||
suffix_name(path, ".tmp")
|
||||
}
|
||||
|
||||
/// Derives the filename for associated metadata for generated files
|
||||
fn metafile_name(path: &Path) -> PathBuf {
|
||||
suffix_name(path, ".meta")
|
||||
}
|
||||
|
||||
/// Creates a directory including all its parents
|
||||
///
|
||||
/// Wrapper around [fs::create_dir_all] that adds a more descriptive error message
|
||||
pub fn create_dir_all(path: &Path) -> Result<()> {
|
||||
fs::create_dir_all(path)
|
||||
.with_context(|| format!("Failed to create directory {}", path.display(),))
|
||||
}
|
||||
|
||||
/// Renames a file or directory
|
||||
///
|
||||
/// Wrapper around [fs::rename] that adds a more descriptive error message
|
||||
pub fn rename(from: &Path, to: &Path) -> Result<()> {
|
||||
fs::rename(from, to)
|
||||
.with_context(|| format!("Failed to rename {} to {}", from.display(), to.display()))
|
||||
}
|
||||
|
||||
/// Creates a new file
|
||||
///
|
||||
/// The contents of the file are defined by the passed function.
|
||||
pub fn create<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,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//! Input/output functions
|
||||
|
||||
pub mod data;
|
||||
pub mod fs;
|
||||
pub mod region;
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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::*;
|
||||
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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])
|
||||
|
|
35
src/types.rs
35
src/types.rs
|
@ -1,3 +1,5 @@
|
|||
//! Common types used by MinedMap
|
||||
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
iter::FusedIterator,
|
||||
|
@ -7,14 +9,20 @@ use std::{
|
|||
use itertools::iproduct;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Const generic AXIS arguments for coordinate types
|
||||
pub mod axis {
|
||||
/// The X axis
|
||||
pub const X: u8 = 0;
|
||||
/// The Y axis (height)
|
||||
pub const Y: u8 = 1;
|
||||
/// The Z axis
|
||||
pub const Z: u8 = 2;
|
||||
}
|
||||
|
||||
/// Generates a generic coordinate type with a given range
|
||||
macro_rules! coord_type {
|
||||
($t:ident, $max:expr) => {
|
||||
($t:ident, $max:expr, $doc:expr $(,)?) => {
|
||||
#[doc = $doc]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct $t<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]))
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//! Data structures describing Minecraft save data
|
||||
|
||||
pub mod chunk;
|
||||
pub mod de;
|
||||
pub mod layer;
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue