use super::graph::{ComparatorMode, LinkKind, RedstoneGraph, RedstoneNode, RedstoneNodeKind};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct RedstoneFingerprint(pub u128);
impl std::fmt::Display for RedstoneFingerprint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:032x}", self.0)
}
}
impl RedstoneFingerprint {
pub fn to_hex(&self) -> String {
format!("{:032x}", self.0)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct GraphFingerprintSpec {
pub iterations: u32,
pub include_delay: bool,
pub include_comparator_mode: bool,
pub include_facing: bool,
pub include_link_strength: bool,
pub include_state: bool,
}
impl GraphFingerprintSpec {
pub fn structural() -> Self {
Self {
iterations: 3,
include_delay: false,
include_comparator_mode: false,
include_facing: false,
include_link_strength: false,
include_state: false,
}
}
pub fn functional() -> Self {
Self {
iterations: 3,
include_delay: true,
include_comparator_mode: true,
include_facing: false,
include_link_strength: false,
include_state: false,
}
}
pub fn exact() -> Self {
Self {
iterations: 3,
include_delay: true,
include_comparator_mode: true,
include_facing: true,
include_link_strength: true,
include_state: true,
}
}
pub const PRESETS: &'static [&'static str] = &["structural", "functional", "exact"];
pub fn from_preset(name: &str) -> Option<Self> {
Some(match name {
"structural" => Self::structural(),
"functional" => Self::functional(),
"exact" => Self::exact(),
_ => return None,
})
}
}
fn hash128(bytes: &[u8]) -> u128 {
let h = blake3::hash(bytes);
let mut buf = [0u8; 16];
buf.copy_from_slice(&h.as_bytes()[..16]);
u128::from_le_bytes(buf)
}
fn kind_discriminant(kind: &RedstoneNodeKind) -> u8 {
match kind {
RedstoneNodeKind::Repeater { .. } => 0,
RedstoneNodeKind::Comparator { .. } => 1,
RedstoneNodeKind::Torch => 2,
RedstoneNodeKind::Lamp => 3,
RedstoneNodeKind::Button => 4,
RedstoneNodeKind::Lever => 5,
RedstoneNodeKind::PressurePlate => 6,
RedstoneNodeKind::Trapdoor => 7,
RedstoneNodeKind::Wire => 8,
RedstoneNodeKind::Constant => 9,
RedstoneNodeKind::NoteBlock => 10,
}
}
fn initial_label(node: &RedstoneNode, spec: &GraphFingerprintSpec) -> u128 {
let mut buf: Vec<u8> = Vec::with_capacity(16);
buf.push(0xA0); buf.push(kind_discriminant(&node.kind));
match &node.kind {
RedstoneNodeKind::Repeater { delay } => {
if spec.include_delay {
buf.push(0xD0);
buf.push(*delay);
}
}
RedstoneNodeKind::Comparator { mode, far_input } => {
if spec.include_comparator_mode {
buf.push(0xC0);
buf.push(match mode {
ComparatorMode::Compare => 0,
ComparatorMode::Subtract => 1,
});
buf.push(far_input.is_some() as u8);
buf.push(far_input.unwrap_or(0));
}
}
_ => {}
}
if spec.include_facing {
buf.push(0xF0);
buf.push(node.facing_diode as u8);
}
if spec.include_state {
buf.push(0x50);
buf.push(node.powered as u8);
buf.push(node.repeater_locked as u8);
buf.push(node.output_strength);
}
hash128(&buf)
}
fn link_label(kind: LinkKind, strength: u8, spec: &GraphFingerprintSpec) -> u8 {
let _ = (strength, spec);
match kind {
LinkKind::Default => 0,
LinkKind::Side => 1,
}
}
const DIR_IN: u8 = 0x01;
const DIR_OUT: u8 = 0x02;
impl RedstoneGraph {
pub fn fingerprint(&self, spec: &GraphFingerprintSpec) -> RedstoneFingerprint {
let n = self.nodes.len();
let mut out_edges: Vec<Vec<(usize, LinkKind, u8)>> = vec![Vec::new(); n];
for node in &self.nodes {
let to = node.id;
for link in &node.inputs {
if link.from < n {
out_edges[link.from].push((to, link.kind, link.strength));
}
}
}
let mut labels: Vec<u128> = self
.nodes
.iter()
.map(|node| initial_label(node, spec))
.collect();
for _ in 0..spec.iterations {
let mut next: Vec<u128> = Vec::with_capacity(n);
for node in &self.nodes {
let idx = node.id;
let mut neigh: Vec<Vec<u8>> = Vec::new();
for link in &node.inputs {
if link.from >= n {
continue;
}
neigh.push(encode_edge(
DIR_IN,
link.kind,
link.strength,
labels[link.from],
spec,
));
}
for &(to, kind, strength) in &out_edges[idx] {
neigh.push(encode_edge(DIR_OUT, kind, strength, labels[to], spec));
}
neigh.sort_unstable();
let mut buf: Vec<u8> = Vec::with_capacity(17 + neigh.len() * 18);
buf.push(0xB0); buf.extend_from_slice(&labels[idx].to_le_bytes());
buf.extend_from_slice(&(neigh.len() as u32).to_le_bytes());
for e in &neigh {
buf.extend_from_slice(&(e.len() as u32).to_le_bytes());
buf.extend_from_slice(e);
}
next.push(hash128(&buf));
}
labels = next;
}
let edge_count: usize = self.nodes.iter().map(|nd| nd.inputs.len()).sum();
let mut final_labels = labels;
final_labels.sort_unstable();
let mut buf: Vec<u8> = Vec::with_capacity(16 + final_labels.len() * 16);
buf.push(0xC1); buf.extend_from_slice(&(n as u64).to_le_bytes());
buf.extend_from_slice(&(edge_count as u64).to_le_bytes());
for l in &final_labels {
buf.extend_from_slice(&l.to_le_bytes());
}
RedstoneFingerprint(hash128(&buf))
}
pub fn is_structurally_equal(&self, other: &RedstoneGraph) -> bool {
let spec = GraphFingerprintSpec::structural();
self.fingerprint(&spec) == other.fingerprint(&spec)
}
}
fn encode_edge(
dir: u8,
kind: LinkKind,
strength: u8,
neighbour_label: u128,
spec: &GraphFingerprintSpec,
) -> Vec<u8> {
let mut e = Vec::with_capacity(19);
e.push(dir);
e.push(link_label(kind, strength, spec));
if spec.include_link_strength {
e.push(strength);
}
e.extend_from_slice(&neighbour_label.to_le_bytes());
e
}
#[cfg(test)]
mod tests {
use super::*;
use crate::simulation::graph::{RedstoneLink, RedstoneNode, RedstoneNodeKind};
use crate::simulation::MchprsWorld;
use crate::{BlockState, UniversalSchematic};
fn redstone_line(len: u32, origin: (i32, i32, i32)) -> UniversalSchematic {
let (ox, oy, oz) = origin;
let mut schematic = UniversalSchematic::new("line".to_string());
for x in 0..(len as i32 + 1) {
schematic.set_block(
ox + x,
oy,
oz,
&BlockState::new("minecraft:gray_concrete".to_string()),
);
}
for x in 1..(len as i32) {
let mut wire = BlockState::new("minecraft:redstone_wire".to_string());
wire.set_property("power", "0");
wire.set_property("east", "side");
wire.set_property("west", "side");
wire.set_property("north", "none");
wire.set_property("south", "none");
schematic.set_block(ox + x, oy + 1, oz, &wire);
}
let mut lever = BlockState::new("minecraft:lever".to_string());
lever.set_property("facing", "east");
lever.set_property("powered", "false");
lever.set_property("face", "floor");
schematic.set_block(ox, oy + 1, oz, &lever);
let mut lamp = BlockState::new("minecraft:redstone_lamp".to_string());
lamp.set_property("lit", "false");
schematic.set_block(ox + len as i32, oy + 1, oz, &lamp);
schematic
}
fn graph_of(schem: UniversalSchematic) -> RedstoneGraph {
MchprsWorld::new(schem)
.expect("world")
.export_graph()
.expect("graph")
}
fn node(id: usize, kind: RedstoneNodeKind, inputs: Vec<RedstoneLink>) -> RedstoneNode {
RedstoneNode {
id,
kind,
pos: Some((id as i32, 0, 0)),
facing_diode: false,
powered: false,
repeater_locked: false,
output_strength: 0,
aliased_blocks: Vec::new(),
inputs,
}
}
fn link(from: usize) -> RedstoneLink {
RedstoneLink {
from,
kind: LinkKind::Default,
strength: 0,
}
}
fn repeater_chain(delay: u8) -> RedstoneGraph {
RedstoneGraph {
nodes: vec![
node(0, RedstoneNodeKind::Lever, vec![]),
node(1, RedstoneNodeKind::Repeater { delay }, vec![link(0)]),
node(2, RedstoneNodeKind::Lamp, vec![link(1)]),
],
}
}
#[test]
fn layout_invariance_structural() {
let a = graph_of(redstone_line(14, (0, 0, 0)));
let b = graph_of(redstone_line(10, (40, 8, -12)));
let spec = GraphFingerprintSpec::structural();
let fa = a.fingerprint(&spec);
let fb = b.fingerprint(&spec);
assert_eq!(
fa,
fb,
"layout-invariant structural dup: {} vs {}",
fa.to_hex(),
fb.to_hex()
);
assert!(a.is_structurally_equal(&b));
println!("LAYOUT_INVARIANCE_HEX a={} b={}", fa.to_hex(), fb.to_hex());
}
#[test]
fn different_circuits_differ() {
let line = graph_of(redstone_line(12, (0, 0, 0)));
let solo = RedstoneGraph {
nodes: vec![
node(0, RedstoneNodeKind::Lever, vec![]),
node(1, RedstoneNodeKind::Torch, vec![link(0)]),
node(2, RedstoneNodeKind::Torch, vec![link(1)]),
],
};
let spec = GraphFingerprintSpec::structural();
assert_ne!(line.fingerprint(&spec), solo.fingerprint(&spec));
}
#[test]
fn mask_sensitivity_delay() {
let a = repeater_chain(1);
let b = repeater_chain(4);
let structural = GraphFingerprintSpec::structural();
assert_eq!(
a.fingerprint(&structural),
b.fingerprint(&structural),
"delay must be invisible to structural"
);
let functional = GraphFingerprintSpec::functional();
assert_ne!(
a.fingerprint(&functional),
b.fingerprint(&functional),
"delay must change functional"
);
}
#[test]
fn determinism_and_order_independence() {
let g = repeater_chain(2);
let spec = GraphFingerprintSpec::exact();
assert_eq!(g.fingerprint(&spec), g.fingerprint(&spec));
let remap = |old: usize| match old {
0 => 1,
1 => 2,
2 => 0,
_ => unreachable!(),
};
let mut shuffled_nodes: Vec<RedstoneNode> = g
.nodes
.iter()
.map(|nd| {
let mut nn = nd.clone();
nn.id = remap(nd.id);
for l in &mut nn.inputs {
l.from = remap(l.from);
}
nn
})
.collect();
shuffled_nodes.sort_by_key(|nd| nd.id);
let shuffled = RedstoneGraph {
nodes: shuffled_nodes,
};
assert_eq!(
g.fingerprint(&spec),
shuffled.fingerprint(&spec),
"fingerprint must be independent of node Vec order"
);
}
#[test]
fn to_hex_format() {
let g = repeater_chain(1);
let hx = g.fingerprint(&GraphFingerprintSpec::structural()).to_hex();
assert_eq!(hx.len(), 32);
assert!(hx
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()));
}
#[test]
fn preset_names_resolve() {
for name in GraphFingerprintSpec::PRESETS {
assert!(GraphFingerprintSpec::from_preset(name).is_some(), "{name}");
}
assert!(GraphFingerprintSpec::from_preset("nope").is_none());
assert_eq!(
GraphFingerprintSpec::from_preset("structural"),
Some(GraphFingerprintSpec::structural())
);
}
}