use crate::block_entity::BlockEntity;
use crate::entity::Entity;
use crate::formats::error::Result;
use crate::region::Region;
use crate::{BlockState, UniversalSchematic};
use flate2::read::GzDecoder;
use quartz_nbt::io::Flavor;
use quartz_nbt::{NbtCompound, NbtList, NbtTag};
use std::time::{SystemTime, UNIX_EPOCH};
pub fn is_litematic(data: &[u8]) -> bool {
let reader = std::io::BufReader::with_capacity(1 << 20, data);
let mut gz = GzDecoder::new(reader);
let (root, _) = match quartz_nbt::io::read_nbt(&mut gz, Flavor::Uncompressed) {
Ok(result) => result,
Err(_) => return false,
};
root.get::<_, i32>("Version").is_ok()
&& root.get::<_, &NbtCompound>("Metadata").is_ok()
&& root.get::<_, &NbtCompound>("Regions").is_ok()
}
const DEFAULT_COMPRESSION: flate2::Compression = flate2::Compression::new(3);
pub fn to_litematic(schematic: &UniversalSchematic) -> Result<Vec<u8>> {
to_litematic_with_compression(schematic, DEFAULT_COMPRESSION)
}
const DATA_VERSION_1_13_2: i32 = 1631; const DATA_VERSION_1_18: i32 = 2860; const DATA_VERSION_1_19_2: i32 = 3120; const DATA_VERSION_1_20_5: i32 = 3837; const DEFAULT_TARGET_DATA_VERSION: i32 = 4790;
pub fn schematic_version_for_data_version(data_version: i32) -> (i32, Option<i32>) {
if data_version < DATA_VERSION_1_13_2 {
(4, None)
} else if data_version < DATA_VERSION_1_18 {
(5, None)
} else if data_version < DATA_VERSION_1_20_5 {
(
6,
if data_version >= DATA_VERSION_1_19_2 {
Some(1)
} else {
None
},
)
} else {
(7, Some(1))
}
}
pub fn to_litematic_for_data_version(
schematic: &UniversalSchematic,
target_data_version: i32,
) -> Result<(Vec<u8>, crate::dataconverter::LossReport)> {
let mut converted = schematic.clone();
let report = converted.convert_to_data_version(target_data_version);
let bytes = to_litematic(&converted)?;
Ok((bytes, report))
}
pub fn to_litematic_with_compression(
schematic: &UniversalSchematic,
compression: flate2::Compression,
) -> Result<Vec<u8>> {
let mut root = NbtCompound::new();
let data_version = schematic
.metadata
.mc_version
.unwrap_or(DEFAULT_TARGET_DATA_VERSION);
let (version, sub_version) = schematic_version_for_data_version(data_version);
root.insert("Version", NbtTag::Int(version));
if let Some(sub) = sub_version {
root.insert("SubVersion", NbtTag::Int(sub));
}
root.insert("MinecraftDataVersion", NbtTag::Int(data_version));
let metadata = create_metadata(schematic, version);
root.insert("Metadata", NbtTag::Compound(metadata));
let regions = create_regions(schematic, version);
root.insert("Regions", NbtTag::Compound(regions));
let mut encoder = flate2::write::GzEncoder::new(Vec::new(), compression);
quartz_nbt::io::write_nbt(
&mut encoder,
None,
&root,
quartz_nbt::io::Flavor::Uncompressed,
)?;
Ok(encoder.finish()?)
}
pub fn from_litematic(data: &[u8]) -> Result<UniversalSchematic> {
let reader = std::io::BufReader::with_capacity(1 << 20, data);
let mut gz = flate2::read::GzDecoder::new(reader);
let (root, _) = quartz_nbt::io::read_nbt(&mut gz, quartz_nbt::io::Flavor::Uncompressed)?;
let mut schematic = UniversalSchematic::new("Unnamed".to_string());
parse_metadata(&root, &mut schematic)?;
parse_regions(&root, &mut schematic)?;
Ok(schematic)
}
fn create_metadata(schematic: &UniversalSchematic, version: i32) -> NbtCompound {
let mut metadata = NbtCompound::new();
metadata.insert(
"Name",
NbtTag::String(schematic.metadata.name.clone().unwrap_or_default()),
);
metadata.insert(
"Description",
NbtTag::String(schematic.metadata.description.clone().unwrap_or_default()),
);
metadata.insert(
"Author",
NbtTag::String(schematic.metadata.author.clone().unwrap_or_default()),
);
let now = if let Some(time) = schematic.metadata.created {
time as i64
} else {
#[cfg(target_arch = "wasm32")]
let current_time = 0i64;
#[cfg(not(target_arch = "wasm32"))]
let current_time = {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64
};
current_time
};
let modified = schematic.metadata.modified.unwrap_or(now as u64) as i64;
metadata.insert("TimeCreated", NbtTag::Long(now));
metadata.insert("TimeModified", NbtTag::Long(modified));
let merged_region = schematic.get_merged_region();
let (width, height, length) = if let Some(content_bounds) = merged_region.get_content_bounds() {
content_bounds.get_dimensions()
} else {
merged_region.get_dimensions()
};
let mut enclosing_size = NbtCompound::new();
enclosing_size.insert("x", NbtTag::Int(width));
enclosing_size.insert("y", NbtTag::Int(height));
enclosing_size.insert("z", NbtTag::Int(length));
metadata.insert("EnclosingSize", NbtTag::Compound(enclosing_size));
if version <= 4 {
metadata.insert("TotalVolume", NbtTag::Long(schematic.total_volume() as i64));
metadata.insert("TotalBlocks", NbtTag::Long(schematic.total_blocks() as i64));
} else {
metadata.insert("TotalVolume", NbtTag::Int(schematic.total_volume()));
metadata.insert("TotalBlocks", NbtTag::Int(schematic.total_blocks()));
}
metadata.insert(
"RegionCount",
NbtTag::Int(schematic.other_regions.len() as i32 + 1),
);
metadata.insert("Software", NbtTag::String("UniversalSchematic".to_string()));
if !schematic.definition_regions.is_empty() {
if let Ok(json) = serde_json::to_string(&schematic.definition_regions) {
metadata.insert("NucleationDefinitions", NbtTag::String(json));
}
}
metadata
}
fn create_regions(schematic: &UniversalSchematic, version: i32) -> NbtCompound {
let mut regions = NbtCompound::new();
for (name, region) in &schematic.get_all_regions() {
let compact_region = region.to_compact();
let mut region_nbt = NbtCompound::new();
let mut position = NbtCompound::new();
position.insert("x", NbtTag::Int(compact_region.position.0));
position.insert("y", NbtTag::Int(compact_region.position.1));
position.insert("z", NbtTag::Int(compact_region.position.2));
region_nbt.insert("Position", NbtTag::Compound(position));
let mut size = NbtCompound::new();
size.insert("x", NbtTag::Int(compact_region.size.0));
size.insert("y", NbtTag::Int(compact_region.size.1));
size.insert("z", NbtTag::Int(compact_region.size.2));
region_nbt.insert("Size", NbtTag::Compound(size));
let mut reordered_palette: Vec<&BlockState> =
Vec::with_capacity(compact_region.palette.len() + 1);
let mut index_mapping = vec![0usize; compact_region.palette.len()];
let air_state = BlockState::new("minecraft:air".to_string());
let has_air = compact_region
.palette
.iter()
.any(|b| b.name == "minecraft:air");
if has_air {
let air_ref = compact_region
.palette
.iter()
.find(|b| b.name == "minecraft:air")
.unwrap();
reordered_palette.push(air_ref);
} else {
reordered_palette.push(&air_state);
}
for (orig_idx, block) in compact_region.palette.iter().enumerate() {
if block.name == "minecraft:air" {
index_mapping[orig_idx] = 0;
} else {
index_mapping[orig_idx] = reordered_palette.len();
reordered_palette.push(block);
}
}
let palette = NbtList::from(
reordered_palette
.iter()
.map(|block_state| block_state.to_nbt())
.collect::<Vec<NbtTag>>(),
);
region_nbt.insert("BlockStatePalette", NbtTag::List(palette));
let bits_per_block = if reordered_palette.len() <= 1 {
2 } else {
std::cmp::max(
(usize::BITS - (reordered_palette.len() - 1).leading_zeros()) as usize,
2,
)
};
let block_count = compact_region.blocks.len();
let expected_len = (block_count * bits_per_block).div_ceil(64);
let mut packed_states = vec![0i64; expected_len];
let mask = (1u64 << bits_per_block) - 1;
if 64 % bits_per_block == 0 {
let entries_per_long = 64 / bits_per_block;
for (chunk_idx, chunk) in compact_region.blocks.chunks(entries_per_long).enumerate() {
let mut packed: u64 = 0;
for (i, &block_state) in chunk.iter().enumerate() {
packed |= (index_mapping[block_state] as u64 & mask) << (i * bits_per_block);
}
packed_states[chunk_idx] = packed as i64;
}
} else {
for (index, &block_state) in compact_region.blocks.iter().enumerate() {
let mapped_state = index_mapping[block_state];
let bit_index = index * bits_per_block;
let start_long_index = bit_index / 64;
let end_long_index = (bit_index + bits_per_block - 1) / 64;
let start_offset = bit_index % 64;
let value = (mapped_state as u64) & mask;
packed_states[start_long_index] |= (value << start_offset) as i64;
if start_long_index != end_long_index {
packed_states[end_long_index] |= (value >> (64 - start_offset)) as i64;
}
}
}
region_nbt.insert("BlockStates", NbtTag::LongArray(packed_states));
let entities = NbtList::from(
compact_region
.entities
.iter()
.map(|entity| {
let mut entity_nbt = if let NbtTag::Compound(c) = entity.to_nbt() {
c
} else {
NbtCompound::new()
};
let rel_x = entity.position.0 - compact_region.position.0 as f64;
let rel_y = entity.position.1 - compact_region.position.1 as f64;
let rel_z = entity.position.2 - compact_region.position.2 as f64;
let pos_list = NbtList::from(vec![
NbtTag::Double(rel_x),
NbtTag::Double(rel_y),
NbtTag::Double(rel_z),
]);
entity_nbt.insert("Pos", NbtTag::List(pos_list));
NbtTag::Compound(entity_nbt)
})
.collect::<Vec<NbtTag>>(),
);
region_nbt.insert("Entities", NbtTag::List(entities));
let tile_entities = NbtList::from(
compact_region
.block_entities
.values()
.map(|block_entity| {
let mut block_entity_nbt = block_entity.to_nbt();
let rel_x = block_entity.position.0 - compact_region.position.0;
let rel_y = block_entity.position.1 - compact_region.position.1;
let rel_z = block_entity.position.2 - compact_region.position.2;
block_entity_nbt.insert("x", NbtTag::Int(rel_x));
block_entity_nbt.insert("y", NbtTag::Int(rel_y));
block_entity_nbt.insert("z", NbtTag::Int(rel_z));
block_entity_nbt.insert("Pos", NbtTag::IntArray(vec![rel_x, rel_y, rel_z]));
NbtTag::Compound(block_entity_nbt)
})
.collect::<Vec<NbtTag>>(),
);
region_nbt.insert("TileEntities", NbtTag::List(tile_entities));
region_nbt.insert("PendingBlockTicks", NbtTag::List(NbtList::new()));
if version >= 5 {
region_nbt.insert("PendingFluidTicks", NbtTag::List(NbtList::new()));
}
regions.insert(name, NbtTag::Compound(region_nbt));
}
regions
}
fn parse_metadata(root: &NbtCompound, schematic: &mut UniversalSchematic) -> Result<()> {
if let Ok(dv) = root.get::<_, i32>("MinecraftDataVersion") {
schematic.metadata.mc_version = Some(dv);
schematic.metadata.source_data_version = Some(dv);
}
let metadata = root.get::<_, &NbtCompound>("Metadata")?;
schematic.metadata.name = metadata.get::<_, &str>("Name").ok().map(String::from);
schematic.metadata.description = metadata
.get::<_, &str>("Description")
.ok()
.map(String::from);
schematic.metadata.author = metadata.get::<_, &str>("Author").ok().map(String::from);
schematic.metadata.created = metadata.get::<_, i64>("TimeCreated").ok().map(|t| t as u64);
schematic.metadata.modified = metadata
.get::<_, i64>("TimeModified")
.ok()
.map(|t| t as u64);
if let Ok(json) = metadata.get::<_, &str>("NucleationDefinitions") {
if let Ok(regions) = serde_json::from_str(json) {
schematic.definition_regions = regions;
}
}
Ok(())
}
fn parse_regions(root: &NbtCompound, schematic: &mut UniversalSchematic) -> Result<()> {
let regions = root.get::<_, &NbtCompound>("Regions")?;
let mut loop_count = 0;
for (name, region_tag) in regions.inner() {
if loop_count == 0 {
schematic.default_region_name = name.clone();
}
loop_count += 1;
if let NbtTag::Compound(region_nbt) = region_tag {
let position = region_nbt.get::<_, &NbtCompound>("Position")?;
let size = region_nbt.get::<_, &NbtCompound>("Size")?;
let position = (
position.get::<_, i32>("x")?,
position.get::<_, i32>("y")?,
position.get::<_, i32>("z")?,
);
let size = (
size.get::<_, i32>("x")?,
size.get::<_, i32>("y")?,
size.get::<_, i32>("z")?,
);
let mut region = Region::new(name.to_string(), position, size);
let palette = region_nbt.get::<_, &NbtList>("BlockStatePalette")?;
region.palette = palette
.iter()
.filter_map(|tag| {
if let NbtTag::Compound(compound) = tag {
BlockState::from_nbt(compound).ok()
} else {
None
}
})
.collect();
let block_states = region_nbt.get::<_, &[i64]>("BlockStates")?;
region.blocks = region.unpack_block_states(block_states);
region.rebuild_palette_index();
region.rebuild_air_index();
region.rebuild_non_air_count();
region.rebuild_tight_bounds();
let min_corner = region.get_bounding_box().min;
if let Ok(entities_list) = region_nbt.get::<_, &NbtList>("Entities") {
region.entities = entities_list
.iter()
.filter_map(|tag| {
if let NbtTag::Compound(compound) = tag {
let mut entity = Entity::from_nbt(compound).ok()?;
entity.position.0 += position.0 as f64;
entity.position.1 += position.1 as f64;
entity.position.2 += position.2 as f64;
Some(entity)
} else {
None
}
})
.collect();
}
if let Ok(tile_entities_list) = region_nbt.get::<_, &NbtList>("TileEntities") {
for tag in tile_entities_list.iter() {
if let NbtTag::Compound(compound) = tag {
let mut block_entity = BlockEntity::from_nbt(compound);
block_entity.position.0 += min_corner.0;
block_entity.position.1 += min_corner.1;
block_entity.position.2 += min_corner.2;
region
.block_entities
.insert(block_entity.position, block_entity);
}
}
}
schematic.add_region(region);
}
}
Ok(())
}
use crate::formats::manager::{SchematicExporter, SchematicImporter};
pub struct LitematicFormat;
impl SchematicImporter for LitematicFormat {
fn name(&self) -> String {
"litematic".to_string()
}
fn detect(&self, data: &[u8]) -> bool {
is_litematic(data)
}
fn read(&self, data: &[u8]) -> Result<UniversalSchematic> {
from_litematic(data)
}
}
impl SchematicExporter for LitematicFormat {
fn name(&self) -> String {
"litematic".to_string()
}
fn extensions(&self) -> Vec<String> {
vec!["litematic".to_string()]
}
fn available_versions(&self) -> Vec<String> {
vec!["default".to_string()]
}
fn default_version(&self) -> String {
"default".to_string()
}
fn write(&self, schematic: &UniversalSchematic, _version: Option<&str>) -> Result<Vec<u8>> {
to_litematic(schematic)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{BlockState, UniversalSchematic};
use std::fs::File;
use std::io::Write;
fn gzip_litematic(root: &NbtCompound) -> Vec<u8> {
use flate2::{write::GzEncoder, Compression};
let mut uncompressed = Vec::new();
quartz_nbt::io::write_nbt(&mut uncompressed, None, root, Flavor::Uncompressed).unwrap();
let mut enc = GzEncoder::new(Vec::new(), Compression::default());
std::io::Write::write_all(&mut enc, &uncompressed).unwrap();
enc.finish().unwrap()
}
fn read_root(bytes: &[u8]) -> NbtCompound {
let mut gz = GzDecoder::new(bytes);
let (root, _) = quartz_nbt::io::read_nbt(&mut gz, Flavor::Uncompressed).unwrap();
root
}
#[test]
fn schematic_version_maps_data_versions_correctly() {
assert_eq!(schematic_version_for_data_version(1343), (4, None)); assert_eq!(schematic_version_for_data_version(1631), (5, None)); assert_eq!(schematic_version_for_data_version(2586), (5, None)); assert_eq!(schematic_version_for_data_version(2860), (6, None)); assert_eq!(schematic_version_for_data_version(3120), (6, Some(1))); assert_eq!(schematic_version_for_data_version(3700), (6, Some(1))); assert_eq!(schematic_version_for_data_version(3837), (7, Some(1))); assert_eq!(schematic_version_for_data_version(4189), (7, Some(1))); }
#[test]
fn writes_version_header_matching_target_data_version() {
let cases = [
(1343, 4, false, false), (1631, 5, false, true),
(2860, 6, false, true),
(3120, 6, true, true),
(3837, 7, true, true),
(4189, 7, true, true),
];
for (dv, want_ver, want_sub, want_fluid) in cases {
let mut schem = UniversalSchematic::new("v".to_string());
schem.metadata.mc_version = Some(dv);
schem.set_block(0, 0, 0, &BlockState::new("minecraft:stone".to_string()));
let bytes = to_litematic(&schem).unwrap();
let root = read_root(&bytes);
assert_eq!(
root.get::<_, i32>("Version").unwrap(),
want_ver,
"Version for dv={}",
dv
);
assert_eq!(root.get::<_, i32>("MinecraftDataVersion").unwrap(), dv);
assert_eq!(
root.get::<_, i32>("SubVersion").is_ok(),
want_sub,
"SubVersion presence for dv={}",
dv
);
let regions = root.get::<_, &NbtCompound>("Regions").unwrap();
let (_, rtag) = regions.inner().iter().next().unwrap();
if let NbtTag::Compound(region) = rtag {
assert_eq!(
region.get::<_, &NbtList>("PendingFluidTicks").is_ok(),
want_fluid,
"PendingFluidTicks presence for dv={}",
dv
);
}
let reparsed = from_litematic(&bytes).unwrap();
assert_eq!(
reparsed.get_block(0, 0, 0).map(|b| b.name.as_str()),
Some("minecraft:stone")
);
assert_eq!(reparsed.metadata.source_data_version, Some(dv));
}
}
#[test]
fn to_litematic_for_data_version_converts_and_writes_matching_header() {
let mut schem = UniversalSchematic::new("conv".to_string());
schem.metadata.mc_version = Some(4189); schem.metadata.source_data_version = Some(4189);
schem.set_block(0, 0, 0, &BlockState::new("minecraft:stone".to_string()));
let (bytes, _loss) = to_litematic_for_data_version(&schem, 1343).unwrap();
let root = read_root(&bytes);
assert_eq!(root.get::<_, i32>("Version").unwrap(), 4);
assert_eq!(root.get::<_, i32>("MinecraftDataVersion").unwrap(), 1343);
assert_eq!(
schem.metadata.mc_version,
Some(4189),
"input schematic must be unchanged"
);
let reparsed = from_litematic(&bytes).unwrap();
assert_eq!(
reparsed.get_block(0, 0, 0).map(|b| b.name.as_str()),
Some("minecraft:stone")
);
}
#[test]
fn v4_writes_total_volume_as_long() {
let mut schem = UniversalSchematic::new("v4".to_string());
schem.metadata.mc_version = Some(1343);
schem.set_block(0, 0, 0, &BlockState::new("minecraft:stone".to_string()));
let meta = read_root(&to_litematic(&schem).unwrap());
let m = meta.get::<_, &NbtCompound>("Metadata").unwrap();
assert!(matches!(
m.get::<_, &NbtTag>("TotalVolume").unwrap(),
NbtTag::Long(_)
));
let mut schem7 = UniversalSchematic::new("v7".to_string());
schem7.metadata.mc_version = Some(4189);
schem7.set_block(0, 0, 0, &BlockState::new("minecraft:stone".to_string()));
let m7root = read_root(&to_litematic(&schem7).unwrap());
let m7 = m7root.get::<_, &NbtCompound>("Metadata").unwrap();
assert!(matches!(
m7.get::<_, &NbtTag>("TotalVolume").unwrap(),
NbtTag::Int(_)
));
}
#[test]
fn negative_size_region_places_block_entity_on_its_block() {
let mut palette = NbtList::new();
let mut air = NbtCompound::new();
air.insert("Name", NbtTag::String("minecraft:air".to_string()));
palette.push(NbtTag::Compound(air));
let mut chest = NbtCompound::new();
chest.insert("Name", NbtTag::String("minecraft:chest".to_string()));
palette.push(NbtTag::Compound(chest));
let block_states = NbtTag::LongArray(vec![1]);
let mut item = NbtCompound::new();
item.insert("id", NbtTag::String("minecraft:stone".to_string()));
item.insert("count", NbtTag::Int(1));
item.insert("Slot", NbtTag::Byte(0));
let mut items = NbtList::new();
items.push(NbtTag::Compound(item));
let mut te = NbtCompound::new();
te.insert("id", NbtTag::String("minecraft:chest".to_string()));
te.insert("x", NbtTag::Int(0));
te.insert("y", NbtTag::Int(0));
te.insert("z", NbtTag::Int(0));
te.insert("Items", NbtTag::List(items));
let mut tile_entities = NbtList::new();
tile_entities.push(NbtTag::Compound(te));
let mut pos = NbtCompound::new();
pos.insert("x", NbtTag::Int(0));
pos.insert("y", NbtTag::Int(5));
pos.insert("z", NbtTag::Int(0));
let mut size = NbtCompound::new();
size.insert("x", NbtTag::Int(1));
size.insert("y", NbtTag::Int(-2));
size.insert("z", NbtTag::Int(1));
let mut region = NbtCompound::new();
region.insert("Position", NbtTag::Compound(pos));
region.insert("Size", NbtTag::Compound(size));
region.insert("BlockStatePalette", NbtTag::List(palette));
region.insert("BlockStates", block_states);
region.insert("TileEntities", NbtTag::List(tile_entities));
let mut regions = NbtCompound::new();
regions.insert("r", NbtTag::Compound(region));
let mut root = NbtCompound::new();
root.insert("MinecraftDataVersion", NbtTag::Int(4189));
root.insert("Version", NbtTag::Int(7));
root.insert("Metadata", NbtTag::Compound(NbtCompound::new()));
root.insert("Regions", NbtTag::Compound(regions));
let schem = from_litematic(&gzip_litematic(&root)).expect("parse");
assert_eq!(
schem.get_block(0, 4, 0).map(|b| b.name.as_str()),
Some("minecraft:chest")
);
assert_eq!(
schem.get_block(0, 5, 0).map(|b| b.name.as_str()),
Some("minecraft:air")
);
let bes = schem.get_block_entities_as_list();
assert_eq!(bes.len(), 1);
assert_eq!(bes[0].id, "minecraft:chest");
assert_eq!(
bes[0].position,
(0, 4, 0),
"block entity must sit on the min-corner-relative cell"
);
let items_len = bes[0]
.nbt
.get("Items")
.and_then(|v| {
if let crate::nbt::NbtValue::List(l) = v {
Some(l.len())
} else {
None
}
})
.unwrap_or(0);
assert_eq!(items_len, 1);
}
#[test]
fn negative_size_region_places_entity_from_region_position() {
let mut palette = NbtList::new();
let mut air = NbtCompound::new();
air.insert("Name", NbtTag::String("minecraft:air".to_string()));
palette.push(NbtTag::Compound(air));
let mut grass = NbtCompound::new();
grass.insert("Name", NbtTag::String("minecraft:grass_block".to_string()));
palette.push(NbtTag::Compound(grass));
let mut block_states = 0i64;
for index in 0..12 {
block_states |= 1i64 << (index * 2);
}
let mut pos = NbtCompound::new();
pos.insert("x", NbtTag::Int(2));
pos.insert("y", NbtTag::Int(0));
pos.insert("z", NbtTag::Int(3));
let mut size = NbtCompound::new();
size.insert("x", NbtTag::Int(-3));
size.insert("y", NbtTag::Int(2));
size.insert("z", NbtTag::Int(-4));
let mut entity_pos = NbtList::new();
entity_pos.push(NbtTag::Double(-0.5));
entity_pos.push(NbtTag::Double(1.0));
entity_pos.push(NbtTag::Double(-1.3125));
let mut entity = NbtCompound::new();
entity.insert("id", NbtTag::String("minecraft:oak_boat".to_string()));
entity.insert("Pos", NbtTag::List(entity_pos));
let mut entities = NbtList::new();
entities.push(NbtTag::Compound(entity));
let mut region = NbtCompound::new();
region.insert("Position", NbtTag::Compound(pos));
region.insert("Size", NbtTag::Compound(size));
region.insert("BlockStatePalette", NbtTag::List(palette));
region.insert("BlockStates", NbtTag::LongArray(vec![block_states]));
region.insert("Entities", NbtTag::List(entities));
let mut regions = NbtCompound::new();
regions.insert("r", NbtTag::Compound(region));
let mut root = NbtCompound::new();
root.insert("MinecraftDataVersion", NbtTag::Int(4189));
root.insert("Version", NbtTag::Int(7));
root.insert("Metadata", NbtTag::Compound(NbtCompound::new()));
root.insert("Regions", NbtTag::Compound(regions));
let schem = from_litematic(&gzip_litematic(&root)).expect("parse");
let entity = &schem.default_region.entities[0];
assert!(
(entity.position.0 - 1.5).abs() < 0.001,
"x={}",
entity.position.0
);
assert!(
(entity.position.1 - 1.0).abs() < 0.001,
"y={}",
entity.position.1
);
assert!(
(entity.position.2 - 1.6875).abs() < 0.001,
"z={}",
entity.position.2
);
}
#[test]
fn test_create_metadata() {
let mut schematic = UniversalSchematic::new("Test Schematic".to_string());
schematic.metadata.author = Some("Test Author".to_string());
schematic.metadata.description = Some("Test Description".to_string());
schematic.metadata.created = Some(1000);
schematic.metadata.modified = Some(2000);
let metadata = create_metadata(&schematic, 7);
assert_eq!(metadata.get::<_, &str>("Name").unwrap(), "Test Schematic");
assert_eq!(metadata.get::<_, &str>("Author").unwrap(), "Test Author");
assert_eq!(
metadata.get::<_, &str>("Description").unwrap(),
"Test Description"
);
assert_eq!(metadata.get::<_, i64>("TimeCreated").unwrap(), 1000);
assert_eq!(metadata.get::<_, i64>("TimeModified").unwrap(), 2000);
assert!(metadata.contains_key("EnclosingSize"));
assert!(metadata.contains_key("TotalVolume"));
assert!(metadata.contains_key("TotalBlocks"));
assert!(metadata.contains_key("RegionCount"));
assert_eq!(
metadata.get::<_, &str>("Software").unwrap(),
"UniversalSchematic"
);
}
#[test]
fn test_create_regions() {
let mut schematic = UniversalSchematic::new("Test Schematic".to_string());
let mut region = Region::new("TestRegion".to_string(), (0, 0, 0), (2, 2, 2));
let stone = BlockState::new("minecraft:stone".to_string());
let _air = BlockState::new("minecraft:air".to_string());
region.set_block(0, 0, 0, &stone);
region.set_block(1, 1, 1, &stone);
let entity = Entity::new("minecraft:creeper".to_string(), (0.5, 0.0, 0.5));
region.add_entity(entity);
let block_entity = BlockEntity::new("minecraft:chest".to_string(), (0, 1, 0));
region.add_block_entity(block_entity);
schematic.add_region(region);
let regions = create_regions(&schematic, 7);
assert!(regions.contains_key("TestRegion"));
let region_nbt = regions.get::<_, &NbtCompound>("TestRegion").unwrap();
assert!(region_nbt.contains_key("Position"));
assert!(region_nbt.contains_key("Size"));
assert!(region_nbt.contains_key("BlockStatePalette"));
assert!(region_nbt.contains_key("BlockStates"));
assert!(region_nbt.contains_key("Entities"));
assert!(region_nbt.contains_key("TileEntities"));
assert!(region_nbt.contains_key("PendingBlockTicks"));
assert!(region_nbt.contains_key("PendingFluidTicks"));
}
#[test]
fn test_parse_metadata() {
let mut root = NbtCompound::new();
let mut metadata = NbtCompound::new();
metadata.insert("Name", NbtTag::String("Test Schematic".to_string()));
metadata.insert("Author", NbtTag::String("Test Author".to_string()));
metadata.insert(
"Description",
NbtTag::String("Test Description".to_string()),
);
metadata.insert("TimeCreated", NbtTag::Long(1000));
metadata.insert("TimeModified", NbtTag::Long(2000));
root.insert("Metadata", NbtTag::Compound(metadata));
let mut schematic = UniversalSchematic::new("".to_string());
parse_metadata(&root, &mut schematic).unwrap();
assert_eq!(schematic.metadata.name, Some("Test Schematic".to_string()));
assert_eq!(schematic.metadata.author, Some("Test Author".to_string()));
assert_eq!(
schematic.metadata.description,
Some("Test Description".to_string())
);
assert_eq!(schematic.metadata.created, Some(1000));
assert_eq!(schematic.metadata.modified, Some(2000));
}
#[test]
fn test_parse_regions() {
let mut root = NbtCompound::new();
let mut regions = NbtCompound::new();
let mut region = NbtCompound::new();
let mut position = NbtCompound::new();
position.insert("x", NbtTag::Int(0));
position.insert("y", NbtTag::Int(0));
position.insert("z", NbtTag::Int(0));
region.insert("Position", NbtTag::Compound(position));
let mut size = NbtCompound::new();
size.insert("x", NbtTag::Int(2));
size.insert("y", NbtTag::Int(2));
size.insert("z", NbtTag::Int(2));
region.insert("Size", NbtTag::Compound(size));
let palette = NbtList::from(vec![
BlockState::new("minecraft:air".to_string()).to_nbt(),
BlockState::new("minecraft:stone".to_string()).to_nbt(),
]);
region.insert("BlockStatePalette", NbtTag::List(palette));
region.insert("BlockStates", NbtTag::LongArray(vec![0b10000001]));
regions.insert("TestRegion", NbtTag::Compound(region));
root.insert("Regions", NbtTag::Compound(regions));
println!("{:?}", root);
let mut schematic = UniversalSchematic::new("Test Schematic".to_string());
parse_regions(&root, &mut schematic).unwrap();
assert_eq!(schematic.default_region_name, "TestRegion");
let parsed_region = schematic.default_region;
assert_eq!(parsed_region.position, (0, 0, 0));
assert_eq!(parsed_region.size, (2, 2, 2));
assert_eq!(parsed_region.palette.len(), 2);
assert_eq!(parsed_region.count_blocks(), 2); }
#[test]
fn test_simple_litematic() {
let mut schematic = UniversalSchematic::new("Simple Cube".to_string());
schematic.metadata.created = Some(1000);
schematic.metadata.modified = Some(2000);
for x in 0..3 {
for y in 0..3 {
for z in 0..3 {
let block = match (x + y + z) % 3 {
0 => BlockState::new("minecraft:stone".to_string()),
1 => BlockState::new("minecraft:dirt".to_string()),
_ => BlockState::new("minecraft:oak_planks".to_string()),
};
schematic.set_block(x, y, z, &block);
}
}
}
schematic.metadata.author = Some("Test Author".to_string());
schematic.metadata.description = Some("A simple 3x3x3 cube for testing".to_string());
let litematic_data =
to_litematic(&schematic).expect("Failed to convert schematic to litematic");
let mut file = File::create("simple_cube.litematic").expect("Failed to create file");
file.write_all(&litematic_data)
.expect("Failed to write to file");
let _loaded_litematic_data =
std::fs::read("simple_cube.litematic").expect("Failed to read file");
}
#[test]
fn test_litematic_roundtrip() {
let mut original_schematic = UniversalSchematic::new("Test Schematic".to_string());
original_schematic.metadata.created = Some(1000);
original_schematic.metadata.modified = Some(2000);
let mut region = Region::new("TestRegion".to_string(), (0, 0, 0), (2, 2, 2));
let stone = BlockState::new("minecraft:stone".to_string());
let _air = BlockState::new("minecraft:air".to_string());
region.set_block(0, 0, 0, &stone);
region.set_block(1, 1, 1, &stone);
original_schematic.add_region(region);
let litematic_data = to_litematic(&original_schematic).unwrap();
let roundtrip_schematic = from_litematic(&litematic_data).unwrap();
assert_eq!(
original_schematic.metadata.name,
roundtrip_schematic.metadata.name
);
assert_eq!(
original_schematic.other_regions.len(),
roundtrip_schematic.other_regions.len()
);
let original_region = original_schematic.get_region("TestRegion").unwrap();
let roundtrip_region = roundtrip_schematic.get_region("TestRegion").unwrap();
assert_eq!(original_region.position, roundtrip_region.position);
assert_eq!(original_region.size, roundtrip_region.size);
assert_eq!(
original_region.count_blocks(),
roundtrip_region.count_blocks()
);
for x in 0..2 {
for y in 0..2 {
for z in 0..2 {
assert_eq!(
original_region.get_block(x, y, z),
roundtrip_region.get_block(x, y, z)
);
}
}
}
}
#[test]
fn test_litematic_relative_positions_in_nbt() {
let mut schematic = UniversalSchematic::new("RelativePositionTest".to_string());
schematic.metadata.created = Some(1000);
schematic.metadata.modified = Some(2000);
let stone = BlockState::new("minecraft:stone".to_string());
schematic.set_block(10, 20, 30, &stone);
schematic.set_block(11, 21, 31, &stone);
let block_entity = BlockEntity::new("minecraft:chest".to_string(), (10, 20, 30));
schematic.default_region.add_block_entity(block_entity);
let entity = Entity::new("minecraft:creeper".to_string(), (10.5, 20.0, 30.5));
schematic.default_region.add_entity(entity);
let litematic_data = to_litematic(&schematic).unwrap();
let mut decoder = flate2::read::GzDecoder::new(litematic_data.as_slice());
let mut decompressed = Vec::new();
std::io::Read::read_to_end(&mut decoder, &mut decompressed).unwrap();
let (root, _) = quartz_nbt::io::read_nbt(
&mut std::io::Cursor::new(decompressed),
quartz_nbt::io::Flavor::Uncompressed,
)
.unwrap();
let regions = root.get::<_, &NbtCompound>("Regions").unwrap();
let (_, region_tag) = regions.inner().iter().next().unwrap();
let region_nbt = if let NbtTag::Compound(c) = region_tag {
c
} else {
panic!("Expected compound")
};
let pos_nbt = region_nbt.get::<_, &NbtCompound>("Position").unwrap();
let region_x = pos_nbt.get::<_, i32>("x").unwrap();
let region_y = pos_nbt.get::<_, i32>("y").unwrap();
let region_z = pos_nbt.get::<_, i32>("z").unwrap();
assert_eq!(region_x, 10);
assert_eq!(region_y, 20);
assert_eq!(region_z, 30);
let tile_entities = region_nbt.get::<_, &NbtList>("TileEntities").unwrap();
assert_eq!(tile_entities.len(), 1);
if let NbtTag::Compound(be_nbt) = &tile_entities[0] {
let pos = be_nbt.get::<_, &[i32]>("Pos").unwrap();
assert_eq!(
pos,
&[0, 0, 0],
"Block entity position should be relative to region, got {:?}",
pos
);
assert_eq!(be_nbt.get::<_, i32>("x").unwrap(), 0);
assert_eq!(be_nbt.get::<_, i32>("y").unwrap(), 0);
assert_eq!(be_nbt.get::<_, i32>("z").unwrap(), 0);
} else {
panic!("Expected compound tag for block entity");
}
let entities = region_nbt.get::<_, &NbtList>("Entities").unwrap();
assert_eq!(entities.len(), 1);
if let NbtTag::Compound(ent_nbt) = &entities[0] {
let pos = ent_nbt.get::<_, &NbtList>("Pos").unwrap();
let x = pos.get::<f64>(0).unwrap();
let y = pos.get::<f64>(1).unwrap();
let z = pos.get::<f64>(2).unwrap();
assert!(
(x - 0.5).abs() < 0.001,
"Entity X should be 0.5 relative, got {}",
x
);
assert!(
(y - 0.0).abs() < 0.001,
"Entity Y should be 0.0 relative, got {}",
y
);
assert!(
(z - 0.5).abs() < 0.001,
"Entity Z should be 0.5 relative, got {}",
z
);
} else {
panic!("Expected compound tag for entity");
}
}
#[test]
fn test_litematic_roundtrip_with_offset_positions() {
let mut schematic = UniversalSchematic::new("OffsetRoundtrip".to_string());
schematic.metadata.created = Some(1000);
schematic.metadata.modified = Some(2000);
let stone = BlockState::new("minecraft:stone".to_string());
for x in 10..13 {
for y in 20..23 {
for z in 30..33 {
schematic.set_block(x, y, z, &stone);
}
}
}
let chest1 = BlockEntity::new("minecraft:chest".to_string(), (10, 20, 30));
let chest2 = BlockEntity::new("minecraft:chest".to_string(), (12, 22, 32));
schematic.default_region.add_block_entity(chest1);
schematic.default_region.add_block_entity(chest2);
let creeper = Entity::new("minecraft:creeper".to_string(), (11.5, 21.0, 31.5));
schematic.default_region.add_entity(creeper);
let litematic_data = to_litematic(&schematic).unwrap();
let roundtrip = from_litematic(&litematic_data).unwrap();
let rt_region = roundtrip
.get_region(&roundtrip.default_region_name)
.unwrap();
assert_eq!(rt_region.block_entities.len(), 2);
assert!(
rt_region.block_entities.contains_key(&(10, 20, 30)),
"Block entity at (10,20,30) should exist after roundtrip"
);
assert!(
rt_region.block_entities.contains_key(&(12, 22, 32)),
"Block entity at (12,22,32) should exist after roundtrip"
);
assert_eq!(rt_region.entities.len(), 1);
let rt_entity = &rt_region.entities[0];
assert!(
(rt_entity.position.0 - 11.5).abs() < 0.001,
"Entity X should be 11.5, got {}",
rt_entity.position.0
);
assert!(
(rt_entity.position.1 - 21.0).abs() < 0.001,
"Entity Y should be 21.0, got {}",
rt_entity.position.1
);
assert!(
(rt_entity.position.2 - 31.5).abs() < 0.001,
"Entity Z should be 31.5, got {}",
rt_entity.position.2
);
for x in 10..13 {
for y in 20..23 {
for z in 30..33 {
assert_eq!(
rt_region.get_block(x, y, z).map(|b| b.name.as_str()),
Some("minecraft:stone"),
"Block at ({},{},{}) should be stone",
x,
y,
z
);
}
}
}
}
}