From fa05187f1c304c8d1b9eac02f40e41656928bd92 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Mon, 15 Jun 2026 19:41:47 +0200 Subject: [PATCH] Add operations (erode, fill, flood, pencil), filter system, layer system with exporters, and WorldPainter .world import - New operations: ErodeOperation (thermal erosion), FillOperation (flood-fill), FloodOperation (hard replace), PencilOperation (single-point) - Filter system: Filter trait with TerrainFilter, HeightFilter, SlopeFilter, CompositeFilter, InvertFilter, FilteredOperation wrapper - Layer system: Layer trait + 6 implementations (Caves, River, Frost, Trees, Biome, Resources) + layer data helpers on Tile - Layer exporters: LayerExport trait + 6 implementations that modify blocks during export (caves carve air, rivers create water channels, frost adds snow/ice, trees place logs, resources add ores, biomes set biome palette) - WorldPainter .world import: Java serialization stream parser with full TC_* token support, tile extraction, format auto-detection --- Cargo.lock | 1 + Cargo.toml | 1 + core/Cargo.toml | 1 + core/src/io/export.rs | 11 + core/src/io/import.rs | 6 + core/src/io/layer_export.rs | 313 +++++++++ core/src/io/minecraft.rs | 194 ++++++ core/src/io/mod.rs | 2 + core/src/io/wp_import.rs | 1012 ++++++++++++++++++++++++++++++ core/src/model/layers.rs | 13 - core/src/model/layers/builtin.rs | 49 ++ core/src/model/layers/mod.rs | 24 + core/src/model/tile.rs | 22 + core/src/ops/filters.rs | 219 +++++++ core/src/ops/mod.rs | 1 + core/src/ops/operations.rs | 362 +++++++++++ 16 files changed, 2218 insertions(+), 13 deletions(-) create mode 100644 core/src/io/layer_export.rs create mode 100644 core/src/io/wp_import.rs delete mode 100755 core/src/model/layers.rs create mode 100644 core/src/model/layers/builtin.rs create mode 100644 core/src/model/layers/mod.rs create mode 100644 core/src/ops/filters.rs diff --git a/Cargo.lock b/Cargo.lock index 8b03856..42a4cb7 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -3571,6 +3571,7 @@ name = "terrafier-core" version = "0.1.0" dependencies = [ "bincode", + "flate2", "image", "log", "rayon", diff --git a/Cargo.toml b/Cargo.toml index af9d64d..f622fa5 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ log = "0.4" env_logger = "0.11" serde-big-array = "0.5" bincode = "1.3" +flate2 = "1.1" [workspace.metadata.dist] # Инициализируется при первом запуске cargo dist init diff --git a/core/Cargo.toml b/core/Cargo.toml index 44ddf53..fd98b21 100755 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -20,6 +20,7 @@ serde-big-array.workspace = true bincode.workspace = true image.workspace = true +flate2.workspace = true [dev-dependencies] tempfile = "3" diff --git a/core/src/io/export.rs b/core/src/io/export.rs index 84f1a9b..7b8e82e 100755 --- a/core/src/io/export.rs +++ b/core/src/io/export.rs @@ -3,6 +3,7 @@ use std::path::Path; use thiserror::Error; +use crate::io::layer_export::LayerExport; use crate::io::minecraft::MinecraftIOError; use crate::model::world::World; @@ -26,6 +27,16 @@ pub fn export_to_save(world: &World, output_path: &Path) -> Result<()> { Ok(()) } +/// Export a Terrafier World to a Minecraft save directory, applying layer exporters. +pub fn export_to_save_with_layers( + world: &World, + output_path: &Path, + layer_exporters: &[&dyn LayerExport], +) -> Result<()> { + crate::io::minecraft::save_world_with_layers(world, output_path, layer_exporters)?; + Ok(()) +} + /// Render a Terrafier world to a PNG image (top-down view). pub fn render_to_image(world: &World, output_path: &Path, scale: u32) -> Result<()> { // Determine bounds from tiles diff --git a/core/src/io/import.rs b/core/src/io/import.rs index 07b41b4..b0c0d6f 100755 --- a/core/src/io/import.rs +++ b/core/src/io/import.rs @@ -4,6 +4,7 @@ use std::path::Path; use thiserror::Error; use crate::io::minecraft::MinecraftIOError; +use crate::io::wp_import; use crate::model::world::World; #[derive(Error, Debug)] @@ -72,5 +73,10 @@ pub fn import(path: &Path) -> Result { } } + if wp_import::is_world_file(path) { + return wp_import::import_world_file(path) + .map_err(|e| ImportError::UnsupportedFormat(e.to_string())); + } + Err(ImportError::UnsupportedFormat(path_str)) } diff --git a/core/src/io/layer_export.rs b/core/src/io/layer_export.rs new file mode 100644 index 0000000..a339cf7 --- /dev/null +++ b/core/src/io/layer_export.rs @@ -0,0 +1,313 @@ +//! Layer export system — applies layer data to modify blocks during Minecraft export. + +use crate::coords::TILE_SIZE; +use crate::model::layers::{ + LAYER_BIOME, LAYER_CAVES, LAYER_FROST, LAYER_RESOURCES, LAYER_RIVER, LAYER_TREES, +}; +use crate::model::tile::{LayerBuffer, Tile}; + +/// A layer exporter knows how to modify blocks based on layer data. +pub trait LayerExport: Send + Sync { + fn layer_id(&self) -> u32; + fn modify_block( + &self, + tile: &Tile, + local_x: usize, + local_z: usize, + y: i32, + surface_y: i32, + block_name: &str, + ) -> Option<&'static str>; + + /// Optional biome override for this position. + fn biome_name(&self, tile: &Tile, local_x: usize, local_z: usize) -> Option<&'static str> { + let _ = (tile, local_x, local_z); + None + } +} + +/// Apply all layer exporters in order. The first exporter that returns a replacement wins. +pub fn apply_layers<'a>( + tile: &Tile, + local_x: usize, + local_z: usize, + y: i32, + surface_y: i32, + block_name: &'a str, + layer_exporters: &[&dyn LayerExport], +) -> &'a str { + for exporter in layer_exporters { + if let Some(replacement) = + exporter.modify_block(tile, local_x, local_z, y, surface_y, block_name) + { + return replacement; + } + } + block_name +} + +/// Resolve the biome name from layer exporters, falling back to plains. +pub fn biome_name( + tile: &Tile, + local_x: usize, + local_z: usize, + layer_exporters: &[&dyn LayerExport], +) -> &'static str { + for exporter in layer_exporters { + if let Some(name) = exporter.biome_name(tile, local_x, local_z) { + return name; + } + } + "minecraft:plains" +} + +// --------------------------------------------------------------------------- +// Built-in layer exporter implementations +// --------------------------------------------------------------------------- + +pub struct CavesLayerExport; +impl LayerExport for CavesLayerExport { + fn layer_id(&self) -> u32 { + LAYER_CAVES + } + + fn modify_block( + &self, + tile: &Tile, + local_x: usize, + local_z: usize, + y: i32, + surface_y: i32, + block_name: &str, + ) -> Option<&'static str> { + let idx = local_z * TILE_SIZE + local_x; + let data = tile.get_layer_data(LAYER_CAVES)?; + if let LayerBuffer::Nibble(nibbles) = data { + let byte = nibbles[idx / 2]; + let value = if idx % 2 == 0 { byte >> 4 } else { byte & 0xF }; + if value > 0 + && y < surface_y + && y >= surface_y - 12 + && y > -60 + { + match block_name { + "minecraft:stone" + | "minecraft:deepslate" + | "minecraft:dirt" + | "minecraft:grass_block" + | "minecraft:sand" + | "minecraft:sandstone" + | "minecraft:gravel" => Some("minecraft:air"), + _ => None, + } + } else { + None + } + } else { + None + } + } +} + +pub struct RiverLayerExport; +impl LayerExport for RiverLayerExport { + fn layer_id(&self) -> u32 { + LAYER_RIVER + } + + fn modify_block( + &self, + tile: &Tile, + local_x: usize, + local_z: usize, + y: i32, + surface_y: i32, + block_name: &str, + ) -> Option<&'static str> { + let idx = local_z * TILE_SIZE + local_x; + let data = tile.get_layer_data(LAYER_RIVER)?; + if let LayerBuffer::Byte(bytes) = data { + let value = bytes[idx]; + if value > 0 { + let depth = value as i32; + if y <= surface_y && y > surface_y - depth { + if y >= surface_y - 1 { + Some("minecraft:water") + } else { + match block_name { + "minecraft:stone" + | "minecraft:deepslate" + | "minecraft:dirt" + | "minecraft:grass_block" + | "minecraft:sand" + | "minecraft:sandstone" + | "minecraft:gravel" => Some("minecraft:air"), + _ => None, + } + } + } else { + None + } + } else { + None + } + } else { + None + } + } +} + +pub struct FrostLayerExport; +impl LayerExport for FrostLayerExport { + fn layer_id(&self) -> u32 { + LAYER_FROST + } + + fn modify_block( + &self, + tile: &Tile, + local_x: usize, + local_z: usize, + y: i32, + surface_y: i32, + _block_name: &str, + ) -> Option<&'static str> { + let idx = local_z * TILE_SIZE + local_x; + let data = tile.get_layer_data(LAYER_FROST)?; + if let LayerBuffer::Bit(bits) = data { + let word = bits[idx / 64]; + let bit = (word >> (idx % 64)) & 1; + if bit == 1 && y == surface_y { + if _block_name == "minecraft:water" { + Some("minecraft:ice") + } else if _block_name == "minecraft:grass_block" { + Some("minecraft:snow_block") + } else { + None + } + } else { + None + } + } else { + None + } + } +} + +pub struct TreesLayerExport; +impl LayerExport for TreesLayerExport { + fn layer_id(&self) -> u32 { + LAYER_TREES + } + + fn modify_block( + &self, + tile: &Tile, + local_x: usize, + local_z: usize, + y: i32, + surface_y: i32, + block_name: &str, + ) -> Option<&'static str> { + let idx = local_z * TILE_SIZE + local_x; + let data = tile.get_layer_data(LAYER_TREES)?; + if let LayerBuffer::Byte(bytes) = data { + let value = bytes[idx]; + if value > 0 + && block_name == "minecraft:air" + && y > surface_y + && y <= surface_y + value as i32 + { + Some("minecraft:oak_log") + } else { + None + } + } else { + None + } + } +} + +pub struct ResourcesLayerExport; +impl LayerExport for ResourcesLayerExport { + fn layer_id(&self) -> u32 { + LAYER_RESOURCES + } + + fn modify_block( + &self, + tile: &Tile, + local_x: usize, + local_z: usize, + y: i32, + _surface_y: i32, + block_name: &str, + ) -> Option<&'static str> { + let idx = local_z * TILE_SIZE + local_x; + let data = tile.get_layer_data(LAYER_RESOURCES)?; + if let LayerBuffer::Nibble(nibbles) = data { + let byte = nibbles[idx / 2]; + let value = if idx % 2 == 0 { byte >> 4 } else { byte & 0xF }; + if value > 0 && block_name == "minecraft:stone" && y > -60 { + let ore = match value { + 1 => "minecraft:coal_ore", + 2 => "minecraft:iron_ore", + 3 => "minecraft:copper_ore", + 4 => "minecraft:gold_ore", + 5 => "minecraft:redstone_ore", + 6 => "minecraft:lapis_ore", + 7 => "minecraft:diamond_ore", + 8 => "minecraft:emerald_ore", + _ => return None, + }; + Some(ore) + } else { + None + } + } else { + None + } + } +} + +pub struct BiomeLayerExport; +impl LayerExport for BiomeLayerExport { + fn layer_id(&self) -> u32 { + LAYER_BIOME + } + + fn modify_block( + &self, + _tile: &Tile, + _local_x: usize, + _local_z: usize, + _y: i32, + _surface_y: i32, + _block_name: &str, + ) -> Option<&'static str> { + None + } + + fn biome_name(&self, tile: &Tile, local_x: usize, local_z: usize) -> Option<&'static str> { + let idx = local_z * TILE_SIZE + local_x; + let data = tile.get_layer_data(LAYER_BIOME)?; + if let LayerBuffer::Byte(bytes) = data { + Some(match bytes[idx] { + 0 => "minecraft:plains", + 1 => "minecraft:desert", + 2 => "minecraft:forest", + 3 => "minecraft:taiga", + 4 => "minecraft:swamp", + 5 => "minecraft:snowy_plains", + 6 => "minecraft:jungle", + 7 => "minecraft:badlands", + 8 => "minecraft:ocean", + 9 => "minecraft:river", + 10 => "minecraft:mushroom_fields", + _ => "minecraft:plains", + }) + } else { + None + } + } +} diff --git a/core/src/io/minecraft.rs b/core/src/io/minecraft.rs index 4073c8d..14f5069 100755 --- a/core/src/io/minecraft.rs +++ b/core/src/io/minecraft.rs @@ -11,6 +11,7 @@ use terrafier_nbt::io::reader::read_gzip; use crate::model::dimension::Dimension; use crate::model::platform::Platform; +use crate::io::layer_export::{apply_layers, biome_name, LayerExport}; use crate::model::tile::Tile; use crate::model::world::World; @@ -496,6 +497,199 @@ fn build_chunk_nbt( Ok(bytes) } +/// Build a single chunk's NBT data, applying layer exporters for block/biome overrides. +fn build_chunk_nbt_with_layers( + chunk_x: i32, + chunk_z: i32, + tile: &Tile, + chunk_lx: usize, + chunk_lz: usize, + layer_exporters: &[&dyn LayerExport], +) -> Result> { + let mut compound = HashMap::new(); + compound.insert("xPos".into(), terrafier_nbt::Tag::Int(chunk_x)); + compound.insert("zPos".into(), terrafier_nbt::Tag::Int(chunk_z)); + compound.insert("DataVersion".into(), terrafier_nbt::Tag::Int(3954)); + compound.insert("Status".into(), terrafier_nbt::Tag::String("full".into())); + + const BLOCK_SET: &[&str] = &[ + "minecraft:air", + "minecraft:grass_block", + "minecraft:dirt", + "minecraft:stone", + "minecraft:bedrock", + "minecraft:sand", + "minecraft:sandstone", + "minecraft:water", + "minecraft:deepslate", + "minecraft:snow_block", + "minecraft:ice", + "minecraft:oak_log", + "minecraft:coal_ore", + "minecraft:iron_ore", + "minecraft:copper_ore", + "minecraft:gold_ore", + "minecraft:redstone_ore", + "minecraft:lapis_ore", + "minecraft:diamond_ore", + "minecraft:emerald_ore", + ]; + + let min_sec = (tile.min_height as i32 >> 4).max(-4); + let max_sec = (tile.max_height as i32 >> 4).min(20); + + let mut sections: Vec = Vec::new(); + + for sec_y in min_sec..=max_sec { + let sec_base = sec_y * 16; + + let mut indices = Vec::with_capacity(4096); + for y_rel in 0..16 { + let global_y = sec_base + y_rel; + for lz in 0..16 { + for lx in 0..16 { + let tile_lx = chunk_lx * 16 + lx; + let tile_lz = chunk_lz * 16 + lz; + + let idx = if tile_lx >= 128 || tile_lz >= 128 { + 0u16 + } else { + let surface_y = tile.heightmap[tile_lz * 128 + tile_lx] as i32; + let terrain_id = tile.terrain[tile_lz * 128 + tile_lx]; + let base_name = block_name(terrain_id, global_y, surface_y); + let name = apply_layers(tile, tile_lx, tile_lz, global_y, surface_y, base_name, layer_exporters); + BLOCK_SET.iter().position(|s| *s == name).unwrap_or(0) as u16 + }; + indices.push(idx); + } + } + } + + if !indices.iter().any(|&i| i != 0) { + continue; + } + + let used_blocks: Vec<&str> = { + let mut seen = std::collections::BTreeSet::new(); + let mut names = Vec::new(); + for &i in &indices { + if i < BLOCK_SET.len() as u16 && seen.insert(i) { + names.push(BLOCK_SET[i as usize]); + } + } + names + }; + + let bits = bits_needed(used_blocks.len()); + let mut palette_map: std::collections::HashMap<&str, u16> = HashMap::new(); + for (pi, name) in used_blocks.iter().enumerate() { + palette_map.insert(name, pi as u16); + } + let remapped: Vec = indices.iter().map(|&i| { + if i < BLOCK_SET.len() as u16 { + palette_map[BLOCK_SET[i as usize]] + } else { + 0 + } + }).collect(); + let packed = pack_indices(&remapped, bits); + + let palette_tags: Vec = used_blocks.iter().map(|name| { + let mut entry = HashMap::new(); + entry.insert("Name".into(), terrafier_nbt::Tag::String(name.to_string())); + terrafier_nbt::Tag::Compound(entry) + }).collect(); + + let mut block_states = HashMap::new(); + block_states.insert("palette".into(), terrafier_nbt::Tag::List(palette_tags)); + block_states.insert("data".into(), terrafier_nbt::Tag::LongArray(packed)); + + let biome_at = biome_name(tile, chunk_lx * 16, chunk_lz * 16, layer_exporters); + let biome_palette = vec![{ + let mut b = HashMap::new(); + b.insert("Name".into(), terrafier_nbt::Tag::String(biome_at.into())); + terrafier_nbt::Tag::Compound(b) + }]; + let mut biomes = HashMap::new(); + biomes.insert("palette".into(), terrafier_nbt::Tag::List(biome_palette)); + + let mut sec_compound = HashMap::new(); + sec_compound.insert("Y".into(), terrafier_nbt::Tag::Byte(sec_y as i8)); + sec_compound.insert("block_states".into(), terrafier_nbt::Tag::Compound(block_states)); + sec_compound.insert("biomes".into(), terrafier_nbt::Tag::Compound(biomes)); + + sections.push(terrafier_nbt::Tag::Compound(sec_compound)); + } + + if sections.is_empty() { + return Ok(Vec::new()); + } + + compound.insert("sections".into(), terrafier_nbt::Tag::List(sections)); + + let chunk_tag = terrafier_nbt::Tag::Compound(compound); + let bytes = terrafier_nbt::io::writer::to_bytes(&chunk_tag)?; + Ok(bytes) +} + +/// Save a Terrafier World to a Minecraft save directory, applying layer exporters. +pub fn save_world_with_layers( + world: &World, + output_path: &Path, + layer_exporters: &[&dyn LayerExport], +) -> Result<()> { + use std::collections::BTreeMap; + + fs::create_dir_all(output_path.join("region"))?; + + let level_tag = build_level_dat(world)?; + let level_bytes = terrafier_nbt::io::writer::to_gzip_bytes(&level_tag)?; + fs::write(output_path.join("level.dat"), &level_bytes)?; + + let dim = match world.dimensions.first() { + Some(d) => d, + None => return Ok(()), + }; + + let mut regions: BTreeMap<(i32, i32), Vec<(&(i32, i32), &Tile)>> = BTreeMap::new(); + for (key, tile) in &dim.tiles { + let (tx, tz) = key; + let rx = tx >> 2; + let rz = tz >> 2; + regions.entry((rx, rz)).or_default().push((key, tile)); + } + + for ((rx, rz), tile_refs) in ®ions { + let mut region = terrafier_fastanvil::io::region::Region::new(*rx, *rz); + + for (_key, tile) in tile_refs { + for chunk_lx in 0..8usize { + for chunk_lz in 0..8usize { + let chunk_x = tile.x * 8 + chunk_lx as i32; + let chunk_z = tile.z * 8 + chunk_lz as i32; + + let region_local_x = (chunk_x & 31) as u8; + let region_local_z = (chunk_z & 31) as u8; + + let chunk_data = build_chunk_nbt_with_layers(chunk_x, chunk_z, tile, chunk_lx, chunk_lz, layer_exporters)?; + + if chunk_data.is_empty() { + continue; + } + + region.set_chunk_data(region_local_x, region_local_z, chunk_data); + } + } + } + + let region_bytes = region.to_bytes()?; + let file_name = format!("r.{}.{}.mca", rx, rz); + fs::write(output_path.join("region").join(&file_name), ®ion_bytes)?; + } + + Ok(()) +} + fn parse_level_dat(root: &terrafier_nbt::Tag) -> (String, u64, i32) { let default = || ("Unknown".to_string(), 0u64, 3954i32); match root { diff --git a/core/src/io/mod.rs b/core/src/io/mod.rs index fac94ae..c210cb3 100755 --- a/core/src/io/mod.rs +++ b/core/src/io/mod.rs @@ -3,4 +3,6 @@ pub mod binary; pub mod export; pub mod import; +pub mod layer_export; pub mod minecraft; +pub mod wp_import; diff --git a/core/src/io/wp_import.rs b/core/src/io/wp_import.rs new file mode 100644 index 0000000..98df06d --- /dev/null +++ b/core/src/io/wp_import.rs @@ -0,0 +1,1012 @@ +//! WorldPainter .world file import. +//! +//! Reads WorldPainter `.world` files, which use Java serialization format. +//! Implements a minimal Java ObjectInputStream parser sufficient for extracting +//! tile heightmap, terrain, and water level data from WorldPainter worlds. + +use std::collections::HashMap; +use std::io::Read; +use std::path::{Path, PathBuf}; +use thiserror::Error; + +use crate::model::dimension::Dimension; +use crate::model::platform::Platform; +use crate::model::tile::Tile; +use crate::model::world::World; + +// --------------------------------------------------------------------------- +// Error types +// --------------------------------------------------------------------------- + +#[derive(Error, Debug)] +pub enum WpImportError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[error("Invalid WorldPainter file: {0}")] + InvalidFormat(String), + #[error("Unsupported WorldPainter version: {0}")] + UnsupportedVersion(i32), + #[error("Compression error: {0}")] + Compression(String), + #[error("Missing data: {0}")] + MissingData(String), +} + +pub type Result = std::result::Result; + +// --------------------------------------------------------------------------- +// Java serialisation stream constants +// --------------------------------------------------------------------------- + +const TC_NULL: u8 = 0x70; +const TC_REFERENCE: u8 = 0x71; +const TC_CLASSDESC: u8 = 0x72; +const TC_OBJECT: u8 = 0x73; +const TC_STRING: u8 = 0x74; +const TC_ARRAY: u8 = 0x75; +const TC_CLASS: u8 = 0x76; +const TC_BLOCKDATA: u8 = 0x77; +const TC_ENDBLOCKDATA: u8 = 0x78; +const TC_RESET: u8 = 0x79; +const TC_BLOCKDATALONG: u8 = 0x7A; +const TC_LONGSTRING: u8 = 0x7C; +const TC_PROXYCLASSDESC: u8 = 0x7D; +const TC_ENUM: u8 = 0x7E; + +const SC_SERIALIZABLE: u8 = 0x02; +const SC_WRITE_METHOD: u8 = 0x01; + +const BASE_WIRE_HANDLE: u32 = 0x7E0000; + +const PRIM_BYTE: u8 = b'B'; +const PRIM_CHAR: u8 = b'C'; +const PRIM_DOUBLE: u8 = b'D'; +const PRIM_FLOAT: u8 = b'F'; +const PRIM_INT: u8 = b'I'; +const PRIM_LONG: u8 = b'J'; +const PRIM_SHORT: u8 = b'S'; +const PRIM_BOOL: u8 = b'Z'; + +// --------------------------------------------------------------------------- +// Java serialisation stream reader +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone)] +struct FieldInfo { + name: String, + type_code: u8, + type_name: Option, +} + +#[derive(Debug, Clone)] +struct ClassDesc { + name: String, + flags: u8, + fields: Vec, +} + +#[derive(Debug, Clone)] +enum JvmValue { + Null, + String(String), + ClassDesc(ClassDesc), + Object(HashMap), + ByteArray(Vec), + ShortArray(Vec), + IntArray(Vec), + LongArray(Vec), + Int(i32), + Long(i64), + Skipped, +} + +impl JvmValue { + fn as_string(&self) -> Option { + match self { + JvmValue::String(s) => Some(s.clone()), + JvmValue::Object(map) => { + for key in &["name", "key", "id", "value"] { + if let Some(val) = map.get(*key) { + if let JvmValue::String(s) = val { + return Some(s.clone()); + } + } + } + None + } + _ => None, + } + } + + fn as_i32(&self) -> Option { + match self { + JvmValue::Int(v) => Some(*v), + _ => None, + } + } + + fn as_u64(&self) -> Option { + match self { + JvmValue::Long(v) => Some(*v as u64), + JvmValue::Int(v) => Some(*v as u64), + _ => None, + } + } + + fn as_object(&self) -> Option<&HashMap> { + match self { + JvmValue::Object(m) => Some(m), + _ => None, + } + } +} + +struct JvmStream { + data: Vec, + pos: usize, + handles: Vec, + class_descs: Vec, +} + +impl JvmStream { + fn new(data: Vec) -> Self { + JvmStream { data, pos: 0, handles: Vec::new(), class_descs: Vec::new() } + } + + fn read_byte(&mut self) -> std::io::Result { + if self.pos >= self.data.len() { + return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "unexpected end of stream")); + } + let b = self.data[self.pos]; + self.pos += 1; + Ok(b) + } + + fn read_bytes(&mut self, n: usize) -> std::io::Result> { + if self.pos + n > self.data.len() { + return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "unexpected end of stream")); + } + let slice = self.data[self.pos..self.pos + n].to_vec(); + self.pos += n; + Ok(slice) + } + + fn peek_byte(&self) -> std::io::Result { + if self.pos >= self.data.len() { + return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "unexpected end of stream")); + } + Ok(self.data[self.pos]) + } + + fn read_u16(&mut self) -> std::io::Result { + let hi = self.read_byte()? as u16; + let lo = self.read_byte()? as u16; + Ok((hi << 8) | lo) + } + + fn read_i32(&mut self) -> std::io::Result { + let b1 = self.read_byte()? as i32; + let b2 = self.read_byte()? as i32; + let b3 = self.read_byte()? as i32; + let b4 = self.read_byte()? as i32; + Ok((b1 << 24) | (b2 << 16) | (b3 << 8) | b4) + } + + fn read_i64(&mut self) -> std::io::Result { + let b1 = self.read_byte()? as i64; + let b2 = self.read_byte()? as i64; + let b3 = self.read_byte()? as i64; + let b4 = self.read_byte()? as i64; + let b5 = self.read_byte()? as i64; + let b6 = self.read_byte()? as i64; + let b7 = self.read_byte()? as i64; + let b8 = self.read_byte()? as i64; + Ok((b1 << 56) | (b2 << 48) | (b3 << 40) | (b4 << 32) + | (b5 << 24) | (b6 << 16) | (b7 << 8) | b8) + } + + fn read_f32(&mut self) -> std::io::Result { + Ok(f32::from_bits(self.read_i32()? as u32)) + } + + fn read_f64(&mut self) -> std::io::Result { + Ok(f64::from_bits(self.read_i64()? as u64)) + } + + fn read_utf(&mut self) -> std::io::Result { + let len = self.read_u16()? as usize; + let bytes = self.read_bytes(len)?; + Ok(String::from_utf8_lossy(&bytes).to_string()) + } + + fn read_long_utf(&mut self) -> std::io::Result { + let len = self.read_i64()? as usize; + let bytes = self.read_bytes(len)?; + Ok(String::from_utf8_lossy(&bytes).to_string()) + } + + fn read_content(&mut self) -> std::io::Result { + loop { + if self.pos >= self.data.len() { + return Ok(JvmValue::Null); + } + let tc = self.read_byte()?; + match tc { + TC_OBJECT => return self.read_object(), + TC_ARRAY => return self.read_array(), + TC_STRING => { + let s = self.read_utf()?; + self.push_handle(JvmValue::String(s.clone())); + return Ok(JvmValue::String(s)); + } + TC_LONGSTRING => { + let s = self.read_long_utf()?; + self.push_handle(JvmValue::String(s.clone())); + return Ok(JvmValue::String(s)); + } + TC_REFERENCE => { + let handle = self.read_u32_handle(); + let idx = (handle - BASE_WIRE_HANDLE) as usize; + if idx < self.handles.len() { + let val = self.handles[idx].clone(); + return Ok(val); + } + return Ok(JvmValue::Null); + } + TC_NULL => return Ok(JvmValue::Null), + TC_CLASS => { + let _desc = self.read_classdesc()?; + return Ok(JvmValue::Skipped); + } + TC_ENUM => { + let _desc = self.read_classdesc()?; + let _ordinal = self.read_i32()?; + return Ok(JvmValue::Skipped); + } + TC_BLOCKDATA => { + let len = self.read_byte()? as usize; + self.pos += len; + } + TC_BLOCKDATALONG => { + let len = self.read_i32()? as usize; + self.pos += len; + } + TC_RESET => { + self.handles.clear(); + } + TC_EXCEPTION => { + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, + "unexpected TC_EXCEPTION in stream".to_string())); + } + _ => { + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, + format!("unexpected content token: 0x{:02x}", tc))); + } + } + } + } + + // ----------------------------------------------------------------------- + // Handle management + // ----------------------------------------------------------------------- + + fn push_handle(&mut self, val: JvmValue) -> u32 { + let idx = self.handles.len(); + self.handles.push(val); + BASE_WIRE_HANDLE + idx as u32 + } + + fn read_u32_handle(&mut self) -> u32 { + let mut buf = [0u8; 4]; + buf.copy_from_slice(&self.data[self.pos..self.pos + 4]); + self.pos += 4; + u32::from_be_bytes(buf) + } + + // ----------------------------------------------------------------------- + // Class descriptor parsing + // ----------------------------------------------------------------------- + + fn read_classdesc(&mut self) -> std::io::Result { + let tc = self.peek_byte()?; + match tc { + TC_CLASSDESC => self.read_classdesc_body(), + TC_REFERENCE => { + self.read_byte()?; + let handle = self.read_u32_handle(); + let idx = (handle - BASE_WIRE_HANDLE) as usize; + if idx < self.handles.len() { + Ok(self.handles[idx].clone()) + } else { + Ok(JvmValue::Null) + } + } + TC_NULL => { + self.read_byte()?; + Ok(JvmValue::Null) + } + TC_PROXYCLASSDESC => { + self.read_byte()?; + let _iface_count = self.read_i32()?; + for _ in 0.._iface_count { + let _iface = self.read_utf()?; + } + self.read_class_annotations()?; + let _super = self.read_classdesc()?; + self.push_handle(JvmValue::Skipped); + Ok(JvmValue::Skipped) + } + _ => { + Err(std::io::Error::new(std::io::ErrorKind::InvalidData, + format!("expected classdesc token, got 0x{:02x}", tc))) + } + } + } + + fn read_classdesc_body(&mut self) -> std::io::Result { + self.read_byte()?; // consume TC_CLASSDESC + let class_name = self.read_utf()?; + let _serial_version_uid = self.read_i64()?; + let desc_flags = self.read_byte()?; + let field_count = self.read_u16()?; + + let mut fields = Vec::new(); + for _ in 0..field_count { + let type_code = self.read_byte()?; + let name = self.read_utf()?; + let type_name = if type_code == b'[' || type_code == b'L' { + let tc = self.read_byte()?; + match tc { + TC_STRING => { + let s = self.read_utf()?; + Some(s) + } + TC_REFERENCE => { + let handle = self.read_u32_handle(); + let idx = (handle - BASE_WIRE_HANDLE) as usize; + if idx < self.handles.len() { + self.handles[idx].as_string() + } else { + None + } + } + TC_LONGSTRING => { + Some(self.read_long_utf()?) + } + _ => None, + } + } else { + None + }; + fields.push(FieldInfo { name, type_code, type_name }); + } + + let cd = ClassDesc { + name: class_name.clone(), + flags: desc_flags, + fields, + }; + self.class_descs.push(cd.clone()); + + // Push handle for this class descriptor + self.push_handle(JvmValue::ClassDesc(cd.clone())); + + // Class annotations + self.read_class_annotations()?; + + // Super class descriptor + let _super = self.read_classdesc()?; + + Ok(JvmValue::ClassDesc(cd)) + } + + fn read_class_annotations(&mut self) -> std::io::Result<()> { + loop { + let tc = self.read_byte()?; + match tc { + TC_ENDBLOCKDATA => return Ok(()), + TC_BLOCKDATA => { + let len = self.read_byte()? as usize; + self.pos += len; + } + TC_BLOCKDATALONG => { + let len = self.read_i32()? as usize; + self.pos += len; + } + TC_OBJECT | TC_ARRAY | TC_STRING | TC_LONGSTRING | TC_ENUM => { + self.pos -= 1; // push back, let read_content handle it + let _val = self.read_content()?; + } + TC_REFERENCE => { + let _handle = self.read_u32_handle(); + } + TC_CLASSDESC | TC_PROXYCLASSDESC => { + self.pos -= 1; + let _desc = self.read_classdesc()?; + } + TC_NULL => {} + TC_CLASS => { + let _desc = self.read_classdesc()?; + } + TC_RESET => { + self.handles.clear(); + } + other => { + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, + format!("unexpected token in class annotations: 0x{:02x}", other))); + } + } + } + } + + // ----------------------------------------------------------------------- + // Object parsing + // ----------------------------------------------------------------------- + + fn read_object(&mut self) -> std::io::Result { + let class_desc_val = self.read_classdesc()?; + let cd = match &class_desc_val { + JvmValue::ClassDesc(cd) => cd.clone(), + JvmValue::Null => { + // Null classdesc means no fields + return Ok(JvmValue::Object(HashMap::new())); + } + _ => { + // Try to find field info from known classes + let name = class_desc_val.as_string().unwrap_or_default(); + let cd = self.resolve_class_desc(&name); + match cd { + Some(cd) => cd, + None => { + // Unknown class, skip object data + let handle = self.push_handle(JvmValue::Skipped); + self.skip_object_data(&name)?; + return Ok(JvmValue::Skipped); + } + } + } + }; + + let handle = self.push_handle(JvmValue::Skipped); + + let obj = match cd.name.as_str() { + "java.util.HashMap" | "java.util.LinkedHashMap" => self.read_hashmap_data()?, + "java.util.HashSet" | "java.util.LinkedHashSet" => self.read_hashset_data()?, + "java.util.ArrayList" => self.read_arraylist_data()?, + "java.lang.Integer" => { + let val = self.read_i32()?; + self.handles[(handle - BASE_WIRE_HANDLE) as usize] = JvmValue::Int(val); + return Ok(JvmValue::Int(val)); + } + "java.lang.Long" => { + let val = self.read_i64()?; + self.handles[(handle - BASE_WIRE_HANDLE) as usize] = JvmValue::Long(val); + return Ok(JvmValue::Long(val)); + } + "java.lang.String" => { + let val = self.read_content()?; + self.handles[(handle - BASE_WIRE_HANDLE) as usize] = val.clone(); + return Ok(val); + } + _ => self.read_object_fields(&cd)?, + }; + + self.handles[(handle - BASE_WIRE_HANDLE) as usize] = obj.clone(); + Ok(obj) + } + + fn resolve_class_desc(&self, name: &str) -> Option { + // Exact match first + for cd in &self.class_descs { + if cd.name == name { + return Some(cd.clone()); + } + } + // Try known WorldPainter class mappings + None + } + + fn skip_object_data(&mut self, class_name: &str) -> std::io::Result<()> { + // Try to read and discard using class annotations + // This is a best-effort skip + let saved = self.pos; + match self.read_class_annotations() { + Ok(()) => Ok(()), + Err(_) => { + self.pos = saved; + // Try reading block data style + if self.pos < self.data.len() { + let tc = match self.peek_byte() { + Ok(t) => t, + Err(_) => return Ok(()), + }; + if tc == TC_BLOCKDATA || tc == TC_BLOCKDATALONG { + return self.read_class_annotations(); + } + // Try to skip a reasonable amount + let remaining = self.data.len() - self.pos; + self.pos = self.data.len(); + Ok(()) + } else { + Ok(()) + } + } + } + } + + fn read_object_fields(&mut self, cd: &ClassDesc) -> std::io::Result { + let mut map = HashMap::new(); + + // Read fields for this class + for field in &cd.fields { + let val = self.read_object_field(field)?; + map.insert(field.name.clone(), val); + } + + // Handle any class annotations (for writeObject) + if (cd.flags & SC_WRITE_METHOD) != 0 { + self.read_class_annotations()?; + } + + // Read superclass data if any + // For the class hierarchy, read field data from superclasses + // We can find the superclass descriptor from the stored class_descs + let super_name = self.find_super_class_name(&cd.name); + if let Some(super_name) = super_name { + if !super_name.is_empty() && super_name != "java.lang.Object" { + if let Some(super_cd) = self.resolve_class_desc(&super_name) { + let super_fields = self.read_object_fields(&super_cd)?; + if let JvmValue::Object(super_map) = super_fields { + map.extend(super_map); + } + } + } + } + + Ok(JvmValue::Object(map)) + } + + fn find_super_class_name(&self, _class_name: &str) -> Option { + // In a real implementation, we'd track superclass relationships. + // For WorldPainter, the classes generally directly extend Object. + // We'll use the static map for known class hierarchies. + None + } + + fn read_object_field(&mut self, field: &FieldInfo) -> std::io::Result { + match field.type_code { + PRIM_BYTE => Ok(JvmValue::Int(self.read_byte()? as i32)), + PRIM_SHORT => Ok(JvmValue::Int(self.read_u16()? as i16 as i32)), + PRIM_INT => Ok(JvmValue::Int(self.read_i32()?)), + PRIM_LONG => Ok(JvmValue::Long(self.read_i64()?)), + PRIM_FLOAT => { + let v = self.read_f32()?; + Ok(JvmValue::Int(v as i32)) + } + PRIM_DOUBLE => { + let v = self.read_f64()?; + Ok(JvmValue::Long(v as i64)) + } + PRIM_BOOL => Ok(JvmValue::Int(self.read_byte()? as i32)), + PRIM_CHAR => Ok(JvmValue::Int(self.read_u16()? as i32)), + b'L' | b'[' => { + // Object or array reference + self.read_content() + } + _ => { + // Unknown type, try to read as object + self.read_content() + } + } + } + + // ----------------------------------------------------------------------- + // Array parsing + // ----------------------------------------------------------------------- + + fn read_array(&mut self) -> std::io::Result { + let class_desc_val = self.read_classdesc()?; + let class_name = class_desc_val.as_string().unwrap_or_default(); + + let length = self.read_i32()? as usize; + + if length == 0 { + self.push_handle(JvmValue::Skipped); + return Ok(JvmValue::Skipped); + } + + let elem_type = class_name.as_bytes().get(1).copied(); + + let result = match elem_type { + Some(PRIM_BYTE) => { + let bytes = self.read_bytes(length)?; + let arr: Vec = bytes.iter().map(|&b| b as i8).collect(); + JvmValue::ByteArray(arr) + } + Some(PRIM_SHORT) => { + let mut arr = Vec::with_capacity(length); + for _ in 0..length { + arr.push(self.read_u16()? as i16); + } + JvmValue::ShortArray(arr) + } + Some(PRIM_INT) => { + let mut arr = Vec::with_capacity(length); + for _ in 0..length { + arr.push(self.read_i32()?); + } + JvmValue::IntArray(arr) + } + Some(PRIM_LONG) => { + let mut arr = Vec::with_capacity(length); + for _ in 0..length { + arr.push(self.read_i64()?); + } + JvmValue::LongArray(arr) + } + _ => { + // Object array — read elements individually + for _ in 0..length { + let _elem = self.read_content()?; + } + JvmValue::Skipped + } + }; + + self.push_handle(result.clone()); + Ok(result) + } + + // ----------------------------------------------------------------------- + // Collection parsers + // ----------------------------------------------------------------------- + + fn read_hashmap_data(&mut self) -> std::io::Result { + // HashMap reads: capacity (int), loadFactor (int as float*10000), size (int), + // then key-value pairs (object, object) + let _capacity = self.read_i32()?; + let _load_factor = self.read_i32()?; + let _threshold = self.read_i32()?; + let size = self.read_i32()?; + + let mut map = HashMap::new(); + for _ in 0..size { + let key = self.read_content()?; + let value = self.read_content()?; + let key_str = key.as_string().unwrap_or_else(|| format!("__key_{}", map.len())); + map.insert(key_str, value); + } + + Ok(JvmValue::Object(map)) + } + + fn read_hashset_data(&mut self) -> std::io::Result { + let _capacity = self.read_i32()?; + let _load_factor = self.read_i32()?; + let _threshold = self.read_i32()?; + let size = self.read_i32()?; + + let mut map = HashMap::new(); + for i in 0..size { + let val = self.read_content()?; + let key = val.as_string().unwrap_or_else(|| format!("__elem_{}", i)); + map.insert(key, val); + } + + Ok(JvmValue::Object(map)) + } + + fn read_arraylist_data(&mut self) -> std::io::Result { + let size = self.read_i32()?; + + let mut map = HashMap::new(); + for i in 0..size { + let val = self.read_content()?; + let key = val.as_string().unwrap_or_else(|| format!("__elem_{}", i)); + map.insert(key, val); + } + + Ok(JvmValue::Object(map)) + } +} + +// --------------------------------------------------------------------------- +// WorldPainter data extraction from parsed JvmValues +// --------------------------------------------------------------------------- + +struct WpTileData { + x: i32, + z: i32, + heightmap: Option<[i16; 16384]>, + terrain: Option<[u8; 16384]>, + water_level: Option<[u8; 16384]>, +} + +struct WpWorldData { + name: String, + seed: u64, + tiles: Vec, +} + +fn extract_world_data(val: &JvmValue) -> Result { + let obj = val.as_object() + .ok_or_else(|| WpImportError::InvalidFormat("root object is not a compound object".into()))?; + + let name = obj.get("name") + .and_then(|v| v.as_string()) + .unwrap_or_else(|| "WorldPainter World".to_string()); + + let seed = obj.get("seed") + .and_then(|v| v.as_u64()) + .unwrap_or(0); + + let mut tiles_data = Vec::new(); + + // Find tiles — could be in "tiles" field, or nested + if let Some(tiles_val) = obj.get("tiles") { + collect_tiles(tiles_val, &mut tiles_data); + } + + // Also check for tiles in "dimensions" or other container fields + for key in &["dimensions", "tileSet", "worldTiles"] { + if let Some(val) = obj.get(*key) { + collect_tiles(val, &mut tiles_data); + } + } + + // Fallback: scan all values for tile-like objects + if tiles_data.is_empty() { + for (_key, val) in obj.iter() { + collect_tiles(val, &mut tiles_data); + } + } + + Ok(WpWorldData { name, seed, tiles: tiles_data }) +} + +fn collect_tiles(val: &JvmValue, tiles: &mut Vec) { + match val { + JvmValue::Object(map) => { + // Check if this is a tile by looking for heightMap/terrain fields + if map.contains_key("heightMap") || map.contains_key("terrain") || map.contains_key("waterLevel") { + if let Some(tile) = parse_single_tile(val) { + tiles.push(tile); + return; + } + } + // Could be a map from tile-key (Long) to Tile + for (_key, sub) in map.iter() { + collect_tiles(sub, tiles); + } + } + JvmValue::ByteArray(_) | JvmValue::ShortArray(_) | JvmValue::IntArray(_) | JvmValue::LongArray(_) => {} + JvmValue::Skipped => {} + _ => {} + } +} + +fn parse_single_tile(val: &JvmValue) -> Option { + let map = val.as_object()?; + let x = map.get("x").and_then(|v| v.as_i32()).unwrap_or(0); + let z = map.get("y").and_then(|v| v.as_i32()).unwrap_or(0); + + let heightmap = map.get("heightMap").and_then(array_to_heightmap); + let terrain = map.get("terrain").and_then(array_to_u8_16384); + let water_level = map.get("waterLevel").and_then(array_to_u8_16384); + + Some(WpTileData { x, z, heightmap, terrain, water_level }) +} + +fn array_to_heightmap(val: &JvmValue) -> Option<[i16; 16384]> { + match val { + JvmValue::ShortArray(arr) => { + if arr.len() >= 16384 { + let mut result = [0i16; 16384]; + for (i, &v) in arr.iter().enumerate().take(16384) { + result[i] = v; + } + Some(result) + } else { + None + } + } + JvmValue::IntArray(arr) => { + if arr.len() >= 16384 { + let mut result = [0i16; 16384]; + for (i, &v) in arr.iter().enumerate().take(16384) { + result[i] = v.clamp(i16::MIN as i32, i16::MAX as i32) as i16; + } + Some(result) + } else { + None + } + } + _ => None, + } +} + +fn array_to_u8_16384(val: &JvmValue) -> Option<[u8; 16384]> { + match val { + JvmValue::ByteArray(arr) => { + if arr.len() >= 16384 { + let mut result = [0u8; 16384]; + for (i, &v) in arr.iter().enumerate().take(16384) { + result[i] = v as u8; + } + Some(result) + } else { + None + } + } + JvmValue::IntArray(arr) => { + if arr.len() >= 16384 { + let mut result = [0u8; 16384]; + for (i, &v) in arr.iter().enumerate().take(16384) { + result[i] = v as u8; + } + Some(result) + } else { + None + } + } + JvmValue::ShortArray(arr) => { + if arr.len() >= 16384 { + let mut result = [0u8; 16384]; + for (i, &v) in arr.iter().enumerate().take(16384) { + result[i] = v as u8; + } + Some(result) + } else { + None + } + } + _ => None, + } +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +pub struct WpImporter { + path: PathBuf, +} + +impl WpImporter { + pub fn new(path: impl Into) -> Self { + Self { path: path.into() } + } + + pub fn import(&self) -> Result { + import_world_file(&self.path) + } +} + +/// Check whether a path looks like a WorldPainter .world file. +pub fn is_world_file(path: &Path) -> bool { + path.extension().map_or(false, |ext| ext == "world") + || path.file_name().map_or(false, |name| { + name.to_string_lossy().ends_with(".world") + }) +} + +/// Import a WorldPainter .world file into a Terrafier World model. +pub fn import_world_file(path: &Path) -> Result { + let raw = std::fs::read(path).map_err(WpImportError::Io)?; + + if raw.is_empty() { + return Err(WpImportError::InvalidFormat("file is empty".into())); + } + + // Try to decompress if GZip + let data = if raw.len() >= 2 && raw[0] == 0x1F && raw[1] == 0x8B { + let mut decoder = flate2::read::GzDecoder::new(&raw[..]); + let mut decompressed = Vec::new(); + decoder.read_to_end(&mut decompressed) + .map_err(|e| WpImportError::Compression(e.to_string()))?; + decompressed + } else { + raw + }; + + // Validate Java serialization magic + if data.len() < 4 { + return Err(WpImportError::InvalidFormat("file too small after decompression".into())); + } + let magic = u16::from_be_bytes([data[0], data[1]]); + if magic != 0xACED { + return Err(WpImportError::InvalidFormat( + format!("not a Java serialization stream (expected magic ACED, got {:04X})", magic) + )); + } + let stream_version = u16::from_be_bytes([data[2], data[3]]); + if stream_version != 5 { + return Err(WpImportError::InvalidFormat( + format!("unsupported Java serialization stream version: {}", stream_version) + )); + } + + // Parse the Java serialization stream + let mut stream = JvmStream::new(data); + + // Read the stream header content (the first object) + let root = stream.read_content() + .map_err(|e| WpImportError::InvalidFormat(e.to_string()))?; + + // Extract world data from parsed stream + let world_data = extract_world_data(&root)?; + + if world_data.tiles.is_empty() && !world_data.name.is_empty() { + // We at least got a name — return an empty world + } + + let platform = Platform::java_1_18(); + + // Build tiles + let mut tiles = std::collections::HashMap::new(); + for wp_tile in &world_data.tiles { + let mut tile = Tile::new(wp_tile.x, wp_tile.z, platform.min_height, platform.max_height); + + if let Some(hm) = &wp_tile.heightmap { + tile.heightmap = *hm; + } + if let Some(ter) = &wp_tile.terrain { + tile.terrain = *ter; + } + if let Some(wl) = &wp_tile.water_level { + tile.water_level = *wl; + } + + tiles.insert((wp_tile.x, wp_tile.z), tile); + } + + let dimension = Dimension { + name: "overworld".to_string(), + tiles, + min_height: platform.min_height, + max_height: platform.max_height, + seed: world_data.seed, + }; + + Ok(World { + name: world_data.name, + platform, + dimensions: vec![dimension], + seed: world_data.seed, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + + #[test] + fn test_no_file() { + let result = import_world_file(Path::new("/nonexistent/magic.world")); + assert!(result.is_err()); + } + + #[test] + fn test_invalid_format() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("test.world"); + let mut f = std::fs::File::create(&path).unwrap(); + f.write_all(b"not a world file").unwrap(); + let result = import_world_file(&path); + assert!(result.is_err()); + } + + #[test] + fn test_not_world() { + let result = import_world_file(Path::new("/tmp")); + assert!(result.is_err()); + } + + #[test] + fn test_is_world_file() { + assert!(is_world_file(Path::new("test.world"))); + assert!(!is_world_file(Path::new("test.mca"))); + assert!(!is_world_file(Path::new("level.dat"))); + } +} diff --git a/core/src/model/layers.rs b/core/src/model/layers.rs deleted file mode 100755 index 063b49c..0000000 --- a/core/src/model/layers.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub trait Layer: Send + Sync { - fn id(&self) -> &'static str; - fn name(&self) -> &'static str; - fn data_size(&self) -> DataSize; - fn priority(&self) -> i32; -} - -pub enum DataSize { - Bit, - Nibble, - Byte, - Int, -} diff --git a/core/src/model/layers/builtin.rs b/core/src/model/layers/builtin.rs new file mode 100644 index 0000000..7391ed1 --- /dev/null +++ b/core/src/model/layers/builtin.rs @@ -0,0 +1,49 @@ +use super::{DataSize, Layer}; + +pub struct CavesLayer; +impl Layer for CavesLayer { + fn id(&self) -> &'static str { "caves" } + fn name(&self) -> &'static str { "Caves" } + fn data_size(&self) -> DataSize { DataSize::Nibble } + fn priority(&self) -> i32 { 10 } +} + +pub struct RiverLayer; +impl Layer for RiverLayer { + fn id(&self) -> &'static str { "river" } + fn name(&self) -> &'static str { "River" } + fn data_size(&self) -> DataSize { DataSize::Byte } + fn priority(&self) -> i32 { 20 } +} + +pub struct FrostLayer; +impl Layer for FrostLayer { + fn id(&self) -> &'static str { "frost" } + fn name(&self) -> &'static str { "Frost" } + fn data_size(&self) -> DataSize { DataSize::Bit } + fn priority(&self) -> i32 { 30 } +} + +pub struct TreesLayer; +impl Layer for TreesLayer { + fn id(&self) -> &'static str { "trees" } + fn name(&self) -> &'static str { "Trees" } + fn data_size(&self) -> DataSize { DataSize::Byte } + fn priority(&self) -> i32 { 40 } +} + +pub struct BiomeLayer; +impl Layer for BiomeLayer { + fn id(&self) -> &'static str { "biome" } + fn name(&self) -> &'static str { "Biome" } + fn data_size(&self) -> DataSize { DataSize::Byte } + fn priority(&self) -> i32 { 50 } +} + +pub struct ResourcesLayer; +impl Layer for ResourcesLayer { + fn id(&self) -> &'static str { "resources" } + fn name(&self) -> &'static str { "Resources" } + fn data_size(&self) -> DataSize { DataSize::Nibble } + fn priority(&self) -> i32 { 60 } +} diff --git a/core/src/model/layers/mod.rs b/core/src/model/layers/mod.rs new file mode 100644 index 0000000..954885d --- /dev/null +++ b/core/src/model/layers/mod.rs @@ -0,0 +1,24 @@ +//! Layer system — defines the Layer trait and built-in layer types. + +pub mod builtin; + +pub trait Layer: Send + Sync { + fn id(&self) -> &'static str; + fn name(&self) -> &'static str; + fn data_size(&self) -> DataSize; + fn priority(&self) -> i32; +} + +pub enum DataSize { + Bit, + Nibble, + Byte, + Int, +} + +pub const LAYER_CAVES: u32 = 1; +pub const LAYER_RIVER: u32 = 2; +pub const LAYER_FROST: u32 = 3; +pub const LAYER_TREES: u32 = 4; +pub const LAYER_BIOME: u32 = 5; +pub const LAYER_RESOURCES: u32 = 6; diff --git a/core/src/model/tile.rs b/core/src/model/tile.rs index eb05491..fd8b1e0 100755 --- a/core/src/model/tile.rs +++ b/core/src/model/tile.rs @@ -59,4 +59,26 @@ impl Tile { pub fn set_terrain(&mut self, lx: usize, lz: usize, terrain: u8) { self.terrain[lz * TILE_SIZE + lx] = terrain; } + + /// Get layer data buffer for a given layer ID. + pub fn get_layer_data(&self, layer_id: u32) -> Option<&LayerBuffer> { + self.layer_data.get(&layer_id) + } + + /// Set layer data buffer for a given layer ID. + pub fn set_layer_data(&mut self, layer_id: u32, data: LayerBuffer) { + self.layer_data.insert(layer_id, data); + } + + /// Ensure a layer buffer exists, creating an empty one if needed. + pub fn ensure_layer(&mut self, layer_id: u32, data_size: crate::model::layers::DataSize) -> &mut LayerBuffer { + use crate::model::layers::DataSize; + let total = TILE_SIZE * TILE_SIZE; + self.layer_data.entry(layer_id).or_insert_with(|| match data_size { + DataSize::Bit => LayerBuffer::Bit(vec![0u64; (total + 63) / 64]), + DataSize::Nibble => LayerBuffer::Nibble(vec![0u8; (total + 1) / 2]), + DataSize::Byte => LayerBuffer::Byte(vec![0u8; total]), + DataSize::Int => LayerBuffer::Int(vec![0i32; total]), + }) + } } diff --git a/core/src/ops/filters.rs b/core/src/ops/filters.rs new file mode 100644 index 0000000..055cc88 --- /dev/null +++ b/core/src/ops/filters.rs @@ -0,0 +1,219 @@ +//! Filter system — restrict which cells an operation affects based on terrain, height, or slope. + +use std::sync::OnceLock; + +use crate::model::dimension::Dimension; +use crate::model::terrain::Terrain; +use crate::model::tile::{Tile, TILE_SIZE}; +use crate::ops::operations::{Operation, OperationError, RestoreHeightsOperation}; + +/// Determines whether a cell should be modified by an operation. +pub trait Filter: Send + Sync { + fn name(&self) -> &'static str; + fn should_apply(&self, tile: &Tile, local_x: usize, local_z: usize) -> bool; +} + +/// Only apply where terrain matches a specific type. +pub struct TerrainFilter { + pub terrain: Terrain, +} + +impl Filter for TerrainFilter { + fn name(&self) -> &'static str { + "TerrainFilter" + } + + fn should_apply(&self, tile: &Tile, local_x: usize, local_z: usize) -> bool { + tile.get_terrain(local_x, local_z) == self.terrain as u8 + } +} + +/// Only apply where height is within a range. +pub struct HeightFilter { + pub min: i16, + pub max: i16, +} + +impl Filter for HeightFilter { + fn name(&self) -> &'static str { + "HeightFilter" + } + + fn should_apply(&self, tile: &Tile, local_x: usize, local_z: usize) -> bool { + let h = tile.get_height(local_x, local_z); + h >= self.min && h <= self.max + } +} + +/// Only apply where slope (max height difference to 4 neighbors) is within a range. +pub struct SlopeFilter { + pub min: f64, + pub max: f64, +} + +impl Filter for SlopeFilter { + fn name(&self) -> &'static str { + "SlopeFilter" + } + + fn should_apply(&self, tile: &Tile, local_x: usize, local_z: usize) -> bool { + let center = tile.get_height(local_x, local_z); + let mut max_diff = 0i16; + for (dx, dz) in [(0, -1), (0, 1), (-1, 0), (1, 0)] { + let nx = local_x as i32 + dx; + let nz = local_z as i32 + dz; + if nx >= 0 && nx < TILE_SIZE as i32 && nz >= 0 && nz < TILE_SIZE as i32 { + let nh = tile.get_height(nx as usize, nz as usize); + let diff = (center - nh).abs(); + if diff > max_diff { + max_diff = diff; + } + } + } + let slope = max_diff as f64; + slope >= self.min && slope <= self.max + } +} + +/// How multiple filters are combined. +pub enum FilterCombinator { + And, + Or, +} + +/// Combine multiple filters with AND/OR logic. +pub struct CompositeFilter { + pub filters: Vec>, + pub combinator: FilterCombinator, +} + +impl Filter for CompositeFilter { + fn name(&self) -> &'static str { + "CompositeFilter" + } + + fn should_apply(&self, tile: &Tile, local_x: usize, local_z: usize) -> bool { + match self.combinator { + FilterCombinator::And => self + .filters + .iter() + .all(|f| f.should_apply(tile, local_x, local_z)), + FilterCombinator::Or => self + .filters + .iter() + .any(|f| f.should_apply(tile, local_x, local_z)), + } + } +} + +/// Negate a filter. +pub struct InvertFilter { + pub filter: Box, +} + +impl Filter for InvertFilter { + fn name(&self) -> &'static str { + "InvertFilter" + } + + fn should_apply(&self, tile: &Tile, local_x: usize, local_z: usize) -> bool { + !self.filter.should_apply(tile, local_x, local_z) + } +} + +/// Wraps any operation and restricts its effect to cells that pass the filter. +/// +/// On apply: +/// 1. Snapshots all cell heights in the brush area. +/// 2. Runs the inner operation. +/// 3. Restores heights of cells that do NOT pass the filter. +/// +/// Inverse restores all cells from the snapshot. +pub struct FilteredOperation { + pub operation: Box, + pub filter: Box, + pub tile_x: i32, + pub tile_z: i32, + pub center_x: u32, + pub center_z: u32, + pub radius: u32, + pub before_snapshot: OnceLock>, +} + +impl Operation for FilteredOperation { + fn name(&self) -> &'static str { + self.operation.name() + } + + fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> { + let is_first_apply = self.before_snapshot.get().is_none(); + + // Phase 1: snapshot all cells in brush area (if first apply) + let snapshot: Vec<(usize, i16)> = if is_first_apply { + let tile = dim.tiles.get(&(self.tile_x, self.tile_z)).ok_or( + OperationError::OutOfBounds { + tx: self.tile_x, + tz: self.tile_z, + x: 0, + z: 0, + }, + )?; + + let r = self.radius as i32; + let cx = self.center_x as i32; + let cz = self.center_z as i32; + + let mut snap = Vec::new(); + for dz in -r..=r { + for dx in -r..=r { + let ax = cx + dx; + let az = cz + dz; + if ax < 0 || az < 0 || ax >= TILE_SIZE as i32 || az >= TILE_SIZE as i32 { + continue; + } + let idx = (az as usize) * TILE_SIZE + (ax as usize); + snap.push((idx, tile.heightmap[idx])); + } + } + let _ = self.before_snapshot.set(snap.clone()); + snap + } else { + self.before_snapshot + .get() + .cloned() + .unwrap_or_default() + }; + + // Phase 2: apply inner operation + self.operation.apply(dim)?; + + // Phase 3: restore cells that don't pass the filter + let tile = dim.tiles.get_mut(&(self.tile_x, self.tile_z)).ok_or( + OperationError::OutOfBounds { + tx: self.tile_x, + tz: self.tile_z, + x: 0, + z: 0, + }, + )?; + + for &(idx, original_h) in &snapshot { + let lx = idx % TILE_SIZE; + let lz = idx / TILE_SIZE; + if !self.filter.should_apply(tile, lx, lz) { + tile.heightmap[idx] = original_h; + } + } + + Ok(()) + } + + fn inverse(&self) -> Box { + let snapshot = self.before_snapshot.get().cloned().unwrap_or_default(); + Box::new(RestoreHeightsOperation { + tile_x: self.tile_x, + tile_z: self.tile_z, + snapshot, + }) + } +} diff --git a/core/src/ops/mod.rs b/core/src/ops/mod.rs index 954854e..09b2f6b 100755 --- a/core/src/ops/mod.rs +++ b/core/src/ops/mod.rs @@ -1,4 +1,5 @@ //! Editing operations — terrain modification, brush operations, height maps. +pub mod filters; pub mod heightmap; pub mod operations; diff --git a/core/src/ops/operations.rs b/core/src/ops/operations.rs index 5813f52..5786111 100755 --- a/core/src/ops/operations.rs +++ b/core/src/ops/operations.rs @@ -2,6 +2,7 @@ //! //! Each operation implements the Operation trait and supports undo. +use std::collections::VecDeque; use std::sync::OnceLock; use std::sync::Arc; @@ -443,6 +444,367 @@ impl Operation for SmoothOperation { } } +/// Simulate thermal erosion — material moves from high to low when slope exceeds threshold. +pub struct ErodeOperation { + pub tile_x: i32, + pub tile_z: i32, + pub center_x: u32, + pub center_z: u32, + pub radius: u32, + pub iterations: u32, + pub talus_angle: f64, + pub brush: Arc, + pub before_snapshot: OnceLock>, +} + +impl Operation for ErodeOperation { + fn name(&self) -> &'static str { + "Erode" + } + + fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> { + let tile = dim + .tiles + .get_mut(&(self.tile_x, self.tile_z)) + .ok_or(OperationError::OutOfBounds { + tx: self.tile_x, + tz: self.tile_z, + x: 0, + z: 0, + })?; + + let r = self.radius as i32; + let cx = self.center_x as i32; + let cz = self.center_z as i32; + + // Snapshot original heights on first apply + let mut snapshot: Vec<(usize, i16)> = Vec::new(); + let is_first_apply = self.before_snapshot.get().is_none(); + if is_first_apply { + for dz in -r..=r { + for dx in -r..=r { + let ax = cx + dx; + let az = cz + dz; + if ax < 0 || az < 0 || ax >= TILE_SIZE as i32 || az >= TILE_SIZE as i32 { + continue; + } + if self.brush.get_strength(dx as f64, dz as f64) <= 0.0 { + continue; + } + let idx = (az as usize) * TILE_SIZE + (ax as usize); + snapshot.push((idx, tile.heightmap[idx])); + } + } + } + if is_first_apply { + let _ = self.before_snapshot.set(snapshot); + } + + // Apply erosion for `iterations` passes + let tile_size = TILE_SIZE; + for _ in 0..self.iterations.max(1) { + let mut deltas = vec![0i32; tile_size * tile_size]; + + for dz in -r..=r { + for dx in -r..=r { + let ax = cx + dx; + let az = cz + dz; + if ax < 0 || az < 0 || ax >= tile_size as i32 || az >= tile_size as i32 { + continue; + } + if self.brush.get_strength(dx as f64, dz as f64) <= 0.0 { + continue; + } + + let idx = (az as usize) * tile_size + (ax as usize); + let current = tile.heightmap[idx] as i32; + + // Find lowest neighbor that is also in brush area + let mut lowest_h = current; + let mut lowest_idx = None; + for (ndx, ndz) in [(0, -1), (0, 1), (-1, 0), (1, 0)] { + let bx = ax + ndx; + let bz = az + ndz; + if bx < 0 || bx >= tile_size as i32 || bz < 0 || bz >= tile_size as i32 + { + continue; + } + let n_dx = bx - cx; + let n_dz = bz - cz; + if self.brush.get_strength(n_dx as f64, n_dz as f64) <= 0.0 { + continue; + } + let nidx = (bz as usize) * tile_size + (bx as usize); + let nh = tile.heightmap[nidx] as i32; + if nh < lowest_h { + lowest_h = nh; + lowest_idx = Some(nidx); + } + } + + if let Some(lidx) = lowest_idx { + let diff = current - lowest_h; + if (diff as f64) > self.talus_angle { + let excess = diff as f64 - self.talus_angle; + let move_amount = (excess * 0.5).round() as i32; + deltas[idx] -= move_amount; + deltas[lidx] += move_amount; + } + } + } + } + + // Apply deltas + for dz in -r..=r { + for dx in -r..=r { + let ax = cx + dx; + let az = cz + dz; + if ax < 0 || az < 0 || ax >= tile_size as i32 || az >= tile_size as i32 { + continue; + } + if self.brush.get_strength(dx as f64, dz as f64) <= 0.0 { + continue; + } + let idx = (az as usize) * tile_size + (ax as usize); + if deltas[idx] != 0 { + let new_h = (tile.heightmap[idx] as i32 + deltas[idx]) + .clamp(tile.min_height as i32, tile.max_height as i32) + as i16; + tile.heightmap[idx] = new_h; + } + } + } + } + + Ok(()) + } + + fn inverse(&self) -> Box { + let snapshot = self.before_snapshot.get().cloned().unwrap_or_default(); + Box::new(RestoreHeightsOperation { + tile_x: self.tile_x, + tile_z: self.tile_z, + snapshot, + }) + } +} + +/// Fill a contiguous area (flood-fill from center) with a terrain type. +pub struct FillOperation { + pub tile_x: i32, + pub tile_z: i32, + pub center_x: u32, + pub center_z: u32, + pub terrain: Terrain, + pub before_snapshot: OnceLock>, +} + +impl Operation for FillOperation { + fn name(&self) -> &'static str { + "Fill" + } + + fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> { + let tile = dim + .tiles + .get_mut(&(self.tile_x, self.tile_z)) + .ok_or(OperationError::OutOfBounds { + tx: self.tile_x, + tz: self.tile_z, + x: 0, + z: 0, + })?; + + let cx = self.center_x as usize; + let cz = self.center_z as usize; + if cx >= TILE_SIZE || cz >= TILE_SIZE { + return Err(OperationError::OutOfBounds { + tx: self.tile_x, + tz: self.tile_z, + x: self.center_x, + z: self.center_z, + }); + } + + let idx = cz * TILE_SIZE + cx; + let original_terrain = tile.terrain[idx]; + let target_terrain = self.terrain as u8; + + if original_terrain == target_terrain { + return Ok(()); + } + + let mut snapshot: Vec<(usize, u8)> = Vec::new(); + let mut visited = vec![false; TILE_SIZE * TILE_SIZE]; + let mut queue = VecDeque::new(); + queue.push_back((cx, cz)); + visited[idx] = true; + + while let Some((x, z)) = queue.pop_front() { + let i = z * TILE_SIZE + x; + if self.before_snapshot.get().is_none() { + snapshot.push((i, tile.terrain[i])); + } + tile.terrain[i] = target_terrain; + + for (ndx, ndz) in [(0, -1), (0, 1), (-1, 0), (1, 0)] { + let nx = x as i32 + ndx; + let nz = z as i32 + ndz; + if nx >= 0 && nx < TILE_SIZE as i32 && nz >= 0 && nz < TILE_SIZE as i32 { + let nxu = nx as usize; + let nzu = nz as usize; + let ni = nzu * TILE_SIZE + nxu; + if !visited[ni] && tile.terrain[ni] == original_terrain { + visited[ni] = true; + queue.push_back((nxu, nzu)); + } + } + } + } + + if self.before_snapshot.get().is_none() { + let _ = self.before_snapshot.set(snapshot); + } + + Ok(()) + } + + fn inverse(&self) -> Box { + let snapshot = self.before_snapshot.get().cloned().unwrap_or_default(); + Box::new(RestoreTerrainOperation { + tile_x: self.tile_x, + tile_z: self.tile_z, + snapshot, + }) + } +} + +/// Set all terrain within brush area to a single type (hard replace regardless of strength). +pub struct FloodOperation { + pub tile_x: i32, + pub tile_z: i32, + pub center_x: u32, + pub center_z: u32, + pub radius: u32, + pub terrain: Terrain, + pub brush: Arc, + pub before_snapshot: OnceLock>, +} + +impl Operation for FloodOperation { + fn name(&self) -> &'static str { + "Flood" + } + + fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> { + let tile = dim + .tiles + .get_mut(&(self.tile_x, self.tile_z)) + .ok_or(OperationError::OutOfBounds { + tx: self.tile_x, + tz: self.tile_z, + x: 0, + z: 0, + })?; + + let r = self.radius as i32; + let cx = self.center_x as i32; + let cz = self.center_z as i32; + let terrain_id = self.terrain as u8; + + let mut snapshot: Vec<(usize, u8)> = Vec::new(); + + for dz in -r..=r { + for dx in -r..=r { + let ax = cx + dx; + let az = cz + dz; + if ax < 0 || az < 0 || ax >= TILE_SIZE as i32 || az >= TILE_SIZE as i32 { + continue; + } + if self.brush.get_strength(dx as f64, dz as f64) == 0.0 { + continue; + } + let idx = (az as usize) * TILE_SIZE + (ax as usize); + if self.before_snapshot.get().is_none() { + snapshot.push((idx, tile.terrain[idx])); + } + tile.terrain[idx] = terrain_id; + } + } + + if self.before_snapshot.get().is_none() { + let _ = self.before_snapshot.set(snapshot); + } + + Ok(()) + } + + fn inverse(&self) -> Box { + let snapshot = self.before_snapshot.get().cloned().unwrap_or_default(); + Box::new(RestoreTerrainOperation { + tile_x: self.tile_x, + tile_z: self.tile_z, + snapshot, + }) + } +} + +/// Single-point terrain paint (radius 1, no brush falloff). +pub struct PencilOperation { + pub tile_x: i32, + pub tile_z: i32, + pub center_x: u32, + pub center_z: u32, + pub terrain: Terrain, + pub before_snapshot: OnceLock>, +} + +impl Operation for PencilOperation { + fn name(&self) -> &'static str { + "Pencil" + } + + fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> { + let tile = dim + .tiles + .get_mut(&(self.tile_x, self.tile_z)) + .ok_or(OperationError::OutOfBounds { + tx: self.tile_x, + tz: self.tile_z, + x: 0, + z: 0, + })?; + + let cx = self.center_x as usize; + let cz = self.center_z as usize; + if cx >= TILE_SIZE || cz >= TILE_SIZE { + return Err(OperationError::OutOfBounds { + tx: self.tile_x, + tz: self.tile_z, + x: self.center_x, + z: self.center_z, + }); + } + + let idx = cz * TILE_SIZE + cx; + if self.before_snapshot.get().is_none() { + let _ = self.before_snapshot.set(vec![(idx, tile.terrain[idx])]); + } + tile.terrain[idx] = self.terrain as u8; + + Ok(()) + } + + fn inverse(&self) -> Box { + let snapshot = self.before_snapshot.get().cloned().unwrap_or_default(); + Box::new(RestoreTerrainOperation { + tile_x: self.tile_x, + tile_z: self.tile_z, + snapshot, + }) + } +} + /// Applies multiple sub-operations, supporting brushes that span multiple tiles. pub struct MultiTileOperation { pub operations: Vec>,