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:
loki5512344 2026-06-24 12:27:55 +02:00
parent 3d3798136d
commit bdd4564240
104 changed files with 5184 additions and 3912 deletions

View file

@ -1,4 +1,9 @@
//! NBT I/O — binary reader and writer for Java Edition (Big Endian).
pub mod reader;
pub mod writer;
pub mod read;
pub mod write;
pub use read::reader;
pub use read::read_tag;
pub use write::writer;
pub use write::write_tag;

View file

@ -0,0 +1,2 @@
pub mod reader;
pub mod read_tag;

View file

@ -1,100 +1,11 @@
//! NBT binary reader for Java Edition (Big Endian).
use std::collections::HashMap;
use std::io::{Cursor, Read};
use thiserror::Error;
use std::io::Read;
use crate::tag::Tag;
#[derive(Error, Debug)]
pub enum ReadError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Unknown tag type: {0}")]
UnknownTagType(u8),
#[error("Invalid string length: {0}")]
InvalidStringLength(usize),
#[error("Invalid array length: {0}")]
InvalidArrayLength(usize),
}
pub type Result<T> = std::result::Result<T, ReadError>;
pub fn read_bytes(data: &[u8]) -> Result<Tag> {
let cursor = Cursor::new(data);
let mut de = NbtReader::new(cursor);
de.read_tag_compound_root()
}
pub fn read_gzip(data: &[u8]) -> Result<Tag> {
let mut dec = flate2::read::GzDecoder::new(data);
let mut buf = Vec::new();
dec.read_to_end(&mut buf)?;
read_bytes(&buf)
}
struct NbtReader<R: Read> {
inner: R,
buf: Vec<u8>,
}
use super::reader::{NbtReader, ReadError, Result};
impl<R: Read> NbtReader<R> {
fn new(inner: R) -> Self {
Self {
inner,
buf: Vec::new(),
}
}
fn read_exact(&mut self, len: usize) -> Result<&[u8]> {
self.buf.clear();
self.buf.resize(len, 0);
self.inner.read_exact(&mut self.buf)?;
Ok(&self.buf)
}
fn read_u8(&mut self) -> Result<u8> {
let mut byte = [0u8; 1];
self.inner.read_exact(&mut byte)?;
Ok(byte[0])
}
fn read_i16_be(&mut self) -> Result<i16> {
let b = self.read_exact(2)?;
Ok(i16::from_be_bytes([b[0], b[1]]))
}
fn read_i32_be(&mut self) -> Result<i32> {
let b = self.read_exact(4)?;
Ok(i32::from_be_bytes([b[0], b[1], b[2], b[3]]))
}
fn read_i64_be(&mut self) -> Result<i64> {
let b = self.read_exact(8)?;
Ok(i64::from_be_bytes([
b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
]))
}
fn read_f32_be(&mut self) -> Result<f32> {
let b = self.read_exact(4)?;
Ok(f32::from_be_bytes([b[0], b[1], b[2], b[3]]))
}
fn read_f64_be(&mut self) -> Result<f64> {
let b = self.read_exact(8)?;
Ok(f64::from_be_bytes([
b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
]))
}
fn read_string(&mut self) -> Result<String> {
let len = self.read_i16_be()? as u16 as usize;
let bytes = self.read_exact(len)?.to_vec();
String::from_utf8(bytes).map_err(|_| ReadError::InvalidStringLength(len))
}
fn read_tag_payload(&mut self, tag_type: u8) -> Result<Tag> {
pub(crate) fn read_tag_payload(&mut self, tag_type: u8) -> Result<Tag> {
match tag_type {
0 => Ok(Tag::End),
1 => Ok(Tag::Byte(self.read_u8()? as i8)),
@ -151,7 +62,7 @@ impl<R: Read> NbtReader<R> {
}
}
fn read_tag_compound_root(&mut self) -> Result<Tag> {
pub(crate) fn read_tag_compound_root(&mut self) -> Result<Tag> {
let t = self.read_u8()?;
if t == 0 {
return Ok(Tag::Compound(HashMap::new()));
@ -166,7 +77,9 @@ impl<R: Read> NbtReader<R> {
#[cfg(test)]
mod tests {
use super::*;
use super::super::reader::read_bytes;
use crate::tag::Tag;
use std::collections::HashMap;
fn root_envelope(data: &[u8]) -> Vec<u8> {
let mut buf = vec![0x09, 0x00, 0x00];

View file

@ -0,0 +1,95 @@
//! NBT binary reader for Java Edition (Big Endian).
use std::io::{Cursor, Read};
use thiserror::Error;
use crate::tag::Tag;
#[derive(Error, Debug)]
pub enum ReadError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Unknown tag type: {0}")]
UnknownTagType(u8),
#[error("Invalid string length: {0}")]
InvalidStringLength(usize),
#[error("Invalid array length: {0}")]
InvalidArrayLength(usize),
}
pub type Result<T> = std::result::Result<T, ReadError>;
pub fn read_bytes(data: &[u8]) -> Result<Tag> {
let cursor = Cursor::new(data);
let mut de = NbtReader::new(cursor);
de.read_tag_compound_root()
}
pub fn read_gzip(data: &[u8]) -> Result<Tag> {
let mut dec = flate2::read::GzDecoder::new(data);
let mut buf = Vec::new();
dec.read_to_end(&mut buf)?;
read_bytes(&buf)
}
pub(crate) struct NbtReader<R: Read> {
pub(crate) inner: R,
pub(crate) buf: Vec<u8>,
}
impl<R: Read> NbtReader<R> {
pub(crate) fn new(inner: R) -> Self {
Self {
inner,
buf: Vec::new(),
}
}
pub(crate) fn read_exact(&mut self, len: usize) -> Result<&[u8]> {
self.buf.clear();
self.buf.resize(len, 0);
self.inner.read_exact(&mut self.buf)?;
Ok(&self.buf)
}
pub(crate) fn read_u8(&mut self) -> Result<u8> {
let mut byte = [0u8; 1];
self.inner.read_exact(&mut byte)?;
Ok(byte[0])
}
pub(crate) fn read_i16_be(&mut self) -> Result<i16> {
let b = self.read_exact(2)?;
Ok(i16::from_be_bytes([b[0], b[1]]))
}
pub(crate) fn read_i32_be(&mut self) -> Result<i32> {
let b = self.read_exact(4)?;
Ok(i32::from_be_bytes([b[0], b[1], b[2], b[3]]))
}
pub(crate) fn read_i64_be(&mut self) -> Result<i64> {
let b = self.read_exact(8)?;
Ok(i64::from_be_bytes([
b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
]))
}
pub(crate) fn read_f32_be(&mut self) -> Result<f32> {
let b = self.read_exact(4)?;
Ok(f32::from_be_bytes([b[0], b[1], b[2], b[3]]))
}
pub(crate) fn read_f64_be(&mut self) -> Result<f64> {
let b = self.read_exact(8)?;
Ok(f64::from_be_bytes([
b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
]))
}
pub(crate) fn read_string(&mut self) -> Result<String> {
let len = self.read_i16_be()? as u16 as usize;
let bytes = self.read_exact(len)?.to_vec();
String::from_utf8(bytes).map_err(|_| ReadError::InvalidStringLength(len))
}
}

View file

@ -0,0 +1,2 @@
pub mod writer;
pub mod write_tag;

View file

@ -1,91 +1,10 @@
//! NBT binary writer for Java Edition (Big Endian).
use std::io::Write;
use thiserror::Error;
use crate::tag::Tag;
#[derive(Error, Debug)]
pub enum WriteError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Unsupported tag type in list: {0}")]
UnsupportedListType(u8),
#[error("Empty list cannot determine element type")]
EmptyList,
}
pub type Result<T> = std::result::Result<T, WriteError>;
/// Serialize a Tag tree to bytes (Big Endian, no compression).
pub fn to_bytes(tag: &Tag) -> Result<Vec<u8>> {
let mut buf = Vec::new();
let mut w = NbtWriter::new(&mut buf);
w.write_tag_compound_root(tag)?;
Ok(buf)
}
/// Serialize a Tag tree to gzip-compressed bytes.
pub fn to_gzip_bytes(tag: &Tag) -> Result<Vec<u8>> {
let raw = to_bytes(tag)?;
let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
encoder.write_all(&raw)?;
Ok(encoder.finish()?)
}
struct NbtWriter<W: Write> {
inner: W,
}
use super::writer::{NbtWriter, WriteError, Result};
impl<W: Write> NbtWriter<W> {
fn new(inner: W) -> Self {
Self { inner }
}
fn write_u8(&mut self, val: u8) -> Result<()> {
self.inner.write_all(&[val])?;
Ok(())
}
fn write_i16_be(&mut self, val: i16) -> Result<()> {
self.inner.write_all(&val.to_be_bytes())?;
Ok(())
}
fn write_i32_be(&mut self, val: i32) -> Result<()> {
self.inner.write_all(&val.to_be_bytes())?;
Ok(())
}
fn write_i64_be(&mut self, val: i64) -> Result<()> {
self.inner.write_all(&val.to_be_bytes())?;
Ok(())
}
fn write_f32_be(&mut self, val: f32) -> Result<()> {
self.inner.write_all(&val.to_be_bytes())?;
Ok(())
}
fn write_f64_be(&mut self, val: f64) -> Result<()> {
self.inner.write_all(&val.to_be_bytes())?;
Ok(())
}
fn write_string(&mut self, s: &str) -> Result<()> {
let bytes = s.as_bytes();
if bytes.len() > u16::MAX as usize {
return Err(WriteError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"String too long for NBT",
)));
}
self.write_i16_be(bytes.len() as i16)?;
self.inner.write_all(bytes)?;
Ok(())
}
fn write_tag(&mut self, tag: &Tag, name: Option<&str>) -> Result<()> {
pub(crate) fn write_tag(&mut self, tag: &Tag, name: Option<&str>) -> Result<()> {
self.write_u8(tag.id())?;
if let Some(n) = name {
self.write_string(n)?;
@ -94,7 +13,7 @@ impl<W: Write> NbtWriter<W> {
Ok(())
}
fn write_tag_payload(&mut self, tag: &Tag) -> Result<()> {
pub(crate) fn write_tag_payload(&mut self, tag: &Tag) -> Result<()> {
match tag {
Tag::End => {}
Tag::Byte(v) => self.write_u8(*v as u8)?,
@ -106,7 +25,7 @@ impl<W: Write> NbtWriter<W> {
Tag::String(v) => self.write_string(v)?,
Tag::List(items) => {
if items.is_empty() {
self.write_u8(1)?; // TAG_Byte as fallback
self.write_u8(1)?;
self.write_i32_be(0)?;
} else {
let elem_type = items[0].id();
@ -125,7 +44,7 @@ impl<W: Write> NbtWriter<W> {
self.write_tag(val, Some(key))?;
}
}
self.write_u8(0)?; // TAG_End
self.write_u8(0)?;
}
Tag::ByteArray(v) => {
self.write_i32_be(v.len() as i32)?;
@ -149,11 +68,11 @@ impl<W: Write> NbtWriter<W> {
Ok(())
}
fn write_tag_compound_root(&mut self, tag: &Tag) -> Result<()> {
pub(crate) fn write_tag_compound_root(&mut self, tag: &Tag) -> Result<()> {
match tag {
Tag::Compound(map) => {
self.write_u8(9)?; // TAG_Compound
self.write_string("")?; // empty root name
self.write_u8(9)?;
self.write_string("")?;
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
for key in keys {
@ -161,7 +80,7 @@ impl<W: Write> NbtWriter<W> {
self.write_tag(val, Some(key))?;
}
}
self.write_u8(0)?; // TAG_End
self.write_u8(0)?;
Ok(())
}
_ => Err(WriteError::Io(std::io::Error::new(
@ -174,8 +93,10 @@ impl<W: Write> NbtWriter<W> {
#[cfg(test)]
mod tests {
use super::*;
use super::super::writer::to_bytes;
use super::super::writer::to_gzip_bytes;
use crate::io::reader;
use crate::tag::Tag;
use std::collections::HashMap;
#[test]

View file

@ -0,0 +1,87 @@
//! NBT binary writer for Java Edition (Big Endian).
use std::io::Write;
use thiserror::Error;
use crate::tag::Tag;
#[derive(Error, Debug)]
pub enum WriteError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Unsupported tag type in list: {0}")]
UnsupportedListType(u8),
#[error("Empty list cannot determine element type")]
EmptyList,
}
pub type Result<T> = std::result::Result<T, WriteError>;
/// Serialize a Tag tree to bytes (Big Endian, no compression).
pub fn to_bytes(tag: &Tag) -> Result<Vec<u8>> {
let mut buf = Vec::new();
let mut w = NbtWriter::new(&mut buf);
w.write_tag_compound_root(tag)?;
Ok(buf)
}
/// Serialize a Tag tree to gzip-compressed bytes.
pub fn to_gzip_bytes(tag: &Tag) -> Result<Vec<u8>> {
let raw = to_bytes(tag)?;
let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
encoder.write_all(&raw)?;
Ok(encoder.finish()?)
}
pub(crate) struct NbtWriter<W: Write> {
pub(crate) inner: W,
}
impl<W: Write> NbtWriter<W> {
pub(crate) fn new(inner: W) -> Self {
Self { inner }
}
pub(crate) fn write_u8(&mut self, val: u8) -> Result<()> {
self.inner.write_all(&[val])?;
Ok(())
}
pub(crate) fn write_i16_be(&mut self, val: i16) -> Result<()> {
self.inner.write_all(&val.to_be_bytes())?;
Ok(())
}
pub(crate) fn write_i32_be(&mut self, val: i32) -> Result<()> {
self.inner.write_all(&val.to_be_bytes())?;
Ok(())
}
pub(crate) fn write_i64_be(&mut self, val: i64) -> Result<()> {
self.inner.write_all(&val.to_be_bytes())?;
Ok(())
}
pub(crate) fn write_f32_be(&mut self, val: f32) -> Result<()> {
self.inner.write_all(&val.to_be_bytes())?;
Ok(())
}
pub(crate) fn write_f64_be(&mut self, val: f64) -> Result<()> {
self.inner.write_all(&val.to_be_bytes())?;
Ok(())
}
pub(crate) fn write_string(&mut self, s: &str) -> Result<()> {
let bytes = s.as_bytes();
if bytes.len() > u16::MAX as usize {
return Err(WriteError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"String too long for NBT",
)));
}
self.write_i16_be(bytes.len() as i16)?;
self.inner.write_all(bytes)?;
Ok(())
}
}