#![allow(clippy::needless_range_loop)]
const FLATTENING_DATA_VERSION: i32 = 1519;
fn modernized(schematic: &crate::UniversalSchematic) -> Option<crate::UniversalSchematic> {
let from = schematic
.metadata
.source_data_version
.or(schematic.metadata.mc_version)?;
if from >= FLATTENING_DATA_VERSION {
return None;
}
let mut converted = schematic.clone();
converted.convert_to_data_version(crate::dataconverter::CANONICAL_DATA_VERSION);
Some(converted)
}
fn compound_snbt(map: &crate::utils::NbtMap) -> String {
use std::fmt::Write as _;
let mut entries: Vec<(&String, &crate::nbt::NbtValue)> = map.iter().collect();
entries.sort_by(|a, b| a.0.cmp(b.0));
let mut out = String::from("{");
for (index, (key, value)) in entries.into_iter().enumerate() {
if index > 0 {
out.push_str(", ");
}
let bare = !key.is_empty()
&& key
.chars()
.all(|c| c.is_ascii_alphanumeric() || "_-.+".contains(c));
if bare {
out.push_str(key);
} else {
let _ = write!(out, "{}", quoted_snbt(key));
}
out.push_str(": ");
out.push_str(&value_snbt(value));
}
out.push('}');
out
}
fn value_snbt(value: &crate::nbt::NbtValue) -> String {
use crate::nbt::NbtValue as V;
use std::fmt::Write as _;
match value {
V::Byte(v) => format!("{v}b"),
V::Short(v) => format!("{v}s"),
V::Int(v) => v.to_string(),
V::Long(v) => format!("{v}L"),
V::Float(v) => format!("{v}f"),
V::Double(v) => format!("{}", snbt_double(*v)),
V::String(v) => quoted_snbt(v),
V::ByteArray(values) => {
let body: Vec<String> = values.iter().map(|v| format!("{v}b")).collect();
format!("[B; {}]", body.join(", "))
}
V::IntArray(values) => {
let body: Vec<String> = values.iter().map(i32::to_string).collect();
format!("[I; {}]", body.join(", "))
}
V::LongArray(values) => {
let body: Vec<String> = values.iter().map(|v| format!("{v}L")).collect();
format!("[L; {}]", body.join(", "))
}
V::List(values) => {
let body: Vec<String> = values.iter().map(value_snbt).collect();
format!("[{}]", body.join(", "))
}
V::Compound(map) => {
let mut out = String::new();
let _ = write!(out, "{}", compound_snbt(map));
out
}
}
}
fn entity_value_snbt(value: &crate::entity::NbtValue) -> String {
use crate::entity::NbtValue as V;
use std::fmt::Write as _;
match value {
V::Byte(v) => format!("{v}b"),
V::Short(v) => format!("{v}s"),
V::Int(v) => v.to_string(),
V::Long(v) => format!("{v}L"),
V::Float(v) => format!("{v}f"),
V::Double(v) => snbt_double(*v),
V::String(v) => quoted_snbt(v),
V::Boolean(v) => format!("{}b", if *v { 1 } else { 0 }),
V::ByteArray(values) => {
let body: Vec<String> = values.iter().map(|v| format!("{v}b")).collect();
format!("[B; {}]", body.join(", "))
}
V::IntArray(values) => {
let body: Vec<String> = values.iter().map(i32::to_string).collect();
format!("[I; {}]", body.join(", "))
}
V::LongArray(values) => {
let body: Vec<String> = values.iter().map(|v| format!("{v}L")).collect();
format!("[L; {}]", body.join(", "))
}
V::List(values) => {
let body: Vec<String> = values.iter().map(entity_value_snbt).collect();
format!("[{}]", body.join(", "))
}
V::Compound(map) => {
let mut entries: Vec<_> = map.iter().collect();
entries.sort_by(|a, b| a.0.cmp(b.0));
let mut out = String::from("{");
for (index, (key, value)) in entries.into_iter().enumerate() {
if index > 0 {
out.push_str(", ");
}
let bare = !key.is_empty()
&& key
.chars()
.all(|c| c.is_ascii_alphanumeric() || "_-.+".contains(c));
if bare {
out.push_str(key);
} else {
let _ = write!(out, "{}", quoted_snbt(key));
}
let _ = write!(out, ": {}", entity_value_snbt(value));
}
out.push('}');
out
}
}
}
fn quoted_snbt(text: &str) -> String {
if text.contains('"') && !text.contains('\'') {
format!("'{text}'")
} else {
let escaped = text.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{escaped}\"")
}
}
fn snbt_double(value: f64) -> String {
if value.is_nan() {
return "NaN".to_string();
}
if value.is_infinite() {
return if value.is_sign_positive() {
"Infinity"
} else {
"-Infinity"
}
.to_string();
}
let mut text = format!("{value}");
if !text.contains('.') {
text.push_str(".0");
}
text.push('d');
text
}
fn nbt_number(value: &crate::entity::NbtValue) -> Option<f64> {
use crate::entity::NbtValue as V;
match value {
V::Double(v) => Some(*v),
V::Float(v) => Some(f64::from(*v)),
V::Int(v) => Some(f64::from(*v)),
V::Short(v) => Some(f64::from(*v)),
V::Byte(v) => Some(f64::from(*v)),
V::Long(v) => Some(*v as f64),
_ => None,
}
}
fn nbt_vec3(value: Option<&crate::entity::NbtValue>) -> [f64; 3] {
if let Some(crate::entity::NbtValue::List(items)) = value {
if items.len() == 3 {
let parsed: Vec<f64> = items.iter().filter_map(nbt_number).collect();
if parsed.len() == 3 {
return [parsed[0], parsed[1], parsed[2]];
}
}
}
[0.0; 3]
}
fn entities_snbt(schematic: &crate::UniversalSchematic, min: (i32, i32, i32)) -> String {
use crate::entity::NbtValue as V;
use std::fmt::Write as _;
let (mx, my, mz) = min;
let mut out = String::new();
for entity in schematic.get_entities_as_list() {
let id = if entity.id.contains(':') {
entity.id.clone()
} else {
format!("minecraft:{}", entity.id)
};
let pos = [
entity.position.0 - f64::from(mx),
entity.position.1 - f64::from(my),
entity.position.2 - f64::from(mz),
];
let motion = nbt_vec3(entity.nbt.get("Motion"));
if !out.is_empty() {
out.push_str(",\n ");
}
let _ = write!(
out,
"{{pos: [{}, {}, {}], blockPos: [{}, {}, {}], nbt: {{id: \"{id}\"",
snbt_double(pos[0]),
snbt_double(pos[1]),
snbt_double(pos[2]),
pos[0].floor() as i32,
pos[1].floor() as i32,
pos[2].floor() as i32,
);
let _ = write!(
out,
", Motion: [{}, {}, {}]",
snbt_double(motion[0]),
snbt_double(motion[1]),
snbt_double(motion[2])
);
if let Some(V::Compound(stack)) = entity.nbt.get("Item") {
if let Some(V::String(item_id)) = stack.get("id") {
let count = stack
.get("count")
.or_else(|| stack.get("Count"))
.and_then(nbt_number)
.unwrap_or(1.0);
let _ = write!(
out,
", Item: {{id: \"{item_id}\", count: {}b}}",
count as i64
);
}
}
if let Some(delay) = entity.nbt.get("PickupDelay").and_then(nbt_number) {
let _ = write!(out, ", PickupDelay: {}s", delay as i64);
}
if let Some(V::List(rotation)) = entity.nbt.get("Rotation") {
if let Some(yaw) = rotation.first().and_then(nbt_number) {
let mut text = format!("{yaw}");
if !text.contains('.') {
text.push_str(".0");
}
let _ = write!(out, ", Rotation: [{text}f, 0.0f]");
}
}
if let Some(value) = entity.nbt.get("UUID") {
let _ = write!(out, ", UUID: {}", entity_value_snbt(value));
}
for key in ["leash", "Leash"] {
if let Some(value) = entity.nbt.get(key) {
let _ = write!(out, ", {key}: {}", entity_value_snbt(value));
}
}
for (tag, key) in [("Fuel", "Fuel"), ("PushX", "PushX"), ("PushZ", "PushZ")] {
if let Some(value) = entity.nbt.get(key).and_then(nbt_number) {
if value != 0.0 {
if tag == "Fuel" {
let _ = write!(out, ", Fuel: {}s", value as i64);
} else {
let _ = write!(out, ", {tag}: {}", snbt_double(value));
}
}
}
}
if let Some(V::List(items)) = entity.nbt.get("Items") {
let mut body = String::new();
for item in items {
let V::Compound(fields) = item else { continue };
let Some(V::String(id)) = fields.get("id") else {
continue;
};
let slot = match fields.get("Slot") {
Some(V::Byte(v)) => i64::from(*v),
Some(V::Int(v)) => i64::from(*v),
_ => 0,
};
let count = match fields.get("count").or_else(|| fields.get("Count")) {
Some(V::Byte(v)) => i64::from(*v),
Some(V::Int(v)) => i64::from(*v),
_ => 1,
};
if !body.is_empty() {
body.push_str(", ");
}
let _ = write!(body, "{{Slot: {slot}b, count: {count}, id: \"{id}\"}}");
}
if !body.is_empty() {
let _ = write!(out, ", Items: [{body}]");
}
}
if let Some(V::List(passengers)) = entity.nbt.get("Passengers") {
let mut riders = String::new();
for rider in passengers {
let V::Compound(fields) = rider else { continue };
let Some(V::String(rider_id)) = fields.get("id") else {
continue;
};
let rider_id = if rider_id.contains(':') {
rider_id.clone()
} else {
format!("minecraft:{rider_id}")
};
let seat: Vec<f64> = match fields.get("Pos") {
Some(V::List(values)) if values.len() == 3 => values
.iter()
.map(|v| nbt_number(v).unwrap_or(0.0))
.collect(),
_ => Vec::new(),
};
let motion = nbt_vec3(fields.get("Motion"));
if !riders.is_empty() {
riders.push_str(", ");
}
let _ = write!(riders, "{{id: \"{rider_id}\"");
if seat.len() == 3 {
let _ = write!(
riders,
", Pos: [{}, {}, {}]",
snbt_double(seat[0] - f64::from(mx)),
snbt_double(seat[1] - f64::from(my)),
snbt_double(seat[2] - f64::from(mz)),
);
}
if let Some(value) = fields.get("UUID") {
let _ = write!(riders, ", UUID: {}", entity_value_snbt(value));
}
let _ = write!(
riders,
", Motion: [{}, {}, {}]}}",
snbt_double(motion[0]),
snbt_double(motion[1]),
snbt_double(motion[2])
);
}
if !riders.is_empty() {
let _ = write!(out, ", Passengers: [{riders}]");
}
}
out.push_str("}}");
}
out
}
pub fn to_gametest_snbt(schematic: &crate::UniversalSchematic) -> String {
let data_version = source_data_version(schematic);
if let Some(modern) = modernized(schematic) {
return render(&modern, data_version);
}
render(schematic, data_version)
}
fn source_data_version(schematic: &crate::UniversalSchematic) -> i32 {
schematic
.metadata
.source_data_version
.or(schematic.metadata.mc_version)
.unwrap_or(crate::dataconverter::CANONICAL_DATA_VERSION)
}
fn render(schematic: &crate::UniversalSchematic, data_version: i32) -> String {
use std::collections::HashMap;
use std::fmt::Write as _;
let bb = schematic.get_bounding_box();
let (mx, my, mz) = bb.min;
let size = (bb.max.0 - mx + 1, bb.max.1 - my + 1, bb.max.2 - mz + 1);
let mut nbt_at: HashMap<(i32, i32, i32), String> = HashMap::new();
for be in schematic.get_block_entities_as_list() {
nbt_at.insert(be.position, compound_snbt(&be.nbt));
}
let mut palette: Vec<String> = Vec::new();
let mut palette_index: HashMap<String, usize> = HashMap::new();
let mut blocks = String::new();
for (pos, state) in schematic.iter_blocks() {
if state.name == "minecraft:air" {
continue;
}
let mut props: Vec<(&str, &str)> = state
.properties
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
props.sort();
let mut entry = format!("{{Name:\"{}\"", state.name);
if !props.is_empty() {
entry.push_str(", Properties:{");
for (i, (k, v)) in props.iter().enumerate() {
if i > 0 {
entry.push_str(", ");
}
let _ = write!(entry, "{k}: \"{v}\"");
}
entry.push('}');
}
entry.push('}');
let index = *palette_index.entry(entry.clone()).or_insert_with(|| {
palette.push(entry);
palette.len() - 1
});
if !blocks.is_empty() {
blocks.push_str(",\n ");
}
let _ = write!(
blocks,
"{{pos: [{}, {}, {}], state: {}",
pos.x - mx,
pos.y - my,
pos.z - mz,
index
);
if let Some(nbt) = nbt_at.get(&(pos.x, pos.y, pos.z)) {
let _ = write!(blocks, ", nbt: {nbt}");
}
blocks.push('}');
}
format!(
"{{\n DataVersion: {},\n size: [{}, {}, {}],\n palette: [\n {}\n ],\n blocks: [\n {}\n ],\n entities: [\n {}\n ]\n}}\n",
data_version,
size.0,
size.1,
size.2,
palette.join(",\n "),
blocks,
entities_snbt(schematic, (mx, my, mz))
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gametest_snbt_preserves_the_boat_fence_leash() {
let mut schematic = crate::UniversalSchematic::new("boat fence".to_string());
schematic.set_block(
1,
6,
3,
&crate::BlockState::new("minecraft:oak_fence".to_string()),
);
let mut boat = crate::Entity::new(
"minecraft:oak_boat".to_string(),
(
1.488_202_109_234_411,
0.045_043_285_781_332_54,
2.812_601_257_115_602_5,
),
);
boat.nbt.insert(
"Motion".to_string(),
crate::NbtValue::List(vec![
crate::NbtValue::Double(-4.323_888_315_084_626_6e-16),
crate::NbtValue::Double(0.011_681_267_954_871_115),
crate::NbtValue::Double(-3.611_030_807_389_778_4e-85),
]),
);
boat.nbt.insert(
"leash".to_string(),
crate::NbtValue::IntArray(vec![-23, -52, -46]),
);
schematic.add_entity(boat);
let snbt = to_gametest_snbt(&schematic);
assert!(snbt.contains("leash: [I; -23, -52, -46]"), "{snbt}");
}
#[test]
fn gametest_snbt_preserves_elevator_vehicle_and_rider_identity() {
use std::collections::HashMap;
let mut schematic = crate::UniversalSchematic::new("elevator boat".to_string());
schematic.set_block(
0,
0,
0,
&crate::BlockState::new("minecraft:stone".to_string()),
);
let mut boat = crate::Entity::new("minecraft:pale_oak_boat".to_string(), (0.5, 1.0, 0.5));
boat.nbt.insert(
"Motion".to_string(),
crate::NbtValue::List(vec![
crate::NbtValue::Double(0.0),
crate::NbtValue::Double(0.0),
crate::NbtValue::Double(-5.0e-6),
]),
);
boat.nbt.insert(
"UUID".to_string(),
crate::NbtValue::IntArray(vec![1, 2, 3, 4]),
);
let mut leash = HashMap::new();
leash.insert(
"UUID".to_string(),
crate::NbtValue::IntArray(vec![5, 6, 7, 8]),
);
boat.nbt
.insert("leash".to_string(), crate::NbtValue::Compound(leash));
let mut rider = HashMap::new();
rider.insert(
"id".to_string(),
crate::NbtValue::String("minecraft:silverfish".to_string()),
);
rider.insert(
"UUID".to_string(),
crate::NbtValue::IntArray(vec![9, 10, 11, 12]),
);
rider.insert(
"Pos".to_string(),
crate::NbtValue::List(vec![
crate::NbtValue::Double(0.5),
crate::NbtValue::Double(1.1875),
crate::NbtValue::Double(0.5),
]),
);
boat.nbt.insert(
"Passengers".to_string(),
crate::NbtValue::List(vec![crate::NbtValue::Compound(rider)]),
);
schematic.add_entity(boat);
let snbt = to_gametest_snbt(&schematic);
assert!(snbt.contains("UUID: [I; 1, 2, 3, 4]"), "{snbt}");
assert!(snbt.contains("leash: {UUID: [I; 5, 6, 7, 8]}"), "{snbt}");
assert!(snbt.contains("UUID: [I; 9, 10, 11, 12]"), "{snbt}");
let structure = mc_tick::Structure::parse(&snbt).expect("rendered structure parses");
let mc_tick::structure::SpawnedEntity::Body(boat) = &structure.entities[0] else {
panic!("expected pale-oak boat body");
};
assert!(boat.leashed);
assert_eq!(boat.passengers.len(), 1);
assert_eq!(boat.passengers[0].kind(), "minecraft:silverfish");
}
}