Refactor structure, implement layers, add Erode/Fill/layer panel
- Restructure folder hierarchy: max 4 .rs per dir using subfolders (ops/operations -> brush/ + shaping/, model/ -> world/, io/* -> subtypes, etc.) - Fix golden_test.rs comment, extract BrushApply helper, add World::with_heightmap - Implement 6 layer data generators (Caves, River, Frost, Trees, Biome, Resources) with noise-based (OpenSimplex) and terrain-derived algorithms - Fix clippy warnings, dead code cleanup, import path fixes - GUI: add Erode&Fill tool modes with radio buttons/sliders/apply_tool - GUI: add Layers panel with 6 checkboxes + CPU viewport overlay rendering
This commit is contained in:
parent
3d3798136d
commit
bdd4564240
104 changed files with 5184 additions and 3912 deletions
|
|
@ -1,5 +1,4 @@
|
|||
//! Chunk data structures for Minecraft Anvil format.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A parsed chunk from an Anvil (.mca) region file.
|
||||
|
|
@ -27,7 +26,6 @@ pub struct ChunkSection {
|
|||
pub block_light: Option<Vec<i8>>,
|
||||
pub sky_light: Option<Vec<i8>>,
|
||||
}
|
||||
|
||||
impl Chunk {
|
||||
/// Parse a chunk from an NBT Compound tag.
|
||||
pub fn from_nbt(tag: &terrafier_nbt::Tag) -> Option<Self> {
|
||||
|
|
@ -86,37 +84,7 @@ impl Chunk {
|
|||
raw,
|
||||
})
|
||||
}
|
||||
|
||||
/// Serialize this chunk back to an NBT Compound tag.
|
||||
pub fn to_nbt(&self) -> terrafier_nbt::Tag {
|
||||
let mut compound = self.raw.clone();
|
||||
|
||||
compound.insert("xPos".into(), terrafier_nbt::Tag::Int(self.x));
|
||||
compound.insert("zPos".into(), terrafier_nbt::Tag::Int(self.z));
|
||||
compound.insert(
|
||||
"DataVersion".into(),
|
||||
terrafier_nbt::Tag::Int(self.data_version),
|
||||
);
|
||||
|
||||
let sections_list: Vec<terrafier_nbt::Tag> =
|
||||
self.sections.iter().map(|s| s.to_nbt()).collect();
|
||||
compound.insert("sections".into(), terrafier_nbt::Tag::List(sections_list));
|
||||
|
||||
if let Some(status) = &self.status {
|
||||
compound.insert("Status".into(), terrafier_nbt::Tag::String(status.clone()));
|
||||
}
|
||||
|
||||
if !self.heightmaps.is_empty() {
|
||||
compound.insert(
|
||||
"Heightmaps".into(),
|
||||
terrafier_nbt::Tag::Compound(self.heightmaps.clone()),
|
||||
);
|
||||
}
|
||||
|
||||
terrafier_nbt::Tag::Compound(compound)
|
||||
}
|
||||
}
|
||||
|
||||
impl ChunkSection {
|
||||
/// Parse a section from an NBT Compound tag.
|
||||
pub fn from_nbt(tag: &terrafier_nbt::Tag) -> Option<Self> {
|
||||
|
|
@ -216,64 +184,7 @@ impl ChunkSection {
|
|||
sky_light,
|
||||
})
|
||||
}
|
||||
|
||||
/// Serialize section back to NBT Compound.
|
||||
pub fn to_nbt(&self) -> terrafier_nbt::Tag {
|
||||
let mut compound = HashMap::new();
|
||||
compound.insert("Y".into(), terrafier_nbt::Tag::Byte(self.section_y));
|
||||
|
||||
// Block states
|
||||
let palette_list: Vec<terrafier_nbt::Tag> = self
|
||||
.palette
|
||||
.iter()
|
||||
.map(|p| terrafier_nbt::Tag::Compound(p.clone()))
|
||||
.collect();
|
||||
let mut block_states = HashMap::new();
|
||||
block_states.insert("palette".into(), terrafier_nbt::Tag::List(palette_list));
|
||||
if !self.block_data.is_empty() {
|
||||
block_states.insert(
|
||||
"data".into(),
|
||||
terrafier_nbt::Tag::LongArray(self.block_data.clone()),
|
||||
);
|
||||
}
|
||||
compound.insert(
|
||||
"block_states".into(),
|
||||
terrafier_nbt::Tag::Compound(block_states),
|
||||
);
|
||||
|
||||
// Biomes
|
||||
let biome_palette_list: Vec<terrafier_nbt::Tag> = self
|
||||
.biome_palette
|
||||
.iter()
|
||||
.map(|p| terrafier_nbt::Tag::Compound(p.clone()))
|
||||
.collect();
|
||||
let mut biomes = HashMap::new();
|
||||
biomes.insert(
|
||||
"palette".into(),
|
||||
terrafier_nbt::Tag::List(biome_palette_list),
|
||||
);
|
||||
if !self.biome_data.is_empty() {
|
||||
biomes.insert(
|
||||
"data".into(),
|
||||
terrafier_nbt::Tag::LongArray(self.biome_data.clone()),
|
||||
);
|
||||
}
|
||||
compound.insert("biomes".into(), terrafier_nbt::Tag::Compound(biomes));
|
||||
|
||||
if let Some(bl) = &self.block_light {
|
||||
compound.insert(
|
||||
"BlockLight".into(),
|
||||
terrafier_nbt::Tag::ByteArray(bl.clone()),
|
||||
);
|
||||
}
|
||||
if let Some(sl) = &self.sky_light {
|
||||
compound.insert("SkyLight".into(), terrafier_nbt::Tag::ByteArray(sl.clone()));
|
||||
}
|
||||
|
||||
terrafier_nbt::Tag::Compound(compound)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_int(map: &HashMap<String, terrafier_nbt::Tag>, key: &str) -> Option<i32> {
|
||||
map.get(key).and_then(|t| match t {
|
||||
terrafier_nbt::Tag::Int(v) => Some(*v),
|
||||
90
crates/fastanvil/src/io/chunk/chunk_write.rs
Normal file
90
crates/fastanvil/src/io/chunk/chunk_write.rs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use super::chunk::{Chunk, ChunkSection};
|
||||
|
||||
impl Chunk {
|
||||
/// Serialize this chunk back to an NBT Compound tag.
|
||||
pub fn to_nbt(&self) -> terrafier_nbt::Tag {
|
||||
let mut compound = self.raw.clone();
|
||||
|
||||
compound.insert("xPos".into(), terrafier_nbt::Tag::Int(self.x));
|
||||
compound.insert("zPos".into(), terrafier_nbt::Tag::Int(self.z));
|
||||
compound.insert(
|
||||
"DataVersion".into(),
|
||||
terrafier_nbt::Tag::Int(self.data_version),
|
||||
);
|
||||
|
||||
let sections_list: Vec<terrafier_nbt::Tag> =
|
||||
self.sections.iter().map(|s| s.to_nbt()).collect();
|
||||
compound.insert("sections".into(), terrafier_nbt::Tag::List(sections_list));
|
||||
|
||||
if let Some(status) = &self.status {
|
||||
compound.insert("Status".into(), terrafier_nbt::Tag::String(status.clone()));
|
||||
}
|
||||
|
||||
if !self.heightmaps.is_empty() {
|
||||
compound.insert(
|
||||
"Heightmaps".into(),
|
||||
terrafier_nbt::Tag::Compound(self.heightmaps.clone()),
|
||||
);
|
||||
}
|
||||
|
||||
terrafier_nbt::Tag::Compound(compound)
|
||||
}
|
||||
}
|
||||
|
||||
impl ChunkSection {
|
||||
/// Serialize section back to NBT Compound.
|
||||
pub fn to_nbt(&self) -> terrafier_nbt::Tag {
|
||||
let mut compound = HashMap::new();
|
||||
compound.insert("Y".into(), terrafier_nbt::Tag::Byte(self.section_y));
|
||||
|
||||
let palette_list: Vec<terrafier_nbt::Tag> = self
|
||||
.palette
|
||||
.iter()
|
||||
.map(|p| terrafier_nbt::Tag::Compound(p.clone()))
|
||||
.collect();
|
||||
let mut block_states = HashMap::new();
|
||||
block_states.insert("palette".into(), terrafier_nbt::Tag::List(palette_list));
|
||||
if !self.block_data.is_empty() {
|
||||
block_states.insert(
|
||||
"data".into(),
|
||||
terrafier_nbt::Tag::LongArray(self.block_data.clone()),
|
||||
);
|
||||
}
|
||||
compound.insert(
|
||||
"block_states".into(),
|
||||
terrafier_nbt::Tag::Compound(block_states),
|
||||
);
|
||||
|
||||
let biome_palette_list: Vec<terrafier_nbt::Tag> = self
|
||||
.biome_palette
|
||||
.iter()
|
||||
.map(|p| terrafier_nbt::Tag::Compound(p.clone()))
|
||||
.collect();
|
||||
let mut biomes = HashMap::new();
|
||||
biomes.insert(
|
||||
"palette".into(),
|
||||
terrafier_nbt::Tag::List(biome_palette_list),
|
||||
);
|
||||
if !self.biome_data.is_empty() {
|
||||
biomes.insert(
|
||||
"data".into(),
|
||||
terrafier_nbt::Tag::LongArray(self.biome_data.clone()),
|
||||
);
|
||||
}
|
||||
compound.insert("biomes".into(), terrafier_nbt::Tag::Compound(biomes));
|
||||
|
||||
if let Some(bl) = &self.block_light {
|
||||
compound.insert(
|
||||
"BlockLight".into(),
|
||||
terrafier_nbt::Tag::ByteArray(bl.clone()),
|
||||
);
|
||||
}
|
||||
if let Some(sl) = &self.sky_light {
|
||||
compound.insert("SkyLight".into(), terrafier_nbt::Tag::ByteArray(sl.clone()));
|
||||
}
|
||||
|
||||
terrafier_nbt::Tag::Compound(compound)
|
||||
}
|
||||
}
|
||||
4
crates/fastanvil/src/io/chunk/mod.rs
Normal file
4
crates/fastanvil/src/io/chunk/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
mod chunk;
|
||||
mod chunk_write;
|
||||
|
||||
pub use chunk::*;
|
||||
|
|
@ -1,4 +1,2 @@
|
|||
//! Anvil format I/O — region file and chunk data structures.
|
||||
|
||||
pub mod chunk;
|
||||
pub mod region;
|
||||
|
|
|
|||
4
crates/fastanvil/src/io/region/mod.rs
Normal file
4
crates/fastanvil/src/io/region/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
mod region;
|
||||
mod region_write;
|
||||
|
||||
pub use region::*;
|
||||
|
|
@ -28,7 +28,7 @@ pub type Result<T> = std::result::Result<T, RegionError>;
|
|||
pub struct Region {
|
||||
pub x: i32,
|
||||
pub z: i32,
|
||||
chunks: HashMap<(u8, u8), ChunkEntry>,
|
||||
pub(crate) chunks: HashMap<(u8, u8), ChunkEntry>,
|
||||
}
|
||||
|
||||
pub struct ChunkEntry {
|
||||
|
|
@ -163,68 +163,5 @@ impl Region {
|
|||
self.chunks.len()
|
||||
}
|
||||
|
||||
/// Serialize region back to .mca bytes.
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>> {
|
||||
let sector_size: u64 = 4096;
|
||||
let mut locations = [0u32; 1024];
|
||||
let mut timestamps = [0u32; 1024];
|
||||
let mut sector_data: Vec<Vec<u8>> = Vec::new();
|
||||
|
||||
for i in 0..1024 {
|
||||
let local_x = (i % 32) as u8;
|
||||
let local_z = (i / 32) as u8;
|
||||
|
||||
if let Some(entry) = self.chunks.get(&(local_x, local_z)) {
|
||||
timestamps[i] = entry.timestamp;
|
||||
|
||||
// Serialize NBT and compress with Zlib
|
||||
let compressed = compression::compress(
|
||||
entry.data.as_deref().unwrap_or_default(),
|
||||
CompressionType::Zlib,
|
||||
)?;
|
||||
|
||||
// Prepend: length (4 bytes BE) + compression type (1 byte)
|
||||
let total_len = 1 + compressed.len();
|
||||
let mut sector = Vec::with_capacity(4 + total_len);
|
||||
sector.extend(&(total_len as u32).to_be_bytes());
|
||||
sector.push(CompressionType::Zlib.id());
|
||||
sector.extend(&compressed);
|
||||
|
||||
// Pad to sector boundary
|
||||
while sector.len() % sector_size as usize != 0 {
|
||||
sector.push(0);
|
||||
}
|
||||
|
||||
let offset = 2 + sector_data.len() as u32;
|
||||
locations[i] =
|
||||
(offset << 8) | ((sector.len() / sector_size as usize) as u32 & 0xFF);
|
||||
sector_data.push(sector);
|
||||
}
|
||||
}
|
||||
|
||||
// Build output: header + sector data
|
||||
let mut output =
|
||||
Vec::with_capacity(2 * 4096 + sector_data.iter().map(|s| s.len()).sum::<usize>());
|
||||
|
||||
// Location table
|
||||
for loc in locations.iter() {
|
||||
output.extend(&loc.to_be_bytes());
|
||||
}
|
||||
// Timestamp table
|
||||
for ts in timestamps.iter() {
|
||||
output.extend(&ts.to_be_bytes());
|
||||
}
|
||||
|
||||
// Pad header to exactly 2 sectors
|
||||
while output.len() < 2 * sector_size as usize {
|
||||
output.push(0);
|
||||
}
|
||||
|
||||
// Sector data
|
||||
for sector in §or_data {
|
||||
output.extend(sector);
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
62
crates/fastanvil/src/io/region/region_write.rs
Normal file
62
crates/fastanvil/src/io/region/region_write.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
use crate::compression::{self, CompressionType};
|
||||
|
||||
use super::region::{Region, Result};
|
||||
|
||||
impl Region {
|
||||
/// Serialize region back to .mca bytes.
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>> {
|
||||
let sector_size: u64 = 4096;
|
||||
let mut locations = [0u32; 1024];
|
||||
let mut timestamps = [0u32; 1024];
|
||||
let mut sector_data: Vec<Vec<u8>> = Vec::new();
|
||||
|
||||
for i in 0..1024 {
|
||||
let local_x = (i % 32) as u8;
|
||||
let local_z = (i / 32) as u8;
|
||||
|
||||
if let Some(entry) = self.chunks.get(&(local_x, local_z)) {
|
||||
timestamps[i] = entry.timestamp;
|
||||
|
||||
let compressed = compression::compress(
|
||||
entry.data.as_deref().unwrap_or_default(),
|
||||
CompressionType::Zlib,
|
||||
)?;
|
||||
|
||||
let total_len = 1 + compressed.len();
|
||||
let mut sector = Vec::with_capacity(4 + total_len);
|
||||
sector.extend(&(total_len as u32).to_be_bytes());
|
||||
sector.push(CompressionType::Zlib.id());
|
||||
sector.extend(&compressed);
|
||||
|
||||
while sector.len() % sector_size as usize != 0 {
|
||||
sector.push(0);
|
||||
}
|
||||
|
||||
let offset = 2 + sector_data.len() as u32;
|
||||
locations[i] =
|
||||
(offset << 8) | ((sector.len() / sector_size as usize) as u32 & 0xFF);
|
||||
sector_data.push(sector);
|
||||
}
|
||||
}
|
||||
|
||||
let mut output =
|
||||
Vec::with_capacity(2 * 4096 + sector_data.iter().map(|s| s.len()).sum::<usize>());
|
||||
|
||||
for loc in locations.iter() {
|
||||
output.extend(&loc.to_be_bytes());
|
||||
}
|
||||
for ts in timestamps.iter() {
|
||||
output.extend(&ts.to_be_bytes());
|
||||
}
|
||||
|
||||
while output.len() < 2 * sector_size as usize {
|
||||
output.push(0);
|
||||
}
|
||||
|
||||
for sector in §or_data {
|
||||
output.extend(sector);
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue