Fix clippy warnings: needless_range_loop, collapsible_if, borrowed_box, type_complexity, dead code cleanup

This commit is contained in:
loki5512344 2026-06-15 19:52:21 +02:00
parent fa05187f1c
commit 3d3798136d
22 changed files with 579 additions and 428 deletions

View file

@ -1,6 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use criterion::{black_box, criterion_group, criterion_main, Criterion}; use criterion::{Criterion, black_box, criterion_group, criterion_main};
use terrafier_nbt::tag::Tag; use terrafier_nbt::tag::Tag;
use terrafier_nbt::{reader, writer}; use terrafier_nbt::{reader, writer};

View file

@ -1,4 +1,4 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion}; use criterion::{Criterion, black_box, criterion_group, criterion_main};
use terrafier_core::io::export::render_to_image; use terrafier_core::io::export::render_to_image;
use terrafier_core::model::tile::Tile; use terrafier_core::model::tile::Tile;

View file

@ -1,14 +1,12 @@
use std::sync::Arc; use std::sync::Arc;
use criterion::{black_box, criterion_group, criterion_main, Criterion}; use criterion::{Criterion, black_box, criterion_group, criterion_main};
use terrafier_core::io::export::render_to_image; use terrafier_core::io::export::render_to_image;
use terrafier_core::model::brush::SymmetricBrush; use terrafier_core::model::brush::SymmetricBrush;
use terrafier_core::model::terrain::Terrain; use terrafier_core::model::terrain::Terrain;
use terrafier_core::model::world::World; use terrafier_core::model::world::World;
use terrafier_core::ops::operations::{ use terrafier_core::ops::operations::{HeightOperation, Operation, PaintOperation};
HeightOperation, Operation, PaintOperation,
};
fn bench_world_new(c: &mut Criterion) { fn bench_world_new(c: &mut Criterion) {
c.bench_function("world/new_default", |b| { c.bench_function("world/new_default", |b| {

View file

@ -23,8 +23,7 @@ pub type Result<T> = std::result::Result<T, BinaryError>;
/// Save a `World` to a binary file. /// Save a `World` to a binary file.
pub fn save_binary(world: &World, path: &Path) -> Result<()> { pub fn save_binary(world: &World, path: &Path) -> Result<()> {
let bytes = bincode::serialize(world) let bytes = bincode::serialize(world).map_err(|e| BinaryError::Serialize(e.to_string()))?;
.map_err(|e| BinaryError::Serialize(e.to_string()))?;
std::fs::write(path, &bytes)?; std::fs::write(path, &bytes)?;
Ok(()) Ok(())
@ -33,8 +32,8 @@ pub fn save_binary(world: &World, path: &Path) -> Result<()> {
/// Load a `World` from a binary file. /// Load a `World` from a binary file.
pub fn load_binary(path: &Path) -> Result<World> { pub fn load_binary(path: &Path) -> Result<World> {
let bytes = std::fs::read(path)?; let bytes = std::fs::read(path)?;
let world: World = bincode::deserialize(&bytes) let world: World =
.map_err(|e| BinaryError::Deserialize(e.to_string()))?; bincode::deserialize(&bytes).map_err(|e| BinaryError::Deserialize(e.to_string()))?;
Ok(world) Ok(world)
} }

View file

@ -51,7 +51,7 @@ pub fn validate_save(path: &Path) -> Result<()> {
let region_count = std::fs::read_dir(&region_dir) let region_count = std::fs::read_dir(&region_dir)
.map_err(|e| ImportError::ValidationFailed(e.to_string()))? .map_err(|e| ImportError::ValidationFailed(e.to_string()))?
.filter_map(|e| e.ok()) .filter_map(|e| e.ok())
.filter(|e| e.path().extension().map_or(false, |ext| ext == "mca")) .filter(|e| e.path().extension().is_some_and(|ext| ext == "mca"))
.count(); .count();
if region_count == 0 { if region_count == 0 {
@ -67,11 +67,9 @@ pub fn validate_save(path: &Path) -> Result<()> {
pub fn import(path: &Path) -> Result<World> { pub fn import(path: &Path) -> Result<World> {
let path_str = path.display().to_string(); let path_str = path.display().to_string();
if path.is_dir() { if path.is_dir() && path.join("level.dat").exists() && path.join("region").is_dir() {
if path.join("level.dat").exists() && path.join("region").is_dir() {
return import_minecraft_save(path); return import_minecraft_save(path);
} }
}
if wp_import::is_world_file(path) { if wp_import::is_world_file(path) {
return wp_import::import_world_file(path) return wp_import::import_world_file(path)

View file

@ -84,12 +84,12 @@ impl LayerExport for CavesLayerExport {
let data = tile.get_layer_data(LAYER_CAVES)?; let data = tile.get_layer_data(LAYER_CAVES)?;
if let LayerBuffer::Nibble(nibbles) = data { if let LayerBuffer::Nibble(nibbles) = data {
let byte = nibbles[idx / 2]; let byte = nibbles[idx / 2];
let value = if idx % 2 == 0 { byte >> 4 } else { byte & 0xF }; let value = if idx.is_multiple_of(2) {
if value > 0 byte >> 4
&& y < surface_y } else {
&& y >= surface_y - 12 byte & 0xF
&& y > -60 };
{ if value > 0 && y < surface_y && y >= surface_y - 12 && y > -60 {
match block_name { match block_name {
"minecraft:stone" "minecraft:stone"
| "minecraft:deepslate" | "minecraft:deepslate"
@ -247,7 +247,11 @@ impl LayerExport for ResourcesLayerExport {
let data = tile.get_layer_data(LAYER_RESOURCES)?; let data = tile.get_layer_data(LAYER_RESOURCES)?;
if let LayerBuffer::Nibble(nibbles) = data { if let LayerBuffer::Nibble(nibbles) = data {
let byte = nibbles[idx / 2]; let byte = nibbles[idx / 2];
let value = if idx % 2 == 0 { byte >> 4 } else { byte & 0xF }; let value = if idx.is_multiple_of(2) {
byte >> 4
} else {
byte & 0xF
};
if value > 0 && block_name == "minecraft:stone" && y > -60 { if value > 0 && block_name == "minecraft:stone" && y > -60 {
let ore = match value { let ore = match value {
1 => "minecraft:coal_ore", 1 => "minecraft:coal_ore",

View file

@ -1,20 +1,23 @@
//! Minecraft world I/O — read and write Java Edition saves. //! Minecraft world I/O — read and write Java Edition saves.
use std::collections::HashMap; use log;
use std::collections::{BTreeMap, HashMap};
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use thiserror::Error; use thiserror::Error;
use log;
use terrafier_fastanvil::io::region::Region; use terrafier_fastanvil::io::region::Region;
use terrafier_nbt::io::reader::read_gzip; use terrafier_nbt::io::reader::read_gzip;
use crate::io::layer_export::{LayerExport, apply_layers, biome_name};
use crate::model::dimension::Dimension; use crate::model::dimension::Dimension;
use crate::model::platform::Platform; use crate::model::platform::Platform;
use crate::io::layer_export::{apply_layers, biome_name, LayerExport};
use crate::model::tile::Tile; use crate::model::tile::Tile;
use crate::model::world::World; use crate::model::world::World;
#[allow(clippy::type_complexity)]
type RegionTiles<'a> = BTreeMap<(i32, i32), Vec<(&'a (i32, i32), &'a Tile)>>;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum MinecraftIOError { pub enum MinecraftIOError {
#[error("IO error: {0}")] #[error("IO error: {0}")]
@ -49,14 +52,17 @@ pub fn version_from_data_version(dv: i32) -> Option<Platform> {
max_height: 320, max_height: 320,
}), }),
2866..=2974 => { 2866..=2974 => {
log::warn!("DataVersion {} is between 1.18 and 1.19, falling back to 1.18", dv); log::warn!(
"DataVersion {} is between 1.18 and 1.19, falling back to 1.18",
dv
);
Some(Platform { Some(Platform {
id: "java_1_18".into(), id: "java_1_18".into(),
display_name: "Minecraft Java 1.18".into(), display_name: "Minecraft Java 1.18".into(),
min_height: -64, min_height: -64,
max_height: 320, max_height: 320,
}) })
}, }
2975..=3117 => Some(Platform { 2975..=3117 => Some(Platform {
id: "java_1_19".into(), id: "java_1_19".into(),
display_name: "Minecraft Java 1.19".into(), display_name: "Minecraft Java 1.19".into(),
@ -64,14 +70,17 @@ pub fn version_from_data_version(dv: i32) -> Option<Platform> {
max_height: 320, max_height: 320,
}), }),
3118..=3336 => { 3118..=3336 => {
log::warn!("DataVersion {} is between 1.19 and 1.20, falling back to 1.19", dv); log::warn!(
"DataVersion {} is between 1.19 and 1.20, falling back to 1.19",
dv
);
Some(Platform { Some(Platform {
id: "java_1_19".into(), id: "java_1_19".into(),
display_name: "Minecraft Java 1.19".into(), display_name: "Minecraft Java 1.19".into(),
min_height: -64, min_height: -64,
max_height: 320, max_height: 320,
}) })
}, }
3337..=3460 => Some(Platform { 3337..=3460 => Some(Platform {
id: "java_1_20".into(), id: "java_1_20".into(),
display_name: "Minecraft Java 1.20".into(), display_name: "Minecraft Java 1.20".into(),
@ -79,14 +88,17 @@ pub fn version_from_data_version(dv: i32) -> Option<Platform> {
max_height: 320, max_height: 320,
}), }),
3461..=3577 => { 3461..=3577 => {
log::warn!("DataVersion {} is between 1.20 and 1.20.5, falling back to 1.20", dv); log::warn!(
"DataVersion {} is between 1.20 and 1.20.5, falling back to 1.20",
dv
);
Some(Platform { Some(Platform {
id: "java_1_20".into(), id: "java_1_20".into(),
display_name: "Minecraft Java 1.20 (fallback)".into(), display_name: "Minecraft Java 1.20 (fallback)".into(),
min_height: -64, min_height: -64,
max_height: 320, max_height: 320,
}) })
}, }
3578..=3700 => Some(Platform { 3578..=3700 => Some(Platform {
id: "java_1_20_5".into(), id: "java_1_20_5".into(),
display_name: "Minecraft Java 1.20.5+".into(), display_name: "Minecraft Java 1.20.5+".into(),
@ -94,14 +106,17 @@ pub fn version_from_data_version(dv: i32) -> Option<Platform> {
max_height: 320, max_height: 320,
}), }),
3701..=3818 => { 3701..=3818 => {
log::warn!("DataVersion {} is between 1.20.5 and 1.21, falling back to 1.20.5", dv); log::warn!(
"DataVersion {} is between 1.20.5 and 1.21, falling back to 1.20.5",
dv
);
Some(Platform { Some(Platform {
id: "java_1_20_5".into(), id: "java_1_20_5".into(),
display_name: "Minecraft Java 1.20.5 (fallback)".into(), display_name: "Minecraft Java 1.20.5 (fallback)".into(),
min_height: -64, min_height: -64,
max_height: 320, max_height: 320,
}) })
}, }
3819..=3953 => Some(Platform { 3819..=3953 => Some(Platform {
id: "java_1_21".into(), id: "java_1_21".into(),
display_name: "Minecraft Java 1.21".into(), display_name: "Minecraft Java 1.21".into(),
@ -148,17 +163,18 @@ pub fn load_save(path: &Path) -> Result<World> {
for entry in region_entries { for entry in region_entries {
let entry = entry?; let entry = entry?;
let path = entry.path(); let path = entry.path();
if path.extension().map_or(false, |e| e == "mca") { if path.extension().is_some_and(|e| e == "mca") {
let file_name = path.file_stem().and_then(|s| s.to_str()).unwrap_or(""); let file_name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
let parts: Vec<&str> = file_name.split('.').collect(); let parts: Vec<&str> = file_name.split('.').collect();
if parts.len() >= 3 { if parts.len() >= 3
if let (Ok(rx), Ok(rz)) = (parts[1].parse::<i32>(), parts[2].parse::<i32>()) { && let (Ok(rx), Ok(rz)) = (parts[1].parse::<i32>(), parts[2].parse::<i32>())
{
let region_bytes = fs::read(&path)?; let region_bytes = fs::read(&path)?;
let region = Region::from_bytes(rx, rz, &region_bytes)?; let region = Region::from_bytes(rx, rz, &region_bytes)?;
for (local_x, local_z) in region.chunk_coords() { for (local_x, local_z) in region.chunk_coords() {
let chunk_data = region.get_chunk_data(local_x, local_z).unwrap(); let chunk_data = region.get_chunk_data(local_x, local_z).unwrap();
if let Ok(chunk_tag) = read_gzip(chunk_data) { if let Ok(chunk_tag) = read_gzip(chunk_data)
if let Some(chunk) = && let Some(chunk) =
terrafier_fastanvil::io::chunk::Chunk::from_nbt(&chunk_tag) terrafier_fastanvil::io::chunk::Chunk::from_nbt(&chunk_tag)
{ {
let tile_x = chunk.x >> 3; let tile_x = chunk.x >> 3;
@ -168,33 +184,25 @@ pub fn load_save(path: &Path) -> Result<World> {
let chunk_local_x = (chunk.x & 7) as usize; let chunk_local_x = (chunk.x & 7) as usize;
let chunk_local_z = (chunk.z & 7) as usize; let chunk_local_z = (chunk.z & 7) as usize;
let tile = tiles let tile = tiles.entry((tile_x, tile_z)).or_insert_with(|| {
.entry((tile_x, tile_z)) Tile::new(tile_x, tile_z, platform.min_height, platform.max_height)
.or_insert_with(|| Tile::new( });
tile_x,
tile_z,
platform.min_height,
platform.max_height,
));
for lx in 0..16usize { for lx in 0..16usize {
for lz in 0..16usize { for lz in 0..16usize {
let mut surface_y = None; let mut surface_y = None;
if !chunk.sections.is_empty() { if !chunk.sections.is_empty() {
let mut sorted: Vec<_> = let mut sorted: Vec<_> = chunk.sections.iter().collect();
chunk.sections.iter().collect(); sorted.sort_by_key(|b| std::cmp::Reverse(b.section_y));
sorted
.sort_by(|a, b| b.section_y.cmp(&a.section_y));
for section in &sorted { for section in &sorted {
if section.palette.is_empty() { if section.palette.is_empty() {
continue; continue;
} }
let has_blocks = let has_blocks = section.palette.iter().any(|p| {
section.palette.iter().any(|p| { p.get("Name").is_some_and(|n| {
p.get("Name").map_or(false, |n| {
matches!( matches!(
n, n,
terrafier_nbt::Tag::String(s) terrafier_nbt::Tag::String(s)
@ -208,27 +216,20 @@ pub fn load_save(path: &Path) -> Result<World> {
} }
if section.block_data.is_empty() { if section.block_data.is_empty() {
if section.palette[0] if section.palette[0].get("Name").is_some_and(|n| {
.get("Name")
.map_or(false, |n| {
matches!( matches!(
n, n,
terrafier_nbt::Tag::String(s) terrafier_nbt::Tag::String(s)
if s == "minecraft:air" if s == "minecraft:air"
) )
}) }) {
{
continue; continue;
} }
surface_y = Some( surface_y = Some((section.section_y as i32) * 16 + 15);
(section.section_y as i32) * 16 + 15,
);
break; break;
} }
surface_y = Some( surface_y = Some((section.section_y as i32) * 16 + 15);
(section.section_y as i32) * 16 + 15,
);
break; break;
} }
} }
@ -236,15 +237,13 @@ pub fn load_save(path: &Path) -> Result<World> {
let tile_local_x = chunk_local_x * 16 + lx; let tile_local_x = chunk_local_x * 16 + lx;
let tile_local_z = chunk_local_z * 16 + lz; let tile_local_z = chunk_local_z * 16 + lz;
if tile_local_x < 128 && tile_local_z < 128 { if tile_local_x < 128
if let Some(y) = surface_y { && tile_local_z < 128
let clamped = (y as i16) && let Some(y) = surface_y
.clamp(tile.min_height, tile.max_height); {
tile.heightmap let clamped =
[tile_local_z * 128 + tile_local_x] = clamped; (y as i16).clamp(tile.min_height, tile.max_height);
} tile.heightmap[tile_local_z * 128 + tile_local_x] = clamped;
}
}
} }
} }
} }
@ -272,8 +271,6 @@ pub fn load_save(path: &Path) -> Result<World> {
/// Save a Terrafier World to a Minecraft save directory. /// Save a Terrafier World to a Minecraft save directory.
pub fn save_world(world: &World, output_path: &Path) -> Result<()> { pub fn save_world(world: &World, output_path: &Path) -> Result<()> {
use std::collections::BTreeMap;
fs::create_dir_all(output_path.join("region"))?; fs::create_dir_all(output_path.join("region"))?;
// Write level.dat // Write level.dat
@ -287,7 +284,7 @@ pub fn save_world(world: &World, output_path: &Path) -> Result<()> {
}; };
// Group tiles by region (512×512 blocks = 4×4 tiles of 128×128 each) // Group tiles by region (512×512 blocks = 4×4 tiles of 128×128 each)
let mut regions: BTreeMap<(i32, i32), Vec<(&(i32, i32), &Tile)>> = BTreeMap::new(); let mut regions: RegionTiles = BTreeMap::new();
for (key, tile) in &dim.tiles { for (key, tile) in &dim.tiles {
let (tx, tz) = key; let (tx, tz) = key;
let rx = tx >> 2; let rx = tx >> 2;
@ -345,7 +342,7 @@ fn pack_indices(indices: &[u16], bits: usize) -> Vec<i64> {
return Vec::new(); return Vec::new();
} }
let total_bits = indices.len() * bits; let total_bits = indices.len() * bits;
let longs = (total_bits + 63) / 64; let longs = total_bits.div_ceil(64);
let mut data = vec![0i64; longs]; let mut data = vec![0i64; longs];
let mask = (1i64 << bits) - 1; let mask = (1i64 << bits) - 1;
for (i, &idx) in indices.iter().enumerate() { for (i, &idx) in indices.iter().enumerate() {
@ -459,11 +456,14 @@ fn build_chunk_nbt(
let bits = bits_needed(BLOCK_SET.len()); let bits = bits_needed(BLOCK_SET.len());
let packed = pack_indices(&indices, bits); let packed = pack_indices(&indices, bits);
let palette_tags: Vec<terrafier_nbt::Tag> = BLOCK_SET.iter().map(|name| { let palette_tags: Vec<terrafier_nbt::Tag> = BLOCK_SET
.iter()
.map(|name| {
let mut entry = HashMap::new(); let mut entry = HashMap::new();
entry.insert("Name".into(), terrafier_nbt::Tag::String(name.to_string())); entry.insert("Name".into(), terrafier_nbt::Tag::String(name.to_string()));
terrafier_nbt::Tag::Compound(entry) terrafier_nbt::Tag::Compound(entry)
}).collect(); })
.collect();
let mut block_states = HashMap::new(); let mut block_states = HashMap::new();
block_states.insert("palette".into(), terrafier_nbt::Tag::List(palette_tags)); block_states.insert("palette".into(), terrafier_nbt::Tag::List(palette_tags));
@ -471,7 +471,10 @@ fn build_chunk_nbt(
let biome_palette = vec![{ let biome_palette = vec![{
let mut b = HashMap::new(); let mut b = HashMap::new();
b.insert("Name".into(), terrafier_nbt::Tag::String("minecraft:plains".into())); b.insert(
"Name".into(),
terrafier_nbt::Tag::String("minecraft:plains".into()),
);
terrafier_nbt::Tag::Compound(b) terrafier_nbt::Tag::Compound(b)
}]; }];
let mut biomes = HashMap::new(); let mut biomes = HashMap::new();
@ -479,7 +482,10 @@ fn build_chunk_nbt(
let mut sec_compound = HashMap::new(); let mut sec_compound = HashMap::new();
sec_compound.insert("Y".into(), terrafier_nbt::Tag::Byte(sec_y as i8)); 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(
"block_states".into(),
terrafier_nbt::Tag::Compound(block_states),
);
sec_compound.insert("biomes".into(), terrafier_nbt::Tag::Compound(biomes)); sec_compound.insert("biomes".into(), terrafier_nbt::Tag::Compound(biomes));
sections.push(terrafier_nbt::Tag::Compound(sec_compound)); sections.push(terrafier_nbt::Tag::Compound(sec_compound));
@ -557,7 +563,15 @@ fn build_chunk_nbt_with_layers(
let surface_y = tile.heightmap[tile_lz * 128 + tile_lx] as i32; let surface_y = tile.heightmap[tile_lz * 128 + tile_lx] as i32;
let terrain_id = tile.terrain[tile_lz * 128 + tile_lx]; let terrain_id = tile.terrain[tile_lz * 128 + tile_lx];
let base_name = block_name(terrain_id, global_y, surface_y); 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); 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 BLOCK_SET.iter().position(|s| *s == name).unwrap_or(0) as u16
}; };
indices.push(idx); indices.push(idx);
@ -585,20 +599,26 @@ fn build_chunk_nbt_with_layers(
for (pi, name) in used_blocks.iter().enumerate() { for (pi, name) in used_blocks.iter().enumerate() {
palette_map.insert(name, pi as u16); palette_map.insert(name, pi as u16);
} }
let remapped: Vec<u16> = indices.iter().map(|&i| { let remapped: Vec<u16> = indices
.iter()
.map(|&i| {
if i < BLOCK_SET.len() as u16 { if i < BLOCK_SET.len() as u16 {
palette_map[BLOCK_SET[i as usize]] palette_map[BLOCK_SET[i as usize]]
} else { } else {
0 0
} }
}).collect(); })
.collect();
let packed = pack_indices(&remapped, bits); let packed = pack_indices(&remapped, bits);
let palette_tags: Vec<terrafier_nbt::Tag> = used_blocks.iter().map(|name| { let palette_tags: Vec<terrafier_nbt::Tag> = used_blocks
.iter()
.map(|name| {
let mut entry = HashMap::new(); let mut entry = HashMap::new();
entry.insert("Name".into(), terrafier_nbt::Tag::String(name.to_string())); entry.insert("Name".into(), terrafier_nbt::Tag::String(name.to_string()));
terrafier_nbt::Tag::Compound(entry) terrafier_nbt::Tag::Compound(entry)
}).collect(); })
.collect();
let mut block_states = HashMap::new(); let mut block_states = HashMap::new();
block_states.insert("palette".into(), terrafier_nbt::Tag::List(palette_tags)); block_states.insert("palette".into(), terrafier_nbt::Tag::List(palette_tags));
@ -615,7 +635,10 @@ fn build_chunk_nbt_with_layers(
let mut sec_compound = HashMap::new(); let mut sec_compound = HashMap::new();
sec_compound.insert("Y".into(), terrafier_nbt::Tag::Byte(sec_y as i8)); 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(
"block_states".into(),
terrafier_nbt::Tag::Compound(block_states),
);
sec_compound.insert("biomes".into(), terrafier_nbt::Tag::Compound(biomes)); sec_compound.insert("biomes".into(), terrafier_nbt::Tag::Compound(biomes));
sections.push(terrafier_nbt::Tag::Compound(sec_compound)); sections.push(terrafier_nbt::Tag::Compound(sec_compound));
@ -638,8 +661,6 @@ pub fn save_world_with_layers(
output_path: &Path, output_path: &Path,
layer_exporters: &[&dyn LayerExport], layer_exporters: &[&dyn LayerExport],
) -> Result<()> { ) -> Result<()> {
use std::collections::BTreeMap;
fs::create_dir_all(output_path.join("region"))?; fs::create_dir_all(output_path.join("region"))?;
let level_tag = build_level_dat(world)?; let level_tag = build_level_dat(world)?;
@ -651,7 +672,7 @@ pub fn save_world_with_layers(
None => return Ok(()), None => return Ok(()),
}; };
let mut regions: BTreeMap<(i32, i32), Vec<(&(i32, i32), &Tile)>> = BTreeMap::new(); let mut regions: RegionTiles = BTreeMap::new();
for (key, tile) in &dim.tiles { for (key, tile) in &dim.tiles {
let (tx, tz) = key; let (tx, tz) = key;
let rx = tx >> 2; let rx = tx >> 2;
@ -671,7 +692,14 @@ pub fn save_world_with_layers(
let region_local_x = (chunk_x & 31) as u8; let region_local_x = (chunk_x & 31) as u8;
let region_local_z = (chunk_z & 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)?; let chunk_data = build_chunk_nbt_with_layers(
chunk_x,
chunk_z,
tile,
chunk_lx,
chunk_lz,
layer_exporters,
)?;
if chunk_data.is_empty() { if chunk_data.is_empty() {
continue; continue;
@ -746,7 +774,7 @@ pub fn discover_region_files(world_path: &Path) -> Result<Vec<PathBuf>> {
for entry in fs::read_dir(&region_dir)? { for entry in fs::read_dir(&region_dir)? {
let entry = entry?; let entry = entry?;
let path = entry.path(); let path = entry.path();
if path.extension().map_or(false, |e| e == "mca") { if path.extension().is_some_and(|e| e == "mca") {
files.push(path); files.push(path);
} }
} }

View file

@ -53,7 +53,6 @@ const TC_LONGSTRING: u8 = 0x7C;
const TC_PROXYCLASSDESC: u8 = 0x7D; const TC_PROXYCLASSDESC: u8 = 0x7D;
const TC_ENUM: u8 = 0x7E; const TC_ENUM: u8 = 0x7E;
const SC_SERIALIZABLE: u8 = 0x02;
const SC_WRITE_METHOD: u8 = 0x01; const SC_WRITE_METHOD: u8 = 0x01;
const BASE_WIRE_HANDLE: u32 = 0x7E0000; const BASE_WIRE_HANDLE: u32 = 0x7E0000;
@ -75,7 +74,6 @@ const PRIM_BOOL: u8 = b'Z';
struct FieldInfo { struct FieldInfo {
name: String, name: String,
type_code: u8, type_code: u8,
type_name: Option<String>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -94,7 +92,7 @@ enum JvmValue {
ByteArray(Vec<i8>), ByteArray(Vec<i8>),
ShortArray(Vec<i16>), ShortArray(Vec<i16>),
IntArray(Vec<i32>), IntArray(Vec<i32>),
LongArray(Vec<i64>), LongArray(()),
Int(i32), Int(i32),
Long(i64), Long(i64),
Skipped, Skipped,
@ -106,12 +104,12 @@ impl JvmValue {
JvmValue::String(s) => Some(s.clone()), JvmValue::String(s) => Some(s.clone()),
JvmValue::Object(map) => { JvmValue::Object(map) => {
for key in &["name", "key", "id", "value"] { for key in &["name", "key", "id", "value"] {
if let Some(val) = map.get(*key) { if let Some(val) = map.get(*key)
if let JvmValue::String(s) = val { && let JvmValue::String(s) = val
{
return Some(s.clone()); return Some(s.clone());
} }
} }
}
None None
} }
_ => None, _ => None,
@ -150,12 +148,20 @@ struct JvmStream {
impl JvmStream { impl JvmStream {
fn new(data: Vec<u8>) -> Self { fn new(data: Vec<u8>) -> Self {
JvmStream { data, pos: 0, handles: Vec::new(), class_descs: Vec::new() } JvmStream {
data,
pos: 0,
handles: Vec::new(),
class_descs: Vec::new(),
}
} }
fn read_byte(&mut self) -> std::io::Result<u8> { fn read_byte(&mut self) -> std::io::Result<u8> {
if self.pos >= self.data.len() { if self.pos >= self.data.len() {
return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "unexpected end of stream")); return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"unexpected end of stream",
));
} }
let b = self.data[self.pos]; let b = self.data[self.pos];
self.pos += 1; self.pos += 1;
@ -164,7 +170,10 @@ impl JvmStream {
fn read_bytes(&mut self, n: usize) -> std::io::Result<Vec<u8>> { fn read_bytes(&mut self, n: usize) -> std::io::Result<Vec<u8>> {
if self.pos + n > self.data.len() { if self.pos + n > self.data.len() {
return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "unexpected end of stream")); 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(); let slice = self.data[self.pos..self.pos + n].to_vec();
self.pos += n; self.pos += n;
@ -173,7 +182,10 @@ impl JvmStream {
fn peek_byte(&self) -> std::io::Result<u8> { fn peek_byte(&self) -> std::io::Result<u8> {
if self.pos >= self.data.len() { if self.pos >= self.data.len() {
return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "unexpected end of stream")); return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"unexpected end of stream",
));
} }
Ok(self.data[self.pos]) Ok(self.data[self.pos])
} }
@ -201,8 +213,14 @@ impl JvmStream {
let b6 = self.read_byte()? as i64; let b6 = self.read_byte()? as i64;
let b7 = self.read_byte()? as i64; let b7 = self.read_byte()? as i64;
let b8 = self.read_byte()? as i64; let b8 = self.read_byte()? as i64;
Ok((b1 << 56) | (b2 << 48) | (b3 << 40) | (b4 << 32) Ok((b1 << 56)
| (b5 << 24) | (b6 << 16) | (b7 << 8) | b8) | (b2 << 48)
| (b3 << 40)
| (b4 << 32)
| (b5 << 24)
| (b6 << 16)
| (b7 << 8)
| b8)
} }
fn read_f32(&mut self) -> std::io::Result<f32> { fn read_f32(&mut self) -> std::io::Result<f32> {
@ -274,13 +292,11 @@ impl JvmStream {
TC_RESET => { TC_RESET => {
self.handles.clear(); 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, return Err(std::io::Error::new(
format!("unexpected content token: 0x{:02x}", tc))); std::io::ErrorKind::InvalidData,
format!("unexpected content token: 0x{:02x}", tc),
));
} }
} }
} }
@ -336,10 +352,10 @@ impl JvmStream {
self.push_handle(JvmValue::Skipped); self.push_handle(JvmValue::Skipped);
Ok(JvmValue::Skipped) Ok(JvmValue::Skipped)
} }
_ => { _ => Err(std::io::Error::new(
Err(std::io::Error::new(std::io::ErrorKind::InvalidData, std::io::ErrorKind::InvalidData,
format!("expected classdesc token, got 0x{:02x}", tc))) format!("expected classdesc token, got 0x{:02x}", tc),
} )),
} }
} }
@ -354,31 +370,26 @@ impl JvmStream {
for _ in 0..field_count { for _ in 0..field_count {
let type_code = self.read_byte()?; let type_code = self.read_byte()?;
let name = self.read_utf()?; let name = self.read_utf()?;
let type_name = if type_code == b'[' || type_code == b'L' { if type_code == b'[' || type_code == b'L' {
let tc = self.read_byte()?; let tc = self.read_byte()?;
match tc { match tc {
TC_STRING => { TC_STRING => {
let s = self.read_utf()?; self.read_utf()?;
Some(s)
} }
TC_REFERENCE => { TC_REFERENCE => {
let handle = self.read_u32_handle(); let handle = self.read_u32_handle();
let idx = (handle - BASE_WIRE_HANDLE) as usize; let idx = (handle - BASE_WIRE_HANDLE) as usize;
if idx < self.handles.len() { if idx < self.handles.len() {
self.handles[idx].as_string() let _ = self.handles[idx].as_string();
} else {
None
} }
} }
TC_LONGSTRING => { TC_LONGSTRING => {
Some(self.read_long_utf()?) self.read_long_utf()?;
} }
_ => None, _ => {}
} }
} else { }
None fields.push(FieldInfo { name, type_code });
};
fields.push(FieldInfo { name, type_code, type_name });
} }
let cd = ClassDesc { let cd = ClassDesc {
@ -432,8 +443,10 @@ impl JvmStream {
self.handles.clear(); self.handles.clear();
} }
other => { other => {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, return Err(std::io::Error::new(
format!("unexpected token in class annotations: 0x{:02x}", other))); std::io::ErrorKind::InvalidData,
format!("unexpected token in class annotations: 0x{:02x}", other),
));
} }
} }
} }
@ -459,7 +472,7 @@ impl JvmStream {
Some(cd) => cd, Some(cd) => cd,
None => { None => {
// Unknown class, skip object data // Unknown class, skip object data
let handle = self.push_handle(JvmValue::Skipped); let _handle = self.push_handle(JvmValue::Skipped);
self.skip_object_data(&name)?; self.skip_object_data(&name)?;
return Ok(JvmValue::Skipped); return Ok(JvmValue::Skipped);
} }
@ -506,7 +519,7 @@ impl JvmStream {
None None
} }
fn skip_object_data(&mut self, class_name: &str) -> std::io::Result<()> { fn skip_object_data(&mut self, _class_name: &str) -> std::io::Result<()> {
// Try to read and discard using class annotations // Try to read and discard using class annotations
// This is a best-effort skip // This is a best-effort skip
let saved = self.pos; let saved = self.pos;
@ -524,7 +537,7 @@ impl JvmStream {
return self.read_class_annotations(); return self.read_class_annotations();
} }
// Try to skip a reasonable amount // Try to skip a reasonable amount
let remaining = self.data.len() - self.pos; let _remaining = self.data.len() - self.pos;
self.pos = self.data.len(); self.pos = self.data.len();
Ok(()) Ok(())
} else { } else {
@ -552,16 +565,16 @@ impl JvmStream {
// For the class hierarchy, read field data from superclasses // For the class hierarchy, read field data from superclasses
// We can find the superclass descriptor from the stored class_descs // We can find the superclass descriptor from the stored class_descs
let super_name = self.find_super_class_name(&cd.name); let super_name = self.find_super_class_name(&cd.name);
if let Some(super_name) = super_name { if let Some(super_name) = super_name
if !super_name.is_empty() && super_name != "java.lang.Object" { && !super_name.is_empty()
if let Some(super_cd) = self.resolve_class_desc(&super_name) { && super_name != "java.lang.Object"
&& let Some(super_cd) = self.resolve_class_desc(&super_name)
{
let super_fields = self.read_object_fields(&super_cd)?; let super_fields = self.read_object_fields(&super_cd)?;
if let JvmValue::Object(super_map) = super_fields { if let JvmValue::Object(super_map) = super_fields {
map.extend(super_map); map.extend(super_map);
} }
} }
}
}
Ok(JvmValue::Object(map)) Ok(JvmValue::Object(map))
} }
@ -638,11 +651,10 @@ impl JvmStream {
JvmValue::IntArray(arr) JvmValue::IntArray(arr)
} }
Some(PRIM_LONG) => { Some(PRIM_LONG) => {
let mut arr = Vec::with_capacity(length);
for _ in 0..length { for _ in 0..length {
arr.push(self.read_i64()?); let _ = self.read_i64()?;
} }
JvmValue::LongArray(arr) JvmValue::LongArray(())
} }
_ => { _ => {
// Object array — read elements individually // Object array — read elements individually
@ -673,7 +685,9 @@ impl JvmStream {
for _ in 0..size { for _ in 0..size {
let key = self.read_content()?; let key = self.read_content()?;
let value = self.read_content()?; let value = self.read_content()?;
let key_str = key.as_string().unwrap_or_else(|| format!("__key_{}", map.len())); let key_str = key
.as_string()
.unwrap_or_else(|| format!("__key_{}", map.len()));
map.insert(key_str, value); map.insert(key_str, value);
} }
@ -729,16 +743,16 @@ struct WpWorldData {
} }
fn extract_world_data(val: &JvmValue) -> Result<WpWorldData> { fn extract_world_data(val: &JvmValue) -> Result<WpWorldData> {
let obj = val.as_object() let obj = val.as_object().ok_or_else(|| {
.ok_or_else(|| WpImportError::InvalidFormat("root object is not a compound object".into()))?; WpImportError::InvalidFormat("root object is not a compound object".into())
})?;
let name = obj.get("name") let name = obj
.get("name")
.and_then(|v| v.as_string()) .and_then(|v| v.as_string())
.unwrap_or_else(|| "WorldPainter World".to_string()); .unwrap_or_else(|| "WorldPainter World".to_string());
let seed = obj.get("seed") let seed = obj.get("seed").and_then(|v| v.as_u64()).unwrap_or(0);
.and_then(|v| v.as_u64())
.unwrap_or(0);
let mut tiles_data = Vec::new(); let mut tiles_data = Vec::new();
@ -761,25 +775,34 @@ fn extract_world_data(val: &JvmValue) -> Result<WpWorldData> {
} }
} }
Ok(WpWorldData { name, seed, tiles: tiles_data }) Ok(WpWorldData {
name,
seed,
tiles: tiles_data,
})
} }
fn collect_tiles(val: &JvmValue, tiles: &mut Vec<WpTileData>) { fn collect_tiles(val: &JvmValue, tiles: &mut Vec<WpTileData>) {
match val { match val {
JvmValue::Object(map) => { JvmValue::Object(map) => {
// Check if this is a tile by looking for heightMap/terrain fields // 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 (map.contains_key("heightMap")
if let Some(tile) = parse_single_tile(val) { || map.contains_key("terrain")
|| map.contains_key("waterLevel"))
&& let Some(tile) = parse_single_tile(val)
{
tiles.push(tile); tiles.push(tile);
return; return;
} }
}
// Could be a map from tile-key (Long) to Tile // Could be a map from tile-key (Long) to Tile
for (_key, sub) in map.iter() { for (_key, sub) in map.iter() {
collect_tiles(sub, tiles); collect_tiles(sub, tiles);
} }
} }
JvmValue::ByteArray(_) | JvmValue::ShortArray(_) | JvmValue::IntArray(_) | JvmValue::LongArray(_) => {} JvmValue::ByteArray(_)
| JvmValue::ShortArray(_)
| JvmValue::IntArray(_)
| JvmValue::LongArray(_) => {}
JvmValue::Skipped => {} JvmValue::Skipped => {}
_ => {} _ => {}
} }
@ -794,7 +817,13 @@ fn parse_single_tile(val: &JvmValue) -> Option<WpTileData> {
let terrain = map.get("terrain").and_then(array_to_u8_16384); let terrain = map.get("terrain").and_then(array_to_u8_16384);
let water_level = map.get("waterLevel").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 }) Some(WpTileData {
x,
z,
heightmap,
terrain,
water_level,
})
} }
fn array_to_heightmap(val: &JvmValue) -> Option<[i16; 16384]> { fn array_to_heightmap(val: &JvmValue) -> Option<[i16; 16384]> {
@ -884,10 +913,10 @@ impl WpImporter {
/// Check whether a path looks like a WorldPainter .world file. /// Check whether a path looks like a WorldPainter .world file.
pub fn is_world_file(path: &Path) -> bool { pub fn is_world_file(path: &Path) -> bool {
path.extension().map_or(false, |ext| ext == "world") path.extension().is_some_and(|ext| ext == "world")
|| path.file_name().map_or(false, |name| { || path
name.to_string_lossy().ends_with(".world") .file_name()
}) .is_some_and(|name| name.to_string_lossy().ends_with(".world"))
} }
/// Import a WorldPainter .world file into a Terrafier World model. /// Import a WorldPainter .world file into a Terrafier World model.
@ -902,7 +931,8 @@ pub fn import_world_file(path: &Path) -> Result<World> {
let data = if raw.len() >= 2 && raw[0] == 0x1F && raw[1] == 0x8B { let data = if raw.len() >= 2 && raw[0] == 0x1F && raw[1] == 0x8B {
let mut decoder = flate2::read::GzDecoder::new(&raw[..]); let mut decoder = flate2::read::GzDecoder::new(&raw[..]);
let mut decompressed = Vec::new(); let mut decompressed = Vec::new();
decoder.read_to_end(&mut decompressed) decoder
.read_to_end(&mut decompressed)
.map_err(|e| WpImportError::Compression(e.to_string()))?; .map_err(|e| WpImportError::Compression(e.to_string()))?;
decompressed decompressed
} else { } else {
@ -911,26 +941,31 @@ pub fn import_world_file(path: &Path) -> Result<World> {
// Validate Java serialization magic // Validate Java serialization magic
if data.len() < 4 { if data.len() < 4 {
return Err(WpImportError::InvalidFormat("file too small after decompression".into())); return Err(WpImportError::InvalidFormat(
"file too small after decompression".into(),
));
} }
let magic = u16::from_be_bytes([data[0], data[1]]); let magic = u16::from_be_bytes([data[0], data[1]]);
if magic != 0xACED { if magic != 0xACED {
return Err(WpImportError::InvalidFormat( return Err(WpImportError::InvalidFormat(format!(
format!("not a Java serialization stream (expected magic ACED, got {:04X})", magic) "not a Java serialization stream (expected magic ACED, got {:04X})",
)); magic
)));
} }
let stream_version = u16::from_be_bytes([data[2], data[3]]); let stream_version = u16::from_be_bytes([data[2], data[3]]);
if stream_version != 5 { if stream_version != 5 {
return Err(WpImportError::InvalidFormat( return Err(WpImportError::InvalidFormat(format!(
format!("unsupported Java serialization stream version: {}", stream_version) "unsupported Java serialization stream version: {}",
)); stream_version
)));
} }
// Parse the Java serialization stream // Parse the Java serialization stream
let mut stream = JvmStream::new(data); let mut stream = JvmStream::new(data);
// Read the stream header content (the first object) // Read the stream header content (the first object)
let root = stream.read_content() let root = stream
.read_content()
.map_err(|e| WpImportError::InvalidFormat(e.to_string()))?; .map_err(|e| WpImportError::InvalidFormat(e.to_string()))?;
// Extract world data from parsed stream // Extract world data from parsed stream
@ -945,7 +980,12 @@ pub fn import_world_file(path: &Path) -> Result<World> {
// Build tiles // Build tiles
let mut tiles = std::collections::HashMap::new(); let mut tiles = std::collections::HashMap::new();
for wp_tile in &world_data.tiles { for wp_tile in &world_data.tiles {
let mut tile = Tile::new(wp_tile.x, wp_tile.z, platform.min_height, platform.max_height); let mut tile = Tile::new(
wp_tile.x,
wp_tile.z,
platform.min_height,
platform.max_height,
);
if let Some(hm) = &wp_tile.heightmap { if let Some(hm) = &wp_tile.heightmap {
tile.heightmap = *hm; tile.heightmap = *hm;

View file

@ -51,12 +51,12 @@ impl<'de> Deserialize<'de> for Dimension {
let dim = Dim::deserialize(d)?; let dim = Dim::deserialize(d)?;
let mut tiles = HashMap::with_capacity(dim.tiles.len()); let mut tiles = HashMap::with_capacity(dim.tiles.len());
for (key, tile) in dim.tiles { for (key, tile) in dim.tiles {
if let Some((tx_s, tz_s)) = key.split_once(',') { if let Some((tx_s, tz_s)) = key.split_once(',')
if let (Ok(tx), Ok(tz)) = (tx_s.parse::<i32>(), tz_s.parse::<i32>()) { && let (Ok(tx), Ok(tz)) = (tx_s.parse::<i32>(), tz_s.parse::<i32>())
{
tiles.insert((tx, tz), tile); tiles.insert((tx, tz), tile);
} }
} }
}
Ok(Self { Ok(Self {
name: dim.name, name: dim.name,
tiles, tiles,

View file

@ -2,48 +2,96 @@ use super::{DataSize, Layer};
pub struct CavesLayer; pub struct CavesLayer;
impl Layer for CavesLayer { impl Layer for CavesLayer {
fn id(&self) -> &'static str { "caves" } fn id(&self) -> &'static str {
fn name(&self) -> &'static str { "Caves" } "caves"
fn data_size(&self) -> DataSize { DataSize::Nibble } }
fn priority(&self) -> i32 { 10 } fn name(&self) -> &'static str {
"Caves"
}
fn data_size(&self) -> DataSize {
DataSize::Nibble
}
fn priority(&self) -> i32 {
10
}
} }
pub struct RiverLayer; pub struct RiverLayer;
impl Layer for RiverLayer { impl Layer for RiverLayer {
fn id(&self) -> &'static str { "river" } fn id(&self) -> &'static str {
fn name(&self) -> &'static str { "River" } "river"
fn data_size(&self) -> DataSize { DataSize::Byte } }
fn priority(&self) -> i32 { 20 } fn name(&self) -> &'static str {
"River"
}
fn data_size(&self) -> DataSize {
DataSize::Byte
}
fn priority(&self) -> i32 {
20
}
} }
pub struct FrostLayer; pub struct FrostLayer;
impl Layer for FrostLayer { impl Layer for FrostLayer {
fn id(&self) -> &'static str { "frost" } fn id(&self) -> &'static str {
fn name(&self) -> &'static str { "Frost" } "frost"
fn data_size(&self) -> DataSize { DataSize::Bit } }
fn priority(&self) -> i32 { 30 } fn name(&self) -> &'static str {
"Frost"
}
fn data_size(&self) -> DataSize {
DataSize::Bit
}
fn priority(&self) -> i32 {
30
}
} }
pub struct TreesLayer; pub struct TreesLayer;
impl Layer for TreesLayer { impl Layer for TreesLayer {
fn id(&self) -> &'static str { "trees" } fn id(&self) -> &'static str {
fn name(&self) -> &'static str { "Trees" } "trees"
fn data_size(&self) -> DataSize { DataSize::Byte } }
fn priority(&self) -> i32 { 40 } fn name(&self) -> &'static str {
"Trees"
}
fn data_size(&self) -> DataSize {
DataSize::Byte
}
fn priority(&self) -> i32 {
40
}
} }
pub struct BiomeLayer; pub struct BiomeLayer;
impl Layer for BiomeLayer { impl Layer for BiomeLayer {
fn id(&self) -> &'static str { "biome" } fn id(&self) -> &'static str {
fn name(&self) -> &'static str { "Biome" } "biome"
fn data_size(&self) -> DataSize { DataSize::Byte } }
fn priority(&self) -> i32 { 50 } fn name(&self) -> &'static str {
"Biome"
}
fn data_size(&self) -> DataSize {
DataSize::Byte
}
fn priority(&self) -> i32 {
50
}
} }
pub struct ResourcesLayer; pub struct ResourcesLayer;
impl Layer for ResourcesLayer { impl Layer for ResourcesLayer {
fn id(&self) -> &'static str { "resources" } fn id(&self) -> &'static str {
fn name(&self) -> &'static str { "Resources" } "resources"
fn data_size(&self) -> DataSize { DataSize::Nibble } }
fn priority(&self) -> i32 { 60 } fn name(&self) -> &'static str {
"Resources"
}
fn data_size(&self) -> DataSize {
DataSize::Nibble
}
fn priority(&self) -> i32 {
60
}
} }

View file

@ -71,12 +71,18 @@ impl Tile {
} }
/// Ensure a layer buffer exists, creating an empty one if needed. /// 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 { pub fn ensure_layer(
&mut self,
layer_id: u32,
data_size: crate::model::layers::DataSize,
) -> &mut LayerBuffer {
use crate::model::layers::DataSize; use crate::model::layers::DataSize;
let total = TILE_SIZE * TILE_SIZE; let total = TILE_SIZE * TILE_SIZE;
self.layer_data.entry(layer_id).or_insert_with(|| match data_size { self.layer_data
DataSize::Bit => LayerBuffer::Bit(vec![0u64; (total + 63) / 64]), .entry(layer_id)
DataSize::Nibble => LayerBuffer::Nibble(vec![0u8; (total + 1) / 2]), .or_insert_with(|| match data_size {
DataSize::Bit => LayerBuffer::Bit(vec![0u64; total.div_ceil(64)]),
DataSize::Nibble => LayerBuffer::Nibble(vec![0u8; total.div_ceil(2)]),
DataSize::Byte => LayerBuffer::Byte(vec![0u8; total]), DataSize::Byte => LayerBuffer::Byte(vec![0u8; total]),
DataSize::Int => LayerBuffer::Int(vec![0i32; total]), DataSize::Int => LayerBuffer::Int(vec![0i32; total]),
}) })

View file

@ -4,7 +4,7 @@ use std::sync::OnceLock;
use crate::model::dimension::Dimension; use crate::model::dimension::Dimension;
use crate::model::terrain::Terrain; use crate::model::terrain::Terrain;
use crate::model::tile::{Tile, TILE_SIZE}; use crate::model::tile::{TILE_SIZE, Tile};
use crate::ops::operations::{Operation, OperationError, RestoreHeightsOperation}; use crate::ops::operations::{Operation, OperationError, RestoreHeightsOperation};
/// Determines whether a cell should be modified by an operation. /// Determines whether a cell should be modified by an operation.
@ -150,14 +150,15 @@ impl Operation for FilteredOperation {
// Phase 1: snapshot all cells in brush area (if first apply) // Phase 1: snapshot all cells in brush area (if first apply)
let snapshot: Vec<(usize, i16)> = if is_first_apply { let snapshot: Vec<(usize, i16)> = if is_first_apply {
let tile = dim.tiles.get(&(self.tile_x, self.tile_z)).ok_or( let tile =
OperationError::OutOfBounds { dim.tiles
.get(&(self.tile_x, self.tile_z))
.ok_or(OperationError::OutOfBounds {
tx: self.tile_x, tx: self.tile_x,
tz: self.tile_z, tz: self.tile_z,
x: 0, x: 0,
z: 0, z: 0,
}, })?;
)?;
let r = self.radius as i32; let r = self.radius as i32;
let cx = self.center_x as i32; let cx = self.center_x as i32;
@ -178,24 +179,22 @@ impl Operation for FilteredOperation {
let _ = self.before_snapshot.set(snap.clone()); let _ = self.before_snapshot.set(snap.clone());
snap snap
} else { } else {
self.before_snapshot self.before_snapshot.get().cloned().unwrap_or_default()
.get()
.cloned()
.unwrap_or_default()
}; };
// Phase 2: apply inner operation // Phase 2: apply inner operation
self.operation.apply(dim)?; self.operation.apply(dim)?;
// Phase 3: restore cells that don't pass the filter // Phase 3: restore cells that don't pass the filter
let tile = dim.tiles.get_mut(&(self.tile_x, self.tile_z)).ok_or( let tile =
OperationError::OutOfBounds { dim.tiles
.get_mut(&(self.tile_x, self.tile_z))
.ok_or(OperationError::OutOfBounds {
tx: self.tile_x, tx: self.tile_x,
tz: self.tile_z, tz: self.tile_z,
x: 0, x: 0,
z: 0, z: 0,
}, })?;
)?;
for &(idx, original_h) in &snapshot { for &(idx, original_h) in &snapshot {
let lx = idx % TILE_SIZE; let lx = idx % TILE_SIZE;

View file

@ -67,7 +67,7 @@ impl HeightMapSource for NoiseHeightMap {
let world_z = (tile.z as f64 * tile_size as f64 + lz as f64) * self.scale_z; let world_z = (tile.z as f64 * tile_size as f64 + lz as f64) * self.scale_z;
let n = noise_gen.get([world_x * self.frequency, world_z * self.frequency]); let n = noise_gen.get([world_x * self.frequency, world_z * self.frequency]);
let height = (self.base_height + n as f64 * self.amplitude) let height = (self.base_height + n * self.amplitude)
.round() .round()
.clamp(tile.min_height as f64, tile.max_height as f64) .clamp(tile.min_height as f64, tile.max_height as f64)
as i16; as i16;
@ -108,16 +108,16 @@ impl HeightMapSource for CombinedHeightMap {
let mut temp_tile = Tile::new(tile.x, tile.z, tile.min_height, tile.max_height); let mut temp_tile = Tile::new(tile.x, tile.z, tile.min_height, tile.max_height);
source.generate(&mut temp_tile, seed.wrapping_add(*weight as u64 * 100)); source.generate(&mut temp_tile, seed.wrapping_add(*weight as u64 * 100));
for i in 0..16384 { for (i, acc) in accumulated_heights.iter_mut().enumerate() {
accumulated_heights[i] += temp_tile.heightmap[i] as f64 * weight; *acc += temp_tile.heightmap[i] as f64 * weight;
} }
} }
let weight_sum: f64 = self.sources.iter().map(|(_, w)| w).sum(); let weight_sum: f64 = self.sources.iter().map(|(_, w)| w).sum();
if weight_sum > 0.0 { if weight_sum > 0.0 {
for i in 0..16384 { for (i, acc) in accumulated_heights.iter().enumerate() {
let h = (accumulated_heights[i] / weight_sum) let h = (*acc / weight_sum)
.round() .round()
.clamp(tile.min_height as f64, tile.max_height as f64) .clamp(tile.min_height as f64, tile.max_height as f64)
as i16; as i16;

View file

@ -3,8 +3,8 @@
//! Each operation implements the Operation trait and supports undo. //! Each operation implements the Operation trait and supports undo.
use std::collections::VecDeque; use std::collections::VecDeque;
use std::sync::OnceLock;
use std::sync::Arc; use std::sync::Arc;
use std::sync::OnceLock;
use crate::model::dimension::Dimension; use crate::model::dimension::Dimension;
use crate::model::terrain::Terrain; use crate::model::terrain::Terrain;
@ -37,11 +37,7 @@ pub struct HeightOperation {
impl Operation for HeightOperation { impl Operation for HeightOperation {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
if self.delta >= 0 { if self.delta >= 0 { "Raise" } else { "Lower" }
"Raise"
} else {
"Lower"
}
} }
fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> { fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> {
@ -82,7 +78,7 @@ impl Operation for HeightOperation {
let change = (self.delta as f64 * strength).round() as i16; let change = (self.delta as f64 * strength).round() as i16;
let new_height = let new_height =
(tile.heightmap[idx] as i16 + change).clamp(tile.min_height, tile.max_height); (tile.heightmap[idx] + change).clamp(tile.min_height, tile.max_height);
tile.heightmap[idx] = new_height; tile.heightmap[idx] = new_height;
} }
} }
@ -95,9 +91,7 @@ impl Operation for HeightOperation {
} }
fn inverse(&self) -> Box<dyn Operation> { fn inverse(&self) -> Box<dyn Operation> {
let snapshot = self.before_snapshot.get() let snapshot = self.before_snapshot.get().cloned().unwrap_or_default();
.cloned()
.unwrap_or_default();
Box::new(RestoreHeightsOperation { Box::new(RestoreHeightsOperation {
tile_x: self.tile_x, tile_x: self.tile_x,
tile_z: self.tile_z, tile_z: self.tile_z,
@ -179,9 +173,7 @@ impl Operation for FlattenOperation {
} }
fn inverse(&self) -> Box<dyn Operation> { fn inverse(&self) -> Box<dyn Operation> {
let snapshot = self.before_snapshot.get() let snapshot = self.before_snapshot.get().cloned().unwrap_or_default();
.cloned()
.unwrap_or_default();
Box::new(RestoreHeightsOperation { Box::new(RestoreHeightsOperation {
tile_x: self.tile_x, tile_x: self.tile_x,
tile_z: self.tile_z, tile_z: self.tile_z,
@ -250,9 +242,7 @@ impl Operation for PaintOperation {
} }
fn inverse(&self) -> Box<dyn Operation> { fn inverse(&self) -> Box<dyn Operation> {
let snapshot = self.before_snapshot.get() let snapshot = self.before_snapshot.get().cloned().unwrap_or_default();
.cloned()
.unwrap_or_default();
Box::new(RestoreTerrainOperation { Box::new(RestoreTerrainOperation {
tile_x: self.tile_x, tile_x: self.tile_x,
tile_z: self.tile_z, tile_z: self.tile_z,
@ -414,8 +404,8 @@ impl Operation for SmoothOperation {
let bz = az + ny; let bz = az + ny;
if bx >= 0 && bx < TILE_SIZE as i32 && bz >= 0 && bz < TILE_SIZE as i32 if bx >= 0 && bx < TILE_SIZE as i32 && bz >= 0 && bz < TILE_SIZE as i32
{ {
sum += sum += tile.heightmap[(bz as usize) * TILE_SIZE + (bx as usize)]
tile.heightmap[(bz as usize) * TILE_SIZE + (bx as usize)] as i32; as i32;
count += 1; count += 1;
} }
} }
@ -463,8 +453,8 @@ impl Operation for ErodeOperation {
} }
fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> { fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> {
let tile = dim let tile =
.tiles dim.tiles
.get_mut(&(self.tile_x, self.tile_z)) .get_mut(&(self.tile_x, self.tile_z))
.ok_or(OperationError::OutOfBounds { .ok_or(OperationError::OutOfBounds {
tx: self.tile_x, tx: self.tile_x,
@ -525,8 +515,7 @@ impl Operation for ErodeOperation {
for (ndx, ndz) in [(0, -1), (0, 1), (-1, 0), (1, 0)] { for (ndx, ndz) in [(0, -1), (0, 1), (-1, 0), (1, 0)] {
let bx = ax + ndx; let bx = ax + ndx;
let bz = az + ndz; let bz = az + ndz;
if bx < 0 || bx >= tile_size as i32 || bz < 0 || bz >= tile_size as i32 if bx < 0 || bx >= tile_size as i32 || bz < 0 || bz >= tile_size as i32 {
{
continue; continue;
} }
let n_dx = bx - cx; let n_dx = bx - cx;
@ -605,8 +594,8 @@ impl Operation for FillOperation {
} }
fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> { fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> {
let tile = dim let tile =
.tiles dim.tiles
.get_mut(&(self.tile_x, self.tile_z)) .get_mut(&(self.tile_x, self.tile_z))
.ok_or(OperationError::OutOfBounds { .ok_or(OperationError::OutOfBounds {
tx: self.tile_x, tx: self.tile_x,
@ -697,8 +686,8 @@ impl Operation for FloodOperation {
} }
fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> { fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> {
let tile = dim let tile =
.tiles dim.tiles
.get_mut(&(self.tile_x, self.tile_z)) .get_mut(&(self.tile_x, self.tile_z))
.ok_or(OperationError::OutOfBounds { .ok_or(OperationError::OutOfBounds {
tx: self.tile_x, tx: self.tile_x,
@ -765,8 +754,8 @@ impl Operation for PencilOperation {
} }
fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> { fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> {
let tile = dim let tile =
.tiles dim.tiles
.get_mut(&(self.tile_x, self.tile_z)) .get_mut(&(self.tile_x, self.tile_z))
.ok_or(OperationError::OutOfBounds { .ok_or(OperationError::OutOfBounds {
tx: self.tile_x, tx: self.tile_x,

View file

@ -8,6 +8,12 @@ pub struct PluginRegistry {
pub sources: Vec<Box<dyn TileSourcePlugin>>, pub sources: Vec<Box<dyn TileSourcePlugin>>,
} }
impl Default for PluginRegistry {
fn default() -> Self {
Self::new()
}
}
impl PluginRegistry { impl PluginRegistry {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@ -52,22 +58,34 @@ impl PluginRegistry {
} }
/// Find a layer plugin by name. /// Find a layer plugin by name.
pub fn find_layer_by_name(&self, name: &str) -> Option<&Box<dyn LayerPlugin>> { pub fn find_layer_by_name(&self, name: &str) -> Option<&dyn LayerPlugin> {
self.layers.iter().find(|p| p.name() == name) self.layers
.iter()
.find(|p| p.name() == name)
.map(Box::as_ref)
} }
/// Find an operation plugin by name. /// Find an operation plugin by name.
pub fn find_operation_by_name(&self, name: &str) -> Option<&Box<dyn OperationPlugin>> { pub fn find_operation_by_name(&self, name: &str) -> Option<&dyn OperationPlugin> {
self.operations.iter().find(|p| p.name() == name) self.operations
.iter()
.find(|p| p.name() == name)
.map(Box::as_ref)
} }
/// Find an export plugin by name. /// Find an export plugin by name.
pub fn find_export_by_name(&self, name: &str) -> Option<&Box<dyn ExportPlugin>> { pub fn find_export_by_name(&self, name: &str) -> Option<&dyn ExportPlugin> {
self.exports.iter().find(|p| p.name() == name) self.exports
.iter()
.find(|p| p.name() == name)
.map(Box::as_ref)
} }
/// Find a tile source plugin by name. /// Find a tile source plugin by name.
pub fn find_source_by_name(&self, name: &str) -> Option<&Box<dyn TileSourcePlugin>> { pub fn find_source_by_name(&self, name: &str) -> Option<&dyn TileSourcePlugin> {
self.sources.iter().find(|p| p.name() == name) self.sources
.iter()
.find(|p| p.name() == name)
.map(Box::as_ref)
} }
} }

View file

@ -8,9 +8,9 @@ use std::collections::HashMap;
use terrafier_core::io::minecraft::load_save; use terrafier_core::io::minecraft::load_save;
use terrafier_fastanvil::io::chunk::Chunk; use terrafier_fastanvil::io::chunk::Chunk;
use terrafier_fastanvil::io::region::Region; use terrafier_fastanvil::io::region::Region;
use terrafier_nbt::Tag;
use terrafier_nbt::io::reader::read_gzip; use terrafier_nbt::io::reader::read_gzip;
use terrafier_nbt::io::writer::to_gzip_bytes; use terrafier_nbt::io::writer::to_gzip_bytes;
use terrafier_nbt::Tag;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Helpers: synthetic NBT and region file writer // Helpers: synthetic NBT and region file writer
@ -48,10 +48,7 @@ fn make_chunk_nbt_gzip(cx: i32, cz: i32) -> Vec<u8> {
// block_states with a single-entry palette // block_states with a single-entry palette
let mut palette_list: Vec<Tag> = Vec::new(); let mut palette_list: Vec<Tag> = Vec::new();
let mut grass = HashMap::new(); let mut grass = HashMap::new();
grass.insert( grass.insert("Name".into(), Tag::String("minecraft:grass_block".into()));
"Name".into(),
Tag::String("minecraft:grass_block".into()),
);
palette_list.push(Tag::Compound(grass)); palette_list.push(Tag::Compound(grass));
let mut block_states = HashMap::new(); let mut block_states = HashMap::new();
block_states.insert("palette".into(), Tag::List(palette_list)); block_states.insert("palette".into(), Tag::List(palette_list));
@ -164,7 +161,9 @@ fn test_golden_mca_roundtrip() {
let coords = region.chunk_coords(); let coords = region.chunk_coords();
assert_eq!(coords, vec![(0, 0)]); assert_eq!(coords, vec![(0, 0)]);
let raw = region.get_chunk_data(0, 0).expect("chunk (0,0) should exist"); let raw = region
.get_chunk_data(0, 0)
.expect("chunk (0,0) should exist");
assert!(!raw.is_empty(), "chunk data should not be empty"); assert!(!raw.is_empty(), "chunk data should not be empty");
// The stored payload is gzip-compressed NBT → decompress via read_gzip // The stored payload is gzip-compressed NBT → decompress via read_gzip
@ -175,11 +174,7 @@ fn test_golden_mca_roundtrip() {
assert_eq!(chunk.x, 0, "chunk xPos"); assert_eq!(chunk.x, 0, "chunk xPos");
assert_eq!(chunk.z, 0, "chunk zPos"); assert_eq!(chunk.z, 0, "chunk zPos");
assert_eq!(chunk.data_version, 3954, "chunk DataVersion"); assert_eq!(chunk.data_version, 3954, "chunk DataVersion");
assert_eq!( assert_eq!(chunk.status.as_deref(), Some("full"), "chunk Status");
chunk.status.as_deref(),
Some("full"),
"chunk Status"
);
// Sections // Sections
assert_eq!(chunk.sections.len(), 1, "should have 1 section"); assert_eq!(chunk.sections.len(), 1, "should have 1 section");
@ -211,5 +206,9 @@ fn test_golden_mca_roundtrip() {
// --- 4. Region round-trip via to_bytes / from_bytes --- // --- 4. Region round-trip via to_bytes / from_bytes ---
let reencoded = region.to_bytes().expect("region should re-serialize"); let reencoded = region.to_bytes().expect("region should re-serialize");
let region2 = Region::from_bytes(0, 0, &reencoded).expect("re-serialized region should parse"); let region2 = Region::from_bytes(0, 0, &reencoded).expect("re-serialized region should parse");
assert_eq!(region2.chunk_count(), 1, "round-tripped region should still have 1 chunk"); assert_eq!(
region2.chunk_count(),
1,
"round-tripped region should still have 1 chunk"
);
} }

View file

@ -46,17 +46,17 @@ impl Region {
let mut timestamps = [0u32; 1024]; let mut timestamps = [0u32; 1024];
// Read location table (first 4096 bytes: 1024 entries x 4 bytes) // Read location table (first 4096 bytes: 1024 entries x 4 bytes)
for i in 0..1024 { for loc in locations.iter_mut() {
let mut buf = [0u8; 4]; let mut buf = [0u8; 4];
reader.read_exact(&mut buf)?; reader.read_exact(&mut buf)?;
locations[i] = u32::from_be_bytes(buf); *loc = u32::from_be_bytes(buf);
} }
// Read timestamp table (second 4096 bytes: 1024 entries x 4 bytes) // Read timestamp table (second 4096 bytes: 1024 entries x 4 bytes)
for i in 0..1024 { for ts in timestamps.iter_mut() {
let mut buf = [0u8; 4]; let mut buf = [0u8; 4];
reader.read_exact(&mut buf)?; reader.read_exact(&mut buf)?;
timestamps[i] = u32::from_be_bytes(buf); *ts = u32::from_be_bytes(buf);
} }
let mut chunks = HashMap::new(); let mut chunks = HashMap::new();
@ -111,7 +111,7 @@ impl Region {
(local_x, local_z), (local_x, local_z),
ChunkEntry { ChunkEntry {
offset: sector_offset, offset: sector_offset,
size: sector_count as u32, size: sector_count,
timestamp, timestamp,
data: Some(decompressed), data: Some(decompressed),
}, },
@ -140,12 +140,15 @@ impl Region {
/// Set chunk data at local coordinates (0..32, 0..32). /// Set chunk data at local coordinates (0..32, 0..32).
/// `data` should be decompressed NBT bytes. /// `data` should be decompressed NBT bytes.
pub fn set_chunk_data(&mut self, local_x: u8, local_z: u8, data: Vec<u8>) { pub fn set_chunk_data(&mut self, local_x: u8, local_z: u8, data: Vec<u8>) {
self.chunks.insert((local_x, local_z), ChunkEntry { self.chunks.insert(
(local_x, local_z),
ChunkEntry {
offset: 0, offset: 0,
size: 0, size: 0,
timestamp: 0, timestamp: 0,
data: Some(data), data: Some(data),
}); },
);
} }
/// List all chunk coordinates present in this region. /// List all chunk coordinates present in this region.

View file

@ -84,6 +84,12 @@ pub struct BlockPalette {
index_map: HashMap<BlockState, u32>, index_map: HashMap<BlockState, u32>,
} }
impl Default for BlockPalette {
fn default() -> Self {
Self::new()
}
}
impl BlockPalette { impl BlockPalette {
pub fn new() -> Self { pub fn new() -> Self {
BlockPalette { BlockPalette {

View file

@ -10,6 +10,12 @@ pub struct SectionData {
pub storage: BitArray, pub storage: BitArray,
} }
impl Default for SectionData {
fn default() -> Self {
Self::new()
}
}
impl SectionData { impl SectionData {
/// Create a new section filled with air. /// Create a new section filled with air.
pub fn new() -> Self { pub fn new() -> Self {

View file

@ -1,8 +1,8 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use terrafier_core::World;
use terrafier_core::model::terrain::Terrain; use terrafier_core::model::terrain::Terrain;
use terrafier_core::ops::operations::Operation; use terrafier_core::ops::operations::Operation;
use terrafier_core::World;
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq)]
pub enum ToolMode { pub enum ToolMode {
@ -41,8 +41,6 @@ pub struct TerrafierApp {
pub brush_local_x: Option<u32>, pub brush_local_x: Option<u32>,
pub brush_local_z: Option<u32>, pub brush_local_z: Option<u32>,
pub show_heightmap: bool, pub show_heightmap: bool,
pub zoom: f32,
pub view_offset: (f32, f32),
pub show_new_world: bool, pub show_new_world: bool,
pub show_export: bool, pub show_export: bool,
pub status_message: String, pub status_message: String,
@ -67,8 +65,6 @@ impl TerrafierApp {
brush_local_x: None, brush_local_x: None,
brush_local_z: None, brush_local_z: None,
show_heightmap: false, show_heightmap: false,
zoom: 1.0,
view_offset: (0.0, 0.0),
show_new_world: false, show_new_world: false,
show_export: false, show_export: false,
status_message: "Ready".to_string(), status_message: "Ready".to_string(),
@ -129,8 +125,9 @@ impl eframe::App for TerrafierApp {
if ui.button("New World").clicked() { if ui.button("New World").clicked() {
self.show_new_world = true; self.show_new_world = true;
} }
if ui.button("Open").clicked() { if ui.button("Open").clicked()
if let Some(path) = rfd::FileDialog::new().pick_folder() { && let Some(path) = rfd::FileDialog::new().pick_folder()
{
match terrafier_core::io::import::import(&path) { match terrafier_core::io::import::import(&path) {
Ok(world) => { Ok(world) => {
self.world = Some(world); self.world = Some(world);
@ -144,7 +141,6 @@ impl eframe::App for TerrafierApp {
} }
} }
} }
}
if ui.button("Export").clicked() { if ui.button("Export").clicked() {
self.show_export = true; self.show_export = true;
} }

View file

@ -1,12 +1,12 @@
use std::sync::Arc; use std::sync::Arc;
use terrafier_core::Terrain;
use terrafier_core::model::brush::SymmetricBrush; use terrafier_core::model::brush::SymmetricBrush;
use terrafier_core::model::tile::TILE_SIZE; use terrafier_core::model::tile::TILE_SIZE;
use terrafier_core::ops::operations::{ use terrafier_core::ops::operations::{
FlattenOperation, HeightOperation, MultiTileOperation, Operation, PaintOperation, FlattenOperation, HeightOperation, MultiTileOperation, Operation, PaintOperation,
SmoothOperation, SmoothOperation,
}; };
use terrafier_core::Terrain;
use crate::app::{TerrafierApp, ToolMode}; use crate::app::{TerrafierApp, ToolMode};
@ -103,8 +103,10 @@ fn apply_tool(app: &mut TerrafierApp) {
for otx in min_tx..=max_tx { for otx in min_tx..=max_tx {
for otz in min_tz..=max_tz { for otz in min_tz..=max_tz {
let local_cx = (global_cx - otx * TILE_SIZE as i32).clamp(0, TILE_SIZE as i32 - 1) as u32; let local_cx =
let local_cz = (global_cz - otz * TILE_SIZE as i32).clamp(0, TILE_SIZE as i32 - 1) as u32; (global_cx - otx * TILE_SIZE as i32).clamp(0, TILE_SIZE as i32 - 1) as u32;
let local_cz =
(global_cz - otz * TILE_SIZE as i32).clamp(0, TILE_SIZE as i32 - 1) as u32;
if dim.tiles.contains_key(&(otx, otz)) { if dim.tiles.contains_key(&(otx, otz)) {
let brush = Arc::new(SymmetricBrush::new(app.brush_radius as f64)); let brush = Arc::new(SymmetricBrush::new(app.brush_radius as f64));

View file

@ -107,8 +107,12 @@ pub fn show_viewport(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) {
); );
// Draw selection border // Draw selection border
if let Some((sel_tx, sel_tz)) = app.selected_tile { if let Some((sel_tx, sel_tz)) = app.selected_tile
if sel_tx >= min_tx && sel_tx <= max_tx && sel_tz >= min_tz && sel_tz <= max_tz { && sel_tx >= min_tx
&& sel_tx <= max_tx
&& sel_tz >= min_tz
&& sel_tz <= max_tz
{
let bx = rect.min.x + (sel_tx - min_tx) as f32 * display_size as f32; let bx = rect.min.x + (sel_tx - min_tx) as f32 * display_size as f32;
let bz = rect.min.y + (sel_tz - min_tz) as f32 * display_size as f32; let bz = rect.min.y + (sel_tz - min_tz) as f32 * display_size as f32;
let border_rect = let border_rect =
@ -120,14 +124,17 @@ pub fn show_viewport(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) {
egui::StrokeKind::Middle, egui::StrokeKind::Middle,
); );
} }
}
// Draw brush position marker // Draw brush position marker
if let (Some((sel_tx, sel_tz)), Some(bx), Some(bz)) = (app.selected_tile, app.brush_local_x, app.brush_local_z) { if let (Some((sel_tx, sel_tz)), Some(bx), Some(bz)) =
(app.selected_tile, app.brush_local_x, app.brush_local_z)
{
let tile_x_in_pixels = (sel_tx - min_tx) as f32 * display_size as f32; let tile_x_in_pixels = (sel_tx - min_tx) as f32 * display_size as f32;
let tile_z_in_pixels = (sel_tz - min_tz) as f32 * display_size as f32; let tile_z_in_pixels = (sel_tz - min_tz) as f32 * display_size as f32;
let brush_x = rect.min.x + tile_x_in_pixels + (bx as f32 * display_size as f32 / TILE_SIZE as f32); let brush_x =
let brush_z = rect.min.y + tile_z_in_pixels + (bz as f32 * display_size as f32 / TILE_SIZE as f32); rect.min.x + tile_x_in_pixels + (bx as f32 * display_size as f32 / TILE_SIZE as f32);
let brush_z =
rect.min.y + tile_z_in_pixels + (bz as f32 * display_size as f32 / TILE_SIZE as f32);
let brush_radius_px = app.brush_radius as f32 * display_size as f32 / TILE_SIZE as f32; let brush_radius_px = app.brush_radius as f32 * display_size as f32 / TILE_SIZE as f32;
painter.circle_stroke( painter.circle_stroke(
egui::pos2(brush_x, brush_z), egui::pos2(brush_x, brush_z),
@ -137,8 +144,9 @@ pub fn show_viewport(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) {
} }
// Handle click to select tile // Handle click to select tile
if response.clicked() { if response.clicked()
if let Some(pos) = response.interact_pointer_pos() { && let Some(pos) = response.interact_pointer_pos()
{
let lx = pos.x - rect.min.x; let lx = pos.x - rect.min.x;
let lz = pos.y - rect.min.y; let lz = pos.y - rect.min.y;
if lx >= 0.0 && lz >= 0.0 { if lx >= 0.0 && lz >= 0.0 {
@ -146,12 +154,16 @@ pub fn show_viewport(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) {
let tz = (lz / display_size as f32).floor() as i32 + min_tz; let tz = (lz / display_size as f32).floor() as i32 + min_tz;
if dim.tiles.contains_key(&(tx, tz)) { if dim.tiles.contains_key(&(tx, tz)) {
app.selected_tile = Some((tx, tz)); app.selected_tile = Some((tx, tz));
let local_x = ((lx as u32 % display_size) * TILE_SIZE as u32 / display_size).min(127); let local_x =
let local_z = ((lz as u32 % display_size) * TILE_SIZE as u32 / display_size).min(127); ((lx as u32 % display_size) * TILE_SIZE as u32 / display_size).min(127);
let local_z =
((lz as u32 % display_size) * TILE_SIZE as u32 / display_size).min(127);
app.brush_local_x = Some(local_x); app.brush_local_x = Some(local_x);
app.brush_local_z = Some(local_z); app.brush_local_z = Some(local_z);
app.status_message = format!("Selected tile ({}, {}) at local ({}, {})", tx, tz, local_x, local_z); app.status_message = format!(
} "Selected tile ({}, {}) at local ({}, {})",
tx, tz, local_x, local_z
);
} }
} }
} }