use crate::block_entity::BlockEntity;
use crate::entity::Entity;
use crate::formats::error::Result;
use crate::BlockState;
use flate2::read::{GzDecoder, ZlibDecoder};
use flate2::write::ZlibEncoder;
use flate2::Compression;
use quartz_nbt::io::Flavor;
use quartz_nbt::{NbtCompound, NbtList, NbtTag};
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CompressionType {
Gzip = 1,
Zlib = 2,
Uncompressed = 3,
Lz4 = 4,
}
impl CompressionType {
pub fn from_byte(b: u8) -> Result<Self> {
match b {
1 => Ok(CompressionType::Gzip),
2 => Ok(CompressionType::Zlib),
3 => Ok(CompressionType::Uncompressed),
4 => Err("LZ4 compression (type 4) is not supported".into()),
_ => Err(format!("Unknown compression type: {}", b).into()),
}
}
}
#[derive(Debug, Clone)]
pub struct ChunkSection {
pub y: i8,
pub palette: Vec<BlockState>,
pub block_states: Vec<u16>,
pub biomes: Option<NbtCompound>,
}
#[derive(Debug, Clone)]
pub struct ChunkData {
pub x: i32,
pub z: i32,
pub data_version: i32,
pub status: String,
pub sections: Vec<ChunkSection>,
pub block_entities: Vec<BlockEntity>,
pub entities: Vec<Entity>,
pub y_pos: i32,
}
#[derive(Debug, Clone)]
pub struct McaFile {
pub chunks: Vec<Option<ChunkData>>,
pub region_x: i32,
pub region_z: i32,
}
pub fn is_mca(data: &[u8]) -> bool {
if data.len() < 8192 {
return false;
}
if data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04 {
return false;
}
for i in 0..1024 {
let offset = i * 4;
let loc_offset = ((data[offset] as u32) << 16)
| ((data[offset + 1] as u32) << 8)
| (data[offset + 2] as u32);
let sector_count = data[offset + 3];
if loc_offset >= 2 && sector_count > 0 {
return true;
}
}
false
}
impl McaFile {
pub fn from_bytes(data: &[u8], region_x: i32, region_z: i32) -> Result<Self> {
if data.len() < 8192 {
return Err("MCA file too small (< 8192 bytes)".into());
}
let mut chunks = Vec::with_capacity(1024);
for _ in 0..1024 {
chunks.push(None);
}
for i in 0..1024u32 {
let offset = (i as usize) * 4;
let loc_offset = ((data[offset] as u32) << 16)
| ((data[offset + 1] as u32) << 8)
| (data[offset + 2] as u32);
let sector_count = data[offset + 3] as u32;
if loc_offset < 2 || sector_count == 0 {
continue;
}
let byte_offset = (loc_offset as usize) * 4096;
if byte_offset + 5 > data.len() {
continue;
}
let chunk_len = ((data[byte_offset] as u32) << 24)
| ((data[byte_offset + 1] as u32) << 16)
| ((data[byte_offset + 2] as u32) << 8)
| (data[byte_offset + 3] as u32);
if chunk_len <= 1 {
continue;
}
let compression_byte = data[byte_offset + 4];
let compression = CompressionType::from_byte(compression_byte)?;
let compressed_start = byte_offset + 5;
let compressed_len = (chunk_len as usize) - 1;
if compressed_start + compressed_len > data.len() {
continue;
}
let compressed_data = &data[compressed_start..compressed_start + compressed_len];
let decompressed = decompress_chunk(compressed_data, compression)?;
let (nbt, _) =
quartz_nbt::io::read_nbt(&mut Cursor::new(&decompressed), Flavor::Uncompressed)?;
let chunk_x = (region_x * 32) + ((i % 32) as i32);
let chunk_z = (region_z * 32) + ((i / 32) as i32);
match parse_chunk_nbt(&nbt, chunk_x, chunk_z) {
Ok(chunk) => {
chunks[i as usize] = Some(chunk);
}
Err(_e) => {
continue;
}
}
}
Ok(McaFile {
chunks,
region_x,
region_z,
})
}
pub fn from_bytes_auto(data: &[u8]) -> Result<Self> {
let mut region_x = 0i32;
let mut region_z = 0i32;
let mut found = false;
if data.len() < 8192 {
return Err("MCA file too small (< 8192 bytes)".into());
}
for i in 0..1024u32 {
let offset = (i as usize) * 4;
let loc_offset = ((data[offset] as u32) << 16)
| ((data[offset + 1] as u32) << 8)
| (data[offset + 2] as u32);
let sector_count = data[offset + 3] as u32;
if loc_offset < 2 || sector_count == 0 {
continue;
}
let byte_offset = (loc_offset as usize) * 4096;
if byte_offset + 5 > data.len() {
continue;
}
let chunk_len = ((data[byte_offset] as u32) << 24)
| ((data[byte_offset + 1] as u32) << 16)
| ((data[byte_offset + 2] as u32) << 8)
| (data[byte_offset + 3] as u32);
if chunk_len <= 1 {
continue;
}
let compression_byte = data[byte_offset + 4];
let compression = match CompressionType::from_byte(compression_byte) {
Ok(c) => c,
Err(_) => continue,
};
let compressed_start = byte_offset + 5;
let compressed_len = (chunk_len as usize) - 1;
if compressed_start + compressed_len > data.len() {
continue;
}
let compressed_data = &data[compressed_start..compressed_start + compressed_len];
let decompressed = match decompress_chunk(compressed_data, compression) {
Ok(d) => d,
Err(_) => continue,
};
let (nbt, _) = match quartz_nbt::io::read_nbt(
&mut Cursor::new(&decompressed),
Flavor::Uncompressed,
) {
Ok(r) => r,
Err(_) => continue,
};
if let (Ok(cx), Ok(cz)) = (nbt.get::<_, i32>("xPos"), nbt.get::<_, i32>("zPos")) {
region_x = floor_div(cx, 32);
region_z = floor_div(cz, 32);
found = true;
break;
}
}
if !found {
region_x = 0;
region_z = 0;
}
Self::from_bytes(data, region_x, region_z)
}
}
fn decompress_chunk(data: &[u8], compression: CompressionType) -> Result<Vec<u8>> {
let mut decompressed = Vec::new();
match compression {
CompressionType::Zlib => {
let mut decoder = ZlibDecoder::new(data);
decoder.read_to_end(&mut decompressed)?;
}
CompressionType::Gzip => {
let mut decoder = GzDecoder::new(data);
decoder.read_to_end(&mut decompressed)?;
}
CompressionType::Uncompressed => {
decompressed = data.to_vec();
}
CompressionType::Lz4 => {
return Err("LZ4 compression is not supported".into());
}
}
Ok(decompressed)
}
fn parse_chunk_nbt(nbt: &NbtCompound, chunk_x: i32, chunk_z: i32) -> Result<ChunkData> {
let data_version = nbt.get::<_, i32>("DataVersion").unwrap_or(3700);
let status = nbt
.get::<_, &str>("Status")
.map(|s| s.to_string())
.unwrap_or_else(|_| "minecraft:full".to_string());
let x_pos = nbt.get::<_, i32>("xPos").unwrap_or(chunk_x);
let z_pos = nbt.get::<_, i32>("zPos").unwrap_or(chunk_z);
let y_pos = nbt.get::<_, i32>("yPos").unwrap_or(-4);
let mut sections = Vec::new();
if let Ok(section_list) = nbt.get::<_, &NbtList>("sections") {
for section_tag in section_list.iter() {
if let NbtTag::Compound(section_nbt) = section_tag {
if let Ok(section) = parse_section(section_nbt) {
sections.push(section);
}
}
}
}
let mut block_entities = Vec::new();
if let Ok(be_list) = nbt.get::<_, &NbtList>("block_entities") {
for be_tag in be_list.iter() {
if let NbtTag::Compound(be_nbt) = be_tag {
if let Ok(be) = parse_block_entity(be_nbt) {
block_entities.push(be);
}
}
}
}
let mut entities = Vec::new();
if let Ok(entity_list) = nbt.get::<_, &NbtList>("Entities") {
for entity_tag in entity_list.iter() {
if let NbtTag::Compound(entity_nbt) = entity_tag {
if let Ok(entity) = Entity::from_nbt(entity_nbt) {
entities.push(entity);
}
}
}
}
Ok(ChunkData {
x: x_pos,
z: z_pos,
data_version,
status,
sections,
block_entities,
entities,
y_pos,
})
}
fn parse_section(section_nbt: &NbtCompound) -> Result<ChunkSection> {
let y = section_nbt.get::<_, i8>("Y")?;
let block_states_compound = match section_nbt.get::<_, &NbtCompound>("block_states") {
Ok(bs) => bs,
Err(_) => {
return Ok(ChunkSection {
y,
palette: vec![BlockState::new("minecraft:air".to_string())],
block_states: vec![0; 4096],
biomes: section_nbt.get::<_, &NbtCompound>("biomes").ok().cloned(),
});
}
};
let palette = match block_states_compound.get::<_, &NbtList>("palette") {
Ok(palette_list) => {
let mut palette = Vec::new();
for tag in palette_list.iter() {
if let NbtTag::Compound(compound) = tag {
palette.push(BlockState::from_nbt(compound)?);
}
}
palette
}
Err(_) => {
vec![BlockState::new("minecraft:air".to_string())]
}
};
let block_states = if palette.len() <= 1 {
vec![0u16; 4096]
} else {
match block_states_compound.get::<_, &[i64]>("data") {
Ok(packed_data) => unpack_block_states(packed_data, palette.len()),
Err(_) => vec![0u16; 4096],
}
};
Ok(ChunkSection {
y,
palette,
block_states,
biomes: section_nbt.get::<_, &NbtCompound>("biomes").ok().cloned(),
})
}
pub fn single_biome_compound(biome: &str) -> NbtCompound {
let mut biomes = NbtCompound::new();
let biome_palette = vec![NbtTag::String(biome.to_string())];
biomes.insert("palette", NbtTag::List(NbtList::from(biome_palette)));
biomes
}
pub fn unpack_block_states(packed: &[i64], palette_size: usize) -> Vec<u16> {
let bits_per_entry = std::cmp::max(
(palette_size as f64).log2().ceil() as u32,
4, );
let entries_per_long = 64 / bits_per_entry;
let mask = (1u64 << bits_per_entry) - 1;
let mut result = Vec::with_capacity(4096);
for &long_val in packed {
let long_unsigned = long_val as u64;
for j in 0..entries_per_long {
if result.len() >= 4096 {
break;
}
let index = (long_unsigned >> (j * bits_per_entry)) & mask;
result.push(index as u16);
}
}
result.resize(4096, 0);
result
}
pub fn pack_block_states(indices: &[u16], palette_size: usize) -> Vec<i64> {
if palette_size <= 1 {
return Vec::new();
}
let bits_per_entry = std::cmp::max((palette_size as f64).log2().ceil() as u32, 4);
let entries_per_long = 64 / bits_per_entry;
let num_longs = 4096_usize.div_ceil(entries_per_long as usize);
let mask = (1u64 << bits_per_entry) - 1;
let mut packed = vec![0i64; num_longs];
for (i, &index) in indices.iter().enumerate().take(4096) {
let long_index = i / entries_per_long as usize;
let bit_offset = (i % entries_per_long as usize) as u32 * bits_per_entry;
let value = (index as u64) & mask;
packed[long_index] |= (value << bit_offset) as i64;
}
packed
}
fn parse_block_entity(nbt: &NbtCompound) -> Result<BlockEntity> {
let id = nbt
.get::<_, &str>("id")
.map(|s| s.to_string())
.unwrap_or_default();
let x = nbt.get::<_, i32>("x").unwrap_or(0);
let y = nbt.get::<_, i32>("y").unwrap_or(0);
let z = nbt.get::<_, i32>("z").unwrap_or(0);
let mut block_entity = BlockEntity::new(id, (x, y, z));
for (key, value) in nbt.inner() {
match key.as_str() {
"x" | "y" | "z" | "id" => continue,
_ => {
block_entity
.nbt_mut()
.insert(key.clone(), crate::utils::NbtValue::from_quartz_nbt(value));
}
}
}
Ok(block_entity)
}
#[derive(Debug, Clone)]
pub struct EntityChunkData {
pub chunk_x: i32,
pub chunk_z: i32,
pub entities: Vec<Entity>,
}
pub fn parse_entity_mca(data: &[u8], region_x: i32, region_z: i32) -> Result<Vec<EntityChunkData>> {
if data.len() < 8192 {
return Err("Entity MCA file too small (< 8192 bytes)".into());
}
let mut result = Vec::new();
for i in 0..1024u32 {
let offset = (i as usize) * 4;
let loc_offset = ((data[offset] as u32) << 16)
| ((data[offset + 1] as u32) << 8)
| (data[offset + 2] as u32);
let sector_count = data[offset + 3] as u32;
if loc_offset < 2 || sector_count == 0 {
continue;
}
let byte_offset = (loc_offset as usize) * 4096;
if byte_offset + 5 > data.len() {
continue;
}
let chunk_len = ((data[byte_offset] as u32) << 24)
| ((data[byte_offset + 1] as u32) << 16)
| ((data[byte_offset + 2] as u32) << 8)
| (data[byte_offset + 3] as u32);
if chunk_len <= 1 {
continue;
}
let compression_byte = data[byte_offset + 4];
let compression = match CompressionType::from_byte(compression_byte) {
Ok(c) => c,
Err(_) => continue,
};
let compressed_start = byte_offset + 5;
let compressed_len = (chunk_len as usize) - 1;
if compressed_start + compressed_len > data.len() {
continue;
}
let compressed_data = &data[compressed_start..compressed_start + compressed_len];
let decompressed = match decompress_chunk(compressed_data, compression) {
Ok(d) => d,
Err(_) => continue,
};
let (nbt, _) =
match quartz_nbt::io::read_nbt(&mut Cursor::new(&decompressed), Flavor::Uncompressed) {
Ok(r) => r,
Err(_) => continue,
};
let (chunk_x, chunk_z) = if let Ok(pos) = nbt.get::<_, &[i32]>("Position") {
if pos.len() >= 2 {
(pos[0], pos[1])
} else {
let cx = (region_x * 32) + ((i % 32) as i32);
let cz = (region_z * 32) + ((i / 32) as i32);
(cx, cz)
}
} else {
let cx = (region_x * 32) + ((i % 32) as i32);
let cz = (region_z * 32) + ((i / 32) as i32);
(cx, cz)
};
let mut entities = Vec::new();
if let Ok(entity_list) = nbt.get::<_, &NbtList>("Entities") {
for entity_tag in entity_list.iter() {
if let NbtTag::Compound(entity_nbt) = entity_tag {
if let Ok(entity) = Entity::from_nbt(entity_nbt) {
entities.push(entity);
}
}
}
}
if !entities.is_empty() {
result.push(EntityChunkData {
chunk_x,
chunk_z,
entities,
});
}
}
Ok(result)
}
fn build_entity_chunk_nbt(chunk: &EntityChunkData, data_version: i32) -> NbtCompound {
let mut root = NbtCompound::new();
root.insert("DataVersion", NbtTag::Int(data_version));
root.insert(
"Position",
NbtTag::IntArray(vec![chunk.chunk_x, chunk.chunk_z]),
);
let entity_tags: Vec<NbtTag> = chunk.entities.iter().map(|e| e.to_nbt()).collect();
root.insert("Entities", NbtTag::List(NbtList::from(entity_tags)));
root
}
pub fn write_entity_mca(
chunks: &[EntityChunkData],
_region_x: i32,
_region_z: i32,
data_version: i32,
) -> Result<Vec<u8>> {
let mut chunk_data_parts: Vec<(u32, Vec<u8>)> = Vec::new();
for chunk in chunks {
let local_x = floor_mod(chunk.chunk_x, 32) as u32;
let local_z = floor_mod(chunk.chunk_z, 32) as u32;
let index = local_x + local_z * 32;
let nbt = build_entity_chunk_nbt(chunk, data_version);
let mut nbt_bytes = Vec::new();
quartz_nbt::io::write_nbt(&mut nbt_bytes, None, &nbt, Flavor::Uncompressed)?;
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&nbt_bytes)?;
let compressed = encoder.finish()?;
chunk_data_parts.push((index, compressed));
}
chunk_data_parts.sort_by_key(|(idx, _)| *idx);
let mut location_table = vec![0u8; 4096];
let timestamp_table = vec![0u8; 4096];
let mut data_sectors = Vec::new();
let mut current_sector: u32 = 2;
for (index, compressed) in &chunk_data_parts {
let chunk_payload_len = compressed.len() as u32 + 1;
let total_len = 4 + chunk_payload_len;
let sector_count = (total_len as usize).div_ceil(4096);
let loc_offset = *index as usize * 4;
location_table[loc_offset] = ((current_sector >> 16) & 0xFF) as u8;
location_table[loc_offset + 1] = ((current_sector >> 8) & 0xFF) as u8;
location_table[loc_offset + 2] = (current_sector & 0xFF) as u8;
location_table[loc_offset + 3] = sector_count as u8;
let mut chunk_sector = Vec::new();
chunk_sector.push(((chunk_payload_len >> 24) & 0xFF) as u8);
chunk_sector.push(((chunk_payload_len >> 16) & 0xFF) as u8);
chunk_sector.push(((chunk_payload_len >> 8) & 0xFF) as u8);
chunk_sector.push((chunk_payload_len & 0xFF) as u8);
chunk_sector.push(2); chunk_sector.extend_from_slice(compressed);
let padded_len = sector_count * 4096;
chunk_sector.resize(padded_len, 0);
data_sectors.extend_from_slice(&chunk_sector);
current_sector += sector_count as u32;
}
let mut result = Vec::new();
result.extend_from_slice(&location_table);
result.extend_from_slice(×tamp_table);
result.extend_from_slice(&data_sectors);
Ok(result)
}
impl McaFile {
pub fn to_bytes(&self) -> Result<Vec<u8>> {
let mut chunk_data_parts: Vec<(u32, Vec<u8>)> = Vec::new();
for (i, chunk_opt) in self.chunks.iter().enumerate() {
if let Some(chunk) = chunk_opt {
let nbt = build_chunk_nbt(chunk);
let mut nbt_bytes = Vec::new();
quartz_nbt::io::write_nbt(&mut nbt_bytes, None, &nbt, Flavor::Uncompressed)?;
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&nbt_bytes)?;
let compressed = encoder.finish()?;
chunk_data_parts.push((i as u32, compressed));
}
}
let mut location_table = vec![0u8; 4096];
let timestamp_table = vec![0u8; 4096];
let mut data_sectors = Vec::new();
let mut current_sector: u32 = 2;
for (index, compressed) in &chunk_data_parts {
let chunk_payload_len = compressed.len() as u32 + 1; let total_len = 4 + chunk_payload_len;
let sector_count = (total_len as usize).div_ceil(4096);
let loc_offset = *index as usize * 4;
location_table[loc_offset] = ((current_sector >> 16) & 0xFF) as u8;
location_table[loc_offset + 1] = ((current_sector >> 8) & 0xFF) as u8;
location_table[loc_offset + 2] = (current_sector & 0xFF) as u8;
location_table[loc_offset + 3] = sector_count as u8;
let mut chunk_sector = Vec::new();
chunk_sector.push(((chunk_payload_len >> 24) & 0xFF) as u8);
chunk_sector.push(((chunk_payload_len >> 16) & 0xFF) as u8);
chunk_sector.push(((chunk_payload_len >> 8) & 0xFF) as u8);
chunk_sector.push((chunk_payload_len & 0xFF) as u8);
chunk_sector.push(2);
chunk_sector.extend_from_slice(compressed);
let padded_len = sector_count * 4096;
chunk_sector.resize(padded_len, 0);
data_sectors.extend_from_slice(&chunk_sector);
current_sector += sector_count as u32;
}
let mut result = Vec::new();
result.extend_from_slice(&location_table);
result.extend_from_slice(×tamp_table);
result.extend_from_slice(&data_sectors);
Ok(result)
}
}
fn build_chunk_nbt(chunk: &ChunkData) -> NbtCompound {
let mut root = NbtCompound::new();
root.insert("DataVersion", NbtTag::Int(chunk.data_version));
root.insert("xPos", NbtTag::Int(chunk.x));
root.insert("yPos", NbtTag::Int(chunk.y_pos));
root.insert("zPos", NbtTag::Int(chunk.z));
root.insert("Status", NbtTag::String(chunk.status.clone()));
let mut section_list = Vec::new();
for section in &chunk.sections {
section_list.push(NbtTag::Compound(build_section_nbt(section)));
}
root.insert("sections", NbtTag::List(NbtList::from(section_list)));
let mut be_list = Vec::new();
for be in &chunk.block_entities {
let mut be_nbt = be.to_nbt();
be_nbt.insert("x", NbtTag::Int(be.position.0));
be_nbt.insert("y", NbtTag::Int(be.position.1));
be_nbt.insert("z", NbtTag::Int(be.position.2));
be_nbt.insert("id", NbtTag::String(be.id.clone()));
be_list.push(NbtTag::Compound(be_nbt));
}
root.insert("block_entities", NbtTag::List(NbtList::from(be_list)));
root.insert("Heightmaps", NbtTag::Compound(compute_heightmaps(chunk)));
root.insert("isLightOn", NbtTag::Byte(0));
root
}
fn compute_heightmaps(chunk: &ChunkData) -> NbtCompound {
let world_min_y = chunk.y_pos * 16;
let mut motion_blocking = vec![0i32; 256];
let mut world_surface = vec![0i32; 256];
let mut sorted_sections: Vec<&ChunkSection> = chunk.sections.iter().collect();
sorted_sections.sort_by(|a, b| b.y.cmp(&a.y));
for lz in 0..16usize {
for lx in 0..16usize {
let col_idx = lz * 16 + lx;
'outer: for section in &sorted_sections {
let section_base_y = (section.y as i32) * 16;
for ly in (0..16i32).rev() {
let block_idx = (ly * 256 + lz as i32 * 16 + lx as i32) as usize;
let palette_idx = section.block_states[block_idx] as usize;
if palette_idx >= section.palette.len() {
continue;
}
let name = §ion.palette[palette_idx].name;
if !matches!(
name.as_str(),
"minecraft:air" | "minecraft:cave_air" | "minecraft:void_air"
) {
let world_y = section_base_y + ly;
let hm_value = world_y - world_min_y + 1;
motion_blocking[col_idx] = hm_value;
world_surface[col_idx] = hm_value;
break 'outer;
}
}
}
}
}
let mut heightmaps = NbtCompound::new();
heightmaps.insert(
"MOTION_BLOCKING",
NbtTag::LongArray(pack_heightmap(&motion_blocking)),
);
heightmaps.insert(
"WORLD_SURFACE",
NbtTag::LongArray(pack_heightmap(&world_surface)),
);
heightmaps
}
fn pack_heightmap(values: &[i32]) -> Vec<i64> {
let bits_per_entry: usize = 9;
let entries_per_long = 64 / bits_per_entry; let num_longs = 256_usize.div_ceil(entries_per_long); let mask = (1u64 << bits_per_entry) - 1;
let mut packed = vec![0i64; num_longs];
for (i, &value) in values.iter().enumerate().take(256) {
let long_index = i / entries_per_long;
let bit_offset = (i % entries_per_long) * bits_per_entry;
let v = (value as u64) & mask;
packed[long_index] |= (v << bit_offset) as i64;
}
packed
}
fn build_section_nbt(section: &ChunkSection) -> NbtCompound {
let mut section_nbt = NbtCompound::new();
section_nbt.insert("Y", NbtTag::Byte(section.y));
let mut block_states_compound = NbtCompound::new();
let palette_nbt: Vec<NbtTag> = section.palette.iter().map(|bs| bs.to_nbt()).collect();
block_states_compound.insert("palette", NbtTag::List(NbtList::from(palette_nbt)));
if section.palette.len() > 1 {
let packed = pack_block_states(§ion.block_states, section.palette.len());
block_states_compound.insert("data", NbtTag::LongArray(packed));
}
section_nbt.insert("block_states", NbtTag::Compound(block_states_compound));
let biomes = match §ion.biomes {
Some(b) => b.clone(),
None => single_biome_compound("minecraft:plains"),
};
section_nbt.insert("biomes", NbtTag::Compound(biomes));
section_nbt
}
pub struct RegionReader<R: Read + Seek> {
reader: R,
region_x: i32,
region_z: i32,
locations: Vec<(u32, u64)>,
}
impl<R: Read + Seek> RegionReader<R> {
pub fn new(mut reader: R, region_x: i32, region_z: i32) -> Result<Self> {
let mut header = [0u8; 4096];
reader
.read_exact(&mut header)
.map_err(|_| "MCA file too small (< 4096 byte header)")?;
let mut locations = Vec::new();
for i in 0..1024u32 {
let o = (i as usize) * 4;
let loc =
((header[o] as u32) << 16) | ((header[o + 1] as u32) << 8) | (header[o + 2] as u32);
let sector_count = header[o + 3];
if loc >= 2 && sector_count > 0 {
locations.push((i, (loc as u64) * 4096));
}
}
Ok(Self {
reader,
region_x,
region_z,
locations,
})
}
pub fn new_auto(reader: R) -> Result<Self> {
let mut rr = Self::new(reader, 0, 0)?;
let indices: Vec<u32> = rr.locations.iter().map(|(i, _)| *i).collect();
for i in indices {
if let Ok(Some(nbt)) = rr.read_chunk_nbt_at(i) {
if let (Ok(cx), Ok(cz)) = (nbt.get::<_, i32>("xPos"), nbt.get::<_, i32>("zPos")) {
rr.region_x = floor_div(cx, 32);
rr.region_z = floor_div(cz, 32);
return Ok(rr);
}
}
}
Err("Could not determine region coordinates from MCA data".into())
}
pub fn region_position(&self) -> (i32, i32) {
(self.region_x, self.region_z)
}
pub fn chunk_positions(&self) -> Vec<(i32, i32)> {
self.locations
.iter()
.map(|(i, _)| {
(
self.region_x * 32 + (*i % 32) as i32,
self.region_z * 32 + (*i / 32) as i32,
)
})
.collect()
}
pub fn read_chunk(&mut self, cx: i32, cz: i32) -> Result<Option<ChunkData>> {
let local_x = cx - self.region_x * 32;
let local_z = cz - self.region_z * 32;
if !(0..32).contains(&local_x) || !(0..32).contains(&local_z) {
return Ok(None);
}
let index = (local_z * 32 + local_x) as u32;
debug_assert!(index < 1024, "local chunk index out of range");
match self.read_chunk_nbt_at(index)? {
None => Ok(None),
Some(nbt) => Ok(Some(parse_chunk_nbt(&nbt, cx, cz)?)),
}
}
fn read_chunk_nbt_at(&mut self, index: u32) -> Result<Option<quartz_nbt::NbtCompound>> {
let offset = match self.locations.iter().find(|(i, _)| *i == index) {
Some((_, off)) => *off,
None => return Ok(None),
};
self.reader.seek(SeekFrom::Start(offset))?;
let mut head = [0u8; 5];
self.reader.read_exact(&mut head)?;
let chunk_len = u32::from_be_bytes([head[0], head[1], head[2], head[3]]);
if chunk_len <= 1 {
return Ok(None);
}
let compression = CompressionType::from_byte(head[4])?;
let mut compressed = vec![0u8; (chunk_len as usize) - 1];
self.reader.read_exact(&mut compressed)?;
let decompressed = decompress_chunk(&compressed, compression)?;
let (nbt, _) =
quartz_nbt::io::read_nbt(&mut Cursor::new(&decompressed), Flavor::Uncompressed)?;
Ok(Some(nbt))
}
}
pub fn floor_div(a: i32, b: i32) -> i32 {
let d = a / b;
let r = a % b;
if (r != 0) && ((r ^ b) < 0) {
d - 1
} else {
d
}
}
pub fn floor_mod(a: i32, b: i32) -> i32 {
((a % b) + b) % b
}
#[cfg(test)]
mod tests {
use super::*;
use smol_str::SmolStr;
#[test]
fn test_floor_div() {
assert_eq!(floor_div(7, 32), 0);
assert_eq!(floor_div(32, 32), 1);
assert_eq!(floor_div(-1, 32), -1);
assert_eq!(floor_div(-32, 32), -1);
assert_eq!(floor_div(-33, 32), -2);
assert_eq!(floor_div(0, 32), 0);
}
#[test]
fn test_floor_mod() {
assert_eq!(floor_mod(0, 32), 0);
assert_eq!(floor_mod(1, 32), 1);
assert_eq!(floor_mod(31, 32), 31);
assert_eq!(floor_mod(32, 32), 0);
assert_eq!(floor_mod(-1, 32), 31);
assert_eq!(floor_mod(-32, 32), 0);
}
#[test]
fn test_chunk_index_formula() {
assert_eq!((0u32 & 31) + (0u32 & 31) * 32, 0);
assert_eq!((1u32 & 31) + (0u32 & 31) * 32, 1);
assert_eq!((0u32 & 31) + (1u32 & 31) * 32, 32);
assert_eq!((31u32 & 31) + (31u32 & 31) * 32, 1023);
assert_eq!((5u32 & 31) + (10u32 & 31) * 32, 325);
for i in 0..1024u32 {
let x = i % 32;
let z = i / 32;
assert_eq!((x & 31) + (z & 31) * 32, i);
}
}
#[test]
fn test_pack_unpack_roundtrip() {
let mut indices = vec![0u16; 4096];
indices[0] = 1;
indices[1] = 2;
indices[15] = 3;
indices[256] = 4;
indices[4095] = 5;
let palette_size = 6;
let packed = pack_block_states(&indices, palette_size);
let unpacked = unpack_block_states(&packed, palette_size);
assert_eq!(indices, unpacked);
}
#[test]
fn test_pack_unpack_4bit_minimum() {
let mut indices = vec![0u16; 4096];
indices[0] = 2;
indices[100] = 1;
let palette_size = 3;
let packed = pack_block_states(&indices, palette_size);
assert_eq!(packed.len(), 256);
let unpacked = unpack_block_states(&packed, palette_size);
assert_eq!(indices, unpacked);
}
#[test]
fn test_pack_unpack_exact_4bit_palette() {
let mut indices = vec![0u16; 4096];
for i in 0..4096 {
indices[i] = (i % 16) as u16;
}
let palette_size = 16;
let packed = pack_block_states(&indices, palette_size);
assert_eq!(packed.len(), 256); let unpacked = unpack_block_states(&packed, palette_size);
assert_eq!(indices, unpacked);
}
#[test]
fn test_pack_unpack_5bit() {
let mut indices = vec![0u16; 4096];
for i in 0..4096 {
indices[i] = (i % 32) as u16;
}
let palette_size = 32;
let packed = pack_block_states(&indices, palette_size);
assert_eq!(packed.len(), 342);
let unpacked = unpack_block_states(&packed, palette_size);
assert_eq!(indices, unpacked);
}
#[test]
fn test_pack_unpack_6bit() {
let mut indices = vec![0u16; 4096];
for i in 0..4096 {
indices[i] = (i % 64) as u16;
}
let palette_size = 64;
let packed = pack_block_states(&indices, palette_size);
assert_eq!(packed.len(), 410);
let unpacked = unpack_block_states(&packed, palette_size);
assert_eq!(indices, unpacked);
}
#[test]
fn test_pack_unpack_8bit() {
let mut indices = vec![0u16; 4096];
for i in 0..4096 {
indices[i] = (i % 256) as u16;
}
let palette_size = 256;
let packed = pack_block_states(&indices, palette_size);
assert_eq!(packed.len(), 512); let unpacked = unpack_block_states(&packed, palette_size);
assert_eq!(indices, unpacked);
}
#[test]
fn test_pack_unpack_12bit_max() {
let mut indices = vec![0u16; 4096];
for i in 0..4096 {
indices[i] = i as u16;
}
let palette_size = 4096;
let packed = pack_block_states(&indices, palette_size);
assert_eq!(packed.len(), 820);
let unpacked = unpack_block_states(&packed, palette_size);
assert_eq!(indices, unpacked);
}
#[test]
fn test_entries_dont_span_long_boundaries() {
let palette_size = 32; let entries_per_long = 64 / 5; assert_eq!(entries_per_long, 12);
let wasted_bits = 64 - entries_per_long * 5; assert_eq!(wasted_bits, 4);
let mut indices = vec![0u16; 4096];
indices[11] = 31; indices[12] = 31;
let packed = pack_block_states(&indices, palette_size);
let first_long = packed[0] as u64;
let entry_11 = (first_long >> (11 * 5)) & 0x1F;
assert_eq!(entry_11, 31);
let padding_bits = first_long >> 60;
assert_eq!(padding_bits, 0);
let second_long = packed[1] as u64;
let entry_12 = second_long & 0x1F;
assert_eq!(entry_12, 31);
let unpacked = unpack_block_states(&packed, palette_size);
assert_eq!(indices, unpacked);
}
#[test]
fn test_single_palette_no_data() {
let packed = pack_block_states(&[0u16; 4096], 1);
assert!(packed.is_empty());
}
#[test]
fn test_is_mca_detection() {
assert!(!is_mca(&[]));
assert!(!is_mca(&[0; 100]));
assert!(!is_mca(&[0; 8191]));
assert!(!is_mca(&[0; 8192]));
let mut data = vec![0u8; 8192 + 4096];
data[0] = 0;
data[1] = 0;
data[2] = 2;
data[3] = 1;
assert!(is_mca(&data));
}
#[test]
fn test_is_mca_rejects_offset_less_than_2() {
let mut data = vec![0u8; 8192 + 4096];
data[0] = 0;
data[1] = 0;
data[2] = 1;
data[3] = 1;
assert!(!is_mca(&data));
}
#[test]
fn test_mca_header_layout() {
let mca = make_single_chunk_mca(0, 0, 0);
let bytes = mca.to_bytes().unwrap();
assert!(bytes.len() >= 8192 + 4096);
assert_eq!(bytes.len() % 4096, 0);
let offset = ((bytes[0] as u32) << 16) | ((bytes[1] as u32) << 8) | (bytes[2] as u32);
let sector_count = bytes[3];
assert_eq!(offset, 2);
assert!(sector_count >= 1);
for i in 1..1024 {
let off = i * 4;
let loc = ((bytes[off] as u32) << 16)
| ((bytes[off + 1] as u32) << 8)
| (bytes[off + 2] as u32);
let cnt = bytes[off + 3];
assert_eq!(loc, 0, "slot {} should have offset 0", i);
assert_eq!(cnt, 0, "slot {} should have count 0", i);
}
}
#[test]
fn test_mca_chunk_data_layout() {
let mca = make_single_chunk_mca(0, 0, 0);
let bytes = mca.to_bytes().unwrap();
let data_start = 8192;
let length = ((bytes[data_start] as u32) << 24)
| ((bytes[data_start + 1] as u32) << 16)
| ((bytes[data_start + 2] as u32) << 8)
| (bytes[data_start + 3] as u32);
assert!(length > 1, "length must include at least compression byte");
let compression = bytes[data_start + 4];
assert_eq!(compression, 2, "should be zlib (type 2)");
let compressed_len = (length - 1) as usize;
assert!(compressed_len > 0);
let compressed = &bytes[data_start + 5..data_start + 5 + compressed_len];
let decompressed = decompress_chunk(compressed, CompressionType::Zlib).unwrap();
assert!(!decompressed.is_empty());
let (nbt, _) =
quartz_nbt::io::read_nbt(&mut Cursor::new(&decompressed), Flavor::Uncompressed)
.unwrap();
assert!(nbt.get::<_, i32>("DataVersion").is_ok());
}
#[test]
fn test_mca_sector_alignment() {
let mca = make_single_chunk_mca(0, 0, 0);
let bytes = mca.to_bytes().unwrap();
assert_eq!(bytes.len() % 4096, 0, "file size must be 4096-byte aligned");
let offset = ((bytes[0] as u32) << 16) | ((bytes[1] as u32) << 8) | (bytes[2] as u32);
let sector_count = bytes[3] as usize;
let chunk_data_start = (offset as usize) * 4096;
let chunk_data_end = chunk_data_start + sector_count * 4096;
assert!(chunk_data_end <= bytes.len());
}
#[test]
fn test_mca_multiple_chunks_indexing() {
let mut chunks: Vec<Option<ChunkData>> = (0..1024).map(|_| None).collect();
chunks[0] = Some(make_chunk(0, 0));
chunks[101] = Some(make_chunk(5, 3));
chunks[1023] = Some(make_chunk(31, 31));
let mca = McaFile {
chunks,
region_x: 0,
region_z: 0,
};
let bytes = mca.to_bytes().unwrap();
let mca2 = McaFile::from_bytes(&bytes, 0, 0).unwrap();
assert!(mca2.chunks[0].is_some());
assert!(mca2.chunks[101].is_some());
assert!(mca2.chunks[1023].is_some());
assert_eq!(mca2.chunks[0].as_ref().unwrap().x, 0);
assert_eq!(mca2.chunks[0].as_ref().unwrap().z, 0);
assert_eq!(mca2.chunks[101].as_ref().unwrap().x, 5);
assert_eq!(mca2.chunks[101].as_ref().unwrap().z, 3);
assert_eq!(mca2.chunks[1023].as_ref().unwrap().x, 31);
assert_eq!(mca2.chunks[1023].as_ref().unwrap().z, 31);
let count = mca2.chunks.iter().filter(|c| c.is_some()).count();
assert_eq!(count, 3);
}
#[test]
fn test_mca_negative_region_coords() {
let mut chunks: Vec<Option<ChunkData>> = (0..1024).map(|_| None).collect();
chunks[0] = Some(make_chunk(-32, -64));
let mca = McaFile {
chunks,
region_x: -1,
region_z: -2,
};
let bytes = mca.to_bytes().unwrap();
let mca2 = McaFile::from_bytes(&bytes, -1, -2).unwrap();
let chunk = mca2.chunks[0].as_ref().unwrap();
assert_eq!(chunk.x, -32);
assert_eq!(chunk.z, -64);
}
#[test]
fn test_chunk_nbt_has_required_fields() {
let chunk = make_chunk(5, 10);
let nbt = build_chunk_nbt(&chunk);
assert_eq!(nbt.get::<_, i32>("DataVersion").unwrap(), 3700);
assert_eq!(nbt.get::<_, i32>("xPos").unwrap(), 5);
assert_eq!(nbt.get::<_, i32>("zPos").unwrap(), 10);
assert_eq!(nbt.get::<_, i32>("yPos").unwrap(), -4);
assert_eq!(nbt.get::<_, &str>("Status").unwrap(), "minecraft:full");
assert!(nbt.get::<_, &NbtList>("sections").is_ok());
assert!(nbt.get::<_, &NbtList>("block_entities").is_ok());
}
#[test]
fn test_section_nbt_structure() {
let section = ChunkSection {
y: 4,
palette: vec![
BlockState::new("minecraft:air".to_string()),
BlockState::new("minecraft:stone".to_string()),
],
block_states: vec![0u16; 4096],
biomes: None,
};
let nbt = build_section_nbt(§ion);
assert_eq!(nbt.get::<_, i8>("Y").unwrap(), 4);
let bs = nbt.get::<_, &NbtCompound>("block_states").unwrap();
assert!(bs.get::<_, &NbtList>("palette").is_ok());
assert!(bs.get::<_, &[i64]>("data").is_ok());
let biomes = nbt.get::<_, &NbtCompound>("biomes").unwrap();
assert!(biomes.get::<_, &NbtList>("palette").is_ok());
}
#[test]
fn test_section_single_palette_omits_data() {
let section = ChunkSection {
y: 0,
palette: vec![BlockState::new("minecraft:air".to_string())],
block_states: vec![0u16; 4096],
biomes: None,
};
let nbt = build_section_nbt(§ion);
let bs = nbt.get::<_, &NbtCompound>("block_states").unwrap();
assert!(
bs.get::<_, &[i64]>("data").is_err(),
"single-palette section should omit data"
);
}
#[test]
fn test_palette_nbt_format() {
let section = ChunkSection {
y: 0,
palette: vec![
BlockState::new("minecraft:air".to_string()),
BlockState::new("minecraft:oak_stairs".to_string())
.with_property("facing".to_string(), "north".to_string())
.with_property("half".to_string(), "bottom".to_string()),
],
block_states: vec![0u16; 4096],
biomes: None,
};
let nbt = build_section_nbt(§ion);
let bs = nbt.get::<_, &NbtCompound>("block_states").unwrap();
let palette = bs.get::<_, &NbtList>("palette").unwrap();
if let NbtTag::Compound(entry) = &palette[0] {
assert_eq!(entry.get::<_, &str>("Name").unwrap(), "minecraft:air");
assert!(entry.get::<_, &NbtCompound>("Properties").is_err());
} else {
panic!("palette entry should be Compound");
}
if let NbtTag::Compound(entry) = &palette[1] {
assert_eq!(
entry.get::<_, &str>("Name").unwrap(),
"minecraft:oak_stairs"
);
let props = entry.get::<_, &NbtCompound>("Properties").unwrap();
assert_eq!(props.get::<_, &str>("facing").unwrap(), "north");
assert_eq!(props.get::<_, &str>("half").unwrap(), "bottom");
} else {
panic!("palette entry should be Compound");
}
}
#[test]
fn test_block_entity_roundtrip() {
let mut be = BlockEntity::new("minecraft:chest".to_string(), (10, 64, 20));
be.nbt_mut().insert(
"CustomName".to_string(),
crate::utils::NbtValue::String("Test Chest".to_string()),
);
let section = ChunkSection {
y: 4,
palette: vec![
BlockState::new("minecraft:air".to_string()),
BlockState::new("minecraft:chest".to_string()),
],
block_states: {
let mut bs = vec![0u16; 4096];
bs[0] = 1;
bs
},
biomes: None,
};
let chunk = ChunkData {
x: 0,
z: 1,
data_version: 3700,
status: "minecraft:full".to_string(),
sections: vec![section],
block_entities: vec![be],
entities: Vec::new(),
y_pos: -4,
};
let mut chunks: Vec<Option<ChunkData>> = (0..1024).map(|_| None).collect();
chunks[32] = Some(chunk);
let mca = McaFile {
chunks,
region_x: 0,
region_z: 0,
};
let bytes = mca.to_bytes().unwrap();
let mca2 = McaFile::from_bytes(&bytes, 0, 0).unwrap();
let chunk2 = mca2.chunks[32].as_ref().unwrap();
assert_eq!(chunk2.block_entities.len(), 1);
assert_eq!(chunk2.block_entities[0].id, "minecraft:chest");
assert_eq!(chunk2.block_entities[0].position, (10, 64, 20));
}
#[test]
fn test_compression_type_values() {
assert_eq!(
CompressionType::from_byte(1).unwrap(),
CompressionType::Gzip
);
assert_eq!(
CompressionType::from_byte(2).unwrap(),
CompressionType::Zlib
);
assert_eq!(
CompressionType::from_byte(3).unwrap(),
CompressionType::Uncompressed
);
assert!(CompressionType::from_byte(4).is_err());
assert!(CompressionType::from_byte(5).is_err());
assert!(CompressionType::from_byte(127).is_err());
}
#[test]
fn test_multiple_sections_y_ordering() {
let sections = vec![
make_section(-4, "minecraft:bedrock"),
make_section(0, "minecraft:stone"),
make_section(4, "minecraft:air"),
];
let chunk = ChunkData {
x: 0,
z: 0,
data_version: 3700,
status: "minecraft:full".to_string(),
sections,
block_entities: Vec::new(),
entities: Vec::new(),
y_pos: -4,
};
let mut chunks: Vec<Option<ChunkData>> = (0..1024).map(|_| None).collect();
chunks[0] = Some(chunk);
let mca = McaFile {
chunks,
region_x: 0,
region_z: 0,
};
let bytes = mca.to_bytes().unwrap();
let mca2 = McaFile::from_bytes(&bytes, 0, 0).unwrap();
let chunk2 = mca2.chunks[0].as_ref().unwrap();
assert_eq!(chunk2.sections.len(), 3);
let ys: Vec<i8> = chunk2.sections.iter().map(|s| s.y).collect();
assert!(ys.contains(&-4));
assert!(ys.contains(&0));
assert!(ys.contains(&4));
}
#[test]
fn test_mca_roundtrip() {
let mut section = ChunkSection {
y: 0,
palette: vec![
BlockState::new("minecraft:air".to_string()),
BlockState::new("minecraft:stone".to_string()),
],
block_states: vec![0u16; 4096],
biomes: None,
};
section.block_states[0] = 1;
section.block_states[1] = 1;
section.block_states[100] = 1;
let chunk = ChunkData {
x: 0,
z: 0,
data_version: 3700,
status: "minecraft:full".to_string(),
sections: vec![section],
block_entities: Vec::new(),
entities: Vec::new(),
y_pos: -4,
};
let mut chunks: Vec<Option<ChunkData>> = (0..1024).map(|_| None).collect();
chunks[0] = Some(chunk);
let mca = McaFile {
chunks,
region_x: 0,
region_z: 0,
};
let bytes = mca.to_bytes().unwrap();
assert!(is_mca(&bytes));
let mca2 = McaFile::from_bytes(&bytes, 0, 0).unwrap();
let chunk2 = mca2.chunks[0].as_ref().unwrap();
assert_eq!(chunk2.x, 0);
assert_eq!(chunk2.z, 0);
assert_eq!(chunk2.data_version, 3700);
assert_eq!(chunk2.status, "minecraft:full");
assert_eq!(chunk2.y_pos, -4);
assert_eq!(chunk2.sections.len(), 1);
assert_eq!(chunk2.sections[0].palette.len(), 2);
assert_eq!(chunk2.sections[0].palette[0].name, "minecraft:air");
assert_eq!(chunk2.sections[0].palette[1].name, "minecraft:stone");
assert_eq!(chunk2.sections[0].block_states[0], 1);
assert_eq!(chunk2.sections[0].block_states[1], 1);
assert_eq!(chunk2.sections[0].block_states[100], 1);
assert_eq!(chunk2.sections[0].block_states[2], 0);
}
#[test]
fn test_mca_roundtrip_with_properties() {
let section = ChunkSection {
y: 0,
palette: vec![
BlockState::new("minecraft:air".to_string()),
BlockState::new("minecraft:redstone_wire".to_string())
.with_property("power".to_string(), "15".to_string())
.with_property("east".to_string(), "side".to_string()),
BlockState::new("minecraft:oak_stairs".to_string())
.with_property("facing".to_string(), "north".to_string())
.with_property("half".to_string(), "top".to_string())
.with_property("shape".to_string(), "straight".to_string()),
],
block_states: {
let mut bs = vec![0u16; 4096];
bs[0] = 1;
bs[1] = 2;
bs
},
biomes: None,
};
let chunk = ChunkData {
x: 0,
z: 0,
data_version: 3700,
status: "minecraft:full".to_string(),
sections: vec![section],
block_entities: Vec::new(),
entities: Vec::new(),
y_pos: -4,
};
let mut chunks: Vec<Option<ChunkData>> = (0..1024).map(|_| None).collect();
chunks[0] = Some(chunk);
let mca = McaFile {
chunks,
region_x: 0,
region_z: 0,
};
let bytes = mca.to_bytes().unwrap();
let mca2 = McaFile::from_bytes(&bytes, 0, 0).unwrap();
let chunk2 = mca2.chunks[0].as_ref().unwrap();
let redstone = &chunk2.sections[0].palette[1];
assert_eq!(redstone.name, "minecraft:redstone_wire");
assert_eq!(redstone.get_property("power"), Some(&SmolStr::from("15")));
assert_eq!(redstone.get_property("east"), Some(&SmolStr::from("side")));
let stairs = &chunk2.sections[0].palette[2];
assert_eq!(stairs.name, "minecraft:oak_stairs");
assert_eq!(stairs.get_property("facing"), Some(&SmolStr::from("north")));
assert_eq!(stairs.get_property("half"), Some(&SmolStr::from("top")));
}
fn make_chunk(x: i32, z: i32) -> ChunkData {
ChunkData {
x,
z,
data_version: 3700,
status: "minecraft:full".to_string(),
sections: vec![ChunkSection {
y: 0,
palette: vec![
BlockState::new("minecraft:air".to_string()),
BlockState::new("minecraft:stone".to_string()),
],
block_states: {
let mut bs = vec![0u16; 4096];
bs[0] = 1;
bs
},
biomes: None,
}],
block_entities: Vec::new(),
entities: Vec::new(),
y_pos: -4,
}
}
fn make_section(y: i8, block_name: &str) -> ChunkSection {
ChunkSection {
y,
palette: vec![
BlockState::new("minecraft:air".to_string()),
BlockState::new(block_name.to_string()),
],
block_states: {
let mut bs = vec![0u16; 4096];
bs[0] = 1;
bs
},
biomes: None,
}
}
fn make_single_chunk_mca(index: usize, region_x: i32, region_z: i32) -> McaFile {
let chunk_x = region_x * 32 + (index % 32) as i32;
let chunk_z = region_z * 32 + (index / 32) as i32;
let mut chunks: Vec<Option<ChunkData>> = (0..1024).map(|_| None).collect();
chunks[index] = Some(make_chunk(chunk_x, chunk_z));
McaFile {
chunks,
region_x,
region_z,
}
}
}