pub mod classifier;
pub mod footprint;
pub mod rulesets;
pub mod symmetry;
pub mod voxel;
pub use footprint::{footprint, Footprint};
#[cfg(test)]
pub(crate) mod testgen;
use crate::block_state::BlockState;
use crate::fingerprint::classifier::{Classifier, Token};
use crate::fingerprint::symmetry::Symmetry;
use crate::utils::{NbtMap, NbtValue};
fn is_positional_key(k: &str) -> bool {
matches!(k, "x" | "y" | "z" | "id" | "Id" | "Pos")
}
fn is_directional_key(k: &str) -> bool {
matches!(
k,
"Rotation" | "rotation" | "Facing" | "facing" | "Rot" | "rot"
)
}
fn write_nbt_value(out: &mut String, v: &NbtValue) {
use std::fmt::Write;
let write_str = |out: &mut String, s: &str| {
let _ = write!(out, "{}:", s.len());
out.push_str(s);
};
match v {
NbtValue::Byte(x) => {
let _ = write!(out, "B{x}");
}
NbtValue::Short(x) => {
let _ = write!(out, "S{x}");
}
NbtValue::Int(x) => {
let _ = write!(out, "I{x}");
}
NbtValue::Long(x) => {
let _ = write!(out, "L{x}");
}
NbtValue::Float(x) => {
let _ = write!(out, "F{}", x.to_bits());
}
NbtValue::Double(x) => {
let _ = write!(out, "D{}", x.to_bits());
}
NbtValue::String(s) => {
out.push('T');
write_str(out, s);
}
NbtValue::ByteArray(a) => {
out.push_str("BA[");
for x in a {
let _ = write!(out, "{x},");
}
out.push(']');
}
NbtValue::IntArray(a) => {
out.push_str("IA[");
for x in a {
let _ = write!(out, "{x},");
}
out.push(']');
}
NbtValue::LongArray(a) => {
out.push_str("LA[");
for x in a {
let _ = write!(out, "{x},");
}
out.push(']');
}
NbtValue::List(l) => {
out.push('[');
for x in l {
write_nbt_value(out, x);
out.push(',');
}
out.push(']');
}
NbtValue::Compound(m) => {
let mut entries: Vec<(&String, &NbtValue)> = m.iter().collect();
entries.sort_by(|a, b| a.0.cmp(b.0));
out.push('{');
for (k, v) in entries {
write_str(out, k);
out.push('=');
write_nbt_value(out, v);
out.push(';');
}
out.push('}');
}
}
}
pub(crate) fn stable_nbt_token(nbt: &NbtMap, ignore_directional: bool) -> Option<Token> {
let mut entries: Vec<(&String, &NbtValue)> = nbt
.iter()
.filter(|(k, _)| !is_positional_key(k))
.filter(|(k, _)| !(ignore_directional && is_directional_key(k)))
.collect();
if entries.is_empty() {
return None;
}
entries.sort_by(|a, b| a.0.cmp(b.0));
let mut out = String::new();
out.push('{');
for (k, v) in entries {
use std::fmt::Write;
let _ = write!(out, "{}:", k.len());
out.push_str(k);
out.push('=');
write_nbt_value(&mut out, v);
out.push(';');
}
out.push('}');
let hash = blake3::hash(out.as_bytes());
let hex = hash.to_hex();
Some(Token::from(&hex.as_str()[..NBT_TOKEN_HEX_LEN]))
}
pub(crate) const NBT_MARKER: &str = "#nbt:";
pub(crate) const NBT_TOKEN_HEX_LEN: usize = 16;
pub(crate) fn token_with_nbt(tok: &Token, nbt_tok: &Token) -> Token {
let mut s = String::with_capacity(tok.len() + nbt_tok.len() + NBT_MARKER.len());
s.push_str(tok.as_str());
s.push_str(NBT_MARKER);
s.push_str(nbt_tok.as_str());
Token::from(s)
}
pub(crate) fn token_has_nbt(tok: &Token) -> bool {
match tok.as_str().rsplit_once(NBT_MARKER) {
Some((_, hash)) => {
hash.len() == NBT_TOKEN_HEX_LEN && hash.bytes().all(|b| b.is_ascii_hexdigit())
}
None => false,
}
}
pub(crate) fn is_air(name: &str) -> bool {
matches!(
name,
"minecraft:air" | "minecraft:cave_air" | "minecraft:void_air"
)
}
#[derive(Clone)]
pub enum BlockPolicy {
Exact,
IdOnly,
Classify(Classifier),
}
impl BlockPolicy {
pub fn tokenize(&self, b: &BlockState) -> Option<Token> {
match self {
BlockPolicy::Classify(c) => c.tokenize(b),
BlockPolicy::IdOnly => {
if is_air(b.get_name()) {
None
} else {
Some(Token::from(b.get_name()))
}
}
BlockPolicy::Exact => {
if is_air(b.get_name()) {
return None;
}
let mut props: Vec<(&str, &str)> = b
.properties
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
props.sort_unstable();
let mut s = b.get_name().to_string();
for (k, v) in props {
s.push('|');
s.push_str(k);
s.push('=');
s.push_str(v);
}
Some(Token::from(s))
}
}
}
}
#[derive(Clone)]
pub struct FingerprintSpec {
pub symmetry: Symmetry,
pub blocks: BlockPolicy,
pub block_entities: bool,
}
impl FingerprintSpec {
pub fn exact() -> Self {
Self {
symmetry: Symmetry::None,
blocks: BlockPolicy::Exact,
block_entities: true,
}
}
pub fn shape() -> Self {
Self {
symmetry: Symmetry::Octahedral,
blocks: BlockPolicy::IdOnly,
block_entities: false,
}
}
pub fn structural() -> Self {
Self {
symmetry: Symmetry::YawMirror,
blocks: BlockPolicy::Classify(rulesets::structural()),
block_entities: false,
}
}
pub fn redstone_computational() -> Self {
Self {
symmetry: Symmetry::YawMirror,
blocks: BlockPolicy::Classify(rulesets::redstone_computational()),
block_entities: false,
}
}
pub fn redstone_survival() -> Self {
Self {
symmetry: Symmetry::YawMirror,
blocks: BlockPolicy::Classify(rulesets::redstone_survival()),
block_entities: false,
}
}
pub fn custom(symmetry: Symmetry, blocks: BlockPolicy) -> Self {
Self {
symmetry,
blocks,
block_entities: false,
}
}
pub fn with_block_entities(mut self, on: bool) -> Self {
self.block_entities = on;
self
}
pub const PRESETS: &'static [&'static str] = &[
"exact",
"shape",
"structural",
"redstone_computational",
"redstone",
"redstone_survival",
];
pub fn from_preset(name: &str) -> Option<Self> {
Some(match name {
"exact" => Self::exact(),
"shape" => Self::shape(),
"structural" => Self::structural(),
"redstone" | "redstone_computational" => Self::redstone_computational(),
"redstone_survival" => Self::redstone_survival(),
_ => return None,
})
}
}
use std::collections::{BTreeMap, HashMap};
use crate::universal_schematic::UniversalSchematic;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Signature {
pub dims_sorted: [u32; 3],
pub histogram: BTreeMap<Token, u32>,
pub count: u32,
}
pub fn signature(schem: &UniversalSchematic, spec: &FingerprintSpec) -> Signature {
let mut histogram: BTreeMap<Token, u32> = BTreeMap::new();
let mut count = 0u32;
let mut mn = (i32::MAX, i32::MAX, i32::MAX);
let mut mx = (i32::MIN, i32::MIN, i32::MIN);
for (pos, block) in schem.iter_blocks() {
if let Some(tok) = spec.blocks.tokenize(block) {
*histogram.entry(tok).or_default() += 1;
count += 1;
mn = (mn.0.min(pos.x), mn.1.min(pos.y), mn.2.min(pos.z));
mx = (mx.0.max(pos.x), mx.1.max(pos.y), mx.2.max(pos.z));
}
}
let mut dims = if count == 0 {
[0, 0, 0]
} else {
[
(mx.0 - mn.0 + 1) as u32,
(mx.1 - mn.1 + 1) as u32,
(mx.2 - mn.2 + 1) as u32,
]
};
dims.sort_unstable();
Signature {
dims_sorted: dims,
histogram,
count,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Fingerprint(pub u128);
impl std::fmt::Display for Fingerprint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:032x}", self.0)
}
}
impl Fingerprint {
pub fn to_hex(&self) -> String {
format!("{:032x}", self.0)
}
}
pub fn is_duplicate(
a: &UniversalSchematic,
b: &UniversalSchematic,
spec: &FingerprintSpec,
) -> bool {
fingerprint(a, spec) == fingerprint(b, spec)
}
pub fn footprint_distance(
a: &UniversalSchematic,
b: &UniversalSchematic,
spec: &FingerprintSpec,
) -> f32 {
footprint(a, spec).distance(&footprint(b, spec))
}
impl Signature {
pub fn to_json(&self) -> String {
let hist: serde_json::Map<String, serde_json::Value> = self
.histogram
.iter()
.map(|(k, v)| (k.to_string(), serde_json::json!(v)))
.collect();
serde_json::json!({
"dims_sorted": self.dims_sorted,
"count": self.count,
"histogram": hist,
})
.to_string()
}
}
pub fn fingerprint(schem: &UniversalSchematic, spec: &FingerprintSpec) -> Fingerprint {
let elements = spec.symmetry.elements();
if elements.len() == 1 {
return fingerprint_single_orbit(schem, spec, &elements[0]);
}
let mut palette: Vec<&BlockState> = Vec::new();
let mut index: HashMap<&BlockState, usize> = HashMap::new();
let mut cell_list: Vec<((i32, i32, i32), usize)> = Vec::new();
for (pos, block) in schem.iter_blocks() {
let id = *index.entry(block).or_insert_with(|| {
palette.push(block);
palette.len() - 1
});
cell_list.push(((pos.x, pos.y, pos.z), id));
}
let mut nbt_memo: HashMap<*const NbtMap, Option<Token>> = HashMap::new();
let mut nbt_by_pos: HashMap<(i32, i32, i32), Token> = HashMap::new();
let ignore_directional = spec.symmetry != Symmetry::None;
if spec.block_entities {
for (pos, _) in &cell_list {
if let Some(be) = schem.get_block_entity(crate::block_position::BlockPosition {
x: pos.0,
y: pos.1,
z: pos.2,
}) {
let key = std::sync::Arc::as_ptr(&be.nbt);
let tok = nbt_memo
.entry(key)
.or_insert_with(|| stable_nbt_token(&be.nbt, ignore_directional))
.clone();
if let Some(t) = tok {
nbt_by_pos.insert(*pos, t);
}
}
}
}
let mut best: Option<Vec<u8>> = None;
for g in spec.symmetry.elements() {
let toks: Vec<Option<Token>> = palette
.iter()
.map(|b| spec.blocks.tokenize(&g.apply_block(b)))
.collect();
let mut cells: Vec<((i32, i32, i32), Token)> = Vec::with_capacity(cell_list.len());
for (pos, id) in &cell_list {
if let Some(tok) = &toks[*id] {
let tok = match nbt_by_pos.get(pos) {
Some(nbt) => token_with_nbt(tok, nbt),
None => tok.clone(),
};
cells.push((g.apply_pos(*pos), tok));
}
}
if cells.is_empty() {
continue;
}
let mn = cells
.iter()
.fold((i32::MAX, i32::MAX, i32::MAX), |m, (p, _)| {
(m.0.min(p.0), m.1.min(p.1), m.2.min(p.2))
});
for (p, _) in cells.iter_mut() {
*p = (p.0 - mn.0, p.1 - mn.1, p.2 - mn.2);
}
cells.sort();
let ser = serialize_cells(&cells);
best = Some(match best {
Some(cur) if cur <= ser => cur,
_ => ser,
});
}
let bytes = best.unwrap_or_default();
let hash = blake3::hash(&bytes);
let mut buf = [0u8; 16];
buf.copy_from_slice(&hash.as_bytes()[..16]);
Fingerprint(u128::from_le_bytes(buf))
}
fn serialize_cells(cells: &[((i32, i32, i32), Token)]) -> Vec<u8> {
let mut out = Vec::with_capacity(cells.len() * 16);
out.extend_from_slice(&(cells.len() as u32).to_le_bytes());
for ((x, y, z), tok) in cells {
out.extend_from_slice(&x.to_le_bytes());
out.extend_from_slice(&y.to_le_bytes());
out.extend_from_slice(&z.to_le_bytes());
out.extend_from_slice(&(tok.len() as u32).to_le_bytes());
out.extend_from_slice(tok.as_bytes());
}
out
}
fn fingerprint_single_orbit(
schem: &UniversalSchematic,
spec: &FingerprintSpec,
g: &crate::fingerprint::symmetry::RigidOp,
) -> Fingerprint {
const NO_NBT: u32 = u32::MAX;
let ignore_directional = spec.symmetry != Symmetry::None;
let mut index: HashMap<&BlockState, u32> = HashMap::new();
let mut toks: Vec<Option<Token>> = Vec::new();
let mut nbt_memo: HashMap<*const NbtMap, Option<Token>> = HashMap::new();
let mut nbt_tokens: Vec<Token> = Vec::new();
let mut cells: Vec<(i32, i32, i32, u32, u32)> = Vec::new();
for (pos, block) in schem.iter_blocks() {
let id = *index.entry(block).or_insert_with(|| {
toks.push(spec.blocks.tokenize(&g.apply_block(block)));
(toks.len() - 1) as u32
});
if toks[id as usize].is_none() {
continue;
}
let nbt_id = if spec.block_entities {
match schem.get_block_entity(crate::block_position::BlockPosition {
x: pos.x,
y: pos.y,
z: pos.z,
}) {
Some(be) => {
let key = std::sync::Arc::as_ptr(&be.nbt);
let tok = nbt_memo
.entry(key)
.or_insert_with(|| stable_nbt_token(&be.nbt, ignore_directional))
.clone();
match tok {
Some(t) => {
nbt_tokens.push(t);
(nbt_tokens.len() - 1) as u32
}
None => NO_NBT,
}
}
None => NO_NBT,
}
} else {
NO_NBT
};
let (x, y, z) = g.apply_pos((pos.x, pos.y, pos.z));
cells.push((x, y, z, id, nbt_id));
}
let hash = if cells.is_empty() {
blake3::hash(&[])
} else {
let mn = cells.iter().fold((i32::MAX, i32::MAX, i32::MAX), |m, c| {
(m.0.min(c.0), m.1.min(c.1), m.2.min(c.2))
});
for c in cells.iter_mut() {
c.0 -= mn.0;
c.1 -= mn.1;
c.2 -= mn.2;
}
let reconstruct = |pid: u32, nbt_id: u32| -> Token {
let base = toks[pid as usize].as_ref().unwrap();
if nbt_id == NO_NBT {
base.clone()
} else {
token_with_nbt(base, &nbt_tokens[nbt_id as usize])
}
};
cells.sort_by(|a, b| {
(a.0, a.1, a.2).cmp(&(b.0, b.1, b.2)).then_with(|| {
reconstruct(a.3, a.4)
.as_str()
.cmp(reconstruct(b.3, b.4).as_str())
})
});
let mut hasher = blake3::Hasher::new();
let mut buf: Vec<u8> = Vec::with_capacity(1 << 16);
buf.extend_from_slice(&(cells.len() as u32).to_le_bytes());
for &(x, y, z, pid, nbt_id) in &cells {
buf.extend_from_slice(&x.to_le_bytes());
buf.extend_from_slice(&y.to_le_bytes());
buf.extend_from_slice(&z.to_le_bytes());
let base = toks[pid as usize].as_ref().unwrap();
if nbt_id == NO_NBT {
buf.extend_from_slice(&(base.len() as u32).to_le_bytes());
buf.extend_from_slice(base.as_bytes());
} else {
let full = token_with_nbt(base, &nbt_tokens[nbt_id as usize]);
buf.extend_from_slice(&(full.len() as u32).to_le_bytes());
buf.extend_from_slice(full.as_bytes());
}
if buf.len() >= 1 << 16 {
hasher.update(&buf);
buf.clear();
}
}
if !buf.is_empty() {
hasher.update(&buf);
}
hasher.finalize()
};
let mut out = [0u8; 16];
out.copy_from_slice(&hash.as_bytes()[..16]);
Fingerprint(u128::from_le_bytes(out))
}
#[cfg(test)]
mod signature_tests {
use super::*;
use crate::fingerprint::testgen::{filled_box, translated};
#[test]
fn signature_is_translation_invariant() {
let a = filled_box((0, 0, 0), (3, 2, 1), "minecraft:stone");
let b = translated(&a, (40, -5, 12));
let spec = FingerprintSpec::structural();
assert_eq!(signature(&a, &spec), signature(&b, &spec));
}
}
#[cfg(test)]
mod block_entity_tests {
use super::*;
use crate::block_entity::BlockEntity;
use crate::block_position::BlockPosition;
use crate::fingerprint::testgen::filled_box;
use crate::utils::NbtValue;
fn signed_at(text: &str, at: (i32, i32, i32)) -> UniversalSchematic {
let mut s = filled_box(at, (at.0 + 1, at.1, at.2), "minecraft:oak_sign");
s.set_block_entity(
BlockPosition {
x: at.0,
y: at.1,
z: at.2,
},
BlockEntity::new("minecraft:oak_sign".to_string(), at)
.with_nbt_data("Text1".to_string(), NbtValue::String(text.to_string())),
);
s
}
fn signed(text: &str) -> UniversalSchematic {
signed_at(text, (0, 0, 0))
}
#[test]
fn differing_block_entity_nbt_changes_the_fingerprint() {
let spec = FingerprintSpec::exact();
let a = signed("Alpha");
let b = signed("Beta");
assert_ne!(
fingerprint(&a, &spec),
fingerprint(&b, &spec),
"sign text must be part of the fingerprint"
);
}
#[test]
fn identical_block_entities_share_a_fingerprint() {
let spec = FingerprintSpec::exact();
let a = signed("Alpha");
let b = signed("Alpha");
assert_eq!(fingerprint(&a, &spec), fingerprint(&b, &spec));
}
#[test]
fn block_entity_fingerprint_is_translation_invariant() {
let spec = FingerprintSpec::exact();
let a = signed("Alpha");
let b = signed_at("Alpha", (13, 2, -7));
assert_eq!(
fingerprint(&a, &spec),
fingerprint(&b, &spec),
"translated copy (block entities included) must fingerprint equal"
);
}
fn sign_with_key(key: &str, value: &str) -> UniversalSchematic {
let at = (0, 0, 0);
let mut s = filled_box(at, (1, 0, 0), "minecraft:oak_sign");
s.set_block_entity(
BlockPosition { x: 0, y: 0, z: 0 },
BlockEntity::new("minecraft:oak_sign".to_string(), at)
.with_nbt_data(key.to_string(), NbtValue::String(value.to_string())),
);
s
}
#[test]
fn fuzzy_presets_ignore_block_entity_nbt() {
let a = signed("Alpha");
let b = signed("Beta");
for (name, spec) in [
("shape", FingerprintSpec::shape()),
("structural", FingerprintSpec::structural()),
("redstone_survival", FingerprintSpec::redstone_survival()),
] {
assert!(!spec.block_entities, "{name} must not fold NBT");
assert_eq!(
fingerprint(&a, &spec),
fingerprint(&b, &spec),
"{name}: differing sign text must NOT change a fuzzy fingerprint"
);
}
let exact = FingerprintSpec::exact();
assert!(exact.block_entities);
assert_ne!(fingerprint(&a, &exact), fingerprint(&b, &exact));
}
#[test]
fn rotation_tolerant_specs_ignore_directional_nbt() {
let rot = FingerprintSpec::custom(Symmetry::YawMirror, BlockPolicy::Exact)
.with_block_entities(true);
let a = sign_with_key("Rotation", "4");
let b = sign_with_key("Rotation", "12");
assert_eq!(
fingerprint(&a, &rot),
fingerprint(&b, &rot),
"rotation-tolerant spec must ignore directional NBT keys"
);
let c = sign_with_key("Text1", "Alpha");
let d = sign_with_key("Text1", "Beta");
assert_ne!(
fingerprint(&c, &rot),
fingerprint(&d, &rot),
"non-directional NBT is still content"
);
let none =
FingerprintSpec::custom(Symmetry::None, BlockPolicy::Exact).with_block_entities(true);
assert_ne!(
fingerprint(&a, &none),
fingerprint(&b, &none),
"Symmetry::None must keep directional NBT"
);
}
#[test]
fn token_has_nbt_requires_a_well_formed_marker() {
let base = Token::from("minecraft:stone");
let nbt = Token::from("0123456789abcdef");
assert!(token_has_nbt(&token_with_nbt(&base, &nbt)));
assert!(!token_has_nbt(&base));
assert!(!token_has_nbt(&Token::from("modid:weird#nbt:block")));
assert!(!token_has_nbt(&Token::from("foo#nbt:")));
assert!(!token_has_nbt(&Token::from("foo#nbt:0123456789abcde")));
assert!(!token_has_nbt(&Token::from("foo#nbt:0123456789abcdefg")));
assert!(!token_has_nbt(&Token::from("foo#nbt:zzzzzzzzzzzzzzzz")));
}
}
#[cfg(test)]
mod fp_tests {
use super::*;
use crate::fingerprint::testgen::{filled_box, rotated_y, translated};
#[test]
fn translation_invariant() {
let a = filled_box((0, 0, 0), (3, 2, 1), "minecraft:stone");
let b = translated(&a, (17, 4, -9));
let spec = FingerprintSpec::structural();
assert_eq!(fingerprint(&a, &spec), fingerprint(&b, &spec));
}
#[test]
fn yaw_rotation_invariant_under_yawmirror() {
let a = filled_box((0, 0, 0), (3, 0, 1), "minecraft:stone");
let b = rotated_y(&a, 90);
let spec = FingerprintSpec::structural();
assert_eq!(fingerprint(&a, &spec), fingerprint(&b, &spec));
}
#[test]
fn different_builds_differ() {
let a = filled_box((0, 0, 0), (3, 0, 1), "minecraft:stone");
let c = filled_box((0, 0, 0), (5, 0, 1), "minecraft:stone");
let spec = FingerprintSpec::structural();
assert_ne!(fingerprint(&a, &spec), fingerprint(&c, &spec));
}
}
#[cfg(test)]
mod spec_tests {
use super::*;
#[test]
fn policies_tokenize() {
let repeater = BlockState::new("minecraft:repeater")
.with_properties(vec![("facing".into(), "east".into())]);
assert_eq!(
BlockPolicy::Exact.tokenize(&repeater).as_deref(),
Some("minecraft:repeater|facing=east")
);
assert_eq!(
BlockPolicy::IdOnly.tokenize(&repeater).as_deref(),
Some("minecraft:repeater")
);
assert_eq!(
BlockPolicy::Exact.tokenize(&BlockState::new("minecraft:air")),
None
);
}
#[test]
fn presets_build() {
let _ = FingerprintSpec::redstone_computational();
let _ = FingerprintSpec::structural();
let _ = FingerprintSpec::exact();
let _ = FingerprintSpec::shape();
let _ = FingerprintSpec::redstone_survival();
}
#[test]
fn preset_names_resolve() {
for name in FingerprintSpec::PRESETS {
assert!(FingerprintSpec::from_preset(name).is_some(), "{name}");
}
assert!(FingerprintSpec::from_preset("nope").is_none());
}
#[test]
fn fingerprint_hex_and_dup() {
use crate::fingerprint::testgen::filled_box;
let spec = FingerprintSpec::structural();
let a = filled_box((0, 0, 0), (2, 2, 2), "minecraft:stone");
let b = filled_box((10, 0, 0), (12, 2, 2), "minecraft:stone"); let hx = fingerprint(&a, &spec).to_hex();
assert_eq!(hx.len(), 32);
assert!(hx.chars().all(|c| c.is_ascii_hexdigit()));
assert!(
is_duplicate(&a, &b, &spec),
"translation-invariant structural dup"
);
}
#[test]
fn signature_json_and_footprint_distance() {
use crate::fingerprint::testgen::filled_box;
let spec = FingerprintSpec::structural();
let a = filled_box((0, 0, 0), (2, 2, 2), "minecraft:stone");
let b = filled_box((10, 0, 0), (12, 2, 2), "minecraft:stone");
let sig: serde_json::Value = serde_json::from_str(&signature(&a, &spec).to_json()).unwrap();
assert!(sig["count"].as_u64().is_some());
assert!(sig["histogram"].is_object());
assert!(footprint_distance(&a, &b, &spec) < 1e-3);
}
}
#[cfg(test)]
mod smoke {
#[test]
fn module_compiles() {
assert_eq!(2 + 2, 4);
}
}
#[cfg(test)]
mod exact_memory_goldens {
use super::*;
use crate::block_entity::BlockEntity;
use crate::block_position::BlockPosition;
use crate::fingerprint::testgen::{filled_box, translated};
use crate::utils::NbtValue;
fn fixtures() -> Vec<(&'static str, UniversalSchematic)> {
let mut v: Vec<(&'static str, UniversalSchematic)> = Vec::new();
v.push((
"small_stone",
filled_box((0, 0, 0), (2, 2, 2), "minecraft:stone"),
));
{
let mut s = UniversalSchematic::new("g".into());
for x in 0..20 {
for y in 0..8 {
for z in 0..12 {
let name = match (x + y + z) % 3 {
0 => "minecraft:stone",
1 => "minecraft:oak_planks",
_ => "minecraft:glass",
};
s.set_block(x, y, z, &BlockState::new(name));
}
}
}
v.push(("medium_mixed", s));
}
{
let faces = ["north", "south", "east", "west"];
let mut s = UniversalSchematic::new("g".into());
for x in 0..10 {
for z in 0..10 {
let f = faces[((x + z) as usize) % 4];
s.set_block(
x,
0,
z,
&BlockState::new("minecraft:repeater")
.with_properties(vec![("facing".into(), f.into())]),
);
}
}
v.push(("directional", s));
}
{
let at = (1, 2, 3);
let mut s = filled_box(at, (at.0 + 1, at.1, at.2), "minecraft:oak_sign");
s.set_block_entity(
BlockPosition {
x: at.0,
y: at.1,
z: at.2,
},
BlockEntity::new("minecraft:oak_sign".to_string(), at)
.with_nbt_data("Text1".to_string(), NbtValue::String("Alpha".to_string())),
);
v.push(("single_sign", s));
}
{
let mut s = filled_box((0, 0, 0), (4, 0, 0), "minecraft:chest");
for x in 0..=4 {
s.set_block_entity(
BlockPosition { x, y: 0, z: 0 },
BlockEntity::new("minecraft:chest".to_string(), (x, 0, 0))
.with_nbt_data("Lock".to_string(), NbtValue::String(format!("key{x}"))),
);
}
v.push(("multi_be", s));
}
v.push((
"dense_fill",
filled_box((0, 0, 0), (40, 40, 40), "minecraft:stone"),
));
{
let mut s = UniversalSchematic::new("g".into());
for x in 0..30 {
for y in 0..30 {
for z in 0..30 {
let name = if (x * 7 + y * 13 + z) % 5 == 0 {
"minecraft:glass"
} else {
"minecraft:stone"
};
s.set_block(x, y, z, &BlockState::new(name));
}
}
}
v.push(("dense_two_palette", s));
}
v
}
const GOLDENS: &[(&str, &str)] = &[
("small_stone", "df3e2b19f2380b53c06e7f75fa543e8c"),
("medium_mixed", "60528fd493f006f74afa098a6d84eba0"),
("directional", "3a71c16dafd6244f6aef8b59c29cfcbf"),
("single_sign", "a5e179503d27ef3951337afd223156a4"),
("multi_be", "b48400517f6760bae33601b1d645993e"),
("dense_fill", "17f19f9a0e2bd65f5979ded604079c49"),
("dense_two_palette", "dc670e931be3a5887b113b56d8fffde2"),
];
#[test]
fn exact_fingerprint_is_byte_identical_to_goldens() {
let spec = FingerprintSpec::exact();
let map: std::collections::HashMap<&str, &str> = GOLDENS.iter().copied().collect();
let mut missing = false;
for (name, s) in fixtures() {
let hx = fingerprint(&s, &spec).to_hex();
match map.get(name) {
Some(exp) if *exp == "PLACEHOLDER" => {
eprintln!("GOLDEN\t{name}\t{hx}");
missing = true;
}
Some(exp) => assert_eq!(&hx, exp, "exact fingerprint drift for `{name}`"),
None => {
eprintln!("GOLDEN\t{name}\t{hx}");
missing = true;
}
}
}
assert!(
!missing,
"fill in PLACEHOLDER goldens from the printed lines"
);
}
#[test]
fn exact_dense_fill_translation_invariant() {
let spec = FingerprintSpec::exact();
let a = filled_box((0, 0, 0), (20, 20, 20), "minecraft:stone");
let b = translated(&a, (37, -11, 5));
assert_eq!(fingerprint(&a, &spec), fingerprint(&b, &spec));
}
}