Initial commit: Terrafier v0.1.0

- Core world model (World, Dimension, Tile, Terrain, Platform)
- NBT reader/writer with full tag support
- Anvil region (.mca) format reader with chunk parsing
- Block palette compression and export
- Biome database with Minecraft 1.21 biomes
- CLI commands: new, import, export, info, render
- GUI application with egui (viewport, tools, undo/redo)
- Noise-based heightmap generation and editing operations

License: GPL-3.0-or-later
This commit is contained in:
loki5512344 2026-06-15 19:14:11 +02:00
commit 91835402b3
94 changed files with 12372 additions and 0 deletions

11
crates/nbt/Cargo.toml Executable file
View file

@ -0,0 +1,11 @@
[package]
name = "terrafier-nbt"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "Zero-copy NBT (Named Binary Tag) parser and writer for Minecraft data"
[dependencies]
serde = { workspace = true, optional = true }
thiserror.workspace = true
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }

4
crates/nbt/src/io/mod.rs Executable file
View file

@ -0,0 +1,4 @@
//! NBT I/O — binary reader and writer for Java Edition (Big Endian).
pub mod reader;
pub mod writer;

230
crates/nbt/src/io/reader.rs Executable file
View file

@ -0,0 +1,230 @@
//! NBT binary reader for Java Edition (Big Endian).
use std::collections::HashMap;
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)
}
struct NbtReader<R: Read> {
inner: R,
buf: Vec<u8>,
}
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> {
match tag_type {
0 => Ok(Tag::End),
1 => Ok(Tag::Byte(self.read_u8()? as i8)),
2 => Ok(Tag::Short(self.read_i16_be()?)),
3 => Ok(Tag::Int(self.read_i32_be()?)),
4 => Ok(Tag::Long(self.read_i64_be()?)),
5 => Ok(Tag::Float(self.read_f32_be()?)),
6 => Ok(Tag::Double(self.read_f64_be()?)),
7 => Ok(Tag::String(self.read_string()?)),
8 => {
let elem_type = self.read_u8()?;
let len = self.read_i32_be()? as usize;
let mut items = Vec::with_capacity(len);
for _ in 0..len {
items.push(self.read_tag_payload(elem_type)?);
}
Ok(Tag::List(items))
}
9 => {
let mut map = HashMap::new();
loop {
let t = self.read_u8()?;
if t == 0 {
break;
}
let name = self.read_string()?;
let val = self.read_tag_payload(t)?;
map.insert(name, val);
}
Ok(Tag::Compound(map))
}
10 => {
let len = self.read_i32_be()? as usize;
let bytes = self.read_exact(len)?.to_vec();
Ok(Tag::ByteArray(bytes.into_iter().map(|b| b as i8).collect()))
}
11 => {
let len = self.read_i32_be()? as usize;
let mut vals = Vec::with_capacity(len);
for _ in 0..len {
vals.push(self.read_i32_be()?);
}
Ok(Tag::IntArray(vals))
}
12 => {
let len = self.read_i32_be()? as usize;
let mut vals = Vec::with_capacity(len);
for _ in 0..len {
vals.push(self.read_i64_be()?);
}
Ok(Tag::LongArray(vals))
}
_ => Err(ReadError::UnknownTagType(tag_type)),
}
}
fn read_tag_compound_root(&mut self) -> Result<Tag> {
let t = self.read_u8()?;
if t == 0 {
return Ok(Tag::Compound(HashMap::new()));
}
if t != 9 {
return Err(ReadError::UnknownTagType(t));
}
let _name = self.read_string()?;
self.read_tag_payload(9)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn root_envelope(data: &[u8]) -> Vec<u8> {
let mut buf = vec![0x09, 0x00, 0x00];
buf.extend(data);
buf.push(0x00);
buf
}
#[test]
fn test_read_byte() {
let payload = vec![0x01, 0x00, 0x04, b't', b'e', b's', b't', 0x2a, 0x00];
let tag = read_bytes(&root_envelope(&payload)).unwrap();
let expected = Tag::Compound(HashMap::from([("test".into(), Tag::Byte(42))]));
assert_eq!(tag, expected);
}
#[test]
fn test_read_string() {
let payload = vec![
0x07, 0x00, 0x04, b'n', b'a', b'm', b'e', 0x00, 0x05, b'H', b'e', b'l', b'l', b'o',
];
let tag = read_bytes(&root_envelope(&payload)).unwrap();
let expected = Tag::Compound(HashMap::from([(
"name".into(),
Tag::String("Hello".into()),
)]));
assert_eq!(tag, expected);
}
#[test]
fn test_read_compound_nested() {
let inner = vec![0x01, 0x00, 0x03, b'k', b'e', b'y', 0x07, 0x00];
let mut payload = vec![0x09, 0x00, 0x05, b'c', b'h', b'i', b'l', b'd'];
payload.extend(inner);
payload.push(0x00);
let tag = read_bytes(&root_envelope(&payload)).unwrap();
let inner_map = HashMap::from([("key".into(), Tag::Byte(7))]);
let expected = Tag::Compound(HashMap::from([("child".into(), Tag::Compound(inner_map))]));
assert_eq!(tag, expected);
}
#[test]
fn test_read_int_array() {
let mut payload = vec![0x0b, 0x00, 0x03, b'a', b'r', b'r', 0x00, 0x00, 0x00, 0x02];
payload.extend(&[0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02]);
payload.push(0x00);
let tag = read_bytes(&root_envelope(&payload)).unwrap();
let expected = Tag::Compound(HashMap::from([("arr".into(), Tag::IntArray(vec![1, 2]))]));
assert_eq!(tag, expected);
}
#[test]
fn test_read_long_array() {
let mut payload = vec![0x0c, 0x00, 0x03, b'l', b'n', b'g', 0x00, 0x00, 0x00, 0x01];
payload.extend(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a]);
payload.push(0x00);
let tag = read_bytes(&root_envelope(&payload)).unwrap();
let expected = Tag::Compound(HashMap::from([("lng".into(), Tag::LongArray(vec![42]))]));
assert_eq!(tag, expected);
}
}

227
crates/nbt/src/io/writer.rs Executable file
View file

@ -0,0 +1,227 @@
//! 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,
}
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<()> {
self.write_u8(tag.id())?;
if let Some(n) = name {
self.write_string(n)?;
}
self.write_tag_payload(tag)?;
Ok(())
}
fn write_tag_payload(&mut self, tag: &Tag) -> Result<()> {
match tag {
Tag::End => {}
Tag::Byte(v) => self.write_u8(*v as u8)?,
Tag::Short(v) => self.write_i16_be(*v)?,
Tag::Int(v) => self.write_i32_be(*v)?,
Tag::Long(v) => self.write_i64_be(*v)?,
Tag::Float(v) => self.write_f32_be(*v)?,
Tag::Double(v) => self.write_f64_be(*v)?,
Tag::String(v) => self.write_string(v)?,
Tag::List(items) => {
if items.is_empty() {
self.write_u8(1)?; // TAG_Byte as fallback
self.write_i32_be(0)?;
} else {
let elem_type = items[0].id();
self.write_u8(elem_type)?;
self.write_i32_be(items.len() as i32)?;
for item in items {
self.write_tag_payload(item)?;
}
}
}
Tag::Compound(map) => {
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
for key in keys {
if let Some(val) = map.get(key) {
self.write_tag(val, Some(key))?;
}
}
self.write_u8(0)?; // TAG_End
}
Tag::ByteArray(v) => {
self.write_i32_be(v.len() as i32)?;
for b in v {
self.inner.write_all(&[*b as u8])?;
}
}
Tag::IntArray(v) => {
self.write_i32_be(v.len() as i32)?;
for n in v {
self.write_i32_be(*n)?;
}
}
Tag::LongArray(v) => {
self.write_i32_be(v.len() as i32)?;
for n in v {
self.write_i64_be(*n)?;
}
}
}
Ok(())
}
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
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
for key in keys {
if let Some(val) = map.get(key) {
self.write_tag(val, Some(key))?;
}
}
self.write_u8(0)?; // TAG_End
Ok(())
}
_ => Err(WriteError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Root tag must be Compound",
))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::io::reader;
use std::collections::HashMap;
#[test]
fn test_roundtrip_byte() {
let tag = Tag::Compound(HashMap::from([("val".into(), Tag::Byte(42))]));
let bytes = to_bytes(&tag).unwrap();
let parsed = reader::read_bytes(&bytes).unwrap();
assert_eq!(tag, parsed);
}
#[test]
fn test_roundtrip_nested() {
let inner = Tag::Compound(HashMap::from([
("x".into(), Tag::Int(100)),
("y".into(), Tag::Int(200)),
]));
let tag = Tag::Compound(HashMap::from([("pos".into(), inner)]));
let bytes = to_bytes(&tag).unwrap();
let parsed = reader::read_bytes(&bytes).unwrap();
assert_eq!(tag, parsed);
}
#[test]
fn test_roundtrip_all_types() {
let mut map = HashMap::new();
map.insert("byte".into(), Tag::Byte(1));
map.insert("short".into(), Tag::Short(2));
map.insert("int".into(), Tag::Int(3));
map.insert("long".into(), Tag::Long(4));
map.insert("float".into(), Tag::Float(5.0));
map.insert("double".into(), Tag::Double(6.0));
map.insert("string".into(), Tag::String("hello".into()));
map.insert("bytearray".into(), Tag::ByteArray(vec![1, 2, 3]));
map.insert("intarray".into(), Tag::IntArray(vec![4, 5, 6]));
map.insert("longarray".into(), Tag::LongArray(vec![7, 8, 9]));
let tag = Tag::Compound(map);
let bytes = to_bytes(&tag).unwrap();
let parsed = reader::read_bytes(&bytes).unwrap();
assert_eq!(tag, parsed);
}
#[test]
fn test_gzip_roundtrip() {
let tag = Tag::Compound(HashMap::from([("val".into(), Tag::Int(12345))]));
let gz = to_gzip_bytes(&tag).unwrap();
let parsed = reader::read_gzip(&gz).unwrap();
assert_eq!(tag, parsed);
}
}

20
crates/nbt/src/lib.rs Executable file
View file

@ -0,0 +1,20 @@
//! Zero-copy NBT (Named Binary Tag) parser and writer.
//!
//! Implements the Minecraft NBT format with optional serde support.
//! Uses zero-copy deserialization where possible to minimize allocations.
//!
//! ## Format support
//!
//! - All tag types: Byte, Short, Int, Long, Float, Double, String,
//! List, Compound, IntArray, LongArray, ByteArray
//! - GZip compressed streams
//! - Java edition (Big Endian) and Bedrock edition (Little Endian) variants
#![deny(unsafe_code)]
pub mod io;
pub mod tag;
pub use io::reader;
pub use io::writer;
pub use tag::Tag;

58
crates/nbt/src/tag.rs Executable file
View file

@ -0,0 +1,58 @@
//! NBT tag types.
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub enum Tag {
End,
Byte(i8),
Short(i16),
Int(i32),
Long(i64),
Float(f32),
Double(f64),
String(String),
List(Vec<Tag>),
Compound(HashMap<String, Tag>),
ByteArray(Vec<i8>),
IntArray(Vec<i32>),
LongArray(Vec<i64>),
}
impl Tag {
pub fn name(&self) -> &'static str {
match self {
Tag::End => "TAG_End",
Tag::Byte(_) => "TAG_Byte",
Tag::Short(_) => "TAG_Short",
Tag::Int(_) => "TAG_Int",
Tag::Long(_) => "TAG_Long",
Tag::Float(_) => "TAG_Float",
Tag::Double(_) => "TAG_Double",
Tag::String(_) => "TAG_String",
Tag::List(_) => "TAG_List",
Tag::Compound(_) => "TAG_Compound",
Tag::ByteArray(_) => "TAG_Byte_Array",
Tag::IntArray(_) => "TAG_Int_Array",
Tag::LongArray(_) => "TAG_Long_Array",
}
}
pub fn id(&self) -> u8 {
match self {
Tag::End => 0,
Tag::Byte(_) => 1,
Tag::Short(_) => 2,
Tag::Int(_) => 3,
Tag::Long(_) => 4,
Tag::Float(_) => 5,
Tag::Double(_) => 6,
Tag::String(_) => 7,
Tag::List(_) => 8,
Tag::Compound(_) => 9,
Tag::ByteArray(_) => 10,
Tag::IntArray(_) => 11,
Tag::LongArray(_) => 12,
}
}
}