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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
2
crates/nbt/src/io/read/mod.rs
Normal file
2
crates/nbt/src/io/read/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod reader;
|
||||
pub mod read_tag;
|
||||
101
crates/nbt/src/io/reader.rs → crates/nbt/src/io/read/read_tag.rs
Executable file → Normal file
101
crates/nbt/src/io/reader.rs → crates/nbt/src/io/read/read_tag.rs
Executable file → Normal 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];
|
||||
95
crates/nbt/src/io/read/reader.rs
Executable file
95
crates/nbt/src/io/read/reader.rs
Executable 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))
|
||||
}
|
||||
}
|
||||
2
crates/nbt/src/io/write/mod.rs
Normal file
2
crates/nbt/src/io/write/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod writer;
|
||||
pub mod write_tag;
|
||||
103
crates/nbt/src/io/writer.rs → crates/nbt/src/io/write/write_tag.rs
Executable file → Normal file
103
crates/nbt/src/io/writer.rs → crates/nbt/src/io/write/write_tag.rs
Executable file → Normal 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]
|
||||
87
crates/nbt/src/io/write/writer.rs
Executable file
87
crates/nbt/src/io/write/writer.rs
Executable 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(())
|
||||
}
|
||||
}
|
||||
4
crates/palette-compress/src/palette/mod.rs
Normal file
4
crates/palette-compress/src/palette/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
mod palette;
|
||||
mod palette_io;
|
||||
|
||||
pub use palette::*;
|
||||
82
crates/palette-compress/src/palette/palette.rs
Executable file
82
crates/palette-compress/src/palette/palette.rs
Executable file
|
|
@ -0,0 +1,82 @@
|
|||
use std::collections::HashMap;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct BlockState {
|
||||
pub name: String,
|
||||
pub properties: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Hash for BlockState {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.name.hash(state);
|
||||
let mut keys: Vec<&String> = self.properties.keys().collect();
|
||||
keys.sort();
|
||||
for k in keys {
|
||||
k.hash(state);
|
||||
self.properties[k].hash(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockState {
|
||||
pub fn new(name: &str) -> Self {
|
||||
BlockState {
|
||||
name: name.to_string(),
|
||||
properties: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_property(mut self, key: &str, val: &str) -> Self {
|
||||
self.properties.insert(key.to_string(), val.to_string());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A palette mapping unique `BlockState`s to compact indices.
|
||||
pub struct BlockPalette {
|
||||
pub entries: Vec<BlockState>,
|
||||
index_map: HashMap<BlockState, u32>,
|
||||
}
|
||||
|
||||
impl Default for BlockPalette {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockPalette {
|
||||
pub fn new() -> Self {
|
||||
BlockPalette {
|
||||
entries: Vec::new(),
|
||||
index_map: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a block state, or return its existing index.
|
||||
pub fn add_or_get(&mut self, state: BlockState) -> u32 {
|
||||
if let Some(&idx) = self.index_map.get(&state) {
|
||||
return idx;
|
||||
}
|
||||
let idx = self.entries.len() as u32;
|
||||
self.index_map.insert(state.clone(), idx);
|
||||
self.entries.push(state);
|
||||
idx
|
||||
}
|
||||
|
||||
pub fn get(&self, index: u32) -> Option<&BlockState> {
|
||||
self.entries.get(index as usize)
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.entries.len()
|
||||
}
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.entries.is_empty()
|
||||
}
|
||||
|
||||
/// Minimum bits per entry needed for this palette.
|
||||
pub fn bits_per_entry(&self) -> u8 {
|
||||
crate::bits::BitArray::bits_needed(self.entries.len())
|
||||
}
|
||||
}
|
||||
81
crates/palette-compress/src/palette.rs → crates/palette-compress/src/palette/palette_io.rs
Executable file → Normal file
81
crates/palette-compress/src/palette.rs → crates/palette-compress/src/palette/palette_io.rs
Executable file → Normal file
|
|
@ -1,39 +1,9 @@
|
|||
use std::collections::HashMap;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use terrafier_nbt::Tag;
|
||||
|
||||
/// A Minecraft block state — name plus key-value properties.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct BlockState {
|
||||
pub name: String,
|
||||
pub properties: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Hash for BlockState {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.name.hash(state);
|
||||
let mut keys: Vec<&String> = self.properties.keys().collect();
|
||||
keys.sort();
|
||||
for k in keys {
|
||||
k.hash(state);
|
||||
self.properties[k].hash(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
use super::palette::{BlockPalette, BlockState};
|
||||
|
||||
impl BlockState {
|
||||
pub fn new(name: &str) -> Self {
|
||||
BlockState {
|
||||
name: name.to_string(),
|
||||
properties: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_property(mut self, key: &str, val: &str) -> Self {
|
||||
self.properties.insert(key.to_string(), val.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Parse from `TAG_Compound` with `"Name"` and optional `"Properties"`.
|
||||
pub fn from_nbt(tag: &Tag) -> Option<Self> {
|
||||
let map = match tag {
|
||||
|
|
@ -78,53 +48,7 @@ impl BlockState {
|
|||
}
|
||||
}
|
||||
|
||||
/// A palette mapping unique `BlockState`s to compact indices.
|
||||
pub struct BlockPalette {
|
||||
pub entries: Vec<BlockState>,
|
||||
index_map: HashMap<BlockState, u32>,
|
||||
}
|
||||
|
||||
impl Default for BlockPalette {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockPalette {
|
||||
pub fn new() -> Self {
|
||||
BlockPalette {
|
||||
entries: Vec::new(),
|
||||
index_map: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a block state, or return its existing index.
|
||||
pub fn add_or_get(&mut self, state: BlockState) -> u32 {
|
||||
if let Some(&idx) = self.index_map.get(&state) {
|
||||
return idx;
|
||||
}
|
||||
let idx = self.entries.len() as u32;
|
||||
self.index_map.insert(state.clone(), idx);
|
||||
self.entries.push(state);
|
||||
idx
|
||||
}
|
||||
|
||||
pub fn get(&self, index: u32) -> Option<&BlockState> {
|
||||
self.entries.get(index as usize)
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.entries.len()
|
||||
}
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.entries.is_empty()
|
||||
}
|
||||
|
||||
/// Minimum bits per entry needed for this palette.
|
||||
pub fn bits_per_entry(&self) -> u8 {
|
||||
crate::bits::BitArray::bits_needed(self.entries.len())
|
||||
}
|
||||
|
||||
/// Import from NBT palette list + block data `LongArray`.
|
||||
pub fn from_nbt(palette_list: &[Tag], data: &[i64]) -> (Self, crate::bits::BitArray) {
|
||||
let mut palette = BlockPalette::new();
|
||||
|
|
@ -149,7 +73,8 @@ impl BlockPalette {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::super::palette::{BlockPalette, BlockState};
|
||||
use terrafier_nbt::Tag;
|
||||
|
||||
#[test]
|
||||
fn test_add_get() {
|
||||
Loading…
Add table
Add a link
Reference in a new issue