MinedMap/src/core/region_processor.rs
Matthias Schiffer e74e7be686
core: region_processor: ignore empty region files
Minecraft generates empty region files in some cases. Just ignore these
files instead of printing an error message for each.
2024-06-14 16:19:55 +02:00

425 lines
12 KiB
Rust

//! The [RegionProcessor] and related functions
use std::{ffi::OsStr, path::PathBuf, sync::mpsc, time::SystemTime};
use anyhow::{Context, Result};
use enum_map::{Enum, EnumMap};
use rayon::prelude::*;
use tracing::{debug, info, warn};
use super::common::*;
use crate::{
io::{fs, storage},
resource,
types::*,
world::{self, layer},
};
/// Parses a filename in the format r.X.Z.mca into the contained X and Z values
fn parse_region_filename(file_name: &OsStr) -> Option<TileCoords> {
let parts: Vec<_> = file_name.to_str()?.split('.').collect();
let &["r", x, z, "mca"] = parts.as_slice() else {
return None;
};
Some(TileCoords {
x: x.parse().ok()?,
z: z.parse().ok()?,
})
}
/// [RegionProcessor::process_region] return values
#[derive(Debug, Clone, Copy, PartialEq, Eq, Enum)]
enum RegionProcessorStatus {
/// Region was processed
Ok,
/// Region was processed, unknown blocks or biomes were encountered
OkWithUnknown,
/// Region was unchanged and skipped
Skipped,
/// Reading the region failed, previous processed data is reused
ErrorOk,
/// Reading the region failed, no previous data available
ErrorMissing,
}
/// Handles processing for a single region
struct SingleRegionProcessor<'a> {
/// Registry of known block types
block_types: &'a resource::BlockTypes,
/// Registry of known biome types
biome_types: &'a resource::BiomeTypes,
/// Coordinates of the region this instance is processing
coords: TileCoords,
/// Input region filename
input_path: PathBuf,
/// Processed region data output filename
output_path: PathBuf,
/// Lightmap output filename
lightmap_path: PathBuf,
/// Processed entity output filename
entities_path: PathBuf,
/// Timestamp of last modification of input file
input_timestamp: SystemTime,
/// Timestamp of last modification of processed region output file (if valid)
output_timestamp: Option<SystemTime>,
/// Timestamp of last modification of lightmap output file (if valid)
lightmap_timestamp: Option<SystemTime>,
/// Timestamp of last modification of entity list output file (if valid)
entities_timestamp: Option<SystemTime>,
/// True if processed region output file needs to be updated
output_needed: bool,
/// True if lightmap output file needs to be updated
lightmap_needed: bool,
/// True if entity output file needs to be updated
entities_needed: bool,
/// Processed region intermediate data
processed_region: ProcessedRegion,
/// Lightmap intermediate data
lightmap: image::GrayAlphaImage,
/// Processed entity intermediate data
entities: ProcessedEntities,
/// True if any unknown block or biome types were encountered during processing
has_unknown: bool,
}
impl<'a> SingleRegionProcessor<'a> {
/// Initializes a [SingleRegionProcessor]
fn new(processor: &'a RegionProcessor<'a>, coords: TileCoords) -> Result<Self> {
/// Width/height of the region data
const N: u32 = (BLOCKS_PER_CHUNK * CHUNKS_PER_REGION) as u32;
let input_path = processor.config.region_path(coords);
let input_timestamp = fs::modified_timestamp(&input_path)?;
let output_path = processor.config.processed_path(coords);
let output_timestamp = fs::read_timestamp(&output_path, REGION_FILE_META_VERSION);
let lightmap_path = processor.config.tile_path(TileKind::Lightmap, 0, coords);
let lightmap_timestamp = fs::read_timestamp(&lightmap_path, LIGHTMAP_FILE_META_VERSION);
let entities_path = processor.config.entities_path(0, coords);
let entities_timestamp = fs::read_timestamp(&entities_path, ENTITIES_FILE_META_VERSION);
let output_needed = Some(input_timestamp) > output_timestamp;
let lightmap_needed = Some(input_timestamp) > lightmap_timestamp;
let entities_needed = Some(input_timestamp) > entities_timestamp;
let processed_region = ProcessedRegion::default();
let lightmap = image::GrayAlphaImage::new(N, N);
let entities = ProcessedEntities::default();
Ok(SingleRegionProcessor {
block_types: &processor.block_types,
biome_types: &processor.biome_types,
coords,
input_path,
output_path,
lightmap_path,
entities_path,
input_timestamp,
output_timestamp,
lightmap_timestamp,
entities_timestamp,
output_needed,
lightmap_needed,
entities_needed,
processed_region,
lightmap,
entities,
has_unknown: false,
})
}
/// 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| {
let v: f32 = block_light[LayerBlockCoords {
x: BlockX::new(x),
z: BlockZ::new(z),
}]
.into();
image::LumaA([0, (192.0 * (1.0 - v / 15.0)) as u8])
})
}
/// Saves processed region data
///
/// The timestamp is the time of the last modification of the input region data.
fn save_region(&self) -> Result<()> {
if !self.output_needed {
return Ok(());
}
storage::write_file(
&self.output_path,
&self.processed_region,
storage::Format::Bincode,
REGION_FILE_META_VERSION,
self.input_timestamp,
)
}
/// Saves a lightmap tile
///
/// The timestamp is the time of the last modification of the input region data.
fn save_lightmap(&self) -> Result<()> {
if !self.lightmap_needed {
return Ok(());
}
fs::create_with_timestamp(
&self.lightmap_path,
LIGHTMAP_FILE_META_VERSION,
self.input_timestamp,
|file| {
self.lightmap
.write_to(file, image::ImageFormat::Png)
.context("Failed to save image")
},
)
}
/// Saves processed entity data
///
/// The timestamp is the time of the last modification of the input region data.
fn save_entities(&mut self) -> Result<()> {
if !self.entities_needed {
return Ok(());
}
self.entities.block_entities.sort_unstable();
storage::write_file(
&self.entities_path,
&self.entities,
storage::Format::Json,
ENTITIES_FILE_META_VERSION,
self.input_timestamp,
)
}
/// Processes a single chunk
fn process_chunk(&mut self, chunk_coords: ChunkCoords, data: world::de::Chunk) -> Result<()> {
let (chunk, has_unknown) =
world::chunk::Chunk::new(&data, self.block_types, self.biome_types)
.with_context(|| format!("Failed to decode chunk {:?}", chunk_coords))?;
self.has_unknown |= has_unknown;
if self.output_needed || self.lightmap_needed {
if let Some(layer::LayerData {
blocks,
biomes,
block_light,
depths,
}) = world::layer::top_layer(&mut self.processed_region.biome_list, &chunk)
.with_context(|| format!("Failed to process chunk {:?}", chunk_coords))?
{
if self.output_needed {
self.processed_region.chunks[chunk_coords] = Some(Box::new(ProcessedChunk {
blocks,
biomes,
depths,
}));
}
if self.lightmap_needed {
let chunk_lightmap = Self::render_chunk_lightmap(block_light);
overlay_chunk(&mut self.lightmap, &chunk_lightmap, chunk_coords);
}
}
}
if self.entities_needed {
let mut block_entities = chunk.block_entities().with_context(|| {
format!(
"Failed to process block entities for chunk {:?}",
chunk_coords,
)
})?;
self.entities.block_entities.append(&mut block_entities);
}
Ok(())
}
/// Processes the chunks of the region
fn process_chunks(&mut self) -> Result<()> {
crate::nbt::region::from_file(&self.input_path)?
.foreach_chunk(|chunk_coords, data| self.process_chunk(chunk_coords, data))
}
/// Processes the region
fn run(mut self) -> Result<RegionProcessorStatus> {
if !self.output_needed && !self.lightmap_needed && !self.entities_needed {
debug!(
"Skipping unchanged region r.{}.{}.mca",
self.coords.x, self.coords.z
);
return Ok(RegionProcessorStatus::Skipped);
}
debug!(
"Processing region r.{}.{}.mca",
self.coords.x, self.coords.z
);
if let Err(err) = self.process_chunks() {
if self.output_timestamp.is_some()
&& self.lightmap_timestamp.is_some()
&& self.entities_timestamp.is_some()
{
warn!(
"Failed to process region {:?}, using old data: {:?}",
self.coords, err
);
return Ok(RegionProcessorStatus::ErrorOk);
} else {
warn!(
"Failed to process region {:?}, no old data available: {:?}",
self.coords, err
);
return Ok(RegionProcessorStatus::ErrorMissing);
}
}
self.save_region()?;
self.save_lightmap()?;
self.save_entities()?;
Ok(if self.has_unknown {
RegionProcessorStatus::OkWithUnknown
} else {
RegionProcessorStatus::Ok
})
}
}
/// 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(),
biome_types: resource::BiomeTypes::default(),
config,
}
}
/// Generates a list of all regions of the input Minecraft save data
fn collect_regions(&self) -> Result<Vec<TileCoords>> {
Ok(self
.config
.region_dir
.read_dir()
.with_context(|| {
format!(
"Failed to read directory {}",
self.config.region_dir.display()
)
})?
.filter_map(|entry| entry.ok())
.filter(|entry| {
(|| {
// We are only interested in regular files
let file_type = entry.file_type().ok()?;
if !file_type.is_file() {
return None;
}
let metadata = entry.metadata().ok()?;
if metadata.len() == 0 {
return None;
}
Some(())
})()
.is_some()
})
.filter_map(|entry| parse_region_filename(&entry.file_name()))
.collect())
}
/// Processes a single region file
fn process_region(&self, coords: TileCoords) -> Result<RegionProcessorStatus> {
SingleRegionProcessor::new(self, coords)?.run()
}
/// Iterates over all region files of a Minecraft save directory
///
/// Returns a list of the coordinates of all processed regions
pub fn run(self) -> Result<Vec<TileCoords>> {
use RegionProcessorStatus as Status;
fs::create_dir_all(&self.config.processed_dir)?;
fs::create_dir_all(&self.config.tile_dir(TileKind::Lightmap, 0))?;
fs::create_dir_all(&self.config.entities_dir(0))?;
info!("Processing region files...");
let (region_send, region_recv) = mpsc::channel();
let (status_send, status_recv) = mpsc::channel();
self.collect_regions()?.par_iter().try_for_each(|&coords| {
let ret = self
.process_region(coords)
.with_context(|| format!("Failed to process region {:?}", coords))?;
if ret != Status::ErrorMissing {
region_send.send(coords).unwrap();
}
status_send.send(ret).unwrap();
anyhow::Ok(())
})?;
drop(region_send);
let mut regions: Vec<_> = region_recv.into_iter().collect();
drop(status_send);
let mut status = EnumMap::<_, usize>::default();
for ret in status_recv {
status[ret] += 1;
}
info!(
"Processed region files ({} processed, {} unchanged, {} errors)",
status[Status::Ok] + status[Status::OkWithUnknown],
status[Status::Skipped],
status[Status::ErrorOk] + status[Status::ErrorMissing],
);
if status[Status::OkWithUnknown] > 0 {
warn!("Unknown block or biome types found during processing");
eprint!(concat!(
"\n",
" If you're encountering this issue with an unmodified Minecraft version supported by MinedMap,\n",
" please file a bug report including the output with the --verbose flag.\n",
"\n",
));
}
// Sort regions in a zig-zag pattern to optimize cache usage
regions.sort_unstable_by_key(|&TileCoords { x, z }| (x, if x % 2 == 0 { z } else { -z }));
Ok(regions)
}
}