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:
commit
91835402b3
94 changed files with 12372 additions and 0 deletions
12
crates/fastanvil/Cargo.toml
Executable file
12
crates/fastanvil/Cargo.toml
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "terrafier-fastanvil"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "Fast Anvil/MCRegion format reader and writer for Minecraft world files"
|
||||
|
||||
[dependencies]
|
||||
terrafier-nbt = { path = "../nbt" }
|
||||
thiserror.workspace = true
|
||||
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
|
||||
rayon.workspace = true
|
||||
95
crates/fastanvil/src/compression.rs
Executable file
95
crates/fastanvil/src/compression.rs
Executable file
|
|
@ -0,0 +1,95 @@
|
|||
//! Compression utilities for Minecraft region files.
|
||||
//!
|
||||
//! Minecraft uses Zlib (deflate) compression for chunk data in .mca files.
|
||||
//! MCRegion files use GZip compression.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum CompressionError {
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("Decompression error: {0}")]
|
||||
Decompress(String),
|
||||
#[error("Compression error: {0}")]
|
||||
Compress(String),
|
||||
#[error("Unknown compression scheme: {0}")]
|
||||
UnknownScheme(u8),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, CompressionError>;
|
||||
|
||||
/// Compression type identifiers used in .mca chunk headers.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CompressionType {
|
||||
/// GZip compression (MCRegion, type=1)
|
||||
GZip,
|
||||
/// Zlib deflate compression (Anvil / .mca, type=2)
|
||||
Zlib,
|
||||
/// Uncompressed (type=3)
|
||||
Uncompressed,
|
||||
}
|
||||
|
||||
impl CompressionType {
|
||||
pub fn from_id(id: u8) -> Option<Self> {
|
||||
match id {
|
||||
1 => Some(CompressionType::GZip),
|
||||
2 => Some(CompressionType::Zlib),
|
||||
3 => Some(CompressionType::Uncompressed),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> u8 {
|
||||
match self {
|
||||
CompressionType::GZip => 1,
|
||||
CompressionType::Zlib => 2,
|
||||
CompressionType::Uncompressed => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Decompress chunk data given the compression type.
|
||||
pub fn decompress(data: &[u8], scheme: CompressionType) -> Result<Vec<u8>> {
|
||||
use std::io::Read;
|
||||
match scheme {
|
||||
CompressionType::GZip => {
|
||||
let mut dec = flate2::read::GzDecoder::new(data);
|
||||
let mut buf = Vec::new();
|
||||
dec.read_to_end(&mut buf)
|
||||
.map_err(|e| CompressionError::Decompress(e.to_string()))?;
|
||||
Ok(buf)
|
||||
}
|
||||
CompressionType::Zlib => {
|
||||
let mut dec = flate2::read::ZlibDecoder::new(data);
|
||||
let mut buf = Vec::new();
|
||||
dec.read_to_end(&mut buf)
|
||||
.map_err(|e| CompressionError::Decompress(e.to_string()))?;
|
||||
Ok(buf)
|
||||
}
|
||||
CompressionType::Uncompressed => Ok(data.to_vec()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Compress chunk data using the specified compression scheme.
|
||||
pub fn compress(data: &[u8], scheme: CompressionType) -> Result<Vec<u8>> {
|
||||
use std::io::Write;
|
||||
match scheme {
|
||||
CompressionType::GZip => {
|
||||
let mut enc = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
|
||||
enc.write_all(data)
|
||||
.map_err(|e| CompressionError::Compress(e.to_string()))?;
|
||||
enc.finish()
|
||||
.map_err(|e| CompressionError::Compress(e.to_string()))
|
||||
}
|
||||
CompressionType::Zlib => {
|
||||
let mut enc =
|
||||
flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
|
||||
enc.write_all(data)
|
||||
.map_err(|e| CompressionError::Compress(e.to_string()))?;
|
||||
enc.finish()
|
||||
.map_err(|e| CompressionError::Compress(e.to_string()))
|
||||
}
|
||||
CompressionType::Uncompressed => Ok(data.to_vec()),
|
||||
}
|
||||
}
|
||||
289
crates/fastanvil/src/io/chunk.rs
Executable file
289
crates/fastanvil/src/io/chunk.rs
Executable file
|
|
@ -0,0 +1,289 @@
|
|||
//! Chunk data structures for Minecraft Anvil format.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A parsed chunk from an Anvil (.mca) region file.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Chunk {
|
||||
pub x: i32,
|
||||
pub z: i32,
|
||||
pub data_version: i32,
|
||||
pub sections: Vec<ChunkSection>,
|
||||
pub block_entities: HashMap<String, HashMap<String, terrafier_nbt::Tag>>,
|
||||
pub heightmaps: HashMap<String, terrafier_nbt::Tag>,
|
||||
pub status: Option<String>,
|
||||
pub biomes: Vec<i32>,
|
||||
pub raw: HashMap<String, terrafier_nbt::Tag>,
|
||||
}
|
||||
|
||||
/// A single vertical section (16x16x16 blocks) within a chunk.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChunkSection {
|
||||
pub section_y: i8,
|
||||
pub palette: Vec<HashMap<String, terrafier_nbt::Tag>>,
|
||||
pub block_data: Vec<i64>,
|
||||
pub biome_palette: Vec<HashMap<String, terrafier_nbt::Tag>>,
|
||||
pub biome_data: Vec<i64>,
|
||||
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> {
|
||||
let compound = match tag {
|
||||
terrafier_nbt::Tag::Compound(m) => m,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let x = get_int(compound, "xPos")?;
|
||||
let z = get_int(compound, "zPos")?;
|
||||
let data_version = get_int(compound, "DataVersion").unwrap_or(0);
|
||||
|
||||
let mut sections = Vec::new();
|
||||
if let Some(terrafier_nbt::Tag::List(section_list)) = compound.get("sections") {
|
||||
for section_tag in section_list {
|
||||
if let Some(section) = ChunkSection::from_nbt(section_tag) {
|
||||
sections.push(section);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut block_entities = HashMap::new();
|
||||
if let Some(terrafier_nbt::Tag::List(entity_list)) = compound.get("block_entities") {
|
||||
for entity in entity_list {
|
||||
if let terrafier_nbt::Tag::Compound(m) = entity {
|
||||
let key = format!("{:?}", m.get("id"));
|
||||
block_entities.insert(key, m.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut heightmaps = HashMap::new();
|
||||
if let Some(terrafier_nbt::Tag::Compound(hm)) = compound.get("Heightmaps") {
|
||||
for (k, v) in hm {
|
||||
heightmaps.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let status = compound.get("Status").and_then(|t| match t {
|
||||
terrafier_nbt::Tag::String(s) => Some(s.clone()),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
let biomes = Vec::new();
|
||||
let raw = compound.clone();
|
||||
|
||||
Some(Self {
|
||||
x,
|
||||
z,
|
||||
data_version,
|
||||
sections,
|
||||
block_entities,
|
||||
heightmaps,
|
||||
status,
|
||||
biomes,
|
||||
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> {
|
||||
let compound = match tag {
|
||||
terrafier_nbt::Tag::Compound(m) => m,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let section_y = get_byte(compound, "Y")?;
|
||||
|
||||
let palette = if let Some(terrafier_nbt::Tag::List(list)) =
|
||||
compound.get("block_states").and_then(|t| match t {
|
||||
terrafier_nbt::Tag::Compound(m) => m.get("palette"),
|
||||
_ => None,
|
||||
}) {
|
||||
list.iter()
|
||||
.filter_map(|t| match t {
|
||||
terrafier_nbt::Tag::Compound(m) => Some(m.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let block_data =
|
||||
if let Some(terrafier_nbt::Tag::Compound(bs)) = compound.get("block_states") {
|
||||
if let Some(terrafier_nbt::Tag::LongArray(data)) = bs.get("data") {
|
||||
data.clone()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let biome_palette = compound
|
||||
.get("biomes")
|
||||
.and_then(|t| match t {
|
||||
terrafier_nbt::Tag::Compound(m) => m.get("palette"),
|
||||
_ => None,
|
||||
})
|
||||
.and_then(|t| {
|
||||
if let terrafier_nbt::Tag::List(list) = t {
|
||||
Some(
|
||||
list.iter()
|
||||
.filter_map(|t| match t {
|
||||
terrafier_nbt::Tag::Compound(m) => Some(m.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let biome_data = compound
|
||||
.get("biomes")
|
||||
.and_then(|t| match t {
|
||||
terrafier_nbt::Tag::Compound(m) => m.get("data"),
|
||||
_ => None,
|
||||
})
|
||||
.and_then(|t| {
|
||||
if let terrafier_nbt::Tag::LongArray(data) = t {
|
||||
Some(data.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let block_light = compound.get("BlockLight").and_then(|t| {
|
||||
if let terrafier_nbt::Tag::ByteArray(data) = t {
|
||||
Some(data.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let sky_light = compound.get("SkyLight").and_then(|t| {
|
||||
if let terrafier_nbt::Tag::ByteArray(data) = t {
|
||||
Some(data.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
Some(Self {
|
||||
section_y,
|
||||
palette,
|
||||
block_data,
|
||||
biome_palette,
|
||||
biome_data,
|
||||
block_light,
|
||||
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),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_byte(map: &HashMap<String, terrafier_nbt::Tag>, key: &str) -> Option<i8> {
|
||||
map.get(key).and_then(|t| match t {
|
||||
terrafier_nbt::Tag::Byte(v) => Some(*v),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
4
crates/fastanvil/src/io/mod.rs
Executable file
4
crates/fastanvil/src/io/mod.rs
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
//! Anvil format I/O — region file and chunk data structures.
|
||||
|
||||
pub mod chunk;
|
||||
pub mod region;
|
||||
227
crates/fastanvil/src/io/region.rs
Executable file
227
crates/fastanvil/src/io/region.rs
Executable file
|
|
@ -0,0 +1,227 @@
|
|||
//! Region file format (.mca / .mcr).
|
||||
//!
|
||||
//! A region file contains 32x32 chunks stored in a 8KB header
|
||||
//! (2KB location table + 2KB timestamp table + 4KB padding)
|
||||
//! followed by chunk data sectors of 4KB each.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::compression::{self, CompressionType};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum RegionError {
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("Compression error: {0}")]
|
||||
Compression(#[from] compression::CompressionError),
|
||||
#[error("Invalid region header at offset {0}")]
|
||||
InvalidHeader(u32),
|
||||
#[error("Chunk ({0}, {1}) not found in region")]
|
||||
ChunkNotFound(i32, i32),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, RegionError>;
|
||||
|
||||
/// A region file containing up to 32x32 chunks.
|
||||
pub struct Region {
|
||||
pub x: i32,
|
||||
pub z: i32,
|
||||
chunks: HashMap<(u8, u8), ChunkEntry>,
|
||||
}
|
||||
|
||||
pub struct ChunkEntry {
|
||||
pub offset: u32,
|
||||
pub size: u32,
|
||||
pub timestamp: u32,
|
||||
pub data: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl Region {
|
||||
/// Open a region file from raw bytes.
|
||||
pub fn from_bytes(x: i32, z: i32, data: &[u8]) -> Result<Self> {
|
||||
let mut reader = Cursor::new(data);
|
||||
let mut locations = [0u32; 1024];
|
||||
let mut timestamps = [0u32; 1024];
|
||||
|
||||
// Read location table (first 4096 bytes: 1024 entries x 4 bytes)
|
||||
for i in 0..1024 {
|
||||
let mut buf = [0u8; 4];
|
||||
reader.read_exact(&mut buf)?;
|
||||
locations[i] = u32::from_be_bytes(buf);
|
||||
}
|
||||
|
||||
// Read timestamp table (second 4096 bytes: 1024 entries x 4 bytes)
|
||||
for i in 0..1024 {
|
||||
let mut buf = [0u8; 4];
|
||||
reader.read_exact(&mut buf)?;
|
||||
timestamps[i] = u32::from_be_bytes(buf);
|
||||
}
|
||||
|
||||
let mut chunks = HashMap::new();
|
||||
|
||||
for i in 0..1024 {
|
||||
let loc = locations[i];
|
||||
if loc == 0 {
|
||||
continue;
|
||||
}
|
||||
let sector_offset = loc >> 8;
|
||||
let sector_count = loc & 0xFF;
|
||||
let timestamp = timestamps[i];
|
||||
|
||||
if sector_offset == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read chunk header: 4 bytes length (including 1 byte compression type)
|
||||
let byte_offset = (sector_offset as u64) * 4096;
|
||||
reader.seek(SeekFrom::Start(byte_offset))?;
|
||||
let mut len_buf = [0u8; 4];
|
||||
reader.read_exact(&mut len_buf)?;
|
||||
let chunk_data_len = u32::from_be_bytes(len_buf);
|
||||
|
||||
// Compression type byte follows the length
|
||||
let mut comp_type_buf = [0u8; 1];
|
||||
reader.read_exact(&mut comp_type_buf)?;
|
||||
let compression_scheme = comp_type_buf[0];
|
||||
|
||||
// Read compressed chunk payload
|
||||
let payload_len = if chunk_data_len > 0 {
|
||||
chunk_data_len as usize - 1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let mut compressed = vec![0u8; payload_len];
|
||||
reader.read_exact(&mut compressed)?;
|
||||
|
||||
// Determine compression type
|
||||
let scheme = match CompressionType::from_id(compression_scheme) {
|
||||
Some(s) => s,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
// Decompress
|
||||
let decompressed = compression::decompress(&compressed, scheme)?;
|
||||
|
||||
let local_x = (i % 32) as u8;
|
||||
let local_z = (i / 32) as u8;
|
||||
|
||||
chunks.insert(
|
||||
(local_x, local_z),
|
||||
ChunkEntry {
|
||||
offset: sector_offset,
|
||||
size: sector_count as u32,
|
||||
timestamp,
|
||||
data: Some(decompressed),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Self { x, z, chunks })
|
||||
}
|
||||
|
||||
/// Get the decompressed NBT data for a chunk at local coordinates (0..32).
|
||||
pub fn get_chunk_data(&self, local_x: u8, local_z: u8) -> Option<&[u8]> {
|
||||
self.chunks
|
||||
.get(&(local_x, local_z))
|
||||
.and_then(|e| e.data.as_deref())
|
||||
}
|
||||
|
||||
/// Create a new empty region.
|
||||
pub fn new(x: i32, z: i32) -> Self {
|
||||
Self {
|
||||
x,
|
||||
z,
|
||||
chunks: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set chunk data at local coordinates (0..32, 0..32).
|
||||
/// `data` should be decompressed NBT bytes.
|
||||
pub fn set_chunk_data(&mut self, local_x: u8, local_z: u8, data: Vec<u8>) {
|
||||
self.chunks.insert((local_x, local_z), ChunkEntry {
|
||||
offset: 0,
|
||||
size: 0,
|
||||
timestamp: 0,
|
||||
data: Some(data),
|
||||
});
|
||||
}
|
||||
|
||||
/// List all chunk coordinates present in this region.
|
||||
pub fn chunk_coords(&self) -> Vec<(u8, u8)> {
|
||||
let mut coords: Vec<_> = self.chunks.keys().copied().collect();
|
||||
coords.sort();
|
||||
coords
|
||||
}
|
||||
|
||||
/// Number of chunks in this region.
|
||||
pub fn chunk_count(&self) -> usize {
|
||||
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)
|
||||
}
|
||||
}
|
||||
15
crates/fastanvil/src/lib.rs
Executable file
15
crates/fastanvil/src/lib.rs
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
//! # Terrafier FastAnvil
|
||||
//!
|
||||
//! Fast reading and writing of Minecraft Anvil (.mca) and MCRegion (.mcr) files.
|
||||
//!
|
||||
//! Supports:
|
||||
//! - Reading existing regions
|
||||
//! - Writing new regions
|
||||
//! - Chunk-level access (compressed NBT data)
|
||||
//! - Parallel chunk processing
|
||||
|
||||
pub mod compression;
|
||||
pub mod io;
|
||||
|
||||
pub use io::chunk;
|
||||
pub use io::region;
|
||||
Loading…
Add table
Add a link
Reference in a new issue