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 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};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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| {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ pub fn validate_save(path: &Path) -> Result<()> {
|
||||||
let region_count = std::fs::read_dir(®ion_dir)
|
let region_count = std::fs::read_dir(®ion_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)
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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, ®ion_bytes)?;
|
let region = Region::from_bytes(rx, rz, ®ion_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(®ion_dir)? {
|
for entry in fs::read_dir(®ion_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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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]),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue