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

View file

@ -0,0 +1,112 @@
/// Compact storage of fixed-size non-negative integers in a `Vec<i64>`.
///
/// Minecraft Anvil stores block palette indices as packed integers.
/// N bits per entry, entries packed consecutively into 64-bit longs.
/// Entries never cross long boundaries.
pub struct BitArray {
pub data: Vec<i64>,
pub bits_per_entry: u8,
pub size: usize,
}
impl BitArray {
/// Create a new `BitArray` with all entries initialized to 0.
pub fn new(size: usize, bits_per_entry: u8) -> Self {
assert!(
bits_per_entry > 0 && bits_per_entry <= 64,
"bits_per_entry must be 1..=64"
);
let entries_per_long = 64 / bits_per_entry as usize;
let data_len = size.div_ceil(entries_per_long);
BitArray {
data: vec![0i64; data_len],
bits_per_entry,
size,
}
}
/// Wrap existing raw data as a `BitArray`.
pub fn from_raw(data: Vec<i64>, bits_per_entry: u8, size: usize) -> Self {
BitArray {
data,
bits_per_entry,
size,
}
}
/// Read the value at `index`.
pub fn get(&self, index: usize) -> i64 {
assert!(index < self.size, "index out of bounds");
let epb = 64 / self.bits_per_entry as usize;
let long_idx = index / epb;
let offset = (index % epb) * self.bits_per_entry as usize;
let mask = (1i64 << self.bits_per_entry) - 1;
(self.data[long_idx] >> offset) & mask
}
/// Write `value` at `index`.
pub fn set(&mut self, index: usize, value: i64) {
assert!(index < self.size, "index out of bounds");
let epb = 64 / self.bits_per_entry as usize;
let long_idx = index / epb;
let offset = (index % epb) * self.bits_per_entry as usize;
let mask = (1i64 << self.bits_per_entry) - 1;
self.data[long_idx] =
(self.data[long_idx] & !(mask << offset)) | ((value & mask) << offset);
}
/// Borrow the raw underlying long array.
pub fn to_raw(&self) -> &[i64] {
&self.data
}
/// Minimum bits needed to represent `count` distinct values.
///
/// Minecraft requires at least 1 bit per entry, even for a single block.
pub fn bits_needed(count: usize) -> u8 {
if count <= 2 {
return 1; // 0 and 1 both fit in 1 bit; 2 values → 1 bit
}
let count = count as u64;
(64 - count.saturating_sub(1).leading_zeros()) as u8
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pack_unpack() {
let mut ba = BitArray::new(10, 4);
for i in 0..10 {
ba.set(i, (i % 15) as i64);
}
for i in 0..10 {
assert_eq!(ba.get(i), (i % 15) as i64);
}
}
#[test]
fn test_bits_needed() {
assert_eq!(BitArray::bits_needed(0), 1);
assert_eq!(BitArray::bits_needed(1), 1);
assert_eq!(BitArray::bits_needed(2), 1);
assert_eq!(BitArray::bits_needed(3), 2);
assert_eq!(BitArray::bits_needed(4), 2);
assert_eq!(BitArray::bits_needed(8), 3);
assert_eq!(BitArray::bits_needed(16), 4);
assert_eq!(BitArray::bits_needed(256), 8);
}
#[test]
fn test_large_array() {
let mut ba = BitArray::new(4096, 13);
for i in 0..4096 {
ba.set(i, (i % 8191) as i64);
}
for i in 0..4096 {
assert_eq!(ba.get(i), (i % 8191) as i64);
}
}
}

View file

@ -0,0 +1,14 @@
//! Block palette compression for Minecraft Anvil format.
//!
//! Minecraft 1.13+ uses a palette-based block storage format where each
//! section stores a list of unique block states (palette) and indices
//! into that palette (packed into a BitArray). This crate provides
//! the building blocks for reading and writing compressed block data.
pub mod bits;
pub mod palette;
pub mod section;
pub use bits::BitArray;
pub use palette::{BlockPalette, BlockState};
pub use section::SectionData;

View file

@ -0,0 +1,203 @@
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);
}
}
}
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 {
Tag::Compound(m) => m,
_ => return None,
};
let name = match map.get("Name")? {
Tag::String(s) => s.clone(),
_ => return None,
};
let properties = match map.get("Properties") {
Some(Tag::Compound(props)) => props
.iter()
.map(|(k, v)| {
(
k.clone(),
match v {
Tag::String(s) => s.clone(),
_ => String::new(),
},
)
})
.collect(),
_ => HashMap::new(),
};
Some(BlockState { name, properties })
}
/// Serialize to `TAG_Compound`.
pub fn to_nbt(&self) -> Tag {
let mut map = HashMap::new();
map.insert("Name".into(), Tag::String(self.name.clone()));
if !self.properties.is_empty() {
let props = self
.properties
.iter()
.map(|(k, v)| (k.clone(), Tag::String(v.clone())))
.collect();
map.insert("Properties".into(), Tag::Compound(props));
}
Tag::Compound(map)
}
}
/// A palette mapping unique `BlockState`s to compact indices.
pub struct BlockPalette {
pub entries: Vec<BlockState>,
index_map: HashMap<BlockState, u32>,
}
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();
for tag in palette_list {
if let Some(state) = BlockState::from_nbt(tag) {
palette.add_or_get(state);
}
}
let bpe = palette.bits_per_entry();
(
palette,
crate::bits::BitArray::from_raw(data.to_vec(), bpe, 4096),
)
}
/// Export to NBT palette list + block data `LongArray`.
pub fn to_nbt(&self, bitarray: &crate::bits::BitArray) -> (Vec<Tag>, Vec<i64>) {
let list: Vec<Tag> = self.entries.iter().map(|s| s.to_nbt()).collect();
(list, bitarray.data.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_get() {
let mut p = BlockPalette::new();
let air = BlockState::new("minecraft:air");
let stone = BlockState::new("minecraft:stone");
assert_eq!(p.add_or_get(air.clone()), 0);
assert_eq!(p.add_or_get(stone.clone()), 1);
assert_eq!(p.add_or_get(air.clone()), 0);
assert_eq!(p.get(0), Some(&air));
assert_eq!(p.get(1), Some(&stone));
assert!(p.get(99).is_none());
}
#[test]
fn test_from_nbt() {
let s = BlockState::new("minecraft:grass_block").with_property("snowy", "true");
assert_eq!(BlockState::from_nbt(&s.to_nbt()).unwrap(), s);
}
#[test]
fn test_to_nbt() {
let nbt = BlockState::new("minecraft:stone").to_nbt();
let map = match nbt {
Tag::Compound(ref m) => m,
_ => panic!("expected compound"),
};
let name = match map.get("Name").unwrap() {
Tag::String(s) => s.as_str(),
_ => panic!("expected string"),
};
assert_eq!(name, "minecraft:stone");
}
#[test]
fn test_roundtrip() {
let states = vec![
BlockState::new("minecraft:air"),
BlockState::new("minecraft:stone"),
BlockState::new("minecraft:grass_block").with_property("snowy", "true"),
];
let mut palette = BlockPalette::new();
for s in &states {
palette.add_or_get(s.clone());
}
let mut ba = crate::bits::BitArray::new(4096, palette.bits_per_entry());
for i in 0..10 {
ba.set(i, (i % 3) as i64);
}
let (list, data) = palette.to_nbt(&ba);
let (palette2, ba2) = BlockPalette::from_nbt(&list, &data);
assert_eq!(palette.len(), palette2.len());
for i in 0..10 {
assert_eq!(ba.get(i), ba2.get(i));
}
}
}

View file

@ -0,0 +1,163 @@
use std::collections::HashMap;
use terrafier_nbt::Tag;
use crate::bits::BitArray;
use crate::palette::{BlockPalette, BlockState};
/// A 16×16×16 chunk section with palette-based block storage.
pub struct SectionData {
pub palette: BlockPalette,
pub storage: BitArray,
}
impl SectionData {
/// Create a new section filled with air.
pub fn new() -> Self {
let mut palette = BlockPalette::new();
let air = BlockState::new("minecraft:air");
palette.add_or_get(air);
let bpe = palette.bits_per_entry();
let storage = BitArray::new(4096, bpe);
// index 0 = air
SectionData { palette, storage }
}
/// Get the block at the given section-local coordinates.
///
/// Coordinates must be in 0..16.
pub fn get_block(&self, x: u8, y: u8, z: u8) -> Option<&BlockState> {
let idx = index(x, y, z);
let pal_idx = self.storage.get(idx) as u32;
self.palette.get(pal_idx)
}
/// Set the block at the given section-local coordinates.
///
/// Coordinates must be in 0..16.
pub fn set_block(&mut self, x: u8, y: u8, z: u8, block: BlockState) {
let idx = index(x, y, z);
let pal_idx = self.palette.add_or_get(block);
// Grow storage if bits_per_entry changed
let new_bpe = self.palette.bits_per_entry();
if new_bpe != self.storage.bits_per_entry {
let mut new_storage = BitArray::new(4096, new_bpe);
for i in 0..4096 {
let v = self.storage.get(i);
new_storage.set(i, v);
}
self.storage = new_storage;
}
self.storage.set(idx, pal_idx as i64);
}
/// Fill the entire section with one block type.
pub fn fill(&mut self, block: BlockState) {
let pal_idx = self.palette.add_or_get(block);
let bpe = self.palette.bits_per_entry();
self.storage = BitArray::new(4096, bpe);
// Set all entries to pal_idx
for i in 0..4096 {
self.storage.set(i, pal_idx as i64);
}
}
/// Number of unique block states in the palette.
pub fn palette_size(&self) -> usize {
self.palette.len()
}
/// Parse from an NBT `TAG_Compound` representing a section.
///
/// Expects `"BlockStates"` (LongArray) and `"Palette"` (List of Compound).
pub fn from_nbt(tag: &Tag) -> Option<Self> {
let compound = match tag {
Tag::Compound(map) => map,
_ => return None,
};
let block_states = match compound.get("BlockStates")? {
Tag::LongArray(arr) => arr,
_ => return None,
};
let palette_list = match compound.get("Palette")? {
Tag::List(list) => list,
_ => return None,
};
let (palette, storage) = BlockPalette::from_nbt(palette_list, block_states);
Some(SectionData { palette, storage })
}
/// Serialize to an NBT `TAG_Compound`.
pub fn to_nbt(&self) -> Tag {
let (palette_list, block_states) = self.palette.to_nbt(&self.storage);
let mut map = HashMap::new();
map.insert("BlockStates".to_string(), Tag::LongArray(block_states));
map.insert("Palette".to_string(), Tag::List(palette_list));
Tag::Compound(map)
}
}
/// Convert section-local (x, y, z) to linear index.
///
/// Minecraft order: y → z → x
fn index(x: u8, y: u8, z: u8) -> usize {
(y as usize) * 256 + (z as usize) * 16 + (x as usize)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_section() {
let sec = SectionData::new();
assert_eq!(sec.palette_size(), 1);
assert_eq!(
sec.get_block(0, 0, 0),
Some(&BlockState::new("minecraft:air"))
);
}
#[test]
fn test_set_get_block() {
let mut sec = SectionData::new();
let stone = BlockState::new("minecraft:stone");
sec.set_block(0, 0, 0, stone.clone());
assert_eq!(sec.get_block(0, 0, 0), Some(&stone));
assert_eq!(sec.palette_size(), 2);
}
#[test]
fn test_fill() {
let mut sec = SectionData::new();
let stone = BlockState::new("minecraft:stone");
sec.fill(stone.clone());
for x in 0..16 {
for y in 0..16 {
for z in 0..16 {
assert_eq!(sec.get_block(x, y, z), Some(&stone));
}
}
}
}
#[test]
fn test_nbt_roundtrip() {
let mut sec = SectionData::new();
sec.set_block(1, 2, 3, BlockState::new("minecraft:stone"));
sec.set_block(
4,
5,
6,
BlockState::new("minecraft:grass_block").with_property("snowy", "true"),
);
let nbt = sec.to_nbt();
let restored = SectionData::from_nbt(&nbt).unwrap();
assert_eq!(sec.palette_size(), restored.palette_size());
assert_eq!(sec.get_block(1, 2, 3), restored.get_block(1, 2, 3));
assert_eq!(sec.get_block(4, 5, 6), restored.get_block(4, 5, 6));
assert_eq!(sec.get_block(0, 0, 0), restored.get_block(0, 0, 0));
}
}