diff --git a/src/core/region_processor.rs b/src/core/region_processor.rs index 830b0f2..712436f 100644 --- a/src/core/region_processor.rs +++ b/src/core/region_processor.rs @@ -1,21 +1,22 @@ //! The [RegionProcessor] and related functions -use std::{ffi::OsStr, path::Path, sync::mpsc, time::SystemTime}; +use std::{ + ffi::OsStr, + path::{Path, PathBuf}, + sync::mpsc, + time::SystemTime, +}; use anyhow::{Context, Result}; -use indexmap::IndexSet; use rayon::prelude::*; use tracing::{debug, info, warn}; use super::common::*; use crate::{ io::{fs, storage}, - resource::{self, Biome}, + resource, types::*, - world::{ - self, - layer::{self, LayerData}, - }, + world::{self, layer}, }; /// Parses a filename in the format r.X.Z.mca into the contained X and Z values @@ -44,6 +45,182 @@ enum RegionProcessorStatus { 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, + /// Timestamp of last modification of input file + input_timestamp: SystemTime, + /// Timestamp of last modification of processed region output file (if valid) + output_timestamp: Option, + /// Timestamp of last modification of lightmap output file (if valid) + lightmap_timestamp: Option, + /// Processed region intermediate data + processed_region: ProcessedRegion, + /// Lightmap intermediate data + lightmap: image::GrayAlphaImage, +} + +impl<'a> SingleRegionProcessor<'a> { + /// Initializes a [SingleRegionProcessor] + fn new(processor: &'a RegionProcessor<'a>, coords: TileCoords) -> Result { + /// 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 processed_region = ProcessedRegion::default(); + let lightmap = image::GrayAlphaImage::new(N, N); + + Ok(SingleRegionProcessor { + block_types: &processor.block_types, + biome_types: &processor.biome_types, + coords, + input_path, + output_path, + lightmap_path, + input_timestamp, + output_timestamp, + lightmap_timestamp, + processed_region, + lightmap, + }) + } + + /// Renders a lightmap subtile from chunk block light data + fn render_chunk_lightmap( + block_light: Box, + ) -> 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( + path: &Path, + processed_region: &ProcessedRegion, + timestamp: SystemTime, + ) -> Result<()> { + storage::write(path, processed_region, 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, + timestamp: SystemTime, + ) -> Result<()> { + fs::create_with_timestamp(path, LIGHTMAP_FILE_META_VERSION, timestamp, |file| { + lightmap + .write_to(file, image::ImageFormat::Png) + .context("Failed to save image") + }) + } + + /// Processes the region + fn run(mut self) -> Result { + if Some(self.input_timestamp) <= self.output_timestamp + && Some(self.input_timestamp) <= self.lightmap_timestamp + { + 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) = (|| -> Result<()> { + crate::nbt::region::from_file(&self.input_path)?.foreach_chunk( + |chunk_coords, data: world::de::Chunk| { + let chunk = world::chunk::Chunk::new(&data, self.block_types, self.biome_types) + .with_context(|| format!("Failed to decode chunk {:?}", chunk_coords))?; + 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))? + else { + return Ok(()); + }; + self.processed_region.chunks[chunk_coords] = Some(Box::new(ProcessedChunk { + blocks, + biomes, + depths, + })); + + let chunk_lightmap = Self::render_chunk_lightmap(block_light); + overlay_chunk(&mut self.lightmap, &chunk_lightmap, chunk_coords); + + Ok(()) + }, + ) + })() { + if self.output_timestamp.is_some() && self.lightmap_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); + } + } + + if Some(self.input_timestamp) > self.output_timestamp { + Self::save_region( + &self.output_path, + &self.processed_region, + self.input_timestamp, + )?; + } + if Some(self.input_timestamp) > self.lightmap_timestamp { + Self::save_lightmap(&self.lightmap_path, &self.lightmap, self.input_timestamp)?; + } + + Ok(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 @@ -91,133 +268,9 @@ impl<'a> RegionProcessor<'a> { .collect()) } - /// Processes a single chunk - fn process_chunk( - &self, - biome_list: &mut IndexSet, - data: world::de::Chunk, - ) -> Result> { - let chunk = world::chunk::Chunk::new(&data, &self.block_types, &self.biome_types)?; - world::layer::top_layer(biome_list, &chunk) - } - - /// Renders a lightmap subtile from chunk block light data - fn render_chunk_lightmap( - block_light: Box, - ) -> 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( - path: &Path, - processed_region: &ProcessedRegion, - timestamp: SystemTime, - ) -> Result<()> { - storage::write(path, processed_region, 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, - timestamp: SystemTime, - ) -> Result<()> { - fs::create_with_timestamp(path, LIGHTMAP_FILE_META_VERSION, timestamp, |file| { - lightmap - .write_to(file, image::ImageFormat::Png) - .context("Failed to save image") - }) - } - /// 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(); - let mut lightmap = image::GrayAlphaImage::new(N, N); - - let input_path = self.config.region_path(coords); - let input_timestamp = fs::modified_timestamp(&input_path)?; - - let output_path = self.config.processed_path(coords); - let output_timestamp = fs::read_timestamp(&output_path, REGION_FILE_META_VERSION); - let lightmap_path = self.config.tile_path(TileKind::Lightmap, 0, coords); - let lightmap_timestamp = fs::read_timestamp(&lightmap_path, LIGHTMAP_FILE_META_VERSION); - - if Some(input_timestamp) <= output_timestamp && Some(input_timestamp) <= lightmap_timestamp - { - debug!("Skipping unchanged region r.{}.{}.mca", coords.x, coords.z); - return Ok(RegionProcessorStatus::Skipped); - } - - debug!("Processing region r.{}.{}.mca", coords.x, coords.z); - - if let Err(err) = (|| -> Result<()> { - crate::nbt::region::from_file(input_path)?.foreach_chunk( - |chunk_coords, data: world::de::Chunk| { - let Some(layer::LayerData { - blocks, - biomes, - block_light, - depths, - }) = self - .process_chunk(&mut processed_region.biome_list, data) - .with_context(|| format!("Failed to process chunk {:?}", chunk_coords))? - else { - return Ok(()); - }; - processed_region.chunks[chunk_coords] = Some(Box::new(ProcessedChunk { - blocks, - biomes, - depths, - })); - - let chunk_lightmap = Self::render_chunk_lightmap(block_light); - overlay_chunk(&mut lightmap, &chunk_lightmap, chunk_coords); - - Ok(()) - }, - ) - })() { - if output_timestamp.is_some() && lightmap_timestamp.is_some() { - warn!( - "Failed to process region {:?}, using old data: {:?}", - coords, err - ); - return Ok(RegionProcessorStatus::ErrorOk); - } else { - warn!( - "Failed to process region {:?}, no old data available: {:?}", - coords, err - ); - return Ok(RegionProcessorStatus::ErrorMissing); - } - } - - if Some(input_timestamp) > output_timestamp { - Self::save_region(&output_path, &processed_region, input_timestamp)?; - } - if Some(input_timestamp) > lightmap_timestamp { - Self::save_lightmap(&lightmap_path, &lightmap, input_timestamp)?; - } - - Ok(RegionProcessorStatus::Ok) + SingleRegionProcessor::new(self, coords)?.run() } /// Iterates over all region files of a Minecraft save directory