Fix clippy warnings: needless_range_loop, collapsible_if, borrowed_box, type_complexity, dead code cleanup
This commit is contained in:
parent
fa05187f1c
commit
3d3798136d
22 changed files with 579 additions and 428 deletions
|
|
@ -1,6 +1,6 @@
|
|||
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::{reader, writer};
|
||||
|
|
|
|||
|
|
@ -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::model::tile::Tile;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
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::model::brush::SymmetricBrush;
|
||||
use terrafier_core::model::terrain::Terrain;
|
||||
use terrafier_core::model::world::World;
|
||||
use terrafier_core::ops::operations::{
|
||||
HeightOperation, Operation, PaintOperation,
|
||||
};
|
||||
use terrafier_core::ops::operations::{HeightOperation, Operation, PaintOperation};
|
||||
|
||||
fn bench_world_new(c: &mut Criterion) {
|
||||
c.bench_function("world/new_default", |b| {
|
||||
|
|
|
|||
|
|
@ -23,8 +23,7 @@ pub type Result<T> = std::result::Result<T, BinaryError>;
|
|||
|
||||
/// Save a `World` to a binary file.
|
||||
pub fn save_binary(world: &World, path: &Path) -> Result<()> {
|
||||
let bytes = bincode::serialize(world)
|
||||
.map_err(|e| BinaryError::Serialize(e.to_string()))?;
|
||||
let bytes = bincode::serialize(world).map_err(|e| BinaryError::Serialize(e.to_string()))?;
|
||||
|
||||
std::fs::write(path, &bytes)?;
|
||||
Ok(())
|
||||
|
|
@ -33,8 +32,8 @@ pub fn save_binary(world: &World, path: &Path) -> Result<()> {
|
|||
/// Load a `World` from a binary file.
|
||||
pub fn load_binary(path: &Path) -> Result<World> {
|
||||
let bytes = std::fs::read(path)?;
|
||||
let world: World = bincode::deserialize(&bytes)
|
||||
.map_err(|e| BinaryError::Deserialize(e.to_string()))?;
|
||||
let world: World =
|
||||
bincode::deserialize(&bytes).map_err(|e| BinaryError::Deserialize(e.to_string()))?;
|
||||
Ok(world)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ pub fn validate_save(path: &Path) -> Result<()> {
|
|||
let region_count = std::fs::read_dir(®ion_dir)
|
||||
.map_err(|e| ImportError::ValidationFailed(e.to_string()))?
|
||||
.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();
|
||||
|
||||
if region_count == 0 {
|
||||
|
|
@ -67,11 +67,9 @@ pub fn validate_save(path: &Path) -> Result<()> {
|
|||
pub fn import(path: &Path) -> Result<World> {
|
||||
let path_str = path.display().to_string();
|
||||
|
||||
if path.is_dir() {
|
||||
if path.join("level.dat").exists() && path.join("region").is_dir() {
|
||||
if path.is_dir() && path.join("level.dat").exists() && path.join("region").is_dir() {
|
||||
return import_minecraft_save(path);
|
||||
}
|
||||
}
|
||||
|
||||
if wp_import::is_world_file(path) {
|
||||
return wp_import::import_world_file(path)
|
||||
|
|
|
|||
|
|
@ -84,12 +84,12 @@ impl LayerExport for CavesLayerExport {
|
|||
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
|
||||
{
|
||||
let value = if idx.is_multiple_of(2) {
|
||||
byte >> 4
|
||||
} else {
|
||||
byte & 0xF
|
||||
};
|
||||
if value > 0 && y < surface_y && y >= surface_y - 12 && y > -60 {
|
||||
match block_name {
|
||||
"minecraft:stone"
|
||||
| "minecraft:deepslate"
|
||||
|
|
@ -247,7 +247,11 @@ impl LayerExport for ResourcesLayerExport {
|
|||
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 };
|
||||
let value = if idx.is_multiple_of(2) {
|
||||
byte >> 4
|
||||
} else {
|
||||
byte & 0xF
|
||||
};
|
||||
if value > 0 && block_name == "minecraft:stone" && y > -60 {
|
||||
let ore = match value {
|
||||
1 => "minecraft:coal_ore",
|
||||
|
|
|
|||
|
|
@ -1,20 +1,23 @@
|
|||
//! 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::path::{Path, PathBuf};
|
||||
use thiserror::Error;
|
||||
use log;
|
||||
|
||||
use terrafier_fastanvil::io::region::Region;
|
||||
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::platform::Platform;
|
||||
use crate::io::layer_export::{apply_layers, biome_name, LayerExport};
|
||||
use crate::model::tile::Tile;
|
||||
use crate::model::world::World;
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
type RegionTiles<'a> = BTreeMap<(i32, i32), Vec<(&'a (i32, i32), &'a Tile)>>;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MinecraftIOError {
|
||||
#[error("IO error: {0}")]
|
||||
|
|
@ -49,14 +52,17 @@ pub fn version_from_data_version(dv: i32) -> Option<Platform> {
|
|||
max_height: 320,
|
||||
}),
|
||||
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 {
|
||||
id: "java_1_18".into(),
|
||||
display_name: "Minecraft Java 1.18".into(),
|
||||
min_height: -64,
|
||||
max_height: 320,
|
||||
})
|
||||
},
|
||||
}
|
||||
2975..=3117 => Some(Platform {
|
||||
id: "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,
|
||||
}),
|
||||
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 {
|
||||
id: "java_1_19".into(),
|
||||
display_name: "Minecraft Java 1.19".into(),
|
||||
min_height: -64,
|
||||
max_height: 320,
|
||||
})
|
||||
},
|
||||
}
|
||||
3337..=3460 => Some(Platform {
|
||||
id: "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,
|
||||
}),
|
||||
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 {
|
||||
id: "java_1_20".into(),
|
||||
display_name: "Minecraft Java 1.20 (fallback)".into(),
|
||||
min_height: -64,
|
||||
max_height: 320,
|
||||
})
|
||||
},
|
||||
}
|
||||
3578..=3700 => Some(Platform {
|
||||
id: "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,
|
||||
}),
|
||||
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 {
|
||||
id: "java_1_20_5".into(),
|
||||
display_name: "Minecraft Java 1.20.5 (fallback)".into(),
|
||||
min_height: -64,
|
||||
max_height: 320,
|
||||
})
|
||||
},
|
||||
}
|
||||
3819..=3953 => Some(Platform {
|
||||
id: "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 {
|
||||
let entry = entry?;
|
||||
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 parts: Vec<&str> = file_name.split('.').collect();
|
||||
if parts.len() >= 3 {
|
||||
if let (Ok(rx), Ok(rz)) = (parts[1].parse::<i32>(), parts[2].parse::<i32>()) {
|
||||
if parts.len() >= 3
|
||||
&& let (Ok(rx), Ok(rz)) = (parts[1].parse::<i32>(), parts[2].parse::<i32>())
|
||||
{
|
||||
let region_bytes = fs::read(&path)?;
|
||||
let region = Region::from_bytes(rx, rz, ®ion_bytes)?;
|
||||
for (local_x, local_z) in region.chunk_coords() {
|
||||
let chunk_data = region.get_chunk_data(local_x, local_z).unwrap();
|
||||
if let Ok(chunk_tag) = read_gzip(chunk_data) {
|
||||
if let Some(chunk) =
|
||||
if let Ok(chunk_tag) = read_gzip(chunk_data)
|
||||
&& let Some(chunk) =
|
||||
terrafier_fastanvil::io::chunk::Chunk::from_nbt(&chunk_tag)
|
||||
{
|
||||
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_z = (chunk.z & 7) as usize;
|
||||
|
||||
let tile = tiles
|
||||
.entry((tile_x, tile_z))
|
||||
.or_insert_with(|| Tile::new(
|
||||
tile_x,
|
||||
tile_z,
|
||||
platform.min_height,
|
||||
platform.max_height,
|
||||
));
|
||||
let tile = tiles.entry((tile_x, tile_z)).or_insert_with(|| {
|
||||
Tile::new(tile_x, tile_z, platform.min_height, platform.max_height)
|
||||
});
|
||||
|
||||
for lx in 0..16usize {
|
||||
for lz in 0..16usize {
|
||||
let mut surface_y = None;
|
||||
|
||||
if !chunk.sections.is_empty() {
|
||||
let mut sorted: Vec<_> =
|
||||
chunk.sections.iter().collect();
|
||||
sorted
|
||||
.sort_by(|a, b| b.section_y.cmp(&a.section_y));
|
||||
let mut sorted: Vec<_> = chunk.sections.iter().collect();
|
||||
sorted.sort_by_key(|b| std::cmp::Reverse(b.section_y));
|
||||
|
||||
for section in &sorted {
|
||||
if section.palette.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let has_blocks =
|
||||
section.palette.iter().any(|p| {
|
||||
p.get("Name").map_or(false, |n| {
|
||||
let has_blocks = section.palette.iter().any(|p| {
|
||||
p.get("Name").is_some_and(|n| {
|
||||
matches!(
|
||||
n,
|
||||
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.palette[0]
|
||||
.get("Name")
|
||||
.map_or(false, |n| {
|
||||
if section.palette[0].get("Name").is_some_and(|n| {
|
||||
matches!(
|
||||
n,
|
||||
terrafier_nbt::Tag::String(s)
|
||||
if s == "minecraft:air"
|
||||
)
|
||||
})
|
||||
{
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
surface_y = Some(
|
||||
(section.section_y as i32) * 16 + 15,
|
||||
);
|
||||
surface_y = Some((section.section_y as i32) * 16 + 15);
|
||||
break;
|
||||
}
|
||||
|
||||
surface_y = Some(
|
||||
(section.section_y as i32) * 16 + 15,
|
||||
);
|
||||
surface_y = Some((section.section_y as i32) * 16 + 15);
|
||||
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_z = chunk_local_z * 16 + lz;
|
||||
|
||||
if tile_local_x < 128 && tile_local_z < 128 {
|
||||
if let Some(y) = surface_y {
|
||||
let clamped = (y as i16)
|
||||
.clamp(tile.min_height, tile.max_height);
|
||||
tile.heightmap
|
||||
[tile_local_z * 128 + tile_local_x] = clamped;
|
||||
}
|
||||
}
|
||||
}
|
||||
if tile_local_x < 128
|
||||
&& tile_local_z < 128
|
||||
&& let Some(y) = surface_y
|
||||
{
|
||||
let 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.
|
||||
pub fn save_world(world: &World, output_path: &Path) -> Result<()> {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fs::create_dir_all(output_path.join("region"))?;
|
||||
|
||||
// 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)
|
||||
let mut regions: BTreeMap<(i32, i32), Vec<(&(i32, i32), &Tile)>> = BTreeMap::new();
|
||||
let mut regions: RegionTiles = BTreeMap::new();
|
||||
for (key, tile) in &dim.tiles {
|
||||
let (tx, tz) = key;
|
||||
let rx = tx >> 2;
|
||||
|
|
@ -345,7 +342,7 @@ fn pack_indices(indices: &[u16], bits: usize) -> Vec<i64> {
|
|||
return Vec::new();
|
||||
}
|
||||
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 mask = (1i64 << bits) - 1;
|
||||
for (i, &idx) in indices.iter().enumerate() {
|
||||
|
|
@ -459,11 +456,14 @@ fn build_chunk_nbt(
|
|||
let bits = bits_needed(BLOCK_SET.len());
|
||||
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();
|
||||
entry.insert("Name".into(), terrafier_nbt::Tag::String(name.to_string()));
|
||||
terrafier_nbt::Tag::Compound(entry)
|
||||
}).collect();
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut block_states = HashMap::new();
|
||||
block_states.insert("palette".into(), terrafier_nbt::Tag::List(palette_tags));
|
||||
|
|
@ -471,7 +471,10 @@ fn build_chunk_nbt(
|
|||
|
||||
let biome_palette = vec![{
|
||||
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)
|
||||
}];
|
||||
let mut biomes = HashMap::new();
|
||||
|
|
@ -479,7 +482,10 @@ fn build_chunk_nbt(
|
|||
|
||||
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(
|
||||
"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));
|
||||
|
|
@ -557,7 +563,15 @@ fn build_chunk_nbt_with_layers(
|
|||
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);
|
||||
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);
|
||||
|
|
@ -585,20 +599,26 @@ fn build_chunk_nbt_with_layers(
|
|||
for (pi, name) in used_blocks.iter().enumerate() {
|
||||
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 {
|
||||
palette_map[BLOCK_SET[i as usize]]
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}).collect();
|
||||
})
|
||||
.collect();
|
||||
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();
|
||||
entry.insert("Name".into(), terrafier_nbt::Tag::String(name.to_string()));
|
||||
terrafier_nbt::Tag::Compound(entry)
|
||||
}).collect();
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut block_states = HashMap::new();
|
||||
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();
|
||||
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));
|
||||
|
||||
sections.push(terrafier_nbt::Tag::Compound(sec_compound));
|
||||
|
|
@ -638,8 +661,6 @@ pub fn save_world_with_layers(
|
|||
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)?;
|
||||
|
|
@ -651,7 +672,7 @@ pub fn save_world_with_layers(
|
|||
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 {
|
||||
let (tx, tz) = key;
|
||||
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_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() {
|
||||
continue;
|
||||
|
|
@ -746,7 +774,7 @@ pub fn discover_region_files(world_path: &Path) -> Result<Vec<PathBuf>> {
|
|||
for entry in fs::read_dir(®ion_dir)? {
|
||||
let entry = entry?;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,6 @@ 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;
|
||||
|
|
@ -75,7 +74,6 @@ const PRIM_BOOL: u8 = b'Z';
|
|||
struct FieldInfo {
|
||||
name: String,
|
||||
type_code: u8,
|
||||
type_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -94,7 +92,7 @@ enum JvmValue {
|
|||
ByteArray(Vec<i8>),
|
||||
ShortArray(Vec<i16>),
|
||||
IntArray(Vec<i32>),
|
||||
LongArray(Vec<i64>),
|
||||
LongArray(()),
|
||||
Int(i32),
|
||||
Long(i64),
|
||||
Skipped,
|
||||
|
|
@ -106,12 +104,12 @@ impl JvmValue {
|
|||
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 {
|
||||
if let Some(val) = map.get(*key)
|
||||
&& let JvmValue::String(s) = val
|
||||
{
|
||||
return Some(s.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
|
|
@ -150,12 +148,20 @@ struct JvmStream {
|
|||
|
||||
impl JvmStream {
|
||||
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> {
|
||||
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];
|
||||
self.pos += 1;
|
||||
|
|
@ -164,7 +170,10 @@ impl JvmStream {
|
|||
|
||||
fn read_bytes(&mut self, n: usize) -> std::io::Result<Vec<u8>> {
|
||||
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();
|
||||
self.pos += n;
|
||||
|
|
@ -173,7 +182,10 @@ impl JvmStream {
|
|||
|
||||
fn peek_byte(&self) -> std::io::Result<u8> {
|
||||
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])
|
||||
}
|
||||
|
|
@ -201,8 +213,14 @@ impl JvmStream {
|
|||
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)
|
||||
Ok((b1 << 56)
|
||||
| (b2 << 48)
|
||||
| (b3 << 40)
|
||||
| (b4 << 32)
|
||||
| (b5 << 24)
|
||||
| (b6 << 16)
|
||||
| (b7 << 8)
|
||||
| b8)
|
||||
}
|
||||
|
||||
fn read_f32(&mut self) -> std::io::Result<f32> {
|
||||
|
|
@ -274,13 +292,11 @@ impl JvmStream {
|
|||
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)));
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("unexpected content token: 0x{:02x}", tc),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -336,10 +352,10 @@ impl JvmStream {
|
|||
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)))
|
||||
}
|
||||
_ => Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("expected classdesc token, got 0x{:02x}", tc),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -354,31 +370,26 @@ impl JvmStream {
|
|||
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' {
|
||||
if type_code == b'[' || type_code == b'L' {
|
||||
let tc = self.read_byte()?;
|
||||
match tc {
|
||||
TC_STRING => {
|
||||
let s = self.read_utf()?;
|
||||
Some(s)
|
||||
self.read_utf()?;
|
||||
}
|
||||
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
|
||||
let _ = self.handles[idx].as_string();
|
||||
}
|
||||
}
|
||||
TC_LONGSTRING => {
|
||||
Some(self.read_long_utf()?)
|
||||
self.read_long_utf()?;
|
||||
}
|
||||
_ => None,
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
fields.push(FieldInfo { name, type_code, type_name });
|
||||
}
|
||||
fields.push(FieldInfo { name, type_code });
|
||||
}
|
||||
|
||||
let cd = ClassDesc {
|
||||
|
|
@ -432,8 +443,10 @@ impl JvmStream {
|
|||
self.handles.clear();
|
||||
}
|
||||
other => {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData,
|
||||
format!("unexpected token in class annotations: 0x{:02x}", other)));
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("unexpected token in class annotations: 0x{:02x}", other),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -459,7 +472,7 @@ impl JvmStream {
|
|||
Some(cd) => cd,
|
||||
None => {
|
||||
// Unknown class, skip object data
|
||||
let handle = self.push_handle(JvmValue::Skipped);
|
||||
let _handle = self.push_handle(JvmValue::Skipped);
|
||||
self.skip_object_data(&name)?;
|
||||
return Ok(JvmValue::Skipped);
|
||||
}
|
||||
|
|
@ -506,7 +519,7 @@ impl JvmStream {
|
|||
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
|
||||
// This is a best-effort skip
|
||||
let saved = self.pos;
|
||||
|
|
@ -524,7 +537,7 @@ impl JvmStream {
|
|||
return self.read_class_annotations();
|
||||
}
|
||||
// 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();
|
||||
Ok(())
|
||||
} else {
|
||||
|
|
@ -552,16 +565,16 @@ impl JvmStream {
|
|||
// 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) {
|
||||
if let Some(super_name) = super_name
|
||||
&& !super_name.is_empty()
|
||||
&& super_name != "java.lang.Object"
|
||||
&& 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))
|
||||
}
|
||||
|
|
@ -638,11 +651,10 @@ impl JvmStream {
|
|||
JvmValue::IntArray(arr)
|
||||
}
|
||||
Some(PRIM_LONG) => {
|
||||
let mut arr = Vec::with_capacity(length);
|
||||
for _ in 0..length {
|
||||
arr.push(self.read_i64()?);
|
||||
let _ = self.read_i64()?;
|
||||
}
|
||||
JvmValue::LongArray(arr)
|
||||
JvmValue::LongArray(())
|
||||
}
|
||||
_ => {
|
||||
// Object array — read elements individually
|
||||
|
|
@ -673,7 +685,9 @@ impl JvmStream {
|
|||
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()));
|
||||
let key_str = key
|
||||
.as_string()
|
||||
.unwrap_or_else(|| format!("__key_{}", map.len()));
|
||||
map.insert(key_str, value);
|
||||
}
|
||||
|
||||
|
|
@ -729,16 +743,16 @@ struct WpWorldData {
|
|||
}
|
||||
|
||||
fn extract_world_data(val: &JvmValue) -> Result<WpWorldData> {
|
||||
let obj = val.as_object()
|
||||
.ok_or_else(|| WpImportError::InvalidFormat("root object is not a compound object".into()))?;
|
||||
let obj = val.as_object().ok_or_else(|| {
|
||||
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())
|
||||
.unwrap_or_else(|| "WorldPainter World".to_string());
|
||||
|
||||
let seed = obj.get("seed")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(0);
|
||||
let seed = obj.get("seed").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
|
||||
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>) {
|
||||
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) {
|
||||
if (map.contains_key("heightMap")
|
||||
|| map.contains_key("terrain")
|
||||
|| map.contains_key("waterLevel"))
|
||||
&& 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::ByteArray(_)
|
||||
| JvmValue::ShortArray(_)
|
||||
| JvmValue::IntArray(_)
|
||||
| JvmValue::LongArray(_) => {}
|
||||
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 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]> {
|
||||
|
|
@ -884,10 +913,10 @@ impl WpImporter {
|
|||
|
||||
/// 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")
|
||||
})
|
||||
path.extension().is_some_and(|ext| ext == "world")
|
||||
|| path
|
||||
.file_name()
|
||||
.is_some_and(|name| name.to_string_lossy().ends_with(".world"))
|
||||
}
|
||||
|
||||
/// 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 mut decoder = flate2::read::GzDecoder::new(&raw[..]);
|
||||
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()))?;
|
||||
decompressed
|
||||
} else {
|
||||
|
|
@ -911,26 +941,31 @@ pub fn import_world_file(path: &Path) -> Result<World> {
|
|||
|
||||
// Validate Java serialization magic
|
||||
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]]);
|
||||
if magic != 0xACED {
|
||||
return Err(WpImportError::InvalidFormat(
|
||||
format!("not a Java serialization stream (expected magic ACED, got {:04X})", magic)
|
||||
));
|
||||
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)
|
||||
));
|
||||
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()
|
||||
let root = stream
|
||||
.read_content()
|
||||
.map_err(|e| WpImportError::InvalidFormat(e.to_string()))?;
|
||||
|
||||
// Extract world data from parsed stream
|
||||
|
|
@ -945,7 +980,12 @@ pub fn import_world_file(path: &Path) -> Result<World> {
|
|||
// 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);
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -51,12 +51,12 @@ impl<'de> Deserialize<'de> for Dimension {
|
|||
let dim = Dim::deserialize(d)?;
|
||||
let mut tiles = HashMap::with_capacity(dim.tiles.len());
|
||||
for (key, tile) in dim.tiles {
|
||||
if let Some((tx_s, tz_s)) = key.split_once(',') {
|
||||
if let (Ok(tx), Ok(tz)) = (tx_s.parse::<i32>(), tz_s.parse::<i32>()) {
|
||||
if let Some((tx_s, tz_s)) = key.split_once(',')
|
||||
&& let (Ok(tx), Ok(tz)) = (tx_s.parse::<i32>(), tz_s.parse::<i32>())
|
||||
{
|
||||
tiles.insert((tx, tz), tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
name: dim.name,
|
||||
tiles,
|
||||
|
|
|
|||
|
|
@ -2,48 +2,96 @@ 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 }
|
||||
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 }
|
||||
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 }
|
||||
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 }
|
||||
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 }
|
||||
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 }
|
||||
fn id(&self) -> &'static str {
|
||||
"resources"
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"Resources"
|
||||
}
|
||||
fn data_size(&self) -> DataSize {
|
||||
DataSize::Nibble
|
||||
}
|
||||
fn priority(&self) -> i32 {
|
||||
60
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,12 +71,18 @@ impl Tile {
|
|||
}
|
||||
|
||||
/// 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;
|
||||
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]),
|
||||
self.layer_data
|
||||
.entry(layer_id)
|
||||
.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::Int => LayerBuffer::Int(vec![0i32; total]),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use std::sync::OnceLock;
|
|||
|
||||
use crate::model::dimension::Dimension;
|
||||
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};
|
||||
|
||||
/// 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)
|
||||
let snapshot: Vec<(usize, i16)> = if is_first_apply {
|
||||
let tile = dim.tiles.get(&(self.tile_x, self.tile_z)).ok_or(
|
||||
OperationError::OutOfBounds {
|
||||
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;
|
||||
|
|
@ -178,24 +179,22 @@ impl Operation for FilteredOperation {
|
|||
let _ = self.before_snapshot.set(snap.clone());
|
||||
snap
|
||||
} else {
|
||||
self.before_snapshot
|
||||
.get()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
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 {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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 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()
|
||||
.clamp(tile.min_height as f64, tile.max_height as f64)
|
||||
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);
|
||||
source.generate(&mut temp_tile, seed.wrapping_add(*weight as u64 * 100));
|
||||
|
||||
for i in 0..16384 {
|
||||
accumulated_heights[i] += temp_tile.heightmap[i] as f64 * weight;
|
||||
for (i, acc) in accumulated_heights.iter_mut().enumerate() {
|
||||
*acc += temp_tile.heightmap[i] as f64 * weight;
|
||||
}
|
||||
}
|
||||
|
||||
let weight_sum: f64 = self.sources.iter().map(|(_, w)| w).sum();
|
||||
|
||||
if weight_sum > 0.0 {
|
||||
for i in 0..16384 {
|
||||
let h = (accumulated_heights[i] / weight_sum)
|
||||
for (i, acc) in accumulated_heights.iter().enumerate() {
|
||||
let h = (*acc / weight_sum)
|
||||
.round()
|
||||
.clamp(tile.min_height as f64, tile.max_height as f64)
|
||||
as i16;
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
//! Each operation implements the Operation trait and supports undo.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use crate::model::dimension::Dimension;
|
||||
use crate::model::terrain::Terrain;
|
||||
|
|
@ -37,11 +37,7 @@ pub struct HeightOperation {
|
|||
|
||||
impl Operation for HeightOperation {
|
||||
fn name(&self) -> &'static str {
|
||||
if self.delta >= 0 {
|
||||
"Raise"
|
||||
} else {
|
||||
"Lower"
|
||||
}
|
||||
if self.delta >= 0 { "Raise" } else { "Lower" }
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -95,9 +91,7 @@ impl Operation for HeightOperation {
|
|||
}
|
||||
|
||||
fn inverse(&self) -> Box<dyn Operation> {
|
||||
let snapshot = self.before_snapshot.get()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let snapshot = self.before_snapshot.get().cloned().unwrap_or_default();
|
||||
Box::new(RestoreHeightsOperation {
|
||||
tile_x: self.tile_x,
|
||||
tile_z: self.tile_z,
|
||||
|
|
@ -179,9 +173,7 @@ impl Operation for FlattenOperation {
|
|||
}
|
||||
|
||||
fn inverse(&self) -> Box<dyn Operation> {
|
||||
let snapshot = self.before_snapshot.get()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let snapshot = self.before_snapshot.get().cloned().unwrap_or_default();
|
||||
Box::new(RestoreHeightsOperation {
|
||||
tile_x: self.tile_x,
|
||||
tile_z: self.tile_z,
|
||||
|
|
@ -250,9 +242,7 @@ impl Operation for PaintOperation {
|
|||
}
|
||||
|
||||
fn inverse(&self) -> Box<dyn Operation> {
|
||||
let snapshot = self.before_snapshot.get()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let snapshot = self.before_snapshot.get().cloned().unwrap_or_default();
|
||||
Box::new(RestoreTerrainOperation {
|
||||
tile_x: self.tile_x,
|
||||
tile_z: self.tile_z,
|
||||
|
|
@ -414,8 +404,8 @@ impl Operation for SmoothOperation {
|
|||
let bz = az + ny;
|
||||
if bx >= 0 && bx < TILE_SIZE as i32 && bz >= 0 && bz < TILE_SIZE as i32
|
||||
{
|
||||
sum +=
|
||||
tile.heightmap[(bz as usize) * TILE_SIZE + (bx as usize)] as i32;
|
||||
sum += tile.heightmap[(bz as usize) * TILE_SIZE + (bx as usize)]
|
||||
as i32;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -463,8 +453,8 @@ impl Operation for ErodeOperation {
|
|||
}
|
||||
|
||||
fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> {
|
||||
let tile = dim
|
||||
.tiles
|
||||
let tile =
|
||||
dim.tiles
|
||||
.get_mut(&(self.tile_x, self.tile_z))
|
||||
.ok_or(OperationError::OutOfBounds {
|
||||
tx: self.tile_x,
|
||||
|
|
@ -525,8 +515,7 @@ impl Operation for ErodeOperation {
|
|||
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
|
||||
{
|
||||
if bx < 0 || bx >= tile_size as i32 || bz < 0 || bz >= tile_size as i32 {
|
||||
continue;
|
||||
}
|
||||
let n_dx = bx - cx;
|
||||
|
|
@ -605,8 +594,8 @@ impl Operation for FillOperation {
|
|||
}
|
||||
|
||||
fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> {
|
||||
let tile = dim
|
||||
.tiles
|
||||
let tile =
|
||||
dim.tiles
|
||||
.get_mut(&(self.tile_x, self.tile_z))
|
||||
.ok_or(OperationError::OutOfBounds {
|
||||
tx: self.tile_x,
|
||||
|
|
@ -697,8 +686,8 @@ impl Operation for FloodOperation {
|
|||
}
|
||||
|
||||
fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> {
|
||||
let tile = dim
|
||||
.tiles
|
||||
let tile =
|
||||
dim.tiles
|
||||
.get_mut(&(self.tile_x, self.tile_z))
|
||||
.ok_or(OperationError::OutOfBounds {
|
||||
tx: self.tile_x,
|
||||
|
|
@ -765,8 +754,8 @@ impl Operation for PencilOperation {
|
|||
}
|
||||
|
||||
fn apply(&self, dim: &mut Dimension) -> Result<(), OperationError> {
|
||||
let tile = dim
|
||||
.tiles
|
||||
let tile =
|
||||
dim.tiles
|
||||
.get_mut(&(self.tile_x, self.tile_z))
|
||||
.ok_or(OperationError::OutOfBounds {
|
||||
tx: self.tile_x,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@ pub struct PluginRegistry {
|
|||
pub sources: Vec<Box<dyn TileSourcePlugin>>,
|
||||
}
|
||||
|
||||
impl Default for PluginRegistry {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PluginRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
|
@ -52,22 +58,34 @@ impl PluginRegistry {
|
|||
}
|
||||
|
||||
/// Find a layer plugin by name.
|
||||
pub fn find_layer_by_name(&self, name: &str) -> Option<&Box<dyn LayerPlugin>> {
|
||||
self.layers.iter().find(|p| p.name() == name)
|
||||
pub fn find_layer_by_name(&self, name: &str) -> Option<&dyn LayerPlugin> {
|
||||
self.layers
|
||||
.iter()
|
||||
.find(|p| p.name() == name)
|
||||
.map(Box::as_ref)
|
||||
}
|
||||
|
||||
/// Find an operation plugin by name.
|
||||
pub fn find_operation_by_name(&self, name: &str) -> Option<&Box<dyn OperationPlugin>> {
|
||||
self.operations.iter().find(|p| p.name() == name)
|
||||
pub fn find_operation_by_name(&self, name: &str) -> Option<&dyn OperationPlugin> {
|
||||
self.operations
|
||||
.iter()
|
||||
.find(|p| p.name() == name)
|
||||
.map(Box::as_ref)
|
||||
}
|
||||
|
||||
/// Find an export plugin by name.
|
||||
pub fn find_export_by_name(&self, name: &str) -> Option<&Box<dyn ExportPlugin>> {
|
||||
self.exports.iter().find(|p| p.name() == name)
|
||||
pub fn find_export_by_name(&self, name: &str) -> Option<&dyn ExportPlugin> {
|
||||
self.exports
|
||||
.iter()
|
||||
.find(|p| p.name() == name)
|
||||
.map(Box::as_ref)
|
||||
}
|
||||
|
||||
/// Find a tile source plugin by name.
|
||||
pub fn find_source_by_name(&self, name: &str) -> Option<&Box<dyn TileSourcePlugin>> {
|
||||
self.sources.iter().find(|p| p.name() == name)
|
||||
pub fn find_source_by_name(&self, name: &str) -> Option<&dyn TileSourcePlugin> {
|
||||
self.sources
|
||||
.iter()
|
||||
.find(|p| p.name() == name)
|
||||
.map(Box::as_ref)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ use std::collections::HashMap;
|
|||
use terrafier_core::io::minecraft::load_save;
|
||||
use terrafier_fastanvil::io::chunk::Chunk;
|
||||
use terrafier_fastanvil::io::region::Region;
|
||||
use terrafier_nbt::Tag;
|
||||
use terrafier_nbt::io::reader::read_gzip;
|
||||
use terrafier_nbt::io::writer::to_gzip_bytes;
|
||||
use terrafier_nbt::Tag;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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
|
||||
let mut palette_list: Vec<Tag> = Vec::new();
|
||||
let mut grass = HashMap::new();
|
||||
grass.insert(
|
||||
"Name".into(),
|
||||
Tag::String("minecraft:grass_block".into()),
|
||||
);
|
||||
grass.insert("Name".into(), Tag::String("minecraft:grass_block".into()));
|
||||
palette_list.push(Tag::Compound(grass));
|
||||
let mut block_states = HashMap::new();
|
||||
block_states.insert("palette".into(), Tag::List(palette_list));
|
||||
|
|
@ -164,7 +161,9 @@ fn test_golden_mca_roundtrip() {
|
|||
let coords = region.chunk_coords();
|
||||
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");
|
||||
|
||||
// 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.z, 0, "chunk zPos");
|
||||
assert_eq!(chunk.data_version, 3954, "chunk DataVersion");
|
||||
assert_eq!(
|
||||
chunk.status.as_deref(),
|
||||
Some("full"),
|
||||
"chunk Status"
|
||||
);
|
||||
assert_eq!(chunk.status.as_deref(), Some("full"), "chunk Status");
|
||||
|
||||
// Sections
|
||||
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 ---
|
||||
let reencoded = region.to_bytes().expect("region should re-serialize");
|
||||
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"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,17 +46,17 @@ impl Region {
|
|||
let mut timestamps = [0u32; 1024];
|
||||
|
||||
// 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];
|
||||
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)
|
||||
for i in 0..1024 {
|
||||
for ts in timestamps.iter_mut() {
|
||||
let mut buf = [0u8; 4];
|
||||
reader.read_exact(&mut buf)?;
|
||||
timestamps[i] = u32::from_be_bytes(buf);
|
||||
*ts = u32::from_be_bytes(buf);
|
||||
}
|
||||
|
||||
let mut chunks = HashMap::new();
|
||||
|
|
@ -111,7 +111,7 @@ impl Region {
|
|||
(local_x, local_z),
|
||||
ChunkEntry {
|
||||
offset: sector_offset,
|
||||
size: sector_count as u32,
|
||||
size: sector_count,
|
||||
timestamp,
|
||||
data: Some(decompressed),
|
||||
},
|
||||
|
|
@ -140,12 +140,15 @@ impl Region {
|
|||
/// Set chunk data at local coordinates (0..32, 0..32).
|
||||
/// `data` should be decompressed NBT bytes.
|
||||
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,
|
||||
size: 0,
|
||||
timestamp: 0,
|
||||
data: Some(data),
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// List all chunk coordinates present in this region.
|
||||
|
|
|
|||
|
|
@ -84,6 +84,12 @@ pub struct BlockPalette {
|
|||
index_map: HashMap<BlockState, u32>,
|
||||
}
|
||||
|
||||
impl Default for BlockPalette {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockPalette {
|
||||
pub fn new() -> Self {
|
||||
BlockPalette {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,12 @@ pub struct SectionData {
|
|||
pub storage: BitArray,
|
||||
}
|
||||
|
||||
impl Default for SectionData {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl SectionData {
|
||||
/// Create a new section filled with air.
|
||||
pub fn new() -> Self {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
use std::collections::VecDeque;
|
||||
|
||||
use terrafier_core::World;
|
||||
use terrafier_core::model::terrain::Terrain;
|
||||
use terrafier_core::ops::operations::Operation;
|
||||
use terrafier_core::World;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum ToolMode {
|
||||
|
|
@ -41,8 +41,6 @@ pub struct TerrafierApp {
|
|||
pub brush_local_x: Option<u32>,
|
||||
pub brush_local_z: Option<u32>,
|
||||
pub show_heightmap: bool,
|
||||
pub zoom: f32,
|
||||
pub view_offset: (f32, f32),
|
||||
pub show_new_world: bool,
|
||||
pub show_export: bool,
|
||||
pub status_message: String,
|
||||
|
|
@ -67,8 +65,6 @@ impl TerrafierApp {
|
|||
brush_local_x: None,
|
||||
brush_local_z: None,
|
||||
show_heightmap: false,
|
||||
zoom: 1.0,
|
||||
view_offset: (0.0, 0.0),
|
||||
show_new_world: false,
|
||||
show_export: false,
|
||||
status_message: "Ready".to_string(),
|
||||
|
|
@ -129,8 +125,9 @@ impl eframe::App for TerrafierApp {
|
|||
if ui.button("New World").clicked() {
|
||||
self.show_new_world = true;
|
||||
}
|
||||
if ui.button("Open").clicked() {
|
||||
if let Some(path) = rfd::FileDialog::new().pick_folder() {
|
||||
if ui.button("Open").clicked()
|
||||
&& let Some(path) = rfd::FileDialog::new().pick_folder()
|
||||
{
|
||||
match terrafier_core::io::import::import(&path) {
|
||||
Ok(world) => {
|
||||
self.world = Some(world);
|
||||
|
|
@ -144,7 +141,6 @@ impl eframe::App for TerrafierApp {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ui.button("Export").clicked() {
|
||||
self.show_export = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use terrafier_core::Terrain;
|
||||
use terrafier_core::model::brush::SymmetricBrush;
|
||||
use terrafier_core::model::tile::TILE_SIZE;
|
||||
use terrafier_core::ops::operations::{
|
||||
FlattenOperation, HeightOperation, MultiTileOperation, Operation, PaintOperation,
|
||||
SmoothOperation,
|
||||
};
|
||||
use terrafier_core::Terrain;
|
||||
|
||||
use crate::app::{TerrafierApp, ToolMode};
|
||||
|
||||
|
|
@ -103,8 +103,10 @@ fn apply_tool(app: &mut TerrafierApp) {
|
|||
|
||||
for otx in min_tx..=max_tx {
|
||||
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_cz = (global_cz - otz * TILE_SIZE as i32).clamp(0, TILE_SIZE as i32 - 1) as u32;
|
||||
let local_cx =
|
||||
(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)) {
|
||||
let brush = Arc::new(SymmetricBrush::new(app.brush_radius as f64));
|
||||
|
|
|
|||
|
|
@ -107,8 +107,12 @@ pub fn show_viewport(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) {
|
|||
);
|
||||
|
||||
// Draw selection border
|
||||
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 {
|
||||
if let Some((sel_tx, sel_tz)) = app.selected_tile
|
||||
&& 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 bz = rect.min.y + (sel_tz - min_tz) as f32 * display_size as f32;
|
||||
let border_rect =
|
||||
|
|
@ -120,14 +124,17 @@ pub fn show_viewport(ui: &mut egui::Ui, app: &mut crate::app::TerrafierApp) {
|
|||
egui::StrokeKind::Middle,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 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_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_z = rect.min.y + tile_z_in_pixels + (bz as f32 * display_size as f32 / TILE_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_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;
|
||||
painter.circle_stroke(
|
||||
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
|
||||
if response.clicked() {
|
||||
if let Some(pos) = response.interact_pointer_pos() {
|
||||
if response.clicked()
|
||||
&& let Some(pos) = response.interact_pointer_pos()
|
||||
{
|
||||
let lx = pos.x - rect.min.x;
|
||||
let lz = pos.y - rect.min.y;
|
||||
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;
|
||||
if dim.tiles.contains_key(&(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_z = ((lz as u32 % display_size) * TILE_SIZE as u32 / display_size).min(127);
|
||||
let local_x =
|
||||
((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_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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue